summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.extra_dist656
-rw-r--r--.fmf/version1
-rw-r--r--AUTHORS5
-rw-r--r--COPYING502
-rw-r--r--Makefile.am192
-rw-r--r--Makefile.in9169
-rw-r--r--README.md32
-rw-r--r--aclocal.m41853
-rwxr-xr-xbuild.js230
-rw-r--r--config.h.in188
-rwxr-xr-xconfigure9081
-rw-r--r--configure.ac502
-rw-r--r--containers/Makefile.am11
-rw-r--r--containers/flatpak/Makefile.am27
-rw-r--r--dist/apps/apps.css.LEGAL.txt0
-rw-r--r--dist/apps/apps.css.gzbin0 -> 45222 bytes
-rw-r--r--dist/apps/apps.js.LEGAL.txt46
-rw-r--r--dist/apps/apps.js.gzbin0 -> 118067 bytes
-rw-r--r--dist/apps/default.pngbin0 -> 2538 bytes
-rw-r--r--dist/apps/index.html38
-rw-r--r--dist/apps/manifest.json23
-rw-r--r--dist/apps/po.cs.js.gzbin0 -> 5790 bytes
-rw-r--r--dist/apps/po.de.js.gzbin0 -> 5315 bytes
-rw-r--r--dist/apps/po.es.js.gzbin0 -> 5359 bytes
-rw-r--r--dist/apps/po.fi.js.gzbin0 -> 5377 bytes
-rw-r--r--dist/apps/po.fr.js.gzbin0 -> 5321 bytes
-rw-r--r--dist/apps/po.he.js.gzbin0 -> 5503 bytes
-rw-r--r--dist/apps/po.it.js.gzbin0 -> 4973 bytes
-rw-r--r--dist/apps/po.ja.js.gzbin0 -> 5981 bytes
-rw-r--r--dist/apps/po.ka.js.gzbin0 -> 6213 bytes
-rw-r--r--dist/apps/po.ko.js.gzbin0 -> 5717 bytes
-rw-r--r--dist/apps/po.manifest.cs.js.gzbin0 -> 355 bytes
-rw-r--r--dist/apps/po.manifest.de.js.gzbin0 -> 293 bytes
-rw-r--r--dist/apps/po.manifest.es.js.gzbin0 -> 302 bytes
-rw-r--r--dist/apps/po.manifest.fi.js.gzbin0 -> 311 bytes
-rw-r--r--dist/apps/po.manifest.fr.js.gzbin0 -> 283 bytes
-rw-r--r--dist/apps/po.manifest.he.js.gzbin0 -> 350 bytes
-rw-r--r--dist/apps/po.manifest.it.js.gzbin0 -> 289 bytes
-rw-r--r--dist/apps/po.manifest.ja.js.gzbin0 -> 354 bytes
-rw-r--r--dist/apps/po.manifest.ka.js.gzbin0 -> 368 bytes
-rw-r--r--dist/apps/po.manifest.ko.js.gzbin0 -> 353 bytes
-rw-r--r--dist/apps/po.manifest.nb_NO.js.gzbin0 -> 285 bytes
-rw-r--r--dist/apps/po.manifest.nl.js.gzbin0 -> 285 bytes
-rw-r--r--dist/apps/po.manifest.pl.js.gzbin0 -> 355 bytes
-rw-r--r--dist/apps/po.manifest.pt_BR.js.gzbin0 -> 283 bytes
-rw-r--r--dist/apps/po.manifest.ru.js.gzbin0 -> 450 bytes
-rw-r--r--dist/apps/po.manifest.sk.js.gzbin0 -> 336 bytes
-rw-r--r--dist/apps/po.manifest.sv.js.gzbin0 -> 288 bytes
-rw-r--r--dist/apps/po.manifest.tr.js.gzbin0 -> 305 bytes
-rw-r--r--dist/apps/po.manifest.uk.js.gzbin0 -> 425 bytes
-rw-r--r--dist/apps/po.manifest.zh_CN.js.gzbin0 -> 323 bytes
-rw-r--r--dist/apps/po.nb_NO.js.gzbin0 -> 4386 bytes
-rw-r--r--dist/apps/po.nl.js.gzbin0 -> 5128 bytes
-rw-r--r--dist/apps/po.pl.js.gzbin0 -> 5571 bytes
-rw-r--r--dist/apps/po.pt_BR.js.gzbin0 -> 4431 bytes
-rw-r--r--dist/apps/po.ru.js.gzbin0 -> 5995 bytes
-rw-r--r--dist/apps/po.sk.js.gzbin0 -> 4577 bytes
-rw-r--r--dist/apps/po.sv.js.gzbin0 -> 5263 bytes
-rw-r--r--dist/apps/po.tr.js.gzbin0 -> 5409 bytes
-rw-r--r--dist/apps/po.uk.js.gzbin0 -> 6593 bytes
-rw-r--r--dist/apps/po.zh_CN.js.gzbin0 -> 5462 bytes
-rw-r--r--dist/base1/cockpit.js.LEGAL.txt0
-rw-r--r--dist/base1/cockpit.js.gzbin0 -> 16777 bytes
-rw-r--r--dist/base1/manifest.json5
-rw-r--r--dist/base1/po.cs.js.gzbin0 -> 5446 bytes
-rw-r--r--dist/base1/po.de.js.gzbin0 -> 5034 bytes
-rw-r--r--dist/base1/po.es.js.gzbin0 -> 5027 bytes
-rw-r--r--dist/base1/po.fi.js.gzbin0 -> 5066 bytes
-rw-r--r--dist/base1/po.fr.js.gzbin0 -> 5028 bytes
-rw-r--r--dist/base1/po.he.js.gzbin0 -> 5199 bytes
-rw-r--r--dist/base1/po.it.js.gzbin0 -> 4688 bytes
-rw-r--r--dist/base1/po.ja.js.gzbin0 -> 5632 bytes
-rw-r--r--dist/base1/po.ka.js.gzbin0 -> 5859 bytes
-rw-r--r--dist/base1/po.ko.js.gzbin0 -> 5378 bytes
-rw-r--r--dist/base1/po.manifest.cs.js.gzbin0 -> 265 bytes
-rw-r--r--dist/base1/po.manifest.de.js.gzbin0 -> 207 bytes
-rw-r--r--dist/base1/po.manifest.es.js.gzbin0 -> 234 bytes
-rw-r--r--dist/base1/po.manifest.fi.js.gzbin0 -> 224 bytes
-rw-r--r--dist/base1/po.manifest.fr.js.gzbin0 -> 213 bytes
-rw-r--r--dist/base1/po.manifest.he.js.gzbin0 -> 274 bytes
-rw-r--r--dist/base1/po.manifest.it.js.gzbin0 -> 214 bytes
-rw-r--r--dist/base1/po.manifest.ja.js.gzbin0 -> 257 bytes
-rw-r--r--dist/base1/po.manifest.ka.js.gzbin0 -> 276 bytes
-rw-r--r--dist/base1/po.manifest.ko.js.gzbin0 -> 250 bytes
-rw-r--r--dist/base1/po.manifest.nb_NO.js.gzbin0 -> 215 bytes
-rw-r--r--dist/base1/po.manifest.nl.js.gzbin0 -> 207 bytes
-rw-r--r--dist/base1/po.manifest.pl.js.gzbin0 -> 273 bytes
-rw-r--r--dist/base1/po.manifest.pt_BR.js.gzbin0 -> 230 bytes
-rw-r--r--dist/base1/po.manifest.ru.js.gzbin0 -> 317 bytes
-rw-r--r--dist/base1/po.manifest.sk.js.gzbin0 -> 245 bytes
-rw-r--r--dist/base1/po.manifest.sv.js.gzbin0 -> 217 bytes
-rw-r--r--dist/base1/po.manifest.tr.js.gzbin0 -> 234 bytes
-rw-r--r--dist/base1/po.manifest.uk.js.gzbin0 -> 331 bytes
-rw-r--r--dist/base1/po.manifest.zh_CN.js.gzbin0 -> 236 bytes
-rw-r--r--dist/base1/po.nb_NO.js.gzbin0 -> 4106 bytes
-rw-r--r--dist/base1/po.nl.js.gzbin0 -> 4829 bytes
-rw-r--r--dist/base1/po.pl.js.gzbin0 -> 5237 bytes
-rw-r--r--dist/base1/po.pt_BR.js.gzbin0 -> 4149 bytes
-rw-r--r--dist/base1/po.ru.js.gzbin0 -> 5652 bytes
-rw-r--r--dist/base1/po.sk.js.gzbin0 -> 4251 bytes
-rw-r--r--dist/base1/po.sv.js.gzbin0 -> 4936 bytes
-rw-r--r--dist/base1/po.tr.js.gzbin0 -> 5100 bytes
-rw-r--r--dist/base1/po.uk.js.gzbin0 -> 6266 bytes
-rw-r--r--dist/base1/po.zh_CN.js.gzbin0 -> 5155 bytes
-rw-r--r--dist/kdump/index.html37
-rw-r--r--dist/kdump/kdump.css.LEGAL.txt0
-rw-r--r--dist/kdump/kdump.css.gzbin0 -> 64466 bytes
-rw-r--r--dist/kdump/kdump.js.LEGAL.txt46
-rw-r--r--dist/kdump/kdump.js.gzbin0 -> 150726 bytes
-rw-r--r--dist/kdump/manifest.json22
-rw-r--r--dist/kdump/po.cs.js.gzbin0 -> 6640 bytes
-rw-r--r--dist/kdump/po.de.js.gzbin0 -> 6192 bytes
-rw-r--r--dist/kdump/po.es.js.gzbin0 -> 6363 bytes
-rw-r--r--dist/kdump/po.fi.js.gzbin0 -> 6018 bytes
-rw-r--r--dist/kdump/po.fr.js.gzbin0 -> 5910 bytes
-rw-r--r--dist/kdump/po.he.js.gzbin0 -> 6086 bytes
-rw-r--r--dist/kdump/po.it.js.gzbin0 -> 5483 bytes
-rw-r--r--dist/kdump/po.ja.js.gzbin0 -> 7079 bytes
-rw-r--r--dist/kdump/po.ka.js.gzbin0 -> 7413 bytes
-rw-r--r--dist/kdump/po.ko.js.gzbin0 -> 6779 bytes
-rw-r--r--dist/kdump/po.manifest.cs.js.gzbin0 -> 305 bytes
-rw-r--r--dist/kdump/po.manifest.de.js.gzbin0 -> 246 bytes
-rw-r--r--dist/kdump/po.manifest.es.js.gzbin0 -> 277 bytes
-rw-r--r--dist/kdump/po.manifest.fi.js.gzbin0 -> 273 bytes
-rw-r--r--dist/kdump/po.manifest.fr.js.gzbin0 -> 266 bytes
-rw-r--r--dist/kdump/po.manifest.he.js.gzbin0 -> 318 bytes
-rw-r--r--dist/kdump/po.manifest.it.js.gzbin0 -> 248 bytes
-rw-r--r--dist/kdump/po.manifest.ja.js.gzbin0 -> 305 bytes
-rw-r--r--dist/kdump/po.manifest.ka.js.gzbin0 -> 319 bytes
-rw-r--r--dist/kdump/po.manifest.ko.js.gzbin0 -> 290 bytes
-rw-r--r--dist/kdump/po.manifest.nb_NO.js.gzbin0 -> 249 bytes
-rw-r--r--dist/kdump/po.manifest.nl.js.gzbin0 -> 245 bytes
-rw-r--r--dist/kdump/po.manifest.pl.js.gzbin0 -> 311 bytes
-rw-r--r--dist/kdump/po.manifest.pt_BR.js.gzbin0 -> 242 bytes
-rw-r--r--dist/kdump/po.manifest.ru.js.gzbin0 -> 366 bytes
-rw-r--r--dist/kdump/po.manifest.sk.js.gzbin0 -> 282 bytes
-rw-r--r--dist/kdump/po.manifest.sv.js.gzbin0 -> 250 bytes
-rw-r--r--dist/kdump/po.manifest.tr.js.gzbin0 -> 275 bytes
-rw-r--r--dist/kdump/po.manifest.uk.js.gzbin0 -> 386 bytes
-rw-r--r--dist/kdump/po.manifest.zh_CN.js.gzbin0 -> 282 bytes
-rw-r--r--dist/kdump/po.nb_NO.js.gzbin0 -> 4814 bytes
-rw-r--r--dist/kdump/po.nl.js.gzbin0 -> 5941 bytes
-rw-r--r--dist/kdump/po.pl.js.gzbin0 -> 6393 bytes
-rw-r--r--dist/kdump/po.pt_BR.js.gzbin0 -> 4938 bytes
-rw-r--r--dist/kdump/po.ru.js.gzbin0 -> 6637 bytes
-rw-r--r--dist/kdump/po.sk.js.gzbin0 -> 5098 bytes
-rw-r--r--dist/kdump/po.sv.js.gzbin0 -> 6225 bytes
-rw-r--r--dist/kdump/po.tr.js.gzbin0 -> 6460 bytes
-rw-r--r--dist/kdump/po.uk.js.gzbin0 -> 7610 bytes
-rw-r--r--dist/kdump/po.zh_CN.js.gzbin0 -> 6552 bytes
-rw-r--r--dist/metrics/index.css.LEGAL.txt0
-rw-r--r--dist/metrics/index.css.gzbin0 -> 147061 bytes
-rw-r--r--dist/metrics/index.html37
-rw-r--r--dist/metrics/index.js.LEGAL.txt46
-rw-r--r--dist/metrics/index.js.gzbin0 -> 174833 bytes
-rw-r--r--dist/metrics/manifest.json11
-rw-r--r--dist/metrics/po.cs.js.gzbin0 -> 6822 bytes
-rw-r--r--dist/metrics/po.de.js.gzbin0 -> 6262 bytes
-rw-r--r--dist/metrics/po.es.js.gzbin0 -> 6221 bytes
-rw-r--r--dist/metrics/po.fi.js.gzbin0 -> 6326 bytes
-rw-r--r--dist/metrics/po.fr.js.gzbin0 -> 6214 bytes
-rw-r--r--dist/metrics/po.he.js.gzbin0 -> 6469 bytes
-rw-r--r--dist/metrics/po.it.js.gzbin0 -> 5698 bytes
-rw-r--r--dist/metrics/po.ja.js.gzbin0 -> 6952 bytes
-rw-r--r--dist/metrics/po.ka.js.gzbin0 -> 7170 bytes
-rw-r--r--dist/metrics/po.ko.js.gzbin0 -> 6660 bytes
-rw-r--r--dist/metrics/po.manifest.cs.js.gzbin0 -> 294 bytes
-rw-r--r--dist/metrics/po.manifest.de.js.gzbin0 -> 241 bytes
-rw-r--r--dist/metrics/po.manifest.es.js.gzbin0 -> 262 bytes
-rw-r--r--dist/metrics/po.manifest.fi.js.gzbin0 -> 257 bytes
-rw-r--r--dist/metrics/po.manifest.fr.js.gzbin0 -> 245 bytes
-rw-r--r--dist/metrics/po.manifest.he.js.gzbin0 -> 305 bytes
-rw-r--r--dist/metrics/po.manifest.it.js.gzbin0 -> 239 bytes
-rw-r--r--dist/metrics/po.manifest.ja.js.gzbin0 -> 283 bytes
-rw-r--r--dist/metrics/po.manifest.ka.js.gzbin0 -> 314 bytes
-rw-r--r--dist/metrics/po.manifest.ko.js.gzbin0 -> 282 bytes
-rw-r--r--dist/metrics/po.manifest.nb_NO.js.gzbin0 -> 238 bytes
-rw-r--r--dist/metrics/po.manifest.nl.js.gzbin0 -> 237 bytes
-rw-r--r--dist/metrics/po.manifest.pl.js.gzbin0 -> 302 bytes
-rw-r--r--dist/metrics/po.manifest.pt_BR.js.gzbin0 -> 262 bytes
-rw-r--r--dist/metrics/po.manifest.ru.js.gzbin0 -> 360 bytes
-rw-r--r--dist/metrics/po.manifest.sk.js.gzbin0 -> 277 bytes
-rw-r--r--dist/metrics/po.manifest.sv.js.gzbin0 -> 247 bytes
-rw-r--r--dist/metrics/po.manifest.tr.js.gzbin0 -> 265 bytes
-rw-r--r--dist/metrics/po.manifest.uk.js.gzbin0 -> 367 bytes
-rw-r--r--dist/metrics/po.manifest.zh_CN.js.gzbin0 -> 269 bytes
-rw-r--r--dist/metrics/po.nb_NO.js.gzbin0 -> 4835 bytes
-rw-r--r--dist/metrics/po.nl.js.gzbin0 -> 5999 bytes
-rw-r--r--dist/metrics/po.pl.js.gzbin0 -> 6531 bytes
-rw-r--r--dist/metrics/po.pt_BR.js.gzbin0 -> 4827 bytes
-rw-r--r--dist/metrics/po.ru.js.gzbin0 -> 6963 bytes
-rw-r--r--dist/metrics/po.sk.js.gzbin0 -> 5083 bytes
-rw-r--r--dist/metrics/po.sv.js.gzbin0 -> 6113 bytes
-rw-r--r--dist/metrics/po.tr.js.gzbin0 -> 6335 bytes
-rw-r--r--dist/metrics/po.uk.js.gzbin0 -> 7650 bytes
-rw-r--r--dist/metrics/po.zh_CN.js.gzbin0 -> 6359 bytes
-rw-r--r--dist/networkmanager/firewall.css.LEGAL.txt0
-rw-r--r--dist/networkmanager/firewall.css.gzbin0 -> 134596 bytes
-rw-r--r--dist/networkmanager/firewall.html36
-rw-r--r--dist/networkmanager/firewall.js.LEGAL.txt46
-rw-r--r--dist/networkmanager/firewall.js.gzbin0 -> 137792 bytes
-rw-r--r--dist/networkmanager/index.html39
-rw-r--r--dist/networkmanager/manifest.json43
-rw-r--r--dist/networkmanager/networkmanager.css.LEGAL.txt0
-rw-r--r--dist/networkmanager/networkmanager.css.gzbin0 -> 148001 bytes
-rw-r--r--dist/networkmanager/networkmanager.js.LEGAL.txt46
-rw-r--r--dist/networkmanager/networkmanager.js.gzbin0 -> 196363 bytes
-rw-r--r--dist/networkmanager/po.cs.js.gzbin0 -> 10236 bytes
-rw-r--r--dist/networkmanager/po.de.js.gzbin0 -> 9420 bytes
-rw-r--r--dist/networkmanager/po.es.js.gzbin0 -> 9622 bytes
-rw-r--r--dist/networkmanager/po.fi.js.gzbin0 -> 9282 bytes
-rw-r--r--dist/networkmanager/po.fr.js.gzbin0 -> 9075 bytes
-rw-r--r--dist/networkmanager/po.he.js.gzbin0 -> 9683 bytes
-rw-r--r--dist/networkmanager/po.it.js.gzbin0 -> 8579 bytes
-rw-r--r--dist/networkmanager/po.ja.js.gzbin0 -> 10293 bytes
-rw-r--r--dist/networkmanager/po.ka.js.gzbin0 -> 11042 bytes
-rw-r--r--dist/networkmanager/po.ko.js.gzbin0 -> 10103 bytes
-rw-r--r--dist/networkmanager/po.manifest.cs.js.gzbin0 -> 509 bytes
-rw-r--r--dist/networkmanager/po.manifest.de.js.gzbin0 -> 430 bytes
-rw-r--r--dist/networkmanager/po.manifest.es.js.gzbin0 -> 450 bytes
-rw-r--r--dist/networkmanager/po.manifest.fi.js.gzbin0 -> 448 bytes
-rw-r--r--dist/networkmanager/po.manifest.fr.js.gzbin0 -> 446 bytes
-rw-r--r--dist/networkmanager/po.manifest.he.js.gzbin0 -> 511 bytes
-rw-r--r--dist/networkmanager/po.manifest.it.js.gzbin0 -> 410 bytes
-rw-r--r--dist/networkmanager/po.manifest.ja.js.gzbin0 -> 519 bytes
-rw-r--r--dist/networkmanager/po.manifest.ka.js.gzbin0 -> 543 bytes
-rw-r--r--dist/networkmanager/po.manifest.ko.js.gzbin0 -> 484 bytes
-rw-r--r--dist/networkmanager/po.manifest.nb_NO.js.gzbin0 -> 419 bytes
-rw-r--r--dist/networkmanager/po.manifest.nl.js.gzbin0 -> 403 bytes
-rw-r--r--dist/networkmanager/po.manifest.pl.js.gzbin0 -> 517 bytes
-rw-r--r--dist/networkmanager/po.manifest.pt_BR.js.gzbin0 -> 316 bytes
-rw-r--r--dist/networkmanager/po.manifest.ru.js.gzbin0 -> 614 bytes
-rw-r--r--dist/networkmanager/po.manifest.sk.js.gzbin0 -> 397 bytes
-rw-r--r--dist/networkmanager/po.manifest.sv.js.gzbin0 -> 428 bytes
-rw-r--r--dist/networkmanager/po.manifest.tr.js.gzbin0 -> 478 bytes
-rw-r--r--dist/networkmanager/po.manifest.uk.js.gzbin0 -> 605 bytes
-rw-r--r--dist/networkmanager/po.manifest.zh_CN.js.gzbin0 -> 458 bytes
-rw-r--r--dist/networkmanager/po.nb_NO.js.gzbin0 -> 7568 bytes
-rw-r--r--dist/networkmanager/po.nl.js.gzbin0 -> 9319 bytes
-rw-r--r--dist/networkmanager/po.pl.js.gzbin0 -> 9889 bytes
-rw-r--r--dist/networkmanager/po.pt_BR.js.gzbin0 -> 7565 bytes
-rw-r--r--dist/networkmanager/po.ru.js.gzbin0 -> 10250 bytes
-rw-r--r--dist/networkmanager/po.sk.js.gzbin0 -> 7552 bytes
-rw-r--r--dist/networkmanager/po.sv.js.gzbin0 -> 9437 bytes
-rw-r--r--dist/networkmanager/po.tr.js.gzbin0 -> 9701 bytes
-rw-r--r--dist/networkmanager/po.uk.js.gzbin0 -> 11457 bytes
-rw-r--r--dist/networkmanager/po.zh_CN.js.gzbin0 -> 9753 bytes
-rw-r--r--dist/packagekit/index.html37
-rw-r--r--dist/packagekit/manifest.json29
-rw-r--r--dist/packagekit/po.cs.js.gzbin0 -> 8033 bytes
-rw-r--r--dist/packagekit/po.de.js.gzbin0 -> 7464 bytes
-rw-r--r--dist/packagekit/po.es.js.gzbin0 -> 7451 bytes
-rw-r--r--dist/packagekit/po.fi.js.gzbin0 -> 7569 bytes
-rw-r--r--dist/packagekit/po.fr.js.gzbin0 -> 7475 bytes
-rw-r--r--dist/packagekit/po.he.js.gzbin0 -> 7680 bytes
-rw-r--r--dist/packagekit/po.it.js.gzbin0 -> 6844 bytes
-rw-r--r--dist/packagekit/po.ja.js.gzbin0 -> 8106 bytes
-rw-r--r--dist/packagekit/po.ka.js.gzbin0 -> 8433 bytes
-rw-r--r--dist/packagekit/po.ko.js.gzbin0 -> 7760 bytes
-rw-r--r--dist/packagekit/po.manifest.cs.js.gzbin0 -> 378 bytes
-rw-r--r--dist/packagekit/po.manifest.de.js.gzbin0 -> 312 bytes
-rw-r--r--dist/packagekit/po.manifest.es.js.gzbin0 -> 336 bytes
-rw-r--r--dist/packagekit/po.manifest.fi.js.gzbin0 -> 336 bytes
-rw-r--r--dist/packagekit/po.manifest.fr.js.gzbin0 -> 327 bytes
-rw-r--r--dist/packagekit/po.manifest.he.js.gzbin0 -> 383 bytes
-rw-r--r--dist/packagekit/po.manifest.it.js.gzbin0 -> 315 bytes
-rw-r--r--dist/packagekit/po.manifest.ja.js.gzbin0 -> 388 bytes
-rw-r--r--dist/packagekit/po.manifest.ka.js.gzbin0 -> 424 bytes
-rw-r--r--dist/packagekit/po.manifest.ko.js.gzbin0 -> 366 bytes
-rw-r--r--dist/packagekit/po.manifest.nb_NO.js.gzbin0 -> 320 bytes
-rw-r--r--dist/packagekit/po.manifest.nl.js.gzbin0 -> 305 bytes
-rw-r--r--dist/packagekit/po.manifest.pl.js.gzbin0 -> 403 bytes
-rw-r--r--dist/packagekit/po.manifest.pt_BR.js.gzbin0 -> 291 bytes
-rw-r--r--dist/packagekit/po.manifest.ru.js.gzbin0 -> 468 bytes
-rw-r--r--dist/packagekit/po.manifest.sk.js.gzbin0 -> 367 bytes
-rw-r--r--dist/packagekit/po.manifest.sv.js.gzbin0 -> 320 bytes
-rw-r--r--dist/packagekit/po.manifest.tr.js.gzbin0 -> 342 bytes
-rw-r--r--dist/packagekit/po.manifest.uk.js.gzbin0 -> 466 bytes
-rw-r--r--dist/packagekit/po.manifest.zh_CN.js.gzbin0 -> 348 bytes
-rw-r--r--dist/packagekit/po.nb_NO.js.gzbin0 -> 5981 bytes
-rw-r--r--dist/packagekit/po.nl.js.gzbin0 -> 7185 bytes
-rw-r--r--dist/packagekit/po.pl.js.gzbin0 -> 7770 bytes
-rw-r--r--dist/packagekit/po.pt_BR.js.gzbin0 -> 6082 bytes
-rw-r--r--dist/packagekit/po.ru.js.gzbin0 -> 8375 bytes
-rw-r--r--dist/packagekit/po.sk.js.gzbin0 -> 5823 bytes
-rw-r--r--dist/packagekit/po.sv.js.gzbin0 -> 7307 bytes
-rw-r--r--dist/packagekit/po.tr.js.gzbin0 -> 7507 bytes
-rw-r--r--dist/packagekit/po.uk.js.gzbin0 -> 9015 bytes
-rw-r--r--dist/packagekit/po.zh_CN.js.gzbin0 -> 7508 bytes
-rw-r--r--dist/packagekit/updates.css.LEGAL.txt0
-rw-r--r--dist/packagekit/updates.css.gzbin0 -> 139914 bytes
-rw-r--r--dist/packagekit/updates.js.LEGAL.txt46
-rw-r--r--dist/packagekit/updates.js.gzbin0 -> 341373 bytes
-rw-r--r--dist/playground/exception.css.LEGAL.txt0
-rw-r--r--dist/playground/exception.css.gzbin0 -> 5537 bytes
-rw-r--r--dist/playground/exception.html23
-rw-r--r--dist/playground/exception.js.LEGAL.txt0
-rw-r--r--dist/playground/exception.js.gzbin0 -> 480 bytes
-rw-r--r--dist/playground/hammer.gifbin0 -> 74176 bytes
-rw-r--r--dist/playground/index.css.LEGAL.txt0
-rw-r--r--dist/playground/index.css.gzbin0 -> 16613 bytes
-rw-r--r--dist/playground/index.html29
-rw-r--r--dist/playground/index.js.LEGAL.txt0
-rw-r--r--dist/playground/index.js.gzbin0 -> 16195 bytes
-rw-r--r--dist/playground/journal.css.LEGAL.txt0
-rw-r--r--dist/playground/journal.css.gzbin0 -> 20604 bytes
-rw-r--r--dist/playground/journal.html21
-rw-r--r--dist/playground/journal.js.LEGAL.txt34
-rw-r--r--dist/playground/journal.js.gzbin0 -> 54214 bytes
-rw-r--r--dist/playground/manifest.json48
-rw-r--r--dist/playground/metrics.css.LEGAL.txt0
-rw-r--r--dist/playground/metrics.css.gzbin0 -> 5537 bytes
-rw-r--r--dist/playground/metrics.html22
-rw-r--r--dist/playground/metrics.js.LEGAL.txt0
-rw-r--r--dist/playground/metrics.js.gzbin0 -> 552 bytes
-rw-r--r--dist/playground/notifications-receiver.html18
-rw-r--r--dist/playground/notifications-receiver.js.LEGAL.txt0
-rw-r--r--dist/playground/notifications-receiver.js.gzbin0 -> 15652 bytes
-rw-r--r--dist/playground/pkgs.css.LEGAL.txt0
-rw-r--r--dist/playground/pkgs.css.gzbin0 -> 5537 bytes
-rw-r--r--dist/playground/pkgs.html19
-rw-r--r--dist/playground/pkgs.js.LEGAL.txt0
-rw-r--r--dist/playground/pkgs.js.gzbin0 -> 643 bytes
-rw-r--r--dist/playground/plot.css.LEGAL.txt0
-rw-r--r--dist/playground/plot.css.gzbin0 -> 11848 bytes
-rw-r--r--dist/playground/plot.html15
-rw-r--r--dist/playground/plot.js.LEGAL.txt34
-rw-r--r--dist/playground/plot.js.gzbin0 -> 64658 bytes
-rw-r--r--dist/playground/po.cs.js.gzbin0 -> 5569 bytes
-rw-r--r--dist/playground/po.de.js.gzbin0 -> 5173 bytes
-rw-r--r--dist/playground/po.es.js.gzbin0 -> 5153 bytes
-rw-r--r--dist/playground/po.fi.js.gzbin0 -> 5191 bytes
-rw-r--r--dist/playground/po.fr.js.gzbin0 -> 5157 bytes
-rw-r--r--dist/playground/po.he.js.gzbin0 -> 5340 bytes
-rw-r--r--dist/playground/po.it.js.gzbin0 -> 4810 bytes
-rw-r--r--dist/playground/po.ja.js.gzbin0 -> 5756 bytes
-rw-r--r--dist/playground/po.ka.js.gzbin0 -> 5984 bytes
-rw-r--r--dist/playground/po.ko.js.gzbin0 -> 5493 bytes
-rw-r--r--dist/playground/po.manifest.cs.js.gzbin0 -> 279 bytes
-rw-r--r--dist/playground/po.manifest.de.js.gzbin0 -> 229 bytes
-rw-r--r--dist/playground/po.manifest.es.js.gzbin0 -> 248 bytes
-rw-r--r--dist/playground/po.manifest.fi.js.gzbin0 -> 238 bytes
-rw-r--r--dist/playground/po.manifest.fr.js.gzbin0 -> 232 bytes
-rw-r--r--dist/playground/po.manifest.he.js.gzbin0 -> 291 bytes
-rw-r--r--dist/playground/po.manifest.it.js.gzbin0 -> 230 bytes
-rw-r--r--dist/playground/po.manifest.ja.js.gzbin0 -> 273 bytes
-rw-r--r--dist/playground/po.manifest.ka.js.gzbin0 -> 297 bytes
-rw-r--r--dist/playground/po.manifest.ko.js.gzbin0 -> 267 bytes
-rw-r--r--dist/playground/po.manifest.nb_NO.js.gzbin0 -> 231 bytes
-rw-r--r--dist/playground/po.manifest.nl.js.gzbin0 -> 230 bytes
-rw-r--r--dist/playground/po.manifest.pl.js.gzbin0 -> 291 bytes
-rw-r--r--dist/playground/po.manifest.pt_BR.js.gzbin0 -> 245 bytes
-rw-r--r--dist/playground/po.manifest.ru.js.gzbin0 -> 341 bytes
-rw-r--r--dist/playground/po.manifest.sk.js.gzbin0 -> 261 bytes
-rw-r--r--dist/playground/po.manifest.sv.js.gzbin0 -> 235 bytes
-rw-r--r--dist/playground/po.manifest.tr.js.gzbin0 -> 253 bytes
-rw-r--r--dist/playground/po.manifest.uk.js.gzbin0 -> 350 bytes
-rw-r--r--dist/playground/po.manifest.zh_CN.js.gzbin0 -> 257 bytes
-rw-r--r--dist/playground/po.nb_NO.js.gzbin0 -> 4219 bytes
-rw-r--r--dist/playground/po.nl.js.gzbin0 -> 4951 bytes
-rw-r--r--dist/playground/po.pl.js.gzbin0 -> 5357 bytes
-rw-r--r--dist/playground/po.pt_BR.js.gzbin0 -> 4274 bytes
-rw-r--r--dist/playground/po.ru.js.gzbin0 -> 5791 bytes
-rw-r--r--dist/playground/po.sk.js.gzbin0 -> 4396 bytes
-rw-r--r--dist/playground/po.sv.js.gzbin0 -> 5056 bytes
-rw-r--r--dist/playground/po.tr.js.gzbin0 -> 5218 bytes
-rw-r--r--dist/playground/po.uk.js.gzbin0 -> 6413 bytes
-rw-r--r--dist/playground/po.zh_CN.js.gzbin0 -> 5274 bytes
-rw-r--r--dist/playground/preloaded.html20
-rw-r--r--dist/playground/preloaded.js.LEGAL.txt0
-rw-r--r--dist/playground/preloaded.js.gzbin0 -> 586 bytes
-rw-r--r--dist/playground/react-patterns.css.LEGAL.txt0
-rw-r--r--dist/playground/react-patterns.css.gzbin0 -> 33963 bytes
-rw-r--r--dist/playground/react-patterns.html35
-rw-r--r--dist/playground/react-patterns.js.LEGAL.txt46
-rw-r--r--dist/playground/react-patterns.js.gzbin0 -> 96994 bytes
-rw-r--r--dist/playground/service.css.LEGAL.txt0
-rw-r--r--dist/playground/service.css.gzbin0 -> 5537 bytes
-rw-r--r--dist/playground/service.html24
-rw-r--r--dist/playground/service.js.LEGAL.txt0
-rw-r--r--dist/playground/service.js.gzbin0 -> 1863 bytes
-rw-r--r--dist/playground/speed.css.LEGAL.txt0
-rw-r--r--dist/playground/speed.css.gzbin0 -> 12943 bytes
-rw-r--r--dist/playground/speed.html128
-rw-r--r--dist/playground/speed.js.LEGAL.txt0
-rw-r--r--dist/playground/speed.js.gzbin0 -> 1879 bytes
-rw-r--r--dist/playground/test.css.LEGAL.txt0
-rw-r--r--dist/playground/test.css.gzbin0 -> 12901 bytes
-rw-r--r--dist/playground/test.html48
-rw-r--r--dist/playground/test.js.LEGAL.txt0
-rw-r--r--dist/playground/test.js.gzbin0 -> 1300 bytes
-rw-r--r--dist/playground/translate.css.LEGAL.txt0
-rw-r--r--dist/playground/translate.css.gzbin0 -> 5537 bytes
-rw-r--r--dist/playground/translate.html138
-rw-r--r--dist/playground/translate.js.LEGAL.txt0
-rw-r--r--dist/playground/translate.js.gzbin0 -> 626 bytes
-rw-r--r--dist/selinux/index.html38
-rw-r--r--dist/selinux/manifest.json15
-rw-r--r--dist/selinux/po.cs.js.gzbin0 -> 6294 bytes
-rw-r--r--dist/selinux/po.de.js.gzbin0 -> 5862 bytes
-rw-r--r--dist/selinux/po.es.js.gzbin0 -> 5839 bytes
-rw-r--r--dist/selinux/po.fi.js.gzbin0 -> 5874 bytes
-rw-r--r--dist/selinux/po.fr.js.gzbin0 -> 5855 bytes
-rw-r--r--dist/selinux/po.he.js.gzbin0 -> 6027 bytes
-rw-r--r--dist/selinux/po.it.js.gzbin0 -> 5428 bytes
-rw-r--r--dist/selinux/po.ja.js.gzbin0 -> 6475 bytes
-rw-r--r--dist/selinux/po.ka.js.gzbin0 -> 6721 bytes
-rw-r--r--dist/selinux/po.ko.js.gzbin0 -> 6207 bytes
-rw-r--r--dist/selinux/po.manifest.cs.js.gzbin0 -> 296 bytes
-rw-r--r--dist/selinux/po.manifest.de.js.gzbin0 -> 239 bytes
-rw-r--r--dist/selinux/po.manifest.es.js.gzbin0 -> 262 bytes
-rw-r--r--dist/selinux/po.manifest.fi.js.gzbin0 -> 253 bytes
-rw-r--r--dist/selinux/po.manifest.fr.js.gzbin0 -> 246 bytes
-rw-r--r--dist/selinux/po.manifest.he.js.gzbin0 -> 305 bytes
-rw-r--r--dist/selinux/po.manifest.it.js.gzbin0 -> 242 bytes
-rw-r--r--dist/selinux/po.manifest.ja.js.gzbin0 -> 286 bytes
-rw-r--r--dist/selinux/po.manifest.ka.js.gzbin0 -> 306 bytes
-rw-r--r--dist/selinux/po.manifest.ko.js.gzbin0 -> 280 bytes
-rw-r--r--dist/selinux/po.manifest.nb_NO.js.gzbin0 -> 245 bytes
-rw-r--r--dist/selinux/po.manifest.nl.js.gzbin0 -> 240 bytes
-rw-r--r--dist/selinux/po.manifest.pl.js.gzbin0 -> 309 bytes
-rw-r--r--dist/selinux/po.manifest.pt_BR.js.gzbin0 -> 246 bytes
-rw-r--r--dist/selinux/po.manifest.ru.js.gzbin0 -> 351 bytes
-rw-r--r--dist/selinux/po.manifest.sk.js.gzbin0 -> 274 bytes
-rw-r--r--dist/selinux/po.manifest.sv.js.gzbin0 -> 247 bytes
-rw-r--r--dist/selinux/po.manifest.tr.js.gzbin0 -> 264 bytes
-rw-r--r--dist/selinux/po.manifest.uk.js.gzbin0 -> 363 bytes
-rw-r--r--dist/selinux/po.manifest.zh_CN.js.gzbin0 -> 273 bytes
-rw-r--r--dist/selinux/po.nb_NO.js.gzbin0 -> 4799 bytes
-rw-r--r--dist/selinux/po.nl.js.gzbin0 -> 5619 bytes
-rw-r--r--dist/selinux/po.pl.js.gzbin0 -> 6075 bytes
-rw-r--r--dist/selinux/po.pt_BR.js.gzbin0 -> 4880 bytes
-rw-r--r--dist/selinux/po.ru.js.gzbin0 -> 6550 bytes
-rw-r--r--dist/selinux/po.sk.js.gzbin0 -> 4962 bytes
-rw-r--r--dist/selinux/po.sv.js.gzbin0 -> 5721 bytes
-rw-r--r--dist/selinux/po.tr.js.gzbin0 -> 5901 bytes
-rw-r--r--dist/selinux/po.uk.js.gzbin0 -> 7184 bytes
-rw-r--r--dist/selinux/po.zh_CN.js.gzbin0 -> 5957 bytes
-rw-r--r--dist/selinux/selinux.css.LEGAL.txt0
-rw-r--r--dist/selinux/selinux.css.gzbin0 -> 134370 bytes
-rw-r--r--dist/selinux/selinux.js.LEGAL.txt46
-rw-r--r--dist/selinux/selinux.js.gzbin0 -> 129431 bytes
-rw-r--r--dist/shell/images/bg-plain.jpgbin0 -> 81737 bytes
-rw-r--r--dist/shell/images/cockpit-icon.svg1
-rw-r--r--dist/shell/images/server-error.pngbin0 -> 720 bytes
-rw-r--r--dist/shell/images/server-large.pngbin0 -> 1296 bytes
-rw-r--r--dist/shell/images/server-small.pngbin0 -> 508 bytes
-rw-r--r--dist/shell/index.html57
-rw-r--r--dist/shell/manifest.json58
-rw-r--r--dist/shell/po.cs.js.gzbin0 -> 9717 bytes
-rw-r--r--dist/shell/po.de.js.gzbin0 -> 9007 bytes
-rw-r--r--dist/shell/po.es.js.gzbin0 -> 9225 bytes
-rw-r--r--dist/shell/po.fi.js.gzbin0 -> 8979 bytes
-rw-r--r--dist/shell/po.fr.js.gzbin0 -> 8874 bytes
-rw-r--r--dist/shell/po.he.js.gzbin0 -> 9303 bytes
-rw-r--r--dist/shell/po.it.js.gzbin0 -> 7961 bytes
-rw-r--r--dist/shell/po.ja.js.gzbin0 -> 9915 bytes
-rw-r--r--dist/shell/po.ka.js.gzbin0 -> 10349 bytes
-rw-r--r--dist/shell/po.ko.js.gzbin0 -> 9567 bytes
-rw-r--r--dist/shell/po.manifest.cs.js.gzbin0 -> 287 bytes
-rw-r--r--dist/shell/po.manifest.de.js.gzbin0 -> 226 bytes
-rw-r--r--dist/shell/po.manifest.es.js.gzbin0 -> 248 bytes
-rw-r--r--dist/shell/po.manifest.fi.js.gzbin0 -> 239 bytes
-rw-r--r--dist/shell/po.manifest.fr.js.gzbin0 -> 232 bytes
-rw-r--r--dist/shell/po.manifest.he.js.gzbin0 -> 296 bytes
-rw-r--r--dist/shell/po.manifest.it.js.gzbin0 -> 228 bytes
-rw-r--r--dist/shell/po.manifest.ja.js.gzbin0 -> 278 bytes
-rw-r--r--dist/shell/po.manifest.ka.js.gzbin0 -> 299 bytes
-rw-r--r--dist/shell/po.manifest.ko.js.gzbin0 -> 273 bytes
-rw-r--r--dist/shell/po.manifest.nb_NO.js.gzbin0 -> 231 bytes
-rw-r--r--dist/shell/po.manifest.nl.js.gzbin0 -> 227 bytes
-rw-r--r--dist/shell/po.manifest.pl.js.gzbin0 -> 295 bytes
-rw-r--r--dist/shell/po.manifest.pt_BR.js.gzbin0 -> 245 bytes
-rw-r--r--dist/shell/po.manifest.ru.js.gzbin0 -> 346 bytes
-rw-r--r--dist/shell/po.manifest.sk.js.gzbin0 -> 266 bytes
-rw-r--r--dist/shell/po.manifest.sv.js.gzbin0 -> 235 bytes
-rw-r--r--dist/shell/po.manifest.tr.js.gzbin0 -> 251 bytes
-rw-r--r--dist/shell/po.manifest.uk.js.gzbin0 -> 358 bytes
-rw-r--r--dist/shell/po.manifest.zh_CN.js.gzbin0 -> 262 bytes
-rw-r--r--dist/shell/po.nb_NO.js.gzbin0 -> 6829 bytes
-rw-r--r--dist/shell/po.nl.js.gzbin0 -> 8899 bytes
-rw-r--r--dist/shell/po.pl.js.gzbin0 -> 9472 bytes
-rw-r--r--dist/shell/po.pt_BR.js.gzbin0 -> 6923 bytes
-rw-r--r--dist/shell/po.ru.js.gzbin0 -> 10078 bytes
-rw-r--r--dist/shell/po.sk.js.gzbin0 -> 7116 bytes
-rw-r--r--dist/shell/po.sv.js.gzbin0 -> 8958 bytes
-rw-r--r--dist/shell/po.tr.js.gzbin0 -> 9248 bytes
-rw-r--r--dist/shell/po.uk.js.gzbin0 -> 10966 bytes
-rw-r--r--dist/shell/po.zh_CN.js.gzbin0 -> 9242 bytes
-rw-r--r--dist/shell/shell.css.LEGAL.txt0
-rw-r--r--dist/shell/shell.css.gzbin0 -> 139612 bytes
-rw-r--r--dist/shell/shell.html15
-rw-r--r--dist/shell/shell.js.LEGAL.txt46
-rw-r--r--dist/shell/shell.js.gzbin0 -> 202774 bytes
-rw-r--r--dist/sosreport/index.html33
-rw-r--r--dist/sosreport/manifest.json12
-rw-r--r--dist/sosreport/po.cs.js.gzbin0 -> 6088 bytes
-rw-r--r--dist/sosreport/po.de.js.gzbin0 -> 5575 bytes
-rw-r--r--dist/sosreport/po.es.js.gzbin0 -> 5532 bytes
-rw-r--r--dist/sosreport/po.fi.js.gzbin0 -> 5606 bytes
-rw-r--r--dist/sosreport/po.fr.js.gzbin0 -> 5560 bytes
-rw-r--r--dist/sosreport/po.he.js.gzbin0 -> 5753 bytes
-rw-r--r--dist/sosreport/po.it.js.gzbin0 -> 5105 bytes
-rw-r--r--dist/sosreport/po.ja.js.gzbin0 -> 6205 bytes
-rw-r--r--dist/sosreport/po.ka.js.gzbin0 -> 6490 bytes
-rw-r--r--dist/sosreport/po.ko.js.gzbin0 -> 5944 bytes
-rw-r--r--dist/sosreport/po.manifest.cs.js.gzbin0 -> 273 bytes
-rw-r--r--dist/sosreport/po.manifest.de.js.gzbin0 -> 218 bytes
-rw-r--r--dist/sosreport/po.manifest.es.js.gzbin0 -> 239 bytes
-rw-r--r--dist/sosreport/po.manifest.fi.js.gzbin0 -> 231 bytes
-rw-r--r--dist/sosreport/po.manifest.fr.js.gzbin0 -> 224 bytes
-rw-r--r--dist/sosreport/po.manifest.he.js.gzbin0 -> 287 bytes
-rw-r--r--dist/sosreport/po.manifest.it.js.gzbin0 -> 220 bytes
-rw-r--r--dist/sosreport/po.manifest.ja.js.gzbin0 -> 262 bytes
-rw-r--r--dist/sosreport/po.manifest.ka.js.gzbin0 -> 279 bytes
-rw-r--r--dist/sosreport/po.manifest.ko.js.gzbin0 -> 257 bytes
-rw-r--r--dist/sosreport/po.manifest.nb_NO.js.gzbin0 -> 222 bytes
-rw-r--r--dist/sosreport/po.manifest.nl.js.gzbin0 -> 218 bytes
-rw-r--r--dist/sosreport/po.manifest.pl.js.gzbin0 -> 281 bytes
-rw-r--r--dist/sosreport/po.manifest.pt_BR.js.gzbin0 -> 235 bytes
-rw-r--r--dist/sosreport/po.manifest.ru.js.gzbin0 -> 324 bytes
-rw-r--r--dist/sosreport/po.manifest.sk.js.gzbin0 -> 251 bytes
-rw-r--r--dist/sosreport/po.manifest.sv.js.gzbin0 -> 224 bytes
-rw-r--r--dist/sosreport/po.manifest.tr.js.gzbin0 -> 241 bytes
-rw-r--r--dist/sosreport/po.manifest.uk.js.gzbin0 -> 338 bytes
-rw-r--r--dist/sosreport/po.manifest.zh_CN.js.gzbin0 -> 245 bytes
-rw-r--r--dist/sosreport/po.nb_NO.js.gzbin0 -> 4231 bytes
-rw-r--r--dist/sosreport/po.nl.js.gzbin0 -> 5353 bytes
-rw-r--r--dist/sosreport/po.pl.js.gzbin0 -> 5806 bytes
-rw-r--r--dist/sosreport/po.pt_BR.js.gzbin0 -> 4377 bytes
-rw-r--r--dist/sosreport/po.ru.js.gzbin0 -> 6303 bytes
-rw-r--r--dist/sosreport/po.sk.js.gzbin0 -> 4399 bytes
-rw-r--r--dist/sosreport/po.sv.js.gzbin0 -> 5452 bytes
-rw-r--r--dist/sosreport/po.tr.js.gzbin0 -> 5664 bytes
-rw-r--r--dist/sosreport/po.uk.js.gzbin0 -> 6901 bytes
-rw-r--r--dist/sosreport/po.zh_CN.js.gzbin0 -> 5674 bytes
-rw-r--r--dist/sosreport/sosreport.css.LEGAL.txt0
-rw-r--r--dist/sosreport/sosreport.css.gzbin0 -> 133941 bytes
-rw-r--r--dist/sosreport/sosreport.js.LEGAL.txt46
-rw-r--r--dist/sosreport/sosreport.js.gzbin0 -> 277744 bytes
-rw-r--r--dist/sosreport/sosreport.pngbin0 -> 5818 bytes
-rw-r--r--dist/static/fonts/RedHatDisplay-Black.woff2bin0 -> 25400 bytes
-rw-r--r--dist/static/fonts/RedHatDisplay-BlackItalic.woff2bin0 -> 27104 bytes
-rw-r--r--dist/static/fonts/RedHatDisplay-Bold.woff2bin0 -> 26628 bytes
-rw-r--r--dist/static/fonts/RedHatDisplay-BoldItalic.woff2bin0 -> 28040 bytes
-rw-r--r--dist/static/fonts/RedHatDisplay-Italic.woff2bin0 -> 27108 bytes
-rw-r--r--dist/static/fonts/RedHatDisplay-Medium.woff2bin0 -> 26664 bytes
-rw-r--r--dist/static/fonts/RedHatDisplay-MediumItalic.woff2bin0 -> 28032 bytes
-rw-r--r--dist/static/fonts/RedHatDisplay-Regular.woff2bin0 -> 25696 bytes
-rw-r--r--dist/static/fonts/RedHatMono-Bold.woff2bin0 -> 20936 bytes
-rw-r--r--dist/static/fonts/RedHatMono-BoldItalic.woff2bin0 -> 22484 bytes
-rw-r--r--dist/static/fonts/RedHatMono-Italic.woff2bin0 -> 32704 bytes
-rw-r--r--dist/static/fonts/RedHatMono-Medium.woff2bin0 -> 21340 bytes
-rw-r--r--dist/static/fonts/RedHatMono-MediumItalic.woff2bin0 -> 23152 bytes
-rw-r--r--dist/static/fonts/RedHatMono-Regular.woff2bin0 -> 20952 bytes
-rw-r--r--dist/static/fonts/RedHatText-Bold.woff2bin0 -> 25764 bytes
-rw-r--r--dist/static/fonts/RedHatText-BoldItalic.woff2bin0 -> 26984 bytes
-rw-r--r--dist/static/fonts/RedHatText-Italic.woff2bin0 -> 27044 bytes
-rw-r--r--dist/static/fonts/RedHatText-Medium.woff2bin0 -> 26880 bytes
-rw-r--r--dist/static/fonts/RedHatText-MediumItalic.woff2bin0 -> 28316 bytes
-rw-r--r--dist/static/fonts/RedHatText-Regular.woff2bin0 -> 25844 bytes
-rw-r--r--dist/static/login.css1
-rw-r--r--dist/static/login.css.LEGAL.txt0
-rw-r--r--dist/static/login.html178
-rw-r--r--dist/static/login.js1
-rw-r--r--dist/static/login.js.LEGAL.txt0
-rw-r--r--dist/static/manifest.json1
-rw-r--r--dist/static/po.cs.js967
-rw-r--r--dist/static/po.de.js933
-rw-r--r--dist/static/po.es.js965
-rw-r--r--dist/static/po.fi.js933
-rw-r--r--dist/static/po.fr.js933
-rw-r--r--dist/static/po.he.js961
-rw-r--r--dist/static/po.it.js917
-rw-r--r--dist/static/po.ja.js959
-rw-r--r--dist/static/po.ka.js965
-rw-r--r--dist/static/po.ko.js959
-rw-r--r--dist/static/po.manifest.cs.js27
-rw-r--r--dist/static/po.manifest.de.js27
-rw-r--r--dist/static/po.manifest.es.js27
-rw-r--r--dist/static/po.manifest.fi.js27
-rw-r--r--dist/static/po.manifest.fr.js27
-rw-r--r--dist/static/po.manifest.he.js27
-rw-r--r--dist/static/po.manifest.it.js27
-rw-r--r--dist/static/po.manifest.ja.js27
-rw-r--r--dist/static/po.manifest.ka.js27
-rw-r--r--dist/static/po.manifest.ko.js27
-rw-r--r--dist/static/po.manifest.nb_NO.js27
-rw-r--r--dist/static/po.manifest.nl.js27
-rw-r--r--dist/static/po.manifest.pl.js27
-rw-r--r--dist/static/po.manifest.pt_BR.js27
-rw-r--r--dist/static/po.manifest.ru.js27
-rw-r--r--dist/static/po.manifest.sk.js27
-rw-r--r--dist/static/po.manifest.sv.js27
-rw-r--r--dist/static/po.manifest.tr.js27
-rw-r--r--dist/static/po.manifest.uk.js27
-rw-r--r--dist/static/po.manifest.zh_CN.js27
-rw-r--r--dist/static/po.nb_NO.js885
-rw-r--r--dist/static/po.nl.js961
-rw-r--r--dist/static/po.pl.js939
-rw-r--r--dist/static/po.pt_BR.js853
-rw-r--r--dist/static/po.ru.js927
-rw-r--r--dist/static/po.sk.js895
-rw-r--r--dist/static/po.sv.js965
-rw-r--r--dist/static/po.tr.js965
-rw-r--r--dist/static/po.uk.js967
-rw-r--r--dist/static/po.zh_CN.js959
-rw-r--r--dist/storaged/index.html37
-rw-r--r--dist/storaged/manifest.json75
-rw-r--r--dist/storaged/po.cs.js.gzbin0 -> 16757 bytes
-rw-r--r--dist/storaged/po.de.js.gzbin0 -> 14970 bytes
-rw-r--r--dist/storaged/po.es.js.gzbin0 -> 17460 bytes
-rw-r--r--dist/storaged/po.fi.js.gzbin0 -> 14699 bytes
-rw-r--r--dist/storaged/po.fr.js.gzbin0 -> 14140 bytes
-rw-r--r--dist/storaged/po.he.js.gzbin0 -> 15213 bytes
-rw-r--r--dist/storaged/po.it.js.gzbin0 -> 13233 bytes
-rw-r--r--dist/storaged/po.ja.js.gzbin0 -> 18354 bytes
-rw-r--r--dist/storaged/po.ka.js.gzbin0 -> 20251 bytes
-rw-r--r--dist/storaged/po.ko.js.gzbin0 -> 17894 bytes
-rw-r--r--dist/storaged/po.manifest.cs.js.gzbin0 -> 618 bytes
-rw-r--r--dist/storaged/po.manifest.de.js.gzbin0 -> 539 bytes
-rw-r--r--dist/storaged/po.manifest.es.js.gzbin0 -> 545 bytes
-rw-r--r--dist/storaged/po.manifest.fi.js.gzbin0 -> 551 bytes
-rw-r--r--dist/storaged/po.manifest.fr.js.gzbin0 -> 537 bytes
-rw-r--r--dist/storaged/po.manifest.he.js.gzbin0 -> 623 bytes
-rw-r--r--dist/storaged/po.manifest.it.js.gzbin0 -> 461 bytes
-rw-r--r--dist/storaged/po.manifest.ja.js.gzbin0 -> 632 bytes
-rw-r--r--dist/storaged/po.manifest.ka.js.gzbin0 -> 649 bytes
-rw-r--r--dist/storaged/po.manifest.ko.js.gzbin0 -> 618 bytes
-rw-r--r--dist/storaged/po.manifest.nb_NO.js.gzbin0 -> 507 bytes
-rw-r--r--dist/storaged/po.manifest.nl.js.gzbin0 -> 525 bytes
-rw-r--r--dist/storaged/po.manifest.pl.js.gzbin0 -> 613 bytes
-rw-r--r--dist/storaged/po.manifest.pt_BR.js.gzbin0 -> 335 bytes
-rw-r--r--dist/storaged/po.manifest.ru.js.gzbin0 -> 740 bytes
-rw-r--r--dist/storaged/po.manifest.sk.js.gzbin0 -> 455 bytes
-rw-r--r--dist/storaged/po.manifest.sv.js.gzbin0 -> 512 bytes
-rw-r--r--dist/storaged/po.manifest.tr.js.gzbin0 -> 574 bytes
-rw-r--r--dist/storaged/po.manifest.uk.js.gzbin0 -> 714 bytes
-rw-r--r--dist/storaged/po.manifest.zh_CN.js.gzbin0 -> 572 bytes
-rw-r--r--dist/storaged/po.nb_NO.js.gzbin0 -> 10691 bytes
-rw-r--r--dist/storaged/po.nl.js.gzbin0 -> 15248 bytes
-rw-r--r--dist/storaged/po.pl.js.gzbin0 -> 15063 bytes
-rw-r--r--dist/storaged/po.pt_BR.js.gzbin0 -> 11475 bytes
-rw-r--r--dist/storaged/po.ru.js.gzbin0 -> 15307 bytes
-rw-r--r--dist/storaged/po.sk.js.gzbin0 -> 10931 bytes
-rw-r--r--dist/storaged/po.sv.js.gzbin0 -> 17086 bytes
-rw-r--r--dist/storaged/po.tr.js.gzbin0 -> 17240 bytes
-rw-r--r--dist/storaged/po.uk.js.gzbin0 -> 19703 bytes
-rw-r--r--dist/storaged/po.zh_CN.js.gzbin0 -> 17341 bytes
-rw-r--r--dist/storaged/storaged.css.LEGAL.txt0
-rw-r--r--dist/storaged/storaged.css.gzbin0 -> 142605 bytes
-rw-r--r--dist/storaged/storaged.js.LEGAL.txt66
-rw-r--r--dist/storaged/storaged.js.gzbin0 -> 382368 bytes
-rw-r--r--dist/systemd/hwinfo.css.LEGAL.txt0
-rw-r--r--dist/systemd/hwinfo.css.gzbin0 -> 133602 bytes
-rw-r--r--dist/systemd/hwinfo.html15
-rw-r--r--dist/systemd/hwinfo.js.LEGAL.txt46
-rw-r--r--dist/systemd/hwinfo.js.gzbin0 -> 137607 bytes
-rw-r--r--dist/systemd/index.html18
-rw-r--r--dist/systemd/logs.css.LEGAL.txt0
-rw-r--r--dist/systemd/logs.css.gzbin0 -> 136521 bytes
-rw-r--r--dist/systemd/logs.html38
-rw-r--r--dist/systemd/logs.js.LEGAL.txt46
-rw-r--r--dist/systemd/logs.js.gzbin0 -> 176518 bytes
-rw-r--r--dist/systemd/manifest.json88
-rw-r--r--dist/systemd/overview.css.LEGAL.txt0
-rw-r--r--dist/systemd/overview.css.gzbin0 -> 77058 bytes
-rw-r--r--dist/systemd/overview.js.LEGAL.txt46
-rw-r--r--dist/systemd/overview.js.gzbin0 -> 319135 bytes
-rw-r--r--dist/systemd/po.cs.js.gzbin0 -> 13906 bytes
-rw-r--r--dist/systemd/po.de.js.gzbin0 -> 13007 bytes
-rw-r--r--dist/systemd/po.es.js.gzbin0 -> 13022 bytes
-rw-r--r--dist/systemd/po.fi.js.gzbin0 -> 13263 bytes
-rw-r--r--dist/systemd/po.fr.js.gzbin0 -> 12967 bytes
-rw-r--r--dist/systemd/po.he.js.gzbin0 -> 13417 bytes
-rw-r--r--dist/systemd/po.it.js.gzbin0 -> 12199 bytes
-rw-r--r--dist/systemd/po.ja.js.gzbin0 -> 13964 bytes
-rw-r--r--dist/systemd/po.ka.js.gzbin0 -> 14805 bytes
-rw-r--r--dist/systemd/po.ko.js.gzbin0 -> 13482 bytes
-rw-r--r--dist/systemd/po.manifest.cs.js.gzbin0 -> 952 bytes
-rw-r--r--dist/systemd/po.manifest.de.js.gzbin0 -> 823 bytes
-rw-r--r--dist/systemd/po.manifest.es.js.gzbin0 -> 860 bytes
-rw-r--r--dist/systemd/po.manifest.fi.js.gzbin0 -> 902 bytes
-rw-r--r--dist/systemd/po.manifest.fr.js.gzbin0 -> 843 bytes
-rw-r--r--dist/systemd/po.manifest.he.js.gzbin0 -> 970 bytes
-rw-r--r--dist/systemd/po.manifest.it.js.gzbin0 -> 777 bytes
-rw-r--r--dist/systemd/po.manifest.ja.js.gzbin0 -> 1032 bytes
-rw-r--r--dist/systemd/po.manifest.ka.js.gzbin0 -> 1026 bytes
-rw-r--r--dist/systemd/po.manifest.ko.js.gzbin0 -> 977 bytes
-rw-r--r--dist/systemd/po.manifest.nb_NO.js.gzbin0 -> 751 bytes
-rw-r--r--dist/systemd/po.manifest.nl.js.gzbin0 -> 790 bytes
-rw-r--r--dist/systemd/po.manifest.pl.js.gzbin0 -> 981 bytes
-rw-r--r--dist/systemd/po.manifest.pt_BR.js.gzbin0 -> 582 bytes
-rw-r--r--dist/systemd/po.manifest.ru.js.gzbin0 -> 1144 bytes
-rw-r--r--dist/systemd/po.manifest.sk.js.gzbin0 -> 918 bytes
-rw-r--r--dist/systemd/po.manifest.sv.js.gzbin0 -> 817 bytes
-rw-r--r--dist/systemd/po.manifest.tr.js.gzbin0 -> 907 bytes
-rw-r--r--dist/systemd/po.manifest.uk.js.gzbin0 -> 1131 bytes
-rw-r--r--dist/systemd/po.manifest.zh_CN.js.gzbin0 -> 961 bytes
-rw-r--r--dist/systemd/po.nb_NO.js.gzbin0 -> 9940 bytes
-rw-r--r--dist/systemd/po.nl.js.gzbin0 -> 12633 bytes
-rw-r--r--dist/systemd/po.pl.js.gzbin0 -> 13326 bytes
-rw-r--r--dist/systemd/po.pt_BR.js.gzbin0 -> 9776 bytes
-rw-r--r--dist/systemd/po.ru.js.gzbin0 -> 14505 bytes
-rw-r--r--dist/systemd/po.sk.js.gzbin0 -> 10035 bytes
-rw-r--r--dist/systemd/po.sv.js.gzbin0 -> 12708 bytes
-rw-r--r--dist/systemd/po.tr.js.gzbin0 -> 12970 bytes
-rw-r--r--dist/systemd/po.uk.js.gzbin0 -> 15364 bytes
-rw-r--r--dist/systemd/po.zh_CN.js.gzbin0 -> 12915 bytes
-rw-r--r--dist/systemd/services.css.LEGAL.txt0
-rw-r--r--dist/systemd/services.css.gzbin0 -> 134617 bytes
-rw-r--r--dist/systemd/services.html17
-rw-r--r--dist/systemd/services.js.LEGAL.txt46
-rw-r--r--dist/systemd/services.js.gzbin0 -> 194125 bytes
-rw-r--r--dist/systemd/terminal.css.LEGAL.txt35
-rw-r--r--dist/systemd/terminal.css.gzbin0 -> 32165 bytes
-rw-r--r--dist/systemd/terminal.html16
-rw-r--r--dist/systemd/terminal.js.LEGAL.txt46
-rw-r--r--dist/systemd/terminal.js.gzbin0 -> 178609 bytes
-rw-r--r--dist/users/index.html36
-rw-r--r--dist/users/manifest.json19
-rw-r--r--dist/users/po.cs.js.gzbin0 -> 7949 bytes
-rw-r--r--dist/users/po.de.js.gzbin0 -> 7454 bytes
-rw-r--r--dist/users/po.es.js.gzbin0 -> 7336 bytes
-rw-r--r--dist/users/po.fi.js.gzbin0 -> 7556 bytes
-rw-r--r--dist/users/po.fr.js.gzbin0 -> 7431 bytes
-rw-r--r--dist/users/po.he.js.gzbin0 -> 7632 bytes
-rw-r--r--dist/users/po.it.js.gzbin0 -> 6797 bytes
-rw-r--r--dist/users/po.ja.js.gzbin0 -> 8073 bytes
-rw-r--r--dist/users/po.ka.js.gzbin0 -> 8332 bytes
-rw-r--r--dist/users/po.ko.js.gzbin0 -> 7728 bytes
-rw-r--r--dist/users/po.manifest.cs.js.gzbin0 -> 413 bytes
-rw-r--r--dist/users/po.manifest.de.js.gzbin0 -> 342 bytes
-rw-r--r--dist/users/po.manifest.es.js.gzbin0 -> 363 bytes
-rw-r--r--dist/users/po.manifest.fi.js.gzbin0 -> 372 bytes
-rw-r--r--dist/users/po.manifest.fr.js.gzbin0 -> 360 bytes
-rw-r--r--dist/users/po.manifest.he.js.gzbin0 -> 429 bytes
-rw-r--r--dist/users/po.manifest.it.js.gzbin0 -> 330 bytes
-rw-r--r--dist/users/po.manifest.ja.js.gzbin0 -> 406 bytes
-rw-r--r--dist/users/po.manifest.ka.js.gzbin0 -> 433 bytes
-rw-r--r--dist/users/po.manifest.ko.js.gzbin0 -> 404 bytes
-rw-r--r--dist/users/po.manifest.nb_NO.js.gzbin0 -> 338 bytes
-rw-r--r--dist/users/po.manifest.nl.js.gzbin0 -> 342 bytes
-rw-r--r--dist/users/po.manifest.pl.js.gzbin0 -> 429 bytes
-rw-r--r--dist/users/po.manifest.pt_BR.js.gzbin0 -> 316 bytes
-rw-r--r--dist/users/po.manifest.ru.js.gzbin0 -> 513 bytes
-rw-r--r--dist/users/po.manifest.sk.js.gzbin0 -> 388 bytes
-rw-r--r--dist/users/po.manifest.sv.js.gzbin0 -> 350 bytes
-rw-r--r--dist/users/po.manifest.tr.js.gzbin0 -> 377 bytes
-rw-r--r--dist/users/po.manifest.uk.js.gzbin0 -> 512 bytes
-rw-r--r--dist/users/po.manifest.zh_CN.js.gzbin0 -> 386 bytes
-rw-r--r--dist/users/po.nb_NO.js.gzbin0 -> 5510 bytes
-rw-r--r--dist/users/po.nl.js.gzbin0 -> 7164 bytes
-rw-r--r--dist/users/po.pl.js.gzbin0 -> 7676 bytes
-rw-r--r--dist/users/po.pt_BR.js.gzbin0 -> 5828 bytes
-rw-r--r--dist/users/po.ru.js.gzbin0 -> 7946 bytes
-rw-r--r--dist/users/po.sk.js.gzbin0 -> 5767 bytes
-rw-r--r--dist/users/po.sv.js.gzbin0 -> 7270 bytes
-rw-r--r--dist/users/po.tr.js.gzbin0 -> 7431 bytes
-rw-r--r--dist/users/po.uk.js.gzbin0 -> 9042 bytes
-rw-r--r--dist/users/po.zh_CN.js.gzbin0 -> 7524 bytes
-rw-r--r--dist/users/users.css.LEGAL.txt0
-rw-r--r--dist/users/users.css.gzbin0 -> 140566 bytes
-rw-r--r--dist/users/users.js.LEGAL.txt46
-rw-r--r--dist/users/users.js.gzbin0 -> 185528 bytes
-rw-r--r--doc/Makefile-doc.am14
-rw-r--r--doc/cockpit-transport.pngbin0 -> 315832 bytes
-rw-r--r--doc/cockpit-transport.svg1562
-rw-r--r--doc/guide/Makefile-guide.am114
-rw-r--r--doc/guide/api-base1.xml32
-rw-r--r--doc/guide/api-cockpit.xml23
-rw-r--r--doc/guide/api-shell.xml44
-rw-r--r--doc/guide/api-system.xml143
-rw-r--r--doc/guide/authentication.xml148
-rw-r--r--doc/guide/cert-authentication.xml296
-rw-r--r--doc/guide/cockpit-cache.xml70
-rw-r--r--doc/guide/cockpit-channel.xml351
-rw-r--r--doc/guide/cockpit-dbus.xml909
-rw-r--r--doc/guide/cockpit-error.xml74
-rw-r--r--doc/guide/cockpit-file.xml236
-rw-r--r--doc/guide/cockpit-guide.xml63
-rw-r--r--doc/guide/cockpit-http.xml302
-rw-r--r--doc/guide/cockpit-locale.xml103
-rw-r--r--doc/guide/cockpit-location.xml185
-rw-r--r--doc/guide/cockpit-manifest.xml20
-rw-r--r--doc/guide/cockpit-metrics.xml126
-rw-r--r--doc/guide/cockpit-series.xml277
-rw-r--r--doc/guide/cockpit-session.xml107
-rw-r--r--doc/guide/cockpit-spawn.xml227
-rw-r--r--doc/guide/cockpit-util.xml153
-rw-r--r--doc/guide/embedding.xml110
-rw-r--r--doc/guide/feature-firewall.xml34
-rw-r--r--doc/guide/feature-journal.xml19
-rw-r--r--doc/guide/feature-machines.xml107
-rw-r--r--doc/guide/feature-networkmanager.xml27
-rw-r--r--doc/guide/feature-packagekit.xml44
-rw-r--r--doc/guide/feature-pcp.xml48
-rw-r--r--doc/guide/feature-realmd.xml39
-rw-r--r--doc/guide/feature-selinux.xml22
-rw-r--r--doc/guide/feature-sosreport.xml10
-rw-r--r--doc/guide/feature-storaged.xml49
-rw-r--r--doc/guide/feature-systemd.xml124
-rw-r--r--doc/guide/feature-terminal.xml11
-rw-r--r--doc/guide/feature-tuned.xml10
-rw-r--r--doc/guide/feature-users.xml11
-rw-r--r--doc/guide/gtk-doc.xsl901
-rw-r--r--doc/guide/https.xml78
-rw-r--r--doc/guide/listen.xml102
-rw-r--r--doc/guide/packages.xml517
-rw-r--r--doc/guide/privileges.xml64
-rw-r--r--doc/guide/sso.xml201
-rw-r--r--doc/guide/startup.xml48
-rw-r--r--doc/guide/static/gtk-doc.css278
-rw-r--r--doc/guide/static/home.pngbin0 -> 654 bytes
-rw-r--r--doc/guide/static/left.pngbin0 -> 459 bytes
-rw-r--r--doc/guide/static/right.pngbin0 -> 472 bytes
-rw-r--r--doc/guide/static/style.css227
-rw-r--r--doc/guide/static/up.pngbin0 -> 406 bytes
-rw-r--r--doc/guide/urls.xml76
-rw-r--r--doc/guide/version-greater-or-equal.xsl54
-rw-r--r--doc/guide/version.in1
-rw-r--r--doc/man/Makefile-man.am36
-rw-r--r--doc/man/cockpit-bridge.xml116
-rw-r--r--doc/man/cockpit-desktop.xml124
-rw-r--r--doc/man/cockpit-tls.xml209
-rw-r--r--doc/man/cockpit-ws.xml243
-rw-r--r--doc/man/cockpit.conf.xml285
-rw-r--r--doc/man/cockpit.xml81
-rw-r--r--doc/man/pam_ssh_add.xml97
-rw-r--r--files.js192
-rw-r--r--node_modules/chrome-remote-interface/LICENSE18
-rw-r--r--node_modules/chrome-remote-interface/README.md992
-rwxr-xr-xnode_modules/chrome-remote-interface/bin/client.js311
-rw-r--r--node_modules/chrome-remote-interface/chrome-remote-interface.js1
-rw-r--r--node_modules/chrome-remote-interface/index.js44
-rw-r--r--node_modules/chrome-remote-interface/lib/api.js92
-rw-r--r--node_modules/chrome-remote-interface/lib/chrome.js314
-rw-r--r--node_modules/chrome-remote-interface/lib/defaults.js4
-rw-r--r--node_modules/chrome-remote-interface/lib/devtools.js127
-rw-r--r--node_modules/chrome-remote-interface/lib/external-request.js44
-rw-r--r--node_modules/chrome-remote-interface/lib/protocol.json27862
-rw-r--r--node_modules/chrome-remote-interface/lib/websocket-wrapper.js39
-rw-r--r--node_modules/chrome-remote-interface/package.json64
-rw-r--r--node_modules/chrome-remote-interface/webpack.config.js48
-rw-r--r--node_modules/commander/History.md298
-rw-r--r--node_modules/commander/LICENSE22
-rw-r--r--node_modules/commander/Readme.md351
-rw-r--r--node_modules/commander/index.js1137
-rw-r--r--node_modules/commander/package.json29
-rw-r--r--node_modules/sizzle/AUTHORS.txt67
-rw-r--r--node_modules/sizzle/LICENSE.txt36
-rw-r--r--node_modules/sizzle/README.md55
-rw-r--r--node_modules/sizzle/dist/sizzle.js2514
-rw-r--r--node_modules/sizzle/dist/sizzle.min.js3
-rw-r--r--node_modules/sizzle/dist/sizzle.min.map1
-rw-r--r--node_modules/sizzle/package.json85
-rw-r--r--node_modules/ws/LICENSE21
-rw-r--r--node_modules/ws/README.md495
-rw-r--r--node_modules/ws/browser.js8
-rw-r--r--node_modules/ws/index.js10
-rw-r--r--node_modules/ws/lib/buffer-util.js129
-rw-r--r--node_modules/ws/lib/constants.js10
-rw-r--r--node_modules/ws/lib/event-target.js184
-rw-r--r--node_modules/ws/lib/extension.js223
-rw-r--r--node_modules/ws/lib/limiter.js55
-rw-r--r--node_modules/ws/lib/permessage-deflate.js518
-rw-r--r--node_modules/ws/lib/receiver.js607
-rw-r--r--node_modules/ws/lib/sender.js409
-rw-r--r--node_modules/ws/lib/stream.js180
-rw-r--r--node_modules/ws/lib/validation.js104
-rw-r--r--node_modules/ws/lib/websocket-server.js447
-rw-r--r--node_modules/ws/lib/websocket.js1195
-rw-r--r--node_modules/ws/package.json56
-rw-r--r--package-lock.json5810
-rw-r--r--package.json64
-rw-r--r--pkg/Makefile.am56
-rw-r--r--pkg/Makefile.qunit2
-rw-r--r--pkg/apps/application-list.jsx221
-rw-r--r--pkg/apps/application.jsx155
-rw-r--r--pkg/apps/application.scss45
-rw-r--r--pkg/apps/apps.jsx78
-rw-r--r--pkg/apps/appstream.js58
-rw-r--r--pkg/apps/content-security-policy.override0
-rw-r--r--pkg/apps/default.pngbin0 -> 2538 bytes
-rw-r--r--pkg/apps/index.html38
-rw-r--r--pkg/apps/manifest.json23
-rw-r--r--pkg/apps/packagekit.js187
-rw-r--r--pkg/apps/utils.jsx99
-rw-r--r--pkg/apps/watch-appstream.py367
-rw-r--r--pkg/base1/cockpit.js21
-rw-r--r--pkg/base1/manifest.json5
-rw-r--r--pkg/base1/test-base64.js75
-rw-r--r--pkg/base1/test-browser-storage.js81
-rw-r--r--pkg/base1/test-cache.js103
-rw-r--r--pkg/base1/test-chan.js844
-rw-r--r--pkg/base1/test-dbus-address.js24
-rw-r--r--pkg/base1/test-dbus-common.js1016
-rw-r--r--pkg/base1/test-dbus-framed.js84
-rw-r--r--pkg/base1/test-dbus.js548
-rw-r--r--pkg/base1/test-echo.js125
-rw-r--r--pkg/base1/test-events.js49
-rw-r--r--pkg/base1/test-external.js162
-rw-r--r--pkg/base1/test-file.js432
-rw-r--r--pkg/base1/test-format.js207
-rw-r--r--pkg/base1/test-framed-cache.js92
-rw-r--r--pkg/base1/test-framed.js101
-rw-r--r--pkg/base1/test-http.js396
-rw-r--r--pkg/base1/test-journal-renderer.js314
-rw-r--r--pkg/base1/test-locale.js235
-rw-r--r--pkg/base1/test-location.js319
-rw-r--r--pkg/base1/test-metrics.js195
-rw-r--r--pkg/base1/test-no-jquery.js29
-rw-r--r--pkg/base1/test-permissions.js67
-rw-r--r--pkg/base1/test-promise.js21
-rw-r--r--pkg/base1/test-protocol.js117
-rw-r--r--pkg/base1/test-series.js496
-rw-r--r--pkg/base1/test-spawn-proc.js209
-rw-r--r--pkg/base1/test-spawn.js353
-rw-r--r--pkg/base1/test-stream.js94
-rw-r--r--pkg/base1/test-user.js38
-rw-r--r--pkg/base1/test-utf8.js129
-rw-r--r--pkg/base1/test-websocket.js81
-rw-r--r--pkg/kdump/config-client-suse.js265
-rw-r--r--pkg/kdump/config-client.js358
-rw-r--r--pkg/kdump/crashkernel.sh9
-rw-r--r--pkg/kdump/index.html37
-rw-r--r--pkg/kdump/kdump-client.js182
-rw-r--r--pkg/kdump/kdump-view.jsx639
-rw-r--r--pkg/kdump/kdump.js129
-rw-r--r--pkg/kdump/kdump.scss34
-rw-r--r--pkg/kdump/manifest.json22
-rw-r--r--pkg/kdump/test-config-client.js82
-rw-r--r--pkg/kdump/testwritable.sh5
-rw-r--r--pkg/lib/README5
-rw-r--r--pkg/lib/_global-variables.scss14
-rw-r--r--pkg/lib/cockpit-components-context-menu.jsx110
-rw-r--r--pkg/lib/cockpit-components-dialog.jsx363
-rw-r--r--pkg/lib/cockpit-components-dialog.scss8
-rw-r--r--pkg/lib/cockpit-components-dynamic-list.jsx143
-rw-r--r--pkg/lib/cockpit-components-dynamic-list.scss39
-rw-r--r--pkg/lib/cockpit-components-empty-state.css3
-rw-r--r--pkg/lib/cockpit-components-empty-state.jsx63
-rw-r--r--pkg/lib/cockpit-components-file-autocomplete.jsx212
-rw-r--r--pkg/lib/cockpit-components-firewalld-request.jsx167
-rw-r--r--pkg/lib/cockpit-components-firewalld-request.scss12
-rw-r--r--pkg/lib/cockpit-components-form-helper.jsx43
-rw-r--r--pkg/lib/cockpit-components-inline-notification.css7
-rw-r--r--pkg/lib/cockpit-components-inline-notification.jsx96
-rw-r--r--pkg/lib/cockpit-components-install-dialog.css43
-rw-r--r--pkg/lib/cockpit-components-install-dialog.jsx211
-rw-r--r--pkg/lib/cockpit-components-listing-panel.jsx87
-rw-r--r--pkg/lib/cockpit-components-listing-panel.scss93
-rw-r--r--pkg/lib/cockpit-components-logs-panel.jsx185
-rw-r--r--pkg/lib/cockpit-components-logs-panel.scss15
-rw-r--r--pkg/lib/cockpit-components-modifications.css28
-rw-r--r--pkg/lib/cockpit-components-modifications.jsx182
-rw-r--r--pkg/lib/cockpit-components-password.jsx188
-rw-r--r--pkg/lib/cockpit-components-password.scss11
-rw-r--r--pkg/lib/cockpit-components-plot.jsx513
-rw-r--r--pkg/lib/cockpit-components-plot.scss119
-rw-r--r--pkg/lib/cockpit-components-privileged.jsx77
-rw-r--r--pkg/lib/cockpit-components-shutdown.jsx248
-rw-r--r--pkg/lib/cockpit-components-shutdown.scss17
-rw-r--r--pkg/lib/cockpit-components-table.jsx297
-rw-r--r--pkg/lib/cockpit-components-table.scss106
-rw-r--r--pkg/lib/cockpit-components-terminal.jsx362
-rw-r--r--pkg/lib/cockpit-components-truncate.jsx42
-rw-r--r--pkg/lib/cockpit-components-truncate.scss4
-rw-r--r--pkg/lib/cockpit-dark-theme.js70
-rw-r--r--pkg/lib/cockpit-po-plugin.js159
-rw-r--r--pkg/lib/cockpit-rsync-plugin.js49
-rw-r--r--pkg/lib/cockpit.js4444
-rw-r--r--pkg/lib/console.css16
-rw-r--r--pkg/lib/context-menu.scss20
-rw-r--r--pkg/lib/credentials-ssh-private-keys.sh34
-rw-r--r--pkg/lib/credentials-ssh-remove-key.sh10
-rw-r--r--pkg/lib/credentials.js344
-rw-r--r--pkg/lib/ct-card.scss63
-rw-r--r--pkg/lib/dialogs.jsx131
-rw-r--r--pkg/lib/esbuild-cleanup-plugin.js17
-rw-r--r--pkg/lib/esbuild-common.js40
-rw-r--r--pkg/lib/esbuild-compress-plugin.js50
-rw-r--r--pkg/lib/esbuild-test-html-plugin.js28
-rw-r--r--pkg/lib/get-timesync-backend.py61
-rw-r--r--pkg/lib/hooks.js326
-rwxr-xr-xpkg/lib/html2po.js235
-rw-r--r--pkg/lib/inotify.py72
-rw-r--r--pkg/lib/journal.css161
-rw-r--r--pkg/lib/journal.js453
-rw-r--r--pkg/lib/long-running-process.js166
-rw-r--r--pkg/lib/machine-info.js259
-rwxr-xr-xpkg/lib/manifest2po.js179
-rw-r--r--pkg/lib/menu-select-widget.scss35
-rw-r--r--pkg/lib/notifications.js167
-rw-r--r--pkg/lib/os-release.js38
-rw-r--r--pkg/lib/packagekit.js528
-rw-r--r--pkg/lib/page.scss197
-rw-r--r--pkg/lib/pam_user_parser.js95
-rw-r--r--pkg/lib/patternfly/_fonts.scss38
-rw-r--r--pkg/lib/patternfly/patternfly-5-cockpit.scss9
-rw-r--r--pkg/lib/patternfly/patternfly-5-overrides.scss391
-rw-r--r--pkg/lib/plot.js574
-rw-r--r--pkg/lib/polyfills.js25
-rw-r--r--pkg/lib/python.js30
-rw-r--r--pkg/lib/qunit-template.html.in35
-rw-r--r--pkg/lib/qunit-tests.js90
-rw-r--r--pkg/lib/serverTime.js768
-rw-r--r--pkg/lib/serverTime.scss7
-rw-r--r--pkg/lib/service.js344
-rw-r--r--pkg/lib/superuser.js126
-rw-r--r--pkg/lib/table.css138
-rw-r--r--pkg/lib/timeformat.js66
-rw-r--r--pkg/lib/utils.jsx33
-rw-r--r--pkg/metrics/index.html37
-rw-r--r--pkg/metrics/index.js30
-rw-r--r--pkg/metrics/manifest.json11
-rw-r--r--pkg/metrics/metrics.jsx2022
-rw-r--r--pkg/metrics/metrics.scss852
-rw-r--r--pkg/networkmanager/bond.jsx231
-rw-r--r--pkg/networkmanager/bridge.jsx147
-rw-r--r--pkg/networkmanager/bridgeport.jsx91
-rw-r--r--pkg/networkmanager/dialogs-common.jsx331
-rw-r--r--pkg/networkmanager/firewall-client.js547
-rw-r--r--pkg/networkmanager/firewall-switch.jsx85
-rw-r--r--pkg/networkmanager/firewall.html36
-rw-r--r--pkg/networkmanager/firewall.jsx1096
-rw-r--r--pkg/networkmanager/helpers.js58
-rw-r--r--pkg/networkmanager/index.html39
-rw-r--r--pkg/networkmanager/interfaces.js1822
-rw-r--r--pkg/networkmanager/ip-settings.jsx403
-rw-r--r--pkg/networkmanager/mac.jsx78
-rw-r--r--pkg/networkmanager/manifest.json43
-rw-r--r--pkg/networkmanager/model-context.jsx3
-rw-r--r--pkg/networkmanager/mtu.jsx97
-rw-r--r--pkg/networkmanager/network-interface-members.jsx211
-rw-r--r--pkg/networkmanager/network-interface.jsx756
-rw-r--r--pkg/networkmanager/network-main.jsx215
-rw-r--r--pkg/networkmanager/networking.scss316
-rw-r--r--pkg/networkmanager/networkmanager.jsx156
-rw-r--r--pkg/networkmanager/plots.js49
-rw-r--r--pkg/networkmanager/team.jsx228
-rw-r--r--pkg/networkmanager/teamport.jsx99
-rw-r--r--pkg/networkmanager/test-utils.js318
-rw-r--r--pkg/networkmanager/utils.js247
-rw-r--r--pkg/networkmanager/vlan.jsx134
-rw-r--r--pkg/networkmanager/wireguard.jsx364
-rw-r--r--pkg/networkmanager/wireguard.scss39
-rw-r--r--pkg/packagekit/autoupdates.jsx413
-rw-r--r--pkg/packagekit/callTracer.py19
-rw-r--r--pkg/packagekit/history.jsx120
-rw-r--r--pkg/packagekit/index.html37
-rw-r--r--pkg/packagekit/kpatch.jsx393
-rw-r--r--pkg/packagekit/manifest.json29
-rw-r--r--pkg/packagekit/mock-updates.js111
-rw-r--r--pkg/packagekit/pf-security.woffbin0 -> 36660 bytes
-rw-r--r--pkg/packagekit/updates.jsx1650
-rw-r--r--pkg/packagekit/updates.scss275
-rw-r--r--pkg/pcp/manifest.json11
-rw-r--r--pkg/playground/exception.html23
-rw-r--r--pkg/playground/exception.js15
-rw-r--r--pkg/playground/hammer.gifbin0 -> 74176 bytes
-rw-r--r--pkg/playground/index.html29
-rw-r--r--pkg/playground/index.js38
-rw-r--r--pkg/playground/journal.html21
-rw-r--r--pkg/playground/journal.jsx37
-rw-r--r--pkg/playground/manifest.json48
-rw-r--r--pkg/playground/metrics.html22
-rw-r--r--pkg/playground/metrics.js20
-rw-r--r--pkg/playground/notifications-receiver.html18
-rw-r--r--pkg/playground/notifications-receiver.js24
-rw-r--r--pkg/playground/pkgs.html19
-rw-r--r--pkg/playground/pkgs.js42
-rw-r--r--pkg/playground/plot.css3
-rw-r--r--pkg/playground/plot.html15
-rw-r--r--pkg/playground/plot.js38
-rw-r--r--pkg/playground/preloaded.html20
-rw-r--r--pkg/playground/preloaded.js43
-rw-r--r--pkg/playground/react-demo-cards.jsx68
-rw-r--r--pkg/playground/react-demo-dialog.jsx40
-rw-r--r--pkg/playground/react-demo-file-autocomplete.jsx33
-rw-r--r--pkg/playground/react-patterns.html35
-rw-r--r--pkg/playground/react-patterns.js129
-rw-r--r--pkg/playground/service.html24
-rw-r--r--pkg/playground/service.js42
-rw-r--r--pkg/playground/speed.css5
-rw-r--r--pkg/playground/speed.html128
-rw-r--r--pkg/playground/speed.js281
-rw-r--r--pkg/playground/test.html48
-rw-r--r--pkg/playground/test.js130
-rw-r--r--pkg/playground/translate.html138
-rw-r--r--pkg/playground/translate.js39
-rw-r--r--pkg/ruff.toml9
-rw-r--r--pkg/selinux/index.html38
-rw-r--r--pkg/selinux/manifest.json15
-rw-r--r--pkg/selinux/selinux-client.js236
-rw-r--r--pkg/selinux/selinux.js306
-rw-r--r--pkg/selinux/setroubleshoot-client.js179
-rw-r--r--pkg/selinux/setroubleshoot-view.jsx468
-rw-r--r--pkg/selinux/setroubleshoot.scss67
-rw-r--r--pkg/shell/active-pages-modal.jsx115
-rw-r--r--pkg/shell/base_index.js927
-rw-r--r--pkg/shell/credentials.jsx324
-rw-r--r--pkg/shell/credentials.scss22
-rw-r--r--pkg/shell/failures.jsx81
-rw-r--r--pkg/shell/hosts.jsx248
-rw-r--r--pkg/shell/hosts_dialog.jsx1074
-rw-r--r--pkg/shell/images/bg-plain.jpgbin0 -> 81737 bytes
-rw-r--r--pkg/shell/images/cockpit-icon.svg1
-rw-r--r--pkg/shell/images/server-error.pngbin0 -> 720 bytes
-rw-r--r--pkg/shell/images/server-large.pngbin0 -> 1296 bytes
-rw-r--r--pkg/shell/images/server-small.pngbin0 -> 508 bytes
-rw-r--r--pkg/shell/index.html57
-rw-r--r--pkg/shell/indexes.jsx623
-rw-r--r--pkg/shell/machines/machines.js792
-rwxr-xr-xpkg/shell/machines/ssh-add-key.sh24
-rwxr-xr-xpkg/shell/machines/ssh-show-default-key.sh16
-rw-r--r--pkg/shell/machines/test-machines.js209
-rw-r--r--pkg/shell/manifest.json58
-rw-r--r--pkg/shell/nav.jsx250
-rw-r--r--pkg/shell/nav.scss824
-rw-r--r--pkg/shell/shell-modals.jsx203
-rw-r--r--pkg/shell/shell.html15
-rw-r--r--pkg/shell/shell.js34
-rw-r--r--pkg/shell/shell.scss273
-rw-r--r--pkg/shell/superuser.jsx349
-rw-r--r--pkg/shell/topnav.jsx289
-rw-r--r--pkg/sosreport/cockpit-sosreport.pngbin0 -> 1908 bytes
-rw-r--r--pkg/sosreport/index.html33
-rw-r--r--pkg/sosreport/manifest.json12
-rw-r--r--pkg/sosreport/sosreport.jsx532
-rw-r--r--pkg/sosreport/sosreport.pngbin0 -> 5818 bytes
-rw-r--r--pkg/sosreport/sosreport.scss25
-rw-r--r--pkg/static/login.html178
-rw-r--r--pkg/static/login.js1047
-rw-r--r--pkg/static/login.scss1043
-rw-r--r--pkg/static/manifest.json1
-rw-r--r--pkg/storaged/anaconda.jsx166
-rw-r--r--pkg/storaged/block/create-pages.jsx129
-rw-r--r--pkg/storaged/block/format-dialog.jsx669
-rw-r--r--pkg/storaged/block/other.jsx67
-rw-r--r--pkg/storaged/block/resize.jsx656
-rw-r--r--pkg/storaged/block/unformatted-data.jsx39
-rw-r--r--pkg/storaged/block/unrecognized-data.jsx57
-rw-r--r--pkg/storaged/btrfs/device.jsx97
-rw-r--r--pkg/storaged/btrfs/filesystem.jsx92
-rw-r--r--pkg/storaged/btrfs/subvolume.jsx343
-rw-r--r--pkg/storaged/btrfs/utils.jsx90
-rw-r--r--pkg/storaged/btrfs/volume.jsx202
-rw-r--r--pkg/storaged/client.js1767
-rw-r--r--pkg/storaged/crypto/actions.jsx76
-rwxr-xr-xpkg/storaged/crypto/clevis-luks-passphrase.sh65
-rw-r--r--pkg/storaged/crypto/encryption.jsx243
-rw-r--r--pkg/storaged/crypto/keyslots.jsx797
-rw-r--r--pkg/storaged/crypto/locked-encrypted-data.jsx41
-rwxr-xr-xpkg/storaged/crypto/luksmeta-monitor-hack.py152
-rw-r--r--pkg/storaged/crypto/tang.jsx133
-rw-r--r--pkg/storaged/dialog.jsx1374
-rw-r--r--pkg/storaged/drive/drive.jsx142
-rw-r--r--pkg/storaged/filesystem/filesystem.jsx166
-rw-r--r--pkg/storaged/filesystem/mismounting.jsx231
-rw-r--r--pkg/storaged/filesystem/mounting-dialog.jsx362
-rw-r--r--pkg/storaged/filesystem/utils.jsx244
-rw-r--r--pkg/storaged/icons/gnome-icons.jsx55
-rw-r--r--pkg/storaged/index.html37
-rw-r--r--pkg/storaged/iscsi/create-dialog.jsx181
-rw-r--r--pkg/storaged/iscsi/session.jsx98
-rw-r--r--pkg/storaged/jobs-panel.jsx232
-rw-r--r--pkg/storaged/legacy-vdo/legacy-vdo.jsx348
-rw-r--r--pkg/storaged/legacy-vdo/vdo-monitor.py132
-rw-r--r--pkg/storaged/logs-panel.jsx41
-rw-r--r--pkg/storaged/lvm2/block-logical-volume.jsx410
-rw-r--r--pkg/storaged/lvm2/create-dialog.jsx71
-rw-r--r--pkg/storaged/lvm2/create-logical-volume-dialog.jsx298
-rw-r--r--pkg/storaged/lvm2/inactive-logical-volume.jsx47
-rw-r--r--pkg/storaged/lvm2/physical-volume.jsx128
-rw-r--r--pkg/storaged/lvm2/thin-pool-logical-volume.jsx149
-rw-r--r--pkg/storaged/lvm2/unsupported-logical-volume.jsx68
-rw-r--r--pkg/storaged/lvm2/utils.jsx60
-rw-r--r--pkg/storaged/lvm2/vdo-pool.jsx95
-rw-r--r--pkg/storaged/lvm2/volume-group.jsx398
-rw-r--r--pkg/storaged/manifest.json75
-rw-r--r--pkg/storaged/mdraid/create-dialog.jsx122
-rw-r--r--pkg/storaged/mdraid/mdraid-disk.jsx135
-rw-r--r--pkg/storaged/mdraid/mdraid.jsx307
-rw-r--r--pkg/storaged/mount-users.py164
-rw-r--r--pkg/storaged/multipath.jsx77
-rwxr-xr-xpkg/storaged/nfs/nfs-mounts.py245
-rw-r--r--pkg/storaged/nfs/nfs.jsx337
-rw-r--r--pkg/storaged/overview/overview.jsx203
-rw-r--r--pkg/storaged/pages.jsx850
-rw-r--r--pkg/storaged/partitions/actions.jsx40
-rw-r--r--pkg/storaged/partitions/format-disk-dialog.jsx90
-rw-r--r--pkg/storaged/partitions/partition-table.jsx113
-rw-r--r--pkg/storaged/partitions/partition.jsx240
-rw-r--r--pkg/storaged/plot.jsx106
-rw-r--r--pkg/storaged/storage-controls.jsx269
-rw-r--r--pkg/storaged/storage.scss362
-rw-r--r--pkg/storaged/storaged.jsx100
-rw-r--r--pkg/storaged/stratis/blockdev.jsx94
-rw-r--r--pkg/storaged/stratis/create-dialog.jsx164
-rw-r--r--pkg/storaged/stratis/filesystem.jsx242
-rw-r--r--pkg/storaged/stratis/pool.jsx575
-rw-r--r--pkg/storaged/stratis/stopped-pool.jsx146
-rw-r--r--pkg/storaged/stratis/stratis2-set-key.py10
-rw-r--r--pkg/storaged/stratis/stratis3-set-key.py10
-rw-r--r--pkg/storaged/stratis/utils.jsx187
-rw-r--r--pkg/storaged/swap/swap.jsx130
-rw-r--r--pkg/storaged/test-util.js88
-rw-r--r--pkg/storaged/utils.js1140
-rw-r--r--pkg/systemd/README-realmd.md111
-rw-r--r--pkg/systemd/abrtLog.jsx330
-rw-r--r--pkg/systemd/busnames.js11
-rw-r--r--pkg/systemd/hw-detect.js172
-rw-r--r--pkg/systemd/hwinfo.html15
-rw-r--r--pkg/systemd/hwinfo.jsx352
-rw-r--r--pkg/systemd/hwinfo.scss30
-rw-r--r--pkg/systemd/index.html18
-rwxr-xr-xpkg/systemd/kernelopt.sh72
-rw-r--r--pkg/systemd/logDetails.jsx208
-rw-r--r--pkg/systemd/logs.html38
-rw-r--r--pkg/systemd/logs.jsx431
-rw-r--r--pkg/systemd/logs.scss148
-rw-r--r--pkg/systemd/logsHelpers.js143
-rw-r--r--pkg/systemd/logsJournal.jsx343
-rw-r--r--pkg/systemd/manifest.json88
-rw-r--r--pkg/systemd/overview-cards/configurationCard.jsx306
-rw-r--r--pkg/systemd/overview-cards/configurationCard.scss14
-rw-r--r--pkg/systemd/overview-cards/cryptoPolicies.jsx245
-rw-r--r--pkg/systemd/overview-cards/cryptoPolicies.scss3
-rw-r--r--pkg/systemd/overview-cards/healthCard.jsx47
-rw-r--r--pkg/systemd/overview-cards/healthCard.scss29
-rw-r--r--pkg/systemd/overview-cards/insights-poll-hack.sh44
-rw-r--r--pkg/systemd/overview-cards/insights.jsx236
-rw-r--r--pkg/systemd/overview-cards/insights.scss61
-rw-r--r--pkg/systemd/overview-cards/lastLogin.jsx145
-rw-r--r--pkg/systemd/overview-cards/lastLogin.scss28
-rw-r--r--pkg/systemd/overview-cards/motdCard.jsx123
-rw-r--r--pkg/systemd/overview-cards/motdCard.scss15
-rw-r--r--pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx87
-rw-r--r--pkg/systemd/overview-cards/realmd.jsx489
-rw-r--r--pkg/systemd/overview-cards/realmd.scss13
-rw-r--r--pkg/systemd/overview-cards/shutdownStatus.jsx114
-rw-r--r--pkg/systemd/overview-cards/shutdownStatus.scss3
-rw-r--r--pkg/systemd/overview-cards/ssh-list-host-keys.sh58
-rw-r--r--pkg/systemd/overview-cards/systemInformationCard.jsx145
-rw-r--r--pkg/systemd/overview-cards/systemInformationCard.scss16
-rw-r--r--pkg/systemd/overview-cards/tuned-dialog.jsx326
-rw-r--r--pkg/systemd/overview-cards/usageCard.jsx166
-rw-r--r--pkg/systemd/overview-cards/usageCard.scss11
-rw-r--r--pkg/systemd/overview.jsx187
-rw-r--r--pkg/systemd/overview.scss278
-rw-r--r--pkg/systemd/page-status.jsx117
-rw-r--r--pkg/systemd/page-status.scss3
-rw-r--r--pkg/systemd/reporting.jsx445
-rw-r--r--pkg/systemd/reporting.scss5
-rw-r--r--pkg/systemd/service-details.jsx730
-rw-r--r--pkg/systemd/service-details.scss72
-rw-r--r--pkg/systemd/service-tabs.jsx72
-rw-r--r--pkg/systemd/service.jsx171
-rw-r--r--pkg/systemd/services-list.jsx153
-rw-r--r--pkg/systemd/services.html17
-rw-r--r--pkg/systemd/services.jsx943
-rw-r--r--pkg/systemd/services.scss102
-rw-r--r--pkg/systemd/superuser-alert.jsx41
-rw-r--r--pkg/systemd/system-global.scss17
-rw-r--r--pkg/systemd/terminal.html16
-rw-r--r--pkg/systemd/terminal.jsx202
-rw-r--r--pkg/systemd/terminal.scss80
-rw-r--r--pkg/systemd/timer-dialog-helpers.js91
-rw-r--r--pkg/systemd/timer-dialog.jsx397
-rw-r--r--pkg/systemd/timers.scss15
-rw-r--r--pkg/users/account-create-dialog.js590
-rw-r--r--pkg/users/account-details.js477
-rw-r--r--pkg/users/account-logs-panel.jsx88
-rw-r--r--pkg/users/accounts-list.js489
-rw-r--r--pkg/users/authorized-keys-panel.js182
-rw-r--r--pkg/users/authorized-keys.js138
-rw-r--r--pkg/users/delete-account-dialog.js86
-rw-r--r--pkg/users/delete-group-dialog.js66
-rw-r--r--pkg/users/dialog-utils.js73
-rw-r--r--pkg/users/expiration-dialogs.js242
-rw-r--r--pkg/users/group-actions.jsx57
-rw-r--r--pkg/users/group-create-dialog.js157
-rw-r--r--pkg/users/index.html36
-rw-r--r--pkg/users/lock-account-dialog.js46
-rw-r--r--pkg/users/logout-account-dialog.js46
-rw-r--r--pkg/users/manifest.json19
-rw-r--r--pkg/users/mock/ssh/1-input7
-rw-r--r--pkg/users/mock/ssh/1-output4
-rw-r--r--pkg/users/mock/ssh/2-input0
-rw-r--r--pkg/users/mock/ssh/2-output0
-rw-r--r--pkg/users/mock/ssh/3-input1
-rw-r--r--pkg/users/mock/ssh/3-output2
-rw-r--r--pkg/users/mock/ssh/4-input1
-rw-r--r--pkg/users/mock/ssh/4-output2
-rw-r--r--pkg/users/password-dialogs.js289
-rw-r--r--pkg/users/rename-group-dialog.jsx85
-rw-r--r--pkg/users/shell-dialog.js102
-rw-r--r--pkg/users/ssh-add-public-key.sh16
-rw-r--r--pkg/users/ssh-list-public-keys.sh27
-rwxr-xr-xpkg/users/test-list-public-keys.sh29
-rwxr-xr-xpkg/users/users.js216
-rw-r--r--pkg/users/users.scss123
-rw-r--r--pkg/users/utils.js10
-rw-r--r--plans/all.fmf27
-rw-r--r--plans/podman.fmf29
-rw-r--r--po/LINGUAS20
-rwxr-xr-xpo/Makefile.am58
-rw-r--r--po/cs.po13854
-rw-r--r--po/de.po13184
-rw-r--r--po/es.po13230
-rw-r--r--po/fi.po12939
-rw-r--r--po/fr.po13412
-rw-r--r--po/he.po12992
-rw-r--r--po/it.po13733
-rw-r--r--po/its/polkit.its8
-rw-r--r--po/its/polkit.loc6
-rw-r--r--po/ja.po12844
-rw-r--r--po/ka.po10451
-rw-r--r--po/ko.po12810
-rw-r--r--po/nb_NO.po12807
-rw-r--r--po/nl.po13233
-rw-r--r--po/pl.po13285
-rw-r--r--po/pt_BR.po13318
-rw-r--r--po/ru.po13636
-rw-r--r--po/sk.po12369
-rw-r--r--po/sv.po12578
-rw-r--r--po/tr.po12594
-rw-r--r--po/uk.po13122
-rw-r--r--po/zh_CN.po12557
-rw-r--r--pyproject.toml156
-rw-r--r--selinux/Makefile.am35
-rw-r--r--selinux/cockpit.fc20
-rw-r--r--selinux/cockpit.if279
-rw-r--r--selinux/cockpit.te210
-rw-r--r--selinux/cockpit_session_selinux.8cockpit255
-rw-r--r--selinux/cockpit_ws_selinux.8cockpit207
-rw-r--r--src/Makefile.am89
-rw-r--r--src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in17
-rw-r--r--src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in19
-rw-r--r--src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in17
-rw-r--r--src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in22
-rw-r--r--src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in17
-rw-r--r--src/branding/arch/Makefile.am10
-rw-r--r--src/branding/arch/branding.css26
-rw-r--r--src/branding/centos/Makefile.am11
-rw-r--r--src/branding/centos/branding.css26
-rw-r--r--src/branding/debian/Makefile.am10
-rw-r--r--src/branding/debian/branding.css26
-rw-r--r--src/branding/default/Makefile.am12
-rw-r--r--src/branding/default/apple-touch-icon.pngbin0 -> 5543 bytes
-rw-r--r--src/branding/default/bg-plain.jpgbin0 -> 81737 bytes
-rw-r--r--src/branding/default/brand-large.pngbin0 -> 1152 bytes
-rw-r--r--src/branding/default/branding.css33
-rw-r--r--src/branding/default/favicon.icobin0 -> 1150 bytes
-rw-r--r--src/branding/default/logo.pngbin0 -> 2970 bytes
-rwxr-xr-xsrc/branding/default/logo.svg123
-rw-r--r--src/branding/fedora/Makefile.am11
-rw-r--r--src/branding/fedora/branding.css26
-rw-r--r--src/branding/kubernetes/Makefile.am5
-rw-r--r--src/branding/kubernetes/branding.css30
-rw-r--r--src/branding/opensuse/Makefile.am10
-rw-r--r--src/branding/opensuse/branding.css30
-rw-r--r--src/branding/registry/Makefile.am5
-rw-r--r--src/branding/rhel/Makefile.am11
-rw-r--r--src/branding/rhel/branding.css36
-rw-r--r--src/branding/scientific/Makefile.am12
-rw-r--r--src/branding/scientific/branding.css25
-rw-r--r--src/branding/ubuntu/Makefile.am10
-rw-r--r--src/branding/ubuntu/branding.css30
-rw-r--r--src/branding/ubuntu/favicon.icobin0 -> 9662 bytes
-rw-r--r--src/bridge/Makefile.am316
-rw-r--r--src/bridge/askpass.c230
-rw-r--r--src/bridge/bridge.c647
-rw-r--r--src/bridge/cockpit.pam.insecure10
-rw-r--r--src/bridge/cockpitblocksamples.c118
-rw-r--r--src/bridge/cockpitblocksamples.h32
-rw-r--r--src/bridge/cockpitcgroupsamples.c289
-rw-r--r--src/bridge/cockpitcgroupsamples.h32
-rw-r--r--src/bridge/cockpitconnect.c657
-rw-r--r--src/bridge/cockpitconnect.h75
-rw-r--r--src/bridge/cockpitcpusamples.c233
-rw-r--r--src/bridge/cockpitcpusamples.h33
-rw-r--r--src/bridge/cockpitdbuscache.c2131
-rw-r--r--src/bridge/cockpitdbuscache.h100
-rw-r--r--src/bridge/cockpitdbusconfig.c171
-rw-r--r--src/bridge/cockpitdbusinternal.c126
-rw-r--r--src/bridge/cockpitdbusinternal.h52
-rw-r--r--src/bridge/cockpitdbusjson.c3264
-rw-r--r--src/bridge/cockpitdbusjson.h37
-rw-r--r--src/bridge/cockpitdbusloginmessages.c97
-rw-r--r--src/bridge/cockpitdbusmachines.c253
-rw-r--r--src/bridge/cockpitdbusmeta.c483
-rw-r--r--src/bridge/cockpitdbusmeta.h36
-rw-r--r--src/bridge/cockpitdbusprocess.c166
-rw-r--r--src/bridge/cockpitdbusrules.c341
-rw-r--r--src/bridge/cockpitdbusrules.h53
-rw-r--r--src/bridge/cockpitdbususer.c210
-rw-r--r--src/bridge/cockpitdisksamples.c287
-rw-r--r--src/bridge/cockpitdisksamples.h33
-rw-r--r--src/bridge/cockpitechochannel.c89
-rw-r--r--src/bridge/cockpitechochannel.h31
-rw-r--r--src/bridge/cockpitfslist.c358
-rw-r--r--src/bridge/cockpitfslist.h38
-rw-r--r--src/bridge/cockpitfsread.c382
-rw-r--r--src/bridge/cockpitfsread.h42
-rw-r--r--src/bridge/cockpitfsreplace.c347
-rw-r--r--src/bridge/cockpitfsreplace.h38
-rw-r--r--src/bridge/cockpitfswatch.c294
-rw-r--r--src/bridge/cockpitfswatch.h45
-rw-r--r--src/bridge/cockpithttpstream.c1103
-rw-r--r--src/bridge/cockpithttpstream.h36
-rw-r--r--src/bridge/cockpitinteracttransport.c289
-rw-r--r--src/bridge/cockpitinteracttransport.h43
-rw-r--r--src/bridge/cockpitinternalmetrics.c570
-rw-r--r--src/bridge/cockpitinternalmetrics.h31
-rw-r--r--src/bridge/cockpitmemorysamples.c83
-rw-r--r--src/bridge/cockpitmemorysamples.h32
-rw-r--r--src/bridge/cockpitmetrics.c613
-rw-r--r--src/bridge/cockpitmetrics.h97
-rw-r--r--src/bridge/cockpitmountsamples.c94
-rw-r--r--src/bridge/cockpitmountsamples.h32
-rw-r--r--src/bridge/cockpitnetworksamples.c106
-rw-r--r--src/bridge/cockpitnetworksamples.h32
-rw-r--r--src/bridge/cockpitnullchannel.c72
-rw-r--r--src/bridge/cockpitnullchannel.h31
-rw-r--r--src/bridge/cockpitpackages.c1678
-rw-r--r--src/bridge/cockpitpackages.h59
-rw-r--r--src/bridge/cockpitpacketchannel.c679
-rw-r--r--src/bridge/cockpitpacketchannel.h37
-rw-r--r--src/bridge/cockpitpaths.c218
-rw-r--r--src/bridge/cockpitpaths.h59
-rw-r--r--src/bridge/cockpitpcp.c149
-rw-r--r--src/bridge/cockpitpcpmetrics.c1091
-rw-r--r--src/bridge/cockpitpcpmetrics.h31
-rw-r--r--src/bridge/cockpitpeer.c1172
-rw-r--r--src/bridge/cockpitpeer.h65
-rw-r--r--src/bridge/cockpitpipechannel.c635
-rw-r--r--src/bridge/cockpitpipechannel.h41
-rw-r--r--src/bridge/cockpitpolkitagent.c477
-rw-r--r--src/bridge/cockpitpolkitagent.h43
-rw-r--r--src/bridge/cockpitrouter.c1735
-rw-r--r--src/bridge/cockpitrouter.h77
-rw-r--r--src/bridge/cockpitsamples.c45
-rw-r--r--src/bridge/cockpitsamples.h53
-rw-r--r--src/bridge/cockpitstream.c950
-rw-r--r--src/bridge/cockpitstream.h65
-rw-r--r--src/bridge/cockpitwebsocketstream.c408
-rw-r--r--src/bridge/cockpitwebsocketstream.h26
-rw-r--r--src/bridge/mock-bridge.c352
-rw-r--r--src/bridge/mock-client.crt17
-rw-r--r--src/bridge/mock-client.key29
-rw-r--r--src/bridge/mock-pmda.c197
-rw-r--r--src/bridge/mock-pmns13
-rw-r--r--src/bridge/mock-resource/bad-bridges/cockpit/broken-bridge/manifest.json6
-rw-r--r--src/bridge/mock-resource/bad-bridges/cockpit/broken-bridges/manifest.json4
-rw-r--r--src/bridge/mock-resource/bad-bridges/cockpit/broken-environ/manifest.json9
-rw-r--r--src/bridge/mock-resource/bad-bridges/cockpit/broken-match/manifest.json8
-rw-r--r--src/bridge/mock-resource/bad-bridges/cockpit/broken-problem/manifest.json9
-rw-r--r--src/bridge/mock-resource/bad-bridges/cockpit/broken-spawn/manifest.json9
-rw-r--r--src/bridge/mock-resource/bad-bridges/cockpit/missing-match/manifest.json8
-rw-r--r--src/bridge/mock-resource/bad-directory/cockpit/ok/file.txt1
-rw-r--r--src/bridge/mock-resource/bad-directory/cockpit/ok/manifest.json1
-rw-r--r--src/bridge/mock-resource/bad-directory/cockpit/package/+dirname/file.txt1
-rw-r--r--src/bridge/mock-resource/bad-directory/cockpit/package/manifest.json1
-rw-r--r--src/bridge/mock-resource/bad-file/cockpit/ok/file.txt1
-rw-r--r--src/bridge/mock-resource/bad-file/cockpit/ok/manifest.json1
-rw-r--r--src/bridge/mock-resource/bad-file/cockpit/package/+file.txt1
-rw-r--r--src/bridge/mock-resource/bad-file/cockpit/package/.txt1
-rw-r--r--src/bridge/mock-resource/bad-file/cockpit/package/manifest.json1
-rw-r--r--src/bridge/mock-resource/bad-package/cockpit/%%module/manifest.json1
-rw-r--r--src/bridge/mock-resource/bad-package/cockpit/ok/file.txt1
-rw-r--r--src/bridge/mock-resource/bad-package/cockpit/ok/manifest.json1
-rw-r--r--src/bridge/mock-resource/config-override/second.override.json4
-rw-r--r--src/bridge/mock-resource/csp/cockpit/strip/manifest.json3
-rw-r--r--src/bridge/mock-resource/csp/cockpit/strip/test.html6
-rw-r--r--src/bridge/mock-resource/glob/cockpit/a/file.txt1
-rw-r--r--src/bridge/mock-resource/glob/cockpit/a/manifest.json1
-rw-r--r--src/bridge/mock-resource/glob/cockpit/b/file.txt.gzbin0 -> 22 bytes
-rw-r--r--src/bridge/mock-resource/glob/cockpit/b/manifest.json1
-rw-r--r--src/bridge/mock-resource/glob/cockpit/c/manifest.json1
-rw-r--r--src/bridge/mock-resource/gzip/cockpit/package/file.txt.gzbin0 -> 9377 bytes
-rw-r--r--src/bridge/mock-resource/gzip/cockpit/package/manifest.json1
-rw-r--r--src/bridge/mock-resource/home/cockpit/another-renamed/manifest.json10
-rw-r--r--src/bridge/mock-resource/home/cockpit/another-renamed/test-file.min.txt.gzbin0 -> 52 bytes
-rw-r--r--src/bridge/mock-resource/home/cockpit/another-renamed/test.de.html6
-rw-r--r--src/bridge/mock-resource/home/cockpit/another-renamed/test.html6
-rw-r--r--src/bridge/mock-resource/home/cockpit/another-renamed/test.pig.html6
-rw-r--r--src/bridge/mock-resource/home/cockpit/another-renamed/test.pig_PEN.html6
-rw-r--r--src/bridge/mock-resource/home/cockpit/incompatible/manifest.json6
-rw-r--r--src/bridge/mock-resource/home/cockpit/incompatible/test.html6
-rw-r--r--src/bridge/mock-resource/home/cockpit/requires/manifest.json6
-rw-r--r--src/bridge/mock-resource/home/cockpit/requires/test.html6
-rw-r--r--src/bridge/mock-resource/reload.new/cockpit/new/manifest.json1
-rw-r--r--src/bridge/mock-resource/reload.new/cockpit/old/manifest.json2
-rw-r--r--src/bridge/mock-resource/reload.old/cockpit/old/manifest.json2
-rw-r--r--src/bridge/mock-resource/reload.updated/cockpit/old/manifest.json3
-rw-r--r--src/bridge/mock-resource/system/cockpit/another/manifest.json3
-rw-r--r--src/bridge/mock-resource/system/cockpit/another/test.html6
-rw-r--r--src/bridge/mock-resource/system/cockpit/second/manifest.json10
-rw-r--r--src/bridge/mock-resource/system/cockpit/second/test.html9
-rw-r--r--src/bridge/mock-resource/system/cockpit/static/branding.css3
-rw-r--r--src/bridge/mock-resource/system/cockpit/test-priority/_modules/@testorg/toolkit.js2
-rw-r--r--src/bridge/mock-resource/system/cockpit/test-priority/manifest.json17
-rw-r--r--src/bridge/mock-resource/system/cockpit/test-priority/override.json4
-rw-r--r--src/bridge/mock-resource/system/cockpit/test-priority/sub/COPYING502
-rw-r--r--src/bridge/mock-resource/system/cockpit/test-priority/sub/file.ext2
-rw-r--r--src/bridge/mock-resource/system/cockpit/test-priority/sub/file.min.ext1
-rw-r--r--src/bridge/mock-resource/system/cockpit/test/manifest.json4
-rw-r--r--src/bridge/mock-resource/system/cockpit/test/sub/COPYING502
-rw-r--r--src/bridge/mock-server.crt18
-rw-r--r--src/bridge/mock-server.key29
-rw-r--r--src/bridge/org.cockpit-project.cockpit-bridge.policy.in19
-rw-r--r--src/bridge/pmlogconf/cockpit9
-rw-r--r--src/bridge/test-bridge.c309
-rw-r--r--src/bridge/test-connect.c466
-rw-r--r--src/bridge/test-dbus-meta.c760
-rw-r--r--src/bridge/test-fs.c1075
-rw-r--r--src/bridge/test-httpstream.c904
-rw-r--r--src/bridge/test-metrics.c998
-rw-r--r--src/bridge/test-packages.c1360
-rw-r--r--src/bridge/test-packet-channel.c645
-rw-r--r--src/bridge/test-paths.c246
-rw-r--r--src/bridge/test-pcp-archives.c439
-rw-r--r--src/bridge/test-pcp.c639
-rw-r--r--src/bridge/test-peer.c618
-rw-r--r--src/bridge/test-pipe-channel.c970
-rw-r--r--src/bridge/test-process.c154
-rw-r--r--src/bridge/test-router.c895
-rw-r--r--src/bridge/test-rules.c278
-rw-r--r--src/bridge/test-stream.c1070
-rw-r--r--src/bridge/test-websocketstream.c401
-rw-r--r--src/build_backend.py140
-rw-r--r--src/client/Makefile.am35
-rwxr-xr-xsrc/client/cockpit-client344
-rw-r--r--src/client/cockpit-client-symbolic.svg48
-rw-r--r--src/client/cockpit-client.svg74
-rwxr-xr-xsrc/client/cockpit-client.ui99
-rw-r--r--src/client/org.cockpit_project.CockpitClient.desktop7
-rw-r--r--src/client/org.cockpit_project.CockpitClient.metainfo.xml62
-rw-r--r--src/client/org.cockpit_project.CockpitClient.service3
-rw-r--r--src/cockpit/__init__.py1
-rw-r--r--src/cockpit/_vendor/__init__.py0
-rw-r--r--src/cockpit/_vendor/bei/__init__.py0
-rw-r--r--src/cockpit/_vendor/bei/beiboot.py160
-rw-r--r--src/cockpit/_vendor/bei/beipack.py217
-rw-r--r--src/cockpit/_vendor/bei/bootloader.py101
-rw-r--r--src/cockpit/_vendor/bei/data/__init__.py13
-rw-r--r--src/cockpit/_vendor/bei/data/beipack_loader.py85
-rw-r--r--src/cockpit/_vendor/bei/spawn.py37
-rw-r--r--src/cockpit/_vendor/bei/tmpfs.py21
-rw-r--r--src/cockpit/_vendor/ferny/__init__.py61
-rw-r--r--src/cockpit/_vendor/ferny/askpass.py4
-rw-r--r--src/cockpit/_vendor/ferny/interaction_agent.py421
-rwxr-xr-xsrc/cockpit/_vendor/ferny/interaction_client.py41
-rw-r--r--src/cockpit/_vendor/ferny/py.typed0
-rw-r--r--src/cockpit/_vendor/ferny/session.py198
-rw-r--r--src/cockpit/_vendor/ferny/ssh_askpass.py199
-rw-r--r--src/cockpit/_vendor/ferny/ssh_errors.py127
-rw-r--r--src/cockpit/_vendor/ferny/transport.py396
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/__init__.py39
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/bus.py861
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/bustypes.py551
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/event.py138
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/inotify.py74
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/introspection.py92
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/librarywrapper.py214
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/libsystemd.py334
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/pathwatch.py282
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/py.typed0
-rw-r--r--src/cockpit/_vendor/systemd_ctypes/typing.py58
-rw-r--r--src/cockpit/_version.py1
-rw-r--r--src/cockpit/beiboot.py340
-rw-r--r--src/cockpit/beipack.py76
-rw-r--r--src/cockpit/bridge.py315
-rw-r--r--src/cockpit/channel.py527
-rw-r--r--src/cockpit/channels/__init__.py40
-rw-r--r--src/cockpit/channels/dbus.py520
-rw-r--r--src/cockpit/channels/filesystem.py549
-rw-r--r--src/cockpit/channels/http.py158
-rw-r--r--src/cockpit/channels/metrics.py185
-rw-r--r--src/cockpit/channels/packages.py101
-rw-r--r--src/cockpit/channels/stream.py120
-rw-r--r--src/cockpit/channels/trivial.py46
-rw-r--r--src/cockpit/config.py89
-rw-r--r--src/cockpit/data/__init__.py18
-rw-r--r--src/cockpit/data/fail.html44
-rw-r--r--src/cockpit/internal_endpoints.py157
-rw-r--r--src/cockpit/jsonutil.py180
-rw-r--r--src/cockpit/packages.py580
-rw-r--r--src/cockpit/peer.py330
-rw-r--r--src/cockpit/polkit.py171
-rw-r--r--src/cockpit/polyfills.py57
-rw-r--r--src/cockpit/protocol.py248
-rw-r--r--src/cockpit/remote.py233
-rw-r--r--src/cockpit/router.py266
-rw-r--r--src/cockpit/samples.py438
-rw-r--r--src/cockpit/superuser.py245
-rw-r--r--src/cockpit/transports.py552
-rw-r--r--src/common/Makefile-common.am219
-rw-r--r--src/common/cockpitauthorize.c598
-rw-r--r--src/common/cockpitauthorize.h54
-rw-r--r--src/common/cockpitbase64.c263
-rw-r--r--src/common/cockpitbase64.h60
-rw-r--r--src/common/cockpitchannel.c1177
-rw-r--r--src/common/cockpitchannel.h90
-rw-r--r--src/common/cockpitclosefrom.c85
-rw-r--r--src/common/cockpitconf.c391
-rw-r--r--src/common/cockpitconf.h53
-rw-r--r--src/common/cockpitcontrolmessages.c98
-rw-r--r--src/common/cockpitcontrolmessages.h52
-rw-r--r--src/common/cockpiterror.c32
-rw-r--r--src/common/cockpiterror.h53
-rw-r--r--src/common/cockpitfdpassing.c156
-rw-r--r--src/common/cockpitfdpassing.h37
-rw-r--r--src/common/cockpitflow.c96
-rw-r--r--src/common/cockpitflow.h45
-rw-r--r--src/common/cockpitframe.c302
-rw-r--r--src/common/cockpitframe.h40
-rw-r--r--src/common/cockpithacks-glib.h80
-rw-r--r--src/common/cockpithacks.h52
-rw-r--r--src/common/cockpithash.c41
-rw-r--r--src/common/cockpithash.h34
-rw-r--r--src/common/cockpithex.c86
-rw-r--r--src/common/cockpithex.h32
-rw-r--r--src/common/cockpitjson.c1131
-rw-r--r--src/common/cockpitjson.h113
-rw-r--r--src/common/cockpitjsonprint.c247
-rw-r--r--src/common/cockpitjsonprint.h47
-rw-r--r--src/common/cockpitlocale.c111
-rw-r--r--src/common/cockpitlocale.h35
-rw-r--r--src/common/cockpitloopback.c147
-rw-r--r--src/common/cockpitloopback.h34
-rw-r--r--src/common/cockpitmachinesjson.c237
-rw-r--r--src/common/cockpitmachinesjson.h39
-rw-r--r--src/common/cockpitmemfdread.c168
-rw-r--r--src/common/cockpitmemfdread.h43
-rw-r--r--src/common/cockpitmemory.c143
-rw-r--r--src/common/cockpitmemory.h47
-rw-r--r--src/common/cockpitpipe.c1855
-rw-r--r--src/common/cockpitpipe.h112
-rw-r--r--src/common/cockpitpipetransport.c393
-rw-r--r--src/common/cockpitpipetransport.h43
-rw-r--r--src/common/cockpitsocket.c90
-rw-r--r--src/common/cockpitsocket.h30
-rw-r--r--src/common/cockpitsystem.c211
-rw-r--r--src/common/cockpitsystem.h42
-rw-r--r--src/common/cockpittemplate.c216
-rw-r--r--src/common/cockpittemplate.h41
-rw-r--r--src/common/cockpittransport.c530
-rw-r--r--src/common/cockpittransport.h111
-rw-r--r--src/common/cockpitunicode.c76
-rw-r--r--src/common/cockpitunicode.h33
-rw-r--r--src/common/cockpitunixsignal.c120
-rw-r--r--src/common/cockpitunixsignal.h31
-rw-r--r--src/common/cockpitversion.c76
-rw-r--r--src/common/cockpitversion.h32
-rw-r--r--src/common/cockpitwebcertificate.c142
-rw-r--r--src/common/cockpitwebcertificate.h28
-rw-r--r--src/common/cockpitwebfilter.c63
-rw-r--r--src/common/cockpitwebfilter.h46
-rw-r--r--src/common/cockpitwebinject.c224
-rw-r--r--src/common/cockpitwebinject.h36
-rw-r--r--src/common/cockpitwebrequest-private.h24
-rw-r--r--src/common/cockpitwebresponse.c1950
-rw-r--r--src/common/cockpitwebresponse.h157
-rw-r--r--src/common/cockpitwebserver.c1437
-rw-r--r--src/common/cockpitwebserver.h139
-rw-r--r--src/common/fail.html44
-rw-r--r--src/common/mock-content/directory/.empty0
-rw-r--r--src/common/mock-content/large.min.js.gzbin0 -> 29215 bytes
-rw-r--r--src/common/mock-content/test-file.txt1
-rw-r--r--src/common/mock-content/test-file.txt.gzbin0 -> 52 bytes
-rw-r--r--src/common/mock-content/test-file.zh_CN.txt1
-rw-r--r--src/common/mock-content/test.css3
-rw-r--r--src/common/mock-content/test.wasm1
-rwxr-xr-xsrc/common/mock-stderr7
-rw-r--r--src/common/preload-temp-home.c62
-rw-r--r--src/common/test-authorize.c438
-rw-r--r--src/common/test-base64.c207
-rw-r--r--src/common/test-channel.c806
-rw-r--r--src/common/test-config.c167
-rw-r--r--src/common/test-frame.c204
-rw-r--r--src/common/test-hash.c53
-rw-r--r--src/common/test-hex.c82
-rw-r--r--src/common/test-json.c893
-rw-r--r--src/common/test-jsonfds.c962
-rw-r--r--src/common/test-locale.c200
-rw-r--r--src/common/test-pipe.c1360
-rw-r--r--src/common/test-system.c148
-rw-r--r--src/common/test-template.c200
-rw-r--r--src/common/test-transport.c799
-rw-r--r--src/common/test-unicode.c112
-rw-r--r--src/common/test-unixsignal.c69
-rw-r--r--src/common/test-version.c95
-rw-r--r--src/common/test-webcertificate.c125
-rw-r--r--src/common/test-webresponse.c1558
-rw-r--r--src/common/test-webserver.c1184
-rw-r--r--src/pam-ssh-add/Makefile.am39
-rwxr-xr-xsrc/pam-ssh-add/mock-environment12
-rwxr-xr-xsrc/pam-ssh-add/mock-ssh-add40
-rwxr-xr-xsrc/pam-ssh-add/mock-ssh-agent17
-rw-r--r--src/pam-ssh-add/pam-ssh-add.c1004
-rw-r--r--src/pam-ssh-add/pam-ssh-add.h48
-rw-r--r--src/pam-ssh-add/test-ssh-add.c426
-rw-r--r--src/session/Makefile-session.am28
-rw-r--r--src/session/client-certificate.c355
-rw-r--r--src/session/client-certificate.h23
-rw-r--r--src/session/session-utils.c606
-rw-r--r--src/session/session-utils.h78
-rw-r--r--src/session/session.c1056
-rw-r--r--src/ssh/Makefile-ssh.am86
-rw-r--r--src/ssh/cockpitsshoptions.c127
-rw-r--r--src/ssh/cockpitsshoptions.h41
-rw-r--r--src/ssh/cockpitsshrelay.c2303
-rw-r--r--src/ssh/cockpitsshrelay.h48
-rw-r--r--src/ssh/invalid_known_hosts1
-rw-r--r--src/ssh/manifest.json33
-rw-r--r--src/ssh/mock-config/cockpit/cockpit.conf2
-rwxr-xr-xsrc/ssh/mock-pid-cat7
-rw-r--r--src/ssh/mock-sshd.c1096
-rw-r--r--src/ssh/mock_ecdsa_key9
-rw-r--r--src/ssh/mock_known_hosts1
-rw-r--r--src/ssh/mock_rsa_key27
-rw-r--r--src/ssh/ssh.c105
-rw-r--r--src/ssh/test-sshbridge.c1485
-rw-r--r--src/ssh/test-sshoptions.c150
-rw-r--r--src/ssh/test_rsa27
-rw-r--r--src/ssh/test_rsa.pub1
-rw-r--r--src/systemd/Makefile.am57
-rw-r--r--src/systemd/cockpit-motd.service.in9
-rw-r--r--src/systemd/cockpit-session.socket.in9
-rw-r--r--src/systemd/cockpit-session@.service.in9
-rw-r--r--src/systemd/cockpit-tempfiles.conf.in3
-rw-r--r--src/systemd/cockpit-wsinstance-http.service.in9
-rw-r--r--src/systemd/cockpit-wsinstance-http.socket.in9
-rw-r--r--src/systemd/cockpit-wsinstance-https-factory.socket.in10
-rw-r--r--src/systemd/cockpit-wsinstance-https-factory@.service.in7
-rw-r--r--src/systemd/cockpit-wsinstance-https@.service.in10
-rw-r--r--src/systemd/cockpit-wsinstance-https@.socket.in13
-rw-r--r--src/systemd/cockpit.service.in23
-rw-r--r--src/systemd/cockpit.socket.in13
-rw-r--r--src/systemd/inactive.motd2
-rw-r--r--src/systemd/system-cockpithttps.slice9
-rw-r--r--src/systemd/update-motd24
-rw-r--r--src/testlib/Makefile.am60
-rw-r--r--src/testlib/cockpittest.c621
-rw-r--r--src/testlib/cockpittest.h175
-rw-r--r--src/testlib/mock-auth.c70
-rw-r--r--src/testlib/mock-auth.h35
-rw-r--r--src/testlib/mock-channel.c75
-rw-r--r--src/testlib/mock-channel.h36
-rw-r--r--src/testlib/mock-pressure.c54
-rw-r--r--src/testlib/mock-pressure.h33
-rw-r--r--src/testlib/mock-transport.c221
-rw-r--r--src/testlib/mock-transport.h56
-rw-r--r--src/testlib/retest.c422
-rw-r--r--src/testlib/retest.h151
-rw-r--r--src/tls/Makefile-tls.am102
-rw-r--r--src/tls/ca/alice-expired.pem19
-rw-r--r--src/tls/ca/alice.key27
-rw-r--r--src/tls/ca/alice.p12bin0 -> 2373 bytes
-rw-r--r--src/tls/ca/alice.pem19
-rw-r--r--src/tls/ca/bob.key27
-rw-r--r--src/tls/ca/bob.p12bin0 -> 2373 bytes
-rw-r--r--src/tls/ca/bob.pem19
-rw-r--r--src/tls/ca/ca.conf30
-rw-r--r--src/tls/ca/ca.key28
-rw-r--r--src/tls/ca/ca.pem19
-rwxr-xr-xsrc/tls/ca/generate.sh27
-rw-r--r--src/tls/certificate.c96
-rw-r--r--src/tls/certificate.h37
-rw-r--r--src/tls/client-certificate.c381
-rw-r--r--src/tls/client-certificate.h36
-rw-r--r--src/tls/cockpit-certificate-ensure.c442
-rw-r--r--src/tls/cockpit-certificate-helper.in186
-rw-r--r--src/tls/connection.c914
-rw-r--r--src/tls/connection.h43
-rw-r--r--src/tls/httpredirect.c174
-rw-r--r--src/tls/httpredirect.h23
-rw-r--r--src/tls/main.c139
-rw-r--r--src/tls/server.c375
-rw-r--r--src/tls/server.h47
-rw-r--r--src/tls/socket-activation-helper.c324
-rw-r--r--src/tls/socket-io.c419
-rw-r--r--src/tls/socket-io.h53
-rw-r--r--src/tls/test-cockpit-certificate-ensure.c494
-rw-r--r--src/tls/test-connection.c31
-rw-r--r--src/tls/test-server.c827
-rwxr-xr-xsrc/tls/test-socket-activation-helper.sh74
-rw-r--r--src/tls/testing.h23
-rw-r--r--src/tls/utils.h57
-rw-r--r--src/tls/wsinstance-factory.c152
-rw-r--r--src/tls/wsinstance-start.c70
-rw-r--r--src/websocket/Makefile-websocket.am65
-rw-r--r--src/websocket/frob-websocket.c164
-rw-r--r--src/websocket/test-websocket.c1334
-rw-r--r--src/websocket/websocket.c517
-rw-r--r--src/websocket/websocket.h88
-rw-r--r--src/websocket/websocketclient.c567
-rw-r--r--src/websocket/websocketclient.h54
-rw-r--r--src/websocket/websocketconnection.c1930
-rw-r--r--src/websocket/websocketconnection.h80
-rw-r--r--src/websocket/websocketprivate.h79
-rw-r--r--src/websocket/websocketserver.c493
-rw-r--r--src/websocket/websocketserver.h46
-rw-r--r--src/ws/Makefile-ws.am204
-rw-r--r--src/ws/cockpit-desktop.in139
-rw-r--r--src/ws/cockpit.appdata.xml.in46
-rw-r--r--src/ws/cockpit.pngbin0 -> 5459 bytes
-rw-r--r--src/ws/cockpitauth.c1708
-rw-r--r--src/ws/cockpitauth.h113
-rw-r--r--src/ws/cockpitbranding.c211
-rw-r--r--src/ws/cockpitbranding.h41
-rw-r--r--src/ws/cockpitchannelresponse.c789
-rw-r--r--src/ws/cockpitchannelresponse.h41
-rw-r--r--src/ws/cockpitchannelsocket.c233
-rw-r--r--src/ws/cockpitchannelsocket.h36
-rw-r--r--src/ws/cockpitcompat.c149
-rw-r--r--src/ws/cockpitcompat.h26
-rw-r--r--src/ws/cockpitcreds.c281
-rw-r--r--src/ws/cockpitcreds.h80
-rw-r--r--src/ws/cockpithandlers.c757
-rw-r--r--src/ws/cockpithandlers.h74
-rw-r--r--src/ws/cockpitwebservice.c1476
-rw-r--r--src/ws/cockpitwebservice.h87
-rw-r--r--src/ws/cockpitws.h45
-rw-r--r--src/ws/com.redhat.Cockpit.DBusTests.xml180
-rw-r--r--src/ws/main.c313
-rwxr-xr-xsrc/ws/mock-auth-command.c329
-rwxr-xr-xsrc/ws/mock-cat-with-init6
-rw-r--r--src/ws/mock-combined.crt54
-rw-r--r--src/ws/mock-config/cockpit/cockpit-alt.conf10
-rw-r--r--src/ws/mock-config/cockpit/cockpit-deprecated.conf12
-rw-r--r--src/ws/mock-config/cockpit/cockpit.conf50
-rw-r--r--src/ws/mock-ecc.crt13
-rw-r--r--src/ws/mock-ecc.key10
-rw-r--r--src/ws/mock-echo.c70
-rwxr-xr-xsrc/ws/mock-kdc86
-rw-r--r--src/ws/mock-kdc.conf.in18
-rw-r--r--src/ws/mock-krb5.conf.in19
-rw-r--r--src/ws/mock-pam-conv-mod.c95
-rwxr-xr-xsrc/ws/mock-pipes/cockpit-session3
-rwxr-xr-xsrc/ws/mock-pipes/exit1273
-rwxr-xr-xsrc/ws/mock-pipes/someprogram3
-rw-r--r--src/ws/mock-service.c839
-rw-r--r--src/ws/mock-service.h28
-rw-r--r--src/ws/mock-static/index.html8
-rw-r--r--src/ws/pam_cockpit_cert.c48
-rw-r--r--src/ws/test-auth.c1320
-rw-r--r--src/ws/test-authssh.c277
-rw-r--r--src/ws/test-channelresponse.c1161
-rw-r--r--src/ws/test-compat.c114
-rw-r--r--src/ws/test-creds.c201
-rw-r--r--src/ws/test-handlers.c907
-rw-r--r--src/ws/test-kerberos.c509
-rw-r--r--src/ws/test-server.c1003
-rw-r--r--src/ws/test-webservice.c1354
-rw-r--r--test/ARCHITECTURE.md256
-rw-r--r--test/README.md343
-rw-r--r--test/__init__.py0
-rwxr-xr-xtest/browser/browser.sh116
-rw-r--r--test/browser/main.fmf80
-rwxr-xr-xtest/browser/run-test.sh239
-rw-r--r--test/common/__init__.py0
-rw-r--r--test/common/cdp.py381
-rwxr-xr-xtest/common/chromium-cdp-driver.js346
-rwxr-xr-xtest/common/firefox-cdp-driver.js392
-rw-r--r--test/common/git-utils.sh150
-rwxr-xr-xtest/common/lcov.py505
-rw-r--r--test/common/link-patterns.json39
-rwxr-xr-xtest/common/make-bots28
-rw-r--r--test/common/netlib.py214
-rw-r--r--test/common/packagelib.py428
-rwxr-xr-xtest/common/pixel-tests261
-rw-r--r--test/common/pixeldiff.html623
-rwxr-xr-xtest/common/pywrap30
-rw-r--r--test/common/ruff.toml11
-rwxr-xr-xtest/common/run-tests585
-rw-r--r--test/common/storagelib.py669
-rwxr-xr-xtest/common/tap-cdp119
-rw-r--r--test/common/test-functions.js360
-rw-r--r--test/common/testlib.py2531
-rw-r--r--test/data/100years/0-self-signed.cert26
-rw-r--r--test/data/100years/0-self-signed.key28
-rw-r--r--test/data/README2
-rw-r--r--test/data/expired/0-self-signed.cert26
-rw-r--r--test/data/expired/0-self-signed.key28
-rw-r--r--test/data/expired/1.cert26
-rw-r--r--test/data/expired/1.key28
-rw-r--r--test/data/expired/combined.cert54
-rwxr-xr-xtest/example/check-example104
-rwxr-xr-xtest/example/check-retry24
-rw-r--r--test/example/ruff.toml6
-rwxr-xr-xtest/image-prepare278
-rw-r--r--test/ostree.install42
-rw-r--r--test/pytest/__init__.py0
-rw-r--r--test/pytest/conftest.py63
-rw-r--r--test/pytest/mockpeer.py46
-rw-r--r--test/pytest/mocktransport.py245
-rw-r--r--test/pytest/pseudo.py18
-rw-r--r--test/pytest/test_beiboot.py28
-rw-r--r--test/pytest/test_bridge.py1132
-rw-r--r--test/pytest/test_browser.py48
-rw-r--r--test/pytest/test_packages.py286
-rw-r--r--test/pytest/test_peer.py214
-rw-r--r--test/pytest/test_samples.py158
-rw-r--r--test/pytest/test_transport.py392
-rw-r--r--test/reference-image1
-rwxr-xr-xtest/run74
-rwxr-xr-xtest/static-code200
-rw-r--r--test/verify/__init__.py0
-rwxr-xr-xtest/verify/check-apps393
-rwxr-xr-xtest/verify/check-bots-api167
-rwxr-xr-xtest/verify/check-client246
-rwxr-xr-xtest/verify/check-connection1446
-rwxr-xr-xtest/verify/check-embed106
-rwxr-xr-xtest/verify/check-examples186
-rwxr-xr-xtest/verify/check-kdump710
-rwxr-xr-xtest/verify/check-loopback72
-rwxr-xr-xtest/verify/check-metrics1537
-rwxr-xr-xtest/verify/check-networkmanager-basic332
-rwxr-xr-xtest/verify/check-networkmanager-bond335
-rwxr-xr-xtest/verify/check-networkmanager-bridge125
-rwxr-xr-xtest/verify/check-networkmanager-checkpoints180
-rwxr-xr-xtest/verify/check-networkmanager-firewall567
-rwxr-xr-xtest/verify/check-networkmanager-mac105
-rwxr-xr-xtest/verify/check-networkmanager-mtu73
-rwxr-xr-xtest/verify/check-networkmanager-other58
-rwxr-xr-xtest/verify/check-networkmanager-settings221
-rwxr-xr-xtest/verify/check-networkmanager-team145
-rwxr-xr-xtest/verify/check-networkmanager-unmanaged45
-rwxr-xr-xtest/verify/check-networkmanager-vlan72
-rwxr-xr-xtest/verify/check-networkmanager-wireguard251
-rwxr-xr-xtest/verify/check-packagekit1756
-rwxr-xr-xtest/verify/check-packages113
-rwxr-xr-xtest/verify/check-pages868
-rwxr-xr-xtest/verify/check-reauthorize127
-rwxr-xr-xtest/verify/check-selinux353
-rwxr-xr-xtest/verify/check-session70
-rwxr-xr-xtest/verify/check-shell-active-pages101
-rwxr-xr-xtest/verify/check-shell-host-switching524
-rwxr-xr-xtest/verify/check-shell-keys355
-rwxr-xr-xtest/verify/check-shell-menu160
-rwxr-xr-xtest/verify/check-shell-multi-machine838
-rwxr-xr-xtest/verify/check-shell-multi-machine-key310
-rwxr-xr-xtest/verify/check-shell-multi-os247
-rwxr-xr-xtest/verify/check-sosreport191
-rwxr-xr-xtest/verify/check-static-login956
-rwxr-xr-xtest/verify/check-storage-anaconda487
-rwxr-xr-xtest/verify/check-storage-basic93
-rwxr-xr-xtest/verify/check-storage-btrfs596
-rwxr-xr-xtest/verify/check-storage-format194
-rwxr-xr-xtest/verify/check-storage-hidden153
-rwxr-xr-xtest/verify/check-storage-ignored52
-rwxr-xr-xtest/verify/check-storage-iscsi126
-rwxr-xr-xtest/verify/check-storage-luks720
-rwxr-xr-xtest/verify/check-storage-lvm2926
-rwxr-xr-xtest/verify/check-storage-mdraid352
-rwxr-xr-xtest/verify/check-storage-mounting691
-rwxr-xr-xtest/verify/check-storage-msdos88
-rwxr-xr-xtest/verify/check-storage-multipath98
-rwxr-xr-xtest/verify/check-storage-nfs361
-rwxr-xr-xtest/verify/check-storage-partitions231
-rwxr-xr-xtest/verify/check-storage-raid154
-rwxr-xr-xtest/verify/check-storage-resize346
-rwxr-xr-xtest/verify/check-storage-scaling50
-rwxr-xr-xtest/verify/check-storage-stratis980
-rwxr-xr-xtest/verify/check-storage-swap120
-rwxr-xr-xtest/verify/check-storage-unrecognized61
-rwxr-xr-xtest/verify/check-storage-unused83
-rwxr-xr-xtest/verify/check-storage-used195
-rwxr-xr-xtest/verify/check-storage-vdo436
-rwxr-xr-xtest/verify/check-superuser758
-rwxr-xr-xtest/verify/check-system-info1109
-rwxr-xr-xtest/verify/check-system-journal725
-rwxr-xr-xtest/verify/check-system-realms1244
-rwxr-xr-xtest/verify/check-system-services1630
-rwxr-xr-xtest/verify/check-system-shutdown-restart145
-rwxr-xr-xtest/verify/check-system-terminal266
-rwxr-xr-xtest/verify/check-system-tuned123
-rwxr-xr-xtest/verify/check-testlib290
-rwxr-xr-xtest/verify/check-users1289
-rwxr-xr-xtest/verify/check-users-roles126
-rwxr-xr-xtest/verify/check-ws-bastion261
-rw-r--r--test/verify/conftest.py80
-rw-r--r--test/verify/files/cert-chain.cert31
-rw-r--r--test/verify/files/cert-chain.key16
-rw-r--r--test/verify/files/cockpit_event.conf5
-rw-r--r--test/verify/files/dmi/DMIbin0 -> 3081 bytes
-rw-r--r--test/verify/files/dmi/smbios_entry_pointbin0 -> 24 bytes
-rw-r--r--test/verify/files/embed-cockpit/embed.css25
-rw-r--r--test/verify/files/embed-cockpit/embed.js31
-rw-r--r--test/verify/files/embed-cockpit/index.html96
-rw-r--r--test/verify/files/embed-cockpit/manifest.json12
-rw-r--r--test/verify/files/metrics-archives/2corescpu.tar.gzbin0 -> 223811 bytes
-rw-r--r--test/verify/files/metrics-archives/cpu_network.tar.gzbin0 -> 508286 bytes
-rw-r--r--test/verify/files/metrics-archives/disk.tar.gzbin0 -> 110431 bytes
-rw-r--r--test/verify/files/metrics-archives/double_events.zipbin0 -> 1197739 bytes
-rw-r--r--test/verify/files/metrics-archives/journal.journal.gzbin0 -> 446287 bytes
-rw-r--r--test/verify/files/metrics-archives/memory.tar.gzbin0 -> 625794 bytes
-rw-r--r--test/verify/files/metrics-archives/with_journal.tar.gzbin0 -> 87639 bytes
-rwxr-xr-xtest/verify/files/mock-bugzilla-server.py18
-rwxr-xr-xtest/verify/files/mock-faf-server.py65
-rwxr-xr-xtest/verify/files/mock-insights156
-rw-r--r--test/verify/files/ssh/id_ecdsa5
-rw-r--r--test/verify/files/ssh/id_ecdsa.pub1
-rw-r--r--test/verify/files/ssh/id_ed255198
-rw-r--r--test/verify/files/ssh/id_ed25519.pub1
-rw-r--r--test/verify/files/ssh/id_rsa30
-rw-r--r--test/verify/files/ssh/id_rsa.pub1
-rw-r--r--test/verify/files/test.pngbin0 -> 3166 bytes
-rw-r--r--test/verify/files/workflow_Cockpit.xml10
-rw-r--r--test/verify/ruff.toml7
-rw-r--r--test/vm.install57
-rw-r--r--tools/Makefile-tools.am25
-rw-r--r--tools/Makefile.redirect12
-rw-r--r--tools/README.dist13
-rw-r--r--tools/README.node_modules72
-rwxr-xr-xtools/adjust-distdir-timestamps48
-rw-r--r--tools/arch/PKGBUILD142
-rw-r--r--tools/arch/cockpit-ws.sysuser.conf1
-rw-r--r--tools/arch/cockpit-wsinstance.sysuser.conf1
-rw-r--r--tools/arch/cockpit.pam7
-rwxr-xr-xtools/build-debian-copyright145
-rw-r--r--tools/cockpit.debian.pam24
-rw-r--r--tools/cockpit.pam19
-rw-r--r--tools/cockpit.spec814
-rwxr-xr-xtools/compile348
-rw-r--r--tools/debian/changelog5
-rw-r--r--tools/debian/clean2
-rw-r--r--tools/debian/cockpit-bridge.install8
-rw-r--r--tools/debian/cockpit-doc.install1
-rw-r--r--tools/debian/cockpit-doc.lintian-overrides2
-rw-r--r--tools/debian/cockpit-networkmanager.install2
-rw-r--r--tools/debian/cockpit-packagekit.install2
-rw-r--r--tools/debian/cockpit-pcp.install3
-rw-r--r--tools/debian/cockpit-sosreport.install3
-rw-r--r--tools/debian/cockpit-storaged.install2
-rw-r--r--tools/debian/cockpit-system.install4
-rw-r--r--tools/debian/cockpit-tests.install3
-rw-r--r--tools/debian/cockpit-ws.install32
-rw-r--r--tools/debian/cockpit-ws.lintian-overrides5
-rw-r--r--tools/debian/cockpit-ws.postinst39
-rw-r--r--tools/debian/cockpit-ws.postrm11
-rw-r--r--tools/debian/cockpit.install3
-rw-r--r--tools/debian/control193
-rw-r--r--tools/debian/copyright301
-rw-r--r--tools/debian/copyright.template212
-rwxr-xr-xtools/debian/rules71
-rw-r--r--tools/debian/source/format1
-rw-r--r--tools/debian/source/lintian-overrides8
-rw-r--r--tools/debian/source/options2
-rw-r--r--tools/debian/tests/control4
-rwxr-xr-xtools/debian/tests/smoke27
-rw-r--r--tools/debian/watch5
-rwxr-xr-xtools/depcomp791
-rwxr-xr-xtools/escape-to-c12
-rwxr-xr-xtools/fix-spec17
-rwxr-xr-xtools/git-hook-post-commit40
-rwxr-xr-xtools/git-hook-pre-push50
-rwxr-xr-xtools/git-hook-pre-rebase16
-rw-r--r--tools/glib.supp15
-rwxr-xr-xtools/install-sh541
-rwxr-xr-xtools/make-debs77
-rwxr-xr-xtools/make-dist56
-rwxr-xr-xtools/make-rpms71
-rwxr-xr-xtools/missing215
-rw-r--r--tools/mock-build-env/README.md5
-rw-r--r--tools/mock-build-env/gnutls.pc3
-rw-r--r--tools/mock-build-env/json-glib-1.0.pc3
-rw-r--r--tools/mock-build-env/krb5-gssapi.pc3
-rw-r--r--tools/mock-build-env/krb5.pc3
-rw-r--r--tools/mock-build-env/security/pam_appl.h1
-rwxr-xr-xtools/node-modules198
-rwxr-xr-xtools/patch-metainfo35
-rwxr-xr-xtools/termschutz29
-rwxr-xr-xtools/test-driver153
-rwxr-xr-xtools/urls-check144
-rw-r--r--tools/vulture-suppressions/cockpit.py16
-rw-r--r--tools/vulture-suppressions/ferny.py4
-rw-r--r--tools/vulture-suppressions/pytest.py3
-rw-r--r--tools/vulture-suppressions/ruff.toml6
-rw-r--r--tools/vulture-suppressions/stdlib.py22
-rw-r--r--tools/vulture-suppressions/testlib.py8
-rwxr-xr-xtools/webpack-make.js3
-rwxr-xr-xtools/webpack-watch3
-rw-r--r--version.m41
2088 files changed, 606298 insertions, 0 deletions
diff --git a/.extra_dist b/.extra_dist
new file mode 100644
index 0000000..76d9aa1
--- /dev/null
+++ b/.extra_dist
@@ -0,0 +1,656 @@
+.fmf/version
+pkg/Makefile.am
+pkg/Makefile.qunit
+pkg/apps/application-list.jsx
+pkg/apps/application.jsx
+pkg/apps/application.scss
+pkg/apps/apps.jsx
+pkg/apps/appstream.js
+pkg/apps/content-security-policy.override
+pkg/apps/default.png
+pkg/apps/index.html
+pkg/apps/manifest.json
+pkg/apps/packagekit.js
+pkg/apps/utils.jsx
+pkg/apps/watch-appstream.py
+pkg/base1/cockpit.js
+pkg/base1/manifest.json
+pkg/base1/test-base64.js
+pkg/base1/test-browser-storage.js
+pkg/base1/test-cache.js
+pkg/base1/test-chan.js
+pkg/base1/test-dbus-address.js
+pkg/base1/test-dbus-common.js
+pkg/base1/test-dbus-framed.js
+pkg/base1/test-dbus.js
+pkg/base1/test-echo.js
+pkg/base1/test-events.js
+pkg/base1/test-external.js
+pkg/base1/test-file.js
+pkg/base1/test-format.js
+pkg/base1/test-framed-cache.js
+pkg/base1/test-framed.js
+pkg/base1/test-http.js
+pkg/base1/test-journal-renderer.js
+pkg/base1/test-locale.js
+pkg/base1/test-location.js
+pkg/base1/test-metrics.js
+pkg/base1/test-no-jquery.js
+pkg/base1/test-permissions.js
+pkg/base1/test-promise.js
+pkg/base1/test-protocol.js
+pkg/base1/test-series.js
+pkg/base1/test-spawn-proc.js
+pkg/base1/test-spawn.js
+pkg/base1/test-stream.js
+pkg/base1/test-user.js
+pkg/base1/test-utf8.js
+pkg/base1/test-websocket.js
+pkg/kdump/config-client-suse.js
+pkg/kdump/config-client.js
+pkg/kdump/crashkernel.sh
+pkg/kdump/index.html
+pkg/kdump/kdump-client.js
+pkg/kdump/kdump-view.jsx
+pkg/kdump/kdump.js
+pkg/kdump/kdump.scss
+pkg/kdump/manifest.json
+pkg/kdump/test-config-client.js
+pkg/kdump/testwritable.sh
+pkg/lib/README
+pkg/lib/_global-variables.scss
+pkg/lib/cockpit-components-context-menu.jsx
+pkg/lib/cockpit-components-dialog.jsx
+pkg/lib/cockpit-components-dialog.scss
+pkg/lib/cockpit-components-dynamic-list.jsx
+pkg/lib/cockpit-components-dynamic-list.scss
+pkg/lib/cockpit-components-empty-state.css
+pkg/lib/cockpit-components-empty-state.jsx
+pkg/lib/cockpit-components-file-autocomplete.jsx
+pkg/lib/cockpit-components-firewalld-request.jsx
+pkg/lib/cockpit-components-firewalld-request.scss
+pkg/lib/cockpit-components-form-helper.jsx
+pkg/lib/cockpit-components-inline-notification.css
+pkg/lib/cockpit-components-inline-notification.jsx
+pkg/lib/cockpit-components-install-dialog.css
+pkg/lib/cockpit-components-install-dialog.jsx
+pkg/lib/cockpit-components-listing-panel.jsx
+pkg/lib/cockpit-components-listing-panel.scss
+pkg/lib/cockpit-components-logs-panel.jsx
+pkg/lib/cockpit-components-logs-panel.scss
+pkg/lib/cockpit-components-modifications.css
+pkg/lib/cockpit-components-modifications.jsx
+pkg/lib/cockpit-components-password.jsx
+pkg/lib/cockpit-components-password.scss
+pkg/lib/cockpit-components-plot.jsx
+pkg/lib/cockpit-components-plot.scss
+pkg/lib/cockpit-components-privileged.jsx
+pkg/lib/cockpit-components-shutdown.jsx
+pkg/lib/cockpit-components-shutdown.scss
+pkg/lib/cockpit-components-table.jsx
+pkg/lib/cockpit-components-table.scss
+pkg/lib/cockpit-components-terminal.jsx
+pkg/lib/cockpit-components-truncate.jsx
+pkg/lib/cockpit-components-truncate.scss
+pkg/lib/cockpit-dark-theme.js
+pkg/lib/cockpit-po-plugin.js
+pkg/lib/cockpit-rsync-plugin.js
+pkg/lib/cockpit.js
+pkg/lib/console.css
+pkg/lib/context-menu.scss
+pkg/lib/credentials-ssh-private-keys.sh
+pkg/lib/credentials-ssh-remove-key.sh
+pkg/lib/credentials.js
+pkg/lib/ct-card.scss
+pkg/lib/dialogs.jsx
+pkg/lib/esbuild-cleanup-plugin.js
+pkg/lib/esbuild-common.js
+pkg/lib/esbuild-compress-plugin.js
+pkg/lib/esbuild-test-html-plugin.js
+pkg/lib/get-timesync-backend.py
+pkg/lib/hooks.js
+pkg/lib/html2po.js
+pkg/lib/inotify.py
+pkg/lib/journal.css
+pkg/lib/journal.js
+pkg/lib/long-running-process.js
+pkg/lib/machine-info.js
+pkg/lib/manifest2po.js
+pkg/lib/menu-select-widget.scss
+pkg/lib/notifications.js
+pkg/lib/os-release.js
+pkg/lib/packagekit.js
+pkg/lib/page.scss
+pkg/lib/pam_user_parser.js
+pkg/lib/patternfly/_fonts.scss
+pkg/lib/patternfly/patternfly-5-cockpit.scss
+pkg/lib/patternfly/patternfly-5-overrides.scss
+pkg/lib/plot.js
+pkg/lib/polyfills.js
+pkg/lib/python.js
+pkg/lib/qunit-template.html.in
+pkg/lib/qunit-tests.js
+pkg/lib/serverTime.js
+pkg/lib/serverTime.scss
+pkg/lib/service.js
+pkg/lib/superuser.js
+pkg/lib/table.css
+pkg/lib/timeformat.js
+pkg/lib/utils.jsx
+pkg/metrics/index.html
+pkg/metrics/index.js
+pkg/metrics/manifest.json
+pkg/metrics/metrics.jsx
+pkg/metrics/metrics.scss
+pkg/networkmanager/bond.jsx
+pkg/networkmanager/bridge.jsx
+pkg/networkmanager/bridgeport.jsx
+pkg/networkmanager/dialogs-common.jsx
+pkg/networkmanager/firewall-client.js
+pkg/networkmanager/firewall-switch.jsx
+pkg/networkmanager/firewall.html
+pkg/networkmanager/firewall.jsx
+pkg/networkmanager/helpers.js
+pkg/networkmanager/index.html
+pkg/networkmanager/interfaces.js
+pkg/networkmanager/ip-settings.jsx
+pkg/networkmanager/mac.jsx
+pkg/networkmanager/manifest.json
+pkg/networkmanager/model-context.jsx
+pkg/networkmanager/mtu.jsx
+pkg/networkmanager/network-interface-members.jsx
+pkg/networkmanager/network-interface.jsx
+pkg/networkmanager/network-main.jsx
+pkg/networkmanager/networking.scss
+pkg/networkmanager/networkmanager.jsx
+pkg/networkmanager/plots.js
+pkg/networkmanager/team.jsx
+pkg/networkmanager/teamport.jsx
+pkg/networkmanager/test-utils.js
+pkg/networkmanager/utils.js
+pkg/networkmanager/vlan.jsx
+pkg/networkmanager/wireguard.jsx
+pkg/networkmanager/wireguard.scss
+pkg/packagekit/autoupdates.jsx
+pkg/packagekit/callTracer.py
+pkg/packagekit/history.jsx
+pkg/packagekit/index.html
+pkg/packagekit/kpatch.jsx
+pkg/packagekit/manifest.json
+pkg/packagekit/mock-updates.js
+pkg/packagekit/pf-security.woff
+pkg/packagekit/updates.jsx
+pkg/packagekit/updates.scss
+pkg/pcp/manifest.json
+pkg/playground/exception.html
+pkg/playground/exception.js
+pkg/playground/hammer.gif
+pkg/playground/index.html
+pkg/playground/index.js
+pkg/playground/journal.html
+pkg/playground/journal.jsx
+pkg/playground/manifest.json
+pkg/playground/metrics.html
+pkg/playground/metrics.js
+pkg/playground/notifications-receiver.html
+pkg/playground/notifications-receiver.js
+pkg/playground/pkgs.html
+pkg/playground/pkgs.js
+pkg/playground/plot.css
+pkg/playground/plot.html
+pkg/playground/plot.js
+pkg/playground/preloaded.html
+pkg/playground/preloaded.js
+pkg/playground/react-demo-cards.jsx
+pkg/playground/react-demo-dialog.jsx
+pkg/playground/react-demo-file-autocomplete.jsx
+pkg/playground/react-patterns.html
+pkg/playground/react-patterns.js
+pkg/playground/service.html
+pkg/playground/service.js
+pkg/playground/speed.css
+pkg/playground/speed.html
+pkg/playground/speed.js
+pkg/playground/test.html
+pkg/playground/test.js
+pkg/playground/translate.html
+pkg/playground/translate.js
+pkg/ruff.toml
+pkg/selinux/index.html
+pkg/selinux/manifest.json
+pkg/selinux/selinux-client.js
+pkg/selinux/selinux.js
+pkg/selinux/setroubleshoot-client.js
+pkg/selinux/setroubleshoot-view.jsx
+pkg/selinux/setroubleshoot.scss
+pkg/shell/active-pages-modal.jsx
+pkg/shell/base_index.js
+pkg/shell/credentials.jsx
+pkg/shell/credentials.scss
+pkg/shell/failures.jsx
+pkg/shell/hosts.jsx
+pkg/shell/hosts_dialog.jsx
+pkg/shell/images/bg-plain.jpg
+pkg/shell/images/cockpit-icon.svg
+pkg/shell/images/server-error.png
+pkg/shell/images/server-large.png
+pkg/shell/images/server-small.png
+pkg/shell/index.html
+pkg/shell/indexes.jsx
+pkg/shell/machines/machines.js
+pkg/shell/machines/ssh-add-key.sh
+pkg/shell/machines/ssh-show-default-key.sh
+pkg/shell/machines/test-machines.js
+pkg/shell/manifest.json
+pkg/shell/nav.jsx
+pkg/shell/nav.scss
+pkg/shell/shell-modals.jsx
+pkg/shell/shell.html
+pkg/shell/shell.js
+pkg/shell/shell.scss
+pkg/shell/superuser.jsx
+pkg/shell/topnav.jsx
+pkg/sosreport/cockpit-sosreport.png
+pkg/sosreport/index.html
+pkg/sosreport/manifest.json
+pkg/sosreport/sosreport.jsx
+pkg/sosreport/sosreport.png
+pkg/sosreport/sosreport.scss
+pkg/static/login.html
+pkg/static/login.js
+pkg/static/login.scss
+pkg/static/manifest.json
+pkg/storaged/anaconda.jsx
+pkg/storaged/block/create-pages.jsx
+pkg/storaged/block/format-dialog.jsx
+pkg/storaged/block/other.jsx
+pkg/storaged/block/resize.jsx
+pkg/storaged/block/unformatted-data.jsx
+pkg/storaged/block/unrecognized-data.jsx
+pkg/storaged/btrfs/device.jsx
+pkg/storaged/btrfs/filesystem.jsx
+pkg/storaged/btrfs/subvolume.jsx
+pkg/storaged/btrfs/utils.jsx
+pkg/storaged/btrfs/volume.jsx
+pkg/storaged/client.js
+pkg/storaged/crypto/actions.jsx
+pkg/storaged/crypto/clevis-luks-passphrase.sh
+pkg/storaged/crypto/encryption.jsx
+pkg/storaged/crypto/keyslots.jsx
+pkg/storaged/crypto/locked-encrypted-data.jsx
+pkg/storaged/crypto/luksmeta-monitor-hack.py
+pkg/storaged/crypto/tang.jsx
+pkg/storaged/dialog.jsx
+pkg/storaged/drive/drive.jsx
+pkg/storaged/filesystem/filesystem.jsx
+pkg/storaged/filesystem/mismounting.jsx
+pkg/storaged/filesystem/mounting-dialog.jsx
+pkg/storaged/filesystem/utils.jsx
+pkg/storaged/icons/gnome-icons.jsx
+pkg/storaged/index.html
+pkg/storaged/iscsi/create-dialog.jsx
+pkg/storaged/iscsi/session.jsx
+pkg/storaged/jobs-panel.jsx
+pkg/storaged/legacy-vdo/legacy-vdo.jsx
+pkg/storaged/legacy-vdo/vdo-monitor.py
+pkg/storaged/logs-panel.jsx
+pkg/storaged/lvm2/block-logical-volume.jsx
+pkg/storaged/lvm2/create-dialog.jsx
+pkg/storaged/lvm2/create-logical-volume-dialog.jsx
+pkg/storaged/lvm2/inactive-logical-volume.jsx
+pkg/storaged/lvm2/physical-volume.jsx
+pkg/storaged/lvm2/thin-pool-logical-volume.jsx
+pkg/storaged/lvm2/unsupported-logical-volume.jsx
+pkg/storaged/lvm2/utils.jsx
+pkg/storaged/lvm2/vdo-pool.jsx
+pkg/storaged/lvm2/volume-group.jsx
+pkg/storaged/manifest.json
+pkg/storaged/mdraid/create-dialog.jsx
+pkg/storaged/mdraid/mdraid-disk.jsx
+pkg/storaged/mdraid/mdraid.jsx
+pkg/storaged/mount-users.py
+pkg/storaged/multipath.jsx
+pkg/storaged/nfs/nfs-mounts.py
+pkg/storaged/nfs/nfs.jsx
+pkg/storaged/overview/overview.jsx
+pkg/storaged/pages.jsx
+pkg/storaged/partitions/actions.jsx
+pkg/storaged/partitions/format-disk-dialog.jsx
+pkg/storaged/partitions/partition-table.jsx
+pkg/storaged/partitions/partition.jsx
+pkg/storaged/plot.jsx
+pkg/storaged/storage-controls.jsx
+pkg/storaged/storage.scss
+pkg/storaged/storaged.jsx
+pkg/storaged/stratis/blockdev.jsx
+pkg/storaged/stratis/create-dialog.jsx
+pkg/storaged/stratis/filesystem.jsx
+pkg/storaged/stratis/pool.jsx
+pkg/storaged/stratis/stopped-pool.jsx
+pkg/storaged/stratis/stratis2-set-key.py
+pkg/storaged/stratis/stratis3-set-key.py
+pkg/storaged/stratis/utils.jsx
+pkg/storaged/swap/swap.jsx
+pkg/storaged/test-util.js
+pkg/storaged/utils.js
+pkg/systemd/README-realmd.md
+pkg/systemd/abrtLog.jsx
+pkg/systemd/busnames.js
+pkg/systemd/hw-detect.js
+pkg/systemd/hwinfo.html
+pkg/systemd/hwinfo.jsx
+pkg/systemd/hwinfo.scss
+pkg/systemd/index.html
+pkg/systemd/kernelopt.sh
+pkg/systemd/logDetails.jsx
+pkg/systemd/logs.html
+pkg/systemd/logs.jsx
+pkg/systemd/logs.scss
+pkg/systemd/logsHelpers.js
+pkg/systemd/logsJournal.jsx
+pkg/systemd/manifest.json
+pkg/systemd/overview-cards/configurationCard.jsx
+pkg/systemd/overview-cards/configurationCard.scss
+pkg/systemd/overview-cards/cryptoPolicies.jsx
+pkg/systemd/overview-cards/cryptoPolicies.scss
+pkg/systemd/overview-cards/healthCard.jsx
+pkg/systemd/overview-cards/healthCard.scss
+pkg/systemd/overview-cards/insights-poll-hack.sh
+pkg/systemd/overview-cards/insights.jsx
+pkg/systemd/overview-cards/insights.scss
+pkg/systemd/overview-cards/lastLogin.jsx
+pkg/systemd/overview-cards/lastLogin.scss
+pkg/systemd/overview-cards/motdCard.jsx
+pkg/systemd/overview-cards/motdCard.scss
+pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx
+pkg/systemd/overview-cards/realmd.jsx
+pkg/systemd/overview-cards/realmd.scss
+pkg/systemd/overview-cards/shutdownStatus.jsx
+pkg/systemd/overview-cards/shutdownStatus.scss
+pkg/systemd/overview-cards/ssh-list-host-keys.sh
+pkg/systemd/overview-cards/systemInformationCard.jsx
+pkg/systemd/overview-cards/systemInformationCard.scss
+pkg/systemd/overview-cards/tuned-dialog.jsx
+pkg/systemd/overview-cards/usageCard.jsx
+pkg/systemd/overview-cards/usageCard.scss
+pkg/systemd/overview.jsx
+pkg/systemd/overview.scss
+pkg/systemd/page-status.jsx
+pkg/systemd/page-status.scss
+pkg/systemd/reporting.jsx
+pkg/systemd/reporting.scss
+pkg/systemd/service-details.jsx
+pkg/systemd/service-details.scss
+pkg/systemd/service-tabs.jsx
+pkg/systemd/service.jsx
+pkg/systemd/services-list.jsx
+pkg/systemd/services.html
+pkg/systemd/services.jsx
+pkg/systemd/services.scss
+pkg/systemd/superuser-alert.jsx
+pkg/systemd/system-global.scss
+pkg/systemd/terminal.html
+pkg/systemd/terminal.jsx
+pkg/systemd/terminal.scss
+pkg/systemd/timer-dialog-helpers.js
+pkg/systemd/timer-dialog.jsx
+pkg/systemd/timers.scss
+pkg/users/account-create-dialog.js
+pkg/users/account-details.js
+pkg/users/account-logs-panel.jsx
+pkg/users/accounts-list.js
+pkg/users/authorized-keys-panel.js
+pkg/users/authorized-keys.js
+pkg/users/delete-account-dialog.js
+pkg/users/delete-group-dialog.js
+pkg/users/dialog-utils.js
+pkg/users/expiration-dialogs.js
+pkg/users/group-actions.jsx
+pkg/users/group-create-dialog.js
+pkg/users/index.html
+pkg/users/lock-account-dialog.js
+pkg/users/logout-account-dialog.js
+pkg/users/manifest.json
+pkg/users/mock/ssh/1-input
+pkg/users/mock/ssh/1-output
+pkg/users/mock/ssh/2-input
+pkg/users/mock/ssh/2-output
+pkg/users/mock/ssh/3-input
+pkg/users/mock/ssh/3-output
+pkg/users/mock/ssh/4-input
+pkg/users/mock/ssh/4-output
+pkg/users/password-dialogs.js
+pkg/users/rename-group-dialog.jsx
+pkg/users/shell-dialog.js
+pkg/users/ssh-add-public-key.sh
+pkg/users/ssh-list-public-keys.sh
+pkg/users/test-list-public-keys.sh
+pkg/users/users.js
+pkg/users/users.scss
+pkg/users/utils.js
+plans/all.fmf
+plans/podman.fmf
+test/ARCHITECTURE.md
+test/README.md
+test/__init__.py
+test/browser/browser.sh
+test/browser/main.fmf
+test/browser/run-test.sh
+test/common/__init__.py
+test/common/cdp.py
+test/common/chromium-cdp-driver.js
+test/common/firefox-cdp-driver.js
+test/common/git-utils.sh
+test/common/lcov.py
+test/common/link-patterns.json
+test/common/make-bots
+test/common/netlib.py
+test/common/packagelib.py
+test/common/pixel-tests
+test/common/pixeldiff.html
+test/common/pywrap
+test/common/ruff.toml
+test/common/run-tests
+test/common/storagelib.py
+test/common/tap-cdp
+test/common/test-functions.js
+test/common/testlib.py
+test/data/100years/0-self-signed.cert
+test/data/100years/0-self-signed.key
+test/data/README
+test/data/expired/0-self-signed.cert
+test/data/expired/0-self-signed.key
+test/data/expired/1.cert
+test/data/expired/1.key
+test/data/expired/combined.cert
+test/example/check-example
+test/example/check-retry
+test/example/ruff.toml
+test/image-prepare
+test/ostree.install
+test/pytest/__init__.py
+test/pytest/conftest.py
+test/pytest/mockpeer.py
+test/pytest/mocktransport.py
+test/pytest/pseudo.py
+test/pytest/test_beiboot.py
+test/pytest/test_bridge.py
+test/pytest/test_browser.py
+test/pytest/test_packages.py
+test/pytest/test_peer.py
+test/pytest/test_samples.py
+test/pytest/test_transport.py
+test/reference
+test/reference-image
+test/run
+test/static-code
+test/verify/__init__.py
+test/verify/check-apps
+test/verify/check-bots-api
+test/verify/check-client
+test/verify/check-connection
+test/verify/check-embed
+test/verify/check-examples
+test/verify/check-kdump
+test/verify/check-loopback
+test/verify/check-metrics
+test/verify/check-networkmanager-basic
+test/verify/check-networkmanager-bond
+test/verify/check-networkmanager-bridge
+test/verify/check-networkmanager-checkpoints
+test/verify/check-networkmanager-firewall
+test/verify/check-networkmanager-mac
+test/verify/check-networkmanager-mtu
+test/verify/check-networkmanager-other
+test/verify/check-networkmanager-settings
+test/verify/check-networkmanager-team
+test/verify/check-networkmanager-unmanaged
+test/verify/check-networkmanager-vlan
+test/verify/check-networkmanager-wireguard
+test/verify/check-packagekit
+test/verify/check-packages
+test/verify/check-pages
+test/verify/check-reauthorize
+test/verify/check-selinux
+test/verify/check-session
+test/verify/check-shell-active-pages
+test/verify/check-shell-host-switching
+test/verify/check-shell-keys
+test/verify/check-shell-menu
+test/verify/check-shell-multi-machine
+test/verify/check-shell-multi-machine-key
+test/verify/check-shell-multi-os
+test/verify/check-sosreport
+test/verify/check-static-login
+test/verify/check-storage-anaconda
+test/verify/check-storage-basic
+test/verify/check-storage-btrfs
+test/verify/check-storage-format
+test/verify/check-storage-hidden
+test/verify/check-storage-ignored
+test/verify/check-storage-iscsi
+test/verify/check-storage-luks
+test/verify/check-storage-lvm2
+test/verify/check-storage-mdraid
+test/verify/check-storage-mounting
+test/verify/check-storage-msdos
+test/verify/check-storage-multipath
+test/verify/check-storage-nfs
+test/verify/check-storage-partitions
+test/verify/check-storage-raid1
+test/verify/check-storage-resize
+test/verify/check-storage-scaling
+test/verify/check-storage-stratis
+test/verify/check-storage-swap
+test/verify/check-storage-unrecognized
+test/verify/check-storage-unused
+test/verify/check-storage-used
+test/verify/check-storage-vdo
+test/verify/check-superuser
+test/verify/check-system-info
+test/verify/check-system-journal
+test/verify/check-system-realms
+test/verify/check-system-services
+test/verify/check-system-shutdown-restart
+test/verify/check-system-terminal
+test/verify/check-system-tuned
+test/verify/check-testlib
+test/verify/check-users
+test/verify/check-users-roles
+test/verify/check-ws-bastion
+test/verify/conftest.py
+test/verify/files/cert-chain.cert
+test/verify/files/cert-chain.key
+test/verify/files/cockpit_event.conf
+test/verify/files/dmi/DMI
+test/verify/files/dmi/smbios_entry_point
+test/verify/files/embed-cockpit/embed.css
+test/verify/files/embed-cockpit/embed.js
+test/verify/files/embed-cockpit/index.html
+test/verify/files/embed-cockpit/manifest.json
+test/verify/files/metrics-archives/2corescpu.tar.gz
+test/verify/files/metrics-archives/cpu_network.tar.gz
+test/verify/files/metrics-archives/disk.tar.gz
+test/verify/files/metrics-archives/double_events.zip
+test/verify/files/metrics-archives/journal.journal.gz
+test/verify/files/metrics-archives/memory.tar.gz
+test/verify/files/metrics-archives/with_journal.tar.gz
+test/verify/files/mock-bugzilla-server.py
+test/verify/files/mock-faf-server.py
+test/verify/files/mock-insights
+test/verify/files/ssh/id_ecdsa
+test/verify/files/ssh/id_ecdsa.pub
+test/verify/files/ssh/id_ed25519
+test/verify/files/ssh/id_ed25519.pub
+test/verify/files/ssh/id_rsa
+test/verify/files/ssh/id_rsa.pub
+test/verify/files/test.png
+test/verify/files/workflow_Cockpit.xml
+test/verify/ruff.toml
+test/vm.install
+tools/Makefile-tools.am
+tools/Makefile.redirect
+tools/README.dist
+tools/README.node_modules
+tools/adjust-distdir-timestamps
+tools/arch/PKGBUILD
+tools/arch/cockpit-ws.sysuser.conf
+tools/arch/cockpit-wsinstance.sysuser.conf
+tools/arch/cockpit.pam
+tools/build-debian-copyright
+tools/cockpit.debian.pam
+tools/cockpit.pam
+tools/cockpit.spec
+tools/debian/changelog
+tools/debian/clean
+tools/debian/cockpit-bridge.install
+tools/debian/cockpit-doc.install
+tools/debian/cockpit-doc.lintian-overrides
+tools/debian/cockpit-networkmanager.install
+tools/debian/cockpit-packagekit.install
+tools/debian/cockpit-pcp.install
+tools/debian/cockpit-sosreport.install
+tools/debian/cockpit-storaged.install
+tools/debian/cockpit-system.install
+tools/debian/cockpit-tests.install
+tools/debian/cockpit-ws.install
+tools/debian/cockpit-ws.lintian-overrides
+tools/debian/cockpit-ws.postinst
+tools/debian/cockpit-ws.postrm
+tools/debian/cockpit.install
+tools/debian/control
+tools/debian/copyright.template
+tools/debian/rules
+tools/debian/source/format
+tools/debian/source/lintian-overrides
+tools/debian/source/options
+tools/debian/tests/control
+tools/debian/tests/smoke
+tools/debian/watch
+tools/escape-to-c
+tools/fix-spec
+tools/git-hook-post-commit
+tools/git-hook-pre-push
+tools/git-hook-pre-rebase
+tools/glib.supp
+tools/make-debs
+tools/make-dist
+tools/make-rpms
+tools/mock-build-env/README.md
+tools/mock-build-env/gnutls.pc
+tools/mock-build-env/json-glib-1.0.pc
+tools/mock-build-env/krb5-gssapi.pc
+tools/mock-build-env/krb5.pc
+tools/mock-build-env/security/pam_appl.h
+tools/node-modules
+tools/patch-metainfo
+tools/termschutz
+tools/urls-check
+tools/vulture-suppressions/cockpit.py
+tools/vulture-suppressions/ferny.py
+tools/vulture-suppressions/pytest.py
+tools/vulture-suppressions/ruff.toml
+tools/vulture-suppressions/stdlib.py
+tools/vulture-suppressions/testlib.py
+tools/webpack-make.js
+tools/webpack-watch
diff --git a/.fmf/version b/.fmf/version
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/.fmf/version
@@ -0,0 +1 @@
+1
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..7bf09df
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,5 @@
+Cockpit is being developed by many contributors. Please see
+https://github.com/cockpit-project/cockpit/graphs/contributors
+for a list of authors.
+
+In a git checkout of the project you can also use `git shortlog -sn`.
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..4362b49
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..a4a3e02
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,192 @@
+NULL =
+CLEANFILES =
+dist_noinst_DATA =
+man_MANS =
+noinst_DATA =
+noinst_LIBRARIES =
+noinst_PROGRAMS =
+
+# See https://www.gnu.org/software/make/manual/html_node/Force-Targets.html
+FORCE:
+
+# -----------------------------------------------------------------------------
+# node_modules/package-lock.json dependency
+
+V_NODE_MODULES = $(V_NODE_MODULES_$(V))
+V_NODE_MODULES_ = $(V_NODE_MODULES_$(AM_DEFAULT_VERBOSITY))
+V_NODE_MODULES_0 = @V=0
+
+# We want tools/node-modules to run every time package-lock.json is requested
+$(srcdir)/package-lock.json: FORCE
+ $(V_NODE_MODULES) $(srcdir)/tools/node-modules make_package_lock_json
+
+# -----------------------------------------------------------------------------
+# make dist
+
+EXTRA_DIST = README.md
+CLEANFILES += cockpit-*.tar.xz
+
+# We override distdir as we want to dist some git-tracked files and dist/ without explicitly listing.
+# The spec also gets patched to declare bundled NPM dependencies.
+EXTRA_DIST += $(EXTRA_FILES)
+distdir: $(DISTFILES)
+ @if [ -e '$(srcdir)/.git' ]; then \
+ git -C '$(srcdir)' ls-files -x test/reference .fmf plans pkg test tools > .extra_dist.tmp && \
+ mv .extra_dist.tmp '$(srcdir)/.extra_dist'; fi
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am EXTRA_FILES="$$(tr '\n' ' ' < $(srcdir)/.extra_dist) .extra_dist"
+ sed -i "s/[@]VERSION@/$(VERSION)/" "$(distdir)/src/client/org.cockpit_project.CockpitClient.metainfo.xml"
+ $(srcdir)/tools/fix-spec $(distdir)/tools/cockpit.spec $(VERSION)
+ test -z '$(HACK_SPEC_FOR_PYTHON)' || \
+ sed -i 's/\(define enable_old_bridge\) 1/\1 0/' $(distdir)/tools/cockpit.spec
+ sed -i "/^pkgver=/ s/0/$(VERSION)/" "$(distdir)/tools/arch/PKGBUILD"
+ sed -i "1 s/0/$(VERSION)/" "$(distdir)/tools/debian/changelog"
+ cp -r "$(srcdir)/dist" "$(distdir)"
+ $(srcdir)/tools/adjust-distdir-timestamps "$(distdir)"
+ @echo ' DIST $(DIST_ARCHIVES)'
+
+# Needed to ensure the tarball is correct for $(VERSION) override
+dist-hook: $(distdir)/src/cockpit/_version.py
+$(distdir)/src/cockpit/_version.py: FORCE
+ python3 '$(srcdir)'/src/build_backend.py --copy '$(srcdir)' '$(distdir)'
+ @rm -f $(distdir)/src/cockpit/_version.py
+ $(AM_V_GEN) echo "__version__ = '$(VERSION)'" > $@
+
+$(distdir)/version.m4: FORCE
+ @rm -f $(distdir)/version.m4
+ $(AM_V_GEN) echo 'm4_define(VERSION_NUMBER, [$(VERSION)])' > $@
+
+# be careful not to include autotools cache into dist tarballs
+dist-hook: $(distdir)/configure
+$(distdir)/configure: $(distdir)/version.m4
+ @rm -f $(distdir)/configure
+ $(AM_V_GEN) autoreconf $(distdir)
+ @rm -r $(distdir)/autom4te.cache
+
+# Various downstream packaging assets
+dist-hook: $(distdir)/tools/debian/copyright
+$(distdir)/tools/debian/copyright: $(DIST_STAMP)
+ $(AM_V_GEN) NODE_ENV=$(NODE_ENV) $(srcdir)/tools/build-debian-copyright > $@
+
+DISTCHECK_CONFIGURE_FLAGS = --enable-prefix-only $(EXTRA_DISTCHECK_CONFIGURE_FLAGS)
+
+# Validate our AppStream metadata
+distcheck-hook::
+ find $(distdir) -name '*.metainfo.xml' -o -name '*.appdata.xml' | xargs appstream-util validate --nonet
+
+# validate that we don't bundle the embedded patternfly font files, we use them from /static/fonts/
+distcheck-hook::
+ ! grep --color=auto -rn "\.\./fonts/OpenSans\|fonts/.*eot\|truetype" $(distdir)/dist
+
+# -----------------------------------------------------------------------------
+
+TESTS = \
+ test/static-code \
+ $(NULL)
+
+clean-local::
+ find $(builddir) -name '*.gc??' -delete
+ find $(srcdir) -name '*.pyc' -delete
+
+# required for running unit and integration tests; commander and ws are deps of chrome-remote-interface
+node_modules/%: $(srcdir)/package-lock.json
+ @true
+
+EXTRA_DIST += \
+ node_modules/chrome-remote-interface \
+ node_modules/commander \
+ node_modules/sizzle \
+ node_modules/ws \
+ $(NULL)
+
+check: export VERBOSE=1
+
+TEST_EXTENSIONS = .html .sh
+HTML_LOG_COMPILER = $(top_srcdir)/test/common/tap-cdp --strip=$(abs_top_srcdir)/ $(HTML_TEST_WRAPPER) ./test-server $(COCKPIT_BRIDGE)
+
+VALGRIND = valgrind --trace-children=yes --quiet --error-exitcode=33 --gen-suppressions=all \
+ $(foreach file,$(wildcard $(srcdir)/tools/*.supp),--suppressions=$(file)) \
+ --num-callers=16 --leak-check=yes --show-leak-kinds=definite \
+ --errors-for-leak-kinds=definite --trace-children-skip='*mock*,/bin*,/usr/bin/*,/usr/local/bin'
+
+check-memory:
+ $(MAKE) LOG_FLAGS="$(VALGRIND)" \
+ HTML_TEST_WRAPPER="$(VALGRIND)" \
+ COCKPIT_SKIP_SLOW_TESTS=1 \
+ $(AM_MAKEFLAGS) check TESTS="$(filter-out test/% bots/%,$(TESTS))"
+recheck-memory:
+ $(MAKE) LOG_FLAGS="$(VALGRIND_ARGS)" \
+ HTML_TEST_WRAPPER="$(VALGRIND)" \
+ $(AM_MAKEFLAGS) recheck
+
+# checkout Cockpit's bots for standard test VM images and API to launch them
+# must be from main, as only that has current and existing images; but testvm.py API is stable
+# support CI testing against a bots change
+bots:
+ test/common/make-bots
+
+.PHONY: rsync
+RSYNC_HOST ?= c
+RSYNC_DEST ?= $(RSYNC_HOST):/
+rsync:
+ $(MAKE)
+ $(MAKE) install DESTDIR=tmp/rsync >/dev/null
+ rsync --recursive --links --checksum --verbose --inplace tmp/rsync/ $(RSYNC_DEST)
+
+# We use these to add conditionally-enabled extra rules
+# automake doesn't like mixing `::` and `if`
+
+CHECK_LOCAL_TARGETS =
+.PHONY: $(CHECK_LOCAL_TARGETS)
+check-local:: $(CHECK_LOCAL_TARGETS)
+
+CLEAN_LOCAL_TARGETS =
+.PHONY: $(CLEAN_LOCAL_TARGETS)
+clean-local:: $(CLEAN_LOCAL_TARGETS)
+
+INSTALL_DATA_LOCAL_TARGETS =
+.PHONY: $(INSTALL_DATA_LOCAL_TARGETS)
+install-data-local:: $(INSTALL_DATA_LOCAL_TARGETS)
+
+INSTALL_EXEC_HOOK_TARGETS =
+.PHONY: $(INSTALL_EXEC_HOOK_TARGETS)
+install-exec-hook:: $(INSTALL_EXEC_HOOK_TARGETS)
+
+UNINSTALL_LOCAL_TARGETS =
+.PHONY: $(UNINSTALL_LOCAL_TARGETS)
+uninstall-local:: $(UNINSTALL_LOCAL_TARGETS)
+
+
+# This Makefile includes several variable definitions that must come first
+include src/testlib/Makefile.am
+
+include containers/Makefile.am
+include containers/flatpak/Makefile.am
+include doc/Makefile-doc.am
+include doc/guide/Makefile-guide.am
+include doc/man/Makefile-man.am
+include pkg/Makefile.am
+include po/Makefile.am
+include selinux/Makefile.am
+include src/Makefile.am
+include src/branding/arch/Makefile.am
+include src/branding/centos/Makefile.am
+include src/branding/debian/Makefile.am
+include src/branding/default/Makefile.am
+include src/branding/fedora/Makefile.am
+include src/branding/kubernetes/Makefile.am
+include src/branding/opensuse/Makefile.am
+include src/branding/registry/Makefile.am
+include src/branding/rhel/Makefile.am
+include src/branding/scientific/Makefile.am
+include src/branding/ubuntu/Makefile.am
+include src/bridge/Makefile.am
+include src/client/Makefile.am
+include src/common/Makefile-common.am
+include src/pam-ssh-add/Makefile.am
+include src/session/Makefile-session.am
+include src/ssh/Makefile-ssh.am
+include src/systemd/Makefile.am
+include src/tls/Makefile-tls.am
+include src/websocket/Makefile-websocket.am
+include src/ws/Makefile-ws.am
+include tools/Makefile-tools.am
diff --git a/Makefile.in b/Makefile.in
new file mode 100644
index 0000000..39c6dd6
--- /dev/null
+++ b/Makefile.in
@@ -0,0 +1,9169 @@
+# Makefile.in generated by automake 1.16.5 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2021 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+# -----------------------------------------------------------------------------
+# Common cpp and ld flags for tests: these will be used an awful lot
+
+# -----------------------------------------------------------------------------
+# libcockpit-metrics.a: code used in both cockpit-bridge and cockpit-pcp
+
+# -----------------------------------------------------------------------------
+# Cockpit Client
+
+# We actually always install cockpit-client in libexec,
+# but ENABLE_COCKPIT_CLIENT controls whether we install
+# the desktop file, icons and a symlink in /usr/bin.
+# This is currently **EXPERIMENTAL**
+
+# -----------------------------------------------------------------------------
+# libcockpit-common-nodeps.a: code that has no dependencies other than libc
+
+# -----------------------------------------------------------------------------
+# libpam_ssh_add.a: code used in pam_ssh_add.so and its tests
+
+# -----------------------------------------------------------------------------
+# cockpit-session
+
+# -----------------------------------------------------------------------------
+# all systemd units, tmpfiles, and related helpers
+
+# -----------------------------------------------------------------------------
+# libcockpit-tls.a: code used in cockpit-tls, helpers, and tests
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+# Note: Because of its use of cockpitflow, libwebsocket_a_LIBS should
+# rightfully include a reference to libcockpit_common_a_LIBS, but it
+# can't do this because libcockpit-common.a depends, in turn, on
+# libwebsocket.
+#
+# At this point, libwebsocket should probably just be merged into
+# libcockpit-common, but we can't do that because the log domain is
+# different, and some of the tests depend on that.
+
+# -----------------------------------------------------------------------------
+# libwebsocket.a: low-level websocket handling code
+
+# -----------------------------------------------------------------------------
+# libcockpit-ws.a: code used in cockpit-ws and its tests
+
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+noinst_PROGRAMS = mock-pam-conv-mod.so$(EXEEXT)
+TESTS = test/static-code $(am__EXEEXT_14) $(am__EXEEXT_7) \
+ $(am__EXEEXT_14) $(dist_TEST_SCRIPT) $(am__EXEEXT_15)
+check_PROGRAMS = $(am__EXEEXT_7) $(am__EXEEXT_8) $(am__EXEEXT_9) \
+ libpreload-temp-home.so$(EXEEXT) $(am__EXEEXT_10) \
+ wsinstance-start$(EXEEXT) socket-activation-helper$(EXEEXT) \
+ frob-websocket$(EXEEXT) test-server$(EXEEXT) \
+ mock-echo$(EXEEXT) mock-auth-command$(EXEEXT)
+@ENABLE_DOC_TRUE@am__append_1 = \
+@ENABLE_DOC_TRUE@ $(GUIDE_DOCBOOK) \
+@ENABLE_DOC_TRUE@ $(GUIDE_INCLUDES) \
+@ENABLE_DOC_TRUE@ $(GUIDE_XSLT) \
+@ENABLE_DOC_TRUE@ $(GUIDE_STATIC) \
+@ENABLE_DOC_TRUE@ $(NULL)
+
+@ENABLE_DOC_TRUE@am__append_2 = doc/guide/html/index.html
+@ENABLE_DOC_TRUE@am__append_3 = clean-guide
+@ENABLE_DOC_TRUE@am__append_4 = check-guide
+@ENABLE_DOC_TRUE@am__append_5 = install-guide
+@ENABLE_DOC_TRUE@am__append_6 = uninstall-guide
+@ENABLE_DOC_TRUE@am__append_7 = $(MANPAGES)
+@ENABLE_DOC_TRUE@am__append_8 = $(MANPAGES)
+@SELINUX_POLICY_ENABLED_TRUE@am__append_9 = \
+@SELINUX_POLICY_ENABLED_TRUE@ $(SELINUX_POLICY_FILES) \
+@SELINUX_POLICY_ENABLED_TRUE@ $(SELINUX_POLICY_MANPAGES) \
+@SELINUX_POLICY_ENABLED_TRUE@ $(NULL)
+
+@SELINUX_POLICY_ENABLED_TRUE@am__append_10 = install-selinux
+@SELINUX_POLICY_ENABLED_TRUE@am__append_11 = $(SELINUX_POLICY_MANPAGES)
+bin_PROGRAMS = $(am__EXEEXT_1)
+libexec_PROGRAMS = $(am__EXEEXT_11) $(am__EXEEXT_12) \
+ cockpit-session$(EXEEXT) $(am__EXEEXT_13) cockpit-tls$(EXEEXT) \
+ cockpit-certificate-ensure$(EXEEXT) \
+ cockpit-wsinstance-factory$(EXEEXT)
+sbin_PROGRAMS =
+@WITH_OLD_BRIDGE_FALSE@am__append_12 = install-python
+@WITH_OLD_BRIDGE_FALSE@am__append_13 = uninstall-python
+
+# -----------------------------------------------------------------------------
+# libcockpit-bridge.a: code used in cockpit-bridge and its tests
+@WITH_OLD_BRIDGE_TRUE@am__append_14 = libcockpit-bridge.a
+@WITH_OLD_BRIDGE_TRUE@@WITH_POLKIT_TRUE@am__append_15 = $(polkit_CFLAGS)
+@WITH_OLD_BRIDGE_TRUE@@WITH_POLKIT_TRUE@am__append_16 = $(polkit_LIBS)
+@WITH_OLD_BRIDGE_TRUE@@WITH_POLKIT_TRUE@am__append_17 = \
+@WITH_OLD_BRIDGE_TRUE@@WITH_POLKIT_TRUE@ src/bridge/cockpitpolkitagent.c \
+@WITH_OLD_BRIDGE_TRUE@@WITH_POLKIT_TRUE@ src/bridge/cockpitpolkitagent.h \
+@WITH_OLD_BRIDGE_TRUE@@WITH_POLKIT_TRUE@ $(NULL)
+
+
+# -----------------------------------------------------------------------------
+# PROGRAMS
+@WITH_OLD_BRIDGE_TRUE@am__append_18 = cockpit-bridge
+@WITH_OLD_BRIDGE_TRUE@am__append_19 = cockpit-askpass
+
+# -----------------------------------------------------------------------------
+# TESTS
+@WITH_OLD_BRIDGE_TRUE@am__append_20 = mock-bridge
+@WITH_OLD_BRIDGE_TRUE@am__append_21 = test-bridge test-connect \
+@WITH_OLD_BRIDGE_TRUE@ test-dbus-meta test-fs test-httpstream \
+@WITH_OLD_BRIDGE_TRUE@ test-metrics test-packages \
+@WITH_OLD_BRIDGE_TRUE@ test-packet-channel test-paths test-peer \
+@WITH_OLD_BRIDGE_TRUE@ test-pipe-channel test-process \
+@WITH_OLD_BRIDGE_TRUE@ test-router test-rules test-stream \
+@WITH_OLD_BRIDGE_TRUE@ test-websocketstream
+@WITH_OLD_BRIDGE_TRUE@am__append_22 = \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/mock-resource \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/mock-pmda.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/mock-pmns \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/mock-client.crt \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/mock-client.key \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/mock-server.crt \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/mock-server.key \
+@WITH_OLD_BRIDGE_TRUE@ $(NULL)
+
+@WITH_POLKIT_TRUE@am__append_23 = $(polkit_DATA)
+@ENABLE_PCP_TRUE@am__append_24 = cockpit-pcp
+@ENABLE_PCP_TRUE@am__append_25 = libcockpit-pcp.a
+@ENABLE_PCP_TRUE@am__append_26 = mock-pmda.so
+@ENABLE_PCP_TRUE@am__append_27 = test-pcp test-pcp-archives
+@ENABLE_PCP_TRUE@am__append_28 = mock-archives/*
+@ENABLE_COCKPIT_CLIENT_TRUE@am__append_29 = install-cockpit-client-symlink
+pam_PROGRAMS = pam_ssh_add.so$(EXEEXT) pam_cockpit_cert.so$(EXEEXT)
+
+# -----------------------------------------------------------------------------
+# libcockpit-ssh.a: code used in cockpit-ssh and its tests
+@WITH_COCKPIT_SSH_TRUE@am__append_30 = libcockpit-ssh.a
+
+# -----------------------------------------------------------------------------
+# cockpit-ssh
+@WITH_COCKPIT_SSH_TRUE@am__append_31 = cockpit-ssh
+
+# -----------------------------------------------------------------------------
+# mock-ssh
+@WITH_COCKPIT_SSH_TRUE@am__append_32 = mock-sshd
+
+# -----------------------------------------------------------------------------
+# Unit tests
+@WITH_COCKPIT_SSH_TRUE@am__append_33 = \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/mock_rsa_key \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/mock_ecdsa_key \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/test_rsa \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/test_rsa.pub \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/mock_known_hosts \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/mock-pid-cat \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/mock-config \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/invalid_known_hosts \
+@WITH_COCKPIT_SSH_TRUE@ $(NULL)
+
+@WITH_COCKPIT_SSH_TRUE@am__append_34 = test-sshbridge test-sshoptions
+@WITH_COCKPIT_SSH_TRUE@am__append_35 = test_rsa_key
+@WITH_COCKPIT_SSH_TRUE@am__append_36 = test_rsa_key
+cockpitws_PROGRAMS = cockpit-ws$(EXEEXT)
+
+# These are -ws tests but they involve invoking ./cockpit-bridge.
+@WITH_OLD_BRIDGE_TRUE@am__append_37 = test-channelresponse \
+@WITH_OLD_BRIDGE_TRUE@ test-handlers test-webservice
+@WITH_COCKPIT_SSH_TRUE@@WITH_OLD_BRIDGE_TRUE@am__append_38 = test-authssh
+subdir = .
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/version.m4 \
+ $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(top_srcdir)/configure \
+ $(am__configure_deps) $(dist_check_SCRIPTS) \
+ $(dist_cockpitclient_SCRIPTS) $(dist_motd_SCRIPTS) \
+ $(am__dist_applications_DATA_DIST) $(dist_archbranding_DATA) \
+ $(dist_centosbranding_DATA) $(am__dist_check_DATA_DIST) \
+ $(am__dist_client_metainfo_DATA_DIST) \
+ $(dist_cockpitclient_DATA) $(am__dist_dbusservices_DATA_DIST) \
+ $(dist_debianbranding_DATA) $(dist_defaultbranding_DATA) \
+ $(dist_fedorabranding_DATA) $(dist_kubernetesbranding_DATA) \
+ $(dist_motd_DATA) $(am__dist_noinst_DATA_DIST) \
+ $(dist_opensusebranding_DATA) \
+ $(am__dist_pcpmanifest_DATA_DIST) $(dist_pixmap_DATA) \
+ $(am__dist_pmlogconf_DATA_DIST) $(dist_registrybranding_DATA) \
+ $(dist_rhelbranding_DATA) $(am__dist_scalableicon_DATA_DIST) \
+ $(dist_scientificbranding_DATA) \
+ $(am__dist_sshmanifest_DATA_DIST) \
+ $(am__dist_symbolicicon_DATA_DIST) $(dist_systemdunit_DATA) \
+ $(dist_ubuntubranding_DATA) $(am__DIST_COMMON)
+am__CONFIG_DISTCLEAN_FILES = config.status config.cache config.log \
+ configure.lineno config.status.lineno
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = config.h
+CONFIG_CLEAN_FILES = doc/guide/version \
+ src/tls/cockpit-certificate-helper src/ws/cockpit-desktop
+CONFIG_CLEAN_VPATH_FILES =
+@WITH_OLD_BRIDGE_TRUE@am__EXEEXT_1 = cockpit-bridge$(EXEEXT)
+am__installdirs = "$(DESTDIR)$(bindir)" "$(DESTDIR)$(cockpitwsdir)" \
+ "$(DESTDIR)$(libexecdir)" "$(DESTDIR)$(pamdir)" \
+ "$(DESTDIR)$(sbindir)" "$(DESTDIR)$(cockpitclientdir)" \
+ "$(DESTDIR)$(motddir)" "$(DESTDIR)$(libexecdir)" \
+ "$(DESTDIR)$(man1dir)" "$(DESTDIR)$(man5dir)" \
+ "$(DESTDIR)$(man8dir)" "$(DESTDIR)$(applicationsdir)" \
+ "$(DESTDIR)$(archbrandingdir)" \
+ "$(DESTDIR)$(centosbrandingdir)" \
+ "$(DESTDIR)$(client_metainfodir)" \
+ "$(DESTDIR)$(cockpitclientdir)" "$(DESTDIR)$(dbusservicesdir)" \
+ "$(DESTDIR)$(debianbrandingdir)" \
+ "$(DESTDIR)$(defaultbrandingdir)" \
+ "$(DESTDIR)$(fedorabrandingdir)" \
+ "$(DESTDIR)$(kubernetesbrandingdir)" "$(DESTDIR)$(motddir)" \
+ "$(DESTDIR)$(opensusebrandingdir)" \
+ "$(DESTDIR)$(pcpmanifestdir)" "$(DESTDIR)$(pixmapdir)" \
+ "$(DESTDIR)$(pmlogconfdir)" "$(DESTDIR)$(registrybrandingdir)" \
+ "$(DESTDIR)$(rhelbrandingdir)" "$(DESTDIR)$(scalableicondir)" \
+ "$(DESTDIR)$(scientificbrandingdir)" \
+ "$(DESTDIR)$(sshmanifestdir)" "$(DESTDIR)$(symbolicicondir)" \
+ "$(DESTDIR)$(systemdunitdir)" "$(DESTDIR)$(ubuntubrandingdir)" \
+ "$(DESTDIR)$(appdatadir)" "$(DESTDIR)$(metainfodir)" \
+ "$(DESTDIR)$(systemdunitdir)" "$(DESTDIR)$(tempconfdir)" \
+ "$(DESTDIR)$(pixmapsdir)" "$(DESTDIR)$(polkitdir)" \
+ "$(DESTDIR)$(selinuxpackagesdir)"
+@WITH_OLD_BRIDGE_TRUE@am__EXEEXT_2 = test-bridge$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-connect$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-dbus-meta$(EXEEXT) test-fs$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-httpstream$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-metrics$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-packages$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-packet-channel$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-paths$(EXEEXT) test-peer$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-pipe-channel$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-process$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-router$(EXEEXT) test-rules$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-stream$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-websocketstream$(EXEEXT)
+@ENABLE_PCP_TRUE@am__EXEEXT_3 = test-pcp$(EXEEXT) \
+@ENABLE_PCP_TRUE@ test-pcp-archives$(EXEEXT)
+@WITH_COCKPIT_SSH_TRUE@am__EXEEXT_4 = test-sshbridge$(EXEEXT) \
+@WITH_COCKPIT_SSH_TRUE@ test-sshoptions$(EXEEXT)
+@WITH_OLD_BRIDGE_TRUE@am__EXEEXT_5 = test-channelresponse$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-handlers$(EXEEXT) \
+@WITH_OLD_BRIDGE_TRUE@ test-webservice$(EXEEXT)
+@WITH_COCKPIT_SSH_TRUE@@WITH_OLD_BRIDGE_TRUE@am__EXEEXT_6 = test-authssh$(EXEEXT)
+am__EXEEXT_7 = $(am__EXEEXT_2) $(am__EXEEXT_3) test-authorize$(EXEEXT) \
+ test-base64$(EXEEXT) test-channel$(EXEEXT) \
+ test-config$(EXEEXT) test-frame$(EXEEXT) test-hash$(EXEEXT) \
+ test-hex$(EXEEXT) test-json$(EXEEXT) test-jsonfds$(EXEEXT) \
+ test-locale$(EXEEXT) test-pipe$(EXEEXT) test-system$(EXEEXT) \
+ test-template$(EXEEXT) test-transport$(EXEEXT) \
+ test-unicode$(EXEEXT) test-unixsignal$(EXEEXT) \
+ test-version$(EXEEXT) test-webcertificate$(EXEEXT) \
+ test-webresponse$(EXEEXT) test-webserver$(EXEEXT) \
+ test-ssh-add$(EXEEXT) $(am__EXEEXT_4) \
+ test-cockpit-certificate-ensure$(EXEEXT) \
+ test-tls-connection$(EXEEXT) test-tls-server$(EXEEXT) \
+ test-websocket$(EXEEXT) test-auth$(EXEEXT) \
+ test-compat$(EXEEXT) test-creds$(EXEEXT) \
+ test-kerberos$(EXEEXT) $(am__EXEEXT_5) $(am__EXEEXT_6)
+@WITH_OLD_BRIDGE_TRUE@am__EXEEXT_8 = mock-bridge$(EXEEXT)
+@ENABLE_PCP_TRUE@am__EXEEXT_9 = mock-pmda.so$(EXEEXT)
+@WITH_COCKPIT_SSH_TRUE@am__EXEEXT_10 = mock-sshd$(EXEEXT)
+@WITH_OLD_BRIDGE_TRUE@am__EXEEXT_11 = cockpit-askpass$(EXEEXT)
+@ENABLE_PCP_TRUE@am__EXEEXT_12 = cockpit-pcp$(EXEEXT)
+@WITH_COCKPIT_SSH_TRUE@am__EXEEXT_13 = cockpit-ssh$(EXEEXT)
+PROGRAMS = $(bin_PROGRAMS) $(cockpitws_PROGRAMS) $(libexec_PROGRAMS) \
+ $(noinst_PROGRAMS) $(pam_PROGRAMS) $(sbin_PROGRAMS)
+LIBRARIES = $(noinst_LIBRARIES)
+ARFLAGS = cru
+AM_V_AR = $(am__v_AR_@AM_V@)
+am__v_AR_ = $(am__v_AR_@AM_DEFAULT_V@)
+am__v_AR_0 = @echo " AR " $@;
+am__v_AR_1 =
+libcockpit_bridge_a_AR = $(AR) $(ARFLAGS)
+libcockpit_bridge_a_LIBADD =
+am__libcockpit_bridge_a_SOURCES_DIST = src/bridge/cockpitconnect.c \
+ src/bridge/cockpitconnect.h src/bridge/cockpitdbuscache.c \
+ src/bridge/cockpitdbuscache.h src/bridge/cockpitdbusconfig.c \
+ src/bridge/cockpitdbusinternal.c \
+ src/bridge/cockpitdbusinternal.h src/bridge/cockpitdbusjson.c \
+ src/bridge/cockpitdbusjson.h \
+ src/bridge/cockpitdbusloginmessages.c \
+ src/bridge/cockpitdbusmachines.c src/bridge/cockpitdbusmeta.c \
+ src/bridge/cockpitdbusmeta.h src/bridge/cockpitdbusprocess.c \
+ src/bridge/cockpitdbusrules.c src/bridge/cockpitdbusrules.h \
+ src/bridge/cockpitdbususer.c src/bridge/cockpitechochannel.c \
+ src/bridge/cockpitechochannel.h src/bridge/cockpitfslist.c \
+ src/bridge/cockpitfslist.h src/bridge/cockpitfsread.c \
+ src/bridge/cockpitfsread.h src/bridge/cockpitfsreplace.c \
+ src/bridge/cockpitfsreplace.h src/bridge/cockpitfswatch.c \
+ src/bridge/cockpitfswatch.h src/bridge/cockpithttpstream.c \
+ src/bridge/cockpithttpstream.h \
+ src/bridge/cockpitinteracttransport.c \
+ src/bridge/cockpitinteracttransport.h \
+ src/bridge/cockpitnullchannel.c \
+ src/bridge/cockpitnullchannel.h src/bridge/cockpitpackages.c \
+ src/bridge/cockpitpackages.h src/bridge/cockpitpacketchannel.c \
+ src/bridge/cockpitpacketchannel.h src/bridge/cockpitpaths.c \
+ src/bridge/cockpitpaths.h src/bridge/cockpitpeer.c \
+ src/bridge/cockpitpeer.h src/bridge/cockpitpipechannel.c \
+ src/bridge/cockpitpipechannel.h src/bridge/cockpitrouter.c \
+ src/bridge/cockpitrouter.h src/bridge/cockpitstream.c \
+ src/bridge/cockpitstream.h src/bridge/cockpitwebsocketstream.c \
+ src/bridge/cockpitwebsocketstream.h \
+ src/bridge/cockpitpolkitagent.c \
+ src/bridge/cockpitpolkitagent.h
+am__dirstamp = $(am__leading_dot)dirstamp
+am__objects_1 =
+@WITH_OLD_BRIDGE_TRUE@@WITH_POLKIT_TRUE@am__objects_2 = src/bridge/libcockpit_bridge_a-cockpitpolkitagent.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@@WITH_POLKIT_TRUE@ $(am__objects_1)
+@WITH_OLD_BRIDGE_TRUE@am_libcockpit_bridge_a_OBJECTS = src/bridge/libcockpit_bridge_a-cockpitconnect.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitdbuscache.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitdbusconfig.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitdbusinternal.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitdbusjson.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitdbusmachines.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitdbusmeta.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitdbusprocess.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitdbusrules.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitdbususer.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitechochannel.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitfslist.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitfsread.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitfsreplace.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitfswatch.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpithttpstream.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitinteracttransport.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitnullchannel.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitpackages.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitpacketchannel.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitpaths.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitpeer.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitpipechannel.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitrouter.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitstream.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.$(OBJEXT) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__objects_1) $(am__objects_2)
+libcockpit_bridge_a_OBJECTS = $(am_libcockpit_bridge_a_OBJECTS)
+libcockpit_common_nodeps_a_AR = $(AR) $(ARFLAGS)
+libcockpit_common_nodeps_a_LIBADD =
+am_libcockpit_common_nodeps_a_OBJECTS = \
+ src/common/cockpitauthorize.$(OBJEXT) \
+ src/common/cockpitbase64.$(OBJEXT) \
+ src/common/cockpitconf.$(OBJEXT) \
+ src/common/cockpitfdpassing.$(OBJEXT) \
+ src/common/cockpitframe.$(OBJEXT) \
+ src/common/cockpithex.$(OBJEXT) \
+ src/common/cockpitjsonprint.$(OBJEXT) \
+ src/common/cockpitmemory.$(OBJEXT) \
+ src/common/cockpitwebcertificate.$(OBJEXT) $(am__objects_1)
+libcockpit_common_nodeps_a_OBJECTS = \
+ $(am_libcockpit_common_nodeps_a_OBJECTS)
+libcockpit_common_a_AR = $(AR) $(ARFLAGS)
+libcockpit_common_a_LIBADD =
+am_libcockpit_common_a_OBJECTS = \
+ src/common/libcockpit_common_a-cockpitchannel.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitclosefrom.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitcontrolmessages.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpiterror.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitflow.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpithash.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitjson.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitlocale.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitloopback.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitmachinesjson.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitmemfdread.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitpipe.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitpipetransport.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitsocket.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitsystem.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpittemplate.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpittransport.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitunicode.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitunixsignal.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitversion.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitwebfilter.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitwebinject.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitwebresponse.$(OBJEXT) \
+ src/common/libcockpit_common_a-cockpitwebserver.$(OBJEXT) \
+ $(am__objects_1)
+nodist_libcockpit_common_a_OBJECTS = \
+ src/common/libcockpit_common_a-fail.html.$(OBJEXT)
+libcockpit_common_a_OBJECTS = $(am_libcockpit_common_a_OBJECTS) \
+ $(nodist_libcockpit_common_a_OBJECTS)
+libcockpit_metrics_a_AR = $(AR) $(ARFLAGS)
+libcockpit_metrics_a_LIBADD =
+am_libcockpit_metrics_a_OBJECTS = \
+ src/bridge/libcockpit_metrics_a-cockpitblocksamples.$(OBJEXT) \
+ src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.$(OBJEXT) \
+ src/bridge/libcockpit_metrics_a-cockpitcpusamples.$(OBJEXT) \
+ src/bridge/libcockpit_metrics_a-cockpitdisksamples.$(OBJEXT) \
+ src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.$(OBJEXT) \
+ src/bridge/libcockpit_metrics_a-cockpitmemorysamples.$(OBJEXT) \
+ src/bridge/libcockpit_metrics_a-cockpitmetrics.$(OBJEXT) \
+ src/bridge/libcockpit_metrics_a-cockpitmountsamples.$(OBJEXT) \
+ src/bridge/libcockpit_metrics_a-cockpitnetworksamples.$(OBJEXT) \
+ src/bridge/libcockpit_metrics_a-cockpitsamples.$(OBJEXT) \
+ $(am__objects_1)
+libcockpit_metrics_a_OBJECTS = $(am_libcockpit_metrics_a_OBJECTS)
+libcockpit_pcp_a_AR = $(AR) $(ARFLAGS)
+libcockpit_pcp_a_LIBADD =
+am__libcockpit_pcp_a_SOURCES_DIST = src/bridge/cockpitconnect.c \
+ src/bridge/cockpitconnect.h src/bridge/cockpitpcpmetrics.c \
+ src/bridge/cockpitpcpmetrics.h \
+ src/bridge/cockpitdbusinternal.c \
+ src/bridge/cockpitdbusinternal.h src/bridge/cockpitpeer.c \
+ src/bridge/cockpitpeer.h src/bridge/cockpitrouter.c \
+ src/bridge/cockpitrouter.h
+@ENABLE_PCP_TRUE@am_libcockpit_pcp_a_OBJECTS = src/bridge/libcockpit_pcp_a-cockpitconnect.$(OBJEXT) \
+@ENABLE_PCP_TRUE@ src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.$(OBJEXT) \
+@ENABLE_PCP_TRUE@ src/bridge/libcockpit_pcp_a-cockpitdbusinternal.$(OBJEXT) \
+@ENABLE_PCP_TRUE@ src/bridge/libcockpit_pcp_a-cockpitpeer.$(OBJEXT) \
+@ENABLE_PCP_TRUE@ src/bridge/libcockpit_pcp_a-cockpitrouter.$(OBJEXT) \
+@ENABLE_PCP_TRUE@ $(am__objects_1)
+libcockpit_pcp_a_OBJECTS = $(am_libcockpit_pcp_a_OBJECTS)
+libcockpit_ssh_a_AR = $(AR) $(ARFLAGS)
+libcockpit_ssh_a_LIBADD =
+am__libcockpit_ssh_a_SOURCES_DIST = src/ssh/cockpitsshoptions.c \
+ src/ssh/cockpitsshoptions.h src/ssh/cockpitsshrelay.h \
+ src/ssh/cockpitsshrelay.c
+@WITH_COCKPIT_SSH_TRUE@am_libcockpit_ssh_a_OBJECTS = src/ssh/libcockpit_ssh_a-cockpitsshoptions.$(OBJEXT) \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/libcockpit_ssh_a-cockpitsshrelay.$(OBJEXT) \
+@WITH_COCKPIT_SSH_TRUE@ $(am__objects_1)
+libcockpit_ssh_a_OBJECTS = $(am_libcockpit_ssh_a_OBJECTS)
+libcockpit_test_a_AR = $(AR) $(ARFLAGS)
+libcockpit_test_a_LIBADD =
+am_libcockpit_test_a_OBJECTS = \
+ src/testlib/libcockpit_test_a-cockpittest.$(OBJEXT) \
+ src/testlib/libcockpit_test_a-mock-auth.$(OBJEXT) \
+ src/testlib/libcockpit_test_a-mock-channel.$(OBJEXT) \
+ src/testlib/libcockpit_test_a-mock-pressure.$(OBJEXT) \
+ src/testlib/libcockpit_test_a-mock-transport.$(OBJEXT) \
+ src/testlib/libcockpit_test_a-retest.$(OBJEXT) \
+ $(am__objects_1)
+libcockpit_test_a_OBJECTS = $(am_libcockpit_test_a_OBJECTS)
+libcockpit_tls_a_AR = $(AR) $(ARFLAGS)
+libcockpit_tls_a_LIBADD =
+am_libcockpit_tls_a_OBJECTS = src/tls/certificate.$(OBJEXT) \
+ src/tls/client-certificate.$(OBJEXT) \
+ src/tls/connection.$(OBJEXT) src/tls/httpredirect.$(OBJEXT) \
+ src/tls/server.$(OBJEXT) src/tls/socket-io.$(OBJEXT) \
+ $(am__objects_1)
+libcockpit_tls_a_OBJECTS = $(am_libcockpit_tls_a_OBJECTS)
+libcockpit_ws_a_AR = $(AR) $(ARFLAGS)
+libcockpit_ws_a_LIBADD =
+am_libcockpit_ws_a_OBJECTS = \
+ src/ws/libcockpit_ws_a-cockpithandlers.$(OBJEXT) \
+ src/ws/libcockpit_ws_a-cockpitauth.$(OBJEXT) \
+ src/ws/libcockpit_ws_a-cockpitcompat.$(OBJEXT) \
+ src/ws/libcockpit_ws_a-cockpitbranding.$(OBJEXT) \
+ src/ws/libcockpit_ws_a-cockpitchannelresponse.$(OBJEXT) \
+ src/ws/libcockpit_ws_a-cockpitchannelsocket.$(OBJEXT) \
+ src/ws/libcockpit_ws_a-cockpitcreds.$(OBJEXT) \
+ src/ws/libcockpit_ws_a-cockpitwebservice.$(OBJEXT) \
+ $(am__objects_1)
+libcockpit_ws_a_OBJECTS = $(am_libcockpit_ws_a_OBJECTS)
+libpam_ssh_add_a_AR = $(AR) $(ARFLAGS)
+libpam_ssh_add_a_LIBADD =
+am_libpam_ssh_add_a_OBJECTS = \
+ src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.$(OBJEXT) \
+ $(am__objects_1)
+libpam_ssh_add_a_OBJECTS = $(am_libpam_ssh_add_a_OBJECTS)
+libwebsocket_a_AR = $(AR) $(ARFLAGS)
+libwebsocket_a_LIBADD =
+am_libwebsocket_a_OBJECTS = \
+ src/websocket/libwebsocket_a-websocket.$(OBJEXT) \
+ src/websocket/libwebsocket_a-websocketclient.$(OBJEXT) \
+ src/websocket/libwebsocket_a-websocketserver.$(OBJEXT) \
+ src/websocket/libwebsocket_a-websocketconnection.$(OBJEXT) \
+ $(am__objects_1)
+libwebsocket_a_OBJECTS = $(am_libwebsocket_a_OBJECTS)
+am__cockpit_askpass_SOURCES_DIST = src/bridge/askpass.c
+@WITH_OLD_BRIDGE_TRUE@am_cockpit_askpass_OBJECTS = src/bridge/cockpit_askpass-askpass.$(OBJEXT)
+cockpit_askpass_OBJECTS = $(am_cockpit_askpass_OBJECTS)
+am__DEPENDENCIES_1 =
+am__DEPENDENCIES_2 = libwebsocket.a $(am__DEPENDENCIES_1) \
+ $(am__DEPENDENCIES_1)
+am__DEPENDENCIES_3 = libcockpit-common.a \
+ $(libcockpit_common_nodeps_a_LIBS) $(am__DEPENDENCIES_2) \
+ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \
+ $(am__DEPENDENCIES_1)
+am__DEPENDENCIES_4 = libcockpit-metrics.a $(am__DEPENDENCIES_3) \
+ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1)
+@WITH_OLD_BRIDGE_TRUE@@WITH_POLKIT_TRUE@am__DEPENDENCIES_5 = \
+@WITH_OLD_BRIDGE_TRUE@@WITH_POLKIT_TRUE@ $(am__DEPENDENCIES_1)
+@WITH_OLD_BRIDGE_TRUE@am__DEPENDENCIES_6 = libcockpit-bridge.a \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_4) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_1) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_1) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_5)
+@WITH_OLD_BRIDGE_TRUE@cockpit_askpass_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6)
+am__cockpit_bridge_SOURCES_DIST = src/bridge/bridge.c
+@WITH_OLD_BRIDGE_TRUE@am_cockpit_bridge_OBJECTS = src/bridge/cockpit_bridge-bridge.$(OBJEXT)
+cockpit_bridge_OBJECTS = $(am_cockpit_bridge_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@cockpit_bridge_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6)
+am_cockpit_certificate_ensure_OBJECTS = \
+ src/tls/cockpit-certificate-ensure.$(OBJEXT)
+cockpit_certificate_ensure_OBJECTS = \
+ $(am_cockpit_certificate_ensure_OBJECTS)
+am__DEPENDENCIES_7 = libcockpit-tls.a \
+ $(libcockpit_common_nodeps_a_LIBS) $(am__DEPENDENCIES_1) \
+ $(am__DEPENDENCIES_1)
+cockpit_certificate_ensure_DEPENDENCIES = $(am__DEPENDENCIES_7)
+am__cockpit_pcp_SOURCES_DIST = src/bridge/cockpitpcp.c
+@ENABLE_PCP_TRUE@am_cockpit_pcp_OBJECTS = \
+@ENABLE_PCP_TRUE@ src/bridge/cockpit_pcp-cockpitpcp.$(OBJEXT)
+cockpit_pcp_OBJECTS = $(am_cockpit_pcp_OBJECTS)
+@ENABLE_PCP_TRUE@am__DEPENDENCIES_8 = libcockpit-pcp.a \
+@ENABLE_PCP_TRUE@ $(am__DEPENDENCIES_4) $(am__DEPENDENCIES_1)
+@ENABLE_PCP_TRUE@cockpit_pcp_DEPENDENCIES = $(am__DEPENDENCIES_8)
+am_cockpit_session_OBJECTS = src/common/cockpitclosefrom.$(OBJEXT) \
+ src/session/client-certificate.$(OBJEXT) \
+ src/session/session-utils.$(OBJEXT) \
+ src/session/session.$(OBJEXT) $(am__objects_1)
+cockpit_session_OBJECTS = $(am_cockpit_session_OBJECTS)
+cockpit_session_DEPENDENCIES = $(libcockpit_common_nodeps_a_LIBS) \
+ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \
+ $(am__DEPENDENCIES_1)
+am__cockpit_ssh_SOURCES_DIST = src/ssh/ssh.c
+@WITH_COCKPIT_SSH_TRUE@am_cockpit_ssh_OBJECTS = \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/cockpit_ssh-ssh.$(OBJEXT)
+cockpit_ssh_OBJECTS = $(am_cockpit_ssh_OBJECTS)
+@WITH_COCKPIT_SSH_TRUE@am__DEPENDENCIES_9 = libcockpit-ssh.a \
+@WITH_COCKPIT_SSH_TRUE@ $(am__DEPENDENCIES_3) \
+@WITH_COCKPIT_SSH_TRUE@ $(am__DEPENDENCIES_1) \
+@WITH_COCKPIT_SSH_TRUE@ $(am__DEPENDENCIES_1)
+@WITH_COCKPIT_SSH_TRUE@cockpit_ssh_DEPENDENCIES = \
+@WITH_COCKPIT_SSH_TRUE@ $(am__DEPENDENCIES_9)
+am_cockpit_tls_OBJECTS = src/tls/main.$(OBJEXT)
+cockpit_tls_OBJECTS = $(am_cockpit_tls_OBJECTS)
+cockpit_tls_DEPENDENCIES = $(am__DEPENDENCIES_7) $(am__DEPENDENCIES_1)
+am_cockpit_ws_OBJECTS = src/ws/cockpit_ws-main.$(OBJEXT) \
+ $(am__objects_1)
+cockpit_ws_OBJECTS = $(am_cockpit_ws_OBJECTS)
+am__DEPENDENCIES_10 = libcockpit-ws.a $(am__DEPENDENCIES_3) \
+ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1)
+cockpit_ws_DEPENDENCIES = $(am__DEPENDENCIES_10)
+am_cockpit_wsinstance_factory_OBJECTS = src/tls/cockpit_wsinstance_factory-wsinstance-factory.$(OBJEXT)
+cockpit_wsinstance_factory_OBJECTS = \
+ $(am_cockpit_wsinstance_factory_OBJECTS)
+cockpit_wsinstance_factory_DEPENDENCIES = $(am__DEPENDENCIES_7) \
+ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1)
+am_frob_websocket_OBJECTS = \
+ src/websocket/frob_websocket-frob-websocket.$(OBJEXT)
+frob_websocket_OBJECTS = $(am_frob_websocket_OBJECTS)
+am__DEPENDENCIES_11 = libcockpit-test.a $(am__DEPENDENCIES_3) \
+ $(am__DEPENDENCIES_1)
+frob_websocket_DEPENDENCIES = $(am__DEPENDENCIES_2) \
+ $(am__DEPENDENCIES_11)
+am_libpreload_temp_home_so_OBJECTS = src/common/libpreload_temp_home_so-preload-temp-home.$(OBJEXT)
+libpreload_temp_home_so_OBJECTS = \
+ $(am_libpreload_temp_home_so_OBJECTS)
+libpreload_temp_home_so_LDADD = $(LDADD)
+libpreload_temp_home_so_LINK = $(CCLD) \
+ $(libpreload_temp_home_so_CFLAGS) $(CFLAGS) \
+ $(libpreload_temp_home_so_LDFLAGS) $(LDFLAGS) -o $@
+am_mock_auth_command_OBJECTS = \
+ src/ws/mock_auth_command-mock-auth-command.$(OBJEXT)
+mock_auth_command_OBJECTS = $(am_mock_auth_command_OBJECTS)
+mock_auth_command_DEPENDENCIES = $(am__DEPENDENCIES_3) \
+ $(am__DEPENDENCIES_11)
+am__mock_bridge_SOURCES_DIST = src/bridge/mock-bridge.c
+@WITH_OLD_BRIDGE_TRUE@am_mock_bridge_OBJECTS = src/bridge/mock_bridge-mock-bridge.$(OBJEXT)
+mock_bridge_OBJECTS = $(am_mock_bridge_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@mock_bridge_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6)
+am_mock_echo_OBJECTS = src/ws/mock_echo-mock-echo.$(OBJEXT)
+mock_echo_OBJECTS = $(am_mock_echo_OBJECTS)
+mock_echo_DEPENDENCIES = $(am__DEPENDENCIES_1)
+am_mock_pam_conv_mod_so_OBJECTS = \
+ src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.$(OBJEXT)
+mock_pam_conv_mod_so_OBJECTS = $(am_mock_pam_conv_mod_so_OBJECTS)
+mock_pam_conv_mod_so_DEPENDENCIES = $(am__DEPENDENCIES_1)
+mock_pam_conv_mod_so_LINK = $(CCLD) $(mock_pam_conv_mod_so_CFLAGS) \
+ $(CFLAGS) $(mock_pam_conv_mod_so_LDFLAGS) $(LDFLAGS) -o $@
+am__mock_pmda_so_SOURCES_DIST = src/bridge/mock-pmda.c
+@ENABLE_PCP_TRUE@am_mock_pmda_so_OBJECTS = \
+@ENABLE_PCP_TRUE@ src/bridge/mock_pmda_so-mock-pmda.$(OBJEXT)
+mock_pmda_so_OBJECTS = $(am_mock_pmda_so_OBJECTS)
+mock_pmda_so_DEPENDENCIES =
+mock_pmda_so_LINK = $(CCLD) $(mock_pmda_so_CFLAGS) $(CFLAGS) \
+ $(mock_pmda_so_LDFLAGS) $(LDFLAGS) -o $@
+am__mock_sshd_SOURCES_DIST = src/ssh/mock-sshd.c
+@WITH_COCKPIT_SSH_TRUE@am_mock_sshd_OBJECTS = \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/mock_sshd-mock-sshd.$(OBJEXT)
+mock_sshd_OBJECTS = $(am_mock_sshd_OBJECTS)
+@WITH_COCKPIT_SSH_TRUE@mock_sshd_DEPENDENCIES = $(am__DEPENDENCIES_9) \
+@WITH_COCKPIT_SSH_TRUE@ $(am__DEPENDENCIES_11)
+am_pam_cockpit_cert_so_OBJECTS = \
+ src/ws/pam_cockpit_cert_so-pam_cockpit_cert.$(OBJEXT)
+pam_cockpit_cert_so_OBJECTS = $(am_pam_cockpit_cert_so_OBJECTS)
+pam_cockpit_cert_so_DEPENDENCIES =
+pam_cockpit_cert_so_LINK = $(CCLD) $(pam_cockpit_cert_so_CFLAGS) \
+ $(CFLAGS) $(pam_cockpit_cert_so_LDFLAGS) $(LDFLAGS) -o $@
+am_pam_ssh_add_so_OBJECTS = \
+ src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.$(OBJEXT)
+pam_ssh_add_so_OBJECTS = $(am_pam_ssh_add_so_OBJECTS)
+am__DEPENDENCIES_12 = libpam_ssh_add.a $(am__DEPENDENCIES_1)
+pam_ssh_add_so_DEPENDENCIES = $(am__DEPENDENCIES_12)
+pam_ssh_add_so_LINK = $(CCLD) $(pam_ssh_add_so_CFLAGS) $(CFLAGS) \
+ $(pam_ssh_add_so_LDFLAGS) $(LDFLAGS) -o $@
+am_socket_activation_helper_OBJECTS = \
+ src/tls/socket-activation-helper.$(OBJEXT)
+socket_activation_helper_OBJECTS = \
+ $(am_socket_activation_helper_OBJECTS)
+socket_activation_helper_DEPENDENCIES = $(am__DEPENDENCIES_7)
+am_test_auth_OBJECTS = src/ws/test_auth-test-auth.$(OBJEXT)
+test_auth_OBJECTS = $(am_test_auth_OBJECTS)
+test_auth_DEPENDENCIES = $(am__DEPENDENCIES_10) $(am__DEPENDENCIES_11)
+am_test_authorize_OBJECTS = \
+ src/common/test_authorize-test-authorize.$(OBJEXT)
+test_authorize_OBJECTS = $(am_test_authorize_OBJECTS)
+test_authorize_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am__test_authssh_SOURCES_DIST = src/ws/test-authssh.c
+@WITH_COCKPIT_SSH_TRUE@@WITH_OLD_BRIDGE_TRUE@am_test_authssh_OBJECTS = src/ws/test_authssh-test-authssh.$(OBJEXT)
+test_authssh_OBJECTS = $(am_test_authssh_OBJECTS)
+@WITH_COCKPIT_SSH_TRUE@@WITH_OLD_BRIDGE_TRUE@test_authssh_DEPENDENCIES = $(am__DEPENDENCIES_10) \
+@WITH_COCKPIT_SSH_TRUE@@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_base64_OBJECTS = src/common/test_base64-test-base64.$(OBJEXT)
+test_base64_OBJECTS = $(am_test_base64_OBJECTS)
+test_base64_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am__test_bridge_SOURCES_DIST = src/bridge/test-bridge.c
+@WITH_OLD_BRIDGE_TRUE@am_test_bridge_OBJECTS = src/bridge/test_bridge-test-bridge.$(OBJEXT)
+test_bridge_OBJECTS = $(am_test_bridge_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_bridge_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_channel_OBJECTS = \
+ src/common/test_channel-test-channel.$(OBJEXT)
+test_channel_OBJECTS = $(am_test_channel_OBJECTS)
+test_channel_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am__test_channelresponse_SOURCES_DIST = src/ws/test-channelresponse.c
+@WITH_OLD_BRIDGE_TRUE@am_test_channelresponse_OBJECTS = src/ws/test_channelresponse-test-channelresponse.$(OBJEXT)
+test_channelresponse_OBJECTS = $(am_test_channelresponse_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_channelresponse_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_10) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_cockpit_certificate_ensure_OBJECTS = src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.$(OBJEXT)
+test_cockpit_certificate_ensure_OBJECTS = \
+ $(am_test_cockpit_certificate_ensure_OBJECTS)
+test_cockpit_certificate_ensure_DEPENDENCIES = $(am__DEPENDENCIES_7) \
+ $(am__DEPENDENCIES_11)
+am_test_compat_OBJECTS = src/ws/test_compat-test-compat.$(OBJEXT)
+test_compat_OBJECTS = $(am_test_compat_OBJECTS)
+test_compat_DEPENDENCIES = $(am__DEPENDENCIES_10) \
+ $(am__DEPENDENCIES_11)
+am_test_config_OBJECTS = src/common/test_config-test-config.$(OBJEXT)
+test_config_OBJECTS = $(am_test_config_OBJECTS)
+test_config_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am__test_connect_SOURCES_DIST = src/bridge/test-connect.c
+@WITH_OLD_BRIDGE_TRUE@am_test_connect_OBJECTS = src/bridge/test_connect-test-connect.$(OBJEXT)
+test_connect_OBJECTS = $(am_test_connect_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@am__DEPENDENCIES_13 = $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+@WITH_OLD_BRIDGE_TRUE@test_connect_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_13) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_creds_OBJECTS = src/ws/test_creds-test-creds.$(OBJEXT)
+test_creds_OBJECTS = $(am_test_creds_OBJECTS)
+test_creds_DEPENDENCIES = $(am__DEPENDENCIES_10) \
+ $(am__DEPENDENCIES_11)
+am__test_dbus_meta_SOURCES_DIST = src/bridge/test-dbus-meta.c
+@WITH_OLD_BRIDGE_TRUE@am_test_dbus_meta_OBJECTS = src/bridge/test_dbus_meta-test-dbus-meta.$(OBJEXT)
+test_dbus_meta_OBJECTS = $(am_test_dbus_meta_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_dbus_meta_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_frame_OBJECTS = src/common/test_frame-test-frame.$(OBJEXT)
+test_frame_OBJECTS = $(am_test_frame_OBJECTS)
+test_frame_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am__test_fs_SOURCES_DIST = src/bridge/test-fs.c
+@WITH_OLD_BRIDGE_TRUE@am_test_fs_OBJECTS = \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/test_fs-test-fs.$(OBJEXT)
+test_fs_OBJECTS = $(am_test_fs_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_fs_DEPENDENCIES = $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am__test_handlers_SOURCES_DIST = src/ws/test-handlers.c
+@WITH_OLD_BRIDGE_TRUE@am_test_handlers_OBJECTS = src/ws/test_handlers-test-handlers.$(OBJEXT)
+test_handlers_OBJECTS = $(am_test_handlers_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_handlers_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_10) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_hash_OBJECTS = src/common/test_hash-test-hash.$(OBJEXT)
+test_hash_OBJECTS = $(am_test_hash_OBJECTS)
+test_hash_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_hex_OBJECTS = src/common/test_hex-test-hex.$(OBJEXT)
+test_hex_OBJECTS = $(am_test_hex_OBJECTS)
+test_hex_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am__test_httpstream_SOURCES_DIST = src/bridge/test-httpstream.c
+@WITH_OLD_BRIDGE_TRUE@am_test_httpstream_OBJECTS = src/bridge/test_httpstream-test-httpstream.$(OBJEXT)
+test_httpstream_OBJECTS = $(am_test_httpstream_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_httpstream_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_json_OBJECTS = src/common/test_json-test-json.$(OBJEXT)
+test_json_OBJECTS = $(am_test_json_OBJECTS)
+test_json_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_jsonfds_OBJECTS = \
+ src/common/test_jsonfds-test-jsonfds.$(OBJEXT)
+test_jsonfds_OBJECTS = $(am_test_jsonfds_OBJECTS)
+test_jsonfds_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_kerberos_OBJECTS = \
+ src/ws/test_kerberos-test-kerberos.$(OBJEXT)
+test_kerberos_OBJECTS = $(am_test_kerberos_OBJECTS)
+test_kerberos_DEPENDENCIES = $(am__DEPENDENCIES_10) \
+ $(am__DEPENDENCIES_11) $(am__DEPENDENCIES_1)
+am_test_locale_OBJECTS = src/common/test_locale-test-locale.$(OBJEXT)
+test_locale_OBJECTS = $(am_test_locale_OBJECTS)
+test_locale_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am__test_metrics_SOURCES_DIST = src/bridge/test-metrics.c
+@WITH_OLD_BRIDGE_TRUE@am_test_metrics_OBJECTS = src/bridge/test_metrics-test-metrics.$(OBJEXT)
+test_metrics_OBJECTS = $(am_test_metrics_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_metrics_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am__test_packages_SOURCES_DIST = src/bridge/test-packages.c
+@WITH_OLD_BRIDGE_TRUE@am_test_packages_OBJECTS = src/bridge/test_packages-test-packages.$(OBJEXT)
+test_packages_OBJECTS = $(am_test_packages_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_packages_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am__test_packet_channel_SOURCES_DIST = \
+ src/bridge/test-packet-channel.c
+@WITH_OLD_BRIDGE_TRUE@am_test_packet_channel_OBJECTS = src/bridge/test_packet_channel-test-packet-channel.$(OBJEXT)
+test_packet_channel_OBJECTS = $(am_test_packet_channel_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_packet_channel_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am__test_paths_SOURCES_DIST = src/bridge/test-paths.c
+@WITH_OLD_BRIDGE_TRUE@am_test_paths_OBJECTS = src/bridge/test_paths-test-paths.$(OBJEXT)
+test_paths_OBJECTS = $(am_test_paths_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_paths_DEPENDENCIES = $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am__test_pcp_SOURCES_DIST = src/bridge/test-pcp.c
+@ENABLE_PCP_TRUE@am_test_pcp_OBJECTS = \
+@ENABLE_PCP_TRUE@ src/bridge/test_pcp-test-pcp.$(OBJEXT)
+test_pcp_OBJECTS = $(am_test_pcp_OBJECTS)
+@ENABLE_PCP_TRUE@test_pcp_DEPENDENCIES = $(am__DEPENDENCIES_8) \
+@ENABLE_PCP_TRUE@ $(am__DEPENDENCIES_11)
+am__test_pcp_archives_SOURCES_DIST = src/bridge/test-pcp-archives.c
+@ENABLE_PCP_TRUE@am_test_pcp_archives_OBJECTS = src/bridge/test_pcp_archives-test-pcp-archives.$(OBJEXT)
+test_pcp_archives_OBJECTS = $(am_test_pcp_archives_OBJECTS)
+@ENABLE_PCP_TRUE@test_pcp_archives_DEPENDENCIES = \
+@ENABLE_PCP_TRUE@ $(am__DEPENDENCIES_8) $(am__DEPENDENCIES_11)
+am__test_peer_SOURCES_DIST = src/bridge/test-peer.c
+@WITH_OLD_BRIDGE_TRUE@am_test_peer_OBJECTS = src/bridge/test_peer-test-peer.$(OBJEXT)
+test_peer_OBJECTS = $(am_test_peer_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_peer_DEPENDENCIES = $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_pipe_OBJECTS = src/common/test_pipe-test-pipe.$(OBJEXT)
+test_pipe_OBJECTS = $(am_test_pipe_OBJECTS)
+test_pipe_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am__test_pipe_channel_SOURCES_DIST = src/bridge/test-pipe-channel.c
+@WITH_OLD_BRIDGE_TRUE@am_test_pipe_channel_OBJECTS = src/bridge/test_pipe_channel-test-pipe-channel.$(OBJEXT)
+test_pipe_channel_OBJECTS = $(am_test_pipe_channel_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_pipe_channel_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am__test_process_SOURCES_DIST = src/bridge/test-process.c
+@WITH_OLD_BRIDGE_TRUE@am_test_process_OBJECTS = src/bridge/test_process-test-process.$(OBJEXT)
+test_process_OBJECTS = $(am_test_process_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_process_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am__test_router_SOURCES_DIST = src/bridge/test-router.c
+@WITH_OLD_BRIDGE_TRUE@am_test_router_OBJECTS = src/bridge/test_router-test-router.$(OBJEXT)
+test_router_OBJECTS = $(am_test_router_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_router_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am__test_rules_SOURCES_DIST = src/bridge/test-rules.c
+@WITH_OLD_BRIDGE_TRUE@am_test_rules_OBJECTS = src/bridge/test_rules-test-rules.$(OBJEXT)
+test_rules_OBJECTS = $(am_test_rules_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_rules_DEPENDENCIES = $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_server_OBJECTS = src/ws/test_server-mock-service.$(OBJEXT) \
+ src/ws/test_server-test-server.$(OBJEXT) $(am__objects_1)
+am__objects_3 = src/ws/test_server-mock-dbus-tests.$(OBJEXT) \
+ $(am__objects_1)
+nodist_test_server_OBJECTS = $(am__objects_3)
+test_server_OBJECTS = $(am_test_server_OBJECTS) \
+ $(nodist_test_server_OBJECTS)
+test_server_DEPENDENCIES = $(am__DEPENDENCIES_10) \
+ $(am__DEPENDENCIES_11)
+am_test_ssh_add_OBJECTS = \
+ src/pam-ssh-add/test_ssh_add-test-ssh-add.$(OBJEXT)
+test_ssh_add_OBJECTS = $(am_test_ssh_add_OBJECTS)
+test_ssh_add_DEPENDENCIES = $(am__DEPENDENCIES_12) \
+ $(am__DEPENDENCIES_11)
+am__test_sshbridge_SOURCES_DIST = src/ssh/test-sshbridge.c
+@WITH_COCKPIT_SSH_TRUE@am_test_sshbridge_OBJECTS = src/ssh/test_sshbridge-test-sshbridge.$(OBJEXT)
+test_sshbridge_OBJECTS = $(am_test_sshbridge_OBJECTS)
+@WITH_COCKPIT_SSH_TRUE@test_sshbridge_DEPENDENCIES = \
+@WITH_COCKPIT_SSH_TRUE@ $(am__DEPENDENCIES_9) \
+@WITH_COCKPIT_SSH_TRUE@ $(am__DEPENDENCIES_11)
+am__test_sshoptions_SOURCES_DIST = src/ssh/test-sshoptions.c
+@WITH_COCKPIT_SSH_TRUE@am_test_sshoptions_OBJECTS = src/ssh/test_sshoptions-test-sshoptions.$(OBJEXT)
+test_sshoptions_OBJECTS = $(am_test_sshoptions_OBJECTS)
+@WITH_COCKPIT_SSH_TRUE@test_sshoptions_DEPENDENCIES = \
+@WITH_COCKPIT_SSH_TRUE@ $(am__DEPENDENCIES_9) \
+@WITH_COCKPIT_SSH_TRUE@ $(am__DEPENDENCIES_11)
+am__test_stream_SOURCES_DIST = src/bridge/test-stream.c
+@WITH_OLD_BRIDGE_TRUE@am_test_stream_OBJECTS = src/bridge/test_stream-test-stream.$(OBJEXT)
+test_stream_OBJECTS = $(am_test_stream_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_stream_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_system_OBJECTS = src/common/test_system-test-system.$(OBJEXT)
+test_system_OBJECTS = $(am_test_system_OBJECTS)
+test_system_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_template_OBJECTS = \
+ src/common/test_template-test-template.$(OBJEXT)
+test_template_OBJECTS = $(am_test_template_OBJECTS)
+test_template_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_tls_connection_OBJECTS = \
+ src/tls/test_tls_connection-test-connection.$(OBJEXT)
+test_tls_connection_OBJECTS = $(am_test_tls_connection_OBJECTS)
+test_tls_connection_DEPENDENCIES = $(am__DEPENDENCIES_7) \
+ $(am__DEPENDENCIES_11)
+am_test_tls_server_OBJECTS = \
+ src/tls/test_tls_server-test-server.$(OBJEXT)
+test_tls_server_OBJECTS = $(am_test_tls_server_OBJECTS)
+test_tls_server_DEPENDENCIES = $(am__DEPENDENCIES_7) \
+ $(am__DEPENDENCIES_11)
+am_test_transport_OBJECTS = \
+ src/common/test_transport-test-transport.$(OBJEXT)
+test_transport_OBJECTS = $(am_test_transport_OBJECTS)
+test_transport_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_unicode_OBJECTS = \
+ src/common/test_unicode-test-unicode.$(OBJEXT)
+test_unicode_OBJECTS = $(am_test_unicode_OBJECTS)
+test_unicode_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_unixsignal_OBJECTS = \
+ src/common/test_unixsignal-test-unixsignal.$(OBJEXT)
+test_unixsignal_OBJECTS = $(am_test_unixsignal_OBJECTS)
+test_unixsignal_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_version_OBJECTS = \
+ src/common/test_version-test-version.$(OBJEXT)
+test_version_OBJECTS = $(am_test_version_OBJECTS)
+test_version_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_webcertificate_OBJECTS = \
+ src/common/test_webcertificate-test-webcertificate.$(OBJEXT)
+test_webcertificate_OBJECTS = $(am_test_webcertificate_OBJECTS)
+test_webcertificate_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_webresponse_OBJECTS = \
+ src/common/test_webresponse-test-webresponse.$(OBJEXT)
+test_webresponse_OBJECTS = $(am_test_webresponse_OBJECTS)
+test_webresponse_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am_test_webserver_OBJECTS = \
+ src/common/test_webserver-test-webserver.$(OBJEXT)
+test_webserver_OBJECTS = $(am_test_webserver_OBJECTS)
+test_webserver_DEPENDENCIES = $(am__DEPENDENCIES_11)
+am__test_webservice_SOURCES_DIST = src/ws/test-webservice.c
+@WITH_OLD_BRIDGE_TRUE@am_test_webservice_OBJECTS = src/ws/test_webservice-test-webservice.$(OBJEXT)
+test_webservice_OBJECTS = $(am_test_webservice_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_webservice_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_10) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_test_websocket_OBJECTS = \
+ src/websocket/test_websocket-test-websocket.$(OBJEXT)
+test_websocket_OBJECTS = $(am_test_websocket_OBJECTS)
+test_websocket_DEPENDENCIES = $(am__DEPENDENCIES_2) \
+ $(am__DEPENDENCIES_11)
+am__test_websocketstream_SOURCES_DIST = \
+ src/bridge/test-websocketstream.c
+@WITH_OLD_BRIDGE_TRUE@am_test_websocketstream_OBJECTS = src/bridge/test_websocketstream-test-websocketstream.$(OBJEXT)
+test_websocketstream_OBJECTS = $(am_test_websocketstream_OBJECTS)
+@WITH_OLD_BRIDGE_TRUE@test_websocketstream_DEPENDENCIES = \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_6) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__DEPENDENCIES_11)
+am_wsinstance_start_OBJECTS = src/tls/wsinstance-start.$(OBJEXT)
+wsinstance_start_OBJECTS = $(am_wsinstance_start_OBJECTS)
+wsinstance_start_DEPENDENCIES = $(am__DEPENDENCIES_7)
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+SCRIPTS = $(dist_cockpitclient_SCRIPTS) $(dist_motd_SCRIPTS) \
+ $(libexec_SCRIPTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@
+depcomp = $(SHELL) $(top_srcdir)/tools/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = src/bridge/$(DEPDIR)/cockpit_askpass-askpass.Po \
+ src/bridge/$(DEPDIR)/cockpit_bridge-bridge.Po \
+ src/bridge/$(DEPDIR)/cockpit_pcp-cockpitpcp.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitconnect.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbuscache.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusconfig.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusinternal.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusjson.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusloginmessages.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmachines.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmeta.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusprocess.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusrules.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbususer.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitechochannel.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfslist.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsread.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsreplace.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfswatch.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpithttpstream.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitinteracttransport.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitnullchannel.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpackages.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpacketchannel.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpaths.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpeer.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpipechannel.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpolkitagent.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitrouter.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitstream.Po \
+ src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitwebsocketstream.Po \
+ src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitblocksamples.Po \
+ src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcgroupsamples.Po \
+ src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcpusamples.Po \
+ src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitdisksamples.Po \
+ src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitinternalmetrics.Po \
+ src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmemorysamples.Po \
+ src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmetrics.Po \
+ src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmountsamples.Po \
+ src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitnetworksamples.Po \
+ src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitsamples.Po \
+ src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitconnect.Po \
+ src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitdbusinternal.Po \
+ src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpcpmetrics.Po \
+ src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpeer.Po \
+ src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitrouter.Po \
+ src/bridge/$(DEPDIR)/mock_bridge-mock-bridge.Po \
+ src/bridge/$(DEPDIR)/mock_pmda_so-mock-pmda.Po \
+ src/bridge/$(DEPDIR)/test_bridge-test-bridge.Po \
+ src/bridge/$(DEPDIR)/test_connect-test-connect.Po \
+ src/bridge/$(DEPDIR)/test_dbus_meta-test-dbus-meta.Po \
+ src/bridge/$(DEPDIR)/test_fs-test-fs.Po \
+ src/bridge/$(DEPDIR)/test_httpstream-test-httpstream.Po \
+ src/bridge/$(DEPDIR)/test_metrics-test-metrics.Po \
+ src/bridge/$(DEPDIR)/test_packages-test-packages.Po \
+ src/bridge/$(DEPDIR)/test_packet_channel-test-packet-channel.Po \
+ src/bridge/$(DEPDIR)/test_paths-test-paths.Po \
+ src/bridge/$(DEPDIR)/test_pcp-test-pcp.Po \
+ src/bridge/$(DEPDIR)/test_pcp_archives-test-pcp-archives.Po \
+ src/bridge/$(DEPDIR)/test_peer-test-peer.Po \
+ src/bridge/$(DEPDIR)/test_pipe_channel-test-pipe-channel.Po \
+ src/bridge/$(DEPDIR)/test_process-test-process.Po \
+ src/bridge/$(DEPDIR)/test_router-test-router.Po \
+ src/bridge/$(DEPDIR)/test_rules-test-rules.Po \
+ src/bridge/$(DEPDIR)/test_stream-test-stream.Po \
+ src/bridge/$(DEPDIR)/test_websocketstream-test-websocketstream.Po \
+ src/common/$(DEPDIR)/cockpitauthorize.Po \
+ src/common/$(DEPDIR)/cockpitbase64.Po \
+ src/common/$(DEPDIR)/cockpitclosefrom.Po \
+ src/common/$(DEPDIR)/cockpitconf.Po \
+ src/common/$(DEPDIR)/cockpitfdpassing.Po \
+ src/common/$(DEPDIR)/cockpitframe.Po \
+ src/common/$(DEPDIR)/cockpithex.Po \
+ src/common/$(DEPDIR)/cockpitjsonprint.Po \
+ src/common/$(DEPDIR)/cockpitmemory.Po \
+ src/common/$(DEPDIR)/cockpitwebcertificate.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitchannel.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitclosefrom.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitcontrolmessages.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpiterror.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitflow.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpithash.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitjson.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitlocale.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitloopback.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitmachinesjson.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitmemfdread.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipe.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipetransport.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitsocket.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitsystem.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpittemplate.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpittransport.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitunicode.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitunixsignal.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitversion.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebfilter.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebinject.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebresponse.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebserver.Po \
+ src/common/$(DEPDIR)/libcockpit_common_a-fail.html.Po \
+ src/common/$(DEPDIR)/libpreload_temp_home_so-preload-temp-home.Po \
+ src/common/$(DEPDIR)/test_authorize-test-authorize.Po \
+ src/common/$(DEPDIR)/test_base64-test-base64.Po \
+ src/common/$(DEPDIR)/test_channel-test-channel.Po \
+ src/common/$(DEPDIR)/test_config-test-config.Po \
+ src/common/$(DEPDIR)/test_frame-test-frame.Po \
+ src/common/$(DEPDIR)/test_hash-test-hash.Po \
+ src/common/$(DEPDIR)/test_hex-test-hex.Po \
+ src/common/$(DEPDIR)/test_json-test-json.Po \
+ src/common/$(DEPDIR)/test_jsonfds-test-jsonfds.Po \
+ src/common/$(DEPDIR)/test_locale-test-locale.Po \
+ src/common/$(DEPDIR)/test_pipe-test-pipe.Po \
+ src/common/$(DEPDIR)/test_system-test-system.Po \
+ src/common/$(DEPDIR)/test_template-test-template.Po \
+ src/common/$(DEPDIR)/test_transport-test-transport.Po \
+ src/common/$(DEPDIR)/test_unicode-test-unicode.Po \
+ src/common/$(DEPDIR)/test_unixsignal-test-unixsignal.Po \
+ src/common/$(DEPDIR)/test_version-test-version.Po \
+ src/common/$(DEPDIR)/test_webcertificate-test-webcertificate.Po \
+ src/common/$(DEPDIR)/test_webresponse-test-webresponse.Po \
+ src/common/$(DEPDIR)/test_webserver-test-webserver.Po \
+ src/pam-ssh-add/$(DEPDIR)/libpam_ssh_add_a-pam-ssh-add.Po \
+ src/pam-ssh-add/$(DEPDIR)/pam_ssh_add_so-pam-ssh-add.Po \
+ src/pam-ssh-add/$(DEPDIR)/test_ssh_add-test-ssh-add.Po \
+ src/session/$(DEPDIR)/client-certificate.Po \
+ src/session/$(DEPDIR)/session-utils.Po \
+ src/session/$(DEPDIR)/session.Po \
+ src/ssh/$(DEPDIR)/cockpit_ssh-ssh.Po \
+ src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshoptions.Po \
+ src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshrelay.Po \
+ src/ssh/$(DEPDIR)/mock_sshd-mock-sshd.Po \
+ src/ssh/$(DEPDIR)/test_sshbridge-test-sshbridge.Po \
+ src/ssh/$(DEPDIR)/test_sshoptions-test-sshoptions.Po \
+ src/testlib/$(DEPDIR)/libcockpit_test_a-cockpittest.Po \
+ src/testlib/$(DEPDIR)/libcockpit_test_a-mock-auth.Po \
+ src/testlib/$(DEPDIR)/libcockpit_test_a-mock-channel.Po \
+ src/testlib/$(DEPDIR)/libcockpit_test_a-mock-pressure.Po \
+ src/testlib/$(DEPDIR)/libcockpit_test_a-mock-transport.Po \
+ src/testlib/$(DEPDIR)/libcockpit_test_a-retest.Po \
+ src/tls/$(DEPDIR)/certificate.Po \
+ src/tls/$(DEPDIR)/client-certificate.Po \
+ src/tls/$(DEPDIR)/cockpit-certificate-ensure.Po \
+ src/tls/$(DEPDIR)/cockpit_wsinstance_factory-wsinstance-factory.Po \
+ src/tls/$(DEPDIR)/connection.Po \
+ src/tls/$(DEPDIR)/httpredirect.Po src/tls/$(DEPDIR)/main.Po \
+ src/tls/$(DEPDIR)/server.Po \
+ src/tls/$(DEPDIR)/socket-activation-helper.Po \
+ src/tls/$(DEPDIR)/socket-io.Po \
+ src/tls/$(DEPDIR)/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.Po \
+ src/tls/$(DEPDIR)/test_tls_connection-test-connection.Po \
+ src/tls/$(DEPDIR)/test_tls_server-test-server.Po \
+ src/tls/$(DEPDIR)/wsinstance-start.Po \
+ src/websocket/$(DEPDIR)/frob_websocket-frob-websocket.Po \
+ src/websocket/$(DEPDIR)/libwebsocket_a-websocket.Po \
+ src/websocket/$(DEPDIR)/libwebsocket_a-websocketclient.Po \
+ src/websocket/$(DEPDIR)/libwebsocket_a-websocketconnection.Po \
+ src/websocket/$(DEPDIR)/libwebsocket_a-websocketserver.Po \
+ src/websocket/$(DEPDIR)/test_websocket-test-websocket.Po \
+ src/ws/$(DEPDIR)/cockpit_ws-main.Po \
+ src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitauth.Po \
+ src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitbranding.Po \
+ src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelresponse.Po \
+ src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelsocket.Po \
+ src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcompat.Po \
+ src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcreds.Po \
+ src/ws/$(DEPDIR)/libcockpit_ws_a-cockpithandlers.Po \
+ src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitwebservice.Po \
+ src/ws/$(DEPDIR)/mock_auth_command-mock-auth-command.Po \
+ src/ws/$(DEPDIR)/mock_echo-mock-echo.Po \
+ src/ws/$(DEPDIR)/mock_pam_conv_mod_so-mock-pam-conv-mod.Po \
+ src/ws/$(DEPDIR)/pam_cockpit_cert_so-pam_cockpit_cert.Po \
+ src/ws/$(DEPDIR)/test_auth-test-auth.Po \
+ src/ws/$(DEPDIR)/test_authssh-test-authssh.Po \
+ src/ws/$(DEPDIR)/test_channelresponse-test-channelresponse.Po \
+ src/ws/$(DEPDIR)/test_compat-test-compat.Po \
+ src/ws/$(DEPDIR)/test_creds-test-creds.Po \
+ src/ws/$(DEPDIR)/test_handlers-test-handlers.Po \
+ src/ws/$(DEPDIR)/test_kerberos-test-kerberos.Po \
+ src/ws/$(DEPDIR)/test_server-mock-dbus-tests.Po \
+ src/ws/$(DEPDIR)/test_server-mock-service.Po \
+ src/ws/$(DEPDIR)/test_server-test-server.Po \
+ src/ws/$(DEPDIR)/test_webservice-test-webservice.Po
+am__mv = mv -f
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(CCLD) $(AM_CFLAGS) $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+SOURCES = $(libcockpit_bridge_a_SOURCES) \
+ $(libcockpit_common_nodeps_a_SOURCES) \
+ $(libcockpit_common_a_SOURCES) \
+ $(nodist_libcockpit_common_a_SOURCES) \
+ $(libcockpit_metrics_a_SOURCES) $(libcockpit_pcp_a_SOURCES) \
+ $(libcockpit_ssh_a_SOURCES) $(libcockpit_test_a_SOURCES) \
+ $(libcockpit_tls_a_SOURCES) $(libcockpit_ws_a_SOURCES) \
+ $(libpam_ssh_add_a_SOURCES) $(libwebsocket_a_SOURCES) \
+ $(cockpit_askpass_SOURCES) $(cockpit_bridge_SOURCES) \
+ $(cockpit_certificate_ensure_SOURCES) $(cockpit_pcp_SOURCES) \
+ $(cockpit_session_SOURCES) $(cockpit_ssh_SOURCES) \
+ $(cockpit_tls_SOURCES) $(cockpit_ws_SOURCES) \
+ $(cockpit_wsinstance_factory_SOURCES) \
+ $(frob_websocket_SOURCES) $(libpreload_temp_home_so_SOURCES) \
+ $(mock_auth_command_SOURCES) $(mock_bridge_SOURCES) \
+ $(mock_echo_SOURCES) $(mock_pam_conv_mod_so_SOURCES) \
+ $(mock_pmda_so_SOURCES) $(mock_sshd_SOURCES) \
+ $(pam_cockpit_cert_so_SOURCES) $(pam_ssh_add_so_SOURCES) \
+ $(socket_activation_helper_SOURCES) $(test_auth_SOURCES) \
+ $(test_authorize_SOURCES) $(test_authssh_SOURCES) \
+ $(test_base64_SOURCES) $(test_bridge_SOURCES) \
+ $(test_channel_SOURCES) $(test_channelresponse_SOURCES) \
+ $(test_cockpit_certificate_ensure_SOURCES) \
+ $(test_compat_SOURCES) $(test_config_SOURCES) \
+ $(test_connect_SOURCES) $(test_creds_SOURCES) \
+ $(test_dbus_meta_SOURCES) $(test_frame_SOURCES) \
+ $(test_fs_SOURCES) $(test_handlers_SOURCES) \
+ $(test_hash_SOURCES) $(test_hex_SOURCES) \
+ $(test_httpstream_SOURCES) $(test_json_SOURCES) \
+ $(test_jsonfds_SOURCES) $(test_kerberos_SOURCES) \
+ $(test_locale_SOURCES) $(test_metrics_SOURCES) \
+ $(test_packages_SOURCES) $(test_packet_channel_SOURCES) \
+ $(test_paths_SOURCES) $(test_pcp_SOURCES) \
+ $(test_pcp_archives_SOURCES) $(test_peer_SOURCES) \
+ $(test_pipe_SOURCES) $(test_pipe_channel_SOURCES) \
+ $(test_process_SOURCES) $(test_router_SOURCES) \
+ $(test_rules_SOURCES) $(test_server_SOURCES) \
+ $(nodist_test_server_SOURCES) $(test_ssh_add_SOURCES) \
+ $(test_sshbridge_SOURCES) $(test_sshoptions_SOURCES) \
+ $(test_stream_SOURCES) $(test_system_SOURCES) \
+ $(test_template_SOURCES) $(test_tls_connection_SOURCES) \
+ $(test_tls_server_SOURCES) $(test_transport_SOURCES) \
+ $(test_unicode_SOURCES) $(test_unixsignal_SOURCES) \
+ $(test_version_SOURCES) $(test_webcertificate_SOURCES) \
+ $(test_webresponse_SOURCES) $(test_webserver_SOURCES) \
+ $(test_webservice_SOURCES) $(test_websocket_SOURCES) \
+ $(test_websocketstream_SOURCES) $(wsinstance_start_SOURCES)
+DIST_SOURCES = $(am__libcockpit_bridge_a_SOURCES_DIST) \
+ $(libcockpit_common_nodeps_a_SOURCES) \
+ $(libcockpit_common_a_SOURCES) $(libcockpit_metrics_a_SOURCES) \
+ $(am__libcockpit_pcp_a_SOURCES_DIST) \
+ $(am__libcockpit_ssh_a_SOURCES_DIST) \
+ $(libcockpit_test_a_SOURCES) $(libcockpit_tls_a_SOURCES) \
+ $(libcockpit_ws_a_SOURCES) $(libpam_ssh_add_a_SOURCES) \
+ $(libwebsocket_a_SOURCES) $(am__cockpit_askpass_SOURCES_DIST) \
+ $(am__cockpit_bridge_SOURCES_DIST) \
+ $(cockpit_certificate_ensure_SOURCES) \
+ $(am__cockpit_pcp_SOURCES_DIST) $(cockpit_session_SOURCES) \
+ $(am__cockpit_ssh_SOURCES_DIST) $(cockpit_tls_SOURCES) \
+ $(cockpit_ws_SOURCES) $(cockpit_wsinstance_factory_SOURCES) \
+ $(frob_websocket_SOURCES) $(libpreload_temp_home_so_SOURCES) \
+ $(mock_auth_command_SOURCES) $(am__mock_bridge_SOURCES_DIST) \
+ $(mock_echo_SOURCES) $(mock_pam_conv_mod_so_SOURCES) \
+ $(am__mock_pmda_so_SOURCES_DIST) $(am__mock_sshd_SOURCES_DIST) \
+ $(pam_cockpit_cert_so_SOURCES) $(pam_ssh_add_so_SOURCES) \
+ $(socket_activation_helper_SOURCES) $(test_auth_SOURCES) \
+ $(test_authorize_SOURCES) $(am__test_authssh_SOURCES_DIST) \
+ $(test_base64_SOURCES) $(am__test_bridge_SOURCES_DIST) \
+ $(test_channel_SOURCES) \
+ $(am__test_channelresponse_SOURCES_DIST) \
+ $(test_cockpit_certificate_ensure_SOURCES) \
+ $(test_compat_SOURCES) $(test_config_SOURCES) \
+ $(am__test_connect_SOURCES_DIST) $(test_creds_SOURCES) \
+ $(am__test_dbus_meta_SOURCES_DIST) $(test_frame_SOURCES) \
+ $(am__test_fs_SOURCES_DIST) $(am__test_handlers_SOURCES_DIST) \
+ $(test_hash_SOURCES) $(test_hex_SOURCES) \
+ $(am__test_httpstream_SOURCES_DIST) $(test_json_SOURCES) \
+ $(test_jsonfds_SOURCES) $(test_kerberos_SOURCES) \
+ $(test_locale_SOURCES) $(am__test_metrics_SOURCES_DIST) \
+ $(am__test_packages_SOURCES_DIST) \
+ $(am__test_packet_channel_SOURCES_DIST) \
+ $(am__test_paths_SOURCES_DIST) $(am__test_pcp_SOURCES_DIST) \
+ $(am__test_pcp_archives_SOURCES_DIST) \
+ $(am__test_peer_SOURCES_DIST) $(test_pipe_SOURCES) \
+ $(am__test_pipe_channel_SOURCES_DIST) \
+ $(am__test_process_SOURCES_DIST) \
+ $(am__test_router_SOURCES_DIST) $(am__test_rules_SOURCES_DIST) \
+ $(test_server_SOURCES) $(test_ssh_add_SOURCES) \
+ $(am__test_sshbridge_SOURCES_DIST) \
+ $(am__test_sshoptions_SOURCES_DIST) \
+ $(am__test_stream_SOURCES_DIST) $(test_system_SOURCES) \
+ $(test_template_SOURCES) $(test_tls_connection_SOURCES) \
+ $(test_tls_server_SOURCES) $(test_transport_SOURCES) \
+ $(test_unicode_SOURCES) $(test_unixsignal_SOURCES) \
+ $(test_version_SOURCES) $(test_webcertificate_SOURCES) \
+ $(test_webresponse_SOURCES) $(test_webserver_SOURCES) \
+ $(am__test_webservice_SOURCES_DIST) $(test_websocket_SOURCES) \
+ $(am__test_websocketstream_SOURCES_DIST) \
+ $(wsinstance_start_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+man1dir = $(mandir)/man1
+man5dir = $(mandir)/man5
+man8dir = $(mandir)/man8
+NROFF = nroff
+MANS = $(man_MANS)
+am__dist_applications_DATA_DIST = \
+ src/client/org.cockpit_project.CockpitClient.desktop
+am__dist_check_DATA_DIST = src/bridge/mock-resource \
+ src/bridge/mock-pmda.c src/bridge/mock-pmns \
+ src/bridge/mock-client.crt src/bridge/mock-client.key \
+ src/bridge/mock-server.crt src/bridge/mock-server.key \
+ src/common/mock-content src/pam-ssh-add/mock-ssh-agent \
+ src/pam-ssh-add/mock-ssh-add src/pam-ssh-add/mock-environment \
+ src/ssh/mock_rsa_key src/ssh/mock_ecdsa_key src/ssh/test_rsa \
+ src/ssh/test_rsa.pub src/ssh/mock_known_hosts \
+ src/ssh/mock-pid-cat src/ssh/mock-config \
+ src/ssh/invalid_known_hosts src/tls/ca/alice-expired.pem \
+ src/tls/ca/alice.key src/tls/ca/alice.p12 src/tls/ca/alice.pem \
+ src/tls/ca/bob.key src/tls/ca/bob.p12 src/tls/ca/bob.pem \
+ src/tls/ca/ca.conf src/tls/ca/ca.key src/tls/ca/ca.pem \
+ src/tls/ca/generate.sh src/ws/mock-combined.crt \
+ src/ws/mock-ecc.crt src/ws/mock-ecc.key \
+ src/ws/mock-cat-with-init src/ws/mock-kdc \
+ src/ws/mock-krb5.conf.in src/ws/mock-kdc.conf.in \
+ src/ws/mock-static src/ws/mock-config \
+ src/ws/mock-pipes/exit127 src/ws/mock-pipes/someprogram \
+ src/ws/mock-pipes/cockpit-session
+am__dist_client_metainfo_DATA_DIST = \
+ src/client/org.cockpit_project.CockpitClient.metainfo.xml
+am__dist_dbusservices_DATA_DIST = \
+ src/client/org.cockpit_project.CockpitClient.service
+am__dist_noinst_DATA_DIST = doc/guide/cockpit-guide.xml \
+ doc/guide/api-cockpit.xml doc/guide/api-base1.xml \
+ doc/guide/api-shell.xml doc/guide/api-system.xml \
+ doc/guide/cockpit-cache.xml doc/guide/cockpit-channel.xml \
+ doc/guide/cockpit-dbus.xml doc/guide/cockpit-error.xml \
+ doc/guide/cockpit-file.xml doc/guide/cockpit-http.xml \
+ doc/guide/cockpit-locale.xml doc/guide/cockpit-location.xml \
+ doc/guide/cockpit-manifest.xml doc/guide/cockpit-metrics.xml \
+ doc/guide/cockpit-series.xml doc/guide/cockpit-session.xml \
+ doc/guide/cockpit-spawn.xml doc/guide/cockpit-util.xml \
+ doc/guide/authentication.xml doc/guide/embedding.xml \
+ doc/guide/feature-firewall.xml doc/guide/feature-journal.xml \
+ doc/guide/feature-machines.xml \
+ doc/guide/feature-networkmanager.xml \
+ doc/guide/feature-packagekit.xml doc/guide/feature-pcp.xml \
+ doc/guide/feature-realmd.xml doc/guide/feature-selinux.xml \
+ doc/guide/feature-sosreport.xml doc/guide/feature-storaged.xml \
+ doc/guide/feature-systemd.xml doc/guide/feature-terminal.xml \
+ doc/guide/feature-tuned.xml doc/guide/feature-users.xml \
+ doc/guide/packages.xml doc/guide/privileges.xml \
+ doc/guide/https.xml doc/guide/listen.xml doc/guide/sso.xml \
+ doc/guide/cert-authentication.xml doc/guide/startup.xml \
+ doc/guide/urls.xml doc/guide/gtk-doc.xsl \
+ doc/guide/version-greater-or-equal.xsl \
+ doc/guide/static/home.png doc/guide/static/gtk-doc.css \
+ doc/guide/static/left.png doc/guide/static/right.png \
+ doc/guide/static/style.css doc/guide/static/up.png \
+ selinux/cockpit.fc selinux/cockpit.if selinux/cockpit.te \
+ selinux/cockpit_session_selinux.8cockpit \
+ selinux/cockpit_ws_selinux.8cockpit
+am__dist_pcpmanifest_DATA_DIST = pkg/pcp/manifest.json
+am__dist_pmlogconf_DATA_DIST = src/bridge/pmlogconf/cockpit
+am__dist_scalableicon_DATA_DIST = src/client/cockpit-client.svg
+am__dist_sshmanifest_DATA_DIST = src/ssh/manifest.json
+am__dist_symbolicicon_DATA_DIST = \
+ src/client/cockpit-client-symbolic.svg
+DATA = $(dist_applications_DATA) $(dist_archbranding_DATA) \
+ $(dist_centosbranding_DATA) $(dist_client_metainfo_DATA) \
+ $(dist_cockpitclient_DATA) $(dist_dbusservices_DATA) \
+ $(dist_debianbranding_DATA) $(dist_defaultbranding_DATA) \
+ $(dist_fedorabranding_DATA) $(dist_kubernetesbranding_DATA) \
+ $(dist_motd_DATA) $(dist_noinst_DATA) \
+ $(dist_opensusebranding_DATA) $(dist_pcpmanifest_DATA) \
+ $(dist_pixmap_DATA) $(dist_pmlogconf_DATA) \
+ $(dist_registrybranding_DATA) $(dist_rhelbranding_DATA) \
+ $(dist_scalableicon_DATA) $(dist_scientificbranding_DATA) \
+ $(dist_sshmanifest_DATA) $(dist_symbolicicon_DATA) \
+ $(dist_systemdunit_DATA) $(dist_ubuntubranding_DATA) \
+ $(nodist_appdata_DATA) $(nodist_metainfo_DATA) \
+ $(nodist_systemdunit_DATA) $(nodist_tempconf_DATA) \
+ $(noinst_DATA) $(pixmaps_DATA) $(polkit_DATA) \
+ $(selinuxpackages_DATA)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) \
+ config.h.in
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+AM_RECURSIVE_TARGETS = cscope check recheck
+am__tty_colors_dummy = \
+ mgn= red= grn= lgn= blu= brg= std=; \
+ am__color_tests=no
+am__tty_colors = { \
+ $(am__tty_colors_dummy); \
+ if test "X$(AM_COLOR_TESTS)" = Xno; then \
+ am__color_tests=no; \
+ elif test "X$(AM_COLOR_TESTS)" = Xalways; then \
+ am__color_tests=yes; \
+ elif test "X$$TERM" != Xdumb && { test -t 1; } 2>/dev/null; then \
+ am__color_tests=yes; \
+ fi; \
+ if test $$am__color_tests = yes; then \
+ red=''; \
+ grn=''; \
+ lgn=''; \
+ blu=''; \
+ mgn=''; \
+ brg=''; \
+ std=''; \
+ fi; \
+}
+am__recheck_rx = ^[ ]*:recheck:[ ]*
+am__global_test_result_rx = ^[ ]*:global-test-result:[ ]*
+am__copy_in_global_log_rx = ^[ ]*:copy-in-global-log:[ ]*
+# A command that, given a newline-separated list of test names on the
+# standard input, print the name of the tests that are to be re-run
+# upon "make recheck".
+am__list_recheck_tests = $(AWK) '{ \
+ recheck = 1; \
+ while ((rc = (getline line < ($$0 ".trs"))) != 0) \
+ { \
+ if (rc < 0) \
+ { \
+ if ((getline line2 < ($$0 ".log")) < 0) \
+ recheck = 0; \
+ break; \
+ } \
+ else if (line ~ /$(am__recheck_rx)[nN][Oo]/) \
+ { \
+ recheck = 0; \
+ break; \
+ } \
+ else if (line ~ /$(am__recheck_rx)[yY][eE][sS]/) \
+ { \
+ break; \
+ } \
+ }; \
+ if (recheck) \
+ print $$0; \
+ close ($$0 ".trs"); \
+ close ($$0 ".log"); \
+}'
+# A command that, given a newline-separated list of test names on the
+# standard input, create the global log from their .trs and .log files.
+am__create_global_log = $(AWK) ' \
+function fatal(msg) \
+{ \
+ print "fatal: making $@: " msg | "cat >&2"; \
+ exit 1; \
+} \
+function rst_section(header) \
+{ \
+ print header; \
+ len = length(header); \
+ for (i = 1; i <= len; i = i + 1) \
+ printf "="; \
+ printf "\n\n"; \
+} \
+{ \
+ copy_in_global_log = 1; \
+ global_test_result = "RUN"; \
+ while ((rc = (getline line < ($$0 ".trs"))) != 0) \
+ { \
+ if (rc < 0) \
+ fatal("failed to read from " $$0 ".trs"); \
+ if (line ~ /$(am__global_test_result_rx)/) \
+ { \
+ sub("$(am__global_test_result_rx)", "", line); \
+ sub("[ ]*$$", "", line); \
+ global_test_result = line; \
+ } \
+ else if (line ~ /$(am__copy_in_global_log_rx)[nN][oO]/) \
+ copy_in_global_log = 0; \
+ }; \
+ if (copy_in_global_log) \
+ { \
+ rst_section(global_test_result ": " $$0); \
+ while ((rc = (getline line < ($$0 ".log"))) != 0) \
+ { \
+ if (rc < 0) \
+ fatal("failed to read from " $$0 ".log"); \
+ print line; \
+ }; \
+ printf "\n"; \
+ }; \
+ close ($$0 ".trs"); \
+ close ($$0 ".log"); \
+}'
+# Restructured Text title.
+am__rst_title = { sed 's/.*/ & /;h;s/./=/g;p;x;s/ *$$//;p;g' && echo; }
+# Solaris 10 'make', and several other traditional 'make' implementations,
+# pass "-e" to $(SHELL), and POSIX 2008 even requires this. Work around it
+# by disabling -e (using the XSI extension "set +e") if it's set.
+am__sh_e_setup = case $$- in *e*) set +e;; esac
+# Default flags passed to test drivers.
+am__common_driver_flags = \
+ --color-tests "$$am__color_tests" \
+ --enable-hard-errors "$$am__enable_hard_errors" \
+ --expect-failure "$$am__expect_failure"
+# To be inserted before the command running the test. Creates the
+# directory for the log if needed. Stores in $dir the directory
+# containing $f, in $tst the test, in $log the log. Executes the
+# developer- defined test setup AM_TESTS_ENVIRONMENT (if any), and
+# passes TESTS_ENVIRONMENT. Set up options for the wrapper that
+# will run the test scripts (or their associated LOG_COMPILER, if
+# thy have one).
+am__check_pre = \
+$(am__sh_e_setup); \
+$(am__vpath_adj_setup) $(am__vpath_adj) \
+$(am__tty_colors); \
+srcdir=$(srcdir); export srcdir; \
+case "$@" in \
+ */*) am__odir=`echo "./$@" | sed 's|/[^/]*$$||'`;; \
+ *) am__odir=.;; \
+esac; \
+test "x$$am__odir" = x"." || test -d "$$am__odir" \
+ || $(MKDIR_P) "$$am__odir" || exit $$?; \
+if test -f "./$$f"; then dir=./; \
+elif test -f "$$f"; then dir=; \
+else dir="$(srcdir)/"; fi; \
+tst=$$dir$$f; log='$@'; \
+if test -n '$(DISABLE_HARD_ERRORS)'; then \
+ am__enable_hard_errors=no; \
+else \
+ am__enable_hard_errors=yes; \
+fi; \
+case " $(XFAIL_TESTS) " in \
+ *[\ \ ]$$f[\ \ ]* | *[\ \ ]$$dir$$f[\ \ ]*) \
+ am__expect_failure=yes;; \
+ *) \
+ am__expect_failure=no;; \
+esac; \
+$(AM_TESTS_ENVIRONMENT) $(TESTS_ENVIRONMENT)
+# A shell command to get the names of the tests scripts with any registered
+# extension removed (i.e., equivalently, the names of the test logs, with
+# the '.log' extension removed). The result is saved in the shell variable
+# '$bases'. This honors runtime overriding of TESTS and TEST_LOGS. Sadly,
+# we cannot use something simpler, involving e.g., "$(TEST_LOGS:.log=)",
+# since that might cause problem with VPATH rewrites for suffix-less tests.
+# See also 'test-harness-vpath-rewrite.sh' and 'test-trs-basic.sh'.
+am__set_TESTS_bases = \
+ bases='$(TEST_LOGS)'; \
+ bases=`for i in $$bases; do echo $$i; done | sed 's/\.log$$//'`; \
+ bases=`echo $$bases`
+AM_TESTSUITE_SUMMARY_HEADER = ' for $(PACKAGE_STRING)'
+RECHECK_LOGS = $(TEST_LOGS)
+am__EXEEXT_14 =
+am__EXEEXT_15 = pkg/users/test-list-public-keys.sh $(am__EXEEXT_14)
+TEST_SUITE_LOG = test-suite.log
+LOG_DRIVER = $(SHELL) $(top_srcdir)/tools/test-driver
+LOG_COMPILE = $(LOG_COMPILER) $(AM_LOG_FLAGS) $(LOG_FLAGS)
+am__set_b = \
+ case '$@' in \
+ */*) \
+ case '$*' in \
+ */*) b='$*';; \
+ *) b=`echo '$@' | sed 's/\.log$$//'`; \
+ esac;; \
+ *) \
+ b='$*';; \
+ esac
+am__test_logs1 = $(TESTS:=.log)
+am__test_logs2 = $(am__test_logs1:@EXEEXT@.log=.log)
+am__test_logs3 = $(am__test_logs2:.html.log=.log)
+HTML_LOG_DRIVER = $(SHELL) $(top_srcdir)/tools/test-driver
+HTML_LOG_COMPILE = $(HTML_LOG_COMPILER) $(AM_HTML_LOG_FLAGS) \
+ $(HTML_LOG_FLAGS)
+TEST_LOGS = $(am__test_logs3:.sh.log=.log)
+SH_LOG_DRIVER = $(SHELL) $(top_srcdir)/tools/test-driver
+SH_LOG_COMPILE = $(SH_LOG_COMPILER) $(AM_SH_LOG_FLAGS) $(SH_LOG_FLAGS)
+am__DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/config.h.in \
+ $(srcdir)/containers/Makefile.am \
+ $(srcdir)/containers/flatpak/Makefile.am \
+ $(srcdir)/doc/Makefile-doc.am \
+ $(srcdir)/doc/guide/Makefile-guide.am \
+ $(srcdir)/doc/man/Makefile-man.am $(srcdir)/pkg/Makefile.am \
+ $(srcdir)/po/Makefile.am $(srcdir)/selinux/Makefile.am \
+ $(srcdir)/src/Makefile.am \
+ $(srcdir)/src/branding/arch/Makefile.am \
+ $(srcdir)/src/branding/centos/Makefile.am \
+ $(srcdir)/src/branding/debian/Makefile.am \
+ $(srcdir)/src/branding/default/Makefile.am \
+ $(srcdir)/src/branding/fedora/Makefile.am \
+ $(srcdir)/src/branding/kubernetes/Makefile.am \
+ $(srcdir)/src/branding/opensuse/Makefile.am \
+ $(srcdir)/src/branding/registry/Makefile.am \
+ $(srcdir)/src/branding/rhel/Makefile.am \
+ $(srcdir)/src/branding/scientific/Makefile.am \
+ $(srcdir)/src/branding/ubuntu/Makefile.am \
+ $(srcdir)/src/bridge/Makefile.am \
+ $(srcdir)/src/client/Makefile.am \
+ $(srcdir)/src/common/Makefile-common.am \
+ $(srcdir)/src/pam-ssh-add/Makefile.am \
+ $(srcdir)/src/session/Makefile-session.am \
+ $(srcdir)/src/ssh/Makefile-ssh.am \
+ $(srcdir)/src/systemd/Makefile.am \
+ $(srcdir)/src/testlib/Makefile.am \
+ $(srcdir)/src/tls/Makefile-tls.am \
+ $(srcdir)/src/websocket/Makefile-websocket.am \
+ $(srcdir)/src/ws/Makefile-ws.am \
+ $(srcdir)/tools/Makefile-tools.am \
+ $(top_srcdir)/doc/guide/version.in \
+ $(top_srcdir)/src/tls/cockpit-certificate-helper.in \
+ $(top_srcdir)/src/ws/cockpit-desktop.in \
+ $(top_srcdir)/tools/compile $(top_srcdir)/tools/depcomp \
+ $(top_srcdir)/tools/install-sh $(top_srcdir)/tools/missing \
+ $(top_srcdir)/tools/test-driver AUTHORS COPYING README.md \
+ tools/compile tools/depcomp tools/install-sh tools/missing
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+distdir = $(PACKAGE)-$(VERSION)
+top_distdir = $(distdir)
+am__remove_distdir = \
+ if test -d "$(distdir)"; then \
+ find "$(distdir)" -type d ! -perm -200 -exec chmod u+w {} ';' \
+ && rm -rf "$(distdir)" \
+ || { sleep 5 && rm -rf "$(distdir)"; }; \
+ else :; fi
+am__post_remove_distdir = $(am__remove_distdir)
+GZIP_ENV = --best
+DIST_ARCHIVES = $(distdir).tar.xz
+DIST_TARGETS = dist-xz
+# Exists only to be overridden by the user if desired.
+AM_DISTCHECK_DVI_TARGET = dvi
+distuninstallcheck_listfiles = find . -type f -print
+am__distuninstallcheck_listfiles = $(distuninstallcheck_listfiles) \
+ | sed 's|^\./|$(prefix)/|' | grep -v '$(infodir)/dir$$'
+distcleancheck_listfiles = find . -type f -print
+ACLOCAL = @ACLOCAL@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+AR = @AR@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CFLAGS = @CFLAGS@
+COCKPIT_GROUP = @COCKPIT_GROUP@
+COCKPIT_USER = @COCKPIT_USER@
+COCKPIT_WSINSTANCE_GROUP = @COCKPIT_WSINSTANCE_GROUP@
+COCKPIT_WSINSTANCE_USER = @COCKPIT_WSINSTANCE_USER@
+CPPFLAGS = @CPPFLAGS@
+CSCOPE = @CSCOPE@
+CTAGS = @CTAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+ETAGS = @ETAGS@
+EXEEXT = @EXEEXT@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+LDFLAGS = @LDFLAGS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LTLIBOBJS = @LTLIBOBJS@
+MAKEINFO = @MAKEINFO@
+MKDIR_P = @MKDIR_P@
+NODE_ENV = @NODE_ENV@
+OBJEXT = @OBJEXT@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PAM_LIBS = @PAM_LIBS@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+PYTHON = @PYTHON@
+PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@
+PYTHON_PLATFORM = @PYTHON_PLATFORM@
+PYTHON_PREFIX = @PYTHON_PREFIX@
+PYTHON_VERSION = @PYTHON_VERSION@
+RANLIB = @RANLIB@
+SELINUX_POLICY_TYPE = @SELINUX_POLICY_TYPE@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SSH_ADD = @SSH_ADD@
+SSH_AGENT = @SSH_AGENT@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VERSION = @VERSION@
+XGETTEXT = @XGETTEXT@
+XMLTO = @XMLTO@
+XSLTPROC = @XSLTPROC@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_CC = @ac_ct_CC@
+admin_group = @admin_group@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+argp_LIBS = @argp_LIBS@
+bindir = @bindir@
+build_alias = @build_alias@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+debugdir = @debugdir@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+fts_LIBS = @fts_LIBS@
+glib_CFLAGS = @glib_CFLAGS@
+glib_LIBS = @glib_LIBS@
+gnutls_CFLAGS = @gnutls_CFLAGS@
+gnutls_LIBS = @gnutls_LIBS@
+host_alias = @host_alias@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+json_glib_CFLAGS = @json_glib_CFLAGS@
+json_glib_LIBS = @json_glib_LIBS@
+krb5_CFLAGS = @krb5_CFLAGS@
+krb5_LIBS = @krb5_LIBS@
+libdir = @libdir@
+libexecdir = @libexecdir@
+libssh_CFLAGS = @libssh_CFLAGS@
+libssh_LIBS = @libssh_LIBS@
+libsystemd_CFLAGS = @libsystemd_CFLAGS@
+libsystemd_LIBS = @libsystemd_LIBS@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+oldincludedir = @oldincludedir@
+pamdir = @pamdir@
+pdfdir = @pdfdir@
+pkgpyexecdir = @pkgpyexecdir@
+pkgpythondir = @pkgpythondir@
+polkit_CFLAGS = @polkit_CFLAGS@
+polkit_LIBS = @polkit_LIBS@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+pyexecdir = @pyexecdir@
+pythondir = @pythondir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+srcdir = @srcdir@
+sysconfdir = @sysconfdir@
+systemdunitdir = @systemdunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+NULL =
+CLEANFILES = cockpit-*.tar.xz $(WS_DIR)/rpms $(am__append_8) \
+ dist/shell/*.po src/ws/*.po po/cockpit*.pot $(NULL) \
+ $(nodist_metainfo_DATA) $(am__append_23) $(am__append_28) \
+ src/common/fail.html.c $(am__append_36) $(systemdgenerated) \
+ $(GDBUS_CODEGEN_GENERATED) $(nodist_appdata_DATA)
+dist_noinst_DATA = $(am__append_1) $(am__append_9)
+man_MANS = $(am__append_7) $(am__append_11)
+noinst_DATA = $(am__append_2)
+
+# -----------------------------------------------------------------------------
+# libcockpit-common.a: code that has other dependencies, like glib or libsystemd
+noinst_LIBRARIES = libcockpit-metrics.a $(am__append_14) \
+ $(am__append_25) libcockpit-common-nodeps.a \
+ libcockpit-common.a libpam_ssh_add.a $(am__append_30) \
+ libcockpit-tls.a libwebsocket.a libcockpit-ws.a
+
+# -----------------------------------------------------------------------------
+# node_modules/package-lock.json dependency
+V_NODE_MODULES = $(V_NODE_MODULES_$(V))
+V_NODE_MODULES_ = $(V_NODE_MODULES_$(AM_DEFAULT_VERBOSITY))
+V_NODE_MODULES_0 = @V=0
+
+# -----------------------------------------------------------------------------
+# make dist
+
+# We override distdir as we want to dist some git-tracked files and dist/ without explicitly listing.
+# The spec also gets patched to declare bundled NPM dependencies.
+EXTRA_DIST = README.md $(EXTRA_FILES) \
+ node_modules/chrome-remote-interface node_modules/commander \
+ node_modules/sizzle node_modules/ws $(NULL) \
+ doc/cockpit-transport.svg doc/cockpit-transport.png $(NULL) \
+ doc/man/cockpit.xml doc/man/cockpit-bridge.xml \
+ doc/man/cockpit-ws.xml doc/man/cockpit-tls.xml \
+ doc/man/cockpit-desktop.xml doc/man/cockpit.conf.xml \
+ doc/man/pam_ssh_add.xml $(NULL) pkg/users/mock \
+ pkg/lib/qunit-template.html.in pkg/lib/cockpit-po-plugin.js \
+ $(pkg_TESTS) $(metainfo_DATA) $(pixmaps_DATA) $(NULL) build.js \
+ files.js package.json package-lock.json $(PO_INPUTS) \
+ po/LINGUAS po/its/polkit.its po/its/polkit.loc $(NULL) \
+ $(metainfo_in) src/branding/default/logo.svg \
+ $(polkit_in_files) src/bridge/cockpit.pam.insecure $(NULL) \
+ src/common/fail.html $(systemdgenerated_in) \
+ $(GDBUS_CODEGEN_XML) $(appdata_in) \
+ pkg/apps/content-security-policy.override
+DISTCHECK_CONFIGURE_FLAGS = --enable-prefix-only $(EXTRA_DISTCHECK_CONFIGURE_FLAGS)
+TEST_EXTENSIONS = .html .sh
+HTML_LOG_COMPILER = $(top_srcdir)/test/common/tap-cdp --strip=$(abs_top_srcdir)/ $(HTML_TEST_WRAPPER) ./test-server $(COCKPIT_BRIDGE)
+VALGRIND = valgrind --trace-children=yes --quiet --error-exitcode=33 --gen-suppressions=all \
+ $(foreach file,$(wildcard $(srcdir)/tools/*.supp),--suppressions=$(file)) \
+ --num-callers=16 --leak-check=yes --show-leak-kinds=definite \
+ --errors-for-leak-kinds=definite --trace-children-skip='*mock*,/bin*,/usr/bin/*,/usr/local/bin'
+
+
+# We use these to add conditionally-enabled extra rules
+# automake doesn't like mixing `::` and `if`
+CHECK_LOCAL_TARGETS = $(am__append_4)
+CLEAN_LOCAL_TARGETS = $(am__append_3)
+INSTALL_DATA_LOCAL_TARGETS = $(am__append_5) install-bundles \
+ $(am__append_10) $(am__append_12)
+INSTALL_EXEC_HOOK_TARGETS = $(am__append_29)
+UNINSTALL_LOCAL_TARGETS = $(am__append_6) uninstall-bundles \
+ $(am__append_13)
+TEST_CPP = \
+ -DSRCDIR=\"$(abs_srcdir)\" \
+ -DBUILDDIR=\"$(abs_builddir)\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+TEST_LIBS = \
+ libcockpit-test.a \
+ $(libcockpit_common_a_LIBS) \
+ $(NULL)
+
+
+# -----------------------------------------------------------------------------
+# Some helpful variables to get tests into TESTS and check_* in one go
+
+# Unfortunately, we can't call these _PROGRAMS or _SCRIPTS because otherwise
+# automake will treat them differently, which we don't want.
+
+# -----------------------------------------------------------------------------
+# Unit tests
+TEST_PROGRAM = $(am__append_21) $(am__append_27) test-authorize \
+ test-base64 test-channel test-config test-frame test-hash \
+ test-hex test-json test-jsonfds test-locale test-pipe \
+ test-system test-template test-transport test-unicode \
+ test-unixsignal test-version test-webcertificate \
+ test-webresponse test-webserver test-ssh-add $(am__append_34) \
+ test-cockpit-certificate-ensure test-tls-connection \
+ test-tls-server test-websocket test-auth test-compat \
+ test-creds test-kerberos $(am__append_37) $(am__append_38)
+TEST_SCRIPT =
+
+# -----------------------------------------------------------------------------
+# Unit tests
+check_SCRIPTS = $(TEST_SCRIPT) src/ws/mock-cat-with-init
+dist_TEST_SCRIPT = src/tls/test-socket-activation-helper.sh
+
+# -----------------------------------------------------------------------------
+# Unit tests
+dist_check_SCRIPTS = $(dist_TEST_SCRIPT) src/common/mock-stderr
+
+# Testing assets should add themselves here (not EXTRA_DIST)
+check_DATA = $(am__append_35)
+
+# -----------------------------------------------------------------------------
+# Unit tests
+dist_check_DATA = $(am__append_22) src/common/mock-content \
+ src/pam-ssh-add/mock-ssh-agent src/pam-ssh-add/mock-ssh-add \
+ src/pam-ssh-add/mock-environment $(NULL) $(am__append_33) \
+ src/tls/ca/alice-expired.pem src/tls/ca/alice.key \
+ src/tls/ca/alice.p12 src/tls/ca/alice.pem src/tls/ca/bob.key \
+ src/tls/ca/bob.p12 src/tls/ca/bob.pem src/tls/ca/ca.conf \
+ src/tls/ca/ca.key src/tls/ca/ca.pem src/tls/ca/generate.sh \
+ $(NULL) src/ws/mock-combined.crt src/ws/mock-ecc.crt \
+ src/ws/mock-ecc.key src/ws/mock-cat-with-init src/ws/mock-kdc \
+ src/ws/mock-krb5.conf.in src/ws/mock-kdc.conf.in \
+ src/ws/mock-static src/ws/mock-config \
+ src/ws/mock-pipes/exit127 src/ws/mock-pipes/someprogram \
+ src/ws/mock-pipes/cockpit-session $(NULL)
+
+# -----------------------------------------------------------------------------
+# libcockpit-test.a: code used only for unit tests
+check_LIBRARIES = libcockpit-test.a
+libcockpit_test_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-test\" \
+ $(TEST_CPP)
+
+libcockpit_test_a_SOURCES = \
+ src/testlib/cockpittest.c \
+ src/testlib/cockpittest.h \
+ src/testlib/mock-auth.c \
+ src/testlib/mock-auth.h \
+ src/testlib/mock-channel.c \
+ src/testlib/mock-channel.h \
+ src/testlib/mock-pressure.c \
+ src/testlib/mock-pressure.h \
+ src/testlib/mock-transport.c \
+ src/testlib/mock-transport.h \
+ src/testlib/retest.c \
+ src/testlib/retest.h \
+ $(NULL)
+
+WS_DIR = $(srcdir)/containers/ws
+INSTALL_FLATPAK_TARGETS = \
+ install-cockpitwsPROGRAMS \
+ install-dist_client_metainfoDATA \
+ install-dist_defaultbrandingDATA \
+ install-dist_applicationsDATA \
+ install-dist_cockpitclientDATA \
+ install-dist_cockpitclientSCRIPTS \
+ install-dist_dbusservicesDATA \
+ install-dist_scalableiconDATA \
+ install-dist_symboliciconDATA \
+ install-cockpit-client-symlink \
+ install-python \
+ install-bundles \
+ $(NULL)
+
+@ENABLE_DOC_TRUE@GUIDE_DOCBOOK = doc/guide/cockpit-guide.xml
+@ENABLE_DOC_TRUE@GUIDE_INCLUDES = \
+@ENABLE_DOC_TRUE@ doc/guide/api-cockpit.xml \
+@ENABLE_DOC_TRUE@ doc/guide/api-base1.xml \
+@ENABLE_DOC_TRUE@ doc/guide/api-shell.xml \
+@ENABLE_DOC_TRUE@ doc/guide/api-system.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-cache.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-channel.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-dbus.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-error.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-file.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-http.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-locale.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-location.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-manifest.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-metrics.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-series.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-session.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-spawn.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cockpit-util.xml \
+@ENABLE_DOC_TRUE@ doc/guide/authentication.xml \
+@ENABLE_DOC_TRUE@ doc/guide/embedding.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-firewall.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-journal.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-machines.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-networkmanager.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-packagekit.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-pcp.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-realmd.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-selinux.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-sosreport.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-storaged.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-systemd.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-terminal.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-tuned.xml \
+@ENABLE_DOC_TRUE@ doc/guide/feature-users.xml \
+@ENABLE_DOC_TRUE@ doc/guide/packages.xml \
+@ENABLE_DOC_TRUE@ doc/guide/privileges.xml \
+@ENABLE_DOC_TRUE@ doc/guide/https.xml \
+@ENABLE_DOC_TRUE@ doc/guide/listen.xml \
+@ENABLE_DOC_TRUE@ doc/guide/sso.xml \
+@ENABLE_DOC_TRUE@ doc/guide/cert-authentication.xml \
+@ENABLE_DOC_TRUE@ doc/guide/startup.xml \
+@ENABLE_DOC_TRUE@ doc/guide/urls.xml \
+@ENABLE_DOC_TRUE@ $(NULL)
+
+@ENABLE_DOC_TRUE@GUIDE_STATIC = \
+@ENABLE_DOC_TRUE@ doc/guide/static/home.png \
+@ENABLE_DOC_TRUE@ doc/guide/static/gtk-doc.css \
+@ENABLE_DOC_TRUE@ doc/guide/static/left.png \
+@ENABLE_DOC_TRUE@ doc/guide/static/right.png \
+@ENABLE_DOC_TRUE@ doc/guide/static/style.css \
+@ENABLE_DOC_TRUE@ doc/guide/static/up.png \
+@ENABLE_DOC_TRUE@ $(NULL)
+
+@ENABLE_DOC_TRUE@GUIDE_XSLT = \
+@ENABLE_DOC_TRUE@ doc/guide/gtk-doc.xsl \
+@ENABLE_DOC_TRUE@ doc/guide/version-greater-or-equal.xsl \
+@ENABLE_DOC_TRUE@ $(NULL)
+
+
+# keep in sync with doc/guide/static/style.css
+@ENABLE_DOC_TRUE@GUIDE_FONTS = \
+@ENABLE_DOC_TRUE@ doc/guide/html/RedHatText-Regular.woff2 \
+@ENABLE_DOC_TRUE@ doc/guide/html/RedHatText-Medium.woff2 \
+@ENABLE_DOC_TRUE@ $(NULL)
+
+@ENABLE_DOC_TRUE@MANPAGES = \
+@ENABLE_DOC_TRUE@ doc/man/cockpit.1 \
+@ENABLE_DOC_TRUE@ doc/man/cockpit-bridge.1 \
+@ENABLE_DOC_TRUE@ doc/man/cockpit-desktop.1 \
+@ENABLE_DOC_TRUE@ doc/man/cockpit-ws.8 \
+@ENABLE_DOC_TRUE@ doc/man/cockpit-tls.8 \
+@ENABLE_DOC_TRUE@ doc/man/cockpit.conf.5 \
+@ENABLE_DOC_TRUE@ doc/man/pam_ssh_add.8 \
+@ENABLE_DOC_TRUE@ $(NULL)
+
+@ENABLE_DOC_TRUE@MAN_PROC = mkdir -p doc/man/ && $(XSLTPROC) -nonet --param man.output.quietly 1 --output $@ \
+@ENABLE_DOC_TRUE@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $<
+
+pkg_TESTS = \
+ pkg/users/test-list-public-keys.sh \
+ $(NULL)
+
+pixmapsdir = ${datarootdir}/pixmaps
+pixmaps_DATA = pkg/sosreport/cockpit-sosreport.png
+@ENABLE_PCP_TRUE@pcpmanifestdir = $(datadir)/cockpit/pcp
+@ENABLE_PCP_TRUE@dist_pcpmanifest_DATA = pkg/pcp/manifest.json
+
+# one built file in dist/ which we use as dependency
+DIST_STAMP = $(srcdir)/dist/static/manifest.json
+
+# dynamic pkg → dist dependency, to rebuild the bundles if any web related file changes
+# exclude automake unit test log files
+PKG_INPUTS = $(shell find $(srcdir)/pkg -type f ! -name 'test*.trs' ! -name 'test*.log')
+V_BUNDLE = $(V_BUNDLE_$(V))
+V_BUNDLE_ = $(V_BUNDLE_$(AM_DEFAULT_VERBOSITY))
+V_BUNDLE_0 = @echo " BUNDLE dist";
+
+# The concrete set of linguas we are using
+LINGUAS = $(shell cat $(srcdir)/po/LINGUAS)
+
+# The full list of various input and output file types
+PO_LINGUAS = $(addprefix po/,$(LINGUAS))
+PO_INPUTS = $(addsuffix .po,$(PO_LINGUAS))
+@SELINUX_POLICY_ENABLED_TRUE@SELINUX_POLICY_FILES = \
+@SELINUX_POLICY_ENABLED_TRUE@ selinux/cockpit.fc \
+@SELINUX_POLICY_ENABLED_TRUE@ selinux/cockpit.if \
+@SELINUX_POLICY_ENABLED_TRUE@ selinux/cockpit.te \
+@SELINUX_POLICY_ENABLED_TRUE@ $(NULL)
+
+@SELINUX_POLICY_ENABLED_TRUE@SELINUX_POLICY_MANPAGES = \
+@SELINUX_POLICY_ENABLED_TRUE@ selinux/cockpit_session_selinux.8cockpit \
+@SELINUX_POLICY_ENABLED_TRUE@ selinux/cockpit_ws_selinux.8cockpit \
+@SELINUX_POLICY_ENABLED_TRUE@ $(NULL)
+
+@SELINUX_POLICY_ENABLED_TRUE@selinuxpackagesdir = $(datadir)/selinux/packages/$(SELINUX_POLICY_TYPE)/
+@SELINUX_POLICY_ENABLED_TRUE@selinuxactivedir = $(sharedstatedir)/selinux/$(SELINUX_POLICY_TYPE)/active/modules/200/cockpit
+@SELINUX_POLICY_ENABLED_TRUE@selinuxpackages_DATA = cockpit.pp.bz2
+BUILT_SOURCES = $(GDBUS_CODEGEN_GENERATED)
+libexec_SCRIPTS = src/tls/cockpit-certificate-helper \
+ src/ws/cockpit-desktop
+
+# -----------------------------------------------------------------------------
+# C
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src \
+ -DDATADIR=\"$(datadir)\" \
+ -DLIBEXECDIR=\""$(libexecdir)"\" \
+ -DPACKAGE_SYSCONF_DIR=\""$(sysconfdir)"\" \
+ $(NULL)
+
+AM_CFLAGS = \
+ -std=gnu18 \
+ -pthread \
+ -Wall \
+ -Werror=strict-prototypes \
+ -Werror=missing-prototypes \
+ -Werror=implicit-function-declaration \
+ -Werror=implicit-int \
+ -Werror=int-conversion \
+ -Werror=old-style-definition \
+ -Werror=pointer-arith \
+ -Werror=init-self \
+ -Werror=format=2 \
+ -Werror=return-type \
+ -Werror=missing-include-dirs \
+ $(NULL)
+
+metainfodir = ${datarootdir}/metainfo
+nodist_metainfo_DATA = \
+ src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml \
+ src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml \
+ src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml \
+ src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml \
+ src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml \
+ $(NULL)
+
+metainfo_in = $(patsubst %,%.in,$(nodist_metainfo_DATA))
+archbrandingdir = $(datadir)/cockpit/branding/arch
+dist_archbranding_DATA = \
+ src/branding/arch/branding.css \
+ $(NULL)
+
+centosbrandingdir = $(datadir)/cockpit/branding/centos
+dist_centosbranding_DATA = \
+ src/branding/centos/branding.css \
+ $(NULL)
+
+debianbrandingdir = $(datadir)/cockpit/branding/debian
+dist_debianbranding_DATA = \
+ src/branding/debian/branding.css \
+ $(NULL)
+
+defaultbrandingdir = $(datadir)/cockpit/branding/default
+dist_defaultbranding_DATA = \
+ src/branding/default/apple-touch-icon.png \
+ src/branding/default/bg-plain.jpg \
+ src/branding/default/branding.css \
+ src/branding/default/brand-large.png \
+ src/branding/default/favicon.ico \
+ src/branding/default/logo.png \
+ $(NULL)
+
+fedorabrandingdir = $(datadir)/cockpit/branding/fedora
+dist_fedorabranding_DATA = \
+ src/branding/fedora/branding.css \
+ $(NULL)
+
+kubernetesbrandingdir = $(datadir)/cockpit/branding/kubernetes
+dist_kubernetesbranding_DATA = \
+ src/branding/kubernetes/branding.css \
+ $(NULL)
+
+opensusebrandingdir = $(datadir)/cockpit/branding/opensuse
+dist_opensusebranding_DATA = \
+ src/branding/opensuse/branding.css \
+ $(NULL)
+
+registrybrandingdir = $(datadir)/cockpit/branding/registry
+dist_registrybranding_DATA = \
+ src/branding/kubernetes/branding.css \
+ $(NULL)
+
+rhelbrandingdir = $(datadir)/cockpit/branding/rhel
+dist_rhelbranding_DATA = \
+ src/branding/rhel/branding.css \
+ $(NULL)
+
+scientificbrandingdir = $(datadir)/cockpit/branding/scientific
+dist_scientificbranding_DATA = \
+ src/branding/scientific/branding.css \
+ $(NULL)
+
+ubuntubrandingdir = $(datadir)/cockpit/branding/ubuntu
+dist_ubuntubranding_DATA = \
+ src/branding/ubuntu/branding.css \
+ src/branding/ubuntu/favicon.ico \
+ $(NULL)
+
+libcockpit_metrics_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-bridge\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libcockpit_metrics_a_LIBS = \
+ libcockpit-metrics.a \
+ $(libcockpit_common_a_LIBS) \
+ $(fts_LIBS) \
+ -lm \
+ $(NULL)
+
+libcockpit_metrics_a_SOURCES = \
+ src/bridge/cockpitblocksamples.c \
+ src/bridge/cockpitblocksamples.h \
+ src/bridge/cockpitcgroupsamples.c \
+ src/bridge/cockpitcgroupsamples.h \
+ src/bridge/cockpitcpusamples.c \
+ src/bridge/cockpitcpusamples.h \
+ src/bridge/cockpitdisksamples.c \
+ src/bridge/cockpitdisksamples.h \
+ src/bridge/cockpitinternalmetrics.c \
+ src/bridge/cockpitinternalmetrics.h \
+ src/bridge/cockpitmemorysamples.c \
+ src/bridge/cockpitmemorysamples.h \
+ src/bridge/cockpitmetrics.c \
+ src/bridge/cockpitmetrics.h \
+ src/bridge/cockpitmountsamples.c \
+ src/bridge/cockpitmountsamples.h \
+ src/bridge/cockpitnetworksamples.c \
+ src/bridge/cockpitnetworksamples.h \
+ src/bridge/cockpitsamples.c \
+ src/bridge/cockpitsamples.h \
+ $(NULL)
+
+@WITH_OLD_BRIDGE_TRUE@libcockpit_bridge_a_CPPFLAGS = \
+@WITH_OLD_BRIDGE_TRUE@ -DG_LOG_DOMAIN=\"cockpit-bridge\" \
+@WITH_OLD_BRIDGE_TRUE@ $(glib_CFLAGS) $(json_glib_CFLAGS) \
+@WITH_OLD_BRIDGE_TRUE@ $(AM_CPPFLAGS) $(am__append_15)
+@WITH_OLD_BRIDGE_TRUE@libcockpit_bridge_a_LIBS = libcockpit-bridge.a \
+@WITH_OLD_BRIDGE_TRUE@ $(libcockpit_metrics_a_LIBS) \
+@WITH_OLD_BRIDGE_TRUE@ $(libsystemd_LIBS) $(NULL) \
+@WITH_OLD_BRIDGE_TRUE@ $(am__append_16)
+@WITH_OLD_BRIDGE_TRUE@libcockpit_bridge_a_SOURCES = \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitconnect.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitconnect.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbuscache.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbuscache.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusconfig.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusinternal.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusinternal.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusjson.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusjson.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusloginmessages.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusmachines.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusmeta.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusmeta.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusprocess.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusrules.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbusrules.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitdbususer.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitechochannel.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitechochannel.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitfslist.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitfslist.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitfsread.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitfsread.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitfsreplace.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitfsreplace.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitfswatch.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitfswatch.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpithttpstream.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpithttpstream.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitinteracttransport.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitinteracttransport.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitnullchannel.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitnullchannel.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitpackages.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitpackages.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitpacketchannel.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitpacketchannel.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitpaths.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitpaths.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitpeer.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitpeer.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitpipechannel.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitpipechannel.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitrouter.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitrouter.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitstream.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitstream.h \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitwebsocketstream.c \
+@WITH_OLD_BRIDGE_TRUE@ src/bridge/cockpitwebsocketstream.h \
+@WITH_OLD_BRIDGE_TRUE@ $(NULL) $(am__append_17)
+@WITH_OLD_BRIDGE_TRUE@cockpit_bridge_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS)
+@WITH_OLD_BRIDGE_TRUE@cockpit_bridge_LDADD = $(libcockpit_bridge_a_LIBS)
+@WITH_OLD_BRIDGE_TRUE@cockpit_bridge_SOURCES = src/bridge/bridge.c
+@WITH_OLD_BRIDGE_TRUE@cockpit_askpass_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS)
+@WITH_OLD_BRIDGE_TRUE@cockpit_askpass_LDADD = $(libcockpit_bridge_a_LIBS)
+@WITH_OLD_BRIDGE_TRUE@cockpit_askpass_SOURCES = src/bridge/askpass.c
+@WITH_OLD_BRIDGE_TRUE@mock_bridge_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS)
+@WITH_OLD_BRIDGE_TRUE@mock_bridge_LDADD = $(libcockpit_bridge_a_LIBS)
+@WITH_OLD_BRIDGE_TRUE@mock_bridge_SOURCES = src/bridge/mock-bridge.c
+@WITH_OLD_BRIDGE_TRUE@test_bridge_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_bridge_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_bridge_SOURCES = src/bridge/test-bridge.c
+@WITH_OLD_BRIDGE_TRUE@test_connect_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_connect_LDADD = $(test_bridge_LDADD) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_connect_SOURCES = src/bridge/test-connect.c
+@WITH_OLD_BRIDGE_TRUE@test_dbus_meta_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_dbus_meta_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_dbus_meta_SOURCES = src/bridge/test-dbus-meta.c
+@WITH_OLD_BRIDGE_TRUE@test_fs_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_fs_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_fs_SOURCES = src/bridge/test-fs.c
+@WITH_OLD_BRIDGE_TRUE@test_httpstream_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_httpstream_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_httpstream_SOURCES = src/bridge/test-httpstream.c
+@WITH_OLD_BRIDGE_TRUE@test_metrics_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_metrics_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_metrics_SOURCES = src/bridge/test-metrics.c
+@WITH_OLD_BRIDGE_TRUE@test_packages_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_packages_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_packages_SOURCES = src/bridge/test-packages.c
+@WITH_OLD_BRIDGE_TRUE@test_packet_channel_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_packet_channel_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_packet_channel_SOURCES = src/bridge/test-packet-channel.c
+@WITH_OLD_BRIDGE_TRUE@test_paths_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_paths_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_paths_SOURCES = src/bridge/test-paths.c
+@WITH_OLD_BRIDGE_TRUE@test_peer_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_peer_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_peer_SOURCES = src/bridge/test-peer.c
+@WITH_OLD_BRIDGE_TRUE@test_pipe_channel_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_pipe_channel_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_pipe_channel_SOURCES = src/bridge/test-pipe-channel.c
+@WITH_OLD_BRIDGE_TRUE@test_process_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_process_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_process_SOURCES = src/bridge/test-process.c
+@WITH_OLD_BRIDGE_TRUE@test_router_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_router_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_router_SOURCES = src/bridge/test-router.c
+@WITH_OLD_BRIDGE_TRUE@test_rules_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_rules_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_rules_SOURCES = src/bridge/test-rules.c
+@WITH_OLD_BRIDGE_TRUE@test_stream_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_stream_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_stream_SOURCES = src/bridge/test-stream.c
+@WITH_OLD_BRIDGE_TRUE@test_websocketstream_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_websocketstream_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_websocketstream_SOURCES = src/bridge/test-websocketstream.c
+
+# -----------------------------------------------------------------------------
+# polkit
+
+# make sure this ends up in the tarball, even with --disable-ssh
+polkit_in_files = src/bridge/org.cockpit-project.cockpit-bridge.policy.in
+@WITH_POLKIT_TRUE@polkitdir = $(datadir)/polkit-1/actions
+@WITH_POLKIT_TRUE@polkit_DATA = $(polkit_in_files:.policy.in=.policy)
+
+# -----------------------------------------------------------------------------
+# PCP
+@ENABLE_PCP_TRUE@pmlogconfdir = $(localstatedir)/lib/pcp/config/pmlogconf/tools
+@ENABLE_PCP_TRUE@dist_pmlogconf_DATA = src/bridge/pmlogconf/cockpit
+@ENABLE_PCP_TRUE@libcockpit_pcp_a_CPPFLAGS = \
+@ENABLE_PCP_TRUE@ -DG_LOG_DOMAIN=\"cockpit-pcp\" \
+@ENABLE_PCP_TRUE@ $(glib_CFLAGS) \
+@ENABLE_PCP_TRUE@ $(json_glib_CFLAGS) \
+@ENABLE_PCP_TRUE@ $(AM_CPPFLAGS)
+
+@ENABLE_PCP_TRUE@libcockpit_pcp_a_LIBS = \
+@ENABLE_PCP_TRUE@ libcockpit-pcp.a \
+@ENABLE_PCP_TRUE@ $(libcockpit_metrics_a_LIBS) \
+@ENABLE_PCP_TRUE@ -lpcp_pmda -lpcp -lpcp_import \
+@ENABLE_PCP_TRUE@ $(NULL)
+
+@ENABLE_PCP_TRUE@libcockpit_pcp_a_SOURCES = \
+@ENABLE_PCP_TRUE@ src/bridge/cockpitconnect.c \
+@ENABLE_PCP_TRUE@ src/bridge/cockpitconnect.h \
+@ENABLE_PCP_TRUE@ src/bridge/cockpitpcpmetrics.c \
+@ENABLE_PCP_TRUE@ src/bridge/cockpitpcpmetrics.h \
+@ENABLE_PCP_TRUE@ src/bridge/cockpitdbusinternal.c \
+@ENABLE_PCP_TRUE@ src/bridge/cockpitdbusinternal.h \
+@ENABLE_PCP_TRUE@ src/bridge/cockpitpeer.c \
+@ENABLE_PCP_TRUE@ src/bridge/cockpitpeer.h \
+@ENABLE_PCP_TRUE@ src/bridge/cockpitrouter.c \
+@ENABLE_PCP_TRUE@ src/bridge/cockpitrouter.h \
+@ENABLE_PCP_TRUE@ $(libcockpit_bridge_METRICS) \
+@ENABLE_PCP_TRUE@ $(NULL)
+
+@ENABLE_PCP_TRUE@cockpit_pcp_CPPFLAGS = $(libcockpit_pcp_a_CPPFLAGS)
+@ENABLE_PCP_TRUE@cockpit_pcp_LDADD = $(libcockpit_pcp_a_LIBS)
+@ENABLE_PCP_TRUE@cockpit_pcp_SOURCES = src/bridge/cockpitpcp.c
+@ENABLE_PCP_TRUE@mock_pmda_so_CFLAGS = -fPIC $(AM_CFLAGS)
+@ENABLE_PCP_TRUE@mock_pmda_so_LDFLAGS = -shared
+@ENABLE_PCP_TRUE@mock_pmda_so_LDADD = -lpcp_pmda -lpcp
+@ENABLE_PCP_TRUE@mock_pmda_so_SOURCES = src/bridge/mock-pmda.c
+@ENABLE_PCP_TRUE@test_pcp_CPPFLAGS = $(libcockpit_pcp_a_CFLAGS) $(TEST_CPP)
+@ENABLE_PCP_TRUE@test_pcp_LDADD = $(libcockpit_pcp_a_LIBS) $(TEST_LIBS) -ldl
+@ENABLE_PCP_TRUE@test_pcp_SOURCES = src/bridge/test-pcp.c
+@ENABLE_PCP_TRUE@test_pcp_archives_CPPFLAGS = $(libcockpit_pcp_a_CPPFLAGS) $(TEST_CPP)
+@ENABLE_PCP_TRUE@test_pcp_archives_LDADD = $(libcockpit_pcp_a_LIBS) $(TEST_LIBS)
+@ENABLE_PCP_TRUE@test_pcp_archives_SOURCES = src/bridge/test-pcp-archives.c
+cockpitclientdir = $(libexecdir)
+dist_cockpitclient_SCRIPTS = src/client/cockpit-client
+dist_cockpitclient_DATA = src/client/cockpit-client.ui
+@ENABLE_COCKPIT_CLIENT_TRUE@dbusservicesdir = $(datadir)/dbus-1/services
+@ENABLE_COCKPIT_CLIENT_TRUE@dist_dbusservices_DATA = src/client/org.cockpit_project.CockpitClient.service
+@ENABLE_COCKPIT_CLIENT_TRUE@applicationsdir = $(datadir)/applications
+@ENABLE_COCKPIT_CLIENT_TRUE@dist_applications_DATA = src/client/org.cockpit_project.CockpitClient.desktop
+@ENABLE_COCKPIT_CLIENT_TRUE@scalableicondir = $(datadir)/icons/hicolor/scalable/apps
+@ENABLE_COCKPIT_CLIENT_TRUE@dist_scalableicon_DATA = src/client/cockpit-client.svg
+@ENABLE_COCKPIT_CLIENT_TRUE@symbolicicondir = $(datadir)/icons/hicolor/symbolic/apps
+@ENABLE_COCKPIT_CLIENT_TRUE@dist_symbolicicon_DATA = src/client/cockpit-client-symbolic.svg
+@ENABLE_COCKPIT_CLIENT_TRUE@client_metainfodir = $(datadir)/metainfo
+@ENABLE_COCKPIT_CLIENT_TRUE@dist_client_metainfo_DATA = src/client/org.cockpit_project.CockpitClient.metainfo.xml
+libcockpit_common_nodeps_a_LIBS = libcockpit-common-nodeps.a
+libcockpit_common_nodeps_a_SOURCES = \
+ src/common/cockpitauthorize.c \
+ src/common/cockpitauthorize.h \
+ src/common/cockpitbase64.c \
+ src/common/cockpitbase64.h \
+ src/common/cockpitconf.h \
+ src/common/cockpitconf.c \
+ src/common/cockpitfdpassing.c \
+ src/common/cockpitfdpassing.h \
+ src/common/cockpitframe.c \
+ src/common/cockpitframe.h \
+ src/common/cockpithacks.h \
+ src/common/cockpithex.c \
+ src/common/cockpithex.h \
+ src/common/cockpitjsonprint.c \
+ src/common/cockpitjsonprint.h \
+ src/common/cockpitmemory.c \
+ src/common/cockpitmemory.h \
+ src/common/cockpitwebcertificate.h \
+ src/common/cockpitwebcertificate.c \
+ $(NULL)
+
+libcockpit_common_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-protocol\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libcockpit_common_a_LIBS = \
+ libcockpit-common.a \
+ $(libcockpit_common_nodeps_a_LIBS) \
+ $(libwebsocket_a_LIBS) \
+ $(json_glib_LIBS) \
+ $(libsystemd_LIBS) \
+ -lutil \
+ $(NULL)
+
+libcockpit_common_a_SOURCES = \
+ src/common/cockpitchannel.c \
+ src/common/cockpitchannel.h \
+ src/common/cockpitclosefrom.c \
+ src/common/cockpitcontrolmessages.c \
+ src/common/cockpitcontrolmessages.h \
+ src/common/cockpiterror.c \
+ src/common/cockpiterror.h \
+ src/common/cockpitflow.c \
+ src/common/cockpitflow.h \
+ src/common/cockpithacks-glib.h \
+ src/common/cockpithash.c \
+ src/common/cockpithash.h \
+ src/common/cockpitjson.c \
+ src/common/cockpitjson.h \
+ src/common/cockpitlocale.c \
+ src/common/cockpitlocale.h \
+ src/common/cockpitloopback.c \
+ src/common/cockpitloopback.h \
+ src/common/cockpitmachinesjson.c \
+ src/common/cockpitmachinesjson.h \
+ src/common/cockpitmemfdread.c \
+ src/common/cockpitmemfdread.h \
+ src/common/cockpitpipe.c \
+ src/common/cockpitpipe.h \
+ src/common/cockpitpipetransport.c \
+ src/common/cockpitpipetransport.h \
+ src/common/cockpitsocket.c \
+ src/common/cockpitsocket.h \
+ src/common/cockpitsystem.c \
+ src/common/cockpitsystem.h \
+ src/common/cockpittemplate.c \
+ src/common/cockpittemplate.h \
+ src/common/cockpittransport.c \
+ src/common/cockpittransport.h \
+ src/common/cockpitunicode.c \
+ src/common/cockpitunicode.h \
+ src/common/cockpitunixsignal.c \
+ src/common/cockpitunixsignal.h \
+ src/common/cockpitversion.c \
+ src/common/cockpitversion.h \
+ src/common/cockpitwebfilter.c \
+ src/common/cockpitwebfilter.h \
+ src/common/cockpitwebinject.c \
+ src/common/cockpitwebinject.h \
+ src/common/cockpitwebrequest-private.h \
+ src/common/cockpitwebresponse.c \
+ src/common/cockpitwebresponse.h \
+ src/common/cockpitwebserver.c \
+ src/common/cockpitwebserver.h \
+ $(NULL)
+
+
+# libcockpit-common.a static-links an HTML template to use on failures
+nodist_libcockpit_common_a_SOURCES = src/common/fail.html.c
+libpreload_temp_home_so_SOURCES = src/common/preload-temp-home.c
+libpreload_temp_home_so_CFLAGS = -fPIC $(AM_CFLAGS)
+libpreload_temp_home_so_LDFLAGS = -shared
+test_authorize_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_authorize_LDADD = $(TEST_LIBS)
+test_authorize_SOURCES = src/common/test-authorize.c
+test_base64_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_base64_LDADD = $(TEST_LIBS)
+test_base64_SOURCES = src/common/test-base64.c
+test_channel_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_channel_LDADD = $(TEST_LIBS)
+test_channel_SOURCES = src/common/test-channel.c
+test_config_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_config_LDADD = $(TEST_LIBS)
+test_config_SOURCES = src/common/test-config.c
+test_frame_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_frame_LDADD = $(TEST_LIBS)
+test_frame_SOURCES = src/common/test-frame.c
+test_hash_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_hash_LDADD = $(TEST_LIBS)
+test_hash_SOURCES = src/common/test-hash.c
+test_hex_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_hex_LDADD = $(TEST_LIBS)
+test_hex_SOURCES = src/common/test-hex.c
+test_json_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_json_LDADD = $(TEST_LIBS)
+test_json_SOURCES = src/common/test-json.c
+test_jsonfds_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_jsonfds_LDADD = $(TEST_LIBS)
+test_jsonfds_SOURCES = src/common/test-jsonfds.c
+test_locale_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_locale_LDADD = $(TEST_LIBS)
+test_locale_SOURCES = src/common/test-locale.c
+test_pipe_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_pipe_LDADD = $(TEST_LIBS)
+test_pipe_SOURCES = src/common/test-pipe.c
+test_system_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_system_LDADD = $(TEST_LIBS)
+test_system_SOURCES = src/common/test-system.c
+test_template_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_template_LDADD = $(TEST_LIBS)
+test_template_SOURCES = src/common/test-template.c
+test_transport_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_transport_LDADD = $(TEST_LIBS)
+test_transport_SOURCES = src/common/test-transport.c
+test_unicode_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_unicode_LDADD = $(TEST_LIBS)
+test_unicode_SOURCES = src/common/test-unicode.c
+test_unixsignal_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_unixsignal_LDADD = $(TEST_LIBS)
+test_unixsignal_SOURCES = src/common/test-unixsignal.c
+test_version_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_version_LDADD = $(TEST_LIBS)
+test_version_SOURCES = src/common/test-version.c
+test_webcertificate_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_webcertificate_LDADD = $(TEST_LIBS)
+test_webcertificate_SOURCES = src/common/test-webcertificate.c
+test_webresponse_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_webresponse_LDADD = $(TEST_LIBS)
+test_webresponse_SOURCES = src/common/test-webresponse.c
+test_webserver_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_webserver_LDADD = $(TEST_LIBS)
+test_webserver_SOURCES = src/common/test-webserver.c
+libpam_ssh_add_a_CFLAGS = -fPIC $(AM_CFLAGS)
+libpam_ssh_add_a_LIBS = \
+ libpam_ssh_add.a \
+ -lpam \
+ $(NULL)
+
+libpam_ssh_add_a_SOURCES = \
+ src/pam-ssh-add/pam-ssh-add.c \
+ src/pam-ssh-add/pam-ssh-add.h \
+ $(NULL)
+
+pam_ssh_add_so_CFLAGS = -fPIC $(AM_CFLAGS)
+pam_ssh_add_so_LDFLAGS = -shared
+pam_ssh_add_so_LDADD = $(libpam_ssh_add_a_LIBS)
+pam_ssh_add_so_SOURCES = src/pam-ssh-add/pam-ssh-add.c
+test_ssh_add_CPPFLAGS = $(TEST_CPP)
+test_ssh_add_LDADD = $(libpam_ssh_add_a_LIBS) $(TEST_LIBS)
+test_ssh_add_SOURCES = src/pam-ssh-add/test-ssh-add.c
+cockpit_session_LDADD = \
+ $(libcockpit_common_nodeps_a_LIBS) \
+ $(krb5_LIBS) \
+ $(libsystemd_LIBS) \
+ -lpam \
+ $(NULL)
+
+cockpit_session_SOURCES = \
+ src/common/cockpitclosefrom.c \
+ src/common/cockpithacks.h \
+ src/session/client-certificate.h \
+ src/session/client-certificate.c \
+ src/session/client-certificate.h \
+ src/session/session-utils.c \
+ src/session/session-utils.h \
+ src/session/session.c \
+ $(NULL)
+
+@WITH_COCKPIT_SSH_TRUE@libcockpit_ssh_a_CPPFLAGS = \
+@WITH_COCKPIT_SSH_TRUE@ -DG_LOG_DOMAIN=\"cockpit-ssh\" \
+@WITH_COCKPIT_SSH_TRUE@ $(glib_CFLAGS) \
+@WITH_COCKPIT_SSH_TRUE@ $(json_glib_CFLAGS) \
+@WITH_COCKPIT_SSH_TRUE@ $(libssh_CFLAGS) \
+@WITH_COCKPIT_SSH_TRUE@ $(AM_CPPFLAGS)
+
+@WITH_COCKPIT_SSH_TRUE@libcockpit_ssh_a_LIBS = \
+@WITH_COCKPIT_SSH_TRUE@ libcockpit-ssh.a \
+@WITH_COCKPIT_SSH_TRUE@ $(libcockpit_common_a_LIBS) \
+@WITH_COCKPIT_SSH_TRUE@ $(libssh_LIBS) \
+@WITH_COCKPIT_SSH_TRUE@ $(NULL)
+
+@WITH_COCKPIT_SSH_TRUE@libcockpit_ssh_a_SOURCES = \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/cockpitsshoptions.c \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/cockpitsshoptions.h \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/cockpitsshrelay.h \
+@WITH_COCKPIT_SSH_TRUE@ src/ssh/cockpitsshrelay.c \
+@WITH_COCKPIT_SSH_TRUE@ $(NULL)
+
+@WITH_COCKPIT_SSH_TRUE@cockpit_ssh_CPPFLAGS = $(libcockpit_ssh_a_CPPFLAGS)
+@WITH_COCKPIT_SSH_TRUE@cockpit_ssh_LDADD = $(libcockpit_ssh_a_LIBS)
+@WITH_COCKPIT_SSH_TRUE@cockpit_ssh_SOURCES = src/ssh/ssh.c
+
+# -----------------------------------------------------------------------------
+# C bridge config; Python bridge handles it internally
+@WITH_COCKPIT_SSH_TRUE@@WITH_OLD_BRIDGE_TRUE@sshmanifestdir = $(datadir)/cockpit/ssh
+@WITH_COCKPIT_SSH_TRUE@@WITH_OLD_BRIDGE_TRUE@dist_sshmanifest_DATA = src/ssh/manifest.json
+@WITH_COCKPIT_SSH_TRUE@mock_sshd_CPPFLAGS = $(libcockpit_ssh_a_CPPFLAGS) $(TEST_CPP)
+@WITH_COCKPIT_SSH_TRUE@mock_sshd_LDADD = $(libcockpit_ssh_a_LIBS) $(TEST_LIBS)
+@WITH_COCKPIT_SSH_TRUE@mock_sshd_SOURCES = src/ssh/mock-sshd.c
+@WITH_COCKPIT_SSH_TRUE@test_sshbridge_CPPFLAGS = $(libcockpit_ssh_a_CPPFLAGS) $(TEST_CPP)
+@WITH_COCKPIT_SSH_TRUE@test_sshbridge_LDADD = $(libcockpit_ssh_a_LIBS) $(TEST_LIBS)
+@WITH_COCKPIT_SSH_TRUE@test_sshbridge_SOURCES = src/ssh/test-sshbridge.c
+@WITH_COCKPIT_SSH_TRUE@test_sshoptions_CPPFLAGS = $(libcockpit_ssh_a_CPPFLAGS) $(TEST_CPP)
+@WITH_COCKPIT_SSH_TRUE@test_sshoptions_LDADD = $(libcockpit_ssh_a_LIBS) $(TEST_LIBS)
+@WITH_COCKPIT_SSH_TRUE@test_sshoptions_SOURCES = src/ssh/test-sshoptions.c
+nodist_systemdunit_DATA = \
+ src/systemd/cockpit-motd.service \
+ src/systemd/cockpit.service \
+ src/systemd/cockpit.socket \
+ src/systemd/cockpit-session@.service \
+ src/systemd/cockpit-session.socket \
+ src/systemd/cockpit-wsinstance-http.service \
+ src/systemd/cockpit-wsinstance-http.socket \
+ src/systemd/cockpit-wsinstance-https-factory@.service \
+ src/systemd/cockpit-wsinstance-https-factory.socket \
+ src/systemd/cockpit-wsinstance-https@.service \
+ src/systemd/cockpit-wsinstance-https@.socket \
+ $(NULL)
+
+dist_systemdunit_DATA = \
+ src/systemd/system-cockpithttps.slice \
+ $(NULL)
+
+motddir = $(datadir)/$(PACKAGE)/motd/
+dist_motd_DATA = src/systemd/inactive.motd
+dist_motd_SCRIPTS = src/systemd/update-motd
+tempconfdir = $(prefix)/lib/tmpfiles.d
+nodist_tempconf_DATA = src/systemd/cockpit-tempfiles.conf
+systemdgenerated = \
+ $(nodist_systemdunit_DATA) \
+ $(nodist_tempconf_DATA) \
+ $(NULL)
+
+systemdgenerated_in = $(patsubst %,%.in,$(systemdgenerated))
+libcockpit_tls_a_LIBS = \
+ libcockpit-tls.a \
+ $(libcockpit_common_nodeps_a_LIBS) \
+ $(gnutls_LIBS) \
+ -lpthread \
+ $(NULL)
+
+libcockpit_tls_a_SOURCES = \
+ src/tls/certificate.c \
+ src/tls/certificate.h \
+ src/tls/client-certificate.c \
+ src/tls/client-certificate.h \
+ src/tls/connection.c \
+ src/tls/connection.h \
+ src/tls/httpredirect.c \
+ src/tls/httpredirect.h \
+ src/tls/server.c \
+ src/tls/server.h \
+ src/tls/socket-io.c \
+ src/tls/socket-io.h \
+ src/tls/testing.h \
+ src/tls/utils.h \
+ $(NULL)
+
+cockpit_tls_LDADD = $(libcockpit_tls_a_LIBS) $(argp_LIBS)
+cockpit_tls_SOURCES = src/tls/main.c
+cockpit_certificate_ensure_LDADD = $(libcockpit_tls_a_LIBS)
+cockpit_certificate_ensure_SOURCES = src/tls/cockpit-certificate-ensure.c
+cockpit_wsinstance_factory_CPPFLAGS = \
+ $(libsystemd_CPPFLAGS) \
+ $(AM_CPPFLAGS)
+
+cockpit_wsinstance_factory_LDADD = \
+ $(libcockpit_tls_a_LIBS) \
+ $(libsystemd_LIBS) \
+ $(NULL)
+
+cockpit_wsinstance_factory_SOURCES = src/tls/wsinstance-factory.c
+wsinstance_start_LDADD = $(libcockpit_tls_a_LIBS)
+wsinstance_start_SOURCES = src/tls/wsinstance-start.c
+socket_activation_helper_LDADD = $(libcockpit_tls_a_LIBS)
+socket_activation_helper_SOURCES = src/tls/socket-activation-helper.c
+test_cockpit_certificate_ensure_CPPFLAGS = $(TEST_CPP)
+test_cockpit_certificate_ensure_LDADD = $(libcockpit_tls_a_LIBS) $(TEST_LIBS)
+test_cockpit_certificate_ensure_SOURCES = src/tls/test-cockpit-certificate-ensure.c
+test_tls_connection_CPPFLAGS = $(TEST_CPP)
+test_tls_connection_LDADD = $(libcockpit_tls_a_LIBS) $(TEST_LIBS)
+test_tls_connection_SOURCES = src/tls/test-connection.c
+test_tls_server_CPPFLAGS = $(TEST_CPP)
+test_tls_server_LDADD = $(libcockpit_tls_a_LIBS) $(TEST_LIBS)
+test_tls_server_SOURCES = src/tls/test-server.c
+libwebsocket_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"WebSocket\" \
+ $(glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libwebsocket_a_LIBS = \
+ libwebsocket.a \
+ $(glib_LIBS) \
+ $(NULL)
+
+libwebsocket_a_SOURCES = \
+ src/websocket/websocket.h \
+ src/websocket/websocket.c \
+ src/websocket/websocketclient.h \
+ src/websocket/websocketclient.c \
+ src/websocket/websocketserver.h \
+ src/websocket/websocketserver.c \
+ src/websocket/websocketconnection.h \
+ src/websocket/websocketconnection.c \
+ src/websocket/websocketprivate.h \
+ $(NULL)
+
+frob_websocket_CPPFLAGS = $(libwebsocket_a_CPPFLAGS) $(TEST_CPP)
+frob_websocket_LDADD = $(libwebsocket_a_LIBS) $(TEST_LIBS)
+frob_websocket_SOURCES = src/websocket/frob-websocket.c
+test_websocket_CPPFLAGS = $(libwebsocket_a_CPPFLAGS) $(TEST_CPP)
+test_websocket_LDADD = $(libwebsocket_a_LIBS) $(TEST_LIBS)
+test_websocket_SOURCES = src/websocket/test-websocket.c
+libcockpit_ws_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-ws\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libcockpit_ws_a_LIBS = \
+ libcockpit-ws.a \
+ $(libcockpit_common_a_LIBS) \
+ $(libsystemd_LIBS) \
+ -lcrypt \
+ $(NULL)
+
+libcockpit_ws_a_SOURCES = \
+ src/ws/cockpitws.h \
+ src/ws/cockpithandlers.h \
+ src/ws/cockpithandlers.c \
+ src/ws/cockpitauth.h \
+ src/ws/cockpitauth.c \
+ src/ws/cockpitcompat.c \
+ src/ws/cockpitcompat.h \
+ src/ws/cockpitbranding.h \
+ src/ws/cockpitbranding.c \
+ src/ws/cockpitchannelresponse.h \
+ src/ws/cockpitchannelresponse.c \
+ src/ws/cockpitchannelsocket.h \
+ src/ws/cockpitchannelsocket.c \
+ src/ws/cockpitcreds.h src/ws/cockpitcreds.c \
+ src/ws/cockpitwebservice.h \
+ src/ws/cockpitwebservice.c \
+ $(NULL)
+
+
+# -----------------------------------------------------------------------------
+# cockpit-ws
+cockpitwsdir = $(libexecdir)
+cockpit_ws_SOURCES = \
+ src/ws/main.c \
+ $(NULL)
+
+cockpit_ws_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS)
+cockpit_ws_LDADD = $(libcockpit_ws_a_LIBS)
+pam_cockpit_cert_so_CFLAGS = -fPIC $(AM_CFLAGS)
+pam_cockpit_cert_so_LDADD = -lpam
+pam_cockpit_cert_so_LDFLAGS = -shared
+pam_cockpit_cert_so_SOURCES = src/ws/pam_cockpit_cert.c
+test_server_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP) \
+ -I$(top_builddir)/src/ws
+test_server_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_server_SOURCES = \
+ src/ws/mock-service.c \
+ src/ws/mock-service.h \
+ src/ws/test-server.c \
+ $(NULL)
+
+nodist_test_server_SOURCES = $(GDBUS_CODEGEN_GENERATED)
+GDBUS_CODEGEN_GENERATED = \
+ src/ws/mock-dbus-tests.h \
+ src/ws/mock-dbus-tests.c \
+ $(NULL)
+
+GDBUS_CODEGEN_XML = $(srcdir)/src/ws/com.redhat.Cockpit.DBusTests.xml
+GDBUS_CODEGEN_INVOCATION = \
+ $(AM_V_GEN) gdbus-codegen \
+ --interface-prefix com.redhat.Cockpit.DBusTests \
+ --c-namespace Test \
+ --c-generate-object-manager \
+ $(NULL)
+
+mock_echo_CPPFLAGS = $(glib_CFLAGS) $(AM_CPPFLAGS)
+mock_echo_LDADD = $(glib_LIBS)
+mock_echo_SOURCES = src/ws/mock-echo.c
+mock_auth_command_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+mock_auth_command_LDADD = $(libcockpit_common_a_LIBS) $(TEST_LIBS)
+mock_auth_command_SOURCES = src/ws/mock-auth-command.c
+test_auth_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_auth_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_auth_SOURCES = src/ws/test-auth.c
+test_compat_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_compat_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_compat_SOURCES = src/ws/test-compat.c
+test_creds_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_creds_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_creds_SOURCES = src/ws/test-creds.c
+test_kerberos_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_kerberos_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS) $(krb5_LIBS)
+test_kerberos_SOURCES = src/ws/test-kerberos.c
+@WITH_OLD_BRIDGE_TRUE@test_channelresponse_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_channelresponse_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_channelresponse_SOURCES = src/ws/test-channelresponse.c
+@WITH_OLD_BRIDGE_TRUE@test_handlers_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_handlers_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_handlers_SOURCES = src/ws/test-handlers.c
+@WITH_OLD_BRIDGE_TRUE@test_webservice_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+@WITH_OLD_BRIDGE_TRUE@test_webservice_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+@WITH_OLD_BRIDGE_TRUE@test_webservice_SOURCES = src/ws/test-webservice.c
+@WITH_COCKPIT_SSH_TRUE@@WITH_OLD_BRIDGE_TRUE@test_authssh_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+@WITH_COCKPIT_SSH_TRUE@@WITH_OLD_BRIDGE_TRUE@test_authssh_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+@WITH_COCKPIT_SSH_TRUE@@WITH_OLD_BRIDGE_TRUE@test_authssh_SOURCES = src/ws/test-authssh.c
+mock_pam_conv_mod_so_SOURCES = src/ws/mock-pam-conv-mod.c
+mock_pam_conv_mod_so_CFLAGS = -fPIC $(AM_CFLAGS)
+mock_pam_conv_mod_so_LDFLAGS = -shared
+mock_pam_conv_mod_so_LDADD = $(PAM_LIBS)
+appdatadir = $(datadir)/metainfo
+nodist_appdata_DATA = src/ws/cockpit.appdata.xml
+appdata_in = src/ws/cockpit.appdata.xml.in
+pixmapdir = $(datadir)/pixmaps
+dist_pixmap_DATA = src/ws/cockpit.png
+all: $(BUILT_SOURCES) config.h
+ $(MAKE) $(AM_MAKEFLAGS) all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .html .html$(EXEEXT) .log .o .obj .sh .sh$(EXEEXT) .trs
+am--refresh: Makefile
+ @:
+$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(srcdir)/src/testlib/Makefile.am $(srcdir)/containers/Makefile.am $(srcdir)/containers/flatpak/Makefile.am $(srcdir)/doc/Makefile-doc.am $(srcdir)/doc/guide/Makefile-guide.am $(srcdir)/doc/man/Makefile-man.am $(srcdir)/pkg/Makefile.am $(srcdir)/po/Makefile.am $(srcdir)/selinux/Makefile.am $(srcdir)/src/Makefile.am $(srcdir)/src/branding/arch/Makefile.am $(srcdir)/src/branding/centos/Makefile.am $(srcdir)/src/branding/debian/Makefile.am $(srcdir)/src/branding/default/Makefile.am $(srcdir)/src/branding/fedora/Makefile.am $(srcdir)/src/branding/kubernetes/Makefile.am $(srcdir)/src/branding/opensuse/Makefile.am $(srcdir)/src/branding/registry/Makefile.am $(srcdir)/src/branding/rhel/Makefile.am $(srcdir)/src/branding/scientific/Makefile.am $(srcdir)/src/branding/ubuntu/Makefile.am $(srcdir)/src/bridge/Makefile.am $(srcdir)/src/client/Makefile.am $(srcdir)/src/common/Makefile-common.am $(srcdir)/src/pam-ssh-add/Makefile.am $(srcdir)/src/session/Makefile-session.am $(srcdir)/src/ssh/Makefile-ssh.am $(srcdir)/src/systemd/Makefile.am $(srcdir)/src/tls/Makefile-tls.am $(srcdir)/src/websocket/Makefile-websocket.am $(srcdir)/src/ws/Makefile-ws.am $(srcdir)/tools/Makefile-tools.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ echo ' cd $(srcdir) && $(AUTOMAKE) --foreign'; \
+ $(am__cd) $(srcdir) && $(AUTOMAKE) --foreign \
+ && exit 0; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ echo ' $(SHELL) ./config.status'; \
+ $(SHELL) ./config.status;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $@ $(am__maybe_remake_depfiles);; \
+ esac;
+$(srcdir)/src/testlib/Makefile.am $(srcdir)/containers/Makefile.am $(srcdir)/containers/flatpak/Makefile.am $(srcdir)/doc/Makefile-doc.am $(srcdir)/doc/guide/Makefile-guide.am $(srcdir)/doc/man/Makefile-man.am $(srcdir)/pkg/Makefile.am $(srcdir)/po/Makefile.am $(srcdir)/selinux/Makefile.am $(srcdir)/src/Makefile.am $(srcdir)/src/branding/arch/Makefile.am $(srcdir)/src/branding/centos/Makefile.am $(srcdir)/src/branding/debian/Makefile.am $(srcdir)/src/branding/default/Makefile.am $(srcdir)/src/branding/fedora/Makefile.am $(srcdir)/src/branding/kubernetes/Makefile.am $(srcdir)/src/branding/opensuse/Makefile.am $(srcdir)/src/branding/registry/Makefile.am $(srcdir)/src/branding/rhel/Makefile.am $(srcdir)/src/branding/scientific/Makefile.am $(srcdir)/src/branding/ubuntu/Makefile.am $(srcdir)/src/bridge/Makefile.am $(srcdir)/src/client/Makefile.am $(srcdir)/src/common/Makefile-common.am $(srcdir)/src/pam-ssh-add/Makefile.am $(srcdir)/src/session/Makefile-session.am $(srcdir)/src/ssh/Makefile-ssh.am $(srcdir)/src/systemd/Makefile.am $(srcdir)/src/tls/Makefile-tls.am $(srcdir)/src/websocket/Makefile-websocket.am $(srcdir)/src/ws/Makefile-ws.am $(srcdir)/tools/Makefile-tools.am $(am__empty):
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ $(SHELL) ./config.status --recheck
+
+$(top_srcdir)/configure: $(am__configure_deps)
+ $(am__cd) $(srcdir) && $(AUTOCONF)
+$(ACLOCAL_M4): $(am__aclocal_m4_deps)
+ $(am__cd) $(srcdir) && $(ACLOCAL) $(ACLOCAL_AMFLAGS)
+$(am__aclocal_m4_deps):
+
+config.h: stamp-h1
+ @test -f $@ || rm -f stamp-h1
+ @test -f $@ || $(MAKE) $(AM_MAKEFLAGS) stamp-h1
+
+stamp-h1: $(srcdir)/config.h.in $(top_builddir)/config.status
+ @rm -f stamp-h1
+ cd $(top_builddir) && $(SHELL) ./config.status config.h
+$(srcdir)/config.h.in: $(am__configure_deps)
+ ($(am__cd) $(top_srcdir) && $(AUTOHEADER))
+ rm -f stamp-h1
+ touch $@
+
+distclean-hdr:
+ -rm -f config.h stamp-h1
+doc/guide/version: $(top_builddir)/config.status $(top_srcdir)/doc/guide/version.in
+ cd $(top_builddir) && $(SHELL) ./config.status $@
+src/tls/cockpit-certificate-helper: $(top_builddir)/config.status $(top_srcdir)/src/tls/cockpit-certificate-helper.in
+ cd $(top_builddir) && $(SHELL) ./config.status $@
+src/ws/cockpit-desktop: $(top_builddir)/config.status $(top_srcdir)/src/ws/cockpit-desktop.in
+ cd $(top_builddir) && $(SHELL) ./config.status $@
+install-binPROGRAMS: $(bin_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(bindir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(bindir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(bindir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(bindir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-binPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(bin_PROGRAMS)'; test -n "$(bindir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(bindir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(bindir)" && rm -f $$files
+
+clean-binPROGRAMS:
+ -test -z "$(bin_PROGRAMS)" || rm -f $(bin_PROGRAMS)
+
+clean-checkPROGRAMS:
+ -test -z "$(check_PROGRAMS)" || rm -f $(check_PROGRAMS)
+install-cockpitwsPROGRAMS: $(cockpitws_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(cockpitws_PROGRAMS)'; test -n "$(cockpitwsdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(cockpitwsdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(cockpitwsdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(cockpitwsdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(cockpitwsdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-cockpitwsPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(cockpitws_PROGRAMS)'; test -n "$(cockpitwsdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(cockpitwsdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(cockpitwsdir)" && rm -f $$files
+
+clean-cockpitwsPROGRAMS:
+ -test -z "$(cockpitws_PROGRAMS)" || rm -f $(cockpitws_PROGRAMS)
+install-libexecPROGRAMS: $(libexec_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(libexec_PROGRAMS)'; test -n "$(libexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(libexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(libexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(libexecdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(libexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-libexecPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(libexec_PROGRAMS)'; test -n "$(libexecdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(libexecdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(libexecdir)" && rm -f $$files
+
+clean-libexecPROGRAMS:
+ -test -z "$(libexec_PROGRAMS)" || rm -f $(libexec_PROGRAMS)
+
+clean-noinstPROGRAMS:
+ -test -z "$(noinst_PROGRAMS)" || rm -f $(noinst_PROGRAMS)
+install-pamPROGRAMS: $(pam_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(pam_PROGRAMS)'; test -n "$(pamdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pamdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pamdir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pamdir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pamdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-pamPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pam_PROGRAMS)'; test -n "$(pamdir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(pamdir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(pamdir)" && rm -f $$files
+
+clean-pamPROGRAMS:
+ -test -z "$(pam_PROGRAMS)" || rm -f $(pam_PROGRAMS)
+install-sbinPROGRAMS: $(sbin_PROGRAMS)
+ @$(NORMAL_INSTALL)
+ @list='$(sbin_PROGRAMS)'; test -n "$(sbindir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(sbindir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(sbindir)" || exit 1; \
+ fi; \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed 's/$(EXEEXT)$$//' | \
+ while read p p1; do if test -f $$p \
+ ; then echo "$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n;h' \
+ -e 's|.*|.|' \
+ -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \
+ sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) files[d] = files[d] " " $$1; \
+ else { print "f", $$3 "/" $$4, $$1; } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(sbindir)$$dir'"; \
+ $(INSTALL_PROGRAM_ENV) $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(sbindir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-sbinPROGRAMS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(sbin_PROGRAMS)'; test -n "$(sbindir)" || list=; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \
+ -e 's/$$/$(EXEEXT)/' \
+ `; \
+ test -n "$$list" || exit 0; \
+ echo " ( cd '$(DESTDIR)$(sbindir)' && rm -f" $$files ")"; \
+ cd "$(DESTDIR)$(sbindir)" && rm -f $$files
+
+clean-sbinPROGRAMS:
+ -test -z "$(sbin_PROGRAMS)" || rm -f $(sbin_PROGRAMS)
+
+clean-checkLIBRARIES:
+ -test -z "$(check_LIBRARIES)" || rm -f $(check_LIBRARIES)
+
+clean-noinstLIBRARIES:
+ -test -z "$(noinst_LIBRARIES)" || rm -f $(noinst_LIBRARIES)
+src/bridge/$(am__dirstamp):
+ @$(MKDIR_P) src/bridge
+ @: > src/bridge/$(am__dirstamp)
+src/bridge/$(DEPDIR)/$(am__dirstamp):
+ @$(MKDIR_P) src/bridge/$(DEPDIR)
+ @: > src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitconnect.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitdbuscache.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitdbusconfig.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitdbusinternal.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitdbusjson.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitdbusmachines.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitdbusmeta.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitdbusprocess.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitdbusrules.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitdbususer.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitechochannel.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitfslist.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitfsread.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitfsreplace.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitfswatch.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpithttpstream.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitinteracttransport.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitnullchannel.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitpackages.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitpacketchannel.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitpaths.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitpeer.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitpipechannel.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitrouter.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitstream.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_bridge_a-cockpitpolkitagent.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+libcockpit-bridge.a: $(libcockpit_bridge_a_OBJECTS) $(libcockpit_bridge_a_DEPENDENCIES) $(EXTRA_libcockpit_bridge_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libcockpit-bridge.a
+ $(AM_V_AR)$(libcockpit_bridge_a_AR) libcockpit-bridge.a $(libcockpit_bridge_a_OBJECTS) $(libcockpit_bridge_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libcockpit-bridge.a
+src/common/$(am__dirstamp):
+ @$(MKDIR_P) src/common
+ @: > src/common/$(am__dirstamp)
+src/common/$(DEPDIR)/$(am__dirstamp):
+ @$(MKDIR_P) src/common/$(DEPDIR)
+ @: > src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/cockpitauthorize.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/cockpitbase64.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/cockpitconf.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/cockpitfdpassing.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/cockpitframe.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/cockpithex.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/cockpitjsonprint.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/cockpitmemory.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/cockpitwebcertificate.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+libcockpit-common-nodeps.a: $(libcockpit_common_nodeps_a_OBJECTS) $(libcockpit_common_nodeps_a_DEPENDENCIES) $(EXTRA_libcockpit_common_nodeps_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libcockpit-common-nodeps.a
+ $(AM_V_AR)$(libcockpit_common_nodeps_a_AR) libcockpit-common-nodeps.a $(libcockpit_common_nodeps_a_OBJECTS) $(libcockpit_common_nodeps_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libcockpit-common-nodeps.a
+src/common/libcockpit_common_a-cockpitchannel.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitclosefrom.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitcontrolmessages.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpiterror.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitflow.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpithash.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitjson.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitlocale.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitloopback.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitmachinesjson.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitmemfdread.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitpipe.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitpipetransport.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitsocket.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitsystem.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpittemplate.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpittransport.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitunicode.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitunixsignal.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitversion.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitwebfilter.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitwebinject.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitwebresponse.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-cockpitwebserver.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/common/libcockpit_common_a-fail.html.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+libcockpit-common.a: $(libcockpit_common_a_OBJECTS) $(libcockpit_common_a_DEPENDENCIES) $(EXTRA_libcockpit_common_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libcockpit-common.a
+ $(AM_V_AR)$(libcockpit_common_a_AR) libcockpit-common.a $(libcockpit_common_a_OBJECTS) $(libcockpit_common_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libcockpit-common.a
+src/bridge/libcockpit_metrics_a-cockpitblocksamples.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_metrics_a-cockpitcpusamples.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_metrics_a-cockpitdisksamples.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_metrics_a-cockpitmemorysamples.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_metrics_a-cockpitmetrics.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_metrics_a-cockpitmountsamples.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_metrics_a-cockpitnetworksamples.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_metrics_a-cockpitsamples.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+libcockpit-metrics.a: $(libcockpit_metrics_a_OBJECTS) $(libcockpit_metrics_a_DEPENDENCIES) $(EXTRA_libcockpit_metrics_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libcockpit-metrics.a
+ $(AM_V_AR)$(libcockpit_metrics_a_AR) libcockpit-metrics.a $(libcockpit_metrics_a_OBJECTS) $(libcockpit_metrics_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libcockpit-metrics.a
+src/bridge/libcockpit_pcp_a-cockpitconnect.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_pcp_a-cockpitdbusinternal.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_pcp_a-cockpitpeer.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+src/bridge/libcockpit_pcp_a-cockpitrouter.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+libcockpit-pcp.a: $(libcockpit_pcp_a_OBJECTS) $(libcockpit_pcp_a_DEPENDENCIES) $(EXTRA_libcockpit_pcp_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libcockpit-pcp.a
+ $(AM_V_AR)$(libcockpit_pcp_a_AR) libcockpit-pcp.a $(libcockpit_pcp_a_OBJECTS) $(libcockpit_pcp_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libcockpit-pcp.a
+src/ssh/$(am__dirstamp):
+ @$(MKDIR_P) src/ssh
+ @: > src/ssh/$(am__dirstamp)
+src/ssh/$(DEPDIR)/$(am__dirstamp):
+ @$(MKDIR_P) src/ssh/$(DEPDIR)
+ @: > src/ssh/$(DEPDIR)/$(am__dirstamp)
+src/ssh/libcockpit_ssh_a-cockpitsshoptions.$(OBJEXT): \
+ src/ssh/$(am__dirstamp) src/ssh/$(DEPDIR)/$(am__dirstamp)
+src/ssh/libcockpit_ssh_a-cockpitsshrelay.$(OBJEXT): \
+ src/ssh/$(am__dirstamp) src/ssh/$(DEPDIR)/$(am__dirstamp)
+
+libcockpit-ssh.a: $(libcockpit_ssh_a_OBJECTS) $(libcockpit_ssh_a_DEPENDENCIES) $(EXTRA_libcockpit_ssh_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libcockpit-ssh.a
+ $(AM_V_AR)$(libcockpit_ssh_a_AR) libcockpit-ssh.a $(libcockpit_ssh_a_OBJECTS) $(libcockpit_ssh_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libcockpit-ssh.a
+src/testlib/$(am__dirstamp):
+ @$(MKDIR_P) src/testlib
+ @: > src/testlib/$(am__dirstamp)
+src/testlib/$(DEPDIR)/$(am__dirstamp):
+ @$(MKDIR_P) src/testlib/$(DEPDIR)
+ @: > src/testlib/$(DEPDIR)/$(am__dirstamp)
+src/testlib/libcockpit_test_a-cockpittest.$(OBJEXT): \
+ src/testlib/$(am__dirstamp) \
+ src/testlib/$(DEPDIR)/$(am__dirstamp)
+src/testlib/libcockpit_test_a-mock-auth.$(OBJEXT): \
+ src/testlib/$(am__dirstamp) \
+ src/testlib/$(DEPDIR)/$(am__dirstamp)
+src/testlib/libcockpit_test_a-mock-channel.$(OBJEXT): \
+ src/testlib/$(am__dirstamp) \
+ src/testlib/$(DEPDIR)/$(am__dirstamp)
+src/testlib/libcockpit_test_a-mock-pressure.$(OBJEXT): \
+ src/testlib/$(am__dirstamp) \
+ src/testlib/$(DEPDIR)/$(am__dirstamp)
+src/testlib/libcockpit_test_a-mock-transport.$(OBJEXT): \
+ src/testlib/$(am__dirstamp) \
+ src/testlib/$(DEPDIR)/$(am__dirstamp)
+src/testlib/libcockpit_test_a-retest.$(OBJEXT): \
+ src/testlib/$(am__dirstamp) \
+ src/testlib/$(DEPDIR)/$(am__dirstamp)
+
+libcockpit-test.a: $(libcockpit_test_a_OBJECTS) $(libcockpit_test_a_DEPENDENCIES) $(EXTRA_libcockpit_test_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libcockpit-test.a
+ $(AM_V_AR)$(libcockpit_test_a_AR) libcockpit-test.a $(libcockpit_test_a_OBJECTS) $(libcockpit_test_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libcockpit-test.a
+src/tls/$(am__dirstamp):
+ @$(MKDIR_P) src/tls
+ @: > src/tls/$(am__dirstamp)
+src/tls/$(DEPDIR)/$(am__dirstamp):
+ @$(MKDIR_P) src/tls/$(DEPDIR)
+ @: > src/tls/$(DEPDIR)/$(am__dirstamp)
+src/tls/certificate.$(OBJEXT): src/tls/$(am__dirstamp) \
+ src/tls/$(DEPDIR)/$(am__dirstamp)
+src/tls/client-certificate.$(OBJEXT): src/tls/$(am__dirstamp) \
+ src/tls/$(DEPDIR)/$(am__dirstamp)
+src/tls/connection.$(OBJEXT): src/tls/$(am__dirstamp) \
+ src/tls/$(DEPDIR)/$(am__dirstamp)
+src/tls/httpredirect.$(OBJEXT): src/tls/$(am__dirstamp) \
+ src/tls/$(DEPDIR)/$(am__dirstamp)
+src/tls/server.$(OBJEXT): src/tls/$(am__dirstamp) \
+ src/tls/$(DEPDIR)/$(am__dirstamp)
+src/tls/socket-io.$(OBJEXT): src/tls/$(am__dirstamp) \
+ src/tls/$(DEPDIR)/$(am__dirstamp)
+
+libcockpit-tls.a: $(libcockpit_tls_a_OBJECTS) $(libcockpit_tls_a_DEPENDENCIES) $(EXTRA_libcockpit_tls_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libcockpit-tls.a
+ $(AM_V_AR)$(libcockpit_tls_a_AR) libcockpit-tls.a $(libcockpit_tls_a_OBJECTS) $(libcockpit_tls_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libcockpit-tls.a
+src/ws/$(am__dirstamp):
+ @$(MKDIR_P) src/ws
+ @: > src/ws/$(am__dirstamp)
+src/ws/$(DEPDIR)/$(am__dirstamp):
+ @$(MKDIR_P) src/ws/$(DEPDIR)
+ @: > src/ws/$(DEPDIR)/$(am__dirstamp)
+src/ws/libcockpit_ws_a-cockpithandlers.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+src/ws/libcockpit_ws_a-cockpitauth.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+src/ws/libcockpit_ws_a-cockpitcompat.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+src/ws/libcockpit_ws_a-cockpitbranding.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+src/ws/libcockpit_ws_a-cockpitchannelresponse.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+src/ws/libcockpit_ws_a-cockpitchannelsocket.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+src/ws/libcockpit_ws_a-cockpitcreds.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+src/ws/libcockpit_ws_a-cockpitwebservice.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+
+libcockpit-ws.a: $(libcockpit_ws_a_OBJECTS) $(libcockpit_ws_a_DEPENDENCIES) $(EXTRA_libcockpit_ws_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libcockpit-ws.a
+ $(AM_V_AR)$(libcockpit_ws_a_AR) libcockpit-ws.a $(libcockpit_ws_a_OBJECTS) $(libcockpit_ws_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libcockpit-ws.a
+src/pam-ssh-add/$(am__dirstamp):
+ @$(MKDIR_P) src/pam-ssh-add
+ @: > src/pam-ssh-add/$(am__dirstamp)
+src/pam-ssh-add/$(DEPDIR)/$(am__dirstamp):
+ @$(MKDIR_P) src/pam-ssh-add/$(DEPDIR)
+ @: > src/pam-ssh-add/$(DEPDIR)/$(am__dirstamp)
+src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.$(OBJEXT): \
+ src/pam-ssh-add/$(am__dirstamp) \
+ src/pam-ssh-add/$(DEPDIR)/$(am__dirstamp)
+
+libpam_ssh_add.a: $(libpam_ssh_add_a_OBJECTS) $(libpam_ssh_add_a_DEPENDENCIES) $(EXTRA_libpam_ssh_add_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libpam_ssh_add.a
+ $(AM_V_AR)$(libpam_ssh_add_a_AR) libpam_ssh_add.a $(libpam_ssh_add_a_OBJECTS) $(libpam_ssh_add_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libpam_ssh_add.a
+src/websocket/$(am__dirstamp):
+ @$(MKDIR_P) src/websocket
+ @: > src/websocket/$(am__dirstamp)
+src/websocket/$(DEPDIR)/$(am__dirstamp):
+ @$(MKDIR_P) src/websocket/$(DEPDIR)
+ @: > src/websocket/$(DEPDIR)/$(am__dirstamp)
+src/websocket/libwebsocket_a-websocket.$(OBJEXT): \
+ src/websocket/$(am__dirstamp) \
+ src/websocket/$(DEPDIR)/$(am__dirstamp)
+src/websocket/libwebsocket_a-websocketclient.$(OBJEXT): \
+ src/websocket/$(am__dirstamp) \
+ src/websocket/$(DEPDIR)/$(am__dirstamp)
+src/websocket/libwebsocket_a-websocketserver.$(OBJEXT): \
+ src/websocket/$(am__dirstamp) \
+ src/websocket/$(DEPDIR)/$(am__dirstamp)
+src/websocket/libwebsocket_a-websocketconnection.$(OBJEXT): \
+ src/websocket/$(am__dirstamp) \
+ src/websocket/$(DEPDIR)/$(am__dirstamp)
+
+libwebsocket.a: $(libwebsocket_a_OBJECTS) $(libwebsocket_a_DEPENDENCIES) $(EXTRA_libwebsocket_a_DEPENDENCIES)
+ $(AM_V_at)-rm -f libwebsocket.a
+ $(AM_V_AR)$(libwebsocket_a_AR) libwebsocket.a $(libwebsocket_a_OBJECTS) $(libwebsocket_a_LIBADD)
+ $(AM_V_at)$(RANLIB) libwebsocket.a
+src/bridge/cockpit_askpass-askpass.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+cockpit-askpass$(EXEEXT): $(cockpit_askpass_OBJECTS) $(cockpit_askpass_DEPENDENCIES) $(EXTRA_cockpit_askpass_DEPENDENCIES)
+ @rm -f cockpit-askpass$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(cockpit_askpass_OBJECTS) $(cockpit_askpass_LDADD) $(LIBS)
+src/bridge/cockpit_bridge-bridge.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+cockpit-bridge$(EXEEXT): $(cockpit_bridge_OBJECTS) $(cockpit_bridge_DEPENDENCIES) $(EXTRA_cockpit_bridge_DEPENDENCIES)
+ @rm -f cockpit-bridge$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(cockpit_bridge_OBJECTS) $(cockpit_bridge_LDADD) $(LIBS)
+src/tls/cockpit-certificate-ensure.$(OBJEXT): src/tls/$(am__dirstamp) \
+ src/tls/$(DEPDIR)/$(am__dirstamp)
+
+cockpit-certificate-ensure$(EXEEXT): $(cockpit_certificate_ensure_OBJECTS) $(cockpit_certificate_ensure_DEPENDENCIES) $(EXTRA_cockpit_certificate_ensure_DEPENDENCIES)
+ @rm -f cockpit-certificate-ensure$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(cockpit_certificate_ensure_OBJECTS) $(cockpit_certificate_ensure_LDADD) $(LIBS)
+src/bridge/cockpit_pcp-cockpitpcp.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+cockpit-pcp$(EXEEXT): $(cockpit_pcp_OBJECTS) $(cockpit_pcp_DEPENDENCIES) $(EXTRA_cockpit_pcp_DEPENDENCIES)
+ @rm -f cockpit-pcp$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(cockpit_pcp_OBJECTS) $(cockpit_pcp_LDADD) $(LIBS)
+src/common/cockpitclosefrom.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+src/session/$(am__dirstamp):
+ @$(MKDIR_P) src/session
+ @: > src/session/$(am__dirstamp)
+src/session/$(DEPDIR)/$(am__dirstamp):
+ @$(MKDIR_P) src/session/$(DEPDIR)
+ @: > src/session/$(DEPDIR)/$(am__dirstamp)
+src/session/client-certificate.$(OBJEXT): src/session/$(am__dirstamp) \
+ src/session/$(DEPDIR)/$(am__dirstamp)
+src/session/session-utils.$(OBJEXT): src/session/$(am__dirstamp) \
+ src/session/$(DEPDIR)/$(am__dirstamp)
+src/session/session.$(OBJEXT): src/session/$(am__dirstamp) \
+ src/session/$(DEPDIR)/$(am__dirstamp)
+
+cockpit-session$(EXEEXT): $(cockpit_session_OBJECTS) $(cockpit_session_DEPENDENCIES) $(EXTRA_cockpit_session_DEPENDENCIES)
+ @rm -f cockpit-session$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(cockpit_session_OBJECTS) $(cockpit_session_LDADD) $(LIBS)
+src/ssh/cockpit_ssh-ssh.$(OBJEXT): src/ssh/$(am__dirstamp) \
+ src/ssh/$(DEPDIR)/$(am__dirstamp)
+
+cockpit-ssh$(EXEEXT): $(cockpit_ssh_OBJECTS) $(cockpit_ssh_DEPENDENCIES) $(EXTRA_cockpit_ssh_DEPENDENCIES)
+ @rm -f cockpit-ssh$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(cockpit_ssh_OBJECTS) $(cockpit_ssh_LDADD) $(LIBS)
+src/tls/main.$(OBJEXT): src/tls/$(am__dirstamp) \
+ src/tls/$(DEPDIR)/$(am__dirstamp)
+
+cockpit-tls$(EXEEXT): $(cockpit_tls_OBJECTS) $(cockpit_tls_DEPENDENCIES) $(EXTRA_cockpit_tls_DEPENDENCIES)
+ @rm -f cockpit-tls$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(cockpit_tls_OBJECTS) $(cockpit_tls_LDADD) $(LIBS)
+src/ws/cockpit_ws-main.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+
+cockpit-ws$(EXEEXT): $(cockpit_ws_OBJECTS) $(cockpit_ws_DEPENDENCIES) $(EXTRA_cockpit_ws_DEPENDENCIES)
+ @rm -f cockpit-ws$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(cockpit_ws_OBJECTS) $(cockpit_ws_LDADD) $(LIBS)
+src/tls/cockpit_wsinstance_factory-wsinstance-factory.$(OBJEXT): \
+ src/tls/$(am__dirstamp) src/tls/$(DEPDIR)/$(am__dirstamp)
+
+cockpit-wsinstance-factory$(EXEEXT): $(cockpit_wsinstance_factory_OBJECTS) $(cockpit_wsinstance_factory_DEPENDENCIES) $(EXTRA_cockpit_wsinstance_factory_DEPENDENCIES)
+ @rm -f cockpit-wsinstance-factory$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(cockpit_wsinstance_factory_OBJECTS) $(cockpit_wsinstance_factory_LDADD) $(LIBS)
+src/websocket/frob_websocket-frob-websocket.$(OBJEXT): \
+ src/websocket/$(am__dirstamp) \
+ src/websocket/$(DEPDIR)/$(am__dirstamp)
+
+frob-websocket$(EXEEXT): $(frob_websocket_OBJECTS) $(frob_websocket_DEPENDENCIES) $(EXTRA_frob_websocket_DEPENDENCIES)
+ @rm -f frob-websocket$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(frob_websocket_OBJECTS) $(frob_websocket_LDADD) $(LIBS)
+src/common/libpreload_temp_home_so-preload-temp-home.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+libpreload-temp-home.so$(EXEEXT): $(libpreload_temp_home_so_OBJECTS) $(libpreload_temp_home_so_DEPENDENCIES) $(EXTRA_libpreload_temp_home_so_DEPENDENCIES)
+ @rm -f libpreload-temp-home.so$(EXEEXT)
+ $(AM_V_CCLD)$(libpreload_temp_home_so_LINK) $(libpreload_temp_home_so_OBJECTS) $(libpreload_temp_home_so_LDADD) $(LIBS)
+src/ws/mock_auth_command-mock-auth-command.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+
+mock-auth-command$(EXEEXT): $(mock_auth_command_OBJECTS) $(mock_auth_command_DEPENDENCIES) $(EXTRA_mock_auth_command_DEPENDENCIES)
+ @rm -f mock-auth-command$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(mock_auth_command_OBJECTS) $(mock_auth_command_LDADD) $(LIBS)
+src/bridge/mock_bridge-mock-bridge.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+mock-bridge$(EXEEXT): $(mock_bridge_OBJECTS) $(mock_bridge_DEPENDENCIES) $(EXTRA_mock_bridge_DEPENDENCIES)
+ @rm -f mock-bridge$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(mock_bridge_OBJECTS) $(mock_bridge_LDADD) $(LIBS)
+src/ws/mock_echo-mock-echo.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+
+mock-echo$(EXEEXT): $(mock_echo_OBJECTS) $(mock_echo_DEPENDENCIES) $(EXTRA_mock_echo_DEPENDENCIES)
+ @rm -f mock-echo$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(mock_echo_OBJECTS) $(mock_echo_LDADD) $(LIBS)
+src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+
+mock-pam-conv-mod.so$(EXEEXT): $(mock_pam_conv_mod_so_OBJECTS) $(mock_pam_conv_mod_so_DEPENDENCIES) $(EXTRA_mock_pam_conv_mod_so_DEPENDENCIES)
+ @rm -f mock-pam-conv-mod.so$(EXEEXT)
+ $(AM_V_CCLD)$(mock_pam_conv_mod_so_LINK) $(mock_pam_conv_mod_so_OBJECTS) $(mock_pam_conv_mod_so_LDADD) $(LIBS)
+src/bridge/mock_pmda_so-mock-pmda.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+mock-pmda.so$(EXEEXT): $(mock_pmda_so_OBJECTS) $(mock_pmda_so_DEPENDENCIES) $(EXTRA_mock_pmda_so_DEPENDENCIES)
+ @rm -f mock-pmda.so$(EXEEXT)
+ $(AM_V_CCLD)$(mock_pmda_so_LINK) $(mock_pmda_so_OBJECTS) $(mock_pmda_so_LDADD) $(LIBS)
+src/ssh/mock_sshd-mock-sshd.$(OBJEXT): src/ssh/$(am__dirstamp) \
+ src/ssh/$(DEPDIR)/$(am__dirstamp)
+
+mock-sshd$(EXEEXT): $(mock_sshd_OBJECTS) $(mock_sshd_DEPENDENCIES) $(EXTRA_mock_sshd_DEPENDENCIES)
+ @rm -f mock-sshd$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(mock_sshd_OBJECTS) $(mock_sshd_LDADD) $(LIBS)
+src/ws/pam_cockpit_cert_so-pam_cockpit_cert.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+
+pam_cockpit_cert.so$(EXEEXT): $(pam_cockpit_cert_so_OBJECTS) $(pam_cockpit_cert_so_DEPENDENCIES) $(EXTRA_pam_cockpit_cert_so_DEPENDENCIES)
+ @rm -f pam_cockpit_cert.so$(EXEEXT)
+ $(AM_V_CCLD)$(pam_cockpit_cert_so_LINK) $(pam_cockpit_cert_so_OBJECTS) $(pam_cockpit_cert_so_LDADD) $(LIBS)
+src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.$(OBJEXT): \
+ src/pam-ssh-add/$(am__dirstamp) \
+ src/pam-ssh-add/$(DEPDIR)/$(am__dirstamp)
+
+pam_ssh_add.so$(EXEEXT): $(pam_ssh_add_so_OBJECTS) $(pam_ssh_add_so_DEPENDENCIES) $(EXTRA_pam_ssh_add_so_DEPENDENCIES)
+ @rm -f pam_ssh_add.so$(EXEEXT)
+ $(AM_V_CCLD)$(pam_ssh_add_so_LINK) $(pam_ssh_add_so_OBJECTS) $(pam_ssh_add_so_LDADD) $(LIBS)
+src/tls/socket-activation-helper.$(OBJEXT): src/tls/$(am__dirstamp) \
+ src/tls/$(DEPDIR)/$(am__dirstamp)
+
+socket-activation-helper$(EXEEXT): $(socket_activation_helper_OBJECTS) $(socket_activation_helper_DEPENDENCIES) $(EXTRA_socket_activation_helper_DEPENDENCIES)
+ @rm -f socket-activation-helper$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(socket_activation_helper_OBJECTS) $(socket_activation_helper_LDADD) $(LIBS)
+src/ws/test_auth-test-auth.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+
+test-auth$(EXEEXT): $(test_auth_OBJECTS) $(test_auth_DEPENDENCIES) $(EXTRA_test_auth_DEPENDENCIES)
+ @rm -f test-auth$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_auth_OBJECTS) $(test_auth_LDADD) $(LIBS)
+src/common/test_authorize-test-authorize.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-authorize$(EXEEXT): $(test_authorize_OBJECTS) $(test_authorize_DEPENDENCIES) $(EXTRA_test_authorize_DEPENDENCIES)
+ @rm -f test-authorize$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_authorize_OBJECTS) $(test_authorize_LDADD) $(LIBS)
+src/ws/test_authssh-test-authssh.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+
+test-authssh$(EXEEXT): $(test_authssh_OBJECTS) $(test_authssh_DEPENDENCIES) $(EXTRA_test_authssh_DEPENDENCIES)
+ @rm -f test-authssh$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_authssh_OBJECTS) $(test_authssh_LDADD) $(LIBS)
+src/common/test_base64-test-base64.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-base64$(EXEEXT): $(test_base64_OBJECTS) $(test_base64_DEPENDENCIES) $(EXTRA_test_base64_DEPENDENCIES)
+ @rm -f test-base64$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_base64_OBJECTS) $(test_base64_LDADD) $(LIBS)
+src/bridge/test_bridge-test-bridge.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-bridge$(EXEEXT): $(test_bridge_OBJECTS) $(test_bridge_DEPENDENCIES) $(EXTRA_test_bridge_DEPENDENCIES)
+ @rm -f test-bridge$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_bridge_OBJECTS) $(test_bridge_LDADD) $(LIBS)
+src/common/test_channel-test-channel.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-channel$(EXEEXT): $(test_channel_OBJECTS) $(test_channel_DEPENDENCIES) $(EXTRA_test_channel_DEPENDENCIES)
+ @rm -f test-channel$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_channel_OBJECTS) $(test_channel_LDADD) $(LIBS)
+src/ws/test_channelresponse-test-channelresponse.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+
+test-channelresponse$(EXEEXT): $(test_channelresponse_OBJECTS) $(test_channelresponse_DEPENDENCIES) $(EXTRA_test_channelresponse_DEPENDENCIES)
+ @rm -f test-channelresponse$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_channelresponse_OBJECTS) $(test_channelresponse_LDADD) $(LIBS)
+src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.$(OBJEXT): \
+ src/tls/$(am__dirstamp) src/tls/$(DEPDIR)/$(am__dirstamp)
+
+test-cockpit-certificate-ensure$(EXEEXT): $(test_cockpit_certificate_ensure_OBJECTS) $(test_cockpit_certificate_ensure_DEPENDENCIES) $(EXTRA_test_cockpit_certificate_ensure_DEPENDENCIES)
+ @rm -f test-cockpit-certificate-ensure$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_cockpit_certificate_ensure_OBJECTS) $(test_cockpit_certificate_ensure_LDADD) $(LIBS)
+src/ws/test_compat-test-compat.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+
+test-compat$(EXEEXT): $(test_compat_OBJECTS) $(test_compat_DEPENDENCIES) $(EXTRA_test_compat_DEPENDENCIES)
+ @rm -f test-compat$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_compat_OBJECTS) $(test_compat_LDADD) $(LIBS)
+src/common/test_config-test-config.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-config$(EXEEXT): $(test_config_OBJECTS) $(test_config_DEPENDENCIES) $(EXTRA_test_config_DEPENDENCIES)
+ @rm -f test-config$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_config_OBJECTS) $(test_config_LDADD) $(LIBS)
+src/bridge/test_connect-test-connect.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-connect$(EXEEXT): $(test_connect_OBJECTS) $(test_connect_DEPENDENCIES) $(EXTRA_test_connect_DEPENDENCIES)
+ @rm -f test-connect$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_connect_OBJECTS) $(test_connect_LDADD) $(LIBS)
+src/ws/test_creds-test-creds.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+
+test-creds$(EXEEXT): $(test_creds_OBJECTS) $(test_creds_DEPENDENCIES) $(EXTRA_test_creds_DEPENDENCIES)
+ @rm -f test-creds$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_creds_OBJECTS) $(test_creds_LDADD) $(LIBS)
+src/bridge/test_dbus_meta-test-dbus-meta.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-dbus-meta$(EXEEXT): $(test_dbus_meta_OBJECTS) $(test_dbus_meta_DEPENDENCIES) $(EXTRA_test_dbus_meta_DEPENDENCIES)
+ @rm -f test-dbus-meta$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_dbus_meta_OBJECTS) $(test_dbus_meta_LDADD) $(LIBS)
+src/common/test_frame-test-frame.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-frame$(EXEEXT): $(test_frame_OBJECTS) $(test_frame_DEPENDENCIES) $(EXTRA_test_frame_DEPENDENCIES)
+ @rm -f test-frame$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_frame_OBJECTS) $(test_frame_LDADD) $(LIBS)
+src/bridge/test_fs-test-fs.$(OBJEXT): src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-fs$(EXEEXT): $(test_fs_OBJECTS) $(test_fs_DEPENDENCIES) $(EXTRA_test_fs_DEPENDENCIES)
+ @rm -f test-fs$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_fs_OBJECTS) $(test_fs_LDADD) $(LIBS)
+src/ws/test_handlers-test-handlers.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+
+test-handlers$(EXEEXT): $(test_handlers_OBJECTS) $(test_handlers_DEPENDENCIES) $(EXTRA_test_handlers_DEPENDENCIES)
+ @rm -f test-handlers$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_handlers_OBJECTS) $(test_handlers_LDADD) $(LIBS)
+src/common/test_hash-test-hash.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-hash$(EXEEXT): $(test_hash_OBJECTS) $(test_hash_DEPENDENCIES) $(EXTRA_test_hash_DEPENDENCIES)
+ @rm -f test-hash$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_hash_OBJECTS) $(test_hash_LDADD) $(LIBS)
+src/common/test_hex-test-hex.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-hex$(EXEEXT): $(test_hex_OBJECTS) $(test_hex_DEPENDENCIES) $(EXTRA_test_hex_DEPENDENCIES)
+ @rm -f test-hex$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_hex_OBJECTS) $(test_hex_LDADD) $(LIBS)
+src/bridge/test_httpstream-test-httpstream.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-httpstream$(EXEEXT): $(test_httpstream_OBJECTS) $(test_httpstream_DEPENDENCIES) $(EXTRA_test_httpstream_DEPENDENCIES)
+ @rm -f test-httpstream$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_httpstream_OBJECTS) $(test_httpstream_LDADD) $(LIBS)
+src/common/test_json-test-json.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-json$(EXEEXT): $(test_json_OBJECTS) $(test_json_DEPENDENCIES) $(EXTRA_test_json_DEPENDENCIES)
+ @rm -f test-json$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_json_OBJECTS) $(test_json_LDADD) $(LIBS)
+src/common/test_jsonfds-test-jsonfds.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-jsonfds$(EXEEXT): $(test_jsonfds_OBJECTS) $(test_jsonfds_DEPENDENCIES) $(EXTRA_test_jsonfds_DEPENDENCIES)
+ @rm -f test-jsonfds$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_jsonfds_OBJECTS) $(test_jsonfds_LDADD) $(LIBS)
+src/ws/test_kerberos-test-kerberos.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+
+test-kerberos$(EXEEXT): $(test_kerberos_OBJECTS) $(test_kerberos_DEPENDENCIES) $(EXTRA_test_kerberos_DEPENDENCIES)
+ @rm -f test-kerberos$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_kerberos_OBJECTS) $(test_kerberos_LDADD) $(LIBS)
+src/common/test_locale-test-locale.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-locale$(EXEEXT): $(test_locale_OBJECTS) $(test_locale_DEPENDENCIES) $(EXTRA_test_locale_DEPENDENCIES)
+ @rm -f test-locale$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_locale_OBJECTS) $(test_locale_LDADD) $(LIBS)
+src/bridge/test_metrics-test-metrics.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-metrics$(EXEEXT): $(test_metrics_OBJECTS) $(test_metrics_DEPENDENCIES) $(EXTRA_test_metrics_DEPENDENCIES)
+ @rm -f test-metrics$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_metrics_OBJECTS) $(test_metrics_LDADD) $(LIBS)
+src/bridge/test_packages-test-packages.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-packages$(EXEEXT): $(test_packages_OBJECTS) $(test_packages_DEPENDENCIES) $(EXTRA_test_packages_DEPENDENCIES)
+ @rm -f test-packages$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_packages_OBJECTS) $(test_packages_LDADD) $(LIBS)
+src/bridge/test_packet_channel-test-packet-channel.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-packet-channel$(EXEEXT): $(test_packet_channel_OBJECTS) $(test_packet_channel_DEPENDENCIES) $(EXTRA_test_packet_channel_DEPENDENCIES)
+ @rm -f test-packet-channel$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_packet_channel_OBJECTS) $(test_packet_channel_LDADD) $(LIBS)
+src/bridge/test_paths-test-paths.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-paths$(EXEEXT): $(test_paths_OBJECTS) $(test_paths_DEPENDENCIES) $(EXTRA_test_paths_DEPENDENCIES)
+ @rm -f test-paths$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_paths_OBJECTS) $(test_paths_LDADD) $(LIBS)
+src/bridge/test_pcp-test-pcp.$(OBJEXT): src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-pcp$(EXEEXT): $(test_pcp_OBJECTS) $(test_pcp_DEPENDENCIES) $(EXTRA_test_pcp_DEPENDENCIES)
+ @rm -f test-pcp$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_pcp_OBJECTS) $(test_pcp_LDADD) $(LIBS)
+src/bridge/test_pcp_archives-test-pcp-archives.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-pcp-archives$(EXEEXT): $(test_pcp_archives_OBJECTS) $(test_pcp_archives_DEPENDENCIES) $(EXTRA_test_pcp_archives_DEPENDENCIES)
+ @rm -f test-pcp-archives$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_pcp_archives_OBJECTS) $(test_pcp_archives_LDADD) $(LIBS)
+src/bridge/test_peer-test-peer.$(OBJEXT): src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-peer$(EXEEXT): $(test_peer_OBJECTS) $(test_peer_DEPENDENCIES) $(EXTRA_test_peer_DEPENDENCIES)
+ @rm -f test-peer$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_peer_OBJECTS) $(test_peer_LDADD) $(LIBS)
+src/common/test_pipe-test-pipe.$(OBJEXT): src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-pipe$(EXEEXT): $(test_pipe_OBJECTS) $(test_pipe_DEPENDENCIES) $(EXTRA_test_pipe_DEPENDENCIES)
+ @rm -f test-pipe$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_pipe_OBJECTS) $(test_pipe_LDADD) $(LIBS)
+src/bridge/test_pipe_channel-test-pipe-channel.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-pipe-channel$(EXEEXT): $(test_pipe_channel_OBJECTS) $(test_pipe_channel_DEPENDENCIES) $(EXTRA_test_pipe_channel_DEPENDENCIES)
+ @rm -f test-pipe-channel$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_pipe_channel_OBJECTS) $(test_pipe_channel_LDADD) $(LIBS)
+src/bridge/test_process-test-process.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-process$(EXEEXT): $(test_process_OBJECTS) $(test_process_DEPENDENCIES) $(EXTRA_test_process_DEPENDENCIES)
+ @rm -f test-process$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_process_OBJECTS) $(test_process_LDADD) $(LIBS)
+src/bridge/test_router-test-router.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-router$(EXEEXT): $(test_router_OBJECTS) $(test_router_DEPENDENCIES) $(EXTRA_test_router_DEPENDENCIES)
+ @rm -f test-router$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_router_OBJECTS) $(test_router_LDADD) $(LIBS)
+src/bridge/test_rules-test-rules.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-rules$(EXEEXT): $(test_rules_OBJECTS) $(test_rules_DEPENDENCIES) $(EXTRA_test_rules_DEPENDENCIES)
+ @rm -f test-rules$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_rules_OBJECTS) $(test_rules_LDADD) $(LIBS)
+src/ws/test_server-mock-service.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+src/ws/test_server-test-server.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+src/ws/test_server-mock-dbus-tests.$(OBJEXT): src/ws/$(am__dirstamp) \
+ src/ws/$(DEPDIR)/$(am__dirstamp)
+
+test-server$(EXEEXT): $(test_server_OBJECTS) $(test_server_DEPENDENCIES) $(EXTRA_test_server_DEPENDENCIES)
+ @rm -f test-server$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_server_OBJECTS) $(test_server_LDADD) $(LIBS)
+src/pam-ssh-add/test_ssh_add-test-ssh-add.$(OBJEXT): \
+ src/pam-ssh-add/$(am__dirstamp) \
+ src/pam-ssh-add/$(DEPDIR)/$(am__dirstamp)
+
+test-ssh-add$(EXEEXT): $(test_ssh_add_OBJECTS) $(test_ssh_add_DEPENDENCIES) $(EXTRA_test_ssh_add_DEPENDENCIES)
+ @rm -f test-ssh-add$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_ssh_add_OBJECTS) $(test_ssh_add_LDADD) $(LIBS)
+src/ssh/test_sshbridge-test-sshbridge.$(OBJEXT): \
+ src/ssh/$(am__dirstamp) src/ssh/$(DEPDIR)/$(am__dirstamp)
+
+test-sshbridge$(EXEEXT): $(test_sshbridge_OBJECTS) $(test_sshbridge_DEPENDENCIES) $(EXTRA_test_sshbridge_DEPENDENCIES)
+ @rm -f test-sshbridge$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_sshbridge_OBJECTS) $(test_sshbridge_LDADD) $(LIBS)
+src/ssh/test_sshoptions-test-sshoptions.$(OBJEXT): \
+ src/ssh/$(am__dirstamp) src/ssh/$(DEPDIR)/$(am__dirstamp)
+
+test-sshoptions$(EXEEXT): $(test_sshoptions_OBJECTS) $(test_sshoptions_DEPENDENCIES) $(EXTRA_test_sshoptions_DEPENDENCIES)
+ @rm -f test-sshoptions$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_sshoptions_OBJECTS) $(test_sshoptions_LDADD) $(LIBS)
+src/bridge/test_stream-test-stream.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-stream$(EXEEXT): $(test_stream_OBJECTS) $(test_stream_DEPENDENCIES) $(EXTRA_test_stream_DEPENDENCIES)
+ @rm -f test-stream$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_stream_OBJECTS) $(test_stream_LDADD) $(LIBS)
+src/common/test_system-test-system.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-system$(EXEEXT): $(test_system_OBJECTS) $(test_system_DEPENDENCIES) $(EXTRA_test_system_DEPENDENCIES)
+ @rm -f test-system$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_system_OBJECTS) $(test_system_LDADD) $(LIBS)
+src/common/test_template-test-template.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-template$(EXEEXT): $(test_template_OBJECTS) $(test_template_DEPENDENCIES) $(EXTRA_test_template_DEPENDENCIES)
+ @rm -f test-template$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_template_OBJECTS) $(test_template_LDADD) $(LIBS)
+src/tls/test_tls_connection-test-connection.$(OBJEXT): \
+ src/tls/$(am__dirstamp) src/tls/$(DEPDIR)/$(am__dirstamp)
+
+test-tls-connection$(EXEEXT): $(test_tls_connection_OBJECTS) $(test_tls_connection_DEPENDENCIES) $(EXTRA_test_tls_connection_DEPENDENCIES)
+ @rm -f test-tls-connection$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_tls_connection_OBJECTS) $(test_tls_connection_LDADD) $(LIBS)
+src/tls/test_tls_server-test-server.$(OBJEXT): \
+ src/tls/$(am__dirstamp) src/tls/$(DEPDIR)/$(am__dirstamp)
+
+test-tls-server$(EXEEXT): $(test_tls_server_OBJECTS) $(test_tls_server_DEPENDENCIES) $(EXTRA_test_tls_server_DEPENDENCIES)
+ @rm -f test-tls-server$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_tls_server_OBJECTS) $(test_tls_server_LDADD) $(LIBS)
+src/common/test_transport-test-transport.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-transport$(EXEEXT): $(test_transport_OBJECTS) $(test_transport_DEPENDENCIES) $(EXTRA_test_transport_DEPENDENCIES)
+ @rm -f test-transport$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_transport_OBJECTS) $(test_transport_LDADD) $(LIBS)
+src/common/test_unicode-test-unicode.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-unicode$(EXEEXT): $(test_unicode_OBJECTS) $(test_unicode_DEPENDENCIES) $(EXTRA_test_unicode_DEPENDENCIES)
+ @rm -f test-unicode$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_unicode_OBJECTS) $(test_unicode_LDADD) $(LIBS)
+src/common/test_unixsignal-test-unixsignal.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-unixsignal$(EXEEXT): $(test_unixsignal_OBJECTS) $(test_unixsignal_DEPENDENCIES) $(EXTRA_test_unixsignal_DEPENDENCIES)
+ @rm -f test-unixsignal$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_unixsignal_OBJECTS) $(test_unixsignal_LDADD) $(LIBS)
+src/common/test_version-test-version.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-version$(EXEEXT): $(test_version_OBJECTS) $(test_version_DEPENDENCIES) $(EXTRA_test_version_DEPENDENCIES)
+ @rm -f test-version$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_version_OBJECTS) $(test_version_LDADD) $(LIBS)
+src/common/test_webcertificate-test-webcertificate.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-webcertificate$(EXEEXT): $(test_webcertificate_OBJECTS) $(test_webcertificate_DEPENDENCIES) $(EXTRA_test_webcertificate_DEPENDENCIES)
+ @rm -f test-webcertificate$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_webcertificate_OBJECTS) $(test_webcertificate_LDADD) $(LIBS)
+src/common/test_webresponse-test-webresponse.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-webresponse$(EXEEXT): $(test_webresponse_OBJECTS) $(test_webresponse_DEPENDENCIES) $(EXTRA_test_webresponse_DEPENDENCIES)
+ @rm -f test-webresponse$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_webresponse_OBJECTS) $(test_webresponse_LDADD) $(LIBS)
+src/common/test_webserver-test-webserver.$(OBJEXT): \
+ src/common/$(am__dirstamp) \
+ src/common/$(DEPDIR)/$(am__dirstamp)
+
+test-webserver$(EXEEXT): $(test_webserver_OBJECTS) $(test_webserver_DEPENDENCIES) $(EXTRA_test_webserver_DEPENDENCIES)
+ @rm -f test-webserver$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_webserver_OBJECTS) $(test_webserver_LDADD) $(LIBS)
+src/ws/test_webservice-test-webservice.$(OBJEXT): \
+ src/ws/$(am__dirstamp) src/ws/$(DEPDIR)/$(am__dirstamp)
+
+test-webservice$(EXEEXT): $(test_webservice_OBJECTS) $(test_webservice_DEPENDENCIES) $(EXTRA_test_webservice_DEPENDENCIES)
+ @rm -f test-webservice$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_webservice_OBJECTS) $(test_webservice_LDADD) $(LIBS)
+src/websocket/test_websocket-test-websocket.$(OBJEXT): \
+ src/websocket/$(am__dirstamp) \
+ src/websocket/$(DEPDIR)/$(am__dirstamp)
+
+test-websocket$(EXEEXT): $(test_websocket_OBJECTS) $(test_websocket_DEPENDENCIES) $(EXTRA_test_websocket_DEPENDENCIES)
+ @rm -f test-websocket$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_websocket_OBJECTS) $(test_websocket_LDADD) $(LIBS)
+src/bridge/test_websocketstream-test-websocketstream.$(OBJEXT): \
+ src/bridge/$(am__dirstamp) \
+ src/bridge/$(DEPDIR)/$(am__dirstamp)
+
+test-websocketstream$(EXEEXT): $(test_websocketstream_OBJECTS) $(test_websocketstream_DEPENDENCIES) $(EXTRA_test_websocketstream_DEPENDENCIES)
+ @rm -f test-websocketstream$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_websocketstream_OBJECTS) $(test_websocketstream_LDADD) $(LIBS)
+src/tls/wsinstance-start.$(OBJEXT): src/tls/$(am__dirstamp) \
+ src/tls/$(DEPDIR)/$(am__dirstamp)
+
+wsinstance-start$(EXEEXT): $(wsinstance_start_OBJECTS) $(wsinstance_start_DEPENDENCIES) $(EXTRA_wsinstance_start_DEPENDENCIES)
+ @rm -f wsinstance-start$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(wsinstance_start_OBJECTS) $(wsinstance_start_LDADD) $(LIBS)
+install-dist_cockpitclientSCRIPTS: $(dist_cockpitclient_SCRIPTS)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_cockpitclient_SCRIPTS)'; test -n "$(cockpitclientdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(cockpitclientdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(cockpitclientdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n' \
+ -e 'h;s|.*|.|' \
+ -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) { files[d] = files[d] " " $$1; \
+ if (++n[d] == $(am__install_max)) { \
+ print "f", d, files[d]; n[d] = 0; files[d] = "" } } \
+ else { print "f", d "/" $$4, $$1 } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(cockpitclientdir)$$dir'"; \
+ $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(cockpitclientdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-dist_cockpitclientSCRIPTS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_cockpitclient_SCRIPTS)'; test -n "$(cockpitclientdir)" || exit 0; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 's,.*/,,;$(transform)'`; \
+ dir='$(DESTDIR)$(cockpitclientdir)'; $(am__uninstall_files_from_dir)
+install-dist_motdSCRIPTS: $(dist_motd_SCRIPTS)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_motd_SCRIPTS)'; test -n "$(motddir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(motddir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(motddir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n' \
+ -e 'h;s|.*|.|' \
+ -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) { files[d] = files[d] " " $$1; \
+ if (++n[d] == $(am__install_max)) { \
+ print "f", d, files[d]; n[d] = 0; files[d] = "" } } \
+ else { print "f", d "/" $$4, $$1 } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(motddir)$$dir'"; \
+ $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(motddir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-dist_motdSCRIPTS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_motd_SCRIPTS)'; test -n "$(motddir)" || exit 0; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 's,.*/,,;$(transform)'`; \
+ dir='$(DESTDIR)$(motddir)'; $(am__uninstall_files_from_dir)
+install-libexecSCRIPTS: $(libexec_SCRIPTS)
+ @$(NORMAL_INSTALL)
+ @list='$(libexec_SCRIPTS)'; test -n "$(libexecdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(libexecdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(libexecdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \
+ done | \
+ sed -e 'p;s,.*/,,;n' \
+ -e 'h;s|.*|.|' \
+ -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \
+ $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \
+ { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
+ if ($$2 == $$4) { files[d] = files[d] " " $$1; \
+ if (++n[d] == $(am__install_max)) { \
+ print "f", d, files[d]; n[d] = 0; files[d] = "" } } \
+ else { print "f", d "/" $$4, $$1 } } \
+ END { for (d in files) print "f", d, files[d] }' | \
+ while read type dir files; do \
+ if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
+ test -z "$$files" || { \
+ echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(libexecdir)$$dir'"; \
+ $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(libexecdir)$$dir" || exit $$?; \
+ } \
+ ; done
+
+uninstall-libexecSCRIPTS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(libexec_SCRIPTS)'; test -n "$(libexecdir)" || exit 0; \
+ files=`for p in $$list; do echo "$$p"; done | \
+ sed -e 's,.*/,,;$(transform)'`; \
+ dir='$(DESTDIR)$(libexecdir)'; $(am__uninstall_files_from_dir)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+ -rm -f src/bridge/*.$(OBJEXT)
+ -rm -f src/common/*.$(OBJEXT)
+ -rm -f src/pam-ssh-add/*.$(OBJEXT)
+ -rm -f src/session/*.$(OBJEXT)
+ -rm -f src/ssh/*.$(OBJEXT)
+ -rm -f src/testlib/*.$(OBJEXT)
+ -rm -f src/tls/*.$(OBJEXT)
+ -rm -f src/websocket/*.$(OBJEXT)
+ -rm -f src/ws/*.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/cockpit_askpass-askpass.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/cockpit_bridge-bridge.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/cockpit_pcp-cockpitpcp.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitconnect.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbuscache.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusconfig.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusinternal.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusjson.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusloginmessages.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmachines.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmeta.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusprocess.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusrules.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbususer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitechochannel.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfslist.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsread.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsreplace.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfswatch.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpithttpstream.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitinteracttransport.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitnullchannel.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpackages.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpacketchannel.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpaths.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpeer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpipechannel.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpolkitagent.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitrouter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitstream.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitwebsocketstream.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitblocksamples.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcgroupsamples.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcpusamples.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitdisksamples.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitinternalmetrics.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmemorysamples.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmetrics.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmountsamples.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitnetworksamples.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitsamples.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitconnect.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitdbusinternal.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpcpmetrics.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpeer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitrouter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/mock_bridge-mock-bridge.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/mock_pmda_so-mock-pmda.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_bridge-test-bridge.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_connect-test-connect.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_dbus_meta-test-dbus-meta.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_fs-test-fs.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_httpstream-test-httpstream.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_metrics-test-metrics.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_packages-test-packages.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_packet_channel-test-packet-channel.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_paths-test-paths.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_pcp-test-pcp.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_pcp_archives-test-pcp-archives.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_peer-test-peer.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_pipe_channel-test-pipe-channel.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_process-test-process.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_router-test-router.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_rules-test-rules.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_stream-test-stream.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/bridge/$(DEPDIR)/test_websocketstream-test-websocketstream.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/cockpitauthorize.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/cockpitbase64.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/cockpitclosefrom.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/cockpitconf.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/cockpitfdpassing.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/cockpitframe.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/cockpithex.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/cockpitjsonprint.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/cockpitmemory.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/cockpitwebcertificate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitchannel.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitclosefrom.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitcontrolmessages.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpiterror.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitflow.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpithash.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitjson.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitlocale.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitloopback.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitmachinesjson.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitmemfdread.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipe.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipetransport.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitsocket.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitsystem.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpittemplate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpittransport.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitunicode.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitunixsignal.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitversion.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebfilter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebinject.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebresponse.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebserver.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libcockpit_common_a-fail.html.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/libpreload_temp_home_so-preload-temp-home.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_authorize-test-authorize.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_base64-test-base64.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_channel-test-channel.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_config-test-config.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_frame-test-frame.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_hash-test-hash.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_hex-test-hex.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_json-test-json.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_jsonfds-test-jsonfds.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_locale-test-locale.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_pipe-test-pipe.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_system-test-system.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_template-test-template.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_transport-test-transport.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_unicode-test-unicode.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_unixsignal-test-unixsignal.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_version-test-version.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_webcertificate-test-webcertificate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_webresponse-test-webresponse.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/common/$(DEPDIR)/test_webserver-test-webserver.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/pam-ssh-add/$(DEPDIR)/libpam_ssh_add_a-pam-ssh-add.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/pam-ssh-add/$(DEPDIR)/pam_ssh_add_so-pam-ssh-add.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/pam-ssh-add/$(DEPDIR)/test_ssh_add-test-ssh-add.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/session/$(DEPDIR)/client-certificate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/session/$(DEPDIR)/session-utils.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/session/$(DEPDIR)/session.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ssh/$(DEPDIR)/cockpit_ssh-ssh.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshoptions.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshrelay.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ssh/$(DEPDIR)/mock_sshd-mock-sshd.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ssh/$(DEPDIR)/test_sshbridge-test-sshbridge.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ssh/$(DEPDIR)/test_sshoptions-test-sshoptions.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/testlib/$(DEPDIR)/libcockpit_test_a-cockpittest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/testlib/$(DEPDIR)/libcockpit_test_a-mock-auth.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/testlib/$(DEPDIR)/libcockpit_test_a-mock-channel.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/testlib/$(DEPDIR)/libcockpit_test_a-mock-pressure.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/testlib/$(DEPDIR)/libcockpit_test_a-mock-transport.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/testlib/$(DEPDIR)/libcockpit_test_a-retest.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/certificate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/client-certificate.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/cockpit-certificate-ensure.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/cockpit_wsinstance_factory-wsinstance-factory.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/httpredirect.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/server.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/socket-activation-helper.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/socket-io.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/test_tls_connection-test-connection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/test_tls_server-test-server.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/tls/$(DEPDIR)/wsinstance-start.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/websocket/$(DEPDIR)/frob_websocket-frob-websocket.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/websocket/$(DEPDIR)/libwebsocket_a-websocket.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/websocket/$(DEPDIR)/libwebsocket_a-websocketclient.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/websocket/$(DEPDIR)/libwebsocket_a-websocketconnection.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/websocket/$(DEPDIR)/libwebsocket_a-websocketserver.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/websocket/$(DEPDIR)/test_websocket-test-websocket.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/cockpit_ws-main.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitauth.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitbranding.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelresponse.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelsocket.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcompat.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcreds.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/libcockpit_ws_a-cockpithandlers.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitwebservice.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/mock_auth_command-mock-auth-command.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/mock_echo-mock-echo.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/mock_pam_conv_mod_so-mock-pam-conv-mod.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/pam_cockpit_cert_so-pam_cockpit_cert.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_auth-test-auth.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_authssh-test-authssh.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_channelresponse-test-channelresponse.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_compat-test-compat.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_creds-test-creds.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_handlers-test-handlers.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_kerberos-test-kerberos.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_server-mock-dbus-tests.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_server-mock-service.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_server-test-server.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@src/ws/$(DEPDIR)/test_webservice-test-webservice.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)depbase=`echo $@ | sed 's|[^/]*$$|$(DEPDIR)/&|;s|\.o$$||'`;\
+@am__fastdepCC_TRUE@ $(COMPILE) -MT $@ -MD -MP -MF $$depbase.Tpo -c -o $@ $< &&\
+@am__fastdepCC_TRUE@ $(am__mv) $$depbase.Tpo $$depbase.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)depbase=`echo $@ | sed 's|[^/]*$$|$(DEPDIR)/&|;s|\.obj$$||'`;\
+@am__fastdepCC_TRUE@ $(COMPILE) -MT $@ -MD -MP -MF $$depbase.Tpo -c -o $@ `$(CYGPATH_W) '$<'` &&\
+@am__fastdepCC_TRUE@ $(am__mv) $$depbase.Tpo $$depbase.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+src/bridge/libcockpit_bridge_a-cockpitconnect.o: src/bridge/cockpitconnect.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitconnect.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitconnect.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitconnect.o `test -f 'src/bridge/cockpitconnect.c' || echo '$(srcdir)/'`src/bridge/cockpitconnect.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitconnect.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitconnect.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitconnect.c' object='src/bridge/libcockpit_bridge_a-cockpitconnect.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitconnect.o `test -f 'src/bridge/cockpitconnect.c' || echo '$(srcdir)/'`src/bridge/cockpitconnect.c
+
+src/bridge/libcockpit_bridge_a-cockpitconnect.obj: src/bridge/cockpitconnect.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitconnect.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitconnect.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitconnect.obj `if test -f 'src/bridge/cockpitconnect.c'; then $(CYGPATH_W) 'src/bridge/cockpitconnect.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitconnect.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitconnect.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitconnect.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitconnect.c' object='src/bridge/libcockpit_bridge_a-cockpitconnect.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitconnect.obj `if test -f 'src/bridge/cockpitconnect.c'; then $(CYGPATH_W) 'src/bridge/cockpitconnect.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitconnect.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitdbuscache.o: src/bridge/cockpitdbuscache.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbuscache.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbuscache.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbuscache.o `test -f 'src/bridge/cockpitdbuscache.c' || echo '$(srcdir)/'`src/bridge/cockpitdbuscache.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbuscache.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbuscache.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbuscache.c' object='src/bridge/libcockpit_bridge_a-cockpitdbuscache.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbuscache.o `test -f 'src/bridge/cockpitdbuscache.c' || echo '$(srcdir)/'`src/bridge/cockpitdbuscache.c
+
+src/bridge/libcockpit_bridge_a-cockpitdbuscache.obj: src/bridge/cockpitdbuscache.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbuscache.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbuscache.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbuscache.obj `if test -f 'src/bridge/cockpitdbuscache.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbuscache.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbuscache.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbuscache.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbuscache.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbuscache.c' object='src/bridge/libcockpit_bridge_a-cockpitdbuscache.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbuscache.obj `if test -f 'src/bridge/cockpitdbuscache.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbuscache.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbuscache.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitdbusconfig.o: src/bridge/cockpitdbusconfig.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusconfig.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusconfig.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusconfig.o `test -f 'src/bridge/cockpitdbusconfig.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusconfig.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusconfig.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusconfig.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusconfig.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusconfig.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusconfig.o `test -f 'src/bridge/cockpitdbusconfig.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusconfig.c
+
+src/bridge/libcockpit_bridge_a-cockpitdbusconfig.obj: src/bridge/cockpitdbusconfig.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusconfig.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusconfig.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusconfig.obj `if test -f 'src/bridge/cockpitdbusconfig.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusconfig.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusconfig.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusconfig.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusconfig.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusconfig.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusconfig.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusconfig.obj `if test -f 'src/bridge/cockpitdbusconfig.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusconfig.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusconfig.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitdbusinternal.o: src/bridge/cockpitdbusinternal.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusinternal.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusinternal.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusinternal.o `test -f 'src/bridge/cockpitdbusinternal.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusinternal.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusinternal.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusinternal.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusinternal.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusinternal.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusinternal.o `test -f 'src/bridge/cockpitdbusinternal.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusinternal.c
+
+src/bridge/libcockpit_bridge_a-cockpitdbusinternal.obj: src/bridge/cockpitdbusinternal.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusinternal.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusinternal.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusinternal.obj `if test -f 'src/bridge/cockpitdbusinternal.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusinternal.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusinternal.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusinternal.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusinternal.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusinternal.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusinternal.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusinternal.obj `if test -f 'src/bridge/cockpitdbusinternal.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusinternal.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusinternal.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitdbusjson.o: src/bridge/cockpitdbusjson.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusjson.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusjson.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusjson.o `test -f 'src/bridge/cockpitdbusjson.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusjson.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusjson.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusjson.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusjson.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusjson.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusjson.o `test -f 'src/bridge/cockpitdbusjson.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusjson.c
+
+src/bridge/libcockpit_bridge_a-cockpitdbusjson.obj: src/bridge/cockpitdbusjson.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusjson.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusjson.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusjson.obj `if test -f 'src/bridge/cockpitdbusjson.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusjson.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusjson.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusjson.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusjson.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusjson.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusjson.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusjson.obj `if test -f 'src/bridge/cockpitdbusjson.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusjson.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusjson.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.o: src/bridge/cockpitdbusloginmessages.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusloginmessages.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.o `test -f 'src/bridge/cockpitdbusloginmessages.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusloginmessages.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusloginmessages.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusloginmessages.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusloginmessages.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.o `test -f 'src/bridge/cockpitdbusloginmessages.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusloginmessages.c
+
+src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.obj: src/bridge/cockpitdbusloginmessages.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusloginmessages.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.obj `if test -f 'src/bridge/cockpitdbusloginmessages.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusloginmessages.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusloginmessages.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusloginmessages.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusloginmessages.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusloginmessages.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusloginmessages.obj `if test -f 'src/bridge/cockpitdbusloginmessages.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusloginmessages.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusloginmessages.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitdbusmachines.o: src/bridge/cockpitdbusmachines.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusmachines.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmachines.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusmachines.o `test -f 'src/bridge/cockpitdbusmachines.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusmachines.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmachines.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmachines.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusmachines.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusmachines.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusmachines.o `test -f 'src/bridge/cockpitdbusmachines.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusmachines.c
+
+src/bridge/libcockpit_bridge_a-cockpitdbusmachines.obj: src/bridge/cockpitdbusmachines.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusmachines.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmachines.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusmachines.obj `if test -f 'src/bridge/cockpitdbusmachines.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusmachines.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusmachines.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmachines.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmachines.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusmachines.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusmachines.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusmachines.obj `if test -f 'src/bridge/cockpitdbusmachines.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusmachines.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusmachines.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitdbusmeta.o: src/bridge/cockpitdbusmeta.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusmeta.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmeta.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusmeta.o `test -f 'src/bridge/cockpitdbusmeta.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusmeta.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmeta.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmeta.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusmeta.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusmeta.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusmeta.o `test -f 'src/bridge/cockpitdbusmeta.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusmeta.c
+
+src/bridge/libcockpit_bridge_a-cockpitdbusmeta.obj: src/bridge/cockpitdbusmeta.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusmeta.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmeta.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusmeta.obj `if test -f 'src/bridge/cockpitdbusmeta.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusmeta.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusmeta.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmeta.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmeta.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusmeta.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusmeta.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusmeta.obj `if test -f 'src/bridge/cockpitdbusmeta.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusmeta.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusmeta.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitdbusprocess.o: src/bridge/cockpitdbusprocess.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusprocess.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusprocess.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusprocess.o `test -f 'src/bridge/cockpitdbusprocess.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusprocess.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusprocess.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusprocess.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusprocess.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusprocess.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusprocess.o `test -f 'src/bridge/cockpitdbusprocess.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusprocess.c
+
+src/bridge/libcockpit_bridge_a-cockpitdbusprocess.obj: src/bridge/cockpitdbusprocess.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusprocess.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusprocess.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusprocess.obj `if test -f 'src/bridge/cockpitdbusprocess.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusprocess.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusprocess.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusprocess.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusprocess.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusprocess.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusprocess.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusprocess.obj `if test -f 'src/bridge/cockpitdbusprocess.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusprocess.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusprocess.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitdbusrules.o: src/bridge/cockpitdbusrules.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusrules.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusrules.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusrules.o `test -f 'src/bridge/cockpitdbusrules.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusrules.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusrules.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusrules.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusrules.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusrules.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusrules.o `test -f 'src/bridge/cockpitdbusrules.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusrules.c
+
+src/bridge/libcockpit_bridge_a-cockpitdbusrules.obj: src/bridge/cockpitdbusrules.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbusrules.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusrules.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbusrules.obj `if test -f 'src/bridge/cockpitdbusrules.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusrules.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusrules.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusrules.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusrules.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusrules.c' object='src/bridge/libcockpit_bridge_a-cockpitdbusrules.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbusrules.obj `if test -f 'src/bridge/cockpitdbusrules.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusrules.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusrules.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitdbususer.o: src/bridge/cockpitdbususer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbususer.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbususer.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbususer.o `test -f 'src/bridge/cockpitdbususer.c' || echo '$(srcdir)/'`src/bridge/cockpitdbususer.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbususer.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbususer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbususer.c' object='src/bridge/libcockpit_bridge_a-cockpitdbususer.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbususer.o `test -f 'src/bridge/cockpitdbususer.c' || echo '$(srcdir)/'`src/bridge/cockpitdbususer.c
+
+src/bridge/libcockpit_bridge_a-cockpitdbususer.obj: src/bridge/cockpitdbususer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitdbususer.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbususer.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitdbususer.obj `if test -f 'src/bridge/cockpitdbususer.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbususer.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbususer.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbususer.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbususer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbususer.c' object='src/bridge/libcockpit_bridge_a-cockpitdbususer.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitdbususer.obj `if test -f 'src/bridge/cockpitdbususer.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbususer.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbususer.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitechochannel.o: src/bridge/cockpitechochannel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitechochannel.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitechochannel.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitechochannel.o `test -f 'src/bridge/cockpitechochannel.c' || echo '$(srcdir)/'`src/bridge/cockpitechochannel.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitechochannel.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitechochannel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitechochannel.c' object='src/bridge/libcockpit_bridge_a-cockpitechochannel.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitechochannel.o `test -f 'src/bridge/cockpitechochannel.c' || echo '$(srcdir)/'`src/bridge/cockpitechochannel.c
+
+src/bridge/libcockpit_bridge_a-cockpitechochannel.obj: src/bridge/cockpitechochannel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitechochannel.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitechochannel.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitechochannel.obj `if test -f 'src/bridge/cockpitechochannel.c'; then $(CYGPATH_W) 'src/bridge/cockpitechochannel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitechochannel.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitechochannel.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitechochannel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitechochannel.c' object='src/bridge/libcockpit_bridge_a-cockpitechochannel.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitechochannel.obj `if test -f 'src/bridge/cockpitechochannel.c'; then $(CYGPATH_W) 'src/bridge/cockpitechochannel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitechochannel.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitfslist.o: src/bridge/cockpitfslist.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitfslist.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfslist.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitfslist.o `test -f 'src/bridge/cockpitfslist.c' || echo '$(srcdir)/'`src/bridge/cockpitfslist.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfslist.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfslist.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitfslist.c' object='src/bridge/libcockpit_bridge_a-cockpitfslist.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitfslist.o `test -f 'src/bridge/cockpitfslist.c' || echo '$(srcdir)/'`src/bridge/cockpitfslist.c
+
+src/bridge/libcockpit_bridge_a-cockpitfslist.obj: src/bridge/cockpitfslist.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitfslist.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfslist.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitfslist.obj `if test -f 'src/bridge/cockpitfslist.c'; then $(CYGPATH_W) 'src/bridge/cockpitfslist.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitfslist.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfslist.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfslist.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitfslist.c' object='src/bridge/libcockpit_bridge_a-cockpitfslist.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitfslist.obj `if test -f 'src/bridge/cockpitfslist.c'; then $(CYGPATH_W) 'src/bridge/cockpitfslist.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitfslist.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitfsread.o: src/bridge/cockpitfsread.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitfsread.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsread.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitfsread.o `test -f 'src/bridge/cockpitfsread.c' || echo '$(srcdir)/'`src/bridge/cockpitfsread.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsread.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsread.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitfsread.c' object='src/bridge/libcockpit_bridge_a-cockpitfsread.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitfsread.o `test -f 'src/bridge/cockpitfsread.c' || echo '$(srcdir)/'`src/bridge/cockpitfsread.c
+
+src/bridge/libcockpit_bridge_a-cockpitfsread.obj: src/bridge/cockpitfsread.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitfsread.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsread.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitfsread.obj `if test -f 'src/bridge/cockpitfsread.c'; then $(CYGPATH_W) 'src/bridge/cockpitfsread.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitfsread.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsread.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsread.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitfsread.c' object='src/bridge/libcockpit_bridge_a-cockpitfsread.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitfsread.obj `if test -f 'src/bridge/cockpitfsread.c'; then $(CYGPATH_W) 'src/bridge/cockpitfsread.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitfsread.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitfsreplace.o: src/bridge/cockpitfsreplace.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitfsreplace.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsreplace.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitfsreplace.o `test -f 'src/bridge/cockpitfsreplace.c' || echo '$(srcdir)/'`src/bridge/cockpitfsreplace.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsreplace.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsreplace.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitfsreplace.c' object='src/bridge/libcockpit_bridge_a-cockpitfsreplace.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitfsreplace.o `test -f 'src/bridge/cockpitfsreplace.c' || echo '$(srcdir)/'`src/bridge/cockpitfsreplace.c
+
+src/bridge/libcockpit_bridge_a-cockpitfsreplace.obj: src/bridge/cockpitfsreplace.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitfsreplace.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsreplace.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitfsreplace.obj `if test -f 'src/bridge/cockpitfsreplace.c'; then $(CYGPATH_W) 'src/bridge/cockpitfsreplace.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitfsreplace.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsreplace.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsreplace.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitfsreplace.c' object='src/bridge/libcockpit_bridge_a-cockpitfsreplace.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitfsreplace.obj `if test -f 'src/bridge/cockpitfsreplace.c'; then $(CYGPATH_W) 'src/bridge/cockpitfsreplace.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitfsreplace.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitfswatch.o: src/bridge/cockpitfswatch.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitfswatch.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfswatch.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitfswatch.o `test -f 'src/bridge/cockpitfswatch.c' || echo '$(srcdir)/'`src/bridge/cockpitfswatch.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfswatch.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfswatch.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitfswatch.c' object='src/bridge/libcockpit_bridge_a-cockpitfswatch.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitfswatch.o `test -f 'src/bridge/cockpitfswatch.c' || echo '$(srcdir)/'`src/bridge/cockpitfswatch.c
+
+src/bridge/libcockpit_bridge_a-cockpitfswatch.obj: src/bridge/cockpitfswatch.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitfswatch.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfswatch.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitfswatch.obj `if test -f 'src/bridge/cockpitfswatch.c'; then $(CYGPATH_W) 'src/bridge/cockpitfswatch.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitfswatch.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfswatch.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfswatch.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitfswatch.c' object='src/bridge/libcockpit_bridge_a-cockpitfswatch.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitfswatch.obj `if test -f 'src/bridge/cockpitfswatch.c'; then $(CYGPATH_W) 'src/bridge/cockpitfswatch.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitfswatch.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpithttpstream.o: src/bridge/cockpithttpstream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpithttpstream.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpithttpstream.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpithttpstream.o `test -f 'src/bridge/cockpithttpstream.c' || echo '$(srcdir)/'`src/bridge/cockpithttpstream.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpithttpstream.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpithttpstream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpithttpstream.c' object='src/bridge/libcockpit_bridge_a-cockpithttpstream.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpithttpstream.o `test -f 'src/bridge/cockpithttpstream.c' || echo '$(srcdir)/'`src/bridge/cockpithttpstream.c
+
+src/bridge/libcockpit_bridge_a-cockpithttpstream.obj: src/bridge/cockpithttpstream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpithttpstream.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpithttpstream.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpithttpstream.obj `if test -f 'src/bridge/cockpithttpstream.c'; then $(CYGPATH_W) 'src/bridge/cockpithttpstream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpithttpstream.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpithttpstream.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpithttpstream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpithttpstream.c' object='src/bridge/libcockpit_bridge_a-cockpithttpstream.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpithttpstream.obj `if test -f 'src/bridge/cockpithttpstream.c'; then $(CYGPATH_W) 'src/bridge/cockpithttpstream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpithttpstream.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitinteracttransport.o: src/bridge/cockpitinteracttransport.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitinteracttransport.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitinteracttransport.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitinteracttransport.o `test -f 'src/bridge/cockpitinteracttransport.c' || echo '$(srcdir)/'`src/bridge/cockpitinteracttransport.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitinteracttransport.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitinteracttransport.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitinteracttransport.c' object='src/bridge/libcockpit_bridge_a-cockpitinteracttransport.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitinteracttransport.o `test -f 'src/bridge/cockpitinteracttransport.c' || echo '$(srcdir)/'`src/bridge/cockpitinteracttransport.c
+
+src/bridge/libcockpit_bridge_a-cockpitinteracttransport.obj: src/bridge/cockpitinteracttransport.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitinteracttransport.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitinteracttransport.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitinteracttransport.obj `if test -f 'src/bridge/cockpitinteracttransport.c'; then $(CYGPATH_W) 'src/bridge/cockpitinteracttransport.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitinteracttransport.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitinteracttransport.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitinteracttransport.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitinteracttransport.c' object='src/bridge/libcockpit_bridge_a-cockpitinteracttransport.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitinteracttransport.obj `if test -f 'src/bridge/cockpitinteracttransport.c'; then $(CYGPATH_W) 'src/bridge/cockpitinteracttransport.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitinteracttransport.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitnullchannel.o: src/bridge/cockpitnullchannel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitnullchannel.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitnullchannel.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitnullchannel.o `test -f 'src/bridge/cockpitnullchannel.c' || echo '$(srcdir)/'`src/bridge/cockpitnullchannel.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitnullchannel.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitnullchannel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitnullchannel.c' object='src/bridge/libcockpit_bridge_a-cockpitnullchannel.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitnullchannel.o `test -f 'src/bridge/cockpitnullchannel.c' || echo '$(srcdir)/'`src/bridge/cockpitnullchannel.c
+
+src/bridge/libcockpit_bridge_a-cockpitnullchannel.obj: src/bridge/cockpitnullchannel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitnullchannel.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitnullchannel.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitnullchannel.obj `if test -f 'src/bridge/cockpitnullchannel.c'; then $(CYGPATH_W) 'src/bridge/cockpitnullchannel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitnullchannel.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitnullchannel.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitnullchannel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitnullchannel.c' object='src/bridge/libcockpit_bridge_a-cockpitnullchannel.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitnullchannel.obj `if test -f 'src/bridge/cockpitnullchannel.c'; then $(CYGPATH_W) 'src/bridge/cockpitnullchannel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitnullchannel.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitpackages.o: src/bridge/cockpitpackages.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpackages.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpackages.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpackages.o `test -f 'src/bridge/cockpitpackages.c' || echo '$(srcdir)/'`src/bridge/cockpitpackages.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpackages.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpackages.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpackages.c' object='src/bridge/libcockpit_bridge_a-cockpitpackages.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpackages.o `test -f 'src/bridge/cockpitpackages.c' || echo '$(srcdir)/'`src/bridge/cockpitpackages.c
+
+src/bridge/libcockpit_bridge_a-cockpitpackages.obj: src/bridge/cockpitpackages.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpackages.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpackages.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpackages.obj `if test -f 'src/bridge/cockpitpackages.c'; then $(CYGPATH_W) 'src/bridge/cockpitpackages.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpackages.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpackages.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpackages.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpackages.c' object='src/bridge/libcockpit_bridge_a-cockpitpackages.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpackages.obj `if test -f 'src/bridge/cockpitpackages.c'; then $(CYGPATH_W) 'src/bridge/cockpitpackages.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpackages.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitpacketchannel.o: src/bridge/cockpitpacketchannel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpacketchannel.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpacketchannel.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpacketchannel.o `test -f 'src/bridge/cockpitpacketchannel.c' || echo '$(srcdir)/'`src/bridge/cockpitpacketchannel.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpacketchannel.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpacketchannel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpacketchannel.c' object='src/bridge/libcockpit_bridge_a-cockpitpacketchannel.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpacketchannel.o `test -f 'src/bridge/cockpitpacketchannel.c' || echo '$(srcdir)/'`src/bridge/cockpitpacketchannel.c
+
+src/bridge/libcockpit_bridge_a-cockpitpacketchannel.obj: src/bridge/cockpitpacketchannel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpacketchannel.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpacketchannel.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpacketchannel.obj `if test -f 'src/bridge/cockpitpacketchannel.c'; then $(CYGPATH_W) 'src/bridge/cockpitpacketchannel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpacketchannel.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpacketchannel.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpacketchannel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpacketchannel.c' object='src/bridge/libcockpit_bridge_a-cockpitpacketchannel.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpacketchannel.obj `if test -f 'src/bridge/cockpitpacketchannel.c'; then $(CYGPATH_W) 'src/bridge/cockpitpacketchannel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpacketchannel.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitpaths.o: src/bridge/cockpitpaths.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpaths.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpaths.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpaths.o `test -f 'src/bridge/cockpitpaths.c' || echo '$(srcdir)/'`src/bridge/cockpitpaths.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpaths.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpaths.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpaths.c' object='src/bridge/libcockpit_bridge_a-cockpitpaths.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpaths.o `test -f 'src/bridge/cockpitpaths.c' || echo '$(srcdir)/'`src/bridge/cockpitpaths.c
+
+src/bridge/libcockpit_bridge_a-cockpitpaths.obj: src/bridge/cockpitpaths.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpaths.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpaths.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpaths.obj `if test -f 'src/bridge/cockpitpaths.c'; then $(CYGPATH_W) 'src/bridge/cockpitpaths.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpaths.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpaths.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpaths.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpaths.c' object='src/bridge/libcockpit_bridge_a-cockpitpaths.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpaths.obj `if test -f 'src/bridge/cockpitpaths.c'; then $(CYGPATH_W) 'src/bridge/cockpitpaths.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpaths.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitpeer.o: src/bridge/cockpitpeer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpeer.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpeer.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpeer.o `test -f 'src/bridge/cockpitpeer.c' || echo '$(srcdir)/'`src/bridge/cockpitpeer.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpeer.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpeer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpeer.c' object='src/bridge/libcockpit_bridge_a-cockpitpeer.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpeer.o `test -f 'src/bridge/cockpitpeer.c' || echo '$(srcdir)/'`src/bridge/cockpitpeer.c
+
+src/bridge/libcockpit_bridge_a-cockpitpeer.obj: src/bridge/cockpitpeer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpeer.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpeer.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpeer.obj `if test -f 'src/bridge/cockpitpeer.c'; then $(CYGPATH_W) 'src/bridge/cockpitpeer.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpeer.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpeer.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpeer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpeer.c' object='src/bridge/libcockpit_bridge_a-cockpitpeer.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpeer.obj `if test -f 'src/bridge/cockpitpeer.c'; then $(CYGPATH_W) 'src/bridge/cockpitpeer.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpeer.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitpipechannel.o: src/bridge/cockpitpipechannel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpipechannel.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpipechannel.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpipechannel.o `test -f 'src/bridge/cockpitpipechannel.c' || echo '$(srcdir)/'`src/bridge/cockpitpipechannel.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpipechannel.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpipechannel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpipechannel.c' object='src/bridge/libcockpit_bridge_a-cockpitpipechannel.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpipechannel.o `test -f 'src/bridge/cockpitpipechannel.c' || echo '$(srcdir)/'`src/bridge/cockpitpipechannel.c
+
+src/bridge/libcockpit_bridge_a-cockpitpipechannel.obj: src/bridge/cockpitpipechannel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpipechannel.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpipechannel.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpipechannel.obj `if test -f 'src/bridge/cockpitpipechannel.c'; then $(CYGPATH_W) 'src/bridge/cockpitpipechannel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpipechannel.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpipechannel.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpipechannel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpipechannel.c' object='src/bridge/libcockpit_bridge_a-cockpitpipechannel.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpipechannel.obj `if test -f 'src/bridge/cockpitpipechannel.c'; then $(CYGPATH_W) 'src/bridge/cockpitpipechannel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpipechannel.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitrouter.o: src/bridge/cockpitrouter.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitrouter.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitrouter.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitrouter.o `test -f 'src/bridge/cockpitrouter.c' || echo '$(srcdir)/'`src/bridge/cockpitrouter.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitrouter.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitrouter.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitrouter.c' object='src/bridge/libcockpit_bridge_a-cockpitrouter.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitrouter.o `test -f 'src/bridge/cockpitrouter.c' || echo '$(srcdir)/'`src/bridge/cockpitrouter.c
+
+src/bridge/libcockpit_bridge_a-cockpitrouter.obj: src/bridge/cockpitrouter.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitrouter.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitrouter.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitrouter.obj `if test -f 'src/bridge/cockpitrouter.c'; then $(CYGPATH_W) 'src/bridge/cockpitrouter.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitrouter.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitrouter.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitrouter.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitrouter.c' object='src/bridge/libcockpit_bridge_a-cockpitrouter.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitrouter.obj `if test -f 'src/bridge/cockpitrouter.c'; then $(CYGPATH_W) 'src/bridge/cockpitrouter.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitrouter.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitstream.o: src/bridge/cockpitstream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitstream.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitstream.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitstream.o `test -f 'src/bridge/cockpitstream.c' || echo '$(srcdir)/'`src/bridge/cockpitstream.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitstream.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitstream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitstream.c' object='src/bridge/libcockpit_bridge_a-cockpitstream.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitstream.o `test -f 'src/bridge/cockpitstream.c' || echo '$(srcdir)/'`src/bridge/cockpitstream.c
+
+src/bridge/libcockpit_bridge_a-cockpitstream.obj: src/bridge/cockpitstream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitstream.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitstream.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitstream.obj `if test -f 'src/bridge/cockpitstream.c'; then $(CYGPATH_W) 'src/bridge/cockpitstream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitstream.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitstream.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitstream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitstream.c' object='src/bridge/libcockpit_bridge_a-cockpitstream.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitstream.obj `if test -f 'src/bridge/cockpitstream.c'; then $(CYGPATH_W) 'src/bridge/cockpitstream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitstream.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.o: src/bridge/cockpitwebsocketstream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitwebsocketstream.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.o `test -f 'src/bridge/cockpitwebsocketstream.c' || echo '$(srcdir)/'`src/bridge/cockpitwebsocketstream.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitwebsocketstream.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitwebsocketstream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitwebsocketstream.c' object='src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.o `test -f 'src/bridge/cockpitwebsocketstream.c' || echo '$(srcdir)/'`src/bridge/cockpitwebsocketstream.c
+
+src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.obj: src/bridge/cockpitwebsocketstream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitwebsocketstream.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.obj `if test -f 'src/bridge/cockpitwebsocketstream.c'; then $(CYGPATH_W) 'src/bridge/cockpitwebsocketstream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitwebsocketstream.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitwebsocketstream.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitwebsocketstream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitwebsocketstream.c' object='src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitwebsocketstream.obj `if test -f 'src/bridge/cockpitwebsocketstream.c'; then $(CYGPATH_W) 'src/bridge/cockpitwebsocketstream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitwebsocketstream.c'; fi`
+
+src/bridge/libcockpit_bridge_a-cockpitpolkitagent.o: src/bridge/cockpitpolkitagent.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpolkitagent.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpolkitagent.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpolkitagent.o `test -f 'src/bridge/cockpitpolkitagent.c' || echo '$(srcdir)/'`src/bridge/cockpitpolkitagent.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpolkitagent.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpolkitagent.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpolkitagent.c' object='src/bridge/libcockpit_bridge_a-cockpitpolkitagent.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpolkitagent.o `test -f 'src/bridge/cockpitpolkitagent.c' || echo '$(srcdir)/'`src/bridge/cockpitpolkitagent.c
+
+src/bridge/libcockpit_bridge_a-cockpitpolkitagent.obj: src/bridge/cockpitpolkitagent.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_bridge_a-cockpitpolkitagent.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpolkitagent.Tpo -c -o src/bridge/libcockpit_bridge_a-cockpitpolkitagent.obj `if test -f 'src/bridge/cockpitpolkitagent.c'; then $(CYGPATH_W) 'src/bridge/cockpitpolkitagent.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpolkitagent.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpolkitagent.Tpo src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpolkitagent.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpolkitagent.c' object='src/bridge/libcockpit_bridge_a-cockpitpolkitagent.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_bridge_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_bridge_a-cockpitpolkitagent.obj `if test -f 'src/bridge/cockpitpolkitagent.c'; then $(CYGPATH_W) 'src/bridge/cockpitpolkitagent.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpolkitagent.c'; fi`
+
+src/common/libcockpit_common_a-cockpitchannel.o: src/common/cockpitchannel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitchannel.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitchannel.Tpo -c -o src/common/libcockpit_common_a-cockpitchannel.o `test -f 'src/common/cockpitchannel.c' || echo '$(srcdir)/'`src/common/cockpitchannel.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitchannel.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitchannel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitchannel.c' object='src/common/libcockpit_common_a-cockpitchannel.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitchannel.o `test -f 'src/common/cockpitchannel.c' || echo '$(srcdir)/'`src/common/cockpitchannel.c
+
+src/common/libcockpit_common_a-cockpitchannel.obj: src/common/cockpitchannel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitchannel.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitchannel.Tpo -c -o src/common/libcockpit_common_a-cockpitchannel.obj `if test -f 'src/common/cockpitchannel.c'; then $(CYGPATH_W) 'src/common/cockpitchannel.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitchannel.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitchannel.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitchannel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitchannel.c' object='src/common/libcockpit_common_a-cockpitchannel.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitchannel.obj `if test -f 'src/common/cockpitchannel.c'; then $(CYGPATH_W) 'src/common/cockpitchannel.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitchannel.c'; fi`
+
+src/common/libcockpit_common_a-cockpitclosefrom.o: src/common/cockpitclosefrom.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitclosefrom.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitclosefrom.Tpo -c -o src/common/libcockpit_common_a-cockpitclosefrom.o `test -f 'src/common/cockpitclosefrom.c' || echo '$(srcdir)/'`src/common/cockpitclosefrom.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitclosefrom.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitclosefrom.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitclosefrom.c' object='src/common/libcockpit_common_a-cockpitclosefrom.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitclosefrom.o `test -f 'src/common/cockpitclosefrom.c' || echo '$(srcdir)/'`src/common/cockpitclosefrom.c
+
+src/common/libcockpit_common_a-cockpitclosefrom.obj: src/common/cockpitclosefrom.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitclosefrom.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitclosefrom.Tpo -c -o src/common/libcockpit_common_a-cockpitclosefrom.obj `if test -f 'src/common/cockpitclosefrom.c'; then $(CYGPATH_W) 'src/common/cockpitclosefrom.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitclosefrom.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitclosefrom.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitclosefrom.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitclosefrom.c' object='src/common/libcockpit_common_a-cockpitclosefrom.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitclosefrom.obj `if test -f 'src/common/cockpitclosefrom.c'; then $(CYGPATH_W) 'src/common/cockpitclosefrom.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitclosefrom.c'; fi`
+
+src/common/libcockpit_common_a-cockpitcontrolmessages.o: src/common/cockpitcontrolmessages.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitcontrolmessages.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitcontrolmessages.Tpo -c -o src/common/libcockpit_common_a-cockpitcontrolmessages.o `test -f 'src/common/cockpitcontrolmessages.c' || echo '$(srcdir)/'`src/common/cockpitcontrolmessages.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitcontrolmessages.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitcontrolmessages.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitcontrolmessages.c' object='src/common/libcockpit_common_a-cockpitcontrolmessages.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitcontrolmessages.o `test -f 'src/common/cockpitcontrolmessages.c' || echo '$(srcdir)/'`src/common/cockpitcontrolmessages.c
+
+src/common/libcockpit_common_a-cockpitcontrolmessages.obj: src/common/cockpitcontrolmessages.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitcontrolmessages.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitcontrolmessages.Tpo -c -o src/common/libcockpit_common_a-cockpitcontrolmessages.obj `if test -f 'src/common/cockpitcontrolmessages.c'; then $(CYGPATH_W) 'src/common/cockpitcontrolmessages.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitcontrolmessages.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitcontrolmessages.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitcontrolmessages.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitcontrolmessages.c' object='src/common/libcockpit_common_a-cockpitcontrolmessages.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitcontrolmessages.obj `if test -f 'src/common/cockpitcontrolmessages.c'; then $(CYGPATH_W) 'src/common/cockpitcontrolmessages.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitcontrolmessages.c'; fi`
+
+src/common/libcockpit_common_a-cockpiterror.o: src/common/cockpiterror.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpiterror.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpiterror.Tpo -c -o src/common/libcockpit_common_a-cockpiterror.o `test -f 'src/common/cockpiterror.c' || echo '$(srcdir)/'`src/common/cockpiterror.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpiterror.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpiterror.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpiterror.c' object='src/common/libcockpit_common_a-cockpiterror.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpiterror.o `test -f 'src/common/cockpiterror.c' || echo '$(srcdir)/'`src/common/cockpiterror.c
+
+src/common/libcockpit_common_a-cockpiterror.obj: src/common/cockpiterror.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpiterror.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpiterror.Tpo -c -o src/common/libcockpit_common_a-cockpiterror.obj `if test -f 'src/common/cockpiterror.c'; then $(CYGPATH_W) 'src/common/cockpiterror.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpiterror.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpiterror.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpiterror.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpiterror.c' object='src/common/libcockpit_common_a-cockpiterror.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpiterror.obj `if test -f 'src/common/cockpiterror.c'; then $(CYGPATH_W) 'src/common/cockpiterror.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpiterror.c'; fi`
+
+src/common/libcockpit_common_a-cockpitflow.o: src/common/cockpitflow.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitflow.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitflow.Tpo -c -o src/common/libcockpit_common_a-cockpitflow.o `test -f 'src/common/cockpitflow.c' || echo '$(srcdir)/'`src/common/cockpitflow.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitflow.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitflow.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitflow.c' object='src/common/libcockpit_common_a-cockpitflow.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitflow.o `test -f 'src/common/cockpitflow.c' || echo '$(srcdir)/'`src/common/cockpitflow.c
+
+src/common/libcockpit_common_a-cockpitflow.obj: src/common/cockpitflow.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitflow.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitflow.Tpo -c -o src/common/libcockpit_common_a-cockpitflow.obj `if test -f 'src/common/cockpitflow.c'; then $(CYGPATH_W) 'src/common/cockpitflow.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitflow.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitflow.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitflow.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitflow.c' object='src/common/libcockpit_common_a-cockpitflow.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitflow.obj `if test -f 'src/common/cockpitflow.c'; then $(CYGPATH_W) 'src/common/cockpitflow.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitflow.c'; fi`
+
+src/common/libcockpit_common_a-cockpithash.o: src/common/cockpithash.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpithash.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpithash.Tpo -c -o src/common/libcockpit_common_a-cockpithash.o `test -f 'src/common/cockpithash.c' || echo '$(srcdir)/'`src/common/cockpithash.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpithash.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpithash.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpithash.c' object='src/common/libcockpit_common_a-cockpithash.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpithash.o `test -f 'src/common/cockpithash.c' || echo '$(srcdir)/'`src/common/cockpithash.c
+
+src/common/libcockpit_common_a-cockpithash.obj: src/common/cockpithash.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpithash.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpithash.Tpo -c -o src/common/libcockpit_common_a-cockpithash.obj `if test -f 'src/common/cockpithash.c'; then $(CYGPATH_W) 'src/common/cockpithash.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpithash.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpithash.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpithash.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpithash.c' object='src/common/libcockpit_common_a-cockpithash.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpithash.obj `if test -f 'src/common/cockpithash.c'; then $(CYGPATH_W) 'src/common/cockpithash.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpithash.c'; fi`
+
+src/common/libcockpit_common_a-cockpitjson.o: src/common/cockpitjson.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitjson.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitjson.Tpo -c -o src/common/libcockpit_common_a-cockpitjson.o `test -f 'src/common/cockpitjson.c' || echo '$(srcdir)/'`src/common/cockpitjson.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitjson.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitjson.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitjson.c' object='src/common/libcockpit_common_a-cockpitjson.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitjson.o `test -f 'src/common/cockpitjson.c' || echo '$(srcdir)/'`src/common/cockpitjson.c
+
+src/common/libcockpit_common_a-cockpitjson.obj: src/common/cockpitjson.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitjson.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitjson.Tpo -c -o src/common/libcockpit_common_a-cockpitjson.obj `if test -f 'src/common/cockpitjson.c'; then $(CYGPATH_W) 'src/common/cockpitjson.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitjson.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitjson.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitjson.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitjson.c' object='src/common/libcockpit_common_a-cockpitjson.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitjson.obj `if test -f 'src/common/cockpitjson.c'; then $(CYGPATH_W) 'src/common/cockpitjson.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitjson.c'; fi`
+
+src/common/libcockpit_common_a-cockpitlocale.o: src/common/cockpitlocale.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitlocale.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitlocale.Tpo -c -o src/common/libcockpit_common_a-cockpitlocale.o `test -f 'src/common/cockpitlocale.c' || echo '$(srcdir)/'`src/common/cockpitlocale.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitlocale.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitlocale.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitlocale.c' object='src/common/libcockpit_common_a-cockpitlocale.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitlocale.o `test -f 'src/common/cockpitlocale.c' || echo '$(srcdir)/'`src/common/cockpitlocale.c
+
+src/common/libcockpit_common_a-cockpitlocale.obj: src/common/cockpitlocale.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitlocale.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitlocale.Tpo -c -o src/common/libcockpit_common_a-cockpitlocale.obj `if test -f 'src/common/cockpitlocale.c'; then $(CYGPATH_W) 'src/common/cockpitlocale.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitlocale.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitlocale.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitlocale.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitlocale.c' object='src/common/libcockpit_common_a-cockpitlocale.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitlocale.obj `if test -f 'src/common/cockpitlocale.c'; then $(CYGPATH_W) 'src/common/cockpitlocale.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitlocale.c'; fi`
+
+src/common/libcockpit_common_a-cockpitloopback.o: src/common/cockpitloopback.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitloopback.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitloopback.Tpo -c -o src/common/libcockpit_common_a-cockpitloopback.o `test -f 'src/common/cockpitloopback.c' || echo '$(srcdir)/'`src/common/cockpitloopback.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitloopback.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitloopback.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitloopback.c' object='src/common/libcockpit_common_a-cockpitloopback.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitloopback.o `test -f 'src/common/cockpitloopback.c' || echo '$(srcdir)/'`src/common/cockpitloopback.c
+
+src/common/libcockpit_common_a-cockpitloopback.obj: src/common/cockpitloopback.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitloopback.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitloopback.Tpo -c -o src/common/libcockpit_common_a-cockpitloopback.obj `if test -f 'src/common/cockpitloopback.c'; then $(CYGPATH_W) 'src/common/cockpitloopback.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitloopback.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitloopback.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitloopback.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitloopback.c' object='src/common/libcockpit_common_a-cockpitloopback.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitloopback.obj `if test -f 'src/common/cockpitloopback.c'; then $(CYGPATH_W) 'src/common/cockpitloopback.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitloopback.c'; fi`
+
+src/common/libcockpit_common_a-cockpitmachinesjson.o: src/common/cockpitmachinesjson.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitmachinesjson.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitmachinesjson.Tpo -c -o src/common/libcockpit_common_a-cockpitmachinesjson.o `test -f 'src/common/cockpitmachinesjson.c' || echo '$(srcdir)/'`src/common/cockpitmachinesjson.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitmachinesjson.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitmachinesjson.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitmachinesjson.c' object='src/common/libcockpit_common_a-cockpitmachinesjson.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitmachinesjson.o `test -f 'src/common/cockpitmachinesjson.c' || echo '$(srcdir)/'`src/common/cockpitmachinesjson.c
+
+src/common/libcockpit_common_a-cockpitmachinesjson.obj: src/common/cockpitmachinesjson.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitmachinesjson.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitmachinesjson.Tpo -c -o src/common/libcockpit_common_a-cockpitmachinesjson.obj `if test -f 'src/common/cockpitmachinesjson.c'; then $(CYGPATH_W) 'src/common/cockpitmachinesjson.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitmachinesjson.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitmachinesjson.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitmachinesjson.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitmachinesjson.c' object='src/common/libcockpit_common_a-cockpitmachinesjson.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitmachinesjson.obj `if test -f 'src/common/cockpitmachinesjson.c'; then $(CYGPATH_W) 'src/common/cockpitmachinesjson.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitmachinesjson.c'; fi`
+
+src/common/libcockpit_common_a-cockpitmemfdread.o: src/common/cockpitmemfdread.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitmemfdread.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitmemfdread.Tpo -c -o src/common/libcockpit_common_a-cockpitmemfdread.o `test -f 'src/common/cockpitmemfdread.c' || echo '$(srcdir)/'`src/common/cockpitmemfdread.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitmemfdread.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitmemfdread.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitmemfdread.c' object='src/common/libcockpit_common_a-cockpitmemfdread.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitmemfdread.o `test -f 'src/common/cockpitmemfdread.c' || echo '$(srcdir)/'`src/common/cockpitmemfdread.c
+
+src/common/libcockpit_common_a-cockpitmemfdread.obj: src/common/cockpitmemfdread.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitmemfdread.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitmemfdread.Tpo -c -o src/common/libcockpit_common_a-cockpitmemfdread.obj `if test -f 'src/common/cockpitmemfdread.c'; then $(CYGPATH_W) 'src/common/cockpitmemfdread.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitmemfdread.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitmemfdread.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitmemfdread.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitmemfdread.c' object='src/common/libcockpit_common_a-cockpitmemfdread.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitmemfdread.obj `if test -f 'src/common/cockpitmemfdread.c'; then $(CYGPATH_W) 'src/common/cockpitmemfdread.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitmemfdread.c'; fi`
+
+src/common/libcockpit_common_a-cockpitpipe.o: src/common/cockpitpipe.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitpipe.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipe.Tpo -c -o src/common/libcockpit_common_a-cockpitpipe.o `test -f 'src/common/cockpitpipe.c' || echo '$(srcdir)/'`src/common/cockpitpipe.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipe.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipe.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitpipe.c' object='src/common/libcockpit_common_a-cockpitpipe.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitpipe.o `test -f 'src/common/cockpitpipe.c' || echo '$(srcdir)/'`src/common/cockpitpipe.c
+
+src/common/libcockpit_common_a-cockpitpipe.obj: src/common/cockpitpipe.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitpipe.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipe.Tpo -c -o src/common/libcockpit_common_a-cockpitpipe.obj `if test -f 'src/common/cockpitpipe.c'; then $(CYGPATH_W) 'src/common/cockpitpipe.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitpipe.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipe.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipe.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitpipe.c' object='src/common/libcockpit_common_a-cockpitpipe.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitpipe.obj `if test -f 'src/common/cockpitpipe.c'; then $(CYGPATH_W) 'src/common/cockpitpipe.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitpipe.c'; fi`
+
+src/common/libcockpit_common_a-cockpitpipetransport.o: src/common/cockpitpipetransport.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitpipetransport.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipetransport.Tpo -c -o src/common/libcockpit_common_a-cockpitpipetransport.o `test -f 'src/common/cockpitpipetransport.c' || echo '$(srcdir)/'`src/common/cockpitpipetransport.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipetransport.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipetransport.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitpipetransport.c' object='src/common/libcockpit_common_a-cockpitpipetransport.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitpipetransport.o `test -f 'src/common/cockpitpipetransport.c' || echo '$(srcdir)/'`src/common/cockpitpipetransport.c
+
+src/common/libcockpit_common_a-cockpitpipetransport.obj: src/common/cockpitpipetransport.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitpipetransport.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipetransport.Tpo -c -o src/common/libcockpit_common_a-cockpitpipetransport.obj `if test -f 'src/common/cockpitpipetransport.c'; then $(CYGPATH_W) 'src/common/cockpitpipetransport.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitpipetransport.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipetransport.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipetransport.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitpipetransport.c' object='src/common/libcockpit_common_a-cockpitpipetransport.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitpipetransport.obj `if test -f 'src/common/cockpitpipetransport.c'; then $(CYGPATH_W) 'src/common/cockpitpipetransport.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitpipetransport.c'; fi`
+
+src/common/libcockpit_common_a-cockpitsocket.o: src/common/cockpitsocket.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitsocket.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitsocket.Tpo -c -o src/common/libcockpit_common_a-cockpitsocket.o `test -f 'src/common/cockpitsocket.c' || echo '$(srcdir)/'`src/common/cockpitsocket.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitsocket.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitsocket.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitsocket.c' object='src/common/libcockpit_common_a-cockpitsocket.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitsocket.o `test -f 'src/common/cockpitsocket.c' || echo '$(srcdir)/'`src/common/cockpitsocket.c
+
+src/common/libcockpit_common_a-cockpitsocket.obj: src/common/cockpitsocket.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitsocket.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitsocket.Tpo -c -o src/common/libcockpit_common_a-cockpitsocket.obj `if test -f 'src/common/cockpitsocket.c'; then $(CYGPATH_W) 'src/common/cockpitsocket.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitsocket.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitsocket.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitsocket.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitsocket.c' object='src/common/libcockpit_common_a-cockpitsocket.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitsocket.obj `if test -f 'src/common/cockpitsocket.c'; then $(CYGPATH_W) 'src/common/cockpitsocket.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitsocket.c'; fi`
+
+src/common/libcockpit_common_a-cockpitsystem.o: src/common/cockpitsystem.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitsystem.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitsystem.Tpo -c -o src/common/libcockpit_common_a-cockpitsystem.o `test -f 'src/common/cockpitsystem.c' || echo '$(srcdir)/'`src/common/cockpitsystem.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitsystem.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitsystem.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitsystem.c' object='src/common/libcockpit_common_a-cockpitsystem.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitsystem.o `test -f 'src/common/cockpitsystem.c' || echo '$(srcdir)/'`src/common/cockpitsystem.c
+
+src/common/libcockpit_common_a-cockpitsystem.obj: src/common/cockpitsystem.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitsystem.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitsystem.Tpo -c -o src/common/libcockpit_common_a-cockpitsystem.obj `if test -f 'src/common/cockpitsystem.c'; then $(CYGPATH_W) 'src/common/cockpitsystem.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitsystem.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitsystem.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitsystem.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitsystem.c' object='src/common/libcockpit_common_a-cockpitsystem.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitsystem.obj `if test -f 'src/common/cockpitsystem.c'; then $(CYGPATH_W) 'src/common/cockpitsystem.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitsystem.c'; fi`
+
+src/common/libcockpit_common_a-cockpittemplate.o: src/common/cockpittemplate.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpittemplate.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpittemplate.Tpo -c -o src/common/libcockpit_common_a-cockpittemplate.o `test -f 'src/common/cockpittemplate.c' || echo '$(srcdir)/'`src/common/cockpittemplate.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpittemplate.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpittemplate.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpittemplate.c' object='src/common/libcockpit_common_a-cockpittemplate.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpittemplate.o `test -f 'src/common/cockpittemplate.c' || echo '$(srcdir)/'`src/common/cockpittemplate.c
+
+src/common/libcockpit_common_a-cockpittemplate.obj: src/common/cockpittemplate.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpittemplate.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpittemplate.Tpo -c -o src/common/libcockpit_common_a-cockpittemplate.obj `if test -f 'src/common/cockpittemplate.c'; then $(CYGPATH_W) 'src/common/cockpittemplate.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpittemplate.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpittemplate.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpittemplate.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpittemplate.c' object='src/common/libcockpit_common_a-cockpittemplate.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpittemplate.obj `if test -f 'src/common/cockpittemplate.c'; then $(CYGPATH_W) 'src/common/cockpittemplate.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpittemplate.c'; fi`
+
+src/common/libcockpit_common_a-cockpittransport.o: src/common/cockpittransport.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpittransport.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpittransport.Tpo -c -o src/common/libcockpit_common_a-cockpittransport.o `test -f 'src/common/cockpittransport.c' || echo '$(srcdir)/'`src/common/cockpittransport.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpittransport.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpittransport.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpittransport.c' object='src/common/libcockpit_common_a-cockpittransport.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpittransport.o `test -f 'src/common/cockpittransport.c' || echo '$(srcdir)/'`src/common/cockpittransport.c
+
+src/common/libcockpit_common_a-cockpittransport.obj: src/common/cockpittransport.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpittransport.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpittransport.Tpo -c -o src/common/libcockpit_common_a-cockpittransport.obj `if test -f 'src/common/cockpittransport.c'; then $(CYGPATH_W) 'src/common/cockpittransport.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpittransport.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpittransport.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpittransport.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpittransport.c' object='src/common/libcockpit_common_a-cockpittransport.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpittransport.obj `if test -f 'src/common/cockpittransport.c'; then $(CYGPATH_W) 'src/common/cockpittransport.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpittransport.c'; fi`
+
+src/common/libcockpit_common_a-cockpitunicode.o: src/common/cockpitunicode.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitunicode.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitunicode.Tpo -c -o src/common/libcockpit_common_a-cockpitunicode.o `test -f 'src/common/cockpitunicode.c' || echo '$(srcdir)/'`src/common/cockpitunicode.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitunicode.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitunicode.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitunicode.c' object='src/common/libcockpit_common_a-cockpitunicode.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitunicode.o `test -f 'src/common/cockpitunicode.c' || echo '$(srcdir)/'`src/common/cockpitunicode.c
+
+src/common/libcockpit_common_a-cockpitunicode.obj: src/common/cockpitunicode.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitunicode.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitunicode.Tpo -c -o src/common/libcockpit_common_a-cockpitunicode.obj `if test -f 'src/common/cockpitunicode.c'; then $(CYGPATH_W) 'src/common/cockpitunicode.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitunicode.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitunicode.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitunicode.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitunicode.c' object='src/common/libcockpit_common_a-cockpitunicode.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitunicode.obj `if test -f 'src/common/cockpitunicode.c'; then $(CYGPATH_W) 'src/common/cockpitunicode.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitunicode.c'; fi`
+
+src/common/libcockpit_common_a-cockpitunixsignal.o: src/common/cockpitunixsignal.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitunixsignal.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitunixsignal.Tpo -c -o src/common/libcockpit_common_a-cockpitunixsignal.o `test -f 'src/common/cockpitunixsignal.c' || echo '$(srcdir)/'`src/common/cockpitunixsignal.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitunixsignal.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitunixsignal.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitunixsignal.c' object='src/common/libcockpit_common_a-cockpitunixsignal.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitunixsignal.o `test -f 'src/common/cockpitunixsignal.c' || echo '$(srcdir)/'`src/common/cockpitunixsignal.c
+
+src/common/libcockpit_common_a-cockpitunixsignal.obj: src/common/cockpitunixsignal.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitunixsignal.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitunixsignal.Tpo -c -o src/common/libcockpit_common_a-cockpitunixsignal.obj `if test -f 'src/common/cockpitunixsignal.c'; then $(CYGPATH_W) 'src/common/cockpitunixsignal.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitunixsignal.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitunixsignal.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitunixsignal.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitunixsignal.c' object='src/common/libcockpit_common_a-cockpitunixsignal.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitunixsignal.obj `if test -f 'src/common/cockpitunixsignal.c'; then $(CYGPATH_W) 'src/common/cockpitunixsignal.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitunixsignal.c'; fi`
+
+src/common/libcockpit_common_a-cockpitversion.o: src/common/cockpitversion.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitversion.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitversion.Tpo -c -o src/common/libcockpit_common_a-cockpitversion.o `test -f 'src/common/cockpitversion.c' || echo '$(srcdir)/'`src/common/cockpitversion.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitversion.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitversion.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitversion.c' object='src/common/libcockpit_common_a-cockpitversion.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitversion.o `test -f 'src/common/cockpitversion.c' || echo '$(srcdir)/'`src/common/cockpitversion.c
+
+src/common/libcockpit_common_a-cockpitversion.obj: src/common/cockpitversion.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitversion.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitversion.Tpo -c -o src/common/libcockpit_common_a-cockpitversion.obj `if test -f 'src/common/cockpitversion.c'; then $(CYGPATH_W) 'src/common/cockpitversion.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitversion.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitversion.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitversion.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitversion.c' object='src/common/libcockpit_common_a-cockpitversion.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitversion.obj `if test -f 'src/common/cockpitversion.c'; then $(CYGPATH_W) 'src/common/cockpitversion.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitversion.c'; fi`
+
+src/common/libcockpit_common_a-cockpitwebfilter.o: src/common/cockpitwebfilter.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitwebfilter.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebfilter.Tpo -c -o src/common/libcockpit_common_a-cockpitwebfilter.o `test -f 'src/common/cockpitwebfilter.c' || echo '$(srcdir)/'`src/common/cockpitwebfilter.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebfilter.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebfilter.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitwebfilter.c' object='src/common/libcockpit_common_a-cockpitwebfilter.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitwebfilter.o `test -f 'src/common/cockpitwebfilter.c' || echo '$(srcdir)/'`src/common/cockpitwebfilter.c
+
+src/common/libcockpit_common_a-cockpitwebfilter.obj: src/common/cockpitwebfilter.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitwebfilter.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebfilter.Tpo -c -o src/common/libcockpit_common_a-cockpitwebfilter.obj `if test -f 'src/common/cockpitwebfilter.c'; then $(CYGPATH_W) 'src/common/cockpitwebfilter.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitwebfilter.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebfilter.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebfilter.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitwebfilter.c' object='src/common/libcockpit_common_a-cockpitwebfilter.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitwebfilter.obj `if test -f 'src/common/cockpitwebfilter.c'; then $(CYGPATH_W) 'src/common/cockpitwebfilter.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitwebfilter.c'; fi`
+
+src/common/libcockpit_common_a-cockpitwebinject.o: src/common/cockpitwebinject.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitwebinject.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebinject.Tpo -c -o src/common/libcockpit_common_a-cockpitwebinject.o `test -f 'src/common/cockpitwebinject.c' || echo '$(srcdir)/'`src/common/cockpitwebinject.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebinject.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebinject.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitwebinject.c' object='src/common/libcockpit_common_a-cockpitwebinject.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitwebinject.o `test -f 'src/common/cockpitwebinject.c' || echo '$(srcdir)/'`src/common/cockpitwebinject.c
+
+src/common/libcockpit_common_a-cockpitwebinject.obj: src/common/cockpitwebinject.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitwebinject.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebinject.Tpo -c -o src/common/libcockpit_common_a-cockpitwebinject.obj `if test -f 'src/common/cockpitwebinject.c'; then $(CYGPATH_W) 'src/common/cockpitwebinject.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitwebinject.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebinject.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebinject.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitwebinject.c' object='src/common/libcockpit_common_a-cockpitwebinject.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitwebinject.obj `if test -f 'src/common/cockpitwebinject.c'; then $(CYGPATH_W) 'src/common/cockpitwebinject.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitwebinject.c'; fi`
+
+src/common/libcockpit_common_a-cockpitwebresponse.o: src/common/cockpitwebresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitwebresponse.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebresponse.Tpo -c -o src/common/libcockpit_common_a-cockpitwebresponse.o `test -f 'src/common/cockpitwebresponse.c' || echo '$(srcdir)/'`src/common/cockpitwebresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebresponse.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebresponse.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitwebresponse.c' object='src/common/libcockpit_common_a-cockpitwebresponse.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitwebresponse.o `test -f 'src/common/cockpitwebresponse.c' || echo '$(srcdir)/'`src/common/cockpitwebresponse.c
+
+src/common/libcockpit_common_a-cockpitwebresponse.obj: src/common/cockpitwebresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitwebresponse.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebresponse.Tpo -c -o src/common/libcockpit_common_a-cockpitwebresponse.obj `if test -f 'src/common/cockpitwebresponse.c'; then $(CYGPATH_W) 'src/common/cockpitwebresponse.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitwebresponse.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebresponse.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebresponse.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitwebresponse.c' object='src/common/libcockpit_common_a-cockpitwebresponse.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitwebresponse.obj `if test -f 'src/common/cockpitwebresponse.c'; then $(CYGPATH_W) 'src/common/cockpitwebresponse.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitwebresponse.c'; fi`
+
+src/common/libcockpit_common_a-cockpitwebserver.o: src/common/cockpitwebserver.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitwebserver.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebserver.Tpo -c -o src/common/libcockpit_common_a-cockpitwebserver.o `test -f 'src/common/cockpitwebserver.c' || echo '$(srcdir)/'`src/common/cockpitwebserver.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebserver.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebserver.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitwebserver.c' object='src/common/libcockpit_common_a-cockpitwebserver.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitwebserver.o `test -f 'src/common/cockpitwebserver.c' || echo '$(srcdir)/'`src/common/cockpitwebserver.c
+
+src/common/libcockpit_common_a-cockpitwebserver.obj: src/common/cockpitwebserver.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-cockpitwebserver.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebserver.Tpo -c -o src/common/libcockpit_common_a-cockpitwebserver.obj `if test -f 'src/common/cockpitwebserver.c'; then $(CYGPATH_W) 'src/common/cockpitwebserver.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitwebserver.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebserver.Tpo src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebserver.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/cockpitwebserver.c' object='src/common/libcockpit_common_a-cockpitwebserver.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-cockpitwebserver.obj `if test -f 'src/common/cockpitwebserver.c'; then $(CYGPATH_W) 'src/common/cockpitwebserver.c'; else $(CYGPATH_W) '$(srcdir)/src/common/cockpitwebserver.c'; fi`
+
+src/common/libcockpit_common_a-fail.html.o: src/common/fail.html.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-fail.html.o -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-fail.html.Tpo -c -o src/common/libcockpit_common_a-fail.html.o `test -f 'src/common/fail.html.c' || echo '$(srcdir)/'`src/common/fail.html.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-fail.html.Tpo src/common/$(DEPDIR)/libcockpit_common_a-fail.html.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/fail.html.c' object='src/common/libcockpit_common_a-fail.html.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-fail.html.o `test -f 'src/common/fail.html.c' || echo '$(srcdir)/'`src/common/fail.html.c
+
+src/common/libcockpit_common_a-fail.html.obj: src/common/fail.html.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/libcockpit_common_a-fail.html.obj -MD -MP -MF src/common/$(DEPDIR)/libcockpit_common_a-fail.html.Tpo -c -o src/common/libcockpit_common_a-fail.html.obj `if test -f 'src/common/fail.html.c'; then $(CYGPATH_W) 'src/common/fail.html.c'; else $(CYGPATH_W) '$(srcdir)/src/common/fail.html.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libcockpit_common_a-fail.html.Tpo src/common/$(DEPDIR)/libcockpit_common_a-fail.html.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/fail.html.c' object='src/common/libcockpit_common_a-fail.html.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_common_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/libcockpit_common_a-fail.html.obj `if test -f 'src/common/fail.html.c'; then $(CYGPATH_W) 'src/common/fail.html.c'; else $(CYGPATH_W) '$(srcdir)/src/common/fail.html.c'; fi`
+
+src/bridge/libcockpit_metrics_a-cockpitblocksamples.o: src/bridge/cockpitblocksamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitblocksamples.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitblocksamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitblocksamples.o `test -f 'src/bridge/cockpitblocksamples.c' || echo '$(srcdir)/'`src/bridge/cockpitblocksamples.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitblocksamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitblocksamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitblocksamples.c' object='src/bridge/libcockpit_metrics_a-cockpitblocksamples.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitblocksamples.o `test -f 'src/bridge/cockpitblocksamples.c' || echo '$(srcdir)/'`src/bridge/cockpitblocksamples.c
+
+src/bridge/libcockpit_metrics_a-cockpitblocksamples.obj: src/bridge/cockpitblocksamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitblocksamples.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitblocksamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitblocksamples.obj `if test -f 'src/bridge/cockpitblocksamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitblocksamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitblocksamples.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitblocksamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitblocksamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitblocksamples.c' object='src/bridge/libcockpit_metrics_a-cockpitblocksamples.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitblocksamples.obj `if test -f 'src/bridge/cockpitblocksamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitblocksamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitblocksamples.c'; fi`
+
+src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.o: src/bridge/cockpitcgroupsamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcgroupsamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.o `test -f 'src/bridge/cockpitcgroupsamples.c' || echo '$(srcdir)/'`src/bridge/cockpitcgroupsamples.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcgroupsamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcgroupsamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitcgroupsamples.c' object='src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.o `test -f 'src/bridge/cockpitcgroupsamples.c' || echo '$(srcdir)/'`src/bridge/cockpitcgroupsamples.c
+
+src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.obj: src/bridge/cockpitcgroupsamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcgroupsamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.obj `if test -f 'src/bridge/cockpitcgroupsamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitcgroupsamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitcgroupsamples.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcgroupsamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcgroupsamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitcgroupsamples.c' object='src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitcgroupsamples.obj `if test -f 'src/bridge/cockpitcgroupsamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitcgroupsamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitcgroupsamples.c'; fi`
+
+src/bridge/libcockpit_metrics_a-cockpitcpusamples.o: src/bridge/cockpitcpusamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitcpusamples.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcpusamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitcpusamples.o `test -f 'src/bridge/cockpitcpusamples.c' || echo '$(srcdir)/'`src/bridge/cockpitcpusamples.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcpusamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcpusamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitcpusamples.c' object='src/bridge/libcockpit_metrics_a-cockpitcpusamples.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitcpusamples.o `test -f 'src/bridge/cockpitcpusamples.c' || echo '$(srcdir)/'`src/bridge/cockpitcpusamples.c
+
+src/bridge/libcockpit_metrics_a-cockpitcpusamples.obj: src/bridge/cockpitcpusamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitcpusamples.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcpusamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitcpusamples.obj `if test -f 'src/bridge/cockpitcpusamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitcpusamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitcpusamples.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcpusamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcpusamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitcpusamples.c' object='src/bridge/libcockpit_metrics_a-cockpitcpusamples.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitcpusamples.obj `if test -f 'src/bridge/cockpitcpusamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitcpusamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitcpusamples.c'; fi`
+
+src/bridge/libcockpit_metrics_a-cockpitdisksamples.o: src/bridge/cockpitdisksamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitdisksamples.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitdisksamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitdisksamples.o `test -f 'src/bridge/cockpitdisksamples.c' || echo '$(srcdir)/'`src/bridge/cockpitdisksamples.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitdisksamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitdisksamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdisksamples.c' object='src/bridge/libcockpit_metrics_a-cockpitdisksamples.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitdisksamples.o `test -f 'src/bridge/cockpitdisksamples.c' || echo '$(srcdir)/'`src/bridge/cockpitdisksamples.c
+
+src/bridge/libcockpit_metrics_a-cockpitdisksamples.obj: src/bridge/cockpitdisksamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitdisksamples.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitdisksamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitdisksamples.obj `if test -f 'src/bridge/cockpitdisksamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitdisksamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdisksamples.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitdisksamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitdisksamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdisksamples.c' object='src/bridge/libcockpit_metrics_a-cockpitdisksamples.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitdisksamples.obj `if test -f 'src/bridge/cockpitdisksamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitdisksamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdisksamples.c'; fi`
+
+src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.o: src/bridge/cockpitinternalmetrics.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitinternalmetrics.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.o `test -f 'src/bridge/cockpitinternalmetrics.c' || echo '$(srcdir)/'`src/bridge/cockpitinternalmetrics.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitinternalmetrics.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitinternalmetrics.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitinternalmetrics.c' object='src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.o `test -f 'src/bridge/cockpitinternalmetrics.c' || echo '$(srcdir)/'`src/bridge/cockpitinternalmetrics.c
+
+src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.obj: src/bridge/cockpitinternalmetrics.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitinternalmetrics.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.obj `if test -f 'src/bridge/cockpitinternalmetrics.c'; then $(CYGPATH_W) 'src/bridge/cockpitinternalmetrics.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitinternalmetrics.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitinternalmetrics.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitinternalmetrics.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitinternalmetrics.c' object='src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitinternalmetrics.obj `if test -f 'src/bridge/cockpitinternalmetrics.c'; then $(CYGPATH_W) 'src/bridge/cockpitinternalmetrics.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitinternalmetrics.c'; fi`
+
+src/bridge/libcockpit_metrics_a-cockpitmemorysamples.o: src/bridge/cockpitmemorysamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitmemorysamples.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmemorysamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitmemorysamples.o `test -f 'src/bridge/cockpitmemorysamples.c' || echo '$(srcdir)/'`src/bridge/cockpitmemorysamples.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmemorysamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmemorysamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitmemorysamples.c' object='src/bridge/libcockpit_metrics_a-cockpitmemorysamples.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitmemorysamples.o `test -f 'src/bridge/cockpitmemorysamples.c' || echo '$(srcdir)/'`src/bridge/cockpitmemorysamples.c
+
+src/bridge/libcockpit_metrics_a-cockpitmemorysamples.obj: src/bridge/cockpitmemorysamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitmemorysamples.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmemorysamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitmemorysamples.obj `if test -f 'src/bridge/cockpitmemorysamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitmemorysamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitmemorysamples.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmemorysamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmemorysamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitmemorysamples.c' object='src/bridge/libcockpit_metrics_a-cockpitmemorysamples.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitmemorysamples.obj `if test -f 'src/bridge/cockpitmemorysamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitmemorysamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitmemorysamples.c'; fi`
+
+src/bridge/libcockpit_metrics_a-cockpitmetrics.o: src/bridge/cockpitmetrics.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitmetrics.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmetrics.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitmetrics.o `test -f 'src/bridge/cockpitmetrics.c' || echo '$(srcdir)/'`src/bridge/cockpitmetrics.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmetrics.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmetrics.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitmetrics.c' object='src/bridge/libcockpit_metrics_a-cockpitmetrics.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitmetrics.o `test -f 'src/bridge/cockpitmetrics.c' || echo '$(srcdir)/'`src/bridge/cockpitmetrics.c
+
+src/bridge/libcockpit_metrics_a-cockpitmetrics.obj: src/bridge/cockpitmetrics.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitmetrics.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmetrics.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitmetrics.obj `if test -f 'src/bridge/cockpitmetrics.c'; then $(CYGPATH_W) 'src/bridge/cockpitmetrics.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitmetrics.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmetrics.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmetrics.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitmetrics.c' object='src/bridge/libcockpit_metrics_a-cockpitmetrics.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitmetrics.obj `if test -f 'src/bridge/cockpitmetrics.c'; then $(CYGPATH_W) 'src/bridge/cockpitmetrics.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitmetrics.c'; fi`
+
+src/bridge/libcockpit_metrics_a-cockpitmountsamples.o: src/bridge/cockpitmountsamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitmountsamples.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmountsamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitmountsamples.o `test -f 'src/bridge/cockpitmountsamples.c' || echo '$(srcdir)/'`src/bridge/cockpitmountsamples.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmountsamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmountsamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitmountsamples.c' object='src/bridge/libcockpit_metrics_a-cockpitmountsamples.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitmountsamples.o `test -f 'src/bridge/cockpitmountsamples.c' || echo '$(srcdir)/'`src/bridge/cockpitmountsamples.c
+
+src/bridge/libcockpit_metrics_a-cockpitmountsamples.obj: src/bridge/cockpitmountsamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitmountsamples.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmountsamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitmountsamples.obj `if test -f 'src/bridge/cockpitmountsamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitmountsamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitmountsamples.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmountsamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmountsamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitmountsamples.c' object='src/bridge/libcockpit_metrics_a-cockpitmountsamples.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitmountsamples.obj `if test -f 'src/bridge/cockpitmountsamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitmountsamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitmountsamples.c'; fi`
+
+src/bridge/libcockpit_metrics_a-cockpitnetworksamples.o: src/bridge/cockpitnetworksamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitnetworksamples.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitnetworksamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitnetworksamples.o `test -f 'src/bridge/cockpitnetworksamples.c' || echo '$(srcdir)/'`src/bridge/cockpitnetworksamples.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitnetworksamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitnetworksamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitnetworksamples.c' object='src/bridge/libcockpit_metrics_a-cockpitnetworksamples.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitnetworksamples.o `test -f 'src/bridge/cockpitnetworksamples.c' || echo '$(srcdir)/'`src/bridge/cockpitnetworksamples.c
+
+src/bridge/libcockpit_metrics_a-cockpitnetworksamples.obj: src/bridge/cockpitnetworksamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitnetworksamples.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitnetworksamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitnetworksamples.obj `if test -f 'src/bridge/cockpitnetworksamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitnetworksamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitnetworksamples.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitnetworksamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitnetworksamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitnetworksamples.c' object='src/bridge/libcockpit_metrics_a-cockpitnetworksamples.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitnetworksamples.obj `if test -f 'src/bridge/cockpitnetworksamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitnetworksamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitnetworksamples.c'; fi`
+
+src/bridge/libcockpit_metrics_a-cockpitsamples.o: src/bridge/cockpitsamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitsamples.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitsamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitsamples.o `test -f 'src/bridge/cockpitsamples.c' || echo '$(srcdir)/'`src/bridge/cockpitsamples.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitsamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitsamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitsamples.c' object='src/bridge/libcockpit_metrics_a-cockpitsamples.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitsamples.o `test -f 'src/bridge/cockpitsamples.c' || echo '$(srcdir)/'`src/bridge/cockpitsamples.c
+
+src/bridge/libcockpit_metrics_a-cockpitsamples.obj: src/bridge/cockpitsamples.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_metrics_a-cockpitsamples.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitsamples.Tpo -c -o src/bridge/libcockpit_metrics_a-cockpitsamples.obj `if test -f 'src/bridge/cockpitsamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitsamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitsamples.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitsamples.Tpo src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitsamples.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitsamples.c' object='src/bridge/libcockpit_metrics_a-cockpitsamples.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_metrics_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_metrics_a-cockpitsamples.obj `if test -f 'src/bridge/cockpitsamples.c'; then $(CYGPATH_W) 'src/bridge/cockpitsamples.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitsamples.c'; fi`
+
+src/bridge/libcockpit_pcp_a-cockpitconnect.o: src/bridge/cockpitconnect.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_pcp_a-cockpitconnect.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitconnect.Tpo -c -o src/bridge/libcockpit_pcp_a-cockpitconnect.o `test -f 'src/bridge/cockpitconnect.c' || echo '$(srcdir)/'`src/bridge/cockpitconnect.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitconnect.Tpo src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitconnect.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitconnect.c' object='src/bridge/libcockpit_pcp_a-cockpitconnect.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_pcp_a-cockpitconnect.o `test -f 'src/bridge/cockpitconnect.c' || echo '$(srcdir)/'`src/bridge/cockpitconnect.c
+
+src/bridge/libcockpit_pcp_a-cockpitconnect.obj: src/bridge/cockpitconnect.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_pcp_a-cockpitconnect.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitconnect.Tpo -c -o src/bridge/libcockpit_pcp_a-cockpitconnect.obj `if test -f 'src/bridge/cockpitconnect.c'; then $(CYGPATH_W) 'src/bridge/cockpitconnect.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitconnect.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitconnect.Tpo src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitconnect.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitconnect.c' object='src/bridge/libcockpit_pcp_a-cockpitconnect.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_pcp_a-cockpitconnect.obj `if test -f 'src/bridge/cockpitconnect.c'; then $(CYGPATH_W) 'src/bridge/cockpitconnect.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitconnect.c'; fi`
+
+src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.o: src/bridge/cockpitpcpmetrics.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpcpmetrics.Tpo -c -o src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.o `test -f 'src/bridge/cockpitpcpmetrics.c' || echo '$(srcdir)/'`src/bridge/cockpitpcpmetrics.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpcpmetrics.Tpo src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpcpmetrics.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpcpmetrics.c' object='src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.o `test -f 'src/bridge/cockpitpcpmetrics.c' || echo '$(srcdir)/'`src/bridge/cockpitpcpmetrics.c
+
+src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.obj: src/bridge/cockpitpcpmetrics.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpcpmetrics.Tpo -c -o src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.obj `if test -f 'src/bridge/cockpitpcpmetrics.c'; then $(CYGPATH_W) 'src/bridge/cockpitpcpmetrics.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpcpmetrics.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpcpmetrics.Tpo src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpcpmetrics.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpcpmetrics.c' object='src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_pcp_a-cockpitpcpmetrics.obj `if test -f 'src/bridge/cockpitpcpmetrics.c'; then $(CYGPATH_W) 'src/bridge/cockpitpcpmetrics.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpcpmetrics.c'; fi`
+
+src/bridge/libcockpit_pcp_a-cockpitdbusinternal.o: src/bridge/cockpitdbusinternal.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_pcp_a-cockpitdbusinternal.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitdbusinternal.Tpo -c -o src/bridge/libcockpit_pcp_a-cockpitdbusinternal.o `test -f 'src/bridge/cockpitdbusinternal.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusinternal.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitdbusinternal.Tpo src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitdbusinternal.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusinternal.c' object='src/bridge/libcockpit_pcp_a-cockpitdbusinternal.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_pcp_a-cockpitdbusinternal.o `test -f 'src/bridge/cockpitdbusinternal.c' || echo '$(srcdir)/'`src/bridge/cockpitdbusinternal.c
+
+src/bridge/libcockpit_pcp_a-cockpitdbusinternal.obj: src/bridge/cockpitdbusinternal.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_pcp_a-cockpitdbusinternal.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitdbusinternal.Tpo -c -o src/bridge/libcockpit_pcp_a-cockpitdbusinternal.obj `if test -f 'src/bridge/cockpitdbusinternal.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusinternal.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusinternal.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitdbusinternal.Tpo src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitdbusinternal.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitdbusinternal.c' object='src/bridge/libcockpit_pcp_a-cockpitdbusinternal.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_pcp_a-cockpitdbusinternal.obj `if test -f 'src/bridge/cockpitdbusinternal.c'; then $(CYGPATH_W) 'src/bridge/cockpitdbusinternal.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitdbusinternal.c'; fi`
+
+src/bridge/libcockpit_pcp_a-cockpitpeer.o: src/bridge/cockpitpeer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_pcp_a-cockpitpeer.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpeer.Tpo -c -o src/bridge/libcockpit_pcp_a-cockpitpeer.o `test -f 'src/bridge/cockpitpeer.c' || echo '$(srcdir)/'`src/bridge/cockpitpeer.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpeer.Tpo src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpeer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpeer.c' object='src/bridge/libcockpit_pcp_a-cockpitpeer.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_pcp_a-cockpitpeer.o `test -f 'src/bridge/cockpitpeer.c' || echo '$(srcdir)/'`src/bridge/cockpitpeer.c
+
+src/bridge/libcockpit_pcp_a-cockpitpeer.obj: src/bridge/cockpitpeer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_pcp_a-cockpitpeer.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpeer.Tpo -c -o src/bridge/libcockpit_pcp_a-cockpitpeer.obj `if test -f 'src/bridge/cockpitpeer.c'; then $(CYGPATH_W) 'src/bridge/cockpitpeer.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpeer.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpeer.Tpo src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpeer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpeer.c' object='src/bridge/libcockpit_pcp_a-cockpitpeer.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_pcp_a-cockpitpeer.obj `if test -f 'src/bridge/cockpitpeer.c'; then $(CYGPATH_W) 'src/bridge/cockpitpeer.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpeer.c'; fi`
+
+src/bridge/libcockpit_pcp_a-cockpitrouter.o: src/bridge/cockpitrouter.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_pcp_a-cockpitrouter.o -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitrouter.Tpo -c -o src/bridge/libcockpit_pcp_a-cockpitrouter.o `test -f 'src/bridge/cockpitrouter.c' || echo '$(srcdir)/'`src/bridge/cockpitrouter.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitrouter.Tpo src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitrouter.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitrouter.c' object='src/bridge/libcockpit_pcp_a-cockpitrouter.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_pcp_a-cockpitrouter.o `test -f 'src/bridge/cockpitrouter.c' || echo '$(srcdir)/'`src/bridge/cockpitrouter.c
+
+src/bridge/libcockpit_pcp_a-cockpitrouter.obj: src/bridge/cockpitrouter.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/libcockpit_pcp_a-cockpitrouter.obj -MD -MP -MF src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitrouter.Tpo -c -o src/bridge/libcockpit_pcp_a-cockpitrouter.obj `if test -f 'src/bridge/cockpitrouter.c'; then $(CYGPATH_W) 'src/bridge/cockpitrouter.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitrouter.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitrouter.Tpo src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitrouter.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitrouter.c' object='src/bridge/libcockpit_pcp_a-cockpitrouter.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_pcp_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/libcockpit_pcp_a-cockpitrouter.obj `if test -f 'src/bridge/cockpitrouter.c'; then $(CYGPATH_W) 'src/bridge/cockpitrouter.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitrouter.c'; fi`
+
+src/ssh/libcockpit_ssh_a-cockpitsshoptions.o: src/ssh/cockpitsshoptions.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ssh_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/libcockpit_ssh_a-cockpitsshoptions.o -MD -MP -MF src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshoptions.Tpo -c -o src/ssh/libcockpit_ssh_a-cockpitsshoptions.o `test -f 'src/ssh/cockpitsshoptions.c' || echo '$(srcdir)/'`src/ssh/cockpitsshoptions.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshoptions.Tpo src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshoptions.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/cockpitsshoptions.c' object='src/ssh/libcockpit_ssh_a-cockpitsshoptions.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ssh_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/libcockpit_ssh_a-cockpitsshoptions.o `test -f 'src/ssh/cockpitsshoptions.c' || echo '$(srcdir)/'`src/ssh/cockpitsshoptions.c
+
+src/ssh/libcockpit_ssh_a-cockpitsshoptions.obj: src/ssh/cockpitsshoptions.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ssh_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/libcockpit_ssh_a-cockpitsshoptions.obj -MD -MP -MF src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshoptions.Tpo -c -o src/ssh/libcockpit_ssh_a-cockpitsshoptions.obj `if test -f 'src/ssh/cockpitsshoptions.c'; then $(CYGPATH_W) 'src/ssh/cockpitsshoptions.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/cockpitsshoptions.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshoptions.Tpo src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshoptions.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/cockpitsshoptions.c' object='src/ssh/libcockpit_ssh_a-cockpitsshoptions.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ssh_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/libcockpit_ssh_a-cockpitsshoptions.obj `if test -f 'src/ssh/cockpitsshoptions.c'; then $(CYGPATH_W) 'src/ssh/cockpitsshoptions.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/cockpitsshoptions.c'; fi`
+
+src/ssh/libcockpit_ssh_a-cockpitsshrelay.o: src/ssh/cockpitsshrelay.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ssh_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/libcockpit_ssh_a-cockpitsshrelay.o -MD -MP -MF src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshrelay.Tpo -c -o src/ssh/libcockpit_ssh_a-cockpitsshrelay.o `test -f 'src/ssh/cockpitsshrelay.c' || echo '$(srcdir)/'`src/ssh/cockpitsshrelay.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshrelay.Tpo src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshrelay.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/cockpitsshrelay.c' object='src/ssh/libcockpit_ssh_a-cockpitsshrelay.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ssh_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/libcockpit_ssh_a-cockpitsshrelay.o `test -f 'src/ssh/cockpitsshrelay.c' || echo '$(srcdir)/'`src/ssh/cockpitsshrelay.c
+
+src/ssh/libcockpit_ssh_a-cockpitsshrelay.obj: src/ssh/cockpitsshrelay.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ssh_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/libcockpit_ssh_a-cockpitsshrelay.obj -MD -MP -MF src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshrelay.Tpo -c -o src/ssh/libcockpit_ssh_a-cockpitsshrelay.obj `if test -f 'src/ssh/cockpitsshrelay.c'; then $(CYGPATH_W) 'src/ssh/cockpitsshrelay.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/cockpitsshrelay.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshrelay.Tpo src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshrelay.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/cockpitsshrelay.c' object='src/ssh/libcockpit_ssh_a-cockpitsshrelay.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ssh_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/libcockpit_ssh_a-cockpitsshrelay.obj `if test -f 'src/ssh/cockpitsshrelay.c'; then $(CYGPATH_W) 'src/ssh/cockpitsshrelay.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/cockpitsshrelay.c'; fi`
+
+src/testlib/libcockpit_test_a-cockpittest.o: src/testlib/cockpittest.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-cockpittest.o -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-cockpittest.Tpo -c -o src/testlib/libcockpit_test_a-cockpittest.o `test -f 'src/testlib/cockpittest.c' || echo '$(srcdir)/'`src/testlib/cockpittest.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-cockpittest.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-cockpittest.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/cockpittest.c' object='src/testlib/libcockpit_test_a-cockpittest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-cockpittest.o `test -f 'src/testlib/cockpittest.c' || echo '$(srcdir)/'`src/testlib/cockpittest.c
+
+src/testlib/libcockpit_test_a-cockpittest.obj: src/testlib/cockpittest.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-cockpittest.obj -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-cockpittest.Tpo -c -o src/testlib/libcockpit_test_a-cockpittest.obj `if test -f 'src/testlib/cockpittest.c'; then $(CYGPATH_W) 'src/testlib/cockpittest.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/cockpittest.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-cockpittest.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-cockpittest.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/cockpittest.c' object='src/testlib/libcockpit_test_a-cockpittest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-cockpittest.obj `if test -f 'src/testlib/cockpittest.c'; then $(CYGPATH_W) 'src/testlib/cockpittest.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/cockpittest.c'; fi`
+
+src/testlib/libcockpit_test_a-mock-auth.o: src/testlib/mock-auth.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-mock-auth.o -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-mock-auth.Tpo -c -o src/testlib/libcockpit_test_a-mock-auth.o `test -f 'src/testlib/mock-auth.c' || echo '$(srcdir)/'`src/testlib/mock-auth.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-mock-auth.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-mock-auth.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/mock-auth.c' object='src/testlib/libcockpit_test_a-mock-auth.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-mock-auth.o `test -f 'src/testlib/mock-auth.c' || echo '$(srcdir)/'`src/testlib/mock-auth.c
+
+src/testlib/libcockpit_test_a-mock-auth.obj: src/testlib/mock-auth.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-mock-auth.obj -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-mock-auth.Tpo -c -o src/testlib/libcockpit_test_a-mock-auth.obj `if test -f 'src/testlib/mock-auth.c'; then $(CYGPATH_W) 'src/testlib/mock-auth.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/mock-auth.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-mock-auth.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-mock-auth.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/mock-auth.c' object='src/testlib/libcockpit_test_a-mock-auth.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-mock-auth.obj `if test -f 'src/testlib/mock-auth.c'; then $(CYGPATH_W) 'src/testlib/mock-auth.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/mock-auth.c'; fi`
+
+src/testlib/libcockpit_test_a-mock-channel.o: src/testlib/mock-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-mock-channel.o -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-mock-channel.Tpo -c -o src/testlib/libcockpit_test_a-mock-channel.o `test -f 'src/testlib/mock-channel.c' || echo '$(srcdir)/'`src/testlib/mock-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-mock-channel.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-mock-channel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/mock-channel.c' object='src/testlib/libcockpit_test_a-mock-channel.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-mock-channel.o `test -f 'src/testlib/mock-channel.c' || echo '$(srcdir)/'`src/testlib/mock-channel.c
+
+src/testlib/libcockpit_test_a-mock-channel.obj: src/testlib/mock-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-mock-channel.obj -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-mock-channel.Tpo -c -o src/testlib/libcockpit_test_a-mock-channel.obj `if test -f 'src/testlib/mock-channel.c'; then $(CYGPATH_W) 'src/testlib/mock-channel.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/mock-channel.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-mock-channel.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-mock-channel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/mock-channel.c' object='src/testlib/libcockpit_test_a-mock-channel.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-mock-channel.obj `if test -f 'src/testlib/mock-channel.c'; then $(CYGPATH_W) 'src/testlib/mock-channel.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/mock-channel.c'; fi`
+
+src/testlib/libcockpit_test_a-mock-pressure.o: src/testlib/mock-pressure.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-mock-pressure.o -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-mock-pressure.Tpo -c -o src/testlib/libcockpit_test_a-mock-pressure.o `test -f 'src/testlib/mock-pressure.c' || echo '$(srcdir)/'`src/testlib/mock-pressure.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-mock-pressure.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-mock-pressure.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/mock-pressure.c' object='src/testlib/libcockpit_test_a-mock-pressure.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-mock-pressure.o `test -f 'src/testlib/mock-pressure.c' || echo '$(srcdir)/'`src/testlib/mock-pressure.c
+
+src/testlib/libcockpit_test_a-mock-pressure.obj: src/testlib/mock-pressure.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-mock-pressure.obj -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-mock-pressure.Tpo -c -o src/testlib/libcockpit_test_a-mock-pressure.obj `if test -f 'src/testlib/mock-pressure.c'; then $(CYGPATH_W) 'src/testlib/mock-pressure.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/mock-pressure.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-mock-pressure.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-mock-pressure.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/mock-pressure.c' object='src/testlib/libcockpit_test_a-mock-pressure.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-mock-pressure.obj `if test -f 'src/testlib/mock-pressure.c'; then $(CYGPATH_W) 'src/testlib/mock-pressure.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/mock-pressure.c'; fi`
+
+src/testlib/libcockpit_test_a-mock-transport.o: src/testlib/mock-transport.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-mock-transport.o -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-mock-transport.Tpo -c -o src/testlib/libcockpit_test_a-mock-transport.o `test -f 'src/testlib/mock-transport.c' || echo '$(srcdir)/'`src/testlib/mock-transport.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-mock-transport.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-mock-transport.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/mock-transport.c' object='src/testlib/libcockpit_test_a-mock-transport.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-mock-transport.o `test -f 'src/testlib/mock-transport.c' || echo '$(srcdir)/'`src/testlib/mock-transport.c
+
+src/testlib/libcockpit_test_a-mock-transport.obj: src/testlib/mock-transport.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-mock-transport.obj -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-mock-transport.Tpo -c -o src/testlib/libcockpit_test_a-mock-transport.obj `if test -f 'src/testlib/mock-transport.c'; then $(CYGPATH_W) 'src/testlib/mock-transport.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/mock-transport.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-mock-transport.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-mock-transport.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/mock-transport.c' object='src/testlib/libcockpit_test_a-mock-transport.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-mock-transport.obj `if test -f 'src/testlib/mock-transport.c'; then $(CYGPATH_W) 'src/testlib/mock-transport.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/mock-transport.c'; fi`
+
+src/testlib/libcockpit_test_a-retest.o: src/testlib/retest.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-retest.o -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-retest.Tpo -c -o src/testlib/libcockpit_test_a-retest.o `test -f 'src/testlib/retest.c' || echo '$(srcdir)/'`src/testlib/retest.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-retest.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-retest.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/retest.c' object='src/testlib/libcockpit_test_a-retest.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-retest.o `test -f 'src/testlib/retest.c' || echo '$(srcdir)/'`src/testlib/retest.c
+
+src/testlib/libcockpit_test_a-retest.obj: src/testlib/retest.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/testlib/libcockpit_test_a-retest.obj -MD -MP -MF src/testlib/$(DEPDIR)/libcockpit_test_a-retest.Tpo -c -o src/testlib/libcockpit_test_a-retest.obj `if test -f 'src/testlib/retest.c'; then $(CYGPATH_W) 'src/testlib/retest.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/retest.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/testlib/$(DEPDIR)/libcockpit_test_a-retest.Tpo src/testlib/$(DEPDIR)/libcockpit_test_a-retest.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/testlib/retest.c' object='src/testlib/libcockpit_test_a-retest.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_test_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/testlib/libcockpit_test_a-retest.obj `if test -f 'src/testlib/retest.c'; then $(CYGPATH_W) 'src/testlib/retest.c'; else $(CYGPATH_W) '$(srcdir)/src/testlib/retest.c'; fi`
+
+src/ws/libcockpit_ws_a-cockpithandlers.o: src/ws/cockpithandlers.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpithandlers.o -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpithandlers.Tpo -c -o src/ws/libcockpit_ws_a-cockpithandlers.o `test -f 'src/ws/cockpithandlers.c' || echo '$(srcdir)/'`src/ws/cockpithandlers.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpithandlers.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpithandlers.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpithandlers.c' object='src/ws/libcockpit_ws_a-cockpithandlers.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpithandlers.o `test -f 'src/ws/cockpithandlers.c' || echo '$(srcdir)/'`src/ws/cockpithandlers.c
+
+src/ws/libcockpit_ws_a-cockpithandlers.obj: src/ws/cockpithandlers.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpithandlers.obj -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpithandlers.Tpo -c -o src/ws/libcockpit_ws_a-cockpithandlers.obj `if test -f 'src/ws/cockpithandlers.c'; then $(CYGPATH_W) 'src/ws/cockpithandlers.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpithandlers.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpithandlers.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpithandlers.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpithandlers.c' object='src/ws/libcockpit_ws_a-cockpithandlers.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpithandlers.obj `if test -f 'src/ws/cockpithandlers.c'; then $(CYGPATH_W) 'src/ws/cockpithandlers.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpithandlers.c'; fi`
+
+src/ws/libcockpit_ws_a-cockpitauth.o: src/ws/cockpitauth.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitauth.o -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitauth.Tpo -c -o src/ws/libcockpit_ws_a-cockpitauth.o `test -f 'src/ws/cockpitauth.c' || echo '$(srcdir)/'`src/ws/cockpitauth.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitauth.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitauth.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitauth.c' object='src/ws/libcockpit_ws_a-cockpitauth.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitauth.o `test -f 'src/ws/cockpitauth.c' || echo '$(srcdir)/'`src/ws/cockpitauth.c
+
+src/ws/libcockpit_ws_a-cockpitauth.obj: src/ws/cockpitauth.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitauth.obj -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitauth.Tpo -c -o src/ws/libcockpit_ws_a-cockpitauth.obj `if test -f 'src/ws/cockpitauth.c'; then $(CYGPATH_W) 'src/ws/cockpitauth.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitauth.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitauth.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitauth.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitauth.c' object='src/ws/libcockpit_ws_a-cockpitauth.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitauth.obj `if test -f 'src/ws/cockpitauth.c'; then $(CYGPATH_W) 'src/ws/cockpitauth.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitauth.c'; fi`
+
+src/ws/libcockpit_ws_a-cockpitcompat.o: src/ws/cockpitcompat.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitcompat.o -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcompat.Tpo -c -o src/ws/libcockpit_ws_a-cockpitcompat.o `test -f 'src/ws/cockpitcompat.c' || echo '$(srcdir)/'`src/ws/cockpitcompat.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcompat.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcompat.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitcompat.c' object='src/ws/libcockpit_ws_a-cockpitcompat.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitcompat.o `test -f 'src/ws/cockpitcompat.c' || echo '$(srcdir)/'`src/ws/cockpitcompat.c
+
+src/ws/libcockpit_ws_a-cockpitcompat.obj: src/ws/cockpitcompat.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitcompat.obj -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcompat.Tpo -c -o src/ws/libcockpit_ws_a-cockpitcompat.obj `if test -f 'src/ws/cockpitcompat.c'; then $(CYGPATH_W) 'src/ws/cockpitcompat.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitcompat.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcompat.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcompat.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitcompat.c' object='src/ws/libcockpit_ws_a-cockpitcompat.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitcompat.obj `if test -f 'src/ws/cockpitcompat.c'; then $(CYGPATH_W) 'src/ws/cockpitcompat.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitcompat.c'; fi`
+
+src/ws/libcockpit_ws_a-cockpitbranding.o: src/ws/cockpitbranding.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitbranding.o -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitbranding.Tpo -c -o src/ws/libcockpit_ws_a-cockpitbranding.o `test -f 'src/ws/cockpitbranding.c' || echo '$(srcdir)/'`src/ws/cockpitbranding.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitbranding.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitbranding.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitbranding.c' object='src/ws/libcockpit_ws_a-cockpitbranding.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitbranding.o `test -f 'src/ws/cockpitbranding.c' || echo '$(srcdir)/'`src/ws/cockpitbranding.c
+
+src/ws/libcockpit_ws_a-cockpitbranding.obj: src/ws/cockpitbranding.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitbranding.obj -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitbranding.Tpo -c -o src/ws/libcockpit_ws_a-cockpitbranding.obj `if test -f 'src/ws/cockpitbranding.c'; then $(CYGPATH_W) 'src/ws/cockpitbranding.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitbranding.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitbranding.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitbranding.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitbranding.c' object='src/ws/libcockpit_ws_a-cockpitbranding.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitbranding.obj `if test -f 'src/ws/cockpitbranding.c'; then $(CYGPATH_W) 'src/ws/cockpitbranding.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitbranding.c'; fi`
+
+src/ws/libcockpit_ws_a-cockpitchannelresponse.o: src/ws/cockpitchannelresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitchannelresponse.o -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelresponse.Tpo -c -o src/ws/libcockpit_ws_a-cockpitchannelresponse.o `test -f 'src/ws/cockpitchannelresponse.c' || echo '$(srcdir)/'`src/ws/cockpitchannelresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelresponse.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelresponse.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitchannelresponse.c' object='src/ws/libcockpit_ws_a-cockpitchannelresponse.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitchannelresponse.o `test -f 'src/ws/cockpitchannelresponse.c' || echo '$(srcdir)/'`src/ws/cockpitchannelresponse.c
+
+src/ws/libcockpit_ws_a-cockpitchannelresponse.obj: src/ws/cockpitchannelresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitchannelresponse.obj -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelresponse.Tpo -c -o src/ws/libcockpit_ws_a-cockpitchannelresponse.obj `if test -f 'src/ws/cockpitchannelresponse.c'; then $(CYGPATH_W) 'src/ws/cockpitchannelresponse.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitchannelresponse.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelresponse.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelresponse.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitchannelresponse.c' object='src/ws/libcockpit_ws_a-cockpitchannelresponse.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitchannelresponse.obj `if test -f 'src/ws/cockpitchannelresponse.c'; then $(CYGPATH_W) 'src/ws/cockpitchannelresponse.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitchannelresponse.c'; fi`
+
+src/ws/libcockpit_ws_a-cockpitchannelsocket.o: src/ws/cockpitchannelsocket.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitchannelsocket.o -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelsocket.Tpo -c -o src/ws/libcockpit_ws_a-cockpitchannelsocket.o `test -f 'src/ws/cockpitchannelsocket.c' || echo '$(srcdir)/'`src/ws/cockpitchannelsocket.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelsocket.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelsocket.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitchannelsocket.c' object='src/ws/libcockpit_ws_a-cockpitchannelsocket.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitchannelsocket.o `test -f 'src/ws/cockpitchannelsocket.c' || echo '$(srcdir)/'`src/ws/cockpitchannelsocket.c
+
+src/ws/libcockpit_ws_a-cockpitchannelsocket.obj: src/ws/cockpitchannelsocket.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitchannelsocket.obj -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelsocket.Tpo -c -o src/ws/libcockpit_ws_a-cockpitchannelsocket.obj `if test -f 'src/ws/cockpitchannelsocket.c'; then $(CYGPATH_W) 'src/ws/cockpitchannelsocket.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitchannelsocket.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelsocket.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelsocket.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitchannelsocket.c' object='src/ws/libcockpit_ws_a-cockpitchannelsocket.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitchannelsocket.obj `if test -f 'src/ws/cockpitchannelsocket.c'; then $(CYGPATH_W) 'src/ws/cockpitchannelsocket.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitchannelsocket.c'; fi`
+
+src/ws/libcockpit_ws_a-cockpitcreds.o: src/ws/cockpitcreds.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitcreds.o -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcreds.Tpo -c -o src/ws/libcockpit_ws_a-cockpitcreds.o `test -f 'src/ws/cockpitcreds.c' || echo '$(srcdir)/'`src/ws/cockpitcreds.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcreds.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcreds.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitcreds.c' object='src/ws/libcockpit_ws_a-cockpitcreds.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitcreds.o `test -f 'src/ws/cockpitcreds.c' || echo '$(srcdir)/'`src/ws/cockpitcreds.c
+
+src/ws/libcockpit_ws_a-cockpitcreds.obj: src/ws/cockpitcreds.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitcreds.obj -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcreds.Tpo -c -o src/ws/libcockpit_ws_a-cockpitcreds.obj `if test -f 'src/ws/cockpitcreds.c'; then $(CYGPATH_W) 'src/ws/cockpitcreds.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitcreds.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcreds.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcreds.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitcreds.c' object='src/ws/libcockpit_ws_a-cockpitcreds.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitcreds.obj `if test -f 'src/ws/cockpitcreds.c'; then $(CYGPATH_W) 'src/ws/cockpitcreds.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitcreds.c'; fi`
+
+src/ws/libcockpit_ws_a-cockpitwebservice.o: src/ws/cockpitwebservice.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitwebservice.o -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitwebservice.Tpo -c -o src/ws/libcockpit_ws_a-cockpitwebservice.o `test -f 'src/ws/cockpitwebservice.c' || echo '$(srcdir)/'`src/ws/cockpitwebservice.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitwebservice.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitwebservice.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitwebservice.c' object='src/ws/libcockpit_ws_a-cockpitwebservice.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitwebservice.o `test -f 'src/ws/cockpitwebservice.c' || echo '$(srcdir)/'`src/ws/cockpitwebservice.c
+
+src/ws/libcockpit_ws_a-cockpitwebservice.obj: src/ws/cockpitwebservice.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/libcockpit_ws_a-cockpitwebservice.obj -MD -MP -MF src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitwebservice.Tpo -c -o src/ws/libcockpit_ws_a-cockpitwebservice.obj `if test -f 'src/ws/cockpitwebservice.c'; then $(CYGPATH_W) 'src/ws/cockpitwebservice.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitwebservice.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitwebservice.Tpo src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitwebservice.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/cockpitwebservice.c' object='src/ws/libcockpit_ws_a-cockpitwebservice.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcockpit_ws_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/libcockpit_ws_a-cockpitwebservice.obj `if test -f 'src/ws/cockpitwebservice.c'; then $(CYGPATH_W) 'src/ws/cockpitwebservice.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/cockpitwebservice.c'; fi`
+
+src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.o: src/pam-ssh-add/pam-ssh-add.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpam_ssh_add_a_CFLAGS) $(CFLAGS) -MT src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.o -MD -MP -MF src/pam-ssh-add/$(DEPDIR)/libpam_ssh_add_a-pam-ssh-add.Tpo -c -o src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.o `test -f 'src/pam-ssh-add/pam-ssh-add.c' || echo '$(srcdir)/'`src/pam-ssh-add/pam-ssh-add.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/pam-ssh-add/$(DEPDIR)/libpam_ssh_add_a-pam-ssh-add.Tpo src/pam-ssh-add/$(DEPDIR)/libpam_ssh_add_a-pam-ssh-add.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/pam-ssh-add/pam-ssh-add.c' object='src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpam_ssh_add_a_CFLAGS) $(CFLAGS) -c -o src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.o `test -f 'src/pam-ssh-add/pam-ssh-add.c' || echo '$(srcdir)/'`src/pam-ssh-add/pam-ssh-add.c
+
+src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.obj: src/pam-ssh-add/pam-ssh-add.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpam_ssh_add_a_CFLAGS) $(CFLAGS) -MT src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.obj -MD -MP -MF src/pam-ssh-add/$(DEPDIR)/libpam_ssh_add_a-pam-ssh-add.Tpo -c -o src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.obj `if test -f 'src/pam-ssh-add/pam-ssh-add.c'; then $(CYGPATH_W) 'src/pam-ssh-add/pam-ssh-add.c'; else $(CYGPATH_W) '$(srcdir)/src/pam-ssh-add/pam-ssh-add.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/pam-ssh-add/$(DEPDIR)/libpam_ssh_add_a-pam-ssh-add.Tpo src/pam-ssh-add/$(DEPDIR)/libpam_ssh_add_a-pam-ssh-add.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/pam-ssh-add/pam-ssh-add.c' object='src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpam_ssh_add_a_CFLAGS) $(CFLAGS) -c -o src/pam-ssh-add/libpam_ssh_add_a-pam-ssh-add.obj `if test -f 'src/pam-ssh-add/pam-ssh-add.c'; then $(CYGPATH_W) 'src/pam-ssh-add/pam-ssh-add.c'; else $(CYGPATH_W) '$(srcdir)/src/pam-ssh-add/pam-ssh-add.c'; fi`
+
+src/websocket/libwebsocket_a-websocket.o: src/websocket/websocket.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/libwebsocket_a-websocket.o -MD -MP -MF src/websocket/$(DEPDIR)/libwebsocket_a-websocket.Tpo -c -o src/websocket/libwebsocket_a-websocket.o `test -f 'src/websocket/websocket.c' || echo '$(srcdir)/'`src/websocket/websocket.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/libwebsocket_a-websocket.Tpo src/websocket/$(DEPDIR)/libwebsocket_a-websocket.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/websocket.c' object='src/websocket/libwebsocket_a-websocket.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/libwebsocket_a-websocket.o `test -f 'src/websocket/websocket.c' || echo '$(srcdir)/'`src/websocket/websocket.c
+
+src/websocket/libwebsocket_a-websocket.obj: src/websocket/websocket.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/libwebsocket_a-websocket.obj -MD -MP -MF src/websocket/$(DEPDIR)/libwebsocket_a-websocket.Tpo -c -o src/websocket/libwebsocket_a-websocket.obj `if test -f 'src/websocket/websocket.c'; then $(CYGPATH_W) 'src/websocket/websocket.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/websocket.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/libwebsocket_a-websocket.Tpo src/websocket/$(DEPDIR)/libwebsocket_a-websocket.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/websocket.c' object='src/websocket/libwebsocket_a-websocket.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/libwebsocket_a-websocket.obj `if test -f 'src/websocket/websocket.c'; then $(CYGPATH_W) 'src/websocket/websocket.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/websocket.c'; fi`
+
+src/websocket/libwebsocket_a-websocketclient.o: src/websocket/websocketclient.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/libwebsocket_a-websocketclient.o -MD -MP -MF src/websocket/$(DEPDIR)/libwebsocket_a-websocketclient.Tpo -c -o src/websocket/libwebsocket_a-websocketclient.o `test -f 'src/websocket/websocketclient.c' || echo '$(srcdir)/'`src/websocket/websocketclient.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/libwebsocket_a-websocketclient.Tpo src/websocket/$(DEPDIR)/libwebsocket_a-websocketclient.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/websocketclient.c' object='src/websocket/libwebsocket_a-websocketclient.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/libwebsocket_a-websocketclient.o `test -f 'src/websocket/websocketclient.c' || echo '$(srcdir)/'`src/websocket/websocketclient.c
+
+src/websocket/libwebsocket_a-websocketclient.obj: src/websocket/websocketclient.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/libwebsocket_a-websocketclient.obj -MD -MP -MF src/websocket/$(DEPDIR)/libwebsocket_a-websocketclient.Tpo -c -o src/websocket/libwebsocket_a-websocketclient.obj `if test -f 'src/websocket/websocketclient.c'; then $(CYGPATH_W) 'src/websocket/websocketclient.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/websocketclient.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/libwebsocket_a-websocketclient.Tpo src/websocket/$(DEPDIR)/libwebsocket_a-websocketclient.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/websocketclient.c' object='src/websocket/libwebsocket_a-websocketclient.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/libwebsocket_a-websocketclient.obj `if test -f 'src/websocket/websocketclient.c'; then $(CYGPATH_W) 'src/websocket/websocketclient.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/websocketclient.c'; fi`
+
+src/websocket/libwebsocket_a-websocketserver.o: src/websocket/websocketserver.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/libwebsocket_a-websocketserver.o -MD -MP -MF src/websocket/$(DEPDIR)/libwebsocket_a-websocketserver.Tpo -c -o src/websocket/libwebsocket_a-websocketserver.o `test -f 'src/websocket/websocketserver.c' || echo '$(srcdir)/'`src/websocket/websocketserver.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/libwebsocket_a-websocketserver.Tpo src/websocket/$(DEPDIR)/libwebsocket_a-websocketserver.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/websocketserver.c' object='src/websocket/libwebsocket_a-websocketserver.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/libwebsocket_a-websocketserver.o `test -f 'src/websocket/websocketserver.c' || echo '$(srcdir)/'`src/websocket/websocketserver.c
+
+src/websocket/libwebsocket_a-websocketserver.obj: src/websocket/websocketserver.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/libwebsocket_a-websocketserver.obj -MD -MP -MF src/websocket/$(DEPDIR)/libwebsocket_a-websocketserver.Tpo -c -o src/websocket/libwebsocket_a-websocketserver.obj `if test -f 'src/websocket/websocketserver.c'; then $(CYGPATH_W) 'src/websocket/websocketserver.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/websocketserver.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/libwebsocket_a-websocketserver.Tpo src/websocket/$(DEPDIR)/libwebsocket_a-websocketserver.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/websocketserver.c' object='src/websocket/libwebsocket_a-websocketserver.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/libwebsocket_a-websocketserver.obj `if test -f 'src/websocket/websocketserver.c'; then $(CYGPATH_W) 'src/websocket/websocketserver.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/websocketserver.c'; fi`
+
+src/websocket/libwebsocket_a-websocketconnection.o: src/websocket/websocketconnection.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/libwebsocket_a-websocketconnection.o -MD -MP -MF src/websocket/$(DEPDIR)/libwebsocket_a-websocketconnection.Tpo -c -o src/websocket/libwebsocket_a-websocketconnection.o `test -f 'src/websocket/websocketconnection.c' || echo '$(srcdir)/'`src/websocket/websocketconnection.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/libwebsocket_a-websocketconnection.Tpo src/websocket/$(DEPDIR)/libwebsocket_a-websocketconnection.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/websocketconnection.c' object='src/websocket/libwebsocket_a-websocketconnection.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/libwebsocket_a-websocketconnection.o `test -f 'src/websocket/websocketconnection.c' || echo '$(srcdir)/'`src/websocket/websocketconnection.c
+
+src/websocket/libwebsocket_a-websocketconnection.obj: src/websocket/websocketconnection.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/libwebsocket_a-websocketconnection.obj -MD -MP -MF src/websocket/$(DEPDIR)/libwebsocket_a-websocketconnection.Tpo -c -o src/websocket/libwebsocket_a-websocketconnection.obj `if test -f 'src/websocket/websocketconnection.c'; then $(CYGPATH_W) 'src/websocket/websocketconnection.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/websocketconnection.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/libwebsocket_a-websocketconnection.Tpo src/websocket/$(DEPDIR)/libwebsocket_a-websocketconnection.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/websocketconnection.c' object='src/websocket/libwebsocket_a-websocketconnection.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libwebsocket_a_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/libwebsocket_a-websocketconnection.obj `if test -f 'src/websocket/websocketconnection.c'; then $(CYGPATH_W) 'src/websocket/websocketconnection.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/websocketconnection.c'; fi`
+
+src/bridge/cockpit_askpass-askpass.o: src/bridge/askpass.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_askpass_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/cockpit_askpass-askpass.o -MD -MP -MF src/bridge/$(DEPDIR)/cockpit_askpass-askpass.Tpo -c -o src/bridge/cockpit_askpass-askpass.o `test -f 'src/bridge/askpass.c' || echo '$(srcdir)/'`src/bridge/askpass.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/cockpit_askpass-askpass.Tpo src/bridge/$(DEPDIR)/cockpit_askpass-askpass.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/askpass.c' object='src/bridge/cockpit_askpass-askpass.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_askpass_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/cockpit_askpass-askpass.o `test -f 'src/bridge/askpass.c' || echo '$(srcdir)/'`src/bridge/askpass.c
+
+src/bridge/cockpit_askpass-askpass.obj: src/bridge/askpass.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_askpass_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/cockpit_askpass-askpass.obj -MD -MP -MF src/bridge/$(DEPDIR)/cockpit_askpass-askpass.Tpo -c -o src/bridge/cockpit_askpass-askpass.obj `if test -f 'src/bridge/askpass.c'; then $(CYGPATH_W) 'src/bridge/askpass.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/askpass.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/cockpit_askpass-askpass.Tpo src/bridge/$(DEPDIR)/cockpit_askpass-askpass.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/askpass.c' object='src/bridge/cockpit_askpass-askpass.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_askpass_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/cockpit_askpass-askpass.obj `if test -f 'src/bridge/askpass.c'; then $(CYGPATH_W) 'src/bridge/askpass.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/askpass.c'; fi`
+
+src/bridge/cockpit_bridge-bridge.o: src/bridge/bridge.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/cockpit_bridge-bridge.o -MD -MP -MF src/bridge/$(DEPDIR)/cockpit_bridge-bridge.Tpo -c -o src/bridge/cockpit_bridge-bridge.o `test -f 'src/bridge/bridge.c' || echo '$(srcdir)/'`src/bridge/bridge.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/cockpit_bridge-bridge.Tpo src/bridge/$(DEPDIR)/cockpit_bridge-bridge.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/bridge.c' object='src/bridge/cockpit_bridge-bridge.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/cockpit_bridge-bridge.o `test -f 'src/bridge/bridge.c' || echo '$(srcdir)/'`src/bridge/bridge.c
+
+src/bridge/cockpit_bridge-bridge.obj: src/bridge/bridge.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/cockpit_bridge-bridge.obj -MD -MP -MF src/bridge/$(DEPDIR)/cockpit_bridge-bridge.Tpo -c -o src/bridge/cockpit_bridge-bridge.obj `if test -f 'src/bridge/bridge.c'; then $(CYGPATH_W) 'src/bridge/bridge.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/bridge.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/cockpit_bridge-bridge.Tpo src/bridge/$(DEPDIR)/cockpit_bridge-bridge.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/bridge.c' object='src/bridge/cockpit_bridge-bridge.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/cockpit_bridge-bridge.obj `if test -f 'src/bridge/bridge.c'; then $(CYGPATH_W) 'src/bridge/bridge.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/bridge.c'; fi`
+
+src/bridge/cockpit_pcp-cockpitpcp.o: src/bridge/cockpitpcp.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_pcp_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/cockpit_pcp-cockpitpcp.o -MD -MP -MF src/bridge/$(DEPDIR)/cockpit_pcp-cockpitpcp.Tpo -c -o src/bridge/cockpit_pcp-cockpitpcp.o `test -f 'src/bridge/cockpitpcp.c' || echo '$(srcdir)/'`src/bridge/cockpitpcp.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/cockpit_pcp-cockpitpcp.Tpo src/bridge/$(DEPDIR)/cockpit_pcp-cockpitpcp.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpcp.c' object='src/bridge/cockpit_pcp-cockpitpcp.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_pcp_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/cockpit_pcp-cockpitpcp.o `test -f 'src/bridge/cockpitpcp.c' || echo '$(srcdir)/'`src/bridge/cockpitpcp.c
+
+src/bridge/cockpit_pcp-cockpitpcp.obj: src/bridge/cockpitpcp.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_pcp_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/cockpit_pcp-cockpitpcp.obj -MD -MP -MF src/bridge/$(DEPDIR)/cockpit_pcp-cockpitpcp.Tpo -c -o src/bridge/cockpit_pcp-cockpitpcp.obj `if test -f 'src/bridge/cockpitpcp.c'; then $(CYGPATH_W) 'src/bridge/cockpitpcp.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpcp.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/cockpit_pcp-cockpitpcp.Tpo src/bridge/$(DEPDIR)/cockpit_pcp-cockpitpcp.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/cockpitpcp.c' object='src/bridge/cockpit_pcp-cockpitpcp.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_pcp_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/cockpit_pcp-cockpitpcp.obj `if test -f 'src/bridge/cockpitpcp.c'; then $(CYGPATH_W) 'src/bridge/cockpitpcp.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/cockpitpcp.c'; fi`
+
+src/ssh/cockpit_ssh-ssh.o: src/ssh/ssh.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_ssh_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/cockpit_ssh-ssh.o -MD -MP -MF src/ssh/$(DEPDIR)/cockpit_ssh-ssh.Tpo -c -o src/ssh/cockpit_ssh-ssh.o `test -f 'src/ssh/ssh.c' || echo '$(srcdir)/'`src/ssh/ssh.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/cockpit_ssh-ssh.Tpo src/ssh/$(DEPDIR)/cockpit_ssh-ssh.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/ssh.c' object='src/ssh/cockpit_ssh-ssh.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_ssh_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/cockpit_ssh-ssh.o `test -f 'src/ssh/ssh.c' || echo '$(srcdir)/'`src/ssh/ssh.c
+
+src/ssh/cockpit_ssh-ssh.obj: src/ssh/ssh.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_ssh_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/cockpit_ssh-ssh.obj -MD -MP -MF src/ssh/$(DEPDIR)/cockpit_ssh-ssh.Tpo -c -o src/ssh/cockpit_ssh-ssh.obj `if test -f 'src/ssh/ssh.c'; then $(CYGPATH_W) 'src/ssh/ssh.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/ssh.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/cockpit_ssh-ssh.Tpo src/ssh/$(DEPDIR)/cockpit_ssh-ssh.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/ssh.c' object='src/ssh/cockpit_ssh-ssh.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_ssh_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/cockpit_ssh-ssh.obj `if test -f 'src/ssh/ssh.c'; then $(CYGPATH_W) 'src/ssh/ssh.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/ssh.c'; fi`
+
+src/ws/cockpit_ws-main.o: src/ws/main.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_ws_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/cockpit_ws-main.o -MD -MP -MF src/ws/$(DEPDIR)/cockpit_ws-main.Tpo -c -o src/ws/cockpit_ws-main.o `test -f 'src/ws/main.c' || echo '$(srcdir)/'`src/ws/main.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/cockpit_ws-main.Tpo src/ws/$(DEPDIR)/cockpit_ws-main.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/main.c' object='src/ws/cockpit_ws-main.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_ws_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/cockpit_ws-main.o `test -f 'src/ws/main.c' || echo '$(srcdir)/'`src/ws/main.c
+
+src/ws/cockpit_ws-main.obj: src/ws/main.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_ws_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/cockpit_ws-main.obj -MD -MP -MF src/ws/$(DEPDIR)/cockpit_ws-main.Tpo -c -o src/ws/cockpit_ws-main.obj `if test -f 'src/ws/main.c'; then $(CYGPATH_W) 'src/ws/main.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/main.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/cockpit_ws-main.Tpo src/ws/$(DEPDIR)/cockpit_ws-main.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/main.c' object='src/ws/cockpit_ws-main.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_ws_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/cockpit_ws-main.obj `if test -f 'src/ws/main.c'; then $(CYGPATH_W) 'src/ws/main.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/main.c'; fi`
+
+src/tls/cockpit_wsinstance_factory-wsinstance-factory.o: src/tls/wsinstance-factory.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_wsinstance_factory_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/tls/cockpit_wsinstance_factory-wsinstance-factory.o -MD -MP -MF src/tls/$(DEPDIR)/cockpit_wsinstance_factory-wsinstance-factory.Tpo -c -o src/tls/cockpit_wsinstance_factory-wsinstance-factory.o `test -f 'src/tls/wsinstance-factory.c' || echo '$(srcdir)/'`src/tls/wsinstance-factory.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/tls/$(DEPDIR)/cockpit_wsinstance_factory-wsinstance-factory.Tpo src/tls/$(DEPDIR)/cockpit_wsinstance_factory-wsinstance-factory.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/tls/wsinstance-factory.c' object='src/tls/cockpit_wsinstance_factory-wsinstance-factory.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_wsinstance_factory_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/tls/cockpit_wsinstance_factory-wsinstance-factory.o `test -f 'src/tls/wsinstance-factory.c' || echo '$(srcdir)/'`src/tls/wsinstance-factory.c
+
+src/tls/cockpit_wsinstance_factory-wsinstance-factory.obj: src/tls/wsinstance-factory.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_wsinstance_factory_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/tls/cockpit_wsinstance_factory-wsinstance-factory.obj -MD -MP -MF src/tls/$(DEPDIR)/cockpit_wsinstance_factory-wsinstance-factory.Tpo -c -o src/tls/cockpit_wsinstance_factory-wsinstance-factory.obj `if test -f 'src/tls/wsinstance-factory.c'; then $(CYGPATH_W) 'src/tls/wsinstance-factory.c'; else $(CYGPATH_W) '$(srcdir)/src/tls/wsinstance-factory.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/tls/$(DEPDIR)/cockpit_wsinstance_factory-wsinstance-factory.Tpo src/tls/$(DEPDIR)/cockpit_wsinstance_factory-wsinstance-factory.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/tls/wsinstance-factory.c' object='src/tls/cockpit_wsinstance_factory-wsinstance-factory.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(cockpit_wsinstance_factory_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/tls/cockpit_wsinstance_factory-wsinstance-factory.obj `if test -f 'src/tls/wsinstance-factory.c'; then $(CYGPATH_W) 'src/tls/wsinstance-factory.c'; else $(CYGPATH_W) '$(srcdir)/src/tls/wsinstance-factory.c'; fi`
+
+src/websocket/frob_websocket-frob-websocket.o: src/websocket/frob-websocket.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(frob_websocket_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/frob_websocket-frob-websocket.o -MD -MP -MF src/websocket/$(DEPDIR)/frob_websocket-frob-websocket.Tpo -c -o src/websocket/frob_websocket-frob-websocket.o `test -f 'src/websocket/frob-websocket.c' || echo '$(srcdir)/'`src/websocket/frob-websocket.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/frob_websocket-frob-websocket.Tpo src/websocket/$(DEPDIR)/frob_websocket-frob-websocket.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/frob-websocket.c' object='src/websocket/frob_websocket-frob-websocket.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(frob_websocket_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/frob_websocket-frob-websocket.o `test -f 'src/websocket/frob-websocket.c' || echo '$(srcdir)/'`src/websocket/frob-websocket.c
+
+src/websocket/frob_websocket-frob-websocket.obj: src/websocket/frob-websocket.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(frob_websocket_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/frob_websocket-frob-websocket.obj -MD -MP -MF src/websocket/$(DEPDIR)/frob_websocket-frob-websocket.Tpo -c -o src/websocket/frob_websocket-frob-websocket.obj `if test -f 'src/websocket/frob-websocket.c'; then $(CYGPATH_W) 'src/websocket/frob-websocket.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/frob-websocket.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/frob_websocket-frob-websocket.Tpo src/websocket/$(DEPDIR)/frob_websocket-frob-websocket.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/frob-websocket.c' object='src/websocket/frob_websocket-frob-websocket.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(frob_websocket_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/frob_websocket-frob-websocket.obj `if test -f 'src/websocket/frob-websocket.c'; then $(CYGPATH_W) 'src/websocket/frob-websocket.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/frob-websocket.c'; fi`
+
+src/common/libpreload_temp_home_so-preload-temp-home.o: src/common/preload-temp-home.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpreload_temp_home_so_CFLAGS) $(CFLAGS) -MT src/common/libpreload_temp_home_so-preload-temp-home.o -MD -MP -MF src/common/$(DEPDIR)/libpreload_temp_home_so-preload-temp-home.Tpo -c -o src/common/libpreload_temp_home_so-preload-temp-home.o `test -f 'src/common/preload-temp-home.c' || echo '$(srcdir)/'`src/common/preload-temp-home.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libpreload_temp_home_so-preload-temp-home.Tpo src/common/$(DEPDIR)/libpreload_temp_home_so-preload-temp-home.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/preload-temp-home.c' object='src/common/libpreload_temp_home_so-preload-temp-home.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpreload_temp_home_so_CFLAGS) $(CFLAGS) -c -o src/common/libpreload_temp_home_so-preload-temp-home.o `test -f 'src/common/preload-temp-home.c' || echo '$(srcdir)/'`src/common/preload-temp-home.c
+
+src/common/libpreload_temp_home_so-preload-temp-home.obj: src/common/preload-temp-home.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpreload_temp_home_so_CFLAGS) $(CFLAGS) -MT src/common/libpreload_temp_home_so-preload-temp-home.obj -MD -MP -MF src/common/$(DEPDIR)/libpreload_temp_home_so-preload-temp-home.Tpo -c -o src/common/libpreload_temp_home_so-preload-temp-home.obj `if test -f 'src/common/preload-temp-home.c'; then $(CYGPATH_W) 'src/common/preload-temp-home.c'; else $(CYGPATH_W) '$(srcdir)/src/common/preload-temp-home.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/libpreload_temp_home_so-preload-temp-home.Tpo src/common/$(DEPDIR)/libpreload_temp_home_so-preload-temp-home.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/preload-temp-home.c' object='src/common/libpreload_temp_home_so-preload-temp-home.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(libpreload_temp_home_so_CFLAGS) $(CFLAGS) -c -o src/common/libpreload_temp_home_so-preload-temp-home.obj `if test -f 'src/common/preload-temp-home.c'; then $(CYGPATH_W) 'src/common/preload-temp-home.c'; else $(CYGPATH_W) '$(srcdir)/src/common/preload-temp-home.c'; fi`
+
+src/ws/mock_auth_command-mock-auth-command.o: src/ws/mock-auth-command.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_auth_command_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/mock_auth_command-mock-auth-command.o -MD -MP -MF src/ws/$(DEPDIR)/mock_auth_command-mock-auth-command.Tpo -c -o src/ws/mock_auth_command-mock-auth-command.o `test -f 'src/ws/mock-auth-command.c' || echo '$(srcdir)/'`src/ws/mock-auth-command.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/mock_auth_command-mock-auth-command.Tpo src/ws/$(DEPDIR)/mock_auth_command-mock-auth-command.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/mock-auth-command.c' object='src/ws/mock_auth_command-mock-auth-command.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_auth_command_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/mock_auth_command-mock-auth-command.o `test -f 'src/ws/mock-auth-command.c' || echo '$(srcdir)/'`src/ws/mock-auth-command.c
+
+src/ws/mock_auth_command-mock-auth-command.obj: src/ws/mock-auth-command.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_auth_command_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/mock_auth_command-mock-auth-command.obj -MD -MP -MF src/ws/$(DEPDIR)/mock_auth_command-mock-auth-command.Tpo -c -o src/ws/mock_auth_command-mock-auth-command.obj `if test -f 'src/ws/mock-auth-command.c'; then $(CYGPATH_W) 'src/ws/mock-auth-command.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/mock-auth-command.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/mock_auth_command-mock-auth-command.Tpo src/ws/$(DEPDIR)/mock_auth_command-mock-auth-command.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/mock-auth-command.c' object='src/ws/mock_auth_command-mock-auth-command.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_auth_command_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/mock_auth_command-mock-auth-command.obj `if test -f 'src/ws/mock-auth-command.c'; then $(CYGPATH_W) 'src/ws/mock-auth-command.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/mock-auth-command.c'; fi`
+
+src/bridge/mock_bridge-mock-bridge.o: src/bridge/mock-bridge.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/mock_bridge-mock-bridge.o -MD -MP -MF src/bridge/$(DEPDIR)/mock_bridge-mock-bridge.Tpo -c -o src/bridge/mock_bridge-mock-bridge.o `test -f 'src/bridge/mock-bridge.c' || echo '$(srcdir)/'`src/bridge/mock-bridge.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/mock_bridge-mock-bridge.Tpo src/bridge/$(DEPDIR)/mock_bridge-mock-bridge.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/mock-bridge.c' object='src/bridge/mock_bridge-mock-bridge.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/mock_bridge-mock-bridge.o `test -f 'src/bridge/mock-bridge.c' || echo '$(srcdir)/'`src/bridge/mock-bridge.c
+
+src/bridge/mock_bridge-mock-bridge.obj: src/bridge/mock-bridge.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/mock_bridge-mock-bridge.obj -MD -MP -MF src/bridge/$(DEPDIR)/mock_bridge-mock-bridge.Tpo -c -o src/bridge/mock_bridge-mock-bridge.obj `if test -f 'src/bridge/mock-bridge.c'; then $(CYGPATH_W) 'src/bridge/mock-bridge.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/mock-bridge.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/mock_bridge-mock-bridge.Tpo src/bridge/$(DEPDIR)/mock_bridge-mock-bridge.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/mock-bridge.c' object='src/bridge/mock_bridge-mock-bridge.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/mock_bridge-mock-bridge.obj `if test -f 'src/bridge/mock-bridge.c'; then $(CYGPATH_W) 'src/bridge/mock-bridge.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/mock-bridge.c'; fi`
+
+src/ws/mock_echo-mock-echo.o: src/ws/mock-echo.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_echo_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/mock_echo-mock-echo.o -MD -MP -MF src/ws/$(DEPDIR)/mock_echo-mock-echo.Tpo -c -o src/ws/mock_echo-mock-echo.o `test -f 'src/ws/mock-echo.c' || echo '$(srcdir)/'`src/ws/mock-echo.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/mock_echo-mock-echo.Tpo src/ws/$(DEPDIR)/mock_echo-mock-echo.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/mock-echo.c' object='src/ws/mock_echo-mock-echo.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_echo_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/mock_echo-mock-echo.o `test -f 'src/ws/mock-echo.c' || echo '$(srcdir)/'`src/ws/mock-echo.c
+
+src/ws/mock_echo-mock-echo.obj: src/ws/mock-echo.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_echo_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/mock_echo-mock-echo.obj -MD -MP -MF src/ws/$(DEPDIR)/mock_echo-mock-echo.Tpo -c -o src/ws/mock_echo-mock-echo.obj `if test -f 'src/ws/mock-echo.c'; then $(CYGPATH_W) 'src/ws/mock-echo.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/mock-echo.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/mock_echo-mock-echo.Tpo src/ws/$(DEPDIR)/mock_echo-mock-echo.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/mock-echo.c' object='src/ws/mock_echo-mock-echo.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_echo_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/mock_echo-mock-echo.obj `if test -f 'src/ws/mock-echo.c'; then $(CYGPATH_W) 'src/ws/mock-echo.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/mock-echo.c'; fi`
+
+src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.o: src/ws/mock-pam-conv-mod.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(mock_pam_conv_mod_so_CFLAGS) $(CFLAGS) -MT src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.o -MD -MP -MF src/ws/$(DEPDIR)/mock_pam_conv_mod_so-mock-pam-conv-mod.Tpo -c -o src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.o `test -f 'src/ws/mock-pam-conv-mod.c' || echo '$(srcdir)/'`src/ws/mock-pam-conv-mod.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/mock_pam_conv_mod_so-mock-pam-conv-mod.Tpo src/ws/$(DEPDIR)/mock_pam_conv_mod_so-mock-pam-conv-mod.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/mock-pam-conv-mod.c' object='src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(mock_pam_conv_mod_so_CFLAGS) $(CFLAGS) -c -o src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.o `test -f 'src/ws/mock-pam-conv-mod.c' || echo '$(srcdir)/'`src/ws/mock-pam-conv-mod.c
+
+src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.obj: src/ws/mock-pam-conv-mod.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(mock_pam_conv_mod_so_CFLAGS) $(CFLAGS) -MT src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.obj -MD -MP -MF src/ws/$(DEPDIR)/mock_pam_conv_mod_so-mock-pam-conv-mod.Tpo -c -o src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.obj `if test -f 'src/ws/mock-pam-conv-mod.c'; then $(CYGPATH_W) 'src/ws/mock-pam-conv-mod.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/mock-pam-conv-mod.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/mock_pam_conv_mod_so-mock-pam-conv-mod.Tpo src/ws/$(DEPDIR)/mock_pam_conv_mod_so-mock-pam-conv-mod.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/mock-pam-conv-mod.c' object='src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(mock_pam_conv_mod_so_CFLAGS) $(CFLAGS) -c -o src/ws/mock_pam_conv_mod_so-mock-pam-conv-mod.obj `if test -f 'src/ws/mock-pam-conv-mod.c'; then $(CYGPATH_W) 'src/ws/mock-pam-conv-mod.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/mock-pam-conv-mod.c'; fi`
+
+src/bridge/mock_pmda_so-mock-pmda.o: src/bridge/mock-pmda.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(mock_pmda_so_CFLAGS) $(CFLAGS) -MT src/bridge/mock_pmda_so-mock-pmda.o -MD -MP -MF src/bridge/$(DEPDIR)/mock_pmda_so-mock-pmda.Tpo -c -o src/bridge/mock_pmda_so-mock-pmda.o `test -f 'src/bridge/mock-pmda.c' || echo '$(srcdir)/'`src/bridge/mock-pmda.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/mock_pmda_so-mock-pmda.Tpo src/bridge/$(DEPDIR)/mock_pmda_so-mock-pmda.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/mock-pmda.c' object='src/bridge/mock_pmda_so-mock-pmda.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(mock_pmda_so_CFLAGS) $(CFLAGS) -c -o src/bridge/mock_pmda_so-mock-pmda.o `test -f 'src/bridge/mock-pmda.c' || echo '$(srcdir)/'`src/bridge/mock-pmda.c
+
+src/bridge/mock_pmda_so-mock-pmda.obj: src/bridge/mock-pmda.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(mock_pmda_so_CFLAGS) $(CFLAGS) -MT src/bridge/mock_pmda_so-mock-pmda.obj -MD -MP -MF src/bridge/$(DEPDIR)/mock_pmda_so-mock-pmda.Tpo -c -o src/bridge/mock_pmda_so-mock-pmda.obj `if test -f 'src/bridge/mock-pmda.c'; then $(CYGPATH_W) 'src/bridge/mock-pmda.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/mock-pmda.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/mock_pmda_so-mock-pmda.Tpo src/bridge/$(DEPDIR)/mock_pmda_so-mock-pmda.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/mock-pmda.c' object='src/bridge/mock_pmda_so-mock-pmda.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(mock_pmda_so_CFLAGS) $(CFLAGS) -c -o src/bridge/mock_pmda_so-mock-pmda.obj `if test -f 'src/bridge/mock-pmda.c'; then $(CYGPATH_W) 'src/bridge/mock-pmda.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/mock-pmda.c'; fi`
+
+src/ssh/mock_sshd-mock-sshd.o: src/ssh/mock-sshd.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_sshd_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/mock_sshd-mock-sshd.o -MD -MP -MF src/ssh/$(DEPDIR)/mock_sshd-mock-sshd.Tpo -c -o src/ssh/mock_sshd-mock-sshd.o `test -f 'src/ssh/mock-sshd.c' || echo '$(srcdir)/'`src/ssh/mock-sshd.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/mock_sshd-mock-sshd.Tpo src/ssh/$(DEPDIR)/mock_sshd-mock-sshd.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/mock-sshd.c' object='src/ssh/mock_sshd-mock-sshd.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_sshd_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/mock_sshd-mock-sshd.o `test -f 'src/ssh/mock-sshd.c' || echo '$(srcdir)/'`src/ssh/mock-sshd.c
+
+src/ssh/mock_sshd-mock-sshd.obj: src/ssh/mock-sshd.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_sshd_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/mock_sshd-mock-sshd.obj -MD -MP -MF src/ssh/$(DEPDIR)/mock_sshd-mock-sshd.Tpo -c -o src/ssh/mock_sshd-mock-sshd.obj `if test -f 'src/ssh/mock-sshd.c'; then $(CYGPATH_W) 'src/ssh/mock-sshd.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/mock-sshd.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/mock_sshd-mock-sshd.Tpo src/ssh/$(DEPDIR)/mock_sshd-mock-sshd.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/mock-sshd.c' object='src/ssh/mock_sshd-mock-sshd.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(mock_sshd_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/mock_sshd-mock-sshd.obj `if test -f 'src/ssh/mock-sshd.c'; then $(CYGPATH_W) 'src/ssh/mock-sshd.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/mock-sshd.c'; fi`
+
+src/ws/pam_cockpit_cert_so-pam_cockpit_cert.o: src/ws/pam_cockpit_cert.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(pam_cockpit_cert_so_CFLAGS) $(CFLAGS) -MT src/ws/pam_cockpit_cert_so-pam_cockpit_cert.o -MD -MP -MF src/ws/$(DEPDIR)/pam_cockpit_cert_so-pam_cockpit_cert.Tpo -c -o src/ws/pam_cockpit_cert_so-pam_cockpit_cert.o `test -f 'src/ws/pam_cockpit_cert.c' || echo '$(srcdir)/'`src/ws/pam_cockpit_cert.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/pam_cockpit_cert_so-pam_cockpit_cert.Tpo src/ws/$(DEPDIR)/pam_cockpit_cert_so-pam_cockpit_cert.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/pam_cockpit_cert.c' object='src/ws/pam_cockpit_cert_so-pam_cockpit_cert.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(pam_cockpit_cert_so_CFLAGS) $(CFLAGS) -c -o src/ws/pam_cockpit_cert_so-pam_cockpit_cert.o `test -f 'src/ws/pam_cockpit_cert.c' || echo '$(srcdir)/'`src/ws/pam_cockpit_cert.c
+
+src/ws/pam_cockpit_cert_so-pam_cockpit_cert.obj: src/ws/pam_cockpit_cert.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(pam_cockpit_cert_so_CFLAGS) $(CFLAGS) -MT src/ws/pam_cockpit_cert_so-pam_cockpit_cert.obj -MD -MP -MF src/ws/$(DEPDIR)/pam_cockpit_cert_so-pam_cockpit_cert.Tpo -c -o src/ws/pam_cockpit_cert_so-pam_cockpit_cert.obj `if test -f 'src/ws/pam_cockpit_cert.c'; then $(CYGPATH_W) 'src/ws/pam_cockpit_cert.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/pam_cockpit_cert.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/pam_cockpit_cert_so-pam_cockpit_cert.Tpo src/ws/$(DEPDIR)/pam_cockpit_cert_so-pam_cockpit_cert.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/pam_cockpit_cert.c' object='src/ws/pam_cockpit_cert_so-pam_cockpit_cert.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(pam_cockpit_cert_so_CFLAGS) $(CFLAGS) -c -o src/ws/pam_cockpit_cert_so-pam_cockpit_cert.obj `if test -f 'src/ws/pam_cockpit_cert.c'; then $(CYGPATH_W) 'src/ws/pam_cockpit_cert.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/pam_cockpit_cert.c'; fi`
+
+src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.o: src/pam-ssh-add/pam-ssh-add.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(pam_ssh_add_so_CFLAGS) $(CFLAGS) -MT src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.o -MD -MP -MF src/pam-ssh-add/$(DEPDIR)/pam_ssh_add_so-pam-ssh-add.Tpo -c -o src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.o `test -f 'src/pam-ssh-add/pam-ssh-add.c' || echo '$(srcdir)/'`src/pam-ssh-add/pam-ssh-add.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/pam-ssh-add/$(DEPDIR)/pam_ssh_add_so-pam-ssh-add.Tpo src/pam-ssh-add/$(DEPDIR)/pam_ssh_add_so-pam-ssh-add.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/pam-ssh-add/pam-ssh-add.c' object='src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(pam_ssh_add_so_CFLAGS) $(CFLAGS) -c -o src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.o `test -f 'src/pam-ssh-add/pam-ssh-add.c' || echo '$(srcdir)/'`src/pam-ssh-add/pam-ssh-add.c
+
+src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.obj: src/pam-ssh-add/pam-ssh-add.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(pam_ssh_add_so_CFLAGS) $(CFLAGS) -MT src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.obj -MD -MP -MF src/pam-ssh-add/$(DEPDIR)/pam_ssh_add_so-pam-ssh-add.Tpo -c -o src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.obj `if test -f 'src/pam-ssh-add/pam-ssh-add.c'; then $(CYGPATH_W) 'src/pam-ssh-add/pam-ssh-add.c'; else $(CYGPATH_W) '$(srcdir)/src/pam-ssh-add/pam-ssh-add.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/pam-ssh-add/$(DEPDIR)/pam_ssh_add_so-pam-ssh-add.Tpo src/pam-ssh-add/$(DEPDIR)/pam_ssh_add_so-pam-ssh-add.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/pam-ssh-add/pam-ssh-add.c' object='src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(pam_ssh_add_so_CFLAGS) $(CFLAGS) -c -o src/pam-ssh-add/pam_ssh_add_so-pam-ssh-add.obj `if test -f 'src/pam-ssh-add/pam-ssh-add.c'; then $(CYGPATH_W) 'src/pam-ssh-add/pam-ssh-add.c'; else $(CYGPATH_W) '$(srcdir)/src/pam-ssh-add/pam-ssh-add.c'; fi`
+
+src/ws/test_auth-test-auth.o: src/ws/test-auth.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_auth-test-auth.o -MD -MP -MF src/ws/$(DEPDIR)/test_auth-test-auth.Tpo -c -o src/ws/test_auth-test-auth.o `test -f 'src/ws/test-auth.c' || echo '$(srcdir)/'`src/ws/test-auth.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_auth-test-auth.Tpo src/ws/$(DEPDIR)/test_auth-test-auth.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-auth.c' object='src/ws/test_auth-test-auth.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_auth-test-auth.o `test -f 'src/ws/test-auth.c' || echo '$(srcdir)/'`src/ws/test-auth.c
+
+src/ws/test_auth-test-auth.obj: src/ws/test-auth.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_auth-test-auth.obj -MD -MP -MF src/ws/$(DEPDIR)/test_auth-test-auth.Tpo -c -o src/ws/test_auth-test-auth.obj `if test -f 'src/ws/test-auth.c'; then $(CYGPATH_W) 'src/ws/test-auth.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-auth.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_auth-test-auth.Tpo src/ws/$(DEPDIR)/test_auth-test-auth.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-auth.c' object='src/ws/test_auth-test-auth.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_auth_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_auth-test-auth.obj `if test -f 'src/ws/test-auth.c'; then $(CYGPATH_W) 'src/ws/test-auth.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-auth.c'; fi`
+
+src/common/test_authorize-test-authorize.o: src/common/test-authorize.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_authorize_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_authorize-test-authorize.o -MD -MP -MF src/common/$(DEPDIR)/test_authorize-test-authorize.Tpo -c -o src/common/test_authorize-test-authorize.o `test -f 'src/common/test-authorize.c' || echo '$(srcdir)/'`src/common/test-authorize.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_authorize-test-authorize.Tpo src/common/$(DEPDIR)/test_authorize-test-authorize.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-authorize.c' object='src/common/test_authorize-test-authorize.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_authorize_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_authorize-test-authorize.o `test -f 'src/common/test-authorize.c' || echo '$(srcdir)/'`src/common/test-authorize.c
+
+src/common/test_authorize-test-authorize.obj: src/common/test-authorize.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_authorize_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_authorize-test-authorize.obj -MD -MP -MF src/common/$(DEPDIR)/test_authorize-test-authorize.Tpo -c -o src/common/test_authorize-test-authorize.obj `if test -f 'src/common/test-authorize.c'; then $(CYGPATH_W) 'src/common/test-authorize.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-authorize.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_authorize-test-authorize.Tpo src/common/$(DEPDIR)/test_authorize-test-authorize.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-authorize.c' object='src/common/test_authorize-test-authorize.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_authorize_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_authorize-test-authorize.obj `if test -f 'src/common/test-authorize.c'; then $(CYGPATH_W) 'src/common/test-authorize.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-authorize.c'; fi`
+
+src/ws/test_authssh-test-authssh.o: src/ws/test-authssh.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_authssh_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_authssh-test-authssh.o -MD -MP -MF src/ws/$(DEPDIR)/test_authssh-test-authssh.Tpo -c -o src/ws/test_authssh-test-authssh.o `test -f 'src/ws/test-authssh.c' || echo '$(srcdir)/'`src/ws/test-authssh.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_authssh-test-authssh.Tpo src/ws/$(DEPDIR)/test_authssh-test-authssh.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-authssh.c' object='src/ws/test_authssh-test-authssh.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_authssh_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_authssh-test-authssh.o `test -f 'src/ws/test-authssh.c' || echo '$(srcdir)/'`src/ws/test-authssh.c
+
+src/ws/test_authssh-test-authssh.obj: src/ws/test-authssh.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_authssh_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_authssh-test-authssh.obj -MD -MP -MF src/ws/$(DEPDIR)/test_authssh-test-authssh.Tpo -c -o src/ws/test_authssh-test-authssh.obj `if test -f 'src/ws/test-authssh.c'; then $(CYGPATH_W) 'src/ws/test-authssh.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-authssh.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_authssh-test-authssh.Tpo src/ws/$(DEPDIR)/test_authssh-test-authssh.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-authssh.c' object='src/ws/test_authssh-test-authssh.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_authssh_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_authssh-test-authssh.obj `if test -f 'src/ws/test-authssh.c'; then $(CYGPATH_W) 'src/ws/test-authssh.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-authssh.c'; fi`
+
+src/common/test_base64-test-base64.o: src/common/test-base64.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_base64_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_base64-test-base64.o -MD -MP -MF src/common/$(DEPDIR)/test_base64-test-base64.Tpo -c -o src/common/test_base64-test-base64.o `test -f 'src/common/test-base64.c' || echo '$(srcdir)/'`src/common/test-base64.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_base64-test-base64.Tpo src/common/$(DEPDIR)/test_base64-test-base64.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-base64.c' object='src/common/test_base64-test-base64.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_base64_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_base64-test-base64.o `test -f 'src/common/test-base64.c' || echo '$(srcdir)/'`src/common/test-base64.c
+
+src/common/test_base64-test-base64.obj: src/common/test-base64.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_base64_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_base64-test-base64.obj -MD -MP -MF src/common/$(DEPDIR)/test_base64-test-base64.Tpo -c -o src/common/test_base64-test-base64.obj `if test -f 'src/common/test-base64.c'; then $(CYGPATH_W) 'src/common/test-base64.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-base64.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_base64-test-base64.Tpo src/common/$(DEPDIR)/test_base64-test-base64.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-base64.c' object='src/common/test_base64-test-base64.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_base64_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_base64-test-base64.obj `if test -f 'src/common/test-base64.c'; then $(CYGPATH_W) 'src/common/test-base64.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-base64.c'; fi`
+
+src/bridge/test_bridge-test-bridge.o: src/bridge/test-bridge.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_bridge-test-bridge.o -MD -MP -MF src/bridge/$(DEPDIR)/test_bridge-test-bridge.Tpo -c -o src/bridge/test_bridge-test-bridge.o `test -f 'src/bridge/test-bridge.c' || echo '$(srcdir)/'`src/bridge/test-bridge.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_bridge-test-bridge.Tpo src/bridge/$(DEPDIR)/test_bridge-test-bridge.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-bridge.c' object='src/bridge/test_bridge-test-bridge.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_bridge-test-bridge.o `test -f 'src/bridge/test-bridge.c' || echo '$(srcdir)/'`src/bridge/test-bridge.c
+
+src/bridge/test_bridge-test-bridge.obj: src/bridge/test-bridge.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_bridge-test-bridge.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_bridge-test-bridge.Tpo -c -o src/bridge/test_bridge-test-bridge.obj `if test -f 'src/bridge/test-bridge.c'; then $(CYGPATH_W) 'src/bridge/test-bridge.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-bridge.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_bridge-test-bridge.Tpo src/bridge/$(DEPDIR)/test_bridge-test-bridge.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-bridge.c' object='src/bridge/test_bridge-test-bridge.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_bridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_bridge-test-bridge.obj `if test -f 'src/bridge/test-bridge.c'; then $(CYGPATH_W) 'src/bridge/test-bridge.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-bridge.c'; fi`
+
+src/common/test_channel-test-channel.o: src/common/test-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_channel-test-channel.o -MD -MP -MF src/common/$(DEPDIR)/test_channel-test-channel.Tpo -c -o src/common/test_channel-test-channel.o `test -f 'src/common/test-channel.c' || echo '$(srcdir)/'`src/common/test-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_channel-test-channel.Tpo src/common/$(DEPDIR)/test_channel-test-channel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-channel.c' object='src/common/test_channel-test-channel.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_channel-test-channel.o `test -f 'src/common/test-channel.c' || echo '$(srcdir)/'`src/common/test-channel.c
+
+src/common/test_channel-test-channel.obj: src/common/test-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_channel-test-channel.obj -MD -MP -MF src/common/$(DEPDIR)/test_channel-test-channel.Tpo -c -o src/common/test_channel-test-channel.obj `if test -f 'src/common/test-channel.c'; then $(CYGPATH_W) 'src/common/test-channel.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-channel.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_channel-test-channel.Tpo src/common/$(DEPDIR)/test_channel-test-channel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-channel.c' object='src/common/test_channel-test-channel.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_channel-test-channel.obj `if test -f 'src/common/test-channel.c'; then $(CYGPATH_W) 'src/common/test-channel.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-channel.c'; fi`
+
+src/ws/test_channelresponse-test-channelresponse.o: src/ws/test-channelresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_channelresponse_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_channelresponse-test-channelresponse.o -MD -MP -MF src/ws/$(DEPDIR)/test_channelresponse-test-channelresponse.Tpo -c -o src/ws/test_channelresponse-test-channelresponse.o `test -f 'src/ws/test-channelresponse.c' || echo '$(srcdir)/'`src/ws/test-channelresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_channelresponse-test-channelresponse.Tpo src/ws/$(DEPDIR)/test_channelresponse-test-channelresponse.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-channelresponse.c' object='src/ws/test_channelresponse-test-channelresponse.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_channelresponse_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_channelresponse-test-channelresponse.o `test -f 'src/ws/test-channelresponse.c' || echo '$(srcdir)/'`src/ws/test-channelresponse.c
+
+src/ws/test_channelresponse-test-channelresponse.obj: src/ws/test-channelresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_channelresponse_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_channelresponse-test-channelresponse.obj -MD -MP -MF src/ws/$(DEPDIR)/test_channelresponse-test-channelresponse.Tpo -c -o src/ws/test_channelresponse-test-channelresponse.obj `if test -f 'src/ws/test-channelresponse.c'; then $(CYGPATH_W) 'src/ws/test-channelresponse.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-channelresponse.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_channelresponse-test-channelresponse.Tpo src/ws/$(DEPDIR)/test_channelresponse-test-channelresponse.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-channelresponse.c' object='src/ws/test_channelresponse-test-channelresponse.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_channelresponse_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_channelresponse-test-channelresponse.obj `if test -f 'src/ws/test-channelresponse.c'; then $(CYGPATH_W) 'src/ws/test-channelresponse.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-channelresponse.c'; fi`
+
+src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.o: src/tls/test-cockpit-certificate-ensure.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_cockpit_certificate_ensure_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.o -MD -MP -MF src/tls/$(DEPDIR)/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.Tpo -c -o src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.o `test -f 'src/tls/test-cockpit-certificate-ensure.c' || echo '$(srcdir)/'`src/tls/test-cockpit-certificate-ensure.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/tls/$(DEPDIR)/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.Tpo src/tls/$(DEPDIR)/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/tls/test-cockpit-certificate-ensure.c' object='src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_cockpit_certificate_ensure_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.o `test -f 'src/tls/test-cockpit-certificate-ensure.c' || echo '$(srcdir)/'`src/tls/test-cockpit-certificate-ensure.c
+
+src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.obj: src/tls/test-cockpit-certificate-ensure.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_cockpit_certificate_ensure_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.obj -MD -MP -MF src/tls/$(DEPDIR)/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.Tpo -c -o src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.obj `if test -f 'src/tls/test-cockpit-certificate-ensure.c'; then $(CYGPATH_W) 'src/tls/test-cockpit-certificate-ensure.c'; else $(CYGPATH_W) '$(srcdir)/src/tls/test-cockpit-certificate-ensure.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/tls/$(DEPDIR)/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.Tpo src/tls/$(DEPDIR)/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/tls/test-cockpit-certificate-ensure.c' object='src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_cockpit_certificate_ensure_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/tls/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.obj `if test -f 'src/tls/test-cockpit-certificate-ensure.c'; then $(CYGPATH_W) 'src/tls/test-cockpit-certificate-ensure.c'; else $(CYGPATH_W) '$(srcdir)/src/tls/test-cockpit-certificate-ensure.c'; fi`
+
+src/ws/test_compat-test-compat.o: src/ws/test-compat.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_compat_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_compat-test-compat.o -MD -MP -MF src/ws/$(DEPDIR)/test_compat-test-compat.Tpo -c -o src/ws/test_compat-test-compat.o `test -f 'src/ws/test-compat.c' || echo '$(srcdir)/'`src/ws/test-compat.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_compat-test-compat.Tpo src/ws/$(DEPDIR)/test_compat-test-compat.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-compat.c' object='src/ws/test_compat-test-compat.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_compat_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_compat-test-compat.o `test -f 'src/ws/test-compat.c' || echo '$(srcdir)/'`src/ws/test-compat.c
+
+src/ws/test_compat-test-compat.obj: src/ws/test-compat.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_compat_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_compat-test-compat.obj -MD -MP -MF src/ws/$(DEPDIR)/test_compat-test-compat.Tpo -c -o src/ws/test_compat-test-compat.obj `if test -f 'src/ws/test-compat.c'; then $(CYGPATH_W) 'src/ws/test-compat.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-compat.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_compat-test-compat.Tpo src/ws/$(DEPDIR)/test_compat-test-compat.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-compat.c' object='src/ws/test_compat-test-compat.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_compat_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_compat-test-compat.obj `if test -f 'src/ws/test-compat.c'; then $(CYGPATH_W) 'src/ws/test-compat.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-compat.c'; fi`
+
+src/common/test_config-test-config.o: src/common/test-config.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_config_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_config-test-config.o -MD -MP -MF src/common/$(DEPDIR)/test_config-test-config.Tpo -c -o src/common/test_config-test-config.o `test -f 'src/common/test-config.c' || echo '$(srcdir)/'`src/common/test-config.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_config-test-config.Tpo src/common/$(DEPDIR)/test_config-test-config.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-config.c' object='src/common/test_config-test-config.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_config_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_config-test-config.o `test -f 'src/common/test-config.c' || echo '$(srcdir)/'`src/common/test-config.c
+
+src/common/test_config-test-config.obj: src/common/test-config.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_config_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_config-test-config.obj -MD -MP -MF src/common/$(DEPDIR)/test_config-test-config.Tpo -c -o src/common/test_config-test-config.obj `if test -f 'src/common/test-config.c'; then $(CYGPATH_W) 'src/common/test-config.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-config.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_config-test-config.Tpo src/common/$(DEPDIR)/test_config-test-config.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-config.c' object='src/common/test_config-test-config.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_config_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_config-test-config.obj `if test -f 'src/common/test-config.c'; then $(CYGPATH_W) 'src/common/test-config.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-config.c'; fi`
+
+src/bridge/test_connect-test-connect.o: src/bridge/test-connect.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_connect_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_connect-test-connect.o -MD -MP -MF src/bridge/$(DEPDIR)/test_connect-test-connect.Tpo -c -o src/bridge/test_connect-test-connect.o `test -f 'src/bridge/test-connect.c' || echo '$(srcdir)/'`src/bridge/test-connect.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_connect-test-connect.Tpo src/bridge/$(DEPDIR)/test_connect-test-connect.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-connect.c' object='src/bridge/test_connect-test-connect.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_connect_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_connect-test-connect.o `test -f 'src/bridge/test-connect.c' || echo '$(srcdir)/'`src/bridge/test-connect.c
+
+src/bridge/test_connect-test-connect.obj: src/bridge/test-connect.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_connect_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_connect-test-connect.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_connect-test-connect.Tpo -c -o src/bridge/test_connect-test-connect.obj `if test -f 'src/bridge/test-connect.c'; then $(CYGPATH_W) 'src/bridge/test-connect.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-connect.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_connect-test-connect.Tpo src/bridge/$(DEPDIR)/test_connect-test-connect.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-connect.c' object='src/bridge/test_connect-test-connect.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_connect_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_connect-test-connect.obj `if test -f 'src/bridge/test-connect.c'; then $(CYGPATH_W) 'src/bridge/test-connect.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-connect.c'; fi`
+
+src/ws/test_creds-test-creds.o: src/ws/test-creds.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_creds_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_creds-test-creds.o -MD -MP -MF src/ws/$(DEPDIR)/test_creds-test-creds.Tpo -c -o src/ws/test_creds-test-creds.o `test -f 'src/ws/test-creds.c' || echo '$(srcdir)/'`src/ws/test-creds.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_creds-test-creds.Tpo src/ws/$(DEPDIR)/test_creds-test-creds.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-creds.c' object='src/ws/test_creds-test-creds.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_creds_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_creds-test-creds.o `test -f 'src/ws/test-creds.c' || echo '$(srcdir)/'`src/ws/test-creds.c
+
+src/ws/test_creds-test-creds.obj: src/ws/test-creds.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_creds_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_creds-test-creds.obj -MD -MP -MF src/ws/$(DEPDIR)/test_creds-test-creds.Tpo -c -o src/ws/test_creds-test-creds.obj `if test -f 'src/ws/test-creds.c'; then $(CYGPATH_W) 'src/ws/test-creds.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-creds.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_creds-test-creds.Tpo src/ws/$(DEPDIR)/test_creds-test-creds.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-creds.c' object='src/ws/test_creds-test-creds.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_creds_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_creds-test-creds.obj `if test -f 'src/ws/test-creds.c'; then $(CYGPATH_W) 'src/ws/test-creds.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-creds.c'; fi`
+
+src/bridge/test_dbus_meta-test-dbus-meta.o: src/bridge/test-dbus-meta.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_dbus_meta_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_dbus_meta-test-dbus-meta.o -MD -MP -MF src/bridge/$(DEPDIR)/test_dbus_meta-test-dbus-meta.Tpo -c -o src/bridge/test_dbus_meta-test-dbus-meta.o `test -f 'src/bridge/test-dbus-meta.c' || echo '$(srcdir)/'`src/bridge/test-dbus-meta.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_dbus_meta-test-dbus-meta.Tpo src/bridge/$(DEPDIR)/test_dbus_meta-test-dbus-meta.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-dbus-meta.c' object='src/bridge/test_dbus_meta-test-dbus-meta.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_dbus_meta_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_dbus_meta-test-dbus-meta.o `test -f 'src/bridge/test-dbus-meta.c' || echo '$(srcdir)/'`src/bridge/test-dbus-meta.c
+
+src/bridge/test_dbus_meta-test-dbus-meta.obj: src/bridge/test-dbus-meta.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_dbus_meta_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_dbus_meta-test-dbus-meta.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_dbus_meta-test-dbus-meta.Tpo -c -o src/bridge/test_dbus_meta-test-dbus-meta.obj `if test -f 'src/bridge/test-dbus-meta.c'; then $(CYGPATH_W) 'src/bridge/test-dbus-meta.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-dbus-meta.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_dbus_meta-test-dbus-meta.Tpo src/bridge/$(DEPDIR)/test_dbus_meta-test-dbus-meta.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-dbus-meta.c' object='src/bridge/test_dbus_meta-test-dbus-meta.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_dbus_meta_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_dbus_meta-test-dbus-meta.obj `if test -f 'src/bridge/test-dbus-meta.c'; then $(CYGPATH_W) 'src/bridge/test-dbus-meta.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-dbus-meta.c'; fi`
+
+src/common/test_frame-test-frame.o: src/common/test-frame.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_frame_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_frame-test-frame.o -MD -MP -MF src/common/$(DEPDIR)/test_frame-test-frame.Tpo -c -o src/common/test_frame-test-frame.o `test -f 'src/common/test-frame.c' || echo '$(srcdir)/'`src/common/test-frame.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_frame-test-frame.Tpo src/common/$(DEPDIR)/test_frame-test-frame.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-frame.c' object='src/common/test_frame-test-frame.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_frame_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_frame-test-frame.o `test -f 'src/common/test-frame.c' || echo '$(srcdir)/'`src/common/test-frame.c
+
+src/common/test_frame-test-frame.obj: src/common/test-frame.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_frame_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_frame-test-frame.obj -MD -MP -MF src/common/$(DEPDIR)/test_frame-test-frame.Tpo -c -o src/common/test_frame-test-frame.obj `if test -f 'src/common/test-frame.c'; then $(CYGPATH_W) 'src/common/test-frame.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-frame.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_frame-test-frame.Tpo src/common/$(DEPDIR)/test_frame-test-frame.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-frame.c' object='src/common/test_frame-test-frame.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_frame_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_frame-test-frame.obj `if test -f 'src/common/test-frame.c'; then $(CYGPATH_W) 'src/common/test-frame.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-frame.c'; fi`
+
+src/bridge/test_fs-test-fs.o: src/bridge/test-fs.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_fs_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_fs-test-fs.o -MD -MP -MF src/bridge/$(DEPDIR)/test_fs-test-fs.Tpo -c -o src/bridge/test_fs-test-fs.o `test -f 'src/bridge/test-fs.c' || echo '$(srcdir)/'`src/bridge/test-fs.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_fs-test-fs.Tpo src/bridge/$(DEPDIR)/test_fs-test-fs.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-fs.c' object='src/bridge/test_fs-test-fs.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_fs_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_fs-test-fs.o `test -f 'src/bridge/test-fs.c' || echo '$(srcdir)/'`src/bridge/test-fs.c
+
+src/bridge/test_fs-test-fs.obj: src/bridge/test-fs.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_fs_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_fs-test-fs.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_fs-test-fs.Tpo -c -o src/bridge/test_fs-test-fs.obj `if test -f 'src/bridge/test-fs.c'; then $(CYGPATH_W) 'src/bridge/test-fs.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-fs.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_fs-test-fs.Tpo src/bridge/$(DEPDIR)/test_fs-test-fs.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-fs.c' object='src/bridge/test_fs-test-fs.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_fs_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_fs-test-fs.obj `if test -f 'src/bridge/test-fs.c'; then $(CYGPATH_W) 'src/bridge/test-fs.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-fs.c'; fi`
+
+src/ws/test_handlers-test-handlers.o: src/ws/test-handlers.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_handlers_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_handlers-test-handlers.o -MD -MP -MF src/ws/$(DEPDIR)/test_handlers-test-handlers.Tpo -c -o src/ws/test_handlers-test-handlers.o `test -f 'src/ws/test-handlers.c' || echo '$(srcdir)/'`src/ws/test-handlers.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_handlers-test-handlers.Tpo src/ws/$(DEPDIR)/test_handlers-test-handlers.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-handlers.c' object='src/ws/test_handlers-test-handlers.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_handlers_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_handlers-test-handlers.o `test -f 'src/ws/test-handlers.c' || echo '$(srcdir)/'`src/ws/test-handlers.c
+
+src/ws/test_handlers-test-handlers.obj: src/ws/test-handlers.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_handlers_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_handlers-test-handlers.obj -MD -MP -MF src/ws/$(DEPDIR)/test_handlers-test-handlers.Tpo -c -o src/ws/test_handlers-test-handlers.obj `if test -f 'src/ws/test-handlers.c'; then $(CYGPATH_W) 'src/ws/test-handlers.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-handlers.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_handlers-test-handlers.Tpo src/ws/$(DEPDIR)/test_handlers-test-handlers.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-handlers.c' object='src/ws/test_handlers-test-handlers.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_handlers_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_handlers-test-handlers.obj `if test -f 'src/ws/test-handlers.c'; then $(CYGPATH_W) 'src/ws/test-handlers.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-handlers.c'; fi`
+
+src/common/test_hash-test-hash.o: src/common/test-hash.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_hash_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_hash-test-hash.o -MD -MP -MF src/common/$(DEPDIR)/test_hash-test-hash.Tpo -c -o src/common/test_hash-test-hash.o `test -f 'src/common/test-hash.c' || echo '$(srcdir)/'`src/common/test-hash.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_hash-test-hash.Tpo src/common/$(DEPDIR)/test_hash-test-hash.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-hash.c' object='src/common/test_hash-test-hash.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_hash_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_hash-test-hash.o `test -f 'src/common/test-hash.c' || echo '$(srcdir)/'`src/common/test-hash.c
+
+src/common/test_hash-test-hash.obj: src/common/test-hash.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_hash_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_hash-test-hash.obj -MD -MP -MF src/common/$(DEPDIR)/test_hash-test-hash.Tpo -c -o src/common/test_hash-test-hash.obj `if test -f 'src/common/test-hash.c'; then $(CYGPATH_W) 'src/common/test-hash.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-hash.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_hash-test-hash.Tpo src/common/$(DEPDIR)/test_hash-test-hash.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-hash.c' object='src/common/test_hash-test-hash.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_hash_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_hash-test-hash.obj `if test -f 'src/common/test-hash.c'; then $(CYGPATH_W) 'src/common/test-hash.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-hash.c'; fi`
+
+src/common/test_hex-test-hex.o: src/common/test-hex.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_hex_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_hex-test-hex.o -MD -MP -MF src/common/$(DEPDIR)/test_hex-test-hex.Tpo -c -o src/common/test_hex-test-hex.o `test -f 'src/common/test-hex.c' || echo '$(srcdir)/'`src/common/test-hex.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_hex-test-hex.Tpo src/common/$(DEPDIR)/test_hex-test-hex.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-hex.c' object='src/common/test_hex-test-hex.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_hex_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_hex-test-hex.o `test -f 'src/common/test-hex.c' || echo '$(srcdir)/'`src/common/test-hex.c
+
+src/common/test_hex-test-hex.obj: src/common/test-hex.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_hex_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_hex-test-hex.obj -MD -MP -MF src/common/$(DEPDIR)/test_hex-test-hex.Tpo -c -o src/common/test_hex-test-hex.obj `if test -f 'src/common/test-hex.c'; then $(CYGPATH_W) 'src/common/test-hex.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-hex.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_hex-test-hex.Tpo src/common/$(DEPDIR)/test_hex-test-hex.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-hex.c' object='src/common/test_hex-test-hex.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_hex_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_hex-test-hex.obj `if test -f 'src/common/test-hex.c'; then $(CYGPATH_W) 'src/common/test-hex.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-hex.c'; fi`
+
+src/bridge/test_httpstream-test-httpstream.o: src/bridge/test-httpstream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_httpstream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_httpstream-test-httpstream.o -MD -MP -MF src/bridge/$(DEPDIR)/test_httpstream-test-httpstream.Tpo -c -o src/bridge/test_httpstream-test-httpstream.o `test -f 'src/bridge/test-httpstream.c' || echo '$(srcdir)/'`src/bridge/test-httpstream.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_httpstream-test-httpstream.Tpo src/bridge/$(DEPDIR)/test_httpstream-test-httpstream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-httpstream.c' object='src/bridge/test_httpstream-test-httpstream.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_httpstream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_httpstream-test-httpstream.o `test -f 'src/bridge/test-httpstream.c' || echo '$(srcdir)/'`src/bridge/test-httpstream.c
+
+src/bridge/test_httpstream-test-httpstream.obj: src/bridge/test-httpstream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_httpstream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_httpstream-test-httpstream.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_httpstream-test-httpstream.Tpo -c -o src/bridge/test_httpstream-test-httpstream.obj `if test -f 'src/bridge/test-httpstream.c'; then $(CYGPATH_W) 'src/bridge/test-httpstream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-httpstream.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_httpstream-test-httpstream.Tpo src/bridge/$(DEPDIR)/test_httpstream-test-httpstream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-httpstream.c' object='src/bridge/test_httpstream-test-httpstream.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_httpstream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_httpstream-test-httpstream.obj `if test -f 'src/bridge/test-httpstream.c'; then $(CYGPATH_W) 'src/bridge/test-httpstream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-httpstream.c'; fi`
+
+src/common/test_json-test-json.o: src/common/test-json.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_json_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_json-test-json.o -MD -MP -MF src/common/$(DEPDIR)/test_json-test-json.Tpo -c -o src/common/test_json-test-json.o `test -f 'src/common/test-json.c' || echo '$(srcdir)/'`src/common/test-json.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_json-test-json.Tpo src/common/$(DEPDIR)/test_json-test-json.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-json.c' object='src/common/test_json-test-json.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_json_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_json-test-json.o `test -f 'src/common/test-json.c' || echo '$(srcdir)/'`src/common/test-json.c
+
+src/common/test_json-test-json.obj: src/common/test-json.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_json_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_json-test-json.obj -MD -MP -MF src/common/$(DEPDIR)/test_json-test-json.Tpo -c -o src/common/test_json-test-json.obj `if test -f 'src/common/test-json.c'; then $(CYGPATH_W) 'src/common/test-json.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-json.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_json-test-json.Tpo src/common/$(DEPDIR)/test_json-test-json.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-json.c' object='src/common/test_json-test-json.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_json_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_json-test-json.obj `if test -f 'src/common/test-json.c'; then $(CYGPATH_W) 'src/common/test-json.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-json.c'; fi`
+
+src/common/test_jsonfds-test-jsonfds.o: src/common/test-jsonfds.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_jsonfds_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_jsonfds-test-jsonfds.o -MD -MP -MF src/common/$(DEPDIR)/test_jsonfds-test-jsonfds.Tpo -c -o src/common/test_jsonfds-test-jsonfds.o `test -f 'src/common/test-jsonfds.c' || echo '$(srcdir)/'`src/common/test-jsonfds.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_jsonfds-test-jsonfds.Tpo src/common/$(DEPDIR)/test_jsonfds-test-jsonfds.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-jsonfds.c' object='src/common/test_jsonfds-test-jsonfds.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_jsonfds_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_jsonfds-test-jsonfds.o `test -f 'src/common/test-jsonfds.c' || echo '$(srcdir)/'`src/common/test-jsonfds.c
+
+src/common/test_jsonfds-test-jsonfds.obj: src/common/test-jsonfds.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_jsonfds_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_jsonfds-test-jsonfds.obj -MD -MP -MF src/common/$(DEPDIR)/test_jsonfds-test-jsonfds.Tpo -c -o src/common/test_jsonfds-test-jsonfds.obj `if test -f 'src/common/test-jsonfds.c'; then $(CYGPATH_W) 'src/common/test-jsonfds.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-jsonfds.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_jsonfds-test-jsonfds.Tpo src/common/$(DEPDIR)/test_jsonfds-test-jsonfds.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-jsonfds.c' object='src/common/test_jsonfds-test-jsonfds.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_jsonfds_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_jsonfds-test-jsonfds.obj `if test -f 'src/common/test-jsonfds.c'; then $(CYGPATH_W) 'src/common/test-jsonfds.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-jsonfds.c'; fi`
+
+src/ws/test_kerberos-test-kerberos.o: src/ws/test-kerberos.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_kerberos_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_kerberos-test-kerberos.o -MD -MP -MF src/ws/$(DEPDIR)/test_kerberos-test-kerberos.Tpo -c -o src/ws/test_kerberos-test-kerberos.o `test -f 'src/ws/test-kerberos.c' || echo '$(srcdir)/'`src/ws/test-kerberos.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_kerberos-test-kerberos.Tpo src/ws/$(DEPDIR)/test_kerberos-test-kerberos.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-kerberos.c' object='src/ws/test_kerberos-test-kerberos.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_kerberos_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_kerberos-test-kerberos.o `test -f 'src/ws/test-kerberos.c' || echo '$(srcdir)/'`src/ws/test-kerberos.c
+
+src/ws/test_kerberos-test-kerberos.obj: src/ws/test-kerberos.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_kerberos_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_kerberos-test-kerberos.obj -MD -MP -MF src/ws/$(DEPDIR)/test_kerberos-test-kerberos.Tpo -c -o src/ws/test_kerberos-test-kerberos.obj `if test -f 'src/ws/test-kerberos.c'; then $(CYGPATH_W) 'src/ws/test-kerberos.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-kerberos.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_kerberos-test-kerberos.Tpo src/ws/$(DEPDIR)/test_kerberos-test-kerberos.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-kerberos.c' object='src/ws/test_kerberos-test-kerberos.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_kerberos_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_kerberos-test-kerberos.obj `if test -f 'src/ws/test-kerberos.c'; then $(CYGPATH_W) 'src/ws/test-kerberos.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-kerberos.c'; fi`
+
+src/common/test_locale-test-locale.o: src/common/test-locale.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_locale_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_locale-test-locale.o -MD -MP -MF src/common/$(DEPDIR)/test_locale-test-locale.Tpo -c -o src/common/test_locale-test-locale.o `test -f 'src/common/test-locale.c' || echo '$(srcdir)/'`src/common/test-locale.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_locale-test-locale.Tpo src/common/$(DEPDIR)/test_locale-test-locale.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-locale.c' object='src/common/test_locale-test-locale.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_locale_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_locale-test-locale.o `test -f 'src/common/test-locale.c' || echo '$(srcdir)/'`src/common/test-locale.c
+
+src/common/test_locale-test-locale.obj: src/common/test-locale.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_locale_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_locale-test-locale.obj -MD -MP -MF src/common/$(DEPDIR)/test_locale-test-locale.Tpo -c -o src/common/test_locale-test-locale.obj `if test -f 'src/common/test-locale.c'; then $(CYGPATH_W) 'src/common/test-locale.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-locale.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_locale-test-locale.Tpo src/common/$(DEPDIR)/test_locale-test-locale.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-locale.c' object='src/common/test_locale-test-locale.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_locale_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_locale-test-locale.obj `if test -f 'src/common/test-locale.c'; then $(CYGPATH_W) 'src/common/test-locale.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-locale.c'; fi`
+
+src/bridge/test_metrics-test-metrics.o: src/bridge/test-metrics.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_metrics_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_metrics-test-metrics.o -MD -MP -MF src/bridge/$(DEPDIR)/test_metrics-test-metrics.Tpo -c -o src/bridge/test_metrics-test-metrics.o `test -f 'src/bridge/test-metrics.c' || echo '$(srcdir)/'`src/bridge/test-metrics.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_metrics-test-metrics.Tpo src/bridge/$(DEPDIR)/test_metrics-test-metrics.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-metrics.c' object='src/bridge/test_metrics-test-metrics.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_metrics_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_metrics-test-metrics.o `test -f 'src/bridge/test-metrics.c' || echo '$(srcdir)/'`src/bridge/test-metrics.c
+
+src/bridge/test_metrics-test-metrics.obj: src/bridge/test-metrics.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_metrics_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_metrics-test-metrics.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_metrics-test-metrics.Tpo -c -o src/bridge/test_metrics-test-metrics.obj `if test -f 'src/bridge/test-metrics.c'; then $(CYGPATH_W) 'src/bridge/test-metrics.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-metrics.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_metrics-test-metrics.Tpo src/bridge/$(DEPDIR)/test_metrics-test-metrics.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-metrics.c' object='src/bridge/test_metrics-test-metrics.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_metrics_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_metrics-test-metrics.obj `if test -f 'src/bridge/test-metrics.c'; then $(CYGPATH_W) 'src/bridge/test-metrics.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-metrics.c'; fi`
+
+src/bridge/test_packages-test-packages.o: src/bridge/test-packages.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_packages_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_packages-test-packages.o -MD -MP -MF src/bridge/$(DEPDIR)/test_packages-test-packages.Tpo -c -o src/bridge/test_packages-test-packages.o `test -f 'src/bridge/test-packages.c' || echo '$(srcdir)/'`src/bridge/test-packages.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_packages-test-packages.Tpo src/bridge/$(DEPDIR)/test_packages-test-packages.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-packages.c' object='src/bridge/test_packages-test-packages.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_packages_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_packages-test-packages.o `test -f 'src/bridge/test-packages.c' || echo '$(srcdir)/'`src/bridge/test-packages.c
+
+src/bridge/test_packages-test-packages.obj: src/bridge/test-packages.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_packages_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_packages-test-packages.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_packages-test-packages.Tpo -c -o src/bridge/test_packages-test-packages.obj `if test -f 'src/bridge/test-packages.c'; then $(CYGPATH_W) 'src/bridge/test-packages.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-packages.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_packages-test-packages.Tpo src/bridge/$(DEPDIR)/test_packages-test-packages.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-packages.c' object='src/bridge/test_packages-test-packages.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_packages_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_packages-test-packages.obj `if test -f 'src/bridge/test-packages.c'; then $(CYGPATH_W) 'src/bridge/test-packages.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-packages.c'; fi`
+
+src/bridge/test_packet_channel-test-packet-channel.o: src/bridge/test-packet-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_packet_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_packet_channel-test-packet-channel.o -MD -MP -MF src/bridge/$(DEPDIR)/test_packet_channel-test-packet-channel.Tpo -c -o src/bridge/test_packet_channel-test-packet-channel.o `test -f 'src/bridge/test-packet-channel.c' || echo '$(srcdir)/'`src/bridge/test-packet-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_packet_channel-test-packet-channel.Tpo src/bridge/$(DEPDIR)/test_packet_channel-test-packet-channel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-packet-channel.c' object='src/bridge/test_packet_channel-test-packet-channel.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_packet_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_packet_channel-test-packet-channel.o `test -f 'src/bridge/test-packet-channel.c' || echo '$(srcdir)/'`src/bridge/test-packet-channel.c
+
+src/bridge/test_packet_channel-test-packet-channel.obj: src/bridge/test-packet-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_packet_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_packet_channel-test-packet-channel.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_packet_channel-test-packet-channel.Tpo -c -o src/bridge/test_packet_channel-test-packet-channel.obj `if test -f 'src/bridge/test-packet-channel.c'; then $(CYGPATH_W) 'src/bridge/test-packet-channel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-packet-channel.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_packet_channel-test-packet-channel.Tpo src/bridge/$(DEPDIR)/test_packet_channel-test-packet-channel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-packet-channel.c' object='src/bridge/test_packet_channel-test-packet-channel.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_packet_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_packet_channel-test-packet-channel.obj `if test -f 'src/bridge/test-packet-channel.c'; then $(CYGPATH_W) 'src/bridge/test-packet-channel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-packet-channel.c'; fi`
+
+src/bridge/test_paths-test-paths.o: src/bridge/test-paths.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_paths_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_paths-test-paths.o -MD -MP -MF src/bridge/$(DEPDIR)/test_paths-test-paths.Tpo -c -o src/bridge/test_paths-test-paths.o `test -f 'src/bridge/test-paths.c' || echo '$(srcdir)/'`src/bridge/test-paths.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_paths-test-paths.Tpo src/bridge/$(DEPDIR)/test_paths-test-paths.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-paths.c' object='src/bridge/test_paths-test-paths.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_paths_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_paths-test-paths.o `test -f 'src/bridge/test-paths.c' || echo '$(srcdir)/'`src/bridge/test-paths.c
+
+src/bridge/test_paths-test-paths.obj: src/bridge/test-paths.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_paths_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_paths-test-paths.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_paths-test-paths.Tpo -c -o src/bridge/test_paths-test-paths.obj `if test -f 'src/bridge/test-paths.c'; then $(CYGPATH_W) 'src/bridge/test-paths.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-paths.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_paths-test-paths.Tpo src/bridge/$(DEPDIR)/test_paths-test-paths.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-paths.c' object='src/bridge/test_paths-test-paths.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_paths_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_paths-test-paths.obj `if test -f 'src/bridge/test-paths.c'; then $(CYGPATH_W) 'src/bridge/test-paths.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-paths.c'; fi`
+
+src/bridge/test_pcp-test-pcp.o: src/bridge/test-pcp.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pcp_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_pcp-test-pcp.o -MD -MP -MF src/bridge/$(DEPDIR)/test_pcp-test-pcp.Tpo -c -o src/bridge/test_pcp-test-pcp.o `test -f 'src/bridge/test-pcp.c' || echo '$(srcdir)/'`src/bridge/test-pcp.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_pcp-test-pcp.Tpo src/bridge/$(DEPDIR)/test_pcp-test-pcp.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-pcp.c' object='src/bridge/test_pcp-test-pcp.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pcp_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_pcp-test-pcp.o `test -f 'src/bridge/test-pcp.c' || echo '$(srcdir)/'`src/bridge/test-pcp.c
+
+src/bridge/test_pcp-test-pcp.obj: src/bridge/test-pcp.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pcp_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_pcp-test-pcp.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_pcp-test-pcp.Tpo -c -o src/bridge/test_pcp-test-pcp.obj `if test -f 'src/bridge/test-pcp.c'; then $(CYGPATH_W) 'src/bridge/test-pcp.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-pcp.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_pcp-test-pcp.Tpo src/bridge/$(DEPDIR)/test_pcp-test-pcp.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-pcp.c' object='src/bridge/test_pcp-test-pcp.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pcp_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_pcp-test-pcp.obj `if test -f 'src/bridge/test-pcp.c'; then $(CYGPATH_W) 'src/bridge/test-pcp.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-pcp.c'; fi`
+
+src/bridge/test_pcp_archives-test-pcp-archives.o: src/bridge/test-pcp-archives.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pcp_archives_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_pcp_archives-test-pcp-archives.o -MD -MP -MF src/bridge/$(DEPDIR)/test_pcp_archives-test-pcp-archives.Tpo -c -o src/bridge/test_pcp_archives-test-pcp-archives.o `test -f 'src/bridge/test-pcp-archives.c' || echo '$(srcdir)/'`src/bridge/test-pcp-archives.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_pcp_archives-test-pcp-archives.Tpo src/bridge/$(DEPDIR)/test_pcp_archives-test-pcp-archives.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-pcp-archives.c' object='src/bridge/test_pcp_archives-test-pcp-archives.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pcp_archives_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_pcp_archives-test-pcp-archives.o `test -f 'src/bridge/test-pcp-archives.c' || echo '$(srcdir)/'`src/bridge/test-pcp-archives.c
+
+src/bridge/test_pcp_archives-test-pcp-archives.obj: src/bridge/test-pcp-archives.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pcp_archives_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_pcp_archives-test-pcp-archives.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_pcp_archives-test-pcp-archives.Tpo -c -o src/bridge/test_pcp_archives-test-pcp-archives.obj `if test -f 'src/bridge/test-pcp-archives.c'; then $(CYGPATH_W) 'src/bridge/test-pcp-archives.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-pcp-archives.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_pcp_archives-test-pcp-archives.Tpo src/bridge/$(DEPDIR)/test_pcp_archives-test-pcp-archives.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-pcp-archives.c' object='src/bridge/test_pcp_archives-test-pcp-archives.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pcp_archives_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_pcp_archives-test-pcp-archives.obj `if test -f 'src/bridge/test-pcp-archives.c'; then $(CYGPATH_W) 'src/bridge/test-pcp-archives.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-pcp-archives.c'; fi`
+
+src/bridge/test_peer-test-peer.o: src/bridge/test-peer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_peer_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_peer-test-peer.o -MD -MP -MF src/bridge/$(DEPDIR)/test_peer-test-peer.Tpo -c -o src/bridge/test_peer-test-peer.o `test -f 'src/bridge/test-peer.c' || echo '$(srcdir)/'`src/bridge/test-peer.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_peer-test-peer.Tpo src/bridge/$(DEPDIR)/test_peer-test-peer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-peer.c' object='src/bridge/test_peer-test-peer.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_peer_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_peer-test-peer.o `test -f 'src/bridge/test-peer.c' || echo '$(srcdir)/'`src/bridge/test-peer.c
+
+src/bridge/test_peer-test-peer.obj: src/bridge/test-peer.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_peer_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_peer-test-peer.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_peer-test-peer.Tpo -c -o src/bridge/test_peer-test-peer.obj `if test -f 'src/bridge/test-peer.c'; then $(CYGPATH_W) 'src/bridge/test-peer.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-peer.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_peer-test-peer.Tpo src/bridge/$(DEPDIR)/test_peer-test-peer.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-peer.c' object='src/bridge/test_peer-test-peer.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_peer_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_peer-test-peer.obj `if test -f 'src/bridge/test-peer.c'; then $(CYGPATH_W) 'src/bridge/test-peer.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-peer.c'; fi`
+
+src/common/test_pipe-test-pipe.o: src/common/test-pipe.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pipe_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_pipe-test-pipe.o -MD -MP -MF src/common/$(DEPDIR)/test_pipe-test-pipe.Tpo -c -o src/common/test_pipe-test-pipe.o `test -f 'src/common/test-pipe.c' || echo '$(srcdir)/'`src/common/test-pipe.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_pipe-test-pipe.Tpo src/common/$(DEPDIR)/test_pipe-test-pipe.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-pipe.c' object='src/common/test_pipe-test-pipe.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pipe_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_pipe-test-pipe.o `test -f 'src/common/test-pipe.c' || echo '$(srcdir)/'`src/common/test-pipe.c
+
+src/common/test_pipe-test-pipe.obj: src/common/test-pipe.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pipe_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_pipe-test-pipe.obj -MD -MP -MF src/common/$(DEPDIR)/test_pipe-test-pipe.Tpo -c -o src/common/test_pipe-test-pipe.obj `if test -f 'src/common/test-pipe.c'; then $(CYGPATH_W) 'src/common/test-pipe.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-pipe.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_pipe-test-pipe.Tpo src/common/$(DEPDIR)/test_pipe-test-pipe.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-pipe.c' object='src/common/test_pipe-test-pipe.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pipe_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_pipe-test-pipe.obj `if test -f 'src/common/test-pipe.c'; then $(CYGPATH_W) 'src/common/test-pipe.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-pipe.c'; fi`
+
+src/bridge/test_pipe_channel-test-pipe-channel.o: src/bridge/test-pipe-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pipe_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_pipe_channel-test-pipe-channel.o -MD -MP -MF src/bridge/$(DEPDIR)/test_pipe_channel-test-pipe-channel.Tpo -c -o src/bridge/test_pipe_channel-test-pipe-channel.o `test -f 'src/bridge/test-pipe-channel.c' || echo '$(srcdir)/'`src/bridge/test-pipe-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_pipe_channel-test-pipe-channel.Tpo src/bridge/$(DEPDIR)/test_pipe_channel-test-pipe-channel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-pipe-channel.c' object='src/bridge/test_pipe_channel-test-pipe-channel.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pipe_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_pipe_channel-test-pipe-channel.o `test -f 'src/bridge/test-pipe-channel.c' || echo '$(srcdir)/'`src/bridge/test-pipe-channel.c
+
+src/bridge/test_pipe_channel-test-pipe-channel.obj: src/bridge/test-pipe-channel.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pipe_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_pipe_channel-test-pipe-channel.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_pipe_channel-test-pipe-channel.Tpo -c -o src/bridge/test_pipe_channel-test-pipe-channel.obj `if test -f 'src/bridge/test-pipe-channel.c'; then $(CYGPATH_W) 'src/bridge/test-pipe-channel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-pipe-channel.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_pipe_channel-test-pipe-channel.Tpo src/bridge/$(DEPDIR)/test_pipe_channel-test-pipe-channel.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-pipe-channel.c' object='src/bridge/test_pipe_channel-test-pipe-channel.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_pipe_channel_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_pipe_channel-test-pipe-channel.obj `if test -f 'src/bridge/test-pipe-channel.c'; then $(CYGPATH_W) 'src/bridge/test-pipe-channel.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-pipe-channel.c'; fi`
+
+src/bridge/test_process-test-process.o: src/bridge/test-process.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_process_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_process-test-process.o -MD -MP -MF src/bridge/$(DEPDIR)/test_process-test-process.Tpo -c -o src/bridge/test_process-test-process.o `test -f 'src/bridge/test-process.c' || echo '$(srcdir)/'`src/bridge/test-process.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_process-test-process.Tpo src/bridge/$(DEPDIR)/test_process-test-process.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-process.c' object='src/bridge/test_process-test-process.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_process_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_process-test-process.o `test -f 'src/bridge/test-process.c' || echo '$(srcdir)/'`src/bridge/test-process.c
+
+src/bridge/test_process-test-process.obj: src/bridge/test-process.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_process_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_process-test-process.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_process-test-process.Tpo -c -o src/bridge/test_process-test-process.obj `if test -f 'src/bridge/test-process.c'; then $(CYGPATH_W) 'src/bridge/test-process.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-process.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_process-test-process.Tpo src/bridge/$(DEPDIR)/test_process-test-process.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-process.c' object='src/bridge/test_process-test-process.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_process_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_process-test-process.obj `if test -f 'src/bridge/test-process.c'; then $(CYGPATH_W) 'src/bridge/test-process.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-process.c'; fi`
+
+src/bridge/test_router-test-router.o: src/bridge/test-router.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_router_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_router-test-router.o -MD -MP -MF src/bridge/$(DEPDIR)/test_router-test-router.Tpo -c -o src/bridge/test_router-test-router.o `test -f 'src/bridge/test-router.c' || echo '$(srcdir)/'`src/bridge/test-router.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_router-test-router.Tpo src/bridge/$(DEPDIR)/test_router-test-router.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-router.c' object='src/bridge/test_router-test-router.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_router_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_router-test-router.o `test -f 'src/bridge/test-router.c' || echo '$(srcdir)/'`src/bridge/test-router.c
+
+src/bridge/test_router-test-router.obj: src/bridge/test-router.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_router_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_router-test-router.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_router-test-router.Tpo -c -o src/bridge/test_router-test-router.obj `if test -f 'src/bridge/test-router.c'; then $(CYGPATH_W) 'src/bridge/test-router.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-router.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_router-test-router.Tpo src/bridge/$(DEPDIR)/test_router-test-router.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-router.c' object='src/bridge/test_router-test-router.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_router_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_router-test-router.obj `if test -f 'src/bridge/test-router.c'; then $(CYGPATH_W) 'src/bridge/test-router.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-router.c'; fi`
+
+src/bridge/test_rules-test-rules.o: src/bridge/test-rules.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_rules_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_rules-test-rules.o -MD -MP -MF src/bridge/$(DEPDIR)/test_rules-test-rules.Tpo -c -o src/bridge/test_rules-test-rules.o `test -f 'src/bridge/test-rules.c' || echo '$(srcdir)/'`src/bridge/test-rules.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_rules-test-rules.Tpo src/bridge/$(DEPDIR)/test_rules-test-rules.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-rules.c' object='src/bridge/test_rules-test-rules.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_rules_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_rules-test-rules.o `test -f 'src/bridge/test-rules.c' || echo '$(srcdir)/'`src/bridge/test-rules.c
+
+src/bridge/test_rules-test-rules.obj: src/bridge/test-rules.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_rules_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_rules-test-rules.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_rules-test-rules.Tpo -c -o src/bridge/test_rules-test-rules.obj `if test -f 'src/bridge/test-rules.c'; then $(CYGPATH_W) 'src/bridge/test-rules.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-rules.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_rules-test-rules.Tpo src/bridge/$(DEPDIR)/test_rules-test-rules.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-rules.c' object='src/bridge/test_rules-test-rules.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_rules_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_rules-test-rules.obj `if test -f 'src/bridge/test-rules.c'; then $(CYGPATH_W) 'src/bridge/test-rules.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-rules.c'; fi`
+
+src/ws/test_server-mock-service.o: src/ws/mock-service.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_server-mock-service.o -MD -MP -MF src/ws/$(DEPDIR)/test_server-mock-service.Tpo -c -o src/ws/test_server-mock-service.o `test -f 'src/ws/mock-service.c' || echo '$(srcdir)/'`src/ws/mock-service.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_server-mock-service.Tpo src/ws/$(DEPDIR)/test_server-mock-service.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/mock-service.c' object='src/ws/test_server-mock-service.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_server-mock-service.o `test -f 'src/ws/mock-service.c' || echo '$(srcdir)/'`src/ws/mock-service.c
+
+src/ws/test_server-mock-service.obj: src/ws/mock-service.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_server-mock-service.obj -MD -MP -MF src/ws/$(DEPDIR)/test_server-mock-service.Tpo -c -o src/ws/test_server-mock-service.obj `if test -f 'src/ws/mock-service.c'; then $(CYGPATH_W) 'src/ws/mock-service.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/mock-service.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_server-mock-service.Tpo src/ws/$(DEPDIR)/test_server-mock-service.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/mock-service.c' object='src/ws/test_server-mock-service.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_server-mock-service.obj `if test -f 'src/ws/mock-service.c'; then $(CYGPATH_W) 'src/ws/mock-service.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/mock-service.c'; fi`
+
+src/ws/test_server-test-server.o: src/ws/test-server.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_server-test-server.o -MD -MP -MF src/ws/$(DEPDIR)/test_server-test-server.Tpo -c -o src/ws/test_server-test-server.o `test -f 'src/ws/test-server.c' || echo '$(srcdir)/'`src/ws/test-server.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_server-test-server.Tpo src/ws/$(DEPDIR)/test_server-test-server.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-server.c' object='src/ws/test_server-test-server.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_server-test-server.o `test -f 'src/ws/test-server.c' || echo '$(srcdir)/'`src/ws/test-server.c
+
+src/ws/test_server-test-server.obj: src/ws/test-server.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_server-test-server.obj -MD -MP -MF src/ws/$(DEPDIR)/test_server-test-server.Tpo -c -o src/ws/test_server-test-server.obj `if test -f 'src/ws/test-server.c'; then $(CYGPATH_W) 'src/ws/test-server.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-server.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_server-test-server.Tpo src/ws/$(DEPDIR)/test_server-test-server.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-server.c' object='src/ws/test_server-test-server.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_server-test-server.obj `if test -f 'src/ws/test-server.c'; then $(CYGPATH_W) 'src/ws/test-server.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-server.c'; fi`
+
+src/ws/test_server-mock-dbus-tests.o: src/ws/mock-dbus-tests.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_server-mock-dbus-tests.o -MD -MP -MF src/ws/$(DEPDIR)/test_server-mock-dbus-tests.Tpo -c -o src/ws/test_server-mock-dbus-tests.o `test -f 'src/ws/mock-dbus-tests.c' || echo '$(srcdir)/'`src/ws/mock-dbus-tests.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_server-mock-dbus-tests.Tpo src/ws/$(DEPDIR)/test_server-mock-dbus-tests.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/mock-dbus-tests.c' object='src/ws/test_server-mock-dbus-tests.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_server-mock-dbus-tests.o `test -f 'src/ws/mock-dbus-tests.c' || echo '$(srcdir)/'`src/ws/mock-dbus-tests.c
+
+src/ws/test_server-mock-dbus-tests.obj: src/ws/mock-dbus-tests.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_server-mock-dbus-tests.obj -MD -MP -MF src/ws/$(DEPDIR)/test_server-mock-dbus-tests.Tpo -c -o src/ws/test_server-mock-dbus-tests.obj `if test -f 'src/ws/mock-dbus-tests.c'; then $(CYGPATH_W) 'src/ws/mock-dbus-tests.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/mock-dbus-tests.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_server-mock-dbus-tests.Tpo src/ws/$(DEPDIR)/test_server-mock-dbus-tests.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/mock-dbus-tests.c' object='src/ws/test_server-mock-dbus-tests.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_server-mock-dbus-tests.obj `if test -f 'src/ws/mock-dbus-tests.c'; then $(CYGPATH_W) 'src/ws/mock-dbus-tests.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/mock-dbus-tests.c'; fi`
+
+src/pam-ssh-add/test_ssh_add-test-ssh-add.o: src/pam-ssh-add/test-ssh-add.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_ssh_add_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/pam-ssh-add/test_ssh_add-test-ssh-add.o -MD -MP -MF src/pam-ssh-add/$(DEPDIR)/test_ssh_add-test-ssh-add.Tpo -c -o src/pam-ssh-add/test_ssh_add-test-ssh-add.o `test -f 'src/pam-ssh-add/test-ssh-add.c' || echo '$(srcdir)/'`src/pam-ssh-add/test-ssh-add.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/pam-ssh-add/$(DEPDIR)/test_ssh_add-test-ssh-add.Tpo src/pam-ssh-add/$(DEPDIR)/test_ssh_add-test-ssh-add.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/pam-ssh-add/test-ssh-add.c' object='src/pam-ssh-add/test_ssh_add-test-ssh-add.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_ssh_add_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/pam-ssh-add/test_ssh_add-test-ssh-add.o `test -f 'src/pam-ssh-add/test-ssh-add.c' || echo '$(srcdir)/'`src/pam-ssh-add/test-ssh-add.c
+
+src/pam-ssh-add/test_ssh_add-test-ssh-add.obj: src/pam-ssh-add/test-ssh-add.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_ssh_add_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/pam-ssh-add/test_ssh_add-test-ssh-add.obj -MD -MP -MF src/pam-ssh-add/$(DEPDIR)/test_ssh_add-test-ssh-add.Tpo -c -o src/pam-ssh-add/test_ssh_add-test-ssh-add.obj `if test -f 'src/pam-ssh-add/test-ssh-add.c'; then $(CYGPATH_W) 'src/pam-ssh-add/test-ssh-add.c'; else $(CYGPATH_W) '$(srcdir)/src/pam-ssh-add/test-ssh-add.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/pam-ssh-add/$(DEPDIR)/test_ssh_add-test-ssh-add.Tpo src/pam-ssh-add/$(DEPDIR)/test_ssh_add-test-ssh-add.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/pam-ssh-add/test-ssh-add.c' object='src/pam-ssh-add/test_ssh_add-test-ssh-add.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_ssh_add_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/pam-ssh-add/test_ssh_add-test-ssh-add.obj `if test -f 'src/pam-ssh-add/test-ssh-add.c'; then $(CYGPATH_W) 'src/pam-ssh-add/test-ssh-add.c'; else $(CYGPATH_W) '$(srcdir)/src/pam-ssh-add/test-ssh-add.c'; fi`
+
+src/ssh/test_sshbridge-test-sshbridge.o: src/ssh/test-sshbridge.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_sshbridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/test_sshbridge-test-sshbridge.o -MD -MP -MF src/ssh/$(DEPDIR)/test_sshbridge-test-sshbridge.Tpo -c -o src/ssh/test_sshbridge-test-sshbridge.o `test -f 'src/ssh/test-sshbridge.c' || echo '$(srcdir)/'`src/ssh/test-sshbridge.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/test_sshbridge-test-sshbridge.Tpo src/ssh/$(DEPDIR)/test_sshbridge-test-sshbridge.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/test-sshbridge.c' object='src/ssh/test_sshbridge-test-sshbridge.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_sshbridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/test_sshbridge-test-sshbridge.o `test -f 'src/ssh/test-sshbridge.c' || echo '$(srcdir)/'`src/ssh/test-sshbridge.c
+
+src/ssh/test_sshbridge-test-sshbridge.obj: src/ssh/test-sshbridge.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_sshbridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/test_sshbridge-test-sshbridge.obj -MD -MP -MF src/ssh/$(DEPDIR)/test_sshbridge-test-sshbridge.Tpo -c -o src/ssh/test_sshbridge-test-sshbridge.obj `if test -f 'src/ssh/test-sshbridge.c'; then $(CYGPATH_W) 'src/ssh/test-sshbridge.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/test-sshbridge.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/test_sshbridge-test-sshbridge.Tpo src/ssh/$(DEPDIR)/test_sshbridge-test-sshbridge.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/test-sshbridge.c' object='src/ssh/test_sshbridge-test-sshbridge.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_sshbridge_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/test_sshbridge-test-sshbridge.obj `if test -f 'src/ssh/test-sshbridge.c'; then $(CYGPATH_W) 'src/ssh/test-sshbridge.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/test-sshbridge.c'; fi`
+
+src/ssh/test_sshoptions-test-sshoptions.o: src/ssh/test-sshoptions.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_sshoptions_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/test_sshoptions-test-sshoptions.o -MD -MP -MF src/ssh/$(DEPDIR)/test_sshoptions-test-sshoptions.Tpo -c -o src/ssh/test_sshoptions-test-sshoptions.o `test -f 'src/ssh/test-sshoptions.c' || echo '$(srcdir)/'`src/ssh/test-sshoptions.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/test_sshoptions-test-sshoptions.Tpo src/ssh/$(DEPDIR)/test_sshoptions-test-sshoptions.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/test-sshoptions.c' object='src/ssh/test_sshoptions-test-sshoptions.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_sshoptions_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/test_sshoptions-test-sshoptions.o `test -f 'src/ssh/test-sshoptions.c' || echo '$(srcdir)/'`src/ssh/test-sshoptions.c
+
+src/ssh/test_sshoptions-test-sshoptions.obj: src/ssh/test-sshoptions.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_sshoptions_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ssh/test_sshoptions-test-sshoptions.obj -MD -MP -MF src/ssh/$(DEPDIR)/test_sshoptions-test-sshoptions.Tpo -c -o src/ssh/test_sshoptions-test-sshoptions.obj `if test -f 'src/ssh/test-sshoptions.c'; then $(CYGPATH_W) 'src/ssh/test-sshoptions.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/test-sshoptions.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ssh/$(DEPDIR)/test_sshoptions-test-sshoptions.Tpo src/ssh/$(DEPDIR)/test_sshoptions-test-sshoptions.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ssh/test-sshoptions.c' object='src/ssh/test_sshoptions-test-sshoptions.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_sshoptions_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ssh/test_sshoptions-test-sshoptions.obj `if test -f 'src/ssh/test-sshoptions.c'; then $(CYGPATH_W) 'src/ssh/test-sshoptions.c'; else $(CYGPATH_W) '$(srcdir)/src/ssh/test-sshoptions.c'; fi`
+
+src/bridge/test_stream-test-stream.o: src/bridge/test-stream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_stream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_stream-test-stream.o -MD -MP -MF src/bridge/$(DEPDIR)/test_stream-test-stream.Tpo -c -o src/bridge/test_stream-test-stream.o `test -f 'src/bridge/test-stream.c' || echo '$(srcdir)/'`src/bridge/test-stream.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_stream-test-stream.Tpo src/bridge/$(DEPDIR)/test_stream-test-stream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-stream.c' object='src/bridge/test_stream-test-stream.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_stream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_stream-test-stream.o `test -f 'src/bridge/test-stream.c' || echo '$(srcdir)/'`src/bridge/test-stream.c
+
+src/bridge/test_stream-test-stream.obj: src/bridge/test-stream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_stream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_stream-test-stream.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_stream-test-stream.Tpo -c -o src/bridge/test_stream-test-stream.obj `if test -f 'src/bridge/test-stream.c'; then $(CYGPATH_W) 'src/bridge/test-stream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-stream.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_stream-test-stream.Tpo src/bridge/$(DEPDIR)/test_stream-test-stream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-stream.c' object='src/bridge/test_stream-test-stream.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_stream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_stream-test-stream.obj `if test -f 'src/bridge/test-stream.c'; then $(CYGPATH_W) 'src/bridge/test-stream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-stream.c'; fi`
+
+src/common/test_system-test-system.o: src/common/test-system.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_system_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_system-test-system.o -MD -MP -MF src/common/$(DEPDIR)/test_system-test-system.Tpo -c -o src/common/test_system-test-system.o `test -f 'src/common/test-system.c' || echo '$(srcdir)/'`src/common/test-system.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_system-test-system.Tpo src/common/$(DEPDIR)/test_system-test-system.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-system.c' object='src/common/test_system-test-system.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_system_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_system-test-system.o `test -f 'src/common/test-system.c' || echo '$(srcdir)/'`src/common/test-system.c
+
+src/common/test_system-test-system.obj: src/common/test-system.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_system_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_system-test-system.obj -MD -MP -MF src/common/$(DEPDIR)/test_system-test-system.Tpo -c -o src/common/test_system-test-system.obj `if test -f 'src/common/test-system.c'; then $(CYGPATH_W) 'src/common/test-system.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-system.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_system-test-system.Tpo src/common/$(DEPDIR)/test_system-test-system.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-system.c' object='src/common/test_system-test-system.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_system_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_system-test-system.obj `if test -f 'src/common/test-system.c'; then $(CYGPATH_W) 'src/common/test-system.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-system.c'; fi`
+
+src/common/test_template-test-template.o: src/common/test-template.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_template_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_template-test-template.o -MD -MP -MF src/common/$(DEPDIR)/test_template-test-template.Tpo -c -o src/common/test_template-test-template.o `test -f 'src/common/test-template.c' || echo '$(srcdir)/'`src/common/test-template.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_template-test-template.Tpo src/common/$(DEPDIR)/test_template-test-template.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-template.c' object='src/common/test_template-test-template.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_template_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_template-test-template.o `test -f 'src/common/test-template.c' || echo '$(srcdir)/'`src/common/test-template.c
+
+src/common/test_template-test-template.obj: src/common/test-template.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_template_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_template-test-template.obj -MD -MP -MF src/common/$(DEPDIR)/test_template-test-template.Tpo -c -o src/common/test_template-test-template.obj `if test -f 'src/common/test-template.c'; then $(CYGPATH_W) 'src/common/test-template.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-template.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_template-test-template.Tpo src/common/$(DEPDIR)/test_template-test-template.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-template.c' object='src/common/test_template-test-template.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_template_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_template-test-template.obj `if test -f 'src/common/test-template.c'; then $(CYGPATH_W) 'src/common/test-template.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-template.c'; fi`
+
+src/tls/test_tls_connection-test-connection.o: src/tls/test-connection.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_tls_connection_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/tls/test_tls_connection-test-connection.o -MD -MP -MF src/tls/$(DEPDIR)/test_tls_connection-test-connection.Tpo -c -o src/tls/test_tls_connection-test-connection.o `test -f 'src/tls/test-connection.c' || echo '$(srcdir)/'`src/tls/test-connection.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/tls/$(DEPDIR)/test_tls_connection-test-connection.Tpo src/tls/$(DEPDIR)/test_tls_connection-test-connection.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/tls/test-connection.c' object='src/tls/test_tls_connection-test-connection.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_tls_connection_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/tls/test_tls_connection-test-connection.o `test -f 'src/tls/test-connection.c' || echo '$(srcdir)/'`src/tls/test-connection.c
+
+src/tls/test_tls_connection-test-connection.obj: src/tls/test-connection.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_tls_connection_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/tls/test_tls_connection-test-connection.obj -MD -MP -MF src/tls/$(DEPDIR)/test_tls_connection-test-connection.Tpo -c -o src/tls/test_tls_connection-test-connection.obj `if test -f 'src/tls/test-connection.c'; then $(CYGPATH_W) 'src/tls/test-connection.c'; else $(CYGPATH_W) '$(srcdir)/src/tls/test-connection.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/tls/$(DEPDIR)/test_tls_connection-test-connection.Tpo src/tls/$(DEPDIR)/test_tls_connection-test-connection.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/tls/test-connection.c' object='src/tls/test_tls_connection-test-connection.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_tls_connection_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/tls/test_tls_connection-test-connection.obj `if test -f 'src/tls/test-connection.c'; then $(CYGPATH_W) 'src/tls/test-connection.c'; else $(CYGPATH_W) '$(srcdir)/src/tls/test-connection.c'; fi`
+
+src/tls/test_tls_server-test-server.o: src/tls/test-server.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_tls_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/tls/test_tls_server-test-server.o -MD -MP -MF src/tls/$(DEPDIR)/test_tls_server-test-server.Tpo -c -o src/tls/test_tls_server-test-server.o `test -f 'src/tls/test-server.c' || echo '$(srcdir)/'`src/tls/test-server.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/tls/$(DEPDIR)/test_tls_server-test-server.Tpo src/tls/$(DEPDIR)/test_tls_server-test-server.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/tls/test-server.c' object='src/tls/test_tls_server-test-server.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_tls_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/tls/test_tls_server-test-server.o `test -f 'src/tls/test-server.c' || echo '$(srcdir)/'`src/tls/test-server.c
+
+src/tls/test_tls_server-test-server.obj: src/tls/test-server.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_tls_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/tls/test_tls_server-test-server.obj -MD -MP -MF src/tls/$(DEPDIR)/test_tls_server-test-server.Tpo -c -o src/tls/test_tls_server-test-server.obj `if test -f 'src/tls/test-server.c'; then $(CYGPATH_W) 'src/tls/test-server.c'; else $(CYGPATH_W) '$(srcdir)/src/tls/test-server.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/tls/$(DEPDIR)/test_tls_server-test-server.Tpo src/tls/$(DEPDIR)/test_tls_server-test-server.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/tls/test-server.c' object='src/tls/test_tls_server-test-server.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_tls_server_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/tls/test_tls_server-test-server.obj `if test -f 'src/tls/test-server.c'; then $(CYGPATH_W) 'src/tls/test-server.c'; else $(CYGPATH_W) '$(srcdir)/src/tls/test-server.c'; fi`
+
+src/common/test_transport-test-transport.o: src/common/test-transport.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_transport_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_transport-test-transport.o -MD -MP -MF src/common/$(DEPDIR)/test_transport-test-transport.Tpo -c -o src/common/test_transport-test-transport.o `test -f 'src/common/test-transport.c' || echo '$(srcdir)/'`src/common/test-transport.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_transport-test-transport.Tpo src/common/$(DEPDIR)/test_transport-test-transport.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-transport.c' object='src/common/test_transport-test-transport.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_transport_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_transport-test-transport.o `test -f 'src/common/test-transport.c' || echo '$(srcdir)/'`src/common/test-transport.c
+
+src/common/test_transport-test-transport.obj: src/common/test-transport.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_transport_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_transport-test-transport.obj -MD -MP -MF src/common/$(DEPDIR)/test_transport-test-transport.Tpo -c -o src/common/test_transport-test-transport.obj `if test -f 'src/common/test-transport.c'; then $(CYGPATH_W) 'src/common/test-transport.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-transport.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_transport-test-transport.Tpo src/common/$(DEPDIR)/test_transport-test-transport.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-transport.c' object='src/common/test_transport-test-transport.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_transport_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_transport-test-transport.obj `if test -f 'src/common/test-transport.c'; then $(CYGPATH_W) 'src/common/test-transport.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-transport.c'; fi`
+
+src/common/test_unicode-test-unicode.o: src/common/test-unicode.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_unicode_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_unicode-test-unicode.o -MD -MP -MF src/common/$(DEPDIR)/test_unicode-test-unicode.Tpo -c -o src/common/test_unicode-test-unicode.o `test -f 'src/common/test-unicode.c' || echo '$(srcdir)/'`src/common/test-unicode.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_unicode-test-unicode.Tpo src/common/$(DEPDIR)/test_unicode-test-unicode.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-unicode.c' object='src/common/test_unicode-test-unicode.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_unicode_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_unicode-test-unicode.o `test -f 'src/common/test-unicode.c' || echo '$(srcdir)/'`src/common/test-unicode.c
+
+src/common/test_unicode-test-unicode.obj: src/common/test-unicode.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_unicode_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_unicode-test-unicode.obj -MD -MP -MF src/common/$(DEPDIR)/test_unicode-test-unicode.Tpo -c -o src/common/test_unicode-test-unicode.obj `if test -f 'src/common/test-unicode.c'; then $(CYGPATH_W) 'src/common/test-unicode.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-unicode.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_unicode-test-unicode.Tpo src/common/$(DEPDIR)/test_unicode-test-unicode.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-unicode.c' object='src/common/test_unicode-test-unicode.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_unicode_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_unicode-test-unicode.obj `if test -f 'src/common/test-unicode.c'; then $(CYGPATH_W) 'src/common/test-unicode.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-unicode.c'; fi`
+
+src/common/test_unixsignal-test-unixsignal.o: src/common/test-unixsignal.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_unixsignal_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_unixsignal-test-unixsignal.o -MD -MP -MF src/common/$(DEPDIR)/test_unixsignal-test-unixsignal.Tpo -c -o src/common/test_unixsignal-test-unixsignal.o `test -f 'src/common/test-unixsignal.c' || echo '$(srcdir)/'`src/common/test-unixsignal.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_unixsignal-test-unixsignal.Tpo src/common/$(DEPDIR)/test_unixsignal-test-unixsignal.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-unixsignal.c' object='src/common/test_unixsignal-test-unixsignal.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_unixsignal_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_unixsignal-test-unixsignal.o `test -f 'src/common/test-unixsignal.c' || echo '$(srcdir)/'`src/common/test-unixsignal.c
+
+src/common/test_unixsignal-test-unixsignal.obj: src/common/test-unixsignal.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_unixsignal_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_unixsignal-test-unixsignal.obj -MD -MP -MF src/common/$(DEPDIR)/test_unixsignal-test-unixsignal.Tpo -c -o src/common/test_unixsignal-test-unixsignal.obj `if test -f 'src/common/test-unixsignal.c'; then $(CYGPATH_W) 'src/common/test-unixsignal.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-unixsignal.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_unixsignal-test-unixsignal.Tpo src/common/$(DEPDIR)/test_unixsignal-test-unixsignal.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-unixsignal.c' object='src/common/test_unixsignal-test-unixsignal.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_unixsignal_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_unixsignal-test-unixsignal.obj `if test -f 'src/common/test-unixsignal.c'; then $(CYGPATH_W) 'src/common/test-unixsignal.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-unixsignal.c'; fi`
+
+src/common/test_version-test-version.o: src/common/test-version.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_version_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_version-test-version.o -MD -MP -MF src/common/$(DEPDIR)/test_version-test-version.Tpo -c -o src/common/test_version-test-version.o `test -f 'src/common/test-version.c' || echo '$(srcdir)/'`src/common/test-version.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_version-test-version.Tpo src/common/$(DEPDIR)/test_version-test-version.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-version.c' object='src/common/test_version-test-version.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_version_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_version-test-version.o `test -f 'src/common/test-version.c' || echo '$(srcdir)/'`src/common/test-version.c
+
+src/common/test_version-test-version.obj: src/common/test-version.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_version_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_version-test-version.obj -MD -MP -MF src/common/$(DEPDIR)/test_version-test-version.Tpo -c -o src/common/test_version-test-version.obj `if test -f 'src/common/test-version.c'; then $(CYGPATH_W) 'src/common/test-version.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-version.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_version-test-version.Tpo src/common/$(DEPDIR)/test_version-test-version.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-version.c' object='src/common/test_version-test-version.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_version_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_version-test-version.obj `if test -f 'src/common/test-version.c'; then $(CYGPATH_W) 'src/common/test-version.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-version.c'; fi`
+
+src/common/test_webcertificate-test-webcertificate.o: src/common/test-webcertificate.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webcertificate_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_webcertificate-test-webcertificate.o -MD -MP -MF src/common/$(DEPDIR)/test_webcertificate-test-webcertificate.Tpo -c -o src/common/test_webcertificate-test-webcertificate.o `test -f 'src/common/test-webcertificate.c' || echo '$(srcdir)/'`src/common/test-webcertificate.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_webcertificate-test-webcertificate.Tpo src/common/$(DEPDIR)/test_webcertificate-test-webcertificate.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-webcertificate.c' object='src/common/test_webcertificate-test-webcertificate.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webcertificate_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_webcertificate-test-webcertificate.o `test -f 'src/common/test-webcertificate.c' || echo '$(srcdir)/'`src/common/test-webcertificate.c
+
+src/common/test_webcertificate-test-webcertificate.obj: src/common/test-webcertificate.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webcertificate_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_webcertificate-test-webcertificate.obj -MD -MP -MF src/common/$(DEPDIR)/test_webcertificate-test-webcertificate.Tpo -c -o src/common/test_webcertificate-test-webcertificate.obj `if test -f 'src/common/test-webcertificate.c'; then $(CYGPATH_W) 'src/common/test-webcertificate.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-webcertificate.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_webcertificate-test-webcertificate.Tpo src/common/$(DEPDIR)/test_webcertificate-test-webcertificate.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-webcertificate.c' object='src/common/test_webcertificate-test-webcertificate.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webcertificate_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_webcertificate-test-webcertificate.obj `if test -f 'src/common/test-webcertificate.c'; then $(CYGPATH_W) 'src/common/test-webcertificate.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-webcertificate.c'; fi`
+
+src/common/test_webresponse-test-webresponse.o: src/common/test-webresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webresponse_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_webresponse-test-webresponse.o -MD -MP -MF src/common/$(DEPDIR)/test_webresponse-test-webresponse.Tpo -c -o src/common/test_webresponse-test-webresponse.o `test -f 'src/common/test-webresponse.c' || echo '$(srcdir)/'`src/common/test-webresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_webresponse-test-webresponse.Tpo src/common/$(DEPDIR)/test_webresponse-test-webresponse.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-webresponse.c' object='src/common/test_webresponse-test-webresponse.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webresponse_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_webresponse-test-webresponse.o `test -f 'src/common/test-webresponse.c' || echo '$(srcdir)/'`src/common/test-webresponse.c
+
+src/common/test_webresponse-test-webresponse.obj: src/common/test-webresponse.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webresponse_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_webresponse-test-webresponse.obj -MD -MP -MF src/common/$(DEPDIR)/test_webresponse-test-webresponse.Tpo -c -o src/common/test_webresponse-test-webresponse.obj `if test -f 'src/common/test-webresponse.c'; then $(CYGPATH_W) 'src/common/test-webresponse.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-webresponse.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_webresponse-test-webresponse.Tpo src/common/$(DEPDIR)/test_webresponse-test-webresponse.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-webresponse.c' object='src/common/test_webresponse-test-webresponse.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webresponse_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_webresponse-test-webresponse.obj `if test -f 'src/common/test-webresponse.c'; then $(CYGPATH_W) 'src/common/test-webresponse.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-webresponse.c'; fi`
+
+src/common/test_webserver-test-webserver.o: src/common/test-webserver.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webserver_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_webserver-test-webserver.o -MD -MP -MF src/common/$(DEPDIR)/test_webserver-test-webserver.Tpo -c -o src/common/test_webserver-test-webserver.o `test -f 'src/common/test-webserver.c' || echo '$(srcdir)/'`src/common/test-webserver.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_webserver-test-webserver.Tpo src/common/$(DEPDIR)/test_webserver-test-webserver.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-webserver.c' object='src/common/test_webserver-test-webserver.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webserver_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_webserver-test-webserver.o `test -f 'src/common/test-webserver.c' || echo '$(srcdir)/'`src/common/test-webserver.c
+
+src/common/test_webserver-test-webserver.obj: src/common/test-webserver.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webserver_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/common/test_webserver-test-webserver.obj -MD -MP -MF src/common/$(DEPDIR)/test_webserver-test-webserver.Tpo -c -o src/common/test_webserver-test-webserver.obj `if test -f 'src/common/test-webserver.c'; then $(CYGPATH_W) 'src/common/test-webserver.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-webserver.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/common/$(DEPDIR)/test_webserver-test-webserver.Tpo src/common/$(DEPDIR)/test_webserver-test-webserver.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/common/test-webserver.c' object='src/common/test_webserver-test-webserver.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webserver_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/common/test_webserver-test-webserver.obj `if test -f 'src/common/test-webserver.c'; then $(CYGPATH_W) 'src/common/test-webserver.c'; else $(CYGPATH_W) '$(srcdir)/src/common/test-webserver.c'; fi`
+
+src/ws/test_webservice-test-webservice.o: src/ws/test-webservice.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webservice_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_webservice-test-webservice.o -MD -MP -MF src/ws/$(DEPDIR)/test_webservice-test-webservice.Tpo -c -o src/ws/test_webservice-test-webservice.o `test -f 'src/ws/test-webservice.c' || echo '$(srcdir)/'`src/ws/test-webservice.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_webservice-test-webservice.Tpo src/ws/$(DEPDIR)/test_webservice-test-webservice.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-webservice.c' object='src/ws/test_webservice-test-webservice.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webservice_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_webservice-test-webservice.o `test -f 'src/ws/test-webservice.c' || echo '$(srcdir)/'`src/ws/test-webservice.c
+
+src/ws/test_webservice-test-webservice.obj: src/ws/test-webservice.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webservice_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/ws/test_webservice-test-webservice.obj -MD -MP -MF src/ws/$(DEPDIR)/test_webservice-test-webservice.Tpo -c -o src/ws/test_webservice-test-webservice.obj `if test -f 'src/ws/test-webservice.c'; then $(CYGPATH_W) 'src/ws/test-webservice.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-webservice.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/ws/$(DEPDIR)/test_webservice-test-webservice.Tpo src/ws/$(DEPDIR)/test_webservice-test-webservice.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/ws/test-webservice.c' object='src/ws/test_webservice-test-webservice.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_webservice_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/ws/test_webservice-test-webservice.obj `if test -f 'src/ws/test-webservice.c'; then $(CYGPATH_W) 'src/ws/test-webservice.c'; else $(CYGPATH_W) '$(srcdir)/src/ws/test-webservice.c'; fi`
+
+src/websocket/test_websocket-test-websocket.o: src/websocket/test-websocket.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_websocket_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/test_websocket-test-websocket.o -MD -MP -MF src/websocket/$(DEPDIR)/test_websocket-test-websocket.Tpo -c -o src/websocket/test_websocket-test-websocket.o `test -f 'src/websocket/test-websocket.c' || echo '$(srcdir)/'`src/websocket/test-websocket.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/test_websocket-test-websocket.Tpo src/websocket/$(DEPDIR)/test_websocket-test-websocket.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/test-websocket.c' object='src/websocket/test_websocket-test-websocket.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_websocket_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/test_websocket-test-websocket.o `test -f 'src/websocket/test-websocket.c' || echo '$(srcdir)/'`src/websocket/test-websocket.c
+
+src/websocket/test_websocket-test-websocket.obj: src/websocket/test-websocket.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_websocket_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/websocket/test_websocket-test-websocket.obj -MD -MP -MF src/websocket/$(DEPDIR)/test_websocket-test-websocket.Tpo -c -o src/websocket/test_websocket-test-websocket.obj `if test -f 'src/websocket/test-websocket.c'; then $(CYGPATH_W) 'src/websocket/test-websocket.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/test-websocket.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/websocket/$(DEPDIR)/test_websocket-test-websocket.Tpo src/websocket/$(DEPDIR)/test_websocket-test-websocket.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/websocket/test-websocket.c' object='src/websocket/test_websocket-test-websocket.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_websocket_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/websocket/test_websocket-test-websocket.obj `if test -f 'src/websocket/test-websocket.c'; then $(CYGPATH_W) 'src/websocket/test-websocket.c'; else $(CYGPATH_W) '$(srcdir)/src/websocket/test-websocket.c'; fi`
+
+src/bridge/test_websocketstream-test-websocketstream.o: src/bridge/test-websocketstream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_websocketstream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_websocketstream-test-websocketstream.o -MD -MP -MF src/bridge/$(DEPDIR)/test_websocketstream-test-websocketstream.Tpo -c -o src/bridge/test_websocketstream-test-websocketstream.o `test -f 'src/bridge/test-websocketstream.c' || echo '$(srcdir)/'`src/bridge/test-websocketstream.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_websocketstream-test-websocketstream.Tpo src/bridge/$(DEPDIR)/test_websocketstream-test-websocketstream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-websocketstream.c' object='src/bridge/test_websocketstream-test-websocketstream.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_websocketstream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_websocketstream-test-websocketstream.o `test -f 'src/bridge/test-websocketstream.c' || echo '$(srcdir)/'`src/bridge/test-websocketstream.c
+
+src/bridge/test_websocketstream-test-websocketstream.obj: src/bridge/test-websocketstream.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_websocketstream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT src/bridge/test_websocketstream-test-websocketstream.obj -MD -MP -MF src/bridge/$(DEPDIR)/test_websocketstream-test-websocketstream.Tpo -c -o src/bridge/test_websocketstream-test-websocketstream.obj `if test -f 'src/bridge/test-websocketstream.c'; then $(CYGPATH_W) 'src/bridge/test-websocketstream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-websocketstream.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) src/bridge/$(DEPDIR)/test_websocketstream-test-websocketstream.Tpo src/bridge/$(DEPDIR)/test_websocketstream-test-websocketstream.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='src/bridge/test-websocketstream.c' object='src/bridge/test_websocketstream-test-websocketstream.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(test_websocketstream_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o src/bridge/test_websocketstream-test-websocketstream.obj `if test -f 'src/bridge/test-websocketstream.c'; then $(CYGPATH_W) 'src/bridge/test-websocketstream.c'; else $(CYGPATH_W) '$(srcdir)/src/bridge/test-websocketstream.c'; fi`
+install-man1: $(man_MANS)
+ @$(NORMAL_INSTALL)
+ @list1=''; \
+ list2='$(man_MANS)'; \
+ test -n "$(man1dir)" \
+ && test -n "`echo $$list1$$list2`" \
+ || exit 0; \
+ echo " $(MKDIR_P) '$(DESTDIR)$(man1dir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(man1dir)" || exit 1; \
+ { for i in $$list1; do echo "$$i"; done; \
+ if test -n "$$list2"; then \
+ for i in $$list2; do echo "$$i"; done \
+ | sed -n '/\.1[a-z]*$$/p'; \
+ fi; \
+ } | while read p; do \
+ if test -f $$p; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; echo "$$p"; \
+ done | \
+ sed -e 'n;s,.*/,,;p;h;s,.*\.,,;s,^[^1][0-9a-z]*$$,1,;x' \
+ -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,' | \
+ sed 'N;N;s,\n, ,g' | { \
+ list=; while read file base inst; do \
+ if test "$$base" = "$$inst"; then list="$$list $$file"; else \
+ echo " $(INSTALL_DATA) '$$file' '$(DESTDIR)$(man1dir)/$$inst'"; \
+ $(INSTALL_DATA) "$$file" "$(DESTDIR)$(man1dir)/$$inst" || exit $$?; \
+ fi; \
+ done; \
+ for i in $$list; do echo "$$i"; done | $(am__base_list) | \
+ while read files; do \
+ test -z "$$files" || { \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(man1dir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(man1dir)" || exit $$?; }; \
+ done; }
+
+uninstall-man1:
+ @$(NORMAL_UNINSTALL)
+ @list=''; test -n "$(man1dir)" || exit 0; \
+ files=`{ for i in $$list; do echo "$$i"; done; \
+ l2='$(man_MANS)'; for i in $$l2; do echo "$$i"; done | \
+ sed -n '/\.1[a-z]*$$/p'; \
+ } | sed -e 's,.*/,,;h;s,.*\.,,;s,^[^1][0-9a-z]*$$,1,;x' \
+ -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,'`; \
+ dir='$(DESTDIR)$(man1dir)'; $(am__uninstall_files_from_dir)
+install-man5: $(man_MANS)
+ @$(NORMAL_INSTALL)
+ @list1=''; \
+ list2='$(man_MANS)'; \
+ test -n "$(man5dir)" \
+ && test -n "`echo $$list1$$list2`" \
+ || exit 0; \
+ echo " $(MKDIR_P) '$(DESTDIR)$(man5dir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(man5dir)" || exit 1; \
+ { for i in $$list1; do echo "$$i"; done; \
+ if test -n "$$list2"; then \
+ for i in $$list2; do echo "$$i"; done \
+ | sed -n '/\.5[a-z]*$$/p'; \
+ fi; \
+ } | while read p; do \
+ if test -f $$p; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; echo "$$p"; \
+ done | \
+ sed -e 'n;s,.*/,,;p;h;s,.*\.,,;s,^[^5][0-9a-z]*$$,5,;x' \
+ -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,' | \
+ sed 'N;N;s,\n, ,g' | { \
+ list=; while read file base inst; do \
+ if test "$$base" = "$$inst"; then list="$$list $$file"; else \
+ echo " $(INSTALL_DATA) '$$file' '$(DESTDIR)$(man5dir)/$$inst'"; \
+ $(INSTALL_DATA) "$$file" "$(DESTDIR)$(man5dir)/$$inst" || exit $$?; \
+ fi; \
+ done; \
+ for i in $$list; do echo "$$i"; done | $(am__base_list) | \
+ while read files; do \
+ test -z "$$files" || { \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(man5dir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(man5dir)" || exit $$?; }; \
+ done; }
+
+uninstall-man5:
+ @$(NORMAL_UNINSTALL)
+ @list=''; test -n "$(man5dir)" || exit 0; \
+ files=`{ for i in $$list; do echo "$$i"; done; \
+ l2='$(man_MANS)'; for i in $$l2; do echo "$$i"; done | \
+ sed -n '/\.5[a-z]*$$/p'; \
+ } | sed -e 's,.*/,,;h;s,.*\.,,;s,^[^5][0-9a-z]*$$,5,;x' \
+ -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,'`; \
+ dir='$(DESTDIR)$(man5dir)'; $(am__uninstall_files_from_dir)
+install-man8: $(man_MANS)
+ @$(NORMAL_INSTALL)
+ @list1=''; \
+ list2='$(man_MANS)'; \
+ test -n "$(man8dir)" \
+ && test -n "`echo $$list1$$list2`" \
+ || exit 0; \
+ echo " $(MKDIR_P) '$(DESTDIR)$(man8dir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(man8dir)" || exit 1; \
+ { for i in $$list1; do echo "$$i"; done; \
+ if test -n "$$list2"; then \
+ for i in $$list2; do echo "$$i"; done \
+ | sed -n '/\.8[a-z]*$$/p'; \
+ fi; \
+ } | while read p; do \
+ if test -f $$p; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; echo "$$p"; \
+ done | \
+ sed -e 'n;s,.*/,,;p;h;s,.*\.,,;s,^[^8][0-9a-z]*$$,8,;x' \
+ -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,' | \
+ sed 'N;N;s,\n, ,g' | { \
+ list=; while read file base inst; do \
+ if test "$$base" = "$$inst"; then list="$$list $$file"; else \
+ echo " $(INSTALL_DATA) '$$file' '$(DESTDIR)$(man8dir)/$$inst'"; \
+ $(INSTALL_DATA) "$$file" "$(DESTDIR)$(man8dir)/$$inst" || exit $$?; \
+ fi; \
+ done; \
+ for i in $$list; do echo "$$i"; done | $(am__base_list) | \
+ while read files; do \
+ test -z "$$files" || { \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(man8dir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(man8dir)" || exit $$?; }; \
+ done; }
+
+uninstall-man8:
+ @$(NORMAL_UNINSTALL)
+ @list=''; test -n "$(man8dir)" || exit 0; \
+ files=`{ for i in $$list; do echo "$$i"; done; \
+ l2='$(man_MANS)'; for i in $$l2; do echo "$$i"; done | \
+ sed -n '/\.8[a-z]*$$/p'; \
+ } | sed -e 's,.*/,,;h;s,.*\.,,;s,^[^8][0-9a-z]*$$,8,;x' \
+ -e 's,\.[0-9a-z]*$$,,;$(transform);G;s,\n,.,'`; \
+ dir='$(DESTDIR)$(man8dir)'; $(am__uninstall_files_from_dir)
+install-dist_applicationsDATA: $(dist_applications_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_applications_DATA)'; test -n "$(applicationsdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(applicationsdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(applicationsdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(applicationsdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(applicationsdir)" || exit $$?; \
+ done
+
+uninstall-dist_applicationsDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_applications_DATA)'; test -n "$(applicationsdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(applicationsdir)'; $(am__uninstall_files_from_dir)
+install-dist_archbrandingDATA: $(dist_archbranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_archbranding_DATA)'; test -n "$(archbrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(archbrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(archbrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(archbrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(archbrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_archbrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_archbranding_DATA)'; test -n "$(archbrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(archbrandingdir)'; $(am__uninstall_files_from_dir)
+install-dist_centosbrandingDATA: $(dist_centosbranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_centosbranding_DATA)'; test -n "$(centosbrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(centosbrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(centosbrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(centosbrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(centosbrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_centosbrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_centosbranding_DATA)'; test -n "$(centosbrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(centosbrandingdir)'; $(am__uninstall_files_from_dir)
+install-dist_client_metainfoDATA: $(dist_client_metainfo_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_client_metainfo_DATA)'; test -n "$(client_metainfodir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(client_metainfodir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(client_metainfodir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(client_metainfodir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(client_metainfodir)" || exit $$?; \
+ done
+
+uninstall-dist_client_metainfoDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_client_metainfo_DATA)'; test -n "$(client_metainfodir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(client_metainfodir)'; $(am__uninstall_files_from_dir)
+install-dist_cockpitclientDATA: $(dist_cockpitclient_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_cockpitclient_DATA)'; test -n "$(cockpitclientdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(cockpitclientdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(cockpitclientdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(cockpitclientdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(cockpitclientdir)" || exit $$?; \
+ done
+
+uninstall-dist_cockpitclientDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_cockpitclient_DATA)'; test -n "$(cockpitclientdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(cockpitclientdir)'; $(am__uninstall_files_from_dir)
+install-dist_dbusservicesDATA: $(dist_dbusservices_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_dbusservices_DATA)'; test -n "$(dbusservicesdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(dbusservicesdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(dbusservicesdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(dbusservicesdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(dbusservicesdir)" || exit $$?; \
+ done
+
+uninstall-dist_dbusservicesDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_dbusservices_DATA)'; test -n "$(dbusservicesdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(dbusservicesdir)'; $(am__uninstall_files_from_dir)
+install-dist_debianbrandingDATA: $(dist_debianbranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_debianbranding_DATA)'; test -n "$(debianbrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(debianbrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(debianbrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(debianbrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(debianbrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_debianbrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_debianbranding_DATA)'; test -n "$(debianbrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(debianbrandingdir)'; $(am__uninstall_files_from_dir)
+install-dist_defaultbrandingDATA: $(dist_defaultbranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_defaultbranding_DATA)'; test -n "$(defaultbrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(defaultbrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(defaultbrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(defaultbrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(defaultbrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_defaultbrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_defaultbranding_DATA)'; test -n "$(defaultbrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(defaultbrandingdir)'; $(am__uninstall_files_from_dir)
+install-dist_fedorabrandingDATA: $(dist_fedorabranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_fedorabranding_DATA)'; test -n "$(fedorabrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(fedorabrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(fedorabrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(fedorabrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(fedorabrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_fedorabrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_fedorabranding_DATA)'; test -n "$(fedorabrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(fedorabrandingdir)'; $(am__uninstall_files_from_dir)
+install-dist_kubernetesbrandingDATA: $(dist_kubernetesbranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_kubernetesbranding_DATA)'; test -n "$(kubernetesbrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(kubernetesbrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(kubernetesbrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(kubernetesbrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(kubernetesbrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_kubernetesbrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_kubernetesbranding_DATA)'; test -n "$(kubernetesbrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(kubernetesbrandingdir)'; $(am__uninstall_files_from_dir)
+install-dist_motdDATA: $(dist_motd_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_motd_DATA)'; test -n "$(motddir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(motddir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(motddir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(motddir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(motddir)" || exit $$?; \
+ done
+
+uninstall-dist_motdDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_motd_DATA)'; test -n "$(motddir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(motddir)'; $(am__uninstall_files_from_dir)
+install-dist_opensusebrandingDATA: $(dist_opensusebranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_opensusebranding_DATA)'; test -n "$(opensusebrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(opensusebrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(opensusebrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(opensusebrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(opensusebrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_opensusebrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_opensusebranding_DATA)'; test -n "$(opensusebrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(opensusebrandingdir)'; $(am__uninstall_files_from_dir)
+install-dist_pcpmanifestDATA: $(dist_pcpmanifest_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_pcpmanifest_DATA)'; test -n "$(pcpmanifestdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pcpmanifestdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pcpmanifestdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(pcpmanifestdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(pcpmanifestdir)" || exit $$?; \
+ done
+
+uninstall-dist_pcpmanifestDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_pcpmanifest_DATA)'; test -n "$(pcpmanifestdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pcpmanifestdir)'; $(am__uninstall_files_from_dir)
+install-dist_pixmapDATA: $(dist_pixmap_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_pixmap_DATA)'; test -n "$(pixmapdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pixmapdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pixmapdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(pixmapdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(pixmapdir)" || exit $$?; \
+ done
+
+uninstall-dist_pixmapDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_pixmap_DATA)'; test -n "$(pixmapdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pixmapdir)'; $(am__uninstall_files_from_dir)
+install-dist_pmlogconfDATA: $(dist_pmlogconf_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_pmlogconf_DATA)'; test -n "$(pmlogconfdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pmlogconfdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pmlogconfdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(pmlogconfdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(pmlogconfdir)" || exit $$?; \
+ done
+
+uninstall-dist_pmlogconfDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_pmlogconf_DATA)'; test -n "$(pmlogconfdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pmlogconfdir)'; $(am__uninstall_files_from_dir)
+install-dist_registrybrandingDATA: $(dist_registrybranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_registrybranding_DATA)'; test -n "$(registrybrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(registrybrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(registrybrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(registrybrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(registrybrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_registrybrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_registrybranding_DATA)'; test -n "$(registrybrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(registrybrandingdir)'; $(am__uninstall_files_from_dir)
+install-dist_rhelbrandingDATA: $(dist_rhelbranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_rhelbranding_DATA)'; test -n "$(rhelbrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(rhelbrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(rhelbrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(rhelbrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(rhelbrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_rhelbrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_rhelbranding_DATA)'; test -n "$(rhelbrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(rhelbrandingdir)'; $(am__uninstall_files_from_dir)
+install-dist_scalableiconDATA: $(dist_scalableicon_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_scalableicon_DATA)'; test -n "$(scalableicondir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(scalableicondir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(scalableicondir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(scalableicondir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(scalableicondir)" || exit $$?; \
+ done
+
+uninstall-dist_scalableiconDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_scalableicon_DATA)'; test -n "$(scalableicondir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(scalableicondir)'; $(am__uninstall_files_from_dir)
+install-dist_scientificbrandingDATA: $(dist_scientificbranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_scientificbranding_DATA)'; test -n "$(scientificbrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(scientificbrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(scientificbrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(scientificbrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(scientificbrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_scientificbrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_scientificbranding_DATA)'; test -n "$(scientificbrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(scientificbrandingdir)'; $(am__uninstall_files_from_dir)
+install-dist_sshmanifestDATA: $(dist_sshmanifest_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_sshmanifest_DATA)'; test -n "$(sshmanifestdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(sshmanifestdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(sshmanifestdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(sshmanifestdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(sshmanifestdir)" || exit $$?; \
+ done
+
+uninstall-dist_sshmanifestDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_sshmanifest_DATA)'; test -n "$(sshmanifestdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(sshmanifestdir)'; $(am__uninstall_files_from_dir)
+install-dist_symboliciconDATA: $(dist_symbolicicon_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_symbolicicon_DATA)'; test -n "$(symbolicicondir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(symbolicicondir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(symbolicicondir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(symbolicicondir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(symbolicicondir)" || exit $$?; \
+ done
+
+uninstall-dist_symboliciconDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_symbolicicon_DATA)'; test -n "$(symbolicicondir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(symbolicicondir)'; $(am__uninstall_files_from_dir)
+install-dist_systemdunitDATA: $(dist_systemdunit_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_systemdunit_DATA)'; test -n "$(systemdunitdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(systemdunitdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(systemdunitdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(systemdunitdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(systemdunitdir)" || exit $$?; \
+ done
+
+uninstall-dist_systemdunitDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_systemdunit_DATA)'; test -n "$(systemdunitdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(systemdunitdir)'; $(am__uninstall_files_from_dir)
+install-dist_ubuntubrandingDATA: $(dist_ubuntubranding_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(dist_ubuntubranding_DATA)'; test -n "$(ubuntubrandingdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(ubuntubrandingdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(ubuntubrandingdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(ubuntubrandingdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(ubuntubrandingdir)" || exit $$?; \
+ done
+
+uninstall-dist_ubuntubrandingDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(dist_ubuntubranding_DATA)'; test -n "$(ubuntubrandingdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(ubuntubrandingdir)'; $(am__uninstall_files_from_dir)
+install-nodist_appdataDATA: $(nodist_appdata_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(nodist_appdata_DATA)'; test -n "$(appdatadir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(appdatadir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(appdatadir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(appdatadir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(appdatadir)" || exit $$?; \
+ done
+
+uninstall-nodist_appdataDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(nodist_appdata_DATA)'; test -n "$(appdatadir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(appdatadir)'; $(am__uninstall_files_from_dir)
+install-nodist_metainfoDATA: $(nodist_metainfo_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(nodist_metainfo_DATA)'; test -n "$(metainfodir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(metainfodir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(metainfodir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(metainfodir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(metainfodir)" || exit $$?; \
+ done
+
+uninstall-nodist_metainfoDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(nodist_metainfo_DATA)'; test -n "$(metainfodir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(metainfodir)'; $(am__uninstall_files_from_dir)
+install-nodist_systemdunitDATA: $(nodist_systemdunit_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(nodist_systemdunit_DATA)'; test -n "$(systemdunitdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(systemdunitdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(systemdunitdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(systemdunitdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(systemdunitdir)" || exit $$?; \
+ done
+
+uninstall-nodist_systemdunitDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(nodist_systemdunit_DATA)'; test -n "$(systemdunitdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(systemdunitdir)'; $(am__uninstall_files_from_dir)
+install-nodist_tempconfDATA: $(nodist_tempconf_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(nodist_tempconf_DATA)'; test -n "$(tempconfdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(tempconfdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(tempconfdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(tempconfdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(tempconfdir)" || exit $$?; \
+ done
+
+uninstall-nodist_tempconfDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(nodist_tempconf_DATA)'; test -n "$(tempconfdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(tempconfdir)'; $(am__uninstall_files_from_dir)
+install-pixmapsDATA: $(pixmaps_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(pixmaps_DATA)'; test -n "$(pixmapsdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pixmapsdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pixmapsdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(pixmapsdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(pixmapsdir)" || exit $$?; \
+ done
+
+uninstall-pixmapsDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pixmaps_DATA)'; test -n "$(pixmapsdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pixmapsdir)'; $(am__uninstall_files_from_dir)
+install-polkitDATA: $(polkit_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(polkit_DATA)'; test -n "$(polkitdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(polkitdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(polkitdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(polkitdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(polkitdir)" || exit $$?; \
+ done
+
+uninstall-polkitDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(polkit_DATA)'; test -n "$(polkitdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(polkitdir)'; $(am__uninstall_files_from_dir)
+install-selinuxpackagesDATA: $(selinuxpackages_DATA)
+ @$(NORMAL_INSTALL)
+ @list='$(selinuxpackages_DATA)'; test -n "$(selinuxpackagesdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(selinuxpackagesdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(selinuxpackagesdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(selinuxpackagesdir)'"; \
+ $(INSTALL_DATA) $$files "$(DESTDIR)$(selinuxpackagesdir)" || exit $$?; \
+ done
+
+uninstall-selinuxpackagesDATA:
+ @$(NORMAL_UNINSTALL)
+ @list='$(selinuxpackages_DATA)'; test -n "$(selinuxpackagesdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(selinuxpackagesdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscope: cscope.files
+ test ! -s cscope.files \
+ || $(CSCOPE) -b -q $(AM_CSCOPEFLAGS) $(CSCOPEFLAGS) -i cscope.files $(CSCOPE_ARGS)
+clean-cscope:
+ -rm -f cscope.files
+cscope.files: clean-cscope cscopelist
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+ -rm -f cscope.out cscope.in.out cscope.po.out cscope.files
+
+# Recover from deleted '.trs' file; this should ensure that
+# "rm -f foo.log; make foo.trs" re-run 'foo.test', and re-create
+# both 'foo.log' and 'foo.trs'. Break the recipe in two subshells
+# to avoid problems with "make -n".
+.log.trs:
+ rm -f $< $@
+ $(MAKE) $(AM_MAKEFLAGS) $<
+
+# Leading 'am--fnord' is there to ensure the list of targets does not
+# expand to empty, as could happen e.g. with make check TESTS=''.
+am--fnord $(TEST_LOGS) $(TEST_LOGS:.log=.trs): $(am__force_recheck)
+am--force-recheck:
+ @:
+
+$(TEST_SUITE_LOG): $(TEST_LOGS)
+ @$(am__set_TESTS_bases); \
+ am__f_ok () { test -f "$$1" && test -r "$$1"; }; \
+ redo_bases=`for i in $$bases; do \
+ am__f_ok $$i.trs && am__f_ok $$i.log || echo $$i; \
+ done`; \
+ if test -n "$$redo_bases"; then \
+ redo_logs=`for i in $$redo_bases; do echo $$i.log; done`; \
+ redo_results=`for i in $$redo_bases; do echo $$i.trs; done`; \
+ if $(am__make_dryrun); then :; else \
+ rm -f $$redo_logs && rm -f $$redo_results || exit 1; \
+ fi; \
+ fi; \
+ if test -n "$$am__remaking_logs"; then \
+ echo "fatal: making $(TEST_SUITE_LOG): possible infinite" \
+ "recursion detected" >&2; \
+ elif test -n "$$redo_logs"; then \
+ am__remaking_logs=yes $(MAKE) $(AM_MAKEFLAGS) $$redo_logs; \
+ fi; \
+ if $(am__make_dryrun); then :; else \
+ st=0; \
+ errmsg="fatal: making $(TEST_SUITE_LOG): failed to create"; \
+ for i in $$redo_bases; do \
+ test -f $$i.trs && test -r $$i.trs \
+ || { echo "$$errmsg $$i.trs" >&2; st=1; }; \
+ test -f $$i.log && test -r $$i.log \
+ || { echo "$$errmsg $$i.log" >&2; st=1; }; \
+ done; \
+ test $$st -eq 0 || exit 1; \
+ fi
+ @$(am__sh_e_setup); $(am__tty_colors); $(am__set_TESTS_bases); \
+ ws='[ ]'; \
+ results=`for b in $$bases; do echo $$b.trs; done`; \
+ test -n "$$results" || results=/dev/null; \
+ all=` grep "^$$ws*:test-result:" $$results | wc -l`; \
+ pass=` grep "^$$ws*:test-result:$$ws*PASS" $$results | wc -l`; \
+ fail=` grep "^$$ws*:test-result:$$ws*FAIL" $$results | wc -l`; \
+ skip=` grep "^$$ws*:test-result:$$ws*SKIP" $$results | wc -l`; \
+ xfail=`grep "^$$ws*:test-result:$$ws*XFAIL" $$results | wc -l`; \
+ xpass=`grep "^$$ws*:test-result:$$ws*XPASS" $$results | wc -l`; \
+ error=`grep "^$$ws*:test-result:$$ws*ERROR" $$results | wc -l`; \
+ if test `expr $$fail + $$xpass + $$error` -eq 0; then \
+ success=true; \
+ else \
+ success=false; \
+ fi; \
+ br='==================='; br=$$br$$br$$br$$br; \
+ result_count () \
+ { \
+ if test x"$$1" = x"--maybe-color"; then \
+ maybe_colorize=yes; \
+ elif test x"$$1" = x"--no-color"; then \
+ maybe_colorize=no; \
+ else \
+ echo "$@: invalid 'result_count' usage" >&2; exit 4; \
+ fi; \
+ shift; \
+ desc=$$1 count=$$2; \
+ if test $$maybe_colorize = yes && test $$count -gt 0; then \
+ color_start=$$3 color_end=$$std; \
+ else \
+ color_start= color_end=; \
+ fi; \
+ echo "$${color_start}# $$desc $$count$${color_end}"; \
+ }; \
+ create_testsuite_report () \
+ { \
+ result_count $$1 "TOTAL:" $$all "$$brg"; \
+ result_count $$1 "PASS: " $$pass "$$grn"; \
+ result_count $$1 "SKIP: " $$skip "$$blu"; \
+ result_count $$1 "XFAIL:" $$xfail "$$lgn"; \
+ result_count $$1 "FAIL: " $$fail "$$red"; \
+ result_count $$1 "XPASS:" $$xpass "$$red"; \
+ result_count $$1 "ERROR:" $$error "$$mgn"; \
+ }; \
+ { \
+ echo "$(PACKAGE_STRING): $(subdir)/$(TEST_SUITE_LOG)" | \
+ $(am__rst_title); \
+ create_testsuite_report --no-color; \
+ echo; \
+ echo ".. contents:: :depth: 2"; \
+ echo; \
+ for b in $$bases; do echo $$b; done \
+ | $(am__create_global_log); \
+ } >$(TEST_SUITE_LOG).tmp || exit 1; \
+ mv $(TEST_SUITE_LOG).tmp $(TEST_SUITE_LOG); \
+ if $$success; then \
+ col="$$grn"; \
+ else \
+ col="$$red"; \
+ test x"$$VERBOSE" = x || cat $(TEST_SUITE_LOG); \
+ fi; \
+ echo "$${col}$$br$${std}"; \
+ echo "$${col}Testsuite summary"$(AM_TESTSUITE_SUMMARY_HEADER)"$${std}"; \
+ echo "$${col}$$br$${std}"; \
+ create_testsuite_report --maybe-color; \
+ echo "$$col$$br$$std"; \
+ if $$success; then :; else \
+ echo "$${col}See $(subdir)/$(TEST_SUITE_LOG)$${std}"; \
+ if test -n "$(PACKAGE_BUGREPORT)"; then \
+ echo "$${col}Please report to $(PACKAGE_BUGREPORT)$${std}"; \
+ fi; \
+ echo "$$col$$br$$std"; \
+ fi; \
+ $$success || exit 1
+
+check-TESTS: $(check_PROGRAMS) $(check_LIBRARIES) $(check_SCRIPTS) $(dist_check_SCRIPTS) $(check_DATA) $(dist_check_DATA)
+ @list='$(RECHECK_LOGS)'; test -z "$$list" || rm -f $$list
+ @list='$(RECHECK_LOGS:.log=.trs)'; test -z "$$list" || rm -f $$list
+ @test -z "$(TEST_SUITE_LOG)" || rm -f $(TEST_SUITE_LOG)
+ @set +e; $(am__set_TESTS_bases); \
+ log_list=`for i in $$bases; do echo $$i.log; done`; \
+ trs_list=`for i in $$bases; do echo $$i.trs; done`; \
+ log_list=`echo $$log_list`; trs_list=`echo $$trs_list`; \
+ $(MAKE) $(AM_MAKEFLAGS) $(TEST_SUITE_LOG) TEST_LOGS="$$log_list"; \
+ exit $$?;
+recheck: all $(check_PROGRAMS) $(check_LIBRARIES) $(check_SCRIPTS) $(dist_check_SCRIPTS) $(check_DATA) $(dist_check_DATA)
+ @test -z "$(TEST_SUITE_LOG)" || rm -f $(TEST_SUITE_LOG)
+ @set +e; $(am__set_TESTS_bases); \
+ bases=`for i in $$bases; do echo $$i; done \
+ | $(am__list_recheck_tests)` || exit 1; \
+ log_list=`for i in $$bases; do echo $$i.log; done`; \
+ log_list=`echo $$log_list`; \
+ $(MAKE) $(AM_MAKEFLAGS) $(TEST_SUITE_LOG) \
+ am__force_recheck=am--force-recheck \
+ TEST_LOGS="$$log_list"; \
+ exit $$?
+test/static-code.log: test/static-code
+ @p='test/static-code'; \
+ b='test/static-code'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-bridge.log: test-bridge$(EXEEXT)
+ @p='test-bridge$(EXEEXT)'; \
+ b='test-bridge'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-connect.log: test-connect$(EXEEXT)
+ @p='test-connect$(EXEEXT)'; \
+ b='test-connect'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-dbus-meta.log: test-dbus-meta$(EXEEXT)
+ @p='test-dbus-meta$(EXEEXT)'; \
+ b='test-dbus-meta'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-fs.log: test-fs$(EXEEXT)
+ @p='test-fs$(EXEEXT)'; \
+ b='test-fs'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-httpstream.log: test-httpstream$(EXEEXT)
+ @p='test-httpstream$(EXEEXT)'; \
+ b='test-httpstream'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-metrics.log: test-metrics$(EXEEXT)
+ @p='test-metrics$(EXEEXT)'; \
+ b='test-metrics'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-packages.log: test-packages$(EXEEXT)
+ @p='test-packages$(EXEEXT)'; \
+ b='test-packages'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-packet-channel.log: test-packet-channel$(EXEEXT)
+ @p='test-packet-channel$(EXEEXT)'; \
+ b='test-packet-channel'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-paths.log: test-paths$(EXEEXT)
+ @p='test-paths$(EXEEXT)'; \
+ b='test-paths'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-peer.log: test-peer$(EXEEXT)
+ @p='test-peer$(EXEEXT)'; \
+ b='test-peer'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-pipe-channel.log: test-pipe-channel$(EXEEXT)
+ @p='test-pipe-channel$(EXEEXT)'; \
+ b='test-pipe-channel'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-process.log: test-process$(EXEEXT)
+ @p='test-process$(EXEEXT)'; \
+ b='test-process'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-router.log: test-router$(EXEEXT)
+ @p='test-router$(EXEEXT)'; \
+ b='test-router'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-rules.log: test-rules$(EXEEXT)
+ @p='test-rules$(EXEEXT)'; \
+ b='test-rules'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-stream.log: test-stream$(EXEEXT)
+ @p='test-stream$(EXEEXT)'; \
+ b='test-stream'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-websocketstream.log: test-websocketstream$(EXEEXT)
+ @p='test-websocketstream$(EXEEXT)'; \
+ b='test-websocketstream'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-pcp.log: test-pcp$(EXEEXT)
+ @p='test-pcp$(EXEEXT)'; \
+ b='test-pcp'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-pcp-archives.log: test-pcp-archives$(EXEEXT)
+ @p='test-pcp-archives$(EXEEXT)'; \
+ b='test-pcp-archives'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-authorize.log: test-authorize$(EXEEXT)
+ @p='test-authorize$(EXEEXT)'; \
+ b='test-authorize'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-base64.log: test-base64$(EXEEXT)
+ @p='test-base64$(EXEEXT)'; \
+ b='test-base64'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-channel.log: test-channel$(EXEEXT)
+ @p='test-channel$(EXEEXT)'; \
+ b='test-channel'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-config.log: test-config$(EXEEXT)
+ @p='test-config$(EXEEXT)'; \
+ b='test-config'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-frame.log: test-frame$(EXEEXT)
+ @p='test-frame$(EXEEXT)'; \
+ b='test-frame'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-hash.log: test-hash$(EXEEXT)
+ @p='test-hash$(EXEEXT)'; \
+ b='test-hash'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-hex.log: test-hex$(EXEEXT)
+ @p='test-hex$(EXEEXT)'; \
+ b='test-hex'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-json.log: test-json$(EXEEXT)
+ @p='test-json$(EXEEXT)'; \
+ b='test-json'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-jsonfds.log: test-jsonfds$(EXEEXT)
+ @p='test-jsonfds$(EXEEXT)'; \
+ b='test-jsonfds'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-locale.log: test-locale$(EXEEXT)
+ @p='test-locale$(EXEEXT)'; \
+ b='test-locale'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-pipe.log: test-pipe$(EXEEXT)
+ @p='test-pipe$(EXEEXT)'; \
+ b='test-pipe'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-system.log: test-system$(EXEEXT)
+ @p='test-system$(EXEEXT)'; \
+ b='test-system'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-template.log: test-template$(EXEEXT)
+ @p='test-template$(EXEEXT)'; \
+ b='test-template'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-transport.log: test-transport$(EXEEXT)
+ @p='test-transport$(EXEEXT)'; \
+ b='test-transport'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-unicode.log: test-unicode$(EXEEXT)
+ @p='test-unicode$(EXEEXT)'; \
+ b='test-unicode'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-unixsignal.log: test-unixsignal$(EXEEXT)
+ @p='test-unixsignal$(EXEEXT)'; \
+ b='test-unixsignal'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-version.log: test-version$(EXEEXT)
+ @p='test-version$(EXEEXT)'; \
+ b='test-version'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-webcertificate.log: test-webcertificate$(EXEEXT)
+ @p='test-webcertificate$(EXEEXT)'; \
+ b='test-webcertificate'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-webresponse.log: test-webresponse$(EXEEXT)
+ @p='test-webresponse$(EXEEXT)'; \
+ b='test-webresponse'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-webserver.log: test-webserver$(EXEEXT)
+ @p='test-webserver$(EXEEXT)'; \
+ b='test-webserver'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-ssh-add.log: test-ssh-add$(EXEEXT)
+ @p='test-ssh-add$(EXEEXT)'; \
+ b='test-ssh-add'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-sshbridge.log: test-sshbridge$(EXEEXT)
+ @p='test-sshbridge$(EXEEXT)'; \
+ b='test-sshbridge'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-sshoptions.log: test-sshoptions$(EXEEXT)
+ @p='test-sshoptions$(EXEEXT)'; \
+ b='test-sshoptions'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-cockpit-certificate-ensure.log: test-cockpit-certificate-ensure$(EXEEXT)
+ @p='test-cockpit-certificate-ensure$(EXEEXT)'; \
+ b='test-cockpit-certificate-ensure'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-tls-connection.log: test-tls-connection$(EXEEXT)
+ @p='test-tls-connection$(EXEEXT)'; \
+ b='test-tls-connection'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-tls-server.log: test-tls-server$(EXEEXT)
+ @p='test-tls-server$(EXEEXT)'; \
+ b='test-tls-server'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-websocket.log: test-websocket$(EXEEXT)
+ @p='test-websocket$(EXEEXT)'; \
+ b='test-websocket'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-auth.log: test-auth$(EXEEXT)
+ @p='test-auth$(EXEEXT)'; \
+ b='test-auth'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-compat.log: test-compat$(EXEEXT)
+ @p='test-compat$(EXEEXT)'; \
+ b='test-compat'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-creds.log: test-creds$(EXEEXT)
+ @p='test-creds$(EXEEXT)'; \
+ b='test-creds'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-kerberos.log: test-kerberos$(EXEEXT)
+ @p='test-kerberos$(EXEEXT)'; \
+ b='test-kerberos'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-channelresponse.log: test-channelresponse$(EXEEXT)
+ @p='test-channelresponse$(EXEEXT)'; \
+ b='test-channelresponse'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-handlers.log: test-handlers$(EXEEXT)
+ @p='test-handlers$(EXEEXT)'; \
+ b='test-handlers'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-webservice.log: test-webservice$(EXEEXT)
+ @p='test-webservice$(EXEEXT)'; \
+ b='test-webservice'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+test-authssh.log: test-authssh$(EXEEXT)
+ @p='test-authssh$(EXEEXT)'; \
+ b='test-authssh'; \
+ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+.html.log:
+ @p='$<'; \
+ $(am__set_b); \
+ $(am__check_pre) $(HTML_LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_HTML_LOG_DRIVER_FLAGS) $(HTML_LOG_DRIVER_FLAGS) -- $(HTML_LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+@am__EXEEXT_TRUE@.html$(EXEEXT).log:
+@am__EXEEXT_TRUE@ @p='$<'; \
+@am__EXEEXT_TRUE@ $(am__set_b); \
+@am__EXEEXT_TRUE@ $(am__check_pre) $(HTML_LOG_DRIVER) --test-name "$$f" \
+@am__EXEEXT_TRUE@ --log-file $$b.log --trs-file $$b.trs \
+@am__EXEEXT_TRUE@ $(am__common_driver_flags) $(AM_HTML_LOG_DRIVER_FLAGS) $(HTML_LOG_DRIVER_FLAGS) -- $(HTML_LOG_COMPILE) \
+@am__EXEEXT_TRUE@ "$$tst" $(AM_TESTS_FD_REDIRECT)
+.sh.log:
+ @p='$<'; \
+ $(am__set_b); \
+ $(am__check_pre) $(SH_LOG_DRIVER) --test-name "$$f" \
+ --log-file $$b.log --trs-file $$b.trs \
+ $(am__common_driver_flags) $(AM_SH_LOG_DRIVER_FLAGS) $(SH_LOG_DRIVER_FLAGS) -- $(SH_LOG_COMPILE) \
+ "$$tst" $(AM_TESTS_FD_REDIRECT)
+@am__EXEEXT_TRUE@.sh$(EXEEXT).log:
+@am__EXEEXT_TRUE@ @p='$<'; \
+@am__EXEEXT_TRUE@ $(am__set_b); \
+@am__EXEEXT_TRUE@ $(am__check_pre) $(SH_LOG_DRIVER) --test-name "$$f" \
+@am__EXEEXT_TRUE@ --log-file $$b.log --trs-file $$b.trs \
+@am__EXEEXT_TRUE@ $(am__common_driver_flags) $(AM_SH_LOG_DRIVER_FLAGS) $(SH_LOG_DRIVER_FLAGS) -- $(SH_LOG_COMPILE) \
+@am__EXEEXT_TRUE@ "$$tst" $(AM_TESTS_FD_REDIRECT)
+
+distdir-am: $(DISTFILES)
+ $(am__remove_distdir)
+ test -d "$(distdir)" || mkdir "$(distdir)"
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+ $(MAKE) $(AM_MAKEFLAGS) \
+ top_distdir="$(top_distdir)" distdir="$(distdir)" \
+ dist-hook
+ -test -n "$(am__skip_mode_fix)" \
+ || find "$(distdir)" -type d ! -perm -755 \
+ -exec chmod u+rwx,go+rx {} \; -o \
+ ! -type d ! -perm -444 -links 1 -exec chmod a+r {} \; -o \
+ ! -type d ! -perm -400 -exec chmod a+r {} \; -o \
+ ! -type d ! -perm -444 -exec $(install_sh) -c -m a+r {} {} \; \
+ || chmod -R a+r "$(distdir)"
+dist-gzip: distdir
+ tardir=$(distdir) && $(am__tar) | eval GZIP= gzip $(GZIP_ENV) -c >$(distdir).tar.gz
+ $(am__post_remove_distdir)
+
+dist-bzip2: distdir
+ tardir=$(distdir) && $(am__tar) | BZIP2=$${BZIP2--9} bzip2 -c >$(distdir).tar.bz2
+ $(am__post_remove_distdir)
+
+dist-lzip: distdir
+ tardir=$(distdir) && $(am__tar) | lzip -c $${LZIP_OPT--9} >$(distdir).tar.lz
+ $(am__post_remove_distdir)
+dist-xz: distdir
+ tardir=$(distdir) && $(am__tar) | XZ_OPT=$${XZ_OPT--e} xz -c >$(distdir).tar.xz
+ $(am__post_remove_distdir)
+
+dist-zstd: distdir
+ tardir=$(distdir) && $(am__tar) | zstd -c $${ZSTD_CLEVEL-$${ZSTD_OPT--19}} >$(distdir).tar.zst
+ $(am__post_remove_distdir)
+
+dist-tarZ: distdir
+ @echo WARNING: "Support for distribution archives compressed with" \
+ "legacy program 'compress' is deprecated." >&2
+ @echo WARNING: "It will be removed altogether in Automake 2.0" >&2
+ tardir=$(distdir) && $(am__tar) | compress -c >$(distdir).tar.Z
+ $(am__post_remove_distdir)
+
+dist-shar: distdir
+ @echo WARNING: "Support for shar distribution archives is" \
+ "deprecated." >&2
+ @echo WARNING: "It will be removed altogether in Automake 2.0" >&2
+ shar $(distdir) | eval GZIP= gzip $(GZIP_ENV) -c >$(distdir).shar.gz
+ $(am__post_remove_distdir)
+
+dist-zip: distdir
+ -rm -f $(distdir).zip
+ zip -rq $(distdir).zip $(distdir)
+ $(am__post_remove_distdir)
+
+dist dist-all:
+ $(MAKE) $(AM_MAKEFLAGS) $(DIST_TARGETS) am__post_remove_distdir='@:'
+ $(am__post_remove_distdir)
+
+# This target untars the dist file and tries a VPATH configuration. Then
+# it guarantees that the distribution is self-contained by making another
+# tarfile.
+distcheck: dist
+ case '$(DIST_ARCHIVES)' in \
+ *.tar.gz*) \
+ eval GZIP= gzip $(GZIP_ENV) -dc $(distdir).tar.gz | $(am__untar) ;;\
+ *.tar.bz2*) \
+ bzip2 -dc $(distdir).tar.bz2 | $(am__untar) ;;\
+ *.tar.lz*) \
+ lzip -dc $(distdir).tar.lz | $(am__untar) ;;\
+ *.tar.xz*) \
+ xz -dc $(distdir).tar.xz | $(am__untar) ;;\
+ *.tar.Z*) \
+ uncompress -c $(distdir).tar.Z | $(am__untar) ;;\
+ *.shar.gz*) \
+ eval GZIP= gzip $(GZIP_ENV) -dc $(distdir).shar.gz | unshar ;;\
+ *.zip*) \
+ unzip $(distdir).zip ;;\
+ *.tar.zst*) \
+ zstd -dc $(distdir).tar.zst | $(am__untar) ;;\
+ esac
+ chmod -R a-w $(distdir)
+ chmod u+w $(distdir)
+ mkdir $(distdir)/_build $(distdir)/_build/sub $(distdir)/_inst
+ chmod a-w $(distdir)
+ test -d $(distdir)/_build || exit 0; \
+ dc_install_base=`$(am__cd) $(distdir)/_inst && pwd | sed -e 's,^[^:\\/]:[\\/],/,'` \
+ && dc_destdir="$${TMPDIR-/tmp}/am-dc-$$$$/" \
+ && $(MAKE) $(AM_MAKEFLAGS) distcheck-hook \
+ && am__cwd=`pwd` \
+ && $(am__cd) $(distdir)/_build/sub \
+ && ../../configure \
+ $(AM_DISTCHECK_CONFIGURE_FLAGS) \
+ $(DISTCHECK_CONFIGURE_FLAGS) \
+ --srcdir=../.. --prefix="$$dc_install_base" \
+ && $(MAKE) $(AM_MAKEFLAGS) \
+ && $(MAKE) $(AM_MAKEFLAGS) $(AM_DISTCHECK_DVI_TARGET) \
+ && $(MAKE) $(AM_MAKEFLAGS) check \
+ && $(MAKE) $(AM_MAKEFLAGS) install \
+ && $(MAKE) $(AM_MAKEFLAGS) installcheck \
+ && $(MAKE) $(AM_MAKEFLAGS) uninstall \
+ && $(MAKE) $(AM_MAKEFLAGS) distuninstallcheck_dir="$$dc_install_base" \
+ distuninstallcheck \
+ && chmod -R a-w "$$dc_install_base" \
+ && ({ \
+ (cd ../.. && umask 077 && mkdir "$$dc_destdir") \
+ && $(MAKE) $(AM_MAKEFLAGS) DESTDIR="$$dc_destdir" install \
+ && $(MAKE) $(AM_MAKEFLAGS) DESTDIR="$$dc_destdir" uninstall \
+ && $(MAKE) $(AM_MAKEFLAGS) DESTDIR="$$dc_destdir" \
+ distuninstallcheck_dir="$$dc_destdir" distuninstallcheck; \
+ } || { rm -rf "$$dc_destdir"; exit 1; }) \
+ && rm -rf "$$dc_destdir" \
+ && $(MAKE) $(AM_MAKEFLAGS) dist \
+ && rm -rf $(DIST_ARCHIVES) \
+ && $(MAKE) $(AM_MAKEFLAGS) distcleancheck \
+ && cd "$$am__cwd" \
+ || exit 1
+ $(am__post_remove_distdir)
+ @(echo "$(distdir) archives ready for distribution: "; \
+ list='$(DIST_ARCHIVES)'; for i in $$list; do echo $$i; done) | \
+ sed -e 1h -e 1s/./=/g -e 1p -e 1x -e '$$p' -e '$$x'
+distuninstallcheck:
+ @test -n '$(distuninstallcheck_dir)' || { \
+ echo 'ERROR: trying to run $@ with an empty' \
+ '$$(distuninstallcheck_dir)' >&2; \
+ exit 1; \
+ }; \
+ $(am__cd) '$(distuninstallcheck_dir)' || { \
+ echo 'ERROR: cannot chdir into $(distuninstallcheck_dir)' >&2; \
+ exit 1; \
+ }; \
+ test `$(am__distuninstallcheck_listfiles) | wc -l` -eq 0 \
+ || { echo "ERROR: files left after uninstall:" ; \
+ if test -n "$(DESTDIR)"; then \
+ echo " (check DESTDIR support)"; \
+ fi ; \
+ $(distuninstallcheck_listfiles) ; \
+ exit 1; } >&2
+distcleancheck: distclean
+ @if test '$(srcdir)' = . ; then \
+ echo "ERROR: distcleancheck can only run from a VPATH build" ; \
+ exit 1 ; \
+ fi
+ @test `$(distcleancheck_listfiles) | wc -l` -eq 0 \
+ || { echo "ERROR: files left in build directory after distclean:" ; \
+ $(distcleancheck_listfiles) ; \
+ exit 1; } >&2
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) $(check_PROGRAMS) $(check_LIBRARIES) \
+ $(check_SCRIPTS) $(dist_check_SCRIPTS) $(check_DATA) \
+ $(dist_check_DATA)
+ $(MAKE) $(AM_MAKEFLAGS) check-TESTS check-local
+check: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) check-am
+all-am: Makefile $(PROGRAMS) $(LIBRARIES) $(SCRIPTS) $(MANS) $(DATA) \
+ config.h
+installdirs:
+ for dir in "$(DESTDIR)$(bindir)" "$(DESTDIR)$(cockpitwsdir)" "$(DESTDIR)$(libexecdir)" "$(DESTDIR)$(pamdir)" "$(DESTDIR)$(sbindir)" "$(DESTDIR)$(cockpitclientdir)" "$(DESTDIR)$(motddir)" "$(DESTDIR)$(libexecdir)" "$(DESTDIR)$(man1dir)" "$(DESTDIR)$(man5dir)" "$(DESTDIR)$(man8dir)" "$(DESTDIR)$(applicationsdir)" "$(DESTDIR)$(archbrandingdir)" "$(DESTDIR)$(centosbrandingdir)" "$(DESTDIR)$(client_metainfodir)" "$(DESTDIR)$(cockpitclientdir)" "$(DESTDIR)$(dbusservicesdir)" "$(DESTDIR)$(debianbrandingdir)" "$(DESTDIR)$(defaultbrandingdir)" "$(DESTDIR)$(fedorabrandingdir)" "$(DESTDIR)$(kubernetesbrandingdir)" "$(DESTDIR)$(motddir)" "$(DESTDIR)$(opensusebrandingdir)" "$(DESTDIR)$(pcpmanifestdir)" "$(DESTDIR)$(pixmapdir)" "$(DESTDIR)$(pmlogconfdir)" "$(DESTDIR)$(registrybrandingdir)" "$(DESTDIR)$(rhelbrandingdir)" "$(DESTDIR)$(scalableicondir)" "$(DESTDIR)$(scientificbrandingdir)" "$(DESTDIR)$(sshmanifestdir)" "$(DESTDIR)$(symbolicicondir)" "$(DESTDIR)$(systemdunitdir)" "$(DESTDIR)$(ubuntubrandingdir)" "$(DESTDIR)$(appdatadir)" "$(DESTDIR)$(metainfodir)" "$(DESTDIR)$(systemdunitdir)" "$(DESTDIR)$(tempconfdir)" "$(DESTDIR)$(pixmapsdir)" "$(DESTDIR)$(polkitdir)" "$(DESTDIR)$(selinuxpackagesdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) install-am
+install-exec: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+ -test -z "$(TEST_LOGS)" || rm -f $(TEST_LOGS)
+ -test -z "$(TEST_LOGS:.log=.trs)" || rm -f $(TEST_LOGS:.log=.trs)
+ -test -z "$(TEST_SUITE_LOG)" || rm -f $(TEST_SUITE_LOG)
+
+clean-generic:
+ -test -z "$(CLEANFILES)" || rm -f $(CLEANFILES)
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+ -rm -f src/bridge/$(DEPDIR)/$(am__dirstamp)
+ -rm -f src/bridge/$(am__dirstamp)
+ -rm -f src/common/$(DEPDIR)/$(am__dirstamp)
+ -rm -f src/common/$(am__dirstamp)
+ -rm -f src/pam-ssh-add/$(DEPDIR)/$(am__dirstamp)
+ -rm -f src/pam-ssh-add/$(am__dirstamp)
+ -rm -f src/session/$(DEPDIR)/$(am__dirstamp)
+ -rm -f src/session/$(am__dirstamp)
+ -rm -f src/ssh/$(DEPDIR)/$(am__dirstamp)
+ -rm -f src/ssh/$(am__dirstamp)
+ -rm -f src/testlib/$(DEPDIR)/$(am__dirstamp)
+ -rm -f src/testlib/$(am__dirstamp)
+ -rm -f src/tls/$(DEPDIR)/$(am__dirstamp)
+ -rm -f src/tls/$(am__dirstamp)
+ -rm -f src/websocket/$(DEPDIR)/$(am__dirstamp)
+ -rm -f src/websocket/$(am__dirstamp)
+ -rm -f src/ws/$(DEPDIR)/$(am__dirstamp)
+ -rm -f src/ws/$(am__dirstamp)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+ -test -z "$(BUILT_SOURCES)" || rm -f $(BUILT_SOURCES)
+clean: clean-am
+
+clean-am: clean-binPROGRAMS clean-checkLIBRARIES clean-checkPROGRAMS \
+ clean-cockpitwsPROGRAMS clean-generic clean-libexecPROGRAMS \
+ clean-local clean-noinstLIBRARIES clean-noinstPROGRAMS \
+ clean-pamPROGRAMS clean-sbinPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f $(am__CONFIG_DISTCLEAN_FILES)
+ -rm -f src/bridge/$(DEPDIR)/cockpit_askpass-askpass.Po
+ -rm -f src/bridge/$(DEPDIR)/cockpit_bridge-bridge.Po
+ -rm -f src/bridge/$(DEPDIR)/cockpit_pcp-cockpitpcp.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitconnect.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbuscache.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusconfig.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusinternal.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusjson.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusloginmessages.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmachines.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmeta.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusprocess.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusrules.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbususer.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitechochannel.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfslist.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsread.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsreplace.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfswatch.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpithttpstream.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitinteracttransport.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitnullchannel.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpackages.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpacketchannel.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpaths.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpeer.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpipechannel.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpolkitagent.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitrouter.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitstream.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitwebsocketstream.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitblocksamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcgroupsamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcpusamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitdisksamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitinternalmetrics.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmemorysamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmetrics.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmountsamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitnetworksamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitsamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitconnect.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitdbusinternal.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpcpmetrics.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpeer.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitrouter.Po
+ -rm -f src/bridge/$(DEPDIR)/mock_bridge-mock-bridge.Po
+ -rm -f src/bridge/$(DEPDIR)/mock_pmda_so-mock-pmda.Po
+ -rm -f src/bridge/$(DEPDIR)/test_bridge-test-bridge.Po
+ -rm -f src/bridge/$(DEPDIR)/test_connect-test-connect.Po
+ -rm -f src/bridge/$(DEPDIR)/test_dbus_meta-test-dbus-meta.Po
+ -rm -f src/bridge/$(DEPDIR)/test_fs-test-fs.Po
+ -rm -f src/bridge/$(DEPDIR)/test_httpstream-test-httpstream.Po
+ -rm -f src/bridge/$(DEPDIR)/test_metrics-test-metrics.Po
+ -rm -f src/bridge/$(DEPDIR)/test_packages-test-packages.Po
+ -rm -f src/bridge/$(DEPDIR)/test_packet_channel-test-packet-channel.Po
+ -rm -f src/bridge/$(DEPDIR)/test_paths-test-paths.Po
+ -rm -f src/bridge/$(DEPDIR)/test_pcp-test-pcp.Po
+ -rm -f src/bridge/$(DEPDIR)/test_pcp_archives-test-pcp-archives.Po
+ -rm -f src/bridge/$(DEPDIR)/test_peer-test-peer.Po
+ -rm -f src/bridge/$(DEPDIR)/test_pipe_channel-test-pipe-channel.Po
+ -rm -f src/bridge/$(DEPDIR)/test_process-test-process.Po
+ -rm -f src/bridge/$(DEPDIR)/test_router-test-router.Po
+ -rm -f src/bridge/$(DEPDIR)/test_rules-test-rules.Po
+ -rm -f src/bridge/$(DEPDIR)/test_stream-test-stream.Po
+ -rm -f src/bridge/$(DEPDIR)/test_websocketstream-test-websocketstream.Po
+ -rm -f src/common/$(DEPDIR)/cockpitauthorize.Po
+ -rm -f src/common/$(DEPDIR)/cockpitbase64.Po
+ -rm -f src/common/$(DEPDIR)/cockpitclosefrom.Po
+ -rm -f src/common/$(DEPDIR)/cockpitconf.Po
+ -rm -f src/common/$(DEPDIR)/cockpitfdpassing.Po
+ -rm -f src/common/$(DEPDIR)/cockpitframe.Po
+ -rm -f src/common/$(DEPDIR)/cockpithex.Po
+ -rm -f src/common/$(DEPDIR)/cockpitjsonprint.Po
+ -rm -f src/common/$(DEPDIR)/cockpitmemory.Po
+ -rm -f src/common/$(DEPDIR)/cockpitwebcertificate.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitchannel.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitclosefrom.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitcontrolmessages.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpiterror.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitflow.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpithash.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitjson.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitlocale.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitloopback.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitmachinesjson.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitmemfdread.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipe.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipetransport.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitsocket.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitsystem.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpittemplate.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpittransport.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitunicode.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitunixsignal.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitversion.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebfilter.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebinject.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebresponse.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebserver.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-fail.html.Po
+ -rm -f src/common/$(DEPDIR)/libpreload_temp_home_so-preload-temp-home.Po
+ -rm -f src/common/$(DEPDIR)/test_authorize-test-authorize.Po
+ -rm -f src/common/$(DEPDIR)/test_base64-test-base64.Po
+ -rm -f src/common/$(DEPDIR)/test_channel-test-channel.Po
+ -rm -f src/common/$(DEPDIR)/test_config-test-config.Po
+ -rm -f src/common/$(DEPDIR)/test_frame-test-frame.Po
+ -rm -f src/common/$(DEPDIR)/test_hash-test-hash.Po
+ -rm -f src/common/$(DEPDIR)/test_hex-test-hex.Po
+ -rm -f src/common/$(DEPDIR)/test_json-test-json.Po
+ -rm -f src/common/$(DEPDIR)/test_jsonfds-test-jsonfds.Po
+ -rm -f src/common/$(DEPDIR)/test_locale-test-locale.Po
+ -rm -f src/common/$(DEPDIR)/test_pipe-test-pipe.Po
+ -rm -f src/common/$(DEPDIR)/test_system-test-system.Po
+ -rm -f src/common/$(DEPDIR)/test_template-test-template.Po
+ -rm -f src/common/$(DEPDIR)/test_transport-test-transport.Po
+ -rm -f src/common/$(DEPDIR)/test_unicode-test-unicode.Po
+ -rm -f src/common/$(DEPDIR)/test_unixsignal-test-unixsignal.Po
+ -rm -f src/common/$(DEPDIR)/test_version-test-version.Po
+ -rm -f src/common/$(DEPDIR)/test_webcertificate-test-webcertificate.Po
+ -rm -f src/common/$(DEPDIR)/test_webresponse-test-webresponse.Po
+ -rm -f src/common/$(DEPDIR)/test_webserver-test-webserver.Po
+ -rm -f src/pam-ssh-add/$(DEPDIR)/libpam_ssh_add_a-pam-ssh-add.Po
+ -rm -f src/pam-ssh-add/$(DEPDIR)/pam_ssh_add_so-pam-ssh-add.Po
+ -rm -f src/pam-ssh-add/$(DEPDIR)/test_ssh_add-test-ssh-add.Po
+ -rm -f src/session/$(DEPDIR)/client-certificate.Po
+ -rm -f src/session/$(DEPDIR)/session-utils.Po
+ -rm -f src/session/$(DEPDIR)/session.Po
+ -rm -f src/ssh/$(DEPDIR)/cockpit_ssh-ssh.Po
+ -rm -f src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshoptions.Po
+ -rm -f src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshrelay.Po
+ -rm -f src/ssh/$(DEPDIR)/mock_sshd-mock-sshd.Po
+ -rm -f src/ssh/$(DEPDIR)/test_sshbridge-test-sshbridge.Po
+ -rm -f src/ssh/$(DEPDIR)/test_sshoptions-test-sshoptions.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-cockpittest.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-mock-auth.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-mock-channel.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-mock-pressure.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-mock-transport.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-retest.Po
+ -rm -f src/tls/$(DEPDIR)/certificate.Po
+ -rm -f src/tls/$(DEPDIR)/client-certificate.Po
+ -rm -f src/tls/$(DEPDIR)/cockpit-certificate-ensure.Po
+ -rm -f src/tls/$(DEPDIR)/cockpit_wsinstance_factory-wsinstance-factory.Po
+ -rm -f src/tls/$(DEPDIR)/connection.Po
+ -rm -f src/tls/$(DEPDIR)/httpredirect.Po
+ -rm -f src/tls/$(DEPDIR)/main.Po
+ -rm -f src/tls/$(DEPDIR)/server.Po
+ -rm -f src/tls/$(DEPDIR)/socket-activation-helper.Po
+ -rm -f src/tls/$(DEPDIR)/socket-io.Po
+ -rm -f src/tls/$(DEPDIR)/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.Po
+ -rm -f src/tls/$(DEPDIR)/test_tls_connection-test-connection.Po
+ -rm -f src/tls/$(DEPDIR)/test_tls_server-test-server.Po
+ -rm -f src/tls/$(DEPDIR)/wsinstance-start.Po
+ -rm -f src/websocket/$(DEPDIR)/frob_websocket-frob-websocket.Po
+ -rm -f src/websocket/$(DEPDIR)/libwebsocket_a-websocket.Po
+ -rm -f src/websocket/$(DEPDIR)/libwebsocket_a-websocketclient.Po
+ -rm -f src/websocket/$(DEPDIR)/libwebsocket_a-websocketconnection.Po
+ -rm -f src/websocket/$(DEPDIR)/libwebsocket_a-websocketserver.Po
+ -rm -f src/websocket/$(DEPDIR)/test_websocket-test-websocket.Po
+ -rm -f src/ws/$(DEPDIR)/cockpit_ws-main.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitauth.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitbranding.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelresponse.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelsocket.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcompat.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcreds.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpithandlers.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitwebservice.Po
+ -rm -f src/ws/$(DEPDIR)/mock_auth_command-mock-auth-command.Po
+ -rm -f src/ws/$(DEPDIR)/mock_echo-mock-echo.Po
+ -rm -f src/ws/$(DEPDIR)/mock_pam_conv_mod_so-mock-pam-conv-mod.Po
+ -rm -f src/ws/$(DEPDIR)/pam_cockpit_cert_so-pam_cockpit_cert.Po
+ -rm -f src/ws/$(DEPDIR)/test_auth-test-auth.Po
+ -rm -f src/ws/$(DEPDIR)/test_authssh-test-authssh.Po
+ -rm -f src/ws/$(DEPDIR)/test_channelresponse-test-channelresponse.Po
+ -rm -f src/ws/$(DEPDIR)/test_compat-test-compat.Po
+ -rm -f src/ws/$(DEPDIR)/test_creds-test-creds.Po
+ -rm -f src/ws/$(DEPDIR)/test_handlers-test-handlers.Po
+ -rm -f src/ws/$(DEPDIR)/test_kerberos-test-kerberos.Po
+ -rm -f src/ws/$(DEPDIR)/test_server-mock-dbus-tests.Po
+ -rm -f src/ws/$(DEPDIR)/test_server-mock-service.Po
+ -rm -f src/ws/$(DEPDIR)/test_server-test-server.Po
+ -rm -f src/ws/$(DEPDIR)/test_webservice-test-webservice.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-hdr distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-cockpitwsPROGRAMS install-data-local \
+ install-dist_applicationsDATA install-dist_archbrandingDATA \
+ install-dist_centosbrandingDATA \
+ install-dist_client_metainfoDATA \
+ install-dist_cockpitclientDATA \
+ install-dist_cockpitclientSCRIPTS \
+ install-dist_dbusservicesDATA install-dist_debianbrandingDATA \
+ install-dist_defaultbrandingDATA \
+ install-dist_fedorabrandingDATA \
+ install-dist_kubernetesbrandingDATA install-dist_motdDATA \
+ install-dist_motdSCRIPTS install-dist_opensusebrandingDATA \
+ install-dist_pcpmanifestDATA install-dist_pixmapDATA \
+ install-dist_pmlogconfDATA install-dist_registrybrandingDATA \
+ install-dist_rhelbrandingDATA install-dist_scalableiconDATA \
+ install-dist_scientificbrandingDATA \
+ install-dist_sshmanifestDATA install-dist_symboliciconDATA \
+ install-dist_systemdunitDATA install-dist_ubuntubrandingDATA \
+ install-man install-nodist_appdataDATA \
+ install-nodist_metainfoDATA install-nodist_systemdunitDATA \
+ install-nodist_tempconfDATA install-pamPROGRAMS \
+ install-pixmapsDATA install-polkitDATA \
+ install-selinuxpackagesDATA
+ @$(NORMAL_INSTALL)
+ $(MAKE) $(AM_MAKEFLAGS) install-data-hook
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am: install-binPROGRAMS install-libexecPROGRAMS \
+ install-libexecSCRIPTS install-sbinPROGRAMS
+ @$(NORMAL_INSTALL)
+ $(MAKE) $(AM_MAKEFLAGS) install-exec-hook
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man: install-man1 install-man5 install-man8
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f $(am__CONFIG_DISTCLEAN_FILES)
+ -rm -rf $(top_srcdir)/autom4te.cache
+ -rm -f src/bridge/$(DEPDIR)/cockpit_askpass-askpass.Po
+ -rm -f src/bridge/$(DEPDIR)/cockpit_bridge-bridge.Po
+ -rm -f src/bridge/$(DEPDIR)/cockpit_pcp-cockpitpcp.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitconnect.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbuscache.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusconfig.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusinternal.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusjson.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusloginmessages.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmachines.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusmeta.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusprocess.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbusrules.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitdbususer.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitechochannel.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfslist.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsread.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfsreplace.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitfswatch.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpithttpstream.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitinteracttransport.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitnullchannel.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpackages.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpacketchannel.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpaths.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpeer.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpipechannel.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitpolkitagent.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitrouter.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitstream.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_bridge_a-cockpitwebsocketstream.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitblocksamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcgroupsamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitcpusamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitdisksamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitinternalmetrics.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmemorysamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmetrics.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitmountsamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitnetworksamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_metrics_a-cockpitsamples.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitconnect.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitdbusinternal.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpcpmetrics.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitpeer.Po
+ -rm -f src/bridge/$(DEPDIR)/libcockpit_pcp_a-cockpitrouter.Po
+ -rm -f src/bridge/$(DEPDIR)/mock_bridge-mock-bridge.Po
+ -rm -f src/bridge/$(DEPDIR)/mock_pmda_so-mock-pmda.Po
+ -rm -f src/bridge/$(DEPDIR)/test_bridge-test-bridge.Po
+ -rm -f src/bridge/$(DEPDIR)/test_connect-test-connect.Po
+ -rm -f src/bridge/$(DEPDIR)/test_dbus_meta-test-dbus-meta.Po
+ -rm -f src/bridge/$(DEPDIR)/test_fs-test-fs.Po
+ -rm -f src/bridge/$(DEPDIR)/test_httpstream-test-httpstream.Po
+ -rm -f src/bridge/$(DEPDIR)/test_metrics-test-metrics.Po
+ -rm -f src/bridge/$(DEPDIR)/test_packages-test-packages.Po
+ -rm -f src/bridge/$(DEPDIR)/test_packet_channel-test-packet-channel.Po
+ -rm -f src/bridge/$(DEPDIR)/test_paths-test-paths.Po
+ -rm -f src/bridge/$(DEPDIR)/test_pcp-test-pcp.Po
+ -rm -f src/bridge/$(DEPDIR)/test_pcp_archives-test-pcp-archives.Po
+ -rm -f src/bridge/$(DEPDIR)/test_peer-test-peer.Po
+ -rm -f src/bridge/$(DEPDIR)/test_pipe_channel-test-pipe-channel.Po
+ -rm -f src/bridge/$(DEPDIR)/test_process-test-process.Po
+ -rm -f src/bridge/$(DEPDIR)/test_router-test-router.Po
+ -rm -f src/bridge/$(DEPDIR)/test_rules-test-rules.Po
+ -rm -f src/bridge/$(DEPDIR)/test_stream-test-stream.Po
+ -rm -f src/bridge/$(DEPDIR)/test_websocketstream-test-websocketstream.Po
+ -rm -f src/common/$(DEPDIR)/cockpitauthorize.Po
+ -rm -f src/common/$(DEPDIR)/cockpitbase64.Po
+ -rm -f src/common/$(DEPDIR)/cockpitclosefrom.Po
+ -rm -f src/common/$(DEPDIR)/cockpitconf.Po
+ -rm -f src/common/$(DEPDIR)/cockpitfdpassing.Po
+ -rm -f src/common/$(DEPDIR)/cockpitframe.Po
+ -rm -f src/common/$(DEPDIR)/cockpithex.Po
+ -rm -f src/common/$(DEPDIR)/cockpitjsonprint.Po
+ -rm -f src/common/$(DEPDIR)/cockpitmemory.Po
+ -rm -f src/common/$(DEPDIR)/cockpitwebcertificate.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitchannel.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitclosefrom.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitcontrolmessages.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpiterror.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitflow.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpithash.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitjson.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitlocale.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitloopback.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitmachinesjson.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitmemfdread.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipe.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitpipetransport.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitsocket.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitsystem.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpittemplate.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpittransport.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitunicode.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitunixsignal.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitversion.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebfilter.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebinject.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebresponse.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-cockpitwebserver.Po
+ -rm -f src/common/$(DEPDIR)/libcockpit_common_a-fail.html.Po
+ -rm -f src/common/$(DEPDIR)/libpreload_temp_home_so-preload-temp-home.Po
+ -rm -f src/common/$(DEPDIR)/test_authorize-test-authorize.Po
+ -rm -f src/common/$(DEPDIR)/test_base64-test-base64.Po
+ -rm -f src/common/$(DEPDIR)/test_channel-test-channel.Po
+ -rm -f src/common/$(DEPDIR)/test_config-test-config.Po
+ -rm -f src/common/$(DEPDIR)/test_frame-test-frame.Po
+ -rm -f src/common/$(DEPDIR)/test_hash-test-hash.Po
+ -rm -f src/common/$(DEPDIR)/test_hex-test-hex.Po
+ -rm -f src/common/$(DEPDIR)/test_json-test-json.Po
+ -rm -f src/common/$(DEPDIR)/test_jsonfds-test-jsonfds.Po
+ -rm -f src/common/$(DEPDIR)/test_locale-test-locale.Po
+ -rm -f src/common/$(DEPDIR)/test_pipe-test-pipe.Po
+ -rm -f src/common/$(DEPDIR)/test_system-test-system.Po
+ -rm -f src/common/$(DEPDIR)/test_template-test-template.Po
+ -rm -f src/common/$(DEPDIR)/test_transport-test-transport.Po
+ -rm -f src/common/$(DEPDIR)/test_unicode-test-unicode.Po
+ -rm -f src/common/$(DEPDIR)/test_unixsignal-test-unixsignal.Po
+ -rm -f src/common/$(DEPDIR)/test_version-test-version.Po
+ -rm -f src/common/$(DEPDIR)/test_webcertificate-test-webcertificate.Po
+ -rm -f src/common/$(DEPDIR)/test_webresponse-test-webresponse.Po
+ -rm -f src/common/$(DEPDIR)/test_webserver-test-webserver.Po
+ -rm -f src/pam-ssh-add/$(DEPDIR)/libpam_ssh_add_a-pam-ssh-add.Po
+ -rm -f src/pam-ssh-add/$(DEPDIR)/pam_ssh_add_so-pam-ssh-add.Po
+ -rm -f src/pam-ssh-add/$(DEPDIR)/test_ssh_add-test-ssh-add.Po
+ -rm -f src/session/$(DEPDIR)/client-certificate.Po
+ -rm -f src/session/$(DEPDIR)/session-utils.Po
+ -rm -f src/session/$(DEPDIR)/session.Po
+ -rm -f src/ssh/$(DEPDIR)/cockpit_ssh-ssh.Po
+ -rm -f src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshoptions.Po
+ -rm -f src/ssh/$(DEPDIR)/libcockpit_ssh_a-cockpitsshrelay.Po
+ -rm -f src/ssh/$(DEPDIR)/mock_sshd-mock-sshd.Po
+ -rm -f src/ssh/$(DEPDIR)/test_sshbridge-test-sshbridge.Po
+ -rm -f src/ssh/$(DEPDIR)/test_sshoptions-test-sshoptions.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-cockpittest.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-mock-auth.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-mock-channel.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-mock-pressure.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-mock-transport.Po
+ -rm -f src/testlib/$(DEPDIR)/libcockpit_test_a-retest.Po
+ -rm -f src/tls/$(DEPDIR)/certificate.Po
+ -rm -f src/tls/$(DEPDIR)/client-certificate.Po
+ -rm -f src/tls/$(DEPDIR)/cockpit-certificate-ensure.Po
+ -rm -f src/tls/$(DEPDIR)/cockpit_wsinstance_factory-wsinstance-factory.Po
+ -rm -f src/tls/$(DEPDIR)/connection.Po
+ -rm -f src/tls/$(DEPDIR)/httpredirect.Po
+ -rm -f src/tls/$(DEPDIR)/main.Po
+ -rm -f src/tls/$(DEPDIR)/server.Po
+ -rm -f src/tls/$(DEPDIR)/socket-activation-helper.Po
+ -rm -f src/tls/$(DEPDIR)/socket-io.Po
+ -rm -f src/tls/$(DEPDIR)/test_cockpit_certificate_ensure-test-cockpit-certificate-ensure.Po
+ -rm -f src/tls/$(DEPDIR)/test_tls_connection-test-connection.Po
+ -rm -f src/tls/$(DEPDIR)/test_tls_server-test-server.Po
+ -rm -f src/tls/$(DEPDIR)/wsinstance-start.Po
+ -rm -f src/websocket/$(DEPDIR)/frob_websocket-frob-websocket.Po
+ -rm -f src/websocket/$(DEPDIR)/libwebsocket_a-websocket.Po
+ -rm -f src/websocket/$(DEPDIR)/libwebsocket_a-websocketclient.Po
+ -rm -f src/websocket/$(DEPDIR)/libwebsocket_a-websocketconnection.Po
+ -rm -f src/websocket/$(DEPDIR)/libwebsocket_a-websocketserver.Po
+ -rm -f src/websocket/$(DEPDIR)/test_websocket-test-websocket.Po
+ -rm -f src/ws/$(DEPDIR)/cockpit_ws-main.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitauth.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitbranding.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelresponse.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitchannelsocket.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcompat.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitcreds.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpithandlers.Po
+ -rm -f src/ws/$(DEPDIR)/libcockpit_ws_a-cockpitwebservice.Po
+ -rm -f src/ws/$(DEPDIR)/mock_auth_command-mock-auth-command.Po
+ -rm -f src/ws/$(DEPDIR)/mock_echo-mock-echo.Po
+ -rm -f src/ws/$(DEPDIR)/mock_pam_conv_mod_so-mock-pam-conv-mod.Po
+ -rm -f src/ws/$(DEPDIR)/pam_cockpit_cert_so-pam_cockpit_cert.Po
+ -rm -f src/ws/$(DEPDIR)/test_auth-test-auth.Po
+ -rm -f src/ws/$(DEPDIR)/test_authssh-test-authssh.Po
+ -rm -f src/ws/$(DEPDIR)/test_channelresponse-test-channelresponse.Po
+ -rm -f src/ws/$(DEPDIR)/test_compat-test-compat.Po
+ -rm -f src/ws/$(DEPDIR)/test_creds-test-creds.Po
+ -rm -f src/ws/$(DEPDIR)/test_handlers-test-handlers.Po
+ -rm -f src/ws/$(DEPDIR)/test_kerberos-test-kerberos.Po
+ -rm -f src/ws/$(DEPDIR)/test_server-mock-dbus-tests.Po
+ -rm -f src/ws/$(DEPDIR)/test_server-mock-service.Po
+ -rm -f src/ws/$(DEPDIR)/test_server-test-server.Po
+ -rm -f src/ws/$(DEPDIR)/test_webservice-test-webservice.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-binPROGRAMS uninstall-cockpitwsPROGRAMS \
+ uninstall-dist_applicationsDATA \
+ uninstall-dist_archbrandingDATA \
+ uninstall-dist_centosbrandingDATA \
+ uninstall-dist_client_metainfoDATA \
+ uninstall-dist_cockpitclientDATA \
+ uninstall-dist_cockpitclientSCRIPTS \
+ uninstall-dist_dbusservicesDATA \
+ uninstall-dist_debianbrandingDATA \
+ uninstall-dist_defaultbrandingDATA \
+ uninstall-dist_fedorabrandingDATA \
+ uninstall-dist_kubernetesbrandingDATA uninstall-dist_motdDATA \
+ uninstall-dist_motdSCRIPTS uninstall-dist_opensusebrandingDATA \
+ uninstall-dist_pcpmanifestDATA uninstall-dist_pixmapDATA \
+ uninstall-dist_pmlogconfDATA \
+ uninstall-dist_registrybrandingDATA \
+ uninstall-dist_rhelbrandingDATA \
+ uninstall-dist_scalableiconDATA \
+ uninstall-dist_scientificbrandingDATA \
+ uninstall-dist_sshmanifestDATA uninstall-dist_symboliciconDATA \
+ uninstall-dist_systemdunitDATA \
+ uninstall-dist_ubuntubrandingDATA uninstall-libexecPROGRAMS \
+ uninstall-libexecSCRIPTS uninstall-local uninstall-man \
+ uninstall-nodist_appdataDATA uninstall-nodist_metainfoDATA \
+ uninstall-nodist_systemdunitDATA uninstall-nodist_tempconfDATA \
+ uninstall-pamPROGRAMS uninstall-pixmapsDATA \
+ uninstall-polkitDATA uninstall-sbinPROGRAMS \
+ uninstall-selinuxpackagesDATA
+
+uninstall-man: uninstall-man1 uninstall-man5 uninstall-man8
+
+.MAKE: all check check-am install install-am install-data-am \
+ install-exec install-exec-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles am--refresh check \
+ check-TESTS check-am check-local clean clean-binPROGRAMS \
+ clean-checkLIBRARIES clean-checkPROGRAMS \
+ clean-cockpitwsPROGRAMS clean-cscope clean-generic \
+ clean-libexecPROGRAMS clean-local clean-noinstLIBRARIES \
+ clean-noinstPROGRAMS clean-pamPROGRAMS clean-sbinPROGRAMS \
+ cscope cscopelist-am ctags ctags-am dist dist-all dist-bzip2 \
+ dist-gzip dist-hook dist-lzip dist-shar dist-tarZ dist-xz \
+ dist-zip dist-zstd distcheck distclean distclean-compile \
+ distclean-generic distclean-hdr distclean-tags distcleancheck \
+ distdir distuninstallcheck dvi dvi-am html html-am info \
+ info-am install install-am install-binPROGRAMS \
+ install-cockpitwsPROGRAMS install-data install-data-am \
+ install-data-hook install-data-local \
+ install-dist_applicationsDATA install-dist_archbrandingDATA \
+ install-dist_centosbrandingDATA \
+ install-dist_client_metainfoDATA \
+ install-dist_cockpitclientDATA \
+ install-dist_cockpitclientSCRIPTS \
+ install-dist_dbusservicesDATA install-dist_debianbrandingDATA \
+ install-dist_defaultbrandingDATA \
+ install-dist_fedorabrandingDATA \
+ install-dist_kubernetesbrandingDATA install-dist_motdDATA \
+ install-dist_motdSCRIPTS install-dist_opensusebrandingDATA \
+ install-dist_pcpmanifestDATA install-dist_pixmapDATA \
+ install-dist_pmlogconfDATA install-dist_registrybrandingDATA \
+ install-dist_rhelbrandingDATA install-dist_scalableiconDATA \
+ install-dist_scientificbrandingDATA \
+ install-dist_sshmanifestDATA install-dist_symboliciconDATA \
+ install-dist_systemdunitDATA install-dist_ubuntubrandingDATA \
+ install-dvi install-dvi-am install-exec install-exec-am \
+ install-exec-hook install-html install-html-am install-info \
+ install-info-am install-libexecPROGRAMS install-libexecSCRIPTS \
+ install-man install-man1 install-man5 install-man8 \
+ install-nodist_appdataDATA install-nodist_metainfoDATA \
+ install-nodist_systemdunitDATA install-nodist_tempconfDATA \
+ install-pamPROGRAMS install-pdf install-pdf-am \
+ install-pixmapsDATA install-polkitDATA install-ps \
+ install-ps-am install-sbinPROGRAMS install-selinuxpackagesDATA \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic pdf pdf-am ps ps-am \
+ recheck tags tags-am uninstall uninstall-am \
+ uninstall-binPROGRAMS uninstall-cockpitwsPROGRAMS \
+ uninstall-dist_applicationsDATA \
+ uninstall-dist_archbrandingDATA \
+ uninstall-dist_centosbrandingDATA \
+ uninstall-dist_client_metainfoDATA \
+ uninstall-dist_cockpitclientDATA \
+ uninstall-dist_cockpitclientSCRIPTS \
+ uninstall-dist_dbusservicesDATA \
+ uninstall-dist_debianbrandingDATA \
+ uninstall-dist_defaultbrandingDATA \
+ uninstall-dist_fedorabrandingDATA \
+ uninstall-dist_kubernetesbrandingDATA uninstall-dist_motdDATA \
+ uninstall-dist_motdSCRIPTS uninstall-dist_opensusebrandingDATA \
+ uninstall-dist_pcpmanifestDATA uninstall-dist_pixmapDATA \
+ uninstall-dist_pmlogconfDATA \
+ uninstall-dist_registrybrandingDATA \
+ uninstall-dist_rhelbrandingDATA \
+ uninstall-dist_scalableiconDATA \
+ uninstall-dist_scientificbrandingDATA \
+ uninstall-dist_sshmanifestDATA uninstall-dist_symboliciconDATA \
+ uninstall-dist_systemdunitDATA \
+ uninstall-dist_ubuntubrandingDATA uninstall-libexecPROGRAMS \
+ uninstall-libexecSCRIPTS uninstall-local uninstall-man \
+ uninstall-man1 uninstall-man5 uninstall-man8 \
+ uninstall-nodist_appdataDATA uninstall-nodist_metainfoDATA \
+ uninstall-nodist_systemdunitDATA uninstall-nodist_tempconfDATA \
+ uninstall-pamPROGRAMS uninstall-pixmapsDATA \
+ uninstall-polkitDATA uninstall-sbinPROGRAMS \
+ uninstall-selinuxpackagesDATA
+
+.PRECIOUS: Makefile
+
+
+# See https://www.gnu.org/software/make/manual/html_node/Force-Targets.html
+FORCE:
+
+# We want tools/node-modules to run every time package-lock.json is requested
+$(srcdir)/package-lock.json: FORCE
+ $(V_NODE_MODULES) $(srcdir)/tools/node-modules make_package_lock_json
+distdir: $(DISTFILES)
+ @if [ -e '$(srcdir)/.git' ]; then \
+ git -C '$(srcdir)' ls-files -x test/reference .fmf plans pkg test tools > .extra_dist.tmp && \
+ mv .extra_dist.tmp '$(srcdir)/.extra_dist'; fi
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am EXTRA_FILES="$$(tr '\n' ' ' < $(srcdir)/.extra_dist) .extra_dist"
+ sed -i "s/[@]VERSION@/$(VERSION)/" "$(distdir)/src/client/org.cockpit_project.CockpitClient.metainfo.xml"
+ $(srcdir)/tools/fix-spec $(distdir)/tools/cockpit.spec $(VERSION)
+ test -z '$(HACK_SPEC_FOR_PYTHON)' || \
+ sed -i 's/\(define enable_old_bridge\) 1/\1 0/' $(distdir)/tools/cockpit.spec
+ sed -i "/^pkgver=/ s/0/$(VERSION)/" "$(distdir)/tools/arch/PKGBUILD"
+ sed -i "1 s/0/$(VERSION)/" "$(distdir)/tools/debian/changelog"
+ cp -r "$(srcdir)/dist" "$(distdir)"
+ $(srcdir)/tools/adjust-distdir-timestamps "$(distdir)"
+ @echo ' DIST $(DIST_ARCHIVES)'
+
+# Needed to ensure the tarball is correct for $(VERSION) override
+dist-hook: $(distdir)/src/cockpit/_version.py
+$(distdir)/src/cockpit/_version.py: FORCE
+ python3 '$(srcdir)'/src/build_backend.py --copy '$(srcdir)' '$(distdir)'
+ @rm -f $(distdir)/src/cockpit/_version.py
+ $(AM_V_GEN) echo "__version__ = '$(VERSION)'" > $@
+
+$(distdir)/version.m4: FORCE
+ @rm -f $(distdir)/version.m4
+ $(AM_V_GEN) echo 'm4_define(VERSION_NUMBER, [$(VERSION)])' > $@
+
+# be careful not to include autotools cache into dist tarballs
+dist-hook: $(distdir)/configure
+$(distdir)/configure: $(distdir)/version.m4
+ @rm -f $(distdir)/configure
+ $(AM_V_GEN) autoreconf $(distdir)
+ @rm -r $(distdir)/autom4te.cache
+
+# Various downstream packaging assets
+dist-hook: $(distdir)/tools/debian/copyright
+$(distdir)/tools/debian/copyright: $(DIST_STAMP)
+ $(AM_V_GEN) NODE_ENV=$(NODE_ENV) $(srcdir)/tools/build-debian-copyright > $@
+
+# Validate our AppStream metadata
+distcheck-hook::
+ find $(distdir) -name '*.metainfo.xml' -o -name '*.appdata.xml' | xargs appstream-util validate --nonet
+
+# validate that we don't bundle the embedded patternfly font files, we use them from /static/fonts/
+distcheck-hook::
+ ! grep --color=auto -rn "\.\./fonts/OpenSans\|fonts/.*eot\|truetype" $(distdir)/dist
+
+clean-local::
+ find $(builddir) -name '*.gc??' -delete
+ find $(srcdir) -name '*.pyc' -delete
+
+# required for running unit and integration tests; commander and ws are deps of chrome-remote-interface
+node_modules/%: $(srcdir)/package-lock.json
+ @true
+
+check: export VERBOSE=1
+
+check-memory:
+ $(MAKE) LOG_FLAGS="$(VALGRIND)" \
+ HTML_TEST_WRAPPER="$(VALGRIND)" \
+ COCKPIT_SKIP_SLOW_TESTS=1 \
+ $(AM_MAKEFLAGS) check TESTS="$(filter-out test/% bots/%,$(TESTS))"
+recheck-memory:
+ $(MAKE) LOG_FLAGS="$(VALGRIND_ARGS)" \
+ HTML_TEST_WRAPPER="$(VALGRIND)" \
+ $(AM_MAKEFLAGS) recheck
+
+# checkout Cockpit's bots for standard test VM images and API to launch them
+# must be from main, as only that has current and existing images; but testvm.py API is stable
+# support CI testing against a bots change
+bots:
+ test/common/make-bots
+
+.PHONY: rsync
+RSYNC_HOST ?= c
+RSYNC_DEST ?= $(RSYNC_HOST):/
+rsync:
+ $(MAKE)
+ $(MAKE) install DESTDIR=tmp/rsync >/dev/null
+ rsync --recursive --links --checksum --verbose --inplace tmp/rsync/ $(RSYNC_DEST)
+.PHONY: $(CHECK_LOCAL_TARGETS)
+check-local:: $(CHECK_LOCAL_TARGETS)
+.PHONY: $(CLEAN_LOCAL_TARGETS)
+clean-local:: $(CLEAN_LOCAL_TARGETS)
+.PHONY: $(INSTALL_DATA_LOCAL_TARGETS)
+install-data-local:: $(INSTALL_DATA_LOCAL_TARGETS)
+.PHONY: $(INSTALL_EXEC_HOOK_TARGETS)
+install-exec-hook:: $(INSTALL_EXEC_HOOK_TARGETS)
+.PHONY: $(UNINSTALL_LOCAL_TARGETS)
+uninstall-local:: $(UNINSTALL_LOCAL_TARGETS)
+
+ws-container:
+ rm -rf $(WS_DIR)/rpms
+ if [ -e cockpit-bridge-*.rpm ]; then mkdir -p $(WS_DIR)/rpms; cp *.rpm $(WS_DIR)/rpms; fi
+ podman build -t quay.io/cockpit/ws $(WS_DIR)
+
+ws-container-shell:
+ podman run -ti --rm --publish=9001:9090 quay.io/cockpit/ws /bin/bash
+build-for-flatpak: cockpit-ws
+
+install-for-flatpak: $(INSTALL_FLATPAK_TARGETS)
+ appstream-util validate --nonet src/client/org.cockpit_project.CockpitClient.metainfo.xml
+ if test -s "${DOWNSTREAM_RELEASES_XML}"; then \
+ $(top_srcdir)/tools/patch-metainfo \
+ '$(DESTDIR)$(datadir)/metainfo/org.cockpit_project.CockpitClient.metainfo.xml' \
+ "${DOWNSTREAM_RELEASES_XML}"; \
+ fi
+ appstream-util validate --nonet $(DESTDIR)$(datadir)/metainfo/org.cockpit_project.CockpitClient.metainfo.xml
+ cp -rT dist/static $(DESTDIR)$(pkgdatadir)/static
+ rm -rf $(DESTDIR)$(pkgdatadir)/apps $(DESTDIR)$(pkgdatadir)/playground
+
+@ENABLE_DOC_TRUE@render-doc-images:
+@ENABLE_DOC_TRUE@ inkscape --without-gui --export-area-page \
+@ENABLE_DOC_TRUE@ --export-width=1280 --export-height=960 \
+@ENABLE_DOC_TRUE@ --export-png=$(srcdir)/doc/cockpit-transport.png \
+@ENABLE_DOC_TRUE@ $(srcdir)/doc/cockpit-transport.svg
+
+@ENABLE_DOC_TRUE@doc/guide/html/%.woff2: dist/static/manifest.json
+@ENABLE_DOC_TRUE@ @target='$(dir $@)'; \
+@ENABLE_DOC_TRUE@ source='dist/static/fonts/$(notdir $@)'; \
+@ENABLE_DOC_TRUE@ printf ' %-8s %s → %s\n' 'COPY' "$${source}" "$${target}"; \
+@ENABLE_DOC_TRUE@ mkdir -p "$${target}" && \
+@ENABLE_DOC_TRUE@ cp "$(top_srcdir)/$${source}" "$${target}"
+
+@ENABLE_DOC_TRUE@doc/guide/html/index.html: $(GUIDE_DOCBOOK) $(GUIDE_INCLUDES) $(GUIDE_STATIC) $(GUIDE_XSLT) $(GUIDE_FONTS)
+@ENABLE_DOC_TRUE@ $(AM_V_GEN) mkdir -p doc/guide/html/ && \
+@ENABLE_DOC_TRUE@ cp $(addprefix $(srcdir)/,$(GUIDE_STATIC)) doc/guide/html/ && \
+@ENABLE_DOC_TRUE@ LANG=C.UTF-8 $(XMLTO) html -m $(srcdir)/doc/guide/gtk-doc.xsl -o doc/guide/html/ \
+@ENABLE_DOC_TRUE@ --searchpath $(abs_builddir):$(abs_srcdir):$(abs_builddir)/doc/guide \
+@ENABLE_DOC_TRUE@ $(srcdir)/$(GUIDE_DOCBOOK) && \
+@ENABLE_DOC_TRUE@ rm -f doc/guide/html/cockpit-guide.proc
+@ENABLE_DOC_TRUE@clean-guide:
+@ENABLE_DOC_TRUE@ rm -rf doc/guide/html
+@ENABLE_DOC_TRUE@check-guide:
+@ENABLE_DOC_TRUE@ if grep -n -r 'name="id' doc/guide/html >&2; then \
+@ENABLE_DOC_TRUE@ echo "Unexpected generated id in the documentation" >&2; \
+@ENABLE_DOC_TRUE@ exit 1; \
+@ENABLE_DOC_TRUE@ fi
+@ENABLE_DOC_TRUE@install-guide:
+@ENABLE_DOC_TRUE@ mkdir -p $(DESTDIR)$(htmldir)
+@ENABLE_DOC_TRUE@ $(INSTALL_DATA) doc/guide/html/* $(DESTDIR)$(htmldir)
+@ENABLE_DOC_TRUE@uninstall-guide:
+@ENABLE_DOC_TRUE@ rm -rf $(DESTDIR)$(htmldir)
+
+@ENABLE_DOC_TRUE@%.8: %.xml
+@ENABLE_DOC_TRUE@ $(AM_V_GEN) $(MAN_PROC)
+@ENABLE_DOC_TRUE@%.1: %.xml
+@ENABLE_DOC_TRUE@ $(AM_V_GEN) $(MAN_PROC)
+@ENABLE_DOC_TRUE@%.5: %.xml
+@ENABLE_DOC_TRUE@ $(AM_V_GEN) $(MAN_PROC)
+
+# delete the stamp first; neither webpack nor esbuild touch it if the contents didn't change,
+# but this is just a representative for all of dist/*
+$(DIST_STAMP): $(srcdir)/package-lock.json $(PKG_INPUTS)
+ @rm -f $(DIST_STAMP)
+ $(V_BUNDLE) cd $(srcdir) && NODE_ENV='$(NODE_ENV)' tools/termschutz ./build.js
+
+# This is how the qunit tests get included. We need to prevent automake from
+# seeing them during ./autogen.sh, but need make to find them at compile time.
+# We don't run them in the pybridge case since they're part of `pytest`.
+@WITH_OLD_BRIDGE_TRUE@-include $(wildcard pkg/Makefile.qunit*)
+install-bundles:
+ cd $(srcdir)/dist; find */* -type f -exec install -D -m 644 '{}' '$(abspath $(DESTDIR)$(datadir))/cockpit/{}' \;
+uninstall-bundles:
+ rm -rf $(DESTDIR)$(datadir)/cockpit
+
+# Extract translate attribute, Glade style, angular-gettext HTML translations
+po/cockpit.html.pot: $(srcdir)/package-lock.json
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ $(srcdir)/pkg/lib/html2po.js -d $(srcdir) -o $@ \
+ $$(cd $(srcdir) && find pkg/ -name '*.html')
+
+# Extract cockpit style javascript translations
+po/cockpit.js.pot:
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ xgettext --default-domain=cockpit --output=- --language=C --keyword= \
+ --keyword=_:1,1t --keyword=_:1c,2,2t --keyword=C_:1c,2 \
+ --keyword=N_ --keyword=NC_:1c,2 \
+ --keyword=gettext:1,1t --keyword=gettext:1c,2,2t \
+ --keyword=ngettext:1,2,3t --keyword=ngettext:1c,2,3,4t \
+ --keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \
+ --from-code=UTF-8 --directory=$(srcdir) \
+ $$( cd $(srcdir) && find pkg/ ! -name 'test-*' -name '*.js' -o -name '*.jsx') | \
+ sed '/^#/ s/, c-format//' > $@
+
+po/cockpit.manifest.pot: $(srcdir)/package-lock.json
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ $(srcdir)/pkg/lib/manifest2po.js -d $(srcdir) -o $@ \
+ $$(cd $(srcdir) && find pkg/ -name 'manifest.json')
+
+po/cockpit.appstream.pot:
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ GETTEXTDATADIRS=$(srcdir)/po xgettext --output=$@ --directory=$(srcdir) \
+ $$(cd $(srcdir) && find pkg/ src/ -name '*.appdata.xml.in' -o -name '*.metainfo.xml.in')
+
+po/cockpit.polkit.pot:
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ GETTEXTDATADIRS=$(srcdir)/po xgettext --output=$@ --directory=$(srcdir) \
+ $$(cd $(srcdir) && find src/ -name '*.policy.in')
+
+# Combine the above pot files into one
+po/cockpit.pot: po/cockpit.html.pot po/cockpit.js.pot po/cockpit.manifest.pot po/cockpit.appstream.pot po/cockpit.polkit.pot
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ msgcat --sort-output --output-file=$@ $^
+
+@SELINUX_POLICY_ENABLED_TRUE@cockpit.pp: $(SELINUX_POLICY_FILES)
+@SELINUX_POLICY_ENABLED_TRUE@ $(AM_V_GEN) make -sf /usr/share/selinux/devel/Makefile cockpit.pp
+
+@SELINUX_POLICY_ENABLED_TRUE@cockpit.pp.bz2: cockpit.pp
+@SELINUX_POLICY_ENABLED_TRUE@ $(AM_V_GEN) bzip2 -9 < $< > $@.tmp && mv $@.tmp $@
+@SELINUX_POLICY_ENABLED_TRUE@install-selinux:
+@SELINUX_POLICY_ENABLED_TRUE@ $(INSTALL) -d -m 700 $(DESTDIR)$(selinuxactivedir)
+
+# -----------------------------------------------------------------------------
+# Python
+
+# Will only be honoured if pytest-timeout plugin is installed
+export PYTEST_TIMEOUT = 120
+
+.PHONY: pytest
+pytest: $(BUILT_SOURCES) $(DIST_STAMP) $(MANIFESTS)
+ $(MAKE) test-server
+ cd '$(srcdir)' && abs_builddir='$(abs_builddir)' pytest
+
+.PHONY: pytest-cov
+pytest-cov: $(BUILT_SOURCES) $(DIST_STAMP) $(MANIFESTS)
+ $(MAKE) test-server
+ cd '$(srcdir)' && abs_builddir='$(abs_builddir)' pytest --cov
+@WITH_OLD_BRIDGE_FALSE@install-python:
+@WITH_OLD_BRIDGE_FALSE@ @# wheel-based installation with .dist-info.
+@WITH_OLD_BRIDGE_FALSE@ @# This needs to work on RHEL8 up through modern Fedora, offline, with
+@WITH_OLD_BRIDGE_FALSE@ @# system packages available to the build.
+@WITH_OLD_BRIDGE_FALSE@ python3 -m pip install --no-index --force-reinstall --root='$(DESTDIR)/' --prefix='$(prefix)' \
+@WITH_OLD_BRIDGE_FALSE@ "$$(python3 '$(srcdir)'/src/build_backend.py --wheel '$(srcdir)' tmp/wheel)"
+@WITH_OLD_BRIDGE_FALSE@ mkdir -p $(DESTDIR)$(libexecdir)
+@WITH_OLD_BRIDGE_FALSE@ mv -t $(DESTDIR)$(libexecdir) $(DESTDIR)$(bindir)/cockpit-askpass
+@WITH_OLD_BRIDGE_FALSE@uninstall-python:
+@WITH_OLD_BRIDGE_FALSE@ rm -rf tmp/wheel
+@WITH_OLD_BRIDGE_FALSE@ rm -f $(DESTDIR)$(libexecdir)/cockpit-askpass
+@WITH_OLD_BRIDGE_FALSE@ rm -f $(DESTDIR)$(bindir)/cockpit-bridge
+@WITH_OLD_BRIDGE_FALSE@ @# HACK: pip uninstall does not know about --root and --prefix
+@WITH_OLD_BRIDGE_FALSE@ rm -r $(DESTDIR)$(prefix)/lib/python*/*-packages/cockpit \
+@WITH_OLD_BRIDGE_FALSE@ $(DESTDIR)$(prefix)/lib/python*/*-packages/cockpit-*.dist-info
+
+# -----------------------------------------------------------------------------
+# AppStream metadata
+#
+%.metainfo.xml: %.metainfo.xml.in
+ $(AM_V_GEN) mkdir -p $(dir $@) && msgfmt --xml -d $(top_srcdir)/po --template $< --output $@
+
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/archlinux-logo.png $(DESTDIR)$(archbrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/archlinux-logo.png $(DESTDIR)$(archbrandingdir)/apple-touch-icon.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/archlinux-logo.png $(DESTDIR)$(archbrandingdir)/favicon.ico
+
+# Opportunistically use fedora-logos
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/system-logo-white.png $(DESTDIR)$(centosbrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/fedora-logo-sprite.png $(DESTDIR)$(centosbrandingdir)/apple-touch-icon.png
+ ln -sTfr $(DESTDIR)/etc/favicon.png $(DESTDIR)$(centosbrandingdir)/favicon.ico
+
+# Opportunistically use debconf debian logos
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/debian-logo.png $(DESTDIR)$(debianbrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/debian-logo.png $(DESTDIR)$(debianbrandingdir)/favicon.ico
+
+# Opportunistically use fedora-logos
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/system-logo-white.png $(DESTDIR)$(fedorabrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/fedora-logo-sprite.png $(DESTDIR)$(fedorabrandingdir)/apple-touch-icon.png
+ ln -sTfr $(DESTDIR)/etc/favicon.png $(DESTDIR)$(fedorabrandingdir)/favicon.ico
+
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/wallpapers/default-1920x1200.jpg $(DESTDIR)$(opensusebrandingdir)/default-1920x1200.jpg
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/distribution-logos/square-hicolor.svg $(DESTDIR)$(opensusebrandingdir)/square-hicolor.svg
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/distribution-logos/favicon.ico $(DESTDIR)$(opensusebrandingdir)/favicon.ico
+
+# Opportunistically use redhat-logos ... yes they're called 'fedora'
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/system-logo-white.png $(DESTDIR)$(rhelbrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/fedora-logo-sprite.png $(DESTDIR)$(rhelbrandingdir)/apple-touch-icon.png
+ ln -sTfr $(DESTDIR)/etc/favicon.png $(DESTDIR)$(rhelbrandingdir)/favicon.ico
+
+# Opportunistically use Scientific Linux logos.
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/system-logo-white.png $(DESTDIR)$(scientificbrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/fedora-logo-sprite.png $(DESTDIR)$(scientificbrandingdir)/apple-touch-icon.png
+ ln -sTfr $(DESTDIR)/etc/favicon.png $(DESTDIR)$(scientificbrandingdir)/favicon.ico
+
+# Opportunistically use plymouth ubuntu logo
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/plymouth/ubuntu-logo.png $(DESTDIR)$(ubuntubrandingdir)/logo.png
+
+@WITH_POLKIT_TRUE@%.policy: %.policy.in $(PO_FILES)
+@WITH_POLKIT_TRUE@ $(AM_V_GEN) GETTEXTDATADIRS=$(srcdir)/po msgfmt --xml -d $(top_srcdir)/po --template $< --output $@
+@ENABLE_COCKPIT_CLIENT_TRUE@install-cockpit-client-symlink:
+@ENABLE_COCKPIT_CLIENT_TRUE@ mkdir -p $(DESTDIR)/$(bindir)
+@ENABLE_COCKPIT_CLIENT_TRUE@ ln -sfTv $(cockpitclientdir)/cockpit-client $(DESTDIR)/$(bindir)/cockpit-client
+src/common/fail.html.c: src/common/fail.html
+ $(AM_V_GEN) $(top_srcdir)/tools/escape-to-c cockpit_webresponse_fail_html_text < $< > $@.tmp && mv $@.tmp $@
+
+# If running cockpit-ws as a non-standard user, we also set up
+# cockpit-session to be setuid root, but only runnable by cockpit-session
+install-exec-hook::
+ chown -f root:$(COCKPIT_WSINSTANCE_GROUP) $(DESTDIR)$(libexecdir)/cockpit-session || true
+ test "$(COCKPIT_USER)" != "root" && chmod -f 4750 $(DESTDIR)$(libexecdir)/cockpit-session || true
+@WITH_COCKPIT_SSH_TRUE@test_rsa_key: src/ssh/test_rsa
+@WITH_COCKPIT_SSH_TRUE@ $(AM_V_GEN) cp $< $@ && chmod 600 $@
+
+@WITH_COCKPIT_SSH_TRUE@update-known-hosts:
+@WITH_COCKPIT_SSH_TRUE@ cat $(srcdir)/src/ssh/mock_*.pub | \
+@WITH_COCKPIT_SSH_TRUE@ sed -ne 's/\(.*\) [^ ]\+$$/[localhost]:*,[127.0.0.1]:* \1/p' > \
+@WITH_COCKPIT_SSH_TRUE@ $(srcdir)/src/ssh/mock_known_hosts
+
+# Automake: 'Variables using ... ‘sysconf’ ... are installed by install-exec.'
+install-exec-hook::
+ mkdir -p $(DESTDIR)$(sysconfdir)/motd.d
+ ln -sTfr $(DESTDIR)/run/cockpit/motd $(DESTDIR)$(sysconfdir)/motd.d/cockpit
+ mkdir -p $(DESTDIR)$(sysconfdir)/issue.d
+ ln -sTfr $(DESTDIR)/run/cockpit/motd $(DESTDIR)$(sysconfdir)/issue.d/cockpit.issue
+
+# we can't generate these with config.status because,
+# eg. it does "@libexecdir@" -> "${exec_prefix}/libexec"
+src/systemd/%: src/systemd/%.in
+ $(AM_V_GEN) mkdir -p $(dir $@) && sed \
+ -e 's,[@]PACKAGE[@],$(PACKAGE),g' \
+ -e 's,[@]admin_group[@],$(admin_group),g' \
+ -e 's,[@]datadir[@],$(datadir),g' \
+ -e 's,[@]group[@],$(COCKPIT_GROUP),g' \
+ -e 's,[@]libexecdir[@],$(libexecdir),g' \
+ -e 's,[@]user[@],$(COCKPIT_USER),g' \
+ -e 's,[@]wsinstancegroup[@],$(COCKPIT_WSINSTANCE_GROUP),g' \
+ -e 's,[@]wsinstanceuser[@],$(COCKPIT_WSINSTANCE_USER),g' \
+ $< > $@.tmp && mv -f $@.tmp $@
+
+src/ws/mock-dbus-tests.h: $(GDBUS_CODEGEN_XML)
+ $(GDBUS_CODEGEN_INVOCATION) --header --output $@ $<
+
+src/ws/mock-dbus-tests.c: $(GDBUS_CODEGEN_XML)
+ $(GDBUS_CODEGEN_INVOCATION) --body --output $@ $<
+
+install-tests::
+ mkdir -p $(DESTDIR)$(pamdir) $(DESTDIR)/etc/cockpit
+ $(INSTALL_PROGRAM) mock-pam-conv-mod.so $(DESTDIR)$(pamdir)/
+
+install-exec-hook::
+ mkdir -p $(DESTDIR)$(sysconfdir)/cockpit/ws-certs.d $(DESTDIR)$(sysconfdir)/cockpit/machines.d
+ chmod 755 $(DESTDIR)$(sysconfdir)/cockpit/ws-certs.d $(DESTDIR)$(sysconfdir)/cockpit/machines.d
+$(nodist_appdata_DATA): $(appdata_in) $(PO_FILES)
+ $(AM_V_GEN) msgfmt --xml -d $(top_srcdir)/po --template $< --output $@
+
+@WITH_COVERAGE_TRUE@coverage:
+@WITH_COVERAGE_TRUE@ mkdir -p tools/coverage
+@WITH_COVERAGE_TRUE@ $(MAKE)
+@WITH_COVERAGE_TRUE@ lcov --directory . --capture --initial \
+@WITH_COVERAGE_TRUE@ --output-file tools/coverage.base
+@WITH_COVERAGE_TRUE@ $(MAKE) check
+@WITH_COVERAGE_TRUE@ lcov --directory . --capture \
+@WITH_COVERAGE_TRUE@ --output-file tools/coverage.test
+@WITH_COVERAGE_TRUE@ lcov --directory . \
+@WITH_COVERAGE_TRUE@ --output tools/coverage.all \
+@WITH_COVERAGE_TRUE@ --add-tracefile tools/coverage.base \
+@WITH_COVERAGE_TRUE@ --add-tracefile tools/coverage.test
+@WITH_COVERAGE_TRUE@ lcov --directory . \
+@WITH_COVERAGE_TRUE@ --remove tools/coverage.all \
+@WITH_COVERAGE_TRUE@ --output tools/coverage.info \
+@WITH_COVERAGE_TRUE@ $(BUILT_SOURCES) 'test-*' 'mock-*' 'frob-*' '/usr/include/*'
+@WITH_COVERAGE_TRUE@ genhtml --output-directory tools/coverage \
+@WITH_COVERAGE_TRUE@ --title "cockpit $(PACKAGE_VERSION)" \
+@WITH_COVERAGE_TRUE@ tools/coverage.info
+@WITH_COVERAGE_TRUE@ @echo "file://$(abs_top_builddir)/tools/coverage/index.html"
+
+# This Makefile includes several variable definitions that must come first
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0b5ab08
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+# Cockpit
+**A sysadmin login session in a web browser**
+
+[cockpit-project.org](https://cockpit-project.org/)
+
+Cockpit is an interactive server admin interface. It is easy to use and very lightweight.
+Cockpit interacts directly with the operating system from a real Linux session in a browser.
+
+### Using Cockpit
+
+You can [install Cockpit](https://cockpit-project.org/running.html) on many Linux operating
+systems including Debian, Fedora and RHEL.
+
+Cockpit makes Linux discoverable, allowing sysadmins to easily perform tasks such as starting
+containers, storage administration, network configuration, inspecting logs and so on.
+
+Jumping between the terminal and the web tool is no problem. A service started via Cockpit
+can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen
+in the Cockpit journal interface.
+
+You can also easily add other machines that have Cockpit installed and are accessible via SSH and jump
+between these hosts.
+
+### Development
+
+ * [Making changes to Cockpit](HACKING.md)
+ * [How to contribute, developer documentation](https://github.com/cockpit-project/cockpit/wiki/Contributing)
+ * Matrix Channel: [#cockpit:fedoraproject.org](https://matrix.to/#/#cockpit:fedoraproject.org)
+ * [Mailing List](https://lists.fedorahosted.org/admin/lists/cockpit-devel.lists.fedorahosted.org/)
+ * [Guiding Principles](https://cockpit-project.org/ideals.html)
+ * [Release Notes](https://cockpit-project.org/blog/category/release.html)
+ * [Privacy Policy](https://cockpit-project.org/privacy.html)
diff --git a/aclocal.m4 b/aclocal.m4
new file mode 100644
index 0000000..989c08f
--- /dev/null
+++ b/aclocal.m4
@@ -0,0 +1,1853 @@
+# generated automatically by aclocal 1.16.5 -*- Autoconf -*-
+
+# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+m4_ifndef([AC_CONFIG_MACRO_DIRS], [m4_defun([_AM_CONFIG_MACRO_DIRS], [])m4_defun([AC_CONFIG_MACRO_DIRS], [_AM_CONFIG_MACRO_DIRS($@)])])
+m4_ifndef([AC_AUTOCONF_VERSION],
+ [m4_copy([m4_PACKAGE_VERSION], [AC_AUTOCONF_VERSION])])dnl
+m4_if(m4_defn([AC_AUTOCONF_VERSION]), [2.71],,
+[m4_warning([this file was generated for autoconf 2.71.
+You have another version of autoconf. It may work, but is not guaranteed to.
+If you have problems, you may need to regenerate the build system entirely.
+To do so, use the procedure documented by the package, typically 'autoreconf'.])])
+
+# pkg.m4 - Macros to locate and use pkg-config. -*- Autoconf -*-
+# serial 12 (pkg-config-0.29.2)
+
+dnl Copyright © 2004 Scott James Remnant <scott@netsplit.com>.
+dnl Copyright © 2012-2015 Dan Nicholson <dbn.lists@gmail.com>
+dnl
+dnl This program is free software; you can redistribute it and/or modify
+dnl it under the terms of the GNU General Public License as published by
+dnl the Free Software Foundation; either version 2 of the License, or
+dnl (at your option) any later version.
+dnl
+dnl This program is distributed in the hope that it will be useful, but
+dnl WITHOUT ANY WARRANTY; without even the implied warranty of
+dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+dnl General Public License for more details.
+dnl
+dnl You should have received a copy of the GNU General Public License
+dnl along with this program; if not, write to the Free Software
+dnl Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+dnl 02111-1307, USA.
+dnl
+dnl As a special exception to the GNU General Public License, if you
+dnl distribute this file as part of a program that contains a
+dnl configuration script generated by Autoconf, you may include it under
+dnl the same distribution terms that you use for the rest of that
+dnl program.
+
+dnl PKG_PREREQ(MIN-VERSION)
+dnl -----------------------
+dnl Since: 0.29
+dnl
+dnl Verify that the version of the pkg-config macros are at least
+dnl MIN-VERSION. Unlike PKG_PROG_PKG_CONFIG, which checks the user's
+dnl installed version of pkg-config, this checks the developer's version
+dnl of pkg.m4 when generating configure.
+dnl
+dnl To ensure that this macro is defined, also add:
+dnl m4_ifndef([PKG_PREREQ],
+dnl [m4_fatal([must install pkg-config 0.29 or later before running autoconf/autogen])])
+dnl
+dnl See the "Since" comment for each macro you use to see what version
+dnl of the macros you require.
+m4_defun([PKG_PREREQ],
+[m4_define([PKG_MACROS_VERSION], [0.29.2])
+m4_if(m4_version_compare(PKG_MACROS_VERSION, [$1]), -1,
+ [m4_fatal([pkg.m4 version $1 or higher is required but ]PKG_MACROS_VERSION[ found])])
+])dnl PKG_PREREQ
+
+dnl PKG_PROG_PKG_CONFIG([MIN-VERSION])
+dnl ----------------------------------
+dnl Since: 0.16
+dnl
+dnl Search for the pkg-config tool and set the PKG_CONFIG variable to
+dnl first found in the path. Checks that the version of pkg-config found
+dnl is at least MIN-VERSION. If MIN-VERSION is not specified, 0.9.0 is
+dnl used since that's the first version where most current features of
+dnl pkg-config existed.
+AC_DEFUN([PKG_PROG_PKG_CONFIG],
+[m4_pattern_forbid([^_?PKG_[A-Z_]+$])
+m4_pattern_allow([^PKG_CONFIG(_(PATH|LIBDIR|SYSROOT_DIR|ALLOW_SYSTEM_(CFLAGS|LIBS)))?$])
+m4_pattern_allow([^PKG_CONFIG_(DISABLE_UNINSTALLED|TOP_BUILD_DIR|DEBUG_SPEW)$])
+AC_ARG_VAR([PKG_CONFIG], [path to pkg-config utility])
+AC_ARG_VAR([PKG_CONFIG_PATH], [directories to add to pkg-config's search path])
+AC_ARG_VAR([PKG_CONFIG_LIBDIR], [path overriding pkg-config's built-in search path])
+
+if test "x$ac_cv_env_PKG_CONFIG_set" != "xset"; then
+ AC_PATH_TOOL([PKG_CONFIG], [pkg-config])
+fi
+if test -n "$PKG_CONFIG"; then
+ _pkg_min_version=m4_default([$1], [0.9.0])
+ AC_MSG_CHECKING([pkg-config is at least version $_pkg_min_version])
+ if $PKG_CONFIG --atleast-pkgconfig-version $_pkg_min_version; then
+ AC_MSG_RESULT([yes])
+ else
+ AC_MSG_RESULT([no])
+ PKG_CONFIG=""
+ fi
+fi[]dnl
+])dnl PKG_PROG_PKG_CONFIG
+
+dnl PKG_CHECK_EXISTS(MODULES, [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])
+dnl -------------------------------------------------------------------
+dnl Since: 0.18
+dnl
+dnl Check to see whether a particular set of modules exists. Similar to
+dnl PKG_CHECK_MODULES(), but does not set variables or print errors.
+dnl
+dnl Please remember that m4 expands AC_REQUIRE([PKG_PROG_PKG_CONFIG])
+dnl only at the first occurrence in configure.ac, so if the first place
+dnl it's called might be skipped (such as if it is within an "if", you
+dnl have to call PKG_CHECK_EXISTS manually
+AC_DEFUN([PKG_CHECK_EXISTS],
+[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl
+if test -n "$PKG_CONFIG" && \
+ AC_RUN_LOG([$PKG_CONFIG --exists --print-errors "$1"]); then
+ m4_default([$2], [:])
+m4_ifvaln([$3], [else
+ $3])dnl
+fi])
+
+dnl _PKG_CONFIG([VARIABLE], [COMMAND], [MODULES])
+dnl ---------------------------------------------
+dnl Internal wrapper calling pkg-config via PKG_CONFIG and setting
+dnl pkg_failed based on the result.
+m4_define([_PKG_CONFIG],
+[if test -n "$$1"; then
+ pkg_cv_[]$1="$$1"
+ elif test -n "$PKG_CONFIG"; then
+ PKG_CHECK_EXISTS([$3],
+ [pkg_cv_[]$1=`$PKG_CONFIG --[]$2 "$3" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes ],
+ [pkg_failed=yes])
+ else
+ pkg_failed=untried
+fi[]dnl
+])dnl _PKG_CONFIG
+
+dnl _PKG_SHORT_ERRORS_SUPPORTED
+dnl ---------------------------
+dnl Internal check to see if pkg-config supports short errors.
+AC_DEFUN([_PKG_SHORT_ERRORS_SUPPORTED],
+[AC_REQUIRE([PKG_PROG_PKG_CONFIG])
+if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
+ _pkg_short_errors_supported=yes
+else
+ _pkg_short_errors_supported=no
+fi[]dnl
+])dnl _PKG_SHORT_ERRORS_SUPPORTED
+
+
+dnl PKG_CHECK_MODULES(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND],
+dnl [ACTION-IF-NOT-FOUND])
+dnl --------------------------------------------------------------
+dnl Since: 0.4.0
+dnl
+dnl Note that if there is a possibility the first call to
+dnl PKG_CHECK_MODULES might not happen, you should be sure to include an
+dnl explicit call to PKG_PROG_PKG_CONFIG in your configure.ac
+AC_DEFUN([PKG_CHECK_MODULES],
+[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl
+AC_ARG_VAR([$1][_CFLAGS], [C compiler flags for $1, overriding pkg-config])dnl
+AC_ARG_VAR([$1][_LIBS], [linker flags for $1, overriding pkg-config])dnl
+
+pkg_failed=no
+AC_MSG_CHECKING([for $2])
+
+_PKG_CONFIG([$1][_CFLAGS], [cflags], [$2])
+_PKG_CONFIG([$1][_LIBS], [libs], [$2])
+
+m4_define([_PKG_TEXT], [Alternatively, you may set the environment variables $1[]_CFLAGS
+and $1[]_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details.])
+
+if test $pkg_failed = yes; then
+ AC_MSG_RESULT([no])
+ _PKG_SHORT_ERRORS_SUPPORTED
+ if test $_pkg_short_errors_supported = yes; then
+ $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1`
+ else
+ $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1`
+ fi
+ # Put the nasty error message in config.log where it belongs
+ echo "$$1[]_PKG_ERRORS" >&AS_MESSAGE_LOG_FD
+
+ m4_default([$4], [AC_MSG_ERROR(
+[Package requirements ($2) were not met:
+
+$$1_PKG_ERRORS
+
+Consider adjusting the PKG_CONFIG_PATH environment variable if you
+installed software in a non-standard prefix.
+
+_PKG_TEXT])[]dnl
+ ])
+elif test $pkg_failed = untried; then
+ AC_MSG_RESULT([no])
+ m4_default([$4], [AC_MSG_FAILURE(
+[The pkg-config script could not be found or is too old. Make sure it
+is in your PATH or set the PKG_CONFIG environment variable to the full
+path to pkg-config.
+
+_PKG_TEXT
+
+To get pkg-config, see <http://pkg-config.freedesktop.org/>.])[]dnl
+ ])
+else
+ $1[]_CFLAGS=$pkg_cv_[]$1[]_CFLAGS
+ $1[]_LIBS=$pkg_cv_[]$1[]_LIBS
+ AC_MSG_RESULT([yes])
+ $3
+fi[]dnl
+])dnl PKG_CHECK_MODULES
+
+
+dnl PKG_CHECK_MODULES_STATIC(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND],
+dnl [ACTION-IF-NOT-FOUND])
+dnl ---------------------------------------------------------------------
+dnl Since: 0.29
+dnl
+dnl Checks for existence of MODULES and gathers its build flags with
+dnl static libraries enabled. Sets VARIABLE-PREFIX_CFLAGS from --cflags
+dnl and VARIABLE-PREFIX_LIBS from --libs.
+dnl
+dnl Note that if there is a possibility the first call to
+dnl PKG_CHECK_MODULES_STATIC might not happen, you should be sure to
+dnl include an explicit call to PKG_PROG_PKG_CONFIG in your
+dnl configure.ac.
+AC_DEFUN([PKG_CHECK_MODULES_STATIC],
+[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl
+_save_PKG_CONFIG=$PKG_CONFIG
+PKG_CONFIG="$PKG_CONFIG --static"
+PKG_CHECK_MODULES($@)
+PKG_CONFIG=$_save_PKG_CONFIG[]dnl
+])dnl PKG_CHECK_MODULES_STATIC
+
+
+dnl PKG_INSTALLDIR([DIRECTORY])
+dnl -------------------------
+dnl Since: 0.27
+dnl
+dnl Substitutes the variable pkgconfigdir as the location where a module
+dnl should install pkg-config .pc files. By default the directory is
+dnl $libdir/pkgconfig, but the default can be changed by passing
+dnl DIRECTORY. The user can override through the --with-pkgconfigdir
+dnl parameter.
+AC_DEFUN([PKG_INSTALLDIR],
+[m4_pushdef([pkg_default], [m4_default([$1], ['${libdir}/pkgconfig'])])
+m4_pushdef([pkg_description],
+ [pkg-config installation directory @<:@]pkg_default[@:>@])
+AC_ARG_WITH([pkgconfigdir],
+ [AS_HELP_STRING([--with-pkgconfigdir], pkg_description)],,
+ [with_pkgconfigdir=]pkg_default)
+AC_SUBST([pkgconfigdir], [$with_pkgconfigdir])
+m4_popdef([pkg_default])
+m4_popdef([pkg_description])
+])dnl PKG_INSTALLDIR
+
+
+dnl PKG_NOARCH_INSTALLDIR([DIRECTORY])
+dnl --------------------------------
+dnl Since: 0.27
+dnl
+dnl Substitutes the variable noarch_pkgconfigdir as the location where a
+dnl module should install arch-independent pkg-config .pc files. By
+dnl default the directory is $datadir/pkgconfig, but the default can be
+dnl changed by passing DIRECTORY. The user can override through the
+dnl --with-noarch-pkgconfigdir parameter.
+AC_DEFUN([PKG_NOARCH_INSTALLDIR],
+[m4_pushdef([pkg_default], [m4_default([$1], ['${datadir}/pkgconfig'])])
+m4_pushdef([pkg_description],
+ [pkg-config arch-independent installation directory @<:@]pkg_default[@:>@])
+AC_ARG_WITH([noarch-pkgconfigdir],
+ [AS_HELP_STRING([--with-noarch-pkgconfigdir], pkg_description)],,
+ [with_noarch_pkgconfigdir=]pkg_default)
+AC_SUBST([noarch_pkgconfigdir], [$with_noarch_pkgconfigdir])
+m4_popdef([pkg_default])
+m4_popdef([pkg_description])
+])dnl PKG_NOARCH_INSTALLDIR
+
+
+dnl PKG_CHECK_VAR(VARIABLE, MODULE, CONFIG-VARIABLE,
+dnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])
+dnl -------------------------------------------
+dnl Since: 0.28
+dnl
+dnl Retrieves the value of the pkg-config variable for the given module.
+AC_DEFUN([PKG_CHECK_VAR],
+[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl
+AC_ARG_VAR([$1], [value of $3 for $2, overriding pkg-config])dnl
+
+_PKG_CONFIG([$1], [variable="][$3]["], [$2])
+AS_VAR_COPY([$1], [pkg_cv_][$1])
+
+AS_VAR_IF([$1], [""], [$5], [$4])dnl
+])dnl PKG_CHECK_VAR
+
+dnl PKG_WITH_MODULES(VARIABLE-PREFIX, MODULES,
+dnl [ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND],
+dnl [DESCRIPTION], [DEFAULT])
+dnl ------------------------------------------
+dnl
+dnl Prepare a "--with-" configure option using the lowercase
+dnl [VARIABLE-PREFIX] name, merging the behaviour of AC_ARG_WITH and
+dnl PKG_CHECK_MODULES in a single macro.
+AC_DEFUN([PKG_WITH_MODULES],
+[
+m4_pushdef([with_arg], m4_tolower([$1]))
+
+m4_pushdef([description],
+ [m4_default([$5], [build with ]with_arg[ support])])
+
+m4_pushdef([def_arg], [m4_default([$6], [auto])])
+m4_pushdef([def_action_if_found], [AS_TR_SH([with_]with_arg)=yes])
+m4_pushdef([def_action_if_not_found], [AS_TR_SH([with_]with_arg)=no])
+
+m4_case(def_arg,
+ [yes],[m4_pushdef([with_without], [--without-]with_arg)],
+ [m4_pushdef([with_without],[--with-]with_arg)])
+
+AC_ARG_WITH(with_arg,
+ AS_HELP_STRING(with_without, description[ @<:@default=]def_arg[@:>@]),,
+ [AS_TR_SH([with_]with_arg)=def_arg])
+
+AS_CASE([$AS_TR_SH([with_]with_arg)],
+ [yes],[PKG_CHECK_MODULES([$1],[$2],$3,$4)],
+ [auto],[PKG_CHECK_MODULES([$1],[$2],
+ [m4_n([def_action_if_found]) $3],
+ [m4_n([def_action_if_not_found]) $4])])
+
+m4_popdef([with_arg])
+m4_popdef([description])
+m4_popdef([def_arg])
+
+])dnl PKG_WITH_MODULES
+
+dnl PKG_HAVE_WITH_MODULES(VARIABLE-PREFIX, MODULES,
+dnl [DESCRIPTION], [DEFAULT])
+dnl -----------------------------------------------
+dnl
+dnl Convenience macro to trigger AM_CONDITIONAL after PKG_WITH_MODULES
+dnl check._[VARIABLE-PREFIX] is exported as make variable.
+AC_DEFUN([PKG_HAVE_WITH_MODULES],
+[
+PKG_WITH_MODULES([$1],[$2],,,[$3],[$4])
+
+AM_CONDITIONAL([HAVE_][$1],
+ [test "$AS_TR_SH([with_]m4_tolower([$1]))" = "yes"])
+])dnl PKG_HAVE_WITH_MODULES
+
+dnl PKG_HAVE_DEFINE_WITH_MODULES(VARIABLE-PREFIX, MODULES,
+dnl [DESCRIPTION], [DEFAULT])
+dnl ------------------------------------------------------
+dnl
+dnl Convenience macro to run AM_CONDITIONAL and AC_DEFINE after
+dnl PKG_WITH_MODULES check. HAVE_[VARIABLE-PREFIX] is exported as make
+dnl and preprocessor variable.
+AC_DEFUN([PKG_HAVE_DEFINE_WITH_MODULES],
+[
+PKG_HAVE_WITH_MODULES([$1],[$2],[$3],[$4])
+
+AS_IF([test "$AS_TR_SH([with_]m4_tolower([$1]))" = "yes"],
+ [AC_DEFINE([HAVE_][$1], 1, [Enable ]m4_tolower([$1])[ support])])
+])dnl PKG_HAVE_DEFINE_WITH_MODULES
+
+# Copyright (C) 2002-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_AUTOMAKE_VERSION(VERSION)
+# ----------------------------
+# Automake X.Y traces this macro to ensure aclocal.m4 has been
+# generated from the m4 files accompanying Automake X.Y.
+# (This private macro should not be called outside this file.)
+AC_DEFUN([AM_AUTOMAKE_VERSION],
+[am__api_version='1.16'
+dnl Some users find AM_AUTOMAKE_VERSION and mistake it for a way to
+dnl require some minimum version. Point them to the right macro.
+m4_if([$1], [1.16.5], [],
+ [AC_FATAL([Do not call $0, use AM_INIT_AUTOMAKE([$1]).])])dnl
+])
+
+# _AM_AUTOCONF_VERSION(VERSION)
+# -----------------------------
+# aclocal traces this macro to find the Autoconf version.
+# This is a private macro too. Using m4_define simplifies
+# the logic in aclocal, which can simply ignore this definition.
+m4_define([_AM_AUTOCONF_VERSION], [])
+
+# AM_SET_CURRENT_AUTOMAKE_VERSION
+# -------------------------------
+# Call AM_AUTOMAKE_VERSION and AM_AUTOMAKE_VERSION so they can be traced.
+# This function is AC_REQUIREd by AM_INIT_AUTOMAKE.
+AC_DEFUN([AM_SET_CURRENT_AUTOMAKE_VERSION],
+[AM_AUTOMAKE_VERSION([1.16.5])dnl
+m4_ifndef([AC_AUTOCONF_VERSION],
+ [m4_copy([m4_PACKAGE_VERSION], [AC_AUTOCONF_VERSION])])dnl
+_AM_AUTOCONF_VERSION(m4_defn([AC_AUTOCONF_VERSION]))])
+
+# AM_AUX_DIR_EXPAND -*- Autoconf -*-
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# For projects using AC_CONFIG_AUX_DIR([foo]), Autoconf sets
+# $ac_aux_dir to '$srcdir/foo'. In other projects, it is set to
+# '$srcdir', '$srcdir/..', or '$srcdir/../..'.
+#
+# Of course, Automake must honor this variable whenever it calls a
+# tool from the auxiliary directory. The problem is that $srcdir (and
+# therefore $ac_aux_dir as well) can be either absolute or relative,
+# depending on how configure is run. This is pretty annoying, since
+# it makes $ac_aux_dir quite unusable in subdirectories: in the top
+# source directory, any form will work fine, but in subdirectories a
+# relative path needs to be adjusted first.
+#
+# $ac_aux_dir/missing
+# fails when called from a subdirectory if $ac_aux_dir is relative
+# $top_srcdir/$ac_aux_dir/missing
+# fails if $ac_aux_dir is absolute,
+# fails when called from a subdirectory in a VPATH build with
+# a relative $ac_aux_dir
+#
+# The reason of the latter failure is that $top_srcdir and $ac_aux_dir
+# are both prefixed by $srcdir. In an in-source build this is usually
+# harmless because $srcdir is '.', but things will broke when you
+# start a VPATH build or use an absolute $srcdir.
+#
+# So we could use something similar to $top_srcdir/$ac_aux_dir/missing,
+# iff we strip the leading $srcdir from $ac_aux_dir. That would be:
+# am_aux_dir='\$(top_srcdir)/'`expr "$ac_aux_dir" : "$srcdir//*\(.*\)"`
+# and then we would define $MISSING as
+# MISSING="\${SHELL} $am_aux_dir/missing"
+# This will work as long as MISSING is not called from configure, because
+# unfortunately $(top_srcdir) has no meaning in configure.
+# However there are other variables, like CC, which are often used in
+# configure, and could therefore not use this "fixed" $ac_aux_dir.
+#
+# Another solution, used here, is to always expand $ac_aux_dir to an
+# absolute PATH. The drawback is that using absolute paths prevent a
+# configured tree to be moved without reconfiguration.
+
+AC_DEFUN([AM_AUX_DIR_EXPAND],
+[AC_REQUIRE([AC_CONFIG_AUX_DIR_DEFAULT])dnl
+# Expand $ac_aux_dir to an absolute path.
+am_aux_dir=`cd "$ac_aux_dir" && pwd`
+])
+
+# AM_CONDITIONAL -*- Autoconf -*-
+
+# Copyright (C) 1997-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_CONDITIONAL(NAME, SHELL-CONDITION)
+# -------------------------------------
+# Define a conditional.
+AC_DEFUN([AM_CONDITIONAL],
+[AC_PREREQ([2.52])dnl
+ m4_if([$1], [TRUE], [AC_FATAL([$0: invalid condition: $1])],
+ [$1], [FALSE], [AC_FATAL([$0: invalid condition: $1])])dnl
+AC_SUBST([$1_TRUE])dnl
+AC_SUBST([$1_FALSE])dnl
+_AM_SUBST_NOTMAKE([$1_TRUE])dnl
+_AM_SUBST_NOTMAKE([$1_FALSE])dnl
+m4_define([_AM_COND_VALUE_$1], [$2])dnl
+if $2; then
+ $1_TRUE=
+ $1_FALSE='#'
+else
+ $1_TRUE='#'
+ $1_FALSE=
+fi
+AC_CONFIG_COMMANDS_PRE(
+[if test -z "${$1_TRUE}" && test -z "${$1_FALSE}"; then
+ AC_MSG_ERROR([[conditional "$1" was never defined.
+Usually this means the macro was only invoked conditionally.]])
+fi])])
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+
+# There are a few dirty hacks below to avoid letting 'AC_PROG_CC' be
+# written in clear, in which case automake, when reading aclocal.m4,
+# will think it sees a *use*, and therefore will trigger all it's
+# C support machinery. Also note that it means that autoscan, seeing
+# CC etc. in the Makefile, will ask for an AC_PROG_CC use...
+
+
+# _AM_DEPENDENCIES(NAME)
+# ----------------------
+# See how the compiler implements dependency checking.
+# NAME is "CC", "CXX", "OBJC", "OBJCXX", "UPC", or "GJC".
+# We try a few techniques and use that to set a single cache variable.
+#
+# We don't AC_REQUIRE the corresponding AC_PROG_CC since the latter was
+# modified to invoke _AM_DEPENDENCIES(CC); we would have a circular
+# dependency, and given that the user is not expected to run this macro,
+# just rely on AC_PROG_CC.
+AC_DEFUN([_AM_DEPENDENCIES],
+[AC_REQUIRE([AM_SET_DEPDIR])dnl
+AC_REQUIRE([AM_OUTPUT_DEPENDENCY_COMMANDS])dnl
+AC_REQUIRE([AM_MAKE_INCLUDE])dnl
+AC_REQUIRE([AM_DEP_TRACK])dnl
+
+m4_if([$1], [CC], [depcc="$CC" am_compiler_list=],
+ [$1], [CXX], [depcc="$CXX" am_compiler_list=],
+ [$1], [OBJC], [depcc="$OBJC" am_compiler_list='gcc3 gcc'],
+ [$1], [OBJCXX], [depcc="$OBJCXX" am_compiler_list='gcc3 gcc'],
+ [$1], [UPC], [depcc="$UPC" am_compiler_list=],
+ [$1], [GCJ], [depcc="$GCJ" am_compiler_list='gcc3 gcc'],
+ [depcc="$$1" am_compiler_list=])
+
+AC_CACHE_CHECK([dependency style of $depcc],
+ [am_cv_$1_dependencies_compiler_type],
+[if test -z "$AMDEP_TRUE" && test -f "$am_depcomp"; then
+ # We make a subdir and do the tests there. Otherwise we can end up
+ # making bogus files that we don't know about and never remove. For
+ # instance it was reported that on HP-UX the gcc test will end up
+ # making a dummy file named 'D' -- because '-MD' means "put the output
+ # in D".
+ rm -rf conftest.dir
+ mkdir conftest.dir
+ # Copy depcomp to subdir because otherwise we won't find it if we're
+ # using a relative directory.
+ cp "$am_depcomp" conftest.dir
+ cd conftest.dir
+ # We will build objects and dependencies in a subdirectory because
+ # it helps to detect inapplicable dependency modes. For instance
+ # both Tru64's cc and ICC support -MD to output dependencies as a
+ # side effect of compilation, but ICC will put the dependencies in
+ # the current directory while Tru64 will put them in the object
+ # directory.
+ mkdir sub
+
+ am_cv_$1_dependencies_compiler_type=none
+ if test "$am_compiler_list" = ""; then
+ am_compiler_list=`sed -n ['s/^#*\([a-zA-Z0-9]*\))$/\1/p'] < ./depcomp`
+ fi
+ am__universal=false
+ m4_case([$1], [CC],
+ [case " $depcc " in #(
+ *\ -arch\ *\ -arch\ *) am__universal=true ;;
+ esac],
+ [CXX],
+ [case " $depcc " in #(
+ *\ -arch\ *\ -arch\ *) am__universal=true ;;
+ esac])
+
+ for depmode in $am_compiler_list; do
+ # Setup a source with many dependencies, because some compilers
+ # like to wrap large dependency lists on column 80 (with \), and
+ # we should not choose a depcomp mode which is confused by this.
+ #
+ # We need to recreate these files for each test, as the compiler may
+ # overwrite some of them when testing with obscure command lines.
+ # This happens at least with the AIX C compiler.
+ : > sub/conftest.c
+ for i in 1 2 3 4 5 6; do
+ echo '#include "conftst'$i'.h"' >> sub/conftest.c
+ # Using ": > sub/conftst$i.h" creates only sub/conftst1.h with
+ # Solaris 10 /bin/sh.
+ echo '/* dummy */' > sub/conftst$i.h
+ done
+ echo "${am__include} ${am__quote}sub/conftest.Po${am__quote}" > confmf
+
+ # We check with '-c' and '-o' for the sake of the "dashmstdout"
+ # mode. It turns out that the SunPro C++ compiler does not properly
+ # handle '-M -o', and we need to detect this. Also, some Intel
+ # versions had trouble with output in subdirs.
+ am__obj=sub/conftest.${OBJEXT-o}
+ am__minus_obj="-o $am__obj"
+ case $depmode in
+ gcc)
+ # This depmode causes a compiler race in universal mode.
+ test "$am__universal" = false || continue
+ ;;
+ nosideeffect)
+ # After this tag, mechanisms are not by side-effect, so they'll
+ # only be used when explicitly requested.
+ if test "x$enable_dependency_tracking" = xyes; then
+ continue
+ else
+ break
+ fi
+ ;;
+ msvc7 | msvc7msys | msvisualcpp | msvcmsys)
+ # This compiler won't grok '-c -o', but also, the minuso test has
+ # not run yet. These depmodes are late enough in the game, and
+ # so weak that their functioning should not be impacted.
+ am__obj=conftest.${OBJEXT-o}
+ am__minus_obj=
+ ;;
+ none) break ;;
+ esac
+ if depmode=$depmode \
+ source=sub/conftest.c object=$am__obj \
+ depfile=sub/conftest.Po tmpdepfile=sub/conftest.TPo \
+ $SHELL ./depcomp $depcc -c $am__minus_obj sub/conftest.c \
+ >/dev/null 2>conftest.err &&
+ grep sub/conftst1.h sub/conftest.Po > /dev/null 2>&1 &&
+ grep sub/conftst6.h sub/conftest.Po > /dev/null 2>&1 &&
+ grep $am__obj sub/conftest.Po > /dev/null 2>&1 &&
+ ${MAKE-make} -s -f confmf > /dev/null 2>&1; then
+ # icc doesn't choke on unknown options, it will just issue warnings
+ # or remarks (even with -Werror). So we grep stderr for any message
+ # that says an option was ignored or not supported.
+ # When given -MP, icc 7.0 and 7.1 complain thusly:
+ # icc: Command line warning: ignoring option '-M'; no argument required
+ # The diagnosis changed in icc 8.0:
+ # icc: Command line remark: option '-MP' not supported
+ if (grep 'ignoring option' conftest.err ||
+ grep 'not supported' conftest.err) >/dev/null 2>&1; then :; else
+ am_cv_$1_dependencies_compiler_type=$depmode
+ break
+ fi
+ fi
+ done
+
+ cd ..
+ rm -rf conftest.dir
+else
+ am_cv_$1_dependencies_compiler_type=none
+fi
+])
+AC_SUBST([$1DEPMODE], [depmode=$am_cv_$1_dependencies_compiler_type])
+AM_CONDITIONAL([am__fastdep$1], [
+ test "x$enable_dependency_tracking" != xno \
+ && test "$am_cv_$1_dependencies_compiler_type" = gcc3])
+])
+
+
+# AM_SET_DEPDIR
+# -------------
+# Choose a directory name for dependency files.
+# This macro is AC_REQUIREd in _AM_DEPENDENCIES.
+AC_DEFUN([AM_SET_DEPDIR],
+[AC_REQUIRE([AM_SET_LEADING_DOT])dnl
+AC_SUBST([DEPDIR], ["${am__leading_dot}deps"])dnl
+])
+
+
+# AM_DEP_TRACK
+# ------------
+AC_DEFUN([AM_DEP_TRACK],
+[AC_ARG_ENABLE([dependency-tracking], [dnl
+AS_HELP_STRING(
+ [--enable-dependency-tracking],
+ [do not reject slow dependency extractors])
+AS_HELP_STRING(
+ [--disable-dependency-tracking],
+ [speeds up one-time build])])
+if test "x$enable_dependency_tracking" != xno; then
+ am_depcomp="$ac_aux_dir/depcomp"
+ AMDEPBACKSLASH='\'
+ am__nodep='_no'
+fi
+AM_CONDITIONAL([AMDEP], [test "x$enable_dependency_tracking" != xno])
+AC_SUBST([AMDEPBACKSLASH])dnl
+_AM_SUBST_NOTMAKE([AMDEPBACKSLASH])dnl
+AC_SUBST([am__nodep])dnl
+_AM_SUBST_NOTMAKE([am__nodep])dnl
+])
+
+# Generate code to set up dependency tracking. -*- Autoconf -*-
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# _AM_OUTPUT_DEPENDENCY_COMMANDS
+# ------------------------------
+AC_DEFUN([_AM_OUTPUT_DEPENDENCY_COMMANDS],
+[{
+ # Older Autoconf quotes --file arguments for eval, but not when files
+ # are listed without --file. Let's play safe and only enable the eval
+ # if we detect the quoting.
+ # TODO: see whether this extra hack can be removed once we start
+ # requiring Autoconf 2.70 or later.
+ AS_CASE([$CONFIG_FILES],
+ [*\'*], [eval set x "$CONFIG_FILES"],
+ [*], [set x $CONFIG_FILES])
+ shift
+ # Used to flag and report bootstrapping failures.
+ am_rc=0
+ for am_mf
+ do
+ # Strip MF so we end up with the name of the file.
+ am_mf=`AS_ECHO(["$am_mf"]) | sed -e 's/:.*$//'`
+ # Check whether this is an Automake generated Makefile which includes
+ # dependency-tracking related rules and includes.
+ # Grep'ing the whole file directly is not great: AIX grep has a line
+ # limit of 2048, but all sed's we know have understand at least 4000.
+ sed -n 's,^am--depfiles:.*,X,p' "$am_mf" | grep X >/dev/null 2>&1 \
+ || continue
+ am_dirpart=`AS_DIRNAME(["$am_mf"])`
+ am_filepart=`AS_BASENAME(["$am_mf"])`
+ AM_RUN_LOG([cd "$am_dirpart" \
+ && sed -e '/# am--include-marker/d' "$am_filepart" \
+ | $MAKE -f - am--depfiles]) || am_rc=$?
+ done
+ if test $am_rc -ne 0; then
+ AC_MSG_FAILURE([Something went wrong bootstrapping makefile fragments
+ for automatic dependency tracking. If GNU make was not used, consider
+ re-running the configure script with MAKE="gmake" (or whatever is
+ necessary). You can also try re-running configure with the
+ '--disable-dependency-tracking' option to at least be able to build
+ the package (albeit without support for automatic dependency tracking).])
+ fi
+ AS_UNSET([am_dirpart])
+ AS_UNSET([am_filepart])
+ AS_UNSET([am_mf])
+ AS_UNSET([am_rc])
+ rm -f conftest-deps.mk
+}
+])# _AM_OUTPUT_DEPENDENCY_COMMANDS
+
+
+# AM_OUTPUT_DEPENDENCY_COMMANDS
+# -----------------------------
+# This macro should only be invoked once -- use via AC_REQUIRE.
+#
+# This code is only required when automatic dependency tracking is enabled.
+# This creates each '.Po' and '.Plo' makefile fragment that we'll need in
+# order to bootstrap the dependency handling code.
+AC_DEFUN([AM_OUTPUT_DEPENDENCY_COMMANDS],
+[AC_CONFIG_COMMANDS([depfiles],
+ [test x"$AMDEP_TRUE" != x"" || _AM_OUTPUT_DEPENDENCY_COMMANDS],
+ [AMDEP_TRUE="$AMDEP_TRUE" MAKE="${MAKE-make}"])])
+
+# Do all the work for Automake. -*- Autoconf -*-
+
+# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This macro actually does too much. Some checks are only needed if
+# your package does certain things. But this isn't really a big deal.
+
+dnl Redefine AC_PROG_CC to automatically invoke _AM_PROG_CC_C_O.
+m4_define([AC_PROG_CC],
+m4_defn([AC_PROG_CC])
+[_AM_PROG_CC_C_O
+])
+
+# AM_INIT_AUTOMAKE(PACKAGE, VERSION, [NO-DEFINE])
+# AM_INIT_AUTOMAKE([OPTIONS])
+# -----------------------------------------------
+# The call with PACKAGE and VERSION arguments is the old style
+# call (pre autoconf-2.50), which is being phased out. PACKAGE
+# and VERSION should now be passed to AC_INIT and removed from
+# the call to AM_INIT_AUTOMAKE.
+# We support both call styles for the transition. After
+# the next Automake release, Autoconf can make the AC_INIT
+# arguments mandatory, and then we can depend on a new Autoconf
+# release and drop the old call support.
+AC_DEFUN([AM_INIT_AUTOMAKE],
+[AC_PREREQ([2.65])dnl
+m4_ifdef([_$0_ALREADY_INIT],
+ [m4_fatal([$0 expanded multiple times
+]m4_defn([_$0_ALREADY_INIT]))],
+ [m4_define([_$0_ALREADY_INIT], m4_expansion_stack)])dnl
+dnl Autoconf wants to disallow AM_ names. We explicitly allow
+dnl the ones we care about.
+m4_pattern_allow([^AM_[A-Z]+FLAGS$])dnl
+AC_REQUIRE([AM_SET_CURRENT_AUTOMAKE_VERSION])dnl
+AC_REQUIRE([AC_PROG_INSTALL])dnl
+if test "`cd $srcdir && pwd`" != "`pwd`"; then
+ # Use -I$(srcdir) only when $(srcdir) != ., so that make's output
+ # is not polluted with repeated "-I."
+ AC_SUBST([am__isrc], [' -I$(srcdir)'])_AM_SUBST_NOTMAKE([am__isrc])dnl
+ # test to see if srcdir already configured
+ if test -f $srcdir/config.status; then
+ AC_MSG_ERROR([source directory already configured; run "make distclean" there first])
+ fi
+fi
+
+# test whether we have cygpath
+if test -z "$CYGPATH_W"; then
+ if (cygpath --version) >/dev/null 2>/dev/null; then
+ CYGPATH_W='cygpath -w'
+ else
+ CYGPATH_W=echo
+ fi
+fi
+AC_SUBST([CYGPATH_W])
+
+# Define the identity of the package.
+dnl Distinguish between old-style and new-style calls.
+m4_ifval([$2],
+[AC_DIAGNOSE([obsolete],
+ [$0: two- and three-arguments forms are deprecated.])
+m4_ifval([$3], [_AM_SET_OPTION([no-define])])dnl
+ AC_SUBST([PACKAGE], [$1])dnl
+ AC_SUBST([VERSION], [$2])],
+[_AM_SET_OPTIONS([$1])dnl
+dnl Diagnose old-style AC_INIT with new-style AM_AUTOMAKE_INIT.
+m4_if(
+ m4_ifset([AC_PACKAGE_NAME], [ok]):m4_ifset([AC_PACKAGE_VERSION], [ok]),
+ [ok:ok],,
+ [m4_fatal([AC_INIT should be called with package and version arguments])])dnl
+ AC_SUBST([PACKAGE], ['AC_PACKAGE_TARNAME'])dnl
+ AC_SUBST([VERSION], ['AC_PACKAGE_VERSION'])])dnl
+
+_AM_IF_OPTION([no-define],,
+[AC_DEFINE_UNQUOTED([PACKAGE], ["$PACKAGE"], [Name of package])
+ AC_DEFINE_UNQUOTED([VERSION], ["$VERSION"], [Version number of package])])dnl
+
+# Some tools Automake needs.
+AC_REQUIRE([AM_SANITY_CHECK])dnl
+AC_REQUIRE([AC_ARG_PROGRAM])dnl
+AM_MISSING_PROG([ACLOCAL], [aclocal-${am__api_version}])
+AM_MISSING_PROG([AUTOCONF], [autoconf])
+AM_MISSING_PROG([AUTOMAKE], [automake-${am__api_version}])
+AM_MISSING_PROG([AUTOHEADER], [autoheader])
+AM_MISSING_PROG([MAKEINFO], [makeinfo])
+AC_REQUIRE([AM_PROG_INSTALL_SH])dnl
+AC_REQUIRE([AM_PROG_INSTALL_STRIP])dnl
+AC_REQUIRE([AC_PROG_MKDIR_P])dnl
+# For better backward compatibility. To be removed once Automake 1.9.x
+# dies out for good. For more background, see:
+# <https://lists.gnu.org/archive/html/automake/2012-07/msg00001.html>
+# <https://lists.gnu.org/archive/html/automake/2012-07/msg00014.html>
+AC_SUBST([mkdir_p], ['$(MKDIR_P)'])
+# We need awk for the "check" target (and possibly the TAP driver). The
+# system "awk" is bad on some platforms.
+AC_REQUIRE([AC_PROG_AWK])dnl
+AC_REQUIRE([AC_PROG_MAKE_SET])dnl
+AC_REQUIRE([AM_SET_LEADING_DOT])dnl
+_AM_IF_OPTION([tar-ustar], [_AM_PROG_TAR([ustar])],
+ [_AM_IF_OPTION([tar-pax], [_AM_PROG_TAR([pax])],
+ [_AM_PROG_TAR([v7])])])
+_AM_IF_OPTION([no-dependencies],,
+[AC_PROVIDE_IFELSE([AC_PROG_CC],
+ [_AM_DEPENDENCIES([CC])],
+ [m4_define([AC_PROG_CC],
+ m4_defn([AC_PROG_CC])[_AM_DEPENDENCIES([CC])])])dnl
+AC_PROVIDE_IFELSE([AC_PROG_CXX],
+ [_AM_DEPENDENCIES([CXX])],
+ [m4_define([AC_PROG_CXX],
+ m4_defn([AC_PROG_CXX])[_AM_DEPENDENCIES([CXX])])])dnl
+AC_PROVIDE_IFELSE([AC_PROG_OBJC],
+ [_AM_DEPENDENCIES([OBJC])],
+ [m4_define([AC_PROG_OBJC],
+ m4_defn([AC_PROG_OBJC])[_AM_DEPENDENCIES([OBJC])])])dnl
+AC_PROVIDE_IFELSE([AC_PROG_OBJCXX],
+ [_AM_DEPENDENCIES([OBJCXX])],
+ [m4_define([AC_PROG_OBJCXX],
+ m4_defn([AC_PROG_OBJCXX])[_AM_DEPENDENCIES([OBJCXX])])])dnl
+])
+# Variables for tags utilities; see am/tags.am
+if test -z "$CTAGS"; then
+ CTAGS=ctags
+fi
+AC_SUBST([CTAGS])
+if test -z "$ETAGS"; then
+ ETAGS=etags
+fi
+AC_SUBST([ETAGS])
+if test -z "$CSCOPE"; then
+ CSCOPE=cscope
+fi
+AC_SUBST([CSCOPE])
+
+AC_REQUIRE([AM_SILENT_RULES])dnl
+dnl The testsuite driver may need to know about EXEEXT, so add the
+dnl 'am__EXEEXT' conditional if _AM_COMPILER_EXEEXT was seen. This
+dnl macro is hooked onto _AC_COMPILER_EXEEXT early, see below.
+AC_CONFIG_COMMANDS_PRE(dnl
+[m4_provide_if([_AM_COMPILER_EXEEXT],
+ [AM_CONDITIONAL([am__EXEEXT], [test -n "$EXEEXT"])])])dnl
+
+# POSIX will say in a future version that running "rm -f" with no argument
+# is OK; and we want to be able to make that assumption in our Makefile
+# recipes. So use an aggressive probe to check that the usage we want is
+# actually supported "in the wild" to an acceptable degree.
+# See automake bug#10828.
+# To make any issue more visible, cause the running configure to be aborted
+# by default if the 'rm' program in use doesn't match our expectations; the
+# user can still override this though.
+if rm -f && rm -fr && rm -rf; then : OK; else
+ cat >&2 <<'END'
+Oops!
+
+Your 'rm' program seems unable to run without file operands specified
+on the command line, even when the '-f' option is present. This is contrary
+to the behaviour of most rm programs out there, and not conforming with
+the upcoming POSIX standard: <http://austingroupbugs.net/view.php?id=542>
+
+Please tell bug-automake@gnu.org about your system, including the value
+of your $PATH and any error possibly output before this message. This
+can help us improve future automake versions.
+
+END
+ if test x"$ACCEPT_INFERIOR_RM_PROGRAM" = x"yes"; then
+ echo 'Configuration will proceed anyway, since you have set the' >&2
+ echo 'ACCEPT_INFERIOR_RM_PROGRAM variable to "yes"' >&2
+ echo >&2
+ else
+ cat >&2 <<'END'
+Aborting the configuration process, to ensure you take notice of the issue.
+
+You can download and install GNU coreutils to get an 'rm' implementation
+that behaves properly: <https://www.gnu.org/software/coreutils/>.
+
+If you want to complete the configuration process using your problematic
+'rm' anyway, export the environment variable ACCEPT_INFERIOR_RM_PROGRAM
+to "yes", and re-run configure.
+
+END
+ AC_MSG_ERROR([Your 'rm' program is bad, sorry.])
+ fi
+fi
+dnl The trailing newline in this macro's definition is deliberate, for
+dnl backward compatibility and to allow trailing 'dnl'-style comments
+dnl after the AM_INIT_AUTOMAKE invocation. See automake bug#16841.
+])
+
+dnl Hook into '_AC_COMPILER_EXEEXT' early to learn its expansion. Do not
+dnl add the conditional right here, as _AC_COMPILER_EXEEXT may be further
+dnl mangled by Autoconf and run in a shell conditional statement.
+m4_define([_AC_COMPILER_EXEEXT],
+m4_defn([_AC_COMPILER_EXEEXT])[m4_provide([_AM_COMPILER_EXEEXT])])
+
+# When config.status generates a header, we must update the stamp-h file.
+# This file resides in the same directory as the config header
+# that is generated. The stamp files are numbered to have different names.
+
+# Autoconf calls _AC_AM_CONFIG_HEADER_HOOK (when defined) in the
+# loop where config.status creates the headers, so we can generate
+# our stamp files there.
+AC_DEFUN([_AC_AM_CONFIG_HEADER_HOOK],
+[# Compute $1's index in $config_headers.
+_am_arg=$1
+_am_stamp_count=1
+for _am_header in $config_headers :; do
+ case $_am_header in
+ $_am_arg | $_am_arg:* )
+ break ;;
+ * )
+ _am_stamp_count=`expr $_am_stamp_count + 1` ;;
+ esac
+done
+echo "timestamp for $_am_arg" >`AS_DIRNAME(["$_am_arg"])`/stamp-h[]$_am_stamp_count])
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_PROG_INSTALL_SH
+# ------------------
+# Define $install_sh.
+AC_DEFUN([AM_PROG_INSTALL_SH],
+[AC_REQUIRE([AM_AUX_DIR_EXPAND])dnl
+if test x"${install_sh+set}" != xset; then
+ case $am_aux_dir in
+ *\ * | *\ *)
+ install_sh="\${SHELL} '$am_aux_dir/install-sh'" ;;
+ *)
+ install_sh="\${SHELL} $am_aux_dir/install-sh"
+ esac
+fi
+AC_SUBST([install_sh])])
+
+# Copyright (C) 2003-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# Check whether the underlying file-system supports filenames
+# with a leading dot. For instance MS-DOS doesn't.
+AC_DEFUN([AM_SET_LEADING_DOT],
+[rm -rf .tst 2>/dev/null
+mkdir .tst 2>/dev/null
+if test -d .tst; then
+ am__leading_dot=.
+else
+ am__leading_dot=_
+fi
+rmdir .tst 2>/dev/null
+AC_SUBST([am__leading_dot])])
+
+# Check to see how 'make' treats includes. -*- Autoconf -*-
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_MAKE_INCLUDE()
+# -----------------
+# Check whether make has an 'include' directive that can support all
+# the idioms we need for our automatic dependency tracking code.
+AC_DEFUN([AM_MAKE_INCLUDE],
+[AC_MSG_CHECKING([whether ${MAKE-make} supports the include directive])
+cat > confinc.mk << 'END'
+am__doit:
+ @echo this is the am__doit target >confinc.out
+.PHONY: am__doit
+END
+am__include="#"
+am__quote=
+# BSD make does it like this.
+echo '.include "confinc.mk" # ignored' > confmf.BSD
+# Other make implementations (GNU, Solaris 10, AIX) do it like this.
+echo 'include confinc.mk # ignored' > confmf.GNU
+_am_result=no
+for s in GNU BSD; do
+ AM_RUN_LOG([${MAKE-make} -f confmf.$s && cat confinc.out])
+ AS_CASE([$?:`cat confinc.out 2>/dev/null`],
+ ['0:this is the am__doit target'],
+ [AS_CASE([$s],
+ [BSD], [am__include='.include' am__quote='"'],
+ [am__include='include' am__quote=''])])
+ if test "$am__include" != "#"; then
+ _am_result="yes ($s style)"
+ break
+ fi
+done
+rm -f confinc.* confmf.*
+AC_MSG_RESULT([${_am_result}])
+AC_SUBST([am__include])])
+AC_SUBST([am__quote])])
+
+# Fake the existence of programs that GNU maintainers use. -*- Autoconf -*-
+
+# Copyright (C) 1997-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_MISSING_PROG(NAME, PROGRAM)
+# ------------------------------
+AC_DEFUN([AM_MISSING_PROG],
+[AC_REQUIRE([AM_MISSING_HAS_RUN])
+$1=${$1-"${am_missing_run}$2"}
+AC_SUBST($1)])
+
+# AM_MISSING_HAS_RUN
+# ------------------
+# Define MISSING if not defined so far and test if it is modern enough.
+# If it is, set am_missing_run to use it, otherwise, to nothing.
+AC_DEFUN([AM_MISSING_HAS_RUN],
+[AC_REQUIRE([AM_AUX_DIR_EXPAND])dnl
+AC_REQUIRE_AUX_FILE([missing])dnl
+if test x"${MISSING+set}" != xset; then
+ MISSING="\${SHELL} '$am_aux_dir/missing'"
+fi
+# Use eval to expand $SHELL
+if eval "$MISSING --is-lightweight"; then
+ am_missing_run="$MISSING "
+else
+ am_missing_run=
+ AC_MSG_WARN(['missing' script is too old or missing])
+fi
+])
+
+# Helper functions for option handling. -*- Autoconf -*-
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# _AM_MANGLE_OPTION(NAME)
+# -----------------------
+AC_DEFUN([_AM_MANGLE_OPTION],
+[[_AM_OPTION_]m4_bpatsubst($1, [[^a-zA-Z0-9_]], [_])])
+
+# _AM_SET_OPTION(NAME)
+# --------------------
+# Set option NAME. Presently that only means defining a flag for this option.
+AC_DEFUN([_AM_SET_OPTION],
+[m4_define(_AM_MANGLE_OPTION([$1]), [1])])
+
+# _AM_SET_OPTIONS(OPTIONS)
+# ------------------------
+# OPTIONS is a space-separated list of Automake options.
+AC_DEFUN([_AM_SET_OPTIONS],
+[m4_foreach_w([_AM_Option], [$1], [_AM_SET_OPTION(_AM_Option)])])
+
+# _AM_IF_OPTION(OPTION, IF-SET, [IF-NOT-SET])
+# -------------------------------------------
+# Execute IF-SET if OPTION is set, IF-NOT-SET otherwise.
+AC_DEFUN([_AM_IF_OPTION],
+[m4_ifset(_AM_MANGLE_OPTION([$1]), [$2], [$3])])
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# _AM_PROG_CC_C_O
+# ---------------
+# Like AC_PROG_CC_C_O, but changed for automake. We rewrite AC_PROG_CC
+# to automatically call this.
+AC_DEFUN([_AM_PROG_CC_C_O],
+[AC_REQUIRE([AM_AUX_DIR_EXPAND])dnl
+AC_REQUIRE_AUX_FILE([compile])dnl
+AC_LANG_PUSH([C])dnl
+AC_CACHE_CHECK(
+ [whether $CC understands -c and -o together],
+ [am_cv_prog_cc_c_o],
+ [AC_LANG_CONFTEST([AC_LANG_PROGRAM([])])
+ # Make sure it works both with $CC and with simple cc.
+ # Following AC_PROG_CC_C_O, we do the test twice because some
+ # compilers refuse to overwrite an existing .o file with -o,
+ # though they will create one.
+ am_cv_prog_cc_c_o=yes
+ for am_i in 1 2; do
+ if AM_RUN_LOG([$CC -c conftest.$ac_ext -o conftest2.$ac_objext]) \
+ && test -f conftest2.$ac_objext; then
+ : OK
+ else
+ am_cv_prog_cc_c_o=no
+ break
+ fi
+ done
+ rm -f core conftest*
+ unset am_i])
+if test "$am_cv_prog_cc_c_o" != yes; then
+ # Losing compiler, so override with the script.
+ # FIXME: It is wrong to rewrite CC.
+ # But if we don't then we get into trouble of one sort or another.
+ # A longer-term fix would be to have automake use am__CC in this case,
+ # and then we could set am__CC="\$(top_srcdir)/compile \$(CC)"
+ CC="$am_aux_dir/compile $CC"
+fi
+AC_LANG_POP([C])])
+
+# For backward compatibility.
+AC_DEFUN_ONCE([AM_PROG_CC_C_O], [AC_REQUIRE([AC_PROG_CC])])
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+
+# AM_PATH_PYTHON([MINIMUM-VERSION], [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND])
+# ---------------------------------------------------------------------------
+# Adds support for distributing Python modules and packages. To
+# install modules, copy them to $(pythondir), using the python_PYTHON
+# automake variable. To install a package with the same name as the
+# automake package, install to $(pkgpythondir), or use the
+# pkgpython_PYTHON automake variable.
+#
+# The variables $(pyexecdir) and $(pkgpyexecdir) are provided as
+# locations to install python extension modules (shared libraries).
+# Another macro is required to find the appropriate flags to compile
+# extension modules.
+#
+# If your package is configured with a different prefix to python,
+# users will have to add the install directory to the PYTHONPATH
+# environment variable, or create a .pth file (see the python
+# documentation for details).
+#
+# If the MINIMUM-VERSION argument is passed, AM_PATH_PYTHON will
+# cause an error if the version of python installed on the system
+# doesn't meet the requirement. MINIMUM-VERSION should consist of
+# numbers and dots only.
+AC_DEFUN([AM_PATH_PYTHON],
+ [
+ dnl Find a Python interpreter. Python versions prior to 2.0 are not
+ dnl supported. (2.0 was released on October 16, 2000).
+ m4_define_default([_AM_PYTHON_INTERPRETER_LIST],
+[python python2 python3 dnl
+ python3.11 python3.10 dnl
+ python3.9 python3.8 python3.7 python3.6 python3.5 python3.4 python3.3 dnl
+ python3.2 python3.1 python3.0 dnl
+ python2.7 python2.6 python2.5 python2.4 python2.3 python2.2 python2.1 dnl
+ python2.0])
+
+ AC_ARG_VAR([PYTHON], [the Python interpreter])
+
+ m4_if([$1],[],[
+ dnl No version check is needed.
+ # Find any Python interpreter.
+ if test -z "$PYTHON"; then
+ AC_PATH_PROGS([PYTHON], _AM_PYTHON_INTERPRETER_LIST, :)
+ fi
+ am_display_PYTHON=python
+ ], [
+ dnl A version check is needed.
+ if test -n "$PYTHON"; then
+ # If the user set $PYTHON, use it and don't search something else.
+ AC_MSG_CHECKING([whether $PYTHON version is >= $1])
+ AM_PYTHON_CHECK_VERSION([$PYTHON], [$1],
+ [AC_MSG_RESULT([yes])],
+ [AC_MSG_RESULT([no])
+ AC_MSG_ERROR([Python interpreter is too old])])
+ am_display_PYTHON=$PYTHON
+ else
+ # Otherwise, try each interpreter until we find one that satisfies
+ # VERSION.
+ AC_CACHE_CHECK([for a Python interpreter with version >= $1],
+ [am_cv_pathless_PYTHON],[
+ for am_cv_pathless_PYTHON in _AM_PYTHON_INTERPRETER_LIST none; do
+ test "$am_cv_pathless_PYTHON" = none && break
+ AM_PYTHON_CHECK_VERSION([$am_cv_pathless_PYTHON], [$1], [break])
+ done])
+ # Set $PYTHON to the absolute path of $am_cv_pathless_PYTHON.
+ if test "$am_cv_pathless_PYTHON" = none; then
+ PYTHON=:
+ else
+ AC_PATH_PROG([PYTHON], [$am_cv_pathless_PYTHON])
+ fi
+ am_display_PYTHON=$am_cv_pathless_PYTHON
+ fi
+ ])
+
+ if test "$PYTHON" = :; then
+ dnl Run any user-specified action, or abort.
+ m4_default([$3], [AC_MSG_ERROR([no suitable Python interpreter found])])
+ else
+
+ dnl Query Python for its version number. Although site.py simply uses
+ dnl sys.version[:3], printing that failed with Python 3.10, since the
+ dnl trailing zero was eliminated. So now we output just the major
+ dnl and minor version numbers, as numbers. Apparently the tertiary
+ dnl version is not of interest.
+ dnl
+ AC_CACHE_CHECK([for $am_display_PYTHON version], [am_cv_python_version],
+ [am_cv_python_version=`$PYTHON -c "import sys; print ('%u.%u' % sys.version_info[[:2]])"`])
+ AC_SUBST([PYTHON_VERSION], [$am_cv_python_version])
+
+ dnl At times, e.g., when building shared libraries, you may want
+ dnl to know which OS platform Python thinks this is.
+ dnl
+ AC_CACHE_CHECK([for $am_display_PYTHON platform], [am_cv_python_platform],
+ [am_cv_python_platform=`$PYTHON -c "import sys; sys.stdout.write(sys.platform)"`])
+ AC_SUBST([PYTHON_PLATFORM], [$am_cv_python_platform])
+
+ dnl emacs-page
+ dnl If --with-python-sys-prefix is given, use the values of sys.prefix
+ dnl and sys.exec_prefix for the corresponding values of PYTHON_PREFIX
+ dnl and PYTHON_EXEC_PREFIX. Otherwise, use the GNU ${prefix} and
+ dnl ${exec_prefix} variables.
+ dnl
+ dnl The two are made distinct variables so they can be overridden if
+ dnl need be, although general consensus is that you shouldn't need
+ dnl this separation.
+ dnl
+ dnl Also allow directly setting the prefixes via configure options,
+ dnl overriding any default.
+ dnl
+ if test "x$prefix" = xNONE; then
+ am__usable_prefix=$ac_default_prefix
+ else
+ am__usable_prefix=$prefix
+ fi
+
+ # Allow user to request using sys.* values from Python,
+ # instead of the GNU $prefix values.
+ AC_ARG_WITH([python-sys-prefix],
+ [AS_HELP_STRING([--with-python-sys-prefix],
+ [use Python's sys.prefix and sys.exec_prefix values])],
+ [am_use_python_sys=:],
+ [am_use_python_sys=false])
+
+ # Allow user to override whatever the default Python prefix is.
+ AC_ARG_WITH([python_prefix],
+ [AS_HELP_STRING([--with-python_prefix],
+ [override the default PYTHON_PREFIX])],
+ [am_python_prefix_subst=$withval
+ am_cv_python_prefix=$withval
+ AC_MSG_CHECKING([for explicit $am_display_PYTHON prefix])
+ AC_MSG_RESULT([$am_cv_python_prefix])],
+ [
+ if $am_use_python_sys; then
+ # using python sys.prefix value, not GNU
+ AC_CACHE_CHECK([for python default $am_display_PYTHON prefix],
+ [am_cv_python_prefix],
+ [am_cv_python_prefix=`$PYTHON -c "import sys; sys.stdout.write(sys.prefix)"`])
+
+ dnl If sys.prefix is a subdir of $prefix, replace the literal value of
+ dnl $prefix with a variable reference so it can be overridden.
+ case $am_cv_python_prefix in
+ $am__usable_prefix*)
+ am__strip_prefix=`echo "$am__usable_prefix" | sed 's|.|.|g'`
+ am_python_prefix_subst=`echo "$am_cv_python_prefix" | sed "s,^$am__strip_prefix,\\${prefix},"`
+ ;;
+ *)
+ am_python_prefix_subst=$am_cv_python_prefix
+ ;;
+ esac
+ else # using GNU prefix value, not python sys.prefix
+ am_python_prefix_subst='${prefix}'
+ am_python_prefix=$am_python_prefix_subst
+ AC_MSG_CHECKING([for GNU default $am_display_PYTHON prefix])
+ AC_MSG_RESULT([$am_python_prefix])
+ fi])
+ # Substituting python_prefix_subst value.
+ AC_SUBST([PYTHON_PREFIX], [$am_python_prefix_subst])
+
+ # emacs-page Now do it all over again for Python exec_prefix, but with yet
+ # another conditional: fall back to regular prefix if that was specified.
+ AC_ARG_WITH([python_exec_prefix],
+ [AS_HELP_STRING([--with-python_exec_prefix],
+ [override the default PYTHON_EXEC_PREFIX])],
+ [am_python_exec_prefix_subst=$withval
+ am_cv_python_exec_prefix=$withval
+ AC_MSG_CHECKING([for explicit $am_display_PYTHON exec_prefix])
+ AC_MSG_RESULT([$am_cv_python_exec_prefix])],
+ [
+ # no explicit --with-python_exec_prefix, but if
+ # --with-python_prefix was given, use its value for python_exec_prefix too.
+ AS_IF([test -n "$with_python_prefix"],
+ [am_python_exec_prefix_subst=$with_python_prefix
+ am_cv_python_exec_prefix=$with_python_prefix
+ AC_MSG_CHECKING([for python_prefix-given $am_display_PYTHON exec_prefix])
+ AC_MSG_RESULT([$am_cv_python_exec_prefix])],
+ [
+ # Set am__usable_exec_prefix whether using GNU or Python values,
+ # since we use that variable for pyexecdir.
+ if test "x$exec_prefix" = xNONE; then
+ am__usable_exec_prefix=$am__usable_prefix
+ else
+ am__usable_exec_prefix=$exec_prefix
+ fi
+ #
+ if $am_use_python_sys; then # using python sys.exec_prefix, not GNU
+ AC_CACHE_CHECK([for python default $am_display_PYTHON exec_prefix],
+ [am_cv_python_exec_prefix],
+ [am_cv_python_exec_prefix=`$PYTHON -c "import sys; sys.stdout.write(sys.exec_prefix)"`])
+ dnl If sys.exec_prefix is a subdir of $exec_prefix, replace the
+ dnl literal value of $exec_prefix with a variable reference so it can
+ dnl be overridden.
+ case $am_cv_python_exec_prefix in
+ $am__usable_exec_prefix*)
+ am__strip_prefix=`echo "$am__usable_exec_prefix" | sed 's|.|.|g'`
+ am_python_exec_prefix_subst=`echo "$am_cv_python_exec_prefix" | sed "s,^$am__strip_prefix,\\${exec_prefix},"`
+ ;;
+ *)
+ am_python_exec_prefix_subst=$am_cv_python_exec_prefix
+ ;;
+ esac
+ else # using GNU $exec_prefix, not python sys.exec_prefix
+ am_python_exec_prefix_subst='${exec_prefix}'
+ am_python_exec_prefix=$am_python_exec_prefix_subst
+ AC_MSG_CHECKING([for GNU default $am_display_PYTHON exec_prefix])
+ AC_MSG_RESULT([$am_python_exec_prefix])
+ fi])])
+ # Substituting python_exec_prefix_subst.
+ AC_SUBST([PYTHON_EXEC_PREFIX], [$am_python_exec_prefix_subst])
+
+ # Factor out some code duplication into this shell variable.
+ am_python_setup_sysconfig="\
+import sys
+# Prefer sysconfig over distutils.sysconfig, for better compatibility
+# with python 3.x. See automake bug#10227.
+try:
+ import sysconfig
+except ImportError:
+ can_use_sysconfig = 0
+else:
+ can_use_sysconfig = 1
+# Can't use sysconfig in CPython 2.7, since it's broken in virtualenvs:
+# <https://github.com/pypa/virtualenv/issues/118>
+try:
+ from platform import python_implementation
+ if python_implementation() == 'CPython' and sys.version[[:3]] == '2.7':
+ can_use_sysconfig = 0
+except ImportError:
+ pass"
+
+ dnl emacs-page Set up 4 directories:
+
+ dnl 1. pythondir: where to install python scripts. This is the
+ dnl site-packages directory, not the python standard library
+ dnl directory like in previous automake betas. This behavior
+ dnl is more consistent with lispdir.m4 for example.
+ dnl Query distutils for this directory.
+ dnl
+ AC_CACHE_CHECK([for $am_display_PYTHON script directory (pythondir)],
+ [am_cv_python_pythondir],
+ [if test "x$am_cv_python_prefix" = x; then
+ am_py_prefix=$am__usable_prefix
+ else
+ am_py_prefix=$am_cv_python_prefix
+ fi
+ am_cv_python_pythondir=`$PYTHON -c "
+$am_python_setup_sysconfig
+if can_use_sysconfig:
+ if hasattr(sysconfig, 'get_default_scheme'):
+ scheme = sysconfig.get_default_scheme()
+ else:
+ scheme = sysconfig._get_default_scheme()
+ if scheme == 'posix_local':
+ # Debian's default scheme installs to /usr/local/ but we want to find headers in /usr/
+ scheme = 'posix_prefix'
+ sitedir = sysconfig.get_path('purelib', scheme, vars={'base':'$am_py_prefix'})
+else:
+ from distutils import sysconfig
+ sitedir = sysconfig.get_python_lib(0, 0, prefix='$am_py_prefix')
+sys.stdout.write(sitedir)"`
+ #
+ case $am_cv_python_pythondir in
+ $am_py_prefix*)
+ am__strip_prefix=`echo "$am_py_prefix" | sed 's|.|.|g'`
+ am_cv_python_pythondir=`echo "$am_cv_python_pythondir" | sed "s,^$am__strip_prefix,\\${PYTHON_PREFIX},"`
+ ;;
+ *)
+ case $am_py_prefix in
+ /usr|/System*) ;;
+ *) am_cv_python_pythondir="\${PYTHON_PREFIX}/lib/python$PYTHON_VERSION/site-packages"
+ ;;
+ esac
+ ;;
+ esac
+ ])
+ AC_SUBST([pythondir], [$am_cv_python_pythondir])
+
+ dnl 2. pkgpythondir: $PACKAGE directory under pythondir. Was
+ dnl PYTHON_SITE_PACKAGE in previous betas, but this naming is
+ dnl more consistent with the rest of automake.
+ dnl
+ AC_SUBST([pkgpythondir], [\${pythondir}/$PACKAGE])
+
+ dnl 3. pyexecdir: directory for installing python extension modules
+ dnl (shared libraries).
+ dnl Query distutils for this directory.
+ dnl
+ AC_CACHE_CHECK([for $am_display_PYTHON extension module directory (pyexecdir)],
+ [am_cv_python_pyexecdir],
+ [if test "x$am_cv_python_exec_prefix" = x; then
+ am_py_exec_prefix=$am__usable_exec_prefix
+ else
+ am_py_exec_prefix=$am_cv_python_exec_prefix
+ fi
+ am_cv_python_pyexecdir=`$PYTHON -c "
+$am_python_setup_sysconfig
+if can_use_sysconfig:
+ if hasattr(sysconfig, 'get_default_scheme'):
+ scheme = sysconfig.get_default_scheme()
+ else:
+ scheme = sysconfig._get_default_scheme()
+ if scheme == 'posix_local':
+ # Debian's default scheme installs to /usr/local/ but we want to find headers in /usr/
+ scheme = 'posix_prefix'
+ sitedir = sysconfig.get_path('platlib', scheme, vars={'platbase':'$am_py_exec_prefix'})
+else:
+ from distutils import sysconfig
+ sitedir = sysconfig.get_python_lib(1, 0, prefix='$am_py_exec_prefix')
+sys.stdout.write(sitedir)"`
+ #
+ case $am_cv_python_pyexecdir in
+ $am_py_exec_prefix*)
+ am__strip_prefix=`echo "$am_py_exec_prefix" | sed 's|.|.|g'`
+ am_cv_python_pyexecdir=`echo "$am_cv_python_pyexecdir" | sed "s,^$am__strip_prefix,\\${PYTHON_EXEC_PREFIX},"`
+ ;;
+ *)
+ case $am_py_exec_prefix in
+ /usr|/System*) ;;
+ *) am_cv_python_pyexecdir="\${PYTHON_EXEC_PREFIX}/lib/python$PYTHON_VERSION/site-packages"
+ ;;
+ esac
+ ;;
+ esac
+ ])
+ AC_SUBST([pyexecdir], [$am_cv_python_pyexecdir])
+
+ dnl 4. pkgpyexecdir: $(pyexecdir)/$(PACKAGE)
+ dnl
+ AC_SUBST([pkgpyexecdir], [\${pyexecdir}/$PACKAGE])
+
+ dnl Run any user-specified action.
+ $2
+ fi
+])
+
+
+# AM_PYTHON_CHECK_VERSION(PROG, VERSION, [ACTION-IF-TRUE], [ACTION-IF-FALSE])
+# ---------------------------------------------------------------------------
+# Run ACTION-IF-TRUE if the Python interpreter PROG has version >= VERSION.
+# Run ACTION-IF-FALSE otherwise.
+# This test uses sys.hexversion instead of the string equivalent (first
+# word of sys.version), in order to cope with versions such as 2.2c1.
+# This supports Python 2.0 or higher. (2.0 was released on October 16, 2000).
+AC_DEFUN([AM_PYTHON_CHECK_VERSION],
+ [prog="import sys
+# split strings by '.' and convert to numeric. Append some zeros
+# because we need at least 4 digits for the hex conversion.
+# map returns an iterator in Python 3.0 and a list in 2.x
+minver = list(map(int, '$2'.split('.'))) + [[0, 0, 0]]
+minverhex = 0
+# xrange is not present in Python 3.0 and range returns an iterator
+for i in list(range(0, 4)): minverhex = (minverhex << 8) + minver[[i]]
+sys.exit(sys.hexversion < minverhex)"
+ AS_IF([AM_RUN_LOG([$1 -c "$prog"])], [$3], [$4])])
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_RUN_LOG(COMMAND)
+# -------------------
+# Run COMMAND, save the exit status in ac_status, and log it.
+# (This has been adapted from Autoconf's _AC_RUN_LOG macro.)
+AC_DEFUN([AM_RUN_LOG],
+[{ echo "$as_me:$LINENO: $1" >&AS_MESSAGE_LOG_FD
+ ($1) >&AS_MESSAGE_LOG_FD 2>&AS_MESSAGE_LOG_FD
+ ac_status=$?
+ echo "$as_me:$LINENO: \$? = $ac_status" >&AS_MESSAGE_LOG_FD
+ (exit $ac_status); }])
+
+# Check to make sure that the build environment is sane. -*- Autoconf -*-
+
+# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_SANITY_CHECK
+# ---------------
+AC_DEFUN([AM_SANITY_CHECK],
+[AC_MSG_CHECKING([whether build environment is sane])
+# Reject unsafe characters in $srcdir or the absolute working directory
+# name. Accept space and tab only in the latter.
+am_lf='
+'
+case `pwd` in
+ *[[\\\"\#\$\&\'\`$am_lf]]*)
+ AC_MSG_ERROR([unsafe absolute working directory name]);;
+esac
+case $srcdir in
+ *[[\\\"\#\$\&\'\`$am_lf\ \ ]]*)
+ AC_MSG_ERROR([unsafe srcdir value: '$srcdir']);;
+esac
+
+# Do 'set' in a subshell so we don't clobber the current shell's
+# arguments. Must try -L first in case configure is actually a
+# symlink; some systems play weird games with the mod time of symlinks
+# (eg FreeBSD returns the mod time of the symlink's containing
+# directory).
+if (
+ am_has_slept=no
+ for am_try in 1 2; do
+ echo "timestamp, slept: $am_has_slept" > conftest.file
+ set X `ls -Lt "$srcdir/configure" conftest.file 2> /dev/null`
+ if test "$[*]" = "X"; then
+ # -L didn't work.
+ set X `ls -t "$srcdir/configure" conftest.file`
+ fi
+ if test "$[*]" != "X $srcdir/configure conftest.file" \
+ && test "$[*]" != "X conftest.file $srcdir/configure"; then
+
+ # If neither matched, then we have a broken ls. This can happen
+ # if, for instance, CONFIG_SHELL is bash and it inherits a
+ # broken ls alias from the environment. This has actually
+ # happened. Such a system could not be considered "sane".
+ AC_MSG_ERROR([ls -t appears to fail. Make sure there is not a broken
+ alias in your environment])
+ fi
+ if test "$[2]" = conftest.file || test $am_try -eq 2; then
+ break
+ fi
+ # Just in case.
+ sleep 1
+ am_has_slept=yes
+ done
+ test "$[2]" = conftest.file
+ )
+then
+ # Ok.
+ :
+else
+ AC_MSG_ERROR([newly created file is older than distributed files!
+Check your system clock])
+fi
+AC_MSG_RESULT([yes])
+# If we didn't sleep, we still need to ensure time stamps of config.status and
+# generated files are strictly newer.
+am_sleep_pid=
+if grep 'slept: no' conftest.file >/dev/null 2>&1; then
+ ( sleep 1 ) &
+ am_sleep_pid=$!
+fi
+AC_CONFIG_COMMANDS_PRE(
+ [AC_MSG_CHECKING([that generated files are newer than configure])
+ if test -n "$am_sleep_pid"; then
+ # Hide warnings about reused PIDs.
+ wait $am_sleep_pid 2>/dev/null
+ fi
+ AC_MSG_RESULT([done])])
+rm -f conftest.file
+])
+
+# Copyright (C) 2009-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_SILENT_RULES([DEFAULT])
+# --------------------------
+# Enable less verbose build rules; with the default set to DEFAULT
+# ("yes" being less verbose, "no" or empty being verbose).
+AC_DEFUN([AM_SILENT_RULES],
+[AC_ARG_ENABLE([silent-rules], [dnl
+AS_HELP_STRING(
+ [--enable-silent-rules],
+ [less verbose build output (undo: "make V=1")])
+AS_HELP_STRING(
+ [--disable-silent-rules],
+ [verbose build output (undo: "make V=0")])dnl
+])
+case $enable_silent_rules in @%:@ (((
+ yes) AM_DEFAULT_VERBOSITY=0;;
+ no) AM_DEFAULT_VERBOSITY=1;;
+ *) AM_DEFAULT_VERBOSITY=m4_if([$1], [yes], [0], [1]);;
+esac
+dnl
+dnl A few 'make' implementations (e.g., NonStop OS and NextStep)
+dnl do not support nested variable expansions.
+dnl See automake bug#9928 and bug#10237.
+am_make=${MAKE-make}
+AC_CACHE_CHECK([whether $am_make supports nested variables],
+ [am_cv_make_support_nested_variables],
+ [if AS_ECHO([['TRUE=$(BAR$(V))
+BAR0=false
+BAR1=true
+V=1
+am__doit:
+ @$(TRUE)
+.PHONY: am__doit']]) | $am_make -f - >/dev/null 2>&1; then
+ am_cv_make_support_nested_variables=yes
+else
+ am_cv_make_support_nested_variables=no
+fi])
+if test $am_cv_make_support_nested_variables = yes; then
+ dnl Using '$V' instead of '$(V)' breaks IRIX make.
+ AM_V='$(V)'
+ AM_DEFAULT_V='$(AM_DEFAULT_VERBOSITY)'
+else
+ AM_V=$AM_DEFAULT_VERBOSITY
+ AM_DEFAULT_V=$AM_DEFAULT_VERBOSITY
+fi
+AC_SUBST([AM_V])dnl
+AM_SUBST_NOTMAKE([AM_V])dnl
+AC_SUBST([AM_DEFAULT_V])dnl
+AM_SUBST_NOTMAKE([AM_DEFAULT_V])dnl
+AC_SUBST([AM_DEFAULT_VERBOSITY])dnl
+AM_BACKSLASH='\'
+AC_SUBST([AM_BACKSLASH])dnl
+_AM_SUBST_NOTMAKE([AM_BACKSLASH])dnl
+])
+
+# Copyright (C) 2001-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# AM_PROG_INSTALL_STRIP
+# ---------------------
+# One issue with vendor 'install' (even GNU) is that you can't
+# specify the program used to strip binaries. This is especially
+# annoying in cross-compiling environments, where the build's strip
+# is unlikely to handle the host's binaries.
+# Fortunately install-sh will honor a STRIPPROG variable, so we
+# always use install-sh in "make install-strip", and initialize
+# STRIPPROG with the value of the STRIP variable (set by the user).
+AC_DEFUN([AM_PROG_INSTALL_STRIP],
+[AC_REQUIRE([AM_PROG_INSTALL_SH])dnl
+# Installed binaries are usually stripped using 'strip' when the user
+# run "make install-strip". However 'strip' might not be the right
+# tool to use in cross-compilation environments, therefore Automake
+# will honor the 'STRIP' environment variable to overrule this program.
+dnl Don't test for $cross_compiling = yes, because it might be 'maybe'.
+if test "$cross_compiling" != no; then
+ AC_CHECK_TOOL([STRIP], [strip], :)
+fi
+INSTALL_STRIP_PROGRAM="\$(install_sh) -c -s"
+AC_SUBST([INSTALL_STRIP_PROGRAM])])
+
+# Copyright (C) 2006-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# _AM_SUBST_NOTMAKE(VARIABLE)
+# ---------------------------
+# Prevent Automake from outputting VARIABLE = @VARIABLE@ in Makefile.in.
+# This macro is traced by Automake.
+AC_DEFUN([_AM_SUBST_NOTMAKE])
+
+# AM_SUBST_NOTMAKE(VARIABLE)
+# --------------------------
+# Public sister of _AM_SUBST_NOTMAKE.
+AC_DEFUN([AM_SUBST_NOTMAKE], [_AM_SUBST_NOTMAKE($@)])
+
+# Check how to create a tarball. -*- Autoconf -*-
+
+# Copyright (C) 2004-2021 Free Software Foundation, Inc.
+#
+# This file is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# _AM_PROG_TAR(FORMAT)
+# --------------------
+# Check how to create a tarball in format FORMAT.
+# FORMAT should be one of 'v7', 'ustar', or 'pax'.
+#
+# Substitute a variable $(am__tar) that is a command
+# writing to stdout a FORMAT-tarball containing the directory
+# $tardir.
+# tardir=directory && $(am__tar) > result.tar
+#
+# Substitute a variable $(am__untar) that extract such
+# a tarball read from stdin.
+# $(am__untar) < result.tar
+#
+AC_DEFUN([_AM_PROG_TAR],
+[# Always define AMTAR for backward compatibility. Yes, it's still used
+# in the wild :-( We should find a proper way to deprecate it ...
+AC_SUBST([AMTAR], ['$${TAR-tar}'])
+
+# We'll loop over all known methods to create a tar archive until one works.
+_am_tools='gnutar m4_if([$1], [ustar], [plaintar]) pax cpio none'
+
+m4_if([$1], [v7],
+ [am__tar='$${TAR-tar} chof - "$$tardir"' am__untar='$${TAR-tar} xf -'],
+
+ [m4_case([$1],
+ [ustar],
+ [# The POSIX 1988 'ustar' format is defined with fixed-size fields.
+ # There is notably a 21 bits limit for the UID and the GID. In fact,
+ # the 'pax' utility can hang on bigger UID/GID (see automake bug#8343
+ # and bug#13588).
+ am_max_uid=2097151 # 2^21 - 1
+ am_max_gid=$am_max_uid
+ # The $UID and $GID variables are not portable, so we need to resort
+ # to the POSIX-mandated id(1) utility. Errors in the 'id' calls
+ # below are definitely unexpected, so allow the users to see them
+ # (that is, avoid stderr redirection).
+ am_uid=`id -u || echo unknown`
+ am_gid=`id -g || echo unknown`
+ AC_MSG_CHECKING([whether UID '$am_uid' is supported by ustar format])
+ if test $am_uid -le $am_max_uid; then
+ AC_MSG_RESULT([yes])
+ else
+ AC_MSG_RESULT([no])
+ _am_tools=none
+ fi
+ AC_MSG_CHECKING([whether GID '$am_gid' is supported by ustar format])
+ if test $am_gid -le $am_max_gid; then
+ AC_MSG_RESULT([yes])
+ else
+ AC_MSG_RESULT([no])
+ _am_tools=none
+ fi],
+
+ [pax],
+ [],
+
+ [m4_fatal([Unknown tar format])])
+
+ AC_MSG_CHECKING([how to create a $1 tar archive])
+
+ # Go ahead even if we have the value already cached. We do so because we
+ # need to set the values for the 'am__tar' and 'am__untar' variables.
+ _am_tools=${am_cv_prog_tar_$1-$_am_tools}
+
+ for _am_tool in $_am_tools; do
+ case $_am_tool in
+ gnutar)
+ for _am_tar in tar gnutar gtar; do
+ AM_RUN_LOG([$_am_tar --version]) && break
+ done
+ am__tar="$_am_tar --format=m4_if([$1], [pax], [posix], [$1]) -chf - "'"$$tardir"'
+ am__tar_="$_am_tar --format=m4_if([$1], [pax], [posix], [$1]) -chf - "'"$tardir"'
+ am__untar="$_am_tar -xf -"
+ ;;
+ plaintar)
+ # Must skip GNU tar: if it does not support --format= it doesn't create
+ # ustar tarball either.
+ (tar --version) >/dev/null 2>&1 && continue
+ am__tar='tar chf - "$$tardir"'
+ am__tar_='tar chf - "$tardir"'
+ am__untar='tar xf -'
+ ;;
+ pax)
+ am__tar='pax -L -x $1 -w "$$tardir"'
+ am__tar_='pax -L -x $1 -w "$tardir"'
+ am__untar='pax -r'
+ ;;
+ cpio)
+ am__tar='find "$$tardir" -print | cpio -o -H $1 -L'
+ am__tar_='find "$tardir" -print | cpio -o -H $1 -L'
+ am__untar='cpio -i -H $1 -d'
+ ;;
+ none)
+ am__tar=false
+ am__tar_=false
+ am__untar=false
+ ;;
+ esac
+
+ # If the value was cached, stop now. We just wanted to have am__tar
+ # and am__untar set.
+ test -n "${am_cv_prog_tar_$1}" && break
+
+ # tar/untar a dummy directory, and stop if the command works.
+ rm -rf conftest.dir
+ mkdir conftest.dir
+ echo GrepMe > conftest.dir/file
+ AM_RUN_LOG([tardir=conftest.dir && eval $am__tar_ >conftest.tar])
+ rm -rf conftest.dir
+ if test -s conftest.tar; then
+ AM_RUN_LOG([$am__untar <conftest.tar])
+ AM_RUN_LOG([cat conftest.dir/file])
+ grep GrepMe conftest.dir/file >/dev/null 2>&1 && break
+ fi
+ done
+ rm -rf conftest.dir
+
+ AC_CACHE_VAL([am_cv_prog_tar_$1], [am_cv_prog_tar_$1=$_am_tool])
+ AC_MSG_RESULT([$am_cv_prog_tar_$1])])
+
+AC_SUBST([am__tar])
+AC_SUBST([am__untar])
+]) # _AM_PROG_TAR
+
diff --git a/build.js b/build.js
new file mode 100755
index 0000000..51b9545
--- /dev/null
+++ b/build.js
@@ -0,0 +1,230 @@
+#!/usr/bin/env node
+
+import child_process from 'child_process';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+import process from 'process';
+
+import { getFiles, getTestFiles, all_subdirs } from './files.js';
+
+const production = process.env.NODE_ENV === 'production';
+const useWasm = os.arch() != 'x64';
+
+// ensure node_modules is present and up to date
+child_process.spawnSync('tools/node-modules', ['make_package_lock_json'], { stdio: 'inherit' });
+
+// List of directories to use when resolving import statements
+const nodePaths = ['pkg/lib'];
+
+// context options for distributed pages in dist/
+const pkgOptions = {
+ ...!production ? { sourcemap: "linked" } : {},
+ bundle: true,
+ external: ['*.woff', '*.woff2', '*.jpg', '*.svg', '../../assets*'], // Allow external font files which live in ../../static/fonts
+ legalComments: 'external', // Move all legal comments to a .LEGAL.txt file
+ loader: {
+ ".js": "jsx",
+ ".py": "text",
+ ".sh": "text",
+ },
+ minify: production,
+ nodePaths,
+ outbase: './pkg',
+ outdir: "./dist",
+ target: ['es2020'],
+};
+
+// context options for qunit tests in qunit/
+const qunitOptions = {
+ bundle: true,
+ minify: false,
+ nodePaths,
+ outbase: './pkg',
+ outdir: "./qunit",
+ loader: {
+ ".sh": "text",
+ },
+};
+
+const parser = (await import('argparse')).default.ArgumentParser();
+parser.add_argument('-r', '--rsync', { help: "rsync bundles to ssh target after build", metavar: "HOST" });
+parser.add_argument('-w', '--watch', { action: 'store_true', help: "Enable watch mode" });
+parser.add_argument('onlydir', { nargs: '?', help: "The pkg/<DIRECTORY> to build (eg. base1, shell, ...)", metavar: "DIRECTORY" });
+const args = parser.parse_args();
+
+if (args.onlydir?.includes('/'))
+ parser.error("Directory must not contain '/'");
+
+if (useWasm && args.watch)
+ parser.error("watch mode is not supported with esbuild-wasm");
+
+if (args.onlydir)
+ process.env.ONLYDIR = args.onlydir;
+if (args.rsync)
+ process.env.RSYNC = args.rsync;
+
+// keep cockpit.js as global external, except on base1 (as that's what exports it), and kdump (for testing that bundling works)
+const cockpitJSResolvePlugin = {
+ name: 'cockpit-js-resolve',
+ setup(build) {
+ build.onResolve({ filter: /^cockpit$/ }, args => {
+ if (args.resolveDir.endsWith('/base1') || args.resolveDir.endsWith('/kdump'))
+ return null;
+ return { path: args.path, namespace: 'external-global' };
+ });
+
+ build.onLoad({ filter: /.*/, namespace: 'external-global' },
+ args => ({ contents: `module.exports = ${args.path}` }));
+ },
+};
+
+// similar to fs.watch(), but recursively watches all subdirectories
+function watch_dirs(dir, on_change) {
+ const callback = (ev, dir, fname) => {
+ // only listen for "change" events, as renames are noisy
+ if (ev !== "change")
+ return;
+ on_change(path.join(dir, fname));
+ };
+
+ fs.watch(dir, {}, (ev, path) => callback(ev, dir, path));
+
+ // watch all subdirectories in dir
+ const d = fs.opendirSync(dir);
+ let dirent;
+ while ((dirent = d.readSync()) !== null) {
+ if (dirent.isDirectory())
+ watch_dirs(path.join(dir, dirent.name), on_change);
+ }
+ d.closeSync();
+}
+
+async function build() {
+ // dynamic imports which need node_modules
+ const copy = (await import('esbuild-plugin-copy')).default;
+ const esbuild = (await import(useWasm ? 'esbuild-wasm' : 'esbuild')).default;
+
+ const cleanPlugin = (await import('./pkg/lib/esbuild-cleanup-plugin.js')).cleanPlugin;
+ const cockpitCompressPlugin = (await import('./pkg/lib/esbuild-compress-plugin.js')).cockpitCompressPlugin;
+ const cockpitPoEsbuildPlugin = (await import('./pkg/lib/cockpit-po-plugin.js')).cockpitPoEsbuildPlugin;
+ const cockpitRsyncEsbuildPlugin = (await import('./pkg/lib/cockpit-rsync-plugin.js')).cockpitRsyncEsbuildPlugin;
+ const cockpitTestHtmlPlugin = (await import('./pkg/lib/esbuild-test-html-plugin.js')).cockpitTestHtmlPlugin;
+
+ const esbuildStylesPlugins = (await import('./pkg/lib/esbuild-common.js')).esbuildStylesPlugins;
+
+ const { entryPoints, assetFiles, redhat_fonts } = getFiles(args.onlydir);
+ const tests = getTestFiles();
+ const testEntryPoints = tests.map(test => "pkg/" + test + ".js");
+
+ const pkgFirstPlugins = [
+ cleanPlugin({ subdir: args.onlydir }),
+ ];
+
+ const pkgPlugins = [
+ cockpitJSResolvePlugin,
+ ...esbuildStylesPlugins
+ ];
+
+ const getTime = () => new Date().toTimeString().split(' ')[0];
+
+ const pkgLastPlugins = [
+ cockpitPoEsbuildPlugin({
+ subdirs: args.onlydir ? [args.onlydir] : all_subdirs,
+ // login page does not have cockpit.js, but reads window.cockpit_po
+ wrapper: subdir => subdir == "static" ? "window.cockpit_po = PO_DATA;" : undefined,
+ }),
+ // Esbuild will only copy assets that are explicitly imported and used
+ // in the code. This is a problem for index.html and manifest.json which are not imported
+ copy({ assets: [...assetFiles, ...redhat_fonts] }),
+ // cockpit-ws cannot currently serve compressed login page
+ ...production ? [cockpitCompressPlugin({ subdir: args.onlydir, exclude: /\/static/ })] : [],
+
+ {
+ name: 'notify-end',
+ setup(build) {
+ build.onEnd(() => console.log(`${getTime()}: Build finished`));
+ }
+ },
+
+ ...(args.rsync || process.env.RSYNC)
+ ? [cockpitRsyncEsbuildPlugin({ source: "dist/" + (args.onlydir || '') })]
+ : [],
+ ];
+
+ if (useWasm) {
+ // build each entry point individually, as otherwise it runs out of memory
+ // See https://github.com/evanw/esbuild/issues/3006
+ const numEntries = entryPoints.length;
+ for (const [index, entryPoint] of entryPoints.entries()) {
+ console.log("building", entryPoint);
+ const context = await esbuild.context({
+ ...pkgOptions,
+ entryPoints: [entryPoint],
+ plugins: [
+ ...(index === 0 ? pkgFirstPlugins : []),
+ ...pkgPlugins,
+ ...(index === numEntries - 1 ? pkgLastPlugins : []),
+ ],
+ });
+
+ await context.rebuild();
+ context.dispose();
+ }
+
+ // build all tests in one go, they are small enough
+ console.log("building qunit tests");
+ const context = await esbuild.context({
+ ...qunitOptions,
+ entryPoints: testEntryPoints,
+ plugins: [
+ cockpitTestHtmlPlugin({ testFiles: tests }),
+ ],
+ });
+
+ await context.rebuild();
+ context.dispose();
+ } else {
+ // with native esbuild, build everything in one go, that's fastest
+ const pkgContext = await esbuild.context({
+ ...pkgOptions,
+ entryPoints,
+ plugins: [...pkgFirstPlugins, ...pkgPlugins, ...pkgLastPlugins],
+ });
+
+ const qunitContext = await esbuild.context({
+ ...qunitOptions,
+ entryPoints: testEntryPoints,
+ plugins: [
+ cockpitTestHtmlPlugin({ testFiles: tests }),
+ ],
+ });
+
+ try {
+ await Promise.all([pkgContext.rebuild(), qunitContext.rebuild()]);
+ } catch (e) {
+ if (!args.watch)
+ process.exit(1);
+ // ignore errors in watch mode
+ }
+
+ if (args.watch) {
+ const on_change = async path => {
+ console.log("change detected:", path);
+ await Promise.all([pkgContext.cancel(), qunitContext.cancel()]);
+ try {
+ await Promise.all([pkgContext.rebuild(), qunitContext.rebuild()]);
+ } catch (e) {} // ignore in watch mode
+ };
+
+ watch_dirs('pkg', on_change);
+ // wait forever until Control-C
+ await new Promise(() => {});
+ }
+
+ pkgContext.dispose();
+ qunitContext.dispose();
+ }
+}
+
+build();
diff --git a/config.h.in b/config.h.in
new file mode 100644
index 0000000..7f01f5f
--- /dev/null
+++ b/config.h.in
@@ -0,0 +1,188 @@
+/* config.h.in. Generated from configure.ac by autoheader. */
+
+/* Default value of PATH for cockpit-session */
+#undef DEFAULT_SESSION_PATH
+
+/* Define to 1 if you have the `closefrom' function. */
+#undef HAVE_CLOSEFROM
+
+/* Define to 1 if you have the <inttypes.h> header file. */
+#undef HAVE_INTTYPES_H
+
+/* Define to 1 if you have the <minix/config.h> header file. */
+#undef HAVE_MINIX_CONFIG_H
+
+/* Define to 1 if you have the <pcp/import.h> header file. */
+#undef HAVE_PCP_IMPORT_H
+
+/* Define to 1 if you have the <pcp/pmda.h> header file. */
+#undef HAVE_PCP_PMDA_H
+
+/* Define to 1 if you have the
+ `ssh_userauth_publickey_auto_get_current_identity' function. */
+#undef HAVE_SSH_USERAUTH_PUBLICKEY_AUTO_GET_CURRENT_IDENTITY
+
+/* Define to 1 if you have the <stdint.h> header file. */
+#undef HAVE_STDINT_H
+
+/* Define to 1 if you have the <stdio.h> header file. */
+#undef HAVE_STDIO_H
+
+/* Define to 1 if you have the <stdlib.h> header file. */
+#undef HAVE_STDLIB_H
+
+/* Define to 1 if you have the <strings.h> header file. */
+#undef HAVE_STRINGS_H
+
+/* Define to 1 if you have the <string.h> header file. */
+#undef HAVE_STRING_H
+
+/* Define to 1 if you have the <sys/stat.h> header file. */
+#undef HAVE_SYS_STAT_H
+
+/* Define to 1 if you have the <sys/types.h> header file. */
+#undef HAVE_SYS_TYPES_H
+
+/* Define to 1 if you have the <unistd.h> header file. */
+#undef HAVE_UNISTD_H
+
+/* Define to 1 if you have the <wchar.h> header file. */
+#undef HAVE_WCHAR_H
+
+/* Name of package */
+#undef PACKAGE
+
+/* Define to the address where bug reports for this package should be sent. */
+#undef PACKAGE_BUGREPORT
+
+/* Define to the full name of this package. */
+#undef PACKAGE_NAME
+
+/* Define to the full name and version of this package. */
+#undef PACKAGE_STRING
+
+/* Define to the one symbol short name of this package. */
+#undef PACKAGE_TARNAME
+
+/* Define to the home page for this package. */
+#undef PACKAGE_URL
+
+/* Define to the version of this package. */
+#undef PACKAGE_VERSION
+
+/* Location of ssh-add binary */
+#undef PATH_SSH_ADD
+
+/* Location of ssh-agent binary */
+#undef PATH_SSH_AGENT
+
+/* Define to 1 if all of the C90 standard headers exist (not just the ones
+ required in a freestanding environment). This macro is provided for
+ backward compatibility; new code need not use it. */
+#undef STDC_HEADERS
+
+/* Enable extensions on AIX 3, Interix. */
+#ifndef _ALL_SOURCE
+# undef _ALL_SOURCE
+#endif
+/* Enable general extensions on macOS. */
+#ifndef _DARWIN_C_SOURCE
+# undef _DARWIN_C_SOURCE
+#endif
+/* Enable general extensions on Solaris. */
+#ifndef __EXTENSIONS__
+# undef __EXTENSIONS__
+#endif
+/* Enable GNU extensions on systems that have them. */
+#ifndef _GNU_SOURCE
+# undef _GNU_SOURCE
+#endif
+/* Enable X/Open compliant socket functions that do not require linking
+ with -lxnet on HP-UX 11.11. */
+#ifndef _HPUX_ALT_XOPEN_SOCKET_API
+# undef _HPUX_ALT_XOPEN_SOCKET_API
+#endif
+/* Identify the host operating system as Minix.
+ This macro does not affect the system headers' behavior.
+ A future release of Autoconf may stop defining this macro. */
+#ifndef _MINIX
+# undef _MINIX
+#endif
+/* Enable general extensions on NetBSD.
+ Enable NetBSD compatibility extensions on Minix. */
+#ifndef _NETBSD_SOURCE
+# undef _NETBSD_SOURCE
+#endif
+/* Enable OpenBSD compatibility extensions on NetBSD.
+ Oddly enough, this does nothing on OpenBSD. */
+#ifndef _OPENBSD_SOURCE
+# undef _OPENBSD_SOURCE
+#endif
+/* Define to 1 if needed for POSIX-compatible behavior. */
+#ifndef _POSIX_SOURCE
+# undef _POSIX_SOURCE
+#endif
+/* Define to 2 if needed for POSIX-compatible behavior. */
+#ifndef _POSIX_1_SOURCE
+# undef _POSIX_1_SOURCE
+#endif
+/* Enable POSIX-compatible threading on Solaris. */
+#ifndef _POSIX_PTHREAD_SEMANTICS
+# undef _POSIX_PTHREAD_SEMANTICS
+#endif
+/* Enable extensions specified by ISO/IEC TS 18661-5:2014. */
+#ifndef __STDC_WANT_IEC_60559_ATTRIBS_EXT__
+# undef __STDC_WANT_IEC_60559_ATTRIBS_EXT__
+#endif
+/* Enable extensions specified by ISO/IEC TS 18661-1:2014. */
+#ifndef __STDC_WANT_IEC_60559_BFP_EXT__
+# undef __STDC_WANT_IEC_60559_BFP_EXT__
+#endif
+/* Enable extensions specified by ISO/IEC TS 18661-2:2015. */
+#ifndef __STDC_WANT_IEC_60559_DFP_EXT__
+# undef __STDC_WANT_IEC_60559_DFP_EXT__
+#endif
+/* Enable extensions specified by ISO/IEC TS 18661-4:2015. */
+#ifndef __STDC_WANT_IEC_60559_FUNCS_EXT__
+# undef __STDC_WANT_IEC_60559_FUNCS_EXT__
+#endif
+/* Enable extensions specified by ISO/IEC TS 18661-3:2015. */
+#ifndef __STDC_WANT_IEC_60559_TYPES_EXT__
+# undef __STDC_WANT_IEC_60559_TYPES_EXT__
+#endif
+/* Enable extensions specified by ISO/IEC TR 24731-2:2010. */
+#ifndef __STDC_WANT_LIB_EXT2__
+# undef __STDC_WANT_LIB_EXT2__
+#endif
+/* Enable extensions specified by ISO/IEC 24747:2009. */
+#ifndef __STDC_WANT_MATH_SPEC_FUNCS__
+# undef __STDC_WANT_MATH_SPEC_FUNCS__
+#endif
+/* Enable extensions on HP NonStop. */
+#ifndef _TANDEM_SOURCE
+# undef _TANDEM_SOURCE
+#endif
+/* Enable X/Open extensions. Define to 500 only if necessary
+ to make mbstate_t available. */
+#ifndef _XOPEN_SOURCE
+# undef _XOPEN_SOURCE
+#endif
+
+
+/* Version number of package */
+#undef VERSION
+
+/* Print debug output */
+#undef WITH_DEBUG
+
+/* Whether to build with polkit support */
+#undef WITH_POLKIT
+
+/* In debug mode */
+#undef _DEBUG
+
+/* Number of bits in a file offset, on hosts where this is settable. */
+#undef _FILE_OFFSET_BITS
+
+/* Define for large files, on AIX-style hosts. */
+#undef _LARGE_FILES
diff --git a/configure b/configure
new file mode 100755
index 0000000..b03617c
--- /dev/null
+++ b/configure
@@ -0,0 +1,9081 @@
+#! /bin/sh
+# Guess values for system-dependent variables and create Makefiles.
+# Generated by GNU Autoconf 2.71 for Cockpit 311.
+#
+# Report bugs to <devel@lists.cockpit-project.org>.
+#
+#
+# Copyright (C) 1992-1996, 1998-2017, 2020-2021 Free Software Foundation,
+# Inc.
+#
+#
+# This configure script is free software; the Free Software Foundation
+# gives unlimited permission to copy, distribute and modify it.
+## -------------------- ##
+## M4sh Initialization. ##
+## -------------------- ##
+
+# Be more Bourne compatible
+DUALCASE=1; export DUALCASE # for MKS sh
+as_nop=:
+if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1
+then :
+ emulate sh
+ NULLCMD=:
+ # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which
+ # is contrary to our usage. Disable this feature.
+ alias -g '${1+"$@"}'='"$@"'
+ setopt NO_GLOB_SUBST
+else $as_nop
+ case `(set -o) 2>/dev/null` in #(
+ *posix*) :
+ set -o posix ;; #(
+ *) :
+ ;;
+esac
+fi
+
+
+
+# Reset variables that may have inherited troublesome values from
+# the environment.
+
+# IFS needs to be set, to space, tab, and newline, in precisely that order.
+# (If _AS_PATH_WALK were called with IFS unset, it would have the
+# side effect of setting IFS to empty, thus disabling word splitting.)
+# Quoting is to prevent editors from complaining about space-tab.
+as_nl='
+'
+export as_nl
+IFS=" "" $as_nl"
+
+PS1='$ '
+PS2='> '
+PS4='+ '
+
+# Ensure predictable behavior from utilities with locale-dependent output.
+LC_ALL=C
+export LC_ALL
+LANGUAGE=C
+export LANGUAGE
+
+# We cannot yet rely on "unset" to work, but we need these variables
+# to be unset--not just set to an empty or harmless value--now, to
+# avoid bugs in old shells (e.g. pre-3.0 UWIN ksh). This construct
+# also avoids known problems related to "unset" and subshell syntax
+# in other old shells (e.g. bash 2.01 and pdksh 5.2.14).
+for as_var in BASH_ENV ENV MAIL MAILPATH CDPATH
+do eval test \${$as_var+y} \
+ && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || :
+done
+
+# Ensure that fds 0, 1, and 2 are open.
+if (exec 3>&0) 2>/dev/null; then :; else exec 0</dev/null; fi
+if (exec 3>&1) 2>/dev/null; then :; else exec 1>/dev/null; fi
+if (exec 3>&2) ; then :; else exec 2>/dev/null; fi
+
+# The user is always right.
+if ${PATH_SEPARATOR+false} :; then
+ PATH_SEPARATOR=:
+ (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && {
+ (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 ||
+ PATH_SEPARATOR=';'
+ }
+fi
+
+
+# Find who we are. Look in the path if we contain no directory separator.
+as_myself=
+case $0 in #((
+ *[\\/]* ) as_myself=$0 ;;
+ *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ test -r "$as_dir$0" && as_myself=$as_dir$0 && break
+ done
+IFS=$as_save_IFS
+
+ ;;
+esac
+# We did not find ourselves, most probably we were run as `sh COMMAND'
+# in which case we are not to be found in the path.
+if test "x$as_myself" = x; then
+ as_myself=$0
+fi
+if test ! -f "$as_myself"; then
+ printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2
+ exit 1
+fi
+
+
+# Use a proper internal environment variable to ensure we don't fall
+ # into an infinite loop, continuously re-executing ourselves.
+ if test x"${_as_can_reexec}" != xno && test "x$CONFIG_SHELL" != x; then
+ _as_can_reexec=no; export _as_can_reexec;
+ # We cannot yet assume a decent shell, so we have to provide a
+# neutralization value for shells without unset; and this also
+# works around shells that cannot unset nonexistent variables.
+# Preserve -v and -x to the replacement shell.
+BASH_ENV=/dev/null
+ENV=/dev/null
+(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV
+case $- in # ((((
+ *v*x* | *x*v* ) as_opts=-vx ;;
+ *v* ) as_opts=-v ;;
+ *x* ) as_opts=-x ;;
+ * ) as_opts= ;;
+esac
+exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"}
+# Admittedly, this is quite paranoid, since all the known shells bail
+# out after a failed `exec'.
+printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2
+exit 255
+ fi
+ # We don't want this to propagate to other subprocesses.
+ { _as_can_reexec=; unset _as_can_reexec;}
+if test "x$CONFIG_SHELL" = x; then
+ as_bourne_compatible="as_nop=:
+if test \${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1
+then :
+ emulate sh
+ NULLCMD=:
+ # Pre-4.2 versions of Zsh do word splitting on \${1+\"\$@\"}, which
+ # is contrary to our usage. Disable this feature.
+ alias -g '\${1+\"\$@\"}'='\"\$@\"'
+ setopt NO_GLOB_SUBST
+else \$as_nop
+ case \`(set -o) 2>/dev/null\` in #(
+ *posix*) :
+ set -o posix ;; #(
+ *) :
+ ;;
+esac
+fi
+"
+ as_required="as_fn_return () { (exit \$1); }
+as_fn_success () { as_fn_return 0; }
+as_fn_failure () { as_fn_return 1; }
+as_fn_ret_success () { return 0; }
+as_fn_ret_failure () { return 1; }
+
+exitcode=0
+as_fn_success || { exitcode=1; echo as_fn_success failed.; }
+as_fn_failure && { exitcode=1; echo as_fn_failure succeeded.; }
+as_fn_ret_success || { exitcode=1; echo as_fn_ret_success failed.; }
+as_fn_ret_failure && { exitcode=1; echo as_fn_ret_failure succeeded.; }
+if ( set x; as_fn_ret_success y && test x = \"\$1\" )
+then :
+
+else \$as_nop
+ exitcode=1; echo positional parameters were not saved.
+fi
+test x\$exitcode = x0 || exit 1
+blah=\$(echo \$(echo blah))
+test x\"\$blah\" = xblah || exit 1
+test -x / || exit 1"
+ as_suggested=" as_lineno_1=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_1a=\$LINENO
+ as_lineno_2=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_2a=\$LINENO
+ eval 'test \"x\$as_lineno_1'\$as_run'\" != \"x\$as_lineno_2'\$as_run'\" &&
+ test \"x\`expr \$as_lineno_1'\$as_run' + 1\`\" = \"x\$as_lineno_2'\$as_run'\"' || exit 1"
+ if (eval "$as_required") 2>/dev/null
+then :
+ as_have_required=yes
+else $as_nop
+ as_have_required=no
+fi
+ if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null
+then :
+
+else $as_nop
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+as_found=false
+for as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ as_found=:
+ case $as_dir in #(
+ /*)
+ for as_base in sh bash ksh sh5; do
+ # Try only shells that exist, to save several forks.
+ as_shell=$as_dir$as_base
+ if { test -f "$as_shell" || test -f "$as_shell.exe"; } &&
+ as_run=a "$as_shell" -c "$as_bourne_compatible""$as_required" 2>/dev/null
+then :
+ CONFIG_SHELL=$as_shell as_have_required=yes
+ if as_run=a "$as_shell" -c "$as_bourne_compatible""$as_suggested" 2>/dev/null
+then :
+ break 2
+fi
+fi
+ done;;
+ esac
+ as_found=false
+done
+IFS=$as_save_IFS
+if $as_found
+then :
+
+else $as_nop
+ if { test -f "$SHELL" || test -f "$SHELL.exe"; } &&
+ as_run=a "$SHELL" -c "$as_bourne_compatible""$as_required" 2>/dev/null
+then :
+ CONFIG_SHELL=$SHELL as_have_required=yes
+fi
+fi
+
+
+ if test "x$CONFIG_SHELL" != x
+then :
+ export CONFIG_SHELL
+ # We cannot yet assume a decent shell, so we have to provide a
+# neutralization value for shells without unset; and this also
+# works around shells that cannot unset nonexistent variables.
+# Preserve -v and -x to the replacement shell.
+BASH_ENV=/dev/null
+ENV=/dev/null
+(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV
+case $- in # ((((
+ *v*x* | *x*v* ) as_opts=-vx ;;
+ *v* ) as_opts=-v ;;
+ *x* ) as_opts=-x ;;
+ * ) as_opts= ;;
+esac
+exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"}
+# Admittedly, this is quite paranoid, since all the known shells bail
+# out after a failed `exec'.
+printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2
+exit 255
+fi
+
+ if test x$as_have_required = xno
+then :
+ printf "%s\n" "$0: This script requires a shell more modern than all"
+ printf "%s\n" "$0: the shells that I found on your system."
+ if test ${ZSH_VERSION+y} ; then
+ printf "%s\n" "$0: In particular, zsh $ZSH_VERSION has bugs and should"
+ printf "%s\n" "$0: be upgraded to zsh 4.3.4 or later."
+ else
+ printf "%s\n" "$0: Please tell bug-autoconf@gnu.org and
+$0: devel@lists.cockpit-project.org about your system,
+$0: including any error possibly output before this
+$0: message. Then install a modern shell, or manually run
+$0: the script under such a shell if you do have one."
+ fi
+ exit 1
+fi
+fi
+fi
+SHELL=${CONFIG_SHELL-/bin/sh}
+export SHELL
+# Unset more variables known to interfere with behavior of common tools.
+CLICOLOR_FORCE= GREP_OPTIONS=
+unset CLICOLOR_FORCE GREP_OPTIONS
+
+## --------------------- ##
+## M4sh Shell Functions. ##
+## --------------------- ##
+# as_fn_unset VAR
+# ---------------
+# Portably unset VAR.
+as_fn_unset ()
+{
+ { eval $1=; unset $1;}
+}
+as_unset=as_fn_unset
+
+
+# as_fn_set_status STATUS
+# -----------------------
+# Set $? to STATUS, without forking.
+as_fn_set_status ()
+{
+ return $1
+} # as_fn_set_status
+
+# as_fn_exit STATUS
+# -----------------
+# Exit the shell with STATUS, even in a "trap 0" or "set -e" context.
+as_fn_exit ()
+{
+ set +e
+ as_fn_set_status $1
+ exit $1
+} # as_fn_exit
+# as_fn_nop
+# ---------
+# Do nothing but, unlike ":", preserve the value of $?.
+as_fn_nop ()
+{
+ return $?
+}
+as_nop=as_fn_nop
+
+# as_fn_mkdir_p
+# -------------
+# Create "$as_dir" as a directory, including parents if necessary.
+as_fn_mkdir_p ()
+{
+
+ case $as_dir in #(
+ -*) as_dir=./$as_dir;;
+ esac
+ test -d "$as_dir" || eval $as_mkdir_p || {
+ as_dirs=
+ while :; do
+ case $as_dir in #(
+ *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'(
+ *) as_qdir=$as_dir;;
+ esac
+ as_dirs="'$as_qdir' $as_dirs"
+ as_dir=`$as_dirname -- "$as_dir" ||
+$as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+ X"$as_dir" : 'X\(//\)[^/]' \| \
+ X"$as_dir" : 'X\(//\)$' \| \
+ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$as_dir" |
+ sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)[^/].*/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\).*/{
+ s//\1/
+ q
+ }
+ s/.*/./; q'`
+ test -d "$as_dir" && break
+ done
+ test -z "$as_dirs" || eval "mkdir $as_dirs"
+ } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir"
+
+
+} # as_fn_mkdir_p
+
+# as_fn_executable_p FILE
+# -----------------------
+# Test if FILE is an executable regular file.
+as_fn_executable_p ()
+{
+ test -f "$1" && test -x "$1"
+} # as_fn_executable_p
+# as_fn_append VAR VALUE
+# ----------------------
+# Append the text in VALUE to the end of the definition contained in VAR. Take
+# advantage of any shell optimizations that allow amortized linear growth over
+# repeated appends, instead of the typical quadratic growth present in naive
+# implementations.
+if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null
+then :
+ eval 'as_fn_append ()
+ {
+ eval $1+=\$2
+ }'
+else $as_nop
+ as_fn_append ()
+ {
+ eval $1=\$$1\$2
+ }
+fi # as_fn_append
+
+# as_fn_arith ARG...
+# ------------------
+# Perform arithmetic evaluation on the ARGs, and store the result in the
+# global $as_val. Take advantage of shells that can avoid forks. The arguments
+# must be portable across $(()) and expr.
+if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null
+then :
+ eval 'as_fn_arith ()
+ {
+ as_val=$(( $* ))
+ }'
+else $as_nop
+ as_fn_arith ()
+ {
+ as_val=`expr "$@" || test $? -eq 1`
+ }
+fi # as_fn_arith
+
+# as_fn_nop
+# ---------
+# Do nothing but, unlike ":", preserve the value of $?.
+as_fn_nop ()
+{
+ return $?
+}
+as_nop=as_fn_nop
+
+# as_fn_error STATUS ERROR [LINENO LOG_FD]
+# ----------------------------------------
+# Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are
+# provided, also output the error to LOG_FD, referencing LINENO. Then exit the
+# script with STATUS, using 1 if that was 0.
+as_fn_error ()
+{
+ as_status=$1; test $as_status -eq 0 && as_status=1
+ if test "$4"; then
+ as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4
+ fi
+ printf "%s\n" "$as_me: error: $2" >&2
+ as_fn_exit $as_status
+} # as_fn_error
+
+if expr a : '\(a\)' >/dev/null 2>&1 &&
+ test "X`expr 00001 : '.*\(...\)'`" = X001; then
+ as_expr=expr
+else
+ as_expr=false
+fi
+
+if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then
+ as_basename=basename
+else
+ as_basename=false
+fi
+
+if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then
+ as_dirname=dirname
+else
+ as_dirname=false
+fi
+
+as_me=`$as_basename -- "$0" ||
+$as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \
+ X"$0" : 'X\(//\)$' \| \
+ X"$0" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X/"$0" |
+ sed '/^.*\/\([^/][^/]*\)\/*$/{
+ s//\1/
+ q
+ }
+ /^X\/\(\/\/\)$/{
+ s//\1/
+ q
+ }
+ /^X\/\(\/\).*/{
+ s//\1/
+ q
+ }
+ s/.*/./; q'`
+
+# Avoid depending upon Character Ranges.
+as_cr_letters='abcdefghijklmnopqrstuvwxyz'
+as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+as_cr_Letters=$as_cr_letters$as_cr_LETTERS
+as_cr_digits='0123456789'
+as_cr_alnum=$as_cr_Letters$as_cr_digits
+
+
+ as_lineno_1=$LINENO as_lineno_1a=$LINENO
+ as_lineno_2=$LINENO as_lineno_2a=$LINENO
+ eval 'test "x$as_lineno_1'$as_run'" != "x$as_lineno_2'$as_run'" &&
+ test "x`expr $as_lineno_1'$as_run' + 1`" = "x$as_lineno_2'$as_run'"' || {
+ # Blame Lee E. McMahon (1931-1989) for sed's syntax. :-)
+ sed -n '
+ p
+ /[$]LINENO/=
+ ' <$as_myself |
+ sed '
+ s/[$]LINENO.*/&-/
+ t lineno
+ b
+ :lineno
+ N
+ :loop
+ s/[$]LINENO\([^'$as_cr_alnum'_].*\n\)\(.*\)/\2\1\2/
+ t loop
+ s/-\n.*//
+ ' >$as_me.lineno &&
+ chmod +x "$as_me.lineno" ||
+ { printf "%s\n" "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; }
+
+ # If we had to re-execute with $CONFIG_SHELL, we're ensured to have
+ # already done that, so ensure we don't try to do so again and fall
+ # in an infinite loop. This has already happened in practice.
+ _as_can_reexec=no; export _as_can_reexec
+ # Don't try to exec as it changes $[0], causing all sort of problems
+ # (the dirname of $[0] is not the place where we might find the
+ # original and so on. Autoconf is especially sensitive to this).
+ . "./$as_me.lineno"
+ # Exit status is that of the last command.
+ exit
+}
+
+
+# Determine whether it's possible to make 'echo' print without a newline.
+# These variables are no longer used directly by Autoconf, but are AC_SUBSTed
+# for compatibility with existing Makefiles.
+ECHO_C= ECHO_N= ECHO_T=
+case `echo -n x` in #(((((
+-n*)
+ case `echo 'xy\c'` in
+ *c*) ECHO_T=' ';; # ECHO_T is single tab character.
+ xy) ECHO_C='\c';;
+ *) echo `echo ksh88 bug on AIX 6.1` > /dev/null
+ ECHO_T=' ';;
+ esac;;
+*)
+ ECHO_N='-n';;
+esac
+
+# For backward compatibility with old third-party macros, we provide
+# the shell variables $as_echo and $as_echo_n. New code should use
+# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively.
+as_echo='printf %s\n'
+as_echo_n='printf %s'
+
+
+rm -f conf$$ conf$$.exe conf$$.file
+if test -d conf$$.dir; then
+ rm -f conf$$.dir/conf$$.file
+else
+ rm -f conf$$.dir
+ mkdir conf$$.dir 2>/dev/null
+fi
+if (echo >conf$$.file) 2>/dev/null; then
+ if ln -s conf$$.file conf$$ 2>/dev/null; then
+ as_ln_s='ln -s'
+ # ... but there are two gotchas:
+ # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail.
+ # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable.
+ # In both cases, we have to default to `cp -pR'.
+ ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe ||
+ as_ln_s='cp -pR'
+ elif ln conf$$.file conf$$ 2>/dev/null; then
+ as_ln_s=ln
+ else
+ as_ln_s='cp -pR'
+ fi
+else
+ as_ln_s='cp -pR'
+fi
+rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file
+rmdir conf$$.dir 2>/dev/null
+
+if mkdir -p . 2>/dev/null; then
+ as_mkdir_p='mkdir -p "$as_dir"'
+else
+ test -d ./-p && rmdir ./-p
+ as_mkdir_p=false
+fi
+
+as_test_x='test -x'
+as_executable_p=as_fn_executable_p
+
+# Sed expression to map a string onto a valid CPP name.
+as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'"
+
+# Sed expression to map a string onto a valid variable name.
+as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'"
+
+
+test -n "$DJDIR" || exec 7<&0 </dev/null
+exec 6>&1
+
+# Name of the host.
+# hostname on some systems (SVR3.2, old GNU/Linux) returns a bogus exit status,
+# so uname gets run too.
+ac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q`
+
+#
+# Initializations.
+#
+ac_default_prefix=/usr/local
+ac_clean_files=
+ac_config_libobj_dir=.
+LIBOBJS=
+cross_compiling=no
+subdirs=
+MFLAGS=
+MAKEFLAGS=
+
+# Identity of this package.
+PACKAGE_NAME='Cockpit'
+PACKAGE_TARNAME='cockpit'
+PACKAGE_VERSION='311'
+PACKAGE_STRING='Cockpit 311'
+PACKAGE_BUGREPORT='devel@lists.cockpit-project.org'
+PACKAGE_URL='https://cockpit-project.org/'
+
+ac_unique_file="src"
+# Factoring default headers for most tests.
+ac_includes_default="\
+#include <stddef.h>
+#ifdef HAVE_STDIO_H
+# include <stdio.h>
+#endif
+#ifdef HAVE_STDLIB_H
+# include <stdlib.h>
+#endif
+#ifdef HAVE_STRING_H
+# include <string.h>
+#endif
+#ifdef HAVE_INTTYPES_H
+# include <inttypes.h>
+#endif
+#ifdef HAVE_STDINT_H
+# include <stdint.h>
+#endif
+#ifdef HAVE_STRINGS_H
+# include <strings.h>
+#endif
+#ifdef HAVE_SYS_TYPES_H
+# include <sys/types.h>
+#endif
+#ifdef HAVE_SYS_STAT_H
+# include <sys/stat.h>
+#endif
+#ifdef HAVE_UNISTD_H
+# include <unistd.h>
+#endif"
+
+ac_header_c_list=
+ac_subst_vars='am__EXEEXT_FALSE
+am__EXEEXT_TRUE
+LTLIBOBJS
+LIBOBJS
+PAM_LIBS
+pkgpyexecdir
+pyexecdir
+pkgpythondir
+pythondir
+PYTHON_EXEC_PREFIX
+PYTHON_PREFIX
+PYTHON_PLATFORM
+PYTHON_VERSION
+PYTHON
+WITH_COVERAGE_FALSE
+WITH_COVERAGE_TRUE
+debugdir
+NODE_ENV
+WITH_DEBUG_FALSE
+WITH_DEBUG_TRUE
+ENABLE_COCKPIT_CLIENT_FALSE
+ENABLE_COCKPIT_CLIENT_TRUE
+ENABLE_DOC_FALSE
+ENABLE_DOC_TRUE
+XMLTO
+XSLTPROC
+admin_group
+COCKPIT_WSINSTANCE_GROUP
+COCKPIT_WSINSTANCE_USER
+COCKPIT_GROUP
+COCKPIT_USER
+WITH_ASAN_FALSE
+WITH_ASAN_TRUE
+SSH_AGENT
+SSH_ADD
+XGETTEXT
+systemdunitdir
+SYSTEMD_LIBS
+SYSTEMD_CFLAGS
+ENABLE_PCP_FALSE
+ENABLE_PCP_TRUE
+pamdir
+libssh_LIBS
+libssh_CFLAGS
+polkit_LIBS
+polkit_CFLAGS
+krb5_LIBS
+krb5_CFLAGS
+gnutls_LIBS
+gnutls_CFLAGS
+json_glib_LIBS
+json_glib_CFLAGS
+libsystemd_LIBS
+libsystemd_CFLAGS
+glib_LIBS
+glib_CFLAGS
+PKG_CONFIG_LIBDIR
+PKG_CONFIG_PATH
+PKG_CONFIG
+fts_LIBS
+argp_LIBS
+WITH_OLD_BRIDGE_FALSE
+WITH_OLD_BRIDGE_TRUE
+WITH_COCKPIT_SSH_FALSE
+WITH_COCKPIT_SSH_TRUE
+WITH_POLKIT_FALSE
+WITH_POLKIT_TRUE
+SELINUX_POLICY_TYPE
+SELINUX_POLICY_ENABLED_FALSE
+SELINUX_POLICY_ENABLED_TRUE
+AR
+RANLIB
+am__fastdepCC_FALSE
+am__fastdepCC_TRUE
+CCDEPMODE
+am__nodep
+AMDEPBACKSLASH
+AMDEP_FALSE
+AMDEP_TRUE
+am__include
+DEPDIR
+OBJEXT
+EXEEXT
+ac_ct_CC
+CPPFLAGS
+LDFLAGS
+CFLAGS
+CC
+AM_BACKSLASH
+AM_DEFAULT_VERBOSITY
+AM_DEFAULT_V
+AM_V
+CSCOPE
+ETAGS
+CTAGS
+am__untar
+am__tar
+AMTAR
+am__leading_dot
+SET_MAKE
+AWK
+mkdir_p
+MKDIR_P
+INSTALL_STRIP_PROGRAM
+STRIP
+install_sh
+MAKEINFO
+AUTOHEADER
+AUTOMAKE
+AUTOCONF
+ACLOCAL
+VERSION
+PACKAGE
+CYGPATH_W
+am__isrc
+INSTALL_DATA
+INSTALL_SCRIPT
+INSTALL_PROGRAM
+target_alias
+host_alias
+build_alias
+LIBS
+ECHO_T
+ECHO_N
+ECHO_C
+DEFS
+mandir
+localedir
+libdir
+psdir
+pdfdir
+dvidir
+htmldir
+infodir
+docdir
+oldincludedir
+includedir
+runstatedir
+localstatedir
+sharedstatedir
+sysconfdir
+datadir
+datarootdir
+libexecdir
+sbindir
+bindir
+program_transform_name
+prefix
+exec_prefix
+PACKAGE_URL
+PACKAGE_BUGREPORT
+PACKAGE_STRING
+PACKAGE_VERSION
+PACKAGE_TARNAME
+PACKAGE_NAME
+PATH_SEPARATOR
+SHELL
+am__quote'
+ac_subst_files=''
+ac_user_opts='
+enable_option_checking
+enable_silent_rules
+enable_dependency_tracking
+enable_largefile
+enable_prefix_only
+enable_selinux_policy
+enable_polkit
+enable_ssh
+enable_old_bridge
+with_pamdir
+enable_pcp
+with_systemdunitdir
+enable_asan
+with_cockpit_user
+with_cockpit_group
+with_cockpit_ws_instance_user
+with_cockpit_ws_instance_group
+with_admin_group
+with_default_session_path
+enable_doc
+enable_cockpit_client
+enable_debug
+enable_coverage
+enable_strict
+with_python_sys_prefix
+with_python_prefix
+with_python_exec_prefix
+'
+ ac_precious_vars='build_alias
+host_alias
+target_alias
+CC
+CFLAGS
+LDFLAGS
+LIBS
+CPPFLAGS
+PKG_CONFIG
+PKG_CONFIG_PATH
+PKG_CONFIG_LIBDIR
+glib_CFLAGS
+glib_LIBS
+libsystemd_CFLAGS
+libsystemd_LIBS
+json_glib_CFLAGS
+json_glib_LIBS
+gnutls_CFLAGS
+gnutls_LIBS
+krb5_CFLAGS
+krb5_LIBS
+polkit_CFLAGS
+polkit_LIBS
+libssh_CFLAGS
+libssh_LIBS
+SYSTEMD_CFLAGS
+SYSTEMD_LIBS
+PYTHON'
+
+
+# Initialize some variables set by options.
+ac_init_help=
+ac_init_version=false
+ac_unrecognized_opts=
+ac_unrecognized_sep=
+# The variables have the same names as the options, with
+# dashes changed to underlines.
+cache_file=/dev/null
+exec_prefix=NONE
+no_create=
+no_recursion=
+prefix=NONE
+program_prefix=NONE
+program_suffix=NONE
+program_transform_name=s,x,x,
+silent=
+site=
+srcdir=
+verbose=
+x_includes=NONE
+x_libraries=NONE
+
+# Installation directory options.
+# These are left unexpanded so users can "make install exec_prefix=/foo"
+# and all the variables that are supposed to be based on exec_prefix
+# by default will actually change.
+# Use braces instead of parens because sh, perl, etc. also accept them.
+# (The list follows the same order as the GNU Coding Standards.)
+bindir='${exec_prefix}/bin'
+sbindir='${exec_prefix}/sbin'
+libexecdir='${exec_prefix}/libexec'
+datarootdir='${prefix}/share'
+datadir='${datarootdir}'
+sysconfdir='${prefix}/etc'
+sharedstatedir='${prefix}/com'
+localstatedir='${prefix}/var'
+runstatedir='${localstatedir}/run'
+includedir='${prefix}/include'
+oldincludedir='/usr/include'
+docdir='${datarootdir}/doc/${PACKAGE_TARNAME}'
+infodir='${datarootdir}/info'
+htmldir='${docdir}'
+dvidir='${docdir}'
+pdfdir='${docdir}'
+psdir='${docdir}'
+libdir='${exec_prefix}/lib'
+localedir='${datarootdir}/locale'
+mandir='${datarootdir}/man'
+
+ac_prev=
+ac_dashdash=
+for ac_option
+do
+ # If the previous option needs an argument, assign it.
+ if test -n "$ac_prev"; then
+ eval $ac_prev=\$ac_option
+ ac_prev=
+ continue
+ fi
+
+ case $ac_option in
+ *=?*) ac_optarg=`expr "X$ac_option" : '[^=]*=\(.*\)'` ;;
+ *=) ac_optarg= ;;
+ *) ac_optarg=yes ;;
+ esac
+
+ case $ac_dashdash$ac_option in
+ --)
+ ac_dashdash=yes ;;
+
+ -bindir | --bindir | --bindi | --bind | --bin | --bi)
+ ac_prev=bindir ;;
+ -bindir=* | --bindir=* | --bindi=* | --bind=* | --bin=* | --bi=*)
+ bindir=$ac_optarg ;;
+
+ -build | --build | --buil | --bui | --bu)
+ ac_prev=build_alias ;;
+ -build=* | --build=* | --buil=* | --bui=* | --bu=*)
+ build_alias=$ac_optarg ;;
+
+ -cache-file | --cache-file | --cache-fil | --cache-fi \
+ | --cache-f | --cache- | --cache | --cach | --cac | --ca | --c)
+ ac_prev=cache_file ;;
+ -cache-file=* | --cache-file=* | --cache-fil=* | --cache-fi=* \
+ | --cache-f=* | --cache-=* | --cache=* | --cach=* | --cac=* | --ca=* | --c=*)
+ cache_file=$ac_optarg ;;
+
+ --config-cache | -C)
+ cache_file=config.cache ;;
+
+ -datadir | --datadir | --datadi | --datad)
+ ac_prev=datadir ;;
+ -datadir=* | --datadir=* | --datadi=* | --datad=*)
+ datadir=$ac_optarg ;;
+
+ -datarootdir | --datarootdir | --datarootdi | --datarootd | --dataroot \
+ | --dataroo | --dataro | --datar)
+ ac_prev=datarootdir ;;
+ -datarootdir=* | --datarootdir=* | --datarootdi=* | --datarootd=* \
+ | --dataroot=* | --dataroo=* | --dataro=* | --datar=*)
+ datarootdir=$ac_optarg ;;
+
+ -disable-* | --disable-*)
+ ac_useropt=`expr "x$ac_option" : 'x-*disable-\(.*\)'`
+ # Reject names that are not valid shell variable names.
+ expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null &&
+ as_fn_error $? "invalid feature name: \`$ac_useropt'"
+ ac_useropt_orig=$ac_useropt
+ ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'`
+ case $ac_user_opts in
+ *"
+"enable_$ac_useropt"
+"*) ;;
+ *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--disable-$ac_useropt_orig"
+ ac_unrecognized_sep=', ';;
+ esac
+ eval enable_$ac_useropt=no ;;
+
+ -docdir | --docdir | --docdi | --doc | --do)
+ ac_prev=docdir ;;
+ -docdir=* | --docdir=* | --docdi=* | --doc=* | --do=*)
+ docdir=$ac_optarg ;;
+
+ -dvidir | --dvidir | --dvidi | --dvid | --dvi | --dv)
+ ac_prev=dvidir ;;
+ -dvidir=* | --dvidir=* | --dvidi=* | --dvid=* | --dvi=* | --dv=*)
+ dvidir=$ac_optarg ;;
+
+ -enable-* | --enable-*)
+ ac_useropt=`expr "x$ac_option" : 'x-*enable-\([^=]*\)'`
+ # Reject names that are not valid shell variable names.
+ expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null &&
+ as_fn_error $? "invalid feature name: \`$ac_useropt'"
+ ac_useropt_orig=$ac_useropt
+ ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'`
+ case $ac_user_opts in
+ *"
+"enable_$ac_useropt"
+"*) ;;
+ *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--enable-$ac_useropt_orig"
+ ac_unrecognized_sep=', ';;
+ esac
+ eval enable_$ac_useropt=\$ac_optarg ;;
+
+ -exec-prefix | --exec_prefix | --exec-prefix | --exec-prefi \
+ | --exec-pref | --exec-pre | --exec-pr | --exec-p | --exec- \
+ | --exec | --exe | --ex)
+ ac_prev=exec_prefix ;;
+ -exec-prefix=* | --exec_prefix=* | --exec-prefix=* | --exec-prefi=* \
+ | --exec-pref=* | --exec-pre=* | --exec-pr=* | --exec-p=* | --exec-=* \
+ | --exec=* | --exe=* | --ex=*)
+ exec_prefix=$ac_optarg ;;
+
+ -gas | --gas | --ga | --g)
+ # Obsolete; use --with-gas.
+ with_gas=yes ;;
+
+ -help | --help | --hel | --he | -h)
+ ac_init_help=long ;;
+ -help=r* | --help=r* | --hel=r* | --he=r* | -hr*)
+ ac_init_help=recursive ;;
+ -help=s* | --help=s* | --hel=s* | --he=s* | -hs*)
+ ac_init_help=short ;;
+
+ -host | --host | --hos | --ho)
+ ac_prev=host_alias ;;
+ -host=* | --host=* | --hos=* | --ho=*)
+ host_alias=$ac_optarg ;;
+
+ -htmldir | --htmldir | --htmldi | --htmld | --html | --htm | --ht)
+ ac_prev=htmldir ;;
+ -htmldir=* | --htmldir=* | --htmldi=* | --htmld=* | --html=* | --htm=* \
+ | --ht=*)
+ htmldir=$ac_optarg ;;
+
+ -includedir | --includedir | --includedi | --included | --include \
+ | --includ | --inclu | --incl | --inc)
+ ac_prev=includedir ;;
+ -includedir=* | --includedir=* | --includedi=* | --included=* | --include=* \
+ | --includ=* | --inclu=* | --incl=* | --inc=*)
+ includedir=$ac_optarg ;;
+
+ -infodir | --infodir | --infodi | --infod | --info | --inf)
+ ac_prev=infodir ;;
+ -infodir=* | --infodir=* | --infodi=* | --infod=* | --info=* | --inf=*)
+ infodir=$ac_optarg ;;
+
+ -libdir | --libdir | --libdi | --libd)
+ ac_prev=libdir ;;
+ -libdir=* | --libdir=* | --libdi=* | --libd=*)
+ libdir=$ac_optarg ;;
+
+ -libexecdir | --libexecdir | --libexecdi | --libexecd | --libexec \
+ | --libexe | --libex | --libe)
+ ac_prev=libexecdir ;;
+ -libexecdir=* | --libexecdir=* | --libexecdi=* | --libexecd=* | --libexec=* \
+ | --libexe=* | --libex=* | --libe=*)
+ libexecdir=$ac_optarg ;;
+
+ -localedir | --localedir | --localedi | --localed | --locale)
+ ac_prev=localedir ;;
+ -localedir=* | --localedir=* | --localedi=* | --localed=* | --locale=*)
+ localedir=$ac_optarg ;;
+
+ -localstatedir | --localstatedir | --localstatedi | --localstated \
+ | --localstate | --localstat | --localsta | --localst | --locals)
+ ac_prev=localstatedir ;;
+ -localstatedir=* | --localstatedir=* | --localstatedi=* | --localstated=* \
+ | --localstate=* | --localstat=* | --localsta=* | --localst=* | --locals=*)
+ localstatedir=$ac_optarg ;;
+
+ -mandir | --mandir | --mandi | --mand | --man | --ma | --m)
+ ac_prev=mandir ;;
+ -mandir=* | --mandir=* | --mandi=* | --mand=* | --man=* | --ma=* | --m=*)
+ mandir=$ac_optarg ;;
+
+ -nfp | --nfp | --nf)
+ # Obsolete; use --without-fp.
+ with_fp=no ;;
+
+ -no-create | --no-create | --no-creat | --no-crea | --no-cre \
+ | --no-cr | --no-c | -n)
+ no_create=yes ;;
+
+ -no-recursion | --no-recursion | --no-recursio | --no-recursi \
+ | --no-recurs | --no-recur | --no-recu | --no-rec | --no-re | --no-r)
+ no_recursion=yes ;;
+
+ -oldincludedir | --oldincludedir | --oldincludedi | --oldincluded \
+ | --oldinclude | --oldinclud | --oldinclu | --oldincl | --oldinc \
+ | --oldin | --oldi | --old | --ol | --o)
+ ac_prev=oldincludedir ;;
+ -oldincludedir=* | --oldincludedir=* | --oldincludedi=* | --oldincluded=* \
+ | --oldinclude=* | --oldinclud=* | --oldinclu=* | --oldincl=* | --oldinc=* \
+ | --oldin=* | --oldi=* | --old=* | --ol=* | --o=*)
+ oldincludedir=$ac_optarg ;;
+
+ -prefix | --prefix | --prefi | --pref | --pre | --pr | --p)
+ ac_prev=prefix ;;
+ -prefix=* | --prefix=* | --prefi=* | --pref=* | --pre=* | --pr=* | --p=*)
+ prefix=$ac_optarg ;;
+
+ -program-prefix | --program-prefix | --program-prefi | --program-pref \
+ | --program-pre | --program-pr | --program-p)
+ ac_prev=program_prefix ;;
+ -program-prefix=* | --program-prefix=* | --program-prefi=* \
+ | --program-pref=* | --program-pre=* | --program-pr=* | --program-p=*)
+ program_prefix=$ac_optarg ;;
+
+ -program-suffix | --program-suffix | --program-suffi | --program-suff \
+ | --program-suf | --program-su | --program-s)
+ ac_prev=program_suffix ;;
+ -program-suffix=* | --program-suffix=* | --program-suffi=* \
+ | --program-suff=* | --program-suf=* | --program-su=* | --program-s=*)
+ program_suffix=$ac_optarg ;;
+
+ -program-transform-name | --program-transform-name \
+ | --program-transform-nam | --program-transform-na \
+ | --program-transform-n | --program-transform- \
+ | --program-transform | --program-transfor \
+ | --program-transfo | --program-transf \
+ | --program-trans | --program-tran \
+ | --progr-tra | --program-tr | --program-t)
+ ac_prev=program_transform_name ;;
+ -program-transform-name=* | --program-transform-name=* \
+ | --program-transform-nam=* | --program-transform-na=* \
+ | --program-transform-n=* | --program-transform-=* \
+ | --program-transform=* | --program-transfor=* \
+ | --program-transfo=* | --program-transf=* \
+ | --program-trans=* | --program-tran=* \
+ | --progr-tra=* | --program-tr=* | --program-t=*)
+ program_transform_name=$ac_optarg ;;
+
+ -pdfdir | --pdfdir | --pdfdi | --pdfd | --pdf | --pd)
+ ac_prev=pdfdir ;;
+ -pdfdir=* | --pdfdir=* | --pdfdi=* | --pdfd=* | --pdf=* | --pd=*)
+ pdfdir=$ac_optarg ;;
+
+ -psdir | --psdir | --psdi | --psd | --ps)
+ ac_prev=psdir ;;
+ -psdir=* | --psdir=* | --psdi=* | --psd=* | --ps=*)
+ psdir=$ac_optarg ;;
+
+ -q | -quiet | --quiet | --quie | --qui | --qu | --q \
+ | -silent | --silent | --silen | --sile | --sil)
+ silent=yes ;;
+
+ -runstatedir | --runstatedir | --runstatedi | --runstated \
+ | --runstate | --runstat | --runsta | --runst | --runs \
+ | --run | --ru | --r)
+ ac_prev=runstatedir ;;
+ -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \
+ | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \
+ | --run=* | --ru=* | --r=*)
+ runstatedir=$ac_optarg ;;
+
+ -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb)
+ ac_prev=sbindir ;;
+ -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \
+ | --sbi=* | --sb=*)
+ sbindir=$ac_optarg ;;
+
+ -sharedstatedir | --sharedstatedir | --sharedstatedi \
+ | --sharedstated | --sharedstate | --sharedstat | --sharedsta \
+ | --sharedst | --shareds | --shared | --share | --shar \
+ | --sha | --sh)
+ ac_prev=sharedstatedir ;;
+ -sharedstatedir=* | --sharedstatedir=* | --sharedstatedi=* \
+ | --sharedstated=* | --sharedstate=* | --sharedstat=* | --sharedsta=* \
+ | --sharedst=* | --shareds=* | --shared=* | --share=* | --shar=* \
+ | --sha=* | --sh=*)
+ sharedstatedir=$ac_optarg ;;
+
+ -site | --site | --sit)
+ ac_prev=site ;;
+ -site=* | --site=* | --sit=*)
+ site=$ac_optarg ;;
+
+ -srcdir | --srcdir | --srcdi | --srcd | --src | --sr)
+ ac_prev=srcdir ;;
+ -srcdir=* | --srcdir=* | --srcdi=* | --srcd=* | --src=* | --sr=*)
+ srcdir=$ac_optarg ;;
+
+ -sysconfdir | --sysconfdir | --sysconfdi | --sysconfd | --sysconf \
+ | --syscon | --sysco | --sysc | --sys | --sy)
+ ac_prev=sysconfdir ;;
+ -sysconfdir=* | --sysconfdir=* | --sysconfdi=* | --sysconfd=* | --sysconf=* \
+ | --syscon=* | --sysco=* | --sysc=* | --sys=* | --sy=*)
+ sysconfdir=$ac_optarg ;;
+
+ -target | --target | --targe | --targ | --tar | --ta | --t)
+ ac_prev=target_alias ;;
+ -target=* | --target=* | --targe=* | --targ=* | --tar=* | --ta=* | --t=*)
+ target_alias=$ac_optarg ;;
+
+ -v | -verbose | --verbose | --verbos | --verbo | --verb)
+ verbose=yes ;;
+
+ -version | --version | --versio | --versi | --vers | -V)
+ ac_init_version=: ;;
+
+ -with-* | --with-*)
+ ac_useropt=`expr "x$ac_option" : 'x-*with-\([^=]*\)'`
+ # Reject names that are not valid shell variable names.
+ expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null &&
+ as_fn_error $? "invalid package name: \`$ac_useropt'"
+ ac_useropt_orig=$ac_useropt
+ ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'`
+ case $ac_user_opts in
+ *"
+"with_$ac_useropt"
+"*) ;;
+ *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--with-$ac_useropt_orig"
+ ac_unrecognized_sep=', ';;
+ esac
+ eval with_$ac_useropt=\$ac_optarg ;;
+
+ -without-* | --without-*)
+ ac_useropt=`expr "x$ac_option" : 'x-*without-\(.*\)'`
+ # Reject names that are not valid shell variable names.
+ expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null &&
+ as_fn_error $? "invalid package name: \`$ac_useropt'"
+ ac_useropt_orig=$ac_useropt
+ ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'`
+ case $ac_user_opts in
+ *"
+"with_$ac_useropt"
+"*) ;;
+ *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--without-$ac_useropt_orig"
+ ac_unrecognized_sep=', ';;
+ esac
+ eval with_$ac_useropt=no ;;
+
+ --x)
+ # Obsolete; use --with-x.
+ with_x=yes ;;
+
+ -x-includes | --x-includes | --x-include | --x-includ | --x-inclu \
+ | --x-incl | --x-inc | --x-in | --x-i)
+ ac_prev=x_includes ;;
+ -x-includes=* | --x-includes=* | --x-include=* | --x-includ=* | --x-inclu=* \
+ | --x-incl=* | --x-inc=* | --x-in=* | --x-i=*)
+ x_includes=$ac_optarg ;;
+
+ -x-libraries | --x-libraries | --x-librarie | --x-librari \
+ | --x-librar | --x-libra | --x-libr | --x-lib | --x-li | --x-l)
+ ac_prev=x_libraries ;;
+ -x-libraries=* | --x-libraries=* | --x-librarie=* | --x-librari=* \
+ | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*)
+ x_libraries=$ac_optarg ;;
+
+ -*) as_fn_error $? "unrecognized option: \`$ac_option'
+Try \`$0 --help' for more information"
+ ;;
+
+ *=*)
+ ac_envvar=`expr "x$ac_option" : 'x\([^=]*\)='`
+ # Reject names that are not valid shell variable names.
+ case $ac_envvar in #(
+ '' | [0-9]* | *[!_$as_cr_alnum]* )
+ as_fn_error $? "invalid variable name: \`$ac_envvar'" ;;
+ esac
+ eval $ac_envvar=\$ac_optarg
+ export $ac_envvar ;;
+
+ *)
+ # FIXME: should be removed in autoconf 3.0.
+ printf "%s\n" "$as_me: WARNING: you should use --build, --host, --target" >&2
+ expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null &&
+ printf "%s\n" "$as_me: WARNING: invalid host type: $ac_option" >&2
+ : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}"
+ ;;
+
+ esac
+done
+
+if test -n "$ac_prev"; then
+ ac_option=--`echo $ac_prev | sed 's/_/-/g'`
+ as_fn_error $? "missing argument to $ac_option"
+fi
+
+if test -n "$ac_unrecognized_opts"; then
+ case $enable_option_checking in
+ no) ;;
+ fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;;
+ *) printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;;
+ esac
+fi
+
+# Check all directory arguments for consistency.
+for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \
+ datadir sysconfdir sharedstatedir localstatedir includedir \
+ oldincludedir docdir infodir htmldir dvidir pdfdir psdir \
+ libdir localedir mandir runstatedir
+do
+ eval ac_val=\$$ac_var
+ # Remove trailing slashes.
+ case $ac_val in
+ */ )
+ ac_val=`expr "X$ac_val" : 'X\(.*[^/]\)' \| "X$ac_val" : 'X\(.*\)'`
+ eval $ac_var=\$ac_val;;
+ esac
+ # Be sure to have absolute directory names.
+ case $ac_val in
+ [\\/$]* | ?:[\\/]* ) continue;;
+ NONE | '' ) case $ac_var in *prefix ) continue;; esac;;
+ esac
+ as_fn_error $? "expected an absolute directory name for --$ac_var: $ac_val"
+done
+
+# There might be people who depend on the old broken behavior: `$host'
+# used to hold the argument of --host etc.
+# FIXME: To remove some day.
+build=$build_alias
+host=$host_alias
+target=$target_alias
+
+# FIXME: To remove some day.
+if test "x$host_alias" != x; then
+ if test "x$build_alias" = x; then
+ cross_compiling=maybe
+ elif test "x$build_alias" != "x$host_alias"; then
+ cross_compiling=yes
+ fi
+fi
+
+ac_tool_prefix=
+test -n "$host_alias" && ac_tool_prefix=$host_alias-
+
+test "$silent" = yes && exec 6>/dev/null
+
+
+ac_pwd=`pwd` && test -n "$ac_pwd" &&
+ac_ls_di=`ls -di .` &&
+ac_pwd_ls_di=`cd "$ac_pwd" && ls -di .` ||
+ as_fn_error $? "working directory cannot be determined"
+test "X$ac_ls_di" = "X$ac_pwd_ls_di" ||
+ as_fn_error $? "pwd does not report name of working directory"
+
+
+# Find the source files, if location was not specified.
+if test -z "$srcdir"; then
+ ac_srcdir_defaulted=yes
+ # Try the directory containing this script, then the parent directory.
+ ac_confdir=`$as_dirname -- "$as_myself" ||
+$as_expr X"$as_myself" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+ X"$as_myself" : 'X\(//\)[^/]' \| \
+ X"$as_myself" : 'X\(//\)$' \| \
+ X"$as_myself" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$as_myself" |
+ sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)[^/].*/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\).*/{
+ s//\1/
+ q
+ }
+ s/.*/./; q'`
+ srcdir=$ac_confdir
+ if test ! -r "$srcdir/$ac_unique_file"; then
+ srcdir=..
+ fi
+else
+ ac_srcdir_defaulted=no
+fi
+if test ! -r "$srcdir/$ac_unique_file"; then
+ test "$ac_srcdir_defaulted" = yes && srcdir="$ac_confdir or .."
+ as_fn_error $? "cannot find sources ($ac_unique_file) in $srcdir"
+fi
+ac_msg="sources are in $srcdir, but \`cd $srcdir' does not work"
+ac_abs_confdir=`(
+ cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error $? "$ac_msg"
+ pwd)`
+# When building in place, set srcdir=.
+if test "$ac_abs_confdir" = "$ac_pwd"; then
+ srcdir=.
+fi
+# Remove unnecessary trailing slashes from srcdir.
+# Double slashes in file names in object file debugging info
+# mess up M-x gdb in Emacs.
+case $srcdir in
+*/) srcdir=`expr "X$srcdir" : 'X\(.*[^/]\)' \| "X$srcdir" : 'X\(.*\)'`;;
+esac
+for ac_var in $ac_precious_vars; do
+ eval ac_env_${ac_var}_set=\${${ac_var}+set}
+ eval ac_env_${ac_var}_value=\$${ac_var}
+ eval ac_cv_env_${ac_var}_set=\${${ac_var}+set}
+ eval ac_cv_env_${ac_var}_value=\$${ac_var}
+done
+
+#
+# Report the --help message.
+#
+if test "$ac_init_help" = "long"; then
+ # Omit some internal or obsolete options to make the list less imposing.
+ # This message is too long to be a string in the A/UX 3.1 sh.
+ cat <<_ACEOF
+\`configure' configures Cockpit 311 to adapt to many kinds of systems.
+
+Usage: $0 [OPTION]... [VAR=VALUE]...
+
+To assign environment variables (e.g., CC, CFLAGS...), specify them as
+VAR=VALUE. See below for descriptions of some of the useful variables.
+
+Defaults for the options are specified in brackets.
+
+Configuration:
+ -h, --help display this help and exit
+ --help=short display options specific to this package
+ --help=recursive display the short help of all the included packages
+ -V, --version display version information and exit
+ -q, --quiet, --silent do not print \`checking ...' messages
+ --cache-file=FILE cache test results in FILE [disabled]
+ -C, --config-cache alias for \`--cache-file=config.cache'
+ -n, --no-create do not create output files
+ --srcdir=DIR find the sources in DIR [configure dir or \`..']
+
+Installation directories:
+ --prefix=PREFIX install architecture-independent files in PREFIX
+ [$ac_default_prefix]
+ --exec-prefix=EPREFIX install architecture-dependent files in EPREFIX
+ [PREFIX]
+
+By default, \`make install' will install all the files in
+\`$ac_default_prefix/bin', \`$ac_default_prefix/lib' etc. You can specify
+an installation prefix other than \`$ac_default_prefix' using \`--prefix',
+for instance \`--prefix=\$HOME'.
+
+For better control, use the options below.
+
+Fine tuning of the installation directories:
+ --bindir=DIR user executables [EPREFIX/bin]
+ --sbindir=DIR system admin executables [EPREFIX/sbin]
+ --libexecdir=DIR program executables [EPREFIX/libexec]
+ --sysconfdir=DIR read-only single-machine data [PREFIX/etc]
+ --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com]
+ --localstatedir=DIR modifiable single-machine data [PREFIX/var]
+ --runstatedir=DIR modifiable per-process data [LOCALSTATEDIR/run]
+ --libdir=DIR object code libraries [EPREFIX/lib]
+ --includedir=DIR C header files [PREFIX/include]
+ --oldincludedir=DIR C header files for non-gcc [/usr/include]
+ --datarootdir=DIR read-only arch.-independent data root [PREFIX/share]
+ --datadir=DIR read-only architecture-independent data [DATAROOTDIR]
+ --infodir=DIR info documentation [DATAROOTDIR/info]
+ --localedir=DIR locale-dependent data [DATAROOTDIR/locale]
+ --mandir=DIR man documentation [DATAROOTDIR/man]
+ --docdir=DIR documentation root [DATAROOTDIR/doc/cockpit]
+ --htmldir=DIR html documentation [DOCDIR]
+ --dvidir=DIR dvi documentation [DOCDIR]
+ --pdfdir=DIR pdf documentation [DOCDIR]
+ --psdir=DIR ps documentation [DOCDIR]
+_ACEOF
+
+ cat <<\_ACEOF
+
+Program names:
+ --program-prefix=PREFIX prepend PREFIX to installed program names
+ --program-suffix=SUFFIX append SUFFIX to installed program names
+ --program-transform-name=PROGRAM run sed PROGRAM on installed program names
+_ACEOF
+fi
+
+if test -n "$ac_init_help"; then
+ case $ac_init_help in
+ short | recursive ) echo "Configuration of Cockpit 311:";;
+ esac
+ cat <<\_ACEOF
+
+Optional Features:
+ --disable-option-checking ignore unrecognized --enable/--with options
+ --disable-FEATURE do not include FEATURE (same as --enable-FEATURE=no)
+ --enable-FEATURE[=ARG] include FEATURE [ARG=yes]
+ --enable-silent-rules less verbose build output (undo: "make V=1")
+ --disable-silent-rules verbose build output (undo: "make V=0")
+ --enable-dependency-tracking
+ do not reject slow dependency extractors
+ --disable-dependency-tracking
+ speeds up one-time build
+ --disable-largefile omit support for large files
+ --enable-prefix-only Whether to install to prefix only
+ --enable-selinux-policy=type
+ Whether to build selinux policy, and which
+ --disable-polkit Disable usage of polkit
+ --disable-ssh Disable cockpit-ssh build and libssh dependency
+ --enable-old-bridge Install old C cockpit-bridge
+ --disable-pcp Disable usage of PCP
+ --enable-asan=no/yes Turn the Address Sanitizer on or off
+
+ --disable-doc Disable building documentation
+
+ --enable-cockpit-client Whether to install cockpit-client
+ --enable-debug=no/default/yes
+ Turn on or off debugging
+
+ --enable-coverage Whether to enable coverage testing
+
+ --enable-strict Strict code compilation
+
+
+Optional Packages:
+ --with-PACKAGE[=ARG] use PACKAGE [ARG=yes]
+ --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no)
+ --with-pamdir=DIR directory to install pam modules in
+ --with-systemdunitdir=DIR
+ directory to install systemd unit files in
+ --with-cockpit-user=<user>
+ User for running cockpit (root)
+
+ --with-cockpit-group=<group>
+ Group for running cockpit
+
+ --with-cockpit-ws-instance-user=<user>
+ User for running cockpit-ws instances from
+ cockpit-tls (root)
+
+ --with-cockpit-ws-instance-group=<group>
+ Group for running cockpit-ws instances from
+ cockpit-tls
+
+ --with-admin-group=GROUP
+ system group to which admin users belong
+ --with-default-session-path=PATH
+ The value for the PATH environment variable in a
+ session started by cockpit-session
+ --with-python-sys-prefix
+ use Python's sys.prefix and sys.exec_prefix values
+ --with-python_prefix override the default PYTHON_PREFIX
+ --with-python_exec_prefix
+ override the default PYTHON_EXEC_PREFIX
+
+Some influential environment variables:
+ CC C compiler command
+ CFLAGS C compiler flags
+ LDFLAGS linker flags, e.g. -L<lib dir> if you have libraries in a
+ nonstandard directory <lib dir>
+ LIBS libraries to pass to the linker, e.g. -l<library>
+ CPPFLAGS (Objective) C/C++ preprocessor flags, e.g. -I<include dir> if
+ you have headers in a nonstandard directory <include dir>
+ PKG_CONFIG path to pkg-config utility
+ PKG_CONFIG_PATH
+ directories to add to pkg-config's search path
+ PKG_CONFIG_LIBDIR
+ path overriding pkg-config's built-in search path
+ glib_CFLAGS C compiler flags for glib, overriding pkg-config
+ glib_LIBS linker flags for glib, overriding pkg-config
+ libsystemd_CFLAGS
+ C compiler flags for libsystemd, overriding pkg-config
+ libsystemd_LIBS
+ linker flags for libsystemd, overriding pkg-config
+ json_glib_CFLAGS
+ C compiler flags for json_glib, overriding pkg-config
+ json_glib_LIBS
+ linker flags for json_glib, overriding pkg-config
+ gnutls_CFLAGS
+ C compiler flags for gnutls, overriding pkg-config
+ gnutls_LIBS linker flags for gnutls, overriding pkg-config
+ krb5_CFLAGS C compiler flags for krb5, overriding pkg-config
+ krb5_LIBS linker flags for krb5, overriding pkg-config
+ polkit_CFLAGS
+ C compiler flags for polkit, overriding pkg-config
+ polkit_LIBS linker flags for polkit, overriding pkg-config
+ libssh_CFLAGS
+ C compiler flags for libssh, overriding pkg-config
+ libssh_LIBS linker flags for libssh, overriding pkg-config
+ SYSTEMD_CFLAGS
+ C compiler flags for SYSTEMD, overriding pkg-config
+ SYSTEMD_LIBS
+ linker flags for SYSTEMD, overriding pkg-config
+ PYTHON the Python interpreter
+
+Use these variables to override the choices made by `configure' or to help
+it to find libraries and programs with nonstandard names/locations.
+
+Report bugs to <devel@lists.cockpit-project.org>.
+Cockpit home page: <https://cockpit-project.org/>.
+_ACEOF
+ac_status=$?
+fi
+
+if test "$ac_init_help" = "recursive"; then
+ # If there are subdirs, report their specific --help.
+ for ac_dir in : $ac_subdirs_all; do test "x$ac_dir" = x: && continue
+ test -d "$ac_dir" ||
+ { cd "$srcdir" && ac_pwd=`pwd` && srcdir=. && test -d "$ac_dir"; } ||
+ continue
+ ac_builddir=.
+
+case "$ac_dir" in
+.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;;
+*)
+ ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'`
+ # A ".." for each directory in $ac_dir_suffix.
+ ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'`
+ case $ac_top_builddir_sub in
+ "") ac_top_builddir_sub=. ac_top_build_prefix= ;;
+ *) ac_top_build_prefix=$ac_top_builddir_sub/ ;;
+ esac ;;
+esac
+ac_abs_top_builddir=$ac_pwd
+ac_abs_builddir=$ac_pwd$ac_dir_suffix
+# for backward compatibility:
+ac_top_builddir=$ac_top_build_prefix
+
+case $srcdir in
+ .) # We are building in place.
+ ac_srcdir=.
+ ac_top_srcdir=$ac_top_builddir_sub
+ ac_abs_top_srcdir=$ac_pwd ;;
+ [\\/]* | ?:[\\/]* ) # Absolute name.
+ ac_srcdir=$srcdir$ac_dir_suffix;
+ ac_top_srcdir=$srcdir
+ ac_abs_top_srcdir=$srcdir ;;
+ *) # Relative name.
+ ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix
+ ac_top_srcdir=$ac_top_build_prefix$srcdir
+ ac_abs_top_srcdir=$ac_pwd/$srcdir ;;
+esac
+ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix
+
+ cd "$ac_dir" || { ac_status=$?; continue; }
+ # Check for configure.gnu first; this name is used for a wrapper for
+ # Metaconfig's "Configure" on case-insensitive file systems.
+ if test -f "$ac_srcdir/configure.gnu"; then
+ echo &&
+ $SHELL "$ac_srcdir/configure.gnu" --help=recursive
+ elif test -f "$ac_srcdir/configure"; then
+ echo &&
+ $SHELL "$ac_srcdir/configure" --help=recursive
+ else
+ printf "%s\n" "$as_me: WARNING: no configuration information is in $ac_dir" >&2
+ fi || ac_status=$?
+ cd "$ac_pwd" || { ac_status=$?; break; }
+ done
+fi
+
+test -n "$ac_init_help" && exit $ac_status
+if $ac_init_version; then
+ cat <<\_ACEOF
+Cockpit configure 311
+generated by GNU Autoconf 2.71
+
+Copyright (C) 2021 Free Software Foundation, Inc.
+This configure script is free software; the Free Software Foundation
+gives unlimited permission to copy, distribute and modify it.
+_ACEOF
+ exit
+fi
+
+## ------------------------ ##
+## Autoconf initialization. ##
+## ------------------------ ##
+
+# ac_fn_c_try_compile LINENO
+# --------------------------
+# Try to compile conftest.$ac_ext, and return whether this succeeded.
+ac_fn_c_try_compile ()
+{
+ as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+ rm -f conftest.$ac_objext conftest.beam
+ if { { ac_try="$ac_compile"
+case "(($ac_try" in
+ *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+ *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+ (eval "$ac_compile") 2>conftest.err
+ ac_status=$?
+ if test -s conftest.err; then
+ grep -v '^ *+' conftest.err >conftest.er1
+ cat conftest.er1 >&5
+ mv -f conftest.er1 conftest.err
+ fi
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; } && {
+ test -z "$ac_c_werror_flag" ||
+ test ! -s conftest.err
+ } && test -s conftest.$ac_objext
+then :
+ ac_retval=0
+else $as_nop
+ printf "%s\n" "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+ ac_retval=1
+fi
+ eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
+ as_fn_set_status $ac_retval
+
+} # ac_fn_c_try_compile
+
+# ac_fn_c_check_header_compile LINENO HEADER VAR INCLUDES
+# -------------------------------------------------------
+# Tests whether HEADER exists and can be compiled using the include files in
+# INCLUDES, setting the cache variable VAR accordingly.
+ac_fn_c_check_header_compile ()
+{
+ as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5
+printf %s "checking for $2... " >&6; }
+if eval test \${$3+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+$4
+#include <$2>
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ eval "$3=yes"
+else $as_nop
+ eval "$3=no"
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+eval ac_res=\$$3
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
+printf "%s\n" "$ac_res" >&6; }
+ eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
+
+} # ac_fn_c_check_header_compile
+
+# ac_fn_c_try_link LINENO
+# -----------------------
+# Try to link conftest.$ac_ext, and return whether this succeeded.
+ac_fn_c_try_link ()
+{
+ as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+ rm -f conftest.$ac_objext conftest.beam conftest$ac_exeext
+ if { { ac_try="$ac_link"
+case "(($ac_try" in
+ *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+ *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+ (eval "$ac_link") 2>conftest.err
+ ac_status=$?
+ if test -s conftest.err; then
+ grep -v '^ *+' conftest.err >conftest.er1
+ cat conftest.er1 >&5
+ mv -f conftest.er1 conftest.err
+ fi
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; } && {
+ test -z "$ac_c_werror_flag" ||
+ test ! -s conftest.err
+ } && test -s conftest$ac_exeext && {
+ test "$cross_compiling" = yes ||
+ test -x conftest$ac_exeext
+ }
+then :
+ ac_retval=0
+else $as_nop
+ printf "%s\n" "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+ ac_retval=1
+fi
+ # Delete the IPA/IPO (Inter Procedural Analysis/Optimization) information
+ # created by the PGI compiler (conftest_ipa8_conftest.oo), as it would
+ # interfere with the next link command; also delete a directory that is
+ # left behind by Apple's compiler. We do this before executing the actions.
+ rm -rf conftest.dSYM conftest_ipa8_conftest.oo
+ eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
+ as_fn_set_status $ac_retval
+
+} # ac_fn_c_try_link
+
+# ac_fn_c_check_func LINENO FUNC VAR
+# ----------------------------------
+# Tests whether FUNC exists, setting the cache variable VAR accordingly
+ac_fn_c_check_func ()
+{
+ as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5
+printf %s "checking for $2... " >&6; }
+if eval test \${$3+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+/* Define $2 to an innocuous variant, in case <limits.h> declares $2.
+ For example, HP-UX 11i <limits.h> declares gettimeofday. */
+#define $2 innocuous_$2
+
+/* System header to define __stub macros and hopefully few prototypes,
+ which can conflict with char $2 (); below. */
+
+#include <limits.h>
+#undef $2
+
+/* Override any GCC internal prototype to avoid an error.
+ Use char because int might match the return type of a GCC
+ builtin and then its argument prototype would still apply. */
+#ifdef __cplusplus
+extern "C"
+#endif
+char $2 ();
+/* The GNU C library defines this for functions which it implements
+ to always fail with ENOSYS. Some functions are actually named
+ something starting with __ and the normal name is an alias. */
+#if defined __stub_$2 || defined __stub___$2
+choke me
+#endif
+
+int
+main (void)
+{
+return $2 ();
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+ eval "$3=yes"
+else $as_nop
+ eval "$3=no"
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+ conftest$ac_exeext conftest.$ac_ext
+fi
+eval ac_res=\$$3
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
+printf "%s\n" "$ac_res" >&6; }
+ eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
+
+} # ac_fn_c_check_func
+ac_configure_args_raw=
+for ac_arg
+do
+ case $ac_arg in
+ *\'*)
+ ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;;
+ esac
+ as_fn_append ac_configure_args_raw " '$ac_arg'"
+done
+
+case $ac_configure_args_raw in
+ *$as_nl*)
+ ac_safe_unquote= ;;
+ *)
+ ac_unsafe_z='|&;<>()$`\\"*?[ '' ' # This string ends in space, tab.
+ ac_unsafe_a="$ac_unsafe_z#~"
+ ac_safe_unquote="s/ '\\([^$ac_unsafe_a][^$ac_unsafe_z]*\\)'/ \\1/g"
+ ac_configure_args_raw=` printf "%s\n" "$ac_configure_args_raw" | sed "$ac_safe_unquote"`;;
+esac
+
+cat >config.log <<_ACEOF
+This file contains any messages produced by compilers while
+running configure, to aid debugging if configure makes a mistake.
+
+It was created by Cockpit $as_me 311, which was
+generated by GNU Autoconf 2.71. Invocation command line was
+
+ $ $0$ac_configure_args_raw
+
+_ACEOF
+exec 5>>config.log
+{
+cat <<_ASUNAME
+## --------- ##
+## Platform. ##
+## --------- ##
+
+hostname = `(hostname || uname -n) 2>/dev/null | sed 1q`
+uname -m = `(uname -m) 2>/dev/null || echo unknown`
+uname -r = `(uname -r) 2>/dev/null || echo unknown`
+uname -s = `(uname -s) 2>/dev/null || echo unknown`
+uname -v = `(uname -v) 2>/dev/null || echo unknown`
+
+/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null || echo unknown`
+/bin/uname -X = `(/bin/uname -X) 2>/dev/null || echo unknown`
+
+/bin/arch = `(/bin/arch) 2>/dev/null || echo unknown`
+/usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null || echo unknown`
+/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null || echo unknown`
+/usr/bin/hostinfo = `(/usr/bin/hostinfo) 2>/dev/null || echo unknown`
+/bin/machine = `(/bin/machine) 2>/dev/null || echo unknown`
+/usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null || echo unknown`
+/bin/universe = `(/bin/universe) 2>/dev/null || echo unknown`
+
+_ASUNAME
+
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ printf "%s\n" "PATH: $as_dir"
+ done
+IFS=$as_save_IFS
+
+} >&5
+
+cat >&5 <<_ACEOF
+
+
+## ----------- ##
+## Core tests. ##
+## ----------- ##
+
+_ACEOF
+
+
+# Keep a trace of the command line.
+# Strip out --no-create and --no-recursion so they do not pile up.
+# Strip out --silent because we don't want to record it for future runs.
+# Also quote any args containing shell meta-characters.
+# Make two passes to allow for proper duplicate-argument suppression.
+ac_configure_args=
+ac_configure_args0=
+ac_configure_args1=
+ac_must_keep_next=false
+for ac_pass in 1 2
+do
+ for ac_arg
+ do
+ case $ac_arg in
+ -no-create | --no-c* | -n | -no-recursion | --no-r*) continue ;;
+ -q | -quiet | --quiet | --quie | --qui | --qu | --q \
+ | -silent | --silent | --silen | --sile | --sil)
+ continue ;;
+ *\'*)
+ ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;;
+ esac
+ case $ac_pass in
+ 1) as_fn_append ac_configure_args0 " '$ac_arg'" ;;
+ 2)
+ as_fn_append ac_configure_args1 " '$ac_arg'"
+ if test $ac_must_keep_next = true; then
+ ac_must_keep_next=false # Got value, back to normal.
+ else
+ case $ac_arg in
+ *=* | --config-cache | -C | -disable-* | --disable-* \
+ | -enable-* | --enable-* | -gas | --g* | -nfp | --nf* \
+ | -q | -quiet | --q* | -silent | --sil* | -v | -verb* \
+ | -with-* | --with-* | -without-* | --without-* | --x)
+ case "$ac_configure_args0 " in
+ "$ac_configure_args1"*" '$ac_arg' "* ) continue ;;
+ esac
+ ;;
+ -* ) ac_must_keep_next=true ;;
+ esac
+ fi
+ as_fn_append ac_configure_args " '$ac_arg'"
+ ;;
+ esac
+ done
+done
+{ ac_configure_args0=; unset ac_configure_args0;}
+{ ac_configure_args1=; unset ac_configure_args1;}
+
+# When interrupted or exit'd, cleanup temporary files, and complete
+# config.log. We remove comments because anyway the quotes in there
+# would cause problems or look ugly.
+# WARNING: Use '\'' to represent an apostrophe within the trap.
+# WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug.
+trap 'exit_status=$?
+ # Sanitize IFS.
+ IFS=" "" $as_nl"
+ # Save into config.log some information that might help in debugging.
+ {
+ echo
+
+ printf "%s\n" "## ---------------- ##
+## Cache variables. ##
+## ---------------- ##"
+ echo
+ # The following way of writing the cache mishandles newlines in values,
+(
+ for ac_var in `(set) 2>&1 | sed -n '\''s/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'\''`; do
+ eval ac_val=\$$ac_var
+ case $ac_val in #(
+ *${as_nl}*)
+ case $ac_var in #(
+ *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5
+printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;;
+ esac
+ case $ac_var in #(
+ _ | IFS | as_nl) ;; #(
+ BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #(
+ *) { eval $ac_var=; unset $ac_var;} ;;
+ esac ;;
+ esac
+ done
+ (set) 2>&1 |
+ case $as_nl`(ac_space='\'' '\''; set) 2>&1` in #(
+ *${as_nl}ac_space=\ *)
+ sed -n \
+ "s/'\''/'\''\\\\'\'''\''/g;
+ s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\''\\2'\''/p"
+ ;; #(
+ *)
+ sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p"
+ ;;
+ esac |
+ sort
+)
+ echo
+
+ printf "%s\n" "## ----------------- ##
+## Output variables. ##
+## ----------------- ##"
+ echo
+ for ac_var in $ac_subst_vars
+ do
+ eval ac_val=\$$ac_var
+ case $ac_val in
+ *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;;
+ esac
+ printf "%s\n" "$ac_var='\''$ac_val'\''"
+ done | sort
+ echo
+
+ if test -n "$ac_subst_files"; then
+ printf "%s\n" "## ------------------- ##
+## File substitutions. ##
+## ------------------- ##"
+ echo
+ for ac_var in $ac_subst_files
+ do
+ eval ac_val=\$$ac_var
+ case $ac_val in
+ *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;;
+ esac
+ printf "%s\n" "$ac_var='\''$ac_val'\''"
+ done | sort
+ echo
+ fi
+
+ if test -s confdefs.h; then
+ printf "%s\n" "## ----------- ##
+## confdefs.h. ##
+## ----------- ##"
+ echo
+ cat confdefs.h
+ echo
+ fi
+ test "$ac_signal" != 0 &&
+ printf "%s\n" "$as_me: caught signal $ac_signal"
+ printf "%s\n" "$as_me: exit $exit_status"
+ } >&5
+ rm -f core *.core core.conftest.* &&
+ rm -f -r conftest* confdefs* conf$$* $ac_clean_files &&
+ exit $exit_status
+' 0
+for ac_signal in 1 2 13 15; do
+ trap 'ac_signal='$ac_signal'; as_fn_exit 1' $ac_signal
+done
+ac_signal=0
+
+# confdefs.h avoids OS command line length limits that DEFS can exceed.
+rm -f -r conftest* confdefs.h
+
+printf "%s\n" "/* confdefs.h */" > confdefs.h
+
+# Predefined preprocessor variables.
+
+printf "%s\n" "#define PACKAGE_NAME \"$PACKAGE_NAME\"" >>confdefs.h
+
+printf "%s\n" "#define PACKAGE_TARNAME \"$PACKAGE_TARNAME\"" >>confdefs.h
+
+printf "%s\n" "#define PACKAGE_VERSION \"$PACKAGE_VERSION\"" >>confdefs.h
+
+printf "%s\n" "#define PACKAGE_STRING \"$PACKAGE_STRING\"" >>confdefs.h
+
+printf "%s\n" "#define PACKAGE_BUGREPORT \"$PACKAGE_BUGREPORT\"" >>confdefs.h
+
+printf "%s\n" "#define PACKAGE_URL \"$PACKAGE_URL\"" >>confdefs.h
+
+
+# Let the site file select an alternate cache file if it wants to.
+# Prefer an explicitly selected file to automatically selected ones.
+if test -n "$CONFIG_SITE"; then
+ ac_site_files="$CONFIG_SITE"
+elif test "x$prefix" != xNONE; then
+ ac_site_files="$prefix/share/config.site $prefix/etc/config.site"
+else
+ ac_site_files="$ac_default_prefix/share/config.site $ac_default_prefix/etc/config.site"
+fi
+
+for ac_site_file in $ac_site_files
+do
+ case $ac_site_file in #(
+ */*) :
+ ;; #(
+ *) :
+ ac_site_file=./$ac_site_file ;;
+esac
+ if test -f "$ac_site_file" && test -r "$ac_site_file"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5
+printf "%s\n" "$as_me: loading site script $ac_site_file" >&6;}
+ sed 's/^/| /' "$ac_site_file" >&5
+ . "$ac_site_file" \
+ || { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "failed to load site script $ac_site_file
+See \`config.log' for more details" "$LINENO" 5; }
+ fi
+done
+
+if test -r "$cache_file"; then
+ # Some versions of bash will fail to source /dev/null (special files
+ # actually), so we avoid doing that. DJGPP emulates it as a regular file.
+ if test /dev/null != "$cache_file" && test -f "$cache_file"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5
+printf "%s\n" "$as_me: loading cache $cache_file" >&6;}
+ case $cache_file in
+ [\\/]* | ?:[\\/]* ) . "$cache_file";;
+ *) . "./$cache_file";;
+ esac
+ fi
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5
+printf "%s\n" "$as_me: creating cache $cache_file" >&6;}
+ >$cache_file
+fi
+
+as_fn_append ac_header_c_list " stdio.h stdio_h HAVE_STDIO_H"
+# Test code for whether the C compiler supports C89 (global declarations)
+ac_c_conftest_c89_globals='
+/* Does the compiler advertise C89 conformance?
+ Do not test the value of __STDC__, because some compilers set it to 0
+ while being otherwise adequately conformant. */
+#if !defined __STDC__
+# error "Compiler does not advertise C89 conformance"
+#endif
+
+#include <stddef.h>
+#include <stdarg.h>
+struct stat;
+/* Most of the following tests are stolen from RCS 5.7 src/conf.sh. */
+struct buf { int x; };
+struct buf * (*rcsopen) (struct buf *, struct stat *, int);
+static char *e (p, i)
+ char **p;
+ int i;
+{
+ return p[i];
+}
+static char *f (char * (*g) (char **, int), char **p, ...)
+{
+ char *s;
+ va_list v;
+ va_start (v,p);
+ s = g (p, va_arg (v,int));
+ va_end (v);
+ return s;
+}
+
+/* OSF 4.0 Compaq cc is some sort of almost-ANSI by default. It has
+ function prototypes and stuff, but not \xHH hex character constants.
+ These do not provoke an error unfortunately, instead are silently treated
+ as an "x". The following induces an error, until -std is added to get
+ proper ANSI mode. Curiously \x00 != x always comes out true, for an
+ array size at least. It is necessary to write \x00 == 0 to get something
+ that is true only with -std. */
+int osf4_cc_array ['\''\x00'\'' == 0 ? 1 : -1];
+
+/* IBM C 6 for AIX is almost-ANSI by default, but it replaces macro parameters
+ inside strings and character constants. */
+#define FOO(x) '\''x'\''
+int xlc6_cc_array[FOO(a) == '\''x'\'' ? 1 : -1];
+
+int test (int i, double x);
+struct s1 {int (*f) (int a);};
+struct s2 {int (*f) (double a);};
+int pairnames (int, char **, int *(*)(struct buf *, struct stat *, int),
+ int, int);'
+
+# Test code for whether the C compiler supports C89 (body of main).
+ac_c_conftest_c89_main='
+ok |= (argc == 0 || f (e, argv, 0) != argv[0] || f (e, argv, 1) != argv[1]);
+'
+
+# Test code for whether the C compiler supports C99 (global declarations)
+ac_c_conftest_c99_globals='
+// Does the compiler advertise C99 conformance?
+#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 199901L
+# error "Compiler does not advertise C99 conformance"
+#endif
+
+#include <stdbool.h>
+extern int puts (const char *);
+extern int printf (const char *, ...);
+extern int dprintf (int, const char *, ...);
+extern void *malloc (size_t);
+
+// Check varargs macros. These examples are taken from C99 6.10.3.5.
+// dprintf is used instead of fprintf to avoid needing to declare
+// FILE and stderr.
+#define debug(...) dprintf (2, __VA_ARGS__)
+#define showlist(...) puts (#__VA_ARGS__)
+#define report(test,...) ((test) ? puts (#test) : printf (__VA_ARGS__))
+static void
+test_varargs_macros (void)
+{
+ int x = 1234;
+ int y = 5678;
+ debug ("Flag");
+ debug ("X = %d\n", x);
+ showlist (The first, second, and third items.);
+ report (x>y, "x is %d but y is %d", x, y);
+}
+
+// Check long long types.
+#define BIG64 18446744073709551615ull
+#define BIG32 4294967295ul
+#define BIG_OK (BIG64 / BIG32 == 4294967297ull && BIG64 % BIG32 == 0)
+#if !BIG_OK
+ #error "your preprocessor is broken"
+#endif
+#if BIG_OK
+#else
+ #error "your preprocessor is broken"
+#endif
+static long long int bignum = -9223372036854775807LL;
+static unsigned long long int ubignum = BIG64;
+
+struct incomplete_array
+{
+ int datasize;
+ double data[];
+};
+
+struct named_init {
+ int number;
+ const wchar_t *name;
+ double average;
+};
+
+typedef const char *ccp;
+
+static inline int
+test_restrict (ccp restrict text)
+{
+ // See if C++-style comments work.
+ // Iterate through items via the restricted pointer.
+ // Also check for declarations in for loops.
+ for (unsigned int i = 0; *(text+i) != '\''\0'\''; ++i)
+ continue;
+ return 0;
+}
+
+// Check varargs and va_copy.
+static bool
+test_varargs (const char *format, ...)
+{
+ va_list args;
+ va_start (args, format);
+ va_list args_copy;
+ va_copy (args_copy, args);
+
+ const char *str = "";
+ int number = 0;
+ float fnumber = 0;
+
+ while (*format)
+ {
+ switch (*format++)
+ {
+ case '\''s'\'': // string
+ str = va_arg (args_copy, const char *);
+ break;
+ case '\''d'\'': // int
+ number = va_arg (args_copy, int);
+ break;
+ case '\''f'\'': // float
+ fnumber = va_arg (args_copy, double);
+ break;
+ default:
+ break;
+ }
+ }
+ va_end (args_copy);
+ va_end (args);
+
+ return *str && number && fnumber;
+}
+'
+
+# Test code for whether the C compiler supports C99 (body of main).
+ac_c_conftest_c99_main='
+ // Check bool.
+ _Bool success = false;
+ success |= (argc != 0);
+
+ // Check restrict.
+ if (test_restrict ("String literal") == 0)
+ success = true;
+ char *restrict newvar = "Another string";
+
+ // Check varargs.
+ success &= test_varargs ("s, d'\'' f .", "string", 65, 34.234);
+ test_varargs_macros ();
+
+ // Check flexible array members.
+ struct incomplete_array *ia =
+ malloc (sizeof (struct incomplete_array) + (sizeof (double) * 10));
+ ia->datasize = 10;
+ for (int i = 0; i < ia->datasize; ++i)
+ ia->data[i] = i * 1.234;
+
+ // Check named initializers.
+ struct named_init ni = {
+ .number = 34,
+ .name = L"Test wide string",
+ .average = 543.34343,
+ };
+
+ ni.number = 58;
+
+ int dynamic_array[ni.number];
+ dynamic_array[0] = argv[0][0];
+ dynamic_array[ni.number - 1] = 543;
+
+ // work around unused variable warnings
+ ok |= (!success || bignum == 0LL || ubignum == 0uLL || newvar[0] == '\''x'\''
+ || dynamic_array[ni.number - 1] != 543);
+'
+
+# Test code for whether the C compiler supports C11 (global declarations)
+ac_c_conftest_c11_globals='
+// Does the compiler advertise C11 conformance?
+#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112L
+# error "Compiler does not advertise C11 conformance"
+#endif
+
+// Check _Alignas.
+char _Alignas (double) aligned_as_double;
+char _Alignas (0) no_special_alignment;
+extern char aligned_as_int;
+char _Alignas (0) _Alignas (int) aligned_as_int;
+
+// Check _Alignof.
+enum
+{
+ int_alignment = _Alignof (int),
+ int_array_alignment = _Alignof (int[100]),
+ char_alignment = _Alignof (char)
+};
+_Static_assert (0 < -_Alignof (int), "_Alignof is signed");
+
+// Check _Noreturn.
+int _Noreturn does_not_return (void) { for (;;) continue; }
+
+// Check _Static_assert.
+struct test_static_assert
+{
+ int x;
+ _Static_assert (sizeof (int) <= sizeof (long int),
+ "_Static_assert does not work in struct");
+ long int y;
+};
+
+// Check UTF-8 literals.
+#define u8 syntax error!
+char const utf8_literal[] = u8"happens to be ASCII" "another string";
+
+// Check duplicate typedefs.
+typedef long *long_ptr;
+typedef long int *long_ptr;
+typedef long_ptr long_ptr;
+
+// Anonymous structures and unions -- taken from C11 6.7.2.1 Example 1.
+struct anonymous
+{
+ union {
+ struct { int i; int j; };
+ struct { int k; long int l; } w;
+ };
+ int m;
+} v1;
+'
+
+# Test code for whether the C compiler supports C11 (body of main).
+ac_c_conftest_c11_main='
+ _Static_assert ((offsetof (struct anonymous, i)
+ == offsetof (struct anonymous, w.k)),
+ "Anonymous union alignment botch");
+ v1.i = 2;
+ v1.w.k = 5;
+ ok |= v1.i != 5;
+'
+
+# Test code for whether the C compiler supports C11 (complete).
+ac_c_conftest_c11_program="${ac_c_conftest_c89_globals}
+${ac_c_conftest_c99_globals}
+${ac_c_conftest_c11_globals}
+
+int
+main (int argc, char **argv)
+{
+ int ok = 0;
+ ${ac_c_conftest_c89_main}
+ ${ac_c_conftest_c99_main}
+ ${ac_c_conftest_c11_main}
+ return ok;
+}
+"
+
+# Test code for whether the C compiler supports C99 (complete).
+ac_c_conftest_c99_program="${ac_c_conftest_c89_globals}
+${ac_c_conftest_c99_globals}
+
+int
+main (int argc, char **argv)
+{
+ int ok = 0;
+ ${ac_c_conftest_c89_main}
+ ${ac_c_conftest_c99_main}
+ return ok;
+}
+"
+
+# Test code for whether the C compiler supports C89 (complete).
+ac_c_conftest_c89_program="${ac_c_conftest_c89_globals}
+
+int
+main (int argc, char **argv)
+{
+ int ok = 0;
+ ${ac_c_conftest_c89_main}
+ return ok;
+}
+"
+
+as_fn_append ac_header_c_list " stdlib.h stdlib_h HAVE_STDLIB_H"
+as_fn_append ac_header_c_list " string.h string_h HAVE_STRING_H"
+as_fn_append ac_header_c_list " inttypes.h inttypes_h HAVE_INTTYPES_H"
+as_fn_append ac_header_c_list " stdint.h stdint_h HAVE_STDINT_H"
+as_fn_append ac_header_c_list " strings.h strings_h HAVE_STRINGS_H"
+as_fn_append ac_header_c_list " sys/stat.h sys_stat_h HAVE_SYS_STAT_H"
+as_fn_append ac_header_c_list " sys/types.h sys_types_h HAVE_SYS_TYPES_H"
+as_fn_append ac_header_c_list " unistd.h unistd_h HAVE_UNISTD_H"
+as_fn_append ac_header_c_list " wchar.h wchar_h HAVE_WCHAR_H"
+as_fn_append ac_header_c_list " minix/config.h minix_config_h HAVE_MINIX_CONFIG_H"
+
+# Auxiliary files required by this configure script.
+ac_aux_files="compile missing install-sh"
+
+# Locations in which to look for auxiliary files.
+ac_aux_dir_candidates="${srcdir}/tools"
+
+# Search for a directory containing all of the required auxiliary files,
+# $ac_aux_files, from the $PATH-style list $ac_aux_dir_candidates.
+# If we don't find one directory that contains all the files we need,
+# we report the set of missing files from the *first* directory in
+# $ac_aux_dir_candidates and give up.
+ac_missing_aux_files=""
+ac_first_candidate=:
+printf "%s\n" "$as_me:${as_lineno-$LINENO}: looking for aux files: $ac_aux_files" >&5
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+as_found=false
+for as_dir in $ac_aux_dir_candidates
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ as_found=:
+
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: trying $as_dir" >&5
+ ac_aux_dir_found=yes
+ ac_install_sh=
+ for ac_aux in $ac_aux_files
+ do
+ # As a special case, if "install-sh" is required, that requirement
+ # can be satisfied by any of "install-sh", "install.sh", or "shtool",
+ # and $ac_install_sh is set appropriately for whichever one is found.
+ if test x"$ac_aux" = x"install-sh"
+ then
+ if test -f "${as_dir}install-sh"; then
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}install-sh found" >&5
+ ac_install_sh="${as_dir}install-sh -c"
+ elif test -f "${as_dir}install.sh"; then
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}install.sh found" >&5
+ ac_install_sh="${as_dir}install.sh -c"
+ elif test -f "${as_dir}shtool"; then
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}shtool found" >&5
+ ac_install_sh="${as_dir}shtool install -c"
+ else
+ ac_aux_dir_found=no
+ if $ac_first_candidate; then
+ ac_missing_aux_files="${ac_missing_aux_files} install-sh"
+ else
+ break
+ fi
+ fi
+ else
+ if test -f "${as_dir}${ac_aux}"; then
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}${ac_aux} found" >&5
+ else
+ ac_aux_dir_found=no
+ if $ac_first_candidate; then
+ ac_missing_aux_files="${ac_missing_aux_files} ${ac_aux}"
+ else
+ break
+ fi
+ fi
+ fi
+ done
+ if test "$ac_aux_dir_found" = yes; then
+ ac_aux_dir="$as_dir"
+ break
+ fi
+ ac_first_candidate=false
+
+ as_found=false
+done
+IFS=$as_save_IFS
+if $as_found
+then :
+
+else $as_nop
+ as_fn_error $? "cannot find required auxiliary files:$ac_missing_aux_files" "$LINENO" 5
+fi
+
+
+# These three variables are undocumented and unsupported,
+# and are intended to be withdrawn in a future Autoconf release.
+# They can cause serious problems if a builder's source tree is in a directory
+# whose full name contains unusual characters.
+if test -f "${ac_aux_dir}config.guess"; then
+ ac_config_guess="$SHELL ${ac_aux_dir}config.guess"
+fi
+if test -f "${ac_aux_dir}config.sub"; then
+ ac_config_sub="$SHELL ${ac_aux_dir}config.sub"
+fi
+if test -f "$ac_aux_dir/configure"; then
+ ac_configure="$SHELL ${ac_aux_dir}configure"
+fi
+
+# Check that the precious variables saved in the cache have kept the same
+# value.
+ac_cache_corrupted=false
+for ac_var in $ac_precious_vars; do
+ eval ac_old_set=\$ac_cv_env_${ac_var}_set
+ eval ac_new_set=\$ac_env_${ac_var}_set
+ eval ac_old_val=\$ac_cv_env_${ac_var}_value
+ eval ac_new_val=\$ac_env_${ac_var}_value
+ case $ac_old_set,$ac_new_set in
+ set,)
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&5
+printf "%s\n" "$as_me: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&2;}
+ ac_cache_corrupted=: ;;
+ ,set)
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was not set in the previous run" >&5
+printf "%s\n" "$as_me: error: \`$ac_var' was not set in the previous run" >&2;}
+ ac_cache_corrupted=: ;;
+ ,);;
+ *)
+ if test "x$ac_old_val" != "x$ac_new_val"; then
+ # differences in whitespace do not lead to failure.
+ ac_old_val_w=`echo x $ac_old_val`
+ ac_new_val_w=`echo x $ac_new_val`
+ if test "$ac_old_val_w" != "$ac_new_val_w"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' has changed since the previous run:" >&5
+printf "%s\n" "$as_me: error: \`$ac_var' has changed since the previous run:" >&2;}
+ ac_cache_corrupted=:
+ else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&5
+printf "%s\n" "$as_me: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&2;}
+ eval $ac_var=\$ac_old_val
+ fi
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: former value: \`$ac_old_val'" >&5
+printf "%s\n" "$as_me: former value: \`$ac_old_val'" >&2;}
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: current value: \`$ac_new_val'" >&5
+printf "%s\n" "$as_me: current value: \`$ac_new_val'" >&2;}
+ fi;;
+ esac
+ # Pass precious variables to config.status.
+ if test "$ac_new_set" = set; then
+ case $ac_new_val in
+ *\'*) ac_arg=$ac_var=`printf "%s\n" "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;;
+ *) ac_arg=$ac_var=$ac_new_val ;;
+ esac
+ case " $ac_configure_args " in
+ *" '$ac_arg' "*) ;; # Avoid dups. Use of quotes ensures accuracy.
+ *) as_fn_append ac_configure_args " '$ac_arg'" ;;
+ esac
+ fi
+done
+if $ac_cache_corrupted; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5
+printf "%s\n" "$as_me: error: changes in the environment can compromise the build" >&2;}
+ as_fn_error $? "run \`${MAKE-make} distclean' and/or \`rm $cache_file'
+ and start over" "$LINENO" 5
+fi
+## -------------------- ##
+## Main body of script. ##
+## -------------------- ##
+
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+
+
+
+ac_config_headers="$ac_config_headers config.h"
+
+
+
+am__api_version='1.16'
+
+
+
+ # Find a good install program. We prefer a C program (faster),
+# so one script is as good as another. But avoid the broken or
+# incompatible versions:
+# SysV /etc/install, /usr/sbin/install
+# SunOS /usr/etc/install
+# IRIX /sbin/install
+# AIX /bin/install
+# AmigaOS /C/install, which installs bootblocks on floppy discs
+# AIX 4 /usr/bin/installbsd, which doesn't work without a -g flag
+# AFS /usr/afsws/bin/install, which mishandles nonexistent args
+# SVR4 /usr/ucb/install, which tries to use the nonexistent group "staff"
+# OS/2's system install, which has a completely different semantic
+# ./install, which can be erroneously created by make from ./install.sh.
+# Reject install programs that cannot install multiple files.
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5
+printf %s "checking for a BSD-compatible install... " >&6; }
+if test -z "$INSTALL"; then
+if test ${ac_cv_path_install+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ # Account for fact that we put trailing slashes in our PATH walk.
+case $as_dir in #((
+ ./ | /[cC]/* | \
+ /etc/* | /usr/sbin/* | /usr/etc/* | /sbin/* | /usr/afsws/bin/* | \
+ ?:[\\/]os2[\\/]install[\\/]* | ?:[\\/]OS2[\\/]INSTALL[\\/]* | \
+ /usr/ucb/* ) ;;
+ *)
+ # OSF1 and SCO ODT 3.0 have their own names for install.
+ # Don't use installbsd from OSF since it installs stuff as root
+ # by default.
+ for ac_prog in ginstall scoinst install; do
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_prog$ac_exec_ext"; then
+ if test $ac_prog = install &&
+ grep dspmsg "$as_dir$ac_prog$ac_exec_ext" >/dev/null 2>&1; then
+ # AIX install. It has an incompatible calling convention.
+ :
+ elif test $ac_prog = install &&
+ grep pwplus "$as_dir$ac_prog$ac_exec_ext" >/dev/null 2>&1; then
+ # program-specific install script used by HP pwplus--don't use.
+ :
+ else
+ rm -rf conftest.one conftest.two conftest.dir
+ echo one > conftest.one
+ echo two > conftest.two
+ mkdir conftest.dir
+ if "$as_dir$ac_prog$ac_exec_ext" -c conftest.one conftest.two "`pwd`/conftest.dir/" &&
+ test -s conftest.one && test -s conftest.two &&
+ test -s conftest.dir/conftest.one &&
+ test -s conftest.dir/conftest.two
+ then
+ ac_cv_path_install="$as_dir$ac_prog$ac_exec_ext -c"
+ break 3
+ fi
+ fi
+ fi
+ done
+ done
+ ;;
+esac
+
+ done
+IFS=$as_save_IFS
+
+rm -rf conftest.one conftest.two conftest.dir
+
+fi
+ if test ${ac_cv_path_install+y}; then
+ INSTALL=$ac_cv_path_install
+ else
+ # As a last resort, use the slow shell script. Don't cache a
+ # value for INSTALL within a source directory, because that will
+ # break other packages using the cache if that directory is
+ # removed, or if the value is a relative name.
+ INSTALL=$ac_install_sh
+ fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5
+printf "%s\n" "$INSTALL" >&6; }
+
+# Use test -z because SunOS4 sh mishandles braces in ${var-val}.
+# It thinks the first close brace ends the variable substitution.
+test -z "$INSTALL_PROGRAM" && INSTALL_PROGRAM='${INSTALL}'
+
+test -z "$INSTALL_SCRIPT" && INSTALL_SCRIPT='${INSTALL}'
+
+test -z "$INSTALL_DATA" && INSTALL_DATA='${INSTALL} -m 644'
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether build environment is sane" >&5
+printf %s "checking whether build environment is sane... " >&6; }
+# Reject unsafe characters in $srcdir or the absolute working directory
+# name. Accept space and tab only in the latter.
+am_lf='
+'
+case `pwd` in
+ *[\\\"\#\$\&\'\`$am_lf]*)
+ as_fn_error $? "unsafe absolute working directory name" "$LINENO" 5;;
+esac
+case $srcdir in
+ *[\\\"\#\$\&\'\`$am_lf\ \ ]*)
+ as_fn_error $? "unsafe srcdir value: '$srcdir'" "$LINENO" 5;;
+esac
+
+# Do 'set' in a subshell so we don't clobber the current shell's
+# arguments. Must try -L first in case configure is actually a
+# symlink; some systems play weird games with the mod time of symlinks
+# (eg FreeBSD returns the mod time of the symlink's containing
+# directory).
+if (
+ am_has_slept=no
+ for am_try in 1 2; do
+ echo "timestamp, slept: $am_has_slept" > conftest.file
+ set X `ls -Lt "$srcdir/configure" conftest.file 2> /dev/null`
+ if test "$*" = "X"; then
+ # -L didn't work.
+ set X `ls -t "$srcdir/configure" conftest.file`
+ fi
+ if test "$*" != "X $srcdir/configure conftest.file" \
+ && test "$*" != "X conftest.file $srcdir/configure"; then
+
+ # If neither matched, then we have a broken ls. This can happen
+ # if, for instance, CONFIG_SHELL is bash and it inherits a
+ # broken ls alias from the environment. This has actually
+ # happened. Such a system could not be considered "sane".
+ as_fn_error $? "ls -t appears to fail. Make sure there is not a broken
+ alias in your environment" "$LINENO" 5
+ fi
+ if test "$2" = conftest.file || test $am_try -eq 2; then
+ break
+ fi
+ # Just in case.
+ sleep 1
+ am_has_slept=yes
+ done
+ test "$2" = conftest.file
+ )
+then
+ # Ok.
+ :
+else
+ as_fn_error $? "newly created file is older than distributed files!
+Check your system clock" "$LINENO" 5
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+# If we didn't sleep, we still need to ensure time stamps of config.status and
+# generated files are strictly newer.
+am_sleep_pid=
+if grep 'slept: no' conftest.file >/dev/null 2>&1; then
+ ( sleep 1 ) &
+ am_sleep_pid=$!
+fi
+
+rm -f conftest.file
+
+test "$program_prefix" != NONE &&
+ program_transform_name="s&^&$program_prefix&;$program_transform_name"
+# Use a double $ so make ignores it.
+test "$program_suffix" != NONE &&
+ program_transform_name="s&\$&$program_suffix&;$program_transform_name"
+# Double any \ or $.
+# By default was `s,x,x', remove it if useless.
+ac_script='s/[\\$]/&&/g;s/;s,x,x,$//'
+program_transform_name=`printf "%s\n" "$program_transform_name" | sed "$ac_script"`
+
+
+# Expand $ac_aux_dir to an absolute path.
+am_aux_dir=`cd "$ac_aux_dir" && pwd`
+
+
+ if test x"${MISSING+set}" != xset; then
+ MISSING="\${SHELL} '$am_aux_dir/missing'"
+fi
+# Use eval to expand $SHELL
+if eval "$MISSING --is-lightweight"; then
+ am_missing_run="$MISSING "
+else
+ am_missing_run=
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: 'missing' script is too old or missing" >&5
+printf "%s\n" "$as_me: WARNING: 'missing' script is too old or missing" >&2;}
+fi
+
+if test x"${install_sh+set}" != xset; then
+ case $am_aux_dir in
+ *\ * | *\ *)
+ install_sh="\${SHELL} '$am_aux_dir/install-sh'" ;;
+ *)
+ install_sh="\${SHELL} $am_aux_dir/install-sh"
+ esac
+fi
+
+# Installed binaries are usually stripped using 'strip' when the user
+# run "make install-strip". However 'strip' might not be the right
+# tool to use in cross-compilation environments, therefore Automake
+# will honor the 'STRIP' environment variable to overrule this program.
+if test "$cross_compiling" != no; then
+ if test -n "$ac_tool_prefix"; then
+ # Extract the first word of "${ac_tool_prefix}strip", so it can be a program name with args.
+set dummy ${ac_tool_prefix}strip; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_STRIP+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$STRIP"; then
+ ac_cv_prog_STRIP="$STRIP" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_STRIP="${ac_tool_prefix}strip"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+STRIP=$ac_cv_prog_STRIP
+if test -n "$STRIP"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $STRIP" >&5
+printf "%s\n" "$STRIP" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_STRIP"; then
+ ac_ct_STRIP=$STRIP
+ # Extract the first word of "strip", so it can be a program name with args.
+set dummy strip; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_STRIP+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$ac_ct_STRIP"; then
+ ac_cv_prog_ac_ct_STRIP="$ac_ct_STRIP" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_ac_ct_STRIP="strip"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_STRIP=$ac_cv_prog_ac_ct_STRIP
+if test -n "$ac_ct_STRIP"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_STRIP" >&5
+printf "%s\n" "$ac_ct_STRIP" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+ if test "x$ac_ct_STRIP" = x; then
+ STRIP=":"
+ else
+ case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+ STRIP=$ac_ct_STRIP
+ fi
+else
+ STRIP="$ac_cv_prog_STRIP"
+fi
+
+fi
+INSTALL_STRIP_PROGRAM="\$(install_sh) -c -s"
+
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for a race-free mkdir -p" >&5
+printf %s "checking for a race-free mkdir -p... " >&6; }
+if test -z "$MKDIR_P"; then
+ if test ${ac_cv_path_mkdir+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH$PATH_SEPARATOR/opt/sfw/bin
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_prog in mkdir gmkdir; do
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ as_fn_executable_p "$as_dir$ac_prog$ac_exec_ext" || continue
+ case `"$as_dir$ac_prog$ac_exec_ext" --version 2>&1` in #(
+ 'mkdir ('*'coreutils) '* | \
+ 'BusyBox '* | \
+ 'mkdir (fileutils) '4.1*)
+ ac_cv_path_mkdir=$as_dir$ac_prog$ac_exec_ext
+ break 3;;
+ esac
+ done
+ done
+ done
+IFS=$as_save_IFS
+
+fi
+
+ test -d ./--version && rmdir ./--version
+ if test ${ac_cv_path_mkdir+y}; then
+ MKDIR_P="$ac_cv_path_mkdir -p"
+ else
+ # As a last resort, use the slow shell script. Don't cache a
+ # value for MKDIR_P within a source directory, because that will
+ # break other packages using the cache if that directory is
+ # removed, or if the value is a relative name.
+ MKDIR_P="$ac_install_sh -d"
+ fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $MKDIR_P" >&5
+printf "%s\n" "$MKDIR_P" >&6; }
+
+for ac_prog in gawk mawk nawk awk
+do
+ # Extract the first word of "$ac_prog", so it can be a program name with args.
+set dummy $ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_AWK+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$AWK"; then
+ ac_cv_prog_AWK="$AWK" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_AWK="$ac_prog"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+AWK=$ac_cv_prog_AWK
+if test -n "$AWK"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $AWK" >&5
+printf "%s\n" "$AWK" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+ test -n "$AWK" && break
+done
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} sets \$(MAKE)" >&5
+printf %s "checking whether ${MAKE-make} sets \$(MAKE)... " >&6; }
+set x ${MAKE-make}
+ac_make=`printf "%s\n" "$2" | sed 's/+/p/g; s/[^a-zA-Z0-9_]/_/g'`
+if eval test \${ac_cv_prog_make_${ac_make}_set+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ cat >conftest.make <<\_ACEOF
+SHELL = /bin/sh
+all:
+ @echo '@@@%%%=$(MAKE)=@@@%%%'
+_ACEOF
+# GNU make sometimes prints "make[1]: Entering ...", which would confuse us.
+case `${MAKE-make} -f conftest.make 2>/dev/null` in
+ *@@@%%%=?*=@@@%%%*)
+ eval ac_cv_prog_make_${ac_make}_set=yes;;
+ *)
+ eval ac_cv_prog_make_${ac_make}_set=no;;
+esac
+rm -f conftest.make
+fi
+if eval test \$ac_cv_prog_make_${ac_make}_set = yes; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+ SET_MAKE=
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ SET_MAKE="MAKE=${MAKE-make}"
+fi
+
+rm -rf .tst 2>/dev/null
+mkdir .tst 2>/dev/null
+if test -d .tst; then
+ am__leading_dot=.
+else
+ am__leading_dot=_
+fi
+rmdir .tst 2>/dev/null
+
+# Check whether --enable-silent-rules was given.
+if test ${enable_silent_rules+y}
+then :
+ enableval=$enable_silent_rules;
+fi
+
+case $enable_silent_rules in # (((
+ yes) AM_DEFAULT_VERBOSITY=0;;
+ no) AM_DEFAULT_VERBOSITY=1;;
+ *) AM_DEFAULT_VERBOSITY=1;;
+esac
+am_make=${MAKE-make}
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $am_make supports nested variables" >&5
+printf %s "checking whether $am_make supports nested variables... " >&6; }
+if test ${am_cv_make_support_nested_variables+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if printf "%s\n" 'TRUE=$(BAR$(V))
+BAR0=false
+BAR1=true
+V=1
+am__doit:
+ @$(TRUE)
+.PHONY: am__doit' | $am_make -f - >/dev/null 2>&1; then
+ am_cv_make_support_nested_variables=yes
+else
+ am_cv_make_support_nested_variables=no
+fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_make_support_nested_variables" >&5
+printf "%s\n" "$am_cv_make_support_nested_variables" >&6; }
+if test $am_cv_make_support_nested_variables = yes; then
+ AM_V='$(V)'
+ AM_DEFAULT_V='$(AM_DEFAULT_VERBOSITY)'
+else
+ AM_V=$AM_DEFAULT_VERBOSITY
+ AM_DEFAULT_V=$AM_DEFAULT_VERBOSITY
+fi
+AM_BACKSLASH='\'
+
+if test "`cd $srcdir && pwd`" != "`pwd`"; then
+ # Use -I$(srcdir) only when $(srcdir) != ., so that make's output
+ # is not polluted with repeated "-I."
+ am__isrc=' -I$(srcdir)'
+ # test to see if srcdir already configured
+ if test -f $srcdir/config.status; then
+ as_fn_error $? "source directory already configured; run \"make distclean\" there first" "$LINENO" 5
+ fi
+fi
+
+# test whether we have cygpath
+if test -z "$CYGPATH_W"; then
+ if (cygpath --version) >/dev/null 2>/dev/null; then
+ CYGPATH_W='cygpath -w'
+ else
+ CYGPATH_W=echo
+ fi
+fi
+
+
+# Define the identity of the package.
+ PACKAGE='cockpit'
+ VERSION='311'
+
+
+printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h
+
+
+printf "%s\n" "#define VERSION \"$VERSION\"" >>confdefs.h
+
+# Some tools Automake needs.
+
+ACLOCAL=${ACLOCAL-"${am_missing_run}aclocal-${am__api_version}"}
+
+
+AUTOCONF=${AUTOCONF-"${am_missing_run}autoconf"}
+
+
+AUTOMAKE=${AUTOMAKE-"${am_missing_run}automake-${am__api_version}"}
+
+
+AUTOHEADER=${AUTOHEADER-"${am_missing_run}autoheader"}
+
+
+MAKEINFO=${MAKEINFO-"${am_missing_run}makeinfo"}
+
+# For better backward compatibility. To be removed once Automake 1.9.x
+# dies out for good. For more background, see:
+# <https://lists.gnu.org/archive/html/automake/2012-07/msg00001.html>
+# <https://lists.gnu.org/archive/html/automake/2012-07/msg00014.html>
+mkdir_p='$(MKDIR_P)'
+
+# We need awk for the "check" target (and possibly the TAP driver). The
+# system "awk" is bad on some platforms.
+# Always define AMTAR for backward compatibility. Yes, it's still used
+# in the wild :-( We should find a proper way to deprecate it ...
+AMTAR='$${TAR-tar}'
+
+
+# We'll loop over all known methods to create a tar archive until one works.
+_am_tools='gnutar pax cpio none'
+
+am__tar='$${TAR-tar} chof - "$$tardir"' am__untar='$${TAR-tar} xf -'
+
+
+
+
+
+# Variables for tags utilities; see am/tags.am
+if test -z "$CTAGS"; then
+ CTAGS=ctags
+fi
+
+if test -z "$ETAGS"; then
+ ETAGS=etags
+fi
+
+if test -z "$CSCOPE"; then
+ CSCOPE=cscope
+fi
+
+
+
+# POSIX will say in a future version that running "rm -f" with no argument
+# is OK; and we want to be able to make that assumption in our Makefile
+# recipes. So use an aggressive probe to check that the usage we want is
+# actually supported "in the wild" to an acceptable degree.
+# See automake bug#10828.
+# To make any issue more visible, cause the running configure to be aborted
+# by default if the 'rm' program in use doesn't match our expectations; the
+# user can still override this though.
+if rm -f && rm -fr && rm -rf; then : OK; else
+ cat >&2 <<'END'
+Oops!
+
+Your 'rm' program seems unable to run without file operands specified
+on the command line, even when the '-f' option is present. This is contrary
+to the behaviour of most rm programs out there, and not conforming with
+the upcoming POSIX standard: <http://austingroupbugs.net/view.php?id=542>
+
+Please tell bug-automake@gnu.org about your system, including the value
+of your $PATH and any error possibly output before this message. This
+can help us improve future automake versions.
+
+END
+ if test x"$ACCEPT_INFERIOR_RM_PROGRAM" = x"yes"; then
+ echo 'Configuration will proceed anyway, since you have set the' >&2
+ echo 'ACCEPT_INFERIOR_RM_PROGRAM variable to "yes"' >&2
+ echo >&2
+ else
+ cat >&2 <<'END'
+Aborting the configuration process, to ensure you take notice of the issue.
+
+You can download and install GNU coreutils to get an 'rm' implementation
+that behaves properly: <https://www.gnu.org/software/coreutils/>.
+
+If you want to complete the configuration process using your problematic
+'rm' anyway, export the environment variable ACCEPT_INFERIOR_RM_PROGRAM
+to "yes", and re-run configure.
+
+END
+ as_fn_error $? "Your 'rm' program is bad, sorry." "$LINENO" 5
+ fi
+fi
+
+# we want tar-ustar to avoid introducing extra metadata (ctime, atime) which
+# only adds useless non-determinism to the result. we also want to sort.
+am__tar='tar --format=ustar --sort=name --owner=root:0 --group=root:0 -chf - "$$tardir"'
+
+
+
+
+
+
+
+
+
+
+DEPDIR="${am__leading_dot}deps"
+
+ac_config_commands="$ac_config_commands depfiles"
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} supports the include directive" >&5
+printf %s "checking whether ${MAKE-make} supports the include directive... " >&6; }
+cat > confinc.mk << 'END'
+am__doit:
+ @echo this is the am__doit target >confinc.out
+.PHONY: am__doit
+END
+am__include="#"
+am__quote=
+# BSD make does it like this.
+echo '.include "confinc.mk" # ignored' > confmf.BSD
+# Other make implementations (GNU, Solaris 10, AIX) do it like this.
+echo 'include confinc.mk # ignored' > confmf.GNU
+_am_result=no
+for s in GNU BSD; do
+ { echo "$as_me:$LINENO: ${MAKE-make} -f confmf.$s && cat confinc.out" >&5
+ (${MAKE-make} -f confmf.$s && cat confinc.out) >&5 2>&5
+ ac_status=$?
+ echo "$as_me:$LINENO: \$? = $ac_status" >&5
+ (exit $ac_status); }
+ case $?:`cat confinc.out 2>/dev/null` in #(
+ '0:this is the am__doit target') :
+ case $s in #(
+ BSD) :
+ am__include='.include' am__quote='"' ;; #(
+ *) :
+ am__include='include' am__quote='' ;;
+esac ;; #(
+ *) :
+ ;;
+esac
+ if test "$am__include" != "#"; then
+ _am_result="yes ($s style)"
+ break
+ fi
+done
+rm -f confinc.* confmf.*
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${_am_result}" >&5
+printf "%s\n" "${_am_result}" >&6; }
+
+# Check whether --enable-dependency-tracking was given.
+if test ${enable_dependency_tracking+y}
+then :
+ enableval=$enable_dependency_tracking;
+fi
+
+if test "x$enable_dependency_tracking" != xno; then
+ am_depcomp="$ac_aux_dir/depcomp"
+ AMDEPBACKSLASH='\'
+ am__nodep='_no'
+fi
+ if test "x$enable_dependency_tracking" != xno; then
+ AMDEP_TRUE=
+ AMDEP_FALSE='#'
+else
+ AMDEP_TRUE='#'
+ AMDEP_FALSE=
+fi
+
+
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+if test -n "$ac_tool_prefix"; then
+ # Extract the first word of "${ac_tool_prefix}gcc", so it can be a program name with args.
+set dummy ${ac_tool_prefix}gcc; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_CC+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$CC"; then
+ ac_cv_prog_CC="$CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_CC="${ac_tool_prefix}gcc"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+CC=$ac_cv_prog_CC
+if test -n "$CC"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5
+printf "%s\n" "$CC" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_CC"; then
+ ac_ct_CC=$CC
+ # Extract the first word of "gcc", so it can be a program name with args.
+set dummy gcc; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_CC+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$ac_ct_CC"; then
+ ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_ac_ct_CC="gcc"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_CC=$ac_cv_prog_ac_ct_CC
+if test -n "$ac_ct_CC"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5
+printf "%s\n" "$ac_ct_CC" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+ if test "x$ac_ct_CC" = x; then
+ CC=""
+ else
+ case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+ CC=$ac_ct_CC
+ fi
+else
+ CC="$ac_cv_prog_CC"
+fi
+
+if test -z "$CC"; then
+ if test -n "$ac_tool_prefix"; then
+ # Extract the first word of "${ac_tool_prefix}cc", so it can be a program name with args.
+set dummy ${ac_tool_prefix}cc; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_CC+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$CC"; then
+ ac_cv_prog_CC="$CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_CC="${ac_tool_prefix}cc"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+CC=$ac_cv_prog_CC
+if test -n "$CC"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5
+printf "%s\n" "$CC" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+ fi
+fi
+if test -z "$CC"; then
+ # Extract the first word of "cc", so it can be a program name with args.
+set dummy cc; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_CC+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$CC"; then
+ ac_cv_prog_CC="$CC" # Let the user override the test.
+else
+ ac_prog_rejected=no
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ if test "$as_dir$ac_word$ac_exec_ext" = "/usr/ucb/cc"; then
+ ac_prog_rejected=yes
+ continue
+ fi
+ ac_cv_prog_CC="cc"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+if test $ac_prog_rejected = yes; then
+ # We found a bogon in the path, so make sure we never use it.
+ set dummy $ac_cv_prog_CC
+ shift
+ if test $# != 0; then
+ # We chose a different compiler from the bogus one.
+ # However, it has the same basename, so the bogon will be chosen
+ # first if we set CC to just the basename; use the full file name.
+ shift
+ ac_cv_prog_CC="$as_dir$ac_word${1+' '}$@"
+ fi
+fi
+fi
+fi
+CC=$ac_cv_prog_CC
+if test -n "$CC"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5
+printf "%s\n" "$CC" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$CC"; then
+ if test -n "$ac_tool_prefix"; then
+ for ac_prog in cl.exe
+ do
+ # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args.
+set dummy $ac_tool_prefix$ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_CC+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$CC"; then
+ ac_cv_prog_CC="$CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_CC="$ac_tool_prefix$ac_prog"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+CC=$ac_cv_prog_CC
+if test -n "$CC"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5
+printf "%s\n" "$CC" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+ test -n "$CC" && break
+ done
+fi
+if test -z "$CC"; then
+ ac_ct_CC=$CC
+ for ac_prog in cl.exe
+do
+ # Extract the first word of "$ac_prog", so it can be a program name with args.
+set dummy $ac_prog; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_CC+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$ac_ct_CC"; then
+ ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_ac_ct_CC="$ac_prog"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_CC=$ac_cv_prog_ac_ct_CC
+if test -n "$ac_ct_CC"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5
+printf "%s\n" "$ac_ct_CC" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+ test -n "$ac_ct_CC" && break
+done
+
+ if test "x$ac_ct_CC" = x; then
+ CC=""
+ else
+ case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+ CC=$ac_ct_CC
+ fi
+fi
+
+fi
+if test -z "$CC"; then
+ if test -n "$ac_tool_prefix"; then
+ # Extract the first word of "${ac_tool_prefix}clang", so it can be a program name with args.
+set dummy ${ac_tool_prefix}clang; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_CC+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$CC"; then
+ ac_cv_prog_CC="$CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_CC="${ac_tool_prefix}clang"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+CC=$ac_cv_prog_CC
+if test -n "$CC"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $CC" >&5
+printf "%s\n" "$CC" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_CC"; then
+ ac_ct_CC=$CC
+ # Extract the first word of "clang", so it can be a program name with args.
+set dummy clang; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_CC+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$ac_ct_CC"; then
+ ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_ac_ct_CC="clang"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_CC=$ac_cv_prog_ac_ct_CC
+if test -n "$ac_ct_CC"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5
+printf "%s\n" "$ac_ct_CC" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+ if test "x$ac_ct_CC" = x; then
+ CC=""
+ else
+ case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+ CC=$ac_ct_CC
+ fi
+else
+ CC="$ac_cv_prog_CC"
+fi
+
+fi
+
+
+test -z "$CC" && { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "no acceptable C compiler found in \$PATH
+See \`config.log' for more details" "$LINENO" 5; }
+
+# Provide some information about the compiler.
+printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5
+set X $ac_compile
+ac_compiler=$2
+for ac_option in --version -v -V -qversion -version; do
+ { { ac_try="$ac_compiler $ac_option >&5"
+case "(($ac_try" in
+ *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+ *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+ (eval "$ac_compiler $ac_option >&5") 2>conftest.err
+ ac_status=$?
+ if test -s conftest.err; then
+ sed '10a\
+... rest of stderr output deleted ...
+ 10q' conftest.err >conftest.er1
+ cat conftest.er1 >&5
+ fi
+ rm -f conftest.er1 conftest.err
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }
+done
+
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+ac_clean_files_save=$ac_clean_files
+ac_clean_files="$ac_clean_files a.out a.out.dSYM a.exe b.out"
+# Try to create an executable without -o first, disregard a.out.
+# It will help us diagnose broken compilers, and finding out an intuition
+# of exeext.
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the C compiler works" >&5
+printf %s "checking whether the C compiler works... " >&6; }
+ac_link_default=`printf "%s\n" "$ac_link" | sed 's/ -o *conftest[^ ]*//'`
+
+# The possible output files:
+ac_files="a.out conftest.exe conftest a.exe a_out.exe b.out conftest.*"
+
+ac_rmfiles=
+for ac_file in $ac_files
+do
+ case $ac_file in
+ *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM | *.o | *.obj ) ;;
+ * ) ac_rmfiles="$ac_rmfiles $ac_file";;
+ esac
+done
+rm -f $ac_rmfiles
+
+if { { ac_try="$ac_link_default"
+case "(($ac_try" in
+ *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+ *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+ (eval "$ac_link_default") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }
+then :
+ # Autoconf-2.13 could set the ac_cv_exeext variable to `no'.
+# So ignore a value of `no', otherwise this would lead to `EXEEXT = no'
+# in a Makefile. We should not override ac_cv_exeext if it was cached,
+# so that the user can short-circuit this test for compilers unknown to
+# Autoconf.
+for ac_file in $ac_files ''
+do
+ test -f "$ac_file" || continue
+ case $ac_file in
+ *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM | *.o | *.obj )
+ ;;
+ [ab].out )
+ # We found the default executable, but exeext='' is most
+ # certainly right.
+ break;;
+ *.* )
+ if test ${ac_cv_exeext+y} && test "$ac_cv_exeext" != no;
+ then :; else
+ ac_cv_exeext=`expr "$ac_file" : '[^.]*\(\..*\)'`
+ fi
+ # We set ac_cv_exeext here because the later test for it is not
+ # safe: cross compilers may not add the suffix if given an `-o'
+ # argument, so we may need to know it at that point already.
+ # Even if this section looks crufty: it has the advantage of
+ # actually working.
+ break;;
+ * )
+ break;;
+ esac
+done
+test "$ac_cv_exeext" = no && ac_cv_exeext=
+
+else $as_nop
+ ac_file=''
+fi
+if test -z "$ac_file"
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+printf "%s\n" "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error 77 "C compiler cannot create executables
+See \`config.log' for more details" "$LINENO" 5; }
+else $as_nop
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name" >&5
+printf %s "checking for C compiler default output file name... " >&6; }
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_file" >&5
+printf "%s\n" "$ac_file" >&6; }
+ac_exeext=$ac_cv_exeext
+
+rm -f -r a.out a.out.dSYM a.exe conftest$ac_cv_exeext b.out
+ac_clean_files=$ac_clean_files_save
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of executables" >&5
+printf %s "checking for suffix of executables... " >&6; }
+if { { ac_try="$ac_link"
+case "(($ac_try" in
+ *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+ *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+ (eval "$ac_link") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }
+then :
+ # If both `conftest.exe' and `conftest' are `present' (well, observable)
+# catch `conftest.exe'. For instance with Cygwin, `ls conftest' will
+# work properly (i.e., refer to `conftest.exe'), while it won't with
+# `rm'.
+for ac_file in conftest.exe conftest conftest.*; do
+ test -f "$ac_file" || continue
+ case $ac_file in
+ *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM | *.o | *.obj ) ;;
+ *.* ) ac_cv_exeext=`expr "$ac_file" : '[^.]*\(\..*\)'`
+ break;;
+ * ) break;;
+ esac
+done
+else $as_nop
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "cannot compute suffix of executables: cannot compile and link
+See \`config.log' for more details" "$LINENO" 5; }
+fi
+rm -f conftest conftest$ac_cv_exeext
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext" >&5
+printf "%s\n" "$ac_cv_exeext" >&6; }
+
+rm -f conftest.$ac_ext
+EXEEXT=$ac_cv_exeext
+ac_exeext=$EXEEXT
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+#include <stdio.h>
+int
+main (void)
+{
+FILE *f = fopen ("conftest.out", "w");
+ return ferror (f) || fclose (f) != 0;
+
+ ;
+ return 0;
+}
+_ACEOF
+ac_clean_files="$ac_clean_files conftest.out"
+# Check that the compiler produces executables we can run. If not, either
+# the compiler is broken, or we cross compile.
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether we are cross compiling" >&5
+printf %s "checking whether we are cross compiling... " >&6; }
+if test "$cross_compiling" != yes; then
+ { { ac_try="$ac_link"
+case "(($ac_try" in
+ *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+ *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+ (eval "$ac_link") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }
+ if { ac_try='./conftest$ac_cv_exeext'
+ { { case "(($ac_try" in
+ *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+ *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+ (eval "$ac_try") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; }; then
+ cross_compiling=no
+ else
+ if test "$cross_compiling" = maybe; then
+ cross_compiling=yes
+ else
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error 77 "cannot run C compiled programs.
+If you meant to cross compile, use \`--host'.
+See \`config.log' for more details" "$LINENO" 5; }
+ fi
+ fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $cross_compiling" >&5
+printf "%s\n" "$cross_compiling" >&6; }
+
+rm -f conftest.$ac_ext conftest$ac_cv_exeext conftest.out
+ac_clean_files=$ac_clean_files_save
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for suffix of object files" >&5
+printf %s "checking for suffix of object files... " >&6; }
+if test ${ac_cv_objext+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+rm -f conftest.o conftest.obj
+if { { ac_try="$ac_compile"
+case "(($ac_try" in
+ *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;;
+ *) ac_try_echo=$ac_try;;
+esac
+eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\""
+printf "%s\n" "$ac_try_echo"; } >&5
+ (eval "$ac_compile") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }
+then :
+ for ac_file in conftest.o conftest.obj conftest.*; do
+ test -f "$ac_file" || continue;
+ case $ac_file in
+ *.$ac_ext | *.xcoff | *.tds | *.d | *.pdb | *.xSYM | *.bb | *.bbg | *.map | *.inf | *.dSYM ) ;;
+ *) ac_cv_objext=`expr "$ac_file" : '.*\.\(.*\)'`
+ break;;
+ esac
+done
+else $as_nop
+ printf "%s\n" "$as_me: failed program was:" >&5
+sed 's/^/| /' conftest.$ac_ext >&5
+
+{ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "cannot compute suffix of object files: cannot compile
+See \`config.log' for more details" "$LINENO" 5; }
+fi
+rm -f conftest.$ac_cv_objext conftest.$ac_ext
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext" >&5
+printf "%s\n" "$ac_cv_objext" >&6; }
+OBJEXT=$ac_cv_objext
+ac_objext=$OBJEXT
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the compiler supports GNU C" >&5
+printf %s "checking whether the compiler supports GNU C... " >&6; }
+if test ${ac_cv_c_compiler_gnu+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+int
+main (void)
+{
+#ifndef __GNUC__
+ choke me
+#endif
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_compiler_gnu=yes
+else $as_nop
+ ac_compiler_gnu=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+ac_cv_c_compiler_gnu=$ac_compiler_gnu
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5
+printf "%s\n" "$ac_cv_c_compiler_gnu" >&6; }
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+if test $ac_compiler_gnu = yes; then
+ GCC=yes
+else
+ GCC=
+fi
+ac_test_CFLAGS=${CFLAGS+y}
+ac_save_CFLAGS=$CFLAGS
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5
+printf %s "checking whether $CC accepts -g... " >&6; }
+if test ${ac_cv_prog_cc_g+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_save_c_werror_flag=$ac_c_werror_flag
+ ac_c_werror_flag=yes
+ ac_cv_prog_cc_g=no
+ CFLAGS="-g"
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_prog_cc_g=yes
+else $as_nop
+ CFLAGS=""
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+
+else $as_nop
+ ac_c_werror_flag=$ac_save_c_werror_flag
+ CFLAGS="-g"
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_prog_cc_g=yes
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+ ac_c_werror_flag=$ac_save_c_werror_flag
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5
+printf "%s\n" "$ac_cv_prog_cc_g" >&6; }
+if test $ac_test_CFLAGS; then
+ CFLAGS=$ac_save_CFLAGS
+elif test $ac_cv_prog_cc_g = yes; then
+ if test "$GCC" = yes; then
+ CFLAGS="-g -O2"
+ else
+ CFLAGS="-g"
+ fi
+else
+ if test "$GCC" = yes; then
+ CFLAGS="-O2"
+ else
+ CFLAGS=
+ fi
+fi
+ac_prog_cc_stdc=no
+if test x$ac_prog_cc_stdc = xno
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C11 features" >&5
+printf %s "checking for $CC option to enable C11 features... " >&6; }
+if test ${ac_cv_prog_cc_c11+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_cv_prog_cc_c11=no
+ac_save_CC=$CC
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+$ac_c_conftest_c11_program
+_ACEOF
+for ac_arg in '' -std=gnu11
+do
+ CC="$ac_save_CC $ac_arg"
+ if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_prog_cc_c11=$ac_arg
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam
+ test "x$ac_cv_prog_cc_c11" != "xno" && break
+done
+rm -f conftest.$ac_ext
+CC=$ac_save_CC
+fi
+
+if test "x$ac_cv_prog_cc_c11" = xno
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5
+printf "%s\n" "unsupported" >&6; }
+else $as_nop
+ if test "x$ac_cv_prog_cc_c11" = x
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5
+printf "%s\n" "none needed" >&6; }
+else $as_nop
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11" >&5
+printf "%s\n" "$ac_cv_prog_cc_c11" >&6; }
+ CC="$CC $ac_cv_prog_cc_c11"
+fi
+ ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c11
+ ac_prog_cc_stdc=c11
+fi
+fi
+if test x$ac_prog_cc_stdc = xno
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C99 features" >&5
+printf %s "checking for $CC option to enable C99 features... " >&6; }
+if test ${ac_cv_prog_cc_c99+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_cv_prog_cc_c99=no
+ac_save_CC=$CC
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+$ac_c_conftest_c99_program
+_ACEOF
+for ac_arg in '' -std=gnu99 -std=c99 -c99 -qlanglvl=extc1x -qlanglvl=extc99 -AC99 -D_STDC_C99=
+do
+ CC="$ac_save_CC $ac_arg"
+ if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_prog_cc_c99=$ac_arg
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam
+ test "x$ac_cv_prog_cc_c99" != "xno" && break
+done
+rm -f conftest.$ac_ext
+CC=$ac_save_CC
+fi
+
+if test "x$ac_cv_prog_cc_c99" = xno
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5
+printf "%s\n" "unsupported" >&6; }
+else $as_nop
+ if test "x$ac_cv_prog_cc_c99" = x
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5
+printf "%s\n" "none needed" >&6; }
+else $as_nop
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99" >&5
+printf "%s\n" "$ac_cv_prog_cc_c99" >&6; }
+ CC="$CC $ac_cv_prog_cc_c99"
+fi
+ ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c99
+ ac_prog_cc_stdc=c99
+fi
+fi
+if test x$ac_prog_cc_stdc = xno
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $CC option to enable C89 features" >&5
+printf %s "checking for $CC option to enable C89 features... " >&6; }
+if test ${ac_cv_prog_cc_c89+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_cv_prog_cc_c89=no
+ac_save_CC=$CC
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+$ac_c_conftest_c89_program
+_ACEOF
+for ac_arg in '' -qlanglvl=extc89 -qlanglvl=ansi -std -Ae "-Aa -D_HPUX_SOURCE" "-Xc -D__EXTENSIONS__"
+do
+ CC="$ac_save_CC $ac_arg"
+ if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_prog_cc_c89=$ac_arg
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam
+ test "x$ac_cv_prog_cc_c89" != "xno" && break
+done
+rm -f conftest.$ac_ext
+CC=$ac_save_CC
+fi
+
+if test "x$ac_cv_prog_cc_c89" = xno
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5
+printf "%s\n" "unsupported" >&6; }
+else $as_nop
+ if test "x$ac_cv_prog_cc_c89" = x
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: none needed" >&5
+printf "%s\n" "none needed" >&6; }
+else $as_nop
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5
+printf "%s\n" "$ac_cv_prog_cc_c89" >&6; }
+ CC="$CC $ac_cv_prog_cc_c89"
+fi
+ ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c89
+ ac_prog_cc_stdc=c89
+fi
+fi
+
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+
+ ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CC understands -c and -o together" >&5
+printf %s "checking whether $CC understands -c and -o together... " >&6; }
+if test ${am_cv_prog_cc_c_o+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+ # Make sure it works both with $CC and with simple cc.
+ # Following AC_PROG_CC_C_O, we do the test twice because some
+ # compilers refuse to overwrite an existing .o file with -o,
+ # though they will create one.
+ am_cv_prog_cc_c_o=yes
+ for am_i in 1 2; do
+ if { echo "$as_me:$LINENO: $CC -c conftest.$ac_ext -o conftest2.$ac_objext" >&5
+ ($CC -c conftest.$ac_ext -o conftest2.$ac_objext) >&5 2>&5
+ ac_status=$?
+ echo "$as_me:$LINENO: \$? = $ac_status" >&5
+ (exit $ac_status); } \
+ && test -f conftest2.$ac_objext; then
+ : OK
+ else
+ am_cv_prog_cc_c_o=no
+ break
+ fi
+ done
+ rm -f core conftest*
+ unset am_i
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_prog_cc_c_o" >&5
+printf "%s\n" "$am_cv_prog_cc_c_o" >&6; }
+if test "$am_cv_prog_cc_c_o" != yes; then
+ # Losing compiler, so override with the script.
+ # FIXME: It is wrong to rewrite CC.
+ # But if we don't then we get into trouble of one sort or another.
+ # A longer-term fix would be to have automake use am__CC in this case,
+ # and then we could set am__CC="\$(top_srcdir)/compile \$(CC)"
+ CC="$am_aux_dir/compile $CC"
+fi
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+
+depcc="$CC" am_compiler_list=
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking dependency style of $depcc" >&5
+printf %s "checking dependency style of $depcc... " >&6; }
+if test ${am_cv_CC_dependencies_compiler_type+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -z "$AMDEP_TRUE" && test -f "$am_depcomp"; then
+ # We make a subdir and do the tests there. Otherwise we can end up
+ # making bogus files that we don't know about and never remove. For
+ # instance it was reported that on HP-UX the gcc test will end up
+ # making a dummy file named 'D' -- because '-MD' means "put the output
+ # in D".
+ rm -rf conftest.dir
+ mkdir conftest.dir
+ # Copy depcomp to subdir because otherwise we won't find it if we're
+ # using a relative directory.
+ cp "$am_depcomp" conftest.dir
+ cd conftest.dir
+ # We will build objects and dependencies in a subdirectory because
+ # it helps to detect inapplicable dependency modes. For instance
+ # both Tru64's cc and ICC support -MD to output dependencies as a
+ # side effect of compilation, but ICC will put the dependencies in
+ # the current directory while Tru64 will put them in the object
+ # directory.
+ mkdir sub
+
+ am_cv_CC_dependencies_compiler_type=none
+ if test "$am_compiler_list" = ""; then
+ am_compiler_list=`sed -n 's/^#*\([a-zA-Z0-9]*\))$/\1/p' < ./depcomp`
+ fi
+ am__universal=false
+ case " $depcc " in #(
+ *\ -arch\ *\ -arch\ *) am__universal=true ;;
+ esac
+
+ for depmode in $am_compiler_list; do
+ # Setup a source with many dependencies, because some compilers
+ # like to wrap large dependency lists on column 80 (with \), and
+ # we should not choose a depcomp mode which is confused by this.
+ #
+ # We need to recreate these files for each test, as the compiler may
+ # overwrite some of them when testing with obscure command lines.
+ # This happens at least with the AIX C compiler.
+ : > sub/conftest.c
+ for i in 1 2 3 4 5 6; do
+ echo '#include "conftst'$i'.h"' >> sub/conftest.c
+ # Using ": > sub/conftst$i.h" creates only sub/conftst1.h with
+ # Solaris 10 /bin/sh.
+ echo '/* dummy */' > sub/conftst$i.h
+ done
+ echo "${am__include} ${am__quote}sub/conftest.Po${am__quote}" > confmf
+
+ # We check with '-c' and '-o' for the sake of the "dashmstdout"
+ # mode. It turns out that the SunPro C++ compiler does not properly
+ # handle '-M -o', and we need to detect this. Also, some Intel
+ # versions had trouble with output in subdirs.
+ am__obj=sub/conftest.${OBJEXT-o}
+ am__minus_obj="-o $am__obj"
+ case $depmode in
+ gcc)
+ # This depmode causes a compiler race in universal mode.
+ test "$am__universal" = false || continue
+ ;;
+ nosideeffect)
+ # After this tag, mechanisms are not by side-effect, so they'll
+ # only be used when explicitly requested.
+ if test "x$enable_dependency_tracking" = xyes; then
+ continue
+ else
+ break
+ fi
+ ;;
+ msvc7 | msvc7msys | msvisualcpp | msvcmsys)
+ # This compiler won't grok '-c -o', but also, the minuso test has
+ # not run yet. These depmodes are late enough in the game, and
+ # so weak that their functioning should not be impacted.
+ am__obj=conftest.${OBJEXT-o}
+ am__minus_obj=
+ ;;
+ none) break ;;
+ esac
+ if depmode=$depmode \
+ source=sub/conftest.c object=$am__obj \
+ depfile=sub/conftest.Po tmpdepfile=sub/conftest.TPo \
+ $SHELL ./depcomp $depcc -c $am__minus_obj sub/conftest.c \
+ >/dev/null 2>conftest.err &&
+ grep sub/conftst1.h sub/conftest.Po > /dev/null 2>&1 &&
+ grep sub/conftst6.h sub/conftest.Po > /dev/null 2>&1 &&
+ grep $am__obj sub/conftest.Po > /dev/null 2>&1 &&
+ ${MAKE-make} -s -f confmf > /dev/null 2>&1; then
+ # icc doesn't choke on unknown options, it will just issue warnings
+ # or remarks (even with -Werror). So we grep stderr for any message
+ # that says an option was ignored or not supported.
+ # When given -MP, icc 7.0 and 7.1 complain thusly:
+ # icc: Command line warning: ignoring option '-M'; no argument required
+ # The diagnosis changed in icc 8.0:
+ # icc: Command line remark: option '-MP' not supported
+ if (grep 'ignoring option' conftest.err ||
+ grep 'not supported' conftest.err) >/dev/null 2>&1; then :; else
+ am_cv_CC_dependencies_compiler_type=$depmode
+ break
+ fi
+ fi
+ done
+
+ cd ..
+ rm -rf conftest.dir
+else
+ am_cv_CC_dependencies_compiler_type=none
+fi
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_CC_dependencies_compiler_type" >&5
+printf "%s\n" "$am_cv_CC_dependencies_compiler_type" >&6; }
+CCDEPMODE=depmode=$am_cv_CC_dependencies_compiler_type
+
+ if
+ test "x$enable_dependency_tracking" != xno \
+ && test "$am_cv_CC_dependencies_compiler_type" = gcc3; then
+ am__fastdepCC_TRUE=
+ am__fastdepCC_FALSE='#'
+else
+ am__fastdepCC_TRUE='#'
+ am__fastdepCC_FALSE=
+fi
+
+
+
+ac_header= ac_cache=
+for ac_item in $ac_header_c_list
+do
+ if test $ac_cache; then
+ ac_fn_c_check_header_compile "$LINENO" $ac_header ac_cv_header_$ac_cache "$ac_includes_default"
+ if eval test \"x\$ac_cv_header_$ac_cache\" = xyes; then
+ printf "%s\n" "#define $ac_item 1" >> confdefs.h
+ fi
+ ac_header= ac_cache=
+ elif test $ac_header; then
+ ac_cache=$ac_item
+ else
+ ac_header=$ac_item
+ fi
+done
+
+
+
+
+
+
+
+
+if test $ac_cv_header_stdlib_h = yes && test $ac_cv_header_string_h = yes
+then :
+
+printf "%s\n" "#define STDC_HEADERS 1" >>confdefs.h
+
+fi
+
+
+
+
+
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether it is safe to define __EXTENSIONS__" >&5
+printf %s "checking whether it is safe to define __EXTENSIONS__... " >&6; }
+if test ${ac_cv_safe_to_define___extensions__+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+# define __EXTENSIONS__ 1
+ $ac_includes_default
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_safe_to_define___extensions__=yes
+else $as_nop
+ ac_cv_safe_to_define___extensions__=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_safe_to_define___extensions__" >&5
+printf "%s\n" "$ac_cv_safe_to_define___extensions__" >&6; }
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether _XOPEN_SOURCE should be defined" >&5
+printf %s "checking whether _XOPEN_SOURCE should be defined... " >&6; }
+if test ${ac_cv_should_define__xopen_source+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_cv_should_define__xopen_source=no
+ if test $ac_cv_header_wchar_h = yes
+then :
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+ #include <wchar.h>
+ mbstate_t x;
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+
+else $as_nop
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+ #define _XOPEN_SOURCE 500
+ #include <wchar.h>
+ mbstate_t x;
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_should_define__xopen_source=yes
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_should_define__xopen_source" >&5
+printf "%s\n" "$ac_cv_should_define__xopen_source" >&6; }
+
+ printf "%s\n" "#define _ALL_SOURCE 1" >>confdefs.h
+
+ printf "%s\n" "#define _DARWIN_C_SOURCE 1" >>confdefs.h
+
+ printf "%s\n" "#define _GNU_SOURCE 1" >>confdefs.h
+
+ printf "%s\n" "#define _HPUX_ALT_XOPEN_SOCKET_API 1" >>confdefs.h
+
+ printf "%s\n" "#define _NETBSD_SOURCE 1" >>confdefs.h
+
+ printf "%s\n" "#define _OPENBSD_SOURCE 1" >>confdefs.h
+
+ printf "%s\n" "#define _POSIX_PTHREAD_SEMANTICS 1" >>confdefs.h
+
+ printf "%s\n" "#define __STDC_WANT_IEC_60559_ATTRIBS_EXT__ 1" >>confdefs.h
+
+ printf "%s\n" "#define __STDC_WANT_IEC_60559_BFP_EXT__ 1" >>confdefs.h
+
+ printf "%s\n" "#define __STDC_WANT_IEC_60559_DFP_EXT__ 1" >>confdefs.h
+
+ printf "%s\n" "#define __STDC_WANT_IEC_60559_FUNCS_EXT__ 1" >>confdefs.h
+
+ printf "%s\n" "#define __STDC_WANT_IEC_60559_TYPES_EXT__ 1" >>confdefs.h
+
+ printf "%s\n" "#define __STDC_WANT_LIB_EXT2__ 1" >>confdefs.h
+
+ printf "%s\n" "#define __STDC_WANT_MATH_SPEC_FUNCS__ 1" >>confdefs.h
+
+ printf "%s\n" "#define _TANDEM_SOURCE 1" >>confdefs.h
+
+ if test $ac_cv_header_minix_config_h = yes
+then :
+ MINIX=yes
+ printf "%s\n" "#define _MINIX 1" >>confdefs.h
+
+ printf "%s\n" "#define _POSIX_SOURCE 1" >>confdefs.h
+
+ printf "%s\n" "#define _POSIX_1_SOURCE 2" >>confdefs.h
+
+else $as_nop
+ MINIX=
+fi
+ if test $ac_cv_safe_to_define___extensions__ = yes
+then :
+ printf "%s\n" "#define __EXTENSIONS__ 1" >>confdefs.h
+
+fi
+ if test $ac_cv_should_define__xopen_source = yes
+then :
+ printf "%s\n" "#define _XOPEN_SOURCE 500" >>confdefs.h
+
+fi
+
+# Check whether --enable-largefile was given.
+if test ${enable_largefile+y}
+then :
+ enableval=$enable_largefile;
+fi
+
+if test "$enable_largefile" != no; then
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for special C compiler options needed for large files" >&5
+printf %s "checking for special C compiler options needed for large files... " >&6; }
+if test ${ac_cv_sys_largefile_CC+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_cv_sys_largefile_CC=no
+ if test "$GCC" != yes; then
+ ac_save_CC=$CC
+ while :; do
+ # IRIX 6.2 and later do not support large files by default,
+ # so use the C compiler's -n32 option if that helps.
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+#include <sys/types.h>
+ /* Check that off_t can represent 2**63 - 1 correctly.
+ We can't simply define LARGE_OFF_T to be 9223372036854775807,
+ since some C++ compilers masquerading as C compilers
+ incorrectly reject 9223372036854775807. */
+#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31))
+ int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721
+ && LARGE_OFF_T % 2147483647 == 1)
+ ? 1 : -1];
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+ if ac_fn_c_try_compile "$LINENO"
+then :
+ break
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam
+ CC="$CC -n32"
+ if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_sys_largefile_CC=' -n32'; break
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam
+ break
+ done
+ CC=$ac_save_CC
+ rm -f conftest.$ac_ext
+ fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_largefile_CC" >&5
+printf "%s\n" "$ac_cv_sys_largefile_CC" >&6; }
+ if test "$ac_cv_sys_largefile_CC" != no; then
+ CC=$CC$ac_cv_sys_largefile_CC
+ fi
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for _FILE_OFFSET_BITS value needed for large files" >&5
+printf %s "checking for _FILE_OFFSET_BITS value needed for large files... " >&6; }
+if test ${ac_cv_sys_file_offset_bits+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ while :; do
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+#include <sys/types.h>
+ /* Check that off_t can represent 2**63 - 1 correctly.
+ We can't simply define LARGE_OFF_T to be 9223372036854775807,
+ since some C++ compilers masquerading as C compilers
+ incorrectly reject 9223372036854775807. */
+#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31))
+ int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721
+ && LARGE_OFF_T % 2147483647 == 1)
+ ? 1 : -1];
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_sys_file_offset_bits=no; break
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+#define _FILE_OFFSET_BITS 64
+#include <sys/types.h>
+ /* Check that off_t can represent 2**63 - 1 correctly.
+ We can't simply define LARGE_OFF_T to be 9223372036854775807,
+ since some C++ compilers masquerading as C compilers
+ incorrectly reject 9223372036854775807. */
+#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31))
+ int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721
+ && LARGE_OFF_T % 2147483647 == 1)
+ ? 1 : -1];
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_sys_file_offset_bits=64; break
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+ ac_cv_sys_file_offset_bits=unknown
+ break
+done
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_file_offset_bits" >&5
+printf "%s\n" "$ac_cv_sys_file_offset_bits" >&6; }
+case $ac_cv_sys_file_offset_bits in #(
+ no | unknown) ;;
+ *)
+printf "%s\n" "#define _FILE_OFFSET_BITS $ac_cv_sys_file_offset_bits" >>confdefs.h
+;;
+esac
+rm -rf conftest*
+ if test $ac_cv_sys_file_offset_bits = unknown; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for _LARGE_FILES value needed for large files" >&5
+printf %s "checking for _LARGE_FILES value needed for large files... " >&6; }
+if test ${ac_cv_sys_large_files+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ while :; do
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+#include <sys/types.h>
+ /* Check that off_t can represent 2**63 - 1 correctly.
+ We can't simply define LARGE_OFF_T to be 9223372036854775807,
+ since some C++ compilers masquerading as C compilers
+ incorrectly reject 9223372036854775807. */
+#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31))
+ int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721
+ && LARGE_OFF_T % 2147483647 == 1)
+ ? 1 : -1];
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_sys_large_files=no; break
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+ cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+#define _LARGE_FILES 1
+#include <sys/types.h>
+ /* Check that off_t can represent 2**63 - 1 correctly.
+ We can't simply define LARGE_OFF_T to be 9223372036854775807,
+ since some C++ compilers masquerading as C compilers
+ incorrectly reject 9223372036854775807. */
+#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31))
+ int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721
+ && LARGE_OFF_T % 2147483647 == 1)
+ ? 1 : -1];
+int
+main (void)
+{
+
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"
+then :
+ ac_cv_sys_large_files=1; break
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext
+ ac_cv_sys_large_files=unknown
+ break
+done
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_large_files" >&5
+printf "%s\n" "$ac_cv_sys_large_files" >&6; }
+case $ac_cv_sys_large_files in #(
+ no | unknown) ;;
+ *)
+printf "%s\n" "#define _LARGE_FILES $ac_cv_sys_large_files" >>confdefs.h
+;;
+esac
+rm -rf conftest*
+ fi
+fi
+
+if test -n "$ac_tool_prefix"; then
+ # Extract the first word of "${ac_tool_prefix}ranlib", so it can be a program name with args.
+set dummy ${ac_tool_prefix}ranlib; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_RANLIB+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$RANLIB"; then
+ ac_cv_prog_RANLIB="$RANLIB" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_RANLIB="${ac_tool_prefix}ranlib"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+RANLIB=$ac_cv_prog_RANLIB
+if test -n "$RANLIB"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $RANLIB" >&5
+printf "%s\n" "$RANLIB" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_RANLIB"; then
+ ac_ct_RANLIB=$RANLIB
+ # Extract the first word of "ranlib", so it can be a program name with args.
+set dummy ranlib; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_RANLIB+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$ac_ct_RANLIB"; then
+ ac_cv_prog_ac_ct_RANLIB="$ac_ct_RANLIB" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_ac_ct_RANLIB="ranlib"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_RANLIB=$ac_cv_prog_ac_ct_RANLIB
+if test -n "$ac_ct_RANLIB"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_RANLIB" >&5
+printf "%s\n" "$ac_ct_RANLIB" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+ if test "x$ac_ct_RANLIB" = x; then
+ RANLIB=":"
+ else
+ case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+ RANLIB=$ac_ct_RANLIB
+ fi
+else
+ RANLIB="$ac_cv_prog_RANLIB"
+fi
+
+
+# This is required to find the correct `ar` for cross-compiling
+if test -n "$ac_tool_prefix"; then
+ # Extract the first word of "${ac_tool_prefix}ar", so it can be a program name with args.
+set dummy ${ac_tool_prefix}ar; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_AR+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$AR"; then
+ ac_cv_prog_AR="$AR" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_AR="${ac_tool_prefix}ar"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+AR=$ac_cv_prog_AR
+if test -n "$AR"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $AR" >&5
+printf "%s\n" "$AR" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_prog_AR"; then
+ ac_ct_AR=$AR
+ # Extract the first word of "ar", so it can be a program name with args.
+set dummy ar; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_prog_ac_ct_AR+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test -n "$ac_ct_AR"; then
+ ac_cv_prog_ac_ct_AR="$ac_ct_AR" # Let the user override the test.
+else
+as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_prog_ac_ct_AR="ar"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+fi
+fi
+ac_ct_AR=$ac_cv_prog_ac_ct_AR
+if test -n "$ac_ct_AR"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_ct_AR" >&5
+printf "%s\n" "$ac_ct_AR" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+ if test "x$ac_ct_AR" = x; then
+ AR=""
+ else
+ case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+ AR=$ac_ct_AR
+ fi
+else
+ AR="$ac_cv_prog_AR"
+fi
+
+
+ac_fn_c_check_func "$LINENO" "closefrom" "ac_cv_func_closefrom"
+if test "x$ac_cv_func_closefrom" = xyes
+then :
+ printf "%s\n" "#define HAVE_CLOSEFROM 1" >>confdefs.h
+
+fi
+
+
+# Check whether --enable-silent-rules was given.
+if test ${enable_silent_rules+y}
+then :
+ enableval=$enable_silent_rules;
+fi
+
+case $enable_silent_rules in # (((
+ yes) AM_DEFAULT_VERBOSITY=0;;
+ no) AM_DEFAULT_VERBOSITY=1;;
+ *) AM_DEFAULT_VERBOSITY=0;;
+esac
+am_make=${MAKE-make}
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $am_make supports nested variables" >&5
+printf %s "checking whether $am_make supports nested variables... " >&6; }
+if test ${am_cv_make_support_nested_variables+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if printf "%s\n" 'TRUE=$(BAR$(V))
+BAR0=false
+BAR1=true
+V=1
+am__doit:
+ @$(TRUE)
+.PHONY: am__doit' | $am_make -f - >/dev/null 2>&1; then
+ am_cv_make_support_nested_variables=yes
+else
+ am_cv_make_support_nested_variables=no
+fi
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_make_support_nested_variables" >&5
+printf "%s\n" "$am_cv_make_support_nested_variables" >&6; }
+if test $am_cv_make_support_nested_variables = yes; then
+ AM_V='$(V)'
+ AM_DEFAULT_V='$(AM_DEFAULT_VERBOSITY)'
+else
+ AM_V=$AM_DEFAULT_VERBOSITY
+ AM_DEFAULT_V=$AM_DEFAULT_VERBOSITY
+fi
+AM_BACKSLASH='\'
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to install to prefix only" >&5
+printf %s "checking whether to install to prefix only... " >&6; }
+# Check whether --enable-prefix-only was given.
+if test ${enable_prefix_only+y}
+then :
+ enableval=$enable_prefix_only;
+else $as_nop
+ enable_prefix_only=no
+fi
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_prefix_only" >&5
+printf "%s\n" "$enable_prefix_only" >&6; }
+
+
+# --enable-selinux-policy=[type]
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to build selinux policy, and which" >&5
+printf %s "checking whether to build selinux policy, and which... " >&6; }
+# Check whether --enable-selinux-policy was given.
+if test ${enable_selinux_policy+y}
+then :
+ enableval=$enable_selinux_policy;
+fi
+
+if test "${enable_selinux_policy:=no}" = 'yes'; then
+ as_fn_error $? "--enable-selinux-policy requires a type (eg: targeted)" "$LINENO" 5
+fi
+ if test "$enable_selinux_policy" != "no"; then
+ SELINUX_POLICY_ENABLED_TRUE=
+ SELINUX_POLICY_ENABLED_FALSE='#'
+else
+ SELINUX_POLICY_ENABLED_TRUE='#'
+ SELINUX_POLICY_ENABLED_FALSE=
+fi
+
+SELINUX_POLICY_TYPE=${enable_selinux_policy}
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_selinux_policy" >&5
+printf "%s\n" "$enable_selinux_policy" >&6; }
+
+# --disable-polkit
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to build polkit support" >&5
+printf %s "checking whether to build polkit support... " >&6; }
+# Check whether --enable-polkit was given.
+if test ${enable_polkit+y}
+then :
+ enableval=$enable_polkit;
+fi
+
+ if test "$enable_polkit" != 'no'; then
+ WITH_POLKIT_TRUE=
+ WITH_POLKIT_FALSE='#'
+else
+ WITH_POLKIT_TRUE='#'
+ WITH_POLKIT_FALSE=
+fi
+
+if test "$enable_polkit" != 'no'; then
+
+printf "%s\n" "#define WITH_POLKIT 1" >>confdefs.h
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${enable_polkit:=yes}" >&5
+printf "%s\n" "${enable_polkit:=yes}" >&6; }
+
+# --disable-ssh
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to build cockpit-ssh" >&5
+printf %s "checking whether to build cockpit-ssh... " >&6; }
+# Check whether --enable-ssh was given.
+if test ${enable_ssh+y}
+then :
+ enableval=$enable_ssh;
+fi
+
+ if test "$enable_ssh" != "no"; then
+ WITH_COCKPIT_SSH_TRUE=
+ WITH_COCKPIT_SSH_FALSE='#'
+else
+ WITH_COCKPIT_SSH_TRUE='#'
+ WITH_COCKPIT_SSH_FALSE=
+fi
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${enable_ssh:=yes}" >&5
+printf "%s\n" "${enable_ssh:=yes}" >&6; }
+
+# --enable-old-bridge
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to install the old C cockpit-bridge" >&5
+printf %s "checking whether to install the old C cockpit-bridge... " >&6; }
+# Check whether --enable-old_bridge was given.
+if test ${enable_old_bridge+y}
+then :
+ enableval=$enable_old_bridge;
+fi
+
+ if test "$enable_old_bridge" = "yes"; then
+ WITH_OLD_BRIDGE_TRUE=
+ WITH_OLD_BRIDGE_FALSE='#'
+else
+ WITH_OLD_BRIDGE_TRUE='#'
+ WITH_OLD_BRIDGE_FALSE=
+fi
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: ${enable_old_bridge:=no}" >&5
+printf "%s\n" "${enable_old_bridge:=no}" >&6; }
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing argp_parse" >&5
+printf %s "checking for library containing argp_parse... " >&6; }
+if test ${ac_cv_search_argp_parse+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_func_search_save_LIBS=$LIBS
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+/* Override any GCC internal prototype to avoid an error.
+ Use char because int might match the return type of a GCC
+ builtin and then its argument prototype would still apply. */
+char argp_parse ();
+int
+main (void)
+{
+return argp_parse ();
+ ;
+ return 0;
+}
+_ACEOF
+for ac_lib in '' argp
+do
+ if test -z "$ac_lib"; then
+ ac_res="none required"
+ else
+ ac_res=-l$ac_lib
+ LIBS="-l$ac_lib $ac_func_search_save_LIBS"
+ fi
+ if ac_fn_c_try_link "$LINENO"
+then :
+ ac_cv_search_argp_parse=$ac_res
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+ conftest$ac_exeext
+ if test ${ac_cv_search_argp_parse+y}
+then :
+ break
+fi
+done
+if test ${ac_cv_search_argp_parse+y}
+then :
+
+else $as_nop
+ ac_cv_search_argp_parse=no
+fi
+rm conftest.$ac_ext
+LIBS=$ac_func_search_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_argp_parse" >&5
+printf "%s\n" "$ac_cv_search_argp_parse" >&6; }
+ac_res=$ac_cv_search_argp_parse
+if test "$ac_res" != no
+then :
+ test "$ac_res" = "none required" || LIBS="$ac_res $LIBS"
+
+fi
+
+case "$ac_cv_search_argp_parse" in
+ no) { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "failed to find argp_parse
+See \`config.log' for more details" "$LINENO" 5; } ;;
+ -l*) argp_LIBS="$ac_cv_search_argp_parse" ;;
+ *) argp_LIBS= ;;
+esac
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for library containing fts_close" >&5
+printf %s "checking for library containing fts_close... " >&6; }
+if test ${ac_cv_search_fts_close+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_func_search_save_LIBS=$LIBS
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+/* Override any GCC internal prototype to avoid an error.
+ Use char because int might match the return type of a GCC
+ builtin and then its argument prototype would still apply. */
+char fts_close ();
+int
+main (void)
+{
+return fts_close ();
+ ;
+ return 0;
+}
+_ACEOF
+for ac_lib in '' fts
+do
+ if test -z "$ac_lib"; then
+ ac_res="none required"
+ else
+ ac_res=-l$ac_lib
+ LIBS="-l$ac_lib $ac_func_search_save_LIBS"
+ fi
+ if ac_fn_c_try_link "$LINENO"
+then :
+ ac_cv_search_fts_close=$ac_res
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+ conftest$ac_exeext
+ if test ${ac_cv_search_fts_close+y}
+then :
+ break
+fi
+done
+if test ${ac_cv_search_fts_close+y}
+then :
+
+else $as_nop
+ ac_cv_search_fts_close=no
+fi
+rm conftest.$ac_ext
+LIBS=$ac_func_search_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_fts_close" >&5
+printf "%s\n" "$ac_cv_search_fts_close" >&6; }
+ac_res=$ac_cv_search_fts_close
+if test "$ac_res" != no
+then :
+ test "$ac_res" = "none required" || LIBS="$ac_res $LIBS"
+
+fi
+
+case "$ac_cv_search_fts_close" in
+ no) { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "failed to find fts_close
+See \`config.log' for more details" "$LINENO" 5; } ;;
+ -l*) fts_LIBS="$ac_cv_search_fts_close" ;;
+ *) fts_LIBS= ;;
+esac
+
+
+# pkg-config
+GLIB_API_VERSION="GLIB_VERSION_2_56"
+
+
+
+
+
+
+
+if test "x$ac_cv_env_PKG_CONFIG_set" != "xset"; then
+ if test -n "$ac_tool_prefix"; then
+ # Extract the first word of "${ac_tool_prefix}pkg-config", so it can be a program name with args.
+set dummy ${ac_tool_prefix}pkg-config; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_path_PKG_CONFIG+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ case $PKG_CONFIG in
+ [\\/]* | ?:[\\/]*)
+ ac_cv_path_PKG_CONFIG="$PKG_CONFIG" # Let the user override the test with a path.
+ ;;
+ *)
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_path_PKG_CONFIG="$as_dir$ac_word$ac_exec_ext"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+ ;;
+esac
+fi
+PKG_CONFIG=$ac_cv_path_PKG_CONFIG
+if test -n "$PKG_CONFIG"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $PKG_CONFIG" >&5
+printf "%s\n" "$PKG_CONFIG" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+fi
+if test -z "$ac_cv_path_PKG_CONFIG"; then
+ ac_pt_PKG_CONFIG=$PKG_CONFIG
+ # Extract the first word of "pkg-config", so it can be a program name with args.
+set dummy pkg-config; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_path_ac_pt_PKG_CONFIG+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ case $ac_pt_PKG_CONFIG in
+ [\\/]* | ?:[\\/]*)
+ ac_cv_path_ac_pt_PKG_CONFIG="$ac_pt_PKG_CONFIG" # Let the user override the test with a path.
+ ;;
+ *)
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_path_ac_pt_PKG_CONFIG="$as_dir$ac_word$ac_exec_ext"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+ ;;
+esac
+fi
+ac_pt_PKG_CONFIG=$ac_cv_path_ac_pt_PKG_CONFIG
+if test -n "$ac_pt_PKG_CONFIG"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_pt_PKG_CONFIG" >&5
+printf "%s\n" "$ac_pt_PKG_CONFIG" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+ if test "x$ac_pt_PKG_CONFIG" = x; then
+ PKG_CONFIG=""
+ else
+ case $cross_compiling:$ac_tool_warned in
+yes:)
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5
+printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;}
+ac_tool_warned=yes ;;
+esac
+ PKG_CONFIG=$ac_pt_PKG_CONFIG
+ fi
+else
+ PKG_CONFIG="$ac_cv_path_PKG_CONFIG"
+fi
+
+fi
+if test -n "$PKG_CONFIG"; then
+ _pkg_min_version=0.9.0
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking pkg-config is at least version $_pkg_min_version" >&5
+printf %s "checking pkg-config is at least version $_pkg_min_version... " >&6; }
+ if $PKG_CONFIG --atleast-pkgconfig-version $_pkg_min_version; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+ else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ PKG_CONFIG=""
+ fi
+fi
+
+pkg_failed=no
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for gio-2.0 >= 2.56 gio-unix-2.0" >&5
+printf %s "checking for gio-2.0 >= 2.56 gio-unix-2.0... " >&6; }
+
+if test -n "$glib_CFLAGS"; then
+ pkg_cv_glib_CFLAGS="$glib_CFLAGS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"gio-2.0 >= 2.56 gio-unix-2.0\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "gio-2.0 >= 2.56 gio-unix-2.0") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_glib_CFLAGS=`$PKG_CONFIG --cflags "gio-2.0 >= 2.56 gio-unix-2.0" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+if test -n "$glib_LIBS"; then
+ pkg_cv_glib_LIBS="$glib_LIBS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"gio-2.0 >= 2.56 gio-unix-2.0\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "gio-2.0 >= 2.56 gio-unix-2.0") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_glib_LIBS=`$PKG_CONFIG --libs "gio-2.0 >= 2.56 gio-unix-2.0" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+
+
+
+if test $pkg_failed = yes; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+
+if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
+ _pkg_short_errors_supported=yes
+else
+ _pkg_short_errors_supported=no
+fi
+ if test $_pkg_short_errors_supported = yes; then
+ glib_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "gio-2.0 >= 2.56 gio-unix-2.0" 2>&1`
+ else
+ glib_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "gio-2.0 >= 2.56 gio-unix-2.0" 2>&1`
+ fi
+ # Put the nasty error message in config.log where it belongs
+ echo "$glib_PKG_ERRORS" >&5
+
+ as_fn_error $? "Package requirements (gio-2.0 >= 2.56 gio-unix-2.0) were not met:
+
+$glib_PKG_ERRORS
+
+Consider adjusting the PKG_CONFIG_PATH environment variable if you
+installed software in a non-standard prefix.
+
+Alternatively, you may set the environment variables glib_CFLAGS
+and glib_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details." "$LINENO" 5
+elif test $pkg_failed = untried; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it
+is in your PATH or set the PKG_CONFIG environment variable to the full
+path to pkg-config.
+
+Alternatively, you may set the environment variables glib_CFLAGS
+and glib_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details.
+
+To get pkg-config, see <http://pkg-config.freedesktop.org/>.
+See \`config.log' for more details" "$LINENO" 5; }
+else
+ glib_CFLAGS=$pkg_cv_glib_CFLAGS
+ glib_LIBS=$pkg_cv_glib_LIBS
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+
+fi
+glib_CFLAGS="${glib_CFLAGS} -DGLIB_VERSION_MIN_REQUIRED=$GLIB_API_VERSION"
+glib_CFLAGS="${glib_CFLAGS} -DGLIB_VERSION_MAX_ALLOWED=$GLIB_API_VERSION"
+
+
+pkg_failed=no
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libsystemd >= 235" >&5
+printf %s "checking for libsystemd >= 235... " >&6; }
+
+if test -n "$libsystemd_CFLAGS"; then
+ pkg_cv_libsystemd_CFLAGS="$libsystemd_CFLAGS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libsystemd >= 235\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "libsystemd >= 235") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_libsystemd_CFLAGS=`$PKG_CONFIG --cflags "libsystemd >= 235" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+if test -n "$libsystemd_LIBS"; then
+ pkg_cv_libsystemd_LIBS="$libsystemd_LIBS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libsystemd >= 235\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "libsystemd >= 235") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_libsystemd_LIBS=`$PKG_CONFIG --libs "libsystemd >= 235" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+
+
+
+if test $pkg_failed = yes; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+
+if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
+ _pkg_short_errors_supported=yes
+else
+ _pkg_short_errors_supported=no
+fi
+ if test $_pkg_short_errors_supported = yes; then
+ libsystemd_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libsystemd >= 235" 2>&1`
+ else
+ libsystemd_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libsystemd >= 235" 2>&1`
+ fi
+ # Put the nasty error message in config.log where it belongs
+ echo "$libsystemd_PKG_ERRORS" >&5
+
+ as_fn_error $? "Package requirements (libsystemd >= 235) were not met:
+
+$libsystemd_PKG_ERRORS
+
+Consider adjusting the PKG_CONFIG_PATH environment variable if you
+installed software in a non-standard prefix.
+
+Alternatively, you may set the environment variables libsystemd_CFLAGS
+and libsystemd_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details." "$LINENO" 5
+elif test $pkg_failed = untried; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it
+is in your PATH or set the PKG_CONFIG environment variable to the full
+path to pkg-config.
+
+Alternatively, you may set the environment variables libsystemd_CFLAGS
+and libsystemd_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details.
+
+To get pkg-config, see <http://pkg-config.freedesktop.org/>.
+See \`config.log' for more details" "$LINENO" 5; }
+else
+ libsystemd_CFLAGS=$pkg_cv_libsystemd_CFLAGS
+ libsystemd_LIBS=$pkg_cv_libsystemd_LIBS
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+
+fi
+
+pkg_failed=no
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for json-glib-1.0 >= 1.4" >&5
+printf %s "checking for json-glib-1.0 >= 1.4... " >&6; }
+
+if test -n "$json_glib_CFLAGS"; then
+ pkg_cv_json_glib_CFLAGS="$json_glib_CFLAGS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"json-glib-1.0 >= 1.4\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "json-glib-1.0 >= 1.4") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_json_glib_CFLAGS=`$PKG_CONFIG --cflags "json-glib-1.0 >= 1.4" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+if test -n "$json_glib_LIBS"; then
+ pkg_cv_json_glib_LIBS="$json_glib_LIBS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"json-glib-1.0 >= 1.4\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "json-glib-1.0 >= 1.4") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_json_glib_LIBS=`$PKG_CONFIG --libs "json-glib-1.0 >= 1.4" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+
+
+
+if test $pkg_failed = yes; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+
+if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
+ _pkg_short_errors_supported=yes
+else
+ _pkg_short_errors_supported=no
+fi
+ if test $_pkg_short_errors_supported = yes; then
+ json_glib_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "json-glib-1.0 >= 1.4" 2>&1`
+ else
+ json_glib_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "json-glib-1.0 >= 1.4" 2>&1`
+ fi
+ # Put the nasty error message in config.log where it belongs
+ echo "$json_glib_PKG_ERRORS" >&5
+
+ as_fn_error $? "Package requirements (json-glib-1.0 >= 1.4) were not met:
+
+$json_glib_PKG_ERRORS
+
+Consider adjusting the PKG_CONFIG_PATH environment variable if you
+installed software in a non-standard prefix.
+
+Alternatively, you may set the environment variables json_glib_CFLAGS
+and json_glib_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details." "$LINENO" 5
+elif test $pkg_failed = untried; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it
+is in your PATH or set the PKG_CONFIG environment variable to the full
+path to pkg-config.
+
+Alternatively, you may set the environment variables json_glib_CFLAGS
+and json_glib_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details.
+
+To get pkg-config, see <http://pkg-config.freedesktop.org/>.
+See \`config.log' for more details" "$LINENO" 5; }
+else
+ json_glib_CFLAGS=$pkg_cv_json_glib_CFLAGS
+ json_glib_LIBS=$pkg_cv_json_glib_LIBS
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+
+fi
+
+pkg_failed=no
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for gnutls >= 3.6.0" >&5
+printf %s "checking for gnutls >= 3.6.0... " >&6; }
+
+if test -n "$gnutls_CFLAGS"; then
+ pkg_cv_gnutls_CFLAGS="$gnutls_CFLAGS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"gnutls >= 3.6.0\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "gnutls >= 3.6.0") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_gnutls_CFLAGS=`$PKG_CONFIG --cflags "gnutls >= 3.6.0" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+if test -n "$gnutls_LIBS"; then
+ pkg_cv_gnutls_LIBS="$gnutls_LIBS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"gnutls >= 3.6.0\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "gnutls >= 3.6.0") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_gnutls_LIBS=`$PKG_CONFIG --libs "gnutls >= 3.6.0" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+
+
+
+if test $pkg_failed = yes; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+
+if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
+ _pkg_short_errors_supported=yes
+else
+ _pkg_short_errors_supported=no
+fi
+ if test $_pkg_short_errors_supported = yes; then
+ gnutls_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "gnutls >= 3.6.0" 2>&1`
+ else
+ gnutls_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "gnutls >= 3.6.0" 2>&1`
+ fi
+ # Put the nasty error message in config.log where it belongs
+ echo "$gnutls_PKG_ERRORS" >&5
+
+ as_fn_error $? "Package requirements (gnutls >= 3.6.0) were not met:
+
+$gnutls_PKG_ERRORS
+
+Consider adjusting the PKG_CONFIG_PATH environment variable if you
+installed software in a non-standard prefix.
+
+Alternatively, you may set the environment variables gnutls_CFLAGS
+and gnutls_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details." "$LINENO" 5
+elif test $pkg_failed = untried; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it
+is in your PATH or set the PKG_CONFIG environment variable to the full
+path to pkg-config.
+
+Alternatively, you may set the environment variables gnutls_CFLAGS
+and gnutls_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details.
+
+To get pkg-config, see <http://pkg-config.freedesktop.org/>.
+See \`config.log' for more details" "$LINENO" 5; }
+else
+ gnutls_CFLAGS=$pkg_cv_gnutls_CFLAGS
+ gnutls_LIBS=$pkg_cv_gnutls_LIBS
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+
+fi
+
+pkg_failed=no
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for krb5-gssapi >= 1.11 krb5 >= 1.11" >&5
+printf %s "checking for krb5-gssapi >= 1.11 krb5 >= 1.11... " >&6; }
+
+if test -n "$krb5_CFLAGS"; then
+ pkg_cv_krb5_CFLAGS="$krb5_CFLAGS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"krb5-gssapi >= 1.11 krb5 >= 1.11\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "krb5-gssapi >= 1.11 krb5 >= 1.11") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_krb5_CFLAGS=`$PKG_CONFIG --cflags "krb5-gssapi >= 1.11 krb5 >= 1.11" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+if test -n "$krb5_LIBS"; then
+ pkg_cv_krb5_LIBS="$krb5_LIBS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"krb5-gssapi >= 1.11 krb5 >= 1.11\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "krb5-gssapi >= 1.11 krb5 >= 1.11") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_krb5_LIBS=`$PKG_CONFIG --libs "krb5-gssapi >= 1.11 krb5 >= 1.11" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+
+
+
+if test $pkg_failed = yes; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+
+if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
+ _pkg_short_errors_supported=yes
+else
+ _pkg_short_errors_supported=no
+fi
+ if test $_pkg_short_errors_supported = yes; then
+ krb5_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "krb5-gssapi >= 1.11 krb5 >= 1.11" 2>&1`
+ else
+ krb5_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "krb5-gssapi >= 1.11 krb5 >= 1.11" 2>&1`
+ fi
+ # Put the nasty error message in config.log where it belongs
+ echo "$krb5_PKG_ERRORS" >&5
+
+ as_fn_error $? "Package requirements (krb5-gssapi >= 1.11 krb5 >= 1.11) were not met:
+
+$krb5_PKG_ERRORS
+
+Consider adjusting the PKG_CONFIG_PATH environment variable if you
+installed software in a non-standard prefix.
+
+Alternatively, you may set the environment variables krb5_CFLAGS
+and krb5_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details." "$LINENO" 5
+elif test $pkg_failed = untried; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it
+is in your PATH or set the PKG_CONFIG environment variable to the full
+path to pkg-config.
+
+Alternatively, you may set the environment variables krb5_CFLAGS
+and krb5_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details.
+
+To get pkg-config, see <http://pkg-config.freedesktop.org/>.
+See \`config.log' for more details" "$LINENO" 5; }
+else
+ krb5_CFLAGS=$pkg_cv_krb5_CFLAGS
+ krb5_LIBS=$pkg_cv_krb5_LIBS
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+
+fi
+if test "$enable_polkit" != "no"; then
+
+pkg_failed=no
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for polkit-agent-1 >= 0.105" >&5
+printf %s "checking for polkit-agent-1 >= 0.105... " >&6; }
+
+if test -n "$polkit_CFLAGS"; then
+ pkg_cv_polkit_CFLAGS="$polkit_CFLAGS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"polkit-agent-1 >= 0.105\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "polkit-agent-1 >= 0.105") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_polkit_CFLAGS=`$PKG_CONFIG --cflags "polkit-agent-1 >= 0.105" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+if test -n "$polkit_LIBS"; then
+ pkg_cv_polkit_LIBS="$polkit_LIBS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"polkit-agent-1 >= 0.105\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "polkit-agent-1 >= 0.105") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_polkit_LIBS=`$PKG_CONFIG --libs "polkit-agent-1 >= 0.105" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+
+
+
+if test $pkg_failed = yes; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+
+if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
+ _pkg_short_errors_supported=yes
+else
+ _pkg_short_errors_supported=no
+fi
+ if test $_pkg_short_errors_supported = yes; then
+ polkit_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "polkit-agent-1 >= 0.105" 2>&1`
+ else
+ polkit_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "polkit-agent-1 >= 0.105" 2>&1`
+ fi
+ # Put the nasty error message in config.log where it belongs
+ echo "$polkit_PKG_ERRORS" >&5
+
+ as_fn_error $? "Package requirements (polkit-agent-1 >= 0.105) were not met:
+
+$polkit_PKG_ERRORS
+
+Consider adjusting the PKG_CONFIG_PATH environment variable if you
+installed software in a non-standard prefix.
+
+Alternatively, you may set the environment variables polkit_CFLAGS
+and polkit_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details." "$LINENO" 5
+elif test $pkg_failed = untried; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it
+is in your PATH or set the PKG_CONFIG environment variable to the full
+path to pkg-config.
+
+Alternatively, you may set the environment variables polkit_CFLAGS
+and polkit_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details.
+
+To get pkg-config, see <http://pkg-config.freedesktop.org/>.
+See \`config.log' for more details" "$LINENO" 5; }
+else
+ polkit_CFLAGS=$pkg_cv_polkit_CFLAGS
+ polkit_LIBS=$pkg_cv_polkit_LIBS
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+
+fi
+fi
+if test "$enable_ssh" != "no"; then
+
+pkg_failed=no
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for libssh >= 0.8.5" >&5
+printf %s "checking for libssh >= 0.8.5... " >&6; }
+
+if test -n "$libssh_CFLAGS"; then
+ pkg_cv_libssh_CFLAGS="$libssh_CFLAGS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libssh >= 0.8.5\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "libssh >= 0.8.5") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_libssh_CFLAGS=`$PKG_CONFIG --cflags "libssh >= 0.8.5" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+if test -n "$libssh_LIBS"; then
+ pkg_cv_libssh_LIBS="$libssh_LIBS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libssh >= 0.8.5\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "libssh >= 0.8.5") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_libssh_LIBS=`$PKG_CONFIG --libs "libssh >= 0.8.5" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+
+
+
+if test $pkg_failed = yes; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+
+if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
+ _pkg_short_errors_supported=yes
+else
+ _pkg_short_errors_supported=no
+fi
+ if test $_pkg_short_errors_supported = yes; then
+ libssh_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libssh >= 0.8.5" 2>&1`
+ else
+ libssh_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libssh >= 0.8.5" 2>&1`
+ fi
+ # Put the nasty error message in config.log where it belongs
+ echo "$libssh_PKG_ERRORS" >&5
+
+ as_fn_error $? "Package requirements (libssh >= 0.8.5) were not met:
+
+$libssh_PKG_ERRORS
+
+Consider adjusting the PKG_CONFIG_PATH environment variable if you
+installed software in a non-standard prefix.
+
+Alternatively, you may set the environment variables libssh_CFLAGS
+and libssh_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details." "$LINENO" 5
+elif test $pkg_failed = untried; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it
+is in your PATH or set the PKG_CONFIG environment variable to the full
+path to pkg-config.
+
+Alternatively, you may set the environment variables libssh_CFLAGS
+and libssh_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details.
+
+To get pkg-config, see <http://pkg-config.freedesktop.org/>.
+See \`config.log' for more details" "$LINENO" 5; }
+else
+ libssh_CFLAGS=$pkg_cv_libssh_CFLAGS
+ libssh_LIBS=$pkg_cv_libssh_LIBS
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+
+fi
+ old_CFLAGS=$CFLAGS; CFLAGS=$libssh_CFLAGS
+ old_LIBS=$LIBS; LIBS=$libssh_LIBS
+ ac_fn_c_check_func "$LINENO" "ssh_userauth_publickey_auto_get_current_identity" "ac_cv_func_ssh_userauth_publickey_auto_get_current_identity"
+if test "x$ac_cv_func_ssh_userauth_publickey_auto_get_current_identity" = xyes
+then :
+ printf "%s\n" "#define HAVE_SSH_USERAUTH_PUBLICKEY_AUTO_GET_CURRENT_IDENTITY 1" >>confdefs.h
+
+fi
+
+ CFLAGS=$old_CFLAGS
+ LIBS=$old_LIBS
+fi
+
+# pam
+ac_fn_c_check_header_compile "$LINENO" "security/pam_appl.h" "ac_cv_header_security_pam_appl_h" "$ac_includes_default"
+if test "x$ac_cv_header_security_pam_appl_h" = xyes
+then :
+
+else $as_nop
+ as_fn_error $? "Couldn't find PAM headers. Try installing pam-devel" "$LINENO" 5
+
+fi
+
+PAM_LIBS="-lpam"
+COCKPIT_SESSION_LIBS="$COCKPIT_SESSION_LIBS $PAM_LIBS"
+
+# pam module directory
+
+# Check whether --with-pamdir was given.
+if test ${with_pamdir+y}
+then :
+ withval=$with_pamdir;
+else $as_nop
+ with_pamdir='${libdir}/security'
+fi
+
+pamdir=$with_pamdir
+
+
+# crypt
+ac_fn_c_check_header_compile "$LINENO" "crypt.h" "ac_cv_header_crypt_h" "$ac_includes_default"
+if test "x$ac_cv_header_crypt_h" = xyes
+then :
+
+else $as_nop
+ as_fn_error $? "Couldn't find crypt headers. Try installing glibc-headers" "$LINENO" 5
+
+fi
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for crypt_r in -lcrypt" >&5
+printf %s "checking for crypt_r in -lcrypt... " >&6; }
+if test ${ac_cv_lib_crypt_crypt_r+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_check_lib_save_LIBS=$LIBS
+LIBS="-lcrypt $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+/* Override any GCC internal prototype to avoid an error.
+ Use char because int might match the return type of a GCC
+ builtin and then its argument prototype would still apply. */
+char crypt_r ();
+int
+main (void)
+{
+return crypt_r ();
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+ ac_cv_lib_crypt_crypt_r=yes
+else $as_nop
+ ac_cv_lib_crypt_crypt_r=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+ conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_crypt_crypt_r" >&5
+printf "%s\n" "$ac_cv_lib_crypt_crypt_r" >&6; }
+if test "x$ac_cv_lib_crypt_crypt_r" = xyes
+then :
+ true
+else $as_nop
+ as_fn_error $? "Couldn't find crypt library. Try installing glibc-devel" "$LINENO" 5
+
+fi
+
+COCKPIT_WS_LIBS="$COCKPIT_WS_LIBS -lcrypt"
+
+# pcp
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to build with PCP" >&5
+printf %s "checking whether to build with PCP... " >&6; }
+# Check whether --enable-pcp was given.
+if test ${enable_pcp+y}
+then :
+ enableval=$enable_pcp;
+fi
+
+
+if test "$enable_pcp" = "no"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_pcp" >&5
+printf "%s\n" "$enable_pcp" >&6; }
+
+else
+ if test "$enable_pcp" = ""; then
+ disable_msg="(perhaps --disable-pcp)"
+ fi
+
+ enable_pcp="yes"
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_pcp" >&5
+printf "%s\n" "$enable_pcp" >&6; }
+
+ ac_fn_c_check_header_compile "$LINENO" "pcp/pmapi.h" "ac_cv_header_pcp_pmapi_h" "$ac_includes_default"
+if test "x$ac_cv_header_pcp_pmapi_h" = xyes
+then :
+
+else $as_nop
+ as_fn_error $? "Couldn't find pcp headers $disable_msg" "$LINENO" 5
+
+fi
+
+ for ac_header in pcp/import.h pcp/pmda.h
+do :
+ as_ac_Header=`printf "%s\n" "ac_cv_header_$ac_header" | $as_tr_sh`
+ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "#include <pcp/pmapi.h>
+
+"
+if eval test \"x\$"$as_ac_Header"\" = x"yes"
+then :
+ cat >>confdefs.h <<_ACEOF
+#define `printf "%s\n" "HAVE_$ac_header" | $as_tr_cpp` 1
+_ACEOF
+
+else $as_nop
+ as_fn_error $? "Couldn't find pcp headers $disable_msg" "$LINENO" 5
+fi
+
+done
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for pmNewContext in -lpcp" >&5
+printf %s "checking for pmNewContext in -lpcp... " >&6; }
+if test ${ac_cv_lib_pcp_pmNewContext+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_check_lib_save_LIBS=$LIBS
+LIBS="-lpcp $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+/* Override any GCC internal prototype to avoid an error.
+ Use char because int might match the return type of a GCC
+ builtin and then its argument prototype would still apply. */
+char pmNewContext ();
+int
+main (void)
+{
+return pmNewContext ();
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+ ac_cv_lib_pcp_pmNewContext=yes
+else $as_nop
+ ac_cv_lib_pcp_pmNewContext=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+ conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_pcp_pmNewContext" >&5
+printf "%s\n" "$ac_cv_lib_pcp_pmNewContext" >&6; }
+if test "x$ac_cv_lib_pcp_pmNewContext" = xyes
+then :
+ true
+else $as_nop
+ as_fn_error $? "Couldn't find pcp library $disable_msg" "$LINENO" 5
+
+fi
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for pmdaCacheLookup in -lpcp_pmda" >&5
+printf %s "checking for pmdaCacheLookup in -lpcp_pmda... " >&6; }
+if test ${ac_cv_lib_pcp_pmda_pmdaCacheLookup+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_check_lib_save_LIBS=$LIBS
+LIBS="-lpcp_pmda $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+/* Override any GCC internal prototype to avoid an error.
+ Use char because int might match the return type of a GCC
+ builtin and then its argument prototype would still apply. */
+char pmdaCacheLookup ();
+int
+main (void)
+{
+return pmdaCacheLookup ();
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+ ac_cv_lib_pcp_pmda_pmdaCacheLookup=yes
+else $as_nop
+ ac_cv_lib_pcp_pmda_pmdaCacheLookup=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+ conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_pcp_pmda_pmdaCacheLookup" >&5
+printf "%s\n" "$ac_cv_lib_pcp_pmda_pmdaCacheLookup" >&6; }
+if test "x$ac_cv_lib_pcp_pmda_pmdaCacheLookup" = xyes
+then :
+ true
+else $as_nop
+ as_fn_error $? "Couldn't find pcp_pmda library $disable_msg" "$LINENO" 5
+
+fi
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for pmiStart in -lpcp_import" >&5
+printf %s "checking for pmiStart in -lpcp_import... " >&6; }
+if test ${ac_cv_lib_pcp_import_pmiStart+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_check_lib_save_LIBS=$LIBS
+LIBS="-lpcp_import $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h. */
+
+/* Override any GCC internal prototype to avoid an error.
+ Use char because int might match the return type of a GCC
+ builtin and then its argument prototype would still apply. */
+char pmiStart ();
+int
+main (void)
+{
+return pmiStart ();
+ ;
+ return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"
+then :
+ ac_cv_lib_pcp_import_pmiStart=yes
+else $as_nop
+ ac_cv_lib_pcp_import_pmiStart=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.beam \
+ conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_pcp_import_pmiStart" >&5
+printf "%s\n" "$ac_cv_lib_pcp_import_pmiStart" >&6; }
+if test "x$ac_cv_lib_pcp_import_pmiStart" = xyes
+then :
+ true
+else $as_nop
+ as_fn_error $? "Couldn't find pcp_import library $disable_msg" "$LINENO" 5
+
+fi
+
+ COCKPIT_PCP_LIBS="$COCKPIT_PCP_LIBS -lpcp"
+fi
+
+ if test "$enable_pcp" = "yes"; then
+ ENABLE_PCP_TRUE=
+ ENABLE_PCP_FALSE='#'
+else
+ ENABLE_PCP_TRUE='#'
+ ENABLE_PCP_FALSE=
+fi
+
+
+# systemd
+
+# Check whether --with-systemdunitdir was given.
+if test ${with_systemdunitdir+y}
+then :
+ withval=$with_systemdunitdir;
+fi
+
+
+if test ! -z "$with_systemdunitdir"; then
+ systemdunitdir=$with_systemdunitdir
+elif test "$enable_prefix_only" = "yes"; then
+ systemdunitdir='${prefix}/lib/systemd/system'
+else
+
+pkg_failed=no
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for systemd" >&5
+printf %s "checking for systemd... " >&6; }
+
+if test -n "$SYSTEMD_CFLAGS"; then
+ pkg_cv_SYSTEMD_CFLAGS="$SYSTEMD_CFLAGS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"systemd\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "systemd") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_SYSTEMD_CFLAGS=`$PKG_CONFIG --cflags "systemd" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+if test -n "$SYSTEMD_LIBS"; then
+ pkg_cv_SYSTEMD_LIBS="$SYSTEMD_LIBS"
+ elif test -n "$PKG_CONFIG"; then
+ if test -n "$PKG_CONFIG" && \
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"systemd\""; } >&5
+ ($PKG_CONFIG --exists --print-errors "systemd") 2>&5
+ ac_status=$?
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+ test $ac_status = 0; }; then
+ pkg_cv_SYSTEMD_LIBS=`$PKG_CONFIG --libs "systemd" 2>/dev/null`
+ test "x$?" != "x0" && pkg_failed=yes
+else
+ pkg_failed=yes
+fi
+ else
+ pkg_failed=untried
+fi
+
+
+
+if test $pkg_failed = yes; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+
+if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
+ _pkg_short_errors_supported=yes
+else
+ _pkg_short_errors_supported=no
+fi
+ if test $_pkg_short_errors_supported = yes; then
+ SYSTEMD_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "systemd" 2>&1`
+ else
+ SYSTEMD_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "systemd" 2>&1`
+ fi
+ # Put the nasty error message in config.log where it belongs
+ echo "$SYSTEMD_PKG_ERRORS" >&5
+
+ as_fn_error $? "Package requirements (systemd) were not met:
+
+$SYSTEMD_PKG_ERRORS
+
+Consider adjusting the PKG_CONFIG_PATH environment variable if you
+installed software in a non-standard prefix.
+
+Alternatively, you may set the environment variables SYSTEMD_CFLAGS
+and SYSTEMD_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details." "$LINENO" 5
+elif test $pkg_failed = untried; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it
+is in your PATH or set the PKG_CONFIG environment variable to the full
+path to pkg-config.
+
+Alternatively, you may set the environment variables SYSTEMD_CFLAGS
+and SYSTEMD_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details.
+
+To get pkg-config, see <http://pkg-config.freedesktop.org/>.
+See \`config.log' for more details" "$LINENO" 5; }
+else
+ SYSTEMD_CFLAGS=$pkg_cv_SYSTEMD_CFLAGS
+ SYSTEMD_LIBS=$pkg_cv_SYSTEMD_LIBS
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+
+fi
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for systemd unit dir" >&5
+printf %s "checking for systemd unit dir... " >&6; }
+ systemdunitdir=$($PKG_CONFIG systemd --variable=systemdsystemunitdir)
+ if test "$systemdunitdir" = ""; then
+ as_fn_error $? "systemd's pkg-config file doesn't contain 'systemdsystemunitdir' variable" "$LINENO" 5
+ fi
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $systemdunitdir" >&5
+printf "%s\n" "$systemdunitdir" >&6; }
+fi
+systemdunitdir=$systemdunitdir
+
+
+# We need msgcat, msgfmt, and xgettext, but they're all in the same
+# package as xgettext, and we find them by PATH, so just check for the one.
+# Extract the first word of "xgettext", so it can be a program name with args.
+set dummy xgettext; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_path_XGETTEXT+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ case $XGETTEXT in
+ [\\/]* | ?:[\\/]*)
+ ac_cv_path_XGETTEXT="$XGETTEXT" # Let the user override the test with a path.
+ ;;
+ *)
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_path_XGETTEXT="$as_dir$ac_word$ac_exec_ext"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+ test -z "$ac_cv_path_XGETTEXT" && ac_cv_path_XGETTEXT="no"
+ ;;
+esac
+fi
+XGETTEXT=$ac_cv_path_XGETTEXT
+if test -n "$XGETTEXT"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $XGETTEXT" >&5
+printf "%s\n" "$XGETTEXT" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+if test "$XGETTEXT" = "no"; then
+ as_fn_error $? "Please install gettext tools" "$LINENO" 5
+fi
+
+# ssh-add
+# Extract the first word of "ssh-add", so it can be a program name with args.
+set dummy ssh-add; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_path_SSH_ADD+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ case $SSH_ADD in
+ [\\/]* | ?:[\\/]*)
+ ac_cv_path_SSH_ADD="$SSH_ADD" # Let the user override the test with a path.
+ ;;
+ *)
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+as_dummy="$PATH:/usr/local/sbin:/usr/sbin:/sbin"
+for as_dir in $as_dummy
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_path_SSH_ADD="$as_dir$ac_word$ac_exec_ext"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+ test -z "$ac_cv_path_SSH_ADD" && ac_cv_path_SSH_ADD="/usr/bin/ssh-add"
+ ;;
+esac
+fi
+SSH_ADD=$ac_cv_path_SSH_ADD
+if test -n "$SSH_ADD"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $SSH_ADD" >&5
+printf "%s\n" "$SSH_ADD" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+
+printf "%s\n" "#define PATH_SSH_ADD \"$SSH_ADD\"" >>confdefs.h
+
+
+# ssh-agent
+# Extract the first word of "ssh-agent", so it can be a program name with args.
+set dummy ssh-agent; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_path_SSH_AGENT+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ case $SSH_AGENT in
+ [\\/]* | ?:[\\/]*)
+ ac_cv_path_SSH_AGENT="$SSH_AGENT" # Let the user override the test with a path.
+ ;;
+ *)
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+as_dummy="$PATH:/usr/local/bin:/usr/bin:/bin"
+for as_dir in $as_dummy
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_path_SSH_AGENT="$as_dir$ac_word$ac_exec_ext"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+ test -z "$ac_cv_path_SSH_AGENT" && ac_cv_path_SSH_AGENT="/usr/bin/ssh-agent"
+ ;;
+esac
+fi
+SSH_AGENT=$ac_cv_path_SSH_AGENT
+if test -n "$SSH_AGENT"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $SSH_AGENT" >&5
+printf "%s\n" "$SSH_AGENT" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+
+printf "%s\n" "#define PATH_SSH_AGENT \"$SSH_AGENT\"" >>confdefs.h
+
+
+# Address sanitizer
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for asan flags" >&5
+printf %s "checking for asan flags... " >&6; }
+# Check whether --enable-asan was given.
+if test ${enable_asan+y}
+then :
+ enableval=$enable_asan;
+fi
+
+
+if test "$enable_asan" = "yes"; then
+ CFLAGS="$CFLAGS -fsanitize=address -O1 -fno-omit-frame-pointer -g"
+ asan_status="yes"
+else
+ asan_status="no"
+fi
+ if test "$enable_asan" = "yes"; then
+ WITH_ASAN_TRUE=
+ WITH_ASAN_FALSE='#'
+else
+ WITH_ASAN_TRUE='#'
+ WITH_ASAN_FALSE=
+fi
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $asan_status" >&5
+printf "%s\n" "$asan_status" >&6; }
+
+# User and group for running cockpit web server (cockpit-tls or -ws in customized setups)
+
+
+# Check whether --with-cockpit_user was given.
+if test ${with_cockpit_user+y}
+then :
+ withval=$with_cockpit_user;
+fi
+
+
+# Check whether --with-cockpit_group was given.
+if test ${with_cockpit_group+y}
+then :
+ withval=$with_cockpit_group;
+fi
+
+if test -z "$with_cockpit_user"; then
+ COCKPIT_USER=root
+ COCKPIT_GROUP=
+else
+ COCKPIT_USER=$with_cockpit_user
+ if test -z "$with_cockpit_group"; then
+ COCKPIT_GROUP=$with_cockpit_user
+ else
+ COCKPIT_GROUP=$with_cockpit_group
+ fi
+fi
+
+
+
+
+# User for running cockpit-ws instances from cockpit-tls
+
+
+# Check whether --with-cockpit_ws_instance_user was given.
+if test ${with_cockpit_ws_instance_user+y}
+then :
+ withval=$with_cockpit_ws_instance_user;
+fi
+
+
+# Check whether --with-cockpit_ws_instance_group was given.
+if test ${with_cockpit_ws_instance_group+y}
+then :
+ withval=$with_cockpit_ws_instance_group;
+fi
+
+if test -z "$with_cockpit_ws_instance_user"; then
+ if test "$COCKPIT_USER" != "root"; then
+ as_fn_error $? "--with-cockpit-ws-instance-user is required when setting --with-cockpit-user" "$LINENO" 5
+ fi
+ COCKPIT_WSINSTANCE_USER=root
+else
+ COCKPIT_WSINSTANCE_USER=$with_cockpit_ws_instance_user
+ if test -z "$with_cockpit_ws_instance_group"; then
+ COCKPIT_WSINSTANCE_GROUP=$with_cockpit_ws_instance_user
+ else
+ COCKPIT_WSINSTANCE_GROUP=$with_cockpit_ws_instance_group
+ fi
+fi
+
+
+
+
+# admin users group
+
+# Check whether --with-admin-group was given.
+if test ${with_admin_group+y}
+then :
+ withval=$with_admin_group; admin_group=$withval
+else $as_nop
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for system group to which admin users belong" >&5
+printf %s "checking for system group to which admin users belong... " >&6; }
+ CANDIDATE_GROUPS="wheel sudo root"
+ admin_group="$(getent group ${CANDIDATE_GROUPS} | head -n1 | cut -f1 -d:)"
+ if test -n "$admin_group"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $admin_group" >&5
+printf "%s\n" "$admin_group" >&6; }
+ else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: unable to detect" >&5
+printf "%s\n" "unable to detect" >&6; }
+ as_fn_error $? "none of '${CANDIDATE_GROUPS}' exist: please specify a group with --with-admin-group=" "$LINENO" 5
+ fi
+
+fi
+
+
+
+# Default PATH for cockpit-session
+
+# Check whether --with-default-session-path was given.
+if test ${with_default_session_path+y}
+then :
+ withval=$with_default_session_path; default_session_path=$withval
+else $as_nop
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for cockpit-session PATH value" >&5
+printf %s "checking for cockpit-session PATH value... " >&6; }
+ if test "$(readlink /sbin)" == "usr/bin"; then
+ # This is Arch where "sbin" is symlinked to "bin" and
+ # "/bin" is symlinked to "/usr/bin". We use the
+ # normal Arch PATH which omits "sbin" and "/bin" for
+ # those reasons. Otherwise "pkexec" will find
+ # cockpit-bridge in "/usr/sbin" and our rule wont
+ # match.
+ default_session_path=/usr/local/sbin:/usr/local/bin:/usr/bin
+ else
+ default_session_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ fi
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $default_session_path" >&5
+printf "%s\n" "$default_session_path" >&6; }
+
+fi
+
+
+printf "%s\n" "#define DEFAULT_SESSION_PATH \"$default_session_path\"" >>confdefs.h
+
+
+# Documentation
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to build documentation" >&5
+printf %s "checking whether to build documentation... " >&6; }
+# Check whether --enable-doc was given.
+if test ${enable_doc+y}
+then :
+ enableval=$enable_doc;
+fi
+
+
+if test "$enable_doc" = "no"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_doc" >&5
+printf "%s\n" "$enable_doc" >&6; }
+
+else
+ if test "$enable_doc" = ""; then
+ disable_msg="(perhaps --disable-doc)"
+ fi
+
+ enable_doc="yes"
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_doc" >&5
+printf "%s\n" "$enable_doc" >&6; }
+
+ # Extract the first word of "xsltproc", so it can be a program name with args.
+set dummy xsltproc; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_path_XSLTPROC+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ case $XSLTPROC in
+ [\\/]* | ?:[\\/]*)
+ ac_cv_path_XSLTPROC="$XSLTPROC" # Let the user override the test with a path.
+ ;;
+ *)
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_path_XSLTPROC="$as_dir$ac_word$ac_exec_ext"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+ test -z "$ac_cv_path_XSLTPROC" && ac_cv_path_XSLTPROC="no"
+ ;;
+esac
+fi
+XSLTPROC=$ac_cv_path_XSLTPROC
+if test -n "$XSLTPROC"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $XSLTPROC" >&5
+printf "%s\n" "$XSLTPROC" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+ if test "$XSLTPROC" = "no"; then
+ as_fn_error $? "the xsltproc command was not found $disable_msg" "$LINENO" 5
+ fi
+
+ # Extract the first word of "xmlto", so it can be a program name with args.
+set dummy xmlto; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_path_XMLTO+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ case $XMLTO in
+ [\\/]* | ?:[\\/]*)
+ ac_cv_path_XMLTO="$XMLTO" # Let the user override the test with a path.
+ ;;
+ *)
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_path_XMLTO="$as_dir$ac_word$ac_exec_ext"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+ test -z "$ac_cv_path_XMLTO" && ac_cv_path_XMLTO="no"
+ ;;
+esac
+fi
+XMLTO=$ac_cv_path_XMLTO
+if test -n "$XMLTO"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $XMLTO" >&5
+printf "%s\n" "$XMLTO" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+ if test "$XMLTO" = "no"; then
+ as_fn_error $? "the xmlto command was not found $disable_msg" "$LINENO" 5
+ fi
+
+
+
+fi
+
+ if test "$enable_doc" = "yes"; then
+ ENABLE_DOC_TRUE=
+ ENABLE_DOC_FALSE='#'
+else
+ ENABLE_DOC_TRUE='#'
+ ENABLE_DOC_FALSE=
+fi
+
+
+# cockpit-client
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to install cockpit-client" >&5
+printf %s "checking whether to install cockpit-client... " >&6; }
+# Check whether --enable-cockpit-client was given.
+if test ${enable_cockpit_client+y}
+then :
+ enableval=$enable_cockpit_client;
+else $as_nop
+ enable_cockpit_client=no
+fi
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_cockpit_client" >&5
+printf "%s\n" "$enable_cockpit_client" >&6; }
+ if test "$enable_cockpit_client" = "yes"; then
+ ENABLE_COCKPIT_CLIENT_TRUE=
+ ENABLE_COCKPIT_CLIENT_FALSE='#'
+else
+ ENABLE_COCKPIT_CLIENT_TRUE='#'
+ ENABLE_COCKPIT_CLIENT_FALSE=
+fi
+
+
+if test "$enable_cockpit_client" = "yes" && test "$enable_old_bridge" = "yes"; then
+ as_fn_error $? "--enable-cockpit-client conflicts with --enable-old-bridge" "$LINENO" 5
+fi
+
+# Debug
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for debug mode" >&5
+printf %s "checking for debug mode... " >&6; }
+# Check whether --enable-debug was given.
+if test ${enable_debug+y}
+then :
+ enableval=$enable_debug;
+fi
+
+
+if test "$enable_debug" != "no"; then
+
+printf "%s\n" "#define WITH_DEBUG 1" >>confdefs.h
+
+
+printf "%s\n" "#define _DEBUG 1" >>confdefs.h
+
+ CFLAGS="$CFLAGS -g"
+fi
+debugdir='${prefix}/src/debug'
+if test "$enable_debug" = "yes"; then
+ debug_status="yes"
+ debugdir=
+ CFLAGS="$CFLAGS -O0"
+ NODE_ENV="development"
+elif test "$enable_debug" = "no"; then
+ debug_status="no"
+ CFLAGS="$CFLAGS -O2"
+ NODE_ENV="production"
+else
+ debug_status="default"
+ NODE_ENV="${NODE_ENV:-production}"
+fi
+ if test "$enable_debug" = "yes"; then
+ WITH_DEBUG_TRUE=
+ WITH_DEBUG_FALSE='#'
+else
+ WITH_DEBUG_TRUE='#'
+ WITH_DEBUG_FALSE=
+fi
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $debug_status" >&5
+printf "%s\n" "$debug_status" >&6; }
+
+
+
+# Coverage
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to build with coverage" >&5
+printf %s "checking whether to build with coverage... " >&6; }
+# Check whether --enable-coverage was given.
+if test ${enable_coverage+y}
+then :
+ enableval=$enable_coverage;
+else $as_nop
+ enable_coverage=no
+fi
+
+
+if test "$enable_coverage" = "yes"; then
+ if test "$GCC" != "yes"; then
+ as_fn_error $? "Coverage testing requires GCC" "$LINENO" 5
+ fi
+
+ CFLAGS="$CFLAGS -O0 -g --coverage"
+ LDFLAGS="$LDFLAGS --coverage"
+fi
+
+ if test "$enable_coverage" = "yes"; then
+ WITH_COVERAGE_TRUE=
+ WITH_COVERAGE_FALSE='#'
+else
+ WITH_COVERAGE_TRUE='#'
+ WITH_COVERAGE_FALSE=
+fi
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_coverage" >&5
+printf "%s\n" "$enable_coverage" >&6; }
+
+# Strict
+
+# Check whether --enable-strict was given.
+if test ${enable_strict+y}
+then :
+ enableval=$enable_strict;
+fi
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking build strict" >&5
+printf %s "checking build strict... " >&6; }
+if test "$enable_strict" = "yes"; then
+ CFLAGS="$CFLAGS -Werror"
+else
+ enable_strict="no"
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_strict" >&5
+printf "%s\n" "$enable_strict" >&6; }
+
+
+
+
+
+
+
+ if test -n "$PYTHON"; then
+ # If the user set $PYTHON, use it and don't search something else.
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $PYTHON version is >= 3.6" >&5
+printf %s "checking whether $PYTHON version is >= 3.6... " >&6; }
+ prog="import sys
+# split strings by '.' and convert to numeric. Append some zeros
+# because we need at least 4 digits for the hex conversion.
+# map returns an iterator in Python 3.0 and a list in 2.x
+minver = list(map(int, '3.6'.split('.'))) + [0, 0, 0]
+minverhex = 0
+# xrange is not present in Python 3.0 and range returns an iterator
+for i in list(range(0, 4)): minverhex = (minverhex << 8) + minver[i]
+sys.exit(sys.hexversion < minverhex)"
+ if { echo "$as_me:$LINENO: $PYTHON -c "$prog"" >&5
+ ($PYTHON -c "$prog") >&5 2>&5
+ ac_status=$?
+ echo "$as_me:$LINENO: \$? = $ac_status" >&5
+ (exit $ac_status); }
+then :
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+printf "%s\n" "yes" >&6; }
+else $as_nop
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+ as_fn_error $? "Python interpreter is too old" "$LINENO" 5
+fi
+ am_display_PYTHON=$PYTHON
+ else
+ # Otherwise, try each interpreter until we find one that satisfies
+ # VERSION.
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for a Python interpreter with version >= 3.6" >&5
+printf %s "checking for a Python interpreter with version >= 3.6... " >&6; }
+if test ${am_cv_pathless_PYTHON+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+
+ for am_cv_pathless_PYTHON in python python2 python3 python3.11 python3.10 python3.9 python3.8 python3.7 python3.6 python3.5 python3.4 python3.3 python3.2 python3.1 python3.0 python2.7 python2.6 python2.5 python2.4 python2.3 python2.2 python2.1 python2.0 none; do
+ test "$am_cv_pathless_PYTHON" = none && break
+ prog="import sys
+# split strings by '.' and convert to numeric. Append some zeros
+# because we need at least 4 digits for the hex conversion.
+# map returns an iterator in Python 3.0 and a list in 2.x
+minver = list(map(int, '3.6'.split('.'))) + [0, 0, 0]
+minverhex = 0
+# xrange is not present in Python 3.0 and range returns an iterator
+for i in list(range(0, 4)): minverhex = (minverhex << 8) + minver[i]
+sys.exit(sys.hexversion < minverhex)"
+ if { echo "$as_me:$LINENO: $am_cv_pathless_PYTHON -c "$prog"" >&5
+ ($am_cv_pathless_PYTHON -c "$prog") >&5 2>&5
+ ac_status=$?
+ echo "$as_me:$LINENO: \$? = $ac_status" >&5
+ (exit $ac_status); }
+then :
+ break
+fi
+ done
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_pathless_PYTHON" >&5
+printf "%s\n" "$am_cv_pathless_PYTHON" >&6; }
+ # Set $PYTHON to the absolute path of $am_cv_pathless_PYTHON.
+ if test "$am_cv_pathless_PYTHON" = none; then
+ PYTHON=:
+ else
+ # Extract the first word of "$am_cv_pathless_PYTHON", so it can be a program name with args.
+set dummy $am_cv_pathless_PYTHON; ac_word=$2
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
+printf %s "checking for $ac_word... " >&6; }
+if test ${ac_cv_path_PYTHON+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ case $PYTHON in
+ [\\/]* | ?:[\\/]*)
+ ac_cv_path_PYTHON="$PYTHON" # Let the user override the test with a path.
+ ;;
+ *)
+ as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ for ac_exec_ext in '' $ac_executable_extensions; do
+ if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then
+ ac_cv_path_PYTHON="$as_dir$ac_word$ac_exec_ext"
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5
+ break 2
+ fi
+done
+ done
+IFS=$as_save_IFS
+
+ ;;
+esac
+fi
+PYTHON=$ac_cv_path_PYTHON
+if test -n "$PYTHON"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $PYTHON" >&5
+printf "%s\n" "$PYTHON" >&6; }
+else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
+printf "%s\n" "no" >&6; }
+fi
+
+
+ fi
+ am_display_PYTHON=$am_cv_pathless_PYTHON
+ fi
+
+
+ if test "$PYTHON" = :; then
+ as_fn_error $? "no suitable Python interpreter found" "$LINENO" 5
+ else
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $am_display_PYTHON version" >&5
+printf %s "checking for $am_display_PYTHON version... " >&6; }
+if test ${am_cv_python_version+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ am_cv_python_version=`$PYTHON -c "import sys; print ('%u.%u' % sys.version_info[:2])"`
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_python_version" >&5
+printf "%s\n" "$am_cv_python_version" >&6; }
+ PYTHON_VERSION=$am_cv_python_version
+
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $am_display_PYTHON platform" >&5
+printf %s "checking for $am_display_PYTHON platform... " >&6; }
+if test ${am_cv_python_platform+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ am_cv_python_platform=`$PYTHON -c "import sys; sys.stdout.write(sys.platform)"`
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_python_platform" >&5
+printf "%s\n" "$am_cv_python_platform" >&6; }
+ PYTHON_PLATFORM=$am_cv_python_platform
+
+
+ if test "x$prefix" = xNONE; then
+ am__usable_prefix=$ac_default_prefix
+ else
+ am__usable_prefix=$prefix
+ fi
+
+ # Allow user to request using sys.* values from Python,
+ # instead of the GNU $prefix values.
+
+# Check whether --with-python-sys-prefix was given.
+if test ${with_python_sys_prefix+y}
+then :
+ withval=$with_python_sys_prefix; am_use_python_sys=:
+else $as_nop
+ am_use_python_sys=false
+fi
+
+
+ # Allow user to override whatever the default Python prefix is.
+
+# Check whether --with-python_prefix was given.
+if test ${with_python_prefix+y}
+then :
+ withval=$with_python_prefix; am_python_prefix_subst=$withval
+ am_cv_python_prefix=$withval
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for explicit $am_display_PYTHON prefix" >&5
+printf %s "checking for explicit $am_display_PYTHON prefix... " >&6; }
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_python_prefix" >&5
+printf "%s\n" "$am_cv_python_prefix" >&6; }
+else $as_nop
+
+ if $am_use_python_sys; then
+ # using python sys.prefix value, not GNU
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for python default $am_display_PYTHON prefix" >&5
+printf %s "checking for python default $am_display_PYTHON prefix... " >&6; }
+if test ${am_cv_python_prefix+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ am_cv_python_prefix=`$PYTHON -c "import sys; sys.stdout.write(sys.prefix)"`
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_python_prefix" >&5
+printf "%s\n" "$am_cv_python_prefix" >&6; }
+
+ case $am_cv_python_prefix in
+ $am__usable_prefix*)
+ am__strip_prefix=`echo "$am__usable_prefix" | sed 's|.|.|g'`
+ am_python_prefix_subst=`echo "$am_cv_python_prefix" | sed "s,^$am__strip_prefix,\\${prefix},"`
+ ;;
+ *)
+ am_python_prefix_subst=$am_cv_python_prefix
+ ;;
+ esac
+ else # using GNU prefix value, not python sys.prefix
+ am_python_prefix_subst='${prefix}'
+ am_python_prefix=$am_python_prefix_subst
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GNU default $am_display_PYTHON prefix" >&5
+printf %s "checking for GNU default $am_display_PYTHON prefix... " >&6; }
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_python_prefix" >&5
+printf "%s\n" "$am_python_prefix" >&6; }
+ fi
+fi
+
+ # Substituting python_prefix_subst value.
+ PYTHON_PREFIX=$am_python_prefix_subst
+
+
+ # emacs-page Now do it all over again for Python exec_prefix, but with yet
+ # another conditional: fall back to regular prefix if that was specified.
+
+# Check whether --with-python_exec_prefix was given.
+if test ${with_python_exec_prefix+y}
+then :
+ withval=$with_python_exec_prefix; am_python_exec_prefix_subst=$withval
+ am_cv_python_exec_prefix=$withval
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for explicit $am_display_PYTHON exec_prefix" >&5
+printf %s "checking for explicit $am_display_PYTHON exec_prefix... " >&6; }
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_python_exec_prefix" >&5
+printf "%s\n" "$am_cv_python_exec_prefix" >&6; }
+else $as_nop
+
+ # no explicit --with-python_exec_prefix, but if
+ # --with-python_prefix was given, use its value for python_exec_prefix too.
+ if test -n "$with_python_prefix"
+then :
+ am_python_exec_prefix_subst=$with_python_prefix
+ am_cv_python_exec_prefix=$with_python_prefix
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for python_prefix-given $am_display_PYTHON exec_prefix" >&5
+printf %s "checking for python_prefix-given $am_display_PYTHON exec_prefix... " >&6; }
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_python_exec_prefix" >&5
+printf "%s\n" "$am_cv_python_exec_prefix" >&6; }
+else $as_nop
+
+ # Set am__usable_exec_prefix whether using GNU or Python values,
+ # since we use that variable for pyexecdir.
+ if test "x$exec_prefix" = xNONE; then
+ am__usable_exec_prefix=$am__usable_prefix
+ else
+ am__usable_exec_prefix=$exec_prefix
+ fi
+ #
+ if $am_use_python_sys; then # using python sys.exec_prefix, not GNU
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for python default $am_display_PYTHON exec_prefix" >&5
+printf %s "checking for python default $am_display_PYTHON exec_prefix... " >&6; }
+if test ${am_cv_python_exec_prefix+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ am_cv_python_exec_prefix=`$PYTHON -c "import sys; sys.stdout.write(sys.exec_prefix)"`
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_python_exec_prefix" >&5
+printf "%s\n" "$am_cv_python_exec_prefix" >&6; }
+ case $am_cv_python_exec_prefix in
+ $am__usable_exec_prefix*)
+ am__strip_prefix=`echo "$am__usable_exec_prefix" | sed 's|.|.|g'`
+ am_python_exec_prefix_subst=`echo "$am_cv_python_exec_prefix" | sed "s,^$am__strip_prefix,\\${exec_prefix},"`
+ ;;
+ *)
+ am_python_exec_prefix_subst=$am_cv_python_exec_prefix
+ ;;
+ esac
+ else # using GNU $exec_prefix, not python sys.exec_prefix
+ am_python_exec_prefix_subst='${exec_prefix}'
+ am_python_exec_prefix=$am_python_exec_prefix_subst
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GNU default $am_display_PYTHON exec_prefix" >&5
+printf %s "checking for GNU default $am_display_PYTHON exec_prefix... " >&6; }
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_python_exec_prefix" >&5
+printf "%s\n" "$am_python_exec_prefix" >&6; }
+ fi
+fi
+fi
+
+ # Substituting python_exec_prefix_subst.
+ PYTHON_EXEC_PREFIX=$am_python_exec_prefix_subst
+
+
+ # Factor out some code duplication into this shell variable.
+ am_python_setup_sysconfig="\
+import sys
+# Prefer sysconfig over distutils.sysconfig, for better compatibility
+# with python 3.x. See automake bug#10227.
+try:
+ import sysconfig
+except ImportError:
+ can_use_sysconfig = 0
+else:
+ can_use_sysconfig = 1
+# Can't use sysconfig in CPython 2.7, since it's broken in virtualenvs:
+# <https://github.com/pypa/virtualenv/issues/118>
+try:
+ from platform import python_implementation
+ if python_implementation() == 'CPython' and sys.version[:3] == '2.7':
+ can_use_sysconfig = 0
+except ImportError:
+ pass"
+
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $am_display_PYTHON script directory (pythondir)" >&5
+printf %s "checking for $am_display_PYTHON script directory (pythondir)... " >&6; }
+if test ${am_cv_python_pythondir+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test "x$am_cv_python_prefix" = x; then
+ am_py_prefix=$am__usable_prefix
+ else
+ am_py_prefix=$am_cv_python_prefix
+ fi
+ am_cv_python_pythondir=`$PYTHON -c "
+$am_python_setup_sysconfig
+if can_use_sysconfig:
+ if hasattr(sysconfig, 'get_default_scheme'):
+ scheme = sysconfig.get_default_scheme()
+ else:
+ scheme = sysconfig._get_default_scheme()
+ if scheme == 'posix_local':
+ # Debian's default scheme installs to /usr/local/ but we want to find headers in /usr/
+ scheme = 'posix_prefix'
+ sitedir = sysconfig.get_path('purelib', scheme, vars={'base':'$am_py_prefix'})
+else:
+ from distutils import sysconfig
+ sitedir = sysconfig.get_python_lib(0, 0, prefix='$am_py_prefix')
+sys.stdout.write(sitedir)"`
+ #
+ case $am_cv_python_pythondir in
+ $am_py_prefix*)
+ am__strip_prefix=`echo "$am_py_prefix" | sed 's|.|.|g'`
+ am_cv_python_pythondir=`echo "$am_cv_python_pythondir" | sed "s,^$am__strip_prefix,\\${PYTHON_PREFIX},"`
+ ;;
+ *)
+ case $am_py_prefix in
+ /usr|/System*) ;;
+ *) am_cv_python_pythondir="\${PYTHON_PREFIX}/lib/python$PYTHON_VERSION/site-packages"
+ ;;
+ esac
+ ;;
+ esac
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_python_pythondir" >&5
+printf "%s\n" "$am_cv_python_pythondir" >&6; }
+ pythondir=$am_cv_python_pythondir
+
+
+ pkgpythondir=\${pythondir}/$PACKAGE
+
+
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $am_display_PYTHON extension module directory (pyexecdir)" >&5
+printf %s "checking for $am_display_PYTHON extension module directory (pyexecdir)... " >&6; }
+if test ${am_cv_python_pyexecdir+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test "x$am_cv_python_exec_prefix" = x; then
+ am_py_exec_prefix=$am__usable_exec_prefix
+ else
+ am_py_exec_prefix=$am_cv_python_exec_prefix
+ fi
+ am_cv_python_pyexecdir=`$PYTHON -c "
+$am_python_setup_sysconfig
+if can_use_sysconfig:
+ if hasattr(sysconfig, 'get_default_scheme'):
+ scheme = sysconfig.get_default_scheme()
+ else:
+ scheme = sysconfig._get_default_scheme()
+ if scheme == 'posix_local':
+ # Debian's default scheme installs to /usr/local/ but we want to find headers in /usr/
+ scheme = 'posix_prefix'
+ sitedir = sysconfig.get_path('platlib', scheme, vars={'platbase':'$am_py_exec_prefix'})
+else:
+ from distutils import sysconfig
+ sitedir = sysconfig.get_python_lib(1, 0, prefix='$am_py_exec_prefix')
+sys.stdout.write(sitedir)"`
+ #
+ case $am_cv_python_pyexecdir in
+ $am_py_exec_prefix*)
+ am__strip_prefix=`echo "$am_py_exec_prefix" | sed 's|.|.|g'`
+ am_cv_python_pyexecdir=`echo "$am_cv_python_pyexecdir" | sed "s,^$am__strip_prefix,\\${PYTHON_EXEC_PREFIX},"`
+ ;;
+ *)
+ case $am_py_exec_prefix in
+ /usr|/System*) ;;
+ *) am_cv_python_pyexecdir="\${PYTHON_EXEC_PREFIX}/lib/python$PYTHON_VERSION/site-packages"
+ ;;
+ esac
+ ;;
+ esac
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $am_cv_python_pyexecdir" >&5
+printf "%s\n" "$am_cv_python_pyexecdir" >&6; }
+ pyexecdir=$am_cv_python_pyexecdir
+
+
+ pkgpyexecdir=\${pyexecdir}/$PACKAGE
+
+
+
+ fi
+
+
+# Generate
+#
+
+
+
+ac_config_files="$ac_config_files Makefile doc/guide/version src/tls/cockpit-certificate-helper src/ws/cockpit-desktop"
+
+cat >confcache <<\_ACEOF
+# This file is a shell script that caches the results of configure
+# tests run on this system so they can be shared between configure
+# scripts and configure runs, see configure's option --config-cache.
+# It is not useful on other systems. If it contains results you don't
+# want to keep, you may remove or edit it.
+#
+# config.status only pays attention to the cache file if you give it
+# the --recheck option to rerun configure.
+#
+# `ac_cv_env_foo' variables (set or unset) will be overridden when
+# loading this file, other *unset* `ac_cv_foo' will be assigned the
+# following values.
+
+_ACEOF
+
+# The following way of writing the cache mishandles newlines in values,
+# but we know of no workaround that is simple, portable, and efficient.
+# So, we kill variables containing newlines.
+# Ultrix sh set writes to stderr and can't be redirected directly,
+# and sets the high bit in the cache file unless we assign to the vars.
+(
+ for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do
+ eval ac_val=\$$ac_var
+ case $ac_val in #(
+ *${as_nl}*)
+ case $ac_var in #(
+ *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5
+printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;;
+ esac
+ case $ac_var in #(
+ _ | IFS | as_nl) ;; #(
+ BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #(
+ *) { eval $ac_var=; unset $ac_var;} ;;
+ esac ;;
+ esac
+ done
+
+ (set) 2>&1 |
+ case $as_nl`(ac_space=' '; set) 2>&1` in #(
+ *${as_nl}ac_space=\ *)
+ # `set' does not quote correctly, so add quotes: double-quote
+ # substitution turns \\\\ into \\, and sed turns \\ into \.
+ sed -n \
+ "s/'/'\\\\''/g;
+ s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p"
+ ;; #(
+ *)
+ # `set' quotes correctly as required by POSIX, so do not add quotes.
+ sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p"
+ ;;
+ esac |
+ sort
+) |
+ sed '
+ /^ac_cv_env_/b end
+ t clear
+ :clear
+ s/^\([^=]*\)=\(.*[{}].*\)$/test ${\1+y} || &/
+ t end
+ s/^\([^=]*\)=\(.*\)$/\1=${\1=\2}/
+ :end' >>confcache
+if diff "$cache_file" confcache >/dev/null 2>&1; then :; else
+ if test -w "$cache_file"; then
+ if test "x$cache_file" != "x/dev/null"; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5
+printf "%s\n" "$as_me: updating cache $cache_file" >&6;}
+ if test ! -f "$cache_file" || test -h "$cache_file"; then
+ cat confcache >"$cache_file"
+ else
+ case $cache_file in #(
+ */* | ?:*)
+ mv -f confcache "$cache_file"$$ &&
+ mv -f "$cache_file"$$ "$cache_file" ;; #(
+ *)
+ mv -f confcache "$cache_file" ;;
+ esac
+ fi
+ fi
+ else
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5
+printf "%s\n" "$as_me: not updating unwritable cache $cache_file" >&6;}
+ fi
+fi
+rm -f confcache
+
+test "x$prefix" = xNONE && prefix=$ac_default_prefix
+# Let make expand exec_prefix.
+test "x$exec_prefix" = xNONE && exec_prefix='${prefix}'
+
+DEFS=-DHAVE_CONFIG_H
+
+ac_libobjs=
+ac_ltlibobjs=
+U=
+for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue
+ # 1. Remove the extension, and $U if already installed.
+ ac_script='s/\$U\././;s/\.o$//;s/\.obj$//'
+ ac_i=`printf "%s\n" "$ac_i" | sed "$ac_script"`
+ # 2. Prepend LIBOBJDIR. When used with automake>=1.10 LIBOBJDIR
+ # will be set to the directory where LIBOBJS objects are built.
+ as_fn_append ac_libobjs " \${LIBOBJDIR}$ac_i\$U.$ac_objext"
+ as_fn_append ac_ltlibobjs " \${LIBOBJDIR}$ac_i"'$U.lo'
+done
+LIBOBJS=$ac_libobjs
+
+LTLIBOBJS=$ac_ltlibobjs
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking that generated files are newer than configure" >&5
+printf %s "checking that generated files are newer than configure... " >&6; }
+ if test -n "$am_sleep_pid"; then
+ # Hide warnings about reused PIDs.
+ wait $am_sleep_pid 2>/dev/null
+ fi
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: done" >&5
+printf "%s\n" "done" >&6; }
+ if test -n "$EXEEXT"; then
+ am__EXEEXT_TRUE=
+ am__EXEEXT_FALSE='#'
+else
+ am__EXEEXT_TRUE='#'
+ am__EXEEXT_FALSE=
+fi
+
+if test -z "${AMDEP_TRUE}" && test -z "${AMDEP_FALSE}"; then
+ as_fn_error $? "conditional \"AMDEP\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${am__fastdepCC_TRUE}" && test -z "${am__fastdepCC_FALSE}"; then
+ as_fn_error $? "conditional \"am__fastdepCC\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${SELINUX_POLICY_ENABLED_TRUE}" && test -z "${SELINUX_POLICY_ENABLED_FALSE}"; then
+ as_fn_error $? "conditional \"SELINUX_POLICY_ENABLED\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${WITH_POLKIT_TRUE}" && test -z "${WITH_POLKIT_FALSE}"; then
+ as_fn_error $? "conditional \"WITH_POLKIT\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${WITH_COCKPIT_SSH_TRUE}" && test -z "${WITH_COCKPIT_SSH_FALSE}"; then
+ as_fn_error $? "conditional \"WITH_COCKPIT_SSH\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${WITH_OLD_BRIDGE_TRUE}" && test -z "${WITH_OLD_BRIDGE_FALSE}"; then
+ as_fn_error $? "conditional \"WITH_OLD_BRIDGE\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${ENABLE_PCP_TRUE}" && test -z "${ENABLE_PCP_FALSE}"; then
+ as_fn_error $? "conditional \"ENABLE_PCP\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${WITH_ASAN_TRUE}" && test -z "${WITH_ASAN_FALSE}"; then
+ as_fn_error $? "conditional \"WITH_ASAN\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${ENABLE_DOC_TRUE}" && test -z "${ENABLE_DOC_FALSE}"; then
+ as_fn_error $? "conditional \"ENABLE_DOC\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${ENABLE_COCKPIT_CLIENT_TRUE}" && test -z "${ENABLE_COCKPIT_CLIENT_FALSE}"; then
+ as_fn_error $? "conditional \"ENABLE_COCKPIT_CLIENT\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${WITH_DEBUG_TRUE}" && test -z "${WITH_DEBUG_FALSE}"; then
+ as_fn_error $? "conditional \"WITH_DEBUG\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+if test -z "${WITH_COVERAGE_TRUE}" && test -z "${WITH_COVERAGE_FALSE}"; then
+ as_fn_error $? "conditional \"WITH_COVERAGE\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
+
+: "${CONFIG_STATUS=./config.status}"
+ac_write_fail=0
+ac_clean_files_save=$ac_clean_files
+ac_clean_files="$ac_clean_files $CONFIG_STATUS"
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5
+printf "%s\n" "$as_me: creating $CONFIG_STATUS" >&6;}
+as_write_fail=0
+cat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1
+#! $SHELL
+# Generated by $as_me.
+# Run this file to recreate the current configuration.
+# Compiler output produced by configure, useful for debugging
+# configure, is in config.log if it exists.
+
+debug=false
+ac_cs_recheck=false
+ac_cs_silent=false
+
+SHELL=\${CONFIG_SHELL-$SHELL}
+export SHELL
+_ASEOF
+cat >>$CONFIG_STATUS <<\_ASEOF || as_write_fail=1
+## -------------------- ##
+## M4sh Initialization. ##
+## -------------------- ##
+
+# Be more Bourne compatible
+DUALCASE=1; export DUALCASE # for MKS sh
+as_nop=:
+if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1
+then :
+ emulate sh
+ NULLCMD=:
+ # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which
+ # is contrary to our usage. Disable this feature.
+ alias -g '${1+"$@"}'='"$@"'
+ setopt NO_GLOB_SUBST
+else $as_nop
+ case `(set -o) 2>/dev/null` in #(
+ *posix*) :
+ set -o posix ;; #(
+ *) :
+ ;;
+esac
+fi
+
+
+
+# Reset variables that may have inherited troublesome values from
+# the environment.
+
+# IFS needs to be set, to space, tab, and newline, in precisely that order.
+# (If _AS_PATH_WALK were called with IFS unset, it would have the
+# side effect of setting IFS to empty, thus disabling word splitting.)
+# Quoting is to prevent editors from complaining about space-tab.
+as_nl='
+'
+export as_nl
+IFS=" "" $as_nl"
+
+PS1='$ '
+PS2='> '
+PS4='+ '
+
+# Ensure predictable behavior from utilities with locale-dependent output.
+LC_ALL=C
+export LC_ALL
+LANGUAGE=C
+export LANGUAGE
+
+# We cannot yet rely on "unset" to work, but we need these variables
+# to be unset--not just set to an empty or harmless value--now, to
+# avoid bugs in old shells (e.g. pre-3.0 UWIN ksh). This construct
+# also avoids known problems related to "unset" and subshell syntax
+# in other old shells (e.g. bash 2.01 and pdksh 5.2.14).
+for as_var in BASH_ENV ENV MAIL MAILPATH CDPATH
+do eval test \${$as_var+y} \
+ && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || :
+done
+
+# Ensure that fds 0, 1, and 2 are open.
+if (exec 3>&0) 2>/dev/null; then :; else exec 0</dev/null; fi
+if (exec 3>&1) 2>/dev/null; then :; else exec 1>/dev/null; fi
+if (exec 3>&2) ; then :; else exec 2>/dev/null; fi
+
+# The user is always right.
+if ${PATH_SEPARATOR+false} :; then
+ PATH_SEPARATOR=:
+ (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && {
+ (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 ||
+ PATH_SEPARATOR=';'
+ }
+fi
+
+
+# Find who we are. Look in the path if we contain no directory separator.
+as_myself=
+case $0 in #((
+ *[\\/]* ) as_myself=$0 ;;
+ *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
+for as_dir in $PATH
+do
+ IFS=$as_save_IFS
+ case $as_dir in #(((
+ '') as_dir=./ ;;
+ */) ;;
+ *) as_dir=$as_dir/ ;;
+ esac
+ test -r "$as_dir$0" && as_myself=$as_dir$0 && break
+ done
+IFS=$as_save_IFS
+
+ ;;
+esac
+# We did not find ourselves, most probably we were run as `sh COMMAND'
+# in which case we are not to be found in the path.
+if test "x$as_myself" = x; then
+ as_myself=$0
+fi
+if test ! -f "$as_myself"; then
+ printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2
+ exit 1
+fi
+
+
+
+# as_fn_error STATUS ERROR [LINENO LOG_FD]
+# ----------------------------------------
+# Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are
+# provided, also output the error to LOG_FD, referencing LINENO. Then exit the
+# script with STATUS, using 1 if that was 0.
+as_fn_error ()
+{
+ as_status=$1; test $as_status -eq 0 && as_status=1
+ if test "$4"; then
+ as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
+ printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4
+ fi
+ printf "%s\n" "$as_me: error: $2" >&2
+ as_fn_exit $as_status
+} # as_fn_error
+
+
+
+# as_fn_set_status STATUS
+# -----------------------
+# Set $? to STATUS, without forking.
+as_fn_set_status ()
+{
+ return $1
+} # as_fn_set_status
+
+# as_fn_exit STATUS
+# -----------------
+# Exit the shell with STATUS, even in a "trap 0" or "set -e" context.
+as_fn_exit ()
+{
+ set +e
+ as_fn_set_status $1
+ exit $1
+} # as_fn_exit
+
+# as_fn_unset VAR
+# ---------------
+# Portably unset VAR.
+as_fn_unset ()
+{
+ { eval $1=; unset $1;}
+}
+as_unset=as_fn_unset
+
+# as_fn_append VAR VALUE
+# ----------------------
+# Append the text in VALUE to the end of the definition contained in VAR. Take
+# advantage of any shell optimizations that allow amortized linear growth over
+# repeated appends, instead of the typical quadratic growth present in naive
+# implementations.
+if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null
+then :
+ eval 'as_fn_append ()
+ {
+ eval $1+=\$2
+ }'
+else $as_nop
+ as_fn_append ()
+ {
+ eval $1=\$$1\$2
+ }
+fi # as_fn_append
+
+# as_fn_arith ARG...
+# ------------------
+# Perform arithmetic evaluation on the ARGs, and store the result in the
+# global $as_val. Take advantage of shells that can avoid forks. The arguments
+# must be portable across $(()) and expr.
+if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null
+then :
+ eval 'as_fn_arith ()
+ {
+ as_val=$(( $* ))
+ }'
+else $as_nop
+ as_fn_arith ()
+ {
+ as_val=`expr "$@" || test $? -eq 1`
+ }
+fi # as_fn_arith
+
+
+if expr a : '\(a\)' >/dev/null 2>&1 &&
+ test "X`expr 00001 : '.*\(...\)'`" = X001; then
+ as_expr=expr
+else
+ as_expr=false
+fi
+
+if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then
+ as_basename=basename
+else
+ as_basename=false
+fi
+
+if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then
+ as_dirname=dirname
+else
+ as_dirname=false
+fi
+
+as_me=`$as_basename -- "$0" ||
+$as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \
+ X"$0" : 'X\(//\)$' \| \
+ X"$0" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X/"$0" |
+ sed '/^.*\/\([^/][^/]*\)\/*$/{
+ s//\1/
+ q
+ }
+ /^X\/\(\/\/\)$/{
+ s//\1/
+ q
+ }
+ /^X\/\(\/\).*/{
+ s//\1/
+ q
+ }
+ s/.*/./; q'`
+
+# Avoid depending upon Character Ranges.
+as_cr_letters='abcdefghijklmnopqrstuvwxyz'
+as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+as_cr_Letters=$as_cr_letters$as_cr_LETTERS
+as_cr_digits='0123456789'
+as_cr_alnum=$as_cr_Letters$as_cr_digits
+
+
+# Determine whether it's possible to make 'echo' print without a newline.
+# These variables are no longer used directly by Autoconf, but are AC_SUBSTed
+# for compatibility with existing Makefiles.
+ECHO_C= ECHO_N= ECHO_T=
+case `echo -n x` in #(((((
+-n*)
+ case `echo 'xy\c'` in
+ *c*) ECHO_T=' ';; # ECHO_T is single tab character.
+ xy) ECHO_C='\c';;
+ *) echo `echo ksh88 bug on AIX 6.1` > /dev/null
+ ECHO_T=' ';;
+ esac;;
+*)
+ ECHO_N='-n';;
+esac
+
+# For backward compatibility with old third-party macros, we provide
+# the shell variables $as_echo and $as_echo_n. New code should use
+# AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively.
+as_echo='printf %s\n'
+as_echo_n='printf %s'
+
+rm -f conf$$ conf$$.exe conf$$.file
+if test -d conf$$.dir; then
+ rm -f conf$$.dir/conf$$.file
+else
+ rm -f conf$$.dir
+ mkdir conf$$.dir 2>/dev/null
+fi
+if (echo >conf$$.file) 2>/dev/null; then
+ if ln -s conf$$.file conf$$ 2>/dev/null; then
+ as_ln_s='ln -s'
+ # ... but there are two gotchas:
+ # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail.
+ # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable.
+ # In both cases, we have to default to `cp -pR'.
+ ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe ||
+ as_ln_s='cp -pR'
+ elif ln conf$$.file conf$$ 2>/dev/null; then
+ as_ln_s=ln
+ else
+ as_ln_s='cp -pR'
+ fi
+else
+ as_ln_s='cp -pR'
+fi
+rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file
+rmdir conf$$.dir 2>/dev/null
+
+
+# as_fn_mkdir_p
+# -------------
+# Create "$as_dir" as a directory, including parents if necessary.
+as_fn_mkdir_p ()
+{
+
+ case $as_dir in #(
+ -*) as_dir=./$as_dir;;
+ esac
+ test -d "$as_dir" || eval $as_mkdir_p || {
+ as_dirs=
+ while :; do
+ case $as_dir in #(
+ *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'(
+ *) as_qdir=$as_dir;;
+ esac
+ as_dirs="'$as_qdir' $as_dirs"
+ as_dir=`$as_dirname -- "$as_dir" ||
+$as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+ X"$as_dir" : 'X\(//\)[^/]' \| \
+ X"$as_dir" : 'X\(//\)$' \| \
+ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$as_dir" |
+ sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)[^/].*/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\).*/{
+ s//\1/
+ q
+ }
+ s/.*/./; q'`
+ test -d "$as_dir" && break
+ done
+ test -z "$as_dirs" || eval "mkdir $as_dirs"
+ } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir"
+
+
+} # as_fn_mkdir_p
+if mkdir -p . 2>/dev/null; then
+ as_mkdir_p='mkdir -p "$as_dir"'
+else
+ test -d ./-p && rmdir ./-p
+ as_mkdir_p=false
+fi
+
+
+# as_fn_executable_p FILE
+# -----------------------
+# Test if FILE is an executable regular file.
+as_fn_executable_p ()
+{
+ test -f "$1" && test -x "$1"
+} # as_fn_executable_p
+as_test_x='test -x'
+as_executable_p=as_fn_executable_p
+
+# Sed expression to map a string onto a valid CPP name.
+as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'"
+
+# Sed expression to map a string onto a valid variable name.
+as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'"
+
+
+exec 6>&1
+## ----------------------------------- ##
+## Main body of $CONFIG_STATUS script. ##
+## ----------------------------------- ##
+_ASEOF
+test $as_write_fail = 0 && chmod +x $CONFIG_STATUS || ac_write_fail=1
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+# Save the log message, to keep $0 and so on meaningful, and to
+# report actual input values of CONFIG_FILES etc. instead of their
+# values after options handling.
+ac_log="
+This file was extended by Cockpit $as_me 311, which was
+generated by GNU Autoconf 2.71. Invocation command line was
+
+ CONFIG_FILES = $CONFIG_FILES
+ CONFIG_HEADERS = $CONFIG_HEADERS
+ CONFIG_LINKS = $CONFIG_LINKS
+ CONFIG_COMMANDS = $CONFIG_COMMANDS
+ $ $0 $@
+
+on `(hostname || uname -n) 2>/dev/null | sed 1q`
+"
+
+_ACEOF
+
+case $ac_config_files in *"
+"*) set x $ac_config_files; shift; ac_config_files=$*;;
+esac
+
+case $ac_config_headers in *"
+"*) set x $ac_config_headers; shift; ac_config_headers=$*;;
+esac
+
+
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+# Files that config.status was made for.
+config_files="$ac_config_files"
+config_headers="$ac_config_headers"
+config_commands="$ac_config_commands"
+
+_ACEOF
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+ac_cs_usage="\
+\`$as_me' instantiates files and other configuration actions
+from templates according to the current configuration. Unless the files
+and actions are specified as TAGs, all are instantiated by default.
+
+Usage: $0 [OPTION]... [TAG]...
+
+ -h, --help print this help, then exit
+ -V, --version print version number and configuration settings, then exit
+ --config print configuration, then exit
+ -q, --quiet, --silent
+ do not print progress messages
+ -d, --debug don't remove temporary files
+ --recheck update $as_me by reconfiguring in the same conditions
+ --file=FILE[:TEMPLATE]
+ instantiate the configuration file FILE
+ --header=FILE[:TEMPLATE]
+ instantiate the configuration header FILE
+
+Configuration files:
+$config_files
+
+Configuration headers:
+$config_headers
+
+Configuration commands:
+$config_commands
+
+Report bugs to <devel@lists.cockpit-project.org>.
+Cockpit home page: <https://cockpit-project.org/>."
+
+_ACEOF
+ac_cs_config=`printf "%s\n" "$ac_configure_args" | sed "$ac_safe_unquote"`
+ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\''/g"`
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+ac_cs_config='$ac_cs_config_escaped'
+ac_cs_version="\\
+Cockpit config.status 311
+configured by $0, generated by GNU Autoconf 2.71,
+ with options \\"\$ac_cs_config\\"
+
+Copyright (C) 2021 Free Software Foundation, Inc.
+This config.status script is free software; the Free Software Foundation
+gives unlimited permission to copy, distribute and modify it."
+
+ac_pwd='$ac_pwd'
+srcdir='$srcdir'
+INSTALL='$INSTALL'
+MKDIR_P='$MKDIR_P'
+AWK='$AWK'
+test -n "\$AWK" || AWK=awk
+_ACEOF
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+# The default lists apply if the user does not specify any file.
+ac_need_defaults=:
+while test $# != 0
+do
+ case $1 in
+ --*=?*)
+ ac_option=`expr "X$1" : 'X\([^=]*\)='`
+ ac_optarg=`expr "X$1" : 'X[^=]*=\(.*\)'`
+ ac_shift=:
+ ;;
+ --*=)
+ ac_option=`expr "X$1" : 'X\([^=]*\)='`
+ ac_optarg=
+ ac_shift=:
+ ;;
+ *)
+ ac_option=$1
+ ac_optarg=$2
+ ac_shift=shift
+ ;;
+ esac
+
+ case $ac_option in
+ # Handling of the options.
+ -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r)
+ ac_cs_recheck=: ;;
+ --version | --versio | --versi | --vers | --ver | --ve | --v | -V )
+ printf "%s\n" "$ac_cs_version"; exit ;;
+ --config | --confi | --conf | --con | --co | --c )
+ printf "%s\n" "$ac_cs_config"; exit ;;
+ --debug | --debu | --deb | --de | --d | -d )
+ debug=: ;;
+ --file | --fil | --fi | --f )
+ $ac_shift
+ case $ac_optarg in
+ *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;;
+ '') as_fn_error $? "missing file argument" ;;
+ esac
+ as_fn_append CONFIG_FILES " '$ac_optarg'"
+ ac_need_defaults=false;;
+ --header | --heade | --head | --hea )
+ $ac_shift
+ case $ac_optarg in
+ *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;;
+ esac
+ as_fn_append CONFIG_HEADERS " '$ac_optarg'"
+ ac_need_defaults=false;;
+ --he | --h)
+ # Conflict between --help and --header
+ as_fn_error $? "ambiguous option: \`$1'
+Try \`$0 --help' for more information.";;
+ --help | --hel | -h )
+ printf "%s\n" "$ac_cs_usage"; exit ;;
+ -q | -quiet | --quiet | --quie | --qui | --qu | --q \
+ | -silent | --silent | --silen | --sile | --sil | --si | --s)
+ ac_cs_silent=: ;;
+
+ # This is an error.
+ -*) as_fn_error $? "unrecognized option: \`$1'
+Try \`$0 --help' for more information." ;;
+
+ *) as_fn_append ac_config_targets " $1"
+ ac_need_defaults=false ;;
+
+ esac
+ shift
+done
+
+ac_configure_extra_args=
+
+if $ac_cs_silent; then
+ exec 6>/dev/null
+ ac_configure_extra_args="$ac_configure_extra_args --silent"
+fi
+
+_ACEOF
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+if \$ac_cs_recheck; then
+ set X $SHELL '$0' $ac_configure_args \$ac_configure_extra_args --no-create --no-recursion
+ shift
+ \printf "%s\n" "running CONFIG_SHELL=$SHELL \$*" >&6
+ CONFIG_SHELL='$SHELL'
+ export CONFIG_SHELL
+ exec "\$@"
+fi
+
+_ACEOF
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+exec 5>>config.log
+{
+ echo
+ sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX
+## Running $as_me. ##
+_ASBOX
+ printf "%s\n" "$ac_log"
+} >&5
+
+_ACEOF
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+#
+# INIT-COMMANDS
+#
+AMDEP_TRUE="$AMDEP_TRUE" MAKE="${MAKE-make}"
+
+_ACEOF
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+
+# Handling of arguments.
+for ac_config_target in $ac_config_targets
+do
+ case $ac_config_target in
+ "config.h") CONFIG_HEADERS="$CONFIG_HEADERS config.h" ;;
+ "depfiles") CONFIG_COMMANDS="$CONFIG_COMMANDS depfiles" ;;
+ "Makefile") CONFIG_FILES="$CONFIG_FILES Makefile" ;;
+ "doc/guide/version") CONFIG_FILES="$CONFIG_FILES doc/guide/version" ;;
+ "src/tls/cockpit-certificate-helper") CONFIG_FILES="$CONFIG_FILES src/tls/cockpit-certificate-helper" ;;
+ "src/ws/cockpit-desktop") CONFIG_FILES="$CONFIG_FILES src/ws/cockpit-desktop" ;;
+
+ *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;;
+ esac
+done
+
+
+# If the user did not use the arguments to specify the items to instantiate,
+# then the envvar interface is used. Set only those that are not.
+# We use the long form for the default assignment because of an extremely
+# bizarre bug on SunOS 4.1.3.
+if $ac_need_defaults; then
+ test ${CONFIG_FILES+y} || CONFIG_FILES=$config_files
+ test ${CONFIG_HEADERS+y} || CONFIG_HEADERS=$config_headers
+ test ${CONFIG_COMMANDS+y} || CONFIG_COMMANDS=$config_commands
+fi
+
+# Have a temporary directory for convenience. Make it in the build tree
+# simply because there is no reason against having it here, and in addition,
+# creating and moving files from /tmp can sometimes cause problems.
+# Hook for its removal unless debugging.
+# Note that there is a small window in which the directory will not be cleaned:
+# after its creation but before its name has been assigned to `$tmp'.
+$debug ||
+{
+ tmp= ac_tmp=
+ trap 'exit_status=$?
+ : "${ac_tmp:=$tmp}"
+ { test ! -d "$ac_tmp" || rm -fr "$ac_tmp"; } && exit $exit_status
+' 0
+ trap 'as_fn_exit 1' 1 2 13 15
+}
+# Create a (secure) tmp directory for tmp files.
+
+{
+ tmp=`(umask 077 && mktemp -d "./confXXXXXX") 2>/dev/null` &&
+ test -d "$tmp"
+} ||
+{
+ tmp=./conf$$-$RANDOM
+ (umask 077 && mkdir "$tmp")
+} || as_fn_error $? "cannot create a temporary directory in ." "$LINENO" 5
+ac_tmp=$tmp
+
+# Set up the scripts for CONFIG_FILES section.
+# No need to generate them if there are no CONFIG_FILES.
+# This happens for instance with `./config.status config.h'.
+if test -n "$CONFIG_FILES"; then
+
+
+ac_cr=`echo X | tr X '\015'`
+# On cygwin, bash can eat \r inside `` if the user requested igncr.
+# But we know of no other shell where ac_cr would be empty at this
+# point, so we can use a bashism as a fallback.
+if test "x$ac_cr" = x; then
+ eval ac_cr=\$\'\\r\'
+fi
+ac_cs_awk_cr=`$AWK 'BEGIN { print "a\rb" }' </dev/null 2>/dev/null`
+if test "$ac_cs_awk_cr" = "a${ac_cr}b"; then
+ ac_cs_awk_cr='\\r'
+else
+ ac_cs_awk_cr=$ac_cr
+fi
+
+echo 'BEGIN {' >"$ac_tmp/subs1.awk" &&
+_ACEOF
+
+
+{
+ echo "cat >conf$$subs.awk <<_ACEOF" &&
+ echo "$ac_subst_vars" | sed 's/.*/&!$&$ac_delim/' &&
+ echo "_ACEOF"
+} >conf$$subs.sh ||
+ as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5
+ac_delim_num=`echo "$ac_subst_vars" | grep -c '^'`
+ac_delim='%!_!# '
+for ac_last_try in false false false false false :; do
+ . ./conf$$subs.sh ||
+ as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5
+
+ ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | grep -c X`
+ if test $ac_delim_n = $ac_delim_num; then
+ break
+ elif $ac_last_try; then
+ as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5
+ else
+ ac_delim="$ac_delim!$ac_delim _$ac_delim!! "
+ fi
+done
+rm -f conf$$subs.sh
+
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+cat >>"\$ac_tmp/subs1.awk" <<\\_ACAWK &&
+_ACEOF
+sed -n '
+h
+s/^/S["/; s/!.*/"]=/
+p
+g
+s/^[^!]*!//
+:repl
+t repl
+s/'"$ac_delim"'$//
+t delim
+:nl
+h
+s/\(.\{148\}\)..*/\1/
+t more1
+s/["\\]/\\&/g; s/^/"/; s/$/\\n"\\/
+p
+n
+b repl
+:more1
+s/["\\]/\\&/g; s/^/"/; s/$/"\\/
+p
+g
+s/.\{148\}//
+t nl
+:delim
+h
+s/\(.\{148\}\)..*/\1/
+t more2
+s/["\\]/\\&/g; s/^/"/; s/$/"/
+p
+b
+:more2
+s/["\\]/\\&/g; s/^/"/; s/$/"\\/
+p
+g
+s/.\{148\}//
+t delim
+' <conf$$subs.awk | sed '
+/^[^""]/{
+ N
+ s/\n//
+}
+' >>$CONFIG_STATUS || ac_write_fail=1
+rm -f conf$$subs.awk
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+_ACAWK
+cat >>"\$ac_tmp/subs1.awk" <<_ACAWK &&
+ for (key in S) S_is_set[key] = 1
+ FS = ""
+
+}
+{
+ line = $ 0
+ nfields = split(line, field, "@")
+ substed = 0
+ len = length(field[1])
+ for (i = 2; i < nfields; i++) {
+ key = field[i]
+ keylen = length(key)
+ if (S_is_set[key]) {
+ value = S[key]
+ line = substr(line, 1, len) "" value "" substr(line, len + keylen + 3)
+ len += length(value) + length(field[++i])
+ substed = 1
+ } else
+ len += 1 + keylen
+ }
+
+ print line
+}
+
+_ACAWK
+_ACEOF
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then
+ sed "s/$ac_cr\$//; s/$ac_cr/$ac_cs_awk_cr/g"
+else
+ cat
+fi < "$ac_tmp/subs1.awk" > "$ac_tmp/subs.awk" \
+ || as_fn_error $? "could not setup config files machinery" "$LINENO" 5
+_ACEOF
+
+# VPATH may cause trouble with some makes, so we remove sole $(srcdir),
+# ${srcdir} and @srcdir@ entries from VPATH if srcdir is ".", strip leading and
+# trailing colons and then remove the whole line if VPATH becomes empty
+# (actually we leave an empty line to preserve line numbers).
+if test "x$srcdir" = x.; then
+ ac_vpsub='/^[ ]*VPATH[ ]*=[ ]*/{
+h
+s///
+s/^/:/
+s/[ ]*$/:/
+s/:\$(srcdir):/:/g
+s/:\${srcdir}:/:/g
+s/:@srcdir@:/:/g
+s/^:*//
+s/:*$//
+x
+s/\(=[ ]*\).*/\1/
+G
+s/\n//
+s/^[^=]*=[ ]*$//
+}'
+fi
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+fi # test -n "$CONFIG_FILES"
+
+# Set up the scripts for CONFIG_HEADERS section.
+# No need to generate them if there are no CONFIG_HEADERS.
+# This happens for instance with `./config.status Makefile'.
+if test -n "$CONFIG_HEADERS"; then
+cat >"$ac_tmp/defines.awk" <<\_ACAWK ||
+BEGIN {
+_ACEOF
+
+# Transform confdefs.h into an awk script `defines.awk', embedded as
+# here-document in config.status, that substitutes the proper values into
+# config.h.in to produce config.h.
+
+# Create a delimiter string that does not exist in confdefs.h, to ease
+# handling of long lines.
+ac_delim='%!_!# '
+for ac_last_try in false false :; do
+ ac_tt=`sed -n "/$ac_delim/p" confdefs.h`
+ if test -z "$ac_tt"; then
+ break
+ elif $ac_last_try; then
+ as_fn_error $? "could not make $CONFIG_HEADERS" "$LINENO" 5
+ else
+ ac_delim="$ac_delim!$ac_delim _$ac_delim!! "
+ fi
+done
+
+# For the awk script, D is an array of macro values keyed by name,
+# likewise P contains macro parameters if any. Preserve backslash
+# newline sequences.
+
+ac_word_re=[_$as_cr_Letters][_$as_cr_alnum]*
+sed -n '
+s/.\{148\}/&'"$ac_delim"'/g
+t rset
+:rset
+s/^[ ]*#[ ]*define[ ][ ]*/ /
+t def
+d
+:def
+s/\\$//
+t bsnl
+s/["\\]/\\&/g
+s/^ \('"$ac_word_re"'\)\(([^()]*)\)[ ]*\(.*\)/P["\1"]="\2"\
+D["\1"]=" \3"/p
+s/^ \('"$ac_word_re"'\)[ ]*\(.*\)/D["\1"]=" \2"/p
+d
+:bsnl
+s/["\\]/\\&/g
+s/^ \('"$ac_word_re"'\)\(([^()]*)\)[ ]*\(.*\)/P["\1"]="\2"\
+D["\1"]=" \3\\\\\\n"\\/p
+t cont
+s/^ \('"$ac_word_re"'\)[ ]*\(.*\)/D["\1"]=" \2\\\\\\n"\\/p
+t cont
+d
+:cont
+n
+s/.\{148\}/&'"$ac_delim"'/g
+t clear
+:clear
+s/\\$//
+t bsnlc
+s/["\\]/\\&/g; s/^/"/; s/$/"/p
+d
+:bsnlc
+s/["\\]/\\&/g; s/^/"/; s/$/\\\\\\n"\\/p
+b cont
+' <confdefs.h | sed '
+s/'"$ac_delim"'/"\\\
+"/g' >>$CONFIG_STATUS || ac_write_fail=1
+
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+ for (key in D) D_is_set[key] = 1
+ FS = ""
+}
+/^[\t ]*#[\t ]*(define|undef)[\t ]+$ac_word_re([\t (]|\$)/ {
+ line = \$ 0
+ split(line, arg, " ")
+ if (arg[1] == "#") {
+ defundef = arg[2]
+ mac1 = arg[3]
+ } else {
+ defundef = substr(arg[1], 2)
+ mac1 = arg[2]
+ }
+ split(mac1, mac2, "(") #)
+ macro = mac2[1]
+ prefix = substr(line, 1, index(line, defundef) - 1)
+ if (D_is_set[macro]) {
+ # Preserve the white space surrounding the "#".
+ print prefix "define", macro P[macro] D[macro]
+ next
+ } else {
+ # Replace #undef with comments. This is necessary, for example,
+ # in the case of _POSIX_SOURCE, which is predefined and required
+ # on some systems where configure will not decide to define it.
+ if (defundef == "undef") {
+ print "/*", prefix defundef, macro, "*/"
+ next
+ }
+ }
+}
+{ print }
+_ACAWK
+_ACEOF
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+ as_fn_error $? "could not setup config headers machinery" "$LINENO" 5
+fi # test -n "$CONFIG_HEADERS"
+
+
+eval set X " :F $CONFIG_FILES :H $CONFIG_HEADERS :C $CONFIG_COMMANDS"
+shift
+for ac_tag
+do
+ case $ac_tag in
+ :[FHLC]) ac_mode=$ac_tag; continue;;
+ esac
+ case $ac_mode$ac_tag in
+ :[FHL]*:*);;
+ :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5;;
+ :[FH]-) ac_tag=-:-;;
+ :[FH]*) ac_tag=$ac_tag:$ac_tag.in;;
+ esac
+ ac_save_IFS=$IFS
+ IFS=:
+ set x $ac_tag
+ IFS=$ac_save_IFS
+ shift
+ ac_file=$1
+ shift
+
+ case $ac_mode in
+ :L) ac_source=$1;;
+ :[FH])
+ ac_file_inputs=
+ for ac_f
+ do
+ case $ac_f in
+ -) ac_f="$ac_tmp/stdin";;
+ *) # Look for the file first in the build tree, then in the source tree
+ # (if the path is not absolute). The absolute path cannot be DOS-style,
+ # because $ac_f cannot contain `:'.
+ test -f "$ac_f" ||
+ case $ac_f in
+ [\\/$]*) false;;
+ *) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";;
+ esac ||
+ as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5;;
+ esac
+ case $ac_f in *\'*) ac_f=`printf "%s\n" "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac
+ as_fn_append ac_file_inputs " '$ac_f'"
+ done
+
+ # Let's still pretend it is `configure' which instantiates (i.e., don't
+ # use $as_me), people would be surprised to read:
+ # /* config.h. Generated by config.status. */
+ configure_input='Generated from '`
+ printf "%s\n" "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g'
+ `' by configure.'
+ if test x"$ac_file" != x-; then
+ configure_input="$ac_file. $configure_input"
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5
+printf "%s\n" "$as_me: creating $ac_file" >&6;}
+ fi
+ # Neutralize special characters interpreted by sed in replacement strings.
+ case $configure_input in #(
+ *\&* | *\|* | *\\* )
+ ac_sed_conf_input=`printf "%s\n" "$configure_input" |
+ sed 's/[\\\\&|]/\\\\&/g'`;; #(
+ *) ac_sed_conf_input=$configure_input;;
+ esac
+
+ case $ac_tag in
+ *:-:* | *:-) cat >"$ac_tmp/stdin" \
+ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;;
+ esac
+ ;;
+ esac
+
+ ac_dir=`$as_dirname -- "$ac_file" ||
+$as_expr X"$ac_file" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+ X"$ac_file" : 'X\(//\)[^/]' \| \
+ X"$ac_file" : 'X\(//\)$' \| \
+ X"$ac_file" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$ac_file" |
+ sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)[^/].*/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\).*/{
+ s//\1/
+ q
+ }
+ s/.*/./; q'`
+ as_dir="$ac_dir"; as_fn_mkdir_p
+ ac_builddir=.
+
+case "$ac_dir" in
+.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;;
+*)
+ ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'`
+ # A ".." for each directory in $ac_dir_suffix.
+ ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'`
+ case $ac_top_builddir_sub in
+ "") ac_top_builddir_sub=. ac_top_build_prefix= ;;
+ *) ac_top_build_prefix=$ac_top_builddir_sub/ ;;
+ esac ;;
+esac
+ac_abs_top_builddir=$ac_pwd
+ac_abs_builddir=$ac_pwd$ac_dir_suffix
+# for backward compatibility:
+ac_top_builddir=$ac_top_build_prefix
+
+case $srcdir in
+ .) # We are building in place.
+ ac_srcdir=.
+ ac_top_srcdir=$ac_top_builddir_sub
+ ac_abs_top_srcdir=$ac_pwd ;;
+ [\\/]* | ?:[\\/]* ) # Absolute name.
+ ac_srcdir=$srcdir$ac_dir_suffix;
+ ac_top_srcdir=$srcdir
+ ac_abs_top_srcdir=$srcdir ;;
+ *) # Relative name.
+ ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix
+ ac_top_srcdir=$ac_top_build_prefix$srcdir
+ ac_abs_top_srcdir=$ac_pwd/$srcdir ;;
+esac
+ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix
+
+
+ case $ac_mode in
+ :F)
+ #
+ # CONFIG_FILE
+ #
+
+ case $INSTALL in
+ [\\/$]* | ?:[\\/]* ) ac_INSTALL=$INSTALL ;;
+ *) ac_INSTALL=$ac_top_build_prefix$INSTALL ;;
+ esac
+ ac_MKDIR_P=$MKDIR_P
+ case $MKDIR_P in
+ [\\/$]* | ?:[\\/]* ) ;;
+ */*) ac_MKDIR_P=$ac_top_build_prefix$MKDIR_P ;;
+ esac
+_ACEOF
+
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+# If the template does not know about datarootdir, expand it.
+# FIXME: This hack should be removed a few years after 2.60.
+ac_datarootdir_hack=; ac_datarootdir_seen=
+ac_sed_dataroot='
+/datarootdir/ {
+ p
+ q
+}
+/@datadir@/p
+/@docdir@/p
+/@infodir@/p
+/@localedir@/p
+/@mandir@/p'
+case `eval "sed -n \"\$ac_sed_dataroot\" $ac_file_inputs"` in
+*datarootdir*) ac_datarootdir_seen=yes;;
+*@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*)
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5
+printf "%s\n" "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;}
+_ACEOF
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+ ac_datarootdir_hack='
+ s&@datadir@&$datadir&g
+ s&@docdir@&$docdir&g
+ s&@infodir@&$infodir&g
+ s&@localedir@&$localedir&g
+ s&@mandir@&$mandir&g
+ s&\\\${datarootdir}&$datarootdir&g' ;;
+esac
+_ACEOF
+
+# Neutralize VPATH when `$srcdir' = `.'.
+# Shell code in configure.ac might set extrasub.
+# FIXME: do we really want to maintain this feature?
+cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
+ac_sed_extra="$ac_vpsub
+$extrasub
+_ACEOF
+cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
+:t
+/@[a-zA-Z_][a-zA-Z_0-9]*@/!b
+s|@configure_input@|$ac_sed_conf_input|;t t
+s&@top_builddir@&$ac_top_builddir_sub&;t t
+s&@top_build_prefix@&$ac_top_build_prefix&;t t
+s&@srcdir@&$ac_srcdir&;t t
+s&@abs_srcdir@&$ac_abs_srcdir&;t t
+s&@top_srcdir@&$ac_top_srcdir&;t t
+s&@abs_top_srcdir@&$ac_abs_top_srcdir&;t t
+s&@builddir@&$ac_builddir&;t t
+s&@abs_builddir@&$ac_abs_builddir&;t t
+s&@abs_top_builddir@&$ac_abs_top_builddir&;t t
+s&@INSTALL@&$ac_INSTALL&;t t
+s&@MKDIR_P@&$ac_MKDIR_P&;t t
+$ac_datarootdir_hack
+"
+eval sed \"\$ac_sed_extra\" "$ac_file_inputs" | $AWK -f "$ac_tmp/subs.awk" \
+ >$ac_tmp/out || as_fn_error $? "could not create $ac_file" "$LINENO" 5
+
+test -z "$ac_datarootdir_hack$ac_datarootdir_seen" &&
+ { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } &&
+ { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \
+ "$ac_tmp/out"`; test -z "$ac_out"; } &&
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir'
+which seems to be undefined. Please make sure it is defined" >&5
+printf "%s\n" "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir'
+which seems to be undefined. Please make sure it is defined" >&2;}
+
+ rm -f "$ac_tmp/stdin"
+ case $ac_file in
+ -) cat "$ac_tmp/out" && rm -f "$ac_tmp/out";;
+ *) rm -f "$ac_file" && mv "$ac_tmp/out" "$ac_file";;
+ esac \
+ || as_fn_error $? "could not create $ac_file" "$LINENO" 5
+ ;;
+ :H)
+ #
+ # CONFIG_HEADER
+ #
+ if test x"$ac_file" != x-; then
+ {
+ printf "%s\n" "/* $configure_input */" >&1 \
+ && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs"
+ } >"$ac_tmp/config.h" \
+ || as_fn_error $? "could not create $ac_file" "$LINENO" 5
+ if diff "$ac_file" "$ac_tmp/config.h" >/dev/null 2>&1; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: $ac_file is unchanged" >&5
+printf "%s\n" "$as_me: $ac_file is unchanged" >&6;}
+ else
+ rm -f "$ac_file"
+ mv "$ac_tmp/config.h" "$ac_file" \
+ || as_fn_error $? "could not create $ac_file" "$LINENO" 5
+ fi
+ else
+ printf "%s\n" "/* $configure_input */" >&1 \
+ && eval '$AWK -f "$ac_tmp/defines.awk"' "$ac_file_inputs" \
+ || as_fn_error $? "could not create -" "$LINENO" 5
+ fi
+# Compute "$ac_file"'s index in $config_headers.
+_am_arg="$ac_file"
+_am_stamp_count=1
+for _am_header in $config_headers :; do
+ case $_am_header in
+ $_am_arg | $_am_arg:* )
+ break ;;
+ * )
+ _am_stamp_count=`expr $_am_stamp_count + 1` ;;
+ esac
+done
+echo "timestamp for $_am_arg" >`$as_dirname -- "$_am_arg" ||
+$as_expr X"$_am_arg" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+ X"$_am_arg" : 'X\(//\)[^/]' \| \
+ X"$_am_arg" : 'X\(//\)$' \| \
+ X"$_am_arg" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$_am_arg" |
+ sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)[^/].*/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\).*/{
+ s//\1/
+ q
+ }
+ s/.*/./; q'`/stamp-h$_am_stamp_count
+ ;;
+
+ :C) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: executing $ac_file commands" >&5
+printf "%s\n" "$as_me: executing $ac_file commands" >&6;}
+ ;;
+ esac
+
+
+ case $ac_file$ac_mode in
+ "depfiles":C) test x"$AMDEP_TRUE" != x"" || {
+ # Older Autoconf quotes --file arguments for eval, but not when files
+ # are listed without --file. Let's play safe and only enable the eval
+ # if we detect the quoting.
+ # TODO: see whether this extra hack can be removed once we start
+ # requiring Autoconf 2.70 or later.
+ case $CONFIG_FILES in #(
+ *\'*) :
+ eval set x "$CONFIG_FILES" ;; #(
+ *) :
+ set x $CONFIG_FILES ;; #(
+ *) :
+ ;;
+esac
+ shift
+ # Used to flag and report bootstrapping failures.
+ am_rc=0
+ for am_mf
+ do
+ # Strip MF so we end up with the name of the file.
+ am_mf=`printf "%s\n" "$am_mf" | sed -e 's/:.*$//'`
+ # Check whether this is an Automake generated Makefile which includes
+ # dependency-tracking related rules and includes.
+ # Grep'ing the whole file directly is not great: AIX grep has a line
+ # limit of 2048, but all sed's we know have understand at least 4000.
+ sed -n 's,^am--depfiles:.*,X,p' "$am_mf" | grep X >/dev/null 2>&1 \
+ || continue
+ am_dirpart=`$as_dirname -- "$am_mf" ||
+$as_expr X"$am_mf" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \
+ X"$am_mf" : 'X\(//\)[^/]' \| \
+ X"$am_mf" : 'X\(//\)$' \| \
+ X"$am_mf" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X"$am_mf" |
+ sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)[^/].*/{
+ s//\1/
+ q
+ }
+ /^X\(\/\/\)$/{
+ s//\1/
+ q
+ }
+ /^X\(\/\).*/{
+ s//\1/
+ q
+ }
+ s/.*/./; q'`
+ am_filepart=`$as_basename -- "$am_mf" ||
+$as_expr X/"$am_mf" : '.*/\([^/][^/]*\)/*$' \| \
+ X"$am_mf" : 'X\(//\)$' \| \
+ X"$am_mf" : 'X\(/\)' \| . 2>/dev/null ||
+printf "%s\n" X/"$am_mf" |
+ sed '/^.*\/\([^/][^/]*\)\/*$/{
+ s//\1/
+ q
+ }
+ /^X\/\(\/\/\)$/{
+ s//\1/
+ q
+ }
+ /^X\/\(\/\).*/{
+ s//\1/
+ q
+ }
+ s/.*/./; q'`
+ { echo "$as_me:$LINENO: cd "$am_dirpart" \
+ && sed -e '/# am--include-marker/d' "$am_filepart" \
+ | $MAKE -f - am--depfiles" >&5
+ (cd "$am_dirpart" \
+ && sed -e '/# am--include-marker/d' "$am_filepart" \
+ | $MAKE -f - am--depfiles) >&5 2>&5
+ ac_status=$?
+ echo "$as_me:$LINENO: \$? = $ac_status" >&5
+ (exit $ac_status); } || am_rc=$?
+ done
+ if test $am_rc -ne 0; then
+ { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "Something went wrong bootstrapping makefile fragments
+ for automatic dependency tracking. If GNU make was not used, consider
+ re-running the configure script with MAKE=\"gmake\" (or whatever is
+ necessary). You can also try re-running configure with the
+ '--disable-dependency-tracking' option to at least be able to build
+ the package (albeit without support for automatic dependency tracking).
+See \`config.log' for more details" "$LINENO" 5; }
+ fi
+ { am_dirpart=; unset am_dirpart;}
+ { am_filepart=; unset am_filepart;}
+ { am_mf=; unset am_mf;}
+ { am_rc=; unset am_rc;}
+ rm -f conftest-deps.mk
+}
+ ;;
+
+ esac
+done # for ac_tag
+
+
+as_fn_exit 0
+_ACEOF
+ac_clean_files=$ac_clean_files_save
+
+test $ac_write_fail = 0 ||
+ as_fn_error $? "write failure creating $CONFIG_STATUS" "$LINENO" 5
+
+
+# configure is writing to config.log, and then calls config.status.
+# config.status does its own redirection, appending to config.log.
+# Unfortunately, on DOS this fails, as config.log is still kept open
+# by configure, so config.status won't be able to write to it; its
+# output is simply discarded. So we exec the FD to /dev/null,
+# effectively closing config.log, so it can be properly (re)opened and
+# appended to by config.status. When coming back to configure, we
+# need to make the FD available again.
+if test "$no_create" != yes; then
+ ac_cs_success=:
+ ac_config_status_args=
+ test "$silent" = yes &&
+ ac_config_status_args="$ac_config_status_args --quiet"
+ exec 5>/dev/null
+ $SHELL $CONFIG_STATUS $ac_config_status_args || ac_cs_success=false
+ exec 5>>config.log
+ # Use ||, not &&, to avoid exiting from the if with $? = 1, which
+ # would make configure fail if this is the last instruction.
+ $ac_cs_success || as_fn_exit 1
+fi
+if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then
+ { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5
+printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;}
+fi
+
+
+
+echo "
+ Cockpit $VERSION
+ ================
+
+ prefix: ${prefix}
+ exec_prefix: ${exec_prefix}
+ libdir: ${libdir}
+ libexecdir: ${libexecdir}
+ bindir: ${bindir}
+ sbindir: ${sbindir}
+ datarootdir: ${datarootdir}
+ datadir: ${datadir}
+ sysconfdir: ${sysconfdir}
+ localstatedir: ${localstatedir}
+ pamdir: ${pamdir}
+ systemd unit dir: ${systemdunitdir}
+
+ compiler: ${CC}
+ cflags: ${CFLAGS}
+ cppflags: ${CPPFLAGS}
+
+ cockpit-ws user: ${COCKPIT_USER}
+ cockpit-ws group: ${COCKPIT_GROUP}
+ cockpit-ws instance user: ${COCKPIT_WSINSTANCE_USER}
+ cockpit-ws instance group: ${COCKPIT_WSINSTANCE_GROUP}
+ admin group: ${admin_group}
+ cockpit-session PATH: ${default_session_path}
+
+ Building docs: ${enable_doc}
+ Debug mode: ${debug_status}
+ Node environment: ${NODE_ENV}
+ With coverage: ${enable_coverage}
+ With address sanitizer: ${asan_status}
+ With PCP: ${enable_pcp}
+ With polkit: ${enable_polkit}
+ SELinux Policy: ${enable_selinux_policy}
+
+ cockpit-client: ${enable_cockpit_client}
+ cockpit-ssh: ${enable_ssh}
+
+ ssh-add: ${SSH_ADD}
+ ssh-agent: ${SSH_AGENT}
+
+Now type 'make' to compile cockpit."
+
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..ad8c853
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,502 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+m4_include([version.m4])
+AC_INIT([Cockpit],
+ [VERSION_NUMBER],
+ [devel@lists.cockpit-project.org],
+ [cockpit],
+ [https://cockpit-project.org/])
+
+AC_CONFIG_SRCDIR([src])
+AC_CONFIG_HEADERS([config.h])
+AC_CONFIG_AUX_DIR([tools])
+
+AM_INIT_AUTOMAKE([1.11 foreign dist-xz no-dist-gzip subdir-objects])
+# we want tar-ustar to avoid introducing extra metadata (ctime, atime) which
+# only adds useless non-determinism to the result. we also want to sort.
+am__tar='tar --format=ustar --sort=name --owner=root:0 --group=root:0 -chf - "$$tardir"'
+
+AC_USE_SYSTEM_EXTENSIONS
+AC_SYS_LARGEFILE
+AC_PROG_RANLIB
+
+# This is required to find the correct `ar` for cross-compiling
+AC_CHECK_TOOL(AR, ar)
+
+AC_CHECK_FUNCS(
+ closefrom
+)
+
+AM_SILENT_RULES([yes])
+
+AC_MSG_CHECKING([whether to install to prefix only])
+AC_ARG_ENABLE([prefix-only],
+ [AS_HELP_STRING([--enable-prefix-only], [Whether to install to prefix only])],
+ [], [enable_prefix_only=no])
+AC_MSG_RESULT($enable_prefix_only)
+
+
+# --enable-selinux-policy=[type]
+AC_MSG_CHECKING([whether to build selinux policy, and which])
+AC_ARG_ENABLE([selinux-policy], [AS_HELP_STRING([--enable-selinux-policy=type], [Whether to build selinux policy, and which])])
+if test "${enable_selinux_policy:=no}" = 'yes'; then
+ AC_MSG_ERROR([--enable-selinux-policy requires a type (eg: targeted)])
+fi
+AM_CONDITIONAL(SELINUX_POLICY_ENABLED, test "$enable_selinux_policy" != "no")
+AC_SUBST(SELINUX_POLICY_TYPE, [${enable_selinux_policy}])
+AC_MSG_RESULT($enable_selinux_policy)
+
+# --disable-polkit
+AC_MSG_CHECKING([whether to build polkit support])
+AC_ARG_ENABLE(polkit, AS_HELP_STRING([--disable-polkit], [Disable usage of polkit]))
+AM_CONDITIONAL(WITH_POLKIT, test "$enable_polkit" != 'no')
+if test "$enable_polkit" != 'no'; then
+ AC_DEFINE([WITH_POLKIT], 1, [Whether to build with polkit support])
+fi
+AC_MSG_RESULT(${enable_polkit:=yes})
+
+# --disable-ssh
+AC_MSG_CHECKING([whether to build cockpit-ssh])
+AC_ARG_ENABLE(ssh, AS_HELP_STRING([--disable-ssh], [Disable cockpit-ssh build and libssh dependency]))
+AM_CONDITIONAL(WITH_COCKPIT_SSH, test "$enable_ssh" != "no")
+AC_MSG_RESULT(${enable_ssh:=yes})
+
+# --enable-old-bridge
+AC_MSG_CHECKING([whether to install the old C cockpit-bridge])
+AC_ARG_ENABLE(old_bridge, AS_HELP_STRING([--enable-old-bridge], [Install old C cockpit-bridge]))
+AM_CONDITIONAL(WITH_OLD_BRIDGE, test "$enable_old_bridge" = "yes")
+AC_MSG_RESULT(${enable_old_bridge:=no})
+
+AC_SEARCH_LIBS([argp_parse], [argp])
+case "$ac_cv_search_argp_parse" in
+ no) AC_MSG_FAILURE([failed to find argp_parse]) ;;
+ -l*) argp_LIBS="$ac_cv_search_argp_parse" ;;
+ *) argp_LIBS= ;;
+esac
+AC_SUBST([argp_LIBS])
+
+AC_SEARCH_LIBS([fts_close], [fts])
+case "$ac_cv_search_fts_close" in
+ no) AC_MSG_FAILURE([failed to find fts_close]) ;;
+ -l*) fts_LIBS="$ac_cv_search_fts_close" ;;
+ *) fts_LIBS= ;;
+esac
+AC_SUBST([fts_LIBS])
+
+# pkg-config
+GLIB_API_VERSION="GLIB_VERSION_2_56"
+PKG_CHECK_MODULES(glib, [gio-2.0 >= 2.56 gio-unix-2.0])
+glib_CFLAGS="${glib_CFLAGS} -DGLIB_VERSION_MIN_REQUIRED=$GLIB_API_VERSION"
+glib_CFLAGS="${glib_CFLAGS} -DGLIB_VERSION_MAX_ALLOWED=$GLIB_API_VERSION"
+
+PKG_CHECK_MODULES(libsystemd, [libsystemd >= 235])
+PKG_CHECK_MODULES(json_glib, [json-glib-1.0 >= 1.4])
+PKG_CHECK_MODULES(gnutls, [gnutls >= 3.6.0])
+PKG_CHECK_MODULES(krb5, [krb5-gssapi >= 1.11 krb5 >= 1.11])
+if test "$enable_polkit" != "no"; then
+ PKG_CHECK_MODULES(polkit, [polkit-agent-1 >= 0.105])
+fi
+if test "$enable_ssh" != "no"; then
+ PKG_CHECK_MODULES(libssh, [libssh >= 0.8.5])
+ old_CFLAGS=$CFLAGS; CFLAGS=$libssh_CFLAGS
+ old_LIBS=$LIBS; LIBS=$libssh_LIBS
+ AC_CHECK_FUNCS(ssh_userauth_publickey_auto_get_current_identity)
+ CFLAGS=$old_CFLAGS
+ LIBS=$old_LIBS
+fi
+
+# pam
+AC_CHECK_HEADER([security/pam_appl.h], ,
+ [AC_MSG_ERROR([Couldn't find PAM headers. Try installing pam-devel])]
+)
+PAM_LIBS="-lpam"
+COCKPIT_SESSION_LIBS="$COCKPIT_SESSION_LIBS $PAM_LIBS"
+
+# pam module directory
+AC_ARG_WITH([pamdir],
+ [AS_HELP_STRING([--with-pamdir=DIR],
+ [directory to install pam modules in])],
+ [], [with_pamdir='${libdir}/security'])
+pamdir=$with_pamdir
+AC_SUBST(pamdir)
+
+# crypt
+AC_CHECK_HEADER([crypt.h], ,
+ [AC_MSG_ERROR([Couldn't find crypt headers. Try installing glibc-headers])]
+)
+AC_CHECK_LIB(crypt, crypt_r, [ true ],
+ [AC_MSG_ERROR([Couldn't find crypt library. Try installing glibc-devel])]
+)
+COCKPIT_WS_LIBS="$COCKPIT_WS_LIBS -lcrypt"
+
+# pcp
+AC_MSG_CHECKING([whether to build with PCP])
+AC_ARG_ENABLE(pcp, AS_HELP_STRING([--disable-pcp], [Disable usage of PCP]))
+
+if test "$enable_pcp" = "no"; then
+ AC_MSG_RESULT($enable_pcp)
+
+else
+ if test "$enable_pcp" = ""; then
+ disable_msg="(perhaps --disable-pcp)"
+ fi
+
+ enable_pcp="yes"
+ AC_MSG_RESULT($enable_pcp)
+
+ AC_CHECK_HEADER([pcp/pmapi.h], ,
+ [AC_MSG_ERROR([Couldn't find pcp headers $disable_msg])]
+ )
+ AC_CHECK_HEADERS([pcp/import.h pcp/pmda.h], ,
+ [AC_MSG_ERROR([Couldn't find pcp headers $disable_msg])],
+ [#include <pcp/pmapi.h>]
+ )
+ AC_CHECK_LIB(pcp, pmNewContext, [ true ],
+ [AC_MSG_ERROR([Couldn't find pcp library $disable_msg])]
+ )
+ AC_CHECK_LIB(pcp_pmda, pmdaCacheLookup, [ true ],
+ [AC_MSG_ERROR([Couldn't find pcp_pmda library $disable_msg])]
+ )
+ AC_CHECK_LIB(pcp_import, pmiStart, [ true ],
+ [AC_MSG_ERROR([Couldn't find pcp_import library $disable_msg])]
+ )
+ COCKPIT_PCP_LIBS="$COCKPIT_PCP_LIBS -lpcp"
+fi
+
+AM_CONDITIONAL([ENABLE_PCP], [test "$enable_pcp" = "yes"])
+
+# systemd
+AC_ARG_WITH([systemdunitdir], [AS_HELP_STRING([--with-systemdunitdir=DIR],
+ [directory to install systemd unit files in])])
+
+if test ! -z "$with_systemdunitdir"; then
+ systemdunitdir=$with_systemdunitdir
+elif test "$enable_prefix_only" = "yes"; then
+ systemdunitdir='${prefix}/lib/systemd/system'
+else
+ PKG_CHECK_MODULES(SYSTEMD, [systemd])
+ AC_MSG_CHECKING(for systemd unit dir)
+ systemdunitdir=$($PKG_CONFIG systemd --variable=systemdsystemunitdir)
+ if test "$systemdunitdir" = ""; then
+ AC_MSG_ERROR([systemd's pkg-config file doesn't contain 'systemdsystemunitdir' variable])
+ fi
+ AC_MSG_RESULT($systemdunitdir)
+fi
+AC_SUBST([systemdunitdir], [$systemdunitdir])
+
+# We need msgcat, msgfmt, and xgettext, but they're all in the same
+# package as xgettext, and we find them by PATH, so just check for the one.
+AC_PATH_PROG([XGETTEXT], [xgettext], [no])
+if test "$XGETTEXT" = "no"; then
+ AC_MSG_ERROR([Please install gettext tools])
+fi
+
+# ssh-add
+AC_PATH_PROG([SSH_ADD], [ssh-add], [/usr/bin/ssh-add], [$PATH:/usr/local/sbin:/usr/sbin:/sbin])
+AC_DEFINE_UNQUOTED([PATH_SSH_ADD], ["$SSH_ADD"], [Location of ssh-add binary])
+
+# ssh-agent
+AC_PATH_PROG([SSH_AGENT], [ssh-agent], [/usr/bin/ssh-agent], [$PATH:/usr/local/bin:/usr/bin:/bin])
+AC_DEFINE_UNQUOTED([PATH_SSH_AGENT], ["$SSH_AGENT"], [Location of ssh-agent binary])
+
+# Address sanitizer
+AC_MSG_CHECKING([for asan flags])
+AC_ARG_ENABLE(asan,
+ AS_HELP_STRING([--enable-asan=no/yes],
+ [Turn the Address Sanitizer on or off])
+ )
+
+if test "$enable_asan" = "yes"; then
+ CFLAGS="$CFLAGS -fsanitize=address -O1 -fno-omit-frame-pointer -g"
+ asan_status="yes"
+else
+ asan_status="no"
+fi
+AM_CONDITIONAL(WITH_ASAN, test "$enable_asan" = "yes")
+AC_MSG_RESULT($asan_status)
+
+# User and group for running cockpit web server (cockpit-tls or -ws in customized setups)
+
+AC_ARG_WITH(cockpit_user,
+ AS_HELP_STRING([--with-cockpit-user=<user>],
+ [User for running cockpit (root)]
+ )
+ )
+AC_ARG_WITH(cockpit_group,
+ AS_HELP_STRING([--with-cockpit-group=<group>],
+ [Group for running cockpit]
+ )
+ )
+if test -z "$with_cockpit_user"; then
+ COCKPIT_USER=root
+ COCKPIT_GROUP=
+else
+ COCKPIT_USER=$with_cockpit_user
+ if test -z "$with_cockpit_group"; then
+ COCKPIT_GROUP=$with_cockpit_user
+ else
+ COCKPIT_GROUP=$with_cockpit_group
+ fi
+fi
+
+AC_SUBST(COCKPIT_USER)
+AC_SUBST(COCKPIT_GROUP)
+
+# User for running cockpit-ws instances from cockpit-tls
+
+AC_ARG_WITH(cockpit_ws_instance_user,
+ AS_HELP_STRING([--with-cockpit-ws-instance-user=<user>],
+ [User for running cockpit-ws instances from cockpit-tls (root)]
+ )
+ )
+AC_ARG_WITH(cockpit_ws_instance_group,
+ AS_HELP_STRING([--with-cockpit-ws-instance-group=<group>],
+ [Group for running cockpit-ws instances from cockpit-tls]
+ )
+ )
+if test -z "$with_cockpit_ws_instance_user"; then
+ if test "$COCKPIT_USER" != "root"; then
+ AC_MSG_ERROR([--with-cockpit-ws-instance-user is required when setting --with-cockpit-user])
+ fi
+ COCKPIT_WSINSTANCE_USER=root
+else
+ COCKPIT_WSINSTANCE_USER=$with_cockpit_ws_instance_user
+ if test -z "$with_cockpit_ws_instance_group"; then
+ COCKPIT_WSINSTANCE_GROUP=$with_cockpit_ws_instance_user
+ else
+ COCKPIT_WSINSTANCE_GROUP=$with_cockpit_ws_instance_group
+ fi
+fi
+
+AC_SUBST(COCKPIT_WSINSTANCE_USER)
+AC_SUBST(COCKPIT_WSINSTANCE_GROUP)
+
+# admin users group
+AC_ARG_WITH([admin-group],
+ [AS_HELP_STRING([--with-admin-group=GROUP],
+ [system group to which admin users belong])],
+ [admin_group=$withval],
+ [
+ AC_MSG_CHECKING([for system group to which admin users belong])
+ CANDIDATE_GROUPS="wheel sudo root"
+ admin_group="$(getent group ${CANDIDATE_GROUPS} | head -n1 | cut -f1 -d:)"
+ if test -n "$admin_group"; then
+ AC_MSG_RESULT([$admin_group])
+ else
+ AC_MSG_RESULT([unable to detect])
+ AC_MSG_ERROR([none of '${CANDIDATE_GROUPS}' exist: please specify a group with --with-admin-group=])
+ fi
+ ])
+AC_SUBST(admin_group)
+
+# Default PATH for cockpit-session
+AC_ARG_WITH([default-session-path],
+ [AS_HELP_STRING([--with-default-session-path=PATH],
+ [The value for the PATH environment variable in a session started by cockpit-session])],
+ [default_session_path=$withval],
+ [
+ AC_MSG_CHECKING([for cockpit-session PATH value])
+ if test "$(readlink /sbin)" == "usr/bin"; then
+ # This is Arch where "sbin" is symlinked to "bin" and
+ # "/bin" is symlinked to "/usr/bin". We use the
+ # normal Arch PATH which omits "sbin" and "/bin" for
+ # those reasons. Otherwise "pkexec" will find
+ # cockpit-bridge in "/usr/sbin" and our rule wont
+ # match.
+ default_session_path=/usr/local/sbin:/usr/local/bin:/usr/bin
+ else
+ default_session_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+ fi
+ AC_MSG_RESULT([$default_session_path])
+ ])
+AC_DEFINE_UNQUOTED([DEFAULT_SESSION_PATH], ["$default_session_path"], [Default value of PATH for cockpit-session])
+
+# Documentation
+
+AC_MSG_CHECKING([whether to build documentation])
+AC_ARG_ENABLE(doc,
+ AS_HELP_STRING([--disable-doc],
+ [Disable building documentation])
+ )
+
+if test "$enable_doc" = "no"; then
+ AC_MSG_RESULT($enable_doc)
+
+else
+ if test "$enable_doc" = ""; then
+ disable_msg="(perhaps --disable-doc)"
+ fi
+
+ enable_doc="yes"
+ AC_MSG_RESULT($enable_doc)
+
+ AC_PATH_PROG([XSLTPROC], [xsltproc], [no])
+ if test "$XSLTPROC" = "no"; then
+ AC_MSG_ERROR([the xsltproc command was not found $disable_msg])
+ fi
+
+ AC_PATH_PROG([XMLTO], [xmlto], [no])
+ if test "$XMLTO" = "no"; then
+ AC_MSG_ERROR([the xmlto command was not found $disable_msg])
+ fi
+
+ AC_SUBST(XSLTPROC)
+ AC_SUBST(XMLTO)
+fi
+
+AM_CONDITIONAL([ENABLE_DOC], [test "$enable_doc" = "yes"])
+
+# cockpit-client
+AC_MSG_CHECKING([whether to install cockpit-client])
+AC_ARG_ENABLE([cockpit-client],
+ [AS_HELP_STRING([--enable-cockpit-client], [Whether to install cockpit-client])],
+ [], [enable_cockpit_client=no])
+AC_MSG_RESULT($enable_cockpit_client)
+AM_CONDITIONAL([ENABLE_COCKPIT_CLIENT], [test "$enable_cockpit_client" = "yes"])
+
+if test "$enable_cockpit_client" = "yes" && test "$enable_old_bridge" = "yes"; then
+ AC_MSG_ERROR([--enable-cockpit-client conflicts with --enable-old-bridge])
+fi
+
+# Debug
+
+AC_MSG_CHECKING([for debug mode])
+AC_ARG_ENABLE(debug,
+ AS_HELP_STRING([--enable-debug=no/default/yes],
+ [Turn on or off debugging])
+ )
+
+if test "$enable_debug" != "no"; then
+ AC_DEFINE_UNQUOTED(WITH_DEBUG, 1, [Print debug output])
+ AC_DEFINE_UNQUOTED(_DEBUG, 1, [In debug mode])
+ CFLAGS="$CFLAGS -g"
+fi
+debugdir='${prefix}/src/debug'
+if test "$enable_debug" = "yes"; then
+ debug_status="yes"
+ debugdir=
+ CFLAGS="$CFLAGS -O0"
+ NODE_ENV="development"
+elif test "$enable_debug" = "no"; then
+ debug_status="no"
+ CFLAGS="$CFLAGS -O2"
+ NODE_ENV="production"
+else
+ debug_status="default"
+ NODE_ENV="${NODE_ENV:-production}"
+fi
+AM_CONDITIONAL(WITH_DEBUG, test "$enable_debug" = "yes")
+AC_MSG_RESULT($debug_status)
+AC_SUBST(NODE_ENV)
+AC_SUBST(debugdir)
+
+# Coverage
+
+AC_MSG_CHECKING([whether to build with coverage])
+AC_ARG_ENABLE([coverage],
+ [AS_HELP_STRING([--enable-coverage], [Whether to enable coverage testing])],
+ [],
+ [enable_coverage=no])
+
+if test "$enable_coverage" = "yes"; then
+ if test "$GCC" != "yes"; then
+ AC_MSG_ERROR(Coverage testing requires GCC)
+ fi
+
+ CFLAGS="$CFLAGS -O0 -g --coverage"
+ LDFLAGS="$LDFLAGS --coverage"
+fi
+
+AM_CONDITIONAL([WITH_COVERAGE], [test "$enable_coverage" = "yes"])
+AC_MSG_RESULT([$enable_coverage])
+
+# Strict
+
+AC_ARG_ENABLE(strict, [
+ AS_HELP_STRING([--enable-strict], [Strict code compilation])
+ ])
+
+AC_MSG_CHECKING([build strict])
+if test "$enable_strict" = "yes"; then
+ CFLAGS="$CFLAGS -Werror"
+else
+ enable_strict="no"
+fi
+AC_MSG_RESULT($enable_strict)
+
+AM_PATH_PYTHON([3.6])
+
+# Generate
+#
+
+AC_SUBST(PAM_LIBS)
+
+AC_CONFIG_FILES([
+Makefile
+doc/guide/version
+src/tls/cockpit-certificate-helper
+src/ws/cockpit-desktop
+])
+AC_OUTPUT
+
+
+dnl ==========================================================================
+echo "
+ Cockpit $VERSION
+ ================
+
+ prefix: ${prefix}
+ exec_prefix: ${exec_prefix}
+ libdir: ${libdir}
+ libexecdir: ${libexecdir}
+ bindir: ${bindir}
+ sbindir: ${sbindir}
+ datarootdir: ${datarootdir}
+ datadir: ${datadir}
+ sysconfdir: ${sysconfdir}
+ localstatedir: ${localstatedir}
+ pamdir: ${pamdir}
+ systemd unit dir: ${systemdunitdir}
+
+ compiler: ${CC}
+ cflags: ${CFLAGS}
+ cppflags: ${CPPFLAGS}
+
+ cockpit-ws user: ${COCKPIT_USER}
+ cockpit-ws group: ${COCKPIT_GROUP}
+ cockpit-ws instance user: ${COCKPIT_WSINSTANCE_USER}
+ cockpit-ws instance group: ${COCKPIT_WSINSTANCE_GROUP}
+ admin group: ${admin_group}
+ cockpit-session PATH: ${default_session_path}
+
+ Building docs: ${enable_doc}
+ Debug mode: ${debug_status}
+ Node environment: ${NODE_ENV}
+ With coverage: ${enable_coverage}
+ With address sanitizer: ${asan_status}
+ With PCP: ${enable_pcp}
+ With polkit: ${enable_polkit}
+ SELinux Policy: ${enable_selinux_policy}
+
+ cockpit-client: ${enable_cockpit_client}
+ cockpit-ssh: ${enable_ssh}
+
+ ssh-add: ${SSH_ADD}
+ ssh-agent: ${SSH_AGENT}
+
+Now type 'make' to compile cockpit."
diff --git a/containers/Makefile.am b/containers/Makefile.am
new file mode 100644
index 0000000..cbb385a
--- /dev/null
+++ b/containers/Makefile.am
@@ -0,0 +1,11 @@
+WS_DIR = $(srcdir)/containers/ws
+
+CLEANFILES += $(WS_DIR)/rpms
+
+ws-container:
+ rm -rf $(WS_DIR)/rpms
+ if [ -e cockpit-bridge-*.rpm ]; then mkdir -p $(WS_DIR)/rpms; cp *.rpm $(WS_DIR)/rpms; fi
+ podman build -t quay.io/cockpit/ws $(WS_DIR)
+
+ws-container-shell:
+ podman run -ti --rm --publish=9001:9090 quay.io/cockpit/ws /bin/bash
diff --git a/containers/flatpak/Makefile.am b/containers/flatpak/Makefile.am
new file mode 100644
index 0000000..314ee2d
--- /dev/null
+++ b/containers/flatpak/Makefile.am
@@ -0,0 +1,27 @@
+build-for-flatpak: cockpit-ws
+
+INSTALL_FLATPAK_TARGETS = \
+ install-cockpitwsPROGRAMS \
+ install-dist_client_metainfoDATA \
+ install-dist_defaultbrandingDATA \
+ install-dist_applicationsDATA \
+ install-dist_cockpitclientDATA \
+ install-dist_cockpitclientSCRIPTS \
+ install-dist_dbusservicesDATA \
+ install-dist_scalableiconDATA \
+ install-dist_symboliciconDATA \
+ install-cockpit-client-symlink \
+ install-python \
+ install-bundles \
+ $(NULL)
+
+install-for-flatpak: $(INSTALL_FLATPAK_TARGETS)
+ appstream-util validate --nonet src/client/org.cockpit_project.CockpitClient.metainfo.xml
+ if test -s "${DOWNSTREAM_RELEASES_XML}"; then \
+ $(top_srcdir)/tools/patch-metainfo \
+ '$(DESTDIR)$(datadir)/metainfo/org.cockpit_project.CockpitClient.metainfo.xml' \
+ "${DOWNSTREAM_RELEASES_XML}"; \
+ fi
+ appstream-util validate --nonet $(DESTDIR)$(datadir)/metainfo/org.cockpit_project.CockpitClient.metainfo.xml
+ cp -rT dist/static $(DESTDIR)$(pkgdatadir)/static
+ rm -rf $(DESTDIR)$(pkgdatadir)/apps $(DESTDIR)$(pkgdatadir)/playground
diff --git a/dist/apps/apps.css.LEGAL.txt b/dist/apps/apps.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/apps/apps.css.LEGAL.txt
diff --git a/dist/apps/apps.css.gz b/dist/apps/apps.css.gz
new file mode 100644
index 0000000..8269b6b
--- /dev/null
+++ b/dist/apps/apps.css.gz
Binary files differ
diff --git a/dist/apps/apps.js.LEGAL.txt b/dist/apps/apps.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/apps/apps.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/apps/apps.js.gz b/dist/apps/apps.js.gz
new file mode 100644
index 0000000..0d83816
--- /dev/null
+++ b/dist/apps/apps.js.gz
Binary files differ
diff --git a/dist/apps/default.png b/dist/apps/default.png
new file mode 100644
index 0000000..fa28647
--- /dev/null
+++ b/dist/apps/default.png
Binary files differ
diff --git a/dist/apps/index.html b/dist/apps/index.html
new file mode 100644
index 0000000..2c14ed3
--- /dev/null
+++ b/dist/apps/index.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2017 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html id="applications-page">
+ <head>
+ <title translate="yes">Applications</title>
+ <meta charset="utf-8" />
+ <link href="apps.css" type="text/css" rel="stylesheet" />
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script type="text/javascript" src="../manifests.js"></script>
+ <script type="text/javascript" src="apps.js"></script>
+ </head>
+ <body class="pf-v5-m-tabular-nums">
+
+ <div class="ct-page-fill" id="apps-page">
+ </div>
+
+ </body>
+</html>
diff --git a/dist/apps/manifest.json b/dist/apps/manifest.json
new file mode 100644
index 0000000..56039e5
--- /dev/null
+++ b/dist/apps/manifest.json
@@ -0,0 +1,23 @@
+{
+ "tools": {
+ "index": {
+ "label": "Applications",
+ "keywords": [
+ {
+ "matches": ["plugin", "apps", "addon", "add-on", "install", "extension"]
+ }
+ ]
+ }
+ },
+
+ "content-security-policy": "img-src *",
+
+ "config": {
+ "appstream_config_packages": {
+ "debian": ["appstream"]
+ },
+ "appstream_data_packages": {
+ "fedora": ["appstream-data"]
+ }
+ }
+}
diff --git a/dist/apps/po.cs.js.gz b/dist/apps/po.cs.js.gz
new file mode 100644
index 0000000..a9fe81b
--- /dev/null
+++ b/dist/apps/po.cs.js.gz
Binary files differ
diff --git a/dist/apps/po.de.js.gz b/dist/apps/po.de.js.gz
new file mode 100644
index 0000000..2e1d49a
--- /dev/null
+++ b/dist/apps/po.de.js.gz
Binary files differ
diff --git a/dist/apps/po.es.js.gz b/dist/apps/po.es.js.gz
new file mode 100644
index 0000000..87e1d05
--- /dev/null
+++ b/dist/apps/po.es.js.gz
Binary files differ
diff --git a/dist/apps/po.fi.js.gz b/dist/apps/po.fi.js.gz
new file mode 100644
index 0000000..fc05737
--- /dev/null
+++ b/dist/apps/po.fi.js.gz
Binary files differ
diff --git a/dist/apps/po.fr.js.gz b/dist/apps/po.fr.js.gz
new file mode 100644
index 0000000..d9ce6b7
--- /dev/null
+++ b/dist/apps/po.fr.js.gz
Binary files differ
diff --git a/dist/apps/po.he.js.gz b/dist/apps/po.he.js.gz
new file mode 100644
index 0000000..69fd524
--- /dev/null
+++ b/dist/apps/po.he.js.gz
Binary files differ
diff --git a/dist/apps/po.it.js.gz b/dist/apps/po.it.js.gz
new file mode 100644
index 0000000..a1ea9fa
--- /dev/null
+++ b/dist/apps/po.it.js.gz
Binary files differ
diff --git a/dist/apps/po.ja.js.gz b/dist/apps/po.ja.js.gz
new file mode 100644
index 0000000..9f116c4
--- /dev/null
+++ b/dist/apps/po.ja.js.gz
Binary files differ
diff --git a/dist/apps/po.ka.js.gz b/dist/apps/po.ka.js.gz
new file mode 100644
index 0000000..c346454
--- /dev/null
+++ b/dist/apps/po.ka.js.gz
Binary files differ
diff --git a/dist/apps/po.ko.js.gz b/dist/apps/po.ko.js.gz
new file mode 100644
index 0000000..ef7bd05
--- /dev/null
+++ b/dist/apps/po.ko.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.cs.js.gz b/dist/apps/po.manifest.cs.js.gz
new file mode 100644
index 0000000..8af005c
--- /dev/null
+++ b/dist/apps/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.de.js.gz b/dist/apps/po.manifest.de.js.gz
new file mode 100644
index 0000000..ea767ba
--- /dev/null
+++ b/dist/apps/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.es.js.gz b/dist/apps/po.manifest.es.js.gz
new file mode 100644
index 0000000..68fa34a
--- /dev/null
+++ b/dist/apps/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.fi.js.gz b/dist/apps/po.manifest.fi.js.gz
new file mode 100644
index 0000000..dd6c059
--- /dev/null
+++ b/dist/apps/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.fr.js.gz b/dist/apps/po.manifest.fr.js.gz
new file mode 100644
index 0000000..0f12c4d
--- /dev/null
+++ b/dist/apps/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.he.js.gz b/dist/apps/po.manifest.he.js.gz
new file mode 100644
index 0000000..adb56ed
--- /dev/null
+++ b/dist/apps/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.it.js.gz b/dist/apps/po.manifest.it.js.gz
new file mode 100644
index 0000000..067f32c
--- /dev/null
+++ b/dist/apps/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.ja.js.gz b/dist/apps/po.manifest.ja.js.gz
new file mode 100644
index 0000000..fea286f
--- /dev/null
+++ b/dist/apps/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.ka.js.gz b/dist/apps/po.manifest.ka.js.gz
new file mode 100644
index 0000000..d2e0024
--- /dev/null
+++ b/dist/apps/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.ko.js.gz b/dist/apps/po.manifest.ko.js.gz
new file mode 100644
index 0000000..809bc81
--- /dev/null
+++ b/dist/apps/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.nb_NO.js.gz b/dist/apps/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..38aa98d
--- /dev/null
+++ b/dist/apps/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.nl.js.gz b/dist/apps/po.manifest.nl.js.gz
new file mode 100644
index 0000000..cdbe019
--- /dev/null
+++ b/dist/apps/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.pl.js.gz b/dist/apps/po.manifest.pl.js.gz
new file mode 100644
index 0000000..53b7c82
--- /dev/null
+++ b/dist/apps/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.pt_BR.js.gz b/dist/apps/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..4aae0a3
--- /dev/null
+++ b/dist/apps/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.ru.js.gz b/dist/apps/po.manifest.ru.js.gz
new file mode 100644
index 0000000..c707342
--- /dev/null
+++ b/dist/apps/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.sk.js.gz b/dist/apps/po.manifest.sk.js.gz
new file mode 100644
index 0000000..b469435
--- /dev/null
+++ b/dist/apps/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.sv.js.gz b/dist/apps/po.manifest.sv.js.gz
new file mode 100644
index 0000000..e6ced97
--- /dev/null
+++ b/dist/apps/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.tr.js.gz b/dist/apps/po.manifest.tr.js.gz
new file mode 100644
index 0000000..ac5f262
--- /dev/null
+++ b/dist/apps/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.uk.js.gz b/dist/apps/po.manifest.uk.js.gz
new file mode 100644
index 0000000..d29ba12
--- /dev/null
+++ b/dist/apps/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/apps/po.manifest.zh_CN.js.gz b/dist/apps/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..e7d7783
--- /dev/null
+++ b/dist/apps/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/apps/po.nb_NO.js.gz b/dist/apps/po.nb_NO.js.gz
new file mode 100644
index 0000000..68ede04
--- /dev/null
+++ b/dist/apps/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/apps/po.nl.js.gz b/dist/apps/po.nl.js.gz
new file mode 100644
index 0000000..6c291a5
--- /dev/null
+++ b/dist/apps/po.nl.js.gz
Binary files differ
diff --git a/dist/apps/po.pl.js.gz b/dist/apps/po.pl.js.gz
new file mode 100644
index 0000000..21b1c8e
--- /dev/null
+++ b/dist/apps/po.pl.js.gz
Binary files differ
diff --git a/dist/apps/po.pt_BR.js.gz b/dist/apps/po.pt_BR.js.gz
new file mode 100644
index 0000000..d180dd0
--- /dev/null
+++ b/dist/apps/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/apps/po.ru.js.gz b/dist/apps/po.ru.js.gz
new file mode 100644
index 0000000..c52b847
--- /dev/null
+++ b/dist/apps/po.ru.js.gz
Binary files differ
diff --git a/dist/apps/po.sk.js.gz b/dist/apps/po.sk.js.gz
new file mode 100644
index 0000000..dbb9d24
--- /dev/null
+++ b/dist/apps/po.sk.js.gz
Binary files differ
diff --git a/dist/apps/po.sv.js.gz b/dist/apps/po.sv.js.gz
new file mode 100644
index 0000000..33e551d
--- /dev/null
+++ b/dist/apps/po.sv.js.gz
Binary files differ
diff --git a/dist/apps/po.tr.js.gz b/dist/apps/po.tr.js.gz
new file mode 100644
index 0000000..63bd2be
--- /dev/null
+++ b/dist/apps/po.tr.js.gz
Binary files differ
diff --git a/dist/apps/po.uk.js.gz b/dist/apps/po.uk.js.gz
new file mode 100644
index 0000000..1bf492f
--- /dev/null
+++ b/dist/apps/po.uk.js.gz
Binary files differ
diff --git a/dist/apps/po.zh_CN.js.gz b/dist/apps/po.zh_CN.js.gz
new file mode 100644
index 0000000..ee91d4e
--- /dev/null
+++ b/dist/apps/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/base1/cockpit.js.LEGAL.txt b/dist/base1/cockpit.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/base1/cockpit.js.LEGAL.txt
diff --git a/dist/base1/cockpit.js.gz b/dist/base1/cockpit.js.gz
new file mode 100644
index 0000000..34a7a83
--- /dev/null
+++ b/dist/base1/cockpit.js.gz
Binary files differ
diff --git a/dist/base1/manifest.json b/dist/base1/manifest.json
new file mode 100644
index 0000000..765f494
--- /dev/null
+++ b/dist/base1/manifest.json
@@ -0,0 +1,5 @@
+{
+ "version": "219",
+ "version-note": "last API change: https://github.com/cockpit-project/cockpit/pull/13482",
+ "priority": -1
+}
diff --git a/dist/base1/po.cs.js.gz b/dist/base1/po.cs.js.gz
new file mode 100644
index 0000000..7998005
--- /dev/null
+++ b/dist/base1/po.cs.js.gz
Binary files differ
diff --git a/dist/base1/po.de.js.gz b/dist/base1/po.de.js.gz
new file mode 100644
index 0000000..20ab11b
--- /dev/null
+++ b/dist/base1/po.de.js.gz
Binary files differ
diff --git a/dist/base1/po.es.js.gz b/dist/base1/po.es.js.gz
new file mode 100644
index 0000000..3c84cf2
--- /dev/null
+++ b/dist/base1/po.es.js.gz
Binary files differ
diff --git a/dist/base1/po.fi.js.gz b/dist/base1/po.fi.js.gz
new file mode 100644
index 0000000..3f29c58
--- /dev/null
+++ b/dist/base1/po.fi.js.gz
Binary files differ
diff --git a/dist/base1/po.fr.js.gz b/dist/base1/po.fr.js.gz
new file mode 100644
index 0000000..7970c10
--- /dev/null
+++ b/dist/base1/po.fr.js.gz
Binary files differ
diff --git a/dist/base1/po.he.js.gz b/dist/base1/po.he.js.gz
new file mode 100644
index 0000000..408efe9
--- /dev/null
+++ b/dist/base1/po.he.js.gz
Binary files differ
diff --git a/dist/base1/po.it.js.gz b/dist/base1/po.it.js.gz
new file mode 100644
index 0000000..6eaf8f5
--- /dev/null
+++ b/dist/base1/po.it.js.gz
Binary files differ
diff --git a/dist/base1/po.ja.js.gz b/dist/base1/po.ja.js.gz
new file mode 100644
index 0000000..a7ad6f9
--- /dev/null
+++ b/dist/base1/po.ja.js.gz
Binary files differ
diff --git a/dist/base1/po.ka.js.gz b/dist/base1/po.ka.js.gz
new file mode 100644
index 0000000..7512dee
--- /dev/null
+++ b/dist/base1/po.ka.js.gz
Binary files differ
diff --git a/dist/base1/po.ko.js.gz b/dist/base1/po.ko.js.gz
new file mode 100644
index 0000000..c8ab19f
--- /dev/null
+++ b/dist/base1/po.ko.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.cs.js.gz b/dist/base1/po.manifest.cs.js.gz
new file mode 100644
index 0000000..6ba5ade
--- /dev/null
+++ b/dist/base1/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.de.js.gz b/dist/base1/po.manifest.de.js.gz
new file mode 100644
index 0000000..0be71bb
--- /dev/null
+++ b/dist/base1/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.es.js.gz b/dist/base1/po.manifest.es.js.gz
new file mode 100644
index 0000000..cf24fe2
--- /dev/null
+++ b/dist/base1/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.fi.js.gz b/dist/base1/po.manifest.fi.js.gz
new file mode 100644
index 0000000..1a84aef
--- /dev/null
+++ b/dist/base1/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.fr.js.gz b/dist/base1/po.manifest.fr.js.gz
new file mode 100644
index 0000000..cc0877b
--- /dev/null
+++ b/dist/base1/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.he.js.gz b/dist/base1/po.manifest.he.js.gz
new file mode 100644
index 0000000..d74276b
--- /dev/null
+++ b/dist/base1/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.it.js.gz b/dist/base1/po.manifest.it.js.gz
new file mode 100644
index 0000000..8e4f6af
--- /dev/null
+++ b/dist/base1/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.ja.js.gz b/dist/base1/po.manifest.ja.js.gz
new file mode 100644
index 0000000..103221a
--- /dev/null
+++ b/dist/base1/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.ka.js.gz b/dist/base1/po.manifest.ka.js.gz
new file mode 100644
index 0000000..94e0f75
--- /dev/null
+++ b/dist/base1/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.ko.js.gz b/dist/base1/po.manifest.ko.js.gz
new file mode 100644
index 0000000..a8cf98b
--- /dev/null
+++ b/dist/base1/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.nb_NO.js.gz b/dist/base1/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..938ce73
--- /dev/null
+++ b/dist/base1/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.nl.js.gz b/dist/base1/po.manifest.nl.js.gz
new file mode 100644
index 0000000..98bfd8f
--- /dev/null
+++ b/dist/base1/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.pl.js.gz b/dist/base1/po.manifest.pl.js.gz
new file mode 100644
index 0000000..8a6ec92
--- /dev/null
+++ b/dist/base1/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.pt_BR.js.gz b/dist/base1/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..9de85e0
--- /dev/null
+++ b/dist/base1/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.ru.js.gz b/dist/base1/po.manifest.ru.js.gz
new file mode 100644
index 0000000..9580f39
--- /dev/null
+++ b/dist/base1/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.sk.js.gz b/dist/base1/po.manifest.sk.js.gz
new file mode 100644
index 0000000..cc370c8
--- /dev/null
+++ b/dist/base1/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.sv.js.gz b/dist/base1/po.manifest.sv.js.gz
new file mode 100644
index 0000000..9fd86dc
--- /dev/null
+++ b/dist/base1/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.tr.js.gz b/dist/base1/po.manifest.tr.js.gz
new file mode 100644
index 0000000..87bec13
--- /dev/null
+++ b/dist/base1/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.uk.js.gz b/dist/base1/po.manifest.uk.js.gz
new file mode 100644
index 0000000..5e28105
--- /dev/null
+++ b/dist/base1/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/base1/po.manifest.zh_CN.js.gz b/dist/base1/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..08f53cd
--- /dev/null
+++ b/dist/base1/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/base1/po.nb_NO.js.gz b/dist/base1/po.nb_NO.js.gz
new file mode 100644
index 0000000..dd48cdb
--- /dev/null
+++ b/dist/base1/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/base1/po.nl.js.gz b/dist/base1/po.nl.js.gz
new file mode 100644
index 0000000..d24d9eb
--- /dev/null
+++ b/dist/base1/po.nl.js.gz
Binary files differ
diff --git a/dist/base1/po.pl.js.gz b/dist/base1/po.pl.js.gz
new file mode 100644
index 0000000..d3c509f
--- /dev/null
+++ b/dist/base1/po.pl.js.gz
Binary files differ
diff --git a/dist/base1/po.pt_BR.js.gz b/dist/base1/po.pt_BR.js.gz
new file mode 100644
index 0000000..2bdb1e5
--- /dev/null
+++ b/dist/base1/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/base1/po.ru.js.gz b/dist/base1/po.ru.js.gz
new file mode 100644
index 0000000..703b921
--- /dev/null
+++ b/dist/base1/po.ru.js.gz
Binary files differ
diff --git a/dist/base1/po.sk.js.gz b/dist/base1/po.sk.js.gz
new file mode 100644
index 0000000..1add50b
--- /dev/null
+++ b/dist/base1/po.sk.js.gz
Binary files differ
diff --git a/dist/base1/po.sv.js.gz b/dist/base1/po.sv.js.gz
new file mode 100644
index 0000000..094b537
--- /dev/null
+++ b/dist/base1/po.sv.js.gz
Binary files differ
diff --git a/dist/base1/po.tr.js.gz b/dist/base1/po.tr.js.gz
new file mode 100644
index 0000000..5cdc36d
--- /dev/null
+++ b/dist/base1/po.tr.js.gz
Binary files differ
diff --git a/dist/base1/po.uk.js.gz b/dist/base1/po.uk.js.gz
new file mode 100644
index 0000000..050c067
--- /dev/null
+++ b/dist/base1/po.uk.js.gz
Binary files differ
diff --git a/dist/base1/po.zh_CN.js.gz b/dist/base1/po.zh_CN.js.gz
new file mode 100644
index 0000000..94d4737
--- /dev/null
+++ b/dist/base1/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/kdump/index.html b/dist/kdump/index.html
new file mode 100644
index 0000000..e1d43dd
--- /dev/null
+++ b/dist/kdump/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2016 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="kdump-page" lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title translate="yes">Kernel dump</title>
+ <meta name="description" content="" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+ <link rel="stylesheet" href="kdump.css" />
+
+ <script type="text/javascript" src="kdump.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app"></div>
+</body>
+</html>
diff --git a/dist/kdump/kdump.css.LEGAL.txt b/dist/kdump/kdump.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/kdump/kdump.css.LEGAL.txt
diff --git a/dist/kdump/kdump.css.gz b/dist/kdump/kdump.css.gz
new file mode 100644
index 0000000..95e2833
--- /dev/null
+++ b/dist/kdump/kdump.css.gz
Binary files differ
diff --git a/dist/kdump/kdump.js.LEGAL.txt b/dist/kdump/kdump.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/kdump/kdump.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/kdump/kdump.js.gz b/dist/kdump/kdump.js.gz
new file mode 100644
index 0000000..36bf278
--- /dev/null
+++ b/dist/kdump/kdump.js.gz
Binary files differ
diff --git a/dist/kdump/manifest.json b/dist/kdump/manifest.json
new file mode 100644
index 0000000..cdc3b62
--- /dev/null
+++ b/dist/kdump/manifest.json
@@ -0,0 +1,22 @@
+{
+ "conditions": [
+ {"path-exists": "/usr/sbin/kexec"}
+ ],
+ "tools": {
+ "index": {
+ "label": "Kernel dump",
+ "docs": [
+ {
+ "label": "Configuring kdump",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-kdump-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["kdump", "crash"]
+ }
+ ]
+ }
+ },
+ "content-security-policy": "img-src 'self' data:"
+}
diff --git a/dist/kdump/po.cs.js.gz b/dist/kdump/po.cs.js.gz
new file mode 100644
index 0000000..b43e585
--- /dev/null
+++ b/dist/kdump/po.cs.js.gz
Binary files differ
diff --git a/dist/kdump/po.de.js.gz b/dist/kdump/po.de.js.gz
new file mode 100644
index 0000000..5db5a48
--- /dev/null
+++ b/dist/kdump/po.de.js.gz
Binary files differ
diff --git a/dist/kdump/po.es.js.gz b/dist/kdump/po.es.js.gz
new file mode 100644
index 0000000..8eb1ec7
--- /dev/null
+++ b/dist/kdump/po.es.js.gz
Binary files differ
diff --git a/dist/kdump/po.fi.js.gz b/dist/kdump/po.fi.js.gz
new file mode 100644
index 0000000..8bc0eb3
--- /dev/null
+++ b/dist/kdump/po.fi.js.gz
Binary files differ
diff --git a/dist/kdump/po.fr.js.gz b/dist/kdump/po.fr.js.gz
new file mode 100644
index 0000000..d1ab75a
--- /dev/null
+++ b/dist/kdump/po.fr.js.gz
Binary files differ
diff --git a/dist/kdump/po.he.js.gz b/dist/kdump/po.he.js.gz
new file mode 100644
index 0000000..5c5f4d1
--- /dev/null
+++ b/dist/kdump/po.he.js.gz
Binary files differ
diff --git a/dist/kdump/po.it.js.gz b/dist/kdump/po.it.js.gz
new file mode 100644
index 0000000..7e24d64
--- /dev/null
+++ b/dist/kdump/po.it.js.gz
Binary files differ
diff --git a/dist/kdump/po.ja.js.gz b/dist/kdump/po.ja.js.gz
new file mode 100644
index 0000000..08db643
--- /dev/null
+++ b/dist/kdump/po.ja.js.gz
Binary files differ
diff --git a/dist/kdump/po.ka.js.gz b/dist/kdump/po.ka.js.gz
new file mode 100644
index 0000000..982ad9a
--- /dev/null
+++ b/dist/kdump/po.ka.js.gz
Binary files differ
diff --git a/dist/kdump/po.ko.js.gz b/dist/kdump/po.ko.js.gz
new file mode 100644
index 0000000..97e7cca
--- /dev/null
+++ b/dist/kdump/po.ko.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.cs.js.gz b/dist/kdump/po.manifest.cs.js.gz
new file mode 100644
index 0000000..6977121
--- /dev/null
+++ b/dist/kdump/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.de.js.gz b/dist/kdump/po.manifest.de.js.gz
new file mode 100644
index 0000000..96be21d
--- /dev/null
+++ b/dist/kdump/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.es.js.gz b/dist/kdump/po.manifest.es.js.gz
new file mode 100644
index 0000000..ba8797d
--- /dev/null
+++ b/dist/kdump/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.fi.js.gz b/dist/kdump/po.manifest.fi.js.gz
new file mode 100644
index 0000000..187de27
--- /dev/null
+++ b/dist/kdump/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.fr.js.gz b/dist/kdump/po.manifest.fr.js.gz
new file mode 100644
index 0000000..62ec472
--- /dev/null
+++ b/dist/kdump/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.he.js.gz b/dist/kdump/po.manifest.he.js.gz
new file mode 100644
index 0000000..b543994
--- /dev/null
+++ b/dist/kdump/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.it.js.gz b/dist/kdump/po.manifest.it.js.gz
new file mode 100644
index 0000000..afba5b4
--- /dev/null
+++ b/dist/kdump/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.ja.js.gz b/dist/kdump/po.manifest.ja.js.gz
new file mode 100644
index 0000000..c56c553
--- /dev/null
+++ b/dist/kdump/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.ka.js.gz b/dist/kdump/po.manifest.ka.js.gz
new file mode 100644
index 0000000..5b46155
--- /dev/null
+++ b/dist/kdump/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.ko.js.gz b/dist/kdump/po.manifest.ko.js.gz
new file mode 100644
index 0000000..696ce88
--- /dev/null
+++ b/dist/kdump/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.nb_NO.js.gz b/dist/kdump/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..b9e56e2
--- /dev/null
+++ b/dist/kdump/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.nl.js.gz b/dist/kdump/po.manifest.nl.js.gz
new file mode 100644
index 0000000..9f51891
--- /dev/null
+++ b/dist/kdump/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.pl.js.gz b/dist/kdump/po.manifest.pl.js.gz
new file mode 100644
index 0000000..03789ca
--- /dev/null
+++ b/dist/kdump/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.pt_BR.js.gz b/dist/kdump/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..6f44ab1
--- /dev/null
+++ b/dist/kdump/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.ru.js.gz b/dist/kdump/po.manifest.ru.js.gz
new file mode 100644
index 0000000..a4e597c
--- /dev/null
+++ b/dist/kdump/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.sk.js.gz b/dist/kdump/po.manifest.sk.js.gz
new file mode 100644
index 0000000..70b26ee
--- /dev/null
+++ b/dist/kdump/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.sv.js.gz b/dist/kdump/po.manifest.sv.js.gz
new file mode 100644
index 0000000..58856b2
--- /dev/null
+++ b/dist/kdump/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.tr.js.gz b/dist/kdump/po.manifest.tr.js.gz
new file mode 100644
index 0000000..cb4936c
--- /dev/null
+++ b/dist/kdump/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.uk.js.gz b/dist/kdump/po.manifest.uk.js.gz
new file mode 100644
index 0000000..4510f57
--- /dev/null
+++ b/dist/kdump/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/kdump/po.manifest.zh_CN.js.gz b/dist/kdump/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..e33617b
--- /dev/null
+++ b/dist/kdump/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/kdump/po.nb_NO.js.gz b/dist/kdump/po.nb_NO.js.gz
new file mode 100644
index 0000000..e5463e1
--- /dev/null
+++ b/dist/kdump/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/kdump/po.nl.js.gz b/dist/kdump/po.nl.js.gz
new file mode 100644
index 0000000..9b70659
--- /dev/null
+++ b/dist/kdump/po.nl.js.gz
Binary files differ
diff --git a/dist/kdump/po.pl.js.gz b/dist/kdump/po.pl.js.gz
new file mode 100644
index 0000000..7caec42
--- /dev/null
+++ b/dist/kdump/po.pl.js.gz
Binary files differ
diff --git a/dist/kdump/po.pt_BR.js.gz b/dist/kdump/po.pt_BR.js.gz
new file mode 100644
index 0000000..71d536b
--- /dev/null
+++ b/dist/kdump/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/kdump/po.ru.js.gz b/dist/kdump/po.ru.js.gz
new file mode 100644
index 0000000..ccce05d
--- /dev/null
+++ b/dist/kdump/po.ru.js.gz
Binary files differ
diff --git a/dist/kdump/po.sk.js.gz b/dist/kdump/po.sk.js.gz
new file mode 100644
index 0000000..bdfb048
--- /dev/null
+++ b/dist/kdump/po.sk.js.gz
Binary files differ
diff --git a/dist/kdump/po.sv.js.gz b/dist/kdump/po.sv.js.gz
new file mode 100644
index 0000000..f30342e
--- /dev/null
+++ b/dist/kdump/po.sv.js.gz
Binary files differ
diff --git a/dist/kdump/po.tr.js.gz b/dist/kdump/po.tr.js.gz
new file mode 100644
index 0000000..ea38912
--- /dev/null
+++ b/dist/kdump/po.tr.js.gz
Binary files differ
diff --git a/dist/kdump/po.uk.js.gz b/dist/kdump/po.uk.js.gz
new file mode 100644
index 0000000..54a982a
--- /dev/null
+++ b/dist/kdump/po.uk.js.gz
Binary files differ
diff --git a/dist/kdump/po.zh_CN.js.gz b/dist/kdump/po.zh_CN.js.gz
new file mode 100644
index 0000000..4f8eba8
--- /dev/null
+++ b/dist/kdump/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/metrics/index.css.LEGAL.txt b/dist/metrics/index.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/metrics/index.css.LEGAL.txt
diff --git a/dist/metrics/index.css.gz b/dist/metrics/index.css.gz
new file mode 100644
index 0000000..3a5c2e3
--- /dev/null
+++ b/dist/metrics/index.css.gz
Binary files differ
diff --git a/dist/metrics/index.html b/dist/metrics/index.html
new file mode 100644
index 0000000..3fd14f9
--- /dev/null
+++ b/dist/metrics/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this package; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="metrics-page" lang="en">
+<head>
+ <title translate="yes">Metrics and history</title>
+ <meta charset="utf-8" />
+ <meta name="description" content="" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+ <link rel="stylesheet" href="index.css" />
+
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script type="text/javascript" src="../manifests.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script type="text/javascript" src="index.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app"></div>
+</body>
+</html>
diff --git a/dist/metrics/index.js.LEGAL.txt b/dist/metrics/index.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/metrics/index.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/metrics/index.js.gz b/dist/metrics/index.js.gz
new file mode 100644
index 0000000..c0f6c4e
--- /dev/null
+++ b/dist/metrics/index.js.gz
Binary files differ
diff --git a/dist/metrics/manifest.json b/dist/metrics/manifest.json
new file mode 100644
index 0000000..6b3d3ec
--- /dev/null
+++ b/dist/metrics/manifest.json
@@ -0,0 +1,11 @@
+{
+ "parent": {
+ "component": "system",
+ "docs": [
+ {
+ "label": "Performance Co-Pilot",
+ "url": "https://pcp.readthedocs.io/en/latest/"
+ }
+ ]
+ }
+}
diff --git a/dist/metrics/po.cs.js.gz b/dist/metrics/po.cs.js.gz
new file mode 100644
index 0000000..219cd7e
--- /dev/null
+++ b/dist/metrics/po.cs.js.gz
Binary files differ
diff --git a/dist/metrics/po.de.js.gz b/dist/metrics/po.de.js.gz
new file mode 100644
index 0000000..f2bb05b
--- /dev/null
+++ b/dist/metrics/po.de.js.gz
Binary files differ
diff --git a/dist/metrics/po.es.js.gz b/dist/metrics/po.es.js.gz
new file mode 100644
index 0000000..7679d4a
--- /dev/null
+++ b/dist/metrics/po.es.js.gz
Binary files differ
diff --git a/dist/metrics/po.fi.js.gz b/dist/metrics/po.fi.js.gz
new file mode 100644
index 0000000..a603f38
--- /dev/null
+++ b/dist/metrics/po.fi.js.gz
Binary files differ
diff --git a/dist/metrics/po.fr.js.gz b/dist/metrics/po.fr.js.gz
new file mode 100644
index 0000000..073dcd7
--- /dev/null
+++ b/dist/metrics/po.fr.js.gz
Binary files differ
diff --git a/dist/metrics/po.he.js.gz b/dist/metrics/po.he.js.gz
new file mode 100644
index 0000000..853daac
--- /dev/null
+++ b/dist/metrics/po.he.js.gz
Binary files differ
diff --git a/dist/metrics/po.it.js.gz b/dist/metrics/po.it.js.gz
new file mode 100644
index 0000000..a72ac0d
--- /dev/null
+++ b/dist/metrics/po.it.js.gz
Binary files differ
diff --git a/dist/metrics/po.ja.js.gz b/dist/metrics/po.ja.js.gz
new file mode 100644
index 0000000..fcc0fb2
--- /dev/null
+++ b/dist/metrics/po.ja.js.gz
Binary files differ
diff --git a/dist/metrics/po.ka.js.gz b/dist/metrics/po.ka.js.gz
new file mode 100644
index 0000000..9dbdd06
--- /dev/null
+++ b/dist/metrics/po.ka.js.gz
Binary files differ
diff --git a/dist/metrics/po.ko.js.gz b/dist/metrics/po.ko.js.gz
new file mode 100644
index 0000000..d118793
--- /dev/null
+++ b/dist/metrics/po.ko.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.cs.js.gz b/dist/metrics/po.manifest.cs.js.gz
new file mode 100644
index 0000000..d243bfd
--- /dev/null
+++ b/dist/metrics/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.de.js.gz b/dist/metrics/po.manifest.de.js.gz
new file mode 100644
index 0000000..e0d3c44
--- /dev/null
+++ b/dist/metrics/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.es.js.gz b/dist/metrics/po.manifest.es.js.gz
new file mode 100644
index 0000000..ad4c0ea
--- /dev/null
+++ b/dist/metrics/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.fi.js.gz b/dist/metrics/po.manifest.fi.js.gz
new file mode 100644
index 0000000..4b23054
--- /dev/null
+++ b/dist/metrics/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.fr.js.gz b/dist/metrics/po.manifest.fr.js.gz
new file mode 100644
index 0000000..44b0a4d
--- /dev/null
+++ b/dist/metrics/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.he.js.gz b/dist/metrics/po.manifest.he.js.gz
new file mode 100644
index 0000000..644812c
--- /dev/null
+++ b/dist/metrics/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.it.js.gz b/dist/metrics/po.manifest.it.js.gz
new file mode 100644
index 0000000..13882fa
--- /dev/null
+++ b/dist/metrics/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.ja.js.gz b/dist/metrics/po.manifest.ja.js.gz
new file mode 100644
index 0000000..9b1f34c
--- /dev/null
+++ b/dist/metrics/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.ka.js.gz b/dist/metrics/po.manifest.ka.js.gz
new file mode 100644
index 0000000..6edfceb
--- /dev/null
+++ b/dist/metrics/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.ko.js.gz b/dist/metrics/po.manifest.ko.js.gz
new file mode 100644
index 0000000..5362201
--- /dev/null
+++ b/dist/metrics/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.nb_NO.js.gz b/dist/metrics/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..200f3f3
--- /dev/null
+++ b/dist/metrics/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.nl.js.gz b/dist/metrics/po.manifest.nl.js.gz
new file mode 100644
index 0000000..789725a
--- /dev/null
+++ b/dist/metrics/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.pl.js.gz b/dist/metrics/po.manifest.pl.js.gz
new file mode 100644
index 0000000..756f1c8
--- /dev/null
+++ b/dist/metrics/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.pt_BR.js.gz b/dist/metrics/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..0a48f55
--- /dev/null
+++ b/dist/metrics/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.ru.js.gz b/dist/metrics/po.manifest.ru.js.gz
new file mode 100644
index 0000000..52be72c
--- /dev/null
+++ b/dist/metrics/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.sk.js.gz b/dist/metrics/po.manifest.sk.js.gz
new file mode 100644
index 0000000..ffce0e2
--- /dev/null
+++ b/dist/metrics/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.sv.js.gz b/dist/metrics/po.manifest.sv.js.gz
new file mode 100644
index 0000000..d841977
--- /dev/null
+++ b/dist/metrics/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.tr.js.gz b/dist/metrics/po.manifest.tr.js.gz
new file mode 100644
index 0000000..054e2ff
--- /dev/null
+++ b/dist/metrics/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.uk.js.gz b/dist/metrics/po.manifest.uk.js.gz
new file mode 100644
index 0000000..8cf2072
--- /dev/null
+++ b/dist/metrics/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/metrics/po.manifest.zh_CN.js.gz b/dist/metrics/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..af85386
--- /dev/null
+++ b/dist/metrics/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/metrics/po.nb_NO.js.gz b/dist/metrics/po.nb_NO.js.gz
new file mode 100644
index 0000000..93ec160
--- /dev/null
+++ b/dist/metrics/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/metrics/po.nl.js.gz b/dist/metrics/po.nl.js.gz
new file mode 100644
index 0000000..21a8d6e
--- /dev/null
+++ b/dist/metrics/po.nl.js.gz
Binary files differ
diff --git a/dist/metrics/po.pl.js.gz b/dist/metrics/po.pl.js.gz
new file mode 100644
index 0000000..c13b135
--- /dev/null
+++ b/dist/metrics/po.pl.js.gz
Binary files differ
diff --git a/dist/metrics/po.pt_BR.js.gz b/dist/metrics/po.pt_BR.js.gz
new file mode 100644
index 0000000..d70ab7f
--- /dev/null
+++ b/dist/metrics/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/metrics/po.ru.js.gz b/dist/metrics/po.ru.js.gz
new file mode 100644
index 0000000..a85fd75
--- /dev/null
+++ b/dist/metrics/po.ru.js.gz
Binary files differ
diff --git a/dist/metrics/po.sk.js.gz b/dist/metrics/po.sk.js.gz
new file mode 100644
index 0000000..628ac4d
--- /dev/null
+++ b/dist/metrics/po.sk.js.gz
Binary files differ
diff --git a/dist/metrics/po.sv.js.gz b/dist/metrics/po.sv.js.gz
new file mode 100644
index 0000000..a767895
--- /dev/null
+++ b/dist/metrics/po.sv.js.gz
Binary files differ
diff --git a/dist/metrics/po.tr.js.gz b/dist/metrics/po.tr.js.gz
new file mode 100644
index 0000000..6c1e14a
--- /dev/null
+++ b/dist/metrics/po.tr.js.gz
Binary files differ
diff --git a/dist/metrics/po.uk.js.gz b/dist/metrics/po.uk.js.gz
new file mode 100644
index 0000000..444f53d
--- /dev/null
+++ b/dist/metrics/po.uk.js.gz
Binary files differ
diff --git a/dist/metrics/po.zh_CN.js.gz b/dist/metrics/po.zh_CN.js.gz
new file mode 100644
index 0000000..8664a63
--- /dev/null
+++ b/dist/metrics/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/networkmanager/firewall.css.LEGAL.txt b/dist/networkmanager/firewall.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/networkmanager/firewall.css.LEGAL.txt
diff --git a/dist/networkmanager/firewall.css.gz b/dist/networkmanager/firewall.css.gz
new file mode 100644
index 0000000..904df35
--- /dev/null
+++ b/dist/networkmanager/firewall.css.gz
Binary files differ
diff --git a/dist/networkmanager/firewall.html b/dist/networkmanager/firewall.html
new file mode 100644
index 0000000..4035878
--- /dev/null
+++ b/dist/networkmanager/firewall.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2017 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+ <title translate="yes">Firewall</title>
+ <meta charset="utf-8" />
+
+ <link href="firewall.css" type="text/css" rel="stylesheet" />
+
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+ <script src="firewall.js"></script>
+ </head>
+
+ <body class="pf-v5-m-tabular-nums">
+ <div id="firewall" class="ct-page-fill"></div>
+ </body>
+</html>
diff --git a/dist/networkmanager/firewall.js.LEGAL.txt b/dist/networkmanager/firewall.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/networkmanager/firewall.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/networkmanager/firewall.js.gz b/dist/networkmanager/firewall.js.gz
new file mode 100644
index 0000000..d58b7c7
--- /dev/null
+++ b/dist/networkmanager/firewall.js.gz
Binary files differ
diff --git a/dist/networkmanager/index.html b/dist/networkmanager/index.html
new file mode 100644
index 0000000..a2ca810
--- /dev/null
+++ b/dist/networkmanager/index.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html id="networkmanager-page">
+<head>
+ <title translate="yes">Networking</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="networkmanager.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../manifests.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+ <script src="networkmanager.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+
+ <div id="network-page" class="ct-page-fill network-page">
+ </div>
+
+</body>
+</html>
diff --git a/dist/networkmanager/manifest.json b/dist/networkmanager/manifest.json
new file mode 100644
index 0000000..8ef35f8
--- /dev/null
+++ b/dist/networkmanager/manifest.json
@@ -0,0 +1,43 @@
+{
+ "name": "network",
+ "conditions": [
+ {"path-exists": "/usr/share/dbus-1/system.d/org.freedesktop.NetworkManager.conf"}
+ ],
+ "menu": {
+ "index": {
+ "label": "Networking",
+ "order": 40,
+ "docs": [
+ {
+ "label": "Managing networking bonds",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-network-bonds-using-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing networking teams",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-network-teams-using-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing networking bridges",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-network-bridges-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing VLANs",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-vlans-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing firewall",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing_firewall_using_the_web_console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["network", "interface", "bridge", "vlan", "bond", "team", "port", "mac", "ipv4", "ipv6"]
+ },
+ {
+ "matches": ["firewall", "firewalld", "zone", "tcp", "udp"],
+ "goto": "/network/firewall"
+ }
+ ]
+ }
+ }
+}
diff --git a/dist/networkmanager/networkmanager.css.LEGAL.txt b/dist/networkmanager/networkmanager.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/networkmanager/networkmanager.css.LEGAL.txt
diff --git a/dist/networkmanager/networkmanager.css.gz b/dist/networkmanager/networkmanager.css.gz
new file mode 100644
index 0000000..36781cb
--- /dev/null
+++ b/dist/networkmanager/networkmanager.css.gz
Binary files differ
diff --git a/dist/networkmanager/networkmanager.js.LEGAL.txt b/dist/networkmanager/networkmanager.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/networkmanager/networkmanager.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/networkmanager/networkmanager.js.gz b/dist/networkmanager/networkmanager.js.gz
new file mode 100644
index 0000000..f099c53
--- /dev/null
+++ b/dist/networkmanager/networkmanager.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.cs.js.gz b/dist/networkmanager/po.cs.js.gz
new file mode 100644
index 0000000..2dbca86
--- /dev/null
+++ b/dist/networkmanager/po.cs.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.de.js.gz b/dist/networkmanager/po.de.js.gz
new file mode 100644
index 0000000..8770e8a
--- /dev/null
+++ b/dist/networkmanager/po.de.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.es.js.gz b/dist/networkmanager/po.es.js.gz
new file mode 100644
index 0000000..20c9abd
--- /dev/null
+++ b/dist/networkmanager/po.es.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.fi.js.gz b/dist/networkmanager/po.fi.js.gz
new file mode 100644
index 0000000..235c597
--- /dev/null
+++ b/dist/networkmanager/po.fi.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.fr.js.gz b/dist/networkmanager/po.fr.js.gz
new file mode 100644
index 0000000..8dcb998
--- /dev/null
+++ b/dist/networkmanager/po.fr.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.he.js.gz b/dist/networkmanager/po.he.js.gz
new file mode 100644
index 0000000..6919ac9
--- /dev/null
+++ b/dist/networkmanager/po.he.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.it.js.gz b/dist/networkmanager/po.it.js.gz
new file mode 100644
index 0000000..8ceae4c
--- /dev/null
+++ b/dist/networkmanager/po.it.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.ja.js.gz b/dist/networkmanager/po.ja.js.gz
new file mode 100644
index 0000000..e39bb23
--- /dev/null
+++ b/dist/networkmanager/po.ja.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.ka.js.gz b/dist/networkmanager/po.ka.js.gz
new file mode 100644
index 0000000..7fb9c38
--- /dev/null
+++ b/dist/networkmanager/po.ka.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.ko.js.gz b/dist/networkmanager/po.ko.js.gz
new file mode 100644
index 0000000..2c3c01a
--- /dev/null
+++ b/dist/networkmanager/po.ko.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.cs.js.gz b/dist/networkmanager/po.manifest.cs.js.gz
new file mode 100644
index 0000000..b32c475
--- /dev/null
+++ b/dist/networkmanager/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.de.js.gz b/dist/networkmanager/po.manifest.de.js.gz
new file mode 100644
index 0000000..60dbe39
--- /dev/null
+++ b/dist/networkmanager/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.es.js.gz b/dist/networkmanager/po.manifest.es.js.gz
new file mode 100644
index 0000000..3acae0a
--- /dev/null
+++ b/dist/networkmanager/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.fi.js.gz b/dist/networkmanager/po.manifest.fi.js.gz
new file mode 100644
index 0000000..f3019fd
--- /dev/null
+++ b/dist/networkmanager/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.fr.js.gz b/dist/networkmanager/po.manifest.fr.js.gz
new file mode 100644
index 0000000..988fe40
--- /dev/null
+++ b/dist/networkmanager/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.he.js.gz b/dist/networkmanager/po.manifest.he.js.gz
new file mode 100644
index 0000000..af2b253
--- /dev/null
+++ b/dist/networkmanager/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.it.js.gz b/dist/networkmanager/po.manifest.it.js.gz
new file mode 100644
index 0000000..e6ce931
--- /dev/null
+++ b/dist/networkmanager/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.ja.js.gz b/dist/networkmanager/po.manifest.ja.js.gz
new file mode 100644
index 0000000..ab4ddea
--- /dev/null
+++ b/dist/networkmanager/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.ka.js.gz b/dist/networkmanager/po.manifest.ka.js.gz
new file mode 100644
index 0000000..f8738f1
--- /dev/null
+++ b/dist/networkmanager/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.ko.js.gz b/dist/networkmanager/po.manifest.ko.js.gz
new file mode 100644
index 0000000..16cd179
--- /dev/null
+++ b/dist/networkmanager/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.nb_NO.js.gz b/dist/networkmanager/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..133bba2
--- /dev/null
+++ b/dist/networkmanager/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.nl.js.gz b/dist/networkmanager/po.manifest.nl.js.gz
new file mode 100644
index 0000000..2478537
--- /dev/null
+++ b/dist/networkmanager/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.pl.js.gz b/dist/networkmanager/po.manifest.pl.js.gz
new file mode 100644
index 0000000..8c5a00e
--- /dev/null
+++ b/dist/networkmanager/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.pt_BR.js.gz b/dist/networkmanager/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..92eed12
--- /dev/null
+++ b/dist/networkmanager/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.ru.js.gz b/dist/networkmanager/po.manifest.ru.js.gz
new file mode 100644
index 0000000..0301d3a
--- /dev/null
+++ b/dist/networkmanager/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.sk.js.gz b/dist/networkmanager/po.manifest.sk.js.gz
new file mode 100644
index 0000000..e324b81
--- /dev/null
+++ b/dist/networkmanager/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.sv.js.gz b/dist/networkmanager/po.manifest.sv.js.gz
new file mode 100644
index 0000000..1fe07ad
--- /dev/null
+++ b/dist/networkmanager/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.tr.js.gz b/dist/networkmanager/po.manifest.tr.js.gz
new file mode 100644
index 0000000..11df637
--- /dev/null
+++ b/dist/networkmanager/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.uk.js.gz b/dist/networkmanager/po.manifest.uk.js.gz
new file mode 100644
index 0000000..2283423
--- /dev/null
+++ b/dist/networkmanager/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.manifest.zh_CN.js.gz b/dist/networkmanager/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..c840600
--- /dev/null
+++ b/dist/networkmanager/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.nb_NO.js.gz b/dist/networkmanager/po.nb_NO.js.gz
new file mode 100644
index 0000000..359f30d
--- /dev/null
+++ b/dist/networkmanager/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.nl.js.gz b/dist/networkmanager/po.nl.js.gz
new file mode 100644
index 0000000..dafbb2c
--- /dev/null
+++ b/dist/networkmanager/po.nl.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.pl.js.gz b/dist/networkmanager/po.pl.js.gz
new file mode 100644
index 0000000..a408d50
--- /dev/null
+++ b/dist/networkmanager/po.pl.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.pt_BR.js.gz b/dist/networkmanager/po.pt_BR.js.gz
new file mode 100644
index 0000000..7fd8f1c
--- /dev/null
+++ b/dist/networkmanager/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.ru.js.gz b/dist/networkmanager/po.ru.js.gz
new file mode 100644
index 0000000..3a5c962
--- /dev/null
+++ b/dist/networkmanager/po.ru.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.sk.js.gz b/dist/networkmanager/po.sk.js.gz
new file mode 100644
index 0000000..ef48f6d
--- /dev/null
+++ b/dist/networkmanager/po.sk.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.sv.js.gz b/dist/networkmanager/po.sv.js.gz
new file mode 100644
index 0000000..f59df4f
--- /dev/null
+++ b/dist/networkmanager/po.sv.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.tr.js.gz b/dist/networkmanager/po.tr.js.gz
new file mode 100644
index 0000000..7efe18c
--- /dev/null
+++ b/dist/networkmanager/po.tr.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.uk.js.gz b/dist/networkmanager/po.uk.js.gz
new file mode 100644
index 0000000..d9d861a
--- /dev/null
+++ b/dist/networkmanager/po.uk.js.gz
Binary files differ
diff --git a/dist/networkmanager/po.zh_CN.js.gz b/dist/networkmanager/po.zh_CN.js.gz
new file mode 100644
index 0000000..6ca6548
--- /dev/null
+++ b/dist/networkmanager/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/packagekit/index.html b/dist/packagekit/index.html
new file mode 100644
index 0000000..f4f8a98
--- /dev/null
+++ b/dist/packagekit/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2017 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="packagekit-page">
+
+<head>
+ <title translate="yes">Software updates</title>
+ <meta charset="utf-8" />
+
+ <link href="updates.css" rel="stylesheet" />
+
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="updates.js"></script>
+ <script src="po.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app"></div>
+</body>
+
+</html>
diff --git a/dist/packagekit/manifest.json b/dist/packagekit/manifest.json
new file mode 100644
index 0000000..8decc72
--- /dev/null
+++ b/dist/packagekit/manifest.json
@@ -0,0 +1,29 @@
+{
+ "name": "updates",
+ "priority": 0,
+ "conditions": [
+ {"path-exists": "/lib/systemd/system/packagekit.service"},
+ {"path-not-exists": "/sysroot/ostree"}
+ ],
+
+ "tools": {
+ "index": {
+ "label": "Software updates",
+ "docs": [
+ {
+ "label": "Managing software updates",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-software-updates-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["package", "packagekit", "dnf", "yum", "apt-get", "security"]
+ }
+ ]
+ }
+ },
+
+ "preload": [ "index" ],
+
+ "content-security-policy": "default-src 'self'; img-src 'self' data:; font-src 'self' data:;"
+}
diff --git a/dist/packagekit/po.cs.js.gz b/dist/packagekit/po.cs.js.gz
new file mode 100644
index 0000000..62c435c
--- /dev/null
+++ b/dist/packagekit/po.cs.js.gz
Binary files differ
diff --git a/dist/packagekit/po.de.js.gz b/dist/packagekit/po.de.js.gz
new file mode 100644
index 0000000..a5b346d
--- /dev/null
+++ b/dist/packagekit/po.de.js.gz
Binary files differ
diff --git a/dist/packagekit/po.es.js.gz b/dist/packagekit/po.es.js.gz
new file mode 100644
index 0000000..e64b73b
--- /dev/null
+++ b/dist/packagekit/po.es.js.gz
Binary files differ
diff --git a/dist/packagekit/po.fi.js.gz b/dist/packagekit/po.fi.js.gz
new file mode 100644
index 0000000..9d3acee
--- /dev/null
+++ b/dist/packagekit/po.fi.js.gz
Binary files differ
diff --git a/dist/packagekit/po.fr.js.gz b/dist/packagekit/po.fr.js.gz
new file mode 100644
index 0000000..9e8c162
--- /dev/null
+++ b/dist/packagekit/po.fr.js.gz
Binary files differ
diff --git a/dist/packagekit/po.he.js.gz b/dist/packagekit/po.he.js.gz
new file mode 100644
index 0000000..6478b70
--- /dev/null
+++ b/dist/packagekit/po.he.js.gz
Binary files differ
diff --git a/dist/packagekit/po.it.js.gz b/dist/packagekit/po.it.js.gz
new file mode 100644
index 0000000..141a4cc
--- /dev/null
+++ b/dist/packagekit/po.it.js.gz
Binary files differ
diff --git a/dist/packagekit/po.ja.js.gz b/dist/packagekit/po.ja.js.gz
new file mode 100644
index 0000000..9dd9711
--- /dev/null
+++ b/dist/packagekit/po.ja.js.gz
Binary files differ
diff --git a/dist/packagekit/po.ka.js.gz b/dist/packagekit/po.ka.js.gz
new file mode 100644
index 0000000..f2f7715
--- /dev/null
+++ b/dist/packagekit/po.ka.js.gz
Binary files differ
diff --git a/dist/packagekit/po.ko.js.gz b/dist/packagekit/po.ko.js.gz
new file mode 100644
index 0000000..31f3bc5
--- /dev/null
+++ b/dist/packagekit/po.ko.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.cs.js.gz b/dist/packagekit/po.manifest.cs.js.gz
new file mode 100644
index 0000000..11ecbb3
--- /dev/null
+++ b/dist/packagekit/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.de.js.gz b/dist/packagekit/po.manifest.de.js.gz
new file mode 100644
index 0000000..335fe9a
--- /dev/null
+++ b/dist/packagekit/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.es.js.gz b/dist/packagekit/po.manifest.es.js.gz
new file mode 100644
index 0000000..12e6744
--- /dev/null
+++ b/dist/packagekit/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.fi.js.gz b/dist/packagekit/po.manifest.fi.js.gz
new file mode 100644
index 0000000..582dd12
--- /dev/null
+++ b/dist/packagekit/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.fr.js.gz b/dist/packagekit/po.manifest.fr.js.gz
new file mode 100644
index 0000000..70a2e49
--- /dev/null
+++ b/dist/packagekit/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.he.js.gz b/dist/packagekit/po.manifest.he.js.gz
new file mode 100644
index 0000000..8dbc9e1
--- /dev/null
+++ b/dist/packagekit/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.it.js.gz b/dist/packagekit/po.manifest.it.js.gz
new file mode 100644
index 0000000..f9df62f
--- /dev/null
+++ b/dist/packagekit/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.ja.js.gz b/dist/packagekit/po.manifest.ja.js.gz
new file mode 100644
index 0000000..1cac945
--- /dev/null
+++ b/dist/packagekit/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.ka.js.gz b/dist/packagekit/po.manifest.ka.js.gz
new file mode 100644
index 0000000..0b53695
--- /dev/null
+++ b/dist/packagekit/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.ko.js.gz b/dist/packagekit/po.manifest.ko.js.gz
new file mode 100644
index 0000000..0099384
--- /dev/null
+++ b/dist/packagekit/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.nb_NO.js.gz b/dist/packagekit/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..4b547f9
--- /dev/null
+++ b/dist/packagekit/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.nl.js.gz b/dist/packagekit/po.manifest.nl.js.gz
new file mode 100644
index 0000000..b364eaa
--- /dev/null
+++ b/dist/packagekit/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.pl.js.gz b/dist/packagekit/po.manifest.pl.js.gz
new file mode 100644
index 0000000..b375bdb
--- /dev/null
+++ b/dist/packagekit/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.pt_BR.js.gz b/dist/packagekit/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..06ff5d0
--- /dev/null
+++ b/dist/packagekit/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.ru.js.gz b/dist/packagekit/po.manifest.ru.js.gz
new file mode 100644
index 0000000..4c99b33
--- /dev/null
+++ b/dist/packagekit/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.sk.js.gz b/dist/packagekit/po.manifest.sk.js.gz
new file mode 100644
index 0000000..2979b3c
--- /dev/null
+++ b/dist/packagekit/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.sv.js.gz b/dist/packagekit/po.manifest.sv.js.gz
new file mode 100644
index 0000000..29b0a94
--- /dev/null
+++ b/dist/packagekit/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.tr.js.gz b/dist/packagekit/po.manifest.tr.js.gz
new file mode 100644
index 0000000..63bb22b
--- /dev/null
+++ b/dist/packagekit/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.uk.js.gz b/dist/packagekit/po.manifest.uk.js.gz
new file mode 100644
index 0000000..44c55dc
--- /dev/null
+++ b/dist/packagekit/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/packagekit/po.manifest.zh_CN.js.gz b/dist/packagekit/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..8aa4a93
--- /dev/null
+++ b/dist/packagekit/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/packagekit/po.nb_NO.js.gz b/dist/packagekit/po.nb_NO.js.gz
new file mode 100644
index 0000000..2203556
--- /dev/null
+++ b/dist/packagekit/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/packagekit/po.nl.js.gz b/dist/packagekit/po.nl.js.gz
new file mode 100644
index 0000000..b4cc9f8
--- /dev/null
+++ b/dist/packagekit/po.nl.js.gz
Binary files differ
diff --git a/dist/packagekit/po.pl.js.gz b/dist/packagekit/po.pl.js.gz
new file mode 100644
index 0000000..f709ab5
--- /dev/null
+++ b/dist/packagekit/po.pl.js.gz
Binary files differ
diff --git a/dist/packagekit/po.pt_BR.js.gz b/dist/packagekit/po.pt_BR.js.gz
new file mode 100644
index 0000000..db68ae5
--- /dev/null
+++ b/dist/packagekit/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/packagekit/po.ru.js.gz b/dist/packagekit/po.ru.js.gz
new file mode 100644
index 0000000..2b38e1f
--- /dev/null
+++ b/dist/packagekit/po.ru.js.gz
Binary files differ
diff --git a/dist/packagekit/po.sk.js.gz b/dist/packagekit/po.sk.js.gz
new file mode 100644
index 0000000..a61f431
--- /dev/null
+++ b/dist/packagekit/po.sk.js.gz
Binary files differ
diff --git a/dist/packagekit/po.sv.js.gz b/dist/packagekit/po.sv.js.gz
new file mode 100644
index 0000000..4fa3381
--- /dev/null
+++ b/dist/packagekit/po.sv.js.gz
Binary files differ
diff --git a/dist/packagekit/po.tr.js.gz b/dist/packagekit/po.tr.js.gz
new file mode 100644
index 0000000..aa30d85
--- /dev/null
+++ b/dist/packagekit/po.tr.js.gz
Binary files differ
diff --git a/dist/packagekit/po.uk.js.gz b/dist/packagekit/po.uk.js.gz
new file mode 100644
index 0000000..34d5be3
--- /dev/null
+++ b/dist/packagekit/po.uk.js.gz
Binary files differ
diff --git a/dist/packagekit/po.zh_CN.js.gz b/dist/packagekit/po.zh_CN.js.gz
new file mode 100644
index 0000000..a1927a7
--- /dev/null
+++ b/dist/packagekit/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/packagekit/updates.css.LEGAL.txt b/dist/packagekit/updates.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/packagekit/updates.css.LEGAL.txt
diff --git a/dist/packagekit/updates.css.gz b/dist/packagekit/updates.css.gz
new file mode 100644
index 0000000..91a0373
--- /dev/null
+++ b/dist/packagekit/updates.css.gz
Binary files differ
diff --git a/dist/packagekit/updates.js.LEGAL.txt b/dist/packagekit/updates.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/packagekit/updates.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/packagekit/updates.js.gz b/dist/packagekit/updates.js.gz
new file mode 100644
index 0000000..324d98c
--- /dev/null
+++ b/dist/packagekit/updates.js.gz
Binary files differ
diff --git a/dist/playground/exception.css.LEGAL.txt b/dist/playground/exception.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/exception.css.LEGAL.txt
diff --git a/dist/playground/exception.css.gz b/dist/playground/exception.css.gz
new file mode 100644
index 0000000..bbc5834
--- /dev/null
+++ b/dist/playground/exception.css.gz
Binary files differ
diff --git a/dist/playground/exception.html b/dist/playground/exception.html
new file mode 100644
index 0000000..00f8dc0
--- /dev/null
+++ b/dist/playground/exception.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Javascript exceptions</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div id="internal" class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h2>Exception</h2>
+
+ <p>Clicking this button should make a javascript exception happen.</p>
+
+ <button id="exception">Exception</button>
+ </section>
+ </main>
+ </div>
+ <script src="exception.js"></script>
+</body>
+</html>
diff --git a/dist/playground/exception.js.LEGAL.txt b/dist/playground/exception.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/exception.js.LEGAL.txt
diff --git a/dist/playground/exception.js.gz b/dist/playground/exception.js.gz
new file mode 100644
index 0000000..65bfef0
--- /dev/null
+++ b/dist/playground/exception.js.gz
Binary files differ
diff --git a/dist/playground/hammer.gif b/dist/playground/hammer.gif
new file mode 100644
index 0000000..5588eb8
--- /dev/null
+++ b/dist/playground/hammer.gif
Binary files differ
diff --git a/dist/playground/index.css.LEGAL.txt b/dist/playground/index.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/index.css.LEGAL.txt
diff --git a/dist/playground/index.css.gz b/dist/playground/index.css.gz
new file mode 100644
index 0000000..452dcf3
--- /dev/null
+++ b/dist/playground/index.css.gz
Binary files differ
diff --git a/dist/playground/index.html b/dist/playground/index.html
new file mode 100644
index 0000000..81672d1
--- /dev/null
+++ b/dist/playground/index.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html id="playground-page">
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Development Playground</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="index.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../manifests.js"></script>
+ <script src="index.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h2>Development Playground</h2>
+ <ul id="nav"></ul>
+ </section>
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h2>Page Status</h2>
+ <label>Type <input id="type" /></label>
+ <label>Title <input id="title" /></label>
+ <button id="set-status" class="pf-v5-c-button pf-m-primary" type="button">Set</button>
+ <button id="clear-status" class="pf-v5-c-button pf-m-secondary" type="button">Clear</button>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/dist/playground/index.js.LEGAL.txt b/dist/playground/index.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/index.js.LEGAL.txt
diff --git a/dist/playground/index.js.gz b/dist/playground/index.js.gz
new file mode 100644
index 0000000..5d0365d
--- /dev/null
+++ b/dist/playground/index.js.gz
Binary files differ
diff --git a/dist/playground/journal.css.LEGAL.txt b/dist/playground/journal.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/journal.css.LEGAL.txt
diff --git a/dist/playground/journal.css.gz b/dist/playground/journal.css.gz
new file mode 100644
index 0000000..5676bce
--- /dev/null
+++ b/dist/playground/journal.css.gz
Binary files differ
diff --git a/dist/playground/journal.html b/dist/playground/journal.html
new file mode 100644
index 0000000..56c34f7
--- /dev/null
+++ b/dist/playground/journal.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Journal Box</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="journal.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="journal.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <div id="journal-box"></div>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/dist/playground/journal.js.LEGAL.txt b/dist/playground/journal.js.LEGAL.txt
new file mode 100644
index 0000000..090762e
--- /dev/null
+++ b/dist/playground/journal.js.LEGAL.txt
@@ -0,0 +1,34 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
diff --git a/dist/playground/journal.js.gz b/dist/playground/journal.js.gz
new file mode 100644
index 0000000..00d71f4
--- /dev/null
+++ b/dist/playground/journal.js.gz
Binary files differ
diff --git a/dist/playground/manifest.json b/dist/playground/manifest.json
new file mode 100644
index 0000000..fadfbe9
--- /dev/null
+++ b/dist/playground/manifest.json
@@ -0,0 +1,48 @@
+{
+ "tools": {
+ "index": {
+ "label": "Development"
+ }
+ },
+
+ "playground": {
+ "react-patterns": {
+ "label": "React Patterns"
+ },
+ "translate": {
+ "label": "Translating"
+ },
+ "exception": {
+ "label": "Exceptions"
+ },
+ "pkgs": {
+ "label": "Packages"
+ },
+ "preloaded": {
+ "label": "Preloaded"
+ },
+ "notifications-receiver": {
+ "label": "Notifications Receiver"
+ },
+ "metrics": {
+ "label": "Monitoring"
+ },
+ "plot": {
+ "label": "Plots"
+ },
+ "service": {
+ "label": "Generic Service Monitor"
+ },
+ "speed": {
+ "label": "Speed Tests"
+ },
+ "test": {
+ "label": "Playground"
+ },
+ "journal": {
+ "label": "Logs Box"
+ }
+ },
+ "preload": [ "preloaded" ],
+ "content-security-policy": "img-src 'self' data:"
+}
diff --git a/dist/playground/metrics.css.LEGAL.txt b/dist/playground/metrics.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/metrics.css.LEGAL.txt
diff --git a/dist/playground/metrics.css.gz b/dist/playground/metrics.css.gz
new file mode 100644
index 0000000..bbc5834
--- /dev/null
+++ b/dist/playground/metrics.css.gz
Binary files differ
diff --git a/dist/playground/metrics.html b/dist/playground/metrics.html
new file mode 100644
index 0000000..2554d48
--- /dev/null
+++ b/dist/playground/metrics.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Monitoring</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="metrics.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="metrics.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>Monitoring</h1>
+ <button id="reload">Reload</button>
+ <div id="results"></div>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/dist/playground/metrics.js.LEGAL.txt b/dist/playground/metrics.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/metrics.js.LEGAL.txt
diff --git a/dist/playground/metrics.js.gz b/dist/playground/metrics.js.gz
new file mode 100644
index 0000000..97dbf42
--- /dev/null
+++ b/dist/playground/metrics.js.gz
Binary files differ
diff --git a/dist/playground/notifications-receiver.html b/dist/playground/notifications-receiver.html
new file mode 100644
index 0000000..e005e7e
--- /dev/null
+++ b/dist/playground/notifications-receiver.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Notifications Receiver</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="notifications-receiver.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <h1>Notifications Receiver</h1>
+
+ <span id="received-type"></span> / <span id="received-title"></span>
+
+</body>
+
+</html>
diff --git a/dist/playground/notifications-receiver.js.LEGAL.txt b/dist/playground/notifications-receiver.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/notifications-receiver.js.LEGAL.txt
diff --git a/dist/playground/notifications-receiver.js.gz b/dist/playground/notifications-receiver.js.gz
new file mode 100644
index 0000000..afd5750
--- /dev/null
+++ b/dist/playground/notifications-receiver.js.gz
Binary files differ
diff --git a/dist/playground/pkgs.css.LEGAL.txt b/dist/playground/pkgs.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/pkgs.css.LEGAL.txt
diff --git a/dist/playground/pkgs.css.gz b/dist/playground/pkgs.css.gz
new file mode 100644
index 0000000..bbc5834
--- /dev/null
+++ b/dist/playground/pkgs.css.gz
Binary files differ
diff --git a/dist/playground/pkgs.html b/dist/playground/pkgs.html
new file mode 100644
index 0000000..b70d2a7
--- /dev/null
+++ b/dist/playground/pkgs.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Packages</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="pkgs.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <button id="reload">Reload</button>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/dist/playground/pkgs.js.LEGAL.txt b/dist/playground/pkgs.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/pkgs.js.LEGAL.txt
diff --git a/dist/playground/pkgs.js.gz b/dist/playground/pkgs.js.gz
new file mode 100644
index 0000000..410ed10
--- /dev/null
+++ b/dist/playground/pkgs.js.gz
Binary files differ
diff --git a/dist/playground/plot.css.LEGAL.txt b/dist/playground/plot.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/plot.css.LEGAL.txt
diff --git a/dist/playground/plot.css.gz b/dist/playground/plot.css.gz
new file mode 100644
index 0000000..9d897f6
--- /dev/null
+++ b/dist/playground/plot.css.gz
Binary files differ
diff --git a/dist/playground/plot.html b/dist/playground/plot.html
new file mode 100644
index 0000000..4583551
--- /dev/null
+++ b/dist/playground/plot.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Plots</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="plot.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="plot.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div id="plot-direct"></div>
+ <div id="plot-pmcd"></div>
+</body>
+</html>
diff --git a/dist/playground/plot.js.LEGAL.txt b/dist/playground/plot.js.LEGAL.txt
new file mode 100644
index 0000000..090762e
--- /dev/null
+++ b/dist/playground/plot.js.LEGAL.txt
@@ -0,0 +1,34 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
diff --git a/dist/playground/plot.js.gz b/dist/playground/plot.js.gz
new file mode 100644
index 0000000..3a49c23
--- /dev/null
+++ b/dist/playground/plot.js.gz
Binary files differ
diff --git a/dist/playground/po.cs.js.gz b/dist/playground/po.cs.js.gz
new file mode 100644
index 0000000..f67983d
--- /dev/null
+++ b/dist/playground/po.cs.js.gz
Binary files differ
diff --git a/dist/playground/po.de.js.gz b/dist/playground/po.de.js.gz
new file mode 100644
index 0000000..024d740
--- /dev/null
+++ b/dist/playground/po.de.js.gz
Binary files differ
diff --git a/dist/playground/po.es.js.gz b/dist/playground/po.es.js.gz
new file mode 100644
index 0000000..d38f66c
--- /dev/null
+++ b/dist/playground/po.es.js.gz
Binary files differ
diff --git a/dist/playground/po.fi.js.gz b/dist/playground/po.fi.js.gz
new file mode 100644
index 0000000..f6b3466
--- /dev/null
+++ b/dist/playground/po.fi.js.gz
Binary files differ
diff --git a/dist/playground/po.fr.js.gz b/dist/playground/po.fr.js.gz
new file mode 100644
index 0000000..2fbd4a4
--- /dev/null
+++ b/dist/playground/po.fr.js.gz
Binary files differ
diff --git a/dist/playground/po.he.js.gz b/dist/playground/po.he.js.gz
new file mode 100644
index 0000000..16eff10
--- /dev/null
+++ b/dist/playground/po.he.js.gz
Binary files differ
diff --git a/dist/playground/po.it.js.gz b/dist/playground/po.it.js.gz
new file mode 100644
index 0000000..b0e4f6d
--- /dev/null
+++ b/dist/playground/po.it.js.gz
Binary files differ
diff --git a/dist/playground/po.ja.js.gz b/dist/playground/po.ja.js.gz
new file mode 100644
index 0000000..717ec7e
--- /dev/null
+++ b/dist/playground/po.ja.js.gz
Binary files differ
diff --git a/dist/playground/po.ka.js.gz b/dist/playground/po.ka.js.gz
new file mode 100644
index 0000000..ae65490
--- /dev/null
+++ b/dist/playground/po.ka.js.gz
Binary files differ
diff --git a/dist/playground/po.ko.js.gz b/dist/playground/po.ko.js.gz
new file mode 100644
index 0000000..cc6eff4
--- /dev/null
+++ b/dist/playground/po.ko.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.cs.js.gz b/dist/playground/po.manifest.cs.js.gz
new file mode 100644
index 0000000..747b5b1
--- /dev/null
+++ b/dist/playground/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.de.js.gz b/dist/playground/po.manifest.de.js.gz
new file mode 100644
index 0000000..5d345e7
--- /dev/null
+++ b/dist/playground/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.es.js.gz b/dist/playground/po.manifest.es.js.gz
new file mode 100644
index 0000000..885d8ac
--- /dev/null
+++ b/dist/playground/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.fi.js.gz b/dist/playground/po.manifest.fi.js.gz
new file mode 100644
index 0000000..c566ce9
--- /dev/null
+++ b/dist/playground/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.fr.js.gz b/dist/playground/po.manifest.fr.js.gz
new file mode 100644
index 0000000..74dec19
--- /dev/null
+++ b/dist/playground/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.he.js.gz b/dist/playground/po.manifest.he.js.gz
new file mode 100644
index 0000000..3c1faa6
--- /dev/null
+++ b/dist/playground/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.it.js.gz b/dist/playground/po.manifest.it.js.gz
new file mode 100644
index 0000000..683bd33
--- /dev/null
+++ b/dist/playground/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.ja.js.gz b/dist/playground/po.manifest.ja.js.gz
new file mode 100644
index 0000000..b175173
--- /dev/null
+++ b/dist/playground/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.ka.js.gz b/dist/playground/po.manifest.ka.js.gz
new file mode 100644
index 0000000..6b083a5
--- /dev/null
+++ b/dist/playground/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.ko.js.gz b/dist/playground/po.manifest.ko.js.gz
new file mode 100644
index 0000000..c253df9
--- /dev/null
+++ b/dist/playground/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.nb_NO.js.gz b/dist/playground/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..5a814ed
--- /dev/null
+++ b/dist/playground/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.nl.js.gz b/dist/playground/po.manifest.nl.js.gz
new file mode 100644
index 0000000..3c4aeb5
--- /dev/null
+++ b/dist/playground/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.pl.js.gz b/dist/playground/po.manifest.pl.js.gz
new file mode 100644
index 0000000..76f8f11
--- /dev/null
+++ b/dist/playground/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.pt_BR.js.gz b/dist/playground/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..2e51ac4
--- /dev/null
+++ b/dist/playground/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.ru.js.gz b/dist/playground/po.manifest.ru.js.gz
new file mode 100644
index 0000000..d88418d
--- /dev/null
+++ b/dist/playground/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.sk.js.gz b/dist/playground/po.manifest.sk.js.gz
new file mode 100644
index 0000000..2ab8a5d
--- /dev/null
+++ b/dist/playground/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.sv.js.gz b/dist/playground/po.manifest.sv.js.gz
new file mode 100644
index 0000000..2501e02
--- /dev/null
+++ b/dist/playground/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.tr.js.gz b/dist/playground/po.manifest.tr.js.gz
new file mode 100644
index 0000000..db8c51e
--- /dev/null
+++ b/dist/playground/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.uk.js.gz b/dist/playground/po.manifest.uk.js.gz
new file mode 100644
index 0000000..29e0434
--- /dev/null
+++ b/dist/playground/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/playground/po.manifest.zh_CN.js.gz b/dist/playground/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..e4b3c21
--- /dev/null
+++ b/dist/playground/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/playground/po.nb_NO.js.gz b/dist/playground/po.nb_NO.js.gz
new file mode 100644
index 0000000..ec42868
--- /dev/null
+++ b/dist/playground/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/playground/po.nl.js.gz b/dist/playground/po.nl.js.gz
new file mode 100644
index 0000000..8b532e6
--- /dev/null
+++ b/dist/playground/po.nl.js.gz
Binary files differ
diff --git a/dist/playground/po.pl.js.gz b/dist/playground/po.pl.js.gz
new file mode 100644
index 0000000..ecdce5f
--- /dev/null
+++ b/dist/playground/po.pl.js.gz
Binary files differ
diff --git a/dist/playground/po.pt_BR.js.gz b/dist/playground/po.pt_BR.js.gz
new file mode 100644
index 0000000..2cc6a5a
--- /dev/null
+++ b/dist/playground/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/playground/po.ru.js.gz b/dist/playground/po.ru.js.gz
new file mode 100644
index 0000000..7b5183a
--- /dev/null
+++ b/dist/playground/po.ru.js.gz
Binary files differ
diff --git a/dist/playground/po.sk.js.gz b/dist/playground/po.sk.js.gz
new file mode 100644
index 0000000..0c328de
--- /dev/null
+++ b/dist/playground/po.sk.js.gz
Binary files differ
diff --git a/dist/playground/po.sv.js.gz b/dist/playground/po.sv.js.gz
new file mode 100644
index 0000000..a84aef7
--- /dev/null
+++ b/dist/playground/po.sv.js.gz
Binary files differ
diff --git a/dist/playground/po.tr.js.gz b/dist/playground/po.tr.js.gz
new file mode 100644
index 0000000..30bf958
--- /dev/null
+++ b/dist/playground/po.tr.js.gz
Binary files differ
diff --git a/dist/playground/po.uk.js.gz b/dist/playground/po.uk.js.gz
new file mode 100644
index 0000000..336e5c0
--- /dev/null
+++ b/dist/playground/po.uk.js.gz
Binary files differ
diff --git a/dist/playground/po.zh_CN.js.gz b/dist/playground/po.zh_CN.js.gz
new file mode 100644
index 0000000..bfefcf0
--- /dev/null
+++ b/dist/playground/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/playground/preloaded.html b/dist/playground/preloaded.html
new file mode 100644
index 0000000..8526b65
--- /dev/null
+++ b/dist/playground/preloaded.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Preloaded Page</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="preloaded.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <h1>Preloaded</h1>
+
+ <div id="path"></div>
+ <pre id="host"></pre>
+ <pre id="release"></pre>
+
+</body>
+
+</html>
diff --git a/dist/playground/preloaded.js.LEGAL.txt b/dist/playground/preloaded.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/preloaded.js.LEGAL.txt
diff --git a/dist/playground/preloaded.js.gz b/dist/playground/preloaded.js.gz
new file mode 100644
index 0000000..232b915
--- /dev/null
+++ b/dist/playground/preloaded.js.gz
Binary files differ
diff --git a/dist/playground/react-patterns.css.LEGAL.txt b/dist/playground/react-patterns.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/react-patterns.css.LEGAL.txt
diff --git a/dist/playground/react-patterns.css.gz b/dist/playground/react-patterns.css.gz
new file mode 100644
index 0000000..ca5547f
--- /dev/null
+++ b/dist/playground/react-patterns.css.gz
Binary files differ
diff --git a/dist/playground/react-patterns.html b/dist/playground/react-patterns.html
new file mode 100644
index 0000000..6b0ac1e
--- /dev/null
+++ b/dist/playground/react-patterns.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit React Patterns Usage</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="react-patterns.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="react-patterns.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h3>Select file</h3>
+ <div id="demo-file-ac"></div>
+ <div id="demo-file-ac-preselected"></div>
+ </section>
+
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h3>Dialogs</h3>
+ <button id="demo-show-dialog" class="pf-v5-c-button pf-m-secondary">Show Dialog</button>
+ <button id="demo-show-error-dialog" class="pf-v5-c-button pf-m-secondary">Show Error-Dialog</button>
+ <div id="demo-dialog-result"></div>
+ </section>
+
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h3>Cards</h3>
+ <div id="demo-cards"></div>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/dist/playground/react-patterns.js.LEGAL.txt b/dist/playground/react-patterns.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/playground/react-patterns.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/playground/react-patterns.js.gz b/dist/playground/react-patterns.js.gz
new file mode 100644
index 0000000..01a1b54
--- /dev/null
+++ b/dist/playground/react-patterns.js.gz
Binary files differ
diff --git a/dist/playground/service.css.LEGAL.txt b/dist/playground/service.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/service.css.LEGAL.txt
diff --git a/dist/playground/service.css.gz b/dist/playground/service.css.gz
new file mode 100644
index 0000000..bbc5834
--- /dev/null
+++ b/dist/playground/service.css.gz
Binary files differ
diff --git a/dist/playground/service.html b/dist/playground/service.html
new file mode 100644
index 0000000..a1cf397
--- /dev/null
+++ b/dist/playground/service.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Generic Service Monitor</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="service.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <table>
+ <tbody>
+ <tr><td>Exists</td><td id="exists"></td></tr>
+ <tr><td>State</td><td id="state"></td></tr>
+ <tr><td>Enabled</td><td id="enabled"></td></tr>
+ </tbody>
+ </table>
+
+ <button id="start">Start</button>
+ <button id="stop">Stop</button>
+ <button id="enable">Enable</button>
+ <button id="disable">Disable</button>
+</body>
+</html>
diff --git a/dist/playground/service.js.LEGAL.txt b/dist/playground/service.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/service.js.LEGAL.txt
diff --git a/dist/playground/service.js.gz b/dist/playground/service.js.gz
new file mode 100644
index 0000000..6807dc6
--- /dev/null
+++ b/dist/playground/service.js.gz
Binary files differ
diff --git a/dist/playground/speed.css.LEGAL.txt b/dist/playground/speed.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/speed.css.LEGAL.txt
diff --git a/dist/playground/speed.css.gz b/dist/playground/speed.css.gz
new file mode 100644
index 0000000..b8f5051
--- /dev/null
+++ b/dist/playground/speed.css.gz
Binary files differ
diff --git a/dist/playground/speed.html b/dist/playground/speed.html
new file mode 100644
index 0000000..2238460
--- /dev/null
+++ b/dist/playground/speed.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Speed Tests</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="speed.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="speed.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>Speed Tests</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label">Speed: </label></td>
+ <td><label id="speed"></label></td>
+ </tr>
+ <tr>
+ <td><label class="control-label">Process ID: </label></td>
+ <td><label id="pid"></label></td>
+ </tr>
+ <tr>
+ <td><label class="control-label">Bridge Memory: </label></td>
+ <td><label id="memory"></label></td>
+ </tr>
+ </tbody>
+ </table>
+ <p><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="stop">Stop</button></p>
+
+ <h2>Echo Tests</h2>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label" for="message">Message</label></td>
+ <td><input class="form-control" id="message" value="100000" /></td>
+ </tr>
+ <tr>
+ <td><label class="control-label" for="batch">Batch</label></td>
+ <td><input class="form-control" id="batch" value="1" /></td>
+ </tr>
+ <tr>
+ <td><label class="control-label" for="interval">Interval</label></td>
+ <td><input class="form-control" id="interval" value="100" /></td>
+ </tr>
+ <tr>
+ <td><label class="control-label" for="binary">Binary</label></td>
+ <td><input type="checkbox" id="binary" /></td>
+ </tr>
+ <tr>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="echo-normal">Normal Channel</button></td>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="echo-sideband">Sideband Channel</button></td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>Read Test</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label" for="message">Path</label></td>
+ <td><input class="form-control" id="read-path" value="/dev/sda" /></td>
+ </tr>
+ <tr>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="read-normal">Normal Channel</button></td>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="read-sideband">Sideband Channel</button></td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>Download Test</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label" for="message">Path</label></td>
+ <td><input class="form-control" id="download-path" value="/dev/sda" /></td>
+ </tr>
+ <tr>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="download-external">External Download</button></td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>spawn test</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label" for="spawn">command</label></td>
+ <td><input class="form-control" id="spawn-command" value="seq 1000000" /></td>
+ </tr>
+ <tr>
+ <td><label class="control-label" for="spawn-result">result</label></td>
+ <td><label class="form-control" id="spawn-result"></label></td>
+ </tr>
+ <tr>
+ <td><label class="control-label" for="spawn-output">output block</label></td>
+ <td><pre id="spawn-output"></pre></td>
+ </tr>
+ <tr>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="spawn">spawn command</button></td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>spawn input test</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label" for="spawn-input-result">result</label></td>
+ <td><label class="form-control" id="spawn-input-result"></label></td>
+ </tr>
+ <tr>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="spawn-input">spawn command</button></td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/dist/playground/speed.js.LEGAL.txt b/dist/playground/speed.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/speed.js.LEGAL.txt
diff --git a/dist/playground/speed.js.gz b/dist/playground/speed.js.gz
new file mode 100644
index 0000000..5b48de8
--- /dev/null
+++ b/dist/playground/speed.js.gz
Binary files differ
diff --git a/dist/playground/test.css.LEGAL.txt b/dist/playground/test.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/test.css.LEGAL.txt
diff --git a/dist/playground/test.css.gz b/dist/playground/test.css.gz
new file mode 100644
index 0000000..c892a79
--- /dev/null
+++ b/dist/playground/test.css.gz
Binary files differ
diff --git a/dist/playground/test.html b/dist/playground/test.html
new file mode 100644
index 0000000..478bd8e
--- /dev/null
+++ b/dist/playground/test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit playground</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="test.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="test.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section id="internal" class="pf-v5-c-page__main-section pf-m-light">
+ <img src="hammer.gif" id="hammer" />
+ <div id="nav"></div>
+ <button class="pf-v5-c-button pf-m-secondary" id="go-down">Go down</button>
+ <br/>
+ <br/>
+ <div class="cockpit-internal-reauthorize">
+ <button class="pf-v5-c-button pf-m-secondary">Privileged Action</button>
+ <span></span>
+ </div>
+ <div class="super-channel">
+ <button class="pf-v5-c-button pf-m-secondary">Superuser</button>
+ <span></span>
+ </div>
+ <div class="lock-channel">
+ <button class="pf-v5-c-button pf-m-secondary">Lock /tmp/playground-test-lock</button>
+ <span></span>
+ </div>
+ <button class="pf-v5-c-button pf-m-secondary" id="modify-file">Increment /tmp/counter atomically</button>
+ <div id="file-content"></div>
+ <div id="file-error"></div>
+ <br/>
+ <div>
+ <button class="pf-v5-c-button pf-m-secondary" id="load-file">Load /tmp/counter</button>
+ <button class="pf-v5-c-button pf-m-secondary" id="save-file">Overwrite /tmp/counter</button>
+ <button class="pf-v5-c-button pf-m-secondary" id="delete-file">Delete /tmp/counter</button>
+ </div>
+ <textarea id="edit-file"></textarea>
+ <br/>
+ Visibility: <label id="hidden"></label>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/dist/playground/test.js.LEGAL.txt b/dist/playground/test.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/test.js.LEGAL.txt
diff --git a/dist/playground/test.js.gz b/dist/playground/test.js.gz
new file mode 100644
index 0000000..59c0389
--- /dev/null
+++ b/dist/playground/test.js.gz
Binary files differ
diff --git a/dist/playground/translate.css.LEGAL.txt b/dist/playground/translate.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/translate.css.LEGAL.txt
diff --git a/dist/playground/translate.css.gz b/dist/playground/translate.css.gz
new file mode 100644
index 0000000..bbc5834
--- /dev/null
+++ b/dist/playground/translate.css.gz
Binary files differ
diff --git a/dist/playground/translate.html b/dist/playground/translate.html
new file mode 100644
index 0000000..bf2bc64
--- /dev/null
+++ b/dist/playground/translate.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit playground</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section id="internal" class="pf-v5-c-page__main-section pf-m-light">
+ <h2>HTML translations</h2>
+
+ <p>For translating HTML use these forms:</p>
+
+ <p>
+ <code>&lt;span translate&gt;Ready&lt;/span&gt;</code>
+ = <span translate="yes" id="translate-html">Ready</span>
+ </p>
+
+ <p>
+ <code>&lt;span translate translate-context="verb"&gt;Ready&lt;/span&gt;</code>
+ = <span translate="yes" translate-context="verb" id="translate-html-context">Ready</span>
+ </p>
+
+ <p>
+ <code>&lt;span translate="yes"&gt;Not ready&lt;/span&gt;</code>
+ = <span translate="yes" id="translate-html-yes">Not ready</span>
+ </p>
+
+ <p>
+ <code>&lt;span translate="title" title="Unavailable"&gt;Cancel&lt;/span&gt;</code>
+ = <span translate="title" title="Unavailable" id="translate-html-title">Cancel</span>
+ </p>
+
+ <p>
+ <code>&lt;span translate="yes title" title="Unavailable"&gt;Cancel&lt;/span&gt;</code>
+ = <span translate="yes title" title="Unavailable" id="translate-html-yes-title">Cancel</span>
+ </p>
+
+ <p>Note that we do <b>not support</b>:</p>
+ <ul>
+ <li>Interpolation of variables.</li>
+ <li>Pluralization</li>
+ <li>The <code>&lt;translate&gt;</code> element</li>
+ </ul>
+
+ <hr />
+
+ <h2>Old Glade style</h2>
+
+ <p>The old Glade style is not recommended:</p>
+
+ <p>
+ <code>&lt;span translate="yes"&gt;Empty&lt;/span&gt;</code>
+ = <span translate="yes" id="translatable-glade">Empty</span>
+ </p>
+
+ <p>
+ <code>&lt;span translate="yes" context="verb"&gt;Empty&lt;/span&gt;</code>
+ = <span translate="yes" context="verb" id="translatable-glade-context">Empty</span>
+ </p>
+
+ <p>Note that we do <b>not support</b>:</p>
+ <ul>
+ <li>Interpolation of variables.</li>
+ <li>Translatable attributes.</li>
+ <li>Pluralization</li>
+ <li>The <code>&lt;translate&gt;</code> element</li>
+ </ul>
+
+ <hr />
+
+ <h2>Code Translations</h2>
+
+ <p>For translating in javascript code, use these forms:</p>
+
+ <p>
+ <code>_("Empty")</code>
+ = <span id="underscore-empty"></span>
+ </p>
+
+ <p>
+ <code>_("verb", "Empty")</code>
+ = <span id="underscore-context-empty"></span>
+ </p>
+
+ <p>
+ <code>C_("verb", "Empty")</code>
+ = <span id="cunderscore-context-empty"></span>
+ </p>
+
+ <p>
+ <code>cockpit.gettext("Control")</code>
+ = <span id="gettext-control"></span>
+ </p>
+
+ <p>
+ <code>cockpit.gettext("key", "Control")</code>
+ = <span id="gettext-context-control"></span>
+ </p>
+
+ <p>
+ <code>cockpit.ngettext("$0 disk is missing", "$0 disks are missing", 1)</code>
+ = <span id="ngettext-disks-1"></span>
+ </p>
+
+ <p>
+ <code>cockpit.ngettext("$0 disk is missing", "$0 disks are missing", 2)</code>
+ = <span id="ngettext-disks-2"></span>
+ </p>
+
+ <p>
+ <code>cockpit.ngettext("disk-non-rotational", "$0 disk is missing", "$0 disks are missing", 1)</code>
+ = <span id="ngettext-context-disks-1"></span>
+ </p>
+
+ <p>
+ <code>cockpit.ngettext("disk-non-rotational", "$0 disk is missing", "$0 disks are missing", 2)</code>
+ = <span id="ngettext-context-disks-2"></span>
+ </p>
+
+ <p>Note that we do <b>not support</b>:</p>
+ <ul>
+ <li>Extraction of single quoted strings.</li>
+ </ul>
+ </section>
+ </main>
+ </div>
+ <script src="translate.js"></script>
+ <!-- Bring in initial translations -->
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+ <!-- Override translations from here -->
+ <script src="po.extra.js"></script>
+</body>
+</html>
diff --git a/dist/playground/translate.js.LEGAL.txt b/dist/playground/translate.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/playground/translate.js.LEGAL.txt
diff --git a/dist/playground/translate.js.gz b/dist/playground/translate.js.gz
new file mode 100644
index 0000000..f5152db
--- /dev/null
+++ b/dist/playground/translate.js.gz
Binary files differ
diff --git a/dist/selinux/index.html b/dist/selinux/index.html
new file mode 100644
index 0000000..a1330aa
--- /dev/null
+++ b/dist/selinux/index.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2016 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="selinux-page" lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title translate="yes">SELinux troubleshoot</title>
+ <meta name="description" content="" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+ <link rel="stylesheet" href="selinux.css" />
+
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script type="text/javascript" src="selinux.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app"></div>
+</body>
+</html>
diff --git a/dist/selinux/manifest.json b/dist/selinux/manifest.json
new file mode 100644
index 0000000..30b5cdf
--- /dev/null
+++ b/dist/selinux/manifest.json
@@ -0,0 +1,15 @@
+{
+ "conditions": [
+ {"path-exists": "/sys/fs/selinux"}
+ ],
+ "tools": {
+ "index": {
+ "label": "SELinux",
+ "keywords": [
+ {
+ "matches": ["setroubleshoot", "semanage", "avc"]
+ }
+ ]
+ }
+ }
+}
diff --git a/dist/selinux/po.cs.js.gz b/dist/selinux/po.cs.js.gz
new file mode 100644
index 0000000..23c3a38
--- /dev/null
+++ b/dist/selinux/po.cs.js.gz
Binary files differ
diff --git a/dist/selinux/po.de.js.gz b/dist/selinux/po.de.js.gz
new file mode 100644
index 0000000..b059d25
--- /dev/null
+++ b/dist/selinux/po.de.js.gz
Binary files differ
diff --git a/dist/selinux/po.es.js.gz b/dist/selinux/po.es.js.gz
new file mode 100644
index 0000000..8f55de1
--- /dev/null
+++ b/dist/selinux/po.es.js.gz
Binary files differ
diff --git a/dist/selinux/po.fi.js.gz b/dist/selinux/po.fi.js.gz
new file mode 100644
index 0000000..4b91bec
--- /dev/null
+++ b/dist/selinux/po.fi.js.gz
Binary files differ
diff --git a/dist/selinux/po.fr.js.gz b/dist/selinux/po.fr.js.gz
new file mode 100644
index 0000000..9953bc7
--- /dev/null
+++ b/dist/selinux/po.fr.js.gz
Binary files differ
diff --git a/dist/selinux/po.he.js.gz b/dist/selinux/po.he.js.gz
new file mode 100644
index 0000000..ceca6ac
--- /dev/null
+++ b/dist/selinux/po.he.js.gz
Binary files differ
diff --git a/dist/selinux/po.it.js.gz b/dist/selinux/po.it.js.gz
new file mode 100644
index 0000000..ec68fad
--- /dev/null
+++ b/dist/selinux/po.it.js.gz
Binary files differ
diff --git a/dist/selinux/po.ja.js.gz b/dist/selinux/po.ja.js.gz
new file mode 100644
index 0000000..e98b13d
--- /dev/null
+++ b/dist/selinux/po.ja.js.gz
Binary files differ
diff --git a/dist/selinux/po.ka.js.gz b/dist/selinux/po.ka.js.gz
new file mode 100644
index 0000000..8ac84f0
--- /dev/null
+++ b/dist/selinux/po.ka.js.gz
Binary files differ
diff --git a/dist/selinux/po.ko.js.gz b/dist/selinux/po.ko.js.gz
new file mode 100644
index 0000000..dfba634
--- /dev/null
+++ b/dist/selinux/po.ko.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.cs.js.gz b/dist/selinux/po.manifest.cs.js.gz
new file mode 100644
index 0000000..e3db605
--- /dev/null
+++ b/dist/selinux/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.de.js.gz b/dist/selinux/po.manifest.de.js.gz
new file mode 100644
index 0000000..f1e205c
--- /dev/null
+++ b/dist/selinux/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.es.js.gz b/dist/selinux/po.manifest.es.js.gz
new file mode 100644
index 0000000..97a912b
--- /dev/null
+++ b/dist/selinux/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.fi.js.gz b/dist/selinux/po.manifest.fi.js.gz
new file mode 100644
index 0000000..c02323c
--- /dev/null
+++ b/dist/selinux/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.fr.js.gz b/dist/selinux/po.manifest.fr.js.gz
new file mode 100644
index 0000000..384c0fa
--- /dev/null
+++ b/dist/selinux/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.he.js.gz b/dist/selinux/po.manifest.he.js.gz
new file mode 100644
index 0000000..9b0fd76
--- /dev/null
+++ b/dist/selinux/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.it.js.gz b/dist/selinux/po.manifest.it.js.gz
new file mode 100644
index 0000000..64b9298
--- /dev/null
+++ b/dist/selinux/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.ja.js.gz b/dist/selinux/po.manifest.ja.js.gz
new file mode 100644
index 0000000..0db60de
--- /dev/null
+++ b/dist/selinux/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.ka.js.gz b/dist/selinux/po.manifest.ka.js.gz
new file mode 100644
index 0000000..d8c793a
--- /dev/null
+++ b/dist/selinux/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.ko.js.gz b/dist/selinux/po.manifest.ko.js.gz
new file mode 100644
index 0000000..aa5c1a3
--- /dev/null
+++ b/dist/selinux/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.nb_NO.js.gz b/dist/selinux/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..1429b4b
--- /dev/null
+++ b/dist/selinux/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.nl.js.gz b/dist/selinux/po.manifest.nl.js.gz
new file mode 100644
index 0000000..5e7e5ab
--- /dev/null
+++ b/dist/selinux/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.pl.js.gz b/dist/selinux/po.manifest.pl.js.gz
new file mode 100644
index 0000000..28029f0
--- /dev/null
+++ b/dist/selinux/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.pt_BR.js.gz b/dist/selinux/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..38cf4d2
--- /dev/null
+++ b/dist/selinux/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.ru.js.gz b/dist/selinux/po.manifest.ru.js.gz
new file mode 100644
index 0000000..98fc746
--- /dev/null
+++ b/dist/selinux/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.sk.js.gz b/dist/selinux/po.manifest.sk.js.gz
new file mode 100644
index 0000000..907b283
--- /dev/null
+++ b/dist/selinux/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.sv.js.gz b/dist/selinux/po.manifest.sv.js.gz
new file mode 100644
index 0000000..e108729
--- /dev/null
+++ b/dist/selinux/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.tr.js.gz b/dist/selinux/po.manifest.tr.js.gz
new file mode 100644
index 0000000..d33cb0d
--- /dev/null
+++ b/dist/selinux/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.uk.js.gz b/dist/selinux/po.manifest.uk.js.gz
new file mode 100644
index 0000000..73dea3f
--- /dev/null
+++ b/dist/selinux/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/selinux/po.manifest.zh_CN.js.gz b/dist/selinux/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..4ec8863
--- /dev/null
+++ b/dist/selinux/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/selinux/po.nb_NO.js.gz b/dist/selinux/po.nb_NO.js.gz
new file mode 100644
index 0000000..b8e41cf
--- /dev/null
+++ b/dist/selinux/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/selinux/po.nl.js.gz b/dist/selinux/po.nl.js.gz
new file mode 100644
index 0000000..ad427f4
--- /dev/null
+++ b/dist/selinux/po.nl.js.gz
Binary files differ
diff --git a/dist/selinux/po.pl.js.gz b/dist/selinux/po.pl.js.gz
new file mode 100644
index 0000000..5c51840
--- /dev/null
+++ b/dist/selinux/po.pl.js.gz
Binary files differ
diff --git a/dist/selinux/po.pt_BR.js.gz b/dist/selinux/po.pt_BR.js.gz
new file mode 100644
index 0000000..b4192f5
--- /dev/null
+++ b/dist/selinux/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/selinux/po.ru.js.gz b/dist/selinux/po.ru.js.gz
new file mode 100644
index 0000000..7d49454
--- /dev/null
+++ b/dist/selinux/po.ru.js.gz
Binary files differ
diff --git a/dist/selinux/po.sk.js.gz b/dist/selinux/po.sk.js.gz
new file mode 100644
index 0000000..2bf5109
--- /dev/null
+++ b/dist/selinux/po.sk.js.gz
Binary files differ
diff --git a/dist/selinux/po.sv.js.gz b/dist/selinux/po.sv.js.gz
new file mode 100644
index 0000000..f881baf
--- /dev/null
+++ b/dist/selinux/po.sv.js.gz
Binary files differ
diff --git a/dist/selinux/po.tr.js.gz b/dist/selinux/po.tr.js.gz
new file mode 100644
index 0000000..ba1f250
--- /dev/null
+++ b/dist/selinux/po.tr.js.gz
Binary files differ
diff --git a/dist/selinux/po.uk.js.gz b/dist/selinux/po.uk.js.gz
new file mode 100644
index 0000000..3f4b69d
--- /dev/null
+++ b/dist/selinux/po.uk.js.gz
Binary files differ
diff --git a/dist/selinux/po.zh_CN.js.gz b/dist/selinux/po.zh_CN.js.gz
new file mode 100644
index 0000000..4efb722
--- /dev/null
+++ b/dist/selinux/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/selinux/selinux.css.LEGAL.txt b/dist/selinux/selinux.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/selinux/selinux.css.LEGAL.txt
diff --git a/dist/selinux/selinux.css.gz b/dist/selinux/selinux.css.gz
new file mode 100644
index 0000000..6b5b79d
--- /dev/null
+++ b/dist/selinux/selinux.css.gz
Binary files differ
diff --git a/dist/selinux/selinux.js.LEGAL.txt b/dist/selinux/selinux.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/selinux/selinux.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/selinux/selinux.js.gz b/dist/selinux/selinux.js.gz
new file mode 100644
index 0000000..e4332c8
--- /dev/null
+++ b/dist/selinux/selinux.js.gz
Binary files differ
diff --git a/dist/shell/images/bg-plain.jpg b/dist/shell/images/bg-plain.jpg
new file mode 100644
index 0000000..a874f34
--- /dev/null
+++ b/dist/shell/images/bg-plain.jpg
Binary files differ
diff --git a/dist/shell/images/cockpit-icon.svg b/dist/shell/images/cockpit-icon.svg
new file mode 100644
index 0000000..d6adfb5
--- /dev/null
+++ b/dist/shell/images/cockpit-icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs/><g fill="#fff" class="ct-icon"><path class="ct-icon-circle" d="M16 0A16 16 0 000 16a16 16 0 0016 16 16 16 0 0016-16A16 16 0 0016 0zm0 2.5A13.5 13.5 0 0129.5 16 13.5 13.5 0 0116 29.5 13.5 13.5 0 012.5 16 13.5 13.5 0 0116 2.5z"/><path class="ct-icon-plane" d="M21.26 10c-.664-.024-1.67.498-2.575 1.398l-1.951 1.94-5.846-1.963-1.14 1.177 4.408 3.35-.986.98c-.349.346-.64.71-.87 1.066l-2.652-.197-.648.656 2.641 1.956L13.571 23l.652-.653-.196-2.679c.334-.22.677-.494 1.005-.822l1.038-1.031 3.382 4.442 1.177-1.14-1.973-5.875 1.89-1.879c1.207-1.2 1.763-2.603 1.248-3.149-.13-.136-.313-.205-.534-.213z"/></g></svg>
diff --git a/dist/shell/images/server-error.png b/dist/shell/images/server-error.png
new file mode 100644
index 0000000..3ac6db5
--- /dev/null
+++ b/dist/shell/images/server-error.png
Binary files differ
diff --git a/dist/shell/images/server-large.png b/dist/shell/images/server-large.png
new file mode 100644
index 0000000..7d429b3
--- /dev/null
+++ b/dist/shell/images/server-large.png
Binary files differ
diff --git a/dist/shell/images/server-small.png b/dist/shell/images/server-small.png
new file mode 100644
index 0000000..2837870
--- /dev/null
+++ b/dist/shell/images/server-small.png
Binary files differ
diff --git a/dist/shell/index.html b/dist/shell/index.html
new file mode 100644
index 0000000..96a76a8
--- /dev/null
+++ b/dist/shell/index.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html id="shell-page" class="index-page pf-v5-theme-dark">
+ <head>
+ <title>Cockpit</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="shell.css" rel="stylesheet" />
+ <link href="../../static/branding.css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../manifests.js"></script>
+ <!-- HACK: C bridge loads translations via glob and Python via manifest.js -->
+ <script src="../*/po.manifest.js"></script>
+ <script src="../*/po.js"></script>
+ <script src="po.js"></script>
+ </head>
+ <body class="pf-v5-m-tabular-nums" hidden="true">
+ <div id="main" class="page">
+ <a class="screenreader-text skiplink desktop_v" href="#content" translate="yes">Skip to content</a>
+ <a class="screenreader-text skiplink desktop_v" href="#hosts-sel" translate="yes">Skip main navigation</a>
+
+ <div id="sidebar-toggle" class="pf-v5-c-select pf-m-dark sidebar-toggle">
+ </div>
+
+ <div id="nav-system" class="area-ct-subnav nav-system-menu sidebar interact">
+ <nav id="host-apps" class="host-apps">
+ <!-- Navigation goes here !-->
+ </nav>
+ </div>
+
+ <nav id="hosts-sel" class="navbar navbar-default navbar-pf navbar-pf-vertical" tabindex="-1">
+ <!-- Hosts selector goes here !-->
+ </nav>
+
+ <div id="nav-hosts" class="area-ct-subnav nav-hosts-menu sidebar">
+ <!-- Hosts go here !-->
+ </div>
+
+ <div id="topnav" class="header">
+ <!-- Top navigation goes here !-->
+ </div>
+
+
+ <div id="troubleshoot-dialog"></div>
+ <div id="session-timeout-dialog"></div>
+
+ <div id="content" role="main" class="area-ct-content" tabindex="-1">
+ <!-- This is where the iframes appear -->
+ <div id="early-failure-ready" class="curtains-ct" hidden="true"></div>
+ </div>
+ </div>
+
+ <div id="early-failure" class="early-failure" hidden="true">
+ </div>
+
+ <script src="shell.js"></script>
+ </body>
+</html>
diff --git a/dist/shell/manifest.json b/dist/shell/manifest.json
new file mode 100644
index 0000000..a3f965f
--- /dev/null
+++ b/dist/shell/manifest.json
@@ -0,0 +1,58 @@
+{
+ "requires": {
+ "cockpit": "239"
+ },
+ "locales": {
+ "cs-cz": "čeština",
+ "de-de": "Deutsch",
+ "en-us": "English",
+ "es-es": "español",
+ "fi-fi": "suomi",
+ "fr-fr": "français",
+ "he-il": "עברית",
+ "it-it": "italiano",
+ "ja-jp": "日本語",
+ "ka-ge": "ქართული",
+ "ko-kr": "한국어",
+ "nb-no": "norsk bokmål",
+ "nl-nl": "Nederlands",
+ "pl-pl": "polski",
+ "pt-br": "português (Brasil)",
+ "ru-ru": "русский",
+ "sk-sk": "slovenčina",
+ "sv-se": "svenska",
+ "tr-tr": "Türkçe",
+ "uk-ua": "Українська",
+ "zh-cn": "中文(中国)"
+ },
+ "docs": [
+ {
+ "label": "Web Console",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/index"
+ }
+ ],
+ "bridges": [
+ {
+ "privileged": true,
+ "environ": [
+ "SUDO_ASKPASS=${libexecdir}/cockpit-askpass"
+ ],
+ "spawn": [
+ "sudo",
+ "-k",
+ "-A",
+ "cockpit-bridge",
+ "--privileged"
+ ]
+ },
+ {
+ "privileged": true,
+ "spawn": [
+ "pkexec",
+ "--disable-internal-agent",
+ "cockpit-bridge",
+ "--privileged"
+ ]
+ }
+ ]
+}
diff --git a/dist/shell/po.cs.js.gz b/dist/shell/po.cs.js.gz
new file mode 100644
index 0000000..c1ac3f1
--- /dev/null
+++ b/dist/shell/po.cs.js.gz
Binary files differ
diff --git a/dist/shell/po.de.js.gz b/dist/shell/po.de.js.gz
new file mode 100644
index 0000000..4a7bd14
--- /dev/null
+++ b/dist/shell/po.de.js.gz
Binary files differ
diff --git a/dist/shell/po.es.js.gz b/dist/shell/po.es.js.gz
new file mode 100644
index 0000000..68cc54f
--- /dev/null
+++ b/dist/shell/po.es.js.gz
Binary files differ
diff --git a/dist/shell/po.fi.js.gz b/dist/shell/po.fi.js.gz
new file mode 100644
index 0000000..88aebec
--- /dev/null
+++ b/dist/shell/po.fi.js.gz
Binary files differ
diff --git a/dist/shell/po.fr.js.gz b/dist/shell/po.fr.js.gz
new file mode 100644
index 0000000..8405999
--- /dev/null
+++ b/dist/shell/po.fr.js.gz
Binary files differ
diff --git a/dist/shell/po.he.js.gz b/dist/shell/po.he.js.gz
new file mode 100644
index 0000000..f1a79dc
--- /dev/null
+++ b/dist/shell/po.he.js.gz
Binary files differ
diff --git a/dist/shell/po.it.js.gz b/dist/shell/po.it.js.gz
new file mode 100644
index 0000000..1c236a6
--- /dev/null
+++ b/dist/shell/po.it.js.gz
Binary files differ
diff --git a/dist/shell/po.ja.js.gz b/dist/shell/po.ja.js.gz
new file mode 100644
index 0000000..efb9c4d
--- /dev/null
+++ b/dist/shell/po.ja.js.gz
Binary files differ
diff --git a/dist/shell/po.ka.js.gz b/dist/shell/po.ka.js.gz
new file mode 100644
index 0000000..353726d
--- /dev/null
+++ b/dist/shell/po.ka.js.gz
Binary files differ
diff --git a/dist/shell/po.ko.js.gz b/dist/shell/po.ko.js.gz
new file mode 100644
index 0000000..8a03ac4
--- /dev/null
+++ b/dist/shell/po.ko.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.cs.js.gz b/dist/shell/po.manifest.cs.js.gz
new file mode 100644
index 0000000..71d7a79
--- /dev/null
+++ b/dist/shell/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.de.js.gz b/dist/shell/po.manifest.de.js.gz
new file mode 100644
index 0000000..746b979
--- /dev/null
+++ b/dist/shell/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.es.js.gz b/dist/shell/po.manifest.es.js.gz
new file mode 100644
index 0000000..58bf1e9
--- /dev/null
+++ b/dist/shell/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.fi.js.gz b/dist/shell/po.manifest.fi.js.gz
new file mode 100644
index 0000000..ba54e59
--- /dev/null
+++ b/dist/shell/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.fr.js.gz b/dist/shell/po.manifest.fr.js.gz
new file mode 100644
index 0000000..758a252
--- /dev/null
+++ b/dist/shell/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.he.js.gz b/dist/shell/po.manifest.he.js.gz
new file mode 100644
index 0000000..7e5abff
--- /dev/null
+++ b/dist/shell/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.it.js.gz b/dist/shell/po.manifest.it.js.gz
new file mode 100644
index 0000000..bc74bf1
--- /dev/null
+++ b/dist/shell/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.ja.js.gz b/dist/shell/po.manifest.ja.js.gz
new file mode 100644
index 0000000..ea9a150
--- /dev/null
+++ b/dist/shell/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.ka.js.gz b/dist/shell/po.manifest.ka.js.gz
new file mode 100644
index 0000000..2150563
--- /dev/null
+++ b/dist/shell/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.ko.js.gz b/dist/shell/po.manifest.ko.js.gz
new file mode 100644
index 0000000..382fc25
--- /dev/null
+++ b/dist/shell/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.nb_NO.js.gz b/dist/shell/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..7b6afac
--- /dev/null
+++ b/dist/shell/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.nl.js.gz b/dist/shell/po.manifest.nl.js.gz
new file mode 100644
index 0000000..7bb8251
--- /dev/null
+++ b/dist/shell/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.pl.js.gz b/dist/shell/po.manifest.pl.js.gz
new file mode 100644
index 0000000..6c64ef4
--- /dev/null
+++ b/dist/shell/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.pt_BR.js.gz b/dist/shell/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..4ea65d9
--- /dev/null
+++ b/dist/shell/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.ru.js.gz b/dist/shell/po.manifest.ru.js.gz
new file mode 100644
index 0000000..42211a6
--- /dev/null
+++ b/dist/shell/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.sk.js.gz b/dist/shell/po.manifest.sk.js.gz
new file mode 100644
index 0000000..85c38a2
--- /dev/null
+++ b/dist/shell/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.sv.js.gz b/dist/shell/po.manifest.sv.js.gz
new file mode 100644
index 0000000..89636bc
--- /dev/null
+++ b/dist/shell/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.tr.js.gz b/dist/shell/po.manifest.tr.js.gz
new file mode 100644
index 0000000..962803a
--- /dev/null
+++ b/dist/shell/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.uk.js.gz b/dist/shell/po.manifest.uk.js.gz
new file mode 100644
index 0000000..f55832f
--- /dev/null
+++ b/dist/shell/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/shell/po.manifest.zh_CN.js.gz b/dist/shell/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..e16bef2
--- /dev/null
+++ b/dist/shell/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/shell/po.nb_NO.js.gz b/dist/shell/po.nb_NO.js.gz
new file mode 100644
index 0000000..c59f855
--- /dev/null
+++ b/dist/shell/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/shell/po.nl.js.gz b/dist/shell/po.nl.js.gz
new file mode 100644
index 0000000..434c160
--- /dev/null
+++ b/dist/shell/po.nl.js.gz
Binary files differ
diff --git a/dist/shell/po.pl.js.gz b/dist/shell/po.pl.js.gz
new file mode 100644
index 0000000..91a38fe
--- /dev/null
+++ b/dist/shell/po.pl.js.gz
Binary files differ
diff --git a/dist/shell/po.pt_BR.js.gz b/dist/shell/po.pt_BR.js.gz
new file mode 100644
index 0000000..ad5f0bd
--- /dev/null
+++ b/dist/shell/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/shell/po.ru.js.gz b/dist/shell/po.ru.js.gz
new file mode 100644
index 0000000..98ed2bd
--- /dev/null
+++ b/dist/shell/po.ru.js.gz
Binary files differ
diff --git a/dist/shell/po.sk.js.gz b/dist/shell/po.sk.js.gz
new file mode 100644
index 0000000..97092cd
--- /dev/null
+++ b/dist/shell/po.sk.js.gz
Binary files differ
diff --git a/dist/shell/po.sv.js.gz b/dist/shell/po.sv.js.gz
new file mode 100644
index 0000000..7b41b5c
--- /dev/null
+++ b/dist/shell/po.sv.js.gz
Binary files differ
diff --git a/dist/shell/po.tr.js.gz b/dist/shell/po.tr.js.gz
new file mode 100644
index 0000000..ac752e1
--- /dev/null
+++ b/dist/shell/po.tr.js.gz
Binary files differ
diff --git a/dist/shell/po.uk.js.gz b/dist/shell/po.uk.js.gz
new file mode 100644
index 0000000..daa416e
--- /dev/null
+++ b/dist/shell/po.uk.js.gz
Binary files differ
diff --git a/dist/shell/po.zh_CN.js.gz b/dist/shell/po.zh_CN.js.gz
new file mode 100644
index 0000000..8346594
--- /dev/null
+++ b/dist/shell/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/shell/shell.css.LEGAL.txt b/dist/shell/shell.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/shell/shell.css.LEGAL.txt
diff --git a/dist/shell/shell.css.gz b/dist/shell/shell.css.gz
new file mode 100644
index 0000000..114d505
--- /dev/null
+++ b/dist/shell/shell.css.gz
Binary files differ
diff --git a/dist/shell/shell.html b/dist/shell/shell.html
new file mode 100644
index 0000000..6a6b000
--- /dev/null
+++ b/dist/shell/shell.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Things have moved</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="stylesheet" href="index.css" />
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="curtains-ct">
+ <h1>Things have moved</h1>
+ <p>This old version of Cockpit doesn't know where to find default pages. Use the navigation menus to help it find its way.</p>
+ </div>
+</body>
+</html>
diff --git a/dist/shell/shell.js.LEGAL.txt b/dist/shell/shell.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/shell/shell.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/shell/shell.js.gz b/dist/shell/shell.js.gz
new file mode 100644
index 0000000..51bfbf3
--- /dev/null
+++ b/dist/shell/shell.js.gz
Binary files differ
diff --git a/dist/sosreport/index.html b/dist/sosreport/index.html
new file mode 100644
index 0000000..1d770cc
--- /dev/null
+++ b/dist/sosreport/index.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2021 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+ <title translate="yes">Diagnostic reports</title>
+ <meta charset="utf-8" />
+ <link href="sosreport.css" rel="stylesheet" />
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script type="text/javascript" src="sosreport.js"></script>
+ </head>
+ <body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app"></div>
+ </body>
+</html>
diff --git a/dist/sosreport/manifest.json b/dist/sosreport/manifest.json
new file mode 100644
index 0000000..6f33ae2
--- /dev/null
+++ b/dist/sosreport/manifest.json
@@ -0,0 +1,12 @@
+{
+ "tools": {
+ "index": {
+ "label": "Diagnostic reports",
+ "keywords": [
+ {
+ "matches": ["sos"]
+ }
+ ]
+ }
+ }
+}
diff --git a/dist/sosreport/po.cs.js.gz b/dist/sosreport/po.cs.js.gz
new file mode 100644
index 0000000..9f0c7a5
--- /dev/null
+++ b/dist/sosreport/po.cs.js.gz
Binary files differ
diff --git a/dist/sosreport/po.de.js.gz b/dist/sosreport/po.de.js.gz
new file mode 100644
index 0000000..9152448
--- /dev/null
+++ b/dist/sosreport/po.de.js.gz
Binary files differ
diff --git a/dist/sosreport/po.es.js.gz b/dist/sosreport/po.es.js.gz
new file mode 100644
index 0000000..7f1679e
--- /dev/null
+++ b/dist/sosreport/po.es.js.gz
Binary files differ
diff --git a/dist/sosreport/po.fi.js.gz b/dist/sosreport/po.fi.js.gz
new file mode 100644
index 0000000..6004cb8
--- /dev/null
+++ b/dist/sosreport/po.fi.js.gz
Binary files differ
diff --git a/dist/sosreport/po.fr.js.gz b/dist/sosreport/po.fr.js.gz
new file mode 100644
index 0000000..19415a1
--- /dev/null
+++ b/dist/sosreport/po.fr.js.gz
Binary files differ
diff --git a/dist/sosreport/po.he.js.gz b/dist/sosreport/po.he.js.gz
new file mode 100644
index 0000000..ad00b7a
--- /dev/null
+++ b/dist/sosreport/po.he.js.gz
Binary files differ
diff --git a/dist/sosreport/po.it.js.gz b/dist/sosreport/po.it.js.gz
new file mode 100644
index 0000000..3555558
--- /dev/null
+++ b/dist/sosreport/po.it.js.gz
Binary files differ
diff --git a/dist/sosreport/po.ja.js.gz b/dist/sosreport/po.ja.js.gz
new file mode 100644
index 0000000..bcccf7d
--- /dev/null
+++ b/dist/sosreport/po.ja.js.gz
Binary files differ
diff --git a/dist/sosreport/po.ka.js.gz b/dist/sosreport/po.ka.js.gz
new file mode 100644
index 0000000..952ce0c
--- /dev/null
+++ b/dist/sosreport/po.ka.js.gz
Binary files differ
diff --git a/dist/sosreport/po.ko.js.gz b/dist/sosreport/po.ko.js.gz
new file mode 100644
index 0000000..81deb6c
--- /dev/null
+++ b/dist/sosreport/po.ko.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.cs.js.gz b/dist/sosreport/po.manifest.cs.js.gz
new file mode 100644
index 0000000..d5ad187
--- /dev/null
+++ b/dist/sosreport/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.de.js.gz b/dist/sosreport/po.manifest.de.js.gz
new file mode 100644
index 0000000..c1b3cb5
--- /dev/null
+++ b/dist/sosreport/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.es.js.gz b/dist/sosreport/po.manifest.es.js.gz
new file mode 100644
index 0000000..dc066d0
--- /dev/null
+++ b/dist/sosreport/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.fi.js.gz b/dist/sosreport/po.manifest.fi.js.gz
new file mode 100644
index 0000000..d1daffc
--- /dev/null
+++ b/dist/sosreport/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.fr.js.gz b/dist/sosreport/po.manifest.fr.js.gz
new file mode 100644
index 0000000..21a0488
--- /dev/null
+++ b/dist/sosreport/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.he.js.gz b/dist/sosreport/po.manifest.he.js.gz
new file mode 100644
index 0000000..2732256
--- /dev/null
+++ b/dist/sosreport/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.it.js.gz b/dist/sosreport/po.manifest.it.js.gz
new file mode 100644
index 0000000..0b6b362
--- /dev/null
+++ b/dist/sosreport/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.ja.js.gz b/dist/sosreport/po.manifest.ja.js.gz
new file mode 100644
index 0000000..affbb02
--- /dev/null
+++ b/dist/sosreport/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.ka.js.gz b/dist/sosreport/po.manifest.ka.js.gz
new file mode 100644
index 0000000..56e9ee3
--- /dev/null
+++ b/dist/sosreport/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.ko.js.gz b/dist/sosreport/po.manifest.ko.js.gz
new file mode 100644
index 0000000..7e76ee7
--- /dev/null
+++ b/dist/sosreport/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.nb_NO.js.gz b/dist/sosreport/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..680a480
--- /dev/null
+++ b/dist/sosreport/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.nl.js.gz b/dist/sosreport/po.manifest.nl.js.gz
new file mode 100644
index 0000000..bd8e8b0
--- /dev/null
+++ b/dist/sosreport/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.pl.js.gz b/dist/sosreport/po.manifest.pl.js.gz
new file mode 100644
index 0000000..698c591
--- /dev/null
+++ b/dist/sosreport/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.pt_BR.js.gz b/dist/sosreport/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..b9a9d3c
--- /dev/null
+++ b/dist/sosreport/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.ru.js.gz b/dist/sosreport/po.manifest.ru.js.gz
new file mode 100644
index 0000000..2dc7987
--- /dev/null
+++ b/dist/sosreport/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.sk.js.gz b/dist/sosreport/po.manifest.sk.js.gz
new file mode 100644
index 0000000..84debc6
--- /dev/null
+++ b/dist/sosreport/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.sv.js.gz b/dist/sosreport/po.manifest.sv.js.gz
new file mode 100644
index 0000000..0575aad
--- /dev/null
+++ b/dist/sosreport/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.tr.js.gz b/dist/sosreport/po.manifest.tr.js.gz
new file mode 100644
index 0000000..848313e
--- /dev/null
+++ b/dist/sosreport/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.uk.js.gz b/dist/sosreport/po.manifest.uk.js.gz
new file mode 100644
index 0000000..5cc0f1f
--- /dev/null
+++ b/dist/sosreport/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/sosreport/po.manifest.zh_CN.js.gz b/dist/sosreport/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..6a953e7
--- /dev/null
+++ b/dist/sosreport/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/sosreport/po.nb_NO.js.gz b/dist/sosreport/po.nb_NO.js.gz
new file mode 100644
index 0000000..3881d7a
--- /dev/null
+++ b/dist/sosreport/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/sosreport/po.nl.js.gz b/dist/sosreport/po.nl.js.gz
new file mode 100644
index 0000000..b8ce2e0
--- /dev/null
+++ b/dist/sosreport/po.nl.js.gz
Binary files differ
diff --git a/dist/sosreport/po.pl.js.gz b/dist/sosreport/po.pl.js.gz
new file mode 100644
index 0000000..88cdda3
--- /dev/null
+++ b/dist/sosreport/po.pl.js.gz
Binary files differ
diff --git a/dist/sosreport/po.pt_BR.js.gz b/dist/sosreport/po.pt_BR.js.gz
new file mode 100644
index 0000000..c3d2738
--- /dev/null
+++ b/dist/sosreport/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/sosreport/po.ru.js.gz b/dist/sosreport/po.ru.js.gz
new file mode 100644
index 0000000..f5629a9
--- /dev/null
+++ b/dist/sosreport/po.ru.js.gz
Binary files differ
diff --git a/dist/sosreport/po.sk.js.gz b/dist/sosreport/po.sk.js.gz
new file mode 100644
index 0000000..d0273a2
--- /dev/null
+++ b/dist/sosreport/po.sk.js.gz
Binary files differ
diff --git a/dist/sosreport/po.sv.js.gz b/dist/sosreport/po.sv.js.gz
new file mode 100644
index 0000000..dc3164f
--- /dev/null
+++ b/dist/sosreport/po.sv.js.gz
Binary files differ
diff --git a/dist/sosreport/po.tr.js.gz b/dist/sosreport/po.tr.js.gz
new file mode 100644
index 0000000..67216a4
--- /dev/null
+++ b/dist/sosreport/po.tr.js.gz
Binary files differ
diff --git a/dist/sosreport/po.uk.js.gz b/dist/sosreport/po.uk.js.gz
new file mode 100644
index 0000000..45837a1
--- /dev/null
+++ b/dist/sosreport/po.uk.js.gz
Binary files differ
diff --git a/dist/sosreport/po.zh_CN.js.gz b/dist/sosreport/po.zh_CN.js.gz
new file mode 100644
index 0000000..e85bb0e
--- /dev/null
+++ b/dist/sosreport/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/sosreport/sosreport.css.LEGAL.txt b/dist/sosreport/sosreport.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/sosreport/sosreport.css.LEGAL.txt
diff --git a/dist/sosreport/sosreport.css.gz b/dist/sosreport/sosreport.css.gz
new file mode 100644
index 0000000..07ebfa4
--- /dev/null
+++ b/dist/sosreport/sosreport.css.gz
Binary files differ
diff --git a/dist/sosreport/sosreport.js.LEGAL.txt b/dist/sosreport/sosreport.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/sosreport/sosreport.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/sosreport/sosreport.js.gz b/dist/sosreport/sosreport.js.gz
new file mode 100644
index 0000000..a6d3a8c
--- /dev/null
+++ b/dist/sosreport/sosreport.js.gz
Binary files differ
diff --git a/dist/sosreport/sosreport.png b/dist/sosreport/sosreport.png
new file mode 100644
index 0000000..df1440f
--- /dev/null
+++ b/dist/sosreport/sosreport.png
Binary files differ
diff --git a/dist/static/fonts/RedHatDisplay-Black.woff2 b/dist/static/fonts/RedHatDisplay-Black.woff2
new file mode 100644
index 0000000..8ac2e0b
--- /dev/null
+++ b/dist/static/fonts/RedHatDisplay-Black.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatDisplay-BlackItalic.woff2 b/dist/static/fonts/RedHatDisplay-BlackItalic.woff2
new file mode 100644
index 0000000..6ea7b2c
--- /dev/null
+++ b/dist/static/fonts/RedHatDisplay-BlackItalic.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatDisplay-Bold.woff2 b/dist/static/fonts/RedHatDisplay-Bold.woff2
new file mode 100644
index 0000000..889f92e
--- /dev/null
+++ b/dist/static/fonts/RedHatDisplay-Bold.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatDisplay-BoldItalic.woff2 b/dist/static/fonts/RedHatDisplay-BoldItalic.woff2
new file mode 100644
index 0000000..8dd4226
--- /dev/null
+++ b/dist/static/fonts/RedHatDisplay-BoldItalic.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatDisplay-Italic.woff2 b/dist/static/fonts/RedHatDisplay-Italic.woff2
new file mode 100644
index 0000000..4489a21
--- /dev/null
+++ b/dist/static/fonts/RedHatDisplay-Italic.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatDisplay-Medium.woff2 b/dist/static/fonts/RedHatDisplay-Medium.woff2
new file mode 100644
index 0000000..7a76ecc
--- /dev/null
+++ b/dist/static/fonts/RedHatDisplay-Medium.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatDisplay-MediumItalic.woff2 b/dist/static/fonts/RedHatDisplay-MediumItalic.woff2
new file mode 100644
index 0000000..4ff857b
--- /dev/null
+++ b/dist/static/fonts/RedHatDisplay-MediumItalic.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatDisplay-Regular.woff2 b/dist/static/fonts/RedHatDisplay-Regular.woff2
new file mode 100644
index 0000000..f5b242d
--- /dev/null
+++ b/dist/static/fonts/RedHatDisplay-Regular.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatMono-Bold.woff2 b/dist/static/fonts/RedHatMono-Bold.woff2
new file mode 100644
index 0000000..bfac6ea
--- /dev/null
+++ b/dist/static/fonts/RedHatMono-Bold.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatMono-BoldItalic.woff2 b/dist/static/fonts/RedHatMono-BoldItalic.woff2
new file mode 100644
index 0000000..327f5e5
--- /dev/null
+++ b/dist/static/fonts/RedHatMono-BoldItalic.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatMono-Italic.woff2 b/dist/static/fonts/RedHatMono-Italic.woff2
new file mode 100644
index 0000000..6c0d95b
--- /dev/null
+++ b/dist/static/fonts/RedHatMono-Italic.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatMono-Medium.woff2 b/dist/static/fonts/RedHatMono-Medium.woff2
new file mode 100644
index 0000000..a934a25
--- /dev/null
+++ b/dist/static/fonts/RedHatMono-Medium.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatMono-MediumItalic.woff2 b/dist/static/fonts/RedHatMono-MediumItalic.woff2
new file mode 100644
index 0000000..163b6e1
--- /dev/null
+++ b/dist/static/fonts/RedHatMono-MediumItalic.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatMono-Regular.woff2 b/dist/static/fonts/RedHatMono-Regular.woff2
new file mode 100644
index 0000000..585e763
--- /dev/null
+++ b/dist/static/fonts/RedHatMono-Regular.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatText-Bold.woff2 b/dist/static/fonts/RedHatText-Bold.woff2
new file mode 100644
index 0000000..923b865
--- /dev/null
+++ b/dist/static/fonts/RedHatText-Bold.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatText-BoldItalic.woff2 b/dist/static/fonts/RedHatText-BoldItalic.woff2
new file mode 100644
index 0000000..e42cec8
--- /dev/null
+++ b/dist/static/fonts/RedHatText-BoldItalic.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatText-Italic.woff2 b/dist/static/fonts/RedHatText-Italic.woff2
new file mode 100644
index 0000000..f14577f
--- /dev/null
+++ b/dist/static/fonts/RedHatText-Italic.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatText-Medium.woff2 b/dist/static/fonts/RedHatText-Medium.woff2
new file mode 100644
index 0000000..4956b5d
--- /dev/null
+++ b/dist/static/fonts/RedHatText-Medium.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatText-MediumItalic.woff2 b/dist/static/fonts/RedHatText-MediumItalic.woff2
new file mode 100644
index 0000000..2e2f65f
--- /dev/null
+++ b/dist/static/fonts/RedHatText-MediumItalic.woff2
Binary files differ
diff --git a/dist/static/fonts/RedHatText-Regular.woff2 b/dist/static/fonts/RedHatText-Regular.woff2
new file mode 100644
index 0000000..c3944bd
--- /dev/null
+++ b/dist/static/fonts/RedHatText-Regular.woff2
Binary files differ
diff --git a/dist/static/login.css b/dist/static/login.css
new file mode 100644
index 0000000..6ed698b
--- /dev/null
+++ b/dist/static/login.css
@@ -0,0 +1 @@
+@charset "UTF-8";@font-face{font-family:RedHatText;font-style:normal;font-weight:400;src:url(fonts/RedHatText-Regular.woff2) format("woff2")}@font-face{font-family:RedHatText;font-style:normal;font-weight:700;src:url(fonts/RedHatText-Medium.woff2) format("woff2")}*,:after,:before{box-sizing:border-box}[hidden]:not([hidden=false]){display:none!important}html{font-family:sans-serif;block-size:100%;background:#000}body{margin:0;font-family:RedHatText,Helvetica,Arial,sans-serif;background-color:#333;color:#fff;line-height:1.5}h1,h2,h3,h4{font-size:1.125rem;font-weight:400;margin:0}h1{font-size:1.75rem;line-height:1.3;padding-block:0 1rem;padding-inline:0}h2{font-size:1.3rem}pre{white-space:pre-wrap}.pf-v5-c-button,label{font-weight:600}.pf-v5-c-button,img{vertical-align:middle}#option-group,.pf-v5-c-button,.input-clear,button{cursor:pointer}#option-group svg,.input-clear{opacity:.7}a,summary{color:#06c;text-decoration:none}.pf-v5-c-button:focus,.host-remove:focus,button:focus,a:focus{outline:-webkit-focus-ring-color auto 5px;outline:dotted thin;outline-offset:-2px}a:focus,a:hover{color:#fff;text-decoration:underline}img{border:0}button,input,select,textarea{font-family:inherit;margin:0;font-size:inherit;line-height:inherit}button{overflow:visible;border-radius:.1875rem;border:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}p{margin-block:0 1rem;margin-inline:0}.container{margin-inline:auto}#option-group,#option-group[hidden]+#server-group{margin-block-start:1rem}#option-group{margin-block-end:1rem}.form-group:not(:first-child){margin-block-start:1rem}.login-actions{display:flex;align-items:baseline;grid-gap:1rem}label{display:inline-block}#option-group,.control-label{font-size:.875rem}[role=form] .control-label,form .control-label{display:flex;align-items:center;min-block-size:2rem;padding-block:0 .5rem;padding-inline:0}.form-control{color:#151515}.pf-v5-c-button.pf-m-control,.form-control[type=password],.form-control[type=text]{display:block;inline-size:100%;padding-block:.25rem;padding-inline:.5rem;background-color:#fff;background-image:none;border:1px solid #ededed;border-block-end-color:#72767b;border-radius:1px;transition:border-color .15s ease-in-out;color:#151515}.pf-v5-c-button.pf-m-control:hover,.form-control[type=password]:hover,.form-control[type=text]:hover,.form-control[type=password]:focus,.form-control[type=text]:focus{border-block-end-color:#06c}.pf-v5-c-button.pf-m-control:focus,.form-control[type=password]:focus,.form-control[type=text]:focus{padding-block-end:calc(.25rem - 1px);border-block-end-width:2px}.pf-v5-c-button.pf-m-control:focus,.form-control:focus{outline:0}.form-control::placeholder{color:#999;font-style:italic;opacity:1}.checkbox-row{margin-block:1rem 0;margin-inline:0;display:flex;align-items:baseline}.checkbox-row>input[type=checkbox]{inline-size:1rem;block-size:1rem;align-self:flex-start;margin-block-start:.25rem;margin-inline-end:.5rem}label.checkbox{font:inherit}.help-block{display:block;margin-block:5px 1rem;color:#737373}.pf-v5-c-button,.caret{display:inline-block}.pf-v5-c-button[spinning] .spinner{display:inline-flex}.form-group:after{clear:both;margin-block-end:1rem}.pf-v5-c-button{text-align:center;background-image:none;border:1px solid transparent;white-space:nowrap;padding-block:.375rem;padding-inline:1rem;border-radius:3px;font-size:inherit;font-weight:400;user-select:none}.pf-v5-c-button:focus,.pf-v5-c-button:hover{text-decoration:none}.pf-v5-c-button:active{outline:0;background-image:none}.pf-m-primary{background-color:#06c;border-color:#06c;color:#fff}.pf-m-primary:active,.pf-m-primary:not([disabled]):focus,.pf-m-primary:not([disabled]):hover{background-color:#004080;border-color:#004080;color:#fff;outline:none}.pf-m-tertiary{background-color:transparent;border-color:#151515;color:#151515}.pf-m-tertiary:active,.pf-m-tertiary:focus,.pf-m-tertiary:hover{background-color:transparent;border-color:#151515;color:#151515;border-width:2px}.pf-m-danger{background-color:#c9190b;border-color:#c9190b;color:#fff}.pf-m-danger:hover,.pf-m-danger:focus{background-color:#a30000;border-color:#a30000;outline:none}.pf-v5-c-button.pf-m-warning{background-color:#f0ab00;border-color:#f0ab00;color:#000}.pf-v5-c-button.pf-m-warning:hover,.pf-v5-c-button.pf-m-warning:focus{background-color:#c58c00;border-color:#c58c00;color:#000}.login-pf{block-size:100%}.login-pf #brand img{display:block;margin-block:0;margin-inline:auto;max-inline-size:100%}.unsupported-browser #brand{display:none}.login-pf #banner{margin-block:1rem .5rem;margin-inline:0;grid-area:banner;inline-size:100%}#banner-message{white-space:pre-wrap;max-block-size:12em;overflow:auto}.login-pf .container{background-color:#ffffff80;background:#fff;color:#333;padding-block:3rem 2rem;padding-inline:3rem;inline-size:100%}#main{grid-area:login}.login-pf .container .details p:first-child{border-block-start:1px solid #474747;padding-block-start:1.5rem;margin-block-start:1.5rem}.login-pf .details p{margin:0}.login-pf .details p+p{margin-block-start:.5rem}.login-pf .container .control-label{font-weight:600}.login-pf .container .help-block{color:#fff}.login-pf .container .form-group:last-child,.login-pf .container .form-group:last-child .help-block:last-child{margin-block-end:0}.spinner{animation:.6s linear infinite rotation;border:4px solid rgba(0,0,0,.25);border-block-start-color:#000000bf;border-radius:100%;block-size:1.5rem;inline-size:1.5rem}.pf-v5-c-alert{color:#151515;position:relative;grid-template-columns:max-content 1fr max-content;grid-template-rows:1fr auto;grid-template-areas:"icon title action" ". content content";background-color:#fff;margin-block:0 1.5rem;margin-inline:0;display:grid;border:3px solid #009596;border-width:2px 0 0;box-shadow:#03030329 0 .5rem 1rem,#03030314 0 0 .5rem}.pf-v5-c-alert.pf-m-inline{box-shadow:none}.pf-v5-c-alert>svg{grid-area:icon;block-size:1.125rem;inline-size:1.125rem;margin-block:1.25rem 1rem;margin-inline:1rem;float:inline-start;color:#009596}@supports (display: grid){.pf-v5-c-alert>svg{float:none;margin-inline-end:0}}.pf-v5-c-alert__title{grid-area:title;font-size:1rem;margin:1rem}.pf-v5-c-alert.pf-m-inline.pf-m-danger{background:#faeae8;border-color:#c9190b}.pf-v5-c-alert.pf-m-danger>svg{color:#c9190b}.pf-v5-c-alert.pf-m-danger .pf-v5-c-alert__title{color:#a30000}.pf-v5-c-alert.pf-m-inline.pf-m-warning{background:#fdf7e7;border-color:#f0ab00}.pf-v5-c-alert.pf-m-warning>svg{color:#f0ab00}.pf-v5-c-alert.pf-m-warning .pf-v5-c-alert__title{color:#795600}.pf-v5-c-alert.pf-m-inline.pf-m-info{background:#e7f1fa;border-color:#73bcf7}.pf-v5-c-alert.pf-m-info>svg{color:#73bcf7}.pf-v5-c-alert.pf-m-info .pf-v5-c-alert__title{color:#004368}#server-group:before{clear:both;margin-block-start:5px}.login-fatal{font-size:130%}.unsupported-browser ul{display:inline-block;margin-block:0;margin-inline:auto;text-align:start}.unsupported-browser a{font-weight:700}.input-clear,.inline .container .help-block{color:#000}.caret,.server-box{position:relative}.conversation-prompt{white-space:normal;word-wrap:break-word}.control-label{white-space:nowrap}.spinner{border-color:rgba(255,255,255,.75) rgba(255,255,255,.25) rgba(255,255,255,.25)}#hostkey-fingerprint{font-size:large;font-weight:700;margin-block-end:0}#hostkey-type{font-size:small}#hostkey-verify-help-cmds{border-radius:.25rem;padding-block:.5rem;padding-inline:1rem;background:#96969633}.pf-v5-c-button .spinner,.pf-v5-c-button[spinning] .button-text,.hide-before:before{display:none}.inline #badge,.inline #brand,.inline #login-details{display:none}.inline body{background:0 0!important;color:#000}.caret{vertical-align:middle;margin-block:calc((1.5rem - 16px)/2);margin-inline:0;margin-inline-end:.25rem;transition:all .3s}[data-state=true] svg.caret{transform:rotate(90deg) translate(-3px);transform-origin:.5rem}.input-clear{display:flex;padding-block:.5rem;padding-inline:.75rem;position:absolute;inset-block:0;inset-inline-end:0}.input-clear>svg{block-size:1rem;inline-size:auto}.form-control:placeholder-shown+.input-clear{display:none}.server-box>.form-control{padding-inline-end:2rem}#option-group:hover svg,.input-clear:hover{opacity:1}#option-group div{margin-inline-start:-3px;margin-block:3px 1rem}.login-button{display:flex;align-items:center;justify-content:center;block-size:2.5rem;flex-basis:100%}#hostkey-group:not([hidden])~.login-actions>.login-button{flex-basis:max-content}.pf-v5-c-button[disabled]{background-color:#333;background-image:none;border-color:#555;cursor:default}@media (max-width: 480px){.login-pf{display:flex;flex-direction:column-reverse;position:relative}.row{display:flex;flex-direction:column}.login-pf .container{inline-size:100%;padding:2rem}.login-pf #badge{max-inline-size:calc(100vw - 2rem)}.login-pf #brand{font-size:inherit;background-position:50% 50%}.details{text-align:center}}@media (min-width: 481px){.unsupported-browser-heading{margin-block:2rem 1rem;margin-inline:0}.login-pf #brand{padding:0;margin-block-end:1rem}}@media (min-width: 1024px){.control-label{text-align:end}.login-pf #brand img{margin:0;text-align:start}.login-pf .container .login-area{border-inline-end:1px solid #474747}}.row{display:flex;flex-flow:column;margin:0}body.login-pf{color:#151515;display:grid;grid-template-areas:"banner" "login" "details" "recent" "logo";grid-template-rows:repeat(3,auto) 1fr;background-position:0 0!important;padding:0}.login-pf .container .details p:first-child{border-block-start:0;padding-block-start:0;margin-block-start:0}.login-button{inline-size:100%}.login-pf .details{grid-area:details;padding-block:1.5rem;padding-inline:3rem;inline-size:100%;background:#ededed}#recent-hosts{grid-area:recent;margin-block-start:1rem;padding-block:3rem 1.5rem;padding-inline:1.5rem}#recent-hosts>h1{padding-inline-start:1.5rem}.login-pf #badge{grid-area:logo}.login-pf .container .login-area{border:none;inline-size:auto}details>summary{display:block;padding-block:.5rem;padding-inline:0;cursor:pointer}details>summary:hover{text-decoration:underline}details[open]>summary>.caret{transform:rotate(90deg) translate(-3px)}.login-pf #brand{text-transform:initial}#brand strong,#brand b{font-weight:600}.login-pf #brand.text-brand{inline-size:auto;background:none}a:focus,a:hover{color:#004080}.caret polygon{fill:#06c}.host-line{display:grid;grid:1fr/1fr auto;border:1px solid #d2d2d2;border-width:1px 0}.host-line+.host-line{border-block-start-width:0}.host-name{flex:auto;text-align:start;color:#06c;border:none;padding-block:.5rem;padding-inline:1.5rem 1rem}.host-name:hover{color:#004080;text-decoration:underline}.host-remove{background:transparent;margin-inline-end:1.5rem;padding-block:.375rem;padding-inline:1rem;position:relative}.host-remove:before{content:"";display:block;position:absolute;inset:0;background-color:#151515;border:none;mask:center/contain no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 352 512'%3E%3Cpath d='m242.7 256 100-100a31.5 31.5 0 0 0 0-44.5l-22.2-22.3a31.5 31.5 0 0 0-44.4 0L176 189.2 76 89.3a31.5 31.5 0 0 0-44.5 0L9.2 111.5a31.5 31.5 0 0 0 0 44.4l100 100.1-100 100a31.5 31.5 0 0 0 0 44.5l22.3 22.3a31.5 31.5 0 0 0 44.4 0l100.1-100 100 100a31.5 31.5 0 0 0 44.5 0l22.3-22.2a31.5 31.5 0 0 0 0-44.5L242.8 256z'/%3E%3C/svg%3E");mask-size:1rem 1rem;opacity:.6}.host-remove:focus:before,.host-remove:hover:before{opacity:1}@media (max-width: 1023px){body.login-pf{justify-items:center}#badge{background-position:center;margin:2rem}}@media (min-width: 1024px){body.login-pf{grid-template-areas:". banner banner ." ". . . ." ". login logo ." ". details logo ." ". recent logo ." ". . . .";grid-template-rows:auto 1fr minmax(min-content,max-content) repeat(2,auto) 1fr;grid-template-columns:minmax(0,1fr) repeat(2,minmax(auto,34rem)) minmax(0,1fr);grid-gap:0 4rem}}.unsupported-browser ul{color:#555;margin-block:0 1rem;margin-inline:0}.browser-recommendations{margin-block:1rem 0;margin-inline:0}.browser-recommendations h3{font:inherit;margin-block-start:0}.dialog-error{margin-block:0 1rem;margin-inline:0}#login-again{display:block;margin-block-start:1rem}.password-with-toggle{display:grid;grid-template-columns:1fr auto}.pf-v5-c-button.login-password-toggle{display:flex;align-items:center;border-inline-start-width:0}.login-password-toggle>svg{block-size:1rem;inline-size:auto}input[type=text]+.login-password-toggle .password-show,input[type=password]+.login-password-toggle .password-hide{display:none}.pf-v5-c-helper-text{margin-block-start:1rem}.pf-v5-c-helper-text.pf-m-warning{color:#795600}.pf-v5-theme-dark .login-pf .container,.pf-v5-theme-dark .login-pf .details{z-index:2}.pf-v5-theme-dark .login-pf .container{background:#26292d;color:#f0f0f0}.pf-v5-theme-dark .login-pf .details{background:#151515;color:#f0f0f0}.pf-v5-theme-dark .pf-v5-c-button.pf-m-control,.pf-v5-theme-dark .form-control[type=password],.pf-v5-theme-dark .form-control[type=text]{background:#393f44;border-color:#393f44;border-block-end-color:#6c6f72;color:#f0f0f0}.pf-v5-theme-dark a:active,.pf-v5-theme-dark a:focus,.pf-v5-theme-dark a:hover,.pf-v5-theme-dark a,.pf-v5-theme-dark summary{color:#8ac0f6}.pf-v5-theme-dark .input-clear,.pf-v5-theme-dark .inline .container .help-block{color:#f0f0f0}.pf-v5-theme-dark .host-line{border-color:#34373b}.pf-v5-theme-dark .host-remove:focus{outline-color:#f1f1f1}.pf-v5-theme-dark .host-remove:before{background-color:#f1f1f1}.pf-v5-theme-dark .unsupported-browser ul{color:#999}.pf-v5-theme-dark .pf-v5-c-alert.pf-m-inline.pf-m-danger{background:#1e2125;border-color:#fe5142}.pf-v5-theme-dark .pf-v5-c-alert.pf-m-danger>svg,.pf-v5-theme-dark .pf-v5-c-alert.pf-m-danger .pf-v5-c-alert__title{color:#fe5142}.pf-v5-theme-dark .pf-v5-c-button[disabled]{background-color:#444548;border-color:#444548;color:#c6c7c8}.pf-v5-theme-dark .login-pf:after{pointer-events:none;background:#000;opacity:.66;display:block;content:"";position:absolute;inset:0;z-index:1}.pf-v5-theme-dark .pf-v5-c-helper-text.pf-m-warning{color:#f0ab00}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(359deg)}}
diff --git a/dist/static/login.css.LEGAL.txt b/dist/static/login.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/static/login.css.LEGAL.txt
diff --git a/dist/static/login.html b/dist/static/login.html
new file mode 100644
index 0000000..d912019
--- /dev/null
+++ b/dist/static/login.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Loading...</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="robots" content="noindex" />
+ <meta insert="dynamic_content_here" />
+ <script type="text/javascript">/*insert_translations_here*/</script>
+ <script type="text/javascript" src="cockpit/static/login.js"></script>
+ <link href="cockpit/static/login.css" type="text/css" rel="stylesheet" />
+ <link href="cockpit/static/branding.css" type="text/css" rel="stylesheet" />
+</head>
+
+<body class="login-pf">
+ <div id="banner" class="pf-v5-c-alert pf-m-info pf-m-inline dialog-error" aria-label="inline danger alert" hidden="true">
+ <svg fill="currentColor" viewBox="0 0 448 512" aria-hidden="true">
+ <path d="M224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64zm215.39-149.71c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71z" />
+ </svg>
+ <span id="banner-message" class="pf-v5-c-alert__title"></span>
+ </div>
+
+ <span id="badge"></span>
+
+ <div class="container" id="main">
+ <h1 id="brand" class="hide-before"></h1>
+
+ <div id="error-group" class="pf-v5-c-alert pf-m-danger pf-m-inline dialog-error noscript" aria-label="inline danger alert">
+ <svg fill="currentColor" viewBox="0 0 512 512" aria-hidden="true">
+ <path d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h28.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z" />
+ </svg>
+ <h2 id="login-error-message" class="pf-v5-c-alert__title">
+ <span class="noscript" translate="yes">Please enable JavaScript to use the Web Console.</span>
+ </h2>
+ </div>
+
+ <div class="unsupported-browser" id="unsupported-browser" hidden="true">
+ <h2 class="unsupported-browser-heading" translate="yes">A modern browser is required for security, reliability, and performance.</h2>
+ <div class="browser-recommendations">
+ <div class="browser-download">
+ <h3 translate="yes">Download a new browser for free</h3>
+ <ul>
+ <li><a href="https://firefox.com/">Mozilla Firefox</a> / Linux, Windows, macOS</li>
+ <li><a href="https://www.google.com/chrome/">Google Chrome</a> / Linux, Windows, macOS</li>
+ </ul>
+ </div>
+ <div class="browser-bundled">
+ <h3 translate="yes">Or use a bundled browser</h3>
+ <ul>
+ <li><a href="https://www.microsoft.com/">Microsoft Edge</a> / Windows</li>
+ <li><a href="https://www.apple.com/safari/">Apple Safari</a> / macOS</li>
+ </ul>
+ </div>
+ </div>
+ <details id="login-override">
+ <summary class="pf-v5-c-expandable-section">
+ <svg height="16" width="16" viewBox="0 0 16 16" id="option-caret" class="caret caret-right" aria-hidden="true">
+ <polygon fill="#ffffff" points="4,0 4,14 12,7"></polygon>
+ </svg>
+ <span id="bypass-browser-check" translate="yes">Bypass browser check</span>
+ </summary>
+ <div id="login-override-content"></div>
+ </details>
+ </div>
+
+ <div id="info-group" class="pf-v5-c-alert pf-m-info pf-m-inline dialog-error" aria-label="inline danger alert" hidden="true">
+ <svg fill="currentColor" viewBox="0 0 512 512" aria-hidden="true">
+ <path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z" />
+ </svg>
+ <h2 id="login-info-message" class="pf-v5-c-alert__title"></h2>
+ </div>
+
+ <div id="login" class="login-area" hidden="true">
+ <form onsubmit="return false">
+
+ <div id="hostkey-group" class="form-group" hidden="true">
+ <h1 id="hostkey-title"></h1>
+ <div id="hostkey-warning-group" class="pf-v5-c-alert pf-m-warning pf-m-inline dialog-error" aria-label="inline warning alert" hidden="true">
+ <svg fill="currentColor" viewBox="0 0 576 512" aria-hidden="true"><path d="M569.52 440.01c18.46 32-4.71 71.99-41.58 71.99H48.05c-36.93 0-60-40.05-41.57-71.99L246.42 24c18.47-32.01 64.72-31.96 83.16 0L569.52 440zM288 354a46 46 0 100 92 46 46 0 000-92zm-43.67-165.35l7.41 136A12 12 0 00263.74 336h48.54a12 12 0 0011.98-11.35l7.42-136A12 12 0 00319.7 176h-63.38a12 12 0 00-11.98 12.65z"/></svg>
+ <h2 translate="yes" class="pf-v5-c-alert__title">Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.</h2>
+ </div>
+ <p id="hostkey-message-1"></p>
+ <p translate="yes">To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:</p>
+ <pre id="hostkey-fingerprint"></pre>
+ <p id="hostkey-type"></p>
+ <p id="hostkey-verify-help-1"></p>
+ <pre id="hostkey-verify-help-cmds"></pre>
+ <p translate="yes">The resulting fingerprint is fine to share via public methods, including email.</p>
+ <p translate="yes">If the fingerprint matches, click "Accept key and log in". Otherwise, do not log in and contact your administrator.</p>
+ </div>
+
+ <div id="user-group" class="form-group">
+ <label for="login-user-input" class="control-label" translate="yes">User name</label>
+ <input type="text" class="form-control" id="login-user-input" autocorrect="off" autocapitalize="none" autofocus="true" autocomplete="username" />
+ </div>
+
+ <div id="password-group" class="form-group">
+ <label for="login-password-input" class="control-label" translate="yes">Password</label>
+ <div class="password-with-toggle">
+ <input type="password" class="form-control" id="login-password-input" autocomplete="current-password" />
+ <button type="button" id="login-password-toggle" class="pf-v5-c-button pf-m-control login-password-toggle" aria-label="Show password">
+ <svg fill="currentColor" aria-hidden="true" viewBox="0 0 640 512">
+ <path class="password-show" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"/>
+ <path class="password-hide" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"/>
+ </svg>
+ </button>
+ </div>
+ </div>
+
+ <div id="conversation-group" class="form-group" hidden="true">
+ <div id="conversation-message"></div>
+ <label id="conversation-prompt" for="conversation-input"></label>
+ <input type="password" class="form-control" id="conversation-input" autocomplete="one-time-code" />
+ </div>
+
+ <div id="option-group">
+ <a href="#" id="show-other-login-options">
+ <svg height="16" width="16" viewBox="0 0 16 16" id="option-caret" class="caret caret-right" aria-hidden="true">
+ <polygon fill="#ffffff" points="4,0 4,14 12,7"></polygon>
+ </svg><span id="show-other-login-options-text" translate="yes">Other options</span>
+ <!-- Above: Span needs to be immediately next to SVG to prevent whitespace in the link -->
+ </a>
+ </div>
+
+ <div id="server-group" class="form-group" hidden="true">
+ <label title="Log in to another system. Leave blank to log in to the local system." for="server-field" id="server-field-label" class="control-label" translate="yes">Connect to</label>
+ <div class="server-box">
+ <input type="text" class="form-control" id="server-field" placeholder=" " />
+ <span class="input-clear" id="server-clear" aria-hidden="true">
+ <svg fill="currentColor" viewBox="0 0 352 512"><path d="m242.72 256 100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.29 12.28-32.19 0-44.48z"/></svg>
+ </span>
+ </div>
+ </div>
+
+ <div class="form-group login-actions">
+ <button class="pf-v5-c-button pf-m-primary login-button" id="login-button" type="submit">
+ <span class="spinner"></span>
+ <span id="login-button-text" class="button-text" translate="yes">Log in</span>
+ </button>
+ <a id="get-out-link" href="/" translate="yes">Cancel</a>
+ </div>
+ </form>
+ </div>
+
+ <div id="login-wait-validating" hidden="true">
+ <span class="help-block" translate="yes">Validating authentication token</span>
+ <span class="spinner col-xs-15"></span>
+ </div>
+
+ <div id="login-fatal" hidden="true">
+ <div id="login-fatal-message"></div>
+ <a id="login-again" class="pf-v5-c-button pf-m-primary" href="#" translate="yes" hidden="true">Try again</a>
+ </div>
+ </div>
+
+ <div class="details" id="login-details" hidden="true">
+ <p>
+ <label class="control-label"><span translate="yes">Server</span>: <b id="server-name"></b></label>
+ </p>
+ <p id="login-note" class="login-note"></p>
+ </div>
+
+ <div class="container" id="recent-hosts" hidden="true">
+ <h1 translate="yes">Recent hosts</h1>
+ <div class="server-box" id="recent-hosts-list">
+ <!-- Here show up recent hosts -->
+ </div>
+ </div>
+
+ <script type="text/javascript">
+ /* Hide everything classed as "noscript" */
+ document.querySelectorAll('.noscript').forEach(function(element){
+ element.hidden = true;
+ });
+ </script>
+</body>
+</html>
diff --git a/dist/static/login.js b/dist/static/login.js
new file mode 100644
index 0000000..cdbaa38
--- /dev/null
+++ b/dist/static/login.js
@@ -0,0 +1 @@
+(()=>{(function(h){let c;try{c=window.localStorage,window.localStorage.removeItem("url-root"),window.localStorage.removeItem("standard-login")}catch(e){c=window.sessionStorage,h.warn(String(e))}let T=c.getItem("shell:style")||"auto";window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches&&T==="auto"||T==="dark"?document.documentElement.classList.add("pf-v5-theme-dark"):document.documentElement.classList.remove("pf-v5-theme-dark"),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",e=>{e.matches&&T==="auto"||T==="dark"?document.documentElement.classList.add("pf-v5-theme-dark"):document.documentElement.classList.remove("pf-v5-theme-dark")});let g,l=window.environment||{},u=l.OAuth||null;u&&(u.TokenParam||(u.TokenParam="access_token"),u.ErrorParam||(u.ErrorParam="error_description"));let V=/\$\{([^}]+)\}|\$([a-zA-Z0-9_]+)/g;function v(e){let t=Array.prototype.slice.call(arguments,1);return e.replace(V,function(i,n,r){return t[n||r]||""})}function q(e){if(window.cockpit_po){let t=window.cockpit_po[e];if(t&&t[1])return t[1]}return e}function Y(){if(!document.querySelectorAll)return;let e=document.querySelectorAll("[translate]");for(let t=0;t<e.length;t++)e[t].textContent=q(e[t].textContent)}let s=q,y,m,A,S,Z=/[?&]?([^=]+)=([^&]*)/g,x=null;function P(e){e=e.split("+").join(" ");let t={};for(;;){let i=Z.exec(e);if(!i)break;t[decodeURIComponent(i[1])]=decodeURIComponent(i[2])}return t}h||(h=function(){});function o(e){return document.getElementById(e)}function k(e,t){typeof e=="string"&&(e=[e]);for(let i=0;i<e.length;i++)if(typeof e[i]=="string"){let n=document.querySelectorAll(e[i]);n&&n.forEach(function(r){r.hidden!==!!t&&(r.hidden=!!t)})}else e[i].hidden!==!!t&&(e[i].hidden=!!t)}function f(){k(arguments,!1)}function d(){k(arguments,!0)}function K(e){window.console&&h.warn("stderr:",e),d("#login-wait-validating"),d("#login","#login-details"),f("#login-fatal"),o("login-again").onclick=()=>{d("#login-fatal"),z()},f("#login-again");let t=o("login-fatal-message");t.textContent="",t.appendChild(document.createTextNode(e))}function w(e){window.console&&h.warn("fatal:",e),d("#login-again","#login-wait-validating"),x&&(o("login-again").href=x,f("#login-again")),d("#login","#login-details"),f("#login-fatal");let t=o("login-fatal-message");t.textContent="",t.appendChild(document.createTextNode(e))}function E(e,t){let i=o(e),n=i&&window.getComputedStyle?window.getComputedStyle(i,":before"):null;if(!n)return;let r=n.content;if(r&&r!="none"&&r!="normal"){let a=r.length;(r[0]==='"'||r[0]==="'")&&a>2&&r[a-1]===r[0]&&(r=r.substr(1,a-2)),i.innerHTML=r||t}else i.removeAttribute("class")}function ee(){function e(a){a?(f("#login","#login-details","#login-override"),d("#get-out-link"),o("login-override-content").appendChild(o("login")),o("login-button").classList.add("pf-m-warning"),document.querySelector("#login .login-actions").insertAdjacentHTML("beforebegin","<div class='pf-v5-c-helper-text pf-m-warning' id='bypass-warning'>"+s("Cockpit might not render correctly in your browser")+"</div>")):d("#login","#login-details","#login-override")}function t(a,b){a==="supports"&&(a="@supports API");let _=v(s("This web browser is too old to run the Web Console (missing $0)"),a);window.console&&h.warn(_),o("login-error-message").textContent=_,f("#unsupported-browser","#error-group"),document.body.classList.add("unsupported-browser"),e(b)}function i(a,b){let _;try{_=b&&b[a]}catch(pe){throw w(v(s("The web browser configuration prevents Cockpit from running (inaccessible $0)"),a)),pe}return _===void 0?(t(a),!1):!0}function n(){let a=[].join.call(arguments,": ");return!window.CSS||!window.CSS.supports.apply(this,arguments)?(t(a,"bypass"),!1):!0}return i("WebSocket",window)&&i("XMLHttpRequest",window)&&i("sessionStorage",window)&&i("JSON",window)&&i("defineProperty",Object)&&i("console",window)&&i("pushState",window.history)&&i("textContent",document)&&i("replaceAll",String.prototype)&&i("finally",Promise.prototype)&&i("supports",window.CSS)?(n("display","flex")&&n("display","grid")&&n("selector(test)")&&n("selector(:is(*):where(*))"),!0):!1}function N(e){return e.replace(/^\s+|\s+$/g,"")}function M(e){let t=document.createElement("a"),i=document.baseURI;e=e||"/",t.href=i,t.pathname!="/"&&(g=t.pathname.replace(/^\/+|\/+$/g,""),c.setItem("url-root",g),g&&e.indexOf("/"+g)===0&&(e=e.replace("/"+g,"")||"/")),e.indexOf("/=")===0?(l.hostname=e.substring(2).split("/")[0],o("server-field").value=l.hostname,C(null,!0),e="/cockpit+"+e.split("/")[1]):e.indexOf("/cockpit/")!==0&&e.indexOf("/cockpit+")!==0&&(e="/cockpit"),m=e.split("/")[1],y="/"+m+"/login",g&&(y="/"+g+y),S=m,A=y}function C(e,t){e&&e.type==="keypress"&&e.key!==" "||(e&&e.type==="click"&&e.preventDefault(),t===void 0&&(t=o("server-group").hidden),k("#server-group",!t),o("option-group").setAttribute("data-state",t))}function te(e){let t=o("login-password-input");t.setAttribute("type",t.getAttribute("type")==="password"?"text":"password"),e.stopPropagation()}function oe(){window.onload=null,Y(),window.cockpit_po&&window.cockpit_po[""]&&(document.documentElement.lang=window.cockpit_po[""].language,window.cockpit_po[""]["language-direction"]&&(document.documentElement.dir=window.cockpit_po[""]["language-direction"])),M(window.location.pathname),(window.location.pathname.indexOf("/"+g+"/cockpit/")===0||window.location.pathname.indexOf("/"+g+"/cockpit+")===0)&&document.documentElement.setAttribute("class","inline");let e=l.page.title;if(l.is_cockpit_client&&(e=s("Login")),(!e||m.indexOf("cockpit+=")===0)&&(e=l.hostname),document.title=e,m.indexOf("cockpit+=")===0?d("#brand","#badge"):(E("badge",""),E("brand","Cockpit")),!ee())return;l.banner&&(f("#banner"),o("banner-message").textContent=l.banner.trimEnd()),o("bypass-browser-check").addEventListener("click",C),o("bypass-browser-check").addEventListener("keypress",C),o("show-other-login-options").addEventListener("click",C),o("show-other-login-options").addEventListener("keypress",C),o("server-clear").addEventListener("click",function(){let n=o("server-field");n.value="",n.focus()});let t=window.sessionStorage.getItem("logout-intent")=="explicit";t&&window.sessionStorage.removeItem("logout-intent");let i=window.sessionStorage.getItem("logout-reason");i&&window.sessionStorage.removeItem("logout-reason"),u?(d("#login-details","#login"),t?($(),o("login-again").textContent=s("Login again"),w(s("Logout successful"))):ie()):t?z(i):X()?z():ne()}function ne(){let e=new XMLHttpRequest;e.open("GET",y,!0),e.onreadystatechange=function(){e.readyState==4&&(e.status==200?U(JSON.parse(e.responseText)):e.status==401?z():e.statusText?w(decodeURIComponent(e.statusText)):e.status===0?z():w(v(s("$0 error"),e.status)))},e.send()}function $(){let e=window.location.href.split("#",2);x=u.URL,u.URL.indexOf("?")>-1?x+="&":x+="?",x+="redirect_uri="+encodeURIComponent(e[0])}function ie(){let e=document.createElement("a");if(!u.URL)return w(s("Cockpit authentication is configured incorrectly."));let t=!window.location.search&&window.location.hash?P(window.location.hash.slice(1)):P(window.location.search);if($(),t[u.TokenParam]){window.sessionStorage.getItem("login-wanted")&&(e.href=window.sessionStorage.getItem("login-wanted"),M(e.pathname));let i=t[u.TokenParam];f("#login-wait-validating");let n=new XMLHttpRequest;n.open("GET",y,!0),n.setRequestHeader("Authorization","Bearer "+i),n.onreadystatechange=function(){if(n.readyState==4)if(n.status==200)U(JSON.parse(n.responseText));else{let r=F(n.getResponseHeader("WWW-Authenticate"),n.responseText);r?j(r):w(decodeURIComponent(n.statusText))}},n.send()}else t[u.ErrorParam]?w(t[u.ErrorParam]):(window.sessionStorage.setItem("login-wanted",window.location.href),window.location=x)}function J(){d("#error-group"),o("login-error-message").textContent=""}function W(){d("#info-group"),o("login-info-message").textContent=""}function p(e,t){J(),e&&(u?w(e):(L(t||"login"),o("login-error-message").textContent=e,f("#error-group")))}function re(e){W(),e&&(o("login-info-message").textContent=e,f("#info-group"))}function I(e){o("server-field").value?(J(),o("login-error-message").textContent=e,f("#error-group"),C(null,!0),L("login")):p(e)}function ae(e){let t=o("login-note");e?(f(t),t.textContent=e):t.innerHTML="&nbsp;"}function X(){return l.page.require_host&&S.indexOf("cockpit+=")===-1}function B(){let e=[];try{e=JSON.parse(c.getItem("cockpit-client-sessions")||"[]")}catch(t){h.log("Failed to parse 'cockpit-client-sessions':",t)}return e}function O(){p(null);let e=N(o("login-user-input").value);if(e===""&&!l.is_cockpit_client)p(s("User name cannot be empty"));else if(X()&&o("server-field").value==="")p(s("Please specify the host to connect to"));else{let t=o("server-field").value;t?(m="cockpit+="+t,y=A.replace("/"+S+"/","/"+m+"/"),o("brand").style.display="none",o("badge").style.visibility="hidden"):(m=S,y=A,E("badge",""),E("brand","Cockpit")),o("server-name").textContent=t||l.hostname,o("login-button").removeEventListener("click",O);let i=o("login-password-input").value,n="superuser:"+e+(t?":"+t:""),r=c.getItem(n)||"none";c.setItem("superuser-key",n),c.setItem(n,r),c.setItem("standard-login",!0);let a={Authorization:"Basic "+window.btoa(D(e+":"+i)),"X-Superuser":r};t&&(a["X-SSH-Connect-Unknown-Hosts"]="yes"),Q("GET",a,!1)}}function G(){let e=B(),t=o("recent-hosts-list");t.innerHTML="",e.forEach(i=>{let n=document.createElement("div");n.classList.add("host-line");let r=document.createElement("button");r.textContent=i,r.classList.add("pf-v5-c-button","pf-m-tertiary","host-name"),r.addEventListener("click",()=>{o("server-field").value=i,O()});let a=document.createElement("button");a.title=s("Remove host"),a.ariaLabel=a.title,a.classList.add("host-remove"),a.addEventListener("click",()=>{let b=e.indexOf(i);e.splice(b,1),c.setItem("cockpit-client-sessions",JSON.stringify(e)),G()}),n.append(r,a),t.append(n)}),k("#recent-hosts",e.length==0)}function L(e){let t=l.page.connect,i=o("option-group").getAttribute("data-state");if(d("#login-wait-validating"),f("#login"),k("#login-details",l.is_cockpit_client),k("#server-field-label",l.is_cockpit_client),l.is_cockpit_client){let n=o("brand");n.textContent=s("Connect to:"),n.classList.add("text-brand")}k(["#user-group","#password-group"],e!="login"||l.is_cockpit_client),k("#conversation-group",e!="conversation"),k("#hostkey-group",e!="hostkey"),o("login-button-text").textContent=e=="hostkey"?s("Accept key and log in"):s("Log in"),e!="login"&&(o("login-password-input").value=""),l.page.require_host?(d("#option-group"),i=!0):k("#option-group",!t||e!="login"),!t||e!="login"?d("#server-group"):k("#server-group",!i),o("login-button").removeAttribute("disabled"),o("login-button").removeAttribute("spinning"),o("login-button").classList.remove("pf-m-danger"),o("login-button").classList.add("pf-m-primary"),d("#get-out-link"),e=="login"&&o("login-button").addEventListener("click",O),l.is_cockpit_client&&(G(),document.body.classList.add("cockpit-client"))}function z(e){re(e),o("server-name").textContent=document.title,ae(s("Log in with your server user account.")),o("login-user-input").addEventListener("keydown",function(i){p(null),W(),i.which==13&&o("login-password-input").focus()},!1);let t=function(i){p(null),i.which==13&&O()};o("login-password-input").addEventListener("keydown",t),o("login-password-toggle").addEventListener("click",te),L("login"),l.is_cockpit_client?l.page.require_host&&o("server-field").focus():o("login-user-input").focus()}function se(){try{return JSON.parse(c.getItem("known_hosts")||"{ }")}catch(e){return h.warn("Can't parse known_hosts database in localStorage",e),{}}}function le(e){try{c.setItem("known_hosts",JSON.stringify(e))}catch(t){h.warn("Can't write known_hosts database to localStorage",t)}}function ce(e){let t=se(),i=e["host-key"],n=i.split(" ")[0],r=i.split(" ")[1];if(t[n]==i){R(e.id,e.default);return}t[n]?(o("hostkey-title").textContent=v(s("$0 key changed"),o("server-field").value),f("#hostkey-warning-group"),o("hostkey-message-1").textContent=""):(o("hostkey-title").textContent=s("New host"),d("#hostkey-warning-group"),o("hostkey-message-1").textContent=v(s("You are connecting to $0 for the first time."),o("server-field").value)),o("hostkey-verify-help-1").textContent=v(s("To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:"),o("server-field").value),o("hostkey-verify-help-cmds").textContent=v("ssh-keyscan$0 localhost | ssh-keygen -lf -",r?" -t "+r:""),o("hostkey-fingerprint").textContent=e.default,r?(o("hostkey-type").textContent=v("($0)",r),f("#hostkey-type")):d("#hostkey-type"),p("");function a(){o("login-button").removeEventListener("click",a),p(null,"hostkey"),t[n]=i,le(t),R(e.id,e.default)}o("login-button").addEventListener("click",a),L("hostkey"),f("#get-out-link"),t[n]&&(o("login-button").classList.add("pf-m-danger"),o("login-button").classList.remove("pf-m-primary"))}function j(e){if(e["host-key"]){ce(e);return}let t=e.echo?"text":"password";o("conversation-prompt").textContent=e.prompt;let i=o("conversation-message"),n=e.error||e.message;n?(i.textContent=n,f(i)):d(i);let r=o("conversation-input");r.value="",e.default&&(r.value=e.default),r.setAttribute("type",t),p("");function a(){o("conversation-input").removeEventListener("keydown",b),o("login-button").removeEventListener("click",a),p(null,"conversation"),R(e.id,o("conversation-input").value)}function b(_){p(null,"conversation"),_.which==13&&a()}o("conversation-input").addEventListener("keydown",b),o("login-button").addEventListener("click",a),L("conversation"),r.focus()}function D(e){return window.unescape(encodeURIComponent(e))}function F(e,t){if(!e)return null;let i=e.split(" ");if(i[0].toLowerCase()!=="x-conversation"&&i.length!=3)return null;let n=i[1],r;try{r=window.atob(i[2])}catch(b){return window.console&&h.error("Invalid prompt data",b),null}let a;try{a=JSON.parse(t)}catch(b){window.console&&h.log("Got invalid JSON response for prompt data",b),a={}}return a.id=n,a.prompt=r,a}function Q(e,t,i){o("login-button").setAttribute("disabled","true"),o("login-button").setAttribute("spinning","true");let n=new XMLHttpRequest;n.open(e,y,!0);for(let r in t)n.setRequestHeader(r,t[r]);n.onreadystatechange=function(){if(n.readyState==4)if(n.status==200){let r=JSON.parse(n.responseText);U(r)}else if(n.status==401){let r=n.getResponseHeader("WWW-Authenticate");if(r&&r.toLowerCase().indexOf("x-conversation")===0){let a=F(r,n.responseText);a?j(a):w(s("Internal error: Invalid challenge header"))}else if(window.console&&h.log(n.statusText),n.statusText.startsWith("captured-stderr:"))K(decodeURIComponent(n.statusText.replace(/^captured-stderr:/,"")));else if(n.statusText.indexOf("authentication-not-supported")>-1){let a=N(o("login-user-input").value);w(v(s("The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available."),a))}else n.statusText.indexOf("terminated")>-1?p(s("Authentication failed: Server closed connection")):n.statusText.indexOf("no-host")>-1?I(s("Unable to connect to that address")):n.statusText.indexOf("unknown-hostkey")>-1?I(s("Refusing to connect. Hostkey is unknown")):n.statusText.indexOf("unknown-host")>-1?I(s("Refusing to connect. Host is unknown")):n.statusText.indexOf("invalid-hostkey")>-1?I(s("Refusing to connect. Hostkey does not match")):p(s(i?"Authentication failed":"Wrong user name or password"))}else n.status==403?p(s(decodeURIComponent(n.statusText))||s("Permission denied")):n.statusText?w(decodeURIComponent(n.statusText)):w(v(s("$0 error"),n.status))},n.send()}function R(e,t){let i={Authorization:"X-Conversation "+e+" "+window.btoa(D(t))};Q("GET",i,!0)}function de(e){let t=window.setTimeout(function(){t=null,window.location.reload(!0)},100);e&&e!=window.location.href&&(window.location=e),window.onbeforeunload=function(){t&&window.clearTimeout(t),t=null}}function H(e,t,i){let n=0;for(;n<e.length;){let r=e.key(n);i&&r.indexOf("cockpit")!==0||r.indexOf(t)===0?e.removeItem(r):n++}}function fe(e){if(H(window.sessionStorage,m,!0),c.removeItem("login-data"),H(c,m,!1),e&&e["login-data"]){let i=JSON.stringify(e["login-data"]);c.setItem(m+"login-data",i),c.setItem("login-data",i)}g&&c.setItem("url-root",g);let t=l.CACertUrl;t&&window.sessionStorage.setItem("CACertUrl",t)}function U(e){let t=window.sessionStorage.getItem("login-wanted"),i=o("server-field").value;if(i&&l.is_cockpit_client){let n=B();n.indexOf(i)<0&&(n.push(i),c.setItem("cockpit-client-sessions",JSON.stringify(n)))}i&&m!=S&&(t="/="+i,g&&(t="/"+g+t)),H(window.sessionStorage,m,!1),fe(e),de(t)}window.onload=oe})(window.console);})();
diff --git a/dist/static/login.js.LEGAL.txt b/dist/static/login.js.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/static/login.js.LEGAL.txt
diff --git a/dist/static/manifest.json b/dist/static/manifest.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/dist/static/manifest.json
@@ -0,0 +1 @@
+{}
diff --git a/dist/static/po.cs.js b/dist/static/po.cs.js
new file mode 100644
index 0000000..3fa9929
--- /dev/null
+++ b/dist/static/po.cs.js
@@ -0,0 +1,967 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2,
+ "language": "cs",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 den",
+ "$0 dny",
+ "$0 dnů"
+ ],
+ "$0 error": [
+ null,
+ "$0 chyba"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 skončilo s kódem $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 se nezdařilo"
+ ],
+ "$0 hour": [
+ null,
+ "$0 hodina",
+ "$0 hodiny",
+ "$0 hodin"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 není k dispozici z žádného z repozitářů."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 klíč změněn"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 vynuceně ukončeno signálem $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minuta",
+ "$0 minuty",
+ "$0 minut"
+ ],
+ "$0 month": [
+ null,
+ "$0 měsíc",
+ "$0 měsíce",
+ "$0 měsíců"
+ ],
+ "$0 week": [
+ null,
+ "$0 týden",
+ "$0 týdny",
+ "$0 týdnů"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 bude nainstalováno."
+ ],
+ "$0 year": [
+ null,
+ "$0 rok",
+ "$0 roky",
+ "$0 let"
+ ],
+ "1 day": [
+ null,
+ "1 den"
+ ],
+ "1 hour": [
+ null,
+ "1 hodina"
+ ],
+ "1 minute": [
+ null,
+ "1 minuta"
+ ],
+ "1 week": [
+ null,
+ "1 týden"
+ ],
+ "20 minutes": [
+ null,
+ "20 minut"
+ ],
+ "40 minutes": [
+ null,
+ "40 minut"
+ ],
+ "5 minutes": [
+ null,
+ "5 minut"
+ ],
+ "6 hours": [
+ null,
+ "6 hodin"
+ ],
+ "60 minutes": [
+ null,
+ "60 minut"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Z důvodu zabezpečení, spolehlivosti a rychlosti je zapotřebí moderní prohlížeč."
+ ],
+ "Absent": [
+ null,
+ "Chybí"
+ ],
+ "Accept key and log in": [
+ null,
+ "Přijmout klíč a přihlásit se"
+ ],
+ "Acceptable password": [
+ null,
+ "Přijatelné heslo"
+ ],
+ "Add $0": [
+ null,
+ "Přidat $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Další balíčky:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Správa pomocí webové konzole Cockpit"
+ ],
+ "Advanced TCA": [
+ null,
+ "Pokročilé TCA"
+ ],
+ "All-in-one": [
+ null,
+ "Vše-v-jednom"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Dokumentace k Ansible rolím"
+ ],
+ "Authentication failed": [
+ null,
+ "Ověření se nezdařilo"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Ověření se nezdařilo: Server přerušil spojení"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Pro provádění privilegovaných úloh pomocí webové konzole Cockpit je třeba ověřit se"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Automatické použití NTP"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Automatické použití dalších NTP serverů"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Automatické použití uvedených NTP serverů"
+ ],
+ "Automation script": [
+ null,
+ "Automatizační skript"
+ ],
+ "Blade": [
+ null,
+ "Blade server"
+ ],
+ "Blade enclosure": [
+ null,
+ "Skříň se šachtami pro blade servery"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Skříň rozšíření sběrnice"
+ ],
+ "Cancel": [
+ null,
+ "Storno"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Nedaří přeposlat přístupové údaje"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Nelze naplánovat událost v minulosti"
+ ],
+ "Change": [
+ null,
+ "Změnit"
+ ],
+ "Change system time": [
+ null,
+ "Změnit systémový čas"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Změněné klíče jsou často výsledkem přeinstalace operačního systému. Nicméně, neočekávaná změna může značit pokus třetí strany o vložení se do vaší komunikace."
+ ],
+ "Checking installed software": [
+ null,
+ "Zjišťuje se nainstalovaný sofware"
+ ],
+ "Close": [
+ null,
+ "Zavřít"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Cockpit ověřování není nastaveno správně."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "Nastavování NetworkManager a Firewalld v pomocí Cockpit"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit se nepodařilo daný stroj kontaktovat."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit je správce serveru, který usnadňuje správu Linuxových serverů prostřednictvím webového prohlížeče. Není žádným problémem přecházet mezi terminálem a webovým nástrojem. Služba spuštěná přes Cockpit může být zastavena v terminálu. Podobně, pokud dojde k chybě v terminálu, je toto vidět v rozhraní žurnálu v Cockpit."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit není kompatibilní se softwarem v systému."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit není v systému nainstalován."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit je ideální nástroj pro nové správce serverů, neboť jim umožňuje snadno provádět jednoduché úkoly, jako je správa úložišť, kontrola žurnálu či spouštění a zastavování služeb. Můžete současně sledovat a spravovat několik serverů najednou. Stačí je přidat jedním kliknutím a vaše stroje se budou starat o své kamarády."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Může se stávat, že ve vámi používaném webovém prohlížeči nebude Cockpit správně zobrazován"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Shromáždit a zabalit data pro diagnostiku a podporu"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Shromáždit výpisy pádů jádra systému"
+ ],
+ "Compact PCI": [
+ null,
+ "Compact PCI"
+ ],
+ "Connect to": [
+ null,
+ "Připojit se k"
+ ],
+ "Connect to:": [
+ null,
+ "Připojit se k:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Překročen časový limit připojení."
+ ],
+ "Convertible": [
+ null,
+ "Počítač 2v1"
+ ],
+ "Copy": [
+ null,
+ "Zkopírovat"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Zkopírovat do schránky"
+ ],
+ "Create": [
+ null,
+ "Vytvořit"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Vytvořit nový soubor s úlohou s tímto obsahem."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Prodleva"
+ ],
+ "Desktop": [
+ null,
+ "Desktop"
+ ],
+ "Detachable": [
+ null,
+ "Odpojitelné"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Diagnostická hlášení"
+ ],
+ "Docking station": [
+ null,
+ "Dokovací stanice"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Zdarma si stáhnout nový prohlížeč"
+ ],
+ "Downloading $0": [
+ null,
+ "Stahuje se $0"
+ ],
+ "Dual rank": [
+ null,
+ "Dual rank"
+ ],
+ "Embedded PC": [
+ null,
+ "Jednodeskový počítač"
+ ],
+ "Excellent password": [
+ null,
+ "Skvělé heslo"
+ ],
+ "Expansion chassis": [
+ null,
+ "Rozšiřující šasi"
+ ],
+ "Failed to change password": [
+ null,
+ "Nepodařilo se změnit heslo"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "Nepodařilo se povolit $0 ve firewalld"
+ ],
+ "Go to now": [
+ null,
+ "Přejít na nyní"
+ ],
+ "Handheld": [
+ null,
+ "Pro držení v rukou"
+ ],
+ "Hide confirmation password": [
+ null,
+ "Skrýt potvrzení hesla"
+ ],
+ "Hide password": [
+ null,
+ "Skrýt heslo"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Klíč stroje není správný"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Pokud se otisk shoduje, klikněte na „Přijmout klíč a přihlásit se“. V opačném případě se nepřipojujte a obraťte se na správce."
+ ],
+ "Install": [
+ null,
+ "Nainstalovat"
+ ],
+ "Install software": [
+ null,
+ "Nainstalovat software"
+ ],
+ "Installing $0": [
+ null,
+ "Instaluje se $0"
+ ],
+ "Internal error": [
+ null,
+ "Vnitřní chyba"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Vnitřní chyba: neplatná hlavička výzvy"
+ ],
+ "Invalid date format": [
+ null,
+ "Neplatný formát data"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Neplatný formát data a času"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Neplatná souborová práva"
+ ],
+ "Invalid time format": [
+ null,
+ "Neplatný formát času"
+ ],
+ "Invalid timezone": [
+ null,
+ "Neplatné časové pásmo"
+ ],
+ "IoT gateway": [
+ null,
+ "Brána Internetu věcí (IoT)"
+ ],
+ "Kernel dump": [
+ null,
+ "Výpis paměti jádra"
+ ],
+ "Laptop": [
+ null,
+ "Notebook"
+ ],
+ "Learn more": [
+ null,
+ "Další informace naleznete"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Načítání modifikací systému…"
+ ],
+ "Log in": [
+ null,
+ "Přihlásit se"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Přihlášení pomocí uživatelského účtu na tomto serveru."
+ ],
+ "Log messages": [
+ null,
+ "Zprávy záznamu událostí"
+ ],
+ "Login": [
+ null,
+ "Přihlásit"
+ ],
+ "Login again": [
+ null,
+ "Znovu přihlásit"
+ ],
+ "Login failed": [
+ null,
+ "Přihlášení se nezdařilo"
+ ],
+ "Logout successful": [
+ null,
+ "Odhlášení úspěšné"
+ ],
+ "Low profile desktop": [
+ null,
+ "Nízký desktop"
+ ],
+ "Lunch box": [
+ null,
+ "Kufříkový počítač"
+ ],
+ "Main server chassis": [
+ null,
+ "Hlavní skříň serveru"
+ ],
+ "Manage storage": [
+ null,
+ "Spravovat úložiště"
+ ],
+ "Manually": [
+ null,
+ "Ručně"
+ ],
+ "Message to logged in users": [
+ null,
+ "Zpráva přihlášeným uživatelům"
+ ],
+ "Mini PC": [
+ null,
+ "Mini PC"
+ ],
+ "Mini tower": [
+ null,
+ "Mini věž"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Skříň pro více systémů"
+ ],
+ "NTP server": [
+ null,
+ "NTP server"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Je třeba alespoň jeden NTP server"
+ ],
+ "Networking": [
+ null,
+ "Síť"
+ ],
+ "New host": [
+ null,
+ "Nový hostitel"
+ ],
+ "New password was not accepted": [
+ null,
+ "Nové heslo nebylo přijato"
+ ],
+ "No delay": [
+ null,
+ "Bez prodlevy"
+ ],
+ "No such file or directory": [
+ null,
+ "Žádný takový soubor nebo složka"
+ ],
+ "No system modifications": [
+ null,
+ "Žádné modifikace systému"
+ ],
+ "Not a valid private key": [
+ null,
+ "Není platná soukromá část klíče"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Neoprávněni k provedení této akce."
+ ],
+ "Not synchronized": [
+ null,
+ "Nesynchronizováno"
+ ],
+ "Notebook": [
+ null,
+ "Notebook"
+ ],
+ "Occurrences": [
+ null,
+ "Výskyty"
+ ],
+ "Ok": [
+ null,
+ "OK"
+ ],
+ "Old password not accepted": [
+ null,
+ "Původní heslo nebylo přijato"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Jakmile bude Cockpit nainstalovaný, zapněte ho pomocí příkazu „systemctl enable --now cockpit.socket“."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Nebo použijte přibalený prohlížeč"
+ ],
+ "Other": [
+ null,
+ "Ostatní"
+ ],
+ "Other options": [
+ null,
+ "Ostatní volby"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit zhavaroval"
+ ],
+ "Password": [
+ null,
+ "Heslo"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Heslo není přijatelné"
+ ],
+ "Password is too weak": [
+ null,
+ "Heslo je příliš slabé"
+ ],
+ "Password not accepted": [
+ null,
+ "Heslo nebylo přijato"
+ ],
+ "Paste": [
+ null,
+ "Vložit"
+ ],
+ "Paste error": [
+ null,
+ "Chyba vkládání"
+ ],
+ "Path to file": [
+ null,
+ "Popis umístění serveru"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Skříň periferií"
+ ],
+ "Permission denied": [
+ null,
+ "Přístup zamítnut"
+ ],
+ "Pick date": [
+ null,
+ "Vyberte datum"
+ ],
+ "Pizza box": [
+ null,
+ "Velikost „krabice od pizzy“"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Pokud chcete používat webovou konzoli, zapněte podporu pro JavaScript."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Zadejte stroj ke kterému se připojit"
+ ],
+ "Portable": [
+ null,
+ "Přenosný"
+ ],
+ "Present": [
+ null,
+ "Přítomno"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Časový limit výzvy prostřednictvím ssh-add překročen"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Časový limit výzvy prostřednictvím ssh-keygen překročen"
+ ],
+ "RAID chassis": [
+ null,
+ "RAID skříň"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Skříň do stojanu"
+ ],
+ "Reboot": [
+ null,
+ "Restartovat"
+ ],
+ "Recent hosts": [
+ null,
+ "Nedávní hostitelé"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Odmítá se připojit. Stroj není znám"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Odmítá se připojit. Klíč stroje neodpovídá"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Odmítá se připojit. Klíč stroje není znám"
+ ],
+ "Removals:": [
+ null,
+ "Odebrání:"
+ ],
+ "Remove host": [
+ null,
+ "Odebrat hostitele"
+ ],
+ "Removing $0": [
+ null,
+ "Odebírá se $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "Počítač se zapečetěnou skříní"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Nastavení SELinux a řešení problémů"
+ ],
+ "Server": [
+ null,
+ "Server"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Server zavřel spojení."
+ ],
+ "Set time": [
+ null,
+ "Nastavit čas"
+ ],
+ "Shell script": [
+ null,
+ "Shellový skript"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Show confirmation password": [
+ null,
+ "Zobrazit potvrzení hesla"
+ ],
+ "Show password": [
+ null,
+ "Zobrazit heslo"
+ ],
+ "Shut down": [
+ null,
+ "Vypnout"
+ ],
+ "Single rank": [
+ null,
+ "Single rank"
+ ],
+ "Space-saving computer": [
+ null,
+ "Prostorově úsporný počítač"
+ ],
+ "Specific time": [
+ null,
+ "Konkrétní čas"
+ ],
+ "Stick PC": [
+ null,
+ "Počítač v klíčence"
+ ],
+ "Storage": [
+ null,
+ "Úložiště"
+ ],
+ "Strong password": [
+ null,
+ "Odolné heslo"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Zmenšená skříň"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Zmenšený notebook"
+ ],
+ "Synchronized": [
+ null,
+ "Synchronizováno"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Synchronizováno s $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Synchronizuje se"
+ ],
+ "Tablet": [
+ null,
+ "Tablet"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "Přihlášený uživatel není oprávněn zobrazovat modifikace systému"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Zadání hesla se neshodují."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "Výsledný otisk je možné sdílet veřejnými způsoby, včetně e-mailu."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Server odmítl ověřit „$0“ pomocí ověření heslem a nejsou k dispozici žádné další metody ověření."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Server odmítl ověřit u všech podporovaných metod."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Nastavení webového prohlížeče brání ve spuštění Cockpit (nepřístupné $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "Tento nástroj nastavuje pravidla pro SELinux a může pomoci s porozuměním a řešením porušení pravidel."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "Tento nástroj nastavuje systém pro zapisování výpisů pádů jádra na disk."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "Tento nástroj vytváří archiv nastavení a diagnostických informací z běžícího systému. Archiv je možné uložit lokálně nebo centrálně pro účely sledování či záznamu nebo je možné ho poslat zástupcům technické podpory, vývojářům nebo správcům systémů aby pomohli s hledáním technických selhání a laděním."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "Tento nástroj spravuje místní úložiště, jako například souborové systémy, LVM2 skupiny svazků a NFS připojení."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "Tento nástroj spravuje síťování jako například spřažení, mosty, spojení, VLAN sítě a brány firewall pomocí NetworkManager a Firewalld. NetworkManager není kompatibilní s Ubuntu ve výchozím stavu používaným systemd-networkd a skripty ifupdown v distribuci Debian."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Tento webový prohlížeč je příliš starý pro spuštění Web Console (chybí $0)"
+ ],
+ "Time zone": [
+ null,
+ "Časová zóna"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Abyste zajistili, že do vašeho připojení není zasahováno záškodnickou třetí stranou, ověřte otisk klíče hostitele:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Pokud chcete otisk ověřit, spusťte následující na $0 když jste fyzicky u stroje nebo prostřednictvím důvěryhodné sítě:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Přepnout volič datumů"
+ ],
+ "Too much data": [
+ null,
+ "Příliš mnoho dat"
+ ],
+ "Total size: $0": [
+ null,
+ "Celková velikost: $0"
+ ],
+ "Tower": [
+ null,
+ "Věž"
+ ],
+ "Try again": [
+ null,
+ "Zkusit znovu"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Pokus o synchronizaci se $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "K této adrese se nedaří připojit"
+ ],
+ "Unknown": [
+ null,
+ "Neznámé"
+ ],
+ "Untrusted host": [
+ null,
+ "Nedůvěryhodný stroj"
+ ],
+ "User name": [
+ null,
+ "Uživatelské jméno"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Uživatelské jméno je třeba vyplnit"
+ ],
+ "Validating authentication token": [
+ null,
+ "Kontroluje se ověřovací token"
+ ],
+ "View all logs": [
+ null,
+ "Zobrazit všechny záznamy událostí"
+ ],
+ "View automation script": [
+ null,
+ "Zobrazit automatizační skript"
+ ],
+ "Visit firewall": [
+ null,
+ "Jít na bránu firewall"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Čeká se na dokončení ostatních operací správy balíčků"
+ ],
+ "Weak password": [
+ null,
+ "Snadno prolomitelné heslo"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Webová konzole pro linuxové servery"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Chybné uživatelské jméno nebo heslo"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "K $0 se připojujete poprvé."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "Vámi využívaný prohlížeč neumožňuje vkládání z kontextové nabídky. Náhradně je možné použít Shift+Insert."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Vaše sezení bylo ukončeno."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Platnost vašeho sezení skončila. Přihlaste se znovu."
+ ],
+ "Zone": [
+ null,
+ "Zóna"
+ ],
+ "[binary data]": [
+ null,
+ "[binární data]"
+ ],
+ "[no data]": [
+ null,
+ "[žádná data]"
+ ],
+ "password quality": [
+ null,
+ "odolnost hesla"
+ ],
+ "show less": [
+ null,
+ "zobrazit méně"
+ ],
+ "show more": [
+ null,
+ "zobrazit více"
+ ]
+};
diff --git a/dist/static/po.de.js b/dist/static/po.de.js
new file mode 100644
index 0000000..bbf04d8
--- /dev/null
+++ b/dist/static/po.de.js
@@ -0,0 +1,933 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "de",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 Tag",
+ "$0 Tage"
+ ],
+ "$0 error": [
+ null,
+ "$0 Error"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 mit Code $1 beendet"
+ ],
+ "$0 failed": [
+ null,
+ "$0 fehlgeschlagen"
+ ],
+ "$0 hour": [
+ null,
+ "$0 Stunde",
+ "$0 Stunden"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 ist in keinem Repository verfügbar."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 Schlüssel geändert"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 mit Signal $1 beendet"
+ ],
+ "$0 minute": [
+ null,
+ "$0 Minute",
+ "$0 Minuten"
+ ],
+ "$0 month": [
+ null,
+ "$0 Monat",
+ "$0 Monate"
+ ],
+ "$0 week": [
+ null,
+ "$0 Woche",
+ "$0 Wochen"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 wird installiert."
+ ],
+ "$0 year": [
+ null,
+ "$0 Jahr",
+ "$0 Jahre"
+ ],
+ "1 day": [
+ null,
+ "1 Tag"
+ ],
+ "1 hour": [
+ null,
+ "1 Stunde"
+ ],
+ "1 minute": [
+ null,
+ "1 Minute"
+ ],
+ "1 week": [
+ null,
+ "1 Woche"
+ ],
+ "20 minutes": [
+ null,
+ "20 Minuten"
+ ],
+ "40 minutes": [
+ null,
+ "40 Minuten"
+ ],
+ "5 minutes": [
+ null,
+ "5 Minuten"
+ ],
+ "6 hours": [
+ null,
+ "6 Stunden"
+ ],
+ "60 minutes": [
+ null,
+ "60 Minuten"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Ein moderner Browser ist für Sicherheit, Zuverlässigkeit und Leistung erforderlich."
+ ],
+ "Absent": [
+ null,
+ "Abwesend"
+ ],
+ "Accept key and log in": [
+ null,
+ "Schlüssel akzeptieren und anmelden"
+ ],
+ "Add $0": [
+ null,
+ "$0 hinzufügen"
+ ],
+ "Additional packages:": [
+ null,
+ "Zusatzpakete:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Mit der Cockpit Web Konsole administrieren"
+ ],
+ "Advanced TCA": [
+ null,
+ "Fortgeschrittenes TCA"
+ ],
+ "All-in-one": [
+ null,
+ "Alles-in-einem"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Ansible Rollendokumentation"
+ ],
+ "Authentication failed": [
+ null,
+ "Authentifizierung fehlgeschlagen"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Authentifizierung fehlgeschlagen: Server geschlossene Verbindung"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Privilegierte Aktionen der Cockpit Web-Konsole benötigen Berechtigung"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Automatisch (NTP)"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Automatische Benutzung zusätzlicher NTP-Server"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Automatisch (spezifische NTP-Server)"
+ ],
+ "Automation script": [
+ null,
+ "Automatisierungs-Skript"
+ ],
+ "Blade": [
+ null,
+ "Blade"
+ ],
+ "Blade enclosure": [
+ null,
+ "Bladegehäuse"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Bus-Erweiterungsgehäuse"
+ ],
+ "Cancel": [
+ null,
+ "Abbrechen"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Anmeldeinformationen können nicht weitergeleitet werden"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Vorgang kann nicht für die Vergangenheit geplant werden"
+ ],
+ "Change": [
+ null,
+ "Ändern"
+ ],
+ "Change system time": [
+ null,
+ "Systemzeit ändern"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Geänderte Schlüssel sind oft das Ergebnis einer Neuinstallation des Betriebssystems. Allerdings kann eine unerwartete Änderung auf einen Versuch eines Dritten hinweisen, Ihre Verbindung auszuspähen."
+ ],
+ "Checking installed software": [
+ null,
+ "Installierte Software wird überprüft"
+ ],
+ "Close": [
+ null,
+ "Schließen"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Die Cockpit-Authentifizierung ist falsch konfiguriert."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "Cockpit Konfiguration von NetworkManager und Firewalld"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit konnte den angegebenen Host nicht erreichen."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit ist ein Server Manager zur einfachen Verwaltung Ihrer Linux Server via Web Browser. Ein Wechsel zwischen dem Terminal und der Weboberfläche ist kein Problem. Ein Service, der via Cockpit gestartet wurde, kann im Terminal beendet werden. Genauso können Fehler, welche im Terminal vorkommen, im Cockpit Journal angezeigt werden."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit ist mit der Software auf dem System nicht kompatibel."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit ist auf dem System nicht installiert."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit ist perfekt für neue Systemadministratoren, da es ihnen auf einfache Weise ermöglicht, simple Aufgaben wie Speicherverwaltung, Journal / Logfile Analyse oder das Starten und Stoppen von Diensten durchzuführen. Sie können gleichzeitig mehrere Server überwachen und verwalten. Fügen Sie weitere Maschinen mit einem Klick hinzu und Ihre Maschinen schauen zu ihren Kumpels."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit wird in Ihrem Browser möglicherweise nicht korrekt dargestellt"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Sammeln und Packen von Diagnose und Support Daten"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Sammeln von Kernel-Absturz-Auszügen"
+ ],
+ "Compact PCI": [
+ null,
+ "Kompakte PCI"
+ ],
+ "Connect to": [
+ null,
+ "Verbinden zu"
+ ],
+ "Connect to:": [
+ null,
+ "Verbinden mit:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Zeitüberschreitung bei der Verbindung."
+ ],
+ "Convertible": [
+ null,
+ "Convertible"
+ ],
+ "Copy": [
+ null,
+ "Kopieren"
+ ],
+ "Copy to clipboard": [
+ null,
+ "In Zwischenablage kopieren"
+ ],
+ "Create": [
+ null,
+ "Erstellen"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Neue Task-Datei mit diesem Inhalt erstellen."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Strg+Einfügen"
+ ],
+ "Delay": [
+ null,
+ "Verzögerung"
+ ],
+ "Desktop": [
+ null,
+ "Desktop"
+ ],
+ "Detachable": [
+ null,
+ "Abnehmbar"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Diagnoseberichte"
+ ],
+ "Docking station": [
+ null,
+ "Dockingstation"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Laden Sie kostenlos einen neuen Browser herunter"
+ ],
+ "Downloading $0": [
+ null,
+ "wird heruntergeladen $0"
+ ],
+ "Dual rank": [
+ null,
+ "Doppelter Rang"
+ ],
+ "Embedded PC": [
+ null,
+ "Embedded PC"
+ ],
+ "Excellent password": [
+ null,
+ "Perfektes Passwort"
+ ],
+ "Expansion chassis": [
+ null,
+ "Erweiterungsgehäuse"
+ ],
+ "Failed to change password": [
+ null,
+ "Passwort konnte nicht geändert werden"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "$0 konnte nicht in firewalld aktiviert werden"
+ ],
+ "Go to now": [
+ null,
+ "Zu 'Jetzt' gehen"
+ ],
+ "Handheld": [
+ null,
+ "Handheld"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Host-Schlüssel ist falsch"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Wenn der Fingerabdruck übereinstimmt, klicke \"Schlüssel akzeptieren und einloggen\". Andernfalls, Login abbrechen und den Administrator kontaktieren."
+ ],
+ "Install": [
+ null,
+ "Installation"
+ ],
+ "Install software": [
+ null,
+ "Software installieren"
+ ],
+ "Installing $0": [
+ null,
+ "$0 wird installiert"
+ ],
+ "Internal error": [
+ null,
+ "Interner Fehler"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Interner Fehler: Ungültiger Challenge-Header"
+ ],
+ "Invalid date format": [
+ null,
+ "Ungültiges Datumsformat"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Ungültiges Datumsformat und ungültiges Zeitformat"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Ungültige Dateiberechtigungen"
+ ],
+ "Invalid time format": [
+ null,
+ "Ungültiges Zeitformat"
+ ],
+ "Invalid timezone": [
+ null,
+ "Ungültige Zeitzone"
+ ],
+ "IoT gateway": [
+ null,
+ "IoT-Gateway"
+ ],
+ "Kernel dump": [
+ null,
+ "Kernel dump"
+ ],
+ "Laptop": [
+ null,
+ "Laptop"
+ ],
+ "Learn more": [
+ null,
+ "Mehr erfahren"
+ ],
+ "Loading system modifications...": [
+ null,
+ "System-Änderungen laden..."
+ ],
+ "Log in": [
+ null,
+ "Anmelden"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Melden Sie sich mit dem Server-Benutzerkonto an."
+ ],
+ "Log messages": [
+ null,
+ "Nachrichten protokollieren"
+ ],
+ "Login": [
+ null,
+ "Anmeldung"
+ ],
+ "Login again": [
+ null,
+ "Nochmal anmelden"
+ ],
+ "Login failed": [
+ null,
+ "Anmeldung fehlgeschlagen"
+ ],
+ "Logout successful": [
+ null,
+ "Abmeldung erfolgreich"
+ ],
+ "Low profile desktop": [
+ null,
+ "Low-Profile-Desktop"
+ ],
+ "Lunch box": [
+ null,
+ "Brotdose"
+ ],
+ "Main server chassis": [
+ null,
+ "Hauptservergehäuse"
+ ],
+ "Manage storage": [
+ null,
+ "Speicher verwalten"
+ ],
+ "Manually": [
+ null,
+ "Manuell"
+ ],
+ "Message to logged in users": [
+ null,
+ "Nachricht an angemeldete Benutzer"
+ ],
+ "Mini PC": [
+ null,
+ "Mini PC"
+ ],
+ "Mini tower": [
+ null,
+ "Mini-Tower"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Multi-System-Chassis"
+ ],
+ "NTP server": [
+ null,
+ "NTP-Server"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Benötigen Sie mindestens einen NTP-Server"
+ ],
+ "Networking": [
+ null,
+ "Netzwerk"
+ ],
+ "New host": [
+ null,
+ "Neuer Host"
+ ],
+ "New password was not accepted": [
+ null,
+ "Das neue Passwort wurde nicht akzeptiert"
+ ],
+ "No delay": [
+ null,
+ "Keine Verzögerung"
+ ],
+ "No such file or directory": [
+ null,
+ "Datei oder Verzeichnis nicht vorhanden"
+ ],
+ "No system modifications": [
+ null,
+ "Keine Systemänderungen"
+ ],
+ "Not a valid private key": [
+ null,
+ "Ungültiger privater Schlüssel"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Diese Aktion darf nicht ausgeführt werden."
+ ],
+ "Not synchronized": [
+ null,
+ "Nicht synchronisiert"
+ ],
+ "Notebook": [
+ null,
+ "Notizbuch"
+ ],
+ "Occurrences": [
+ null,
+ "Vorkommnisse"
+ ],
+ "Ok": [
+ null,
+ "OK"
+ ],
+ "Old password not accepted": [
+ null,
+ "Altes Passwort wurde nicht akzeptiert"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Wenn Cockpit installiert ist, aktivieren Sie es mit \"systemctl enable --now cockpit.socket\"."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Oder verwenden Sie einen gebündelten Browser"
+ ],
+ "Other": [
+ null,
+ "Weitere"
+ ],
+ "Other options": [
+ null,
+ "Andere Einstellungen"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit ist abgestürzt"
+ ],
+ "Password": [
+ null,
+ "Passwort"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Das Passwort kann nicht akzeptiert werden"
+ ],
+ "Password is too weak": [
+ null,
+ "Das gewählte Passwort ist zu schwach"
+ ],
+ "Password not accepted": [
+ null,
+ "Passwort wurde nicht akzeptiert"
+ ],
+ "Paste": [
+ null,
+ "Einfügen"
+ ],
+ "Paste error": [
+ null,
+ "Fehler beim Einfügen"
+ ],
+ "Path to file": [
+ null,
+ "Pfad zur Datei"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Peripheriechassis"
+ ],
+ "Permission denied": [
+ null,
+ "Erlaubnis verweigert"
+ ],
+ "Pick date": [
+ null,
+ "Datum auswählen"
+ ],
+ "Pizza box": [
+ null,
+ "Pizza-Box"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Bitte aktivieren Sie JavaScript, um die Web-Konsole verwenden zu können."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Bitte geben Sie den Host an, zu dem eine Verbindung hergestellt werden soll"
+ ],
+ "Portable": [
+ null,
+ "tragbar"
+ ],
+ "Present": [
+ null,
+ "Derzeit"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Die Aufforderung über ssh-add ist abgelaufen"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Die Aufforderung über ssh-keygen ist abgelaufen"
+ ],
+ "RAID chassis": [
+ null,
+ "RAID-Chassis"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Rack-Einbaugehäuse"
+ ],
+ "Reboot": [
+ null,
+ "Neustart"
+ ],
+ "Recent hosts": [
+ null,
+ "Zuletzt genutzte Hosts"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Verbindung ablehnen Host ist unbekannt"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Verbindung ablehnen Hostkey stimmt nicht überein"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Verbindung ablehnen Hostkey ist unbekannt"
+ ],
+ "Removals:": [
+ null,
+ "Umzüge:"
+ ],
+ "Remove host": [
+ null,
+ "Host entfernen"
+ ],
+ "Removing $0": [
+ null,
+ "Entfernen $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "PC mit versiegeltem Gehäuse"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Sicherheitsverstärkte Linuxkonfiguration und Problemlösung"
+ ],
+ "Server": [
+ null,
+ "Server"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Der Server hat die Verbindung beendet."
+ ],
+ "Set time": [
+ null,
+ "Zeit setzen"
+ ],
+ "Shell script": [
+ null,
+ "Shell script"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Einfügen"
+ ],
+ "Shut down": [
+ null,
+ "Herunterfahren"
+ ],
+ "Single rank": [
+ null,
+ "Einzelner Rang"
+ ],
+ "Space-saving computer": [
+ null,
+ "Platzsparender Computer"
+ ],
+ "Specific time": [
+ null,
+ "Bestimmte Zeit"
+ ],
+ "Stick PC": [
+ null,
+ "Stick PC"
+ ],
+ "Storage": [
+ null,
+ "Speicher"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Sub-Chassis"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Sub-Notebook"
+ ],
+ "Synchronized": [
+ null,
+ "Synchronisiert"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Mit $0 synchronisiert"
+ ],
+ "Synchronizing": [
+ null,
+ "Wird synchronisiert"
+ ],
+ "Tablet": [
+ null,
+ "Tablett"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "Der angemeldete Benutzer ist nicht berechtigt, Systemänderungen einzusehen"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Die Passwörter stimmen nicht überein."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "Der entstandene Fingerabdruck kann über öffentliche Methoden, einschließlich E-Mail, weitergegeben werden."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Der Server hat die Authentifizierung abgelehnt. '$0Mit der Passwort-Authentifizierung stehen keine anderen unterstützten Authentifizierungsmethoden zur Verfügung."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Der Server hat die Authentifizierung mit allen unterstützten Methoden abgelehnt."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Die Konfiguration des Webbrowsers verhindert, dass Cockpit ausgeführt wird (nicht erreichbar) $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "Dieses Tool konfiguriert die SELinux Policy und hilft dabei Verletzungen der Policy zu verstehen und aufzulösen."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "Dieses Tool konfiguriert das System zum Schreiben von Kernel Absturz Auszügen auf Datenträger."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "Dieses Tool generiert ein Archiv der Konfiguration und Diagnoseinformation des laufenden Systems.Das Archiv kann lokal oder zentral abgespeichert werden zum Zweck der Archivierung oder Nachverfolgung oder kann an den Technischen Support, Entwickler oder Systemadministratoren gesendet werden, um bei der Fehlersuche oder Debugging zu helfen."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "Dieses Tool verwaltet den lokalen Speicher, wie etwa Dateisysteme, LVM2 Volume Gruppen und NFS Einhängepunkte."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "Dieses Tool verwaltet die Netzwerkumgebung wie etwa Bindungen, Bridges, Teams, VLANs und Firewalls durch den NetworkManager und Firewalld. Der NetworkManager ist inkompatibel mit dem Ubuntus Standard systemd-networkd und Debians ifupdown Scipts."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Dieser Webbrowser ist zu alt, um die Web-Konsole auszuführen (fehlende $0)"
+ ],
+ "Time zone": [
+ null,
+ "Zeitzone"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Überprüfen Sie bitte den Fingerabdruck des Host-Schlüssels, um sicherzustellen, dass Ihre Verbindung nicht von einem böswilligen Dritten ausgespäht wird:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Um einen Fingerabdruck zu überprüfen, führen Sie die folgenden Schritte auf $0 aus, während Sie physisch an der Maschine sitzen oder über ein vertrauenswürdiges Netzwerk:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Datumsauswahl umschalten"
+ ],
+ "Too much data": [
+ null,
+ "Zu viele Daten"
+ ],
+ "Total size: $0": [
+ null,
+ "Gesamtgröße: $0"
+ ],
+ "Tower": [
+ null,
+ "Turm"
+ ],
+ "Try again": [
+ null,
+ "Versuchen Sie es nochmal"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Versuche mit {{Server}} zu synchronisieren"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Es kann keine Verbindung zu dieser Adresse hergestellt werden"
+ ],
+ "Unknown": [
+ null,
+ "Unbekannt"
+ ],
+ "Untrusted host": [
+ null,
+ "Nicht vertrauenswürdiger Host"
+ ],
+ "User name": [
+ null,
+ "Benutzername"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Der Benutzername darf nicht leer sein"
+ ],
+ "Validating authentication token": [
+ null,
+ "Authentifizierungstoken überprüfen"
+ ],
+ "View all logs": [
+ null,
+ "Alle Protokolle ansehen"
+ ],
+ "View automation script": [
+ null,
+ "Automatisierungs-Script anzeigen"
+ ],
+ "Visit firewall": [
+ null,
+ "Firewall besuchen"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Warten, bis andere Software-Verwaltungsvorgänge abgeschlossen sind"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Webkonsole für Linux-Server"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Benutzername oder Passwort falsch"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Sie stellen zum ersten Mal eine Verbindung zu $0 her."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "Ihr Browser lässt das Einfügen über das Kontextmenü nicht zu. Sie können Umschalt+Einfügen verwenden."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Ihre Sitzung wurde beendet."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Die Session ist abgelaufen. Bitte neu einloggen."
+ ],
+ "Zone": [
+ null,
+ "Zone"
+ ],
+ "[binary data]": [
+ null,
+ "[Binärdaten]"
+ ],
+ "[no data]": [
+ null,
+ "[keine Daten]"
+ ],
+ "password quality": [
+ null,
+ "Passwortqualität"
+ ],
+ "show less": [
+ null,
+ "zeige weniger"
+ ],
+ "show more": [
+ null,
+ "Zeig mehr"
+ ]
+};
diff --git a/dist/static/po.es.js b/dist/static/po.es.js
new file mode 100644
index 0000000..eac2c98
--- /dev/null
+++ b/dist/static/po.es.js
@@ -0,0 +1,965 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "es",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 día",
+ "$0 días"
+ ],
+ "$0 error": [
+ null,
+ "Error $0"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 finalizó con el código $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 falló"
+ ],
+ "$0 hour": [
+ null,
+ "$0 hora",
+ "$0 horas"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 no está disponible en ningún repositorio."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 clave cambiada"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 terminado con señal $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minuto",
+ "$0 minutos"
+ ],
+ "$0 month": [
+ null,
+ "$0 mes",
+ "$0 meses"
+ ],
+ "$0 week": [
+ null,
+ "$0 semana",
+ "$0 semanas"
+ ],
+ "$0 will be installed.": [
+ null,
+ "Se instalará: $0."
+ ],
+ "$0 year": [
+ null,
+ "$0 año",
+ "$0 años"
+ ],
+ "1 day": [
+ null,
+ "1 día"
+ ],
+ "1 hour": [
+ null,
+ "1 hora"
+ ],
+ "1 minute": [
+ null,
+ "1 minuto"
+ ],
+ "1 week": [
+ null,
+ "1 semana"
+ ],
+ "20 minutes": [
+ null,
+ "20 minutos"
+ ],
+ "40 minutes": [
+ null,
+ "40 minutos"
+ ],
+ "5 minutes": [
+ null,
+ "5 minutos"
+ ],
+ "6 hours": [
+ null,
+ "6 horas"
+ ],
+ "60 minutes": [
+ null,
+ "60 minutos"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Se requiere un navegador moderno por seguridad, fiabilidad y rendimiento."
+ ],
+ "Absent": [
+ null,
+ "Ausente"
+ ],
+ "Accept key and log in": [
+ null,
+ "Aceptar la clave y loguearse"
+ ],
+ "Acceptable password": [
+ null,
+ "Contraseña aceptable"
+ ],
+ "Add $0": [
+ null,
+ "Añadir $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Paquetes adicionales:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Administrando con la consola Web de Cockpit"
+ ],
+ "Advanced TCA": [
+ null,
+ "TCA avanzado"
+ ],
+ "All-in-one": [
+ null,
+ "Todo en uno"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Documentación de los roles de Ansible"
+ ],
+ "Authentication failed": [
+ null,
+ "Falló la autenticación"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Falló la autenticación: se ha cerrado la conexión con el servidor"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Se requiere autenticación para elevar privilegios y realizar tareas de administración en la consola Web de Cockpit"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Utilizando NTP de forma automática"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Utilizando automáticamente servidores NTP adicionales"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Utilizando automáticamente servidores NTP específicos"
+ ],
+ "Automation script": [
+ null,
+ "Script de automatización"
+ ],
+ "Blade": [
+ null,
+ "Blade"
+ ],
+ "Blade enclosure": [
+ null,
+ "Chasis tipo blade"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Chasis de expansión de bus"
+ ],
+ "Bypass browser check": [
+ null,
+ "Omitir la verificación del navegador"
+ ],
+ "Cancel": [
+ null,
+ "Cancelar"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "No se pueden transferir los datos de acceso"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "No se puede planificar un evento ocurrido en el pasado"
+ ],
+ "Change": [
+ null,
+ "Cambiar"
+ ],
+ "Change system time": [
+ null,
+ "Cambiar la hora del sistema"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Las llaves cambiadas pueden llevar una reinstalación del sistema operativo. Sin embargo, un cambio inesperado puede indicar un intento de interceptar su conexión mediante un tercero."
+ ],
+ "Checking installed software": [
+ null,
+ "Comprobando el software instalado"
+ ],
+ "Close": [
+ null,
+ "Cerrar"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "La configuración de autenticación de Cockpit está mal configurada."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "Configuración en Cockpit de NetworkManager y Firewalld"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit no se pudo conectar con el anfitrión especificado."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit es un gestor de servidores que facilita la administración de servidores Linux a través de un navegador web. Es posible alternar entre el portal web y la consola sin problemas. Un servicio que haya empezado por Cockpit se puede parar desde la terminal. Asimismo, si se produce un error en el terminal, podrá verlo en la bitácora de Cockpit."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit no es compatible con el software del sistema."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit no está instalado en el sistema."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit es ideal para administradores de sistemas nóveles, ya que permite realizar con sencillez tareas como gestionar almacenamiento, inspeccionar bitácoras e iniciar y detener servicios. Puede monitorizar y administrar varios servidores a la vez. Tan solo añádalos con solo pulsar un botón y sus máquinas se encargarán del resto."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Puede que Cockpit no se muestre correctamente en su navegador"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Recoger y empaquetar datos de diagnóstico y soporte"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Recoger volcados de colapso del kernel"
+ ],
+ "Compact PCI": [
+ null,
+ "PCI compacto"
+ ],
+ "Connect to": [
+ null,
+ "Conectar a"
+ ],
+ "Connect to:": [
+ null,
+ "Conectar a:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "La conexión ha caducado."
+ ],
+ "Convertible": [
+ null,
+ "Convertible"
+ ],
+ "Copy": [
+ null,
+ "Copiar"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Copiar al portapapeles"
+ ],
+ "Create": [
+ null,
+ "Crear"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Crear un archivo de tarea con este contenido."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Retardo"
+ ],
+ "Desktop": [
+ null,
+ "Escritorio"
+ ],
+ "Detachable": [
+ null,
+ "Desmontable"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Informes de diagnóstico"
+ ],
+ "Docking station": [
+ null,
+ "Estación de acoplamiento"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Descargar un navegador libre"
+ ],
+ "Downloading $0": [
+ null,
+ "Descargando $0"
+ ],
+ "Dual rank": [
+ null,
+ "Rango dual"
+ ],
+ "Embedded PC": [
+ null,
+ "PC integrado"
+ ],
+ "Excellent password": [
+ null,
+ "Contraseña excelente"
+ ],
+ "Expansion chassis": [
+ null,
+ "Chasis de expansión"
+ ],
+ "Failed to change password": [
+ null,
+ "Error al cambiar contraseña"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "Fallo al habilitar $0 en firewalld"
+ ],
+ "Go to now": [
+ null,
+ "Ir a ahora"
+ ],
+ "Handheld": [
+ null,
+ "Dispositivo de mano"
+ ],
+ "Hide confirmation password": [
+ null,
+ "Ocultar contraseña de confirmación"
+ ],
+ "Hide password": [
+ null,
+ "Ocultar contraseña"
+ ],
+ "Host key is incorrect": [
+ null,
+ "La tecla del anfitrión es incorrecta"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Si la huella coincide, pulse \"Aceptar la clave e iniciar sesión\". En caso contrario, no inicie sesión y contacte con su administrador."
+ ],
+ "Install": [
+ null,
+ "Instalar"
+ ],
+ "Install software": [
+ null,
+ "Instalar software"
+ ],
+ "Installing $0": [
+ null,
+ "Instalando $0"
+ ],
+ "Internal error": [
+ null,
+ "Error interno"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Error Interno: Encabezado del reto no valido"
+ ],
+ "Invalid date format": [
+ null,
+ "Formato de fecha inválido"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Formato de fecha y hora inválidos"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Permisos de archivo invalidos"
+ ],
+ "Invalid time format": [
+ null,
+ "Formato de hora inválido"
+ ],
+ "Invalid timezone": [
+ null,
+ "Zona horaria no válida"
+ ],
+ "IoT gateway": [
+ null,
+ "Pasarela IoT"
+ ],
+ "Kernel dump": [
+ null,
+ "Volcado del kernel"
+ ],
+ "Laptop": [
+ null,
+ "Portátil"
+ ],
+ "Learn more": [
+ null,
+ "Aprenda más"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Cargando modificaciones del sistema..."
+ ],
+ "Log in": [
+ null,
+ "Iniciar sesión"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Acceda con su cuenta de usuario del servidor."
+ ],
+ "Log messages": [
+ null,
+ "Mensajes de registro"
+ ],
+ "Login": [
+ null,
+ "Iniciar sesión"
+ ],
+ "Login again": [
+ null,
+ "Acceda de nuevo"
+ ],
+ "Login failed": [
+ null,
+ "Inicio de sesión fallido"
+ ],
+ "Logout successful": [
+ null,
+ "Ha cerrado sesión de forma exitosa"
+ ],
+ "Low profile desktop": [
+ null,
+ "Perfil bajo de escritorio"
+ ],
+ "Lunch box": [
+ null,
+ "Caja de almuerzo"
+ ],
+ "Main server chassis": [
+ null,
+ "Chasis del servidor principal"
+ ],
+ "Manage storage": [
+ null,
+ "Gestionar el almacenamiento"
+ ],
+ "Manually": [
+ null,
+ "Manualmente"
+ ],
+ "Message to logged in users": [
+ null,
+ "Mensaje para usuarios activos"
+ ],
+ "Mini PC": [
+ null,
+ "Mini PC"
+ ],
+ "Mini tower": [
+ null,
+ "Mini Torre"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Chasis multisistema"
+ ],
+ "NTP server": [
+ null,
+ "Servidor NTP"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Se requiere al menos un servidor NTP"
+ ],
+ "Networking": [
+ null,
+ "Redes"
+ ],
+ "New host": [
+ null,
+ "Añadir un nuevo anfitrión"
+ ],
+ "New password was not accepted": [
+ null,
+ "No se aceptó la nueva contraseña"
+ ],
+ "No delay": [
+ null,
+ "Sin retardo"
+ ],
+ "No such file or directory": [
+ null,
+ "No existe el archivo o directorio"
+ ],
+ "No system modifications": [
+ null,
+ "No hay modificaciones para el sistema"
+ ],
+ "Not a valid private key": [
+ null,
+ "No es una clave privada válida"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "No está permitido llevar a cabo esta acción."
+ ],
+ "Not synchronized": [
+ null,
+ "No está sincronizado"
+ ],
+ "Notebook": [
+ null,
+ "Portátil"
+ ],
+ "Occurrences": [
+ null,
+ "Ocurrencias"
+ ],
+ "Ok": [
+ null,
+ "Aceptar"
+ ],
+ "Old password not accepted": [
+ null,
+ "No se aceptó la contraseña vieja"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Una vez que se instale Cockpit, habilítelo con \"systemctl enable --now cockpit.socket\"."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "O use un navegador integrado"
+ ],
+ "Other": [
+ null,
+ "Otro"
+ ],
+ "Other options": [
+ null,
+ "Otras opciones"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit colapsó"
+ ],
+ "Password": [
+ null,
+ "Contraseña"
+ ],
+ "Password is not acceptable": [
+ null,
+ "La contraseña no es válida"
+ ],
+ "Password is too weak": [
+ null,
+ "La contraseña es muy débil"
+ ],
+ "Password not accepted": [
+ null,
+ "Contraseña no válida"
+ ],
+ "Paste": [
+ null,
+ "Pegar"
+ ],
+ "Paste error": [
+ null,
+ "Error al pegar"
+ ],
+ "Path to file": [
+ null,
+ "Ruta al fichero"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Chasis periférico"
+ ],
+ "Permission denied": [
+ null,
+ "Permiso denegado"
+ ],
+ "Pick date": [
+ null,
+ "Selecciona una fecha"
+ ],
+ "Pizza box": [
+ null,
+ "Caja de pizza"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Por favor, habilite JavaScript para usar la consola Web."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Por favor especifique el anfitrión a conectarse"
+ ],
+ "Portable": [
+ null,
+ "Portable"
+ ],
+ "Present": [
+ null,
+ "Presente"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Ha expirado el tiempo de ssh-add"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Se ha agotado el tiempo de espera para ssh-keygen"
+ ],
+ "RAID chassis": [
+ null,
+ "Chasis RAID"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Chasis montado en rack"
+ ],
+ "Reboot": [
+ null,
+ "Reiniciar"
+ ],
+ "Recent hosts": [
+ null,
+ "Anfitriones recientes"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Rechazando la conexión. Se desconoce el anfitrión"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Rechazando conexión. La clave del anfitrión no coincide"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Rechazando la conexión. La clave de anfitrión es desconocida"
+ ],
+ "Removals:": [
+ null,
+ "Borrados:"
+ ],
+ "Remove host": [
+ null,
+ "Eliminar anfitrión"
+ ],
+ "Removing $0": [
+ null,
+ "Eliminando $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "PC de caja sellada"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Configuración de Security Enhanced Linux y resolución de problemas"
+ ],
+ "Server": [
+ null,
+ "Servidor"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "El servidor ha cerrado la conexión."
+ ],
+ "Set time": [
+ null,
+ "Establecer la hora"
+ ],
+ "Shell script": [
+ null,
+ "Script de shell"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Show confirmation password": [
+ null,
+ "Mostrar contraseña de confirmación"
+ ],
+ "Show password": [
+ null,
+ "Mostrar contraseña"
+ ],
+ "Shut down": [
+ null,
+ "Apagar"
+ ],
+ "Single rank": [
+ null,
+ "Rango único"
+ ],
+ "Space-saving computer": [
+ null,
+ "Ordenador compacto"
+ ],
+ "Specific time": [
+ null,
+ "Hora específica"
+ ],
+ "Stick PC": [
+ null,
+ "PC USB"
+ ],
+ "Storage": [
+ null,
+ "Almacenamiento"
+ ],
+ "Strong password": [
+ null,
+ "Contraseña fuerte"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Sub Chasis"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Subportátil"
+ ],
+ "Synchronized": [
+ null,
+ "Sincronizado"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Sincronizado con $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Sincronizando"
+ ],
+ "Tablet": [
+ null,
+ "Tableta"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "El usuario autenticado no tiene permisos para ver las modificaciones del sistema"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Las contraseñas no coinciden."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "La huella resultante es apta para compartirse en público, incluyendo correo electrónico."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "El servidor rechazó autenticar '$0' utilizando la autenticación de contraseña y no hay disponble otro método de autenticación soportado."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "El servido rehusó autenticar usando los métodos soportados."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "La configuración del navegador evita que se ejecute Cockpit (inaccesible $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "Esta herramienta configura la política de SELinux y puede ayudarle a comprender y resolver infracciones de la política."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "Esta herramienta configura el sistema para que escriba en disco los volcados de colapso del kernel."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "Esta herramienta genera un archivo de configuración e información de diagnóstico del sistema en ejecución. El archivo puede ser almacenado localmente o centralmente para propósitos de registro o seguimiento, o puede ser enviado a representantes de soporte técnico, desarrolladores o administradores de sistemas para asistir con la detección de fallos y su corrección."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "Esta herramienta gestiona el almacenamiento local, como sistemas de archivos, grupos de volúmenes LVM2 y montajes NFS."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "Esta herramienta gestiona la configuración de red como agrupaciones, puentes, grupos, las VLAN y cortafuegos usando NetworkManager y Firewalld. NetworkManager es incompatible con systemd-networkd habilitado por defecto en Ubuntu y con los scripts de ifupdown de Debian."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Este navegador web es demasiado viejo para ejecutar la consola Web (no soporta $0)"
+ ],
+ "Time zone": [
+ null,
+ "Huso horario"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Para garantizar que su conexión no sea interceptada por un tercero malicioso, por favor verifique la huella de clave del anfitrión:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Para verificar una huella, ejecute lo siguiente en $0 mientras se sitúa físicamente frente a la máquina o a través de una red de confianza:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Alternar el selector de fecha"
+ ],
+ "Too much data": [
+ null,
+ "Demasiados datos"
+ ],
+ "Total size: $0": [
+ null,
+ "Tamaño total: $0"
+ ],
+ "Tower": [
+ null,
+ "Torre"
+ ],
+ "Try again": [
+ null,
+ "Intentar otra vez"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Intentando sincronizar con $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "No se pudo realizar una conexión con esa dirección"
+ ],
+ "Unknown": [
+ null,
+ "Desconocido"
+ ],
+ "Untrusted host": [
+ null,
+ "Anfitrión no seguro"
+ ],
+ "User name": [
+ null,
+ "Nombre de usuario"
+ ],
+ "User name cannot be empty": [
+ null,
+ "El nombre de usuario no puede estar vacío"
+ ],
+ "Validating authentication token": [
+ null,
+ "Validando código de autenticación"
+ ],
+ "View all logs": [
+ null,
+ "Ver todos los registros"
+ ],
+ "View automation script": [
+ null,
+ "Ver el script de automatización"
+ ],
+ "Visit firewall": [
+ null,
+ "Ir al cortafuegos"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Esperando a que finalicen otras operaciones de gestión de software"
+ ],
+ "Weak password": [
+ null,
+ "Contraseña débil"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Consola web para servidores Linux"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Nombre de usuario o contraseña equivocada"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Se está conectando con $0 por primera vez."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "Tu navegador no admite pegar desde el menú contextual. Puedes usar Shift+Insert."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Su sesión se ha terminado."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Su sesión ha expirado. Por favor inicie sesión otra vez."
+ ],
+ "Zone": [
+ null,
+ "Zona"
+ ],
+ "[binary data]": [
+ null,
+ "[datos binarios]"
+ ],
+ "[no data]": [
+ null,
+ "[no hay datos]"
+ ],
+ "password quality": [
+ null,
+ "calidad de la contraseña"
+ ],
+ "show less": [
+ null,
+ "mostrar menos"
+ ],
+ "show more": [
+ null,
+ "mostrar más"
+ ]
+};
diff --git a/dist/static/po.fi.js b/dist/static/po.fi.js
new file mode 100644
index 0000000..55ce34c
--- /dev/null
+++ b/dist/static/po.fi.js
@@ -0,0 +1,933 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "fi",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 Git"
+ ],
+ "$0 day": [
+ null,
+ "$0 päivä",
+ "$0 päivää"
+ ],
+ "$0 error": [
+ null,
+ "$0 virhe"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 poistui koodilla $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 epäonnistui"
+ ],
+ "$0 hour": [
+ null,
+ "$0 tunti",
+ "$0 tuntia"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 ei ole saatavilla mistään ohjelmistovarastosta."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 avain muuttunut"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 tapettu signaalilla $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minuutti",
+ "$0 minuuttia"
+ ],
+ "$0 month": [
+ null,
+ "$0 kuukausi",
+ "$0 kuukautta"
+ ],
+ "$0 week": [
+ null,
+ "$0 viikko",
+ "$0 viikkoa"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 asennetaan."
+ ],
+ "$0 year": [
+ null,
+ "$0 vuosi",
+ "$0 vuotta"
+ ],
+ "1 day": [
+ null,
+ "1 päivä"
+ ],
+ "1 hour": [
+ null,
+ "1 tunti"
+ ],
+ "1 minute": [
+ null,
+ "1 minuutti"
+ ],
+ "1 week": [
+ null,
+ "1 viikko"
+ ],
+ "20 minutes": [
+ null,
+ "20 minuuttia"
+ ],
+ "40 minutes": [
+ null,
+ "40 minuuttia"
+ ],
+ "5 minutes": [
+ null,
+ "5 minuuttia"
+ ],
+ "6 hours": [
+ null,
+ "6 tuntia"
+ ],
+ "60 minutes": [
+ null,
+ "60 minuuttia"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Nykyaikainen selain vaaditaan turvallisuuden, luotettavuuden ja suorituskyvyn takaamiseksi."
+ ],
+ "Absent": [
+ null,
+ "Poissa"
+ ],
+ "Accept key and log in": [
+ null,
+ "Hyväksy avain ja kirjaudu sisään"
+ ],
+ "Add $0": [
+ null,
+ "Lisää $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Ylimääräiset paketit:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Hallinta Cockpit-verkkokonsolilla"
+ ],
+ "Advanced TCA": [
+ null,
+ "Edistynyt TCA"
+ ],
+ "All-in-one": [
+ null,
+ "Kaikki yhdessä"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Ansible-roolien dokumentaatio"
+ ],
+ "Authentication failed": [
+ null,
+ "Tunnistautuminen epäonnistui"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Tunnistautuminen epäonnistui: Palvelin sulki yhteyden"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Todennus vaaditaan etuoikeutettujen tehtävien suorittamiseen Cockpit Web Console:ssa"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Käytetään automaattisesti NTP:tä"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Käytetään automaattisesti lisättyjä NTP-palvelimia"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Käytetään automaattisesti tiettyjä NTP-palvelimia"
+ ],
+ "Automation script": [
+ null,
+ "Automaatio-komentosarja"
+ ],
+ "Blade": [
+ null,
+ "Terä"
+ ],
+ "Blade enclosure": [
+ null,
+ "Teräkotelo"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Väylän laajennusalusta"
+ ],
+ "Cancel": [
+ null,
+ "Peru"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Kirjautumistietoja ei voi välittää eteenpäin"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Tapahtumaa ei voi aikatauluttaa menneisyyteen"
+ ],
+ "Change": [
+ null,
+ "Vaihda"
+ ],
+ "Change system time": [
+ null,
+ "Vaihda järjestelmän aika"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Muutetut avaimet ovat usein seurausta käyttöjärjestelmän uudelleenasennuksesta. Odottamaton muutos voi kuitenkin tarkoittaa kolmannen osapuolen yritystä siepata yhteys."
+ ],
+ "Checking installed software": [
+ null,
+ "Tarkistetaan asennettu ohjelmisto"
+ ],
+ "Close": [
+ null,
+ "Sulje"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Cockpitin tunnistautuminen on konfiguroitu virheellisesti."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "NetworkManagerin ja Firewalld:n Cockit-asetukset"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit ei saanut yhteyttä koneeseen."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit on palvelinhallintatyökalu, joka tekee ylläpidon helpoksi selaimen kautta. Liikkuminen päätteen ja verkkokäyttöliittymän välillä ei ole ongelma. Cockpitissä aloitettu palvelu voidaan lopettaa päätteessä. Samaten päätteessä näkyvä virheilmoitus voidaan nähdä myös Cockpitin journal-näkymässä."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit ei ole yhteensopiva järjestelmän ohjelmiston kanssa."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit ei ole asennettu järjestelmässä."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit on täydellinen uusille ylläpitäjille. Sen avulla voi tehdä helposti toimenpiteitä kuten tallennustilan hallintaa, lokien tarkistamista sekä palveluiden käynnistämistä ja lopettamista. Voit monitoroida ja hallita useita palvelimia samanaikaisesti. Lisää ne yhdellä napsautuksella ja koneesi katsovat kavereidensa perään."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit ei ehkä näy oikein selaimessasi"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Kerää ja paketoi diagnostiikkaa ja tukitietoja"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Kerää ytimen kaatumisvedoksia"
+ ],
+ "Compact PCI": [
+ null,
+ "Kompakti PCI"
+ ],
+ "Connect to": [
+ null,
+ "Yhdistä"
+ ],
+ "Connect to:": [
+ null,
+ "Yhdistä kohteeseen:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Yhteys aikakatkaistiin."
+ ],
+ "Convertible": [
+ null,
+ "Muunnettavissa"
+ ],
+ "Copy": [
+ null,
+ "Kopio"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Kopioi leikepöydälle"
+ ],
+ "Create": [
+ null,
+ "Luo"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Luo uusi tehtävätiedosto tällä sisällöllä."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Viive"
+ ],
+ "Desktop": [
+ null,
+ "Työpöytä"
+ ],
+ "Detachable": [
+ null,
+ "Irrotettava"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Diagnostiikkaraportit"
+ ],
+ "Docking station": [
+ null,
+ "Telakka"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Lataa uusi selain ilmaiseksi"
+ ],
+ "Downloading $0": [
+ null,
+ "Ladataan $0"
+ ],
+ "Dual rank": [
+ null,
+ "Kaksinkertainen sijoitus"
+ ],
+ "Embedded PC": [
+ null,
+ "Sulautettu tietokone"
+ ],
+ "Excellent password": [
+ null,
+ "Erinomainen salasana"
+ ],
+ "Expansion chassis": [
+ null,
+ "Laajennusrunko"
+ ],
+ "Failed to change password": [
+ null,
+ "Salasanan vaihtaminen epäonnistui"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "$0:n käyttöönotto firewalld:ssä epäonnistui"
+ ],
+ "Go to now": [
+ null,
+ "Mene nyt"
+ ],
+ "Handheld": [
+ null,
+ "Kädessä pidettävä"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Koneen avain on väärin"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Jos sormenjälki on sama, napsauta \"Hyväksy avain ja kirjaudu sisään\". Muussa tapauksessa älä kirjaudu sisään ja ota yhteyttä ylläpitäjään."
+ ],
+ "Install": [
+ null,
+ "Asennus"
+ ],
+ "Install software": [
+ null,
+ "Asennetaan ohjelmistoja"
+ ],
+ "Installing $0": [
+ null,
+ "Asennetaan $0"
+ ],
+ "Internal error": [
+ null,
+ "Sisäinen virhe"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Sisäinen virhe: Virheellinen haasteen otsikko"
+ ],
+ "Invalid date format": [
+ null,
+ "Virheellinen päivämuoto"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Virheellinen päivämuoto ja aikamuoto"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Virheelliset tiedosto-oikeudet"
+ ],
+ "Invalid time format": [
+ null,
+ "Virheellinen aikamuoto"
+ ],
+ "Invalid timezone": [
+ null,
+ "Virheellinen aikavyöhyke"
+ ],
+ "IoT gateway": [
+ null,
+ "IoT -yhdyskäytävä"
+ ],
+ "Kernel dump": [
+ null,
+ "Ytimen tyhjennys"
+ ],
+ "Laptop": [
+ null,
+ "Kannettava"
+ ],
+ "Learn more": [
+ null,
+ "Opi lisää"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Ladataan järjestelmän muutoksia ..."
+ ],
+ "Log in": [
+ null,
+ "Kirjaudu sisään"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Kirjaudu sisään palvelimesi käyttäjätilillä."
+ ],
+ "Log messages": [
+ null,
+ "Kirjaa viestit"
+ ],
+ "Login": [
+ null,
+ "Sisäänkirjautuminen"
+ ],
+ "Login again": [
+ null,
+ "Kirjaudu sisään uudelleen"
+ ],
+ "Login failed": [
+ null,
+ "Kirjautuminen epäonnistui"
+ ],
+ "Logout successful": [
+ null,
+ "Uloskirjautuminen onnistui"
+ ],
+ "Low profile desktop": [
+ null,
+ "Matalan tason työpöytä"
+ ],
+ "Lunch box": [
+ null,
+ "Eväslaatikko"
+ ],
+ "Main server chassis": [
+ null,
+ "Pääpalvelimen runko"
+ ],
+ "Manage storage": [
+ null,
+ "Tallennustilan hallinta"
+ ],
+ "Manually": [
+ null,
+ "Manuaalisesti"
+ ],
+ "Message to logged in users": [
+ null,
+ "Viesti sisäänkirjautuneille käyttäjille"
+ ],
+ "Mini PC": [
+ null,
+ "Minitietokone"
+ ],
+ "Mini tower": [
+ null,
+ "Minitorni"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Monijärjestelmäinen alusta"
+ ],
+ "NTP server": [
+ null,
+ "NTP-palvelin"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Tarvitaan vähintään yksi NTP-palvelin"
+ ],
+ "Networking": [
+ null,
+ "Verkko"
+ ],
+ "New host": [
+ null,
+ "Uusi kone"
+ ],
+ "New password was not accepted": [
+ null,
+ "Uutta salasanaa ei hyväksytty"
+ ],
+ "No delay": [
+ null,
+ "Ei viivettä"
+ ],
+ "No such file or directory": [
+ null,
+ "Tiedostoa tai hakemistoa ei löydy"
+ ],
+ "No system modifications": [
+ null,
+ "Ei järjestelmän muutoksia"
+ ],
+ "Not a valid private key": [
+ null,
+ "Ei kelvollinen yksityinen avain"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Ei oikeutta suorittaa tätä toimintoa."
+ ],
+ "Not synchronized": [
+ null,
+ "Ei synkronisoitu"
+ ],
+ "Notebook": [
+ null,
+ "Muistikirja"
+ ],
+ "Occurrences": [
+ null,
+ "Tapahtumat"
+ ],
+ "Ok": [
+ null,
+ "OK"
+ ],
+ "Old password not accepted": [
+ null,
+ "Vanhaa salasanaa ei hyväksytty"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Kun Cockpit on asennettu, ota se käyttöön 'systemctl enable --now cockpit.socket'."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Tai käytä valmiiksi asennettua selainta"
+ ],
+ "Other": [
+ null,
+ "Muu"
+ ],
+ "Other options": [
+ null,
+ "Muut valinnat"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit kaatui"
+ ],
+ "Password": [
+ null,
+ "Salasana"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Salasana ei ole hyväksyttävä"
+ ],
+ "Password is too weak": [
+ null,
+ "Salasana on liian heikko"
+ ],
+ "Password not accepted": [
+ null,
+ "Salasanaa ei hyväksytty"
+ ],
+ "Paste": [
+ null,
+ "Siirrä"
+ ],
+ "Paste error": [
+ null,
+ "Liittämisvirhe"
+ ],
+ "Path to file": [
+ null,
+ "Polku tiedostoon"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Lisälaitteen kotelo"
+ ],
+ "Permission denied": [
+ null,
+ "Käyttö estetty"
+ ],
+ "Pick date": [
+ null,
+ "Valitse päivämäärä"
+ ],
+ "Pizza box": [
+ null,
+ "Pizza-laatikko"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Ota JavaScript käyttöön, jotta voit käyttää Verkkokonsolia."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Määritä kone, johon haluat muodostaa yhteyden"
+ ],
+ "Portable": [
+ null,
+ "Kannettava"
+ ],
+ "Present": [
+ null,
+ "Nykyinen"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Kysely 'ssh-add':in kautta aikakatkaistiin"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Kysely 'ssh-keygen':in kautta aikakatkaistiin"
+ ],
+ "RAID chassis": [
+ null,
+ "RAID-runko"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Räkkiin liitettävä runko"
+ ],
+ "Reboot": [
+ null,
+ "Käynnistä uudelleen"
+ ],
+ "Recent hosts": [
+ null,
+ "Viimeaikaiset isännät"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Kieltäytyminen yhteyden muodostamisesta. Kone on tuntematon"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Kieltäytyminen yhteyden muodostamisesta. Koneen avain ei täsmää"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Kieltäytyminen yhteyden muodostamisesta. Koneen avain on tuntematon"
+ ],
+ "Removals:": [
+ null,
+ "Poistot:"
+ ],
+ "Remove host": [
+ null,
+ "Poista kone"
+ ],
+ "Removing $0": [
+ null,
+ "Poistetaan $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "Suljettu tietokonekotelo"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Security Enhanced Linuxin asetukset ja ongelmanratkaisu"
+ ],
+ "Server": [
+ null,
+ "Palvelin"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Palvelin on sulkenut yhteyden."
+ ],
+ "Set time": [
+ null,
+ "Aseta aika"
+ ],
+ "Shell script": [
+ null,
+ "Komentotulkin komentosarja"
+ ],
+ "Shift+Insert": [
+ null,
+ "Vaihto + Syöttö"
+ ],
+ "Shut down": [
+ null,
+ "Sammuta"
+ ],
+ "Single rank": [
+ null,
+ "Yksi sijoitus"
+ ],
+ "Space-saving computer": [
+ null,
+ "Tilaa säästävä tietokone"
+ ],
+ "Specific time": [
+ null,
+ "Tietty aika"
+ ],
+ "Stick PC": [
+ null,
+ "Tikku-PC"
+ ],
+ "Storage": [
+ null,
+ "Tallennustila"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Alirunko"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Pieni kannettava tietokone"
+ ],
+ "Synchronized": [
+ null,
+ "Synkronoitu"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Synkronoi palvelimen $0 kanssa"
+ ],
+ "Synchronizing": [
+ null,
+ "Synkronoidaan"
+ ],
+ "Tablet": [
+ null,
+ "Tabletti"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "Sisäänkirjautunut käyttäjä ei saa tarkastella järjestelmän muutoksia"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Salasanat eivät täsmää."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "Tuloksena oleva sormenjälki sopii jakaa julkisilla menetelmillä, mukaan lukien sähköposti."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Palvelin kieltäytyi todentamasta käyttäjää '$0' salasanatodennusta käyttäen, eikä muita tuettuja todennustapoja ole käytettävissä."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Palvelin kieltäytyi tunnistautumista käyttäen mitään tuetuista tavoista."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Verkkoselaimen kokoonpano estää Cockpitin suorittamasta (ei käytettävissä $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "Tämä työkalu asettaa SELinux-käytännön ja auttaa käytäntörikkeiden ymmärtämisessä ja ratkaisemisessa."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "Tämä työkalu asettaa järjestelmän kirjoittamaan ytimen kaatumisvedokset levylle."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "Tämä työkalu luo arkiston konfiguraatio- ja diagnostiikkatiedoista käynnissä olevasta järjestelmästä. Arkisto voidaan tallentaa paikallisesti tai keskitetysti tallennus- tai seurantatarkoituksiin tai se voidaan lähettää teknisen tuen edustajille, kehittäjille tai järjestelmänvalvojille auttamaan teknisten vikojen etsinnässä ja virheenkorjauksessa."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "Tämä työkalu hallinnoi paikallista tallennustilaa, kuten tiedostojärjestelmiä, LVM2-taltioryhmiä ja NFS-liitoksia."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "Tämä työkalu hallitsee verkkoyhteyksiä, kuten sidoksia, siltoja, ryhmiä, VLAN-verkkoja ja palomuureja NetworkManagerin ja Firewalld:n avulla. NetworkManager ei ole yhteensopiva Ubuntun oletusarvoisten systemd-networkd- ja Debianin ifupdown-komentosarjojen kanssa."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Tämä verkkoselain on liian vanha Web-konsolin käyttämiseen (puuttuu $0)"
+ ],
+ "Time zone": [
+ null,
+ "Aikavyöhyke"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Tarkista koneen avaimen sormenjälki varmistaaksesi, että haitallinen kolmas osapuoli ei sieppaa yhteyttä:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Vahvistaaksesi sormenjäljen, suorita seuraava koneella $0 istuessasi fyysisesti koneen ääressä tai luotettavan verkon kautta:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Vaihda päivämäärän valitsin"
+ ],
+ "Too much data": [
+ null,
+ "Liian paljon dataa"
+ ],
+ "Total size: $0": [
+ null,
+ "Koko yhteensä: $0"
+ ],
+ "Tower": [
+ null,
+ "Torni"
+ ],
+ "Try again": [
+ null,
+ "Yritä uudelleen"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Yritetään synkronoida palvelimen $0 kanssa"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Siihen osoitteeseen yhdistäminen epäonnistui"
+ ],
+ "Unknown": [
+ null,
+ "Tuntematon"
+ ],
+ "Untrusted host": [
+ null,
+ "Epäluotettava kone"
+ ],
+ "User name": [
+ null,
+ "Käyttäjänimi"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Käyttäjätunnus ei voi olla tyhjä"
+ ],
+ "Validating authentication token": [
+ null,
+ "Vahvistetaan todennustunnus"
+ ],
+ "View all logs": [
+ null,
+ "Katso kaikki lokit"
+ ],
+ "View automation script": [
+ null,
+ "Näytä automaatio-komentosarja"
+ ],
+ "Visit firewall": [
+ null,
+ "Käy palomuurissa"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Odotetaan muiden ohjelmistojen hallintatoimintojen päättymistä"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Verkkokonsoli Linux-palvelimille"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Väärä käyttäjätunnus tai salasana"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Yhdistät koneeseen $0 ensimmäistä kertaa."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "Selaimesi ei salli liittämistä pikavalikosta. Voit käyttää Vaihto+Insert."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Istuntosi on päätetty."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Istuntosi on vahventunut. Ole hyvä ja kirjaudu uudelleen sisään."
+ ],
+ "Zone": [
+ null,
+ "Alue"
+ ],
+ "[binary data]": [
+ null,
+ "[binääridata]"
+ ],
+ "[no data]": [
+ null,
+ "[ei dataa]"
+ ],
+ "password quality": [
+ null,
+ "salasanan laatu"
+ ],
+ "show less": [
+ null,
+ "näytä vähemmän"
+ ],
+ "show more": [
+ null,
+ "näytä enemmän"
+ ]
+};
diff --git a/dist/static/po.fr.js b/dist/static/po.fr.js
new file mode 100644
index 0000000..8190647
--- /dev/null
+++ b/dist/static/po.fr.js
@@ -0,0 +1,933 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n > 1,
+ "language": "fr",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 jour",
+ "$0 jours"
+ ],
+ "$0 error": [
+ null,
+ "$0 erreur"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 quitté avec le code $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 échoué"
+ ],
+ "$0 hour": [
+ null,
+ "$0 heure",
+ "$0 heures"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 n’est disponible dans aucun référentiel."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 clé modifiée"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 killed avec un signal $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minute",
+ "$0 minutes"
+ ],
+ "$0 month": [
+ null,
+ "$0 mois",
+ "$0 mois"
+ ],
+ "$0 week": [
+ null,
+ "$0 semaine",
+ "$0 semaines"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 sera installé."
+ ],
+ "$0 year": [
+ null,
+ "$0 an",
+ "$0 ans"
+ ],
+ "1 day": [
+ null,
+ "1 jour"
+ ],
+ "1 hour": [
+ null,
+ "1 heure"
+ ],
+ "1 minute": [
+ null,
+ "1 minute"
+ ],
+ "1 week": [
+ null,
+ "1 semaine"
+ ],
+ "20 minutes": [
+ null,
+ "20 minutes"
+ ],
+ "40 minutes": [
+ null,
+ "40 minutes"
+ ],
+ "5 minutes": [
+ null,
+ "5 minutes"
+ ],
+ "6 hours": [
+ null,
+ "6 heures"
+ ],
+ "60 minutes": [
+ null,
+ "60 minutes"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Un navigateur moderne est nécessaire pour la sécurité, la fiabilité et la performance."
+ ],
+ "Absent": [
+ null,
+ "Absent"
+ ],
+ "Accept key and log in": [
+ null,
+ "Accepter la clé et se connecter"
+ ],
+ "Add $0": [
+ null,
+ "Ajouter $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Paquets supplémentaires :"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Administration avec la console web de Cockpit"
+ ],
+ "Advanced TCA": [
+ null,
+ "TCA avancé"
+ ],
+ "All-in-one": [
+ null,
+ "Tout en un"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Documentation des rôles Ansible"
+ ],
+ "Authentication failed": [
+ null,
+ "Échec d’authentification"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Échec d’authentification : connexion close par le serveur"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Une authentification est nécessaire pour effectuer les tâches privilégiées dans la console web Cockpit"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Utiliser automatiquement NTP"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Utilisation automatique de serveurs NTP supplémentaires"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Utiliser automatiquement des serveurs NTP spécifiques"
+ ],
+ "Automation script": [
+ null,
+ "Script d’automatisation"
+ ],
+ "Blade": [
+ null,
+ "Lame"
+ ],
+ "Blade enclosure": [
+ null,
+ "Boîtier en lame"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Châssis d’extension de bus"
+ ],
+ "Cancel": [
+ null,
+ "Annuler"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Impossible de transférer les identifiants de connexion"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Impossible de planifier un événement dans le passé"
+ ],
+ "Change": [
+ null,
+ "Modification"
+ ],
+ "Change system time": [
+ null,
+ "Modifier l’heure du système"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Les changements de clés sont souvent le résultat d’une réinstallation du système d’exploitation. Cependant, un changement inattendu peut indiquer une tentative d’interception de votre connexion par un tiers."
+ ],
+ "Checking installed software": [
+ null,
+ "Vérification des logiciels installés"
+ ],
+ "Close": [
+ null,
+ "Fermer"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "L’authentification de Cockpit est mal configurée."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "Configuration du cockpit de NetworkManager et Firewalld"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit n’a pas pu contacter l’hôte indiqué."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit est un gestionnaire de serveur qui facilite l’administration de vos serveurs Linux via un navigateur Web. Sauter entre le terminal et l’outil web n’est pas un problème. Un service démarré via Cockpit peut être arrêté via le terminal. De même, si une erreur se produit dans le terminal, elle s’affiche dans l’interface du journal Cockpit."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit n’est pas compatible avec le logiciel sur le système."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit n’est pas installé sur le système."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit est parfait pour les nouveaux administrateurs système, leur permettant d’effectuer facilement des tâches simples telles que l’administration du stockage, l’inspection des journaux, le démarrage et l’arrêt des services. Vous pouvez surveiller et administrer plusieurs serveurs en même temps. Il suffit de les ajouter en un seul clic et vos machines s’occuperont de leurs pairs."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit peut ne pas s'afficher correctement dans votre navigateur"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Collecte et regroupement des données de diagnostic et d'assistance"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Collecter les vidages de mémoire sur incidents du noyau"
+ ],
+ "Compact PCI": [
+ null,
+ "PCI compact"
+ ],
+ "Connect to": [
+ null,
+ "Se connecter à"
+ ],
+ "Connect to:": [
+ null,
+ "Se connecter à :"
+ ],
+ "Connection has timed out.": [
+ null,
+ "La connexion a expiré."
+ ],
+ "Convertible": [
+ null,
+ "Convertible"
+ ],
+ "Copy": [
+ null,
+ "Copier"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Copier dans le presse-papier"
+ ],
+ "Create": [
+ null,
+ "Créer"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Créez un nouveau fichier de tâches avec ce contenu."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Inser"
+ ],
+ "Delay": [
+ null,
+ "Retard"
+ ],
+ "Desktop": [
+ null,
+ "Bureau"
+ ],
+ "Detachable": [
+ null,
+ "Détachable"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Rapports de diagnostic"
+ ],
+ "Docking station": [
+ null,
+ "Station d’accueil"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Télécharger un nouveau navigateur gratuitement"
+ ],
+ "Downloading $0": [
+ null,
+ "Téléchargement $0"
+ ],
+ "Dual rank": [
+ null,
+ "Double rang"
+ ],
+ "Embedded PC": [
+ null,
+ "PC intégré"
+ ],
+ "Excellent password": [
+ null,
+ "Mot de passe excellent"
+ ],
+ "Expansion chassis": [
+ null,
+ "Châssis d’extension"
+ ],
+ "Failed to change password": [
+ null,
+ "Échec de la modification du mot de passe"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "Échec de l’activation $0 dans firewalld"
+ ],
+ "Go to now": [
+ null,
+ "Aller à maintenant"
+ ],
+ "Handheld": [
+ null,
+ "Portable"
+ ],
+ "Host key is incorrect": [
+ null,
+ "La clé de l’hôte est incorrecte"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Si l’empreinte digitale correspond, cliquez sur « Accepter la clé et se connecter ». Sinon, ne vous connectez pas et contactez votre administrateur."
+ ],
+ "Install": [
+ null,
+ "Installer"
+ ],
+ "Install software": [
+ null,
+ "Installer le logiciel"
+ ],
+ "Installing $0": [
+ null,
+ "Installation de $0"
+ ],
+ "Internal error": [
+ null,
+ "Erreur interne"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Erreur interne : en-tête de la question de Challenge non valide"
+ ],
+ "Invalid date format": [
+ null,
+ "Format de date non valide"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Format de date non valide et format d’heure non valide"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Les autorisations de fichier ne sont pas valides"
+ ],
+ "Invalid time format": [
+ null,
+ "Format d’heure non valide"
+ ],
+ "Invalid timezone": [
+ null,
+ "Fuseau horaire incorrect"
+ ],
+ "IoT gateway": [
+ null,
+ "Passerelle IoT"
+ ],
+ "Kernel dump": [
+ null,
+ "Kernel Dump"
+ ],
+ "Laptop": [
+ null,
+ "Portable"
+ ],
+ "Learn more": [
+ null,
+ "En savoir plus"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Chargement des modifications système..."
+ ],
+ "Log in": [
+ null,
+ "Connexion"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Connectez-vous avec votre compte d’utilisateur du serveur."
+ ],
+ "Log messages": [
+ null,
+ "Enregistrer les messages"
+ ],
+ "Login": [
+ null,
+ "Nom d’utilisateur"
+ ],
+ "Login again": [
+ null,
+ "Se connecter à nouveau"
+ ],
+ "Login failed": [
+ null,
+ "Échec de la connexion"
+ ],
+ "Logout successful": [
+ null,
+ "Déconnexion réussie"
+ ],
+ "Low profile desktop": [
+ null,
+ "Bureau de profil bas"
+ ],
+ "Lunch box": [
+ null,
+ "Lunch Box"
+ ],
+ "Main server chassis": [
+ null,
+ "Châssis principal du serveur"
+ ],
+ "Manage storage": [
+ null,
+ "Gérer le stockage"
+ ],
+ "Manually": [
+ null,
+ "Manuellement"
+ ],
+ "Message to logged in users": [
+ null,
+ "Message aux utilisateurs connectés"
+ ],
+ "Mini PC": [
+ null,
+ "Mini PC"
+ ],
+ "Mini tower": [
+ null,
+ "Mini Tour"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Châssis multi-système"
+ ],
+ "NTP server": [
+ null,
+ "Serveur NTP"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Besoin d’au moins un serveur NTP"
+ ],
+ "Networking": [
+ null,
+ "Réseau"
+ ],
+ "New host": [
+ null,
+ "Nouvel hôte"
+ ],
+ "New password was not accepted": [
+ null,
+ "Le nouveau mot de passe n’a pas été accepté"
+ ],
+ "No delay": [
+ null,
+ "Aucun délai"
+ ],
+ "No such file or directory": [
+ null,
+ "Aucun fichier ou répertoire de ce nom"
+ ],
+ "No system modifications": [
+ null,
+ "Aucune modification système"
+ ],
+ "Not a valid private key": [
+ null,
+ "Clé privée non valide"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Non autorisé à effectuer cette action."
+ ],
+ "Not synchronized": [
+ null,
+ "Non synchronisé"
+ ],
+ "Notebook": [
+ null,
+ "Ordinateur portable"
+ ],
+ "Occurrences": [
+ null,
+ "Occurrences"
+ ],
+ "Ok": [
+ null,
+ "Ok"
+ ],
+ "Old password not accepted": [
+ null,
+ "Ancien mot de passe non accepté"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Une fois que Cockpit est installé, l’activer avec « systemctl enable --now cockpit.socket »."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Ou utilisez un navigateur groupé"
+ ],
+ "Other": [
+ null,
+ "Autre"
+ ],
+ "Other options": [
+ null,
+ "Autres options"
+ ],
+ "PackageKit crashed": [
+ null,
+ "Plantage de « PackageKit »"
+ ],
+ "Password": [
+ null,
+ "Mot de passe"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Le mot de passe n’est pas acceptable"
+ ],
+ "Password is too weak": [
+ null,
+ "Le mot de passe est trop faible"
+ ],
+ "Password not accepted": [
+ null,
+ "Mot de passe non accepté"
+ ],
+ "Paste": [
+ null,
+ "Coller"
+ ],
+ "Paste error": [
+ null,
+ "Erreur de collage"
+ ],
+ "Path to file": [
+ null,
+ "Chemin d’accès au fichier"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Châssis périphérique"
+ ],
+ "Permission denied": [
+ null,
+ "Permission refusée"
+ ],
+ "Pick date": [
+ null,
+ "Choisissez une date"
+ ],
+ "Pizza box": [
+ null,
+ "Pizza Box"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Veuillez activer JavaScript pour utiliser la console Web."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Veuillez spécifier l’hôte auquel vous connecter"
+ ],
+ "Portable": [
+ null,
+ "Portable"
+ ],
+ "Present": [
+ null,
+ "Présent"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "L’invite via ssh-add a expiré"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "L’invite via ssh-keygen a expiré"
+ ],
+ "RAID chassis": [
+ null,
+ "Châssis RAID"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Châssis de montage en rack"
+ ],
+ "Reboot": [
+ null,
+ "Redémarrer"
+ ],
+ "Recent hosts": [
+ null,
+ "Hôtes récents"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Connexion refusée. L’hôte est inconnu"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Connexion refusée. Hostkey ne correspond pas"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Connexion refusée. Hostkey est inconnu"
+ ],
+ "Removals:": [
+ null,
+ "Suppressions :"
+ ],
+ "Remove host": [
+ null,
+ "Retirer l’hôte"
+ ],
+ "Removing $0": [
+ null,
+ "Suppression de $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "PC scellé"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Configuration et dépannage de Linux avec renforcement de la sécurité"
+ ],
+ "Server": [
+ null,
+ "Serveur"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Le serveur a fermé la connexion."
+ ],
+ "Set time": [
+ null,
+ "Régler l’heure"
+ ],
+ "Shell script": [
+ null,
+ "Script d’interpréteur de commande"
+ ],
+ "Shift+Insert": [
+ null,
+ "Maj+Inser"
+ ],
+ "Shut down": [
+ null,
+ "Fermeture"
+ ],
+ "Single rank": [
+ null,
+ "Rang unique"
+ ],
+ "Space-saving computer": [
+ null,
+ "Ordinateur gain de place"
+ ],
+ "Specific time": [
+ null,
+ "Temps spécifique"
+ ],
+ "Stick PC": [
+ null,
+ "Stick PC"
+ ],
+ "Storage": [
+ null,
+ "Stockage"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Sous-châssis"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Sub-Notebook"
+ ],
+ "Synchronized": [
+ null,
+ "Synchronisé"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Synchronisé avec $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Synchronisation"
+ ],
+ "Tablet": [
+ null,
+ "Tablette"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "L’utilisateur actuellement connecté n’est pas autorisé à voir les modifications système"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Le mot de passe ne correspond pas."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "L’empreinte digitale qui en résulte peut être partagée via des méthodes publiques, y compris par courrier électronique."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Le serveur a refusé d’authentifier « $0 » en utilisant l’authentification par mot de passe, et aucune autre méthode d’authentification prise en charge n’est disponible."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Le serveur a refusé d’authentifier en utilisant des méthodes prises en charge."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "La configuration du navigateur Web empêche l’exécution de Cockpit (inaccessible $0 )"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "Cet outil configure la politique SELinux et peut aider à comprendre et à résoudre les violations de la politique."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "Cet outil configure le système pour qu'il écrive les vidages de mémoire sur incidents du noyau sur le disque."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "Cet outil génère une archive des informations de configuration et de diagnostic du système en cours d'exécution. Ces archives peuvent être stockées localement ou centralement à des fins d'enregistrement ou de suivi, ou être envoyées aux représentants du support technique, aux développeurs ou aux administrateurs système pour les aider dans la recherche d'erreurs techniques et le débogage."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "Cet outil gère le stockage local, tel que les systèmes de fichiers, les groupes de volumes LVM2 et les montages NFS."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "Cet outil gère les réseaux tels que les liens, les ponts, les équipes, les VLAN et les pare-feu en utilisant NetworkManager et Firewalld. NetworkManager est incompatible avec les scripts par défaut systemd-networkd d'Ubuntu et ifupdown de Debian."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Ce navigateur est trop vieux pour faire fonctionner la console Web (manquant $0)"
+ ],
+ "Time zone": [
+ null,
+ "Fuseau horaire"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Pour vous assurer que votre connexion n’est pas interceptée par un tiers malveillant, veuillez vérifier l’empreinte de la clé de l’hôte :"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Pour vérifier une empreinte digitale, exécutez les opérations suivantes sur $0 en étant physiquement assis devant la machine ou en passant par un réseau de confiance :"
+ ],
+ "Toggle date picker": [
+ null,
+ "Basculer le sélecteur de date"
+ ],
+ "Too much data": [
+ null,
+ "Trop de données"
+ ],
+ "Total size: $0": [
+ null,
+ "Taille totale : $0"
+ ],
+ "Tower": [
+ null,
+ "Tower"
+ ],
+ "Try again": [
+ null,
+ "Réessayer"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Essaye de synchroniser avec $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Incapable de se connecter à cette adresse"
+ ],
+ "Unknown": [
+ null,
+ "Inconnu"
+ ],
+ "Untrusted host": [
+ null,
+ "Hôte non sécurisé"
+ ],
+ "User name": [
+ null,
+ "Nom d’utilisateur"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Le nom d’utilisateur ne peut pas être vide"
+ ],
+ "Validating authentication token": [
+ null,
+ "Validation du jeton d’authentification"
+ ],
+ "View all logs": [
+ null,
+ "Voir tous les journaux"
+ ],
+ "View automation script": [
+ null,
+ "Afficher le script d’automation"
+ ],
+ "Visit firewall": [
+ null,
+ "Visitez le pare-feu"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Attente de la fin des autres opérations de gestion du logiciel"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Console web pour serveurs Linux"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Mauvais nom d’utilisateur ou mot de passe"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Vous vous connectez à $0 pour la première fois."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "Votre navigateur n’autorise pas le collage à partir du menu contextuel. Vous pouvez utiliser Maj+Insérer."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Votre session a été interrompue."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Votre session a expiré. Veuillez vous reconnecter."
+ ],
+ "Zone": [
+ null,
+ "Zone"
+ ],
+ "[binary data]": [
+ null,
+ "[données binaires]"
+ ],
+ "[no data]": [
+ null,
+ "[pas de données]"
+ ],
+ "password quality": [
+ null,
+ "qualité mot de passe"
+ ],
+ "show less": [
+ null,
+ "montrer moins"
+ ],
+ "show more": [
+ null,
+ "montrer plus"
+ ]
+};
diff --git a/dist/static/po.he.js b/dist/static/po.he.js
new file mode 100644
index 0000000..a757d96
--- /dev/null
+++ b/dist/static/po.he.js
@@ -0,0 +1,961 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => (n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && n % 10 == 0) ? 2 : 3)),
+ "language": "he",
+ "language-direction": "rtl"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "יום",
+ "יומיים",
+ "$0 ימים",
+ "$0 ימים"
+ ],
+ "$0 error": [
+ null,
+ "שגיאת $0"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 הסתיים עם הקוד $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 נכשל"
+ ],
+ "$0 hour": [
+ null,
+ "שעה",
+ "שעתיים",
+ "$0 שעות",
+ "$0 שעות"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 אינו זמין מאף מאגר."
+ ],
+ "$0 key changed": [
+ null,
+ "המפתח $0 השתנה"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 נקטל עם האות $1"
+ ],
+ "$0 minute": [
+ null,
+ "דקה",
+ "$0 דקות",
+ "$0 דקות",
+ "$0 דקות"
+ ],
+ "$0 month": [
+ null,
+ "חודש",
+ "חודשיים",
+ "$0 חודשים",
+ "$0 חודשים"
+ ],
+ "$0 week": [
+ null,
+ "שבוע",
+ "שבועיים",
+ "$0 שבועות",
+ "$0 שבועות"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 יותקן."
+ ],
+ "$0 year": [
+ null,
+ "שנה",
+ "שנתיים",
+ "$0 שנים",
+ "$0 שנים"
+ ],
+ "1 day": [
+ null,
+ "יום"
+ ],
+ "1 hour": [
+ null,
+ "שעה"
+ ],
+ "1 minute": [
+ null,
+ "דקה"
+ ],
+ "1 week": [
+ null,
+ "שבוע"
+ ],
+ "20 minutes": [
+ null,
+ "20 דקות"
+ ],
+ "40 minutes": [
+ null,
+ "40 דקות"
+ ],
+ "5 minutes": [
+ null,
+ "5 דקות"
+ ],
+ "6 hours": [
+ null,
+ "6 שעות"
+ ],
+ "60 minutes": [
+ null,
+ "60 דקות"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "נדרש דפדפן עדכני לטובת אבטחה, אמינות וביצועים."
+ ],
+ "Absent": [
+ null,
+ "חסר"
+ ],
+ "Accept key and log in": [
+ null,
+ "לקבל את המפתח ולהיכנס"
+ ],
+ "Acceptable password": [
+ null,
+ "סיסמה מקובלת"
+ ],
+ "Add $0": [
+ null,
+ "הוספת $0"
+ ],
+ "Additional packages:": [
+ null,
+ "חבילות נוספות:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "ניהול עם המסוף המקוון Cockpit"
+ ],
+ "Advanced TCA": [
+ null,
+ "TCA מתקדם"
+ ],
+ "All-in-one": [
+ null,
+ "אול אין ואן"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "תיעוד תפקידים של Ansible"
+ ],
+ "Authentication failed": [
+ null,
+ "האימות נכשל"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "האימות נכשל: השרת סגר את החיבור"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "נדרש אימות כדי לבצע משימות שדורשות השראה עם המסוף המקוון Cockpit"
+ ],
+ "Automatically using NTP": [
+ null,
+ "אוטומטית באמצעות NTP"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "אוטומטית באמצעות שרתי NTP נוספים"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "אוטומטית באמצעות שרתי NTP מסוימים"
+ ],
+ "Automation script": [
+ null,
+ "סקריפט אוטומטי"
+ ],
+ "Blade": [
+ null,
+ "בלייד"
+ ],
+ "Blade enclosure": [
+ null,
+ "אריזת בלייד"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "שלדת הרחבת אפיקים"
+ ],
+ "Cancel": [
+ null,
+ "ביטול"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "לא ניתן להעביר פרטי גישה"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "לא ניתן לתזמן אירוע לעבר"
+ ],
+ "Change": [
+ null,
+ "החלפה"
+ ],
+ "Change system time": [
+ null,
+ "החלפת שעון המערכת"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "מפתחות שהוחלפו הם לעתים תוצאה של התקנת מערכת הפעלה מחדש. עם זאת, שינוי בלתי צפוי עשוי להעיד שגורם צד־שלישי מנסה ליירט את החיבור שלך."
+ ],
+ "Checking installed software": [
+ null,
+ "התכנה שמותקנת נבדקת"
+ ],
+ "Close": [
+ null,
+ "סגירה"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "האימות של Cockpit לא מוגדר נכון."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "ההגדרות של Cockpit ל־NetworkManager ול־Firewalld"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "ל־Cockpit אין אפשרות ליצור קשר עם המארח שסופק."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit הוא מנהל שרתים שמקל על ניהול שרתי הלינוקס שלך דרך הדפדפן. מעבר חטוף בין המסוף והכלי המקוון הוא פשוט וקל. שירות שהופעל דרך Cockpit ניתן לעצור דרך המסוף. באותו האופן, אם מתרחשת שגיאה במסוף ניתן לצפות בה במנשק היומן של Cockpit."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit אינו תואם לתכנה שרצה על המערכת."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit אינו מותקן על המערכת."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit הוא מושלם למנהלי מערכות מתחילים, מאפשר להם לבצע משימות פשוטות בקלות כגון ניהול אחסון, חקירת יומנים והפעלה ועצירה של שירותים. ניתן לנהל ולעקוב אחר מספר שרתים בו־זמנית. כל שעליך לעשות הוא להוסיף אותם בלחיצה בודדת והמכונות שלך תדאגנה לחברותיהן."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "יכול להיות ש־Cockpit לא יעובד כראוי בדפדפן שלך"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "איסוף ואריזה של נתוני ניתוח ותמיכה"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "איסוף היטלי קריסת ליבה"
+ ],
+ "Compact PCI": [
+ null,
+ "PCI חסכוני"
+ ],
+ "Connect to": [
+ null,
+ "התחברות אל"
+ ],
+ "Connect to:": [
+ null,
+ "התחברות אל:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "הזמן שהוקצב להתחברות תם."
+ ],
+ "Convertible": [
+ null,
+ "מתהפך"
+ ],
+ "Copy": [
+ null,
+ "העתקה"
+ ],
+ "Copy to clipboard": [
+ null,
+ "העתקה ללוח הגזירים"
+ ],
+ "Create": [
+ null,
+ "יצירה"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "יצירת קובץ משימה עם התוכן הזה."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "השהיה"
+ ],
+ "Desktop": [
+ null,
+ "שולחן עבודה"
+ ],
+ "Detachable": [
+ null,
+ "נתיק"
+ ],
+ "Diagnostic reports": [
+ null,
+ "דוחות אבחון"
+ ],
+ "Docking station": [
+ null,
+ "תחנת עגינה"
+ ],
+ "Download a new browser for free": [
+ null,
+ "הורדת דפדפן חדש בחינם"
+ ],
+ "Downloading $0": [
+ null,
+ "$0 בהורדה"
+ ],
+ "Dual rank": [
+ null,
+ "דו־צדדי"
+ ],
+ "Embedded PC": [
+ null,
+ "מחשב משובץ"
+ ],
+ "Excellent password": [
+ null,
+ "סיסמה מצוינת"
+ ],
+ "Expansion chassis": [
+ null,
+ "שלדת הרחבה"
+ ],
+ "Failed to change password": [
+ null,
+ "החלפת הסיסמה נכשלה"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "הפעלת $0 ב־firewalld נכשלה"
+ ],
+ "Go to now": [
+ null,
+ "לעבור כעת"
+ ],
+ "Handheld": [
+ null,
+ "נישא"
+ ],
+ "Hide confirmation password": [
+ null,
+ "הסתרת סיסמת האישור"
+ ],
+ "Hide password": [
+ null,
+ "הסתרת הסיסמה"
+ ],
+ "Host key is incorrect": [
+ null,
+ "מפתח המארח שגוי"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "אם טביעת האצבע תואמת, יש ללחוץ על „לקבל את המפתח להיכנס”. אחרת, לא להתחבר וליצור קשר עם הנהלת המערכת."
+ ],
+ "Install": [
+ null,
+ "התקנה"
+ ],
+ "Install software": [
+ null,
+ "התקנת תכנה"
+ ],
+ "Installing $0": [
+ null,
+ "$0 בהתקנה"
+ ],
+ "Internal error": [
+ null,
+ "שגיאה פנימה"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "שגיאה פנימית: כותרת אתגר שגויה"
+ ],
+ "Invalid date format": [
+ null,
+ "מבנה התאריך שגוי"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "מבנה תאריך שגוי ומבנה שעה שגוי"
+ ],
+ "Invalid file permissions": [
+ null,
+ "הרשאות הקובץ שגויות"
+ ],
+ "Invalid time format": [
+ null,
+ "מבנה השעה שגוי"
+ ],
+ "Invalid timezone": [
+ null,
+ "אזור זמן שגוי"
+ ],
+ "IoT gateway": [
+ null,
+ "שער גישה IoT"
+ ],
+ "Kernel dump": [
+ null,
+ "היטל ליבה"
+ ],
+ "Laptop": [
+ null,
+ "מחשב נייד"
+ ],
+ "Learn more": [
+ null,
+ "מידע נוסף"
+ ],
+ "Loading system modifications...": [
+ null,
+ "השינויים למערכת נטענים…"
+ ],
+ "Log in": [
+ null,
+ "כניסה"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "כניסה עם חשבון המשתמש שלך בשרת."
+ ],
+ "Log messages": [
+ null,
+ "הודעות יומן"
+ ],
+ "Login": [
+ null,
+ "כניסה"
+ ],
+ "Login again": [
+ null,
+ "נא להיכנס שוב"
+ ],
+ "Login failed": [
+ null,
+ "הכניסה נכשלה"
+ ],
+ "Logout successful": [
+ null,
+ "היציאה הצליחה"
+ ],
+ "Low profile desktop": [
+ null,
+ "מחשב שולחני עם פרופיל נמוך"
+ ],
+ "Lunch box": [
+ null,
+ "קופסת אוכל"
+ ],
+ "Main server chassis": [
+ null,
+ "שלדת שרת ראשית"
+ ],
+ "Manage storage": [
+ null,
+ "ניהול אחסון"
+ ],
+ "Manually": [
+ null,
+ "ידנית"
+ ],
+ "Message to logged in users": [
+ null,
+ "הודעה למשתמשים שנמצאים במערכת"
+ ],
+ "Mini PC": [
+ null,
+ "מחשב מוקטן"
+ ],
+ "Mini tower": [
+ null,
+ "מארז מוקטן"
+ ],
+ "Multi-system chassis": [
+ null,
+ "שלדה למגוון מערכות"
+ ],
+ "NTP server": [
+ null,
+ "שרת NTP"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "נדרש שרת NTP אחד לפחות"
+ ],
+ "Networking": [
+ null,
+ "תקשורת"
+ ],
+ "New host": [
+ null,
+ "מארח חדש"
+ ],
+ "New password was not accepted": [
+ null,
+ "הסיסמה החדשה לא התקבלה"
+ ],
+ "No delay": [
+ null,
+ "אין השהיה"
+ ],
+ "No such file or directory": [
+ null,
+ "אין קובץ או תיקייה בשם הזה"
+ ],
+ "No system modifications": [
+ null,
+ "אין שינויים במערכת"
+ ],
+ "Not a valid private key": [
+ null,
+ "לא מפתח פרטי תקני"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "לא מורשה לבצע את הפעולה הזאת."
+ ],
+ "Not synchronized": [
+ null,
+ "לא מסונכרן"
+ ],
+ "Notebook": [
+ null,
+ "מחברת"
+ ],
+ "Occurrences": [
+ null,
+ "מופעים"
+ ],
+ "Ok": [
+ null,
+ "אישור"
+ ],
+ "Old password not accepted": [
+ null,
+ "הסיסמה הישנה לא התקבלה"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "לאחר התקנת Cockpit, יש להפעיל את השירות בעזרת „systemctl enable --now cockpit.socket”."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "או להשתמש בדפדפן מובנה"
+ ],
+ "Other": [
+ null,
+ "אחר"
+ ],
+ "Other options": [
+ null,
+ "אפשרויות אחרות"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit קרס"
+ ],
+ "Password": [
+ null,
+ "סיסמה"
+ ],
+ "Password is not acceptable": [
+ null,
+ "הסיסמה לא מקובלת"
+ ],
+ "Password is too weak": [
+ null,
+ "הסיסמה חלשה מדי"
+ ],
+ "Password not accepted": [
+ null,
+ "הסיסמה לא התקבלה"
+ ],
+ "Paste": [
+ null,
+ "הדבקה"
+ ],
+ "Paste error": [
+ null,
+ "שגיאת הדבקה"
+ ],
+ "Path to file": [
+ null,
+ "הנתיב לקובץ"
+ ],
+ "Peripheral chassis": [
+ null,
+ "שלדת התקנים חיצוניים"
+ ],
+ "Permission denied": [
+ null,
+ "ההרשאה נדחתה"
+ ],
+ "Pick date": [
+ null,
+ "בחירת תאריך"
+ ],
+ "Pizza box": [
+ null,
+ "קופסת פיצה"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "נא להפעיל JavaScript כדי להשתמש במסוף לדפדפן."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "נא לציין את המארח לחיבור"
+ ],
+ "Portable": [
+ null,
+ "נייד"
+ ],
+ "Present": [
+ null,
+ "נוכחי"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "משך הצגת הבקשה דרך ssh-add תם"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "משך הצגת הבקשה דרך ssh-keygen תם"
+ ],
+ "RAID chassis": [
+ null,
+ "שלדת RAID"
+ ],
+ "Rack mount chassis": [
+ null,
+ "שלדת מעגן מתלה (Rack)"
+ ],
+ "Reboot": [
+ null,
+ "הפעלה מחדש"
+ ],
+ "Recent hosts": [
+ null,
+ "מארחים אחרונים"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "החיבור מסורב. המארח אינו מוכר"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "החיבור מסורב. מפתח המארח לא תואם"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "החיבור מסורב. מפתח המארח אינו מוכר"
+ ],
+ "Removals:": [
+ null,
+ "הסרות:"
+ ],
+ "Remove host": [
+ null,
+ "הסרת מארח"
+ ],
+ "Removing $0": [
+ null,
+ "$0 בהסרה"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "מחשב במארז אטום"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "הגדרות ופתרון תקלות של Security Enhanced Linux"
+ ],
+ "Server": [
+ null,
+ "שרת"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "השרת סגר את החיבור."
+ ],
+ "Set time": [
+ null,
+ "הגדרת שעה"
+ ],
+ "Shell script": [
+ null,
+ "סקריפט מעטפת"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Show password": [
+ null,
+ "הצגת הסיסמה"
+ ],
+ "Shut down": [
+ null,
+ "כיבוי"
+ ],
+ "Single rank": [
+ null,
+ "שורה אחת"
+ ],
+ "Space-saving computer": [
+ null,
+ "מחשב חסכוני במקום"
+ ],
+ "Specific time": [
+ null,
+ "זמן מסוים"
+ ],
+ "Stick PC": [
+ null,
+ "מחשב מקלון"
+ ],
+ "Storage": [
+ null,
+ "אחסון"
+ ],
+ "Sub-Chassis": [
+ null,
+ "תת שלדה"
+ ],
+ "Sub-Notebook": [
+ null,
+ "תת מחברת"
+ ],
+ "Synchronized": [
+ null,
+ "מסונכרן"
+ ],
+ "Synchronized with $0": [
+ null,
+ "מסונכרן עם $0"
+ ],
+ "Synchronizing": [
+ null,
+ "מתבצע סנכרון"
+ ],
+ "Tablet": [
+ null,
+ "מחשב לוח"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "המשתמש הנוכחי שנכנס למערכת אינו מורשה לצפות בשינויים שבוצעו במערכת"
+ ],
+ "The passwords do not match.": [
+ null,
+ "הסיסמאות אינן תואמות."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "זה בסדר לשתף את טביעת האצבע באופן ציבורי, לרבות בדוא״ל."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "השרת סירב לאמת את ‚$0’ עם אימות בסיסמה ואין שיטות אימות אחרות נתמכות."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "השרת סירב לאמת בעזרת השיטות הנתמכות."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "הגדרות הדפדפן מונעות מ־Cockpit לפעול ($0 בלתי נגיש)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "הכלי הזה מגדיר את המדיניות של SELinux ויכול לסייע והבנת ופתרון הפרות של המדיניות."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "הכלי הזה מגדיר למערכת לכתוב את היטלי הקריסה של הליבה לכונן."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "כלי זה מייצר ארכיון של הגדרות ופרטי ניתוח של המערכת. אפשר לאחסן את הארכיון מקומית או באופן מרכזי למטרות תיעוד או מעקב או לנציגי תמיכה, מתכנתים או מנהלי מערכות כדי שיוכלו לסייע באיתור תקלות טכניות וניפוי שגיאות."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "כלי זה מנהל אחסון מקומי כגון מערכות קבצים, קבוצות כרכים ב־LVM2 ועיגונים של NFS."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ ""
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "דפדפן זה מיושן מכדי להריץ את המסוף לדפדפן (חסר $0)"
+ ],
+ "Time zone": [
+ null,
+ "אזור זמן"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "כדי לוודא שהחיבור שלך לא מיורט על ידי גורמי צד־שלישי זדוניים, נא לאמת את טביעת האצבע של מפתח המארח:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "כדי לאמת את טביעת האצבע, יש להריץ את הפקודה הבאה על $0 במהלך ישיבה פיזית מול המכונה או דרך רשת מהימנה:"
+ ],
+ "Toggle date picker": [
+ null,
+ "החלפת מצב בורר תאריכים"
+ ],
+ "Too much data": [
+ null,
+ "יותר מדי נתונים"
+ ],
+ "Total size: $0": [
+ null,
+ "גודל כולל: $0"
+ ],
+ "Tower": [
+ null,
+ "מארז גבוה"
+ ],
+ "Try again": [
+ null,
+ "לנסות שוב"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "מתבצע ניסיון להסתנכרן מול $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "לא ניתן להתחבר לכתובת הזו"
+ ],
+ "Unknown": [
+ null,
+ "לא ידוע"
+ ],
+ "Untrusted host": [
+ null,
+ "מארח בלתי מהימן"
+ ],
+ "User name": [
+ null,
+ "שם משתמש"
+ ],
+ "User name cannot be empty": [
+ null,
+ "שם המשתמש לא יכול להיות ריק"
+ ],
+ "Validating authentication token": [
+ null,
+ "אסימון האימות מתוקף"
+ ],
+ "View all logs": [
+ null,
+ "הצגת כל היומנים"
+ ],
+ "View automation script": [
+ null,
+ "הצגת סקריפט אוטומציה"
+ ],
+ "Visit firewall": [
+ null,
+ "ביקור בחומת האש"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "בהמתנה לסיום פעולות ניהול תכנה אחרות"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "מסוף מקוון לשרתי לינוקס"
+ ],
+ "Wrong user name or password": [
+ null,
+ "שם משתמש או סיסמה שגויים"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "זאת ההתחברות הראשונה שלך אל $0."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "הדפדפן שלך לא מרשה להדביק מתפריט ההקשר. אפשר להשתמש ב־Shift+Insert."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "ההפעלה שלך הושמדה."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "תוקף ההפעלה שלך פג. נא להיכנס שוב."
+ ],
+ "Zone": [
+ null,
+ "אזור"
+ ],
+ "[binary data]": [
+ null,
+ "[נתונים בינריים]"
+ ],
+ "[no data]": [
+ null,
+ "[אין נתונים]"
+ ],
+ "password quality": [
+ null,
+ "איכות הסיסמה"
+ ],
+ "show less": [
+ null,
+ "להציג פחות"
+ ],
+ "show more": [
+ null,
+ "להציג יותר"
+ ]
+};
diff --git a/dist/static/po.it.js b/dist/static/po.it.js
new file mode 100644
index 0000000..0da2307
--- /dev/null
+++ b/dist/static/po.it.js
@@ -0,0 +1,917 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "it",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 giorno",
+ "$0 giorni"
+ ],
+ "$0 error": [
+ null,
+ "Errore $0"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 terminato con codice $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 fallito"
+ ],
+ "$0 hour": [
+ null,
+ "$0 ora",
+ "$0 ore"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 non è disponibile in nessun archivio web."
+ ],
+ "$0 key changed": [
+ null,
+ "Chiave $0 modificata"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 terminato con codice $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minuto",
+ "$0 minuti"
+ ],
+ "$0 month": [
+ null,
+ "$0 mese",
+ "$0 mesi"
+ ],
+ "$0 week": [
+ null,
+ "$0 settimana",
+ "$0 settimane"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 sarà installato."
+ ],
+ "$0 year": [
+ null,
+ "$0 anno",
+ "$0 anni"
+ ],
+ "1 day": [
+ null,
+ "1 giorno"
+ ],
+ "1 hour": [
+ null,
+ "1 ora"
+ ],
+ "1 minute": [
+ null,
+ "1 minuto"
+ ],
+ "1 week": [
+ null,
+ "1 settimana"
+ ],
+ "20 minutes": [
+ null,
+ "20 minuti"
+ ],
+ "40 minutes": [
+ null,
+ "40 minuti"
+ ],
+ "5 minutes": [
+ null,
+ "5 minuti"
+ ],
+ "6 hours": [
+ null,
+ "6 ore"
+ ],
+ "60 minutes": [
+ null,
+ "60 minuti"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Un browser moderno è necessario per la sicurezza, l'affidabilità e le prestazioni."
+ ],
+ "Absent": [
+ null,
+ "Assente"
+ ],
+ "Accept key and log in": [
+ null,
+ "Accetta la chiave ed effettua l'accesso"
+ ],
+ "Add $0": [
+ null,
+ "Aggiungi $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Pacchetti aggiuntivi:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Amministrazione con Cockpit Web Console"
+ ],
+ "Advanced TCA": [
+ null,
+ "TCA avanzato"
+ ],
+ "All-in-one": [
+ null,
+ "Tutto in una volta"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Documentazione sui ruoli Ansible"
+ ],
+ "Authentication failed": [
+ null,
+ "Autenticazione fallita"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Autenticazione fallita: il server ha terminato la connessione"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "E' necessario autenticarsi per eseguire azioni privilegiate con Cockpit Web Console"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Automaticamente utilizzando NTP"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Automaticamente utilizzando server NTP addizionali"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Automaticamente utilizzando server NTP specifici"
+ ],
+ "Automation script": [
+ null,
+ "Script di automazione"
+ ],
+ "Blade": [
+ null,
+ "Blade"
+ ],
+ "Blade enclosure": [
+ null,
+ "Chassis del blade"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Bus di espansione chassis"
+ ],
+ "Cancel": [
+ null,
+ "Annulla"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Impossibile inoltrare le credenziali di accesso"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Non è possibile programmare eventi del passato"
+ ],
+ "Change": [
+ null,
+ "Cambia"
+ ],
+ "Change system time": [
+ null,
+ "Modifica dell'ora di sistema"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "La modifica delle chiavi sono di solito il risultato di una reinstallazione del sistema. Tuttavia, una modifica inaspettata può indicare un tentativo di intercettazione da parte di un soggetto terzo."
+ ],
+ "Checking installed software": [
+ null,
+ "Verifica del software installato"
+ ],
+ "Close": [
+ null,
+ "Chiudi"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "L'autenticazione di Cockpit non è configurata correttamente."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "Configurazione Cockpit del NetworkManager e del Firewalld"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit non ha potuto contattare l'host inserito."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit è un gestore di server che rende facile amministrare i server Linux tramite un browser web. Cambiare tra il terminale e lo strumento web non è un problema. Un servizio avviato tramite Cockpit può essere interrotto tramite il terminale. Allo stesso modo, se si verifica un errore nel terminale, può essere visto nella sezione del registro di Cockpit."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit non è compatibile con il software del sistema."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit non è installato sul sistema."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit è perfetto per i nuovi amministratori di sistema, consentendo loro di eseguire facilmente semplici operazioni come la gestione dell'archiviazione, l'ispezione del registro e l'avvio e l'arresto dei servizi. È possibile monitorare e amministrare più server allo stesso tempo. Basta aggiungerli con un solo clic e se ne prenderà subito cura."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit potrebbe non essere visualizzato correttamente nel tuo browser"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Raccogliere e creare un pacchetto di dati diagnostici e di supporto"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Acquisisci i dump dei crash del kernel"
+ ],
+ "Compact PCI": [
+ null,
+ "PCI compatto"
+ ],
+ "Connect to": [
+ null,
+ "Collegati a"
+ ],
+ "Connect to:": [
+ null,
+ "Collegati a:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Il collegamento è scaduto."
+ ],
+ "Convertible": [
+ null,
+ "Convertibile"
+ ],
+ "Copy": [
+ null,
+ "Copia"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Copia negli appunti"
+ ],
+ "Create": [
+ null,
+ "Crea"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Crea un nuovo file di attività con questo contenuto."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Ritardo"
+ ],
+ "Desktop": [
+ null,
+ "Desktop"
+ ],
+ "Detachable": [
+ null,
+ "Rimovibile"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Rapporti diagnostici"
+ ],
+ "Docking station": [
+ null,
+ "Stazione di docking"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Scarica gratuitamente un nuovo browser"
+ ],
+ "Downloading $0": [
+ null,
+ "Download di $0"
+ ],
+ "Dual rank": [
+ null,
+ "Dual rank"
+ ],
+ "Embedded PC": [
+ null,
+ "PC integrato"
+ ],
+ "Excellent password": [
+ null,
+ "Password eccellente"
+ ],
+ "Expansion chassis": [
+ null,
+ "Chassis di espansione"
+ ],
+ "Failed to change password": [
+ null,
+ "Impossibile cambiare la password"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "Impossibile abilitare firewalld"
+ ],
+ "Go to now": [
+ null,
+ "Vai ora"
+ ],
+ "Handheld": [
+ null,
+ "Palmare"
+ ],
+ "Host key is incorrect": [
+ null,
+ "La chiave host non è corretta"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Se l'impronta digitale coincide, clicca \"Accetta la chiave e autentica\". Altrimenti, non autenticare e contatta l'amministratore."
+ ],
+ "Install": [
+ null,
+ "Installa"
+ ],
+ "Install software": [
+ null,
+ "Installa il software"
+ ],
+ "Installing $0": [
+ null,
+ "Installazione di $0"
+ ],
+ "Internal error": [
+ null,
+ "Errore interno"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Errore interno: challenge header non valido"
+ ],
+ "Invalid date format": [
+ null,
+ "Formato data non valido"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Formato data non valido e formato ora non valido"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Autorizzazioni file non valide"
+ ],
+ "Invalid time format": [
+ null,
+ "Formato ora non valido"
+ ],
+ "Invalid timezone": [
+ null,
+ "Fuso orario non valido"
+ ],
+ "IoT gateway": [
+ null,
+ "Gateway IoT"
+ ],
+ "Kernel dump": [
+ null,
+ "Kernel dump"
+ ],
+ "Laptop": [
+ null,
+ "Portatile"
+ ],
+ "Learn more": [
+ null,
+ "Per saperne di più"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Caricamento modifiche del sistema..."
+ ],
+ "Log in": [
+ null,
+ "Accedi"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Accedi con il tuo account utente del server."
+ ],
+ "Log messages": [
+ null,
+ "Messaggi di log"
+ ],
+ "Login": [
+ null,
+ "Accesso"
+ ],
+ "Login again": [
+ null,
+ "Effettua di nuovo il login"
+ ],
+ "Login failed": [
+ null,
+ "Login fallito"
+ ],
+ "Logout successful": [
+ null,
+ "Logout riuscito"
+ ],
+ "Low profile desktop": [
+ null,
+ "Desktop a basso profilo"
+ ],
+ "Lunch box": [
+ null,
+ "Lunch box"
+ ],
+ "Main server chassis": [
+ null,
+ "Chassis del server principale"
+ ],
+ "Manage storage": [
+ null,
+ "Gestisci archiviazione"
+ ],
+ "Manually": [
+ null,
+ "Manualmente"
+ ],
+ "Message to logged in users": [
+ null,
+ "Messaggio agli utenti autenticati"
+ ],
+ "Mini PC": [
+ null,
+ "Mini PC"
+ ],
+ "Mini tower": [
+ null,
+ "Mini tower"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Chassis multisistema"
+ ],
+ "NTP server": [
+ null,
+ "Server NTP"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "E' necessario almeno un server NTP"
+ ],
+ "Networking": [
+ null,
+ "Rete"
+ ],
+ "New host": [
+ null,
+ "Nuovo host"
+ ],
+ "New password was not accepted": [
+ null,
+ "La nuova password non è stata accettata"
+ ],
+ "No delay": [
+ null,
+ "Nessun ritardo"
+ ],
+ "No such file or directory": [
+ null,
+ "Nessun file o directory"
+ ],
+ "No system modifications": [
+ null,
+ "Nessuna modifica di sistema"
+ ],
+ "Not a valid private key": [
+ null,
+ "Chiave privata invalida"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Non è consentito eseguire questa azione."
+ ],
+ "Not synchronized": [
+ null,
+ "Non sincronizzato"
+ ],
+ "Notebook": [
+ null,
+ "Portatile"
+ ],
+ "Occurrences": [
+ null,
+ "Occorrenze"
+ ],
+ "Ok": [
+ null,
+ "Ok"
+ ],
+ "Old password not accepted": [
+ null,
+ "Vecchia password non accettata"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Una volta installato Cockpit, abilitarlo con \"systemctl enable --now cockpit.socket\"."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Oppure utilizzare un browser in bundle"
+ ],
+ "Other": [
+ null,
+ "Altro"
+ ],
+ "Other options": [
+ null,
+ "Altre opzioni"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit si è interrotto"
+ ],
+ "Password": [
+ null,
+ "Password"
+ ],
+ "Password is not acceptable": [
+ null,
+ "La password non è accettabile"
+ ],
+ "Password is too weak": [
+ null,
+ "La password è troppo debole"
+ ],
+ "Password not accepted": [
+ null,
+ "Password non accettata"
+ ],
+ "Paste": [
+ null,
+ "Incolla"
+ ],
+ "Paste error": [
+ null,
+ "Incolla errore"
+ ],
+ "Path to file": [
+ null,
+ "Percorso del file"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Chassis periferico"
+ ],
+ "Permission denied": [
+ null,
+ "Permesso negato"
+ ],
+ "Pick date": [
+ null,
+ "Scegli una data"
+ ],
+ "Pizza box": [
+ null,
+ "Pizza box"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Abilita Javascript per usare Web Console"
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Specifica l'host a cui connettersi"
+ ],
+ "Portable": [
+ null,
+ "Portatile"
+ ],
+ "Present": [
+ null,
+ "Presente"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Richiesta tramite ssh-add scaduta"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Richiesta tramite ssh-keygen scaduta"
+ ],
+ "RAID chassis": [
+ null,
+ "Chassis RAID"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Chassis a rack"
+ ],
+ "Reboot": [
+ null,
+ "Riavvia"
+ ],
+ "Recent hosts": [
+ null,
+ "Host recenti"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Rifiuto di connessione. L'host è sconosciuto"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Rifiuto di connessione. Il tasto Hostkey non corrisponde"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Rifiuto di connessione. La chiave host è sconosciuta"
+ ],
+ "Removals:": [
+ null,
+ "Rimozioni:"
+ ],
+ "Remove host": [
+ null,
+ "Rimuovere l'host"
+ ],
+ "Removing $0": [
+ null,
+ "Rimozione $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "PC sigillato"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Configurazione e risoluzione dei problemi di Security Enhanced Linux"
+ ],
+ "Server": [
+ null,
+ "Server"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Il server ha chiuso la connessione."
+ ],
+ "Set time": [
+ null,
+ "Imposta tempo"
+ ],
+ "Shell script": [
+ null,
+ "Script di shell"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Shut down": [
+ null,
+ "Arresto"
+ ],
+ "Single rank": [
+ null,
+ "Single rank"
+ ],
+ "Space-saving computer": [
+ null,
+ "Computer space-saving"
+ ],
+ "Specific time": [
+ null,
+ "Tempo specifico"
+ ],
+ "Stick PC": [
+ null,
+ "Stick PC"
+ ],
+ "Storage": [
+ null,
+ "Archiviazione"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Sub-Chassis"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Sub-Notebook"
+ ],
+ "Synchronized": [
+ null,
+ "Sincronizzato"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Sincronizzato con $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Sincronizzazione"
+ ],
+ "Tablet": [
+ null,
+ "Tablet"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "L'utente che ha effettuato l'accesso non è autorizzato a visualizzare le modifiche di sistema"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Le password non corrispondono."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "L'impronta digitale risultante è idonea per la condivisione pubblica, email inclusa."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Il server ha rifiutato di autenticare '$0' utilizzando l'autenticazione con password, e non sono disponibili altri metodi di autenticazione supportati."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Il server ha rifiutato di autenticarsi utilizzando qualsiasi metodo supportato."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "La configurazione del browser web impedisce l'esecuzione di Cockpit ($0 inaccessibile)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "Questo strumento configura i criteri SELinux e può aiutare a comprendere e risolvere le violazioni dei criteri."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "Questo strumento configura il sistema per scrivere i crash dump del kernel su disco."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "Questo strumento genera un archivio di informazioni sulla configurazione e sulla diagnostica del sistema in esecuzione. L'archivio può essere conservato localmente o centralmente per scopi di registrazione o tracciamento oppure può essere inviato ai rappresentanti dell'assistenza tecnica, agli sviluppatori o agli amministratori di sistema per aiutarli nella ricerca di errori e nel debug."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "Questo strumento gestisce lo storage locale, come i filesystem, i gruppi di volumi LVM2 e i mount NFS."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "Questo strumento gestisce le reti, come i bond, i bridge, i team, le VLAN e i firewall utilizzando NetworkManager e Firewalld. NetworkManager è incompatibile con gli script systemd-networkd di Ubuntu e ifupdown di Debian."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Questo browser web è troppo vecchio per eseguire Web Console ($0 mancante)"
+ ],
+ "Time zone": [
+ null,
+ "Fuso Orario"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Per assicurare che la connessione non sia intercettata da un soggetto terzo malelvolo, verifica l'impronta digitale dell'host:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Per verificare un'impronta digitale, esegui su $0 mentre sei fisicamente di fronte alla macchina o attraverso una rete fidata:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Attiva/disattiva la selezione della data"
+ ],
+ "Too much data": [
+ null,
+ "Troppi dati"
+ ],
+ "Total size: $0": [
+ null,
+ "Dimensione totale: $0"
+ ],
+ "Tower": [
+ null,
+ "Tower"
+ ],
+ "Try again": [
+ null,
+ "Riprova"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Tentativo di sincronizzazione con $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Impossibile connettersi a quell'indirizzo"
+ ],
+ "Unknown": [
+ null,
+ "Sconosciuto"
+ ],
+ "Untrusted host": [
+ null,
+ "Host non fidato"
+ ],
+ "User name": [
+ null,
+ "Nome utente"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Il nome utente non può essere vuoto"
+ ],
+ "Validating authentication token": [
+ null,
+ "Convalida del token di autenticazione"
+ ],
+ "View automation script": [
+ null,
+ "Visualizza script di automazione"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "In attesa che finiscano le altre operazioni di gestione del software"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Web Console per server Linux"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Nome utente o password errata"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Collegamento a $0 per la prima volta"
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "Il tuo browser non consente l'incolla dal menu contestuale. Puoi usare Maiusc+Ins."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "La tua sessione e' terminata."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "La sessione è scaduta. Effettua di nuovo il login."
+ ],
+ "[binary data]": [
+ null,
+ "[dati binari]"
+ ],
+ "[no data]": [
+ null,
+ "[nessun dato]"
+ ],
+ "show less": [
+ null,
+ "mostra meno"
+ ],
+ "show more": [
+ null,
+ "mostra di più"
+ ]
+};
diff --git a/dist/static/po.ja.js b/dist/static/po.ja.js
new file mode 100644
index 0000000..a63fd61
--- /dev/null
+++ b/dist/static/po.ja.js
@@ -0,0 +1,959 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => 0,
+ "language": "ja",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 日"
+ ],
+ "$0 error": [
+ null,
+ "$0 エラー"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 がコード $1 で終了しました"
+ ],
+ "$0 failed": [
+ null,
+ "$0 が失敗しました"
+ ],
+ "$0 hour": [
+ null,
+ "$0 時間"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 は、あらゆるリポジトリーから利用できません。"
+ ],
+ "$0 key changed": [
+ null,
+ "$0 キーが変更されました"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 がシグナル $1 で終了しました"
+ ],
+ "$0 minute": [
+ null,
+ "$0 分"
+ ],
+ "$0 month": [
+ null,
+ "$0 カ月"
+ ],
+ "$0 week": [
+ null,
+ "$0 週"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 がインストールされます。"
+ ],
+ "$0 year": [
+ null,
+ "$0 年"
+ ],
+ "1 day": [
+ null,
+ "1 日"
+ ],
+ "1 hour": [
+ null,
+ "1 時間"
+ ],
+ "1 minute": [
+ null,
+ "1 分"
+ ],
+ "1 week": [
+ null,
+ "1 週間"
+ ],
+ "20 minutes": [
+ null,
+ "20 分"
+ ],
+ "40 minutes": [
+ null,
+ "40 分"
+ ],
+ "5 minutes": [
+ null,
+ "5 分"
+ ],
+ "6 hours": [
+ null,
+ "6 時間"
+ ],
+ "60 minutes": [
+ null,
+ "60 分"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "セキュリティー、信頼性、およびパフォーマンスに対して最新のブラウザーが必要です。"
+ ],
+ "Absent": [
+ null,
+ "不在"
+ ],
+ "Accept key and log in": [
+ null,
+ "キーを受け入れてログイン"
+ ],
+ "Acceptable password": [
+ null,
+ "受け入れられるパスワード"
+ ],
+ "Add $0": [
+ null,
+ "$0 の追加"
+ ],
+ "Additional packages:": [
+ null,
+ "追加のパッケージ:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Cockpit Web コンソールでの管理"
+ ],
+ "Advanced TCA": [
+ null,
+ "高度な TCA"
+ ],
+ "All-in-one": [
+ null,
+ "オールインワン"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Ansible ロールのドキュメント"
+ ],
+ "Authentication failed": [
+ null,
+ "認証に失敗しました"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "認証に失敗しました: サーバーの接続が切断されました"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Cockpit Web コンソールでの特権タスクの実行には、認証が必要です"
+ ],
+ "Automatically using NTP": [
+ null,
+ "NTP を自動的に使用"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "追加の NTP サーバーを自動的に使用"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "特定の NTP サーバーを自動的に使用"
+ ],
+ "Automation script": [
+ null,
+ "オートメーションスクリプト"
+ ],
+ "Blade": [
+ null,
+ "ブレード"
+ ],
+ "Blade enclosure": [
+ null,
+ "ブレードエンクロージャー"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "バス拡張シャーシ"
+ ],
+ "Bypass browser check": [
+ null,
+ "ブラウザーチェックの回避"
+ ],
+ "Cancel": [
+ null,
+ "取り消し"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "ログインのクレデンシャルをフォワードできません"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "過去のイベントはスケジュールできません"
+ ],
+ "Change": [
+ null,
+ "変更"
+ ],
+ "Change system time": [
+ null,
+ "システム時間の変更"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "キーを変更すると、オペレーティングシステムを再インストールすることになることが多くあります。ただし、予期しない変更により接続の傍受が試行される場合もあります。"
+ ],
+ "Checking installed software": [
+ null,
+ "インストールされたソフトウェアの確認中"
+ ],
+ "Close": [
+ null,
+ "閉じる"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Cockpit の認証が間違って設定されています。"
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "Cockpit の NetworkManager と Firewalld の設定"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit は該当するホストに接続できませんでした。"
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit は、Web ブラウザーで Linux サーバーを簡単に管理できるサーバーマネージャーです。端末と Web ツールを区別せずに使用できます。Cockpit で起動されたサービスは端末で停止できます。同様に、端末でエラーが発生した場合は、そのエラーを Cockpit ジャーナルインターフェースで確認できます。"
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit にはシステム上のそのソフトウェアとの互換性がありません。"
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit はシステムにインストールされていません。"
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit は経験が少ないシステム管理者に最適です。これらのシステム管理者はストレージの管理、ジャーナルの検査、サービスの起動および停止などの単純なタスクを簡単に実行できるようになります。また、複数のサーバーを同時に監視および管理できます。これらのサーバーはクリックするだけで追加できます。追加後に、ご使用のマシンは他のマシンを管理するようになります。"
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit がお使いのブラウザーで正しくレンダリングされない可能性があります"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "診断およびサポートデータの収集とパッケージ化"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "カーネルクラッシュダンプの収集"
+ ],
+ "Compact PCI": [
+ null,
+ "PCI の圧縮"
+ ],
+ "Connect to": [
+ null,
+ "接続先"
+ ],
+ "Connect to:": [
+ null,
+ "接続先:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "接続がタイムアウトしました。"
+ ],
+ "Convertible": [
+ null,
+ "変換可能"
+ ],
+ "Copy": [
+ null,
+ "コピー"
+ ],
+ "Copy to clipboard": [
+ null,
+ "クリップボードにコピー"
+ ],
+ "Create": [
+ null,
+ "作成"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "このコンテンツで新しいタスクファイルを作成します。"
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "遅延"
+ ],
+ "Desktop": [
+ null,
+ "デスクトップ"
+ ],
+ "Detachable": [
+ null,
+ "割り当て解除可能"
+ ],
+ "Diagnostic reports": [
+ null,
+ "診断レポート"
+ ],
+ "Docking station": [
+ null,
+ "ドッキングステーション"
+ ],
+ "Download a new browser for free": [
+ null,
+ "新しいブラウザーを無料でダウンロードします"
+ ],
+ "Downloading $0": [
+ null,
+ "$0 をダウンロード中"
+ ],
+ "Dual rank": [
+ null,
+ "デュアルランク"
+ ],
+ "Embedded PC": [
+ null,
+ "組み込み PC"
+ ],
+ "Excellent password": [
+ null,
+ "優れたパスワード"
+ ],
+ "Expansion chassis": [
+ null,
+ "拡張シャーシ"
+ ],
+ "Failed to change password": [
+ null,
+ "パスワードの変更に失敗しました"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "firewalld での $0 の有効化に失敗しました"
+ ],
+ "Go to now": [
+ null,
+ "今すぐ移動"
+ ],
+ "Handheld": [
+ null,
+ "ハンドヘルド"
+ ],
+ "Hide confirmation password": [
+ null,
+ "確認パスワードの非表示"
+ ],
+ "Hide password": [
+ null,
+ "パスワードの非表示"
+ ],
+ "Host key is incorrect": [
+ null,
+ "ホスト鍵が正しくありません"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "フィンガープリントが一致する場合は、Accept key and login をクリックします。一致しない場合は、ログインせずに、管理者にお問い合わせください。"
+ ],
+ "Install": [
+ null,
+ "インストール"
+ ],
+ "Install software": [
+ null,
+ "ソフトウェアをインストール"
+ ],
+ "Installing $0": [
+ null,
+ "$0 をインストール中"
+ ],
+ "Internal error": [
+ null,
+ "内部エラー"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "内部エラー: 無効なチャレンジヘッダー"
+ ],
+ "Invalid date format": [
+ null,
+ "無効な日付形式"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "無効な日付形式と無効な時間形式"
+ ],
+ "Invalid file permissions": [
+ null,
+ "無効なファイルパーミッション"
+ ],
+ "Invalid time format": [
+ null,
+ "無効な時間形式"
+ ],
+ "Invalid timezone": [
+ null,
+ "無効なタイムゾーン"
+ ],
+ "IoT gateway": [
+ null,
+ "IoT ゲートウェイ"
+ ],
+ "Kernel dump": [
+ null,
+ "カーネルダンプ"
+ ],
+ "Laptop": [
+ null,
+ "ラップトップ"
+ ],
+ "Learn more": [
+ null,
+ "もっと詳しく"
+ ],
+ "Loading system modifications...": [
+ null,
+ "システム変更をロード中..."
+ ],
+ "Log in": [
+ null,
+ "ログイン"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "サーバーのユーザーアカウントでログインします。"
+ ],
+ "Log messages": [
+ null,
+ "ログメッセージ"
+ ],
+ "Login": [
+ null,
+ "ログイン"
+ ],
+ "Login again": [
+ null,
+ "再ログイン"
+ ],
+ "Login failed": [
+ null,
+ "ログインが失敗しました"
+ ],
+ "Logout successful": [
+ null,
+ "ログアウトが正常に行われました"
+ ],
+ "Low profile desktop": [
+ null,
+ "低プロファイルデスクトップ"
+ ],
+ "Lunch box": [
+ null,
+ "ランチボックス"
+ ],
+ "Main server chassis": [
+ null,
+ "メインサーバーシャーシ"
+ ],
+ "Manage storage": [
+ null,
+ "ストレージの管理"
+ ],
+ "Manually": [
+ null,
+ "手動"
+ ],
+ "Message to logged in users": [
+ null,
+ "ログインしているユーザーへのメッセージ"
+ ],
+ "Mini PC": [
+ null,
+ "ミニ PC"
+ ],
+ "Mini tower": [
+ null,
+ "ミニタワー"
+ ],
+ "Multi-system chassis": [
+ null,
+ "マルチシステムシャーシ"
+ ],
+ "NTP server": [
+ null,
+ "NTP サーバー"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "少なくとも 1 つの NTP サーバーが必要です"
+ ],
+ "Networking": [
+ null,
+ "ネットワーキング"
+ ],
+ "New host": [
+ null,
+ "新規ホスト"
+ ],
+ "New password was not accepted": [
+ null,
+ "新規パスワードは受け入れられませんでした"
+ ],
+ "No delay": [
+ null,
+ "遅延なし"
+ ],
+ "No such file or directory": [
+ null,
+ "このようなファイルまたはディレクトリーがありません"
+ ],
+ "No system modifications": [
+ null,
+ "システム変更がありません"
+ ],
+ "Not a valid private key": [
+ null,
+ "有効な秘密鍵ではありません"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "この動作を実行する権限がありません。"
+ ],
+ "Not synchronized": [
+ null,
+ "同期されていません"
+ ],
+ "Notebook": [
+ null,
+ "ノートブック"
+ ],
+ "Occurrences": [
+ null,
+ "発生"
+ ],
+ "Ok": [
+ null,
+ "OK"
+ ],
+ "Old password not accepted": [
+ null,
+ "古いパスワードは受け入れられません"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Cockpit がインストールされたら、\"systemctl enable --now cockpit.socket\" コマンドで有効にします。"
+ ],
+ "Or use a bundled browser": [
+ null,
+ "あるいは、バンドルされたブラウザーを使用します"
+ ],
+ "Other": [
+ null,
+ "その他"
+ ],
+ "Other options": [
+ null,
+ "他のオプション"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit がクラッシュしました"
+ ],
+ "Password": [
+ null,
+ "パスワード"
+ ],
+ "Password is not acceptable": [
+ null,
+ "パスワードは受け入れられません"
+ ],
+ "Password is too weak": [
+ null,
+ "パスワードが弱すぎます"
+ ],
+ "Password not accepted": [
+ null,
+ "パスワードは受け入れられません"
+ ],
+ "Paste": [
+ null,
+ "貼り付け"
+ ],
+ "Paste error": [
+ null,
+ "貼り付けエラー"
+ ],
+ "Path to file": [
+ null,
+ "ファイルのパス"
+ ],
+ "Peripheral chassis": [
+ null,
+ "周辺機器シャーシ"
+ ],
+ "Permission denied": [
+ null,
+ "パーミッションが拒否されました"
+ ],
+ "Pick date": [
+ null,
+ "日付けの選択"
+ ],
+ "Pizza box": [
+ null,
+ "Pizza box"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Web コンソールを使用するには JavaScript を有効にしてください。"
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "接続するホストを指定してください"
+ ],
+ "Portable": [
+ null,
+ "ポータブル"
+ ],
+ "Present": [
+ null,
+ "存在"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "ssh-add によるプロンプトがタイムアウトしました"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "ssh-keygen によるプロンプトがタイムアウトしました"
+ ],
+ "RAID chassis": [
+ null,
+ "RAID シャーシ"
+ ],
+ "Rack mount chassis": [
+ null,
+ "ラックマウントシャーシ"
+ ],
+ "Reboot": [
+ null,
+ "再起動"
+ ],
+ "Recent hosts": [
+ null,
+ "直近のホスト"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "接続を拒否しています。ホストが不明です"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "接続を拒否しています。ホストキーが一致しません"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "接続を拒否しています。ホストキーが不明です"
+ ],
+ "Removals:": [
+ null,
+ "削除:"
+ ],
+ "Remove host": [
+ null,
+ "ホストの削除"
+ ],
+ "Removing $0": [
+ null,
+ "$0 を削除中"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "シールドケース PC"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Security Enhanced Linux の設定とトラブルシューティング"
+ ],
+ "Server": [
+ null,
+ "サーバー"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "サーバーの接続が終了しました。"
+ ],
+ "Set time": [
+ null,
+ "時間の設定"
+ ],
+ "Shell script": [
+ null,
+ "シェルスクリプト"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Show confirmation password": [
+ null,
+ "確認パスワードの表示"
+ ],
+ "Show password": [
+ null,
+ "パスワードを表示する"
+ ],
+ "Shut down": [
+ null,
+ "シャットダウン"
+ ],
+ "Single rank": [
+ null,
+ "シングルランク"
+ ],
+ "Space-saving computer": [
+ null,
+ "省スペースコンピューター"
+ ],
+ "Specific time": [
+ null,
+ "特定の時間"
+ ],
+ "Stick PC": [
+ null,
+ "スティッキー PC"
+ ],
+ "Storage": [
+ null,
+ "ストレージ"
+ ],
+ "Strong password": [
+ null,
+ "強固なパスワード"
+ ],
+ "Sub-Chassis": [
+ null,
+ "サブシャーシ"
+ ],
+ "Sub-Notebook": [
+ null,
+ "サブノート"
+ ],
+ "Synchronized": [
+ null,
+ "同期済み"
+ ],
+ "Synchronized with $0": [
+ null,
+ "$0 と同期済み"
+ ],
+ "Synchronizing": [
+ null,
+ "同期中"
+ ],
+ "Tablet": [
+ null,
+ "タブレット"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "ログインしているユーザーには、システム変更を表示する権限がありません"
+ ],
+ "The passwords do not match.": [
+ null,
+ "パスワードが一致しません。"
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "作成されたフィンガープリントは、電子メールを含むパブリックメソッドを介して共有すると問題ありません。"
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "サーバーはパスワード認証を使用した '$0' の認証を拒否しました。サポートされた他の認証方法は利用できません。"
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "サーバーはサポートされた方法を使用した認証を拒否しました。"
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Web ブラウザーの設定により、Cockpit の実行は防がれます (アクセスできない $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "このツールは、SELinux ポリシーを設定します。また、ポリシー違反の把握と解決に役立ちます。"
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "このツールは、カーネルクラッシュダンプをディスクに書き込むようにシステムを設定します。"
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "このツールは、実行中のシステムから設定および診断情報のアーカイブを生成します。アーカイブは、記録や追跡の目的でローカルまたは一元的に保存することも、技術的な障害の発見やデバッグを支援するためにテクニカルサポート担当者、開発者、システム管理者に送信することもできます。"
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "このツールは、ファイルシステム、LVM2 ボリュームグループ、NFS マウントなどのローカルストレージを管理します。"
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "このツールは、NetworkManager と Firewalld を使用して、ボンディング、ブリッジ、チーム、VLAN、ファイアウォールなどのネットワーク設定を管理します。NetworkManager は、Ubuntu のデフォルトの systemd-networkd および Debian の ifupdown スクリプトと互換性がありません。"
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "この Web ブラウザーは古いため、Web コンソールを実行できません ($0 が不明)"
+ ],
+ "Time zone": [
+ null,
+ "タイムゾーン"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "悪意のあるサードパーティーによって接続がインターセプトされないようにするには、ホストキーフィンガープリントを確認してください:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "フィンガープリントを確認するには、マシン上に物理的に置かれるか、信頼できるネットワークを介して $0 で次のコマンドを実行します:"
+ ],
+ "Toggle date picker": [
+ null,
+ "日付選択の切り替え"
+ ],
+ "Too much data": [
+ null,
+ "データが多すぎます"
+ ],
+ "Total size: $0": [
+ null,
+ "合計サイズ: $0"
+ ],
+ "Tower": [
+ null,
+ "タワー"
+ ],
+ "Try again": [
+ null,
+ "再試行します"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "$0 との同期を試行中です"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "そのアドレスに接続できません"
+ ],
+ "Unknown": [
+ null,
+ "不明"
+ ],
+ "Untrusted host": [
+ null,
+ "信用できないホスト"
+ ],
+ "User name": [
+ null,
+ "ユーザー名"
+ ],
+ "User name cannot be empty": [
+ null,
+ "ユーザー名は空にできません"
+ ],
+ "Validating authentication token": [
+ null,
+ "認証トークンの検証"
+ ],
+ "View all logs": [
+ null,
+ "すべてのログの表示"
+ ],
+ "View automation script": [
+ null,
+ "オートメーションスクリプトの表示"
+ ],
+ "Visit firewall": [
+ null,
+ "ファイアウォールへのアクセス"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "他のソフトウェア管理オペレーションが終了するまで待機中"
+ ],
+ "Weak password": [
+ null,
+ "脆弱なパスワード"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Linux サーバー用 Web コンソール"
+ ],
+ "Wrong user name or password": [
+ null,
+ "ユーザー名またはパスワードが間違っています"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "初めて $0 に接続しています。"
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "お使いのブラウザーでは、コンテキストメニューからの貼り付けが許可されていません。Shift+Insert を使用できます。"
+ ],
+ "Your session has been terminated.": [
+ null,
+ "セッションが終了しました。"
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "セッションの有効期限が切れました。再度ログインしてください。"
+ ],
+ "Zone": [
+ null,
+ "ゾーン"
+ ],
+ "[binary data]": [
+ null,
+ "[バイナリーデータ]"
+ ],
+ "[no data]": [
+ null,
+ "[データなし]"
+ ],
+ "password quality": [
+ null,
+ "パスワードの強度"
+ ],
+ "show less": [
+ null,
+ "簡易表示"
+ ],
+ "show more": [
+ null,
+ "詳細表示"
+ ]
+};
diff --git a/dist/static/po.ka.js b/dist/static/po.ka.js
new file mode 100644
index 0000000..376cb8d
--- /dev/null
+++ b/dist/static/po.ka.js
@@ -0,0 +1,965 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "ka",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 გიბ"
+ ],
+ "$0 day": [
+ null,
+ "$0 დღე",
+ "დღეები: $0"
+ ],
+ "$0 error": [
+ null,
+ "$0 შეცდომა"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0-ის გამოსვლის კოდია $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 წარუმატებელია"
+ ],
+ "$0 hour": [
+ null,
+ "$0 საათი",
+ "საათი: $0"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 ხელმიუწვდომელია ყველა რეპოზიტორიიდან."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 გასაღები შეიცვალა"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 მოკვდა სიგნალით $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 წუთი",
+ "წუთი: $0"
+ ],
+ "$0 month": [
+ null,
+ "$0 თვე",
+ "თვე: $0"
+ ],
+ "$0 week": [
+ null,
+ "$0 კვირა",
+ "კვირა: $0"
+ ],
+ "$0 will be installed.": [
+ null,
+ "დაყენდება $0."
+ ],
+ "$0 year": [
+ null,
+ "$0 წელი",
+ "წელი: $0"
+ ],
+ "1 day": [
+ null,
+ "1 დღე"
+ ],
+ "1 hour": [
+ null,
+ "1 საათი"
+ ],
+ "1 minute": [
+ null,
+ "1 წთ"
+ ],
+ "1 week": [
+ null,
+ "1 კვირა"
+ ],
+ "20 minutes": [
+ null,
+ "20 წთ"
+ ],
+ "40 minutes": [
+ null,
+ "40 წთ"
+ ],
+ "5 minutes": [
+ null,
+ "5 წთ"
+ ],
+ "6 hours": [
+ null,
+ "5 სთ"
+ ],
+ "60 minutes": [
+ null,
+ "60 წთ"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "უსაფრთხოებისთვის, წარმადობისთვის და საიმედოობისთვის საჭიროა ახალი ბრაუზერი."
+ ],
+ "Absent": [
+ null,
+ "აკლია"
+ ],
+ "Accept key and log in": [
+ null,
+ "მიიღეთ გასაღები და შედით"
+ ],
+ "Acceptable password": [
+ null,
+ "მისაღები პაროლი"
+ ],
+ "Add $0": [
+ null,
+ "$0-ის დამატება"
+ ],
+ "Additional packages:": [
+ null,
+ "დამატებითი პაკეტები:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Cockput ვებ კონსოლით ადმინისტრირება"
+ ],
+ "Advanced TCA": [
+ null,
+ "დამატებითი TCA"
+ ],
+ "All-in-one": [
+ null,
+ "ყველა-ერთში"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Ansible-ის როლების დოკუმენტაცია"
+ ],
+ "Authentication failed": [
+ null,
+ "ავთენტიკაციის შეცდომა"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "ავთენტიკაციის შეცდომა: სერვერმა კავშირი დახურა"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Cockpit ვებ კონსოლში პრივილეგირებული ამოცანების შესასრულებლად საჭიროა ავთენტიკაცია"
+ ],
+ "Automatically using NTP": [
+ null,
+ "ავტომატურად, NTP-ით"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "დამატებითი NTP სერვერების ავტომატური გამოყენება"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "მითითებული NTP სერვერების ავტომატური გამოყენება"
+ ],
+ "Automation script": [
+ null,
+ "ავტომატიზაციის სკრიპტი"
+ ],
+ "Blade": [
+ null,
+ "Blade"
+ ],
+ "Blade enclosure": [
+ null,
+ "კალათი"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "მატარებლის გაფართოების შასი"
+ ],
+ "Bypass browser check": [
+ null,
+ "ბრაუზერის შემოწმების გამოტოვება"
+ ],
+ "Cancel": [
+ null,
+ "გაუქმება"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "მომხმარებლისადაპაროლის გადაგზავნის შეცდომა"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "მოვლენის წარსულ დროში დანიშვნა შეუძლებელია"
+ ],
+ "Change": [
+ null,
+ "შეცვლა"
+ ],
+ "Change system time": [
+ null,
+ "სისტემური დროის შეცვლა"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "შეცვლილი გასაღებები ხშირად ოპერაციული სისტემის გადაყენებაზე მიუთითებს. ამავე დროს მოულოდნელი ცვლილება კავშირის გადასაჭერად მესამე პირის ჩარევასაც შეიძლება ნიშნავდეს."
+ ],
+ "Checking installed software": [
+ null,
+ "დაყენებული პროგრამული უზრუნველყოფის შემოწმება"
+ ],
+ "Close": [
+ null,
+ "დახურვა"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Cockpit-ის ავთენტიკაციის არასწორი კონფიგურაცია."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "NetworkManager-ის და Firewalld-ის მორგება Cockpit-ით"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit-ს მითითებულ ჰოსტთან დაკავშირება არ შეუძლია ."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit წარმოადგენს სერვერის მმართველს, რომლითაც Linux სერვერების ადმინისტრირება ბრაუზერითაც შეგიძლიათ. ტერმინალსა და ვებ ხელსაწყოს შორის გადართვა პრობლემა არაა. Cockpit-ით გაშვებული სერვისი შეგიძლიათ გააჩეროთ ტერმინალთაც. ასევე, თუ შეცდომა დაფიქსირდება ტერმინალში, მისი ნახვა Cockpit-ის საშუალებითაც შეგიძლიათ."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit-ი შეუთავსებელია თქვენს სერვერზე დაყენებულ პროგრამულ უზრუნველყოფასთან."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit-ი ამ სისტემაზე დაყენებული არაა."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit შესანიშნავია ახალი სისტემური ადმინისტრატორებისთვის. ის მათ საშუალებას აძლევს ადვილად შეასრულონ ისეთი მარტივი ამოცანები, როგორიცაა შენახვის ადმინისტრირება, ჟურნალების შემოწმება და სერვისების დაწყება და გაჩერება. შეგიძლიათ ერთდროულად რამდენიმე სერვერის მონიტორინგი და ადმინისტრირება. უბრალოდ დაამატეთ ისინი ერთი დაწკაპუნებით და თქვენი მანქანები იზრუნებს მის მეგობრებზე."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit-ი თქვენს ბრაუზერში შეიძლება არასწორად გამოჩნდეს"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "მოაგროვეთ დიაგნოსტიკური და მხარდაჭერის მონაცემები"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "ოპერაციული სისტემის ბირთვის ავარიის დამპები"
+ ],
+ "Compact PCI": [
+ null,
+ "კომპაქტური PCI"
+ ],
+ "Connect to": [
+ null,
+ "დაკავშირება"
+ ],
+ "Connect to:": [
+ null,
+ "დაკავშირება:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "კავშირის დრო გავიდა."
+ ],
+ "Convertible": [
+ null,
+ "გარდაქმნადი"
+ ],
+ "Copy": [
+ null,
+ "კოპირება"
+ ],
+ "Copy to clipboard": [
+ null,
+ "ბაფერში კოპირება"
+ ],
+ "Create": [
+ null,
+ "შექმნა"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "ახალი ამოცანის ამ შემცველობით შექმნა."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "დაყოვნება"
+ ],
+ "Desktop": [
+ null,
+ "სამუშაო მაგიდა"
+ ],
+ "Detachable": [
+ null,
+ "მოძრობადი"
+ ],
+ "Diagnostic reports": [
+ null,
+ "დიაგნოსტიკის ანგარიშები"
+ ],
+ "Docking station": [
+ null,
+ "სამაგრი დაფა"
+ ],
+ "Download a new browser for free": [
+ null,
+ "გადმოწერეთ ახალი ბრაუზერი უფასოდ"
+ ],
+ "Downloading $0": [
+ null,
+ "$0-ის გადმოწერა"
+ ],
+ "Dual rank": [
+ null,
+ "ორმაგი რანგი"
+ ],
+ "Embedded PC": [
+ null,
+ "ჩაშენებული PC"
+ ],
+ "Excellent password": [
+ null,
+ "გადასარევი პაროლი"
+ ],
+ "Expansion chassis": [
+ null,
+ "გაფართოების კორპუსი"
+ ],
+ "Failed to change password": [
+ null,
+ "პაროლის შეცვლის შეცდომა"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "firewalld-ში $0-ის ჩართვის შეცდომა"
+ ],
+ "Go to now": [
+ null,
+ "ახლავე გადასვლა"
+ ],
+ "Handheld": [
+ null,
+ "ჯიბის"
+ ],
+ "Hide confirmation password": [
+ null,
+ "დადასტურების პაროლის დამალვა"
+ ],
+ "Hide password": [
+ null,
+ "პაროლის დამალვა"
+ ],
+ "Host key is incorrect": [
+ null,
+ "ჰოსტის გასაღები არასწორია"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "თუ ანაბეჭდი ემთხვევა, დააწექით \"გასაღების მიღება და შესვლა\"-ს. ან არ შეხვიდეთ და დაუკავშირდით ადმინისტრატორს."
+ ],
+ "Install": [
+ null,
+ "დაყენება"
+ ],
+ "Install software": [
+ null,
+ "პროგრამების დაყენება"
+ ],
+ "Installing $0": [
+ null,
+ "$0-ის დაყენება"
+ ],
+ "Internal error": [
+ null,
+ "შიდა შეცდომა"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "შიდა შეცდომა: გამოწვევის არასწორი თავსართი"
+ ],
+ "Invalid date format": [
+ null,
+ "თარიღის არასწორი ფორმატი"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "თარიღისა და დროის არასწორი ფორმატი"
+ ],
+ "Invalid file permissions": [
+ null,
+ "ფაილის არასწორი წვდომები"
+ ],
+ "Invalid time format": [
+ null,
+ "დროის არასწორი ფორმატი"
+ ],
+ "Invalid timezone": [
+ null,
+ "დროის არასწორი სარტყელი"
+ ],
+ "IoT gateway": [
+ null,
+ "IoT gateway"
+ ],
+ "Kernel dump": [
+ null,
+ "ბირთვის დამპი"
+ ],
+ "Laptop": [
+ null,
+ "ლეპტოპი"
+ ],
+ "Learn more": [
+ null,
+ "გაიგეთ მეტი"
+ ],
+ "Loading system modifications...": [
+ null,
+ "სისტემის ცვლილებების ჩატვირთვა..."
+ ],
+ "Log in": [
+ null,
+ "შესვლა"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "შედით სერვერის თქვენი ანგარიშით."
+ ],
+ "Log messages": [
+ null,
+ "ჟურნალის შეტყობინებები"
+ ],
+ "Login": [
+ null,
+ "შესვლა"
+ ],
+ "Login again": [
+ null,
+ "თავიდან შესვლა"
+ ],
+ "Login failed": [
+ null,
+ "შესვლა წარუმატებელია"
+ ],
+ "Logout successful": [
+ null,
+ "გასვლა წარმატებულია"
+ ],
+ "Low profile desktop": [
+ null,
+ "დაბალი პროფილის სამუშაო მაგიდა"
+ ],
+ "Lunch box": [
+ null,
+ "Lunch box"
+ ],
+ "Main server chassis": [
+ null,
+ "სერვერის მთავარი შასი"
+ ],
+ "Manage storage": [
+ null,
+ "საცავის მართვა"
+ ],
+ "Manually": [
+ null,
+ "ხელით მითითებული"
+ ],
+ "Message to logged in users": [
+ null,
+ "შესული მომხმარებლებისთვის შეტყობინების გაგზავნა"
+ ],
+ "Mini PC": [
+ null,
+ "მინი PC"
+ ],
+ "Mini tower": [
+ null,
+ "კომპიუტერი პატარა ყუთით"
+ ],
+ "Multi-system chassis": [
+ null,
+ "მრავალსისტემიანი ყუთი"
+ ],
+ "NTP server": [
+ null,
+ "NTP სერვერი"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "საჭიროა ერთი NTP სერვერი მაინც"
+ ],
+ "Networking": [
+ null,
+ "ქსელი"
+ ],
+ "New host": [
+ null,
+ "ახალი ჰოსტი"
+ ],
+ "New password was not accepted": [
+ null,
+ "ახალი პაროლი მიუღებელია"
+ ],
+ "No delay": [
+ null,
+ "დაყოვნების გარეშე"
+ ],
+ "No such file or directory": [
+ null,
+ "ფაილი ან საქაღალდე არ არსებობს"
+ ],
+ "No system modifications": [
+ null,
+ "სისტემა შეცვლილი არაა"
+ ],
+ "Not a valid private key": [
+ null,
+ "არ წარმოადგენს სწორ პირად გასაღებს"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "არ გაქვთ მითითებული მოქმედების შესასრულებლად საკმარისი წვდომა."
+ ],
+ "Not synchronized": [
+ null,
+ "სინქრონიზებული არაა"
+ ],
+ "Notebook": [
+ null,
+ "ნოუთბუქი"
+ ],
+ "Occurrences": [
+ null,
+ "გამოვლენები"
+ ],
+ "Ok": [
+ null,
+ "დიახ"
+ ],
+ "Old password not accepted": [
+ null,
+ "ძველი პაროლი მიუღებელია"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "როცა Cockpit-ს დააყენებთ, შეგიძლიათ მისი ჩართვაც, ბრძანებით \"systemctl enable --now cockpit.socket\"."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "ან გამოიყენეთ მოყოლილი ბრაუზერი"
+ ],
+ "Other": [
+ null,
+ "სხვა"
+ ],
+ "Other options": [
+ null,
+ "სხვა პარამეტრები"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit-ის ავარია"
+ ],
+ "Password": [
+ null,
+ "პაროლი"
+ ],
+ "Password is not acceptable": [
+ null,
+ "პაროლი მიუღებელია"
+ ],
+ "Password is too weak": [
+ null,
+ "პაროლი ძალიან სუსტია"
+ ],
+ "Password not accepted": [
+ null,
+ "პაროლი მიუღებელია"
+ ],
+ "Paste": [
+ null,
+ "ჩასმა"
+ ],
+ "Paste error": [
+ null,
+ "ჩასმის შეცდომა"
+ ],
+ "Path to file": [
+ null,
+ "ბილიკი ფაილამდე"
+ ],
+ "Peripheral chassis": [
+ null,
+ "გარე კორპუსი"
+ ],
+ "Permission denied": [
+ null,
+ "წვდომა აკრძალულია"
+ ],
+ "Pick date": [
+ null,
+ "აირჩიეთ თარიღი"
+ ],
+ "Pizza box": [
+ null,
+ "პიცისყუთი"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "ვებ კონსოლის გამოსაყენებლად საჭიროა JavaScript-ის ჩართვა."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "შეიყვანეთ მისაერთებელი ჰოსტის სახელი"
+ ],
+ "Portable": [
+ null,
+ "გადატანადი"
+ ],
+ "Present": [
+ null,
+ "წარმოდგენილია"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "მოთხოვნას ssh-add-ის გავლით დრო გაუვიდა"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "მოთხოვნას ssh-keygen-ის გავლით დრო გაუვიდა"
+ ],
+ "RAID chassis": [
+ null,
+ "RAID კალათი"
+ ],
+ "Rack mount chassis": [
+ null,
+ "რეკში ჩასადგმელი შასი"
+ ],
+ "Reboot": [
+ null,
+ "გადატვირთვა"
+ ],
+ "Recent hosts": [
+ null,
+ "ბოლო ჰოსტები"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "კავშირი უარყოფილია. ჰოსტი უცნობია"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "კავშირი უარყოფითია. ჰოსტის გასაღებები არ ემთხვევა"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "კავშირი უარყოფილია. ჰოსტის უცნობი გასაღები"
+ ],
+ "Removals:": [
+ null,
+ "წაიშლება:"
+ ],
+ "Remove host": [
+ null,
+ "ჰოსტის წაშლა"
+ ],
+ "Removing $0": [
+ null,
+ "$0-ის წაშლა"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "დალუქული PC"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Linux-ის გაფართოებული უსაფრთხოების (SELinux) მორგება და გამართვა"
+ ],
+ "Server": [
+ null,
+ "სერვერი"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "სერვერმა დახურა კავშირი."
+ ],
+ "Set time": [
+ null,
+ "დროის დაყენება"
+ ],
+ "Shell script": [
+ null,
+ "გარსის სკრიპტი"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Show confirmation password": [
+ null,
+ "დადასტურების პაროლის ჩვენება"
+ ],
+ "Show password": [
+ null,
+ "პაროლის ჩვენება"
+ ],
+ "Shut down": [
+ null,
+ "გამორთვა"
+ ],
+ "Single rank": [
+ null,
+ "ერთრანგიანი"
+ ],
+ "Space-saving computer": [
+ null,
+ "პატარა ზომის კომპიუტერი"
+ ],
+ "Specific time": [
+ null,
+ "მითითებული დრო"
+ ],
+ "Stick PC": [
+ null,
+ "Stick PC"
+ ],
+ "Storage": [
+ null,
+ "საცავი"
+ ],
+ "Strong password": [
+ null,
+ "ძლიერი პაროლი"
+ ],
+ "Sub-Chassis": [
+ null,
+ "ქვე-კორპუსი"
+ ],
+ "Sub-Notebook": [
+ null,
+ "ქვე-ნოუთბუქი"
+ ],
+ "Synchronized": [
+ null,
+ "სინქრონიზებულია"
+ ],
+ "Synchronized with $0": [
+ null,
+ "სინქრონიზებულია $0-თან"
+ ],
+ "Synchronizing": [
+ null,
+ "სინქრონიზაცია"
+ ],
+ "Tablet": [
+ null,
+ "ტაბლეტი"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "შესულ მომხმარებელს არ აქვს სისტემური ცვლილებების ნახვს უფლება"
+ ],
+ "The passwords do not match.": [
+ null,
+ "პაროლები არ ემთხვევა."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "მიღებული ანაბეჭდების გაზიარება პრობლემა არაა."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "სერვერმა უარყო $0-ის ავთენტიკაცია პაროლის საშუალებით და ავთენტიკაციის სხვა საშუალებები ხელმიუწვდომელია."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "სერვერმა ყველა მხარდაჭერილი მეთოდით ავთენტიკაცია უარჰყო."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Cockpit-ის გაშვება შეუძლებელია ბრაუზერის კონფიგურაციის გამო ($0 მიუწვდომელია)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "ეს პროგრამა როგორც SELinux-ის პოლიტიკის მორგებაში, ასევე მის ბოლომდე გაგებაში და პოლიტიკის დარღვევის გადაწყვეტაში დაგეხმარებათ."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "ეს პროგრამა სისტემას ბირთვის ავარიის შემთხვევაში დისკზე დამპის ჩაწერის მორგებაში დაგეხმარებათ."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "ეს პროგრამა გაშვებული სისტემიდან კონფიგურაცისა და დიაგნოსტიკის მოგროვებაში დაგეხმარებათ. არქივი შეგიძლიათ ლოკალურად შეინახოთ, ან ცენტრალურად, ჩაწერისა და ტრეკინგის მიზნებისთვის, ან შეგიძლიათ გადააგზავნოთ მხარდაჭერის, პროგრამისტებისა და სისტემური ადმინისტრატორების ჯგუფებთან, რათა აპარატურული პრობლემები აღმოაჩინოთ და გადაჭრათ."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "ეს პროგრამა ლოკალურ საცავს, როგორიცაა ფაილურ სისტემები, LVM2 ტომის ჯგუფები და NFS მიმაგრებები, მართავს."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "ეს პროგრამა მართავს ქსელს. bond ინტერფეისების, ხიდების, ჯგუფური ინტერფეისების, VLAN-ების და ბრანდმაუერების მართვა NetworkManager-ისა და FIrewalld-ის საშუალებით. NetworkManager-ი Ubuntu-ის ნაგულისხმებ systemd-networkd და Debian-ის ifupdown სკრიპტებთან შეუთავსებელია."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "ეს ბრაუზერ ძალიან ძველია ვებ კონსოლის გასაშვებად (არ გააჩნია $0)"
+ ],
+ "Time zone": [
+ null,
+ "დროის სარტყელი"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "იმაში დასარწმუნებლად, რომ არ ხდება კავშირის გადაჭერა, შეამოწმეთ ჰოსტის ანაბეჭდი:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "ანაბეჭდის შესამოწმებლად გაუშვით ეს ბრძანება $0-ზე როცა ფიზიკურად ან სანდო ქსელით იქნებით შესული მანქანაზე:"
+ ],
+ "Toggle date picker": [
+ null,
+ "თარიღის ამრჩევის გადართვა"
+ ],
+ "Too much data": [
+ null,
+ "მეტისმეტად ბევრი მონაცემი"
+ ],
+ "Total size: $0": [
+ null,
+ "ჯამური ზომა: $0"
+ ],
+ "Tower": [
+ null,
+ "კომპიუტერის კორპუსი"
+ ],
+ "Try again": [
+ null,
+ "თავიდან სცადეთ"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "$0-თან სინქრონიზაციის მცდელობა"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "მისამართზე დაკავშირების შეცდომა"
+ ],
+ "Unknown": [
+ null,
+ "უცნობი"
+ ],
+ "Untrusted host": [
+ null,
+ "არასანდო ჰოსტი"
+ ],
+ "User name": [
+ null,
+ "მომხმარებლის სახელი"
+ ],
+ "User name cannot be empty": [
+ null,
+ "მომხმარებლის სახელი ცარიელი არ შეიძლება იყოს"
+ ],
+ "Validating authentication token": [
+ null,
+ "ავთენტიკაციის კოდის გადამოწმება"
+ ],
+ "View all logs": [
+ null,
+ "ყველა ჟურნალის ნახვა"
+ ],
+ "View automation script": [
+ null,
+ "ავტომატიზაციის სკრიპტის ნახვა"
+ ],
+ "Visit firewall": [
+ null,
+ "ბრანდმაუერზე გადასვლა"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "პროგრამების მართვის სხვა ოპერაციების დასრულების მოლოდინი"
+ ],
+ "Weak password": [
+ null,
+ "სუსტი პაროლი"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "ვებ კონსოლი Linux სერვერებისთვის"
+ ],
+ "Wrong user name or password": [
+ null,
+ "არასწორი მოხმარებელი ან პაროლი"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "$0-ს პირველად უკავშირდებით."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "თქვენს ბრაუზერს არ გააჩნია კონტექსტური მენიუდან ჩასმის მხარდაჭერა. სცადეთ დააჭიროთ Shift+Insert-ს."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "თქვენი სესია გაწყვეტილია."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "სესიის ვადა გასულია. თავიდან შედით."
+ ],
+ "Zone": [
+ null,
+ "ზონა"
+ ],
+ "[binary data]": [
+ null,
+ "[ბინარული მონაცემები]"
+ ],
+ "[no data]": [
+ null,
+ "[მონაცემების გარეშე]"
+ ],
+ "password quality": [
+ null,
+ "პაროლის ხარისხი"
+ ],
+ "show less": [
+ null,
+ "ნაკლების ჩვენება"
+ ],
+ "show more": [
+ null,
+ "მეტის ჩვენება"
+ ]
+};
diff --git a/dist/static/po.ko.js b/dist/static/po.ko.js
new file mode 100644
index 0000000..5755f3a
--- /dev/null
+++ b/dist/static/po.ko.js
@@ -0,0 +1,959 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => 0,
+ "language": "ko",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 일"
+ ],
+ "$0 error": [
+ null,
+ "$0 오류"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0가 코드 $1로 종료됨"
+ ],
+ "$0 failed": [
+ null,
+ "$0가 실패"
+ ],
+ "$0 hour": [
+ null,
+ "$0 시"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0는 저장소에서 사용 할 수 없습니다."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 키 변경됩니다"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$1 시그널에 의해 $0가 종료되었습니다"
+ ],
+ "$0 minute": [
+ null,
+ "$0 분"
+ ],
+ "$0 month": [
+ null,
+ "$0 달"
+ ],
+ "$0 week": [
+ null,
+ "$0 주"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0가 설치됩니다."
+ ],
+ "$0 year": [
+ null,
+ "$0 년"
+ ],
+ "1 day": [
+ null,
+ "1 일"
+ ],
+ "1 hour": [
+ null,
+ "1시간"
+ ],
+ "1 minute": [
+ null,
+ "1 분"
+ ],
+ "1 week": [
+ null,
+ "1 주"
+ ],
+ "20 minutes": [
+ null,
+ "20분"
+ ],
+ "40 minutes": [
+ null,
+ "40 분"
+ ],
+ "5 minutes": [
+ null,
+ "5분"
+ ],
+ "6 hours": [
+ null,
+ "6 시간"
+ ],
+ "60 minutes": [
+ null,
+ "60 분"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "보안, 안정성, 성능을 위해 최신 브라우저가 필요합니다."
+ ],
+ "Absent": [
+ null,
+ "부재"
+ ],
+ "Accept key and log in": [
+ null,
+ "키 수락 및 로그인"
+ ],
+ "Acceptable password": [
+ null,
+ "허용되는 비밀번호"
+ ],
+ "Add $0": [
+ null,
+ "$0 추가"
+ ],
+ "Additional packages:": [
+ null,
+ "추가 꾸러미 :"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Cockpit 웹 콘솔로 관리"
+ ],
+ "Advanced TCA": [
+ null,
+ "고급 TCA"
+ ],
+ "All-in-one": [
+ null,
+ "일체형"
+ ],
+ "Ansible": [
+ null,
+ "앤서블"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Ansible 역할 문서"
+ ],
+ "Authentication failed": [
+ null,
+ "인증 실패"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "인증 실패: 서버 연결 끊김"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Cockpit 웹 콘솔의 권한 작업을 수행하려면 인증이 필요합니다"
+ ],
+ "Automatically using NTP": [
+ null,
+ "자동으로 NTP 사용"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "추가 NTP 서버를 자동으로 사용"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "특정 NTP 서버를 자동으로 사용"
+ ],
+ "Automation script": [
+ null,
+ "자동 스크립트"
+ ],
+ "Blade": [
+ null,
+ "블레이드"
+ ],
+ "Blade enclosure": [
+ null,
+ "블레이드 인클로저"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "버스 확장 섀시"
+ ],
+ "Bypass browser check": [
+ null,
+ "웹 탐색기 점검을 우회합니다"
+ ],
+ "Cancel": [
+ null,
+ "취소"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "로그인 정보를 전송할 수 없습니다"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "이전 이벤트를 예약할 수 없습니다"
+ ],
+ "Change": [
+ null,
+ "변경"
+ ],
+ "Change system time": [
+ null,
+ "시스템 시간 변경"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "때때로 운영 체제 재설치로 인해 키가 변경될 수 있습니다. 그러나 예기치 않은 변경은 연결을 가로채는 타사의 시도를 나타낼 수도 있습니다."
+ ],
+ "Checking installed software": [
+ null,
+ "설치된 소프트웨어 확인 중"
+ ],
+ "Close": [
+ null,
+ "닫기"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Cockpit 인증이 잘못 설정되어 있습니다."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "NetworkManager 및 Firewalld의 Cockpit 구성"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit을 지정된 호스트에 연결 할 수 없습니다."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit은 웹 브라우저에서 리눅스 서버를 쉽게 관리 할 수 있는 서버 관리자입니다. 터미널과 웹 도구을 구분하지 않고 사용할 수 있습니다. Cockpit에서 시작된 서비스는 터미널을 통해 중지할 수 있습니다. 마찬가지로 터미널에서 오류가 발생한 경우 해당 오류를 Cockpit 저널 연결장치에서 확인 할 수 있습니다."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit은 시스템의 소프트웨어와 호환성이 없습니다."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "시스템에 Cockpit이 설치되어 있지 않습니다."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit은 경험이 적은 시스템 관리자에게 적합합니다. 시스템 관리자는 저장장치 관리, 저널 검사, 서비스 시작 및 중지 등의 간단한 작업을 쉽게 수행할 수 있으며 여러 서버를 동시에 모니터링 및 관리 할 수 있습니다. 간단하게 장비를 추가하여 서버를 추가할 수 있으며 추가 후 다른 기기를 관리할 수 있습니다."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit은 자신의 웹 탐색기에서 정확하게 렌더링 되지 않을 수 있습니다"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "진단 및 지원 자료 수집 및 꾸러미"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "커널 충돌 덤프 수집"
+ ],
+ "Compact PCI": [
+ null,
+ "PCI 압축"
+ ],
+ "Connect to": [
+ null,
+ "연결 대상"
+ ],
+ "Connect to:": [
+ null,
+ "연결 대상:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "연결 시간 초과."
+ ],
+ "Convertible": [
+ null,
+ "변환 가능"
+ ],
+ "Copy": [
+ null,
+ "복사"
+ ],
+ "Copy to clipboard": [
+ null,
+ "클립보드로 복사"
+ ],
+ "Create": [
+ null,
+ "생성"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "이 컨텐츠로 신규 작업 파일을 만듭니다."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "지연"
+ ],
+ "Desktop": [
+ null,
+ "데스크탑"
+ ],
+ "Detachable": [
+ null,
+ "분리 가능"
+ ],
+ "Diagnostic reports": [
+ null,
+ "진단 보고서"
+ ],
+ "Docking station": [
+ null,
+ "도킹 스테이션"
+ ],
+ "Download a new browser for free": [
+ null,
+ "신규 브라우저 내려받기 (무료)"
+ ],
+ "Downloading $0": [
+ null,
+ "$0 내려받기 중"
+ ],
+ "Dual rank": [
+ null,
+ "듀얼 랭크"
+ ],
+ "Embedded PC": [
+ null,
+ "임베디드 PC"
+ ],
+ "Excellent password": [
+ null,
+ "우수한 비밀번호"
+ ],
+ "Expansion chassis": [
+ null,
+ "확장 섀시"
+ ],
+ "Failed to change password": [
+ null,
+ "비밀번호 변경 실패"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "방화벽에서 $0 활성화에 실패"
+ ],
+ "Go to now": [
+ null,
+ "지금 바로 가기"
+ ],
+ "Handheld": [
+ null,
+ "휴대용"
+ ],
+ "Hide confirmation password": [
+ null,
+ "확인 비밀번호 숨기기"
+ ],
+ "Hide password": [
+ null,
+ "비밀번호 숨기기"
+ ],
+ "Host key is incorrect": [
+ null,
+ "호스트 키가 잘못되었습니다"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "지문이 일치하면 \"키 수락 및 로그인\"을 눌러주세요. 일치 하지 않을 경우 로그인하지 않고 관리자에게 문의하십시오."
+ ],
+ "Install": [
+ null,
+ "설치"
+ ],
+ "Install software": [
+ null,
+ "소프트웨어 설치"
+ ],
+ "Installing $0": [
+ null,
+ "$0 설치 중"
+ ],
+ "Internal error": [
+ null,
+ "내부 오류"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "내부 오류: 잘못된 챌린지 헤더"
+ ],
+ "Invalid date format": [
+ null,
+ "잘못된 날짜 형식"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "잘못된 날짜 형식 및 잘못된 시간 형식"
+ ],
+ "Invalid file permissions": [
+ null,
+ "잘못된 파일 권한"
+ ],
+ "Invalid time format": [
+ null,
+ "잘못된 시간 형식"
+ ],
+ "Invalid timezone": [
+ null,
+ "잘못된 시간대"
+ ],
+ "IoT gateway": [
+ null,
+ "IoT 게이트웨이"
+ ],
+ "Kernel dump": [
+ null,
+ "커널 덤프"
+ ],
+ "Laptop": [
+ null,
+ "랩탑"
+ ],
+ "Learn more": [
+ null,
+ "더 알아보기"
+ ],
+ "Loading system modifications...": [
+ null,
+ "시스템 수정 적재 중..."
+ ],
+ "Log in": [
+ null,
+ "로그인"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "서버 사용자 계정으로 로그인합니다."
+ ],
+ "Log messages": [
+ null,
+ "로그 메세지"
+ ],
+ "Login": [
+ null,
+ "로그인"
+ ],
+ "Login again": [
+ null,
+ "다시 로그인"
+ ],
+ "Login failed": [
+ null,
+ "로그인 실패"
+ ],
+ "Logout successful": [
+ null,
+ "성공적으로 로그아웃되었습니다"
+ ],
+ "Low profile desktop": [
+ null,
+ "낮은 프로파일 데스크탑"
+ ],
+ "Lunch box": [
+ null,
+ "Lunch Box"
+ ],
+ "Main server chassis": [
+ null,
+ "메인 서버 섀시"
+ ],
+ "Manage storage": [
+ null,
+ "관리 저장소"
+ ],
+ "Manually": [
+ null,
+ "수동"
+ ],
+ "Message to logged in users": [
+ null,
+ "로그인한 사용자에게 보내는 메세지"
+ ],
+ "Mini PC": [
+ null,
+ "미니 PC"
+ ],
+ "Mini tower": [
+ null,
+ "미니 타워"
+ ],
+ "Multi-system chassis": [
+ null,
+ "멀티 시스템 섀시"
+ ],
+ "NTP server": [
+ null,
+ "NTP 서버"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "최소 하나의 NTP 서버가 필요합니다"
+ ],
+ "Networking": [
+ null,
+ "네트워킹"
+ ],
+ "New host": [
+ null,
+ "신규 호스트"
+ ],
+ "New password was not accepted": [
+ null,
+ "신규 비밀번호가 허용되지 않습니다"
+ ],
+ "No delay": [
+ null,
+ "지연 없음"
+ ],
+ "No such file or directory": [
+ null,
+ "이러한 파일 또는 디렉토리가 없습니다"
+ ],
+ "No system modifications": [
+ null,
+ "시스템 수정 없음"
+ ],
+ "Not a valid private key": [
+ null,
+ "유효한 개인 키가 없습니다"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "이 작업을 실행할 수 있는 권한이 없습니다."
+ ],
+ "Not synchronized": [
+ null,
+ "동기화 되어 있지 않습니다"
+ ],
+ "Notebook": [
+ null,
+ "노트북"
+ ],
+ "Occurrences": [
+ null,
+ "발생"
+ ],
+ "Ok": [
+ null,
+ "확인"
+ ],
+ "Old password not accepted": [
+ null,
+ "이전 비밀번호가 허용되지 않습니다"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Cockpit이 설치되면 \"systemctl enable --now cockpit.socket\"을 사용하여 이를 활성화합니다."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "번들된 브라우저 사용"
+ ],
+ "Other": [
+ null,
+ "기타"
+ ],
+ "Other options": [
+ null,
+ "기타 옵션"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit가 충돌했습니다"
+ ],
+ "Password": [
+ null,
+ "비밀번호"
+ ],
+ "Password is not acceptable": [
+ null,
+ "비밀번호가 허용되지 않습니다"
+ ],
+ "Password is too weak": [
+ null,
+ "비밀번호가 너무 취약합니다"
+ ],
+ "Password not accepted": [
+ null,
+ "비밀번호가 허용되지 않습니다"
+ ],
+ "Paste": [
+ null,
+ "붙여넣기"
+ ],
+ "Paste error": [
+ null,
+ "붙임 오류"
+ ],
+ "Path to file": [
+ null,
+ "파일의 경로"
+ ],
+ "Peripheral chassis": [
+ null,
+ "주변 장치 섀시"
+ ],
+ "Permission denied": [
+ null,
+ "권한이 거부되었습니다"
+ ],
+ "Pick date": [
+ null,
+ "날짜 선택"
+ ],
+ "Pizza box": [
+ null,
+ "피자 박스"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "웹 콘솔을 사용하려면 JavaScript를 활성화하십시오."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "연결할 호스트를 지정해 주십시오"
+ ],
+ "Portable": [
+ null,
+ "이동식"
+ ],
+ "Present": [
+ null,
+ "존재"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "ssh-add를 통한 메세지 제공 시간이 초과되었습니다"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "ssh-keygen을 통한 메세지 제공시간이 초과되었습니다"
+ ],
+ "RAID chassis": [
+ null,
+ "레이드 섀시"
+ ],
+ "Rack mount chassis": [
+ null,
+ "랙 마운트 섀시"
+ ],
+ "Reboot": [
+ null,
+ "재시작"
+ ],
+ "Recent hosts": [
+ null,
+ "최근 호스트"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "연결을 거부하고 있습니다. 알 수 없는 호스트입니다"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "연결을 거부하고 있습니다. 호스트 키가 일치하지 않습니다"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "연결을 거부하고 있습니다. 알 수 없는 호스트 키입니다"
+ ],
+ "Removals:": [
+ null,
+ "삭제:"
+ ],
+ "Remove host": [
+ null,
+ "호스트 제거"
+ ],
+ "Removing $0": [
+ null,
+ "$0 삭제 중"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "쉴드 케이스 PC"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "보안이 향상된 리눅스 구성과 문제해결"
+ ],
+ "Server": [
+ null,
+ "서버"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "서버 연결이 종료되었습니다."
+ ],
+ "Set time": [
+ null,
+ "시간 설정"
+ ],
+ "Shell script": [
+ null,
+ "쉘 스크립트"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Show confirmation password": [
+ null,
+ "비밀번호 확인을 표시합니다"
+ ],
+ "Show password": [
+ null,
+ "비밀번호 표시"
+ ],
+ "Shut down": [
+ null,
+ "종료"
+ ],
+ "Single rank": [
+ null,
+ "단일 등급"
+ ],
+ "Space-saving computer": [
+ null,
+ "공간-절약형 컴퓨터"
+ ],
+ "Specific time": [
+ null,
+ "특정 시간"
+ ],
+ "Stick PC": [
+ null,
+ "스틱 PC"
+ ],
+ "Storage": [
+ null,
+ "저장소"
+ ],
+ "Strong password": [
+ null,
+ "강력한 비밀번호"
+ ],
+ "Sub-Chassis": [
+ null,
+ "서브 섀시"
+ ],
+ "Sub-Notebook": [
+ null,
+ "서브 노트북"
+ ],
+ "Synchronized": [
+ null,
+ "동기화됩니다"
+ ],
+ "Synchronized with $0": [
+ null,
+ "$0와 동기화됩니다"
+ ],
+ "Synchronizing": [
+ null,
+ "동기화 중"
+ ],
+ "Tablet": [
+ null,
+ "테블릿"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "로그인한 사용자는 시스템 수정 사항을 볼 수 없습니다"
+ ],
+ "The passwords do not match.": [
+ null,
+ "비밀번호가 일치하지 않습니다."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "최종 지문을 전자우편을 포함한 공개적인 방법을 통해 공유 할 수 있습니다."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "서버가 비밀번호 인증을 사용하여 '$0' 인증을 거부했습니다. 지원되는 다른 인증 방법을 사용 할 수 없습니다."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "서버가 지원되는 방법을 사용하여 인증을 거부했습니다."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "웹 검색기의 설정에 따라 Cockpit이 실행되지 않습니다 (접근 불가능 $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "이와 같은 도구는 SELinux 정책을 구성하고 정책 위반을 이해하고 해결하는데 도움을 줄 수 있습니다."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "이와 같은 도구는 커널 충돌 덤프를 디스크에 작성하도록 시스템을 구성합니다."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "이와 같은 도구는 동작 중인 시스템에서 구성 및 진단 정보의 아카이브를 생성합니다. 아카이브는 기록 또는 추적 목적으로 로컬 또는 집중적으로 저장되거나 기술적 오류-찾기와 디버깅을 지원하기 위해 기술 지원 담당자, 개발자 또는 시스템 관리자에게 보낼 수 있습니다."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "이와 같은 도구는 파일시스템, LVM2 볼륨 그룹, 그리고 NFS 적재와 같은 로컬 저장소를 관리합니다."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "이와 같은 도구는 NetworkManager 및 Firewalld를 사용하여 bonds, bridges, teams, VLAN과 방화벽과 같은 네트워킹을 관리합니다. NetworkManager는 우분투 기본 systemd-netowrkd 및 데비안의 ifupdown 스크립트와는 호환되지 않습니다."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "이 웹 브라우저는 오래되어 웹 콘솔을 실행할 수 없습니다($0 누락)"
+ ],
+ "Time zone": [
+ null,
+ "시간대"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "악성의 제3자가 귀하의 연결을 가로채지 않도록 하려면 호스트 키 지문을 확인하십시오:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "지문을 확인하려면 물리적인 장치 또는 신뢰할 수 있는 네트워크를 통해 $0에서 다음을 실행합니다:"
+ ],
+ "Toggle date picker": [
+ null,
+ "날짜 선택기 전환"
+ ],
+ "Too much data": [
+ null,
+ "데이터가 너무 많습니다"
+ ],
+ "Total size: $0": [
+ null,
+ "전체 크기: $0"
+ ],
+ "Tower": [
+ null,
+ "타워"
+ ],
+ "Try again": [
+ null,
+ "다시 시도"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "$0와 동기화를 시도 중입니다"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "해당 주소에 연결 할 수 없음"
+ ],
+ "Unknown": [
+ null,
+ "알 수 없음"
+ ],
+ "Untrusted host": [
+ null,
+ "지원되지 않는 호스트"
+ ],
+ "User name": [
+ null,
+ "사용자 이름"
+ ],
+ "User name cannot be empty": [
+ null,
+ "사용자 이름을 입력하셔야 합니다"
+ ],
+ "Validating authentication token": [
+ null,
+ "인증 토큰 확인"
+ ],
+ "View all logs": [
+ null,
+ "모든 기록 보기"
+ ],
+ "View automation script": [
+ null,
+ "자동 스크립트 보기"
+ ],
+ "Visit firewall": [
+ null,
+ "방화벽 방문"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "다른 소프트웨어 관리 작업이 완료될 때 까지 대기 중"
+ ],
+ "Weak password": [
+ null,
+ "취약한 비밀번호"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "리눅스 서버를 위한 웹콘솔"
+ ],
+ "Wrong user name or password": [
+ null,
+ "잘못된 사용자 이름 또는 비밀번호"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "처음으로 $0에 연결됩니다."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "당신의 검색기는 내용 메뉴에서 붙여넣기를 허용하지 않습니다. Shift+Insert를 사용 할 수 있습니다."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "세션이 종료되었습니다."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "세션이 만료되었습니다. 다시 로그인하십시오."
+ ],
+ "Zone": [
+ null,
+ "영역"
+ ],
+ "[binary data]": [
+ null,
+ "[바이너리 데이터]"
+ ],
+ "[no data]": [
+ null,
+ "[데이터 없음]"
+ ],
+ "password quality": [
+ null,
+ "비밀번호 수준"
+ ],
+ "show less": [
+ null,
+ "덜 보기"
+ ],
+ "show more": [
+ null,
+ "더 보기"
+ ]
+};
diff --git a/dist/static/po.manifest.cs.js b/dist/static/po.manifest.cs.js
new file mode 100644
index 0000000..79d717f
--- /dev/null
+++ b/dist/static/po.manifest.cs.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2,
+ "language": "cs",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Diagnostická hlášení"
+ ],
+ "Kernel dump": [
+ null,
+ "Výpis paměti jádra"
+ ],
+ "Networking": [
+ null,
+ "Síť"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Úložiště"
+ ]
+};
diff --git a/dist/static/po.manifest.de.js b/dist/static/po.manifest.de.js
new file mode 100644
index 0000000..253d34b
--- /dev/null
+++ b/dist/static/po.manifest.de.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "de",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Diagnoseberichte"
+ ],
+ "Kernel dump": [
+ null,
+ "Kernel dump"
+ ],
+ "Networking": [
+ null,
+ "Netzwerk"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Speicher"
+ ]
+};
diff --git a/dist/static/po.manifest.es.js b/dist/static/po.manifest.es.js
new file mode 100644
index 0000000..3ca54d2
--- /dev/null
+++ b/dist/static/po.manifest.es.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "es",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Informes de diagnóstico"
+ ],
+ "Kernel dump": [
+ null,
+ "Volcado del kernel"
+ ],
+ "Networking": [
+ null,
+ "Redes"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Almacenamiento"
+ ]
+};
diff --git a/dist/static/po.manifest.fi.js b/dist/static/po.manifest.fi.js
new file mode 100644
index 0000000..26229aa
--- /dev/null
+++ b/dist/static/po.manifest.fi.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "fi",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Diagnostiikkaraportit"
+ ],
+ "Kernel dump": [
+ null,
+ "Ytimen tyhjennys"
+ ],
+ "Networking": [
+ null,
+ "Verkko"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Tallennustila"
+ ]
+};
diff --git a/dist/static/po.manifest.fr.js b/dist/static/po.manifest.fr.js
new file mode 100644
index 0000000..e77ab97
--- /dev/null
+++ b/dist/static/po.manifest.fr.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n > 1,
+ "language": "fr",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Rapports de diagnostic"
+ ],
+ "Kernel dump": [
+ null,
+ "Kernel Dump"
+ ],
+ "Networking": [
+ null,
+ "Réseau"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Stockage"
+ ]
+};
diff --git a/dist/static/po.manifest.he.js b/dist/static/po.manifest.he.js
new file mode 100644
index 0000000..9b21821
--- /dev/null
+++ b/dist/static/po.manifest.he.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => (n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && n % 10 == 0) ? 2 : 3)),
+ "language": "he",
+ "language-direction": "rtl"
+ },
+ "Diagnostic reports": [
+ null,
+ "דוחות אבחון"
+ ],
+ "Kernel dump": [
+ null,
+ "היטל ליבה"
+ ],
+ "Networking": [
+ null,
+ "תקשורת"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "אחסון"
+ ]
+};
diff --git a/dist/static/po.manifest.it.js b/dist/static/po.manifest.it.js
new file mode 100644
index 0000000..968ccbe
--- /dev/null
+++ b/dist/static/po.manifest.it.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "it",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Rapporti diagnostici"
+ ],
+ "Kernel dump": [
+ null,
+ "Kernel dump"
+ ],
+ "Networking": [
+ null,
+ "Rete"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Archiviazione"
+ ]
+};
diff --git a/dist/static/po.manifest.ja.js b/dist/static/po.manifest.ja.js
new file mode 100644
index 0000000..756ac38
--- /dev/null
+++ b/dist/static/po.manifest.ja.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => 0,
+ "language": "ja",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "診断レポート"
+ ],
+ "Kernel dump": [
+ null,
+ "カーネルダンプ"
+ ],
+ "Networking": [
+ null,
+ "ネットワーキング"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "ストレージ"
+ ]
+};
diff --git a/dist/static/po.manifest.ka.js b/dist/static/po.manifest.ka.js
new file mode 100644
index 0000000..b25cfe5
--- /dev/null
+++ b/dist/static/po.manifest.ka.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "ka",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "დიაგნოსტიკის ანგარიშები"
+ ],
+ "Kernel dump": [
+ null,
+ "ბირთვის დამპი"
+ ],
+ "Networking": [
+ null,
+ "ქსელი"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "საცავი"
+ ]
+};
diff --git a/dist/static/po.manifest.ko.js b/dist/static/po.manifest.ko.js
new file mode 100644
index 0000000..57beb6a
--- /dev/null
+++ b/dist/static/po.manifest.ko.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => 0,
+ "language": "ko",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "진단 보고서"
+ ],
+ "Kernel dump": [
+ null,
+ "커널 덤프"
+ ],
+ "Networking": [
+ null,
+ "네트워킹"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "저장소"
+ ]
+};
diff --git a/dist/static/po.manifest.nb_NO.js b/dist/static/po.manifest.nb_NO.js
new file mode 100644
index 0000000..002a7b3
--- /dev/null
+++ b/dist/static/po.manifest.nb_NO.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "nb_NO",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Diagnose rapporter"
+ ],
+ "Kernel dump": [
+ null,
+ "Kjerne dump"
+ ],
+ "Networking": [
+ null,
+ "Nettverk"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Lagring"
+ ]
+};
diff --git a/dist/static/po.manifest.nl.js b/dist/static/po.manifest.nl.js
new file mode 100644
index 0000000..924bd62
--- /dev/null
+++ b/dist/static/po.manifest.nl.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "nl",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Diagnostische rapporten"
+ ],
+ "Kernel dump": [
+ null,
+ "Kerneldump"
+ ],
+ "Networking": [
+ null,
+ "Netwerken"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Opslag"
+ ]
+};
diff --git a/dist/static/po.manifest.pl.js b/dist/static/po.manifest.pl.js
new file mode 100644
index 0000000..84ec655
--- /dev/null
+++ b/dist/static/po.manifest.pl.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2,
+ "language": "pl",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Raporty diagnostyczne"
+ ],
+ "Kernel dump": [
+ null,
+ "Zrzut jądra"
+ ],
+ "Networking": [
+ null,
+ "Sieć"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Przechowywanie danych"
+ ]
+};
diff --git a/dist/static/po.manifest.pt_BR.js b/dist/static/po.manifest.pt_BR.js
new file mode 100644
index 0000000..15fb06b
--- /dev/null
+++ b/dist/static/po.manifest.pt_BR.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => (n != 1),
+ "language": "pt_BR",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Relatório de diagnostico"
+ ],
+ "Kernel dump": [
+ null,
+ "Dump do Kernel"
+ ],
+ "Networking": [
+ null,
+ "Rede"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Armazenamento"
+ ]
+};
diff --git a/dist/static/po.manifest.ru.js b/dist/static/po.manifest.ru.js
new file mode 100644
index 0000000..e2c4b26
--- /dev/null
+++ b/dist/static/po.manifest.ru.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2,
+ "language": "ru",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Диагностические отчёты"
+ ],
+ "Kernel dump": [
+ null,
+ "Дамп ядра"
+ ],
+ "Networking": [
+ null,
+ "Сеть"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Хранилище"
+ ]
+};
diff --git a/dist/static/po.manifest.sk.js b/dist/static/po.manifest.sk.js
new file mode 100644
index 0000000..8cce5f8
--- /dev/null
+++ b/dist/static/po.manifest.sk.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2,
+ "language": "sk",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Diagnostické hlásenia"
+ ],
+ "Kernel dump": [
+ null,
+ ""
+ ],
+ "Networking": [
+ null,
+ "Sieť"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Úložisko"
+ ]
+};
diff --git a/dist/static/po.manifest.sv.js b/dist/static/po.manifest.sv.js
new file mode 100644
index 0000000..c900e9a
--- /dev/null
+++ b/dist/static/po.manifest.sv.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "sv",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Diagnostikrapporter"
+ ],
+ "Kernel dump": [
+ null,
+ "Kärndump"
+ ],
+ "Networking": [
+ null,
+ "Nätverk"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Lagring"
+ ]
+};
diff --git a/dist/static/po.manifest.tr.js b/dist/static/po.manifest.tr.js
new file mode 100644
index 0000000..2f49b2e
--- /dev/null
+++ b/dist/static/po.manifest.tr.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => (n>1),
+ "language": "tr",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Tanılama raporları"
+ ],
+ "Kernel dump": [
+ null,
+ "Çekirdek dökümü"
+ ],
+ "Networking": [
+ null,
+ "Ağ"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Depolama"
+ ]
+};
diff --git a/dist/static/po.manifest.uk.js b/dist/static/po.manifest.uk.js
new file mode 100644
index 0000000..2a82d00
--- /dev/null
+++ b/dist/static/po.manifest.uk.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2,
+ "language": "uk",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "Діагностичні звіти"
+ ],
+ "Kernel dump": [
+ null,
+ "Дамп ядра"
+ ],
+ "Networking": [
+ null,
+ "Робота у мережі"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "Сховище даних"
+ ]
+};
diff --git a/dist/static/po.manifest.zh_CN.js b/dist/static/po.manifest.zh_CN.js
new file mode 100644
index 0000000..f3e5439
--- /dev/null
+++ b/dist/static/po.manifest.zh_CN.js
@@ -0,0 +1,27 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => 0,
+ "language": "zh_CN",
+ "language-direction": "ltr"
+ },
+ "Diagnostic reports": [
+ null,
+ "诊断报告"
+ ],
+ "Kernel dump": [
+ null,
+ "内核转储"
+ ],
+ "Networking": [
+ null,
+ "网络"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Storage": [
+ null,
+ "存储"
+ ]
+};
diff --git a/dist/static/po.nb_NO.js b/dist/static/po.nb_NO.js
new file mode 100644
index 0000000..892168f
--- /dev/null
+++ b/dist/static/po.nb_NO.js
@@ -0,0 +1,885 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "nb_NO",
+ "language-direction": "ltr"
+ },
+ "$0 day": [
+ null,
+ "$0 dag",
+ "$0 dager"
+ ],
+ "$0 error": [
+ null,
+ "$0 feil"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 avsluttet med koden $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 feilet"
+ ],
+ "$0 hour": [
+ null,
+ "$0 time",
+ "$0 timer"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 er ikke tilgjengelig fra noe depot."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 nøkkel endret"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 drept med signal $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minutt",
+ "$0 minutter"
+ ],
+ "$0 month": [
+ null,
+ "$0 måned",
+ "$0 måneder"
+ ],
+ "$0 week": [
+ null,
+ "$0 uke",
+ "$0 uker"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 vil bli installert."
+ ],
+ "$0 year": [
+ null,
+ "$0 år",
+ "$0 år"
+ ],
+ "1 day": [
+ null,
+ "1 dag"
+ ],
+ "1 hour": [
+ null,
+ "1 time"
+ ],
+ "1 minute": [
+ null,
+ "1 minutt"
+ ],
+ "1 week": [
+ null,
+ "1 uke"
+ ],
+ "20 minutes": [
+ null,
+ "20 minutter"
+ ],
+ "40 minutes": [
+ null,
+ "40 minutter"
+ ],
+ "5 minutes": [
+ null,
+ "5 minutter"
+ ],
+ "6 hours": [
+ null,
+ "6 timer"
+ ],
+ "60 minutes": [
+ null,
+ "60 minutter"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Det kreves en moderne nettleser for sikkerhet, pålitelighet og ytelse."
+ ],
+ "Absent": [
+ null,
+ "Fraværende"
+ ],
+ "Accept key and log in": [
+ null,
+ "Godta nøkkel og logg inn"
+ ],
+ "Add $0": [
+ null,
+ "Legg til $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Ekstra pakker:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Administrasjon med Cockpit Web konsoll"
+ ],
+ "Advanced TCA": [
+ null,
+ "Avansert TCA"
+ ],
+ "All-in-one": [
+ null,
+ "Alt i ett"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Ansible roller dokumentasjon"
+ ],
+ "Authentication failed": [
+ null,
+ "Autentisering feilet"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Autentisering feilet: Serveren lukket tilkoblingen"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Autentisering er nødvendig for å utføre privilegerte oppgaver med Cockpit Web konsoll"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Automatisk med bruk av NTP"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Automatisk med bruk av spesifikke NTP servere"
+ ],
+ "Automation script": [
+ null,
+ "Automatiseringsskript"
+ ],
+ "Blade": [
+ null,
+ "Blad"
+ ],
+ "Blade enclosure": [
+ null,
+ "Blad-kabinett"
+ ],
+ "Bus expansion chassis": [
+ null,
+ ""
+ ],
+ "Bypass browser check": [
+ null,
+ ""
+ ],
+ "Cancel": [
+ null,
+ "Avbryt"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Kan ikke videresende påloggingsinformasjonen"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Kan ikke tidsplanlegge hendelse i fortiden"
+ ],
+ "Change": [
+ null,
+ "Endre"
+ ],
+ "Change system time": [
+ null,
+ "Endre systemtid"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Endrede nøkler er ofte resultatet av reinstallering av operativsystemet. Imidlertid kan en uventet endring indikere et tredjepartsforsøk på å fange opp forbindelsen din."
+ ],
+ "Checking installed software": [
+ null,
+ "Kontrollerer installert programvare"
+ ],
+ "Close": [
+ null,
+ "Lukk"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Cockpit autentisering er konfigurert feil."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ ""
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit kunne ikke kontakte den angitte verten."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit er en serveradministrator som gjør det enkelt å administrere Linux-serverne dine via en nettleser. Å bytte mellom terminalen og nettverktøyet er ikke noe problem. En tjeneste startet via Cockpit kan stoppes via terminalen. På samme måte, hvis det oppstår en feil i terminalen, kan den sees i Cockpit journal-grensesnittet."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit er ikke kompatibel med programvaren på systemet."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit er ikke installert på systemet."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit er perfekt for nye sysadminer, slik at de enkelt kan utføre enkle oppgaver som lagringsadministrasjon, inspisere journaler og starte og stoppe tjenester. Du kan overvåke og administrere flere servere samtidig. Bare legg dem til med et enkelt klikk, så vil maskinene dine se etter kompisene."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ ""
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ ""
+ ],
+ "Compact PCI": [
+ null,
+ ""
+ ],
+ "Connect to": [
+ null,
+ "Koble til"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Tidsavbrudd for tilkoblingen."
+ ],
+ "Convertible": [
+ null,
+ "Konverterbar"
+ ],
+ "Copy": [
+ null,
+ "Kopier"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Kopier til utklippstavle"
+ ],
+ "Create": [
+ null,
+ "Opprett"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Opprett ny oppgavefil med dette innholdet."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Forsinkelse"
+ ],
+ "Desktop": [
+ null,
+ ""
+ ],
+ "Detachable": [
+ null,
+ ""
+ ],
+ "Diagnostic reports": [
+ null,
+ "Diagnose rapporter"
+ ],
+ "Docking station": [
+ null,
+ "Dokkingstasjon"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Last ned en ny nettleser gratis"
+ ],
+ "Downloading $0": [
+ null,
+ "Laster ned $0"
+ ],
+ "Dual rank": [
+ null,
+ "Dobbel rangering"
+ ],
+ "Embedded PC": [
+ null,
+ "Innebygd PC"
+ ],
+ "Excellent password": [
+ null,
+ "Utmerket passord"
+ ],
+ "Expansion chassis": [
+ null,
+ ""
+ ],
+ "Failed to change password": [
+ null,
+ "Kunne ikke endre passord"
+ ],
+ "Go to now": [
+ null,
+ "Gå til nå"
+ ],
+ "Handheld": [
+ null,
+ "Håndholdt"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Vertsnøkkelen er feil"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Hvis fingeravtrykket stemmer overens, klikker du på \"Godta nøkkel og logg inn\". Ellers ikke logg inn og kontakt administratoren din."
+ ],
+ "Install": [
+ null,
+ "Installer"
+ ],
+ "Install software": [
+ null,
+ "Installer programvare"
+ ],
+ "Installing $0": [
+ null,
+ "Installerer $0"
+ ],
+ "Internal error": [
+ null,
+ "Intern feil"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ ""
+ ],
+ "Invalid date format": [
+ null,
+ "Ugyldig datoformat"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Ugyldig datoformat og ugyldig tidsformat"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Ugyldige filtillatelser"
+ ],
+ "Invalid time format": [
+ null,
+ "Ugyldig tidsformat"
+ ],
+ "Invalid timezone": [
+ null,
+ "Ugyldig tidssone"
+ ],
+ "IoT gateway": [
+ null,
+ "IoT-gateway"
+ ],
+ "Kernel dump": [
+ null,
+ "Kjerne dump"
+ ],
+ "Laptop": [
+ null,
+ ""
+ ],
+ "Learn more": [
+ null,
+ "Lær mer"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Laster inn systemendringer ..."
+ ],
+ "Log in": [
+ null,
+ "Logg inn"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Logg inn med server brukerkontoen din."
+ ],
+ "Log messages": [
+ null,
+ "Logg meldinger"
+ ],
+ "Login again": [
+ null,
+ "Logg inn igjen"
+ ],
+ "Login failed": [
+ null,
+ "Innlogging feilet"
+ ],
+ "Logout successful": [
+ null,
+ ""
+ ],
+ "Low profile desktop": [
+ null,
+ ""
+ ],
+ "Lunch box": [
+ null,
+ "Lunsjboks"
+ ],
+ "Main server chassis": [
+ null,
+ ""
+ ],
+ "Manually": [
+ null,
+ "Manuelt"
+ ],
+ "Message to logged in users": [
+ null,
+ "Melding til innloggede brukere"
+ ],
+ "Mini PC": [
+ null,
+ "Mini-PC"
+ ],
+ "Mini tower": [
+ null,
+ ""
+ ],
+ "Multi-system chassis": [
+ null,
+ ""
+ ],
+ "NTP server": [
+ null,
+ "NTP Server"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Trenger minst en NTP-server"
+ ],
+ "Networking": [
+ null,
+ "Nettverk"
+ ],
+ "New host": [
+ null,
+ "Ny vert"
+ ],
+ "New password was not accepted": [
+ null,
+ "Nytt passord ble ikke godtatt"
+ ],
+ "No delay": [
+ null,
+ "Ingen forsinkelse"
+ ],
+ "No such file or directory": [
+ null,
+ "Ingen slik fil eller katalog"
+ ],
+ "No system modifications": [
+ null,
+ "Ingen systemendringer"
+ ],
+ "Not a valid private key": [
+ null,
+ "Ikke en gyldig privat nøkkel"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Ikke tillatt å utføre denne handlingen."
+ ],
+ "Not synchronized": [
+ null,
+ "Ikke synkronisert"
+ ],
+ "Notebook": [
+ null,
+ ""
+ ],
+ "Ok": [
+ null,
+ "Ok"
+ ],
+ "Old password not accepted": [
+ null,
+ "Gammelt passord aksepteres ikke"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Når Cockpit er installert, kan den aktiveres med \"systemctl enable --now cockpit.socket\"."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Eller bruk en nettleser som følger med"
+ ],
+ "Other": [
+ null,
+ "Annen"
+ ],
+ "Other options": [
+ null,
+ "Andre alternativer"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit krasjet"
+ ],
+ "Password": [
+ null,
+ "Passord"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Passord er ikke akseptabelt"
+ ],
+ "Password is too weak": [
+ null,
+ "Passordet er for svakt"
+ ],
+ "Password not accepted": [
+ null,
+ "Passord ikke akseptert"
+ ],
+ "Paste": [
+ null,
+ "Lim inn"
+ ],
+ "Path to file": [
+ null,
+ "Sti til fil"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Perifert chassis"
+ ],
+ "Permission denied": [
+ null,
+ "Ingen tilgang"
+ ],
+ "Pick date": [
+ null,
+ "Velg dato"
+ ],
+ "Pizza box": [
+ null,
+ "Pizzaboks"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Aktiver JavaScript for å bruke Web konsollet."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Vennligst angi verten du vil koble til"
+ ],
+ "Portable": [
+ null,
+ "Bærbar"
+ ],
+ "Present": [
+ null,
+ "Til stede"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Spørring via ssh-add ble tidsavbrutt"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Spørring via ssh-keygen ble tidsavbrutt"
+ ],
+ "RAID chassis": [
+ null,
+ "RAID chassis"
+ ],
+ "Rack mount chassis": [
+ null,
+ ""
+ ],
+ "Reboot": [
+ null,
+ "Omstart"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Nekter å koble til. Verten er ukjent"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Nekter å koble til. Vertsnøkkel samsvarer ikke"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Nekter å koble til. Vertsnøkkel er ukjent"
+ ],
+ "Removals:": [
+ null,
+ ""
+ ],
+ "Removing $0": [
+ null,
+ "Fjerner $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ ""
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ ""
+ ],
+ "Server": [
+ null,
+ "Server"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Serveren har lukket forbindelsen."
+ ],
+ "Set time": [
+ null,
+ "Sett tid"
+ ],
+ "Shell script": [
+ null,
+ "Shell-skript"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Shut down": [
+ null,
+ "Slå av"
+ ],
+ "Single rank": [
+ null,
+ ""
+ ],
+ "Space-saving computer": [
+ null,
+ "Plassbesparende datamaskin"
+ ],
+ "Specific time": [
+ null,
+ "Spesifikk tid"
+ ],
+ "Stick PC": [
+ null,
+ ""
+ ],
+ "Storage": [
+ null,
+ "Lagring"
+ ],
+ "Sub-Chassis": [
+ null,
+ ""
+ ],
+ "Sub-Notebook": [
+ null,
+ ""
+ ],
+ "Synchronized": [
+ null,
+ "Synkronisert"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Synkronisert med $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Synkroniserer"
+ ],
+ "Tablet": [
+ null,
+ "Nettbrett"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "Den påloggede brukeren har ikke lov til å se systemendringer"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Passordene samsvarer ikke."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "Det resulterende fingeravtrykket er greit å dele via offentlige metoder, inkludert e-post."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Serveren nektet å autentisere '$0' ved hjelp av passordgodkjenning, og ingen andre støttede godkjenningsmetoder er tilgjengelige."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Serveren nektet å godkjenne ved hjelp av de støttede metodene."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Nettleserkonfigurasjonen forhindrer Cockpit i å kjøre (utilgjengelig $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ ""
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ ""
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ ""
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ ""
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ ""
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Denne nettleseren er for gammel til å kjøre web konsollet (mangler $0)"
+ ],
+ "Time zone": [
+ null,
+ "Tidssone"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "For å sikre at forbindelsen din ikke blir fanget opp av en ondsinnet tredjepart, må du verifisere vertsnøkkelens fingeravtrykk:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "For å bekrefte et fingeravtrykk, kjør følgende på $ 0 mens du sitter fysisk ved maskinen eller gjennom et pålitelig nettverk:"
+ ],
+ "Toggle date picker": [
+ null,
+ ""
+ ],
+ "Too much data": [
+ null,
+ "For mye data"
+ ],
+ "Total size: $0": [
+ null,
+ "Total størrelse: $0"
+ ],
+ "Tower": [
+ null,
+ ""
+ ],
+ "Try again": [
+ null,
+ "Prøv på nytt"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Prøver å synkronisere med $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Kan ikke koble til den adressen"
+ ],
+ "Unknown": [
+ null,
+ "Ukjent"
+ ],
+ "Untrusted host": [
+ null,
+ ""
+ ],
+ "User name": [
+ null,
+ "Brukernavn"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Brukernavnet kan ikke være tomt"
+ ],
+ "Validating authentication token": [
+ null,
+ "Validerer godkjenningstoken"
+ ],
+ "View automation script": [
+ null,
+ "Vis automatiseringsskript"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Venter på at andre programvareadministrasjons-operasjoner skal fullføres"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Web konsoll for Linux servere"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Feil brukernavn eller passord"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Du kobler til $0 for første gang."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ ""
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Økten din er avsluttet."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Økten din har utløpt. Vennligst logg inn igjen."
+ ],
+ "Zone": [
+ null,
+ ""
+ ],
+ "[binary data]": [
+ null,
+ "[binære data]"
+ ],
+ "[no data]": [
+ null,
+ "[ingen data]"
+ ],
+ "password quality": [
+ null,
+ "passordkvalitet"
+ ],
+ "show less": [
+ null,
+ "vis mindre"
+ ],
+ "show more": [
+ null,
+ "vis mer"
+ ]
+};
diff --git a/dist/static/po.nl.js b/dist/static/po.nl.js
new file mode 100644
index 0000000..c7e5f19
--- /dev/null
+++ b/dist/static/po.nl.js
@@ -0,0 +1,961 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "nl",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 dag",
+ "$0 dagen"
+ ],
+ "$0 error": [
+ null,
+ "$0 fout"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 verlaten met code $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 mislukte"
+ ],
+ "$0 hour": [
+ null,
+ "$0 uur",
+ "$0 uren"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 is van geen enkele repository beschikbaar."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 sleutel gewijzigd"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 is afgeschoten met signaal $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minuut",
+ "$0 minuten"
+ ],
+ "$0 month": [
+ null,
+ "$0 maand",
+ "$0 maanden"
+ ],
+ "$0 week": [
+ null,
+ "$0 week",
+ "$0 weken"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 zal geïnstalleerd worden."
+ ],
+ "$0 year": [
+ null,
+ "$0 jaar",
+ "$0 jaren"
+ ],
+ "1 day": [
+ null,
+ "1 dag"
+ ],
+ "1 hour": [
+ null,
+ "1 uur"
+ ],
+ "1 minute": [
+ null,
+ "1 minuut"
+ ],
+ "1 week": [
+ null,
+ "1 week"
+ ],
+ "20 minutes": [
+ null,
+ "20 minuten"
+ ],
+ "40 minutes": [
+ null,
+ "40 minuten"
+ ],
+ "5 minutes": [
+ null,
+ "5 minuten"
+ ],
+ "6 hours": [
+ null,
+ "6 uren"
+ ],
+ "60 minutes": [
+ null,
+ "60 minuten"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Een moderne browser is vereist voor goede beveiliging, betrouwbaarheid en prestaties."
+ ],
+ "Absent": [
+ null,
+ "Afwezig"
+ ],
+ "Accept key and log in": [
+ null,
+ "Accepteer sleutel en log in"
+ ],
+ "Acceptable password": [
+ null,
+ "Acceptabel wachtwoord"
+ ],
+ "Add $0": [
+ null,
+ "Toevoegen $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Extra pakketten:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Beheer met Cockpit Web Console"
+ ],
+ "Advanced TCA": [
+ null,
+ "Geavanceerde TCA"
+ ],
+ "All-in-one": [
+ null,
+ "Alles in een"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Ansible rollen documentatie"
+ ],
+ "Authentication failed": [
+ null,
+ "Authenticatie mislukte"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Authenticatie mislukte: Server verbrak verbinding"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Authenticatie is vereist voor het uitvoeren van bevoorrechte taken met de Cockpit Web Console"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Automatisch gebruik van NTP"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Automatisch gebruik van extra NTP servers"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Automatisch gebruik van specifieke NTP servers"
+ ],
+ "Automation script": [
+ null,
+ "Automatiserings-script"
+ ],
+ "Blade": [
+ null,
+ "Antenne"
+ ],
+ "Blade enclosure": [
+ null,
+ "Antennebehuizing"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Busuitbreidingschassis"
+ ],
+ "Cancel": [
+ null,
+ "Annuleren"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Kan inloggegevens niet doorsturen"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Kan evenement niet in het verleden plannen"
+ ],
+ "Change": [
+ null,
+ "Verandering"
+ ],
+ "Change system time": [
+ null,
+ "Verander systeemtijd"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Gewijzigde sleutels zijn vaak het resultaat van een herinstallatie van het besturingssysteem. Een onverwachte wijziging kan echter wijzen op een poging van derden om je verbinding te onderscheppen."
+ ],
+ "Checking installed software": [
+ null,
+ "Controleren op geïnstalleerde software"
+ ],
+ "Close": [
+ null,
+ "Sluiten"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Cockpit authenticatie is verkeerd geconfigureerd."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "Cockpit configuratie van NetworkManager en Firewalld"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit kon geen contact maken met de opgegeven host."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit is een serverbeheerder waarmee je Linux-servers eenvoudig kunt beheren via een webbrowser. Omschakelen tussen de terminal en het webgereedschap is geen probleem. Een service gestart via Cockpit kan via de terminal worden gestopt. Evenzo, als er een fout optreedt in de terminal, kan deze worden gezien in de Cockpit-logboekinterface."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit is niet compatibel met de software op het systeem."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit is niet op het systeem geïnstalleerd."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit is perfect voor nieuwe systeembeheerders, waardoor ze eenvoudig eenvoudige taken kunnen uitvoeren, zoals opslagbeheer, het inspecteren van logboeken en het starten en stoppen van services. Je kunt meerdere servers tegelijkertijd bewaken en beheren. Voeg ze gewoon toe met een enkele klik en je machines zullen voor zijn maatjes zorgen."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit wordt mogelijk niet correct weergegeven in je browser"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Verzamel en verpak diagnostische en ondersteuningsdata"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Verzamel kernelcrashdumps"
+ ],
+ "Compact PCI": [
+ null,
+ "Compact PCI"
+ ],
+ "Connect to": [
+ null,
+ "Verbinden met"
+ ],
+ "Connect to:": [
+ null,
+ "Verbinden met:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Er is een time-out opgetreden voor de verbinding."
+ ],
+ "Convertible": [
+ null,
+ "Converteerbaar"
+ ],
+ "Copy": [
+ null,
+ "Kopiëren"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Kopiëren naar clipboard"
+ ],
+ "Create": [
+ null,
+ "Aanmaken"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Maak nieuw taakbestand aan met deze inhoud."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Vertraging"
+ ],
+ "Desktop": [
+ null,
+ "Bureaublad"
+ ],
+ "Detachable": [
+ null,
+ "Demonteerbaar"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Diagnostische rapporten"
+ ],
+ "Docking station": [
+ null,
+ "Docking station"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Download gratis een nieuwe browser"
+ ],
+ "Downloading $0": [
+ null,
+ "$0 downloaden"
+ ],
+ "Dual rank": [
+ null,
+ "Dubbele rangorde"
+ ],
+ "Embedded PC": [
+ null,
+ "Ingebouwde pc"
+ ],
+ "Excellent password": [
+ null,
+ "Uitstekend wachtwoord"
+ ],
+ "Expansion chassis": [
+ null,
+ "Uitbreidingschassis"
+ ],
+ "Failed to change password": [
+ null,
+ "Kan wachtwoord niet veranderen"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "Kan $0 niet inschakelen in firewalld"
+ ],
+ "Go to now": [
+ null,
+ "Ga nu naar"
+ ],
+ "Handheld": [
+ null,
+ "Handheld"
+ ],
+ "Hide confirmation password": [
+ null,
+ "Bevestigingswachtwoord verbergen"
+ ],
+ "Hide password": [
+ null,
+ "Wachtwoord verbergen"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Hostsleutel is onjuist"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Als de vingerafdruk overeenkomt, klik dan op \"Accepteer sleutel en log in\". Log anders niet in en neem contact op met je beheerder."
+ ],
+ "Install": [
+ null,
+ "Installeren"
+ ],
+ "Install software": [
+ null,
+ "Installeer software"
+ ],
+ "Installing $0": [
+ null,
+ "$0 installeren"
+ ],
+ "Internal error": [
+ null,
+ "Interne fout"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Interne fout: ongeldige exceptie koptekst"
+ ],
+ "Invalid date format": [
+ null,
+ "Ongeldige datum"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Ongeldige datumnotatie en ongeldige tijdnotatie"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Ongeldige bestandsrechten"
+ ],
+ "Invalid time format": [
+ null,
+ "Ongeldige tijdnotatie"
+ ],
+ "Invalid timezone": [
+ null,
+ "Ongeldige tijdzone"
+ ],
+ "IoT gateway": [
+ null,
+ "IoT-gateway"
+ ],
+ "Kernel dump": [
+ null,
+ "Kerneldump"
+ ],
+ "Laptop": [
+ null,
+ "Laptop"
+ ],
+ "Learn more": [
+ null,
+ "Kom meer te weten"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Systeemwijzigingen laden..."
+ ],
+ "Log in": [
+ null,
+ "Inloggen"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Log in met je servergebruikersaccount."
+ ],
+ "Log messages": [
+ null,
+ "Log berichten"
+ ],
+ "Login": [
+ null,
+ "Log in"
+ ],
+ "Login again": [
+ null,
+ "Log opnieuw in"
+ ],
+ "Login failed": [
+ null,
+ "Inloggen mislukte"
+ ],
+ "Logout successful": [
+ null,
+ "Uitloggen succesvol"
+ ],
+ "Low profile desktop": [
+ null,
+ "Laag profiel bureaublad"
+ ],
+ "Lunch box": [
+ null,
+ "Lunchbox"
+ ],
+ "Main server chassis": [
+ null,
+ "Hoofdserverchassis"
+ ],
+ "Manage storage": [
+ null,
+ "Beheerde opslag"
+ ],
+ "Manually": [
+ null,
+ "Handmatig"
+ ],
+ "Message to logged in users": [
+ null,
+ "Bericht aan ingelogde gebruikers"
+ ],
+ "Mini PC": [
+ null,
+ "Mini PC"
+ ],
+ "Mini tower": [
+ null,
+ "Mini tower"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Chassis met meerdere systemen"
+ ],
+ "NTP server": [
+ null,
+ "NTP-server"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Minimaal één NTP-server nodig"
+ ],
+ "Networking": [
+ null,
+ "Netwerken"
+ ],
+ "New host": [
+ null,
+ "Nieuwe host"
+ ],
+ "New password was not accepted": [
+ null,
+ "Nieuw wachtwoord is niet geaccepteerd"
+ ],
+ "No delay": [
+ null,
+ "Geen vertraging"
+ ],
+ "No such file or directory": [
+ null,
+ "Bestand of map bestaat niet"
+ ],
+ "No system modifications": [
+ null,
+ "Geen systeemwijzigingen"
+ ],
+ "Not a valid private key": [
+ null,
+ "Geen geldige privésleutel"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Niet toegestaan om deze actie uit te voeren."
+ ],
+ "Not synchronized": [
+ null,
+ "Niet gesynchroniseerd"
+ ],
+ "Notebook": [
+ null,
+ "Notebook"
+ ],
+ "Occurrences": [
+ null,
+ "Voorvallen"
+ ],
+ "Ok": [
+ null,
+ "OK"
+ ],
+ "Old password not accepted": [
+ null,
+ "Oude wachtwoord niet geaccepteerd"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Nadat Cockpit geïnstalleerd is, schakel je het in met \"systemctl enable --now cockpit.socket\"."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Of gebruik een gebundelde browser"
+ ],
+ "Other": [
+ null,
+ "Andere"
+ ],
+ "Other options": [
+ null,
+ "Andere opties"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit is gecrasht"
+ ],
+ "Password": [
+ null,
+ "Wachtwoord"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Wachtwoord is niet acceptabel"
+ ],
+ "Password is too weak": [
+ null,
+ "Wachtwoord is te zwak"
+ ],
+ "Password not accepted": [
+ null,
+ "Wachtwoord wordt niet geaccepteerd"
+ ],
+ "Paste": [
+ null,
+ "Plakken"
+ ],
+ "Paste error": [
+ null,
+ "Plakfout"
+ ],
+ "Path to file": [
+ null,
+ "Pad naar bestand"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Randchassis"
+ ],
+ "Permission denied": [
+ null,
+ "Toestemming geweigerd"
+ ],
+ "Pick date": [
+ null,
+ "Kies datum"
+ ],
+ "Pizza box": [
+ null,
+ "Pizzadoos"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Zet JavaScript aan om de Webconsole te gebruiken."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Geef de host op waarmee je verbinding wilt maken"
+ ],
+ "Portable": [
+ null,
+ "Draagbaar"
+ ],
+ "Present": [
+ null,
+ "Aanwezig"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Vragen via ssh-add is verlopen"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Vragen ssh-keygen is verlopen"
+ ],
+ "RAID chassis": [
+ null,
+ "RAID-chassis"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Rackmontagechassis"
+ ],
+ "Reboot": [
+ null,
+ "Opnieuw opstarten"
+ ],
+ "Recent hosts": [
+ null,
+ "Recente hosts"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Verbinding maken geweigerd. Host is onbekend"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Verbinding maken geweigerd. Hostsleutel komt niet overeen"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Verbinding maken geweigerd. Hostsleutel is onbekend"
+ ],
+ "Removals:": [
+ null,
+ "Verwijderingen:"
+ ],
+ "Remove host": [
+ null,
+ "Verwijder host"
+ ],
+ "Removing $0": [
+ null,
+ "$0 verwijderen"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "Gesloten PC"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Security Enhanced Linux configuratie en probleemoplossing"
+ ],
+ "Server": [
+ null,
+ "Server"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Server heeft de verbinding verbroken."
+ ],
+ "Set time": [
+ null,
+ "Stel tijd in"
+ ],
+ "Shell script": [
+ null,
+ "Shell-script"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Show confirmation password": [
+ null,
+ "Bevestigingswachtwoord tonen"
+ ],
+ "Show password": [
+ null,
+ "Wachtwoord tonen"
+ ],
+ "Shut down": [
+ null,
+ "Afsluiten"
+ ],
+ "Single rank": [
+ null,
+ "Enkele rang"
+ ],
+ "Space-saving computer": [
+ null,
+ "Ruimtebesparende computer"
+ ],
+ "Specific time": [
+ null,
+ "Specifieke tijd"
+ ],
+ "Stick PC": [
+ null,
+ "Stick PC"
+ ],
+ "Storage": [
+ null,
+ "Opslag"
+ ],
+ "Strong password": [
+ null,
+ "Sterk wachtwoord"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Sub-chassis"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Sub-notebook"
+ ],
+ "Synchronized": [
+ null,
+ "Gesynchroniseerd"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Gesynchroniseerd met $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Synchroniseren"
+ ],
+ "Tablet": [
+ null,
+ "Tablet"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "De ingelogde gebruiker mag geen systeemwijzigingen bekijken"
+ ],
+ "The passwords do not match.": [
+ null,
+ "De wachtwoorden komen niet overeen."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "De resulterende vingerafdruk is prima te delen via openbare methoden, inclusief e-mail."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "De server weigerde '$0' te verifiëren met wachtwoordverificatie en er zijn geen andere ondersteunde verificatiemethoden beschikbaar."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "De server weigerde te verifiëren met behulp van ondersteunde methoden."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "De configuratie van de webbrowser voorkomt dat Cockpit wordt uitgevoerd (ontoegankelijk $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "Dit gereedschap configureert het SELinux-beleid en kan helpen bij het begrijpen en oplossen van beleidsschendingen."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "Dit gereedschap configureert het systeem om kernelcrashdumps naar schijf te schrijven."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "Dit gereedschap genereert een archief met configuratie- en diagnostische informatie van het draaiende systeem. Het archief kan lokaal of centraal worden opgeslagen voor opname- of trackingsoeleinden of kan worden verzonden naar vertegenwoordigers van de technische ondersteuning, ontwikkelaars of systeembeheerders om te helpen bij het opsporen van technische fouten en het debuggen."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "Dit gereedschap beheert lokale opslag, zoals bestandssystemen, LVM2-volumegroepen en NFS-koppelingen."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "Dit gereedschap beheert netwerken zoals bindingen, bruggen, teams, VLAN's en firewalls met behulp van NetworkManager en Firewalld. NetworkManager is niet compatibel met Ubuntu's standaard systemd-networkd en Debian's ifupdown-scripts."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Deze webbrowser is te oud om de Webconsole uit te voeren ($0 ontbreekt)"
+ ],
+ "Time zone": [
+ null,
+ "Tijdzone"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Controleer de vingerafdruk van de hostsleutel om ervoor te zorgen dat je verbinding niet wordt onderschept door een kwaadwillende derde partij:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Om een vingerafdruk te verifiëren, voer je het volgende uit op $0 terwijl je fysiek achter de machine zit of via een vertrouwd netwerk:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Datumkiezer omschakelen"
+ ],
+ "Too much data": [
+ null,
+ "Teveel data"
+ ],
+ "Total size: $0": [
+ null,
+ "Totale grootte: $0"
+ ],
+ "Tower": [
+ null,
+ "Toren"
+ ],
+ "Try again": [
+ null,
+ "Probeer opnieuw"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Bezig met synchroniseren met $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Kan niet verbinden met dat adres"
+ ],
+ "Unknown": [
+ null,
+ "Onbekend"
+ ],
+ "Untrusted host": [
+ null,
+ "Niet vertrouwde host"
+ ],
+ "User name": [
+ null,
+ "Gebruikersnaam"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Gebruikersnaam mag niet leeg zijn"
+ ],
+ "Validating authentication token": [
+ null,
+ "Verificatietoken valideren"
+ ],
+ "View all logs": [
+ null,
+ "Bekijk alle logboeken"
+ ],
+ "View automation script": [
+ null,
+ "Bekijk automatiseringsscript"
+ ],
+ "Visit firewall": [
+ null,
+ "Bezoek firewall"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Wachten tot andere softwarebeheerhandelingen voltooid zijn"
+ ],
+ "Weak password": [
+ null,
+ "Zwak wachtwoord"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Webconsole voor Linux-servers"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Verkeerde gebruikersnaam of wachtwoord"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Je maakt voor de eerste keer verbinding met $0."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "Je browser staat plakken vanuit het contextmenu niet toe. Je kunt Shift+Insert gebruiken."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Je sessie is beëindigd."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Je sessie is verlopen. Log nogmaals in."
+ ],
+ "Zone": [
+ null,
+ "Zone"
+ ],
+ "[binary data]": [
+ null,
+ "[binaire data]"
+ ],
+ "[no data]": [
+ null,
+ "[geen data]"
+ ],
+ "password quality": [
+ null,
+ "wachtwoordkwaliteit"
+ ],
+ "show less": [
+ null,
+ "toon minder"
+ ],
+ "show more": [
+ null,
+ "toon meer"
+ ]
+};
diff --git a/dist/static/po.pl.js b/dist/static/po.pl.js
new file mode 100644
index 0000000..2cb0556
--- /dev/null
+++ b/dist/static/po.pl.js
@@ -0,0 +1,939 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2,
+ "language": "pl",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 dzień",
+ "$0 dni",
+ "$0 dni"
+ ],
+ "$0 error": [
+ null,
+ "błąd $0"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 zakończyło działanie z kodem $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 się nie powiodło"
+ ],
+ "$0 hour": [
+ null,
+ "$0 godzina",
+ "$0 godziny",
+ "$0 godzin"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 nie jest dostępne w żadnym repozytorium."
+ ],
+ "$0 key changed": [
+ null,
+ "Zmieniono klucz $0"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 zostało zakończone z sygnałem $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minuta",
+ "$0 minuty",
+ "$0 minut"
+ ],
+ "$0 month": [
+ null,
+ "$0 miesiąc",
+ "$0 miesiące",
+ "$0 miesięcy"
+ ],
+ "$0 week": [
+ null,
+ "$0 tydzień",
+ "$0 tygodnie",
+ "$0 tygodni"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 zostanie zainstalowane."
+ ],
+ "$0 year": [
+ null,
+ "$0 rok",
+ "$0 lata",
+ "$0 lat"
+ ],
+ "1 day": [
+ null,
+ "1 dzień"
+ ],
+ "1 hour": [
+ null,
+ "1 godzina"
+ ],
+ "1 minute": [
+ null,
+ "1 minuta"
+ ],
+ "1 week": [
+ null,
+ "1 tydzień"
+ ],
+ "20 minutes": [
+ null,
+ "20 minut"
+ ],
+ "40 minutes": [
+ null,
+ "40 minut"
+ ],
+ "5 minutes": [
+ null,
+ "5 minut"
+ ],
+ "6 hours": [
+ null,
+ "6 godzin"
+ ],
+ "60 minutes": [
+ null,
+ "Godzina"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Z powodów bezpieczeństwa, niezawodności i wydajności wymagana jest nowoczesna przeglądarka."
+ ],
+ "Absent": [
+ null,
+ "Nieobecne"
+ ],
+ "Accept key and log in": [
+ null,
+ "Przyjmij klucz i zaloguj się"
+ ],
+ "Add $0": [
+ null,
+ "Dodaj $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Dodatkowe pakiety:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Administracja za pomocą konsoli internetowej Cockpit"
+ ],
+ "Advanced TCA": [
+ null,
+ "Zaawansowane TCA"
+ ],
+ "All-in-one": [
+ null,
+ "Wszystko w jednym"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Dokumentacja ról Ansible"
+ ],
+ "Authentication failed": [
+ null,
+ "Uwierzytelnienie się nie powiodło"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Uwierzytelnienie się nie powiodło: serwer zamknął połączenie"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Wymagane jest uwierzytelnienie, aby wykonać zadania wymagające uprawnień za pomocą konsoli internetowej Cockpit"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Automatycznie za pomocą NTP"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Automatycznie za pomocą dodatkowych serwerów NTP"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Automatycznie za pomocą podanych serwerów NTP"
+ ],
+ "Automation script": [
+ null,
+ "Skrypt automatyzacji"
+ ],
+ "Blade": [
+ null,
+ "Kasetowy"
+ ],
+ "Blade enclosure": [
+ null,
+ "Obudowa kasetowa"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Obudowa rozszerzenia magistrali"
+ ],
+ "Cancel": [
+ null,
+ "Anuluj"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Nie można przekazać danych uwierzytelniania logowania"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Nie można planować zdarzeń w przeszłości"
+ ],
+ "Change": [
+ null,
+ "Zmień"
+ ],
+ "Change system time": [
+ null,
+ "Zmień czas systemu"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Zmienione klucze są często wynikiem przeinstalowania systemu operacyjnego. Nieoczekiwana zmiana może jednak wskazywać na próbę przechwycenia połączenia przez stronę trzecią."
+ ],
+ "Checking installed software": [
+ null,
+ "Sprawdzanie zainstalowanego oprogramowania"
+ ],
+ "Close": [
+ null,
+ "Zamknij"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Uwierzytelnianie Cockpit jest niepoprawnie skonfigurowane."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "Konfiguracja usług NetworkManager i firewalld w programie Cockpit"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit nie może skontaktować się z podanym komputerem."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit to menedżer serwerów ułatwiający administrowanie serwerów Linux przez przeglądarkę WWW. Można z łatwością przechodzić między terminalem a narzędziami WWW. Usługa uruchomiona za pomocą Cockpit może zostać zatrzymana za pomocą terminala. Jeśli błąd wystąpi w terminalu, to można go zobaczyć w interfejsie dziennika Cockpit."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit nie jest zgodny z oprogramowaniem w systemie."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit nie jest zainstalowany w systemie."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit jest idealny dla nowych administratorów, umożliwiając łatwe wykonywanie prostych zadań, takich jak administracja urządzeniami do przechowywania danych, badanie dzienników oraz uruchamianie i zatrzymywanie usług. Można monitorować i administrować wiele serwerów w tym samym czasie. Wystarczy dodać je pojedynczym kliknięciem."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit może nie być poprawnie wyświetlany w używanej przeglądarce"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Zbieranie i pakowanie danych diagnostycznych i wsparcia"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Zbieranie zrzutów awarii jądra"
+ ],
+ "Compact PCI": [
+ null,
+ "Kompaktowe PCI"
+ ],
+ "Connect to": [
+ null,
+ "Połącz z"
+ ],
+ "Connect to:": [
+ null,
+ "Połącz z:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Połączenie przekroczyło czas oczekiwania."
+ ],
+ "Convertible": [
+ null,
+ "2 w jednym"
+ ],
+ "Copy": [
+ null,
+ "Skopiuj"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Skopiuj do schowka"
+ ],
+ "Create": [
+ null,
+ "Utwórz"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Utwórz nowy plik zadania o tej treści."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Opóźnienie"
+ ],
+ "Desktop": [
+ null,
+ "Komputer stacjonarny"
+ ],
+ "Detachable": [
+ null,
+ "Odłączalny"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Raporty diagnostyczne"
+ ],
+ "Docking station": [
+ null,
+ "Stacja dokująca"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Pobierz nową przeglądarkę za darmo"
+ ],
+ "Downloading $0": [
+ null,
+ "Pobieranie $0"
+ ],
+ "Dual rank": [
+ null,
+ "Podwójny stopień"
+ ],
+ "Embedded PC": [
+ null,
+ "Komputer osadzony"
+ ],
+ "Excellent password": [
+ null,
+ "Doskonałe hasło"
+ ],
+ "Expansion chassis": [
+ null,
+ "Obudowa rozszerzenia"
+ ],
+ "Failed to change password": [
+ null,
+ "Zmiana hasła się nie powiodła"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "Włączenie $0 w usłudze firewalld się nie powiodło"
+ ],
+ "Go to now": [
+ null,
+ "Przejdź teraz"
+ ],
+ "Handheld": [
+ null,
+ "Przenośny"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Klucz komputera jest niepoprawny"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Jeśli odcisk się zgadza, należy kliknąć „Przyjmij klucz i zaloguj się”. W przeciwnym przypadku nie należy się logować i należy skontaktować się z administratorem."
+ ],
+ "Install": [
+ null,
+ "Zainstaluj"
+ ],
+ "Install software": [
+ null,
+ "Zainstaluj oprogramowanie"
+ ],
+ "Installing $0": [
+ null,
+ "Instalowanie $0"
+ ],
+ "Internal error": [
+ null,
+ "Wewnętrzny błąd"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Wewnętrzny błąd: nieprawidłowy nagłówek wyzwania"
+ ],
+ "Invalid date format": [
+ null,
+ "Nieprawidłowy format daty"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Nieprawidłowy format daty i nieprawidłowy format czasu"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Nieprawidłowe uprawnienia pliku"
+ ],
+ "Invalid time format": [
+ null,
+ "Nieprawidłowy format czasu"
+ ],
+ "Invalid timezone": [
+ null,
+ "Nieprawidłowa strefa czasowa"
+ ],
+ "IoT gateway": [
+ null,
+ "Brama IoT"
+ ],
+ "Kernel dump": [
+ null,
+ "Zrzut jądra"
+ ],
+ "Laptop": [
+ null,
+ "Laptop"
+ ],
+ "Learn more": [
+ null,
+ "Więcej informacji"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Wczytywanie modyfikacji systemu…"
+ ],
+ "Log in": [
+ null,
+ "Zaloguj"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Logowanie za pomocą konta użytkownika serwera."
+ ],
+ "Log messages": [
+ null,
+ "Komunikaty dziennika"
+ ],
+ "Login": [
+ null,
+ "Logowanie"
+ ],
+ "Login again": [
+ null,
+ "Zaloguj ponownie"
+ ],
+ "Login failed": [
+ null,
+ "Logowanie się nie powiodło"
+ ],
+ "Logout successful": [
+ null,
+ "Pomyślnie wylogowano"
+ ],
+ "Low profile desktop": [
+ null,
+ "Komputer stacjonarny o mniejszym rozmiarze"
+ ],
+ "Lunch box": [
+ null,
+ "Lunch box"
+ ],
+ "Main server chassis": [
+ null,
+ "Główna obudowa serwera"
+ ],
+ "Manage storage": [
+ null,
+ "Zarządzanie urządzeniami do przechowywania danych"
+ ],
+ "Manually": [
+ null,
+ "Ręcznie"
+ ],
+ "Message to logged in users": [
+ null,
+ "Wiadomość do zalogowanych użytkowników"
+ ],
+ "Mini PC": [
+ null,
+ "Mini PC"
+ ],
+ "Mini tower": [
+ null,
+ "Mini tower"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Obudowa dla wielu komputerów"
+ ],
+ "NTP server": [
+ null,
+ "Serwer NTP"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Wymagany jest co najmniej jeden serwer NTP"
+ ],
+ "Networking": [
+ null,
+ "Sieć"
+ ],
+ "New host": [
+ null,
+ "Nowy komputer"
+ ],
+ "New password was not accepted": [
+ null,
+ "Nie przyjęto nowego hasła"
+ ],
+ "No delay": [
+ null,
+ "Brak opóźnienia"
+ ],
+ "No such file or directory": [
+ null,
+ "Nie ma takiego pliku lub katalogu"
+ ],
+ "No system modifications": [
+ null,
+ "Brak modyfikacji systemu"
+ ],
+ "Not a valid private key": [
+ null,
+ "Nieprawidłowy klucz prywatny"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Brak uprawnień do wykonania tego działania."
+ ],
+ "Not synchronized": [
+ null,
+ "Niesynchronizowane"
+ ],
+ "Notebook": [
+ null,
+ "Notebook"
+ ],
+ "Occurrences": [
+ null,
+ "Wystąpienia"
+ ],
+ "Ok": [
+ null,
+ "OK"
+ ],
+ "Old password not accepted": [
+ null,
+ "Nie przyjęto poprzedniego hasła"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Po zainstalowaniu programu Cockpit należy go włączyć za pomocą polecenia „systemctl enable --now cockpit.socket”."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Lub użyj dołączonej przeglądarki"
+ ],
+ "Other": [
+ null,
+ "Inne"
+ ],
+ "Other options": [
+ null,
+ "Inne opcje"
+ ],
+ "PackageKit crashed": [
+ null,
+ "Usługa PackageKit uległa awarii"
+ ],
+ "Password": [
+ null,
+ "Hasło"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Hasło jest do przyjęcia"
+ ],
+ "Password is too weak": [
+ null,
+ "Hasło jest za słabe"
+ ],
+ "Password not accepted": [
+ null,
+ "Nie przyjęto hasła"
+ ],
+ "Paste": [
+ null,
+ "Wklej"
+ ],
+ "Paste error": [
+ null,
+ "Błąd wklejania"
+ ],
+ "Path to file": [
+ null,
+ "Ścieżka do pliku"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Obudowa peryferyjna"
+ ],
+ "Permission denied": [
+ null,
+ "Odmowa dostępu"
+ ],
+ "Pick date": [
+ null,
+ "Wybierz datę"
+ ],
+ "Pizza box": [
+ null,
+ "Pizza box"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Proszę włączyć obsługę języka JavaScript, aby używać konsoli internetowej."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Proszę podać komputer, z którym się połączyć"
+ ],
+ "Portable": [
+ null,
+ "Przenośne"
+ ],
+ "Present": [
+ null,
+ "Obecne"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Pytanie przez ssh-add przekroczyło czas oczekiwania"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Pytanie przez ssh-keygen przekroczyło czas oczekiwania"
+ ],
+ "RAID chassis": [
+ null,
+ "Obudowa RAID"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Obudowa do montowania w szafie"
+ ],
+ "Reboot": [
+ null,
+ "Uruchom ponownie"
+ ],
+ "Recent hosts": [
+ null,
+ "Ostatnie komputery"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Odmowa połączenia. Komputer jest nieznany"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Odmowa połączenia. Klucze komputera się nie zgadzają"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Odmowa połączenia. Klucz komputera jest nieznany"
+ ],
+ "Removals:": [
+ null,
+ "Usuwane:"
+ ],
+ "Remove host": [
+ null,
+ "Usuń komputer"
+ ],
+ "Removing $0": [
+ null,
+ "Usuwanie $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "Sealed-case PC"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Konfiguracja i rozwiązywanie błędów z SELinuksem"
+ ],
+ "Server": [
+ null,
+ "Serwer"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Serwer zamknął połączenie."
+ ],
+ "Set time": [
+ null,
+ "Ustaw czas"
+ ],
+ "Shell script": [
+ null,
+ "Skrypt powłoki"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Shut down": [
+ null,
+ "Wyłącz"
+ ],
+ "Single rank": [
+ null,
+ "Pojedynczy stopień"
+ ],
+ "Space-saving computer": [
+ null,
+ "Komputer oszczędzający miejsce"
+ ],
+ "Specific time": [
+ null,
+ "Podany czas"
+ ],
+ "Stick PC": [
+ null,
+ "Stick PC"
+ ],
+ "Storage": [
+ null,
+ "Przechowywanie danych"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Obudowa podrzędna"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Sub-Notebook"
+ ],
+ "Synchronized": [
+ null,
+ "Synchronizowane"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Synchronizowane z $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Synchronizowanie"
+ ],
+ "Tablet": [
+ null,
+ "Tablet"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "Zalogowany użytkownik nie ma zezwolenia na wyświetlanie modyfikacji systemu"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Hasła się nie zgadzają."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "Powstały odcisk można udostępniać publicznie, na przykład przez e-mail."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Serwer odmówił uwierzytelnienia „$0” za pomocą hasła, a żadne inne obsługiwane metody uwierzytelniania nie są dostępne."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Serwer odmówił uwierzytelnienia za pomocą wszystkich obsługiwanych metod."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Konfiguracja przeglądarki uniemożliwia działanie programu Cockpit ($0 jest niedostępne)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "To narzędzie konfiguruje zasady SELinuksa i może pomóc w zrozumieniu i rozwiązywaniu naruszeń zasad."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "To narzędzie konfiguruje komputer do zapisywania zrzutów awarii jądra na dysku."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "To narzędzie tworzy archiwum konfiguracji i informacji diagnostycznych z działającego komputera. Archiwum może być przechowywane lokalnie lub centralnie do celów nagrywania i śledzenia, albo może być wysyłane do przedstawicieli pomocy technicznej, programistów lub administratorów komputera w celu wspomagania znajdowania źródła problemu i debugowania."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "To narzędzie zarządza lokalnymi urządzeniami do przechowywania danych, takimi jak systemy plików, grupy woluminów LVM2 czy punkty montowania NFS."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "To narzędzie zarządza sieciami, takimi jak wiązania, mostki, zespoły, VLAN i zapory sieciowe za pomocą usług NetworkManager i firewalld. Usługa NetworkManager jest niezgodna z domyślnymi skryptami systemd-networkd systemu Ubuntu i ifupdown systemu Debian."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Ta przeglądarka jest za stara, aby uruchomić konsolę internetową (brak $0)"
+ ],
+ "Time zone": [
+ null,
+ "Strefa czasowa"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Aby upewnić się, że połączenie nie jest przechwytywane przez szkodliwą stronę trzecią, proszę zweryfikować odcisk klucza komputera:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Aby zweryfikować odcisk klucza, należy wykonać poniższe polecenie na $0 fizycznie siedząc przy komputerze lub przez zaufaną sieć:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Przełącz wybór daty"
+ ],
+ "Too much data": [
+ null,
+ "Za dużo danych"
+ ],
+ "Total size: $0": [
+ null,
+ "Całkowity rozmiar: $0"
+ ],
+ "Tower": [
+ null,
+ "Tower"
+ ],
+ "Try again": [
+ null,
+ "Spróbuj ponownie"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Próbowanie synchronizacji z $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Nie można połączyć z tym adresem"
+ ],
+ "Unknown": [
+ null,
+ "Nieznane"
+ ],
+ "Untrusted host": [
+ null,
+ "Niezaufany komputer"
+ ],
+ "User name": [
+ null,
+ "Nazwa użytkownika"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Nazwa użytkownika nie może być pusta"
+ ],
+ "Validating authentication token": [
+ null,
+ "Sprawdzanie poprawności tokenu uwierzytelniania"
+ ],
+ "View all logs": [
+ null,
+ "Wszystkie dzienniki"
+ ],
+ "View automation script": [
+ null,
+ "Wyświetl skrypt automatyzacji"
+ ],
+ "Visit firewall": [
+ null,
+ "Otwórz zaporę sieciową"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Oczekiwanie na ukończenie pozostałych działań zarządzania oprogramowaniem"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Konsola internetowa dla serwerów systemu Linux"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Błędna nazwa użytkownika lub hasło"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Łączenie z $0 po raz pierwszy."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "Używana przeglądarka nie zezwala na wklejanie z menu kontekstowego. Można użyć klawiszy Shift+Insert."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Sesja została zakończona."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Sesja wygasła. Proszę zalogować się ponownie."
+ ],
+ "Zone": [
+ null,
+ "Strefa"
+ ],
+ "[binary data]": [
+ null,
+ "[dane binarne]"
+ ],
+ "[no data]": [
+ null,
+ "[brak danych]"
+ ],
+ "password quality": [
+ null,
+ "jakość hasła"
+ ],
+ "show less": [
+ null,
+ "wyświetl mniej"
+ ],
+ "show more": [
+ null,
+ "wyświetl więcej"
+ ]
+};
diff --git a/dist/static/po.pt_BR.js b/dist/static/po.pt_BR.js
new file mode 100644
index 0000000..09c4cab
--- /dev/null
+++ b/dist/static/po.pt_BR.js
@@ -0,0 +1,853 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => (n != 1),
+ "language": "pt_BR",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 dia",
+ "$0 dias"
+ ],
+ "$0 error": [
+ null,
+ "$0 erro"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 saiu com o código $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 falhou"
+ ],
+ "$0 hour": [
+ null,
+ "$0 hora",
+ "$0 horas"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 não está disponível em nenhum repositório."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 chave alterada"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 mortos com sinal $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minuto",
+ "$0 minutos"
+ ],
+ "$0 month": [
+ null,
+ "$0 mês",
+ "$0 meses"
+ ],
+ "$0 week": [
+ null,
+ "$0 semana",
+ "$0 semanas"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 será instalado."
+ ],
+ "$0 year": [
+ null,
+ "$0 ano",
+ "$0 anos"
+ ],
+ "1 day": [
+ null,
+ "1 dia"
+ ],
+ "1 hour": [
+ null,
+ "1 hora"
+ ],
+ "1 minute": [
+ null,
+ "1 Minuto"
+ ],
+ "1 week": [
+ null,
+ "1 semana"
+ ],
+ "20 minutes": [
+ null,
+ "20 Minutos"
+ ],
+ "40 minutes": [
+ null,
+ "40 Minutos"
+ ],
+ "5 minutes": [
+ null,
+ "5 minutos"
+ ],
+ "6 hours": [
+ null,
+ "6 horas"
+ ],
+ "60 minutes": [
+ null,
+ "60 Minutos"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Um navegador moderno é necessário para segurança, confiabilidade e desempenho."
+ ],
+ "Absent": [
+ null,
+ "Ausente"
+ ],
+ "Accept key and log in": [
+ null,
+ "Aceitar a chave e entrar"
+ ],
+ "Add $0": [
+ null,
+ "Adicionar $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Pacotes adicionais:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Administração com o Console Web do Cockpit"
+ ],
+ "Advanced TCA": [
+ null,
+ "TCA Avançado"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Documentação de papéis do Ansible"
+ ],
+ "Authentication failed": [
+ null,
+ "Falha na Autenticação"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Falha na Autenticação: Conexão encerrada com servidor"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Autenticação é necessária para executar ações privilegiadas no Console Web do Cockpit"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Usando automaticamente o NTP"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Usando automaticamente servidores NTP específicos"
+ ],
+ "Automation script": [
+ null,
+ "Script de automação"
+ ],
+ "Blade": [
+ null,
+ "Blade"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Chassi de Expansão de Barramento"
+ ],
+ "Cancel": [
+ null,
+ "Cancelar"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Não é possível prosseguir com as credenciais de login"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Não é possível agendar eventos no passado"
+ ],
+ "Change": [
+ null,
+ "Alterar"
+ ],
+ "Change system time": [
+ null,
+ "Alterar Horário do Sistema"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "As chaves alteradas frequentemente são resultado de uma reinstalação do sistema operacional. No entanto, uma alteração inesperada pode indicar uma tentativa de terceiros de interceptar sua conexão."
+ ],
+ "Checking installed software": [
+ null,
+ "Verificando o software instalado"
+ ],
+ "Close": [
+ null,
+ "Fechar"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "A autenticação do Cockpit está configurada incorretamente."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ ""
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "O Cockpit não poderia entrar em contato com o host fornecido."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit é um gerenciador de Servidores que facilita a tarefa de administrar seu servidor Linux via navegador web. Alternar entre o terminal e a ferramenta web, não é dif[icil. Um serviço pode ser iniciado pelo Cockpit e finalizado pelo Terminal. Da mesma forma, se um erro ocorrer no terminal, este pode ser detectado via interface do Cockpit."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "O Cockpit não é compatível com o software no sistema."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit não está instalado no sistema."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit é perfeito para novos administradores de sistemas, permitindo-lhes facilmente realizar tarefas simples, como a administração de armazenamento, inspecionando logs e iniciar/parar serviços. É possível monitorar e administrar vários servidores ao mesmo tempo. Basta adicioná-los com um único clique e suas máquinas vão cuidar de seus companheiros."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ ""
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ ""
+ ],
+ "Compact PCI": [
+ null,
+ "Compacto PCI"
+ ],
+ "Connect to": [
+ null,
+ "Conectar à"
+ ],
+ "Connect to:": [
+ null,
+ "Conectar à:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "A conexão expirou."
+ ],
+ "Convertible": [
+ null,
+ "Conversível"
+ ],
+ "Copy": [
+ null,
+ "Copiar"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Copiar para área de transferência"
+ ],
+ "Create": [
+ null,
+ "Criar"
+ ],
+ "Create new task file with this content.": [
+ null,
+ ""
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Atraso"
+ ],
+ "Desktop": [
+ null,
+ "Desktop"
+ ],
+ "Detachable": [
+ null,
+ "Destacável"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Relatório de diagnostico"
+ ],
+ "Docking station": [
+ null,
+ "Estação de ancoragem"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Baixe um novo navegador gratuitamente"
+ ],
+ "Downloading $0": [
+ null,
+ "Baixando $0"
+ ],
+ "Dual rank": [
+ null,
+ ""
+ ],
+ "Embedded PC": [
+ null,
+ ""
+ ],
+ "Excellent password": [
+ null,
+ "Senha excelente"
+ ],
+ "Expansion chassis": [
+ null,
+ "Chassi de Expansão"
+ ],
+ "Failed to change password": [
+ null,
+ "Falha ao mudar senha"
+ ],
+ "Go to now": [
+ null,
+ "Ir para agora"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Chave de Host incorreta"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ ""
+ ],
+ "Install": [
+ null,
+ "Instale"
+ ],
+ "Install software": [
+ null,
+ "Instale Software"
+ ],
+ "Installing $0": [
+ null,
+ "Instalando $0"
+ ],
+ "Internal error": [
+ null,
+ "Erro interno"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Erro interno: Cabeçalho de desafio inválido"
+ ],
+ "Invalid date format": [
+ null,
+ "Formato de data inválido"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Formato de data inválido e formato de tempo inválido"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Permissão de arquivos inválida"
+ ],
+ "Invalid time format": [
+ null,
+ "Formato de tempo inválido"
+ ],
+ "Invalid timezone": [
+ null,
+ "Fuso horário inválido"
+ ],
+ "IoT gateway": [
+ null,
+ "Gateway IoT"
+ ],
+ "Kernel dump": [
+ null,
+ "Dump do Kernel"
+ ],
+ "Laptop": [
+ null,
+ "Laptop"
+ ],
+ "Learn more": [
+ null,
+ "Saiba mais"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Carregando modificações do sistema..."
+ ],
+ "Log in": [
+ null,
+ "Entrar"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Faça o login com sua conta de usuário do servidor."
+ ],
+ "Log messages": [
+ null,
+ "Mensagens de Log"
+ ],
+ "Login again": [
+ null,
+ "Logar Novamente"
+ ],
+ "Login failed": [
+ null,
+ "Falha ao logar"
+ ],
+ "Logout successful": [
+ null,
+ "Login Bem Sucedido"
+ ],
+ "Low profile desktop": [
+ null,
+ "Desktop de baixo perfil"
+ ],
+ "Lunch box": [
+ null,
+ "Lunch box"
+ ],
+ "Main server chassis": [
+ null,
+ "Chassi do Servidor Principal"
+ ],
+ "Manually": [
+ null,
+ "Manualmente"
+ ],
+ "Message to logged in users": [
+ null,
+ "Mensagem para usuários logados"
+ ],
+ "Mini PC": [
+ null,
+ "Mini PC"
+ ],
+ "Mini tower": [
+ null,
+ "Mini Torre"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Chassi Multi-sistema"
+ ],
+ "NTP server": [
+ null,
+ "Servidor NTP"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Precisa de pelo menos um servidor NTP"
+ ],
+ "Networking": [
+ null,
+ "Rede"
+ ],
+ "New host": [
+ null,
+ "Novo host"
+ ],
+ "New password was not accepted": [
+ null,
+ "Nova senha não foi aceita"
+ ],
+ "No delay": [
+ null,
+ "Sem Atraso"
+ ],
+ "No such file or directory": [
+ null,
+ "Diretório ou arquivo não encontrado"
+ ],
+ "No system modifications": [
+ null,
+ "Nenhuma modificações no sistema"
+ ],
+ "Not a valid private key": [
+ null,
+ "Chave privada não válida"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Não é permitido executar esta ação."
+ ],
+ "Not synchronized": [
+ null,
+ "Não sincronizado"
+ ],
+ "Notebook": [
+ null,
+ "Notebook"
+ ],
+ "Ok": [
+ null,
+ "Ok"
+ ],
+ "Old password not accepted": [
+ null,
+ "Senha antiga não aceita"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Uma vez instalado o Cockpit, habilite-o com \"systemctl enable --now cockpit.socket\"."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Ou use um navegador incluído"
+ ],
+ "Other": [
+ null,
+ "De outros"
+ ],
+ "Other options": [
+ null,
+ "Outras Opções"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit caiu"
+ ],
+ "Password": [
+ null,
+ "Senha"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Senha não é aceitavél"
+ ],
+ "Password is too weak": [
+ null,
+ "Senha é muito fraca"
+ ],
+ "Password not accepted": [
+ null,
+ "Senha não aceita"
+ ],
+ "Paste": [
+ null,
+ "Colar"
+ ],
+ "Path to file": [
+ null,
+ "Caminho para o arquivo"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Chassi Periférico"
+ ],
+ "Permission denied": [
+ null,
+ "Permissão negada"
+ ],
+ "Pick date": [
+ null,
+ ""
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ ""
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Por favor, especifique o host para se conectar"
+ ],
+ "Portable": [
+ null,
+ "Portatil"
+ ],
+ "Present": [
+ null,
+ "Presente"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "A solicitação via ssh-add expirou"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Solicitação via ssh-keygen expirou"
+ ],
+ "Reboot": [
+ null,
+ "Reiniciar"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Recusando-se a se conectar. O host é desconhecido"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Recusando-se a se conectar. A chave não coincide"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Recusando-se a se conectar. A chave é desconhecida"
+ ],
+ "Removals:": [
+ null,
+ "Remoções:"
+ ],
+ "Removing $0": [
+ null,
+ "Removendo $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "PC com caixa vedada"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ ""
+ ],
+ "Server": [
+ null,
+ "Servidor"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "O servidor encerrou a conexão."
+ ],
+ "Set time": [
+ null,
+ "Definir Tempo"
+ ],
+ "Shell script": [
+ null,
+ "Shell script"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Shut down": [
+ null,
+ "Encerrar"
+ ],
+ "Single rank": [
+ null,
+ ""
+ ],
+ "Space-saving computer": [
+ null,
+ "Computador com economia de espaço"
+ ],
+ "Specific time": [
+ null,
+ "Tempo Específico"
+ ],
+ "Stick PC": [
+ null,
+ "Stick PC"
+ ],
+ "Storage": [
+ null,
+ "Armazenamento"
+ ],
+ "Synchronized": [
+ null,
+ "Sincronizado"
+ ],
+ "Synchronizing": [
+ null,
+ "Sincronizando"
+ ],
+ "Tablet": [
+ null,
+ "Tablet"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ ""
+ ],
+ "The passwords do not match.": [
+ null,
+ "As senhas não batem."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ ""
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "O servidor se recusou a autenticar '$0' usando a autenticação de senha e nenhum outro método de autenticação suportado está disponível."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "O servidor se recusou a autenticar usando quaisquer métodos suportados."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "A configuração do navegador da Web impede que o Cockpit seja executado (inacessível $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ ""
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ ""
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ ""
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ ""
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ ""
+ ],
+ "Time zone": [
+ null,
+ "Fuso Horário"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ ""
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ ""
+ ],
+ "Toggle date picker": [
+ null,
+ ""
+ ],
+ "Too much data": [
+ null,
+ "Muitos dados"
+ ],
+ "Total size: $0": [
+ null,
+ "Tamanho total: $0"
+ ],
+ "Tower": [
+ null,
+ "Torre"
+ ],
+ "Try again": [
+ null,
+ "Tentar novamente"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Tentando sincronizar com $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Não é possível conectar a esse endereço"
+ ],
+ "Unknown": [
+ null,
+ "Desconhecido"
+ ],
+ "Untrusted host": [
+ null,
+ "Host não confiável"
+ ],
+ "User name": [
+ null,
+ "Nome do usuário"
+ ],
+ "User name cannot be empty": [
+ null,
+ "O nome de usuário não pode estar vazio"
+ ],
+ "Validating authentication token": [
+ null,
+ "Validando token de autenticação"
+ ],
+ "View all logs": [
+ null,
+ "Ver todos os logs"
+ ],
+ "View automation script": [
+ null,
+ ""
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Aguardando que outras operações de gerenciamento de software terminem"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Console da Web para servidores Linux"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Nome de usuário ou senha incorretos"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ ""
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ ""
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Sua sessão foi encerrada."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Sua sessão expirou. Por favor, faça o login novamente."
+ ],
+ "Zone": [
+ null,
+ "Zona"
+ ],
+ "[binary data]": [
+ null,
+ "[dados binários]"
+ ],
+ "[no data]": [
+ null,
+ "[sem dados]"
+ ],
+ "password quality": [
+ null,
+ ""
+ ],
+ "show less": [
+ null,
+ "mostrar menos"
+ ],
+ "show more": [
+ null,
+ "mostrar mais"
+ ]
+};
diff --git a/dist/static/po.ru.js b/dist/static/po.ru.js
new file mode 100644
index 0000000..b583c41
--- /dev/null
+++ b/dist/static/po.ru.js
@@ -0,0 +1,927 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2,
+ "language": "ru",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 ГиБ"
+ ],
+ "$0 day": [
+ null,
+ "$0 день",
+ "$0 дня",
+ "$0 дней"
+ ],
+ "$0 error": [
+ null,
+ "Ошибка $0"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "Процесс $0 завершил работу с кодом $1"
+ ],
+ "$0 failed": [
+ null,
+ "Сбой процесса $0"
+ ],
+ "$0 hour": [
+ null,
+ "$0 час",
+ "$0 часа",
+ "$0 часов"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "Компонент $0 недоступен в репозиториях."
+ ],
+ "$0 key changed": [
+ null,
+ "Изменен $0 ключ"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "Процесс $0 прерван с сигналом $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 минута",
+ "$0 минуты",
+ "$0 минут"
+ ],
+ "$0 month": [
+ null,
+ "$0 месяц",
+ "$0 месяца",
+ "$0 месяцев"
+ ],
+ "$0 week": [
+ null,
+ "$0 неделя",
+ "$0 недели",
+ "$0 недель"
+ ],
+ "$0 will be installed.": [
+ null,
+ "Будет выполнена установка $0."
+ ],
+ "$0 year": [
+ null,
+ "$0 год",
+ "$0 года",
+ "$0 лет"
+ ],
+ "1 day": [
+ null,
+ "1 день"
+ ],
+ "1 hour": [
+ null,
+ "1 час"
+ ],
+ "1 minute": [
+ null,
+ "1 минута"
+ ],
+ "1 week": [
+ null,
+ "1 неделя"
+ ],
+ "20 minutes": [
+ null,
+ "20 минут"
+ ],
+ "40 minutes": [
+ null,
+ "40 минут"
+ ],
+ "5 minutes": [
+ null,
+ "5 минут"
+ ],
+ "6 hours": [
+ null,
+ "6 часов"
+ ],
+ "60 minutes": [
+ null,
+ "60 минут"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Для обеспечения безопасности, надёжности и производительности необходим современный браузер."
+ ],
+ "Absent": [
+ null,
+ "Отсутствует"
+ ],
+ "Accept key and log in": [
+ null,
+ "Принять ключ и войти в систему"
+ ],
+ "Add $0": [
+ null,
+ "Добавить $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Дополнительные пакеты:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Администрирование с помощью веб-панели управления Cockpit"
+ ],
+ "Advanced TCA": [
+ null,
+ "Расширенный TCA"
+ ],
+ "All-in-one": [
+ null,
+ "Все в одном"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Документация к ролям Ansible"
+ ],
+ "Authentication failed": [
+ null,
+ "Ошибка проверки подлинности"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Не удалось выполнить проверку подлинности: сервер закрыл соединение"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Для выполнения привилегированных задач с помощью веб-консоли Cockpit необходима проверка подлинности"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Автоматически с использованием серверов NTP"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Автоматически с использованием определённых серверов NTP"
+ ],
+ "Automation script": [
+ null,
+ "Сценарий автоматизации"
+ ],
+ "Blade": [
+ null,
+ "Ультракомпактный сервер"
+ ],
+ "Blade enclosure": [
+ null,
+ "Корзина"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Корпус расширения шины"
+ ],
+ "Cancel": [
+ null,
+ "Отмена"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Не удаётся передать учётные данные для входа"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Невозможно запланировать событие в прошлом"
+ ],
+ "Change": [
+ null,
+ "Изменить"
+ ],
+ "Change system time": [
+ null,
+ "Изменить системное время"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Изменение ключей часто происходит в результате переустановки операционной системы. Однако неожиданное изменение может указывать на попытку третьей стороны перехватить ваше соединение."
+ ],
+ "Checking installed software": [
+ null,
+ "Проверка установленного программного обеспечения"
+ ],
+ "Close": [
+ null,
+ "Закрыть"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Проверка подлинности Cockpit настроена неправильно."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ ""
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Не удалось установить связь между Cockpit и заданным узлом."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit представляет собой диспетчер серверов, упрощающий администрирование серверов Linux через веб-браузер. Переключение между терминалом и веб-инструментом не представляет сложности. Служба, запущенная с помощью Cockpit, может быть остановлена через терминал. Аналогично, если в терминале возникает ошибка, её можно увидеть в интерфейсе журнала Cockpit."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit не совместим с программным обеспечением в системе."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit не установлен в системе."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit идеально подходит для новых системных администраторов, позволяя им легко выполнять простые задачи, такие как администрирование хранилищ, проверка журналов и запуск и остановка служб. С помощью Cockpit вы можете контролировать и администрировать несколько серверов одновременно. Просто добавьте их одним щелчком мыши, и ваш компьютер позаботится о своих приятелях."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit может отображаться некорректно в вашем браузере"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ ""
+ ],
+ "Compact PCI": [
+ null,
+ "Компактный PCI"
+ ],
+ "Connect to": [
+ null,
+ "Подключиться к"
+ ],
+ "Connect to:": [
+ null,
+ "Подключиться к:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Превышено время ожидания подключения."
+ ],
+ "Convertible": [
+ null,
+ "Компьютер-трансформер"
+ ],
+ "Copy": [
+ null,
+ "Копировать"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Копировать в буфер обмена"
+ ],
+ "Create": [
+ null,
+ "Создать"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Создайте новый файл задачи с этим содержимым."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Задержка"
+ ],
+ "Desktop": [
+ null,
+ "Настольный компьютер"
+ ],
+ "Detachable": [
+ null,
+ "Съёмный компьютер"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Диагностические отчёты"
+ ],
+ "Docking station": [
+ null,
+ "Стыковочный узел"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Загрузите новый браузер бесплатно"
+ ],
+ "Downloading $0": [
+ null,
+ "Загрузка $0"
+ ],
+ "Dual rank": [
+ null,
+ "Двухранговая"
+ ],
+ "Embedded PC": [
+ null,
+ "Встраиваемый компьютер"
+ ],
+ "Excellent password": [
+ null,
+ "Отличный пароль"
+ ],
+ "Expansion chassis": [
+ null,
+ "Корпус расширения"
+ ],
+ "Failed to change password": [
+ null,
+ "Не удалось изменить пароль"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "Не удалось включить $0 в firewalld"
+ ],
+ "Go to now": [
+ null,
+ "Текущий момент"
+ ],
+ "Handheld": [
+ null,
+ "Наладонный компьютер"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Неверный ключ узла"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "При совпадении отпечатка нажмите кнопку «Принять ключ и войти в систему». В противном случае, не входите в систему и свяжитесь с администратором."
+ ],
+ "Install": [
+ null,
+ "Установить"
+ ],
+ "Install software": [
+ null,
+ "Установка программного обеспечения"
+ ],
+ "Installing $0": [
+ null,
+ "Установка $0"
+ ],
+ "Internal error": [
+ null,
+ "Внутренняя ошибка"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Внутренняя ошибка: недопустимый заголовок запроса"
+ ],
+ "Invalid date format": [
+ null,
+ "Недопустимый формат даты"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Недопустимый формат даты и недопустимый формат времени"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Недопустимые разрешения для файлов"
+ ],
+ "Invalid time format": [
+ null,
+ "Недопустимый формат времени"
+ ],
+ "Invalid timezone": [
+ null,
+ "Недопустимый часовой пояс"
+ ],
+ "IoT gateway": [
+ null,
+ "Шлюз Интернета вещей"
+ ],
+ "Kernel dump": [
+ null,
+ "Дамп ядра"
+ ],
+ "Laptop": [
+ null,
+ "Полноразмерный ноутбук"
+ ],
+ "Learn more": [
+ null,
+ "Подробнее"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Загрузка изменений системы…"
+ ],
+ "Log in": [
+ null,
+ "Войти"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Войдите в систему с помощью своей учётной записи пользователя сервера."
+ ],
+ "Log messages": [
+ null,
+ "Сообщения журнала"
+ ],
+ "Login": [
+ null,
+ "Вход"
+ ],
+ "Login again": [
+ null,
+ "Войти снова"
+ ],
+ "Login failed": [
+ null,
+ "Ошибка входа"
+ ],
+ "Logout successful": [
+ null,
+ "Выход из системы успешно выполнен"
+ ],
+ "Low profile desktop": [
+ null,
+ "Низкопрофильный настольный компьютер"
+ ],
+ "Lunch box": [
+ null,
+ "Портативный компьютер в ударопрочном корпусе"
+ ],
+ "Main server chassis": [
+ null,
+ "Главный серверный корпус"
+ ],
+ "Manually": [
+ null,
+ "Вручную"
+ ],
+ "Message to logged in users": [
+ null,
+ "Сообщение для вошедших пользователей"
+ ],
+ "Mini PC": [
+ null,
+ "Мини-ПК"
+ ],
+ "Mini tower": [
+ null,
+ "Компьютер в корпусе «мини-башня»"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Корпус для нескольких систем"
+ ],
+ "NTP server": [
+ null,
+ "Сервер NTP"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Требуется по крайней мере один сервер NTP"
+ ],
+ "Networking": [
+ null,
+ "Сеть"
+ ],
+ "New host": [
+ null,
+ "Новый узел"
+ ],
+ "New password was not accepted": [
+ null,
+ "Новый пароль не был принят"
+ ],
+ "No delay": [
+ null,
+ "Без задержки"
+ ],
+ "No such file or directory": [
+ null,
+ "Нет такого файла или каталога"
+ ],
+ "No system modifications": [
+ null,
+ "Изменения системы отсутствуют"
+ ],
+ "Not a valid private key": [
+ null,
+ "Недопустимый закрытый ключ"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Нет прав на выполнение этого действия."
+ ],
+ "Not synchronized": [
+ null,
+ "Не синхронизировано"
+ ],
+ "Notebook": [
+ null,
+ "Ноутбук"
+ ],
+ "Occurrences": [
+ null,
+ "События"
+ ],
+ "Ok": [
+ null,
+ "OK"
+ ],
+ "Old password not accepted": [
+ null,
+ "Старый пароль не был принят"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "После установки Cockpit включите его при помощи команды «systemctl enable --now cockpit.socket»."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Или используйте встроенный браузер"
+ ],
+ "Other": [
+ null,
+ "Прочее"
+ ],
+ "Other options": [
+ null,
+ "Другие параметры"
+ ],
+ "PackageKit crashed": [
+ null,
+ "Сбой PackageKit"
+ ],
+ "Password": [
+ null,
+ "Пароль"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Недопустимый пароль"
+ ],
+ "Password is too weak": [
+ null,
+ "Пароль недостаточно надёжен"
+ ],
+ "Password not accepted": [
+ null,
+ "Пароль не принят"
+ ],
+ "Paste": [
+ null,
+ "Вставить"
+ ],
+ "Paste error": [
+ null,
+ "Ошибка вставки"
+ ],
+ "Path to file": [
+ null,
+ "Путь к файлу"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Корпус для периферийных устройств"
+ ],
+ "Permission denied": [
+ null,
+ "В разрешении отказано"
+ ],
+ "Pick date": [
+ null,
+ "Выбор даты"
+ ],
+ "Pizza box": [
+ null,
+ "Ультратонкий корпус"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Включите JavaScript для использования веб-консоли."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Укажите узел для подключения"
+ ],
+ "Portable": [
+ null,
+ "Портативный компьютер"
+ ],
+ "Present": [
+ null,
+ "Присутствует"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Превышено время ожидания запроса по ssh-add"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Превышено время ожидания запроса по ssh-keygen"
+ ],
+ "RAID chassis": [
+ null,
+ "Корпус для RAID-массива"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Монтируемый в стойку корпус"
+ ],
+ "Reboot": [
+ null,
+ "Перезагрузка"
+ ],
+ "Recent hosts": [
+ null,
+ "Недавние узлы"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Отказано в подключении. Неизвестный узел"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Отказано в подключении. Неверный ключ узла"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Отказано в подключении. Неизвестный ключ узла"
+ ],
+ "Removals:": [
+ null,
+ "Для удаления:"
+ ],
+ "Remove host": [
+ null,
+ "Удалить узел"
+ ],
+ "Removing $0": [
+ null,
+ "Удаление $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "Компьютер с невскрываемым корпусом"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ ""
+ ],
+ "Server": [
+ null,
+ "Сервер"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Сервер закрыл соединение."
+ ],
+ "Set time": [
+ null,
+ "Настроить время"
+ ],
+ "Shell script": [
+ null,
+ "Сценарий оболочки"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Shut down": [
+ null,
+ "Завершение работы"
+ ],
+ "Single rank": [
+ null,
+ "Одноранговая"
+ ],
+ "Space-saving computer": [
+ null,
+ "Компактный компьютер"
+ ],
+ "Specific time": [
+ null,
+ "Определённое время"
+ ],
+ "Stick PC": [
+ null,
+ "ПК-брелок"
+ ],
+ "Storage": [
+ null,
+ "Хранилище"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Дополнительный корпус"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Субноутбук"
+ ],
+ "Synchronized": [
+ null,
+ "Синхронизировано"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Синхронизировано с $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Синхронизация"
+ ],
+ "Tablet": [
+ null,
+ "Планшетный ПК"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "Текущему пользователю запрещено просматривать изменения системы"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Пароли не совпадают."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "Полученный отпечаток можно распространять по общедоступным каналам связи, включая электронную почту."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Сервер отклонил проверку подлинности «$0» с помощью пароля, а другие доступные методы проверки подлинности отсутствуют."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Сервер отклонил проверку подлинности с использованием любых поддерживаемых методов."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Конфигурация веб-браузера препятствует запуску Cockpit (недоступен $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ ""
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ ""
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ ""
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ ""
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ ""
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Этот веб-браузер слишком устарел для запуска Веб-консоли (отсутствует $0)"
+ ],
+ "Time zone": [
+ null,
+ "Часовой пояс"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Чтобы убедиться в том, что данные вашего соединения не были перехвачены злоумышленниками, проверьте отпечаток ключа данного узла:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Для проверки отпечатка запустите следующую команду на $0, физически работая на этом компьютере или по доверенной сети:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Переключить средство выбора даты"
+ ],
+ "Too much data": [
+ null,
+ "Слишком много данных"
+ ],
+ "Total size: $0": [
+ null,
+ "Общий размер: $0"
+ ],
+ "Tower": [
+ null,
+ "Компьютер в корпусе «башня»"
+ ],
+ "Try again": [
+ null,
+ "Повторить попытку"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Попытка синхронизации с $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Не удалось подключиться к адресу"
+ ],
+ "Unknown": [
+ null,
+ "Неизвестно"
+ ],
+ "Untrusted host": [
+ null,
+ "Недоверенный узел"
+ ],
+ "User name": [
+ null,
+ "Имя пользователя"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Имя пользователя не может быть пустым"
+ ],
+ "Validating authentication token": [
+ null,
+ "Проверка маркера аутентификации"
+ ],
+ "View all logs": [
+ null,
+ "Смотреть все журналы"
+ ],
+ "View automation script": [
+ null,
+ "Просмотреть сценарий автоматизации"
+ ],
+ "Visit firewall": [
+ null,
+ "Перейти к межсетевому экрану"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Ожидание завершения других операций управления программным обеспечением"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Веб-консоль для серверов Linux"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Неверное имя пользователя или пароль"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Подключение к $0 выполняется в первый раз."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "В данном браузере отсутствует возможность вставки с помощью контекстного меню. Можно использовать комбинацию Shift+Insert."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Сеанс завершён."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Срок действия сеанса истёк. Войдите в систему снова."
+ ],
+ "Zone": [
+ null,
+ "Зона"
+ ],
+ "[binary data]": [
+ null,
+ "[двоичные данные]"
+ ],
+ "[no data]": [
+ null,
+ "[нет данных]"
+ ],
+ "password quality": [
+ null,
+ "качество пароля"
+ ],
+ "show less": [
+ null,
+ "показать меньше"
+ ],
+ "show more": [
+ null,
+ "показать больше"
+ ]
+};
diff --git a/dist/static/po.sk.js b/dist/static/po.sk.js
new file mode 100644
index 0000000..1fb5144
--- /dev/null
+++ b/dist/static/po.sk.js
@@ -0,0 +1,895 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2,
+ "language": "sk",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 deň",
+ "$0 dni",
+ "$0 dní"
+ ],
+ "$0 error": [
+ null,
+ "$0 chyba"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 skončilo s kódom $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 sa nepodarilo"
+ ],
+ "$0 hour": [
+ null,
+ "$0 hodina",
+ "$0 hodiny",
+ "$0 hodín"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 nie je k dispozícií v žiadom repozitári."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 kľúč sa zmenil"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 nútene ukončené signálom $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minúta",
+ "$0 minúty",
+ "$0 minút"
+ ],
+ "$0 month": [
+ null,
+ "$0 mesiac",
+ "$0 mesiace",
+ "$0 mesiacov"
+ ],
+ "$0 week": [
+ null,
+ "$0 týždeň",
+ "$0 týždne",
+ "$0 týždňov"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 bude nainštalovaný."
+ ],
+ "$0 year": [
+ null,
+ "$0 rok",
+ "$0 roky",
+ "$0 rokov"
+ ],
+ "1 day": [
+ null,
+ "1 deň"
+ ],
+ "1 hour": [
+ null,
+ "1 hodina"
+ ],
+ "1 minute": [
+ null,
+ "1 minúta"
+ ],
+ "1 week": [
+ null,
+ "1 týždeň"
+ ],
+ "20 minutes": [
+ null,
+ "20 minút"
+ ],
+ "40 minutes": [
+ null,
+ "40 minút"
+ ],
+ "5 minutes": [
+ null,
+ "5 minút"
+ ],
+ "6 hours": [
+ null,
+ "6 hodín"
+ ],
+ "60 minutes": [
+ null,
+ "60 minút"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Z dôvodu zabezpečenia, spoľahlivosti a rýchlosti je potrebný moderný prehliadač."
+ ],
+ "Absent": [
+ null,
+ "Chýba"
+ ],
+ "Accept key and log in": [
+ null,
+ "Prijať kľúč a prihlásiť sa"
+ ],
+ "Add $0": [
+ null,
+ "Pridať $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Ďalšie balíky:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Správa pomocou webovej konzole Cockpit"
+ ],
+ "Advanced TCA": [
+ null,
+ "Pokročilé TCA"
+ ],
+ "All-in-one": [
+ null,
+ "All-in-one"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Dokumentácia k Ansible roliam"
+ ],
+ "Authentication failed": [
+ null,
+ "Overenie sa nepodarilo"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ ""
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ ""
+ ],
+ "Automatically using NTP": [
+ null,
+ "Automaticky využitím NTP"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Automaticky využitím konkrétnych NTP serverov"
+ ],
+ "Automation script": [
+ null,
+ "Automatizačný skript"
+ ],
+ "Blade": [
+ null,
+ "Blade"
+ ],
+ "Blade enclosure": [
+ null,
+ "Skriňa pre blade servery"
+ ],
+ "Bus expansion chassis": [
+ null,
+ ""
+ ],
+ "Bypass browser check": [
+ null,
+ ""
+ ],
+ "Cancel": [
+ null,
+ "Zrušiť"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Nie je možné preposlať prístupové údaje"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Nie je možné plánovať udalosti do minulosti"
+ ],
+ "Change": [
+ null,
+ "Zmeniť"
+ ],
+ "Change system time": [
+ null,
+ "Zmeniť systémový čas"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Zmenené kľúče sú často výsledkom preinštalovania operačného systému. Avšak neočakávaná zmena môže znamenať, že sa tretia strana snaží odpočúvať vaše spojenie."
+ ],
+ "Checking installed software": [
+ null,
+ "Zisťuje sa nainštalovaný software"
+ ],
+ "Close": [
+ null,
+ "Zavrieť"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ ""
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ ""
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpitu sa nepodarilo kontaktovať daného hostiteľa."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit je správca serveru, který uľahčuje správu Linuxových serverov cez webový prehliadač. Nie je žiadným problémom prechádzať medzi terminálom a webovým nástrojom. Služba spustená cez Cockpit môže byť zastavená v termináli. Podobne, pokiaľ dôjde k chybe v termináli, je toto vidieť v rozhraní žurnálu v Cockpite."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit nie je kompatibilný so sofvérom na systéme."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit nie je nainštalovaný na tomto systéme."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit je skvelý nástroj pre nových správcov serverov, ktorým jednoducho umožňuje vykonávať úlohy ako správa úložiska, kontrola žurnálu a spúšťanie či zastavovanie služieb. Môžte monitorovať a spravovať viacero serverov naraz. Stačí ich pridať jedným kliknutím a vaše stroje sa budú starať o svojích kamarátov."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ ""
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ ""
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ ""
+ ],
+ "Compact PCI": [
+ null,
+ "Kompaktné PCI"
+ ],
+ "Connect to": [
+ null,
+ "Pripojiť k"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Časový limit spojenia vypršal."
+ ],
+ "Convertible": [
+ null,
+ "Počítač 2v1"
+ ],
+ "Copy": [
+ null,
+ "Kopírovať"
+ ],
+ "Copy to clipboard": [
+ null,
+ ""
+ ],
+ "Create": [
+ null,
+ "Vytvoriť"
+ ],
+ "Create new task file with this content.": [
+ null,
+ ""
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Omeškanie"
+ ],
+ "Desktop": [
+ null,
+ "Desktop"
+ ],
+ "Detachable": [
+ null,
+ "Odpojiteľné"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Diagnostické hlásenia"
+ ],
+ "Docking station": [
+ null,
+ "Dokovacia stanica"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Stiahnite si zdarma nový prehliadač"
+ ],
+ "Downloading $0": [
+ null,
+ "Sťahuje sa $0"
+ ],
+ "Dual rank": [
+ null,
+ "Dual rank"
+ ],
+ "Embedded PC": [
+ null,
+ ""
+ ],
+ "Excellent password": [
+ null,
+ "Skvelé heslo"
+ ],
+ "Expansion chassis": [
+ null,
+ ""
+ ],
+ "Failed to change password": [
+ null,
+ "Nepodarilo sa zmeniť heslo"
+ ],
+ "Go to now": [
+ null,
+ "Prejsť na súčasnosť"
+ ],
+ "Handheld": [
+ null,
+ "Pre držanie do ruky"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Kľúč stroja nie je správny"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ ""
+ ],
+ "Install": [
+ null,
+ "Inštalovať"
+ ],
+ "Install software": [
+ null,
+ "Inštalovať software"
+ ],
+ "Installing $0": [
+ null,
+ "Inštaluje sa $0"
+ ],
+ "Internal error": [
+ null,
+ "Interná chyba"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ ""
+ ],
+ "Invalid date format": [
+ null,
+ "Neplatný formát dátumu"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Neplatný formát dátumu a času"
+ ],
+ "Invalid file permissions": [
+ null,
+ ""
+ ],
+ "Invalid time format": [
+ null,
+ "Neplatný formát čase"
+ ],
+ "Invalid timezone": [
+ null,
+ "Neplatná časová zóna"
+ ],
+ "IoT gateway": [
+ null,
+ ""
+ ],
+ "Kernel dump": [
+ null,
+ ""
+ ],
+ "Laptop": [
+ null,
+ "Notebook"
+ ],
+ "Learn more": [
+ null,
+ "Zistiť viac"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Načítavajú sa systémové zmeny..."
+ ],
+ "Log in": [
+ null,
+ "Prihlásiť"
+ ],
+ "Log in with your server user account.": [
+ null,
+ ""
+ ],
+ "Log messages": [
+ null,
+ "Záznamy udalostí"
+ ],
+ "Login again": [
+ null,
+ "Znovu prihlásiť"
+ ],
+ "Login failed": [
+ null,
+ "Prihlásenie sa nepodarilo"
+ ],
+ "Logout successful": [
+ null,
+ "Odhlásenie bolo úspešné"
+ ],
+ "Low profile desktop": [
+ null,
+ "Nízky desktop"
+ ],
+ "Lunch box": [
+ null,
+ "Kufríkový počítač"
+ ],
+ "Main server chassis": [
+ null,
+ "Hlavná skriňa serveru"
+ ],
+ "Manually": [
+ null,
+ "Ručne"
+ ],
+ "Message to logged in users": [
+ null,
+ ""
+ ],
+ "Mini PC": [
+ null,
+ ""
+ ],
+ "Mini tower": [
+ null,
+ ""
+ ],
+ "Multi-system chassis": [
+ null,
+ ""
+ ],
+ "NTP server": [
+ null,
+ "NTP server"
+ ],
+ "Need at least one NTP server": [
+ null,
+ ""
+ ],
+ "Networking": [
+ null,
+ "Sieť"
+ ],
+ "New host": [
+ null,
+ "Nový hostiteľ"
+ ],
+ "New password was not accepted": [
+ null,
+ "Nové heslo nebolo prijaté"
+ ],
+ "No delay": [
+ null,
+ ""
+ ],
+ "No such file or directory": [
+ null,
+ ""
+ ],
+ "No system modifications": [
+ null,
+ "Žiadne systémové zmeny"
+ ],
+ "Not a valid private key": [
+ null,
+ ""
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Neoprávený k vykonaniu tejto akcie."
+ ],
+ "Not synchronized": [
+ null,
+ "Nezosynchronizované"
+ ],
+ "Notebook": [
+ null,
+ "Notebook"
+ ],
+ "Ok": [
+ null,
+ "Ok"
+ ],
+ "Old password not accepted": [
+ null,
+ "Pôvodné heslo nebolo prijaté"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Keď bude Cockpit nainštalovaný, povoľte ho pomocou \"systemctl enable --now cockpit.socket\"."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Alebo použite pribalený prehliadač"
+ ],
+ "Other": [
+ null,
+ "Iný"
+ ],
+ "Other options": [
+ null,
+ "Iné voľby"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit zhavaroval"
+ ],
+ "Password": [
+ null,
+ "Heslo"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Heslo nie je prijatelné"
+ ],
+ "Password is too weak": [
+ null,
+ "Heslo je príliš slabé"
+ ],
+ "Password not accepted": [
+ null,
+ "Heslo nebolo prijaté"
+ ],
+ "Paste": [
+ null,
+ "Vložiť"
+ ],
+ "Path to file": [
+ null,
+ "Cesta k súboru"
+ ],
+ "Peripheral chassis": [
+ null,
+ ""
+ ],
+ "Permission denied": [
+ null,
+ "Prístup odmietnutý"
+ ],
+ "Pick date": [
+ null,
+ "Vybrať dátum"
+ ],
+ "Pizza box": [
+ null,
+ ""
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ ""
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Zadajte stroj, ku ktorému sa chcete pripojiť"
+ ],
+ "Portable": [
+ null,
+ "Prenosný"
+ ],
+ "Present": [
+ null,
+ "Prítomné"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ ""
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ ""
+ ],
+ "RAID chassis": [
+ null,
+ "RAID skriňa"
+ ],
+ "Rack mount chassis": [
+ null,
+ ""
+ ],
+ "Reboot": [
+ null,
+ "Reštartovať"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Odmieta sa pripojenie. Stroj nie je známy"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Odmieta sa pripojenie. Kľúč stroja sa líši"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Odmieta sa pripojenie. Kľúč stroja nie je známy"
+ ],
+ "Removals:": [
+ null,
+ "Odstránenia:"
+ ],
+ "Removing $0": [
+ null,
+ "Odstraňuje sa $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "Počítač so zapäčatenou skriňou"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ ""
+ ],
+ "Server": [
+ null,
+ "Server"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Server zavrel spojenie."
+ ],
+ "Set time": [
+ null,
+ "Nastaviť čas"
+ ],
+ "Shell script": [
+ null,
+ "Shellový skript"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Shut down": [
+ null,
+ "Vypnúť"
+ ],
+ "Single rank": [
+ null,
+ "Single rank"
+ ],
+ "Space-saving computer": [
+ null,
+ "Miesto-šetriaci počítač"
+ ],
+ "Specific time": [
+ null,
+ "Konkrétny čas"
+ ],
+ "Stick PC": [
+ null,
+ ""
+ ],
+ "Storage": [
+ null,
+ "Úložisko"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Podskriňa"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Sub-Notebook"
+ ],
+ "Synchronized": [
+ null,
+ "Synchronizované"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Synchronizované s $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Synchronizuje sa"
+ ],
+ "Tablet": [
+ null,
+ "Tablet"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ ""
+ ],
+ "The passwords do not match.": [
+ null,
+ "Heslá sa líšia."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ ""
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Server odmietol overiť „$0“ pomocou overenia heslom a nie sú k dispozícií žiadne ďalšie metódy overenia."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ ""
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ ""
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ ""
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ ""
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ ""
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ ""
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ ""
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ ""
+ ],
+ "Time zone": [
+ null,
+ "Časová zóna"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ ""
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ ""
+ ],
+ "Toggle date picker": [
+ null,
+ ""
+ ],
+ "Too much data": [
+ null,
+ ""
+ ],
+ "Total size: $0": [
+ null,
+ "Celková veľkosť: $0"
+ ],
+ "Tower": [
+ null,
+ "Veža"
+ ],
+ "Try again": [
+ null,
+ "Skúsiť znova"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ ""
+ ],
+ "Unable to connect to that address": [
+ null,
+ ""
+ ],
+ "Unknown": [
+ null,
+ "Neznáme"
+ ],
+ "Untrusted host": [
+ null,
+ "Nedôveryhodnotný stroj"
+ ],
+ "User name": [
+ null,
+ "Užívateľské meno"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Užívateľské meno nemôže byť prázdne"
+ ],
+ "Validating authentication token": [
+ null,
+ "Kontroluje sa overovací žetón"
+ ],
+ "View automation script": [
+ null,
+ "Ukázať automatizačný skript"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Čaká sa na dokončenie ostatných operácií správy balíčkov"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Webová konzola pre linuxové servery"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Chybné užívateľské meno alebo heslo"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ ""
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ ""
+ ],
+ "Your session has been terminated.": [
+ null,
+ ""
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ ""
+ ],
+ "Zone": [
+ null,
+ "Zóna"
+ ],
+ "[binary data]": [
+ null,
+ "[binárne dáta]"
+ ],
+ "[no data]": [
+ null,
+ "[žiadne dáta]"
+ ],
+ "show less": [
+ null,
+ "ukázať menej"
+ ],
+ "show more": [
+ null,
+ "ukázať viac"
+ ]
+};
diff --git a/dist/static/po.sv.js b/dist/static/po.sv.js
new file mode 100644
index 0000000..07b4cad
--- /dev/null
+++ b/dist/static/po.sv.js
@@ -0,0 +1,965 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n != 1,
+ "language": "sv",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 dag",
+ "$0 dagar"
+ ],
+ "$0 error": [
+ null,
+ "$0 fel"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 avslutade med kod $1"
+ ],
+ "$0 failed": [
+ null,
+ "$0 misslyckades"
+ ],
+ "$0 hour": [
+ null,
+ "$0 timme",
+ "$0 timmar"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 är inte tillgängligt från något förråd."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 nyckel ändrad"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 dödad med signal $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 minut",
+ "$0 minuter"
+ ],
+ "$0 month": [
+ null,
+ "$0 månad",
+ "$0 månader"
+ ],
+ "$0 week": [
+ null,
+ "$0 vecka",
+ "$0 veckor"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 kommer att installeras."
+ ],
+ "$0 year": [
+ null,
+ "$0 år",
+ "$0 år"
+ ],
+ "1 day": [
+ null,
+ "1 dag"
+ ],
+ "1 hour": [
+ null,
+ "1 timme"
+ ],
+ "1 minute": [
+ null,
+ "1 minut"
+ ],
+ "1 week": [
+ null,
+ "1 vecka"
+ ],
+ "20 minutes": [
+ null,
+ "20 minuter"
+ ],
+ "40 minutes": [
+ null,
+ "40 minuter"
+ ],
+ "5 minutes": [
+ null,
+ "5 minuter"
+ ],
+ "6 hours": [
+ null,
+ "6 timmar"
+ ],
+ "60 minutes": [
+ null,
+ "60 minuter"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "En modern webbläsare krävs för säkerhet, pålitlighet och prestanda."
+ ],
+ "Absent": [
+ null,
+ "Frånvarande"
+ ],
+ "Accept key and log in": [
+ null,
+ "Acceptera nyckel och logga in"
+ ],
+ "Acceptable password": [
+ null,
+ "Acceptabelt lösenord"
+ ],
+ "Add $0": [
+ null,
+ "Lägg till $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Ytterligare paket:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Administration med Cockpits webbkonsol"
+ ],
+ "Advanced TCA": [
+ null,
+ "Avancerad TCA"
+ ],
+ "All-in-one": [
+ null,
+ "Allt i ett"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Dokumentation för Ansibleroller"
+ ],
+ "Authentication failed": [
+ null,
+ "Autentisering misslyckades"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Autentiseringen misslyckades: servern stängde ner förbindelsen"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Autentisering krävs för att utföra privilegierade uppgifter med Cockpits webbkonsol"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Använder automatiskt NTP"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Använder automatiskt ytterligare NTP-servrar"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Använder automatiskt specifika NTP-servrar"
+ ],
+ "Automation script": [
+ null,
+ "Automatiseringsskript"
+ ],
+ "Blade": [
+ null,
+ "Blad"
+ ],
+ "Blade enclosure": [
+ null,
+ "Bladhölje"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Bussexpansionschassi"
+ ],
+ "Bypass browser check": [
+ null,
+ "Förbigå webbläsarkontroll"
+ ],
+ "Cancel": [
+ null,
+ "Avbryt"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Kan inte vidarebefordra inloggningsuppgifterna"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Kan inte schemalägga händelser som redan hänt"
+ ],
+ "Change": [
+ null,
+ "Ändra"
+ ],
+ "Change system time": [
+ null,
+ "Ändra systemtid"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Ändrade nycklar är ofta resultatet av en ominstallation av operativsystemet. En oväntad förändring kan dock indikera ett försök från tredje part att avlyssna din anslutning."
+ ],
+ "Checking installed software": [
+ null,
+ "Kontrollerar installerad programvara"
+ ],
+ "Close": [
+ null,
+ "Stäng"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Cockpit-autentisering är felaktigt konfigurerad."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "Cockpit konfiguration av NetworkManager och Firewalld"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit kunde inte kontakta den angivna värden."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit är en serverhanterare som gör det lätt att administrera dina Linuxservrar via en webbläsare. Att hoppa mellan terminalen och webbverktyget är inget problem. En tjänst som startas via Cockpit kan stoppas via terminalen. Likaledes, om ett fel uppstår i terminalen kan det ses i Cockpits journalgränssnitt."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit är inte kompatibelt med programvaran på systemet."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit är inte installerat på systemet."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit är perfekt för nya systemadministratörer, låter dem lätt utföra enkla uppgifter såsom lagringsadministration, inspektion av journaler och att starta och stoppa tjänster. Du kan övervaka och administrera flera servrar på samma gång. Lägg bara till dem med ett enda klick och dina maskiner kommer se efter sina kompisar."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit kanske inte återges korrekt i din webbläsare"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Samla och paketera diagnostik och support data"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Samla kärnkraschdumpar"
+ ],
+ "Compact PCI": [
+ null,
+ "Kompakt PCI"
+ ],
+ "Connect to": [
+ null,
+ "Anslut till"
+ ],
+ "Connect to:": [
+ null,
+ "Anslut till:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Anslutningens tidsgräns överskreds."
+ ],
+ "Convertible": [
+ null,
+ "Konvertibel"
+ ],
+ "Copy": [
+ null,
+ "Kopiera"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Kopiera till urklipp"
+ ],
+ "Create": [
+ null,
+ "Skapa"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Skapa en ny uppgiftsfil med detta innehåll."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Fördröjning"
+ ],
+ "Desktop": [
+ null,
+ "Skrivbord"
+ ],
+ "Detachable": [
+ null,
+ "Frånkopplingsbar"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Diagnostikrapporter"
+ ],
+ "Docking station": [
+ null,
+ "Dockningsstation"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Hämta en ny webbläsare gratis"
+ ],
+ "Downloading $0": [
+ null,
+ "Hämtar $0"
+ ],
+ "Dual rank": [
+ null,
+ "Dubbelrad"
+ ],
+ "Embedded PC": [
+ null,
+ "Inbäddad PC"
+ ],
+ "Excellent password": [
+ null,
+ "Utmärkt lösenord"
+ ],
+ "Expansion chassis": [
+ null,
+ "Expansionschassin"
+ ],
+ "Failed to change password": [
+ null,
+ "Misslyckades att ändra lösenord"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "Misslyckades med att aktivera $0 i firewalld"
+ ],
+ "Go to now": [
+ null,
+ "Gå till nu"
+ ],
+ "Handheld": [
+ null,
+ "Handhållen"
+ ],
+ "Hide confirmation password": [
+ null,
+ "Dölj bekräftelselösenord"
+ ],
+ "Hide password": [
+ null,
+ "Dölj lösenord"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Värdnyckeln är felaktig"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Om fingeravtrycket stämmer, klicka på \"Acceptera nyckel och logga in\". Annars, logga inte in och kontakta din administratör."
+ ],
+ "Install": [
+ null,
+ "Installera"
+ ],
+ "Install software": [
+ null,
+ "Installera programvara"
+ ],
+ "Installing $0": [
+ null,
+ "Installerar $0"
+ ],
+ "Internal error": [
+ null,
+ "Internt fel"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Internt fel: felaktig utmaningshuvud"
+ ],
+ "Invalid date format": [
+ null,
+ "Felaktigt datumformat"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Felaktigt datumformat och felaktigt tidsformat"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Felaktiga filrättigheter"
+ ],
+ "Invalid time format": [
+ null,
+ "Felaktigt tidsformat"
+ ],
+ "Invalid timezone": [
+ null,
+ "Felaktig tidszon"
+ ],
+ "IoT gateway": [
+ null,
+ "IoT-gateway"
+ ],
+ "Kernel dump": [
+ null,
+ "Kärndump"
+ ],
+ "Laptop": [
+ null,
+ "Bärbar dator"
+ ],
+ "Learn more": [
+ null,
+ "Mer information"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Läser in anpassningar till systemet..."
+ ],
+ "Log in": [
+ null,
+ "Logga in"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Logga in med ditt serveranvändarkonto."
+ ],
+ "Log messages": [
+ null,
+ "Loggmeddelanden"
+ ],
+ "Login": [
+ null,
+ "Inloggning"
+ ],
+ "Login again": [
+ null,
+ "Logga in igen"
+ ],
+ "Login failed": [
+ null,
+ "Inloggningen misslyckades"
+ ],
+ "Logout successful": [
+ null,
+ "Utloggningen lyckades"
+ ],
+ "Low profile desktop": [
+ null,
+ "Lågprofilskrivbord"
+ ],
+ "Lunch box": [
+ null,
+ "Lunchlåda"
+ ],
+ "Main server chassis": [
+ null,
+ "Huvudserverchassi"
+ ],
+ "Manage storage": [
+ null,
+ "Hantera lagring"
+ ],
+ "Manually": [
+ null,
+ "Manuellt"
+ ],
+ "Message to logged in users": [
+ null,
+ "Meddelande till inloggade användare"
+ ],
+ "Mini PC": [
+ null,
+ "Mini-PC"
+ ],
+ "Mini tower": [
+ null,
+ "Minitorn"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Multisystemschassi"
+ ],
+ "NTP server": [
+ null,
+ "NTP-server"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Behöver åtminstone en NTP-server"
+ ],
+ "Networking": [
+ null,
+ "Nätverk"
+ ],
+ "New host": [
+ null,
+ "Ny värd"
+ ],
+ "New password was not accepted": [
+ null,
+ "Det nya lösenordet godtogs inte"
+ ],
+ "No delay": [
+ null,
+ "Ingen fördröjning"
+ ],
+ "No such file or directory": [
+ null,
+ "Filen eller katalogen finns inte"
+ ],
+ "No system modifications": [
+ null,
+ "Inga systemändringar"
+ ],
+ "Not a valid private key": [
+ null,
+ "Inte en giltig privat nyckel"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Inte tillåtet att utföra denna åtgärd."
+ ],
+ "Not synchronized": [
+ null,
+ "Inte synkroniserad"
+ ],
+ "Notebook": [
+ null,
+ "Bärbar (notebook)"
+ ],
+ "Occurrences": [
+ null,
+ "Förekomster"
+ ],
+ "Ok": [
+ null,
+ "Ok"
+ ],
+ "Old password not accepted": [
+ null,
+ "Det gamla lösenordet accepterades inte"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "När Cockpit är installerat, aktivera det med ”systemctl enable --now cockpit.socket”."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Eller använd en medpackad bläddrare"
+ ],
+ "Other": [
+ null,
+ "Annan"
+ ],
+ "Other options": [
+ null,
+ "Andra alternativ"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit kraschade"
+ ],
+ "Password": [
+ null,
+ "Lösenord"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Lösenordet är inte godtagbart"
+ ],
+ "Password is too weak": [
+ null,
+ "Lösenordet är för svagt"
+ ],
+ "Password not accepted": [
+ null,
+ "Lösenordet accepterades inte"
+ ],
+ "Paste": [
+ null,
+ "Klistra in"
+ ],
+ "Paste error": [
+ null,
+ "Klistra in fel"
+ ],
+ "Path to file": [
+ null,
+ "Sökväg till filen"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Periferichassi"
+ ],
+ "Permission denied": [
+ null,
+ "Åtkomst nekas"
+ ],
+ "Pick date": [
+ null,
+ "Välj datum"
+ ],
+ "Pizza box": [
+ null,
+ "Pizzalåda"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Aktivera JavaScript för att använda webbkonsolen."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Ange värden att ansluta till"
+ ],
+ "Portable": [
+ null,
+ "Bärbar"
+ ],
+ "Present": [
+ null,
+ "Närvarande"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Tidsgränsen överskreds vid fråga via ssh-add"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Tidsgränsen överskreds vid fråga via ssh-keygen"
+ ],
+ "RAID chassis": [
+ null,
+ "RAID-chassi"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Rackmonteringschassi"
+ ],
+ "Reboot": [
+ null,
+ "Starta om"
+ ],
+ "Recent hosts": [
+ null,
+ "Senaste värdar"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Vägrar att ansluta. Värden är okänd"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Vägrar att ansluta. Värdnyckeln stämmer inte"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Vägrar att ansluta. Värdnyckeln är okänd"
+ ],
+ "Removals:": [
+ null,
+ "Borttagningar:"
+ ],
+ "Remove host": [
+ null,
+ "Ta bort värd"
+ ],
+ "Removing $0": [
+ null,
+ "Tar bort $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "PC med slutet hölje"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Säkerhetsförbättrad Linux-konfiguration och felsökning"
+ ],
+ "Server": [
+ null,
+ "Server"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Servern har stängt förbindelsen."
+ ],
+ "Set time": [
+ null,
+ "Ställ in tiden"
+ ],
+ "Shell script": [
+ null,
+ "Skalskript"
+ ],
+ "Shift+Insert": [
+ null,
+ "Skift+Insert"
+ ],
+ "Show confirmation password": [
+ null,
+ "Visa bekräftelselösenord"
+ ],
+ "Show password": [
+ null,
+ "Visa lösenord"
+ ],
+ "Shut down": [
+ null,
+ "Stäng av"
+ ],
+ "Single rank": [
+ null,
+ "Ensam ordning"
+ ],
+ "Space-saving computer": [
+ null,
+ "Utrymmessparande dator"
+ ],
+ "Specific time": [
+ null,
+ "Specifik tid"
+ ],
+ "Stick PC": [
+ null,
+ "Pinndator"
+ ],
+ "Storage": [
+ null,
+ "Lagring"
+ ],
+ "Strong password": [
+ null,
+ "Starkt lösenord"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Under-chassi"
+ ],
+ "Sub-Notebook": [
+ null,
+ "ULPC"
+ ],
+ "Synchronized": [
+ null,
+ "Synkroniserad"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Synkroniserad med $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Synkroniserar"
+ ],
+ "Tablet": [
+ null,
+ "Platta"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "Den inloggade användaren har inte tillåtelse att se systemändringar"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Lösenorden stämmer inte överens."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "Det resulterande fingeravtrycket går bra att dela via offentliga metoder, inklusive e-post."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Servern vägrade att autentisera ”$0” med lösenordsautentisering, och inga andra stödda autentiseringsmetoder är tillgängliga."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Servern vägrade att autentisera med några stödda metoder."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Webbläsarens konfiguration förhindrar Cockpit från att köra (oåtkomlig $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "Det här verktyget konfigurerar SELinux-policyn och kan hjälpa till med att förstå och lösa policy överträdelser."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "Det här verktyget konfigurerar systemet till att kunna skriva kärnkraschdumpar till disken."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "Detta verktyg genererar ett arkiv med konfiguration och diagnostisk information från det körande systemet. Arkivet kan förvaras lokalt eller centralt för inspelning eller spårningsändamål eller kan skickas till tekniska supportrepresentanter, utvecklare eller systemadministratörer för att hjälpa till med teknisk felspårning och felsökning."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "Detta verktyg hanterar lokal lagring, såsom filsystem, LVM2-volymgrupper och NFS-monteringar."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "Detta verktyg hanterar nätverk som bindningar, broar, team, VLAN och brandväggar med NetworkManager och Firewalld. NetworkManager är inkompatibelt med Ubuntus standard systemd-networkd och Debians ifupdown skript."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Den här webbläsaren är för gammal för att köra webbkonsolen ($0 saknas)"
+ ],
+ "Time zone": [
+ null,
+ "Tidszon"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "För att säkerställa att din anslutning inte fångas upp av en skadlig tredje part, vänligen verifiera värdnyckelns fingeravtryck:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "För att verifiera ett fingeravtryck, kör följande på $0 medan du fysiskt sitter vid maskinen eller genom ett pålitligt nätverk:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Växla datumväljare"
+ ],
+ "Too much data": [
+ null,
+ "För mycket data"
+ ],
+ "Total size: $0": [
+ null,
+ "Total storlek: $0"
+ ],
+ "Tower": [
+ null,
+ "Torn"
+ ],
+ "Try again": [
+ null,
+ "Försök igen"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Försök att synkronisera med $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Kan inte ansluta till den adressen"
+ ],
+ "Unknown": [
+ null,
+ "Okänd"
+ ],
+ "Untrusted host": [
+ null,
+ "Ej betrodd värd"
+ ],
+ "User name": [
+ null,
+ "Användarnamn"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Användarnamnet får inte vara tomt"
+ ],
+ "Validating authentication token": [
+ null,
+ "Validerar autentiseringselementet"
+ ],
+ "View all logs": [
+ null,
+ "Visa alla loggar"
+ ],
+ "View automation script": [
+ null,
+ "Visa automatiseringsskript"
+ ],
+ "Visit firewall": [
+ null,
+ "Besök brandvägg"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Väntar på att andra programvaruhanteringsåtgärder skall bli klara"
+ ],
+ "Weak password": [
+ null,
+ "Svagt lösenord"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Webbkonsol för Linuxservrar"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Fel användarnamn eller lösenord"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Du ansluter till $0 för första gången."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "Din webbläsare tillåter inte att klistra in från sammanhangsmenyn. Du kan använda Skift+Insert."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Din session har avslutats."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Din session har gått ut. Logga in igen."
+ ],
+ "Zone": [
+ null,
+ "Zon"
+ ],
+ "[binary data]": [
+ null,
+ "[binärdata]"
+ ],
+ "[no data]": [
+ null,
+ "[inga data]"
+ ],
+ "password quality": [
+ null,
+ "lösenordskvalitet"
+ ],
+ "show less": [
+ null,
+ "visa mindre"
+ ],
+ "show more": [
+ null,
+ "visa mer"
+ ]
+};
diff --git a/dist/static/po.tr.js b/dist/static/po.tr.js
new file mode 100644
index 0000000..592f303
--- /dev/null
+++ b/dist/static/po.tr.js
@@ -0,0 +1,965 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => (n>1),
+ "language": "tr",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 gün",
+ "$0 gün"
+ ],
+ "$0 error": [
+ null,
+ "$0 hatası"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0, $1 koduyla çıkış yaptı"
+ ],
+ "$0 failed": [
+ null,
+ "$0 başarısız oldu"
+ ],
+ "$0 hour": [
+ null,
+ "$0 saat",
+ "$0 saat"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0, hiçbir depoda yok."
+ ],
+ "$0 key changed": [
+ null,
+ "$0 anahtarı değişti"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0, $1 sinyali ile sonlandırıldı"
+ ],
+ "$0 minute": [
+ null,
+ "$0 dakika",
+ "$0 dakika"
+ ],
+ "$0 month": [
+ null,
+ "$0 ay",
+ "$0 ay"
+ ],
+ "$0 week": [
+ null,
+ "$0 hafta",
+ "$0 hafta"
+ ],
+ "$0 will be installed.": [
+ null,
+ "$0 yüklenecektir."
+ ],
+ "$0 year": [
+ null,
+ "$0 yıl",
+ "$0 yıl"
+ ],
+ "1 day": [
+ null,
+ "1 gün"
+ ],
+ "1 hour": [
+ null,
+ "1 saat"
+ ],
+ "1 minute": [
+ null,
+ "1 dakika"
+ ],
+ "1 week": [
+ null,
+ "1 hafta"
+ ],
+ "20 minutes": [
+ null,
+ "20 dakika"
+ ],
+ "40 minutes": [
+ null,
+ "40 dakika"
+ ],
+ "5 minutes": [
+ null,
+ "5 dakika"
+ ],
+ "6 hours": [
+ null,
+ "6 saat"
+ ],
+ "60 minutes": [
+ null,
+ "60 dakika"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Güvenlik, güvenilirlik ve performans için modern bir tarayıcı gereklidir."
+ ],
+ "Absent": [
+ null,
+ "Yok"
+ ],
+ "Accept key and log in": [
+ null,
+ "Anahtarı kabul et ve oturum aç"
+ ],
+ "Acceptable password": [
+ null,
+ "Kabul edilebilir parola"
+ ],
+ "Add $0": [
+ null,
+ "$0 ekle"
+ ],
+ "Additional packages:": [
+ null,
+ "Ek paketler:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Cockpit Web Konsolu ile Yönetim"
+ ],
+ "Advanced TCA": [
+ null,
+ "Gelişmiş TCA"
+ ],
+ "All-in-one": [
+ null,
+ "Hepsi-bir-arada"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Ansible rolleri belgeleri"
+ ],
+ "Authentication failed": [
+ null,
+ "Kimlik doğrulama başarısız oldu"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Kimlik doğrulama başarısız oldu: Sunucu bağlantıyı kapattı"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Cockpit Web Konsolu ile yetkili görevleri gerçekleştirmek için kimlik doğrulaması gerekir"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Otomatik olarak NTP kullanarak"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Otomatik olarak ek NTP sunucularını kullanarak"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Otomatik olarak belirli NTP sunucularını kullanarak"
+ ],
+ "Automation script": [
+ null,
+ "Otomatikleştirme betiği"
+ ],
+ "Blade": [
+ null,
+ "Blade"
+ ],
+ "Blade enclosure": [
+ null,
+ "Blade kasası"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Veri yolu genişletme kasası"
+ ],
+ "Bypass browser check": [
+ null,
+ "Tarayıcı denetimini atla"
+ ],
+ "Cancel": [
+ null,
+ "İptal"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Oturum açma kimlik bilgileri yönlendirilemiyor"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Geçmişteki olay zamanlanamıyor"
+ ],
+ "Change": [
+ null,
+ "Değiştir"
+ ],
+ "Change system time": [
+ null,
+ "Sistem saatini değiştir"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Değiştirilen anahtarlar genellikle bir işletim sisteminin yeniden yüklenmesinin sonucudur. Ancak, beklenmeyen bir değişiklik, üçüncü tarafın bağlantınıza müdahale etme girişimini gösterebilir."
+ ],
+ "Checking installed software": [
+ null,
+ "Yüklü yazılımlar denetleniyor"
+ ],
+ "Close": [
+ null,
+ "Kapat"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Cockpit kimlik doğrulaması yanlış yapılandırılmış."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "NetworkManager ve Firewalld'un Cockpit yapılandırması"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit, verilen anamakineyle iletişim kuramadı."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit, Linux sunucularınızı bir web tarayıcısı aracılığıyla yönetmenizi kolaylaştıran bir sunucu yöneticisidir. Terminal ve web aracı arasında geçiş yapmak sorun değildir. Cockpit aracılığıyla başlatılan bir hizmet terminal aracılığıyla durdurulabilir. Aynı şekilde, terminalde bir hata meydana gelirse, Cockpit günlüğü arayüzünde görülebilir."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit, sistemdeki yazılımla uyumlu değil."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit sistemde yüklü değil."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit yeni sistem yöneticileri için mükemmeldir; depolama yönetimi, günlükleri inceleme, hizmetleri başlatma ve durdurma gibi basit görevleri kolayca gerçekleştirmelerine olanak tanır. Aynı anda birkaç sunucuyu izleyebilir ve yönetebilirsiniz. Bunları tek bir tıklama ile ekleyin ve makineleriniz arkadaşlarıyla ilgilensin."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit tarayıcınızda düzgün görüntülenmeyebilir"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Tanılama ve destek verilerini topla ve paketle"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Çekirdek çökme dökümlerini topla"
+ ],
+ "Compact PCI": [
+ null,
+ "Compact PCI"
+ ],
+ "Connect to": [
+ null,
+ "Şuna bağlan"
+ ],
+ "Connect to:": [
+ null,
+ "Şuna bağlan:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Bağlantı zaman aşımına uğradı."
+ ],
+ "Convertible": [
+ null,
+ "Dönüştürülebilir"
+ ],
+ "Copy": [
+ null,
+ "Kopyala"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Panoya kopyala"
+ ],
+ "Create": [
+ null,
+ "Oluştur"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Bu içerikle yeni görev dosyası oluştur."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Gecikme"
+ ],
+ "Desktop": [
+ null,
+ "Masaüstü"
+ ],
+ "Detachable": [
+ null,
+ "Ayrılabilir"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Tanılama raporları"
+ ],
+ "Docking station": [
+ null,
+ "Kenetleme istasyonu"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Ücretsiz olarak yeni bir tarayıcı indir"
+ ],
+ "Downloading $0": [
+ null,
+ "$0 indiriliyor"
+ ],
+ "Dual rank": [
+ null,
+ "Çift sıra"
+ ],
+ "Embedded PC": [
+ null,
+ "Gömülü PC"
+ ],
+ "Excellent password": [
+ null,
+ "Mükemmel parola"
+ ],
+ "Expansion chassis": [
+ null,
+ "Genişletme kasası"
+ ],
+ "Failed to change password": [
+ null,
+ "Parolayı değiştirme başarısız oldu"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "firewalld içinde $0 etkinleştirme başarısız oldu"
+ ],
+ "Go to now": [
+ null,
+ "Şimdiye git"
+ ],
+ "Handheld": [
+ null,
+ "Elde taşınan"
+ ],
+ "Hide confirmation password": [
+ null,
+ "Onay parolasını gizle"
+ ],
+ "Hide password": [
+ null,
+ "Parolayı gizle"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Anamakine anahtarı yanlış"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Eğer parmak izi eşleşirse, \"Anahtarı kabul et ve oturum aç\"a tıklayın. Aksi takdirde, oturum açmayın ve yöneticinize başvurun."
+ ],
+ "Install": [
+ null,
+ "Yükle"
+ ],
+ "Install software": [
+ null,
+ "Yazılım yükle"
+ ],
+ "Installing $0": [
+ null,
+ "$0 yükleniyor"
+ ],
+ "Internal error": [
+ null,
+ "Dahili hata"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "İç hata: Geçersiz sınama üstbilgisi"
+ ],
+ "Invalid date format": [
+ null,
+ "Geçersiz tarih biçimi"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Geçersiz tarih ve saat biçimi"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Geçersiz dosya izinleri"
+ ],
+ "Invalid time format": [
+ null,
+ "Geçersiz saat biçimi"
+ ],
+ "Invalid timezone": [
+ null,
+ "Geçersiz saat dilimi"
+ ],
+ "IoT gateway": [
+ null,
+ "IoT ağ geçidi"
+ ],
+ "Kernel dump": [
+ null,
+ "Çekirdek dökümü"
+ ],
+ "Laptop": [
+ null,
+ "Dizüstü"
+ ],
+ "Learn more": [
+ null,
+ "Daha fazla bilgi edinin"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Sistem değişiklikleri yükleniyor..."
+ ],
+ "Log in": [
+ null,
+ "Oturum aç"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Sunucu kullanıcı hesabınızla oturum açın."
+ ],
+ "Log messages": [
+ null,
+ "Günlük iletileri"
+ ],
+ "Login": [
+ null,
+ "Oturum aç"
+ ],
+ "Login again": [
+ null,
+ "Tekrar oturum aç"
+ ],
+ "Login failed": [
+ null,
+ "Oturum açma başarısız oldu"
+ ],
+ "Logout successful": [
+ null,
+ "Oturumu kapatma başarılı oldu"
+ ],
+ "Low profile desktop": [
+ null,
+ "Düşük profilli masaüstü"
+ ],
+ "Lunch box": [
+ null,
+ "Lunch box"
+ ],
+ "Main server chassis": [
+ null,
+ "Ana sunucu kasası"
+ ],
+ "Manage storage": [
+ null,
+ "Depolamayı yönet"
+ ],
+ "Manually": [
+ null,
+ "El ile"
+ ],
+ "Message to logged in users": [
+ null,
+ "Oturum açmış kullanıcılar için ileti"
+ ],
+ "Mini PC": [
+ null,
+ "Mini PC"
+ ],
+ "Mini tower": [
+ null,
+ "Mini tower"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Çok sistemli kasa"
+ ],
+ "NTP server": [
+ null,
+ "NTP sunucusu"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "En az bir NTP sunucusu gerekli"
+ ],
+ "Networking": [
+ null,
+ "Ağ"
+ ],
+ "New host": [
+ null,
+ "Yeni anamakine"
+ ],
+ "New password was not accepted": [
+ null,
+ "Yeni parola kabul edilmedi"
+ ],
+ "No delay": [
+ null,
+ "Gecikme yok"
+ ],
+ "No such file or directory": [
+ null,
+ "Böyle bir dosya ya da dizin yok"
+ ],
+ "No system modifications": [
+ null,
+ "Sistem değişiklikleri yok"
+ ],
+ "Not a valid private key": [
+ null,
+ "Geçerli bir özel anahtar değil"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Bu eylemi gerçekleştirmeye izinli değil."
+ ],
+ "Not synchronized": [
+ null,
+ "Eşitlenmedi"
+ ],
+ "Notebook": [
+ null,
+ "Notebook"
+ ],
+ "Occurrences": [
+ null,
+ "Oluşumlar"
+ ],
+ "Ok": [
+ null,
+ "Tamam"
+ ],
+ "Old password not accepted": [
+ null,
+ "Eski parola kabul edilmedi"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Cockpit yüklendikten sonra, \"systemctl enable --now cockpit.socket\" komutuyla etkinleştirin."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Veya paketlenmiş bir tarayıcı kullanın"
+ ],
+ "Other": [
+ null,
+ "Diğer"
+ ],
+ "Other options": [
+ null,
+ "Diğer seçenekler"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit çöktü"
+ ],
+ "Password": [
+ null,
+ "Parola"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Parola kabul edilebilir değil"
+ ],
+ "Password is too weak": [
+ null,
+ "Parola çok zayıf"
+ ],
+ "Password not accepted": [
+ null,
+ "Parola kabul edilmedi"
+ ],
+ "Paste": [
+ null,
+ "Yapıştır"
+ ],
+ "Paste error": [
+ null,
+ "Yapıştırma hatası"
+ ],
+ "Path to file": [
+ null,
+ "Dosyanın yolu"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Çevresel donanım kasası"
+ ],
+ "Permission denied": [
+ null,
+ "İzin reddedildi"
+ ],
+ "Pick date": [
+ null,
+ "Tarih seçin"
+ ],
+ "Pizza box": [
+ null,
+ "Pizza box"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Web Konsolunu kullanmak için lütfen JavaScript'i etkinleştirin."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Lütfen bağlanılacak anamakineyi belirtin"
+ ],
+ "Portable": [
+ null,
+ "Taşınabilir"
+ ],
+ "Present": [
+ null,
+ "Mevcut"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "ssh-add aracılığıyla sorma zaman aşımına uğradı"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "ssh-keygen aracılığıyla sorma zaman aşımına uğradı"
+ ],
+ "RAID chassis": [
+ null,
+ "RAID kasası"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Raf montajlı kasa"
+ ],
+ "Reboot": [
+ null,
+ "Yeniden başlat"
+ ],
+ "Recent hosts": [
+ null,
+ "En son anamakineler"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Bağlanmayı reddediyor. Anamakine bilinmiyor"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Bağlanmayı reddediyor. Anamakine anahtarı eşleşmiyor"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Bağlanmayı reddediyor. Anamakine anahtarı bilinmiyor"
+ ],
+ "Removals:": [
+ null,
+ "Kaldırılanlar:"
+ ],
+ "Remove host": [
+ null,
+ "Anamakineyi kaldır"
+ ],
+ "Removing $0": [
+ null,
+ "$0 kaldırılıyor"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "Mühürlü Kasa PC"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Güvenlik Gelişmiş Linux yapılandırması ve sorun giderme"
+ ],
+ "Server": [
+ null,
+ "Sunucu"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "Sunucu bağlantıyı kapattı."
+ ],
+ "Set time": [
+ null,
+ "Saati ayarla"
+ ],
+ "Shell script": [
+ null,
+ "Kabuk betiği"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Show confirmation password": [
+ null,
+ "Onay parolasını göster"
+ ],
+ "Show password": [
+ null,
+ "Parolayı göster"
+ ],
+ "Shut down": [
+ null,
+ "Kapat"
+ ],
+ "Single rank": [
+ null,
+ "Tek sıra"
+ ],
+ "Space-saving computer": [
+ null,
+ "Yerden kazandıran bilgisayar"
+ ],
+ "Specific time": [
+ null,
+ "Belirli bir zaman"
+ ],
+ "Stick PC": [
+ null,
+ "Çubuk PC"
+ ],
+ "Storage": [
+ null,
+ "Depolama"
+ ],
+ "Strong password": [
+ null,
+ "Güçlü parola"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Alt Kasa"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Alt Dizüstü"
+ ],
+ "Synchronized": [
+ null,
+ "Eşitlendi"
+ ],
+ "Synchronized with $0": [
+ null,
+ "$0 ile eşitlendi"
+ ],
+ "Synchronizing": [
+ null,
+ "Eşitleniyor"
+ ],
+ "Tablet": [
+ null,
+ "Tablet"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "Oturum açmış kullanıcının sistem değişikliklerini görüntülemesine izin verilmiyor"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Parolalar eşleşmiyor."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "Ortaya çıkan parmak izinin, e-posta dahil olmak üzere herkese açık yöntemlerle paylaşılması uygundur."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Sunucu, parola kimlik doğrulamasını kullanarak '$0' kullanıcısının kimliğini doğrulamayı reddetti ve kullanılabilir başka kimlik doğrulama yöntemi yok."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Sunucu, desteklenen herhangi bir yöntemi kullanarak kimlik doğrulamayı reddetti."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Web tarayıcısı yapılandırması Cockpit'in çalışmasını engelliyor (erişilemeyen $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "Bu araç, SELinux ilkesini yapılandırır ve ilke ihlallerinin anlaşılmasına ve çözülmesine yardımcı olabilir."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "Bu araç, sistemi çekirdek çökme dökümlerini diske yazacak şekilde yapılandırır."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "Bu araç, çalışan sistemden bir yapılandırma ve tanılama bilgileri arşivi oluşturur. Arşiv, kayıt veya izleme amacıyla yerel veya merkezi olarak depolanabilir veya teknik hata bulma ve hata ayıklamaya yardımcı olması için teknik destek temsilcilerine, geliştiricilere veya sistem yöneticilerine gönderilebilir."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "Bu araç, dosya sistemleri, LVM2 birim grupları ve NFS bağlamaları gibi yerel depolamayı yönetir."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "Bu araç, NetworkManager ve Firewalld kullanarak birleştirmeler, köprüler, takımlar, VLAN'lar ve güvenlik duvarları gibi ağları yönetir. NetworkManager, Ubuntu'nun varsayılan systemd-networkd ve Debian'ın ifupdown betikleriyle uyumsuzdur."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Bu web tarayıcısı, Web Konsolunu çalıştırmak için çok eski ($0 eksik)"
+ ],
+ "Time zone": [
+ null,
+ "Saat dilimi"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Bağlantınızın kötü niyetli bir üçüncü tarafça engellenmediğinden emin olmak için lütfen anamakine anahtar parmak izini doğrulayın:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Bir parmak izini doğrulamak için makinede fiziksel olarak bulunurken veya güvenilir bir ağ aracılığıyla 0$ üzerinde aşağıdakileri çalıştırın:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Tarihi seçiciyi aç/kapat"
+ ],
+ "Too much data": [
+ null,
+ "Çok fazla veri"
+ ],
+ "Total size: $0": [
+ null,
+ "Toplam boyut: $0"
+ ],
+ "Tower": [
+ null,
+ "Tower"
+ ],
+ "Try again": [
+ null,
+ "Tekrar dene"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "$0 ile eşitlemeye çalışılıyor"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Bu adrese bağlanılamıyor"
+ ],
+ "Unknown": [
+ null,
+ "Bilinmiyor"
+ ],
+ "Untrusted host": [
+ null,
+ "Güvenilmeyen anamakine"
+ ],
+ "User name": [
+ null,
+ "Kullanıcı adı"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Kullanıcı adı boş olamaz"
+ ],
+ "Validating authentication token": [
+ null,
+ "Kimlik doğrulama belirteci doğrulanıyor"
+ ],
+ "View all logs": [
+ null,
+ "Tüm günlükleri görüntüle"
+ ],
+ "View automation script": [
+ null,
+ "Otomatikleştirme betiğini görüntüle"
+ ],
+ "Visit firewall": [
+ null,
+ "Güvenlik duvarını ziyaret et"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Diğer yazılım yönetimi işlemlerinin bitmesi bekleniyor"
+ ],
+ "Weak password": [
+ null,
+ "Zayıf parola"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Linux sunucuları için Web Konsolu"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Kullanıcı adı veya parola yanlış"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "İlk kez $0 için bağlanıyorsunuz."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "Tarayıcınız, bağlam menüsünden yapıştırmaya izin vermiyor. Shift+Insert kullanabilirsiniz."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Oturumunuz sonlandırıldı."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Oturumunuzun süresi doldu. Lütfen tekrar oturum açın."
+ ],
+ "Zone": [
+ null,
+ "Bölge"
+ ],
+ "[binary data]": [
+ null,
+ "[ikili veri]"
+ ],
+ "[no data]": [
+ null,
+ "[veri yok]"
+ ],
+ "password quality": [
+ null,
+ "parola kalitesi"
+ ],
+ "show less": [
+ null,
+ "daha az göster"
+ ],
+ "show more": [
+ null,
+ "daha fazla göster"
+ ]
+};
diff --git a/dist/static/po.uk.js b/dist/static/po.uk.js
new file mode 100644
index 0000000..5647c70
--- /dev/null
+++ b/dist/static/po.uk.js
@@ -0,0 +1,967 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2,
+ "language": "uk",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 ГіБ"
+ ],
+ "$0 day": [
+ null,
+ "$0 день",
+ "$0 дні",
+ "$0 днів"
+ ],
+ "$0 error": [
+ null,
+ "Помилка $0"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "$0 завершено роботу з кодом $1"
+ ],
+ "$0 failed": [
+ null,
+ "Помилка $0"
+ ],
+ "$0 hour": [
+ null,
+ "$0 година",
+ "$0 години",
+ "$0 годин"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "$0 немає у жодному зі сховищ."
+ ],
+ "$0 key changed": [
+ null,
+ "Змінено ключ $0"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "$0 завершено з сигналом $1"
+ ],
+ "$0 minute": [
+ null,
+ "$0 хвилина",
+ "$0 хвилини",
+ "$0 хвилин"
+ ],
+ "$0 month": [
+ null,
+ "$0 місяць",
+ "$0 місяці",
+ "$0 місяців"
+ ],
+ "$0 week": [
+ null,
+ "$0 тиждень",
+ "$0 тижні",
+ "$0 тижнів"
+ ],
+ "$0 will be installed.": [
+ null,
+ "Буде встановлено $0."
+ ],
+ "$0 year": [
+ null,
+ "$0 рік",
+ "$0 роки",
+ "$0 років"
+ ],
+ "1 day": [
+ null,
+ "1 день"
+ ],
+ "1 hour": [
+ null,
+ "1 година"
+ ],
+ "1 minute": [
+ null,
+ "1 хвилина"
+ ],
+ "1 week": [
+ null,
+ "1 тиждень"
+ ],
+ "20 minutes": [
+ null,
+ "20 хвилин"
+ ],
+ "40 minutes": [
+ null,
+ "40 хвилин"
+ ],
+ "5 minutes": [
+ null,
+ "5 хвилин"
+ ],
+ "6 hours": [
+ null,
+ "6 годин"
+ ],
+ "60 minutes": [
+ null,
+ "60 хвилин"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "Для забезпечення безпеки, надійної роботи та швидкодії слід встановити сучасну програму для перегляду інтернету."
+ ],
+ "Absent": [
+ null,
+ "Відсутній"
+ ],
+ "Accept key and log in": [
+ null,
+ "Прийняти ключ і увійти"
+ ],
+ "Acceptable password": [
+ null,
+ "Прийнятний пароль"
+ ],
+ "Add $0": [
+ null,
+ "Додати $0"
+ ],
+ "Additional packages:": [
+ null,
+ "Додаткові пакунки:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "Адміністрування за допомогою вебконсолі Cockpit"
+ ],
+ "Advanced TCA": [
+ null,
+ "Розширене TCA"
+ ],
+ "All-in-one": [
+ null,
+ "Усе в одному"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Документація з ролей Ansible"
+ ],
+ "Authentication failed": [
+ null,
+ "Не вдалось пройти розпізнавання"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "Не вдалося пройти розпізнавання: з’єднання розірвано сервером"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "Щоб отримати доступ до виконання привілейованих завдань за допомогою вебконсолі Cockpit, слід пройти розпізнавання"
+ ],
+ "Automatically using NTP": [
+ null,
+ "Автоматично на основі NTP"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "Автоматично за допомогою додаткових серверів NTP"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "Автоматично за допомогою певних серверів NTP"
+ ],
+ "Automation script": [
+ null,
+ "Скрипт автоматизації"
+ ],
+ "Blade": [
+ null,
+ "Blade"
+ ],
+ "Blade enclosure": [
+ null,
+ "Обгортка Blade"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "Апаратний блок розширення каналу"
+ ],
+ "Cancel": [
+ null,
+ "Скасувати"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "Не вдалося переспрямувати реєстраційні дані для входу"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "Не можна планувати подію на минуле"
+ ],
+ "Change": [
+ null,
+ "Змінити"
+ ],
+ "Change system time": [
+ null,
+ "Змінити системний час"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "Зміна ключів часто є результатом перевстановлення операційної системи. Втім, неочікувана зміна може вказувати на сторонню спробу перехопити дані вашого з'єднання."
+ ],
+ "Checking installed software": [
+ null,
+ "Перевіряємо встановлене програмне забезпечення"
+ ],
+ "Close": [
+ null,
+ "Закрити"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Розпізнавання у Cockpit налаштовано з помилками."
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "Налаштування Cockpit для NetworkManager і Firewalld"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit не вдалося встановити зв’язок із вказаним вузлом."
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit — програма для керування сервером, яка полегшує адміністрування ваших серверів під керуванням Linux за допомогою програми для перегляду сторінок інтернету. Ви зможете одночасно використовувати термінал і вебінструмент. Службу, яку було запущено за допомогою Cockpit, можна зупинити за допомогою термінала. І навпаки, якщо трапиться помилка у терміналі, ви побачите її у інтерфейсі журналу Cockpit."
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit є несумісним із програмним забезпеченням цієї системи."
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit у цій системі не встановлено."
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit — чудовий інструмент для системних адміністраторів-початківців. За його допомогою вони без проблем впораються із простими завданнями, зокрема адмініструванням сховищ даних, інспектуванням журналів та запуском і зупиненням служб. Ви зможете одночасно стежити за роботою декількох серверів і адмініструвати ці сервери. Просто додайте їх одним клацанням кнопкою миші і ваш комп’ютер сам нагляне за своїми приятелями."
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Можливо, Cockpit не буде показано належним чином у вашому браузері"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "Збирати і пакувати діагностичні дані і дані щодо підтримки"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "Збирати дампи аварій ядра"
+ ],
+ "Compact PCI": [
+ null,
+ "Компактний PCI"
+ ],
+ "Connect to": [
+ null,
+ "З’єднатися з"
+ ],
+ "Connect to:": [
+ null,
+ "З’єднатися з:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "Вичерпано час очікування на з’єднання."
+ ],
+ "Convertible": [
+ null,
+ "Змінюваний"
+ ],
+ "Copy": [
+ null,
+ "Копіювати"
+ ],
+ "Copy to clipboard": [
+ null,
+ "Копіювати до буфера"
+ ],
+ "Create": [
+ null,
+ "Створити"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "Створити файл завдання із цим вмістом."
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "Затримка"
+ ],
+ "Desktop": [
+ null,
+ "Робоча станція"
+ ],
+ "Detachable": [
+ null,
+ "Змінний"
+ ],
+ "Diagnostic reports": [
+ null,
+ "Діагностичні звіти"
+ ],
+ "Docking station": [
+ null,
+ "Станція заряджання"
+ ],
+ "Download a new browser for free": [
+ null,
+ "Отримайте новий браузер безкоштовно"
+ ],
+ "Downloading $0": [
+ null,
+ "Отримуємо $0"
+ ],
+ "Dual rank": [
+ null,
+ "Подвійний ранг"
+ ],
+ "Embedded PC": [
+ null,
+ "Вбудований ПК"
+ ],
+ "Excellent password": [
+ null,
+ "Чудовий пароль"
+ ],
+ "Expansion chassis": [
+ null,
+ "Апаратний блок розширення"
+ ],
+ "Failed to change password": [
+ null,
+ "Не вдалося змінити пароль"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "Не вдалося увімкнути $0 у firewalld"
+ ],
+ "Go to now": [
+ null,
+ "Перейти зараз"
+ ],
+ "Handheld": [
+ null,
+ "Кишеньковий пристрій"
+ ],
+ "Hide confirmation password": [
+ null,
+ "Приховати підтвердження пароля"
+ ],
+ "Hide password": [
+ null,
+ "Приховати пароль"
+ ],
+ "Host key is incorrect": [
+ null,
+ "Ключ вузла є неправильним"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "Якщо відбиток є відповідним, натисніть «Прийняти ключ і увійти». Якщо ж це не так, не входьте і повідомте про подію адміністратору."
+ ],
+ "Install": [
+ null,
+ "Встановити"
+ ],
+ "Install software": [
+ null,
+ "Встановити програмне забезпечення"
+ ],
+ "Installing $0": [
+ null,
+ "Встановлюємо $0"
+ ],
+ "Internal error": [
+ null,
+ "Внутрішня помилка"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "Внутрішня помилка: некоректний заголовок виклику"
+ ],
+ "Invalid date format": [
+ null,
+ "Некоректний формат дати"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "Некоректний формат дати і часу"
+ ],
+ "Invalid file permissions": [
+ null,
+ "Некоректні права доступу до файла"
+ ],
+ "Invalid time format": [
+ null,
+ "Некоректний формат визначення часу"
+ ],
+ "Invalid timezone": [
+ null,
+ "Некоректний часовий пояс"
+ ],
+ "IoT gateway": [
+ null,
+ "Шлюз IoT"
+ ],
+ "Kernel dump": [
+ null,
+ "Дамп ядра"
+ ],
+ "Laptop": [
+ null,
+ "Переносний ПК"
+ ],
+ "Learn more": [
+ null,
+ "Докладніше"
+ ],
+ "Loading system modifications...": [
+ null,
+ "Завантажуємо модифікації системи…"
+ ],
+ "Log in": [
+ null,
+ "Увійти"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "Увійти на основі даних вашого облікового запису користувача сервера."
+ ],
+ "Log messages": [
+ null,
+ "Повідомлення журналу"
+ ],
+ "Login": [
+ null,
+ "Вхід"
+ ],
+ "Login again": [
+ null,
+ "Користувач ще раз"
+ ],
+ "Login failed": [
+ null,
+ "Невдала спроба увійти"
+ ],
+ "Logout successful": [
+ null,
+ "Успішний вихід"
+ ],
+ "Low profile desktop": [
+ null,
+ "Низькопрофільна робоча станція"
+ ],
+ "Lunch box": [
+ null,
+ "Пусковий комп'ютер"
+ ],
+ "Main server chassis": [
+ null,
+ "Апаратний блок основного сервера"
+ ],
+ "Manage storage": [
+ null,
+ "Керування сховищем"
+ ],
+ "Manually": [
+ null,
+ "Вручну"
+ ],
+ "Message to logged in users": [
+ null,
+ "Повідомлення користувачам, які увійшли"
+ ],
+ "Mini PC": [
+ null,
+ "Міні-ПК"
+ ],
+ "Mini tower": [
+ null,
+ "Міні-башточка"
+ ],
+ "Multi-system chassis": [
+ null,
+ "Багатосистемний апаратний блок"
+ ],
+ "NTP server": [
+ null,
+ "Сервер NTP"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "Потрібен принаймні один сервер NTP"
+ ],
+ "Networking": [
+ null,
+ "Робота у мережі"
+ ],
+ "New host": [
+ null,
+ "Новий вузол"
+ ],
+ "New password was not accepted": [
+ null,
+ "Новий пароль не прийнято"
+ ],
+ "No delay": [
+ null,
+ "Без затримки"
+ ],
+ "No such file or directory": [
+ null,
+ "Немає такого файла або каталогу"
+ ],
+ "No system modifications": [
+ null,
+ "Немає модифікацій системи"
+ ],
+ "Not a valid private key": [
+ null,
+ "Некоректний закритий ключ"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "Немає дозволу на виконання цієї дії."
+ ],
+ "Not synchronized": [
+ null,
+ "Не синхронізовано"
+ ],
+ "Notebook": [
+ null,
+ "Ноутбук"
+ ],
+ "Occurrences": [
+ null,
+ "Випадки"
+ ],
+ "Ok": [
+ null,
+ "Гаразд"
+ ],
+ "Old password not accepted": [
+ null,
+ "Старий пароль не прийнято"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "Після встановлення Cockpit його можна увімкнути за допомогою команди «systemctl enable --now cockpit.socket»."
+ ],
+ "Or use a bundled browser": [
+ null,
+ "Або скористайтеся комплектним браузером"
+ ],
+ "Other": [
+ null,
+ "Інше"
+ ],
+ "Other options": [
+ null,
+ "Інші параметри"
+ ],
+ "PackageKit crashed": [
+ null,
+ "Аварійне завершення роботи PackageKit"
+ ],
+ "Password": [
+ null,
+ "Пароль"
+ ],
+ "Password is not acceptable": [
+ null,
+ "Пароль є неприйнятним"
+ ],
+ "Password is too weak": [
+ null,
+ "Пароль є надто простим"
+ ],
+ "Password not accepted": [
+ null,
+ "Пароль не прийнято"
+ ],
+ "Paste": [
+ null,
+ "Вставити"
+ ],
+ "Paste error": [
+ null,
+ "Помилка вставлення"
+ ],
+ "Path to file": [
+ null,
+ "Шлях до файла"
+ ],
+ "Peripheral chassis": [
+ null,
+ "Периферійний апаратний блок"
+ ],
+ "Permission denied": [
+ null,
+ "Доступ заборонено"
+ ],
+ "Pick date": [
+ null,
+ "Вибрати дату"
+ ],
+ "Pizza box": [
+ null,
+ "З коробку для піци"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "Щоб мати змогу користуватися вебконсоллю, будь ласка, увімкніть JavaScript."
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "Будь ласка, вкажіть вузол, з яким слід встановити з'єднання"
+ ],
+ "Portable": [
+ null,
+ "Портативний"
+ ],
+ "Present": [
+ null,
+ "Поточна"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "Час очікування відповіді на запит за допомогою ssh-add вичерпано"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "Час очікування відповіді на запит за допомогою ssh-keygen вичерпано"
+ ],
+ "RAID chassis": [
+ null,
+ "Апаратний блок RAID"
+ ],
+ "Rack mount chassis": [
+ null,
+ "Апаратний блок монтування стійок"
+ ],
+ "Reboot": [
+ null,
+ "Перезавантажити"
+ ],
+ "Recent hosts": [
+ null,
+ "Нещодавні вузли"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "Відмовляємо у з’єднанні. Невідомий вузол."
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "Відмовляємо у з’єднанні. Ключі вузла не збігаються."
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "Відмовляємо у з’єднанні. Невідомий ключ вузла."
+ ],
+ "Removals:": [
+ null,
+ "Вилучення:"
+ ],
+ "Remove host": [
+ null,
+ "Вилучити вузол"
+ ],
+ "Removing $0": [
+ null,
+ "Вилучаємо $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "ПК з опломбованим корпусом"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Налаштування Security Enhanced Linux та усування вад"
+ ],
+ "Server": [
+ null,
+ "Сервер"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "З’єднання розірвано сервером."
+ ],
+ "Set time": [
+ null,
+ "Встановити час"
+ ],
+ "Shell script": [
+ null,
+ "Скрипт оболонки"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Show confirmation password": [
+ null,
+ "Показати підтвердження пароля"
+ ],
+ "Show password": [
+ null,
+ "Показати пароль"
+ ],
+ "Shut down": [
+ null,
+ "Вимкнути"
+ ],
+ "Single rank": [
+ null,
+ "Єдиний ранг"
+ ],
+ "Space-saving computer": [
+ null,
+ "Компактний комп'ютер"
+ ],
+ "Specific time": [
+ null,
+ "У визначений час"
+ ],
+ "Stick PC": [
+ null,
+ "Паличковий ПК"
+ ],
+ "Storage": [
+ null,
+ "Сховище даних"
+ ],
+ "Strong password": [
+ null,
+ "Складний пароль"
+ ],
+ "Sub-Chassis": [
+ null,
+ "Підблок"
+ ],
+ "Sub-Notebook": [
+ null,
+ "Підноутбук"
+ ],
+ "Synchronized": [
+ null,
+ "Синхронізовано"
+ ],
+ "Synchronized with $0": [
+ null,
+ "Синхронізовано із $0"
+ ],
+ "Synchronizing": [
+ null,
+ "Синхронізація"
+ ],
+ "Tablet": [
+ null,
+ "Планшет"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "Користувач, який увійшов до системи, не має права переглядати модифікації системи"
+ ],
+ "The passwords do not match.": [
+ null,
+ "Паролі не збігаються."
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "Відбиток-результат можна поширювати у спосіб із загальним доступом, зокрема електронною поштою."
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "Сервером відмовлено у розпізнаванні «$0» за допомогою пароля. Інших підтримуваних способів розпізнавання не передбачено."
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "Сервер відмовився розпізнавати користувача за допомогою будь-якого з підтримуваних методів."
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "Налаштування програми для перегляду інтернету забороняють запуск Cockpit (недоступна можливість $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "Цей інструмент налаштовує правила SELinux і може допомогти зрозуміти та усунути порушення правил."
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "Цей інструмент налаштовує систему на запис дампів аварій ядра на диск."
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "Цей інструмент створюдє архів даних щодо налаштувань та діагностики для запущеної системи. Архів може бути збережено локально або централізовано з метою журналювання або стеження або надіслано до представників технічної підтримки, розробників або адміністраторів системи, щоб допомогти з пошуком технічних проблем та діагностикою."
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "Цей інструмент керує локальним сховищем даних, зокрема файловими системами, групами томів LVM2 та монтуваннями NFS."
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "Цей інструмент керує можливостями роботи у мережі, зокрема зв'язками, містками, командами, віртуальними LAN та брандмауерами, за допомогою NetworkManager і Firewalld. NetworkManager є несумісним із типовим для Ubuntu systemd-networkd та скриптами ifupdown Debian."
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "Ця програма для перегляду інтернету є надто старою для роботи з вебконсоллю (не вистачає можливості $0)"
+ ],
+ "Time zone": [
+ null,
+ "Часовий пояс"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "Щоб переконатися, що дані вашого з'єднання не буде перехоплено зловмисниками, будь ласка, підтвердьте відбиток ключа вузла:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "Щоб перевірити відбиток, віддайте вказану нижче команду для $0 під час безпосередньої роботи на комп'ютері або з використанням надійної мережі:"
+ ],
+ "Toggle date picker": [
+ null,
+ "Перемкнути засіб вибору дати"
+ ],
+ "Too much data": [
+ null,
+ "Забагато даних"
+ ],
+ "Total size: $0": [
+ null,
+ "Загальний розмір: $0"
+ ],
+ "Tower": [
+ null,
+ "Башточка"
+ ],
+ "Try again": [
+ null,
+ "Спробувати ще раз"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "Намагаємося синхронізуватися з $0"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "Не вдалося встановити з’єднання із цією адресою"
+ ],
+ "Unknown": [
+ null,
+ "Невідомий"
+ ],
+ "Untrusted host": [
+ null,
+ "Ненадійний вузол"
+ ],
+ "User name": [
+ null,
+ "Ім'я користувача"
+ ],
+ "User name cannot be empty": [
+ null,
+ "Ім’я користувача не може бути порожнім"
+ ],
+ "Validating authentication token": [
+ null,
+ "Перевіряємо ключ розпізнавання"
+ ],
+ "View all logs": [
+ null,
+ "Переглянути усі журнали"
+ ],
+ "View automation script": [
+ null,
+ "Переглянути скрипт автоматизації"
+ ],
+ "Visit firewall": [
+ null,
+ "Відвідати брандмауер"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "Очікуємо на завершення інших дій із програмним забезпеченням"
+ ],
+ "Weak password": [
+ null,
+ "Простий пароль"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Вебконсоль для серверів під керуванням Linux"
+ ],
+ "Wrong user name or password": [
+ null,
+ "Помилкове ім’я користувача чи пароль"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "Ви вперше встановлюєте з'єднання із $0."
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "У вашій програмі для перегляду не передбачено можливості вставлення з контекстного меню. Ви можете скористатися для вставлення комбінацією Shift+Insert."
+ ],
+ "Your session has been terminated.": [
+ null,
+ "Ваш сеанс перервано."
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "Строк роботи у вашому сеансі вичерпано. Будь ласка, увійдіть до системи ще раз."
+ ],
+ "Zone": [
+ null,
+ "Зона"
+ ],
+ "[binary data]": [
+ null,
+ "[двійкові дані]"
+ ],
+ "[no data]": [
+ null,
+ "[немає даних]"
+ ],
+ "password quality": [
+ null,
+ "якість пароля"
+ ],
+ "show less": [
+ null,
+ "показати менше"
+ ],
+ "show more": [
+ null,
+ "показати більше"
+ ]
+};
diff --git a/dist/static/po.zh_CN.js b/dist/static/po.zh_CN.js
new file mode 100644
index 0000000..0b84360
--- /dev/null
+++ b/dist/static/po.zh_CN.js
@@ -0,0 +1,959 @@
+window.cockpit_po = {
+ "": {
+ "plural-forms": (n) => 0,
+ "language": "zh_CN",
+ "language-direction": "ltr"
+ },
+ "$0 GiB": [
+ null,
+ "$0 GiB"
+ ],
+ "$0 day": [
+ null,
+ "$0 天"
+ ],
+ "$0 error": [
+ null,
+ "$0 个错误"
+ ],
+ "$0 exited with code $1": [
+ null,
+ "进程 $0 已退出,返回码为 $1"
+ ],
+ "$0 failed": [
+ null,
+ "进程 $0 运行时出错"
+ ],
+ "$0 hour": [
+ null,
+ "$0 小时"
+ ],
+ "$0 is not available from any repository.": [
+ null,
+ "没有提供 $0 组件的仓库。"
+ ],
+ "$0 key changed": [
+ null,
+ "已更改 $0 个密钥"
+ ],
+ "$0 killed with signal $1": [
+ null,
+ "进程 $0 被信号 $1 终止"
+ ],
+ "$0 minute": [
+ null,
+ "$0 分钟"
+ ],
+ "$0 month": [
+ null,
+ "$0 月"
+ ],
+ "$0 week": [
+ null,
+ "$0 周"
+ ],
+ "$0 will be installed.": [
+ null,
+ "即将安装 $0。"
+ ],
+ "$0 year": [
+ null,
+ "$0 年"
+ ],
+ "1 day": [
+ null,
+ "1 天"
+ ],
+ "1 hour": [
+ null,
+ "1 小时"
+ ],
+ "1 minute": [
+ null,
+ "1 分钟"
+ ],
+ "1 week": [
+ null,
+ "1 周"
+ ],
+ "20 minutes": [
+ null,
+ "20 分钟"
+ ],
+ "40 minutes": [
+ null,
+ "40 分钟"
+ ],
+ "5 minutes": [
+ null,
+ "5 分钟"
+ ],
+ "6 hours": [
+ null,
+ "6 小时"
+ ],
+ "60 minutes": [
+ null,
+ "60 分钟"
+ ],
+ "A modern browser is required for security, reliability, and performance.": [
+ null,
+ "请更新浏览器以确保安全性、可靠性和性能。"
+ ],
+ "Absent": [
+ null,
+ "空缺"
+ ],
+ "Accept key and log in": [
+ null,
+ "接受密钥并登录"
+ ],
+ "Acceptable password": [
+ null,
+ "可行的密码"
+ ],
+ "Add $0": [
+ null,
+ "添加 $0"
+ ],
+ "Additional packages:": [
+ null,
+ "额外软件包:"
+ ],
+ "Administration with Cockpit Web Console": [
+ null,
+ "使用 Cockpit 网页控制台管理系统"
+ ],
+ "Advanced TCA": [
+ null,
+ "高级 TCA"
+ ],
+ "All-in-one": [
+ null,
+ "一体机"
+ ],
+ "Ansible": [
+ null,
+ "Ansible"
+ ],
+ "Ansible roles documentation": [
+ null,
+ "Ansible 角色文档"
+ ],
+ "Authentication failed": [
+ null,
+ "认证失败"
+ ],
+ "Authentication failed: Server closed connection": [
+ null,
+ "认证失败:服务端已关闭连接"
+ ],
+ "Authentication is required to perform privileged tasks with the Cockpit Web Console": [
+ null,
+ "通过Cockpit Web Console进行验证后才能执行特权操作"
+ ],
+ "Automatically using NTP": [
+ null,
+ "自动使用 NTP"
+ ],
+ "Automatically using additional NTP servers": [
+ null,
+ "自动使用额外的 NTP 服务器"
+ ],
+ "Automatically using specific NTP servers": [
+ null,
+ "自动使用指定的 NTP 服务器"
+ ],
+ "Automation script": [
+ null,
+ "自动化脚本"
+ ],
+ "Blade": [
+ null,
+ "刀片"
+ ],
+ "Blade enclosure": [
+ null,
+ "刀片机箱"
+ ],
+ "Bus expansion chassis": [
+ null,
+ "总线扩展机箱"
+ ],
+ "Bypass browser check": [
+ null,
+ "禁用浏览器检查"
+ ],
+ "Cancel": [
+ null,
+ "取消"
+ ],
+ "Cannot forward login credentials": [
+ null,
+ "无法转发登录凭证"
+ ],
+ "Cannot schedule event in the past": [
+ null,
+ "无法调度以前的事件"
+ ],
+ "Change": [
+ null,
+ "变更"
+ ],
+ "Change system time": [
+ null,
+ "修改系统时间"
+ ],
+ "Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.": [
+ null,
+ "密钥的变化通常是操作系统重新安装的结果。但是,意料外的变化可能代表有第三方尝试截获您的连接。"
+ ],
+ "Checking installed software": [
+ null,
+ "检查安装的软件"
+ ],
+ "Close": [
+ null,
+ "关闭"
+ ],
+ "Cockpit": [
+ null,
+ "Cockpit"
+ ],
+ "Cockpit authentication is configured incorrectly.": [
+ null,
+ "Cockpit 验证的配置不正确。"
+ ],
+ "Cockpit configuration of NetworkManager and Firewalld": [
+ null,
+ "NetworkManager 的 Cockpit 配置和防火墙"
+ ],
+ "Cockpit could not contact the given host.": [
+ null,
+ "Cockpit 无法联系指定的主机。"
+ ],
+ "Cockpit is a server manager that makes it easy to administer your Linux servers via a web browser. Jumping between the terminal and the web tool is no problem. A service started via Cockpit can be stopped via the terminal. Likewise, if an error occurs in the terminal, it can be seen in the Cockpit journal interface.": [
+ null,
+ "Cockpit 是一个服务器管理工具,可以方便地通过浏览器来管理您的 Linux 服务器。在终端和 web 工具间自由切换将不是问题。通过 Cockpit 启动的服务可以通过终端停止。同样,如果在终端中发生错误, 也可以在 Cockpit 的日志接口中看到。"
+ ],
+ "Cockpit is not compatible with the software on the system.": [
+ null,
+ "Cockpit 与系统上的软件不兼容。"
+ ],
+ "Cockpit is not installed on the system.": [
+ null,
+ "Cockpit 未安装在系统上。"
+ ],
+ "Cockpit is perfect for new sysadmins, allowing them to easily perform simple tasks such as storage administration, inspecting journals and starting and stopping services. You can monitor and administer several servers at the same time. Just add them with a single click and your machines will look after its buddies.": [
+ null,
+ "Cockpit 是完美的系统管理员工具,它可以轻松完成简单的任务, 如存储管理, 检查日志信息,以及启动/停止服务。 您可以同时监控和管理多个服务器。点一键就可以添加服务器,并开始进行管理。"
+ ],
+ "Cockpit might not render correctly in your browser": [
+ null,
+ "Cockpit 可能无法在您的浏览器中正确呈现"
+ ],
+ "Collect and package diagnostic and support data": [
+ null,
+ "收集并打包诊断和支持数据"
+ ],
+ "Collect kernel crash dumps": [
+ null,
+ "收集内核崩溃转储"
+ ],
+ "Compact PCI": [
+ null,
+ "紧凑型 PCI"
+ ],
+ "Connect to": [
+ null,
+ "连接到"
+ ],
+ "Connect to:": [
+ null,
+ "连接到:"
+ ],
+ "Connection has timed out.": [
+ null,
+ "连接超时。"
+ ],
+ "Convertible": [
+ null,
+ "可转换"
+ ],
+ "Copy": [
+ null,
+ "复制"
+ ],
+ "Copy to clipboard": [
+ null,
+ "复制到剪贴板"
+ ],
+ "Create": [
+ null,
+ "创建"
+ ],
+ "Create new task file with this content.": [
+ null,
+ "使用此内容创建新的任务文件。"
+ ],
+ "Ctrl+Insert": [
+ null,
+ "Ctrl+Insert"
+ ],
+ "Delay": [
+ null,
+ "延时"
+ ],
+ "Desktop": [
+ null,
+ "桌面"
+ ],
+ "Detachable": [
+ null,
+ "可拆开"
+ ],
+ "Diagnostic reports": [
+ null,
+ "诊断报告"
+ ],
+ "Docking station": [
+ null,
+ "扩展坞"
+ ],
+ "Download a new browser for free": [
+ null,
+ "免费下载新的浏览器"
+ ],
+ "Downloading $0": [
+ null,
+ "正在下载 $0"
+ ],
+ "Dual rank": [
+ null,
+ "双通道"
+ ],
+ "Embedded PC": [
+ null,
+ "嵌入式 PC"
+ ],
+ "Excellent password": [
+ null,
+ "密码强度良好"
+ ],
+ "Expansion chassis": [
+ null,
+ "扩展机箱"
+ ],
+ "Failed to change password": [
+ null,
+ "修改密码失败"
+ ],
+ "Failed to enable $0 in firewalld": [
+ null,
+ "在 firewalld 中启用 $0 失败"
+ ],
+ "Go to now": [
+ null,
+ "转到现在"
+ ],
+ "Handheld": [
+ null,
+ "手持式"
+ ],
+ "Hide confirmation password": [
+ null,
+ "隐藏确认密码"
+ ],
+ "Hide password": [
+ null,
+ "隐藏密码"
+ ],
+ "Host key is incorrect": [
+ null,
+ "主机密钥不正确"
+ ],
+ "If the fingerprint matches, click \"Accept key and log in\". Otherwise, do not log in and contact your administrator.": [
+ null,
+ "如果指纹匹配,点\"Accept key and log in\"。否则,请不要登录并联系您的管理员。"
+ ],
+ "Install": [
+ null,
+ "安装"
+ ],
+ "Install software": [
+ null,
+ "安装软件"
+ ],
+ "Installing $0": [
+ null,
+ "正在安装 $0"
+ ],
+ "Internal error": [
+ null,
+ "内部错误"
+ ],
+ "Internal error: Invalid challenge header": [
+ null,
+ "内部错误:无效的挑战字头部"
+ ],
+ "Invalid date format": [
+ null,
+ "无效的日期格式"
+ ],
+ "Invalid date format and invalid time format": [
+ null,
+ "无效的日期格式和时间格式"
+ ],
+ "Invalid file permissions": [
+ null,
+ "无效的文件权限"
+ ],
+ "Invalid time format": [
+ null,
+ "无效的时间格式"
+ ],
+ "Invalid timezone": [
+ null,
+ "无效的时区"
+ ],
+ "IoT gateway": [
+ null,
+ "IoT 网关"
+ ],
+ "Kernel dump": [
+ null,
+ "内核转储"
+ ],
+ "Laptop": [
+ null,
+ "笔记本电脑"
+ ],
+ "Learn more": [
+ null,
+ "了解更多"
+ ],
+ "Loading system modifications...": [
+ null,
+ "加载系统改变..."
+ ],
+ "Log in": [
+ null,
+ "登录"
+ ],
+ "Log in with your server user account.": [
+ null,
+ "使用您的服务器用户帐户登录。"
+ ],
+ "Log messages": [
+ null,
+ "日志消息"
+ ],
+ "Login": [
+ null,
+ "登录"
+ ],
+ "Login again": [
+ null,
+ "再次登录"
+ ],
+ "Login failed": [
+ null,
+ "登录失败"
+ ],
+ "Logout successful": [
+ null,
+ "注销成功"
+ ],
+ "Low profile desktop": [
+ null,
+ "低调桌面"
+ ],
+ "Lunch box": [
+ null,
+ "主机类型"
+ ],
+ "Main server chassis": [
+ null,
+ "主服务器机箱"
+ ],
+ "Manage storage": [
+ null,
+ "管理存储"
+ ],
+ "Manually": [
+ null,
+ "手动的"
+ ],
+ "Message to logged in users": [
+ null,
+ "发送给已登录用户的信息"
+ ],
+ "Mini PC": [
+ null,
+ "迷你电脑"
+ ],
+ "Mini tower": [
+ null,
+ "迷你塔式主机"
+ ],
+ "Multi-system chassis": [
+ null,
+ "多系统机箱"
+ ],
+ "NTP server": [
+ null,
+ "NTP 服务器"
+ ],
+ "Need at least one NTP server": [
+ null,
+ "至少需要一个 NTP 服务器"
+ ],
+ "Networking": [
+ null,
+ "网络"
+ ],
+ "New host": [
+ null,
+ "新主机"
+ ],
+ "New password was not accepted": [
+ null,
+ "新密码不被接受"
+ ],
+ "No delay": [
+ null,
+ "无延时"
+ ],
+ "No such file or directory": [
+ null,
+ "没有该文件或目录"
+ ],
+ "No system modifications": [
+ null,
+ "没有系统改变"
+ ],
+ "Not a valid private key": [
+ null,
+ "无效的私钥"
+ ],
+ "Not permitted to perform this action.": [
+ null,
+ "不允许执行该操作。"
+ ],
+ "Not synchronized": [
+ null,
+ "未同步"
+ ],
+ "Notebook": [
+ null,
+ "笔记本"
+ ],
+ "Occurrences": [
+ null,
+ "发生"
+ ],
+ "Ok": [
+ null,
+ "确认"
+ ],
+ "Old password not accepted": [
+ null,
+ "旧密码不被接受"
+ ],
+ "Once Cockpit is installed, enable it with \"systemctl enable --now cockpit.socket\".": [
+ null,
+ "在安装 Cockpit 后,使用 \"systemctl enable --now cockpit.socket\" 启用它。"
+ ],
+ "Or use a bundled browser": [
+ null,
+ "或者使用捆绑的浏览器"
+ ],
+ "Other": [
+ null,
+ "其他"
+ ],
+ "Other options": [
+ null,
+ "其他选项"
+ ],
+ "PackageKit crashed": [
+ null,
+ "PackageKit 已崩溃"
+ ],
+ "Password": [
+ null,
+ "密码"
+ ],
+ "Password is not acceptable": [
+ null,
+ "不接受该密码"
+ ],
+ "Password is too weak": [
+ null,
+ "密码太弱"
+ ],
+ "Password not accepted": [
+ null,
+ "密码未接受"
+ ],
+ "Paste": [
+ null,
+ "粘贴"
+ ],
+ "Paste error": [
+ null,
+ "粘贴错误"
+ ],
+ "Path to file": [
+ null,
+ "文件路径"
+ ],
+ "Peripheral chassis": [
+ null,
+ "外设机箱"
+ ],
+ "Permission denied": [
+ null,
+ "权限被拒绝"
+ ],
+ "Pick date": [
+ null,
+ "选择日期"
+ ],
+ "Pizza box": [
+ null,
+ "披萨盒"
+ ],
+ "Please enable JavaScript to use the Web Console.": [
+ null,
+ "请启用 JavaScript 来使用 Web 控制台。"
+ ],
+ "Please specify the host to connect to": [
+ null,
+ "请指定要连接的主机"
+ ],
+ "Portable": [
+ null,
+ "手提"
+ ],
+ "Present": [
+ null,
+ "当前"
+ ],
+ "Prompting via ssh-add timed out": [
+ null,
+ "通过 ssh-add 提示超时"
+ ],
+ "Prompting via ssh-keygen timed out": [
+ null,
+ "通过 ssh-keygen 提示超时"
+ ],
+ "RAID chassis": [
+ null,
+ "RAID 机箱"
+ ],
+ "Rack mount chassis": [
+ null,
+ "机架式机箱"
+ ],
+ "Reboot": [
+ null,
+ "重启"
+ ],
+ "Recent hosts": [
+ null,
+ "最近的主机"
+ ],
+ "Refusing to connect. Host is unknown": [
+ null,
+ "拒绝连接。主机未知"
+ ],
+ "Refusing to connect. Hostkey does not match": [
+ null,
+ "拒绝连接。主机密钥不匹配"
+ ],
+ "Refusing to connect. Hostkey is unknown": [
+ null,
+ "拒绝连接。未知主机密钥"
+ ],
+ "Removals:": [
+ null,
+ "移除:"
+ ],
+ "Remove host": [
+ null,
+ "删除主机"
+ ],
+ "Removing $0": [
+ null,
+ "正在删除 $0"
+ ],
+ "SELinux": [
+ null,
+ "SELinux"
+ ],
+ "Sealed-case PC": [
+ null,
+ "密封式 PC"
+ ],
+ "Security Enhanced Linux configuration and troubleshooting": [
+ null,
+ "Security Enhanced Linux 配置和故障排除"
+ ],
+ "Server": [
+ null,
+ "服务器"
+ ],
+ "Server has closed the connection.": [
+ null,
+ "服务器关闭了连接。"
+ ],
+ "Set time": [
+ null,
+ "设置时间"
+ ],
+ "Shell script": [
+ null,
+ "Shell 脚本"
+ ],
+ "Shift+Insert": [
+ null,
+ "Shift+Insert"
+ ],
+ "Show confirmation password": [
+ null,
+ "显示确认密码"
+ ],
+ "Show password": [
+ null,
+ "显示密码"
+ ],
+ "Shut down": [
+ null,
+ "关机"
+ ],
+ "Single rank": [
+ null,
+ "单 rank"
+ ],
+ "Space-saving computer": [
+ null,
+ "节省空间的计算机"
+ ],
+ "Specific time": [
+ null,
+ "指定时间"
+ ],
+ "Stick PC": [
+ null,
+ "PC 棒"
+ ],
+ "Storage": [
+ null,
+ "存储"
+ ],
+ "Strong password": [
+ null,
+ "强密码"
+ ],
+ "Sub-Chassis": [
+ null,
+ "子机箱"
+ ],
+ "Sub-Notebook": [
+ null,
+ "子笔记本"
+ ],
+ "Synchronized": [
+ null,
+ "已同步"
+ ],
+ "Synchronized with $0": [
+ null,
+ "与 $0 同步"
+ ],
+ "Synchronizing": [
+ null,
+ "同步"
+ ],
+ "Tablet": [
+ null,
+ "平板"
+ ],
+ "The logged in user is not permitted to view system modifications": [
+ null,
+ "登陆的用户没有权限查看系统改变"
+ ],
+ "The passwords do not match.": [
+ null,
+ "密码不匹配。"
+ ],
+ "The resulting fingerprint is fine to share via public methods, including email.": [
+ null,
+ "结果指纹可以通过公共方法(包括电子邮件)共享。"
+ ],
+ "The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available.": [
+ null,
+ "服务器拒绝使用密码验证的方法来验证 '$0',并且没有其他支持的验证途径可以使用。"
+ ],
+ "The server refused to authenticate using any supported methods.": [
+ null,
+ "服务器拒绝使用任何支持的方式来验证。"
+ ],
+ "The web browser configuration prevents Cockpit from running (inaccessible $0)": [
+ null,
+ "浏览器配置阻止 Cockpit 运行 (无法访问 $0)"
+ ],
+ "This tool configures the SELinux policy and can help with understanding and resolving policy violations.": [
+ null,
+ "这个工具配置 SELinux 策略,帮助理解和解决策略违规。"
+ ],
+ "This tool configures the system to write kernel crash dumps to disk.": [
+ null,
+ "这个工具配置系统,以将内核崩溃转储写入磁盘。"
+ ],
+ "This tool generates an archive of configuration and diagnostic information from the running system. The archive may be stored locally or centrally for recording or tracking purposes or may be sent to technical support representatives, developers or system administrators to assist with technical fault-finding and debugging.": [
+ null,
+ "此工具从正在运行的系统中生成配置和诊断信息的存档。出于记录或跟踪目的,存档可能被存储在本地或集中存储,或者被发送到技术支持代表、开发人员或系统管理员,以帮助技术故障查找和调试。"
+ ],
+ "This tool manages local storage, such as filesystems, LVM2 volume groups, and NFS mounts.": [
+ null,
+ "此工具管理本地存储,如文件系统、LVM2 卷组和 NFS 挂载。"
+ ],
+ "This tool manages networking such as bonds, bridges, teams, VLANs and firewalls using NetworkManager and Firewalld. NetworkManager is incompatible with Ubuntu's default systemd-networkd and Debian's ifupdown scripts.": [
+ null,
+ "此工具使用 NetworkManager 和 Firewalld 管理网络,如绑定、网桥、团队、VLAN 和防火墙等。NetworkManager 与 Ubuntu 的默认 systemd-networkd 和 Debian 的 ifupdown 脚本不兼容。"
+ ],
+ "This web browser is too old to run the Web Console (missing $0)": [
+ null,
+ "这个浏览器太老,无法运行 Web 控制台(缺少 $0)"
+ ],
+ "Time zone": [
+ null,
+ "时区"
+ ],
+ "To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:": [
+ null,
+ "要确保您的连接没有被恶意第三方截取,请验证主机密钥指纹:"
+ ],
+ "To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:": [
+ null,
+ "要验证指纹,在 $0 上运行以下内容(在实际的物理机器上本地进行,或通过一个可信任的网络进行):"
+ ],
+ "Toggle date picker": [
+ null,
+ "切换日期选择器"
+ ],
+ "Too much data": [
+ null,
+ "太多数据"
+ ],
+ "Total size: $0": [
+ null,
+ "总大小:$0"
+ ],
+ "Tower": [
+ null,
+ "Tower"
+ ],
+ "Try again": [
+ null,
+ "重试"
+ ],
+ "Trying to synchronize with $0": [
+ null,
+ "正在尝试与 $0 同步"
+ ],
+ "Unable to connect to that address": [
+ null,
+ "无法连接至该地址"
+ ],
+ "Unknown": [
+ null,
+ "未知"
+ ],
+ "Untrusted host": [
+ null,
+ "不可信的主机"
+ ],
+ "User name": [
+ null,
+ "用户名"
+ ],
+ "User name cannot be empty": [
+ null,
+ "用户名不能为空"
+ ],
+ "Validating authentication token": [
+ null,
+ "正在校验验证口令"
+ ],
+ "View all logs": [
+ null,
+ "查看所有日志"
+ ],
+ "View automation script": [
+ null,
+ "查看自动化脚本"
+ ],
+ "Visit firewall": [
+ null,
+ "访问防火墙"
+ ],
+ "Waiting for other software management operations to finish": [
+ null,
+ "等待其他软件管理操作完成"
+ ],
+ "Weak password": [
+ null,
+ "弱密码"
+ ],
+ "Web Console for Linux servers": [
+ null,
+ "Linux 服务器的 Web 控制台"
+ ],
+ "Wrong user name or password": [
+ null,
+ "错误的用户名或密码"
+ ],
+ "You are connecting to $0 for the first time.": [
+ null,
+ "您第一次连接到 $0。"
+ ],
+ "Your browser does not allow paste from the context menu. You can use Shift+Insert.": [
+ null,
+ "您的浏览器不允许从上下文菜单中进行粘贴,您可以使用 Shift+Insert。"
+ ],
+ "Your session has been terminated.": [
+ null,
+ "会话被终止。"
+ ],
+ "Your session has expired. Please log in again.": [
+ null,
+ "会话超时。请重新登录。"
+ ],
+ "Zone": [
+ null,
+ "区域"
+ ],
+ "[binary data]": [
+ null,
+ "[二进制数据]"
+ ],
+ "[no data]": [
+ null,
+ "[没有数据]"
+ ],
+ "password quality": [
+ null,
+ "密码质量"
+ ],
+ "show less": [
+ null,
+ "显示更少"
+ ],
+ "show more": [
+ null,
+ "显示更多"
+ ]
+};
diff --git a/dist/storaged/index.html b/dist/storaged/index.html
new file mode 100644
index 0000000..59e2ec5
--- /dev/null
+++ b/dist/storaged/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html>
+<head>
+ <title translate="yes">Storage</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="storaged.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="../manifests.js"></script>
+ <script src="po.js"></script>
+ <script src="storaged.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="storage">
+ </div>
+</body>
+</html>
diff --git a/dist/storaged/manifest.json b/dist/storaged/manifest.json
new file mode 100644
index 0000000..82c6c36
--- /dev/null
+++ b/dist/storaged/manifest.json
@@ -0,0 +1,75 @@
+{
+ "name": "storage",
+ "requires": {
+ "cockpit": "266"
+ },
+ "conditions": [
+ {"path-exists": "/usr/share/dbus-1/system.d/org.freedesktop.UDisks2.conf"}
+ ],
+
+ "menu": {
+ "index": {
+ "label": "Storage",
+ "order": 30,
+ "docs": [
+ {
+ "label": "Managing partitions",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-partitions-using-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing NFS mounts",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-nfs-mounts-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing RAIDs",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-redundant-arrays-of-independent-disks-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing LVMs",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/using-the-web-console-for-configuring-lvm-logical-volumes_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing physical drives",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/using-the-web-console-for-changing-physical-drives-in-volume-groups_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing VDOs",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/using-the-web-console-for-managing-virtual-data-optimizer-volumes_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Using LUKS encryption",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/locking-data-with-luks-password-in-the-rhel-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Using Tang server",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-automated-unlocking-using-a-tang-key-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["filesystem", "partition", "nfs", "raid", "volume", "disk", "vdo", "iscsi", "drive", "mount", "unmount", "udisks", "mkfs", "format", "fstab", "lvm2", "luks", "encryption", "nbde", "tang"]
+ }
+ ]
+ }
+ },
+
+ "config": {
+ "nfs_client_package": {
+ "rhel": "nfs-utils", "fedora": "nfs-utils",
+ "opensuse": "nfs-client", "opensuse-leap": "nfs-client",
+ "debian": "nfs-common", "ubuntu": "nfs-common",
+ "arch": "nfs-utils"
+ },
+ "vdo_package": { "rhel": "vdo", "centos": "vdo" },
+ "stratis_package": { "fedora": "stratisd",
+ "centos": "stratisd",
+ "arch": "stratisd",
+ "platform:el9": "stratisd"
+ },
+ "nbde_root_help": { "fedora": true,
+ "centos": true,
+ "rhel": true
+ }
+ },
+ "content-security-policy": "img-src 'self' data:"
+}
diff --git a/dist/storaged/po.cs.js.gz b/dist/storaged/po.cs.js.gz
new file mode 100644
index 0000000..df6820a
--- /dev/null
+++ b/dist/storaged/po.cs.js.gz
Binary files differ
diff --git a/dist/storaged/po.de.js.gz b/dist/storaged/po.de.js.gz
new file mode 100644
index 0000000..25b2eca
--- /dev/null
+++ b/dist/storaged/po.de.js.gz
Binary files differ
diff --git a/dist/storaged/po.es.js.gz b/dist/storaged/po.es.js.gz
new file mode 100644
index 0000000..1bf167c
--- /dev/null
+++ b/dist/storaged/po.es.js.gz
Binary files differ
diff --git a/dist/storaged/po.fi.js.gz b/dist/storaged/po.fi.js.gz
new file mode 100644
index 0000000..3eb7fe3
--- /dev/null
+++ b/dist/storaged/po.fi.js.gz
Binary files differ
diff --git a/dist/storaged/po.fr.js.gz b/dist/storaged/po.fr.js.gz
new file mode 100644
index 0000000..61e3488
--- /dev/null
+++ b/dist/storaged/po.fr.js.gz
Binary files differ
diff --git a/dist/storaged/po.he.js.gz b/dist/storaged/po.he.js.gz
new file mode 100644
index 0000000..5ae863e
--- /dev/null
+++ b/dist/storaged/po.he.js.gz
Binary files differ
diff --git a/dist/storaged/po.it.js.gz b/dist/storaged/po.it.js.gz
new file mode 100644
index 0000000..a050458
--- /dev/null
+++ b/dist/storaged/po.it.js.gz
Binary files differ
diff --git a/dist/storaged/po.ja.js.gz b/dist/storaged/po.ja.js.gz
new file mode 100644
index 0000000..b581bae
--- /dev/null
+++ b/dist/storaged/po.ja.js.gz
Binary files differ
diff --git a/dist/storaged/po.ka.js.gz b/dist/storaged/po.ka.js.gz
new file mode 100644
index 0000000..214d124
--- /dev/null
+++ b/dist/storaged/po.ka.js.gz
Binary files differ
diff --git a/dist/storaged/po.ko.js.gz b/dist/storaged/po.ko.js.gz
new file mode 100644
index 0000000..d22eea0
--- /dev/null
+++ b/dist/storaged/po.ko.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.cs.js.gz b/dist/storaged/po.manifest.cs.js.gz
new file mode 100644
index 0000000..bfb3332
--- /dev/null
+++ b/dist/storaged/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.de.js.gz b/dist/storaged/po.manifest.de.js.gz
new file mode 100644
index 0000000..972e93b
--- /dev/null
+++ b/dist/storaged/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.es.js.gz b/dist/storaged/po.manifest.es.js.gz
new file mode 100644
index 0000000..4ea02f0
--- /dev/null
+++ b/dist/storaged/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.fi.js.gz b/dist/storaged/po.manifest.fi.js.gz
new file mode 100644
index 0000000..4331dcd
--- /dev/null
+++ b/dist/storaged/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.fr.js.gz b/dist/storaged/po.manifest.fr.js.gz
new file mode 100644
index 0000000..d050145
--- /dev/null
+++ b/dist/storaged/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.he.js.gz b/dist/storaged/po.manifest.he.js.gz
new file mode 100644
index 0000000..117ce52
--- /dev/null
+++ b/dist/storaged/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.it.js.gz b/dist/storaged/po.manifest.it.js.gz
new file mode 100644
index 0000000..c39c6d7
--- /dev/null
+++ b/dist/storaged/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.ja.js.gz b/dist/storaged/po.manifest.ja.js.gz
new file mode 100644
index 0000000..7656218
--- /dev/null
+++ b/dist/storaged/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.ka.js.gz b/dist/storaged/po.manifest.ka.js.gz
new file mode 100644
index 0000000..66dc28a
--- /dev/null
+++ b/dist/storaged/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.ko.js.gz b/dist/storaged/po.manifest.ko.js.gz
new file mode 100644
index 0000000..28e065d
--- /dev/null
+++ b/dist/storaged/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.nb_NO.js.gz b/dist/storaged/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..0967511
--- /dev/null
+++ b/dist/storaged/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.nl.js.gz b/dist/storaged/po.manifest.nl.js.gz
new file mode 100644
index 0000000..b35d476
--- /dev/null
+++ b/dist/storaged/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.pl.js.gz b/dist/storaged/po.manifest.pl.js.gz
new file mode 100644
index 0000000..2d4bcea
--- /dev/null
+++ b/dist/storaged/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.pt_BR.js.gz b/dist/storaged/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..ae844e8
--- /dev/null
+++ b/dist/storaged/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.ru.js.gz b/dist/storaged/po.manifest.ru.js.gz
new file mode 100644
index 0000000..93a7554
--- /dev/null
+++ b/dist/storaged/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.sk.js.gz b/dist/storaged/po.manifest.sk.js.gz
new file mode 100644
index 0000000..d73ab64
--- /dev/null
+++ b/dist/storaged/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.sv.js.gz b/dist/storaged/po.manifest.sv.js.gz
new file mode 100644
index 0000000..ea4b040
--- /dev/null
+++ b/dist/storaged/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.tr.js.gz b/dist/storaged/po.manifest.tr.js.gz
new file mode 100644
index 0000000..eb21029
--- /dev/null
+++ b/dist/storaged/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.uk.js.gz b/dist/storaged/po.manifest.uk.js.gz
new file mode 100644
index 0000000..769ee2a
--- /dev/null
+++ b/dist/storaged/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/storaged/po.manifest.zh_CN.js.gz b/dist/storaged/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..1077d2c
--- /dev/null
+++ b/dist/storaged/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/storaged/po.nb_NO.js.gz b/dist/storaged/po.nb_NO.js.gz
new file mode 100644
index 0000000..5373cf5
--- /dev/null
+++ b/dist/storaged/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/storaged/po.nl.js.gz b/dist/storaged/po.nl.js.gz
new file mode 100644
index 0000000..066b2d6
--- /dev/null
+++ b/dist/storaged/po.nl.js.gz
Binary files differ
diff --git a/dist/storaged/po.pl.js.gz b/dist/storaged/po.pl.js.gz
new file mode 100644
index 0000000..9f6aeb6
--- /dev/null
+++ b/dist/storaged/po.pl.js.gz
Binary files differ
diff --git a/dist/storaged/po.pt_BR.js.gz b/dist/storaged/po.pt_BR.js.gz
new file mode 100644
index 0000000..5e4382c
--- /dev/null
+++ b/dist/storaged/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/storaged/po.ru.js.gz b/dist/storaged/po.ru.js.gz
new file mode 100644
index 0000000..994812d
--- /dev/null
+++ b/dist/storaged/po.ru.js.gz
Binary files differ
diff --git a/dist/storaged/po.sk.js.gz b/dist/storaged/po.sk.js.gz
new file mode 100644
index 0000000..24e1ea6
--- /dev/null
+++ b/dist/storaged/po.sk.js.gz
Binary files differ
diff --git a/dist/storaged/po.sv.js.gz b/dist/storaged/po.sv.js.gz
new file mode 100644
index 0000000..a86dd5b
--- /dev/null
+++ b/dist/storaged/po.sv.js.gz
Binary files differ
diff --git a/dist/storaged/po.tr.js.gz b/dist/storaged/po.tr.js.gz
new file mode 100644
index 0000000..e7560a4
--- /dev/null
+++ b/dist/storaged/po.tr.js.gz
Binary files differ
diff --git a/dist/storaged/po.uk.js.gz b/dist/storaged/po.uk.js.gz
new file mode 100644
index 0000000..ebc0248
--- /dev/null
+++ b/dist/storaged/po.uk.js.gz
Binary files differ
diff --git a/dist/storaged/po.zh_CN.js.gz b/dist/storaged/po.zh_CN.js.gz
new file mode 100644
index 0000000..98c92d0
--- /dev/null
+++ b/dist/storaged/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/storaged/storaged.css.LEGAL.txt b/dist/storaged/storaged.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/storaged/storaged.css.LEGAL.txt
diff --git a/dist/storaged/storaged.css.gz b/dist/storaged/storaged.css.gz
new file mode 100644
index 0000000..27fa564
--- /dev/null
+++ b/dist/storaged/storaged.css.gz
Binary files differ
diff --git a/dist/storaged/storaged.js.LEGAL.txt b/dist/storaged/storaged.js.LEGAL.txt
new file mode 100644
index 0000000..da847c4
--- /dev/null
+++ b/dist/storaged/storaged.js.LEGAL.txt
@@ -0,0 +1,66 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+js-sha1/src/sha1.js:
+ /*
+ * [js-sha1]{@link https://github.com/emn178/js-sha1}
+ *
+ * @version 0.7.0
+ * @author Chen, Yi-Cyuan [emn178@gmail.com]
+ * @copyright Chen, Yi-Cyuan 2014-2024
+ * @license MIT
+ */
+
+js-sha256/src/sha256.js:
+ /**
+ * [js-sha256]{@link https://github.com/emn178/js-sha256}
+ *
+ * @version 0.11.0
+ * @author Chen, Yi-Cyuan [emn178@gmail.com]
+ * @copyright Chen, Yi-Cyuan 2014-2024
+ * @license MIT
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/storaged/storaged.js.gz b/dist/storaged/storaged.js.gz
new file mode 100644
index 0000000..f0260e6
--- /dev/null
+++ b/dist/storaged/storaged.js.gz
Binary files differ
diff --git a/dist/systemd/hwinfo.css.LEGAL.txt b/dist/systemd/hwinfo.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/systemd/hwinfo.css.LEGAL.txt
diff --git a/dist/systemd/hwinfo.css.gz b/dist/systemd/hwinfo.css.gz
new file mode 100644
index 0000000..e40dbc5
--- /dev/null
+++ b/dist/systemd/hwinfo.css.gz
Binary files differ
diff --git a/dist/systemd/hwinfo.html b/dist/systemd/hwinfo.html
new file mode 100644
index 0000000..65ea5da
--- /dev/null
+++ b/dist/systemd/hwinfo.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html id="system-hwinfo-page">
+<head>
+ <title translate="yes">Hardware information</title>
+ <meta charset="utf-8" />
+ <link href="hwinfo.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="hwinfo"></div>
+ <script src="hwinfo.js"></script>
+</body>
+</html>
diff --git a/dist/systemd/hwinfo.js.LEGAL.txt b/dist/systemd/hwinfo.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/systemd/hwinfo.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/systemd/hwinfo.js.gz b/dist/systemd/hwinfo.js.gz
new file mode 100644
index 0000000..aea7ad2
--- /dev/null
+++ b/dist/systemd/hwinfo.js.gz
Binary files differ
diff --git a/dist/systemd/index.html b/dist/systemd/index.html
new file mode 100644
index 0000000..5eeb8d8
--- /dev/null
+++ b/dist/systemd/index.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html id="system-overview-page">
+<head>
+ <meta charset="utf-8" />
+ <title>Overview</title>
+ <meta name="description" content="" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="stylesheet" href="overview.css" />
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="overview.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script src="../manifests.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="overview"></div>
+</body>
+</html>
diff --git a/dist/systemd/logs.css.LEGAL.txt b/dist/systemd/logs.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/systemd/logs.css.LEGAL.txt
diff --git a/dist/systemd/logs.css.gz b/dist/systemd/logs.css.gz
new file mode 100644
index 0000000..f810225
--- /dev/null
+++ b/dist/systemd/logs.css.gz
Binary files differ
diff --git a/dist/systemd/logs.html b/dist/systemd/logs.html
new file mode 100644
index 0000000..d0c996a
--- /dev/null
+++ b/dist/systemd/logs.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2015 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="system-logs-page">
+
+<head>
+ <title translate="yes">Journal</title>
+ <meta charset="utf-8" />
+ <link href="logs.css" rel="stylesheet" />
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="logs">
+ </div>
+
+ <script type="text/javascript" src="logs.js"></script>
+</body>
+
+</html>
diff --git a/dist/systemd/logs.js.LEGAL.txt b/dist/systemd/logs.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/systemd/logs.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/systemd/logs.js.gz b/dist/systemd/logs.js.gz
new file mode 100644
index 0000000..a93984e
--- /dev/null
+++ b/dist/systemd/logs.js.gz
Binary files differ
diff --git a/dist/systemd/manifest.json b/dist/systemd/manifest.json
new file mode 100644
index 0000000..4f8ad90
--- /dev/null
+++ b/dist/systemd/manifest.json
@@ -0,0 +1,88 @@
+{
+ "name": "system",
+
+ "requires": {
+ "cockpit": "265"
+ },
+
+ "menu": {
+ "index": {
+ "label": "Overview",
+ "order": 10,
+ "docs": [
+ {
+ "label": "Configuring system settings",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/getting-started-with-the-rhel-8-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["time", "date", "restart", "shut", "domain", "machine", "operating system", "os", "asset tag", "ssh", "power", "version", "host"]
+ },
+ {
+ "matches": ["hardware", "mitigation", "pci", "memory", "cpu", "bios", "ram", "dimm", "serial"],
+ "goto": "/system/hwinfo"
+ },
+ {
+ "matches": ["graphs", "metrics", "history", "pcp", "cpu", "memory", "disks", "network", "cgroups", "performance"],
+ "goto": "/metrics"
+ }
+ ]
+ },
+ "services": {
+ "label": "Services",
+ "order": 100,
+ "docs": [
+ {
+ "label": "Managing services",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-services-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["service", "systemd", "target", "socket", "timer", "path", "unit", "systemctl"]
+ },
+ {
+ "matches": ["boot", "mask", "unmask", "restart", "enable", "disable"],
+ "weight": 1
+ }
+ ]
+ },
+ "logs": {
+ "label": "Logs",
+ "order": 20,
+ "docs": [
+ {
+ "label": "Reviewing logs",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/reviewing-logs_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["journal", "warning", "error", "debug"]
+ },
+ {
+ "matches": ["abrt", "crash", "coredump"],
+ "goto": "?tag=abrt-notification"
+ }
+ ]
+ }
+ },
+
+ "tools": {
+ "terminal": {
+ "label": "Terminal",
+ "keywords": [
+ {
+ "matches": ["console", "command", "bash", "shell"]
+ }
+ ]
+ }
+ },
+
+ "libexecdir": "${libexecdir}",
+
+ "preload": [ "index", "services" ],
+
+ "content-security-policy": "img-src 'self' data:"
+}
diff --git a/dist/systemd/overview.css.LEGAL.txt b/dist/systemd/overview.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/systemd/overview.css.LEGAL.txt
diff --git a/dist/systemd/overview.css.gz b/dist/systemd/overview.css.gz
new file mode 100644
index 0000000..e05157d
--- /dev/null
+++ b/dist/systemd/overview.css.gz
Binary files differ
diff --git a/dist/systemd/overview.js.LEGAL.txt b/dist/systemd/overview.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/systemd/overview.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/systemd/overview.js.gz b/dist/systemd/overview.js.gz
new file mode 100644
index 0000000..02830ee
--- /dev/null
+++ b/dist/systemd/overview.js.gz
Binary files differ
diff --git a/dist/systemd/po.cs.js.gz b/dist/systemd/po.cs.js.gz
new file mode 100644
index 0000000..5f2b9d9
--- /dev/null
+++ b/dist/systemd/po.cs.js.gz
Binary files differ
diff --git a/dist/systemd/po.de.js.gz b/dist/systemd/po.de.js.gz
new file mode 100644
index 0000000..57cfc19
--- /dev/null
+++ b/dist/systemd/po.de.js.gz
Binary files differ
diff --git a/dist/systemd/po.es.js.gz b/dist/systemd/po.es.js.gz
new file mode 100644
index 0000000..6a626a0
--- /dev/null
+++ b/dist/systemd/po.es.js.gz
Binary files differ
diff --git a/dist/systemd/po.fi.js.gz b/dist/systemd/po.fi.js.gz
new file mode 100644
index 0000000..dc3fa7e
--- /dev/null
+++ b/dist/systemd/po.fi.js.gz
Binary files differ
diff --git a/dist/systemd/po.fr.js.gz b/dist/systemd/po.fr.js.gz
new file mode 100644
index 0000000..3122252
--- /dev/null
+++ b/dist/systemd/po.fr.js.gz
Binary files differ
diff --git a/dist/systemd/po.he.js.gz b/dist/systemd/po.he.js.gz
new file mode 100644
index 0000000..7d0a11f
--- /dev/null
+++ b/dist/systemd/po.he.js.gz
Binary files differ
diff --git a/dist/systemd/po.it.js.gz b/dist/systemd/po.it.js.gz
new file mode 100644
index 0000000..727e64c
--- /dev/null
+++ b/dist/systemd/po.it.js.gz
Binary files differ
diff --git a/dist/systemd/po.ja.js.gz b/dist/systemd/po.ja.js.gz
new file mode 100644
index 0000000..476e6e3
--- /dev/null
+++ b/dist/systemd/po.ja.js.gz
Binary files differ
diff --git a/dist/systemd/po.ka.js.gz b/dist/systemd/po.ka.js.gz
new file mode 100644
index 0000000..5b3cce2
--- /dev/null
+++ b/dist/systemd/po.ka.js.gz
Binary files differ
diff --git a/dist/systemd/po.ko.js.gz b/dist/systemd/po.ko.js.gz
new file mode 100644
index 0000000..f16766f
--- /dev/null
+++ b/dist/systemd/po.ko.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.cs.js.gz b/dist/systemd/po.manifest.cs.js.gz
new file mode 100644
index 0000000..c57678e
--- /dev/null
+++ b/dist/systemd/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.de.js.gz b/dist/systemd/po.manifest.de.js.gz
new file mode 100644
index 0000000..0b5b41d
--- /dev/null
+++ b/dist/systemd/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.es.js.gz b/dist/systemd/po.manifest.es.js.gz
new file mode 100644
index 0000000..6402e24
--- /dev/null
+++ b/dist/systemd/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.fi.js.gz b/dist/systemd/po.manifest.fi.js.gz
new file mode 100644
index 0000000..acb86d9
--- /dev/null
+++ b/dist/systemd/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.fr.js.gz b/dist/systemd/po.manifest.fr.js.gz
new file mode 100644
index 0000000..90b73a3
--- /dev/null
+++ b/dist/systemd/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.he.js.gz b/dist/systemd/po.manifest.he.js.gz
new file mode 100644
index 0000000..c1526eb
--- /dev/null
+++ b/dist/systemd/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.it.js.gz b/dist/systemd/po.manifest.it.js.gz
new file mode 100644
index 0000000..eb09512
--- /dev/null
+++ b/dist/systemd/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.ja.js.gz b/dist/systemd/po.manifest.ja.js.gz
new file mode 100644
index 0000000..1ddcc77
--- /dev/null
+++ b/dist/systemd/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.ka.js.gz b/dist/systemd/po.manifest.ka.js.gz
new file mode 100644
index 0000000..8339113
--- /dev/null
+++ b/dist/systemd/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.ko.js.gz b/dist/systemd/po.manifest.ko.js.gz
new file mode 100644
index 0000000..c613257
--- /dev/null
+++ b/dist/systemd/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.nb_NO.js.gz b/dist/systemd/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..2c301aa
--- /dev/null
+++ b/dist/systemd/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.nl.js.gz b/dist/systemd/po.manifest.nl.js.gz
new file mode 100644
index 0000000..b752bae
--- /dev/null
+++ b/dist/systemd/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.pl.js.gz b/dist/systemd/po.manifest.pl.js.gz
new file mode 100644
index 0000000..11ced3c
--- /dev/null
+++ b/dist/systemd/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.pt_BR.js.gz b/dist/systemd/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..337ade8
--- /dev/null
+++ b/dist/systemd/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.ru.js.gz b/dist/systemd/po.manifest.ru.js.gz
new file mode 100644
index 0000000..6ac51af
--- /dev/null
+++ b/dist/systemd/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.sk.js.gz b/dist/systemd/po.manifest.sk.js.gz
new file mode 100644
index 0000000..5309e89
--- /dev/null
+++ b/dist/systemd/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.sv.js.gz b/dist/systemd/po.manifest.sv.js.gz
new file mode 100644
index 0000000..d8a98f3
--- /dev/null
+++ b/dist/systemd/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.tr.js.gz b/dist/systemd/po.manifest.tr.js.gz
new file mode 100644
index 0000000..1012b16
--- /dev/null
+++ b/dist/systemd/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.uk.js.gz b/dist/systemd/po.manifest.uk.js.gz
new file mode 100644
index 0000000..fc7f68f
--- /dev/null
+++ b/dist/systemd/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/systemd/po.manifest.zh_CN.js.gz b/dist/systemd/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..0608cdf
--- /dev/null
+++ b/dist/systemd/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/systemd/po.nb_NO.js.gz b/dist/systemd/po.nb_NO.js.gz
new file mode 100644
index 0000000..29dc5bf
--- /dev/null
+++ b/dist/systemd/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/systemd/po.nl.js.gz b/dist/systemd/po.nl.js.gz
new file mode 100644
index 0000000..e73eeec
--- /dev/null
+++ b/dist/systemd/po.nl.js.gz
Binary files differ
diff --git a/dist/systemd/po.pl.js.gz b/dist/systemd/po.pl.js.gz
new file mode 100644
index 0000000..5af0795
--- /dev/null
+++ b/dist/systemd/po.pl.js.gz
Binary files differ
diff --git a/dist/systemd/po.pt_BR.js.gz b/dist/systemd/po.pt_BR.js.gz
new file mode 100644
index 0000000..c46c01e
--- /dev/null
+++ b/dist/systemd/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/systemd/po.ru.js.gz b/dist/systemd/po.ru.js.gz
new file mode 100644
index 0000000..642193b
--- /dev/null
+++ b/dist/systemd/po.ru.js.gz
Binary files differ
diff --git a/dist/systemd/po.sk.js.gz b/dist/systemd/po.sk.js.gz
new file mode 100644
index 0000000..ff6c61e
--- /dev/null
+++ b/dist/systemd/po.sk.js.gz
Binary files differ
diff --git a/dist/systemd/po.sv.js.gz b/dist/systemd/po.sv.js.gz
new file mode 100644
index 0000000..f9f05b8
--- /dev/null
+++ b/dist/systemd/po.sv.js.gz
Binary files differ
diff --git a/dist/systemd/po.tr.js.gz b/dist/systemd/po.tr.js.gz
new file mode 100644
index 0000000..a97993a
--- /dev/null
+++ b/dist/systemd/po.tr.js.gz
Binary files differ
diff --git a/dist/systemd/po.uk.js.gz b/dist/systemd/po.uk.js.gz
new file mode 100644
index 0000000..7a2b169
--- /dev/null
+++ b/dist/systemd/po.uk.js.gz
Binary files differ
diff --git a/dist/systemd/po.zh_CN.js.gz b/dist/systemd/po.zh_CN.js.gz
new file mode 100644
index 0000000..701a2f0
--- /dev/null
+++ b/dist/systemd/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/systemd/services.css.LEGAL.txt b/dist/systemd/services.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/systemd/services.css.LEGAL.txt
diff --git a/dist/systemd/services.css.gz b/dist/systemd/services.css.gz
new file mode 100644
index 0000000..a029944
--- /dev/null
+++ b/dist/systemd/services.css.gz
Binary files differ
diff --git a/dist/systemd/services.html b/dist/systemd/services.html
new file mode 100644
index 0000000..b9076a9
--- /dev/null
+++ b/dist/systemd/services.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html id="system-services-page">
+<head>
+ <title translate="yes">Services</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="services.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="services.js"></script>
+ <script src="po.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums" id="services-page">
+ <div class="ct-page-fill" id="services"></div>
+</body>
+</html>
diff --git a/dist/systemd/services.js.LEGAL.txt b/dist/systemd/services.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/systemd/services.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/systemd/services.js.gz b/dist/systemd/services.js.gz
new file mode 100644
index 0000000..f058a17
--- /dev/null
+++ b/dist/systemd/services.js.gz
Binary files differ
diff --git a/dist/systemd/terminal.css.LEGAL.txt b/dist/systemd/terminal.css.LEGAL.txt
new file mode 100644
index 0000000..f1c05c0
--- /dev/null
+++ b/dist/systemd/terminal.css.LEGAL.txt
@@ -0,0 +1,35 @@
+Bundled license information:
+
+xterm/css/xterm.css:
+ /**
+ * Copyright (c) 2014 The xterm.js authors. All rights reserved.
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+ * https://github.com/chjj/term.js
+ * @license MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * Originally forked from (with the author's permission):
+ * Fabrice Bellard's javascript vt100 for jslinux:
+ * http://bellard.org/jslinux/
+ * Copyright (c) 2011 Fabrice Bellard
+ * The original design remains. The terminal itself
+ * has been extended to include xterm CSI codes, among
+ * other features.
+ */
diff --git a/dist/systemd/terminal.css.gz b/dist/systemd/terminal.css.gz
new file mode 100644
index 0000000..b13bd67
--- /dev/null
+++ b/dist/systemd/terminal.css.gz
Binary files differ
diff --git a/dist/systemd/terminal.html b/dist/systemd/terminal.html
new file mode 100644
index 0000000..2872e4a
--- /dev/null
+++ b/dist/systemd/terminal.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html id="system-terminal-page">
+<head>
+ <title translate="yes">Terminal</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="terminal.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="ct-page-fill" id="terminal"></div>
+ <script src="terminal.js"></script>
+</body>
+</html>
diff --git a/dist/systemd/terminal.js.LEGAL.txt b/dist/systemd/terminal.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/systemd/terminal.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/systemd/terminal.js.gz b/dist/systemd/terminal.js.gz
new file mode 100644
index 0000000..aa1b85a
--- /dev/null
+++ b/dist/systemd/terminal.js.gz
Binary files differ
diff --git a/dist/users/index.html b/dist/users/index.html
new file mode 100644
index 0000000..71282a6
--- /dev/null
+++ b/dist/users/index.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html id="users-page">
+<head>
+ <title translate="yes">Local accounts</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="users.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+ <script src="users.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="page">
+ </div>
+</body>
+</html>
diff --git a/dist/users/manifest.json b/dist/users/manifest.json
new file mode 100644
index 0000000..4ee89f2
--- /dev/null
+++ b/dist/users/manifest.json
@@ -0,0 +1,19 @@
+{
+ "menu": {
+ "index": {
+ "label": "Accounts",
+ "order": 70,
+ "docs": [
+ {
+ "label": "Managing user accounts",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-user-accounts-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["user", "password", "useradd", "passwd", "username", "login", "access", "roles", "ssh", "keys"]
+ }
+ ]
+ }
+ }
+}
diff --git a/dist/users/po.cs.js.gz b/dist/users/po.cs.js.gz
new file mode 100644
index 0000000..6793b81
--- /dev/null
+++ b/dist/users/po.cs.js.gz
Binary files differ
diff --git a/dist/users/po.de.js.gz b/dist/users/po.de.js.gz
new file mode 100644
index 0000000..e26e08d
--- /dev/null
+++ b/dist/users/po.de.js.gz
Binary files differ
diff --git a/dist/users/po.es.js.gz b/dist/users/po.es.js.gz
new file mode 100644
index 0000000..fe82ee3
--- /dev/null
+++ b/dist/users/po.es.js.gz
Binary files differ
diff --git a/dist/users/po.fi.js.gz b/dist/users/po.fi.js.gz
new file mode 100644
index 0000000..d3f5c37
--- /dev/null
+++ b/dist/users/po.fi.js.gz
Binary files differ
diff --git a/dist/users/po.fr.js.gz b/dist/users/po.fr.js.gz
new file mode 100644
index 0000000..7bdf614
--- /dev/null
+++ b/dist/users/po.fr.js.gz
Binary files differ
diff --git a/dist/users/po.he.js.gz b/dist/users/po.he.js.gz
new file mode 100644
index 0000000..4e62ce5
--- /dev/null
+++ b/dist/users/po.he.js.gz
Binary files differ
diff --git a/dist/users/po.it.js.gz b/dist/users/po.it.js.gz
new file mode 100644
index 0000000..0f4f4d4
--- /dev/null
+++ b/dist/users/po.it.js.gz
Binary files differ
diff --git a/dist/users/po.ja.js.gz b/dist/users/po.ja.js.gz
new file mode 100644
index 0000000..d96f97d
--- /dev/null
+++ b/dist/users/po.ja.js.gz
Binary files differ
diff --git a/dist/users/po.ka.js.gz b/dist/users/po.ka.js.gz
new file mode 100644
index 0000000..f4eb7f0
--- /dev/null
+++ b/dist/users/po.ka.js.gz
Binary files differ
diff --git a/dist/users/po.ko.js.gz b/dist/users/po.ko.js.gz
new file mode 100644
index 0000000..432353f
--- /dev/null
+++ b/dist/users/po.ko.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.cs.js.gz b/dist/users/po.manifest.cs.js.gz
new file mode 100644
index 0000000..3ce80a5
--- /dev/null
+++ b/dist/users/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.de.js.gz b/dist/users/po.manifest.de.js.gz
new file mode 100644
index 0000000..f4185bf
--- /dev/null
+++ b/dist/users/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.es.js.gz b/dist/users/po.manifest.es.js.gz
new file mode 100644
index 0000000..d7667f7
--- /dev/null
+++ b/dist/users/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.fi.js.gz b/dist/users/po.manifest.fi.js.gz
new file mode 100644
index 0000000..1567629
--- /dev/null
+++ b/dist/users/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.fr.js.gz b/dist/users/po.manifest.fr.js.gz
new file mode 100644
index 0000000..8c523d9
--- /dev/null
+++ b/dist/users/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.he.js.gz b/dist/users/po.manifest.he.js.gz
new file mode 100644
index 0000000..fa3adbb
--- /dev/null
+++ b/dist/users/po.manifest.he.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.it.js.gz b/dist/users/po.manifest.it.js.gz
new file mode 100644
index 0000000..3f0515c
--- /dev/null
+++ b/dist/users/po.manifest.it.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.ja.js.gz b/dist/users/po.manifest.ja.js.gz
new file mode 100644
index 0000000..36213c2
--- /dev/null
+++ b/dist/users/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.ka.js.gz b/dist/users/po.manifest.ka.js.gz
new file mode 100644
index 0000000..88543b9
--- /dev/null
+++ b/dist/users/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.ko.js.gz b/dist/users/po.manifest.ko.js.gz
new file mode 100644
index 0000000..69e4c16
--- /dev/null
+++ b/dist/users/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.nb_NO.js.gz b/dist/users/po.manifest.nb_NO.js.gz
new file mode 100644
index 0000000..448e614
--- /dev/null
+++ b/dist/users/po.manifest.nb_NO.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.nl.js.gz b/dist/users/po.manifest.nl.js.gz
new file mode 100644
index 0000000..375085b
--- /dev/null
+++ b/dist/users/po.manifest.nl.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.pl.js.gz b/dist/users/po.manifest.pl.js.gz
new file mode 100644
index 0000000..1c48ea8
--- /dev/null
+++ b/dist/users/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.pt_BR.js.gz b/dist/users/po.manifest.pt_BR.js.gz
new file mode 100644
index 0000000..6587e74
--- /dev/null
+++ b/dist/users/po.manifest.pt_BR.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.ru.js.gz b/dist/users/po.manifest.ru.js.gz
new file mode 100644
index 0000000..917f669
--- /dev/null
+++ b/dist/users/po.manifest.ru.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.sk.js.gz b/dist/users/po.manifest.sk.js.gz
new file mode 100644
index 0000000..206f276
--- /dev/null
+++ b/dist/users/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.sv.js.gz b/dist/users/po.manifest.sv.js.gz
new file mode 100644
index 0000000..41ca6a5
--- /dev/null
+++ b/dist/users/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.tr.js.gz b/dist/users/po.manifest.tr.js.gz
new file mode 100644
index 0000000..3d45bee
--- /dev/null
+++ b/dist/users/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.uk.js.gz b/dist/users/po.manifest.uk.js.gz
new file mode 100644
index 0000000..a457473
--- /dev/null
+++ b/dist/users/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/users/po.manifest.zh_CN.js.gz b/dist/users/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..1acda27
--- /dev/null
+++ b/dist/users/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/users/po.nb_NO.js.gz b/dist/users/po.nb_NO.js.gz
new file mode 100644
index 0000000..d9d0276
--- /dev/null
+++ b/dist/users/po.nb_NO.js.gz
Binary files differ
diff --git a/dist/users/po.nl.js.gz b/dist/users/po.nl.js.gz
new file mode 100644
index 0000000..881e27a
--- /dev/null
+++ b/dist/users/po.nl.js.gz
Binary files differ
diff --git a/dist/users/po.pl.js.gz b/dist/users/po.pl.js.gz
new file mode 100644
index 0000000..6650c3d
--- /dev/null
+++ b/dist/users/po.pl.js.gz
Binary files differ
diff --git a/dist/users/po.pt_BR.js.gz b/dist/users/po.pt_BR.js.gz
new file mode 100644
index 0000000..7f3f76b
--- /dev/null
+++ b/dist/users/po.pt_BR.js.gz
Binary files differ
diff --git a/dist/users/po.ru.js.gz b/dist/users/po.ru.js.gz
new file mode 100644
index 0000000..63a2744
--- /dev/null
+++ b/dist/users/po.ru.js.gz
Binary files differ
diff --git a/dist/users/po.sk.js.gz b/dist/users/po.sk.js.gz
new file mode 100644
index 0000000..552ee09
--- /dev/null
+++ b/dist/users/po.sk.js.gz
Binary files differ
diff --git a/dist/users/po.sv.js.gz b/dist/users/po.sv.js.gz
new file mode 100644
index 0000000..ea27175
--- /dev/null
+++ b/dist/users/po.sv.js.gz
Binary files differ
diff --git a/dist/users/po.tr.js.gz b/dist/users/po.tr.js.gz
new file mode 100644
index 0000000..6bd328e
--- /dev/null
+++ b/dist/users/po.tr.js.gz
Binary files differ
diff --git a/dist/users/po.uk.js.gz b/dist/users/po.uk.js.gz
new file mode 100644
index 0000000..6649452
--- /dev/null
+++ b/dist/users/po.uk.js.gz
Binary files differ
diff --git a/dist/users/po.zh_CN.js.gz b/dist/users/po.zh_CN.js.gz
new file mode 100644
index 0000000..055b5a3
--- /dev/null
+++ b/dist/users/po.zh_CN.js.gz
Binary files differ
diff --git a/dist/users/users.css.LEGAL.txt b/dist/users/users.css.LEGAL.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dist/users/users.css.LEGAL.txt
diff --git a/dist/users/users.css.gz b/dist/users/users.css.gz
new file mode 100644
index 0000000..534daa5
--- /dev/null
+++ b/dist/users/users.css.gz
Binary files differ
diff --git a/dist/users/users.js.LEGAL.txt b/dist/users/users.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/users/users.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/users/users.js.gz b/dist/users/users.js.gz
new file mode 100644
index 0000000..4f19c20
--- /dev/null
+++ b/dist/users/users.js.gz
Binary files differ
diff --git a/doc/Makefile-doc.am b/doc/Makefile-doc.am
new file mode 100644
index 0000000..933dac7
--- /dev/null
+++ b/doc/Makefile-doc.am
@@ -0,0 +1,14 @@
+EXTRA_DIST += \
+ doc/cockpit-transport.svg \
+ doc/cockpit-transport.png \
+ $(NULL)
+
+if ENABLE_DOC
+
+render-doc-images:
+ inkscape --without-gui --export-area-page \
+ --export-width=1280 --export-height=960 \
+ --export-png=$(srcdir)/doc/cockpit-transport.png \
+ $(srcdir)/doc/cockpit-transport.svg
+
+endif
diff --git a/doc/cockpit-transport.png b/doc/cockpit-transport.png
new file mode 100644
index 0000000..aed6cfc
--- /dev/null
+++ b/doc/cockpit-transport.png
Binary files differ
diff --git a/doc/cockpit-transport.svg b/doc/cockpit-transport.svg
new file mode 100644
index 0000000..6fa856b
--- /dev/null
+++ b/doc/cockpit-transport.svg
@@ -0,0 +1,1562 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="640"
+ height="480"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="cockpit-transport.svg"
+ inkscape:export-filename="/data/src/cockpit/doc/cockpit-transport.png"
+ inkscape:export-xdpi="180"
+ inkscape:export-ydpi="180">
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="1"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1.4142136"
+ inkscape:cx="306.90461"
+ inkscape:cy="250.54492"
+ inkscape:document-units="px"
+ inkscape:current-layer="use6310"
+ showgrid="false"
+ inkscape:window-width="1549"
+ inkscape:window-height="833"
+ inkscape:window-x="1862"
+ inkscape:window-y="94"
+ inkscape:window-maximized="0"
+ showguides="true"
+ inkscape:guide-bbox="true">
+ <sodipodi:guide
+ orientation="1,0"
+ position="24.999999,243.49999"
+ id="guide4015" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="60.999998,24.999999"
+ id="guide4017" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="57.275648,454.66965"
+ id="guide4047" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="613.06156,381.13054"
+ id="guide4049" />
+ </sodipodi:namedview>
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient4054"
+ osb:paint="solid">
+ <stop
+ style="stop-color:#669b00;stop-opacity:1;"
+ offset="0"
+ id="stop4056" />
+ </linearGradient>
+ <marker
+ style="overflow:visible"
+ id="EmptyTriangleOutS"
+ refX="0.0"
+ refY="0.0"
+ orient="auto"
+ inkscape:stockid="EmptyTriangleOutS">
+ <path
+ transform="scale(0.2) translate(-3.0,0)"
+ style="fill-rule:evenodd;fill:#FFFFFF;stroke:#000000;stroke-width:1.0pt"
+ d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z "
+ id="path4212" />
+ </marker>
+ <marker
+ style="overflow:visible"
+ id="EmptyTriangleOutM"
+ refX="0.0"
+ refY="0.0"
+ orient="auto"
+ inkscape:stockid="EmptyTriangleOutM">
+ <path
+ transform="scale(0.4) translate(-4.5,0)"
+ style="fill-rule:evenodd;fill:#FFFFFF;stroke:#000000;stroke-width:1.0pt"
+ d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z "
+ id="path4209" />
+ </marker>
+ <marker
+ inkscape:stockid="EmptyTriangleOutL"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="EmptyTriangleOutL"
+ style="overflow:visible">
+ <path
+ id="path4206"
+ d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z "
+ style="fill-rule:evenodd;fill:#FFFFFF;stroke:#000000;stroke-width:1.0pt"
+ transform="scale(0.8) translate(-6,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="EmptyTriangleInL"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="EmptyTriangleInL"
+ style="overflow:visible">
+ <path
+ id="path4197"
+ d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z "
+ style="fill-rule:evenodd;fill:#FFFFFF;stroke:#000000;stroke-width:1.0pt"
+ transform="scale(-0.8) translate(-6,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow2Send"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow2Send"
+ style="overflow:visible;">
+ <path
+ id="path4079"
+ style="fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;"
+ d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
+ transform="scale(0.3) rotate(180) translate(-2.3,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow2Mend"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow2Mend"
+ style="overflow:visible;">
+ <path
+ id="path4073"
+ style="fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;"
+ d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
+ transform="scale(0.6) rotate(180) translate(0,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow2Sstart"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow2Sstart"
+ style="overflow:visible">
+ <path
+ id="path4076"
+ style="fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
+ transform="scale(0.3) translate(-2.3,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow2Mstart"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow2Mstart"
+ style="overflow:visible">
+ <path
+ id="path4070"
+ style="fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
+ transform="scale(0.6) translate(0,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow1Sstart"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow1Sstart"
+ style="overflow:visible">
+ <path
+ id="path4058"
+ d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
+ style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt"
+ transform="scale(0.2) translate(6,0)" />
+ </marker>
+ <marker
+ inkscape:stockid="Arrow1Lstart"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow1Lstart"
+ style="overflow:visible">
+ <path
+ id="path4046"
+ d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
+ style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt"
+ transform="scale(0.8) translate(12.5,0)" />
+ </marker>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath3970">
+ <rect
+ style="color:#000000;fill:#333333;fill-opacity:0.45490196;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect3972"
+ width="132.85715"
+ height="148.57143"
+ x="254.14285"
+ y="892.07648"
+ rx="6"
+ ry="5.9999995" />
+ </clipPath>
+ <marker
+ inkscape:stockid="EmptyTriangleOutLr"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="EmptyTriangleOutLr"
+ style="overflow:visible">
+ <path
+ id="path8661"
+ d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z "
+ style="stroke-width:1.0pt;stroke:#e40000;fill:#e40000;fill-rule:evenodd"
+ transform="scale(0.8) translate(-6,0)" />
+ </marker>
+ <marker
+ style="overflow:visible"
+ id="EmptyTriangleOutM7"
+ refX="0.0"
+ refY="0.0"
+ orient="auto"
+ inkscape:stockid="EmptyTriangleOutM7">
+ <path
+ transform="scale(0.4) translate(-4.5,0)"
+ style="stroke-width:1.0pt;stroke:#e40000;fill:#e40000;fill-rule:evenodd"
+ d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z "
+ id="path9483" />
+ </marker>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath9740">
+ <rect
+ style="color:#000000;fill:#4d4d4d;fill-opacity:1;fill-rule:nonzero;stroke:#e40000;stroke-width:0.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:0.49999997, 0.49999997;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect9742"
+ width="126.74892"
+ height="116.84943"
+ x="256.97784"
+ y="896.22852"
+ rx="6"
+ ry="5.9999995" />
+ </clipPath>
+ </defs>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-572.36218)">
+ <g
+ transform="translate(206.00002,47.75)"
+ id="use6310">
+ <rect
+ style="color:#000000;fill:#333333;fill-opacity:0.45490196;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect6314"
+ width="160.61426"
+ height="210.4968"
+ x="242.80722"
+ y="603.85443"
+ rx="6"
+ ry="5.9999995" />
+ <rect
+ style="color:#000000;fill:#d4eeff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect6316"
+ width="95.838203"
+ height="29.529198"
+ x="-397.62497"
+ y="736.70099"
+ rx="6"
+ ry="5.999999"
+ transform="scale(-1,1)" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text6318"
+ y="754.59943"
+ x="354.4032"
+ style="font-size:40px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ style="font-size:10px;text-align:center;text-anchor:middle"
+ y="754.59943"
+ x="354.4032"
+ id="tspan6320"
+ sodipodi:role="line">cockpit-bridge</tspan></text>
+ <g
+ id="g6336"
+ transform="translate(0,-1.4061279e-5)">
+ <g
+ id="g6338">
+ <rect
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect6340"
+ width="148.4924"
+ height="51.299866"
+ x="248.49753"
+ y="610.26562"
+ rx="6"
+ ry="6" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text6342"
+ y="628.33929"
+ x="389.69452"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:'Bitstream Vera Sans';text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none"
+ xml:space="preserve"><tspan
+ style="font-weight:bold;font-size:10px;text-align:end;text-anchor:end"
+ y="628.33929"
+ x="393.17108"
+ sodipodi:role="line"
+ id="tspan6348"> System APIs </tspan><tspan
+ style="font-size:9px;text-align:end;text-anchor:end"
+ y="639.84253"
+ x="389.69452"
+ sodipodi:role="line"
+ id="tspan4482"> systemd, journal, realmd</tspan><tspan
+ style="font-size:9px;text-align:end;text-anchor:end"
+ y="651.09253"
+ x="389.69452"
+ sodipodi:role="line"
+ id="tspan4496">NetworkManager, storaged</tspan></text>
+ </g>
+ </g>
+ <rect
+ ry="5.9999995"
+ rx="6"
+ y="773.76562"
+ x="248.49753"
+ height="33.279198"
+ width="150.32707"
+ id="rect6350"
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.49999997000000002;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;opacity:0.5" />
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:'Bitstream Vera Sans';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.5;fill:#000000;fill-opacity:1;stroke:none"
+ x="322.82062"
+ y="792.88324"
+ id="text6352"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan6354"
+ x="322.82062"
+ y="792.88324"
+ style="font-size:10px;text-align:center;text-anchor:middle">cockpit-ws</tspan></text>
+ <rect
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.49999991;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect6356"
+ width="43.434963"
+ height="39.789604"
+ x="-293.13898"
+ y="726.26562"
+ rx="5.999999"
+ ry="6"
+ transform="scale(-1,1)" />
+ <image
+ y="597.6925"
+ x="220.27875"
+ id="image6358"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzsvWmsbNl13/fb+5yaq27Vnec39etms1vNSWQoimJEiqREKo6DOCZsK4CCIEhgiVYCITEMB/nQ +SDJEhQqgSwESkAHtoMYSAIkQeAokW14ICiJQzd7Yvfrfq/fdO+78626NQ/n7L3zYe8zVN26byBp sV/ft9D97qkzD3v991r/tfba8ESeyBM5tyJ+3DfwRP785Otf/3rl+Lj56VDx5Uze/3KlPLM+6PW3 uv3+TWH0keeLHaW4hWZLCHalzO73+8d7L774ov5x3/sT+dcjTwDgfSxf+cpXvI9//FMfDbX5vDH6 5yszlU+trK4WNjcvsryygkCAEGil6HTaDHoDeoMeJycNBv0+w+HAdDu9fhAM94Iw2A1Ds+1JdVcL dnUod4VWt2Qpu7s2O3v8y7/8y90f9/M+kUeXJwDwPpPf+Z3fWRmF8ouhCr+Qz+e+uLq6trK6ti42 NjbJ5LKgDVprjDEYY9xREs+DqDkIIRDCNQ3h4UtBEAaEoaZ50qDX69Hrdej1+3RabQaj/lAF6qTX 697U2hxqo3cF5raW3p2cYjfIqINRp3MTGD2xJt5b8gQAHnN58cUXs7lS9WdGo9Evep74/PzcwodX VtfE2to68/PzKK3RWkOs8JOfXKT+nbI5tUIIgRQCpLRrhYiPFkLgSUmv16Xf79Mb9GmdnNDr9RgO B3Q7XTq99qEK1f5oFOwguCM8doRiV/rebTUMdrPZ8t7f/Ju/cvAjf0lP5Ex5AgCPn4jf/u3fezZQ 4S9oHX6xUCp9ZnV1vby5uSmWl1cQUqC1QSuDERpcJ59S4+knnVwvzt53bP+xXaRdJQQyNiAEAgkI pBRYo0PTPGnS7XXp9XoMBgParSaDQV+HYRAOer13e/3hPYQ6NlrcFoI70vN3UBxK6d/c2rrW+P3f //3hQ76vJ3IfeQIAj4H85m/+5qLC+4zR+sv5YukXZmu1jbX1DbGyukq5VEa7Xt5orNJPVfcHferx 7eJsk+DUMdN3PX2cBQ2BQIPnxXskLodECBBSMgpGBMMRvV6XdqtJp9thNBzS63XodrrdIAgbozDY CYfD21J6u9roXel5t5Wn7ngjcXR8vLD7u7/7hJd4kDwBgPegfOUrX/Fe+OgnflqH4ReE4Eu12bkP r6yt59bX1phbWABj0EZjlMEIA0bEPf20jtyc7t85vRfYk0Q8ABgzqdxnnWWasrt/z7zwaWgywoGE cX897e5cWpBAOCPDuSICBv0+nU6HbrfHYNCl1Wox6A8YDHo6VGa/1WzcM6E50sLclR630HJXSXGU 0erdarWw/dWvfrVL8vbOnTwBgPeI/NbXvnY16Ic/p3X4hUKh8PPLK6vVlZVVNjY3kVLGxJ3WEYcm 0voai1X4ZBl+MACYvkU8YPez3At7D2N7jJ1j+gkFAuOexi4zhikGLBAAQjj3w3N3aSTCs0AxHI7o 97u02k267S6j0ZBer0ur3UIp1RqNRrv9bv9d4ZsDIdg1Wtz1PXnLBNkjKdXu3/pbv7Yz9cHeB/IE AH5M8gd/8AflerP72VAFv+h5/ucXFxevLi+vyI0LFymXitaPH2Pr059qUlFOrzGxysGDP/M02Ji2 VUzX/bHDnekQbZh66bMtCafmFrTceccw4j7vYWxzfJAAtHMzJNJZEEIIpJSpmze0Wm36/R79fo92 u8Wg32cw6BsdhoNmu72twvAoDMMtPHHLl5mdYrb0z3/t1/7D1894mMdC/B/3DZwX+cpXvuJ95BOf +HA4DL5sDJ8fKv1Tl69cKWxsbjI/v4gQBmU0RgmCYIQ2kSkcyaSVak6tHff+09sexsKNlOn0vunz mAkIMG4xXmvM+FnMNHWfAJvIjcHYP/ZEyTIReTgtijHdQoj2F4IUpGgUJNGL2KARCCkoFAoUi6UY JLAgIQQUpCefVko9rY36VL/X59t/9l2+/Wff/DbwyVOP9xjJEwD41yi/8Ru/sRoa8QUdqi9lC/kv Li0uL65tbLC6skY2l3PEnUaFYWzqAqnGfh/VNZOLIrX8qC6tSf19gLWQIgaMMXHXfD94St/f1OtG yuqWY5fFTHueBDyEBKENGpBSgLYgKGOll6lchxR4iZRDIhyMOfQQaJSIlkFIaylkZYZWq823/uQ7 fPfb32M4HHL93Te37v+y3vvyBAB+hPKHf/iHxb29xme0VJ/3Pf/LM7XZ51bWV+Xqyhqzc3MopazC G8NoNEx840ip4kZoUkqTNP7EQHYypq/m0fV+yjWmSvo6JvXbkDL3IQGhs3vqcZGAtnkF2qAAKcFo c2pva8hLEE7LAaOS7Sq9HB0jkpwjIZLnFCKyFgzCJO9UCAcS0oYxfd9Da8Orr77BS99+mTu3tvAz Pr7v4XkevidSV3085QkA/DDy4ovyNzKZ5wJjvqA0v9Bo9z596enLldW1NVZWVkAk5N1wNEqUJfqT 2KrurzODo/ObcTUwqW1xc57sHB9Cn8fl1BlT50kp8sQ5jYnVF4iWUzuJCMJkcrAQoDUCSagVQii0 AoGyx46/ntTjp6ITqX9Eyo4/vTy+jxApIE12GFuWUuJ5Et/32d/f51t/9l1ee+X7KBUifY9cPufO J5FSY1zew+MsTwDgEeXFr31tzvT7XyDgyxlPfG5hde3CysqqWF1fp1AooLXCaFBKY0xIzNaP8dnO IDWTSjyt3ztl65+h32L6OacCQkKORX+sSW1sqrCQeEY75TZIJOn8XSEExmhCZcm1IFQIIdBKO6PA 3oT9Y+zfCW4gUjyrn+PLif6eodyOL5gGAPHmyJJKrecUGAikNE7xMwRBwMsvvcJ3v/M9DvYPyGQz eNLDz/iWJ4iOlwajBd7U7/B4yRMAeIC8+OKLefL+xwnEl7TkC7Vs/hNLm5fk+sYGCwsLKG17MWMU wWgEuP5qrDMd8/CTTn+Kco5Z27HSTJrUzlQ1kfbKmPcXwvawRoLUAu2yAW2YTI9dRRuDCe1xKhyC EARBiBASFVrw0toptbbRiFjBgbEeFhIFc8siYuQ4vWzSPXLsizMVDEx6eZqiCzCpl3n2PTngkCCF h5SSTCbDuzdv8vJ3X+GtN99BSoHvud4+vq+0JQHSSIx8fwxpeAIAU+S3fu/3rgQD9SWjwy8UirnP Lq+szS4vL7O0sorvSbQBoxTD4UQ2asoEH2fOsaw2Se823QIgpeeW5ELbWLc9deSvOiWRzvd1vbHW CqUMoVIIBGEQYgAVKhACpeyYAK0M2kxQc/LsXvjsZRMn75jk4HFlS/XGU8FAOHg00fqUnTSxPHbe 9D3E693rT187ZS0IpFV6z6PT7fLSS6/y+qtv0Gy28X2PbDZzCrwmlV8gMM4CGMPkx1SeAADwd/7O H1YH+vhzWpkvZDL+l0qF0pULz14Q6+sbVKtVZyZrtAGtAaEx2kMrhTGgjUZra/OOu+1W6aWx7TBK WokbuwRpBEophIAgUC6PXxOGilBrdKAwwhCOLN8UhO5vECAMaCMwaCTSKoK09gCQNOR4mTPMbQHa JMpjUjkEIqXQxpxeJuVHTyhk0nunlT4NBslyfGS0LBIUnWZJTHuWyeeK7sNzvr0QkrffvsHLL73K 3Tt3kdL6+7lsxh4jU9/mDOW3J5Z23/cBApxbAPjtr33t3xj1g8+P1OjLMj/4+NW1ZwrrmxvMzs0h jEQZgzGKdrtje1+JSyQReMJDSkEmkyWTkUgvgycFRiuElAShTWHVWjMahfS6fVonHRqNFkBsZo+G I2TsQ9vz2yblElZSjQ+4T28MOuXP67ihJgAQM98pJRvvYWO7YkzBhDndw55Stgj1HmAtjFsIU5Yn 7nkSDCaXmULsxdaCEPi+j+d5nDQavPTSq7z5xjUGwwG+55PJZFx6cepe76P8pNYjDUK/H9T/HAHA iy++6MtS6a+KMPgL5fLMF+bmZ+eWV1bFxoULZHwfbQxaKZTS9IYDNNBs9pDC0OoO0AZ6vQGjUNEf jhiNAsLQ0O310Rr6QcDcTIW5aoXaTIW5aplatcJMpcj6xgLPPlsEoN3p0e106XZ69Lo9ev2BXe73 GAxGaKUQnkS6AJMQnktMgfHGONlLp0NcE8tpxRK40NfpntddYToYuGub1LXHrIX7WAhpMEhCcJPW QrQc3wUP40ZEfyNewpMenifBwOuvfZ/vfe819vYOyGZ8pGd9/ug+JrmCU0rv7nvyWtJIjNCIdGjh MZVzAwDtQPy19dLc31/dvCzyhQLHJ22u73R5+e3vMgwVQaDoDYYIIQiVsmZ03EhO90pWAyKT3/ZK 9XqTw3oDrQ3GMeJK20E7Gd+jVq1QqRSZm6lQq1ZYmq+ysLLIlVKZcqWIJyXD4YBOu0+/36PV7tgq Pf0+g+GIYX9A6Bh3IWQqJJXKGxBnm+lppR97ltTy+D4T1kIKROJ3cgogOG0tpIDhYa2F027EpOWQ WBRSCKQv8T2f3Z1dXn/9Ld588xrGGHzPI5/Ljn23MUVPPXta6c9SfssBwGgwxMCdR2uF7z05NwBw eNgszi0/Jdo9aHW6gCQrC9Rm8gxHQ8IwpFJUBGFIqBSjMGQ0HGImzcSUmNS/OEJMIhBSuN4R2xv5 todqtNs0mm1u6x2UAbTC8nIaKSSlYp652RnmZipUq2XmqhVmazMsb6ywUKvhScEwCOh0evR7ffr9 Pv3ekH6/z3AwpNvrp2Lg1o2IQCLurFLWwvhyGtimLDNhUXAfa2Eaez/5Dh9leQxE7D9S2PfsSY9g OOTVl9/krTffpl5vkPF9fM87pbjTevn0s0X3P+5OTLEAHPhKYUZnt7jHQ84NACDdxyVFugvwhcAv FKKfpDQIIQRBEBAoRRgEDIOAUFmQGIUhoyCwRCCuZxPGhqNMBA0uHh71zxEZKKV1X6XAMwZjPMAw GI7Y2jlg696BJRaVwaBRCrRWlEtFZqsVZkpFZqtllhbmmK2VWKytMFurUCkWGQyHDPoD+v0Bg36f Xt8CxGgYMBwNURHfgIwz3tzDcsqNmLQKouT8B1kLE5bDaeU+w1KZYi2k8wYMID0PT1pC7+6du7z+ xlvcevc2Qgp8z7dMvhhXWIFw6cHTSb00kE1T/jEOIH5PgveD+jz+T/BDShKicwAwkX0npSTveZDN UoExgMANzx2FIcMwYDgcWcAIQkYqZDgYEipl4+dR2qkxzqoYjxhEy9IpnyckUhiMkfieBYkwCNg/ qrO7fwgGlHGFQJS1MKQnWJitsTBbpVIuUKtWrLsxO8t8tUqhaGsCDkcB/V6PwWDIYDBkNBoyHAaM hgGhCm1evX34KLbwUNZCmluYRuaNJe88hLXg9kRIi95Zz6PV6fL6a2/y1rV36PV61uf3fRDjORTR txWp5ehmTpF6k8qfAoHILYh/g40CCIDHPxfg3ACAUNmjfq93ar0B8rkMF1fnabR6nLR7DIYjjDZE HIBtJG55AiAQglw2Sy6bRZTKgFXiKPSktWYYBATBiMEgYDAaMhyN6PQGdPsDlFJujL/r/YQ4Nfw3 4hkM9vpSSIww+MbDSIk1IOz2RrPFUf0EjcG4KkHKpSP7vsdMuUi5VGK+VmFhtkqtOsNcrcTy3ALV ahmJYNgfMBgNGQUBgbMcBg7cjFIYkST4SinHSEe7cNpyGLciziDzYoDAWkfCxu2lFFy/fpPXv/8W 97Z3kb6HLz1834+vEx0f5xsaEUcVGOv9k+Ss6CbG/k2BQBqgYuAQwrlTaah5fOXcAIARuq71+NgN Y2BpfoafuLLBu1v7PH91k82NRTIZ6PaGNDs9Gs0une7Isvc962d3usNxcMA4Ys64SjW2USqt8aWg lM/jlQsIYVlq4Rq350mUUgyDgH5/QG8wpN3p0u4N6fV6tDs9esOhbczGYKQDCSNdZZwoVm7iBzLG jowTRtj9JXhGxiDS7vRotXps7+yhNKAVmih12TBTLrEwO0OlXKRaKTNXqzBbrTJXm2OuVsHzbLmu 0TAgGI0YDoeEoWI0GhEqRRgGznKY7P3HlyNFGwvrgTXlpR1sU2+c8Nrrb3Lt2juEocL3PRe+S3/X 8V5epBbSGZTCJWJFA4AYA4dxwy6R6L4niMGYgH385dwAwDRRWvO5n36eX/6V3+fouM2VK5v8/Bc/ DRgqpRyVco5i0adcKbC0uEg+K/Fzkowvabd7dAdDOt0Bvd6QXm9Evz9kOBwRKo3RBk8IjBFIz5ro Qki0MUgNWtrogCcExVyeSqmI53xMz/fxpIgtiG5vQLvTpdMd0Op2aXf6NDsdTpodWm1n1cQMe9rv jqwJIO1yCJBIhGcw0sdzbLkxhuFwxN29A1DYWoOA0cryEEZRyOWYq81QLZeYm60wW51hrlphplxk caHCTKnEaDS0IDEKCUNFEIwIA0WoQ5dijCvGkdyPNeUlQaC4du0Gr7/xJkdHdfyMj+cSduxTxIc4 i4iUNTHZL4tTxzjjICXWejBj50nAKU39Jud/7KN/sZxrAMCAJwXZTIZ8LkM24xENG+0PLfl30vHw fYnvS5c5Jsn6HtmsRy6XpVQuMFMTZCQIzzYNpUJGo5DBYMBwGDAYhIRBSDAKCUaBZbBFwmTbS2o0 dp3WCmNsdV8hDOVSnupMCSls7+5L3yYk+TYbsdXp0mp3aXcHNNptjhtt6vU29ZMW3W6fYaCSSjgG O5jFiDi/wBoHSUOXxqa7ekIijQWJiIfQRnNUP2H/sI65ZeKKRdGAn4znMVerUqkUmJ2pUJspM1ub YXGuRq0yQ6mYR0ibphyGli8ZjEZsb2/z+hvXuHnrtnNXLKE31tWme37SyjipwOP+/2lgSD23WxAR gQtEbG1EFeDqHowByftEzhUATDPbtEmIHGMEYahII7yPQAuIUr+tTy0YBaCNIQi1HRvuR0NJPXw/ Rz4P+VwZhU0TNkYRqJCjvSO6zZ6NXcsICGzPH/m7UZKKECb+bRxACG3r2mgjUFojBdQqFeZqM3jS Ema+J/GcnxyGik6vT73R5rjR5OD4hGa7x+HxCYf1FofHJ2gFWS+DnG2jT0qYwEOLELyosbvEF7Dc CMYRhdNV4aTdpt5qcWd7LwYGg0Frg+/5VMp5ZqszeH6GpaVlavNLfONf/Am9VhPPmzLGboLgG1+O XIpkG9F2R7hGCpxmC+J9xmDELpk0hEQbUic2CVEx9fkfJzlXAKD1adNNRf6gsPF4pTRpMirJGIt+ AxONLmGW7Topbe8qpIdnJJ5nt+ezArWoGXaGRLyBdOdPK3+yzoJCdA+eS0VGJCAhpcSGH0FpkGhC 96yhsCG/SrFItVLi6ctrSCHwfTsSzvPtvbXqAf/1jV/CXGiiggDTybL5zi/y7jd9mq06g/YOQmYR fh6lPaTnu/tKK4AZi6BEXEj8W9rnGoWK61uH7Lxyi3a3zy9+7lP87OISvufH8wYIZ6ubiOKbIPRS V4y/x2TvPHUOlCnbk2FWqX2iE6fcBRPtawRGawbDIaD3Tl/h8ZJzAwD9fu/2oN89bQZoHQ+LN8YQ hpr0ePHxqjKk1jMGEuP7KOcy6NiliMN80kdkJIT6oZVfCpt8klgHbt2ExRAdI9MAISRRFp9SCiMl OnCWRSiQnmRupgTZkPm5eYQQlMUsf/sn/jb8VfiPf+VXeefOKlKC0m3++q/+m2wdjHj9zWvcvfU6 nueDl0UpD78wjyc9cM9h78e+m0Z7wM5xm3q7TxRey2T8VJFO+zfSxITQGwfgU3odEQEpYs+I0585 2jceNZwmBNw6p/PuC6f2ca5T+rcxGiFoTbnKYyXnBgDmRXVq1pYy43V2lEoAIN37jyv/WHmM+HcC Coli2x4taY3aaDysGTlN+WVq0NGYoouU8svJdcl5JpU/WUcCKNG5pUA6cBLGWg1SCEIUo6FCCE2o FYXcRTy/glaGv/aX/21CZfjD//Hr7LxjKHlX6Xfr/Be//lG8fJbdwwYHR02O6k3evbvDtduH7DU6 hMo4IJvuNug4oOHM9cgCn+bXR3ob7WOS7xMb+SnddoekSIFxcs/EPn46QmDi72gcyETbhMAlHWWn PsvjJOcGAHpAcXLlRFtUSsclvGBaz3563Wn3YPzEmcxpEBBCglBTlT8x+c9Wft/l+P7Qyi8c9+B7 KRAE0HFVHwzkckX8TBGtDMNgQBh4KK3IZouUiotIOculy1dZXS1Y4hJBNuvzd//h/8f/8yf/O/lc Jsk4PEMmvelJtj9i68fXpV308dh/DBCkOvkIKFLAERkd47xCyvGPTpRCHKVsuLTXzt//oR4DOTcA ME0E0Ook+fPGGIJg3AWA6b3/uPJP8gRibB/f910Y0DYqIQziB1R+74dU/oR8TF1vSljLGIEUGoMi VyyT9SooZUCD0RqNJuPnyeZLaAK00gRBgHJZiVorV/fvAZo/+U3idxd/FGebi1hp9RRtjkN3p0Ai CvMlyUKIhGuYNPkja4jJbdH5DHi+R8b3eewrgnKOAMD2/uNkDwJGYfIZjTFnugD297TlSZ5AEKWI JvsoMhkZN2opPHD8wHTlFxNKnSi/ED8q5U+O8Vw8MJ7QExBoHB9KPlch689YklQrO3ZBGzLZPMXC DFKESOlhjPWNbVjQRiweVjTCKm+kmLFZnzLVgfG8v7QbMM70g0G4+uDjLkLqQOPAT7h8Ce3O6/wM IZLjSRkC5WKR4bDL+wEBzg0A6DmjVRiacdUlYZjd8iQAxPvGjVGf6u0T94Azlu1CNhtNXwWocaIs UXSB7w5Or/Mm9kkrdUIeTir6tHVpsMElKxlyXoHOSYfKfIWszqMMNnvOGKTwyPhZwmimIiFs0ZH4 f1yoM1J+QFgr4eHF+hupFCbLuIs4FhDDd5SGnPT1qbOkACS+fPp7AUImAYsIEBKAMPGgJxshjtKz E58iX8jT73ffD/r/PghkPqT8D//tb+4qrfqTfr8xmmi6PWNcKa7QhgOjv8n/auK33Se9fxiasW1K meQ80Vh+zlZ+789Z+REwDAf85bW/zu1bW9y6dYtL3Z8kNAEoQ7U6y+7BHzMchvS7PQwynp9wNOoy GHbpDzoueSmpj2B0Kq//kcRE/8Vznqb9fUNCGEYm/mkHZuxsRFEYg3MhdPQ7OXO0LX2d5BiLJsYY cplMyv54/CHg3FgAQBTMGZPJirs2A0/FPX66J48sgFNnPaPnj35HfwMXZZDSNqFpyi8eQvn9VC7A Wco/zi9MuAOerVOIAKUMQgT8ZO3n+PqHv8md/nU+WPoYIzVEhfCfffWr/Cdf/TVeufYav/RXfpkw fA6lNEsLS+wd/gOKuasMR75Nf3ZViiMXYFrexdnfJk3okY64neLiks8VheiihJ/Er5/EnoghSFsY kcuR8HtRzkFEKEZWSHKefD6PctOxP/7qf84AYJpoY/3dSISwqQFhaEfoTZr70T72/8hlmNwnTRQm bkNsyko7mu4HUX7vR6z8YNAaFAMyXp6rpRcYqSEajQkNc7Nz/B//6z9iMBiRzfoMByOEEPziL3yJ d2/e4o/++Gt89jN/gY2NjxMEYSotePy9PPA7wKnQXQQGkwodE6oRIBDtNBEXjEdXEvvwSUA/dcax w0zK3E+5HsbgSY9sNosxhsFwiNA0H/4J35ty7gHAGE3EVUXKZtfjsgKjbRB7nWMWAWP7nGUJRKKx E1EIo6cqf1rRz1pnLYgHKP8EESilsMBjnAntUnqTVF0wtrBAYh4bCMKQILTrBgObSqGVRumAv/Er v8J/+tVfjQf8pE1rI0TsWj1QUgRdrM8x2++UMiL2UjuPRehEclzMAYg0WEzS/q4+g+MYYisiJcYk IASQyWYx2qAEhGGIlKP9h3zC96w8AQAdEX6SRqNOq9WiWCzg+5lUb2atAiE0WosYGM5KFBp3E8ZD h0aapAefovzTfPgflfKLSPnNNOU3MeMdKb+e2C96XwZsOuxgkPj8KeAwxm5/2IIZAhLnLGUFpPvq RKGJmfp429hxpxyJ6LSkf0T9u3BnUcLExKNIuQIRBQmCQi5HqDWetKM6+w/1dO9tOVcAMBwOmgJK yRrbBIyyzUNrzcsvv4QQkMtlyeUKFAo5SqUShUKecrlEuVymWMzj+zYX3oa+EnJJa+Pch9NWgBS2 jr9VzB9c+U+b+GeQgxHJ6JQ/UeRpyp9W4BSZF+3nWLH0MaeUf+J8j5IDkJ6zKPbtHUOjJxEh2scI dDpJJ/YfkiG+Y51/bAREvX90rYgFFnHlJhFbH/Y437eVh+JEMWOg//irz+P/BI8gKgwOQKxFv4UQ 7Owe85f+vU/x9b/3x5gApPTwfRiNAoJAMRh0OTlpxP68ELbQZzabpVgsUqmUqVTKlMsFKpUSMzNl SqUiUnrO/7Sj26xLgRtmK/AFMNVfHwcBT6b8flIKPk3RJ3mBH0L54x7/B1R+SwI+2veJ9TvV20cK n3jjSZ+cPibq0XGKbw8aB4ekZyfl2zM2PiAmGp1LIIyNZmQzOTt/A9Yl0caQebTHe0/KuQKASREC traPeObqGn//f/rPabZ6HB21ODg64fioxcFBk0ajQ6PR5uigZevK+7anHw4DwrBFp9Nmb8/NT+/E 8yTlcpFyuUStNkO5XKRarTA3V6U8myVj58B+KOWXU5T/VJjvIZU/ZrWNfiTlj/ebXDdmIbigWrRd aIx4eASwiq+tVUXSWyf8feIKRNxA3NOLVI+ethLGlHqKpeAuIlInN+633TWKLggy+SzauBiCsQBw usDc4yfnFgAiZfJ8yfb2IXfvHjrFzVMp5lj6wAYfeuES2VzGVqXJejSbPfYPTzipd2g1e3RaXVrN Hif1Nt3uAOnZUtzGCHq9Af3+kKOjhjP5Lan4U595gY8+ezHu1SdN9jPdAfHDKb+Uhk57QLvXp1Ip UchnEdi89mi0YMLeu0lBf1Dlx+YB8AiZgP1BH4OwCUgiIf7EmFImjHzkW0X+f6rrJrIJrAXmlHxs t3HASP8WjmRI/85kMwgj0A440YZHiHC+p+X8AYCw2U/dTpdXr93h1s61AQRzAAAgAElEQVQ+tZky C7MzVGdsscxaucRctUwu64OQ1gyUAuFJZnyfuY1FvEsSL+vj+RKZ8VEa2u0OrZMenVaPdrtPs9mh 0+7ROumgQjtLrx2Lb3uVPy/lt8oqaTQ73Lm3x+rSAgC3bu/y8Q8/TWgMu3tHrK0sks1l6PV6VCpF a8JrjRbW6NbaDYPFxGTiVOU3BvMIU2cJIRiNRmPmPCYh+0gpMdF2kh9p/97uPl4IJAIra1CIxDIQ FqwtyKRBIfYhQFj2P1BhzGlEGZLvBzlfAOAaxUD4vFq+yubPrFDYPeRgZ4/r9/Y4fPktN8e9wfM8 Zsp5ajMVlhZnWZq1te+WF6rM1ypUCnn0MLQ9lrQTb5SEoDxbRi5Ukb4HnrT56NJjEAQM+kNm8zk8 pWMf/pRSe3I8R2DCSpim/GPuQ6z840Qe2EbuSY9Wu8u9nUOEFDS7fe5u73P1yjrSF3znpbeoVcv4 vken2+dDz19BGNjZPWZtdY7hUBOEI3K5fKJQbuLUMQJRmEcxAFJK7dyUFBmQBgX7HUmUmJRfnzI6 0ryA3S5ioIp2tiClE6RJg4C7FyEkvswQpTnbqI59zkwmfLT29x6UcwUAo9FoKxiNPmJEjlebFfZn ZvmpT36En38qY/PDgWY94Gj3gOOdPY72Dzk5OGZ7Z5dX3r5Dv9UBYdOGSoW8ndprtsLK4hwLtQrL izU2luZYmp9x8TZla/ChyaGZdXFk7peyy6Mrf3yeM5Q/sgIwcHzcZH1tkQ8+c5Fr1++yf1Ann83Q avcYDgPmZis89+wlvvlnbzA/N0Oz3aWQz3Nw2AAMO7vHlEp5NteXuXb9DlJKnr6ygefJOAQITs8e JRPQKVU0Z8KpgJ6z8JMxAZHJ7zamIvaWFyC2gqy+G9f5p33809xAZAlE8OH7GTS2RiPurmwpuIBR 1+/8UA3yPSDnCgCEoS9cL+tLQ6s74o++vcc/+57Hhy6X+djVMpVihtxT66xdWU/al+td+l3DydEx Rzt71Pf2ON7bZ29nj2svv03npGlLawOe57E0X2F9eYGl+SrrDhTWVuZZX6hRLhZiujtium2xTly7 u7/yT2P/hRhX/nEiLzq5lXs7R3SrRZaXagghHSeQQWtDp9vnT771OoVSnoX5KgdHTQr5PnNzM9zd PuSF5y6Ty/ncvnvAlUtrCAHbOwdcuriKVqlrqkfzk7UeN+1FTNBZUIhCd+nefyyNO4nx2bU6NhHG ttnOPpVWNEEdiMiScS5HJpMhVCaOOwghQBvCMESIwc7DP+F7U84XAHgST3rMeGH8QX0JRhteudHl 5Rtd1hbyvHCpyJWVHJ6t3BVVDUN6grmVBeZWF4CfsK3J6dWob6gfHtPY2+PkqE5jd4/jnT3e/f4d Ot94hVEYWnCQglKxwObKHJvrS6wu1LiwtsDqvP07Vy3je27Mf9yDE/usNgw1Rfk5W/kNYLQml81w 9fIG1WqRwTDg1u0dPvjsJd65vs1TV9bwPEG5XOSDH7jEt77zOtVqmRu379Htejz91Do7O4fc2z2k 1xtSLhfY268zGI6Ym62MWxzaYHyDeITRgAL7jscy8gyoCBxJk3UmrtuXjumLSPkdgTi+zVkV0ejC hEdM8QjJSmGiegw26SftfURRgMc+D5hzBgBWrwxIiU51HmBZek8IDpoB/+SVFp6QXFzOcXUtx6XF DJ60cXxlsHP2RSQbTtEQzC4sUFtYSFxK51aoANqNE+p7e7SOjqnv79PY2+e713c5+cb3CEI7vyBA sZhjaa7KpY1F1hZnubi+zMW1eS6sznNlY5mMb+cRRCTXRWs3QMWSdDCh/MYqwmy1Qq1mlTUMFeVS gXw+y8c+8jS37uxRLuXJ57KusIdAK0Muk6Xb65PLZSmXiywvzTFTKfHu7R0W5qsEQUi73QWsTx1V ADYatBA8rCSJQGLM14/N9DQFmFJqkQIBIQxGOuV2ynwq6SfV3UfuhIjdAMaIw0zWDnKKDAnizU9I wMdSjLaJPhk52TAF0ScWWKtACLh7OOLOUQgINhYyXFrIsrHgUczZ8JlyloF1sW2vkO6FE2JMkK+U WCs/zdrTTwOghYxJtO5Jk/r+Hif7h3TqdU72D7i2t8+337xLr9kkMkC1NmyuzLO+Os9Tm0usL1tQ WF+ucXF1gcU5W7UnVArtZh5WWqGVDWFpo2MlldKCTTRy78rFVQcWmiAI+fALT6F0yIWNJQbDEWEY 8tSVNW68e49hEFItl0BCqVRg76A+rvzu+R8mDSB2TCJLJ47tO1+eyFxP+/yno37Re455B+c+nLIS UmeMrIno1HbRcRBCIGUmjv2nRxFq95zvBzlXAICPwSgQqWKOQqBHPYSXI1uuOMIpYp+Ni0nD3oli tzFEvCso5QTrs5K1WcliNUPGx82cQ+wHj7d9Gxc3RjlgSMXQtUFmcixuXGBh86JtYLZsKAYIhkMa +/u0Dg9pHhzQqTfY3t/n+//qDQadDmE4so1T2DkKr2wscWlzkcvrS6wtzXJ5fYGNpTmW56tkszk7 Wak2KKUItZ2B2Ma3jRvMkxCIWgn8jE8l46GMrTb0zNULKG1BQkqBxrAwV7N5A2nSUfBQBUGMkLEL k1Zm2xmnGHrSZH3iBkQkXhIyjHaMfjvSMNXLS3d8DCrp49w6z5X8EpGZRyq1WSdk5+Mu5woAfCNv Dvp9asUcNV/T1zafv3nr++x94/8iN7tEfmGdwtImpdWLFJY28QsV7Bh3ew5hYDDS3NhVvH3PAH1m CoKlGclizWNhJkMhIxAxd2DQivv3F0anrAeDNnZYrXYDb2qLS8wsLrHx3PPY/HerBKPRkG7jhNbx Ia3DQ/rNE5qHh/zpG1v8k2+8aicetacnm8swXytzaX2ZyxsLrC/OcnFtgYvrC6wv1igV8naMuzJ2 4pEQAq2Q2s2dEIX6TBQmtaFPoSRzsxU7fDjNP2iTNqzOlAhkNWbC1E/etyEx9a1op6fC8QMTcX9D rPDjwBDlKrixGnFIL/UXCyq+79vJVcfuJSEmH6XWwXtZzhUASITyhDUFfaGJDFCtDIEWqGaDQbNB 8+Yb1g3UhuzcEqWlCxQ3rlBZuUp57SLaKFAKoUKM0XT6mmZXcX13iAoN+ZxgsSKZKUpmKxnmKj45 XxK6xJrQGDvAJTxtSJ5qVs5iiAFCu9+uty5WZ8hXKixdfopokmGNwKiQXqtF+/iIbv2Ybv2YzvEx b9074qW3bhEOBzGHoTXM18qsLM1ycW2Ri6vzrCzOcml1jrWFGhsrc4ShQmmDNnaeQKUVRgk0zrWY Fnl4UCKAgUIhTzRmP7I8xhl7t2+kxGP8QGTiR29OJIwpyZ84CygeB0AKEBJHQ7iLCqJJTyZ6egcy 1gV4/KcGh3MGANoHPDvpZtQ4lFJ88S/9PB/8j36G2zfucvv6He7d3eHWtRu0TzoER/v0jvYRb30H AXi5HKXli1QuPktl/SqltYvgZRChAhMgjCYINDt1xdaRRuseRhtyWcFCxaNS9JgpeMyUMhRzEl/Y QTNR6i2auDedJpOrtdYxGMR/jcEYTSZXoLa2QW11E422w4ExaKUYdrp0G8f0Gsf0my26Jw32Tuq8 +51rBP2+TarBRT+k4NL6EmuLNS6tzrM8X+XCygIXVmdZqFUoF/Px7MJKKUtqRhaTTLHtkyIMlXIR YrKSWLENiZlvUc25ZZGLIKJCH86MNwlGJPP8JXxAtBjRf2NIkLIejMFW/I3QlCT6EtUWiHiT94Oc KwDwsWafRMQfUAp4+e0T3ilkWJ57hqc+9wI/PZthsQZmpLl3e4vtm9vcu73D9p1tdrd2adx+h+Ob byEwSOlR2bxKefNpZi58gOLyBRePV0iCuFcJQ81OPcQc2V5cGYUEijmPcl5SKngU8z7FvKRcsBaD lMJyB9oQOgIPF7ac7H/MxA8Tmeux1aBtOW/3v5fxKS8uUZ6fR7s8d4ydsTgMRnTrdfqtEwatJsNW i1anzf61Lb71yvU4J95ogycl1UqJ1cUam6tzbC7P8dFnL/CxD160UVIjSZJ4x0Viq3OOTTEWc32T Q3Yjxx7SgBCVEkpchHS0gES5HQ8gzITFkBALkQmC5/muFkICKtEyziUJVaCy/eFjbwacKwDQ0k7g KT3BjK9oKy9G9k4/pHOvw/Wdnms0knzOY25mgbWLa/zET2T4bBUWZwBlONw5ZHd7j8PdQw73jmjs 77Dzr77DfigY5ReYffojVJ96zibtuOsL4lPbpBYD/WFId2CgbnvwSFGlBxlPUMx5lPIeGV+Qy0ry GY9izs6jl81KtDJIYeIZjpKsPxN3cMZom0xj4ggXYFwdBI12U4FjQjdmwZCvVcnPVNBcABdVMEA4 HDJotxj1ugw7HYbtNsGgz+3DJm/f2WMUKv6tz3yIjz932VoyURB/ihhhbBlzYU1qHXHt6SG7TomN SSksU/x20mQfidkRW/8xKzje+9uWEX8hKb3E0zAmVvqUEWFdD6UbL730Tx/7VIBzBQAedIPRAJgl J8cJHrAf2eXguBR3zVEr4KgVYpCxPZuRgtnSDJVCjdkXnmPjkx5zZUE5B9kMlLLgeTAcwf/5J8fc 3p86K1lyXfePm+czbvBBoGkGIY22I52MwWjlCCi7LutJPM9QzPsYA7msDS9mPIHvWf8+n89YBda2 0H/O99DKWD9eWxBQyvnybjKAMFQYrdzIN0W/N7CuhVIMAwOZPLro4eXLhMMhfqjR+3cxjRN8IW0i DQIhkvkQTj23kC52nxRWSTPxUUqAMCl9Nsn7imPyACaxAAQJP4AjCMfqAZokLThNGIJB+NKZ/8Th SEi+i7VMjCVG3wdyrgBAGO9eGIR2gM7YBqd9UxzvqNd2kTbXEDWNjuGoZbh1YNChcSajLQUeUwxG Y1yF4R+GMxYkk3YYmVgUUoAyChUYhiMbOcAkloRIkYdoZYkrZTA6dPdmMMYOaDFKx+vBAoEd+Qda hQi0Y8UNQilrbWhl93dsuQotH6Fioswg5NnlweMZkqL3HiU2QdKTu149Iuti7Y+d+oTwE9E5UmiR 7vSjw50aE7kOaZSRwk/F/pNj0iFFLZ8kAj2ekrWNzk5d7fxOIVC9FmG/S3HlEsLPuDyAFFl0hkRK KSS20i7g+SI2ww3G7vTn3FiESBJsouEFSMd7SKdkrsc0xt6fkcTrMcJl1Nlt0fDlhCyDaYhmDEm0 Qrgw2304AIEbai0kUZGSNGPvdpog6yJznsTkJ/WOo/ED8TIpAHFfxT1Hii2w9yrtnAc2LJmCiRR4 GLAg+gN+m/eanCsA8JTCExJPSmY8W9VdCMHgpM7WP/46IpMhP7tKYXGVwuplSkublFYvJTxS5Hfq x6MivHIJK0LYYck2vKVd49aATUyKO9sI+ASxz32Grk+VGDaNcESl5SfuawE4NyAqt3YKK6NeHDEG DpFi2lWO1ksNHoqsAXelJNQnovXpsf92q5Q+oYkiD2YsQUmmQcGYmCR83OVcAQAQ2/F5jzj05Ps+ oauRHx7t0DvaQVx7ye0qKSxvUlq7wszG05TWLpMt19AqRCsFRr0nAWGmnONnP3GJztEhb3z3dW68 dYNhb2DDoIDwPLxsIZ4ZWAiJzOXQWiMzWad00i4bkJ6PQTnlcyawSUJlsaagQURhzIfjAEQ8W5H9 ndoDe9aEt8cQK3PUd0fJfJNsvl2VBgUHiDFZmJCKNjYUWW/u2JhHcEGH2GWwWZ3vBzlnAJAdK5kN YIzm6gvP8F9+9e9x58Yut96+yfatHe7d3eLW27cZ9PqMtm/R2r7F3rf/KQLI1xYpr1+huH6V6uVn yZRnLaNuRiij4T3QOJqdES+/scUHLs7zF//Kl7h0scbJYZtbb9/k7s0t9u7tc+/mFgf39hkOR0iR nj7cWQxupCHg/ByJyGbtsuchpIcRIEtly3qCdR90VIrLliI7ywKQwthUYOdmGB0Z3WmJHIjx9Yme mug/0gnYSW8f4ULCL6Rn/omMCuFJy3FElEB0zZh1jK5rQeE0hfx4yrkCgJyUx4N+zzY2kTSru7td fvd/67NQzbK68lGef/ZT/Ny8ZLEKw+aArdv32L61xd72AffubLG7tcvuq38Kr34TjCE3U6Oy/jTV K89TuvgBsvkKRocYFfDD0X8/uAgM2/td7uy2CZVGGMXifIWVxRJrV1/gMz/9MywtVliY9Qh6ht2t XXa29jncO+Bo/5Cj/WNOjhrsbu0SFesUEsRgMAYOAixvUiwSZzIJbZVf4/ItpiOAQTiAkO78ekzx IifkNCicpmbGonpuzVgyUGQxpJ36+E2BJ7zE/I8Mgyi0yDgomCdjAR5PMb451lq5PACT5Ie4D95o BRx3FNzugqv5Xyp4LMysM3/1Ih/5hM/nyzA/A1kDezvH7G3tcrR/wN7WPs3jN+n96XfYHXlUr3yI 0qXnGRr5Y5uB1YY1BdKNS66fdDg+avHaG3dtSnKo0DqkXMyytFBhtlpkbmGdD37gWZaXatRmyszU PPQAGvUmjcM6e1u7NBstOu02B/f2adZPqGs/PbmajQoo6zM/0AUwlrGUUmCMtB2uBLSYVN1Epihf HByYtm9EHKY3CmKAENKF/tKDkkhA5hQoWCvnfQEB5woAgHhyjeIZRd0lxOmrQkCoDHsnQ3ZPArgj YyZcCmzx0NLTVJ75AB/+WIZSDspFyKMpFmws/F++fsIrN09NSvxjEYF7NiRIm/NgjK0NsH2vzt27 BxhtOQ0VKsuIu/53eblKrVKkXM4zP7/ExSuXef7TRVbXFvhf/sH/zY0bdwAwWjFfq7hY+kOEAYVH GAQEoxGe7yfhTpEooCGJaqRT/acCRMpijyIBk45FlCsV9+uu6AcmGfGX4gdPg4KB0bB7+yFf+3ta zh0AjJXPSq03qb9R8pqdUNNzDdUpkBt8Yoyh0Q45btopwbU2GKEwoYn9X4PGE9hJQN7jYlMhhPP1 reVg7CB/jFYcH51wsHeMMQoThmhjUKHNG/ClDaGBVZpSIeuGAj9kIhBW+WTSzU5X/tT6tFi3XKQi Bu4c8fbxg4RIQYYReNK3Pn2UM0EaUMzY/Qt3QxovuP8bfTzkXAKAjKr1Cg88j3Ixx+xMlqKvKecl xbxPLivp1Q/I+hIQ1BZmKRZz1lwVlvOSrjjPYGgbpjbQ6WnCQNMdKvoDxTDQnHRGDEaKk05Itx/E vY9AxATWj8tNeBSJzHR8D2kMnoQ4EShi04WxA5t0RLhNV1q7zUUApMCTtlz6pF0tzlhORwiEs/+N EO49phKK4sSgtAtgeQmDQXie/Y46AR+T3jUFCvE69eMneX9Ucu4AQDrFW8hp8gdvMTy4ze7JIW8f H9NpddEqGckWtVxrHcZ2IxjIl4qUygUKpRKV2Sq5YoHq7CwzsxXK1TLLa6sszVcpV8rMLZQTXxQ4 alqW/qQdcNIZcdQc0GwFNLsjgsAQ0WaGJNnoB5FQaedT2xx3XB6A9XW1LYFuXEQMFd9gEkmLBsnq 2PK5rxhX3FMIex10HF6bJnEikDHg4XIV7ifpMN1kbCCtwCLmBOLl1J5xzhACmcmAsYmg0fnTacfT QEEAvAdDvz+InCsA6IX5O91uB8+XKKkpn7zN0fY2e1t7BEohfR/f8xGej+d5p1OGU2KMsROANLvs 7+xb3XHpr0SDeowNM2ayWeYW55hbXmBxcZHa8hwr6yvMLi7w1NVV8sVZNBAE0Ooo6u0+jeaAw3qf emtIvTmwTH588bN71UgyvuA/+Hc+Rtjv8Oq3XuPN732f/e09lApBSjcxqcQvla2JKwwiW7AtXUpk NmtHDWbzaKORmZx9Ns+L4+kxoukUOGIQbogyELtP0yROBELiYcOzU3Ya67xPuxNuDEDKP0h3+JFp dVqZXWVlzxs3/R0xmKQAnAYFGQPq4y/nCgA2vfpQyFl8Ifjnu1kKn/4lfvVvzLK5CMf7Xe7cuMP+ vT22b91lf3uP3a09WvWGbaSej5fxkMLD87zIinShsAd3083jJo2jBjf0OxYotI7H7ZfKJRaWl5hf W2Jlc425pSUWVpb44MfWQPqEoaLVGXJQ79Fo9jlsdDlqDBgMR4CZ6j4EoeEf/eNXeXq9ytMf/xif /4ufZ76W4WivwfbtbbZubrO3vcv+9h57d3fpDfpu8I6Iw3yWA4mUzj2j59vnzkT5ABJZqkA2S8TA aZEMt34QB4CwboP03HUjhU5TeWKyDz91ptNhQDHx04GCEcRAI3wPT7rZh1PDhKN7PwsUohGD7wc5 VwDQBBaFHQ5czRqu77X4h3/UZqGa45PPVvnQJ56DTz7nctoBCf0uHO7ss3XzDgfbu+ze3uJwZ5/d 7R1UGDprIWMz65w/O1WErak3TVuV0uxt77K7tcNrf/YyqKRE2PzyPIvr68yvLbO4tsrGhXWef/oC 4NHtBRzU29TrPQ5POjSafYIgCciNAsNr7zb43vVjlFJIo1iaL7G6NMPaU89x+ROfZHmxwvxsBhPC 0e4xh3uHNqx574jDe7s06iecHDU4qTcdSRjYRCpHfEhA5Ao2QQhcYUQ7kAi4byqwTGUBSgSejF5O WqEjX59x0zz9YqMjplwoqk8YZQma1DEZL4NxEZ3YOnBkjr3+dFCQMnIZHn85VwAAFxBCIxDUMtZn zEhBqxvw/75U55+9KvnIU2Weu1CkVpQE1l1mcWOZxY1lIpJcCFsevH7QZe/OFju3b3O0s8/O3W0O 7+3TbpwgfM+5ExlbP+8BLSbq/SRy7Kt0ml1aJ9e48fqbseXgeR5zK4ssbWywuLnO4vo6Tz2/jJ/L 02gPOa63OTjuctLs0R0MkULgZ2yM/fikx+FRi1e/fwcVapQOQBmyWcnCbImFuRkWF2pc+MlNPvTZ T1OrlllaLpHPwtFej8ZRg3azxeG9fer1E/qtNm/cPGAQeQECV3QjUkZLok4Tg0v9lRLheeNsO1MO k045p2y0l044mziakB5UlAaTOP14HFAizElhzzgoIBDC++HImfeQnDMAAF96eCIaeJ9eb9H95etd vnu9z9p8lmfWczy1nCeXg1BB4Ip7RvX2y5USVz/0LFc/9GwctxYS+l3FwdYO+3fvsnt3m+Pdffbv 3qNxdEQYhPieD5mMcyfuVzOLlOUwDiAnh3XqB8e89d3vYbQl6cozFVauXmZ+ZY3NjXVeuHqBvoLj epvjepd6o83A9dyekEgffOODb4Hl4LDF/n4DYxQ6dEVAtEGpkKwvKZXy1GaKzM/PUC7nqV24yNPL cxwE3+TunXsQ8R8isQAeyAGYKBWY6RzA6ZcRm/IpJwHrq0fnHT8kvQ/YbL+Mn0V4dvQfJKSncabG KeY/dR7pCbJPLIDHT/LLQ6NVViPPxm/pEmQOmwEHJ4pvvDVgddbn2Y08F+Z9MhlXAlxDqI2t4Wdc tdkoWiAlS5vrLF3Y5IUIGHBWw94Rh9v32Lu7RWP/gP3tbep7B3RbTaSXRXoZhJQPJCERjjRLAUO/ P+Dmq9/n3e+9jta2cm91YY7FCxepra5w6ZlLBH6e+nGLZrPL8UkLNQpjr8QCmABjwSEeKqwFGE2v 26fT7rJ1d8fWHFAhGIU3dh8GoRMHfGw8walHEDERGE2FlmT1pFj7UweeER4c6+XHeYRIsaPQipfx 0NogZbr4Z9r3F1NBQQA6HBEads/+OI+PnCsAePHXf/3kv/nv/vu6J+VC1kuHcQQykyM2JAWgba+E hMOW4uD7PbSQrFZ9NhY81mc9ZkugjCTUxoa/IrZau1p8rty37RhtbLpcq1KeneXSC88jhWfTZYSg 1+5wtLvL8e4uR9v3qB8ccnzvHq3jBuEoQPo2OiE9z9UzOEOppHU3InVs1Zs0j1/BvGRrAZZmytRW V1nYvMDzF1YZeXlOGl0aJ00G/QfVL49AwpruUni2K06VzxYCdGrmnAcnArlzuanOEj1MjomnTHTf KhmNH9nvaVsgmVQk/X2J97CJP3HiE6QY/2g5If1Og4IANFKI9v3f1OMh5woAAKT0DUKwUEhMRpnJ cv1//i2K609RWLlEaWmT4tIm0ss4MsimtfoCjrua457m5Zsj8hnYmJMs1SRLMz6FrIdS2s4YJJLJ MWOdMknFXvs3iN0JfMni5iaLG5uYTwCu5wyDkFb9mMbuHgfb27QOjzje3aV1eESv3bKZipksUnqx co6JcIrmQGE4GLH/7i12b9zEGE0un2N2bYO59Q3yq3P0tODkpE2/23sgGEyKzX00ceUgeAgXQJrx SVCjm07d/zi/l4q6TCRKJDP4Tru3CFcEmWwmzhyMgNlGEpNBPmbsmkkkQAgIQy8Gj8ddzh0ApCfW jNu3gN5JnWGnzck7r1hF8nwKixuU15+itHqZyoVn8HJlF+lSCGPLX988CLm+a3v8ck6wXLOzBS3O +OSzIiLFXZmuKToVh9Bt7b2o7n88SYg2FCozFCozrD7zDODFiTn9bo+TgwNO9vep7+7SOTqisb9P ++gIpTXSy1rF91yd+5iXiywEjzDUHN65w8HtWxhtKFRKzCyvsLiwhCrM0O2M6Pe6D/dyYyIwUaQH JQJhJMZIpPRSUYCJ9yRO/4ijAiKV7COTXvsUuec0Wook38BEyyYhFq3xZ5OfovSCJA9AuKiBwXt/ 6P/5AwBPGDzpkS7oEI4CfvW/+irXXr/B7eu3uXPjDmoYEGzfonPvlmO2DfmFFSoXnqGy8QyVi8/i 58sYFSJkgA40/ZHi3f2Q6ztDjDZkMrA441EteixWM9RKPllfEmqN0BE4pMwEY8Yafpxao1MzB+kg qfOHYHZ5merKMpde+BBxqpCA1vExjd0dWodHtA73adfrdI/r9FpNZCaLED4ymoVYWGZbSBj2hxze uo25eQspobywRHl+AQpleiNNOBo+4A27OQGN5QIeGAYUBi8aa5Fi5WMlfZiPmspSHIsknAIOQcbP IH0vLqw6jfSTAqLKxG5EQzzPo62d9n7JAjiHACB8O7jHS5twRrPIYRIAACAASURBVHOQf4YP/sIL fO7fz7I2D3u3D9i6tc3tG3e4884dtm5t0drfob2/g/jOvwA0xfkVKheeZebSB6isP42XLyLCEVoo jNJopditK3aODN939f3zOUG16FEreVTLHtVylkLGVaNRmjAiE6fU/idlQUTxaa1TE4JEf4Fsocjy U1dZvPK0nRDE8RrBaEjr8IDm/h7dep3O8THdkwa9kyZaKYTvIaWdQcEYQfvwkPbBAQhBfqZCrjqH KZQZKjuPwJiGqpAoEUgZV+j7PjODxPMCCmmn4nYBeZGaImxch8/udhNWIF1d2KFJyn3ws76L5Yv4 7Pcj/eLaipEr4lZ475NEgHMHAL1Op4WQi+vlhEySAq5ttXnzbgc3OJ3ZSpbl2We5/LMf5dP/rmCh BmaguX3jFvdu32N/e597t7bYufMa+y//S5RWlFYuUbv8HDNPvUB+fgVjlO13XHjQCAgCzeFJyF7d hsyUNnhSU877lIoeM3mPYtGjUshQyEmkL+NKu7FZasCEEQQw9hdwk1cajHLuRGq+AWMMpdl5SrNz tvqtsa6JUiHDdovWwT69ZpNuo06/1WLQajEaDMAIuo0T5EkTIQR+pYJXqqLyBaeXBhOG9trKuLLg 8r7DgXE9vxFmDADiQPwYvZ8i/k6dJ73PuFInecE27demHut41OCDST84DQoC/f7Q//MHAEEQ7ksh nkoYZCuC1JwAErq9kJs9zY3dEVGjzGd9FqsXWHz6Mh/6mORnS7BQBd/A0d4xh7uHNA7q7Nx9jUx7 i+oHPs71Pc1gFJ66DymApLwAnX5Aqz/6/9l78yBJsvu+7/NeZlZVV9/33TM9987ei8Vyl1iABCBc JhniAYmWTJOUHCH7HzPCofDfVjhshf/RHzZJ26IVEimGITugIE0qaJMhkAQJLLFY7A5md2d2jr67 q7uru7q7rq478z3/8V5mZfUxMwsQwMw2HrDTdWRlZWbl7/t+73d8v2yGYh3KdNUlPUF3yqUrKUl6 DqmkJJVw6Eq4eJ60hB+nCINEHoOJGRjP3AbptLJde1YtSGtkIsng9Cz9UzNGKchG9/1anVqxyGFh n3q5TKNUolapUMssg+Pg9g9BTy/t2goV6ec9iBUYJRgbHQLs2vzYrGoN0wb+Qmmu40Mc+UuHt6Bt 3tBLuDgClI6l/kQUgnkEUDDeiW9B4KMwzhwAODb9I4XkYQKPQpjtw6yb0ordYovdUovAbwfrpFAM dKfo7Zpj6Nw8M3PP0dfbRSu/x4vzad64e/hIt4vgCNW4EASBpnjYIF/WtsnIshJr4z24DniOQzpl ypCTrsRzBI401X+OFHiuKXxQgcZ17bkHEAhTuSscEEqZdUJY0GAilwYIpENyoJ9Ufx9KBRYwQAQB h8U8tXyBaqmEHzYV2TW5OYeHFQKJdhrwaHozivTFr1AICvZ5ONHT6SOE70cQIiQJ10NpjRNG/gln +naU/zRQMNuYv9IVOOKjYTofjbP4EMPwWTpIGeNzEAIdtHC6+wydnQRUXEfuyD4w3kKoBaA1FA9b FA41aztGcANdszNw5W9FGiD0ciWgbTWc8R40zVaLRhOr76ejTAI2q2CcggCtVaRbiFakkg4q8M1M HWDYgLSyzxWB8k1UPDACJxqFUIpGvYmyoiBaBwil0F4X2k1YWXRzDQzL1iOwAtvZ/5gH0F6mxx8c eRw+j4NCO2qPnc1d19zqIchEQb8wgPggUIAIDIRwaDRqSBHsPvKP9xiPMwcAoQDGbF87EyDdBEv/ 7n9BaU33xDmjCTAxS3r8PNLzzOx4NDF8wghncOthRpPph82nf68jbivSPtC2xBlti1hUO3BXr7ci dR+tlVEPsueqdeyxUhZMwhJfHYHP0SuiaCvoAg9UBxaWEBS7/pdhyWR8Pd+5tO84V93x5nGAiF92 1/VOLfYxX/looAAYWnUpCyef1ZM1zhwAOJwg1qOhpTWtcpHm4XsUlt6zsQDXpv6eont8jr7zT+F0 9YAOUC2rB/AYUICfNlQkDCIsfVHYbis6ST+0CegLodqz6Pc6RPt7AePin7I/qW0VoNadpcBHDfoo goQR+WjbjpePHb+UkoTnRFWZ7YKfozoA7T0cSw+GyxrsEvKJ4HB6+DhzABAEerdWreGEVX6A32ry T//5f8vBxhrL91dZvb/C+so69Wqd5tYGh9sbJjWlNenxWXomztN77il6Zy/jpntRfhPlW5GQxyZF LPjCpy5TO6xw81s3WFtcA224/hDgJVPgJUArZDJt4iHSRbouCIFwXLQfRHz6QsdYlGMzY8fQ2mYg aKfyjk7O8c1tOzC0i7OijTtm/hOMXLS1HaK1+ilOmuclbDxHRIYvaJdnm0M/IRbAcVAwfQsSnCde GRw4gwAAFEBHhgAmUPUX9zWTw1e4+rkX+NQvuwz3Qq1QJrOa4f7799la3WRjNcN+dpPi1jryxl8B gu7RSXrOXWNg/jo905eQCRfl+8adDn50N4lG8/4HGZ67Ms5/8U9+nsHeJOuLa6wsrLK6sMb64irb K9s0m0YUREYkIAIhHYQjDemHdBBeyjQJuZ7tlnKMFoAQKGHW1UdLmJRVHX5wM5CZR6vVMp5nqxXD 98BSixEBj3m9UwU4HieMtocO4Z+Ezf0fY/aJR/pFGxQMYdrJoCDsOu+jMf+fQQAwLDeS0bSkK1bP Wa35LGxWWMjUzE2uBH09HhND55n6xBVe+hmHkQFIO7CT2SWzkmFjbZOdjW2y64ss3Pg6vq/ombtM 37nrDF55EberC03wI7lZBLB9UGfjjRX+6K+WSLiayeFu5s/N8PwXrvGz/3iY8RGX4n6d7Y1tNlc3 2M5k2clk2cvukd3chmrdFumU6GAKEm3wRIMem0SkuuwXa4QOpcHkg2MAVhnIpNSciLL99BWI6LR4 QNOO9IeswpGhCnAcF0dIlPXgorV9Rzyg/TgEBXkKKID1ouRHw3Q+GmfxIYYEHMe4nke99UgTwP5X bwQs7zRYyTYNKYSUJBzJUM8AQ0PDzM28xIv9MNQDgz1QKTbJ5w4o7uc52NsgMTxL1u9naav6IzhT Y0euFDieBK3Y2jtkM1u0nP8BKgjo600yOdbLyPAg5z42x8e/2M/46AD9A4LDgyY7W1myVjFoN5uj tF9gd2uHXHYvaohJqQBLlm7bo4UVz9QRaJx4fGFXoV2aHM0CtOm7H5QBOOH12NogkfBMOlIJiGZ8 YT8hTIFUuLWOu/pEj+OgIIWtvXjYxX9CxpkDALMmdTjtRgqLZ4Ruu5HRxwT4CnJln51Ci8DWkyul QGmSHvQkEwz1zxGkhhlJduM2q8TDUz/KIcAYpGuUgrQrqNebLK/mWFjOon2fIPBNJR8BPT0pRob7 GBzoYWR8mpeeeYaR4X4GBnrp7nbw64pyocS/+bd/TCYTZsU07Uo7EWksnHg8OpQFE0hbu9AW44of tf17Siags/GnDRhCOLiuY1KhMXLQ48bNsWXAaaBg5gFhy6Wf/PHROIsPM4SrtQpwHHtDSaMNkE55 9HZ79HV7dHkSzxN0JR0SrkRaN1Y6bYIrIYweQFNBq+lTb5jinHy1RaXmU214LN0/tJ3ElnUGUzsQ L1h5HIYQ1q11JY60DEHagMPGRo611Sxa+QR+QKACCEwGpKsrychIH6V8OWZVphdAPFIMwOYkhMDB McG1aNMHXJ2YZxD+FrE3o8+6rnX/ZYz1p2PdbwDkUWMDgKEvM4Uij3x9H+dx5gDAEWqlXC4zMdzP ObnP+toGyWaJYrnAdrlEo1KneliOzf5m3Znu7ybd3YMGBoaHSHQl6e7poae/l8GRIdLdKcZmprg+ 3cXAUMrwBwpoNqDpQ76syJcb5EstDg4blA99DspNS+KpbYGPOcbHBRigPeMhTDWhqwUoidYOQeCz k90ztQUC0NoYdFhBCA8EgFCRWCAs0WbnbH9sAWCj+ELE1/4xTyBeO4AgmUygtcZBdngIcYLPk0Hh 9GUACJTfQsgfE4I8mcPzfNcV7NU1T3lZ3n7rT9lc38ZxEziJJG4ihfQ8Q/0dG839IoW9AlprNlc3 bLFMWGXX1gFAKxzXY3B0kLHJcfqHBhgaH2FkfJjxmSnOXxinb6gPW4xHuQa5fINcvs5+oc5+sUHh sEGl1gRhqw2/R0TwA1MVaEpyrby3MOq9Qqto/Su0NayoMEY/tEz6pKEx1cSmXDlMsZx+/DpSBg6s Wy07tm0bdmzI9jvHd9ye/UMOhDB0F5X0CqPuC9CWDHswFXgICsaFs5JpQvyYEuxJHF6g8BBkij5/ 0nqef/QvvsjTc4LF26usL66QWVpl7e4imbVNDg8rOE4CN5HESSYR0jXMOx17PHltX86XKR4UY/X7 bR2AZDLJ6OQYYzOTjEyMMTY9wdS5Ga4+N4GXdAh8qNQC9gtVdg6q7OxX2S/UqFSbBNp22j0EFPxA 8es//zxJR/POG+9w57u3yaxkCAIfISWJ3n7QZu0tU0baWyRTgGkK0hpIJM3OHBcRUWXHYiPadMvH h1AaLQRaWA/gAYVAwhYCCW069aTTDs6dOqI8X9wDiGcFzCael8CVYeNPzLCJpw2jncYKguIdgMdB IaxVSHw0dEHOHgAEHigpuDzs0aiX+eNvbvC1pMtrT0/wqZ89j3QsCYSA/WyV9cVltpbXWLu3wObq GpmldSsSYjwG6SVNajGMYEdW0tYBOClinMvm2N3atW26JpAoJfQPDzI+M8Xo9CRj0xNMz8/zzCsz aOHQaPjs56tk9w/Zz1fZOahQqzXbrm9suI7k3/7RTS7PDXD1qeu8/oXXGRtKsZPJsbqwyur9NbbW MqyvbFDKrEauelgP0KbNFuC6CMcB6Rr+f2FrARwPLUG4Cfv11ogs4QmiHTg7aQihEVauTIZeij2T iPU3MvhjUMPJXoBZdiQTbqzxx2yvo2lc2DLtOCiIKE0YVhieBApCSFxHYGpKn/xx5gDAU4bMIawA cyW0fMXXb+7zzVuSFy/28PyFHtJJQXdfmisvPsO1l5+JkgdawdZajrV799leWWN7ZY3VewsUdg7s EqILJ5FAukm7dm4Hx+LeghAC4RwvKa2Wqizdvs/i+/eiVl3HlQyPjTJ+bpaRqQmm5s8z/9QkXtcc lWqTnVyZfLFCdv+QQrmKpQ1ASsn9tSJ3VvKmeSdQTE/2MT0+wOwrr/ETPzfA2Eg3fWnIrO6xvbFN dmObvd19shtZsptb5HMFpGxGYCBiXXthBF0ODCP6+u2JmaCeaSd+cAwgVAYCo7YTry8wnn47BROP +Mf2QIdmoX3gupb0M0SgmCsfGraOHrc/3BEb4GRQCEHS+WjY/9kDAClEtVavMSg6Dc+xDTw3Fiq8 tVjj4kSSp2bTnBszqjl+oG11mGBoYpThiVH49CeiGa5eh/V799leWSW7usb63QV2NjLU63UctwuZ SOIlEraCLrbWPQIMcc8hPssU9vMc7B2gv/NddGCIPfqHBpm6NM/w1BQT589x6flJtJNkP18hd1Dm IH/IQamG1ALHleDAzm6J7e08KggItEK1WggBk6N9jAz3MDYyxbWr1/mpsX6GB3vp6YF8tsJOdpe9 nRy5rRyFgzy7m+b5YalM4AfmRjKB/4hfAB7BAxASRygcCa6UJ0Qe2qDQXuET7TTyA2Jfkkx6mAsY FiQJOtN5R0GBjrV+Z/lv5zJAyAfwGzyB48wBgJYy02y2ECf55ZgiwKSE9VyTtZxPynO4NJXg6lSS 4V6HQBl+fz+IaQFYt3L2yhXmrl6JqtC0gPxOge3lZbJra2wuLLK9skxuexeNxPMSOAkDDobOOzR4 fQIwxHQA7K9WrVa5f/N99I13I3GQ4YlxRuZmGZub4+nzs3jdsxRLNXZyRfYOylQqdRAC15U4GrR0 QSty+RK7uQNuBVYMRLVQfoAjJT3dSYYGexgb6Wd0aoaJa1f55Ogg/X3djI6m+Pf/91/xl19/yx6v nUnDYptHKARCGLFNIYWBvI7tQ1Lw00ebTdh4Pa5jG38wQGuCfkfYfR4KCvb9KAAorFclkK7E+fES 4MkdAhhKSbq89o0mHBPgQ5vqNWlzw4HS3N1qcmfTpy/tMDficWHcY7hH4AfaUIBjO+BUOzOgNGih Sff1ceHFF7j44ktmNpTQbATsbW2ys7bG1sISO2srbC4uUSmVcbwk0kvhpFI4XgJTThszgiPAII8I 1RX29jnI7XHvO++gA0V3XzeD4xOMXZjn6uwcif5J9gsVDvbLFAplmkHQrm2QRpwMqXG0i3YMAWat 1iBTqbC+tkWgFPg+gVKooAVa4UpDXWYupMky6CjDcHoSo10IZDQYxAmlwNE8L9rPTvpFQ94gz4v1 /XNS0C8+o39YUDCvSfRHJQRwNgFAK9vTHgaXpMvBu9+gUdglPX2RnskLpEYmCYn9pQ171xqKu5st 7mwFpDzN7JBkdsRjrM9BupogMLONH9qnxqYHTX99xMGHZnB8gsGJSZ569RMm4CUl9UqFrYVFdtfX yS4vsrOyQj67Q7PZMsBgg46Ol+jk/4+DwhFPodFosb26xtbKKlopPM9heO4cQ5NTPHX+HE0nxf5B mVKhbIRBThlxtWCEi6NVpBjUIQyiwkyAboPKA2IAQts05RFa8LgXIGL/tvEhti7QELp0yUQiWsNH fAWireoTXqlofS/aOw09BSHiLc2dLcOmN8L9qNj/2QSAsE49+lkFtGpVDu7eoLj4LgBOVzc9U5fo nr5I3/zTpAZGjatp3cKW77OU9bm31UJKzVivZLzfYWY4QXfK1MYECBPIo72GtIzZZobUGq18I6Zh 3f6pS1eYvHyV5z/3eUMJpjTF3V121tfIra6wu7rMweY6+d19czMm0jheAjeRMlH6mERXCAxm3Qo4 Rokot7bOzsoq+o1vkkqnGZyaYnh6luTcMOVaQKlUpl6tffgLq0EhUAi0Mn/jcdCjQ9qAoUBYqrZ2 JVSHkYcP49N5/F0JpphIGq1FrSx2x0p7rQt/DAjij2Px2rZqUHurkE1Yax8tnRil1JM7ziQACJta CgFABYqXP/UK9VmPhTtLbCxt0Cof0rx/k/zCTeTX/z1udz890xfpv/As/ReewevuRQcBIghQqkWu 6JPNt7ixXKMrARP9LqMDDkM9Lr1dDkEYZAog0O0vjyZvm5ZSOohaVw3VN6T7+jj37HMR978Ugma9 zsHODvvrK+yurJDf3CS3vkKjVke6SZxEEieRQnoJEA4yan4yByJtKqvVbLG7vEp2aRkB9AwP0Tcx Sd/gEE2RolSp4tcfpgUQnotuu/9aAco0UZ2CAKZ9V+MIgRtpIcZm9vYvFo8FEkODjm2TyYT1UCQy NmvLmKqPUp1cAGE68KGegn0sHQe/1cIXevWRLspjPs4iAOTL5TJKaya7Bbs1jURRT41y9Ytf5nO/ 5jGUhrX7ayx8sMj60iof3LhD8aBArXSDvbvvILSma2icvvnr9F+4Ts/cNaQrEL6PUAHNls/abpPV XUUQaBIujA+49HcbgZD+tIsQAj9QIK0XHWtACUdosIGKA0LI/S8YGB1nYGycCx97jTDyWDsscbCZ YXdlifz2FgcbK5R3NwmURibSSM/DdVOIRLIddHQMhTdaUykUqRzk0VqTTHfRMz5Bum8Q3+uiWqk/ nONAtSnBJO1y35OGCOMXIlwGtOsAjlj8ic9DozV/hGn80bG+fYg9tsdknYwIFDDdi0dBgdjvEfcU hP2upJRHf64ncpw5APB9P9dq+WgESU+ga+Z33MnX2TlomlvCkUwPDzJ++RN85vXP8p/9N+C0AlYX Vli4dZ+1pXUyy+tsvvXnbH77a0gp6Ltwnf75Zxi68gJOugftB6B9hA4IAk1mr8n6jrK95YqBHpf+ tEN/t8tQX4LulIMjBDKwFN9aoP0jNBvxJ5GEmKUQ1yYIKV2X4dl5hufmjW4nApSgkt8lt7pMfnOd wvYWpewW1fwBOAncZJcpaPKSCNdFOua2aDaaHKyvg1rF8Ty6hoeRfUM0PZeg5QNHYwbGVELJbTDh udMrAbWNAWgTA3BOa7KN7UEIwlYcgfEi0BrXc3GktLRfJzH9PAAU7Osng4KRCWsXApmYRuKUI33S xpkDALCFHrE+cAij4ESR+p1Cg51Ci3dXaiAESU8yMTTD2MuXePpzMD5o0oVbK1tsrmywvrrB5tIH ZP/4ryj7Cbrnn2X42sdwu7qjKLu0TMJaC8si3EQFJkAI0J10GOhx6U45DPR4dKc9042IRvsaX5ob 0dTYGBPQR+Yhk41QUfDRPAcv3cPUU88yce0560BrWo0G5Z1tDjZWKe1uc5jboZDdxG/6ptLRSyIS XThuAt8PONzdhZ1dnGQCr28A3dVNIKUNlkYLKpsFEShC9d8HBAGjLIAk3gzUNvjwn452n3a6UANS kEx4oC3tl3hU1t9HAYX28SgtcBynU1XqCR9nEwCwAhoP4PM0rl4bELSG7YMW2wcB70pJECg8B/rS g4yNjXLt8iv85C8KhvqgJwHlfAXXcdg6dPjTtw5o+sc9xhAUDGOvptrwOay3otJgrRWOFKSTDr1p h1TCIZ1y6O1KkEhIPBeCQNrMg23gsbUJCmIdq+1yY/M3iHoTekZGSQ+PWs/EfKRRLlLczFDKbVM9 yFHe2aK6t48Wjqlb8FI0SwVkIoWb7kH09SO6us03BTAxMoRSPqYd+DQxD+x7Zm3unKAL0OHic7xR KBwm9y8IHQ8dRfTilN7ttfzDQaGd/jsKClHXovfR8AHOJACgQSvBeI9g/RBzQ6gA2dWF1MbgVTz5 a0ccFKQDaE3x0Oeg1OTuunXdlSZQirSnSboav14hEF2PfGjRd8SAoVJrUa42jNsfc/1dCemkS8KT pJKSdNI1QiGuQzLh4rgCF0EQgAi0iTfYcj2NMNJkVolIB4FVI/JxHY/B2fP0z84ZYFDmE4e5LJW9 HJX9HJX9XWr5Xeq7ywjHw+3uRwVNImEQZWIAD/QATPLfZCzDfgpxspVHPkFHE5B57tnqSsfRHYYc pfCiWf4RQMFM9YQ8AeG+4uxEp53PkzjOJACYaLsi5YYpZElx4V12v/UndE9fIj19kd7pS3SNzSCk ixAKYuWhR0ck82XKBXAk+ErgNzWadNR++v2M0BOWYfl8BA5NDqshMKhIFxCbXBRAV8JBSiLFIEeC 65hZVwUBCc8UQCllHmsVmEKfUAfAN2XDXk8fg+ke+mfOmXSmUtQrFSr7u9QLBxRWl+zRKhCmdFo+ KAagNAO9PVQPC2YJEMUAwsBeeMU7QUEceZJIhMG/TuN8YLHPaaBgd3saKDiOgxAC70P+fo/rOHMA UKlU1guFAnYCjIZS0KjV8VfvUlq7y44A6Xr0TF+ie/oyvbOX6ZmaNzez0ASBD8Hjowlgml/ABvMJ b3StNbVGqy3qYQ2dwCgAqcCPuAHC1wg0Gh8dGMFxFSgEvlEIsqpBCm2k0YUhANGJAUikzIyqiPgA HqwOLEglE5TzTdOWHCsEMhOuMfCoDvLofoTAcdpdi50c/53r+9Div19QMMu2H3sAT+xIp9N1k+Vp d5EpFfCTn32NSz93lTvv3+f+rfss31uidlijsXSb/PJthADXS9Izd4WBC8/SO3OJrtFpgpZvXF8V dNyAj9VorykQCIQIwriaLZkVpnNOWvUgabMQVkEljFPI6H1TKh2+b4Yi5PZD0uYDeEgMwAQA2oYc SwJ2kH/IaB9h+s8cf9L1kI403opoG3m726+T7uv7AwVMpkIptKc/EpxgZw4AwPz4gVKMdRlYF8Cd 9Rr5kUGmn/tpfvFzX2J6BJqlKre/+wFri+ss3l5kY3md3L132b/3LqBJ9vQzcPl5+i88Te/sVZxE EuX7BKplOoZ+lOcIjA/3oIIWy3eXjYcgBG6yy/T3o5GuhwoEoTSYwAibCDDegurwtx86Qm6/MBCJ eEgMwGYAHEwOP5zx2+v9cMsjbMHW9RdC4HoOSmnrPRzn8ft+QSH6bJRUEDQbDUSrFa53nuhxJgEg vDuTTmxVLzTZgwbZ/SZv3y+DlnSnHaaGn+HKZ1/ik39PMtIL5f0i9967y+baFmv3V1lffpftt/8S ISQ9M5fov/Qsg5efx+seBG08gx/JKSo4N9nLTzw7zcCXX+Ldb7/L0r1l7n73u2RWNowcuJfA8Tyc VBonkUB4CUQiZYJxXhLpeWjXQ+JAeK00pzICGY4Dcy1VYFIRD6wDwGoDWoEQnJjotjXW9nPzqN0W ZUDDtD2EhtppzCeSe2Ipvex+2rP/6aAgYiDiug7Slezs9D6m7t6HG2cSADTYhqDOISDSBEBomi3F SrbGym6dkNxuoDfF+PDLXLjo8NovwNgg0NRkljfYWs+QXd9m89YfESQHcacuI0Yu4vs/fBCQEt58 b4tvfDdDOiG5MDvIU69/mk///V9gtN8jm8mxdHeR1furRuRkeY1atR5x80WKveHywXEQyRTC9awy kGEIwvOi5YWUEld6lpBEQ8gudKoHYDrrpDT9ClJKa4i29iH09UXnZ8KnRvEnNHJj3p1uvPl8fGnW Kfhh9tehAsSDQMGSl2oBl77/3+hxGGcSAFTgK42Ol4tHI3wtdIU7/hNQrvgc1hT3t1Qkw510NSO9 I6Ys97LLCylIOoqRAY/A13z1m3sUyv4P7wTtkFKQEBI/0NxZ3OPW/awpPFKKmfFepqcGmX/tdX76 7w8zNZ7Gr2s2ljdYX1wls5phZ3OHjdUM5ULJlPTWa+ZvXCGINiOQsNkQAGF6pB9cCiwkWkhbt6AN lVg4a9uKPx0rFArLi7UIJcxMN2F7/R7O1G2PoA0OxAz6hGIfYcqM2qBwkqdgPivER2LyB84gAPzG b/xG41/9q3+9qbScHU4LlHQQjktPOkF32qGnK0HSEyQ9h66EYyPN5rNSGNe3WgdQaF9Tqvv4rYBK PWBpu8XdjKJa8xFagPBBG9qxH3Xc2KTYTSmiVprdXIntGxl0bwAAIABJREFUnTyB76OUEQNJd7lM TgwwPT7I/E+8yicmhxgbHaArCbmt/YgybCezzXYmSzaT5bBQJtmjcDH7d60b/+iMQMa4hcIEFcOh tdEXCCsXwh4Du/b3XNdqEob7smXP0Vo/vqaPB/bgZFAIlw32ep3oKQhcKUE4HxUH4OwBAICUjlJK MZkK6N1+l+ruBtnDEo1aFQE0ajWk4+AlE4SI73oJBkZHSHQlGRgawkt4DI6NcG5ynIHxQXoHekim TC9504fSIRzWfEoVUyhUqvnsFxoc1nwTFhBEMuU/Epkpu3Y2KkEu2lX4fsDqeo7VlW2CwCcIjMCp 5zqMDPUwOjbA+NgAT33yAp8eH2JkZJDuLvi//t3X+OYb38URZlYOW55DMpPTAcCUAndmT8J8m4iq 9MKPh8xLwn5WRVvQjjTEqMANKGCLfux2HwoUjnsKQoLzYw/gyR5KBThScGv3kJHWGsvZJbbWNmkG ILwk0utCOG7n+lVrtta3zAwTRoWVzYfbCr3+oX4Gh4cYmx5ndHqciZlJRsZHuHJlkoGhBFpDqwX7 Jcjla+wWGhRLDbIHdQqHjehm/pEAQvjdJtGNELbm3TVLhv2DIru7B7yngg5GIFN16EWuvus4gEQL U4xkWH9ORgCJ8QCQkkAcP28Rs1wdZgc0EciE5KfRtgCiDRtRHP8kUBCio0CrUxb8dFCQjgPOKYj2 BI4zCQACEFqxUO6h+NSX+eVfGeP6PKzfz7F69x7rC8ssv3+X7Y1tarUm2k0gvBTS8ZAPoIJpNppk M9tsb2yZZpygrQXgJZNMz00zeX6GydlJJs/N8PTsNIPPj6M0NBqQ3auzX6yznatwUGywV6wZ19ce 9Ie97ZQ2wBQaNdpo9YUVgtpEQxHRDf/gYSjDJUcZgVrNpnnfztpShPLg4cU+eX9amDZkqbXpy3Da r8csu/3HWrQUMsZCZA0UwvJ/wnBeO4NwgqeghanwtHtQNsjzIFAAUz15VBniSR5nEgC0NOq1Xa6m WKrxB3+xwTeHkrz2zAA/8bnXeeVzr5tWWgUHO2XW7txl7c49NpdXWLu/SrXWQDsewk0YsZAYR3TI ayeRx67u9voWW2sZW3Ov0EqRSCaYuTDH2PQk0xfmmLl4nksvTOElPRotze5eje2DErm9KjsHNfPd j7BsUApee26Wvp4kt29+wAfv3EIFCikFbrob6TgILwGOg0wmjdW5DkKFbLrWvHSoFhScUgjdHkIK tDRBvdBLfqg2oIibMUT+uog97UABAUITRAq/px2VOAEUzA7Nbtrf+2FAQUqJcJyPzBrgTAJApVwt ow03nzD3PQeHLf7D3+zRk/Z4+UoP12e7EBJ6Bnp5+rWP8/TrH4/WsoXdQzYXl9hcXGL97j0yS6uU yxW0TBhvQZg02dHb3oCDc8yLyCxvsL64xnf+8lsoZWSyhsZHmb1wjsmL55mYneHqi3NIN0Gt1mJ7 /5CtbJncfoVCudY21tg+pYRbi7s8f2mYn/3ZT/BPfv1zZFY2+ODmHVbuLrH4wQL5/bzNo5u1uvQS RgHJ9RDJLoTjIDwPnCTSc9GOa1SC8K33oImblrCluQBBpA34sBiASTeavgGi89C06w2ij2sTMFTh wl7EHQVxxGk4yUZjQBJme+y/x0EhXHJ0goKUAr/ZyP7mb/5m8+SzerLGmQSARqOW1fBMnIReYICg 3vT56/dLvHGnwjNzXVyb7WK4R9JqalvcJ0j39XD5Y89z+ZXnTXWAhMJ+ha37S2wvL7Oztsba3QXy BwWEkyCQCYTrnmoMQgqcCBXMT1I6KPD+Xp53v30DHZjZd2JmkvG5GSYunOfihXlefuYijaZid/+Q ndwh2f0yhXLNsPJIOKw1+eubm3z9nQ3QitmJfi6ev8xPfewVfm12gJSEzMoWy3eX2FjdYHMlw+ba FpVC0dYDmKCeFCLG2CtMJaGXtPUALiLVBZ4XGbO5StaY5ANKgaW2acDOSHy43GmXGbWRQEgZdfGF VZxmCx0tEdqhwqMridNBIYoXdHxGc9xTkDiu2zxlZ0/cOJMAoISh8x5OceLa15Hmx7+1XuO9tQbD fR5PzyW4MJbEMY1zRiikFc5UgkRXN+eff475l56LbuZKqc7O8jLbi4vsrq6ytbrO/m4OhUvgeCAc hHROniFtXb1ZSpifaX93n73sHu+/eQOtAhLJBNMXL1hQmOfyCzNoJ0E2Vya3V2Ln4JBKtWGJMiGz U2Rj26b+Ap+E5zA52svM1BCXfvI8n/qlQabGe6AFa8vrbK5ukM1k2VzJkN3YJp/PowMQsokUdQsK 4A6NIiwACBsyV6GLrh/gAWhT8INZ0kcEomEArs3HZ41f2J5/oTqMtb3Wj7kE8Sn+ETyF9vd2vtMJ CpZI5nHt+fgexpkEAAITIAvbgcHcGMLxENZYECC1xpFQrgW8cafOt+43mR32mB/3mBkyJBRBoFFC YRvtDI2XNsUqbiLB9LXrTD11PcqH16sNdpYWya2vsrtqpMUOdnK0tCAQHsjTQSGSE3Mk4KKBjYUl 1u8totRfgICx6UkmLpxn/Px5Lj8/T80X5A7KRilo/5BAaFxXIqSL1orN7TyZzT0CFaD9gCAISKUc JscGmBofZOLpZ3nupz/JxPgg/X0e+9ki2xtb7GWNlNhuZpudcoOKIkoDAra3gIc2AwkBMgYSpqU3 fExH8NMEK9vFOR1RAIEtCjr2JdGPbEN6J4NCx3edDgqqI57w5I8zCQAaEQW3zBAErQYLv/ff03fu adLTF+iZvkR6bAZC91QohIDNfEDmQCEFTA5Kzo06zA55OFYXIAj55JWJMRiK6lBFyBjDxOWrTF6+ hpICKSTNRp2DzQzZ+/fY29ggt7FBbjOLrwW+dFEYUDipC9UoCmHq9YH9nRx72zu8/41vIQQMjo8z NjfHxIV5rn3sHIVyk53dAvlCiUZdGaCLovvgugIdKDJbOdbWs4bgxPcNfbnSDPSnGR8dYGx8kOmn r/PSZ17nne/c5s03bxqvxTQDWEowHlgHIMM6AHTMA7AzvibKxbd/t84HxmZ1x/tHQQEdywZEF+0E UDjyHeaj4hgoYKsFPyrjTAKAoavufEUjaNbq5JdvUVy5ZVzbdA+9c1fpPfcUvXPXSPQN2inAdM9t 530yez5a1xjtk0wOOswMe/R1ObQCEyCzGiTt79EarQILCm0Q6p+cpn9qjmuYNlwdBOxvrJPPrJFb WSG3sUF+N0et3sKXHgqBEM6x3nTjJTiE5YvFvX3yuT3ufudtJDB6fo7hmTmuzs2hU73k9kvkD4rU 6o2OzIJAGKEOoXGEC9poHNSqdVaWNllcXEf5hmfAESYFGNbKG5OyK3jZ4aR3XnMZ1gEcaTw8cUHe qe4bO9CO7SJQiAX6wg2PZQLiezsGChz511wTpeCj0QhsxpkEACnIFYsl+r0UmgDQdHUl+Gf/6z/j 5rdvsbywxt3v3qZWLlH/4G3273wHgSA9OkXvuesMXHyG3tkrgIvwA7T2OSgH5Ao+N1dqdCUEU4Mu o/0OY/0JEp4kCAIzK1qR0fiIwEBZIk9t5qT+8Ql6xyc59/InQAiCIOAwl2V/dZniVoZcJsPB1jb1 eosWLr44ARTCmn3rmu9tbLK7voH+5jdIdXUxev484zOzJCfHKZTr5AtFmvUHB7iFFDhaIl2jK2hy 8hpHClzpoglTZ+qhMQCBMEsAixdR6o5wCWBDemHcz74WGbAmtv/ja/f2807rPh4ofBRQCNmWPjou wJkEAKVEPvB9epNtZuCGr/nL1V4uvPRZfvFLXUwNQW5zlw9u3GHhgwXuvXuXQnaDUjbD5rf/DC+R oG/+KfrOP8vgledxU92IVgutfZrNgKXtBotbGijTl3IYHXAZG3AZ6UvguZogELZQB44hAoAFAqMD 0IpAoWtgkKnnX2bqhVe4hgAdcHiwz+F2hoP1NQrbmxxsbVOtNWjh0FLCxBRkm0dfCgekQ6vls3Xv Ppk7d5GOpH9igv7JaRLj41TqLUqFQ1TwiE1MNu4hbDLD2K18aAxAhksArJHGXLNoBaAxvQKxmb3D 3Y/W7w+J8h/zCj48KEQxhI/IOJsAgFHnidudAPLlBt+50+StOyW0kIwOJJg6/yqfffWn+LVhCCp1 Fj5YYOXeMvfeu8fawh1yt2/A/wvdE3OMXHuZgSsvkuwbQvgttA5QgaJc8ylUWixkNL4K6O9yGeh1 GBtIMNDtGeUgKyEWaBA20nTsPtNtkRCzEjGmk0j3MnTpKQYuXY9SddXiAYeb6xzuZslvbVLY3qZa rdFSDi1tqLNFmP+33kExu0NhOwta0z00SP/UNKJvmGpTUa9UORmp2sOJpQGV9QweVgeg7RIgSrOF s390zubHCcK1fGTsIjLS0w34w4NCxBN8yj6VFijx0VkDnEkACOmrTKlX5w0SEtMKCaWKT7FS4c56 DaUF6ZTL+OBVLnz6WV79eZgYhHw2z+rCOqv3l9lcWWH9D79OnTQ9F55j6PrH8dJpq5ADCPCEsPTf TdazVZRWOA4MpD0GelwGelz60gnSKYlQ5jYPtEJq8OPU4lHfsom4h/0I2hY4uYk0fReu0Dt/lUlt uuia5SL1/V0Otzco7u5S2t2lWi7RCCQtFQMFoFosUc0X0ELT1T9A1/AYurufestHNU9ZIog2mIQ1 9EI+gBBEhL3/EiVsJbB1Atozr2UBjkXkw3LmaD9h1gbR8brd3YfyFKKHp4GCUlTKtd2TL8CTN84m AFgprpTTieQi0s9rD2nBwAH8QLG532Jz349m4J6uJON915j55DO8/HcdRvogJaGwe0C1UkX1jvD1 d/Pky51akoKQ4VeA0uTLDQ6KDZRShoHXEfT1uPQmXbrTRgugO+3gOlYHAIXWJi2m1BEYUxiNwRAU LGOw9FKkxmdIjc0wpE0GwK+Uqef3aRzkqOxmKe3lqJQqNAOoa2OA1UKJaqGIBLqGhvF6BwgSXQRH xVVEu6ApTNeJI0y9nddAICQ0GnWTRrUZgKiNJzRW3VmOe9RYw9jgMVAg3I146PKhw91/QFwh0BAE fvHEE3oCx9kEAOFo31cMJ9tsMUI6HGYW6Zm+hJNMme1OCPcKzM1hZnRNvaFZzfksb9eN6CcKrQKS MmBkqI/WnVV8t/vRDku0QUErTaHY4EDV21oAKNKepCftRiIhXUmXVNI810JbV9lEy3QQLqnNebRB wQqDKIV2PBLD47hD43RffIoRBKpRo7G/Q1AqUM0XqBzsUy2VqLc01f099N4eTjKB2zcA3b3moJUR MQmpvbW1QPmAJYAUEokg8IOoNboz927MLrAg0F4edLrpnb+O3Sa2eI+AQcfiCsc/3Onun+AtaG30 IMRJ+dgndJxRAAhWSsUiqr+n/ZLrsvFnvw9KkRqfpWfqIt1TF+idu4aTTKGDIH73dO6OGF8/oJH4 WpDdb6B1L7r1va8Z26AAaGi0AhoFPwIFbEpRCkh6DqmUpCvh4LkuqaTEcxySrsRNmJoDLRQ+baVg ZdOVKGWIQbRGa4k3OIbTP4o3q+kNNMpvEhzm0aUitUKeaqlIo3xAo7hHM9VN0N3XzjiAXV5Zd/4U e9EirLEwM6sMo+5HU4DHn9BZKxDbRsQfxuIBsdjCIy0hTogBhC8flipD81evPteqVBYymcz3oKP+ +IwzCQDKF75Ju7XvNq00AyPD7Gb3aG1nqO5sIm7+NQJN9/RFeueu0nfuOj3TFwwzjQrQQQv9UPbf H0zIWIhQB8C2+GpNvelTbygOrFZfOOObclqNA3ieJOkacRDPMdNtMiGtKIiLVoF15SEITMeiNBaD kx5EdfXRNTJFEiBoocpFVKVMs1rGrWqkNtyJIedilH046RykjEmCm2PRIuzM45gWSxwbTrquZilw gqdw/Js7Hp66hDjqKUhzzO9+8MFL3en+d2si0fy7v/DaG3/0h1/9zCmn+NiPMwkAlua+oxTYbzb5 p7/1z0nrJvffv8vtG7dZvLPExvIGhfVFiuuLyDf+BC+VpvfcVQYuPEPv/DN43f3ooIHyA+MlPAYj NACJQFsa3ZC/sNFoUa8FJnevFSoIwAqDGMGPABFolBUGQWscYUhUQlEQbfP+QgemhDgIUMpjWHgM y5Zx4+0xiGOzdOw4tY6sTyuFcpyI4y9sBXxQVN588qhPpjvW8HaBd2wbYvsMt4jMPwYK8bCih0Op VCLd04sAfvU//5WEajU+/Ud/+NWH/yiP6TiTAKCQEAiGEu3bx5Hwe//fFp7nMDt2nmtffJrP/5rD SBoWbi+yeHeZe+/eZeXeMju33yH3wTuAomfiHINXXqDv/NOkx89ZMLDG8yNOGCuNJQQRZvGqdZsQ JPwbgYX5R1phEBkJg2DlvnW7vBltg49GiszM3xpXYMVD2oQgpuf/5ONLJr3IqDsi/x0xgJgxWiR4 GCgcX96fsHzgOHAc/WwUjhQCL+GxtrrKf/iTP6G3p5tf/Yf/KROTE3z7W2+efHJPyDiTAIBsoOOs NXa4ErRSrGdrrO800LfMunlyeIqpZ8/zS5/9PJNDUDkos/jBAqv311m+u8D62/+RlT//A5J9Awxf +zhDV14gOXEOofwj0tk/vKE0vHB1jNnJQd554x3u3PwALRxDBuK6uKkU0kuCdIxAiAqgQxhExy3x kUZCgJSuAQQ7k8sHEKIO9vW1p+pQfrHD/6fzwzr650ODQiwp0N7HI3gKQghc1+WNN97gzTff5PLl S/zy3/slHCloNVungtuTMs4mACjHbzYax/oBwhHWAYQdfLlik51CixvLEoGkJ51grP95Ln7mJT7x SzDaD41SlY2lDdZX1tlc+jbFtTfoOvcMrYE5ApE41Qh+UEMKuL24S9KVfPFnPsF/9V/+JyzfXuL9 t99nZWGV++98m2arhZQyEgdxkylIJJGJFCKRAMdDJFJI6R4XBokH3KLrJiI1L6U1QqiYpNfxobVZ SmhtgoDOA2Zt+w0df9qbPRoohNvElYd0x2l0Lh+ElPhBwJ/+2Z+yurbG5/7OZ/nkJ16j0WzhBwpP OrR8/4muCjqTAKA8d7lSq+IIjRt1wJjpxzDkGeIH84r5X3RjCKjUA1bqAUtbynaGKXqSMNo7y8j1 C7zyMvQlNf29HvX8Lm+vSe5n/R86CAQK3nx/izduZhBac26yl0tPvcSXv/QF5qa6KOTKLN9b4f6t +6zdX2Pl/hKtRisiA5Ey1AAwzUUilUS4hjEIL2Elhh2DlpbT34nSgET1CY9y3ib9F0u5mVc7Iv0n B/XEsYdat9N3naAQbnR0SXAUFASe67C3l+Nrf/41At/nV//hP2B6Zop6o2mWUsJ0lDbqteVHOL3H dpxJAHAbppAnITUJV9KTcknLFgnp051y6O3ykNKPmHBS3Wl6+3vNDe5CtQkoaLSg0VKUDn3qrYBK 3ef91SaVmk/g6yi47eggqgT8YQ9pW461Uqxly6xuFfjTv3wf1QoYH+tlbmKAyec+xis/83kunOuj vF9nfWmN1XsrbKxtGpag1YwBAysMEolyhN8xOALpHsNVINwo6PgoI9yHUiIMUzwk0n8KKJwU0Dvm KYSfiH93Z84xdPnv3b/L2995i/Hxcb74hc+TSiZpNPxYb4NGCRsWeYLHmQQAAKGgGUh+fWyDv3nz Jm+9t8rKdg6NMAUtUlrjaa8LvVSS3r4eBkaGSXalGJ6cYGRylOHxES5NjDEwPEi6x1TCHdagUNYU DhsclHxKlSZ7hRalWtCuUguP5Yd0zoYdWCBdB6QgX6iwv1fknXeXCVQAQUB3T4q5ySEmJyd55Zln mZkeZWQowe7WPtmNLJmlDTbWMmyuZDjY3aNZb5DwfSucFrYDh4b8aGcWshNrHWcFbD/8nkGhYzzc UzB8g5o3/+YbbGQyPPfss7z26qsoFUSBUK3bZyXUEx4A4KwCgGuaVVqB4uMXRviJS5/HlZJKvcEH y1luLW9ya3GbW0tb5A9ruFLiSEkQKIqFMsV8yUSub942DTkh/bYD3X19jE2OM3V+mqlz00xMT/Dy /AwDI0NICbUa7BYCtvfq7BXq7FtdABVoW4jywwMEQdtDcDTgCJqNJguLW9xdWCPwA5SvcKRmaLCX sdE+pqZHeOWpa0zPjDE8lKZRbfD//OGf8zffuoUjLIcA2IalR5sctTYVjI6R4Ok04I58fPvA/3Y8 hTAjIJCupFo55MY7b1GtVvk7n/kMM7OztFqmhNvEg4QlLDGfNk1BP9pMz/c7ziQANMAEqtAEvgId UNNmhnx6fpJnLkzhfN7MCDsHJd5b2OTe+i7vLm6ysL6DH4DjmJvd6Nq3992sN1hfWmVtYcUCQ4BS mnRXF+OzE5y7epHZC3NMzk3x3MfmcD1o+bCXD8geVMlkK+QKdfYKVSI2mx/yRCMktt8fcMzyoVgs c7Bf4PYHK+D7+Eqh/BaDA7040lbxCYGURhgEPpxpaGHnYh1bu9N2038woGB+c8eVbG9tcu/uLbq7 e/jUJz9FOp2m2Wza8wq/K5adQNsW5Sd7nEkA0HVVqFarhC1mykaKTN2Lil4TQtPdleAnn7vI689f xHGMi3hvY4eFtV3eW9xiMZNjdfsA1zGdcOHyoU2vYy6xQrO1tklmJYNWyirrKEYmRzh/+SJT87PM XZrn9WfPkUgnabUgm6uwvVdha++Q3f0a9UaA9eIfaSilaQXKLmMsLwDKWoABvqgmQHUWvZw0jgmD uIJatRZ+2GQBHJP2DD4kcYZWCh3nS48ZqTgCCsAJlOF8aFAIlYGX737Abm6LmZk5nnv2OUDbtKhV CD62fDBfpoR4WIf0Yz/OJAAkEu6u7/uEarBmbacjrntTPdt+TWtDiqGb5oefGxvi/MQQX3z1OlII SrU6Cxs53l/cZDGT487qLuVqA8cRkYttugolJkjuRBe+Uqzw/ls3effNG9FNNzA8wIXrV5iYm+X8 9cs8/coMWjrkCzW2c4ds5w7ZOahQqbaitNvRoRS8+sIMX/rJi9y/dZ/vvvld3n/nDnv7RWQiidfV hZvsQiQSyEQXIpFEOi464RmmH9U2PmNEcSW+k4dwZCwGoI7FOk4b5hqLtqBofJ90Vugd/eCjgULn h8M4T6vZYGX5HrVahevXn2NqcprAFnBFoiVCdO6s3T4YdTw+yeNMAgCE6044zfjDnHCkGx+Bgcld +4GOPEJHCp46N8b1cxM4VkFyc7fAYmaX95ayrGzts7CZAw1OGFyMGHpAOjJi+gWolqu8960b3Hzj bZQKcF2H6flzTJyfY+7qRT52+TypvjmKxRrrO0V29w7JHVRoNP3I8ZAS3ryZ4VvfXefSzADXX/sk P/ePfhnRbLB6f4XbN26xvrjG8v0FmrW6DXraCL+XwEl1IbyUUQ9yEwjPBS+B9IxgasTPF0vdOVpE ir1Ci0eeHE0tQDsIGHfV2ytuIhv88KDQ/oTxhASlUpHtzRUc6fDCCy/T3dVN4LdAiCjK3xEtDJcA FhCE0GgFzcD/cTPQkzcaUQS4bdhxgz/N+DURL5z5f1Rjbz7Wls8a7OvilevzvPbMPEIImn7AWvaA D1ayLGX2uJ/JkT0o4zjSMunEOulOAIWt1Qyby+t852t/TRAEjE6OMXPlMlOXzvPixYukXpxlL18h myuzvVukUDT3pRCSxY0i91b3+eqf3Sbpai7Pj3Hu+Y/z8pe+wIVzvRR3D1m+v8ra4jKr91fZWFqn uH+AiNcDxNh+SCQQjmcEQlwX0ZW2gYOwHdgEAE+k6T5p2JxdvAS4sy04DNaFZvg9goItVMrvZSnl d+nvH2Bubh7HcfBDGvNjh3wkaGAfaC3RWuE3muuPdpKP5zijAAD8bRi/7vQejr2GT8tvvz87OsDc WD/y1WsgBIfVBguZPe6u7bCY2WNxc596s9X2EmJ19EKaNbzEwcWjlC9y61tv8d4bb6IDRe9AL1OX LjB77RovX70AyTQ7uSI7e4fs7x8aIRHXrP3vLmxz524G32oB9PemmJka4Nz8Nb7w2mvMzoziodle 3zSKQcsZ1pfX2FzZpFI5RDRbSGGMXQDu0Ah095o0oBAkPc8A4yP/EqbzMNyfue5HewhOCerxaKAg pECpgOL+No1mg5GxSUZHJ9FaEQQqivITi/LH3f1oWYBGSgelNG+//WYrkUr9n494mo/lOJMA0ABs 9k6cZvxtAIgZNUeMX+kocn3c+DsBBsAPgvZrQuM6kqfmxnj63JgNrmm294rc28ixtHnA8tY+G7sF DOOukeiKGH+FQDqOESb1oF5vsvjeByzcvAVKMTA2yuTFecbn57n2sXkO64rtbIGd/SKB71uQAS2h Vm9wd3GLO3c3CFSAagW4nmB8pI+5uXFmXniBV770WaamhiBQBhhWMuxkttlc3mCn3KAc0PYSlDIa gh8mBmBpv2Mr7Mj+2jgQq8i02xzb1wmgIIWk1axTL++D0ExOzZJKpVHKByzQ6DAEGmMROLL+10Ij pMvh4SFvf+etXYn7K7/92//iPz7aWT6e40wCQGZpaXOwe1BrrcUPy/g7gopoUEZAs/2aj0Aw2NvN a0+n+cmn55ESGs0WmVyJO6s7rOeK3F3boVhtRAYcBhgBm30wbno5X6T4nRvc+bbRAxiemWLiwgWe u3yVIDHJzl6Bg70SlWrNchYKCIN4UqB1wM5ugWz2gMAP0KpF4Ad4nsv4WD9TU8OMz81z/bVXuHnj Nm+8cRNHuG1OQEty+mhD2yxAO31o4iMiuuaRUdI2+yhF+CBQEIJWs0zQrJJIJuntG0Y6jp31Q3Vi QYwAoH0EOvYYjSs9dnd39Pvv3bjlyoGf/+3/7X96osuA4YwCwO/+7u/W/+ffelUbTr0frPFH28WN X58ACNoUJ+noWEJuAc3USD/TI31RGq5cNVmH1ewBS9kD1rYLNP0gWjaY7Qw4hIBwsL3D3uY27//V N0j3djN2bp6ZixdwZybJl+rs5Qo0G42O62SaogR8sJkiAAAgAElEQVTClaBdw0eoFbvZA7a3ciZo pkyZs2PZVKUt5NFH2TweMKIlVEf0LnwjvAqdoEDHOyeBglH1lUEVqZu4qW5SXb3m91UhYsSNnigu FL0W0yRwXZe7d+/o5aWFPyRo/urv//7/Xnmkk3vMx5kEAAAVmMjzoxu/3e4HZPztGMNJ8QeTr9e+ Sct5Eq6fG+fp+bGozn8nX2Fxa4/V7QIr2QN28ocmmBilIc2SAcehUW+yfucOa7dv4zgOQ7MzjM1f wBsbplBpUMqX8f0HkJvY/QrXMdRf0TraBgEFaKE6DfpBQwhDCaZPEOcIvzB67dFAwZEBSWmi+srt w3ETKNuwdFyDIDR0TccRaFv3gOSdt99q7eX2/4d6pfQ/fvWrX20LF7d38ESOMwcAX/nKV54uFov/ dTLVI7UOTjD+zqBfZPzqiFHzt2P87aBiZ0CyA5jC9bBuZxy0DtABGOZPzUBvFx+/OsOrV2fREhrN gPXdAivbB2zkSixvH1CpN2Negkn7aWB/I8Pe2joI6J8YZ3B6FjE8RKnaonb46BOdEAZsCML19MPt QghBuXwIyHYMICy6C7cJH3U4FSeDAkDKg3RCo7RLw3cBQ3kmxFGDD3+XTm9AoBFIpCNoNuvcePut UqD4x7/3b/7lH5x2Gg890ccUJM4MAHzlK1+5Vq1W/7tUKvXl119/3e0fGOLm+wsdVXXGsB5f4z/5 O+wxBwFoaOrQ8ASzo32cH+9Ha+OiFw5rLG/vs7lXZjl7wNb+IYEyhKJSmNbf0k6OYnYXAfRNTNA/ Ok6Q7qNSqxGcpgdgh3AMLbgSysh9PeItr6zgnrA1BG3PIWbkDwAF47obMBnodUi60GpJak1b6d9x INbQ7XUXJ6z/te0CzRfyvHvjnYWE6/3S//E7v3Wb44b+YYz6+/nsD2ycGQBYz5bf+OUv/+zQ+dkp wNx0z16/SKFQIl8oU67UDJGlxpIG/nCMP6x+O3F/8SXBke+IwCMCiaPgYVxwP1B2O5+kK3lqdtxk HbgIAjJ7JTb3DlnfKZDZL7JXCusHoJDNUshmcV1J99AoyZFRmsKj1WhyEpuKg2UAUoB89CCg1kb5 R8cUQuNBv86agOOgAIbsdHwwhSsFxcMGtYYVf4liCUfW/B1GH+7HCJW4jsfa6jL379z682RC/oPf +Z1/uX/KoX8/M/9Jn/2hg8KZAQCcxNBytsbS1j2G+7sZ6Esx1NfN/Plp5u0m5cMKB/tF9otlioVD IrIXaeXEiRvySUsGHhPjP/n7lV06ENjthWCkr5uR3m5evDCGkJJ6s0Vmt8RWvsT6TolsvkKp3qSZ zUJ2m2Q6TXJ4BJ3upYUk1MrW9jpJx0Gho6KmR7EQIUzQLjzeKABPaLbxfXWCglKCof4kk8NplFJk c4c0W0fc/WOsRKHbH+7v/2/v3aPkOM770F9Vdff09Mzse/aFBR8ACJIgKZGUSAriAyRIRaKkyInz sGw5jo/j4yiJ7XPto+v4+J5c88TXkp3Iuk58mXvlHMWSrFi2KOsVUXxIJCG+SZEACQIkQYB47Xux u7Pznn5V3T+6q6emd2Z2drEgQGK+c3qnu7qmp3emv9/3+7766quABchlzA4eeJmfWTrz5zPTp/6P ffv2uU3+jXNl+d/xuMLFAwAIqC4HQa5QQa5YwYnJZVAGZFIWBnosDPWncOml47g0fEelamNhcRlL ywXkV4qwHRf1B5CsUv4GZT0b5W92vRaf0anyR2Ai+wHRasQQAh5CcIDARLYHlwz3YveVwcSj5WIV s7kSTp8pYD5XxNTkJASARE8P0NMPqukACdYBpGGyDJfX7kB4WKG4HoCL6UzI8RtAIVzu7NKxHmT7 LBTKNUzPFSBCV0L+5tG14iygIWNIgBIG3/fx8s9esl3H/tff+Op//0abW34nLP874jJcNABAowSP QAgAyoKWcqWGSqWGqbklgACZlIm+TBL9mRQu2zqGy7aOAQiSbRYXczizlMdyLo9ypRaWCZM+w/lR /lVsRPmslsovWcwqZhPcTz1HQcBMaNg+OoDto/3RXIPp5SLmV0qYWixiocCxSHQIkUKPxkF4nSF1 IsFwLJFribRIIVaG6wAYBsM124ZhJXTMLOYxM5eLahEEvQXUiP6q/H4IyKFKjWgoFAt47ZWXJ7nw f/F/fu0rL6BzRW0lm235yRrnNyQXDQDUnGoFlFhyYVcJB9JNrBd8IKjWHFSrDuYXCgDlMA0DfT0W ejMWJiZGMDExAgDwPA9zC0tYzhUxv7CMYqkSDJFJstqB8jcoZivlb3Avzlb5Y64FlM8IHy3VzREA wAFPNGYxDmUsDGUsXHNJFhQE1bBQJhHhikVCze1vI9GQoSwLLNVUIfykDgqcc4wM9uKq7cOgIHjj 2AxW8hUwFk7dDRU9msQbWv7GWwnaKBXQmI6Z6SkcfeuNpz3b+8wDD3x9pv3drinn2vJvqptw0QCA 57hTFHQnR9wlDBBAmYcTbGEyDSEUrudjMVfEmVwBFASmqSGTspBJmdgyNoKJ8RHgGsD3fSwurWB+ IYf5hWWs5IuQHjIl9N2p/A33ErIbmcUYXg8isLVaWC9BCICuo1qO4ICcAai01n+QkLVwDly7YxyX TQyiWKph/+vH4bhBApSIrLtAg3+vRgqV84QEJcyPvHlYzM/N/PXhgy/924MHDzrYfMvf7v1na/nP mhVcNAAAOeYbHkSFXqC8qo1o/HUIEE7OIXBdH7mVInIrJQBnkEhoSFtJpFNJDA0NYGR4MHrf3MIy FhdzmJlfwpnFlUBZCA0/T3URWit/43BhC+UXoqnrsWHlj9piyq98llR+0XC94DP5OqbKcxFMBmJM wm9oo0N95QJIaAy33rQTA30pnJ5ewMHDJ0FZOHVXKMof9+/lbrhDECQr+dzHwZdftGtO6fe/+Y3/ 8d/UXk2kE8vf8EkdvL+dZV/rfKtz6waCiwYACMJFbFUdl5a/idKrr1EfNbIdFgulhMD3OArFMgph 0ozGKDKpJJKWieGhPowOD+DaXdsBALmVAqZmzmBufglnlnJwXT9aH4/gQlN+FaA6V34hOteY4B1h DEDII/n5AeUfy/bhtpuvgsYoXn71KE6cmoeus+jzVkX5I0BQz4evjKFSKePwwVeWHLfy6W/9zdef kr9u01vrrE3KRvz+jVr+9bS3lIsGAJr/xnWFbmAEUWCgsU/8tRl4SItUKldRLlWxSJbBGEXSNGEl TfRk0rhuVw+uCwFhJV/G7PwZTE0v4MxiDpWqHXivNCwRukHlFw3vbaf8MdBZ1da58q8n8Bf8D+H3 JoJEIBGLAdieh5uu24YPXHM5KtUaHt13AIViCUxjYZ9Q0eU9oq7o6pUCF4VCoxrOzM/jrTcPvWpX yr/wgx88cHKNW2wGCmsBhbq/mZb/bNubykUDAJ7v1ITwQRFMu23QcQB16qjEASCDg/V2lQmobdKG N/yaNLTrAqjWarDtGnIreVAKmIkEdF1HOpXE1Tsvw9U7LwMAlMtVnJ6ax9yZHKam51EuV8PAlrLq 7Tus/HEWUgeDjSs/FwJD/b2AAHwe1BCkodsgAGiM4Oc/cgu2jg/i5Ok5PPHMK/B9Ecw2lPcZ/WAy 5Fe3/EJxCwiloBrD28eOiNnZyW+/feTgvztw4EARzRVU3sJ6JO4ttgIEte96Lf9G2tf8Py4aAKjZ tdPCx/uUx6YBARpYflPcJ6v6yRbScCL2NJA4uwAggFqthmq1hpV8HoQQJIwEDNOAlUzi6isvw9VX Xhbcd83B5MwCpqfPYHruDJaWC0F9QUKV0cdWVH0t2r8O5Q+vIfd5rP+6RQQTiuKPqOcHlP8ff3Q3 LNPA8/tfx/Mvvw5D18PcfUXR5Q3FlD+i+6QOyYde3e+XKoU/+vY3v/anHdzdZvr8cQUVTfo0u9Zm AMGabOCiAQAgqDhLqaSeRFHmJr9XjP7HDD8aTqh9gIiCNjgPDe+t/x4kLIRRs21UazXkVgqAEDAT CSTMBJKmiSu2TeCKbRMAgpGGE6fmML+whBOnZrG4nEeQR0/DjMVOlT/u83ei/GJzlL/hW6jrheP6 uOX6nfjI7TfAsV088MN9mJ5ZhK5rQb/YpJ16Oq/SJqQDEZT/cuwqDh88kHe58+vf+duvPxj/JVre VmeyEZ+/Vf9zaflbsoGLCAAEZLGaWLA/kDXof71fI/2vA0OsY9PrkdW/mlTIcCckrajaNqo1G8tY gRBAQk8gmUwgnbKwY9sW7Ni2Bbd+6H3gnGN2bgknT8/i1NQ8ZmYXIcfDaXhza9H+xrhCLPgY8/tX VVE+SxEI3AHP5/jMP74LV+3YitNT8/i77z2GWs0GY6xO55sW7Whh+RlFPr+MNw8fOlIr1X7xoYf+ 9s0Ob+lsrP9aSh7vIxXzbIFgwyBw0QCADhIl+gAInqdV9J/UD1RRLHojMCgdJf1X+7ZyJ4DIp441 1f8I+YgHF7AdB7ZjYzlXABcCiYQOK2kibVnYMp7FlvEsbg2vM7+wjJOn5nD81DROTc2H8+BlSXLS Rvnxjio/ECh/dqAP//SjtyA72IdnXjyIHzz8DDRdC2odIFaXv4H2QykDHn5bJFiwZfrU25g6deLR E/m5X3v18cdzaP1LNFx5HdKp9V+LhsunZD1AsGltFw0AcI06M1OTGBkbh2EkQARHvWylQsmVvUZa 3+SiCl4013Wyql/8N1nNBGKidAg9F1ACOI4L23GRy+XhC4GEYcBKmsikUxjO9mNkeAC33LQLALC4 tILjJ2ZxYnIGp07PoWa7wfRfEgTVWtP+c6n8Ap7HYekU//TeWwBC8dffehivvHYUZkKvf1dCXb1v NQsgai2/EHHfevOQWFmc+y/fe+rH/ycWF9tUNmmQzfT7m/Xr5HynQLCetra/1kUDAD1a6ndfevGF /SDe3b09fbuzw8Pm8Og4RkdGoetGUDACgPz+W439y/0ovBRRAuXDpN6riq+4Dq3o/6rzkHS89f8l wjJcjuvBdovIrRThCwFdY+hJp2AmTfT19+DmwT7c/MGrAQCFYgnHjs/g2PFJTE6dwUq+FCbVBHUB Wit/+3vpVDgX8LnAJ/fciImRPpw5k8P9X/l7LC4XYOha5KrUFR4IvvEWgBAWOPFcB0fefLVcqVZ/ 54ff+ebfhG+kq25A+frW2R7cSGd9O7H8a7kRaynzmgrepE/Dcaeo956S++67P11j5Q8Tz9ujUbYn lUrdNDwyZoyNj2N4dAQa0xGVC0NYqkrSeyIVW9kP/e2I/ivt9WNZfVa1tsH9NETYUafiDQG86Lx6 3AgQDf572M7BIXwBXdORTifRm7KQTlswdD36PsrlKo4en8bk9DzePHoay8uFoMw/CETIFNZr+QmA XK2G5Wqtod3zOIb6M/i//rdfwWB/Bq8dPoa//Pr3AYhwnUWifL8k9r2TVeeDtf00lEtFHHnj4GSp Wvrlx3/0nf2d32nHIlrst+u3nvPN2uNsoF2/DfW5KAEgLvfdf39aLJQ/zAnfwwz20Z507w0joyN0 fMsEstnhINdcAAhnyMk14ilioBA9nPV9FRgiWy8UJYaquGGPhtfG85ElDt7Y5H2NABBv5wC474Mx it50Gpl0Cn09KSQUQHBcD28dO41Tk/M4enwSk1PzoJRF6xU0jpo0l2YAULUd3HrjLvzv/+rnAQA/ eOhJfP/hp2AlE00Uvonyo/E8pQRM07A4P4O3jx55YXll6pd+9vTTZ9a4rfAXaCmdwtxagLCRzziX INAFgE7lS1/60kDR8W/nrr+HMba3r29g19joiB4AQhbB11ZnCAQiXKNvtdKTOADElTKi2jHr30C5 YwE6NFH0OCtocj7aR32fcw4ugnUH0ikLfT0W+nt6kEwY0fchhMCxEzN4+/gUjrx9GlPTC4CQdQWV tQoUUQFACMBxHPybz3wCH/nwDQCAz3/pqzhxehYJQ4++IxlEpQ1Wvq7wQfyjrvyUEJw6cVQsLsx/ df+Lj//+/Px8bdWNbI6I2Ot6+6zX1WjWHp9dcbag0AWATuXzn/98llP9w77v32vq5t7egd5tW8bH 2ejoOIaGhoIEFSGC1UbAobKCCBjQqICraTvQ3Lp3Qv+bAMQarGCV+xHu83CpM0YIMpkUMqkkBnvS SKeshu/k1NQ8jr09hWPHp/D2yWlwHi5eIlkCAgCYL5WRSZq477c/g0vHh3Fqcg5f+n+/Cdt2wiXH VLDshAUExTohOI4dOeQUS5Xfe+QH3/grnN2zvA7npqH/Wkq3ESVt994NWfl2x10A2ID82Z/dv9Xx q7dxn+/VE/qeoYGhHcMjw+SySy9Bf/8gwHmQ2kqCKjdESFuPBvrfqKCN9L+BFWB9ir4aIFSgacIK Gt6n3h+CBT5E4PJkUhb6Min096bRl041fCdnzuTw+tHTOHZsEicmZ2FXXSxVy5i4dBz/8bc/AwB4 4umX8dffegiJRCLSVhlIbab8KqsK2igYo7CrFRw7cnChUi7++uOPfv+pzftlG2Qtih8/1065NwoE 5xoEugCwGfKnX/nKOBZzd/s++Zhu4PaxLVsmxkdGyPiWLRgYGAD3fQjhw/cBDj9aenvD9L/BajdX epXmt6L/q+IQcfYhP1MFl3BUQHCOTNrCQG8ag70Z9PekG76T5eUVnJpZxA3X7gAA/MVfPoBXDh1F IqEjLmrMpJXlp5SCMYbc0gJOnThy4MzCzK/uf+Gp+MKc632ez4Xlb3XuXIFAFwAuJLnvvvtoT0/P Np/Su4Tj3CmEuH3r1q0TExMTZHx8Aul0Cj7nweKZ3A+q4cYUvTn9P7vg33pYQQO4oN39AX5QHQRc AGkrgYHeDLL9vRjqSwMgWFzK40//618jXyhD01qPyDUGUckq5deYhqnTx8SZhenvHXjh+d9cXJw8 V/5+8M82vrbrt16FP1sLDrSPB6zrel0AOMcihCBf+MIXrrAs6x/YjnN30jRvGxkdHRwZGSVbtm5B 0rTCcXE/XGqcN1p/tGMCzRVdPY8W7Z3Qf6isIPb50bUhGj6HAxB+EEfoyVj42v/4ezBdqy993kJW swCp/AwAx8ljr3ul0soXfvzgt/8rGhWgSQbGmtKJkjU7vx7L3669ncJ2cn/rYQFdALiQ5L777qOW Zb2PEHKn5+OOZDKxJ5sdHpjYOoGJiQnougGfc/jhSsKcx9lAc/rfjuar7a2Cf52wi6bnY/cB5X2U UTzwzQeh6Z3lm8VZAGMMrmPjxLFDK+VC+Td++vh3HwcawwebLKrStgOF9Vr+TtvWc8zbnOsYELoA cJ7lvvvuo4lM5v3E9+8FF3dY6fSHs9nhzOj4OMbHRqEbZuAq+EEwTnC+IfrfjhXErb96nbXYxypA UdrZegEAyjCgxlDK53Dy+OtHlxcXfvWVnz15tM3b2h3HZT3Wfy1AaKfIcaU7Gyve7HijbkAXAC5k +a3f+q3E1q3bPugL5w4uxF1WMv2hkdGRzJaJCWRHRqFpGgQPFuPkvh88BfGgHUJFX8taI0bn2wT/ oLTH6b96/mwAAAhqBDDGsDA7KWZmTz96/PAbvzk19Xo+PB1lVmNzn91OLX+zSqfNFL7ZtdY6bnWN Vue6AHAxyH333WeyVOpDxPXvFiB3ZXoyN42MjBijo1uQHR4G0zT4vhe4C74AD4cYVln3Jta60Zq3 su7rcQ8a29fNAMJ8gMmTb/Fifvn/eeLRv/98cNUWc62Ut65xHN5d2+Nm59opaTMFbKZo6wGB9QBA u/d2fM0uALzL5HOf+1xqcGRkt+N4dwJibzrTe+PY2FhifMsWDAxlgyKlPg/jB364HFij9QeaKG5D 8K85ADSwgjXoPyEBAHzrmw9C09YGAEopXMfB8bcPV6rF3O8+s+9H30NzRSZYGxDWI51Y/1aWvxOr HgeKtY7XAwhdALjY5f77708v50t7PZ/fSQn29vQN7BodHdGHR0YxNDQIQRh87gEC8H2BYAmuQM4l /RdCgDGGb//dGgAQDvNVSgUcP/baydzCmV8/eOCpw2oPrE371/Mcn43lb6Xw67XyG1HkZue6ANAV AIoCfO4P/zCb0ZO7Pe7tZYTt6R8cuHZkbJxlR0bQ3z8YxA+EAPc5AA65IPJm0X8VUNYCAJndt7gw LWamjj+/MHnss0eOHFpQ/qdmln6z4gBrWf5myi3b4xH4tZS6Wf92x83248edMocuAFwE0mosnNz3 7/9kS4Gs3HNmbuEr267YiZSVwfDYKEbHxjA4lIXgAn4YTORCAMoow0bpv2xvBwAywWfy5FGxdGb+ qy8/9/AfVqtVuRJvXPHXUvpOXIJ2FH8ty9+MtseVbD1KvlbftfabXSN+z51cowsA7xFpCQAAMDQ0 ZF111VWFP/iDP0A6nUKl5mBqahqTMzNI9fRjbHQcI+Nj6O3tC7IUfQ4fwbThptZf7rcI/gkR1O5l jOGBJgBAKYXnezh17JBdyBf/8Nl93/+aPNXk/pspN42dX6+I2BY/18y6rgUCzY7jn9fJubMFgHWB wUVTEeg9LB1lwlFK4fs+NE3H1VdehmuuvgrVagXz8ws49MabePPwq+AAsiNjyGZHMTI2BiudDkcX /CjDT7KCZhJ/wkkT9k4pRbVaxsljh+ZzhdxvHHjmxy/KW4zdfzMwUBV/1b/Y6n/HauWVwCGU80I5 FxZcX9UWz0AU6zi+IKULABeJ1Kcji6AGAOfwPB+WZWHr+BiI7+H05CQOPP80uAB6+/rQO5hFdnQc g4NZjI6Nw0xaEJA5CM0X/xP1KCFA6n3kJJ+V3AImjx957dTs1L889fpLs+HptZSfoTUz6IQBSHCI W3753iDfuK7g8rrNgCHe54JX8nbSBYD3rhAA6Lv+VzNi2PxFxzmGVYtkC1EHhFBxCQRKhTwSOkPS 0FCgAuXlBbx24HlQPYnhkVGMjo5hKDsCw7IgPA5f+IBfdw/kh0dLsYfFEWamTojF2envHHz9ud/L z89Xwq6qsseVmjQ534oFtGIAcautKq1aMFR+jh/rv9kK3u567ah8p9dYV78uALyHJfuJ3/tlKtw/ Msd2bq2lPwjhOeu+hs4oBvp7kU4mkM/nMXf8DXzvyQMYtAxcc+mgGB0dJ0Mj4+gbysIwTPi+By5B BQABBRccp99+0yvml//oqSe+/2XUlbeVzy/PqUpNsdriN2uLi0rnW1n+Tq38Zg03bqTfOfmcLgC8 F2XHvfrY1bv+Su8f+bS19X2EC4F8aXFDj5rKEgBApwQncx7259N4bMEmff5L4pJEZfYD20ZOZnoH 3jcwPJwezA5jKDuMhJmE49Zw+q0jK9VS/t899+SPfoLWlrqVpZfn1gKD+HHch5fn4lSfYjUTaLW4 +UaV9Z1kEeuSLgC8B2X86l1/lZzY9enEyDbiuw4ADkI27xmkhECnDKAEBWMLOejUxp97ZH952Jt8 37137t6RtPru0A1z79BI9oMnjr12NHdm7tcP7n/mbTQqcTMFjj4itq8ex+MB7ViAtN481p8D8JQ+ 7ZS+U+mUvrd6zzvZL5IuALzHZPTn/2BPIjP46cTYTsIdGwBABAVEuyD5OoUQgFAQQoPhvmQGqS1X XpGbJZ/96pe//AUALwP4v8PecZofXaXNfjsm0C44yJT3SQZAw3YfjZZftsU/fz3SicK1G5Zb77U2 q190bhOfiq5cCDKoeZ+auHwHgecF5csBgApQCKxRk6NjoYSBUBYCAYOAgNYzDGIk7l3jrZ2k8rbr 04oJUAAGAoOmh5sR9pEMginvj3/GhsfROzzXSs77CEIXAN790vAQXZJhiWxPEjIEF5QsDlYO9j23 oTJPvf5eoz40q/sf9YWAy/3w0rS+2AmhIJoxuJ577UDauQgSEHQACQTKz5pszXIJNpp8c67eE5d3 7HpdAHhviXhxzoXBGAyNIjL5hMNPDuCRV0/B1CkSRlCYk4eVhzzPg+/70cYYC8t7B4U4JSAwCLx8 dApFbSBooxQgDIRSQHAIHrnRndDcje5LJZYKrjXZl1afKMftriuU/U4y7NoBSDs57xY/Ll0AeI8J I8Drix7u2dEHFqxQAgICnXA87FyDP37gebzy0s/A3RqSCQO6xiIlJ4REiTyUUjBKYWgaNEowNzWJ 7z/7Op6rXQJGKUDkBhAhwChTb0NVEHWYLa5sne6r+QGqkmuoKzlT2gnqQb5mFlTek0DjCEA7YNgI GLyTzGAj/bpBwFbygQ98QB8cHPwl3/f3cs5N3/ePM8b2VSqV51544YXC+b6/mNTHtgmEwwVemOf4 JzeM47njSzg2XwQjBBpl2C92Yv+TFWz5ySO4ssfH5X0EfqUAu1zAmfl5rCzOwXVd2NUS5hZzKB46 hsmCwBnaB2GMg4bMgoQAkNAAksjAtl2gdSxNHXtX9znqCqvu+2j02VUfXlV8NYIfHwJkCEAgDkCq wsetPW9xTp5f7/5GgKHdZ55tAHGVdAGgifzCL/zC9mKx+J3h4eH39fb2gnOOYrGIQqHw+4Zh1O64 444XCSGPCSH2LS0tPX/48OH1Z9icQ6EAKg7w8GngQ5dfio9c4+CZo/M4NLkM3/NBiIHTxuU4XRPg MxzC98D8GngSwDYOwX0IwsCJDvgu+IgDU3AI1wHnHhghsHSChJWGY/TALpeAmgMifA91BVcpu1Qu quyr6bYqCKhWXLXqquIDjTRf9e191JVF3TjqIwECATisFxiaKXUrloBYv2bvaddvo9IpoADoAsAq +bVf+7X327b9+Ic+9KEBxhg8z4Pruujp6cHIyAg8zzNt275jZWXljlKpBMMwVoaGhp4G8JQQ4qEn n3zyEM6PrycAEI0BglBQFhjPl84Ar1v9uHZnFh+9mcMpl/D23AoOnz6DycU8Ko4PEAGfGBBMBNOB GQM4B3wHAhwJTUc6wZA00iC+C5sL1Ix+0GQPeGEZnHtB4VLHPonViq/uqyAg26N7R6Pia6grua6c U/MCVCBRXQVfua4EBJX2q8qvJgYBqxlDqwyMc90AACAASURBVJl78XPx32IjMYL4NVodb5or0AUA RT7zmc/0GIbxnZtvvnmAUoparRYFyFzXhed58DwPyWQS6XRaBtD6arXaJ1dWVj5ZLBb/ZO/evQue 5z3GOX/MMIyfPv7448fxzgGCgAh8cpmXTyHAOXBokeBnpyogfg1b0knctOty7DUAAz48z8divoyq bcN2HTi2CwqgVMyhkl9GsVjE/FIJc2UP3MzA6ulHUicQ8RmwRKiWEmgNAlLxfdQVPR7EU0FAVXx1 rF9V+LgSqwovgUBuKitop+ydKv9mxwvisum+v5QuACiydevWP77qqqu2DQ8Po1qtwrZtOI4D13Wj TYKAupmmid7eXnieR1zXHalUKr+Uz+d/qVQqiT179hwVQjxOCHmyWq0++uKLLy6t554u/eU/HoPr fg5E+zmaSPb6Ts31a6WfMUKf8Fz9yRnz8Kt44IE6fSXklG8XAQwrVwmWNNco4Dgcb82W8drbZdQq eVQLOVTzOTilHOxiHsQpAk4FcMqAUwShOojVD5LsA031wzTr12whcRcg3i5FjeTHlV/dV6P+qqVX gUQe+8q+VFAfq+m/el5lAq2Oz0b5OwUGtDnXqWvRSlqe6wJAKF/84hd3UUo/e8MNN6BarQaBMNuG 53kREDQDhDg78P1gim1fXx983yeu6+4sl8s78/n8Z4vFon/77bcfpJTuc133YcMwnt23b1+p1T3t +Fd/8c/g1r6cuvLmfmNwAtz3AOHDrRY/5a7Mf8pemcOl+WuW3X+07SlG6JOuhodFtWw3C8Y1aB4B GCXQGIHOKHyNQWgauK4BQle2BEBYONzX/prKUauHTfXdmw3dNRu/l33V96uZglKpVVdCKO1yU0Eg HvVvV95rs5Q/LutR1rNlDG2lCwChOI7zb2+77TbNMIxoOEwqt+/7cBwnAgMJBHJrxw48z0MqlcLg 4CA8z2Oe592Qz+dvKBQKv1MqlZw77rjjZwAeIoQ86TjOS88991wVAK78jS//G800/6Jv1ydZMBnH BSUawAlIqg+G1YPk6HYAYsCvVn6ulpv6OWd55ouO7bsQsXIcBAAhUNcXFISDcIKgKJR0GZRnWXYk 8gKIVE9O7+VhIxFy+XMKrH745G2sNW6vo6746vi98skNChhvUy1/K8UXWK3UaNLWCSuIv7az/M1e 0eR4Peda9VvPuS4AAMCXv/xlq1ar/eLOnTtRrVahaVqUJGMYRmTVXdcF57wBDDzPQ61WawoIcXYg GUIqlcLQ0BB83zds2761UCjcWiqVUCwW83tuv/0pu3fi9XK653ezH7iXCacW1OzjGgTzITgH5T4g OHwuILgHlsqAWVciOXYlAfcM33MCg00oCAmSejjnoVILQHCAC3ARrFoswuP6eUUfSGj9Ze4/oaCM gVIN4AKC++DCB+d+EDhs9PnluL3cpKKrfr8EApXqx2cGqteTE3mE0iapvxz7jyu+CgaqtAKDTmMB nRwDG1f+jVD/jq0/0AUAAEChULh7586dA57nQdd16LoeVc0JKud44JxD13UIIZBIJCCEaMoOJAjY tg3XdSNAiLOCRCIBz/NgWRYymYwMKPba1conl2r+J/Mf+iegwoVPKKjvBYrGAwAIXn0QziG4BiE4 KOehMrIgI0/UnzkhNm8eAIBoivAqCaYcqtRdbiy23y5rT2UAQKPfLoUBcNVbQmPgT1V8L3ZOfU98 fy13oNn71rL0Z6v861HwThlDJF0AAFCtVu/s6+tDpRIUqaGUQtO06NUwjEjhJTNQAUFadskOpMLH ASEOBvHYged5SJsMc5f/CrhbhSAAA4VIGCCcBFV7fA8+9xqAgIfKL7gPcAHfOU9pCUIQ1CflSAXX 0Vrx1VegeSaRCgTxbCM10KeCgB/b1lL+du5Auz7rOUaL441a/rWu05F0AQBAtVq9JZVKoVwuR2mx jDFomlZfn17TIlAAECmsCgi+78M0zahddQMcx4lcBRlglMxBAgH3HEzRnThjjEO3yyCEwScE1KMg VANhFFTTQYgGIgh83w2WFOdeBABCCJBqs0TF1RN+ghz/s6AGETkP0o1BBANgotH6x5U/Tvfjll9e WbXearKPp/Tjyqtq+ePKrwb+4tKpRW/WdjZWv9XxRt67YTegCwAAbNveMT8/D8YYTNOEYRhIJBJg jLUEBMYYEokEAKxyFSQgSDBQ2UEzQJCbcGz80PkAeK0AlxAQSkEoA6UMhGqgNEjBDdo1MMYgNApN GBAiWEGYgIMyHYLzcLltAKCBuxDmBwTl/4N7FiJsF7FhcxkHkBdRZw4SAlACEsYNBBdhTUFBAaTQ PLoft/wktgF1vx6oT/F1Uffx4+W9VPfARSNoeGi0/vFU4Wb76nE7xZf7G7X6rY43w/J3YwDrFc/z RmZnZyOllgCQTCZhGAYMw4Cu69A0rSkgyNlzZjhILmfVdcoObNsG9xy8sKgjV/BgsBIoY4ry0/BV 3dcgaBicC9mBpukgjIBSFipkoKxBEDBkySK+BfECEW+HCB+lKPQPOQGI0PCaCIFD+BCeDcF9B0AS jX5/s1RedeaQGvCTyuwp5zXUqb0aCJR91SE/1e9X5wF0qhSdKP1abRtR/LN5f6fnmkoXAAAsLS1h bm4OmqYhmUxC07QICCQYJBIJmKYJ0zSjuIA6XVaCgXQTZNyAENIw5VYGGOX54JyLBBF45k0HcMvw fKZYf9oSCHwqQUIDoQSEUTBfD615XZoG7M5GRHhNtcwY90FdbxmNDEDN3otb+1YWWQKGo7RrAGw0 Wn8Xq0HAR6Plb0f9W/xnbffXstrvpPKfleWX0gUAACsrK0vLy8uD0qrrug7DMCJ3IL6ZphkBgmyT 7kIrQGjnLlAkcOxMDUcWVpAyQoXXWDjsJhW/FRDUQYJQBqF5TZN2zqkQAkIAs3/oE8kb/+HlhLHX qrMnXrKnXj0e9oiP5ze8G3X/XbX0OgKll9KK+jfz+yUQxJWi2RfTCe1eD9Vv1taqT6cKfTasoK10 AQBAPp8/try8PCiVP/6q63rEAuJgINvjgCALaqhA0Mpd0KnAT55fAJwKXB4quOoCaBoYIQBlSgxg NRhQSuF7GoQfM3yrHvvmlYDWK7IoCLerSCUToFfcbILzG4VTuVEf3Pov+RUfKHul3Ou8VjjslZdf q546fBq+o47Hq0ogLb8ba1PXC4wn+cQpv7T+Kv1vxTRaSTtA2IginivW0O4zO5YuAACoVquPzMzM 3NLX1weVBaivKhioyr8WGKjBxFaAYCUSePS1OQjHg+uFiq0CAGMR3SeMRorPKAt98sbYAOd+ACBR lJ+CcC8avxeQOQ5+fUxfqM+l8kyRuv9fDwIyMKqBcw/V+dMwdQKqmUFSITUAw0SyZxCEainhuTd5 5ZWbwF3Rc/UdZWdx+jWvWjjkzJ86bC8em4bvS2UF6olDcoxfAkAz698q4i9BoBWtbod6Z2PNO31f q/ta67M2VfGldAEAgG3b/21ubu53CCEZXdcjEJBBv2asIA4IKkOQjMEwDCSTSei6DsuyonNquS1D 1/H6UgGn5paRSQbMIVB+xbIzhe4zJQbAYi5AyA4AgLIg3haMWgh4ngzq1ZVdVX4RD/7JuIEM/smA IqFgGoNTKaBw6jDSPT314GC41QOGBDASYOYYAEoI1dLJLdfu9soru/kVRSHKxRW/VnjTLi0erEwe /pk7f2wBjSm+UF7VsX5J71WfP+7/t5KNKE4nbkKrtnaBw/Vcp9Nz65IuAAA4dOjQ/JVXXvkvTp48 +Tc9PT1WKpVq8OslGKhMIL4vwaATd0HGEBKJBIShY99rs4BbgUdcENbIABrpPmt0DVSgCNNzCSMg oND0ZPT/bWoMkFGU5k9Dc0ro6RtoUH5EpcLqFYMawIAQUJaGnsyAU0oo1fqFW9vtlpd3p7fd8K/9 SmHBKy4fsnNzLxQPPvxTNNJl1W1QM/uaxQHOlWwECM5W8c+J5ZfC1u5yccjS0tKRZDL5Ldd1C/l8 vr9YLA7VajWqDtvJpB05ji+TetRXOb5fq9VQrVajfdu2o7ZKpYJKpYJqtQrhufjWCydwfD4PItN9 fT9YmjvcF74PcD8ovOH74L4H4XtBAlDYN3hfeJ5zMF2HnkiC6QaYbkBwH77nwvec6NVzbHhODb5T g+fUIDwb4C7ghxv3AM0ANBPQkqCGCTc3i4yZQKp3ACBaMC+AaSBMA1U3rX6u/qopfUOXQjPArF6M XboTg5dcmaK9Y5dzs+c2Y2Diyur80WfguzL6r/r4Phr9fvXcWgrSaeBjo/GCThS/Wds7qvhSugxA kampqWMA/gOA/zA+Pr7V87y7VlZW7qWU3p1MJodM0ySWZcE0zYgZxBlCM3agjizEYwfVlIlXj82C Oy5cj9UZQIz6R3SfBX4+oaRxmDDmJjiaBqt3qP7PyXhAOLsvSuiJS9PAIAG4A+TnMTC6BcxMBrW7 KAGYFn0uoQQQFIIGSUzMSIIwDURLBOsIYLV2cMGxfcjC5OwSlspVUEGRGNtJqGHe3G+XfyX34rfv x+qZfM1Sfpvl+8f+iYaPXo+s9Z6NKn2ztndE8aV0AaCFzMzMTAL4erhh27ZtHywWix9njO3VNG23 aZpGKpWK8gbU2IEKCnEXIQ4I1bSJpdwKCCh4WGbbb+P7yxjBqkCh6i4wBu466B25BKDB6j1BJqAP wYPkHTl/AFE2YKg7nEMmBwXPHgWED4P46BkaAQTguw4o44HygyNh9sMcGIfZOwgt1QfNSILqiYB1 +B4ED3IgEA57Cu4F2YO+jyt6XLz65tsoVmtBJSMhIHwBrX8LaN/Yp9jI5d/w508sojHw12r4T60N EJdzoUidjAy0at9owHFTpQsAHcrx48dfAvASgP/Y39/f29/ff3exWLwHwEdN07w8mUwSy7KQTCZb soNmrCBf0FEtF5DQNYBqAGUAYRA0AIIoys9URVcAga0GC0IpfEJQWl5ANtUTDNXFjVM4319E+02C 1AQA05Awk0j19ofKzEGgwRwYgTV0KdKj26An08GCI4QGrIAyyBoEhDLAZ6A0nLrMfHAeJENttRwc PvwmCpVqdA8yGMm5C31gImGOXntHef7EA2ht+ePWfz1R/43IRhV+Pe8/54ovpQsAG5BcLpfP5XLf AfAdAGTLli1XVKvVjywvL39c07TbTNPMJJNJkk6nI2YgX1VQMAwdFY+CuRVAsAAACAsUSIIBZRAh IHBCARqz/quCgvX93ORbGJq4YmP/JKGA2Ydkpg/MSMCzazAy/chceg3So1dAM00AAr7ngpfzTXIS QkbCNFBNBxUAJ3oQ0+ACFvNRmT+GxZUiaLQegToqAVCrD1rSuh7At9A4/NcKBJqJqkybAQadugOd tL/jFj8uXQA4exHT09NvAXgLwP0A9C1bttxWKBQ+vri4uNcwjOtN06Squ1AfXtRQ9giI4wB+o9JH IEBixwoQgLBGdtDgJmjw7CqWpt7C2JU3dv7fEAAQILoJw0qDwIfRm0XP5dfC7NsCwIfr1eCXnbqS UxYsFkLDzEXGQEljshLTguxgynRAI7iETuM7b56CRmngdVACzgnkCkNUCAjNAKfaBFYX9Yin+7YL /q03EWg9shEwOK8WPy7dUYDNF14sFk8WCoUfr6ys/CVj7Muc88OFQsEpFAqD5XI5bdt2OGGIo+J4 mK0EiTrgYeRdbr4HcD/W5odtPiCCTchRAC/wt6ORAu6jnFtE9rKrQSmB5zrwfQ++54B7LjzXhmdX weUogO8C1WA0QjcSMIfG0Xvlh2GN7YCgOrhXA/e8+ueF5cDrW6C8QmkLRivC4zAeMGZ6OHLoAHKl GgiCuRJbFp7GrvJL2IoFpEQFHEAVJryVKXNnyj1BKJ0rl8sVBIlBLlaPCgCbT/c3ImfDAN5xuRC+ sItJ9K1bt14P4COEkI8Zhn4LMXuMKfNqgNt1ZRfK86wyAtKEJcTbZBFPScUJxdDlu3DFrfeikl+G UyvDqZThVIuolfKoFXKwy3nU8isQS2+D+C6M/izSl14Poy8bPiAy/ZiuDkTG5iI0zlnQopmDMjYB quGDfSV885FnoTMGnwtcNfVdbB8bCD4nnFfACCAoRcX2US0XYdu2UywWXyeEPO44zk9PnDjx7MzM TAl1ENiMZ3kzA4jrjQ2cF+kCwHmUSy65pH9W3/o8G7tmJ/EVAIi/ymdGUn/pGjC2OmYQHetRYg7R DGy76R4MXHIFqsUVONUSnGoJteIKauUCqkuzsE8dCCYtTexCMnsZhAgvRVYHGRllAKtPVJJg0DAk SWIxgbD/RH8ShdOv47XjMxC6ifFj38Yt27MA1aIMSVlzgRISAkidqBaLRRSLReTz+YLrus8C+BEh 5MlHHnnkYJuv+p14zjcaGziv0gWA8yz61fd8XRve+S9AaUj5mwCA+tqKHawCgMZjZvVg+63/EGa6 B7VSHk6tBKdcQmHqCEpv/BQ0nYWx9VowpgfFPtCYX9A88CinIcdrFcTBINgHYbhzRwr/84f7oBEN Zu4o9vZMY2B4DEKISPkBNACBWpVJTq/2fR/VahW5XA6FQkGUy+Vp13UfF0I8TAh5+tFHH53E+Xu+ L0hlbyZdADjPol/7sd+m/Zf+F2r1h9l3YXVd7gbKvhYgROyAtQcEQqEPTmD77nsBQuDUalh87Qnk 33gGZGg7aM9oQDBIPLGIhoqthZZeDktqMVdAB2Mkcj1W1TNgDKah4/JEBfteOQowHdfN/QB33nxj kFSkrFCszlQkhETJU+o0apmZqdZkLJVKEhD8crl8WAjxBIBHC4XC0x0s6LqZGYLvGukCwPmW9/38 hGVZx/TxaxOeW4XwvTD116sH+xoYQBtAkIVAwhl7q+IFLAHaO4odt30Kcy98H/njB0CGdgC6CSDM 128xpFhX5uaZh63YguwPQvH+rYM49MbrWC7UYBVP4aMDc7jk8h3BpCjDaJghGfwbBKlUCqlUalVR VbXIilqfUT1XLBaxvLyMUqlkl8vlZyilPyWEPPLoo4++hHM7Z+BdI10AuAAkdfOn/8689P3/nBhJ +I4D7nvB+HqY898UCNYEBIUdqExAMyC0BEh5CUgNrWYOpPF1VRISC2sTxii/OhS5qi289h1XjeB/ 7XsJRNexdfIR/LO7bgTTjYaJVGqiVCaTQTabbSimqs69UBdkkUAglV9lB5xz2LaNlZUVrKysiGq1 WqhWq08AeIwx9vAjjzxy7Hz+/udTugBwAUjyzs9MJEjy1dSO3QPc59EwHfc9cM+D77vh8FswjNYW EBoYgQoUkh3QMIhIG12F+KhCdH41GKzOPIyNBKzqQ2EkErhqJI0XXgt0ba/9BG758O0N9RdUANB1 HVdcESQxqTUUWwFBnB2oDCHODjjnKJfLWFlZQT6fF5VK5ZTneT9hjD2WSqV+/N3vfndd6ze+m6UL ABeIZD7y2VtNlngwedkNvURPBrP2XBvcdRVGEAAB9z2FHawDECLGoBTLoTKpSAWC2LBiC7BomIfQ kKq8erhwy/AgPLuK0ws5pGuz+PjgGWzfeVVUM0G6ABIEBgcHMT4+HilvvYpx45JtcgGW9bKDeFm2 QqEgAcGrVquHCCGPCCEeO3PmzLMHDx4sn+/n41xJFwAuIMn8g9+8KsG9/8ysgY/pfVmNpgdBDSt0 C2z4jhsyAxe+7wX7nhcAg68kCbUNHqqJRTKZSGUHrbIQ9ShGsDrgKMf845Y/cBcECG66djsOvHEC ngCGll7Fp67tw9DwKHRdh2maSCaTERAkEgns2LEDlmWtAoD4Fl+haSPsoFkcIQwmIp/PVxzHeX54 ePixfD7/9Yceemjq/D4lmytdALiwhAJIWzt2bzeGL9tLEtZdzLR2G5mRfpbJEpbuDx5814bveQFD kIwgchmaxA7WCwiAEkhsNcy4Ol7QlB0wBkEobrnuarz0xtsgVMOWuSfwj267DgkzuarisiyWcv31 10PXdRBCImWX9L0dGGw2O+CcI5lMYnZ2Fm+88cacEOKmffv2vWdAoDsX4MISAaBWOfbckcqx544D +BqS/VZm1203asmBezQzuYelB65hPUPESA+BpHrhu7UgrdexmwKB3NAsmCiaAEDDOQ544TJjDeyg FSDU3YloNiNl4CBwfRdCBAuSmn4ZjutChHUJNE2LlNF13aC4aaiQavk0WVlZXXeh2SZLr8sFXddi B7LQS5wd+L6PwcFBHDhwAJxzjI2NjZ4+ffq3APz78/WAbLZ0AeDCEoGgHr6LoCQ2QzVXKb78vx4D 8FMAhr7l+rHMpVfuFYZ1F0v03Kr3DPTrmSz0nqEg395z4LsefM9WXAQvch14NMzYJGbQChBku+8C vtN6mJEygNQzECVAmMkkCqUq4PsQIGC+jXy+gEQi0WCB1RwA2w4qgkdZgcomA4dy1eN27ECu9JxM JhvYgVyQpR1DyGazePbZZ4NaBgAsy4IQ4hPoAkBXzrGoxS8coL6yjjv9Sml5+pWTAL6OZDJpbb/9 /cbAyN1UN+/U0kO79PSAYWSyQGoQwvXg+ja444L7Tj1e4AVBRSHZwbqGGRVA8MKy/fHYQZR3wAAw WBkL5VIlWI6cUzjVIsrlckOZNUm/HcdBMplEsVhsqKvQDAjOhh2sFTuglGJ2dhblchmU0qivpmm7 3vnH4dxJFwDeHSILXsiFMYNltqrVSuXQo09WgGcB/Il++TWjyZGr9rBE7116quc2Zg0MsswQSWSG 4AsO7tbgex54FDtoHGqUswnXBARfGU2QgBCxAxqLD1Aw9KFQrgDcB+ECVcePAECl3dIq27aN6elp pFKpqMiqHB5UU4JbgcHZsgPbtpFOp/Hggw+CMRaBQ/i+91TcrAsA7z5R2YEsoc0AMPfE4bJ74vBp AN8EkLCu/8T7jZ6Be5ie2MOsgev03mEtkR4EUtloiNH3bXC3zgzkkKMIp/LWg4lrBBGF3wgcIlje TxBAowS28IMlzYUHBxrsWm1VoVXVAs/OziKTyayqoyjBIL5W42ayg76+Puzfvx8AooCgGhdAY9ny d7V0AeDdLWqRTBdoWItPq7zy4NMV4HkA/8kYvjZrXrrrTs1K3akle29l6f4sS/cTPT0ETgi4E+Yd eMFQY+AiBHP4OfeiRKQ18wxigEB4UIG46tWiGoQFz0C1WgbTjMhCxxmAnPQjqynJNRXiS7SpC7du Fjvo6+vD888/DwAN7oHsj2DZsnZlyN410gWA95aoroKMHTAAzFk4VHIWDk0B+FsApnXtx9+nZ/rv IKa5x7D6btB6srqRGQJJD8N37DDvwA7zDeruQgQK0cjCGoDge+AIVhESQgCejYo1gnz+NKxUT0MQ UB2K830/UmhV8dWMwXPBDoQQyOVyyOfzke+vDhk6jlMDkAi/Zz/2+q4Dgi4AvHdFdRUABQwAVCqH fvQ0Anbw5xjY0de//do9SPbexczMbXp6YJRmssRM94fswIXn1bMS1SFGCQhCzmRUXYaQBTgcYBqD 5/JwtaBezJYtXMKq4E2UX25yleZ4UdU4GGwmO0gmk5H1V+9FiQ9MArDQuDaBrxy3W5XogpMuAFw8 ItmBXGwzchWwfKyUWz72LQRFTo3UNffs0nqydxPT2qNb/e/Xe7MJPTUAag0FTMCx4fkuuOtEQUQ1 PZlLZhDOanQ4q68k7nsAAVbSO+DXZjDiF5D2A3fDV5RNKlxvb2/bFZriABBnB+qKTCo7UNdnVIHA siwcOHCgwS1Rg4O2bb+NYAl0WZZMLVEGnPvViTZVugBwcYrKDiQgBGAAsPLhn7wAYD+APzf6JwaT 2268jVh9e7Vk761aqm+U9WSJmeoHCIPr1sIgogwmSnehvoKRJzgSjMCVKkIZiOAomeMoiDHo3EaP U0G2Mod0KgnOzYaEnGQyGVlwtahqM0DolB1omhYt8KIu614ul3Hy5MlomFANVJbLZdi2fRpAJvze auF3JmMvQD1A+K5gAl0A6Eq7QCJzclNl5+WpbwH4LgAjdc3du7S+4b1UM3Zr1uAHtZ5B08gMgViD wRCj74QxBNVV8OH7YUYhwiSicLVvCgGfJbBULqOwvPJWv7ZoJJPJCcuytHQ6HSmgTAtupvxrsYNW sYM4SzAMAysrK3AcB4SQBvrveZ5c1u0ogAEAFQTBwCrqjEpdXEFd5vyClS4AdCUu8ZyDICMxZAjl w4+9iIAdGEgPZjI7br6NpgbupMnMbUZ6YIJlhqiZHgSnWjCyEMYO/FoZcJzg0pQG44OCgICB14rw CjMn7dkT/18JyOu6zgcHBy83TfMGwzA+aFnWaDqdJplMBolEImIDqu/eDgw6CSRKZjA/Px8FA+MB wEqlUsvn81MATOU7AeogKuMBEkQveFegCwBdaSctcw4AMJSWisVXHvougB8CMBJX3LrTyG65U9Os PbrVewPryVpGZgikLwvu9cKpVeA74YxCzkGoBm7n4eamFvzpI38OYA5AyXXd6tzc3CEAfwfAzmaz 4/39/bctLS3t1TTtFsuyMpZlQS68Iv33jbADdd80TSwtLa0qOyaHJ2u12mHf900EAFlBXfGlKyWp zQVv+aW8p7KauvKOSiMYBMZEA2AA0JEe6c3suGk3TfXeSc3MrUbP4CUk2UcKc6eDtQN8D7w4C7Ey +6Y7/dqX4NZmEShVBUAJAbWuIWAgTrj52Ww2MTIystv3/bsJIR9LJBLXWZZFU6kULMs6K3aQTqdx +vRp7N+/P8oMlCBQqVQwOzv7t7lc7unw/ooAyuGWD19LsXu+4IGgCwBd2QyRPnCQorwaEAxz203b E8OX77VXlm9Np/TtKyv5eV6rPMWnX/kRAmWvog4AdrjFFwBZpVDbt28fTiaTH/V9/15N0/aYpjku 2YFhGGBhIdNO2IFlWVhYWMChQ4ciAPC8IHK5sLBQnZqa+jPO+RKAAoAV1IEgjzogSAC44Ok/0AWA rpwbUXMOVDDQIRlCkEzDUJ/wVEPdBG5IpgAAAV9JREFU0scVv1PRrrvuuvd7nncPpfRjuq7vTiaT iXQ6DcuyIqVvxw6EENEwoOsGU5ht28bs7OwLS0tLDyJQ/Fy4lcLjAuqsRQLXu0K6ANCVcy1qzoEK BmoAzcPqpJqzps/btm3rNQzjDsbYxwB8LJlMXm5ZFkmn01GCUbORhYMHD0ZpwYQQLC4u2lNTU1/0 PG8ewDLqyr8UvqrU32t1PxeidAGgK++kqIFE+ezJIchzPna+c+fObYlE4h4hxMcopXsty+pVg4mM MZimicOHDwMIFifJ5/NiaWnpb5aWln6MgOoXUKf8RdRjFc65vv9zIV0A6MpFKTt27Eik0+kPuq57 L6X0XsMwrksmk3omk0GlUkGpVEK1WhX5fP6H09PT/wn1GIV8VS3+u8LfbyZdAOhKVwDs2LEjm0gk 7qGUfhTAJ2u1WqpWq90/OTn5p6hP+JGpv5vmpnSlK13pynmT/x9S0uMXDcYeBwAAAABJRU5ErkJg gg== "
+ height="55.977482"
+ width="55.977482" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text6362"
+ y="740.11218"
+ x="259.57803"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ style="font-size:10px"
+ y="740.11218"
+ x="259.57803"
+ id="tspan6364"
+ sodipodi:role="line">sshd</tspan></text>
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#bf3030;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.875;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path6366"
+ sodipodi:cx="34.25"
+ sodipodi:cy="369.75"
+ sodipodi:rx="12.25"
+ sodipodi:ry="12.25"
+ d="M 46.5,369.75 A 12.25,12.25 0 0 1 34.25,382 12.25,12.25 0 0 1 22,369.75 12.25,12.25 0 0 1 34.25,357.5 12.25,12.25 0 0 1 46.5,369.75 Z"
+ transform="matrix(-0.57142858,0,0,0.57142858,265.05683,535.13259)" />
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#669b00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.875;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path6372"
+ sodipodi:cx="34.25"
+ sodipodi:cy="369.75"
+ sodipodi:rx="12.25"
+ sodipodi:ry="12.25"
+ d="M 46.5,369.75 A 12.25,12.25 0 0 1 34.25,382 12.25,12.25 0 0 1 22,369.75 12.25,12.25 0 0 1 34.25,357.5 12.25,12.25 0 0 1 46.5,369.75 Z"
+ transform="matrix(0.57142857,0,0,0.57142857,303.79141,599.36829)" />
+ <path
+ sodipodi:nodetypes="csssc"
+ inkscape:connector-curvature="0"
+ id="path6382"
+ d="m 251.56003,749.86218 c -1.20645,2.10718 -3.46039,3.53125 -6.0625,3.53125 -3.86599,0 -7,-3.13401 -7,-7 0,-3.86599 3.13401,-7 7,-7 2.57733,0 4.81629,1.39468 6.03125,3.46875"
+ style="color:#000000;fill:#bf3030;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-opacity:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ sodipodi:nodetypes="csssc"
+ inkscape:connector-curvature="0"
+ id="path6386"
+ d="m 320.13034,804.51843 c -2.14987,1.19474 -3.59375,3.49105 -3.59375,6.125 0,3.86599 3.13401,7 7,7 3.86599,0 7,-3.13401 7,-7 0,-2.5416 -1.37968,-4.74241 -3.40625,-5.96875"
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.5;stroke-opacity:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ style="color:#000000;fill:#000000;fill-opacity:0.48309183;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect6388"
+ width="59.384914"
+ height="24.950771"
+ x="-397.18994"
+ y="668.92932"
+ rx="6"
+ ry="5.9999995"
+ transform="scale(-1,1)" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text6394"
+ y="684.95953"
+ x="352.31845"
+ style="font-size:10px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ y="684.95953"
+ x="352.31845"
+ id="tspan6396"
+ sodipodi:role="line">kernel</tspan></text>
+ <path
+ transform="matrix(-0.61538462,0,0,0.61538462,421.09073,540.39317)"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ sodipodi:ry="2.4375"
+ sodipodi:rx="2.4375"
+ sodipodi:cy="222.6875"
+ sodipodi:cx="123.6875"
+ id="path6404"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <path
+ sodipodi:nodetypes="cc"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 344.99753,674.89879 0,-18.125"
+ id="path6406"
+ inkscape:connector-curvature="0" />
+ <rect
+ ry="5.9999995"
+ rx="6.0000005"
+ y="672.51501"
+ x="-319.43436"
+ height="21.371223"
+ width="68.30262"
+ id="rect6538-9"
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ transform="scale(-1,1)" />
+ <text
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="282.51901"
+ y="687.19165"
+ id="text6540-4"
+ sodipodi:linespacing="125%"><tspan
+ id="tspan6544-8"
+ sodipodi:role="line"
+ x="282.51901"
+ y="687.19165"
+ style="font-size:10px;text-align:center;text-anchor:middle">docker</tspan></text>
+ <g
+ id="g6586-05"
+ transform="matrix(-1,0,0,1,628.51509,0.210121)">
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ d="m 297.71429,745.5404 c 0,-20.64274 0,-45.28548 0,-65.92822 m 0,-4.4375 0,-14.31605"
+ style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:1, 1;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none"
+ id="path6588-9" />
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path6590-1"
+ sodipodi:cx="123.6875"
+ sodipodi:cy="222.6875"
+ sodipodi:rx="2.4375"
+ sodipodi:ry="2.4375"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ transform="matrix(0.61538462,0,0,0.61538462,221.61388,520.72527)" />
+ </g>
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path6584-2"
+ sodipodi:cx="123.6875"
+ sodipodi:cy="222.6875"
+ sodipodi:rx="2.4375"
+ sodipodi:ry="2.4375"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ transform="matrix(-0.61538462,0,0,0.61538462,384.46736,550.0429)" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:3.99999999, 0.99999999;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none"
+ d="m 326.49277,745.13833 7e-5,-40.43431 m 10e-6,-0.80071 7e-5,-16.84207 -15.99986,-0.0771"
+ id="path6570-9"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path6402"
+ d="m 317.24753,677.48718 25.25,0"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ </g>
+ <g
+ id="g3096"
+ transform="translate(2,11)"
+ clip-path="url(#clipPath3970)">
+ <image
+ width="197.14285"
+ height="173.2782"
+ xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAFOAXwDASIA AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3 ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3 uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iii gAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKA CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKjfrUlRv1oAkooooAKKKKACiiigAooooA KKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAo oooAKKKKACiiigAooooAKKKKACo361JUb9aAJKKKKACiiigAooooAKKKKACiiigAoqu17bqxHmg4 ODtBbB9OKPt1v/fP/fB/woAsUVX+3W/98/8AfB/wo+3W/wDfP/fB/wAKALFFV/t1v/fP/fB/wo+3 W/8AfP8A3wf8KALFFV/t1v8A3z/3wf8ACj7db/3z/wB8H/CgCxRVf7db/wB8/wDfB/wo+3W/98/9 8H/CgCxRVf7db/3z/wB8H/Cj7db/AN8/98H/AAoAsUVX+3W/98/98H/Cj7db/wB8/wDfB/woAsUV X+3W/wDfP/fB/wAKPt1v/fP/AHwf8KALFFV/t1v/AHz/AN8H/Cj7db/3z/3wf8KALFFV/t1v/fP/ AHwf8KPt1v8A3z/3wf8ACgCxRVf7db/3z/3wf8KPt1v/AHz/AN8H/CgCxRVf7db/AN8/98H/AAo+ 3W/98/8AfB/woAsUVX+3W/8AfP8A3wf8KPt1v/fP/fB/woAsUVX+3W/98/8AfB/woW9tmP8ArQO2 WBUE/U0AWKKKKACiiigAooooAKKKKACo361JUb9aAJKKKKACiiigAooooAKKKKACsK/vWupZIImP lRNtYD+Mjr+A6Y7n267tc3o8bPaQSKMzSrvyf4c8k/mTQA9LKYhWbbGvbe2OKn+yn/ntB/38q+ba 3j+aYhmPUsetN/0D/pnTApfZT/z2g/7+UfZD/wA9of8Avurv+gf9M6XdY7cZjxQBQ+yn/ntB/wB9 0v2U/wDPaD/v5V3/AED/AKZ0f6B/0zoApfZT/wA9oP8Av5R9lP8Az2g/7+Vd/wBA/wCmdH+gf9M6 BFL7Kf8AntB/38o+yn/ntB/38q7/AKB/0zo/0D/pnQBS+yn/AJ7Qf9/KPsp/57Qf9/Ku/wCgf9M6 P9A/6Z0AUvsp/wCe0H/fyj7Kf+e0H/fyrv8AoH/TOj/QP+mdAFL7Kf8AntB/38o+yn/ntB/38q7/ AKB/0zo/0D/pnQBS+yn/AJ7Qf9/KPsp/57Qf9/Ku/wCgf9M6P9A/6Z0AUvsp/wCe0H/fyj7Kf+e0 H/fyrv8AoH/TOj/QP+mdAFL7Kf8AntB/38o+yn/ntB/38q7/AKB/0zo/0D/pnQBS+yn/AJ7Qf9/K Psp/57Qf9/Ku/wCgf9M6P9A/6Z0AUvsp/wCe0H/fyj7Kf+e0H/fyrv8AoH/TOj/QP+mdAFL7Kf8A ntB/38qA2MwyUKOO+xs1qgWB6eXQLe2mX90wVh3Q9KQzMs75rW4jhY/6O7bMH+Bj0I9BnjHbI963 653V4GNpcb+JljJDj+IY6/UVvxMZIUc9WUH9KAH0UUUAFFFFABRRRQAVG/WpKjfrQBJRRRQAUUUU AFFFFABRRRQAVj+HQDpsL9/JjH/jorYrI8Of8gmH/rmn/oIoA5HxjrFzDfpDFK6Arv8AkYqepA5H Pb9a51tSuZ1LpNOJByyCd+fcc/pWj41/5Daf9cR/6E1Z+g6bBqd88VxJcRqib1MDhTnIHUg+tdkE uRM4ajk6jVyD+1Lv/n4uP+/7/wCNH9qXf/Pxcf8Af9/8a6l/B2kysXEuoK3dVnUfj9yo/wDhDdJ/ 576l/wCBC/8AxFO8ewuSfc5r+1Lv/n4uP+/7/wCNH9qXf/Pxcf8Af9/8a6X/AIQ3Sf8AnvqX/gQn /wARSf8ACG6T/wA99S/8CE/+IouuwuSXc5v+1Lv/AJ+Lj/v+/wDjR/al3/z8XH/f9/8AGuk/4Q7S f+e+pf8AgQv/AMRSf8IdpH/PfUv/AAIX/wCIovHsHLLuc5/al3/z8XH/AH/f/Gj+1Lv/AJ+Lj/v+ /wDjXR/8IfpH/PbUv/Ahf/iKP+EP0n/ntqX/AIEL/wDEUXXYOWXc5z+1Lv8A5+Lj/v8Av/jR/al3 /wA/Fx/3/f8Axrov+EP0n/ntqX/gQv8A8RR/wiGk/wDPbUv/AAIT/wCIp3XYOWXc53+1Lv8A5+Lj /v8Av/jR/al3/wA/Fx/3/f8Axrov+EQ0n/ntqX/gQv8A8RSf8IjpH/PbUv8AwIX/AOIo07Byy7nP f2pd/wDPxcf9/wB/8aP7Uu/+fi4/7/v/AI10P/CI6T/z21L/AMCF/wDiKP8AhEtJ/wCe2pf+BC// ABFGnYOWXc57+1Lv/n4uP+/7/wCNH9qXf/Pxcf8Af9/8a6H/AIRLSf8AnrqX/gQv/wARSf8ACJaT /wA9dS/8CF/+Io07Byy7nP8A9qXf/Pxcf9/3/wAaP7Uu/wDn4uP+/wC/+NdB/wAIlpP/AD11L/wI X/4ij/hEtJ/566l/4EL/APEUadg5Zdzn/wC1Lv8A5+Lj/v8Av/jR/al3/wA/Fx/3/f8AxroP+ET0 n/nrqX/gQv8A8RSf8InpP/PXUv8AwIX/AOIo07Byy7mB/al3/wA/Fx/3/f8Axo/tS7/5+Lj/AL/v /jW//wAIppP/AD11L/wIX/4ij/hFNJ/566l/4EL/APEUadg5ZdzA/tS7/wCfi4/7/v8A40+O/vJW wLicADJY3D4A/Ot1fCWlMf8AW6jjuftC8f8AjlObwzpRjESyaiEBycXC/MfU/JRp2Dll3MGXV7gj ZHNcBB3M75Puea3vCOsXL6l5DyyOu3cN7lsY9zz+FYWs6bbabNGlq1wyuuT50gY5z2wBVvwj/wAh 1f8Arm39KmcVyMdOUlNK56TroA06R+4Rh+BU1ftv+PWH/cH8qo69/wAgqX/db/0E1etv+PWH/cH8 q4zvJaKKKACiiigAooooAKjfrUlRv1oAkooooAKKKKACiiigAooooAKyPDn/ACCYf+uaf+gitesj w5/yCYf+uaf+gigDgPGv/IbT/riP/QmqDwocanL/ANcT/wChCp/Gv/IbT/riP/Qmqt4XONRl/wCu R/mK7YfAjz5/xWdlvIOQea5XxB4wNpO1tpawz3MRzMGyQB3AweT6+n54PGOsXmmaci2kbL5xKtOP 4PYe59a8706/fTb5LtIYppE5XzQSAfXgjmqURyn0PZ7ad57SGV4zG7xqzIeqkjOKl3V56njvU7hA 0VtamVfvxhWyw9V+b9K7mKVpIUdkKMyglT1U+lKw1K50lrpVhJaxyS2cEsjjcWkjDH8zU39j6X/0 DbT/AL8L/hUth/yD7f8A3BVmuNt3Z2KKtsUf7H0v/oG2n/fhf8KP7H0v/oG2n/fhf8KvUUrsfKux R/sfS/8AoG2n/fhf8KP7H0v/AKBtp/34X/Cr1FF2HKuxS/sfS/8AoG2f/fhf8KP7H0v/AKBtn/34 X/CrtFF2HKuxS/sfS/8AoG2f/fhf8KP7H0v/AKBtn/34X/CrtFF2HKuxS/sfS/8AoG2f/fhf8KP7 H0v/AKBtn/34X/CrtFF2HKuxR/sfS/8AoG2f/fhf8KRtJ0pFLNp9mFAySYF4/Sr9ZniGGa48N6nB bZM0lrIqAdSSp4oTd9xNJLY8z1r4hadFePDo+g6dLChx508I+f3AGMD8a3PB/inRPElx9hutHsrW +2koFiUpLjrjjg+1cn8Oda0TRry9bV9sckiKIZnjLhQM7l4Bxnj8qi0p4NT+KUE+jQmO1a7EiKFx hByxx2BwePeulxWq7dTljJ6O+/Q9M1e0t7O4RbeJYkdclUGBn2HavPtaXxPLrMi2bTLacbGjxtAx znvnOa9H8QgteRDoPL5J7c15vr+r+IbfU5ILCGVbVcbGSDfv46k4P5VdP4UKrZSZQnl8WHEcIvhG vc9WPqf8K2vC7a75041XzPJ2jb5uM7s9vbFc/wD234r/ALlx/wCAo/8Aia3vDWoa1dzTDU4nEQUF XeLYc+nQZrSxknqO8SnN1B/uH+dO8I/8h1f+ubf0qPxEc3MP+4f51J4R/wCQ6v8A1zb+lRP4WOH8 RHpWvf8AIKl/3W/9BNXrb/j1h/3B/KqOvf8AIKl/3W/9BNXrb/j1h/3B/KuE9ElooooAKKKKACii igAqN+tSVG/WgCSiiigAooooAKKKKACiiigArI8Of8gmH/rmn/oIrXrI8Of8gmH/AK5p/wCgigDg PGv/ACG0/wCuI/8AQmqp4aOL+X/rkf5irfjX/kNp/wBcR/6E1Zej3ttY3byXU6QoU2hnPU5HFdtP 4EefU/iM6y5ghvLd7eeMPE4wymvOLy1vfCOtLNAS0LH5GPR17q3v/wDrrtv+Eh0j/oIwfmf8KZJr uiSrtkvrZ1znDc/0qxPU07WcT20NwqFPMRXAI5GRnFWCQ43Dhh1H9axv+Eh0j/oIwfmf8KB4i0kE EajACPc0WHc9G04g6dbkHPyCrVeYf2zpUxZoNXkjPLNHBdyxqfUhVIH1qL+3dN/6D91/4MJ//iq5 3Rbe50Kuktj1SivK/wC3dN/6D91/4MJ//iqT+3dN/wCg/df+DCf/AOKpewfcPrC7HqtFeVf27pv/ AEH7r/wYT/8AxVH9vab/ANB+6/8AA+f/AOKo9g+4fWF2PVaK8q/t3Tf+g/df+B8//wAVR/bum/8A Qfuv/A+f/wCKo9g+4fWF2PVaK8q/t7Tf+g/df+B8/wD8VR/bunf9B+6/8D5//iqPYPuH1hdj1Wiv Kv7d07/oP3X/AIHz/wDxVJ/bunf9B+6/8D5//iqPYPuH1hdj1aivKf7e07/oP3X/AIHz/wDxVH9u 6d/0H7r/AMGE/wD8VR7B9w+sLsdJrPw20PV7troedaSucv8AZyArH1wQcH6VgaX4A1fQfG9tc6fM G01Hy0zuNxTHzKy9z24GOh4pses2MhO3XrrCjLH7fNgD/vqh/EGmkBV1652jv9umyf8Ax6tFCaVr mbnBu9jrfEj/AOlxID/Bkj8TXmut+JdUsNUlt4LeNYlxtZ0JLcdetbS65pK5J1NHY8lpJWdj9Sck 07+3tJ/6CEH5mtIR5VYznLmdzkv+Ex1n/nlD/wB+z/jW74a1y/1WaZLuBQiKCHVSOc9Oav8A9v6T /wBBCD8zR/b+k/8AQQg/M1RBS8Q/8fMP+5/WpfCP/IdX/rm39Kpare219NG9tOkqquCV7GrvhH/k Or/1zb+lRU+FlU/jR6Vr3/IKl/3W/wDQTV62/wCPWH/cH8qo69/yCpf91v8A0E1etv8Aj1h/3B/K uE9ElooooAKKKKACiiigAqN+tSVG/WgCSiiigAooooAKKKKACiiigArI8Of8gmH/AK5p/wCgites jw5/yCYf+uaf+gigDgPGv/IbT/riP/QmrM0fw5D4muns5p3hWNDKGQAkkEDHP1rT8a/8htP+uI/9 Cap/AP8AyGrj/r3P/oS12RdqehwSV6tmH/CpbD/oJ3P/AHwtH/CpbD/oJ3P/AHwteiUVl7SXc29n Hsed/wDCpbD/AKCdz/3wtH/CpbD/AKCdz/3wteiUUe0l3D2cex56nwnso3Dpqt0rA5BCLxTpfhTY SvvGozqT1Cxrgn19q9Aope0l3H7OPY87/wCFS2H/AEE7n/vhaP8AhUth/wBBO5/74WvRetJT9pLu L2cex53/AMKlsP8AoJ3P/fC0f8KlsP8AoJ3P/fC16JRR7SXcPZx7Hnf/AAqWw/6Cdz/3wtH/AAqW w/6Cdz/3wteiUUe0l3D2cex53/wqWw/6Cdz/AN8LR/wqWw/6Cdz/AN8LXolFHtJdw9nHsed/8Kls P+gnc/8AfC0f8KlsP+gnc/8AfC16JRR7SXcPZx7Hnf8AwqWw/wCgnc/98LR/wqSx/wCgnc/98LXo mKUmj2ku4ezj2OAf4WWJhWFdSuFQckBF+Y+pqH/hUth/0E7n/vha9Eoo9pLuP2cex53/AMKlsP8A oJ3P/fC0f8KlsP8AoJ3P/fC16JRR7SXcXs49jzv/AIVLYf8AQTuf++Fo/wCFS2H/AEE7n/vha9Eo o9pLuHs49jyPWfDEPhieOCG4knEy7yXUDHOO1WvCP/IdX/rm39K0/iB/yEbP/rkf51meEf8AkOr/ ANc2/pWt26d2YpJVUkela9/yCpf91v8A0E1etv8Aj1h/3B/KqOvf8gqX/db/ANBNXrb/AI9Yf9wf yrjO8looooAKKKKACiiigAqN+tSVG/WgCSiiigAooooAKKKKACiiigArI8Of8gmH/rmn/oIrXrI8 Of8AIJh/65p/6CKAOA8a/wDIbT/riP8A0Jqn8A/8hq4/69z/AOhLUHjX/kNp/wBcR/6E1T+Af+Q1 cf8AXuf/AEJa61/DOF/xj0SiiisDpCiiigAooooAKXrSUUgCilpKYBRRRQAUUUUAFGKKUmkAE0lF FMAooooAKKKKACiiigDgPiB/yEbP/rkf51meEf8AkOr/ANc2/pWn8QP+QjZ/9cj/ADrM8I/8h1f+ ubf0rf8A5dnL/wAvj0rXv+QVL/ut/wCgmr1t/wAesP8AuD+VUde/5BUv+63/AKCavW3/AB6w/wC4 P5VyHeS0UUUAFFFFABRRRQAVG/WpKjfrQBJRRRQAUUUUAFFFFABRRRQAVkeHP+QTD/1zT/0EVr1k eHP+QTD/ANc0/wDQRQBwHjX/AJDaf9cR/wChNU/gH/kNXH/Xuf8A0Jag8a/8htP+uI/9CapfAkix 6zcFjgfZz/6Etda/hnC/4x6NR0qINNJ/q49o/vPxThbbuZXL+w4Fc502GtOgO1cu3oozS7bh+QFQ ehOTVhVVFwoCj2FQvdxIcA7z6LzQOw2OQsSrja46ipKrM8kk8ZaPy/Q9yKe7vvZUKjau4lu9Aiai oBJJIm8FI0/vPQ0zxDMm1gQSpU9aAsT0VGjv5hjkUBsbuD2p+4A4yKAFoozTS4AOCM4ouPlY6iqy F1MLlyfMzkHoKsZHPNK4+UWim5HqKjfLzJGGKg5JI60XHyk1GahgctGNx56fWpaLhyoM0uaQEHoa heVgz7VBVAC2TSuOyJqKTIxntTXkCAEDcScADvRcLIfRUccu8lSpVh2NSUBZHlvxTupoNWsBFJtB gOeB/erJ8AXlxP4nVJJNy+Sxxge1aHxZ/wCQvp//AFwP/oVZHw7/AORqX/ri/wDSutfwjjf8Y9j1 l2bTJQT/AAt/6Ca07b/j1h/3B/KsnWP+QbN/ut/I1rW3/HrD/uD+VcZ2ktFFFABRRRQAUUUUAFRv 1qSo360ASUUUUAFFFFABRRRQAUUUUAFZHhz/AJBMP/XNP/QRWvWR4c/5BMP/AFzT/wBBFAHAeNf+ Q2n/AFxH/oTVa+HaK2sXrkZZYAAfTJ5/lVXxr/yG0/64j/0Jqt/Dr/kK3/8A1xX+ddX/AC6OL/l8 d9M2xsmfYD/DjJ/Ck86V+I48D+8/+FNxm8kz2AxUtYHS2ReSX5ldn9ug/KkIMUyuke4bCuB9ac8m 1gigs56AUjGeNS7qhUdQp5FAK4BZJJFeTaNucKKZIiyTSBxnbHke1Tg5AI6GoT/r5f8ArnSuNR7j EKSQxoXCSJ0DDg1HKnkkGSFcc5weG4qRA5hTdCHQjjaeabODHZeW/wB4kkL/AHRSuVZElzlrlEQY cgfPntzTWEKk/umZFOGctzmnTMEvkc/dCjP6ikaKTa8KpkMSQ+eMGkMNj+d9nLHYBu3d9tNIjZCU t22kfKwbn60/zFN2Vz8hTy8+9A82OLYYido5ORjFAAI2e3gIYKFXJY9qi2xtNGEU+WTgkn79S+aE toVZdyMvzCk2ss0Izuj3fK34dKAEYQKzfuSyKcM248fSlRSl6iZ3KASpPpSvHKPMjVMhySGzwM0o YG+RV5CLtz70AMUQs4HlEKzYD7uc0KCxdJGOyLliOp9KRP8AVxf9df604fPNcxE4L/dPvimAipG7 BRGYmYZRg2aIXUQTb48sv3+fvdaeu8yI8ibFjBJJPtiokJaK6YjGRnH1zSAEVfKDupbcxCJuoZFM bOimN4z8y5yMetOQFoU2DLxMcr6ilbcsUzMu1pQEVT1/zzQA+GMrly24kVLSKMKB6DFLTA8n+LP/ ACF9P/64H/0Ksj4d/wDI1L/1xf8ApWv8Wf8AkL6f/wBcD/6FWR8O/wDkal/64v8A0rrX8I4pfxj2 DWP+QbN/ut/I1rW3/HrD/uD+VZOsf8g2b/db+RrWtv8Aj1h/3B/KuQ7SWiiikAUUUUAFFFFABUb9 akqN+tAElFFFABRRRQAUUUUAFFFFABWR4c/5BMP/AFzT/wBBFa9ZHhz/AJBMP/XNP/QRQBwHjX/k Np/1xH/oTVZ+Hjqus3qE4Z4AVHrg8/zqn45ljh1hGkcKvkjk/wC81R/Dy8S98Q3USKQq2rMH753r XX/y6OL/AJfHp3/L3J9BUmahjRxIzyEEkAZHepa5rnXyjbUZVpT1Yn8AOKW5fbCy/wAT/KBUYSSM nynAUnOCM4pUjIbe7F39T2pFD1G1QPQU14lc5JYHGDg4yKfRQBF5CjO1nT/dbFKIEAIwTnqScmpK KAI1hVc5y2Rg7jnik+zqBgO4X+6GOKlooAZ5SeXs2jb6U3yB0LyFfQscVLRQBGsCK2eTjoCeBUbI sMkbZIXdzk8CrFBAIwRkUAV408wyNvcKXPRsA1IYU2gDK46FTg1IBjp0ooAjMKeWE5AHTBo8hNu0 gnnOT1zUlFAEXkKT8zOwHQM2aVoFZs8jPUA8GpKKAI2hViDyrDupwaFhVW3EszerHNSUUAFFFFAH k/xZ/wCQvp//AFwP/oVZHw7/AORqX/ri/wDStf4s/wDIX0//AK4H/wBCrI+Hf/I1L/1xf+lda/hH FL+Mewax/wAg2b/db+RrWtv+PWH/AHB/KsnWP+QbN/ut/I1rW3/HrD/uD+Vch2ktFFFIAooooAKK KKACo361JUb9aAJKKKKACiiigAooooAKKKKACsTQJNmkwgDkxp/6CK26wdD/AOQXB/1yT/0EUAeV /E0k+JIM/wDPsP8A0N6sfCj/AJGW7/682/8AQ0qv8TP+Rkg/69R/6G9WPhR/yMt3/wBebf8AoaV1 /wDLo4v+Xx69RRRXKdoUUUUAFFFFABRijFKTQAlFFFABRRRQAUUUUAFFFFABRS9aSgAooooAKKKK ACiiigDyf4s/8hfT/wDrgf8A0Ksj4d/8jUv/AFxf+la/xZ/5C+n/APXA/wDoVZHw7/5Gpf8Ari/9 K61/COKX8Y9g1j/kGzf7rfyNa1t/x6w/7g/lWTrH/INm/wB1v5Gta2/49Yf9wfyrkO0looopAFFF FABRRRQAVG/WpKjfrQBJRRRQAUUUUAFFFFABRRRQAVg6H/yC4P8Arkn/AKCK3qwdD/5BcH/XJP8A 0EUwPK/iZ/yMkH/XqP8A0N6sfCj/AJGW7/682/8AQ0qv8TP+Rkg/69R/6G9WPhR/yMt3/wBebf8A oaV1f8uji/5fHr1FFFcp2hRRRQAUYoxSk0ABNJRRQAUUUUAFFFFABRRRQAUUUUAFL1pKKACil60l ABRRRQAUUUUAeT/Fn/kL6f8A9cD/AOhVkfDv/kal/wCuL/0rX+LP/IX0/wD64H/0Ksj4d/8AI1L/ ANcX/pXWv4RxS/jHsGsf8g2b/db+RrWtv+PWH/cH8qydY/5Bs3+638jWtbf8esP+4P5VyHaS0UUU gCiiigAooooAKjfrUlRv1oAkooooAKKKKACiiigAooooAKwdD/5BcH/XJP8A0EVvVg6H/wAguD/r kn/oIpgeV/Ez/kZIP+vUf+hvVj4Uf8jLd/8AXm3/AKGlV/iZ/wAjJB/16j/0N6sfCj/kZbv/AK82 /wDQ0rq/5dHF/wAvj16iiiuU7QoxRilJoACaSiigAooooAKKKKACiiigAooooAKKKKACiiigApet JRQAUUvWkoAKKKKAPJ/iz/yF9P8A+uB/9CrI+Hf/ACNS/wDXF/6Vr/Fn/kL6f/1wP/oVZHw7/wCR qX/ri/8ASutfwjil/GPYNY/5Bs3+638jWtbf8esP+4P5Vk6x/wAg2b/db+RrWtv+PWH/AHB/KuQ7 SWiiikAUUUUAFFFFABUb9akqN+tAElFFFABRRRQAUUUUAFFFFABWDof/ACC4P+uSf+git6sHQ/8A kFwf9ck/9BFMDyv4mf8AIyQf9eo/9DerHwo/5GW7/wCvNv8A0NKrfE3/AJGSD/r1H/ob1heHvEN3 4bvpLuzjhkeSIxETAkYyD2I9K60m6dkcLajVuz6EoxXkkPxQ16Z8C005VAyzGN8KPU/PSS/FfWN5 EVpY7B0LI+T7/frH2Mjo9vA9dJpK8g/4Wtrf/Ppp/wD37f8A+Lo/4Wtrf/Ppp/8A37f/AOLo9jMP rED1+ivIP+Fra3/z6af/AN+3/wDi6P8Aha2t/wDPpp//AH7f/wCLo9jMPrED1+ivIP8Aha2t/wDP pp//AH7f/wCLo/4Wtrf/AD6af/37f/4uj2Mw+sQPX6K8g/4Wtrf/AD6af/37f/4uj/ha2t/8+mn/ APft/wD4uj2Mw+sQPX6K8g/4Wtrf/Ppp/wD37f8A+Lo/4Wtrf/Ppp/8A37f/AOLo9jMPrED1+ivI P+Fra3/z6af/AN+3/wDi6P8Aha2t/wDPpp//AH7f/wCLo9jMPrED1+ivIP8Aha2t/wDPpp//AH7f /wCLo/4Wtrf/AD6af/37f/4uj2Mw+sQPX6K8g/4Wtrf/AD6af/37f/4uj/ha2t/8+mn/APft/wD4 uj2Mw+sQPX6K8g/4Wtrf/Ppp/wD37f8A+Lo/4Wtrf/Ppp/8A37f/AOLo9jMPrED1+l614/8A8LW1 v/n00/8A79v/APF0f8LW1z/n00//AL9v/wDF0exmH1iB6/RXk6/FDWpod0VpYeaoy6FH5HqPn/So P+Fra3/z6af/AN+3/wDi6PYyD28Cf4s/8hfT/wDrgf8A0Ksj4d/8jUv/AFxf+lZviLxLeeJbmGe8 igjaJCiiFSARnPOSa0vh3/yNSf8AXF/6VvZqnZnPzKVW6PYNY/5Bs3+638jWtbf8esP+4P5Vk6x/ yDZv91v5Gta2/wCPWH/cH8q4zuJaKKKQBRRRQAUUUUAFRv1qSo360ASUUUUAFFFFABRRRQAUUUUA FYGk/uFe0fhoSY8fTp+Ywa36zNQ0+SWUXVqQLgDDKTgSD09iOxoA4jx14IutduIr3Tp4kuUTYUmB 2OuSRyMlSCTzg5zXGL8NPFZIz/ZAHc/aZf8A41XsCakY/luoZIWHXzF2gfj0P4Gg63Z9pUx/vitF UklZMzlThJ3aPJp/h34pK+TAulCEHPNzKC59T+6/TtVf/hWviz00j/wKl/8AjVew/wBtWn/PVP8A vsUf21af89U/77FP2s+4vY0+x49/wrXxZ6aR/wCBUv8A8ao/4Vr4s9NI/wDAqX/41XsP9tWn/PVP ++xR/bVp/wA9U/77FHtZ9w9jT7Hj3/CtfFnppH/gVL/8ao/4Vr4s9NI/8Cpf/jVew/21af8APVP+ +xR/bVp/z1T/AL7FHtZ9w9jT7Hj3/CtfFnppH/gVL/8AGqP+Fa+LPTSP/AqX/wCNV7D/AG1af89U /wC+xR/bVp/z1T/vsUe1n3D2NPsePf8ACtfFnppH/gVL/wDGqP8AhWviz00j/wACpf8A41XsP9tW n/PVP++xR/bVp/z1T/vsUe1n3D2NPsePf8K18Wemkf8AgVL/APGqP+Fa+LPTSP8AwKl/+NV7D/bV p/z1T/vsUf21af8APVP++xR7WfcPY0+x49/wrXxZ6aR/4FS//GqP+Fa+LPTSP/AqX/41XsP9tWn/ AD1T/vsUf21af89U/wC+xR7WfcPY0+x49/wrXxZ6aR/4FS//ABqj/hWviz00j/wKl/8AjVew/wBt Wn/PVP8AvsUf21af89U/77FHtZ9w9jT7Hj3/AArXxZ6aR/4FS/8Axqj/AIVr4s9NI/8AAqX/AONV 7D/bVp/z1T/vsUf21af89U/77FHtZ9w9jT7Hj3/CtfFnppH/AIFS/wDxqj/hWviz00j/AMCpf/jV ew/21af89U/77FH9tWn/AD1T/vsUe1n3D2NPsePf8K18Wemkf+BUv/xqj/hWviz00j/wKl/+NV7D /bVp/wA9U/77FH9tWn/PVP8AvsUe1n3D2NPseQJ8OPF0bh0/slWByCLqXj/yFU03w48TzKsirpKy n76i5l2n3H7r9K9Z/tq0/wCeqf8AfYo/tq0/56p/32KPaz7h7Gn2PHv+Fa+LPTSP/AqX/wCNV1/g jwJeaLetqGpzQvPsKJHBkogJGTlgCx4x0GOeua7QaxaMDiRMjtvFMOqiUbbWN5mPQRqW/XoPxNJ1 JNWbGqUIu6Qauxkt/s0f+sm/dr9W4/8Ar/hW4ihEVR0AwKzbDT5Vn+13mPOwQiA5EYPXnuff/J1K zNAooooAKKKKACiiigAqN+tSVG/WgCSiiigAooooAKKKKACiiigApjuFHvT6rudzUAI0rk8HH0pN 7/3jUcsqxAZGSewqH7YP+eZ/OgC1vf8AvGje/wDeNVftg/55n86Ptg/55n86YFre/wDeNG9/7xqr 9sH/ADzP50fbB/zzP50AWt7/AN40b3/vGqv2wf8APM/nR9sH/PM/nQBa3v8A3jRvf+8aq/bB/wA8 z+dH2wf88z+dAFre/wDeNG9/7xqr9sH/ADzP50fbB/zzP50AWt7/AN40b3/vGqv2wf8APM/nR9sH /PM/nQBa3v8A3jRvf+8aq/bB/wA8z+dH2wf88z+dAFre/wDeNG9/7xqr9sH/ADzP50fbB/zzP50A Wt7/AN40b3/vGqv2wf8APM/nR9sH/PM/nQBa3v8A3jRvf+8aq/bB/wA8z+dH2wf88z+dAFre/wDe NG9/7xqr9sH/ADzP50fbB/zzP50AWt7/AN40b3/vGqv2wf8APM/nR9sH/PM/nQBa3v8A3jSiR/X8 6qfbB/zzP51LFMsvAGGHagC4jhx6H0p9V1OCCOtWKQBRRRQAUUUUAFFFFABUb9akqN+tAElFFFAB RRRQAUUUUAFFFFABVfFWKhxQBSulzKP92oRGSeBVu4XMg+lMjX5j9KaEyHyW/u0eS392reBRgVfK ieZlTyW/u0eS392reBRgUcqDmZU8lv7tHkt/dq3gUYFHKg5mVPJb+7R5Lf3at4FGBRyoOZlTyW/u 0eS392reBRgUcqDmZU8lv7tHkt/dq3gUYFHKg5mVPJb+7R5Lf3at4FGBRyoOZlTyW/u0eS392reB RgUcqDmZU8lv7tHkt/dq3gUYFHKg5mVPJb+7R5Lf3at4FGBRyoOZlTyW/u0eS392reBRgUcqDmZU 8lv7tHkt/dq3gUYFHKg5mUjGV6ipbZf3v4VJKvIpYFxJ+FQykT4qwOlQ4qYdKQwooooAKKKKACii igAqN+tSUx+tAD6KKKACiiigAooooAKKKKACmYp9NxQBXmXLj6U1F5qaRcsPpTVXBpoTG4oxUmKM VVybEeKMVJijFFwsR4oxUmKMUXCxHijFSYoxRcLEeKMVJijFFwsR4oxUmKMUXCxHijFSYoxRcLEe KMVJijFFwsR4oxUmKMUXCxHijFSYoxRcLEeKMVJijFFwsR4oxUmKMUXCxC680sS4ensuaWNfmqWU iTFPpuKdSGFFFFABRRRQAUUUUAFRv1qSo360ASUUUUAFFFFABRRRQAUUUUAFGKKKAGMMmgLT6KAG 7aNtOop3FYbto206ii4WG7aNtOoouFhu2jbTqKLhYbto206ii4WG7aNtOoouFhu2jbTqKLhYbto2 06ii4WG7aNtOoouFhu2jbTqKLhYbto206ii4WG7aNtOoouFiMrSqMGn0UhhiiiigAooooAKKKKAC iiigAqN+tSVG/WgD/9k= "
+ id="image3093"
+ x="220.47368"
+ y="872.11261"
+ clip-path="url(#clipPath9740)" />
+ <image
+ width="103.16666"
+ height="70.5"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMACAYAAAC6uhUNAAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzs3XV0VEcbx/HvbtyFBIJLCO7uxd0LRQsUWqAFSou8pUIppYLUBYp7gUKhSHBpcXd3Cxb3ZEl2 3z8CC2mCFpq2+X3OyTnZe+fOPHN3IXufOzPXsNHB0YKIiIiIiIiI/KcZqtSspwSAiIiIiIjIv0xk ZCQeHh4ZHYb8SxiNRowZHYSIiIiIiIiIPH9KAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiI ZAK2GR2AiIiIiIiIZDxbW1vy5MqJm5srV64GERYekdEhyTOmBICIiIiIiEgml9XXh64d2+HjkwUA iwX+2LKNtRt+z9jA5JnSFAAREREREZFMzGAw0LF9G+vFf8o2qF2rOiWKF83AyORZUwLgL7Cz+/cP oDAYDNjY2Dxwv9FoxGg0/KU2HlWH0Wj4y22IiIiIiMjTyerrg1+2rOnuK1Wi2DNpo1GDumTL6vtY ZWvVrMbID99l0Fv9AGjetBGfj/qQV7p1Tlu2RlXatGpOm1bNqVm9Kq4uLg+tO6uvL6M+ev+B+997 ZxD+BfI9Vpz/Rs/kCnb+nKm4uqac6Pi4eI6dOMXefQdYsXINAK4uLsyfOzXNcSdOnmHosOEUyJ+X 774ek3J8fAKhoeGEhISwdv1GNm/d8cB2B7zRm0YN67L4txVMmzEHgDKlS/LJyPeJjIyiS/fe1rLO zs7MnTERO3s7QkPC6fHaG1gsFhbMnYaLi3O69Z87d5GBg4el2mZjY0OfV3tQo3oVPD09SE5O5tcl y5kx62fs7e1Z/MusNPV8OPIz9h84zCcjP6BM6RKYzRbOnbvArj17+WXRbxT0z8+oj96na4/e3L6d ZD3Ozc2N2dMn0KffIF6oWY1uXTumqvfy5au88eYQ8ufLy/ffpJy/2Ng49u0/yMrV6zh67MQDz12+ vHkYOKAvhQsVxGAwcOtWCD/PX8iadRut+we91Y+CBfJjsVg4eeoMX303nqCgawDUrVOLQQPf4McJ U1i1Zj0Avr4+TJ/8Ay3adMJisVAowJ+unTtQtkxJ7O3suBUcysefjeHMmfMA5M2Tm8Fv9aegf0ob p06f5atvx3M1KAhIyTi2atmMt4e8lyb+9N63r78dz4ZNmx/YZxERERERScvJ0fHB+5wevO9J9Hm1 B2O/+o6bt4IfHouTE0PeHsDgd97n5s1gcubMQY9unRnyznBCQkLTlO/c8SViY2M5e+48pUuV4L1h g/jk8y/Ytn1XuvWbbpu4eOnyA9svVrSI9dr2v+iZJAC8vTxxdXVlz94D2NraUr/uCzSsX4cTJ09x 7vxFMBjw9vIC4MjR4yQlJQNw48ZNIOWi2tvLC4vFwuUrxygc4E+pksWoW6cWGzZtZvhHn6bbrqur C95eXqk+lHZ2tnh7eWE0ph7cUOeFGmS7k9Xy9vKiXJlS7DtwiMtXgnB2csLR0YGiRQoBcODgEQCu 37iRps26dWpRvlxZ3nhzCBERkbi7u5E/X577zoUXL7/Sh5u3Qqzb4uPjAXB3d2PWnPksXb6KIoUL 8vGH73HlylW27diNxWymRrUqbPpjq/W4hvVrc+NGMDdv3sLJyYnjx0/x8WfjrPvNZjMAtrY2uLm6 0aBpG1xcnGnbqjkfvDuEjl17pXvecubMweQJ3/LdjxMZ8s5wzGYzBf3z06RRA9as24ivrw+Txn/N DxOm8NbgYYCBdm1bMm3id3To0ouIyEgcHOyxsbHhtZ7dWL/xD27fvo3RaLS+zwAD+vVh1+69jPps LLdvJxFQsABR0TEA+GTxZvKEbxg/cSpvDXkXgLatWzBt0ne81KUnERGRODg44OHunm4fvL28eHf4 SE6eOmvdlpCQkG5ZERERERF5sGs3bpKYmIiDg0OafRcvXXkubbq7u1G5UgWcHB3ZtWcfN2/ewtPD g9atmhEXG0t2Pz+y+/lRrkxpwsIiyJMnF/Hx8cTExqap6/fNW1keuBqATh1e5KUX21gTANWrVebQ oaOUK1sKo9HIrt37OHr0uPVYg8FAtaqV8PLyZOfOPWnqLla0MEUKB3Ds+EkSEhKxsTFy/sIlAOzs 7KhauQK+vj4cOnyUs+cuWI/L6utLpYrlsLW14cjR4ynXxY/BYIAWTRuxa8/+RyZLAPLlyU21qpVY sOg3kpOTH1n+mU4BmDxtBv0GDmH7jt0A1KheNU2Zdz/4mH4Dh9Bv4BC+/OaHVPuSkpJ4Y8BgGrdo z4iPPwegXp1a1K1T6y/H1qRRAwC2btuZ8rpxyuuhw4bTb+AQPv50rLXs3fhGj/smTT0Vy5XlwKFD REREAhAVFc2hw8dSlYmNiycmJsb6c/8bkZycTGJiIocOH2PX7n3UqlkdgMDV62japFGqepo2bkTg qjXW10nJyanqjYuLu6+0BZPJRHh4BAsWLiFP7lzkz5c33XPR59UerNvwO8sDV5OcnIzFYuHM2fN8 9+NEAHq83IkdO/ewbMUqzGYLZrOZXxb9xvETp+nY4UVrPSdPneHSlau0bdU8TRv29vaUKlGczVt3 WEc1nDl7nps3bwHQ/eXO7Nqzn9+WrcRsNmM2m1m0eCmHjx6nS8f26cb9Z3F/Os9JSUmPPkhERERE RFIxmUysWLUOiyX19pu3gtnykBHZTytvntxMnfg9NapVoVBAQSaP/4aiRQrh7OxE7pw5cXB0JMC/ AAH+BciePRuurs4E+BfAzc31ofW6urhQuFAAQdeuW7eNGvE+n386gjq1a+Ht7YWPTxaG/e9t6/7h 7w3lrf59yZMrF+NGj8Ld3c26r3HDenw97lNy5sjBwP59+Wj4MJrduWZzcXFm8oRvad2yOX5+2fhi zCgaNagLQKmSxRj//RdkzeqLl6cnzZs1fuxzU61yJapUqkDvnt3IlTP7Q8sGFCzAK906U6JYEapU LP9Y9T/TSey5c+XC1dUVf/98mM0W9u0/kKbMB+8OwXT7NgBbt+9gReCaNGVu377Nug2/07VTBwIC ClCuTGk2PmRo9ws1q5E7Vy4AvDw90uzPli0rZcuUJDo6mjFffEuVyhWoU7smX379A/FPeNd4zboN jP18JF4enmzfuYst23ZiMplSlWnTshmx92Wmfv1tObGx9y7WDQYDRQoXpGKFssycMx+AVavX8eor L5PF25vQsDAK5M9Lvny5Uw1pz5M7F926dLC+vnTlKn9s3paqbRcXZ9q2aUFMTIx1KP2fFSlckPE/ pZ2ScW9/YZYsW55m+569+6latVKqbT9NmsaXYz9h2Z2M210mk4kNm/7gi9EjWbNuEzt27ub4iVP3 tRHAipWpjwHYu+8AtWpUe2Bs92vSqAEVypWxvl65eh0hoWGPdayIiIiIiNxz/ORpQkJnUrZ0KVxd Xbhy5Sr7Dhzm9nO4yfZ6n16sXLWW6bN+BuDKlat069KRd4d/zKo168iVKwdT70zxbt6sEdWqVLa+ Ts+ggW8woF9vnJ2cOXf+Av3eHJJq/9ZtO1mwcDEAuXPltG4PKFiA+nVfoHGL9sTFxVFw/UZmTfvJ ur/Pqz34dPSX1mnpc2ZMsu7r2L4tQdeu8/6HowDYvmMX7w8bwpp1G6ldqybLA1czc/a8Jz43u/bu x79APooUDqBX967MnDOfi5fTjsIoXrQwHdu3xcbGyO69+9m+a/dj1f9MEwDD3xtq/X3U5+M4euxk mjIlSxTDQkpq6eLFSw+t7+z58wQEFMAznYv6+2XLmhVPT0+AdBe0a9KoPgaDgU1/bCU0LIxde/ZT vWol6tSuycrV6x7Zr/vtO3CIrj360qZVM17t2Y1BA/vx409TUtVjb2+HyWRnfW3g3gJ3/V9/jTf6 9MLJyYkVgWv4bVkgACGhYezes5/GDesxd/5CmjRuwKbft6S6y280GrCzu1ev7X19tbOzY+3KxTg5 OmI2W+j9xsBU6wncz93Nneg7Q/HT4+HhRlRUdJrtUTHReN05z3edOn2W/QcO06nDiwSuWptq36jP xlGlcgWaNm5Ix/ZtOXPuPJ98No5r1288uI2o6Ee+33fZ2dmmOh8Go9a0FBERERF5Etmy+lKrelXy 5snFDxOnsfrOmmAAZUoVp1qVSmzdvot9Bw6SnGx+Jm2WKF4Eby8vSpUsAYCLiwtubk8/7/6rb8ez PHA1Xl6edO7Qjm+/Gk2vPgOs+/fs3ZfucQULFuDEydPWa65z5y8SERl5JyZnsmXLysHDR63lDx0+ cl8fiuHr68PX4z5L2WCA7H7ZsLe3Z826DXwy8gOqVanEuvWbWLlm/Z9Gbz9YUlIScxf8yksvtqJk 8aK80q0zc+Yt5My589YyZUuXpF2bFhgMBrZu38XKO2uyPY5nmgCYMGkaXp6edHypLYPe7Mf+/YfS zFvo0KUnYeHhj6zLzs6OmtWrACkZoYf55dff+HHCFAAqVijLt1+OTrW/ccN6QMrc9/8NfhNPj5R5 5U0aN3jiBACkrA0wfuJUxk+cSsvmTej/xmus27DJun/Bot+s6xv82Q8TJvPrkuUUL1aEr7/4jKUr VnLseEqiZMWqNbzWszvzFy6mccN6fDjys1THXrx05YGZr9u3b9OwaVucHB3p27snA/v3pd/AoVj+ PI4HuHHzFtmz+8GBQw/o301y5ciRZnsOPz+uXk07qmDSlJlM/ukbtm3fmWq7xWJhx8497Ni5B2dn Z0Z/MoIX27Tk+/GTuH79FjlzptNGjuxcSaeN9CxbsSrVqAIREREREXl8Hh7u9Hm1O4535v5/+O7g dMu1btGErL5ZWLHqya+d0nPbdJtflyxl776D1m2PM3/9UcLDI/h5wSK6dGqPj08W66KBUQ+4+ZkQ n4DHfUP+bW1tcXZyStmXkEhycjI+WbytNy59fLJw9WrKouiJJhNbt+9k4aLfUvft9m1OnT5Lhy49 KVemFE0aNaBli6Z069n3sfuRnJzM/IVLMJlMlC9bmm5dOjBv4WKOnzhFlUrladG0EQaDgY2/b2H9 Ey6C/kxvmR44eIjvfpzI+o1/4OLiTL/XX0tTJls2H+uCDll9//wYCAM5c2SnSaP6jP1sJK6uriQk JBL4Fz5oxYsVIU/uXFgsFkqVKEazJg0pFOCPxWKhXJlS+Plle+q6AXbv3Y+nh0c6fXm4Y8dPMmnK TIa8fS8ztW37Lry9PHmlW2cSEhI5eOjoQ2pIX3xCAt+Pn4S3l6d1Dsqf/bF5Ky+2aZlmtMTdBfx+ /2MLzZs1wtb2Xn7IwcGBpk0asOmPLWnquxoUxMZNm3m5S8c0++6Ki4vj6LHjBAT4p7SxeSvNmzRM 9ShFe3t7mjVuwO+btz6oGhEREREReUaqVCxvvfh/ZNlKFbC/b/TtX7F1+y7q16tDfEICoWFhhIWH 4+D4eHE8jJOjI21aNuPM2fPpPjHgz44cPY6fXzbrYvD16tTC3t4eSLkI37l7H+1fbIOzszOlSxWn bOmS1mO379hF7Vop67mFhoURGhaGvb09FouFbFl9MZvN7N1/kB9+mky2rD6pRi4/DovFwuKlgezc vRcbGyOdX2pLh3atadmsMQaDgVVrNzzxxT884xEAd02dPpu6tWtRt3Ytps+cS/B9J3/qxHsL/4WG hdGiTSfrazs7WxbOm2F9ff3GjVSPhXsadxf/W/jrUr75foJ1+2ejPqR2reo0aVjPOvfkcbz3v0HE xMZy9PgJXJydadakEYePHOfa9RvWN/XPawBs3b7TulLk/ZYuD6RLx3bUqlmNzVu2k5SUxKo16+nR rQvTZsxOc/f+z2sAJCaaWLBoSZp6k5KSmDpjDq/16sba9ZusTwu4a868XyhbtjQTvv+KlavXEBsb R7FiRShcKIA3BgxmydJAqlapxPjvvuDXJcsw2hhp37Y1x46fYu36TWnaA5g+cy4Lfp5ufe3t5cW4 0R+zbsMmrgZdo1BAQdq1bcm3P0y09r1alUr8+O2X/LpkKUajkXZtW3Py1BlWr9lgrSeLt1eqPsfF xbNoyTIg7RoA+w4cso6mEBERERGRh/Pzy/rYZY1GI76+PqkW2HsSDg4O1mHwP02exv8GvcnyJfM4 c+Y8vr7erFqzgSnT0j5S/XHcXQMgISGRffsPMuaLtIu5pyckNIyvvh3PhO+/5PqNmwRdu55qNPK4 L7/jg/eGsPDn6Zw+c45tO3aTkJgIwPLA1eTMkZ2F82Zw/sJFXF1duHDhEu8O/5ge3bpQvlwpQkLC cHF2YeKUGdy+sw7ek7BYLCwLXEOi6TYv1KhK6ZLF72xbza49+5+4PgBDlZr10o4Rf0Ivd+mAvZ0d ywPXcCs4Zch/i2aNyerrw+Gjxzh0+Bgvd34pzXHx8Qn8vGAR3l5etGnVDICExERCQ8MICQnl0JFj Dz1RNatXpVCAP0eOHmf33pQTkDNHdho3rEdCYiJz5y3kpXatcXN1Ze36TanezKJFClGtSiVu3gpm xco1eHp48GKbFmAwMHX67Ae2mTdPbmrWqEqunDkID49g3/6D1jhtbGzo8XKnNMds3rqdM2fP06Rx fS5evMyJk6et+yqUL4OXpyfrNvwOQI7sfjRpVJ+lK1alylqVKVWC8vdd7ELKsJM5P/+Ct5cXrVo0 SZXIMBoNdO3cgXXrf0/3cYYGg4H6dV+gSOFCODs7c/HiJQJXrbU+VsNgMNCwfh2KFS2C2ZzMkWMn Ui3EGFCwAP4F8rN67b2L9bq1a5I/X16mzpiD0WikauUKlCldCg8Pd06fPsvufQe4fN8CFgaDgQb1 alO8WFHMFjNHjx5Pteihf4F81K5VI1XccfHxzFvwK926dEiTRduzbz+HjxxHRERERCQziIyMxMPj 8dbPSs8rL3cioGCBxy7/05QZXL7y5DdnfXyy8Mvc6TRq/mKq6zt7e3u8vb0IDQ17qgvkZ8Xe3h5n Jyfr/P8HWTB3GlOnz051U9TGxgZfXx8iI6Osj38HcHZ2xs3V5bEe5fc46rxQg/p1arHot+XWx9Y/ KaPR+GwSACIiIiIiIvL3+jckAFq3bErPHi+zcdPmVCOy/w0qVypPvTovEBYeQY1qVTCZTPTp99YD F1t/3nyyeP+lp54ZjcbnMwVARERERERE5OixE3Tv9Trh4REZHcoTO3DwCGazmay+vnzz3XgOHDry TBYrfFrP4pHnSgCIiIiIiIjIc3H23IWMDuGpmUwm9uw9kNFhPFNKAIiIiIiIiGRC8xf9hu2fngz2 MHH3zXGXfyclAERERERERDKheF3QZzrGjA5ARERERERERJ4/JQBEREREREREMgElAEREREREREQy ASUARERERERERDIBJQBEREREREREMgHbFs1qM+Tt/2V0HCIiIiIiIiLynHz59VhsbYxG7O3tMzoW EREREREREXlOjEajpgCIiIiIiIiIZAZKAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiIZAJK AIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiIZAJKAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCI iIiIiIiIZAJKAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiIZAJKAIiIiIiIiIhkAkoAiIiI iIiIiGQCSgCIiIiIiIiIZAJKAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiIZAJKAIiIiIiI iIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiIZAJKAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiI ZAJKAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiIZAJKAIiIiIiIiIhkAkoAiIiIiIiIiGQC SgCIiIiIiIiIZAJKAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiIZAJKAIiIiIiIiIhkAkoA iIiIiIiIiGQCSgCIiIiIiIiIZAJKAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiIZAJKAIiI iIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiIZAJKAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiI iIiIZAJKAIiIiIiIiIhkAkoAiIiIiIiIiGQCSgCIiIiIiIiIZAJKAIiIiIiIiIhkAkoAiIiIiIiI iGQCthkdgIiIiIjIv01sbCwrVwU+dvkihYtQsmSp5xiR/FNdunyBpOSkxyrr5OhMjuw5n3NEkpkp ASAi8oRi4+IwJSbi5eWV0aE8tuDgENw93HGwt0+zLykpiZDQUPyyZXvm7UZFRQPg7u6WZt/zOo9m s5kbN2+SI3v2Z1rvP0liYiLR0dH4+PhkdCgimVZcXCwLF/3y2OVbtWytBEAmdfHyBUymxMcq6+Xp rQSAPFeaAiAi/wjLA1fSq8/rHDx82LotOTmZXn1eJyQk5JHHh4eH89vyFc8zRKvAlav4+LPRf0tb z0rvfv05dOhwuvvOXbhA5249nku7U6ZPZ/L06enuWx64kk9Gj3nmbUZGRtKkZWvMZvMzr/uvOHTk CO+8/wGt2r3EG2++xaIlv3H79u005cxmM8M+GM67wz98YF07d+/mzcFDn2e4IiIi8h+kBICI/CNc v36dQ0eO8M33P1i3WSwW9u7fT0Lio7PmN2/dYtKUqc8zxP+sPLly8dW4Z38hLqmdOHmKiuXL8/W4 sXTu+BJzfp5H4KrVacrNmvszp06f4cjRYxkQpYhkhOjo6L/8b95sNpOcnJzuPovFwsHDh1mydBnn L1xMt0xwcAi79uxJd9/RY8dYv3HTX4pPno1tO3awbccODh05QnDwo2+QAFy/foMFixYBsGfvPrbv 2PnY7a3bsJFjJ048Vax/dv93vLssFgvf/vDjU9VnMpkeuj8mJoYNmzaxPHAlt4KDn6qNp2Eymfhx 4qR09yUlJWGxWP62WNKjKQAi8o9Ru1ZNDh0+wpat26hZo3q6ZQ4eOsSKVauJjo6metWqtGzeDICZ c+YSERnJ2C+/AqDva68xccoU3uzfDwd7ew4dOcK6DRsZ9OYAjEYju/fsJSQ0lKaNGwGwbEUg23fu xMHBgRbNmlKhXDkAjp84yZFjR8mdMxer1q6lVo0aaWLau28/e/bto2eP7ukOsX+U02fOsvi336zD 1nt274aPjw9JSUn8PH8BR44dw8vTi04d2pM/Xz4ANm/ZSlJyEjExsWzfuZNiRYrQtXMnDh0+wrLA QPLnzcuLbVrj4uJibSc2Lo7vx0/galAQL9Ssae17TEws23bspEyplKGpP02eQoN6dQlctZqw8HBa NW9O2TKlrfWEh4czd/4CLl66RP58+ejetQuurq4AJJpMzJw1m7PnL1CpYoUnOg9hYeHMnDOHq0FB 5M+Xjx4vd7XWu+/AAdauW8+t4GD8/Px4uXOnVEP8d+/Zy9IVK/Bwd6dZk8YPbScuLp6Zc+Zw9tw5 svv50b1rV3x9fTh/4QIbNv3Oaz1fsZa9fOUKK1au4o0+vQE4duIEy5avIDwiksqVKtC2VSsMBgMh oaHMW/ALDRvUZ9HiJeTInp1Xur2cqt2O7dtZfy+QPx9B166xaMkSWrdskaq95YErefWV7kyYNOWR 52z/gYP8tmw5+fLmoVXLFmTx9rbuWx64ku07d2Jvb0+Lpk2pUD7lM33w0CGuBl2jedMm1rJjv/yK gQP642Bvzy+LfqVI4cLs3bePYydO8OH77xEaGsq6DRs5eeoU3t7evNTuRQoHBDwyPhF5PGfPnefb H39kxuT0Lxoex+q16zh+8iRD3hqYZt+ywEC2bttO8WLFGD3uC/Lkyc0Hw96x7jebzXw4ahRHjh5j 68b1aY6/dOUKJ0+dpn7dOk8dnzwbb7z5Fj27dyM+IYEzZ89iNBr5ePhwsmf3e+AxRhsjTo5OAJw5 e5b4hASqVa3yWO0dOnyYpKQkihct+pdjX7YikLcG9E+7PXAlA/v3e+L6WrRtR+Bvi7G1TXtJG3Tt Gn37D6BE8eIE+Puze8/eVH/3nqekpGRWr1lDvzvfHe437IPhdOnYMdX3qr+bRgCIyD+Gg4MDfXu/ ynfjx6ebHd2waRPDhn9IQX9/6tWpw5yf5zF5Wsrw8tKlSuHk6Ej1alWpXq0qTk6O7Ni1m4OHDgGw dNkKfln0KydPnQZg4eLFREZGAjB1xkzGT5xEpQoVyJE9O28OGmK9C3Lh0kV+mjyF2fPmUbN6dQrk z5cqptVr1/HRJ5/SoF7dp7r4P3j4MD1e643FYqFr504UCggg4k5cw0d+zIqVq2hQry4AXXr05Nr1 6wAcOHyYz8d+wdFjx6nzQi2WLFvG+yM+YuacOVSrUoU9+/fz+bgvUrX11bff4eTkRIVy5fjy2++s dwPCIsL5dfESa7nFvy3lg49G4uHhQYnixRk4eAjnzp8HIDIqinadu2IymWjTqiXXrl+nV983rMPt hwx7l207dtKoQT0OHT7MipWrHus8JJpMdH2lJ5evXKVZkyYcPX6cnn1et9Z74cJFqlSuTNfOnbC3 s+PlV3oRGxcHpCRgBr0zjLy5c+PvX+CR0wpef3Mge/ftp1mTxoSEhtKxW3diY2Pxy+bH1BkzuXT5 srXsosVLCAkJBWDHrt28NXgouXPnpnHD+iz+bZn1rkVkZCSz5/7M2C++onjRopQvW/aRfb5w8RJZ fbNaX1ssFkaM+oT/DX4bBwfHRx4fFBTE9NmzqVG9GhcuXWLosPes+6bNnMUPE36iQvly5MyenYFD hrJj124gJeG0dfv2VO3Onb+ApDvTETZs+p33R4wgNCyM1i1aYGtry8DBQ3F1daX7y12pWL480dHR j4xPRP66EydPMX7iJCZNmcr5Cxes20NCQliwaBFjvvyKhb8uxmKx8PvmLRw9doxpM2Zy6syZVPW0 bNaMcZ9/Ro+Xu/L9N1+xcdPv1v9DAeb8PI96dR5+cW+xWAhctYpJU6dx4+ZN6/akpCTWbdjInHnz rX+/AOb9stD6+7Xr19m8ZSuQcgf6xMlTzFvwC0tXrCA2Lo5lKwIZ++XIedt8AAAgAElEQVRXTJs5 i8RH3NUVGNi/H8OGDGbqTxOoWa0aI0Z9AqT8jZ47fwFffvsdW7dvJykpZeFBGxsbnJ2dU9URFhbO qjVrra/v3nR4lL379jN52nSCrl2zbjt15gwTJk3mp8lTOHvuXKryx0+c5PvxEx5rFEFCQgILf13M F998y4ZNm1J9FrZu3863P/zI9+MncPnKFfbs3UdMbCwzZs9h0X3fYe76ccJEOrRvz+ejPqZnj+7p Xvxfv3GDP7ZsYe36DXz343hOnjpFSEgIU6bPYNWatalG1FwNCmLytOnsO3AgVb/3HTjA0hUrmDJ9 RpoRGUeOHuOHCT9Zp7devHSJCxcvEbh6NdNmzCQxMZGLly4xfdZsxn31NZv+2PzIc/QsKAEgIv8o rVu0wGS6zco1a9LsGz9xMn1ffZWO7dvRsH49Br31Jj8vSPljVaZUSRwcHKhetSrVq1bFzs6OypUq smt3yoX87r176dC+Hbv27MFisbB7z14qV6oIwMzZcxjQ73Xatm5Fn1d70axJY6bPmm1tNzo6hi9H j6Zh/Xr4Fyhg3T577s9MnzWb6ZMmUtDf/6n6O2XadBrUq8u7/xtKhXLlaN2yBQX9/bkVHMyqNWsZ NXIEDevX5713hlK4UAA/L7i34JSXlxfvvTOURg0a0KF9e1avXcfID4fToF5d3urfz9r3u2pUr8ar r/Sg/Ytt6d3rFWbPnffAuBrUq0f3rl1o16Y1tWvVYsu2lAvGBb8spFBAQQYNfJPqVavy8YfDuXnr FvsPHuTylSts3rKVcaM/o16dOowa8WGqEQgPs37DBmLj4vhq7Gjq1n6Bb8aN5fyFC2zbvgOAdm3b UOeFWvjnz0/rli1wcnKyrmkwd/58WjRrSu9Xe/Fi69a0b9v2ge0cPHyYw0eO8PW4sdStXZvRn4zC ztaWZSsCcXZ2okG9uiy9s5aE2WwmcPUaWrVsDsBPkybT/eWudO3Ukbq1a/Pu0MHM+2WhNVmVaDIx aOAAWrdsQamSJR7a37379rM8cCVvDbh3x2P+wkXkzpmLyhUrPtY5i4mN5ZOPRtCwfj2GDR3C0WPH iImJAVJGxAx443VebN2a3q/2onmTxsy47zP9KCWKF2fooLepWaM6t00mQkJDqFGtKmVKlaJxwwbW ETIi8nxdv3GDShUrULJkSd77cASXr1wB4NMx43BxdqFV82a4ublhMBjwL5Afv2zZqFC+PL5ZUi8Q ajAYrL8HBwdja2uLk2NKovHylavsP3iQFs2aPjSWNWvXEREZRa6cOen9xr27uK++/gZXrlzB1ycL XXv0tF4ETZ46zVrm0uUrrFmfMrJg87ZtfPDRSFxcXChWpCjTZ87ixo0btGzejFw5c1ovWuXxdOrw Env37yfRZCIsLAxfHx8aN6jP1u07mHTnPbh56xZLV6ReJ8nT04MJkyYTGRUFwB9btnD0+MMv0j8b M45Va9ZQpFAhhg57z5pYvn79BhXLl6dsmdKMGPWpNQmwcvUavvvxR8qVLcOsOXMfOaUzJiYGJycn mjRqyJmz5/h87DgADh85yvyFi6hXpzaVK1YkPiGBXLlyYm9vR4VyZSlZoniaunbt2UNYWBivvdGP SVOmEhoWlqbM1atBfDhyFFevXqWgvz9vDf0f3/zwI/4FCrDx99/5dclvQEry4b3hIyhapDBLl61g zJ3RpkePHeP9ER8RH59AtqxZ6TvgTWvdwSGhLAsMpHTJknw2ZhynTp/Gy8sLLy9PihQuTIXy5TEa jbw3fARFChWiWZMmGI2GNDE+D0oAiMg/io2NDf1f78v4nyal+hJgNpu5cPEik6dNp1W7l2jV7iU+ HT2W2JjYB94tqFyxIjt37yHo2jVcXFxo3KA+u3bv5tTp09ja2VEgf37Cw8OJjIqiYvny1uMqli/H 1atB1tfFihTB2dkpVd179+/jx4mT+PHbr/H1TX8l9uDgELq+0tP6k96XmgsXL1G9atU02y9duoyb myuFCha0bqtQvjxXr161vi533/CxPLlyUSB/fjzc3QHInSsXt4KDU52bsqXvla9YvjxB1649cKG8 +89H7ty5uHjpEgBnz1/gxMlT1vegbYdOJCUlcfPWLS5fuUKO7NnJ6uubbowPc/HSZcqXLWv9kuro 6EjxYsW4cqe/q9euo1O37vTuN4Bvvv+B+Ph4wsLDgZQvr/f37WHD6i5eukxAwYKpnkpQvlxZazut mjdnxcpVmM1mtu/ciYuzM6VLlrzT9/PMnTff2vf3R4zEYjYTHh4BgJOTE0WLFHlkXw8fOcrgd4Yx 7rNPyZM7N5Dy5WzGrNn0frUn0dHRxMfHYzabiY6OfuBcwYCC/tb328XZGXcPd65cDSIiMpKIiAjr kH9I+excue+z8yjl77vA9/T05I3evenW6zU6d3+FRYuXkJCQ8Nh1icjTq12rJs5Ozly+cpncuXKx 8fc/gJRHEIaHh+OTJQuNGzYAUv7f9/HxoVTJEnh7p/90lZiYGIa++x4fDHsHo9GIxWJhzBdfMnTQ 24+MpUTx4nTp2IGmjRuRLVtWzl+4wI2bN0lKSqJnj+40atCAJo0b8ceWLY+sq1aN6rRs3oyAgv7E xsURHhmJs7MzDevXw+VPd6rl4ezs7HB0dCQiIoL8+fJRulRJLl6+TFZfX35/yB1lo9FIi2ZNWbYi 5VGWvy5ZyksvtnlgebPZzLYdOxg2dAg1a1Tn1Z6vsH7DBiDl/XR1deHSpcvkzpWTjZt+B1LWDxjY vz/Vq1alX98+j0zu+Pj4UK1KFYKCgnB1dWXnrt2YzWZi4+KIjY3FZLpNhfLlKBwQQHY/P+zt7ClR vDiFCxVKVY/FYiEsPJzgkBDGffYpGAz8OGFium3mzp2Lnj2607RxI7y9vKlbuzZ1XqhFl04d2Xnn Rsq6DRvp/WpPalSrxnvvDOWPzVusf5vLlS5Dx/btaNGsKc7OTtaRmgBD3n6LmjWq07pFc7Zu34GH uzueHh74589PqZIlsLGxITomhvCICPLkzsULNWs+9Pw8K1oDQET+cRrUq8v0WbP59bel1m1GoxEX F+eUPzzVq6U5xmAwpLlQqli+HEOGvcv6jZuoXLECRYsU4ez582zZtp3KFVLmp7u6umI0Grl27Zr1 wvVq0DXc3Fyt9Tg6ph2OXaFcObJlzcrQd9/jh2++TvcLi6enB8PfHWZ9bWNjk6aMh7u79Y7O/dw9 3ImJiSUqOtp6kRcUdA03t3sXrkZj6hyujfHhOd3rN25Yfw+6dh0XF5c0dVjrtkl/u5ubKzWrV2PU iLQr1B8+cpTQ0FBMJhP2d6ZDXLtxA3cPj4fGBeDu7s6169dSbQu6FoSbmxvJycl88/0PfPPFWIoU LgxA45atrO+3u7tbqr5dv36DB/Fwd+fGjRuYzWZr34OuXSNXzpRHLpUvVxZbW1t27d7D8sCVtGrR PFXfB/bvZ/2yfb/wiHAc7O0feD7vOnb8OAMGDWbEB++nmn955epVYmJj6dC1G5AyFNNkMtG4ZWt+ +2VBukkmozHt5wnA1cUFGxsbrl27bn20Y9C1e58dZ2fnVEN/b9y4maYOJ0eHVK9f7tKZl9q3Y9eu 3fw0ZSo3bt6k/+t9H9pXEfnrPvx4FL6+vpQpVZIc2bNz/c7FxdjPP2XR4iUMGDSYvHnyMPqTUY+s Ky4ungGDBtO1U0dq1UxZz2bnrt3cvHWTRYuXkJycTGJiIt/+OJ6B/d5Ic3yuXPceTefh7k5kZBQh IaFk97u3HkuunDk4c/ZcmmOT/vTEk4L+90bTvdW/H8tWBDLyk88A+P7rr9Ik3eXBwsPDSbp9G18f H3bs2s3UGTNo06olhQsFMH3WrIce26ZVS/r2H0D9OnUIDgmmTOkHJ9CjY2KwsbHBzs4OgFw5c3Lx zpS5Tz4fg4urC+XLliVnjhxcu/M3+crVK+TIkfL5yJY1KzYP+G5x17nz5xn+0cd0eKkdhQMCiI6J ISEhkaqVK2Exm5n3y0I+GT2GD99/17puUXoMBgNZsnjTumULPD096dKxAy1fbA+8m6bs3b//kPJ9 4u5rdzd3oqJTRkdcunyZHNlzAPe+E8bGxgKQM51/F54envj6ZLFODfXwcE81beYuo9HIxB+/Z8HC RUyZPoOG9evR97VXH3qOngWNABCRf6S3+vezzu+/q27t2sye+zMRERHWbXfvTOfNk4fIyEjrEGgA FxcXihQuxPRZs6lcqSJGo5GSxYszd/4CKt0Z/m9nZ0eVypVYsnQZiSYTIaGhrF2/nhrV01+E8B4D w4YOoUTx4vTp1z/dOdF2dnYULlTI+nP/EMy76tR+gVVr1nI1KGXEQVJSErFxcfjnTxnK+eviJdbR D9t37kw3+fG4NmzcRFRUNImJiSxbEUi1Ko+3AND96tWuzZp16zl1+rR1281bt0hISKBwoQDc3Nys K9ufv3CBQ4ePPFa91atW5czZc9Z5cus2bCQqMoqKFcpjMpmIio62TidYs25dqov8alWqsGHjJiKj ojCZTKxIZ2X9u8qWKU1ScjKr164DUubnHTl6jBrVUs6rwWCgZfNmzJ0/nz+2bE01JLZe7drMnT8/ 1TDCu5+/x3Hi5Cn6vTWI4e8Oo27tF1Ltq1CuHNs2bbD+fDryI3LmyMG2TRseOMLkQWxtbalauTK/ LVtOYmIioWFhrFm3zvrZqVC+HAcOHiIqKprk5GTrWhAPEhUVTUREBA729tSqWYM6L9QiLCz8iWIS kSd3d7pavz69eaFmzVR3Fl1dXHj1lR7MmzWT8+cvEBwcgre3V6q/gfeLi4tnwNuDaN2yBc2a3JsL XbhQId5+cwAVypejXNmy2NjYUPEJpviULVOaU6dPW0ebbd223fp/Te7cua1/2+5fdwSA+/4e2tjY 0P7FtkydOIFsWbNy6HD6j6yVtGJiYvhk9BheavciRqORPzZv5qUX29KscWOSkpJITn7443B9smQh X968jBj1CW1btXpo2bt3r+++p1u2baPmne9K23fupP/rfaldq2aqC91qVaqwY+cuAHbt3vPIeP7Y vIUmjRvRqnlzXF1drZ/npKQkqlWtwrjPP+W1nq+wZl3KdJKHfearVKrE6TNnAThx6hTFixV7aNsP U7N6dXbsSunHufPnyZrV17pI8ZPy9vK2xmw2m8nu58fbbw5g/uyZLL7vxtfzpBEAIvKPVKliBYoU LmT9wwEw9O23+GT0GOo3bU5B/wKEhYVTpVIlPh4xHEdHR7q/3JUuPVKGUC9eMA9PT08qV6zIiZNz KFemzJ16K7Lx9z+ofN8K9e+/8z8G/W8YjVu0JC4unhrVqtKre7fHinPwwDf5fvwEXn29HxN//B7P x7jbfb/uXbtw7fp1Wr7YnoL+/kRERPDF6M8pVbIEn3/yMf97930WLPqV0LAwOr7Unob16z9R/fcr U7oU7Tp1Jjk5mSze3nz/zVdPXEe1qlV4+80BvNK7L1m8va13vKdN+oks3t588tEIhr73PtNmzsLO zi7Nhe6DFMifj2FDBtP7jf5k8fYmMiqKUR99aL2D/VrPV3ildx/cXN3ImTNHqnnyL3fuxKEjR2jS ohWenp7Wpxukx9PDg09HfsSHH3/Md+PHExoaxlsD+qeaP9iiWVN+mjyFqlUqp5rO0P/1voz+4ksa NW+Jf4ECREREUKJ4Mb4cM/qx+jhvwS+Eh4cz+J17o0JcXV3ZtmnDYx3/JN57Z2jKZ7plK+LjE6ha uTK9enQHwC9bNpo3bULLdu2xs7Wlw31PJ0jPreBb9Ok3gKxZfbFYLDg6OvLJRyOeecwimd3xEydp 1e4l6+thQwbTsnkzevV5ndtJSanuVHbo2o08uXNhNpspWbIEvr4+uLu7MXPOXHr1fZ3uXbpY7/ID LFq8mMNHjxISGsq0GSl3hb8eN5YC+fNZp6ElmkzY2to+9urwkJLo7tm9G6/1fQMbWxsC/Ata1wjp 9FJ7Bg4eioe7O1mz+qY7Cg5SFo+9fTsJe3t7kpOTn/gJMpnRS527pqx8bzDQsH49OndI+dw0adyI Tz4fzeKly/D29MLB4dGLE7dr24Y3Bw3hyzGfP7Jsv9f78OHIURhtjHh5evHO4JSpI21ateTVvq9j NlvIlTOHdZRA+xfb8snnY1i4eDHubu44OT18ZEf9enUZ8s677Ny1G4PRYP0bPH/hItat30DWrL6E R0TwzuDBALzYujWD33mXrFl9+XzUx6lj7dOHAYMGsfH330lKSnqqpwzc1bJZM8Z+9RW/b95MclIy /fr0eeq6Gjaox4RJk5kxZw7Dhgzhf++9j3+B/MTExtK1U8enrvdJGEaP/dTyztD3Hl1SROQfItFk IjQ0FG8vr3SH5z+tsLBwHBzsH3vhumfpbp+yZMmS5mkCwcEhuHu4P9VTBv7MbDYTFhaGj8+T3VX+ M4vFws1bt3B0dEyT9EhOTiY8IgKfLFmeuN7k5GRCQkPx9fFJM5zeZDJhMpkemHUPDw/HxcXFOv3g YcxmM8EhIWTx9k738UEPc/v2bYJDQvDy9Hzkl5mMFh4ejr19+p/puLh4bGxtHutzZTabCQkNxcXF RfNzRe4IDr5FvwFph8o/SKuWrenSuesTtxMTE4O9vX2q/9ssFgu3goPx9PDAwcHhIUf/PcxmMwkJ iWmG7ieaTJiTkx/5f2VYWDh2drapprn9l/yxdSMm08MXwLvLy9ObCuUqP3VbZrOZqOjox74hEbh6 Nfv3H2D4e2mHxz9IbGxsmr8rsbGx2NrZpfs3JSIy8rHjsVgsRERE4OWVeh2L2Lg44uPinvj7S3h4 eJq6nlZ6/f6rkpOTuRUcTFbfByfJnqUx4z5TAkBERERE5EklJSWlWpj1UTw8PfDyfDYXIvLvEhMb g8Xy8OHvd9nY2OLs9PckWr/89jt27drNhB++I4u399/SpmSsMeM+0xQAEREREZEnZWtrS758+TI6 DPkXcHV5uvniz1uvHt0Z9OaAdNcokv8uJQBEREREREQymSddt0j+G/QUABEREREREZFMQAkAERER ERERkUzguU0BiIuLZdvOLYDleTUhIk/AaDBSr86DH4+W0dZtXJXRIfxtsufIzvVr1x9d8D9Aff1v Ul/l3y4zva/q63+T+vrf1aBuk+da/3NeA8BChbKVnm8TIvJIiaZEjp04ktFhPFJm+f8i6OYV9fU/ SH39b8pMfd17YHem6Wtmel/V1/8m9fW/ae+B3c+9DU0BEBEREREREckElAAQERERERERyQSUABAR ERERERHJBJQAEBEREREREckElAAQERERERERyQSUABARERERERHJBJQAEBEREREREckEbDOq4YjI cK5eu0pCQnxGhfBU3N3cyZ0rL06OThkdioiIiIiIiMhjy5ARABaLhUtXLv3rLv4BoqKjuH4jKKPD EBEREREREXkiGZIAMN02cfu2KSOafiZiYmMfWSY4OIQWLVoSExOTavsXX3zJjBkzn1doIiIiIiIi IunKsCkA/3UmUyLbtm3n9u3bqbafPn2a6OjoDIpKREREREREMislADKQyWRixoyZ7Nu3D5PpNlWq VKZXr57Y2qa8Ld999z116tQhMDCQEydO8P333zF69Bh69erJ5MlTcHNzw9vbmypVKlO2bFlrvZMm TaZs2TJUrFgxo7omIiIiIiIi/zB6CsBzdv78ec6cOWP9uf/uf3x8PFFRUXTr1o22bVuzePESRowY ad2/YMEv9O7dh6SkJDp16kRCQgI//TSRt98eRKFChahbty5JSUl89dXX1mOCg0P46KOR5MmT52/t p4iIiIiIiPyzaQTAc9a37+sYDPfyLDdv3qRQoUIAeHh4MGTI4JRFES9d5rXXXuXjj0fx6aejrOXr 1avHBx+8D0BwcDAAb745gPr16wNQqFAAY8aM5caNG/j5+TFv3jzq1atLtmzZ/q4uioiIiIiIyL+A EgDP2dq1a/Dy8rK+7t27j/V3i8XCG2/0Y//+A2TLlhUHB0frRf5d1atXS1Nn9erVrb9nyZKF5s2b MWvWbIYOHcKsWbMYM2b0c+iJiIiIiIiI/JspAZCB1q1bz+nTZ9i1awcAGzduZOvWLanKODs7pTnO ySn1tp49X6FXr9eoXLkSt28nUadOnecXtIiIiIiIiPwraQ2ADHTjxg3c3d0BiI2NZdq06U9VT6VK lfD09GTgwLd5+eWuGI16W0VERERERCQ1XSlmoLZt23D7tokKFSpSuXJVypUr99R1vfJKD65du8bL L3d9hhGKiIiIiIjIf4WmADwnOXPmJCwsJM32SZMmWn93dXVlxYrlhIaG4unpiY2NDYMGvW3dv21b 6ukAvr6+6dYJKQsENm3aVIv/iYiIiIiISLqUAPgHyJIly1Mfe+XKFRYt+pWpU6eyZs2aZxiVPA/J oaEk7tuPY9UqGN3cMjocERERERHJRJQA+JczmUwYDAaWLFlM/vz5MjgaeZjkGzcIatCIpKAgbPz8 yLkqENu8eTM6LBERERERySS0BsC/nL+/P2+9NZDixYtndCjyCCHvfUBSUBCQkgy49Ua/DI5IRERE REQyEyUARP4GiQcOErt0aaptCTt2ErdqdQZFJCIiIiIimY0SAJlQfHw8SUlJGR1GphI2ciRYLGm2 h478GJKTMyAiERERERHJbLQGwHMSHx/P+o3r0mwvWaIU+fLm+/sDus+GTRsoUrgIBf0LZmgcmUX8 xo3Eb773RAfbPHlIunwZgNunTxM1Zy7u3btlVHgiIiIiIpJJaATAc5JsTub69euUKlma8uUqWH+y +vpmdGjyd7JYUu7y3+H1zv/Ic3A/3iM/sm4LHz0GS1xcBgQnIiIiIiKZiUYAPGd+2fxwcHBItS0y MpIzZ89QoXwF67aYmGiOnzhBpYqVAAgNC+XUqZPExceTwy87RYsWw2AwkJiYyMFDByhUqDDHjx/D yckZGxsjuXLmTvU4wcNHDpMje3Z8fHyJi4/j0KGDJCQk4P+nu/4xsTGcPHmS8Ihw7Gxt8S9QkNy5 cwNw/PgxvLNkwS+bn7X8iZPH8fT0Irtf9jR9PX/hPEFBV0lITMTL05Py5SpgMBiwWCycOn2Ka9eC sLOzo2iRovj4pCRCLl26iAVITEzk6tUr+PsXJCQkmArlK6aK8fjxY1SqWBmAsLAwTp46QVx8PNn9 /ChWtDgGgwGTycT+A/soUrgox44fw9HRgfLlKqSJ8+8U8+tiTEeOWl8716sLgEvjRoSN+AiA5Js3 ifhxPF5Dh2REiCIiIiIikkloBMBzFhkZSUREhPUnKSkJV1dXjh47QmhoqLXcyVOniImNAeDGjeus Wr0SV1dXCvoX5Oy5s2zfsR1IeezfkaNH2LptC97eWciZIydJSckcOnzQWldMTAx79+3B1c2N5ORk li77jdjYWPLnL8DRo0cIC7vXbkx0NK6urpQqUYqsWbPx++ZNXA26CoAF2L9/n7WsyWRix84duLq4 puln0LUg9u3fS548eSlZvAR2dnZY7sx5/2Pz75w7d5YC+Qvg4eHBylUrrTFcv3GDbdu3cu1aEAUL BuDt7c3x48cJDgm21n369Gmio6MBuHnrJitXBeLi4kqAf0HOnT/Ptu1bU+K7ff+58SJnzlxP/8Y9 AxaTibBPP3usspHffU9ycMhzjkhERERERDIzjQB4zjb9sREDBuvrunXr4ZPFh4L+BTl15hTVslQD 4MzZ09Sq+QIA+w7so3ixEpQqWRoAd3d3Fi/5lapVqgJgNpupUK4i2bOn3IV3c3Nl/i/zSUhIwNHR kVOnT5Ivbz4cHRw5d/4cSUlJ1K/XAIAc2XMwc/YMazx+ftnx88tOcnIyDo4OhIXl58KF8+TKmYuA gAB279lFVHQU7m7unDl7Gr9sfri5uaXpZ0hICB4enuTMkRNbW1v87owQiIyM5Nz5c3Tu2AUnJycA oqKiOH7iODWq1wTA3s6eOrXrWusqWLAgp0+fwvfOKIHTZ05To1p1ICUhUaxYMUqXSjk3Hh6eLPz1 F6pWSTmPFouFsmXLkTNHzqd7w56hqOkzSLp06bHKmmNjCR87Fp9xY59zVCIiIiIiklkpAfCctW7Z Js0UAIBChQqzes0qqlSqws2bN7FYLOTIngOA8PBwIiOjOHPmtLW8wWAgLi4WAKPRSLZs2az7nJ1d yJ0rN6fPnKJkiVKcPHWSunXqARAVFZlqCL+9vT3eXt7W11HRUWzespm4uDjcXF2Jj4/H1TXlDr+9 nT0F/Qty8uQJKlWszImTJx44pL5QQCFu3rzBnLmzyZMnD8WKFccvmx/hEeEALF+xzFr2dtJt6xQA wJrIuFdXYQJXraBqlWrcCr5FcnKS9W5+eHg44RHhnD171lrexsaG2NhYjDZGDAZDqv5mFHNMDBFf fvlEx0TPnIVH3z7Y+fs/p6hERERERCQzUwIgg2T1zYqjoxOXr1zm0qVLFAoohMGQMlLA3t6B0qVK UbhQkTTHRUdHYzQaMRpTz94oVrQ4W7dvwdPDEzs7O+scfQd7B6JjYlKVvTvVAODIkSP4ZctmnXO/ Y+d263D7u/WuXB1Intx5SIhPIG+evOn2x8nJiYYNGhEbG8uFC+cJXLmCl9p1wN7eHhsbG9q2eRFb 2/Q/bn/e7uPjg4uLCxcvXSQo6CoBBQPunRsHe4oXK0HRIkXT1BMTG4PBYMDGxibddv5OkT/8SHJI 6KML3seSlETYx6PINnPG8wlKRETk/+zdeXxb5ZU38J/23bK8ybJsed+zk60khIQ9YQkUSFialgRo yzAzfVs67RRKXgqllDLzTqcspWVL2CEkJCQsAUICSSDE2RMv8S7bsizZlmwtlmRJ975/yL624iVO bFlezvfz6ae5V1f3PhKyrXPu85wzxXV3d6O1zQp9SnSXARJCyERFNQCiKL+nkF9dXS3ycvO5/ZkZ GSgtLeXu+ANAR2fHsOfS60NT3g9+dxAF+YX99qfCZmvn1tTX1bgCVP4AACAASURBVNXC5/Nxj3d1 uSEWh2YoOJ1O1NbVhp03Pj4eKlUM9n69F3l5+QMSD70cTgdYloVCoUBx8Qwo5Ap4fV4kJSZBJBLh TOlpMAwDAAgGg3C5nIOep/97U15ehpraGuTl9b03GemZKC0rhds98vdmvAVbW9Hx3PODPhYwmUL/ 32we9HH3zl3wHj4csbERQgghUxXLsqg11sBsaQ672UEIIaQPzQCIsP7r7QFg3tx53N323JzQGnut NhkxMTH9jrkEPp8Pb7/7NjQaDXy+bmg0sVh57aphr1VYUISSI4eRl5fH7VOr1fjB4h9gx0fbEaOK gUqlgjapb/nArJmzse+bvaisqkQg4EdWZjacTkfYeYsKi7Dv672D3nXvdfbsWVRWnYVKpYLP64PB YODW8F979bXY980+nDx1EiqVCh6PB4sWLkaOcmAtgV452bn4/vD3SExIRKw6lts/d85ceH1evPNe 33sTq1Zj1crrh31vxpP9L88M2dav/ZFH4TtTCveHHw75fNvGx5Dy2SeRGh4hhBAyJZktzdwNhnpj LYoLZ3IzCAkhhIRQAiBClAolfnrfz4Y9Ri6X4/57fzpgv0AgwNIll+HSHyyB2+2GVCqFSCQCAKhU Kmy4595Bz+fxdCE7KxtSiTRsf3HRDOTnFSAQDAx4TKvVYu3td8Dj8XBF+gY7ryHNMGjxv14L5i8I BedeDxQKZdgf3ISERNz2w9vh8/kQCAQgl8u5xxcvWjzo+WQyGe7bcP+A/QKBAEsvXYpLF1864L1R KpSDPmc8+Wvr4Hz9jSEfZzye0OM9syEG4z18GO5dH0Nxw8RJahBCCCETmcvtgrmlmdv2+rxoNpto KQAhhJyDlgBMYHw+HyqVigtwh+JwOHD8xDGcrTyLBfMXDnqMUCgcEPz3N1jw73a7ceLkCZw6fQqL hgjUz72GUqkaMtsukUigUCjGJBs/0vdmvNmf/BNYv3/Ix3XvvYP0s+VI+WTXsOexPf4E2EBgrIdH CCGETDnBYBB19TVc++FeLVYzujyDz8gjhJDpihIAU0AwGASfz8eN198EhUIxZucNrdlnsfLaVdDE asbsvFOV78RJuLZvH5Nz+aurh51JQAghhJAQY2M9fN2h+kYCgRBKRaibEcuyqDfWDkgMEELIdEZL AKYAjUYDjWbsA3SVSoU5s+eO+Xmnqq5PPgHO8yXD8fqbEO75CsH283cI6PznS4jZsH6shkcIIYRM Oe22NtjsfX9TMwwZkMsUKK0IFR/u8nShxWqGTpsSxVESQsjEQQkAQsYI4/We9xjnm2+O+HxBq3U0 wyGEEEKmNJ/Ph4ZGI7edEJ8ITWwcAECvS0WjqQEA0Gw2QaOOg1Q69FJIQgiZLmgJACFjRLFqJfjD FEq8EEKDAUnPPzcm5yKEEEKmGpZlUVtfjSATBABIJVIYUtO5x7VJyVD0XwrQUDvoeQghZLqhGQCE jBHp4sVIr6xA0GIZ9bkE2mTwJOIxGBUhhBAy9TSbTXB3uQEAPB4PWZk54PPD72tlGDJRVnEGLMvC 5XbB2mpBUqJ2sNMRQsi0QQkAQsYQTyKB0GCI9jAIIYSQKcvpcsBs6Wv5l5qSBrlMPuA4mVSGlGQ9 TOYmAEBTcyPU6lhIxJJxGyshhEw0tASAEEIIIYRMCoFgAHX1fdP5Y1RqaJOShzw+WauDrCc5wDAM jA11ER8jIYRMZJQAIIQQQgghk4KxoQ7d/m4AgFAoRGZ61rDH83g8ZBoywePxAAAOpwNt7a0RHych hExUlAAghBBCCCETXmubFfYOO7edmZ4FkUh03ufJ5YqwWQKNpgYwQSYiYySEkImOEgCEEEIIIWRC 83q9XFs/AEhK1EIdEzvi56ck6yGVhNoABoNBdHZ2jvkYCSFkMqAEACGEEEIImbB6W/4xTOiuvUwm R2pK2gWdg8/nI8OQyW17vV7Y7LYxHSchhEwGlAAghBBCCCETVlNzI7o8XQBCgXxWRvaAln8joVSq kJSQxG03NBkRCATGbJyEEDIZUAKAEEIIIYRMSA5HJyzWFm47VW+ATCq76POl6tMgFosBAIGAH40m 46jHSAghkwklAAghhBBCyIQTCPhRZ+xr+Rerjg27g38x+HwB0tP6lgK029rR6egY1TkJIWQyoQQA IYQQQgiZcOqMdfAH/AAAkUiEDMPwLf9GSh2jhkzeN4vA2FiPIBMck3MTQshERwkAQgghhBAyoVhb LWF35jPTsyEUCsfs/DExMRAJQy0Eu7u70WRqHLNzE0LIREYJAEIIIYQQMmF4PF1hLf+Sk3SIUcWM 6TX4fD4MaencdmubFU6Xc0yvQQghExElAAghhBBCyITAMAxq62vAsiwAQC5XQJ+SGpFraWLjoInV cNv1DXVcq0FCCJmqopIAEIvEEInE0bj0mFAqFNEeAiGEEELIlNNoaoDH6wHQ1/KPx+NF7HqG1AwI BKGlBT6fF81mU8SuRQghE0FUEgA8Hg/paemQjqKNS7TEqGKgS9ZHexiEEEIIIVNKR6cdrW1WbtuQ mg6pRBrRa4pEIqSlGrhtS2sL3F3uiF6TEEKiaeyqqVygWLUGsWrN+Q8khBBCCCFTmt/fjfqGOm5b ExuHhPjEcbl2QlwCbPZ2OBydYFkW9Q11KMovjujMA0IIiRaqAUAIIYQQQqKqzliLQCAAABCLxcgw ZI7r9TPSMsDnCwCEihCaLc3jen1CCBkvlAAghBBCCCFR02Ixw+F0AAgtE81Kz4ZAIBjXMYjFEqT2 KzZobmnmahEQQshUQgkAQgghhBASFe4uN0zmJm47WauDUqmKyliSErXctVmWRb2xlutGQAghUwUl AAghhJAJioIPMpUxTBB1/Vr+KRVKpES50HKGIRN8fujrsbvLDWurJarjIYSQsRa1IoCEEEIIGRrD MHjplZdgNBpRXFSMwsJCFOQXQC6XR3tohIyJhqYGeH1eAIBAIEBmhFv+jYRUIkVKsh5NzY0AAJO5 CbFqDSQSSVTHRQghY4USAIQQQsgE0xv8Hzt2DADwzf5v8M3+b0JtdA3pKCwsRGFhIbKzsiEU0p9y MvnY7Da0tbdy2+lpGZCIJ0aQrU1Khq3Dhq4uNxiGQX1DHfJzC6I9LEIIGRMR+9YQCATA4/Fw5Pjh SF2CEHIBxrugEiHk4pwb/PcXWpdcj3pjPT797FOIxWLk5uSGEgIFhdDr9VG/g0rI+XR3d8PY2Nfy Lz4uAXGa+CiOKByPx0OGIRPlZ0vBsiycLgda26xITEiK9tAIIWTUIpYACAYDEAgEmDNzXqQuQQgZ IafLgaqaqmgPgxByHgzD4OVXXg4L/pctW4Y4TRzKy8tRU1vDtUoDQoFUaVkpSstKAQAqlQoFBQUo KixCYUEhNBrNuL8GQobDsixqjTUIBoMAAIlEAkNaepRHNZBcJkeyVgdzS6gdYFNzI9TqWIhF4iiP jBBCRofmDRJCCCETQG/wf/TYUW7flVdciTW3rwEArLxuJbq7u1FVXYXy8nKUl5fD1GwKKxTodDpR UlKCkpISAIBWq+WSAfn5+ZBKpeP7ogg5h9nSDJfLCaBfyz/+xJyhlpKsh73DDq/Xg2AwCGNDPXKz 86I9LEIIGRVKABBCCCFRdr7gv5dYLEZxUTGKi4oBAA6HAxVnK1BeXo6y8jJ0dHSEHW+xWGCxWLB3 317w+XxkZGRwCYHMzExaGkTGlcvt4u6oA0CKTg+FQhnFEQ2vdylARWUZAKDT0YF2ezviJ9ByBUII uVCUACCEEEKiaKTB/2BiYmKwcMFCLFywEABgbjGjorwCZRVlqKyshNfrDbtObW0tamtrsevjXZBI JMjPy+eWDOh0urF/cYT0CAbDW/6plDHQaVOiPKrzUyqU0CYmw9LaAgBobDJCrVJT8U1CyKRFv70I IYSQKBlN8D8YXbIOumQdVqxYwQX85RWh5QJ19XVgGIY71ufz4dTpUzh1+hQAIFYdi4LCUDKgIL8A arV6dC+OkH6MjfXwdfsAAEKBEJkZWVEe0cjpU1LR0WmHr9uHQCCAhqZ6ZGXkRHtYhBByUSgBQAgh hEQBwzB45dVXxiz4Pxefz0dOTg5ycnJw4w03wuv14uzZsygrL0N5RTksFkvY8R2dHTh06BAOHToE AEhJSUFhQSGKCouQm5tLfdDJRWu3tcFmb+e20w2Zk6qYHp/PR7ohE5XVFQBCLQzjNHbEqqnIJiFk 8qEEACFkwvB4PPj1b34d7WFEHAsWPEyOVm2zZ83GT+//abSHMeX0Bv9Hjh7h9o1l8D8YqVSK2bNn Y/bs2QAAu90eSgaUl6OiogLOnsJsvZqbm9Hc3Iw9X+2BQCBAVlZWaHZAQQHEUvr6QEbG5/OhodHI bSfGJ0ITO/kC5xhVDBLiE9HW3goAMDYaoVLGUB0NQsikE7W/4B2ddjQ1N8Hr9URrCBclRhWDtNR0 yKSyaA+FkCmHBRvW4oxE39FjR1FxtgIF+QXRHsqUMVjwf8WKKyIa/A9Go9FgyaVLsOTSJWBZFk2m Jq6YYHV1Nfx+P3dsMBhEVVUVqqqqsOOjHZBIJSjML0RhYSEKCwqh1WrHdexkcmBZFrX11QgyoZZ/ UqkUaakTr+XfSKXpDeh0dMLv74bf341GUwMyDJnRHhYhhFyQqCQAWJaFsdEIv787GpcfFYfTAXOL idZ+EUKmjfe3vI/fP/x78Pn8aA9l0hsq+F+7Zm0URxWqdp6Wmoa01DRcc/U18Pv9qKmp4ZYLNDY2 hrUb9Hl9OHHyBE6cPAEAiIuL45IBBQUFUClV0XopZAJpNpvg7nID6Gn5l5EzqX+PCAQCpKelo7q2 CgDQ1t6KOE08YlQxUR4ZIYSMXFQSAN09mdPJyuV2n/eY1tY2bNiwAQAgkUiRlpaKoqIi3H33XZDL 5ZEeIiGTklwmx3N/ey7aw4i4ZmsTUpJSoz2MYblcLjz2+GPwer0wmUzYf2A/Ll92ebSHNamNR/Dv 93eDx+OPukK5SCRCQUEBCgpCMz9cLhfXbrC8vBzttvaw4202Gw4ePIiDBw+Cx+MhVZ8aSggUFiI3 JxcikWhU4yGTj9PlgNnS1/IvNSUNctnk//4Tq9YgThMHm90GADA21KG4cOakTmwQQqYXWsQXId3d Phw8+C22bt0CmUyGhoZG7Ny5E8888ww+/HAbiouLoz1EQiak6RAoCIXCCf86NRoNVq1chW0fbgMA 7PhoBxbMX0AJzIvEMAxeeS1ywX8g4Ie5xQxrmwUsy0IqkUKpVEGlVEGpUI26gJ9SqcT8S+Zj/iXz AQCny06ivdWOiooKVJytgMfTt5yPZVk0NjWisakRn3/xOYRCIXJyclBYEEoIGNIM4PEmRw0McnEC wQDq6mu57ZgYNbRJyVEc0dgypGbA4XQgEAjA1+2DqbkJaamGaA+LEEJGhBIAETZnzhxoNBosXrwY a9bcjoce+jV+8Ytf4ssvPwcAdHV14bnnnkdpaRlSU/V48MF/QUpKqC/uli0fQKvVorKyEiUlJVi5 8jpcf/31EIlECAaDePTRjbj33nvx6quvIhAIYN26dZgxI5RYaG1txaZNm1FaWgaZTIpVq1bhxhtv iNr7QAiZfK684krsP7Afra2tcLvd2LlrZ9Snqk9GXPB/pC/4X7FixZi8l8FgAGaLGdZWS1iLP6/P C6/PyxUsE4lEUCp6EgJKFWRS2aiC8Lj4OMwsmo3lly8HwzCoN9aHZgdUlKO2thbBYJA7NhAIhBIF FRX4cPuHUCgUKMgv4JYMJCQkXPwbQCYkY0MduntmegqFQmQaJk/Lv5EQCoVIS01HXX0NAMDS2gKN Jg5KhTLKIyOEkPOjBMA4W79+PZYtuxwulwtKpRJr194BhmHw85//HB9//DFWrLgShw8fglqtxp49 e3Do0Pe4/vrrsXLldfjv//4ftLW147777gXDMHjxxX/g9OnTuPXWW+FyuXDddStRXl4KlUqFpiYT 4uPj8cADP0NVVRUeeeT3CAYDuPnmm6P9FhBCJgmhUIjbb70dL7z4AgBg39f7sGzZMuiSdVEe2eQx VPB/x5o7RnXeIBOE1WpBi9UcFmwDobXW/dfrA4Df74e9wwZ7R2jasoAvgEKh7EkIKKGQKy96CjOf z0dWZhayMrNw/arr4fP5UFlVyS0XaDY3hx3vdrtx9NhRrv1hYmIiNzugIL+AZplMcq1tVtg77Nx2 ZnrWhJ/xdDHiNfGw2drR6egAANQ31KG4YAbNbiGETHiUABhnBkMaAKC+3ojubh++/fY7VFZWID4+ HjfeeAPmzr0Eb7/9Nh544AEAQG5uLp588gkAgMfjxfbt23Hfffdy57vvvnuxevVqAMBnn32Gffv2 4cYbb8TcuXMwd+4c+P1+JCQk4JZbbsZHH+2iBACZ0ILBIM6UnYr2MCIuyATRZm2L9jBGJC4hHgUF BaioqADDMNiyZQv+/d/+PdrDmhQiEfwzDANrmwUtFvOAjhkKuQJ6XSqUShXcXS64XC44XU643S6u CnuvIBOEw9kJh7MTQChpIJcroFKEEgJKheqi6whIJBLMnDETM2fMBAB0dHagoryCKyjocDjCjm9t bUVrayu+2f8NeDwe0g3pXP2A7KzsUdczIOPH6/Wi0dTAbSclaqGOiY3iiCIr3ZCB0vLTCAaD8Ho9 aG4xQa+b2PVdCCGE/qqOs9bW0Jd+rVaLr776CgUFBYiPj+ceX7p0Cerq6sO2e2VlZaK6ujrsfEuW LOX+nZmZhaqq0ONWqxUPPPAgmpqakJaWho6ODsjl1LqQTHz+gP/8B00B/adrT2QWawuuvOIKVFZW gmEYlJaV4tTpU5g1c1a0hzahjXXwz7IsWtusMFuaw9rzAYBMJodep0esuq+3ukoZA5UyBrqe53o8 XXC5QwkBl9s54Bwsy8LtdsHtdgHW0D6pVAaVQgllz7IBifji6gjEqmOxePFiLF68GADQ3NwcSgaU l6Oqugo+ny9sHPXGetQb6/HpZ59CLBYjNyeXWy6g1+vpDusE1dvyr/d3m0wmR5p+aq+LF4vESE1J g7GxHgDQYjFDExs3JYodEkKmLkoAjLPduz9DSkoKEhMToNFoYDKZEAwGIRAIAABGoxGLFi3iju/d PxSBYPApmy+//Apyc3OwdesWAMDTT/8FBw4cGKNXQQiZTroDPixcuBCHDh0CAGz5YAuKi4rP+/tp umIYBq++9mp48L/84oJ/lmXRbmtDc0szurt9YY9JJVKk6PSI08QP8eyQ3rv7crkCSYlaAIDP54PT 7YTLFfqf1+cd8Dyv1wOv14PWnjoCYpGYmx1wbgLhQqSkpCAlJQVXXXkVAoEAautqUV5ejrLyMhiN xrDlC93d3SgtK0VpWSkAQKVSoaCgALNmzsLCBQsvegxk7DU1N6LL0wWgZ1lIRva4J2tYlh33ayYm JMFmt8HpcoQSWA11KMwrokQVIWTCogTAOGBZFmazGR99tBNPPfU0Xngh1OZs4cIF4PF42LLlA9xx x1ocP34Chw+X4JFHHh71Nc3mFuh0yT3/NuODDz5AcvLUqcBLpiaBQIDZM+ZGexgR19LWjOSElGgP Y1gsWNTW18DlcgIA8vJzcfLkSXg8HlitVny19ytcfdXVUR7lxNMb/JccKeH2rVi+AnesvfDg32Zv R7PZNCA4F4vFSEnWIz4u4aKDDIlEAolEgoS4UAG+QMAPp8sFV09SoMvTNaCOQLe/Gza7jWt/ZrfZ oeydIaBQQSFXXHAdAaFQiLzcPOTl5mH1TavR1dUVajdYEaof0NraGna80+lESUkJSkpKUF9fjzW3 r7mo10/GlsPRCYu1hdtO0xsgk47frEOv14u33n4L9g477vnxPeNeWDLDkIHSijNgGAZdXW5YrC1I 1lKtFELIxEQJgAgrKiqGQCBEUlIi8vPzsW3bB1iwYAEAIDY2Fi+++Hc8+OC/4qmn/gyr1YJHHnk4 bAbAxfrZz36K++//KT755FM4nU788Ie3hN2NImSimorFos7F5/MnxevMzcpFeWU5vF4PJBIxLrlk Lg4c+BYAsOvjXVi8aDFUKlWURzlxsAw7JsF/R6cdJrMJnp67qb1EIhF02hQkJiSN+d1FoVAETawG mtjQMgKGYeBy9yUEXG7XgGUrwWAQnY5OdDr66ggo5AouIaBUKiEUXNjXDLlcjnlz52He3HkAgLa2 Ni4ZUHG2Am63mzt2z1d7AICSAFEWCPhRZ+xr+Rer1iAxIWncrl9bV4tXXn0FbW2hJZaP//Fx3PbD 27Bs2bJxG4NEIoVel8rVP2huMSE2VgOpRDpuYyAXZt/X+/D5559HexijEmACEPKnRyg3nV7rD2+L fL226fFORoFer4fNdv4iX9deew2qqs7CbDYjMTExLCh48cW/hx27aNEiHO3pIS0SiQac/9ln/5f7 94wZxfjuu4NobW1FYmLiaF4KIWSaEgiEyMvOR3llGfz+buQX5KO0rBx2mx1erxfbd2zHuh+ti/Yw JwSGYbBt23aUninl9l1o8O9wdsLU3AR3lztsv1AoRLJWh6QE7UVX6r9QfD4fMaoYxKhiAIRmsnV5 unqSAU50OjoHJARYlu1JGrgAmAEAMqmMqyGgUighvsA6AgkJCbhs6WW4bOllYFkWDQ0N2P35bq6D ACUBoq/OWMfVbhGJxMgwZI7LdVmWxaeffYqdu3aGfRZ9Ph/eeuctHD9xHD9e92NoNJphzjJ2khK1 sNnb4e5yh1pjNtShILdwXK5NLozL5cKOHTu4JSuETDeUAJgAeDweUlIiMx2Ygn9CyGiIxWLkZufh bFU5AGDx4kX49JPPAAAHvz2I5ZcvR1paWjSHGHUMw+DVTa9edPDvcjlhMjfB2bPcopdAIIA2KRna pGQI+NGtt9B7d18hV0CLZJgsjYiPTeRmBzhdTvgGqSPg8Xrg8XrQ2haqLBiqI9CXEJBdQLE0Ho+H 9PR03HfvfRAIBDhcchgAJQGiydpq4drgAaGWf+PRtcFut+OV115BVVUVt08ul0OtVsNsDiWfysrL 8PgTj2PtmrVcAcpI4vF4yEjPQlnFmVAyzOWEtdXC1d0gE8eOnRT8k+mNEgCEjKNgezt8R49B+oPF 4NPUaTJJyGVyZGfmoqrmLFJT9TCkG9BgbADLsnjv/ffw64d+He0hRk1v8F9S0jftf/nly0cU/Lu7 3DCZm+DomULfi8/nIylRi2St7oKn0I8nqUQKqUSKhPhQotkf8PcUFXTB6XbCM2QdgXbY7O0AQkmO 3uUCKoUKCoXyvMsb+Hw+1t+zHgDCkgA8Hg+333b7WL9MMgSPpyus5V+yVsfNGImkY8eO4Y233kBX V18Al5ebhw3rN0ClUmHnrp34/IvPQ+vxPV14bfNrOHbiGNbdvS7iS5ZkUhl0ySloNpsAAE3NTYhV x17wzBcSOSaTKawo9v333o/MzPGZtTLWJkM9obEynV5rXUNNxK8xcb9ZEDLFBFtaYLr6WgRMJgiS k6H/9GMI09OjPSxCRiRGFYPM9CzU1tdg8eJFaGpsAsMwqKquwpGjRzD/kvnRHuK4Gyr4v/OOO4d9 nsfrQbO5CfYOe9h+Ho+HxIQk6JJTIBJO/BoR5xIJRdDExkETGwcAYJhgaEmAKzRDwN01VB2BDu4u Mo/Hg0KhhFKhhKqnlsBg3SYGSwJ8uedLAKAkwDhgGAa19TVcgkcuV0CvS43oNX0+H97b8h4OHjzI 7ePz+bjxhhux8rqVXOLolptvwezZs7Fp8yZYLBYAwMmTJ1FTU4O777wb8+bNi+g4ddoU2O02eLwe MEwQ9Y31yMvOj+g1yci9v+V97vfQjBkzMH/+5P3b5Q10hbUSn8qm02ulBAAhU0jbw79HwBS6KxBs aYH1Xx5Eyse7ojwqQkYuThOPbn83gEbMmFGMU6dOAwC2btuK2bNmT4rChmNlsOB/wcL5wwb/Xp8X zWYTd/e7F4/HQ0JcAnTJeojF4oiNebzx+QLEqNSIUakBhNcRcPbUEggEAmHP6Z067XI50WLpqSMg k3MJAU1sHBfoURIgehpNDfB4PQDGp+VfQ0MDXn71ZS6gB0L1Ie7bcN+gd2+zMrPw6COPYtuH27B3 396ez5UL/3jpH1iwYAHuXDt8km40epcCVFSWgWVZOBydaLe1IT5ufDsTkIGOnziOirMVAEKzj9bc RsuGyPRECYBJwOv1QiAQTKsv11ON7/gJuHfsCNvn/e4Quj79DPKV10VpVIRcuOQkHbq7uzF33lxU VVXD4/HAZrNh9+e7ccP1N0R7eONiqDv/y1YsHfT47u5uNLeY0G5rGzAlPl4TjxSdHpJpUC08rI5A UqgtrdfrhcvdlxDw+XwDnufxdMHj6UJrmxWxahuyM3MGJAFYsNx/D0oCRFZHp52r6QAAhtT0iFW7 Z1kWX3z5BXZ8tCMsWbRo4SLcdeddkEqHvq5IJMLaNWsxZ84cbN68Ge22UOKtpKQElZWVWHXDSui1 kalfopArkJSo5VojNjQ1IEalpu9xURQIBPDB1g+47RXLV0CrpfoMZHoan3LC09A3B75BY2Mjt+3x eLDz448G7Nv18c6eO2pDO3BwPyqrzkZsrCTybH/4A3DOF38AaP/D40AwGIUREXLxDKnp0CZpMX/B Jdy+z3Z/BrvdPsyzpgaGYfDaptdGNO3f7/ejocmI02Un0dbeGhb8a2I1KC6cicyM7GkR/A9FKg3V EMhMz8LMotmYPWMusjNzkJSohVwmH3BXuaPTjqrayrClBHw+Hxvu2cC12AVCSYAtH2wZt9cxXfj9 3ahvqOO24zRxXA2IsdbZ2Yn/ffZ/sXXbVi74l0ql2LB+Azas3zBs8N9ffl4+Nj66EUuX9CXoOjs7 8c5b7+L1N1+H1zuweOVY0OtSuZ/tYDCAhqb6iFyHjMyer/ZwrSKVSiWuv/76KI+IkOihBECE8Pl8 1Bn7/kiaW8zo6OgYsM/d5YZYNHWmfJKBPF99Bc83+7ltyOM7IwAAIABJREFUocHA/dtfWQnHm29F Y1iEjEpmehYuueQSxCeE1uT5/X68t+W9KI8qsnqD/97p5sDgwX8gEEBTcyNOl52EtdUSFvirY9Qo yi9GdmYuZFLZuI19shCJQnUEDKnpKCqYgTkz5yEvOz9s+rTD0YmqmrMIMn3JU0oCjI86Yy0XjIvF YqSnRaZ42qlTp/D4Hx9HeXk5ty8rKzStf9HCRRd8PqlUinU/Wod/e/DfoFaruf0HDx7E4088jrOV Y3+Thc/nI8OQwW3bO+ywd9jG/Drk/BwOBz759BNue/WNqyG/gA4khEw1lACIkBRdCszmZm7bbG7G zBmzBuzT6UIVLYPBIE6fOYUvv/oSB789OKAqNMuwOHnqJL786kucPnN6wDTS/mpqqvHN/q/x5Z4v cOz40b5zsCzKysuwZ+8eHDi4HzZb3x+i2rpaNDY1oryiDF9+9SWMDUYcO34s7LwORyeOHO2769XW 1oqD3x7Anq++RHlF3x9pj8eDwyWHYbO148DB/Thx8sRI37aph2VDd/l7aH77GxhOHEPcHx7j9tn/ /DTYLmpHQyYXPp+PvOw8rFh+Obfv+PHjKCsvHeZZk9dIgn+WZdFsNuF02Um0WMxhd6lVShUK8oqQ m50PuVwxrmOfzAQCAWJi1MhMz4Iuua8CtNPlRGVVBYLBvmnhlASIrBaLGQ6nA0BoOUdWevagBRpH w+/3451338Hzf38eLpeLu9aqlavwHw/9BxISRreOfsaMGXhs42Nhn5F2Wzv+56//g3fffxfd3cPP yLxQKmUMEhOSuO2GRiMCwcAwzyCR8OH2D7mZHqn6VCxdOvhyLUKmC0oARIguWYfOzk6uTU2zuRk5 2TlgWYTtS+lJAHzy2cewWCzIy82DRCLB9h3bw1rcnDx9Eu4uNzLSM1BeUY5vvzs48KIA6o31OHn6 FDIyMlFcPAP8fr2jv9zzBYwN9cjJyoZSqcTOjz9CZ2co0dDcbML+/d/AarUiLzcPmlgNTp46gY6O vv6+5RUV3JhMJhN2f7EbMTExyM7KxtnKChz6/hAAwOfz4vSZUzj43UEkJCQiRacbq7d10nFt3Ybu 02e4bfmVVwAAFNddy+0LWizoeP6FcR8bIaMlEAhx+WUrkJOTze175913Bl3HPZmdL/hnGAYtFjOs FiuaW0wI9lvWo1AokZdTgPzcQigVynEf+1Si16UiNaVvzba7y42KqgoEAn5uH5cEmH9OEmArJQFG o7dlZS+dNgVK5di21DOZTPjTU3/Cvq/3cfs0Gg0e+tVDWH3TavD5Y/OVVS6X474N9+G2NbdCqQz9 TLIsi7179+KJJ59AbW3tmFynV2pKGjfT0x/wo7Gp4TzPIOdiGGbYG1/DMTYY8d2h77jtNWvWjNln iZDJin4CIkQmk0ETq0GzuRlerxfBQBBKpRK6ZB23z263I0WnQ5OpCR0dHVix/AoY0gyYf8l8JCYl orKqkjtfbGwsLl18KXKyc7B0yVKUV5QPqJ4MAG1tbYiNjYU+RQ9dsg5zZs8J7W9vQ2NTI65ccRXS 0zMwZ/ZcGNLSw+7cy+VyXL5sOQxpBsTExCArM4urPcCyLKqqq5CXF2plc/T4EcycMQszZ8xCRkYm lly6FKVlZ7hf0MFgEIsWLEZBfgGSkqZnkRW2uxu2J/80omM7//Ysgq1tER4RIWNPLBbjrjvvhlAY qilrtbZi58c7woLgyWyo4P+OtXeAZVlYWy04XXoSTc2NYXf8ZTI5crJyUZhXNC690aeLZK0OhtS+ 9qkeTxcqKsvDaunw+XxsWH9OEuBLSgJcLIYJoq5fyz+lQhk2G2Ms7N23F089/RSa+82SvGTeJdj4 6Ebk5uSO6bV6FRUV4rGNj2H27NncPqvVimf++xls+3DboN+xLoZAIEB6v6UA7bY2dJ4zy5MMzeVy orTiDNcV5EK99/573Gd37py5yM+jloyEUBeACNKlhJYB8Pl8JPfcBdfpdNw+tVoNuVyBmtrQmrqt 2/qqk/q6fVDH9K1TS9bq+v07VD3Z6XJCE6sJu2ZhQSG+OfA13njrdaQbMlBcXIykxCSuONf2HR9y x3b7/WF353Xn3KnPy8vHnq/2YMH8hTCZmiASCblr2+12uN1uVPRLIADg2gIJhUIkJkamMNBk4Xht EwJG44iOZdxu2P/yFyQ885cIj4qQsadP0WPF8uX44stQ9fWDB79Ffn4+ZhTNivLIRodhGGzavCks +L982eVYu2Yt2m1taG4xDZgyLJVIkaJLRZwmbryHO20kJWrBFwhgbKgDy7Lw+rw4W1mOvNwCSMQS AH1JAAAoOdLTHaDn83n7rdQd4EI0NDXA6wtNnxYIBMgcw5Z/TpcTmzdvxukzp7l9EokEa29fiyVL lozJNYajUqnwLz//Fxw6dAjvvv8uPB4PGIbB7s934/SZ09hwzwakpY2+U4A6Jhbxmni097QANTbW YUbhzLBZmiQcwzBoam6EtTXU+tFsaUZ8XDzEPT/jI1FypAQ1NaGe6kKhELfdeltExkrIZEMJgAhK 0aXgyJES8PkCpCT3JACSdThx4jj4fAG3/l8iFkMhV2DN7WuHPJfL5eT+7Xa7wTAM90WnP4VCgZXX roLL5URtXS127voId91xNyRiMcQiMW679fYhpz713sHrpUvWQSgQoMnUhKqqSuTl5nGPicViLJi/ EDnZOQPO0+3zQSAQRLQn8ETHuFzo+O//vqDnODe/DvXPfwZRdvb5DyZkgrnpxtU4XFLSs/TJg/0H DkChUEAiG/mXtYmkN/j//vD33L7Ll12Oa669BqUVZ+DzhVcOl4glkMllyM7Inda/+8ZLQlwC+Dw+ 6oyhO9O+bl8oCZBTwFWHpyTA6NnsNrS1t3Lb6WkZg373uBhl5WV4bdNrcDgc3D6DwYD7Ntw37u3Z Fi9ejPz8fLz+xusoKy8DADQ3N+Opp5/CqpWrsGrlqlFPG09LTUensxOBQADd3d1oam4Km81C+jic Dhgb6uDr7ltOxjAMGk0NyM4c2YwQv9+Prdu2cttXXXnVqGtIEDJV0BKACNIl69DR2YF6Yx0X7KtU KgQCAdQb67j1/3p9KtxdblRVV3HTlLr93WE1AJpMTXC5XGBZFhVnyxEXFw+5fGAF094/pEqlCjOK Z0IikcLr80KrTQbDMigrL+OmqQYCAbjd7mFfQ15ePs6UnoaxwYjcfgmAzIxMnCk9jS5P3xg7OjsG O8W01Pnc8wi2tV/Qc9hAALbHn4jQiAiJLLFYjFt/eCu3feZ0KeqN9XB0OoZ51sQ0WPC/ePFizJ4z E/XG2rDgXyQSw5CWgRlFsyCTyyj4H0dxmjjkZOZygVm3vxsVVeVhf5d6kwDz58/n9tFygJHp7u6G sbGvc1F8XALiNPGjPm8gEMCWrVvwt2f/xn1n4fF4uObqa/Db//ht1HqzazQa/OLff4G777wbEkko yREMBrFz1078+S9/htl8cVPQewmFQhhSM7hta6sl7OYOCb3f9Q11qKyuCAv+e9k77HA4R7Z8Yvfn u7nZrzExMVi1ctWYjpWQyYxmAERQbx0Av98PlaqvWE6yTofq6ipu+r1CocDVV12D/Qe+wfffH4JU JkN3tw/LL1/BBfmGtHTs/PgjAKEvp9defd2g1ywtO4PaulqoVCr4vD7k5eZyywSuvfo6fL1/H44d PwqlQgmP14Mlly6FQjF0Req83FwcOVqCVH1qWAGr+ZcswLffHcTb77yFOE0cvD4vEhIScc1V14zu TZsCgq2t6Hju+UEfC5hMkMyfj0Dz4F8k3Dt3wXv4MKQLF0ZyiIRExMIFC7Hv632ora1FMBjE94cO 4+prroLF2gJtUnK0hzciLMsOCP5nzZqJmbOKuSVOQOjLvE6bgsSEJCooFUVqdSxys/JQVVsJhmEQ CPhxtqoCedl5UPT8zeLz+bh3/b0AgCNHjgCgmQDnw7Isao01XC0PiUQCQ9ro71ZbLBa89MpLaGxs 5Pap1Wqsv2c9CgsKR33+sbBs2TIUFRVh0+ZNqKquAgAYjUY8+dSTuOnGm3D1VVdfdKIvThMHm12D js5QYFrfUIeighn0OwRAp6MDxob6sHoeQoEQaakGOByd3PKJhkYjigtnDvvfwG63Y/fnu7ntW26+ hUvqEEIoARBxt9+2ZsC+K5ZfgSuWXxG2L1WfijvX3gWPxwOWZSGT9d1JuurKq7njurrckMnkQ/7i +8HiSzH/kgXw+XxcddteWq0Wa25bGypKGAxCLu87z9Illw16PqVShZ/e97MB+4VCIZZddjmWLrkM brcbMpmMW0IQG6vBT9bdM8Q7MvXZ//LMkG392h95FL4zpXB/+OGgjwOAbeNjSPnskyEfJ2Si4vF4 WLtmLf789J/Bsizq640wmUJFvUQi8YRfF8+yLF7b9FpY8F9YVIBFi/sScgKBAMlJOmiTtLR+d4JQ qWKQl1OAqpqzCAaDCAYDqKw+i5zsPKh6KtVTEuDCmC3N3N1pHo+HrIwcCEb5ed9/YD/e3/J+WN2M WbNm4SfrfjLg+0q0JSQk4KFfPYQ9e/Zg+0fb4ff7uSnlJ0+dxD0/vuei6xylp6XD6XIgGAzC6/Oi ucUU1t1iugkEA2hsMqLdFj5rUhOrgSE1AyKRCDEqNTo6OxBkQu+ZpbUFyUlDd5ja+uFW7nOWnp6O Hyz+QURfAyGTDaUcJxiZTBYWmJ9LLlecN/MsEomG/WMqlUqhUJz/PCPB5/OhUqkG1A+Yrvy1dXC+ /saQjzMeD5yvvwFmmGnR3sOH4d71cSSGR0jEZaRnYPGixdz2d98dAsuyqDPWwOmauMsBhgr+ly4N FSLj8/nQaVMwq3g2dMkpFPxPMEqFEvk5BdzfoiATRFXN2bBq671JgHOXA3yw9YMB55vOXG4XzC19 1fhTdHoo5EPPFDwft9uNF//5It58600uKBOJRLjzjjvx4AMPTrjgvxePx8NVV12FRx5+BBnpGdz+ 6upqPPHkE9j39b6Lak0nEomRpjdw2xZrC7q6hl+OOVXZO+woLTsdFvwLhSJkZ+YgOzMXIpEIQOjz kqLTc8c0m5vh93cPOB8ANDY2oaSkhNteu2YtLc0i5ByUACBkDNmf/BNYv3/Ix3XvvYP0s+VI+WTX sOexPf4E2DFqQUTIeOs/3dJus6O8rBwsy6K6tipsGv1EwbIsXn7l5UGDfz6fD21SMmYVz4Y+JRUC ASU7Jyq5XIH83EIuaGAYBtW1lbB32LljBksCfPHlF5QE6Kd/yz+VMgY67cW3/KusrMQTTz6B48eP c/v0ej0e/t3DWH758tEOdVzoknX47W9+i5tuvAkCQSjx5/P58M677+B/n/1fbp35hUiIT+Tag7Is i/qejhbTRSDgR01dNWrqquAP9H1nitfEY0bhTGhiB84WS0rUQiqVAQi1pmw0NQ44hmVZ7P60b+r/ ggULkJ1FhZUJORclAAgZI74TJ+Havn1MzuWvrh52JgEhE5larcbK61Zy20ePHofP50MwGERV9dmw NZ7R5vF68Ozzf8ORo0e4fYVFBbjssqVITEjCjKJZSNMbIBSKojhKMlIyqQwFuUVcqzCWZVFbX82t Hwb6JQEuoSTAYHqLrwkFQmRmZF3UORiGwfYd2/H//vr/wgLkFctX4He//R1XBHmy4PP5uH7V9fjd f/4Oen3fnejy8nL84Yk/4Nvvvr3gc6YbMrm1/12erovucz/Z2OztOFN+GvYOG7dPLBIjNysPmRnZ Q84o5fF4YV0TbPZ2OM8povjdoe/Q3FNjSSQS4dZbbgUhZCBKABAyRro++QQ4Twbf8fqbsD/9F3S+ 9PJ5z9f5z5fGamiEjLurr7qaa7nk9Xpx7NgJAKFK7VXVZ7niYtHi6/ahrr4Gz7/wHEpLy7j9hUUF WH3TaswonIX0tAyIReIojpJcDIlEgoLcQkgloXaALMuirr4GrW1W7hg+n497N1ASoL92W1vYdroh 86I+/62trfjLf/0Fn372ab+ZBCr864P/ijvW3sHN0JiM0lLT8PB/Pozrrr2OC949Hg82v74ZL/z9 hbCWhucjEUugT0nltptbTBNyhtRY8fu7UV1bidr6GgT6zXBMiE9EceFMqNWx5z1HjCombHZAQ5OR +4z5fD5s39F3E+baa66FRqMZw1dAyNRBcxkJGSOM13veY5xvvjni8wWt1vMfRMgEJRQKcdutt+HF f7wIACgrLUNhYQFiY9XweD2orq1CXk7+uK/N9Pu7YW5phrXNin17v0ZVVTX32Jw5s3HPj++BTDaw xSqZXMRiMfJzC1FZXcEFVcbGejAMw3Wk6E0CAOBmgHzx5RcAgNtuvS0Ko44en8+HhkYjt52YkMR1 ELoQh74/hHfefQfefn8PiwqLsP6e9YiJiRmTsUabUCjELTffgtmzZ2PT5k2wWCwAgJOnTqK6php3 3XlXWGJpONrEZNjtNrjcLm4pQGFeUSSHP2YqqyrDujkMx+12wd5pB8v03SQRCgXQxMaj0+5ETXXt iK8bCATQYjFzgX9VZTVUShVqa2vR2Rmq+aHRaHDtNddewKshZHqhBAAhY0SxaiWcm18H4xx9X1+h wYCEp/40BqMiJHrmzpmLzMwM1NWFAq8Tx05g+RWXAwCcLgfqjDXIysgZl7EEAgGYLc1obbMiGAzi 633fhAX/l/7gUvx43Y+pWNQUIhKJkJ9XiKrqs3D3FFlrNDWAYRjokkNT0CkJ0LdMIsiEZuVIpdKw InUj4fV68dbbb+FwyWFun1AoxM2rb8ZVV141JX+usjKz8Ogjj2Lbh9uwd99esCwLt9uNl15+CSdO nMCdd9w5bJvlXhmGTJRWnOl5vmtStE09eeok/vnSP8Pu5E8kt95yK8Rimr1FyFAoAUDIGJEuXoz0 ygoEe+4GjIZAmwyehP54kcnv2uuuwT//8TIYhkFVdTUWLFwAhTJ0h91mt0EkarjgYONCBINBtFjN sFgtYJggWJYdEPwvu2wZ7rrzrikZpEx3QoEw1CKwtpJra2cyNyHIBLnWa71JAJZlcfTYUQDTKwnQ bDZxCRIAyMrIuaC+9LW1tXj51ZfR3t5XZ0Gr1eL+e+9HWtrUbm8nEomwds1azJkzB5s3b+aq2Zcc KcHZyrNY96N1mDVz1rDnkEplSEnWw2RuAhD6fMaqNRO2b/1ED/7T0lKxYMGCaA+DkAmNEgCEjCGe RAKhIXLBDCGTTZI2CZctvQxff/M1AODLL/bg3ns3oM3WCiDUAkssEo/5HS+GYWBpbUGLpQXBYOiL KgX/05NAIEBedj6qa6vgcIamCLdYzGAYhisqxufzcd+99wHAgCTAD5YuisKox4fT5YDZ0hy2Tz7C JTAMw+CTTz/Bx598DIZhuP1LlyzF2jVrp9Ud2Py8fGx8dCO2fLAFBw4eAAA4HA48/8LzWHLpEqy5 fQ2kUumQz0/W6mDvsKHL0wWGYVDfWIf8nILxGv6InRv8a7VazCieEXZMMBiAw+ngWj4CAHg8yGVy KBXKMftd6+v2oaNfh484TRxEIjFy8qnqPyHnQwkAQgghEXXTjTehpKQEXZ4uWFutqKysQnZOFlcF utHUALFYPGjrpwvFMAxa26wwW8wI9GsvxbIsDuz/Niz4v2zpZRT8TxN8Ph+52XmoqatGR2coaLC2 WsAEg0g3ZILH4w2ZBHB1OXHPuvVRG3ukBIIB1NX3rb1Wx6jR6egc0XNtNhteee0VVFf3/TzJ5XKs +9E6zJs7b8zHOhlIpVKs+9E6zJ0zF2+8+QY6OjsAAAe/PYjyinL8ZN1PUFAweFDP4/GQYchEeWUZ WJaF0+lAW3srEuITx/MlDOvc4D85ORkP/fKhsNoOFmsLTOamsISQTCZHhiETCvn5l0NcqOraKu7n WSFXoDC/GCbLyOoSEDKdURcAQgghEaVUKnHDDTdw2x9/8jES4hKhUqq4fbX1NQNaOl0IlmXR2mbF mbJTaDQ1hAX/EokUx46eQEVFBbfvsqWX4e677qbgfxrh8XjIzsxBnCae29dma0Odsa/vfW8S4JJ5 l3DHfPftIWzdtnXcxxtpxoY6riWnUChCRvrIWv4dOXoET/zxibDgPy83Dxt/v3HaBv/9zZgxA/93 4//FwgULuX02mw1//dtf8e5774bfGe9HLleEzYRqNDXAP0Fapp4v+Pd6PSivLONqbAChn7eUZD2K 8osjEvwDQFqqgVuu4u5yh3X6IIQMjRIAhBBCIm7F8hVITg59ufV6vdixcwdysvIgk8oAhAL46trK i2qD1W5rw5nyUzA21nMBDRBqB5dhyMSRw0dx7Ngxbj8F/9MXj8dDVkZ22J1Vm92GmrqqAUmAefP6 gtnPv/h8SiUBWtussPebPp2ZngmRcPj2fD6fD5tf34yXXn4JXZ4uAKH3avVNq/GrX/6KWq71I5fL ce+Ge/Gz+38GpVIJIPQ7bu++vXjij0+gprZm0OelJOu59pXBYBDGfp0ZomWwaf+9wT/LsjBbmlFa cQZut4t7jkKuQFF+MVJ0+oj+npWIJUhO0nHb584+IIQMjhIAhBBCIo7P52PNbWu47W+//RYmkwm5 2flcr/FgMIiqmrNhQfxw7B02nCk/jTpjLXw+H7dfLBIjPS0DxQUzsXPXLhz6/hD3GAX/BAhVXk9K 1HLbHZ0dqKo5ywUPfD4f9997/5RMAni9HjSaGrjtpEQt1DHD92A3Go3445/+iG+/+5bbl5CQgN/8 +jdYtXIV/TwNYd68eXhs42OYM3sOt8/aasUz//UMtn24bUAhPT6fjwxDJrfd0WmHzd6OaBku+O/y dKH8bClMzU1hybPUlDQU5BWNWzvVZK0OEnGoYGIgEIBzDDoxETLVUQKAEELIuCguLsbMGTMBhO6G vbflPYjFYuRm50EgEAAAuru7UVVTiWAwOOR5Oh0dKKs4g5q6anj7zRgQCkVI0xswo2gWEuITsfmN zTh0iIJ/MjhDajp02hRu2+F0oLK6gvvs9SYBiooKuWMmexIg1PKvhkt0yGTyYbtwsCyL3Z/vxtPP PA2rtW969aKFi/DoI48iMzNzyOeSEJVKhQd+/gDW/2Q9V2Cx93198k9PoqGhIex4pVIVlpxqaDJG peL+UMG/Wq2G0+lARWUZNxMEAJQKJYoKZiBZqxvX37F8Ph9pqX2f4S53V9i4CCEDUQKAEELIuLn9 ttu5YL+6uholR0ogk8mRk5XLfWn0eLpQXds3JbtX75fOqprKsC94AoEA+pRUzCqeDW1SMng8Hja9 vomCf3Je+pRU6FNSuW2X24Wz1RVc0MPn8/HD226ZMjMBmpobw6bvZ2VkD/kz0dHZgb/+7a/Y9uE2 LikilUqxYf0GbFi/Ydiq9mSgxYsXY+OjG1FUWMTtazY346mnn8Kuj3eFTV1PTUmFuN9d7Yam8V0K cL7gv6q2Mmy2jCE1HQV5RdzyhfEWq9YgJkbNbTc01kdlHIRMFpQAIIQQMm60Wi1WrFjBbW/dthV+ vx8qZQwy+xUhc7ocqDOGKpT3BmVnqyvg6rfOlM8XQJecglnFc6DTpoDP54NlWWx+ne78k5HTaVO4 doAA0NXlxtmqcvj9oUKSU2U5QKejExZrC7edpjdwNTjOdfLkSTz+xONhhTOzsrLw6COPYtHCqdsW MdI0Gg1+8e+/wN133g2JJBTgMwyDnbt24qmnn0KzOdSSkc8XIMOQwT3PZm9HZ09XgUi7kOBfLBKj qGBG2IyFaDGkpnO/411uF9ptbVEeESETFyUACCGEjKsbVt3AdQCw2+3Y/fluAECcJj5sOrLN3o7S 8tOoqCyD0+ng9vP5fGiTkjGreDb0ulRuRkFv8P/doe+4Yyn4JyORlKgNW3vt8Xpwtqqcq9jOJQHm Ts4kQCDgR72xr+VfrFqDxISkQY4L4u133sYLL74At9sNIFQ4cdXKVfiPh/4DCQkJ4zbmqWzZsmXY +PuNyM3J5fY1NDTgyT89ic+/+BwsyyJGpUZCXN/7bWysH3Zp1Fi40OA/P7cganf9zyWVSMO6KDQ1 N0b8/SJksqIEACGEkHElk8mwevVqbnv357tht4cqkmuTkqFN7PsS178rAI/HQ2JCEmYWzUKa3gCh UMg9RsE/Ga2E+MSwKfFenxcVVWVhywHuv29yJgHqjHXw97TGFInEYcmOXk2mJmz/cDu+/uZrbp9G o8FDv3wIq29azbVbI2MjISEBD/3qIdx+6+0QiUIdGAKBALZu24pn/usZWFutSEs1cI91+7vR1By5 HvcXE/xLJkjw3yslWc8lhP1+P5pbTFEeESETE/02J4QQMu6WLlmKtNQ0AKHCf/2DqLRUAzSxcdw2 j8dDQlwCZhTNQnpaBkQ9XQN6DRb8L12ylIJ/csHiNPHIzszhPjfd3d1ob2/nElGTMQlgbbWg09E3 fTwrPSsseQYAe77ag6f+/BTs9r7j5s2bF7pLnZsLEhk8Hg9XXXUVHnn4EWSkZ3D7a2pr8MQfn8D+ AweQpu9bntLaZoXT5RjkTKMzFYJ/IPTzqYpRcdvWVstFtZYlZKqjBAAhhJBxx+PxsGZNX1vAkiMl qKnp642dlZENlTIGcZo4FBfOREZ6FtfqqT+WZfH6G68PCP5/dPePKPgnFyVWrUFudj53x5sJMjhb VY6urtCU+MmUBPB4usJa/iVrdVCpYrhtp9OJZ597Fu9veZ8L/iQSCX78ox/jZ/f/DHL5+LRym+50 yTr89je/xeqbVod1RHnn3Xew+fXNEPD7Ejb1DXVj2ut+sOD/V7/81aQL/nvJZDKolKHPOMuy415A kZDJICoJALFIPOAOzmSiVCiiPQRCCJn08nLzwgqrvff+e1zlfx6Ph/zcAmRl5Ay5xrQ3+O/fm5yC fzIWYlQxyMvO54KxQCAQVoSyNwkwd+5c7jkTLQnAMAxq62u4nymFXAG9rq/jQWlpKR7/4+M4U3qG 25eQEI9HfvcIlixZMu7jne74fD5WrVyF3/3n76AZQOKuAAAgAElEQVTX67n9FRUV2LRpM6qrQglS n88Hk7lpTK45VPAfq46dlMF/L0NaX0FAp9MBm90W5RERMrFEJQHA4/GQnpYO6RDVZyeyGFUMdMn6 8x9ICCHkvG774W3cGldjgzEsmB8OBf8k0pRKFfJzCriZAMFgEJXVFVxBSj6fj5/e99MJmwRoNDX0 W7ogQGZPfYNAIID3t7yPZ59/Fg5H6LXweDxcc/U1WH3zTdBqo1/RfTpLS03Dw//5MK679jrus+f1 erF37z7s/uwLdHV5YLG2wN2vI8rFmKrBPwDIpLKwzgRNpoYxnTVByGQnPP8hkRGr1iBWrYnW5Qkh hEwA8fHxuPqqq/HJp58AALbv2I5L5l0ybI9xCv7JeJHLFYhPiEeHvQN+vx8Mw6CqthLZmTlQx8Ry SYB/vvxPHD9+HEAoCQAAt/7w1qiNu6PTjtY2K7dtSEuHVCKFucWMl195GU1NfXeQ1Wo11t+zHoUF hThy/HA0hkvOIRQKccvNt2D27NnYtHkTLBYLgFCngA+2bMXSpZdCJpWhqGDGRf3OOzf4T0pKmjLB f6+UZD1stnb4A350+7thbmmGPiX1/E8kZBqgGgCEEEKi6rprr0OsOhYA4HA4uGTAYAYL/pcsWULB P4kYoVCI/NxCiMWhpYsMw6C6tgr2jtC04t4WgefOBNj24baojNfv70Z9Qx23HaeJQ0JcAr7Z/w2e /NOTYcH/rFmzsPH3G1FYUBiNoZLzyMrMwqOPPIorVlzB/X7z+XzYs2cvdu36GLV1Nec5w0CDBf8P /eqhKRX8A4BAIECqPo3bbrGa4fV5ozgiQiYOSgAQQgiJKolEgltuuYXb3vPVHlhbrQOOGyr4X3f3 Ogr+SURJJVIU5BZxgRDLsqitr0G7rQ1AKNi4/977MXdOXxJg9+e7o5IEqDPWcsGdWCxBQlwS/v7i 3/HW22/B7+9tBSjCnXfciQcfeBBKpXLcx0hGTiQSYe2atfjl//kl4uPiuf21tXV49rnnUHJk5LM2 pkvw3ys+LgFKRejzzbIsGqkgICEAKAFACCFkAli0cBEyM0O9yQOBAD744IOwxyn4J9EmFotRkFsI WU/9IpZlUWeshbVnqr1AIAgVBoxiEqDFYobD2beunwkwePKpJ3Hi5AnuGL1ej4d/9zCWX7583MZF Ri8/Lx8bH92IpUuWcvs8Hg9efuUVbNq8CR7P8O3uBg3+fzl1g/9ehrQM7t+djk50dNqjNxhCJghK ABBCCIk6Ho+Htbev5YL5k6dOory8HAAF/2TiEIlEyM8thFze1w2oobEeLVYzgOgmAdxdbq46PMMw KDtTjhf/8Q90dHRwx6xYvgK/++3vkKJLifh4yP9n777Do6zSPo5/Z5JMeiMJKaSRQugJvSMSioCu IkVBxY59X9fV1bWuurrq7qprL4uKBRURELBiA+liQQgkJBCSkN57nZn3j+hgliAgmQyQ3+e69roy z2n3M1mHnHvOc07Hc3Nz45KLL+Gahdfg6XnoiMbNWzZz/4P3syd1T7vt0tL2tj/59zu9J/8AHu4e BAV2t73OOagNAUWUABARkZNCz549GTF8hO31L2eTHzb5H63JvziOs7MzCXG9bUuLAQ7m5pCXnws4 JglgsZjJ/PnIv6qqKtas/ogNGzfajgD09vLmxhtu5MILLrSduiGnrsGDBnPTjTcRFxdru1ZeXs5/ nvoPb7/zNo2NjbbrO37awbKl7x+a/Ad1ncn/L8LDwnF2bt33vLGp0ZawE+mqlAAQEZGTxsyZM3F1 dQUgLz+Ph/7x0OGT/4s1+RfHcnJyoldcb3y8fWzX8gpyycnNtpV3ZhIg+2A2DY0N7N2bzvL3V9p2 jQfo26cv995zLwP6D7DL2OIY0VE9mXH2DCZNTradmmK1Wvl63dc8+NCDZGRk2Jb9m81m4OfJ/y1d a/IP4OTkTI+wX20IWJhPU1Pjb7QQOb0pASAiIicNP18/pp01zfY6Ly/P9rMm/3IyMRqNxMX0wvfn EywACosKyMo5ABxKAiQlJtnK7ZEEKCsvIy8/ly+++Ip1X6+3bfTn7OzM7Fmz+eNNf8THx+covcip xmAw0DOyJzExPZk953yio6NsZcXFxfzr8X/x4ksvtv3mvwtO/n8RFBCE58+P7lgsFluyTqQrcrZX x05OzlgsFp0pK3KScDI6OToEkWMyKXkS32z4htLSUts1Tf7lZGQ0GonrGU9m1j7KyluPBSwuKcJi MRMdGYOTkxMLr17ISy+/ZNuI79PPPgXg/Jnnn/D4TU2NbP12M5+v/ZKamhrb9eDgYK6+8moiIiJ+ o7Wc6tzdPQgJDiW/II/JUyaxL2Mfmzdvpb6+HqvVavvmv1u3bl168v+LyIho9qSlAFBeUU5VdSU+ 3r4Ojkqk89ktAeDs3JoAGDpouL2GEJFj1NjUSMqenY4OQ+SYuLi4MHvWbF586UVAk385uRkMBnpG xWI0GCn5+VjA0rJSLBYLMdFxdksCmM1m3npnCVu3bLU96w8wdsxYLph7ASaT6QTuSk4VYSE9qKgo p76hnti4WOLj4/lm/UZ279kNtH7zf9El87r85B/A08OTwIAgSkqLAcjOyaJfnwH6t0W6HLslAERE RH6vwYMG0yu+F4GBgZr8y0nPYDAQHRWD0cmJouLW5+/LK8rJ2L+X2J7xHZ4EKC0r5fkXnicnJ8d2 zd3dnQWXLGDwoMEdcEdyqjAYDERH9mTP3tYJv8Vq4eKLLyIlZTfrv1nPDdfdQF1TTZef/P8iPCyC 8ooyzGYzDY0NZGVn4u7ucfSGJ6Ha2loKiwocHUan6Er32hmUABARkZPSlVdeia+Pryb/csqIDI/C aDRSUNi6y3hlVSXp+/YSH3soCfDiyy+yY8cO4PclAb7d/i1vvvUmDQ0NtmvR0dFcu/Ba/P39O/Bu 5FTh6elFcPcQ2wQp52A2o0eNYvy48QBUZJeRXqDJP7SuUO4RGk72wSwA26qdU1VVZZWjQ+g0Xele 7U2bAIqIyEnJz9dPk3855YSHRdAjNNz2urqmirSMNMxmM05OTlxz9TUkJibayo91Y8DGxkZeW/wa /130X9vk32g0MmbMaO74yx2a/HdxPULDbSeotJhbyMppneBWV1dRVlamyf+vBAV2x+MU/dZfpCNo BYCIiIhIBwoNCcNoNNp2Gq+trSEtfQ+94nrj7OzMNVdfc1wrAQ5kHWDRokUUFRfZrnn7eDN58iTO PGOiEmWC0WgkOqInaRmpAJRXlHEwL4ei4kLbHhGa/Lf65bGJ0rLSo1c+idXUVePl4e3oMDpFV7rX wmL7P+rgsARARWU5B/MO0tBQ76gQfhcfbx8iwqNwd3N3dCgiIiJykgruHoLRyYms7EwA6urrSE3f Q0JcAi4upmNKAlitVj797FNWrV5l29EdIC4+jrFjR9MnoS8mF232J628vX3abHL3y6MooMn///Lw 8MTj52MBT1W5hTn0CO4aJ310pXvtjASAQx4BsFqtZOVknXKTf4Cq6iryC3IdHYaIiIic5IICgoiJ jrV9Q9/QUE9q+h4amxqP+jhARUUFTzz5BCtWrrBN/k0mE2dOnMCZZ55BWGgP/Hy17F/aiugRicv/ JIWcnJw0+RcRG4esAGhqbqK5uckRQ3eImtraY677/vvLWbp0KRkZGQQEBDJx4plce+01+Pn52TFC ERERORl08w/AaDSyLzMDq9VKY2MjaXv30Cu+N26ubq0rAV56kR0/HVoJUFZexu7du6n91d8boaEh nDFhPN7e3ri5uRPRI9JRtyQnMScnJ6IiosjYnw60fvPv6++ryb+I2GgTQDu66657ePDBv3P55Zez evUqHn7475SUlLB8+QpHhyYiIiKdxM/Xn/jYXhiNrX92NTU3kbZ3D/X1da0rARZeQ+LAQysBvv32 W9vk32AwMG7cOKbPmIa3tzcGg4GY6FhbXyL/y8/Xn27+3WzL/p2dteWXiByiTwQ7SUlJYdGiRXzz zTri4+MBCAsLY+jQoTQ1ta5+MJvN/Pe/i9i6dRv+/n5cccXl9OvXz9bH+vXf8N57y2hoqGfy5MnM nTsHgJKSUp599lnOP/98Xn31VVpazFx99VUMGNDf1vann3by1ltvUVxcwrhxY7n00gUYjUZb27lz 5/DKK68SEhLCn/98Sye+MyIiIl2Pj7cvvWITSN+3F7PFTHNLM2npqcTHJeDp4cnCqxfy0ssv2VYC APj7+zPvwgtpNh9aNRkeFqEdzOWoIsKjsJjN+uZfRA6j9LGdrFu3jsTERNvk/9dMptZns2699TZe e20xM2ZMx83NnWnTZpCe3rpk6+uvv2bBgkuJiAhn/PhxPPDAgzz11NMAVFZW8txzz/OXv9zOmDFj 8PLyZMaMs8nMPADAhg0bufDCeURFRTJr1vm888673HPPvYe1HThwAOPHj+uEd0NERES8vLzpFd8b Z6fW719azC3sTU+lpqYaZ2dnFl690LYSYPDgwdz51zsx/OovNV8fX4K7hzgidDnFuDi7aPIvIu3S CgA7ycrKJiws7IjlFRUVvPnmW6xZs4oRI0Ywa9b5pKen89xzz/PEE4/zzDPPMW/ehfzlL7cB4OTk zF133c0f/3gTAM3Nzdx5518ZN24ss2adT2pqGq+88goPPvgAjz32GNdeew3XX389AJGRkUyePIUH Hrjf1vbuu+9ixIgRdn4XRERE5Nc8PTxJiO/N3ow0mluaMVvM7N2XRlxMPD7eviy8eiEpKSkkJiay LzOdpp/3THJ2diE6KsbB0YuIyKlOCQA7CQoKIi0t7YjlmZmZuLi4MGTIENu1sWPH8NVXXwGQkZHB ZZdd2qassrKS8vJyoPW81+HDh7Up//777wFITU0lKyubN998y1ZuMBgoKmo9P9jV1dRmXBEREek8 7u4eJPTqw970VJqam7BYLKTv20tszzj8fP1JTEykuKSI8opyW5ueUTG4OLs4MGoRETkd6BEAO0lM TOTHH3dQUVHRbrmfnz8NDQ22STlAVlYWPj6+APj7+5GTk/2rsmycnJzw8vICwGKxkJubZyvPzs62 nSzg6+vLX/96B9u2bbH9Lz8/l9DQUABcXEzaEEZERMSB3FzdSOjVB1dXV6D1iOR9mRmUlZfS0FBP Tu6hvwGCg0Lw/fnvAxERkROhBICdTJ48ib59+3LDDTfakgBWq5WPP/6ENWs+JCoqkl69erF48euY zWYyMw/w2WdrmTJlEgBTpkxhxYoPKCkppb6+njfeeIMzzjgDF5dD2f8lS5ZgsVjIycnh88+/YOLE iQDMmDGDl19+mcLCQlvdjIyMTrx7ERERORpXkyu94/vg5uYOtP6dsP/APvbuS8NisQDg4e5BeI8I R4YpIiKnESUA7GjJkjfx9fVlwIBEhgwZSlRUT5566mliY2MwGo288MLzLF36HgMHJjF69GimTp3C vHnzAPjjH28iOLg7SUmDSEoaREZGBo8//i9b315eXtTXNzB8+AiGDx9BcnIy5513LgB33HE7AwYM IDFxEGecMYGBA5O49977HPIeiIiIyJG5uJjoHd+nzc7+v5wWZDQaiYmOxWAwOCo8ERE5zWgduB35 +fnx3HPP8swzT5Ofn4+fnx+enp628qSkRH744TsKCwvx8fHB3d3dVubp6ckbb7xOdXU1DQ2NBAUF Htb/Qw89yJ133kFLSwu+voeWBrq5ufHkk0/w2GOPUlBQQGBgIB4erX9YxMbGkJ19wH43LSIiIsfF 2dmZhPg+7N2XRm1tje16RI9I2+oAERGRjqAEQCcwGo306NHjiOXBwcFHLPP29sbb2/uI5b9OKPwv k8lEZGTksQUpIiIiDuPk5ERCXALp+9KprqnCz9efoMDujg5LREROM3oE4BTUrZs/f/rTzY4OQ0RE RDqQ0ehEfGwvAgOCiI7s6ehwRETkNKQVAKcgf38lAERERE5HRqNRk38REbEbrQAQERERERER6QKU ADjFWK1Wamqqba/r6moxm83H3U9LSwt1dXW21zU11VitVgAaGhpobm4+8WClXWV1edQ1Vzk6DBER ERER6WL0CICd1NfX8/mXa/H378bY0WNt11taWvj0s09wdXNj0sRJx91vbW0N7773LldefhUAH33y MSNHjCS8R/hx9XMw9yA/7viB8/4wE4B3lr7DvAvm4+npyfpv1hEeHkHfPn2POz45XHZFCltzVnKg YidZ5TupaSoHIMgziii//kT7D2RCzMV4mfwdHKmIiIiIiJzOlACwE7PFTH5+PpWVlSQlJuHl6QVA 5oFMysrLcDI6dcg4Z4w/Ax9vnxPuZ8b0s3Fzc+uAiOQXZmsLq/f8h9WpT2G2HL6iorg2i+LaLLbn fshnGf/l8sH/ZFDYFAdEKiIiIiIiXYESAHYWFxtHevpeBiUNBmBvehpxsfFkZu5vUy9jXwY5OTkY jUYSevUiJCTUVnbwYA57M9Jxd3MnpmdMm3Z5eXm4RLng6upKVXUVaWlpVFSUYzK5Eh8XR1jYkY8f /LWsrAMEdAvAyak1MWG1Wvhxx4+UlJYQEhxCv779MBgMtLS0sG37Nvr16UfKnhScnZwYPmwEu/fs prCwELO5haCg7vTv19/W166UXQR3D+Zgbg4lJSUkJPSmrq6O3gm9beMXFORTVl5+2qw6yK/O4IWt N5BVsfOY6lc1FPOfTZcxJmoOlwx6GDfnIx/vKCIiIiIi8ntoDwA7S+jVm7S9ewGoqa2hoqKC8PC2 y/W3bN1MSsouoiIjCQwMYO0Xa8nPzwcgJyeHL776gm7+3fD19WXDpm/atE3PSKempgaAqqoqfH18 GDgg0dZPYVHhMcW5K2VXm+f+d+zYQX19PdFR0ezencLmrZsBMJvN7Nq1k/XfrMPXx5eIiEgsFgt1 dbUkJCQQFxdPzsEcNmw8FOf+zH18+fUXNDY2kpDQG29vbzZv2URTc5OtzvbvvzvWt/Sk12Su54mN C4558v9rG7Pe460f77FDVCIiIiIi0tVpBYCd+fn54ebmRkFBAQWF+cTHxWMwGGzlDQ0N7ErZxZxZ c/H19QVa9w9I2b2L0NBQdu76iT69+5CUmASAxWJh67Yt7Y71yz4ALS0tuLq5UlRURGZmJsHdg487 bn9/f0aNHAWAu5s7H3/6ESOGjbCVDxgwkOioaNvroUOGAVBdXc2A/gP46qsvOWP8BFt5SHAoI0eM sr0ODg4hIz2dvn37UVVVSXFxEVMmnx7L39/96e8U1Rz43e2/OfAOw8PPYUDImR0XlIiIiIiIdHlK AHSC3r0SSNubRmFhAVMmT6WmtsZWVlFRAcCnn31iu9ZibrHtGVBVVUX//gNsZSHBIUccp7y8nI2b NlBfX4+3tzc1tTUEBhiOWP+3hISEtPm59fSBGts+AWGhYW3qb9j4DXn5+bi5ueFqMtHU3ITZbLY9 BhAaGtqmft8+fdn+3Xb69u1HaloqsTGxmFxMvyvWk8meog18ue+1E+7n1e9u5aEpX+HucuL7O4iI iIiIiIASAJ0iJjaWLVu34Ofvh5+fX5sEgMnVhMFg4A/nnNvuJnwmV1dqqg8d+/frIwD/146ffiQy MpKBAxIBWLd+HRaL5XfFXF1zKMba2hqsViuurq62a87Oh/6vU1BYQF5eHnNmz8VgMFBUVERWdlab /n5dHyAqMopNmzaSX5BP2t40pk4563fFebJZnfoUVqwn3E9ZfT4bs95nUtzlHRCViIiIiIiI9gDo FCYXE2dNPavNkvhf+Pn64e3tw46ffsRsNgOty/yrf570R4SHsz9zP01NTbS0tJCxL+OI49TW1dq+ Ra+srORAVubvjvngwRxqfp7470lNJTAg8IinBNTV1eFicrFtErhr966j9m8wGOjduw9fr/sKD3cP ugd1/92xniysWDlQ/lOH9XegfEeH9SUiIiIiIqIVAJ3k17v6/5rRaGTKpMl8vf5rXn9zMb6+ftTV 1TE4aRB9+/YjMTGJtZ9/xpJ33sLV1ZW42LgjjpE0cBDfbFhPyu4UzGYzcbFxNDUdfvzcsYiMiGT1 mlUAWC3W3/yGPjoqmtTUPbz97hKsVit9+/Q7pjF6J/Tm+x++I3FU0u+K8WRTVJNFXXNVh/WXqQSA iIiIiIh0ICUA7MTL04uFV13Tbll4j3Dmz7vI9trPz5/z/jCTpqYmmpqb8HD3wGhsXZxhcjExY9rZ 1NfX4+rqitFoZNjQ4ba2s8+fbfu5R48eXHjBPOrr63F3d//N+KKjotts4nfVFVfbfp4yeart57q6 Wjw8Dh1J5+rqeth9GY1Gpk+bQWNjIy4uLhiNRtumhQB/OPvcdmNoaGjA2dmZuLgjJzVOJR357T+0 HiXYZK7H5PTbv0sREREREZFjoQTAScRkMmEytb8R3tEm9L+37tH8evJ/NL/eI+Bodu/ZTWraHoYO HnpabP4H0NhS26H9WaxmmswNSgCIiIiIiEiHUAJAHKKhoYEhg4YQGRnl6FA6TLT/wA7tL8AjHC+T f4f2KSIiIiIiXZcSAOIQgwcNdnQIHa6HbwIuTq40mxs7pL+e/okd0o+IiIiIiAjoFACRDuNkcCbS 99g2QDwWSgCIiIiIiEhHUgJApAONi76wQ/pxdfZgeMQ5HdKXiIiIiIgIKAEg0qEmxFxM/+AzTrif Of3vJMjz9NkfQUREREREHE8JAJEOdsXQx3F38fnd7XsHjSI57vIOjEhEREREREQJAJEO1809lGuG P43H70gChPv24aqhT2LAYIfIRERERESkK1MCQMQOkkIn89CUrxkYMvGY6hsNTpzd+yb+lvwJgZ4R 9g1ORERERES6JB0DKGIn/u4h3DL2TdZnvs2GrKVkV+yioaW2TR0/t2Ci/QdyTp//I7bb6Xc0ooiI iIiInDyUABCxs/E95zG+5zysVgv51RkcqNiJu7M3Pf0T8XMPdnR4IiIiIiLSRSgBINJJDAYjYT69 CPPp5ehQRERERESkC9IeACIiIiIiIiJdgFYAiMhJY/sP2xwdQqcIDQvVvZ6GdK+np650r6DP4dOR 7vX0pHuV30sJABE5aUyeOM3RIXSa/r2THB1Cp9G9np50r6eftV9+rM/h05Tu9fSkez39rP3yY7uP oUcARERERERERLoAJQBEREREREREugAlAERERERERES6ACUARERERERERLoAhyQATC4mXFxMjhi6 Q3h5ejo6BBEREREREZHj4pAEgMFgICoiCjc3d0cMf0J8vH0IDenh6DBEREREREREjovDjgH08/XH z9ffUcOLiIiIiIiIdCnaA0BERERERESkC1ACQERERERERKQLUAJAREREREREpAtQAkBERERERESk C1ACQEROCvGxCY4OQUSkS9PnsIiIY3XG57ASACJyUoiOinF0CCIiXZo+h0VEHKszPoeVABARERER ERHpApQAEBEREREREekClAAQERERERER6QKc7dVxS0sLBoOB7T9ss9cQInIcnJycHB3CEVVWVbFp 8xb2Z2YybswYBg7oD0B1dTUrV69hT2oavXvFM2XyJEKCg4/Yz/KVHxAYEMD4cWNt11at+ZCy8nIu u+Ri27Wv1q1nV0oKN11/nf1uSkSkE53o5+iGTZv47vsfcHJyInHgQEYMG4rJZGLLtm1s/XY71151 Ja6urgCk7NnD5198yUXzLiQwIKBT71NEpDNt3rKVPampeHp5csHs2bbrX61bz4ZNmzCZTEyaeCZD Bg06Yh97UtP4ZsMGFl51pe3a3vQMPv70U+bOmkVoaAgA+QUFLF32PmdNnUJCfLzd7sluCQCzuQUn JyeSBgy21xAicoyqa6pI35fu6DCOKPPAAVLT0kjZs4fAwADbH67ZOTm0tLQw/4I57NyVwjU33MTK 997FYDAc1kd1dTWLXluMyWRqkwD47Isv2JWymzPGjaVndDRWq5WXFi0iPWOfEgAicto40c/Rb7d/ R2VVFckTJrB+wwbeeGsJLz33DN//8CPLV35AfGws08+aCsCiVxfz3Q8/MG3qVCUAROS09vlXX+Hu 7s5X69e3SQB898MPTE6eiNls5sGHH+GBe++xfe7+r1cWL+annbsYNXIkA/r3AyDzQCbLP1iFk5MT N153LdD6RdbyD1aR0KuXXRMAegRARBwuaeBA/vTHm4iJ7tnmer++fbl8wSX079ePeRfMxc3NjfSM fe328fGnnzH9rKn0CAvlhx93tCmbNnUKq9Z8CMD2775nYP/+7SYRREROVR3xORodFcW4sWP46223 sjs1lYqKCgCmTZnMqg9bP0MrKiqoqKggvEcP+96QiMhJ4J6/3sGU5OTDrt968/8xcvhwxowaxezz Z7L2yy/bbV9ZVcXe9AxuvulGVq5a1aZs1IjhfLNxExaLBYvFwvoNGxk9coRd7uPXlAAQkVNCesY+ 6urqiIqMaLd85eo1nD19Gn84ewYrV69uUzZ29Cg2bt6CxWJhxarV/OGcszsjZBGRk8rRPkcBLBYL X69fT0C3bvj5+QEQHByMwWAgP7+ANR9/YlsJICLS1ZnNZtZ+8SUjhw9rt/yjjz9hyqRkJp45gY2b t9DY2Ggrc3FxYcigJLZs3cbWbd8yeFASLs4udo9ZCQAROekVFRdz+1138+B999ieQf21jH37MBgM REVGcsb48WzespW6unpbubOzM8OHDeXTtZ9zICuLfn36dGb4IiIOd7TPUYA3lrzNRZddzpat3/LQ /fe1KTtn+nRWffghn3z2GVMnT+qMkEVETmpWq5UH//EIA/r1ZcyoUe3WWbFqNTOmnYWrycTokSP5 /H9WCpx7ztmsXL2GlatXc+7ZMzojbPvtAXA0FZXlHMw7SEND/dErn0R8vH2ICI/C3c3d0aGIdAkl paXcePMt3H7rLSQlJrZbZ+Wq1ZSWljJ3futGf80tzXz2xRec96tv+s89+2wuX3gNV19xeafELSJy sjiWz1GAS+bPa7Nh6q8lnzmBc2fPJXHgAGJbDR0AACAASURBVLy9ve0VqojIKePRfz+Oi4sLt/7p 5nbLU9PSOHjwIH/5610ANDY1kX0whxnTptnqJPTqRV5+HlYr9E5I6JS4HZIAsFqtZOVk0dzc5Ijh T0hVdRX5BbnERMfZdZzS0lJMJtMR/5HNzs4mPDwco1GLOOTUZzabKa+ooKGhgdqaWkpKSwkMCKC0 rIyF19/I5QsuITYmhpLSUny8vTGZTLa2LS0tfLJ2Le+/8za+Pj4A/PjTTzz59DNtEgDxcbE8cO/d DB08pNPvT0TE3k7kc/RYuLq68vf77yO4e3c73YGIyMmnorKSyspKWlpaKCktxdvbG1eTiUf//TiF hUXcdfttlJSW4trOvG3lqtX83003tNk88JzzZ5Gbl9em3t133N4p9/ILhyQAmpqbTsnJ/y9qamuP Wqe4uIQrrrgCAIPBQEJCAhMmTGD69GnHtPnYX/96J0lJiVx//fWHlTU2NpGUNJjMzH34+voe/w2I nGTyCwq474G/A5Cbl8fGzVv47wvPsX9/Jv5+fqxctZqVq1qf67/xumsZlHToG6y09HSmTppkm/wD JA4YgL+fHxWVlcTHxeHj3Vo2ccIEW50hg5I64c5ERDrHiXyOAkSEh+Pl5XVYv2FhYbj9/MjA0MGH Tnbq0zsBDw+thhSR09uTTz9DTs5BPNw9uP3Ou7nysksZPWok+/dn0tLSwu133QPA4MGDuOGahW3a FhWXcM2vjv4DuGjePHbuSiGgWzd6RkUBbb/5j46Oopu/v13vyfDIYw9Zb7/tzg7vuLy8lB93ft/u MYCNTY3sTNnRTqtTg8nkysB+R15CB5Cbm8uAAYm8//57eHl5sXv3Hh5++B/85z9PctYxbJ6zcOE1 v5kACA0NUwJAjtkvxwAmT5ji6FBERERERMQBHv3nw9oE0N6SkpIYNmwYl166gJkzz2PlypW2srq6 Oh599DEuvfRy7rrrHvLz89u0tVisPPvsc1xxxVU8++xztLS0tCk/cCCLO+74K9dffwMbN24CoLGx kbvuuoeqqipbvebmZu6++17Ky8vteKciIiIiIiJyMlMCwM4OHswlKyuLjz/+hM8++4wpUw59Aztn zly++eYb5syZTXFxEWeemdxm4v788y+QnZ3N2WfP4K23lnDrrbe16fuWW/5M//79iIyMZP78i9iy ZQuurq6kpqaydOl7tnofffQR69atw9/Oy0lERERERETk5OWwUwC6imuvvQ4nJyMHDmQxfvw4Jk48 E4Dt27ezdes29u5NpVu3bpx99gwSEwexZMnbXHvtNQD07BnNo48+AkB4eA9mzDiHBx64H1dXNwCu uupK5s27EIC8vHxefPFlRo4cyRVXXM4//vEIV/38zMnixW9w6aULOvfGRURERERE5KSiFQB2tmbN KtavX8e+felERERw+eWtGwOmp2fQp08funXrZqs7duwYDhw4YHs9cuRI289DhgzBxcWFrKzsdsvH jh1DZuZ+AM46ayoVFRVs27aNrKwsvv32W+bOnWOvWxQREREREZFTgBIAncTFxYWpU6ewZctWzGYz /v7+5ObmYrFYbHWysrLabOqXk5Nj+7moqIiGhgb8/Novz87Oxs/PDwAnJycWLLiE115bzBtvvMl5 552Lz692SBcREREREZGuRwmATpKdnc3ixW8wdepUnJycGDFiOBaLhWXL3gfghx9+YPv275g0KdnW Zt269WRlZWGxWHj99Tfo1asX4eHhtvKlS5fS2NhESUkpq1evZuLEibaySy9dwJo1H/LGG29q+b+I iIiIiIhoDwB769u3HwChoaGceeaZXH/9dQD4+/vzwgvPc8MNN/LQQw9TVFTIPffczbBhw2xtp06d ysyZ5wNQX9/AW2+9gcFgsJWHhYUxcuQoSkpKGD9+HAsXXm0rCw4O5swzJ7B/fyZDhw7tjFsVERER ERGRk5gSAHbSo0cPyspKfrPOWWdNJT09jYKCAoKCgnBxcbGVvfTSi7afCwoK6N69O0Zj64INV1eT re8///kWampqCAgIOKz/oqJirrzyio64HRERERERETnFKQHgYEajkbCwsN+sExIScsQyV1dXXF1d 21zbunUry5evoLq6mosvvqhD4hQREREREZFTm/YAOA3V1NQQFxfH8uXLcHZWjkdERERERES0AuC0 lJycTHLy0euJiIiIiIhI16EVACIiIiIiIiJdgBIAJwmLxUJtba2jwxAREREREZHTlB4BsJP6+no+ /3ItAE5OzoSFhhEdFY2fn1+79cvKSvl07adcNO/io/ZdW1tLfkE+cbFxHRqz2EdjSx17S7ZyoGIn WeU7yarYhbuLF1F+A4jyG0C0/0DiAoY4OkwRERERETnNKQFgJ2aLmfz8fKZPm4HRaCQzcz8rP1jB xRdd0u7GfL6+fkxKnnxMfVdWVfLTTzuUADgFpBZv4r/b/0RJbc5hZdkVKXzDOwD0Cx7PlUMep5vH b58IISIiIiIi8nspAWBnQYFBuLq6EhYaRlZ2NpkHMukR1oPde1LoGR3DntTdeHv7EBcbx8GDOQR3 DwZg+3ffEhsbR3r6XhoaGujVK4GQ4BBaWlpI2Z1CTW0Nm7ZsAmDUiFEYDIY241ZUVrB//35KSopx c3enb+8+BAYGAVBWVkZq2h7q6usJDQmhb59+GAwGmpqa+P6H7+id0IeU3Sm4ubni7OxCjx49CAwI tPW9a9dOugcH0z2oO2azmZTdKRQVF+Hh7k7//gPw8fYBIG1vKr4+vhQVF1NUVMjoUaPx8PDsjLfd 4ZrM9Szd+RBfZLyKFetR66cUrueutWcyP/EBxkVf0AkRioiIiIhIV6M9ADqJ2WymqakRD3cPGhoa +GnnT2zaspGgoCBCQ0Kpr68jNS3VVj81NZWvv/4KNzd3AgOD+PSzT6iorMBoNBIUGITJZCIiPIKI 8IjDJv8Aaz//DGdnJxIHJhEe1oPGpiYACosK+ejjD/H09CI+No59+/ezcdMGAJqam9i5aycbNn5D t27+9OgRTlNTIzt37rT129DQwNZvt+Ll6QXAhx+toai4iF7xvTCZTKz8YAX19fUAZGVl8dW6r6iu rqJXrwScnV3s9v6ebP6z6XI+z3jlmCb/v6hvrmbR9j/xyd4X7RiZiIiIiIh0VVoBYGc/7vgRgwFy cnLw8vKiR48elJWV0dLSwqgRo2zfypeUFB/WNjY2joEDBgJQWFhATk42A/oPpHv37uzfv4+I8Ih2 x2xqbqK6upqI8Ej8/f2BYFvZ999/R9++fUkcmAi0Pnrw3vtLGTVyNABWq5VBgwbTI6wHAO5u7ry/ Yhljm8fi4uJCxr4MeoT1wMPDg5ycHKqqqzh7xjkYjUYiIyIpKioiPSPdFne3bgGMGT22Y97MU8RX +18npXD9726/POVRkkInE+Id04FRiYiIiIhIV6cEQCcwmVw5Y/wZtsk+gIuLS5vX7QkLDbX97OPj S0VF5bGN52JixPARrPloNZ6envTu1fvnb+CdKS8vp7yinIyMDFt9JycnamtrMToZMRgMhASH2Mp8 fX0JDAhkf+Y+Enr1Zm96GkmJgwAoryijubmZZe+/Z6vf2NSIv3832+vQkEP30BUU12bx7k8PnlAf TeYG/rv9Zu6asBKDQYt0RERERESkYygBYGdJiUm4uroedt3JyemobX9r8ne0peX9+vand0If8vLy +P7H76iqrmLkiFGYXE3069ufPr37HNamprYGg8FwWGy9eiWQtncv3YOCqa6uJioyCmhNbHh5eTFn 1twjxtHehoens2W7/kFDy4kf55hRup2tB1cxMuK8DohKREREREREewCckrr5d6OmpoaWlpZ2y5ua mqivr8fJyYmIiAh6RsdQX98AQHRUT1J2p1Bbe2iSWlFZ8ZvjxcTEUFJSzPbvviUuNs6WIAjvEU51 dTX79mVgtVptY9fV13XEbZ6S0ku+7bC+Mko7ri8REREREZGu9fXsacLNzY3eCb1ZtnwZzc1NXDTv YozGQ7mc+vp6Vn+4Cg8PD8CAk5ORM8ZPAGBQ0iAaGht4+90l+Pv709jYhJ+vL9OnzTjieCYXEz2j e5Kekc7M8863Xffy8mLypCl8s2E9m7duxs3NnaamRiZOmIiHu4e9bv+kVdVQTFl9fof1l1n2U4f1 JSIiIiIiYnjksYest992Z4d3XF5eyo87vydpwODDyhqbGtmZsqPDx+wsJpMrA/slOjqMo6qtrcXF xQWTyXRYmcVioba2Fjc3N1xcTnx3/vr6eqxYu+TE/xc/FXzB4xsu6bD+TE5uPH/eXpwMJ56nq66p In1fOskTpnRAZCIiIiIicqp59J8PawXA6czT0/OIZUajEW9v7w4by93dvcP6OlUV1+Z0aH9N5gaq G0rxcw8+emUREREREZGj0B4AIh0kwvfwjRVPhJfJX5N/ERERERHpMEoAiHSQSL/+HXpsX7T/wA7r S0RERERERAkAkQ7i5uxJqHdch/XX0//k32dCREREREROHUoAiHSgpNBJHdKPAQMDQ5M7pC8RERER ERFQAkCkQ53X988Ee/U84X4mxFxMfMCwDohIRERERESklRIAIh3I5OTO1cP+c0J7AQR4hHPBwHs6 MCoRERERERElAEQ6XFzAUM7t86ff1dbk5M5Vw57Azdmrg6MSEREREZGuTgkAETs4r++fuWXsG8d1 jF98wDAenPwFfYLG2DEyERERERHpqpQAELGTgSHJPDT5a0ZHzsLJ6HLEeu4uPlw48F7unLCCYK/o zgtQRERERES6FGdHByByOvM0+bJw+NNcMfTfZFekkFm+g6yKnbg7e9PTP5Fo/0SCvXtiwODoUEVE RERE5DSnBIBIJ3A2mojpNoiYboMcHYqIiIiIiHRRegRAREREREREpAtQAkBERERERESkC1ACQERE RERERKQLUAJAREREREREpAtQAkBERERERESkC1ACQERERERERKQLUAJAREREREREpAtwSALA5GLC xcXkiKE7hJenp6NDEBERERERETkuDkkAGAwGoiKicHNzd8TwJ8TH24fQkB6ODkNERERERETkuDg7 amA/X3/8fP0dNbyIiIiIiIhIl6I9AERERERERES6ACUARERERERERLoAJQBEREREREREugAlAERE RERERES6ACUARERERERERLoAJQBEREREREREugAlAERERERERES6ACUARERERERERLoAJQBERERE REREugAlAERERERERES6AGd7dezk5IzFYmH7D9vsNYSIHAcno5OjQxAREREREQeyWwLA2bk1ATB0 0HB7DSEix6ixqZGUPTsdHYaIiIiIiDiQHgEQERERERER6QKUABARERERERHpAuz2CICIiIi91NXX sWXbRiwWs6NDERGRU5SnpxcmF5OjwxCxCQ0JIyw03K5jKAEgIiKnnPKKUhoa6sk6kO3oUERE5BTk 5+eLd4w3Ad0CHR2KCAC1tTUUFhcoASAiItIei8VCVVWVo8MQEZFTkJubGyaTK74+vo4ORQQAq9VC U1WT3cfRHgAiIiIiIiIiXYASACLSZTQ0NFBSWuroMLqM5uZmCouKHB2GiIiIiPzMYY8AVFSWczDv IA0N9Y4K4Xfx8fYhIjwKdzd3R4cictrIOXiQvz340BHL77rjL8T07HnY9Yx9+8gvKGTcmNHHNM7G zZt5/c0lLF708nHFt2zFSqZOSsbb2/u42nV1+zMzuf6PN/PFJx8B8MGaNYwbPYZu3fwdHJmIiIhI 1+SQBIDVaiUrJ4vmZvs/49DRqqqryC/IJSY6ztGh0NDQQGVlJcHBwY4OReSEBAYEcN3Cq4HW/8b+ dNvtPP7YI7bn8oK7d2+33U87d7Fpy5ZjTgD8Xs88/wLDhw5RAuA4RYRH8Pg/H7W9fnnRq8THxikB ICIiIuIgDkkANDU3nZKT/1/U1NYetU5xcQlXXHEFb7+9BC8vL7vE8eWXX/HEE0+ydu2ndulfpLO4 u7szdMhgAMrKygFIHDCAwMBArFYr769Yybbt3+Hl6cn55/2B/v36UVBYyCefrSW/oIDH/v04Pj4+ XHv1VexKSeGjTz4lv6CAoMAgLpw7h5ie0ccUx2eff87mrduorq4mpmdPrlt4Ne+9v5y6ujpefuVV vL28mDF9Gv369OFgbi5vvv0OJSUl9Ondm0vmz8Nkaj1K6LkXX2LKpGQ+/ORTCgsKefjB+/n8y6/Y sGkTlZVVxMb0ZMFFF+HjcyihsHzlB2zeuo2I8B5MTk5m05YtXHnZpUDrZnfLlq/gux9+wNvbmwvn zCYuNvY372PDxs3U1NbSv19frrh0AWazmfeWr+Cnn3bS3NJM4sCBXDhnNs7Orf8MLH7zLYYOGczX 69aTlZ3DuDGjOWfGdKB1Kf+y5SvYuSuF5pYWBiclMnf2LJycnGxjrvnoY7Zs20Z9fT1DBg1i/oUX UFNbw4ZNm0kcMICVq9dQXlHB62+9RTd/fyYnJ7Nj505GjRhOQq9etn7eeGsJQwYPpm+f3sf0OxMR ERGRY6c9AOykqamRjRs30dzc7OhQRE5pTz79DIvffJNxY0bj6+fLwutvJGXPHry9vIiLjSEgoBtj Ro+yJRAyD2QxeNAgLpk/n27d/Flw5VW2pMJv2bh5M/995TXOHD+eeRfMxc3VFavVSr9+fXFxcWHI oEGMGT2K7oFBlJeXM3/BZdTX13PWlCl8tW4dt/zldltf76/8gDvvuQ93V1emT5sKQFZ2NlOSk5k7 exa5eXnccPPNWK1WAJa88y7/ffU1Ro8cgZ+vL3feex+rP/zI1t+fb7+D9Rs2MG3qVEKCg7li4bVk 5+S0ex/PvvgS/37yKWJjY7h43oW2MRoaG6msrOT8887lrCmT+fzLL/nXE0/a2n30yafcfufdODs7 M2L4UJ569jneWPI2APX1DdTU1nL+zHOZOnkSH3/6GU88/Yyt7WP/fpznX3qZvr17M//CC2huaQGg vLyc5StWAtAnoRdubm4kDhzImNGjCAkJpqGhgVdff8PWT0FhIc+88CJhYaFH/X2JiIiIyPHTMYCd aOnS9/j8889xdXVlzpw5jB8/zlZ28OBBXnrpZTIzMwkKCuK6664lPj4egLVrP2f16jU0Nzdxzjnn MH36tMP6fvfdpQQGBpCcnGy7tmzZ+3h7ezN16hQefvgfzJ49i7fffqd18jJ/HsOHD7f/TYucgObm Zpa8u5Qn/vkoY0e3LvPPy8tj8Rtv8djDfycuNpai4mLGjBpla/PLt9ZVVdX4+/vx9fr1bNm2jeln Tf3NsXbvSSUqKpIRI4bjajIxZNAgAPr16YOLiwuDByURGREBwKLXFhMSEsL999wNQJ/eCUw/dyZ7 0zPoFd/6eNCUyZO46vLLbP3/8m1+cXEJCy66iEuvvIqi4mKCu3dnybtLWXjlFZz3h3Na65SW8s2G jQDsSU1jy7ZtrP1wDV5eXkwYP479mQdYvvIDbr7pxjb30NjYyKuLX+exhx9i4oQzABg8KAkATw8P rrnqSqxWK7l5ecybO4fHHn+SO2671dZ+xPBhXHPVlQAYDUZeePm/XDJ/Hj4+3lx9xeW2thfMmcXT z7/ArTf/HxWVlby99D1efOZphg8bCmB7734toVcv3N3cSBwwwPbt/qzzzuOcWbOpqKjAz8+PFR+s YuKEM/Dz1ZFMIiIiIvagBEAnefbZ53j++Re49dY/U1payoIFl7Jo0cskJyeTk5PD+PETmDJlMldd dSWlpWUUFhYRHx/Piy++xGuvLebGG2/AaDRy9913U11dzQUXzG3Tv4eHO3/72wO2BEBzczN33XUX S5a8BcDixYv54osvmTVrJpGRkVxwwTw+/3wtsbExnf5eiByr3Lw8mpub20wohw0ZwvKVHxyxzTcb NvLCfxfR1NRIaEgoFRWVlJUffQXAzHP/wP0PPczEqdM4Y+xYLpg7m8QBA9qtm3ngAMN+XnEA0CMs jNCQEA7mHrQlAIYObjsJfmXx63z0ySe4uroRFBiAwWikrKycoMBAcvPyGPirsZIGDrAlAPbt34/Z bOGiy66wldfU1jJi6NDD4jqYm4vZbGbUiPaTe/fe/yA7U3bRzb8bbm5ulJWVtSkfnJRo+3nokMEU /L2QpqYmXFxcuOu++9m9Zw8BAd1wNbnaVlVkZWdjMpkYMvjwSf/RBAUFMmbUSD5Y8yGXzJ/HylWr eeiBvx13PyIiIiJybJQA6CRPPfU09957DxddNB+A0tJS/vOfp0hOTua5556nX7++vPjiC23aWK1W HnnkUV577RXOOKP12zyLxcxLL710WAJg2rRp/OUvd7B9+3aGDh3Kxx9/TPfuwQwZMsRWZ/bs87nu uusA2Lx5M2vXriU29hp73rbICfHz9cVqtZJfUGA7BeBgbq5tMz6DwXBYm/88+yy33fInRgwbBsAF Fy+wLYP/LYEBATz9+L8pKCxk7RdfctW11/PhyuV0Dwrif4fx8/XlYG6u7XVDQwOlZWV4ex16pt/N zc32c0lpKYvfeJNVy5fh6+NDZVUVE6dOw2q1YjQa8fX15UBWlm2vggNZ2ba2Xl5eeHt7s/zdt9s8 c98eX19fLBYLB3PziI9ru0fAps1bSEtPZ8XSdwHY9u12Nm3Z0qZOXn6B7efcvDzc3NwwmUys++Yb snOyWflea9uNmzez/bvvbO9FQ0MDRUXFhIaG/GZ87fy6mDt7Fn//x6P0jIrCzc2NoYMHH15JRERE RDqE9gDoBFVVVRQXFzN27BjbtTFjxpCZeQCAvXvT2yzd/0V+fj6VlZXcdtvtDB8+kuHDR/Lvfz9O YeHh52o7Oztz6aULePXV1wB47bXXueznJceHxhxr+zkmJoaMjIwOuDsR+/Hz86N/v368v2IlLS0t 5BcU8OXX6xj7867/8XGxZGVn0/LzM+dms5mysnLbxptbtm0jNS3tmMbKOXgQi8VCSHAw8y+YS0C3 bpRXVLSOExtHxr79trrjxo5h2/bv2J+ZidVqZdmKlXh5edK3b592+66qqgLAzdUVi8XC2+8utcUM MGnimbyz9D1y8/JIS0/n408Pbew5KCkRs9nM0mXvYzabAaivr6ew6PDPgcCAAAb078fb7y6ltq4O gIrKSgCKS0vw8vK0tX/nvWWHtf/q63VUVFbS2NTEqjUfMXrkiNa2JaW297Surp6ly963tYkIDycu NpY3336bhoaGNmP+r7jYOPbt39/m2vChQzEYDPzjn/9i9szz2m0nIiIiIh1DCYBO4OnpiclkIjv7 0KZd2dlZ+P78nKu/vx/7/+ePYgAfHx8AXnvtFbZt28K2bVv4/vvv2LXrp3bHWbDgElavXsOOHT/x 7bffMmfO7DblTk76dcup54F772bj5i1MPedczp01h4ED+jNv7hwA+vbpQ3xcPGefP4sLL1mAk5MT 1y28mpv/fBuzLpzPy4te5cwzxh/TOO+vWMnUs//AZVcvZPa8i5g8KZmEn/fhmH/hBSx6bTHJZ01n 7RdfMmLYMC6edyFzL7qEaX84j1deW8wjf38QTw+PdvuO6dmT5DPP5OyZszh39hzKy8sJCz200d3N N96At7cX8y65lPseeJDJycm4u7euIPD18eHJfz3GO+8tY+JZ07ng4gWcO3sue1LbT2w8/MADpKWn M2nadC68ZAGXXN76TP/USZMAfo5hLv3aSVYMGTKYufMvZsZ5M0nbu5e/3HILANOmTqGxsZFzzp/F eXPmMqB/f1sbo9HIYw//nW+3f0fytBlccPECrr3xpnZjmzt7Fu+9v5zks6azcvUaoHUVx5zzZ1JS Wso5Z8848i9IRERERE6Y4ZHHHrLeftudHd5xXV0tG7esZ+igw59FbWxqZGfKjg4fs7OYTK4M7Jf4 m3Vyc3MZMCCRffvS8ff3Z/78i/D19eWJJx6nurqGefPmM27cWO67715WrFjB7bffwYoVy+nXrx9W q5XKykr8/PyYOXMW3bsH8a9//RNvb28sFguZmQeIjY3ho48+PuwYwEsuWcDOnbsYN24sTz/9lO16 QkJvli9/n379+gHwyCOPUlJSwr/+9U/7vElyUmlsaiRlz06SJ/z2Rngns5LSUjw9PHB3dz9q3ZaW Fmrr6vD9OYl2rOrr66moqCQkJLjdxwv+V3NzM+UVFQQFBh5T/draWpydnXF1df3Neg89+hg1NTX8 48EH2lyvqKigubmFgIBuGI2/ndCrrKqioaGBoMDANnUrKivx9vI67HGCCy5ewE3XX8vokSMpKysj MDDwsD4rKirw8fE54tgVFRU0NTcf8/vxiyeeepqS0lIeuv9vx9wmNz+HH378jpRdu4+5jYiIyC+6 d+9OUlIS0ZE9HR2KCAAVleVUVFUwaODh+zx1lEf/+bD2ALCXuro6nJ2dbZOVRx99hEsvvYyBAxOp q6tnwoQzuPXWPwMwc+ZMUlPTSE6eTExMDDU1Ndx//33MnDmT559/lhtv/CMJCX3o06c3+fkFzJ8/ j7vvvqvdcS+//DJmz5572PJ/kVNdYEDAMdd1dnY+7sk/gLu7+zElGH7h4uJC96CgY67v6enZ7vWd u1JYtnw5MTEx/PDjj/zw4w5efPbpw+r5+fkd81i+Pj7tvgdH22HfaDS2O/k/lvGPJz5ofezi07Wf 88GaD3nnjcXH1VZEREREjp8SAHby4487GDJkiG0jsIiICL788gtKSkpxdTXZNjH7xV//ege33vpn 8vPzCQwMxOPnpcQhISEsW7aUuro6SktL6d49GFdXEwDTp0877EjAoqJiEhMHttn8DyAtLbXN6zvu uB0ROTnExsQwZvQoDubmcca4cdx5+1+OK7HQEeacP5PIiMhOHbOpqQmj0ciiF54jJDi4U8cWERER 6YqUALCDP/7xZtasWcM9P58R/muBgUf+FtPFxYXIyPb/APfw8LAlBdpTUVHBkiVLeO65F3jppReP P2gRcRgPD3em/PyMvqPMPn9mp48Z6S80UAAAIABJREFUGxNDbIyOIhURERHpLEoA2MHEiRO4//77 8Pf377QxW1rM1NXVs2jRy4wYMaLTxpVj09hSR07lbg6U7ySrYifuLt5E+w0g0r8/Yd7xGA2/fbyb iIiIiIjIiVICwA7OO6/zj7IKDAyw7SkgJ4/qxlJe/+FOtud+iNVqabeOycmdc3r/kem9b8DJoP8k RURERETEPjTbELGT73I/4rXvb6e6sfQ36zWZ63k/5VG+z/uUq4f9hzCf+E6KUOTU5Wpyx2QykZj0 2yeyiIiItMdoNFBWXkplVaWjQxEBwGq1EBwUYvdxlADoZBaLhbq6Ory8vIDW0wJMJhPOzvpVnE6W 7foHa1IP38X9t2SW/8h9X0zh5tGL6Rd8bGfXi3RVgQGBjB09AYvZ7OhQRETkFGUyuQDHfmytiL25 uJjsPoZmnXZSX1/P51+ubXPN1eTKqJGjee/9pVxx2ZUAfPLZxwwdMqzTd98W+0kt3syHqc/8rrbN 5kYWbf8TD035CneX4z/GTqQr8XA/8saoIiIiInI4JQDsxGwxk5+fz9QpZ+Hi4gKAk9EJDw8Ppp81 3cHRib00tNSyaPufsGL93X2U1eezZMffuHLo4x0YmYiIiIiIdHVKANhZSHAIrq6utteNjY1kZWcR EhLabv2yslJS01Kpq68nNCSUvn36YjAcvjTJYrGwb38Gubm5tJjNdA/qzsABA21lKbtTKCwqxN3N nf79+uPr6wvA3vS9eHt5U1JaQmFhAb16JVBRUWFr+0sM2Tk5JCUmAVBQUEDGvnSampoID4+gV3wv AKqrq0nPSCe8Rzhpe1Pp1i2Afn3/n737jquq/AM4/oELl72HgIKCDAdTceJAcU+caWo5UrNsaduW Zma2y0p/ZWWWe5aj3As1B+IegDJk7y0XuPf3B3YVESeExvf9evmSe85znvOcc8+555zvM07z6tlx j6j1Zz8hrSDugfPZF7OcDo2G42XbthpKJYQQQgghhBCgW9sF+K/LzMwkIyODjIwMCgsLUalUnDl7 5pZpk1OS2fznZkxMTHFv7E70pSjCDobdMm1kVCTnz5/HvbE7zZo2qzBv85ZNJKck4+nhiYGBARt+ X09hYQEAcfFx7N67i5ycbDw9vbAwt+DI0cMUFhZqlz995jTFxVcBuHz5Erv27MTKyppGDRtx8tRJ Ik4cB6CgsICTJ09w+MjfODg4UM/e/oH316PuZPLOasvrVPKuastLCCGEEEIIIaQFQA0LO7BfW4Pv 6elFo4aNqkwbHn6MZk2b4+dbPqq1hYUFq9eson3b9ujqVozVpKenY21tjZNTfXR1dXFydAIgITGB zKxMRo0cjUKhwMXZhfT0NC5GXsTfLwAASwtLOgR11Obl3MCZyKiL+Pn6U1payqVLlxg4sPxVhkeP HaVlQEs8Pb0AUBoYsHv3Lm1eqhIVHTt00rYwqMuulhaQlBddbfldzjpRbXkJIcSjLDc3F1NT00rX QiGEEELcGwkA1LD+/QZU6AKQl5dXZdqs7Cyyc7KJiorUTlMoFBQUFGBmZlYhrY+3N3v37WXJb7/g 2sgV7+beWFvbkJWVRVlZGWvWrtamLVYVV1je8Vqw4B+enl4cPnIYP19/YmJjsLS0xMrSCrVaTXZO NuER4USciABAg4ZiVTFl10beNjY2lof/a+KyT6PRqKstv5isk9WWlxD/RStXLaP0prcADB08HKWy 5kfQVavVvD1zFiqVio8/nFNp/p59+/jl16VVLr9o4XfMePc9kpNTALC1taFt69Z069ql0u/9w2zL n5vIys665TwPd09aBbauNF2j0XAs/AgtAgLv+oH+hx8XMn7sU1haWlWa9+PPi9l34ACZmVm0bd2K /n374N38ene0jMxM5n78KecvXMC9sRuvTp+Go0Pl1yy98fY7pKam3XL9vXp0x8O9MV9/uwAovzY3 b9aUjkFBtAjwv6ttEEIIIR4GEgB4iCiVSny8fWni1eSOac3NLejXtz+5uTlERUex4Y8NjH58DEql EmNjY4YPe6zKZW9+5aCLswt79+0hNS2VyMiL2tp+XV1d9PX1CWrfAecGzneVV10Wn3OuWvMrUGWT XZSCpVG9as1XiP+K+CvxdA0OwcbWVjvt3/pNWr5yFWfPnaegoOCW85s3bcqUSRMBOH7iBMtWrGTe nA8qpDl95ix9e/eibevWpKSmsuS3pVyMiuK16dNqvPzVJcC/BaoSFQC7d++kXj0Hml7rlmZmeutA hlqtZsfO7fj7taiWGv3I6GgmPzUBh3r12LZjJ1Nfms5fG3/H4Fog6NkXXsTW1pZ5cz7g+x9/4ump z7N+1YpK4+s8/thjFBcXA/Dhx5/QplUgXYODAXBwqEdU9CViYmP5/ON5lJWVceLUKSZPfY4Nq1fi 5HjrcX2EEEKIh408vT1EXBu5cubMaZwbOGNiYgJAdk42lhaWldLm5uZgbm6BubkFPt6+nD59GpVK RX2n+oQd2M/FyIt4uHugo6ODqkRFaUkpxsa3fmWWrq4uHu4eREQcJyk5iZCu3bTzGjVy5eSpE9jZ 2mFoaHjbMtV1dia3DpLcL6XCEDNDm2rNU4j/Gvt6DtR3ql9h2o6d2/D3b8GJExGo1WV0C+lBfn4+ x8KPkJWdhZ2tPa0CW2tbCmg0Gk6eOkFcXCxKpRJ/vwDq1atcQ/yPhMREVq1dx+SnJvDZl1/dMo2t rS221wIT+QUFKJVKAlu2qJSuoYszvj7eQPn7qGfOnvNIBQBuHNDW0MgICwtLXJwbApCUlMhfW7dQ XFyMs7MLAf7l23/4yCEAdu3ega6uLi0CAjEyMiQi4jgpqSnoKhQ0dmtMs6Z3N6jsh+/P0v49cfw4 /ti0mZ27dtO7Zw/OnDvH+QsXCVu4ABNjYz764H3adOzMocNHaNemYusEH+/r6zM1NcXFxaXCdxYV fQl9fX3t9xXg78fuvXvZsXMXY0Y9fi+7TQghhKg1EgB4iAT4t+Dq1assW7EUKysriotVWFpa3vK1 geHHw0lMSsLU1ITi4mJ8fX21QYMe3Xqyd/8eDh/+GyMjI4qLiwkO7lJlAADKuwGsWbuaxm6NKzSf bd+2PfvC9vHr0iVYW1tTVFREg/oN6NwpuNq3/1HXyMqvWvNzsWyOQkdOUSFuJysrE/1rtf4GBgaY m1tw9NgREpMSaeLVFHt7e/IL8vlp8SL8fP3w9fHn1KkTrFqzglEjxwCwcdPvFBYV0sK/JZlZmaxY tYyRI0ZjZ2t3y3XOnD2H6S8+j7rswbv8ZGZlk5iUREpKKstXraZL504PnOfDICU1heUrlxLg34IG DZwJO7CfrKwsunYJob5TAwBcG7mhq1BgZGRIfn4++kolLVu2Ijsri/1h+ygpKcHP996a1+fl5ZGe kYG9ffl3FxkZhaODAybXrn/6+vo0dHEhKjq6UgDgbpSWlZKYlERpaRknT53i0qXLvPHqK/ecjxBC CFFb5OmihpiamDLpqcmVppuZmTF+7ATt58GhQ7R/KxQKOgR1pH27IAoKCjA0NERfX/+W+Qd37oKq RIVKpcLUxLTCvPr16zPysccpKipCo9FgZGSkberY7Yba/RvZWNvcsrxKpZKQLiGUdQqmsLAQIyMj bRNbh3oOjBg+8g57ou4wN7DF2tiJzMLEasmvugMKQvwXHTgYhr5++W+Si3MjunYJAcoDqt7NfQDY vWcnLs4udOoYfC2dC19/8wUpqSno6elxMfICUyZP1QZJs7OziDhxnO4hPSqtb8369djYWNOhfXv2 7tv/wOVf/OuvrFu/gczsLAwNDHnlpRcfOM+HwdGjh2ns5k5w564AGBkZsWbtKjp26ET9+uUBgEaN XLXXE0NDI2xt7VCr1RgbG+Pd3JsLF8/fUwBAo9Ew84M5BLVrR8uA8oFq09LTadSoYYV0jRo2JCvr 1uMW3ElWVjYvTn8FtUZNTGwco0Y8hlujRveVlxBCCFEbJADwENLV1b2rQaCU+kqU+lUPdmVkZFRt ZVIoFI/UwFS1xdO2DYfi1lVTXvdeOyVEXdO3T/8KXQA0Gg1Q/pD/j/T0NJJTkvl+0ULtNB10yM3N Qa0ur8X/bdkS7bwSlQpHp4qDpQKkZ2Twvx9+5PvvviEvL4/Ca0HWvLw8TExM7qs/+0vPTaVn9+4A 7Ny9mxFjnmTPtr+q9fe7NmRkZuDrcz2I6dzABbVaTW5uzi0H8isuvsrvf2wgJycbM3NzSktKKCkp uev1aTQaZn84l5TUVL7/9hvtdAtz80oD+6WmpVVo7n8v7GxtWbn0VwAKC4t48eVX+Prb75j2wvP3 lZ8QQgjxb5MAgBDVaJj3m5xI2k5RSdVve7gbTeza0apB/2oqlRB1j57e9dZTBgaGeHk1vWWNfkzs ZfT09Bn35IQ7DiCYkJhIfkEBI58YC0BZWRlXr16l14BQVi399YEHgusYFERZWRlnzp0jsEXl8QIe JUaGRuTkZGs///O3wbWxZG4WcSICY2Njhg0tH8D26LHDnDp96q7XN/fjTzh3/gILv/laO14NQIP6 9YmJjaW0tBQ9PT00Gg3Rly4x5vEHb71mbGxEu7Zt2L5z1wPnJYQQQvxb5IW6QlQjG+P6jPR974Hy MNAzZnzLT9FB586JhRB35OnhxZkzp8jISNdOy8nJpqysDCfH+ujpKTgWflTbGqC0tJTc3NxK+fj5 +BC2a4f237wPZmNvZ0fYrh0P/PCfl5fHb8tXYGFujp+PzwPl9TBo3NidyMiL5OXnUVZWxvETx6ln Xw9TE1MUCgXW1jakp1+vmS8oyNe+MrewsOCeHv7nffoZESdPsWD+15VaqrVuFYiFuTlLli6jrKyM 5StXoaenR+eOHR54GyOjovlr2za6Bnd+4LyEEEKIf4u0ABCimnVyHcmxxC2cSNp+X8sP856BvWmj 6i2UEHWYl1cTsnOyWbzkJ8zNzFFrNCh0dRk9+kkMlAYMHjSMLVs2cujvg1hYWFBYUEDXrt0wNzev 8bK9+c57vPXeLIyNjWkd2JL333unyrFfHiX+fgEkJSfyv++/w9DAEAMDA0IHDtbOb9umHVv+2kRB QQGhA4fg79+CDb+vZdFP31NcfBUfb1+ioqPuuJ6CggJ+W74CgI4h18e4mfzUBJ6ZPAk9PT0+mfsh L7/+Bj//sgSNRsPHcz647y4WScnJtArqCIBrw4a0b9eWYYMH32EpIYQQ4uGhM3feB5rXXnmz2jMu LCwg7NBeAgMq92MuVhVz6syJal/nv0WpNMC3uQzQJqpWqlax5sw8/ry4AI3m7kYKN1FaMiZgDm2d Q6u9PMWqYs6cO0VIcM9qz1uIR8U//fWVSmWFZuL/uHq1iJLSUkxNTCu9I17cn5KSElQqlfYtNXdS WFiAsfHdpb0XGo2G9PQMbG1t5LsVQghRZ3308RxpASBETdDTVfKYz1u0dOrF90deICX/8m3T+zt2 Z2zLeVga1vuXSihE3aOjo3PbWn1DQyNu3UNd3C99ff17atFQEw//UP7d29nZ1kjeQgghxKNEAgBC 1CB3m0Bmd99JVMYRLmed5HLWCWKzT2GkZ0ojKz9crfxwtfajoeWj3+dXCCGEEEII8XCTAIAQNUxf YUBT+w40tX/wQaeEEEIIIYQQ4n7JWwCEEEIIIYQQQog6QAIAQgghhBBCCCFEHSABACGEEEIIIYQQ og6QAIAQQgghhBBCCFEHSABACCGEEEIIIYSoAyQAIIQQQgghhBBC1AESABBCCCGEEEIIIeoACQAI IYQQQgghhBB1gAQAhBBCCCGEEEKIOkACAEIIIYQQQgghRB0gAQAhhBBCCCGEEKIOqJUAgFJfib6+ sjZWXS1MTUxquwhCCCGEEEIIIcQ9qZUAgI6ODg2dG2JoaFQbq38g5mbmODrUr+1iCCGEEEIIIYQQ 90SvtlZsaWGFpYVVba1eCCGEEEIIIYSoU2QMACGEEEIIIYQQog6QAIAQQgghhBBCCFEHSABACCGE EEIIIYSoAyQAIIQQQgghhBBC1AESABBCCCGEEEIIIeoACQAIIYQQQgghhBB1QI29BrC0tBQdHR2O Hj9cU6sQQtwDhUJR20UQQgghhBBC1KIaCwCUlZWiUCjw92lRU6sQQtylvPxcIqMja7sYQgghhBBC iFokXQCEEEIIIYQQQog6QAIAQgghhBBCCCFEHSABACGEEEIIIYQQog6QAIAQQgghhBBCCFEHSABA CCGEEEIIIYSoAyQAIIQQQgghhBBC1AE19hrAO8nOyeJK4hWuXi2qrSLcF3Mzc5wbNMTI0Ki2iyKE EEIIIYQQQty1WmkBoNFoiI2PfeQe/gFy83JJSk6o7WIIIYQQQgghhBD3pFYCAKoSFSUlqtpYdbXI LyiolnyuXLlCaWlpteR1O8XFxSQnJ9f4eoQQQgghhBBCPLxqrQvAf11aWjrjx48HQEdHBy8vL4KD g+nTpzc6OjoAtGsXxJ49u3Fzc73HvNMYP35CpenPPPMMvXv3qjQ9LCyM996byd69e+5jS4QQQggh hBBC/BdIAKCGqFTFhIUdYM2aVZiamnL27DmmTZuOQqGgV6+eAKxatQInJ8d7zvvq1auEhR1g2bKl mJiYaKe7uzeutvILIYQQQgghhPhvkQBADfP398fKyopWrVpx7tw51q9frw0A/PXXVjw8PDA0NESj 0fDDD4s4cOAgXl6e9OjRnb//PsyUKU9XmXfbtm2wsLC45bwVK1aydes23Nxc8fX1qZFtE0IIIYQQ Qgjx6JDXANawK1cSiI2NZcuWP9m6dSs9evTQzvvhh0Xk5OQC8Mknn/L99z/Qq1dPlEolkydPYe3a dbfN+9KlS0RGRmr/FRcXA7BgwUJmz/6Azp07oVQqmTlzVs1toBBCCCGEEEKIR4K0AKhhTz89BYVC l5iYWDp16kjXrl1ume5///uezz77hP79+wMQFxfHmTNnb5v3pEmT0dG5HsP58cdFeHs359tvv+PN N99g5MgRAKSkpHD48OFq2iIhhBBCCCGEEI8iaQFQwzZu/J29e/cQHR2Js7Mz48aNr5QmPz+fjIwM WrVqpZ12499V2b59G4cPH9L+8/ZujkqlIiEhgbZt22rT3fi3EEIIIYQQQoi6SQIA/xJ9fX169uzB oUN/U1ZWVmGeiYkJBgYGREZGaadFRUXdnMVdUSqVGBkZER8fr512499CCCGEEEIIIeomCQD8S+Li 4li8eAk9e/ZEoVBUmKejo0P//v347rvvSExMJDw8nI0bN973ukJCurJy5UqKi1VkZmayceOmBy2+ EEIIIYQQQohHnAQAalizZs1xdHQiNHQQ1tZWvP32jFum+/DDOajVajp27Myrr75OaGgoJibGt83b 1bUx1ta22n8ffTQPgA8+mM3x4xEEBLSgU6dg2rdvV+3bJYQQQgghhBDi0SKDANaQ+vXrk5mZfts0 8fGx2r9tbGxYvnyZ9vPkyU/TuHHjWy7n7Ox827wbNGhAWNg+UlJSsLOzQ1dX4jxCCCGEEEIIUddJ AOAhsWfPXtavX0/jxo3Zt28fx48fZ8uWzQ+UZ7169aqpdEIIIYQQQgghHnVSNfyQCAjwp0OHDpSW ljJw4EDCwvZX2QJACCGEEEIIIYS4V9IC4CFhbm7OkCGDa7sY4l+QWZiIob4pxvrmtV0UIYQQok4r LCzi6tWrWFtbVWu+Go2GpORkHB0c0NHRqda8hXiUFRUVUVhUhI21dbXnnZiUhEO9etL9+Q4kACBE DYvLPsPf8euJyT5FbNYp8lVZANiZNKShpTeNrHwJdhuNqbJ6bz7+q+72x12tVpOckoKTo+O/VLKq JSUnY29nh0KhIDs7G319fUxMTKpMr1arSUlJxdHR4Y55FxcXk5eXh62tbXUWGahYbiFqWlZWFkoD A0yMbz8ArqhdTz/3PIYGBnz+8bwKD7avvDGDzMxMFsz/Cn19/Vos4b3Zun0723fuZP4Xn1drvkVF V+k9IJRDe3djZGRUrXmL/46nn3ueElUJCoUCPz9fgjt2oHmzZrVdrBq1c/duft+4mYXffF2t+ZaU lNB7QCj7dmzH3NysWvP+r5HwyL9MrVaTn5+v/VxYWEhpaWktlkjUlDJNKevPfsrMnX3YdOEbzqTs 1T78A6QVxHI0YROrT3/Im1uDOZ64tRZL+3B4+rnnuXT5coVpv2/cxPzvFmg/9x4QSlZ29h3zys7O ofeAUDQaTbWWcctfW0lOSbmnZQYNH0FiUhIAcz/5lLUbfr9t+sysLHoNGHhXeR8ND+eZF166p/Lc rSGPjSQhMbFG8n6UnTt/gU+++JIhIx5n8GMjeW3GW8TFXwEgMiqa/QcO1HIJH02z5sxl0+YtAMTE xrJrz95aLpG4lfDjEZw8dZqjx8K1086cPcvR8GMcDQ9HrVbXYumEeLSEH4+gb+9eTJk0ET2FgjHj n9JeT4SoKdICoIYUFRWxfee2CtMMlAa0a9ueVWtWMn7sBAD+3LqFwJatcHF2qY1iihqSlBfFgr+f JTb71F2lz72axpcHxhLUcBhjAuZgqFd17fB/WfjxCPJuCJBBeS10ZFSU9vOiBd9hYV573Sd++e03 rK2tcLjPQTYnTZhwx1d8ioeXWq3mpVdfZWC/fnz60Yfo6Ohw5Fg4V69eBSDixAmOhofToX37Wi7p o2fqlMmYm5Wf22fPnWfzn3/SpXOnWi6VuJW+fXqz/o8/aBXYEigP1Pbt1YslS5dVSHfg4CG27dxJ cbGKkC6dCenSBSgPlP195Aie7u78sXkzdra2PDF6FHl5eaxYtRqFQsHIx4ZX+J3dun0Hu/fuQ6HQ pWf3btpz7HJMDHv3h+HTvDm/b9qEn48PsXFxjB45QtsySqPR8NW33/HY0CF3/O3Ozslh8ZJfiYuP x8XFhXFjxmhrEyNOnuTPrdtISUmhnr09j48YgYtzA+2y4ccjWLthA6ampgwa0P8B97KoKxo3dsPP x4cAfz+OR0Twx6ZNTJk0kU8+/4LhQ4eweu06NBoNr0x7iQuRkaxas5acnBwC/PwYMXyYtkVkSUkJ K1av4eSp0+joQLeuXeke0hWAhMRElq9aTVJSEs2aNmX0yBEolUoAjhw9xp59+0hITKR+/fpMePIJ rKysiImNZev2HZw7fx4rSyuGDh5Es6ZNAPj7yBE2//kXKpWKjkFB9OnVE4DMzCx+XbaMXj16sHrt Ohzq2TN+7JO33f7c3Dx+XrKE2Lg4XJydeXLMaCwtLAA4eeo0W7ZuJflai8SRjw2nUcOG2mUjTp5k zbr1GBsZMXSwdKW+WxIAqCFl6jKSkpLo2aOXtimcQleBsbExfXr1qeXSiZqkKivi87AnSM2Puedl w2JXoaujYELgZ9VfsP+Inbt307RpE/T09FCr1fy2bDknT5/Gw92d1q0CuXgxkuFDh2jTX0lI4Ldl y9HT02PIoFBcGzXSzrt0+TKr164jLT0dfz8/Rgwbqm3uvv/AAfaHHSAlNY2GLs5MmjCBvfv3k5yS wopVa9izdx8dO3SgXZvWlcp4/sIFll+7ib2xLABnz53DwaEe9eztKS4uZsvWbRw+cpTS0lJaBPgz YtjQSvllZmbx85IlDA4dWOHCdytFRUX8vORXoqKjcXRw4InRo7C3swPgQmQkGzdv4cqVK1hbWzN8 yGC8PD21y164eJFlK1eVl1vGJLmlo8fCKSos4umJT2mbPzd0KQ/gJiUls3XHDlJSUpn36WdYWlgw 6anyYO+6Db9z6PBhjI2NGdi/H/6+vkD5zU1UdDS2Njb8tX073UNCaNa0CWvWrScqOhpjY2O6d+1K p44dtGVISkrmp1+WkF9QQO+ePUhITMTPx4emTbyA2x/XN0vPyOC35SuIiYnFwsKckcOH4eXpyYWL F/lj8xYSEhLKj5WhQ/Dy8AAg+tIlDhw8hIeHO5u2/ImJiQlPjh6Fo0N5l5Vz5y+wacsWEhITsbGx YcSwobjfMKhtckoKS5evIP7KFaytrBg1cgRurq6cOn2GRg1d0NHVYePmLVyOiWHep59hZGRE7549 2LNvPxNuuJG8dDmG7Tt2aPex+Pf07BbC1JemU1BYiFJfn+07d/H1559VCACsXruOJUuXMXbMaJRK fb74ej45ObkMDh1I/JUrfP/jT7QMCKB3zx6sWb+e1958C319PXp278658+eZ/OxzbFi9sjyvdev5 dsFCJk4Yj0ql4s233+XtN9+ge0hXEpOSWPTTzzTx8iJ0QH8au7ly6PBh1qzfwORrx8aRo8fY8udf PDfl6dtuV2lpKU9OmIiLizODBgxg7fr1PPnURFYt/RU9PT0uX46hdWBLLC0sOXDoEGPGT+CPNasx NzfjxKlTPDdtOqNGPIaTkxOz5sytuS9A/GdlZmZhY2NDWVkZvy1fwZmz5+jRvRue7u7ExsUx7qlJ hA7oT0iXLvxv0Y9ERkfz7ow3AZj64jRy83IZPXIE1tY2JCaVt+CLi4/niQkTGTFsKAP792fF6tVE nDjJV599QkpqKq/NeIvXX5lOt5CuREVFU3T1KlbA9NfeoH/fPjw5ZjSpqank5uUCEHbwIK/NeJsn Rj2OjY01n3/1Nenp6TwxehQ5uTksWbqMEydP0a9Pbxq7ud12e8vKyhg7cRKODg4MGRTKhj/+YMy4 CaxZvhSlUsnl2BgCWwRgZWnFocOHteecpaUlZ86d49kXXmLk8GG4ODsza86cGv1u/kskAFDDHOo5 YGBgoP1cXFxMbFwsDg637pecmZnB+QvnKSwqwtHBkWZNm91y8Bi1Wk30pSgSEhIoLSvD3s4eXx9f 7bwzZ8+QkpqCkaER3s29sbgWSbsYeREzUzPSM9JJSUnG09OL7Oxs7bL/lCEuPh5/P38AkpOTiYqO RKVS0aCBM54e5Q8MeXl5REZF0qB+Ay5cPI+1tQ3NmzWvnh33CFtxcvZ9Pfz/Y1/Mclo36I+PQ5fq K9QjJCkpGXOz6323srKyKsxxpluhAAAgAElEQVT/bfkKJowbi4mxMV/O/4awgweZMHYsMbGxzHjn PRrUr1/hofuDj+bRq0cPUlNTGTV2PHu3b0VPT48z587x3IvTGD1yBG1at+KX35Zy8eJFZr7zNtGX LjH7w494ZdqL2Fhbc/bceVSqYtwbu2FqYoJ3s6Z4eXni3KB+pfLHxcczfvIUBocOpFmTJsz56GNU KpV2/v4DB2jerBmBLVqwZt169oaF8eTo0Sh0dYmKjr5Ffld47qVpPDl61B0f/gGmPP8Cujq6jH58 BNt27GTkE2P5ffVKTExMiI2NxbtZU0KCOxMecYJxEyezevlSnBwdiYu/wvjJUxg0oD/NmzVjzrxP KL6h3KJcs6ZNKLp6la+++Za+vXvR2M1N+xttZmZKY1dX1GVqgtq3w9DQEIBvFv6PTZu3MHniBK5c SeCZ517g26++wN/Pj6joaL5ZsJCmTbwIHTCAhi7OJCYlYW9vR7u2bYiKimbmnA95b8abdOwQRHFx MU8+NZG2rVvTs3sIa9at43jECV5/5WWaNvG67XF9s6ysLIaOHEVgiwCGDg6lqOgq6RkZeFHeBN/X uzndugQTfjyCcU9NYs2KZTg6OJCQkMj/fvwRL09PRg4fxvaduxg9rvymzNjYiMsxMfj6+BDSpQvH jh/niQkTWb9qBfZ2diSnpDDs8VF0aN+e4UOHkpubQ0ZGJm6uruzZtx+VqjWeHp54eriTnZNDUPt2 6OvpU9/JiR8X/0KXzp1xc20EwG/Ll2N0bR+Lf5exsQkdg9qzddt2zMzM8PXxxsrKskKa+QsW8v67 79Ax6FprGB0dfv6lPJAJ5V0g33/vHUyMjXFycuTxJ8ayYP7XtGvTmr69e7H+j26kpqVhb2fH4iVL mDh+HCOHDwMgPz+fHxcv1tZu5uTmMmfWe9oa/+FDh/Dm2+8ycfw4dHV1Wb1uPYMHDrjj2DG79+4j LT2dtSuWoVAo6BjUno4h3dm1Zy/dQ7oyaOAA7fqsrCzZsWs3R8PD6RrcmWUrVtKzWzeemTwJAH09 BW++c6Z6drj4T1u34Xf2hx3gWPhxUtPT6XutNh3gsWFDtbXrcz/+hBYB/rw6fRoADvXsGTtxMs8/ M4WY2Dj+PnKE7Vs2YWtjUyH/Hxf/QkiXYJ6e+BQAPt7NCenVh6SkZKIvX8La2oo2rVtjYW6uDU4X FBSQkJhIx6D2lR7if/l1KaED+jNpwngAdHV0+WL+NzwxehQAKpWKF6Y+i6+P9x23ff+BAyQkJrLi 11/Q19enc8cOdAzpzvadu+jTqycD+/UDylsJWFlZsnvPXg4fPUqPbt1YvnIVXYM7M/VaYM/Q0IBX 3phxbzu/jpIAQA3LzMzUNrExMjKirKyMM2fP0KZ120ppk1OS2b5jGz7evjg51efkqRNkZWfRoX2H SmkjoyK5ePECAf4B6CoUpKena+dt3rIJA0NDvDy9SE1NZcPv6xk6ZCjGxibExceRlpaKcwNnPD29 sDC3YNv2rbg3dsf42sBLp8+c1gYtLl++xKHDh/D18cPI0JDwiOMUFhbg7xdAQWEBJ0+eIDExAS8v L6wsZRC7c6n72Rn98wPn89Oxl/mgxy6M6uCbAr6YPx8D5fWgWXZODv6+PpXSqdVqlq9azRcfz6Nd 2zYAXI6JJScnp0K6cU+MoU2rVkD5YE/hxyNo3SqQ7xf9yODQgdqmaY3d3Og/eCivvjydc+cv4Ojo QNvWrTExMcHfzw8AS0tLTE1Nad68mTbPm61cvYbAFi14+cUXALC2tmLys8/dMu3Z8+dp4ulJm1aB 6Orq0rpVYIX5p8+c4bUZb/Pma68Q1K7dHffdiVOnOHHyFLu3/YWFuTldg4Pp1X8gG/7YyOMjHqNH t25A+YXdwsKCsIMH2RcWxmNDh7Jq7VoC/P14+aUXAbCxtmbiM8/ecZ11jampKd9++QVLli5l2OOj cXJ0ZPTjIxkSOhBTU1Mau7mRlZ2t/b7UajVLflvKnFnv0TU4GICUlFR+XvIrX1w7rvILCvhs3kfa awWAv68vKpUKSwsLukd3YfvOnXTsEMSOXbvRaDTMerf8gT6wZUs6dAnRLne74/rmwfWWr1qNrY01 n8z9sNJ29uzeHbh+rOw/cJD9YQcYdq1lSG5uHh/OmoWdnS0hXbrQq/9A/ty6lcGhA7U3q/n5+Vha WrB3334OHDxE6ID+/Lp0GW6ubnz4/qwq97GxsRGeHh5ERUdXOO779e7FmnXreGXaSxQVFbHlr638 9vOPd/GtiZowsH8/5n+3AHMzc+2D8T+ysrLIysri488+55PPvwDKa9cLCgu1aTzc3bXHpEuD8mb0 AX7lDx+6uro0cHIiLj4eaysrriQkEtiypXbZwJYtWLZylfZzQxeXCgOhtgwIwMTEhP1hB/D18WbP vn28/NILd9ymyzExtPD307aY0dfXx8/Hh/gr5X2yd+zaxaKff6G0pARHRwfy8/PJvBakjou/og1Q AAT4+99xfUL8Q19fn2eenkQLf390dXUpKSkByo/1f1yOidXe7wD4eHuj1NfnSkIil2Iu4+XpWenh H8pbbSUlJ1cYt0Op1CcpOZn2bduyY9duevcfSIC/P0MHDyK4U0dMTEx4YeqzPDXlGerZ2xM6YAAD +/XFyKg8yPv4iOHavAJbtiA7O1s7xpmBgQHeze9uIMPLMbH4+/peby2tUNDC348r18653Xv38f2P P6FSFePo4Eh2Tg4ZmdfPuYH9+mrzCvCTc+5uSQCghoUd2K+tHfL09KJRw0ZVpg0PP0azps3x8y2/ KbSwsGD1mlW0b9u+UtQ6PT0da2trnJzqo6uri5OjEwAJiQlkZmUyauRoFAoFLs4upKencTHyIv5+ AQBYWljSIaijNi/nBs5ERl3Ez9ef0tJSLl26xMCBoQAcPXaUlgEt8fQsb1qqNDBg9+5d2rxUJSo6 duikbWFQ1/1x/is0PPigc5lFSYTFrqGb+7hqKNWj5aMPZuPnc/2Bf+EPizh77lyldOkZGVy9ehXf G4ID/r4+7Nm3v0K6wBbXL57ODZyJiY2ldatAoi9d4tz5C2zbsVM7X6lUkpKSQkiXYMIOHqR7n360 ad2K4UOGVLjo3k5c/BUC/P1uKJNvla+AGvfEGGbNmUvXXn3oERLCiOHDtLWbUF6b//rL0+/q4R/K L6Qe7u4VxkgIbNmC+IQEoLz5+lfffEtBYSFOTo6kpKSSee1CGh8fTwu/6+X28/OV1+hUIbBlCwJb tqCgsJBtO3byxdfzMTQwqPQQBOUP+0VFRQS2qPjwsvjX37SffX28Kzz8JyUl8+77s0lLT8PRwZGs 7PImoQBXrlzB74Zj3sTYGI8bmtff7rh2c3WtULZLl2OqPLaOHD3G199+d/1YSU3VPugA1Hdyws7u +gNXYMsW2oekg38f5tuFCykqKsLJ0Ym09HQyMzNvWGflAPjdGD50COMmTub5qc/y59ZtNPH0rNCl R/y7WgYEkJ6eQfyVBD6bN5e0GyoijE1M0NXVZe7s97V9hm+mUFT+fbnVb46enh7GxsYkJCbi4V5+ rF9JSMDM1FSbxvAWLUGGDx3CqnXriImNpW3rVtquULdjaWFRaeDTKwkJdDfrilqt5ouv5zN75nva a9TAocPh2kCz5mamJF0b7BXKx68R4m4MGjigwn3PjQwNrh/bFjcdn2np6RSrVJiZmWJpYUFiUiJl ZWWVunyZmpoyeOBAbeuUm707401eeu45du/dy+y5H6GnUNCxQxAjhw9jSOhADh89yveLfuJKQgIv v/hC+XmScL0cVxISUSgU2opEpVJ51/cPtz7nEglq1w6NRsOX8+cz4/XXtPdyQ0c+rj3nzMxMK5xn cs7dPQkA1LD+/QZU6AKQl5dXZdqs7Cyyc7KJiorUTlMoFBQUFGBmVvF1Fj7e3uzdt5clv/2CayNX vJt7Y21tQ1ZWFmVlZaxZu1qbtlhVXGF5x2vBgn94enpx+Mhh/Hz9iYmNwdLSEitLK9RqNdk52YRH hBNxIgIADRqKVcWUlZUBYGxsLA//12jQEJN1stryi8k6UW15/RdZWFigo6NDTGwszZs2BSAmNq5S uqouQqampgzo25eRjw2/5fwP359FZmYWO/fs4ZU3Z/DdV1/i490cHW7/PmczM1OSkq5fhFJSU6t8 E0FjNzcW//A/LsfE8MemzYybOIntWzZp58+ZOZOZc+bg2si1ypvoG1laWJCcnIxardZu95WEBFoH lrcsmL9gAU+MHkW3ruXdS56a8oy2bGZmZiTecPFMSUmV0bzvwMTYmND+/Yi+dIkdu3YxaOCASsEe c4vyYExCYiLm5uWB1CsJCZiZVf3w8uuyZTRv3owXnn0GgM+/+proS5cAtAMz/aOsrIwr1wI8cOfj +kaWlhbExcffct787xYw9onR2lYLEyZPqXAcp2dkUFJSoq21SUhMxO3aw/jX33zLlEkT6dghCIAn xj+lDYtaWlS9zhvdKmjW2M0N98aN2bZ9B6vXra9QAyX+fTo6Onwydw4lJaXo6VW8nTRQKglq346f fvmFd2e8iampKRqNhrj4eO2YGfeiU4cgft+4idaBgZSUlrB5y190DAq67TL9+/Tmq2++5eLFSN56 4/W7Wk/bNq2Z+8mnHDl6jFaBLdm3P4yU1FTatG5FaWkp2Tk52sDD7r37KpyLQe3asfmvvxg2ZDCm pqb8sWnzPW+nELfTqUMQ8xcsIDEpiXr29qxYvYaGLi44N2iApWV5F5w169YzZFBo+WuHc3KwtLAg pEsXFn7/A/379sH5WmubfwbcS0lNxcrKCnNzMwb068vWHTvIyMqioKCA4mIV1tZWdGjfnsuXYzh3 4SIAHTsE8de27fTp1RMDAwPWbfid9m3b3FelQZvWrfjgo3kcOHiI9u3acvDQ38TFx9O2TWvKysrI zMrWnnNhBw8SGXW9q2RQu3asXb+ekcOHY25uxu+bNlW1GnETCQA8RJRKJT7evjTxuvONvrm5Bf36 9ic3N4eo6Cg2/LGB0Y+PQalUYmxszPBhj1W57M0XahdnF/bu20NqWiqRkRe1tf26urro6+sT1L4D zg2c7yqvuiw1P5bCktxqy++yBABuy0CppGOHIH7+5Vdee3ka8fFX2Lt//13fXIZ06cLKNWvoEtxZ Oyp0TGwsjRo2JCkpGTs7W6ytrRg6KJT1v/9OZlZ5DaaHe2Oioy9V2QUgqF07vlmwkJTUVOxsbVn/ x8YqyxAXH4+LszOujRrxxOhRrFyztkIT2Y4dgnj/3Xd5btp0Pps3t8oagn8E+PtRWlbGn1u30adX T06fOcOp02eY9vzzAKSnZ2BqWv6GiVOnz3As/DgtrjVTDWrXjq+++ZbklBTs7ezY8Mcfd7Uf65rI qGhycnO0tRG5uXmEHTiorZV3d2/M6nXrtLUwJsbGtAwIYO2GDbzi9iLZOTls37mLvr17VbmOtPQM mniWD7iXlJTMX9u24964vA9mp44d+Pizz9m4eQtdg4NZumIFRdfeQAC3P65v1rVzZ1569TXOnD2r fe90Tm4uFubmpKWnY2pSftN14tQpwiMitCO+Q/l4Npv/+ouB/fpxITKSU6fP8Nr0aWg0GjIyMzG9 dsMWfjyCk6dP07lTeauzLsGdmT33Iy5ERuLl4YFGoyEvL7/SO5s93BsTGxdfIcgA5bW6X87/hsLC Qrp37XrH70vUrCZeXlXOe++tGbw3+wO69uxN48ZupKdn0LtnD6a98Pw9r2f6C88z/bU36NV/AKVl Zfj5+mj7/VbFxMSEXt27c+DQobtudeLcoAFvv/kGz02bjpWlJdk52bz31gztQ9Pkp55i0rNTMTcz x97ersLbPoYPG0p4xAl6DxyEjbW1tiuMENWlT6+enDh1igFDhmFlaYm+vj6fzP0QhUKBpYUFH384 h3dmvs/CRT9ibmaGl6cHc2e/z5DQgaSkpDDksZE4OztTXFyMsZERy3/9hWPh4Xz25dc0qF+f/IIC nBvUp1/vXiQmJTFu0tPY2dqgo6ODUl/JzHfeAmD82CeJjIqm14BQDA0NcXJ05NOPKncluxuODg68 O+NNpr/+OpYWlmRlZ/POm29oW3dNmTSRqS9Ow9zcHGtrKzp3vN6CeeigUMKPH6f3wFBsbWzoc5vr qqhInt4eIq6NXDlz5jTODZwxMSm/Sc/OycbSwrJS2tzcHMzNLTA3t8DH25fTp0+jUqmo71SfsAP7 uRh5EQ93D3R0dFCVqCgtKdU2zbmZrq4uHu4eREQcJyk5iZCu3bTzGjVy5eSpE9jZ2mlrqaoqU11X nbX/UP4qQVVZEUqFUbXm+1/yzhtv8PbMmQwePgJXV1d6dAu5q9pFgLFjRpOZmUm/QUNwa9SIvPx8 nBwdWbTwO3bt2cOixb/QoH598vLyaOLlRacO5WNxDAodyJfzv2HRz4sZP/ZJRo2oGGzr1aM7fx85 Qr9BQ3CoZ09w5863bJ4K8MXX33D+wgXs7e3Iy8tnyqSJWFpYkJ6RoU3Trk1rPpr9PtNeeY2P5syu 0KXhZhbm5syZNZN3Zs7iq2+/JSMjkxdvGIhnyqSneHfWbMzNzVAqDejZ/fq53qNbCH8fOUL/wUNx qGdP506dMDaWY+9mBYUFzJz9AVnZOdjZ2hATG0dQ+3a8MLV8vARfb29cGzWi76DBWFtZs3TxT7z7 1ptMe/V1eg8IJS8/n67BnRn9+Mgq1zHm8ZG89d5MtmzdSkFBId27hXD58mUA6tnb8+HsWXz25dd8 /vV8evfojoe7O0ZG5d/V7Y7rm7Vr24YpkyYybtLTODk6crX4Kk+NG8fQQaE8M3kSb703E3NzMwwN DenRLaTCsl4eHuzZu58ff/6FKwkJPDt5kvZhcMqkibw24y3Mzc0wMTHVtjgB6B7SlajoaEaPHY9z gwYUFhbywtRn6d2zR4X83Rs3pkWAPwOGDMPQ0IB1K1cAENIlmLmffEr/vn0qdJsQ/57D+/fecrqj gwMnjvyt/WxrY8P8zz/j6tWrZGVlY21jjcG176xrcGe6BnfWpjUzM6uwLMDKpb9ez8vWlsWLvic7 JweFrm6FVo1B7dpV2ZUlPSODoYMH3bZmMnRAf0JveGVfaP9+DOjbh7T0dOxsbSssO3rkCB4bOoSi oquVglYGSiWff/wR2dnZGBkbY6BU8uzTk6tcrxBQ9fmkr69f6ZzQ1dVlxmuv8vKLL5Cbm1ehGxZA 29at2brpD9LS0lEoFFhbl4/NpaOjw7NPT2bKpImkpKRiZmaqDdL27d2bHt26kZ6ega2tjTbg6uLs zPbNG0lLT8fYyEibHspbv3312ScUFBSgUqmwsro+Bphro0bs37n9ttvct3dv+vburf3cr09v+vTq ectzbsSwoQwdFEphYVGlc05fX5+PP5xDdk4ORoaGGBgY8GwV3RxERTpz532gee2VN6s946ysDCJO hePvU/lmtVhVzKkzj27tplJpgG9zv9umyS/IZ+my33hyzNhKXQBWrVnJ+LHlr6ZZu34NgS1b4eLs QllZGQcPHeD8hfNYWVlRXKzC0tLylq8N3L1nF4lJSZiamlBcXIyHu4e2X35CQgJ79++hrLQMIyMj iouLCQ7ugpOjE9t3bsehngPezSuOzJmRmcGatatp7Na4QgBApVKxL2wfly9fwtramqKiIhrUb0Dn TsEkpySze88uRgyv+ka2Ltl7eRk/HpterXnOH3AGU+WDD66Yl59LZHQkIcE97pz4Efb8tJdxc23E i89NvetlSktLSUtLx9zCvMIgacUqFZkZmdSrZ39fzdpyc/PQ19fTPphVJT8/n4LCQurZ29/zOqqi VqtJS0/Hxtq6UisdtVpNbm6utrngzfLy8lAo9OTh/w7y8vLIzcvDxtq6ygDPzTIyMzEyNLrrfZuR mYmNtfVt02RmZhHSuw9rVyyr0B++quP6VtRqNSkpqZXSqtVqcvPytO9j/sfefftZ8P0PLP3lZ3Jy c1Hq61c6zsvKysjLz6+07I3zU1JTsbSwvKdjLTs7mz6hg1m6+Ke7eiuGqH5+re5uPBQhAIYOHsTb d9kFo67Jy8ujww333OK/p2f37sybM7u2i1HBRx/PkQDA/bibAMCDUKvVFBQUYGhoWKHp481UJSpU KpW2mebNioqK0Gg0GBkZVTkI2d0qKyujsLAQIyMjafZfhbjsM7yzvXu15Wdj3IBP+xyulrwehQCA XAj/+x7GC+H9aBXUscKrHYW4F+tXrZDBC2vY0fBwzl+4SEiXYBwdHGq7OEII8dD46OM50gXgYaR7 U/O2qij1lSj1q24Ceaeax3uhUCjuqkx1WX0LL/QVBpSUFVdLfq5WNRdkehjdqgnoneTk5rI/LIyU lFTs7Ozo2CGoyhpHIarLkbB9tbr+C5GRhB+PoLi4GE93d9rf56j69ys2Lo6j4eEMCQ39V9e7YvVq zEzNCOkSXKFlnRA3C2zR4rbdpYQQoi6TAIAQ1USho4eLRXOiM8PvnPgu1LUAwP2wMDev0I9MiLrA y8MDLw+PWlt/QxeX+xrJ/UE9NnTov75OIYQQ4r9GXvIsRDXq2GhEteRjoGdMa+f+d04ohBBCCCGE EHdJAgBCVKNgt9F41+t854R3MMz7TexMZIArIYQQQgghRPWRAIAQ1Wx84GcY6Zvf9/JN7NoR4j6u GkskhBBCCCGEEBIAEKLaWRs5Mrn11xjfRxCggUVTngr8Ah0e7K0NQgghhBBCCHEzCQAIUQP8Hbvz QY/d+Dp0vav0ujoK+jV5jvdC/sTWxLlmCyeEEEIIIYSok+QtAELUECsjB6Z1+JW9l5exP3Ylcdmn uVpaUCGNpWE9Gln50r/pCzS2llcWCSGEEEIIIWqOBACEqGGdXEfSyXUkGo2apLwoYrJPYaRnhquV H5ZG9Wq7eEIIIcRde/q55ylRlaCnp4dDPXsaNmzIoAH9sbKyqu2iPbLem/0Bp8+cQaUqoU3rVgzo 2xcf7+ba+efOX+DL+fNJSk4hsEUAr0x7CUNDwwp5THv1dT6Y+S7bduxk9dp1pKWn08TLk86dOhHa vx8A2dnZLFr8C0eOHkWt1hDcqSODBgzA0dGhyrLl5+ez6OfFHD9xgrS0dBwc6hHcsSODQwdiYmLC pcsxfDD3IwB0dHVo4ulJULt2tGvbpgb21H+LuqCAqL59AdBRKtGrVw99BwdM2rbFasiQWi7do6s0 PZ3Ed98lf+9edJRKzHv2xPHtt9E1MtKmSVu4kPzduyk8fhwjX1/MgoOxmzIFdHRQxcWR8vHH5O/f j8WAAdR7+WUUZmYApHz2GblbtuCxdSvoPLrddSUAIMS/REdHFydzT5zMPWu7KA+lGy+EDX/4AQN3 dwAyly0jfeFCDJs1w+Xbb2uziI+cvJ07SZ43j+KoKAw8PLCfOhWLa/sYIO6ZZ7h69myFZQw8PGj4 /fcAZG/YQNq1fW737LNYDhgAgEal4kKHDtg9+yw2Tz75L23Nw0GO0+pVePw4qV98QWFEBGXZ2ejX r4/VsGHYP/88OgpFlctlb9hA6pdfooqLw7BZMxxefhnTTp208y+PHElJUlKFZYxbt6bBvHkVpsWM G4dF794Yt2rFlZde4uqFC2hKSzEJDMSkXTvsp04F3cq9JWOeeAJVXFyV5XP57juy160jd+vW8gk6 OijMzTFs0gS7qVNROj+6Xb3Cj0fw+svTcXNzJSUlhYN/H6bf4KHMfX8WHTsE1XbxHkn+fn6MHjkC hULBX9u2M/2119m8YR16enrk5ubx1JRnGNivL89MnsycefOY9cGHzHl/pnb5CxcvkpqWipGREXa2 tkx74Tns7Oy4GBnFu7Pep0F9JwJbtCAlNRUdYMZrr6FU6rN4yW+8/+Fcvv3qi1uWKysri2GjRtOm VStemfYSdra2xMbGsXrdOszMzAgd0J/CwgIiTp7kp/8tRK1Rc+7ceV569TUWLfyO5k2b/kt78NGk KSsjb88eoDwAoFGptPNMg4Jw/+MPFBJYuyea4mIuhoRQdPIkZl26oCkrI/nDDyk4eBDPXbsAyNm4 kbinn0bfwQHLwYPJXr+erFWr0KtXD6shQ0ieM4eCw4dx+d//iOzZE2X9+thOmoQqPp7Ed9/FY9Om R/rhHyQAIIR4SNx4ISzLy9NOV8XHk7dnD+qiotoq2iMpb+dOLnbrhsLUFIuBA8nbto2ofv1wXbYM 6xEjACgMD6fg77/Rr3e9JYqOvj5Q/qB7edQoHN96C9RqLo8ahV9KCrrGxqR88gnq4mKsR42qlW2r TXKcVq/8PXvIXLEC0/bt0TUxoeDQIQoOHkRdWIjjjBm3XCZrxQoujRiBnrU1Fn37kr1xIxc3b8b9 zz8x79YNgIK//6Y4NhZ9OzvtcvqOjhXyUV25QsbixdSbPp2ShAQKT5zAuEULStPSyFq1iszly9Gv Vw+rxx6rVIbCiAiuXrwIlN9wwrVz51qwoCw/n6vnz5O3Zw96dnYYuLlReOwY2b//TtGpU7hv3vzg O68WNW7shp+PDwA9u3fHvXFj3vtgDpvXr8XAwAC1Ws2ylauIOHESCwtzhg8ZgqdHebDs4KG/yc3L RUdHh9179tGiRQB9evbE2Li8du5/Pyyia5dg/ty6jbT0dAb060vLgAAAioqKWL12HWfOnUOj0dAq MJDBAwege4sgzaPknxp6gInjx/HTL0s4ffYs/r6+bNn6F/Z2trw6fRpQ/vA+duIkXp3+EpaWlgDs 2rOX4GsBsBtr3us7ObEtKIidu3YT2KIFXp6eeHler4R4ZdqLdOnZm6TkZBwdKrcC+Gbh/2js6sYH M9/TTrO3s6NVYEtUNzys6ujo4OvjDYC/ry/Hjh9n67btEgC4B00OHULP2prsDRtImjWL/LAw4l96 iUY//wyAOj+f1G++oSgiAh19fUzatMF20iTtNTvhjTdQFxVh//zzZCxejCo+HpsxYzDr0gUo/03M XLYMo+bNMWzShPSffvQ+iWwAACAASURBVMLQ3R2b8ePRv/bdZ61ZQ962bZSkpKBraIhhs2bYTZ6M nr19reyT+5F/4ABFJ0+iZ2OD5/btaMrKOGFjQ97u3eTv3Ytpp05krVkDgN2UKTi+8w5KFxcSXn+d 7LVrsRoyhLxduzDr3h2TVq0wbNKE3J07sZ00iSsvvYRlaGiFYPOjSgIAQohHTvJHH3H17FlKs7LQ s7HBtH17bJ54Ah0DAwCuTJuGRq3G/rnnSFu4kNK0NFy++46E118HoN6LL5L61VdoSkrQd3SkJDUV +xdewMDVFYC0BQu4ev48NmPGYNyyZa1t54NIW7gQNBrspk6l/pw5ZCxeTMzYsSTNmqUNAPzDNzm5 0vIFhw+jLijAol8/UKtJmDGDgsOHMXBzI2nOHDz+/BMdPbmE3I4cp3dm1qULPpcuoe/kBMDl0aPJ /O03cjZurDIAkLZgAQCOb7+N/YsvkvLxx1x59VWSZ8/WBgAA9Gxtb3ls/yNnwwYM3Nww8vZGU1KC z+XL2nkxTzxBxpIlZK1de8sAQLOTJ7V/R1haUpaTg9uqVVgOHHi9nNf+t+zfn4aLFnH17FnONG9O zpYtqK5cQdmgwZ130CNi2OBBfPL5F0Rfukyzpk2Y/eFHRJw8ycTx4zh95gxjn5rI0l9+plHDhpw6 c4ZV/2fvrqOjuNoADv92Y8SFJEiCJRDgw93dHQJF2kKBQktb3KEUdystLVSgtEiR4F6gQAgOxR1C EogR27huNt8fGwa2CTUCIeR9zuGczNzZu3OHmZ0775XZtp06tWvRonlTNntt44GvLxPHjgFg5569 /H7cm7atW1GkSGFGj5/ID98up6yHB7FxcWjT0+nZvTth4eH89MtaoiIj+WjQh7l7AHLAk7AwIiIi OHj4CCWKFaPi//4HwL37DyjtXlrZzt3NjfT0dPwDAqj6XABg7szpyjYJCQmEPnnCrdt3uHT5MvPn zMr2O/38AzAzM8PWxjbb9HPnL/DRhwOzTTM1NVX+zsjIIDgkBJ1Ox63bd7h85So9pQv7v2ZaogTO w4eT6u/Pky+/JGrjRkqsWkVGcjK3qlYlxdcXIzs7MlJSiFy3jqiNG/Hw9kZlZET4ypWkx8Sg2bIF XXw86XFxRK5ZQ7lz57CsXZukmzcJ++orjJ2cSI+ORmVmhi4+npj9+yl78iQAEd99hzYiAuNChUi6 cYOoTZuIWr+e/127pty33nTaqCgA/f6q1ah41rCRcOECVo0b49C7N5pNm9B4eWFSpAhRGzeiMjPD oU8fAGzatCHu6FFiDx8m+eZNnD76iNjffiP2yBEq3LmTW0XLUVJ7E0K8cTRbt5Jw7hwACWfPZkkP mTEDi2rVUFtYELNvH5E//0zCuXOUWLUKQP/QlJ5O9M6dpD56hKmLCxmpqYR99RWAst68UiXsOnUi 7KuvUJma4rpwIbqkJALHjSMjJUXf+p1HpWfeBJ+OeVNnjhdNvn0bXXw8aisrZdtblSqBWo1FlSoU mToVs9KlsaxXDyMbGzSbNpGh02Fka4tl3br49emDfY8eWDVs+PoL9YaR8/TlmVepYrBsnNndtUC5 ci/8jFLByzy3VZnndsKFCwbbpWs03KxQAbWZGRa1alHkiy8MHrqjd+7ErmtXfR4mJmSkppJ46RLJ 9+8Td/w46gIFcqSXS/L9+0T+/DNxmd1PTYsVU1rc3hZmZmY42NsTGBSIi0tRduzezc+rfqBKpUq0 a9MaP/8A1v26kS8m6YNbpqamzJ4+DQAnRycmT50GmQEAgJbNmzGgX18Abt68jc+p05T18KCQszMD +vVFp9MRGBRMj25d2bp9x1sRAFizdh3HjnsTHRPD4nlzMc4MsEZERODxXKu9hYW+m3+UJhqAkNBQ EhIScHdzU7a5eOkSy5Z/i39AAO/17sX/srme4uPjmT57DiOHfqb0vviz4JAQnJ2f9aI5c/YcPqdO AVClciXatGoFQHp6OiPHjAPA/9Ej2rVpbTCHgfh3bLt04cmXX5KRmkqqnx/RO3eS4utLgfLlKXfu HBnJydwoU4b4U6eI2bULO0/PZ5/t1Ini33zD48zAcsy+fVjWrq2kp0dFUe7iRcxKlOBqkSIknD2L NioKYwcH3Ly8MLKzQxsWRurjx9xv04bke/dIvHwZy7p1c+NQ/GtWdeuitrAgLTgY/wEDyNBqlXtG WlAQANYtW+L48ceEffUVAR99BIDTp59i07YtAIUnT8bI1pbQuXMpMn06dl27crt2bYrOnInK2Fg/ NFKtxqFXrzw7REMCAEKIN07o3Ll/mV45JAQjW1tSHj4k8cIFHvbuTdTmzcqD1VMWVatS9uRJ1BYW BuutGjWi2JIlYGxMRlISoQsWEPnLL7jMmUPM/v3o4uOx8/TE2NExx8v2ulg3b07skSOEr1hBRmoq Gi8vJS01OJgCHh6gVmNZuzZGDg7EnzhB0rVrxP7+OxVu3sTIzg73HTv0PQkA9+3biTt2jLjjx6l4 9y6JFy8SvXcv1k2aYN20aZ4fD/dfyHmas2IPHiT8++8xKVLkha3/oD+3k65dI3T+fNKCgoj85RcA dImJpEdHY2RnB8bGWNaqhdrCgjgfHxIvXyb+xAn+d+UKKjMz0qOjifP2psi0aUq+aWFh3KlXT1ku +MEH2GQ+4LyMeB8f4n18lGXn4cPfut4z6enpxMTG4ljQkUePHmNqakqlCs8eAGvWqM65888CNDWr V1P+Ll7MlceBgeh0OqUrf83qz96KU6yYK74PHwKg1WqZ8PkUfB/64eTkiAoVkRrNqy7eazFx7Bgm jh3Dzdu36TdwEGt/WkWF8uWxtbElLCxM2U6r1RKl0WCdGcQ97n2Cpo0bGeTVpFEjmjRqRHRMDCNG j+Xb775n1PBhSnpCYiJDhg2nTu1a9OnV84X7VNDBgZiYGGXZzs6OUiVLcuToMZKSk5UAgLGxMVt+ XQ9ASmoqU2fMZNa8BQa9EsQ/pwSU1WpMihYlMbPHkU2LFvrJ6KytsaxVi9gjR0i6fRu75z5bsG9f VCYmmJUpA0BK5lClpyyqV8eialUAjAsWJC04mBRfX4wdHAhfuZLwH34g1d/f4DNpwcGvpJyvgomL C+7btvFo+HAif/4ZExcXTEuWJNXfHxMXFwACR40ibPly7Hv2pMjkyYQuWqR/qNfpKL5yJSaFC1N0 1rNeMyGzZ6O2sMDp44+5XrIkFjVqkKHV8mTJEirev59bRX0peXvQlBDirVT8m28oc+AAZQ4cwHHg n7of6nQEfPghVwsW5Ia7Ow8zu7Pr4uNJj4012LTQ6NGYurpi7OBgsL7whAkYOztj7OCAiYsLdt26 oQ0LI3rXLjRbtgBk/d48ptDYsTgPH056fDyh8+YZRKlNM2+CZX77jXLnzlHmwAEq3L6NytiYtOBg 4o4eBfQPWm6bN+O2eTNWDRrweNgwXGbPJs7HhzsNGpCRmsrDd97hydKluVLG3Cbnac6JXLOGB506 YWRvj8eRI8rkitlxmT2bgv37ow0LI3TePGVsv9rSUv/wD5T/4w/KnjpFmcOHKZdZmU6+c0fpJRCz bx9G9vZY1a+v5Gvi7Ey506cp+fPPmFeoQOQvvxAwePBLl82ua1fKX76M25YtGFlbEzhuHDEHDrx0 vm+S02fOolarcXd3w9bWluTkZCIjo5T0oKAgrK2f9TpS/8UEj/r07KunBw8dJiExkZ1em/lxxbd4 du0CGRk5U4g3RIXy5alcqSJnz50HwNWlKA98HyrpAY8eodVqlZn7j3p7K+P//8zO1pa2bVpzJjMv gMTEJD4dPhL3UqWYPH7cX+5L+XJlOX/xD4Pld7p74uFR5oWfMTM1pWH9+ly6fPnvCyuySLp2jcg1 awCUuVGezmWSEhCgbJcaGAjoH+KfZ5y57YsmUTV+bl6U57dJCw4meMYM0iMjKXf2LFUiI5WeShl5 7BqzaduWivfuUT0lhYr375Mere8tY1GzJgDRe/YAUPD99zGvUoWCffW9jWJ/+y1LXqn+/oTOm0fx FSuIP3WKtNBQis6aRZGpU0l58EDpBZjXSABACPHGsaxfH5u2bbFp2xazsmUN0jReXmi2baNAuXJU vH+fcqdPP0v8003KzN092/z/vN55mL5lJOzrr4nZuxeTokWVrmB5lcrEhGJffUW1uDiqJSXhlNnN rUC5cqgtLclISzN4JY6xk5MyLCA9Pj5LfqELFmBkY4PjkCFoNm/GompVXObOxbZ9e6LWr389hXrD yHmaM4KnTcN/4EBMS5Sg3OnTFMgc+/xU/MmTxB48iDYiAtA/6Jdcs4Zq8fFUS0rCrls3AGUehIyU FIwsLZXPm7q6PpvcMvPcjt6xQ/9Wi+cmj1OZmmJZrx4FP/gAx8wH/9jDh1+6fMYODlhUrYr9O+9Q IHNStPjMMbd5XWRUFPsOHGD6nLkMGTwIWxsbXF2KUqpkSby2b0en0/E4MBCfU6dp2ODl3xAQHhGh tHzHxcWxdceOl84zt2k0GkIy56rIyMjg8pWrXLl6jf9lnitt27Tm/oMH+Jw8RUpKCus3bqJq5cq4 urgQGxuHr+9DqlerquR38/Zt5e+IyEj27N1Hhf/p80pKSmLoyFEUc3Vh2pTPUf1Nz63Phgxh9959 7Nn3bNJKnU5HTHTMCz/jHxDAnn37ada0yb8/GPnY46FDueHmxq0qVUi+excjW1tKZPbAs+/ZE9Rq 4g4fJmjCBB598gnJd+6gtrTEtl27HPl+XVISGWlpZGi1pD5+TMiMGaT9xRwqb7LA8eMJ/+47wr// nntNmpAeHY11ixZYN9GfkwUye0eELl6MZvNmQjPfDvO018TzHo8ciX3PnljVr4+ZmxuoVKQFBpIW GIjK2FiZkyevebv6oAkh3npPW0/TExL0k9osX/7SeVo1aoRF1apKN92/ewVZXpB08yaRP/2ERbVq JPzxB1Hr1gEo3drSgoK427QpDu++i0mhQsTs3Ut6dDRqc3OsGhl2J03x8yN0wQI8fv8dlZERZu7u JJw/T0ZaGqlBQdneNPM7OU//mfDvvydk5kwA/UzMmV3/je3tKb5yJQABgwaRfPcuZQ4exKZNG/24 1717Ma9YkfiTJ4nasAGAopn5JF69in+/ftj37ImRnR3R27aRkZamfxCvVQtdcjIxBw/itnmzsh+B 48eTHhODeaVKpIWEEJk58/bTrrIvI+HiRYImTiT57l0SzutbYi2qVfubT73ZPvz4E9RqNQ729ri6 uvDFpIlKN3S1Ws3cmTMYO3ESO3fvIUqjwbNLFzp3aP/S39u5Ywd+P3aMLj16kpiYyDue3fDz83/p fHNTWEQEH33yGQUKFCA5JYWUlBQ+/fgj6tXRj9suXqwY48eMZuykyRQwM8PW1pZvlul7XfmcPkW9 unUM3oIwf9Fi/AMCsLayJjgkhIYN6jNy2FAALl66zB+X9f+ef6j/7pvlyvc9z6NMaX5Y8Q1zFyxi 3qLFODs5ER0TQ5nS7kqeAGlpadRqoP//d3UpSr06deifOYeD+GcSLlzApFAhLGrWxKpuXYpMnaq0 1lvWrUuptWsJHDv22cNq6dIUX7EC0xIlcuT7zdzdKTx+POHffsvDnj2x79kT84oVSbpxI0fyf50S L13iyaJFgH4yQIf336f4N98o6cW/+w7//v2JP3GC+BMnAH1vi+LffmuQT8y+fcSdOEHFu3cBMC1Z kkKjRuE/aBAqlYrCn3+ep96Q8DwJAAgh8hSH999Hs307cYcP8/Cddyg8eTJxv//+0vk6DRtGwIf6 iaQKDhjw0vnlOq2W8O++Q5eYCIBJoUKUWr8e+x49AFBbWWFSuDCh8+crLdJmZcpQ/Ouvs0S0Hw8f jkOfPsokQI6DBhHv48NVZ2dMixfH5W/GwudHcp7+M+nPjd9+OkkegEnhwkoA4M8yUlIIW7YMXXIy oA8clPr1V6V1x9jREbW5OSHPjeE0r1yZ4itWYFywIDF794JKZfDGALW5OWHLlpGRlpa5Qo1tp06U yHzjwMtIunaNpGvXUFtYUKB8eZw+/li5DvOi8ydP/O02/ytfjv27dhAREYG1tTVmz80g/udZ5e3s 7Lh64Vk32v27DFv1Bzz3IFnQwYH1a35Co9Fga2uLWq3O8xMAli1ThmOHDhIRGYlOp8PJ0RGjPwX2 enTrSrfOndBER+P4XJfvY8e9lXH4T637aTWxsXHExcfhYG+P+XM9vRo1qG9wrP+JqpUrs2XDOlJS UojSaHBydFQmKASoWKHCv85T6BnZ2FDjH3avd3jvPRzee4+0J09QmZhkGTJWNbOb+1POI0fiPHKk suw4cGCWIWOVHj0yWHaZN48iU6eSkZqKkW32b4bICzyOHEEbFYUuLg4TV9csgXIzNzfKnjiBLj6e tPBwjB0d9XMr/IlF9epUuHHDYNiE65IlFJ0zB3g2uXJepJq/cE7GhHGTczxjjSaSK9cvUbVS9Sxp GRkZXLt5lbS01Gw++eZzsHfAreSLxycK8aaJi4/lvu99WjRtndu7kmO0UVEYWVmheu5VRC8j4exZ 7tSrh3WzZnhkjoHP6zLS00kLCUFtZmZwA3ueLiEBbXg4Rra22c9mm5FB8t27mBYvnmWSunSNJs/O gPu6yHn6amSkpenPbSurLBXhp9Lj4vSvtCpYECMbG2V9wKBBymv7DPJMSSEtc7I1k0KFcuz/TIhX JTw8Ant7O4MHciGE+CsLFs3NnR4AKpWKEsVKEBgcSHJyUm7swn9mY21DkcIuub0bQuR7L6r0/xf3 mjcn4fx5VMbGFFu2LMfyzW0qI6O/fde42tIS0+fGS2fNRPXCV7LJw//fk/P01VCZmGBavPhfbmNk bZ1tq07RWbOyBLNA31XUtFixHNtHIV41J6e34w0gQojXK9dChna29tjZSuVRCJH7LKpWxap+fWy7 dMG8cuXc3h0hsiXnac54+tYAIYQQIj+SPkNCiHzPNZ++xk7kLXKeCiGEEOJlyWsAhRBCCCGEEEKI fEACAEIIIYQQQgghRD4gAQAhhBBCCCGEECIfkACAEEIIIYQQQgiRD0gAQAghhBBCCCGEyAdeaQAg I+NV5i6E+DdUub0DQgghxN/QarWEPnmS27shxFtBrieRnVf2GkArKxtUKrh4+fyr+gohxL9QpHDR 3N4FIcQ/oNVqiYiMpHChQrm9K0JkMWTYcMaPHoVbqVKvJP/HgYH0HTiIk0ePvJL830R79x/g0uUr BDx+xMAP+tGgXj0l7dyFC2zZuo179x/gVqokLZo1o2P7dqjVz9rw0tPTGT1+AksXLuDO3bsc/v0o d+/fp2yZMowcNjTL9+3cvYfde/cR+uQJpUqWZMmCeRQoUACAlNRUflm3nvMXLxIcHIKzsxP16tSh Z3dP7O3tiY6OZsyESfqMVOBWqhT16tSmWZMmqFTS1PBvyfX0auzZt5+de/YQEhJKiRLFWTxvLpaW lgD8cfkyK3/4kcjISOrVqcPoEcMxNtY/Env7+LDJayslihdnyOBB2NnaAnD9xk02eXkxZ8b03CpS jnplAQATExOaNW71qrIXQryFhgwbTlpqGsbGxhQu5EyJEiXo1rkT9vb2ub1redakL6YSFhauLJcs WYIvJk1Ulm/fuctX33xDSOgTalavxrjRo5SK4LkLF1j360acHZ345KPBODk5AvDQz5/lK1bw5aKF r7cwbwipsOW823fusm3nTi5dvkJZjzK0b9uWRg3qG2zz08+/UKliRcp6eLB91y5u3rpFVJSGr5cu Vip2T63b8Ct79h/A1NSEXj160KlD+yzfeejIETZ7bct2f8zMzFjx9TLGTpyERhMNQCFnZ+rUqUXL 5s2xtLDIoZLnPZcuXyEuPj63d+OtcvnKFYoUKcyFPy4SHhFpkHbz1m0aN2rIsE8/wc8/gAWLl2Bt bU2zJo2Vbf64dBmtNh0jIyPu3ruPTqfDytKKe/cfZPmun9etZ7PXVvq99y4NG9Tn/oMHyoN7SkoK vfv2w6VoUYZ9+glFCxchMCiIfQcP4rV9Bx99OJC0NC0XL11ixdfLsLay5r7vA+bMXwioaN60ySs9 Tm8juZ5y3oZNm1m34Vfef7cPTRo15IHvQ1QqfcAsJDSUz0aM4v0+valfty6z5s1j8bKvmDh2DFqt lmXLv2X9mtVs3rqN7Tt2MrD/B+h0OhYt/ZJZ06fmcslyzisLAAghxL916fIVJo4dg5tbKZ48ecKZ c+fp6NmD+bNm0qhhg9zevTzp+o2bvNenN2Xc3QEMHpRiY+MY9MmndOnYgU8//pi5Cxcyc8485s6a QUZGBvMXLWHNj9/z+7FjbNi0SWlJWrB4CcOHfpor5XkTSIUt502bNZtGDeqzcvlXXLj4B+MmTebQ 3j3Y2FgDkJGRweat2+jWpTOaaA1+/v6U9fBg+YqVpGm1Bnlt37mLNWvXMWXSBKKiNMxbuAhHR0fq 1altsF2VypVxsHcA4Oz58+w7cJBZ0/QVvKetq1evXaffe+9SpXJlQkJD+Hndeh49esywTz951Yck zzhz9hwHDx0iLU1Lk8YNadPqWeNPbGwc6zdu5IHvQywtLejetQtVq1QB9L9Ne/btJzomhnp1atO1 c6csLcinz5wlMirKIIBz9vx5QkOf0LVzJzZu3kLlypU4c/Yc/gEBtGnVigb16hq0jucFX0zWt6j7 nDyVJW3gB/2Uv0uWKMH9+/fZtmOHQQDg2IkTNG3cCADPrl0AWL9xE6fPnDXIKyEhgR9/WmNwTy3m 6qqkr93wKyqVmq+XLlGOoZOTI9WqViE1NdUgr/+VK4e9vT2VK1Xk4UM/Dh05IgGAHCDX08tJSkri +x9XMf2Lz2netClgeI7v2befsh4eDP1kCABjRo5g3KTJjBo2lId+/hQuXAhLS0tq1ajB96tXMxDw 2r6dWjVrUKJ48Vwo0ashAQAhxBvF3d2NKpUqAdCmVStKu7szfc5c9u/cjpmZGTqdjo1bvLhy9Rq2 tjb07N4djzKlAf2NMzYuFpVKxXFvH6pXr0b7Nm2wsDAH4IdVq2nerCkHDx0mPCKCzh07UKNaNUB/ 09i6fQc3b98mIyODWjVr4tmlc5668b1I+bIeSiXheQcO/YazkyPjx4wG4PMJE+g/+CPGjxlFYlIS 5ubm2NnaUqtGDXbt3gvAwUOHKVbMlQrly7/WMrzJpML2ciKjorh77x4/fLscOzs7OrZvxzcrv+OP y5eVh5xbt29TuHBh7O3tsbe3Z8YXU4iOjmb5ipVZ8tu6Yyd933tXqfzduXePLVu3ZgkAFHJ2ppCz MwBh4eEUKFCAmjWqZ8mvZIkSVK5UkcqVKqLTZfDtd99LACCTt48PU6bPoH/fvthYW7Nw6TKiojT0 6dWTpKQkevftR6mSJenZw5OMDP3/NYDPqdPMnjeffu+/R21nZ1b//DNBwcFKpfwpJycnJkyZQuuW LTAzMwPgm5Xf0atHDwAO/f47W7Zto2WLFtSrU5u5CxYyadxYGjdq+HoPxGvkFxCAs5Ozwbrj3icY 2K/fCz7xzN1799FqtWiioxk2egxmpqb06dVTuQ+eu3CB9m3bZPv7YWpqarAc+iSMpORk7j94gLfP ST79+KOXKJUAuZ5ywgPfhyQlJxMfn8Dw0WMxMTGmV48e1K5VE4D7Dx5QprS7sn1pd3cSE5MICg6h TGl3goNDiImN5czZs9SqUQONRsO2HbtYu/rH3CrSK5F3aghCiHzpHc9uREZG4vvQD4DZ8xawbcdO mjdtgpmpKf0HDcY/IACA6zdvsvjLrzhx8hQtmjfl0OEjfL1ihZLXzj17+XzaDCwtLalcqSKjx0/k 7r17AMTGxaFNT6dn9+40a9KELVu3seqnNa+9vK/Cl8u/ZfCnn7Fo6ZdERWmU9ffuP6C0e2ll2d3N jfT0dPwDAihapAiJiYlEREZy+uw5ataoTkJiIj/9/AvDP82/rf9/5u3jw/jPP6d48eJUq1qFhUuX sXHzFkAfVOr1fl9u3rpNl04daNGsmUGFbezESZQoUZzWLVuwZds2vv3u+yz5Ozk5sXDpUlJSUpR1 36z8DiMjI0BfYZsybTopqalKhe3kqdOvoeQ5p6CDA/Xq1uG7Vat54OvLxs1bUKlUSoUN4Jj3CYMW z7/yIJsK3gNf3/+8f5EaDcEhIVy8dImt23fQ9B/uR37wy7oN9PD05MP+H/BOd08+/nAgq37+BYDd +/aRkprKN8uW0qRRI5o2bkSLZs0AWPnDjwzs34/3eveiZfNmTBgzml8zr5vnlSntjlspN347rB8O c+/+AwIePaJ1yxbKNjWqV+ezjz+iQ7t2dO3cCZ/Teev8/zeOHj/O6bPn+PTjwcq6u/fuYW9npwzR +ishoSHodDp27d1L73d6UNbDg48/G6YMFQgODsHZyUnZ/sbNmyxcspSFS5aydfsOg7ymTJ/ByDHj mDRlKm6lStGgfj3Ey5Hr6eWFhIYAsGP3bnr28OR/5cvz6YiR3Lp9B4DwiAhKliihbF+4UCHMzMzQ RGswNjZm3OiRTJ81m+SUFLp17syXX3/DkMGDSEtLY92GXzlz9lyulCunSQ8AIcQbzczMDAd7ewKD AnFxKcqO3bv5edUPVKlUiXZtWuPnH8C6Xzcq49pNTU2ZPX0aAE6OTkyeOg3GjlHya9m8GQP69QXg 5s3b+Jw6TVkPDwo5OzOgX190Oh2BQcH06NaVrdt38NGgD19/oXNQn149cStZkihNFDt27+GDQYPY smE95ubmRERE4OHhoWxrYWGOk6MjUZljnqdMmsCCxUv0x+aDfqz84Ufe7d0LY2NjNm7xokjhwjRp 1DBfT/z0fIUN9F3Vv1/9E3169WT3vn2kpqXxzbKlWY7R0wrb05YXx4IOfDpiVJYWm+crbJ07dvjL ChtAYFAwPqdP56kWG4AJY0Yz6JPP2HfgAMnJKXy1eJHBOPujx71ZunD+3+YTGxtHSmqqQQWvZIni yjn9X/ywajW/1eT1/wAAIABJREFUbtxEZFQUNjY29Ozu+Z/zetv4BfgzaEB/ZblmjRrMWbCQlJQU /Pz8qV+3Tra/D74PH/LLug38ukn/kJKRkUFaWhqxsXFZtu3VozubtnjRuWMHtu/cScf27ZXWS4Ca 1Z/12ijm6srFS5dysIRvjjNnzzF91hyWL1uKo+Ozh/1j3if+cVDKwsKS1NRUxo4cSflyZWlQrx6n zpzhwG+/4VGmNAULOhATE6Nsb2VlRamSJTl/8SL+AY/o4dlNSVu18lvs7e3RarV8+fVyRo+fwOrv svbIEf+cXE8vz8LCgtTUVEYNG0blShVpWL8+Z8+fZ//Bg/yvfDlsbWwICwtTto+NjSMlJQUrKysA GtavT8P6+vlnrly9SlS0huZNm9C7bz8G9OvLjt17CAkNVYba5FUSABBCvNHS09OJiY3FsaAjjx49 xtTUlEoVKijpNWtU59z5C8+Wq1dT/i5ezJXHgYHodDqlS6PBza2YK74PHwL6mdcnfD4F34d+ODk5 okJFpOZZa3le9V7vXsrfjRs2pGX7jpw6c5aWzZtha2NrcCPUarVEaTRYZ94Ia1avrhyvB76+3Lx1 izEjhjNoyKd07tQBbx8f/Pz9lYBKfiQVtpcXHRNDz/f6Mn/2TJo3bYqffwDvDxjI8i+XUKNaNR4H BpKenm7wUP8iVlaWqNVqwsLCcXVxASAsLFw5p/+LSePG0qhhAzIyMth34CDvfjCA44cOKrNG52d2 trYEBQcry0FBQZiZmmJqaoqtrS2379zN9nNWVlaMHjGcls2bZUmLjDKcBK9Vi+YsXPol12/cZO+B g/yy6geD9Lw03OW/On/hIhOnfMGSBfOVIXJPHT3urQS9/07RIoUBcHZ+1srv7ORMQkIiAOXLluP8 H3/Q9713Af3wl5IlSpCSmpplPoGnjI2NadKoEZu3biM9PV3pnST+PbmeXl7RIkUAKFTo2TAZZycn EhL157iriwsPfB8qaQ98fVGr1VneuqPT6Vi87CvmzZrJ48BATExMaNOqFa6urqz8/oc8HwDI2//L Qoi33ukzZ1Gr1bi7u2Fra0tycjKRkVFKelBQENbWzyr36r+pfKiNsv/ZO3joMAmJiez02syPK77V /7hnZORMId4QVlZWONjbExenf8h0dSlqcCMMePQIrVZLkcxK4vPmL17CxLFjiImNJUoTRZeOHfmw /wccPX78de3+G+nvKmyPHj3O9nNPK2y7tm5h19Yt7N7mxYVTPsqkd89r1aI5jwIDlQpbj25dDdLz eoXt8pWr2NvZ0aJZM1QqFW6lSlKnVk0lsHfsuLcywdnfUavVFC1SxKDL/wNf32zP6X9LpVLRrGkT 4uPjs51dPT9q1KABBw8dJiY2lsTEJHbs3kOD+vVQqVQ0bdyYG7ducf7CRWX76MzW5ZbNmrJ+40aD IUlPh3L9mYmJCd06d2L85M9xd3PD3c3t1RbqDXPx0iXGTZrMgrmzqVWzhkFaSGgo8fHxyjw4f6es hwdlSruzbcdOdDodISGhnDpzhmpV9fOSDBrYn4t//MG6Db+i0+mUz2n+IhgeEhLK1h07adywoTz8 vyS5nl6eW6lS/K98OeUcD33yhJOnTlMtc+6ddm1ac+3GDS5c/IOEhAQ2eW2lYYP62NrYGOSzaYsX 9evWpZirKwUdChIXp5/4Nzw8giKZQYa8LG/XGoQQb63IqCj2HTjA9DlzGTJ4ELY2Nri6FKVUyZJ4 bd+OTqfjcWAgPqdO07DBy78hIDwiQmkljIuLY+uOHX/ziTdfdEwMwSH68XCpqals8trKk7AwpbLX tk1r7j94gM/JU6SkpLB+4yaqVq6stJw+te/AAUq7uVHWwwNrKyvS0rTodDr9jbBw3r8RvgypsL28 WjWqExMby+UrVwH9tf/Hpcs0zpyl/NiJfz7+H/QVvL37D/AkLIyHfn4c8z5Bx/btXno/Y2JjWbfh VwoXLkS5sh5//4G3kFarRavVYpX5NpGPPhyIuXkB2nbqTLsuXQmPCGfS+HEAlC9XlikTJzBy3Hg6 eXanQ1dPZRz5iKFDcXVxoXXHTvTu2482HTuz8ocXT7LVw7MbIaGhWYJfb4vPRoyiSq06XL1+nWkz Z1GlVh0luLp2/a9Ex8Tw8WfDqFKrDlVq1aFDV/0wlOPeJ7IEx3bt3UuVWnVYtPRLTp05Q5VadZjw +RQlfeHcOezdf4BWHTrR0bM7nTq0p21r/cSlzk5OrPtpNUe9vWnQrDld3+lFi7btOXv+Ah/0fc/g e1p37EytBo34cMgnWFtbMfwzmRjz35Lr6dWYP3sWh3//nVYdOtGhqyetWrZQJtKtWKECH/b/gM9G jKRd5674+/szZeIEg89HREaya+8+Pszs3WdhYU7rli34bMQo1q7fQPc83voPoJq/cE7GhHGTc3s/ hBCC2g0bK931HeztcXV14f0+fQwqOLdu32HsxElKd3XPLl2YOG4MarWaH1b/REhoKNM+1/+mRUdH 06RVGy6fO4NaraZ9l27Mmz1T6UK5Zu06fB8+ZPb0aURGRTFizFji4uJJTEzkHc9ubN66jd8P7s+V Y5ETHvr50e/DwVhaWJCQmKhMcNOhbVtlm607drJo6ZcUMDPD1taWb5YtpXixYkp6fHw8Az8awk8/ fKeMkVu7fgNnzp8nOTmZYZ98QvVqVV972XKLVquldsPGeP26Hnc3N+Lj45k45Qv+uHwZU1Mzihdz ZcmC+cpEWjt27WbRl8so6GCPTpdBty6dGTSgP4mJScxduJCDhw5T2t0NjSaaqlUqs2DObPz8/ek7 cBAnjx5RvjcoOJgOXT2ZNW2qwRsBBnz0MX169lTmBNh/8Dd27N7Njyu+fb0H5iVt3rqVzV7bSElJ IV2XTstmzRg7aiTR0dF07/0uh/fvVXo6pKenU71u/Sx5PL3Ok5KSmPD5F1y8dAmtVku7Nq2ZOnnS X7ZO7j/4Gz/+tIYdWzYZrG/VviNRGg1qtRorK0tq16xJ965dDSYozE9u3rrF0FGjOXrwgMHQlvj4 eLRaLXZ2dlk+o9PpeBIWhpWlJdbWhj1cUlNTiYiMxMHengIFCrzwe69dv8Gw0WM4tHe3wfCX/G7w p58xeMCA/3Q+RkRGYmVp+cLjnpaWRkREJAULOmR5A4DIGXI9vVqRUVFYmJtjbm6eJS0tLY24uHgc HOyzpMXHx5OampYlLSExEQtz8zw/79GCRXMlACCEyJsiIiKwtrbO8ZuXRqPB1tY2z3erfkqn0xER GYlapaJgwYLZ3rjS09PRREfjWLBglrTExCQSkxKzpCUlJWFmZvbWHKd/Sipsr1Z0dLTBMdy5Zy+X L19mxtQv/nVecXFxGBkZK68BFS/n+1Wr2bjFiy4dOzBq+LDX8p0ZGRls2uLFtp07ea93b7p16fxa vjevCH3yBGcnp3z3O/w2kOtJ5JYFi+bKJIBCiLzp+VmQc5K9fdZocF6mVqsNXuuUHSMjo2wf/kHf 9S27B6jsIupvu+crbH8OpFj9xSRzarWaIoWzH4NuamqqTFqUnecrbCOHfvZWP/wDWQIozZs0oVU2 E1v9E38OtoiXU75sWTavX0shZ+e/3ziHZGRkEBMby4ihQ2kor5nL4s8Tl4m8Q64nkZskACCEEEL8 A1Jhe/2ymxRR5I7ceLWkWq1myOBBr/17hXjV5HoSuUkCAEIIIcQ/IBU2IYQQQuR1MmhICCGEEEII IYTIByQAIIQQQgghhBBC5AMSABBCCCGEEEIIIfIBCQAIIYQQQgghhBD5gAQAhBBCCCGEEEKIfEAC AEIIIYQQQgghRD4grwEUQuQbWq2WiMhIChcqlNu7IoQQedKQYcMZP3oUbqVKKet2793Ho8ePGfrJ kFzcs7ztyrVrbNy8hdt37uLq4kKL5k3p3KEDJiYmBtuNHj+ROTOmoVKp2LBpMydPnyYjI4NGDRrw bq+emJubv/A79h08yMFDh/H398faypr//a887/bqqfxfTv5iGk/CwgBwcnKkbu3atGzeDCsrq1dW 7vxOrqdX46GfH3PmLzRY16dXT1o2b6Ysb9i0md1792FsbMw73T3p2qkjADqdjjVr13H67FmaNWnC +316K59ZteZnnJ2c6Nyxw+spyCsiAQAhxBsjuxthTnocGEjfgYM4efTIK8n/TXbv/gMWLFlC544d 6NKxo7L+j8uXWfnDj0RGRlKvTh1GjxiOsbH+1uDt48Mmr62UKF6cIYMHYWdrC8D1GzfZ5OXFnBnT c6MouW7IsOGkpaZhbGxM4ULOlChRgm6dO2Fvb5/bu5bnBQUHM23WbJo3acK7vXsZpG3euhVnJ2fK epThi+kzDdK6delMx/btDNbpdDoGf/LZC7+rf7++xMfHs3X7DgBMzUypVrkyzZo2pUxp9xwq0dvn 0uUrxMXHG6wLCQ3l/oMHubRHb4fbd+5Sq2ZNPh40iOCQYBYtXYaRkbHyUAJw9949wsLDMDc3Z+v2 Hfx26DCzpk8FYOrM2ViYm9OnV89s8586YxZXr19nzIjheHiUITo6mjPnzvP1tytYtngRANdv3qRL p47UrlmTJ2FhrN2wgXv37zN+zOhXfwDyKbmeXo34+AT8AwJYMGe2sq548WLK37v27mXVmjVMHj+e uLg4Fi5ZipNjQRrUq8elK1e4e+8+3y3/ms9GjKJ2zZp4lClNYFAQx7xPsHb1j7lRpBwlAQAhxBsj uxuheHk6nY6Zc+cSGxtHUFCwsj4kNJTPRozi/T69qV+3LrPmzWPxsq+YOHYMWq2WZcu/Zf2a1Wze uo3tO3cx8IN+6HQ6Fi39Uql05keXLl9h4tgxuLmV4smTJ5w5d56Onj2YP2smjRo2yO3dy7MyMjKY MXsukZGRPHr8OEv6Zq9tLFu8iOTkZG7fvcvXSxYraS5Fi2bZXqVS8clHg5Xl4WPG8sngQZQvVw6A kiVLsGffflJSUxg/ejQpKSmcOHWKwZ9+xm97dmFmZvYKSpk/7N1/gPMXLhKfkEBZjzL0ffddLCz0 LdM79+ylmKsLt27d5ur164wbPYpNXlvp1L4du/fuQ6PRUKd2baytrAyup98OH8bMrABNGzfKrWK9 Un16vqP87VaqJIFBwWzbscMgAHDM+wRNGzcG4NSZs7Rq2YKyHh4AtG7ZAp9Tp7MNAFy5epUDhw6x e5sXRQoXBqBwoUKUK1uW1NRUg22LFytG5UoVATA2Nmb2/AUSAMhlcj39N6amptSsUT3btG3bd/Je 7960atEcgHv377PJa6s+AHD5ClUrV8LExISqVSpz+eoVPMqUZsGSpYwdOQIjI6PXWYxXQgIAQog3 1pmz5zh46BBpaVqaNG5Im1atlLTY2DjWb9zIA9+HWFpa0L1rF6pWqQLoW6j37NtPdEwM9erUpmvn TqhUKoO8T585S2RUFJ06tFfWnT1/ntDQJ3Tt3ImNm7dQuXIlzpw9h39AAG1ataJBvbqo1Xlv6pSf 166jRrVqPHocaLB+z779lPXwULoZjhk5gnGTJjNq2FAe+vlTuHAhLC0tqVWjBt+vXs3AD/rhtX07 tWrWoETx4rlRlDeGu7sbVSpVAqBNq1aUdndn+py57N+5HTMzM3Q6HRu3eHHl6jVsbW3o2b07HmVK A/rzOjYuFpVKxXFvH6pXr0b7Nm2UCt0Pq1bTvFlTDh46THhEBJ07dqBGtWoAJCUlsXX7Dm7evk1G Rga1atbEs0vnPHle/tm2nbsoWrQIRYsWyZL2ODCQjAwdxYu58tDPD2MjoxdW7J5SqVQG25gYG+Ph USbL56ysrJQHnlo1a7Bn337OX/yDRg3q50Cp3k4hIaHYWFsryxqNxiD9cWAg7du1QavVsnX7DsZP nsw3y74EwOfkSW7fuUvD+vXp0rEj5gXM2bJ1G2fOnqVVixbUrlmTlNRUln79tfLAkp6ezsKly1g8 b+7rK2Qu8/f3x9nJyWDdMe8TzJ05HYB2bVrxw+qfqJZ53zt46BADP/gg27zOXbhIjWpVlYf/55ma mhosa6I1BIeE8ORJGJu9vGjWpHEOlEb8FbmeXo3omGg++nQoBQoUoHHDBnh27aLcK+89eMCHA55d L6Xd3fH2OQlA9apV2bhlC55du/DHpct8PnE8x7xPYGdrS7WqVXKlLDlNAgBCiDeSt48PU6bPoH/f vthYW7Nw6TKiojT06dWTpKQker3fF7dSpejZw5OMDIiMigLA59RpZs+bT7/336O2szOrf/6ZoODg LGPpnJycmDBlCq1btlBa+r5Z+R29evQA4NDvv7Nl2zZatmhBvTq1mbtgIZPGjaVxo4av90C8JD9/ f/bsP8Cmdb8wcYphq/39Bw8MujqXdncnMTGJoOAQypR2Jzg4hJjYWM6cPUutGjXQaDRs27Hrrej+ ltPe8ezG4i+X4fvQj/+VL8fseQu4cu0agwcO4MbNm/QfNJhf1/5MyRIluH7zJl7btlOndi1aNG/K Zq9tPPD1ZeLYMYC+Ref34960bd2KIkUKM3r8RH74djllPTyIjYtDm55Oz+7dCQsP56df1hIVGclH gz7M3QPwkp6EhbF2/QbWr/mJpV9/nSX9uPcJmjZpoiwnJSczZOgwjE1MqF+nDr17vvOfgyApKSkE h4SQnJyCt48PapVKCQiI7C375hvMTJ/1kIiOiaFq5UrK8tOeF6FPnvDB++8z8OMhpKSkKL+1bm6l mDxhnEGeHdu3V8ba6nQ65i9azKXLV6herSrePiexsbZ+ayrff+fCxT/Ys/8AG9f+oqwLCQ0lISEB dzc3AJo2bszJU2f4dPgIUKlo3bIFLZo1zTa/4OBgnJ2cleWY2Fi+/3EVoG/lHz1iuJL289r1bNu+ k0iNBgvzAowfM+YVlFA8T66nnGdvb8fo4cMpWaIEV69f5/tVq3kcGMio4cNISEwkKSmJkiVKKNuX LFECTXQ0ANWrVeX23buMnTiJ9u3a4FK0KBOnTOW75V/jHxDA4d+P0rplizzdECIBACHEG+mXdRvo 4enJh/31EdqMjAy+X/0TfXr1ZPe+faSmpfHNsqVZWvZX/vAjA/v3Ux7kHQs68OmIUVkCAGVKu+NW yo3fDh+hc8cO3Lv/gIBHj2jdsoWyTY3q1fns448ACAwKxuf06TwVANDpdEyfPZcJY0dn2505PCKC KpUrK8uFCxXCzMwMTbQGt1IlGTd6JNNnzaZUyZL07tmTxV8uY8jgQaSlpeG1bTul3d2pV7fOayzR m8vMzAwHe3sCgwJxcSnKjt27+XnVD1SpVIl2bVrj5x/Aul838sWkiYC+1W329GkAODk6MXnqNBj7 rKLdsnkzBvTrC8DNm7fxOXWash4eFHJ2ZkC/vuh0OgKDgunRrStbt+/I8wGAWXPnMfSTIdjYWGeb ftTbm5HDhgJgbWXN6BHDKe3mxo1bt1j360Z8/fyUY/tv3b17j5FjxpGm1RLw6BETx44xaI0TWS2Y M1vpAQPw/arV3Lp9W1n++tsVHD/hg7WVFfb2dqhUKqI0GqUFumb1rL03alavpvytVqvpnnluV69W le07d9LDs9srLNGb4+r164ydOInF8+ZSvJirsv649wmD7trTZ80hJTWFY4cOgkrFrLnzmDpzFvNn z8qSp0PBgvj6PlSWjY2NKVWypBJ4ez4AMGr4MOU+eOToMfr0+wDvw79RoECBV1FcgVxPr0IxV1eK ueqvn1o1a+Do6Mi8hYsYMfQzLMzNMTY2JiwsXHmIfxIehpWVJaA/Xn3f7UPfd/sA+sYhz65dSElN YdS4CYwePoyRY8ezcvlXeXZSaQkACCHeSH4B/gwa0F9ZrlmjBnMWLCQlJQU/P3/q162T5eEfwPfh Q35Zt4FfN20B9IGDtLQ0YmPjsmzbq0d3Nm3xonPHDmzfuZOO7dsbPCg/f1Mt5urKxUuXcrCEr96W bdtxcixIhfLliYuLQ5uuJTU1lcTEJCwszLG1sSEsc8Zn0A+rSElJUWZ8bli/Pg3r67tBX7l6laho Dc2bNqF3334M6NeXHbv3EBIaimfXLrlSvjdJeno6MbGxOBZ05NGjx5iamlKpQgUlvWaN6pw7f+HZ 8nOVs+LFXHkcGIhOp1NasQ3OvWKu+D7UV961Wi0TPp+C70M/nJwcUaEi8k/dRfOaA78dIjU1jXp1 ahMXF0dqahpGajUJiYlYWlgQHR3N48BA5Xg6OTkq46Vr1axBMVdXxk2azPgxozH7U3fmf6Jy5Up8 t1zf6yA6Job3+w/ExMSEbl0651wh85GAR4/YtXcf+3ftwMzUlKDgYI55n4CMZ9tk9zD553WeXbvQ ybM7d+7e5eKly8ydOTPLZ942N27eZPjosUz/YkqW4OpRb28+/vBZoO/chQtMn/I5lpb6h5bOHTrw +fTp2eZbvmxZdu3eQ3JyMgUKFMDSwoJ3unty99491q7f8ML9adywAWlpady6fYfq1aq+fAHFvybX U84o7upKSkoKaWlpmJmZ4VK0KA98falVswYAD3wfUrRw1uFnjx4/5tyFC/yy6kc2e22lXetWNGrY gFt37nD8hA+93+nxuouSI/L+oEEhxFvJztaWoOBnE9YFBQVhZmqKqakptra2PHqUdZIw0I/nHT1i OLu2bmHX1i3s3ubFhVM+2bYstmrRnEeBgVy/cZO9Bw7So1tXg/S8Pq7az8+PM+fO07ZzV9p27srp M2fZsGkz4ydPBsDVxYUHz7UKPfD1Ra1WZ4lo63Q6Fi/7igljRvM4MBATExPatGrFB33f4+jx46+z SG+s02fOolarcXd3w9bWluTkZCIjo5T0oKAgrK2fvUpL/TeTCKmNsj/3Dh46TEJiIju9NvPjim/1 wZeMjGy3zSt8Hz7k5u3bynl66MgRdu3dx6fDRgBw3OckDevXf+H1WKyYKzqdjsSEhJfeFztbW2pU q8ofly+/dF75VURkJGamppgYG6PValm/cdN/ysexYEEa1q/PqHETaNGs6Qt7h7wtbt2+w9BRY5g6 eWKWcfexsXH4+j40eAhv1LABh38/ilarJT09naPe3jRukP0kpC2bN6NQIWdmzJlL/HMT7UZpol+4 P3FxcazfuAl7OzsqVazwwu3EqyXX03/z0M+flJQUACIiIvjpl1+oVLGC0sjTrk1r9h44SOiTJ/j5 +3P02PEsb5IBmL9oCeNGjUKtVlO0aBHCIyMBCAsPp2g2c2rkFXm7diuEeGs1atCAg4cOExMbS2Ji Ejt276FB/XqoVCqaNm7MjVu3OH/horJ9dEwMAC2bNWX9xo1ERT1rFfUPCMj2O0xMTOjWuRPjJ3+O u5ubMrbybTFp/DhOHftd+de4YUMG9OurTB7Urk1rrt24wYWLf5CQkMAmr600bFAfWxsbg3w2bfGi ft26FHN1paBDQeLi9BXI8PAIihTJGjHPTyKjoth34ADT58xlyOBB2NrY4OpSlFIlS+K1fTs6nY7H gYH4nDpNwxdUzv+N8IgIrDN7aMTFxbF1x46XzjO3Df1kiMF52rF9O3p068ovmXNNHPf2Nngg8g8I ICkpCYCoKA0/rv4Jt1KlXvo1jDqdjqvXr3Py9BmaN23y9x8Q2apRrRr/K1+Odp270rn7O1haWCit 1P/WO56eBIeE0KPb291dGWDjli1oNBpGj59IlVp1qFKrDg2btwTA5/Qp6tWtYxAE6/fee0RGRdG2 cxfadupCSEgo/d5/L9u81Wo1K5d/jZGREa06dKRjt+6069yVGXPmMGbUCINtJ30xlVoNGtHRswe3 79xl1rSpmJiYvLqCi78k19N/8/uxYzRp1YaO3brTqkMn4uLimTNjhpI+oF9fnJ0c8ezZm17v96Ny pYp0/1Mj0JGjx3B2dlLmhKlbpw4hISGMGDOWJ2Fh1K1T+7WWKSfJEAAhxBtBq9Wi1WqxyryxffTh QCZO+YK2nTpjampG8WKuLFkwH4Dy5coyZeIERo4bT0EHe3S6DLp16cygAf0ZMXQocxcupHXHTpR2 d0OjiaZqlcoG74J9Xg/PbqxZu45PM8f65ycVK1Tgw/4f8NmIkRQoUIDChQqxfNlSg20iIiPZtXcf a3/STxhlYWFO65Yt+GzEKJKSkhg/ZlRu7Hqu+/DjT1Cr1TjY2+Pq6sIXkyYq43PVajVzZ85g7MRJ 7Ny9hyiNBs8uXej83Bsn/qvOHTvw+7FjdOnRk8TERN7x7Iafn/9L5/umSklJ4fLVawbX7+kzZ/l6 xUocCxYkOCSEch4eLJ7/32ezPnf+ArUaNEKlUlG+bFl69uiuvGpNZHX+5Iks6z7+0xwUi+fPIy4u DjMzM0xNTQ3mYHn6O/68U8d+z/a7IiIj8ShT+q2erOypWdOmMmta9q9XPXbc2+AtOKB/VeC3X31J cnIykH038OfZ2doye/o0Zk79grDwcKytrLI8SO7ZvvW/F0D8J3I9vRqDBw6g77t9iIyKws7ODksL C4P0AgUK8OWihcTFxWFkZKy8hed5VSpVomH9esqymakp3yz7koSEhP8chHlTqOYvnJMxYdzk3N4P IUQ+d/PWLYaOGs3RgwcMxvbHx8ej1Wqxs7PL8hmdTseTsDCsLC2x/tOkXampqURERuJgb/+XFaNr 128wbPQYDu3dnW/f+52WlkZcXDwODllbUOPj40lNTcuSlpCYiIW5ebbzMIhnIiIisLa2zvFzS6PR YGtrm+eHqfydY94n2LVnD8sWLzJYn5KSQmRUFDbW1sqcFeLtER8fz47de1j/60ZmTZtK7Vo1c3uX clV4eAT29nYYG0u7nfj35HoSz1uwaK70ABBC5L7vV61m4xYvunTskOWB8q8q92q1Otv3GoN+lvWi f9E9PSMjg01bvNi2cycjh36Wbx/+QT8UIruHf3jx8f9zNF1kz9HR8ZXk+7Ld3fOKWjWqG0yY+JSZ mdlfXt8ib9Omp5OUlMS8WTNl8jn0E18K8V/J9ST+TAIAQohcV75sWTavX0shZ+e/3ziHZGRkEBMb y4ihQw1v3fVGAAAHqElEQVS6eAkh3hzSup8/2dna8tGHA3N7N4R4K8j1JP5MAgBCiFzXuFHD1/6d arWaIYMHvfbvFUIIIYQQIre83YMHhRBCCCGEEEIIAUgAQAghhBBCCCGEyBckACCEEEIIIYQQQuQD EgAQQgghhBBCCCHyAQkACCGEEEIIIYQQ+YAEAIQQQgghhBBCiHxAAgBCCCGEEEIIIUQ+IAEAIYQQ QgghhBAiH5AAgBBCCCGEEEIIkQ9IAEAIIYQQQgghhMgHJAAghBBCCCGEEELkAxIAEEIIIYQQQggh 8gEJAAghhBBCCCGEEPmABACEEEIIIYQQQoh8QAIAQgghhBBCCCFEPiABACGEEEIIIYQQIh+QAIAQ QgghhBBCCJEPSABACCGEEEIIIYTIByQAIIQQQgghhBBC5AMSABBCCCGEEEIIIfIBCQAIIYQQQggh hBD5gAQAhBBCCCGEEEKIfEACAEIIIYQQQgghRD4gAQAhhBBCCCGEECIfkACAEEIIIYQQQgiRD0gA QAghhBBCCCGEyAckACCEEEIIIYQQQuQDEgAQQgghhBBCCCHyAQkACCGEEEIIIYQQ+YAEAIQQQggh hBBCiHxAAgBCCCGEEEIIIUQ+IAEAIYQQQgghhBAiH5AAgBBCCCGEEEIIkQ9IAEAIIYQQQgghhMgH JAAghBBCCCGEEELkAxIAEEIIIYQQQggh8gEJAAghhBBCCCGEEPmABACEEEIIIYQQQoh8QAIAQggh hBBCCCFEPiABACGEEEIIIYQQIh+QAIAQQgghhBBCCJEPSABACCGEEEIIIYTIByQAIIQQQgghhBBC 5AMSABBCCCGEEEKI/7djxyYAQlEAAxHeDs4siHPqDr9wCrHI3QSpAwEGAAAAAAQYAAAAABBgAAAA AECAAQAAAAABBgAAAAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgAAAAAEGAAAAAAQIABAAAA AAEGAAAAAAQYAAAAABBgAAAAAECAAQAAAAABBgAAAAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAA BBgAAAAAEGAAAAAAQIABAAAAAAEGAAAAAAQYAAAAABBgAAAAAECAAQAAAAABBgAAAAAEGAAAAAAQ YAAAAABAgAEAAAAAAQYAAAAABBgAAAAAEGAAAAAAQIABAAAAAAEGAAAAAAQYAAAAABBgAAAAAECA AQAAAAABBgAAAAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgAAAAAEGAAAAAAQIABAAAAAAEG AAAAAAQYAAAAABBgAAAAAECAAQAAAAABBgAAAAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgA AAAAEGAAAAAAQIABAAAAAAEGAAAAAAQYAAAAABBgAAAAAECAAQAAAAABBgAAAAAEGAAAAAAQYAAA AABAgAEAAAAAAQYAAAAABBgAAAAAEGAAAAAAQIABAAAAAAEGAAAAAAQYAAAAABBgAAAAAECAAQAA AAABBgAAAAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgAAAAAEGAAAAAAQIABAAAAAAEGAAAA AAQYAAAAABBgAAAAAECAAQAAAAABBgAAAAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgAAAAA EGAAAAAAQIABAAAAAAEGAAAAAAQYAAAAABBgAAAAAECAAQAAAAABBgAAAAAEGAAAAAAQYAAAAABA gAEAAAAAAQYAAAAABBgAAAAAEGAAAAAAQIABAAAAAAEGAAAAAAQYAAAAABBgAAAAAECAAQAAAAAB BgAAAAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgAAAAAEGAAAAAAQIABAAAAAAEGAAAAAAQY AAAAABBgAAAAAECAAQAAAAABBgAAAAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgAAAAAEGAA AAAAQIABAAAAAAEGAAAAAAQYAAAAABBgAAAAAECAAQAAAAABBgAAAAAEGAAAAAAQYAAAAABAgAEA AAAAAQYAAAAABBgAAAAAEGAAAAAAQIABAAAAAAEGAAAAAAQYAAAAABBgAAAAAECAAQAAAAABBgAA AAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgAAAAAEGAAAAAAQIABAAAAAAEGAAAAAAQYAAAA ABBgAAAAAECAAQAAAAABBgAAAAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgAAAAAEGAAAAAA QIABAAAAAAEGAAAAAAQYAAAAABBgAAAAAECAAQAAAAABBgAAAAAEGAAAAAAQYAAAAABAgAEAAAAA AQYAAAAABBgAAAAAEGAAAAAAQIABAAAAAAEGAAAAAAQYAAAAABBgAAAAAECAAQAAAAABBgAAAAAE GAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgAAAAAEGAAAAAAQIABAAAAAAEGAAAAAAQYAAAAABBg AAAAAECAAQAAAAABBgAAAAAEGAAAAAAQYAAAAABAgAEAAAAAAQYAAAAABBgAAAAAEGAAAAAAQIAB AAAAAAEGAAAAAAQYAAAAABCwnddxr7X2v0MAAACAb8zM8wKmsJuY2uUlTwAAAABJRU5ErkJggg== "
+ id="image3047"
+ x="268.64252"
+ y="931.88281" />
+ </g>
+ <rect
+ style="color:#000000;fill:#333333;fill-opacity:0.45490196;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect3169"
+ width="160.61426"
+ height="210.4968"
+ x="240.30724"
+ y="603.85443"
+ rx="6"
+ ry="5.9999995" />
+ <rect
+ style="color:#000000;fill:#d4eeff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect4020"
+ width="95.838203"
+ height="29.529198"
+ x="245.99754"
+ y="736.70099"
+ rx="6"
+ ry="5.999999" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text3986"
+ y="754.59943"
+ x="288.91171"
+ style="font-size:40px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ style="font-size:10px;text-align:center;text-anchor:middle"
+ y="754.59943"
+ x="288.91171"
+ id="tspan3988"
+ sodipodi:role="line">cockpit-bridge</tspan></text>
+ <rect
+ ry="5.9999995"
+ rx="6"
+ y="701.15417"
+ x="246.43257"
+ height="28.261431"
+ width="59.384914"
+ id="rect4022"
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <text
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="275.21933"
+ y="718.03479"
+ id="text3998"
+ sodipodi:linespacing="125%"><tspan
+ id="tspan5557"
+ sodipodi:role="line"
+ x="275.21933"
+ y="718.03479"
+ style="font-size:10px;text-align:center;text-anchor:middle">pcp</tspan></text>
+ <g
+ id="g6971"
+ transform="translate(-2.4999847,-1.4061279e-5)">
+ <g
+ id="g6964">
+ <rect
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect4024"
+ width="148.4924"
+ height="51.299866"
+ x="248.49753"
+ y="610.26562"
+ rx="6"
+ ry="6" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text4026"
+ y="628.33929"
+ x="389.69452"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:'Bitstream Vera Sans';text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none"
+ xml:space="preserve"><tspan
+ style="font-weight:bold;font-size:10px;text-align:end;text-anchor:end"
+ y="628.33929"
+ x="393.17108"
+ sodipodi:role="line"
+ id="tspan4032"> System APIs </tspan><tspan
+ style="font-size:9px;text-align:end;text-anchor:end"
+ y="639.84253"
+ x="389.69452"
+ sodipodi:role="line"
+ id="tspan4471"> systemd, journal, realmd</tspan><tspan
+ style="font-size:9px;text-align:end;text-anchor:end"
+ y="651.09253"
+ x="389.69452"
+ sodipodi:role="line"
+ id="tspan4492">NetworkManager, storaged</tspan></text>
+ </g>
+ </g>
+ <rect
+ ry="5.9999995"
+ rx="6"
+ y="773.76562"
+ x="245.99754"
+ height="33.279198"
+ width="150.32707"
+ id="rect4034"
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.49999997;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <text
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="296.03622"
+ y="788.88324"
+ id="text3974"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3976"
+ x="296.03622"
+ y="788.88324"
+ style="font-size:10px;text-align:center;text-anchor:middle">cockpit-ws</tspan></text>
+ <rect
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.49999991;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect7026"
+ width="43.434963"
+ height="39.789604"
+ x="350.48355"
+ y="726.26562"
+ rx="5.999999"
+ ry="6" />
+ <image
+ y="597.6925"
+ x="217.77876"
+ id="image3179"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzsvWmsbNl13/fb+5yaq27Vnec39etms1vNSWQoimJEiqREKo6DOCZsK4CCIEhgiVYCITEMB/nQ +SDJEhQqgSwESkAHtoMYSAIkQeAokW14ICiJQzd7Yvfrfq/fdO+78626NQ/n7L3zYe8zVN26byBp sV/ft9D97qkzD3v991r/tfba8ESeyBM5tyJ+3DfwRP785Otf/3rl+Lj56VDx5Uze/3KlPLM+6PW3 uv3+TWH0keeLHaW4hWZLCHalzO73+8d7L774ov5x3/sT+dcjTwDgfSxf+cpXvI9//FMfDbX5vDH6 5yszlU+trK4WNjcvsryygkCAEGil6HTaDHoDeoMeJycNBv0+w+HAdDu9fhAM94Iw2A1Ds+1JdVcL dnUod4VWt2Qpu7s2O3v8y7/8y90f9/M+kUeXJwDwPpPf+Z3fWRmF8ouhCr+Qz+e+uLq6trK6ti42 NjbJ5LKgDVprjDEYY9xREs+DqDkIIRDCNQ3h4UtBEAaEoaZ50qDX69Hrdej1+3RabQaj/lAF6qTX 697U2hxqo3cF5raW3p2cYjfIqINRp3MTGD2xJt5b8gQAHnN58cUXs7lS9WdGo9Evep74/PzcwodX VtfE2to68/PzKK3RWkOs8JOfXKT+nbI5tUIIgRQCpLRrhYiPFkLgSUmv16Xf79Mb9GmdnNDr9RgO B3Q7XTq99qEK1f5oFOwguCM8doRiV/rebTUMdrPZ8t7f/Ju/cvAjf0lP5Ex5AgCPn4jf/u3fezZQ 4S9oHX6xUCp9ZnV1vby5uSmWl1cQUqC1QSuDERpcJ59S4+knnVwvzt53bP+xXaRdJQQyNiAEAgkI pBRYo0PTPGnS7XXp9XoMBgParSaDQV+HYRAOer13e/3hPYQ6NlrcFoI70vN3UBxK6d/c2rrW+P3f //3hQ76vJ3IfeQIAj4H85m/+5qLC+4zR+sv5YukXZmu1jbX1DbGyukq5VEa7Xt5orNJPVfcHferx 7eJsk+DUMdN3PX2cBQ2BQIPnxXskLodECBBSMgpGBMMRvV6XdqtJp9thNBzS63XodrrdIAgbozDY CYfD21J6u9roXel5t5Wn7ngjcXR8vLD7u7/7hJd4kDwBgPegfOUrX/Fe+OgnflqH4ReE4Eu12bkP r6yt59bX1phbWABj0EZjlMEIA0bEPf20jtyc7t85vRfYk0Q8ABgzqdxnnWWasrt/z7zwaWgywoGE cX897e5cWpBAOCPDuSICBv0+nU6HbrfHYNCl1Wox6A8YDHo6VGa/1WzcM6E50sLclR630HJXSXGU 0erdarWw/dWvfrVL8vbOnTwBgPeI/NbXvnY16Ic/p3X4hUKh8PPLK6vVlZVVNjY3kVLGxJ3WEYcm 0voai1X4ZBl+MACYvkU8YPez3At7D2N7jJ1j+gkFAuOexi4zhikGLBAAQjj3w3N3aSTCs0AxHI7o 97u02k267S6j0ZBer0ur3UIp1RqNRrv9bv9d4ZsDIdg1Wtz1PXnLBNkjKdXu3/pbv7Yz9cHeB/IE AH5M8gd/8AflerP72VAFv+h5/ucXFxevLi+vyI0LFymXitaPH2Pr059qUlFOrzGxysGDP/M02Ji2 VUzX/bHDnekQbZh66bMtCafmFrTceccw4j7vYWxzfJAAtHMzJNJZEEIIpJSpmze0Wm36/R79fo92 u8Wg32cw6BsdhoNmu72twvAoDMMtPHHLl5mdYrb0z3/t1/7D1894mMdC/B/3DZwX+cpXvuJ95BOf +HA4DL5sDJ8fKv1Tl69cKWxsbjI/v4gQBmU0RgmCYIQ2kSkcyaSVak6tHff+09sexsKNlOn0vunz mAkIMG4xXmvM+FnMNHWfAJvIjcHYP/ZEyTIReTgtijHdQoj2F4IUpGgUJNGL2KARCCkoFAoUi6UY JLAgIQQUpCefVko9rY36VL/X59t/9l2+/Wff/DbwyVOP9xjJEwD41yi/8Ru/sRoa8QUdqi9lC/kv Li0uL65tbLC6skY2l3PEnUaFYWzqAqnGfh/VNZOLIrX8qC6tSf19gLWQIgaMMXHXfD94St/f1OtG yuqWY5fFTHueBDyEBKENGpBSgLYgKGOll6lchxR4iZRDIhyMOfQQaJSIlkFIaylkZYZWq823/uQ7 fPfb32M4HHL93Te37v+y3vvyBAB+hPKHf/iHxb29xme0VJ/3Pf/LM7XZ51bWV+Xqyhqzc3MopazC G8NoNEx840ip4kZoUkqTNP7EQHYypq/m0fV+yjWmSvo6JvXbkDL3IQGhs3vqcZGAtnkF2qAAKcFo c2pva8hLEE7LAaOS7Sq9HB0jkpwjIZLnFCKyFgzCJO9UCAcS0oYxfd9Da8Orr77BS99+mTu3tvAz Pr7v4XkevidSV3085QkA/DDy4ovyNzKZ5wJjvqA0v9Bo9z596enLldW1NVZWVkAk5N1wNEqUJfqT 2KrurzODo/ObcTUwqW1xc57sHB9Cn8fl1BlT50kp8sQ5jYnVF4iWUzuJCMJkcrAQoDUCSagVQii0 AoGyx46/ntTjp6ITqX9Eyo4/vTy+jxApIE12GFuWUuJ5Et/32d/f51t/9l1ee+X7KBUifY9cPufO J5FSY1zew+MsTwDgEeXFr31tzvT7XyDgyxlPfG5hde3CysqqWF1fp1AooLXCaFBKY0xIzNaP8dnO IDWTSjyt3ztl65+h32L6OacCQkKORX+sSW1sqrCQeEY75TZIJOn8XSEExmhCZcm1IFQIIdBKO6PA 3oT9Y+zfCW4gUjyrn+PLif6eodyOL5gGAPHmyJJKrecUGAikNE7xMwRBwMsvvcJ3v/M9DvYPyGQz eNLDz/iWJ4iOlwajBd7U7/B4yRMAeIC8+OKLefL+xwnEl7TkC7Vs/hNLm5fk+sYGCwsLKG17MWMU wWgEuP5qrDMd8/CTTn+Kco5Z27HSTJrUzlQ1kfbKmPcXwvawRoLUAu2yAW2YTI9dRRuDCe1xKhyC EARBiBASFVrw0toptbbRiFjBgbEeFhIFc8siYuQ4vWzSPXLsizMVDEx6eZqiCzCpl3n2PTngkCCF h5SSTCbDuzdv8vJ3X+GtN99BSoHvud4+vq+0JQHSSIx8fwxpeAIAU+S3fu/3rgQD9SWjwy8UirnP Lq+szS4vL7O0sorvSbQBoxTD4UQ2asoEH2fOsaw2Se823QIgpeeW5ELbWLc9deSvOiWRzvd1vbHW CqUMoVIIBGEQYgAVKhACpeyYAK0M2kxQc/LsXvjsZRMn75jk4HFlS/XGU8FAOHg00fqUnTSxPHbe 9D3E693rT187ZS0IpFV6z6PT7fLSS6/y+qtv0Gy28X2PbDZzCrwmlV8gMM4CGMPkx1SeAADwd/7O H1YH+vhzWpkvZDL+l0qF0pULz14Q6+sbVKtVZyZrtAGtAaEx2kMrhTGgjUZra/OOu+1W6aWx7TBK WokbuwRpBEophIAgUC6PXxOGilBrdKAwwhCOLN8UhO5vECAMaCMwaCTSKoK09gCQNOR4mTPMbQHa JMpjUjkEIqXQxpxeJuVHTyhk0nunlT4NBslyfGS0LBIUnWZJTHuWyeeK7sNzvr0QkrffvsHLL73K 3Tt3kdL6+7lsxh4jU9/mDOW3J5Z23/cBApxbAPjtr33t3xj1g8+P1OjLMj/4+NW1ZwrrmxvMzs0h jEQZgzGKdrtje1+JSyQReMJDSkEmkyWTkUgvgycFRiuElAShTWHVWjMahfS6fVonHRqNFkBsZo+G I2TsQ9vz2yblElZSjQ+4T28MOuXP67ihJgAQM98pJRvvYWO7YkzBhDndw55Stgj1HmAtjFsIU5Yn 7nkSDCaXmULsxdaCEPi+j+d5nDQavPTSq7z5xjUGwwG+55PJZFx6cepe76P8pNYjDUK/H9T/HAHA iy++6MtS6a+KMPgL5fLMF+bmZ+eWV1bFxoULZHwfbQxaKZTS9IYDNNBs9pDC0OoO0AZ6vQGjUNEf jhiNAsLQ0O310Rr6QcDcTIW5aoXaTIW5aplatcJMpcj6xgLPPlsEoN3p0e106XZ69Lo9ev2BXe73 GAxGaKUQnkS6AJMQnktMgfHGONlLp0NcE8tpxRK40NfpntddYToYuGub1LXHrIX7WAhpMEhCcJPW QrQc3wUP40ZEfyNewpMenifBwOuvfZ/vfe819vYOyGZ8pGd9/ug+JrmCU0rv7nvyWtJIjNCIdGjh MZVzAwDtQPy19dLc31/dvCzyhQLHJ22u73R5+e3vMgwVQaDoDYYIIQiVsmZ03EhO90pWAyKT3/ZK 9XqTw3oDrQ3GMeJK20E7Gd+jVq1QqRSZm6lQq1ZYmq+ysLLIlVKZcqWIJyXD4YBOu0+/36PV7tgq Pf0+g+GIYX9A6Bh3IWQqJJXKGxBnm+lppR97ltTy+D4T1kIKROJ3cgogOG0tpIDhYa2F027EpOWQ WBRSCKQv8T2f3Z1dXn/9Ld588xrGGHzPI5/Ljn23MUVPPXta6c9SfssBwGgwxMCdR2uF7z05NwBw eNgszi0/Jdo9aHW6gCQrC9Rm8gxHQ8IwpFJUBGFIqBSjMGQ0HGImzcSUmNS/OEJMIhBSuN4R2xv5 todqtNs0mm1u6x2UAbTC8nIaKSSlYp652RnmZipUq2XmqhVmazMsb6ywUKvhScEwCOh0evR7ffr9 Pv3ekH6/z3AwpNvrp2Lg1o2IQCLurFLWwvhyGtimLDNhUXAfa2Eaez/5Dh9leQxE7D9S2PfsSY9g OOTVl9/krTffpl5vkPF9fM87pbjTevn0s0X3P+5OTLEAHPhKYUZnt7jHQ84NACDdxyVFugvwhcAv FKKfpDQIIQRBEBAoRRgEDIOAUFmQGIUhoyCwRCCuZxPGhqNMBA0uHh71zxEZKKV1X6XAMwZjPMAw GI7Y2jlg696BJRaVwaBRCrRWlEtFZqsVZkpFZqtllhbmmK2VWKytMFurUCkWGQyHDPoD+v0Bg36f Xt8CxGgYMBwNURHfgIwz3tzDcsqNmLQKouT8B1kLE5bDaeU+w1KZYi2k8wYMID0PT1pC7+6du7z+ xlvcevc2Qgp8z7dMvhhXWIFw6cHTSb00kE1T/jEOIH5PgveD+jz+T/BDShKicwAwkX0npSTveZDN UoExgMANzx2FIcMwYDgcWcAIQkYqZDgYEipl4+dR2qkxzqoYjxhEy9IpnyckUhiMkfieBYkwCNg/ qrO7fwgGlHGFQJS1MKQnWJitsTBbpVIuUKtWrLsxO8t8tUqhaGsCDkcB/V6PwWDIYDBkNBoyHAaM hgGhCm1evX34KLbwUNZCmluYRuaNJe88hLXg9kRIi95Zz6PV6fL6a2/y1rV36PV61uf3fRDjORTR txWp5ehmTpF6k8qfAoHILYh/g40CCIDHPxfg3ACAUNmjfq93ar0B8rkMF1fnabR6nLR7DIYjjDZE HIBtJG55AiAQglw2Sy6bRZTKgFXiKPSktWYYBATBiMEgYDAaMhyN6PQGdPsDlFJujL/r/YQ4Nfw3 4hkM9vpSSIww+MbDSIk1IOz2RrPFUf0EjcG4KkHKpSP7vsdMuUi5VGK+VmFhtkqtOsNcrcTy3ALV ahmJYNgfMBgNGQUBgbMcBg7cjFIYkST4SinHSEe7cNpyGLciziDzYoDAWkfCxu2lFFy/fpPXv/8W 97Z3kb6HLz1834+vEx0f5xsaEUcVGOv9k+Ss6CbG/k2BQBqgYuAQwrlTaah5fOXcAIARuq71+NgN Y2BpfoafuLLBu1v7PH91k82NRTIZ6PaGNDs9Gs0une7Isvc962d3usNxcMA4Ys64SjW2USqt8aWg lM/jlQsIYVlq4Rq350mUUgyDgH5/QG8wpN3p0u4N6fV6tDs9esOhbczGYKQDCSNdZZwoVm7iBzLG jowTRtj9JXhGxiDS7vRotXps7+yhNKAVmih12TBTLrEwO0OlXKRaKTNXqzBbrTJXm2OuVsHzbLmu 0TAgGI0YDoeEoWI0GhEqRRgGznKY7P3HlyNFGwvrgTXlpR1sU2+c8Nrrb3Lt2juEocL3PRe+S3/X 8V5epBbSGZTCJWJFA4AYA4dxwy6R6L4niMGYgH385dwAwDRRWvO5n36eX/6V3+fouM2VK5v8/Bc/ DRgqpRyVco5i0adcKbC0uEg+K/Fzkowvabd7dAdDOt0Bvd6QXm9Evz9kOBwRKo3RBk8IjBFIz5ro Qki0MUgNWtrogCcExVyeSqmI53xMz/fxpIgtiG5vQLvTpdMd0Op2aXf6NDsdTpodWm1n1cQMe9rv jqwJIO1yCJBIhGcw0sdzbLkxhuFwxN29A1DYWoOA0cryEEZRyOWYq81QLZeYm60wW51hrlphplxk caHCTKnEaDS0IDEKCUNFEIwIA0WoQ5dijCvGkdyPNeUlQaC4du0Gr7/xJkdHdfyMj+cSduxTxIc4 i4iUNTHZL4tTxzjjICXWejBj50nAKU39Jud/7KN/sZxrAMCAJwXZTIZ8LkM24xENG+0PLfl30vHw fYnvS5c5Jsn6HtmsRy6XpVQuMFMTZCQIzzYNpUJGo5DBYMBwGDAYhIRBSDAKCUaBZbBFwmTbS2o0 dp3WCmNsdV8hDOVSnupMCSls7+5L3yYk+TYbsdXp0mp3aXcHNNptjhtt6vU29ZMW3W6fYaCSSjgG O5jFiDi/wBoHSUOXxqa7ekIijQWJiIfQRnNUP2H/sI65ZeKKRdGAn4znMVerUqkUmJ2pUJspM1ub YXGuRq0yQ6mYR0ibphyGli8ZjEZsb2/z+hvXuHnrtnNXLKE31tWme37SyjipwOP+/2lgSD23WxAR gQtEbG1EFeDqHowByftEzhUATDPbtEmIHGMEYahII7yPQAuIUr+tTy0YBaCNIQi1HRvuR0NJPXw/ Rz4P+VwZhU0TNkYRqJCjvSO6zZ6NXcsICGzPH/m7UZKKECb+bRxACG3r2mgjUFojBdQqFeZqM3jS Ema+J/GcnxyGik6vT73R5rjR5OD4hGa7x+HxCYf1FofHJ2gFWS+DnG2jT0qYwEOLELyosbvEF7Dc CMYRhdNV4aTdpt5qcWd7LwYGg0Frg+/5VMp5ZqszeH6GpaVlavNLfONf/Am9VhPPmzLGboLgG1+O XIpkG9F2R7hGCpxmC+J9xmDELpk0hEQbUic2CVEx9fkfJzlXAKD1adNNRf6gsPF4pTRpMirJGIt+ AxONLmGW7Topbe8qpIdnJJ5nt+ezArWoGXaGRLyBdOdPK3+yzoJCdA+eS0VGJCAhpcSGH0FpkGhC 96yhsCG/SrFItVLi6ctrSCHwfTsSzvPtvbXqAf/1jV/CXGiiggDTybL5zi/y7jd9mq06g/YOQmYR fh6lPaTnu/tKK4AZi6BEXEj8W9rnGoWK61uH7Lxyi3a3zy9+7lP87OISvufH8wYIZ6ubiOKbIPRS V4y/x2TvPHUOlCnbk2FWqX2iE6fcBRPtawRGawbDIaD3Tl/h8ZJzAwD9fu/2oN89bQZoHQ+LN8YQ hpr0ePHxqjKk1jMGEuP7KOcy6NiliMN80kdkJIT6oZVfCpt8klgHbt2ExRAdI9MAISRRFp9SCiMl OnCWRSiQnmRupgTZkPm5eYQQlMUsf/sn/jb8VfiPf+VXeefOKlKC0m3++q/+m2wdjHj9zWvcvfU6 nueDl0UpD78wjyc9cM9h78e+m0Z7wM5xm3q7TxRey2T8VJFO+zfSxITQGwfgU3odEQEpYs+I0585 2jceNZwmBNw6p/PuC6f2ca5T+rcxGiFoTbnKYyXnBgDmRXVq1pYy43V2lEoAIN37jyv/WHmM+HcC Coli2x4taY3aaDysGTlN+WVq0NGYoouU8svJdcl5JpU/WUcCKNG5pUA6cBLGWg1SCEIUo6FCCE2o FYXcRTy/glaGv/aX/21CZfjD//Hr7LxjKHlX6Xfr/Be//lG8fJbdwwYHR02O6k3evbvDtduH7DU6 hMo4IJvuNug4oOHM9cgCn+bXR3ob7WOS7xMb+SnddoekSIFxcs/EPn46QmDi72gcyETbhMAlHWWn PsvjJOcGAHpAcXLlRFtUSsclvGBaz3563Wn3YPzEmcxpEBBCglBTlT8x+c9Wft/l+P7Qyi8c9+B7 KRAE0HFVHwzkckX8TBGtDMNgQBh4KK3IZouUiotIOculy1dZXS1Y4hJBNuvzd//h/8f/8yf/O/lc Jsk4PEMmvelJtj9i68fXpV308dh/DBCkOvkIKFLAERkd47xCyvGPTpRCHKVsuLTXzt//oR4DOTcA ME0E0Ook+fPGGIJg3AWA6b3/uPJP8gRibB/f910Y0DYqIQziB1R+74dU/oR8TF1vSljLGIEUGoMi VyyT9SooZUCD0RqNJuPnyeZLaAK00gRBgHJZiVorV/fvAZo/+U3idxd/FGebi1hp9RRtjkN3p0Ai CvMlyUKIhGuYNPkja4jJbdH5DHi+R8b3eewrgnKOAMD2/uNkDwJGYfIZjTFnugD297TlSZ5AEKWI JvsoMhkZN2opPHD8wHTlFxNKnSi/ED8q5U+O8Vw8MJ7QExBoHB9KPlch689YklQrO3ZBGzLZPMXC DFKESOlhjPWNbVjQRiweVjTCKm+kmLFZnzLVgfG8v7QbMM70g0G4+uDjLkLqQOPAT7h8Ce3O6/wM IZLjSRkC5WKR4bDL+wEBzg0A6DmjVRiacdUlYZjd8iQAxPvGjVGf6u0T94Azlu1CNhtNXwWocaIs UXSB7w5Or/Mm9kkrdUIeTir6tHVpsMElKxlyXoHOSYfKfIWszqMMNnvOGKTwyPhZwmimIiFs0ZH4 f1yoM1J+QFgr4eHF+hupFCbLuIs4FhDDd5SGnPT1qbOkACS+fPp7AUImAYsIEBKAMPGgJxshjtKz E58iX8jT73ffD/r/PghkPqT8D//tb+4qrfqTfr8xmmi6PWNcKa7QhgOjv8n/auK33Se9fxiasW1K meQ80Vh+zlZ+789Z+REwDAf85bW/zu1bW9y6dYtL3Z8kNAEoQ7U6y+7BHzMchvS7PQwynp9wNOoy GHbpDzoueSmpj2B0Kq//kcRE/8Vznqb9fUNCGEYm/mkHZuxsRFEYg3MhdPQ7OXO0LX2d5BiLJsYY cplMyv54/CHg3FgAQBTMGZPJirs2A0/FPX66J48sgFNnPaPnj35HfwMXZZDSNqFpyi8eQvn9VC7A Wco/zi9MuAOerVOIAKUMQgT8ZO3n+PqHv8md/nU+WPoYIzVEhfCfffWr/Cdf/TVeufYav/RXfpkw fA6lNEsLS+wd/gOKuasMR75Nf3ZViiMXYFrexdnfJk3okY64neLiks8VheiihJ/Er5/EnoghSFsY kcuR8HtRzkFEKEZWSHKefD6PctOxP/7qf84AYJpoY/3dSISwqQFhaEfoTZr70T72/8hlmNwnTRQm bkNsyko7mu4HUX7vR6z8YNAaFAMyXp6rpRcYqSEajQkNc7Nz/B//6z9iMBiRzfoMByOEEPziL3yJ d2/e4o/++Gt89jN/gY2NjxMEYSotePy9PPA7wKnQXQQGkwodE6oRIBDtNBEXjEdXEvvwSUA/dcax w0zK3E+5HsbgSY9sNosxhsFwiNA0H/4J35ty7gHAGE3EVUXKZtfjsgKjbRB7nWMWAWP7nGUJRKKx E1EIo6cqf1rRz1pnLYgHKP8EESilsMBjnAntUnqTVF0wtrBAYh4bCMKQILTrBgObSqGVRumAv/Er v8J/+tVfjQf8pE1rI0TsWj1QUgRdrM8x2++UMiL2UjuPRehEclzMAYg0WEzS/q4+g+MYYisiJcYk IASQyWYx2qAEhGGIlKP9h3zC96w8AQAdEX6SRqNOq9WiWCzg+5lUb2atAiE0WosYGM5KFBp3E8ZD h0aapAefovzTfPgflfKLSPnNNOU3MeMdKb+e2C96XwZsOuxgkPj8KeAwxm5/2IIZAhLnLGUFpPvq RKGJmfp429hxpxyJ6LSkf0T9u3BnUcLExKNIuQIRBQmCQi5HqDWetKM6+w/1dO9tOVcAMBwOmgJK yRrbBIyyzUNrzcsvv4QQkMtlyeUKFAo5SqUShUKecrlEuVymWMzj+zYX3oa+EnJJa+Pch9NWgBS2 jr9VzB9c+U+b+GeQgxHJ6JQ/UeRpyp9W4BSZF+3nWLH0MaeUf+J8j5IDkJ6zKPbtHUOjJxEh2scI dDpJJ/YfkiG+Y51/bAREvX90rYgFFnHlJhFbH/Y437eVh+JEMWOg//irz+P/BI8gKgwOQKxFv4UQ 7Owe85f+vU/x9b/3x5gApPTwfRiNAoJAMRh0OTlpxP68ELbQZzabpVgsUqmUqVTKlMsFKpUSMzNl SqUiUnrO/7Sj26xLgRtmK/AFMNVfHwcBT6b8flIKPk3RJ3mBH0L54x7/B1R+SwI+2veJ9TvV20cK n3jjSZ+cPibq0XGKbw8aB4ekZyfl2zM2PiAmGp1LIIyNZmQzOTt/A9Yl0caQebTHe0/KuQKASREC traPeObqGn//f/rPabZ6HB21ODg64fioxcFBk0ajQ6PR5uigZevK+7anHw4DwrBFp9Nmb8/NT+/E 8yTlcpFyuUStNkO5XKRarTA3V6U8myVj58B+KOWXU5T/VJjvIZU/ZrWNfiTlj/ebXDdmIbigWrRd aIx4eASwiq+tVUXSWyf8feIKRNxA3NOLVI+ethLGlHqKpeAuIlInN+633TWKLggy+SzauBiCsQBw usDc4yfnFgAiZfJ8yfb2IXfvHjrFzVMp5lj6wAYfeuES2VzGVqXJejSbPfYPTzipd2g1e3RaXVrN Hif1Nt3uAOnZUtzGCHq9Af3+kKOjhjP5Lan4U595gY8+ezHu1SdN9jPdAfHDKb+Uhk57QLvXp1Ip UchnEdi89mi0YMLeu0lBf1Dlx+YB8AiZgP1BH4OwCUgiIf7EmFImjHzkW0X+f6rrJrIJrAXmlHxs t3HASP8WjmRI/85kMwgj0A440YZHiHC+p+X8AYCw2U/dTpdXr93h1s61AQRzAAAgAElEQVQ+tZky C7MzVGdsscxaucRctUwu64OQ1gyUAuFJZnyfuY1FvEsSL+vj+RKZ8VEa2u0OrZMenVaPdrtPs9mh 0+7ROumgQjtLrx2Lb3uVPy/lt8oqaTQ73Lm3x+rSAgC3bu/y8Q8/TWgMu3tHrK0sks1l6PV6VCpF a8JrjRbW6NbaDYPFxGTiVOU3BvMIU2cJIRiNRmPmPCYh+0gpMdF2kh9p/97uPl4IJAIra1CIxDIQ FqwtyKRBIfYhQFj2P1BhzGlEGZLvBzlfAOAaxUD4vFq+yubPrFDYPeRgZ4/r9/Y4fPktN8e9wfM8 Zsp5ajMVlhZnWZq1te+WF6rM1ypUCnn0MLQ9lrQTb5SEoDxbRi5Ukb4HnrT56NJjEAQM+kNm8zk8 pWMf/pRSe3I8R2DCSpim/GPuQ6z840Qe2EbuSY9Wu8u9nUOEFDS7fe5u73P1yjrSF3znpbeoVcv4 vken2+dDz19BGNjZPWZtdY7hUBOEI3K5fKJQbuLUMQJRmEcxAFJK7dyUFBmQBgX7HUmUmJRfnzI6 0ryA3S5ioIp2tiClE6RJg4C7FyEkvswQpTnbqI59zkwmfLT29x6UcwUAo9FoKxiNPmJEjlebFfZn ZvmpT36En38qY/PDgWY94Gj3gOOdPY72Dzk5OGZ7Z5dX3r5Dv9UBYdOGSoW8ndprtsLK4hwLtQrL izU2luZYmp9x8TZla/ChyaGZdXFk7peyy6Mrf3yeM5Q/sgIwcHzcZH1tkQ8+c5Fr1++yf1Ann83Q avcYDgPmZis89+wlvvlnbzA/N0Oz3aWQz3Nw2AAMO7vHlEp5NteXuXb9DlJKnr6ygefJOAQITs8e JRPQKVU0Z8KpgJ6z8JMxAZHJ7zamIvaWFyC2gqy+G9f5p33809xAZAlE8OH7GTS2RiPurmwpuIBR 1+/8UA3yPSDnCgCEoS9cL+tLQ6s74o++vcc/+57Hhy6X+djVMpVihtxT66xdWU/al+td+l3DydEx Rzt71Pf2ON7bZ29nj2svv03npGlLawOe57E0X2F9eYGl+SrrDhTWVuZZX6hRLhZiujtium2xTly7 u7/yT2P/hRhX/nEiLzq5lXs7R3SrRZaXagghHSeQQWtDp9vnT771OoVSnoX5KgdHTQr5PnNzM9zd PuSF5y6Ty/ncvnvAlUtrCAHbOwdcuriKVqlrqkfzk7UeN+1FTNBZUIhCd+nefyyNO4nx2bU6NhHG ttnOPpVWNEEdiMiScS5HJpMhVCaOOwghQBvCMESIwc7DP+F7U84XAHgST3rMeGH8QX0JRhteudHl 5Rtd1hbyvHCpyJWVHJ6t3BVVDUN6grmVBeZWF4CfsK3J6dWob6gfHtPY2+PkqE5jd4/jnT3e/f4d Ot94hVEYWnCQglKxwObKHJvrS6wu1LiwtsDqvP07Vy3je27Mf9yDE/usNgw1Rfk5W/kNYLQml81w 9fIG1WqRwTDg1u0dPvjsJd65vs1TV9bwPEG5XOSDH7jEt77zOtVqmRu379Htejz91Do7O4fc2z2k 1xtSLhfY268zGI6Ym62MWxzaYHyDeITRgAL7jscy8gyoCBxJk3UmrtuXjumLSPkdgTi+zVkV0ejC hEdM8QjJSmGiegw26SftfURRgMc+D5hzBgBWrwxIiU51HmBZek8IDpoB/+SVFp6QXFzOcXUtx6XF DJ60cXxlsHP2RSQbTtEQzC4sUFtYSFxK51aoANqNE+p7e7SOjqnv79PY2+e713c5+cb3CEI7vyBA sZhjaa7KpY1F1hZnubi+zMW1eS6sznNlY5mMb+cRRCTXRWs3QMWSdDCh/MYqwmy1Qq1mlTUMFeVS gXw+y8c+8jS37uxRLuXJ57KusIdAK0Muk6Xb65PLZSmXiywvzTFTKfHu7R0W5qsEQUi73QWsTx1V ADYatBA8rCSJQGLM14/N9DQFmFJqkQIBIQxGOuV2ynwq6SfV3UfuhIjdAMaIw0zWDnKKDAnizU9I wMdSjLaJPhk52TAF0ScWWKtACLh7OOLOUQgINhYyXFrIsrHgUczZ8JlyloF1sW2vkO6FE2JMkK+U WCs/zdrTTwOghYxJtO5Jk/r+Hif7h3TqdU72D7i2t8+337xLr9kkMkC1NmyuzLO+Os9Tm0usL1tQ WF+ucXF1gcU5W7UnVArtZh5WWqGVDWFpo2MlldKCTTRy78rFVQcWmiAI+fALT6F0yIWNJQbDEWEY 8tSVNW68e49hEFItl0BCqVRg76A+rvzu+R8mDSB2TCJLJ47tO1+eyFxP+/yno37Re455B+c+nLIS UmeMrIno1HbRcRBCIGUmjv2nRxFq95zvBzlXAICPwSgQqWKOQqBHPYSXI1uuOMIpYp+Ni0nD3oli tzFEvCso5QTrs5K1WcliNUPGx82cQ+wHj7d9Gxc3RjlgSMXQtUFmcixuXGBh86JtYLZsKAYIhkMa +/u0Dg9pHhzQqTfY3t/n+//qDQadDmE4so1T2DkKr2wscWlzkcvrS6wtzXJ5fYGNpTmW56tkszk7 Wak2KKUItZ2B2Ma3jRvMkxCIWgn8jE8l46GMrTb0zNULKG1BQkqBxrAwV7N5A2nSUfBQBUGMkLEL k1Zm2xmnGHrSZH3iBkQkXhIyjHaMfjvSMNXLS3d8DCrp49w6z5X8EpGZRyq1WSdk5+Mu5woAfCNv Dvp9asUcNV/T1zafv3nr++x94/8iN7tEfmGdwtImpdWLFJY28QsV7Bh3ew5hYDDS3NhVvH3PAH1m CoKlGclizWNhJkMhIxAxd2DQivv3F0anrAeDNnZYrXYDb2qLS8wsLrHx3PPY/HerBKPRkG7jhNbx Ia3DQ/rNE5qHh/zpG1v8k2+8aicetacnm8swXytzaX2ZyxsLrC/OcnFtgYvrC6wv1igV8naMuzJ2 4pEQAq2Q2s2dEIX6TBQmtaFPoSRzsxU7fDjNP2iTNqzOlAhkNWbC1E/etyEx9a1op6fC8QMTcX9D rPDjwBDlKrixGnFIL/UXCyq+79vJVcfuJSEmH6XWwXtZzhUASITyhDUFfaGJDFCtDIEWqGaDQbNB 8+Yb1g3UhuzcEqWlCxQ3rlBZuUp57SLaKFAKoUKM0XT6mmZXcX13iAoN+ZxgsSKZKUpmKxnmKj45 XxK6xJrQGDvAJTxtSJ5qVs5iiAFCu9+uty5WZ8hXKixdfopokmGNwKiQXqtF+/iIbv2Ybv2YzvEx b9074qW3bhEOBzGHoTXM18qsLM1ycW2Ri6vzrCzOcml1jrWFGhsrc4ShQmmDNnaeQKUVRgk0zrWY Fnl4UCKAgUIhTzRmP7I8xhl7t2+kxGP8QGTiR29OJIwpyZ84CygeB0AKEBJHQ7iLCqJJTyZ6egcy 1gV4/KcGh3MGANoHPDvpZtQ4lFJ88S/9PB/8j36G2zfucvv6He7d3eHWtRu0TzoER/v0jvYRb30H AXi5HKXli1QuPktl/SqltYvgZRChAhMgjCYINDt1xdaRRuseRhtyWcFCxaNS9JgpeMyUMhRzEl/Y QTNR6i2auDedJpOrtdYxGMR/jcEYTSZXoLa2QW11E422w4ExaKUYdrp0G8f0Gsf0my26Jw32Tuq8 +51rBP2+TarBRT+k4NL6EmuLNS6tzrM8X+XCygIXVmdZqFUoF/Px7MJKKUtqRhaTTLHtkyIMlXIR YrKSWLENiZlvUc25ZZGLIKJCH86MNwlGJPP8JXxAtBjRf2NIkLIejMFW/I3QlCT6EtUWiHiT94Oc KwDwsWafRMQfUAp4+e0T3ilkWJ57hqc+9wI/PZthsQZmpLl3e4vtm9vcu73D9p1tdrd2adx+h+Ob byEwSOlR2bxKefNpZi58gOLyBRePV0iCuFcJQ81OPcQc2V5cGYUEijmPcl5SKngU8z7FvKRcsBaD lMJyB9oQOgIPF7ac7H/MxA8Tmeux1aBtOW/3v5fxKS8uUZ6fR7s8d4ydsTgMRnTrdfqtEwatJsNW i1anzf61Lb71yvU4J95ogycl1UqJ1cUam6tzbC7P8dFnL/CxD160UVIjSZJ4x0Viq3OOTTEWc32T Q3Yjxx7SgBCVEkpchHS0gES5HQ8gzITFkBALkQmC5/muFkICKtEyziUJVaCy/eFjbwacKwDQ0k7g KT3BjK9oKy9G9k4/pHOvw/Wdnms0knzOY25mgbWLa/zET2T4bBUWZwBlONw5ZHd7j8PdQw73jmjs 77Dzr77DfigY5ReYffojVJ96zibtuOsL4lPbpBYD/WFId2CgbnvwSFGlBxlPUMx5lPIeGV+Qy0ry GY9izs6jl81KtDJIYeIZjpKsPxN3cMZom0xj4ggXYFwdBI12U4FjQjdmwZCvVcnPVNBcABdVMEA4 HDJotxj1ugw7HYbtNsGgz+3DJm/f2WMUKv6tz3yIjz932VoyURB/ihhhbBlzYU1qHXHt6SG7TomN SSksU/x20mQfidkRW/8xKzje+9uWEX8hKb3E0zAmVvqUEWFdD6UbL730Tx/7VIBzBQAedIPRAJgl J8cJHrAf2eXguBR3zVEr4KgVYpCxPZuRgtnSDJVCjdkXnmPjkx5zZUE5B9kMlLLgeTAcwf/5J8fc 3p86K1lyXfePm+czbvBBoGkGIY22I52MwWjlCCi7LutJPM9QzPsYA7msDS9mPIHvWf8+n89YBda2 0H/O99DKWD9eWxBQyvnybjKAMFQYrdzIN0W/N7CuhVIMAwOZPLro4eXLhMMhfqjR+3cxjRN8IW0i DQIhkvkQTj23kC52nxRWSTPxUUqAMCl9Nsn7imPyACaxAAQJP4AjCMfqAZokLThNGIJB+NKZ/8Th SEi+i7VMjCVG3wdyrgBAGO9eGIR2gM7YBqd9UxzvqNd2kTbXEDWNjuGoZbh1YNChcSajLQUeUwxG Y1yF4R+GMxYkk3YYmVgUUoAyChUYhiMbOcAkloRIkYdoZYkrZTA6dPdmMMYOaDFKx+vBAoEd+Qda hQi0Y8UNQilrbWhl93dsuQotH6Fioswg5NnlweMZkqL3HiU2QdKTu149Iuti7Y+d+oTwE9E5UmiR 7vSjw50aE7kOaZSRwk/F/pNj0iFFLZ8kAj2ekrWNzk5d7fxOIVC9FmG/S3HlEsLPuDyAFFl0hkRK KSS20i7g+SI2ww3G7vTn3FiESBJsouEFSMd7SKdkrsc0xt6fkcTrMcJl1Nlt0fDlhCyDaYhmDEm0 Qrgw2304AIEbai0kUZGSNGPvdpog6yJznsTkJ/WOo/ED8TIpAHFfxT1Hii2w9yrtnAc2LJmCiRR4 GLAg+gN+m/eanCsA8JTCExJPSmY8W9VdCMHgpM7WP/46IpMhP7tKYXGVwuplSkublFYvJTxS5Hfq x6MivHIJK0LYYck2vKVd49aATUyKO9sI+ASxz32Grk+VGDaNcESl5SfuawE4NyAqt3YKK6NeHDEG DpFi2lWO1ksNHoqsAXelJNQnovXpsf92q5Q+oYkiD2YsQUmmQcGYmCR83OVcAQAQ2/F5jzj05Ps+ oauRHx7t0DvaQVx7ye0qKSxvUlq7wszG05TWLpMt19AqRCsFRr0nAWGmnONnP3GJztEhb3z3dW68 dYNhb2DDoIDwPLxsIZ4ZWAiJzOXQWiMzWad00i4bkJ6PQTnlcyawSUJlsaagQURhzIfjAEQ8W5H9 ndoDe9aEt8cQK3PUd0fJfJNsvl2VBgUHiDFZmJCKNjYUWW/u2JhHcEGH2GWwWZ3vBzlnAJAdK5kN YIzm6gvP8F9+9e9x58Yut96+yfatHe7d3eLW27cZ9PqMtm/R2r7F3rf/KQLI1xYpr1+huH6V6uVn yZRnLaNuRiij4T3QOJqdES+/scUHLs7zF//Kl7h0scbJYZtbb9/k7s0t9u7tc+/mFgf39hkOR0iR nj7cWQxupCHg/ByJyGbtsuchpIcRIEtly3qCdR90VIrLliI7ywKQwthUYOdmGB0Z3WmJHIjx9Yme mug/0gnYSW8f4ULCL6Rn/omMCuFJy3FElEB0zZh1jK5rQeE0hfx4yrkCgJyUx4N+zzY2kTSru7td fvd/67NQzbK68lGef/ZT/Ny8ZLEKw+aArdv32L61xd72AffubLG7tcvuq38Kr34TjCE3U6Oy/jTV K89TuvgBsvkKRocYFfDD0X8/uAgM2/td7uy2CZVGGMXifIWVxRJrV1/gMz/9MywtVliY9Qh6ht2t XXa29jncO+Bo/5Cj/WNOjhrsbu0SFesUEsRgMAYOAixvUiwSZzIJbZVf4/ItpiOAQTiAkO78ekzx IifkNCicpmbGonpuzVgyUGQxpJ36+E2BJ7zE/I8Mgyi0yDgomCdjAR5PMb451lq5PACT5Ie4D95o BRx3FNzugqv5Xyp4LMysM3/1Ih/5hM/nyzA/A1kDezvH7G3tcrR/wN7WPs3jN+n96XfYHXlUr3yI 0qXnGRr5Y5uB1YY1BdKNS66fdDg+avHaG3dtSnKo0DqkXMyytFBhtlpkbmGdD37gWZaXatRmyszU PPQAGvUmjcM6e1u7NBstOu02B/f2adZPqGs/PbmajQoo6zM/0AUwlrGUUmCMtB2uBLSYVN1Epihf HByYtm9EHKY3CmKAENKF/tKDkkhA5hQoWCvnfQEB5woAgHhyjeIZRd0lxOmrQkCoDHsnQ3ZPArgj YyZcCmzx0NLTVJ75AB/+WIZSDspFyKMpFmws/F++fsIrN09NSvxjEYF7NiRIm/NgjK0NsH2vzt27 BxhtOQ0VKsuIu/53eblKrVKkXM4zP7/ExSuXef7TRVbXFvhf/sH/zY0bdwAwWjFfq7hY+kOEAYVH GAQEoxGe7yfhTpEooCGJaqRT/acCRMpijyIBk45FlCsV9+uu6AcmGfGX4gdPg4KB0bB7+yFf+3ta zh0AjJXPSq03qb9R8pqdUNNzDdUpkBt8Yoyh0Q45btopwbU2GKEwoYn9X4PGE9hJQN7jYlMhhPP1 reVg7CB/jFYcH51wsHeMMQoThmhjUKHNG/ClDaGBVZpSIeuGAj9kIhBW+WTSzU5X/tT6tFi3XKQi Bu4c8fbxg4RIQYYReNK3Pn2UM0EaUMzY/Qt3QxovuP8bfTzkXAKAjKr1Cg88j3Ixx+xMlqKvKecl xbxPLivp1Q/I+hIQ1BZmKRZz1lwVlvOSrjjPYGgbpjbQ6WnCQNMdKvoDxTDQnHRGDEaKk05Itx/E vY9AxATWj8tNeBSJzHR8D2kMnoQ4EShi04WxA5t0RLhNV1q7zUUApMCTtlz6pF0tzlhORwiEs/+N EO49phKK4sSgtAtgeQmDQXie/Y46AR+T3jUFCvE69eMneX9Ucu4AQDrFW8hp8gdvMTy4ze7JIW8f H9NpddEqGckWtVxrHcZ2IxjIl4qUygUKpRKV2Sq5YoHq7CwzsxXK1TLLa6sszVcpV8rMLZQTXxQ4 alqW/qQdcNIZcdQc0GwFNLsjgsAQ0WaGJNnoB5FQaedT2xx3XB6A9XW1LYFuXEQMFd9gEkmLBsnq 2PK5rxhX3FMIex10HF6bJnEikDHg4XIV7ifpMN1kbCCtwCLmBOLl1J5xzhACmcmAsYmg0fnTacfT QEEAvAdDvz+InCsA6IX5O91uB8+XKKkpn7zN0fY2e1t7BEohfR/f8xGej+d5p1OGU2KMsROANLvs 7+xb3XHpr0SDeowNM2ayWeYW55hbXmBxcZHa8hwr6yvMLi7w1NVV8sVZNBAE0Ooo6u0+jeaAw3qf emtIvTmwTH588bN71UgyvuA/+Hc+Rtjv8Oq3XuPN732f/e09lApBSjcxqcQvla2JKwwiW7AtXUpk NmtHDWbzaKORmZx9Ns+L4+kxoukUOGIQbogyELtP0yROBELiYcOzU3Ya67xPuxNuDEDKP0h3+JFp dVqZXWVlzxs3/R0xmKQAnAYFGQPq4y/nCgA2vfpQyFl8Ifjnu1kKn/4lfvVvzLK5CMf7Xe7cuMP+ vT22b91lf3uP3a09WvWGbaSej5fxkMLD87zIinShsAd3083jJo2jBjf0OxYotI7H7ZfKJRaWl5hf W2Jlc425pSUWVpb44MfWQPqEoaLVGXJQ79Fo9jlsdDlqDBgMR4CZ6j4EoeEf/eNXeXq9ytMf/xif /4ufZ76W4WivwfbtbbZubrO3vcv+9h57d3fpDfpu8I6Iw3yWA4mUzj2j59vnzkT5ABJZqkA2S8TA aZEMt34QB4CwboP03HUjhU5TeWKyDz91ptNhQDHx04GCEcRAI3wPT7rZh1PDhKN7PwsUohGD7wc5 VwDQBBaFHQ5czRqu77X4h3/UZqGa45PPVvnQJ56DTz7nctoBCf0uHO7ss3XzDgfbu+ze3uJwZ5/d 7R1UGDprIWMz65w/O1WErak3TVuV0uxt77K7tcNrf/YyqKRE2PzyPIvr68yvLbO4tsrGhXWef/oC 4NHtBRzU29TrPQ5POjSafYIgCciNAsNr7zb43vVjlFJIo1iaL7G6NMPaU89x+ROfZHmxwvxsBhPC 0e4xh3uHNqx574jDe7s06iecHDU4qTcdSRjYRCpHfEhA5Ao2QQhcYUQ7kAi4byqwTGUBSgSejF5O WqEjX59x0zz9YqMjplwoqk8YZQma1DEZL4NxEZ3YOnBkjr3+dFCQMnIZHn85VwAAFxBCIxDUMtZn zEhBqxvw/75U55+9KvnIU2Weu1CkVpQE1l1mcWOZxY1lIpJcCFsevH7QZe/OFju3b3O0s8/O3W0O 7+3TbpwgfM+5ExlbP+8BLSbq/SRy7Kt0ml1aJ9e48fqbseXgeR5zK4ssbWywuLnO4vo6Tz2/jJ/L 02gPOa63OTjuctLs0R0MkULgZ2yM/fikx+FRi1e/fwcVapQOQBmyWcnCbImFuRkWF2pc+MlNPvTZ T1OrlllaLpHPwtFej8ZRg3azxeG9fer1E/qtNm/cPGAQeQECV3QjUkZLok4Tg0v9lRLheeNsO1MO k045p2y0l044mziakB5UlAaTOP14HFAizElhzzgoIBDC++HImfeQnDMAAF96eCIaeJ9eb9H95etd vnu9z9p8lmfWczy1nCeXg1BB4Ip7RvX2y5USVz/0LFc/9GwctxYS+l3FwdYO+3fvsnt3m+Pdffbv 3qNxdEQYhPieD5mMcyfuVzOLlOUwDiAnh3XqB8e89d3vYbQl6cozFVauXmZ+ZY3NjXVeuHqBvoLj epvjepd6o83A9dyekEgffOODb4Hl4LDF/n4DYxQ6dEVAtEGpkKwvKZXy1GaKzM/PUC7nqV24yNPL cxwE3+TunXsQ8R8isQAeyAGYKBWY6RzA6ZcRm/IpJwHrq0fnHT8kvQ/YbL+Mn0V4dvQfJKSncabG KeY/dR7pCbJPLIDHT/LLQ6NVViPPxm/pEmQOmwEHJ4pvvDVgddbn2Y08F+Z9MhlXAlxDqI2t4Wdc tdkoWiAlS5vrLF3Y5IUIGHBWw94Rh9v32Lu7RWP/gP3tbep7B3RbTaSXRXoZhJQPJCERjjRLAUO/ P+Dmq9/n3e+9jta2cm91YY7FCxepra5w6ZlLBH6e+nGLZrPL8UkLNQpjr8QCmABjwSEeKqwFGE2v 26fT7rJ1d8fWHFAhGIU3dh8GoRMHfGw8walHEDERGE2FlmT1pFj7UweeER4c6+XHeYRIsaPQipfx 0NogZbr4Z9r3F1NBQQA6HBEads/+OI+PnCsAePHXf/3kv/nv/vu6J+VC1kuHcQQykyM2JAWgba+E hMOW4uD7PbSQrFZ9NhY81mc9ZkugjCTUxoa/IrZau1p8rty37RhtbLpcq1KeneXSC88jhWfTZYSg 1+5wtLvL8e4uR9v3qB8ccnzvHq3jBuEoQPo2OiE9z9UzOEOppHU3InVs1Zs0j1/BvGRrAZZmytRW V1nYvMDzF1YZeXlOGl0aJ00G/QfVL49AwpruUni2K06VzxYCdGrmnAcnArlzuanOEj1MjomnTHTf KhmNH9nvaVsgmVQk/X2J97CJP3HiE6QY/2g5If1Og4IANFKI9v3f1OMh5woAAKT0DUKwUEhMRpnJ cv1//i2K609RWLlEaWmT4tIm0ss4MsimtfoCjrua457m5Zsj8hnYmJMs1SRLMz6FrIdS2s4YJJLJ MWOdMknFXvs3iN0JfMni5iaLG5uYTwCu5wyDkFb9mMbuHgfb27QOjzje3aV1eESv3bKZipksUnqx co6JcIrmQGE4GLH/7i12b9zEGE0un2N2bYO59Q3yq3P0tODkpE2/23sgGEyKzX00ceUgeAgXQJrx SVCjm07d/zi/l4q6TCRKJDP4Tru3CFcEmWwmzhyMgNlGEpNBPmbsmkkkQAgIQy8Gj8ddzh0ApCfW jNu3gN5JnWGnzck7r1hF8nwKixuU15+itHqZyoVn8HJlF+lSCGPLX988CLm+a3v8ck6wXLOzBS3O +OSzIiLFXZmuKToVh9Bt7b2o7n88SYg2FCozFCozrD7zDODFiTn9bo+TgwNO9vep7+7SOTqisb9P ++gIpTXSy1rF91yd+5iXiywEjzDUHN65w8HtWxhtKFRKzCyvsLiwhCrM0O2M6Pe6D/dyYyIwUaQH JQJhJMZIpPRSUYCJ9yRO/4ijAiKV7COTXvsUuec0Wook38BEyyYhFq3xZ5OfovSCJA9AuKiBwXt/ 6P/5AwBPGDzpkS7oEI4CfvW/+irXXr/B7eu3uXPjDmoYEGzfonPvlmO2DfmFFSoXnqGy8QyVi8/i 58sYFSJkgA40/ZHi3f2Q6ztDjDZkMrA441EteixWM9RKPllfEmqN0BE4pMwEY8Yafpxao1MzB+kg qfOHYHZ5merKMpde+BBxqpCA1vExjd0dWodHtA73adfrdI/r9FpNZCaLED4ymoVYWGZbSBj2hxze uo25eQspobywRHl+AQpleiNNOBo+4A27OQGN5QIeGAYUBi8aa5Fi5WMlfZiPmspSHIsknAIOQcbP IH0vLqw6jfSTAqLKxG5EQzzPo62d9n7JAjiHACB8O7jHS5twRrPIYRIAACAASURBVHOQf4YP/sIL fO7fz7I2D3u3D9i6tc3tG3e4884dtm5t0drfob2/g/jOvwA0xfkVKheeZebSB6isP42XLyLCEVoo jNJopditK3aODN939f3zOUG16FEreVTLHtVylkLGVaNRmjAiE6fU/idlQUTxaa1TE4JEf4Fsocjy U1dZvPK0nRDE8RrBaEjr8IDm/h7dep3O8THdkwa9kyZaKYTvIaWdQcEYQfvwkPbBAQhBfqZCrjqH KZQZKjuPwJiGqpAoEUgZV+j7PjODxPMCCmmn4nYBeZGaImxch8/udhNWIF1d2KFJyn3ws76L5Yv4 7Pcj/eLaipEr4lZ475NEgHMHAL1Op4WQi+vlhEySAq5ttXnzbgc3OJ3ZSpbl2We5/LMf5dP/rmCh BmaguX3jFvdu32N/e597t7bYufMa+y//S5RWlFYuUbv8HDNPvUB+fgVjlO13XHjQCAgCzeFJyF7d hsyUNnhSU877lIoeM3mPYtGjUshQyEmkL+NKu7FZasCEEQQw9hdwk1cajHLuRGq+AWMMpdl5SrNz tvqtsa6JUiHDdovWwT69ZpNuo06/1WLQajEaDMAIuo0T5EkTIQR+pYJXqqLyBaeXBhOG9trKuLLg 8r7DgXE9vxFmDADiQPwYvZ8i/k6dJ73PuFInecE27demHut41OCDST84DQoC/f7Q//MHAEEQ7ksh nkoYZCuC1JwAErq9kJs9zY3dEVGjzGd9FqsXWHz6Mh/6mORnS7BQBd/A0d4xh7uHNA7q7Nx9jUx7 i+oHPs71Pc1gFJ66DymApLwAnX5Aqz/6/9l78yBJsvu+7/NeZlZVV9/33TM9987ei8Vyl1iABCBc JhniAYmWTJOUHCH7HzPCofDfVjhshf/RHzZJ26IVEimGITugIE0qaJMhkAQJLLFY7A5md2d2jr67 q7uru7q7rq478z3/8V5mZfUxMwsQwMw2HrDTdWRlZWbl7/t+73d8v2yGYh3KdNUlPUF3yqUrKUl6 DqmkJJVw6Eq4eJ60hB+nCINEHoOJGRjP3AbptLJde1YtSGtkIsng9Cz9UzNGKchG9/1anVqxyGFh n3q5TKNUolapUMssg+Pg9g9BTy/t2goV6ec9iBUYJRgbHQLs2vzYrGoN0wb+Qmmu40Mc+UuHt6Bt 3tBLuDgClI6l/kQUgnkEUDDeiW9B4KMwzhwAODb9I4XkYQKPQpjtw6yb0ordYovdUovAbwfrpFAM dKfo7Zpj6Nw8M3PP0dfbRSu/x4vzad64e/hIt4vgCNW4EASBpnjYIF/WtsnIshJr4z24DniOQzpl ypCTrsRzBI401X+OFHiuKXxQgcZ17bkHEAhTuSscEEqZdUJY0GAilwYIpENyoJ9Ufx9KBRYwQAQB h8U8tXyBaqmEHzYV2TW5OYeHFQKJdhrwaHozivTFr1AICvZ5ONHT6SOE70cQIiQJ10NpjRNG/gln +naU/zRQMNuYv9IVOOKjYTofjbP4EMPwWTpIGeNzEAIdtHC6+wydnQRUXEfuyD4w3kKoBaA1FA9b FA41aztGcANdszNw5W9FGiD0ciWgbTWc8R40zVaLRhOr76ejTAI2q2CcggCtVaRbiFakkg4q8M1M HWDYgLSyzxWB8k1UPDACJxqFUIpGvYmyoiBaBwil0F4X2k1YWXRzDQzL1iOwAtvZ/5gH0F6mxx8c eRw+j4NCO2qPnc1d19zqIchEQb8wgPggUIAIDIRwaDRqSBHsPvKP9xiPMwcAoQDGbF87EyDdBEv/ 7n9BaU33xDmjCTAxS3r8PNLzzOx4NDF8wghncOthRpPph82nf68jbivSPtC2xBlti1hUO3BXr7ci dR+tlVEPsueqdeyxUhZMwhJfHYHP0SuiaCvoAg9UBxaWEBS7/pdhyWR8Pd+5tO84V93x5nGAiF92 1/VOLfYxX/looAAYWnUpCyef1ZM1zhwAOJwg1qOhpTWtcpHm4XsUlt6zsQDXpv6eont8jr7zT+F0 9YAOUC2rB/AYUICfNlQkDCIsfVHYbis6ST+0CegLodqz6Pc6RPt7AePin7I/qW0VoNadpcBHDfoo goQR+WjbjpePHb+UkoTnRFWZ7YKfozoA7T0cSw+GyxrsEvKJ4HB6+DhzABAEerdWreGEVX6A32ry T//5f8vBxhrL91dZvb/C+so69Wqd5tYGh9sbJjWlNenxWXomztN77il6Zy/jpntRfhPlW5GQxyZF LPjCpy5TO6xw81s3WFtcA224/hDgJVPgJUArZDJt4iHSRbouCIFwXLQfRHz6QsdYlGMzY8fQ2mYg aKfyjk7O8c1tOzC0i7OijTtm/hOMXLS1HaK1+ilOmuclbDxHRIYvaJdnm0M/IRbAcVAwfQsSnCde GRw4gwAAFEBHhgAmUPUX9zWTw1e4+rkX+NQvuwz3Qq1QJrOa4f7799la3WRjNcN+dpPi1jryxl8B gu7RSXrOXWNg/jo905eQCRfl+8adDn50N4lG8/4HGZ67Ms5/8U9+nsHeJOuLa6wsrLK6sMb64irb K9s0m0YUREYkIAIhHYQjDemHdBBeyjQJuZ7tlnKMFoAQKGHW1UdLmJRVHX5wM5CZR6vVMp5nqxXD 98BSixEBj3m9UwU4HieMtocO4Z+Ezf0fY/aJR/pFGxQMYdrJoCDsOu+jMf+fQQAwLDeS0bSkK1bP Wa35LGxWWMjUzE2uBH09HhND55n6xBVe+hmHkQFIO7CT2SWzkmFjbZOdjW2y64ss3Pg6vq/ombtM 37nrDF55EberC03wI7lZBLB9UGfjjRX+6K+WSLiayeFu5s/N8PwXrvGz/3iY8RGX4n6d7Y1tNlc3 2M5k2clk2cvukd3chmrdFumU6GAKEm3wRIMem0SkuuwXa4QOpcHkg2MAVhnIpNSciLL99BWI6LR4 QNOO9IeswpGhCnAcF0dIlPXgorV9Rzyg/TgEBXkKKID1ouRHw3Q+GmfxIYYEHMe4nke99UgTwP5X bwQs7zRYyTYNKYSUJBzJUM8AQ0PDzM28xIv9MNQDgz1QKTbJ5w4o7uc52NsgMTxL1u9naav6IzhT Y0euFDieBK3Y2jtkM1u0nP8BKgjo600yOdbLyPAg5z42x8e/2M/46AD9A4LDgyY7W1myVjFoN5uj tF9gd2uHXHYvaohJqQBLlm7bo4UVz9QRaJx4fGFXoV2aHM0CtOm7H5QBOOH12NogkfBMOlIJiGZ8 YT8hTIFUuLWOu/pEj+OgIIWtvXjYxX9CxpkDALMmdTjtRgqLZ4Ruu5HRxwT4CnJln51Ci8DWkyul QGmSHvQkEwz1zxGkhhlJduM2q8TDUz/KIcAYpGuUgrQrqNebLK/mWFjOon2fIPBNJR8BPT0pRob7 GBzoYWR8mpeeeYaR4X4GBnrp7nbw64pyocS/+bd/TCYTZsU07Uo7EWksnHg8OpQFE0hbu9AW44of tf17Siags/GnDRhCOLiuY1KhMXLQ48bNsWXAaaBg5gFhy6Wf/PHROIsPM4SrtQpwHHtDSaMNkE55 9HZ79HV7dHkSzxN0JR0SrkRaN1Y6bYIrIYweQFNBq+lTb5jinHy1RaXmU214LN0/tJ3ElnUGUzsQ L1h5HIYQ1q11JY60DEHagMPGRo611Sxa+QR+QKACCEwGpKsrychIH6V8OWZVphdAPFIMwOYkhMDB McG1aNMHXJ2YZxD+FrE3o8+6rnX/ZYz1p2PdbwDkUWMDgKEvM4Uij3x9H+dx5gDAEWqlXC4zMdzP ObnP+toGyWaJYrnAdrlEo1KneliOzf5m3Znu7ybd3YMGBoaHSHQl6e7poae/l8GRIdLdKcZmprg+ 3cXAUMrwBwpoNqDpQ76syJcb5EstDg4blA99DspNS+KpbYGPOcbHBRigPeMhTDWhqwUoidYOQeCz k90ztQUC0NoYdFhBCA8EgFCRWCAs0WbnbH9sAWCj+ELE1/4xTyBeO4AgmUygtcZBdngIcYLPk0Hh 9GUACJTfQsgfE4I8mcPzfNcV7NU1T3lZ3n7rT9lc38ZxEziJJG4ihfQ8Q/0dG839IoW9AlprNlc3 bLFMWGXX1gFAKxzXY3B0kLHJcfqHBhgaH2FkfJjxmSnOXxinb6gPW4xHuQa5fINcvs5+oc5+sUHh sEGl1gRhqw2/R0TwA1MVaEpyrby3MOq9Qqto/Su0NayoMEY/tEz6pKEx1cSmXDlMsZx+/DpSBg6s Wy07tm0bdmzI9jvHd9ye/UMOhDB0F5X0CqPuC9CWDHswFXgICsaFs5JpQvyYEuxJHF6g8BBkij5/ 0nqef/QvvsjTc4LF26usL66QWVpl7e4imbVNDg8rOE4CN5HESSYR0jXMOx17PHltX86XKR4UY/X7 bR2AZDLJ6OQYYzOTjEyMMTY9wdS5Ga4+N4GXdAh8qNQC9gtVdg6q7OxX2S/UqFSbBNp22j0EFPxA 8es//zxJR/POG+9w57u3yaxkCAIfISWJ3n7QZu0tU0baWyRTgGkK0hpIJM3OHBcRUWXHYiPadMvH h1AaLQRaWA/gAYVAwhYCCW069aTTDs6dOqI8X9wDiGcFzCael8CVYeNPzLCJpw2jncYKguIdgMdB IaxVSHw0dEHOHgAEHigpuDzs0aiX+eNvbvC1pMtrT0/wqZ89j3QsCYSA/WyV9cVltpbXWLu3wObq GpmldSsSYjwG6SVNajGMYEdW0tYBOClinMvm2N3atW26JpAoJfQPDzI+M8Xo9CRj0xNMz8/zzCsz aOHQaPjs56tk9w/Zz1fZOahQqzXbrm9suI7k3/7RTS7PDXD1qeu8/oXXGRtKsZPJsbqwyur9NbbW MqyvbFDKrEauelgP0KbNFuC6CMcB6Rr+f2FrARwPLUG4Cfv11ogs4QmiHTg7aQihEVauTIZeij2T iPU3MvhjUMPJXoBZdiQTbqzxx2yvo2lc2DLtOCiIKE0YVhieBApCSFxHYGpKn/xx5gDAU4bMIawA cyW0fMXXb+7zzVuSFy/28PyFHtJJQXdfmisvPsO1l5+JkgdawdZajrV799leWWN7ZY3VewsUdg7s EqILJ5FAukm7dm4Hx+LeghAC4RwvKa2Wqizdvs/i+/eiVl3HlQyPjTJ+bpaRqQmm5s8z/9QkXtcc lWqTnVyZfLFCdv+QQrmKpQ1ASsn9tSJ3VvKmeSdQTE/2MT0+wOwrr/ETPzfA2Eg3fWnIrO6xvbFN dmObvd19shtZsptb5HMFpGxGYCBiXXthBF0ODCP6+u2JmaCeaSd+cAwgVAYCo7YTry8wnn47BROP +Mf2QIdmoX3gupb0M0SgmCsfGraOHrc/3BEb4GRQCEHS+WjY/9kDAClEtVavMSg6Dc+xDTw3Fiq8 tVjj4kSSp2bTnBszqjl+oG11mGBoYpThiVH49CeiGa5eh/V799leWSW7usb63QV2NjLU63UctwuZ SOIlEraCLrbWPQIMcc8hPssU9vMc7B2gv/NddGCIPfqHBpm6NM/w1BQT589x6flJtJNkP18hd1Dm IH/IQamG1ALHleDAzm6J7e08KggItEK1WggBk6N9jAz3MDYyxbWr1/mpsX6GB3vp6YF8tsJOdpe9 nRy5rRyFgzy7m+b5YalM4AfmRjKB/4hfAB7BAxASRygcCa6UJ0Qe2qDQXuET7TTyA2Jfkkx6mAsY FiQJOtN5R0GBjrV+Z/lv5zJAyAfwGzyB48wBgJYy02y2ECf55ZgiwKSE9VyTtZxPynO4NJXg6lSS 4V6HQBl+fz+IaQFYt3L2yhXmrl6JqtC0gPxOge3lZbJra2wuLLK9skxuexeNxPMSOAkDDobOOzR4 fQIwxHQA7K9WrVa5f/N99I13I3GQ4YlxRuZmGZub4+nzs3jdsxRLNXZyRfYOylQqdRAC15U4GrR0 QSty+RK7uQNuBVYMRLVQfoAjJT3dSYYGexgb6Wd0aoaJa1f55Ogg/X3djI6m+Pf/91/xl19/yx6v nUnDYptHKARCGLFNIYWBvI7tQ1Lw00ebTdh4Pa5jG38wQGuCfkfYfR4KCvb9KAAorFclkK7E+fES 4MkdAhhKSbq89o0mHBPgQ5vqNWlzw4HS3N1qcmfTpy/tMDficWHcY7hH4AfaUIBjO+BUOzOgNGih Sff1ceHFF7j44ktmNpTQbATsbW2ys7bG1sISO2srbC4uUSmVcbwk0kvhpFI4XgJTThszgiPAII8I 1RX29jnI7XHvO++gA0V3XzeD4xOMXZjn6uwcif5J9gsVDvbLFAplmkHQrm2QRpwMqXG0i3YMAWat 1iBTqbC+tkWgFPg+gVKooAVa4UpDXWYupMky6CjDcHoSo10IZDQYxAmlwNE8L9rPTvpFQ94gz4v1 /XNS0C8+o39YUDCvSfRHJQRwNgFAK9vTHgaXpMvBu9+gUdglPX2RnskLpEYmCYn9pQ171xqKu5st 7mwFpDzN7JBkdsRjrM9BupogMLONH9qnxqYHTX99xMGHZnB8gsGJSZ569RMm4CUl9UqFrYVFdtfX yS4vsrOyQj67Q7PZMsBgg46Ol+jk/4+DwhFPodFosb26xtbKKlopPM9heO4cQ5NTPHX+HE0nxf5B mVKhbIRBThlxtWCEi6NVpBjUIQyiwkyAboPKA2IAQts05RFa8LgXIGL/tvEhti7QELp0yUQiWsNH fAWireoTXqlofS/aOw09BSHiLc2dLcOmN8L9qNj/2QSAsE49+lkFtGpVDu7eoLj4LgBOVzc9U5fo nr5I3/zTpAZGjatp3cKW77OU9bm31UJKzVivZLzfYWY4QXfK1MYECBPIo72GtIzZZobUGq18I6Zh 3f6pS1eYvHyV5z/3eUMJpjTF3V121tfIra6wu7rMweY6+d19czMm0jheAjeRMlH6mERXCAxm3Qo4 Rokot7bOzsoq+o1vkkqnGZyaYnh6luTcMOVaQKlUpl6tffgLq0EhUAi0Mn/jcdCjQ9qAoUBYqrZ2 JVSHkYcP49N5/F0JpphIGq1FrSx2x0p7rQt/DAjij2Px2rZqUHurkE1Yax8tnRil1JM7ziQACJta CgFABYqXP/UK9VmPhTtLbCxt0Cof0rx/k/zCTeTX/z1udz890xfpv/As/ReewevuRQcBIghQqkWu 6JPNt7ixXKMrARP9LqMDDkM9Lr1dDkEYZAog0O0vjyZvm5ZSOohaVw3VN6T7+jj37HMR978Ugma9 zsHODvvrK+yurJDf3CS3vkKjVke6SZxEEieRQnoJEA4yan4yByJtKqvVbLG7vEp2aRkB9AwP0Tcx Sd/gEE2RolSp4tcfpgUQnotuu/9aAco0UZ2CAKZ9V+MIgRtpIcZm9vYvFo8FEkODjm2TyYT1UCQy NmvLmKqPUp1cAGE68KGegn0sHQe/1cIXevWRLspjPs4iAOTL5TJKaya7Bbs1jURRT41y9Ytf5nO/ 5jGUhrX7ayx8sMj60iof3LhD8aBArXSDvbvvILSma2icvvnr9F+4Ts/cNaQrEL6PUAHNls/abpPV XUUQaBIujA+49HcbgZD+tIsQAj9QIK0XHWtACUdosIGKA0LI/S8YGB1nYGycCx97jTDyWDsscbCZ YXdlifz2FgcbK5R3NwmURibSSM/DdVOIRLIddHQMhTdaUykUqRzk0VqTTHfRMz5Bum8Q3+uiWqk/ nONAtSnBJO1y35OGCOMXIlwGtOsAjlj8ic9DozV/hGn80bG+fYg9tsdknYwIFDDdi0dBgdjvEfcU hP2upJRHf64ncpw5APB9P9dq+WgESU+ga+Z33MnX2TlomlvCkUwPDzJ++RN85vXP8p/9N+C0AlYX Vli4dZ+1pXUyy+tsvvXnbH77a0gp6Ltwnf75Zxi68gJOugftB6B9hA4IAk1mr8n6jrK95YqBHpf+ tEN/t8tQX4LulIMjBDKwFN9aoP0jNBvxJ5GEmKUQ1yYIKV2X4dl5hufmjW4nApSgkt8lt7pMfnOd wvYWpewW1fwBOAncZJcpaPKSCNdFOua2aDaaHKyvg1rF8Ty6hoeRfUM0PZeg5QNHYwbGVELJbTDh udMrAbWNAWgTA3BOa7KN7UEIwlYcgfEi0BrXc3GktLRfJzH9PAAU7Osng4KRCWsXApmYRuKUI33S xpkDALCFHrE+cAij4ESR+p1Cg51Ci3dXaiAESU8yMTTD2MuXePpzMD5o0oVbK1tsrmywvrrB5tIH ZP/4ryj7Cbrnn2X42sdwu7qjKLu0TMJaC8si3EQFJkAI0J10GOhx6U45DPR4dKc9042IRvsaX5ob 0dTYGBPQR+Yhk41QUfDRPAcv3cPUU88yce0560BrWo0G5Z1tDjZWKe1uc5jboZDdxG/6ptLRSyIS XThuAt8PONzdhZ1dnGQCr28A3dVNIKUNlkYLKpsFEShC9d8HBAGjLIAk3gzUNvjwn452n3a6UANS kEx4oC3tl3hU1t9HAYX28SgtcBynU1XqCR9nEwCwAhoP4PM0rl4bELSG7YMW2wcB70pJECg8B/rS g4yNjXLt8iv85C8KhvqgJwHlfAXXcdg6dPjTtw5o+sc9xhAUDGOvptrwOay3otJgrRWOFKSTDr1p h1TCIZ1y6O1KkEhIPBeCQNrMg23gsbUJCmIdq+1yY/M3iHoTekZGSQ+PWs/EfKRRLlLczFDKbVM9 yFHe2aK6t48Wjqlb8FI0SwVkIoWb7kH09SO6us03BTAxMoRSPqYd+DQxD+x7Zm3unKAL0OHic7xR KBwm9y8IHQ8dRfTilN7ttfzDQaGd/jsKClHXovfR8AHOJACgQSvBeI9g/RBzQ6gA2dWF1MbgVTz5 a0ccFKQDaE3x0Oeg1OTuunXdlSZQirSnSboav14hEF2PfGjRd8SAoVJrUa42jNsfc/1dCemkS8KT pJKSdNI1QiGuQzLh4rgCF0EQgAi0iTfYcj2NMNJkVolIB4FVI/JxHY/B2fP0z84ZYFDmE4e5LJW9 HJX9HJX9XWr5Xeq7ywjHw+3uRwVNImEQZWIAD/QATPLfZCzDfgpxspVHPkFHE5B57tnqSsfRHYYc pfCiWf4RQMFM9YQ8AeG+4uxEp53PkzjOJACYaLsi5YYpZElx4V12v/UndE9fIj19kd7pS3SNzSCk ixAKYuWhR0ck82XKBXAk+ErgNzWadNR++v2M0BOWYfl8BA5NDqshMKhIFxCbXBRAV8JBSiLFIEeC 65hZVwUBCc8UQCllHmsVmEKfUAfAN2XDXk8fg+ke+mfOmXSmUtQrFSr7u9QLBxRWl+zRKhCmdFo+ KAagNAO9PVQPC2YJEMUAwsBeeMU7QUEceZJIhMG/TuN8YLHPaaBgd3saKDiOgxAC70P+fo/rOHMA UKlU1guFAnYCjIZS0KjV8VfvUlq7y44A6Xr0TF+ie/oyvbOX6ZmaNzez0ASBD8Hjowlgml/ABvMJ b3StNbVGqy3qYQ2dwCgAqcCPuAHC1wg0Gh8dGMFxFSgEvlEIsqpBCm2k0YUhANGJAUikzIyqiPgA HqwOLEglE5TzTdOWHCsEMhOuMfCoDvLofoTAcdpdi50c/53r+9Div19QMMu2H3sAT+xIp9N1k+Vp d5EpFfCTn32NSz93lTvv3+f+rfss31uidlijsXSb/PJthADXS9Izd4WBC8/SO3OJrtFpgpZvXF8V dNyAj9VorykQCIQIwriaLZkVpnNOWvUgabMQVkEljFPI6H1TKh2+b4Yi5PZD0uYDeEgMwAQA2oYc SwJ2kH/IaB9h+s8cf9L1kI403opoG3m726+T7uv7AwVMpkIptKc/EpxgZw4AwPz4gVKMdRlYF8Cd 9Rr5kUGmn/tpfvFzX2J6BJqlKre/+wFri+ss3l5kY3md3L132b/3LqBJ9vQzcPl5+i88Te/sVZxE EuX7BKplOoZ+lOcIjA/3oIIWy3eXjYcgBG6yy/T3o5GuhwoEoTSYwAibCDDegurwtx86Qm6/MBCJ eEgMwGYAHEwOP5zx2+v9cMsjbMHW9RdC4HoOSmnrPRzn8ft+QSH6bJRUEDQbDUSrFa53nuhxJgEg vDuTTmxVLzTZgwbZ/SZv3y+DlnSnHaaGn+HKZ1/ik39PMtIL5f0i9967y+baFmv3V1lffpftt/8S ISQ9M5fov/Qsg5efx+seBG08gx/JKSo4N9nLTzw7zcCXX+Ldb7/L0r1l7n73u2RWNowcuJfA8Tyc VBonkUB4CUQiZYJxXhLpeWjXQ+JAeK00pzICGY4Dcy1VYFIRD6wDwGoDWoEQnJjotjXW9nPzqN0W ZUDDtD2EhtppzCeSe2Ipvex+2rP/6aAgYiDiug7Slezs9D6m7t6HG2cSADTYhqDOISDSBEBomi3F SrbGym6dkNxuoDfF+PDLXLjo8NovwNgg0NRkljfYWs+QXd9m89YfESQHcacuI0Yu4vs/fBCQEt58 b4tvfDdDOiG5MDvIU69/mk///V9gtN8jm8mxdHeR1furRuRkeY1atR5x80WKveHywXEQyRTC9awy kGEIwvOi5YWUEld6lpBEQ8gudKoHYDrrpDT9ClJKa4i29iH09UXnZ8KnRvEnNHJj3p1uvPl8fGnW Kfhh9tehAsSDQMGSl2oBl77/3+hxGGcSAFTgK42Ol4tHI3wtdIU7/hNQrvgc1hT3t1Qkw510NSO9 I6Ys97LLCylIOoqRAY/A13z1m3sUyv4P7wTtkFKQEBI/0NxZ3OPW/awpPFKKmfFepqcGmX/tdX76 7w8zNZ7Gr2s2ljdYX1wls5phZ3OHjdUM5ULJlPTWa+ZvXCGINiOQsNkQAGF6pB9cCiwkWkhbt6AN lVg4a9uKPx0rFArLi7UIJcxMN2F7/R7O1G2PoA0OxAz6hGIfYcqM2qBwkqdgPivER2LyB84gAPzG b/xG41/9q3+9qbScHU4LlHQQjktPOkF32qGnK0HSEyQ9h66EYyPN5rNSGNe3WgdQaF9Tqvv4rYBK PWBpu8XdjKJa8xFagPBBG9qxH3Xc2KTYTSmiVprdXIntGxl0bwAAIABJREFUnTyB76OUEQNJd7lM TgwwPT7I/E+8yicmhxgbHaArCbmt/YgybCezzXYmSzaT5bBQJtmjcDH7d60b/+iMQMa4hcIEFcOh tdEXCCsXwh4Du/b3XNdqEob7smXP0Vo/vqaPB/bgZFAIlw32ep3oKQhcKUE4HxUH4OwBAICUjlJK MZkK6N1+l+ruBtnDEo1aFQE0ajWk4+AlE4SI73oJBkZHSHQlGRgawkt4DI6NcG5ynIHxQXoHekim TC9504fSIRzWfEoVUyhUqvnsFxoc1nwTFhBEMuU/Epkpu3Y2KkEu2lX4fsDqeo7VlW2CwCcIjMCp 5zqMDPUwOjbA+NgAT33yAp8eH2JkZJDuLvi//t3X+OYb38URZlYOW55DMpPTAcCUAndmT8J8m4iq 9MKPh8xLwn5WRVvQjjTEqMANKGCLfux2HwoUjnsKQoLzYw/gyR5KBThScGv3kJHWGsvZJbbWNmkG ILwk0utCOG7n+lVrtta3zAwTRoWVzYfbCr3+oX4Gh4cYmx5ndHqciZlJRsZHuHJlkoGhBFpDqwX7 Jcjla+wWGhRLDbIHdQqHjehm/pEAQvjdJtGNELbm3TVLhv2DIru7B7yngg5GIFN16EWuvus4gEQL U4xkWH9ORgCJ8QCQkkAcP28Rs1wdZgc0EciE5KfRtgCiDRtRHP8kUBCio0CrUxb8dFCQjgPOKYj2 BI4zCQACEFqxUO6h+NSX+eVfGeP6PKzfz7F69x7rC8ssv3+X7Y1tarUm2k0gvBTS8ZAPoIJpNppk M9tsb2yZZpygrQXgJZNMz00zeX6GydlJJs/N8PTsNIPPj6M0NBqQ3auzX6yznatwUGywV6wZ19ce 9Ie97ZQ2wBQaNdpo9YUVgtpEQxHRDf/gYSjDJUcZgVrNpnnfztpShPLg4cU+eX9amDZkqbXpy3Da r8csu/3HWrQUMsZCZA0UwvJ/wnBeO4NwgqeghanwtHtQNsjzIFAAUz15VBniSR5nEgC0NOq1Xa6m WKrxB3+xwTeHkrz2zAA/8bnXeeVzr5tWWgUHO2XW7txl7c49NpdXWLu/SrXWQDsewk0YsZAYR3TI ayeRx67u9voWW2sZW3Ov0EqRSCaYuTDH2PQk0xfmmLl4nksvTOElPRotze5eje2DErm9KjsHNfPd j7BsUApee26Wvp4kt29+wAfv3EIFCikFbrob6TgILwGOg0wmjdW5DkKFbLrWvHSoFhScUgjdHkIK tDRBvdBLfqg2oIibMUT+uog97UABAUITRAq/px2VOAEUzA7Nbtrf+2FAQUqJcJyPzBrgTAJApVwt ow03nzD3PQeHLf7D3+zRk/Z4+UoP12e7EBJ6Bnp5+rWP8/TrH4/WsoXdQzYXl9hcXGL97j0yS6uU yxW0TBhvQZg02dHb3oCDc8yLyCxvsL64xnf+8lsoZWSyhsZHmb1wjsmL55mYneHqi3NIN0Gt1mJ7 /5CtbJncfoVCudY21tg+pYRbi7s8f2mYn/3ZT/BPfv1zZFY2+ODmHVbuLrH4wQL5/bzNo5u1uvQS RgHJ9RDJLoTjIDwPnCTSc9GOa1SC8K33oImblrCluQBBpA34sBiASTeavgGi89C06w2ij2sTMFTh wl7EHQVxxGk4yUZjQBJme+y/x0EhXHJ0goKUAr/ZyP7mb/5m8+SzerLGmQSARqOW1fBMnIReYICg 3vT56/dLvHGnwjNzXVyb7WK4R9JqalvcJ0j39XD5Y89z+ZXnTXWAhMJ+ha37S2wvL7Oztsba3QXy BwWEkyCQCYTrnmoMQgqcCBXMT1I6KPD+Xp53v30DHZjZd2JmkvG5GSYunOfihXlefuYijaZid/+Q ndwh2f0yhXLNsPJIOKw1+eubm3z9nQ3QitmJfi6ev8xPfewVfm12gJSEzMoWy3eX2FjdYHMlw+ba FpVC0dYDmKCeFCLG2CtMJaGXtPUALiLVBZ4XGbO5StaY5ANKgaW2acDOSHy43GmXGbWRQEgZdfGF VZxmCx0tEdqhwqMridNBIYoXdHxGc9xTkDiu2zxlZ0/cOJMAoISh8x5OceLa15Hmx7+1XuO9tQbD fR5PzyW4MJbEMY1zRiikFc5UgkRXN+eff475l56LbuZKqc7O8jLbi4vsrq6ytbrO/m4OhUvgeCAc hHROniFtXb1ZSpifaX93n73sHu+/eQOtAhLJBNMXL1hQmOfyCzNoJ0E2Vya3V2Ln4JBKtWGJMiGz U2Rj26b+Ap+E5zA52svM1BCXfvI8n/qlQabGe6AFa8vrbK5ukM1k2VzJkN3YJp/PowMQsokUdQsK 4A6NIiwACBsyV6GLrh/gAWhT8INZ0kcEomEArs3HZ41f2J5/oTqMtb3Wj7kE8Sn+ETyF9vd2vtMJ CpZI5nHt+fgexpkEAAITIAvbgcHcGMLxENZYECC1xpFQrgW8cafOt+43mR32mB/3mBkyJBRBoFFC YRvtDI2XNsUqbiLB9LXrTD11PcqH16sNdpYWya2vsrtqpMUOdnK0tCAQHsjTQSGSE3Mk4KKBjYUl 1u8totRfgICx6UkmLpxn/Px5Lj8/T80X5A7KRilo/5BAaFxXIqSL1orN7TyZzT0CFaD9gCAISKUc JscGmBofZOLpZ3nupz/JxPgg/X0e+9ki2xtb7GWNlNhuZpudcoOKIkoDAra3gIc2AwkBMgYSpqU3 fExH8NMEK9vFOR1RAIEtCjr2JdGPbEN6J4NCx3edDgqqI57w5I8zCQAaEQW3zBAErQYLv/ff03fu adLTF+iZvkR6bAZC91QohIDNfEDmQCEFTA5Kzo06zA55OFYXIAj55JWJMRiK6lBFyBjDxOWrTF6+ hpICKSTNRp2DzQzZ+/fY29ggt7FBbjOLrwW+dFEYUDipC9UoCmHq9YH9nRx72zu8/41vIQQMjo8z NjfHxIV5rn3sHIVyk53dAvlCiUZdGaCLovvgugIdKDJbOdbWs4bgxPcNfbnSDPSnGR8dYGx8kOmn r/PSZ17nne/c5s03bxqvxTQDWEowHlgHIMM6AHTMA7AzvibKxbd/t84HxmZ1x/tHQQEdywZEF+0E UDjyHeaj4hgoYKsFPyrjTAKAoavufEUjaNbq5JdvUVy5ZVzbdA+9c1fpPfcUvXPXSPQN2inAdM9t 530yez5a1xjtk0wOOswMe/R1ObQCEyCzGiTt79EarQILCm0Q6p+cpn9qjmuYNlwdBOxvrJPPrJFb WSG3sUF+N0et3sKXHgqBEM6x3nTjJTiE5YvFvX3yuT3ufudtJDB6fo7hmTmuzs2hU73k9kvkD4rU 6o2OzIJAGKEOoXGEC9poHNSqdVaWNllcXEf5hmfAESYFGNbKG5OyK3jZ4aR3XnMZ1gEcaTw8cUHe qe4bO9CO7SJQiAX6wg2PZQLiezsGChz511wTpeCj0QhsxpkEACnIFYsl+r0UmgDQdHUl+Gf/6z/j 5rdvsbywxt3v3qZWLlH/4G3273wHgSA9OkXvuesMXHyG3tkrgIvwA7T2OSgH5Ao+N1dqdCUEU4Mu o/0OY/0JEp4kCAIzK1qR0fiIwEBZIk9t5qT+8Ql6xyc59/InQAiCIOAwl2V/dZniVoZcJsPB1jb1 eosWLr44ARTCmn3rmu9tbLK7voH+5jdIdXUxev484zOzJCfHKZTr5AtFmvUHB7iFFDhaIl2jK2hy 8hpHClzpoglTZ+qhMQCBMEsAixdR6o5wCWBDemHcz74WGbAmtv/ja/f2807rPh4ofBRQCNmWPjou wJkEAKVEPvB9epNtZuCGr/nL1V4uvPRZfvFLXUwNQW5zlw9u3GHhgwXuvXuXQnaDUjbD5rf/DC+R oG/+KfrOP8vgledxU92IVgutfZrNgKXtBotbGijTl3IYHXAZG3AZ6UvguZogELZQB44hAoAFAqMD 0IpAoWtgkKnnX2bqhVe4hgAdcHiwz+F2hoP1NQrbmxxsbVOtNWjh0FLCxBRkm0dfCgekQ6vls3Xv Ppk7d5GOpH9igv7JaRLj41TqLUqFQ1TwiE1MNu4hbDLD2K18aAxAhksArJHGXLNoBaAxvQKxmb3D 3Y/W7w+J8h/zCj48KEQxhI/IOJsAgFHnidudAPLlBt+50+StOyW0kIwOJJg6/yqfffWn+LVhCCp1 Fj5YYOXeMvfeu8fawh1yt2/A/wvdE3OMXHuZgSsvkuwbQvgttA5QgaJc8ylUWixkNL4K6O9yGeh1 GBtIMNDtGeUgKyEWaBA20nTsPtNtkRCzEjGmk0j3MnTpKQYuXY9SddXiAYeb6xzuZslvbVLY3qZa rdFSDi1tqLNFmP+33kExu0NhOwta0z00SP/UNKJvmGpTUa9UORmp2sOJpQGV9QweVgeg7RIgSrOF s390zubHCcK1fGTsIjLS0w34w4NCxBN8yj6VFijx0VkDnEkACOmrTKlX5w0SEtMKCaWKT7FS4c56 DaUF6ZTL+OBVLnz6WV79eZgYhHw2z+rCOqv3l9lcWWH9D79OnTQ9F55j6PrH8dJpq5ADCPCEsPTf TdazVZRWOA4MpD0GelwGelz60gnSKYlQ5jYPtEJq8OPU4lHfsom4h/0I2hY4uYk0fReu0Dt/lUlt uuia5SL1/V0Otzco7u5S2t2lWi7RCCQtFQMFoFosUc0X0ELT1T9A1/AYurufestHNU9ZIog2mIQ1 9EI+gBBEhL3/EiVsJbB1Atozr2UBjkXkw3LmaD9h1gbR8brd3YfyFKKHp4GCUlTKtd2TL8CTN84m AFgprpTTieQi0s9rD2nBwAH8QLG532Jz349m4J6uJON915j55DO8/HcdRvogJaGwe0C1UkX1jvD1 d/Pky51akoKQ4VeA0uTLDQ6KDZRShoHXEfT1uPQmXbrTRgugO+3gOlYHAIXWJi2m1BEYUxiNwRAU LGOw9FKkxmdIjc0wpE0GwK+Uqef3aRzkqOxmKe3lqJQqNAOoa2OA1UKJaqGIBLqGhvF6BwgSXQRH xVVEu6ApTNeJI0y9nddAICQ0GnWTRrUZgKiNJzRW3VmOe9RYw9jgMVAg3I146PKhw91/QFwh0BAE fvHEE3oCx9kEAOFo31cMJ9tsMUI6HGYW6Zm+hJNMme1OCPcKzM1hZnRNvaFZzfksb9eN6CcKrQKS MmBkqI/WnVV8t/vRDku0QUErTaHY4EDV21oAKNKepCftRiIhXUmXVNI810JbV9lEy3QQLqnNebRB wQqDKIV2PBLD47hD43RffIoRBKpRo7G/Q1AqUM0XqBzsUy2VqLc01f099N4eTjKB2zcA3b3moJUR MQmpvbW1QPmAJYAUEokg8IOoNboz927MLrAg0F4edLrpnb+O3Sa2eI+AQcfiCsc/3Onun+AtaG30 IMRJ+dgndJxRAAhWSsUiqr+n/ZLrsvFnvw9KkRqfpWfqIt1TF+idu4aTTKGDIH73dO6OGF8/oJH4 WpDdb6B1L7r1va8Z26AAaGi0AhoFPwIFbEpRCkh6DqmUpCvh4LkuqaTEcxySrsRNmJoDLRQ+baVg ZdOVKGWIQbRGa4k3OIbTP4o3q+kNNMpvEhzm0aUitUKeaqlIo3xAo7hHM9VN0N3XzjiAXV5Zd/4U e9EirLEwM6sMo+5HU4DHn9BZKxDbRsQfxuIBsdjCIy0hTogBhC8flipD81evPteqVBYymcz3oKP+ +IwzCQDKF75Ju7XvNq00AyPD7Gb3aG1nqO5sIm7+NQJN9/RFeueu0nfuOj3TFwwzjQrQQQv9UPbf H0zIWIhQB8C2+GpNvelTbygOrFZfOOObclqNA3ieJOkacRDPMdNtMiGtKIiLVoF15SEITMeiNBaD kx5EdfXRNTJFEiBoocpFVKVMs1rGrWqkNtyJIedilH046RykjEmCm2PRIuzM45gWSxwbTrquZilw gqdw/Js7Hp66hDjqKUhzzO9+8MFL3en+d2si0fy7v/DaG3/0h1/9zCmn+NiPMwkAlua+oxTYbzb5 p7/1z0nrJvffv8vtG7dZvLPExvIGhfVFiuuLyDf+BC+VpvfcVQYuPEPv/DN43f3ooIHyA+MlPAYj NACJQFsa3ZC/sNFoUa8FJnevFSoIwAqDGMGPABFolBUGQWscYUhUQlEQbfP+QgemhDgIUMpjWHgM y5Zx4+0xiGOzdOw4tY6sTyuFcpyI4y9sBXxQVN588qhPpjvW8HaBd2wbYvsMt4jMPwYK8bCih0Op VCLd04sAfvU//5WEajU+/Ud/+NWH/yiP6TiTAKCQEAiGEu3bx5Hwe//fFp7nMDt2nmtffJrP/5rD SBoWbi+yeHeZe+/eZeXeMju33yH3wTuAomfiHINXXqDv/NOkx89ZMLDG8yNOGCuNJQQRZvGqdZsQ JPwbgYX5R1phEBkJg2DlvnW7vBltg49GiszM3xpXYMVD2oQgpuf/5ONLJr3IqDsi/x0xgJgxWiR4 GCgcX96fsHzgOHAc/WwUjhQCL+GxtrrKf/iTP6G3p5tf/Yf/KROTE3z7W2+efHJPyDiTAIBsoOOs NXa4ErRSrGdrrO800LfMunlyeIqpZ8/zS5/9PJNDUDkos/jBAqv311m+u8D62/+RlT//A5J9Awxf +zhDV14gOXEOofwj0tk/vKE0vHB1jNnJQd554x3u3PwALRxDBuK6uKkU0kuCdIxAiAqgQxhExy3x kUZCgJSuAQQ7k8sHEKIO9vW1p+pQfrHD/6fzwzr650ODQiwp0N7HI3gKQghc1+WNN97gzTff5PLl S/zy3/slHCloNVungtuTMs4mACjHbzYax/oBwhHWAYQdfLlik51CixvLEoGkJ51grP95Ln7mJT7x SzDaD41SlY2lDdZX1tlc+jbFtTfoOvcMrYE5ApE41Qh+UEMKuL24S9KVfPFnPsF/9V/+JyzfXuL9 t99nZWGV++98m2arhZQyEgdxkylIJJGJFCKRAMdDJFJI6R4XBokH3KLrJiI1L6U1QqiYpNfxobVZ SmhtgoDOA2Zt+w0df9qbPRoohNvElYd0x2l0Lh+ElPhBwJ/+2Z+yurbG5/7OZ/nkJ16j0WzhBwpP OrR8/4muCjqTAKA8d7lSq+IIjRt1wJjpxzDkGeIH84r5X3RjCKjUA1bqAUtbynaGKXqSMNo7y8j1 C7zyMvQlNf29HvX8Lm+vSe5n/R86CAQK3nx/izduZhBac26yl0tPvcSXv/QF5qa6KOTKLN9b4f6t +6zdX2Pl/hKtRisiA5Ey1AAwzUUilUS4hjEIL2Elhh2DlpbT34nSgET1CY9y3ib9F0u5mVc7Iv0n B/XEsYdat9N3naAQbnR0SXAUFASe67C3l+Nrf/41At/nV//hP2B6Zop6o2mWUsJ0lDbqteVHOL3H dpxJAHAbppAnITUJV9KTcknLFgnp051y6O3ykNKPmHBS3Wl6+3vNDe5CtQkoaLSg0VKUDn3qrYBK 3ef91SaVmk/g6yi47eggqgT8YQ9pW461Uqxly6xuFfjTv3wf1QoYH+tlbmKAyec+xis/83kunOuj vF9nfWmN1XsrbKxtGpag1YwBAysMEolyhN8xOALpHsNVINwo6PgoI9yHUiIMUzwk0n8KKJwU0Dvm KYSfiH93Z84xdPnv3b/L2995i/Hxcb74hc+TSiZpNPxYb4NGCRsWeYLHmQQAAKGgGUh+fWyDv3nz Jm+9t8rKdg6NMAUtUlrjaa8LvVSS3r4eBkaGSXalGJ6cYGRylOHxES5NjDEwPEi6x1TCHdagUNYU DhsclHxKlSZ7hRalWtCuUguP5Yd0zoYdWCBdB6QgX6iwv1fknXeXCVQAQUB3T4q5ySEmJyd55Zln mZkeZWQowe7WPtmNLJmlDTbWMmyuZDjY3aNZb5DwfSucFrYDh4b8aGcWshNrHWcFbD/8nkGhYzzc UzB8g5o3/+YbbGQyPPfss7z26qsoFUSBUK3bZyXUEx4A4KwCgGuaVVqB4uMXRviJS5/HlZJKvcEH y1luLW9ya3GbW0tb5A9ruFLiSEkQKIqFMsV8yUSub942DTkh/bYD3X19jE2OM3V+mqlz00xMT/Dy /AwDI0NICbUa7BYCtvfq7BXq7FtdABVoW4jywwMEQdtDcDTgCJqNJguLW9xdWCPwA5SvcKRmaLCX sdE+pqZHeOWpa0zPjDE8lKZRbfD//OGf8zffuoUjLIcA2IalR5sctTYVjI6R4Ok04I58fPvA/3Y8 hTAjIJCupFo55MY7b1GtVvk7n/kMM7OztFqmhNvEg4QlLDGfNk1BP9pMz/c7ziQANMAEqtAEvgId UNNmhnx6fpJnLkzhfN7MCDsHJd5b2OTe+i7vLm6ysL6DH4DjmJvd6Nq3992sN1hfWmVtYcUCQ4BS mnRXF+OzE5y7epHZC3NMzk3x3MfmcD1o+bCXD8geVMlkK+QKdfYKVSI2mx/yRCMktt8fcMzyoVgs c7Bf4PYHK+D7+Eqh/BaDA7040lbxCYGURhgEPpxpaGHnYh1bu9N2038woGB+c8eVbG9tcu/uLbq7 e/jUJz9FOp2m2Wza8wq/K5adQNsW5Sd7nEkA0HVVqFarhC1mykaKTN2Lil4TQtPdleAnn7vI689f xHGMi3hvY4eFtV3eW9xiMZNjdfsA1zGdcOHyoU2vYy6xQrO1tklmJYNWyirrKEYmRzh/+SJT87PM XZrn9WfPkUgnabUgm6uwvVdha++Q3f0a9UaA9eIfaSilaQXKLmMsLwDKWoABvqgmQHUWvZw0jgmD uIJatRZ+2GQBHJP2DD4kcYZWCh3nS48ZqTgCCsAJlOF8aFAIlYGX737Abm6LmZk5nnv2OUDbtKhV CD62fDBfpoR4WIf0Yz/OJAAkEu6u7/uEarBmbacjrntTPdt+TWtDiqGb5oefGxvi/MQQX3z1OlII SrU6Cxs53l/cZDGT487qLuVqA8cRkYttugolJkjuRBe+Uqzw/ls3effNG9FNNzA8wIXrV5iYm+X8 9cs8/coMWjrkCzW2c4ds5w7ZOahQqbaitNvRoRS8+sIMX/rJi9y/dZ/vvvld3n/nDnv7RWQiidfV hZvsQiQSyEQXIpFEOi464RmmH9U2PmNEcSW+k4dwZCwGoI7FOk4b5hqLtqBofJ90Vugd/eCjgULn h8M4T6vZYGX5HrVahevXn2NqcprAFnBFoiVCdO6s3T4YdTw+yeNMAgCE6044zfjDnHCkGx+Bgcld +4GOPEJHCp46N8b1cxM4VkFyc7fAYmaX95ayrGzts7CZAw1OGFyMGHpAOjJi+gWolqu8960b3Hzj bZQKcF2H6flzTJyfY+7qRT52+TypvjmKxRrrO0V29w7JHVRoNP3I8ZAS3ryZ4VvfXefSzADXX/sk P/ePfhnRbLB6f4XbN26xvrjG8v0FmrW6DXraCL+XwEl1IbyUUQ9yEwjPBS+B9IxgasTPF0vdOVpE ir1Ci0eeHE0tQDsIGHfV2ytuIhv88KDQ/oTxhASlUpHtzRUc6fDCCy/T3dVN4LdAiCjK3xEtDJcA FhCE0GgFzcD/cTPQkzcaUQS4bdhxgz/N+DURL5z5f1Rjbz7Wls8a7OvilevzvPbMPEIImn7AWvaA D1ayLGX2uJ/JkT0o4zjSMunEOulOAIWt1Qyby+t852t/TRAEjE6OMXPlMlOXzvPixYukXpxlL18h myuzvVukUDT3pRCSxY0i91b3+eqf3Sbpai7Pj3Hu+Y/z8pe+wIVzvRR3D1m+v8ra4jKr91fZWFqn uH+AiNcDxNh+SCQQjmcEQlwX0ZW2gYOwHdgEAE+k6T5p2JxdvAS4sy04DNaFZvg9goItVMrvZSnl d+nvH2Bubh7HcfBDGvNjh3wkaGAfaC3RWuE3muuPdpKP5zijAAD8bRi/7vQejr2GT8tvvz87OsDc WD/y1WsgBIfVBguZPe6u7bCY2WNxc596s9X2EmJ19EKaNbzEwcWjlC9y61tv8d4bb6IDRe9AL1OX LjB77RovX70AyTQ7uSI7e4fs7x8aIRHXrP3vLmxz524G32oB9PemmJka4Nz8Nb7w2mvMzoziodle 3zSKQcsZ1pfX2FzZpFI5RDRbSGGMXQDu0Ah095o0oBAkPc8A4yP/EqbzMNyfue5HewhOCerxaKAg pECpgOL+No1mg5GxSUZHJ9FaEQQqivITi/LH3f1oWYBGSgelNG+//WYrkUr9n494mo/lOJMA0ABs 9k6cZvxtAIgZNUeMX+kocn3c+DsBBsAPgvZrQuM6kqfmxnj63JgNrmm294rc28ixtHnA8tY+G7sF DOOukeiKGH+FQDqOESb1oF5vsvjeByzcvAVKMTA2yuTFecbn57n2sXkO64rtbIGd/SKB71uQAS2h Vm9wd3GLO3c3CFSAagW4nmB8pI+5uXFmXniBV770WaamhiBQBhhWMuxkttlc3mCn3KAc0PYSlDIa gh8mBmBpv2Mr7Mj+2jgQq8i02xzb1wmgIIWk1axTL++D0ExOzZJKpVHKByzQ6DAEGmMROLL+10Ij pMvh4SFvf+etXYn7K7/92//iPz7aWT6e40wCQGZpaXOwe1BrrcUPy/g7gopoUEZAs/2aj0Aw2NvN a0+n+cmn55ESGs0WmVyJO6s7rOeK3F3boVhtRAYcBhgBm30wbno5X6T4nRvc+bbRAxiemWLiwgWe u3yVIDHJzl6Bg70SlWrNchYKCIN4UqB1wM5ugWz2gMAP0KpF4Ad4nsv4WD9TU8OMz81z/bVXuHnj Nm+8cRNHuG1OQEty+mhD2yxAO31o4iMiuuaRUdI2+yhF+CBQEIJWs0zQrJJIJuntG0Y6jp31Q3Vi QYwAoH0EOvYYjSs9dnd39Pvv3bjlyoGf/+3/7X96osuA4YwCwO/+7u/W/+ffelUbTr0frPFH28WN X58ACNoUJ+noWEJuAc3USD/TI31RGq5cNVmH1ewBS9kD1rYLNP0gWjaY7Qw4hIBwsL3D3uY27//V N0j3djN2bp6ZixdwZybJl+rs5Qo0G42O62SaogR8sJkiAAAgAElEQVTClaBdw0eoFbvZA7a3ciZo pkyZs2PZVKUt5NFH2TweMKIlVEf0LnwjvAqdoEDHOyeBglH1lUEVqZu4qW5SXb3m91UhYsSNnigu FL0W0yRwXZe7d+/o5aWFPyRo/urv//7/Xnmkk3vMx5kEAAAVmMjzoxu/3e4HZPztGMNJ8QeTr9e+ Sct5Eq6fG+fp+bGozn8nX2Fxa4/V7QIr2QN28ocmmBilIc2SAcehUW+yfucOa7dv4zgOQ7MzjM1f wBsbplBpUMqX8f0HkJvY/QrXMdRf0TraBgEFaKE6DfpBQwhDCaZPEOcIvzB67dFAwZEBSWmi+srt w3ETKNuwdFyDIDR0TccRaFv3gOSdt99q7eX2/4d6pfQ/fvWrX20LF7d38ESOMwcAX/nKV54uFov/ dTLVI7UOTjD+zqBfZPzqiFHzt2P87aBiZ0CyA5jC9bBuZxy0DtABGOZPzUBvFx+/OsOrV2fREhrN gPXdAivbB2zkSixvH1CpN2Negkn7aWB/I8Pe2joI6J8YZ3B6FjE8RKnaonb46BOdEAZsCML19MPt QghBuXwIyHYMICy6C7cJH3U4FSeDAkDKg3RCo7RLw3cBQ3kmxFGDD3+XTm9AoBFIpCNoNuvcePut UqD4x7/3b/7lH5x2Gg890ccUJM4MAHzlK1+5Vq1W/7tUKvXl119/3e0fGOLm+wsdVXXGsB5f4z/5 O+wxBwFoaOrQ8ASzo32cH+9Ha+OiFw5rLG/vs7lXZjl7wNb+IYEyhKJSmNbf0k6OYnYXAfRNTNA/ Ok6Q7qNSqxGcpgdgh3AMLbgSysh9PeItr6zgnrA1BG3PIWbkDwAF47obMBnodUi60GpJak1b6d9x INbQ7XUXJ6z/te0CzRfyvHvjnYWE6/3S//E7v3Wb44b+YYz6+/nsD2ycGQBYz5bf+OUv/+zQ+dkp wNx0z16/SKFQIl8oU67UDJGlxpIG/nCMP6x+O3F/8SXBke+IwCMCiaPgYVxwP1B2O5+kK3lqdtxk HbgIAjJ7JTb3DlnfKZDZL7JXCusHoJDNUshmcV1J99AoyZFRmsKj1WhyEpuKg2UAUoB89CCg1kb5 R8cUQuNBv86agOOgAIbsdHwwhSsFxcMGtYYVf4liCUfW/B1GH+7HCJW4jsfa6jL379z682RC/oPf +Z1/uX/KoX8/M/9Jn/2hg8KZAQCcxNBytsbS1j2G+7sZ6Esx1NfN/Plp5u0m5cMKB/tF9otlioVD IrIXaeXEiRvySUsGHhPjP/n7lV06ENjthWCkr5uR3m5evDCGkJJ6s0Vmt8RWvsT6TolsvkKp3qSZ zUJ2m2Q6TXJ4BJ3upYUk1MrW9jpJx0Gho6KmR7EQIUzQLjzeKABPaLbxfXWCglKCof4kk8NplFJk c4c0W0fc/WOsRKHbH+7v/2/v3aPkOM770F9Vdff09Mzse/aFBR8ACJIgKZGUSAriAyRIRaKkyInz sGw5jo/j4yiJ7XPto+v4+J5c88TXkp3Iuk58mXvlHMWSrFi2KOsVUXxIJCG+SZEACQIkQYB47Xux u7Pznn5V3T+6q6emd2Z2drEgQGK+c3qnu7qmp3emv9/3+7766quABchlzA4eeJmfWTrz5zPTp/6P ffv2uU3+jXNl+d/xuMLFAwAIqC4HQa5QQa5YwYnJZVAGZFIWBnosDPWncOml47g0fEelamNhcRlL ywXkV4qwHRf1B5CsUv4GZT0b5W92vRaf0anyR2Ai+wHRasQQAh5CcIDARLYHlwz3YveVwcSj5WIV s7kSTp8pYD5XxNTkJASARE8P0NMPqukACdYBpGGyDJfX7kB4WKG4HoCL6UzI8RtAIVzu7NKxHmT7 LBTKNUzPFSBCV0L+5tG14iygIWNIgBIG3/fx8s9esl3H/tff+Op//0abW34nLP874jJcNABAowSP QAgAyoKWcqWGSqWGqbklgACZlIm+TBL9mRQu2zqGy7aOAQiSbRYXczizlMdyLo9ypRaWCZM+w/lR /lVsRPmslsovWcwqZhPcTz1HQcBMaNg+OoDto/3RXIPp5SLmV0qYWixiocCxSHQIkUKPxkF4nSF1 IsFwLJFribRIIVaG6wAYBsM124ZhJXTMLOYxM5eLahEEvQXUiP6q/H4IyKFKjWgoFAt47ZWXJ7nw f/F/fu0rL6BzRW0lm235yRrnNyQXDQDUnGoFlFhyYVcJB9JNrBd8IKjWHFSrDuYXCgDlMA0DfT0W ejMWJiZGMDExAgDwPA9zC0tYzhUxv7CMYqkSDJFJstqB8jcoZivlb3Avzlb5Y64FlM8IHy3VzREA wAFPNGYxDmUsDGUsXHNJFhQE1bBQJhHhikVCze1vI9GQoSwLLNVUIfykDgqcc4wM9uKq7cOgIHjj 2AxW8hUwFk7dDRU9msQbWv7GWwnaKBXQmI6Z6SkcfeuNpz3b+8wDD3x9pv3drinn2vJvqptw0QCA 57hTFHQnR9wlDBBAmYcTbGEyDSEUrudjMVfEmVwBFASmqSGTspBJmdgyNoKJ8RHgGsD3fSwurWB+ IYf5hWWs5IuQHjIl9N2p/A33ErIbmcUYXg8isLVaWC9BCICuo1qO4ICcAai01n+QkLVwDly7YxyX TQyiWKph/+vH4bhBApSIrLtAg3+vRgqV84QEJcyPvHlYzM/N/PXhgy/924MHDzrYfMvf7v1na/nP mhVcNAAAOeYbHkSFXqC8qo1o/HUIEE7OIXBdH7mVInIrJQBnkEhoSFtJpFNJDA0NYGR4MHrf3MIy FhdzmJlfwpnFlUBZCA0/T3URWit/43BhC+UXoqnrsWHlj9piyq98llR+0XC94DP5OqbKcxFMBmJM wm9oo0N95QJIaAy33rQTA30pnJ5ewMHDJ0FZOHVXKMof9+/lbrhDECQr+dzHwZdftGtO6fe/+Y3/ 8d/UXk2kE8vf8EkdvL+dZV/rfKtz6waCiwYACMJFbFUdl5a/idKrr1EfNbIdFgulhMD3OArFMgph 0ozGKDKpJJKWieGhPowOD+DaXdsBALmVAqZmzmBufglnlnJwXT9aH4/gQlN+FaA6V34hOteY4B1h DEDII/n5AeUfy/bhtpuvgsYoXn71KE6cmoeus+jzVkX5I0BQz4evjKFSKePwwVeWHLfy6W/9zdef kr9u01vrrE3KRvz+jVr+9bS3lIsGAJr/xnWFbmAEUWCgsU/8tRl4SItUKldRLlWxSJbBGEXSNGEl TfRk0rhuVw+uCwFhJV/G7PwZTE0v4MxiDpWqHXivNCwRukHlFw3vbaf8MdBZ1da58q8n8Bf8D+H3 JoJEIBGLAdieh5uu24YPXHM5KtUaHt13AIViCUxjYZ9Q0eU9oq7o6pUCF4VCoxrOzM/jrTcPvWpX yr/wgx88cHKNW2wGCmsBhbq/mZb/bNubykUDAJ7v1ITwQRFMu23QcQB16qjEASCDg/V2lQmobdKG N/yaNLTrAqjWarDtGnIreVAKmIkEdF1HOpXE1Tsvw9U7LwMAlMtVnJ6ax9yZHKam51EuV8PAlrLq 7Tus/HEWUgeDjSs/FwJD/b2AAHwe1BCkodsgAGiM4Oc/cgu2jg/i5Ok5PPHMK/B9Ecw2lPcZ/WAy 5Fe3/EJxCwiloBrD28eOiNnZyW+/feTgvztw4EARzRVU3sJ6JO4ttgIEte96Lf9G2tf8Py4aAKjZ tdPCx/uUx6YBARpYflPcJ6v6yRbScCL2NJA4uwAggFqthmq1hpV8HoQQJIwEDNOAlUzi6isvw9VX Xhbcd83B5MwCpqfPYHruDJaWC0F9QUKV0cdWVH0t2r8O5Q+vIfd5rP+6RQQTiuKPqOcHlP8ff3Q3 LNPA8/tfx/Mvvw5D18PcfUXR5Q3FlD+i+6QOyYde3e+XKoU/+vY3v/anHdzdZvr8cQUVTfo0u9Zm AMGabOCiAQAgqDhLqaSeRFHmJr9XjP7HDD8aTqh9gIiCNjgPDe+t/x4kLIRRs21UazXkVgqAEDAT CSTMBJKmiSu2TeCKbRMAgpGGE6fmML+whBOnZrG4nEeQR0/DjMVOlT/u83ei/GJzlL/hW6jrheP6 uOX6nfjI7TfAsV088MN9mJ5ZhK5rQb/YpJ16Oq/SJqQDEZT/cuwqDh88kHe58+vf+duvPxj/JVre VmeyEZ+/Vf9zaflbsoGLCAAEZLGaWLA/kDXof71fI/2vA0OsY9PrkdW/mlTIcCckrajaNqo1G8tY gRBAQk8gmUwgnbKwY9sW7Ni2Bbd+6H3gnGN2bgknT8/i1NQ8ZmYXIcfDaXhza9H+xrhCLPgY8/tX VVE+SxEI3AHP5/jMP74LV+3YitNT8/i77z2GWs0GY6xO55sW7Whh+RlFPr+MNw8fOlIr1X7xoYf+ 9s0Ob+lsrP9aSh7vIxXzbIFgwyBw0QCADhIl+gAInqdV9J/UD1RRLHojMCgdJf1X+7ZyJ4DIp441 1f8I+YgHF7AdB7ZjYzlXABcCiYQOK2kibVnYMp7FlvEsbg2vM7+wjJOn5nD81DROTc2H8+BlSXLS Rvnxjio/ECh/dqAP//SjtyA72IdnXjyIHzz8DDRdC2odIFaXv4H2QykDHn5bJFiwZfrU25g6deLR E/m5X3v18cdzaP1LNFx5HdKp9V+LhsunZD1AsGltFw0AcI06M1OTGBkbh2EkQARHvWylQsmVvUZa 3+SiCl4013Wyql/8N1nNBGKidAg9F1ACOI4L23GRy+XhC4GEYcBKmsikUxjO9mNkeAC33LQLALC4 tILjJ2ZxYnIGp07PoWa7wfRfEgTVWtP+c6n8Ap7HYekU//TeWwBC8dffehivvHYUZkKvf1dCXb1v NQsgai2/EHHfevOQWFmc+y/fe+rH/ycWF9tUNmmQzfT7m/Xr5HynQLCetra/1kUDAD1a6ndfevGF /SDe3b09fbuzw8Pm8Og4RkdGoetGUDACgPz+W439y/0ovBRRAuXDpN6riq+4Dq3o/6rzkHS89f8l wjJcjuvBdovIrRThCwFdY+hJp2AmTfT19+DmwT7c/MGrAQCFYgnHjs/g2PFJTE6dwUq+FCbVBHUB Wit/+3vpVDgX8LnAJ/fciImRPpw5k8P9X/l7LC4XYOha5KrUFR4IvvEWgBAWOPFcB0fefLVcqVZ/ 54ff+ebfhG+kq25A+frW2R7cSGd9O7H8a7kRaynzmgrepE/Dcaeo956S++67P11j5Q8Tz9ujUbYn lUrdNDwyZoyNj2N4dAQa0xGVC0NYqkrSeyIVW9kP/e2I/ivt9WNZfVa1tsH9NETYUafiDQG86Lx6 3AgQDf572M7BIXwBXdORTifRm7KQTlswdD36PsrlKo4en8bk9DzePHoay8uFoMw/CETIFNZr+QmA XK2G5Wqtod3zOIb6M/i//rdfwWB/Bq8dPoa//Pr3AYhwnUWifL8k9r2TVeeDtf00lEtFHHnj4GSp Wvrlx3/0nf2d32nHIlrst+u3nvPN2uNsoF2/DfW5KAEgLvfdf39aLJQ/zAnfwwz20Z507w0joyN0 fMsEstnhINdcAAhnyMk14ilioBA9nPV9FRgiWy8UJYaquGGPhtfG85ElDt7Y5H2NABBv5wC474Mx it50Gpl0Cn09KSQUQHBcD28dO41Tk/M4enwSk1PzoJRF6xU0jpo0l2YAULUd3HrjLvzv/+rnAQA/ eOhJfP/hp2AlE00Uvonyo/E8pQRM07A4P4O3jx55YXll6pd+9vTTZ9a4rfAXaCmdwtxagLCRzziX INAFgE7lS1/60kDR8W/nrr+HMba3r29g19joiB4AQhbB11ZnCAQiXKNvtdKTOADElTKi2jHr30C5 YwE6NFH0OCtocj7aR32fcw4ugnUH0ikLfT0W+nt6kEwY0fchhMCxEzN4+/gUjrx9GlPTC4CQdQWV tQoUUQFACMBxHPybz3wCH/nwDQCAz3/pqzhxehYJQ4++IxlEpQ1Wvq7wQfyjrvyUEJw6cVQsLsx/ df+Lj//+/Px8bdWNbI6I2Ot6+6zX1WjWHp9dcbag0AWATuXzn/98llP9w77v32vq5t7egd5tW8bH 2ejoOIaGhoIEFSGC1UbAobKCCBjQqICraTvQ3Lp3Qv+bAMQarGCV+xHu83CpM0YIMpkUMqkkBnvS SKeshu/k1NQ8jr09hWPHp/D2yWlwHi5eIlkCAgCYL5WRSZq477c/g0vHh3Fqcg5f+n+/Cdt2wiXH VLDshAUExTohOI4dOeQUS5Xfe+QH3/grnN2zvA7npqH/Wkq3ESVt994NWfl2x10A2ID82Z/dv9Xx q7dxn+/VE/qeoYGhHcMjw+SySy9Bf/8gwHmQ2kqCKjdESFuPBvrfqKCN9L+BFWB9ir4aIFSgacIK Gt6n3h+CBT5E4PJkUhb6Min096bRl041fCdnzuTw+tHTOHZsEicmZ2FXXSxVy5i4dBz/8bc/AwB4 4umX8dffegiJRCLSVhlIbab8KqsK2igYo7CrFRw7cnChUi7++uOPfv+pzftlG2Qtih8/1065NwoE 5xoEugCwGfKnX/nKOBZzd/s++Zhu4PaxLVsmxkdGyPiWLRgYGAD3fQjhw/cBDj9aenvD9L/BajdX epXmt6L/q+IQcfYhP1MFl3BUQHCOTNrCQG8ag70Z9PekG76T5eUVnJpZxA3X7gAA/MVfPoBXDh1F IqEjLmrMpJXlp5SCMYbc0gJOnThy4MzCzK/uf+Gp+MKc632ez4Xlb3XuXIFAFwAuJLnvvvtoT0/P Np/Su4Tj3CmEuH3r1q0TExMTZHx8Aul0Cj7nweKZ3A+q4cYUvTn9P7vg33pYQQO4oN39AX5QHQRc AGkrgYHeDLL9vRjqSwMgWFzK40//618jXyhD01qPyDUGUckq5deYhqnTx8SZhenvHXjh+d9cXJw8 V/5+8M82vrbrt16FP1sLDrSPB6zrel0AOMcihCBf+MIXrrAs6x/YjnN30jRvGxkdHRwZGSVbtm5B 0rTCcXE/XGqcN1p/tGMCzRVdPY8W7Z3Qf6isIPb50bUhGj6HAxB+EEfoyVj42v/4ezBdqy993kJW swCp/AwAx8ljr3ul0soXfvzgt/8rGhWgSQbGmtKJkjU7vx7L3669ncJ2cn/rYQFdALiQ5L777qOW Zb2PEHKn5+OOZDKxJ5sdHpjYOoGJiQnougGfc/jhSsKcx9lAc/rfjuar7a2Cf52wi6bnY/cB5X2U UTzwzQeh6Z3lm8VZAGMMrmPjxLFDK+VC+Td++vh3HwcawwebLKrStgOF9Vr+TtvWc8zbnOsYELoA cJ7lvvvuo4lM5v3E9+8FF3dY6fSHs9nhzOj4OMbHRqEbZuAq+EEwTnC+IfrfjhXErb96nbXYxypA UdrZegEAyjCgxlDK53Dy+OtHlxcXfvWVnz15tM3b2h3HZT3Wfy1AaKfIcaU7Gyve7HijbkAXAC5k +a3f+q3E1q3bPugL5w4uxF1WMv2hkdGRzJaJCWRHRqFpGgQPFuPkvh88BfGgHUJFX8taI0bn2wT/ oLTH6b96/mwAAAhqBDDGsDA7KWZmTz96/PAbvzk19Xo+PB1lVmNzn91OLX+zSqfNFL7ZtdY6bnWN Vue6AHAxyH333WeyVOpDxPXvFiB3ZXoyN42MjBijo1uQHR4G0zT4vhe4C74AD4cYVln3Jta60Zq3 su7rcQ8a29fNAMJ8gMmTb/Fifvn/eeLRv/98cNUWc62Ut65xHN5d2+Nm59opaTMFbKZo6wGB9QBA u/d2fM0uALzL5HOf+1xqcGRkt+N4dwJibzrTe+PY2FhifMsWDAxlgyKlPg/jB364HFij9QeaKG5D 8K85ADSwgjXoPyEBAHzrmw9C09YGAEopXMfB8bcPV6rF3O8+s+9H30NzRSZYGxDWI51Y/1aWvxOr HgeKtY7XAwhdALjY5f77708v50t7PZ/fSQn29vQN7BodHdGHR0YxNDQIQRh87gEC8H2BYAmuQM4l /RdCgDGGb//dGgAQDvNVSgUcP/baydzCmV8/eOCpw2oPrE371/Mcn43lb6Xw67XyG1HkZue6ANAV AIoCfO4P/zCb0ZO7Pe7tZYTt6R8cuHZkbJxlR0bQ3z8YxA+EAPc5AA65IPJm0X8VUNYCAJndt7gw LWamjj+/MHnss0eOHFpQ/qdmln6z4gBrWf5myi3b4xH4tZS6Wf92x83248edMocuAFwE0mosnNz3 7/9kS4Gs3HNmbuEr267YiZSVwfDYKEbHxjA4lIXgAn4YTORCAMoow0bpv2xvBwAywWfy5FGxdGb+ qy8/9/AfVqtVuRJvXPHXUvpOXIJ2FH8ty9+MtseVbD1KvlbftfabXSN+z51cowsA7xFpCQAAMDQ0 ZF111VWFP/iDP0A6nUKl5mBqahqTMzNI9fRjbHQcI+Nj6O3tC7IUfQ4fwbThptZf7rcI/gkR1O5l jOGBJgBAKYXnezh17JBdyBf/8Nl93/+aPNXk/pspN42dX6+I2BY/18y6rgUCzY7jn9fJubMFgHWB wUVTEeg9LB1lwlFK4fs+NE3H1VdehmuuvgrVagXz8ws49MabePPwq+AAsiNjyGZHMTI2BiudDkcX /CjDT7KCZhJ/wkkT9k4pRbVaxsljh+ZzhdxvHHjmxy/KW4zdfzMwUBV/1b/Y6n/HauWVwCGU80I5 FxZcX9UWz0AU6zi+IKULABeJ1Kcji6AGAOfwPB+WZWHr+BiI7+H05CQOPP80uAB6+/rQO5hFdnQc g4NZjI6Nw0xaEJA5CM0X/xP1KCFA6n3kJJ+V3AImjx957dTs1L889fpLs+HptZSfoTUz6IQBSHCI W3753iDfuK7g8rrNgCHe54JX8nbSBYD3rhAA6Lv+VzNi2PxFxzmGVYtkC1EHhFBxCQRKhTwSOkPS 0FCgAuXlBbx24HlQPYnhkVGMjo5hKDsCw7IgPA5f+IBfdw/kh0dLsYfFEWamTojF2envHHz9ud/L z89Xwq6qsseVmjQ534oFtGIAcautKq1aMFR+jh/rv9kK3u567ah8p9dYV78uALyHJfuJ3/tlKtw/ Msd2bq2lPwjhOeu+hs4oBvp7kU4mkM/nMXf8DXzvyQMYtAxcc+mgGB0dJ0Mj4+gbysIwTPi+By5B BQABBRccp99+0yvml//oqSe+/2XUlbeVzy/PqUpNsdriN2uLi0rnW1n+Tq38Zg03bqTfOfmcLgC8 F2XHvfrY1bv+Su8f+bS19X2EC4F8aXFDj5rKEgBApwQncx7259N4bMEmff5L4pJEZfYD20ZOZnoH 3jcwPJwezA5jKDuMhJmE49Zw+q0jK9VS/t899+SPfoLWlrqVpZfn1gKD+HHch5fn4lSfYjUTaLW4 +UaV9Z1kEeuSLgC8B2X86l1/lZzY9enEyDbiuw4ADkI27xmkhECnDKAEBWMLOejUxp97ZH952Jt8 37137t6RtPru0A1z79BI9oMnjr12NHdm7tcP7n/mbTQqcTMFjj4itq8ex+MB7ViAtN481p8D8JQ+ 7ZS+U+mUvrd6zzvZL5IuALzHZPTn/2BPIjP46cTYTsIdGwBABAVEuyD5OoUQgFAQQoPhvmQGqS1X XpGbJZ/96pe//AUALwP4v8PecZofXaXNfjsm0C44yJT3SQZAw3YfjZZftsU/fz3SicK1G5Zb77U2 q190bhOfiq5cCDKoeZ+auHwHgecF5csBgApQCKxRk6NjoYSBUBYCAYOAgNYzDGIk7l3jrZ2k8rbr 04oJUAAGAoOmh5sR9pEMginvj3/GhsfROzzXSs77CEIXAN790vAQXZJhiWxPEjIEF5QsDlYO9j23 oTJPvf5eoz40q/sf9YWAy/3w0rS+2AmhIJoxuJ577UDauQgSEHQACQTKz5pszXIJNpp8c67eE5d3 7HpdAHhviXhxzoXBGAyNIjL5hMNPDuCRV0/B1CkSRlCYk4eVhzzPg+/70cYYC8t7B4U4JSAwCLx8 dApFbSBooxQgDIRSQHAIHrnRndDcje5LJZYKrjXZl1afKMftriuU/U4y7NoBSDs57xY/Ll0AeI8J I8Drix7u2dEHFqxQAgICnXA87FyDP37gebzy0s/A3RqSCQO6xiIlJ4REiTyUUjBKYWgaNEowNzWJ 7z/7Op6rXQJGKUDkBhAhwChTb0NVEHWYLa5sne6r+QGqkmuoKzlT2gnqQb5mFlTek0DjCEA7YNgI GLyTzGAj/bpBwFbygQ98QB8cHPwl3/f3cs5N3/ePM8b2VSqV51544YXC+b6/mNTHtgmEwwVemOf4 JzeM47njSzg2XwQjBBpl2C92Yv+TFWz5ySO4ssfH5X0EfqUAu1zAmfl5rCzOwXVd2NUS5hZzKB46 hsmCwBnaB2GMg4bMgoQAkNAAksjAtl2gdSxNHXtX9znqCqvu+2j02VUfXlV8NYIfHwJkCEAgDkCq wsetPW9xTp5f7/5GgKHdZ55tAHGVdAGgifzCL/zC9mKx+J3h4eH39fb2gnOOYrGIQqHw+4Zh1O64 444XCSGPCSH2LS0tPX/48OH1Z9icQ6EAKg7w8GngQ5dfio9c4+CZo/M4NLkM3/NBiIHTxuU4XRPg MxzC98D8GngSwDYOwX0IwsCJDvgu+IgDU3AI1wHnHhghsHSChJWGY/TALpeAmgMifA91BVcpu1Qu quyr6bYqCKhWXLXqquIDjTRf9e191JVF3TjqIwECATisFxiaKXUrloBYv2bvaddvo9IpoADoAsAq +bVf+7X327b9+Ic+9KEBxhg8z4Pruujp6cHIyAg8zzNt275jZWXljlKpBMMwVoaGhp4G8JQQ4qEn n3zyEM6PrycAEI0BglBQFhjPl84Ar1v9uHZnFh+9mcMpl/D23AoOnz6DycU8Ko4PEAGfGBBMBNOB GQM4B3wHAhwJTUc6wZA00iC+C5sL1Ix+0GQPeGEZnHtB4VLHPonViq/uqyAg26N7R6Pia6grua6c U/MCVCBRXQVfua4EBJX2q8qvJgYBqxlDqwyMc90AACAASURBVJl78XPx32IjMYL4NVodb5or0AUA RT7zmc/0GIbxnZtvvnmAUoparRYFyFzXhed58DwPyWQS6XRaBtD6arXaJ1dWVj5ZLBb/ZO/evQue 5z3GOX/MMIyfPv7448fxzgGCgAh8cpmXTyHAOXBokeBnpyogfg1b0knctOty7DUAAz48z8divoyq bcN2HTi2CwqgVMyhkl9GsVjE/FIJc2UP3MzA6ulHUicQ8RmwRKiWEmgNAlLxfdQVPR7EU0FAVXx1 rF9V+LgSqwovgUBuKitop+ydKv9mxwvisum+v5QuACiydevWP77qqqu2DQ8Po1qtwrZtOI4D13Wj TYKAupmmid7eXnieR1zXHalUKr+Uz+d/qVQqiT179hwVQjxOCHmyWq0++uKLLy6t554u/eU/HoPr fg5E+zmaSPb6Ts31a6WfMUKf8Fz9yRnz8Kt44IE6fSXklG8XAQwrVwmWNNco4Dgcb82W8drbZdQq eVQLOVTzOTilHOxiHsQpAk4FcMqAUwShOojVD5LsA031wzTr12whcRcg3i5FjeTHlV/dV6P+qqVX gUQe+8q+VFAfq+m/el5lAq2Oz0b5OwUGtDnXqWvRSlqe6wJAKF/84hd3UUo/e8MNN6BarQaBMNuG 53kREDQDhDg78P1gim1fXx983yeu6+4sl8s78/n8Z4vFon/77bcfpJTuc133YcMwnt23b1+p1T3t +Fd/8c/g1r6cuvLmfmNwAtz3AOHDrRY/5a7Mf8pemcOl+WuW3X+07SlG6JOuhodFtWw3C8Y1aB4B GCXQGIHOKHyNQWgauK4BQle2BEBYONzX/prKUauHTfXdmw3dNRu/l33V96uZglKpVVdCKO1yU0Eg HvVvV95rs5Q/LutR1rNlDG2lCwChOI7zb2+77TbNMIxoOEwqt+/7cBwnAgMJBHJrxw48z0MqlcLg 4CA8z2Oe592Qz+dvKBQKv1MqlZw77rjjZwAeIoQ86TjOS88991wVAK78jS//G800/6Jv1ydZMBnH BSUawAlIqg+G1YPk6HYAYsCvVn6ulpv6OWd55ouO7bsQsXIcBAAhUNcXFISDcIKgKJR0GZRnWXYk 8gKIVE9O7+VhIxFy+XMKrH745G2sNW6vo6746vi98skNChhvUy1/K8UXWK3UaNLWCSuIv7az/M1e 0eR4Peda9VvPuS4AAMCXv/xlq1ar/eLOnTtRrVahaVqUJGMYRmTVXdcF57wBDDzPQ61WawoIcXYg GUIqlcLQ0BB83zds2761UCjcWiqVUCwW83tuv/0pu3fi9XK653ezH7iXCacW1OzjGgTzITgH5T4g OHwuILgHlsqAWVciOXYlAfcM33MCg00oCAmSejjnoVILQHCAC3ARrFoswuP6eUUfSGj9Ze4/oaCM gVIN4AKC++DCB+d+EDhs9PnluL3cpKKrfr8EApXqx2cGqteTE3mE0iapvxz7jyu+CgaqtAKDTmMB nRwDG1f+jVD/jq0/0AUAAEChULh7586dA57nQdd16LoeVc0JKud44JxD13UIIZBIJCCEaMoOJAjY tg3XdSNAiLOCRCIBz/NgWRYymYwMKPba1conl2r+J/Mf+iegwoVPKKjvBYrGAwAIXn0QziG4BiE4 KOehMrIgI0/UnzkhNm8eAIBoivAqCaYcqtRdbiy23y5rT2UAQKPfLoUBcNVbQmPgT1V8L3ZOfU98 fy13oNn71rL0Z6v861HwThlDJF0AAFCtVu/s6+tDpRIUqaGUQtO06NUwjEjhJTNQAUFadskOpMLH ASEOBvHYged5SJsMc5f/CrhbhSAAA4VIGCCcBFV7fA8+9xqAgIfKL7gPcAHfOU9pCUIQ1CflSAXX 0Vrx1VegeSaRCgTxbCM10KeCgB/b1lL+du5Auz7rOUaL441a/rWu05F0AQBAtVq9JZVKoVwuR2mx jDFomlZfn17TIlAAECmsCgi+78M0zahddQMcx4lcBRlglMxBAgH3HEzRnThjjEO3yyCEwScE1KMg VANhFFTTQYgGIgh83w2WFOdeBABCCJBqs0TF1RN+ghz/s6AGETkP0o1BBANgotH6x5U/Tvfjll9e WbXearKPp/Tjyqtq+ePKrwb+4tKpRW/WdjZWv9XxRt67YTegCwAAbNveMT8/D8YYTNOEYRhIJBJg jLUEBMYYEokEAKxyFSQgSDBQ2UEzQJCbcGz80PkAeK0AlxAQSkEoA6UMhGqgNEjBDdo1MMYgNApN GBAiWEGYgIMyHYLzcLltAKCBuxDmBwTl/4N7FiJsF7FhcxkHkBdRZw4SAlACEsYNBBdhTUFBAaTQ PLoft/wktgF1vx6oT/F1Uffx4+W9VPfARSNoeGi0/vFU4Wb76nE7xZf7G7X6rY43w/J3YwDrFc/z RmZnZyOllgCQTCZhGAYMw4Cu69A0rSkgyNlzZjhILmfVdcoObNsG9xy8sKgjV/BgsBIoY4ry0/BV 3dcgaBicC9mBpukgjIBSFipkoKxBEDBkySK+BfECEW+HCB+lKPQPOQGI0PCaCIFD+BCeDcF9B0AS jX5/s1RedeaQGvCTyuwp5zXUqb0aCJR91SE/1e9X5wF0qhSdKP1abRtR/LN5f6fnmkoXAAAsLS1h bm4OmqYhmUxC07QICCQYJBIJmKYJ0zSjuIA6XVaCgXQTZNyAENIw5VYGGOX54JyLBBF45k0HcMvw fKZYf9oSCHwqQUIDoQSEUTBfD615XZoG7M5GRHhNtcwY90FdbxmNDEDN3otb+1YWWQKGo7RrAGw0 Wn8Xq0HAR6Plb0f9W/xnbffXstrvpPKfleWX0gUAACsrK0vLy8uD0qrrug7DMCJ3IL6ZphkBgmyT 7kIrQGjnLlAkcOxMDUcWVpAyQoXXWDjsJhW/FRDUQYJQBqF5TZN2zqkQAkIAs3/oE8kb/+HlhLHX qrMnXrKnXj0e9oiP5ze8G3X/XbX0OgKll9KK+jfz+yUQxJWi2RfTCe1eD9Vv1taqT6cKfTasoK10 AQBAPp8/try8PCiVP/6q63rEAuJgINvjgCALaqhA0Mpd0KnAT55fAJwKXB4quOoCaBoYIQBlSgxg NRhQSuF7GoQfM3yrHvvmlYDWK7IoCLerSCUToFfcbILzG4VTuVEf3Pov+RUfKHul3Ou8VjjslZdf q546fBq+o47Hq0ogLb8ba1PXC4wn+cQpv7T+Kv1vxTRaSTtA2IginivW0O4zO5YuAACoVquPzMzM 3NLX1weVBaivKhioyr8WGKjBxFaAYCUSePS1OQjHg+uFiq0CAGMR3SeMRorPKAt98sbYAOd+ACBR lJ+CcC8avxeQOQ5+fUxfqM+l8kyRuv9fDwIyMKqBcw/V+dMwdQKqmUFSITUAw0SyZxCEainhuTd5 5ZWbwF3Rc/UdZWdx+jWvWjjkzJ86bC8em4bvS2UF6olDcoxfAkAz698q4i9BoBWtbod6Z2PNO31f q/ta67M2VfGldAEAgG3b/21ubu53CCEZXdcjEJBBv2asIA4IKkOQjMEwDCSTSei6DsuyonNquS1D 1/H6UgGn5paRSQbMIVB+xbIzhe4zJQbAYi5AyA4AgLIg3haMWgh4ngzq1ZVdVX4RD/7JuIEM/smA IqFgGoNTKaBw6jDSPT314GC41QOGBDASYOYYAEoI1dLJLdfu9soru/kVRSHKxRW/VnjTLi0erEwe /pk7f2wBjSm+UF7VsX5J71WfP+7/t5KNKE4nbkKrtnaBw/Vcp9Nz65IuAAA4dOjQ/JVXXvkvTp48 +Tc9PT1WKpVq8OslGKhMIL4vwaATd0HGEBKJBIShY99rs4BbgUdcENbIABrpPmt0DVSgCNNzCSMg oND0ZPT/bWoMkFGU5k9Dc0ro6RtoUH5EpcLqFYMawIAQUJaGnsyAU0oo1fqFW9vtlpd3p7fd8K/9 SmHBKy4fsnNzLxQPPvxTNNJl1W1QM/uaxQHOlWwECM5W8c+J5ZfC1u5yccjS0tKRZDL5Ldd1C/l8 vr9YLA7VajWqDtvJpB05ji+TetRXOb5fq9VQrVajfdu2o7ZKpYJKpYJqtQrhufjWCydwfD4PItN9 fT9YmjvcF74PcD8ovOH74L4H4XtBAlDYN3hfeJ5zMF2HnkiC6QaYbkBwH77nwvec6NVzbHhODb5T g+fUIDwb4C7ghxv3AM0ANBPQkqCGCTc3i4yZQKp3ACBaMC+AaSBMA1U3rX6u/qopfUOXQjPArF6M XboTg5dcmaK9Y5dzs+c2Y2Diyur80WfguzL6r/r4Phr9fvXcWgrSaeBjo/GCThS/Wds7qvhSugxA kampqWMA/gOA/zA+Pr7V87y7VlZW7qWU3p1MJodM0ySWZcE0zYgZxBlCM3agjizEYwfVlIlXj82C Oy5cj9UZQIz6R3SfBX4+oaRxmDDmJjiaBqt3qP7PyXhAOLsvSuiJS9PAIAG4A+TnMTC6BcxMBrW7 KAGYFn0uoQQQFIIGSUzMSIIwDURLBOsIYLV2cMGxfcjC5OwSlspVUEGRGNtJqGHe3G+XfyX34rfv x+qZfM1Sfpvl+8f+iYaPXo+s9Z6NKn2ztndE8aV0AaCFzMzMTAL4erhh27ZtHywWix9njO3VNG23 aZpGKpWK8gbU2IEKCnEXIQ4I1bSJpdwKCCh4WGbbb+P7yxjBqkCh6i4wBu466B25BKDB6j1BJqAP wYPkHTl/AFE2YKg7nEMmBwXPHgWED4P46BkaAQTguw4o44HygyNh9sMcGIfZOwgt1QfNSILqiYB1 +B4ED3IgEA57Cu4F2YO+jyt6XLz65tsoVmtBJSMhIHwBrX8LaN/Yp9jI5d/w508sojHw12r4T60N EJdzoUidjAy0at9owHFTpQsAHcrx48dfAvASgP/Y39/f29/ff3exWLwHwEdN07w8mUwSy7KQTCZb soNmrCBf0FEtF5DQNYBqAGUAYRA0AIIoys9URVcAga0GC0IpfEJQWl5ANtUTDNXFjVM4319E+02C 1AQA05Awk0j19ofKzEGgwRwYgTV0KdKj26An08GCI4QGrIAyyBoEhDLAZ6A0nLrMfHAeJENttRwc PvwmCpVqdA8yGMm5C31gImGOXntHef7EA2ht+ePWfz1R/43IRhV+Pe8/54ovpQsAG5BcLpfP5XLf AfAdAGTLli1XVKvVjywvL39c07TbTNPMJJNJkk6nI2YgX1VQMAwdFY+CuRVAsAAACAsUSIIBZRAh IHBCARqz/quCgvX93ORbGJq4YmP/JKGA2Ydkpg/MSMCzazAy/chceg3So1dAM00AAr7ngpfzTXIS QkbCNFBNBxUAJ3oQ0+ACFvNRmT+GxZUiaLQegToqAVCrD1rSuh7At9A4/NcKBJqJqkybAQadugOd tL/jFj8uXQA4exHT09NvAXgLwP0A9C1bttxWKBQ+vri4uNcwjOtN06Squ1AfXtRQ9giI4wB+o9JH IEBixwoQgLBGdtDgJmjw7CqWpt7C2JU3dv7fEAAQILoJw0qDwIfRm0XP5dfC7NsCwIfr1eCXnbqS UxYsFkLDzEXGQEljshLTguxgynRAI7iETuM7b56CRmngdVACzgnkCkNUCAjNAKfaBFYX9Yin+7YL /q03EWg9shEwOK8WPy7dUYDNF14sFk8WCoUfr6ys/CVj7Muc88OFQsEpFAqD5XI5bdt2OGGIo+J4 mK0EiTrgYeRdbr4HcD/W5odtPiCCTchRAC/wt6ORAu6jnFtE9rKrQSmB5zrwfQ++54B7LjzXhmdX weUogO8C1WA0QjcSMIfG0Xvlh2GN7YCgOrhXA/e8+ueF5cDrW6C8QmkLRivC4zAeMGZ6OHLoAHKl GgiCuRJbFp7GrvJL2IoFpEQFHEAVJryVKXNnyj1BKJ0rl8sVBIlBLlaPCgCbT/c3ImfDAN5xuRC+ sItJ9K1bt14P4COEkI8Zhn4LMXuMKfNqgNt1ZRfK86wyAtKEJcTbZBFPScUJxdDlu3DFrfeikl+G UyvDqZThVIuolfKoFXKwy3nU8isQS2+D+C6M/izSl14Poy8bPiAy/ZiuDkTG5iI0zlnQopmDMjYB quGDfSV885FnoTMGnwtcNfVdbB8bCD4nnFfACCAoRcX2US0XYdu2UywWXyeEPO44zk9PnDjx7MzM TAl1ENiMZ3kzA4jrjQ2cF+kCwHmUSy65pH9W3/o8G7tmJ/EVAIi/ymdGUn/pGjC2OmYQHetRYg7R DGy76R4MXHIFqsUVONUSnGoJteIKauUCqkuzsE8dCCYtTexCMnsZhAgvRVYHGRllAKtPVJJg0DAk SWIxgbD/RH8ShdOv47XjMxC6ifFj38Yt27MA1aIMSVlzgRISAkidqBaLRRSLReTz+YLrus8C+BEh 5MlHHnnkYJuv+p14zjcaGziv0gWA8yz61fd8XRve+S9AaUj5mwCA+tqKHawCgMZjZvVg+63/EGa6 B7VSHk6tBKdcQmHqCEpv/BQ0nYWx9VowpgfFPtCYX9A88CinIcdrFcTBINgHYbhzRwr/84f7oBEN Zu4o9vZMY2B4DEKISPkBNACBWpVJTq/2fR/VahW5XA6FQkGUy+Vp13UfF0I8TAh5+tFHH53E+Xu+ L0hlbyZdADjPol/7sd+m/Zf+F2r1h9l3YXVd7gbKvhYgROyAtQcEQqEPTmD77nsBQuDUalh87Qnk 33gGZGg7aM9oQDBIPLGIhoqthZZeDktqMVdAB2Mkcj1W1TNgDKah4/JEBfteOQowHdfN/QB33nxj kFSkrFCszlQkhETJU+o0apmZqdZkLJVKEhD8crl8WAjxBIBHC4XC0x0s6LqZGYLvGukCwPmW9/38 hGVZx/TxaxOeW4XwvTD116sH+xoYQBtAkIVAwhl7q+IFLAHaO4odt30Kcy98H/njB0CGdgC6CSDM 128xpFhX5uaZh63YguwPQvH+rYM49MbrWC7UYBVP4aMDc7jk8h3BpCjDaJghGfwbBKlUCqlUalVR VbXIilqfUT1XLBaxvLyMUqlkl8vlZyilPyWEPPLoo4++hHM7Z+BdI10AuAAkdfOn/8689P3/nBhJ +I4D7nvB+HqY898UCNYEBIUdqExAMyC0BEh5CUgNrWYOpPF1VRISC2sTxii/OhS5qi289h1XjeB/ 7XsJRNexdfIR/LO7bgTTjYaJVGqiVCaTQTabbSimqs69UBdkkUAglV9lB5xz2LaNlZUVrKysiGq1 WqhWq08AeIwx9vAjjzxy7Hz+/udTugBwAUjyzs9MJEjy1dSO3QPc59EwHfc9cM+D77vh8FswjNYW EBoYgQoUkh3QMIhIG12F+KhCdH41GKzOPIyNBKzqQ2EkErhqJI0XXgt0ba/9BG758O0N9RdUANB1 HVdcESQxqTUUWwFBnB2oDCHODjjnKJfLWFlZQT6fF5VK5ZTneT9hjD2WSqV+/N3vfndd6ze+m6UL ABeIZD7y2VtNlngwedkNvURPBrP2XBvcdRVGEAAB9z2FHawDECLGoBTLoTKpSAWC2LBiC7BomIfQ kKq8erhwy/AgPLuK0ws5pGuz+PjgGWzfeVVUM0G6ABIEBgcHMT4+HilvvYpx45JtcgGW9bKDeFm2 QqEgAcGrVquHCCGPCCEeO3PmzLMHDx4sn+/n41xJFwAuIMn8g9+8KsG9/8ysgY/pfVmNpgdBDSt0 C2z4jhsyAxe+7wX7nhcAg68kCbUNHqqJRTKZSGUHrbIQ9ShGsDrgKMf845Y/cBcECG66djsOvHEC ngCGll7Fp67tw9DwKHRdh2maSCaTERAkEgns2LEDlmWtAoD4Fl+haSPsoFkcIQwmIp/PVxzHeX54 ePixfD7/9Yceemjq/D4lmytdALiwhAJIWzt2bzeGL9tLEtZdzLR2G5mRfpbJEpbuDx5814bveQFD kIwgchmaxA7WCwiAEkhsNcy4Ol7QlB0wBkEobrnuarz0xtsgVMOWuSfwj267DgkzuarisiyWcv31 10PXdRBCImWX9L0dGGw2O+CcI5lMYnZ2Fm+88cacEOKmffv2vWdAoDsX4MISAaBWOfbckcqx544D +BqS/VZm1203asmBezQzuYelB65hPUPESA+BpHrhu7UgrdexmwKB3NAsmCiaAEDDOQ544TJjDeyg FSDU3YloNiNl4CBwfRdCBAuSmn4ZjutChHUJNE2LlNF13aC4aaiQavk0WVlZXXeh2SZLr8sFXddi B7LQS5wd+L6PwcFBHDhwAJxzjI2NjZ4+ffq3APz78/WAbLZ0AeDCEoGgHr6LoCQ2QzVXKb78vx4D 8FMAhr7l+rHMpVfuFYZ1F0v03Kr3DPTrmSz0nqEg395z4LsefM9WXAQvch14NMzYJGbQChBku+8C vtN6mJEygNQzECVAmMkkCqUq4PsQIGC+jXy+gEQi0WCB1RwA2w4qgkdZgcomA4dy1eN27ECu9JxM JhvYgVyQpR1DyGazePbZZ4NaBgAsy4IQ4hPoAkBXzrGoxS8coL6yjjv9Sml5+pWTAL6OZDJpbb/9 /cbAyN1UN+/U0kO79PSAYWSyQGoQwvXg+ja444L7Tj1e4AVBRSHZwbqGGRVA8MKy/fHYQZR3wAAw WBkL5VIlWI6cUzjVIsrlckOZNUm/HcdBMplEsVhsqKvQDAjOhh2sFTuglGJ2dhblchmU0qivpmm7 3vnH4dxJFwDeHSILXsiFMYNltqrVSuXQo09WgGcB/Il++TWjyZGr9rBE7116quc2Zg0MsswQSWSG 4AsO7tbgex54FDtoHGqUswnXBARfGU2QgBCxAxqLD1Aw9KFQrgDcB+ECVcePAECl3dIq27aN6elp pFKpqMiqHB5UU4JbgcHZsgPbtpFOp/Hggw+CMRaBQ/i+91TcrAsA7z5R2YEsoc0AMPfE4bJ74vBp AN8EkLCu/8T7jZ6Be5ie2MOsgev03mEtkR4EUtloiNH3bXC3zgzkkKMIp/LWg4lrBBGF3wgcIlje TxBAowS28IMlzYUHBxrsWm1VoVXVAs/OziKTyayqoyjBIL5W42ayg76+Puzfvx8AooCgGhdAY9ny d7V0AeDdLWqRTBdoWItPq7zy4NMV4HkA/8kYvjZrXrrrTs1K3akle29l6f4sS/cTPT0ETgi4E+Yd eMFQY+AiBHP4OfeiRKQ18wxigEB4UIG46tWiGoQFz0C1WgbTjMhCxxmAnPQjqynJNRXiS7SpC7du Fjvo6+vD888/DwAN7oHsj2DZsnZlyN410gWA95aoroKMHTAAzFk4VHIWDk0B+FsApnXtx9+nZ/rv IKa5x7D6btB6srqRGQJJD8N37DDvwA7zDeruQgQK0cjCGoDge+AIVhESQgCejYo1gnz+NKxUT0MQ UB2K830/UmhV8dWMwXPBDoQQyOVyyOfzke+vDhk6jlMDkAi/Zz/2+q4Dgi4AvHdFdRUABQwAVCqH fvQ0Anbw5xjY0de//do9SPbexczMbXp6YJRmssRM94fswIXn1bMS1SFGCQhCzmRUXYaQBTgcYBqD 5/JwtaBezJYtXMKq4E2UX25yleZ4UdU4GGwmO0gmk5H1V+9FiQ9MArDQuDaBrxy3W5XogpMuAFw8 ItmBXGwzchWwfKyUWz72LQRFTo3UNffs0nqydxPT2qNb/e/Xe7MJPTUAag0FTMCx4fkuuOtEQUQ1 PZlLZhDOanQ4q68k7nsAAVbSO+DXZjDiF5D2A3fDV5RNKlxvb2/bFZriABBnB+qKTCo7UNdnVIHA siwcOHCgwS1Rg4O2bb+NYAl0WZZMLVEGnPvViTZVugBwcYrKDiQgBGAAsPLhn7wAYD+APzf6JwaT 2268jVh9e7Vk761aqm+U9WSJmeoHCIPr1sIgogwmSnehvoKRJzgSjMCVKkIZiOAomeMoiDHo3EaP U0G2Mod0KgnOzYaEnGQyGVlwtahqM0DolB1omhYt8KIu614ul3Hy5MlomFANVJbLZdi2fRpAJvze auF3JmMvQD1A+K5gAl0A6Eq7QCJzclNl5+WpbwH4LgAjdc3du7S+4b1UM3Zr1uAHtZ5B08gMgViD wRCj74QxBNVV8OH7YUYhwiSicLVvCgGfJbBULqOwvPJWv7ZoJJPJCcuytHQ6HSmgTAtupvxrsYNW sYM4SzAMAysrK3AcB4SQBvrveZ5c1u0ogAEAFQTBwCrqjEpdXEFd5vyClS4AdCUu8ZyDICMxZAjl w4+9iIAdGEgPZjI7br6NpgbupMnMbUZ6YIJlhqiZHgSnWjCyEMYO/FoZcJzg0pQG44OCgICB14rw CjMn7dkT/18JyOu6zgcHBy83TfMGwzA+aFnWaDqdJplMBolEImIDqu/eDgw6CSRKZjA/Px8FA+MB wEqlUsvn81MATOU7AeogKuMBEkQveFegCwBdaSctcw4AMJSWisVXHvougB8CMBJX3LrTyG65U9Os PbrVewPryVpGZgikLwvu9cKpVeA74YxCzkGoBm7n4eamFvzpI38OYA5AyXXd6tzc3CEAfwfAzmaz 4/39/bctLS3t1TTtFsuyMpZlQS68Iv33jbADdd80TSwtLa0qOyaHJ2u12mHf900EAFlBXfGlKyWp zQVv+aW8p7KauvKOSiMYBMZEA2AA0JEe6c3suGk3TfXeSc3MrUbP4CUk2UcKc6eDtQN8D7w4C7Ey +6Y7/dqX4NZmEShVBUAJAbWuIWAgTrj52Ww2MTIystv3/bsJIR9LJBLXWZZFU6kULMs6K3aQTqdx +vRp7N+/P8oMlCBQqVQwOzv7t7lc7unw/ooAyuGWD19LsXu+4IGgCwBd2QyRPnCQorwaEAxz203b E8OX77VXlm9Np/TtKyv5eV6rPMWnX/kRAmWvog4AdrjFFwBZpVDbt28fTiaTH/V9/15N0/aYpjku 2YFhGGBhIdNO2IFlWVhYWMChQ4ciAPC8IHK5sLBQnZqa+jPO+RKAAoAV1IEgjzogSAC44Ok/0AWA rpwbUXMOVDDQIRlCkEzDUJ/wVEPdBG5IpgAAAV9JREFU0scVv1PRrrvuuvd7nncPpfRjuq7vTiaT iXQ6DcuyIqVvxw6EENEwoOsGU5ht28bs7OwLS0tLDyJQ/Fy4lcLjAuqsRQLXu0K6ANCVcy1qzoEK BmoAzcPqpJqzps/btm3rNQzjDsbYxwB8LJlMXm5ZFkmn01GCUbORhYMHD0ZpwYQQLC4u2lNTU1/0 PG8ewDLqyr8UvqrU32t1PxeidAGgK++kqIFE+ezJIchzPna+c+fObYlE4h4hxMcopXsty+pVg4mM MZimicOHDwMIFifJ5/NiaWnpb5aWln6MgOoXUKf8RdRjFc65vv9zIV0A6MpFKTt27Eik0+kPuq57 L6X0XsMwrksmk3omk0GlUkGpVEK1WhX5fP6H09PT/wn1GIV8VS3+u8LfbyZdAOhKVwDs2LEjm0gk 7qGUfhTAJ2u1WqpWq90/OTn5p6hP+JGpv5vmpnSlK13pynmT/x9S0uMXDcYeBwAAAABJRU5ErkJg gg== "
+ height="55.977482"
+ width="55.977482" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text7028"
+ y="740.11218"
+ x="361.28571"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ style="font-size:10px"
+ y="740.11218"
+ x="361.28571"
+ id="tspan7030"
+ sodipodi:role="line">sshd</tspan></text>
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#bf3030;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.875;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path4072"
+ sodipodi:cx="34.25"
+ sodipodi:cy="369.75"
+ sodipodi:rx="12.25"
+ sodipodi:ry="12.25"
+ d="M 46.5,369.75 A 12.25,12.25 0 0 1 34.25,382 12.25,12.25 0 0 1 22,369.75 12.25,12.25 0 0 1 34.25,357.5 12.25,12.25 0 0 1 46.5,369.75 Z"
+ transform="matrix(0.57142858,0,0,0.57142858,378.5657,536.13259)" />
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#669b00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.875;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path4077"
+ sodipodi:cx="34.25"
+ sodipodi:cy="369.75"
+ sodipodi:rx="12.25"
+ sodipodi:ry="12.25"
+ d="M 46.5,369.75 A 12.25,12.25 0 0 1 34.25,382 12.25,12.25 0 0 1 22,369.75 12.25,12.25 0 0 1 34.25,357.5 12.25,12.25 0 0 1 46.5,369.75 Z"
+ transform="matrix(0.57142857,0,0,0.57142857,301.29143,599.36829)" />
+ <path
+ sodipodi:nodetypes="csssc"
+ inkscape:connector-curvature="0"
+ id="path5613"
+ d="m 392.0625,750.86218 c 1.20645,2.10718 3.46039,3.53125 6.0625,3.53125 3.86599,0 7,-3.13401 7,-7 0,-3.86599 -3.13401,-7 -7,-7 -2.57733,0 -4.81629,1.39468 -6.03125,3.46875"
+ style="color:#000000;fill:#bf3030;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-opacity:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ sodipodi:nodetypes="csssc"
+ inkscape:connector-curvature="0"
+ id="path129"
+ d="m 317.63036,804.51843 c -2.14987,1.19474 -3.59375,3.49105 -3.59375,6.125 0,3.86599 3.13401,7 7,7 3.86599,0 7,-3.13401 7,-7 0,-2.5416 -1.37968,-4.74241 -3.40625,-5.96875"
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.5;stroke-opacity:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ style="color:#000000;fill:#000000;fill-opacity:0.48309183;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect6086"
+ width="59.384914"
+ height="24.950771"
+ x="246.43259"
+ y="668.92932"
+ rx="6"
+ ry="5.9999995" />
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path6088"
+ sodipodi:cx="123.6875"
+ sodipodi:cy="222.6875"
+ sodipodi:rx="2.4375"
+ sodipodi:ry="2.4375"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ transform="matrix(0.61538462,0,0,0.61538462,223.5318,583.39317)" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path6090"
+ d="m 299.75001,741.37984 0,-18.53532"
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:nodetypes="cc" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text6110"
+ y="684.95953"
+ x="260.50818"
+ style="font-size:10px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ y="684.95953"
+ x="260.50818"
+ id="tspan6112"
+ sodipodi:role="line">kernel</tspan></text>
+ <path
+ transform="matrix(0.61538462,0,0,0.61538462,222.5318,540.39317)"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ sodipodi:ry="2.4375"
+ sodipodi:rx="2.4375"
+ sodipodi:cy="222.6875"
+ sodipodi:cx="123.6875"
+ id="path6127"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <path
+ sodipodi:nodetypes="cc"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 298.625,674.89879 0,-18.125"
+ id="path6129"
+ inkscape:connector-curvature="0" />
+ <rect
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect6538-4"
+ width="68.30262"
+ height="21.371223"
+ x="325.56561"
+ y="671.76501"
+ rx="6.0000005"
+ ry="5.9999995" />
+ <g
+ transform="translate(16.484866,-0.539878)"
+ id="g6586-0">
+ <path
+ id="path6588-8"
+ style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:1, 1;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none"
+ d="m 297.71429,746.5404 c 0,-20.64274 0,-46.28548 0,-66.92822 m 0,-4.4375 0,-14.31605"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <path
+ transform="matrix(0.61538462,0,0,0.61538462,221.61388,520.72527)"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ sodipodi:ry="2.4375"
+ sodipodi:rx="2.4375"
+ sodipodi:cy="222.6875"
+ sodipodi:cx="123.6875"
+ id="path6590-7"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ </g>
+ <path
+ transform="matrix(0.61538462,0,0,0.61538462,260.5326,549.2929)"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ sodipodi:ry="2.4375"
+ sodipodi:rx="2.4375"
+ sodipodi:cy="222.6875"
+ sodipodi:cx="123.6875"
+ id="path6584-6"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path6570-6"
+ d="m 318.5072,740.38833 -7e-5,-36.43431 m -1e-5,-0.80071 -7e-5,-16.84207 15.99985,-0.0771"
+ style="fill:none;stroke:#000000;stroke-width:0.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:3.99999999, 0.99999999;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none" />
+ <path
+ sodipodi:nodetypes="cc"
+ inkscape:connector-curvature="0"
+ id="path6125"
+ d="m 332.375,677.48718 -31.25,0"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text6540-5"
+ y="686.44165"
+ x="362.90088"
+ style="font-size:40px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ style="font-size:10px;text-align:center;text-anchor:middle"
+ y="686.44165"
+ x="362.90088"
+ sodipodi:role="line"
+ id="tspan6544-1">docker</tspan></text>
+ <rect
+ ry="5.9999995"
+ rx="6"
+ y="651.85443"
+ x="32.80722"
+ height="210.4968"
+ width="160.61426"
+ id="rect6524"
+ style="color:#000000;fill:#333333;fill-opacity:0.45490196;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <rect
+ ry="5.999999"
+ rx="6"
+ y="784.70099"
+ x="38.497528"
+ height="29.529198"
+ width="95.838203"
+ id="rect6526"
+ style="color:#000000;fill:#d4eeff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <text
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="81.411682"
+ y="802.59943"
+ id="text6528"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan6530"
+ x="81.411682"
+ y="802.59943"
+ style="font-size:10px;text-align:center;text-anchor:middle">cockpit-bridge</tspan></text>
+ <rect
+ ry="5.9999995"
+ rx="6.0000005"
+ y="720.30487"
+ x="118.08072"
+ height="21.371223"
+ width="68.30262"
+ id="rect6538"
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <text
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="155.41599"
+ y="734.98151"
+ id="text6540"
+ sodipodi:linespacing="125%"><tspan
+ id="tspan6544"
+ sodipodi:role="line"
+ x="155.41599"
+ y="734.98151"
+ style="font-size:10px;text-align:center;text-anchor:middle">docker</tspan></text>
+ <g
+ transform="translate(-210,47.999986)"
+ id="g6546">
+ <g
+ id="g6548">
+ <rect
+ ry="6"
+ rx="6"
+ y="610.26562"
+ x="248.49753"
+ height="51.299866"
+ width="148.4924"
+ id="rect6550"
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:'Bitstream Vera Sans';text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#000000;fill-opacity:1;stroke:none"
+ x="389.3454"
+ y="625.83929"
+ id="text6552"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan6554"
+ x="392.82196"
+ y="625.83929"
+ style="font-weight:bold;font-size:10px;text-align:end;text-anchor:end">System APIs </tspan><tspan
+ id="tspan6556"
+ sodipodi:role="line"
+ x="389.3454"
+ y="637.34253"
+ style="font-size:9px;text-align:end;text-anchor:end"> systemd, journal, realmd</tspan><tspan
+ id="tspan6558"
+ sodipodi:role="line"
+ x="389.3454"
+ y="648.59253"
+ style="font-size:9px;text-align:end;text-anchor:end">NetworkManager, storaged</tspan></text>
+ </g>
+ </g>
+ <rect
+ style="opacity:0.5;color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.49999997;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect6560"
+ width="150.32707"
+ height="33.279198"
+ x="38.497528"
+ y="821.76562"
+ rx="6"
+ ry="5.9999995" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text6562"
+ y="838.88324"
+ x="112.53619"
+ style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:'Bitstream Vera Sans';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;opacity:0.5;fill:#000000;fill-opacity:1;stroke:none"
+ xml:space="preserve"><tspan
+ style="font-size:10px;text-align:center;text-anchor:middle"
+ y="838.88324"
+ x="112.53619"
+ id="tspan6564"
+ sodipodi:role="line">cockpit-ws</tspan></text>
+ <rect
+ ry="6"
+ rx="5.999999"
+ y="774.26562"
+ x="142.98352"
+ height="39.789604"
+ width="43.434963"
+ id="rect6566"
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.49999991;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <image
+ width="55.977482"
+ height="55.977482"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABHNCSVQICAgIfAhkiAAAIABJREFU eJzsvWmsbNl13/fb+5yaq27Vnec39etms1vNSWQoimJEiqREKo6DOCZsK4CCIEhgiVYCITEMB/nQ +SDJEhQqgSwESkAHtoMYSAIkQeAokW14ICiJQzd7Yvfrfq/fdO+78626NQ/n7L3zYe8zVN26byBp sV/ft9D97qkzD3v991r/tfba8ESeyBM5tyJ+3DfwRP785Otf/3rl+Lj56VDx5Uze/3KlPLM+6PW3 uv3+TWH0keeLHaW4hWZLCHalzO73+8d7L774ov5x3/sT+dcjTwDgfSxf+cpXvI9//FMfDbX5vDH6 5yszlU+trK4WNjcvsryygkCAEGil6HTaDHoDeoMeJycNBv0+w+HAdDu9fhAM94Iw2A1Ds+1JdVcL dnUod4VWt2Qpu7s2O3v8y7/8y90f9/M+kUeXJwDwPpPf+Z3fWRmF8ouhCr+Qz+e+uLq6trK6ti42 NjbJ5LKgDVprjDEYY9xREs+DqDkIIRDCNQ3h4UtBEAaEoaZ50qDX69Hrdej1+3RabQaj/lAF6qTX 697U2hxqo3cF5raW3p2cYjfIqINRp3MTGD2xJt5b8gQAHnN58cUXs7lS9WdGo9Evep74/PzcwodX VtfE2to68/PzKK3RWkOs8JOfXKT+nbI5tUIIgRQCpLRrhYiPFkLgSUmv16Xf79Mb9GmdnNDr9RgO B3Q7XTq99qEK1f5oFOwguCM8doRiV/rebTUMdrPZ8t7f/Ju/cvAjf0lP5Ex5AgCPn4jf/u3fezZQ 4S9oHX6xUCp9ZnV1vby5uSmWl1cQUqC1QSuDERpcJ59S4+knnVwvzt53bP+xXaRdJQQyNiAEAgkI pBRYo0PTPGnS7XXp9XoMBgParSaDQV+HYRAOer13e/3hPYQ6NlrcFoI70vN3UBxK6d/c2rrW+P3f //3hQ76vJ3IfeQIAj4H85m/+5qLC+4zR+sv5YukXZmu1jbX1DbGyukq5VEa7Xt5orNJPVfcHferx 7eJsk+DUMdN3PX2cBQ2BQIPnxXskLodECBBSMgpGBMMRvV6XdqtJp9thNBzS63XodrrdIAgbozDY CYfD21J6u9roXel5t5Wn7ngjcXR8vLD7u7/7hJd4kDwBgPegfOUrX/Fe+OgnflqH4ReE4Eu12bkP r6yt59bX1phbWABj0EZjlMEIA0bEPf20jtyc7t85vRfYk0Q8ABgzqdxnnWWasrt/z7zwaWgywoGE cX897e5cWpBAOCPDuSICBv0+nU6HbrfHYNCl1Wox6A8YDHo6VGa/1WzcM6E50sLclR630HJXSXGU 0erdarWw/dWvfrVL8vbOnTwBgPeI/NbXvnY16Ic/p3X4hUKh8PPLK6vVlZVVNjY3kVLGxJ3WEYcm 0voai1X4ZBl+MACYvkU8YPez3At7D2N7jJ1j+gkFAuOexi4zhikGLBAAQjj3w3N3aSTCs0AxHI7o 97u02k267S6j0ZBer0ur3UIp1RqNRrv9bv9d4ZsDIdg1Wtz1PXnLBNkjKdXu3/pbv7Yz9cHeB/IE AH5M8gd/8AflerP72VAFv+h5/ucXFxevLi+vyI0LFymXitaPH2Pr059qUlFOrzGxysGDP/M02Ji2 VUzX/bHDnekQbZh66bMtCafmFrTceccw4j7vYWxzfJAAtHMzJNJZEEIIpJSpmze0Wm36/R79fo92 u8Wg32cw6BsdhoNmu72twvAoDMMtPHHLl5mdYrb0z3/t1/7D1894mMdC/B/3DZwX+cpXvuJ95BOf +HA4DL5sDJ8fKv1Tl69cKWxsbjI/v4gQBmU0RgmCYIQ2kSkcyaSVak6tHff+09sexsKNlOn0vunz mAkIMG4xXmvM+FnMNHWfAJvIjcHYP/ZEyTIReTgtijHdQoj2F4IUpGgUJNGL2KARCCkoFAoUi6UY JLAgIQQUpCefVko9rY36VL/X59t/9l2+/Wff/DbwyVOP9xjJEwD41yi/8Ru/sRoa8QUdqi9lC/kv Li0uL65tbLC6skY2l3PEnUaFYWzqAqnGfh/VNZOLIrX8qC6tSf19gLWQIgaMMXHXfD94St/f1OtG yuqWY5fFTHueBDyEBKENGpBSgLYgKGOll6lchxR4iZRDIhyMOfQQaJSIlkFIaylkZYZWq823/uQ7 fPfb32M4HHL93Te37v+y3vvyBAB+hPKHf/iHxb29xme0VJ/3Pf/LM7XZ51bWV+Xqyhqzc3MopazC G8NoNEx840ip4kZoUkqTNP7EQHYypq/m0fV+yjWmSvo6JvXbkDL3IQGhs3vqcZGAtnkF2qAAKcFo c2pva8hLEE7LAaOS7Sq9HB0jkpwjIZLnFCKyFgzCJO9UCAcS0oYxfd9Da8Orr77BS99+mTu3tvAz Pr7v4XkevidSV3085QkA/DDy4ovyNzKZ5wJjvqA0v9Bo9z596enLldW1NVZWVkAk5N1wNEqUJfqT 2KrurzODo/ObcTUwqW1xc57sHB9Cn8fl1BlT50kp8sQ5jYnVF4iWUzuJCMJkcrAQoDUCSagVQii0 AoGyx46/ntTjp6ITqX9Eyo4/vTy+jxApIE12GFuWUuJ5Et/32d/f51t/9l1ee+X7KBUifY9cPufO J5FSY1zew+MsTwDgEeXFr31tzvT7XyDgyxlPfG5hde3CysqqWF1fp1AooLXCaFBKY0xIzNaP8dnO IDWTSjyt3ztl65+h32L6OacCQkKORX+sSW1sqrCQeEY75TZIJOn8XSEExmhCZcm1IFQIIdBKO6PA 3oT9Y+zfCW4gUjyrn+PLif6eodyOL5gGAPHmyJJKrecUGAikNE7xMwRBwMsvvcJ3v/M9DvYPyGQz eNLDz/iWJ4iOlwajBd7U7/B4yRMAeIC8+OKLefL+xwnEl7TkC7Vs/hNLm5fk+sYGCwsLKG17MWMU wWgEuP5qrDMd8/CTTn+Kco5Z27HSTJrUzlQ1kfbKmPcXwvawRoLUAu2yAW2YTI9dRRuDCe1xKhyC EARBiBASFVrw0toptbbRiFjBgbEeFhIFc8siYuQ4vWzSPXLsizMVDEx6eZqiCzCpl3n2PTngkCCF h5SSTCbDuzdv8vJ3X+GtN99BSoHvud4+vq+0JQHSSIx8fwxpeAIAU+S3fu/3rgQD9SWjwy8UirnP Lq+szS4vL7O0sorvSbQBoxTD4UQ2asoEH2fOsaw2Se823QIgpeeW5ELbWLc9deSvOiWRzvd1vbHW CqUMoVIIBGEQYgAVKhACpeyYAK0M2kxQc/LsXvjsZRMn75jk4HFlS/XGU8FAOHg00fqUnTSxPHbe 9D3E693rT187ZS0IpFV6z6PT7fLSS6/y+qtv0Gy28X2PbDZzCrwmlV8gMM4CGMPkx1SeAADwd/7O H1YH+vhzWpkvZDL+l0qF0pULz14Q6+sbVKtVZyZrtAGtAaEx2kMrhTGgjUZra/OOu+1W6aWx7TBK WokbuwRpBEophIAgUC6PXxOGilBrdKAwwhCOLN8UhO5vECAMaCMwaCTSKoK09gCQNOR4mTPMbQHa JMpjUjkEIqXQxpxeJuVHTyhk0nunlT4NBslyfGS0LBIUnWZJTHuWyeeK7sNzvr0QkrffvsHLL73K 3Tt3kdL6+7lsxh4jU9/mDOW3J5Z23/cBApxbAPjtr33t3xj1g8+P1OjLMj/4+NW1ZwrrmxvMzs0h jEQZgzGKdrtje1+JSyQReMJDSkEmkyWTkUgvgycFRiuElAShTWHVWjMahfS6fVonHRqNFkBsZo+G I2TsQ9vz2yblElZSjQ+4T28MOuXP67ihJgAQM98pJRvvYWO7YkzBhDndw55Stgj1HmAtjFsIU5Yn 7nkSDCaXmULsxdaCEPi+j+d5nDQavPTSq7z5xjUGwwG+55PJZFx6cepe76P8pNYjDUK/H9T/HAHA iy++6MtS6a+KMPgL5fLMF+bmZ+eWV1bFxoULZHwfbQxaKZTS9IYDNNBs9pDC0OoO0AZ6vQGjUNEf jhiNAsLQ0O310Rr6QcDcTIW5aoXaTIW5aplatcJMpcj6xgLPPlsEoN3p0e106XZ69Lo9ev2BXe73 GAxGaKUQnkS6AJMQnktMgfHGONlLp0NcE8tpxRK40NfpntddYToYuGub1LXHrIX7WAhpMEhCcJPW QrQc3wUP40ZEfyNewpMenifBwOuvfZ/vfe819vYOyGZ8pGd9/ug+JrmCU0rv7nvyWtJIjNCIdGjh MZVzAwDtQPy19dLc31/dvCzyhQLHJ22u73R5+e3vMgwVQaDoDYYIIQiVsmZ03EhO90pWAyKT3/ZK 9XqTw3oDrQ3GMeJK20E7Gd+jVq1QqRSZm6lQq1ZYmq+ysLLIlVKZcqWIJyXD4YBOu0+/36PV7tgq Pf0+g+GIYX9A6Bh3IWQqJJXKGxBnm+lppR97ltTy+D4T1kIKROJ3cgogOG0tpIDhYa2F027EpOWQ WBRSCKQv8T2f3Z1dXn/9Ld588xrGGHzPI5/Ljn23MUVPPXta6c9SfssBwGgwxMCdR2uF7z05NwBw eNgszi0/Jdo9aHW6gCQrC9Rm8gxHQ8IwpFJUBGFIqBSjMGQ0HGImzcSUmNS/OEJMIhBSuN4R2xv5 todqtNs0mm1u6x2UAbTC8nIaKSSlYp652RnmZipUq2XmqhVmazMsb6ywUKvhScEwCOh0evR7ffr9 Pv3ekH6/z3AwpNvrp2Lg1o2IQCLurFLWwvhyGtimLDNhUXAfa2Eaez/5Dh9leQxE7D9S2PfsSY9g OOTVl9/krTffpl5vkPF9fM87pbjTevn0s0X3P+5OTLEAHPhKYUZnt7jHQ84NACDdxyVFugvwhcAv FKKfpDQIIQRBEBAoRRgEDIOAUFmQGIUhoyCwRCCuZxPGhqNMBA0uHh71zxEZKKV1X6XAMwZjPMAw GI7Y2jlg696BJRaVwaBRCrRWlEtFZqsVZkpFZqtllhbmmK2VWKytMFurUCkWGQyHDPoD+v0Bg36f Xt8CxGgYMBwNURHfgIwz3tzDcsqNmLQKouT8B1kLE5bDaeU+w1KZYi2k8wYMID0PT1pC7+6du7z+ xlvcevc2Qgp8z7dMvhhXWIFw6cHTSb00kE1T/jEOIH5PgveD+jz+T/BDShKicwAwkX0npSTveZDN UoExgMANzx2FIcMwYDgcWcAIQkYqZDgYEipl4+dR2qkxzqoYjxhEy9IpnyckUhiMkfieBYkwCNg/ qrO7fwgGlHGFQJS1MKQnWJitsTBbpVIuUKtWrLsxO8t8tUqhaGsCDkcB/V6PwWDIYDBkNBoyHAaM hgGhCm1evX34KLbwUNZCmluYRuaNJe88hLXg9kRIi95Zz6PV6fL6a2/y1rV36PV61uf3fRDjORTR txWp5ehmTpF6k8qfAoHILYh/g40CCIDHPxfg3ACAUNmjfq93ar0B8rkMF1fnabR6nLR7DIYjjDZE HIBtJG55AiAQglw2Sy6bRZTKgFXiKPSktWYYBATBiMEgYDAaMhyN6PQGdPsDlFJujL/r/YQ4Nfw3 4hkM9vpSSIww+MbDSIk1IOz2RrPFUf0EjcG4KkHKpSP7vsdMuUi5VGK+VmFhtkqtOsNcrcTy3ALV ahmJYNgfMBgNGQUBgbMcBg7cjFIYkST4SinHSEe7cNpyGLciziDzYoDAWkfCxu2lFFy/fpPXv/8W 97Z3kb6HLz1834+vEx0f5xsaEUcVGOv9k+Ss6CbG/k2BQBqgYuAQwrlTaah5fOXcAIARuq71+NgN Y2BpfoafuLLBu1v7PH91k82NRTIZ6PaGNDs9Gs0une7Isvc962d3usNxcMA4Ys64SjW2USqt8aWg lM/jlQsIYVlq4Rq350mUUgyDgH5/QG8wpN3p0u4N6fV6tDs9esOhbczGYKQDCSNdZZwoVm7iBzLG jowTRtj9JXhGxiDS7vRotXps7+yhNKAVmih12TBTLrEwO0OlXKRaKTNXqzBbrTJXm2OuVsHzbLmu 0TAgGI0YDoeEoWI0GhEqRRgGznKY7P3HlyNFGwvrgTXlpR1sU2+c8Nrrb3Lt2juEocL3PRe+S3/X 8V5epBbSGZTCJWJFA4AYA4dxwy6R6L4niMGYgH385dwAwDRRWvO5n36eX/6V3+fouM2VK5v8/Bc/ DRgqpRyVco5i0adcKbC0uEg+K/Fzkowvabd7dAdDOt0Bvd6QXm9Evz9kOBwRKo3RBk8IjBFIz5ro Qki0MUgNWtrogCcExVyeSqmI53xMz/fxpIgtiG5vQLvTpdMd0Op2aXf6NDsdTpodWm1n1cQMe9rv jqwJIO1yCJBIhGcw0sdzbLkxhuFwxN29A1DYWoOA0cryEEZRyOWYq81QLZeYm60wW51hrlphplxk caHCTKnEaDS0IDEKCUNFEIwIA0WoQ5dijCvGkdyPNeUlQaC4du0Gr7/xJkdHdfyMj+cSduxTxIc4 i4iUNTHZL4tTxzjjICXWejBj50nAKU39Jud/7KN/sZxrAMCAJwXZTIZ8LkM24xENG+0PLfl30vHw fYnvS5c5Jsn6HtmsRy6XpVQuMFMTZCQIzzYNpUJGo5DBYMBwGDAYhIRBSDAKCUaBZbBFwmTbS2o0 dp3WCmNsdV8hDOVSnupMCSls7+5L3yYk+TYbsdXp0mp3aXcHNNptjhtt6vU29ZMW3W6fYaCSSjgG O5jFiDi/wBoHSUOXxqa7ekIijQWJiIfQRnNUP2H/sI65ZeKKRdGAn4znMVerUqkUmJ2pUJspM1ub YXGuRq0yQ6mYR0ibphyGli8ZjEZsb2/z+hvXuHnrtnNXLKE31tWme37SyjipwOP+/2lgSD23WxAR gQtEbG1EFeDqHowByftEzhUATDPbtEmIHGMEYahII7yPQAuIUr+tTy0YBaCNIQi1HRvuR0NJPXw/ Rz4P+VwZhU0TNkYRqJCjvSO6zZ6NXcsICGzPH/m7UZKKECb+bRxACG3r2mgjUFojBdQqFeZqM3jS Ema+J/GcnxyGik6vT73R5rjR5OD4hGa7x+HxCYf1FofHJ2gFWS+DnG2jT0qYwEOLELyosbvEF7Dc CMYRhdNV4aTdpt5qcWd7LwYGg0Frg+/5VMp5ZqszeH6GpaVlavNLfONf/Am9VhPPmzLGboLgG1+O XIpkG9F2R7hGCpxmC+J9xmDELpk0hEQbUic2CVEx9fkfJzlXAKD1adNNRf6gsPF4pTRpMirJGIt+ AxONLmGW7Topbe8qpIdnJJ5nt+ezArWoGXaGRLyBdOdPK3+yzoJCdA+eS0VGJCAhpcSGH0FpkGhC 96yhsCG/SrFItVLi6ctrSCHwfTsSzvPtvbXqAf/1jV/CXGiiggDTybL5zi/y7jd9mq06g/YOQmYR fh6lPaTnu/tKK4AZi6BEXEj8W9rnGoWK61uH7Lxyi3a3zy9+7lP87OISvufH8wYIZ6ubiOKbIPRS V4y/x2TvPHUOlCnbk2FWqX2iE6fcBRPtawRGawbDIaD3Tl/h8ZJzAwD9fu/2oN89bQZoHQ+LN8YQ hpr0ePHxqjKk1jMGEuP7KOcy6NiliMN80kdkJIT6oZVfCpt8klgHbt2ExRAdI9MAISRRFp9SCiMl OnCWRSiQnmRupgTZkPm5eYQQlMUsf/sn/jb8VfiPf+VXeefOKlKC0m3++q/+m2wdjHj9zWvcvfU6 nueDl0UpD78wjyc9cM9h78e+m0Z7wM5xm3q7TxRey2T8VJFO+zfSxITQGwfgU3odEQEpYs+I0585 2jceNZwmBNw6p/PuC6f2ca5T+rcxGiFoTbnKYyXnBgDmRXVq1pYy43V2lEoAIN37jyv/WHmM+HcC Coli2x4taY3aaDysGTlN+WVq0NGYoouU8svJdcl5JpU/WUcCKNG5pUA6cBLGWg1SCEIUo6FCCE2o FYXcRTy/glaGv/aX/21CZfjD//Hr7LxjKHlX6Xfr/Be//lG8fJbdwwYHR02O6k3evbvDtduH7DU6 hMo4IJvuNug4oOHM9cgCn+bXR3ob7WOS7xMb+SnddoekSIFxcs/EPn46QmDi72gcyETbhMAlHWWn PsvjJOcGAHpAcXLlRFtUSsclvGBaz3563Wn3YPzEmcxpEBBCglBTlT8x+c9Wft/l+P7Qyi8c9+B7 KRAE0HFVHwzkckX8TBGtDMNgQBh4KK3IZouUiotIOculy1dZXS1Y4hJBNuvzd//h/8f/8yf/O/lc Jsk4PEMmvelJtj9i68fXpV308dh/DBCkOvkIKFLAERkd47xCyvGPTpRCHKVsuLTXzt//oR4DOTcA ME0E0Ook+fPGGIJg3AWA6b3/uPJP8gRibB/f910Y0DYqIQziB1R+74dU/oR8TF1vSljLGIEUGoMi VyyT9SooZUCD0RqNJuPnyeZLaAK00gRBgHJZiVorV/fvAZo/+U3idxd/FGebi1hp9RRtjkN3p0Ai CvMlyUKIhGuYNPkja4jJbdH5DHi+R8b3eewrgnKOAMD2/uNkDwJGYfIZjTFnugD297TlSZ5AEKWI JvsoMhkZN2opPHD8wHTlFxNKnSi/ED8q5U+O8Vw8MJ7QExBoHB9KPlch689YklQrO3ZBGzLZPMXC DFKESOlhjPWNbVjQRiweVjTCKm+kmLFZnzLVgfG8v7QbMM70g0G4+uDjLkLqQOPAT7h8Ce3O6/wM IZLjSRkC5WKR4bDL+wEBzg0A6DmjVRiacdUlYZjd8iQAxPvGjVGf6u0T94Azlu1CNhtNXwWocaIs UXSB7w5Or/Mm9kkrdUIeTir6tHVpsMElKxlyXoHOSYfKfIWszqMMNnvOGKTwyPhZwmimIiFs0ZH4 f1yoM1J+QFgr4eHF+hupFCbLuIs4FhDDd5SGnPT1qbOkACS+fPp7AUImAYsIEBKAMPGgJxshjtKz E58iX8jT73ffD/r/PghkPqT8D//tb+4qrfqTfr8xmmi6PWNcKa7QhgOjv8n/auK33Se9fxiasW1K meQ80Vh+zlZ+789Z+REwDAf85bW/zu1bW9y6dYtL3Z8kNAEoQ7U6y+7BHzMchvS7PQwynp9wNOoy GHbpDzoueSmpj2B0Kq//kcRE/8Vznqb9fUNCGEYm/mkHZuxsRFEYg3MhdPQ7OXO0LX2d5BiLJsYY cplMyv54/CHg3FgAQBTMGZPJirs2A0/FPX66J48sgFNnPaPnj35HfwMXZZDSNqFpyi8eQvn9VC7A Wco/zi9MuAOerVOIAKUMQgT8ZO3n+PqHv8md/nU+WPoYIzVEhfCfffWr/Cdf/TVeufYav/RXfpkw fA6lNEsLS+wd/gOKuasMR75Nf3ZViiMXYFrexdnfJk3okY64neLiks8VheiihJ/Er5/EnoghSFsY kcuR8HtRzkFEKEZWSHKefD6PctOxP/7qf84AYJpoY/3dSISwqQFhaEfoTZr70T72/8hlmNwnTRQm bkNsyko7mu4HUX7vR6z8YNAaFAMyXp6rpRcYqSEajQkNc7Nz/B//6z9iMBiRzfoMByOEEPziL3yJ d2/e4o/++Gt89jN/gY2NjxMEYSotePy9PPA7wKnQXQQGkwodE6oRIBDtNBEXjEdXEvvwSUA/dcax w0zK3E+5HsbgSY9sNosxhsFwiNA0H/4J35ty7gHAGE3EVUXKZtfjsgKjbRB7nWMWAWP7nGUJRKKx E1EIo6cqf1rRz1pnLYgHKP8EESilsMBjnAntUnqTVF0wtrBAYh4bCMKQILTrBgObSqGVRumAv/Er v8J/+tVfjQf8pE1rI0TsWj1QUgRdrM8x2++UMiL2UjuPRehEclzMAYg0WEzS/q4+g+MYYisiJcYk IASQyWYx2qAEhGGIlKP9h3zC96w8AQAdEX6SRqNOq9WiWCzg+5lUb2atAiE0WosYGM5KFBp3E8ZD h0aapAefovzTfPgflfKLSPnNNOU3MeMdKb+e2C96XwZsOuxgkPj8KeAwxm5/2IIZAhLnLGUFpPvq RKGJmfp429hxpxyJ6LSkf0T9u3BnUcLExKNIuQIRBQmCQi5HqDWetKM6+w/1dO9tOVcAMBwOmgJK yRrbBIyyzUNrzcsvv4QQkMtlyeUKFAo5SqUShUKecrlEuVymWMzj+zYX3oa+EnJJa+Pch9NWgBS2 jr9VzB9c+U+b+GeQgxHJ6JQ/UeRpyp9W4BSZF+3nWLH0MaeUf+J8j5IDkJ6zKPbtHUOjJxEh2scI dDpJJ/YfkiG+Y51/bAREvX90rYgFFnHlJhFbH/Y437eVh+JEMWOg//irz+P/BI8gKgwOQKxFv4UQ 7Owe85f+vU/x9b/3x5gApPTwfRiNAoJAMRh0OTlpxP68ELbQZzabpVgsUqmUqVTKlMsFKpUSMzNl SqUiUnrO/7Sj26xLgRtmK/AFMNVfHwcBT6b8flIKPk3RJ3mBH0L54x7/B1R+SwI+2veJ9TvV20cK n3jjSZ+cPibq0XGKbw8aB4ekZyfl2zM2PiAmGp1LIIyNZmQzOTt/A9Yl0caQebTHe0/KuQKASREC traPeObqGn//f/rPabZ6HB21ODg64fioxcFBk0ajQ6PR5uigZevK+7anHw4DwrBFp9Nmb8/NT+/E 8yTlcpFyuUStNkO5XKRarTA3V6U8myVj58B+KOWXU5T/VJjvIZU/ZrWNfiTlj/ebXDdmIbigWrRd aIx4eASwiq+tVUXSWyf8feIKRNxA3NOLVI+ethLGlHqKpeAuIlInN+633TWKLggy+SzauBiCsQBw usDc4yfnFgAiZfJ8yfb2IXfvHjrFzVMp5lj6wAYfeuES2VzGVqXJejSbPfYPTzipd2g1e3RaXVrN Hif1Nt3uAOnZUtzGCHq9Af3+kKOjhjP5Lan4U595gY8+ezHu1SdN9jPdAfHDKb+Uhk57QLvXp1Ip UchnEdi89mi0YMLeu0lBf1Dlx+YB8AiZgP1BH4OwCUgiIf7EmFImjHzkW0X+f6rrJrIJrAXmlHxs t3HASP8WjmRI/85kMwgj0A440YZHiHC+p+X8AYCw2U/dTpdXr93h1s61AQRzAAAgAElEQVQ+tZky C7MzVGdsscxaucRctUwu64OQ1gyUAuFJZnyfuY1FvEsSL+vj+RKZ8VEa2u0OrZMenVaPdrtPs9mh 0+7ROumgQjtLrx2Lb3uVPy/lt8oqaTQ73Lm3x+rSAgC3bu/y8Q8/TWgMu3tHrK0sks1l6PV6VCpF a8JrjRbW6NbaDYPFxGTiVOU3BvMIU2cJIRiNRmPmPCYh+0gpMdF2kh9p/97uPl4IJAIra1CIxDIQ FqwtyKRBIfYhQFj2P1BhzGlEGZLvBzlfAOAaxUD4vFq+yubPrFDYPeRgZ4/r9/Y4fPktN8e9wfM8 Zsp5ajMVlhZnWZq1te+WF6rM1ypUCnn0MLQ9lrQTb5SEoDxbRi5Ukb4HnrT56NJjEAQM+kNm8zk8 pWMf/pRSe3I8R2DCSpim/GPuQ6z840Qe2EbuSY9Wu8u9nUOEFDS7fe5u73P1yjrSF3znpbeoVcv4 vken2+dDz19BGNjZPWZtdY7hUBOEI3K5fKJQbuLUMQJRmEcxAFJK7dyUFBmQBgX7HUmUmJRfnzI6 0ryA3S5ioIp2tiClE6RJg4C7FyEkvswQpTnbqI59zkwmfLT29x6UcwUAo9FoKxiNPmJEjlebFfZn ZvmpT36En38qY/PDgWY94Gj3gOOdPY72Dzk5OGZ7Z5dX3r5Dv9UBYdOGSoW8ndprtsLK4hwLtQrL izU2luZYmp9x8TZla/ChyaGZdXFk7peyy6Mrf3yeM5Q/sgIwcHzcZH1tkQ8+c5Fr1++yf1Ann83Q avcYDgPmZis89+wlvvlnbzA/N0Oz3aWQz3Nw2AAMO7vHlEp5NteXuXb9DlJKnr6ygefJOAQITs8e JRPQKVU0Z8KpgJ6z8JMxAZHJ7zamIvaWFyC2gqy+G9f5p33809xAZAlE8OH7GTS2RiPurmwpuIBR 1+/8UA3yPSDnCgCEoS9cL+tLQ6s74o++vcc/+57Hhy6X+djVMpVihtxT66xdWU/al+td+l3DydEx Rzt71Pf2ON7bZ29nj2svv03npGlLawOe57E0X2F9eYGl+SrrDhTWVuZZX6hRLhZiujtium2xTly7 u7/yT2P/hRhX/nEiLzq5lXs7R3SrRZaXagghHSeQQWtDp9vnT771OoVSnoX5KgdHTQr5PnNzM9zd PuSF5y6Ty/ncvnvAlUtrCAHbOwdcuriKVqlrqkfzk7UeN+1FTNBZUIhCd+nefyyNO4nx2bU6NhHG ttnOPpVWNEEdiMiScS5HJpMhVCaOOwghQBvCMESIwc7DP+F7U84XAHgST3rMeGH8QX0JRhteudHl 5Rtd1hbyvHCpyJWVHJ6t3BVVDUN6grmVBeZWF4CfsK3J6dWob6gfHtPY2+PkqE5jd4/jnT3e/f4d Ot94hVEYWnCQglKxwObKHJvrS6wu1LiwtsDqvP07Vy3je27Mf9yDE/usNgw1Rfk5W/kNYLQml81w 9fIG1WqRwTDg1u0dPvjsJd65vs1TV9bwPEG5XOSDH7jEt77zOtVqmRu379Htejz91Do7O4fc2z2k 1xtSLhfY268zGI6Ym62MWxzaYHyDeITRgAL7jscy8gyoCBxJk3UmrtuXjumLSPkdgTi+zVkV0ejC hEdM8QjJSmGiegw26SftfURRgMc+D5hzBgBWrwxIiU51HmBZek8IDpoB/+SVFp6QXFzOcXUtx6XF DJ60cXxlsHP2RSQbTtEQzC4sUFtYSFxK51aoANqNE+p7e7SOjqnv79PY2+e713c5+cb3CEI7vyBA sZhjaa7KpY1F1hZnubi+zMW1eS6sznNlY5mMb+cRRCTXRWs3QMWSdDCh/MYqwmy1Qq1mlTUMFeVS gXw+y8c+8jS37uxRLuXJ57KusIdAK0Muk6Xb65PLZSmXiywvzTFTKfHu7R0W5qsEQUi73QWsTx1V ADYatBA8rCSJQGLM14/N9DQFmFJqkQIBIQxGOuV2ynwq6SfV3UfuhIjdAMaIw0zWDnKKDAnizU9I wMdSjLaJPhk52TAF0ScWWKtACLh7OOLOUQgINhYyXFrIsrHgUczZ8JlyloF1sW2vkO6FE2JMkK+U WCs/zdrTTwOghYxJtO5Jk/r+Hif7h3TqdU72D7i2t8+337xLr9kkMkC1NmyuzLO+Os9Tm0usL1tQ WF+ucXF1gcU5W7UnVArtZh5WWqGVDWFpo2MlldKCTTRy78rFVQcWmiAI+fALT6F0yIWNJQbDEWEY 8tSVNW68e49hEFItl0BCqVRg76A+rvzu+R8mDSB2TCJLJ47tO1+eyFxP+/yno37Re455B+c+nLIS UmeMrIno1HbRcRBCIGUmjv2nRxFq95zvBzlXAICPwSgQqWKOQqBHPYSXI1uuOMIpYp+Ni0nD3oli tzFEvCso5QTrs5K1WcliNUPGx82cQ+wHj7d9Gxc3RjlgSMXQtUFmcixuXGBh86JtYLZsKAYIhkMa +/u0Dg9pHhzQqTfY3t/n+//qDQadDmE4so1T2DkKr2wscWlzkcvrS6wtzXJ5fYGNpTmW56tkszk7 Wak2KKUItZ2B2Ma3jRvMkxCIWgn8jE8l46GMrTb0zNULKG1BQkqBxrAwV7N5A2nSUfBQBUGMkLEL k1Zm2xmnGHrSZH3iBkQkXhIyjHaMfjvSMNXLS3d8DCrp49w6z5X8EpGZRyq1WSdk5+Mu5woAfCNv Dvp9asUcNV/T1zafv3nr++x94/8iN7tEfmGdwtImpdWLFJY28QsV7Bh3ew5hYDDS3NhVvH3PAH1m CoKlGclizWNhJkMhIxAxd2DQivv3F0anrAeDNnZYrXYDb2qLS8wsLrHx3PPY/HerBKPRkG7jhNbx Ia3DQ/rNE5qHh/zpG1v8k2+8aicetacnm8swXytzaX2ZyxsLrC/OcnFtgYvrC6wv1igV8naMuzJ2 4pEQAq2Q2s2dEIX6TBQmtaFPoSRzsxU7fDjNP2iTNqzOlAhkNWbC1E/etyEx9a1op6fC8QMTcX9D rPDjwBDlKrixGnFIL/UXCyq+79vJVcfuJSEmH6XWwXtZzhUASITyhDUFfaGJDFCtDIEWqGaDQbNB 8+Yb1g3UhuzcEqWlCxQ3rlBZuUp57SLaKFAKoUKM0XT6mmZXcX13iAoN+ZxgsSKZKUpmKxnmKj45 XxK6xJrQGDvAJTxtSJ5qVs5iiAFCu9+uty5WZ8hXKixdfopokmGNwKiQXqtF+/iIbv2Ybv2YzvEx b9074qW3bhEOBzGHoTXM18qsLM1ycW2Ri6vzrCzOcml1jrWFGhsrc4ShQmmDNnaeQKUVRgk0zrWY Fnl4UCKAgUIhTzRmP7I8xhl7t2+kxGP8QGTiR29OJIwpyZ84CygeB0AKEBJHQ7iLCqJJTyZ6egcy 1gV4/KcGh3MGANoHPDvpZtQ4lFJ88S/9PB/8j36G2zfucvv6He7d3eHWtRu0TzoER/v0jvYRb30H AXi5HKXli1QuPktl/SqltYvgZRChAhMgjCYINDt1xdaRRuseRhtyWcFCxaNS9JgpeMyUMhRzEl/Y QTNR6i2auDedJpOrtdYxGMR/jcEYTSZXoLa2QW11E422w4ExaKUYdrp0G8f0Gsf0my26Jw32Tuq8 +51rBP2+TarBRT+k4NL6EmuLNS6tzrM8X+XCygIXVmdZqFUoF/Px7MJKKUtqRhaTTLHtkyIMlXIR YrKSWLENiZlvUc25ZZGLIKJCH86MNwlGJPP8JXxAtBjRf2NIkLIejMFW/I3QlCT6EtUWiHiT94Oc KwDwsWafRMQfUAp4+e0T3ilkWJ57hqc+9wI/PZthsQZmpLl3e4vtm9vcu73D9p1tdrd2adx+h+Ob byEwSOlR2bxKefNpZi58gOLyBRePV0iCuFcJQ81OPcQc2V5cGYUEijmPcl5SKngU8z7FvKRcsBaD lMJyB9oQOgIPF7ac7H/MxA8Tmeux1aBtOW/3v5fxKS8uUZ6fR7s8d4ydsTgMRnTrdfqtEwatJsNW i1anzf61Lb71yvU4J95ogycl1UqJ1cUam6tzbC7P8dFnL/CxD160UVIjSZJ4x0Viq3OOTTEWc32T Q3Yjxx7SgBCVEkpchHS0gES5HQ8gzITFkBALkQmC5/muFkICKtEyziUJVaCy/eFjbwacKwDQ0k7g KT3BjK9oKy9G9k4/pHOvw/Wdnms0knzOY25mgbWLa/zET2T4bBUWZwBlONw5ZHd7j8PdQw73jmjs 77Dzr77DfigY5ReYffojVJ96zibtuOsL4lPbpBYD/WFId2CgbnvwSFGlBxlPUMx5lPIeGV+Qy0ry GY9izs6jl81KtDJIYeIZjpKsPxN3cMZom0xj4ggXYFwdBI12U4FjQjdmwZCvVcnPVNBcABdVMEA4 HDJotxj1ugw7HYbtNsGgz+3DJm/f2WMUKv6tz3yIjz932VoyURB/ihhhbBlzYU1qHXHt6SG7TomN SSksU/x20mQfidkRW/8xKzje+9uWEX8hKb3E0zAmVvqUEWFdD6UbL730Tx/7VIBzBQAedIPRAJgl J8cJHrAf2eXguBR3zVEr4KgVYpCxPZuRgtnSDJVCjdkXnmPjkx5zZUE5B9kMlLLgeTAcwf/5J8fc 3p86K1lyXfePm+czbvBBoGkGIY22I52MwWjlCCi7LutJPM9QzPsYA7msDS9mPIHvWf8+n89YBda2 0H/O99DKWD9eWxBQyvnybjKAMFQYrdzIN0W/N7CuhVIMAwOZPLro4eXLhMMhfqjR+3cxjRN8IW0i DQIhkvkQTj23kC52nxRWSTPxUUqAMCl9Nsn7imPyACaxAAQJP4AjCMfqAZokLThNGIJB+NKZ/8Th SEi+i7VMjCVG3wdyrgBAGO9eGIR2gM7YBqd9UxzvqNd2kTbXEDWNjuGoZbh1YNChcSajLQUeUwxG Y1yF4R+GMxYkk3YYmVgUUoAyChUYhiMbOcAkloRIkYdoZYkrZTA6dPdmMMYOaDFKx+vBAoEd+Qda hQi0Y8UNQilrbWhl93dsuQotH6Fioswg5NnlweMZkqL3HiU2QdKTu149Iuti7Y+d+oTwE9E5UmiR 7vSjw50aE7kOaZSRwk/F/pNj0iFFLZ8kAj2ekrWNzk5d7fxOIVC9FmG/S3HlEsLPuDyAFFl0hkRK KSS20i7g+SI2ww3G7vTn3FiESBJsouEFSMd7SKdkrsc0xt6fkcTrMcJl1Nlt0fDlhCyDaYhmDEm0 Qrgw2304AIEbai0kUZGSNGPvdpog6yJznsTkJ/WOo/ED8TIpAHFfxT1Hii2w9yrtnAc2LJmCiRR4 GLAg+gN+m/eanCsA8JTCExJPSmY8W9VdCMHgpM7WP/46IpMhP7tKYXGVwuplSkublFYvJTxS5Hfq x6MivHIJK0LYYck2vKVd49aATUyKO9sI+ASxz32Grk+VGDaNcESl5SfuawE4NyAqt3YKK6NeHDEG DpFi2lWO1ksNHoqsAXelJNQnovXpsf92q5Q+oYkiD2YsQUmmQcGYmCR83OVcAQAQ2/F5jzj05Ps+ oauRHx7t0DvaQVx7ye0qKSxvUlq7wszG05TWLpMt19AqRCsFRr0nAWGmnONnP3GJztEhb3z3dW68 dYNhb2DDoIDwPLxsIZ4ZWAiJzOXQWiMzWad00i4bkJ6PQTnlcyawSUJlsaagQURhzIfjAEQ8W5H9 ndoDe9aEt8cQK3PUd0fJfJNsvl2VBgUHiDFZmJCKNjYUWW/u2JhHcEGH2GWwWZ3vBzlnAJAdK5kN YIzm6gvP8F9+9e9x58Yut96+yfatHe7d3eLW27cZ9PqMtm/R2r7F3rf/KQLI1xYpr1+huH6V6uVn yZRnLaNuRiij4T3QOJqdES+/scUHLs7zF//Kl7h0scbJYZtbb9/k7s0t9u7tc+/mFgf39hkOR0iR nj7cWQxupCHg/ByJyGbtsuchpIcRIEtly3qCdR90VIrLliI7ywKQwthUYOdmGB0Z3WmJHIjx9Yme mug/0gnYSW8f4ULCL6Rn/omMCuFJy3FElEB0zZh1jK5rQeE0hfx4yrkCgJyUx4N+zzY2kTSru7td fvd/67NQzbK68lGef/ZT/Ny8ZLEKw+aArdv32L61xd72AffubLG7tcvuq38Kr34TjCE3U6Oy/jTV K89TuvgBsvkKRocYFfDD0X8/uAgM2/td7uy2CZVGGMXifIWVxRJrV1/gMz/9MywtVliY9Qh6ht2t XXa29jncO+Bo/5Cj/WNOjhrsbu0SFesUEsRgMAYOAixvUiwSZzIJbZVf4/ItpiOAQTiAkO78ekzx IifkNCicpmbGonpuzVgyUGQxpJ36+E2BJ7zE/I8Mgyi0yDgomCdjAR5PMb451lq5PACT5Ie4D95o BRx3FNzugqv5Xyp4LMysM3/1Ih/5hM/nyzA/A1kDezvH7G3tcrR/wN7WPs3jN+n96XfYHXlUr3yI 0qXnGRr5Y5uB1YY1BdKNS66fdDg+avHaG3dtSnKo0DqkXMyytFBhtlpkbmGdD37gWZaXatRmyszU PPQAGvUmjcM6e1u7NBstOu02B/f2adZPqGs/PbmajQoo6zM/0AUwlrGUUmCMtB2uBLSYVN1Epihf HByYtm9EHKY3CmKAENKF/tKDkkhA5hQoWCvnfQEB5woAgHhyjeIZRd0lxOmrQkCoDHsnQ3ZPArgj YyZcCmzx0NLTVJ75AB/+WIZSDspFyKMpFmws/F++fsIrN09NSvxjEYF7NiRIm/NgjK0NsH2vzt27 BxhtOQ0VKsuIu/53eblKrVKkXM4zP7/ExSuXef7TRVbXFvhf/sH/zY0bdwAwWjFfq7hY+kOEAYVH GAQEoxGe7yfhTpEooCGJaqRT/acCRMpijyIBk45FlCsV9+uu6AcmGfGX4gdPg4KB0bB7+yFf+3ta zh0AjJXPSq03qb9R8pqdUNNzDdUpkBt8Yoyh0Q45btopwbU2GKEwoYn9X4PGE9hJQN7jYlMhhPP1 reVg7CB/jFYcH51wsHeMMQoThmhjUKHNG/ClDaGBVZpSIeuGAj9kIhBW+WTSzU5X/tT6tFi3XKQi Bu4c8fbxg4RIQYYReNK3Pn2UM0EaUMzY/Qt3QxovuP8bfTzkXAKAjKr1Cg88j3Ixx+xMlqKvKecl xbxPLivp1Q/I+hIQ1BZmKRZz1lwVlvOSrjjPYGgbpjbQ6WnCQNMdKvoDxTDQnHRGDEaKk05Itx/E vY9AxATWj8tNeBSJzHR8D2kMnoQ4EShi04WxA5t0RLhNV1q7zUUApMCTtlz6pF0tzlhORwiEs/+N EO49phKK4sSgtAtgeQmDQXie/Y46AR+T3jUFCvE69eMneX9Ucu4AQDrFW8hp8gdvMTy4ze7JIW8f H9NpddEqGckWtVxrHcZ2IxjIl4qUygUKpRKV2Sq5YoHq7CwzsxXK1TLLa6sszVcpV8rMLZQTXxQ4 alqW/qQdcNIZcdQc0GwFNLsjgsAQ0WaGJNnoB5FQaedT2xx3XB6A9XW1LYFuXEQMFd9gEkmLBsnq 2PK5rxhX3FMIex10HF6bJnEikDHg4XIV7ifpMN1kbCCtwCLmBOLl1J5xzhACmcmAsYmg0fnTacfT QEEAvAdDvz+InCsA6IX5O91uB8+XKKkpn7zN0fY2e1t7BEohfR/f8xGej+d5p1OGU2KMsROANLvs 7+xb3XHpr0SDeowNM2ayWeYW55hbXmBxcZHa8hwr6yvMLi7w1NVV8sVZNBAE0Ooo6u0+jeaAw3qf emtIvTmwTH588bN71UgyvuA/+Hc+Rtjv8Oq3XuPN732f/e09lApBSjcxqcQvla2JKwwiW7AtXUpk NmtHDWbzaKORmZx9Ns+L4+kxoukUOGIQbogyELtP0yROBELiYcOzU3Ya67xPuxNuDEDKP0h3+JFp dVqZXWVlzxs3/R0xmKQAnAYFGQPq4y/nCgA2vfpQyFl8Ifjnu1kKn/4lfvVvzLK5CMf7Xe7cuMP+ vT22b91lf3uP3a09WvWGbaSej5fxkMLD87zIinShsAd3083jJo2jBjf0OxYotI7H7ZfKJRaWl5hf W2Jlc425pSUWVpb44MfWQPqEoaLVGXJQ79Fo9jlsdDlqDBgMR4CZ6j4EoeEf/eNXeXq9ytMf/xif /4ufZ76W4WivwfbtbbZubrO3vcv+9h57d3fpDfpu8I6Iw3yWA4mUzj2j59vnzkT5ABJZqkA2S8TA aZEMt34QB4CwboP03HUjhU5TeWKyDz91ptNhQDHx04GCEcRAI3wPT7rZh1PDhKN7PwsUohGD7wc5 VwDQBBaFHQ5czRqu77X4h3/UZqGa45PPVvnQJ56DTz7nctoBCf0uHO7ss3XzDgfbu+ze3uJwZ5/d 7R1UGDprIWMz65w/O1WErak3TVuV0uxt77K7tcNrf/YyqKRE2PzyPIvr68yvLbO4tsrGhXWef/oC 4NHtBRzU29TrPQ5POjSafYIgCciNAsNr7zb43vVjlFJIo1iaL7G6NMPaU89x+ROfZHmxwvxsBhPC 0e4xh3uHNqx574jDe7s06iecHDU4qTcdSRjYRCpHfEhA5Ao2QQhcYUQ7kAi4byqwTGUBSgSejF5O WqEjX59x0zz9YqMjplwoqk8YZQma1DEZL4NxEZ3YOnBkjr3+dFCQMnIZHn85VwAAFxBCIxDUMtZn zEhBqxvw/75U55+9KvnIU2Weu1CkVpQE1l1mcWOZxY1lIpJcCFsevH7QZe/OFju3b3O0s8/O3W0O 7+3TbpwgfM+5ExlbP+8BLSbq/SRy7Kt0ml1aJ9e48fqbseXgeR5zK4ssbWywuLnO4vo6Tz2/jJ/L 02gPOa63OTjuctLs0R0MkULgZ2yM/fikx+FRi1e/fwcVapQOQBmyWcnCbImFuRkWF2pc+MlNPvTZ T1OrlllaLpHPwtFej8ZRg3azxeG9fer1E/qtNm/cPGAQeQECV3QjUkZLok4Tg0v9lRLheeNsO1MO k045p2y0l044mziakB5UlAaTOP14HFAizElhzzgoIBDC++HImfeQnDMAAF96eCIaeJ9eb9H95etd vnu9z9p8lmfWczy1nCeXg1BB4Ip7RvX2y5USVz/0LFc/9GwctxYS+l3FwdYO+3fvsnt3m+Pdffbv 3qNxdEQYhPieD5mMcyfuVzOLlOUwDiAnh3XqB8e89d3vYbQl6cozFVauXmZ+ZY3NjXVeuHqBvoLj epvjepd6o83A9dyekEgffOODb4Hl4LDF/n4DYxQ6dEVAtEGpkKwvKZXy1GaKzM/PUC7nqV24yNPL cxwE3+TunXsQ8R8isQAeyAGYKBWY6RzA6ZcRm/IpJwHrq0fnHT8kvQ/YbL+Mn0V4dvQfJKSncabG KeY/dR7pCbJPLIDHT/LLQ6NVViPPxm/pEmQOmwEHJ4pvvDVgddbn2Y08F+Z9MhlXAlxDqI2t4Wdc tdkoWiAlS5vrLF3Y5IUIGHBWw94Rh9v32Lu7RWP/gP3tbep7B3RbTaSXRXoZhJQPJCERjjRLAUO/ P+Dmq9/n3e+9jta2cm91YY7FCxepra5w6ZlLBH6e+nGLZrPL8UkLNQpjr8QCmABjwSEeKqwFGE2v 26fT7rJ1d8fWHFAhGIU3dh8GoRMHfGw8walHEDERGE2FlmT1pFj7UweeER4c6+XHeYRIsaPQipfx 0NogZbr4Z9r3F1NBQQA6HBEads/+OI+PnCsAePHXf/3kv/nv/vu6J+VC1kuHcQQykyM2JAWgba+E hMOW4uD7PbSQrFZ9NhY81mc9ZkugjCTUxoa/IrZau1p8rty37RhtbLpcq1KeneXSC88jhWfTZYSg 1+5wtLvL8e4uR9v3qB8ccnzvHq3jBuEoQPo2OiE9z9UzOEOppHU3InVs1Zs0j1/BvGRrAZZmytRW V1nYvMDzF1YZeXlOGl0aJ00G/QfVL49AwpruUni2K06VzxYCdGrmnAcnArlzuanOEj1MjomnTHTf KhmNH9nvaVsgmVQk/X2J97CJP3HiE6QY/2g5If1Og4IANFKI9v3f1OMh5woAAKT0DUKwUEhMRpnJ cv1//i2K609RWLlEaWmT4tIm0ss4MsimtfoCjrua457m5Zsj8hnYmJMs1SRLMz6FrIdS2s4YJJLJ MWOdMknFXvs3iN0JfMni5iaLG5uYTwCu5wyDkFb9mMbuHgfb27QOjzje3aV1eESv3bKZipksUnqx co6JcIrmQGE4GLH/7i12b9zEGE0un2N2bYO59Q3yq3P0tODkpE2/23sgGEyKzX00ceUgeAgXQJrx SVCjm07d/zi/l4q6TCRKJDP4Tru3CFcEmWwmzhyMgNlGEpNBPmbsmkkkQAgIQy8Gj8ddzh0ApCfW jNu3gN5JnWGnzck7r1hF8nwKixuU15+itHqZyoVn8HJlF+lSCGPLX988CLm+a3v8ck6wXLOzBS3O +OSzIiLFXZmuKToVh9Bt7b2o7n88SYg2FCozFCozrD7zDODFiTn9bo+TgwNO9vep7+7SOTqisb9P ++gIpTXSy1rF91yd+5iXiywEjzDUHN65w8HtWxhtKFRKzCyvsLiwhCrM0O2M6Pe6D/dyYyIwUaQH JQJhJMZIpPRSUYCJ9yRO/4ijAiKV7COTXvsUuec0Wook38BEyyYhFq3xZ5OfovSCJA9AuKiBwXt/ 6P/5AwBPGDzpkS7oEI4CfvW/+irXXr/B7eu3uXPjDmoYEGzfonPvlmO2DfmFFSoXnqGy8QyVi8/i 58sYFSJkgA40/ZHi3f2Q6ztDjDZkMrA441EteixWM9RKPllfEmqN0BE4pMwEY8Yafpxao1MzB+kg qfOHYHZ5merKMpde+BBxqpCA1vExjd0dWodHtA73adfrdI/r9FpNZCaLED4ymoVYWGZbSBj2hxze uo25eQspobywRHl+AQpleiNNOBo+4A27OQGN5QIeGAYUBi8aa5Fi5WMlfZiPmspSHIsknAIOQcbP IH0vLqw6jfSTAqLKxG5EQzzPo62d9n7JAjiHACB8O7jHS5twRrPIYRIAACAASURBVHOQf4YP/sIL fO7fz7I2D3u3D9i6tc3tG3e4884dtm5t0drfob2/g/jOvwA0xfkVKheeZebSB6isP42XLyLCEVoo jNJopditK3aODN939f3zOUG16FEreVTLHtVylkLGVaNRmjAiE6fU/idlQUTxaa1TE4JEf4Fsocjy U1dZvPK0nRDE8RrBaEjr8IDm/h7dep3O8THdkwa9kyZaKYTvIaWdQcEYQfvwkPbBAQhBfqZCrjqH KZQZKjuPwJiGqpAoEUgZV+j7PjODxPMCCmmn4nYBeZGaImxch8/udhNWIF1d2KFJyn3ws76L5Yv4 7Pcj/eLaipEr4lZ475NEgHMHAL1Op4WQi+vlhEySAq5ttXnzbgc3OJ3ZSpbl2We5/LMf5dP/rmCh BmaguX3jFvdu32N/e597t7bYufMa+y//S5RWlFYuUbv8HDNPvUB+fgVjlO13XHjQCAgCzeFJyF7d hsyUNnhSU877lIoeM3mPYtGjUshQyEmkL+NKu7FZasCEEQQw9hdwk1cajHLuRGq+AWMMpdl5SrNz tvqtsa6JUiHDdovWwT69ZpNuo06/1WLQajEaDMAIuo0T5EkTIQR+pYJXqqLyBaeXBhOG9trKuLLg 8r7DgXE9vxFmDADiQPwYvZ8i/k6dJ73PuFInecE27demHut41OCDST84DQoC/f7Q//MHAEEQ7ksh nkoYZCuC1JwAErq9kJs9zY3dEVGjzGd9FqsXWHz6Mh/6mORnS7BQBd/A0d4xh7uHNA7q7Nx9jUx7 i+oHPs71Pc1gFJ66DymApLwAnX5Aqz/6/9l78yBJsvu+7/NeZlZVV9/33TM9987ei8Vyl1iABCBc JhniAYmWTJOUHCH7HzPCofDfVjhshf/RHzZJ26IVEimGITugIE0qaJMhkAQJLLFY7A5md2d2jr67 q7uru7q7rq478z3/8V5mZfUxMwsQwMw2HrDTdWRlZWbl7/t+73d8v2yGYh3KdNUlPUF3yqUrKUl6 DqmkJJVw6Eq4eJ60hB+nCINEHoOJGRjP3AbptLJde1YtSGtkIsng9Cz9UzNGKchG9/1anVqxyGFh n3q5TKNUolapUMssg+Pg9g9BTy/t2goV6ec9iBUYJRgbHQLs2vzYrGoN0wb+Qmmu40Mc+UuHt6Bt 3tBLuDgClI6l/kQUgnkEUDDeiW9B4KMwzhwAODb9I4XkYQKPQpjtw6yb0ordYovdUovAbwfrpFAM dKfo7Zpj6Nw8M3PP0dfbRSu/x4vzad64e/hIt4vgCNW4EASBpnjYIF/WtsnIshJr4z24DniOQzpl ypCTrsRzBI401X+OFHiuKXxQgcZ17bkHEAhTuSscEEqZdUJY0GAilwYIpENyoJ9Ufx9KBRYwQAQB h8U8tXyBaqmEHzYV2TW5OYeHFQKJdhrwaHozivTFr1AICvZ5ONHT6SOE70cQIiQJ10NpjRNG/gln +naU/zRQMNuYv9IVOOKjYTofjbP4EMPwWTpIGeNzEAIdtHC6+wydnQRUXEfuyD4w3kKoBaA1FA9b FA41aztGcANdszNw5W9FGiD0ciWgbTWc8R40zVaLRhOr76ejTAI2q2CcggCtVaRbiFakkg4q8M1M HWDYgLSyzxWB8k1UPDACJxqFUIpGvYmyoiBaBwil0F4X2k1YWXRzDQzL1iOwAtvZ/5gH0F6mxx8c eRw+j4NCO2qPnc1d19zqIchEQb8wgPggUIAIDIRwaDRqSBHsPvKP9xiPMwcAoQDGbF87EyDdBEv/ 7n9BaU33xDmjCTAxS3r8PNLzzOx4NDF8wghncOthRpPph82nf68jbivSPtC2xBlti1hUO3BXr7ci dR+tlVEPsueqdeyxUhZMwhJfHYHP0SuiaCvoAg9UBxaWEBS7/pdhyWR8Pd+5tO84V93x5nGAiF92 1/VOLfYxX/looAAYWnUpCyef1ZM1zhwAOJwg1qOhpTWtcpHm4XsUlt6zsQDXpv6eont8jr7zT+F0 9YAOUC2rB/AYUICfNlQkDCIsfVHYbis6ST+0CegLodqz6Pc6RPt7AePin7I/qW0VoNadpcBHDfoo goQR+WjbjpePHb+UkoTnRFWZ7YKfozoA7T0cSw+GyxrsEvKJ4HB6+DhzABAEerdWreGEVX6A32ry T//5f8vBxhrL91dZvb/C+so69Wqd5tYGh9sbJjWlNenxWXomztN77il6Zy/jpntRfhPlW5GQxyZF LPjCpy5TO6xw81s3WFtcA224/hDgJVPgJUArZDJt4iHSRbouCIFwXLQfRHz6QsdYlGMzY8fQ2mYg aKfyjk7O8c1tOzC0i7OijTtm/hOMXLS1HaK1+ilOmuclbDxHRIYvaJdnm0M/IRbAcVAwfQsSnCde GRw4gwAAFEBHhgAmUPUX9zWTw1e4+rkX+NQvuwz3Qq1QJrOa4f7799la3WRjNcN+dpPi1jryxl8B gu7RSXrOXWNg/jo905eQCRfl+8adDn50N4lG8/4HGZ67Ms5/8U9+nsHeJOuLa6wsrLK6sMb64irb K9s0m0YUREYkIAIhHYQjDemHdBBeyjQJuZ7tlnKMFoAQKGHW1UdLmJRVHX5wM5CZR6vVMp5nqxXD 98BSixEBj3m9UwU4HieMtocO4Z+Ezf0fY/aJR/pFGxQMYdrJoCDsOu+jMf+fQQAwLDeS0bSkK1bP Wa35LGxWWMjUzE2uBH09HhND55n6xBVe+hmHkQFIO7CT2SWzkmFjbZOdjW2y64ss3Pg6vq/ombtM 37nrDF55EberC03wI7lZBLB9UGfjjRX+6K+WSLiayeFu5s/N8PwXrvGz/3iY8RGX4n6d7Y1tNlc3 2M5k2clk2cvukd3chmrdFumU6GAKEm3wRIMem0SkuuwXa4QOpcHkg2MAVhnIpNSciLL99BWI6LR4 QNOO9IeswpGhCnAcF0dIlPXgorV9Rzyg/TgEBXkKKID1ouRHw3Q+GmfxIYYEHMe4nke99UgTwP5X bwQs7zRYyTYNKYSUJBzJUM8AQ0PDzM28xIv9MNQDgz1QKTbJ5w4o7uc52NsgMTxL1u9naav6IzhT Y0euFDieBK3Y2jtkM1u0nP8BKgjo600yOdbLyPAg5z42x8e/2M/46AD9A4LDgyY7W1myVjFoN5uj tF9gd2uHXHYvaohJqQBLlm7bo4UVz9QRaJx4fGFXoV2aHM0CtOm7H5QBOOH12NogkfBMOlIJiGZ8 YT8hTIFUuLWOu/pEj+OgIIWtvXjYxX9CxpkDALMmdTjtRgqLZ4Ruu5HRxwT4CnJln51Ci8DWkyul QGmSHvQkEwz1zxGkhhlJduM2q8TDUz/KIcAYpGuUgrQrqNebLK/mWFjOon2fIPBNJR8BPT0pRob7 GBzoYWR8mpeeeYaR4X4GBnrp7nbw64pyocS/+bd/TCYTZsU07Uo7EWksnHg8OpQFE0hbu9AW44of tf17Siags/GnDRhCOLiuY1KhMXLQ48bNsWXAaaBg5gFhy6Wf/PHROIsPM4SrtQpwHHtDSaMNkE55 9HZ79HV7dHkSzxN0JR0SrkRaN1Y6bYIrIYweQFNBq+lTb5jinHy1RaXmU214LN0/tJ3ElnUGUzsQ L1h5HIYQ1q11JY60DEHagMPGRo611Sxa+QR+QKACCEwGpKsrychIH6V8OWZVphdAPFIMwOYkhMDB McG1aNMHXJ2YZxD+FrE3o8+6rnX/ZYz1p2PdbwDkUWMDgKEvM4Uij3x9H+dx5gDAEWqlXC4zMdzP ObnP+toGyWaJYrnAdrlEo1KneliOzf5m3Znu7ybd3YMGBoaHSHQl6e7poae/l8GRIdLdKcZmprg+ 3cXAUMrwBwpoNqDpQ76syJcb5EstDg4blA99DspNS+KpbYGPOcbHBRigPeMhTDWhqwUoidYOQeCz k90ztQUC0NoYdFhBCA8EgFCRWCAs0WbnbH9sAWCj+ELE1/4xTyBeO4AgmUygtcZBdngIcYLPk0Hh 9GUACJTfQsgfE4I8mcPzfNcV7NU1T3lZ3n7rT9lc38ZxEziJJG4ihfQ8Q/0dG839IoW9AlprNlc3 bLFMWGXX1gFAKxzXY3B0kLHJcfqHBhgaH2FkfJjxmSnOXxinb6gPW4xHuQa5fINcvs5+oc5+sUHh sEGl1gRhqw2/R0TwA1MVaEpyrby3MOq9Qqto/Su0NayoMEY/tEz6pKEx1cSmXDlMsZx+/DpSBg6s Wy07tm0bdmzI9jvHd9ye/UMOhDB0F5X0CqPuC9CWDHswFXgICsaFs5JpQvyYEuxJHF6g8BBkij5/ 0nqef/QvvsjTc4LF26usL66QWVpl7e4imbVNDg8rOE4CN5HESSYR0jXMOx17PHltX86XKR4UY/X7 bR2AZDLJ6OQYYzOTjEyMMTY9wdS5Ga4+N4GXdAh8qNQC9gtVdg6q7OxX2S/UqFSbBNp22j0EFPxA 8es//zxJR/POG+9w57u3yaxkCAIfISWJ3n7QZu0tU0baWyRTgGkK0hpIJM3OHBcRUWXHYiPadMvH h1AaLQRaWA/gAYVAwhYCCW069aTTDs6dOqI8X9wDiGcFzCael8CVYeNPzLCJpw2jncYKguIdgMdB IaxVSHw0dEHOHgAEHigpuDzs0aiX+eNvbvC1pMtrT0/wqZ89j3QsCYSA/WyV9cVltpbXWLu3wObq GpmldSsSYjwG6SVNajGMYEdW0tYBOClinMvm2N3atW26JpAoJfQPDzI+M8Xo9CRj0xNMz8/zzCsz aOHQaPjs56tk9w/Zz1fZOahQqzXbrm9suI7k3/7RTS7PDXD1qeu8/oXXGRtKsZPJsbqwyur9NbbW MqyvbFDKrEauelgP0KbNFuC6CMcB6Rr+f2FrARwPLUG4Cfv11ogs4QmiHTg7aQihEVauTIZeij2T iPU3MvhjUMPJXoBZdiQTbqzxx2yvo2lc2DLtOCiIKE0YVhieBApCSFxHYGpKn/xx5gDAU4bMIawA cyW0fMXXb+7zzVuSFy/28PyFHtJJQXdfmisvPsO1l5+JkgdawdZajrV799leWWN7ZY3VewsUdg7s EqILJ5FAukm7dm4Hx+LeghAC4RwvKa2Wqizdvs/i+/eiVl3HlQyPjTJ+bpaRqQmm5s8z/9QkXtcc lWqTnVyZfLFCdv+QQrmKpQ1ASsn9tSJ3VvKmeSdQTE/2MT0+wOwrr/ETPzfA2Eg3fWnIrO6xvbFN dmObvd19shtZsptb5HMFpGxGYCBiXXthBF0ODCP6+u2JmaCeaSd+cAwgVAYCo7YTry8wnn47BROP +Mf2QIdmoX3gupb0M0SgmCsfGraOHrc/3BEb4GRQCEHS+WjY/9kDAClEtVavMSg6Dc+xDTw3Fiq8 tVjj4kSSp2bTnBszqjl+oG11mGBoYpThiVH49CeiGa5eh/V799leWSW7usb63QV2NjLU63UctwuZ SOIlEraCLrbWPQIMcc8hPssU9vMc7B2gv/NddGCIPfqHBpm6NM/w1BQT589x6flJtJNkP18hd1Dm IH/IQamG1ALHleDAzm6J7e08KggItEK1WggBk6N9jAz3MDYyxbWr1/mpsX6GB3vp6YF8tsJOdpe9 nRy5rRyFgzy7m+b5YalM4AfmRjKB/4hfAB7BAxASRygcCa6UJ0Qe2qDQXuET7TTyA2Jfkkx6mAsY FiQJOtN5R0GBjrV+Z/lv5zJAyAfwGzyB48wBgJYy02y2ECf55ZgiwKSE9VyTtZxPynO4NJXg6lSS 4V6HQBl+fz+IaQFYt3L2yhXmrl6JqtC0gPxOge3lZbJra2wuLLK9skxuexeNxPMSOAkDDobOOzR4 fQIwxHQA7K9WrVa5f/N99I13I3GQ4YlxRuZmGZub4+nzs3jdsxRLNXZyRfYOylQqdRAC15U4GrR0 QSty+RK7uQNuBVYMRLVQfoAjJT3dSYYGexgb6Wd0aoaJa1f55Ogg/X3djI6m+Pf/91/xl19/yx6v nUnDYptHKARCGLFNIYWBvI7tQ1Lw00ebTdh4Pa5jG38wQGuCfkfYfR4KCvb9KAAorFclkK7E+fES 4MkdAhhKSbq89o0mHBPgQ5vqNWlzw4HS3N1qcmfTpy/tMDficWHcY7hH4AfaUIBjO+BUOzOgNGih Sff1ceHFF7j44ktmNpTQbATsbW2ys7bG1sISO2srbC4uUSmVcbwk0kvhpFI4XgJTThszgiPAII8I 1RX29jnI7XHvO++gA0V3XzeD4xOMXZjn6uwcif5J9gsVDvbLFAplmkHQrm2QRpwMqXG0i3YMAWat 1iBTqbC+tkWgFPg+gVKooAVa4UpDXWYupMky6CjDcHoSo10IZDQYxAmlwNE8L9rPTvpFQ94gz4v1 /XNS0C8+o39YUDCvSfRHJQRwNgFAK9vTHgaXpMvBu9+gUdglPX2RnskLpEYmCYn9pQ171xqKu5st 7mwFpDzN7JBkdsRjrM9BupogMLONH9qnxqYHTX99xMGHZnB8gsGJSZ569RMm4CUl9UqFrYVFdtfX yS4vsrOyQj67Q7PZMsBgg46Ol+jk/4+DwhFPodFosb26xtbKKlopPM9heO4cQ5NTPHX+HE0nxf5B mVKhbIRBThlxtWCEi6NVpBjUIQyiwkyAboPKA2IAQts05RFa8LgXIGL/tvEhti7QELp0yUQiWsNH fAWireoTXqlofS/aOw09BSHiLc2dLcOmN8L9qNj/2QSAsE49+lkFtGpVDu7eoLj4LgBOVzc9U5fo nr5I3/zTpAZGjatp3cKW77OU9bm31UJKzVivZLzfYWY4QXfK1MYECBPIo72GtIzZZobUGq18I6Zh 3f6pS1eYvHyV5z/3eUMJpjTF3V121tfIra6wu7rMweY6+d19czMm0jheAjeRMlH6mERXCAxm3Qo4 Rokot7bOzsoq+o1vkkqnGZyaYnh6luTcMOVaQKlUpl6tffgLq0EhUAi0Mn/jcdCjQ9qAoUBYqrZ2 JVSHkYcP49N5/F0JpphIGq1FrSx2x0p7rQt/DAjij2Px2rZqUHurkE1Yax8tnRil1JM7ziQACJta CgFABYqXP/UK9VmPhTtLbCxt0Cof0rx/k/zCTeTX/z1udz890xfpv/As/ReewevuRQcBIghQqkWu 6JPNt7ixXKMrARP9LqMDDkM9Lr1dDkEYZAog0O0vjyZvm5ZSOohaVw3VN6T7+jj37HMR978Ugma9 zsHODvvrK+yurJDf3CS3vkKjVke6SZxEEieRQnoJEA4yan4yByJtKqvVbLG7vEp2aRkB9AwP0Tcx Sd/gEE2RolSp4tcfpgUQnotuu/9aAco0UZ2CAKZ9V+MIgRtpIcZm9vYvFo8FEkODjm2TyYT1UCQy NmvLmKqPUp1cAGE68KGegn0sHQe/1cIXevWRLspjPs4iAOTL5TJKaya7Bbs1jURRT41y9Ytf5nO/ 5jGUhrX7ayx8sMj60iof3LhD8aBArXSDvbvvILSma2icvvnr9F+4Ts/cNaQrEL6PUAHNls/abpPV XUUQaBIujA+49HcbgZD+tIsQAj9QIK0XHWtACUdosIGKA0LI/S8YGB1nYGycCx97jTDyWDsscbCZ YXdlifz2FgcbK5R3NwmURibSSM/DdVOIRLIddHQMhTdaUykUqRzk0VqTTHfRMz5Bum8Q3+uiWqk/ nONAtSnBJO1y35OGCOMXIlwGtOsAjlj8ic9DozV/hGn80bG+fYg9tsdknYwIFDDdi0dBgdjvEfcU hP2upJRHf64ncpw5APB9P9dq+WgESU+ga+Z33MnX2TlomlvCkUwPDzJ++RN85vXP8p/9N+C0AlYX Vli4dZ+1pXUyy+tsvvXnbH77a0gp6Ltwnf75Zxi68gJOugftB6B9hA4IAk1mr8n6jrK95YqBHpf+ tEN/t8tQX4LulIMjBDKwFN9aoP0jNBvxJ5GEmKUQ1yYIKV2X4dl5hufmjW4nApSgkt8lt7pMfnOd wvYWpewW1fwBOAncZJcpaPKSCNdFOua2aDaaHKyvg1rF8Ty6hoeRfUM0PZeg5QNHYwbGVELJbTDh udMrAbWNAWgTA3BOa7KN7UEIwlYcgfEi0BrXc3GktLRfJzH9PAAU7Osng4KRCWsXApmYRuKUI33S xpkDALCFHrE+cAij4ESR+p1Cg51Ci3dXaiAESU8yMTTD2MuXePpzMD5o0oVbK1tsrmywvrrB5tIH ZP/4ryj7Cbrnn2X42sdwu7qjKLu0TMJaC8si3EQFJkAI0J10GOhx6U45DPR4dKc9042IRvsaX5ob 0dTYGBPQR+Yhk41QUfDRPAcv3cPUU88yce0560BrWo0G5Z1tDjZWKe1uc5jboZDdxG/6ptLRSyIS XThuAt8PONzdhZ1dnGQCr28A3dVNIKUNlkYLKpsFEShC9d8HBAGjLIAk3gzUNvjwn452n3a6UANS kEx4oC3tl3hU1t9HAYX28SgtcBynU1XqCR9nEwCwAhoP4PM0rl4bELSG7YMW2wcB70pJECg8B/rS g4yNjXLt8iv85C8KhvqgJwHlfAXXcdg6dPjTtw5o+sc9xhAUDGOvptrwOay3otJgrRWOFKSTDr1p h1TCIZ1y6O1KkEhIPBeCQNrMg23gsbUJCmIdq+1yY/M3iHoTekZGSQ+PWs/EfKRRLlLczFDKbVM9 yFHe2aK6t48Wjqlb8FI0SwVkIoWb7kH09SO6us03BTAxMoRSPqYd+DQxD+x7Zm3unKAL0OHic7xR KBwm9y8IHQ8dRfTilN7ttfzDQaGd/jsKClHXovfR8AHOJACgQSvBeI9g/RBzQ6gA2dWF1MbgVTz5 a0ccFKQDaE3x0Oeg1OTuunXdlSZQirSnSboav14hEF2PfGjRd8SAoVJrUa42jNsfc/1dCemkS8KT pJKSdNI1QiGuQzLh4rgCF0EQgAi0iTfYcj2NMNJkVolIB4FVI/JxHY/B2fP0z84ZYFDmE4e5LJW9 HJX9HJX9XWr5Xeq7ywjHw+3uRwVNImEQZWIAD/QATPLfZCzDfgpxspVHPkFHE5B57tnqSsfRHYYc pfCiWf4RQMFM9YQ8AeG+4uxEp53PkzjOJACYaLsi5YYpZElx4V12v/UndE9fIj19kd7pS3SNzSCk ixAKYuWhR0ck82XKBXAk+ErgNzWadNR++v2M0BOWYfl8BA5NDqshMKhIFxCbXBRAV8JBSiLFIEeC 65hZVwUBCc8UQCllHmsVmEKfUAfAN2XDXk8fg+ke+mfOmXSmUtQrFSr7u9QLBxRWl+zRKhCmdFo+ KAagNAO9PVQPC2YJEMUAwsBeeMU7QUEceZJIhMG/TuN8YLHPaaBgd3saKDiOgxAC70P+fo/rOHMA UKlU1guFAnYCjIZS0KjV8VfvUlq7y44A6Xr0TF+ie/oyvbOX6ZmaNzez0ASBD8Hjowlgml/ABvMJ b3StNbVGqy3qYQ2dwCgAqcCPuAHC1wg0Gh8dGMFxFSgEvlEIsqpBCm2k0YUhANGJAUikzIyqiPgA HqwOLEglE5TzTdOWHCsEMhOuMfCoDvLofoTAcdpdi50c/53r+9Div19QMMu2H3sAT+xIp9N1k+Vp d5EpFfCTn32NSz93lTvv3+f+rfss31uidlijsXSb/PJthADXS9Izd4WBC8/SO3OJrtFpgpZvXF8V dNyAj9VorykQCIQIwriaLZkVpnNOWvUgabMQVkEljFPI6H1TKh2+b4Yi5PZD0uYDeEgMwAQA2oYc SwJ2kH/IaB9h+s8cf9L1kI403opoG3m726+T7uv7AwVMpkIptKc/EpxgZw4AwPz4gVKMdRlYF8Cd 9Rr5kUGmn/tpfvFzX2J6BJqlKre/+wFri+ss3l5kY3md3L132b/3LqBJ9vQzcPl5+i88Te/sVZxE EuX7BKplOoZ+lOcIjA/3oIIWy3eXjYcgBG6yy/T3o5GuhwoEoTSYwAibCDDegurwtx86Qm6/MBCJ eEgMwGYAHEwOP5zx2+v9cMsjbMHW9RdC4HoOSmnrPRzn8ft+QSH6bJRUEDQbDUSrFa53nuhxJgEg vDuTTmxVLzTZgwbZ/SZv3y+DlnSnHaaGn+HKZ1/ik39PMtIL5f0i9967y+baFmv3V1lffpftt/8S ISQ9M5fov/Qsg5efx+seBG08gx/JKSo4N9nLTzw7zcCXX+Ldb7/L0r1l7n73u2RWNowcuJfA8Tyc VBonkUB4CUQiZYJxXhLpeWjXQ+JAeK00pzICGY4Dcy1VYFIRD6wDwGoDWoEQnJjotjXW9nPzqN0W ZUDDtD2EhtppzCeSe2Ipvex+2rP/6aAgYiDiug7Slezs9D6m7t6HG2cSADTYhqDOISDSBEBomi3F SrbGym6dkNxuoDfF+PDLXLjo8NovwNgg0NRkljfYWs+QXd9m89YfESQHcacuI0Yu4vs/fBCQEt58 b4tvfDdDOiG5MDvIU69/mk///V9gtN8jm8mxdHeR1furRuRkeY1atR5x80WKveHywXEQyRTC9awy kGEIwvOi5YWUEld6lpBEQ8gudKoHYDrrpDT9ClJKa4i29iH09UXnZ8KnRvEnNHJj3p1uvPl8fGnW Kfhh9tehAsSDQMGSl2oBl77/3+hxGGcSAFTgK42Ol4tHI3wtdIU7/hNQrvgc1hT3t1Qkw510NSO9 I6Ys97LLCylIOoqRAY/A13z1m3sUyv4P7wTtkFKQEBI/0NxZ3OPW/awpPFKKmfFepqcGmX/tdX76 7w8zNZ7Gr2s2ljdYX1wls5phZ3OHjdUM5ULJlPTWa+ZvXCGINiOQsNkQAGF6pB9cCiwkWkhbt6AN lVg4a9uKPx0rFArLi7UIJcxMN2F7/R7O1G2PoA0OxAz6hGIfYcqM2qBwkqdgPivER2LyB84gAPzG b/xG41/9q3+9qbScHU4LlHQQjktPOkF32qGnK0HSEyQ9h66EYyPN5rNSGNe3WgdQaF9Tqvv4rYBK PWBpu8XdjKJa8xFagPBBG9qxH3Xc2KTYTSmiVprdXIntGxl0bwAAIABJREFUnTyB76OUEQNJd7lM TgwwPT7I/E+8yicmhxgbHaArCbmt/YgybCezzXYmSzaT5bBQJtmjcDH7d60b/+iMQMa4hcIEFcOh tdEXCCsXwh4Du/b3XNdqEob7smXP0Vo/vqaPB/bgZFAIlw32ep3oKQhcKUE4HxUH4OwBAICUjlJK MZkK6N1+l+ruBtnDEo1aFQE0ajWk4+AlE4SI73oJBkZHSHQlGRgawkt4DI6NcG5ynIHxQXoHekim TC9504fSIRzWfEoVUyhUqvnsFxoc1nwTFhBEMuU/Epkpu3Y2KkEu2lX4fsDqeo7VlW2CwCcIjMCp 5zqMDPUwOjbA+NgAT33yAp8eH2JkZJDuLvi//t3X+OYb38URZlYOW55DMpPTAcCUAndmT8J8m4iq 9MKPh8xLwn5WRVvQjjTEqMANKGCLfux2HwoUjnsKQoLzYw/gyR5KBThScGv3kJHWGsvZJbbWNmkG ILwk0utCOG7n+lVrtta3zAwTRoWVzYfbCr3+oX4Gh4cYmx5ndHqciZlJRsZHuHJlkoGhBFpDqwX7 Jcjla+wWGhRLDbIHdQqHjehm/pEAQvjdJtGNELbm3TVLhv2DIru7B7yngg5GIFN16EWuvus4gEQL U4xkWH9ORgCJ8QCQkkAcP28Rs1wdZgc0EciE5KfRtgCiDRtRHP8kUBCio0CrUxb8dFCQjgPOKYj2 BI4zCQACEFqxUO6h+NSX+eVfGeP6PKzfz7F69x7rC8ssv3+X7Y1tarUm2k0gvBTS8ZAPoIJpNppk M9tsb2yZZpygrQXgJZNMz00zeX6GydlJJs/N8PTsNIPPj6M0NBqQ3auzX6yznatwUGywV6wZ19ce 9Ie97ZQ2wBQaNdpo9YUVgtpEQxHRDf/gYSjDJUcZgVrNpnnfztpShPLg4cU+eX9amDZkqbXpy3Da r8csu/3HWrQUMsZCZA0UwvJ/wnBeO4NwgqeghanwtHtQNsjzIFAAUz15VBniSR5nEgC0NOq1Xa6m WKrxB3+xwTeHkrz2zAA/8bnXeeVzr5tWWgUHO2XW7txl7c49NpdXWLu/SrXWQDsewk0YsZAYR3TI ayeRx67u9voWW2sZW3Ov0EqRSCaYuTDH2PQk0xfmmLl4nksvTOElPRotze5eje2DErm9KjsHNfPd j7BsUApee26Wvp4kt29+wAfv3EIFCikFbrob6TgILwGOg0wmjdW5DkKFbLrWvHSoFhScUgjdHkIK tDRBvdBLfqg2oIibMUT+uog97UABAUITRAq/px2VOAEUzA7Nbtrf+2FAQUqJcJyPzBrgTAJApVwt ow03nzD3PQeHLf7D3+zRk/Z4+UoP12e7EBJ6Bnp5+rWP8/TrH4/WsoXdQzYXl9hcXGL97j0yS6uU yxW0TBhvQZg02dHb3oCDc8yLyCxvsL64xnf+8lsoZWSyhsZHmb1wjsmL55mYneHqi3NIN0Gt1mJ7 /5CtbJncfoVCudY21tg+pYRbi7s8f2mYn/3ZT/BPfv1zZFY2+ODmHVbuLrH4wQL5/bzNo5u1uvQS RgHJ9RDJLoTjIDwPnCTSc9GOa1SC8K33oImblrCluQBBpA34sBiASTeavgGi89C06w2ij2sTMFTh wl7EHQVxxGk4yUZjQBJme+y/x0EhXHJ0goKUAr/ZyP7mb/5m8+SzerLGmQSARqOW1fBMnIReYICg 3vT56/dLvHGnwjNzXVyb7WK4R9JqalvcJ0j39XD5Y89z+ZXnTXWAhMJ+ha37S2wvL7Oztsba3QXy BwWEkyCQCYTrnmoMQgqcCBXMT1I6KPD+Xp53v30DHZjZd2JmkvG5GSYunOfihXlefuYijaZid/+Q ndwh2f0yhXLNsPJIOKw1+eubm3z9nQ3QitmJfi6ev8xPfewVfm12gJSEzMoWy3eX2FjdYHMlw+ba FpVC0dYDmKCeFCLG2CtMJaGXtPUALiLVBZ4XGbO5StaY5ANKgaW2acDOSHy43GmXGbWRQEgZdfGF VZxmCx0tEdqhwqMridNBIYoXdHxGc9xTkDiu2zxlZ0/cOJMAoISh8x5OceLa15Hmx7+1XuO9tQbD fR5PzyW4MJbEMY1zRiikFc5UgkRXN+eff475l56LbuZKqc7O8jLbi4vsrq6ytbrO/m4OhUvgeCAc hHROniFtXb1ZSpifaX93n73sHu+/eQOtAhLJBNMXL1hQmOfyCzNoJ0E2Vya3V2Ln4JBKtWGJMiGz U2Rj26b+Ap+E5zA52svM1BCXfvI8n/qlQabGe6AFa8vrbK5ukM1k2VzJkN3YJp/PowMQsokUdQsK 4A6NIiwACBsyV6GLrh/gAWhT8INZ0kcEomEArs3HZ41f2J5/oTqMtb3Wj7kE8Sn+ETyF9vd2vtMJ CpZI5nHt+fgexpkEAAITIAvbgcHcGMLxENZYECC1xpFQrgW8cafOt+43mR32mB/3mBkyJBRBoFFC YRvtDI2XNsUqbiLB9LXrTD11PcqH16sNdpYWya2vsrtqpMUOdnK0tCAQHsjTQSGSE3Mk4KKBjYUl 1u8totRfgICx6UkmLpxn/Px5Lj8/T80X5A7KRilo/5BAaFxXIqSL1orN7TyZzT0CFaD9gCAISKUc JscGmBofZOLpZ3nupz/JxPgg/X0e+9ki2xtb7GWNlNhuZpudcoOKIkoDAra3gIc2AwkBMgYSpqU3 fExH8NMEK9vFOR1RAIEtCjr2JdGPbEN6J4NCx3edDgqqI57w5I8zCQAaEQW3zBAErQYLv/ff03fu adLTF+iZvkR6bAZC91QohIDNfEDmQCEFTA5Kzo06zA55OFYXIAj55JWJMRiK6lBFyBjDxOWrTF6+ hpICKSTNRp2DzQzZ+/fY29ggt7FBbjOLrwW+dFEYUDipC9UoCmHq9YH9nRx72zu8/41vIQQMjo8z NjfHxIV5rn3sHIVyk53dAvlCiUZdGaCLovvgugIdKDJbOdbWs4bgxPcNfbnSDPSnGR8dYGx8kOmn r/PSZ17nne/c5s03bxqvxTQDWEowHlgHIMM6AHTMA7AzvibKxbd/t84HxmZ1x/tHQQEdywZEF+0E UDjyHeaj4hgoYKsFPyrjTAKAoavufEUjaNbq5JdvUVy5ZVzbdA+9c1fpPfcUvXPXSPQN2inAdM9t 530yez5a1xjtk0wOOswMe/R1ObQCEyCzGiTt79EarQILCm0Q6p+cpn9qjmuYNlwdBOxvrJPPrJFb WSG3sUF+N0et3sKXHgqBEM6x3nTjJTiE5YvFvX3yuT3ufudtJDB6fo7hmTmuzs2hU73k9kvkD4rU 6o2OzIJAGKEOoXGEC9poHNSqdVaWNllcXEf5hmfAESYFGNbKG5OyK3jZ4aR3XnMZ1gEcaTw8cUHe qe4bO9CO7SJQiAX6wg2PZQLiezsGChz511wTpeCj0QhsxpkEACnIFYsl+r0UmgDQdHUl+Gf/6z/j 5rdvsbywxt3v3qZWLlH/4G3273wHgSA9OkXvuesMXHyG3tkrgIvwA7T2OSgH5Ao+N1dqdCUEU4Mu o/0OY/0JEp4kCAIzK1qR0fiIwEBZIk9t5qT+8Ql6xyc59/InQAiCIOAwl2V/dZniVoZcJsPB1jb1 eosWLr44ARTCmn3rmu9tbLK7voH+5jdIdXUxev484zOzJCfHKZTr5AtFmvUHB7iFFDhaIl2jK2hy 8hpHClzpoglTZ+qhMQCBMEsAixdR6o5wCWBDemHcz74WGbAmtv/ja/f2807rPh4ofBRQCNmWPjou wJkEAKVEPvB9epNtZuCGr/nL1V4uvPRZfvFLXUwNQW5zlw9u3GHhgwXuvXuXQnaDUjbD5rf/DC+R oG/+KfrOP8vgledxU92IVgutfZrNgKXtBotbGijTl3IYHXAZG3AZ6UvguZogELZQB44hAoAFAqMD 0IpAoWtgkKnnX2bqhVe4hgAdcHiwz+F2hoP1NQrbmxxsbVOtNWjh0FLCxBRkm0dfCgekQ6vls3Xv Ppk7d5GOpH9igv7JaRLj41TqLUqFQ1TwiE1MNu4hbDLD2K18aAxAhksArJHGXLNoBaAxvQKxmb3D 3Y/W7w+J8h/zCj48KEQxhI/IOJsAgFHnidudAPLlBt+50+StOyW0kIwOJJg6/yqfffWn+LVhCCp1 Fj5YYOXeMvfeu8fawh1yt2/A/wvdE3OMXHuZgSsvkuwbQvgttA5QgaJc8ylUWixkNL4K6O9yGeh1 GBtIMNDtGeUgKyEWaBA20nTsPtNtkRCzEjGmk0j3MnTpKQYuXY9SddXiAYeb6xzuZslvbVLY3qZa rdFSDi1tqLNFmP+33kExu0NhOwta0z00SP/UNKJvmGpTUa9UORmp2sOJpQGV9QweVgeg7RIgSrOF s390zubHCcK1fGTsIjLS0w34w4NCxBN8yj6VFijx0VkDnEkACOmrTKlX5w0SEtMKCaWKT7FS4c56 DaUF6ZTL+OBVLnz6WV79eZgYhHw2z+rCOqv3l9lcWWH9D79OnTQ9F55j6PrH8dJpq5ADCPCEsPTf TdazVZRWOA4MpD0GelwGelz60gnSKYlQ5jYPtEJq8OPU4lHfsom4h/0I2hY4uYk0fReu0Dt/lUlt uuia5SL1/V0Otzco7u5S2t2lWi7RCCQtFQMFoFosUc0X0ELT1T9A1/AYurufestHNU9ZIog2mIQ1 9EI+gBBEhL3/EiVsJbB1Atozr2UBjkXkw3LmaD9h1gbR8brd3YfyFKKHp4GCUlTKtd2TL8CTN84m AFgprpTTieQi0s9rD2nBwAH8QLG532Jz349m4J6uJON915j55DO8/HcdRvogJaGwe0C1UkX1jvD1 d/Pky51akoKQ4VeA0uTLDQ6KDZRShoHXEfT1uPQmXbrTRgugO+3gOlYHAIXWJi2m1BEYUxiNwRAU LGOw9FKkxmdIjc0wpE0GwK+Uqef3aRzkqOxmKe3lqJQqNAOoa2OA1UKJaqGIBLqGhvF6BwgSXQRH xVVEu6ApTNeJI0y9nddAICQ0GnWTRrUZgKiNJzRW3VmOe9RYw9jgMVAg3I146PKhw91/QFwh0BAE fvHEE3oCx9kEAOFo31cMJ9tsMUI6HGYW6Zm+hJNMme1OCPcKzM1hZnRNvaFZzfksb9eN6CcKrQKS MmBkqI/WnVV8t/vRDku0QUErTaHY4EDV21oAKNKepCftRiIhXUmXVNI810JbV9lEy3QQLqnNebRB wQqDKIV2PBLD47hD43RffIoRBKpRo7G/Q1AqUM0XqBzsUy2VqLc01f099N4eTjKB2zcA3b3moJUR MQmpvbW1QPmAJYAUEokg8IOoNboz927MLrAg0F4edLrpnb+O3Sa2eI+AQcfiCsc/3Onun+AtaG30 IMRJ+dgndJxRAAhWSsUiqr+n/ZLrsvFnvw9KkRqfpWfqIt1TF+idu4aTTKGDIH73dO6OGF8/oJH4 WpDdb6B1L7r1va8Z26AAaGi0AhoFPwIFbEpRCkh6DqmUpCvh4LkuqaTEcxySrsRNmJoDLRQ+baVg ZdOVKGWIQbRGa4k3OIbTP4o3q+kNNMpvEhzm0aUitUKeaqlIo3xAo7hHM9VN0N3XzjiAXV5Zd/4U e9EirLEwM6sMo+5HU4DHn9BZKxDbRsQfxuIBsdjCIy0hTogBhC8flipD81evPteqVBYymcz3oKP+ +IwzCQDKF75Ju7XvNq00AyPD7Gb3aG1nqO5sIm7+NQJN9/RFeueu0nfuOj3TFwwzjQrQQQv9UPbf H0zIWIhQB8C2+GpNvelTbygOrFZfOOObclqNA3ieJOkacRDPMdNtMiGtKIiLVoF15SEITMeiNBaD kx5EdfXRNTJFEiBoocpFVKVMs1rGrWqkNtyJIedilH046RykjEmCm2PRIuzM45gWSxwbTrquZilw gqdw/Js7Hp66hDjqKUhzzO9+8MFL3en+d2si0fy7v/DaG3/0h1/9zCmn+NiPMwkAlua+oxTYbzb5 p7/1z0nrJvffv8vtG7dZvLPExvIGhfVFiuuLyDf+BC+VpvfcVQYuPEPv/DN43f3ooIHyA+MlPAYj NACJQFsa3ZC/sNFoUa8FJnevFSoIwAqDGMGPABFolBUGQWscYUhUQlEQbfP+QgemhDgIUMpjWHgM y5Zx4+0xiGOzdOw4tY6sTyuFcpyI4y9sBXxQVN588qhPpjvW8HaBd2wbYvsMt4jMPwYK8bCih0Op VCLd04sAfvU//5WEajU+/Ud/+NWH/yiP6TiTAKCQEAiGEu3bx5Hwe//fFp7nMDt2nmtffJrP/5rD SBoWbi+yeHeZe+/eZeXeMju33yH3wTuAomfiHINXXqDv/NOkx89ZMLDG8yNOGCuNJQQRZvGqdZsQ JPwbgYX5R1phEBkJg2DlvnW7vBltg49GiszM3xpXYMVD2oQgpuf/5ONLJr3IqDsi/x0xgJgxWiR4 GCgcX96fsHzgOHAc/WwUjhQCL+GxtrrKf/iTP6G3p5tf/Yf/KROTE3z7W2+efHJPyDiTAIBsoOOs NXa4ErRSrGdrrO800LfMunlyeIqpZ8/zS5/9PJNDUDkos/jBAqv311m+u8D62/+RlT//A5J9Awxf +zhDV14gOXEOofwj0tk/vKE0vHB1jNnJQd554x3u3PwALRxDBuK6uKkU0kuCdIxAiAqgQxhExy3x kUZCgJSuAQQ7k8sHEKIO9vW1p+pQfrHD/6fzwzr650ODQiwp0N7HI3gKQghc1+WNN97gzTff5PLl S/zy3/slHCloNVungtuTMs4mACjHbzYax/oBwhHWAYQdfLlik51CixvLEoGkJ51grP95Ln7mJT7x SzDaD41SlY2lDdZX1tlc+jbFtTfoOvcMrYE5ApE41Qh+UEMKuL24S9KVfPFnPsF/9V/+JyzfXuL9 t99nZWGV++98m2arhZQyEgdxkylIJJGJFCKRAMdDJFJI6R4XBokH3KLrJiI1L6U1QqiYpNfxobVZ SmhtgoDOA2Zt+w0df9qbPRoohNvElYd0x2l0Lh+ElPhBwJ/+2Z+yurbG5/7OZ/nkJ16j0WzhBwpP OrR8/4muCjqTAKA8d7lSq+IIjRt1wJjpxzDkGeIH84r5X3RjCKjUA1bqAUtbynaGKXqSMNo7y8j1 C7zyMvQlNf29HvX8Lm+vSe5n/R86CAQK3nx/izduZhBac26yl0tPvcSXv/QF5qa6KOTKLN9b4f6t +6zdX2Pl/hKtRisiA5Ey1AAwzUUilUS4hjEIL2Elhh2DlpbT34nSgET1CY9y3ib9F0u5mVc7Iv0n B/XEsYdat9N3naAQbnR0SXAUFASe67C3l+Nrf/41At/nV//hP2B6Zop6o2mWUsJ0lDbqteVHOL3H dpxJAHAbppAnITUJV9KTcknLFgnp051y6O3ykNKPmHBS3Wl6+3vNDe5CtQkoaLSg0VKUDn3qrYBK 3ef91SaVmk/g6yi47eggqgT8YQ9pW461Uqxly6xuFfjTv3wf1QoYH+tlbmKAyec+xis/83kunOuj vF9nfWmN1XsrbKxtGpag1YwBAysMEolyhN8xOALpHsNVINwo6PgoI9yHUiIMUzwk0n8KKJwU0Dvm KYSfiH93Z84xdPnv3b/L2995i/Hxcb74hc+TSiZpNPxYb4NGCRsWeYLHmQQAAKGgGUh+fWyDv3nz Jm+9t8rKdg6NMAUtUlrjaa8LvVSS3r4eBkaGSXalGJ6cYGRylOHxES5NjDEwPEi6x1TCHdagUNYU DhsclHxKlSZ7hRalWtCuUguP5Yd0zoYdWCBdB6QgX6iwv1fknXeXCVQAQUB3T4q5ySEmJyd55Zln mZkeZWQowe7WPtmNLJmlDTbWMmyuZDjY3aNZb5DwfSucFrYDh4b8aGcWshNrHWcFbD/8nkGhYzzc UzB8g5o3/+YbbGQyPPfss7z26qsoFUSBUK3bZyXUEx4A4KwCgGuaVVqB4uMXRviJS5/HlZJKvcEH y1luLW9ya3GbW0tb5A9ruFLiSEkQKIqFMsV8yUSub942DTkh/bYD3X19jE2OM3V+mqlz00xMT/Dy /AwDI0NICbUa7BYCtvfq7BXq7FtdABVoW4jywwMEQdtDcDTgCJqNJguLW9xdWCPwA5SvcKRmaLCX sdE+pqZHeOWpa0zPjDE8lKZRbfD//OGf8zffuoUjLIcA2IalR5sctTYVjI6R4Ok04I58fPvA/3Y8 hTAjIJCupFo55MY7b1GtVvk7n/kMM7OztFqmhNvEg4QlLDGfNk1BP9pMz/c7ziQANMAEqtAEvgId UNNmhnx6fpJnLkzhfN7MCDsHJd5b2OTe+i7vLm6ysL6DH4DjmJvd6Nq3992sN1hfWmVtYcUCQ4BS mnRXF+OzE5y7epHZC3NMzk3x3MfmcD1o+bCXD8geVMlkK+QKdfYKVSI2mx/yRCMktt8fcMzyoVgs c7Bf4PYHK+D7+Eqh/BaDA7040lbxCYGURhgEPpxpaGHnYh1bu9N2038woGB+c8eVbG9tcu/uLbq7 e/jUJz9FOp2m2Wza8wq/K5adQNsW5Sd7nEkA0HVVqFarhC1mykaKTN2Lil4TQtPdleAnn7vI689f xHGMi3hvY4eFtV3eW9xiMZNjdfsA1zGdcOHyoU2vYy6xQrO1tklmJYNWyirrKEYmRzh/+SJT87PM XZrn9WfPkUgnabUgm6uwvVdha++Q3f0a9UaA9eIfaSilaQXKLmMsLwDKWoABvqgmQHUWvZw0jgmD uIJatRZ+2GQBHJP2DD4kcYZWCh3nS48ZqTgCCsAJlOF8aFAIlYGX737Abm6LmZk5nnv2OUDbtKhV CD62fDBfpoR4WIf0Yz/OJAAkEu6u7/uEarBmbacjrntTPdt+TWtDiqGb5oefGxvi/MQQX3z1OlII SrU6Cxs53l/cZDGT487qLuVqA8cRkYttugolJkjuRBe+Uqzw/ls3effNG9FNNzA8wIXrV5iYm+X8 9cs8/coMWjrkCzW2c4ds5w7ZOahQqbaitNvRoRS8+sIMX/rJi9y/dZ/vvvld3n/nDnv7RWQiidfV hZvsQiQSyEQXIpFEOi464RmmH9U2PmNEcSW+k4dwZCwGoI7FOk4b5hqLtqBofJ90Vugd/eCjgULn h8M4T6vZYGX5HrVahevXn2NqcprAFnBFoiVCdO6s3T4YdTw+yeNMAgCE6044zfjDnHCkGx+Bgcld +4GOPEJHCp46N8b1cxM4VkFyc7fAYmaX95ayrGzts7CZAw1OGFyMGHpAOjJi+gWolqu8960b3Hzj bZQKcF2H6flzTJyfY+7qRT52+TypvjmKxRrrO0V29w7JHVRoNP3I8ZAS3ryZ4VvfXefSzADXX/sk P/ePfhnRbLB6f4XbN26xvrjG8v0FmrW6DXraCL+XwEl1IbyUUQ9yEwjPBS+B9IxgasTPF0vdOVpE ir1Ci0eeHE0tQDsIGHfV2ytuIhv88KDQ/oTxhASlUpHtzRUc6fDCCy/T3dVN4LdAiCjK3xEtDJcA FhCE0GgFzcD/cTPQkzcaUQS4bdhxgz/N+DURL5z5f1Rjbz7Wls8a7OvilevzvPbMPEIImn7AWvaA D1ayLGX2uJ/JkT0o4zjSMunEOulOAIWt1Qyby+t852t/TRAEjE6OMXPlMlOXzvPixYukXpxlL18h myuzvVukUDT3pRCSxY0i91b3+eqf3Sbpai7Pj3Hu+Y/z8pe+wIVzvRR3D1m+v8ra4jKr91fZWFqn uH+AiNcDxNh+SCQQjmcEQlwX0ZW2gYOwHdgEAE+k6T5p2JxdvAS4sy04DNaFZvg9goItVMrvZSnl d+nvH2Bubh7HcfBDGvNjh3wkaGAfaC3RWuE3muuPdpKP5zijAAD8bRi/7vQejr2GT8tvvz87OsDc WD/y1WsgBIfVBguZPe6u7bCY2WNxc596s9X2EmJ19EKaNbzEwcWjlC9y61tv8d4bb6IDRe9AL1OX LjB77RovX70AyTQ7uSI7e4fs7x8aIRHXrP3vLmxz524G32oB9PemmJka4Nz8Nb7w2mvMzoziodle 3zSKQcsZ1pfX2FzZpFI5RDRbSGGMXQDu0Ah095o0oBAkPc8A4yP/EqbzMNyfue5HewhOCerxaKAg pECpgOL+No1mg5GxSUZHJ9FaEQQqivITi/LH3f1oWYBGSgelNG+//WYrkUr9n494mo/lOJMA0ABs 9k6cZvxtAIgZNUeMX+kocn3c+DsBBsAPgvZrQuM6kqfmxnj63JgNrmm294rc28ixtHnA8tY+G7sF DOOukeiKGH+FQDqOESb1oF5vsvjeByzcvAVKMTA2yuTFecbn57n2sXkO64rtbIGd/SKB71uQAS2h Vm9wd3GLO3c3CFSAagW4nmB8pI+5uXFmXniBV770WaamhiBQBhhWMuxkttlc3mCn3KAc0PYSlDIa gh8mBmBpv2Mr7Mj+2jgQq8i02xzb1wmgIIWk1axTL++D0ExOzZJKpVHKByzQ6DAEGmMROLL+10Ij pMvh4SFvf+etXYn7K7/92//iPz7aWT6e40wCQGZpaXOwe1BrrcUPy/g7gopoUEZAs/2aj0Aw2NvN a0+n+cmn55ESGs0WmVyJO6s7rOeK3F3boVhtRAYcBhgBm30wbno5X6T4nRvc+bbRAxiemWLiwgWe u3yVIDHJzl6Bg70SlWrNchYKCIN4UqB1wM5ugWz2gMAP0KpF4Ad4nsv4WD9TU8OMz81z/bVXuHnj Nm+8cRNHuG1OQEty+mhD2yxAO31o4iMiuuaRUdI2+yhF+CBQEIJWs0zQrJJIJuntG0Y6jp31Q3Vi QYwAoH0EOvYYjSs9dnd39Pvv3bjlyoGf/+3/7X96osuA4YwCwO/+7u/W/+ffelUbTr0frPFH28WN X58ACNoUJ+noWEJuAc3USD/TI31RGq5cNVmH1ewBS9kD1rYLNP0gWjaY7Qw4hIBwsL3D3uY27//V N0j3djN2bp6ZixdwZybJl+rs5Qo0G42O62SaogR8sJkiAAAgAElEQVTClaBdw0eoFbvZA7a3ciZo pkyZs2PZVKUt5NFH2TweMKIlVEf0LnwjvAqdoEDHOyeBglH1lUEVqZu4qW5SXb3m91UhYsSNnigu FL0W0yRwXZe7d+/o5aWFPyRo/urv//7/Xnmkk3vMx5kEAAAVmMjzoxu/3e4HZPztGMNJ8QeTr9e+ Sct5Eq6fG+fp+bGozn8nX2Fxa4/V7QIr2QN28ocmmBilIc2SAcehUW+yfucOa7dv4zgOQ7MzjM1f wBsbplBpUMqX8f0HkJvY/QrXMdRf0TraBgEFaKE6DfpBQwhDCaZPEOcIvzB67dFAwZEBSWmi+srt w3ETKNuwdFyDIDR0TccRaFv3gOSdt99q7eX2/4d6pfQ/fvWrX20LF7d38ESOMwcAX/nKV54uFov/ dTLVI7UOTjD+zqBfZPzqiFHzt2P87aBiZ0CyA5jC9bBuZxy0DtABGOZPzUBvFx+/OsOrV2fREhrN gPXdAivbB2zkSixvH1CpN2Negkn7aWB/I8Pe2joI6J8YZ3B6FjE8RKnaonb46BOdEAZsCML19MPt QghBuXwIyHYMICy6C7cJH3U4FSeDAkDKg3RCo7RLw3cBQ3kmxFGDD3+XTm9AoBFIpCNoNuvcePut UqD4x7/3b/7lH5x2Gg890ccUJM4MAHzlK1+5Vq1W/7tUKvXl119/3e0fGOLm+wsdVXXGsB5f4z/5 O+wxBwFoaOrQ8ASzo32cH+9Ha+OiFw5rLG/vs7lXZjl7wNb+IYEyhKJSmNbf0k6OYnYXAfRNTNA/ Ok6Q7qNSqxGcpgdgh3AMLbgSysh9PeItr6zgnrA1BG3PIWbkDwAF47obMBnodUi60GpJak1b6d9x INbQ7XUXJ6z/te0CzRfyvHvjnYWE6/3S//E7v3Wb44b+YYz6+/nsD2ycGQBYz5bf+OUv/+zQ+dkp wNx0z16/SKFQIl8oU67UDJGlxpIG/nCMP6x+O3F/8SXBke+IwCMCiaPgYVxwP1B2O5+kK3lqdtxk HbgIAjJ7JTb3DlnfKZDZL7JXCusHoJDNUshmcV1J99AoyZFRmsKj1WhyEpuKg2UAUoB89CCg1kb5 R8cUQuNBv86agOOgAIbsdHwwhSsFxcMGtYYVf4liCUfW/B1GH+7HCJW4jsfa6jL379z682RC/oPf +Z1/uX/KoX8/M/9Jn/2hg8KZAQCcxNBytsbS1j2G+7sZ6Esx1NfN/Plp5u0m5cMKB/tF9otlioVD IrIXaeXEiRvySUsGHhPjP/n7lV06ENjthWCkr5uR3m5evDCGkJJ6s0Vmt8RWvsT6TolsvkKp3qSZ zUJ2m2Q6TXJ4BJ3upYUk1MrW9jpJx0Gho6KmR7EQIUzQLjzeKABPaLbxfXWCglKCof4kk8NplFJk c4c0W0fc/WOsRKHbH+7v/2/v3aPkOM770F9Vdff09Mzse/aFBR8ACJIgKZGUSAriAyRIRaKkyInz sGw5jo/j4yiJ7XPto+v4+J5c88TXkp3Iuk58mXvlHMWSrFi2KOsVUXxIJCG+SZEACQIkQYB47Xux u7Pznn5V3T+6q6emd2Z2drEgQGK+c3qnu7qmp3emv9/3+7766quABchlzA4eeJmfWTrz5zPTp/6P ffv2uU3+jXNl+d/xuMLFAwAIqC4HQa5QQa5YwYnJZVAGZFIWBnosDPWncOml47g0fEelamNhcRlL ywXkV4qwHRf1B5CsUv4GZT0b5W92vRaf0anyR2Ai+wHRasQQAh5CcIDARLYHlwz3YveVwcSj5WIV s7kSTp8pYD5XxNTkJASARE8P0NMPqukACdYBpGGyDJfX7kB4WKG4HoCL6UzI8RtAIVzu7NKxHmT7 LBTKNUzPFSBCV0L+5tG14iygIWNIgBIG3/fx8s9esl3H/tff+Op//0abW34nLP874jJcNABAowSP QAgAyoKWcqWGSqWGqbklgACZlIm+TBL9mRQu2zqGy7aOAQiSbRYXczizlMdyLo9ypRaWCZM+w/lR /lVsRPmslsovWcwqZhPcTz1HQcBMaNg+OoDto/3RXIPp5SLmV0qYWixiocCxSHQIkUKPxkF4nSF1 IsFwLJFribRIIVaG6wAYBsM124ZhJXTMLOYxM5eLahEEvQXUiP6q/H4IyKFKjWgoFAt47ZWXJ7nw f/F/fu0rL6BzRW0lm235yRrnNyQXDQDUnGoFlFhyYVcJB9JNrBd8IKjWHFSrDuYXCgDlMA0DfT0W ejMWJiZGMDExAgDwPA9zC0tYzhUxv7CMYqkSDJFJstqB8jcoZivlb3Avzlb5Y64FlM8IHy3VzREA wAFPNGYxDmUsDGUsXHNJFhQE1bBQJhHhikVCze1vI9GQoSwLLNVUIfykDgqcc4wM9uKq7cOgIHjj 2AxW8hUwFk7dDRU9msQbWv7GWwnaKBXQmI6Z6SkcfeuNpz3b+8wDD3x9pv3drinn2vJvqptw0QCA 57hTFHQnR9wlDBBAmYcTbGEyDSEUrudjMVfEmVwBFASmqSGTspBJmdgyNoKJ8RHgGsD3fSwurWB+ IYf5hWWs5IuQHjIl9N2p/A33ErIbmcUYXg8isLVaWC9BCICuo1qO4ICcAai01n+QkLVwDly7YxyX TQyiWKph/+vH4bhBApSIrLtAg3+vRgqV84QEJcyPvHlYzM/N/PXhgy/924MHDzrYfMvf7v1na/nP mhVcNAAAOeYbHkSFXqC8qo1o/HUIEE7OIXBdH7mVInIrJQBnkEhoSFtJpFNJDA0NYGR4MHrf3MIy FhdzmJlfwpnFlUBZCA0/T3URWit/43BhC+UXoqnrsWHlj9piyq98llR+0XC94DP5OqbKcxFMBmJM wm9oo0N95QJIaAy33rQTA30pnJ5ewMHDJ0FZOHVXKMof9+/lbrhDECQr+dzHwZdftGtO6fe/+Y3/ 8d/UXk2kE8vf8EkdvL+dZV/rfKtz6waCiwYACMJFbFUdl5a/idKrr1EfNbIdFgulhMD3OArFMgph 0ozGKDKpJJKWieGhPowOD+DaXdsBALmVAqZmzmBufglnlnJwXT9aH4/gQlN+FaA6V34hOteY4B1h DEDII/n5AeUfy/bhtpuvgsYoXn71KE6cmoeus+jzVkX5I0BQz4evjKFSKePwwVeWHLfy6W/9zdef kr9u01vrrE3KRvz+jVr+9bS3lIsGAJr/xnWFbmAEUWCgsU/8tRl4SItUKldRLlWxSJbBGEXSNGEl TfRk0rhuVw+uCwFhJV/G7PwZTE0v4MxiDpWqHXivNCwRukHlFw3vbaf8MdBZ1da58q8n8Bf8D+H3 JoJEIBGLAdieh5uu24YPXHM5KtUaHt13AIViCUxjYZ9Q0eU9oq7o6pUCF4VCoxrOzM/jrTcPvWpX yr/wgx88cHKNW2wGCmsBhbq/mZb/bNubykUDAJ7v1ITwQRFMu23QcQB16qjEASCDg/V2lQmobdKG N/yaNLTrAqjWarDtGnIreVAKmIkEdF1HOpXE1Tsvw9U7LwMAlMtVnJ6ax9yZHKam51EuV8PAlrLq 7Tus/HEWUgeDjSs/FwJD/b2AAHwe1BCkodsgAGiM4Oc/cgu2jg/i5Ok5PPHMK/B9Ecw2lPcZ/WAy 5Fe3/EJxCwiloBrD28eOiNnZyW+/feTgvztw4EARzRVU3sJ6JO4ttgIEte96Lf9G2tf8Py4aAKjZ tdPCx/uUx6YBARpYflPcJ6v6yRbScCL2NJA4uwAggFqthmq1hpV8HoQQJIwEDNOAlUzi6isvw9VX Xhbcd83B5MwCpqfPYHruDJaWC0F9QUKV0cdWVH0t2r8O5Q+vIfd5rP+6RQQTiuKPqOcHlP8ff3Q3 LNPA8/tfx/Mvvw5D18PcfUXR5Q3FlD+i+6QOyYde3e+XKoU/+vY3v/anHdzdZvr8cQUVTfo0u9Zm AMGabOCiAQAgqDhLqaSeRFHmJr9XjP7HDD8aTqh9gIiCNjgPDe+t/x4kLIRRs21UazXkVgqAEDAT CSTMBJKmiSu2TeCKbRMAgpGGE6fmML+whBOnZrG4nEeQR0/DjMVOlT/u83ei/GJzlL/hW6jrheP6 uOX6nfjI7TfAsV088MN9mJ5ZhK5rQb/YpJ16Oq/SJqQDEZT/cuwqDh88kHe58+vf+duvPxj/JVre VmeyEZ+/Vf9zaflbsoGLCAAEZLGaWLA/kDXof71fI/2vA0OsY9PrkdW/mlTIcCckrajaNqo1G8tY gRBAQk8gmUwgnbKwY9sW7Ni2Bbd+6H3gnGN2bgknT8/i1NQ8ZmYXIcfDaXhza9H+xrhCLPgY8/tX VVE+SxEI3AHP5/jMP74LV+3YitNT8/i77z2GWs0GY6xO55sW7Whh+RlFPr+MNw8fOlIr1X7xoYf+ 9s0Ob+lsrP9aSh7vIxXzbIFgwyBw0QCADhIl+gAInqdV9J/UD1RRLHojMCgdJf1X+7ZyJ4DIp441 1f8I+YgHF7AdB7ZjYzlXABcCiYQOK2kibVnYMp7FlvEsbg2vM7+wjJOn5nD81DROTc2H8+BlSXLS Rvnxjio/ECh/dqAP//SjtyA72IdnXjyIHzz8DDRdC2odIFaXv4H2QykDHn5bJFiwZfrU25g6deLR E/m5X3v18cdzaP1LNFx5HdKp9V+LhsunZD1AsGltFw0AcI06M1OTGBkbh2EkQARHvWylQsmVvUZa 3+SiCl4013Wyql/8N1nNBGKidAg9F1ACOI4L23GRy+XhC4GEYcBKmsikUxjO9mNkeAC33LQLALC4 tILjJ2ZxYnIGp07PoWa7wfRfEgTVWtP+c6n8Ap7HYekU//TeWwBC8dffehivvHYUZkKvf1dCXb1v NQsgai2/EHHfevOQWFmc+y/fe+rH/ycWF9tUNmmQzfT7m/Xr5HynQLCetra/1kUDAD1a6ndfevGF /SDe3b09fbuzw8Pm8Og4RkdGoetGUDACgPz+W439y/0ovBRRAuXDpN6riq+4Dq3o/6rzkHS89f8l wjJcjuvBdovIrRThCwFdY+hJp2AmTfT19+DmwT7c/MGrAQCFYgnHjs/g2PFJTE6dwUq+FCbVBHUB Wit/+3vpVDgX8LnAJ/fciImRPpw5k8P9X/l7LC4XYOha5KrUFR4IvvEWgBAWOPFcB0fefLVcqVZ/ 54ff+ebfhG+kq25A+frW2R7cSGd9O7H8a7kRaynzmgrepE/Dcaeo956S++67P11j5Q8Tz9ujUbYn lUrdNDwyZoyNj2N4dAQa0xGVC0NYqkrSeyIVW9kP/e2I/ivt9WNZfVa1tsH9NETYUafiDQG86Lx6 3AgQDf572M7BIXwBXdORTifRm7KQTlswdD36PsrlKo4en8bk9DzePHoay8uFoMw/CETIFNZr+QmA XK2G5Wqtod3zOIb6M/i//rdfwWB/Bq8dPoa//Pr3AYhwnUWifL8k9r2TVeeDtf00lEtFHHnj4GSp Wvrlx3/0nf2d32nHIlrst+u3nvPN2uNsoF2/DfW5KAEgLvfdf39aLJQ/zAnfwwz20Z507w0joyN0 fMsEstnhINdcAAhnyMk14ilioBA9nPV9FRgiWy8UJYaquGGPhtfG85ElDt7Y5H2NABBv5wC474Mx it50Gpl0Cn09KSQUQHBcD28dO41Tk/M4enwSk1PzoJRF6xU0jpo0l2YAULUd3HrjLvzv/+rnAQA/ eOhJfP/hp2AlE00Uvonyo/E8pQRM07A4P4O3jx55YXll6pd+9vTTZ9a4rfAXaCmdwtxagLCRzziX INAFgE7lS1/60kDR8W/nrr+HMba3r29g19joiB4AQhbB11ZnCAQiXKNvtdKTOADElTKi2jHr30C5 YwE6NFH0OCtocj7aR32fcw4ugnUH0ikLfT0W+nt6kEwY0fchhMCxEzN4+/gUjrx9GlPTC4CQdQWV tQoUUQFACMBxHPybz3wCH/nwDQCAz3/pqzhxehYJQ4++IxlEpQ1Wvq7wQfyjrvyUEJw6cVQsLsx/ df+Lj//+/Px8bdWNbI6I2Ot6+6zX1WjWHp9dcbag0AWATuXzn/98llP9w77v32vq5t7egd5tW8bH 2ejoOIaGhoIEFSGC1UbAobKCCBjQqICraTvQ3Lp3Qv+bAMQarGCV+xHu83CpM0YIMpkUMqkkBnvS SKeshu/k1NQ8jr09hWPHp/D2yWlwHi5eIlkCAgCYL5WRSZq477c/g0vHh3Fqcg5f+n+/Cdt2wiXH VLDshAUExTohOI4dOeQUS5Xfe+QH3/grnN2zvA7npqH/Wkq3ESVt994NWfl2x10A2ID82Z/dv9Xx q7dxn+/VE/qeoYGhHcMjw+SySy9Bf/8gwHmQ2kqCKjdESFuPBvrfqKCN9L+BFWB9ir4aIFSgacIK Gt6n3h+CBT5E4PJkUhb6Min096bRl041fCdnzuTw+tHTOHZsEicmZ2FXXSxVy5i4dBz/8bc/AwB4 4umX8dffegiJRCLSVhlIbab8KqsK2igYo7CrFRw7cnChUi7++uOPfv+pzftlG2Qtih8/1065NwoE 5xoEugCwGfKnX/nKOBZzd/s++Zhu4PaxLVsmxkdGyPiWLRgYGAD3fQjhw/cBDj9aenvD9L/BajdX epXmt6L/q+IQcfYhP1MFl3BUQHCOTNrCQG8ag70Z9PekG76T5eUVnJpZxA3X7gAA/MVfPoBXDh1F IqEjLmrMpJXlp5SCMYbc0gJOnThy4MzCzK/uf+Gp+MKc632ez4Xlb3XuXIFAFwAuJLnvvvtoT0/P Np/Su4Tj3CmEuH3r1q0TExMTZHx8Aul0Cj7nweKZ3A+q4cYUvTn9P7vg33pYQQO4oN39AX5QHQRc AGkrgYHeDLL9vRjqSwMgWFzK40//618jXyhD01qPyDUGUckq5deYhqnTx8SZhenvHXjh+d9cXJw8 V/5+8M82vrbrt16FP1sLDrSPB6zrel0AOMcihCBf+MIXrrAs6x/YjnN30jRvGxkdHRwZGSVbtm5B 0rTCcXE/XGqcN1p/tGMCzRVdPY8W7Z3Qf6isIPb50bUhGj6HAxB+EEfoyVj42v/4ezBdqy993kJW swCp/AwAx8ljr3ul0soXfvzgt/8rGhWgSQbGmtKJkjU7vx7L3669ncJ2cn/rYQFdALiQ5L777qOW Zb2PEHKn5+OOZDKxJ5sdHpjYOoGJiQnougGfc/jhSsKcx9lAc/rfjuar7a2Cf52wi6bnY/cB5X2U UTzwzQeh6Z3lm8VZAGMMrmPjxLFDK+VC+Td++vh3HwcawwebLKrStgOF9Vr+TtvWc8zbnOsYELoA cJ7lvvvuo4lM5v3E9+8FF3dY6fSHs9nhzOj4OMbHRqEbZuAq+EEwTnC+IfrfjhXErb96nbXYxypA UdrZegEAyjCgxlDK53Dy+OtHlxcXfvWVnz15tM3b2h3HZT3Wfy1AaKfIcaU7Gyve7HijbkAXAC5k +a3f+q3E1q3bPugL5w4uxF1WMv2hkdGRzJaJCWRHRqFpGgQPFuPkvh88BfGgHUJFX8taI0bn2wT/ oLTH6b96/mwAAAhqBDDGsDA7KWZmTz96/PAbvzk19Xo+PB1lVmNzn91OLX+zSqfNFL7ZtdY6bnWN Vue6AHAxyH333WeyVOpDxPXvFiB3ZXoyN42MjBijo1uQHR4G0zT4vhe4C74AD4cYVln3Jta60Zq3 su7rcQ8a29fNAMJ8gMmTb/Fifvn/eeLRv/98cNUWc62Ut65xHN5d2+Nm59opaTMFbKZo6wGB9QBA u/d2fM0uALzL5HOf+1xqcGRkt+N4dwJibzrTe+PY2FhifMsWDAxlgyKlPg/jB364HFij9QeaKG5D 8K85ADSwgjXoPyEBAHzrmw9C09YGAEopXMfB8bcPV6rF3O8+s+9H30NzRSZYGxDWI51Y/1aWvxOr HgeKtY7XAwhdALjY5f77708v50t7PZ/fSQn29vQN7BodHdGHR0YxNDQIQRh87gEC8H2BYAmuQM4l /RdCgDGGb//dGgAQDvNVSgUcP/baydzCmV8/eOCpw2oPrE371/Mcn43lb6Xw67XyG1HkZue6ANAV AIoCfO4P/zCb0ZO7Pe7tZYTt6R8cuHZkbJxlR0bQ3z8YxA+EAPc5AA65IPJm0X8VUNYCAJndt7gw LWamjj+/MHnss0eOHFpQ/qdmln6z4gBrWf5myi3b4xH4tZS6Wf92x83248edMocuAFwE0mosnNz3 7/9kS4Gs3HNmbuEr267YiZSVwfDYKEbHxjA4lIXgAn4YTORCAMoow0bpv2xvBwAywWfy5FGxdGb+ qy8/9/AfVqtVuRJvXPHXUvpOXIJ2FH8ty9+MtseVbD1KvlbftfabXSN+z51cowsA7xFpCQAAMDQ0 ZF111VWFP/iDP0A6nUKl5mBqahqTMzNI9fRjbHQcI+Nj6O3tC7IUfQ4fwbThptZf7rcI/gkR1O5l jOGBJgBAKYXnezh17JBdyBf/8Nl93/+aPNXk/pspN42dX6+I2BY/18y6rgUCzY7jn9fJubMFgHWB wUVTEeg9LB1lwlFK4fs+NE3H1VdehmuuvgrVagXz8ws49MabePPwq+AAsiNjyGZHMTI2BiudDkcX /CjDT7KCZhJ/wkkT9k4pRbVaxsljh+ZzhdxvHHjmxy/KW4zdfzMwUBV/1b/Y6n/HauWVwCGU80I5 FxZcX9UWz0AU6zi+IKULABeJ1Kcji6AGAOfwPB+WZWHr+BiI7+H05CQOPP80uAB6+/rQO5hFdnQc g4NZjI6Nw0xaEJA5CM0X/xP1KCFA6n3kJJ+V3AImjx957dTs1L889fpLs+HptZSfoTUz6IQBSHCI W3753iDfuK7g8rrNgCHe54JX8nbSBYD3rhAA6Lv+VzNi2PxFxzmGVYtkC1EHhFBxCQRKhTwSOkPS 0FCgAuXlBbx24HlQPYnhkVGMjo5hKDsCw7IgPA5f+IBfdw/kh0dLsYfFEWamTojF2envHHz9ud/L z89Xwq6qsseVmjQ534oFtGIAcautKq1aMFR+jh/rv9kK3u567ah8p9dYV78uALyHJfuJ3/tlKtw/ Msd2bq2lPwjhOeu+hs4oBvp7kU4mkM/nMXf8DXzvyQMYtAxcc+mgGB0dJ0Mj4+gbysIwTPi+By5B BQABBRccp99+0yvml//oqSe+/2XUlbeVzy/PqUpNsdriN2uLi0rnW1n+Tq38Zg03bqTfOfmcLgC8 F2XHvfrY1bv+Su8f+bS19X2EC4F8aXFDj5rKEgBApwQncx7259N4bMEmff5L4pJEZfYD20ZOZnoH 3jcwPJwezA5jKDuMhJmE49Zw+q0jK9VS/t899+SPfoLWlrqVpZfn1gKD+HHch5fn4lSfYjUTaLW4 +UaV9Z1kEeuSLgC8B2X86l1/lZzY9enEyDbiuw4ADkI27xmkhECnDKAEBWMLOejUxp97ZH952Jt8 37137t6RtPru0A1z79BI9oMnjr12NHdm7tcP7n/mbTQqcTMFjj4itq8ex+MB7ViAtN481p8D8JQ+ 7ZS+U+mUvrd6zzvZL5IuALzHZPTn/2BPIjP46cTYTsIdGwBABAVEuyD5OoUQgFAQQoPhvmQGqS1X XpGbJZ/96pe//AUALwP4v8PecZofXaXNfjsm0C44yJT3SQZAw3YfjZZftsU/fz3SicK1G5Zb77U2 q190bhOfiq5cCDKoeZ+auHwHgecF5csBgApQCKxRk6NjoYSBUBYCAYOAgNYzDGIk7l3jrZ2k8rbr 04oJUAAGAoOmh5sR9pEMginvj3/GhsfROzzXSs77CEIXAN790vAQXZJhiWxPEjIEF5QsDlYO9j23 oTJPvf5eoz40q/sf9YWAy/3w0rS+2AmhIJoxuJ577UDauQgSEHQACQTKz5pszXIJNpp8c67eE5d3 7HpdAHhviXhxzoXBGAyNIjL5hMNPDuCRV0/B1CkSRlCYk4eVhzzPg+/70cYYC8t7B4U4JSAwCLx8 dApFbSBooxQgDIRSQHAIHrnRndDcje5LJZYKrjXZl1afKMftriuU/U4y7NoBSDs57xY/Ll0AeI8J I8Drix7u2dEHFqxQAgICnXA87FyDP37gebzy0s/A3RqSCQO6xiIlJ4REiTyUUjBKYWgaNEowNzWJ 7z/7Op6rXQJGKUDkBhAhwChTb0NVEHWYLa5sne6r+QGqkmuoKzlT2gnqQb5mFlTek0DjCEA7YNgI GLyTzGAj/bpBwFbygQ98QB8cHPwl3/f3cs5N3/ePM8b2VSqV51544YXC+b6/mNTHtgmEwwVemOf4 JzeM47njSzg2XwQjBBpl2C92Yv+TFWz5ySO4ssfH5X0EfqUAu1zAmfl5rCzOwXVd2NUS5hZzKB46 hsmCwBnaB2GMg4bMgoQAkNAAksjAtl2gdSxNHXtX9znqCqvu+2j02VUfXlV8NYIfHwJkCEAgDkCq wsetPW9xTp5f7/5GgKHdZ55tAHGVdAGgifzCL/zC9mKx+J3h4eH39fb2gnOOYrGIQqHw+4Zh1O64 444XCSGPCSH2LS0tPX/48OH1Z9icQ6EAKg7w8GngQ5dfio9c4+CZo/M4NLkM3/NBiIHTxuU4XRPg MxzC98D8GngSwDYOwX0IwsCJDvgu+IgDU3AI1wHnHhghsHSChJWGY/TALpeAmgMifA91BVcpu1Qu quyr6bYqCKhWXLXqquIDjTRf9e191JVF3TjqIwECATisFxiaKXUrloBYv2bvaddvo9IpoADoAsAq +bVf+7X327b9+Ic+9KEBxhg8z4Pruujp6cHIyAg8zzNt275jZWXljlKpBMMwVoaGhp4G8JQQ4qEn n3zyEM6PrycAEI0BglBQFhjPl84Ar1v9uHZnFh+9mcMpl/D23AoOnz6DycU8Ko4PEAGfGBBMBNOB GQM4B3wHAhwJTUc6wZA00iC+C5sL1Ix+0GQPeGEZnHtB4VLHPonViq/uqyAg26N7R6Pia6grua6c U/MCVCBRXQVfua4EBJX2q8qvJgYBqxlDqwyMc90AACAASURBVJl78XPx32IjMYL4NVodb5or0AUA RT7zmc/0GIbxnZtvvnmAUoparRYFyFzXhed58DwPyWQS6XRaBtD6arXaJ1dWVj5ZLBb/ZO/evQue 5z3GOX/MMIyfPv7448fxzgGCgAh8cpmXTyHAOXBokeBnpyogfg1b0knctOty7DUAAz48z8divoyq bcN2HTi2CwqgVMyhkl9GsVjE/FIJc2UP3MzA6ulHUicQ8RmwRKiWEmgNAlLxfdQVPR7EU0FAVXx1 rF9V+LgSqwovgUBuKitop+ydKv9mxwvisum+v5QuACiydevWP77qqqu2DQ8Po1qtwrZtOI4D13Wj TYKAupmmid7eXnieR1zXHalUKr+Uz+d/qVQqiT179hwVQjxOCHmyWq0++uKLLy6t554u/eU/HoPr fg5E+zmaSPb6Ts31a6WfMUKf8Fz9yRnz8Kt44IE6fSXklG8XAQwrVwmWNNco4Dgcb82W8drbZdQq eVQLOVTzOTilHOxiHsQpAk4FcMqAUwShOojVD5LsA031wzTr12whcRcg3i5FjeTHlV/dV6P+qqVX gUQe+8q+VFAfq+m/el5lAq2Oz0b5OwUGtDnXqWvRSlqe6wJAKF/84hd3UUo/e8MNN6BarQaBMNuG 53kREDQDhDg78P1gim1fXx983yeu6+4sl8s78/n8Z4vFon/77bcfpJTuc133YcMwnt23b1+p1T3t +Fd/8c/g1r6cuvLmfmNwAtz3AOHDrRY/5a7Mf8pemcOl+WuW3X+07SlG6JOuhodFtWw3C8Y1aB4B GCXQGIHOKHyNQWgauK4BQle2BEBYONzX/prKUauHTfXdmw3dNRu/l33V96uZglKpVVdCKO1yU0Eg HvVvV95rs5Q/LutR1rNlDG2lCwChOI7zb2+77TbNMIxoOEwqt+/7cBwnAgMJBHJrxw48z0MqlcLg 4CA8z2Oe592Qz+dvKBQKv1MqlZw77rjjZwAeIoQ86TjOS88991wVAK78jS//G800/6Jv1ydZMBnH BSUawAlIqg+G1YPk6HYAYsCvVn6ulpv6OWd55ouO7bsQsXIcBAAhUNcXFISDcIKgKJR0GZRnWXYk 8gKIVE9O7+VhIxFy+XMKrH745G2sNW6vo6746vi98skNChhvUy1/K8UXWK3UaNLWCSuIv7az/M1e 0eR4Peda9VvPuS4AAMCXv/xlq1ar/eLOnTtRrVahaVqUJGMYRmTVXdcF57wBDDzPQ61WawoIcXYg GUIqlcLQ0BB83zds2761UCjcWiqVUCwW83tuv/0pu3fi9XK653ezH7iXCacW1OzjGgTzITgH5T4g OHwuILgHlsqAWVciOXYlAfcM33MCg00oCAmSejjnoVILQHCAC3ARrFoswuP6eUUfSGj9Ze4/oaCM gVIN4AKC++DCB+d+EDhs9PnluL3cpKKrfr8EApXqx2cGqteTE3mE0iapvxz7jyu+CgaqtAKDTmMB nRwDG1f+jVD/jq0/0AUAAEChULh7586dA57nQdd16LoeVc0JKud44JxD13UIIZBIJCCEaMoOJAjY tg3XdSNAiLOCRCIBz/NgWRYymYwMKPba1conl2r+J/Mf+iegwoVPKKjvBYrGAwAIXn0QziG4BiE4 KOehMrIgI0/UnzkhNm8eAIBoivAqCaYcqtRdbiy23y5rT2UAQKPfLoUBcNVbQmPgT1V8L3ZOfU98 fy13oNn71rL0Z6v861HwThlDJF0AAFCtVu/s6+tDpRIUqaGUQtO06NUwjEjhJTNQAUFadskOpMLH ASEOBvHYged5SJsMc5f/CrhbhSAAA4VIGCCcBFV7fA8+9xqAgIfKL7gPcAHfOU9pCUIQ1CflSAXX 0Vrx1VegeSaRCgTxbCM10KeCgB/b1lL+du5Auz7rOUaL441a/rWu05F0AQBAtVq9JZVKoVwuR2mx jDFomlZfn17TIlAAECmsCgi+78M0zahddQMcx4lcBRlglMxBAgH3HEzRnThjjEO3yyCEwScE1KMg VANhFFTTQYgGIgh83w2WFOdeBABCCJBqs0TF1RN+ghz/s6AGETkP0o1BBANgotH6x5U/Tvfjll9e WbXearKPp/Tjyqtq+ePKrwb+4tKpRW/WdjZWv9XxRt67YTegCwAAbNveMT8/D8YYTNOEYRhIJBJg jLUEBMYYEokEAKxyFSQgSDBQ2UEzQJCbcGz80PkAeK0AlxAQSkEoA6UMhGqgNEjBDdo1MMYgNApN GBAiWEGYgIMyHYLzcLltAKCBuxDmBwTl/4N7FiJsF7FhcxkHkBdRZw4SAlACEsYNBBdhTUFBAaTQ PLoft/wktgF1vx6oT/F1Uffx4+W9VPfARSNoeGi0/vFU4Wb76nE7xZf7G7X6rY43w/J3YwDrFc/z RmZnZyOllgCQTCZhGAYMw4Cu69A0rSkgyNlzZjhILmfVdcoObNsG9xy8sKgjV/BgsBIoY4ry0/BV 3dcgaBicC9mBpukgjIBSFipkoKxBEDBkySK+BfECEW+HCB+lKPQPOQGI0PCaCIFD+BCeDcF9B0AS jX5/s1RedeaQGvCTyuwp5zXUqb0aCJR91SE/1e9X5wF0qhSdKP1abRtR/LN5f6fnmkoXAAAsLS1h bm4OmqYhmUxC07QICCQYJBIJmKYJ0zSjuIA6XVaCgXQTZNyAENIw5VYGGOX54JyLBBF45k0HcMvw fKZYf9oSCHwqQUIDoQSEUTBfD615XZoG7M5GRHhNtcwY90FdbxmNDEDN3otb+1YWWQKGo7RrAGw0 Wn8Xq0HAR6Plb0f9W/xnbffXstrvpPKfleWX0gUAACsrK0vLy8uD0qrrug7DMCJ3IL6ZphkBgmyT 7kIrQGjnLlAkcOxMDUcWVpAyQoXXWDjsJhW/FRDUQYJQBqF5TZN2zqkQAkIAs3/oE8kb/+HlhLHX qrMnXrKnXj0e9oiP5ze8G3X/XbX0OgKll9KK+jfz+yUQxJWi2RfTCe1eD9Vv1taqT6cKfTasoK10 AQBAPp8/try8PCiVP/6q63rEAuJgINvjgCALaqhA0Mpd0KnAT55fAJwKXB4quOoCaBoYIQBlSgxg NRhQSuF7GoQfM3yrHvvmlYDWK7IoCLerSCUToFfcbILzG4VTuVEf3Pov+RUfKHul3Ou8VjjslZdf q546fBq+o47Hq0ogLb8ba1PXC4wn+cQpv7T+Kv1vxTRaSTtA2IginivW0O4zO5YuAACoVquPzMzM 3NLX1weVBaivKhioyr8WGKjBxFaAYCUSePS1OQjHg+uFiq0CAGMR3SeMRorPKAt98sbYAOd+ACBR lJ+CcC8avxeQOQ5+fUxfqM+l8kyRuv9fDwIyMKqBcw/V+dMwdQKqmUFSITUAw0SyZxCEainhuTd5 5ZWbwF3Rc/UdZWdx+jWvWjjkzJ86bC8em4bvS2UF6olDcoxfAkAz698q4i9BoBWtbod6Z2PNO31f q/ta67M2VfGldAEAgG3b/21ubu53CCEZXdcjEJBBv2asIA4IKkOQjMEwDCSTSei6DsuyonNquS1D 1/H6UgGn5paRSQbMIVB+xbIzhe4zJQbAYi5AyA4AgLIg3haMWgh4ngzq1ZVdVX4RD/7JuIEM/smA IqFgGoNTKaBw6jDSPT314GC41QOGBDASYOYYAEoI1dLJLdfu9soru/kVRSHKxRW/VnjTLi0erEwe /pk7f2wBjSm+UF7VsX5J71WfP+7/t5KNKE4nbkKrtnaBw/Vcp9Nz65IuAAA4dOjQ/JVXXvkvTp48 +Tc9PT1WKpVq8OslGKhMIL4vwaATd0HGEBKJBIShY99rs4BbgUdcENbIABrpPmt0DVSgCNNzCSMg oND0ZPT/bWoMkFGU5k9Dc0ro6RtoUH5EpcLqFYMawIAQUJaGnsyAU0oo1fqFW9vtlpd3p7fd8K/9 SmHBKy4fsnNzLxQPPvxTNNJl1W1QM/uaxQHOlWwECM5W8c+J5ZfC1u5yccjS0tKRZDL5Ldd1C/l8 vr9YLA7VajWqDtvJpB05ji+TetRXOb5fq9VQrVajfdu2o7ZKpYJKpYJqtQrhufjWCydwfD4PItN9 fT9YmjvcF74PcD8ovOH74L4H4XtBAlDYN3hfeJ5zMF2HnkiC6QaYbkBwH77nwvec6NVzbHhODb5T g+fUIDwb4C7ghxv3AM0ANBPQkqCGCTc3i4yZQKp3ACBaMC+AaSBMA1U3rX6u/qopfUOXQjPArF6M XboTg5dcmaK9Y5dzs+c2Y2Diyur80WfguzL6r/r4Phr9fvXcWgrSaeBjo/GCThS/Wds7qvhSugxA kampqWMA/gOA/zA+Pr7V87y7VlZW7qWU3p1MJodM0ySWZcE0zYgZxBlCM3agjizEYwfVlIlXj82C Oy5cj9UZQIz6R3SfBX4+oaRxmDDmJjiaBqt3qP7PyXhAOLsvSuiJS9PAIAG4A+TnMTC6BcxMBrW7 KAGYFn0uoQQQFIIGSUzMSIIwDURLBOsIYLV2cMGxfcjC5OwSlspVUEGRGNtJqGHe3G+XfyX34rfv x+qZfM1Sfpvl+8f+iYaPXo+s9Z6NKn2ztndE8aV0AaCFzMzMTAL4erhh27ZtHywWix9njO3VNG23 aZpGKpWK8gbU2IEKCnEXIQ4I1bSJpdwKCCh4WGbbb+P7yxjBqkCh6i4wBu466B25BKDB6j1BJqAP wYPkHTl/AFE2YKg7nEMmBwXPHgWED4P46BkaAQTguw4o44HygyNh9sMcGIfZOwgt1QfNSILqiYB1 +B4ED3IgEA57Cu4F2YO+jyt6XLz65tsoVmtBJSMhIHwBrX8LaN/Yp9jI5d/w508sojHw12r4T60N EJdzoUidjAy0at9owHFTpQsAHcrx48dfAvASgP/Y39/f29/ff3exWLwHwEdN07w8mUwSy7KQTCZb soNmrCBf0FEtF5DQNYBqAGUAYRA0AIIoys9URVcAga0GC0IpfEJQWl5ANtUTDNXFjVM4319E+02C 1AQA05Awk0j19ofKzEGgwRwYgTV0KdKj26An08GCI4QGrIAyyBoEhDLAZ6A0nLrMfHAeJENttRwc PvwmCpVqdA8yGMm5C31gImGOXntHef7EA2ht+ePWfz1R/43IRhV+Pe8/54ovpQsAG5BcLpfP5XLf AfAdAGTLli1XVKvVjywvL39c07TbTNPMJJNJkk6nI2YgX1VQMAwdFY+CuRVAsAAACAsUSIIBZRAh IHBCARqz/quCgvX93ORbGJq4YmP/JKGA2Ydkpg/MSMCzazAy/chceg3So1dAM00AAr7ngpfzTXIS QkbCNFBNBxUAJ3oQ0+ACFvNRmT+GxZUiaLQegToqAVCrD1rSuh7At9A4/NcKBJqJqkybAQadugOd tL/jFj8uXQA4exHT09NvAXgLwP0A9C1bttxWKBQ+vri4uNcwjOtN06Squ1AfXtRQ9giI4wB+o9JH IEBixwoQgLBGdtDgJmjw7CqWpt7C2JU3dv7fEAAQILoJw0qDwIfRm0XP5dfC7NsCwIfr1eCXnbqS UxYsFkLDzEXGQEljshLTguxgynRAI7iETuM7b56CRmngdVACzgnkCkNUCAjNAKfaBFYX9Yin+7YL /q03EWg9shEwOK8WPy7dUYDNF14sFk8WCoUfr6ys/CVj7Muc88OFQsEpFAqD5XI5bdt2OGGIo+J4 mK0EiTrgYeRdbr4HcD/W5odtPiCCTchRAC/wt6ORAu6jnFtE9rKrQSmB5zrwfQ++54B7LjzXhmdX weUogO8C1WA0QjcSMIfG0Xvlh2GN7YCgOrhXA/e8+ueF5cDrW6C8QmkLRivC4zAeMGZ6OHLoAHKl GgiCuRJbFp7GrvJL2IoFpEQFHEAVJryVKXNnyj1BKJ0rl8sVBIlBLlaPCgCbT/c3ImfDAN5xuRC+ sItJ9K1bt14P4COEkI8Zhn4LMXuMKfNqgNt1ZRfK86wyAtKEJcTbZBFPScUJxdDlu3DFrfeikl+G UyvDqZThVIuolfKoFXKwy3nU8isQS2+D+C6M/izSl14Poy8bPiAy/ZiuDkTG5iI0zlnQopmDMjYB quGDfSV885FnoTMGnwtcNfVdbB8bCD4nnFfACCAoRcX2US0XYdu2UywWXyeEPO44zk9PnDjx7MzM TAl1ENiMZ3kzA4jrjQ2cF+kCwHmUSy65pH9W3/o8G7tmJ/EVAIi/ymdGUn/pGjC2OmYQHetRYg7R DGy76R4MXHIFqsUVONUSnGoJteIKauUCqkuzsE8dCCYtTexCMnsZhAgvRVYHGRllAKtPVJJg0DAk SWIxgbD/RH8ShdOv47XjMxC6ifFj38Yt27MA1aIMSVlzgRISAkidqBaLRRSLReTz+YLrus8C+BEh 5MlHHnnkYJuv+p14zjcaGziv0gWA8yz61fd8XRve+S9AaUj5mwCA+tqKHawCgMZjZvVg+63/EGa6 B7VSHk6tBKdcQmHqCEpv/BQ0nYWx9VowpgfFPtCYX9A88CinIcdrFcTBINgHYbhzRwr/84f7oBEN Zu4o9vZMY2B4DEKISPkBNACBWpVJTq/2fR/VahW5XA6FQkGUy+Vp13UfF0I8TAh5+tFHH53E+Xu+ L0hlbyZdADjPol/7sd+m/Zf+F2r1h9l3YXVd7gbKvhYgROyAtQcEQqEPTmD77nsBQuDUalh87Qnk 33gGZGg7aM9oQDBIPLGIhoqthZZeDktqMVdAB2Mkcj1W1TNgDKah4/JEBfteOQowHdfN/QB33nxj kFSkrFCszlQkhETJU+o0apmZqdZkLJVKEhD8crl8WAjxBIBHC4XC0x0s6LqZGYLvGukCwPmW9/38 hGVZx/TxaxOeW4XwvTD116sH+xoYQBtAkIVAwhl7q+IFLAHaO4odt30Kcy98H/njB0CGdgC6CSDM 128xpFhX5uaZh63YguwPQvH+rYM49MbrWC7UYBVP4aMDc7jk8h3BpCjDaJghGfwbBKlUCqlUalVR VbXIilqfUT1XLBaxvLyMUqlkl8vlZyilPyWEPPLoo4++hHM7Z+BdI10AuAAkdfOn/8689P3/nBhJ +I4D7nvB+HqY898UCNYEBIUdqExAMyC0BEh5CUgNrWYOpPF1VRISC2sTxii/OhS5qi289h1XjeB/ 7XsJRNexdfIR/LO7bgTTjYaJVGqiVCaTQTabbSimqs69UBdkkUAglV9lB5xz2LaNlZUVrKysiGq1 WqhWq08AeIwx9vAjjzxy7Hz+/udTugBwAUjyzs9MJEjy1dSO3QPc59EwHfc9cM+D77vh8FswjNYW EBoYgQoUkh3QMIhIG12F+KhCdH41GKzOPIyNBKzqQ2EkErhqJI0XXgt0ba/9BG758O0N9RdUANB1 HVdcESQxqTUUWwFBnB2oDCHODjjnKJfLWFlZQT6fF5VK5ZTneT9hjD2WSqV+/N3vfndd6ze+m6UL ABeIZD7y2VtNlngwedkNvURPBrP2XBvcdRVGEAAB9z2FHawDECLGoBTLoTKpSAWC2LBiC7BomIfQ kKq8erhwy/AgPLuK0ws5pGuz+PjgGWzfeVVUM0G6ABIEBgcHMT4+HilvvYpx45JtcgGW9bKDeFm2 QqEgAcGrVquHCCGPCCEeO3PmzLMHDx4sn+/n41xJFwAuIMn8g9+8KsG9/8ysgY/pfVmNpgdBDSt0 C2z4jhsyAxe+7wX7nhcAg68kCbUNHqqJRTKZSGUHrbIQ9ShGsDrgKMf845Y/cBcECG66djsOvHEC ngCGll7Fp67tw9DwKHRdh2maSCaTERAkEgns2LEDlmWtAoD4Fl+haSPsoFkcIQwmIp/PVxzHeX54 ePixfD7/9Yceemjq/D4lmytdALiwhAJIWzt2bzeGL9tLEtZdzLR2G5mRfpbJEpbuDx5814bveQFD kIwgchmaxA7WCwiAEkhsNcy4Ol7QlB0wBkEobrnuarz0xtsgVMOWuSfwj267DgkzuarisiyWcv31 10PXdRBCImWX9L0dGGw2O+CcI5lMYnZ2Fm+88cacEOKmffv2vWdAoDsX4MISAaBWOfbckcqx544D +BqS/VZm1203asmBezQzuYelB65hPUPESA+BpHrhu7UgrdexmwKB3NAsmCiaAEDDOQ544TJjDeyg FSDU3YloNiNl4CBwfRdCBAuSmn4ZjutChHUJNE2LlNF13aC4aaiQavk0WVlZXXeh2SZLr8sFXddi B7LQS5wd+L6PwcFBHDhwAJxzjI2NjZ4+ffq3APz78/WAbLZ0AeDCEoGgHr6LoCQ2QzVXKb78vx4D 8FMAhr7l+rHMpVfuFYZ1F0v03Kr3DPTrmSz0nqEg395z4LsefM9WXAQvch14NMzYJGbQChBku+8C vtN6mJEygNQzECVAmMkkCqUq4PsQIGC+jXy+gEQi0WCB1RwA2w4qgkdZgcomA4dy1eN27ECu9JxM JhvYgVyQpR1DyGazePbZZ4NaBgAsy4IQ4hPoAkBXzrGoxS8coL6yjjv9Sml5+pWTAL6OZDJpbb/9 /cbAyN1UN+/U0kO79PSAYWSyQGoQwvXg+ja444L7Tj1e4AVBRSHZwbqGGRVA8MKy/fHYQZR3wAAw WBkL5VIlWI6cUzjVIsrlckOZNUm/HcdBMplEsVhsqKvQDAjOhh2sFTuglGJ2dhblchmU0qivpmm7 3vnH4dxJFwDeHSILXsiFMYNltqrVSuXQo09WgGcB/Il++TWjyZGr9rBE7116quc2Zg0MsswQSWSG 4AsO7tbgex54FDtoHGqUswnXBARfGU2QgBCxAxqLD1Aw9KFQrgDcB+ECVcePAECl3dIq27aN6elp pFKpqMiqHB5UU4JbgcHZsgPbtpFOp/Hggw+CMRaBQ/i+91TcrAsA7z5R2YEsoc0AMPfE4bJ74vBp AN8EkLCu/8T7jZ6Be5ie2MOsgev03mEtkR4EUtloiNH3bXC3zgzkkKMIp/LWg4lrBBGF3wgcIlje TxBAowS28IMlzYUHBxrsWm1VoVXVAs/OziKTyayqoyjBIL5W42ayg76+Puzfvx8AooCgGhdAY9ny d7V0AeDdLWqRTBdoWItPq7zy4NMV4HkA/8kYvjZrXrrrTs1K3akle29l6f4sS/cTPT0ETgi4E+Yd eMFQY+AiBHP4OfeiRKQ18wxigEB4UIG46tWiGoQFz0C1WgbTjMhCxxmAnPQjqynJNRXiS7SpC7du Fjvo6+vD888/DwAN7oHsj2DZsnZlyN410gWA95aoroKMHTAAzFk4VHIWDk0B+FsApnXtx9+nZ/rv IKa5x7D6btB6srqRGQJJD8N37DDvwA7zDeruQgQK0cjCGoDge+AIVhESQgCejYo1gnz+NKxUT0MQ UB2K830/UmhV8dWMwXPBDoQQyOVyyOfzke+vDhk6jlMDkAi/Zz/2+q4Dgi4AvHdFdRUABQwAVCqH fvQ0Anbw5xjY0de//do9SPbexczMbXp6YJRmssRM94fswIXn1bMS1SFGCQhCzmRUXYaQBTgcYBqD 5/JwtaBezJYtXMKq4E2UX25yleZ4UdU4GGwmO0gmk5H1V+9FiQ9MArDQuDaBrxy3W5XogpMuAFw8 ItmBXGwzchWwfKyUWz72LQRFTo3UNffs0nqydxPT2qNb/e/Xe7MJPTUAag0FTMCx4fkuuOtEQUQ1 PZlLZhDOanQ4q68k7nsAAVbSO+DXZjDiF5D2A3fDV5RNKlxvb2/bFZriABBnB+qKTCo7UNdnVIHA siwcOHCgwS1Rg4O2bb+NYAl0WZZMLVEGnPvViTZVugBwcYrKDiQgBGAAsPLhn7wAYD+APzf6JwaT 2268jVh9e7Vk761aqm+U9WSJmeoHCIPr1sIgogwmSnehvoKRJzgSjMCVKkIZiOAomeMoiDHo3EaP U0G2Mod0KgnOzYaEnGQyGVlwtahqM0DolB1omhYt8KIu614ul3Hy5MlomFANVJbLZdi2fRpAJvze auF3JmMvQD1A+K5gAl0A6Eq7QCJzclNl5+WpbwH4LgAjdc3du7S+4b1UM3Zr1uAHtZ5B08gMgViD wRCj74QxBNVV8OH7YUYhwiSicLVvCgGfJbBULqOwvPJWv7ZoJJPJCcuytHQ6HSmgTAtupvxrsYNW sYM4SzAMAysrK3AcB4SQBvrveZ5c1u0ogAEAFQTBwCrqjEpdXEFd5vyClS4AdCUu8ZyDICMxZAjl w4+9iIAdGEgPZjI7br6NpgbupMnMbUZ6YIJlhqiZHgSnWjCyEMYO/FoZcJzg0pQG44OCgICB14rw CjMn7dkT/18JyOu6zgcHBy83TfMGwzA+aFnWaDqdJplMBolEImIDqu/eDgw6CSRKZjA/Px8FA+MB wEqlUsvn81MATOU7AeogKuMBEkQveFegCwBdaSctcw4AMJSWisVXHvougB8CMBJX3LrTyG65U9Os PbrVewPryVpGZgikLwvu9cKpVeA74YxCzkGoBm7n4eamFvzpI38OYA5AyXXd6tzc3CEAfwfAzmaz 4/39/bctLS3t1TTtFsuyMpZlQS68Iv33jbADdd80TSwtLa0qOyaHJ2u12mHf900EAFlBXfGlKyWp zQVv+aW8p7KauvKOSiMYBMZEA2AA0JEe6c3suGk3TfXeSc3MrUbP4CUk2UcKc6eDtQN8D7w4C7Ey +6Y7/dqX4NZmEShVBUAJAbWuIWAgTrj52Ww2MTIystv3/bsJIR9LJBLXWZZFU6kULMs6K3aQTqdx +vRp7N+/P8oMlCBQqVQwOzv7t7lc7unw/ooAyuGWD19LsXu+4IGgCwBd2QyRPnCQorwaEAxz203b E8OX77VXlm9Np/TtKyv5eV6rPMWnX/kRAmWvog4AdrjFFwBZpVDbt28fTiaTH/V9/15N0/aYpjku 2YFhGGBhIdNO2IFlWVhYWMChQ4ciAPC8IHK5sLBQnZqa+jPO+RKAAoAV1IEgjzogSAC44Ok/0AWA rpwbUXMOVDDQIRlCkEzDUJ/wVEPdBG5IpgAAAV9JREFU0scVv1PRrrvuuvd7nncPpfRjuq7vTiaT iXQ6DcuyIqVvxw6EENEwoOsGU5ht28bs7OwLS0tLDyJQ/Fy4lcLjAuqsRQLXu0K6ANCVcy1qzoEK BmoAzcPqpJqzps/btm3rNQzjDsbYxwB8LJlMXm5ZFkmn01GCUbORhYMHD0ZpwYQQLC4u2lNTU1/0 PG8ewDLqyr8UvqrU32t1PxeidAGgK++kqIFE+ezJIchzPna+c+fObYlE4h4hxMcopXsty+pVg4mM MZimicOHDwMIFifJ5/NiaWnpb5aWln6MgOoXUKf8RdRjFc65vv9zIV0A6MpFKTt27Eik0+kPuq57 L6X0XsMwrksmk3omk0GlUkGpVEK1WhX5fP6H09PT/wn1GIV8VS3+u8LfbyZdAOhKVwDs2LEjm0gk 7qGUfhTAJ2u1WqpWq90/OTn5p6hP+JGpv5vmpnSlK13pynmT/x9S0uMXDcYeBwAAAABJRU5ErkJg gg== "
+ id="image6568"
+ x="10.278748"
+ y="645.6925" />
+ <text
+ xml:space="preserve"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="153.78568"
+ y="788.11218"
+ id="text6572"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan6574"
+ x="153.78568"
+ y="788.11218"
+ style="font-size:10px">sshd</tspan></text>
+ <path
+ transform="matrix(0.57142858,0,0,0.57142858,171.06568,583.13259)"
+ d="M 46.5,369.75 A 12.25,12.25 0 0 1 34.25,382 12.25,12.25 0 0 1 22,369.75 12.25,12.25 0 0 1 34.25,357.5 12.25,12.25 0 0 1 46.5,369.75 Z"
+ sodipodi:ry="12.25"
+ sodipodi:rx="12.25"
+ sodipodi:cy="369.75"
+ sodipodi:cx="34.25"
+ id="path6576"
+ style="color:#000000;fill:#bf3030;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.875;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <path
+ transform="matrix(0.57142857,0,0,0.57142857,93.79141,647.36829)"
+ d="M 46.5,369.75 A 12.25,12.25 0 0 1 34.25,382 12.25,12.25 0 0 1 22,369.75 12.25,12.25 0 0 1 34.25,357.5 12.25,12.25 0 0 1 46.5,369.75 Z"
+ sodipodi:ry="12.25"
+ sodipodi:rx="12.25"
+ sodipodi:cy="369.75"
+ sodipodi:cx="34.25"
+ id="path6582"
+ style="color:#000000;fill:#669b00;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.875;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <g
+ id="g6586"
+ transform="translate(-191,48)">
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ d="m 297.71429,745.5404 c 0,-20.64274 0,-45.28548 0,-65.92822 m 0,-4.4375 0,-14.31605"
+ style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:1, 1;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none"
+ id="path6588" />
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path6590"
+ sodipodi:cx="123.6875"
+ sodipodi:cy="222.6875"
+ sodipodi:rx="2.4375"
+ sodipodi:ry="2.4375"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ transform="matrix(0.61538462,0,0,0.61538462,221.61388,520.72527)" />
+ </g>
+ <path
+ style="color:#000000;fill:#bf3030;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-opacity:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 184.56248,797.86218 c 1.20645,2.10718 3.46039,3.53125 6.0625,3.53125 3.86599,0 7,-3.13401 7,-7 0,-3.86599 -3.13401,-7 -7,-7 -2.57733,0 -4.81629,1.39468 -6.03125,3.46875"
+ id="path6592"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="csssc" />
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.5;stroke-opacity:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 110.13034,852.51843 c -2.14987,1.19474 -3.59375,3.49105 -3.59375,6.125 0,3.86599 3.13401,7 7,7 3.86599,0 7,-3.13401 7,-7 0,-2.5416 -1.37968,-4.74241 -3.40625,-5.96875"
+ id="path6596"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="csssc" />
+ <rect
+ ry="5.9999995"
+ rx="6"
+ y="716.92932"
+ x="38.932571"
+ height="24.950771"
+ width="59.384914"
+ id="rect6598"
+ style="color:#000000;fill:#000000;fill-opacity:0.48309183;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <path
+ sodipodi:nodetypes="cc"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;enable-background:accumulate"
+ d="m 91.189329,788.49596 0,-60.60817"
+ id="path6602"
+ inkscape:connector-curvature="0" />
+ <text
+ xml:space="preserve"
+ style="font-size:10px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="53.008148"
+ y="732.95953"
+ id="text6604"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan6606"
+ x="53.008148"
+ y="732.95953">kernel</tspan></text>
+ <path
+ sodipodi:nodetypes="cc"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 126.87498,725.48718 -33.25,0"
+ id="path6612"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path6614"
+ sodipodi:cx="123.6875"
+ sodipodi:cy="222.6875"
+ sodipodi:rx="2.4375"
+ sodipodi:ry="2.4375"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ transform="matrix(0.61538462,0,0,0.61538462,15.03178,588.39317)" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path6616"
+ d="m 91.12498,722.89879 0,-18.125"
+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ sodipodi:nodetypes="cc" />
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path6584"
+ sodipodi:cx="123.6875"
+ sodipodi:cy="222.6875"
+ sodipodi:rx="2.4375"
+ sodipodi:ry="2.4375"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ transform="matrix(0.61538462,0,0,0.61538462,53.04772,597.83278)" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:3.99999999, 0.99999999;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none"
+ d="m 111.02232,790.92821 -7e-5,-38.43431 m -1e-5,-0.80071 -7e-5,-16.84207 15.99985,-0.0771"
+ id="path6570"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text9689"
+ y="910.29578"
+ x="24.999998"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ id="tspan9695"
+ y="910.29578"
+ x="24.999998"
+ sodipodi:role="line"
+ style="font-weight:bold">Transports and Ports</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="286.5"
+ y="1005.8622"
+ id="text9707"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan9709"
+ x="286.5"
+ y="1005.8622"
+ style="font-size:10px">Web Interface</tspan></text>
+ <g
+ id="g9722"
+ transform="translate(-8.4159991,-3.6943631)" />
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:8px;line-height:125%;font-family:'Bitstream Vera Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+ x="24.999998"
+ y="610.36218"
+ id="text9732"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan9734"
+ x="24.999998"
+ y="610.36218"
+ style="font-weight:bold;font-size:16px">Cockpit</tspan><tspan
+ sodipodi:role="line"
+ x="24.999998"
+ y="626.37518"
+ style="font-weight:bold;font-size:12px"
+ id="tspan9736">Transport Architecture</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:10px;line-height:125%;font-family:'Bitstream Vera Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+ x="509.44617"
+ y="604.52795"
+ id="text3969"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan3971"
+ x="509.44617"
+ y="604.52795"
+ style="font-size:8px">Last updated: 2015-11-02</tspan></text>
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#679b00;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path4021"
+ sodipodi:cx="34.25"
+ sodipodi:cy="369.75"
+ sodipodi:rx="12.25"
+ sodipodi:ry="12.25"
+ d="M 46.5,369.75 A 12.25,12.25 0 0 1 34.25,382 12.25,12.25 0 0 1 22,369.75 12.25,12.25 0 0 1 34.25,357.5 12.25,12.25 0 0 1 46.5,369.75 Z"
+ transform="translate(4.5000001,624.36218)" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text4023"
+ y="987.44031"
+ x="58.464287"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ id="tspan4031"
+ y="987.44031"
+ x="58.464287"
+ sodipodi:role="line"><tspan
+ style="font-weight:bold"
+ id="tspan4113">WebSocket</tspan>: port 9090</tspan><tspan
+ y="997.44031"
+ x="58.464287"
+ sodipodi:role="line"
+ id="tspan4042"><tspan
+ style="font-size:8px"
+ id="tspan4044">a. GSSAPI + SSL </tspan><tspan
+ id="tspan4035"
+ style="font-size:6px;fill:#666666">or </tspan></tspan><tspan
+ y="1007.4403"
+ x="58.464287"
+ sodipodi:role="line"
+ id="tspan4037">b. Basic Auth + SSL</tspan></text>
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#bf3030;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:0.84705882;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path3975"
+ sodipodi:cx="34.25"
+ sodipodi:cy="369.75"
+ sodipodi:rx="12.25"
+ sodipodi:ry="12.25"
+ d="M 46.5,369.75 A 12.25,12.25 0 0 1 34.25,382 12.25,12.25 0 0 1 22,369.75 12.25,12.25 0 0 1 34.25,357.5 12.25,12.25 0 0 1 46.5,369.75 Z"
+ transform="translate(4.5000001,570.36218)" />
+ <g
+ id="g5908">
+ <text
+ xml:space="preserve"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="58.464287"
+ y="933.29968"
+ id="text3977"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ x="58.464287"
+ y="933.29968"
+ id="tspan3979"
+ style="font-weight:bold">SSH<tspan
+ id="tspan3997"
+ style="font-weight:normal">: port 22</tspan></tspan><tspan
+ sodipodi:role="line"
+ x="58.464287"
+ y="943.29968"
+ id="tspan3987"><tspan
+ style="font-size:8px"
+ id="tspan3993">a. gssapi-mic </tspan><tspan
+ style="font-size:6px;fill:#666666"
+ id="tspan3989">or </tspan></tspan><tspan
+ id="tspan3995"
+ sodipodi:role="line"
+ x="58.464287"
+ y="953.29968">b. password auth</tspan><tspan
+ sodipodi:role="line"
+ x="58.464287"
+ y="963.29968"
+ id="tspan3329">c. pubkey</tspan></text>
+ </g>
+ <path
+ style="fill:none;stroke:#bf3030;stroke-width:7;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 190.3885,794.38433 59.22019,0"
+ id="path4107"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="cc"
+ style="fill:none;stroke:#679b00;stroke-width:7;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="m 320.98317,811.39295 0.19506,97.80509"
+ id="path4111"
+ inkscape:connector-curvature="0" />
+ <path
+ sodipodi:nodetypes="ccc"
+ style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:1, 1;stroke-dashoffset:0"
+ d="m 320.13065,908.14818 1.8e-4,-112.6013 -213.64628,0"
+ id="path8021"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4121"
+ d="m 322.13065,907.39818 1.7e-4,-114.45979 -211.65751,0.19121"
+ style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:4, 1;stroke-dashoffset:0"
+ sodipodi:nodetypes="ccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4257"
+ d="m 450.73,794.25933 -59.22019,0"
+ style="fill:none;stroke:#bf3030;stroke-width:7;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ sodipodi:nodetypes="cc"
+ style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:1, 1;stroke-dashoffset:0"
+ d="m 537.37901,794.96028 -216.8788,0.54863"
+ id="path4261"
+ inkscape:connector-curvature="0" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4263"
+ d="m 530.91914,792.27041 -147.10231,0.75148 -60.85931,0"
+ style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:4, 1;stroke-dashoffset:0"
+ sodipodi:nodetypes="ccc" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text4309"
+ y="925.25134"
+ x="400.35562"
+ style="font-style:normal;font-weight:normal;font-size:10px;line-height:125%;font-family:'Bitstream Vera Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+ xml:space="preserve"><tspan
+ style="font-size:8px"
+ y="925.25134"
+ x="400.35562"
+ sodipodi:role="line"
+ id="tspan6520">All calls use the user's</tspan><tspan
+ style="font-size:8px"
+ y="935.25134"
+ x="400.35562"
+ sodipodi:role="line"
+ id="tspan3325">privileges and polkit/sudo to</tspan><tspan
+ style="font-size:8px"
+ y="945.25134"
+ x="400.35562"
+ sodipodi:role="line"
+ id="tspan3327">escalate if necessary.</tspan></text>
+ <path
+ style="fill:none;stroke:#ff0000;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-start:none;marker-end:url(#EmptyTriangleOutM7)"
+ d="m 453.76817,911.2058 c 16.54323,-16.63267 46.78024,-42.10644 57.9835,-71.87479 17.91169,-47.5934 16.80878,-100.11664 16.80878,-100.11664"
+ id="path4317"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="csc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path4040"
+ d="m 330.02681,794.46066 0,-48.3199 -15.77732,-0.12941"
+ style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:1, 1;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none"
+ sodipodi:nodetypes="ccc" />
+ <text
+ xml:space="preserve"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="528.11328"
+ y="894.01855"
+ id="text5886"
+ sodipodi:linespacing="125%"><tspan
+ style="font-weight:bold"
+ sodipodi:role="line"
+ x="528.11328"
+ y="894.01855"
+ id="tspan5888">Processes</tspan></text>
+ <rect
+ ry="5.9999995"
+ rx="6"
+ y="939.71191"
+ x="529.07336"
+ height="31.157877"
+ width="84.023903"
+ id="rect5830-3"
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.49999997000000002;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <text
+ xml:space="preserve"
+ style="font-size:10px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="571.94031"
+ y="953.28046"
+ id="text5917"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan5919"
+ x="571.94031"
+ y="953.28046"
+ style="font-size:10px;text-align:center;text-anchor:middle">system service</tspan><tspan
+ sodipodi:role="line"
+ x="571.94031"
+ y="963.78699"
+ id="tspan6034"
+ style="font-size:8px;text-align:center;text-anchor:middle">running as root</tspan></text>
+ <rect
+ style="color:#000000;fill:#d4eeff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.49999997000000002;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect5921"
+ width="84.023903"
+ height="46.71423"
+ x="529.07336"
+ y="980.5802"
+ rx="6"
+ ry="5.9999995" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text5923"
+ y="991.78052"
+ x="571.15466"
+ style="font-size:10px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ y="991.78052"
+ x="571.15466"
+ id="tspan5925"
+ sodipodi:role="line"
+ style="text-align:center;text-anchor:middle">user process</tspan><tspan
+ y="1002.287"
+ x="571.15466"
+ sodipodi:role="line"
+ id="tspan5929"
+ style="font-size:8px;text-align:center;text-anchor:middle">subject to</tspan><tspan
+ y="1012.287"
+ x="571.15466"
+ sodipodi:role="line"
+ style="font-size:8px;text-align:center;text-anchor:middle"
+ id="tspan6036">authorization</tspan><tspan
+ y="1022.287"
+ x="571.15466"
+ sodipodi:role="line"
+ id="tspan5933"
+ style="font-size:8px;text-align:center;text-anchor:middle">and audit</tspan></text>
+ <g
+ id="g9717"
+ transform="translate(-354.74166,-46.369173)">
+ <path
+ sodipodi:nodetypes="cc"
+ style="fill:none;stroke:#000000;stroke-width:0.99999994;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:4.00000004, 1.00000001;stroke-dashoffset:0"
+ d="m 504.17392,986.0808 24.96859,0.1"
+ id="path8023"
+ inkscape:connector-curvature="0" />
+ <text
+ xml:space="preserve"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="538.14935"
+ y="989.0542"
+ id="text8025"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ x="538.14935"
+ y="989.0542"
+ id="tspan4431">REST messages</tspan></text>
+ </g>
+ <g
+ transform="translate(-354.74166,-60.369173)"
+ id="g3117">
+ <path
+ inkscape:connector-curvature="0"
+ id="path3119"
+ d="m 504.17392,983.0808 24.96859,0.1"
+ style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:1, 1;stroke-dashoffset:0"
+ sodipodi:nodetypes="cc" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text3121"
+ y="986.0542"
+ x="538.14935"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ id="tspan3123"
+ y="986.0542"
+ x="538.14935"
+ sodipodi:role="line">DBus messages</tspan></text>
+ </g>
+ <text
+ xml:space="preserve"
+ style="font-size:8px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="146.67435"
+ y="909.86774"
+ id="text4324"
+ sodipodi:linespacing="125%"><tspan
+ style="font-weight:bold"
+ sodipodi:role="line"
+ x="146.67435"
+ y="909.86774"
+ id="tspan4326">Messages and Models</tspan></text>
+ <path
+ transform="matrix(0.61538462,0,0,0.61538462,85.558957,841.18858)"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ sodipodi:ry="2.4375"
+ sodipodi:rx="2.4375"
+ sodipodi:cy="222.6875"
+ sodipodi:cx="123.6875"
+ id="path5880"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <text
+ xml:space="preserve"
+ style="font-size:10px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="183.2679"
+ y="980.82147"
+ id="text5882"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan5884"
+ x="183.2679"
+ y="980.82147"
+ style="font-size:8px">API (Interface)</tspan></text>
+ <text
+ sodipodi:linespacing="125%"
+ id="text6078"
+ y="927.44189"
+ x="505.40695"
+ style="font-size:10px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ xml:space="preserve"><tspan
+ style="font-size:8px"
+ y="927.44189"
+ x="505.40695"
+ id="tspan6080"
+ sodipodi:role="line" /></text>
+ <rect
+ style="color:#000000;fill:#111111;fill-opacity:0.71764708;fill-rule:nonzero;stroke:#000000;stroke-width:0.49999997000000002;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect6388-2"
+ width="83.384911"
+ height="24.950771"
+ x="-612.19244"
+ y="904.88678"
+ rx="6"
+ ry="5.9999995"
+ transform="scale(-1,1)" />
+ <text
+ xml:space="preserve"
+ style="font-size:10px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="554.60205"
+ y="921.09021"
+ id="text6426"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan6428"
+ x="554.60205"
+ y="921.09021">kernel</tspan></text>
+ <g
+ transform="translate(-354.74166,-28.369173)"
+ id="g6475">
+ <path
+ inkscape:connector-curvature="0"
+ id="path6477"
+ d="m 504.17392,986.0808 24.96859,0.1"
+ style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ sodipodi:nodetypes="cc" />
+ <text
+ sodipodi:linespacing="125%"
+ id="text6479"
+ y="989.0542"
+ x="538.14935"
+ style="font-style:normal;font-weight:normal;font-size:8px;line-height:125%;font-family:'Bitstream Vera Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+ xml:space="preserve"><tspan
+ id="tspan6481"
+ y="989.0542"
+ x="538.14935"
+ sodipodi:role="line">syscall/api/sysfs</tspan></text>
+ </g>
+ <text
+ xml:space="preserve"
+ style="font-style:normal;font-weight:normal;font-size:10px;line-height:125%;font-family:'Bitstream Vera Sans';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
+ x="611.60352"
+ y="615.8446"
+ id="text6483"
+ sodipodi:linespacing="125%"><tspan
+ sodipodi:role="line"
+ id="tspan6485"
+ x="611.60352"
+ y="615.8446"
+ style="font-size:8px;text-align:end;text-anchor:end"><tspan
+ style="font-weight:bold;text-align:end;text-anchor:end"
+ id="tspan6489">Note</tspan>: (nearly) all processes</tspan><tspan
+ sodipodi:role="line"
+ x="611.60352"
+ y="625.8446"
+ id="tspan6487"
+ style="font-size:8px;text-align:end;text-anchor:end">socket-activated or run-on-demand</tspan></text>
+ <path
+ sodipodi:nodetypes="ccc"
+ inkscape:connector-curvature="0"
+ id="path4207"
+ d="m 334.00015,792.29799 -2.9e-4,-50.79762 -16.04249,0.10187"
+ style="fill:none;stroke:#000000;stroke-width:0.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:4.00000003, 1;stroke-dashoffset:0;marker-start:none;marker-mid:none;marker-end:none" />
+ <g
+ id="g4253"
+ transform="translate(-354.74166,-19.369173)" />
+ <rect
+ ry="5.9999995"
+ rx="6.0000005"
+ y="697.76501"
+ x="325.56561"
+ height="21.371223"
+ width="68.30262"
+ id="rect3266"
+ style="color:#000000;fill:#fff0d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+ <text
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-weight:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="360.90088"
+ y="712.44165"
+ id="text3268"
+ sodipodi:linespacing="125%"><tspan
+ id="tspan3270"
+ sodipodi:role="line"
+ x="360.90088"
+ y="712.44165"
+ style="font-size:10px;text-align:center;text-anchor:middle">kubernetes</tspan></text>
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.625;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path3272"
+ sodipodi:cx="123.6875"
+ sodipodi:cy="222.6875"
+ sodipodi:rx="2.4375"
+ sodipodi:ry="2.4375"
+ d="m 126.125,222.6875 a 2.4375,2.4375 0 0 1 -2.4375,2.4375 2.4375,2.4375 0 0 1 -2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,-2.4375 2.4375,2.4375 0 0 1 2.4375,2.4375 z"
+ transform="matrix(0.61538462,0,0,0.61538462,254.5326,571.2929)" />
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.99999994000000003;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;color:#000000;stroke-miterlimit:4;stroke-dasharray:3.99999999000000006, 0.99999998999999995;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 326.99999,136.00001 -8.5,0"
+ id="path3274"
+ inkscape:connector-curvature="0"
+ transform="translate(0,572.36218)" />
+ </g>
+</svg>
diff --git a/doc/guide/Makefile-guide.am b/doc/guide/Makefile-guide.am
new file mode 100644
index 0000000..66c425c
--- /dev/null
+++ b/doc/guide/Makefile-guide.am
@@ -0,0 +1,114 @@
+if ENABLE_DOC
+
+GUIDE_DOCBOOK = doc/guide/cockpit-guide.xml
+
+GUIDE_INCLUDES = \
+ doc/guide/api-cockpit.xml \
+ doc/guide/api-base1.xml \
+ doc/guide/api-shell.xml \
+ doc/guide/api-system.xml \
+ doc/guide/cockpit-cache.xml \
+ doc/guide/cockpit-channel.xml \
+ doc/guide/cockpit-dbus.xml \
+ doc/guide/cockpit-error.xml \
+ doc/guide/cockpit-file.xml \
+ doc/guide/cockpit-http.xml \
+ doc/guide/cockpit-locale.xml \
+ doc/guide/cockpit-location.xml \
+ doc/guide/cockpit-manifest.xml \
+ doc/guide/cockpit-metrics.xml \
+ doc/guide/cockpit-series.xml \
+ doc/guide/cockpit-session.xml \
+ doc/guide/cockpit-spawn.xml \
+ doc/guide/cockpit-util.xml \
+ doc/guide/authentication.xml \
+ doc/guide/embedding.xml \
+ doc/guide/feature-firewall.xml \
+ doc/guide/feature-journal.xml \
+ doc/guide/feature-machines.xml \
+ doc/guide/feature-networkmanager.xml \
+ doc/guide/feature-packagekit.xml \
+ doc/guide/feature-pcp.xml \
+ doc/guide/feature-realmd.xml \
+ doc/guide/feature-selinux.xml \
+ doc/guide/feature-sosreport.xml \
+ doc/guide/feature-storaged.xml \
+ doc/guide/feature-systemd.xml \
+ doc/guide/feature-terminal.xml \
+ doc/guide/feature-tuned.xml \
+ doc/guide/feature-users.xml \
+ doc/guide/packages.xml \
+ doc/guide/privileges.xml \
+ doc/guide/https.xml \
+ doc/guide/listen.xml \
+ doc/guide/sso.xml \
+ doc/guide/cert-authentication.xml \
+ doc/guide/startup.xml \
+ doc/guide/urls.xml \
+ $(NULL)
+
+GUIDE_STATIC = \
+ doc/guide/static/home.png \
+ doc/guide/static/gtk-doc.css \
+ doc/guide/static/left.png \
+ doc/guide/static/right.png \
+ doc/guide/static/style.css \
+ doc/guide/static/up.png \
+ $(NULL)
+
+GUIDE_XSLT = \
+ doc/guide/gtk-doc.xsl \
+ doc/guide/version-greater-or-equal.xsl \
+ $(NULL)
+
+# keep in sync with doc/guide/static/style.css
+GUIDE_FONTS = \
+ doc/guide/html/RedHatText-Regular.woff2 \
+ doc/guide/html/RedHatText-Medium.woff2 \
+ $(NULL)
+
+dist_noinst_DATA += \
+ $(GUIDE_DOCBOOK) \
+ $(GUIDE_INCLUDES) \
+ $(GUIDE_XSLT) \
+ $(GUIDE_STATIC) \
+ $(NULL)
+
+noinst_DATA += doc/guide/html/index.html
+
+doc/guide/html/%.woff2: dist/static/manifest.json
+ @target='$(dir $@)'; \
+ source='dist/static/fonts/$(notdir $@)'; \
+ printf ' %-8s %s → %s\n' 'COPY' "$${source}" "$${target}"; \
+ mkdir -p "$${target}" && \
+ cp "$(top_srcdir)/$${source}" "$${target}"
+
+doc/guide/html/index.html: $(GUIDE_DOCBOOK) $(GUIDE_INCLUDES) $(GUIDE_STATIC) $(GUIDE_XSLT) $(GUIDE_FONTS)
+ $(AM_V_GEN) mkdir -p doc/guide/html/ && \
+ cp $(addprefix $(srcdir)/,$(GUIDE_STATIC)) doc/guide/html/ && \
+ LANG=C.UTF-8 $(XMLTO) html -m $(srcdir)/doc/guide/gtk-doc.xsl -o doc/guide/html/ \
+ --searchpath $(abs_builddir):$(abs_srcdir):$(abs_builddir)/doc/guide \
+ $(srcdir)/$(GUIDE_DOCBOOK) && \
+ rm -f doc/guide/html/cockpit-guide.proc
+
+CLEAN_LOCAL_TARGETS += clean-guide
+clean-guide:
+ rm -rf doc/guide/html
+
+CHECK_LOCAL_TARGETS += check-guide
+check-guide:
+ if grep -n -r 'name="id' doc/guide/html >&2; then \
+ echo "Unexpected generated id in the documentation" >&2; \
+ exit 1; \
+ fi
+
+INSTALL_DATA_LOCAL_TARGETS += install-guide
+install-guide:
+ mkdir -p $(DESTDIR)$(htmldir)
+ $(INSTALL_DATA) doc/guide/html/* $(DESTDIR)$(htmldir)
+
+UNINSTALL_LOCAL_TARGETS += uninstall-guide
+uninstall-guide:
+ rm -rf $(DESTDIR)$(htmldir)
+
+endif
diff --git a/doc/guide/api-base1.xml b/doc/guide/api-base1.xml
new file mode 100644
index 0000000..6267405
--- /dev/null
+++ b/doc/guide/api-base1.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!DOCTYPE reference PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd"
+[
+ <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+]>
+<reference id="api-base1">
+ <title>API: base1</title>
+
+ <partintro>
+ <para>This package contains basic support API available to other packages.</para>
+ </partintro>
+
+ <xi:include href="api-cockpit.xml"/>
+
+ <xi:include href="cockpit-dbus.xml"/>
+ <xi:include href="cockpit-file.xml"/>
+ <xi:include href="cockpit-http.xml"/>
+ <xi:include href="cockpit-spawn.xml"/>
+ <xi:include href="cockpit-metrics.xml"/>
+ <xi:include href="cockpit-series.xml"/>
+ <xi:include href="cockpit-channel.xml"/>
+
+ <xi:include href="cockpit-location.xml"/>
+
+ <xi:include href="cockpit-locale.xml"/>
+ <xi:include href="cockpit-error.xml"/>
+ <xi:include href="cockpit-session.xml"/>
+ <xi:include href="cockpit-util.xml"/>
+ <xi:include href="cockpit-cache.xml"/>
+ <xi:include href="cockpit-manifest.xml"/>
+</reference>
diff --git a/doc/guide/api-cockpit.xml b/doc/guide/api-cockpit.xml
new file mode 100644
index 0000000..bd957ba
--- /dev/null
+++ b/doc/guide/api-cockpit.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="api-cockpit">
+ <refmeta>
+ <refentrytitle>cockpit.js</refentrytitle>
+ </refmeta>
+
+ <refnamediv>
+ <refname>cockpit.js</refname>
+ <refpurpose>Basic cockpit API to interact with the system</refpurpose>
+ </refnamediv>
+
+ <refsection id="api-cockpit-loading">
+ <title>Loading cockpit.js</title>
+ <para><code>cockpit.js</code> should be loaded via a script tag.</para>
+
+<programlisting><![CDATA[
+<script src="../base1/cockpit.js">
+]]></programlisting>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/api-shell.xml b/doc/guide/api-shell.xml
new file mode 100644
index 0000000..0fa8c5a
--- /dev/null
+++ b/doc/guide/api-shell.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<!DOCTYPE reference PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd"
+[
+ <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+]>
+<reference id="api-shell">
+ <title>API: shell</title>
+
+ <partintro>
+ <para>This package contains the shell that loads other components.</para>
+ </partintro>
+
+ <refentry id="api-shell-html">
+ <refmeta>
+ <refentrytitle>index.html</refentrytitle>
+ </refmeta>
+
+ <refnamediv>
+ <refname>index.html</refname>
+ <refpurpose>Main cockpit shell, for a single machine</refpurpose>
+ </refnamediv>
+
+ <refsection id="api-shell-html-description">
+ <title>Description</title>
+<programlisting>
+&lt;iframe src="http://127.0.0.1:9090/cockpit+app/@localhost/shell/index.html"
+ width="600" height="400"&gt;&lt;/iframe&gt;
+</programlisting>
+
+ <para>This is a Cockpit component that provides an interface to configure a
+ single machine. It loads other components on demand to make that happen.</para>
+
+ <variablelist>
+ <varlistentry>
+ <term>Component URL</term>
+ <listitem><para><code>/cockpit+app/@localhost/shell/index.html</code></para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ </refsection>
+ </refentry>
+
+</reference>
diff --git a/doc/guide/api-system.xml b/doc/guide/api-system.xml
new file mode 100644
index 0000000..b344d77
--- /dev/null
+++ b/doc/guide/api-system.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0"?>
+<!DOCTYPE reference PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd"
+[
+ <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+]>
+<reference id="api-system">
+ <title>API: system</title>
+
+ <partintro>
+ <para>This package contains general components for basic control of a system.</para>
+ </partintro>
+
+ <refentry id="api-logs-html">
+ <refmeta>
+ <refentrytitle>logs.html</refentrytitle>
+ </refmeta>
+
+ <refnamediv>
+ <refname>logs.html</refname>
+ <refpurpose>System log component</refpurpose>
+ </refnamediv>
+
+ <refsection id="api-logs-html-description">
+ <title>Description</title>
+<programlisting>
+&lt;iframe src="http://127.0.0.1:9090/cockpit+app/@localhost/system/logs.html"
+ width="600" height="400"&gt;&lt;/iframe&gt;
+</programlisting>
+
+ <para>This is a Cockpit component that brings up system log viewer, with filtering
+ capabilities. On systemd based systems this displays the entries from journal.</para>
+
+ <variablelist>
+ <varlistentry>
+ <term>Component URL</term>
+ <listitem><para><code>/cockpit+app/@localhost/system/logs.html</code></para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Filter by priority</term>
+ <listitem>
+ <para><code>#?prio=notice</code></para>
+ <para>Filters the log to show entries below the specific priority, inclusive. These
+ <ulink url="https://en.wikipedia.org/wiki/Syslog#Severity_levels">priorities are syslog level keywords</ulink>.
+ Specifying <code>*</code> as a priority will show all available entries. The default
+ priority is <code>err</code>.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Filter by service</term>
+ <listitem>
+ <para><code>#?service=sshd.service</code></para>
+ <para>Filters the log to show entries related to the specific service. The format of
+ the service is specific to the logging implementation. For journald these are systemd
+ service unit names.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Filter by tag</term>
+ <listitem>
+ <para><code>#?tag=kernel</code></para>
+ <para>Filters the log to show entries related to the specific syslog identifier.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Filter by message</term>
+ <listitem>
+ <para><code>#?grep=</code></para>
+ <para>Filters the log to show entries where the <code>MESSAGE=</code> field matches the specified regular expression.
+ PERL-compatible regular expressions are used. If the pattern is all lowercase, matching is case insensitive.
+ Otherwise, matching is case sensitive.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Filter by any field</term>
+ <listitem>
+ <para><code>#?FIELD=VALUE</code></para>
+ <para>A field is referring to the components of a structured journal entry. The match must be exact.
+ Value can be comma separated list in which case they are automatically matched as alternatives.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Start at point</term>
+ <listitem>
+ <para><code>#?start=boot</code></para>
+ <para>Filters the log to show entries after the specific point in time. Valid values
+ are <code>boot</code> (since last boot), <code>last-24h</code> (last 24 hours),
+ <code>last-week</code> (last seven days) and <code>previous-boot</code> (previous boot).</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Follow the journal</term>
+ <listitem>
+ <para><code>#?follow=true</code></para>
+ <para>Keep listening on new entries. Valid values are <code>true</code> (follow) and <code>false</code> (do not follow).</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Show log entry</term>
+ <listitem>
+ <para><code>#/6e272d82993c4e0d...</code></para>
+ <para>To show a specific log entry, put the log entry cursor in a path after
+ the hash above. Note that cursors are logging system specific, and journal
+ cursors are subject to change.</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+
+ </refsection>
+ </refentry>
+
+ <refentry id="api-terminal-html">
+ <refmeta>
+ <refentrytitle>terminal.html</refentrytitle>
+ </refmeta>
+
+ <refnamediv>
+ <refname>terminal.html</refname>
+ <refpurpose>Server terminal component</refpurpose>
+ </refnamediv>
+
+ <refsection id="api-terminal-html-description">
+ <title>Description</title>
+<programlisting>
+&lt;iframe src="http://127.0.0.1:9090/cockpit+app/@localhost/system/terminal.html"
+ width="600" height="400"&gt;&lt;/iframe&gt;
+</programlisting>
+
+ <para>This is a Cockpit component that brings up a web-based terminal for
+ the logged in user.</para>
+
+ <variablelist>
+ <varlistentry>
+ <term>Component URL</term>
+ <listitem><para><code>/cockpit+app/@localhost/system/terminal.html</code></para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ </refsection>
+ </refentry>
+
+
+</reference>
diff --git a/doc/guide/authentication.xml b/doc/guide/authentication.xml
new file mode 100644
index 0000000..e3c8d6a
--- /dev/null
+++ b/doc/guide/authentication.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="authentication">
+ <title>Cockpit Authentication</title>
+
+ <para>While cockpit allows you to monitor and administer several servers at the
+ same time, there is always a primary server your browser connects to
+ that runs the Cockpit web service (cockpit-ws) through which connections to
+ additional servers are established.
+ See <ulink url="https://raw.githubusercontent.com/cockpit-project/cockpit/main/doc/cockpit-transport.png">this diagram</ulink> for how it works.</para>
+
+ <para>Normally, a session is established on the primary server,
+ and you use the Shell UI of that session to connect to secondary
+ servers.</para>
+
+ <para>However, it is also possible to instruct the
+ <filename>cockpit-ws</filename> process on the primary server to
+ directly connect to a secondary server, without opening a
+ session on the primary server at all. This is done on the main
+ login page of Cockpit, by filling out the "Connect to"
+ field.</para>
+
+ <section id="initial-auth">
+ <title>Directly logging into the primary server</title>
+
+ <para>The most common way to use Cockpit is to just log directly
+ into the server that you want to access. This can be done if you
+ have direct network access to port 9090 on that server.</para>
+
+ <para>By default the cockpit web service is installed on the base system and
+ <link linkend="listen">socket activated by systemd</link>. In this setup
+ access is controlled by a cockpit specific pam stack, generally located
+ at <filename>/etc/pam.d/cockpit</filename>. By default this is configured
+ to allow you to login with the username and password of any local account on the
+ system. You can also setup a <link linkend="sso">Kerberos based SSO
+ solution</link> or <link linkend="cert-authentication">certificate/smart
+ card authentication</link>.
+ </para>
+
+ <para>You can also <ulink url="https://github.com/cockpit-project/cockpit/blob/main/doc/authentication.md#actions">disable
+ authentication schemes</ulink> to enforce authentication policies, or to suppress
+ undesired browser GSSAPI authentication dialogs.</para>
+
+ <para>The web server can also be run from the
+ <ulink url="https://hub.docker.com/r/cockpit/ws/">cockpit/ws</ulink>
+ container. If you are running cockpit on a container host operating system like
+ <ulink url="https://getfedora.org/coreos/">Fedora CoreOS</ulink>
+ this will be the only supported mode. In this setup, cockpit establishes an
+ SSH connection from the container to the underlying host, meaning that it is up to
+ your SSH server to grant access. To login with a local account, <filename>sshd
+ </filename> will need to be configured to allow password based authentication.
+ Alternatively you can setup a <link linkend="sso">Kerberos based SSO
+ solution</link>.</para>
+
+ <para>Like <filename>sshd</filename>, cockpit can be configured to limit the number
+ of concurrent login attempts allowed. This is done by adding a <code>MaxStartups</code>
+ option to the <code>WebService</code> section of your <code>cockpit.conf</code>.
+ Additional connections will be dropped until authentication succeeds or
+ the connections are closed.</para>
+
+ </section>
+
+ <section id="secondary-auth">
+ <title>Logging into a secondary server from the primary session</title>
+
+ <para>Once you have a session on the primary server you will be
+ able to connect to additional servers by using the host switching
+ UI of the Cockpit Shell. This is useful if you have direct network
+ access to the primary server, but not to the secondary server.</para>
+
+ <para>On the command line, you would log into the primary server
+ and then use SSH to log into the secondary one. Cockpit does just
+ the same, and uses SSH to log into the secondary server. Instead
+ of running a interactive shell there, however, it starts a
+ <filename>cockpit-bridge</filename> process.</para>
+
+ <para>Thus, these servers will need to be running an SSH server on
+ port 22 and be configured to support one of the following
+ authentication methods.</para>
+
+ <section id="password">
+ <title>Password</title>
+ <para>The target server will need to have password based authentication
+ enabled in <filename>sshd</filename>.</para>
+ </section>
+
+ <section id="kerberos">
+ <title>Kerberos</title>
+ <para>The target server will need to be a member of the same domain as the
+ primary server and your domain must be whitelisted in your browser.
+ See the <link linkend="sso">SSO documentation</link> for how to set
+ this up.</para>
+ </section>
+
+ <section id="public-key">
+ <title>Public key</title>
+
+ <para>When you successfully log into the primary server, a
+ <filename>ssh-agent</filename> is started and keys are loaded into
+ it by running <filename>ssh-add</filename> without any arguments.
+ Any passphrase prompt is answered with the password used to log
+ into the primary server.</para>
+
+ <para>Cockpit provides a user interface for loading other keys into the agent
+ that could not be automatically loaded.</para>
+
+ <para>The target server will need to have public key
+ authentication enabled in <filename>sshd</filename>, and the
+ public key you wish to use must be present in
+ <filename>~/.ssh/authorized_keys</filename>. Cockpit has a user
+ interface for creating SSH keys and for authorizing them.</para>
+
+ </section>
+
+ <section id="host-keys">
+ <title>SSH host keys</title>
+ <para>Cockpit will prompt the user to verify unknown SSH host
+ keys, and will write accepted host keys into
+ <filename>~/.ssh/known_hosts</filename>.</para>
+ </section>
+ </section>
+
+ <section id="direct-secondary-auth">
+ <title>Directly logging into a secondary server without a primary session</title>
+
+ <para>It is also possible to log into a secondary server without
+ opening a session on the primary server. This is useful if you
+ are not actually interested in the primary server and would only
+ use it because you do not have direct network access to the
+ secondary server.</para>
+
+ <para>In this case, <filename>cockpit-ws</filename> still runs on
+ the primary server, but the credentials from the login screen are
+ directly used with SSH to log into the secondary server given in
+ the "Connect To" field of the login screen.</para>
+
+ <para>Thus, the PAM configuration and accounts on the primary
+ server don't matter at all. Often, the only purpose of the primary
+ server is to sit on the boundary of your network and forward
+ connections to internal machines.</para>
+
+ <para>In this case, the login page will prompt you to verify
+ unknown SSH keys. Accepted keys will be remembered in the local
+ storage of your browser.</para>
+ </section>
+
+</chapter>
diff --git a/doc/guide/cert-authentication.xml b/doc/guide/cert-authentication.xml
new file mode 100644
index 0000000..ff09754
--- /dev/null
+++ b/doc/guide/cert-authentication.xml
@@ -0,0 +1,296 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="cert-authentication">
+ <title>Certificate/smart card authentication</title>
+
+ <para>
+ Cockpit can use TLS client certificates for authenticating users. Commonly
+ these are provided by a smart card, but it's equally possible to import
+ certificates directly into the web browser.
+ </para>
+
+ <para>
+ This requires the host to be in an Identity Management domain like
+ <ulink url="https://www.freeipa.org">FreeIPA</ulink> or
+ <ulink url="https://en.wikipedia.org/wiki/Active_Directory">Active Directory</ulink>,
+ which can associate certificates to users.
+ </para>
+
+ <para>To authenticate users from a Identity Management domain, the server that
+ Cockpit is running on must be joined to that domain. See the
+ <link linkend="sso-server">SSO server requirements</link> for details.</para>
+
+ <section id="certauth-server-cert-generation">
+ <title>User certificate generation</title>
+ <para>Generating the certificates for users is usually done with a certificate management system like
+ <ulink url="https://pagure.io/certmonger">certmonger</ulink> or
+ <ulink url="https://www.freeipa.org/page/PKI">FreeIPA</ulink>, which are not documented here.
+ This command generates a simple key and certificate request for the "alice" user:</para>
+
+<programlisting>
+openssl req -new -newkey rsa:2048 -days 365 \
+ -keyout alice.key -out alice.csr -subj "/CN=alice"
+</programlisting>
+
+ <para>Now get this certificate request signed by the Certificate Authority of your Identity
+ Management domain, to get a PEM certificate. Browsers and smart cart utilities accept PKCS#12 format
+ for importing/transfer, so convert the certificate/key pair; it will ask for and protect it
+ with a transfer password:</para>
+
+<programlisting>
+openssl pkcs12 -export -in alice.pem -inkey alice.key -out alice.p12
+</programlisting>
+
+ <para>Don't forget to clean up the key file when you do not need it any more:</para>
+
+<programlisting>
+shred -u alice.key
+</programlisting>
+
+ <para>You can now import <code>alice.p12</code> directly into your browser,
+ with giving the transfer password set above. Or
+ <ulink url="https://linux.die.net/man/1/pkcs15-init">put the certificate onto a smart card</ulink>:</para>
+
+<programlisting>
+pkcs15-init --store-private-key alice.p12 --format pkcs12 --auth-id 01
+</programlisting>
+
+ </section>
+
+ <section id="certauth-server-ipa">
+ <title>Certificate mapping with FreeIPA</title>
+ <para>The recommended method to sign a user certificate request and associate it to a user is
+ <command>ipa cert-request</command>: </para>
+
+<programlisting>
+ipa cert-request alice.csr --principal=alice --certificate-out=alice.pem
+</programlisting>
+
+ <para>Alternatively, if you are using a different CA, you can use
+ <command>ipa user-add-cert</command> to associate the signed certificate to the user.
+ This expects PEM format, but without the <code>-----BEGIN</code>/<code>-----END</code>
+ markers:</para>
+
+<programlisting>
+ipa user-add-cert alice --certificate="$(grep -v ^---- alice.pem)"
+</programlisting>
+
+ <para>See the <ulink url="https://www.freeipa.org/page/V4/User_Certificates#Feature_Management">
+ FreeIPA User Certificates documentation</ulink> for details.</para>
+
+ </section>
+
+ <section id="certauth-server-ms-ad">
+ <title>Certificate mapping with Microsoft Active Directory</title>
+
+ <para>The domain user certificates get imported into the <code>userCertificate;binary</code>
+ LDAP attribute. The following commands convert the PEM certificate into binary DER form, create an
+ <ulink url="https://ldap.com/ldif-the-ldap-data-interchange-format/">LDIF</ulink>
+ file and apply it to the LDAP server running on the domain controller
+ "dc.example.com":</para>
+
+<programlisting>
+openssl x509 -outform der -in alice.pem -out alice.der
+
+cat &lt;&lt;EOF &gt; alice.ldif
+version: 1
+dn: cn=alice,ou=users,ou=YOUR_NETBIOS_NAME,dc=example,dc=com
+changetype: modify
+add: userCertificate;binary
+userCertificate;binary:&lt; file://$(pwd)/alice.der
+EOF
+
+ldapmodify -H ldap://dc.example.com -f alice.ldif
+</programlisting>
+
+ </section>
+
+ <section id="certauth-server-samba-ad">
+ <title>Certificate mapping with Samba Active Directory</title>
+
+ <para>At least some versions of <ulink url="https://www.samba.org/">Samba</ulink>
+ do not support the <code>userCertificate;binary</code> LDAP attribute, so the
+ import has to happen in base64 PEM form into the textual
+ <code>userCertificate</code> attribute instead. Also, Samba uses a slightly
+ different user hierarchy:</para>
+
+<programlisting>
+cat &lt;&lt;EOF &gt; alice.ldif
+version: 1
+dn: cn=alice,cn=users,dc=example,dc=com
+changetype: modify
+add: userCertificate
+userCertificate: $(grep -v ^---- alice.pem | tr -d '\n')
+EOF
+
+ldapmodify -H ldap://dc.example.com -f alice.ldif
+</programlisting>
+
+ <para>As <code>userCertificate</code> is a text instead of binary field, you need to set up a
+ <ulink url="https://www.mankier.com/5/sssd.conf#Certificate_Mapping_Section">certificate mapping rule</ulink>
+ in <citerefentry><refentrytitle>sssd.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+ in a <code>[certmap/domain/rulename]</code> section, for example:</para>
+
+<programlisting>
+[certmap/example.com/adcerts]
+# we match full certificates, so it is not important to check anything here
+matchrule = &lt;KU&gt;digitalSignature
+maprule = LDAP:(userCertificate={cert!base64})
+</programlisting>
+
+ </section>
+
+ <section id="certauth-server-cockpitconf">
+ <title>Cockpit web server configuration</title>
+
+ <para>Set the trusted Certificate Authority of your user certificates in <command>sssd</command>,
+ either by copying the CA PEM file to <code>/etc/sssd/pki/sssd_auth_ca_db.pem</code> or setting the
+ <ulink url="https://www.mankier.com/5/sssd.conf#Services_Sections-PAM_configuration_options">
+ <command>pam_cert_db_path</command></ulink> configuration option to the path of the CA.
+ If you use FreeIPA and its CA:</para>
+
+<programlisting>
+cp /etc/ipa/ca.crt /etc/sssd/pki/sssd_auth_ca_db.pem
+</programlisting>
+
+ <para>Certificate authentication needs to be enabled in
+ <ulink url="./cockpit.conf.5.html">cockpit.conf</ulink> explicitly:</para>
+
+<programlisting>
+[WebService]
+ClientCertAuthentication = yes
+</programlisting>
+
+ <para>When enabling this mode,
+ <ulink url="https://github.com/cockpit-project/cockpit/blob/main/doc/authentication.md">
+ other authentication types</ulink> commonly get disabled, so that <emphasis>only</emphasis>
+ client certificate authentication will be accepted. By default, after a failed certificate
+ authentication attempt, Cockpit's normal login page will appear and permit other login types
+ such as <code>basic</code> (passwords) or <code>negotiate</code> (Kerberos). For example,
+ password authentication gets disabled with:</para>
+
+<programlisting>
+[basic]
+action = none
+</programlisting>
+
+ </section>
+
+ <section id="certauth-server-resourcelimits">
+ <title>Cockpit web server resource limits</title>
+
+ <para>When using certificate authentication, all requests with a particular
+ certificate will be handled by a separate and isolated instance of the
+ <ulink url="./cockpit-ws.8.html">cockpit-ws</ulink> web server. This
+ protects against possible vulnerabilities in the web server and prevents
+ an attacker from impersonating another user. However, this introduces a
+ potential Denial of Service: Some remote attacker could create a
+ large number of certificates and send a large number of http requests
+ to Cockpit with these.</para>
+
+ <para>To mitigate that, all <code>cockpit-ws</code> instances run
+ in a <code>system-cockpithttps.slice</code>
+ <ulink url="https://www.freedesktop.org/software/systemd/man/systemd.slice.html">systemd slice unit</ulink>
+ which <ulink url="https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html">limits
+ the collective resources</ulink> of these web server instances: by default,
+ this slice sets a limit of 200 threads (roughly 100 instances of <code>cockpit-ws</code> -- in other
+ words, a maximum of 100 parallel user sessions with different certificates) and
+ a 75% (soft)/90% (hard) memory limit.</para>
+
+ <para>You are welcome to adjust these limits to your need through
+ a <ulink url="https://www.freedesktop.org/software/systemd/man/systemd.unit.html">drop-in</ulink>.
+ For example:</para>
+
+<programlisting>
+# systemctl edit system-cockpithttps.slice
+
+[Slice]
+# change existing value
+TasksMax=100
+# add new restriction
+CPUQuota=30%
+</programlisting>
+
+ </section>
+
+ <section id="certauth-forwarding">
+ <title>Authentication to other services like sudo and ssh</title>
+
+ <para>Once you logged into Cockpit with a certificate, you likely need to switch to administrative mode
+ (root privileges through sudo), or connect to remote machines through SSH. If your user account has a password,
+ that can be used for authenticating to sudo or ssh as usual.</para>
+
+ <para><emphasis>Supported with FreeIPA only:</emphasis> As an alternative to password authentication, you can
+ also declare the initial Cockpit certificate authentication as trusted for authenticating to SSH,
+ sudo, or other services. For that purpose, Cockpit automatically creates an
+ <ulink url="https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/bde93b0e-f3c9-4ddf-9f44-e1453be7af5a">S4U2Proxy Kerberos ticket</ulink>
+ in the user session:
+ </para>
+
+<programlisting>
+$ klist
+Ticket cache: FILE:/run/user/1894000001/cockpit-session-3692.ccache
+Default principal: user@EXAMPLE.COM
+
+Valid starting Expires Service principal
+07/30/21 09:19:06 07/31/21 09:19:06 HTTP/myhost.example.com@EXAMPLE.COM
+07/30/21 09:19:06 07/31/21 09:19:06 krbtgt/EXAMPLE.COM@EXAMPLE.COM
+ for client HTTP/myhost.example.com@EXAMPLE.COM
+</programlisting>
+
+ <para>You can set up <ulink url="https://www.freeipa.org/page/V4/Service_Constraint_Delegation">constrained delegation rules</ulink>
+ to enumerate which hosts (including its own) that ticket is trusted to access. For example, if the cockpit session runs on host
+ <code>myhost.example.com</code> and should be trusted to access its own host (through sudo) and another host
+ <code>remote.example.com</code> (through ssh), create a delegation like this:
+ </para>
+
+<programlisting>
+# a list of target machines which can be accessed by a particular rule
+ipa servicedelegationtarget-add cockpit-target
+ipa servicedelegationtarget-add-member cockpit-target \
+ --principals=host/myhost.example.com@EXAMPLE.COM \
+ --principals=host/remote.example.com@EXAMPLE.COM
+
+# allow cockpit sessions (HTTP/ principal) to access that host list
+ipa servicedelegationrule-add cockpit-delegation
+ipa servicedelegationrule-add-member cockpit-delegation \
+ --principals=HTTP/myhost.example.com@EXAMPLE.COM
+ipa servicedelegationrule-add-target cockpit-delegation \
+ --servicedelegationtargets=cockpit-target
+</programlisting>
+
+ <para>In addition, you need to enable GSS (Kerberos) authentication in the corresponding services.</para>
+
+ <itemizedlist>
+ <listitem>
+ <para>For SSH, enable <code>GSSAPIAuthentication yes</code> in
+ <ulink url="https://linux.die.net/man/5/sshd_config">/etc/ssh/sshd_config</ulink>.</para>
+ </listitem>
+
+ <listitem>
+ <para>For sudo, enable <code>pam_sss_gss</code> as described in the
+ <ulink url="https://www.mankier.com/8/pam_sss_gss">manpage</ulink>:
+ In <code>/etc/sssd/sssd.conf</code>: Add an entry for your domain:</para>
+
+<programlisting>
+[domain/example.com]
+pam_gssapi_services = sudo, sudo-i
+</programlisting>
+
+ <para>In <code>/etc/pam.d/sudo</code>, enable the module in the first line:</para>
+
+<programlisting>
+auth sufficient pam_sss_gss.so
+</programlisting>
+
+ </listitem>
+ </itemizedlist>
+
+ <para><emphasis>Caveat:</emphasis> The delegated S4U ticket is not yet forwarded to remote SSH
+ hosts when connecting to them from Cockpit, so authenticating to sudo on the remote host with
+ that ticket does not work. This will be provided in a future version.</para>
+
+ </section>
+
+</chapter>
diff --git a/doc/guide/cockpit-cache.xml b/doc/guide/cockpit-cache.xml
new file mode 100644
index 0000000..7e891e8
--- /dev/null
+++ b/doc/guide/cockpit-cache.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-cache">
+ <refnamediv>
+ <refname>cockpit.js: Object Cache</refname>
+ <refpurpose>Caching and sharing data</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <para>If the same information is displayed by multiple components in Cockpit,
+ <code>cockpit.cache()</code> provides a way to share data between them. The shared
+ data should be simple objects, arrays, and values, and not contain functions or
+ other objects.</para>
+</refsynopsisdiv>
+
+ <refsection id="cockpit-cache-func">
+ <title>cockpit.cache()</title>
+<programlisting>
+cache = cockpit.cache(key, provider, consumer)
+</programlisting>
+
+ <para>Create a new cache object. The <code>key</code> should be a globally unique string
+ that describes the data being cached. This string must describe the data, across all
+ machines and all versions of cockpit. It is customary to include a version number in
+ the <code>key</code> string.</para>
+
+<programlisting>
+function provider(result, key) {
+ result("myvalue");
+
+ return {
+ close: function() {
+ /* closed */
+ }
+ };
+}
+</programlisting>
+
+ <para>The <code>provider</code> is a function that will be invoked to start retrieving
+ data for the cache. It will be passed a <code>result</code> function as its first
+ argument. The <code>result</code> should be invoked whenever new data is available.
+ The <code>key</code> argument matches the key string the cache was created with.</para>
+
+ <para>The <code>provider</code> can return an object with a <code>close</code> method.
+ This method will be invoked when the cache no longer needs data from the provider.</para>
+
+<programlisting>
+function consumer(value, key) {
+ /* ... */
+}
+</programlisting>
+
+ <para>The <code>consumer</code> is a function that will be passed new values when they
+ are available, whether they come from the <code>provider</code> or a source in a
+ different component/frame.</para>
+
+ </refsection>
+
+ <refsection id="cockpit-cache-close">
+ <title>cache.close()</title>
+<programlisting>
+cache.close()
+</programlisting>
+
+ <para>Close a cache and stop calling its <code>consumer</code>. If the <code>provider</code>
+ was invoked, then the <code>close()</code> method it returned will be invoked.</para>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/cockpit-channel.xml b/doc/guide/cockpit-channel.xml
new file mode 100644
index 0000000..314d4ad
--- /dev/null
+++ b/doc/guide/cockpit-channel.xml
@@ -0,0 +1,351 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-channels">
+ <refnamediv>
+ <refname>cockpit.js: Raw Channels</refname>
+ <refpurpose>Raw communication channels</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <para>At a low level Cockpit communicates with the system via messages passed
+ through various channels. These are usually exposed via higher level APIs,
+ such as the <code><link linkend="cockpit-spawn">cockpit.spawn()</link></code> function.
+ It is rare to use raw channels directly.</para>
+ </refsynopsisdiv>
+
+ <refsection id="cockpit-channels-channel">
+ <title>cockpit.channel()</title>
+<programlisting>
+channel = cockpit.channel(options)
+</programlisting>
+
+ <para>This function creates a new channel for communication with the system.
+ It returns a new channel object. The <code>options</code> argument is a
+ plain object. At least the <code>"payload"</code> option is required, and
+ based on the payload type, other options may be required.</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>"binary"</code></term>
+ <listitem><para>Set to <code>true</code> to transfer binary payloads. Both messages
+ sent via <link linkend="cockpit-channels-send"><code>channel.send()</code></link>
+ and those received via
+ <link linkend="cockpit-channels-message"><code>channel.onmessage</code></link>
+ should be arrays of bytes, either <code>Uint8Array</code> or <code>Array</code>
+ depending on browser support.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"payload"</code></term>
+ <listitem><para>The payload type for the channel. Only specific payload
+ types are supported.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"superuser"</code></term>
+ <listitem><para>Set to <code>"require"</code> to open this channel as root. If the currently
+ logged in user is not permitted to become root (eg: via <code>pkexec</code>) then the
+ <code>channel</code> will immediately be
+ <link linkend="cockpit-channels-close-ev">closed</link> with a <code>"access-denied"</code>
+ problem code.</para>
+ <para>Set to <code>"try"</code> to try to open the channel as root, but if that fails,
+ then fall back to an unprivileged channel.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>The channel object returned has the following fields and methods and
+ events. You should call the
+ <code><link linkend="cockpit-channels-close">channel.close()</link></code>
+ method when done with the channel.</para>
+
+ <para>A valid channel will always be returned and the is ready to
+ <code><link linkend="cockpit-channels-send">channel.send()</link></code>. The channel may
+ <link linkend="cockpit-channels-close-ev">close shortly afterword</link> due
+ to a failure.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-binary">
+ <title>channel.binary</title>
+ <para>Will be <code>true</code> for an binary channel. Will be set to <code>false</code> if the
+ channel is textual.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-options">
+ <title>channel.options</title>
+ <para>The options used to open this channel. This should not be changed.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-valid">
+ <title>channel.valid</title>
+ <para>Will be <code>true</code> for an open channel. Will be set to <code>false</code> if the channel
+ closes.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-send">
+ <title>channel.send()</title>
+<programlisting>
+channel.send(data)
+</programlisting>
+ <para>Send a message over the channel. The contents of the message depends on the
+ payload type of the channel. If a binary channel, then <code>data</code> is expected
+ to be an <code>Array</code> of bytes or a <code>Uint8Array</code>. If not binary,
+ then the <code>data</code> will be converted to a string if not already a string.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-control">
+ <title>channel.control()</title>
+<programlisting>
+channel.control(options)
+</programlisting>
+ <para>Notify the channel to tune certain parameters on the fly. The <code>options</code>
+ is a plain javascript object, and the contents depend on the <code>"payload"</code>
+ of the channel.</para>
+ <para>One common operation is to set <code>"command"</code> to <code>"done"</code> in the
+ options field. To indicate that no further messages will be sent through the channel.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-wait">
+ <title>channel.wait()</title>
+<programlisting>
+promise = channel.wait([callback])
+</programlisting>
+
+ <para>Returns a <code>promise</code> that is ready when the channel is ready, or fails if the
+ client closes. If a <code>callback</code> is specified, it is attached to the promise. The
+ promise will be rejected or resolved with the contents <code>options</code> passed to the
+ <link linkend="cockpit-channels-onready">channel.onready</link> and
+ <link linkend="cockpit-channels-close-ev">channel.onclose</link> events respectively.</para>
+ <para>In general it's not necessary to wait for the channel before starting to use the channel.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-close">
+ <title>channel.close()</title>
+<programlisting>
+channel.close([options])
+</programlisting>
+ <para>Close the channel.</para>
+ <para>If <code>options</code> is present it can be a plain javascript object
+ containing additional channel close options to send to the peer. If closing for
+ because of a problem, set the <code>"problem"</code> field to a
+ <link linkend="cockpit-problems">problem code</link>. If <code>options</code>
+ is not an object it will be treated as a <code>"problem"</code>.</para>
+ <para>The <link linkend="cockpit-channels-close-ev">close event</link> will fire.
+ A channel can also be closed by a peer or if the underlying transport closes.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-message">
+ <title>channel.onmessage</title>
+<programlisting>
+channel.addEventListener("message", function(event, data) { ... })
+</programlisting>
+ <para>An event triggered when the channel receives a message. The message is
+ passed as a string to the handler in the <code>data</code>. In the case of binary
+ channels <code>data</code> is an <code>Uint8Array</code> or an <code>Array</code>
+ of bytes if the former is not supported by the browser. The contents of
+ the message depends on the payload type of the channel.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-oncontrol">
+ <title>channel.oncontrol</title>
+<programlisting>
+channel.addEventListener("control", function(event, options) { ... })
+</programlisting>
+ <para>An event triggered when the channel receives an control message in the
+ middle of the flow. One particular use is when the <code>command</code> is set to
+ <code>"done"</code> then no further messages will be received in the channel.
+ The exact form of these messages depend on the <code>"payload"</code> of the
+ channel.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-onready">
+ <title>channel.onready</title>
+<programlisting>
+channel.addEventListener("ready", function(event, options) { ... })
+</programlisting>
+ <para>An event triggered when the other end of the channel is ready to start processing
+ messages. This indicates the channel is completely open. It is possible to start
+ sending messages on the channel before this point.</para>
+ </refsection>
+
+ <refsection id="cockpit-channels-close-ev">
+ <title>channel.onclose</title>
+<programlisting>
+channel.addEventListener("close", function(event, options) { ... })
+</programlisting>
+ <para>An event triggered when the channel closes. This can happen either because
+ <link linkend="cockpit-channels-close">channel.close()</link> function was called,
+ or if the peer closed the channel, or the underlying transport closes.</para>
+ <para>The <code>options</code> will contain various close information, including a
+ <code>"problem"</code> field which will be set if the channel was closed because
+ of a problem.</para>
+ </refsection>
+
+ <refsection id="cockpit-transport-origin">
+ <title>cockpit.transport.origin</title>
+<programlisting>
+cockpit.transport.origin
+</programlisting>
+ <para>The HTTP origin that is being used by the underlying channel transport. This is
+ read-only, you should not assign a value. If the browser supports
+ <code>window.location.origin</code> then this will be identical to that value.</para>
+ </refsection>
+
+ <refsection id="cockpit-transport-host">
+ <title>cockpit.transport.host</title>
+<programlisting>
+cockpit.transport.host
+</programlisting>
+ <para>The host that this transport is going to talk to by default. This is
+ read-only, you should not assign a value. If the value is null that means that the
+ transport has not been setup yet.</para>
+ </refsection>
+
+ <refsection id="cockpit-transport-csrf-token">
+ <title>cockpit.transport.csrf_token</title>
+<programlisting>
+cockpit.transport.csrf_token
+</programlisting>
+ <para>A cross site request forgery token for use with external channels. This becomes
+ valid once the connection is properly established.</para>
+ </refsection>
+
+ <refsection id="cockpit-transport-options">
+ <title>cockpit.transport.options</title>
+<programlisting>
+cockpit.transport.options
+</programlisting>
+ <para>Initialization options received over the underlying channel transport. These
+ will be empty until connection is properly established.</para>
+ </refsection>
+
+ <refsection id="cockpit-transport-wait">
+ <title>cockpit.transport.wait()</title>
+<programlisting>
+cockpit.transport.wait(callback)
+</programlisting>
+ <para>Call the <code>callback</code> function once the underlying channel transport is initialized.
+ This will start the initialization if not already in progress or completed. If the
+ channel transport is already initialized, then <code>callback</code> will be called
+ immediately.</para>
+ <para>In general it's not necessary to wait for the transport before starting to open channels.</para>
+ </refsection>
+
+ <refsection id="cockpit-transport-close">
+ <title>cockpit.transport.close()</title>
+<programlisting>
+cockpit.transport.close([problem])
+</programlisting>
+ <para>Close the underlying channel transport. All channels open channels will close.
+ The <code>problem</code> argument should be a problem code string. If not specified
+ it will default to <code>"disconnected"</code>.</para>
+ </refsection>
+
+ <refsection id="cockpit-transport-filter">
+ <title>cockpit.transport.filter()</title>
+<programlisting>
+cockpit.transport.filter((message, channelid, control) => { ... }, [out])
+</programlisting>
+ <para>Add a filter to the underlying channel transport. All incoming messages will be
+ passed to each of the filter callbacks that are registered. If the <code>out</code>
+ argument is equal to <code>true</code> then the filter will receive outgoing messages
+ that being sent on the underlying channel transport.</para>
+ <para>This function is rarely used.</para>
+ <para>Filter callbacks are called in the order they are registered. If a filter
+ callback returns <code>false</code> then the message will not be dispatched
+ further, whether to other filters, or to channels, etc.</para>
+
+ <para>The <code>message</code> is the string or array with the raw message including,
+ the framing. The <code>channelid</code> is the channel identifier or an empty string
+ for control messages. If <code>control</code> is set then this is a control message,d
+ and the <code>control</code> argument contains the parsed JSON object of the
+ control message.</para>
+ </refsection>
+
+ <refsection id="cockpit-transport-inject">
+ <title>cockpit.transport.inject()</title>
+<programlisting>
+cockpit.transport.inject(message, [out])
+</programlisting>
+ <para>Inject a message into the underlying channel transport. The <code>message</code>
+ should be a <code>string</code> or an array of bytes, and should be valid
+ according to the Cockpit message protocol. If the <code>out</code> argument is equal
+ to <code>false</code> then the message will be injected as an incoming message as if
+ it was received on the underlying channel transport.</para>
+ <para>This function is rarely used. In general you should only <code>inject()</code>
+ messages you got from a <code><link linkend="cockpit-transport-filter">filter()</link></code>.</para>
+ </refsection>
+
+ <refsection id="cockpit-base64-encode">
+ <title>cockpit.base64_encode()</title>
+<programlisting>
+string = cockpit.base64_encode(data)
+</programlisting>
+ <para>Encode binary data into a string using the Base64 encoding. The <code>data</code>
+ argument can either be a <code>string</code>, an <code>Array</code>, an <code>ArrayBuffer</code>
+ or a <code>Uint8Array</code>. The return value is a string.</para>
+ </refsection>
+
+ <refsection id="cockpit-base64-decode">
+ <title>cockpit.base64_decode()</title>
+<programlisting>
+data = cockpit.base64_decode(string, [constructor])
+</programlisting>
+ <para>Decode binary data from a Base64 encoded string. The <code>string</code>
+ argument should be a javascript string. The returned <code>data</code>> will be an
+ array of bytes.</para>
+ <para>You can pass <code>Uint8Array</code>, <code>Array</code> or <code>String</code>
+ as an alternate <code>constructor</code> if you want the decoded data in an
+ alternate form. The default is to return an <code>Array</code>. Note that if you use a
+ <code>String</code> for the decoded data, then you must guarantee that the data
+ does not contain bytes that would be invalid for a string.</para>
+ </refsection>
+
+ <refsection id="cockpit-utf8-encoder">
+ <title>cockpit.utf8_encoder()</title>
+<programlisting>
+encoder = cockpit.utf8_encoder([constructor])
+</programlisting>
+ <para>Create an encoder for encoding a string into a UTF8 sequence of bytes.</para>
+ <para>You can pass <code>Uint8Array</code>, <code>Array</code> or <code>String</code>
+ as an alternate <code>constructor</code> if you want the decoded data in an
+ alternate form. The default is to return an <code>Array</code>.</para>
+ </refsection>
+
+ <refsection id="cockpit-utf8-encoder-encode">
+ <title>encoder.encode()</title>
+<programlisting>
+data = encoder.encode(string)
+</programlisting>
+ <para>Encode a <code>string</code> into a UTF8 sequence of bytes.</para>
+ <para>The resulting <code>data</code> is an array of bytes, but it's type may be
+ modified by passing an alternate <code>constructor</code> to
+ <link linkend="cockpit-utf8-encoder">cockpit.utf8_encoder()</link>.</para>
+ </refsection>
+
+ <refsection id="cockpit-utf8-decoder">
+ <title>cockpit.utf8_decoder()</title>
+<programlisting>
+decoder = cockpit.utf8_decoder([fatal])
+</programlisting>
+ <para>Creates a decoder to decode a UTF8 sequence of bytes data into a string.</para>
+ <para>If the <code>fatal</code> is set to <code>true</code> then the <code>decoder</code>
+ will throw an exception when it encounters invalid UTF8 data. By default invalid data
+ will be substituted with special UTF8 characters.</para>
+ </refsection>
+
+ <refsection id="cockpit-utf8-decoder-decode">
+ <title>decoder.decode()</title>
+<programlisting>
+string = decoder.decode(data, [options])
+</programlisting>
+ <para>Decode an array of UTF8 bytes into a <code>string</code>. The <code>data</code>
+ argument may be an <code>Array</code>, a <code>Uint8Array</code> or a string containing
+ binary data.</para>
+ <para>If <code>options</code> is passed it should be a plain javascript object. If
+ <code>options</code> has a <code>stream</code> property equal to <code>true</code>,
+ then multiple invocations of this function can be made with parts of the UTF8 sequence
+ of bytes. Any trailing bytes that don't yet build a complete unicode character, will be
+ cached until the next invocation. To drain the last data, call this function without
+ the <code>stream</code> property set.</para>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/cockpit-dbus.xml b/doc/guide/cockpit-dbus.xml
new file mode 100644
index 0000000..f92567e
--- /dev/null
+++ b/doc/guide/cockpit-dbus.xml
@@ -0,0 +1,909 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-dbus">
+ <refnamediv>
+ <refname>cockpit.js: DBus Client</refname>
+ <refpurpose>DBus API communication</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <para>Cockpit allows access to DBus services via this API.</para>
+ </refsynopsisdiv>
+
+ <refsection id="cockpit-dbus-types">
+ <title>DBus Types</title>
+ <para>DBus values are represented as javascript values and objects as follows:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>BYTE 'y'</code></term>
+ <listitem><para>Javascript number.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>BOOLEAN 'b'</code></term>
+ <listitem><para>Javascript boolean.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>INT16 'n'</code></term>
+ <listitem><para>Javascript number.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>UINT16 'q'</code></term>
+ <listitem><para>Javascript number.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>INT32 'i'</code></term>
+ <listitem><para>Javascript number.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>UINT32 'u'</code></term>
+ <listitem><para>Javascript number.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>INT64 'x'</code></term>
+ <listitem><para>Javascript number.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>UINT64 't'</code></term>
+ <listitem><para>Javascript number.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>DOUBLE 'd'</code></term>
+ <listitem><para>Javascript number.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>STRING 's'</code></term>
+ <listitem><para>Javascript string.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>OBJECT_PATH 'o'</code></term>
+ <listitem><para>Javascript string.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>SIGNATURE 'g'</code></term>
+ <listitem><para>Javascript string.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>ARRAY of BYTE 'ay'</code></term>
+ <listitem><para>A string containing base64 encoded data.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>ARRAY of DICT_ENTRY with STRING keys 'a{s?}'</code></term>
+ <listitem><para>A javascript plain object with the keys as property names.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>ARRAY of DICT_ENTRY with other keys 'a{??}'</code></term>
+ <listitem><para>A javascript plain object each key JSON encoded into a string property
+ name.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>ARRAY of other</code></term>
+ <listitem><para>A javascript array.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>VARIANT</code></term>
+ <listitem><para>A javascript plain object with the <code>"t"</code> property set to a DBus type string,
+ and the <code>"v"</code> property set to a value.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>HANDLE 'h'</code></term>
+ <listitem><para>A javascript object that describes a cockpit channel which represents the
+ passed file descriptor. The <code>payload</code> is always set to <code>stream</code>.
+ Pass it to <link linkend="cockpit-channels-channel">cockpit.channel()</link> to create
+ the channel and start reading or writing on it. Handles can only be
+ received, not sent from within cockpit.</para></listitem>
+ </varlistentry>
+ </variablelist>
+ </refsection>
+
+ <refsection id="cockpit-dbus-dbus">
+ <title>cockpit.dbus()</title>
+<programlisting>
+client = cockpit.dbus(name, [options])
+</programlisting>
+
+ <para>Create a DBus client for the given bus <code>name</code> (eg: service name). Use the
+ following functions to make DBus method calls, watch for events, etc. The optional
+ <code>options</code> argument is a javascript plain object, and may include:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>"bus"</code></term>
+ <listitem><para>The DBus bus to connect to. Specifying <code>"session"</code> will
+ connect to the DBus user session bus, <code>"user"</code> will connect to the
+ user bus (on some systems this is identical to the session bus), <code>"system"</code>
+ will connect to the DBus system bus, and <code>"none"</code> to the non-standard bus
+ specified with the <code>address</code> option. This defaults to "system" if not
+ present.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"address"</code></term>
+ <listitem><para>The bus address to connect to in case <code>bus</code> is
+ <code>"none"</code>.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"superuser"</code></term>
+ <listitem><para>Set to <code>"require"</code> to talk to this service as root.
+ The DBus service will see the DBus method calls and accesses as coming from root,
+ rather than the logged in user. This is useful for talking to services that do not
+ correctly use <ulink url="https://www.freedesktop.org/software/polkit">polkit</ulink>
+ to authorize administrative users. If the currently logged in user is not
+ permitted to become root (eg: via <code>pkexec</code>) then the <code>client</code> will
+ immediately be <link linkend="cockpit-dbus-onclose">closed</link> with a
+ <code>"access-denied"</code> problem code.</para>
+ <para>Set to <code>"try"</code> to try to talk as root, but if that fails,
+ fall back to unprivileged.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"track"</code></term>
+ <listitem><para>It is valid for a DBus service to exit, and be restarted in such a
+ way that clients continue to talk to it across the restart. Some services are not
+ written with this in mind. If the <code>"track"</code> option is set to
+ <code>true</code> then the channel will close when the service exits and/or disconnects
+ from the DBus bus.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>If the <code>name</code> argument is null, and no options other than <code>"bus"</code>
+ are specified, then a shared DBus <code>client</code> is created. When using such a client
+ with a DBus bus, a <code>"name"</code> option must be specified on various other methods
+ in order to specify which client to talk to.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-wait">
+ <title>client.wait()</title>
+<programlisting>
+promise = client.wait([callback])
+</programlisting>
+
+ <para>Returns a <code>promise</code> that is ready when the client is ready, or fails if the
+ client closes. If a <code>callback</code> is specified, it is attached to the promise.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-close">
+ <title>client.close()</title>
+<programlisting>
+client.close([problem])
+</programlisting>
+ <para>Close the DBus client. If <code>problem</code> is specified it should be a
+ <link linkend="cockpit-problems">problem code</link> string.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-onclose">
+ <title>client.onclose</title>
+<programlisting>
+client.addEventListener("close", options => { ... })
+</programlisting>
+ <para>An event triggered when the DBus client closes. This can happen either because
+ <link linkend="cockpit-dbus-close">client.close()</link> function was called,
+ or the DBus service went away, or some other problem or disconnection.</para>
+ <para>The <code>options</code> will contain various close information, including a
+ <code>"problem"</code> field which will be set if the channel was closed because
+ of a problem.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-onowned">
+ <title>client.onowner</title>
+<programlisting>
+client.addEventListener("owner", (event, owner) => { ... })
+</programlisting>
+ <para>An event triggered when the owner of the DBus name changes. The owner
+ value will be the id of the name owner on the bus or null if the name
+ is unowned. The absence of an owner should not be treated as a disconnection.
+ However this makes it possible to take some action based on the
+ actual status of the service, for example disconnecting a pending signal handler.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-options">
+ <title>client.options</title>
+ <para>Set to the options used when creating the client. Will not change for the life of
+ the client.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-unique-name">
+ <title>client.unique_name</title>
+ <para>The unique DBus name of the client. Initially null, and becomes valid once the
+ the client is ready.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxy">
+ <title>client.proxy()</title>
+<programlisting>
+proxy = client.proxy([interface, path], [options])
+</programlisting>
+
+ <para>Create proxy javascript object for a DBus <code>interface</code>. At the
+ specified DBus object <code>path</code>. The proxy will have
+ properties, methods and signals from to the DBus interface, and allows for
+ natural interaction. If no <code>interface</code> is specified then the DBus
+ bus name of the client is used. If no <code>path</code> is specified, then
+ the DBus name of the client is converted to a path.</para>
+
+ <para>If creating lots of proxies for a given <code>interface</code> it is more
+ efficient to use the
+ <link linkend="cockpit-dbus-proxies"><code>client.proxies()</code></link>
+ function.</para>
+
+ <para>The proxy is loaded when the
+ <link linkend="cockpit-dbus-proxy-valid"><code>proxy.valid</code></link> field is
+ <code>true</code>, and it is set to <code>false</code> if the underlying
+ <code>interface</code> and/or <code>path</code> don't or no longer exist, or
+ the <code>client</code> has closed. You can wait for proxy to become valid
+ by passing a callback to its
+ <link linkend="cockpit-dbus-proxy-wait"><code>proxy.wait()</code></link> function.
+ The <link linkend="cockpit-dbus-proxy-onchanged"><code>proxy.onchanged</code></link>
+ event will also fire when the proxy becomes valid or invalid. DBus properties and
+ methods on the proxy are not defined until the proxy becomes valid.</para>
+
+<programlisting>
+value = proxy.Prop1
+proxy.WritableProp = value
+</programlisting>
+
+ <para>All DBus properties on the <code>interface</code> that start with an upper case
+ letter (as is convention) will be automatically defined on this proxy, and will update
+ their values as the DBus property values change. In addition the
+ <link linkend="cockpit-dbus-proxy-onchanged"><code>proxy.onchanged</code></link> event
+ will fire every time the properties change.</para>
+
+ <para>If you assign a value to a writable property on the proxy, the proxy will try to set
+ that property on the DBus <code>interface</code> at <code>path</code>. The actual proxy
+ property value will not update until the DBus service has notified the proxy of the
+ change. If setting a property fails a warning will be logged. In order to have more
+ reliable setting of properties, or track when they have been set, or if setting fails,
+ use the <link linkend="cockpit-dbus-call"><code>client.call()</code></link> directly.
+ It should be noted that DBus service implementations may also be inconsistent in
+ their behavior when setting a property fails.</para>
+
+ <para>You can access the raw property data using the
+ <link linkend="cockpit-dbus-proxy-data"><code>proxy.data</code></link> field, including
+ data for properties that do not start with an upper case letter.</para>
+
+<programlisting>
+proxy.Method(arg1, arg2)
+ .then((retval1, retval2) => {
+ ...
+ })
+ .catch(ex => {
+ ...
+ });
+</programlisting>
+ <para>All DBus methods on the <code>interface</code> that start with an upper case
+ letter (as is convention) will be automatically defined on this proxy. These
+ methods are called with arguments as normal javascript arguments. A
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ that will complete successfully when the method returns, or fail if an error occurs.
+ The return values from the DBus method will be passed to the <code>then</code> handler
+ function directly.</para>
+
+ <para>Methods that do not start with an upper case letter can be invoked by using
+ the usual <link linkend="cockpit-dbus-proxy-call"><code>proxy.call()</code></link> directly.</para>
+
+<programlisting>
+proxy.addEventListener("Signal", (event, arg1, arg2) => {
+ ...
+});
+</programlisting>
+
+ <para>All DBus signals on the <code>interface</code> that start with an upper case
+ letter (as is convention) will be automatically emit events on this proxy. These
+ events will contain the signal arguments after the standard <code>event</code>
+ argument.</para>
+
+ <para>Signals that do not start with an upper case letter can be subscribed to by
+ using <link linkend="cockpit-dbus-proxy-signal"><code>proxy.onsignal</code></link>
+ directly.</para>
+
+ <para>Usually a proxy asks the <code>client</code> to watch and notify it of changes
+ to the relevant object or path. You can pass an <code>options</code> argument with
+ the <code>watch</code> field set to <code>false</code> to prevent this.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxy-client">
+ <title>proxy.client</title>
+ <para>Set to the DBus client of the proxy. Will not change for the life of
+ the proxy.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxy-path">
+ <title>proxy.path</title>
+ <para>Set to the DBus object path of the proxy. Will not change for the life of
+ the proxy.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxy-iface">
+ <title>proxy.iface</title>
+ <para>Set to the DBus interface name of the proxy. Will not change for the life
+ of the proxy.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxy-valid">
+ <title>proxy.valid</title>
+ <para>Set to <code>true</code> when the proxy's DBus interface is present at its
+ DBus path, and all information for the proxy has loaded. Is set to <code>false</code>
+ while loading, and after the proxy no longer refers a DBus interface and path.
+ Also set to <code>false</code> if the <code>client</code> closes.</para>
+ <para>Use the by <link linkend="cockpit-dbus-proxy-wait"><code>proxy.wait()</code></link>
+ function to wait for a proxy to load. The
+ <link linkend="cockpit-dbus-proxy-onchanged"><code>proxy.onchanged</code></link>
+ event will also be emitted when the proxy becomes valid or invalid. DBus properties and
+ methods on the proxy are not defined until the proxy becomes valid.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxy-data">
+ <title>proxy.data</title>
+ <para>A plain javascript object containing all the raw property data that this
+ proxy has loaded. This will be updated automatically as the proxy is notified
+ of property changes from the DBus service. The
+ <link linkend="cockpit-dbus-proxy-onchanged"><code>proxy.onchanged</code></link>
+ event will be emitted when it changes.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxy-call">
+ <title>proxy.call()</title>
+<programlisting>
+invocation = proxy.call(method, args, [options])
+</programlisting>
+ <para>Make a DBus method call on this proxy.</para>
+
+ <para>For DBus methods that start with an upper case letter, is usually more convenient
+ <link linkend="cockpit-dbus-proxy">to call the method directly on the proxy</link>.
+ However if methods that do not follow the usual DBus convention,
+ or specify additional options, or the caller cannot be sure that the method actually
+ exists, you can use this method.</para>
+
+ <para>This function also works on proxies that have are still loading and have not
+ become valid yet.</para>
+
+ <para>The <code>method</code> should be a DBus method name, and the <code>args</code>
+ should be an array of arguments to pass to the method. The <code>options</code>
+ are <link linkend="cockpit-dbus-call">described elsewhere</link>.</para>
+
+ <para>The returned value is identical to the one returned from
+ <link linkend="cockpit-dbus-call">client.call()</link>. It is a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ that will complete successfully when the method returns, or fail if an error occurs.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxy-wait">
+ <title>proxy.wait()</title>
+<programlisting>
+promise = proxy.wait()
+proxy.wait(() => {
+ ...
+});
+</programlisting>
+ <para>Wait for a proxy to finish loading. This function returns a promise. If a callback function
+ is passed as an argument then that function will be invoked when the proxy is ready.
+ If this method is called after a proxy has already loaded, then the promise will be
+ resolved immediately, and any callback will be invoked immediately. Use the promise or
+ <code>proxy.valid</code> to determine whether the proxy is valid.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxy-onchanged">
+ <title>proxy.onchanged</title>
+<programlisting>
+proxy.addEventListener("changed", (event, data) => {
+ ...
+});
+</programlisting>
+ <para>This event is emitted when the proxy's properties change.</para>
+
+ <para>The <code>data</code> has the following form, and will only include
+ properties that have changed:</para>
+
+<programlisting>
+{
+ "Prop1": "value",
+ "Prop2": 5
+}
+</programlisting>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxy-signal">
+ <title>proxy.onsignal</title>
+<programlisting>
+proxy.addEventListener("signal", (event, name, args) => {
+ ...
+});
+</programlisting>
+ <para>This event is emitted when the proxy's emits an event.</para>
+
+ <para>For most events, that have names which start with an upper case letter, you can
+ just <link linkend="cockpit-dbus-proxy">connect to that event as a signal directly</link>.
+ However if you wish to be notified when any signal is emitted, or for signals that do not
+ follow the usual DBus convention, you can connect to this event.</para>
+
+ <para>The <code>name</code> is the DBus signal name, and the <code>args</code> is an array
+ of arguments that were emitted with the signal.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxies">
+ <title>client.proxies()</title>
+<programlisting>
+proxies = client.proxies([interface], [path_namespace], [options])
+</programlisting>
+
+ <para>Create <link linkend="cockpit-dbus-proxy">proxy javascript objects</link> for
+ a DBus interfaces. The proxies will have properties, methods and signals from
+ the DBus <code>interface</code>, and allow for natural interaction. If no
+ <code>interface</code> is specified then the DBus bus name of the client is used.
+ If no <code>path_namespace</code> is provided then <code>"/"</code> will be used.</para>
+
+ <para>Proxies will be automatically created for instances of the
+ <code>interface</code> available at the DBus service. The optional
+ <code>path_namespace</code> argument can be used to restrict the proxies for
+ instances that have DBus paths which have the namespace path prefix.</para>
+
+<programlisting>
+proxy1 = proxies["/dbus/path1"];
+proxy2 = proxies["/dbus/path2"];
+for (proxy in proxies) {
+ ...
+}
+</programlisting>
+ <para>The returned <code>proxies</code> object will is used as a dictionary,
+ and will have values containing proxies for DBus interface instances, with the
+ keys being the DBus paths of those instances. It is possible to enumerate over
+ the returned <code>proxies</code>.</para>
+
+ <para>Proxies will be automatically added and removed from the <code>proxies</code>
+ object as they appear and disappear in the service. The
+ <link linkend="cockpit-dbus-proxies-onadded"><code>proxies.onadded</code></link>
+ and <link linkend="cockpit-dbus-proxies-onremoved"><code>proxies.onremoved</code></link>
+ events will be emitted. DBus services may not support notifications of paths
+ disappearing.</para>
+
+ <para>Use the <code>proxies.wait()</code> function to be notified when the initial
+ set of proxies has been populated.</para>
+
+ <para>Usually a proxies ask the <code>client</code> to watch and be notified of changes
+ to the relevant object or path. You can pass an <code>options</code> argument with
+ the <code>watch</code> field set to <code>false</code> to prevent this.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxies-wait">
+ <title>proxies.wait()</title>
+<programlisting>
+promise = proxies.wait()
+proxies.wait(() => {
+ ...
+});
+</programlisting>
+ <para>Wait for a <code>proxies</code> object to populate its initial set of proxies.
+ This function returns a promise. If a callback function is passed as an argument then
+ that function will be invoked when the proxies are ready. If this method is called after
+ the proxies have populated, then the promise will be resolved immediately, and any callback
+ will be invoked immediately.</para>
+
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxies-client">
+ <title>proxies.client</title>
+ <para>Set to the DBus client of the proxies. Will not change.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxies-iface">
+ <title>proxies.iface</title>
+ <para>Set to the DBus interface name of the proxies. Will not change.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxies-path_namespace">
+ <title>proxies.path_namespace</title>
+ <para>Set to the DBus path namespace used which the proxies must have as a DBus
+ path prefix. Will not change.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxies-onadded">
+ <title>proxies.onadded</title>
+<programlisting>
+proxies.addEventListener("added", (event, proxy) => {
+ ...
+})
+</programlisting>
+ <para>This event is emitted when a proxy is added to the <code>proxies</code> object.
+ The proxy will already have loaded.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxies-onchanged">
+ <title>proxies.onchanged</title>
+<programlisting>
+proxies.addEventListener("changed", (event, proxy) => {
+ ...
+})
+</programlisting>
+ <para>This event is emitted when one of the proxy in the <code>proxies</code> object
+ changes its properties.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-proxies-onremoved">
+ <title>proxies.onremoved</title>
+<programlisting>
+proxies.addEventListener("removed", (event, proxy) => {
+ ...
+})
+</programlisting>
+ <para>This event is emitted when a proxy is removed to the <code>proxies</code> object.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-call">
+ <title>client.call()</title>
+<programlisting>
+invocation = client.call(path, interface, method, args, [options])
+</programlisting>
+
+ <para>Make a DBus method call.</para>
+
+ <para>The <code>path</code> is the DBus object path to make
+ the call on, <code>interface</code> is the DBus interface for the method and
+ <code>method</code> is the name of the method to call. The <code>args</code> is an
+ array of arguments to pass to the method, each of which must be appropriate for the
+ expected <link linkend="cockpit-dbus">DBus type</link> of that argument. The
+ <code>args</code> may be <code>null</code> if no arguments are to be sent.</para>
+
+ <para>The returned value is a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ that will complete successfully when the method returns, or fail if an error occurs.</para>
+
+ <para>If <code>options</code> is specified it should be a plain javascript object,
+ which may contain the following properties:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>flags</code></term>
+ <listitem><para>A string containing DBus message flags. The character <code>"i"</code>
+ indicates to the dbus service that interactive authentication is allowed. If the
+ entire <code>flags</code> field is missing, then <code>"i"</code> is set by
+ default.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>type</code></term>
+ <listitem><para>A valid DBus type signature to use when calling the method. In the
+ absence of this, the DBus service will be introspected (and the result cached) to
+ ask what the method type signature is.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>timeout</code></term>
+ <listitem><para>The timeout of the call in milliseconds. The call will fail with
+ the <code>"timeout"</code> problem code. If "timeout" is not given,
+ the call will never time out.</para></listitem>
+ </varlistentry>
+ </variablelist>
+ </refsection>
+
+ <refsection id="cockpit-dbus-then">
+ <title>invocation.then()</title>
+<programlisting>
+invocation.then((args, options) => { ... })
+</programlisting>
+ <para>This is a standard
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ method. It sets up a handler to be called when the DBus method call finishes
+ successfully.</para>
+ <para>The <code>args</code> argument is an array of return values from the DBus method.
+ Each of them will be converted to an appropriate
+ <link linkend="cockpit-dbus">javascript type</link>.</para>
+ <para>The <code>options</code> argument may contain additional information about the
+ reply. If the <code>type</code> option was specified when performing the method call,
+ then the <code>options</code> in the reply here will also contain a <code>type</code>
+ field containing the DBus type signature of the output. If the <code>flags</code> option
+ was specified when performing the call then the <code>options</code> in the reply here
+ will contain message flags. Possible out message flags are:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>&gt;</code></term>
+ <listitem><para>A big endian message.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>&lt;</code></term>
+ <listitem><para>A little endian message.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ </refsection>
+
+ <refsection id="cockpit-dbus-catch">
+ <title>invocation.catch()</title>
+<programlisting>
+invocation.catch(exception => { ... })
+</programlisting>
+ <para>This is a standard
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink> method.
+ It sets up a handler to be called when the DBus method call fails.</para>
+
+ <para>The <code>exception</code> object passed to the handler can have the
+ following properties:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>problem</code></term>
+ <listitem><para>A <link linkend="cockpit-problems">problem code</link> string when
+ a problem occurred starting or communicating with the DBus service. This is
+ <code>null</code> in the cases where an actual DBus error was occurred.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>name</code></term>
+ <listitem><para>The DBus error name. This will be <code>null</code> in cases where the
+ failure was not due to a DBus error.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>message</code></term>
+ <listitem><para>A DBus error message. This will be <code>null</code> in cases where the
+ failure was not due to a DBus error.</para></listitem>
+ </varlistentry>
+ </variablelist>
+ </refsection>
+
+ <refsection id="cockpit-dbus-subscribe">
+ <title>client.subscribe()</title>
+<programlisting>
+subscription = client.subscribe(match, (path, interface, signal, args) => { ... })
+</programlisting>
+ <para>Subscribe to signals. The <code>match</code> argument is a javascript plain object which
+ defines what signals to subscribe to. Each property in the <code>match</code> argument restricts
+ signals subscribed to. If a property is not present then it is treated as a wildcard, matching
+ anything. If an empty object is specified as <code>match</code> then all signals will be
+ subscribed to. The <code>match</code> argument may contain the following properties:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>interface</code></term>
+ <listitem><para>A DBus interface to match.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>path</code></term>
+ <listitem><para>A DBus object path to match. May not be used together with the
+ <code>path_namespace</code> property. It should be a valid DBus object path,
+ that is, it should have no trailing slash.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>path_namespace</code></term>
+ <listitem><para>A DBus object path prefix to match. Any paths in the hierarchy below this
+ top path will match. May not be used together with the <code>path</code>
+ property.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>member</code></term>
+ <listitem><para>The DBus signal name to match.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>arg0</code></term>
+ <listitem><para>Matches the first argument of a DBus message, which must be a
+ string.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>The handler passed as the second argument will be invoked when the signal is received.
+ A <code>subscription</code> is returned which can be used to remove the subscription by
+ calling its <code>subscription.remove()</code> method.</para>
+
+ <para>It is not a problem to subscribe to the same signals more than once, with identical
+ or slightly different <code>match</code> arguments.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-remove">
+ <title>subscription.remove()</title>
+<programlisting>
+subscription.remove()
+</programlisting>
+
+ <para>Unsubscribe from the DBus signal subscription.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-watch">
+ <title>client.watch()</title>
+<programlisting>
+watch = client.watch(path)
+watch = client.watch({ "path_namespace": path_namespace, "interface": interface })
+</programlisting>
+ <para>Watch for property and interface changes on the given DBus object
+ <code>path</code> DBus <code>path_namespace</code>. If <code>interface</code> is
+ specified only properties on that DBus interface will be watched.</para>
+
+ <para>The <link linkend="cockpit-dbus-proxy"><code>client.proxy()</code></link> and
+ <link linkend="cockpit-dbus-proxies"><code>client.proxies()</code></link> functions and
+ the objects they return are high level wrappers around <code>client.watch()</code>.</para>
+
+ <para>The property and interface changes will be available in raw form on the
+ <link linkend="cockpit-dbus-onnotify"><code>client.onnotify</code></link> event.</para>
+
+ <para>Property and interface changes that are caused by a method call or signal will
+ show up before that method call reply is received, or signal event is triggered.
+ It should be possible to rely on this guarantee, unless the DBus service in question
+ behaves incorrectly. Internally these watches work well with code that implements the
+ <ulink url="https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager">ObjectManager</ulink>
+ portion of the DBus specification. If no ObjectManager implementation is available, the
+ watch falls back to using DBus
+ <ulink url="https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-introspectable">Introspection</ulink> along with the usual
+ <ulink url="https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties">PropertiesChanged</ulink> signal. If the DBus service implements none of these, or implements them in an
+ inconsistent manner, then this function will provide inconsistent or unexpected
+ results.</para>
+
+ <para>The parameter is either a DBus <code>path</code> or a plain javascript object
+ with zero or more of the following fields. If an empty javascript object is used as
+ an argument, then all paths, interfaces and properties will be watched.</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>interface</code></term>
+ <listitem><para>Watch properties on this DBus interface.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>path</code></term>
+ <listitem><para>Watch interfaces and properties at this DBus path. May not be
+ used together with the <code>path_namespace</code> property.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>path_namespace</code></term>
+ <listitem><para>Watch interfaces and properties under this DBus path. It should
+ be a valid DBus object path, that is, it should have no trailing slash.
+ If an ObjectManager implementation is available at this interface, then it
+ is used. May not be used together with the <code>path</code> property.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>The returned value is a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ that will complete successfully when the watch has populated its initial set of properties
+ and interfaces, and these have been notified via
+ <link linkend="cockpit-dbus-onnotify"><code>client.onnotify</code></link>.</para>
+
+ <para>A watch can be removed by calling the
+ <link linkend="cockpit-dbus-watch-remove"><code>watch.remove()</code></link> method on
+ the returned value. If identical watches are added more than once, then they must
+ also be removed the same number of times before the removal takes effect.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-watch-then">
+ <title>watch.then()</title>
+<programlisting>
+watch.then(() => { ... })
+</programlisting>
+ <para>This is a standard
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ method. It sets up a handler to be called when the watch has populated its initial
+ properties and interfaces.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-watch-catch">
+ <title>watch.catch()</title>
+<programlisting>
+watch.catch(ex => { ... })
+</programlisting>
+ <para>This is a standard
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ method. It sets up a handler to be called if the watch fails to populate its initial
+ properties and interfaces. Note that a watch will only fail if the DBus client
+ closes or is somehow disconnected. It does not fail in the case of missing
+ interfaces or properties.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-watch-remove">
+ <title>watch.remove()</title>
+<programlisting>
+watch.remove()
+</programlisting>
+ <para>Remove the watch. This may not have any immediate effect if other watches are in
+ place. In particular, if identical watches are added more than once, then they must
+ also be removed the same number of times before the removal takes effect.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-onnotify">
+ <title>client.onnotify</title>
+<programlisting>
+client.addEventListener("notify", data => { ... })
+</programlisting>
+ <para>An event triggered when
+ <link linkend="cockpit-dbus-watch">watched</link> properties or interfaces change.</para>
+
+ <para>The <link linkend="cockpit-dbus-proxy"><code>client.proxy()</code></link> and
+ <link linkend="cockpit-dbus-proxies"><code>client.proxies()</code></link> functions and
+ the objects they return are high level wrappers around the <code>data</code> provided
+ by this event.</para>
+
+ <para>The <code>data</code> has the following form:</para>
+
+<programlisting>
+{
+ "/path1": {
+ "org.Interface1": {
+ "Prop1": "value",
+ "Prop2": 5
+ },
+ "org.Interface2": null
+ }
+}
+</programlisting>
+
+ <para>Multiple paths may be present, each of which may have multiple interfaces, each
+ of which may have multiple properties. The first time a given path and interface is
+ emitted from this signal, it will have all its properties and interfaces. Thereafter
+ only changes are noted. If an interface is set to <code>null</code>, then that
+ interface has disappeared.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-notify">
+ <title>client.notify()</title>
+<programlisting>
+client.notify(data)
+</programlisting>
+ <para>Emits a synthetic <link
+ linkend="cockpit-dbus-onnotify"><code>notify</code></link> event.
+ The <code>data</code> argument should follow the same layout as
+ described for the <code>notify</code> event.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-onmeta">
+ <title>client.onmeta</title>
+<programlisting>
+client.onmeta = (ev, data) => { ... }
+</programlisting>
+ <para>An event triggered when the meta data about
+ <link linkend="cockpit-dbus-watch">watched</link> interfaces is loaded.</para>
+
+ <para>The <link linkend="cockpit-dbus-proxy"><code>client.proxy()</code></link> and
+ <link linkend="cockpit-dbus-proxies"><code>client.proxies()</code></link> functions and
+ the objects they return are high level wrappers around the <code>data</code> provided
+ by this event.</para>
+
+ <para>The <code>data</code> has the following form:</para>
+
+<programlisting>
+ {
+ "org.Interface": {
+ "methods": {
+ "Method1": {
+ "in": [ "s", "v" ],
+ "out": [ "i" ]
+ },
+ "Method2": { }
+ },
+ "signals": {
+ "Signal": {
+ "in": [ "b", "s" ]
+ }
+ },
+ "properties": {
+ "Prop1": {
+ "flags": "rw",
+ "type": "s"
+ },
+ "Prop2": {
+ "flags": "r",
+ "type": "b"
+ }
+ }
+ }
+ }
+</programlisting>
+
+ <para>Multiple interfaces may be present, each of which may have methods and properties.
+ This is emitted before the first <link linkend="cockpit-dbus-proxy"><code>client.onnotify</code></link>
+ event for the relevant interface.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-variant">
+ <title>cockpit.variant()</title>
+<programlisting>
+variant = cockpit.variant(type, value)
+</programlisting>
+
+ <para>A DBus variant is represented as a plain javascript object with a
+ <code>"t"</code> property represesting the full DBus type of the variant,
+ and a <code>"v"</code> property containing the variant value.</para>
+
+ <para>This is a helper function for creating such a variant object.</para>
+ </refsection>
+
+ <refsection id="cockpit-dbus-byte-array">
+ <title>cockpit.byte_array()</title>
+<programlisting>
+byte_array = cockpit.byte_array(value)
+</programlisting>
+
+ <para>A DBus byte array is represented as base64 data encoded in a string. This
+ is a helper function for creating such a byte array.</para>
+ </refsection>
+</refentry>
diff --git a/doc/guide/cockpit-error.xml b/doc/guide/cockpit-error.xml
new file mode 100644
index 0000000..9911685
--- /dev/null
+++ b/doc/guide/cockpit-error.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-error">
+ <refnamediv>
+ <refname>cockpit.js: Errors</refname>
+ <refpurpose>Problem codes and messages</refpurpose>
+ </refnamediv>
+
+ <refsection id="cockpit-problems">
+ <title>Problem Codes</title>
+ <para>Cockpit represents problems with standardized problem string codes.</para>
+ <variablelist>
+ <varlistentry>
+ <term><code>"access-denied"</code></term>
+ <listitem><para>The user is not permitted to perform the action in question.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"authentication-failed"</code></term>
+ <listitem><para>User authentication failed.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"internal-error"</code></term>
+ <listitem><para>An unexpected internal error without further info. This should
+ not happen during the normal course of operations.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"no-cockpit"</code></term>
+ <listitem><para>The system does not have a compatible version of Cockpit installed
+ or installed properly.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"no-session"</code></term>
+ <listitem><para>Cockpit is not logged in.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"not-found"</code></term>
+ <listitem><para>Something specifically requested was not found, such as a file,
+ executable etc.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"terminated"</code></term>
+ <listitem><para>Something was terminated forcibly, such as a connection, process
+ session, etc.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"timeout"</code></term>
+ <listitem><para>Something timed out.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"unknown-hostkey"</code></term>
+ <listitem><para>The remote host had an unexpected or unknown key.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"no-forwarding"</code></term>
+ <listitem><para>Could not forward authentication credentials to the remote host.</para></listitem>
+ </varlistentry>
+ </variablelist>
+ </refsection>
+
+ <refsection id="cockpit-messages">
+ <title>cockpit.message()</title>
+<programlisting>
+message = cockpit.message(problem)
+message = cockpit.message(exception)
+</programlisting>
+ <para>Return a message for the <code>exception</code> or <code>problem</code> code
+ passed as an argument. If the argument is an object with a <code>"message"</code> property,
+ as is the case with most exceptions, that will be returned directly. If the argument is
+ an object with a <code>"problem"</code> property, then it will be used as the problem code.
+ An appropriate message will be returned for problem codes.</para>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/cockpit-file.xml b/doc/guide/cockpit-file.xml
new file mode 100644
index 0000000..df72fbd
--- /dev/null
+++ b/doc/guide/cockpit-file.xml
@@ -0,0 +1,236 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-file">
+ <refnamediv>
+ <refname>cockpit.js: File Access</refname>
+ <refpurpose>Reading, writing, and watching files.</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <para>The <code>cockpit.file</code> API lets you read, write, and watch regular files in their entirety.
+ It cannot efficiently do random access in a big file or read non-regular files such as
+ <code>/dev/random</code>.
+ </para>
+<programlisting>
+file = cockpit.file(path,
+ { syntax: syntax_object,
+ binary: boolean,
+ max_read_size: int,
+ superuser: string,
+ })
+
+promise = file.read()
+promise
+ .then((content, tag) => { ... })
+ .catch(error => { ... })
+
+promise = file.replace(content, [ expected_tag ])
+promise
+ .then(new_tag => { ... })
+ .catch(error => { ... })
+
+promise = file.modify(callback, [ initial_content, initial_tag ]
+promise
+ .then((new_content, new_tag) => { ... })
+ .catch(error => { ... })
+
+file.watch((content, tag, [error]) => { }, [ { read: boolean } ])
+
+file.close()
+</programlisting>
+ </refsynopsisdiv>
+
+ <refsection id="cockpit-file-simple">
+ <title>Simple reading and writing</title>
+
+ <para>You can read a file with code like this:</para>
+<programlisting>
+cockpit.file("/path/to/file").read()
+ .then((content, tag) => {
+ ...
+ })
+ .catch(error => {
+ ...
+ });
+</programlisting>
+ <para>It is recommended to use absolute paths. Relative paths are resolved against <code>/</code>.
+ To work with the current user's files <link linkend="cockpit-user">cockpit.user()</link> can be used to get the user's home directory.
+ </para>
+ <para>The <code>read()</code> method returns a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>.</para>
+ <para>When successful, the promise will be resolved with the content of the
+ file. Unless you specify options to change this (see below), the file
+ is assumed to be text in the UTF-8 encoding, and <code>content</code>
+ will be a string.</para>
+ <para>The tag that is passed to the <code>then()</code> callback is a short
+ string that is associated with the file and changes whenever the
+ content of the file changes. It is meant to be used with <code>replace()</code>.</para>
+ <para>It is not an error when the file does not exist. In this case, the
+ <code>then()</code> callback will be called with a <code>null</code>
+ value for <code>content</code> and <code>tag</code> is <code>"-"</code>.</para>
+ <para>The <code>superuser</code> option can be used the same way
+ as described in the <link linkend="cockpit-channels-channel">cockpit.channel()</link>
+ to provide a different access level to the file.</para>
+ <para>You can use the <code>max_read_size</code> option to limit
+ the amount of data that is read. If the file is larger than the
+ given number of bytes, no data is read and the channel is closed with
+ problem code <code>too-large</code>. The default limit is 16 MiB.</para>
+ <para>To write to a file, use code like this:
+<programlisting>
+cockpit.file("/path/to/file").replace("my new content\n")
+ .then(tag => {
+ ...
+ })
+ .catch(error => {
+ ...
+ });
+</programlisting>
+ </para>
+ <para>The <code>replace()</code> method returns a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>.</para>
+ <para>When the promise is resolved, the file has been atomically replaced
+ (via the <code>rename()</code> syscall) with the new content. As with
+ <code>read()</code>, by default the new content is a string and will
+ be written to the file as UTF-8. The returned tag corresponds to the
+ new content of the file.</para>
+ <para>When the promise is rejected because of an error, the file or its meta
+ data has not been changed in any way.</para>
+ <para>As a special case, passing the value <code>null</code> to
+ <code>replace()</code> will remove the file.</para>
+ <para>The <code>replace()</code> method can also check for conflicting
+ changes to a file. You can pass a tag (as returned by
+ <code>read()</code> or <code>replace()</code>) to
+ <code>replace()</code>, and the file will only be replaced if it still
+ has the given tag. If the tag of the file has changed,
+ <code>replace()</code> will fail with an error object that has
+ <code>error.problem == "change-conflict"</code>. See
+ <code>modify()</code> below for a convenient way to achieve
+ transactional updates to a file.</para>
+ </refsection>
+
+ <refsection id="cockpit-file-format">
+ <title>File format</title>
+ <para>By default, a file is assumed to be text encoded in UTF-8, and the
+ <code>read()</code> and <code>replace()</code> functions use strings to
+ represent the content.</para>
+ <para>By specifying the <code>syntax.parser()</code> and
+ <code>syntax.stringify()</code> options, you can cause
+ <code>read()</code> to parse the content before passing it back to
+ you, and <code>replace()</code> to unparse it before writing.</para>
+ <para>The main idea is to be able to write <code>{ syntax: JSON }</code>, of
+ course, but you can easily pass in individual functions or make your
+ own parser/unparser object:
+<programlisting>
+cockpit.file("/path/to/file.json", { syntax: JSON })
+
+var syntax_object = {
+ parse: my_parser,
+ stringify: my_unparser
+};
+
+cockpit.file("/path/to/file", { syntax: syntax_object })
+</programlisting>
+ Any exceptions thrown by the <code>parse()</code> and
+ <code>stringify()</code> functions are caught and reported as read or
+ write errors.</para>
+ <para>The <code>null</code> value that is used to represent the content of a
+ non-existing file (see "Simple reading and writing", above) is not
+ passed through the <code>parse()</code> and <code>stringify()</code>
+ functions.</para>
+ </refsection>
+
+ <refsection id="cockpit-file-binary">
+ <title>Binary files</title>
+ <para>By default the content of the file is assumed to be text encoded as
+ UTF-8 and it can not contain zero bytes. The content is represented
+ as a JavaScript string with <code>read()</code>,
+ <code>replace()</code>, etc. By setting the <code>binary</code> option
+ to true when creating the proxy, no assumptions are placed on the
+ content, and it is represented as a <code>Uint8Array</code> in
+ JavaScript.</para>
+ </refsection>
+
+ <refsection id="cockpit-file-atomic">
+ <title>Atomic modifications</title>
+ <para>Use <code>modify()</code> to modify the content of the file safely. A
+ call to <code>modify()</code> will read the content of the file, call
+ <code>callback</code> on the content, and then replace the content of
+ the file with the return value of the callback.</para>
+ <para>The <code>modify()</code> method uses the <code>read()</code> and
+ <code>replace()</code> methods internally in the obvious way. Thus,
+ the <code>syntax.parse()</code> and <code>syntax.stringify()</code>
+ options work as expected, <code>null</code> represents a non-existing
+ file, and the watch callbacks are fired.</para>
+ <para>It will do this one or more times, until no other conflicting changes
+ have been made to the file between reading and replacing it.</para>
+ <para>The callback is called like this
+<programlisting>
+new_content = callback (old_content)
+</programlisting>
+ The callback is allowed to mutate <code>old_content</code>, but note
+ that this will also mutate the objects that are passed to the watch
+ callbacks. Returning <code>undefined</code> from the proxy is the
+ same as returning <code>old_content</code>.</para>
+ <para>The <code>modify()</code> method returns a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>.</para>
+ <para>The promise will be resolved with the new content and its tag, like so
+<programlisting>
+function shout(old_content) {
+ return old_content.toUpperCase();
+}
+
+cockpit.file("/path/to/file").modify(shout)
+ .then((content, tag) => {
+ ...
+ })
+ .catch(error => {
+ ...
+ });
+</programlisting>
+ If you have cached the last content and tag results of the
+ <code>read()</code> or <code>modify()</code> method, or the last
+ values passed to a watch callback, you can pass them to
+ <code>modify()</code> as the second and third argument. In this case,
+ <code>modify()</code> will skip the initial read and start with the
+ given values.</para>
+ </refsection>
+
+ <refsection id="cockpit-file-notify">
+ <title>Change notifications</title>
+ <para>Calling <code>watch()</code> will start monitoring the file for
+ external changes.
+<programlisting>
+handle = file.watch(callback);
+
+handle_no_read = file.watch(callback, { read: false });
+</programlisting>
+ Whenever a change occurs, the <code>callback()</code> is called with
+ the new content and tag of the file. This might happen because of
+ external changes, but also as part of calls to <code>read()</code>,
+ <code>replace()</code>, and <code>modify()</code>.</para>
+ <para>When a read error occurs, the <code>callback()</code> is called with
+ an error as a third argument. Write errors are not reported via the watch callback.</para>
+ <para>Calling <code>watch()</code> will also automatically call <code>read()</code>
+ to get the initial content of the file. Thus, you normally don't need to call
+ <code>read()</code> at all when using <code>watch()</code>.</para>
+ <para>To disable the automatic reading, e.g. for large files or unreadable
+ file system objects, set the <code>read</code> option to <code>false</code>. The first
+ <code>content</code> argument of the callback will then always be <code>null</code>.</para>
+ <para>To free the resources used for monitoring, call <code>handle.remove()</code>.</para>
+ </refsection>
+
+ <refsection id="cockpit-file-path">
+ <title>file.path</title>
+ <para>A string containing the path that was passed to the <code>cockpit.file()</code>
+ method.</para>
+ </refsection>
+
+ <refsection id="cockpit-file-close">
+ <title>Closing</title>
+ <para>Call the <code>close()</code> method on a file proxy to cancel all
+ ongoing operations, such as reading, writing, and monitoring. The
+ proxy should not be used after closing it.</para>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/cockpit-guide.xml b/doc/guide/cockpit-guide.xml
new file mode 100644
index 0000000..77e4cf9
--- /dev/null
+++ b/doc/guide/cockpit-guide.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd"
+[
+ <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+ <!ENTITY version SYSTEM "version">
+]>
+<book id="index">
+ <bookinfo>
+ <title>Cockpit Guide</title>
+ <releaseinfo>Documentation for Cockpit &version; --
+ latest version available at:
+ <ulink url="https://cockpit-project.org/guide/latest">https://cockpit-project.org/guide/latest/</ulink></releaseinfo>
+ </bookinfo>
+
+ <part id="guide">
+ <title>Deployment Guide</title>
+ <chapter id="cockpit-manual">
+ <title>Manual pages</title>
+ <xi:include href="../man/cockpit.conf.xml"/>
+ <xi:include href="../man/cockpit-ws.xml"/>
+ <xi:include href="../man/cockpit-tls.xml"/>
+ <xi:include href="../man/cockpit-desktop.xml"/>
+ <xi:include href="../man/cockpit-bridge.xml"/>
+ </chapter>
+
+ <xi:include href="https.xml"/>
+ <xi:include href="listen.xml"/>
+ <xi:include href="startup.xml"/>
+ <xi:include href="authentication.xml"/>
+ <xi:include href="sso.xml"/>
+ <xi:include href="cert-authentication.xml"/>
+ <xi:include href="privileges.xml"/>
+ </part>
+
+ <part id="features">
+ <title>Feature Internals</title>
+ <xi:include href="feature-systemd.xml"/>
+ <xi:include href="feature-journal.xml"/>
+ <xi:include href="feature-networkmanager.xml"/>
+ <xi:include href="feature-firewall.xml"/>
+ <xi:include href="feature-storaged.xml"/>
+ <xi:include href="feature-users.xml"/>
+ <xi:include href="feature-realmd.xml"/>
+ <xi:include href="feature-terminal.xml"/>
+ <xi:include href="feature-pcp.xml"/>
+ <xi:include href="feature-machines.xml"/>
+ <xi:include href="feature-selinux.xml"/>
+ <xi:include href="feature-tuned.xml"/>
+ <xi:include href="feature-sosreport.xml"/>
+ <xi:include href="feature-packagekit.xml"/>
+ </part>
+
+ <part id="development">
+ <title>Developer Guide</title>
+ <xi:include href="embedding.xml"/>
+ <xi:include href="packages.xml"/>
+ <xi:include href="urls.xml"/>
+ <xi:include href="api-base1.xml"/>
+ <xi:include href="api-shell.xml"/>
+ <xi:include href="api-system.xml"/>
+ </part>
+</book>
diff --git a/doc/guide/cockpit-http.xml b/doc/guide/cockpit-http.xml
new file mode 100644
index 0000000..bc83d10
--- /dev/null
+++ b/doc/guide/cockpit-http.xml
@@ -0,0 +1,302 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-http">
+ <refnamediv>
+ <refname>cockpit.js: HTTP Client</refname>
+ <refpurpose>HTTP and REST API communication</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <para>Cockpit allows access to local HTTP and REST services via this API.</para>
+ </refsynopsisdiv>
+
+ <refsection id="cockpit-http-constructor">
+ <title>cockpit.http()</title>
+<programlisting>
+http = cockpit.http(endpoint, [options])
+http = cockpit.http(options)
+</programlisting>
+
+ <para>Create a new HTTP client. The <code>endpoint</code> can be a file path starting with
+ <code>/</code> to connect to a unix socket, or it can be a port number to connect to.
+ The optional <code>options</code> argument is a javascript plain object, and may
+ include:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>"address"</code></term>
+ <listitem><para>Connect to an address other than localhost. Must be a valid host name or IP address.
+ To use this option you also must provide a port number.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"port"</code></term>
+ <listitem><para>Port number to use with "address" option, when not given in <code>endpoint</code>.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"tls"</code></term>
+ <listitem><para>Object properties for an https connection. See
+ <code><ulink url="https://github.com/cockpit-project/cockpit/blob/main/doc/protocol.md#payload-http-stream2">http-stream2 TLS options</ulink></code>.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"connection"</code></term>
+ <listitem><para>A connection identifier. Subsequent channel requests with the same
+ identifier will try to use the same connection if it is still open.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><code>"headers"</code></term>
+ <listitem><para>Additional HTTP headers to include with the HTTP request. This is a plain
+ javascript object with each key as a header name, and each value as the header
+ value.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><code>"superuser"</code></term>
+ <listitem><para>Set to <code>"require"</code> to open this channel as root. If the
+ currently logged in user is not permitted to become root (eg: via <code>pkexec</code>)
+ then the <code>channel</code> will immediately be
+ <link linkend="cockpit-channels-close-ev">closed</link> with a <code>"access-denied"</code>
+ problem code.</para>
+ <para>Set to <code>"try"</code> to try to make the request as root, but if that fails,
+ fall back to perform an unprivileged request.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><code>"tls"</code></term>
+ <listitem>
+ <para>If set to a plain javascript object, then the connection will be an HTTPS
+ connection and include TLS encryption. The fields of the <code>tls</code> object
+ declare various TLS configuration and data. All fields are optional:</para>
+ <itemizedlist>
+ <listitem><para><code>"authority"</code>: Certificate authority(s) to expect as signers
+ of the server's TLS certificate, represented as a plain javascript object.
+ It should have either a <code>"file"</code> field containing a
+ readable PEM file on the system containing authorities, or a <code>"data"</code> with
+ PEM encoded certificate data.</para></listitem>
+ <listitem><para><code>"certificate"</code>: A client certificate to use, represented as
+ a plain javascript object. It should have either a <code>"file"</code> field containing a
+ readable PEM file on the system to use as a certificate, or a <code>"data"</code> with
+ PEM encoded certificate data.</para></listitem>
+ <listitem><para><code>"key"</code>: A client key to use, represented as
+ a plain javascript object. It should have either a <code>"file"</code> field containing a
+ readable PEM file on the system to use as a key, or a <code>"data"</code> with
+ PEM encoded key data.</para></listitem>
+ <listitem><para><code>"validate"</code>: A boolean that describes whether to validate
+ the server's TLS certificate or not. By default local connections are not validated,
+ and remote connections are validated.</para></listitem>
+ </itemizedlist>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ <para>Here is a somewhat complex example of using most of the above <code>options</code> when
+ when calling <code>cockpit.http()</code>:</para>
+
+<programlisting>
+http = cockpit.http({
+ "address": "localhost",
+ "headers": {
+ "Authorization": "Basic dXNlcjpwYXNzd29yZA=="
+ },
+ "port": 443,
+ "tls": {
+ "validate": true,
+ "authority": {
+ "file": "/etc/pki/tls/certs/ca-bundle.crt",
+ },
+ "certificate": {
+ "data": "-----BEGIN CERTIFICATE-----\nMIIDsDCCA..."
+ },
+ "key": {
+ "data": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBA..."
+ }
+ }
+});
+</programlisting>
+
+ </refsection>
+
+ <refsection id="cockpit-http-get">
+ <title>http.get()</title>
+<programlisting>
+request = http.get(path, [params, [headers]])
+</programlisting>
+ <para>Perform an HTTP GET request for the given <code>path</code>. If the <code>params</code>
+ is specified it should be a plain javascript object, which will be turned into a query string.</para>
+ <para>Optionally a plain javascript object containing headers can be included in the
+ <code>headers</code> argument.</para>
+ <para>The return value is a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ that will complete if the request happens successfully, or fail if there's a problem.</para>
+ </refsection>
+
+ <refsection id="cockpit-http-post">
+ <title>http.post()</title>
+<programlisting>
+request = http.post(path, body, [headers])
+</programlisting>
+ <para>Perform an HTTP POST request for the given <code>path</code>. The <code>body</code>
+ can be a string, or a javascript plain object, which will be encoded as JSON data. If
+ <code>body</code> is <code>undefined</code> or <code>null</code> then an empty HTTP body
+ will be sent.</para>
+ <para>Optionally a plain javascript object containing headers can be included in the
+ <code>headers</code> argument.</para>
+ <para>The return value is a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ that will complete if the request happens successfully, or fail if there's a problem.</para>
+ </refsection>
+
+ <refsection id="cockpit-http-request">
+ <title>http.request()</title>
+<programlisting>
+request = http.request(options)
+</programlisting>
+ <para>Perform an HTTP request. The <code>options</code> can contain the following:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>"body"</code></term>
+ <listitem><para>The HTTP request body. If you do not specify a body, then you must
+ call <link linkend="cockpit-http-input">request.input()</link> to complete the body
+ and allow the request to start.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"headers"</code></term>
+ <listitem><para>A javascript plain object containing HTTP headers.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"method"</code></term>
+ <listitem><para>The HTTP method. Defaults to <code>"GET"</code>.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"params"</code></term>
+ <listitem><para>A javascript plain object containing query string parameters.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"path"</code></term>
+ <listitem><para>The HTTP path. Defaults to <code>/</code>.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>The return value is a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ that will complete if the request happens successfully, or fail if there's a problem.</para>
+ </refsection>
+
+ <refsection id="cockpit-http-then">
+ <title>request.then()</title>
+<programlisting>
+request.then(data => { ... })
+</programlisting>
+ <para>This is a standard
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink>
+ method. It sets up a handler to be called when the request finishes successfully.</para>
+ <para>The <code>data</code> argument contains the body result of the request.
+ If it a string, unless the process was opened in binary mode, in which case the
+ <code>data</code> is an array of bytes. If a
+ <code><link linkend="cockpit-http-stream">request.stream()</link></code>
+ handler is set up, then any standard output data consumed by the handler will not
+ be included in the <code>data</code> argument.</para>
+ </refsection>
+
+ <refsection id="cockpit-http-catch">
+ <title>request.catch()</title>
+<programlisting>
+request.catch((exception[, data]) => { ... })
+</programlisting>
+ <para>This is a standard
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink> method.
+ It sets up a handler to be called when the request fails, or returns an error code.</para>
+
+ <para>The <code>exception</code> object passed to the handler can have the
+ following fields:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>problem</code></term>
+ <listitem><para>A <link linkend="cockpit-problems">problem code</link> string when
+ a problem occurred starting or communicating with the server. This is <code>null</code>
+ if the process exited or was terminated.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>status</code></term>
+ <listitem><para>The numeric status of the response. This is <code>null</code> if
+ no response was received.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>reason</code></term>
+ <listitem><para>A string reason returned in the response. This is <code>null</code> if
+ no response was received.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>message</code></term>
+ <listitem><para>A string message returned in the response. This is <code>null</code> if
+ no response was received.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>If the request returned a response body, it will be available in
+ the <code>data</code> argument. Otherwise this argument will be <code>undefined</code>.</para>
+ </refsection>
+
+ <refsection id="cockpit-http-response">
+ <title>request.response()</title>
+<programlisting>
+request.response((status, headers) => { ... })
+</programlisting>
+ <para>This sets up a handler to be called when the HTTP request gets the initial response
+ from the server. The <code>status</code> argument is the HTTP status integer, and the
+ <code>headers</code> is a plain javascript object containing the headers of the
+ response.</para>
+ </refsection>
+
+ <refsection id="cockpit-http-stream">
+ <title>request.stream()</title>
+<programlisting>
+request.stream(data => { ... })
+</programlisting>
+ <para>This sets up a handler to be called when the request returns output data. The
+ handler will be called multiple times.</para>
+ <para>Only one handler may be registered at a time. Registering an additional handler
+ replaces the previous one. The handler receives either string <code>data</code> or
+ an array of binary bytes as its argument. A stream handler may return a number, which
+ indicates the number of characters or bytes consumed from <code>data</code>. Any data
+ not consumed will be included again the next time the handler is called.</para>
+ <para>If a <code>request.stream()</code> handler is set up, then the
+ <code><link linkend="cockpit-http-then">request.then()</link></code> handlers will
+ only get any remaining data not consumed by the stream handler.</para>
+ </refsection>
+
+ <refsection id="cockpit-http-input">
+ <title>request.input()</title>
+<programlisting>
+request.input(data, [stream])
+</programlisting>
+ <para>This method writes <code>data</code> to the HTTP request body. It is only valid
+ if no <code>"body"</code> has been specified in
+ <link linkend="cockpit-http-request">http.request()</link> options. If <code>stream</code>
+ is <code>true</code> then this function can be called again to provide further data.</para>
+ </refsection>
+
+ <refsection id="cockpit-http-close">
+ <title>request.close()</title>
+<programlisting>
+request.close([problem])
+</programlisting>
+ <para>Cancel the request. If <code>problem</code> is specified it should be a
+ standard <link linkend="cockpit-problems">problem code</link> string.</para>
+ </refsection>
+
+ <refsection id="cockpit-http-close-all">
+ <title>http.close()</title>
+ <programlisting>
+http.close([problem])
+ </programlisting>
+ <para>Cancel all outstanding requests with the given problem code. This is useful when
+ you know that the server is going down soon.
+ </para>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/cockpit-locale.xml b/doc/guide/cockpit-locale.xml
new file mode 100644
index 0000000..1882d5a
--- /dev/null
+++ b/doc/guide/cockpit-locale.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-locale">
+ <refnamediv>
+ <refname>cockpit.js: Localization</refname>
+ <refpurpose>Localization and translations</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <para>Cockpit provides a
+ <ulink url="https://www.gnu.org/software/gettext/"><code>gettext()</code></ulink> like
+ API for easy translation of strings.</para>
+ </refsynopsisdiv>
+
+ <refsection id="cockpit-locale-language">
+ <title>cockpit.language</title>
+ <para>The current locale language code. This is set based on the
+ <link linkend="cockpit-locale-locale"><code>cockpit.locale()</code></link> data loaded.</para>
+ </refsection>
+
+ <refsection id="cockpit-locale-locale">
+ <title>cockpit.locale()</title>
+<programlisting>
+cockpit.locale(po)
+</programlisting>
+
+ <para>Load locale information for a given <code>po</code> data. The data should
+ be JSON data in the <ulink url="https://www.npmjs.org/package/po2json">po2json</ulink>
+ format. The data will be loaded globally. If <code>po</code> data has already been
+ loaded, then this will extend that loaded data with additional strings. Any identical
+ translations strings will be replaced with the new strings. A <code>null</code> argument
+ clears all the locale information previously loaded.</para>
+
+ <para>Various methods such as
+ <link linkend="cockpit-locale-gettext"><code>cockpit.gettext()</code></link> make use
+ of the loaded data.</para>
+ </refsection>
+
+ <refsection id="cockpit-locale-gettext">
+ <title>cockpit.gettext()</title>
+<programlisting>
+translated = cockpit.gettext([context], string)
+var _ = cockpit.gettext
+var C_ = cockpit.gettext
+translated = _("string")
+translated = C_("context", "string")
+</programlisting>
+
+ <para>Lookup <code>string</code> for translation in the loaded locale data. The translated string will
+ be returned, or <code>string</code> will be returned if no such translated string is
+ present. The <code>context</code> argument is an optional string used to qualify the
+ string.</para>
+
+ <para>This function can be assigned to a variable called <code>_</code> (underscore) which
+ will make your code work with the typical <code>_("string")</code> syntax.</para>
+ </refsection>
+
+ <refsection id="cockpit-locale-noop">
+ <title>cockpit.noop()</title>
+<programlisting>
+var N_ = cockpit.noop
+var NC_ = cockpit.noop
+</programlisting>
+
+ <para>A noop function suitable for assigning to <code>N_</code> or <code>NC_</code> so that
+ gettext scanners will be able to find translatable strings. More specifically this function
+ returns its last argument.</para>
+ </refsection>
+
+ <refsection id="cockpit-locale-ngettext">
+ <title>cockpit.ngettext()</title>
+<programlisting>
+translated = cockpit.ngettext([context], string1, stringN, number)
+</programlisting>
+
+ <para>Lookup a string appropriate for a pluralization form of the <code>number</code>.
+ Various languages have complex pluralization forms that go far between the singular
+ and plural forms speakers of English are familiar with. If no such translated
+ string is found then either one of <code>string1</code> or <code>stringN</code> is
+ returned according to simple pluralization rules.</para>
+
+ <para>The <code>context</code> argument is an optional string used to qualify the string.</para>
+ </refsection>
+
+ <refsection id="cockpit-locale-translate">
+ <title>cockpit.translate()</title>
+<programlisting>
+cockpit.translate()
+cockpit.translate(element, ...)
+cockpit.translate(selection)
+</programlisting>
+
+ <para>The document will be scanned for <code>translate</code> tags and they will be translated according
+ to the strings in loaded locale data. One or more <code>element</code> arguments may be specified.
+ These are DOM elements for specific parts of the document to be translated. If no <code>element</code>
+ is specified then the entire document is translated.</para>
+
+ <para>If an array or array-like object is passed as a <code>selection</code> then all DOM elements
+ in the array will be treated as parts of the document to be translated.</para>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/cockpit-location.xml b/doc/guide/cockpit-location.xml
new file mode 100644
index 0000000..eb5f26b
--- /dev/null
+++ b/doc/guide/cockpit-location.xml
@@ -0,0 +1,185 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-location">
+ <refnamediv>
+ <refname>cockpit.js: Page Location and Jumping</refname>
+ <refpurpose>Page location and navigation between components</refpurpose>
+ </refnamediv>
+
+ <refsection id="cockpit-location-general">
+ <title>Page location</title>
+
+<programlisting>
+location = cockpit.location
+cockpit.location = "/path"
+</programlisting>
+
+ <para>Cockpit components often have different views, without changing the HTML file that is
+ being viewed. These are known as pages. <code>cockpit.location</code> is an object that can
+ be used to read the current page and to navigate to a different page location. It works by
+ updating <code>window.location.hash</code>.</para>
+
+ <para>The <code>cockpit.location</code> looks like a HTTP path with a possible query
+ string:</para>
+
+<programlisting>
+/path/sub/page?option=value,option2
+</programlisting>
+
+ <para>The <link linkend="cockpit-location-path"><code>location.path</code></link> and
+ <link linkend="cockpit-location-options"><code>location.options</code></link> contain a parsed
+ form of the location. While the location cannot be modified in place, a new one can be
+ created by assigning a string to <code>cockpit.location</code> or by calling the
+ <link linkend="cockpit-location-go"><code>location.go()</code></link> function.</para>
+
+ <para><code>cockpit.location</code> is designed similarly to <code>window.location</code>
+ in that the location object is preplaced whenever the current page location changes. To be
+ aware of when the page location changes listen for the
+ <link linkend="cockpit-location-changed"><code>cockpit.onlocationchanged</code></link>
+ event.</para>
+
+ <para>Using the location object as a string will result in the
+ <link linkend="cockpit-location-href"><code>location.href</code></link>.</para>
+
+ <refsection id="cockpit-location-href">
+ <title>location.href</title>
+ <para>The string representation of this page location, including any options.</para>
+ </refsection>
+
+ <refsection id="cockpit-location-path">
+ <title>location.path</title>
+ <para>An array of path segments, parsed and decoded appropriately. An empty array denotes
+ the root path.</para>
+ </refsection>
+
+ <refsection id="cockpit-location-options">
+ <title>location.options</title>
+ <para>A javascript object containing the various options present in the location.</para>
+ <para>If an option appears more than once, its value will be an array.</para>
+ </refsection>
+
+ <refsection id="cockpit-location-go">
+ <title>location.go()</title>
+<programlisting>
+location.go(path, [options])
+</programlisting>
+ <para>Changes the current location to the given <code>path</code> and <code>options</code>.
+ If the <code>path</code> argument is a string, it will be parsed into a path. If it is
+ a relative path, then the result will be relative to the current <code>location.path</code>.
+ If the <code>path</code> argument is an array of path segments, it will be treated as a
+ full parsed absolute path.</para>
+
+ <para>Any options found in a <code>path</code> will be added to those in the optional
+ <code>options</code> argument, and used in the result.</para>
+
+ <para>The location change will only take effect if the location has not changed in the
+ meantime. This can be to good effect by saving a <code>cockpit.location</code> object
+ and doing a conditional navigation, by calling the saved <code>location.go()</code>
+ method later. This will only navigate if the user or other code has not navigated in
+ the meantime.</para>
+ </refsection>
+
+ <refsection id="cockpit-location-replace">
+ <title>location.replace()</title>
+<programlisting>
+location.replace(path, [options])
+</programlisting>
+ <para>Similar to <link linkend="cockpit-location-go"><code>location.go()</code></link>
+ except the location change will not result in a navigation change in the browser's
+ history.</para>
+ </refsection>
+
+ <refsection id="cockpit-location-decode">
+ <title>location.decode()</title>
+<programlisting>
+path = location.decode(href, [options])
+</programlisting>
+ <para>Decode a cockpit href into its <code>path</code> array. If the <code>options</code>
+ argument is specified, then it will be populated with options found in the href.</para>
+
+ <para>If href is a relative path it will be resolved relative to
+ <code>location.href</code>.</para>
+ </refsection>
+
+ <refsection id="cockpit-location-encode">
+ <title>location.encode()</title>
+<programlisting>
+href = location.encode(path, [options])
+</programlisting>
+ <para>Encode the given <code>path</code> and <code>options</code> into a cockpit href.
+ The <code>path</code> argument may be an array of path segments, or a string path. If
+ a relative path is passed, it will be resolved relative to <code>location.href</code>.</para>
+ </refsection>
+
+ <refsection id="cockpit-location-changed">
+ <title>cockpit.onlocationchanged</title>
+<programlisting>
+cockpit.addEventListener("locationchanged", function() { ... })
+</programlisting>
+ <para>An event emitted when over the <code>cockpit.location</code> changes. Typically a
+ component reacts to this event by updating its interface to reflect the new
+ <link linkend="cockpit-location-path"><code>cockpit.location.path</code></link> and
+ <link linkend="cockpit-location-options"><code>cockpit.location.options</code></link>.</para>
+
+ <para>This event is not triggered immediately during a <code>location.go()</code> or
+ similar call. It will be triggered asynchronously at a later time.</para>
+ </refsection>
+ </refsection>
+
+ <refsection id="cockpit-jump">
+ <title>Jumping between components</title>
+
+<programlisting>
+cockpit.jump("/system/log")
+</programlisting>
+
+ <para>In Cockpit in there multiple components shown. In order to tell Cockpit to jump to and show
+ another component and a certain location within that component, use the
+ <code>cockpit.jump()</code> function. Stable component paths are documented. Don't assume
+ you can navigate into paths that are not stable API.</para>
+
+ <refsection id="cockpit-jump-jump">
+ <title>cockpit.jump()</title>
+<programlisting>
+cockpit.jump(path, [ host ])
+</programlisting>
+ <para>Ask Cockpit to jump to another component. The location of the current component will
+ not be affected. The <code>path</code> argument can be a string path, starting with <code>/</code>
+ or an array containing the parts of a path that will be joined to create a path. If <code>host</code>
+ is not specified, then the component on the same host as the caller will be displayed. If
+ host is null, then the host portion of the path will be removed, displaying the component on
+ the host that cockpit is connected directly to.</para>
+ <para>If the calling component is not running within Cockpit, or the calling component is not
+ currently displayed, then the jump will not happen, and this function has no effect.</para>
+ </refsection>
+
+ <refsection id="cockpit-jump-hidden">
+ <title>cockpit.hidden</title>
+
+ <para>A boolean property that indicates if the current component page is visible or hidden.
+ When the code or user jumps to another component, the prior one remains loaded and initialized
+ but is hidden. Use this property together with the
+ <link linkend="cockpit-jump-visibilitychange"><code>cockpit.onvisibilitychange</code></link>
+ event to decide whether or not to perform expensive tasks to update the interface.</para>
+
+ <para>This property is analogous to the <code>document.hidden</code> page visibility API, but
+ works with the document and frame implementation of Cockpit.</para>
+ </refsection>
+
+ <refsection id="cockpit-jump-visibilitychange">
+ <title>cockpit.onvisibilitychange</title>
+
+<programlisting>
+cockpit.onvisibilitychange = function() { ... }
+</programlisting>
+
+ <para>This event is emitted when the
+ <link linkend="cockpit-jump-hidden"><code>cockpit.hidden</code></link> property changes.
+ This event is similar to the <code>document.onvisibilitychange</code> API, but works with
+ the document and frame implementation of Cockpit.</para>
+
+ </refsection>
+
+ </refsection>
+</refentry>
diff --git a/doc/guide/cockpit-manifest.xml b/doc/guide/cockpit-manifest.xml
new file mode 100644
index 0000000..8efbb70
--- /dev/null
+++ b/doc/guide/cockpit-manifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-manifest">
+ <refnamediv>
+ <refname>cockpit.js: Manifests</refname>
+ <refpurpose>Manifest info</refpurpose>
+ </refnamediv>
+
+ <refsection id="cockpit-manifest-loading">
+ <title>Loading Manifests</title>
+ <para>You can load manifest info by loading the <code>./manifest.json</code> file in
+ your package. In addition there is a shortcut, by loading the <code>../manifests.json</code>
+ you can load all the manifests at once.</para>
+
+ <para>Lastly load the <code>../manifests.js</code> file to register the manifests at
+ the <code>cockpit.manifests</code> global variable.</para>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/cockpit-metrics.xml b/doc/guide/cockpit-metrics.xml
new file mode 100644
index 0000000..2150407
--- /dev/null
+++ b/doc/guide/cockpit-metrics.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-metrics">
+ <refnamediv>
+ <refname>cockpit.js: Metrics</refname>
+ <refpurpose>Reading and streaming metric data</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <para>Metrics about the system can be retrieved from several sources using
+ <link linkend="cockpit-metrics"><code>cockpit.metrics()</code></link> metrics channels.
+ The metrics are made available as series data, and can be used with the
+ <link linkend="cockpit-series"><code>cockpit.series()</code></link> and
+ <link linkend="cockpit-grid"><code>cockpit.grid()</code></link> facilities.</para>
+ </refsynopsisdiv>
+
+ <refsection id="cockpit-metrics-function">
+ <title>cockpit.metrics()</title>
+<programlisting>
+metrics = cockpit.metrics(interval, options, cache)
+</programlisting>
+
+ <para>Opens a new metrics channel. The data retrieved will be available in the
+ <link linkend="cockpit-metrics-series"><code>metrics.series</code></link> series sink, and can
+ be used together with <link linkend="cockpit-grid"><code>cockpit.grid()</code></link> objects.</para>
+
+ <para>The <code>interval</code> is in milliseconds, and is the granularity of the series data
+ retrieved. Any grids consuming the data must have the same interval.</para>
+
+ <para>The <code>cache</code> argument is a cache identifier. If
+ specified, then this metrics channel will share data with other
+ metrics channels of the same identifier. Make sure to use a
+ globally unique string.</para>
+
+ <para>The <code>options</code> argument is either a javascript
+ plain object, or an array of those. Each object can have the
+ following fields.</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>"metrics"</code></term>
+ <listitem><para>An array of full metric descriptions, as
+ javascript objects. The specifics of these, and how to
+ determine which ones to use, can unfortunately only be found
+ in the low-level protocol documentation. This option is
+ required.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"source"</code></term>
+ <listitem><para>The source to use for real-time data. This
+ is used by the <code>follow</code> method, see below. Set
+ to <code>"internal"</code> to retrieve internal metrics read
+ by the bridge. If set to <code>"direct"</code> or
+ <code>"pmcd"</code> then data will be retrieved from <ulink
+ url="https://pcp.io">PCP</ulink>if it is available. The
+ default is <code>"internal"</code>.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"archive_source"</code></term>
+ <listitem><para>The source to use for retrieving historical
+ data. This is used by the <code>fetch</code> method, see
+ below. Set to <code>"pcp-archive"</code> to retrieve data
+ from PCP archives. The default is not to try to retrieve
+ historical data.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>When the <code>options</code> argument is an array of
+ javascript objects, then the metrics channel tries to use them in
+ order until one succeeds. This way, you can prefer PCP as the
+ source but fall back to internal metrics when PCP is not
+ available, for example. The channel gives no indication which
+ of the options has been used, and <code>fetch</code> and
+ <code>follow</code> might use different entries from the
+ list.</para>
+ </refsection>
+
+ <refsection id="cockpit-metrics-fetch">
+ <title>metrics.fetch()</title>
+<programlisting>
+metrics.fetch(beg, end)
+</programlisting>
+
+ <para>Retrieve archived metrics data between <code>beg</code> and <code>end</code>. The
+ arguments can either be numbers, in which case they are interval based offsets, or they
+ can be javascript Date objects.</para>
+ </refsection>
+
+ <refsection id="cockpit-metrics-follow">
+ <title>metrics.follow()</title>
+<programlisting>
+metrics.follow()
+</programlisting>
+
+ <para>Start retrieving live metrics data as it become available.</para>
+ </refsection>
+
+ <refsection id="cockpit-metrics-close">
+ <title>metrics.close()</title>
+<programlisting>
+metrics.close()
+</programlisting>
+
+ <para>Stop the retrieval of metrics and release resources.</para>
+ </refsection>
+
+ <refsection id="cockpit-metrics-series">
+ <title>metrics.series</title>
+ <para>The series sink where data retrieved data will be processed.</para>
+ </refsection>
+
+ <refsection id="cockpit-metrics-meta">
+ <title>metrics.meta</title>
+ <para>The metrics meta data last received.</para>
+ </refsection>
+
+ <refsection id="cockpit-metrics-onchanged">
+ <title>metrics.onchanged</title>
+<programlisting>
+metrics.onchanged = function() { }
+</programlisting>
+ <para>An event triggered when one of the properties on this metrics object changes.</para>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/cockpit-series.xml b/doc/guide/cockpit-series.xml
new file mode 100644
index 0000000..e3608a9
--- /dev/null
+++ b/doc/guide/cockpit-series.xml
@@ -0,0 +1,277 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-series-data">
+ <refnamediv>
+ <refname>cockpit.js: Series Data</refname>
+ <refpurpose>Representing series data</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+
+ <para>Series data consists of values along a continuous (usually time) axis. We
+ can place these in grids which expose a distinct subset of these values. These are
+ the underlying mechanism for displaying metrics data in graphs.</para>
+ </refsynopsisdiv>
+
+ <refsection id="cockpit-grid">
+ <title>cockpit.grid()</title>
+<programlisting>
+grid = cockpit.grid(interval, [beg, end])
+</programlisting>
+
+ <para>Creates a grid object to contain series data.</para>
+
+ <para>The <code>interval</code> is the granularity of the grid. Usually this is
+ a number of milliseconds, when used with time series data. The <code>beg</code>
+ and <code>end</code> are the bounds of the grid. If omitted they will be set to
+ zero for an initially empty grid.</para>
+
+ <para>If <code>beg</code> and/or <code>end</code> are negative (including negative
+ zero) then they are interpreted in number of intervals relative to the current
+ time. Thus cockpit.grid(1000, -300, -0) will create a grid for the most recent
+ 5 minutes.</para>
+ </refsection>
+
+ <refsection id="cockpit-grid-add">
+ <title>grid.add()</title>
+<programlisting>
+row = grid.add(series, path)
+row = grid.add(callback, [early])
+row = grid.add()
+</programlisting>
+
+ <para>Adds a row to the grid. The returned <code>row</code> is a Javascript array that will contain
+ series data. The arguments control how the row is populated from the series data.
+ The <code>row</code> is a sparse array. Its <code>row.length</code> will not match the
+ expected size of the grid, unless and until the row has been completely filled in. The first
+ index of the <code>row</code> will contain the data from the series data at the
+ <link linkend="cockpit-grid-beg"><code>grid.beg</code></link> offset.</para>
+
+ <para>When no arguments are passed, an empty row is added, and it is not populated with data.</para>
+
+ <para>When called with a <code>series</code> and <code>path</code> argument then the row
+ will be populated directly with series data. The <code>series</code> can either be a
+ <link linkend="cockpit-series">series object</link> or an object that has an <code>obj.series</code>
+ property. The <link linkend="cockpit-series-interval">series interval</link> must match the
+ interval of this grid.
+ If <code>path</code> is missing or empty, then the series data is placed into the row
+ directly. Otherwise <code>path</code> indicates which part of the series data to place in the
+ row. When <code>path</code> is an array, it is used as a set of property names or array indexes
+ to follow into nested series data. When <code>path</code> is a dotted string, it is split and used
+ the same way to locate the correct value in nested series data. The exact format of the series
+ data depends on its producer, and relevant paths will be documented there.</para>
+
+ <para>If a <code>callback</code> function is specified, then it will be invoked to provide series data
+ for the row. The function is invoked as <code>callback(row, index, count)</code>, where the
+ <code>row</code> is the row to fill in, the <code>index</code> is the index to start filling in and
+ <code>count</code> is the number of items to fill in. The <code>this</code> variable will be set
+ to the grid while invoking the <code>callback</code>. The callback is called after other data
+ rows for a given series have been filled in. Callbacks are called in the order added, unless the
+ <code>early</code> argument is set to <code>true</code>, in which case the callback is called earlier
+ than callbacks without the <code>early</code> argument set.</para>
+
+ <para>To remove the row use the
+ <link linkend="cockpit-grid-remove"><code>grid.remove()</code> method.</link>
+ </para>
+
+ <para>The row will start being populated with data when the <code>series</code> produces data.
+ To make this happen right away, use the
+ <link linkend="cockpit-grid-sync"><code>grid.sync()</code></link> method.</para>
+
+ </refsection>
+
+ <refsection id="cockpit-grid-remove">
+ <title>grid.remove()</title>
+<programlisting>
+grid.remove(row)
+</programlisting>
+
+ <para>Remove a previously added <code>row</code> from the grid. The row will no longer be updated
+ with series data.</para>
+ </refsection>
+
+ <refsection id="cockpit-grid-sync">
+ <title>grid.sync()</title>
+<programlisting>
+grid.sync()
+</programlisting>
+
+ <para>Load or reload data from the series into the rows. This does not clear the rows before
+ populating them. Some data may be populated immediately, others may have to wait until data
+ can be loaded. Internally this function calls
+ <link linkend="cockpit-series-load"><code>series.load()</code></link> for each series.</para>
+
+ <para>All rows with callbacks will be invoked to regenerate all the data. The
+ <link linkend="cockpit-grid-onnotify"><code>grid.onnotify</code></link> event will be triggered.
+ It is not necessary to call this function after a call of the
+ <link linkend="cockpit-grid-move"><code>grid.move()</code></link> method.</para>
+
+ </refsection>
+
+ <refsection id="cockpit-grid-move">
+ <title>grid.move()</title>
+<programlisting>
+grid.move(beg[, end])
+</programlisting>
+
+ <para>Move the grid to new <code>beg</code> and <code>end</code> range. Data will be
+ discarded from the rows and <link linkend="cockpit-grid-sync"><code>grid.sync()</code></link>
+ will be called to load or reload series data for the new range of offsets.</para>
+
+ <para>If <code>end</code> is not specified it will be set to <code>beg</code>. If <code>beg</code>
+ and/or <code>end</code> are negative (including negative zero) then they will be set to the
+ number of intervals prior to the current time taken as an interval.</para>
+
+ <para>If <code>beg</code> and/or <code>end</code> are negative (including negative
+ zero) then they are interpreted in number of intervals relative to the current
+ time. Thus cockpit.grid(1000, -300, -0) will create a grid for the most recent
+ 5 minutes.</para>
+ </refsection>
+
+ <refsection id="cockpit-grid-walk">
+ <title>grid.walk()</title>
+<programlisting>
+grid.walk()
+</programlisting>
+
+ <para>Move the grid forward every
+ <link linkend="cockpit-grid-interval"><code>grid.interval</code></link> milliseconds. To stop
+ moving forward, call <link linkend="cockpit-grid-move"><code>grid.move()</code></link>.</para>
+ </refsection>
+
+ <refsection id="cockpit-grid-notify">
+ <title>grid.notify()</title>
+<programlisting>
+grid.notify(index, count)
+</programlisting>
+
+ <para>This function is called to have rows with callbacks recalculate their data. It is not
+ normally necessary to call this function, as it will be invoked automatically when new
+ series data is available or has been loaded. This function triggers the
+ <link linkend="cockpit-grid-onnotify"><code>grid.onnotify</code></link> event.</para>
+
+ </refsection>
+
+ <refsection id="cockpit-grid-onnotify">
+ <title>grid.onnotify</title>
+<programlisting>
+grid.addEventListener("notify", function(index, count) { ... });
+</programlisting>
+
+ <para>An event that is triggered when some part of the series data in grid changes. The
+ <code>index</code> is the row index where things changed, and the <code>count</code>
+ is the length of the data that changed.</para>
+
+ </refsection>
+
+ <refsection id="cockpit-grid-close">
+ <title>grid.close()</title>
+<programlisting>
+grid.close()
+</programlisting>
+
+ <para>Close the grid, and stop updating the rows.</para>
+ </refsection>
+
+ <refsection id="cockpit-grid-interval">
+ <title>grid.interval</title>
+
+ <para>The granularity of the grid. For time series data this is an interval in
+ milliseconds. In order to use a given
+ <link linkend="cockpit-grid">grid</link> and
+ <link linkend="cockpit-series">series</link> together, their interval properties
+ must match.</para>
+ </refsection>
+
+ <refsection id="cockpit-grid-beg">
+ <title>grid.beg</title>
+
+ <para>The beginning offset of the series data in the grid. Do not set this property
+ directly. Use the <link linkend="cockpit-grid-move">grid.move()</link> method instead.</para>
+ </refsection>
+
+ <refsection id="cockpit-grid-end">
+ <title>grid.end</title>
+
+ <para>The ending offset of the series data in the grid. Do not set this property
+ directly. Use the <link linkend="cockpit-grid-move">grid.move()</link> method instead.</para>
+ </refsection>
+
+ <refsection id="cockpit-series">
+ <title>cockpit.series()</title>
+<programlisting>
+series = cockpit.series(interval, [cache, fetch])
+</programlisting>
+
+ <para>Create a new sink of series data. This is usually done by producers of series data,
+ and it is rare to invoke this function directly.</para>
+
+ <para>The <code>interval</code> is the granularity of the series data. For time series data
+ this is an interval in milliseconds. If a <code>cache</code> string is specified, series data
+ will be cached across frames for series with the same <code>cache</code> cache identifier
+ to load and/or reload.</para>
+
+ <para>If a <code>fetch</code> callback
+ is specified, then it will be invoked when grids request certain ranges of data. The
+ <code>fetch</code> callback is invoked with <code>function fetch(beg, end) { ... }</code>
+ range offsets. The <link linkend="cockpit-series-input">series.input()</link> should be
+ called with data retrieved, either immediately or at a later time. The callback may be
+ called multiple times for the same ranges of data. It is up to the callback to determine
+ when or whether it should retrieve the data more than once.</para>
+
+ <para>A producer of series data, usually calls this function and creates itself a
+ <code>obj.series</code> property containing this series object.</para>
+ </refsection>
+
+ <refsection id="cockpit-series-input">
+ <title>series.input()</title>
+<programlisting>
+series.input(beg, items[, mapping])
+</programlisting>
+
+ <para>Send series data into the series sink. Any grids that have added rows based on this
+ series, will have data filled in. The <code>beg</code> is the beginning offset of
+ <code>items</code>. The <code>items</code> are an array one or more series data items.</para>
+
+ <para>Producers may wish to provide additional properties that can be used in lookup paths that
+ rows can pull from. This is done in the <code>mapping</code> argument. If specified it is
+ a tree of objects. Each sub object should have a property with the name <code>""</code>
+ empty string, which will be used as the property name or index in place of the one used
+ in the lookup path.</para>
+
+ </refsection>
+
+ <refsection id="cockpit-series-load">
+ <title>series.load()</title>
+<programlisting>
+series.load(beg, end)
+</programlisting>
+
+ <para>Load data from the series into any grids that have rows based on this series data.
+ Any cached data will be filled in immediately. Any data not cached, will be requested
+ from the producer, if possible, and may arrive at a later time.</para>
+
+ <para>The <code>beg</code> and <code>end</code> denote the range of data to load.</para>
+ </refsection>
+
+ <refsection id="cockpit-series-interval">
+ <title>series.interval</title>
+
+ <para>The granularity of the series. For time series data this is an interval in
+ milliseconds. In order to use a given
+ <link linkend="cockpit-grid">grid</link> and
+ <link linkend="cockpit-series">series</link> together, their interval properties
+ must match.</para>
+ </refsection>
+
+ <refsection id="cockpit-series-limit">
+ <title>series.limit</title>
+
+ <para>The maximum number of items to cache for loading and/or reloading. You can
+ change this value to a different number. Having a number close to zero will break
+ certain usage of grids, such as
+ <link linkend="cockpit-grid-walk"><code>grid.walk()</code></link>.</para>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/cockpit-session.xml b/doc/guide/cockpit-session.xml
new file mode 100644
index 0000000..467d185
--- /dev/null
+++ b/doc/guide/cockpit-session.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-login">
+ <refnamediv>
+ <refname>cockpit.js: User Session</refname>
+ <refpurpose>User information and login session state</refpurpose>
+ </refnamediv>
+
+ <refsection id="cockpit-logout">
+ <title>cockpit.logout()</title>
+<programlisting>
+cockpit.logout([reload])
+</programlisting>
+ <para>Logout of Cockpit. Unless <code>reload</code> is <code>false</code> this will also
+ cause the page to be reloaded, so that the user can see the logged out state.</para>
+ </refsection>
+
+ <refsection id="cockpit-user">
+ <title>cockpit.user()</title>
+<programlisting>
+var promise = cockpit.user();
+promise.then(user => { ... });
+</programlisting>
+ <para>This object contains information about the user that's currently logged into cockpit.
+ The following fields are defined:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>"id"</code></term>
+ <listitem><para>This is unix user id.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"name"</code></term>
+ <listitem><para>This is the unix user name like <code>"root"</code>.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"full_name"</code></term>
+ <listitem><para>This is a readable name for the user.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"groups"</code></term>
+ <listitem><para>This is an array of group names to which the user belongs.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"home"</code></term>
+ <listitem><para>This is user's home directory.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"shell"</code></term>
+ <listitem><para>This is unix user shell.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>Returns a promise that completes once the user information is available.</para>
+ </refsection>
+
+ <refsection id="cockpit-permission">
+ <title>Permission lookup</title>
+
+ <para>Cockpit provides a mechanism for checking if the current user satisfies a
+ given criteria. Currently capable of checking for root users, and group
+ membership. This is meant for updating UI elements based on what actions the
+ user can perform. It is <emphasis>not an access control mechanism</emphasis>.</para>
+
+ <refsection id="cockpit-permission-constructor">
+ <title>cockpit.permission()</title>
+<programlisting>
+permission = cockpit.permission([options])
+</programlisting>
+
+ <para>Create a new permission object to check if the current user has permission.
+ The "root" user is always given permission. The <code>options</code> argument
+ can contain a <code>"group"</code> field, and members of that group are also
+ given permission.</para>
+ </refsection>
+
+ <refsection id="cockpit-permission-allowed">
+ <title>permission.allowed</title>
+
+ <para>A boolean value which indicates if the permission is allowed or not. This will
+ be <code>null</code> if the permission is unknown, or there was an error checking
+ the permission or the permission data has not yet loaded. This property will update
+ asynchronously and if you wish to be notified of changes connect to the
+ <link linkend="cockpit-permission-changed">permission.onchanged</link> event.</para>
+ </refsection>
+
+ <refsection id="cockpit-permission-changed">
+ <title>permission.onchanged</title>
+<programlisting>
+permission.addEventListener("changed", function() { ... })
+</programlisting>
+ <para>This event is fired when the permission changes. In particular the
+ <link linkend="cockpit-permission-allowed">permission.allowed</link> property.</para>
+ </refsection>
+
+ <refsection id="cockpit-permission-close">
+ <title>permission.close()</title>
+<programlisting>
+permission.close()
+</programlisting>
+ <para>Closes the permission object and tears down any registered callbacks and dbus subscriptions.</para>
+ </refsection>
+
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/cockpit-spawn.xml b/doc/guide/cockpit-spawn.xml
new file mode 100644
index 0000000..db8eabd
--- /dev/null
+++ b/doc/guide/cockpit-spawn.xml
@@ -0,0 +1,227 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-spawn">
+ <refnamediv>
+ <refname>cockpit.js: Spawning Processes</refname>
+ <refpurpose>Spawning processes or scripts</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+
+ <para>This is the API for spawning a process and receiving its output, as well
+ as exit codes.</para>
+
+ </refsynopsisdiv>
+
+ <refsection id="cockpit-spawn-spawn">
+ <title>cockpit.spawn()</title>
+<programlisting>
+process = cockpit.spawn(args, [options])
+</programlisting>
+
+ <para>Spawns a process on the system.</para>
+
+ <para>The <code>args</code> should be an array starting with the executable and
+ containing all the arguments to pass on the command line. If <code>args</code>
+ is a string then it is interpreted as an executable name. The optional
+ <code>options</code> argument is a javascript plain object and can contain
+ any of the following fields:
+ </para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>"binary"</code></term>
+ <listitem><para>If set to <code>true</code> then handle the input and output
+ of the process as arrays of binary bytes.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"directory"</code></term>
+ <listitem><para>The directory to spawn the process in.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"err"</code></term>
+ <listitem><para>Controls where the standard error is sent. By default it is logged
+ to the journal. If set to <code>"out"</code> it is included in with the
+ output data. If set to <code>"ignore"</code> then the error output is discarded.
+ If set to <code>"message"</code>, then it will be returned as the error message.
+ When the <code>"pty"</code> field is set, this field has no effect.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"environ"</code></term>
+ <listitem><para>An optional array that contains strings to be used as
+ additional environment variables for the new process. These are
+ <code>"NAME=VALUE"</code> strings.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"pty"</code></term>
+ <listitem><para>Launch the process in its own PTY terminal, and send/receive
+ terminal input and output.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"batch"</code></term>
+ <listitem><para>Batch data coming from the process in blocks of at least this
+ size. This is not a guarantee. After a short timeout the data will be sent
+ even if the data doesn't match the batch size. Defaults to zero.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"latency"</code></term>
+ <listitem><para> The timeout for flushing any cached data in milliseconds.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"superuser"</code></term>
+ <listitem><para>Set to <code>"require"</code> to spawn the process as root instead of
+ the logged in user. If the currently logged in user is not permitted to become root
+ (eg: via <code>pkexec</code>) then the <code>client</code> will immediately be
+ <link linkend="cockpit-dbus-onclose">closed</link> with a <code>"access-denied"</code>
+ problem code.</para>
+ <para>Set to <code>"try"</code> to try to run the process as root, but if that fails,
+ fall back to an unprivileged process.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>The spawned process is a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">promise</ulink>
+ that will complete if the process exits successfully, or fail if there's a problem.
+ Some additional methods besides the standard promise methods are documented
+ below.</para>
+
+ <para>The standard output of the process is made available via the spawned process
+ object. Any non-UTF8 output from the process will be coerced into textual form.
+ It is highly recommended that only textual output be produced by the command.
+ The standard error is logged to the journal.</para>
+ </refsection>
+
+ <refsection id="cockpit-spawn-script">
+ <title>cockpit.script()</title>
+<programlisting>
+process = cockpit.script(script, [args], [options])
+</programlisting>
+
+ <para>Run a shell script on the system.</para>
+
+ <para>This function <link linkend="cockpit-spawn-spawn">spawns</link> a Bourne shell script
+ process. The full text of the <code>/bin/sh</code> shell script should be passed in as
+ the first argument. The <code>args</code> can be an array of arguments, not including
+ the executable, which are passed to the script as <code>$1</code>, <code>$2</code> and
+ so on. Shebang options are not used or respected.</para>
+
+ <para>The <code>options</code> is an optional javascript plain object and can include
+ any of the fields listed for the
+ <link linkend="cockpit-spawn-spawn"><code>cockpit.spawn()</code></link> function.</para>
+
+ <para>The spawned process is a
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">promise</ulink>
+ that will complete if the script exits successfully, or fail if there's a problem.
+ Some additional methods besides the standard promise methods are documented
+ below.</para>
+
+ <para>The standard output of the process is made available via the spawned process
+ object. Any non-UTF8 output from the process will be coerced into textual form.
+ It is highly recommended that only textual output be produced by the command.
+ The standard error is logged to the journal by default.</para>
+ </refsection>
+
+ <refsection id="cockpit-spawn-then">
+ <title>process.then()</title>
+<programlisting>
+process.then((data[, message]) => { ... })
+</programlisting>
+ <para>This is a standard
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">promise</ulink>
+ method. It sets up a handler to be called when the process finishes successfully.</para>
+ <para>The <code>data</code> argument contains the standard output of the process.
+ If it a string, unless the process was opened in binary mode, in which case the
+ <code>data</code> is an array of bytes. If a
+ <code><link linkend="cockpit-spawn-stream">process.stream()</link></code>
+ handler is set up, then any standard output data consumed by the handler will not
+ be included in the <code>data</code> argument.</para>
+ <para>If the process was spawned with the <code>"err"</code> option set to
+ <code>"message"</code> then the second argument will contain the standard error
+ output of the process.</para>
+ </refsection>
+
+ <refsection id="cockpit-spawn-catch">
+ <title>process.catch()</title>
+<programlisting>
+process.catch((exception[, data]) => { ... })
+</programlisting>
+ <para>This is a standard
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</ulink> method.
+ It sets up a handler to be called when the process fails, terminates or exits.</para>
+
+ <para>The <code>exception</code> object passed to the handler can have the
+ following fields:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>message</code></term>
+ <listitem><para>A message describing the exception. If the process was spawned with
+ the <code>"err"</code> option set to <code>"message"</code> then the second argument
+ will contain the standard error output of the process.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>problem</code></term>
+ <listitem><para>A <link linkend="cockpit-problems">problem code</link> string when
+ a problem occurred starting or communicating with the process. This is <code>null</code>
+ if the process exited or was terminated.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>exit_status</code></term>
+ <listitem><para>The numeric exit status of the process. This is <code>null</code> if
+ the process did not exit.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>exit_signal</code></term>
+ <listitem><para>A string representing a unix signal that caused the process to terminate.
+ This is <code>null</code> if the process did not terminate because of a signal.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>If the process actually ran and produced output before failing, it will be available in
+ the <code>data</code> argument. Otherwise this argument will be <code>undefined</code>.</para>
+ </refsection>
+
+ <refsection id="cockpit-spawn-stream">
+ <title>process.stream()</title>
+<programlisting>
+process.stream(data => { ... })
+</programlisting>
+ <para>This sets up a handler to be called when the process has standard output. The
+ handler will be called multiple times. The handler will be called regardless of
+ whether the process ends up exiting successfully or not.</para>
+ <para>Only one handler may be registered at a time. Registering an additional handler
+ replaces the previous one. The handler receives either string <code>data</code> or
+ an array of binary bytes as its argument. A stream handler may return a number, which
+ indicates the number of characters or bytes consumed from <code>data</code>. Any data
+ not consumed will be included again the next time the handler is called.</para>
+ <para>If a <code>process.stream()</code> handler is set up, then the
+ <code><link linkend="cockpit-spawn-then">process.then()</link></code> handlers will
+ only get any remaining data not consumed by the stream handler.</para>
+ </refsection>
+
+ <refsection id="cockpit-spawn-input">
+ <title>process.input()</title>
+<programlisting>
+process.input(data, [stream])
+</programlisting>
+ <para>This method writes <code>data</code> to the standard input of the process.
+ If <code>data</code> is <code>null</code> or <code>undefined</code> it is not sent.
+ The <code>data</code> should be a string or an array of bytes if the process was
+ opened in binary mode.</para>
+ <para>If <code>stream</code> is set to <code>true</code> then this function may be
+ called again with further input. Otherwise the standard input of the process
+ is closed.</para>
+ </refsection>
+
+ <refsection id="cockpit-spawn-close">
+ <title>process.close()</title>
+<programlisting>
+process.close([problem])
+</programlisting>
+ <para>Close the process by closing its standard input and output. If <code>problem</code> is
+ specified it should be a standard
+ <link linkend="cockpit-problems">problem code</link> string. In this case the
+ process will be terminated with a signal.</para>
+ </refsection>
+</refentry>
diff --git a/doc/guide/cockpit-util.xml b/doc/guide/cockpit-util.xml
new file mode 100644
index 0000000..9bff53e
--- /dev/null
+++ b/doc/guide/cockpit-util.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<refentry id="cockpit-util">
+ <refnamediv>
+ <refname>cockpit.js: Utilities</refname>
+ <refpurpose>Various utility functions</refpurpose>
+ </refnamediv>
+
+ <refsection id="cockpit-format">
+ <title>cockpit.format()</title>
+<programlisting>
+string = cockpit.format(template, args)
+string = cockpit.format(template, [arg, ...])
+</programlisting>
+
+ <para>Format a string interpolating <code>args</code> into <code>template</code> using
+ shell like syntax. The <code>args</code> may be either an array or javascript object.
+ The <code>template</code> can contain fields that look like <code>$name</code> or
+ <code>${name}</code> or <code>$0</code>. Numeric fields are used with array
+ <code>args</code> and start at zero.</para>
+
+ <para>In the second form, multiple <code>arg</code> arguments may be passed directly,
+ and interpolated as as numeric fields in the <code>template</code>.</para>
+
+ <para>All falsy arguments except the numbers <code>0</code> and <code>0.0</code>are
+ replaced by an empty string.</para>
+ </refsection>
+
+ <refsection id="cockpit-format-number">
+ <title>cockpit.format_number()</title>
+<programlisting>
+string = cockpit.format_number(number, [precision])
+</programlisting>
+ <para>Formats <code>number</code> into a displayable <code>string</code>. If the number is not
+ an integer, it is rounded to the given number of decimal places, defaulting to 3. If the number
+ is near zero, but not quite zero it is rounded to the smallest non-zero value of the given precision;
+ i.e. ±0.001 for default precision 3.</para>
+
+ <para>If <code>number</code> is <code>null</code> or <code>undefined</code> an empty string
+ will be returned.</para>
+ </refsection>
+
+ <refsection id="cockpit-format-bytes">
+ <title>cockpit.format_bytes()</title>
+<programlisting>
+string = cockpit.format_bytes(number, [factor])
+array = cockpit.format_bytes(number, [factor, options])
+</programlisting>
+ <para>Formats <code>number</code> into a displayable <code>string</code> with a suffix, such as
+ <emphasis>KB</emphasis> or <emphasis>MB</emphasis>. Returns an <code>array</code> of the
+ formatted number and the suffix if <code>options.separate</code> is set to <code>true</code>.</para>
+
+ <para>If specifying 1000 or 1024 is specified as a <code>factor</code> then an appropriate suffix
+ will be chosen. By default the <code>factor</code> is 1000. You can pass a string suffix as a
+ <code>factor</code> in which case the resulting number will be formatted with the same suffix.</para>
+
+ <para>If the <code>number</code> is less than the <code>factor</code> or an unknown factor
+ was passed in, then the formatted number is returned without a suffix. If <code>options.separate</code>
+ is true, returns an array of <code>[formatted_number, suffix]</code> or
+ <code>[formatted_number]</code> if returned without a suffix.</para>
+
+ <para>By default, non-integer numbers will be formatted with 3 digits of precision. This can be changed
+ with <code>options.precision</code>.</para>
+
+ <para>If <code>number</code> is <code>null</code> or <code>undefined</code> an empty string or
+ an array without a suffix will be returned.</para>
+ </refsection>
+
+ <refsection id="cockpit-format-bytes-per-sec">
+ <title>cockpit.format_bytes_per_sec()</title>
+<programlisting>
+ string = cockpit.format_bytes_per_sec(number, [factor])
+ array = cockpit.format_bytes_per_sec(number, [factor, options])
+</programlisting>
+ <para>Format <code>number</code> of bytes into a displayable speed <code>string</code>.</para>
+
+ <para>If specifying 1000 or 1024 is specified as a <code>factor</code> then an appropriate suffix
+ will be chosen. By default the <code>factor</code> is 1000. You can pass a string suffix as a
+ <code>factor</code> in which case the resulting number will be formatted with the same suffix.</para>
+
+ <para>If the <code>number</code> is less than the <code>factor</code> or an unknown factor
+ was passed in, then the formatted number is returned without a suffix. If <code>options.separate</code>
+ is true, returns an array of <code>[formatted_number, suffix]</code> or
+ <code>[formatted_number]</code> if returned without a suffix.</para>
+
+ <para>By default, non-integer numbers will be formatted with 3 digits of precision. This can be changed
+ with <code>options.precision</code>.</para>
+
+ <para>If <code>number</code> is <code>null</code> or <code>undefined</code> an empty string or array
+ will be returned.</para>
+ </refsection>
+
+ <refsection id="cockpit-format-bits-per-sec">
+ <title>cockpit.format_bits_per_sec()</title>
+<programlisting>
+ string = cockpit.format_bits_per_sec(number, [factor])
+ array = cockpit.format_bytes_per_sec(number, [factor, options])
+</programlisting>
+ <para>Format <code>number</code> of bits into a displayable speed <code>string</code>.</para>
+
+ <para>If specifying 1000 or 1024 is specified as a <code>factor</code> then an appropriate suffix
+ will be chosen. By default the <code>factor</code> is 1000. You can pass a string suffix as a
+ <code>factor</code> in which case the resulting number will be formatted with the same suffix.</para>
+
+ <para>If the <code>number</code> is less than the <code>factor</code> or an unknown factor
+ was passed in, then the formatted number is returned without a suffix. If <code>options.separate</code>
+ is true, returns an array of <code>[formatted_number, suffix]</code> or
+ <code>[formatted_number]</code> if returned without a suffix.</para>
+
+ <para>By default, non-integer numbers will be formatted with 3 digits of precision. This can be changed
+ with <code>options.precision</code>.</para>
+
+ <para>If <code>number</code> is <code>null</code> or <code>undefined</code> an empty string or array
+ will be returned.</para>
+ </refsection>
+
+ <refsection id="cockpit-info">
+ <title>cockpit.info</title>
+<programlisting>
+cockpit.info["version"]
+cockpit.info["build"]
+</programlisting>
+ <para>This object contains information about cockpit itself. Note that when cockpit is
+ running on multiple servers, this only reflects the server that was connected to.
+ The following fields are defined:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>"build"</code></term>
+ <listitem><para>A string containing build details.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><code>"version"</code></term>
+ <listitem><para>A string containing the cockpit version number. It is almost always
+ incorrect to use this to make a decision in code.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ </refsection>
+
+ <refsection id="cockpit-event-target">
+ <title>cockpit.event_target</title>
+<programlisting>
+cockpit.event_target(object, [handlers])
+</programlisting>
+ <para>Adds an
+ <ulink url="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget">EventTarget</ulink>
+ implementation to the <code>object</code>. Optionally store the handlers in <code>handlers</code>
+ if its specified.</para>
+ </refsection>
+
+</refentry>
diff --git a/doc/guide/embedding.xml b/doc/guide/embedding.xml
new file mode 100644
index 0000000..7fe8aa9
--- /dev/null
+++ b/doc/guide/embedding.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="embedding">
+ <title>Embedding and Integrating Cockpit</title>
+
+ <para>Cockpit can be embedded in other web applications either as a whole or specific
+ Cockpit components can be integrated. Due to frame security policy restrictions,
+ this only works if Cockpit and the web application have the <emphasis>same origin</emphasis>;
+ this is commonly achieved by running both from a common reverse proxy.</para>
+
+ <section id="embedding-full">
+ <title>Embedding the Cockpit Interface</title>
+
+ <para>Cockpit can be embedded into a larger web page as a frame. To embed
+ the entire Cockpit Window use the URI:
+ <code>https://server.example.com:9090/</code></para>
+
+ <programlisting language="html"><![CDATA[
+<html>
+ <head>
+ <title>Embedded Cockpit</title>
+ </head>
+ <body>
+ This is Cockpit.
+ <br/>
+ <iframe width="800px" height="600px"
+ src="https://server.example.com:9090/"/>
+ </body>
+</html>
+]]></programlisting>
+
+ </section>
+
+ <section id="embedding-components">
+ <title>Integrating Cockpit Components into Web Applications</title>
+
+ <para>Instead of embedding the entirety of Cockpit, you can integrate specific components.
+ Only those components explicitly documented as API should be integrated. Other components
+ can and will change regularly.</para>
+
+ <para>The component will load from the server in question and a WebSocket connection
+ will be established with the server to relay the component's message stream.</para>
+
+ <para>Cockpit components are HTML files contained in
+ <link linkend="packages">packages</link>. These can be placed in an iframe or web browser
+ window. Each documented and stable component has a well-known URL and these are documented
+ in the <link linkend="development">API reference</link>. Each component URL begins with the string
+ <code>/cockpit/@localhost/</code> followed a package name, and then the component itself.</para>
+
+ <para>For example the
+ <link linkend="api-terminal-html">terminal.html</link> in the
+ <link linkend="api-system">system</link> package, has this URL:
+ <code>/cockpit/@localhost/system/terminal.html</code></para>
+
+ <programlisting language="html"><![CDATA[
+<html>
+ <head>
+ <title>Embedded Terminal</title>
+ </head>
+ <body>
+ This is a terminal.
+ <br/>
+ <iframe width="800px" height="600px"
+ src="https://server.example.com:9090/cockpit/@localhost/system/terminal.html"/>
+ </body>
+</html>
+]]></programlisting>
+
+ </section>
+
+ <section id="embedding-deep">
+ <title>Deep Integration</title>
+
+ <para>Most often <link linkend="embedding-components">simple integration</link> will be used
+ to bring Cockpit components into web applications. However it is also possible to do deep
+ integration for embedders who wish to perform non-standard authentication with the server,
+ and relay the component's message stream to the server themselves.</para>
+
+ <warning>
+ <para>Deep integration capability is in heavy flux and is not yet documented.</para>
+ </warning>
+ </section>
+
+ <section id="embedding-cors">
+ <title>Pinging Cockpit</title>
+
+ <para>When embedding Cockpit or integrating Cockpit components, it may be necessary to check
+ whether Cockpit is available on a server before proceeding.</para>
+
+ <para>To do this perform a <code>/ping</code> request to Cockpit. This is a simple HTTP
+ GET request. It returns the following:</para>
+
+<programlisting>
+GET: https://server.example.com:9090/ping
+200 OK: { "service": "cockpit" }
+</programlisting>
+
+ <para>The <code>/ping</code> request allows
+ <ulink url="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing">Cross Origin Resource Sharing</ulink>
+ headers and as such can be performed from Javascript code with any origin. The request can also be
+ made via plain HTTP without SSL. It is by design that no further information is present in the
+ response.</para>
+
+ <para>A complete example of using <code>/ping</code> is available in the Cockpit sources in the
+ <code>/examples/ping-server/</code> directory.</para>
+
+ </section>
+
+</chapter>
diff --git a/doc/guide/feature-firewall.xml b/doc/guide/feature-firewall.xml
new file mode 100644
index 0000000..2cd0453
--- /dev/null
+++ b/doc/guide/feature-firewall.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-firewall">
+ <title>Firewall</title>
+
+ <para>Cockpit uses <ulink url="https://www.firewalld.org">firewalld</ulink> to
+ interact with the system's firewall. No firewall configuration UI will be
+ shown if firewalld is not installed.</para>
+
+ <para>Firewalld controls access to its APIs via PolicyKit. The user logged
+ into Cockpit needs to have the appropriate permissions to view or modify
+ the settings.</para>
+
+ <para>Cockpit can currently only show, add, and remove predefined firewalld
+ services in the default zone.</para>
+
+ <para>To perform similar tasks from the command line, use
+ <ulink url="https://www.firewalld.org/documentation/man-pages/firewall-cmd.html">firewall-cmd</ulink>.
+ For example, to get the same list of allowed services that Cockpit
+ displays:</para>
+
+<programlisting>
+$ <command>sudo firewall-cmd --list-services</command>
+dhcpv6-client samba-client mdns ssh cockpit
+</programlisting>
+
+ <para>To enable an additional service, use:</para>
+<programlisting>
+$ <command>firewall-cmd --add-service pop3</command>
+success
+</programlisting>
+
+</chapter>
diff --git a/doc/guide/feature-journal.xml b/doc/guide/feature-journal.xml
new file mode 100644
index 0000000..8cc443e
--- /dev/null
+++ b/doc/guide/feature-journal.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-journal">
+ <title>Journal</title>
+ <para>The systemd journal provides Cockpit with indexed log data. This log data
+ is found on the Journal page, as well as in various other places when configuring
+ services, storage, networking etc.</para>
+
+ <para>Cockpit accesses Journal data via the
+ <ulink url="https://www.freedesktop.org/software/systemd/man/journalctl.html"><code>journalctl</code></ulink>
+ command. Similar tasks can be performed at the command line:</para>
+
+<programlisting>
+$ <command>sudo journalctl -f -u docker</command>
+...
+</programlisting>
+
+</chapter>
diff --git a/doc/guide/feature-machines.xml b/doc/guide/feature-machines.xml
new file mode 100644
index 0000000..0f733aa
--- /dev/null
+++ b/doc/guide/feature-machines.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-machines">
+ <title>Multiple Machines</title>
+
+ <para>Cockpit can connect to multiple machines from a single Cockpit session.
+ These are listed in the host switcher.</para>
+
+ <para>These additional machines are accessed via SSH from the machine that
+ the first machine connected to, and are
+ <link linkend="authentication">authenticated</link> with the logged in
+ user's password and/or SSH keys.</para>
+
+ <para>Using SSH keys is only supported when the system has the
+ necessary APIs in libssh.</para>
+
+ <para>SSH host keys are stored in
+ <filename>/etc/ssh/ssh_known_hosts</filename>.</para>
+
+ <para>The machine data is stored in
+ <filename>/etc/cockpit/machines.d/*.json</filename>, or below <code>$XDG_CONFIG_DIRS</code>
+ if set (see <ulink url="./cockpit.conf.5.html">cockpit.conf</ulink>). Settings in
+ lexicographically later files amend or override settings in earlier ones.
+ Cockpit itself writes into <filename>99-webui.json</filename>; packages or
+ admins who want to pre-configure machines should ship files like
+ <filename>05-mymachine.json</filename> so that changes from the web
+ interface override the pre-configured files.</para>
+
+ <para>Each JSON file contains an object that maps machine IDs to objects that
+ define the properties of that machine. The ID can be a human readable name
+ or an IP address or any other unique value, and is shown in the web
+ interface until conneting to it the first time, at which point the web
+ interface will show the machine's host name.</para>
+
+ <para>The following properties are recognized:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><code>"address"</code></term>
+ <listitem><para><emphasis>(string, mandatory)</emphasis> IP address or
+ DNS name of the machine</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><code>"visible"</code></term>
+ <listitem><para><emphasis>(boolean, optional)</emphasis> If
+ <code>true</code>, the machine will be displayed and
+ available for managing with Cockpit. If <code>false</code> (the
+ default), it will not be displayed, but still taken into account for
+ type-ahead search when adding new machines in the web
+ interface.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><code>"user"</code></term>
+ <listitem><para><emphasis>(string, optional)</emphasis> User name on the remote machine.
+ When not given, Cockpit will default to the user name that was being
+ used to log into Cockpit itself.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><code>"port"</code></term>
+ <listitem><para><emphasis>(integer, optional)</emphasis> ssh port of the
+ remote machine. When not given, the default port 22 is used.
+ </para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><code>"color"</code></term>
+ <listitem><para><emphasis>(string, optional)</emphasis> Color to
+ assign to the machine label in the web interface. This can be either given as
+ <code>rgb(r_value, g_value, b_value)</code> with each value being an
+ integer between 0 and 255, or as a color name like <code>yellow</code>.
+ When not given, Cockpit will assign an unused color automatically.
+ </para></listitem>
+ </varlistentry>
+
+ <!-- TODO: This cannot sensibly be used right now, as this neither accepts
+ a full (foreign) URL nor a relative path in the dist/ directory
+ <varlistentry>
+ <term><code>"avatar"</code></term>
+ <listitem><para><emphasis>(string, optional)</emphasis> Path to an image
+ file that will be shown as an icon for that machine in the web
+ interface. If not given, a generic "computer" icon is used.
+ </para></listitem>
+ </varlistentry>
+ -->
+ </variablelist>
+
+ <para>Example:</para>
+ <programlisting>
+{
+ "web server": {
+ "address": "192.168.2.4",
+ "visible": true,
+ "color": "rgb(100, 200, 0)",
+ "user": "admin"
+ },
+ "192.168.2.1": {
+ "address": "192.168.2.1",
+ "port": 2222,
+ "visible": true,
+ "color": "green"
+ }
+}</programlisting>
+</chapter>
diff --git a/doc/guide/feature-networkmanager.xml b/doc/guide/feature-networkmanager.xml
new file mode 100644
index 0000000..8a2f235
--- /dev/null
+++ b/doc/guide/feature-networkmanager.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-networkmanager">
+ <title>NetworkManager</title>
+
+ <para>If available on the system, Cockpit uses
+ <ulink url="https://wiki.gnome.org/Projects/NetworkManager">NetworkManager</ulink>
+ and the DBus APIs it provides to interact with the system's network configuration.</para>
+
+ <para>For non root users, NetworkManager controls access to its APIs via
+ <link linkend="privileges">Policy Kit</link> and a user logged into Cockpit will have
+ the same permissions as they do from the command line.</para>
+
+ <para>To perform similar tasks from the command line, use the
+ <ulink url="https://fedoraproject.org/wiki/Networking/CLI">nmcli</ulink> command:</para>
+
+<programlisting>
+$ <command>nmcli general status</command>
+STATE CONNECTIVITY WIFI-HW WIFI WWAN-HW WWAN
+connected full enabled enabled enabled enabled
+</programlisting>
+
+ <para>Devices marked as "not managed" with the <code>NM_CONTROLLED=no</code> setting
+ will not be displayed in the interface.</para>
+
+</chapter>
diff --git a/doc/guide/feature-packagekit.xml b/doc/guide/feature-packagekit.xml
new file mode 100644
index 0000000..9e73532
--- /dev/null
+++ b/doc/guide/feature-packagekit.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-packagekit">
+ <title>Package Updates</title>
+
+ <para>Cockpit uses the <ulink url="https://www.freedesktop.org/software/PackageKit/gtk-doc/api-reference.html">PackageKit</ulink>
+ D-Bus API to get information about available package updates and to apply them, in an Operating System independent manner.</para>
+
+ <para>To perform similar tasks from the command line, use the
+ <ulink url="https://www.freedesktop.org/software/PackageKit/pk-using.html">pkcon</ulink> command:</para>
+
+<programlisting>
+$ <command>pkcon refresh</command>
+
+$ <command>pkcon get-updates</command>
+Available sudo-1.8.20p2-1.fc26.x86_64 (updates-testing)
+ Allows restricted root access for specified users
+Available vim-filesystem-2:8.0.617-1.fc26.x86_64 (updates-testing)
+ VIM filesystem layout
+Available vim-minimal-2:8.0.617-1.fc26.x86_64 (updates-testing)
+ A minimal version of the VIM editor
+
+$ <command>pkcon get-update-detail sudo</command>
+Details about the update:6.x86_64 [fedora]
+ Package: sudo-1.8.20p2-1.fc26.x86_64
+ Bugzilla: https://bugzilla.redhat.com/show_bug.cgi?id=1452941
+ Update text: - update to 1.8.20p2
+ - added sudo package to dnf/yum protected packages
+
+$ <command>pkcon update</command>
+The following packages have to be updated:
+ sudo-1.8.20p2-1.fc26.x86_64 Allows restricted root access for specified users
+ vim-filesystem-2:8.0.617-1.fc26.x86_64 VIM filesystem layout
+ vim-minimal-2:8.0.617-1.fc26.x86_64 A minimal version of the VIM editor
+Proceed with changes? [N/y] y
+[...]
+</programlisting>
+
+<para>Of course you can also use your Operating System specific commands for that, such as
+ <command>dnf updateinfo info</command> on Fedora or
+ <command>sudo apt upgrade</command> on Debian.</para>
+
+</chapter>
diff --git a/doc/guide/feature-pcp.xml b/doc/guide/feature-pcp.xml
new file mode 100644
index 0000000..559709d
--- /dev/null
+++ b/doc/guide/feature-pcp.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-pcp">
+ <title>PCP Metrics</title>
+
+ <para>If available, Cockpit uses the
+ <ulink url="https://pcp.io/">Performance Co-Pilot</ulink> framework to
+ gather metrics data about the system. This data is used to display the history
+ graphs on the "Metrics and history" page.
+ Cockpit can use the PCP logging feature to display archived data about the
+ system from a different point in time. If PCP is not available, then Cockpit
+ gathers the metrics data itself, but archival features are not available.</para>
+
+ <para>Whether or not metrics are archived depends on whether the
+ <ulink url="https://linux.die.net/man/1/pmlogger"><code>pmlogger.service</code></ulink>
+ systemd unit is running or not. The "Enable PCP metrics collector" button on the
+ Metrics page will enable and start this service.</para>
+
+ <para>To see similar metrics data from the command line, you can use tools like
+ <ulink url="https://linux.die.net/man/1/pmstat"><code>pmstat</code></ulink>
+ or <ulink url="https://linux.die.net/man/1/pminfo"><code>pminfo</code></ulink>:</para>
+
+<programlisting>
+$ <command>pmstat</command>
+@ Sat Sep 26 15:30:10 2015
+ loadavg memory swap io system cpu
+ 1 min swpd free buff cache pi po bi bo in cs us sy id
+ 4.19 0 20710m 605148 6450m 0 0 0 2548 5688 14K 19 3 76
+...
+</programlisting>
+
+ <para>These metrics can also be exposed to other machines on a TCP port with
+ <ulink url="https://linux.die.net/man/1/pmproxy">pmproxy</ulink>
+ and <ulink url="https://redis.io/">Redis</ulink>:</para>
+
+<programlisting>
+systemctl enable --now redis pmproxy
+# if you use firewalld, open port 44322:
+firewall-cmd --permanent --add-service pmproxy
+firewall-cmd --reload
+</programlisting>
+
+<para>This allows you to gather and visualize PCP metrics from multiple machines with
+ <ulink url="https://grafana.com/">Grafana</ulink> and the
+ <ulink url="https://grafana-pcp.readthedocs.io">PCP Grafana plugin</ulink>.</para>
+
+</chapter>
diff --git a/doc/guide/feature-realmd.xml b/doc/guide/feature-realmd.xml
new file mode 100644
index 0000000..03c23f2
--- /dev/null
+++ b/doc/guide/feature-realmd.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-realmd">
+ <title>realmd</title>
+
+ <para>If available on the system, Cockpit uses
+ <ulink url="https://www.freedesktop.org/software/realmd/">realmd</ulink>
+ and the DBus APIs it provides to configure the system's Active Directory
+ or IPA domain membership.</para>
+
+ <para>Not all systems can join all kinds of domains. This depends
+ on the availability of the necessary client software.</para>
+
+ <para>For non root users, realmd controls access to its APIs via
+ <link linkend="privileges">Policy Kit</link> and a user logged into Cockpit will have
+ the same permissions as they do from the command line.</para>
+
+ <para>To perform similar tasks from the command line, use the
+ <ulink url="https://www.freedesktop.org/software/realmd/docs/realm.html">realm</ulink> command:</para>
+
+<programlisting>
+$ <command>realm join example.com</command>
+Password for Administrator:
+</programlisting>
+
+ <para>
+ <ulink url="http://www.freedesktop.org/software/realmd/">realmd</ulink>
+ sets up domain-qualified user names by default, i. e. login user names look like
+ "<code>user@example.com</code>". For using unqualified names (just
+ "<code>user</code>"), set the <code>fully-qualified-names</code> option in
+ <ulink url="https://www.freedesktop.org/software/realmd/docs/realmd-conf.html">/etc/realmd.conf</ulink>
+ before joining a domain.</para>
+
+ <para>Cockpit requests an SSL certificate from the IPA server for
+ <command>cockpit-ws</command> with the
+ <ulink url="https://www.freeipa.org/page/Certmonger">ipa-getcert</ulink> command.</para>
+
+</chapter>
diff --git a/doc/guide/feature-selinux.xml b/doc/guide/feature-selinux.xml
new file mode 100644
index 0000000..c421324
--- /dev/null
+++ b/doc/guide/feature-selinux.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-selinux">
+ <title>SELinux Policy</title>
+
+ <para>If present on the system Cockpit can set the SELinux mode to enforcing or permissive.
+ It can also use <code>setroubleshootd</code> to show audit issues and apply suggested
+ fixes.</para>
+
+ <para>To perform similar tasks from the command line use the <code>setenforce</code> and
+ <code>sealert</code> tools.</para>
+
+ <para>To clear out all the information that <code>setroubleshootd</code> tracks, you
+ can use a commands like:</para>
+
+<programlisting>
+$ <command>sudo killall setroubleshootd</command>
+$ <command>sudo rm -rf /var/lib/setroubleshoot/*</command>
+</programlisting>
+
+</chapter>
diff --git a/doc/guide/feature-sosreport.xml b/doc/guide/feature-sosreport.xml
new file mode 100644
index 0000000..eb7c308
--- /dev/null
+++ b/doc/guide/feature-sosreport.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-sosreport">
+ <title>SOS Report</title>
+
+ <para>If present on the system Cockpit can use <code>sosreport</code> to collect system configuration and diagnostic information.</para>
+ <para>To perform similar tasks from the command line, use the <code>sosreport</code> command.</para>
+
+</chapter>
diff --git a/doc/guide/feature-storaged.xml b/doc/guide/feature-storaged.xml
new file mode 100644
index 0000000..f3621cb
--- /dev/null
+++ b/doc/guide/feature-storaged.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-storaged">
+ <title>storaged</title>
+
+ <para>If available on the system, Cockpit uses <ulink
+ url="https://github.com/storaged-project/storaged/"><code>storaged</code></ulink>
+ to configure and monitor storage, disks, mounts etc. on the
+ system. This functionality is present in the Cockpit
+ <emphasis>storaged</emphasis> package.</para>
+
+ <para>The <code>storaged</code> project is originally based on a project called
+ <ulink url="https://www.freedesktop.org/wiki/Software/udisks/"><code>udisks</code></ulink>
+ and added support for many more features such as
+ <ulink url="https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux)">LVM</ulink>,
+ <ulink url="https://en.wikipedia.org/wiki/ISCSI">iSCSI</ulink>,
+ <ulink url="https://en.wikipedia.org/wiki/Linux_DM_Multipath">Multipath</ulink>, and
+ <ulink url="https://btrfs.wiki.kernel.org/index.php/Main_Page">BTRFS</ulink>.
+ The same tools and backwards compatible API are available between <code>storaged</code>
+ and <code>udisks</code> the projects. Cockpit can use <code>udisks</code> but disables
+ many of it's storage related features, including updating <code>/etc/fstab</code>
+ and <code>/etc/crypttab</code> for stability reasons.</para>
+
+ <para>For non root users, storaged controls access to its APIs via
+ <link linkend="privileges">Policy Kit</link> and a user logged into Cockpit will have
+ the same permissions as they do from the command line.</para>
+
+ <para>To perform similar tasks from the command line, use the <code>storagedctl</code> command:</para>
+
+<programlisting>
+$ <command>udisksctl dump</command>
+...
+</programlisting>
+
+ <para>To perform LVM tasks, you may use the various LVM commands, such as
+ <code>vgcreate</code>, <code>lvresize</code> and so on. Cockpit will detect such changes
+ made at the command line.</para>
+
+ <para>Cockpit recognizes devices with multiple paths and can start
+ the <code>multipathd</code> service in case it is not running. On
+ the command line, you can control multipath features with the
+ <code>mpathconf</code>, <code>multipathd</code>, and
+ <code>multipath</code> commands.</para>
+
+ <para>To manage iSCSI initiators from the command line, you can use
+ <code>iscsiadm</code> and related tools.</para>
+
+</chapter>
diff --git a/doc/guide/feature-systemd.xml b/doc/guide/feature-systemd.xml
new file mode 100644
index 0000000..4a4bbb1
--- /dev/null
+++ b/doc/guide/feature-systemd.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-systemd">
+ <title>systemd</title>
+
+ <para>Cockpit uses
+ <ulink url="https://www.freedesktop.org/wiki/Software/systemd/">systemd</ulink>
+ and the DBus APIs it provides to configure and monitor core aspects of the system.
+ Use of alternate system APIs are not currently implemented.</para>
+
+ <para>For non root users, systemd controls access to its APIs via
+ <link linkend="privileges">Policy Kit</link> and a user logged into Cockpit will have
+ the same permissions as they do from the command line.</para>
+
+ <para>Cockpit retrieves information about the host and changes the hostname via the
+ <code>hostnamed</code> daemon. To perform similar tasks from the command line use the
+ <ulink url="https://www.freedesktop.org/software/systemd/man/hostnamectl.html"><code>hostnamectl</code></ulink>
+ command:</para>
+
+<programlisting>
+$ <command>hostnamectl</command>
+ Static hostname: pink.example.com
+ Pretty hostname: Pink
+ Icon name: computer-desktop
+ Chassis: desktop
+ Machine ID: ef00b79be229463cbb844c3e715de96c
+ Boot ID: 934983d64d34465cb5a8383b5a89ad8c
+ Operating System: Fedora 22 (Twenty Two)
+ CPE OS Name: cpe:/o:fedoraproject:fedora:22
+ Kernel: Linux 4.0.4-301.fc22.x86_64
+ Architecture: x86-64
+</programlisting>
+
+ <para>Cockpit configures the system time and time zone via the <code>timedated</code> daemon.
+ To perform similar tasks from the command line use the
+ <ulink url="https://www.freedesktop.org/software/systemd/man/timedatectl.html"><code>timedatectl</code></ulink>
+ command:</para>
+
+<programlisting>
+$ <command>timedatectl list-timezones</command>
+Africa/Abidjan
+Africa/Accra
+Africa/Addis_Ababa
+Africa/Algiers
+...
+</programlisting>
+
+ <para>Cockpit can manage the list of NTP servers used by
+ <code>systemd-timesyncd</code> by putting its own file into
+ <code>/etc/systemd/timesyncd.conf.d/</code>. Note that
+ <code>systemd-timesyncd</code> is not always enabled, depending on
+ the configuration of the machine. In that case, Cockpit disabled the
+ UI for managing the list of NTP servers. In some cases use of
+ <code>ntpd</code> can cause the <code>timedated</code> daemon to
+ behave inconsistently with regards to time synchronization.</para>
+
+ <para>Cockpit reboots or powers down the machine by using the
+ <ulink url="https://www.freedesktop.org/software/systemd/man/shutdown.html"><code>shutdown</code></ulink>
+ command. To perform similar tasks from the command line, run it directly:</para>
+
+<programlisting>
+$ <command>sudo shutdown +15</command>
+Shutdown scheduled for Sa 2015-09-26 15:49:40 CEST, use 'shutdown -c' to cancel.
+</programlisting>
+
+ <para>Cockpit manages system services and sockets via systemd. To perform similar tasks from the
+ command line use the
+ <ulink url="https://www.freedesktop.org/software/systemd/man/systemctl.html"><code>systemctl</code></ulink>
+ command:</para>
+
+<programlisting>
+$ <command>systemctl status cockpit</command>
+● cockpit.service - Cockpit Web Service
+ Loaded: loaded (/usr/lib/systemd/system/cockpit.service; static; vendor preset: disabled)
+ Drop-In: /etc/systemd/system/cockpit.service.d
+ └─debug.conf
+ Active: active (running) since Sa 2015-09-26 13:28:02 CEST; 2h 7min ago
+ Docs: man:cockpit-ws(8)
+ Main PID: 6957 (cockpit-ws)
+ Memory: 1.8M
+ CGroup: /system.slice/cockpit.service
+ ├─ 6957 /usr/libexec/cockpit-ws
+ └─29598 /usr/bin/ssh-agent
+</programlisting>
+
+ <para>In order to customize who can perform various actions in system,
+ <link linkend="privileges-polkit">create polkit rules</link> with the following
+ actions and details:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term><option>org.freedesktop.systemd1.manage-units</option></term>
+ <listitem><para>Permission to manage system services or other units.
+ Details available: <code>unit</code>, <code>verb</code></para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>org.freedesktop.systemd1.manage-unit-files</option></term>
+ <listitem><para>Permission to manage system services or other unit files.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>org.freedesktop.systemd1.reload-daemon</option></term>
+ <listitem><para>Permission to reload the systemd state.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>For example, placing the following polkit rule to
+ <filename>/etc/polkit-1.rules.d/10-http.rule</filename> allows all users in the
+ <code>operators</code> group start, stop, and restart the Apache HTTP service:</para>
+
+<programlisting>
+polkit.addRule(function(action, subject) {
+ if (action.id == "org.freedesktop.systemd1.manage-units") {
+ if (subject.isInGroup("operators") &amp;&amp; action.lookup("unit") == "httpd.service") {
+ var verb = action.lookup("verb");
+ if (verb == "start" || verb == "stop" || verb == "restart") {
+ return polkit.Result.YES;
+ }
+ }
+ }
+});
+</programlisting>
+
+</chapter>
diff --git a/doc/guide/feature-terminal.xml b/doc/guide/feature-terminal.xml
new file mode 100644
index 0000000..0caaeaa
--- /dev/null
+++ b/doc/guide/feature-terminal.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-terminal">
+ <title>Terminal</title>
+
+ <para>Cockpit provides a standard shell in a terminal. This shell and the
+ processes running in it have the <link linkend="privileges">same privileges</link>
+ as if the user had logged in via SSH.</para>
+
+</chapter>
diff --git a/doc/guide/feature-tuned.xml b/doc/guide/feature-tuned.xml
new file mode 100644
index 0000000..9b0ab59
--- /dev/null
+++ b/doc/guide/feature-tuned.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-tuned">
+ <title>Tuned Profiles</title>
+
+ <para>If present on the system Cockpit can use Tuned and the DBUS API it provides to set system performance profiles.
+ To perform similar tasks from the command line, use the <code>tuned-adm</code> command.</para>
+
+</chapter>
diff --git a/doc/guide/feature-users.xml b/doc/guide/feature-users.xml
new file mode 100644
index 0000000..cccfdb3
--- /dev/null
+++ b/doc/guide/feature-users.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="feature-users">
+ <title>User Tools</title>
+
+ <para>Cockpit uses the usual tools to create and modify local user accounts.
+ Examples are <code>useradd</code>, <code>usermod</code> and <code>passwd</code>.
+ These same tools are available for use on the command line.</para>
+
+</chapter>
diff --git a/doc/guide/gtk-doc.xsl b/doc/guide/gtk-doc.xsl
new file mode 100644
index 0000000..762c8b7
--- /dev/null
+++ b/doc/guide/gtk-doc.xsl
@@ -0,0 +1,901 @@
+<?xml version='1.0'?> <!--*- mode: xml -*-->
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:l="http://docbook.sourceforge.net/xmlns/l10n/1.0"
+ exclude-result-prefixes="l"
+ version="1.0">
+
+ <!-- import the chunked XSL stylesheet -->
+ <!-- http://www.sagehill.net/docbookxsl/Chunking.html says we should use
+ "chunkfast.xsl", but I can see a difference -->
+ <xsl:import href="http://docbook.sourceforge.net/release/xsl/current/html/chunk.xsl"/>
+ <xsl:include href="version-greater-or-equal.xsl"/>
+
+ <xsl:key name="acronym.key"
+ match="glossentry/glossterm"
+ use="."/>
+ <xsl:key name="gallery.key"
+ match="para[@role='gallery']/link"
+ use="@linkend"/>
+
+ <!-- change some parameters -->
+ <!-- http://docbook.sourceforge.net/release/xsl/current/doc/html/index.html -->
+ <xsl:param name="toc.section.depth">4</xsl:param>
+ <xsl:param name="generate.section.toc.level" select="3"></xsl:param>
+ <xsl:param name="generate.toc">
+ book toc
+ chapter toc
+ glossary toc
+ index toc
+ part toc
+ reference toc,title,refsection
+ refentry toc
+ </xsl:param>
+
+ <xsl:param name="chunker.output.encoding" select="'UTF-8'"/>
+ <xsl:param name="chunker.output.indent" select="'yes'"/>
+ <xsl:param name="chunker.output.doctype-public" select="'-//W3C//DTD HTML 4.01 Transitional//EN'"/>
+ <xsl:param name="chunk.fast" select="1"/>
+ <xsl:param name="chunk.quietly" select="1"/>
+ <xsl:param name="chunk.section.depth" select="0"/>
+ <xsl:param name="chunk.first.sections" select="1"/>
+
+ <xsl:param name="default.encoding" select="'UTF-8'"/>
+ <xsl:param name="chapter.autolabel" select="0"/>
+ <xsl:param name="reference.autolabel" select="0"/>
+ <xsl:param name="use.id.as.filename" select="1"/>
+ <xsl:param name="html.ext" select="'.html'"/>
+ <xsl:param name="refentry.generate.name" select="0"/>
+ <xsl:param name="refentry.generate.title" select="1"/>
+ <!-- don't generate all those link tags (very slow and hardly used)
+ it does not show much effect as we have a user.head.content template
+ <xsl:param name="html.extra.head.links" select="0" />
+ -->
+
+ <!-- use index filtering (if available) -->
+ <xsl:param name="index.on.role" select="1"/>
+
+ <!-- display variablelists as tables -->
+ <xsl:param name="variablelist.as.table" select="1"/>
+
+ <!-- new things to consider
+ <xsl:param name="glossterm.auto.link" select="0"></xsl:param>
+ -->
+
+ <!-- this gets set on the command line ... -->
+ <xsl:param name="gtkdoc.version" select="''"/>
+ <xsl:param name="gtkdoc.bookname" select="''"/>
+
+ <!-- ========================================================= -->
+
+ <!-- l10n is slow, we don't ue it, so we'd like to turn it off
+ this at least avoid the re-evaluation -->
+ <xsl:template name="l10n.language">en</xsl:template>
+
+ <xsl:param name="gtkdoc.l10n.xml" select="document('http://docbook.sourceforge.net/release/xsl/current/common/en.xml')"/>
+
+ <xsl:key name="gtkdoc.gentext.key"
+ match="l:gentext[@key]"
+ use="@key"/>
+ <xsl:key name="gtkdoc.context.key"
+ match="l:context[@name]"
+ use="@name"/>
+
+ <xsl:template name="gentext">
+ <xsl:param name="key" select="local-name(.)"/>
+
+ <xsl:for-each select="$gtkdoc.l10n.xml">
+ <xsl:variable name="l10n.gentext" select="key('gtkdoc.gentext.key', $key)"/>
+
+ <xsl:choose>
+ <xsl:when test="$l10n.gentext">
+ <xsl:value-of select="$l10n.gentext/@text"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:message>
+ <xsl:text>No "en" localization of "</xsl:text>
+ <xsl:value-of select="$key"/>
+ <xsl:text>" exists.</xsl:text>
+ </xsl:message>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:for-each>
+ </xsl:template>
+
+ <xsl:template name="gentext.dingbat">
+ <xsl:param name="dingbat">bullet</xsl:param>
+
+ <xsl:variable name="l10n.dingbat"
+ select="($gtkdoc.l10n.xml/l:l10n/l:dingbat[@key=$dingbat])[1]"/>
+
+ <xsl:choose>
+ <xsl:when test="$l10n.dingbat">
+ <xsl:value-of select="$l10n.dingbat/@text"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:message>
+ <xsl:text>No "en" localization of dingbat </xsl:text>
+ <xsl:value-of select="$dingbat"/>
+ <xsl:text> exists; using "en".</xsl:text>
+ </xsl:message>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <xsl:template name="gentext.template">
+ <xsl:param name="context" select="'default'"/>
+ <xsl:param name="name" select="'default'"/>
+ <xsl:param name="origname" select="$name"/>
+
+ <!-- cut leading / if any to avoid one recursion -->
+ <xsl:variable name="rname">
+ <xsl:choose>
+ <xsl:when test="starts-with($name, '/')">
+ <xsl:value-of select="substring-after($name, '/')"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="$name"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+
+ <!-- this is called with context="title|title-numbered|title-unnumbered>
+ <xsl:message>
+ <xsl:text>context:</xsl:text><xsl:value-of select="$context"/>
+ <xsl:text>;name:</xsl:text><xsl:value-of select="$rname"/>
+ <xsl:text>;origname:</xsl:text><xsl:value-of select="$origname"/>
+ </xsl:message>
+
+ see html/html.xsl:<xsl:template match="*" mode="html.title.attribute">
+ -->
+
+ <xsl:for-each select="$gtkdoc.l10n.xml">
+ <xsl:variable name="context.node" select="key('gtkdoc.context.key', $context)"/>
+ <xsl:variable name="template.node"
+ select="($context.node/l:template[@name=$rname])[1]"/>
+
+ <xsl:choose>
+ <xsl:when test="$template.node/@text">
+ <xsl:value-of select="$template.node/@text"/>
+ <!-- debug
+ <xsl:message>
+ <xsl:text>=</xsl:text><xsl:value-of select="$template.node/@text"/>
+ </xsl:message>
+ -->
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:choose>
+ <xsl:when test="contains($rname, '/')">
+ <xsl:call-template name="gentext.template">
+ <xsl:with-param name="context" select="$context"/>
+ <xsl:with-param name="name" select="substring-after($rname, '/')"/>
+ <xsl:with-param name="origname" select="$origname"/>
+ </xsl:call-template>
+ </xsl:when>
+ </xsl:choose>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:for-each>
+ </xsl:template>
+
+ <!-- silently test whether a gentext template exists -->
+ <xsl:template name="gentext.template.exists">
+ <xsl:param name="context" select="'default'"/>
+ <xsl:param name="name" select="'default'"/>
+ <xsl:param name="origname" select="$name"/>
+
+ <xsl:variable name="template">
+ <xsl:call-template name="gentext.template">
+ <xsl:with-param name="context" select="$context"/>
+ <xsl:with-param name="name" select="$name"/>
+ <xsl:with-param name="origname" select="$origname"/>
+ </xsl:call-template>
+ </xsl:variable>
+
+ <xsl:choose>
+ <xsl:when test="string-length($template) != 0">1</xsl:when>
+ <xsl:otherwise>0</xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- shortcut version -->
+ <!-- @bug: https://bugzilla.gnome.org/show_bug.cgi?id=617478 -->
+ <xsl:template name="generate.html.title"/>
+ <!--xsl:template name="generate.html.title">
+ <xsl:variable name="has.title.markup">
+ <xsl:apply-templates select="." mode="title.markup">
+ <xsl:with-param name="verbose" select="0"/>
+ </xsl:apply-templates>
+ </xsl:variable>
+ <xsl:if test="$has.title.markup != '???TITLE???'">
+ <xsl:variable name="gentext.title">
+ <xsl:apply-templates select="." mode="object.title.markup.textonly"/>
+ </xsl:variable>
+ <xsl:choose>
+ <xsl:when test="string-length($gentext.title) != 0">
+ <xsl:attribute name="title">
+ <xsl:value-of select="$gentext.title"/>
+ </xsl:attribute>
+ </xsl:when>
+ <xsl:when test="alt">
+ <xsl:attribute name="title">
+ <xsl:value-of select="normalize-space(alt)"/>
+ </xsl:attribute>
+ </xsl:when>
+ </xsl:choose>
+ </xsl:if>
+ </xsl:template-->
+
+ <!-- Generate a title attribute for the context node (e.g. links) -->
+ <xsl:template match="*" mode="html.title.attribute">
+ <xsl:variable name="has.title.markup">
+ <xsl:apply-templates select="." mode="title.markup">
+ <xsl:with-param name="verbose" select="0"/>
+ </xsl:apply-templates>
+ </xsl:variable>
+ <xsl:if test="$has.title.markup != '???TITLE???'">
+ <xsl:variable name="is.title">
+ <xsl:call-template name="gentext.template.exists">
+ <xsl:with-param name="context" select="'title'"/>
+ <xsl:with-param name="name" select="local-name(.)"/>
+ <xsl:with-param name="lang">
+ <xsl:call-template name="l10n.language"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </xsl:variable>
+
+ <xsl:variable name="is.title-numbered">
+ <xsl:if test="$is.title = 0">
+ <xsl:call-template name="gentext.template.exists">
+ <xsl:with-param name="context" select="'title-numbered'"/>
+ <xsl:with-param name="name" select="local-name(.)"/>
+ <xsl:with-param name="lang">
+ <xsl:call-template name="l10n.language"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </xsl:if>
+ </xsl:variable>
+
+
+ <xsl:variable name="is.title-unnumbered">
+ <xsl:if test="$is.title = 0 and $is.title-numbered = 0">
+ <xsl:call-template name="gentext.template.exists">
+ <xsl:with-param name="context" select="'title-unnumbered'"/>
+ <xsl:with-param name="name" select="local-name(.)"/>
+ <xsl:with-param name="lang">
+ <xsl:call-template name="l10n.language"/>
+ </xsl:with-param>
+ </xsl:call-template>
+ </xsl:if>
+ </xsl:variable>
+
+ <xsl:variable name="gentext.title">
+ <xsl:if test="$is.title != 0 or
+ $is.title-numbered != 0 or
+ $is.title-unnumbered != 0">
+ <xsl:apply-templates select="."
+ mode="object.title.markup.textonly"/>
+ </xsl:if>
+ </xsl:variable>
+
+ <xsl:choose>
+ <xsl:when test="string-length($gentext.title) != 0">
+ <xsl:attribute name="title">
+ <xsl:value-of select="$gentext.title"/>
+ </xsl:attribute>
+ </xsl:when>
+ <xsl:when test="alt">
+ <xsl:attribute name="title">
+ <xsl:value-of select="normalize-space(alt)"/>
+ </xsl:attribute>
+ </xsl:when>
+ </xsl:choose>
+ </xsl:if>
+ </xsl:template>
+
+
+
+
+ <!-- ========================================================= -->
+ <!-- template to create the index.sgml anchor index -->
+
+ <xsl:template match="book|article">
+ <xsl:variable name="tooldver">
+ <xsl:call-template name="version-greater-or-equal">
+ <xsl:with-param name="ver1" select="$VERSION" />
+ <xsl:with-param name="ver2">1.36</xsl:with-param>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:if test="$tooldver = 0">
+ <xsl:message terminate="yes">
+FATAL-ERROR: You need the DocBook XSL Stylesheets version 1.36 or higher
+to build the documentation.
+Get a newer version at http://docbook.sourceforge.net/projects/xsl/
+ </xsl:message>
+ </xsl:if>
+ <xsl:apply-imports/>
+
+ <!-- generate the index.sgml href index -->
+ <xsl:call-template name="generate.index"/>
+ </xsl:template>
+
+ <xsl:template name="generate.index">
+ <xsl:call-template name="write.text.chunk">
+ <xsl:with-param name="filename" select="'index.sgml'"/>
+ <xsl:with-param name="content">
+ <xsl:apply-templates select="/book/bookinfo/releaseinfo/ulink"
+ mode="generate.index.mode"/>
+ <!-- check all anchor and refentry elements -->
+ <!--
+ The obvious way to write this is //anchor|//refentry|etc...
+ The obvious way is slow because it causes multiple traversals
+ in libxslt. This take about half the time.
+ -->
+ <xsl:apply-templates select="//*[name()='anchor' or name()='refentry' or name()='refsect1' or
+ name() = 'refsect2' or name()='refsynopsisdiv' or
+ name()='varlistentry']"
+ mode="generate.index.mode"/>
+ </xsl:with-param>
+ <xsl:with-param name="default.encoding" select="'UTF-8'"/>
+ <xsl:with-param name="chunker.output.indent" select="'no'"/>
+ </xsl:call-template>
+ </xsl:template>
+
+ <xsl:template match="*" mode="generate.index.mode">
+ <xsl:if test="not(@href) and count(@id) > 0">
+ <xsl:text>&lt;ANCHOR id=&quot;</xsl:text>
+ <xsl:value-of select="@id"/>
+ <xsl:text>&quot; href=&quot;</xsl:text>
+ <xsl:if test="$gtkdoc.bookname">
+ <xsl:value-of select="$gtkdoc.bookname"/>
+ <xsl:text>/</xsl:text>
+ </xsl:if>
+ <xsl:call-template name="href.target"/>
+ <xsl:text>&quot;&gt;&#10;</xsl:text>
+ </xsl:if>
+ </xsl:template>
+
+ <xsl:template match="/book/bookinfo/releaseinfo/ulink" mode="generate.index.mode">
+ <xsl:if test="@role='online-location'">
+ <xsl:text>&lt;ONLINE href=&quot;</xsl:text>
+ <xsl:value-of select="@url"/>
+ <xsl:text>&quot;&gt;&#10;</xsl:text>
+ </xsl:if>
+ </xsl:template>
+
+ <!-- ========================================================= -->
+ <!-- template to output gtkdoclink elements for the unknown targets -->
+
+ <xsl:template match="link">
+ <xsl:choose>
+ <xsl:when test="id(@linkend)">
+ <xsl:apply-imports/>
+ </xsl:when>
+ <xsl:otherwise>
+ <GTKDOCLINK HREF="{@linkend}">
+ <xsl:apply-templates/>
+ </GTKDOCLINK>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+ <!-- ========================================================= -->
+ <!-- Below are the visual portions of the stylesheet. They provide
+ the normal gtk-doc output style. -->
+
+ <xsl:param name="shade.verbatim" select="0"/>
+ <xsl:param name="refentry.separator" select="0"/>
+
+ <xsl:template match="refsect2">
+ <xsl:if test="preceding-sibling::refsect2">
+ <hr/>
+ </xsl:if>
+ <xsl:apply-imports/>
+ </xsl:template>
+
+ <xsl:template name="user.head.content">
+ <xsl:if test="$gtkdoc.version">
+ <meta name="generator" content="GTK-Doc V{$gtkdoc.version} (XML mode)"/>
+ </xsl:if>
+ <link rel="stylesheet" href="style.css" type="text/css"/>
+ </xsl:template>
+
+ <xsl:template name="user.footer.content">
+ <div class="footer">
+ <hr />
+ <xsl:choose>
+ <xsl:when test="$gtkdoc.version">
+ </xsl:when>
+ <xsl:otherwise>
+ </xsl:otherwise>
+ </xsl:choose>
+ </div>
+ </xsl:template>
+
+ <xsl:template match="title" mode="book.titlepage.recto.mode">
+ <table class="navigation" id="top" width="100%"
+ cellpadding="2" cellspacing="0">
+ <tr>
+ <th valign="middle">
+ <p class="{name(.)}">
+ <xsl:value-of select="."/>
+ </p>
+ </th>
+ </tr>
+ </table>
+ </xsl:template>
+
+ <xsl:template name="header.navigation">
+ <xsl:param name="prev" select="/foo"/>
+ <xsl:param name="next" select="/foo"/>
+ <xsl:variable name="home" select="/*[1]"/>
+ <xsl:variable name="up" select="parent::*"/>
+ <xsl:variable name="refsections" select="./refsect1[@role]"/>
+ <xsl:variable name="glssections" select="./glossdiv/title"/>
+ <xsl:variable name="idxsections" select="./indexdiv/indexdiv/title"/>
+ <xsl:variable name="section_id" select="./@id"/>
+ <xsl:variable name="sect_object_hierarchy" select="./refsect1[@role='object_hierarchy']"/>
+ <xsl:variable name="sect_impl_interfaces" select="./refsect1[@role='impl_interfaces']"/>
+ <xsl:variable name="sect_prerequisites" select="./refsect1[@role='prerequisites']"/>
+ <xsl:variable name="sect_derived_interfaces" select="./refsect1[@role='derived_interfaces']"/>
+ <xsl:variable name="sect_implementations" select="./refsect1[@role='implementations']"/>
+ <xsl:variable name="sect_properties" select="./refsect1[@role='properties']"/>
+ <xsl:variable name="sect_child_properties" select="./refsect1[@role='child_properties']"/>
+ <xsl:variable name="sect_style_properties" select="./refsect1[@role='style_properties']"/>
+ <xsl:variable name="sect_signal_proto" select="./refsect1[@role='signal_proto']"/>
+ <xsl:variable name="sect_desc" select="./refsect1[@role='desc']"/>
+ <xsl:variable name="sect_synopsis" select="./refsynopsisdiv[@role='synopsis']"/>
+ <!--
+ <xsl:variable name="sect_details" select="./refsect1[@id='details']"/>
+ <xsl:variable name="sect_property_details" select="./refsect1[@id='property_details']"/>
+ <xsl:variable name="sect_child_property_details" select="./refsect1[@id='child_property_details']"/>
+ <xsl:variable name="sect_style_property_details" select="./refsect1[@id='style_property_details']"/>
+ <xsl:variable name="sect_signals" select="./refsect1[@id='signals']"/>
+ -->
+
+ <xsl:if test="$suppress.navigation = '0' and $home != .">
+ <table class="navigation" id="top" width="100%"
+ summary = "Navigation header" cellpadding="2" cellspacing="2">
+ <tr valign="middle">
+ <xsl:choose>
+ <xsl:when test="count($prev) > 0">
+ <td>
+ <a accesskey="p">
+ <xsl:attribute name="href">
+ <xsl:call-template name="href.target">
+ <xsl:with-param name="object" select="$prev"/>
+ </xsl:call-template>
+ </xsl:attribute>
+ <img src="left.png" width="24" height="24" border="0">
+ <xsl:attribute name="alt">
+ <xsl:call-template name="gentext">
+ <xsl:with-param name="key">nav-prev</xsl:with-param>
+ </xsl:call-template>
+ </xsl:attribute>
+ </img>
+ </a>
+ </td>
+ </xsl:when>
+ <xsl:otherwise>
+ <td>&#160;</td>
+ </xsl:otherwise>
+ </xsl:choose>
+ <xsl:choose>
+ <xsl:when test="count($up) > 0 and $up != $home">
+ <td>
+ <a accesskey="u">
+ <xsl:attribute name="href">
+ <xsl:call-template name="href.target">
+ <xsl:with-param name="object" select="$up"/>
+ </xsl:call-template>
+ </xsl:attribute>
+ <img src="up.png" width="24" height="24" border="0">
+ <xsl:attribute name="alt">
+ <xsl:call-template name="gentext">
+ <xsl:with-param name="key">nav-up</xsl:with-param>
+ </xsl:call-template>
+ </xsl:attribute>
+ </img>
+ </a>
+ </td>
+ </xsl:when>
+ <xsl:otherwise>
+ <td>&#160;</td>
+ </xsl:otherwise>
+ </xsl:choose>
+ <xsl:choose>
+ <xsl:when test="$home != .">
+ <td>
+ <a accesskey="h">
+ <xsl:attribute name="href">
+ <xsl:call-template name="href.target">
+ <xsl:with-param name="object" select="$home"/>
+ </xsl:call-template>
+ </xsl:attribute>
+ <img src="home.png" width="24" height="24" border="0">
+ <xsl:attribute name="alt">
+ <xsl:call-template name="gentext">
+ <xsl:with-param name="key">nav-home</xsl:with-param>
+ </xsl:call-template>
+ </xsl:attribute>
+ </img>
+ </a>
+ </td>
+ </xsl:when>
+ <xsl:otherwise>
+ <td>&#160;</td>
+ </xsl:otherwise>
+ </xsl:choose>
+ <th width="100%" align="center">
+ <xsl:apply-templates select="$home" mode="object.title.markup"/>
+ </th>
+ <xsl:choose>
+ <xsl:when test="count($next) > 0">
+ <td>
+ <a accesskey="n">
+ <xsl:attribute name="href">
+ <xsl:call-template name="href.target">
+ <xsl:with-param name="object" select="$next"/>
+ </xsl:call-template>
+ </xsl:attribute>
+ <img src="right.png" width="24" height="24" border="0">
+ <xsl:attribute name="alt">
+ <xsl:call-template name="gentext">
+ <xsl:with-param name="key">nav-next</xsl:with-param>
+ </xsl:call-template>
+ </xsl:attribute>
+ </img>
+ </a>
+ </td>
+ </xsl:when>
+ <xsl:otherwise>
+ <td>&#160;</td>
+ </xsl:otherwise>
+ </xsl:choose>
+ </tr>
+ <!--<xsl:if test="name()='refentry'"-->
+ <xsl:choose>
+ <xsl:when test="count($refsections) > 0">
+ <tr>
+ <td colspan="5" class="shortcuts">
+ <xsl:if test="count($sect_synopsis) > 0">
+ <a href="#{$section_id}.synopsis" class="shortcut">Top</a>
+ </xsl:if>
+ <xsl:if test="count($sect_desc) > 0">
+ &#160;|&#160;
+ <a href="#{$section_id}.description" class="shortcut">
+ <xsl:value-of select="./refsect1[@role='desc']/title"/>
+ </a>
+ </xsl:if>
+ <xsl:if test="count($sect_object_hierarchy) > 0">
+ &#160;|&#160;
+ <a href="#{$section_id}.object-hierarchy" class="shortcut">
+ <xsl:value-of select="./refsect1[@role='object_hierarchy']/title"/>
+ </a>
+ </xsl:if>
+ <xsl:if test="count($sect_impl_interfaces) > 0">
+ &#160;|&#160;
+ <a href="#{$section_id}.implemented-interfaces" class="shortcut">
+ <xsl:value-of select="./refsect1[@role='impl_interfaces']/title"/>
+ </a>
+ </xsl:if>
+ <xsl:if test="count($sect_prerequisites) > 0">
+ &#160;|&#160;
+ <a href="#{$section_id}.prerequisites" class="shortcut">
+ <xsl:value-of select="./refsect1[@role='prerequisites']/title"/>
+ </a>
+ </xsl:if>
+ <xsl:if test="count($sect_derived_interfaces) > 0">
+ &#160;|&#160;
+ <a href="#{$section_id}.derived-interfaces" class="shortcut">
+ <xsl:value-of select="./refsect1[@role='derived_interfaces']/title"/>
+ </a>
+ </xsl:if>
+ <xsl:if test="count($sect_implementations) > 0">
+ &#160;|&#160;
+ <a href="#{$section_id}.implementations" class="shortcut">
+ <xsl:value-of select="./refsect1[@role='implementations']/title"/>
+ </a>
+ </xsl:if>
+ <xsl:if test="count($sect_properties) > 0">
+ &#160;|&#160;
+ <a href="#{$section_id}.properties" class="shortcut">
+ <xsl:value-of select="./refsect1[@role='properties']/title"/>
+ </a>
+ </xsl:if>
+ <xsl:if test="count($sect_child_properties) > 0">
+ &#160;|&#160;
+ <a href="#{$section_id}.child-properties" class="shortcut">
+ <xsl:value-of select="./refsect1[@role='child_properties']/title"/>
+ </a>
+ </xsl:if>
+ <xsl:if test="count($sect_style_properties) > 0">
+ &#160;|&#160;
+ <a href="#{$section_id}.style-properties" class="shortcut">
+ <xsl:value-of select="./refsect1[@role='style_properties']/title"/>
+ </a>
+ </xsl:if>
+ <xsl:if test="count($sect_signal_proto) > 0">
+ &#160;|&#160;
+ <a href="#{$section_id}.signals" class="shortcut">
+ <xsl:value-of select="./refsect1[@role='signal_proto']/title"/>
+ </a>
+ </xsl:if>
+ <!--
+ <xsl:if test="count($sect_details) > 0">
+ <a href="#details" class="shortcut">
+ <xsl:value-of select="./refsect1[@id='details']/title"/>
+ </a>
+ &#160;|&#160;
+ </xsl:if>
+ <xsl:if test="count($sect_property_details) > 0">
+ <a href="#property_details" class="shortcut">
+ <xsl:value-of select="./refsect1[@id='property_details']/title"/>
+ </a>
+ &#160;|&#160;
+ </xsl:if>
+ <xsl:if test="count($sect_child_property_details) > 0">
+ <a href="#child_property_details" class="shortcut">
+ <xsl:value-of select="./refsect1[@id='property_child_details']/title"/>
+ </a>
+ &#160;|&#160;
+ </xsl:if>
+ <xsl:if test="count($sect_style_property_details) > 0">
+ <a href="#style_property_details" class="shortcut">
+ <xsl:value-of select="./refsect1[@id='style_property_details']/title"/>
+ </a>
+ &#160;|&#160;
+ </xsl:if>
+ <xsl:if test="count($sect_signals) > 0">
+ <a href="#signals" class="shortcut">
+ <xsl:value-of select="./refsect1[@id='signals']/title"/>
+ </a>
+ &#160;|&#160;
+ </xsl:if>
+ -->
+ </td>
+ </tr>
+ </xsl:when>
+ <!-- this is not yet very nice, as it requires all glossdic/indexdiv
+ elements having a anchor element. maybe we can customize the xsl
+ to automatically create local anchors
+ -->
+ <xsl:when test="count($glssections) > 0">
+ <tr>
+ <td colspan="5" class="shortcuts">
+ <xsl:for-each select="./glossdiv">
+ <xsl:if test="position() > 1">
+ &#160;|&#160;
+ </xsl:if>
+ <a class="shortcut">
+ <xsl:attribute name="href">#gls<xsl:value-of select="./title"/></xsl:attribute>
+ <xsl:value-of select="./title"/>
+ </a>
+ </xsl:for-each>
+ </td>
+ </tr>
+ </xsl:when>
+ <xsl:when test="count($idxsections) > 0">
+ <tr>
+ <td colspan="5" class="shortcuts">
+ <xsl:for-each select="./indexdiv/indexdiv">
+ <xsl:if test="position() > 1">
+ &#160;|&#160;
+ </xsl:if>
+ <a class="shortcut">
+ <xsl:attribute name="href">#idx<xsl:value-of select="./title"/></xsl:attribute>
+ <xsl:value-of select="./title"/>
+ </a>
+ </xsl:for-each>
+ </td>
+ </tr>
+ </xsl:when>
+ </xsl:choose>
+ </table>
+ </xsl:if>
+ </xsl:template>
+
+ <xsl:template name="footer.navigation">
+ </xsl:template>
+
+ <!-- avoid creating multiple identical indices
+ if the stylesheets don't support filtered indices
+ -->
+ <xsl:template match="index">
+ <xsl:variable name="has-filtered-index">
+ <xsl:call-template name="version-greater-or-equal">
+ <xsl:with-param name="ver1" select="$VERSION" />
+ <xsl:with-param name="ver2">1.66</xsl:with-param>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:if test="($has-filtered-index = 1) or (count(@role) = 0)">
+ <xsl:apply-imports/>
+ </xsl:if>
+ </xsl:template>
+
+ <xsl:template match="index" mode="toc">
+ <xsl:variable name="has-filtered-index">
+ <xsl:call-template name="version-greater-or-equal">
+ <xsl:with-param name="ver1" select="$VERSION" />
+ <xsl:with-param name="ver2">1.66</xsl:with-param>
+ </xsl:call-template>
+ </xsl:variable>
+ <xsl:if test="($has-filtered-index = 1) or (count(@role) = 0)">
+ <xsl:apply-imports/>
+ </xsl:if>
+ </xsl:template>
+
+ <xsl:template match="para">
+ <xsl:choose>
+ <xsl:when test="@role = 'gallery'">
+ <div class="container">
+ <div class="gallery-spacer"> </div>
+ <xsl:apply-templates mode="gallery.mode"/>
+ <div class="gallery-spacer"> </div>
+ </div>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:apply-imports/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+ <!-- FIXME: try if that works too -->
+ <!--xsl:template match="para[@role='gallery']">
+ <div class="container">
+ <div class="gallery-spacer"> </div>
+ <xsl:apply-templates mode="gallery.mode"/>
+ <div class="gallery-spacer"> </div>
+ </div>
+ </xsl:template-->
+
+
+
+ <xsl:template match="link" mode="gallery.mode">
+ <div class="gallery-float">
+ <xsl:apply-templates select="."/>
+ </div>
+ </xsl:template>
+
+ <!-- add gallery handling to refnamediv template -->
+ <xsl:template match="refnamediv">
+ <div class="{name(.)}">
+ <table width="100%">
+ <tr><td valign="top">
+ <xsl:call-template name="anchor"/>
+ <xsl:choose>
+ <xsl:when test="$refentry.generate.name != 0">
+ <h2>
+ <xsl:call-template name="gentext">
+ <xsl:with-param name="key" select="'RefName'"/>
+ </xsl:call-template>
+ </h2>
+ </xsl:when>
+ <xsl:when test="$refentry.generate.title != 0">
+ <h2>
+ <xsl:choose>
+ <xsl:when test="../refmeta/refentrytitle">
+ <xsl:apply-templates select="../refmeta/refentrytitle"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:apply-templates select="refname[1]"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </h2>
+ </xsl:when>
+ </xsl:choose>
+ <p>
+ <xsl:apply-templates/>
+ </p>
+ </td>
+ <td valign="top" align="right">
+ <xsl:choose>
+ <xsl:when test="../refmeta/refmiscinfo/inlinegraphic">
+ <xsl:apply-templates select="../refmeta/refmiscinfo/inlinegraphic"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <!-- find the gallery image to use here
+ - determine the id of the enclosing refentry
+ - look for an inlinegraphic inside a link with linkend == refentryid inside a para with role == gallery
+ - use it here
+ -->
+ <xsl:variable name="refentryid" select="../@id"/>
+ <xsl:apply-templates select="key('gallery.key', $refentryid)/inlinegraphic"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </td></tr>
+ </table>
+ </div>
+ </xsl:template>
+
+ <!-- add anchors for index sections -->
+ <xsl:template match="indexdiv">
+ <a><xsl:attribute name="name">idx<xsl:value-of select="./title"/></xsl:attribute></a>
+ <xsl:apply-templates/>
+ </xsl:template>
+
+ <!-- add anchors for glossary sections -->
+ <xsl:template match="glossdiv">
+ <a><xsl:attribute name="name">gls<xsl:value-of select="./title"/></xsl:attribute></a>
+ <xsl:apply-templates/>
+ </xsl:template>
+
+ <!-- Exterminate any trace of indexterms in the main flow -->
+ <xsl:template match="indexterm">
+ </xsl:template>
+
+ <!-- Extra link on the right side of doc-blobs -->
+ <xsl:template name="user.format.extralinks">
+ <xsl:if test="../ulink[@role='extralinks']">
+ <span class="extralinks">
+ <xsl:for-each select="../ulink[@role='extralinks']">
+ <xsl:if test="position() = 1">[&#160;</xsl:if>
+ <xsl:if test="position() > 1">&#160;|&#160;</xsl:if>
+ <a>
+ <xsl:attribute name="href"><xsl:value-of select="@url"/></xsl:attribute>
+ <xsl:copy-of select="text()" />
+ </a>
+ <xsl:if test="position() = last()">&#160;]</xsl:if>
+ </xsl:for-each>
+ </span>
+ </xsl:if>
+ <!--xsl:copy-of select="text()" /-->
+ <xsl:apply-templates/>
+ </xsl:template>
+
+ <!-- this is not in use yet (see gtkdoc-mkdb
+ <xsl:template match="//refsect2/ulink[@role='extralinks']"/>
+ <xsl:template match="//refsect1/ulink[@role='extralinks']"/>
+
+ <xsl:template match="//refsect2/title">
+ <h3><xsl:call-template name="user.format.extralinks"/></h3>
+ </xsl:template>
+
+ <xsl:template match="//refsect1/title">
+ <h2><xsl:call-template name="user.format.extralinks"/></h2>
+ </xsl:template>
+ -->
+
+ <!-- ==================================================================== -->
+
+ <xsl:template match="acronym">
+ <xsl:call-template name="generate.acronym.link"/>
+ </xsl:template>
+
+ <xsl:template name="generate.acronym.link">
+ <xsl:param name="acronym">
+ <xsl:apply-templates/>
+ </xsl:param>
+ <!--
+ We use for-each to change context to the database document because key()
+ only locates elements in the same document as the context node!
+ -->
+
+ <xsl:param name="value" >
+ <xsl:value-of select="key('acronym.key', $acronym)/../glossdef/para[1]" />
+ </xsl:param>
+ <xsl:choose>
+ <xsl:when test="$value=''">
+ <!-- debug -->
+ <xsl:message>
+ In gtk-doc.xsl: For acronym (<xsl:value-of select="$acronym"/>) no value found!
+ </xsl:message>
+ <a>
+ <xsl:attribute name="href">
+ <xsl:text>http://foldoc.org/</xsl:text>
+ <xsl:value-of select="$acronym"/>
+ </xsl:attribute>
+ <xsl:call-template name="inline.charseq"/>
+ </a>
+ </xsl:when>
+ <xsl:otherwise>
+ <!-- found -->
+ <acronym>
+ <xsl:attribute name="title">
+ <xsl:value-of select="$value"/>
+ </xsl:attribute>
+ <xsl:call-template name="inline.charseq"/>
+ </acronym>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:template>
+
+</xsl:stylesheet>
diff --git a/doc/guide/https.xml b/doc/guide/https.xml
new file mode 100644
index 0000000..5cf427b
--- /dev/null
+++ b/doc/guide/https.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="https">
+ <title>SSL/TLS Usage</title>
+
+ <para>Cockpit usually requires that web browsers communicate with it using HTTPS,
+ for security reasons.</para>
+
+ <section id="https-required">
+ <title>HTTPS Requirement</title>
+
+ <para>Cockpit listens for both HTTP and HTTPS connections on the same port, by
+ default 9090. If an HTTP connection is made, Cockpit will redirect that
+ connection to HTTPS. There are some exceptions:</para>
+
+ <itemizedlist>
+ <listitem><para>If an HTTP connection comes from <code>127.0.0.0/8</code>, then
+ Cockpit will allow communication without redirecting to HTTPS.</para></listitem>
+ <listitem><para>Certain URLs, like <code>/ping</code> are not required to use
+ HTTPS.</para></listitem>
+ </itemizedlist>
+
+ <para>This behavior can be overridden by setting the
+ <code>AllowUnencrypted</code> option in <code>cockpit.conf</code>.</para>
+ </section>
+
+ <section id="https-certificates">
+ <title>Certificates</title>
+
+ <para>Cockpit will load a certificate from the <code>/etc/cockpit/ws-certs.d</code>,
+ directory, or below <code>$XDG_CONFIG_DIRS</code> if set (see
+ <ulink url="./cockpit.conf.5.html">cockpit.conf</ulink>).
+ It will use the last file with a <code>.cert</code> or <code>.crt</code>
+ extension in alphabetical order. The file should contain one or more OpenSSL
+ style <literal>BEGIN CERTIFICATE</literal> blocks for the server certificate and
+ the intermediate certificate authorities.</para>
+
+ <para>
+ The private key must be contained in a separate file with the same name as the
+ certificate, but with a <code>.key</code> suffix instead. The key must not be
+ encrypted.</para>
+
+ <para>If no certificate is found, a self-signed certificate is created and
+ stored in the <filename>0-self-signed.cert</filename> file. On some
+ platforms, Cockpit will also generate a ca.crt in that directory, which
+ may be safely imported into client browsers.</para>
+
+ <para>Cockpit will read the files as root, so they can have tight
+ permissions.</para>
+
+ <para>To check which certificate <code>cockpit-ws</code> will use run
+ the following command.</para>
+<programlisting>
+$ sudo /usr/libexec/cockpit-certificate-ensure --check
+</programlisting>
+ <para>Or, on Debian-based systems:</para>
+<programlisting>
+$ sudo /usr/lib/cockpit/cockpit-certificate-ensure --check
+</programlisting>
+
+ <para>If using <code>certmonger</code> to manage certificates, following command can
+ be used to automatically prepare a certificate/key file pair:</para>
+
+ <programlisting>
+getcert request -f /etc/cockpit/ws-certs.d/50-certmonger.cert \
+ -k /etc/cockpit/ws-certs.d/50-certmonger.key \
+ -D myhostname.example.com \
+ [--ca=...]</programlisting>
+
+ <para>This will not work on Red Hat Enterprise Linux/CentOS 8 by default. Adjust the SELinux type of the certificate directory to <code>cert_t</code> to allow certmonger to write its certificates there:</para>
+
+ <programlisting>
+semanage fcontext -a -t cert_t '/etc/cockpit/ws-certs\.d(/.*)?'
+restorecon -v /etc/cockpit/ws-certs.d</programlisting>
+ </section>
+
+</chapter>
diff --git a/doc/guide/listen.xml b/doc/guide/listen.xml
new file mode 100644
index 0000000..327bbe3
--- /dev/null
+++ b/doc/guide/listen.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="listen">
+ <title>TCP Port and Address</title>
+
+ <para>Cockpit's <code>cockpit-ws</code> component is configured by default to accept
+ connections on port <code>9090</code>. This is the port that is documented for a
+ "Web-based System Manager" to listen on. It is also relatively memorable.</para>
+
+ <para>However there are many reasons you may wish to change the default port. For
+ example other software may use port <code>9090</code> or you may wish to setup
+ Cockpit to listen on <code>443</code> instead. It is also possible to have Cockpit
+ only listen on one specific IP address.</para>
+
+ <para>Note that it is only required to have Cockpit listening on a TCP port on
+ the server that you access with your web browser. If you add multiple servers
+ with host switcher, Cockpit will connect to those servers via
+ <code>ssh</code>.</para>
+
+ <para>The systems that Cockpit runs on are typically locked down with firewalls,
+ SELinux, so changing the default port is not as easy as editing a configuration
+ file.</para>
+
+ <section id="listen-systemd">
+ <title>Cockpit systemd Socket</title>
+
+ <para>On servers with
+ <ulink url="https://www.freedesktop.org/wiki/Software/systemd/"><code>systemd</code></ulink>
+ Cockpit starts on demand via socket activation. To change its port and/or address
+ you should place the following content in the
+ <code>/etc/systemd/system/cockpit.socket.d/listen.conf</code> file. Create the file
+ and directories in that path which not already exist. The <code>ListenStream</code>
+ option specifies the desired address and TCP port.</para>
+
+<programlisting>
+[Socket]
+ListenStream=
+ListenStream=443
+</programlisting>
+
+<programlisting>
+[Socket]
+ListenStream=
+ListenStream=7777
+ListenStream=192.168.1.1:443
+FreeBind=yes
+</programlisting>
+
+ <para>NOTE: The first line with an empty value is intentional. <code>systemd</code> allows multiple <code>Listen</code> directives to be declared in a single socket unit; an empty value in a drop-in file resets the list and thus disables the default port 9090 from the original unit.</para>
+
+ <para>The <code>FreeBind</code> option is highly recommended when defining specific IP addresses. See the <ulink url="https://www.freedesktop.org/software/systemd/man/systemd.socket.html"><code>systemd.socket</code> manpage</ulink> for details.</para>
+
+ <para>In order for the changes to take effect, run the following commands:</para>
+
+<programlisting>
+$ sudo systemctl daemon-reload
+$ sudo systemctl restart cockpit.socket
+</programlisting>
+
+ </section>
+
+ <section id="listen-selinux">
+ <title>SELinux Port</title>
+
+ <para>If <ulink url="https://selinuxproject.org/page/Main_Page">SELinux</ulink> is
+ protecting your server, then you will need to tell it to allow Cockpit to listen
+ on the new port. Run the following command to do so. The last argument specifies
+ the desired TCP port.</para>
+
+<programlisting>
+$ sudo semanage port -a -t websm_port_t -p tcp 9999
+</programlisting>
+
+ <para>If the port is already defined by some other part of the SELinux policy, then
+ you will need to use the <code>-m</code> argument to modify the definition. That's
+ the case with the <code>443</code> SSL port, which is typically defined as an
+ <code>http_port_t</code> port.</para>
+
+<programlisting>
+$ sudo semanage port -m -t websm_port_t -p tcp 443
+</programlisting>
+
+ <para>The changes should take effect immediately.</para>
+ </section>
+
+ <section id="listen-firewalld">
+ <title>Firewalld Port</title>
+
+ <para>If <ulink url="https://fedoraproject.org/wiki/FirewallD">Firewalld</ulink> is
+ configured as your firewall, then you will need to tell it to allow Cockpit to
+ receive connections on the new port. Run the following commands to do so. The last
+ options specify the desired TCP port.</para>
+
+<programlisting>
+$ sudo firewall-cmd [--zone=ZONE] --add-port=443/tcp
+$ sudo firewall-cmd --permanent [--zone=ZONE] --add-port=443/tcp
+</programlisting>
+
+ </section>
+
+</chapter>
diff --git a/doc/guide/packages.xml b/doc/guide/packages.xml
new file mode 100644
index 0000000..8a6f086
--- /dev/null
+++ b/doc/guide/packages.xml
@@ -0,0 +1,517 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="packages">
+ <title>Cockpit Packages</title>
+
+ <para>Cockpit is separated into various packages, each of which brings specific
+ features and/or code.</para>
+
+ <warning>
+ <para>In addition, any APIs or behavior not explicitly documented here is an
+ internal API and can be changed at any time.</para>
+ </warning>
+
+ <section id="package-layout">
+ <title>Layout of Package Files</title>
+
+ <para>A package consists of one or more files placed in a directory or its
+ subdirectories. It must have a <code>manifest.json</code> file and follow
+ certain naming conventions.</para>
+
+ <para>The name of a package is the name of the directory.</para>
+
+ <para>The name of the package must be ASCII alphanumeric, and may contain an underscore.
+ Names of directories and files in the package must consist of ASCII alphanumeric
+ along with dash, underscore, dot, and comma. No spaces are allowed.</para>
+
+ <para>Cockpit uses the data directories from the
+ <ulink url="https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG Base Directory
+ Specification</ulink>
+ to locate packages. The <code>$XDG_DATA_DIRS</code> represents a colon separate list of system data
+ directories, and <code>$XDG_DATA_HOME</code> is a user specific data directory. If the environment
+ variables are not set, defaults are used, according to the spec. If cockpit has been built with an
+ alternate <code>--prefix=/path</code> then the <code>$prefix/share/cockpit</code> is used by
+ default.</para>
+
+ <para>A <code>cockpit/</code> subdirectories in any of these data directories is the location where
+ packages are loaded by Cockpit. If Cockpit finds a package with the same name, in multiple data
+ directories, then the first one wins. According to the spec the first data directory is
+ <code>$XDG_DATA_HOME</code> and then <code>$XDG_DATA_DIRS</code> in order.</para>
+
+ <para>This means that, by default the following directories are searched for cockpit packages, and
+ in this order:</para>
+
+ <itemizedlist>
+ <listitem><para><code>~/.local/share/cockpit/</code></para></listitem>
+ <listitem><para><code>/usr/local/share/cockpit/</code></para></listitem>
+ <listitem><para><code>/usr/share/cockpit/</code></para></listitem>
+ </itemizedlist>
+
+ <para>Packages placed in <code>$XDG_DATA_HOME</code> are not cached by Cockpit or the web browser.
+ Other packages are cached aggressively, and are accessed using a checksum of the files in
+ the packages and their names.</para>
+
+ <para>You can use the following command to list the packages installed on a server. You'll note that
+ it's output may change when you run the command as different users, if there are packages installed
+ in the user's home directory.</para>
+
+<programlisting>
+$ cockpit-bridge --packages
+...
+</programlisting>
+
+ <para>To further clarify things, here is an example package called "my-package" and its file layout:</para>
+
+<programlisting>
+/usr/share/cockpit/
+ my-package/
+ manifest.json
+ file.html
+ some.js
+</programlisting>
+
+ <para>Place or symlink packages in your <code>~/.local/share/cockpit</code> directory (or appropriate
+ <code>$XDG_DATA_HOME</code> location) that you would like to modify and develop. System installed
+ packages should not change while Cockpit is running.</para>
+
+ </section>
+
+ <section id="package-manifest">
+ <title>Package Manifest</title>
+
+ <para>Each package has a <code>manifest.json</code> file. It is a JSON object. The following
+ fields may be present in the manifest:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term>content-security-policy</term>
+ <listitem>
+ <para>By default Cockpit serves packages using a strict
+ <ulink url="https://en.wikipedia.org/wiki/Content_Security_Policy">Content Security Policy</ulink>,
+ which among other things does not allow inline styles or scripts. This can
+ be overridden on a per-package basis, with this setting.</para>
+ <para>If the overridden content security policy does not contain a <code>default-src</code>,
+ <code>connect-src</code>, <code>base-uri</code>, <code>form-action</code>, <code>object-src</code>,
+ or <code>block-all-mixed-content</code> then these will be added to the policy from the manifest.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>name</term>
+ <listitem><para>An optional string that changes the name of the package. Normally
+ packages derive their name from the directory that they are located in. This
+ field overrides that name.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>priority</term>
+ <listitem><para>An optional number that specifies which package is preferred in cases
+ where there are conflicts. For example given two packages with the same
+ <code>name</code> a package is chosen based on its priority.</para></listitem>
+ </varlistentry>
+ <!-- TODO: enable this once moving to the Python bridge by default; also update example below
+ <varlistentry>
+ <term>conditions</term>
+ <listitem><para>An optional list of <code>{"predicate": "value"}</code> objects. Cockpit will only
+ consider the package if <emphasis>all</emphasis> conditions are met. Currently supported predicates
+ are <code>path-exists</code> and <code>path-not-exists</code>. Unknown predicates are ignored.
+ This is preferable to using <code>priority</code>, but only available since Cockpit FIXME-RELEASE.</para>
+ </listitem>
+ </varlistentry>
+ -->
+ <varlistentry>
+ <term>requires</term>
+ <listitem><para>An optional JSON object that contains a <code>"cockpit"</code>
+ string version number. The package will only be usable if the Cockpit bridge
+ and javascript base are equal or newer than the given version number.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>version</term>
+ <listitem><para>An informational version number for the package.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>preload</term>
+ <listitem><para>A list of identifiers of the
+ components that should be preloaded. Normally, the files of a
+ component are loaded when the user navigates to it for the
+ first time. The files of a preloaded component are loaded
+ immediately after the user logs in, and the initialization
+ code of the component is invoked.
+ </para><para>
+ The value of this field is an array of strings, where each
+ string is one of the keys used in the <code>dashboard</code>,
+ <code>menu</code>, or <code>tool</code> fields that are
+ explained below.
+ </para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>parent</term>
+ <listitem><para>This option is used when module does not have its own menu item but is
+ a part of a different module. This is described by JSON object with properties <code>component</code>
+ which takes name of the superordinate component and <code>docs</code> with list of documentation URLs
+ for the given page. See below for structure of <code>docs</code> property.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>In addition, the following keys contain information about where components of the package
+ should appear in Cockpit's user interface. Each of these keys is optional and contains an
+ object mapping unique identifiers to menu items, which are described below. (The naming of
+ these fields doesn't perfectly match the current user interface for historical reasons.)
+ </para>
+
+ <variablelist>
+ <varlistentry>
+ <term>dashboard</term>
+ <listitem><para>Dashboard items appear in the menu under the section <emphasis>Apps</emphasis>.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>menu</term>
+ <listitem><para>These items appear in the menu under the section <emphasis>System</emphasis>.
+ This section is roughly ordered into these categories (with their
+ <emphasis>order</emphasis> in parentheses):
+ <itemizedlist>
+ <listitem><para>System Information (10)</para></listitem>
+ <listitem><para>Logs (20)</para></listitem>
+ <listitem><para>Configuring major subsystems (30-40)</para></listitem>
+ <listitem><para>Things running on the machine (VMs, Containers - 50-60)</para></listitem>
+ <listitem><para>Implementation Details (Accounts, Services - 70-100)</para></listitem>
+ </itemizedlist></para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>tools</term>
+ <listitem><para>These items appear in the menu under the section <emphasis>Tools</emphasis>.
+ </para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>Menu items and tools are registered using JSON objects that have the
+ following properties:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term>label</term>
+ <listitem><para>The label for the menu item or tool.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>order</term>
+ <listitem><para>An optional order number to place this menu item or tool. Lower numbers
+ are listed first.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>path</term>
+ <listitem><para>The relative path to the HTML file within the package that implements
+ the menu item or tool.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>docs</term>
+ <listitem><para>List of documentation URLs for the given page.
+ Each item is an object containing <code>label</code> and <code>url</code>.
+ </para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>keywords</term>
+ <listitem><para>Keywords that describe the page and which are used for searching.
+ These keywords should be lowercase. Keywords is a list containing keyword
+ items as described below. Page label is prepended as first keyword
+ in the first keyword item.
+ </para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>Keyword items are registered using JSON objects that have the
+ following properties:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term>matches</term>
+ <listitem><para>List of keywords to be matched.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>goto</term>
+ <listitem><para>Optional path that is used for all keywords in this item.
+ When this argument starts with slash, then it is used as pathname, otherwise
+ it is used as hash. Defining <code>goto:"page_hash"</code> in page with <code>path:"/page_path"</code>
+ would redirect to <code>/page_path#page_hash</code>, while <code>goto:"/page_path"</code>
+ would redirect to <code>/page_path</code> ignoring default page path.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>weight</term>
+ <listitem><para>How much keywords are prioritized over others. Default is 3.
+ </para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>translate</term>
+ <listitem><para><code>false</code> when keywords should not be localized. Default is <code>true</code>.
+ </para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>An example manifest.json with some optional properties set:</para>
+
+<programlisting>
+{
+ "version": 0,
+ "require": {
+ "cockpit": "120"
+ },
+ "tools": {
+ "mytool": {
+ "label": "My Tool",
+ "path": "tool.html"
+ }
+ }
+}
+</programlisting>
+
+<!-- TODO: enable this once moving to the Python bridge by default
+<programlisting>
+{
+ "version": 0,
+ "require": {
+ "cockpit": "120"
+ },
+ "conditions": [
+ {"path-exists": "/usr/bin/mytool"},
+ {"path-exists": "/etc/mytool.conf"},
+ {"path-not-exist": "/etc/incompatible-tool"}
+ ],
+ "tools": {
+ "mytool": {
+ "label": "My Tool",
+ "path": "tool.html"
+ }
+ }
+}
+</programlisting>
+-->
+
+ </section>
+
+ <section id="package-manifest-override">
+ <title>Manifest overrides</title>
+
+ <para>To change a manifest system-wide, a file <filename>&lt;package-directory-name&gt;.override.json</filename>
+ may be placed into <filename>/etc/cockpit/</filename>, or below <code>$XDG_CONFIG_DIRS</code>
+ if set (see <ulink url="./cockpit.conf.5.html">cockpit.conf</ulink>). To change it for a particular user only, put the
+ override into <filename>~/.config/cockpit</filename>.</para>
+
+ <para>These override the information in the manifest in the simple
+ <ulink url="https://tools.ietf.org/html/rfc7386">JSON Merge Patch</ulink> format.</para>
+
+ <para>This can be used to hide or modify menu items of an existing package.
+ For example <filename>/etc/cockpit/systemd.override.json</filename> or
+ <filename>~/.config/cockpit/systemd.override.json</filename> could
+ hide the <emphasis>Logs</emphasis> menu item and move the <emphasis>Services</emphasis>
+ menu item to the top of the menu.</para>
+
+<programlisting>
+{
+ "menu": {
+ "logs": null,
+ "services": {
+ "order": -1
+ }
+ }
+}
+</programlisting>
+
+ </section>
+
+ <section id="package-links">
+ <title>Package Links and Paths</title>
+
+ <para>When referring to files in your package, such as in a hyperlink or a <code>&lt;style&gt;</code>
+ tag or <code>&lt;script&gt;</code> tag, simply use a relative path, and refer to the files
+ in the same directory. When you need to refer to files in another package use a relative link.</para>
+
+ <para>For example here's how to include the base <code>cockpit.js</code> script in your HTML
+ from the <code>latest</code> package:</para>
+
+<programlisting>
+&lt;script src="../base1/cockpit.js"&gt;&lt;/script&gt;
+</programlisting>
+
+ <para>Do not assume you can link to any file in any other package. Refer to the
+ <link linkend="development">list of API packages</link> for those that are
+ available for use.</para>
+ </section>
+
+ <section id="package-minified">
+ <title>Content Negotiation</title>
+
+ <para>In order to support gzipped and/or minified data, the files in a package are
+ loaded using content negotiation logic. A HTTP request for the file <code>test.js</code>
+ in the package named <code>mypackage</code> will return <code>mypackage/test.js</code>
+ or <code>mypackage/test.js.gz</code> (in undefined preference). If neither exists,
+ then it returns <code>mypackage/test.js.min</code> or <code>mypackage/test.js.min.gz</code>
+ (again in undefined preference).</para>
+
+ <para>When packages are loaded from a system directory, Cockpit optimizes the file
+ system lookups above, by pre-listing the files. This is one of the reasons that
+ you should never change packages installed to a system directory while Cockpit
+ is running.</para>
+ </section>
+
+ <section id="package-api">
+ <title>Using Cockpit API</title>
+
+ <para>Cockpit has API available for writing packages. There is no API available
+ for external callers to invoke via HTTP, REST or otherwise.</para>
+
+ <para>API from various packages can be used to implement Cockpit packages. Each package
+ listed here has some API available for use. Only the API explicitly documented should
+ be used.</para>
+
+ <itemizedlist>
+ <listitem><para><link linkend="development">API Listing</link></para></listitem>
+ </itemizedlist>
+
+ <para>To include javascript from the API, simply load it into your HTML using
+ a script tag. Alternatively you can use an javascript loader.</para>
+ </section>
+
+ <section id="package-bridges">
+ <title>Bridges for specific tasks</title>
+
+ <para>On the server side the
+ <link linkend="cockpit-bridge.1"><filename>cockpit-bridge</filename></link> connects
+ to various system APIs that the front end UI requests it to. There are additional
+ bridges for specific tasks that the main <filename>cockpit-bridge</filename> cannot
+ handle, such as using the PCP C library API.</para>
+
+ <para>These additional bridges can be registered in a <code>"bridges"</code> section of a
+ package's <filename>manifest.json</filename> file. Building such a bridge is a complex tasks, and
+ we will skip over that here. However it is useful to adjust how these additional bridges
+ are called, and so we'll look at how they are registered.</para>
+
+ <para>An example <filename>manifest.json</filename> with a bridges section:</para>
+
+<programlisting>
+{
+ "bridges": [
+ {
+ "match": { "payload": "metrics1" },
+ "spawn": [ "/usr/libexec/cockpit-pcp" ]
+ }
+ ]
+}
+</programlisting>
+
+ <para>The bridges are considered in the order they are listed in the array. Use the
+ <filename>manifest.json</filename><code>"priority"</code> field to control order between
+ packages. The bridges are registered using JSON objects that have the following
+ properties:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term>environ</term>
+ <listitem><para>Optional, additional environment variables to pass to the bridge
+ command.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>match</term>
+ <listitem><para>The <code>"match"</code> object describes which channel open command
+ options need to match for a given channel to be handed over to this
+ bridge.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>privileged</term>
+ <listitem><para>If set to <code>true</code>, this marks the bridge as a superuser bridge. Cockpit will start one of these explicitly when trying to escalate the privileges of a session. A privileged bridge can not have a <code>"match"</code> property.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>label</term>
+ <listitem><para>Setting this enables selection of privileged
+ bridges in the UI. When no privileged bridge has a
+ <code>label</code>, then Cockpit will start the bridge that
+ runs <code>sudo</code>. This is the case in a default Cockpit
+ installation. When at least one privileged bridge has a
+ <code>label</code> then the user can select one of them when
+ escalating privileges. As a special case, if only one bridge
+ has a <code>label</code>, then the step of selecting a bridge
+ is omitted in the UI and that one bridge is always started.</para>
+ <para>Thus, if you add a privileged bridge with a
+ <code>label</code> in a new manifest, Cockpit will use that
+ bridge the next time a user opens the "Administrative access"
+ dialog. If you want the user to choose between the
+ <code>sudo</code> method and your new one, you need to
+ duplicate the <code>sudo</code> bridge definition in your
+ manifest and give it a label.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>problem</term>
+ <listitem><para>If a problem is specified, and this bridge fails to start up then
+ channels will be closed with this problem code. Otherwise later bridges or internal
+ handlers for the channel will be invoked.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>spawn</term>
+ <listitem><para>The command and arguments to invoke.</para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>The <code>spawn</code> and <code>environ</code> values can be dynamically
+ taken from a matching open command values. When a value in either the <code>spawn</code>
+ or <code>environ</code> array contains a named variable wrapped in <code>${}</code>,
+ the variable will be replaced with the value contained in the matching open command.
+ Only named variables are supported and name can only contain letters, numbers and
+ the following symbols: <code>._-</code></para>
+
+ <para>For example a bridges section like:</para>
+<programlisting>
+{
+ "bridges": [
+ {
+ "match": { "payload": "example" },
+ "environ": [ "TAG=${tag}" ],
+ "spawn: [ "/example-bridge", "--tag", "${tag}" ],
+ "problem": "access-denied"
+ }
+ ]
+}
+</programlisting>
+
+ <para>when a open command is received with a payload of <code>example</code>
+ with <code>tag</code> value of <code>tag1</code>. The following
+ command will be spawned</para>
+
+ <programlisting>TAG=tag1 /example-bridge --tag tag1</programlisting>
+
+ <para>Processes that are reused so if another open command with a "tag" of
+ <code>tag1</code> is received. The open command will be passed to
+ existing process, rather than spawning a new one. However a open command
+ with an tag of <code>tag2</code> will spawn a new command:</para>
+
+ <programlisting>TAG=tag2 /example-bridge --tag tag2</programlisting>
+
+ <para>If you need to include <code>${}</code>, as an actual value in your arguments
+ you can escape it by prefixing it with a <code>\</code></para>
+
+ </section>
+
+ <section id="package-replace">
+ <title>Replacing an existing package</title>
+
+ <para>If the functionality in a package replaces that of another package
+ then it can replace that package by claiming the same <code>name</code> and a
+ higher <code>priority</code>.</para>
+
+ <para>For example, a package in the <filename>/usr/share/cockpit/disks</filename>
+ directory could replace Cockpit's <emphasis>storage</emphasis> package with
+ a <filename>manifest.json</filename> like this:</para>
+
+<programlisting>
+{
+ "version": 0,
+ "name": "storage",
+ "priority": 10,
+ "menu": {
+ "index": {
+ "label": "Disk Storage",
+ "order": 15
+ }
+ }
+}
+</programlisting>
+
+ </section>
+
+</chapter>
diff --git a/doc/guide/privileges.xml b/doc/guide/privileges.xml
new file mode 100644
index 0000000..b92dec5
--- /dev/null
+++ b/doc/guide/privileges.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="privileges">
+ <title>Privileges and Permissions</title>
+
+ <para>When a user is logged into Cockpit, they are logged into a normal session
+ that has exactly the same privileges as if they logged in via SSH or on
+ the console.</para>
+
+ <para>However, Cockpit will usually try to escalate the privileges
+ of the user using <ulink
+ url="https://www.freedesktop.org/wiki/Software/polkit/">Policy
+ Kit</ulink> or <ulink url="https://www.sudo.ws/">sudo</ulink>. If
+ the user is able to escalate privileges from the command line by
+ typing in their password again (or without typing in any password),
+ then Cockpit will be able to escalate the privileges of the session
+ to "root" immediately upon login.</para>
+
+ <para>The user can change the privileges of a session from within
+ that session, via the "Administrative access" indicator in the top
+ bar. From that indicator, the user can drop "root" privileges and
+ regain them. On the next login, Cockpit will give the session the
+ same privileges.</para>
+
+ <para>Usually a user needs to be in the <code>wheel</code> Unix user group for the
+ user to be able to escalate privileges in this way. However both Policy Kit and
+ sudo may be configured to use other criteria.</para>
+
+ <section id="privileges-polkit">
+ <title>Customizing Polkit Privileges</title>
+
+ <para>Services like <ulink url="https://www.freedesktop.org/wiki/Software/systemd/">systemd</ulink>
+ and <ulink url="https://wiki.gnome.org/Projects/NetworkManager">NetworkManager</ulink> use
+ <ulink url="https://www.freedesktop.org/wiki/Software/polkit/">Polkit</ulink> to
+ validate and escalate privileges. It is possible to customize these rules with files
+ in <filename>/etc/polkit-1/rules.d</filename>.</para>
+
+ <para>Polkit rules files are
+ <ulink url="https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html">javascript with specific methods and objects</ulink>. For example, placing the following polkit rule to
+ <filename>/etc/polkit-1.rules.d/10-operators.rule</filename> allows all users in the
+ <code>operators</code> group to start, stop, restart and otherwise manage systemd services:</para>
+
+<programlisting>
+polkit.addRule(function(action, subject) {
+ if (action.id == "org.freedesktop.systemd1.manage-units") {
+ if (subject.isInGroup("operators")) {
+ return polkit.Result.YES;
+ }
+ }
+});
+</programlisting>
+
+ <para>In order to allow a certain group to perform any administrative action you could add
+ a rule like this:</para>
+
+<programlisting>
+polkit.addAdminRule(function(action, subject) {
+ return ["unix-group:operators"];
+});
+</programlisting>
+
+ </section>
+</chapter>
diff --git a/doc/guide/sso.xml b/doc/guide/sso.xml
new file mode 100644
index 0000000..9cecee9
--- /dev/null
+++ b/doc/guide/sso.xml
@@ -0,0 +1,201 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="sso">
+ <title>Single Sign On</title>
+
+ <para>Cockpit can use Kerberos for Single Sign On authentication, where users are
+ automatically authenticated if they have a valid Kerberos ticket.</para>
+
+ <section id="sso-server">
+ <title>Server Requirements</title>
+
+ <para>To authenticate users, the server that Cockpit is running on must be
+ joined to a domain. This can usually be accomplished using the
+ <ulink url="https://freedesktop.org/software/realmd/docs/realm.html"><code>realm join example.com</code></ulink>
+ command.</para>
+
+ <para>The domain must be resolvable by DNS. For instance, the SRV records of the
+ kerberos server should be resolvable:</para>
+
+<programlisting>
+$ host -t SRV _kerberos._udp.example.com
+_kerberos._udp.example.com has SRV record 0 100 88 dc.example.com
+</programlisting>
+
+ <para>The server running Cockpit should have a fully qualified name that ends with
+ the domain name.</para>
+
+ <para>There must be a valid Kerberos host key for the server in the <code>/etc/krb5.keytab</code>
+ file. Alternatively, if you would like to use a different keytab, you can do so
+ by placing it in <code>/etc/cockpit/krb5.keytab</code>, or below <code>$XDG_CONFIG_DIRS</code>
+ if set (see <ulink url="./cockpit.conf.5.html">cockpit.conf</ulink>). It may be necessary to
+ create a kerberos service principal and update the keytab if it is not present.
+ Depending on your domain type different service names are required:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term>Active Directory</term>
+ <listitem><para><code>HOST/server.example.com@EXAMPLE.COM</code></para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>IPA and MIT</term>
+ <listitem><para><code>HTTP/server.example.com@EXAMPLE.COM</code></para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>When joining an IPA domain with Cockpit and the <code>ipa</code> command line tool is
+ available, both the service principal name and a <code>/etc/cockpit/krb5.keytab</code> get
+ created automatically, so that Kerberos based single sign on into Cockpit works out of the
+ box. If you want/need to do this by hand or in a script, first create or modify the
+ <code>HTTP/</code> service principal:</para>
+
+<programlisting>
+$ sudo ipa service-add --ok-as-delegate=true --ok-to-auth-as-delegate=true \
+ HTTP/server.example.com@EXAMPLE.COM
+# or, if it already exists, just enable delegation:
+$ sudo ipa service-mod --ok-as-delegate=true --ok-to-auth-as-delegate=true \
+ HTTP/server.example.com@EXAMPLE.COM
+</programlisting>
+
+ <para>Then generate a key for that principal:</para>
+
+<programlisting>
+$ sudo ipa-getkeytab -p HTTP/server.example.com@EXAMPLE.COM -k /etc/cockpit/krb5.keytab
+</programlisting>
+
+ <para>The following command can be used to list the <code>/etc/cockpit/krb5.keytab</code>:</para>
+
+<programlisting>
+$ sudo klist -k /etc/cockpit/krb5.keytab
+</programlisting>
+
+ <para>Lastly accounts from the domain must be resolvable to unix accounts on the server
+ running Cockpit. For example:</para>
+
+<programlisting>
+$ getent passwd user@example.com
+user@example.com:*:381001109:381000513:User Name:/home/user:/bin/sh
+</programlisting>
+
+ <para>If you wish to delegate your kerberos credentials to Cockpit, and allow Cockpit
+ to then connect to other machines using those credentials, you should enable delegation
+ for the hosts running Cockpit, and in some cases the <code>HTTP</code> service as well.
+ When joining an IPA domain, this is enabled by default.</para>
+
+ <para>Domain admins (usually the <code>admins@example.com</code> group) should normally
+ also be able to administer any joined machine. Enable sudo access for that group
+ with the following command on the IPA server, for version 4.7.1 and later:</para>
+
+<programlisting>
+ipa-advise enable-admins-sudo | sh -ex
+</programlisting>
+
+ <para>On earlier FreeIPA versions, run these commands instead, as a domain admin on
+ any joined machine:</para>
+
+<programlisting>
+ipa sudorule-add --hostcat=all --cmdcat=all All
+ipa sudorule-add-user --groups=admins All
+</programlisting>
+
+ <para>Note that this does not change security properties; domain admins can give this
+ privilege to themselves, so it is safe to enable by default.</para>
+ </section>
+
+ <section id="sso-client">
+ <title>Client Requirements</title>
+
+ <para>The client side, where your web browser is running, should have a valid kerberos
+ ticket in the current user session. A command like this will get one:</para>
+
+<programlisting>
+$ kinit user@EXAMPLE.COM
+Password for user@EXAMPLE.COM:
+</programlisting>
+
+ <para>In addition your browser must be usually be configured to allow kerberos
+ authentication for the domain.</para>
+
+ <variablelist>
+ <varlistentry>
+ <term>Mozilla Firefox</term>
+ <listitem><para>Go to <code>about:config</code> and set the
+ <code>network.negotiate-auth.trusted-uris</code> setting to your domain name
+ preceded by a dot, ie: <code>.example.com</code></para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Google Chrome</term>
+ <listitem><para>
+ On Linux: create the file
+ <code>/etc/opt/chrome/policies/managed/example-com.json</code>
+ with the contents:
+<programlisting>
+{
+ "AuthServerWhitelist": "*example.com"
+}
+</programlisting>
+ and restart the browser. On other platforms, exit your browser
+ completely, and start it with a command line like this:
+ <code>google-chrome --auth-server-whitelist=*example.com</code>
+ </para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ <para>Use a fully qualified server name (with the domain name at the end) to access
+ Cockpit in your web browser.</para>
+
+ <para>If you wish to connect from one server to another in Cockpit using kerberos SSO,
+ then you have to explicitly enable all sorts of things. For starters, make sure that
+ delegated credentials are allowed by your domain (see above). Next when requesting your
+ kerberos ticket make sure that forwardable tickets are requested:</para>
+
+<programlisting>
+$ kinit -f user@EXAMPLE.COM
+Password for user@EXAMPLE.COM:
+</programlisting>
+
+ <para>Make sure that the forwardable flag <code>F</code> is present in your ticket:</para>
+
+<programlisting>
+$ klist -f
+Ticket cache: KEYRING:persistent:1000:1000
+Default principal: user@EXAMPLE.COM
+
+Valid starting Expires Service principal
+18.03.2017 05:39:23 19.03.2017 05:39:20 krbtgt/EXAMPLE.COM@EXAMPLE.COM
+ Flags: FIA
+</programlisting>
+
+ <para>Lastly configure your browser to allow delegated, forwardable kerberos
+ credentials to be sent to Cockpit:</para>
+
+ <variablelist>
+ <varlistentry>
+ <term>Mozilla Firefox</term>
+ <listitem><para>Go to <code>about:config</code> and set the
+ <code>network.negotiate-auth.delegation-uris</code> setting to your domain name
+ preceded by a dot, ie: <code>.example.com</code></para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Google Chrome</term>
+ <listitem><para>
+ On Linux: create the file
+ <code>/etc/opt/chrome/policies/managed/example-com.json</code>
+ with the contents:
+<programlisting>
+{
+ "AuthServerWhitelist": "*example.com",
+ "AuthNegotiateDelegateWhitelist": "*example.com"
+}
+</programlisting>
+ and restart the browser. On other platforms, exit your browser
+ completely, and start it with a command line like this:
+ <code>google-chrome --auth-server-whitelist=*example.com --auth-negotiate-delegate-whitelist=*example.com</code>
+ </para></listitem>
+ </varlistentry>
+ </variablelist>
+
+ </section>
+
+</chapter>
diff --git a/doc/guide/startup.xml b/doc/guide/startup.xml
new file mode 100644
index 0000000..45226c5
--- /dev/null
+++ b/doc/guide/startup.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="startup">
+ <title>Start up</title>
+
+ <para>Cockpit's <code>cockpit-ws</code> component is what the browser connects to
+ and it typically starts on demand via
+ <ulink url="https://www.freedesktop.org/wiki/Software/systemd/"><code>systemd</code></ulink>
+ socket activation.</para>
+
+ <para>The actual <code>cockpit.service</code> and <code>cockpit-ws</code> process will start on
+ demand when a browser accesses the <code>cockpit.socket</code>,
+ <link linkend="listen">usually on port 9090</link>. Once a user logs in then
+ a <code>cockpit-bridge</code> process will be started in a Linux user login session.</para>
+
+ <para>Only systems that you connect to with your browser need to have the <code>cockpit.socket</code>
+ enabled. For systems that you add through host switcher the bridge is started
+ via SSH on demand.</para>
+
+ <section id="startup-shutdown">
+ <title>Process exit</title>
+ <para>The <code>cockpit-bridge</code> process will exit when the user logs out. In addition,
+ after 10 minutes of inactivity, the <code>cockpit-ws</code> process will exit on its own.
+ The browser will automatically disconnect if it fails to hear from the
+ <code>cockpit-ws</code> process for 30 seconds.</para>
+ </section>
+
+ <section id="startup-boot">
+ <title>Boot start up</title>
+
+ <para>To make Cockpit available by default after system boot the <code>cockpit.socket</code>
+ needs to be enabled:</para>
+
+<programlisting>
+$ sudo systemctl enable cockpit.socket
+</programlisting>
+
+ <para>If you wish to not have Cockpit available by default via a browser, then the
+ <code>cockpit.socket</code> should be disabled:</para>
+
+<programlisting>
+$ sudo systemctl disable cockpit.socket
+</programlisting>
+
+ </section>
+
+</chapter>
diff --git a/doc/guide/static/gtk-doc.css b/doc/guide/static/gtk-doc.css
new file mode 100644
index 0000000..d6e49e0
--- /dev/null
+++ b/doc/guide/static/gtk-doc.css
@@ -0,0 +1,278 @@
+.synopsis, .classsynopsis
+{
+ /* tango:aluminium 1/2 */
+ background: #eeeeec;
+ border: solid 1px #d3d7cf;
+ padding: 0.5em;
+}
+
+.programlisting
+{
+ /* tango:sky blue 0/1 */
+ background: #e6f3ff;
+ border: solid 1px #729fcf;
+ padding: 0.5em;
+}
+
+.variablelist
+{
+ padding: 4px;
+ margin-inline-start: 3em;
+}
+
+.variablelist td:first-child
+{
+ vertical-align: top;
+}
+
+@media screen {
+ sup a.footnote
+ {
+ position: relative;
+ inset-block-start: 0 ! important;
+ }
+ /* this is needed so that the local anchors are displayed below the naviagtion */
+ div.footnote a[name], div.refnamediv a[name], div.refsect1 a[name], div.refsect2 a[name], div.index a[name], div.glossary a[name], div.sect1 a[name]
+ {
+ display: inline-block;
+ position: relative;
+ inset-block-start:-5em;
+ }
+ /* this seems to be a bug in the xsl style sheets when generating indexes */
+ div.index div.index
+ {
+ inset-block-start: 0;
+ }
+ /* make space for the fixed navigation bar and add space at the bottom so that
+ * link targets appear somewhat close to top
+ */
+ body
+ {
+ padding-block: 3.2em 20em;
+ }
+ /* style and size the navigation bar */
+ table.navigation#top
+ {
+ position: fixed;
+ /* tango:scarlet red 0/1 */
+ background: #ffe6e6;
+ border: solid 1px #ef2929;
+ margin-block: 0;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ block-size: 3em;
+ z-index: 10;
+ }
+
+ .navigation a, .navigation a:visited
+ {
+ /* tango:scarlet red 3 */
+ color: #a40000;
+ }
+
+ .navigation a:hover
+ {
+ /* tango:scarlet red 1 */
+ color: #ef2929;
+ }
+
+ td.shortcuts
+ {
+ /* tango:scarlet red 1 */
+ color: #ef2929;
+ font-size: 80%;
+ white-space: nowrap;
+ }
+}
+@media print {
+ table.navigation {
+ visibility: collapse;
+ display: none;
+ }
+
+ div.titlepage table.navigation {
+ visibility: visible;
+ display: table;
+ /* tango:scarlet red 0/1 */
+ background: #ffe6e6;
+ border: solid 1px #ef2929;
+ margin-block: 0;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ block-size: 3em;
+ }
+}
+
+.navigation .title
+{
+ font-size: 200%;
+}
+
+div.gallery-float
+{
+ float: inline-start;
+ padding: 10px;
+}
+
+div.gallery-float img
+{
+ border-style: none;
+}
+
+div.gallery-spacer
+{
+ clear: both;
+}
+
+a, a:visited
+{
+ text-decoration: none;
+ /* tango:sky blue 2 */
+ color: #3465a4;
+}
+
+a:hover
+{
+ text-decoration: underline;
+ /* tango:sky blue 1 */
+ color: #729fcf;
+}
+
+div.table table
+{
+ border-collapse: collapse;
+ border-spacing: 0;
+ /* tango:aluminium 3 */
+ border: solid 1px #babdb6;
+}
+
+div.table table td, div.table table th
+{
+ /* tango:aluminium 3 */
+ border: solid 1px #babdb6;
+ padding: 3px;
+ vertical-align: top;
+}
+
+div.table table th
+{
+ /* tango:aluminium 2 */
+ background-color: #d3d7cf;
+}
+
+hr
+{
+ /* tango:aluminium 3 */
+ color: #babdb6;
+ background: #babdb6;
+ border: none 0;
+ block-size: 1px;
+ clear: both;
+}
+
+.footer
+{
+ padding-block-start: 3.5em;
+ /* tango:aluminium 3 */
+ color: #babdb6;
+ text-align: center;
+ font-size: 80%;
+}
+
+.warning
+{
+ /* tango:orange 0/1 */
+ background: #ffeed9;
+ border-color: #ffb04f;
+}
+
+.note
+{
+ /* tango:chameleon 0/0.5 */
+ background: #d8ffb2;
+ border-color: #abf562;
+}
+
+.note, .warning
+{
+ padding: 0.5em;
+ border-width: 1px;
+ border-style: solid;
+}
+
+.note h3, .warning h3
+{
+ margin-block-start: 0.0
+}
+
+.note p, .warning p
+{
+ margin-block-end: 0.0
+}
+
+/* blob links */
+h2 .extralinks, h3 .extralinks
+{
+ float: inline-end;
+ /* tango:aluminium 3 */
+ color: #babdb6;
+ font-size: 80%;
+ font-weight: normal;
+}
+
+.annotation
+{
+ /* tango:aluminium 5 */
+ color: #555753;
+ font-size: 80%;
+ font-weight: normal;
+}
+
+/* code listings */
+
+.listing_code .programlisting .cbracket { color: #a40000; } /* tango: scarlet red 3 */
+.listing_code .programlisting .comment { color: #a1a39d; } /* tango: aluminium 4 */
+.listing_code .programlisting .function { color: #000; font-weight: bold; }
+.listing_code .programlisting .function a { color: #11326b; font-weight: bold; } /* tango: sky blue 4 */
+.listing_code .programlisting .keyword { color: #4e9a06; } /* tango: chameleon 3 */
+.listing_code .programlisting .linenum { color: #babdb6; } /* tango: aluminium 3 */
+.listing_code .programlisting .normal { color: #000; }
+.listing_code .programlisting .number { color: #75507b; } /* tango: plum 2 */
+.listing_code .programlisting .preproc { color: #204a87; } /* tango: sky blue 3 */
+.listing_code .programlisting .string { color: #c17d11; } /* tango: chocolate 2 */
+.listing_code .programlisting .type { color: #000; }
+.listing_code .programlisting .type a { color: #11326b; } /* tango: sky blue 4 */
+.listing_code .programlisting .symbol { color: #ce5c00; } /* tango: orange 3 */
+
+.listing_frame {
+ /* tango:sky blue 1 */
+ border: solid 1px #729fcf;
+ padding: 0;
+}
+
+.listing_lines, .listing_code {
+ margin-block: 0;
+ padding: 0.5em;
+}
+
+.listing_lines {
+ /* tango:sky blue 0.5 */
+ background: #a6c5e3;
+ /* tango:aluminium 6 */
+ color: #2e3436;
+}
+
+.listing_code {
+ /* tango:sky blue 0 */
+ background: #e6f3ff;
+}
+
+.listing_code .programlisting {
+ /* override from previous */
+ border: none 0;
+ padding: 0;
+}
+
+.listing_lines pre, .listing_code pre {
+ margin: 0;
+}
diff --git a/doc/guide/static/home.png b/doc/guide/static/home.png
new file mode 100644
index 0000000..1700361
--- /dev/null
+++ b/doc/guide/static/home.png
Binary files differ
diff --git a/doc/guide/static/left.png b/doc/guide/static/left.png
new file mode 100644
index 0000000..2d05b3d
--- /dev/null
+++ b/doc/guide/static/left.png
Binary files differ
diff --git a/doc/guide/static/right.png b/doc/guide/static/right.png
new file mode 100644
index 0000000..92832e3
--- /dev/null
+++ b/doc/guide/static/right.png
Binary files differ
diff --git a/doc/guide/static/style.css b/doc/guide/static/style.css
new file mode 100644
index 0000000..8c4ddb7
--- /dev/null
+++ b/doc/guide/static/style.css
@@ -0,0 +1,227 @@
+@import "gtk-doc.css";
+
+/* keep this in sync with node_modules/@redhat/redhat-font/webfonts/red-hat-font.css */
+@font-face {
+ font-family: RedHatText;
+ src: url("./RedHatText-Regular.woff2") format("woff2");
+ /* Modern Browsers */
+ font-style: normal;
+ font-weight: 400;
+ text-rendering: optimizelegibility;
+}
+
+@font-face {
+ font-family: RedHatText;
+ src: url("./RedHatText-Medium.woff2") format("woff2");
+ /* Modern Browsers */
+ font-style: normal;
+ font-weight: 700;
+ text-rendering: optimizelegibility;
+}
+
+h1.guides {
+ background-color: #238b49;
+ color: white;
+ font-family: Georgia, "Times New Roman", Times, serif;
+ font-size: 30pt;
+ padding-block: 5px;
+ padding-inline: 20px 0;
+}
+
+h2.guides {
+ margin-inline-start: 20px;
+}
+
+div.guides {
+ margin-inline: 20px;
+ font-family: RedHatText, Verdana, Arial, 'Bitstream Vera Sans', Helvetica, sans-serif;
+ font-size: 11pt;
+}
+
+div.guides > p {
+ margin-inline-start: 20px;
+}
+
+div.guides > a {
+ margin-inline: 40px 20px;
+ font-size: 12px;
+ line-height: 200%;
+ display: block;
+}
+
+table.navigation {
+ background-color: #238b49 !important;
+ border-width: 0 !important;
+ color: white;
+ font-family: Georgia, "Times New Roman", Times, serif;
+}
+
+table.navigation th {
+ font-size: 30pt !important;
+ font-weight: normal;
+ text-align: start !important;
+ padding-inline-start: 10pt;
+}
+
+table.navigation th:first-child {
+ padding-block: 5px;
+ padding-inline: 20px 0;
+}
+
+.shortcuts {
+ color: white !important;
+}
+
+.shortcuts a {
+ color: white !important;
+ font-family: RedHatText, Verdana, Arial, 'Bitstream Vera Sans', Helvetica, sans-serif;
+}
+
+p.title {
+ font-size: 30pt !important;
+}
+
+div.warning {
+ padding: 15px;
+}
+
+.warning .title {
+ font-weight: bold !important;
+ inset-inline-start: 0;
+}
+
+body {
+ padding-block-start: 5.5em !important;
+ margin: 0;
+}
+
+
+/* Target all Firefox, since firefox has bug wrt TABLE + position: fixed */
+@-moz-document url-prefix() {
+ table.navigation { position: static !important; }
+ body { padding-block-start: 0 !important; }
+}
+
+p.releaseinfo {
+ margin: 1em;
+}
+
+div.toc {
+ margin: 1em;
+}
+
+div.book,
+div.refentry,
+div.chapter,
+div.article,
+div.reference,
+div.index,
+div.footer,
+div.section,
+div.part {
+ font-family: RedHatText, Verdana, Arial, 'Bitstream Vera Sans', Helvetica, sans-serif;
+ font-size: 11pt;
+ line-height: 160%;
+}
+
+code {
+ font-size: 10.5pt;
+}
+
+body > div.footer,
+body > div.part {
+ margin-inline: 1em;
+}
+
+body > div.refentry,
+body > div.chapter,
+body > div.article,
+body > div.reference,
+body > div.index,
+body > div.section,
+body > div.part,
+body > div.book > div.toc {
+ padding-block-start: 50px;
+ max-inline-size: 800px;
+ padding-inline: 40px 20px;
+ margin-inline: auto;
+ /*
+ margin-left: 3em;
+ margin-right: 1em;
+ */
+}
+
+body > div.book > div.toc {
+ padding-block-start: 20px;
+}
+
+div.section {
+ margin-block-start: 3em;
+}
+
+pre.programlisting {
+ font-size: 9.5pt;
+ line-height: 130%;
+ padding-inline-start: 10px;
+}
+
+div.variablelist table {
+ font-size: 13pt;
+ line-height: 150%;
+ margin-inline-start: 0;
+}
+
+div.variablelist table span.term {
+ padding-inline-end: 1em;
+}
+
+div.variablelist {
+ margin-inline-start: 0;
+}
+
+div.refsect1,
+div.refsect2,
+div.refsection,
+div.refnamediv,
+div.refsynopsisdiv {
+ margin-block-end: 2em !important;
+}
+
+div.refsection > h3 {
+ margin-block-start: 1.5em !important;
+}
+
+div.abstract > .title {
+ margin: 0.3em;
+ visibility: hidden;
+}
+
+h1 {
+ font-weight: normal !important;
+}
+
+h2 {
+ position: relative;
+ inset-inline-start: -1em;
+ font-weight: normal !important;
+}
+
+h3 {
+ position: relative;
+ inset-inline-start: -1em;
+ font-weight: normal !important;
+}
+
+dd > dl {
+ margin-block: 0.3em;
+}
+
+pre.screen {
+ border: solid 1px #729fcf;
+ padding: 0.5em;
+ background: #e6f3ff;
+}
+
+code.option {
+ white-space: nowrap;
+}
diff --git a/doc/guide/static/up.png b/doc/guide/static/up.png
new file mode 100644
index 0000000..85b3e2a
--- /dev/null
+++ b/doc/guide/static/up.png
Binary files differ
diff --git a/doc/guide/urls.xml b/doc/guide/urls.xml
new file mode 100644
index 0000000..f5f6770
--- /dev/null
+++ b/doc/guide/urls.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
+<chapter id="urls">
+ <title>Cockpit URLs</title>
+
+ <para>Cockpit URLs follow a specific structure, related to the components they are
+ loading. Various components are loaded in <code>&lt;iframe&gt;</code> tags. The
+ URLs for these components are described first. Further down below you can
+ find information about the top level bookmarkable Cockpit address URLs.</para>
+
+ <section id="urls-components">
+ <title>Component URLs</title>
+
+ <para>Cockpit components are HTML documents. They are organized into
+ <link linkend="packages">packages</link>. Each package contains information about
+ which HTML components are available in that package. Components should always use
+ relative URLs to access resources, such as images, scripts or CSS files, even
+ if they refer to a resource in another package.</para>
+
+ <para>The following are valid component URLs, each bit will be discussed
+ below:</para>
+
+<programlisting>
+/cockpit/@localhost/package/component.html#/hash
+/cockpit/$checksum/package/component.html#/hash
+/cockpit/@server.example.com/package/component.html#/hash
+/cockpit+embedder/@localhost/package/component.html#/hash
+</programlisting>
+
+ <para>All resource URLs are under the <code>/cockpit</code> namespace. In cases
+ where a Cockpit component is being <link linkend="embedding">embedded</link>
+ the <code>/cockpit</code> may be followed by a plus sign and another
+ <code>embedder</code> specific identifier.</para>
+
+ <para>What follows is either a <code>@host</code> or <code>$checksum</code> which
+ tells cockpit where to <link linkend="packages">find the package</link>.
+ Checksums are used when more than one host has identical packages and the resources
+ can be cached.</para>
+
+ <para>The <code>package</code> name is next, followed by the <code>component</code>
+ HTML path inside that package. And lastly a hash allows for navigation within a
+ single component. The hash should follow a URL path and/or query string form.</para>
+
+ <warning>
+ <para>Never assume that the <code>@host</code> or <code>$checksum</code> portion
+ is predictable. Only refer to resources in packages on the same host.</para>
+ </warning>
+ </section>
+
+ <section id="urls-visible">
+ <title>Visible URLs</title>
+
+ <para>The above Component URLs are usually not visible to the user. Instead the
+ Cockpit Web Service wraps the components in a shell which allows navigation, and
+ provides bookmarkable clean URLs to the component. These URLs do not affect
+ <link linkend="embedding">embedders</link> or
+ <link linkend="packages">components</link> directly.</para>
+
+ <para>If no path is present then the Cockpit will redirect to the default page for the server.</para>
+
+ <para>If the first segment of the path begins with an <code>@</code> sign, then
+ the component is being shown from a non-local host.</para>
+
+ <para>The next segment of the path, (or first if the component is being shown on
+ the local host) is the <link linkend="packages">package name</link>.
+ The remainder of the path is a component file in the package. If no further path
+ segments are present, a default <code>index.html</code> component in the package
+ is loaded. An extension of <filename>.html</filename> is automatically
+ appended.</para>
+
+ <para>The hash portion of the path is automatically transferred to the component
+ as the hash of its <link linkend="urls-components">resource URL</link>.</para>
+ </section>
+
+</chapter>
diff --git a/doc/guide/version-greater-or-equal.xsl b/doc/guide/version-greater-or-equal.xsl
new file mode 100644
index 0000000..8edcd81
--- /dev/null
+++ b/doc/guide/version-greater-or-equal.xsl
@@ -0,0 +1,54 @@
+<?xml version='1.0'?> <!--*- mode: xml -*-->
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ version="1.0"
+ xmlns="http://www.w3.org/TR/xhtml1/transitional"
+ exclude-result-prefixes="#default">
+ <xsl:template name="version-greater-or-equal">
+ <xsl:param name="ver1"></xsl:param>
+ <xsl:param name="ver2"></xsl:param>
+ <xsl:variable name="vp1">
+ <xsl:choose>
+ <xsl:when test="contains($ver1, '.')">
+ <xsl:value-of select="substring-before($ver1, '.')"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="$ver1"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+ <xsl:variable name="vp2">
+ <xsl:choose>
+ <xsl:when test="contains($ver2, '.')">
+ <xsl:value-of select="substring-before($ver2, '.')"/>
+ </xsl:when>
+ <xsl:otherwise>
+ <xsl:value-of select="$ver2"/>
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:variable>
+ <xsl:choose>
+ <xsl:when test="$vp1 &gt; $vp2">
+ 1
+ </xsl:when>
+ <xsl:when test="$vp1 &lt; $vp2">
+ 0
+ </xsl:when>
+ <xsl:when test="$vp1 = $vp2">
+ <xsl:choose>
+ <xsl:when test="contains($ver1, '.') and contains($ver2, '.')">
+ <xsl:call-template name="version-greater-or-equal">
+ <xsl:with-param name="ver1" select="substring-after($ver1, '.')"/>
+ <xsl:with-param name="ver2" select="substring-after($ver2, '.')"/>
+ </xsl:call-template>
+ </xsl:when>
+ <xsl:when test="contains($ver2, '.')">
+ 0
+ </xsl:when>
+ <xsl:otherwise>
+ 1
+ </xsl:otherwise>
+ </xsl:choose>
+ </xsl:when>
+ </xsl:choose>
+</xsl:template>
+</xsl:stylesheet>
diff --git a/doc/guide/version.in b/doc/guide/version.in
new file mode 100644
index 0000000..27323da
--- /dev/null
+++ b/doc/guide/version.in
@@ -0,0 +1 @@
+@VERSION@ \ No newline at end of file
diff --git a/doc/man/Makefile-man.am b/doc/man/Makefile-man.am
new file mode 100644
index 0000000..7bc0cdb
--- /dev/null
+++ b/doc/man/Makefile-man.am
@@ -0,0 +1,36 @@
+EXTRA_DIST += \
+ doc/man/cockpit.xml \
+ doc/man/cockpit-bridge.xml \
+ doc/man/cockpit-ws.xml \
+ doc/man/cockpit-tls.xml \
+ doc/man/cockpit-desktop.xml \
+ doc/man/cockpit.conf.xml \
+ doc/man/pam_ssh_add.xml \
+ $(NULL)
+
+if ENABLE_DOC
+
+MANPAGES = \
+ doc/man/cockpit.1 \
+ doc/man/cockpit-bridge.1 \
+ doc/man/cockpit-desktop.1 \
+ doc/man/cockpit-ws.8 \
+ doc/man/cockpit-tls.8 \
+ doc/man/cockpit.conf.5 \
+ doc/man/pam_ssh_add.8 \
+ $(NULL)
+
+man_MANS += $(MANPAGES)
+CLEANFILES += $(MANPAGES)
+
+MAN_PROC = mkdir -p doc/man/ && $(XSLTPROC) -nonet --param man.output.quietly 1 --output $@ \
+ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $<
+
+%.8: %.xml
+ $(AM_V_GEN) $(MAN_PROC)
+%.1: %.xml
+ $(AM_V_GEN) $(MAN_PROC)
+%.5: %.xml
+ $(AM_V_GEN) $(MAN_PROC)
+
+endif ENABLE_DOC
diff --git a/doc/man/cockpit-bridge.xml b/doc/man/cockpit-bridge.xml
new file mode 100644
index 0000000..0d31f1d
--- /dev/null
+++ b/doc/man/cockpit-bridge.xml
@@ -0,0 +1,116 @@
+<refentry id="cockpit-bridge.1">
+
+ <!--
+ This file is part of Cockpit.
+
+ Copyright (C) 2014 Red Hat, Inc.
+
+ Cockpit is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ Cockpit is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+ <refentryinfo>
+ <title>cockpit-bridge</title>
+ <productname>cockpit</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>cockpit-bridge</refentrytitle>
+ <manvolnum>1</manvolnum>
+ <refmiscinfo class="version"></refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>cockpit-bridge</refname>
+ <refpurpose>Cockpit Host Bridge</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>cockpit-bridge</command>
+ <arg><option>--help</option></arg>
+ <arg><option>--packages</option></arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+
+ <refsect1 id="cockpit-bridge-description">
+ <title>DESCRIPTION</title>
+ <para>The <command>cockpit-bridge</command> program is used by Cockpit to
+ relay messages and commands from the Web front end to the server. Among
+ other things it relays DBus, and spawns processes on behalf of the
+ Web user interface.</para>
+ <para>This program is not routinely run by users or administrators. It
+ is in the <code>$PATH</code> so that Cockpit can find it when connecting
+ between hosts. However there are some diagnostics available when running
+ from the command line.</para>
+ </refsect1>
+
+ <refsect1 id="cockpit-bridge-options">
+ <title>OPTIONS</title>
+ <variablelist>
+ <varlistentry>
+ <term><option>--help</option></term>
+ <listitem>
+ <para>
+ Show help options.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--interact=boundary</option></term>
+ <listitem>
+ <para>Interact with the raw cockpit1 protocol. Useful for debugging and testing.
+ Specify a <option>boundary</option> which should be on an empty line between
+ messages.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--packages</option></term>
+ <listitem>
+ <para>List all available Cockpit packages and exit. Note this includes packages
+ available to the user running this command.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--version</option></term>
+ <listitem>
+ <para>Show Cockpit version information.</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="cockpit-bridge-bugs">
+ <title>BUGS</title>
+ <para>
+ Please send bug reports to either the distribution bug tracker or the
+ <ulink url="https://github.com/cockpit-project/cockpit/issues/new">upstream bug tracker</ulink>.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-bridge-author">
+ <title>AUTHOR</title>
+ <para>Cockpit has been written by many
+ <ulink url="https://github.com/cockpit-project/cockpit/">contributors</ulink>.</para>
+ </refsect1>
+
+ <refsect1 id="cockpit-bridge-also">
+ <title>SEE ALSO</title>
+ <para>
+ <citerefentry>
+ <refentrytitle>cockpit-ws</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>
+ </para>
+ </refsect1>
+</refentry>
diff --git a/doc/man/cockpit-desktop.xml b/doc/man/cockpit-desktop.xml
new file mode 100644
index 0000000..eb3b4ab
--- /dev/null
+++ b/doc/man/cockpit-desktop.xml
@@ -0,0 +1,124 @@
+<refentry id="cockpit-desktop.1">
+
+ <!--
+ This file is part of Cockpit.
+
+ Copyright (C) 2018 Red Hat, Inc.
+
+ Cockpit is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ Cockpit is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+ <refentryinfo>
+ <title>cockpit-desktop</title>
+ <productname>cockpit</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>cockpit-desktop</refentrytitle>
+ <manvolnum>1</manvolnum>
+ <refmiscinfo class="version"></refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>cockpit-desktop</refname>
+ <refpurpose>Cockpit Desktop integration</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>cockpit-desktop</command>
+ <arg choice="req">URLPATH</arg>
+ <arg choice="opt">SSH_HOST</arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+
+ <refsect1 id="cockpit-desktop-description">
+ <title>DESCRIPTION</title>
+ <para>
+ The <command>cockpit-desktop</command> program provides secure access to
+ Cockpit pages in an already running desktop session. It starts a web server
+ (<command>cockpit-ws</command>) and a web browser in an isolated network
+ namespace, and a
+ <citerefentry>
+ <refentrytitle>cockpit-bridge</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>
+ in the running user session.
+ </para>
+ <para>
+ This avoids having to log into Cockpit, and having to enable
+ <filename>cockpit.socket</filename> system-wide. The network isolation
+ ensures that no other user, and not even other processes in the user's
+ session, can access this local web server.
+ </para>
+
+ <para>
+ <literal>URLPATH</literal> is the Cockpit page to open, i. e. the path
+ component of Cockpit URLs. It is highly recommended to only open a
+ <ulink url="https://cockpit-project.org/guide/latest/embedding.html">particular page frame</ulink>,
+ not the entire Cockpit navigation and menu. For example, the path
+ <literal>/cockpit/@localhost/storage/index.html</literal> will open the Storage
+ page. It is also possible to give abbreviated forms of urls, such as "<literal>storage</literal>"
+ or "<literal>network/firewall</literal>".
+ </para>
+
+ <para>
+ <literal>SSH_HOST</literal> is an optional SSH remote host specification
+ (<code>hostname</code> or <code>username@hostname</code>). If given,
+ <literal>cockpit-bridge</literal> will be started on the remote host through
+ <citerefentry>
+ <refentrytitle>ssh</refentrytitle><manvolnum>1</manvolnum>
+ </citerefentry>
+ instead, i. e. the Cockpit web browser will show that remote host.
+ Note that this is more of an experimental/demo feature.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-desktop-environment">
+ <title>ENVIRONMENT</title>
+ <para>
+ The <literal>BROWSER </literal> environment variable specifies
+ the browser command (and possibly options) that will be used to open the
+ requested Cockpit page. If not set, <command>cockpit-desktop</command> attempts to use
+ an internal minimalistic WebKit browser, and failing that, will attempt to detect some
+ reasonable alternatives.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-desktop-bugs">
+ <title>BUGS</title>
+ <para>
+ Please send bug reports to either the distribution bug tracker or the
+ <ulink url="https://github.com/cockpit-project/cockpit/issues/new">upstream bug tracker</ulink>.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-desktop-author">
+ <title>AUTHOR</title>
+ <para>Cockpit has been written by many
+ <ulink url="https://github.com/cockpit-project/cockpit/">contributors</ulink>.</para>
+ </refsect1>
+
+ <refsect1 id="cockpit-desktop-also">
+ <title>SEE ALSO</title>
+ <para>
+ <citerefentry>
+ <refentrytitle>cockpit-ws</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>,
+ <citerefentry>
+ <refentrytitle>cockpit-bridge</refentrytitle><manvolnum>1</manvolnum>
+ </citerefentry>
+ </para>
+ </refsect1>
+</refentry>
diff --git a/doc/man/cockpit-tls.xml b/doc/man/cockpit-tls.xml
new file mode 100644
index 0000000..b1b19d6
--- /dev/null
+++ b/doc/man/cockpit-tls.xml
@@ -0,0 +1,209 @@
+<refentry id="cockpit-tls.8">
+
+ <!--
+ This file is part of Cockpit.
+
+ Copyright (C) 2019 Red Hat, Inc.
+
+ Cockpit is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ Cockpit is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+ <refentryinfo>
+ <title>cockpit-tls</title>
+ <productname>cockpit</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>cockpit-tls</refentrytitle>
+ <manvolnum>8</manvolnum>
+ <refmiscinfo class="version"></refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>cockpit-tls</refname>
+ <refpurpose>TLS proxy for Cockpit web service</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>cockpit-tls</command>
+ <arg><option>--help</option></arg>
+ <arg><option>--port</option> <replaceable>PORT</replaceable></arg>
+ <arg><option>--no-tls</option></arg>
+ <arg><option>--idle-timeout</option> <replaceable>SECONDS</replaceable></arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+
+ <refsect1 id="cockpit-tls-description">
+ <title>DESCRIPTION</title>
+ <para>
+ The <command>cockpit-tls</command> program is a TLS terminating HTTP proxy for
+ <citerefentry>
+ <refentrytitle>cockpit-ws</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>. It manages a set of isolated cockpit-ws instances, one per TLS
+ client certificate, plus one for TLS without a client certificate, and one for
+ unencrypted HTTP. With that, one session cannot tamper with another one through
+ possible security vulnerability exploits.
+ </para>
+ <para>
+ Users or administrators should never need to start this program
+ as it automatically started by
+ <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+ via socket activation.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-tls-transport">
+ <title>TRANSPORT SECURITY</title>
+ <para>
+ To specify the TLS certificate the web service should use, simply
+ drop a file with the extension <literal>.cert</literal> in the
+ <filename>/etc/cockpit/ws-certs.d</filename> directory, or below <code>$XDG_CONFIG_DIRS</code>
+ if set (see <ulink url="./cockpit.conf.5.html">cockpit.conf</ulink>). If there are
+ multiple files in this directory, then the highest priority one
+ is chosen after sorting.</para>
+ <para>The <literal>.cert</literal> file should contain at least two
+ OpenSSL style PEM blocks. First one or more <literal>BEGIN CERTIFICATE</literal>
+ blocks for the server certificate and intermediate certificate authorities
+ and a second one containing a <literal>BEGIN PRIVATE KEY</literal> or similar.
+ The key must not be encrypted.</para>
+ <para>If there is no TLS certificate, a self-signed certificate is
+ automatically generated using <command>sscg</command> (if available) or
+ <command>openssl</command> and stored in
+ the <filename>0-self-signed.cert</filename> file.</para>
+ <para>When enrolling into a FreeIPA domain, an SSL certificate is requested from
+ the IPA server and stored in <filename>10-ipa.cert</filename>.</para>
+
+ <para>To check which certificate <command>cockpit-ws</command> will use,
+ run the following command.</para>
+
+<programlisting>
+$ sudo /usr/libexec/cockpit-certificate-ensure --check
+</programlisting>
+ <para>Or, on Debian-based systems:</para>
+<programlisting>
+$ sudo /usr/lib/cockpit/cockpit-certificate-ensure --check
+</programlisting>
+
+ <para>If using <literal>certmonger</literal> to manage certificates, following command can
+ be used to generate a certificate/key pair:</para>
+<programlisting>
+CERT_FILE=/etc/cockpit/ws-certs.d/50-certmonger.crt
+KEY_FILE=/etc/cockpit/ws-certs.d/50-certmonger.key
+
+getcert request -f ${CERT_FILE} -k ${KEY_FILE} -D $(hostname --fqdn)
+</programlisting>
+
+ </refsect1>
+
+ <refsect1 id="cockpit-tls-options">
+ <title>OPTIONS</title>
+ <variablelist>
+ <varlistentry>
+ <term><option>--help</option></term>
+ <listitem>
+ <para>
+ Show help options.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--port</option> <replaceable>PORT</replaceable></term>
+ <listitem>
+ <para>
+ Serve HTTP requests on <replaceable>PORT</replaceable> instead of port 9090.
+ Usually Cockpit is started on demand by <command>systemd</command> socket
+ activation, and this option has no effect. Update the
+ <literal>ListenStream</literal> directive <filename>cockpit.socket</filename>
+ file in the usual <command>systemd</command> manner.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--no-tls</option></term>
+ <listitem>
+ <para>
+ Don't use TLS. Certificates will not be read, and https connections denied. Then
+ <command>cockpit-tls</command> will only manage a single cockpit-ws instance, and
+ thus not do anything different than running <command>cockpit-ws --no-tls</command>
+ directly. Only use this for debugging or testing.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--idle-timeout</option> <replaceable>SECONDS</replaceable></term>
+ <listitem>
+ <para>
+ If greater than 0, exit if no connections have happened for the given number of
+ seconds, i. e. the server is idle. If not given, the default is 90.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="cockpit-tls-environment">
+ <title>ENVIRONMENT</title>
+ <para>
+ The <command>cockpit-tls</command> program expects the <literal>RUNTIME_DIRECTORY</literal>
+ environment variable to be set to an empty directory (preferably in <literal>/run/</literal>)
+ that is only accessible by the system user under which it is running. This contains
+ the Unix sockets for communicating with the <command>cockpit-ws</command> instances,
+ and in the future, state information about client certificates.
+ This variable is normally set by the <literal>cockpit.service</literal> systemd unit.
+ </para>
+ <para>
+ In addition, <command>cockpit-tls</command> will use the <literal>XDG_CONFIG_DIRS</literal>
+ environment variable from the
+ <ulink url="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG
+ basedir spec</ulink> to find its certificates and the
+ <citerefentry>
+ <refentrytitle>cockpit.conf</refentrytitle><manvolnum>5</manvolnum>
+ </citerefentry>
+ configuration file.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-tls-bugs">
+ <title>BUGS</title>
+ <para>
+ Please send bug reports to either the distribution bug tracker or the
+ <ulink url="https://github.com/cockpit-project/cockpit/issues/new">upstream bug tracker</ulink>.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-tls-author">
+ <title>AUTHOR</title>
+ <para>Cockpit has been written by many
+ <ulink url="https://github.com/cockpit-project/cockpit/graphs/contributors">contributors</ulink>.</para>
+ </refsect1>
+
+ <refsect1 id="cockpit-tls-also">
+ <title>SEE ALSO</title>
+ <para>
+ <citerefentry>
+ <refentrytitle>cockpit-ws</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>
+ ,
+ <citerefentry>
+ <refentrytitle>cockpit.conf</refentrytitle><manvolnum>5</manvolnum>
+ </citerefentry>
+ ,
+ <citerefentry>
+ <refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum>
+ </citerefentry>
+ </para>
+ </refsect1>
+</refentry>
diff --git a/doc/man/cockpit-ws.xml b/doc/man/cockpit-ws.xml
new file mode 100644
index 0000000..0031d2d
--- /dev/null
+++ b/doc/man/cockpit-ws.xml
@@ -0,0 +1,243 @@
+<refentry id="cockpit-ws.8">
+
+ <!--
+ This file is part of Cockpit.
+
+ Copyright (C) 2013 Red Hat, Inc.
+
+ Cockpit is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ Cockpit is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+ <refentryinfo>
+ <title>cockpit-ws</title>
+ <productname>cockpit</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>cockpit-ws</refentrytitle>
+ <manvolnum>8</manvolnum>
+ <refmiscinfo class="version"></refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>cockpit-ws</refname>
+ <refpurpose>Cockpit web service</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>cockpit-ws</command>
+ <arg><option>--help</option></arg>
+ <arg><option>--port</option> <replaceable>PORT</replaceable></arg>
+ <arg><option>--address</option> <replaceable>ADDRESS</replaceable></arg>
+ <arg><option>--no-tls</option></arg>
+ <arg><option>--for-tls-proxy</option></arg>
+ <arg><option>--local-ssh</option></arg>
+ <arg><option>--local-session</option> <replaceable>BRIDGE</replaceable></arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+
+ <refsect1 id="cockpit-ws-description">
+ <title>DESCRIPTION</title>
+ <para>
+ The <command>cockpit-ws</command> program is the web service
+ component used for communication between the browser application
+ and various configuration tools and services like
+ <citerefentry>
+ <refentrytitle>cockpit-bridge</refentrytitle><manvolnum>1</manvolnum>
+ </citerefentry>.
+ </para>
+ <para>
+ Users or administrators should never need to start this program
+ as it automatically started by
+ <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+ on bootup, through
+ <citerefentry><refentrytitle>cockpit-tls</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-ws-transport">
+ <title>TRANSPORT SECURITY</title>
+ <para>
+ <command>cockpit-ws</command> is normally run behind the <command>cockpit-tls</command>
+ TLS terminating proxy, and only deals with unencrypted HTTP by itself. But for backwards
+ compatibility it can also handle TLS connections by itself when being run directly.
+ For details how to configure certificates, please refer to the
+ <citerefentry><refentrytitle>cockpit-tls</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+ documentation.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-ws-timeout">
+ <title>TIMEOUT</title>
+ <para>
+ When started via
+ <citerefentry>
+ <refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum>
+ </citerefentry>
+ then <command>cockpit-ws</command> will exit after 90 seconds
+ if nobody logs in, or after the last user is disconnected.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-ws-options">
+ <title>OPTIONS</title>
+ <variablelist>
+ <varlistentry>
+ <term><option>--help</option></term>
+ <listitem>
+ <para>
+ Show help options.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--port</option> <replaceable>PORT</replaceable></term>
+ <listitem>
+ <para>
+ Serve HTTP requests <replaceable>PORT</replaceable> instead of port 9090.
+ Usually Cockpit is started on demand by <command>systemd</command> socket
+ activation, and this option has no effect. Update the
+ <literal>ListenStream</literal> directive <filename>cockpit.socket</filename>
+ file in the usual <command>systemd</command> manner.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--address</option> <replaceable>ADDRESS</replaceable></term>
+ <listitem>
+ <para>
+ Bind to address <replaceable>ADDRESS</replaceable> instead of binding to
+ all available addresses. Usually Cockpit is started on demand by
+ <command>systemd</command> socket activation, and this option has no effect.
+ In that case, update the <literal>ListenStream</literal> directive in the
+ <filename>cockpit.socket</filename> file in the usual <command>systemd</command> manner.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--no-tls</option></term>
+ <listitem>
+ <para>
+ Don't use TLS.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--for-tls-proxy</option></term>
+ <listitem>
+ <para>
+ Tell <command>cockpit-ws</command> that it is running behind a local reverse proxy that
+ does the TLS termination. Then Cockpit puts https:// URLs into the default
+ <literal>Content-Security-Policy</literal>, and accepts only https:// origins, instead of
+ http: ones by default. However, if <literal>Origins</literal> is set in the
+ <citerefentry>
+ <refentrytitle>cockpit.conf</refentrytitle><manvolnum>5</manvolnum>
+ </citerefentry>
+ configuration file, it will override this default.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--local-ssh</option></term>
+ <listitem>
+ <para>
+ Normally <command>cockpit-ws</command> uses
+ <command>cockpit-session</command> and PAM to authenticate the user and start a
+ user session. With this option enabled, it will instead authenticate via SSH at
+ <literal>127.0.0.1</literal> port <literal>22</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--local-session</option> <replaceable>BRIDGE</replaceable></term>
+ <listitem>
+ <para>
+ Skip all authentication and <command>cockpit-session</command>, and launch the
+ <command>cockpit-bridge</command> specified in <replaceable>BRIDGE</replaceable> in
+ the local session. If the <replaceable>BRIDGE</replaceable> is specified as
+ <command>-</command> then expect an already running bridge that is connected to
+ stdin and stdout of this <command>cockpit-ws</command> process. This allows the
+ web server to run as any unprivileged user in an already running session.
+ </para>
+ <para>
+ This mode implies <literal>--no-tls</literal>, thus you need to use http://
+ URLs with this.
+ </para>
+ <warning>
+ <para>
+ If you use this, you <emphasis>have to isolate the opened TCP port</emphasis>
+ somehow (for example in a network namespace), otherwise all other users (or
+ even remote machines if the port is not just listening on localhost) can access
+ the session!
+ </para>
+ </warning>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="cockpit-ws-environment">
+ <title>ENVIRONMENT</title>
+ <para>
+ The <command>cockpit-ws</command> process will use the <literal>XDG_CONFIG_DIRS</literal>
+ environment variable from the
+ <ulink url="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG
+ basedir spec</ulink> to find its
+ <citerefentry>
+ <refentrytitle>cockpit.conf</refentrytitle><manvolnum>5</manvolnum>
+ </citerefentry>
+ configuration file.
+ </para>
+ <para>
+ In addition the <literal>XDG_DATA_DIRS</literal> environment variable from the
+ <ulink url="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG
+ basedir spec</ulink>
+ can be used to override the location to serve static files from. These are the files that
+ are served to a non-logged in user.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-ws-bugs">
+ <title>BUGS</title>
+ <para>
+ Please send bug reports to either the distribution bug tracker or the
+ <ulink url="https://github.com/cockpit-project/cockpit/issues/new">upstream bug tracker</ulink>.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-ws-author">
+ <title>AUTHOR</title>
+ <para>Cockpit has been written by many
+ <ulink url="https://github.com/cockpit-project/cockpit/">contributors</ulink>.</para>
+ </refsect1>
+
+ <refsect1 id="cockpit-ws-also">
+ <title>SEE ALSO</title>
+ <para>
+ <citerefentry>
+ <refentrytitle>cockpit-tls</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>
+ ,
+ <citerefentry>
+ <refentrytitle>cockpit.conf</refentrytitle><manvolnum>5</manvolnum>
+ </citerefentry>
+ ,
+ <citerefentry>
+ <refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum>
+ </citerefentry>
+ </para>
+ </refsect1>
+</refentry>
diff --git a/doc/man/cockpit.conf.xml b/doc/man/cockpit.conf.xml
new file mode 100644
index 0000000..f272571
--- /dev/null
+++ b/doc/man/cockpit.conf.xml
@@ -0,0 +1,285 @@
+<refentry id="cockpit.conf.5">
+
+ <!--
+ This file is part of Cockpit.
+
+ Copyright (C) 2013 Red Hat, Inc.
+
+ Cockpit is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ Cockpit is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+ <refentryinfo>
+ <title>cockpit.conf</title>
+ <productname>cockpit</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>cockpit.conf</refentrytitle>
+ <manvolnum>5</manvolnum>
+ <refmiscinfo class="version"></refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>cockpit.conf</refname>
+ <refpurpose>Cockpit configuration file</refpurpose>
+ </refnamediv>
+
+ <refsect1 id="cockpit-conf-description">
+ <title>DESCRIPTION</title>
+ <para>
+ Cockpit can be configured via /etc/cockpit/cockpit.conf. If <code>$XDG_CONFIG_DIRS</code>
+ is set, then the first path containing a <filename>../cockpit/cockpit.conf</filename> is used
+ instead. Other configuration files and directories are searched for in the same way.</para>
+
+ <para>
+ This file is not required and may need to be created manually. The file has a INI file
+ syntax and thus contains key / value pairs, grouped into topical groups. See the examples
+ below for details. </para>
+
+ <para>Note: The port that cockpit listens on cannot be changed in this file. To change
+ the port change the systemd <filename>cockpit.socket</filename> file.</para>
+ </refsect1>
+
+ <refsect1 id="cockpit-conf-webservice">
+ <title>WebService</title>
+ <variablelist>
+ <varlistentry>
+ <term><option>Origins</option></term>
+ <listitem>
+ <para>By default cockpit will not accept crossdomain websocket connections. Use this
+ setting to allow access from alternate domains. Origins should include scheme, host
+ and port, if necessary.</para>
+
+ <informalexample>
+<programlisting language="ini">
+[WebService]
+Origins = https://somedomain1.com https://somedomain2.com:9090
+</programlisting>
+ </informalexample>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>ProtocolHeader</option></term>
+ <listitem>
+ <para>Configure cockpit to look at the contents of this header to determine if a connection
+ is using tls. This should only be used when cockpit is behind a reverse proxy, and care
+ should be taken to make sure that incoming requests cannot set this header.</para>
+ <informalexample>
+<programlisting language="ini">
+[WebService]
+ProtocolHeader = X-Forwarded-Proto
+</programlisting>
+ </informalexample>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>ForwardedForHeader</option></term>
+ <listitem>
+ <para>Configure cockpit to look at the contents of this header to determine the real origin of a
+ connection. This should only be used when cockpit is behind a reverse proxy, and care
+ should be taken to make sure that incoming requests cannot set this header.</para>
+ <informalexample>
+<programlisting language="ini">
+[WebService]
+ForwardedForHeader = X-Forwarded-For
+</programlisting>
+ </informalexample>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>LoginTitle</option></term>
+ <listitem><para>Set the browser title for the login screen.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>LoginTo</option></term>
+ <listitem>
+ <para>When set to <literal>true</literal> the <emphasis>Connect to</emphasis> option
+ on the login screen is visible and allows logging into another server. When set to
+ <literal>false</literal>, direct remote logins are disallowed. If this option is not specified
+ then it will be automatically detected based on whether the
+ <command>cockpit-ssh</command> process is available or not.</para>
+
+ <para>If cockpit-ws is exposed to the public internet, and also has access to a private
+ internal network, it is recommended to explicitly set <literal>LoginTo=false</literal>. This prevents
+ unauthenticated remote attackers from scanning the internal network for existing machines
+ and open ports.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>RequireHost</option></term>
+ <listitem>
+ <para>When set to <literal>true</literal> cockpit will require users to use the
+ <emphasis>Connect to</emphasis> option to specify the host to log into.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>MaxStartups</option></term>
+ <listitem><para>Same as the <command>sshd</command> configuration option by the same name.
+ Specifies the maximum number of concurrent login attempts
+ allowed. Additional connections will be dropped until authentication
+ succeeds or the connections are closed. Defaults to 10.</para>
+
+ <para>Alternatively, random early drop can be enabled by specifying the
+ three colon separated values <literal>start:rate:full</literal> (e.g.
+ "10:30:60"). Cockpit will start refusing authentication attempts with a
+ probability of <literal>rate/100</literal> (30%) if there are currently
+ <literal>start</literal> (10) unauthenticated connections. The probability
+ increases linearly and all connection attempts are refused if the
+ number of unauthenticated connections reaches <literal>full</literal> (60).</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>AllowUnencrypted</option></term>
+ <listitem>
+ <para>If true, cockpit will accept unencrypted HTTP connections. Otherwise, it
+ redirects all HTTP connections to HTTPS. Exceptions are connections from
+ localhost and for certain URLs (like <code>/ping</code>). Defaults to
+ false.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>UrlRoot</option></term>
+ <listitem>
+ <para>The root URL where you will be serving cockpit. When provided cockpit will expect all
+ requests to be prefixed with the given url. This is mostly useful when you are using
+ cockpit behind a reverse proxy, such as nginx. <code>/cockpit/</code> and <code>/cockpit+</code>
+ are reserved and should not be used. For example <code>/cockpit-new/</code> is ok.
+ <code>/cockpit/</code> and <code>/cockpit+new/</code> are not.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>ClientCertAuthentication</option></term>
+ <listitem>
+ <para>If true, enable TLS client certificates for authenticating users. Commonly
+ these are provided by a smart card, but it's equally possible to import
+ certificates directly into the web browser. Please see the
+ <ulink url="https://cockpit-project.org/guide/latest/cert-authentication.html">Certificate/smart card authentication</ulink>
+ section in the Cockpit guide for details.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>Shell</option></term>
+ <listitem>
+ <para>The relative URL to top level component to display in Cockpit once logged in.
+ Defaults to <code>/shell/index.html</code></para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="cockpit-conf-log">
+ <title>Log</title>
+ <variablelist>
+ <varlistentry>
+ <term><option>Fatal</option></term>
+ <listitem>
+ <para>The kind of log messages in the bridge to treat as fatal. Separate multiple values
+ with spaces. Relevant values are: <code>criticals</code> and <code>warnings</code>.</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="cockpit-conf-oauth">
+ <title>OAuth</title>
+ <para>Cockpit can be configured to support the <ulink url="https://tools.ietf.org/html/rfc6749#section-4.2">
+ implicit grant</ulink> OAuth authorization flow. When successful the resulting oauth
+ token will be passed to cockpit-ws using the <literal>Bearer</literal> auth-scheme.
+ For a login to be successful, cockpit will also need a to be configured to verify
+ and allow <literal>Bearer</literal> tokens.</para>
+ <variablelist>
+ <varlistentry>
+ <term><option>URL</option></term>
+ <listitem>
+ <para>This is the url that cockpit will redirect the users browser to when it needs
+ to obtain an oauth token. Cockpit will add a redirect_uri parameter to the url with
+ the location of where the oauth provider should redirect to once a token has been
+ obtained.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>ErrorParam</option></term>
+ <listitem>
+ <para>When a oauth provider redirects a user back to cockpit, look for this parameter
+ in the querystring or fragment portion of the url to find a error message. When not
+ provided it will default to <literal>error_description</literal></para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>TokenParam</option></term>
+ <listitem>
+ <para>When a oauth provider redirects a user back to cockpit, look for this parameter
+ in the querystring or fragment portion of the url to find the access token. When not
+ provided it will default to <literal>access_token</literal></para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="cockpit-conf-session">
+ <title>Session</title>
+ <variablelist>
+ <varlistentry>
+ <term><option>Banner</option></term>
+ <listitem>
+ <para>The contents of the specified file (commonly <literal>/etc/issue</literal>) are shown on the login page.
+ By default, no banner is displayed.</para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>IdleTimeout</option></term>
+ <listitem>
+ <para>Time in minutes after which session expires and user is logged out if no user action
+ has been performed in the given time. This idle timeout only applies to interactive password logins.
+ With non-interactive authentication methods like Kerberos, OAuth, or certificate login, the browser
+ cannot forget credentials, and thus automatic logouts are not useful for protecting credentials
+ of forgotten sessions. Set to <literal>0</literal> to disable session timeout.</para>
+ <informalexample>
+<programlisting language="ini">
+[Session]
+IdleTimeout=15
+</programlisting>
+ </informalexample>
+ <para>When not specified, there is no idle timeout by default.</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1 id="cockpit-conf-bugs">
+ <title>BUGS</title>
+ <para>
+ Please send bug reports to either the distribution bug tracker or the
+ <ulink url="https://github.com/cockpit-project/cockpit/issues/new">upstream bug tracker</ulink>.
+ </para>
+ </refsect1>
+
+ <refsect1 id="cockpit-conf-author">
+ <title>AUTHOR</title>
+ <para>Cockpit has been written by many
+ <ulink url="https://github.com/cockpit-project/cockpit/">contributors</ulink>.</para>
+ </refsect1>
+
+ <refsect1 id="cockpit-conf-also">
+ <title>SEE ALSO</title>
+ <para>
+ <citerefentry>
+ <refentrytitle>cockpit-ws</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>,
+ <citerefentry>
+ <refentrytitle>cockpit-tls</refentrytitle><manvolnum>8</manvolnum>
+ </citerefentry>
+ </para>
+ </refsect1>
+</refentry>
diff --git a/doc/man/cockpit.xml b/doc/man/cockpit.xml
new file mode 100644
index 0000000..d615e22
--- /dev/null
+++ b/doc/man/cockpit.xml
@@ -0,0 +1,81 @@
+<refentry id="cockpit.1">
+
+ <!--
+ This file is part of Cockpit.
+
+ Copyright (C) 2015 Red Hat, Inc.
+
+ Cockpit is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ Cockpit is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+ <refentryinfo>
+ <title>cockpit</title>
+ <productname>cockpit</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>cockpit</refentrytitle>
+ <manvolnum>1</manvolnum>
+ <refmiscinfo class="version"></refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>cockpit</refname>
+ <refpurpose>Cockpit</refpurpose>
+ </refnamediv>
+
+ <refsect1>
+ <title>DESCRIPTION</title>
+ <para>Cockpit is a web accessible interactive admin interface
+ for Linux machines. Cockpit can usually be accessed on port <literal>9090</literal>
+ of the machine it's installed on. Cockpit starts on demand. Use your
+ system credentials to log in.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>COMPONENTS</title>
+ <para>The <command>cockpit-ws</command> web service listens on port
+ <literal>9090</literal> and is started on demand by <command>systemd</command>.
+ The Cockpit web service authenticates the user, loads Cockpit into the browser, and
+ starts <command>cockpit-bridge</command> in a Linux user session.</para>
+ <para>The <command>cockpit-bridge</command> provides Cockpit in the web browser with
+ access to the system APIs. It does this over its standard in and standard out. The
+ bridge is started like a shell once per Linux user session.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>BUGS</title>
+ <para>
+ Please send bug reports to either the distribution bug tracker or the
+ <ulink url="https://github.com/cockpit-project/cockpit/issues/new">upstream bug tracker</ulink>.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>AUTHOR</title>
+ <para>Cockpit has been written by many
+ <ulink url="https://github.com/cockpit-project/cockpit/">contributors</ulink>.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>SEE ALSO</title>
+ <simplelist type="inline">
+ <member><ulink url="https://cockpit-project.org/guide/latest/">Cockpit Guide</ulink></member>
+ <member><citerefentry><refentrytitle>cockpit-ws</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>cockpit-bridge</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>cockpit.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
+ <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+ </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/man/pam_ssh_add.xml b/doc/man/pam_ssh_add.xml
new file mode 100644
index 0000000..665043a
--- /dev/null
+++ b/doc/man/pam_ssh_add.xml
@@ -0,0 +1,97 @@
+<refentry id="pam_ssh_add.8">
+
+ <!--
+ This file is part of Cockpit.
+
+ Copyright (C) 2015 Red Hat, Inc.
+
+ Cockpit is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ Cockpit is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+ <refentryinfo>
+ <title>pam_ssh_add</title>
+ <productname>pam_ssh_add</productname>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>pam_ssh_add</refentrytitle>
+ <manvolnum>8</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>pam_ssh_add</refname>
+ <refpurpose>PAM module to auto load ssh keys into an agent</refpurpose>
+ </refnamediv>
+
+ <refsect1><title>DESCRIPTION</title>
+ <para>
+ pam_ssh_add provides authentication and session modules that
+ allow users to start their session with a running ssh-agent with as
+ many ssh keys loaded as possible.
+ </para>
+ <para>
+ If used, the authentication module simply stores the authentication
+ token for later use by the session module. Because this module performs
+ no actual authentication it returns PAM_CRED_INSUFFICIENT on success and
+ should always be accompanied by an actual authentication module in your
+ pam configuration.
+ </para>
+ <para>
+ By default the session module will start a new ssh-agent and run
+ ssh-add, loading any keys that exist in the default path for the
+ newly logged in user. If any keys prompt for a password, and a authentication
+ token was successfully stored, that token will be provided as the password.
+ </para>
+
+ </refsect1>
+
+ <refsect1 id="options">
+ <title>Options</title>
+ <variablelist>
+ <varlistentry id="debug">
+ <term><option>debug</option></term>
+ <listitem>
+ <para>This option will turn on debug logging to syslog.</para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>Examples</title>
+ <informalexample>
+ <programlisting>
+ auth required pam_unix.so
+ auth optional pam_ssh_add.so
+ session optional pam_ssh_add.so
+ </programlisting>
+ </informalexample>
+
+ </refsect1>
+
+ <refsect1>
+ <title>AUTHOR</title>
+ <para>Cockpit has been written by many
+ <ulink url="https://github.com/cockpit-project/cockpit/">contributors</ulink>.</para>
+ </refsect1>
+
+ <refsect1>
+ <title>BUGS</title>
+ <para>
+ Please send bug reports to either the distribution bug tracker or the
+ <ulink url="https://github.com/cockpit-project/cockpit/issues/new">upstream bug tracker</ulink>.
+ </para>
+ </refsect1>
+
+</refentry>
diff --git a/files.js b/files.js
new file mode 100644
index 0000000..8a5d234
--- /dev/null
+++ b/files.js
@@ -0,0 +1,192 @@
+import path from 'path';
+
+const info = {
+ entries: [
+ "base1/cockpit.js",
+ "apps/apps.jsx",
+ "kdump/kdump.js",
+ // do *not* call this metrics/metrics -- uBlock origin etc. like to block metrics.{css,js}
+ "metrics/index.js",
+
+ "networkmanager/networkmanager.jsx",
+ "networkmanager/firewall.jsx",
+
+ "playground/index.js",
+ "playground/exception.js",
+ "playground/metrics.js",
+ "playground/pkgs.js",
+ "playground/plot.js",
+ "playground/react-patterns.js",
+ "playground/service.js",
+ "playground/speed.js",
+ "playground/test.js",
+ "playground/translate.js",
+ "playground/preloaded.js",
+ "playground/notifications-receiver.js",
+ "playground/journal.jsx",
+
+ "selinux/selinux.js",
+ "shell/shell.js",
+ "sosreport/sosreport.jsx",
+ "static/login.js",
+ "storaged/storaged.jsx",
+
+ "systemd/services.jsx",
+ "systemd/logs.jsx",
+ "systemd/overview.jsx",
+ "systemd/terminal.jsx",
+ "systemd/hwinfo.jsx",
+
+ "packagekit/updates.jsx",
+ "users/users.js",
+ ],
+
+ tests: [
+ "base1/test-base64",
+ "base1/test-browser-storage",
+ "base1/test-cache",
+ "base1/test-chan",
+ "base1/test-dbus-address",
+ "base1/test-dbus-framed",
+ "base1/test-dbus",
+ "base1/test-echo",
+ "base1/test-events",
+ "base1/test-external",
+ "base1/test-file",
+ "base1/test-format",
+ "base1/test-framed-cache",
+ "base1/test-framed",
+ "base1/test-http",
+ "base1/test-journal-renderer",
+ "base1/test-locale",
+ "base1/test-location",
+ "base1/test-metrics",
+ "base1/test-no-jquery",
+ "base1/test-permissions",
+ "base1/test-promise",
+ "base1/test-protocol",
+ "base1/test-series",
+ "base1/test-spawn-proc",
+ "base1/test-spawn",
+ "base1/test-stream",
+ "base1/test-user",
+ "base1/test-utf8",
+ "base1/test-websocket",
+
+ "kdump/test-config-client",
+
+ "networkmanager/test-utils",
+
+ "shell/machines/test-machines",
+
+ "storaged/test-util",
+ ],
+
+ files: [
+ "apps/index.html",
+ "apps/default.png",
+
+ "kdump/index.html",
+
+ "metrics/index.html",
+
+ "networkmanager/index.html",
+ "networkmanager/firewall.html",
+
+ "packagekit/index.html",
+
+ "playground/index.html",
+ "playground/exception.html",
+ "playground/hammer.gif",
+ "playground/metrics.html",
+ "playground/pkgs.html",
+ "playground/plot.html",
+ "playground/react-patterns.html",
+ "playground/service.html",
+ "playground/speed.html",
+ "playground/test.html",
+ "playground/translate.html",
+ "playground/preloaded.html",
+ "playground/notifications-receiver.html",
+ "playground/journal.html",
+
+ "selinux/index.html",
+
+ "shell/images/server-error.png",
+ "shell/images/server-large.png",
+ "shell/images/server-small.png",
+ "shell/images/cockpit-icon.svg",
+ "shell/images/bg-plain.jpg",
+ "shell/index.html",
+ "shell/shell.html",
+
+ "sosreport/index.html",
+ "sosreport/sosreport.png",
+
+ "static/login.html",
+
+ "storaged/index.html",
+ "storaged/images/storage-array.png",
+ "storaged/images/storage-disk.png",
+
+ "systemd/index.html",
+ "systemd/logs.html",
+ "systemd/services.html",
+ "systemd/terminal.html",
+ "systemd/hwinfo.html",
+
+ "users/index.html",
+ ]
+};
+
+const srcdir = process.env.SRCDIR || '.';
+const nodedir = path.relative(process.cwd(), path.resolve(srcdir, "node_modules"));
+
+export const all_subdirs = Array.from(new Set(info.entries.map(key => key.split('/')[0])));
+
+const redhat_fonts = [
+ "Text-Bold", "Text-BoldItalic", "Text-Italic", "Text-Medium", "Text-MediumItalic", "Text-Regular",
+ "Display-Black", "Display-BlackItalic", "Display-Bold", "Display-BoldItalic",
+ "Display-Italic", "Display-Medium", "Display-MediumItalic", "Display-Regular",
+ "Mono-Bold", "Mono-BoldItalic", "Mono-Italic", "Mono-Medium", "Mono-MediumItalic", "Mono-Regular",
+].map(name => {
+ const subdir = 'RedHat' + name.split('-')[0];
+ const fontsdir = '@patternfly/patternfly/assets/fonts';
+
+ return {
+ from: path.resolve(nodedir, fontsdir, subdir, 'RedHat' + name + '.woff2'),
+ to: 'static/fonts/'
+ };
+});
+
+const pkgfile = suffix => `${srcdir}/pkg/${suffix}`;
+export const getFiles = subdir => {
+ /* Qualify all the paths in entries */
+ const entryPoints = [];
+ info.entries.forEach(key => {
+ if (subdir && key.indexOf(subdir) !== 0)
+ return;
+
+ entryPoints.push(pkgfile(key));
+ });
+
+ /* Qualify all the paths in files listed */
+ const files = [];
+ info.files.forEach(value => {
+ if (!subdir || value.indexOf(subdir) === 0)
+ files.push({ from: pkgfile(value), to: path.dirname(value) });
+ });
+ if (subdir) {
+ const manifest = subdir + "/manifest.json";
+ files.push({ from: pkgfile(manifest), to: subdir });
+ } else {
+ all_subdirs.forEach(subdir => {
+ const manifest = subdir + "/manifest.json";
+ files.push({ from: pkgfile(manifest), to: subdir });
+ });
+ }
+
+ return ({ entryPoints, assetFiles: files, redhat_fonts });
+};
+
+export const getTestFiles = () => info.tests;
diff --git a/node_modules/chrome-remote-interface/LICENSE b/node_modules/chrome-remote-interface/LICENSE
new file mode 100644
index 0000000..91137a0
--- /dev/null
+++ b/node_modules/chrome-remote-interface/LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2023 Andrea Cardaci <cyrus.and@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/node_modules/chrome-remote-interface/README.md b/node_modules/chrome-remote-interface/README.md
new file mode 100644
index 0000000..9800abb
--- /dev/null
+++ b/node_modules/chrome-remote-interface/README.md
@@ -0,0 +1,992 @@
+# chrome-remote-interface
+
+[![CI status](https://github.com/cyrus-and/chrome-remote-interface/actions/workflows/ci.yml/badge.svg)](https://github.com/cyrus-and/chrome-remote-interface/actions?query=workflow:CI)
+
+[Build Status]: https://app.travis-ci.com/cyrus-and/chrome-remote-interface.svg?branch=master
+[travis]: https://app.travis-ci.com/cyrus-and/chrome-remote-interface
+
+[Chrome Debugging Protocol] interface that helps to instrument Chrome (or any
+other suitable [implementation](#implementations)) by providing a simple
+abstraction of commands and notifications using a straightforward JavaScript
+API.
+
+This module is one of the many [third-party protocol clients][3rd-party].
+
+[3rd-party]: https://developer.chrome.com/devtools/docs/debugging-clients#chrome-remote-interface
+
+## Sample API usage
+
+The following snippet loads `https://github.com` and dumps every request made:
+
+```js
+const CDP = require('chrome-remote-interface');
+
+async function example() {
+ let client;
+ try {
+ // connect to endpoint
+ client = await CDP();
+ // extract domains
+ const {Network, Page} = client;
+ // setup handlers
+ Network.requestWillBeSent((params) => {
+ console.log(params.request.url);
+ });
+ // enable events then start!
+ await Network.enable();
+ await Page.enable();
+ await Page.navigate({url: 'https://github.com'});
+ await Page.loadEventFired();
+ } catch (err) {
+ console.error(err);
+ } finally {
+ if (client) {
+ await client.close();
+ }
+ }
+}
+
+example();
+```
+
+Find more examples in the [wiki]. You may also want to take a look at the [FAQ].
+
+[wiki]: https://github.com/cyrus-and/chrome-remote-interface/wiki
+[async-await-example]: https://github.com/cyrus-and/chrome-remote-interface/wiki/Async-await-example
+[FAQ]: https://github.com/cyrus-and/chrome-remote-interface#faq
+
+## Installation
+
+ npm install chrome-remote-interface
+
+Install globally (`-g`) to just use the [bundled client](#bundled-client).
+
+## Implementations
+
+This module should work with every application implementing the
+[Chrome Debugging Protocol]. In particular, it has been tested against the
+following implementations:
+
+Implementation | Protocol version | [Protocol] | [List] | [New] | [Activate] | [Close] | [Version]
+---------------------------|--------------------|------------|--------|-------|------------|---------|-----------
+[Chrome][1.1] | [tip-of-tree][1.2] | yes¹ | yes | yes | yes | yes | yes
+[Opera][2.1] | [tip-of-tree][2.2] | yes | yes | yes | yes | yes | yes
+[Node.js][3.1] ([v6.3.0]+) | [node][3.2] | yes | no | no | no | no | yes
+[Safari (iOS)][4.1] | [*partial*][4.2] | no | yes | no | no | no | no
+[Edge][5.1] | [*partial*][5.2] | yes | yes | no | no | no | yes
+[Firefox (Nightly)][6.1] | [*partial*][6.2] | yes | yes | no | yes | yes | yes
+
+¹ Not available on [Chrome for Android][chrome-mobile-protocol], hence a local version of the protocol must be used.
+
+[chrome-mobile-protocol]: https://bugs.chromium.org/p/chromium/issues/detail?id=824626#c4
+
+[1.1]: #chromechromium
+[1.2]: https://chromedevtools.github.io/devtools-protocol/tot/
+
+[2.1]: #opera
+[2.2]: https://chromedevtools.github.io/devtools-protocol/tot/
+
+[3.1]: #nodejs
+[3.2]: https://chromedevtools.github.io/devtools-protocol/v8/
+
+[4.1]: #safari-ios
+[4.2]: http://trac.webkit.org/browser/trunk/Source/JavaScriptCore/inspector/protocol
+
+[5.1]: #edge
+[5.2]: https://docs.microsoft.com/en-us/microsoft-edge/devtools-protocol/0.1/domains/
+
+[6.1]: #firefox-nightly
+[6.2]: https://firefox-source-docs.mozilla.org/remote/index.html
+
+[v6.3.0]: https://nodejs.org/en/blog/release/v6.3.0/
+
+[Protocol]: #cdpprotocoloptions-callback
+[List]: #cdplistoptions-callback
+[New]: #cdpnewoptions-callback
+[Activate]: #cdpactivateoptions-callback
+[Close]: #cdpcloseoptions-callback
+[Version]: #cdpversionoptions-callback
+
+The meaning of *target* varies according to the implementation, for example,
+each Chrome tab represents a target whereas for Node.js a target is the
+currently inspected script.
+
+## Setup
+
+An instance of either Chrome itself or another implementation needs to be
+running on a known port in order to use this module (defaults to
+`localhost:9222`).
+
+### Chrome/Chromium
+
+#### Desktop
+
+Start Chrome with the `--remote-debugging-port` option, for example:
+
+ google-chrome --remote-debugging-port=9222
+
+##### Headless
+
+Since version 59, additionally use the `--headless` option, for example:
+
+ google-chrome --headless --remote-debugging-port=9222
+
+#### Android
+
+Plug the device and enable the [port forwarding][adb], for example:
+
+ adb forward tcp:9222 localabstract:chrome_devtools_remote
+
+Note that in Android, Chrome does not have its own protocol available, a local
+version must be used. See [here](#chrome-debugging-protocol-versions) for more information.
+
+[adb]: https://developer.chrome.com/devtools/docs/remote-debugging-legacy
+
+##### WebView
+
+In order to be inspectable, a WebView must
+be [configured for debugging][webview] and the corresponding process ID must be
+known. There are several ways to obtain it, for example:
+
+ adb shell grep -a webview_devtools_remote /proc/net/unix
+
+Finally, port forwarding can be enabled as follows:
+
+ adb forward tcp:9222 localabstract:webview_devtools_remote_<pid>
+
+[webview]: https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews#configure_webviews_for_debugging
+
+### Opera
+
+Start Opera with the `--remote-debugging-port` option, for example:
+
+ opera --remote-debugging-port=9222
+
+### Node.js
+
+Start Node.js with the `--inspect` option, for example:
+
+ node --inspect=9222 script.js
+
+### Safari (iOS)
+
+Install and run the [iOS WebKit Debug Proxy][iwdp]. Then use it with the `local`
+option set to `true` to use the local version of the protocol or pass a custom
+descriptor upon connection (`protocol` option).
+
+[iwdp]: https://github.com/google/ios-webkit-debug-proxy
+
+### Edge
+
+Start Edge with the `--devtools-server-port` option, for example:
+
+ MicrosoftEdge.exe --devtools-server-port 9222 about:blank
+
+Please find more information [here][edge-devtools].
+
+[edge-devtools]: https://docs.microsoft.com/en-us/microsoft-edge/devtools-protocol/
+
+### Firefox (Nightly)
+
+Start Firefox with the `--remote-debugging-port` option, for example:
+
+ firefox --remote-debugging-port 9222
+
+Bear in mind that this is an experimental feature of Firefox.
+
+## Bundled client
+
+This module comes with a bundled client application that can be used to
+interactively control a remote instance.
+
+### Target management
+
+The bundled client exposes subcommands to interact with the HTTP frontend
+(e.g., [List](#cdplistoptions-callback), [New](#cdpnewoptions-callback), etc.),
+run with `--help` to display the list of available options.
+
+Here are some examples:
+
+```js
+$ chrome-remote-interface new 'http://example.com'
+{
+ "description": "",
+ "devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/b049bb56-de7d-424c-a331-6ae44cf7ae01",
+ "id": "b049bb56-de7d-424c-a331-6ae44cf7ae01",
+ "thumbnailUrl": "/thumb/b049bb56-de7d-424c-a331-6ae44cf7ae01",
+ "title": "",
+ "type": "page",
+ "url": "http://example.com/",
+ "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/b049bb56-de7d-424c-a331-6ae44cf7ae01"
+}
+$ chrome-remote-interface close 'b049bb56-de7d-424c-a331-6ae44cf7ae01'
+```
+
+### Inspection
+
+Using the `inspect` subcommand it is possible to perform [command execution](#clientdomainmethodparams-callback)
+and [event binding](#clientdomaineventcallback) in a REPL fashion that provides completion.
+
+Here is a sample session:
+
+```js
+$ chrome-remote-interface inspect
+>>> Runtime.evaluate({expression: 'window.location.toString()'})
+{ result: { type: 'string', value: 'about:blank' } }
+>>> Page.enable()
+{}
+>>> Page.loadEventFired(console.log)
+[Function]
+>>> Page.navigate({url: 'https://github.com'})
+{ frameId: 'E1657E22F06E6E0BE13DFA8130C20298',
+ loaderId: '439236ADE39978F98C20E8939A32D3A5' }
+>>> { timestamp: 7454.721299 } // from Page.loadEventFired
+>>> Runtime.evaluate({expression: 'window.location.toString()'})
+{ result: { type: 'string', value: 'https://github.com/' } }
+```
+
+Additionally there are some custom commands available:
+
+```js
+>>> .help
+[...]
+.reset Remove all the registered event handlers
+.target Display the current target
+```
+
+## Embedded documentation
+
+In both the REPL and the regular API every object of the protocol is *decorated*
+with the meta information found within the descriptor. In addition The
+`category` field is added, which determines if the member is a `command`, an
+`event` or a `type`.
+
+For example to learn how to call `Page.navigate`:
+
+```js
+>>> Page.navigate
+{ [Function]
+ category: 'command',
+ parameters: { url: { type: 'string', description: 'URL to navigate the page to.' } },
+ returns:
+ [ { name: 'frameId',
+ '$ref': 'FrameId',
+ hidden: true,
+ description: 'Frame id that will be navigated.' } ],
+ description: 'Navigates current page to the given URL.',
+ handlers: [ 'browser', 'renderer' ] }
+```
+
+To learn about the parameters returned by the `Network.requestWillBeSent` event:
+
+```js
+>>> Network.requestWillBeSent
+{ [Function]
+ category: 'event',
+ description: 'Fired when page is about to send HTTP request.',
+ parameters:
+ { requestId: { '$ref': 'RequestId', description: 'Request identifier.' },
+ frameId:
+ { '$ref': 'Page.FrameId',
+ description: 'Frame identifier.',
+ hidden: true },
+ loaderId: { '$ref': 'LoaderId', description: 'Loader identifier.' },
+ documentURL:
+ { type: 'string',
+ description: 'URL of the document this request is loaded for.' },
+ request: { '$ref': 'Request', description: 'Request data.' },
+ timestamp: { '$ref': 'Timestamp', description: 'Timestamp.' },
+ wallTime:
+ { '$ref': 'Timestamp',
+ hidden: true,
+ description: 'UTC Timestamp.' },
+ initiator: { '$ref': 'Initiator', description: 'Request initiator.' },
+ redirectResponse:
+ { optional: true,
+ '$ref': 'Response',
+ description: 'Redirect response data.' },
+ type:
+ { '$ref': 'Page.ResourceType',
+ optional: true,
+ hidden: true,
+ description: 'Type of this resource.' } } }
+```
+
+To inspect the `Network.Request` (note that unlike commands and events, types
+are named in upper camel case) type:
+
+```js
+>>> Network.Request
+{ category: 'type',
+ id: 'Request',
+ type: 'object',
+ description: 'HTTP request data.',
+ properties:
+ { url: { type: 'string', description: 'Request URL.' },
+ method: { type: 'string', description: 'HTTP request method.' },
+ headers: { '$ref': 'Headers', description: 'HTTP request headers.' },
+ postData:
+ { type: 'string',
+ optional: true,
+ description: 'HTTP POST request data.' },
+ mixedContentType:
+ { optional: true,
+ type: 'string',
+ enum: [Object],
+ description: 'The mixed content status of the request, as defined in http://www.w3.org/TR/mixed-content/' },
+ initialPriority:
+ { '$ref': 'ResourcePriority',
+ description: 'Priority of the resource request at the time request is sent.' } } }
+```
+
+## Chrome Debugging Protocol versions
+
+By default `chrome-remote-interface` *asks* the remote instance to provide its
+own protocol.
+
+This behavior can be changed by setting the `local` option to `true`
+upon [connection](#cdpoptions-callback), in which case the [local version] of
+the protocol descriptor is used. This file is manually updated from time to time
+using `scripts/update-protocol.sh` and pushed to this repository.
+
+To further override the above behavior there are basically two options:
+
+- pass a custom protocol descriptor upon [connection](#cdpoptions-callback)
+ (`protocol` option);
+
+- use the *raw* version of the [commands](#clientsendmethod-params-callback)
+ and [events](#event-domainmethod) interface to use bleeding-edge features that
+ do not appear in the [local version] of the protocol descriptor;
+
+[local version]: lib/protocol.json
+
+## Browser usage
+
+This module is able to run within a web context, with obvious limitations
+though, namely external HTTP requests
+([List](#cdplistoptions-callback), [New](#cdpnewoptions-callback), etc.) cannot
+be performed directly, for this reason the user must provide a global
+`criRequest` in order to use them:
+
+```js
+function criRequest(options, callback) {}
+```
+
+`options` is the same object used by the Node.js `http` module and `callback` is
+a function taking two arguments: `err` (JavaScript `Error` object or `null`) and
+`data` (string result).
+
+### Using [webpack](https://webpack.github.io/)
+
+It just works, simply require this module:
+
+```js
+const CDP = require('chrome-remote-interface');
+```
+
+### Using *vanilla* JavaScript
+
+To generate a JavaScript file that can be used with a `<script>` element:
+
+1. run `npm install` from the root directory;
+
+2. manually run webpack with:
+
+ TARGET=var npm run webpack
+
+3. use as:
+
+ ```html
+ <script>
+ function criRequest(options, callback) { /*...*/ }
+ </script>
+ <script src="chrome-remote-interface.js"></script>
+ ```
+
+## TypeScript Support
+
+[TypeScript][] definitions are kindly provided by [Khairul Azhar Kasmiran][] and [Seth Westphal][], and can be installed from [DefinitelyTyped][]:
+
+```
+npm install --save-dev @types/chrome-remote-interface
+```
+
+Note that the TypeScript definitions are automatically generated from the npm package `devtools-protocol@0.0.927104`. For other versions of devtools-protocol:
+
+1. Install patch-package using [the instructions given](https://github.com/ds300/patch-package#set-up).
+2. Copy the contents of the corresponding https://github.com/ChromeDevTools/devtools-protocol/tree/master/types folder (according to commit) into `node_modules/devtools-protocol/types`.
+3. Run `npx patch-package devtools-protocol` so that the changes persist across an `npm install`.
+
+[TypeScript]: https://www.typescriptlang.org/
+[Khairul Azhar Kasmiran]: https://github.com/kazarmy
+[Seth Westphal]: https://github.com/westy92
+[DefinitelyTyped]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/chrome-remote-interface
+
+## API
+
+The API consists of three parts:
+
+- *DevTools* methods (for those [implementations](#implementations) that support
+ them, e.g., [List](#cdplistoptions-callback), [New](#cdpnewoptions-callback),
+ etc.);
+
+- [connection](#cdpoptions-callback) establishment;
+
+- the actual [protocol interaction](#class-cdp).
+
+### CDP([options], [callback])
+
+Connects to a remote instance using the [Chrome Debugging Protocol].
+
+`options` is an object with the following optional properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function;
+- `target`: determines which target this client should attach to. The behavior
+ changes according to the type:
+
+ - a `function` that takes the array returned by the `List` method and returns
+ a target or its numeric index relative to the array;
+ - a target `object` like those returned by the `New` and `List` methods;
+ - a `string` representing the raw WebSocket URL, in this case `host` and
+ `port` are not used to fetch the target list, yet they are used to complete
+ the URL if relative;
+ - a `string` representing the target id.
+
+ Defaults to a function which returns the first available target according to
+ the implementation (note that at most one connection can be established to the
+ same target);
+- `protocol`: [Chrome Debugging Protocol] descriptor object. Defaults to use the
+ protocol chosen according to the `local` option;
+- `local`: a boolean indicating whether the protocol must be fetched *remotely*
+ or if the local version must be used. It has no effect if the `protocol`
+ option is set. Defaults to `false`.
+
+These options are also valid properties of all the instances of the `CDP`
+class. In addition to that, the `webSocketUrl` field contains the currently used
+WebSocket URL.
+
+`callback` is a listener automatically added to the `connect` event of the
+returned `EventEmitter`. When `callback` is omitted a `Promise` object is
+returned which becomes fulfilled if the `connect` event is triggered and
+rejected if the `error` event is triggered.
+
+The `EventEmitter` supports the following events:
+
+#### Event: 'connect'
+
+```js
+function (client) {}
+```
+
+Emitted when the connection to the WebSocket is established.
+
+`client` is an instance of the `CDP` class.
+
+#### Event: 'error'
+
+```js
+function (err) {}
+```
+
+Emitted when `http://host:port/json` cannot be reached or if it is not possible
+to connect to the WebSocket.
+
+`err` is an instance of `Error`.
+
+### CDP.Protocol([options], [callback])
+
+Fetch the [Chrome Debugging Protocol] descriptor.
+
+`options` is an object with the following optional properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function;
+- `local`: a boolean indicating whether the protocol must be fetched *remotely*
+ or if the local version must be returned. Defaults to `false`.
+
+`callback` is executed when the protocol is fetched, it gets the following
+arguments:
+
+- `err`: a `Error` object indicating the success status;
+- `protocol`: the [Chrome Debugging Protocol] descriptor.
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.Protocol((err, protocol) => {
+ if (!err) {
+ console.log(JSON.stringify(protocol, null, 4));
+ }
+});
+```
+
+### CDP.List([options], [callback])
+
+Request the list of the available open targets/tabs of the remote instance.
+
+`options` is an object with the following optional properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function.
+
+`callback` is executed when the list is correctly received, it gets the
+following arguments:
+
+- `err`: a `Error` object indicating the success status;
+- `targets`: the array returned by `http://host:port/json/list` containing the
+ target list.
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.List((err, targets) => {
+ if (!err) {
+ console.log(targets);
+ }
+});
+```
+
+### CDP.New([options], [callback])
+
+Create a new target/tab in the remote instance.
+
+`options` is an object with the following optional properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function;
+- `url`: URL to load in the new target/tab. Defaults to `about:blank`.
+
+`callback` is executed when the target is created, it gets the following
+arguments:
+
+- `err`: a `Error` object indicating the success status;
+- `target`: the object returned by `http://host:port/json/new` containing the
+ target.
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.New((err, target) => {
+ if (!err) {
+ console.log(target);
+ }
+});
+```
+
+### CDP.Activate([options], [callback])
+
+Activate an open target/tab of the remote instance.
+
+`options` is an object with the following properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function;
+- `id`: Target id. Required, no default.
+
+`callback` is executed when the response to the activation request is
+received. It gets the following arguments:
+
+- `err`: a `Error` object indicating the success status;
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.Activate({id: 'CC46FBFA-3BDA-493B-B2E4-2BE6EB0D97EC'}, (err) => {
+ if (!err) {
+ console.log('target is activated');
+ }
+});
+```
+
+### CDP.Close([options], [callback])
+
+Close an open target/tab of the remote instance.
+
+`options` is an object with the following properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function;
+- `id`: Target id. Required, no default.
+
+`callback` is executed when the response to the close request is received. It
+gets the following arguments:
+
+- `err`: a `Error` object indicating the success status;
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.Close({id: 'CC46FBFA-3BDA-493B-B2E4-2BE6EB0D97EC'}, (err) => {
+ if (!err) {
+ console.log('target is closing');
+ }
+});
+```
+
+Note that the callback is fired when the target is *queued* for removal, but the
+actual removal will occur asynchronously.
+
+### CDP.Version([options], [callback])
+
+Request version information from the remote instance.
+
+`options` is an object with the following optional properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function.
+
+`callback` is executed when the version information is correctly received, it
+gets the following arguments:
+
+- `err`: a `Error` object indicating the success status;
+- `info`: a JSON object returned by `http://host:port/json/version` containing
+ the version information.
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.Version((err, info) => {
+ if (!err) {
+ console.log(info);
+ }
+});
+```
+
+### Class: CDP
+
+#### Event: 'event'
+
+```js
+function (message) {}
+```
+
+Emitted when the remote instance sends any notification through the WebSocket.
+
+`message` is the object received, it has the following properties:
+
+- `method`: a string describing the notification (e.g.,
+ `'Network.requestWillBeSent'`);
+- `params`: an object containing the payload;
+- `sessionId`: an optional string representing the session identifier.
+
+Refer to the [Chrome Debugging Protocol] specification for more information.
+
+For example:
+
+```js
+client.on('event', (message) => {
+ if (message.method === 'Network.requestWillBeSent') {
+ console.log(message.params);
+ }
+});
+```
+
+#### Event: '`<domain>`.`<method>`'
+
+```js
+function (params, sessionId) {}
+```
+
+Emitted when the remote instance sends a notification for `<domain>.<method>`
+through the WebSocket.
+
+`params` is an object containing the payload.
+
+`sessionId` is an optional string representing the session identifier.
+
+This is just a utility event which allows to easily listen for specific
+notifications (see [`'event'`](#event-event)), for example:
+
+```js
+client.on('Network.requestWillBeSent', console.log);
+```
+
+Additionally, the equivalent `<domain>.on('<method>', ...)` syntax is available, for example:
+
+```js
+client.Network.on('requestWillBeSent', console.log);
+```
+
+#### Event: '`<domain>`.`<method>`.`<sessionId>`'
+
+```js
+function (params, sessionId) {}
+```
+
+Equivalent to the following but only for those events belonging to the given `session`:
+
+```js
+client.on('<domain>.<event>', callback);
+```
+
+#### Event: 'ready'
+
+```js
+function () {}
+```
+
+Emitted every time that there are no more pending commands waiting for a
+response from the remote instance. The interaction is asynchronous so the only
+way to serialize a sequence of commands is to use the callback provided by
+the [`send`](#clientsendmethod-params-callback) method. This event acts as a
+barrier and it is useful to avoid the *callback hell* in certain simple
+situations.
+
+Users are encouraged to extensively check the response of each method and should
+prefer the promises API when dealing with complex asynchronous program flows.
+
+For example to load a URL only after having enabled the notifications of both
+`Network` and `Page` domains:
+
+```js
+client.Network.enable();
+client.Page.enable();
+client.once('ready', () => {
+ client.Page.navigate({url: 'https://github.com'});
+});
+```
+
+In this particular case, not enforcing this kind of serialization may cause that
+the remote instance does not properly deliver the desired notifications the
+client.
+
+
+#### Event: 'disconnect'
+
+```js
+function () {}
+```
+
+Emitted when the instance closes the WebSocket connection.
+
+This may happen for example when the user opens DevTools or when the tab is
+closed.
+
+#### client.send(method, [params], [sessionId], [callback])
+
+Issue a command to the remote instance.
+
+`method` is a string describing the command.
+
+`params` is an object containing the payload.
+
+`sessionId` is a string representing the session identifier.
+
+`callback` is executed when the remote instance sends a response to this
+command, it gets the following arguments:
+
+- `error`: a boolean value indicating the success status, as reported by the
+ remote instance;
+- `response`: an object containing either the response (`result` field, if
+ `error === false`) or the indication of the error (`error` field, if `error
+ === true`).
+
+When `callback` is omitted a `Promise` object is returned instead, with the
+fulfilled/rejected states implemented according to the `error` parameter. The
+`Error` object returned contains two additional parameters: `request` and
+`response` which contain the raw massages, useful for debugging purposes. In
+case of low-level WebSocket errors, the `error` parameter contains the
+originating `Error` object and no `response` is returned.
+
+Note that the field `id` mentioned in the [Chrome Debugging Protocol]
+specification is managed internally and it is not exposed to the user.
+
+For example:
+
+```js
+client.send('Page.navigate', {url: 'https://github.com'}, console.log);
+```
+
+#### client.`<domain>`.`<method>`([params], [sessionId], [callback])
+
+Just a shorthand for:
+
+```js
+client.send('<domain>.<method>', params, sessionId, callback);
+```
+
+For example:
+
+```js
+client.Page.navigate({url: 'https://github.com'}, console.log);
+```
+
+#### client.`<domain>`.`<event>`([sessionId], [callback])
+
+Just a shorthand for:
+
+```js
+client.on('<domain>.<event>[.<sessionId>]', callback);
+```
+
+When `callback` is omitted the event is registered only once and a `Promise`
+object is returned. Notice though that in this case the optional `sessionId` usually passed to `callback` is not returned.
+
+When `callback` is provided, it returns a function that can be used to
+unsubscribe `callback` from the event, it can be useful when anonymous functions
+are used as callbacks.
+
+For example:
+
+```js
+const unsubscribe = client.Network.requestWillBeSent((params, sessionId) => {
+ console.log(params.request.url);
+});
+unsubscribe();
+```
+
+#### client.close([callback])
+
+Close the connection to the remote instance.
+
+`callback` is executed when the WebSocket is successfully closed.
+
+When `callback` is omitted a `Promise` object is returned.
+
+#### client['`<domain>`.`<name>`']
+
+Just a shorthand for:
+
+```js
+client.<domain>.<name>
+```
+
+Where `<name>` can be a command, an event, or a type.
+
+## FAQ
+
+### Invoking `Domain.methodOrEvent` I obtain `Domain.methodOrEvent is not a function`
+
+This means that you are trying to use a method or an event that are not present
+in the protocol descriptor that you are using.
+
+If the protocol is fetched from Chrome directly, then it means that this version
+of Chrome does not support that feature. The solution is to update it.
+
+If you are using a local or custom version of the protocol, then it means that
+the version is obsolete. The solution is to provide an up-to-date one, or if you
+are using the protocol embedded in chrome-remote-interface, make sure to be
+running the latest version of this module. In case the embedded protocol is
+obsolete, please [file an issue](https://github.com/cyrus-and/chrome-remote-interface/issues/new).
+
+See [here](#chrome-debugging-protocol-versions) for more information.
+
+### Invoking `Domain.method` I obtain `Domain.method wasn't found`
+
+This means that you are providing a custom or local protocol descriptor
+(`CDP({protocol: customProtocol})`) which declares `Domain.method` while the
+Chrome version that you are using does not support it.
+
+To inspect the currently available protocol descriptor use:
+
+```
+$ chrome-remote-interface inspect
+```
+
+See [here](#chrome-debugging-protocol-versions) for more information.
+
+### Why my program stalls or behave unexpectedly if I run Chrome in a Docker container?
+
+This happens because the size of `/dev/shm` is set to 64MB by default in Docker
+and may not be enough for Chrome to navigate certain web pages.
+
+You can change this value by running your container with, say,
+`--shm-size=256m`.
+
+### Using `Runtime.evaluate` with `awaitPromise: true` I sometimes obtain `Error: Promise was collected`
+
+This is thrown by `Runtime.evaluate` when the browser-side promise gets
+*collected* by the Chrome's garbage collector, this happens when the whole
+JavaScript execution environment is invalidated, e.g., a when page is navigated
+or reloaded while a promise is still waiting to be resolved.
+
+Here is an example:
+
+```
+$ chrome-remote-interface inspect
+>>> Runtime.evaluate({expression: `new Promise(() => {})`, awaitPromise: true})
+>>> Page.reload() // then wait several seconds
+{ result: {} }
+{ error: { code: -32000, message: 'Promise was collected' } }
+```
+
+To fix this, just make sure there are no pending promises before closing,
+reloading, etc. a page.
+
+### How does this compare to Puppeteer?
+
+[Puppeteer] is an additional high-level API built upon the [Chrome Debugging
+Protocol] which, among the other things, may start and use a bundled version of
+Chromium instead of the one installed on your system. Use it if its API meets
+your needs as it would probably be easier to work with.
+
+chrome-remote-interface instead is just a general purpose 1:1 Node.js binding
+for the [Chrome Debugging Protocol]. Use it if you need all the power of the raw
+protocol, e.g., to implement your own high-level API.
+
+See [#240] for a more thorough discussion.
+
+[Puppeteer]: https://github.com/GoogleChrome/puppeteer
+[#240]: https://github.com/cyrus-and/chrome-remote-interface/issues/240
+
+## Contributors
+
+- [Andrey Sidorov](https://github.com/sidorares)
+- [Greg Cochard](https://github.com/gcochard)
+
+## Resources
+
+- [Chrome Debugging Protocol]
+- [Chrome Debugging Protocol Google group](https://groups.google.com/forum/#!forum/chrome-debugging-protocol)
+- [devtools-protocol official repo](https://github.com/ChromeDevTools/devtools-protocol)
+- [Showcase Chrome Debugging Protocol Clients](https://developer.chrome.com/devtools/docs/debugging-clients)
+- [Awesome chrome-devtools](https://github.com/ChromeDevTools/awesome-chrome-devtools)
+
+[Chrome Debugging Protocol]: https://chromedevtools.github.io/devtools-protocol/
diff --git a/node_modules/chrome-remote-interface/bin/client.js b/node_modules/chrome-remote-interface/bin/client.js
new file mode 100755
index 0000000..80b2d4d
--- /dev/null
+++ b/node_modules/chrome-remote-interface/bin/client.js
@@ -0,0 +1,311 @@
+#!/usr/bin/env node
+
+'use strict';
+
+const repl = require('repl');
+const util = require('util');
+const fs = require('fs');
+const path = require('path');
+
+const program = require('commander');
+
+const CDP = require('../');
+const packageInfo = require('../package.json');
+
+function display(object) {
+ return util.inspect(object, {
+ colors: process.stdout.isTTY,
+ depth: null
+ });
+}
+
+function toJSON(object) {
+ return JSON.stringify(object, null, 4);
+}
+
+///
+
+function inspect(target, args, options) {
+ options.local = args.local;
+ // otherwise the active target
+ if (target) {
+ if (args.webSocket) {
+ // by WebSocket URL
+ options.target = target;
+ } else {
+ // by target id
+ options.target = (targets) => {
+ return targets.findIndex((_target) => {
+ return _target.id === target;
+ });
+ };
+ }
+ }
+
+ if (args.protocol) {
+ options.protocol = JSON.parse(fs.readFileSync(args.protocol));
+ }
+
+ CDP(options, (client) => {
+ const cdpRepl = repl.start({
+ prompt: process.stdin.isTTY ? '\x1b[32m>>>\x1b[0m ' : '',
+ ignoreUndefined: true,
+ writer: display
+ });
+
+ // XXX always await promises on the REPL
+ const defaultEval = cdpRepl.eval;
+ cdpRepl.eval = (cmd, context, filename, callback) => {
+ defaultEval(cmd, context, filename, async (err, result) => {
+ if (err) {
+ // propagate errors from the eval
+ callback(err);
+ } else {
+ // awaits the promise and either return result or error
+ try {
+ callback(null, await Promise.resolve(result));
+ } catch (err) {
+ callback(err);
+ }
+ }
+ });
+ };
+
+ const homePath = process.env.HOME || process.env.USERPROFILE;
+ const historyFile = path.join(homePath, '.cri_history');
+ const historySize = 10000;
+
+ function loadHistory() {
+ // only if run from a terminal
+ if (!process.stdin.isTTY) {
+ return;
+ }
+ // attempt to open the history file
+ let fd;
+ try {
+ fd = fs.openSync(historyFile, 'r');
+ } catch (err) {
+ return; // no history file present
+ }
+ // populate the REPL history
+ fs.readFileSync(fd, 'utf8')
+ .split('\n')
+ .filter((entry) => {
+ return entry.trim();
+ })
+ .reverse() // to be compatible with repl.history files
+ .forEach((entry) => {
+ cdpRepl.history.push(entry);
+ });
+ }
+
+ function saveHistory() {
+ // only if run from a terminal
+ if (!process.stdin.isTTY) {
+ return;
+ }
+ // only store the last chunk
+ const entries = cdpRepl.history.slice(0, historySize).reverse().join('\n');
+ fs.writeFileSync(historyFile, entries + '\n');
+ }
+
+ // utility custom command
+ cdpRepl.defineCommand('target', {
+ help: 'Display the current target',
+ action: () => {
+ console.log(client.webSocketUrl);
+ cdpRepl.displayPrompt();
+ }
+ });
+
+ // utility to purge all the event handlers
+ cdpRepl.defineCommand('reset', {
+ help: 'Remove all the registered event handlers',
+ action: () => {
+ client.removeAllListeners();
+ cdpRepl.displayPrompt();
+ }
+ });
+
+ // enable history
+ loadHistory();
+
+ // disconnect on exit
+ cdpRepl.on('exit', () => {
+ if (process.stdin.isTTY) {
+ console.log();
+ }
+ client.close();
+ saveHistory();
+ });
+
+ // exit on disconnection
+ client.on('disconnect', () => {
+ console.error('Disconnected.');
+ saveHistory();
+ process.exit(1);
+ });
+
+ // add protocol API
+ for (const domainObject of client.protocol.domains) {
+ // walk the domain names
+ const domainName = domainObject.domain;
+ cdpRepl.context[domainName] = {};
+ // walk the items in the domain
+ for (const itemName in client[domainName]) {
+ // add CDP object to the REPL context
+ const cdpObject = client[domainName][itemName];
+ cdpRepl.context[domainName][itemName] = cdpObject;
+ }
+ }
+ }).on('error', (err) => {
+ console.error('Cannot connect to remote endpoint:', err.toString());
+ });
+}
+
+function list(options) {
+ CDP.List(options, (err, targets) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ console.log(toJSON(targets));
+ });
+}
+
+function _new(url, options) {
+ options.url = url;
+ CDP.New(options, (err, target) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ console.log(toJSON(target));
+ });
+}
+
+function activate(args, options) {
+ options.id = args;
+ CDP.Activate(options, (err) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ });
+}
+
+function close(args, options) {
+ options.id = args;
+ CDP.Close(options, (err) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ });
+}
+
+function version(options) {
+ CDP.Version(options, (err, info) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ console.log(toJSON(info));
+ });
+}
+
+function protocol(args, options) {
+ options.local = args.local;
+ CDP.Protocol(options, (err, protocol) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ console.log(toJSON(protocol));
+ });
+}
+
+///
+
+let action;
+
+program
+ .option('-v, --v', 'Show this module version')
+ .option('-t, --host <host>', 'HTTP frontend host')
+ .option('-p, --port <port>', 'HTTP frontend port')
+ .option('-s, --secure', 'HTTPS/WSS frontend')
+ .option('-n, --use-host-name', 'Do not perform a DNS lookup of the host');
+
+program
+ .command('inspect [<target>]')
+ .description('inspect a target (defaults to the first available target)')
+ .option('-w, --web-socket', 'interpret <target> as a WebSocket URL instead of a target id')
+ .option('-j, --protocol <file.json>', 'Chrome Debugging Protocol descriptor (overrides `--local`)')
+ .option('-l, --local', 'Use the local protocol descriptor')
+ .action((target, args) => {
+ action = inspect.bind(null, target, args);
+ });
+
+program
+ .command('list')
+ .description('list all the available targets/tabs')
+ .action(() => {
+ action = list;
+ });
+
+program
+ .command('new [<url>]')
+ .description('create a new target/tab')
+ .action((url) => {
+ action = _new.bind(null, url);
+ });
+
+program
+ .command('activate <id>')
+ .description('activate a target/tab by id')
+ .action((id) => {
+ action = activate.bind(null, id);
+ });
+
+program
+ .command('close <id>')
+ .description('close a target/tab by id')
+ .action((id) => {
+ action = close.bind(null, id);
+ });
+
+program
+ .command('version')
+ .description('show the browser version')
+ .action(() => {
+ action = version;
+ });
+
+program
+ .command('protocol')
+ .description('show the currently available protocol descriptor')
+ .option('-l, --local', 'Return the local protocol descriptor')
+ .action((args) => {
+ action = protocol.bind(null, args);
+ });
+
+program.parse(process.argv);
+
+// common options
+const options = {
+ host: program.host,
+ port: program.port,
+ secure: program.secure,
+ useHostName: program.useHostName
+};
+
+if (action) {
+ action(options);
+} else {
+ if (program.v) {
+ console.log(packageInfo.version);
+ } else {
+ program.outputHelp();
+ process.exit(1);
+ }
+}
diff --git a/node_modules/chrome-remote-interface/chrome-remote-interface.js b/node_modules/chrome-remote-interface/chrome-remote-interface.js
new file mode 100644
index 0000000..6835723
--- /dev/null
+++ b/node_modules/chrome-remote-interface/chrome-remote-interface.js
@@ -0,0 +1 @@
+(()=>{var e={6010:(e,t,r)=>{"use strict";var n=r(4155);const i=r(7187),o=r(4782),a=r(7996),s=r(8855);o.setDefaultResultOrder&&o.setDefaultResultOrder("ipv4first"),e.exports=function(e,t){"function"==typeof e&&(t=e,e=void 0);const r=new i;return"function"==typeof t?(n.nextTick((()=>{new s(e,r)})),r.once("connect",t)):new Promise(((t,n)=>{r.once("connect",t),r.once("error",n),new s(e,r)}))},e.exports.Protocol=a.Protocol,e.exports.List=a.List,e.exports.New=a.New,e.exports.Activate=a.Activate,e.exports.Close=a.Close,e.exports.Version=a.Version},7249:e=>{"use strict";function t(e,t,r){e.category=t,Object.keys(r).forEach((n=>{"name"!==n&&(e[n]="type"===t&&"properties"===n||"parameters"===n?function(e){const t={};return e.forEach((e=>{const r=e.name;delete e.name,t[r]=e})),t}(r[n]):r[n])}))}e.exports.prepare=function(e,r){e.protocol=r,r.domains.forEach((r=>{const n=r.domain;e[n]={},(r.commands||[]).forEach((r=>{!function(e,r,n){const i=`${r}.${n.name}`,o=(t,r,n)=>e.send(i,t,r,n);t(o,"command",n),e[i]=e[r][n.name]=o}(e,n,r)})),(r.events||[]).forEach((r=>{!function(e,r,n){const i=`${r}.${n.name}`,o=(t,r)=>{"function"==typeof t&&(r=t,t=void 0);const n=t?`${i}.${t}`:i;return"function"==typeof r?(e.on(n,r),()=>e.removeListener(n,r)):new Promise(((t,r)=>{e.once(n,t)}))};t(o,"event",n),e[i]=e[r][n.name]=o}(e,n,r)})),(r.types||[]).forEach((r=>{!function(e,r,n){const i=`${r}.${n.id}`,o={};t(o,"type",n),e[i]=e[r][n.id]=o}(e,n,r)})),e[n].on=(t,r)=>e[n][t](r)}))}},8855:(e,t,r)=>{"use strict";var n=r(4155);const i=r(7187),o=r(1588),a=r(8575).WU,s=r(8575).Qc,p=r(5529),d=r(7249),c=r(5372),l=r(7996);class u extends Error{constructor(e,t){let{message:r}=t;t.data&&(r+=` (${t.data})`),super(r),this.request=e,this.response=t}}e.exports=class extends i{constructor(e,t){super();e=e||{},this.host=e.host||c.HOST,this.port=e.port||c.PORT,this.secure=!!e.secure,this.useHostName=!!e.useHostName,this.alterPath=e.alterPath||(e=>e),this.protocol=e.protocol,this.local=!!e.local,this.target=e.target||(e=>{let t,r=e.find((e=>!!e.webSocketDebuggerUrl&&(t=t||e,"page"===e.type)));if(r=r||t,r)return r;throw new Error("No inspectable targets")}),this._notifier=t,this._callbacks={},this._nextCommandId=1,this.webSocketUrl=void 0,this._start()}inspect(e,t){return t.customInspect=!1,o.inspect(this,t)}send(e,t,r,n){const i=Array.from(arguments).slice(1);return t=i.find((e=>"object"==typeof e)),r=i.find((e=>"string"==typeof e)),"function"==typeof(n=i.find((e=>"function"==typeof e)))?void this._enqueueCommand(e,t,r,n):new Promise(((n,i)=>{this._enqueueCommand(e,t,r,((o,a)=>{if(o){const n={method:e,params:t,sessionId:r};i(o instanceof Error?o:new u(n,a))}else n(a)}))}))}close(e){const t=e=>{3===this._ws.readyState?e():(this._ws.removeAllListeners("close"),this._ws.once("close",(()=>{this._ws.removeAllListeners(),this._handleConnectionClose(),e()})),this._ws.close())};return"function"==typeof e?void t(e):new Promise(((e,r)=>{t(e)}))}async _start(){const e={host:this.host,port:this.port,secure:this.secure,useHostName:this.useHostName,alterPath:this.alterPath};try{const t=await this._fetchDebuggerURL(e),r=s(t);r.pathname=e.alterPath(r.pathname),this.webSocketUrl=a(r),e.host=r.hostname,e.port=r.port||e.port;const i=await this._fetchProtocol(e);d.prepare(this,i),await this._connectToWebSocket(),n.nextTick((()=>{this._notifier.emit("connect",this)}))}catch(e){this._notifier.emit("error",e)}}async _fetchDebuggerURL(e){const t=this.target;switch(typeof t){case"string":{let r=t;if(r.startsWith("/")&&(r=`ws://${this.host}:${this.port}${r}`),r.match(/^wss?:/i))return r;return(await l.List(e)).find((e=>e.id===r)).webSocketDebuggerUrl}case"object":return t.webSocketDebuggerUrl;case"function":{const r=t,n=await l.List(e),i=r(n);return("number"==typeof i?n[i]:i).webSocketDebuggerUrl}default:throw new Error(`Invalid target argument "${this.target}"`)}}async _fetchProtocol(e){return this.protocol?this.protocol:(e.local=this.local,await l.Protocol(e))}_connectToWebSocket(){return new Promise(((e,t)=>{try{this.secure&&(this.webSocketUrl=this.webSocketUrl.replace(/^ws:/i,"wss:")),this._ws=new p(this.webSocketUrl,[],{maxPayload:268435456,perMessageDeflate:!1,followRedirects:!0})}catch(e){return void t(e)}this._ws.on("open",(()=>{e()})),this._ws.on("message",(e=>{const t=JSON.parse(e);this._handleMessage(t)})),this._ws.on("close",(e=>{this._handleConnectionClose(),this.emit("disconnect")})),this._ws.on("error",(e=>{t(e)}))}))}_handleConnectionClose(){const e=new Error("WebSocket connection closed");for(const t of Object.values(this._callbacks))t(e);this._callbacks={}}_handleMessage(e){if(e.id){const t=this._callbacks[e.id];if(!t)return;e.error?t(!0,e.error):t(!1,e.result||{}),delete this._callbacks[e.id],0===Object.keys(this._callbacks).length&&this.emit("ready")}else if(e.method){const{method:t,params:r,sessionId:n}=e;this.emit("event",e),this.emit(t,r,n),this.emit(`${t}.${n}`,r,n)}}_enqueueCommand(e,t,r,n){const i=this._nextCommandId++,o={id:i,method:e,sessionId:r,params:t||{}};this._ws.send(JSON.stringify(o),(e=>{e?"function"==typeof n&&n(e):this._callbacks[i]=n}))}}},5372:e=>{"use strict";e.exports.HOST="localhost",e.exports.PORT=9222},7996:(e,t,r)=>{"use strict";const n=r(3423),i=r(8532),o=r(5372),a=r(4162);function s(e,t,r){const s=t.secure?i:n,p={method:t.method,host:t.host||o.HOST,port:t.port||o.PORT,useHostName:t.useHostName,path:t.alterPath?t.alterPath(e):e};a(s,p,r)}function p(e){return(t,r)=>("function"==typeof t&&(r=t,t=void 0),t=t||{},"function"==typeof r?void e(t,r):new Promise(((r,n)=>{e(t,((e,t)=>{e?n(e):r(t)}))})))}e.exports.Protocol=p((function(e,t){if(e.local){const e=r(4203);t(null,e)}else s("/json/protocol",e,((e,r)=>{e?t(e):t(null,JSON.parse(r))}))})),e.exports.List=p((function(e,t){s("/json/list",e,((e,r)=>{e?t(e):t(null,JSON.parse(r))}))})),e.exports.New=p((function(e,t){let r="/json/new";Object.prototype.hasOwnProperty.call(e,"url")&&(r+=`?${e.url}`),e.method=e.method||"PUT",s(r,e,((e,r)=>{e?t(e):t(null,JSON.parse(r))}))})),e.exports.Activate=p((function(e,t){s("/json/activate/"+e.id,e,(e=>{t(e||null)}))})),e.exports.Close=p((function(e,t){s("/json/close/"+e.id,e,(e=>{t(e||null)}))})),e.exports.Version=p((function(e,t){s("/json/version",e,((e,r)=>{e?t(e):t(null,JSON.parse(r))}))}))},5529:(e,t,r)=>{"use strict";const n=r(7187);e.exports=class extends n{constructor(e){super(),this._ws=new WebSocket(e),this._ws.onopen=()=>{this.emit("open")},this._ws.onclose=()=>{this.emit("close")},this._ws.onmessage=e=>{this.emit("message",e.data)},this._ws.onerror=()=>{this.emit("error",new Error("WebSocket error"))}}close(){this._ws.close()}send(e,t){try{this._ws.send(e),t()}catch(e){t(e)}}}},6124:(e,t,r)=>{"use strict";if(r(1934),r(5666),r(7694),r.g._babelPolyfill)throw new Error("only one instance of babel-polyfill is allowed");r.g._babelPolyfill=!0;function n(e,t,r){e[t]||Object.defineProperty(e,t,{writable:!0,configurable:!0,value:r})}n(String.prototype,"padLeft","".padStart),n(String.prototype,"padRight","".padEnd),"pop,reverse,shift,keys,values,entries,indexOf,every,some,forEach,map,filter,find,findIndex,includes,join,slice,concat,push,splice,unshift,sort,lastIndexOf,reduce,reduceRight,copyWithin,fill".split(",").forEach((function(e){[][e]&&n(Array,e,Function.call.bind([][e]))}))},1924:(e,t,r)=>{"use strict";var n=r(210),i=r(5559),o=i(n("String.prototype.indexOf"));e.exports=function(e,t){var r=n(e,!!t);return"function"==typeof r&&o(e,".prototype.")>-1?i(r):r}},5559:(e,t,r)=>{"use strict";var n=r(8612),i=r(210),o=i("%Function.prototype.apply%"),a=i("%Function.prototype.call%"),s=i("%Reflect.apply%",!0)||n.call(a,o),p=i("%Object.getOwnPropertyDescriptor%",!0),d=i("%Object.defineProperty%",!0),c=i("%Math.max%");if(d)try{d({},"a",{value:1})}catch(e){d=null}e.exports=function(e){var t=s(n,a,arguments);if(p&&d){var r=p(t,"length");r.configurable&&d(t,"length",{value:1+c(0,e.length-(arguments.length-1))})}return t};var l=function(){return s(n,o,arguments)};d?d(e.exports,"apply",{value:l}):e.exports.apply=l},7694:(e,t,r)=>{r(1761),e.exports=r(5645).RegExp.escape},4963:e=>{e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},3365:(e,t,r)=>{var n=r(2032);e.exports=function(e,t){if("number"!=typeof e&&"Number"!=n(e))throw TypeError(t);return+e}},7722:(e,t,r)=>{var n=r(6314)("unscopables"),i=Array.prototype;null==i[n]&&r(7728)(i,n,{}),e.exports=function(e){i[n][e]=!0}},6793:(e,t,r)=>{"use strict";var n=r(4496)(!0);e.exports=function(e,t,r){return t+(r?n(e,t).length:1)}},3328:e=>{e.exports=function(e,t,r,n){if(!(e instanceof t)||void 0!==n&&n in e)throw TypeError(r+": incorrect invocation!");return e}},7007:(e,t,r)=>{var n=r(5286);e.exports=function(e){if(!n(e))throw TypeError(e+" is not an object!");return e}},5216:(e,t,r)=>{"use strict";var n=r(508),i=r(2337),o=r(875);e.exports=[].copyWithin||function(e,t){var r=n(this),a=o(r.length),s=i(e,a),p=i(t,a),d=arguments.length>2?arguments[2]:void 0,c=Math.min((void 0===d?a:i(d,a))-p,a-s),l=1;for(p<s&&s<p+c&&(l=-1,p+=c-1,s+=c-1);c-- >0;)p in r?r[s]=r[p]:delete r[s],s+=l,p+=l;return r}},6852:(e,t,r)=>{"use strict";var n=r(508),i=r(2337),o=r(875);e.exports=function(e){for(var t=n(this),r=o(t.length),a=arguments.length,s=i(a>1?arguments[1]:void 0,r),p=a>2?arguments[2]:void 0,d=void 0===p?r:i(p,r);d>s;)t[s++]=e;return t}},9490:(e,t,r)=>{var n=r(3531);e.exports=function(e,t){var r=[];return n(e,!1,r.push,r,t),r}},9315:(e,t,r)=>{var n=r(2110),i=r(875),o=r(2337);e.exports=function(e){return function(t,r,a){var s,p=n(t),d=i(p.length),c=o(a,d);if(e&&r!=r){for(;d>c;)if((s=p[c++])!=s)return!0}else for(;d>c;c++)if((e||c in p)&&p[c]===r)return e||c||0;return!e&&-1}}},50:(e,t,r)=>{var n=r(741),i=r(9797),o=r(508),a=r(875),s=r(6886);e.exports=function(e,t){var r=1==e,p=2==e,d=3==e,c=4==e,l=6==e,u=5==e||l,m=t||s;return function(t,s,h){for(var f,y,g=o(t),b=i(g),v=n(s,h,3),w=a(b.length),S=0,I=r?m(t,w):p?m(t,0):void 0;w>S;S++)if((u||S in b)&&(y=v(f=b[S],S,g),e))if(r)I[S]=y;else if(y)switch(e){case 3:return!0;case 5:return f;case 6:return S;case 2:I.push(f)}else if(c)return!1;return l?-1:d||c?c:I}}},7628:(e,t,r)=>{var n=r(4963),i=r(508),o=r(9797),a=r(875);e.exports=function(e,t,r,s,p){n(t);var d=i(e),c=o(d),l=a(d.length),u=p?l-1:0,m=p?-1:1;if(r<2)for(;;){if(u in c){s=c[u],u+=m;break}if(u+=m,p?u<0:l<=u)throw TypeError("Reduce of empty array with no initial value")}for(;p?u>=0:l>u;u+=m)u in c&&(s=t(s,c[u],u,d));return s}},2736:(e,t,r)=>{var n=r(5286),i=r(4302),o=r(6314)("species");e.exports=function(e){var t;return i(e)&&("function"!=typeof(t=e.constructor)||t!==Array&&!i(t.prototype)||(t=void 0),n(t)&&null===(t=t[o])&&(t=void 0)),void 0===t?Array:t}},6886:(e,t,r)=>{var n=r(2736);e.exports=function(e,t){return new(n(e))(t)}},4398:(e,t,r)=>{"use strict";var n=r(4963),i=r(5286),o=r(7242),a=[].slice,s={},p=function(e,t,r){if(!(t in s)){for(var n=[],i=0;i<t;i++)n[i]="a["+i+"]";s[t]=Function("F,a","return new F("+n.join(",")+")")}return s[t](e,r)};e.exports=Function.bind||function(e){var t=n(this),r=a.call(arguments,1),s=function(){var n=r.concat(a.call(arguments));return this instanceof s?p(t,n.length,n):o(t,n,e)};return i(t.prototype)&&(s.prototype=t.prototype),s}},1488:(e,t,r)=>{var n=r(2032),i=r(6314)("toStringTag"),o="Arguments"==n(function(){return arguments}());e.exports=function(e){var t,r,a;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(r=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),i))?r:o?n(t):"Object"==(a=n(t))&&"function"==typeof t.callee?"Arguments":a}},2032:e=>{var t={}.toString;e.exports=function(e){return t.call(e).slice(8,-1)}},9824:(e,t,r)=>{"use strict";var n=r(9275).f,i=r(2503),o=r(4408),a=r(741),s=r(3328),p=r(3531),d=r(2923),c=r(5436),l=r(2974),u=r(7057),m=r(4728).fastKey,h=r(1616),f=u?"_s":"size",y=function(e,t){var r,n=m(t);if("F"!==n)return e._i[n];for(r=e._f;r;r=r.n)if(r.k==t)return r};e.exports={getConstructor:function(e,t,r,d){var c=e((function(e,n){s(e,c,t,"_i"),e._t=t,e._i=i(null),e._f=void 0,e._l=void 0,e[f]=0,null!=n&&p(n,r,e[d],e)}));return o(c.prototype,{clear:function(){for(var e=h(this,t),r=e._i,n=e._f;n;n=n.n)n.r=!0,n.p&&(n.p=n.p.n=void 0),delete r[n.i];e._f=e._l=void 0,e[f]=0},delete:function(e){var r=h(this,t),n=y(r,e);if(n){var i=n.n,o=n.p;delete r._i[n.i],n.r=!0,o&&(o.n=i),i&&(i.p=o),r._f==n&&(r._f=i),r._l==n&&(r._l=o),r[f]--}return!!n},forEach:function(e){h(this,t);for(var r,n=a(e,arguments.length>1?arguments[1]:void 0,3);r=r?r.n:this._f;)for(n(r.v,r.k,this);r&&r.r;)r=r.p},has:function(e){return!!y(h(this,t),e)}}),u&&n(c.prototype,"size",{get:function(){return h(this,t)[f]}}),c},def:function(e,t,r){var n,i,o=y(e,t);return o?o.v=r:(e._l=o={i:i=m(t,!0),k:t,v:r,p:n=e._l,n:void 0,r:!1},e._f||(e._f=o),n&&(n.n=o),e[f]++,"F"!==i&&(e._i[i]=o)),e},getEntry:y,setStrong:function(e,t,r){d(e,t,(function(e,r){this._t=h(e,t),this._k=r,this._l=void 0}),(function(){for(var e=this,t=e._k,r=e._l;r&&r.r;)r=r.p;return e._t&&(e._l=r=r?r.n:e._t._f)?c(0,"keys"==t?r.k:"values"==t?r.v:[r.k,r.v]):(e._t=void 0,c(1))}),r?"entries":"values",!r,!0),l(t)}}},6132:(e,t,r)=>{var n=r(1488),i=r(9490);e.exports=function(e){return function(){if(n(this)!=e)throw TypeError(e+"#toJSON isn't generic");return i(this)}}},3657:(e,t,r)=>{"use strict";var n=r(4408),i=r(4728).getWeak,o=r(7007),a=r(5286),s=r(3328),p=r(3531),d=r(50),c=r(9181),l=r(1616),u=d(5),m=d(6),h=0,f=function(e){return e._l||(e._l=new y)},y=function(){this.a=[]},g=function(e,t){return u(e.a,(function(e){return e[0]===t}))};y.prototype={get:function(e){var t=g(this,e);if(t)return t[1]},has:function(e){return!!g(this,e)},set:function(e,t){var r=g(this,e);r?r[1]=t:this.a.push([e,t])},delete:function(e){var t=m(this.a,(function(t){return t[0]===e}));return~t&&this.a.splice(t,1),!!~t}},e.exports={getConstructor:function(e,t,r,o){var d=e((function(e,n){s(e,d,t,"_i"),e._t=t,e._i=h++,e._l=void 0,null!=n&&p(n,r,e[o],e)}));return n(d.prototype,{delete:function(e){if(!a(e))return!1;var r=i(e);return!0===r?f(l(this,t)).delete(e):r&&c(r,this._i)&&delete r[this._i]},has:function(e){if(!a(e))return!1;var r=i(e);return!0===r?f(l(this,t)).has(e):r&&c(r,this._i)}}),d},def:function(e,t,r){var n=i(o(t),!0);return!0===n?f(e).set(t,r):n[e._i]=r,e},ufstore:f}},5795:(e,t,r)=>{"use strict";var n=r(3816),i=r(2985),o=r(7234),a=r(4408),s=r(4728),p=r(3531),d=r(3328),c=r(5286),l=r(4253),u=r(7462),m=r(2943),h=r(266);e.exports=function(e,t,r,f,y,g){var b=n[e],v=b,w=y?"set":"add",S=v&&v.prototype,I={},x=function(e){var t=S[e];o(S,e,"delete"==e||"has"==e?function(e){return!(g&&!c(e))&&t.call(this,0===e?0:e)}:"get"==e?function(e){return g&&!c(e)?void 0:t.call(this,0===e?0:e)}:"add"==e?function(e){return t.call(this,0===e?0:e),this}:function(e,r){return t.call(this,0===e?0:e,r),this})};if("function"==typeof v&&(g||S.forEach&&!l((function(){(new v).entries().next()})))){var k=new v,T=k[w](g?{}:-0,1)!=k,R=l((function(){k.has(1)})),C=u((function(e){new v(e)})),$=!g&&l((function(){for(var e=new v,t=5;t--;)e[w](t,t);return!e.has(-0)}));C||((v=t((function(t,r){d(t,v,e);var n=h(new b,t,v);return null!=r&&p(r,y,n[w],n),n}))).prototype=S,S.constructor=v),(R||$)&&(x("delete"),x("has"),y&&x("get")),($||T)&&x(w),g&&S.clear&&delete S.clear}else v=f.getConstructor(t,e,y,w),a(v.prototype,r),s.NEED=!0;return m(v,e),I[e]=v,i(i.G+i.W+i.F*(v!=b),I),g||f.setStrong(v,e,y),v}},5645:e=>{var t=e.exports={version:"2.6.12"};"number"==typeof __e&&(__e=t)},2811:(e,t,r)=>{"use strict";var n=r(9275),i=r(681);e.exports=function(e,t,r){t in e?n.f(e,t,i(0,r)):e[t]=r}},741:(e,t,r)=>{var n=r(4963);e.exports=function(e,t,r){if(n(e),void 0===t)return e;switch(r){case 1:return function(r){return e.call(t,r)};case 2:return function(r,n){return e.call(t,r,n)};case 3:return function(r,n,i){return e.call(t,r,n,i)}}return function(){return e.apply(t,arguments)}}},3537:(e,t,r)=>{"use strict";var n=r(4253),i=Date.prototype.getTime,o=Date.prototype.toISOString,a=function(e){return e>9?e:"0"+e};e.exports=n((function(){return"0385-07-25T07:06:39.999Z"!=o.call(new Date(-50000000000001))}))||!n((function(){o.call(new Date(NaN))}))?function(){if(!isFinite(i.call(this)))throw RangeError("Invalid time value");var e=this,t=e.getUTCFullYear(),r=e.getUTCMilliseconds(),n=t<0?"-":t>9999?"+":"";return n+("00000"+Math.abs(t)).slice(n?-6:-4)+"-"+a(e.getUTCMonth()+1)+"-"+a(e.getUTCDate())+"T"+a(e.getUTCHours())+":"+a(e.getUTCMinutes())+":"+a(e.getUTCSeconds())+"."+(r>99?r:"0"+a(r))+"Z"}:o},870:(e,t,r)=>{"use strict";var n=r(7007),i=r(1689),o="number";e.exports=function(e){if("string"!==e&&e!==o&&"default"!==e)throw TypeError("Incorrect hint");return i(n(this),e!=o)}},1355:e=>{e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},7057:(e,t,r)=>{e.exports=!r(4253)((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a}))},2457:(e,t,r)=>{var n=r(5286),i=r(3816).document,o=n(i)&&n(i.createElement);e.exports=function(e){return o?i.createElement(e):{}}},4430:e=>{e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},5541:(e,t,r)=>{var n=r(7184),i=r(4548),o=r(4682);e.exports=function(e){var t=n(e),r=i.f;if(r)for(var a,s=r(e),p=o.f,d=0;s.length>d;)p.call(e,a=s[d++])&&t.push(a);return t}},2985:(e,t,r)=>{var n=r(3816),i=r(5645),o=r(7728),a=r(7234),s=r(741),p=function(e,t,r){var d,c,l,u,m=e&p.F,h=e&p.G,f=e&p.S,y=e&p.P,g=e&p.B,b=h?n:f?n[t]||(n[t]={}):(n[t]||{}).prototype,v=h?i:i[t]||(i[t]={}),w=v.prototype||(v.prototype={});for(d in h&&(r=t),r)l=((c=!m&&b&&void 0!==b[d])?b:r)[d],u=g&&c?s(l,n):y&&"function"==typeof l?s(Function.call,l):l,b&&a(b,d,l,e&p.U),v[d]!=l&&o(v,d,u),y&&w[d]!=l&&(w[d]=l)};n.core=i,p.F=1,p.G=2,p.S=4,p.P=8,p.B=16,p.W=32,p.U=64,p.R=128,e.exports=p},8852:(e,t,r)=>{var n=r(6314)("match");e.exports=function(e){var t=/./;try{"/./"[e](t)}catch(r){try{return t[n]=!1,!"/./"[e](t)}catch(e){}}return!0}},4253:e=>{e.exports=function(e){try{return!!e()}catch(e){return!0}}},8082:(e,t,r)=>{"use strict";r(8269);var n=r(7234),i=r(7728),o=r(4253),a=r(1355),s=r(6314),p=r(1165),d=s("species"),c=!o((function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$<a>")})),l=function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var r="ab".split(e);return 2===r.length&&"a"===r[0]&&"b"===r[1]}();e.exports=function(e,t,r){var u=s(e),m=!o((function(){var t={};return t[u]=function(){return 7},7!=""[e](t)})),h=m?!o((function(){var t=!1,r=/a/;return r.exec=function(){return t=!0,null},"split"===e&&(r.constructor={},r.constructor[d]=function(){return r}),r[u](""),!t})):void 0;if(!m||!h||"replace"===e&&!c||"split"===e&&!l){var f=/./[u],y=r(a,u,""[e],(function(e,t,r,n,i){return t.exec===p?m&&!i?{done:!0,value:f.call(t,r,n)}:{done:!0,value:e.call(r,t,n)}:{done:!1}})),g=y[0],b=y[1];n(String.prototype,e,g),i(RegExp.prototype,u,2==t?function(e,t){return b.call(e,this,t)}:function(e){return b.call(e,this)})}}},3218:(e,t,r)=>{"use strict";var n=r(7007);e.exports=function(){var e=n(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t}},3325:(e,t,r)=>{"use strict";var n=r(4302),i=r(5286),o=r(875),a=r(741),s=r(6314)("isConcatSpreadable");e.exports=function e(t,r,p,d,c,l,u,m){for(var h,f,y=c,g=0,b=!!u&&a(u,m,3);g<d;){if(g in p){if(h=b?b(p[g],g,r):p[g],f=!1,i(h)&&(f=void 0!==(f=h[s])?!!f:n(h)),f&&l>0)y=e(t,r,h,o(h.length),y,l-1)-1;else{if(y>=9007199254740991)throw TypeError();t[y]=h}y++}g++}return y}},3531:(e,t,r)=>{var n=r(741),i=r(8851),o=r(6555),a=r(7007),s=r(875),p=r(9002),d={},c={},l=e.exports=function(e,t,r,l,u){var m,h,f,y,g=u?function(){return e}:p(e),b=n(r,l,t?2:1),v=0;if("function"!=typeof g)throw TypeError(e+" is not iterable!");if(o(g)){for(m=s(e.length);m>v;v++)if((y=t?b(a(h=e[v])[0],h[1]):b(e[v]))===d||y===c)return y}else for(f=g.call(e);!(h=f.next()).done;)if((y=i(f,b,h.value,t))===d||y===c)return y};l.BREAK=d,l.RETURN=c},18:(e,t,r)=>{e.exports=r(3825)("native-function-to-string",Function.toString)},3816:e=>{var t=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=t)},9181:e=>{var t={}.hasOwnProperty;e.exports=function(e,r){return t.call(e,r)}},7728:(e,t,r)=>{var n=r(9275),i=r(681);e.exports=r(7057)?function(e,t,r){return n.f(e,t,i(1,r))}:function(e,t,r){return e[t]=r,e}},639:(e,t,r)=>{var n=r(3816).document;e.exports=n&&n.documentElement},1734:(e,t,r)=>{e.exports=!r(7057)&&!r(4253)((function(){return 7!=Object.defineProperty(r(2457)("div"),"a",{get:function(){return 7}}).a}))},266:(e,t,r)=>{var n=r(5286),i=r(7375).set;e.exports=function(e,t,r){var o,a=t.constructor;return a!==r&&"function"==typeof a&&(o=a.prototype)!==r.prototype&&n(o)&&i&&i(e,o),e}},7242:e=>{e.exports=function(e,t,r){var n=void 0===r;switch(t.length){case 0:return n?e():e.call(r);case 1:return n?e(t[0]):e.call(r,t[0]);case 2:return n?e(t[0],t[1]):e.call(r,t[0],t[1]);case 3:return n?e(t[0],t[1],t[2]):e.call(r,t[0],t[1],t[2]);case 4:return n?e(t[0],t[1],t[2],t[3]):e.call(r,t[0],t[1],t[2],t[3])}return e.apply(r,t)}},9797:(e,t,r)=>{var n=r(2032);e.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==n(e)?e.split(""):Object(e)}},6555:(e,t,r)=>{var n=r(2803),i=r(6314)("iterator"),o=Array.prototype;e.exports=function(e){return void 0!==e&&(n.Array===e||o[i]===e)}},4302:(e,t,r)=>{var n=r(2032);e.exports=Array.isArray||function(e){return"Array"==n(e)}},8367:(e,t,r)=>{var n=r(5286),i=Math.floor;e.exports=function(e){return!n(e)&&isFinite(e)&&i(e)===e}},5286:e=>{e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},5364:(e,t,r)=>{var n=r(5286),i=r(2032),o=r(6314)("match");e.exports=function(e){var t;return n(e)&&(void 0!==(t=e[o])?!!t:"RegExp"==i(e))}},8851:(e,t,r)=>{var n=r(7007);e.exports=function(e,t,r,i){try{return i?t(n(r)[0],r[1]):t(r)}catch(t){var o=e.return;throw void 0!==o&&n(o.call(e)),t}}},9988:(e,t,r)=>{"use strict";var n=r(2503),i=r(681),o=r(2943),a={};r(7728)(a,r(6314)("iterator"),(function(){return this})),e.exports=function(e,t,r){e.prototype=n(a,{next:i(1,r)}),o(e,t+" Iterator")}},2923:(e,t,r)=>{"use strict";var n=r(4461),i=r(2985),o=r(7234),a=r(7728),s=r(2803),p=r(9988),d=r(2943),c=r(468),l=r(6314)("iterator"),u=!([].keys&&"next"in[].keys()),m="keys",h="values",f=function(){return this};e.exports=function(e,t,r,y,g,b,v){p(r,t,y);var w,S,I,x=function(e){if(!u&&e in C)return C[e];switch(e){case m:case h:return function(){return new r(this,e)}}return function(){return new r(this,e)}},k=t+" Iterator",T=g==h,R=!1,C=e.prototype,$=C[l]||C["@@iterator"]||g&&C[g],A=$||x(g),O=g?T?x("entries"):A:void 0,P="Array"==t&&C.entries||$;if(P&&(I=c(P.call(new e)))!==Object.prototype&&I.next&&(d(I,k,!0),n||"function"==typeof I[l]||a(I,l,f)),T&&$&&$.name!==h&&(R=!0,A=function(){return $.call(this)}),n&&!v||!u&&!R&&C[l]||a(C,l,A),s[t]=A,s[k]=f,g)if(w={values:T?A:x(h),keys:b?A:x(m),entries:O},v)for(S in w)S in C||o(C,S,w[S]);else i(i.P+i.F*(u||R),t,w);return w}},7462:(e,t,r)=>{var n=r(6314)("iterator"),i=!1;try{var o=[7][n]();o.return=function(){i=!0},Array.from(o,(function(){throw 2}))}catch(e){}e.exports=function(e,t){if(!t&&!i)return!1;var r=!1;try{var o=[7],a=o[n]();a.next=function(){return{done:r=!0}},o[n]=function(){return a},e(o)}catch(e){}return r}},5436:e=>{e.exports=function(e,t){return{value:t,done:!!e}}},2803:e=>{e.exports={}},4461:e=>{e.exports=!1},3086:e=>{var t=Math.expm1;e.exports=!t||t(10)>22025.465794806718||t(10)<22025.465794806718||-2e-17!=t(-2e-17)?function(e){return 0==(e=+e)?e:e>-1e-6&&e<1e-6?e+e*e/2:Math.exp(e)-1}:t},4934:(e,t,r)=>{var n=r(1801),i=Math.pow,o=i(2,-52),a=i(2,-23),s=i(2,127)*(2-a),p=i(2,-126);e.exports=Math.fround||function(e){var t,r,i=Math.abs(e),d=n(e);return i<p?d*(i/p/a+1/o-1/o)*p*a:(r=(t=(1+a/o)*i)-(t-i))>s||r!=r?d*(1/0):d*r}},6206:e=>{e.exports=Math.log1p||function(e){return(e=+e)>-1e-8&&e<1e-8?e-e*e/2:Math.log(1+e)}},8757:e=>{e.exports=Math.scale||function(e,t,r,n,i){return 0===arguments.length||e!=e||t!=t||r!=r||n!=n||i!=i?NaN:e===1/0||e===-1/0?e:(e-t)*(i-n)/(r-t)+n}},1801:e=>{e.exports=Math.sign||function(e){return 0==(e=+e)||e!=e?e:e<0?-1:1}},4728:(e,t,r)=>{var n=r(3953)("meta"),i=r(5286),o=r(9181),a=r(9275).f,s=0,p=Object.isExtensible||function(){return!0},d=!r(4253)((function(){return p(Object.preventExtensions({}))})),c=function(e){a(e,n,{value:{i:"O"+ ++s,w:{}}})},l=e.exports={KEY:n,NEED:!1,fastKey:function(e,t){if(!i(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!o(e,n)){if(!p(e))return"F";if(!t)return"E";c(e)}return e[n].i},getWeak:function(e,t){if(!o(e,n)){if(!p(e))return!0;if(!t)return!1;c(e)}return e[n].w},onFreeze:function(e){return d&&l.NEED&&p(e)&&!o(e,n)&&c(e),e}}},133:(e,t,r)=>{var n=r(8416),i=r(2985),o=r(3825)("metadata"),a=o.store||(o.store=new(r(147))),s=function(e,t,r){var i=a.get(e);if(!i){if(!r)return;a.set(e,i=new n)}var o=i.get(t);if(!o){if(!r)return;i.set(t,o=new n)}return o};e.exports={store:a,map:s,has:function(e,t,r){var n=s(t,r,!1);return void 0!==n&&n.has(e)},get:function(e,t,r){var n=s(t,r,!1);return void 0===n?void 0:n.get(e)},set:function(e,t,r,n){s(r,n,!0).set(e,t)},keys:function(e,t){var r=s(e,t,!1),n=[];return r&&r.forEach((function(e,t){n.push(t)})),n},key:function(e){return void 0===e||"symbol"==typeof e?e:String(e)},exp:function(e){i(i.S,"Reflect",e)}}},4351:(e,t,r)=>{var n=r(3816),i=r(4193).set,o=n.MutationObserver||n.WebKitMutationObserver,a=n.process,s=n.Promise,p="process"==r(2032)(a);e.exports=function(){var e,t,r,d=function(){var n,i;for(p&&(n=a.domain)&&n.exit();e;){i=e.fn,e=e.next;try{i()}catch(n){throw e?r():t=void 0,n}}t=void 0,n&&n.enter()};if(p)r=function(){a.nextTick(d)};else if(!o||n.navigator&&n.navigator.standalone)if(s&&s.resolve){var c=s.resolve(void 0);r=function(){c.then(d)}}else r=function(){i.call(n,d)};else{var l=!0,u=document.createTextNode("");new o(d).observe(u,{characterData:!0}),r=function(){u.data=l=!l}}return function(n){var i={fn:n,next:void 0};t&&(t.next=i),e||(e=i,r()),t=i}}},3499:(e,t,r)=>{"use strict";var n=r(4963);function i(e){var t,r;this.promise=new e((function(e,n){if(void 0!==t||void 0!==r)throw TypeError("Bad Promise constructor");t=e,r=n})),this.resolve=n(t),this.reject=n(r)}e.exports.f=function(e){return new i(e)}},5345:(e,t,r)=>{"use strict";var n=r(7057),i=r(7184),o=r(4548),a=r(4682),s=r(508),p=r(9797),d=Object.assign;e.exports=!d||r(4253)((function(){var e={},t={},r=Symbol(),n="abcdefghijklmnopqrst";return e[r]=7,n.split("").forEach((function(e){t[e]=e})),7!=d({},e)[r]||Object.keys(d({},t)).join("")!=n}))?function(e,t){for(var r=s(e),d=arguments.length,c=1,l=o.f,u=a.f;d>c;)for(var m,h=p(arguments[c++]),f=l?i(h).concat(l(h)):i(h),y=f.length,g=0;y>g;)m=f[g++],n&&!u.call(h,m)||(r[m]=h[m]);return r}:d},2503:(e,t,r)=>{var n=r(7007),i=r(5588),o=r(4430),a=r(9335)("IE_PROTO"),s=function(){},p=function(){var e,t=r(2457)("iframe"),n=o.length;for(t.style.display="none",r(639).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write("<script>document.F=Object<\/script>"),e.close(),p=e.F;n--;)delete p.prototype[o[n]];return p()};e.exports=Object.create||function(e,t){var r;return null!==e?(s.prototype=n(e),r=new s,s.prototype=null,r[a]=e):r=p(),void 0===t?r:i(r,t)}},9275:(e,t,r)=>{var n=r(7007),i=r(1734),o=r(1689),a=Object.defineProperty;t.f=r(7057)?Object.defineProperty:function(e,t,r){if(n(e),t=o(t,!0),n(r),i)try{return a(e,t,r)}catch(e){}if("get"in r||"set"in r)throw TypeError("Accessors not supported!");return"value"in r&&(e[t]=r.value),e}},5588:(e,t,r)=>{var n=r(9275),i=r(7007),o=r(7184);e.exports=r(7057)?Object.defineProperties:function(e,t){i(e);for(var r,a=o(t),s=a.length,p=0;s>p;)n.f(e,r=a[p++],t[r]);return e}},1670:(e,t,r)=>{"use strict";e.exports=r(4461)||!r(4253)((function(){var e=Math.random();__defineSetter__.call(null,e,(function(){})),delete r(3816)[e]}))},8693:(e,t,r)=>{var n=r(4682),i=r(681),o=r(2110),a=r(1689),s=r(9181),p=r(1734),d=Object.getOwnPropertyDescriptor;t.f=r(7057)?d:function(e,t){if(e=o(e),t=a(t,!0),p)try{return d(e,t)}catch(e){}if(s(e,t))return i(!n.f.call(e,t),e[t])}},9327:(e,t,r)=>{var n=r(2110),i=r(616).f,o={}.toString,a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];e.exports.f=function(e){return a&&"[object Window]"==o.call(e)?function(e){try{return i(e)}catch(e){return a.slice()}}(e):i(n(e))}},616:(e,t,r)=>{var n=r(189),i=r(4430).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return n(e,i)}},4548:(e,t)=>{t.f=Object.getOwnPropertySymbols},468:(e,t,r)=>{var n=r(9181),i=r(508),o=r(9335)("IE_PROTO"),a=Object.prototype;e.exports=Object.getPrototypeOf||function(e){return e=i(e),n(e,o)?e[o]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?a:null}},189:(e,t,r)=>{var n=r(9181),i=r(2110),o=r(9315)(!1),a=r(9335)("IE_PROTO");e.exports=function(e,t){var r,s=i(e),p=0,d=[];for(r in s)r!=a&&n(s,r)&&d.push(r);for(;t.length>p;)n(s,r=t[p++])&&(~o(d,r)||d.push(r));return d}},7184:(e,t,r)=>{var n=r(189),i=r(4430);e.exports=Object.keys||function(e){return n(e,i)}},4682:(e,t)=>{t.f={}.propertyIsEnumerable},3160:(e,t,r)=>{var n=r(2985),i=r(5645),o=r(4253);e.exports=function(e,t){var r=(i.Object||{})[e]||Object[e],a={};a[e]=t(r),n(n.S+n.F*o((function(){r(1)})),"Object",a)}},1131:(e,t,r)=>{var n=r(7057),i=r(7184),o=r(2110),a=r(4682).f;e.exports=function(e){return function(t){for(var r,s=o(t),p=i(s),d=p.length,c=0,l=[];d>c;)r=p[c++],n&&!a.call(s,r)||l.push(e?[r,s[r]]:s[r]);return l}}},7643:(e,t,r)=>{var n=r(616),i=r(4548),o=r(7007),a=r(3816).Reflect;e.exports=a&&a.ownKeys||function(e){var t=n.f(o(e)),r=i.f;return r?t.concat(r(e)):t}},7743:(e,t,r)=>{var n=r(3816).parseFloat,i=r(9599).trim;e.exports=1/n(r(4644)+"-0")!=-1/0?function(e){var t=i(String(e),3),r=n(t);return 0===r&&"-"==t.charAt(0)?-0:r}:n},5960:(e,t,r)=>{var n=r(3816).parseInt,i=r(9599).trim,o=r(4644),a=/^[-+]?0[xX]/;e.exports=8!==n(o+"08")||22!==n(o+"0x16")?function(e,t){var r=i(String(e),3);return n(r,t>>>0||(a.test(r)?16:10))}:n},188:e=>{e.exports=function(e){try{return{e:!1,v:e()}}catch(e){return{e:!0,v:e}}}},94:(e,t,r)=>{var n=r(7007),i=r(5286),o=r(3499);e.exports=function(e,t){if(n(e),i(t)&&t.constructor===e)return t;var r=o.f(e);return(0,r.resolve)(t),r.promise}},681:e=>{e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},4408:(e,t,r)=>{var n=r(7234);e.exports=function(e,t,r){for(var i in t)n(e,i,t[i],r);return e}},7234:(e,t,r)=>{var n=r(3816),i=r(7728),o=r(9181),a=r(3953)("src"),s=r(18),p="toString",d=(""+s).split(p);r(5645).inspectSource=function(e){return s.call(e)},(e.exports=function(e,t,r,s){var p="function"==typeof r;p&&(o(r,"name")||i(r,"name",t)),e[t]!==r&&(p&&(o(r,a)||i(r,a,e[t]?""+e[t]:d.join(String(t)))),e===n?e[t]=r:s?e[t]?e[t]=r:i(e,t,r):(delete e[t],i(e,t,r)))})(Function.prototype,p,(function(){return"function"==typeof this&&this[a]||s.call(this)}))},7787:(e,t,r)=>{"use strict";var n=r(1488),i=RegExp.prototype.exec;e.exports=function(e,t){var r=e.exec;if("function"==typeof r){var o=r.call(e,t);if("object"!=typeof o)throw new TypeError("RegExp exec method returned something other than an Object or null");return o}if("RegExp"!==n(e))throw new TypeError("RegExp#exec called on incompatible receiver");return i.call(e,t)}},1165:(e,t,r)=>{"use strict";var n,i,o=r(3218),a=RegExp.prototype.exec,s=String.prototype.replace,p=a,d=(n=/a/,i=/b*/g,a.call(n,"a"),a.call(i,"a"),0!==n.lastIndex||0!==i.lastIndex),c=void 0!==/()??/.exec("")[1];(d||c)&&(p=function(e){var t,r,n,i,p=this;return c&&(r=new RegExp("^"+p.source+"$(?!\\s)",o.call(p))),d&&(t=p.lastIndex),n=a.call(p,e),d&&n&&(p.lastIndex=p.global?n.index+n[0].length:t),c&&n&&n.length>1&&s.call(n[0],r,(function(){for(i=1;i<arguments.length-2;i++)void 0===arguments[i]&&(n[i]=void 0)})),n}),e.exports=p},5496:e=>{e.exports=function(e,t){var r=t===Object(t)?function(e){return t[e]}:t;return function(t){return String(t).replace(e,r)}}},7195:e=>{e.exports=Object.is||function(e,t){return e===t?0!==e||1/e==1/t:e!=e&&t!=t}},1024:(e,t,r)=>{"use strict";var n=r(2985),i=r(4963),o=r(741),a=r(3531);e.exports=function(e){n(n.S,e,{from:function(e){var t,r,n,s,p=arguments[1];return i(this),(t=void 0!==p)&&i(p),null==e?new this:(r=[],t?(n=0,s=o(p,arguments[2],2),a(e,!1,(function(e){r.push(s(e,n++))}))):a(e,!1,r.push,r),new this(r))}})}},4881:(e,t,r)=>{"use strict";var n=r(2985);e.exports=function(e){n(n.S,e,{of:function(){for(var e=arguments.length,t=new Array(e);e--;)t[e]=arguments[e];return new this(t)}})}},7375:(e,t,r)=>{var n=r(5286),i=r(7007),o=function(e,t){if(i(e),!n(t)&&null!==t)throw TypeError(t+": can't set as prototype!")};e.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(e,t,n){try{(n=r(741)(Function.call,r(8693).f(Object.prototype,"__proto__").set,2))(e,[]),t=!(e instanceof Array)}catch(e){t=!0}return function(e,r){return o(e,r),t?e.__proto__=r:n(e,r),e}}({},!1):void 0),check:o}},2974:(e,t,r)=>{"use strict";var n=r(3816),i=r(9275),o=r(7057),a=r(6314)("species");e.exports=function(e){var t=n[e];o&&t&&!t[a]&&i.f(t,a,{configurable:!0,get:function(){return this}})}},2943:(e,t,r)=>{var n=r(9275).f,i=r(9181),o=r(6314)("toStringTag");e.exports=function(e,t,r){e&&!i(e=r?e:e.prototype,o)&&n(e,o,{configurable:!0,value:t})}},9335:(e,t,r)=>{var n=r(3825)("keys"),i=r(3953);e.exports=function(e){return n[e]||(n[e]=i(e))}},3825:(e,t,r)=>{var n=r(5645),i=r(3816),o="__core-js_shared__",a=i[o]||(i[o]={});(e.exports=function(e,t){return a[e]||(a[e]=void 0!==t?t:{})})("versions",[]).push({version:n.version,mode:r(4461)?"pure":"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})},8364:(e,t,r)=>{var n=r(7007),i=r(4963),o=r(6314)("species");e.exports=function(e,t){var r,a=n(e).constructor;return void 0===a||null==(r=n(a)[o])?t:i(r)}},7717:(e,t,r)=>{"use strict";var n=r(4253);e.exports=function(e,t){return!!e&&n((function(){t?e.call(null,(function(){}),1):e.call(null)}))}},4496:(e,t,r)=>{var n=r(1467),i=r(1355);e.exports=function(e){return function(t,r){var o,a,s=String(i(t)),p=n(r),d=s.length;return p<0||p>=d?e?"":void 0:(o=s.charCodeAt(p))<55296||o>56319||p+1===d||(a=s.charCodeAt(p+1))<56320||a>57343?e?s.charAt(p):o:e?s.slice(p,p+2):a-56320+(o-55296<<10)+65536}}},2094:(e,t,r)=>{var n=r(5364),i=r(1355);e.exports=function(e,t,r){if(n(t))throw TypeError("String#"+r+" doesn't accept regex!");return String(i(e))}},9395:(e,t,r)=>{var n=r(2985),i=r(4253),o=r(1355),a=/"/g,s=function(e,t,r,n){var i=String(o(e)),s="<"+t;return""!==r&&(s+=" "+r+'="'+String(n).replace(a,"&quot;")+'"'),s+">"+i+"</"+t+">"};e.exports=function(e,t){var r={};r[e]=t(s),n(n.P+n.F*i((function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3})),"String",r)}},5442:(e,t,r)=>{var n=r(875),i=r(8595),o=r(1355);e.exports=function(e,t,r,a){var s=String(o(e)),p=s.length,d=void 0===r?" ":String(r),c=n(t);if(c<=p||""==d)return s;var l=c-p,u=i.call(d,Math.ceil(l/d.length));return u.length>l&&(u=u.slice(0,l)),a?u+s:s+u}},8595:(e,t,r)=>{"use strict";var n=r(1467),i=r(1355);e.exports=function(e){var t=String(i(this)),r="",o=n(e);if(o<0||o==1/0)throw RangeError("Count can't be negative");for(;o>0;(o>>>=1)&&(t+=t))1&o&&(r+=t);return r}},9599:(e,t,r)=>{var n=r(2985),i=r(1355),o=r(4253),a=r(4644),s="["+a+"]",p=RegExp("^"+s+s+"*"),d=RegExp(s+s+"*$"),c=function(e,t,r){var i={},s=o((function(){return!!a[e]()||"​…"!="​…"[e]()})),p=i[e]=s?t(l):a[e];r&&(i[r]=p),n(n.P+n.F*s,"String",i)},l=c.trim=function(e,t){return e=String(i(e)),1&t&&(e=e.replace(p,"")),2&t&&(e=e.replace(d,"")),e};e.exports=c},4644:e=>{e.exports="\t\n\v\f\r   ᠎              \u2028\u2029\ufeff"},4193:(e,t,r)=>{var n,i,o,a=r(741),s=r(7242),p=r(639),d=r(2457),c=r(3816),l=c.process,u=c.setImmediate,m=c.clearImmediate,h=c.MessageChannel,f=c.Dispatch,y=0,g={},b="onreadystatechange",v=function(){var e=+this;if(g.hasOwnProperty(e)){var t=g[e];delete g[e],t()}},w=function(e){v.call(e.data)};u&&m||(u=function(e){for(var t=[],r=1;arguments.length>r;)t.push(arguments[r++]);return g[++y]=function(){s("function"==typeof e?e:Function(e),t)},n(y),y},m=function(e){delete g[e]},"process"==r(2032)(l)?n=function(e){l.nextTick(a(v,e,1))}:f&&f.now?n=function(e){f.now(a(v,e,1))}:h?(o=(i=new h).port2,i.port1.onmessage=w,n=a(o.postMessage,o,1)):c.addEventListener&&"function"==typeof postMessage&&!c.importScripts?(n=function(e){c.postMessage(e+"","*")},c.addEventListener("message",w,!1)):n=b in d("script")?function(e){p.appendChild(d("script")).onreadystatechange=function(){p.removeChild(this),v.call(e)}}:function(e){setTimeout(a(v,e,1),0)}),e.exports={set:u,clear:m}},2337:(e,t,r)=>{var n=r(1467),i=Math.max,o=Math.min;e.exports=function(e,t){return(e=n(e))<0?i(e+t,0):o(e,t)}},4843:(e,t,r)=>{var n=r(1467),i=r(875);e.exports=function(e){if(void 0===e)return 0;var t=n(e),r=i(t);if(t!==r)throw RangeError("Wrong length!");return r}},1467:e=>{var t=Math.ceil,r=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?r:t)(e)}},2110:(e,t,r)=>{var n=r(9797),i=r(1355);e.exports=function(e){return n(i(e))}},875:(e,t,r)=>{var n=r(1467),i=Math.min;e.exports=function(e){return e>0?i(n(e),9007199254740991):0}},508:(e,t,r)=>{var n=r(1355);e.exports=function(e){return Object(n(e))}},1689:(e,t,r)=>{var n=r(5286);e.exports=function(e,t){if(!n(e))return e;var r,i;if(t&&"function"==typeof(r=e.toString)&&!n(i=r.call(e)))return i;if("function"==typeof(r=e.valueOf)&&!n(i=r.call(e)))return i;if(!t&&"function"==typeof(r=e.toString)&&!n(i=r.call(e)))return i;throw TypeError("Can't convert object to primitive value")}},8440:(e,t,r)=>{"use strict";if(r(7057)){var n=r(4461),i=r(3816),o=r(4253),a=r(2985),s=r(9383),p=r(1125),d=r(741),c=r(3328),l=r(681),u=r(7728),m=r(4408),h=r(1467),f=r(875),y=r(4843),g=r(2337),b=r(1689),v=r(9181),w=r(1488),S=r(5286),I=r(508),x=r(6555),k=r(2503),T=r(468),R=r(616).f,C=r(9002),$=r(3953),A=r(6314),O=r(50),P=r(9315),D=r(8364),N=r(6997),j=r(2803),E=r(7462),M=r(2974),F=r(6852),q=r(5216),L=r(9275),B=r(8693),U=L.f,W=B.f,H=i.RangeError,_=i.TypeError,V=i.Uint8Array,G="ArrayBuffer",z="SharedArrayBuffer",J="BYTES_PER_ELEMENT",K=Array.prototype,X=p.ArrayBuffer,Y=p.DataView,Q=O(0),Z=O(2),ee=O(3),te=O(4),re=O(5),ne=O(6),ie=P(!0),oe=P(!1),ae=N.values,se=N.keys,pe=N.entries,de=K.lastIndexOf,ce=K.reduce,le=K.reduceRight,ue=K.join,me=K.sort,he=K.slice,fe=K.toString,ye=K.toLocaleString,ge=A("iterator"),be=A("toStringTag"),ve=$("typed_constructor"),we=$("def_constructor"),Se=s.CONSTR,Ie=s.TYPED,xe=s.VIEW,ke="Wrong length!",Te=O(1,(function(e,t){return Oe(D(e,e[we]),t)})),Re=o((function(){return 1===new V(new Uint16Array([1]).buffer)[0]})),Ce=!!V&&!!V.prototype.set&&o((function(){new V(1).set({})})),$e=function(e,t){var r=h(e);if(r<0||r%t)throw H("Wrong offset!");return r},Ae=function(e){if(S(e)&&Ie in e)return e;throw _(e+" is not a typed array!")},Oe=function(e,t){if(!S(e)||!(ve in e))throw _("It is not a typed array constructor!");return new e(t)},Pe=function(e,t){return De(D(e,e[we]),t)},De=function(e,t){for(var r=0,n=t.length,i=Oe(e,n);n>r;)i[r]=t[r++];return i},Ne=function(e,t,r){U(e,t,{get:function(){return this._d[r]}})},je=function(e){var t,r,n,i,o,a,s=I(e),p=arguments.length,c=p>1?arguments[1]:void 0,l=void 0!==c,u=C(s);if(null!=u&&!x(u)){for(a=u.call(s),n=[],t=0;!(o=a.next()).done;t++)n.push(o.value);s=n}for(l&&p>2&&(c=d(c,arguments[2],2)),t=0,r=f(s.length),i=Oe(this,r);r>t;t++)i[t]=l?c(s[t],t):s[t];return i},Ee=function(){for(var e=0,t=arguments.length,r=Oe(this,t);t>e;)r[e]=arguments[e++];return r},Me=!!V&&o((function(){ye.call(new V(1))})),Fe=function(){return ye.apply(Me?he.call(Ae(this)):Ae(this),arguments)},qe={copyWithin:function(e,t){return q.call(Ae(this),e,t,arguments.length>2?arguments[2]:void 0)},every:function(e){return te(Ae(this),e,arguments.length>1?arguments[1]:void 0)},fill:function(e){return F.apply(Ae(this),arguments)},filter:function(e){return Pe(this,Z(Ae(this),e,arguments.length>1?arguments[1]:void 0))},find:function(e){return re(Ae(this),e,arguments.length>1?arguments[1]:void 0)},findIndex:function(e){return ne(Ae(this),e,arguments.length>1?arguments[1]:void 0)},forEach:function(e){Q(Ae(this),e,arguments.length>1?arguments[1]:void 0)},indexOf:function(e){return oe(Ae(this),e,arguments.length>1?arguments[1]:void 0)},includes:function(e){return ie(Ae(this),e,arguments.length>1?arguments[1]:void 0)},join:function(e){return ue.apply(Ae(this),arguments)},lastIndexOf:function(e){return de.apply(Ae(this),arguments)},map:function(e){return Te(Ae(this),e,arguments.length>1?arguments[1]:void 0)},reduce:function(e){return ce.apply(Ae(this),arguments)},reduceRight:function(e){return le.apply(Ae(this),arguments)},reverse:function(){for(var e,t=this,r=Ae(t).length,n=Math.floor(r/2),i=0;i<n;)e=t[i],t[i++]=t[--r],t[r]=e;return t},some:function(e){return ee(Ae(this),e,arguments.length>1?arguments[1]:void 0)},sort:function(e){return me.call(Ae(this),e)},subarray:function(e,t){var r=Ae(this),n=r.length,i=g(e,n);return new(D(r,r[we]))(r.buffer,r.byteOffset+i*r.BYTES_PER_ELEMENT,f((void 0===t?n:g(t,n))-i))}},Le=function(e,t){return Pe(this,he.call(Ae(this),e,t))},Be=function(e){Ae(this);var t=$e(arguments[1],1),r=this.length,n=I(e),i=f(n.length),o=0;if(i+t>r)throw H(ke);for(;o<i;)this[t+o]=n[o++]},Ue={entries:function(){return pe.call(Ae(this))},keys:function(){return se.call(Ae(this))},values:function(){return ae.call(Ae(this))}},We=function(e,t){return S(e)&&e[Ie]&&"symbol"!=typeof t&&t in e&&String(+t)==String(t)},He=function(e,t){return We(e,t=b(t,!0))?l(2,e[t]):W(e,t)},_e=function(e,t,r){return!(We(e,t=b(t,!0))&&S(r)&&v(r,"value"))||v(r,"get")||v(r,"set")||r.configurable||v(r,"writable")&&!r.writable||v(r,"enumerable")&&!r.enumerable?U(e,t,r):(e[t]=r.value,e)};Se||(B.f=He,L.f=_e),a(a.S+a.F*!Se,"Object",{getOwnPropertyDescriptor:He,defineProperty:_e}),o((function(){fe.call({})}))&&(fe=ye=function(){return ue.call(this)});var Ve=m({},qe);m(Ve,Ue),u(Ve,ge,Ue.values),m(Ve,{slice:Le,set:Be,constructor:function(){},toString:fe,toLocaleString:Fe}),Ne(Ve,"buffer","b"),Ne(Ve,"byteOffset","o"),Ne(Ve,"byteLength","l"),Ne(Ve,"length","e"),U(Ve,be,{get:function(){return this[Ie]}}),e.exports=function(e,t,r,p){var d=e+((p=!!p)?"Clamped":"")+"Array",l="get"+e,m="set"+e,h=i[d],g=h||{},b=h&&T(h),v=!h||!s.ABV,I={},x=h&&h.prototype,C=function(e,r){U(e,r,{get:function(){return function(e,r){var n=e._d;return n.v[l](r*t+n.o,Re)}(this,r)},set:function(e){return function(e,r,n){var i=e._d;p&&(n=(n=Math.round(n))<0?0:n>255?255:255&n),i.v[m](r*t+i.o,n,Re)}(this,r,e)},enumerable:!0})};v?(h=r((function(e,r,n,i){c(e,h,d,"_d");var o,a,s,p,l=0,m=0;if(S(r)){if(!(r instanceof X||(p=w(r))==G||p==z))return Ie in r?De(h,r):je.call(h,r);o=r,m=$e(n,t);var g=r.byteLength;if(void 0===i){if(g%t)throw H(ke);if((a=g-m)<0)throw H(ke)}else if((a=f(i)*t)+m>g)throw H(ke);s=a/t}else s=y(r),o=new X(a=s*t);for(u(e,"_d",{b:o,o:m,l:a,e:s,v:new Y(o)});l<s;)C(e,l++)})),x=h.prototype=k(Ve),u(x,"constructor",h)):o((function(){h(1)}))&&o((function(){new h(-1)}))&&E((function(e){new h,new h(null),new h(1.5),new h(e)}),!0)||(h=r((function(e,r,n,i){var o;return c(e,h,d),S(r)?r instanceof X||(o=w(r))==G||o==z?void 0!==i?new g(r,$e(n,t),i):void 0!==n?new g(r,$e(n,t)):new g(r):Ie in r?De(h,r):je.call(h,r):new g(y(r))})),Q(b!==Function.prototype?R(g).concat(R(b)):R(g),(function(e){e in h||u(h,e,g[e])})),h.prototype=x,n||(x.constructor=h));var $=x[ge],A=!!$&&("values"==$.name||null==$.name),O=Ue.values;u(h,ve,!0),u(x,Ie,d),u(x,xe,!0),u(x,we,h),(p?new h(1)[be]==d:be in x)||U(x,be,{get:function(){return d}}),I[d]=h,a(a.G+a.W+a.F*(h!=g),I),a(a.S,d,{BYTES_PER_ELEMENT:t}),a(a.S+a.F*o((function(){g.of.call(h,1)})),d,{from:je,of:Ee}),J in x||u(x,J,t),a(a.P,d,qe),M(d),a(a.P+a.F*Ce,d,{set:Be}),a(a.P+a.F*!A,d,Ue),n||x.toString==fe||(x.toString=fe),a(a.P+a.F*o((function(){new h(1).slice()})),d,{slice:Le}),a(a.P+a.F*(o((function(){return[1,2].toLocaleString()!=new h([1,2]).toLocaleString()}))||!o((function(){x.toLocaleString.call([1,2])}))),d,{toLocaleString:Fe}),j[d]=A?$:O,n||A||u(x,ge,O)}}else e.exports=function(){}},1125:(e,t,r)=>{"use strict";var n=r(3816),i=r(7057),o=r(4461),a=r(9383),s=r(7728),p=r(4408),d=r(4253),c=r(3328),l=r(1467),u=r(875),m=r(4843),h=r(616).f,f=r(9275).f,y=r(6852),g=r(2943),b="ArrayBuffer",v="DataView",w="Wrong index!",S=n.ArrayBuffer,I=n.DataView,x=n.Math,k=n.RangeError,T=n.Infinity,R=S,C=x.abs,$=x.pow,A=x.floor,O=x.log,P=x.LN2,D="buffer",N="byteLength",j="byteOffset",E=i?"_b":D,M=i?"_l":N,F=i?"_o":j;function q(e,t,r){var n,i,o,a=new Array(r),s=8*r-t-1,p=(1<<s)-1,d=p>>1,c=23===t?$(2,-24)-$(2,-77):0,l=0,u=e<0||0===e&&1/e<0?1:0;for((e=C(e))!=e||e===T?(i=e!=e?1:0,n=p):(n=A(O(e)/P),e*(o=$(2,-n))<1&&(n--,o*=2),(e+=n+d>=1?c/o:c*$(2,1-d))*o>=2&&(n++,o/=2),n+d>=p?(i=0,n=p):n+d>=1?(i=(e*o-1)*$(2,t),n+=d):(i=e*$(2,d-1)*$(2,t),n=0));t>=8;a[l++]=255&i,i/=256,t-=8);for(n=n<<t|i,s+=t;s>0;a[l++]=255&n,n/=256,s-=8);return a[--l]|=128*u,a}function L(e,t,r){var n,i=8*r-t-1,o=(1<<i)-1,a=o>>1,s=i-7,p=r-1,d=e[p--],c=127&d;for(d>>=7;s>0;c=256*c+e[p],p--,s-=8);for(n=c&(1<<-s)-1,c>>=-s,s+=t;s>0;n=256*n+e[p],p--,s-=8);if(0===c)c=1-a;else{if(c===o)return n?NaN:d?-T:T;n+=$(2,t),c-=a}return(d?-1:1)*n*$(2,c-t)}function B(e){return e[3]<<24|e[2]<<16|e[1]<<8|e[0]}function U(e){return[255&e]}function W(e){return[255&e,e>>8&255]}function H(e){return[255&e,e>>8&255,e>>16&255,e>>24&255]}function _(e){return q(e,52,8)}function V(e){return q(e,23,4)}function G(e,t,r){f(e.prototype,t,{get:function(){return this[r]}})}function z(e,t,r,n){var i=m(+r);if(i+t>e[M])throw k(w);var o=e[E]._b,a=i+e[F],s=o.slice(a,a+t);return n?s:s.reverse()}function J(e,t,r,n,i,o){var a=m(+r);if(a+t>e[M])throw k(w);for(var s=e[E]._b,p=a+e[F],d=n(+i),c=0;c<t;c++)s[p+c]=d[o?c:t-c-1]}if(a.ABV){if(!d((function(){S(1)}))||!d((function(){new S(-1)}))||d((function(){return new S,new S(1.5),new S(NaN),S.name!=b}))){for(var K,X=(S=function(e){return c(this,S),new R(m(e))}).prototype=R.prototype,Y=h(R),Q=0;Y.length>Q;)(K=Y[Q++])in S||s(S,K,R[K]);o||(X.constructor=S)}var Z=new I(new S(2)),ee=I.prototype.setInt8;Z.setInt8(0,2147483648),Z.setInt8(1,2147483649),!Z.getInt8(0)&&Z.getInt8(1)||p(I.prototype,{setInt8:function(e,t){ee.call(this,e,t<<24>>24)},setUint8:function(e,t){ee.call(this,e,t<<24>>24)}},!0)}else S=function(e){c(this,S,b);var t=m(e);this._b=y.call(new Array(t),0),this[M]=t},I=function(e,t,r){c(this,I,v),c(e,S,v);var n=e[M],i=l(t);if(i<0||i>n)throw k("Wrong offset!");if(i+(r=void 0===r?n-i:u(r))>n)throw k("Wrong length!");this[E]=e,this[F]=i,this[M]=r},i&&(G(S,N,"_l"),G(I,D,"_b"),G(I,N,"_l"),G(I,j,"_o")),p(I.prototype,{getInt8:function(e){return z(this,1,e)[0]<<24>>24},getUint8:function(e){return z(this,1,e)[0]},getInt16:function(e){var t=z(this,2,e,arguments[1]);return(t[1]<<8|t[0])<<16>>16},getUint16:function(e){var t=z(this,2,e,arguments[1]);return t[1]<<8|t[0]},getInt32:function(e){return B(z(this,4,e,arguments[1]))},getUint32:function(e){return B(z(this,4,e,arguments[1]))>>>0},getFloat32:function(e){return L(z(this,4,e,arguments[1]),23,4)},getFloat64:function(e){return L(z(this,8,e,arguments[1]),52,8)},setInt8:function(e,t){J(this,1,e,U,t)},setUint8:function(e,t){J(this,1,e,U,t)},setInt16:function(e,t){J(this,2,e,W,t,arguments[2])},setUint16:function(e,t){J(this,2,e,W,t,arguments[2])},setInt32:function(e,t){J(this,4,e,H,t,arguments[2])},setUint32:function(e,t){J(this,4,e,H,t,arguments[2])},setFloat32:function(e,t){J(this,4,e,V,t,arguments[2])},setFloat64:function(e,t){J(this,8,e,_,t,arguments[2])}});g(S,b),g(I,v),s(I.prototype,a.VIEW,!0),t.ArrayBuffer=S,t.DataView=I},9383:(e,t,r)=>{for(var n,i=r(3816),o=r(7728),a=r(3953),s=a("typed_array"),p=a("view"),d=!(!i.ArrayBuffer||!i.DataView),c=d,l=0,u="Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array".split(",");l<9;)(n=i[u[l++]])?(o(n.prototype,s,!0),o(n.prototype,p,!0)):c=!1;e.exports={ABV:d,CONSTR:c,TYPED:s,VIEW:p}},3953:e=>{var t=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++t+r).toString(36))}},575:(e,t,r)=>{var n=r(3816).navigator;e.exports=n&&n.userAgent||""},1616:(e,t,r)=>{var n=r(5286);e.exports=function(e,t){if(!n(e)||e._t!==t)throw TypeError("Incompatible receiver, "+t+" required!");return e}},6074:(e,t,r)=>{var n=r(3816),i=r(5645),o=r(4461),a=r(8787),s=r(9275).f;e.exports=function(e){var t=i.Symbol||(i.Symbol=o?{}:n.Symbol||{});"_"==e.charAt(0)||e in t||s(t,e,{value:a.f(e)})}},8787:(e,t,r)=>{t.f=r(6314)},6314:(e,t,r)=>{var n=r(3825)("wks"),i=r(3953),o=r(3816).Symbol,a="function"==typeof o;(e.exports=function(e){return n[e]||(n[e]=a&&o[e]||(a?o:i)("Symbol."+e))}).store=n},9002:(e,t,r)=>{var n=r(1488),i=r(6314)("iterator"),o=r(2803);e.exports=r(5645).getIteratorMethod=function(e){if(null!=e)return e[i]||e["@@iterator"]||o[n(e)]}},1761:(e,t,r)=>{var n=r(2985),i=r(5496)(/[\\^$*+?.()|[\]{}]/g,"\\$&");n(n.S,"RegExp",{escape:function(e){return i(e)}})},2e3:(e,t,r)=>{var n=r(2985);n(n.P,"Array",{copyWithin:r(5216)}),r(7722)("copyWithin")},5745:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(4);n(n.P+n.F*!r(7717)([].every,!0),"Array",{every:function(e){return i(this,e,arguments[1])}})},8977:(e,t,r)=>{var n=r(2985);n(n.P,"Array",{fill:r(6852)}),r(7722)("fill")},8837:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(2);n(n.P+n.F*!r(7717)([].filter,!0),"Array",{filter:function(e){return i(this,e,arguments[1])}})},4899:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(6),o="findIndex",a=!0;o in[]&&Array(1)[o]((function(){a=!1})),n(n.P+n.F*a,"Array",{findIndex:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(7722)(o)},2310:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(5),o="find",a=!0;o in[]&&Array(1).find((function(){a=!1})),n(n.P+n.F*a,"Array",{find:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(7722)(o)},4336:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(0),o=r(7717)([].forEach,!0);n(n.P+n.F*!o,"Array",{forEach:function(e){return i(this,e,arguments[1])}})},522:(e,t,r)=>{"use strict";var n=r(741),i=r(2985),o=r(508),a=r(8851),s=r(6555),p=r(875),d=r(2811),c=r(9002);i(i.S+i.F*!r(7462)((function(e){Array.from(e)})),"Array",{from:function(e){var t,r,i,l,u=o(e),m="function"==typeof this?this:Array,h=arguments.length,f=h>1?arguments[1]:void 0,y=void 0!==f,g=0,b=c(u);if(y&&(f=n(f,h>2?arguments[2]:void 0,2)),null==b||m==Array&&s(b))for(r=new m(t=p(u.length));t>g;g++)d(r,g,y?f(u[g],g):u[g]);else for(l=b.call(u),r=new m;!(i=l.next()).done;g++)d(r,g,y?a(l,f,[i.value,g],!0):i.value);return r.length=g,r}})},3369:(e,t,r)=>{"use strict";var n=r(2985),i=r(9315)(!1),o=[].indexOf,a=!!o&&1/[1].indexOf(1,-0)<0;n(n.P+n.F*(a||!r(7717)(o)),"Array",{indexOf:function(e){return a?o.apply(this,arguments)||0:i(this,e,arguments[1])}})},774:(e,t,r)=>{var n=r(2985);n(n.S,"Array",{isArray:r(4302)})},6997:(e,t,r)=>{"use strict";var n=r(7722),i=r(5436),o=r(2803),a=r(2110);e.exports=r(2923)(Array,"Array",(function(e,t){this._t=a(e),this._i=0,this._k=t}),(function(){var e=this._t,t=this._k,r=this._i++;return!e||r>=e.length?(this._t=void 0,i(1)):i(0,"keys"==t?r:"values"==t?e[r]:[r,e[r]])}),"values"),o.Arguments=o.Array,n("keys"),n("values"),n("entries")},7842:(e,t,r)=>{"use strict";var n=r(2985),i=r(2110),o=[].join;n(n.P+n.F*(r(9797)!=Object||!r(7717)(o)),"Array",{join:function(e){return o.call(i(this),void 0===e?",":e)}})},9564:(e,t,r)=>{"use strict";var n=r(2985),i=r(2110),o=r(1467),a=r(875),s=[].lastIndexOf,p=!!s&&1/[1].lastIndexOf(1,-0)<0;n(n.P+n.F*(p||!r(7717)(s)),"Array",{lastIndexOf:function(e){if(p)return s.apply(this,arguments)||0;var t=i(this),r=a(t.length),n=r-1;for(arguments.length>1&&(n=Math.min(n,o(arguments[1]))),n<0&&(n=r+n);n>=0;n--)if(n in t&&t[n]===e)return n||0;return-1}})},1802:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(1);n(n.P+n.F*!r(7717)([].map,!0),"Array",{map:function(e){return i(this,e,arguments[1])}})},8295:(e,t,r)=>{"use strict";var n=r(2985),i=r(2811);n(n.S+n.F*r(4253)((function(){function e(){}return!(Array.of.call(e)instanceof e)})),"Array",{of:function(){for(var e=0,t=arguments.length,r=new("function"==typeof this?this:Array)(t);t>e;)i(r,e,arguments[e++]);return r.length=t,r}})},3750:(e,t,r)=>{"use strict";var n=r(2985),i=r(7628);n(n.P+n.F*!r(7717)([].reduceRight,!0),"Array",{reduceRight:function(e){return i(this,e,arguments.length,arguments[1],!0)}})},3057:(e,t,r)=>{"use strict";var n=r(2985),i=r(7628);n(n.P+n.F*!r(7717)([].reduce,!0),"Array",{reduce:function(e){return i(this,e,arguments.length,arguments[1],!1)}})},110:(e,t,r)=>{"use strict";var n=r(2985),i=r(639),o=r(2032),a=r(2337),s=r(875),p=[].slice;n(n.P+n.F*r(4253)((function(){i&&p.call(i)})),"Array",{slice:function(e,t){var r=s(this.length),n=o(this);if(t=void 0===t?r:t,"Array"==n)return p.call(this,e,t);for(var i=a(e,r),d=a(t,r),c=s(d-i),l=new Array(c),u=0;u<c;u++)l[u]="String"==n?this.charAt(i+u):this[i+u];return l}})},6773:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(3);n(n.P+n.F*!r(7717)([].some,!0),"Array",{some:function(e){return i(this,e,arguments[1])}})},75:(e,t,r)=>{"use strict";var n=r(2985),i=r(4963),o=r(508),a=r(4253),s=[].sort,p=[1,2,3];n(n.P+n.F*(a((function(){p.sort(void 0)}))||!a((function(){p.sort(null)}))||!r(7717)(s)),"Array",{sort:function(e){return void 0===e?s.call(o(this)):s.call(o(this),i(e))}})},1842:(e,t,r)=>{r(2974)("Array")},1822:(e,t,r)=>{var n=r(2985);n(n.S,"Date",{now:function(){return(new Date).getTime()}})},1031:(e,t,r)=>{var n=r(2985),i=r(3537);n(n.P+n.F*(Date.prototype.toISOString!==i),"Date",{toISOString:i})},9977:(e,t,r)=>{"use strict";var n=r(2985),i=r(508),o=r(1689);n(n.P+n.F*r(4253)((function(){return null!==new Date(NaN).toJSON()||1!==Date.prototype.toJSON.call({toISOString:function(){return 1}})})),"Date",{toJSON:function(e){var t=i(this),r=o(t);return"number"!=typeof r||isFinite(r)?t.toISOString():null}})},1560:(e,t,r)=>{var n=r(6314)("toPrimitive"),i=Date.prototype;n in i||r(7728)(i,n,r(870))},6331:(e,t,r)=>{var n=Date.prototype,i="Invalid Date",o="toString",a=n.toString,s=n.getTime;new Date(NaN)+""!=i&&r(7234)(n,o,(function(){var e=s.call(this);return e==e?a.call(this):i}))},9730:(e,t,r)=>{var n=r(2985);n(n.P,"Function",{bind:r(4398)})},8377:(e,t,r)=>{"use strict";var n=r(5286),i=r(468),o=r(6314)("hasInstance"),a=Function.prototype;o in a||r(9275).f(a,o,{value:function(e){if("function"!=typeof this||!n(e))return!1;if(!n(this.prototype))return e instanceof this;for(;e=i(e);)if(this.prototype===e)return!0;return!1}})},6059:(e,t,r)=>{var n=r(9275).f,i=Function.prototype,o=/^\s*function ([^ (]*)/,a="name";a in i||r(7057)&&n(i,a,{configurable:!0,get:function(){try{return(""+this).match(o)[1]}catch(e){return""}}})},8416:(e,t,r)=>{"use strict";var n=r(9824),i=r(1616),o="Map";e.exports=r(5795)(o,(function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}}),{get:function(e){var t=n.getEntry(i(this,o),e);return t&&t.v},set:function(e,t){return n.def(i(this,o),0===e?0:e,t)}},n,!0)},6503:(e,t,r)=>{var n=r(2985),i=r(6206),o=Math.sqrt,a=Math.acosh;n(n.S+n.F*!(a&&710==Math.floor(a(Number.MAX_VALUE))&&a(1/0)==1/0),"Math",{acosh:function(e){return(e=+e)<1?NaN:e>94906265.62425156?Math.log(e)+Math.LN2:i(e-1+o(e-1)*o(e+1))}})},6786:(e,t,r)=>{var n=r(2985),i=Math.asinh;n(n.S+n.F*!(i&&1/i(0)>0),"Math",{asinh:function e(t){return isFinite(t=+t)&&0!=t?t<0?-e(-t):Math.log(t+Math.sqrt(t*t+1)):t}})},932:(e,t,r)=>{var n=r(2985),i=Math.atanh;n(n.S+n.F*!(i&&1/i(-0)<0),"Math",{atanh:function(e){return 0==(e=+e)?e:Math.log((1+e)/(1-e))/2}})},7526:(e,t,r)=>{var n=r(2985),i=r(1801);n(n.S,"Math",{cbrt:function(e){return i(e=+e)*Math.pow(Math.abs(e),1/3)}})},1591:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{clz32:function(e){return(e>>>=0)?31-Math.floor(Math.log(e+.5)*Math.LOG2E):32}})},9073:(e,t,r)=>{var n=r(2985),i=Math.exp;n(n.S,"Math",{cosh:function(e){return(i(e=+e)+i(-e))/2}})},347:(e,t,r)=>{var n=r(2985),i=r(3086);n(n.S+n.F*(i!=Math.expm1),"Math",{expm1:i})},579:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{fround:r(4934)})},4669:(e,t,r)=>{var n=r(2985),i=Math.abs;n(n.S,"Math",{hypot:function(e,t){for(var r,n,o=0,a=0,s=arguments.length,p=0;a<s;)p<(r=i(arguments[a++]))?(o=o*(n=p/r)*n+1,p=r):o+=r>0?(n=r/p)*n:r;return p===1/0?1/0:p*Math.sqrt(o)}})},7710:(e,t,r)=>{var n=r(2985),i=Math.imul;n(n.S+n.F*r(4253)((function(){return-5!=i(4294967295,5)||2!=i.length})),"Math",{imul:function(e,t){var r=65535,n=+e,i=+t,o=r&n,a=r&i;return 0|o*a+((r&n>>>16)*a+o*(r&i>>>16)<<16>>>0)}})},5789:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{log10:function(e){return Math.log(e)*Math.LOG10E}})},3514:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{log1p:r(6206)})},9978:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{log2:function(e){return Math.log(e)/Math.LN2}})},8472:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{sign:r(1801)})},6946:(e,t,r)=>{var n=r(2985),i=r(3086),o=Math.exp;n(n.S+n.F*r(4253)((function(){return-2e-17!=!Math.sinh(-2e-17)})),"Math",{sinh:function(e){return Math.abs(e=+e)<1?(i(e)-i(-e))/2:(o(e-1)-o(-e-1))*(Math.E/2)}})},5068:(e,t,r)=>{var n=r(2985),i=r(3086),o=Math.exp;n(n.S,"Math",{tanh:function(e){var t=i(e=+e),r=i(-e);return t==1/0?1:r==1/0?-1:(t-r)/(o(e)+o(-e))}})},413:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{trunc:function(e){return(e>0?Math.floor:Math.ceil)(e)}})},1246:(e,t,r)=>{"use strict";var n=r(3816),i=r(9181),o=r(2032),a=r(266),s=r(1689),p=r(4253),d=r(616).f,c=r(8693).f,l=r(9275).f,u=r(9599).trim,m="Number",h=n.Number,f=h,y=h.prototype,g=o(r(2503)(y))==m,b="trim"in String.prototype,v=function(e){var t=s(e,!1);if("string"==typeof t&&t.length>2){var r,n,i,o=(t=b?t.trim():u(t,3)).charCodeAt(0);if(43===o||45===o){if(88===(r=t.charCodeAt(2))||120===r)return NaN}else if(48===o){switch(t.charCodeAt(1)){case 66:case 98:n=2,i=49;break;case 79:case 111:n=8,i=55;break;default:return+t}for(var a,p=t.slice(2),d=0,c=p.length;d<c;d++)if((a=p.charCodeAt(d))<48||a>i)return NaN;return parseInt(p,n)}}return+t};if(!h(" 0o1")||!h("0b1")||h("+0x1")){h=function(e){var t=arguments.length<1?0:e,r=this;return r instanceof h&&(g?p((function(){y.valueOf.call(r)})):o(r)!=m)?a(new f(v(t)),r,h):v(t)};for(var w,S=r(7057)?d(f):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger".split(","),I=0;S.length>I;I++)i(f,w=S[I])&&!i(h,w)&&l(h,w,c(f,w));h.prototype=y,y.constructor=h,r(7234)(n,m,h)}},5972:(e,t,r)=>{var n=r(2985);n(n.S,"Number",{EPSILON:Math.pow(2,-52)})},3403:(e,t,r)=>{var n=r(2985),i=r(3816).isFinite;n(n.S,"Number",{isFinite:function(e){return"number"==typeof e&&i(e)}})},2516:(e,t,r)=>{var n=r(2985);n(n.S,"Number",{isInteger:r(8367)})},9371:(e,t,r)=>{var n=r(2985);n(n.S,"Number",{isNaN:function(e){return e!=e}})},6479:(e,t,r)=>{var n=r(2985),i=r(8367),o=Math.abs;n(n.S,"Number",{isSafeInteger:function(e){return i(e)&&o(e)<=9007199254740991}})},1736:(e,t,r)=>{var n=r(2985);n(n.S,"Number",{MAX_SAFE_INTEGER:9007199254740991})},1889:(e,t,r)=>{var n=r(2985);n(n.S,"Number",{MIN_SAFE_INTEGER:-9007199254740991})},5177:(e,t,r)=>{var n=r(2985),i=r(7743);n(n.S+n.F*(Number.parseFloat!=i),"Number",{parseFloat:i})},6943:(e,t,r)=>{var n=r(2985),i=r(5960);n(n.S+n.F*(Number.parseInt!=i),"Number",{parseInt:i})},726:(e,t,r)=>{"use strict";var n=r(2985),i=r(1467),o=r(3365),a=r(8595),s=1..toFixed,p=Math.floor,d=[0,0,0,0,0,0],c="Number.toFixed: incorrect invocation!",l="0",u=function(e,t){for(var r=-1,n=t;++r<6;)n+=e*d[r],d[r]=n%1e7,n=p(n/1e7)},m=function(e){for(var t=6,r=0;--t>=0;)r+=d[t],d[t]=p(r/e),r=r%e*1e7},h=function(){for(var e=6,t="";--e>=0;)if(""!==t||0===e||0!==d[e]){var r=String(d[e]);t=""===t?r:t+a.call(l,7-r.length)+r}return t},f=function(e,t,r){return 0===t?r:t%2==1?f(e,t-1,r*e):f(e*e,t/2,r)};n(n.P+n.F*(!!s&&("0.000"!==8e-5.toFixed(3)||"1"!==.9.toFixed(0)||"1.25"!==1.255.toFixed(2)||"1000000000000000128"!==(0xde0b6b3a7640080).toFixed(0))||!r(4253)((function(){s.call({})}))),"Number",{toFixed:function(e){var t,r,n,s,p=o(this,c),d=i(e),y="",g=l;if(d<0||d>20)throw RangeError(c);if(p!=p)return"NaN";if(p<=-1e21||p>=1e21)return String(p);if(p<0&&(y="-",p=-p),p>1e-21)if(t=function(e){for(var t=0,r=e;r>=4096;)t+=12,r/=4096;for(;r>=2;)t+=1,r/=2;return t}(p*f(2,69,1))-69,r=t<0?p*f(2,-t,1):p/f(2,t,1),r*=4503599627370496,(t=52-t)>0){for(u(0,r),n=d;n>=7;)u(1e7,0),n-=7;for(u(f(10,n,1),0),n=t-1;n>=23;)m(1<<23),n-=23;m(1<<n),u(1,1),m(2),g=h()}else u(0,r),u(1<<-t,0),g=h()+a.call(l,d);return g=d>0?y+((s=g.length)<=d?"0."+a.call(l,d-s)+g:g.slice(0,s-d)+"."+g.slice(s-d)):y+g}})},1901:(e,t,r)=>{"use strict";var n=r(2985),i=r(4253),o=r(3365),a=1..toPrecision;n(n.P+n.F*(i((function(){return"1"!==a.call(1,void 0)}))||!i((function(){a.call({})}))),"Number",{toPrecision:function(e){var t=o(this,"Number#toPrecision: incorrect invocation!");return void 0===e?a.call(t):a.call(t,e)}})},5115:(e,t,r)=>{var n=r(2985);n(n.S+n.F,"Object",{assign:r(5345)})},8132:(e,t,r)=>{var n=r(2985);n(n.S,"Object",{create:r(2503)})},7470:(e,t,r)=>{var n=r(2985);n(n.S+n.F*!r(7057),"Object",{defineProperties:r(5588)})},8388:(e,t,r)=>{var n=r(2985);n(n.S+n.F*!r(7057),"Object",{defineProperty:r(9275).f})},9375:(e,t,r)=>{var n=r(5286),i=r(4728).onFreeze;r(3160)("freeze",(function(e){return function(t){return e&&n(t)?e(i(t)):t}}))},4882:(e,t,r)=>{var n=r(2110),i=r(8693).f;r(3160)("getOwnPropertyDescriptor",(function(){return function(e,t){return i(n(e),t)}}))},9622:(e,t,r)=>{r(3160)("getOwnPropertyNames",(function(){return r(9327).f}))},1520:(e,t,r)=>{var n=r(508),i=r(468);r(3160)("getPrototypeOf",(function(){return function(e){return i(n(e))}}))},9892:(e,t,r)=>{var n=r(5286);r(3160)("isExtensible",(function(e){return function(t){return!!n(t)&&(!e||e(t))}}))},4157:(e,t,r)=>{var n=r(5286);r(3160)("isFrozen",(function(e){return function(t){return!n(t)||!!e&&e(t)}}))},5095:(e,t,r)=>{var n=r(5286);r(3160)("isSealed",(function(e){return function(t){return!n(t)||!!e&&e(t)}}))},9176:(e,t,r)=>{var n=r(2985);n(n.S,"Object",{is:r(7195)})},7476:(e,t,r)=>{var n=r(508),i=r(7184);r(3160)("keys",(function(){return function(e){return i(n(e))}}))},4672:(e,t,r)=>{var n=r(5286),i=r(4728).onFreeze;r(3160)("preventExtensions",(function(e){return function(t){return e&&n(t)?e(i(t)):t}}))},3533:(e,t,r)=>{var n=r(5286),i=r(4728).onFreeze;r(3160)("seal",(function(e){return function(t){return e&&n(t)?e(i(t)):t}}))},8838:(e,t,r)=>{var n=r(2985);n(n.S,"Object",{setPrototypeOf:r(7375).set})},6253:(e,t,r)=>{"use strict";var n=r(1488),i={};i[r(6314)("toStringTag")]="z",i+""!="[object z]"&&r(7234)(Object.prototype,"toString",(function(){return"[object "+n(this)+"]"}),!0)},4299:(e,t,r)=>{var n=r(2985),i=r(7743);n(n.G+n.F*(parseFloat!=i),{parseFloat:i})},1084:(e,t,r)=>{var n=r(2985),i=r(5960);n(n.G+n.F*(parseInt!=i),{parseInt:i})},851:(e,t,r)=>{"use strict";var n,i,o,a,s=r(4461),p=r(3816),d=r(741),c=r(1488),l=r(2985),u=r(5286),m=r(4963),h=r(3328),f=r(3531),y=r(8364),g=r(4193).set,b=r(4351)(),v=r(3499),w=r(188),S=r(575),I=r(94),x="Promise",k=p.TypeError,T=p.process,R=T&&T.versions,C=R&&R.v8||"",$=p.Promise,A="process"==c(T),O=function(){},P=i=v.f,D=!!function(){try{var e=$.resolve(1),t=(e.constructor={})[r(6314)("species")]=function(e){e(O,O)};return(A||"function"==typeof PromiseRejectionEvent)&&e.then(O)instanceof t&&0!==C.indexOf("6.6")&&-1===S.indexOf("Chrome/66")}catch(e){}}(),N=function(e){var t;return!(!u(e)||"function"!=typeof(t=e.then))&&t},j=function(e,t){if(!e._n){e._n=!0;var r=e._c;b((function(){for(var n=e._v,i=1==e._s,o=0,a=function(t){var r,o,a,s=i?t.ok:t.fail,p=t.resolve,d=t.reject,c=t.domain;try{s?(i||(2==e._h&&F(e),e._h=1),!0===s?r=n:(c&&c.enter(),r=s(n),c&&(c.exit(),a=!0)),r===t.promise?d(k("Promise-chain cycle")):(o=N(r))?o.call(r,p,d):p(r)):d(n)}catch(e){c&&!a&&c.exit(),d(e)}};r.length>o;)a(r[o++]);e._c=[],e._n=!1,t&&!e._h&&E(e)}))}},E=function(e){g.call(p,(function(){var t,r,n,i=e._v,o=M(e);if(o&&(t=w((function(){A?T.emit("unhandledRejection",i,e):(r=p.onunhandledrejection)?r({promise:e,reason:i}):(n=p.console)&&n.error&&n.error("Unhandled promise rejection",i)})),e._h=A||M(e)?2:1),e._a=void 0,o&&t.e)throw t.v}))},M=function(e){return 1!==e._h&&0===(e._a||e._c).length},F=function(e){g.call(p,(function(){var t;A?T.emit("rejectionHandled",e):(t=p.onrejectionhandled)&&t({promise:e,reason:e._v})}))},q=function(e){var t=this;t._d||(t._d=!0,(t=t._w||t)._v=e,t._s=2,t._a||(t._a=t._c.slice()),j(t,!0))},L=function(e){var t,r=this;if(!r._d){r._d=!0,r=r._w||r;try{if(r===e)throw k("Promise can't be resolved itself");(t=N(e))?b((function(){var n={_w:r,_d:!1};try{t.call(e,d(L,n,1),d(q,n,1))}catch(e){q.call(n,e)}})):(r._v=e,r._s=1,j(r,!1))}catch(e){q.call({_w:r,_d:!1},e)}}};D||($=function(e){h(this,$,x,"_h"),m(e),n.call(this);try{e(d(L,this,1),d(q,this,1))}catch(e){q.call(this,e)}},(n=function(e){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1}).prototype=r(4408)($.prototype,{then:function(e,t){var r=P(y(this,$));return r.ok="function"!=typeof e||e,r.fail="function"==typeof t&&t,r.domain=A?T.domain:void 0,this._c.push(r),this._a&&this._a.push(r),this._s&&j(this,!1),r.promise},catch:function(e){return this.then(void 0,e)}}),o=function(){var e=new n;this.promise=e,this.resolve=d(L,e,1),this.reject=d(q,e,1)},v.f=P=function(e){return e===$||e===a?new o(e):i(e)}),l(l.G+l.W+l.F*!D,{Promise:$}),r(2943)($,x),r(2974)(x),a=r(5645).Promise,l(l.S+l.F*!D,x,{reject:function(e){var t=P(this);return(0,t.reject)(e),t.promise}}),l(l.S+l.F*(s||!D),x,{resolve:function(e){return I(s&&this===a?$:this,e)}}),l(l.S+l.F*!(D&&r(7462)((function(e){$.all(e).catch(O)}))),x,{all:function(e){var t=this,r=P(t),n=r.resolve,i=r.reject,o=w((function(){var r=[],o=0,a=1;f(e,!1,(function(e){var s=o++,p=!1;r.push(void 0),a++,t.resolve(e).then((function(e){p||(p=!0,r[s]=e,--a||n(r))}),i)})),--a||n(r)}));return o.e&&i(o.v),r.promise},race:function(e){var t=this,r=P(t),n=r.reject,i=w((function(){f(e,!1,(function(e){t.resolve(e).then(r.resolve,n)}))}));return i.e&&n(i.v),r.promise}})},1572:(e,t,r)=>{var n=r(2985),i=r(4963),o=r(7007),a=(r(3816).Reflect||{}).apply,s=Function.apply;n(n.S+n.F*!r(4253)((function(){a((function(){}))})),"Reflect",{apply:function(e,t,r){var n=i(e),p=o(r);return a?a(n,t,p):s.call(n,t,p)}})},2139:(e,t,r)=>{var n=r(2985),i=r(2503),o=r(4963),a=r(7007),s=r(5286),p=r(4253),d=r(4398),c=(r(3816).Reflect||{}).construct,l=p((function(){function e(){}return!(c((function(){}),[],e)instanceof e)})),u=!p((function(){c((function(){}))}));n(n.S+n.F*(l||u),"Reflect",{construct:function(e,t){o(e),a(t);var r=arguments.length<3?e:o(arguments[2]);if(u&&!l)return c(e,t,r);if(e==r){switch(t.length){case 0:return new e;case 1:return new e(t[0]);case 2:return new e(t[0],t[1]);case 3:return new e(t[0],t[1],t[2]);case 4:return new e(t[0],t[1],t[2],t[3])}var n=[null];return n.push.apply(n,t),new(d.apply(e,n))}var p=r.prototype,m=i(s(p)?p:Object.prototype),h=Function.apply.call(e,m,t);return s(h)?h:m}})},685:(e,t,r)=>{var n=r(9275),i=r(2985),o=r(7007),a=r(1689);i(i.S+i.F*r(4253)((function(){Reflect.defineProperty(n.f({},1,{value:1}),1,{value:2})})),"Reflect",{defineProperty:function(e,t,r){o(e),t=a(t,!0),o(r);try{return n.f(e,t,r),!0}catch(e){return!1}}})},5535:(e,t,r)=>{var n=r(2985),i=r(8693).f,o=r(7007);n(n.S,"Reflect",{deleteProperty:function(e,t){var r=i(o(e),t);return!(r&&!r.configurable)&&delete e[t]}})},7347:(e,t,r)=>{"use strict";var n=r(2985),i=r(7007),o=function(e){this._t=i(e),this._i=0;var t,r=this._k=[];for(t in e)r.push(t)};r(9988)(o,"Object",(function(){var e,t=this,r=t._k;do{if(t._i>=r.length)return{value:void 0,done:!0}}while(!((e=r[t._i++])in t._t));return{value:e,done:!1}})),n(n.S,"Reflect",{enumerate:function(e){return new o(e)}})},6633:(e,t,r)=>{var n=r(8693),i=r(2985),o=r(7007);i(i.S,"Reflect",{getOwnPropertyDescriptor:function(e,t){return n.f(o(e),t)}})},8989:(e,t,r)=>{var n=r(2985),i=r(468),o=r(7007);n(n.S,"Reflect",{getPrototypeOf:function(e){return i(o(e))}})},3049:(e,t,r)=>{var n=r(8693),i=r(468),o=r(9181),a=r(2985),s=r(5286),p=r(7007);a(a.S,"Reflect",{get:function e(t,r){var a,d,c=arguments.length<3?t:arguments[2];return p(t)===c?t[r]:(a=n.f(t,r))?o(a,"value")?a.value:void 0!==a.get?a.get.call(c):void 0:s(d=i(t))?e(d,r,c):void 0}})},8270:(e,t,r)=>{var n=r(2985);n(n.S,"Reflect",{has:function(e,t){return t in e}})},4510:(e,t,r)=>{var n=r(2985),i=r(7007),o=Object.isExtensible;n(n.S,"Reflect",{isExtensible:function(e){return i(e),!o||o(e)}})},3984:(e,t,r)=>{var n=r(2985);n(n.S,"Reflect",{ownKeys:r(7643)})},5769:(e,t,r)=>{var n=r(2985),i=r(7007),o=Object.preventExtensions;n(n.S,"Reflect",{preventExtensions:function(e){i(e);try{return o&&o(e),!0}catch(e){return!1}}})},6014:(e,t,r)=>{var n=r(2985),i=r(7375);i&&n(n.S,"Reflect",{setPrototypeOf:function(e,t){i.check(e,t);try{return i.set(e,t),!0}catch(e){return!1}}})},55:(e,t,r)=>{var n=r(9275),i=r(8693),o=r(468),a=r(9181),s=r(2985),p=r(681),d=r(7007),c=r(5286);s(s.S,"Reflect",{set:function e(t,r,s){var l,u,m=arguments.length<4?t:arguments[3],h=i.f(d(t),r);if(!h){if(c(u=o(t)))return e(u,r,s,m);h=p(0)}if(a(h,"value")){if(!1===h.writable||!c(m))return!1;if(l=i.f(m,r)){if(l.get||l.set||!1===l.writable)return!1;l.value=s,n.f(m,r,l)}else n.f(m,r,p(0,s));return!0}return void 0!==h.set&&(h.set.call(m,s),!0)}})},3946:(e,t,r)=>{var n=r(3816),i=r(266),o=r(9275).f,a=r(616).f,s=r(5364),p=r(3218),d=n.RegExp,c=d,l=d.prototype,u=/a/g,m=/a/g,h=new d(u)!==u;if(r(7057)&&(!h||r(4253)((function(){return m[r(6314)("match")]=!1,d(u)!=u||d(m)==m||"/a/i"!=d(u,"i")})))){d=function(e,t){var r=this instanceof d,n=s(e),o=void 0===t;return!r&&n&&e.constructor===d&&o?e:i(h?new c(n&&!o?e.source:e,t):c((n=e instanceof d)?e.source:e,n&&o?p.call(e):t),r?this:l,d)};for(var f=function(e){e in d||o(d,e,{configurable:!0,get:function(){return c[e]},set:function(t){c[e]=t}})},y=a(c),g=0;y.length>g;)f(y[g++]);l.constructor=d,d.prototype=l,r(7234)(n,"RegExp",d)}r(2974)("RegExp")},8269:(e,t,r)=>{"use strict";var n=r(1165);r(2985)({target:"RegExp",proto:!0,forced:n!==/./.exec},{exec:n})},6774:(e,t,r)=>{r(7057)&&"g"!=/./g.flags&&r(9275).f(RegExp.prototype,"flags",{configurable:!0,get:r(3218)})},1466:(e,t,r)=>{"use strict";var n=r(7007),i=r(875),o=r(6793),a=r(7787);r(8082)("match",1,(function(e,t,r,s){return[function(r){var n=e(this),i=null==r?void 0:r[t];return void 0!==i?i.call(r,n):new RegExp(r)[t](String(n))},function(e){var t=s(r,e,this);if(t.done)return t.value;var p=n(e),d=String(this);if(!p.global)return a(p,d);var c=p.unicode;p.lastIndex=0;for(var l,u=[],m=0;null!==(l=a(p,d));){var h=String(l[0]);u[m]=h,""===h&&(p.lastIndex=o(d,i(p.lastIndex),c)),m++}return 0===m?null:u}]}))},9357:(e,t,r)=>{"use strict";var n=r(7007),i=r(508),o=r(875),a=r(1467),s=r(6793),p=r(7787),d=Math.max,c=Math.min,l=Math.floor,u=/\$([$&`']|\d\d?|<[^>]*>)/g,m=/\$([$&`']|\d\d?)/g;r(8082)("replace",2,(function(e,t,r,h){return[function(n,i){var o=e(this),a=null==n?void 0:n[t];return void 0!==a?a.call(n,o,i):r.call(String(o),n,i)},function(e,t){var i=h(r,e,this,t);if(i.done)return i.value;var l=n(e),u=String(this),m="function"==typeof t;m||(t=String(t));var y=l.global;if(y){var g=l.unicode;l.lastIndex=0}for(var b=[];;){var v=p(l,u);if(null===v)break;if(b.push(v),!y)break;""===String(v[0])&&(l.lastIndex=s(u,o(l.lastIndex),g))}for(var w,S="",I=0,x=0;x<b.length;x++){v=b[x];for(var k=String(v[0]),T=d(c(a(v.index),u.length),0),R=[],C=1;C<v.length;C++)R.push(void 0===(w=v[C])?w:String(w));var $=v.groups;if(m){var A=[k].concat(R,T,u);void 0!==$&&A.push($);var O=String(t.apply(void 0,A))}else O=f(k,u,T,R,$,t);T>=I&&(S+=u.slice(I,T)+O,I=T+k.length)}return S+u.slice(I)}];function f(e,t,n,o,a,s){var p=n+e.length,d=o.length,c=m;return void 0!==a&&(a=i(a),c=u),r.call(s,c,(function(r,i){var s;switch(i.charAt(0)){case"$":return"$";case"&":return e;case"`":return t.slice(0,n);case"'":return t.slice(p);case"<":s=a[i.slice(1,-1)];break;default:var c=+i;if(0===c)return r;if(c>d){var u=l(c/10);return 0===u?r:u<=d?void 0===o[u-1]?i.charAt(1):o[u-1]+i.charAt(1):r}s=o[c-1]}return void 0===s?"":s}))}}))},6142:(e,t,r)=>{"use strict";var n=r(7007),i=r(7195),o=r(7787);r(8082)("search",1,(function(e,t,r,a){return[function(r){var n=e(this),i=null==r?void 0:r[t];return void 0!==i?i.call(r,n):new RegExp(r)[t](String(n))},function(e){var t=a(r,e,this);if(t.done)return t.value;var s=n(e),p=String(this),d=s.lastIndex;i(d,0)||(s.lastIndex=0);var c=o(s,p);return i(s.lastIndex,d)||(s.lastIndex=d),null===c?-1:c.index}]}))},1876:(e,t,r)=>{"use strict";var n=r(5364),i=r(7007),o=r(8364),a=r(6793),s=r(875),p=r(7787),d=r(1165),c=r(4253),l=Math.min,u=[].push,m=4294967295,h=!c((function(){RegExp(m,"y")}));r(8082)("split",2,(function(e,t,r,c){var f;return f="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(e,t){var i=String(this);if(void 0===e&&0===t)return[];if(!n(e))return r.call(i,e,t);for(var o,a,s,p=[],c=(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.unicode?"u":"")+(e.sticky?"y":""),l=0,h=void 0===t?m:t>>>0,f=new RegExp(e.source,c+"g");(o=d.call(f,i))&&!((a=f.lastIndex)>l&&(p.push(i.slice(l,o.index)),o.length>1&&o.index<i.length&&u.apply(p,o.slice(1)),s=o[0].length,l=a,p.length>=h));)f.lastIndex===o.index&&f.lastIndex++;return l===i.length?!s&&f.test("")||p.push(""):p.push(i.slice(l)),p.length>h?p.slice(0,h):p}:"0".split(void 0,0).length?function(e,t){return void 0===e&&0===t?[]:r.call(this,e,t)}:r,[function(r,n){var i=e(this),o=null==r?void 0:r[t];return void 0!==o?o.call(r,i,n):f.call(String(i),r,n)},function(e,t){var n=c(f,e,this,t,f!==r);if(n.done)return n.value;var d=i(e),u=String(this),y=o(d,RegExp),g=d.unicode,b=(d.ignoreCase?"i":"")+(d.multiline?"m":"")+(d.unicode?"u":"")+(h?"y":"g"),v=new y(h?d:"^(?:"+d.source+")",b),w=void 0===t?m:t>>>0;if(0===w)return[];if(0===u.length)return null===p(v,u)?[u]:[];for(var S=0,I=0,x=[];I<u.length;){v.lastIndex=h?I:0;var k,T=p(v,h?u:u.slice(I));if(null===T||(k=l(s(v.lastIndex+(h?0:I)),u.length))===S)I=a(u,I,g);else{if(x.push(u.slice(S,I)),x.length===w)return x;for(var R=1;R<=T.length-1;R++)if(x.push(T[R]),x.length===w)return x;I=S=k}}return x.push(u.slice(S)),x}]}))},6108:(e,t,r)=>{"use strict";r(6774);var n=r(7007),i=r(3218),o=r(7057),a="toString",s=/./.toString,p=function(e){r(7234)(RegExp.prototype,a,e,!0)};r(4253)((function(){return"/a/b"!=s.call({source:"a",flags:"b"})}))?p((function(){var e=n(this);return"/".concat(e.source,"/","flags"in e?e.flags:!o&&e instanceof RegExp?i.call(e):void 0)})):s.name!=a&&p((function(){return s.call(this)}))},8184:(e,t,r)=>{"use strict";var n=r(9824),i=r(1616);e.exports=r(5795)("Set",(function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}}),{add:function(e){return n.def(i(this,"Set"),e=0===e?0:e,e)}},n)},856:(e,t,r)=>{"use strict";r(9395)("anchor",(function(e){return function(t){return e(this,"a","name",t)}}))},703:(e,t,r)=>{"use strict";r(9395)("big",(function(e){return function(){return e(this,"big","","")}}))},1539:(e,t,r)=>{"use strict";r(9395)("blink",(function(e){return function(){return e(this,"blink","","")}}))},5292:(e,t,r)=>{"use strict";r(9395)("bold",(function(e){return function(){return e(this,"b","","")}}))},9539:(e,t,r)=>{"use strict";var n=r(2985),i=r(4496)(!1);n(n.P,"String",{codePointAt:function(e){return i(this,e)}})},6620:(e,t,r)=>{"use strict";var n=r(2985),i=r(875),o=r(2094),a="endsWith",s="".endsWith;n(n.P+n.F*r(8852)(a),"String",{endsWith:function(e){var t=o(this,e,a),r=arguments.length>1?arguments[1]:void 0,n=i(t.length),p=void 0===r?n:Math.min(i(r),n),d=String(e);return s?s.call(t,d,p):t.slice(p-d.length,p)===d}})},6629:(e,t,r)=>{"use strict";r(9395)("fixed",(function(e){return function(){return e(this,"tt","","")}}))},3694:(e,t,r)=>{"use strict";r(9395)("fontcolor",(function(e){return function(t){return e(this,"font","color",t)}}))},7648:(e,t,r)=>{"use strict";r(9395)("fontsize",(function(e){return function(t){return e(this,"font","size",t)}}))},191:(e,t,r)=>{var n=r(2985),i=r(2337),o=String.fromCharCode,a=String.fromCodePoint;n(n.S+n.F*(!!a&&1!=a.length),"String",{fromCodePoint:function(e){for(var t,r=[],n=arguments.length,a=0;n>a;){if(t=+arguments[a++],i(t,1114111)!==t)throw RangeError(t+" is not a valid code point");r.push(t<65536?o(t):o(55296+((t-=65536)>>10),t%1024+56320))}return r.join("")}})},2850:(e,t,r)=>{"use strict";var n=r(2985),i=r(2094),o="includes";n(n.P+n.F*r(8852)(o),"String",{includes:function(e){return!!~i(this,e,o).indexOf(e,arguments.length>1?arguments[1]:void 0)}})},7795:(e,t,r)=>{"use strict";r(9395)("italics",(function(e){return function(){return e(this,"i","","")}}))},9115:(e,t,r)=>{"use strict";var n=r(4496)(!0);r(2923)(String,"String",(function(e){this._t=String(e),this._i=0}),(function(){var e,t=this._t,r=this._i;return r>=t.length?{value:void 0,done:!0}:(e=n(t,r),this._i+=e.length,{value:e,done:!1})}))},4531:(e,t,r)=>{"use strict";r(9395)("link",(function(e){return function(t){return e(this,"a","href",t)}}))},8306:(e,t,r)=>{var n=r(2985),i=r(2110),o=r(875);n(n.S,"String",{raw:function(e){for(var t=i(e.raw),r=o(t.length),n=arguments.length,a=[],s=0;r>s;)a.push(String(t[s++])),s<n&&a.push(String(arguments[s]));return a.join("")}})},823:(e,t,r)=>{var n=r(2985);n(n.P,"String",{repeat:r(8595)})},3605:(e,t,r)=>{"use strict";r(9395)("small",(function(e){return function(){return e(this,"small","","")}}))},7732:(e,t,r)=>{"use strict";var n=r(2985),i=r(875),o=r(2094),a="startsWith",s="".startsWith;n(n.P+n.F*r(8852)(a),"String",{startsWith:function(e){var t=o(this,e,a),r=i(Math.min(arguments.length>1?arguments[1]:void 0,t.length)),n=String(e);return s?s.call(t,n,r):t.slice(r,r+n.length)===n}})},6780:(e,t,r)=>{"use strict";r(9395)("strike",(function(e){return function(){return e(this,"strike","","")}}))},9937:(e,t,r)=>{"use strict";r(9395)("sub",(function(e){return function(){return e(this,"sub","","")}}))},511:(e,t,r)=>{"use strict";r(9395)("sup",(function(e){return function(){return e(this,"sup","","")}}))},4564:(e,t,r)=>{"use strict";r(9599)("trim",(function(e){return function(){return e(this,3)}}))},5767:(e,t,r)=>{"use strict";var n=r(3816),i=r(9181),o=r(7057),a=r(2985),s=r(7234),p=r(4728).KEY,d=r(4253),c=r(3825),l=r(2943),u=r(3953),m=r(6314),h=r(8787),f=r(6074),y=r(5541),g=r(4302),b=r(7007),v=r(5286),w=r(508),S=r(2110),I=r(1689),x=r(681),k=r(2503),T=r(9327),R=r(8693),C=r(4548),$=r(9275),A=r(7184),O=R.f,P=$.f,D=T.f,N=n.Symbol,j=n.JSON,E=j&&j.stringify,M=m("_hidden"),F=m("toPrimitive"),q={}.propertyIsEnumerable,L=c("symbol-registry"),B=c("symbols"),U=c("op-symbols"),W=Object.prototype,H="function"==typeof N&&!!C.f,_=n.QObject,V=!_||!_.prototype||!_.prototype.findChild,G=o&&d((function(){return 7!=k(P({},"a",{get:function(){return P(this,"a",{value:7}).a}})).a}))?function(e,t,r){var n=O(W,t);n&&delete W[t],P(e,t,r),n&&e!==W&&P(W,t,n)}:P,z=function(e){var t=B[e]=k(N.prototype);return t._k=e,t},J=H&&"symbol"==typeof N.iterator?function(e){return"symbol"==typeof e}:function(e){return e instanceof N},K=function(e,t,r){return e===W&&K(U,t,r),b(e),t=I(t,!0),b(r),i(B,t)?(r.enumerable?(i(e,M)&&e[M][t]&&(e[M][t]=!1),r=k(r,{enumerable:x(0,!1)})):(i(e,M)||P(e,M,x(1,{})),e[M][t]=!0),G(e,t,r)):P(e,t,r)},X=function(e,t){b(e);for(var r,n=y(t=S(t)),i=0,o=n.length;o>i;)K(e,r=n[i++],t[r]);return e},Y=function(e){var t=q.call(this,e=I(e,!0));return!(this===W&&i(B,e)&&!i(U,e))&&(!(t||!i(this,e)||!i(B,e)||i(this,M)&&this[M][e])||t)},Q=function(e,t){if(e=S(e),t=I(t,!0),e!==W||!i(B,t)||i(U,t)){var r=O(e,t);return!r||!i(B,t)||i(e,M)&&e[M][t]||(r.enumerable=!0),r}},Z=function(e){for(var t,r=D(S(e)),n=[],o=0;r.length>o;)i(B,t=r[o++])||t==M||t==p||n.push(t);return n},ee=function(e){for(var t,r=e===W,n=D(r?U:S(e)),o=[],a=0;n.length>a;)!i(B,t=n[a++])||r&&!i(W,t)||o.push(B[t]);return o};H||(s((N=function(){if(this instanceof N)throw TypeError("Symbol is not a constructor!");var e=u(arguments.length>0?arguments[0]:void 0),t=function(r){this===W&&t.call(U,r),i(this,M)&&i(this[M],e)&&(this[M][e]=!1),G(this,e,x(1,r))};return o&&V&&G(W,e,{configurable:!0,set:t}),z(e)}).prototype,"toString",(function(){return this._k})),R.f=Q,$.f=K,r(616).f=T.f=Z,r(4682).f=Y,C.f=ee,o&&!r(4461)&&s(W,"propertyIsEnumerable",Y,!0),h.f=function(e){return z(m(e))}),a(a.G+a.W+a.F*!H,{Symbol:N});for(var te="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),re=0;te.length>re;)m(te[re++]);for(var ne=A(m.store),ie=0;ne.length>ie;)f(ne[ie++]);a(a.S+a.F*!H,"Symbol",{for:function(e){return i(L,e+="")?L[e]:L[e]=N(e)},keyFor:function(e){if(!J(e))throw TypeError(e+" is not a symbol!");for(var t in L)if(L[t]===e)return t},useSetter:function(){V=!0},useSimple:function(){V=!1}}),a(a.S+a.F*!H,"Object",{create:function(e,t){return void 0===t?k(e):X(k(e),t)},defineProperty:K,defineProperties:X,getOwnPropertyDescriptor:Q,getOwnPropertyNames:Z,getOwnPropertySymbols:ee});var oe=d((function(){C.f(1)}));a(a.S+a.F*oe,"Object",{getOwnPropertySymbols:function(e){return C.f(w(e))}}),j&&a(a.S+a.F*(!H||d((function(){var e=N();return"[null]"!=E([e])||"{}"!=E({a:e})||"{}"!=E(Object(e))}))),"JSON",{stringify:function(e){for(var t,r,n=[e],i=1;arguments.length>i;)n.push(arguments[i++]);if(r=t=n[1],(v(t)||void 0!==e)&&!J(e))return g(t)||(t=function(e,t){if("function"==typeof r&&(t=r.call(this,e,t)),!J(t))return t}),n[1]=t,E.apply(j,n)}}),N.prototype[F]||r(7728)(N.prototype,F,N.prototype.valueOf),l(N,"Symbol"),l(Math,"Math",!0),l(n.JSON,"JSON",!0)},142:(e,t,r)=>{"use strict";var n=r(2985),i=r(9383),o=r(1125),a=r(7007),s=r(2337),p=r(875),d=r(5286),c=r(3816).ArrayBuffer,l=r(8364),u=o.ArrayBuffer,m=o.DataView,h=i.ABV&&c.isView,f=u.prototype.slice,y=i.VIEW,g="ArrayBuffer";n(n.G+n.W+n.F*(c!==u),{ArrayBuffer:u}),n(n.S+n.F*!i.CONSTR,g,{isView:function(e){return h&&h(e)||d(e)&&y in e}}),n(n.P+n.U+n.F*r(4253)((function(){return!new u(2).slice(1,void 0).byteLength})),g,{slice:function(e,t){if(void 0!==f&&void 0===t)return f.call(a(this),e);for(var r=a(this).byteLength,n=s(e,r),i=s(void 0===t?r:t,r),o=new(l(this,u))(p(i-n)),d=new m(this),c=new m(o),h=0;n<i;)c.setUint8(h++,d.getUint8(n++));return o}}),r(2974)(g)},1786:(e,t,r)=>{var n=r(2985);n(n.G+n.W+n.F*!r(9383).ABV,{DataView:r(1125).DataView})},162:(e,t,r)=>{r(8440)("Float32",4,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},3834:(e,t,r)=>{r(8440)("Float64",8,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},4821:(e,t,r)=>{r(8440)("Int16",2,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},1303:(e,t,r)=>{r(8440)("Int32",4,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},5368:(e,t,r)=>{r(8440)("Int8",1,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},9103:(e,t,r)=>{r(8440)("Uint16",2,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},3318:(e,t,r)=>{r(8440)("Uint32",4,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},6964:(e,t,r)=>{r(8440)("Uint8",1,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},2152:(e,t,r)=>{r(8440)("Uint8",1,(function(e){return function(t,r,n){return e(this,t,r,n)}}),!0)},147:(e,t,r)=>{"use strict";var n,i=r(3816),o=r(50)(0),a=r(7234),s=r(4728),p=r(5345),d=r(3657),c=r(5286),l=r(1616),u=r(1616),m=!i.ActiveXObject&&"ActiveXObject"in i,h="WeakMap",f=s.getWeak,y=Object.isExtensible,g=d.ufstore,b=function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}},v={get:function(e){if(c(e)){var t=f(e);return!0===t?g(l(this,h)).get(e):t?t[this._i]:void 0}},set:function(e,t){return d.def(l(this,h),e,t)}},w=e.exports=r(5795)(h,b,v,d,!0,!0);u&&m&&(p((n=d.getConstructor(b,h)).prototype,v),s.NEED=!0,o(["delete","has","get","set"],(function(e){var t=w.prototype,r=t[e];a(t,e,(function(t,i){if(c(t)&&!y(t)){this._f||(this._f=new n);var o=this._f[e](t,i);return"set"==e?this:o}return r.call(this,t,i)}))})))},9192:(e,t,r)=>{"use strict";var n=r(3657),i=r(1616),o="WeakSet";r(5795)(o,(function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}}),{add:function(e){return n.def(i(this,o),e,!0)}},n,!1,!0)},1268:(e,t,r)=>{"use strict";var n=r(2985),i=r(3325),o=r(508),a=r(875),s=r(4963),p=r(6886);n(n.P,"Array",{flatMap:function(e){var t,r,n=o(this);return s(e),t=a(n.length),r=p(n,0),i(r,n,n,t,0,1,e,arguments[1]),r}}),r(7722)("flatMap")},4692:(e,t,r)=>{"use strict";var n=r(2985),i=r(3325),o=r(508),a=r(875),s=r(1467),p=r(6886);n(n.P,"Array",{flatten:function(){var e=arguments[0],t=o(this),r=a(t.length),n=p(t,0);return i(n,t,t,r,0,void 0===e?1:s(e)),n}}),r(7722)("flatten")},2773:(e,t,r)=>{"use strict";var n=r(2985),i=r(9315)(!0);n(n.P,"Array",{includes:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(7722)("includes")},8267:(e,t,r)=>{var n=r(2985),i=r(4351)(),o=r(3816).process,a="process"==r(2032)(o);n(n.G,{asap:function(e){var t=a&&o.domain;i(t?t.bind(e):e)}})},2559:(e,t,r)=>{var n=r(2985),i=r(2032);n(n.S,"Error",{isError:function(e){return"Error"===i(e)}})},5575:(e,t,r)=>{var n=r(2985);n(n.G,{global:r(3816)})},525:(e,t,r)=>{r(1024)("Map")},8211:(e,t,r)=>{r(4881)("Map")},7698:(e,t,r)=>{var n=r(2985);n(n.P+n.R,"Map",{toJSON:r(6132)("Map")})},8865:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{clamp:function(e,t,r){return Math.min(r,Math.max(t,e))}})},368:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{DEG_PER_RAD:Math.PI/180})},6427:(e,t,r)=>{var n=r(2985),i=180/Math.PI;n(n.S,"Math",{degrees:function(e){return e*i}})},286:(e,t,r)=>{var n=r(2985),i=r(8757),o=r(4934);n(n.S,"Math",{fscale:function(e,t,r,n,a){return o(i(e,t,r,n,a))}})},2816:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{iaddh:function(e,t,r,n){var i=e>>>0,o=r>>>0;return(t>>>0)+(n>>>0)+((i&o|(i|o)&~(i+o>>>0))>>>31)|0}})},2082:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{imulh:function(e,t){var r=65535,n=+e,i=+t,o=n&r,a=i&r,s=n>>16,p=i>>16,d=(s*a>>>0)+(o*a>>>16);return s*p+(d>>16)+((o*p>>>0)+(d&r)>>16)}})},5986:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{isubh:function(e,t,r,n){var i=e>>>0,o=r>>>0;return(t>>>0)-(n>>>0)-((~i&o|~(i^o)&i-o>>>0)>>>31)|0}})},6308:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{RAD_PER_DEG:180/Math.PI})},9221:(e,t,r)=>{var n=r(2985),i=Math.PI/180;n(n.S,"Math",{radians:function(e){return e*i}})},3570:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{scale:r(8757)})},3776:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{signbit:function(e){return(e=+e)!=e?e:0==e?1/e==1/0:e>0}})},6754:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{umulh:function(e,t){var r=65535,n=+e,i=+t,o=n&r,a=i&r,s=n>>>16,p=i>>>16,d=(s*a>>>0)+(o*a>>>16);return s*p+(d>>>16)+((o*p>>>0)+(d&r)>>>16)}})},8646:(e,t,r)=>{"use strict";var n=r(2985),i=r(508),o=r(4963),a=r(9275);r(7057)&&n(n.P+r(1670),"Object",{__defineGetter__:function(e,t){a.f(i(this),e,{get:o(t),enumerable:!0,configurable:!0})}})},2658:(e,t,r)=>{"use strict";var n=r(2985),i=r(508),o=r(4963),a=r(9275);r(7057)&&n(n.P+r(1670),"Object",{__defineSetter__:function(e,t){a.f(i(this),e,{set:o(t),enumerable:!0,configurable:!0})}})},3276:(e,t,r)=>{var n=r(2985),i=r(1131)(!0);n(n.S,"Object",{entries:function(e){return i(e)}})},8351:(e,t,r)=>{var n=r(2985),i=r(7643),o=r(2110),a=r(8693),s=r(2811);n(n.S,"Object",{getOwnPropertyDescriptors:function(e){for(var t,r,n=o(e),p=a.f,d=i(n),c={},l=0;d.length>l;)void 0!==(r=p(n,t=d[l++]))&&s(c,t,r);return c}})},6917:(e,t,r)=>{"use strict";var n=r(2985),i=r(508),o=r(1689),a=r(468),s=r(8693).f;r(7057)&&n(n.P+r(1670),"Object",{__lookupGetter__:function(e){var t,r=i(this),n=o(e,!0);do{if(t=s(r,n))return t.get}while(r=a(r))}})},372:(e,t,r)=>{"use strict";var n=r(2985),i=r(508),o=r(1689),a=r(468),s=r(8693).f;r(7057)&&n(n.P+r(1670),"Object",{__lookupSetter__:function(e){var t,r=i(this),n=o(e,!0);do{if(t=s(r,n))return t.set}while(r=a(r))}})},6409:(e,t,r)=>{var n=r(2985),i=r(1131)(!1);n(n.S,"Object",{values:function(e){return i(e)}})},6534:(e,t,r)=>{"use strict";var n=r(2985),i=r(3816),o=r(5645),a=r(4351)(),s=r(6314)("observable"),p=r(4963),d=r(7007),c=r(3328),l=r(4408),u=r(7728),m=r(3531),h=m.RETURN,f=function(e){return null==e?void 0:p(e)},y=function(e){var t=e._c;t&&(e._c=void 0,t())},g=function(e){return void 0===e._o},b=function(e){g(e)||(e._o=void 0,y(e))},v=function(e,t){d(e),this._c=void 0,this._o=e,e=new w(this);try{var r=t(e),n=r;null!=r&&("function"==typeof r.unsubscribe?r=function(){n.unsubscribe()}:p(r),this._c=r)}catch(t){return void e.error(t)}g(this)&&y(this)};v.prototype=l({},{unsubscribe:function(){b(this)}});var w=function(e){this._s=e};w.prototype=l({},{next:function(e){var t=this._s;if(!g(t)){var r=t._o;try{var n=f(r.next);if(n)return n.call(r,e)}catch(e){try{b(t)}finally{throw e}}}},error:function(e){var t=this._s;if(g(t))throw e;var r=t._o;t._o=void 0;try{var n=f(r.error);if(!n)throw e;e=n.call(r,e)}catch(e){try{y(t)}finally{throw e}}return y(t),e},complete:function(e){var t=this._s;if(!g(t)){var r=t._o;t._o=void 0;try{var n=f(r.complete);e=n?n.call(r,e):void 0}catch(e){try{y(t)}finally{throw e}}return y(t),e}}});var S=function(e){c(this,S,"Observable","_f")._f=p(e)};l(S.prototype,{subscribe:function(e){return new v(e,this._f)},forEach:function(e){var t=this;return new(o.Promise||i.Promise)((function(r,n){p(e);var i=t.subscribe({next:function(t){try{return e(t)}catch(e){n(e),i.unsubscribe()}},error:n,complete:r})}))}}),l(S,{from:function(e){var t="function"==typeof this?this:S,r=f(d(e)[s]);if(r){var n=d(r.call(e));return n.constructor===t?n:new t((function(e){return n.subscribe(e)}))}return new t((function(t){var r=!1;return a((function(){if(!r){try{if(m(e,!1,(function(e){if(t.next(e),r)return h}))===h)return}catch(e){if(r)throw e;return void t.error(e)}t.complete()}})),function(){r=!0}}))},of:function(){for(var e=0,t=arguments.length,r=new Array(t);e<t;)r[e]=arguments[e++];return new("function"==typeof this?this:S)((function(e){var t=!1;return a((function(){if(!t){for(var n=0;n<r.length;++n)if(e.next(r[n]),t)return;e.complete()}})),function(){t=!0}}))}}),u(S.prototype,s,(function(){return this})),n(n.G,{Observable:S}),r(2974)("Observable")},9865:(e,t,r)=>{"use strict";var n=r(2985),i=r(5645),o=r(3816),a=r(8364),s=r(94);n(n.P+n.R,"Promise",{finally:function(e){var t=a(this,i.Promise||o.Promise),r="function"==typeof e;return this.then(r?function(r){return s(t,e()).then((function(){return r}))}:e,r?function(r){return s(t,e()).then((function(){throw r}))}:e)}})},1898:(e,t,r)=>{"use strict";var n=r(2985),i=r(3499),o=r(188);n(n.S,"Promise",{try:function(e){var t=i.f(this),r=o(e);return(r.e?t.reject:t.resolve)(r.v),t.promise}})},3364:(e,t,r)=>{var n=r(133),i=r(7007),o=n.key,a=n.set;n.exp({defineMetadata:function(e,t,r,n){a(e,t,i(r),o(n))}})},1432:(e,t,r)=>{var n=r(133),i=r(7007),o=n.key,a=n.map,s=n.store;n.exp({deleteMetadata:function(e,t){var r=arguments.length<3?void 0:o(arguments[2]),n=a(i(t),r,!1);if(void 0===n||!n.delete(e))return!1;if(n.size)return!0;var p=s.get(t);return p.delete(r),!!p.size||s.delete(t)}})},4416:(e,t,r)=>{var n=r(8184),i=r(9490),o=r(133),a=r(7007),s=r(468),p=o.keys,d=o.key,c=function(e,t){var r=p(e,t),o=s(e);if(null===o)return r;var a=c(o,t);return a.length?r.length?i(new n(r.concat(a))):a:r};o.exp({getMetadataKeys:function(e){return c(a(e),arguments.length<2?void 0:d(arguments[1]))}})},6562:(e,t,r)=>{var n=r(133),i=r(7007),o=r(468),a=n.has,s=n.get,p=n.key,d=function(e,t,r){if(a(e,t,r))return s(e,t,r);var n=o(t);return null!==n?d(e,n,r):void 0};n.exp({getMetadata:function(e,t){return d(e,i(t),arguments.length<3?void 0:p(arguments[2]))}})},2213:(e,t,r)=>{var n=r(133),i=r(7007),o=n.keys,a=n.key;n.exp({getOwnMetadataKeys:function(e){return o(i(e),arguments.length<2?void 0:a(arguments[1]))}})},8681:(e,t,r)=>{var n=r(133),i=r(7007),o=n.get,a=n.key;n.exp({getOwnMetadata:function(e,t){return o(e,i(t),arguments.length<3?void 0:a(arguments[2]))}})},3471:(e,t,r)=>{var n=r(133),i=r(7007),o=r(468),a=n.has,s=n.key,p=function(e,t,r){if(a(e,t,r))return!0;var n=o(t);return null!==n&&p(e,n,r)};n.exp({hasMetadata:function(e,t){return p(e,i(t),arguments.length<3?void 0:s(arguments[2]))}})},4329:(e,t,r)=>{var n=r(133),i=r(7007),o=n.has,a=n.key;n.exp({hasOwnMetadata:function(e,t){return o(e,i(t),arguments.length<3?void 0:a(arguments[2]))}})},5159:(e,t,r)=>{var n=r(133),i=r(7007),o=r(4963),a=n.key,s=n.set;n.exp({metadata:function(e,t){return function(r,n){s(e,t,(void 0!==n?i:o)(r),a(n))}}})},9467:(e,t,r)=>{r(1024)("Set")},4837:(e,t,r)=>{r(4881)("Set")},8739:(e,t,r)=>{var n=r(2985);n(n.P+n.R,"Set",{toJSON:r(6132)("Set")})},7220:(e,t,r)=>{"use strict";var n=r(2985),i=r(4496)(!0),o=r(4253)((function(){return"𠮷"!=="𠮷".at(0)}));n(n.P+n.F*o,"String",{at:function(e){return i(this,e)}})},4208:(e,t,r)=>{"use strict";var n=r(2985),i=r(1355),o=r(875),a=r(5364),s=r(3218),p=RegExp.prototype,d=function(e,t){this._r=e,this._s=t};r(9988)(d,"RegExp String",(function(){var e=this._r.exec(this._s);return{value:e,done:null===e}})),n(n.P,"String",{matchAll:function(e){if(i(this),!a(e))throw TypeError(e+" is not a regexp!");var t=String(this),r="flags"in p?String(e.flags):s.call(e),n=new RegExp(e.source,~r.indexOf("g")?r:"g"+r);return n.lastIndex=o(e.lastIndex),new d(n,t)}})},2770:(e,t,r)=>{"use strict";var n=r(2985),i=r(5442),o=r(575),a=/Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(o);n(n.P+n.F*a,"String",{padEnd:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!1)}})},1784:(e,t,r)=>{"use strict";var n=r(2985),i=r(5442),o=r(575),a=/Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(o);n(n.P+n.F*a,"String",{padStart:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!0)}})},5869:(e,t,r)=>{"use strict";r(9599)("trimLeft",(function(e){return function(){return e(this,1)}}),"trimStart")},4325:(e,t,r)=>{"use strict";r(9599)("trimRight",(function(e){return function(){return e(this,2)}}),"trimEnd")},9665:(e,t,r)=>{r(6074)("asyncIterator")},9593:(e,t,r)=>{r(6074)("observable")},8967:(e,t,r)=>{var n=r(2985);n(n.S,"System",{global:r(3816)})},4188:(e,t,r)=>{r(1024)("WeakMap")},7594:(e,t,r)=>{r(4881)("WeakMap")},3495:(e,t,r)=>{r(1024)("WeakSet")},9550:(e,t,r)=>{r(4881)("WeakSet")},1181:(e,t,r)=>{for(var n=r(6997),i=r(7184),o=r(7234),a=r(3816),s=r(7728),p=r(2803),d=r(6314),c=d("iterator"),l=d("toStringTag"),u=p.Array,m={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},h=i(m),f=0;f<h.length;f++){var y,g=h[f],b=m[g],v=a[g],w=v&&v.prototype;if(w&&(w[c]||s(w,c,u),w[l]||s(w,l,g),p[g]=u,b))for(y in n)w[y]||o(w,y,n[y],!0)}},4633:(e,t,r)=>{var n=r(2985),i=r(4193);n(n.G+n.B,{setImmediate:i.set,clearImmediate:i.clear})},2564:(e,t,r)=>{var n=r(3816),i=r(2985),o=r(575),a=[].slice,s=/MSIE .\./.test(o),p=function(e){return function(t,r){var n=arguments.length>2,i=!!n&&a.call(arguments,2);return e(n?function(){("function"==typeof t?t:Function(t)).apply(this,i)}:t,r)}};i(i.G+i.B+i.F*s,{setTimeout:p(n.setTimeout),setInterval:p(n.setInterval)})},1934:(e,t,r)=>{r(5767),r(8132),r(8388),r(7470),r(4882),r(1520),r(7476),r(9622),r(9375),r(3533),r(4672),r(4157),r(5095),r(9892),r(5115),r(9176),r(8838),r(6253),r(9730),r(6059),r(8377),r(1084),r(4299),r(1246),r(726),r(1901),r(5972),r(3403),r(2516),r(9371),r(6479),r(1736),r(1889),r(5177),r(6943),r(6503),r(6786),r(932),r(7526),r(1591),r(9073),r(347),r(579),r(4669),r(7710),r(5789),r(3514),r(9978),r(8472),r(6946),r(5068),r(413),r(191),r(8306),r(4564),r(9115),r(9539),r(6620),r(2850),r(823),r(7732),r(856),r(703),r(1539),r(5292),r(6629),r(3694),r(7648),r(7795),r(4531),r(3605),r(6780),r(9937),r(511),r(1822),r(9977),r(1031),r(6331),r(1560),r(774),r(522),r(8295),r(7842),r(110),r(75),r(4336),r(1802),r(8837),r(6773),r(5745),r(3057),r(3750),r(3369),r(9564),r(2e3),r(8977),r(2310),r(4899),r(1842),r(6997),r(3946),r(8269),r(6108),r(6774),r(1466),r(9357),r(6142),r(1876),r(851),r(8416),r(8184),r(147),r(9192),r(142),r(1786),r(5368),r(6964),r(2152),r(4821),r(9103),r(1303),r(3318),r(162),r(3834),r(1572),r(2139),r(685),r(5535),r(7347),r(3049),r(6633),r(8989),r(8270),r(4510),r(3984),r(5769),r(55),r(6014),r(2773),r(1268),r(4692),r(7220),r(1784),r(2770),r(5869),r(4325),r(4208),r(9665),r(9593),r(8351),r(6409),r(3276),r(8646),r(2658),r(6917),r(372),r(7698),r(8739),r(8211),r(4837),r(7594),r(9550),r(525),r(9467),r(4188),r(3495),r(5575),r(8967),r(2559),r(8865),r(368),r(6427),r(286),r(2816),r(5986),r(2082),r(6308),r(9221),r(3570),r(6754),r(3776),r(9865),r(1898),r(3364),r(1432),r(6562),r(4416),r(8681),r(2213),r(3471),r(4329),r(5159),r(8267),r(6534),r(2564),r(4633),r(1181),e.exports=r(5645)},7187:e=>{"use strict";var t,r="object"==typeof Reflect?Reflect:null,n=r&&"function"==typeof r.apply?r.apply:function(e,t,r){return Function.prototype.apply.call(e,t,r)};t=r&&"function"==typeof r.ownKeys?r.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var i=Number.isNaN||function(e){return e!=e};function o(){o.init.call(this)}e.exports=o,e.exports.once=function(e,t){return new Promise((function(r,n){function i(r){e.removeListener(t,o),n(r)}function o(){"function"==typeof e.removeListener&&e.removeListener("error",i),r([].slice.call(arguments))}f(e,t,o,{once:!0}),"error"!==t&&function(e,t,r){"function"==typeof e.on&&f(e,"error",t,r)}(e,i,{once:!0})}))},o.EventEmitter=o,o.prototype._events=void 0,o.prototype._eventsCount=0,o.prototype._maxListeners=void 0;var a=10;function s(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function p(e){return void 0===e._maxListeners?o.defaultMaxListeners:e._maxListeners}function d(e,t,r,n){var i,o,a,d;if(s(r),void 0===(o=e._events)?(o=e._events=Object.create(null),e._eventsCount=0):(void 0!==o.newListener&&(e.emit("newListener",t,r.listener?r.listener:r),o=e._events),a=o[t]),void 0===a)a=o[t]=r,++e._eventsCount;else if("function"==typeof a?a=o[t]=n?[r,a]:[a,r]:n?a.unshift(r):a.push(r),(i=p(e))>0&&a.length>i&&!a.warned){a.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+a.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=a.length,d=c,console&&console.warn&&console.warn(d)}return e}function c(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function l(e,t,r){var n={fired:!1,wrapFn:void 0,target:e,type:t,listener:r},i=c.bind(n);return i.listener=r,n.wrapFn=i,i}function u(e,t,r){var n=e._events;if(void 0===n)return[];var i=n[t];return void 0===i?[]:"function"==typeof i?r?[i.listener||i]:[i]:r?function(e){for(var t=new Array(e.length),r=0;r<t.length;++r)t[r]=e[r].listener||e[r];return t}(i):h(i,i.length)}function m(e){var t=this._events;if(void 0!==t){var r=t[e];if("function"==typeof r)return 1;if(void 0!==r)return r.length}return 0}function h(e,t){for(var r=new Array(t),n=0;n<t;++n)r[n]=e[n];return r}function f(e,t,r,n){if("function"==typeof e.on)n.once?e.once(t,r):e.on(t,r);else{if("function"!=typeof e.addEventListener)throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type '+typeof e);e.addEventListener(t,(function i(o){n.once&&e.removeEventListener(t,i),r(o)}))}}Object.defineProperty(o,"defaultMaxListeners",{enumerable:!0,get:function(){return a},set:function(e){if("number"!=typeof e||e<0||i(e))throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received '+e+".");a=e}}),o.init=function(){void 0!==this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=Object.create(null),this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},o.prototype.setMaxListeners=function(e){if("number"!=typeof e||e<0||i(e))throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received '+e+".");return this._maxListeners=e,this},o.prototype.getMaxListeners=function(){return p(this)},o.prototype.emit=function(e){for(var t=[],r=1;r<arguments.length;r++)t.push(arguments[r]);var i="error"===e,o=this._events;if(void 0!==o)i=i&&void 0===o.error;else if(!i)return!1;if(i){var a;if(t.length>0&&(a=t[0]),a instanceof Error)throw a;var s=new Error("Unhandled error."+(a?" ("+a.message+")":""));throw s.context=a,s}var p=o[e];if(void 0===p)return!1;if("function"==typeof p)n(p,this,t);else{var d=p.length,c=h(p,d);for(r=0;r<d;++r)n(c[r],this,t)}return!0},o.prototype.addListener=function(e,t){return d(this,e,t,!1)},o.prototype.on=o.prototype.addListener,o.prototype.prependListener=function(e,t){return d(this,e,t,!0)},o.prototype.once=function(e,t){return s(t),this.on(e,l(this,e,t)),this},o.prototype.prependOnceListener=function(e,t){return s(t),this.prependListener(e,l(this,e,t)),this},o.prototype.removeListener=function(e,t){var r,n,i,o,a;if(s(t),void 0===(n=this._events))return this;if(void 0===(r=n[e]))return this;if(r===t||r.listener===t)0==--this._eventsCount?this._events=Object.create(null):(delete n[e],n.removeListener&&this.emit("removeListener",e,r.listener||t));else if("function"!=typeof r){for(i=-1,o=r.length-1;o>=0;o--)if(r[o]===t||r[o].listener===t){a=r[o].listener,i=o;break}if(i<0)return this;0===i?r.shift():function(e,t){for(;t+1<e.length;t++)e[t]=e[t+1];e.pop()}(r,i),1===r.length&&(n[e]=r[0]),void 0!==n.removeListener&&this.emit("removeListener",e,a||t)}return this},o.prototype.off=o.prototype.removeListener,o.prototype.removeAllListeners=function(e){var t,r,n;if(void 0===(r=this._events))return this;if(void 0===r.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==r[e]&&(0==--this._eventsCount?this._events=Object.create(null):delete r[e]),this;if(0===arguments.length){var i,o=Object.keys(r);for(n=0;n<o.length;++n)"removeListener"!==(i=o[n])&&this.removeAllListeners(i);return this.removeAllListeners("removeListener"),this._events=Object.create(null),this._eventsCount=0,this}if("function"==typeof(t=r[e]))this.removeListener(e,t);else if(void 0!==t)for(n=t.length-1;n>=0;n--)this.removeListener(e,t[n]);return this},o.prototype.listeners=function(e){return u(this,e,!0)},o.prototype.rawListeners=function(e){return u(this,e,!1)},o.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):m.call(e,t)},o.prototype.listenerCount=m,o.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]}},4029:(e,t,r)=>{"use strict";var n=r(5320),i=Object.prototype.toString,o=Object.prototype.hasOwnProperty,a=function(e,t,r){for(var n=0,i=e.length;n<i;n++)o.call(e,n)&&(null==r?t(e[n],n,e):t.call(r,e[n],n,e))},s=function(e,t,r){for(var n=0,i=e.length;n<i;n++)null==r?t(e.charAt(n),n,e):t.call(r,e.charAt(n),n,e)},p=function(e,t,r){for(var n in e)o.call(e,n)&&(null==r?t(e[n],n,e):t.call(r,e[n],n,e))};e.exports=function(e,t,r){if(!n(t))throw new TypeError("iterator must be a function");var o;arguments.length>=3&&(o=r),"[object Array]"===i.call(e)?a(e,t,o):"string"==typeof e?s(e,t,o):p(e,t,o)}},9092:e=>{"use strict";var t="Function.prototype.bind called on incompatible ",r=Array.prototype.slice,n=Object.prototype.toString,i="[object Function]";e.exports=function(e){var o=this;if("function"!=typeof o||n.call(o)!==i)throw new TypeError(t+o);for(var a,s=r.call(arguments,1),p=function(){if(this instanceof a){var t=o.apply(this,s.concat(r.call(arguments)));return Object(t)===t?t:this}return o.apply(e,s.concat(r.call(arguments)))},d=Math.max(0,o.length-s.length),c=[],l=0;l<d;l++)c.push("$"+l);if(a=Function("binder","return function ("+c.join(",")+"){ return binder.apply(this,arguments); }")(p),o.prototype){var u=function(){};u.prototype=o.prototype,a.prototype=new u,u.prototype=null}return a}},8612:(e,t,r)=>{"use strict";var n=r(9092);e.exports=Function.prototype.bind||n},210:(e,t,r)=>{"use strict";var n,i=SyntaxError,o=Function,a=TypeError,s=function(e){try{return o('"use strict"; return ('+e+").constructor;")()}catch(e){}},p=Object.getOwnPropertyDescriptor;if(p)try{p({},"")}catch(e){p=null}var d=function(){throw new a},c=p?function(){try{return d}catch(e){try{return p(arguments,"callee").get}catch(e){return d}}}():d,l=r(1405)(),u=Object.getPrototypeOf||function(e){return e.__proto__},m={},h="undefined"==typeof Uint8Array?n:u(Uint8Array),f={"%AggregateError%":"undefined"==typeof AggregateError?n:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?n:ArrayBuffer,"%ArrayIteratorPrototype%":l?u([][Symbol.iterator]()):n,"%AsyncFromSyncIteratorPrototype%":n,"%AsyncFunction%":m,"%AsyncGenerator%":m,"%AsyncGeneratorFunction%":m,"%AsyncIteratorPrototype%":m,"%Atomics%":"undefined"==typeof Atomics?n:Atomics,"%BigInt%":"undefined"==typeof BigInt?n:BigInt,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?n:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":"undefined"==typeof Float32Array?n:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?n:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?n:FinalizationRegistry,"%Function%":o,"%GeneratorFunction%":m,"%Int8Array%":"undefined"==typeof Int8Array?n:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?n:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?n:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":l?u(u([][Symbol.iterator]())):n,"%JSON%":"object"==typeof JSON?JSON:n,"%Map%":"undefined"==typeof Map?n:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&l?u((new Map)[Symbol.iterator]()):n,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?n:Promise,"%Proxy%":"undefined"==typeof Proxy?n:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":"undefined"==typeof Reflect?n:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?n:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&l?u((new Set)[Symbol.iterator]()):n,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?n:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":l?u(""[Symbol.iterator]()):n,"%Symbol%":l?Symbol:n,"%SyntaxError%":i,"%ThrowTypeError%":c,"%TypedArray%":h,"%TypeError%":a,"%Uint8Array%":"undefined"==typeof Uint8Array?n:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?n:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?n:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?n:Uint32Array,"%URIError%":URIError,"%WeakMap%":"undefined"==typeof WeakMap?n:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?n:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?n:WeakSet},y=function e(t){var r;if("%AsyncFunction%"===t)r=s("async function () {}");else if("%GeneratorFunction%"===t)r=s("function* () {}");else if("%AsyncGeneratorFunction%"===t)r=s("async function* () {}");else if("%AsyncGenerator%"===t){var n=e("%AsyncGeneratorFunction%");n&&(r=n.prototype)}else if("%AsyncIteratorPrototype%"===t){var i=e("%AsyncGenerator%");i&&(r=u(i.prototype))}return f[t]=r,r},g={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},b=r(8612),v=r(7642),w=b.call(Function.call,Array.prototype.concat),S=b.call(Function.apply,Array.prototype.splice),I=b.call(Function.call,String.prototype.replace),x=b.call(Function.call,String.prototype.slice),k=b.call(Function.call,RegExp.prototype.exec),T=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,R=/\\(\\)?/g,C=function(e){var t=x(e,0,1),r=x(e,-1);if("%"===t&&"%"!==r)throw new i("invalid intrinsic syntax, expected closing `%`");if("%"===r&&"%"!==t)throw new i("invalid intrinsic syntax, expected opening `%`");var n=[];return I(e,T,(function(e,t,r,i){n[n.length]=r?I(i,R,"$1"):t||e})),n},$=function(e,t){var r,n=e;if(v(g,n)&&(n="%"+(r=g[n])[0]+"%"),v(f,n)){var o=f[n];if(o===m&&(o=y(n)),void 0===o&&!t)throw new a("intrinsic "+e+" exists, but is not available. Please file an issue!");return{alias:r,name:n,value:o}}throw new i("intrinsic "+e+" does not exist!")};e.exports=function(e,t){if("string"!=typeof e||0===e.length)throw new a("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof t)throw new a('"allowMissing" argument must be a boolean');if(null===k(/^%?[^%]*%?$/g,e))throw new i("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var r=C(e),n=r.length>0?r[0]:"",o=$("%"+n+"%",t),s=o.name,d=o.value,c=!1,l=o.alias;l&&(n=l[0],S(r,w([0,1],l)));for(var u=1,m=!0;u<r.length;u+=1){var h=r[u],y=x(h,0,1),g=x(h,-1);if(('"'===y||"'"===y||"`"===y||'"'===g||"'"===g||"`"===g)&&y!==g)throw new i("property names with quotes must have matching quotes");if("constructor"!==h&&m||(c=!0),v(f,s="%"+(n+="."+h)+"%"))d=f[s];else if(null!=d){if(!(h in d)){if(!t)throw new a("base intrinsic for "+e+" exists, but the property is not available.");return}if(p&&u+1>=r.length){var b=p(d,h);d=(m=!!b)&&"get"in b&&!("originalValue"in b.get)?b.get:d[h]}else m=v(d,h),d=d[h];m&&!c&&(f[s]=d)}}return d}},1405:(e,t,r)=>{"use strict";var n="undefined"!=typeof Symbol&&Symbol,i=r(5419);e.exports=function(){return"function"==typeof n&&("function"==typeof Symbol&&("symbol"==typeof n("foo")&&("symbol"==typeof Symbol("bar")&&i())))}},5419:e=>{"use strict";e.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var e={},t=Symbol("test"),r=Object(t);if("string"==typeof t)return!1;if("[object Symbol]"!==Object.prototype.toString.call(t))return!1;if("[object Symbol]"!==Object.prototype.toString.call(r))return!1;for(t in e[t]=42,e)return!1;if("function"==typeof Object.keys&&0!==Object.keys(e).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(e).length)return!1;var n=Object.getOwnPropertySymbols(e);if(1!==n.length||n[0]!==t)return!1;if(!Object.prototype.propertyIsEnumerable.call(e,t))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var i=Object.getOwnPropertyDescriptor(e,t);if(42!==i.value||!0!==i.enumerable)return!1}return!0}},6410:(e,t,r)=>{"use strict";var n=r(5419);e.exports=function(){return n()&&!!Symbol.toStringTag}},7642:(e,t,r)=>{"use strict";var n=r(8612);e.exports=n.call(Function.call,Object.prototype.hasOwnProperty)},5717:e=>{"function"==typeof Object.create?e.exports=function(e,t){t&&(e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}))}:e.exports=function(e,t){if(t){e.super_=t;var r=function(){};r.prototype=t.prototype,e.prototype=new r,e.prototype.constructor=e}}},2584:(e,t,r)=>{"use strict";var n=r(6410)(),i=r(1924)("Object.prototype.toString"),o=function(e){return!(n&&e&&"object"==typeof e&&Symbol.toStringTag in e)&&"[object Arguments]"===i(e)},a=function(e){return!!o(e)||null!==e&&"object"==typeof e&&"number"==typeof e.length&&e.length>=0&&"[object Array]"!==i(e)&&"[object Function]"===i(e.callee)},s=function(){return o(arguments)}();o.isLegacyArguments=a,e.exports=s?o:a},5320:e=>{"use strict";var t,r,n=Function.prototype.toString,i="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof i&&"function"==typeof Object.defineProperty)try{t=Object.defineProperty({},"length",{get:function(){throw r}}),r={},i((function(){throw 42}),null,t)}catch(e){e!==r&&(i=null)}else i=null;var o=/^\s*class\b/,a=function(e){try{var t=n.call(e);return o.test(t)}catch(e){return!1}},s=Object.prototype.toString,p="function"==typeof Symbol&&!!Symbol.toStringTag,d="object"==typeof document&&void 0===document.all&&void 0!==document.all?document.all:{};e.exports=i?function(e){if(e===d)return!0;if(!e)return!1;if("function"!=typeof e&&"object"!=typeof e)return!1;if("function"==typeof e&&!e.prototype)return!0;try{i(e,null,t)}catch(e){if(e!==r)return!1}return!a(e)}:function(e){if(e===d)return!0;if(!e)return!1;if("function"!=typeof e&&"object"!=typeof e)return!1;if("function"==typeof e&&!e.prototype)return!0;if(p)return function(e){try{return!a(e)&&(n.call(e),!0)}catch(e){return!1}}(e);if(a(e))return!1;var t=s.call(e);return"[object Function]"===t||"[object GeneratorFunction]"===t}},8662:(e,t,r)=>{"use strict";var n,i=Object.prototype.toString,o=Function.prototype.toString,a=/^\s*(?:function)?\*/,s=r(6410)(),p=Object.getPrototypeOf;e.exports=function(e){if("function"!=typeof e)return!1;if(a.test(o.call(e)))return!0;if(!s)return"[object GeneratorFunction]"===i.call(e);if(!p)return!1;if(void 0===n){var t=function(){if(!s)return!1;try{return Function("return function*() {}")()}catch(e){}}();n=!!t&&p(t)}return p(e)===n}},5692:(e,t,r)=>{"use strict";var n=r(4029),i=r(3083),o=r(1924),a=o("Object.prototype.toString"),s=r(6410)(),p="undefined"==typeof globalThis?r.g:globalThis,d=i(),c=o("Array.prototype.indexOf",!0)||function(e,t){for(var r=0;r<e.length;r+=1)if(e[r]===t)return r;return-1},l=o("String.prototype.slice"),u={},m=r(882),h=Object.getPrototypeOf;s&&m&&h&&n(d,(function(e){var t=new p[e];if(Symbol.toStringTag in t){var r=h(t),n=m(r,Symbol.toStringTag);if(!n){var i=h(r);n=m(i,Symbol.toStringTag)}u[e]=n.get}}));e.exports=function(e){if(!e||"object"!=typeof e)return!1;if(!s||!(Symbol.toStringTag in e)){var t=l(a(e),8,-1);return c(d,t)>-1}return!!m&&function(e){var t=!1;return n(u,(function(r,n){if(!t)try{t=r.call(e)===n}catch(e){}})),t}(e)}},4155:e=>{var t,r,n=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function o(){throw new Error("clearTimeout has not been defined")}function a(e){if(t===setTimeout)return setTimeout(e,0);if((t===i||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(r){try{return t.call(null,e,0)}catch(r){return t.call(this,e,0)}}}!function(){try{t="function"==typeof setTimeout?setTimeout:i}catch(e){t=i}try{r="function"==typeof clearTimeout?clearTimeout:o}catch(e){r=o}}();var s,p=[],d=!1,c=-1;function l(){d&&s&&(d=!1,s.length?p=s.concat(p):c=-1,p.length&&u())}function u(){if(!d){var e=a(l);d=!0;for(var t=p.length;t;){for(s=p,p=[];++c<t;)s&&s[c].run();c=-1,t=p.length}s=null,d=!1,function(e){if(r===clearTimeout)return clearTimeout(e);if((r===o||!r)&&clearTimeout)return r=clearTimeout,clearTimeout(e);try{r(e)}catch(t){try{return r.call(null,e)}catch(t){return r.call(this,e)}}}(e)}}function m(e,t){this.fun=e,this.array=t}function h(){}n.nextTick=function(e){var t=new Array(arguments.length-1);if(arguments.length>1)for(var r=1;r<arguments.length;r++)t[r-1]=arguments[r];p.push(new m(e,t)),1!==p.length||d||a(u)},m.prototype.run=function(){this.fun.apply(null,this.array)},n.title="browser",n.browser=!0,n.env={},n.argv=[],n.version="",n.versions={},n.on=h,n.addListener=h,n.once=h,n.off=h,n.removeListener=h,n.removeAllListeners=h,n.emit=h,n.prependListener=h,n.prependOnceListener=h,n.listeners=function(e){return[]},n.binding=function(e){throw new Error("process.binding is not supported")},n.cwd=function(){return"/"},n.chdir=function(e){throw new Error("process.chdir is not supported")},n.umask=function(){return 0}},2587:e=>{"use strict";function t(e,t){return Object.prototype.hasOwnProperty.call(e,t)}e.exports=function(e,r,n,i){r=r||"&",n=n||"=";var o={};if("string"!=typeof e||0===e.length)return o;var a=/\+/g;e=e.split(r);var s=1e3;i&&"number"==typeof i.maxKeys&&(s=i.maxKeys);var p=e.length;s>0&&p>s&&(p=s);for(var d=0;d<p;++d){var c,l,u,m,h=e[d].replace(a,"%20"),f=h.indexOf(n);f>=0?(c=h.substr(0,f),l=h.substr(f+1)):(c=h,l=""),u=decodeURIComponent(c),m=decodeURIComponent(l),t(o,u)?Array.isArray(o[u])?o[u].push(m):o[u]=[o[u],m]:o[u]=m}return o}},2361:e=>{"use strict";var t=function(e){switch(typeof e){case"string":return e;case"boolean":return e?"true":"false";case"number":return isFinite(e)?e:"";default:return""}};e.exports=function(e,r,n,i){return r=r||"&",n=n||"=",null===e&&(e=void 0),"object"==typeof e?Object.keys(e).map((function(i){var o=encodeURIComponent(t(i))+n;return Array.isArray(e[i])?e[i].map((function(e){return o+encodeURIComponent(t(e))})).join(r):o+encodeURIComponent(t(e[i]))})).join(r):i?encodeURIComponent(t(i))+n+encodeURIComponent(t(e)):""}},7673:(e,t,r)=>{"use strict";t.decode=t.parse=r(2587),t.encode=t.stringify=r(2361)},5666:function(e,t,r){!function(t){"use strict";var r,n=Object.prototype,i=n.hasOwnProperty,o="function"==typeof Symbol?Symbol:{},a=o.iterator||"@@iterator",s=o.asyncIterator||"@@asyncIterator",p=o.toStringTag||"@@toStringTag",d=t.regeneratorRuntime;if(d)e.exports=d;else{(d=t.regeneratorRuntime=e.exports).wrap=v;var c="suspendedStart",l="suspendedYield",u="executing",m="completed",h={},f={};f[a]=function(){return this};var y=Object.getPrototypeOf,g=y&&y(y(O([])));g&&g!==n&&i.call(g,a)&&(f=g);var b=x.prototype=S.prototype=Object.create(f);I.prototype=b.constructor=x,x.constructor=I,x[p]=I.displayName="GeneratorFunction",d.isGeneratorFunction=function(e){var t="function"==typeof e&&e.constructor;return!!t&&(t===I||"GeneratorFunction"===(t.displayName||t.name))},d.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,x):(e.__proto__=x,p in e||(e[p]="GeneratorFunction")),e.prototype=Object.create(b),e},d.awrap=function(e){return{__await:e}},k(T.prototype),T.prototype[s]=function(){return this},d.AsyncIterator=T,d.async=function(e,t,r,n){var i=new T(v(e,t,r,n));return d.isGeneratorFunction(t)?i:i.next().then((function(e){return e.done?e.value:i.next()}))},k(b),b[p]="Generator",b[a]=function(){return this},b.toString=function(){return"[object Generator]"},d.keys=function(e){var t=[];for(var r in e)t.push(r);return t.reverse(),function r(){for(;t.length;){var n=t.pop();if(n in e)return r.value=n,r.done=!1,r}return r.done=!0,r}},d.values=O,A.prototype={constructor:A,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=r,this.done=!1,this.delegate=null,this.method="next",this.arg=r,this.tryEntries.forEach($),!e)for(var t in this)"t"===t.charAt(0)&&i.call(this,t)&&!isNaN(+t.slice(1))&&(this[t]=r)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var t=this;function n(n,i){return s.type="throw",s.arg=e,t.next=n,i&&(t.method="next",t.arg=r),!!i}for(var o=this.tryEntries.length-1;o>=0;--o){var a=this.tryEntries[o],s=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var p=i.call(a,"catchLoc"),d=i.call(a,"finallyLoc");if(p&&d){if(this.prev<a.catchLoc)return n(a.catchLoc,!0);if(this.prev<a.finallyLoc)return n(a.finallyLoc)}else if(p){if(this.prev<a.catchLoc)return n(a.catchLoc,!0)}else{if(!d)throw new Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return n(a.finallyLoc)}}}},abrupt:function(e,t){for(var r=this.tryEntries.length-1;r>=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&i.call(n,"finallyLoc")&&this.prev<n.finallyLoc){var o=n;break}}o&&("break"===e||"continue"===e)&&o.tryLoc<=t&&t<=o.finallyLoc&&(o=null);var a=o?o.completion:{};return a.type=e,a.arg=t,o?(this.method="next",this.next=o.finallyLoc,h):this.complete(a)},complete:function(e,t){if("throw"===e.type)throw e.arg;return"break"===e.type||"continue"===e.type?this.next=e.arg:"return"===e.type?(this.rval=this.arg=e.arg,this.method="return",this.next="end"):"normal"===e.type&&t&&(this.next=t),h},finish:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),$(r),h}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var i=n.arg;$(r)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,n){return this.delegate={iterator:O(e),resultName:t,nextLoc:n},"next"===this.method&&(this.arg=r),h}}}function v(e,t,r,n){var i=t&&t.prototype instanceof S?t:S,o=Object.create(i.prototype),a=new A(n||[]);return o._invoke=function(e,t,r){var n=c;return function(i,o){if(n===u)throw new Error("Generator is already running");if(n===m){if("throw"===i)throw o;return P()}for(r.method=i,r.arg=o;;){var a=r.delegate;if(a){var s=R(a,r);if(s){if(s===h)continue;return s}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(n===c)throw n=m,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n=u;var p=w(e,t,r);if("normal"===p.type){if(n=r.done?m:l,p.arg===h)continue;return{value:p.arg,done:r.done}}"throw"===p.type&&(n=m,r.method="throw",r.arg=p.arg)}}}(e,r,a),o}function w(e,t,r){try{return{type:"normal",arg:e.call(t,r)}}catch(e){return{type:"throw",arg:e}}}function S(){}function I(){}function x(){}function k(e){["next","throw","return"].forEach((function(t){e[t]=function(e){return this._invoke(t,e)}}))}function T(e){function r(t,n,o,a){var s=w(e[t],e,n);if("throw"!==s.type){var p=s.arg,d=p.value;return d&&"object"==typeof d&&i.call(d,"__await")?Promise.resolve(d.__await).then((function(e){r("next",e,o,a)}),(function(e){r("throw",e,o,a)})):Promise.resolve(d).then((function(e){p.value=e,o(p)}),a)}a(s.arg)}var n;"object"==typeof t.process&&t.process.domain&&(r=t.process.domain.bind(r)),this._invoke=function(e,t){function i(){return new Promise((function(n,i){r(e,t,n,i)}))}return n=n?n.then(i,i):i()}}function R(e,t){var n=e.iterator[t.method];if(n===r){if(t.delegate=null,"throw"===t.method){if(e.iterator.return&&(t.method="return",t.arg=r,R(e,t),"throw"===t.method))return h;t.method="throw",t.arg=new TypeError("The iterator does not provide a 'throw' method")}return h}var i=w(n,e.iterator,t.arg);if("throw"===i.type)return t.method="throw",t.arg=i.arg,t.delegate=null,h;var o=i.arg;return o?o.done?(t[e.resultName]=o.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=r),t.delegate=null,h):o:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,h)}function C(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function $(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function A(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(C,this),this.reset(!0)}function O(e){if(e){var t=e[a];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var n=-1,o=function t(){for(;++n<e.length;)if(i.call(e,n))return t.value=e[n],t.done=!1,t;return t.value=r,t.done=!0,t};return o.next=o}}return{next:P}}function P(){return{value:r,done:!0}}}("object"==typeof r.g?r.g:"object"==typeof window?window:"object"==typeof self?self:this)},2511:function(e,t,r){var n;/*! https://mths.be/punycode v1.3.2 by @mathias */e=r.nmd(e),function(i){t&&t.nodeType,e&&e.nodeType;var o="object"==typeof r.g&&r.g;o.global!==o&&o.window!==o&&o.self;var a,s=2147483647,p=36,d=/^xn--/,c=/[^\x20-\x7E]/,l=/[\x2E\u3002\uFF0E\uFF61]/g,u={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},m=Math.floor,h=String.fromCharCode;function f(e){throw RangeError(u[e])}function y(e,t){for(var r=e.length,n=[];r--;)n[r]=t(e[r]);return n}function g(e,t){var r=e.split("@"),n="";return r.length>1&&(n=r[0]+"@",e=r[1]),n+y((e=e.replace(l,".")).split("."),t).join(".")}function b(e){for(var t,r,n=[],i=0,o=e.length;i<o;)(t=e.charCodeAt(i++))>=55296&&t<=56319&&i<o?56320==(64512&(r=e.charCodeAt(i++)))?n.push(((1023&t)<<10)+(1023&r)+65536):(n.push(t),i--):n.push(t);return n}function v(e){return y(e,(function(e){var t="";return e>65535&&(t+=h((e-=65536)>>>10&1023|55296),e=56320|1023&e),t+=h(e)})).join("")}function w(e,t){return e+22+75*(e<26)-((0!=t)<<5)}function S(e,t,r){var n=0;for(e=r?m(e/700):e>>1,e+=m(e/t);e>455;n+=p)e=m(e/35);return m(n+36*e/(e+38))}function I(e){var t,r,n,i,o,a,d,c,l,u,h,y=[],g=e.length,b=0,w=128,I=72;for((r=e.lastIndexOf("-"))<0&&(r=0),n=0;n<r;++n)e.charCodeAt(n)>=128&&f("not-basic"),y.push(e.charCodeAt(n));for(i=r>0?r+1:0;i<g;){for(o=b,a=1,d=p;i>=g&&f("invalid-input"),((c=(h=e.charCodeAt(i++))-48<10?h-22:h-65<26?h-65:h-97<26?h-97:p)>=p||c>m((s-b)/a))&&f("overflow"),b+=c*a,!(c<(l=d<=I?1:d>=I+26?26:d-I));d+=p)a>m(s/(u=p-l))&&f("overflow"),a*=u;I=S(b-o,t=y.length+1,0==o),m(b/t)>s-w&&f("overflow"),w+=m(b/t),b%=t,y.splice(b++,0,w)}return v(y)}function x(e){var t,r,n,i,o,a,d,c,l,u,y,g,v,I,x,k=[];for(g=(e=b(e)).length,t=128,r=0,o=72,a=0;a<g;++a)(y=e[a])<128&&k.push(h(y));for(n=i=k.length,i&&k.push("-");n<g;){for(d=s,a=0;a<g;++a)(y=e[a])>=t&&y<d&&(d=y);for(d-t>m((s-r)/(v=n+1))&&f("overflow"),r+=(d-t)*v,t=d,a=0;a<g;++a)if((y=e[a])<t&&++r>s&&f("overflow"),y==t){for(c=r,l=p;!(c<(u=l<=o?1:l>=o+26?26:l-o));l+=p)x=c-u,I=p-u,k.push(h(w(u+x%I,0))),c=m(x/I);k.push(h(w(c,0))),o=S(r,v,n==i),r=0,++n}++r,++t}return k.join("")}a={version:"1.3.2",ucs2:{decode:b,encode:v},decode:I,encode:x,toASCII:function(e){return g(e,(function(e){return c.test(e)?"xn--"+x(e):e}))},toUnicode:function(e){return g(e,(function(e){return d.test(e)?I(e.slice(4).toLowerCase()):e}))}},void 0===(n=function(){return a}.call(t,r,t,e))||(e.exports=n)}()},8575:(e,t,r)=>{"use strict";var n=r(2511),i=r(2502);function o(){this.protocol=null,this.slashes=null,this.auth=null,this.host=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.query=null,this.pathname=null,this.path=null,this.href=null}t.Qc=v,t.WU=function(e){i.isString(e)&&(e=v(e));return e instanceof o?e.format():o.prototype.format.call(e)};var a=/^([a-z0-9.+-]+:)/i,s=/:[0-9]*$/,p=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,d=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),c=["'"].concat(d),l=["%","/","?",";","#"].concat(c),u=["/","?","#"],m=/^[+a-z0-9A-Z_-]{0,63}$/,h=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,f={javascript:!0,"javascript:":!0},y={javascript:!0,"javascript:":!0},g={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},b=r(7673);function v(e,t,r){if(e&&i.isObject(e)&&e instanceof o)return e;var n=new o;return n.parse(e,t,r),n}o.prototype.parse=function(e,t,r){if(!i.isString(e))throw new TypeError("Parameter 'url' must be a string, not "+typeof e);var o=e.indexOf("?"),s=-1!==o&&o<e.indexOf("#")?"?":"#",d=e.split(s);d[0]=d[0].replace(/\\/g,"/");var v=e=d.join(s);if(v=v.trim(),!r&&1===e.split("#").length){var w=p.exec(v);if(w)return this.path=v,this.href=v,this.pathname=w[1],w[2]?(this.search=w[2],this.query=t?b.parse(this.search.substr(1)):this.search.substr(1)):t&&(this.search="",this.query={}),this}var S=a.exec(v);if(S){var I=(S=S[0]).toLowerCase();this.protocol=I,v=v.substr(S.length)}if(r||S||v.match(/^\/\/[^@\/]+@[^@\/]+/)){var x="//"===v.substr(0,2);!x||S&&y[S]||(v=v.substr(2),this.slashes=!0)}if(!y[S]&&(x||S&&!g[S])){for(var k,T,R=-1,C=0;C<u.length;C++){-1!==($=v.indexOf(u[C]))&&(-1===R||$<R)&&(R=$)}-1!==(T=-1===R?v.lastIndexOf("@"):v.lastIndexOf("@",R))&&(k=v.slice(0,T),v=v.slice(T+1),this.auth=decodeURIComponent(k)),R=-1;for(C=0;C<l.length;C++){var $;-1!==($=v.indexOf(l[C]))&&(-1===R||$<R)&&(R=$)}-1===R&&(R=v.length),this.host=v.slice(0,R),v=v.slice(R),this.parseHost(),this.hostname=this.hostname||"";var A="["===this.hostname[0]&&"]"===this.hostname[this.hostname.length-1];if(!A)for(var O=this.hostname.split(/\./),P=(C=0,O.length);C<P;C++){var D=O[C];if(D&&!D.match(m)){for(var N="",j=0,E=D.length;j<E;j++)D.charCodeAt(j)>127?N+="x":N+=D[j];if(!N.match(m)){var M=O.slice(0,C),F=O.slice(C+1),q=D.match(h);q&&(M.push(q[1]),F.unshift(q[2])),F.length&&(v="/"+F.join(".")+v),this.hostname=M.join(".");break}}}this.hostname.length>255?this.hostname="":this.hostname=this.hostname.toLowerCase(),A||(this.hostname=n.toASCII(this.hostname));var L=this.port?":"+this.port:"",B=this.hostname||"";this.host=B+L,this.href+=this.host,A&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==v[0]&&(v="/"+v))}if(!f[I])for(C=0,P=c.length;C<P;C++){var U=c[C];if(-1!==v.indexOf(U)){var W=encodeURIComponent(U);W===U&&(W=escape(U)),v=v.split(U).join(W)}}var H=v.indexOf("#");-1!==H&&(this.hash=v.substr(H),v=v.slice(0,H));var _=v.indexOf("?");if(-1!==_?(this.search=v.substr(_),this.query=v.substr(_+1),t&&(this.query=b.parse(this.query)),v=v.slice(0,_)):t&&(this.search="",this.query={}),v&&(this.pathname=v),g[I]&&this.hostname&&!this.pathname&&(this.pathname="/"),this.pathname||this.search){L=this.pathname||"";var V=this.search||"";this.path=L+V}return this.href=this.format(),this},o.prototype.format=function(){var e=this.auth||"";e&&(e=(e=encodeURIComponent(e)).replace(/%3A/i,":"),e+="@");var t=this.protocol||"",r=this.pathname||"",n=this.hash||"",o=!1,a="";this.host?o=e+this.host:this.hostname&&(o=e+(-1===this.hostname.indexOf(":")?this.hostname:"["+this.hostname+"]"),this.port&&(o+=":"+this.port)),this.query&&i.isObject(this.query)&&Object.keys(this.query).length&&(a=b.stringify(this.query));var s=this.search||a&&"?"+a||"";return t&&":"!==t.substr(-1)&&(t+=":"),this.slashes||(!t||g[t])&&!1!==o?(o="//"+(o||""),r&&"/"!==r.charAt(0)&&(r="/"+r)):o||(o=""),n&&"#"!==n.charAt(0)&&(n="#"+n),s&&"?"!==s.charAt(0)&&(s="?"+s),t+o+(r=r.replace(/[?#]/g,(function(e){return encodeURIComponent(e)})))+(s=s.replace("#","%23"))+n},o.prototype.resolve=function(e){return this.resolveObject(v(e,!1,!0)).format()},o.prototype.resolveObject=function(e){if(i.isString(e)){var t=new o;t.parse(e,!1,!0),e=t}for(var r=new o,n=Object.keys(this),a=0;a<n.length;a++){var s=n[a];r[s]=this[s]}if(r.hash=e.hash,""===e.href)return r.href=r.format(),r;if(e.slashes&&!e.protocol){for(var p=Object.keys(e),d=0;d<p.length;d++){var c=p[d];"protocol"!==c&&(r[c]=e[c])}return g[r.protocol]&&r.hostname&&!r.pathname&&(r.path=r.pathname="/"),r.href=r.format(),r}if(e.protocol&&e.protocol!==r.protocol){if(!g[e.protocol]){for(var l=Object.keys(e),u=0;u<l.length;u++){var m=l[u];r[m]=e[m]}return r.href=r.format(),r}if(r.protocol=e.protocol,e.host||y[e.protocol])r.pathname=e.pathname;else{for(var h=(e.pathname||"").split("/");h.length&&!(e.host=h.shift()););e.host||(e.host=""),e.hostname||(e.hostname=""),""!==h[0]&&h.unshift(""),h.length<2&&h.unshift(""),r.pathname=h.join("/")}if(r.search=e.search,r.query=e.query,r.host=e.host||"",r.auth=e.auth,r.hostname=e.hostname||e.host,r.port=e.port,r.pathname||r.search){var f=r.pathname||"",b=r.search||"";r.path=f+b}return r.slashes=r.slashes||e.slashes,r.href=r.format(),r}var v=r.pathname&&"/"===r.pathname.charAt(0),w=e.host||e.pathname&&"/"===e.pathname.charAt(0),S=w||v||r.host&&e.pathname,I=S,x=r.pathname&&r.pathname.split("/")||[],k=(h=e.pathname&&e.pathname.split("/")||[],r.protocol&&!g[r.protocol]);if(k&&(r.hostname="",r.port=null,r.host&&(""===x[0]?x[0]=r.host:x.unshift(r.host)),r.host="",e.protocol&&(e.hostname=null,e.port=null,e.host&&(""===h[0]?h[0]=e.host:h.unshift(e.host)),e.host=null),S=S&&(""===h[0]||""===x[0])),w)r.host=e.host||""===e.host?e.host:r.host,r.hostname=e.hostname||""===e.hostname?e.hostname:r.hostname,r.search=e.search,r.query=e.query,x=h;else if(h.length)x||(x=[]),x.pop(),x=x.concat(h),r.search=e.search,r.query=e.query;else if(!i.isNullOrUndefined(e.search)){if(k)r.hostname=r.host=x.shift(),(A=!!(r.host&&r.host.indexOf("@")>0)&&r.host.split("@"))&&(r.auth=A.shift(),r.host=r.hostname=A.shift());return r.search=e.search,r.query=e.query,i.isNull(r.pathname)&&i.isNull(r.search)||(r.path=(r.pathname?r.pathname:"")+(r.search?r.search:"")),r.href=r.format(),r}if(!x.length)return r.pathname=null,r.search?r.path="/"+r.search:r.path=null,r.href=r.format(),r;for(var T=x.slice(-1)[0],R=(r.host||e.host||x.length>1)&&("."===T||".."===T)||""===T,C=0,$=x.length;$>=0;$--)"."===(T=x[$])?x.splice($,1):".."===T?(x.splice($,1),C++):C&&(x.splice($,1),C--);if(!S&&!I)for(;C--;C)x.unshift("..");!S||""===x[0]||x[0]&&"/"===x[0].charAt(0)||x.unshift(""),R&&"/"!==x.join("/").substr(-1)&&x.push("");var A,O=""===x[0]||x[0]&&"/"===x[0].charAt(0);k&&(r.hostname=r.host=O?"":x.length?x.shift():"",(A=!!(r.host&&r.host.indexOf("@")>0)&&r.host.split("@"))&&(r.auth=A.shift(),r.host=r.hostname=A.shift()));return(S=S||r.host&&x.length)&&!O&&x.unshift(""),x.length?r.pathname=x.join("/"):(r.pathname=null,r.path=null),i.isNull(r.pathname)&&i.isNull(r.search)||(r.path=(r.pathname?r.pathname:"")+(r.search?r.search:"")),r.auth=e.auth||r.auth,r.slashes=r.slashes||e.slashes,r.href=r.format(),r},o.prototype.parseHost=function(){var e=this.host,t=s.exec(e);t&&(":"!==(t=t[0])&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)}},2502:e=>{"use strict";e.exports={isString:function(e){return"string"==typeof e},isObject:function(e){return"object"==typeof e&&null!==e},isNull:function(e){return null===e},isNullOrUndefined:function(e){return null==e}}},384:e=>{e.exports=function(e){return e&&"object"==typeof e&&"function"==typeof e.copy&&"function"==typeof e.fill&&"function"==typeof e.readUInt8}},5955:(e,t,r)=>{"use strict";var n=r(2584),i=r(8662),o=r(6430),a=r(5692);function s(e){return e.call.bind(e)}var p="undefined"!=typeof BigInt,d="undefined"!=typeof Symbol,c=s(Object.prototype.toString),l=s(Number.prototype.valueOf),u=s(String.prototype.valueOf),m=s(Boolean.prototype.valueOf);if(p)var h=s(BigInt.prototype.valueOf);if(d)var f=s(Symbol.prototype.valueOf);function y(e,t){if("object"!=typeof e)return!1;try{return t(e),!0}catch(e){return!1}}function g(e){return"[object Map]"===c(e)}function b(e){return"[object Set]"===c(e)}function v(e){return"[object WeakMap]"===c(e)}function w(e){return"[object WeakSet]"===c(e)}function S(e){return"[object ArrayBuffer]"===c(e)}function I(e){return"undefined"!=typeof ArrayBuffer&&(S.working?S(e):e instanceof ArrayBuffer)}function x(e){return"[object DataView]"===c(e)}function k(e){return"undefined"!=typeof DataView&&(x.working?x(e):e instanceof DataView)}t.isArgumentsObject=n,t.isGeneratorFunction=i,t.isTypedArray=a,t.isPromise=function(e){return"undefined"!=typeof Promise&&e instanceof Promise||null!==e&&"object"==typeof e&&"function"==typeof e.then&&"function"==typeof e.catch},t.isArrayBufferView=function(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):a(e)||k(e)},t.isUint8Array=function(e){return"Uint8Array"===o(e)},t.isUint8ClampedArray=function(e){return"Uint8ClampedArray"===o(e)},t.isUint16Array=function(e){return"Uint16Array"===o(e)},t.isUint32Array=function(e){return"Uint32Array"===o(e)},t.isInt8Array=function(e){return"Int8Array"===o(e)},t.isInt16Array=function(e){return"Int16Array"===o(e)},t.isInt32Array=function(e){return"Int32Array"===o(e)},t.isFloat32Array=function(e){return"Float32Array"===o(e)},t.isFloat64Array=function(e){return"Float64Array"===o(e)},t.isBigInt64Array=function(e){return"BigInt64Array"===o(e)},t.isBigUint64Array=function(e){return"BigUint64Array"===o(e)},g.working="undefined"!=typeof Map&&g(new Map),t.isMap=function(e){return"undefined"!=typeof Map&&(g.working?g(e):e instanceof Map)},b.working="undefined"!=typeof Set&&b(new Set),t.isSet=function(e){return"undefined"!=typeof Set&&(b.working?b(e):e instanceof Set)},v.working="undefined"!=typeof WeakMap&&v(new WeakMap),t.isWeakMap=function(e){return"undefined"!=typeof WeakMap&&(v.working?v(e):e instanceof WeakMap)},w.working="undefined"!=typeof WeakSet&&w(new WeakSet),t.isWeakSet=function(e){return w(e)},S.working="undefined"!=typeof ArrayBuffer&&S(new ArrayBuffer),t.isArrayBuffer=I,x.working="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof DataView&&x(new DataView(new ArrayBuffer(1),0,1)),t.isDataView=k;var T="undefined"!=typeof SharedArrayBuffer?SharedArrayBuffer:void 0;function R(e){return"[object SharedArrayBuffer]"===c(e)}function C(e){return void 0!==T&&(void 0===R.working&&(R.working=R(new T)),R.working?R(e):e instanceof T)}function $(e){return y(e,l)}function A(e){return y(e,u)}function O(e){return y(e,m)}function P(e){return p&&y(e,h)}function D(e){return d&&y(e,f)}t.isSharedArrayBuffer=C,t.isAsyncFunction=function(e){return"[object AsyncFunction]"===c(e)},t.isMapIterator=function(e){return"[object Map Iterator]"===c(e)},t.isSetIterator=function(e){return"[object Set Iterator]"===c(e)},t.isGeneratorObject=function(e){return"[object Generator]"===c(e)},t.isWebAssemblyCompiledModule=function(e){return"[object WebAssembly.Module]"===c(e)},t.isNumberObject=$,t.isStringObject=A,t.isBooleanObject=O,t.isBigIntObject=P,t.isSymbolObject=D,t.isBoxedPrimitive=function(e){return $(e)||A(e)||O(e)||P(e)||D(e)},t.isAnyArrayBuffer=function(e){return"undefined"!=typeof Uint8Array&&(I(e)||C(e))},["isProxy","isExternal","isModuleNamespaceObject"].forEach((function(e){Object.defineProperty(t,e,{enumerable:!1,value:function(){throw new Error(e+" is not supported in userland")}})}))},1588:(e,t,r)=>{var n=r(4155),i=Object.getOwnPropertyDescriptors||function(e){for(var t=Object.keys(e),r={},n=0;n<t.length;n++)r[t[n]]=Object.getOwnPropertyDescriptor(e,t[n]);return r},o=/%[sdj%]/g;t.format=function(e){if(!v(e)){for(var t=[],r=0;r<arguments.length;r++)t.push(d(arguments[r]));return t.join(" ")}r=1;for(var n=arguments,i=n.length,a=String(e).replace(o,(function(e){if("%%"===e)return"%";if(r>=i)return e;switch(e){case"%s":return String(n[r++]);case"%d":return Number(n[r++]);case"%j":try{return JSON.stringify(n[r++])}catch(e){return"[Circular]"}default:return e}})),s=n[r];r<i;s=n[++r])g(s)||!I(s)?a+=" "+s:a+=" "+d(s);return a},t.deprecate=function(e,r){if(void 0!==n&&!0===n.noDeprecation)return e;if(void 0===n)return function(){return t.deprecate(e,r).apply(this,arguments)};var i=!1;return function(){if(!i){if(n.throwDeprecation)throw new Error(r);n.traceDeprecation?console.trace(r):console.error(r),i=!0}return e.apply(this,arguments)}};var a={},s=/^$/;if(n.env.NODE_DEBUG){var p=n.env.NODE_DEBUG;p=p.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".*").replace(/,/g,"$|^").toUpperCase(),s=new RegExp("^"+p+"$","i")}function d(e,r){var n={seen:[],stylize:l};return arguments.length>=3&&(n.depth=arguments[2]),arguments.length>=4&&(n.colors=arguments[3]),y(r)?n.showHidden=r:r&&t._extend(n,r),w(n.showHidden)&&(n.showHidden=!1),w(n.depth)&&(n.depth=2),w(n.colors)&&(n.colors=!1),w(n.customInspect)&&(n.customInspect=!0),n.colors&&(n.stylize=c),u(n,e,n.depth)}function c(e,t){var r=d.styles[t];return r?"["+d.colors[r][0]+"m"+e+"["+d.colors[r][1]+"m":e}function l(e,t){return e}function u(e,r,n){if(e.customInspect&&r&&T(r.inspect)&&r.inspect!==t.inspect&&(!r.constructor||r.constructor.prototype!==r)){var i=r.inspect(n,e);return v(i)||(i=u(e,i,n)),i}var o=function(e,t){if(w(t))return e.stylize("undefined","undefined");if(v(t)){var r="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(r,"string")}if(b(t))return e.stylize(""+t,"number");if(y(t))return e.stylize(""+t,"boolean");if(g(t))return e.stylize("null","null")}(e,r);if(o)return o;var a=Object.keys(r),s=function(e){var t={};return e.forEach((function(e,r){t[e]=!0})),t}(a);if(e.showHidden&&(a=Object.getOwnPropertyNames(r)),k(r)&&(a.indexOf("message")>=0||a.indexOf("description")>=0))return m(r);if(0===a.length){if(T(r)){var p=r.name?": "+r.name:"";return e.stylize("[Function"+p+"]","special")}if(S(r))return e.stylize(RegExp.prototype.toString.call(r),"regexp");if(x(r))return e.stylize(Date.prototype.toString.call(r),"date");if(k(r))return m(r)}var d,c="",l=!1,I=["{","}"];(f(r)&&(l=!0,I=["[","]"]),T(r))&&(c=" [Function"+(r.name?": "+r.name:"")+"]");return S(r)&&(c=" "+RegExp.prototype.toString.call(r)),x(r)&&(c=" "+Date.prototype.toUTCString.call(r)),k(r)&&(c=" "+m(r)),0!==a.length||l&&0!=r.length?n<0?S(r)?e.stylize(RegExp.prototype.toString.call(r),"regexp"):e.stylize("[Object]","special"):(e.seen.push(r),d=l?function(e,t,r,n,i){for(var o=[],a=0,s=t.length;a<s;++a)O(t,String(a))?o.push(h(e,t,r,n,String(a),!0)):o.push("");return i.forEach((function(i){i.match(/^\d+$/)||o.push(h(e,t,r,n,i,!0))})),o}(e,r,n,s,a):a.map((function(t){return h(e,r,n,s,t,l)})),e.seen.pop(),function(e,t,r){if(e.reduce((function(e,t){return t.indexOf("\n")>=0&&0,e+t.replace(/\u001b\[\d\d?m/g,"").length+1}),0)>60)return r[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+r[1];return r[0]+t+" "+e.join(", ")+" "+r[1]}(d,c,I)):I[0]+c+I[1]}function m(e){return"["+Error.prototype.toString.call(e)+"]"}function h(e,t,r,n,i,o){var a,s,p;if((p=Object.getOwnPropertyDescriptor(t,i)||{value:t[i]}).get?s=p.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):p.set&&(s=e.stylize("[Setter]","special")),O(n,i)||(a="["+i+"]"),s||(e.seen.indexOf(p.value)<0?(s=g(r)?u(e,p.value,null):u(e,p.value,r-1)).indexOf("\n")>-1&&(s=o?s.split("\n").map((function(e){return" "+e})).join("\n").substr(2):"\n"+s.split("\n").map((function(e){return" "+e})).join("\n")):s=e.stylize("[Circular]","special")),w(a)){if(o&&i.match(/^\d+$/))return s;(a=JSON.stringify(""+i)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+s}function f(e){return Array.isArray(e)}function y(e){return"boolean"==typeof e}function g(e){return null===e}function b(e){return"number"==typeof e}function v(e){return"string"==typeof e}function w(e){return void 0===e}function S(e){return I(e)&&"[object RegExp]"===R(e)}function I(e){return"object"==typeof e&&null!==e}function x(e){return I(e)&&"[object Date]"===R(e)}function k(e){return I(e)&&("[object Error]"===R(e)||e instanceof Error)}function T(e){return"function"==typeof e}function R(e){return Object.prototype.toString.call(e)}function C(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(e){if(e=e.toUpperCase(),!a[e])if(s.test(e)){var r=n.pid;a[e]=function(){var n=t.format.apply(t,arguments);console.error("%s %d: %s",e,r,n)}}else a[e]=function(){};return a[e]},t.inspect=d,d.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},d.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.types=r(5955),t.isArray=f,t.isBoolean=y,t.isNull=g,t.isNullOrUndefined=function(e){return null==e},t.isNumber=b,t.isString=v,t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=w,t.isRegExp=S,t.types.isRegExp=S,t.isObject=I,t.isDate=x,t.types.isDate=x,t.isError=k,t.types.isNativeError=k,t.isFunction=T,t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=r(384);var $=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function A(){var e=new Date,t=[C(e.getHours()),C(e.getMinutes()),C(e.getSeconds())].join(":");return[e.getDate(),$[e.getMonth()],t].join(" ")}function O(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){console.log("%s - %s",A(),t.format.apply(t,arguments))},t.inherits=r(5717),t._extend=function(e,t){if(!t||!I(t))return e;for(var r=Object.keys(t),n=r.length;n--;)e[r[n]]=t[r[n]];return e};var P="undefined"!=typeof Symbol?Symbol("util.promisify.custom"):void 0;function D(e,t){if(!e){var r=new Error("Promise was rejected with a falsy value");r.reason=e,e=r}return t(e)}t.promisify=function(e){if("function"!=typeof e)throw new TypeError('The "original" argument must be of type Function');if(P&&e[P]){var t;if("function"!=typeof(t=e[P]))throw new TypeError('The "util.promisify.custom" argument must be of type Function');return Object.defineProperty(t,P,{value:t,enumerable:!1,writable:!1,configurable:!0}),t}function t(){for(var t,r,n=new Promise((function(e,n){t=e,r=n})),i=[],o=0;o<arguments.length;o++)i.push(arguments[o]);i.push((function(e,n){e?r(e):t(n)}));try{e.apply(this,i)}catch(e){r(e)}return n}return Object.setPrototypeOf(t,Object.getPrototypeOf(e)),P&&Object.defineProperty(t,P,{value:t,enumerable:!1,writable:!1,configurable:!0}),Object.defineProperties(t,i(e))},t.promisify.custom=P,t.callbackify=function(e){if("function"!=typeof e)throw new TypeError('The "original" argument must be of type Function');function t(){for(var t=[],r=0;r<arguments.length;r++)t.push(arguments[r]);var i=t.pop();if("function"!=typeof i)throw new TypeError("The last argument must be of type Function");var o=this,a=function(){return i.apply(o,arguments)};e.apply(this,t).then((function(e){n.nextTick(a.bind(null,null,e))}),(function(e){n.nextTick(D.bind(null,e,a))}))}return Object.setPrototypeOf(t,Object.getPrototypeOf(e)),Object.defineProperties(t,i(e)),t}},6430:(e,t,r)=>{"use strict";var n=r(4029),i=r(3083),o=r(1924),a=o("Object.prototype.toString"),s=r(6410)(),p="undefined"==typeof globalThis?r.g:globalThis,d=i(),c=o("String.prototype.slice"),l={},u=r(882),m=Object.getPrototypeOf;s&&u&&m&&n(d,(function(e){if("function"==typeof p[e]){var t=new p[e];if(Symbol.toStringTag in t){var r=m(t),n=u(r,Symbol.toStringTag);if(!n){var i=m(r);n=u(i,Symbol.toStringTag)}l[e]=n.get}}}));var h=r(5692);e.exports=function(e){return!!h(e)&&(s&&Symbol.toStringTag in e?function(e){var t=!1;return n(l,(function(r,n){if(!t)try{var i=r.call(e);i===n&&(t=i)}catch(e){}})),t}(e):c(a(e),8,-1))}},4162:e=>{"use strict";e.exports=function(e,t,r){window.criRequest(t,r)}},3423:()=>{},8532:()=>{},4782:()=>{},3083:(e,t,r)=>{"use strict";var n=["BigInt64Array","BigUint64Array","Float32Array","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Uint8Array","Uint8ClampedArray"],i="undefined"==typeof globalThis?r.g:globalThis;e.exports=function(){for(var e=[],t=0;t<n.length;t++)"function"==typeof i[n[t]]&&(e[e.length]=n[t]);return e}},882:(e,t,r)=>{"use strict";var n=r(210)("%Object.getOwnPropertyDescriptor%",!0);if(n)try{n([],"length")}catch(e){n=null}e.exports=n},4203:e=>{"use strict";e.exports=JSON.parse('{"version":{"major":"1","minor":"3"},"domains":[{"domain":"Accessibility","experimental":true,"dependencies":["DOM"],"types":[{"id":"AXNodeId","description":"Unique accessibility node identifier.","type":"string"},{"id":"AXValueType","description":"Enum of possible property types.","type":"string","enum":["boolean","tristate","booleanOrUndefined","idref","idrefList","integer","node","nodeList","number","string","computedString","token","tokenList","domRelation","role","internalRole","valueUndefined"]},{"id":"AXValueSourceType","description":"Enum of possible property sources.","type":"string","enum":["attribute","implicit","style","contents","placeholder","relatedElement"]},{"id":"AXValueNativeSourceType","description":"Enum of possible native property sources (as a subtype of a particular AXValueSourceType).","type":"string","enum":["description","figcaption","label","labelfor","labelwrapped","legend","rubyannotation","tablecaption","title","other"]},{"id":"AXValueSource","description":"A single source for a computed AX property.","type":"object","properties":[{"name":"type","description":"What type of source this is.","$ref":"AXValueSourceType"},{"name":"value","description":"The value of this property source.","optional":true,"$ref":"AXValue"},{"name":"attribute","description":"The name of the relevant attribute, if any.","optional":true,"type":"string"},{"name":"attributeValue","description":"The value of the relevant attribute, if any.","optional":true,"$ref":"AXValue"},{"name":"superseded","description":"Whether this source is superseded by a higher priority source.","optional":true,"type":"boolean"},{"name":"nativeSource","description":"The native markup source for this value, e.g. a `<label>` element.","optional":true,"$ref":"AXValueNativeSourceType"},{"name":"nativeSourceValue","description":"The value, such as a node or node list, of the native source.","optional":true,"$ref":"AXValue"},{"name":"invalid","description":"Whether the value for this property is invalid.","optional":true,"type":"boolean"},{"name":"invalidReason","description":"Reason for the value being invalid, if it is.","optional":true,"type":"string"}]},{"id":"AXRelatedNode","type":"object","properties":[{"name":"backendDOMNodeId","description":"The BackendNodeId of the related DOM node.","$ref":"DOM.BackendNodeId"},{"name":"idref","description":"The IDRef value provided, if any.","optional":true,"type":"string"},{"name":"text","description":"The text alternative of this node in the current context.","optional":true,"type":"string"}]},{"id":"AXProperty","type":"object","properties":[{"name":"name","description":"The name of this property.","$ref":"AXPropertyName"},{"name":"value","description":"The value of this property.","$ref":"AXValue"}]},{"id":"AXValue","description":"A single computed AX property.","type":"object","properties":[{"name":"type","description":"The type of this value.","$ref":"AXValueType"},{"name":"value","description":"The computed value of this property.","optional":true,"type":"any"},{"name":"relatedNodes","description":"One or more related nodes, if applicable.","optional":true,"type":"array","items":{"$ref":"AXRelatedNode"}},{"name":"sources","description":"The sources which contributed to the computation of this property.","optional":true,"type":"array","items":{"$ref":"AXValueSource"}}]},{"id":"AXPropertyName","description":"Values of AXProperty name:\\n- from \'busy\' to \'roledescription\': states which apply to every AX node\\n- from \'live\' to \'root\': attributes which apply to nodes in live regions\\n- from \'autocomplete\' to \'valuetext\': attributes which apply to widgets\\n- from \'checked\' to \'selected\': states which apply to widgets\\n- from \'activedescendant\' to \'owns\' - relationships between elements other than parent/child/sibling.","type":"string","enum":["busy","disabled","editable","focusable","focused","hidden","hiddenRoot","invalid","keyshortcuts","settable","roledescription","live","atomic","relevant","root","autocomplete","hasPopup","level","multiselectable","orientation","multiline","readonly","required","valuemin","valuemax","valuetext","checked","expanded","modal","pressed","selected","activedescendant","controls","describedby","details","errormessage","flowto","labelledby","owns"]},{"id":"AXNode","description":"A node in the accessibility tree.","type":"object","properties":[{"name":"nodeId","description":"Unique identifier for this node.","$ref":"AXNodeId"},{"name":"ignored","description":"Whether this node is ignored for accessibility","type":"boolean"},{"name":"ignoredReasons","description":"Collection of reasons why this node is hidden.","optional":true,"type":"array","items":{"$ref":"AXProperty"}},{"name":"role","description":"This `Node`\'s role, whether explicit or implicit.","optional":true,"$ref":"AXValue"},{"name":"chromeRole","description":"This `Node`\'s Chrome raw role.","optional":true,"$ref":"AXValue"},{"name":"name","description":"The accessible name for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"description","description":"The accessible description for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"value","description":"The value for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"properties","description":"All other properties","optional":true,"type":"array","items":{"$ref":"AXProperty"}},{"name":"parentId","description":"ID for this node\'s parent.","optional":true,"$ref":"AXNodeId"},{"name":"childIds","description":"IDs for each of this node\'s child nodes.","optional":true,"type":"array","items":{"$ref":"AXNodeId"}},{"name":"backendDOMNodeId","description":"The backend ID for the associated DOM node, if any.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"frameId","description":"The frame ID for the frame associated with this nodes document.","optional":true,"$ref":"Page.FrameId"}]}],"commands":[{"name":"disable","description":"Disables the accessibility domain."},{"name":"enable","description":"Enables the accessibility domain which causes `AXNodeId`s to remain consistent between method calls.\\nThis turns on accessibility for the page, which can impact performance until accessibility is disabled."},{"name":"getPartialAXTree","description":"Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node to get the partial accessibility tree for.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to get the partial accessibility tree for.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper to get the partial accessibility tree for.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"fetchRelatives","description":"Whether to fetch this node\'s ancestors, siblings and children. Defaults to true.","optional":true,"type":"boolean"}],"returns":[{"name":"nodes","description":"The `Accessibility.AXNode` for this DOM node, if it exists, plus its ancestors, siblings and\\nchildren, if requested.","type":"array","items":{"$ref":"AXNode"}}]},{"name":"getFullAXTree","description":"Fetches the entire accessibility tree for the root Document","experimental":true,"parameters":[{"name":"depth","description":"The maximum depth at which descendants of the root node should be retrieved.\\nIf omitted, the full tree is returned.","optional":true,"type":"integer"},{"name":"frameId","description":"The frame for whose document the AX tree should be retrieved.\\nIf omited, the root frame is used.","optional":true,"$ref":"Page.FrameId"}],"returns":[{"name":"nodes","type":"array","items":{"$ref":"AXNode"}}]},{"name":"getRootAXNode","description":"Fetches the root node.\\nRequires `enable()` to have been called previously.","experimental":true,"parameters":[{"name":"frameId","description":"The frame in whose document the node resides.\\nIf omitted, the root frame is used.","optional":true,"$ref":"Page.FrameId"}],"returns":[{"name":"node","$ref":"AXNode"}]},{"name":"getAXNodeAndAncestors","description":"Fetches a node and all ancestors up to and including the root.\\nRequires `enable()` to have been called previously.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node to get.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to get.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper to get.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"nodes","type":"array","items":{"$ref":"AXNode"}}]},{"name":"getChildAXNodes","description":"Fetches a particular accessibility node by AXNodeId.\\nRequires `enable()` to have been called previously.","experimental":true,"parameters":[{"name":"id","$ref":"AXNodeId"},{"name":"frameId","description":"The frame in whose document the node resides.\\nIf omitted, the root frame is used.","optional":true,"$ref":"Page.FrameId"}],"returns":[{"name":"nodes","type":"array","items":{"$ref":"AXNode"}}]},{"name":"queryAXTree","description":"Query a DOM node\'s accessibility subtree for accessible name and role.\\nThis command computes the name and role for all nodes in the subtree, including those that are\\nignored for accessibility, and returns those that mactch the specified name and role. If no DOM\\nnode is specified, or the DOM node does not exist, the command returns an error. If neither\\n`accessibleName` or `role` is specified, it returns all the accessibility nodes in the subtree.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node for the root to query.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node for the root to query.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper for the root to query.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"accessibleName","description":"Find nodes with this computed name.","optional":true,"type":"string"},{"name":"role","description":"Find nodes with this computed role.","optional":true,"type":"string"}],"returns":[{"name":"nodes","description":"A list of `Accessibility.AXNode` matching the specified attributes,\\nincluding nodes that are ignored for accessibility.","type":"array","items":{"$ref":"AXNode"}}]}],"events":[{"name":"loadComplete","description":"The loadComplete event mirrors the load complete event sent by the browser to assistive\\ntechnology when the web page has finished loading.","experimental":true,"parameters":[{"name":"root","description":"New document root node.","$ref":"AXNode"}]},{"name":"nodesUpdated","description":"The nodesUpdated event is sent every time a previously requested node has changed the in tree.","experimental":true,"parameters":[{"name":"nodes","description":"Updated node data.","type":"array","items":{"$ref":"AXNode"}}]}]},{"domain":"Animation","experimental":true,"dependencies":["Runtime","DOM"],"types":[{"id":"Animation","description":"Animation instance.","type":"object","properties":[{"name":"id","description":"`Animation`\'s id.","type":"string"},{"name":"name","description":"`Animation`\'s name.","type":"string"},{"name":"pausedState","description":"`Animation`\'s internal paused state.","type":"boolean"},{"name":"playState","description":"`Animation`\'s play state.","type":"string"},{"name":"playbackRate","description":"`Animation`\'s playback rate.","type":"number"},{"name":"startTime","description":"`Animation`\'s start time.","type":"number"},{"name":"currentTime","description":"`Animation`\'s current time.","type":"number"},{"name":"type","description":"Animation type of `Animation`.","type":"string","enum":["CSSTransition","CSSAnimation","WebAnimation"]},{"name":"source","description":"`Animation`\'s source animation node.","optional":true,"$ref":"AnimationEffect"},{"name":"cssId","description":"A unique ID for `Animation` representing the sources that triggered this CSS\\nanimation/transition.","optional":true,"type":"string"}]},{"id":"AnimationEffect","description":"AnimationEffect instance","type":"object","properties":[{"name":"delay","description":"`AnimationEffect`\'s delay.","type":"number"},{"name":"endDelay","description":"`AnimationEffect`\'s end delay.","type":"number"},{"name":"iterationStart","description":"`AnimationEffect`\'s iteration start.","type":"number"},{"name":"iterations","description":"`AnimationEffect`\'s iterations.","type":"number"},{"name":"duration","description":"`AnimationEffect`\'s iteration duration.","type":"number"},{"name":"direction","description":"`AnimationEffect`\'s playback direction.","type":"string"},{"name":"fill","description":"`AnimationEffect`\'s fill mode.","type":"string"},{"name":"backendNodeId","description":"`AnimationEffect`\'s target node.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"keyframesRule","description":"`AnimationEffect`\'s keyframes.","optional":true,"$ref":"KeyframesRule"},{"name":"easing","description":"`AnimationEffect`\'s timing function.","type":"string"}]},{"id":"KeyframesRule","description":"Keyframes Rule","type":"object","properties":[{"name":"name","description":"CSS keyframed animation\'s name.","optional":true,"type":"string"},{"name":"keyframes","description":"List of animation keyframes.","type":"array","items":{"$ref":"KeyframeStyle"}}]},{"id":"KeyframeStyle","description":"Keyframe Style","type":"object","properties":[{"name":"offset","description":"Keyframe\'s time offset.","type":"string"},{"name":"easing","description":"`AnimationEffect`\'s timing function.","type":"string"}]}],"commands":[{"name":"disable","description":"Disables animation domain notifications."},{"name":"enable","description":"Enables animation domain notifications."},{"name":"getCurrentTime","description":"Returns the current time of the an animation.","parameters":[{"name":"id","description":"Id of animation.","type":"string"}],"returns":[{"name":"currentTime","description":"Current time of the page.","type":"number"}]},{"name":"getPlaybackRate","description":"Gets the playback rate of the document timeline.","returns":[{"name":"playbackRate","description":"Playback rate for animations on page.","type":"number"}]},{"name":"releaseAnimations","description":"Releases a set of animations to no longer be manipulated.","parameters":[{"name":"animations","description":"List of animation ids to seek.","type":"array","items":{"type":"string"}}]},{"name":"resolveAnimation","description":"Gets the remote object of the Animation.","parameters":[{"name":"animationId","description":"Animation id.","type":"string"}],"returns":[{"name":"remoteObject","description":"Corresponding remote object.","$ref":"Runtime.RemoteObject"}]},{"name":"seekAnimations","description":"Seek a set of animations to a particular time within each animation.","parameters":[{"name":"animations","description":"List of animation ids to seek.","type":"array","items":{"type":"string"}},{"name":"currentTime","description":"Set the current time of each animation.","type":"number"}]},{"name":"setPaused","description":"Sets the paused state of a set of animations.","parameters":[{"name":"animations","description":"Animations to set the pause state of.","type":"array","items":{"type":"string"}},{"name":"paused","description":"Paused state to set to.","type":"boolean"}]},{"name":"setPlaybackRate","description":"Sets the playback rate of the document timeline.","parameters":[{"name":"playbackRate","description":"Playback rate for animations on page","type":"number"}]},{"name":"setTiming","description":"Sets the timing of an animation node.","parameters":[{"name":"animationId","description":"Animation id.","type":"string"},{"name":"duration","description":"Duration of the animation.","type":"number"},{"name":"delay","description":"Delay of the animation.","type":"number"}]}],"events":[{"name":"animationCanceled","description":"Event for when an animation has been cancelled.","parameters":[{"name":"id","description":"Id of the animation that was cancelled.","type":"string"}]},{"name":"animationCreated","description":"Event for each animation that has been created.","parameters":[{"name":"id","description":"Id of the animation that was created.","type":"string"}]},{"name":"animationStarted","description":"Event for animation that has been started.","parameters":[{"name":"animation","description":"Animation that was started.","$ref":"Animation"}]}]},{"domain":"Audits","description":"Audits domain allows investigation of page violations and possible improvements.","experimental":true,"dependencies":["Network"],"types":[{"id":"AffectedCookie","description":"Information about a cookie that is affected by an inspector issue.","type":"object","properties":[{"name":"name","description":"The following three properties uniquely identify a cookie","type":"string"},{"name":"path","type":"string"},{"name":"domain","type":"string"}]},{"id":"AffectedRequest","description":"Information about a request that is affected by an inspector issue.","type":"object","properties":[{"name":"requestId","description":"The unique request id.","$ref":"Network.RequestId"},{"name":"url","optional":true,"type":"string"}]},{"id":"AffectedFrame","description":"Information about the frame affected by an inspector issue.","type":"object","properties":[{"name":"frameId","$ref":"Page.FrameId"}]},{"id":"CookieExclusionReason","type":"string","enum":["ExcludeSameSiteUnspecifiedTreatedAsLax","ExcludeSameSiteNoneInsecure","ExcludeSameSiteLax","ExcludeSameSiteStrict","ExcludeInvalidSameParty","ExcludeSamePartyCrossPartyContext","ExcludeDomainNonASCII","ExcludeThirdPartyCookieBlockedInFirstPartySet"]},{"id":"CookieWarningReason","type":"string","enum":["WarnSameSiteUnspecifiedCrossSiteContext","WarnSameSiteNoneInsecure","WarnSameSiteUnspecifiedLaxAllowUnsafe","WarnSameSiteStrictLaxDowngradeStrict","WarnSameSiteStrictCrossDowngradeStrict","WarnSameSiteStrictCrossDowngradeLax","WarnSameSiteLaxCrossDowngradeStrict","WarnSameSiteLaxCrossDowngradeLax","WarnAttributeValueExceedsMaxSize","WarnDomainNonASCII","WarnThirdPartyPhaseout"]},{"id":"CookieOperation","type":"string","enum":["SetCookie","ReadCookie"]},{"id":"CookieIssueDetails","description":"This information is currently necessary, as the front-end has a difficult\\ntime finding a specific cookie. With this, we can convey specific error\\ninformation without the cookie.","type":"object","properties":[{"name":"cookie","description":"If AffectedCookie is not set then rawCookieLine contains the raw\\nSet-Cookie header string. This hints at a problem where the\\ncookie line is syntactically or semantically malformed in a way\\nthat no valid cookie could be created.","optional":true,"$ref":"AffectedCookie"},{"name":"rawCookieLine","optional":true,"type":"string"},{"name":"cookieWarningReasons","type":"array","items":{"$ref":"CookieWarningReason"}},{"name":"cookieExclusionReasons","type":"array","items":{"$ref":"CookieExclusionReason"}},{"name":"operation","description":"Optionally identifies the site-for-cookies and the cookie url, which\\nmay be used by the front-end as additional context.","$ref":"CookieOperation"},{"name":"siteForCookies","optional":true,"type":"string"},{"name":"cookieUrl","optional":true,"type":"string"},{"name":"request","optional":true,"$ref":"AffectedRequest"}]},{"id":"MixedContentResolutionStatus","type":"string","enum":["MixedContentBlocked","MixedContentAutomaticallyUpgraded","MixedContentWarning"]},{"id":"MixedContentResourceType","type":"string","enum":["AttributionSrc","Audio","Beacon","CSPReport","Download","EventSource","Favicon","Font","Form","Frame","Image","Import","Manifest","Ping","PluginData","PluginResource","Prefetch","Resource","Script","ServiceWorker","SharedWorker","Stylesheet","Track","Video","Worker","XMLHttpRequest","XSLT"]},{"id":"MixedContentIssueDetails","type":"object","properties":[{"name":"resourceType","description":"The type of resource causing the mixed content issue (css, js, iframe,\\nform,...). Marked as optional because it is mapped to from\\nblink::mojom::RequestContextType, which will be replaced\\nby network::mojom::RequestDestination","optional":true,"$ref":"MixedContentResourceType"},{"name":"resolutionStatus","description":"The way the mixed content issue is being resolved.","$ref":"MixedContentResolutionStatus"},{"name":"insecureURL","description":"The unsafe http url causing the mixed content issue.","type":"string"},{"name":"mainResourceURL","description":"The url responsible for the call to an unsafe url.","type":"string"},{"name":"request","description":"The mixed content request.\\nDoes not always exist (e.g. for unsafe form submission urls).","optional":true,"$ref":"AffectedRequest"},{"name":"frame","description":"Optional because not every mixed content issue is necessarily linked to a frame.","optional":true,"$ref":"AffectedFrame"}]},{"id":"BlockedByResponseReason","description":"Enum indicating the reason a response has been blocked. These reasons are\\nrefinements of the net error BLOCKED_BY_RESPONSE.","type":"string","enum":["CoepFrameResourceNeedsCoepHeader","CoopSandboxedIFrameCannotNavigateToCoopPage","CorpNotSameOrigin","CorpNotSameOriginAfterDefaultedToSameOriginByCoep","CorpNotSameSite"]},{"id":"BlockedByResponseIssueDetails","description":"Details for a request that has been blocked with the BLOCKED_BY_RESPONSE\\ncode. Currently only used for COEP/COOP, but may be extended to include\\nsome CSP errors in the future.","type":"object","properties":[{"name":"request","$ref":"AffectedRequest"},{"name":"parentFrame","optional":true,"$ref":"AffectedFrame"},{"name":"blockedFrame","optional":true,"$ref":"AffectedFrame"},{"name":"reason","$ref":"BlockedByResponseReason"}]},{"id":"HeavyAdResolutionStatus","type":"string","enum":["HeavyAdBlocked","HeavyAdWarning"]},{"id":"HeavyAdReason","type":"string","enum":["NetworkTotalLimit","CpuTotalLimit","CpuPeakLimit"]},{"id":"HeavyAdIssueDetails","type":"object","properties":[{"name":"resolution","description":"The resolution status, either blocking the content or warning.","$ref":"HeavyAdResolutionStatus"},{"name":"reason","description":"The reason the ad was blocked, total network or cpu or peak cpu.","$ref":"HeavyAdReason"},{"name":"frame","description":"The frame that was blocked.","$ref":"AffectedFrame"}]},{"id":"ContentSecurityPolicyViolationType","type":"string","enum":["kInlineViolation","kEvalViolation","kURLViolation","kTrustedTypesSinkViolation","kTrustedTypesPolicyViolation","kWasmEvalViolation"]},{"id":"SourceCodeLocation","type":"object","properties":[{"name":"scriptId","optional":true,"$ref":"Runtime.ScriptId"},{"name":"url","type":"string"},{"name":"lineNumber","type":"integer"},{"name":"columnNumber","type":"integer"}]},{"id":"ContentSecurityPolicyIssueDetails","type":"object","properties":[{"name":"blockedURL","description":"The url not included in allowed sources.","optional":true,"type":"string"},{"name":"violatedDirective","description":"Specific directive that is violated, causing the CSP issue.","type":"string"},{"name":"isReportOnly","type":"boolean"},{"name":"contentSecurityPolicyViolationType","$ref":"ContentSecurityPolicyViolationType"},{"name":"frameAncestor","optional":true,"$ref":"AffectedFrame"},{"name":"sourceCodeLocation","optional":true,"$ref":"SourceCodeLocation"},{"name":"violatingNodeId","optional":true,"$ref":"DOM.BackendNodeId"}]},{"id":"SharedArrayBufferIssueType","type":"string","enum":["TransferIssue","CreationIssue"]},{"id":"SharedArrayBufferIssueDetails","description":"Details for a issue arising from an SAB being instantiated in, or\\ntransferred to a context that is not cross-origin isolated.","type":"object","properties":[{"name":"sourceCodeLocation","$ref":"SourceCodeLocation"},{"name":"isWarning","type":"boolean"},{"name":"type","$ref":"SharedArrayBufferIssueType"}]},{"id":"LowTextContrastIssueDetails","type":"object","properties":[{"name":"violatingNodeId","$ref":"DOM.BackendNodeId"},{"name":"violatingNodeSelector","type":"string"},{"name":"contrastRatio","type":"number"},{"name":"thresholdAA","type":"number"},{"name":"thresholdAAA","type":"number"},{"name":"fontSize","type":"string"},{"name":"fontWeight","type":"string"}]},{"id":"CorsIssueDetails","description":"Details for a CORS related issue, e.g. a warning or error related to\\nCORS RFC1918 enforcement.","type":"object","properties":[{"name":"corsErrorStatus","$ref":"Network.CorsErrorStatus"},{"name":"isWarning","type":"boolean"},{"name":"request","$ref":"AffectedRequest"},{"name":"location","optional":true,"$ref":"SourceCodeLocation"},{"name":"initiatorOrigin","optional":true,"type":"string"},{"name":"resourceIPAddressSpace","optional":true,"$ref":"Network.IPAddressSpace"},{"name":"clientSecurityState","optional":true,"$ref":"Network.ClientSecurityState"}]},{"id":"AttributionReportingIssueType","type":"string","enum":["PermissionPolicyDisabled","UntrustworthyReportingOrigin","InsecureContext","InvalidHeader","InvalidRegisterTriggerHeader","SourceAndTriggerHeaders","SourceIgnored","TriggerIgnored","OsSourceIgnored","OsTriggerIgnored","InvalidRegisterOsSourceHeader","InvalidRegisterOsTriggerHeader","WebAndOsHeaders","NoWebOrOsSupport","NavigationRegistrationWithoutTransientUserActivation"]},{"id":"AttributionReportingIssueDetails","description":"Details for issues around \\"Attribution Reporting API\\" usage.\\nExplainer: https://github.com/WICG/attribution-reporting-api","type":"object","properties":[{"name":"violationType","$ref":"AttributionReportingIssueType"},{"name":"request","optional":true,"$ref":"AffectedRequest"},{"name":"violatingNodeId","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"invalidParameter","optional":true,"type":"string"}]},{"id":"QuirksModeIssueDetails","description":"Details for issues about documents in Quirks Mode\\nor Limited Quirks Mode that affects page layouting.","type":"object","properties":[{"name":"isLimitedQuirksMode","description":"If false, it means the document\'s mode is \\"quirks\\"\\ninstead of \\"limited-quirks\\".","type":"boolean"},{"name":"documentNodeId","$ref":"DOM.BackendNodeId"},{"name":"url","type":"string"},{"name":"frameId","$ref":"Page.FrameId"},{"name":"loaderId","$ref":"Network.LoaderId"}]},{"id":"NavigatorUserAgentIssueDetails","deprecated":true,"type":"object","properties":[{"name":"url","type":"string"},{"name":"location","optional":true,"$ref":"SourceCodeLocation"}]},{"id":"GenericIssueErrorType","type":"string","enum":["CrossOriginPortalPostMessageError","FormLabelForNameError","FormDuplicateIdForInputError","FormInputWithNoLabelError","FormAutocompleteAttributeEmptyError","FormEmptyIdAndNameAttributesForInputError","FormAriaLabelledByToNonExistingId","FormInputAssignedAutocompleteValueToIdOrNameAttributeError","FormLabelHasNeitherForNorNestedInput","FormLabelForMatchesNonExistingIdError","FormInputHasWrongButWellIntendedAutocompleteValueError","ResponseWasBlockedByORB"]},{"id":"GenericIssueDetails","description":"Depending on the concrete errorType, different properties are set.","type":"object","properties":[{"name":"errorType","description":"Issues with the same errorType are aggregated in the frontend.","$ref":"GenericIssueErrorType"},{"name":"frameId","optional":true,"$ref":"Page.FrameId"},{"name":"violatingNodeId","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"violatingNodeAttribute","optional":true,"type":"string"},{"name":"request","optional":true,"$ref":"AffectedRequest"}]},{"id":"DeprecationIssueDetails","description":"This issue tracks information needed to print a deprecation message.\\nhttps://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/frame/third_party/blink/renderer/core/frame/deprecation/README.md","type":"object","properties":[{"name":"affectedFrame","optional":true,"$ref":"AffectedFrame"},{"name":"sourceCodeLocation","$ref":"SourceCodeLocation"},{"name":"type","description":"One of the deprecation names from third_party/blink/renderer/core/frame/deprecation/deprecation.json5","type":"string"}]},{"id":"BounceTrackingIssueDetails","description":"This issue warns about sites in the redirect chain of a finished navigation\\nthat may be flagged as trackers and have their state cleared if they don\'t\\nreceive a user interaction. Note that in this context \'site\' means eTLD+1.\\nFor example, if the URL `https://example.test:80/bounce` was in the\\nredirect chain, the site reported would be `example.test`.","type":"object","properties":[{"name":"trackingSites","type":"array","items":{"type":"string"}}]},{"id":"ClientHintIssueReason","type":"string","enum":["MetaTagAllowListInvalidOrigin","MetaTagModifiedHTML"]},{"id":"FederatedAuthRequestIssueDetails","type":"object","properties":[{"name":"federatedAuthRequestIssueReason","$ref":"FederatedAuthRequestIssueReason"}]},{"id":"FederatedAuthRequestIssueReason","description":"Represents the failure reason when a federated authentication reason fails.\\nShould be updated alongside RequestIdTokenStatus in\\nthird_party/blink/public/mojom/devtools/inspector_issue.mojom to include\\nall cases except for success.","type":"string","enum":["ShouldEmbargo","TooManyRequests","WellKnownHttpNotFound","WellKnownNoResponse","WellKnownInvalidResponse","WellKnownListEmpty","WellKnownInvalidContentType","ConfigNotInWellKnown","WellKnownTooBig","ConfigHttpNotFound","ConfigNoResponse","ConfigInvalidResponse","ConfigInvalidContentType","ClientMetadataHttpNotFound","ClientMetadataNoResponse","ClientMetadataInvalidResponse","ClientMetadataInvalidContentType","DisabledInSettings","ErrorFetchingSignin","InvalidSigninResponse","AccountsHttpNotFound","AccountsNoResponse","AccountsInvalidResponse","AccountsListEmpty","AccountsInvalidContentType","IdTokenHttpNotFound","IdTokenNoResponse","IdTokenInvalidResponse","IdTokenInvalidRequest","IdTokenInvalidContentType","ErrorIdToken","Canceled","RpPageNotVisible","SilentMediationFailure","ThirdPartyCookiesBlocked"]},{"id":"FederatedAuthUserInfoRequestIssueDetails","type":"object","properties":[{"name":"federatedAuthUserInfoRequestIssueReason","$ref":"FederatedAuthUserInfoRequestIssueReason"}]},{"id":"FederatedAuthUserInfoRequestIssueReason","description":"Represents the failure reason when a getUserInfo() call fails.\\nShould be updated alongside FederatedAuthUserInfoRequestResult in\\nthird_party/blink/public/mojom/devtools/inspector_issue.mojom.","type":"string","enum":["NotSameOrigin","NotIframe","NotPotentiallyTrustworthy","NoApiPermission","NotSignedInWithIdp","NoAccountSharingPermission","InvalidConfigOrWellKnown","InvalidAccountsResponse","NoReturningUserFromFetchedAccounts"]},{"id":"ClientHintIssueDetails","description":"This issue tracks client hints related issues. It\'s used to deprecate old\\nfeatures, encourage the use of new ones, and provide general guidance.","type":"object","properties":[{"name":"sourceCodeLocation","$ref":"SourceCodeLocation"},{"name":"clientHintIssueReason","$ref":"ClientHintIssueReason"}]},{"id":"FailedRequestInfo","type":"object","properties":[{"name":"url","description":"The URL that failed to load.","type":"string"},{"name":"failureMessage","description":"The failure message for the failed request.","type":"string"},{"name":"requestId","optional":true,"$ref":"Network.RequestId"}]},{"id":"StyleSheetLoadingIssueReason","type":"string","enum":["LateImportRule","RequestFailed"]},{"id":"StylesheetLoadingIssueDetails","description":"This issue warns when a referenced stylesheet couldn\'t be loaded.","type":"object","properties":[{"name":"sourceCodeLocation","description":"Source code position that referenced the failing stylesheet.","$ref":"SourceCodeLocation"},{"name":"styleSheetLoadingIssueReason","description":"Reason why the stylesheet couldn\'t be loaded.","$ref":"StyleSheetLoadingIssueReason"},{"name":"failedRequestInfo","description":"Contains additional info when the failure was due to a request.","optional":true,"$ref":"FailedRequestInfo"}]},{"id":"InspectorIssueCode","description":"A unique identifier for the type of issue. Each type may use one of the\\noptional fields in InspectorIssueDetails to convey more specific\\ninformation about the kind of issue.","type":"string","enum":["CookieIssue","MixedContentIssue","BlockedByResponseIssue","HeavyAdIssue","ContentSecurityPolicyIssue","SharedArrayBufferIssue","LowTextContrastIssue","CorsIssue","AttributionReportingIssue","QuirksModeIssue","NavigatorUserAgentIssue","GenericIssue","DeprecationIssue","ClientHintIssue","FederatedAuthRequestIssue","BounceTrackingIssue","StylesheetLoadingIssue","FederatedAuthUserInfoRequestIssue"]},{"id":"InspectorIssueDetails","description":"This struct holds a list of optional fields with additional information\\nspecific to the kind of issue. When adding a new issue code, please also\\nadd a new optional field to this type.","type":"object","properties":[{"name":"cookieIssueDetails","optional":true,"$ref":"CookieIssueDetails"},{"name":"mixedContentIssueDetails","optional":true,"$ref":"MixedContentIssueDetails"},{"name":"blockedByResponseIssueDetails","optional":true,"$ref":"BlockedByResponseIssueDetails"},{"name":"heavyAdIssueDetails","optional":true,"$ref":"HeavyAdIssueDetails"},{"name":"contentSecurityPolicyIssueDetails","optional":true,"$ref":"ContentSecurityPolicyIssueDetails"},{"name":"sharedArrayBufferIssueDetails","optional":true,"$ref":"SharedArrayBufferIssueDetails"},{"name":"lowTextContrastIssueDetails","optional":true,"$ref":"LowTextContrastIssueDetails"},{"name":"corsIssueDetails","optional":true,"$ref":"CorsIssueDetails"},{"name":"attributionReportingIssueDetails","optional":true,"$ref":"AttributionReportingIssueDetails"},{"name":"quirksModeIssueDetails","optional":true,"$ref":"QuirksModeIssueDetails"},{"name":"navigatorUserAgentIssueDetails","deprecated":true,"optional":true,"$ref":"NavigatorUserAgentIssueDetails"},{"name":"genericIssueDetails","optional":true,"$ref":"GenericIssueDetails"},{"name":"deprecationIssueDetails","optional":true,"$ref":"DeprecationIssueDetails"},{"name":"clientHintIssueDetails","optional":true,"$ref":"ClientHintIssueDetails"},{"name":"federatedAuthRequestIssueDetails","optional":true,"$ref":"FederatedAuthRequestIssueDetails"},{"name":"bounceTrackingIssueDetails","optional":true,"$ref":"BounceTrackingIssueDetails"},{"name":"stylesheetLoadingIssueDetails","optional":true,"$ref":"StylesheetLoadingIssueDetails"},{"name":"federatedAuthUserInfoRequestIssueDetails","optional":true,"$ref":"FederatedAuthUserInfoRequestIssueDetails"}]},{"id":"IssueId","description":"A unique id for a DevTools inspector issue. Allows other entities (e.g.\\nexceptions, CDP message, console messages, etc.) to reference an issue.","type":"string"},{"id":"InspectorIssue","description":"An inspector issue reported from the back-end.","type":"object","properties":[{"name":"code","$ref":"InspectorIssueCode"},{"name":"details","$ref":"InspectorIssueDetails"},{"name":"issueId","description":"A unique id for this issue. May be omitted if no other entity (e.g.\\nexception, CDP message, etc.) is referencing this issue.","optional":true,"$ref":"IssueId"}]}],"commands":[{"name":"getEncodedResponse","description":"Returns the response body and size if it were re-encoded with the specified settings. Only\\napplies to images.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"Network.RequestId"},{"name":"encoding","description":"The encoding to use.","type":"string","enum":["webp","jpeg","png"]},{"name":"quality","description":"The quality of the encoding (0-1). (defaults to 1)","optional":true,"type":"number"},{"name":"sizeOnly","description":"Whether to only return the size information (defaults to false).","optional":true,"type":"boolean"}],"returns":[{"name":"body","description":"The encoded body as a base64 string. Omitted if sizeOnly is true. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"originalSize","description":"Size before re-encoding.","type":"integer"},{"name":"encodedSize","description":"Size after re-encoding.","type":"integer"}]},{"name":"disable","description":"Disables issues domain, prevents further issues from being reported to the client."},{"name":"enable","description":"Enables issues domain, sends the issues collected so far to the client by means of the\\n`issueAdded` event."},{"name":"checkContrast","description":"Runs the contrast check for the target page. Found issues are reported\\nusing Audits.issueAdded event.","parameters":[{"name":"reportAAA","description":"Whether to report WCAG AAA level issues. Default is false.","optional":true,"type":"boolean"}]},{"name":"checkFormsIssues","description":"Runs the form issues check for the target page. Found issues are reported\\nusing Audits.issueAdded event.","returns":[{"name":"formIssues","type":"array","items":{"$ref":"GenericIssueDetails"}}]}],"events":[{"name":"issueAdded","parameters":[{"name":"issue","$ref":"InspectorIssue"}]}]},{"domain":"Autofill","description":"Defines commands and events for Autofill.","experimental":true,"types":[{"id":"CreditCard","type":"object","properties":[{"name":"number","description":"16-digit credit card number.","type":"string"},{"name":"name","description":"Name of the credit card owner.","type":"string"},{"name":"expiryMonth","description":"2-digit expiry month.","type":"string"},{"name":"expiryYear","description":"4-digit expiry year.","type":"string"},{"name":"cvc","description":"3-digit card verification code.","type":"string"}]},{"id":"AddressField","type":"object","properties":[{"name":"name","description":"address field name, for example GIVEN_NAME.","type":"string"},{"name":"value","description":"address field name, for example Jon Doe.","type":"string"}]},{"id":"Address","type":"object","properties":[{"name":"fields","description":"fields and values defining a test address.","type":"array","items":{"$ref":"AddressField"}}]}],"commands":[{"name":"trigger","description":"Trigger autofill on a form identified by the fieldId.\\nIf the field and related form cannot be autofilled, returns an error.","parameters":[{"name":"fieldId","description":"Identifies a field that serves as an anchor for autofill.","$ref":"DOM.BackendNodeId"},{"name":"frameId","description":"Identifies the frame that field belongs to.","optional":true,"$ref":"Page.FrameId"},{"name":"card","description":"Credit card information to fill out the form. Credit card data is not saved.","$ref":"CreditCard"}]},{"name":"setAddresses","description":"Set addresses so that developers can verify their forms implementation.","parameters":[{"name":"addresses","type":"array","items":{"$ref":"Address"}}]}]},{"domain":"BackgroundService","description":"Defines events for background web platform features.","experimental":true,"types":[{"id":"ServiceName","description":"The Background Service that will be associated with the commands/events.\\nEvery Background Service operates independently, but they share the same\\nAPI.","type":"string","enum":["backgroundFetch","backgroundSync","pushMessaging","notifications","paymentHandler","periodicBackgroundSync"]},{"id":"EventMetadata","description":"A key-value pair for additional event information to pass along.","type":"object","properties":[{"name":"key","type":"string"},{"name":"value","type":"string"}]},{"id":"BackgroundServiceEvent","type":"object","properties":[{"name":"timestamp","description":"Timestamp of the event (in seconds).","$ref":"Network.TimeSinceEpoch"},{"name":"origin","description":"The origin this event belongs to.","type":"string"},{"name":"serviceWorkerRegistrationId","description":"The Service Worker ID that initiated the event.","$ref":"ServiceWorker.RegistrationID"},{"name":"service","description":"The Background Service this event belongs to.","$ref":"ServiceName"},{"name":"eventName","description":"A description of the event.","type":"string"},{"name":"instanceId","description":"An identifier that groups related events together.","type":"string"},{"name":"eventMetadata","description":"A list of event-specific information.","type":"array","items":{"$ref":"EventMetadata"}},{"name":"storageKey","description":"Storage key this event belongs to.","type":"string"}]}],"commands":[{"name":"startObserving","description":"Enables event updates for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]},{"name":"stopObserving","description":"Disables event updates for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]},{"name":"setRecording","description":"Set the recording state for the service.","parameters":[{"name":"shouldRecord","type":"boolean"},{"name":"service","$ref":"ServiceName"}]},{"name":"clearEvents","description":"Clears all stored data for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]}],"events":[{"name":"recordingStateChanged","description":"Called when the recording state for the service has been updated.","parameters":[{"name":"isRecording","type":"boolean"},{"name":"service","$ref":"ServiceName"}]},{"name":"backgroundServiceEventReceived","description":"Called with all existing backgroundServiceEvents when enabled, and all new\\nevents afterwards if enabled and recording.","parameters":[{"name":"backgroundServiceEvent","$ref":"BackgroundServiceEvent"}]}]},{"domain":"Browser","description":"The Browser domain defines methods and events for browser managing.","types":[{"id":"BrowserContextID","experimental":true,"type":"string"},{"id":"WindowID","experimental":true,"type":"integer"},{"id":"WindowState","description":"The state of the browser window.","experimental":true,"type":"string","enum":["normal","minimized","maximized","fullscreen"]},{"id":"Bounds","description":"Browser window bounds information","experimental":true,"type":"object","properties":[{"name":"left","description":"The offset from the left edge of the screen to the window in pixels.","optional":true,"type":"integer"},{"name":"top","description":"The offset from the top edge of the screen to the window in pixels.","optional":true,"type":"integer"},{"name":"width","description":"The window width in pixels.","optional":true,"type":"integer"},{"name":"height","description":"The window height in pixels.","optional":true,"type":"integer"},{"name":"windowState","description":"The window state. Default to normal.","optional":true,"$ref":"WindowState"}]},{"id":"PermissionType","experimental":true,"type":"string","enum":["accessibilityEvents","audioCapture","backgroundSync","backgroundFetch","clipboardReadWrite","clipboardSanitizedWrite","displayCapture","durableStorage","flash","geolocation","idleDetection","localFonts","midi","midiSysex","nfc","notifications","paymentHandler","periodicBackgroundSync","protectedMediaIdentifier","sensors","storageAccess","topLevelStorageAccess","videoCapture","videoCapturePanTiltZoom","wakeLockScreen","wakeLockSystem","windowManagement"]},{"id":"PermissionSetting","experimental":true,"type":"string","enum":["granted","denied","prompt"]},{"id":"PermissionDescriptor","description":"Definition of PermissionDescriptor defined in the Permissions API:\\nhttps://w3c.github.io/permissions/#dictdef-permissiondescriptor.","experimental":true,"type":"object","properties":[{"name":"name","description":"Name of permission.\\nSee https://cs.chromium.org/chromium/src/third_party/blink/renderer/modules/permissions/permission_descriptor.idl for valid permission names.","type":"string"},{"name":"sysex","description":"For \\"midi\\" permission, may also specify sysex control.","optional":true,"type":"boolean"},{"name":"userVisibleOnly","description":"For \\"push\\" permission, may specify userVisibleOnly.\\nNote that userVisibleOnly = true is the only currently supported type.","optional":true,"type":"boolean"},{"name":"allowWithoutSanitization","description":"For \\"clipboard\\" permission, may specify allowWithoutSanitization.","optional":true,"type":"boolean"},{"name":"panTiltZoom","description":"For \\"camera\\" permission, may specify panTiltZoom.","optional":true,"type":"boolean"}]},{"id":"BrowserCommandId","description":"Browser command ids used by executeBrowserCommand.","experimental":true,"type":"string","enum":["openTabSearch","closeTabSearch"]},{"id":"Bucket","description":"Chrome histogram bucket.","experimental":true,"type":"object","properties":[{"name":"low","description":"Minimum value (inclusive).","type":"integer"},{"name":"high","description":"Maximum value (exclusive).","type":"integer"},{"name":"count","description":"Number of samples.","type":"integer"}]},{"id":"Histogram","description":"Chrome histogram.","experimental":true,"type":"object","properties":[{"name":"name","description":"Name.","type":"string"},{"name":"sum","description":"Sum of sample values.","type":"integer"},{"name":"count","description":"Total number of samples.","type":"integer"},{"name":"buckets","description":"Buckets.","type":"array","items":{"$ref":"Bucket"}}]}],"commands":[{"name":"setPermission","description":"Set permission settings for given origin.","experimental":true,"parameters":[{"name":"permission","description":"Descriptor of permission to override.","$ref":"PermissionDescriptor"},{"name":"setting","description":"Setting of the permission.","$ref":"PermissionSetting"},{"name":"origin","description":"Origin the permission applies to, all origins if not specified.","optional":true,"type":"string"},{"name":"browserContextId","description":"Context to override. When omitted, default browser context is used.","optional":true,"$ref":"BrowserContextID"}]},{"name":"grantPermissions","description":"Grant specific permissions to the given origin and reject all others.","experimental":true,"parameters":[{"name":"permissions","type":"array","items":{"$ref":"PermissionType"}},{"name":"origin","description":"Origin the permission applies to, all origins if not specified.","optional":true,"type":"string"},{"name":"browserContextId","description":"BrowserContext to override permissions. When omitted, default browser context is used.","optional":true,"$ref":"BrowserContextID"}]},{"name":"resetPermissions","description":"Reset all permission management for all origins.","experimental":true,"parameters":[{"name":"browserContextId","description":"BrowserContext to reset permissions. When omitted, default browser context is used.","optional":true,"$ref":"BrowserContextID"}]},{"name":"setDownloadBehavior","description":"Set the behavior when downloading a file.","experimental":true,"parameters":[{"name":"behavior","description":"Whether to allow all or deny all download requests, or use default Chrome behavior if\\navailable (otherwise deny). |allowAndName| allows download and names files according to\\ntheir dowmload guids.","type":"string","enum":["deny","allow","allowAndName","default"]},{"name":"browserContextId","description":"BrowserContext to set download behavior. When omitted, default browser context is used.","optional":true,"$ref":"BrowserContextID"},{"name":"downloadPath","description":"The default path to save downloaded files to. This is required if behavior is set to \'allow\'\\nor \'allowAndName\'.","optional":true,"type":"string"},{"name":"eventsEnabled","description":"Whether to emit download events (defaults to false).","optional":true,"type":"boolean"}]},{"name":"cancelDownload","description":"Cancel a download if in progress","experimental":true,"parameters":[{"name":"guid","description":"Global unique identifier of the download.","type":"string"},{"name":"browserContextId","description":"BrowserContext to perform the action in. When omitted, default browser context is used.","optional":true,"$ref":"BrowserContextID"}]},{"name":"close","description":"Close browser gracefully."},{"name":"crash","description":"Crashes browser on the main thread.","experimental":true},{"name":"crashGpuProcess","description":"Crashes GPU process.","experimental":true},{"name":"getVersion","description":"Returns version information.","returns":[{"name":"protocolVersion","description":"Protocol version.","type":"string"},{"name":"product","description":"Product name.","type":"string"},{"name":"revision","description":"Product revision.","type":"string"},{"name":"userAgent","description":"User-Agent.","type":"string"},{"name":"jsVersion","description":"V8 version.","type":"string"}]},{"name":"getBrowserCommandLine","description":"Returns the command line switches for the browser process if, and only if\\n--enable-automation is on the commandline.","experimental":true,"returns":[{"name":"arguments","description":"Commandline parameters","type":"array","items":{"type":"string"}}]},{"name":"getHistograms","description":"Get Chrome histograms.","experimental":true,"parameters":[{"name":"query","description":"Requested substring in name. Only histograms which have query as a\\nsubstring in their name are extracted. An empty or absent query returns\\nall histograms.","optional":true,"type":"string"},{"name":"delta","description":"If true, retrieve delta since last delta call.","optional":true,"type":"boolean"}],"returns":[{"name":"histograms","description":"Histograms.","type":"array","items":{"$ref":"Histogram"}}]},{"name":"getHistogram","description":"Get a Chrome histogram by name.","experimental":true,"parameters":[{"name":"name","description":"Requested histogram name.","type":"string"},{"name":"delta","description":"If true, retrieve delta since last delta call.","optional":true,"type":"boolean"}],"returns":[{"name":"histogram","description":"Histogram.","$ref":"Histogram"}]},{"name":"getWindowBounds","description":"Get position and size of the browser window.","experimental":true,"parameters":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"}],"returns":[{"name":"bounds","description":"Bounds information of the window. When window state is \'minimized\', the restored window\\nposition and size are returned.","$ref":"Bounds"}]},{"name":"getWindowForTarget","description":"Get the browser window that contains the devtools target.","experimental":true,"parameters":[{"name":"targetId","description":"Devtools agent host id. If called as a part of the session, associated targetId is used.","optional":true,"$ref":"Target.TargetID"}],"returns":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"},{"name":"bounds","description":"Bounds information of the window. When window state is \'minimized\', the restored window\\nposition and size are returned.","$ref":"Bounds"}]},{"name":"setWindowBounds","description":"Set position and/or size of the browser window.","experimental":true,"parameters":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"},{"name":"bounds","description":"New window bounds. The \'minimized\', \'maximized\' and \'fullscreen\' states cannot be combined\\nwith \'left\', \'top\', \'width\' or \'height\'. Leaves unspecified fields unchanged.","$ref":"Bounds"}]},{"name":"setDockTile","description":"Set dock tile details, platform-specific.","experimental":true,"parameters":[{"name":"badgeLabel","optional":true,"type":"string"},{"name":"image","description":"Png encoded image. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"}]},{"name":"executeBrowserCommand","description":"Invoke custom browser commands used by telemetry.","experimental":true,"parameters":[{"name":"commandId","$ref":"BrowserCommandId"}]},{"name":"addPrivacySandboxEnrollmentOverride","description":"Allows a site to use privacy sandbox features that require enrollment\\nwithout the site actually being enrolled. Only supported on page targets.","parameters":[{"name":"url","type":"string"}]}],"events":[{"name":"downloadWillBegin","description":"Fired when page is about to start a download.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that caused the download to begin.","$ref":"Page.FrameId"},{"name":"guid","description":"Global unique identifier of the download.","type":"string"},{"name":"url","description":"URL of the resource being downloaded.","type":"string"},{"name":"suggestedFilename","description":"Suggested file name of the resource (the actual name of the file saved on disk may differ).","type":"string"}]},{"name":"downloadProgress","description":"Fired when download makes progress. Last call has |done| == true.","experimental":true,"parameters":[{"name":"guid","description":"Global unique identifier of the download.","type":"string"},{"name":"totalBytes","description":"Total expected bytes to download.","type":"number"},{"name":"receivedBytes","description":"Total bytes received.","type":"number"},{"name":"state","description":"Download status.","type":"string","enum":["inProgress","completed","canceled"]}]}]},{"domain":"CSS","description":"This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles)\\nhave an associated `id` used in subsequent operations on the related object. Each object type has\\na specific `id` structure, and those are not interchangeable between objects of different kinds.\\nCSS objects can be loaded using the `get*ForNode()` calls (which accept a DOM node id). A client\\ncan also keep track of stylesheets via the `styleSheetAdded`/`styleSheetRemoved` events and\\nsubsequently load the required stylesheet contents using the `getStyleSheet[Text]()` methods.","experimental":true,"dependencies":["DOM","Page"],"types":[{"id":"StyleSheetId","type":"string"},{"id":"StyleSheetOrigin","description":"Stylesheet type: \\"injected\\" for stylesheets injected via extension, \\"user-agent\\" for user-agent\\nstylesheets, \\"inspector\\" for stylesheets created by the inspector (i.e. those holding the \\"via\\ninspector\\" rules), \\"regular\\" for regular stylesheets.","type":"string","enum":["injected","user-agent","inspector","regular"]},{"id":"PseudoElementMatches","description":"CSS rule collection for a single pseudo style.","type":"object","properties":[{"name":"pseudoType","description":"Pseudo element type.","$ref":"DOM.PseudoType"},{"name":"pseudoIdentifier","description":"Pseudo element custom ident.","optional":true,"type":"string"},{"name":"matches","description":"Matches of CSS rules applicable to the pseudo style.","type":"array","items":{"$ref":"RuleMatch"}}]},{"id":"InheritedStyleEntry","description":"Inherited CSS rule collection from ancestor node.","type":"object","properties":[{"name":"inlineStyle","description":"The ancestor node\'s inline style, if any, in the style inheritance chain.","optional":true,"$ref":"CSSStyle"},{"name":"matchedCSSRules","description":"Matches of CSS rules matching the ancestor node in the style inheritance chain.","type":"array","items":{"$ref":"RuleMatch"}}]},{"id":"InheritedPseudoElementMatches","description":"Inherited pseudo element matches from pseudos of an ancestor node.","type":"object","properties":[{"name":"pseudoElements","description":"Matches of pseudo styles from the pseudos of an ancestor node.","type":"array","items":{"$ref":"PseudoElementMatches"}}]},{"id":"RuleMatch","description":"Match data for a CSS rule.","type":"object","properties":[{"name":"rule","description":"CSS rule in the match.","$ref":"CSSRule"},{"name":"matchingSelectors","description":"Matching selector indices in the rule\'s selectorList selectors (0-based).","type":"array","items":{"type":"integer"}}]},{"id":"Value","description":"Data for a simple selector (these are delimited by commas in a selector list).","type":"object","properties":[{"name":"text","description":"Value text.","type":"string"},{"name":"range","description":"Value range in the underlying resource (if available).","optional":true,"$ref":"SourceRange"},{"name":"specificity","description":"Specificity of the selector.","experimental":true,"optional":true,"$ref":"Specificity"}]},{"id":"Specificity","description":"Specificity:\\nhttps://drafts.csswg.org/selectors/#specificity-rules","experimental":true,"type":"object","properties":[{"name":"a","description":"The a component, which represents the number of ID selectors.","type":"integer"},{"name":"b","description":"The b component, which represents the number of class selectors, attributes selectors, and\\npseudo-classes.","type":"integer"},{"name":"c","description":"The c component, which represents the number of type selectors and pseudo-elements.","type":"integer"}]},{"id":"SelectorList","description":"Selector list data.","type":"object","properties":[{"name":"selectors","description":"Selectors in the list.","type":"array","items":{"$ref":"Value"}},{"name":"text","description":"Rule selector text.","type":"string"}]},{"id":"CSSStyleSheetHeader","description":"CSS stylesheet metainformation.","type":"object","properties":[{"name":"styleSheetId","description":"The stylesheet identifier.","$ref":"StyleSheetId"},{"name":"frameId","description":"Owner frame identifier.","$ref":"Page.FrameId"},{"name":"sourceURL","description":"Stylesheet resource URL. Empty if this is a constructed stylesheet created using\\nnew CSSStyleSheet() (but non-empty if this is a constructed sylesheet imported\\nas a CSS module script).","type":"string"},{"name":"sourceMapURL","description":"URL of source map associated with the stylesheet (if any).","optional":true,"type":"string"},{"name":"origin","description":"Stylesheet origin.","$ref":"StyleSheetOrigin"},{"name":"title","description":"Stylesheet title.","type":"string"},{"name":"ownerNode","description":"The backend id for the owner node of the stylesheet.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"disabled","description":"Denotes whether the stylesheet is disabled.","type":"boolean"},{"name":"hasSourceURL","description":"Whether the sourceURL field value comes from the sourceURL comment.","optional":true,"type":"boolean"},{"name":"isInline","description":"Whether this stylesheet is created for STYLE tag by parser. This flag is not set for\\ndocument.written STYLE tags.","type":"boolean"},{"name":"isMutable","description":"Whether this stylesheet is mutable. Inline stylesheets become mutable\\nafter they have been modified via CSSOM API.\\n`<link>` element\'s stylesheets become mutable only if DevTools modifies them.\\nConstructed stylesheets (new CSSStyleSheet()) are mutable immediately after creation.","type":"boolean"},{"name":"isConstructed","description":"True if this stylesheet is created through new CSSStyleSheet() or imported as a\\nCSS module script.","type":"boolean"},{"name":"startLine","description":"Line offset of the stylesheet within the resource (zero based).","type":"number"},{"name":"startColumn","description":"Column offset of the stylesheet within the resource (zero based).","type":"number"},{"name":"length","description":"Size of the content (in characters).","type":"number"},{"name":"endLine","description":"Line offset of the end of the stylesheet within the resource (zero based).","type":"number"},{"name":"endColumn","description":"Column offset of the end of the stylesheet within the resource (zero based).","type":"number"},{"name":"loadingFailed","description":"If the style sheet was loaded from a network resource, this indicates when the resource failed to load","experimental":true,"optional":true,"type":"boolean"}]},{"id":"CSSRule","description":"CSS rule representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"selectorList","description":"Rule selector data.","$ref":"SelectorList"},{"name":"nestingSelectors","description":"Array of selectors from ancestor style rules, sorted by distance from the current rule.","experimental":true,"optional":true,"type":"array","items":{"type":"string"}},{"name":"origin","description":"Parent stylesheet\'s origin.","$ref":"StyleSheetOrigin"},{"name":"style","description":"Associated style declaration.","$ref":"CSSStyle"},{"name":"media","description":"Media list array (for rules involving media queries). The array enumerates media queries\\nstarting with the innermost one, going outwards.","optional":true,"type":"array","items":{"$ref":"CSSMedia"}},{"name":"containerQueries","description":"Container query list array (for rules involving container queries).\\nThe array enumerates container queries starting with the innermost one, going outwards.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSContainerQuery"}},{"name":"supports","description":"@supports CSS at-rule array.\\nThe array enumerates @supports at-rules starting with the innermost one, going outwards.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSSupports"}},{"name":"layers","description":"Cascade layer array. Contains the layer hierarchy that this rule belongs to starting\\nwith the innermost layer and going outwards.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSLayer"}},{"name":"scopes","description":"@scope CSS at-rule array.\\nThe array enumerates @scope at-rules starting with the innermost one, going outwards.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSScope"}},{"name":"ruleTypes","description":"The array keeps the types of ancestor CSSRules from the innermost going outwards.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSRuleType"}}]},{"id":"CSSRuleType","description":"Enum indicating the type of a CSS rule, used to represent the order of a style rule\'s ancestors.\\nThis list only contains rule types that are collected during the ancestor rule collection.","experimental":true,"type":"string","enum":["MediaRule","SupportsRule","ContainerRule","LayerRule","ScopeRule","StyleRule"]},{"id":"RuleUsage","description":"CSS coverage information.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","$ref":"StyleSheetId"},{"name":"startOffset","description":"Offset of the start of the rule (including selector) from the beginning of the stylesheet.","type":"number"},{"name":"endOffset","description":"Offset of the end of the rule body from the beginning of the stylesheet.","type":"number"},{"name":"used","description":"Indicates whether the rule was actually used by some element in the page.","type":"boolean"}]},{"id":"SourceRange","description":"Text range within a resource. All numbers are zero-based.","type":"object","properties":[{"name":"startLine","description":"Start line of range.","type":"integer"},{"name":"startColumn","description":"Start column of range (inclusive).","type":"integer"},{"name":"endLine","description":"End line of range","type":"integer"},{"name":"endColumn","description":"End column of range (exclusive).","type":"integer"}]},{"id":"ShorthandEntry","type":"object","properties":[{"name":"name","description":"Shorthand name.","type":"string"},{"name":"value","description":"Shorthand value.","type":"string"},{"name":"important","description":"Whether the property has \\"!important\\" annotation (implies `false` if absent).","optional":true,"type":"boolean"}]},{"id":"CSSComputedStyleProperty","type":"object","properties":[{"name":"name","description":"Computed style property name.","type":"string"},{"name":"value","description":"Computed style property value.","type":"string"}]},{"id":"CSSStyle","description":"CSS style representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"cssProperties","description":"CSS properties in the style.","type":"array","items":{"$ref":"CSSProperty"}},{"name":"shorthandEntries","description":"Computed values for all shorthands found in the style.","type":"array","items":{"$ref":"ShorthandEntry"}},{"name":"cssText","description":"Style declaration text (if available).","optional":true,"type":"string"},{"name":"range","description":"Style declaration range in the enclosing stylesheet (if available).","optional":true,"$ref":"SourceRange"}]},{"id":"CSSProperty","description":"CSS property declaration data.","type":"object","properties":[{"name":"name","description":"The property name.","type":"string"},{"name":"value","description":"The property value.","type":"string"},{"name":"important","description":"Whether the property has \\"!important\\" annotation (implies `false` if absent).","optional":true,"type":"boolean"},{"name":"implicit","description":"Whether the property is implicit (implies `false` if absent).","optional":true,"type":"boolean"},{"name":"text","description":"The full property text as specified in the style.","optional":true,"type":"string"},{"name":"parsedOk","description":"Whether the property is understood by the browser (implies `true` if absent).","optional":true,"type":"boolean"},{"name":"disabled","description":"Whether the property is disabled by the user (present for source-based properties only).","optional":true,"type":"boolean"},{"name":"range","description":"The entire property range in the enclosing style declaration (if available).","optional":true,"$ref":"SourceRange"},{"name":"longhandProperties","description":"Parsed longhand components of this property if it is a shorthand.\\nThis field will be empty if the given property is not a shorthand.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSProperty"}}]},{"id":"CSSMedia","description":"CSS media rule descriptor.","type":"object","properties":[{"name":"text","description":"Media query text.","type":"string"},{"name":"source","description":"Source of the media query: \\"mediaRule\\" if specified by a @media rule, \\"importRule\\" if\\nspecified by an @import rule, \\"linkedSheet\\" if specified by a \\"media\\" attribute in a linked\\nstylesheet\'s LINK tag, \\"inlineSheet\\" if specified by a \\"media\\" attribute in an inline\\nstylesheet\'s STYLE tag.","type":"string","enum":["mediaRule","importRule","linkedSheet","inlineSheet"]},{"name":"sourceURL","description":"URL of the document containing the media query description.","optional":true,"type":"string"},{"name":"range","description":"The associated rule (@media or @import) header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"},{"name":"mediaList","description":"Array of media queries.","optional":true,"type":"array","items":{"$ref":"MediaQuery"}}]},{"id":"MediaQuery","description":"Media query descriptor.","type":"object","properties":[{"name":"expressions","description":"Array of media query expressions.","type":"array","items":{"$ref":"MediaQueryExpression"}},{"name":"active","description":"Whether the media query condition is satisfied.","type":"boolean"}]},{"id":"MediaQueryExpression","description":"Media query expression descriptor.","type":"object","properties":[{"name":"value","description":"Media query expression value.","type":"number"},{"name":"unit","description":"Media query expression units.","type":"string"},{"name":"feature","description":"Media query expression feature.","type":"string"},{"name":"valueRange","description":"The associated range of the value text in the enclosing stylesheet (if available).","optional":true,"$ref":"SourceRange"},{"name":"computedLength","description":"Computed length of media query expression (if applicable).","optional":true,"type":"number"}]},{"id":"CSSContainerQuery","description":"CSS container query rule descriptor.","experimental":true,"type":"object","properties":[{"name":"text","description":"Container query text.","type":"string"},{"name":"range","description":"The associated rule header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"},{"name":"name","description":"Optional name for the container.","optional":true,"type":"string"},{"name":"physicalAxes","description":"Optional physical axes queried for the container.","optional":true,"$ref":"DOM.PhysicalAxes"},{"name":"logicalAxes","description":"Optional logical axes queried for the container.","optional":true,"$ref":"DOM.LogicalAxes"}]},{"id":"CSSSupports","description":"CSS Supports at-rule descriptor.","experimental":true,"type":"object","properties":[{"name":"text","description":"Supports rule text.","type":"string"},{"name":"active","description":"Whether the supports condition is satisfied.","type":"boolean"},{"name":"range","description":"The associated rule header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"}]},{"id":"CSSScope","description":"CSS Scope at-rule descriptor.","experimental":true,"type":"object","properties":[{"name":"text","description":"Scope rule text.","type":"string"},{"name":"range","description":"The associated rule header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"}]},{"id":"CSSLayer","description":"CSS Layer at-rule descriptor.","experimental":true,"type":"object","properties":[{"name":"text","description":"Layer name.","type":"string"},{"name":"range","description":"The associated rule header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"}]},{"id":"CSSLayerData","description":"CSS Layer data.","experimental":true,"type":"object","properties":[{"name":"name","description":"Layer name.","type":"string"},{"name":"subLayers","description":"Direct sub-layers","optional":true,"type":"array","items":{"$ref":"CSSLayerData"}},{"name":"order","description":"Layer order. The order determines the order of the layer in the cascade order.\\nA higher number has higher priority in the cascade order.","type":"number"}]},{"id":"PlatformFontUsage","description":"Information about amount of glyphs that were rendered with given font.","type":"object","properties":[{"name":"familyName","description":"Font\'s family name reported by platform.","type":"string"},{"name":"isCustomFont","description":"Indicates if the font was downloaded or resolved locally.","type":"boolean"},{"name":"glyphCount","description":"Amount of glyphs that were rendered with this font.","type":"number"}]},{"id":"FontVariationAxis","description":"Information about font variation axes for variable fonts","type":"object","properties":[{"name":"tag","description":"The font-variation-setting tag (a.k.a. \\"axis tag\\").","type":"string"},{"name":"name","description":"Human-readable variation name in the default language (normally, \\"en\\").","type":"string"},{"name":"minValue","description":"The minimum value (inclusive) the font supports for this tag.","type":"number"},{"name":"maxValue","description":"The maximum value (inclusive) the font supports for this tag.","type":"number"},{"name":"defaultValue","description":"The default value.","type":"number"}]},{"id":"FontFace","description":"Properties of a web font: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#font-descriptions\\nand additional information such as platformFontFamily and fontVariationAxes.","type":"object","properties":[{"name":"fontFamily","description":"The font-family.","type":"string"},{"name":"fontStyle","description":"The font-style.","type":"string"},{"name":"fontVariant","description":"The font-variant.","type":"string"},{"name":"fontWeight","description":"The font-weight.","type":"string"},{"name":"fontStretch","description":"The font-stretch.","type":"string"},{"name":"fontDisplay","description":"The font-display.","type":"string"},{"name":"unicodeRange","description":"The unicode-range.","type":"string"},{"name":"src","description":"The src.","type":"string"},{"name":"platformFontFamily","description":"The resolved platform font family","type":"string"},{"name":"fontVariationAxes","description":"Available variation settings (a.k.a. \\"axes\\").","optional":true,"type":"array","items":{"$ref":"FontVariationAxis"}}]},{"id":"CSSTryRule","description":"CSS try rule representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"origin","description":"Parent stylesheet\'s origin.","$ref":"StyleSheetOrigin"},{"name":"style","description":"Associated style declaration.","$ref":"CSSStyle"}]},{"id":"CSSPositionFallbackRule","description":"CSS position-fallback rule representation.","type":"object","properties":[{"name":"name","$ref":"Value"},{"name":"tryRules","description":"List of keyframes.","type":"array","items":{"$ref":"CSSTryRule"}}]},{"id":"CSSKeyframesRule","description":"CSS keyframes rule representation.","type":"object","properties":[{"name":"animationName","description":"Animation name.","$ref":"Value"},{"name":"keyframes","description":"List of keyframes.","type":"array","items":{"$ref":"CSSKeyframeRule"}}]},{"id":"CSSKeyframeRule","description":"CSS keyframe rule representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"origin","description":"Parent stylesheet\'s origin.","$ref":"StyleSheetOrigin"},{"name":"keyText","description":"Associated key text.","$ref":"Value"},{"name":"style","description":"Associated style declaration.","$ref":"CSSStyle"}]},{"id":"StyleDeclarationEdit","description":"A descriptor of operation to mutate style declaration text.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier.","$ref":"StyleSheetId"},{"name":"range","description":"The range of the style text in the enclosing stylesheet.","$ref":"SourceRange"},{"name":"text","description":"New style text.","type":"string"}]}],"commands":[{"name":"addRule","description":"Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the\\nposition specified by `location`.","parameters":[{"name":"styleSheetId","description":"The css style sheet identifier where a new rule should be inserted.","$ref":"StyleSheetId"},{"name":"ruleText","description":"The text of a new rule.","type":"string"},{"name":"location","description":"Text position of a new rule in the target style sheet.","$ref":"SourceRange"}],"returns":[{"name":"rule","description":"The newly created rule.","$ref":"CSSRule"}]},{"name":"collectClassNames","description":"Returns all class names from specified stylesheet.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}],"returns":[{"name":"classNames","description":"Class name list.","type":"array","items":{"type":"string"}}]},{"name":"createStyleSheet","description":"Creates a new special \\"via-inspector\\" stylesheet in the frame with given `frameId`.","parameters":[{"name":"frameId","description":"Identifier of the frame where \\"via-inspector\\" stylesheet should be created.","$ref":"Page.FrameId"}],"returns":[{"name":"styleSheetId","description":"Identifier of the created \\"via-inspector\\" stylesheet.","$ref":"StyleSheetId"}]},{"name":"disable","description":"Disables the CSS agent for the given page."},{"name":"enable","description":"Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been\\nenabled until the result of this command is received."},{"name":"forcePseudoState","description":"Ensures that the given node will have specified pseudo-classes whenever its style is computed by\\nthe browser.","parameters":[{"name":"nodeId","description":"The element id for which to force the pseudo state.","$ref":"DOM.NodeId"},{"name":"forcedPseudoClasses","description":"Element pseudo classes to force when computing the element\'s style.","type":"array","items":{"type":"string"}}]},{"name":"getBackgroundColors","parameters":[{"name":"nodeId","description":"Id of the node to get background colors for.","$ref":"DOM.NodeId"}],"returns":[{"name":"backgroundColors","description":"The range of background colors behind this element, if it contains any visible text. If no\\nvisible text is present, this will be undefined. In the case of a flat background color,\\nthis will consist of simply that color. In the case of a gradient, this will consist of each\\nof the color stops. For anything more complicated, this will be an empty array. Images will\\nbe ignored (as if the image had failed to load).","optional":true,"type":"array","items":{"type":"string"}},{"name":"computedFontSize","description":"The computed font size for this node, as a CSS computed value string (e.g. \'12px\').","optional":true,"type":"string"},{"name":"computedFontWeight","description":"The computed font weight for this node, as a CSS computed value string (e.g. \'normal\' or\\n\'100\').","optional":true,"type":"string"}]},{"name":"getComputedStyleForNode","description":"Returns the computed style for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"computedStyle","description":"Computed style for the specified DOM node.","type":"array","items":{"$ref":"CSSComputedStyleProperty"}}]},{"name":"getInlineStylesForNode","description":"Returns the styles defined inline (explicitly in the \\"style\\" attribute and implicitly, using DOM\\nattributes) for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"inlineStyle","description":"Inline style for the specified DOM node.","optional":true,"$ref":"CSSStyle"},{"name":"attributesStyle","description":"Attribute-defined element style (e.g. resulting from \\"width=20 height=100%\\").","optional":true,"$ref":"CSSStyle"}]},{"name":"getMatchedStylesForNode","description":"Returns requested styles for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"inlineStyle","description":"Inline style for the specified DOM node.","optional":true,"$ref":"CSSStyle"},{"name":"attributesStyle","description":"Attribute-defined element style (e.g. resulting from \\"width=20 height=100%\\").","optional":true,"$ref":"CSSStyle"},{"name":"matchedCSSRules","description":"CSS rules matching this node, from all applicable stylesheets.","optional":true,"type":"array","items":{"$ref":"RuleMatch"}},{"name":"pseudoElements","description":"Pseudo style matches for this node.","optional":true,"type":"array","items":{"$ref":"PseudoElementMatches"}},{"name":"inherited","description":"A chain of inherited styles (from the immediate node parent up to the DOM tree root).","optional":true,"type":"array","items":{"$ref":"InheritedStyleEntry"}},{"name":"inheritedPseudoElements","description":"A chain of inherited pseudo element styles (from the immediate node parent up to the DOM tree root).","optional":true,"type":"array","items":{"$ref":"InheritedPseudoElementMatches"}},{"name":"cssKeyframesRules","description":"A list of CSS keyframed animations matching this node.","optional":true,"type":"array","items":{"$ref":"CSSKeyframesRule"}},{"name":"cssPositionFallbackRules","description":"A list of CSS position fallbacks matching this node.","optional":true,"type":"array","items":{"$ref":"CSSPositionFallbackRule"}},{"name":"parentLayoutNodeId","description":"Id of the first parent element that does not have display: contents.","experimental":true,"optional":true,"$ref":"DOM.NodeId"}]},{"name":"getMediaQueries","description":"Returns all media queries parsed by the rendering engine.","returns":[{"name":"medias","type":"array","items":{"$ref":"CSSMedia"}}]},{"name":"getPlatformFontsForNode","description":"Requests information about platform fonts which we used to render child TextNodes in the given\\nnode.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"fonts","description":"Usage statistics for every employed platform font.","type":"array","items":{"$ref":"PlatformFontUsage"}}]},{"name":"getStyleSheetText","description":"Returns the current textual content for a stylesheet.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}],"returns":[{"name":"text","description":"The stylesheet text.","type":"string"}]},{"name":"getLayersForNode","description":"Returns all layers parsed by the rendering engine for the tree scope of a node.\\nGiven a DOM element identified by nodeId, getLayersForNode returns the root\\nlayer for the nearest ancestor document or shadow root. The layer root contains\\nthe full layer tree for the tree scope and their ordering.","experimental":true,"parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"rootLayer","$ref":"CSSLayerData"}]},{"name":"trackComputedStyleUpdates","description":"Starts tracking the given computed styles for updates. The specified array of properties\\nreplaces the one previously specified. Pass empty array to disable tracking.\\nUse takeComputedStyleUpdates to retrieve the list of nodes that had properties modified.\\nThe changes to computed style properties are only tracked for nodes pushed to the front-end\\nby the DOM agent. If no changes to the tracked properties occur after the node has been pushed\\nto the front-end, no updates will be issued for the node.","experimental":true,"parameters":[{"name":"propertiesToTrack","type":"array","items":{"$ref":"CSSComputedStyleProperty"}}]},{"name":"takeComputedStyleUpdates","description":"Polls the next batch of computed style updates.","experimental":true,"returns":[{"name":"nodeIds","description":"The list of node Ids that have their tracked computed styles updated.","type":"array","items":{"$ref":"DOM.NodeId"}}]},{"name":"setEffectivePropertyValueForNode","description":"Find a rule with the given active property for the given node and set the new value for this\\nproperty","parameters":[{"name":"nodeId","description":"The element id for which to set property.","$ref":"DOM.NodeId"},{"name":"propertyName","type":"string"},{"name":"value","type":"string"}]},{"name":"setKeyframeKey","description":"Modifies the keyframe rule key text.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"keyText","type":"string"}],"returns":[{"name":"keyText","description":"The resulting key text after modification.","$ref":"Value"}]},{"name":"setMediaText","description":"Modifies the rule selector.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"text","type":"string"}],"returns":[{"name":"media","description":"The resulting CSS media rule after modification.","$ref":"CSSMedia"}]},{"name":"setContainerQueryText","description":"Modifies the expression of a container query.","experimental":true,"parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"text","type":"string"}],"returns":[{"name":"containerQuery","description":"The resulting CSS container query rule after modification.","$ref":"CSSContainerQuery"}]},{"name":"setSupportsText","description":"Modifies the expression of a supports at-rule.","experimental":true,"parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"text","type":"string"}],"returns":[{"name":"supports","description":"The resulting CSS Supports rule after modification.","$ref":"CSSSupports"}]},{"name":"setScopeText","description":"Modifies the expression of a scope at-rule.","experimental":true,"parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"text","type":"string"}],"returns":[{"name":"scope","description":"The resulting CSS Scope rule after modification.","$ref":"CSSScope"}]},{"name":"setRuleSelector","description":"Modifies the rule selector.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"selector","type":"string"}],"returns":[{"name":"selectorList","description":"The resulting selector list after modification.","$ref":"SelectorList"}]},{"name":"setStyleSheetText","description":"Sets the new stylesheet text.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"text","type":"string"}],"returns":[{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"}]},{"name":"setStyleTexts","description":"Applies specified style edits one after another in the given order.","parameters":[{"name":"edits","type":"array","items":{"$ref":"StyleDeclarationEdit"}}],"returns":[{"name":"styles","description":"The resulting styles after modification.","type":"array","items":{"$ref":"CSSStyle"}}]},{"name":"startRuleUsageTracking","description":"Enables the selector recording."},{"name":"stopRuleUsageTracking","description":"Stop tracking rule usage and return the list of rules that were used since last call to\\n`takeCoverageDelta` (or since start of coverage instrumentation).","returns":[{"name":"ruleUsage","type":"array","items":{"$ref":"RuleUsage"}}]},{"name":"takeCoverageDelta","description":"Obtain list of rules that became used since last call to this method (or since start of coverage\\ninstrumentation).","returns":[{"name":"coverage","type":"array","items":{"$ref":"RuleUsage"}},{"name":"timestamp","description":"Monotonically increasing time, in seconds.","type":"number"}]},{"name":"setLocalFontsEnabled","description":"Enables/disables rendering of local CSS fonts (enabled by default).","experimental":true,"parameters":[{"name":"enabled","description":"Whether rendering of local fonts is enabled.","type":"boolean"}]}],"events":[{"name":"fontsUpdated","description":"Fires whenever a web font is updated. A non-empty font parameter indicates a successfully loaded\\nweb font.","parameters":[{"name":"font","description":"The web font that has loaded.","optional":true,"$ref":"FontFace"}]},{"name":"mediaQueryResultChanged","description":"Fires whenever a MediaQuery result changes (for example, after a browser window has been\\nresized.) The current implementation considers only viewport-dependent media features."},{"name":"styleSheetAdded","description":"Fired whenever an active document stylesheet is added.","parameters":[{"name":"header","description":"Added stylesheet metainfo.","$ref":"CSSStyleSheetHeader"}]},{"name":"styleSheetChanged","description":"Fired whenever a stylesheet is changed as a result of the client operation.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}]},{"name":"styleSheetRemoved","description":"Fired whenever an active document stylesheet is removed.","parameters":[{"name":"styleSheetId","description":"Identifier of the removed stylesheet.","$ref":"StyleSheetId"}]}]},{"domain":"CacheStorage","experimental":true,"dependencies":["Storage"],"types":[{"id":"CacheId","description":"Unique identifier of the Cache object.","type":"string"},{"id":"CachedResponseType","description":"type of HTTP response cached","type":"string","enum":["basic","cors","default","error","opaqueResponse","opaqueRedirect"]},{"id":"DataEntry","description":"Data entry.","type":"object","properties":[{"name":"requestURL","description":"Request URL.","type":"string"},{"name":"requestMethod","description":"Request method.","type":"string"},{"name":"requestHeaders","description":"Request headers","type":"array","items":{"$ref":"Header"}},{"name":"responseTime","description":"Number of seconds since epoch.","type":"number"},{"name":"responseStatus","description":"HTTP response status code.","type":"integer"},{"name":"responseStatusText","description":"HTTP response status text.","type":"string"},{"name":"responseType","description":"HTTP response type","$ref":"CachedResponseType"},{"name":"responseHeaders","description":"Response headers","type":"array","items":{"$ref":"Header"}}]},{"id":"Cache","description":"Cache identifier.","type":"object","properties":[{"name":"cacheId","description":"An opaque unique id of the cache.","$ref":"CacheId"},{"name":"securityOrigin","description":"Security origin of the cache.","type":"string"},{"name":"storageKey","description":"Storage key of the cache.","type":"string"},{"name":"storageBucket","description":"Storage bucket of the cache.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"cacheName","description":"The name of the cache.","type":"string"}]},{"id":"Header","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"CachedResponse","description":"Cached response","type":"object","properties":[{"name":"body","description":"Entry content, base64-encoded. (Encoded as a base64 string when passed over JSON)","type":"string"}]}],"commands":[{"name":"deleteCache","description":"Deletes a cache.","parameters":[{"name":"cacheId","description":"Id of cache for deletion.","$ref":"CacheId"}]},{"name":"deleteEntry","description":"Deletes a cache entry.","parameters":[{"name":"cacheId","description":"Id of cache where the entry will be deleted.","$ref":"CacheId"},{"name":"request","description":"URL spec of the request.","type":"string"}]},{"name":"requestCacheNames","description":"Requests cache names.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"}],"returns":[{"name":"caches","description":"Caches for the security origin.","type":"array","items":{"$ref":"Cache"}}]},{"name":"requestCachedResponse","description":"Fetches cache entry.","parameters":[{"name":"cacheId","description":"Id of cache that contains the entry.","$ref":"CacheId"},{"name":"requestURL","description":"URL spec of the request.","type":"string"},{"name":"requestHeaders","description":"headers of the request.","type":"array","items":{"$ref":"Header"}}],"returns":[{"name":"response","description":"Response read from the cache.","$ref":"CachedResponse"}]},{"name":"requestEntries","description":"Requests data from cache.","parameters":[{"name":"cacheId","description":"ID of cache to get entries from.","$ref":"CacheId"},{"name":"skipCount","description":"Number of records to skip.","optional":true,"type":"integer"},{"name":"pageSize","description":"Number of records to fetch.","optional":true,"type":"integer"},{"name":"pathFilter","description":"If present, only return the entries containing this substring in the path","optional":true,"type":"string"}],"returns":[{"name":"cacheDataEntries","description":"Array of object store data entries.","type":"array","items":{"$ref":"DataEntry"}},{"name":"returnCount","description":"Count of returned entries from this storage. If pathFilter is empty, it\\nis the count of all entries from this storage.","type":"number"}]}]},{"domain":"Cast","description":"A domain for interacting with Cast, Presentation API, and Remote Playback API\\nfunctionalities.","experimental":true,"types":[{"id":"Sink","type":"object","properties":[{"name":"name","type":"string"},{"name":"id","type":"string"},{"name":"session","description":"Text describing the current session. Present only if there is an active\\nsession on the sink.","optional":true,"type":"string"}]}],"commands":[{"name":"enable","description":"Starts observing for sinks that can be used for tab mirroring, and if set,\\nsinks compatible with |presentationUrl| as well. When sinks are found, a\\n|sinksUpdated| event is fired.\\nAlso starts observing for issue messages. When an issue is added or removed,\\nan |issueUpdated| event is fired.","parameters":[{"name":"presentationUrl","optional":true,"type":"string"}]},{"name":"disable","description":"Stops observing for sinks and issues."},{"name":"setSinkToUse","description":"Sets a sink to be used when the web page requests the browser to choose a\\nsink via Presentation API, Remote Playback API, or Cast SDK.","parameters":[{"name":"sinkName","type":"string"}]},{"name":"startDesktopMirroring","description":"Starts mirroring the desktop to the sink.","parameters":[{"name":"sinkName","type":"string"}]},{"name":"startTabMirroring","description":"Starts mirroring the tab to the sink.","parameters":[{"name":"sinkName","type":"string"}]},{"name":"stopCasting","description":"Stops the active Cast session on the sink.","parameters":[{"name":"sinkName","type":"string"}]}],"events":[{"name":"sinksUpdated","description":"This is fired whenever the list of available sinks changes. A sink is a\\ndevice or a software surface that you can cast to.","parameters":[{"name":"sinks","type":"array","items":{"$ref":"Sink"}}]},{"name":"issueUpdated","description":"This is fired whenever the outstanding issue/error message changes.\\n|issueMessage| is empty if there is no issue.","parameters":[{"name":"issueMessage","type":"string"}]}]},{"domain":"DOM","description":"This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object\\nthat has an `id`. This `id` can be used to get additional information on the Node, resolve it into\\nthe JavaScript object wrapper, etc. It is important that client receives DOM events only for the\\nnodes that are known to the client. Backend keeps track of the nodes that were sent to the client\\nand never sends the same node twice. It is client\'s responsibility to collect information about\\nthe nodes that were sent to the client. Note that `iframe` owner elements will return\\ncorresponding document elements as their child nodes.","dependencies":["Runtime"],"types":[{"id":"NodeId","description":"Unique DOM node identifier.","type":"integer"},{"id":"BackendNodeId","description":"Unique DOM node identifier used to reference a node that may not have been pushed to the\\nfront-end.","type":"integer"},{"id":"BackendNode","description":"Backend node with a friendly name.","type":"object","properties":[{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"backendNodeId","$ref":"BackendNodeId"}]},{"id":"PseudoType","description":"Pseudo element type.","type":"string","enum":["first-line","first-letter","before","after","marker","backdrop","selection","target-text","spelling-error","grammar-error","highlight","first-line-inherited","scrollbar","scrollbar-thumb","scrollbar-button","scrollbar-track","scrollbar-track-piece","scrollbar-corner","resizer","input-list-button","view-transition","view-transition-group","view-transition-image-pair","view-transition-old","view-transition-new"]},{"id":"ShadowRootType","description":"Shadow root type.","type":"string","enum":["user-agent","open","closed"]},{"id":"CompatibilityMode","description":"Document compatibility mode.","type":"string","enum":["QuirksMode","LimitedQuirksMode","NoQuirksMode"]},{"id":"PhysicalAxes","description":"ContainerSelector physical axes","type":"string","enum":["Horizontal","Vertical","Both"]},{"id":"LogicalAxes","description":"ContainerSelector logical axes","type":"string","enum":["Inline","Block","Both"]},{"id":"Node","description":"DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes.\\nDOMNode is a base node mirror type.","type":"object","properties":[{"name":"nodeId","description":"Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend\\nwill only push node with given `id` once. It is aware of all requested nodes and will only\\nfire DOM events for nodes known to the client.","$ref":"NodeId"},{"name":"parentId","description":"The id of the parent node if any.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"The BackendNodeId for this node.","$ref":"BackendNodeId"},{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"localName","description":"`Node`\'s localName.","type":"string"},{"name":"nodeValue","description":"`Node`\'s nodeValue.","type":"string"},{"name":"childNodeCount","description":"Child count for `Container` nodes.","optional":true,"type":"integer"},{"name":"children","description":"Child nodes of this node when requested with children.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"attributes","description":"Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`.","optional":true,"type":"array","items":{"type":"string"}},{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","optional":true,"type":"string"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","optional":true,"type":"string"},{"name":"publicId","description":"`DocumentType`\'s publicId.","optional":true,"type":"string"},{"name":"systemId","description":"`DocumentType`\'s systemId.","optional":true,"type":"string"},{"name":"internalSubset","description":"`DocumentType`\'s internalSubset.","optional":true,"type":"string"},{"name":"xmlVersion","description":"`Document`\'s XML version in case of XML documents.","optional":true,"type":"string"},{"name":"name","description":"`Attr`\'s name.","optional":true,"type":"string"},{"name":"value","description":"`Attr`\'s value.","optional":true,"type":"string"},{"name":"pseudoType","description":"Pseudo element type for this node.","optional":true,"$ref":"PseudoType"},{"name":"pseudoIdentifier","description":"Pseudo element identifier for this node. Only present if there is a\\nvalid pseudoType.","optional":true,"type":"string"},{"name":"shadowRootType","description":"Shadow root type.","optional":true,"$ref":"ShadowRootType"},{"name":"frameId","description":"Frame ID for frame owner elements.","optional":true,"$ref":"Page.FrameId"},{"name":"contentDocument","description":"Content document for frame owner elements.","optional":true,"$ref":"Node"},{"name":"shadowRoots","description":"Shadow root list for given element host.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"templateContent","description":"Content document fragment for template elements.","optional":true,"$ref":"Node"},{"name":"pseudoElements","description":"Pseudo elements associated with this node.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"importedDocument","description":"Deprecated, as the HTML Imports API has been removed (crbug.com/937746).\\nThis property used to return the imported document for the HTMLImport links.\\nThe property is always undefined now.","deprecated":true,"optional":true,"$ref":"Node"},{"name":"distributedNodes","description":"Distributed nodes for given insertion point.","optional":true,"type":"array","items":{"$ref":"BackendNode"}},{"name":"isSVG","description":"Whether the node is SVG.","optional":true,"type":"boolean"},{"name":"compatibilityMode","optional":true,"$ref":"CompatibilityMode"},{"name":"assignedSlot","optional":true,"$ref":"BackendNode"}]},{"id":"RGBA","description":"A structure holding an RGBA color.","type":"object","properties":[{"name":"r","description":"The red component, in the [0-255] range.","type":"integer"},{"name":"g","description":"The green component, in the [0-255] range.","type":"integer"},{"name":"b","description":"The blue component, in the [0-255] range.","type":"integer"},{"name":"a","description":"The alpha component, in the [0-1] range (default: 1).","optional":true,"type":"number"}]},{"id":"Quad","description":"An array of quad vertices, x immediately followed by y for each point, points clock-wise.","type":"array","items":{"type":"number"}},{"id":"BoxModel","description":"Box model.","type":"object","properties":[{"name":"content","description":"Content box","$ref":"Quad"},{"name":"padding","description":"Padding box","$ref":"Quad"},{"name":"border","description":"Border box","$ref":"Quad"},{"name":"margin","description":"Margin box","$ref":"Quad"},{"name":"width","description":"Node width","type":"integer"},{"name":"height","description":"Node height","type":"integer"},{"name":"shapeOutside","description":"Shape outside coordinates","optional":true,"$ref":"ShapeOutsideInfo"}]},{"id":"ShapeOutsideInfo","description":"CSS Shape Outside details.","type":"object","properties":[{"name":"bounds","description":"Shape bounds","$ref":"Quad"},{"name":"shape","description":"Shape coordinate details","type":"array","items":{"type":"any"}},{"name":"marginShape","description":"Margin shape bounds","type":"array","items":{"type":"any"}}]},{"id":"Rect","description":"Rectangle.","type":"object","properties":[{"name":"x","description":"X coordinate","type":"number"},{"name":"y","description":"Y coordinate","type":"number"},{"name":"width","description":"Rectangle width","type":"number"},{"name":"height","description":"Rectangle height","type":"number"}]},{"id":"CSSComputedStyleProperty","type":"object","properties":[{"name":"name","description":"Computed style property name.","type":"string"},{"name":"value","description":"Computed style property value.","type":"string"}]}],"commands":[{"name":"collectClassNamesFromSubtree","description":"Collects class names for the node with given id and all of it\'s child nodes.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node to collect class names.","$ref":"NodeId"}],"returns":[{"name":"classNames","description":"Class name list.","type":"array","items":{"type":"string"}}]},{"name":"copyTo","description":"Creates a deep copy of the specified node and places it into the target container before the\\ngiven anchor.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node to copy.","$ref":"NodeId"},{"name":"targetNodeId","description":"Id of the element to drop the copy into.","$ref":"NodeId"},{"name":"insertBeforeNodeId","description":"Drop the copy before this node (if absent, the copy becomes the last child of\\n`targetNodeId`).","optional":true,"$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"Id of the node clone.","$ref":"NodeId"}]},{"name":"describeNode","description":"Describes node given its id, does not require domain to be enabled. Does not start tracking any\\nobjects, can be used for automation.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"node","description":"Node description.","$ref":"Node"}]},{"name":"scrollIntoViewIfNeeded","description":"Scrolls the specified rect of the given node into view if not already visible.\\nNote: exactly one between nodeId, backendNodeId and objectId should be passed\\nto identify the node.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"rect","description":"The rect to be scrolled into view, relative to the node\'s border box, in CSS pixels.\\nWhen omitted, center of the node will be used, similar to Element.scrollIntoView.","optional":true,"$ref":"Rect"}]},{"name":"disable","description":"Disables DOM agent for the given page."},{"name":"discardSearchResults","description":"Discards search results from the session with the given id. `getSearchResults` should no longer\\nbe called for that search.","experimental":true,"parameters":[{"name":"searchId","description":"Unique search session identifier.","type":"string"}]},{"name":"enable","description":"Enables DOM agent for the given page.","parameters":[{"name":"includeWhitespace","description":"Whether to include whitespaces in the children array of returned Nodes.","experimental":true,"optional":true,"type":"string","enum":["none","all"]}]},{"name":"focus","description":"Focuses the given element.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}]},{"name":"getAttributes","description":"Returns attributes for the specified node.","parameters":[{"name":"nodeId","description":"Id of the node to retrieve attibutes for.","$ref":"NodeId"}],"returns":[{"name":"attributes","description":"An interleaved array of node attribute names and values.","type":"array","items":{"type":"string"}}]},{"name":"getBoxModel","description":"Returns boxes for the given node.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"model","description":"Box model for the node.","$ref":"BoxModel"}]},{"name":"getContentQuads","description":"Returns quads that describe node position on the page. This method\\nmight return multiple quads for inline nodes.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"quads","description":"Quads that describe node layout relative to viewport.","type":"array","items":{"$ref":"Quad"}}]},{"name":"getDocument","description":"Returns the root DOM node (and optionally the subtree) to the caller.\\nImplicitly enables the DOM domain events for the current target.","parameters":[{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"root","description":"Resulting node.","$ref":"Node"}]},{"name":"getFlattenedDocument","description":"Returns the root DOM node (and optionally the subtree) to the caller.\\nDeprecated, as it is not designed to work well with the rest of the DOM agent.\\nUse DOMSnapshot.captureSnapshot instead.","deprecated":true,"parameters":[{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"nodes","description":"Resulting node.","type":"array","items":{"$ref":"Node"}}]},{"name":"getNodesForSubtreeByStyle","description":"Finds nodes with a given computed style in a subtree.","experimental":true,"parameters":[{"name":"nodeId","description":"Node ID pointing to the root of a subtree.","$ref":"NodeId"},{"name":"computedStyles","description":"The style to filter nodes by (includes nodes if any of properties matches).","type":"array","items":{"$ref":"CSSComputedStyleProperty"}},{"name":"pierce","description":"Whether or not iframes and shadow roots in the same target should be traversed when returning the\\nresults (default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"nodeIds","description":"Resulting nodes.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"getNodeForLocation","description":"Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is\\neither returned or not.","parameters":[{"name":"x","description":"X coordinate.","type":"integer"},{"name":"y","description":"Y coordinate.","type":"integer"},{"name":"includeUserAgentShadowDOM","description":"False to skip to the nearest non-UA shadow root ancestor (default: false).","optional":true,"type":"boolean"},{"name":"ignorePointerEventsNone","description":"Whether to ignore pointer-events: none on elements and hit test them.","optional":true,"type":"boolean"}],"returns":[{"name":"backendNodeId","description":"Resulting node.","$ref":"BackendNodeId"},{"name":"frameId","description":"Frame this node belongs to.","$ref":"Page.FrameId"},{"name":"nodeId","description":"Id of the node at given coordinates, only when enabled and requested document.","optional":true,"$ref":"NodeId"}]},{"name":"getOuterHTML","description":"Returns node\'s HTML markup.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"outerHTML","description":"Outer HTML markup.","type":"string"}]},{"name":"getRelayoutBoundary","description":"Returns the id of the nearest ancestor that is a relayout boundary.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node.","$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"Relayout boundary node id for the given node.","$ref":"NodeId"}]},{"name":"getSearchResults","description":"Returns search results from given `fromIndex` to given `toIndex` from the search with the given\\nidentifier.","experimental":true,"parameters":[{"name":"searchId","description":"Unique search session identifier.","type":"string"},{"name":"fromIndex","description":"Start index of the search result to be returned.","type":"integer"},{"name":"toIndex","description":"End index of the search result to be returned.","type":"integer"}],"returns":[{"name":"nodeIds","description":"Ids of the search result nodes.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"hideHighlight","description":"Hides any highlight.","redirect":"Overlay"},{"name":"highlightNode","description":"Highlights DOM node.","redirect":"Overlay"},{"name":"highlightRect","description":"Highlights given rectangle.","redirect":"Overlay"},{"name":"markUndoableState","description":"Marks last undoable state.","experimental":true},{"name":"moveTo","description":"Moves node into the new container, places it before the given anchor.","parameters":[{"name":"nodeId","description":"Id of the node to move.","$ref":"NodeId"},{"name":"targetNodeId","description":"Id of the element to drop the moved node into.","$ref":"NodeId"},{"name":"insertBeforeNodeId","description":"Drop node before this one (if absent, the moved node becomes the last child of\\n`targetNodeId`).","optional":true,"$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"New id of the moved node.","$ref":"NodeId"}]},{"name":"performSearch","description":"Searches for a given string in the DOM tree. Use `getSearchResults` to access search results or\\n`cancelSearch` to end this search session.","experimental":true,"parameters":[{"name":"query","description":"Plain text or query selector or XPath search query.","type":"string"},{"name":"includeUserAgentShadowDOM","description":"True to search in user agent shadow DOM.","optional":true,"type":"boolean"}],"returns":[{"name":"searchId","description":"Unique search session identifier.","type":"string"},{"name":"resultCount","description":"Number of search results.","type":"integer"}]},{"name":"pushNodeByPathToFrontend","description":"Requests that the node is sent to the caller given its path. // FIXME, use XPath","experimental":true,"parameters":[{"name":"path","description":"Path to node in the proprietary format.","type":"string"}],"returns":[{"name":"nodeId","description":"Id of the node for given path.","$ref":"NodeId"}]},{"name":"pushNodesByBackendIdsToFrontend","description":"Requests that a batch of nodes is sent to the caller given their backend node ids.","experimental":true,"parameters":[{"name":"backendNodeIds","description":"The array of backend node ids.","type":"array","items":{"$ref":"BackendNodeId"}}],"returns":[{"name":"nodeIds","description":"The array of ids of pushed nodes that correspond to the backend ids specified in\\nbackendNodeIds.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"querySelector","description":"Executes `querySelector` on a given node.","parameters":[{"name":"nodeId","description":"Id of the node to query upon.","$ref":"NodeId"},{"name":"selector","description":"Selector string.","type":"string"}],"returns":[{"name":"nodeId","description":"Query selector result.","$ref":"NodeId"}]},{"name":"querySelectorAll","description":"Executes `querySelectorAll` on a given node.","parameters":[{"name":"nodeId","description":"Id of the node to query upon.","$ref":"NodeId"},{"name":"selector","description":"Selector string.","type":"string"}],"returns":[{"name":"nodeIds","description":"Query selector result.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"getTopLayerElements","description":"Returns NodeIds of current top layer elements.\\nTop layer is rendered closest to the user within a viewport, therefore its elements always\\nappear on top of all other content.","experimental":true,"returns":[{"name":"nodeIds","description":"NodeIds of top layer elements","type":"array","items":{"$ref":"NodeId"}}]},{"name":"redo","description":"Re-does the last undone action.","experimental":true},{"name":"removeAttribute","description":"Removes attribute with given name from an element with given id.","parameters":[{"name":"nodeId","description":"Id of the element to remove attribute from.","$ref":"NodeId"},{"name":"name","description":"Name of the attribute to remove.","type":"string"}]},{"name":"removeNode","description":"Removes node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to remove.","$ref":"NodeId"}]},{"name":"requestChildNodes","description":"Requests that children of the node with given id are returned to the caller in form of\\n`setChildNodes` events where not only immediate children are retrieved, but all children down to\\nthe specified depth.","parameters":[{"name":"nodeId","description":"Id of the node to get children for.","$ref":"NodeId"},{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the sub-tree\\n(default is false).","optional":true,"type":"boolean"}]},{"name":"requestNode","description":"Requests that the node is sent to the caller given the JavaScript node object reference. All\\nnodes that form the path from the node to the root are also sent to the client as a series of\\n`setChildNodes` notifications.","parameters":[{"name":"objectId","description":"JavaScript object id to convert into node.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"nodeId","description":"Node id for given object.","$ref":"NodeId"}]},{"name":"resolveNode","description":"Resolves the JavaScript node object for a given NodeId or BackendNodeId.","parameters":[{"name":"nodeId","description":"Id of the node to resolve.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Backend identifier of the node to resolve.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"executionContextId","description":"Execution context in which to resolve the node.","optional":true,"$ref":"Runtime.ExecutionContextId"}],"returns":[{"name":"object","description":"JavaScript object wrapper for given node.","$ref":"Runtime.RemoteObject"}]},{"name":"setAttributeValue","description":"Sets attribute for an element with given id.","parameters":[{"name":"nodeId","description":"Id of the element to set attribute for.","$ref":"NodeId"},{"name":"name","description":"Attribute name.","type":"string"},{"name":"value","description":"Attribute value.","type":"string"}]},{"name":"setAttributesAsText","description":"Sets attributes on element with given id. This method is useful when user edits some existing\\nattribute value and types in several attribute name/value pairs.","parameters":[{"name":"nodeId","description":"Id of the element to set attributes for.","$ref":"NodeId"},{"name":"text","description":"Text with a number of attributes. Will parse this text using HTML parser.","type":"string"},{"name":"name","description":"Attribute name to replace with new attributes derived from text in case text parsed\\nsuccessfully.","optional":true,"type":"string"}]},{"name":"setFileInputFiles","description":"Sets files for the given file input element.","parameters":[{"name":"files","description":"Array of file paths to set.","type":"array","items":{"type":"string"}},{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}]},{"name":"setNodeStackTracesEnabled","description":"Sets if stack traces should be captured for Nodes. See `Node.getNodeStackTraces`. Default is disabled.","experimental":true,"parameters":[{"name":"enable","description":"Enable or disable.","type":"boolean"}]},{"name":"getNodeStackTraces","description":"Gets stack traces associated with a Node. As of now, only provides stack trace for Node creation.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node to get stack traces for.","$ref":"NodeId"}],"returns":[{"name":"creation","description":"Creation stack trace, if available.","optional":true,"$ref":"Runtime.StackTrace"}]},{"name":"getFileInfo","description":"Returns file information for the given\\nFile wrapper.","experimental":true,"parameters":[{"name":"objectId","description":"JavaScript object id of the node wrapper.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"path","type":"string"}]},{"name":"setInspectedNode","description":"Enables console to refer to the node with given id via $x (see Command Line API for more details\\n$x functions).","experimental":true,"parameters":[{"name":"nodeId","description":"DOM node id to be accessible by means of $x command line API.","$ref":"NodeId"}]},{"name":"setNodeName","description":"Sets node name for a node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to set name for.","$ref":"NodeId"},{"name":"name","description":"New node\'s name.","type":"string"}],"returns":[{"name":"nodeId","description":"New node\'s id.","$ref":"NodeId"}]},{"name":"setNodeValue","description":"Sets node value for a node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to set value for.","$ref":"NodeId"},{"name":"value","description":"New node\'s value.","type":"string"}]},{"name":"setOuterHTML","description":"Sets node HTML markup, returns new node id.","parameters":[{"name":"nodeId","description":"Id of the node to set markup for.","$ref":"NodeId"},{"name":"outerHTML","description":"Outer HTML markup to set.","type":"string"}]},{"name":"undo","description":"Undoes the last performed action.","experimental":true},{"name":"getFrameOwner","description":"Returns iframe node that owns iframe with the given domain.","experimental":true,"parameters":[{"name":"frameId","$ref":"Page.FrameId"}],"returns":[{"name":"backendNodeId","description":"Resulting node.","$ref":"BackendNodeId"},{"name":"nodeId","description":"Id of the node at given coordinates, only when enabled and requested document.","optional":true,"$ref":"NodeId"}]},{"name":"getContainerForNode","description":"Returns the query container of the given node based on container query\\nconditions: containerName, physical, and logical axes. If no axes are\\nprovided, the style container is returned, which is the direct parent or the\\nclosest element with a matching container-name.","experimental":true,"parameters":[{"name":"nodeId","$ref":"NodeId"},{"name":"containerName","optional":true,"type":"string"},{"name":"physicalAxes","optional":true,"$ref":"PhysicalAxes"},{"name":"logicalAxes","optional":true,"$ref":"LogicalAxes"}],"returns":[{"name":"nodeId","description":"The container node for the given node, or null if not found.","optional":true,"$ref":"NodeId"}]},{"name":"getQueryingDescendantsForContainer","description":"Returns the descendants of a container query container that have\\ncontainer queries against this container.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the container node to find querying descendants from.","$ref":"NodeId"}],"returns":[{"name":"nodeIds","description":"Descendant nodes with container queries against the given container.","type":"array","items":{"$ref":"NodeId"}}]}],"events":[{"name":"attributeModified","description":"Fired when `Element`\'s attribute is modified.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"name","description":"Attribute name.","type":"string"},{"name":"value","description":"Attribute value.","type":"string"}]},{"name":"attributeRemoved","description":"Fired when `Element`\'s attribute is removed.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"name","description":"A ttribute name.","type":"string"}]},{"name":"characterDataModified","description":"Mirrors `DOMCharacterDataModified` event.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"characterData","description":"New text value.","type":"string"}]},{"name":"childNodeCountUpdated","description":"Fired when `Container`\'s child node count has changed.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"childNodeCount","description":"New node count.","type":"integer"}]},{"name":"childNodeInserted","description":"Mirrors `DOMNodeInserted` event.","parameters":[{"name":"parentNodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"previousNodeId","description":"Id of the previous sibling.","$ref":"NodeId"},{"name":"node","description":"Inserted node data.","$ref":"Node"}]},{"name":"childNodeRemoved","description":"Mirrors `DOMNodeRemoved` event.","parameters":[{"name":"parentNodeId","description":"Parent id.","$ref":"NodeId"},{"name":"nodeId","description":"Id of the node that has been removed.","$ref":"NodeId"}]},{"name":"distributedNodesUpdated","description":"Called when distribution is changed.","experimental":true,"parameters":[{"name":"insertionPointId","description":"Insertion point where distributed nodes were updated.","$ref":"NodeId"},{"name":"distributedNodes","description":"Distributed nodes for given insertion point.","type":"array","items":{"$ref":"BackendNode"}}]},{"name":"documentUpdated","description":"Fired when `Document` has been totally updated. Node ids are no longer valid."},{"name":"inlineStyleInvalidated","description":"Fired when `Element`\'s inline style is modified via a CSS property modification.","experimental":true,"parameters":[{"name":"nodeIds","description":"Ids of the nodes for which the inline styles have been invalidated.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"pseudoElementAdded","description":"Called when a pseudo element is added to an element.","experimental":true,"parameters":[{"name":"parentId","description":"Pseudo element\'s parent element id.","$ref":"NodeId"},{"name":"pseudoElement","description":"The added pseudo element.","$ref":"Node"}]},{"name":"topLayerElementsUpdated","description":"Called when top layer elements are changed.","experimental":true},{"name":"pseudoElementRemoved","description":"Called when a pseudo element is removed from an element.","experimental":true,"parameters":[{"name":"parentId","description":"Pseudo element\'s parent element id.","$ref":"NodeId"},{"name":"pseudoElementId","description":"The removed pseudo element id.","$ref":"NodeId"}]},{"name":"setChildNodes","description":"Fired when backend wants to provide client with the missing DOM structure. This happens upon\\nmost of the calls requesting node ids.","parameters":[{"name":"parentId","description":"Parent node id to populate with children.","$ref":"NodeId"},{"name":"nodes","description":"Child nodes array.","type":"array","items":{"$ref":"Node"}}]},{"name":"shadowRootPopped","description":"Called when shadow root is popped from the element.","experimental":true,"parameters":[{"name":"hostId","description":"Host element id.","$ref":"NodeId"},{"name":"rootId","description":"Shadow root id.","$ref":"NodeId"}]},{"name":"shadowRootPushed","description":"Called when shadow root is pushed into the element.","experimental":true,"parameters":[{"name":"hostId","description":"Host element id.","$ref":"NodeId"},{"name":"root","description":"Shadow root.","$ref":"Node"}]}]},{"domain":"DOMDebugger","description":"DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript\\nexecution will stop on these operations as if there was a regular breakpoint set.","dependencies":["DOM","Debugger","Runtime"],"types":[{"id":"DOMBreakpointType","description":"DOM breakpoint type.","type":"string","enum":["subtree-modified","attribute-modified","node-removed"]},{"id":"CSPViolationType","description":"CSP Violation type.","experimental":true,"type":"string","enum":["trustedtype-sink-violation","trustedtype-policy-violation"]},{"id":"EventListener","description":"Object event listener.","type":"object","properties":[{"name":"type","description":"`EventListener`\'s type.","type":"string"},{"name":"useCapture","description":"`EventListener`\'s useCapture.","type":"boolean"},{"name":"passive","description":"`EventListener`\'s passive flag.","type":"boolean"},{"name":"once","description":"`EventListener`\'s once flag.","type":"boolean"},{"name":"scriptId","description":"Script id of the handler code.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","type":"integer"},{"name":"handler","description":"Event handler function value.","optional":true,"$ref":"Runtime.RemoteObject"},{"name":"originalHandler","description":"Event original handler function value.","optional":true,"$ref":"Runtime.RemoteObject"},{"name":"backendNodeId","description":"Node the listener is added to (if any).","optional":true,"$ref":"DOM.BackendNodeId"}]}],"commands":[{"name":"getEventListeners","description":"Returns event listeners of the given object.","parameters":[{"name":"objectId","description":"Identifier of the object to return listeners for.","$ref":"Runtime.RemoteObjectId"},{"name":"depth","description":"The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false). Reports listeners for all contexts if pierce is enabled.","optional":true,"type":"boolean"}],"returns":[{"name":"listeners","description":"Array of relevant listeners.","type":"array","items":{"$ref":"EventListener"}}]},{"name":"removeDOMBreakpoint","description":"Removes DOM breakpoint that was set using `setDOMBreakpoint`.","parameters":[{"name":"nodeId","description":"Identifier of the node to remove breakpoint from.","$ref":"DOM.NodeId"},{"name":"type","description":"Type of the breakpoint to remove.","$ref":"DOMBreakpointType"}]},{"name":"removeEventListenerBreakpoint","description":"Removes breakpoint on particular DOM event.","parameters":[{"name":"eventName","description":"Event name.","type":"string"},{"name":"targetName","description":"EventTarget interface name.","experimental":true,"optional":true,"type":"string"}]},{"name":"removeInstrumentationBreakpoint","description":"Removes breakpoint on particular native event.","experimental":true,"parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]},{"name":"removeXHRBreakpoint","description":"Removes breakpoint from XMLHttpRequest.","parameters":[{"name":"url","description":"Resource URL substring.","type":"string"}]},{"name":"setBreakOnCSPViolation","description":"Sets breakpoint on particular CSP violations.","experimental":true,"parameters":[{"name":"violationTypes","description":"CSP Violations to stop upon.","type":"array","items":{"$ref":"CSPViolationType"}}]},{"name":"setDOMBreakpoint","description":"Sets breakpoint on particular operation with DOM.","parameters":[{"name":"nodeId","description":"Identifier of the node to set breakpoint on.","$ref":"DOM.NodeId"},{"name":"type","description":"Type of the operation to stop upon.","$ref":"DOMBreakpointType"}]},{"name":"setEventListenerBreakpoint","description":"Sets breakpoint on particular DOM event.","parameters":[{"name":"eventName","description":"DOM Event name to stop on (any DOM event will do).","type":"string"},{"name":"targetName","description":"EventTarget interface name to stop on. If equal to `\\"*\\"` or not provided, will stop on any\\nEventTarget.","experimental":true,"optional":true,"type":"string"}]},{"name":"setInstrumentationBreakpoint","description":"Sets breakpoint on particular native event.","experimental":true,"parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]},{"name":"setXHRBreakpoint","description":"Sets breakpoint on XMLHttpRequest.","parameters":[{"name":"url","description":"Resource URL substring. All XHRs having this substring in the URL will get stopped upon.","type":"string"}]}]},{"domain":"EventBreakpoints","description":"EventBreakpoints permits setting breakpoints on particular operations and\\nevents in targets that run JavaScript but do not have a DOM.\\nJavaScript execution will stop on these operations as if there was a regular\\nbreakpoint set.","experimental":true,"commands":[{"name":"setInstrumentationBreakpoint","description":"Sets breakpoint on particular native event.","parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]},{"name":"removeInstrumentationBreakpoint","description":"Removes breakpoint on particular native event.","parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]}]},{"domain":"DOMSnapshot","description":"This domain facilitates obtaining document snapshots with DOM, layout, and style information.","experimental":true,"dependencies":["CSS","DOM","DOMDebugger","Page"],"types":[{"id":"DOMNode","description":"A Node in the DOM tree.","type":"object","properties":[{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"nodeValue","description":"`Node`\'s nodeValue.","type":"string"},{"name":"textValue","description":"Only set for textarea elements, contains the text value.","optional":true,"type":"string"},{"name":"inputValue","description":"Only set for input elements, contains the input\'s associated text value.","optional":true,"type":"string"},{"name":"inputChecked","description":"Only set for radio and checkbox input elements, indicates if the element has been checked","optional":true,"type":"boolean"},{"name":"optionSelected","description":"Only set for option elements, indicates if the element has been selected","optional":true,"type":"boolean"},{"name":"backendNodeId","description":"`Node`\'s id, corresponds to DOM.Node.backendNodeId.","$ref":"DOM.BackendNodeId"},{"name":"childNodeIndexes","description":"The indexes of the node\'s child nodes in the `domNodes` array returned by `getSnapshot`, if\\nany.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"attributes","description":"Attributes of an `Element` node.","optional":true,"type":"array","items":{"$ref":"NameValue"}},{"name":"pseudoElementIndexes","description":"Indexes of pseudo elements associated with this node in the `domNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"layoutNodeIndex","description":"The index of the node\'s related layout tree node in the `layoutTreeNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"integer"},{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","optional":true,"type":"string"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","optional":true,"type":"string"},{"name":"contentLanguage","description":"Only set for documents, contains the document\'s content language.","optional":true,"type":"string"},{"name":"documentEncoding","description":"Only set for documents, contains the document\'s character set encoding.","optional":true,"type":"string"},{"name":"publicId","description":"`DocumentType` node\'s publicId.","optional":true,"type":"string"},{"name":"systemId","description":"`DocumentType` node\'s systemId.","optional":true,"type":"string"},{"name":"frameId","description":"Frame ID for frame owner elements and also for the document node.","optional":true,"$ref":"Page.FrameId"},{"name":"contentDocumentIndex","description":"The index of a frame owner element\'s content document in the `domNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"integer"},{"name":"pseudoType","description":"Type of a pseudo element node.","optional":true,"$ref":"DOM.PseudoType"},{"name":"shadowRootType","description":"Shadow root type.","optional":true,"$ref":"DOM.ShadowRootType"},{"name":"isClickable","description":"Whether this DOM node responds to mouse clicks. This includes nodes that have had click\\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\\nclicked.","optional":true,"type":"boolean"},{"name":"eventListeners","description":"Details of the node\'s event listeners, if any.","optional":true,"type":"array","items":{"$ref":"DOMDebugger.EventListener"}},{"name":"currentSourceURL","description":"The selected url for nodes with a srcset attribute.","optional":true,"type":"string"},{"name":"originURL","description":"The url of the script (if any) that generates this node.","optional":true,"type":"string"},{"name":"scrollOffsetX","description":"Scroll offsets, set when this node is a Document.","optional":true,"type":"number"},{"name":"scrollOffsetY","optional":true,"type":"number"}]},{"id":"InlineTextBox","description":"Details of post layout rendered text positions. The exact layout should not be regarded as\\nstable and may change between versions.","type":"object","properties":[{"name":"boundingBox","description":"The bounding box in document coordinates. Note that scroll offset of the document is ignored.","$ref":"DOM.Rect"},{"name":"startCharacterIndex","description":"The starting index in characters, for this post layout textbox substring. Characters that\\nwould be represented as a surrogate pair in UTF-16 have length 2.","type":"integer"},{"name":"numCharacters","description":"The number of characters in this post layout textbox substring. Characters that would be\\nrepresented as a surrogate pair in UTF-16 have length 2.","type":"integer"}]},{"id":"LayoutTreeNode","description":"Details of an element in the DOM tree with a LayoutObject.","type":"object","properties":[{"name":"domNodeIndex","description":"The index of the related DOM node in the `domNodes` array returned by `getSnapshot`.","type":"integer"},{"name":"boundingBox","description":"The bounding box in document coordinates. Note that scroll offset of the document is ignored.","$ref":"DOM.Rect"},{"name":"layoutText","description":"Contents of the LayoutText, if any.","optional":true,"type":"string"},{"name":"inlineTextNodes","description":"The post-layout inline text nodes, if any.","optional":true,"type":"array","items":{"$ref":"InlineTextBox"}},{"name":"styleIndex","description":"Index into the `computedStyles` array returned by `getSnapshot`.","optional":true,"type":"integer"},{"name":"paintOrder","description":"Global paint order index, which is determined by the stacking order of the nodes. Nodes\\nthat are painted together will have the same index. Only provided if includePaintOrder in\\ngetSnapshot was true.","optional":true,"type":"integer"},{"name":"isStackingContext","description":"Set to true to indicate the element begins a new stacking context.","optional":true,"type":"boolean"}]},{"id":"ComputedStyle","description":"A subset of the full ComputedStyle as defined by the request whitelist.","type":"object","properties":[{"name":"properties","description":"Name/value pairs of computed style properties.","type":"array","items":{"$ref":"NameValue"}}]},{"id":"NameValue","description":"A name/value pair.","type":"object","properties":[{"name":"name","description":"Attribute/property name.","type":"string"},{"name":"value","description":"Attribute/property value.","type":"string"}]},{"id":"StringIndex","description":"Index of the string in the strings table.","type":"integer"},{"id":"ArrayOfStrings","description":"Index of the string in the strings table.","type":"array","items":{"$ref":"StringIndex"}},{"id":"RareStringData","description":"Data that is only present on rare nodes.","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}},{"name":"value","type":"array","items":{"$ref":"StringIndex"}}]},{"id":"RareBooleanData","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}}]},{"id":"RareIntegerData","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}},{"name":"value","type":"array","items":{"type":"integer"}}]},{"id":"Rectangle","type":"array","items":{"type":"number"}},{"id":"DocumentSnapshot","description":"Document snapshot.","type":"object","properties":[{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","$ref":"StringIndex"},{"name":"title","description":"Document title.","$ref":"StringIndex"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","$ref":"StringIndex"},{"name":"contentLanguage","description":"Contains the document\'s content language.","$ref":"StringIndex"},{"name":"encodingName","description":"Contains the document\'s character set encoding.","$ref":"StringIndex"},{"name":"publicId","description":"`DocumentType` node\'s publicId.","$ref":"StringIndex"},{"name":"systemId","description":"`DocumentType` node\'s systemId.","$ref":"StringIndex"},{"name":"frameId","description":"Frame ID for frame owner elements and also for the document node.","$ref":"StringIndex"},{"name":"nodes","description":"A table with dom nodes.","$ref":"NodeTreeSnapshot"},{"name":"layout","description":"The nodes in the layout tree.","$ref":"LayoutTreeSnapshot"},{"name":"textBoxes","description":"The post-layout inline text nodes.","$ref":"TextBoxSnapshot"},{"name":"scrollOffsetX","description":"Horizontal scroll offset.","optional":true,"type":"number"},{"name":"scrollOffsetY","description":"Vertical scroll offset.","optional":true,"type":"number"},{"name":"contentWidth","description":"Document content width.","optional":true,"type":"number"},{"name":"contentHeight","description":"Document content height.","optional":true,"type":"number"}]},{"id":"NodeTreeSnapshot","description":"Table containing nodes.","type":"object","properties":[{"name":"parentIndex","description":"Parent node index.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"nodeType","description":"`Node`\'s nodeType.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"shadowRootType","description":"Type of the shadow root the `Node` is in. String values are equal to the `ShadowRootType` enum.","optional":true,"$ref":"RareStringData"},{"name":"nodeName","description":"`Node`\'s nodeName.","optional":true,"type":"array","items":{"$ref":"StringIndex"}},{"name":"nodeValue","description":"`Node`\'s nodeValue.","optional":true,"type":"array","items":{"$ref":"StringIndex"}},{"name":"backendNodeId","description":"`Node`\'s id, corresponds to DOM.Node.backendNodeId.","optional":true,"type":"array","items":{"$ref":"DOM.BackendNodeId"}},{"name":"attributes","description":"Attributes of an `Element` node. Flatten name, value pairs.","optional":true,"type":"array","items":{"$ref":"ArrayOfStrings"}},{"name":"textValue","description":"Only set for textarea elements, contains the text value.","optional":true,"$ref":"RareStringData"},{"name":"inputValue","description":"Only set for input elements, contains the input\'s associated text value.","optional":true,"$ref":"RareStringData"},{"name":"inputChecked","description":"Only set for radio and checkbox input elements, indicates if the element has been checked","optional":true,"$ref":"RareBooleanData"},{"name":"optionSelected","description":"Only set for option elements, indicates if the element has been selected","optional":true,"$ref":"RareBooleanData"},{"name":"contentDocumentIndex","description":"The index of the document in the list of the snapshot documents.","optional":true,"$ref":"RareIntegerData"},{"name":"pseudoType","description":"Type of a pseudo element node.","optional":true,"$ref":"RareStringData"},{"name":"pseudoIdentifier","description":"Pseudo element identifier for this node. Only present if there is a\\nvalid pseudoType.","optional":true,"$ref":"RareStringData"},{"name":"isClickable","description":"Whether this DOM node responds to mouse clicks. This includes nodes that have had click\\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\\nclicked.","optional":true,"$ref":"RareBooleanData"},{"name":"currentSourceURL","description":"The selected url for nodes with a srcset attribute.","optional":true,"$ref":"RareStringData"},{"name":"originURL","description":"The url of the script (if any) that generates this node.","optional":true,"$ref":"RareStringData"}]},{"id":"LayoutTreeSnapshot","description":"Table of details of an element in the DOM tree with a LayoutObject.","type":"object","properties":[{"name":"nodeIndex","description":"Index of the corresponding node in the `NodeTreeSnapshot` array returned by `captureSnapshot`.","type":"array","items":{"type":"integer"}},{"name":"styles","description":"Array of indexes specifying computed style strings, filtered according to the `computedStyles` parameter passed to `captureSnapshot`.","type":"array","items":{"$ref":"ArrayOfStrings"}},{"name":"bounds","description":"The absolute position bounding box.","type":"array","items":{"$ref":"Rectangle"}},{"name":"text","description":"Contents of the LayoutText, if any.","type":"array","items":{"$ref":"StringIndex"}},{"name":"stackingContexts","description":"Stacking context information.","$ref":"RareBooleanData"},{"name":"paintOrders","description":"Global paint order index, which is determined by the stacking order of the nodes. Nodes\\nthat are painted together will have the same index. Only provided if includePaintOrder in\\ncaptureSnapshot was true.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"offsetRects","description":"The offset rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}},{"name":"scrollRects","description":"The scroll rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}},{"name":"clientRects","description":"The client rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}},{"name":"blendedBackgroundColors","description":"The list of background colors that are blended with colors of overlapping elements.","experimental":true,"optional":true,"type":"array","items":{"$ref":"StringIndex"}},{"name":"textColorOpacities","description":"The list of computed text opacities.","experimental":true,"optional":true,"type":"array","items":{"type":"number"}}]},{"id":"TextBoxSnapshot","description":"Table of details of the post layout rendered text positions. The exact layout should not be regarded as\\nstable and may change between versions.","type":"object","properties":[{"name":"layoutIndex","description":"Index of the layout tree node that owns this box collection.","type":"array","items":{"type":"integer"}},{"name":"bounds","description":"The absolute position bounding box.","type":"array","items":{"$ref":"Rectangle"}},{"name":"start","description":"The starting index in characters, for this post layout textbox substring. Characters that\\nwould be represented as a surrogate pair in UTF-16 have length 2.","type":"array","items":{"type":"integer"}},{"name":"length","description":"The number of characters in this post layout textbox substring. Characters that would be\\nrepresented as a surrogate pair in UTF-16 have length 2.","type":"array","items":{"type":"integer"}}]}],"commands":[{"name":"disable","description":"Disables DOM snapshot agent for the given page."},{"name":"enable","description":"Enables DOM snapshot agent for the given page."},{"name":"getSnapshot","description":"Returns a document snapshot, including the full DOM tree of the root node (including iframes,\\ntemplate contents, and imported documents) in a flattened array, as well as layout and\\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\\nflattened.","deprecated":true,"parameters":[{"name":"computedStyleWhitelist","description":"Whitelist of computed styles to return.","type":"array","items":{"type":"string"}},{"name":"includeEventListeners","description":"Whether or not to retrieve details of DOM listeners (default false).","optional":true,"type":"boolean"},{"name":"includePaintOrder","description":"Whether to determine and include the paint order index of LayoutTreeNodes (default false).","optional":true,"type":"boolean"},{"name":"includeUserAgentShadowTree","description":"Whether to include UA shadow tree in the snapshot (default false).","optional":true,"type":"boolean"}],"returns":[{"name":"domNodes","description":"The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.","type":"array","items":{"$ref":"DOMNode"}},{"name":"layoutTreeNodes","description":"The nodes in the layout tree.","type":"array","items":{"$ref":"LayoutTreeNode"}},{"name":"computedStyles","description":"Whitelisted ComputedStyle properties for each node in the layout tree.","type":"array","items":{"$ref":"ComputedStyle"}}]},{"name":"captureSnapshot","description":"Returns a document snapshot, including the full DOM tree of the root node (including iframes,\\ntemplate contents, and imported documents) in a flattened array, as well as layout and\\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\\nflattened.","parameters":[{"name":"computedStyles","description":"Whitelist of computed styles to return.","type":"array","items":{"type":"string"}},{"name":"includePaintOrder","description":"Whether to include layout object paint orders into the snapshot.","optional":true,"type":"boolean"},{"name":"includeDOMRects","description":"Whether to include DOM rectangles (offsetRects, clientRects, scrollRects) into the snapshot","optional":true,"type":"boolean"},{"name":"includeBlendedBackgroundColors","description":"Whether to include blended background colors in the snapshot (default: false).\\nBlended background color is achieved by blending background colors of all elements\\nthat overlap with the current element.","experimental":true,"optional":true,"type":"boolean"},{"name":"includeTextColorOpacities","description":"Whether to include text color opacity in the snapshot (default: false).\\nAn element might have the opacity property set that affects the text color of the element.\\nThe final text color opacity is computed based on the opacity of all overlapping elements.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"documents","description":"The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.","type":"array","items":{"$ref":"DocumentSnapshot"}},{"name":"strings","description":"Shared string table that all string properties refer to with indexes.","type":"array","items":{"type":"string"}}]}]},{"domain":"DOMStorage","description":"Query and modify DOM storage.","experimental":true,"types":[{"id":"SerializedStorageKey","type":"string"},{"id":"StorageId","description":"DOM Storage identifier.","type":"object","properties":[{"name":"securityOrigin","description":"Security origin for the storage.","optional":true,"type":"string"},{"name":"storageKey","description":"Represents a key by which DOM Storage keys its CachedStorageAreas","optional":true,"$ref":"SerializedStorageKey"},{"name":"isLocalStorage","description":"Whether the storage is local storage (not session storage).","type":"boolean"}]},{"id":"Item","description":"DOM Storage item.","type":"array","items":{"type":"string"}}],"commands":[{"name":"clear","parameters":[{"name":"storageId","$ref":"StorageId"}]},{"name":"disable","description":"Disables storage tracking, prevents storage events from being sent to the client."},{"name":"enable","description":"Enables storage tracking, storage events will now be delivered to the client."},{"name":"getDOMStorageItems","parameters":[{"name":"storageId","$ref":"StorageId"}],"returns":[{"name":"entries","type":"array","items":{"$ref":"Item"}}]},{"name":"removeDOMStorageItem","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"}]},{"name":"setDOMStorageItem","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"value","type":"string"}]}],"events":[{"name":"domStorageItemAdded","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"newValue","type":"string"}]},{"name":"domStorageItemRemoved","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"}]},{"name":"domStorageItemUpdated","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"oldValue","type":"string"},{"name":"newValue","type":"string"}]},{"name":"domStorageItemsCleared","parameters":[{"name":"storageId","$ref":"StorageId"}]}]},{"domain":"Database","experimental":true,"types":[{"id":"DatabaseId","description":"Unique identifier of Database object.","type":"string"},{"id":"Database","description":"Database object.","type":"object","properties":[{"name":"id","description":"Database ID.","$ref":"DatabaseId"},{"name":"domain","description":"Database domain.","type":"string"},{"name":"name","description":"Database name.","type":"string"},{"name":"version","description":"Database version.","type":"string"}]},{"id":"Error","description":"Database error.","type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"code","description":"Error code.","type":"integer"}]}],"commands":[{"name":"disable","description":"Disables database tracking, prevents database events from being sent to the client."},{"name":"enable","description":"Enables database tracking, database events will now be delivered to the client."},{"name":"executeSQL","parameters":[{"name":"databaseId","$ref":"DatabaseId"},{"name":"query","type":"string"}],"returns":[{"name":"columnNames","optional":true,"type":"array","items":{"type":"string"}},{"name":"values","optional":true,"type":"array","items":{"type":"any"}},{"name":"sqlError","optional":true,"$ref":"Error"}]},{"name":"getDatabaseTableNames","parameters":[{"name":"databaseId","$ref":"DatabaseId"}],"returns":[{"name":"tableNames","type":"array","items":{"type":"string"}}]}],"events":[{"name":"addDatabase","parameters":[{"name":"database","$ref":"Database"}]}]},{"domain":"DeviceOrientation","experimental":true,"commands":[{"name":"clearDeviceOrientationOverride","description":"Clears the overridden Device Orientation."},{"name":"setDeviceOrientationOverride","description":"Overrides the Device Orientation.","parameters":[{"name":"alpha","description":"Mock alpha","type":"number"},{"name":"beta","description":"Mock beta","type":"number"},{"name":"gamma","description":"Mock gamma","type":"number"}]}]},{"domain":"Emulation","description":"This domain emulates different environments for the page.","dependencies":["DOM","Page","Runtime"],"types":[{"id":"ScreenOrientation","description":"Screen orientation.","type":"object","properties":[{"name":"type","description":"Orientation type.","type":"string","enum":["portraitPrimary","portraitSecondary","landscapePrimary","landscapeSecondary"]},{"name":"angle","description":"Orientation angle.","type":"integer"}]},{"id":"DisplayFeature","type":"object","properties":[{"name":"orientation","description":"Orientation of a display feature in relation to screen","type":"string","enum":["vertical","horizontal"]},{"name":"offset","description":"The offset from the screen origin in either the x (for vertical\\norientation) or y (for horizontal orientation) direction.","type":"integer"},{"name":"maskLength","description":"A display feature may mask content such that it is not physically\\ndisplayed - this length along with the offset describes this area.\\nA display feature that only splits content will have a 0 mask_length.","type":"integer"}]},{"id":"MediaFeature","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"VirtualTimePolicy","description":"advance: If the scheduler runs out of immediate work, the virtual time base may fast forward to\\nallow the next delayed task (if any) to run; pause: The virtual time base may not advance;\\npauseIfNetworkFetchesPending: The virtual time base may not advance if there are any pending\\nresource fetches.","experimental":true,"type":"string","enum":["advance","pause","pauseIfNetworkFetchesPending"]},{"id":"UserAgentBrandVersion","description":"Used to specify User Agent Cient Hints to emulate. See https://wicg.github.io/ua-client-hints","experimental":true,"type":"object","properties":[{"name":"brand","type":"string"},{"name":"version","type":"string"}]},{"id":"UserAgentMetadata","description":"Used to specify User Agent Cient Hints to emulate. See https://wicg.github.io/ua-client-hints\\nMissing optional values will be filled in by the target with what it would normally use.","experimental":true,"type":"object","properties":[{"name":"brands","description":"Brands appearing in Sec-CH-UA.","optional":true,"type":"array","items":{"$ref":"UserAgentBrandVersion"}},{"name":"fullVersionList","description":"Brands appearing in Sec-CH-UA-Full-Version-List.","optional":true,"type":"array","items":{"$ref":"UserAgentBrandVersion"}},{"name":"fullVersion","deprecated":true,"optional":true,"type":"string"},{"name":"platform","type":"string"},{"name":"platformVersion","type":"string"},{"name":"architecture","type":"string"},{"name":"model","type":"string"},{"name":"mobile","type":"boolean"},{"name":"bitness","optional":true,"type":"string"},{"name":"wow64","optional":true,"type":"boolean"}]},{"id":"DisabledImageType","description":"Enum of image types that can be disabled.","experimental":true,"type":"string","enum":["avif","webp"]}],"commands":[{"name":"canEmulate","description":"Tells whether emulation is supported.","returns":[{"name":"result","description":"True if emulation is supported.","type":"boolean"}]},{"name":"clearDeviceMetricsOverride","description":"Clears the overridden device metrics."},{"name":"clearGeolocationOverride","description":"Clears the overridden Geolocation Position and Error."},{"name":"resetPageScaleFactor","description":"Requests that page scale factor is reset to initial values.","experimental":true},{"name":"setFocusEmulationEnabled","description":"Enables or disables simulating a focused and active page.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to enable to disable focus emulation.","type":"boolean"}]},{"name":"setAutoDarkModeOverride","description":"Automatically render all web contents using a dark theme.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to enable or disable automatic dark mode.\\nIf not specified, any existing override will be cleared.","optional":true,"type":"boolean"}]},{"name":"setCPUThrottlingRate","description":"Enables CPU throttling to emulate slow CPUs.","experimental":true,"parameters":[{"name":"rate","description":"Throttling rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc).","type":"number"}]},{"name":"setDefaultBackgroundColorOverride","description":"Sets or clears an override of the default background color of the frame. This override is used\\nif the content does not specify one.","parameters":[{"name":"color","description":"RGBA of the default background color. If not specified, any existing override will be\\ncleared.","optional":true,"$ref":"DOM.RGBA"}]},{"name":"setDeviceMetricsOverride","description":"Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\\nwindow.innerWidth, window.innerHeight, and \\"device-width\\"/\\"device-height\\"-related CSS media\\nquery results).","parameters":[{"name":"width","description":"Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"height","description":"Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"deviceScaleFactor","description":"Overriding device scale factor value. 0 disables the override.","type":"number"},{"name":"mobile","description":"Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\\nautosizing and more.","type":"boolean"},{"name":"scale","description":"Scale to apply to resulting view image.","experimental":true,"optional":true,"type":"number"},{"name":"screenWidth","description":"Overriding screen width value in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"screenHeight","description":"Overriding screen height value in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"positionX","description":"Overriding view X position on screen in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"positionY","description":"Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"dontSetVisibleSize","description":"Do not set visible view size, rely upon explicit setVisibleSize call.","experimental":true,"optional":true,"type":"boolean"},{"name":"screenOrientation","description":"Screen orientation override.","optional":true,"$ref":"ScreenOrientation"},{"name":"viewport","description":"If set, the visible area of the page will be overridden to this viewport. This viewport\\nchange is not observed by the page, e.g. viewport-relative elements do not change positions.","experimental":true,"optional":true,"$ref":"Page.Viewport"},{"name":"displayFeature","description":"If set, the display feature of a multi-segment screen. If not set, multi-segment support\\nis turned-off.","experimental":true,"optional":true,"$ref":"DisplayFeature"}]},{"name":"setScrollbarsHidden","experimental":true,"parameters":[{"name":"hidden","description":"Whether scrollbars should be always hidden.","type":"boolean"}]},{"name":"setDocumentCookieDisabled","experimental":true,"parameters":[{"name":"disabled","description":"Whether document.coookie API should be disabled.","type":"boolean"}]},{"name":"setEmitTouchEventsForMouse","experimental":true,"parameters":[{"name":"enabled","description":"Whether touch emulation based on mouse input should be enabled.","type":"boolean"},{"name":"configuration","description":"Touch/gesture events configuration. Default: current platform.","optional":true,"type":"string","enum":["mobile","desktop"]}]},{"name":"setEmulatedMedia","description":"Emulates the given media type or media feature for CSS media queries.","parameters":[{"name":"media","description":"Media type to emulate. Empty string disables the override.","optional":true,"type":"string"},{"name":"features","description":"Media features to emulate.","optional":true,"type":"array","items":{"$ref":"MediaFeature"}}]},{"name":"setEmulatedVisionDeficiency","description":"Emulates the given vision deficiency.","experimental":true,"parameters":[{"name":"type","description":"Vision deficiency to emulate. Order: best-effort emulations come first, followed by any\\nphysiologically accurate emulations for medically recognized color vision deficiencies.","type":"string","enum":["none","blurredVision","reducedContrast","achromatopsia","deuteranopia","protanopia","tritanopia"]}]},{"name":"setGeolocationOverride","description":"Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\\nunavailable.","parameters":[{"name":"latitude","description":"Mock latitude","optional":true,"type":"number"},{"name":"longitude","description":"Mock longitude","optional":true,"type":"number"},{"name":"accuracy","description":"Mock accuracy","optional":true,"type":"number"}]},{"name":"setIdleOverride","description":"Overrides the Idle state.","experimental":true,"parameters":[{"name":"isUserActive","description":"Mock isUserActive","type":"boolean"},{"name":"isScreenUnlocked","description":"Mock isScreenUnlocked","type":"boolean"}]},{"name":"clearIdleOverride","description":"Clears Idle state overrides.","experimental":true},{"name":"setNavigatorOverrides","description":"Overrides value returned by the javascript navigator object.","experimental":true,"deprecated":true,"parameters":[{"name":"platform","description":"The platform navigator.platform should return.","type":"string"}]},{"name":"setPageScaleFactor","description":"Sets a specified page scale factor.","experimental":true,"parameters":[{"name":"pageScaleFactor","description":"Page scale factor.","type":"number"}]},{"name":"setScriptExecutionDisabled","description":"Switches script execution in the page.","parameters":[{"name":"value","description":"Whether script execution should be disabled in the page.","type":"boolean"}]},{"name":"setTouchEmulationEnabled","description":"Enables touch on platforms which do not support them.","parameters":[{"name":"enabled","description":"Whether the touch event emulation should be enabled.","type":"boolean"},{"name":"maxTouchPoints","description":"Maximum touch points supported. Defaults to one.","optional":true,"type":"integer"}]},{"name":"setVirtualTimePolicy","description":"Turns on virtual time for all frames (replacing real-time with a synthetic time source) and sets\\nthe current virtual time policy. Note this supersedes any previous time budget.","experimental":true,"parameters":[{"name":"policy","$ref":"VirtualTimePolicy"},{"name":"budget","description":"If set, after this many virtual milliseconds have elapsed virtual time will be paused and a\\nvirtualTimeBudgetExpired event is sent.","optional":true,"type":"number"},{"name":"maxVirtualTimeTaskStarvationCount","description":"If set this specifies the maximum number of tasks that can be run before virtual is forced\\nforwards to prevent deadlock.","optional":true,"type":"integer"},{"name":"initialVirtualTime","description":"If set, base::Time::Now will be overridden to initially return this value.","optional":true,"$ref":"Network.TimeSinceEpoch"}],"returns":[{"name":"virtualTimeTicksBase","description":"Absolute timestamp at which virtual time was first enabled (up time in milliseconds).","type":"number"}]},{"name":"setLocaleOverride","description":"Overrides default host system locale with the specified one.","experimental":true,"parameters":[{"name":"locale","description":"ICU style C locale (e.g. \\"en_US\\"). If not specified or empty, disables the override and\\nrestores default host system locale.","optional":true,"type":"string"}]},{"name":"setTimezoneOverride","description":"Overrides default host system timezone with the specified one.","experimental":true,"parameters":[{"name":"timezoneId","description":"The timezone identifier. If empty, disables the override and\\nrestores default host system timezone.","type":"string"}]},{"name":"setVisibleSize","description":"Resizes the frame/viewport of the page. Note that this does not affect the frame\'s container\\n(e.g. browser window). Can be used to produce screenshots of the specified size. Not supported\\non Android.","experimental":true,"deprecated":true,"parameters":[{"name":"width","description":"Frame width (DIP).","type":"integer"},{"name":"height","description":"Frame height (DIP).","type":"integer"}]},{"name":"setDisabledImageTypes","experimental":true,"parameters":[{"name":"imageTypes","description":"Image types to disable.","type":"array","items":{"$ref":"DisabledImageType"}}]},{"name":"setHardwareConcurrencyOverride","experimental":true,"parameters":[{"name":"hardwareConcurrency","description":"Hardware concurrency to report","type":"integer"}]},{"name":"setUserAgentOverride","description":"Allows overriding user agent with the given string.","parameters":[{"name":"userAgent","description":"User agent to use.","type":"string"},{"name":"acceptLanguage","description":"Browser langugage to emulate.","optional":true,"type":"string"},{"name":"platform","description":"The platform navigator.platform should return.","optional":true,"type":"string"},{"name":"userAgentMetadata","description":"To be sent in Sec-CH-UA-* headers and returned in navigator.userAgentData","experimental":true,"optional":true,"$ref":"UserAgentMetadata"}]},{"name":"setAutomationOverride","description":"Allows overriding the automation flag.","experimental":true,"parameters":[{"name":"enabled","description":"Whether the override should be enabled.","type":"boolean"}]}],"events":[{"name":"virtualTimeBudgetExpired","description":"Notification sent after the virtual time budget for the current VirtualTimePolicy has run out.","experimental":true}]},{"domain":"HeadlessExperimental","description":"This domain provides experimental commands only supported in headless mode.","experimental":true,"dependencies":["Page","Runtime"],"types":[{"id":"ScreenshotParams","description":"Encoding options for a screenshot.","type":"object","properties":[{"name":"format","description":"Image compression format (defaults to png).","optional":true,"type":"string","enum":["jpeg","png","webp"]},{"name":"quality","description":"Compression quality from range [0..100] (jpeg and webp only).","optional":true,"type":"integer"},{"name":"optimizeForSpeed","description":"Optimize image encoding for speed, not for resulting size (defaults to false)","optional":true,"type":"boolean"}]}],"commands":[{"name":"beginFrame","description":"Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a\\nscreenshot from the resulting frame. Requires that the target was created with enabled\\nBeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also\\nhttps://goo.gle/chrome-headless-rendering for more background.","parameters":[{"name":"frameTimeTicks","description":"Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set,\\nthe current time will be used.","optional":true,"type":"number"},{"name":"interval","description":"The interval between BeginFrames that is reported to the compositor, in milliseconds.\\nDefaults to a 60 frames/second interval, i.e. about 16.666 milliseconds.","optional":true,"type":"number"},{"name":"noDisplayUpdates","description":"Whether updates should not be committed and drawn onto the display. False by default. If\\ntrue, only side effects of the BeginFrame will be run, such as layout and animations, but\\nany visual updates may not be visible on the display or in screenshots.","optional":true,"type":"boolean"},{"name":"screenshot","description":"If set, a screenshot of the frame will be captured and returned in the response. Otherwise,\\nno screenshot will be captured. Note that capturing a screenshot can fail, for example,\\nduring renderer initialization. In such a case, no screenshot data will be returned.","optional":true,"$ref":"ScreenshotParams"}],"returns":[{"name":"hasDamage","description":"Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the\\ndisplay. Reported for diagnostic uses, may be removed in the future.","type":"boolean"},{"name":"screenshotData","description":"Base64-encoded image data of the screenshot, if one was requested and successfully taken. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"}]},{"name":"disable","description":"Disables headless events for the target.","deprecated":true},{"name":"enable","description":"Enables headless events for the target.","deprecated":true}]},{"domain":"IO","description":"Input/Output operations for streams produced by DevTools.","types":[{"id":"StreamHandle","description":"This is either obtained from another method or specified as `blob:<uuid>` where\\n`<uuid>` is an UUID of a Blob.","type":"string"}],"commands":[{"name":"close","description":"Close the stream, discard any temporary backing storage.","parameters":[{"name":"handle","description":"Handle of the stream to close.","$ref":"StreamHandle"}]},{"name":"read","description":"Read a chunk of the stream","parameters":[{"name":"handle","description":"Handle of the stream to read.","$ref":"StreamHandle"},{"name":"offset","description":"Seek to the specified offset before reading (if not specificed, proceed with offset\\nfollowing the last read). Some types of streams may only support sequential reads.","optional":true,"type":"integer"},{"name":"size","description":"Maximum number of bytes to read (left upon the agent discretion if not specified).","optional":true,"type":"integer"}],"returns":[{"name":"base64Encoded","description":"Set if the data is base64-encoded","optional":true,"type":"boolean"},{"name":"data","description":"Data that were read.","type":"string"},{"name":"eof","description":"Set if the end-of-file condition occurred while reading.","type":"boolean"}]},{"name":"resolveBlob","description":"Return UUID of Blob object specified by a remote object id.","parameters":[{"name":"objectId","description":"Object id of a Blob object wrapper.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"uuid","description":"UUID of the specified Blob.","type":"string"}]}]},{"domain":"IndexedDB","experimental":true,"dependencies":["Runtime","Storage"],"types":[{"id":"DatabaseWithObjectStores","description":"Database with an array of object stores.","type":"object","properties":[{"name":"name","description":"Database name.","type":"string"},{"name":"version","description":"Database version (type is not \'integer\', as the standard\\nrequires the version number to be \'unsigned long long\')","type":"number"},{"name":"objectStores","description":"Object stores in this database.","type":"array","items":{"$ref":"ObjectStore"}}]},{"id":"ObjectStore","description":"Object store.","type":"object","properties":[{"name":"name","description":"Object store name.","type":"string"},{"name":"keyPath","description":"Object store key path.","$ref":"KeyPath"},{"name":"autoIncrement","description":"If true, object store has auto increment flag set.","type":"boolean"},{"name":"indexes","description":"Indexes in this object store.","type":"array","items":{"$ref":"ObjectStoreIndex"}}]},{"id":"ObjectStoreIndex","description":"Object store index.","type":"object","properties":[{"name":"name","description":"Index name.","type":"string"},{"name":"keyPath","description":"Index key path.","$ref":"KeyPath"},{"name":"unique","description":"If true, index is unique.","type":"boolean"},{"name":"multiEntry","description":"If true, index allows multiple entries for a key.","type":"boolean"}]},{"id":"Key","description":"Key.","type":"object","properties":[{"name":"type","description":"Key type.","type":"string","enum":["number","string","date","array"]},{"name":"number","description":"Number value.","optional":true,"type":"number"},{"name":"string","description":"String value.","optional":true,"type":"string"},{"name":"date","description":"Date value.","optional":true,"type":"number"},{"name":"array","description":"Array value.","optional":true,"type":"array","items":{"$ref":"Key"}}]},{"id":"KeyRange","description":"Key range.","type":"object","properties":[{"name":"lower","description":"Lower bound.","optional":true,"$ref":"Key"},{"name":"upper","description":"Upper bound.","optional":true,"$ref":"Key"},{"name":"lowerOpen","description":"If true lower bound is open.","type":"boolean"},{"name":"upperOpen","description":"If true upper bound is open.","type":"boolean"}]},{"id":"DataEntry","description":"Data entry.","type":"object","properties":[{"name":"key","description":"Key object.","$ref":"Runtime.RemoteObject"},{"name":"primaryKey","description":"Primary key object.","$ref":"Runtime.RemoteObject"},{"name":"value","description":"Value object.","$ref":"Runtime.RemoteObject"}]},{"id":"KeyPath","description":"Key path.","type":"object","properties":[{"name":"type","description":"Key path type.","type":"string","enum":["null","string","array"]},{"name":"string","description":"String value.","optional":true,"type":"string"},{"name":"array","description":"Array value.","optional":true,"type":"array","items":{"type":"string"}}]}],"commands":[{"name":"clearObjectStore","description":"Clears all entries from an object store.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"}]},{"name":"deleteDatabase","description":"Deletes a database.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","description":"Database name.","type":"string"}]},{"name":"deleteObjectStoreEntries","description":"Delete a range of entries from an object store","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","type":"string"},{"name":"objectStoreName","type":"string"},{"name":"keyRange","description":"Range of entry keys to delete","$ref":"KeyRange"}]},{"name":"disable","description":"Disables events from backend."},{"name":"enable","description":"Enables events from backend."},{"name":"requestData","description":"Requests data from object store or index.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"},{"name":"indexName","description":"Index name, empty string for object store data requests.","type":"string"},{"name":"skipCount","description":"Number of records to skip.","type":"integer"},{"name":"pageSize","description":"Number of records to fetch.","type":"integer"},{"name":"keyRange","description":"Key range.","optional":true,"$ref":"KeyRange"}],"returns":[{"name":"objectStoreDataEntries","description":"Array of object store data entries.","type":"array","items":{"$ref":"DataEntry"}},{"name":"hasMore","description":"If true, there are more entries to fetch in the given range.","type":"boolean"}]},{"name":"getMetadata","description":"Gets metadata of an object store.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"}],"returns":[{"name":"entriesCount","description":"the entries count","type":"number"},{"name":"keyGeneratorValue","description":"the current value of key generator, to become the next inserted\\nkey into the object store. Valid if objectStore.autoIncrement\\nis true.","type":"number"}]},{"name":"requestDatabase","description":"Requests database with given name in given frame.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","description":"Database name.","type":"string"}],"returns":[{"name":"databaseWithObjectStores","description":"Database with an array of object stores.","$ref":"DatabaseWithObjectStores"}]},{"name":"requestDatabaseNames","description":"Requests database names for given security origin.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"}],"returns":[{"name":"databaseNames","description":"Database names for origin.","type":"array","items":{"type":"string"}}]}]},{"domain":"Input","types":[{"id":"TouchPoint","type":"object","properties":[{"name":"x","description":"X coordinate of the event relative to the main frame\'s viewport in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the event relative to the main frame\'s viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.","type":"number"},{"name":"radiusX","description":"X radius of the touch area (default: 1.0).","optional":true,"type":"number"},{"name":"radiusY","description":"Y radius of the touch area (default: 1.0).","optional":true,"type":"number"},{"name":"rotationAngle","description":"Rotation angle (default: 0.0).","optional":true,"type":"number"},{"name":"force","description":"Force (default: 1.0).","optional":true,"type":"number"},{"name":"tangentialPressure","description":"The normalized tangential pressure, which has a range of [-1,1] (default: 0).","experimental":true,"optional":true,"type":"number"},{"name":"tiltX","description":"The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0)","experimental":true,"optional":true,"type":"integer"},{"name":"tiltY","description":"The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0).","experimental":true,"optional":true,"type":"integer"},{"name":"twist","description":"The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0).","experimental":true,"optional":true,"type":"integer"},{"name":"id","description":"Identifier used to track touch sources between events, must be unique within an event.","optional":true,"type":"number"}]},{"id":"GestureSourceType","experimental":true,"type":"string","enum":["default","touch","mouse"]},{"id":"MouseButton","type":"string","enum":["none","left","middle","right","back","forward"]},{"id":"TimeSinceEpoch","description":"UTC time in seconds, counted from January 1, 1970.","type":"number"},{"id":"DragDataItem","experimental":true,"type":"object","properties":[{"name":"mimeType","description":"Mime type of the dragged data.","type":"string"},{"name":"data","description":"Depending of the value of `mimeType`, it contains the dragged link,\\ntext, HTML markup or any other data.","type":"string"},{"name":"title","description":"Title associated with a link. Only valid when `mimeType` == \\"text/uri-list\\".","optional":true,"type":"string"},{"name":"baseURL","description":"Stores the base URL for the contained markup. Only valid when `mimeType`\\n== \\"text/html\\".","optional":true,"type":"string"}]},{"id":"DragData","experimental":true,"type":"object","properties":[{"name":"items","type":"array","items":{"$ref":"DragDataItem"}},{"name":"files","description":"List of filenames that should be included when dropping","optional":true,"type":"array","items":{"type":"string"}},{"name":"dragOperationsMask","description":"Bit field representing allowed drag operations. Copy = 1, Link = 2, Move = 16","type":"integer"}]}],"commands":[{"name":"dispatchDragEvent","description":"Dispatches a drag event into the page.","experimental":true,"parameters":[{"name":"type","description":"Type of the drag event.","type":"string","enum":["dragEnter","dragOver","drop","dragCancel"]},{"name":"x","description":"X coordinate of the event relative to the main frame\'s viewport in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the event relative to the main frame\'s viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.","type":"number"},{"name":"data","$ref":"DragData"},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"}]},{"name":"dispatchKeyEvent","description":"Dispatches a key event to the page.","parameters":[{"name":"type","description":"Type of the key event.","type":"string","enum":["keyDown","keyUp","rawKeyDown","char"]},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"},{"name":"text","description":"Text as generated by processing a virtual key code with a keyboard layout. Not needed for\\nfor `keyUp` and `rawKeyDown` events (default: \\"\\")","optional":true,"type":"string"},{"name":"unmodifiedText","description":"Text that would have been generated by the keyboard if no modifiers were pressed (except for\\nshift). Useful for shortcut (accelerator) key handling (default: \\"\\").","optional":true,"type":"string"},{"name":"keyIdentifier","description":"Unique key identifier (e.g., \'U+0041\') (default: \\"\\").","optional":true,"type":"string"},{"name":"code","description":"Unique DOM defined string value for each physical key (e.g., \'KeyA\') (default: \\"\\").","optional":true,"type":"string"},{"name":"key","description":"Unique DOM defined string value describing the meaning of the key in the context of active\\nmodifiers, keyboard layout, etc (e.g., \'AltGr\') (default: \\"\\").","optional":true,"type":"string"},{"name":"windowsVirtualKeyCode","description":"Windows virtual key code (default: 0).","optional":true,"type":"integer"},{"name":"nativeVirtualKeyCode","description":"Native virtual key code (default: 0).","optional":true,"type":"integer"},{"name":"autoRepeat","description":"Whether the event was generated from auto repeat (default: false).","optional":true,"type":"boolean"},{"name":"isKeypad","description":"Whether the event was generated from the keypad (default: false).","optional":true,"type":"boolean"},{"name":"isSystemKey","description":"Whether the event was a system key event (default: false).","optional":true,"type":"boolean"},{"name":"location","description":"Whether the event was from the left or right side of the keyboard. 1=Left, 2=Right (default:\\n0).","optional":true,"type":"integer"},{"name":"commands","description":"Editing commands to send with the key event (e.g., \'selectAll\') (default: []).\\nThese are related to but not equal the command names used in `document.execCommand` and NSStandardKeyBindingResponding.\\nSee https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h for valid command names.","experimental":true,"optional":true,"type":"array","items":{"type":"string"}}]},{"name":"insertText","description":"This method emulates inserting text that doesn\'t come from a key press,\\nfor example an emoji keyboard or an IME.","experimental":true,"parameters":[{"name":"text","description":"The text to insert.","type":"string"}]},{"name":"imeSetComposition","description":"This method sets the current candidate text for ime.\\nUse imeCommitComposition to commit the final text.\\nUse imeSetComposition with empty string as text to cancel composition.","experimental":true,"parameters":[{"name":"text","description":"The text to insert","type":"string"},{"name":"selectionStart","description":"selection start","type":"integer"},{"name":"selectionEnd","description":"selection end","type":"integer"},{"name":"replacementStart","description":"replacement start","optional":true,"type":"integer"},{"name":"replacementEnd","description":"replacement end","optional":true,"type":"integer"}]},{"name":"dispatchMouseEvent","description":"Dispatches a mouse event to the page.","parameters":[{"name":"type","description":"Type of the mouse event.","type":"string","enum":["mousePressed","mouseReleased","mouseMoved","mouseWheel"]},{"name":"x","description":"X coordinate of the event relative to the main frame\'s viewport in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the event relative to the main frame\'s viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.","type":"number"},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"},{"name":"button","description":"Mouse button (default: \\"none\\").","optional":true,"$ref":"MouseButton"},{"name":"buttons","description":"A number indicating which buttons are pressed on the mouse when a mouse event is triggered.\\nLeft=1, Right=2, Middle=4, Back=8, Forward=16, None=0.","optional":true,"type":"integer"},{"name":"clickCount","description":"Number of times the mouse button was clicked (default: 0).","optional":true,"type":"integer"},{"name":"force","description":"The normalized pressure, which has a range of [0,1] (default: 0).","experimental":true,"optional":true,"type":"number"},{"name":"tangentialPressure","description":"The normalized tangential pressure, which has a range of [-1,1] (default: 0).","experimental":true,"optional":true,"type":"number"},{"name":"tiltX","description":"The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0).","experimental":true,"optional":true,"type":"integer"},{"name":"tiltY","description":"The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0).","experimental":true,"optional":true,"type":"integer"},{"name":"twist","description":"The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0).","experimental":true,"optional":true,"type":"integer"},{"name":"deltaX","description":"X delta in CSS pixels for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"deltaY","description":"Y delta in CSS pixels for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"pointerType","description":"Pointer type (default: \\"mouse\\").","optional":true,"type":"string","enum":["mouse","pen"]}]},{"name":"dispatchTouchEvent","description":"Dispatches a touch event to the page.","parameters":[{"name":"type","description":"Type of the touch event. TouchEnd and TouchCancel must not contain any touch points, while\\nTouchStart and TouchMove must contains at least one.","type":"string","enum":["touchStart","touchEnd","touchMove","touchCancel"]},{"name":"touchPoints","description":"Active touch points on the touch device. One event per any changed point (compared to\\nprevious touch event in a sequence) is generated, emulating pressing/moving/releasing points\\none by one.","type":"array","items":{"$ref":"TouchPoint"}},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"}]},{"name":"cancelDragging","description":"Cancels any active dragging in the page."},{"name":"emulateTouchFromMouseEvent","description":"Emulates touch event from the mouse event parameters.","experimental":true,"parameters":[{"name":"type","description":"Type of the mouse event.","type":"string","enum":["mousePressed","mouseReleased","mouseMoved","mouseWheel"]},{"name":"x","description":"X coordinate of the mouse pointer in DIP.","type":"integer"},{"name":"y","description":"Y coordinate of the mouse pointer in DIP.","type":"integer"},{"name":"button","description":"Mouse button. Only \\"none\\", \\"left\\", \\"right\\" are supported.","$ref":"MouseButton"},{"name":"timestamp","description":"Time at which the event occurred (default: current time).","optional":true,"$ref":"TimeSinceEpoch"},{"name":"deltaX","description":"X delta in DIP for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"deltaY","description":"Y delta in DIP for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"clickCount","description":"Number of times the mouse button was clicked (default: 0).","optional":true,"type":"integer"}]},{"name":"setIgnoreInputEvents","description":"Ignores input events (useful while auditing page).","parameters":[{"name":"ignore","description":"Ignores input events processing when set to true.","type":"boolean"}]},{"name":"setInterceptDrags","description":"Prevents default drag and drop behavior and instead emits `Input.dragIntercepted` events.\\nDrag and drop behavior can be directly controlled via `Input.dispatchDragEvent`.","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"synthesizePinchGesture","description":"Synthesizes a pinch gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"scaleFactor","description":"Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out).","type":"number"},{"name":"relativeSpeed","description":"Relative pointer speed in pixels per second (default: 800).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"}]},{"name":"synthesizeScrollGesture","description":"Synthesizes a scroll gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"xDistance","description":"The distance to scroll along the X axis (positive to scroll left).","optional":true,"type":"number"},{"name":"yDistance","description":"The distance to scroll along the Y axis (positive to scroll up).","optional":true,"type":"number"},{"name":"xOverscroll","description":"The number of additional pixels to scroll back along the X axis, in addition to the given\\ndistance.","optional":true,"type":"number"},{"name":"yOverscroll","description":"The number of additional pixels to scroll back along the Y axis, in addition to the given\\ndistance.","optional":true,"type":"number"},{"name":"preventFling","description":"Prevent fling (default: true).","optional":true,"type":"boolean"},{"name":"speed","description":"Swipe speed in pixels per second (default: 800).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"},{"name":"repeatCount","description":"The number of times to repeat the gesture (default: 0).","optional":true,"type":"integer"},{"name":"repeatDelayMs","description":"The number of milliseconds delay between each repeat. (default: 250).","optional":true,"type":"integer"},{"name":"interactionMarkerName","description":"The name of the interaction markers to generate, if not empty (default: \\"\\").","optional":true,"type":"string"}]},{"name":"synthesizeTapGesture","description":"Synthesizes a tap gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"duration","description":"Duration between touchdown and touchup events in ms (default: 50).","optional":true,"type":"integer"},{"name":"tapCount","description":"Number of times to perform the tap (e.g. 2 for double tap, default: 1).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"}]}],"events":[{"name":"dragIntercepted","description":"Emitted only when `Input.setInterceptDrags` is enabled. Use this data with `Input.dispatchDragEvent` to\\nrestore normal drag and drop behavior.","experimental":true,"parameters":[{"name":"data","$ref":"DragData"}]}]},{"domain":"Inspector","experimental":true,"commands":[{"name":"disable","description":"Disables inspector domain notifications."},{"name":"enable","description":"Enables inspector domain notifications."}],"events":[{"name":"detached","description":"Fired when remote debugging connection is about to be terminated. Contains detach reason.","parameters":[{"name":"reason","description":"The reason why connection has been terminated.","type":"string"}]},{"name":"targetCrashed","description":"Fired when debugging target has crashed"},{"name":"targetReloadedAfterCrash","description":"Fired when debugging target has reloaded after crash"}]},{"domain":"LayerTree","experimental":true,"dependencies":["DOM"],"types":[{"id":"LayerId","description":"Unique Layer identifier.","type":"string"},{"id":"SnapshotId","description":"Unique snapshot identifier.","type":"string"},{"id":"ScrollRect","description":"Rectangle where scrolling happens on the main thread.","type":"object","properties":[{"name":"rect","description":"Rectangle itself.","$ref":"DOM.Rect"},{"name":"type","description":"Reason for rectangle to force scrolling on the main thread","type":"string","enum":["RepaintsOnScroll","TouchEventHandler","WheelEventHandler"]}]},{"id":"StickyPositionConstraint","description":"Sticky position constraints.","type":"object","properties":[{"name":"stickyBoxRect","description":"Layout rectangle of the sticky element before being shifted","$ref":"DOM.Rect"},{"name":"containingBlockRect","description":"Layout rectangle of the containing block of the sticky element","$ref":"DOM.Rect"},{"name":"nearestLayerShiftingStickyBox","description":"The nearest sticky layer that shifts the sticky box","optional":true,"$ref":"LayerId"},{"name":"nearestLayerShiftingContainingBlock","description":"The nearest sticky layer that shifts the containing block","optional":true,"$ref":"LayerId"}]},{"id":"PictureTile","description":"Serialized fragment of layer picture along with its offset within the layer.","type":"object","properties":[{"name":"x","description":"Offset from owning layer left boundary","type":"number"},{"name":"y","description":"Offset from owning layer top boundary","type":"number"},{"name":"picture","description":"Base64-encoded snapshot data. (Encoded as a base64 string when passed over JSON)","type":"string"}]},{"id":"Layer","description":"Information about a compositing layer.","type":"object","properties":[{"name":"layerId","description":"The unique id for this layer.","$ref":"LayerId"},{"name":"parentLayerId","description":"The id of parent (not present for root).","optional":true,"$ref":"LayerId"},{"name":"backendNodeId","description":"The backend id for the node associated with this layer.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"offsetX","description":"Offset from parent layer, X coordinate.","type":"number"},{"name":"offsetY","description":"Offset from parent layer, Y coordinate.","type":"number"},{"name":"width","description":"Layer width.","type":"number"},{"name":"height","description":"Layer height.","type":"number"},{"name":"transform","description":"Transformation matrix for layer, default is identity matrix","optional":true,"type":"array","items":{"type":"number"}},{"name":"anchorX","description":"Transform anchor point X, absent if no transform specified","optional":true,"type":"number"},{"name":"anchorY","description":"Transform anchor point Y, absent if no transform specified","optional":true,"type":"number"},{"name":"anchorZ","description":"Transform anchor point Z, absent if no transform specified","optional":true,"type":"number"},{"name":"paintCount","description":"Indicates how many time this layer has painted.","type":"integer"},{"name":"drawsContent","description":"Indicates whether this layer hosts any content, rather than being used for\\ntransform/scrolling purposes only.","type":"boolean"},{"name":"invisible","description":"Set if layer is not visible.","optional":true,"type":"boolean"},{"name":"scrollRects","description":"Rectangles scrolling on main thread only.","optional":true,"type":"array","items":{"$ref":"ScrollRect"}},{"name":"stickyPositionConstraint","description":"Sticky position constraint information","optional":true,"$ref":"StickyPositionConstraint"}]},{"id":"PaintProfile","description":"Array of timings, one per paint step.","type":"array","items":{"type":"number"}}],"commands":[{"name":"compositingReasons","description":"Provides the reasons why the given layer was composited.","parameters":[{"name":"layerId","description":"The id of the layer for which we want to get the reasons it was composited.","$ref":"LayerId"}],"returns":[{"name":"compositingReasons","description":"A list of strings specifying reasons for the given layer to become composited.","type":"array","items":{"type":"string"}},{"name":"compositingReasonIds","description":"A list of strings specifying reason IDs for the given layer to become composited.","type":"array","items":{"type":"string"}}]},{"name":"disable","description":"Disables compositing tree inspection."},{"name":"enable","description":"Enables compositing tree inspection."},{"name":"loadSnapshot","description":"Returns the snapshot identifier.","parameters":[{"name":"tiles","description":"An array of tiles composing the snapshot.","type":"array","items":{"$ref":"PictureTile"}}],"returns":[{"name":"snapshotId","description":"The id of the snapshot.","$ref":"SnapshotId"}]},{"name":"makeSnapshot","description":"Returns the layer snapshot identifier.","parameters":[{"name":"layerId","description":"The id of the layer.","$ref":"LayerId"}],"returns":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}]},{"name":"profileSnapshot","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"},{"name":"minRepeatCount","description":"The maximum number of times to replay the snapshot (1, if not specified).","optional":true,"type":"integer"},{"name":"minDuration","description":"The minimum duration (in seconds) to replay the snapshot.","optional":true,"type":"number"},{"name":"clipRect","description":"The clip rectangle to apply when replaying the snapshot.","optional":true,"$ref":"DOM.Rect"}],"returns":[{"name":"timings","description":"The array of paint profiles, one per run.","type":"array","items":{"$ref":"PaintProfile"}}]},{"name":"releaseSnapshot","description":"Releases layer snapshot captured by the back-end.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}]},{"name":"replaySnapshot","description":"Replays the layer snapshot and returns the resulting bitmap.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"},{"name":"fromStep","description":"The first step to replay from (replay from the very start if not specified).","optional":true,"type":"integer"},{"name":"toStep","description":"The last step to replay to (replay till the end if not specified).","optional":true,"type":"integer"},{"name":"scale","description":"The scale to apply while replaying (defaults to 1).","optional":true,"type":"number"}],"returns":[{"name":"dataURL","description":"A data: URL for resulting image.","type":"string"}]},{"name":"snapshotCommandLog","description":"Replays the layer snapshot and returns canvas log.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}],"returns":[{"name":"commandLog","description":"The array of canvas function calls.","type":"array","items":{"type":"object"}}]}],"events":[{"name":"layerPainted","parameters":[{"name":"layerId","description":"The id of the painted layer.","$ref":"LayerId"},{"name":"clip","description":"Clip rectangle.","$ref":"DOM.Rect"}]},{"name":"layerTreeDidChange","parameters":[{"name":"layers","description":"Layer tree, absent if not in the comspositing mode.","optional":true,"type":"array","items":{"$ref":"Layer"}}]}]},{"domain":"Log","description":"Provides access to log entries.","dependencies":["Runtime","Network"],"types":[{"id":"LogEntry","description":"Log entry.","type":"object","properties":[{"name":"source","description":"Log entry source.","type":"string","enum":["xml","javascript","network","storage","appcache","rendering","security","deprecation","worker","violation","intervention","recommendation","other"]},{"name":"level","description":"Log entry severity.","type":"string","enum":["verbose","info","warning","error"]},{"name":"text","description":"Logged text.","type":"string"},{"name":"category","optional":true,"type":"string","enum":["cors"]},{"name":"timestamp","description":"Timestamp when this entry was added.","$ref":"Runtime.Timestamp"},{"name":"url","description":"URL of the resource if known.","optional":true,"type":"string"},{"name":"lineNumber","description":"Line number in the resource.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript stack trace.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"networkRequestId","description":"Identifier of the network request associated with this entry.","optional":true,"$ref":"Network.RequestId"},{"name":"workerId","description":"Identifier of the worker associated with this entry.","optional":true,"type":"string"},{"name":"args","description":"Call arguments.","optional":true,"type":"array","items":{"$ref":"Runtime.RemoteObject"}}]},{"id":"ViolationSetting","description":"Violation configuration setting.","type":"object","properties":[{"name":"name","description":"Violation type.","type":"string","enum":["longTask","longLayout","blockedEvent","blockedParser","discouragedAPIUse","handler","recurringHandler"]},{"name":"threshold","description":"Time threshold to trigger upon.","type":"number"}]}],"commands":[{"name":"clear","description":"Clears the log."},{"name":"disable","description":"Disables log domain, prevents further log entries from being reported to the client."},{"name":"enable","description":"Enables log domain, sends the entries collected so far to the client by means of the\\n`entryAdded` notification."},{"name":"startViolationsReport","description":"start violation reporting.","parameters":[{"name":"config","description":"Configuration for violations.","type":"array","items":{"$ref":"ViolationSetting"}}]},{"name":"stopViolationsReport","description":"Stop violation reporting."}],"events":[{"name":"entryAdded","description":"Issued when new message was logged.","parameters":[{"name":"entry","description":"The entry.","$ref":"LogEntry"}]}]},{"domain":"Memory","experimental":true,"types":[{"id":"PressureLevel","description":"Memory pressure level.","type":"string","enum":["moderate","critical"]},{"id":"SamplingProfileNode","description":"Heap profile sample.","type":"object","properties":[{"name":"size","description":"Size of the sampled allocation.","type":"number"},{"name":"total","description":"Total bytes attributed to this sample.","type":"number"},{"name":"stack","description":"Execution stack at the point of allocation.","type":"array","items":{"type":"string"}}]},{"id":"SamplingProfile","description":"Array of heap profile samples.","type":"object","properties":[{"name":"samples","type":"array","items":{"$ref":"SamplingProfileNode"}},{"name":"modules","type":"array","items":{"$ref":"Module"}}]},{"id":"Module","description":"Executable module information","type":"object","properties":[{"name":"name","description":"Name of the module.","type":"string"},{"name":"uuid","description":"UUID of the module.","type":"string"},{"name":"baseAddress","description":"Base address where the module is loaded into memory. Encoded as a decimal\\nor hexadecimal (0x prefixed) string.","type":"string"},{"name":"size","description":"Size of the module in bytes.","type":"number"}]}],"commands":[{"name":"getDOMCounters","returns":[{"name":"documents","type":"integer"},{"name":"nodes","type":"integer"},{"name":"jsEventListeners","type":"integer"}]},{"name":"prepareForLeakDetection"},{"name":"forciblyPurgeJavaScriptMemory","description":"Simulate OomIntervention by purging V8 memory."},{"name":"setPressureNotificationsSuppressed","description":"Enable/disable suppressing memory pressure notifications in all processes.","parameters":[{"name":"suppressed","description":"If true, memory pressure notifications will be suppressed.","type":"boolean"}]},{"name":"simulatePressureNotification","description":"Simulate a memory pressure notification in all processes.","parameters":[{"name":"level","description":"Memory pressure level of the notification.","$ref":"PressureLevel"}]},{"name":"startSampling","description":"Start collecting native memory profile.","parameters":[{"name":"samplingInterval","description":"Average number of bytes between samples.","optional":true,"type":"integer"},{"name":"suppressRandomness","description":"Do not randomize intervals between samples.","optional":true,"type":"boolean"}]},{"name":"stopSampling","description":"Stop collecting native memory profile."},{"name":"getAllTimeSamplingProfile","description":"Retrieve native memory allocations profile\\ncollected since renderer process startup.","returns":[{"name":"profile","$ref":"SamplingProfile"}]},{"name":"getBrowserSamplingProfile","description":"Retrieve native memory allocations profile\\ncollected since browser process startup.","returns":[{"name":"profile","$ref":"SamplingProfile"}]},{"name":"getSamplingProfile","description":"Retrieve native memory allocations profile collected since last\\n`startSampling` call.","returns":[{"name":"profile","$ref":"SamplingProfile"}]}]},{"domain":"Network","description":"Network domain allows tracking network activities of the page. It exposes information about http,\\nfile, data and other requests and responses, their headers, bodies, timing, etc.","dependencies":["Debugger","Runtime","Security"],"types":[{"id":"ResourceType","description":"Resource type as it was perceived by the rendering engine.","type":"string","enum":["Document","Stylesheet","Image","Media","Font","Script","TextTrack","XHR","Fetch","Prefetch","EventSource","WebSocket","Manifest","SignedExchange","Ping","CSPViolationReport","Preflight","Other"]},{"id":"LoaderId","description":"Unique loader identifier.","type":"string"},{"id":"RequestId","description":"Unique request identifier.","type":"string"},{"id":"InterceptionId","description":"Unique intercepted request identifier.","type":"string"},{"id":"ErrorReason","description":"Network level fetch failure reason.","type":"string","enum":["Failed","Aborted","TimedOut","AccessDenied","ConnectionClosed","ConnectionReset","ConnectionRefused","ConnectionAborted","ConnectionFailed","NameNotResolved","InternetDisconnected","AddressUnreachable","BlockedByClient","BlockedByResponse"]},{"id":"TimeSinceEpoch","description":"UTC time in seconds, counted from January 1, 1970.","type":"number"},{"id":"MonotonicTime","description":"Monotonically increasing time in seconds since an arbitrary point in the past.","type":"number"},{"id":"Headers","description":"Request / response headers as keys / values of JSON object.","type":"object"},{"id":"ConnectionType","description":"The underlying connection technology that the browser is supposedly using.","type":"string","enum":["none","cellular2g","cellular3g","cellular4g","bluetooth","ethernet","wifi","wimax","other"]},{"id":"CookieSameSite","description":"Represents the cookie\'s \'SameSite\' status:\\nhttps://tools.ietf.org/html/draft-west-first-party-cookies","type":"string","enum":["Strict","Lax","None"]},{"id":"CookiePriority","description":"Represents the cookie\'s \'Priority\' status:\\nhttps://tools.ietf.org/html/draft-west-cookie-priority-00","experimental":true,"type":"string","enum":["Low","Medium","High"]},{"id":"CookieSourceScheme","description":"Represents the source scheme of the origin that originally set the cookie.\\nA value of \\"Unset\\" allows protocol clients to emulate legacy cookie scope for the scheme.\\nThis is a temporary ability and it will be removed in the future.","experimental":true,"type":"string","enum":["Unset","NonSecure","Secure"]},{"id":"ResourceTiming","description":"Timing information for the request.","type":"object","properties":[{"name":"requestTime","description":"Timing\'s requestTime is a baseline in seconds, while the other numbers are ticks in\\nmilliseconds relatively to this requestTime.","type":"number"},{"name":"proxyStart","description":"Started resolving proxy.","type":"number"},{"name":"proxyEnd","description":"Finished resolving proxy.","type":"number"},{"name":"dnsStart","description":"Started DNS address resolve.","type":"number"},{"name":"dnsEnd","description":"Finished DNS address resolve.","type":"number"},{"name":"connectStart","description":"Started connecting to the remote host.","type":"number"},{"name":"connectEnd","description":"Connected to the remote host.","type":"number"},{"name":"sslStart","description":"Started SSL handshake.","type":"number"},{"name":"sslEnd","description":"Finished SSL handshake.","type":"number"},{"name":"workerStart","description":"Started running ServiceWorker.","experimental":true,"type":"number"},{"name":"workerReady","description":"Finished Starting ServiceWorker.","experimental":true,"type":"number"},{"name":"workerFetchStart","description":"Started fetch event.","experimental":true,"type":"number"},{"name":"workerRespondWithSettled","description":"Settled fetch event respondWith promise.","experimental":true,"type":"number"},{"name":"sendStart","description":"Started sending request.","type":"number"},{"name":"sendEnd","description":"Finished sending request.","type":"number"},{"name":"pushStart","description":"Time the server started pushing request.","experimental":true,"type":"number"},{"name":"pushEnd","description":"Time the server finished pushing request.","experimental":true,"type":"number"},{"name":"receiveHeadersStart","description":"Started receiving response headers.","experimental":true,"type":"number"},{"name":"receiveHeadersEnd","description":"Finished receiving response headers.","type":"number"}]},{"id":"ResourcePriority","description":"Loading priority of a resource request.","type":"string","enum":["VeryLow","Low","Medium","High","VeryHigh"]},{"id":"PostDataEntry","description":"Post data entry for HTTP request","type":"object","properties":[{"name":"bytes","optional":true,"type":"string"}]},{"id":"Request","description":"HTTP request data.","type":"object","properties":[{"name":"url","description":"Request URL (without fragment).","type":"string"},{"name":"urlFragment","description":"Fragment of the requested URL starting with hash, if present.","optional":true,"type":"string"},{"name":"method","description":"HTTP request method.","type":"string"},{"name":"headers","description":"HTTP request headers.","$ref":"Headers"},{"name":"postData","description":"HTTP POST request data.","optional":true,"type":"string"},{"name":"hasPostData","description":"True when the request has POST data. Note that postData might still be omitted when this flag is true when the data is too long.","optional":true,"type":"boolean"},{"name":"postDataEntries","description":"Request body elements. This will be converted from base64 to binary","experimental":true,"optional":true,"type":"array","items":{"$ref":"PostDataEntry"}},{"name":"mixedContentType","description":"The mixed content type of the request.","optional":true,"$ref":"Security.MixedContentType"},{"name":"initialPriority","description":"Priority of the resource request at the time request is sent.","$ref":"ResourcePriority"},{"name":"referrerPolicy","description":"The referrer policy of the request, as defined in https://www.w3.org/TR/referrer-policy/","type":"string","enum":["unsafe-url","no-referrer-when-downgrade","no-referrer","origin","origin-when-cross-origin","same-origin","strict-origin","strict-origin-when-cross-origin"]},{"name":"isLinkPreload","description":"Whether is loaded via link preload.","optional":true,"type":"boolean"},{"name":"trustTokenParams","description":"Set for requests when the TrustToken API is used. Contains the parameters\\npassed by the developer (e.g. via \\"fetch\\") as understood by the backend.","experimental":true,"optional":true,"$ref":"TrustTokenParams"},{"name":"isSameSite","description":"True if this resource request is considered to be the \'same site\' as the\\nrequest correspondinfg to the main frame.","experimental":true,"optional":true,"type":"boolean"}]},{"id":"SignedCertificateTimestamp","description":"Details of a signed certificate timestamp (SCT).","type":"object","properties":[{"name":"status","description":"Validation status.","type":"string"},{"name":"origin","description":"Origin.","type":"string"},{"name":"logDescription","description":"Log name / description.","type":"string"},{"name":"logId","description":"Log ID.","type":"string"},{"name":"timestamp","description":"Issuance date. Unlike TimeSinceEpoch, this contains the number of\\nmilliseconds since January 1, 1970, UTC, not the number of seconds.","type":"number"},{"name":"hashAlgorithm","description":"Hash algorithm.","type":"string"},{"name":"signatureAlgorithm","description":"Signature algorithm.","type":"string"},{"name":"signatureData","description":"Signature data.","type":"string"}]},{"id":"SecurityDetails","description":"Security details about a request.","type":"object","properties":[{"name":"protocol","description":"Protocol name (e.g. \\"TLS 1.2\\" or \\"QUIC\\").","type":"string"},{"name":"keyExchange","description":"Key Exchange used by the connection, or the empty string if not applicable.","type":"string"},{"name":"keyExchangeGroup","description":"(EC)DH group used by the connection, if applicable.","optional":true,"type":"string"},{"name":"cipher","description":"Cipher name.","type":"string"},{"name":"mac","description":"TLS MAC. Note that AEAD ciphers do not have separate MACs.","optional":true,"type":"string"},{"name":"certificateId","description":"Certificate ID value.","$ref":"Security.CertificateId"},{"name":"subjectName","description":"Certificate subject name.","type":"string"},{"name":"sanList","description":"Subject Alternative Name (SAN) DNS names and IP addresses.","type":"array","items":{"type":"string"}},{"name":"issuer","description":"Name of the issuing CA.","type":"string"},{"name":"validFrom","description":"Certificate valid from date.","$ref":"TimeSinceEpoch"},{"name":"validTo","description":"Certificate valid to (expiration) date","$ref":"TimeSinceEpoch"},{"name":"signedCertificateTimestampList","description":"List of signed certificate timestamps (SCTs).","type":"array","items":{"$ref":"SignedCertificateTimestamp"}},{"name":"certificateTransparencyCompliance","description":"Whether the request complied with Certificate Transparency policy","$ref":"CertificateTransparencyCompliance"},{"name":"serverSignatureAlgorithm","description":"The signature algorithm used by the server in the TLS server signature,\\nrepresented as a TLS SignatureScheme code point. Omitted if not\\napplicable or not known.","optional":true,"type":"integer"},{"name":"encryptedClientHello","description":"Whether the connection used Encrypted ClientHello","type":"boolean"}]},{"id":"CertificateTransparencyCompliance","description":"Whether the request complied with Certificate Transparency policy.","type":"string","enum":["unknown","not-compliant","compliant"]},{"id":"BlockedReason","description":"The reason why request was blocked.","type":"string","enum":["other","csp","mixed-content","origin","inspector","subresource-filter","content-type","coep-frame-resource-needs-coep-header","coop-sandboxed-iframe-cannot-navigate-to-coop-page","corp-not-same-origin","corp-not-same-origin-after-defaulted-to-same-origin-by-coep","corp-not-same-site"]},{"id":"CorsError","description":"The reason why request was blocked.","type":"string","enum":["DisallowedByMode","InvalidResponse","WildcardOriginNotAllowed","MissingAllowOriginHeader","MultipleAllowOriginValues","InvalidAllowOriginValue","AllowOriginMismatch","InvalidAllowCredentials","CorsDisabledScheme","PreflightInvalidStatus","PreflightDisallowedRedirect","PreflightWildcardOriginNotAllowed","PreflightMissingAllowOriginHeader","PreflightMultipleAllowOriginValues","PreflightInvalidAllowOriginValue","PreflightAllowOriginMismatch","PreflightInvalidAllowCredentials","PreflightMissingAllowExternal","PreflightInvalidAllowExternal","PreflightMissingAllowPrivateNetwork","PreflightInvalidAllowPrivateNetwork","InvalidAllowMethodsPreflightResponse","InvalidAllowHeadersPreflightResponse","MethodDisallowedByPreflightResponse","HeaderDisallowedByPreflightResponse","RedirectContainsCredentials","InsecurePrivateNetwork","InvalidPrivateNetworkAccess","UnexpectedPrivateNetworkAccess","NoCorsRedirectModeNotFollow","PreflightMissingPrivateNetworkAccessId","PreflightMissingPrivateNetworkAccessName","PrivateNetworkAccessPermissionUnavailable","PrivateNetworkAccessPermissionDenied"]},{"id":"CorsErrorStatus","type":"object","properties":[{"name":"corsError","$ref":"CorsError"},{"name":"failedParameter","type":"string"}]},{"id":"ServiceWorkerResponseSource","description":"Source of serviceworker response.","type":"string","enum":["cache-storage","http-cache","fallback-code","network"]},{"id":"TrustTokenParams","description":"Determines what type of Trust Token operation is executed and\\ndepending on the type, some additional parameters. The values\\nare specified in third_party/blink/renderer/core/fetch/trust_token.idl.","experimental":true,"type":"object","properties":[{"name":"operation","$ref":"TrustTokenOperationType"},{"name":"refreshPolicy","description":"Only set for \\"token-redemption\\" operation and determine whether\\nto request a fresh SRR or use a still valid cached SRR.","type":"string","enum":["UseCached","Refresh"]},{"name":"issuers","description":"Origins of issuers from whom to request tokens or redemption\\nrecords.","optional":true,"type":"array","items":{"type":"string"}}]},{"id":"TrustTokenOperationType","experimental":true,"type":"string","enum":["Issuance","Redemption","Signing"]},{"id":"AlternateProtocolUsage","description":"The reason why Chrome uses a specific transport protocol for HTTP semantics.","experimental":true,"type":"string","enum":["alternativeJobWonWithoutRace","alternativeJobWonRace","mainJobWonRace","mappingMissing","broken","dnsAlpnH3JobWonWithoutRace","dnsAlpnH3JobWonRace","unspecifiedReason"]},{"id":"Response","description":"HTTP response data.","type":"object","properties":[{"name":"url","description":"Response URL. This URL can be different from CachedResource.url in case of redirect.","type":"string"},{"name":"status","description":"HTTP response status code.","type":"integer"},{"name":"statusText","description":"HTTP response status text.","type":"string"},{"name":"headers","description":"HTTP response headers.","$ref":"Headers"},{"name":"headersText","description":"HTTP response headers text. This has been replaced by the headers in Network.responseReceivedExtraInfo.","deprecated":true,"optional":true,"type":"string"},{"name":"mimeType","description":"Resource mimeType as determined by the browser.","type":"string"},{"name":"requestHeaders","description":"Refined HTTP request headers that were actually transmitted over the network.","optional":true,"$ref":"Headers"},{"name":"requestHeadersText","description":"HTTP request headers text. This has been replaced by the headers in Network.requestWillBeSentExtraInfo.","deprecated":true,"optional":true,"type":"string"},{"name":"connectionReused","description":"Specifies whether physical connection was actually reused for this request.","type":"boolean"},{"name":"connectionId","description":"Physical connection id that was actually used for this request.","type":"number"},{"name":"remoteIPAddress","description":"Remote IP address.","optional":true,"type":"string"},{"name":"remotePort","description":"Remote port.","optional":true,"type":"integer"},{"name":"fromDiskCache","description":"Specifies that the request was served from the disk cache.","optional":true,"type":"boolean"},{"name":"fromServiceWorker","description":"Specifies that the request was served from the ServiceWorker.","optional":true,"type":"boolean"},{"name":"fromPrefetchCache","description":"Specifies that the request was served from the prefetch cache.","optional":true,"type":"boolean"},{"name":"encodedDataLength","description":"Total number of bytes received for this request so far.","type":"number"},{"name":"timing","description":"Timing information for the given request.","optional":true,"$ref":"ResourceTiming"},{"name":"serviceWorkerResponseSource","description":"Response source of response from ServiceWorker.","optional":true,"$ref":"ServiceWorkerResponseSource"},{"name":"responseTime","description":"The time at which the returned response was generated.","optional":true,"$ref":"TimeSinceEpoch"},{"name":"cacheStorageCacheName","description":"Cache Storage Cache Name.","optional":true,"type":"string"},{"name":"protocol","description":"Protocol used to fetch this request.","optional":true,"type":"string"},{"name":"alternateProtocolUsage","description":"The reason why Chrome uses a specific transport protocol for HTTP semantics.","experimental":true,"optional":true,"$ref":"AlternateProtocolUsage"},{"name":"securityState","description":"Security state of the request resource.","$ref":"Security.SecurityState"},{"name":"securityDetails","description":"Security details for the request.","optional":true,"$ref":"SecurityDetails"}]},{"id":"WebSocketRequest","description":"WebSocket request data.","type":"object","properties":[{"name":"headers","description":"HTTP request headers.","$ref":"Headers"}]},{"id":"WebSocketResponse","description":"WebSocket response data.","type":"object","properties":[{"name":"status","description":"HTTP response status code.","type":"integer"},{"name":"statusText","description":"HTTP response status text.","type":"string"},{"name":"headers","description":"HTTP response headers.","$ref":"Headers"},{"name":"headersText","description":"HTTP response headers text.","optional":true,"type":"string"},{"name":"requestHeaders","description":"HTTP request headers.","optional":true,"$ref":"Headers"},{"name":"requestHeadersText","description":"HTTP request headers text.","optional":true,"type":"string"}]},{"id":"WebSocketFrame","description":"WebSocket message data. This represents an entire WebSocket message, not just a fragmented frame as the name suggests.","type":"object","properties":[{"name":"opcode","description":"WebSocket message opcode.","type":"number"},{"name":"mask","description":"WebSocket message mask.","type":"boolean"},{"name":"payloadData","description":"WebSocket message payload data.\\nIf the opcode is 1, this is a text message and payloadData is a UTF-8 string.\\nIf the opcode isn\'t 1, then payloadData is a base64 encoded string representing binary data.","type":"string"}]},{"id":"CachedResource","description":"Information about the cached resource.","type":"object","properties":[{"name":"url","description":"Resource URL. This is the url of the original network request.","type":"string"},{"name":"type","description":"Type of this resource.","$ref":"ResourceType"},{"name":"response","description":"Cached response data.","optional":true,"$ref":"Response"},{"name":"bodySize","description":"Cached response body size.","type":"number"}]},{"id":"Initiator","description":"Information about the request initiator.","type":"object","properties":[{"name":"type","description":"Type of this initiator.","type":"string","enum":["parser","script","preload","SignedExchange","preflight","other"]},{"name":"stack","description":"Initiator JavaScript stack trace, set for Script only.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"url","description":"Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type.","optional":true,"type":"string"},{"name":"lineNumber","description":"Initiator line number, set for Parser type or for Script type (when script is importing\\nmodule) (0-based).","optional":true,"type":"number"},{"name":"columnNumber","description":"Initiator column number, set for Parser type or for Script type (when script is importing\\nmodule) (0-based).","optional":true,"type":"number"},{"name":"requestId","description":"Set if another request triggered this request (e.g. preflight).","optional":true,"$ref":"RequestId"}]},{"id":"Cookie","description":"Cookie object","type":"object","properties":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"domain","description":"Cookie domain.","type":"string"},{"name":"path","description":"Cookie path.","type":"string"},{"name":"expires","description":"Cookie expiration date as the number of seconds since the UNIX epoch.","type":"number"},{"name":"size","description":"Cookie size.","type":"integer"},{"name":"httpOnly","description":"True if cookie is http-only.","type":"boolean"},{"name":"secure","description":"True if cookie is secure.","type":"boolean"},{"name":"session","description":"True in case of session cookie.","type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"},{"name":"priority","description":"Cookie Priority","experimental":true,"$ref":"CookiePriority"},{"name":"sameParty","description":"True if cookie is SameParty.","experimental":true,"type":"boolean"},{"name":"sourceScheme","description":"Cookie source scheme type.","experimental":true,"$ref":"CookieSourceScheme"},{"name":"sourcePort","description":"Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\\nThis is a temporary ability and it will be removed in the future.","experimental":true,"type":"integer"},{"name":"partitionKey","description":"Cookie partition key. The site of the top-level URL the browser was visiting at the start\\nof the request to the endpoint that set the cookie.","experimental":true,"optional":true,"type":"string"},{"name":"partitionKeyOpaque","description":"True if cookie partition key is opaque.","experimental":true,"optional":true,"type":"boolean"}]},{"id":"SetCookieBlockedReason","description":"Types of reasons why a cookie may not be stored from a response.","experimental":true,"type":"string","enum":["SecureOnly","SameSiteStrict","SameSiteLax","SameSiteUnspecifiedTreatedAsLax","SameSiteNoneInsecure","UserPreferences","ThirdPartyBlockedInFirstPartySet","SyntaxError","SchemeNotSupported","OverwriteSecure","InvalidDomain","InvalidPrefix","UnknownError","SchemefulSameSiteStrict","SchemefulSameSiteLax","SchemefulSameSiteUnspecifiedTreatedAsLax","SamePartyFromCrossPartyContext","SamePartyConflictsWithOtherAttributes","NameValuePairExceedsMaxSize"]},{"id":"CookieBlockedReason","description":"Types of reasons why a cookie may not be sent with a request.","experimental":true,"type":"string","enum":["SecureOnly","NotOnPath","DomainMismatch","SameSiteStrict","SameSiteLax","SameSiteUnspecifiedTreatedAsLax","SameSiteNoneInsecure","UserPreferences","ThirdPartyBlockedInFirstPartySet","UnknownError","SchemefulSameSiteStrict","SchemefulSameSiteLax","SchemefulSameSiteUnspecifiedTreatedAsLax","SamePartyFromCrossPartyContext","NameValuePairExceedsMaxSize"]},{"id":"BlockedSetCookieWithReason","description":"A cookie which was not stored from a response with the corresponding reason.","experimental":true,"type":"object","properties":[{"name":"blockedReasons","description":"The reason(s) this cookie was blocked.","type":"array","items":{"$ref":"SetCookieBlockedReason"}},{"name":"cookieLine","description":"The string representing this individual cookie as it would appear in the header.\\nThis is not the entire \\"cookie\\" or \\"set-cookie\\" header which could have multiple cookies.","type":"string"},{"name":"cookie","description":"The cookie object which represents the cookie which was not stored. It is optional because\\nsometimes complete cookie information is not available, such as in the case of parsing\\nerrors.","optional":true,"$ref":"Cookie"}]},{"id":"BlockedCookieWithReason","description":"A cookie with was not sent with a request with the corresponding reason.","experimental":true,"type":"object","properties":[{"name":"blockedReasons","description":"The reason(s) the cookie was blocked.","type":"array","items":{"$ref":"CookieBlockedReason"}},{"name":"cookie","description":"The cookie object representing the cookie which was not sent.","$ref":"Cookie"}]},{"id":"CookieParam","description":"Cookie parameter object","type":"object","properties":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"url","description":"The request-URI to associate with the setting of the cookie. This value can affect the\\ndefault domain, path, source port, and source scheme values of the created cookie.","optional":true,"type":"string"},{"name":"domain","description":"Cookie domain.","optional":true,"type":"string"},{"name":"path","description":"Cookie path.","optional":true,"type":"string"},{"name":"secure","description":"True if cookie is secure.","optional":true,"type":"boolean"},{"name":"httpOnly","description":"True if cookie is http-only.","optional":true,"type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"},{"name":"expires","description":"Cookie expiration date, session cookie if not set","optional":true,"$ref":"TimeSinceEpoch"},{"name":"priority","description":"Cookie Priority.","experimental":true,"optional":true,"$ref":"CookiePriority"},{"name":"sameParty","description":"True if cookie is SameParty.","experimental":true,"optional":true,"type":"boolean"},{"name":"sourceScheme","description":"Cookie source scheme type.","experimental":true,"optional":true,"$ref":"CookieSourceScheme"},{"name":"sourcePort","description":"Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\\nThis is a temporary ability and it will be removed in the future.","experimental":true,"optional":true,"type":"integer"},{"name":"partitionKey","description":"Cookie partition key. The site of the top-level URL the browser was visiting at the start\\nof the request to the endpoint that set the cookie.\\nIf not set, the cookie will be set as not partitioned.","experimental":true,"optional":true,"type":"string"}]},{"id":"AuthChallenge","description":"Authorization challenge for HTTP status code 401 or 407.","experimental":true,"type":"object","properties":[{"name":"source","description":"Source of the authentication challenge.","optional":true,"type":"string","enum":["Server","Proxy"]},{"name":"origin","description":"Origin of the challenger.","type":"string"},{"name":"scheme","description":"The authentication scheme used, such as basic or digest","type":"string"},{"name":"realm","description":"The realm of the challenge. May be empty.","type":"string"}]},{"id":"AuthChallengeResponse","description":"Response to an AuthChallenge.","experimental":true,"type":"object","properties":[{"name":"response","description":"The decision on what to do in response to the authorization challenge. Default means\\ndeferring to the default behavior of the net stack, which will likely either the Cancel\\nauthentication or display a popup dialog box.","type":"string","enum":["Default","CancelAuth","ProvideCredentials"]},{"name":"username","description":"The username to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"},{"name":"password","description":"The password to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"}]},{"id":"InterceptionStage","description":"Stages of the interception to begin intercepting. Request will intercept before the request is\\nsent. Response will intercept after the response is received.","experimental":true,"type":"string","enum":["Request","HeadersReceived"]},{"id":"RequestPattern","description":"Request pattern for interception.","experimental":true,"type":"object","properties":[{"name":"urlPattern","description":"Wildcards (`\'*\'` -> zero or more, `\'?\'` -> exactly one) are allowed. Escape character is\\nbackslash. Omitting is equivalent to `\\"*\\"`.","optional":true,"type":"string"},{"name":"resourceType","description":"If set, only requests for matching resource types will be intercepted.","optional":true,"$ref":"ResourceType"},{"name":"interceptionStage","description":"Stage at which to begin intercepting requests. Default is Request.","optional":true,"$ref":"InterceptionStage"}]},{"id":"SignedExchangeSignature","description":"Information about a signed exchange signature.\\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#rfc.section.3.1","experimental":true,"type":"object","properties":[{"name":"label","description":"Signed exchange signature label.","type":"string"},{"name":"signature","description":"The hex string of signed exchange signature.","type":"string"},{"name":"integrity","description":"Signed exchange signature integrity.","type":"string"},{"name":"certUrl","description":"Signed exchange signature cert Url.","optional":true,"type":"string"},{"name":"certSha256","description":"The hex string of signed exchange signature cert sha256.","optional":true,"type":"string"},{"name":"validityUrl","description":"Signed exchange signature validity Url.","type":"string"},{"name":"date","description":"Signed exchange signature date.","type":"integer"},{"name":"expires","description":"Signed exchange signature expires.","type":"integer"},{"name":"certificates","description":"The encoded certificates.","optional":true,"type":"array","items":{"type":"string"}}]},{"id":"SignedExchangeHeader","description":"Information about a signed exchange header.\\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cbor-representation","experimental":true,"type":"object","properties":[{"name":"requestUrl","description":"Signed exchange request URL.","type":"string"},{"name":"responseCode","description":"Signed exchange response code.","type":"integer"},{"name":"responseHeaders","description":"Signed exchange response headers.","$ref":"Headers"},{"name":"signatures","description":"Signed exchange response signature.","type":"array","items":{"$ref":"SignedExchangeSignature"}},{"name":"headerIntegrity","description":"Signed exchange header integrity hash in the form of `sha256-<base64-hash-value>`.","type":"string"}]},{"id":"SignedExchangeErrorField","description":"Field type for a signed exchange related error.","experimental":true,"type":"string","enum":["signatureSig","signatureIntegrity","signatureCertUrl","signatureCertSha256","signatureValidityUrl","signatureTimestamps"]},{"id":"SignedExchangeError","description":"Information about a signed exchange response.","experimental":true,"type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"signatureIndex","description":"The index of the signature which caused the error.","optional":true,"type":"integer"},{"name":"errorField","description":"The field which caused the error.","optional":true,"$ref":"SignedExchangeErrorField"}]},{"id":"SignedExchangeInfo","description":"Information about a signed exchange response.","experimental":true,"type":"object","properties":[{"name":"outerResponse","description":"The outer response of signed HTTP exchange which was received from network.","$ref":"Response"},{"name":"header","description":"Information about the signed exchange header.","optional":true,"$ref":"SignedExchangeHeader"},{"name":"securityDetails","description":"Security details for the signed exchange header.","optional":true,"$ref":"SecurityDetails"},{"name":"errors","description":"Errors occurred while handling the signed exchagne.","optional":true,"type":"array","items":{"$ref":"SignedExchangeError"}}]},{"id":"ContentEncoding","description":"List of content encodings supported by the backend.","experimental":true,"type":"string","enum":["deflate","gzip","br","zstd"]},{"id":"PrivateNetworkRequestPolicy","experimental":true,"type":"string","enum":["Allow","BlockFromInsecureToMorePrivate","WarnFromInsecureToMorePrivate","PreflightBlock","PreflightWarn"]},{"id":"IPAddressSpace","experimental":true,"type":"string","enum":["Local","Private","Public","Unknown"]},{"id":"ConnectTiming","experimental":true,"type":"object","properties":[{"name":"requestTime","description":"Timing\'s requestTime is a baseline in seconds, while the other numbers are ticks in\\nmilliseconds relatively to this requestTime. Matches ResourceTiming\'s requestTime for\\nthe same request (but not for redirected requests).","type":"number"}]},{"id":"ClientSecurityState","experimental":true,"type":"object","properties":[{"name":"initiatorIsSecureContext","type":"boolean"},{"name":"initiatorIPAddressSpace","$ref":"IPAddressSpace"},{"name":"privateNetworkRequestPolicy","$ref":"PrivateNetworkRequestPolicy"}]},{"id":"CrossOriginOpenerPolicyValue","experimental":true,"type":"string","enum":["SameOrigin","SameOriginAllowPopups","RestrictProperties","UnsafeNone","SameOriginPlusCoep","RestrictPropertiesPlusCoep"]},{"id":"CrossOriginOpenerPolicyStatus","experimental":true,"type":"object","properties":[{"name":"value","$ref":"CrossOriginOpenerPolicyValue"},{"name":"reportOnlyValue","$ref":"CrossOriginOpenerPolicyValue"},{"name":"reportingEndpoint","optional":true,"type":"string"},{"name":"reportOnlyReportingEndpoint","optional":true,"type":"string"}]},{"id":"CrossOriginEmbedderPolicyValue","experimental":true,"type":"string","enum":["None","Credentialless","RequireCorp"]},{"id":"CrossOriginEmbedderPolicyStatus","experimental":true,"type":"object","properties":[{"name":"value","$ref":"CrossOriginEmbedderPolicyValue"},{"name":"reportOnlyValue","$ref":"CrossOriginEmbedderPolicyValue"},{"name":"reportingEndpoint","optional":true,"type":"string"},{"name":"reportOnlyReportingEndpoint","optional":true,"type":"string"}]},{"id":"ContentSecurityPolicySource","experimental":true,"type":"string","enum":["HTTP","Meta"]},{"id":"ContentSecurityPolicyStatus","experimental":true,"type":"object","properties":[{"name":"effectiveDirectives","type":"string"},{"name":"isEnforced","type":"boolean"},{"name":"source","$ref":"ContentSecurityPolicySource"}]},{"id":"SecurityIsolationStatus","experimental":true,"type":"object","properties":[{"name":"coop","optional":true,"$ref":"CrossOriginOpenerPolicyStatus"},{"name":"coep","optional":true,"$ref":"CrossOriginEmbedderPolicyStatus"},{"name":"csp","optional":true,"type":"array","items":{"$ref":"ContentSecurityPolicyStatus"}}]},{"id":"ReportStatus","description":"The status of a Reporting API report.","experimental":true,"type":"string","enum":["Queued","Pending","MarkedForRemoval","Success"]},{"id":"ReportId","experimental":true,"type":"string"},{"id":"ReportingApiReport","description":"An object representing a report generated by the Reporting API.","experimental":true,"type":"object","properties":[{"name":"id","$ref":"ReportId"},{"name":"initiatorUrl","description":"The URL of the document that triggered the report.","type":"string"},{"name":"destination","description":"The name of the endpoint group that should be used to deliver the report.","type":"string"},{"name":"type","description":"The type of the report (specifies the set of data that is contained in the report body).","type":"string"},{"name":"timestamp","description":"When the report was generated.","$ref":"Network.TimeSinceEpoch"},{"name":"depth","description":"How many uploads deep the related request was.","type":"integer"},{"name":"completedAttempts","description":"The number of delivery attempts made so far, not including an active attempt.","type":"integer"},{"name":"body","type":"object"},{"name":"status","$ref":"ReportStatus"}]},{"id":"ReportingApiEndpoint","experimental":true,"type":"object","properties":[{"name":"url","description":"The URL of the endpoint to which reports may be delivered.","type":"string"},{"name":"groupName","description":"Name of the endpoint group.","type":"string"}]},{"id":"LoadNetworkResourcePageResult","description":"An object providing the result of a network resource load.","experimental":true,"type":"object","properties":[{"name":"success","type":"boolean"},{"name":"netError","description":"Optional values used for error reporting.","optional":true,"type":"number"},{"name":"netErrorName","optional":true,"type":"string"},{"name":"httpStatusCode","optional":true,"type":"number"},{"name":"stream","description":"If successful, one of the following two fields holds the result.","optional":true,"$ref":"IO.StreamHandle"},{"name":"headers","description":"Response headers.","optional":true,"$ref":"Network.Headers"}]},{"id":"LoadNetworkResourceOptions","description":"An options object that may be extended later to better support CORS,\\nCORB and streaming.","experimental":true,"type":"object","properties":[{"name":"disableCache","type":"boolean"},{"name":"includeCredentials","type":"boolean"}]}],"commands":[{"name":"setAcceptedEncodings","description":"Sets a list of content encodings that will be accepted. Empty list means no encoding is accepted.","experimental":true,"parameters":[{"name":"encodings","description":"List of accepted content encodings.","type":"array","items":{"$ref":"ContentEncoding"}}]},{"name":"clearAcceptedEncodingsOverride","description":"Clears accepted encodings set by setAcceptedEncodings","experimental":true},{"name":"canClearBrowserCache","description":"Tells whether clearing browser cache is supported.","deprecated":true,"returns":[{"name":"result","description":"True if browser cache can be cleared.","type":"boolean"}]},{"name":"canClearBrowserCookies","description":"Tells whether clearing browser cookies is supported.","deprecated":true,"returns":[{"name":"result","description":"True if browser cookies can be cleared.","type":"boolean"}]},{"name":"canEmulateNetworkConditions","description":"Tells whether emulation of network conditions is supported.","deprecated":true,"returns":[{"name":"result","description":"True if emulation of network conditions is supported.","type":"boolean"}]},{"name":"clearBrowserCache","description":"Clears browser cache."},{"name":"clearBrowserCookies","description":"Clears browser cookies."},{"name":"continueInterceptedRequest","description":"Response to Network.requestIntercepted which either modifies the request to continue with any\\nmodifications, or blocks it, or completes it with the provided response bytes. If a network\\nfetch occurs as a result which encounters a redirect an additional Network.requestIntercepted\\nevent will be sent with the same InterceptionId.\\nDeprecated, use Fetch.continueRequest, Fetch.fulfillRequest and Fetch.failRequest instead.","experimental":true,"deprecated":true,"parameters":[{"name":"interceptionId","$ref":"InterceptionId"},{"name":"errorReason","description":"If set this causes the request to fail with the given reason. Passing `Aborted` for requests\\nmarked with `isNavigationRequest` also cancels the navigation. Must not be set in response\\nto an authChallenge.","optional":true,"$ref":"ErrorReason"},{"name":"rawResponse","description":"If set the requests completes using with the provided base64 encoded raw response, including\\nHTTP status line and headers etc... Must not be set in response to an authChallenge. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"url","description":"If set the request url will be modified in a way that\'s not observable by page. Must not be\\nset in response to an authChallenge.","optional":true,"type":"string"},{"name":"method","description":"If set this allows the request method to be overridden. Must not be set in response to an\\nauthChallenge.","optional":true,"type":"string"},{"name":"postData","description":"If set this allows postData to be set. Must not be set in response to an authChallenge.","optional":true,"type":"string"},{"name":"headers","description":"If set this allows the request headers to be changed. Must not be set in response to an\\nauthChallenge.","optional":true,"$ref":"Headers"},{"name":"authChallengeResponse","description":"Response to a requestIntercepted with an authChallenge. Must not be set otherwise.","optional":true,"$ref":"AuthChallengeResponse"}]},{"name":"deleteCookies","description":"Deletes browser cookies with matching name and url or domain/path pair.","parameters":[{"name":"name","description":"Name of the cookies to remove.","type":"string"},{"name":"url","description":"If specified, deletes all the cookies with the given name where domain and path match\\nprovided URL.","optional":true,"type":"string"},{"name":"domain","description":"If specified, deletes only cookies with the exact domain.","optional":true,"type":"string"},{"name":"path","description":"If specified, deletes only cookies with the exact path.","optional":true,"type":"string"}]},{"name":"disable","description":"Disables network tracking, prevents network events from being sent to the client."},{"name":"emulateNetworkConditions","description":"Activates emulation of network conditions.","parameters":[{"name":"offline","description":"True to emulate internet disconnection.","type":"boolean"},{"name":"latency","description":"Minimum latency from request sent to response headers received (ms).","type":"number"},{"name":"downloadThroughput","description":"Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.","type":"number"},{"name":"uploadThroughput","description":"Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.","type":"number"},{"name":"connectionType","description":"Connection type if known.","optional":true,"$ref":"ConnectionType"}]},{"name":"enable","description":"Enables network tracking, network events will now be delivered to the client.","parameters":[{"name":"maxTotalBufferSize","description":"Buffer size in bytes to use when preserving network payloads (XHRs, etc).","experimental":true,"optional":true,"type":"integer"},{"name":"maxResourceBufferSize","description":"Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).","experimental":true,"optional":true,"type":"integer"},{"name":"maxPostDataSize","description":"Longest post body size (in bytes) that would be included in requestWillBeSent notification","optional":true,"type":"integer"}]},{"name":"getAllCookies","description":"Returns all browser cookies. Depending on the backend support, will return detailed cookie\\ninformation in the `cookies` field.\\nDeprecated. Use Storage.getCookies instead.","deprecated":true,"returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Cookie"}}]},{"name":"getCertificate","description":"Returns the DER-encoded certificate.","experimental":true,"parameters":[{"name":"origin","description":"Origin to get certificate for.","type":"string"}],"returns":[{"name":"tableNames","type":"array","items":{"type":"string"}}]},{"name":"getCookies","description":"Returns all browser cookies for the current URL. Depending on the backend support, will return\\ndetailed cookie information in the `cookies` field.","parameters":[{"name":"urls","description":"The list of URLs for which applicable cookies will be fetched.\\nIf not specified, it\'s assumed to be set to the list containing\\nthe URLs of the page and all of its subframes.","optional":true,"type":"array","items":{"type":"string"}}],"returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Cookie"}}]},{"name":"getResponseBody","description":"Returns content served for the given request.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"RequestId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"getRequestPostData","description":"Returns post data sent with the request. Returns an error when no data was sent with the request.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"RequestId"}],"returns":[{"name":"postData","description":"Request body string, omitting files from multipart requests","type":"string"}]},{"name":"getResponseBodyForInterception","description":"Returns content served for the given currently intercepted request.","experimental":true,"parameters":[{"name":"interceptionId","description":"Identifier for the intercepted request to get body for.","$ref":"InterceptionId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"takeResponseBodyForInterceptionAsStream","description":"Returns a handle to the stream representing the response body. Note that after this command,\\nthe intercepted request can\'t be continued as is -- you either need to cancel it or to provide\\nthe response body. The stream only supports sequential read, IO.read will fail if the position\\nis specified.","experimental":true,"parameters":[{"name":"interceptionId","$ref":"InterceptionId"}],"returns":[{"name":"stream","$ref":"IO.StreamHandle"}]},{"name":"replayXHR","description":"This method sends a new XMLHttpRequest which is identical to the original one. The following\\nparameters should be identical: method, url, async, request body, extra headers, withCredentials\\nattribute, user, password.","experimental":true,"parameters":[{"name":"requestId","description":"Identifier of XHR to replay.","$ref":"RequestId"}]},{"name":"searchInResponseBody","description":"Searches for given string in response content.","experimental":true,"parameters":[{"name":"requestId","description":"Identifier of the network response to search.","$ref":"RequestId"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"Debugger.SearchMatch"}}]},{"name":"setBlockedURLs","description":"Blocks URLs from loading.","experimental":true,"parameters":[{"name":"urls","description":"URL patterns to block. Wildcards (\'*\') are allowed.","type":"array","items":{"type":"string"}}]},{"name":"setBypassServiceWorker","description":"Toggles ignoring of service worker for each request.","experimental":true,"parameters":[{"name":"bypass","description":"Bypass service worker and load from network.","type":"boolean"}]},{"name":"setCacheDisabled","description":"Toggles ignoring cache for each request. If `true`, cache will not be used.","parameters":[{"name":"cacheDisabled","description":"Cache disabled state.","type":"boolean"}]},{"name":"setCookie","description":"Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.","parameters":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"url","description":"The request-URI to associate with the setting of the cookie. This value can affect the\\ndefault domain, path, source port, and source scheme values of the created cookie.","optional":true,"type":"string"},{"name":"domain","description":"Cookie domain.","optional":true,"type":"string"},{"name":"path","description":"Cookie path.","optional":true,"type":"string"},{"name":"secure","description":"True if cookie is secure.","optional":true,"type":"boolean"},{"name":"httpOnly","description":"True if cookie is http-only.","optional":true,"type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"},{"name":"expires","description":"Cookie expiration date, session cookie if not set","optional":true,"$ref":"TimeSinceEpoch"},{"name":"priority","description":"Cookie Priority type.","experimental":true,"optional":true,"$ref":"CookiePriority"},{"name":"sameParty","description":"True if cookie is SameParty.","experimental":true,"optional":true,"type":"boolean"},{"name":"sourceScheme","description":"Cookie source scheme type.","experimental":true,"optional":true,"$ref":"CookieSourceScheme"},{"name":"sourcePort","description":"Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\\nThis is a temporary ability and it will be removed in the future.","experimental":true,"optional":true,"type":"integer"},{"name":"partitionKey","description":"Cookie partition key. The site of the top-level URL the browser was visiting at the start\\nof the request to the endpoint that set the cookie.\\nIf not set, the cookie will be set as not partitioned.","experimental":true,"optional":true,"type":"string"}],"returns":[{"name":"success","description":"Always set to true. If an error occurs, the response indicates protocol error.","deprecated":true,"type":"boolean"}]},{"name":"setCookies","description":"Sets given cookies.","parameters":[{"name":"cookies","description":"Cookies to be set.","type":"array","items":{"$ref":"CookieParam"}}]},{"name":"setExtraHTTPHeaders","description":"Specifies whether to always send extra HTTP headers with the requests from this page.","parameters":[{"name":"headers","description":"Map with extra HTTP headers.","$ref":"Headers"}]},{"name":"setAttachDebugStack","description":"Specifies whether to attach a page script stack id in requests","experimental":true,"parameters":[{"name":"enabled","description":"Whether to attach a page script stack for debugging purpose.","type":"boolean"}]},{"name":"setRequestInterception","description":"Sets the requests to intercept that match the provided patterns and optionally resource types.\\nDeprecated, please use Fetch.enable instead.","experimental":true,"deprecated":true,"parameters":[{"name":"patterns","description":"Requests matching any of these patterns will be forwarded and wait for the corresponding\\ncontinueInterceptedRequest call.","type":"array","items":{"$ref":"RequestPattern"}}]},{"name":"setUserAgentOverride","description":"Allows overriding user agent with the given string.","redirect":"Emulation","parameters":[{"name":"userAgent","description":"User agent to use.","type":"string"},{"name":"acceptLanguage","description":"Browser langugage to emulate.","optional":true,"type":"string"},{"name":"platform","description":"The platform navigator.platform should return.","optional":true,"type":"string"},{"name":"userAgentMetadata","description":"To be sent in Sec-CH-UA-* headers and returned in navigator.userAgentData","experimental":true,"optional":true,"$ref":"Emulation.UserAgentMetadata"}]},{"name":"getSecurityIsolationStatus","description":"Returns information about the COEP/COOP isolation status.","experimental":true,"parameters":[{"name":"frameId","description":"If no frameId is provided, the status of the target is provided.","optional":true,"$ref":"Page.FrameId"}],"returns":[{"name":"status","$ref":"SecurityIsolationStatus"}]},{"name":"enableReportingApi","description":"Enables tracking for the Reporting API, events generated by the Reporting API will now be delivered to the client.\\nEnabling triggers \'reportingApiReportAdded\' for all existing reports.","experimental":true,"parameters":[{"name":"enable","description":"Whether to enable or disable events for the Reporting API","type":"boolean"}]},{"name":"loadNetworkResource","description":"Fetches the resource and returns the content.","experimental":true,"parameters":[{"name":"frameId","description":"Frame id to get the resource for. Mandatory for frame targets, and\\nshould be omitted for worker targets.","optional":true,"$ref":"Page.FrameId"},{"name":"url","description":"URL of the resource to get content for.","type":"string"},{"name":"options","description":"Options for the request.","$ref":"LoadNetworkResourceOptions"}],"returns":[{"name":"resource","$ref":"LoadNetworkResourcePageResult"}]}],"events":[{"name":"dataReceived","description":"Fired when data chunk was received over the network.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"dataLength","description":"Data chunk length.","type":"integer"},{"name":"encodedDataLength","description":"Actual bytes received (might be less than dataLength for compressed encodings).","type":"integer"}]},{"name":"eventSourceMessageReceived","description":"Fired when EventSource message is received.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"eventName","description":"Message type.","type":"string"},{"name":"eventId","description":"Message identifier.","type":"string"},{"name":"data","description":"Message content.","type":"string"}]},{"name":"loadingFailed","description":"Fired when HTTP request has failed to load.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"type","description":"Resource type.","$ref":"ResourceType"},{"name":"errorText","description":"User friendly error message.","type":"string"},{"name":"canceled","description":"True if loading was canceled.","optional":true,"type":"boolean"},{"name":"blockedReason","description":"The reason why loading was blocked, if any.","optional":true,"$ref":"BlockedReason"},{"name":"corsErrorStatus","description":"The reason why loading was blocked by CORS, if any.","optional":true,"$ref":"CorsErrorStatus"}]},{"name":"loadingFinished","description":"Fired when HTTP request has finished loading.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"encodedDataLength","description":"Total number of bytes received for this request.","type":"number"}]},{"name":"requestIntercepted","description":"Details of an intercepted HTTP request, which must be either allowed, blocked, modified or\\nmocked.\\nDeprecated, use Fetch.requestPaused instead.","experimental":true,"deprecated":true,"parameters":[{"name":"interceptionId","description":"Each request the page makes will have a unique id, however if any redirects are encountered\\nwhile processing that fetch, they will be reported with the same id as the original fetch.\\nLikewise if HTTP authentication is needed then the same fetch id will be used.","$ref":"InterceptionId"},{"name":"request","$ref":"Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"ResourceType"},{"name":"isNavigationRequest","description":"Whether this is a navigation request, which can abort the navigation completely.","type":"boolean"},{"name":"isDownload","description":"Set if the request is a navigation that will result in a download.\\nOnly present after response is received from the server (i.e. HeadersReceived stage).","optional":true,"type":"boolean"},{"name":"redirectUrl","description":"Redirect location, only sent if a redirect was intercepted.","optional":true,"type":"string"},{"name":"authChallenge","description":"Details of the Authorization Challenge encountered. If this is set then\\ncontinueInterceptedRequest must contain an authChallengeResponse.","optional":true,"$ref":"AuthChallenge"},{"name":"responseErrorReason","description":"Response error if intercepted at response stage or if redirect occurred while intercepting\\nrequest.","optional":true,"$ref":"ErrorReason"},{"name":"responseStatusCode","description":"Response code if intercepted at response stage or if redirect occurred while intercepting\\nrequest or auth retry occurred.","optional":true,"type":"integer"},{"name":"responseHeaders","description":"Response headers if intercepted at the response stage or if redirect occurred while\\nintercepting request or auth retry occurred.","optional":true,"$ref":"Headers"},{"name":"requestId","description":"If the intercepted request had a corresponding requestWillBeSent event fired for it, then\\nthis requestId will be the same as the requestId present in the requestWillBeSent event.","optional":true,"$ref":"RequestId"}]},{"name":"requestServedFromCache","description":"Fired if request ended up loading from cache.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"}]},{"name":"requestWillBeSent","description":"Fired when page is about to send HTTP request.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"LoaderId"},{"name":"documentURL","description":"URL of the document this request is loaded for.","type":"string"},{"name":"request","description":"Request data.","$ref":"Request"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"wallTime","description":"Timestamp.","$ref":"TimeSinceEpoch"},{"name":"initiator","description":"Request initiator.","$ref":"Initiator"},{"name":"redirectHasExtraInfo","description":"In the case that redirectResponse is populated, this flag indicates whether\\nrequestWillBeSentExtraInfo and responseReceivedExtraInfo events will be or were emitted\\nfor the request which was just redirected.","experimental":true,"type":"boolean"},{"name":"redirectResponse","description":"Redirect response data.","optional":true,"$ref":"Response"},{"name":"type","description":"Type of this resource.","optional":true,"$ref":"ResourceType"},{"name":"frameId","description":"Frame identifier.","optional":true,"$ref":"Page.FrameId"},{"name":"hasUserGesture","description":"Whether the request is initiated by a user gesture. Defaults to false.","optional":true,"type":"boolean"}]},{"name":"resourceChangedPriority","description":"Fired when resource loading priority is changed","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"newPriority","description":"New priority","$ref":"ResourcePriority"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"signedExchangeReceived","description":"Fired when a signed exchange was received over the network","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"info","description":"Information about the signed exchange response.","$ref":"SignedExchangeInfo"}]},{"name":"responseReceived","description":"Fired when HTTP response is available.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"LoaderId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"type","description":"Resource type.","$ref":"ResourceType"},{"name":"response","description":"Response data.","$ref":"Response"},{"name":"hasExtraInfo","description":"Indicates whether requestWillBeSentExtraInfo and responseReceivedExtraInfo events will be\\nor were emitted for this request.","experimental":true,"type":"boolean"},{"name":"frameId","description":"Frame identifier.","optional":true,"$ref":"Page.FrameId"}]},{"name":"webSocketClosed","description":"Fired when WebSocket is closed.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"webSocketCreated","description":"Fired upon WebSocket creation.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"url","description":"WebSocket request URL.","type":"string"},{"name":"initiator","description":"Request initiator.","optional":true,"$ref":"Initiator"}]},{"name":"webSocketFrameError","description":"Fired when WebSocket message error occurs.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"errorMessage","description":"WebSocket error message.","type":"string"}]},{"name":"webSocketFrameReceived","description":"Fired when WebSocket message is received.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketFrame"}]},{"name":"webSocketFrameSent","description":"Fired when WebSocket message is sent.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketFrame"}]},{"name":"webSocketHandshakeResponseReceived","description":"Fired when WebSocket handshake response becomes available.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketResponse"}]},{"name":"webSocketWillSendHandshakeRequest","description":"Fired when WebSocket is about to initiate handshake.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"wallTime","description":"UTC Timestamp.","$ref":"TimeSinceEpoch"},{"name":"request","description":"WebSocket request data.","$ref":"WebSocketRequest"}]},{"name":"webTransportCreated","description":"Fired upon WebTransport creation.","parameters":[{"name":"transportId","description":"WebTransport identifier.","$ref":"RequestId"},{"name":"url","description":"WebTransport request URL.","type":"string"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"initiator","description":"Request initiator.","optional":true,"$ref":"Initiator"}]},{"name":"webTransportConnectionEstablished","description":"Fired when WebTransport handshake is finished.","parameters":[{"name":"transportId","description":"WebTransport identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"webTransportClosed","description":"Fired when WebTransport is disposed.","parameters":[{"name":"transportId","description":"WebTransport identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"requestWillBeSentExtraInfo","description":"Fired when additional information about a requestWillBeSent event is available from the\\nnetwork stack. Not every requestWillBeSent event will have an additional\\nrequestWillBeSentExtraInfo fired for it, and there is no guarantee whether requestWillBeSent\\nor requestWillBeSentExtraInfo will be fired first for the same request.","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier. Used to match this information to an existing requestWillBeSent event.","$ref":"RequestId"},{"name":"associatedCookies","description":"A list of cookies potentially associated to the requested URL. This includes both cookies sent with\\nthe request and the ones not sent; the latter are distinguished by having blockedReason field set.","type":"array","items":{"$ref":"BlockedCookieWithReason"}},{"name":"headers","description":"Raw request headers as they will be sent over the wire.","$ref":"Headers"},{"name":"connectTiming","description":"Connection timing information for the request.","experimental":true,"$ref":"ConnectTiming"},{"name":"clientSecurityState","description":"The client security state set for the request.","optional":true,"$ref":"ClientSecurityState"},{"name":"siteHasCookieInOtherPartition","description":"Whether the site has partitioned cookies stored in a partition different than the current one.","optional":true,"type":"boolean"}]},{"name":"responseReceivedExtraInfo","description":"Fired when additional information about a responseReceived event is available from the network\\nstack. Not every responseReceived event will have an additional responseReceivedExtraInfo for\\nit, and responseReceivedExtraInfo may be fired before or after responseReceived.","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier. Used to match this information to another responseReceived event.","$ref":"RequestId"},{"name":"blockedCookies","description":"A list of cookies which were not stored from the response along with the corresponding\\nreasons for blocking. The cookies here may not be valid due to syntax errors, which\\nare represented by the invalid cookie line string instead of a proper cookie.","type":"array","items":{"$ref":"BlockedSetCookieWithReason"}},{"name":"headers","description":"Raw response headers as they were received over the wire.","$ref":"Headers"},{"name":"resourceIPAddressSpace","description":"The IP address space of the resource. The address space can only be determined once the transport\\nestablished the connection, so we can\'t send it in `requestWillBeSentExtraInfo`.","$ref":"IPAddressSpace"},{"name":"statusCode","description":"The status code of the response. This is useful in cases the request failed and no responseReceived\\nevent is triggered, which is the case for, e.g., CORS errors. This is also the correct status code\\nfor cached requests, where the status in responseReceived is a 200 and this will be 304.","type":"integer"},{"name":"headersText","description":"Raw response header text as it was received over the wire. The raw text may not always be\\navailable, such as in the case of HTTP/2 or QUIC.","optional":true,"type":"string"},{"name":"cookiePartitionKey","description":"The cookie partition key that will be used to store partitioned cookies set in this response.\\nOnly sent when partitioned cookies are enabled.","optional":true,"type":"string"},{"name":"cookiePartitionKeyOpaque","description":"True if partitioned cookies are enabled, but the partition key is not serializeable to string.","optional":true,"type":"boolean"}]},{"name":"trustTokenOperationDone","description":"Fired exactly once for each Trust Token operation. Depending on\\nthe type of the operation and whether the operation succeeded or\\nfailed, the event is fired before the corresponding request was sent\\nor after the response was received.","experimental":true,"parameters":[{"name":"status","description":"Detailed success or error status of the operation.\\n\'AlreadyExists\' also signifies a successful operation, as the result\\nof the operation already exists und thus, the operation was abort\\npreemptively (e.g. a cache hit).","type":"string","enum":["Ok","InvalidArgument","MissingIssuerKeys","FailedPrecondition","ResourceExhausted","AlreadyExists","Unavailable","Unauthorized","BadResponse","InternalError","UnknownError","FulfilledLocally"]},{"name":"type","$ref":"TrustTokenOperationType"},{"name":"requestId","$ref":"RequestId"},{"name":"topLevelOrigin","description":"Top level origin. The context in which the operation was attempted.","optional":true,"type":"string"},{"name":"issuerOrigin","description":"Origin of the issuer in case of a \\"Issuance\\" or \\"Redemption\\" operation.","optional":true,"type":"string"},{"name":"issuedTokenCount","description":"The number of obtained Trust Tokens on a successful \\"Issuance\\" operation.","optional":true,"type":"integer"}]},{"name":"subresourceWebBundleMetadataReceived","description":"Fired once when parsing the .wbn file has succeeded.\\nThe event contains the information about the web bundle contents.","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier. Used to match this information to another event.","$ref":"RequestId"},{"name":"urls","description":"A list of URLs of resources in the subresource Web Bundle.","type":"array","items":{"type":"string"}}]},{"name":"subresourceWebBundleMetadataError","description":"Fired once when parsing the .wbn file has failed.","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier. Used to match this information to another event.","$ref":"RequestId"},{"name":"errorMessage","description":"Error message","type":"string"}]},{"name":"subresourceWebBundleInnerResponseParsed","description":"Fired when handling requests for resources within a .wbn file.\\nNote: this will only be fired for resources that are requested by the webpage.","experimental":true,"parameters":[{"name":"innerRequestId","description":"Request identifier of the subresource request","$ref":"RequestId"},{"name":"innerRequestURL","description":"URL of the subresource resource.","type":"string"},{"name":"bundleRequestId","description":"Bundle request identifier. Used to match this information to another event.\\nThis made be absent in case when the instrumentation was enabled only\\nafter webbundle was parsed.","optional":true,"$ref":"RequestId"}]},{"name":"subresourceWebBundleInnerResponseError","description":"Fired when request for resources within a .wbn file failed.","experimental":true,"parameters":[{"name":"innerRequestId","description":"Request identifier of the subresource request","$ref":"RequestId"},{"name":"innerRequestURL","description":"URL of the subresource resource.","type":"string"},{"name":"errorMessage","description":"Error message","type":"string"},{"name":"bundleRequestId","description":"Bundle request identifier. Used to match this information to another event.\\nThis made be absent in case when the instrumentation was enabled only\\nafter webbundle was parsed.","optional":true,"$ref":"RequestId"}]},{"name":"reportingApiReportAdded","description":"Is sent whenever a new report is added.\\nAnd after \'enableReportingApi\' for all existing reports.","experimental":true,"parameters":[{"name":"report","$ref":"ReportingApiReport"}]},{"name":"reportingApiReportUpdated","experimental":true,"parameters":[{"name":"report","$ref":"ReportingApiReport"}]},{"name":"reportingApiEndpointsChangedForOrigin","experimental":true,"parameters":[{"name":"origin","description":"Origin of the document(s) which configured the endpoints.","type":"string"},{"name":"endpoints","type":"array","items":{"$ref":"ReportingApiEndpoint"}}]}]},{"domain":"Overlay","description":"This domain provides various functionality related to drawing atop the inspected page.","experimental":true,"dependencies":["DOM","Page","Runtime"],"types":[{"id":"SourceOrderConfig","description":"Configuration data for drawing the source order of an elements children.","type":"object","properties":[{"name":"parentOutlineColor","description":"the color to outline the givent element in.","$ref":"DOM.RGBA"},{"name":"childOutlineColor","description":"the color to outline the child elements in.","$ref":"DOM.RGBA"}]},{"id":"GridHighlightConfig","description":"Configuration data for the highlighting of Grid elements.","type":"object","properties":[{"name":"showGridExtensionLines","description":"Whether the extension lines from grid cells to the rulers should be shown (default: false).","optional":true,"type":"boolean"},{"name":"showPositiveLineNumbers","description":"Show Positive line number labels (default: false).","optional":true,"type":"boolean"},{"name":"showNegativeLineNumbers","description":"Show Negative line number labels (default: false).","optional":true,"type":"boolean"},{"name":"showAreaNames","description":"Show area name labels (default: false).","optional":true,"type":"boolean"},{"name":"showLineNames","description":"Show line name labels (default: false).","optional":true,"type":"boolean"},{"name":"showTrackSizes","description":"Show track size labels (default: false).","optional":true,"type":"boolean"},{"name":"gridBorderColor","description":"The grid container border highlight color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"cellBorderColor","description":"The cell border color (default: transparent). Deprecated, please use rowLineColor and columnLineColor instead.","deprecated":true,"optional":true,"$ref":"DOM.RGBA"},{"name":"rowLineColor","description":"The row line color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"columnLineColor","description":"The column line color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"gridBorderDash","description":"Whether the grid border is dashed (default: false).","optional":true,"type":"boolean"},{"name":"cellBorderDash","description":"Whether the cell border is dashed (default: false). Deprecated, please us rowLineDash and columnLineDash instead.","deprecated":true,"optional":true,"type":"boolean"},{"name":"rowLineDash","description":"Whether row lines are dashed (default: false).","optional":true,"type":"boolean"},{"name":"columnLineDash","description":"Whether column lines are dashed (default: false).","optional":true,"type":"boolean"},{"name":"rowGapColor","description":"The row gap highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"rowHatchColor","description":"The row gap hatching fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"columnGapColor","description":"The column gap highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"columnHatchColor","description":"The column gap hatching fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"areaBorderColor","description":"The named grid areas border color (Default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"gridBackgroundColor","description":"The grid container background color (Default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"id":"FlexContainerHighlightConfig","description":"Configuration data for the highlighting of Flex container elements.","type":"object","properties":[{"name":"containerBorder","description":"The style of the container border","optional":true,"$ref":"LineStyle"},{"name":"lineSeparator","description":"The style of the separator between lines","optional":true,"$ref":"LineStyle"},{"name":"itemSeparator","description":"The style of the separator between items","optional":true,"$ref":"LineStyle"},{"name":"mainDistributedSpace","description":"Style of content-distribution space on the main axis (justify-content).","optional":true,"$ref":"BoxStyle"},{"name":"crossDistributedSpace","description":"Style of content-distribution space on the cross axis (align-content).","optional":true,"$ref":"BoxStyle"},{"name":"rowGapSpace","description":"Style of empty space caused by row gaps (gap/row-gap).","optional":true,"$ref":"BoxStyle"},{"name":"columnGapSpace","description":"Style of empty space caused by columns gaps (gap/column-gap).","optional":true,"$ref":"BoxStyle"},{"name":"crossAlignment","description":"Style of the self-alignment line (align-items).","optional":true,"$ref":"LineStyle"}]},{"id":"FlexItemHighlightConfig","description":"Configuration data for the highlighting of Flex item elements.","type":"object","properties":[{"name":"baseSizeBox","description":"Style of the box representing the item\'s base size","optional":true,"$ref":"BoxStyle"},{"name":"baseSizeBorder","description":"Style of the border around the box representing the item\'s base size","optional":true,"$ref":"LineStyle"},{"name":"flexibilityArrow","description":"Style of the arrow representing if the item grew or shrank","optional":true,"$ref":"LineStyle"}]},{"id":"LineStyle","description":"Style information for drawing a line.","type":"object","properties":[{"name":"color","description":"The color of the line (default: transparent)","optional":true,"$ref":"DOM.RGBA"},{"name":"pattern","description":"The line pattern (default: solid)","optional":true,"type":"string","enum":["dashed","dotted"]}]},{"id":"BoxStyle","description":"Style information for drawing a box.","type":"object","properties":[{"name":"fillColor","description":"The background color for the box (default: transparent)","optional":true,"$ref":"DOM.RGBA"},{"name":"hatchColor","description":"The hatching color for the box (default: transparent)","optional":true,"$ref":"DOM.RGBA"}]},{"id":"ContrastAlgorithm","type":"string","enum":["aa","aaa","apca"]},{"id":"HighlightConfig","description":"Configuration data for the highlighting of page elements.","type":"object","properties":[{"name":"showInfo","description":"Whether the node info tooltip should be shown (default: false).","optional":true,"type":"boolean"},{"name":"showStyles","description":"Whether the node styles in the tooltip (default: false).","optional":true,"type":"boolean"},{"name":"showRulers","description":"Whether the rulers should be shown (default: false).","optional":true,"type":"boolean"},{"name":"showAccessibilityInfo","description":"Whether the a11y info should be shown (default: true).","optional":true,"type":"boolean"},{"name":"showExtensionLines","description":"Whether the extension lines from node to the rulers should be shown (default: false).","optional":true,"type":"boolean"},{"name":"contentColor","description":"The content box highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"paddingColor","description":"The padding highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"borderColor","description":"The border highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"marginColor","description":"The margin highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"eventTargetColor","description":"The event target element highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"shapeColor","description":"The shape outside fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"shapeMarginColor","description":"The shape margin fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"cssGridColor","description":"The grid layout color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"colorFormat","description":"The color format used to format color styles (default: hex).","optional":true,"$ref":"ColorFormat"},{"name":"gridHighlightConfig","description":"The grid layout highlight configuration (default: all transparent).","optional":true,"$ref":"GridHighlightConfig"},{"name":"flexContainerHighlightConfig","description":"The flex container highlight configuration (default: all transparent).","optional":true,"$ref":"FlexContainerHighlightConfig"},{"name":"flexItemHighlightConfig","description":"The flex item highlight configuration (default: all transparent).","optional":true,"$ref":"FlexItemHighlightConfig"},{"name":"contrastAlgorithm","description":"The contrast algorithm to use for the contrast ratio (default: aa).","optional":true,"$ref":"ContrastAlgorithm"},{"name":"containerQueryContainerHighlightConfig","description":"The container query container highlight configuration (default: all transparent).","optional":true,"$ref":"ContainerQueryContainerHighlightConfig"}]},{"id":"ColorFormat","type":"string","enum":["rgb","hsl","hwb","hex"]},{"id":"GridNodeHighlightConfig","description":"Configurations for Persistent Grid Highlight","type":"object","properties":[{"name":"gridHighlightConfig","description":"A descriptor for the highlight appearance.","$ref":"GridHighlightConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","$ref":"DOM.NodeId"}]},{"id":"FlexNodeHighlightConfig","type":"object","properties":[{"name":"flexContainerHighlightConfig","description":"A descriptor for the highlight appearance of flex containers.","$ref":"FlexContainerHighlightConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","$ref":"DOM.NodeId"}]},{"id":"ScrollSnapContainerHighlightConfig","type":"object","properties":[{"name":"snapportBorder","description":"The style of the snapport border (default: transparent)","optional":true,"$ref":"LineStyle"},{"name":"snapAreaBorder","description":"The style of the snap area border (default: transparent)","optional":true,"$ref":"LineStyle"},{"name":"scrollMarginColor","description":"The margin highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"scrollPaddingColor","description":"The padding highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"id":"ScrollSnapHighlightConfig","type":"object","properties":[{"name":"scrollSnapContainerHighlightConfig","description":"A descriptor for the highlight appearance of scroll snap containers.","$ref":"ScrollSnapContainerHighlightConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","$ref":"DOM.NodeId"}]},{"id":"HingeConfig","description":"Configuration for dual screen hinge","type":"object","properties":[{"name":"rect","description":"A rectangle represent hinge","$ref":"DOM.Rect"},{"name":"contentColor","description":"The content box highlight fill color (default: a dark color).","optional":true,"$ref":"DOM.RGBA"},{"name":"outlineColor","description":"The content box highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"id":"ContainerQueryHighlightConfig","type":"object","properties":[{"name":"containerQueryContainerHighlightConfig","description":"A descriptor for the highlight appearance of container query containers.","$ref":"ContainerQueryContainerHighlightConfig"},{"name":"nodeId","description":"Identifier of the container node to highlight.","$ref":"DOM.NodeId"}]},{"id":"ContainerQueryContainerHighlightConfig","type":"object","properties":[{"name":"containerBorder","description":"The style of the container border.","optional":true,"$ref":"LineStyle"},{"name":"descendantBorder","description":"The style of the descendants\' borders.","optional":true,"$ref":"LineStyle"}]},{"id":"IsolatedElementHighlightConfig","type":"object","properties":[{"name":"isolationModeHighlightConfig","description":"A descriptor for the highlight appearance of an element in isolation mode.","$ref":"IsolationModeHighlightConfig"},{"name":"nodeId","description":"Identifier of the isolated element to highlight.","$ref":"DOM.NodeId"}]},{"id":"IsolationModeHighlightConfig","type":"object","properties":[{"name":"resizerColor","description":"The fill color of the resizers (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"resizerHandleColor","description":"The fill color for resizer handles (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"maskColor","description":"The fill color for the mask covering non-isolated elements (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"id":"InspectMode","type":"string","enum":["searchForNode","searchForUAShadowDOM","captureAreaScreenshot","showDistances","none"]}],"commands":[{"name":"disable","description":"Disables domain notifications."},{"name":"enable","description":"Enables domain notifications."},{"name":"getHighlightObjectForTest","description":"For testing.","parameters":[{"name":"nodeId","description":"Id of the node to get highlight object for.","$ref":"DOM.NodeId"},{"name":"includeDistance","description":"Whether to include distance info.","optional":true,"type":"boolean"},{"name":"includeStyle","description":"Whether to include style info.","optional":true,"type":"boolean"},{"name":"colorFormat","description":"The color format to get config with (default: hex).","optional":true,"$ref":"ColorFormat"},{"name":"showAccessibilityInfo","description":"Whether to show accessibility info (default: true).","optional":true,"type":"boolean"}],"returns":[{"name":"highlight","description":"Highlight data for the node.","type":"object"}]},{"name":"getGridHighlightObjectsForTest","description":"For Persistent Grid testing.","parameters":[{"name":"nodeIds","description":"Ids of the node to get highlight object for.","type":"array","items":{"$ref":"DOM.NodeId"}}],"returns":[{"name":"highlights","description":"Grid Highlight data for the node ids provided.","type":"object"}]},{"name":"getSourceOrderHighlightObjectForTest","description":"For Source Order Viewer testing.","parameters":[{"name":"nodeId","description":"Id of the node to highlight.","$ref":"DOM.NodeId"}],"returns":[{"name":"highlight","description":"Source order highlight data for the node id provided.","type":"object"}]},{"name":"hideHighlight","description":"Hides any highlight."},{"name":"highlightFrame","description":"Highlights owner element of the frame with given id.\\nDeprecated: Doesn\'t work reliablity and cannot be fixed due to process\\nseparatation (the owner node might be in a different process). Determine\\nthe owner node in the client and use highlightNode.","deprecated":true,"parameters":[{"name":"frameId","description":"Identifier of the frame to highlight.","$ref":"Page.FrameId"},{"name":"contentColor","description":"The content box highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"contentOutlineColor","description":"The content box highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"highlightNode","description":"Highlights DOM node with given id or with the given JavaScript object wrapper. Either nodeId or\\nobjectId must be specified.","parameters":[{"name":"highlightConfig","description":"A descriptor for the highlight appearance.","$ref":"HighlightConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to highlight.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node to be highlighted.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"selector","description":"Selectors to highlight relevant nodes.","optional":true,"type":"string"}]},{"name":"highlightQuad","description":"Highlights given quad. Coordinates are absolute with respect to the main frame viewport.","parameters":[{"name":"quad","description":"Quad to highlight","$ref":"DOM.Quad"},{"name":"color","description":"The highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"outlineColor","description":"The highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"highlightRect","description":"Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport.","parameters":[{"name":"x","description":"X coordinate","type":"integer"},{"name":"y","description":"Y coordinate","type":"integer"},{"name":"width","description":"Rectangle width","type":"integer"},{"name":"height","description":"Rectangle height","type":"integer"},{"name":"color","description":"The highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"outlineColor","description":"The highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"highlightSourceOrder","description":"Highlights the source order of the children of the DOM node with given id or with the given\\nJavaScript object wrapper. Either nodeId or objectId must be specified.","parameters":[{"name":"sourceOrderConfig","description":"A descriptor for the appearance of the overlay drawing.","$ref":"SourceOrderConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to highlight.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node to be highlighted.","optional":true,"$ref":"Runtime.RemoteObjectId"}]},{"name":"setInspectMode","description":"Enters the \'inspect\' mode. In this mode, elements that user is hovering over are highlighted.\\nBackend then generates \'inspectNodeRequested\' event upon element selection.","parameters":[{"name":"mode","description":"Set an inspection mode.","$ref":"InspectMode"},{"name":"highlightConfig","description":"A descriptor for the highlight appearance of hovered-over nodes. May be omitted if `enabled\\n== false`.","optional":true,"$ref":"HighlightConfig"}]},{"name":"setShowAdHighlights","description":"Highlights owner element of all frames detected to be ads.","parameters":[{"name":"show","description":"True for showing ad highlights","type":"boolean"}]},{"name":"setPausedInDebuggerMessage","parameters":[{"name":"message","description":"The message to display, also triggers resume and step over controls.","optional":true,"type":"string"}]},{"name":"setShowDebugBorders","description":"Requests that backend shows debug borders on layers","parameters":[{"name":"show","description":"True for showing debug borders","type":"boolean"}]},{"name":"setShowFPSCounter","description":"Requests that backend shows the FPS counter","parameters":[{"name":"show","description":"True for showing the FPS counter","type":"boolean"}]},{"name":"setShowGridOverlays","description":"Highlight multiple elements with the CSS Grid overlay.","parameters":[{"name":"gridNodeHighlightConfigs","description":"An array of node identifiers and descriptors for the highlight appearance.","type":"array","items":{"$ref":"GridNodeHighlightConfig"}}]},{"name":"setShowFlexOverlays","parameters":[{"name":"flexNodeHighlightConfigs","description":"An array of node identifiers and descriptors for the highlight appearance.","type":"array","items":{"$ref":"FlexNodeHighlightConfig"}}]},{"name":"setShowScrollSnapOverlays","parameters":[{"name":"scrollSnapHighlightConfigs","description":"An array of node identifiers and descriptors for the highlight appearance.","type":"array","items":{"$ref":"ScrollSnapHighlightConfig"}}]},{"name":"setShowContainerQueryOverlays","parameters":[{"name":"containerQueryHighlightConfigs","description":"An array of node identifiers and descriptors for the highlight appearance.","type":"array","items":{"$ref":"ContainerQueryHighlightConfig"}}]},{"name":"setShowPaintRects","description":"Requests that backend shows paint rectangles","parameters":[{"name":"result","description":"True for showing paint rectangles","type":"boolean"}]},{"name":"setShowLayoutShiftRegions","description":"Requests that backend shows layout shift regions","parameters":[{"name":"result","description":"True for showing layout shift regions","type":"boolean"}]},{"name":"setShowScrollBottleneckRects","description":"Requests that backend shows scroll bottleneck rects","parameters":[{"name":"show","description":"True for showing scroll bottleneck rects","type":"boolean"}]},{"name":"setShowHitTestBorders","description":"Deprecated, no longer has any effect.","deprecated":true,"parameters":[{"name":"show","description":"True for showing hit-test borders","type":"boolean"}]},{"name":"setShowWebVitals","description":"Request that backend shows an overlay with web vital metrics.","parameters":[{"name":"show","type":"boolean"}]},{"name":"setShowViewportSizeOnResize","description":"Paints viewport size upon main frame resize.","parameters":[{"name":"show","description":"Whether to paint size or not.","type":"boolean"}]},{"name":"setShowHinge","description":"Add a dual screen device hinge","parameters":[{"name":"hingeConfig","description":"hinge data, null means hideHinge","optional":true,"$ref":"HingeConfig"}]},{"name":"setShowIsolatedElements","description":"Show elements in isolation mode with overlays.","parameters":[{"name":"isolatedElementHighlightConfigs","description":"An array of node identifiers and descriptors for the highlight appearance.","type":"array","items":{"$ref":"IsolatedElementHighlightConfig"}}]}],"events":[{"name":"inspectNodeRequested","description":"Fired when the node should be inspected. This happens after call to `setInspectMode` or when\\nuser manually inspects an element.","parameters":[{"name":"backendNodeId","description":"Id of the node to inspect.","$ref":"DOM.BackendNodeId"}]},{"name":"nodeHighlightRequested","description":"Fired when the node should be highlighted. This happens after call to `setInspectMode`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}]},{"name":"screenshotRequested","description":"Fired when user asks to capture screenshot of some area on the page.","parameters":[{"name":"viewport","description":"Viewport to capture, in device independent pixels (dip).","$ref":"Page.Viewport"}]},{"name":"inspectModeCanceled","description":"Fired when user cancels the inspect mode."}]},{"domain":"Page","description":"Actions and events related to the inspected page belong to the page domain.","dependencies":["Debugger","DOM","IO","Network","Runtime"],"types":[{"id":"FrameId","description":"Unique frame identifier.","type":"string"},{"id":"AdFrameType","description":"Indicates whether a frame has been identified as an ad.","experimental":true,"type":"string","enum":["none","child","root"]},{"id":"AdFrameExplanation","experimental":true,"type":"string","enum":["ParentIsAd","CreatedByAdScript","MatchedBlockingRule"]},{"id":"AdFrameStatus","description":"Indicates whether a frame has been identified as an ad and why.","experimental":true,"type":"object","properties":[{"name":"adFrameType","$ref":"AdFrameType"},{"name":"explanations","optional":true,"type":"array","items":{"$ref":"AdFrameExplanation"}}]},{"id":"AdScriptId","description":"Identifies the bottom-most script which caused the frame to be labelled\\nas an ad.","experimental":true,"type":"object","properties":[{"name":"scriptId","description":"Script Id of the bottom-most script which caused the frame to be labelled\\nas an ad.","$ref":"Runtime.ScriptId"},{"name":"debuggerId","description":"Id of adScriptId\'s debugger.","$ref":"Runtime.UniqueDebuggerId"}]},{"id":"SecureContextType","description":"Indicates whether the frame is a secure context and why it is the case.","experimental":true,"type":"string","enum":["Secure","SecureLocalhost","InsecureScheme","InsecureAncestor"]},{"id":"CrossOriginIsolatedContextType","description":"Indicates whether the frame is cross-origin isolated and why it is the case.","experimental":true,"type":"string","enum":["Isolated","NotIsolated","NotIsolatedFeatureDisabled"]},{"id":"GatedAPIFeatures","experimental":true,"type":"string","enum":["SharedArrayBuffers","SharedArrayBuffersTransferAllowed","PerformanceMeasureMemory","PerformanceProfile"]},{"id":"PermissionsPolicyFeature","description":"All Permissions Policy features. This enum should match the one defined\\nin third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.","experimental":true,"type":"string","enum":["accelerometer","ambient-light-sensor","attribution-reporting","autoplay","bluetooth","browsing-topics","camera","ch-dpr","ch-device-memory","ch-downlink","ch-ect","ch-prefers-color-scheme","ch-prefers-reduced-motion","ch-rtt","ch-save-data","ch-ua","ch-ua-arch","ch-ua-bitness","ch-ua-platform","ch-ua-model","ch-ua-mobile","ch-ua-form-factor","ch-ua-full-version","ch-ua-full-version-list","ch-ua-platform-version","ch-ua-wow64","ch-viewport-height","ch-viewport-width","ch-width","clipboard-read","clipboard-write","compute-pressure","cross-origin-isolated","direct-sockets","display-capture","document-domain","encrypted-media","execution-while-out-of-viewport","execution-while-not-rendered","focus-without-user-activation","fullscreen","frobulate","gamepad","geolocation","gyroscope","hid","identity-credentials-get","idle-detection","interest-cohort","join-ad-interest-group","keyboard-map","local-fonts","magnetometer","microphone","midi","otp-credentials","payment","picture-in-picture","private-aggregation","private-state-token-issuance","private-state-token-redemption","publickey-credentials-get","run-ad-auction","screen-wake-lock","serial","shared-autofill","shared-storage","shared-storage-select-url","smart-card","storage-access","sync-xhr","unload","usb","vertical-scroll","web-share","window-management","window-placement","xr-spatial-tracking"]},{"id":"PermissionsPolicyBlockReason","description":"Reason for a permissions policy feature to be disabled.","experimental":true,"type":"string","enum":["Header","IframeAttribute","InFencedFrameTree","InIsolatedApp"]},{"id":"PermissionsPolicyBlockLocator","experimental":true,"type":"object","properties":[{"name":"frameId","$ref":"FrameId"},{"name":"blockReason","$ref":"PermissionsPolicyBlockReason"}]},{"id":"PermissionsPolicyFeatureState","experimental":true,"type":"object","properties":[{"name":"feature","$ref":"PermissionsPolicyFeature"},{"name":"allowed","type":"boolean"},{"name":"locator","optional":true,"$ref":"PermissionsPolicyBlockLocator"}]},{"id":"OriginTrialTokenStatus","description":"Origin Trial(https://www.chromium.org/blink/origin-trials) support.\\nStatus for an Origin Trial token.","experimental":true,"type":"string","enum":["Success","NotSupported","Insecure","Expired","WrongOrigin","InvalidSignature","Malformed","WrongVersion","FeatureDisabled","TokenDisabled","FeatureDisabledForUser","UnknownTrial"]},{"id":"OriginTrialStatus","description":"Status for an Origin Trial.","experimental":true,"type":"string","enum":["Enabled","ValidTokenNotProvided","OSNotSupported","TrialNotAllowed"]},{"id":"OriginTrialUsageRestriction","experimental":true,"type":"string","enum":["None","Subset"]},{"id":"OriginTrialToken","experimental":true,"type":"object","properties":[{"name":"origin","type":"string"},{"name":"matchSubDomains","type":"boolean"},{"name":"trialName","type":"string"},{"name":"expiryTime","$ref":"Network.TimeSinceEpoch"},{"name":"isThirdParty","type":"boolean"},{"name":"usageRestriction","$ref":"OriginTrialUsageRestriction"}]},{"id":"OriginTrialTokenWithStatus","experimental":true,"type":"object","properties":[{"name":"rawTokenText","type":"string"},{"name":"parsedToken","description":"`parsedToken` is present only when the token is extractable and\\nparsable.","optional":true,"$ref":"OriginTrialToken"},{"name":"status","$ref":"OriginTrialTokenStatus"}]},{"id":"OriginTrial","experimental":true,"type":"object","properties":[{"name":"trialName","type":"string"},{"name":"status","$ref":"OriginTrialStatus"},{"name":"tokensWithStatus","type":"array","items":{"$ref":"OriginTrialTokenWithStatus"}}]},{"id":"Frame","description":"Information about the Frame on the page.","type":"object","properties":[{"name":"id","description":"Frame unique identifier.","$ref":"FrameId"},{"name":"parentId","description":"Parent frame identifier.","optional":true,"$ref":"FrameId"},{"name":"loaderId","description":"Identifier of the loader associated with this frame.","$ref":"Network.LoaderId"},{"name":"name","description":"Frame\'s name as specified in the tag.","optional":true,"type":"string"},{"name":"url","description":"Frame document\'s URL without fragment.","type":"string"},{"name":"urlFragment","description":"Frame document\'s URL fragment including the \'#\'.","experimental":true,"optional":true,"type":"string"},{"name":"domainAndRegistry","description":"Frame document\'s registered domain, taking the public suffixes list into account.\\nExtracted from the Frame\'s url.\\nExample URLs: http://www.google.com/file.html -> \\"google.com\\"\\n http://a.b.co.uk/file.html -> \\"b.co.uk\\"","experimental":true,"type":"string"},{"name":"securityOrigin","description":"Frame document\'s security origin.","type":"string"},{"name":"mimeType","description":"Frame document\'s mimeType as determined by the browser.","type":"string"},{"name":"unreachableUrl","description":"If the frame failed to load, this contains the URL that could not be loaded. Note that unlike url above, this URL may contain a fragment.","experimental":true,"optional":true,"type":"string"},{"name":"adFrameStatus","description":"Indicates whether this frame was tagged as an ad and why.","experimental":true,"optional":true,"$ref":"AdFrameStatus"},{"name":"secureContextType","description":"Indicates whether the main document is a secure context and explains why that is the case.","experimental":true,"$ref":"SecureContextType"},{"name":"crossOriginIsolatedContextType","description":"Indicates whether this is a cross origin isolated context.","experimental":true,"$ref":"CrossOriginIsolatedContextType"},{"name":"gatedAPIFeatures","description":"Indicated which gated APIs / features are available.","experimental":true,"type":"array","items":{"$ref":"GatedAPIFeatures"}}]},{"id":"FrameResource","description":"Information about the Resource on the page.","experimental":true,"type":"object","properties":[{"name":"url","description":"Resource URL.","type":"string"},{"name":"type","description":"Type of this resource.","$ref":"Network.ResourceType"},{"name":"mimeType","description":"Resource mimeType as determined by the browser.","type":"string"},{"name":"lastModified","description":"last-modified timestamp as reported by server.","optional":true,"$ref":"Network.TimeSinceEpoch"},{"name":"contentSize","description":"Resource content size.","optional":true,"type":"number"},{"name":"failed","description":"True if the resource failed to load.","optional":true,"type":"boolean"},{"name":"canceled","description":"True if the resource was canceled during loading.","optional":true,"type":"boolean"}]},{"id":"FrameResourceTree","description":"Information about the Frame hierarchy along with their cached resources.","experimental":true,"type":"object","properties":[{"name":"frame","description":"Frame information for this tree item.","$ref":"Frame"},{"name":"childFrames","description":"Child frames.","optional":true,"type":"array","items":{"$ref":"FrameResourceTree"}},{"name":"resources","description":"Information about frame resources.","type":"array","items":{"$ref":"FrameResource"}}]},{"id":"FrameTree","description":"Information about the Frame hierarchy.","type":"object","properties":[{"name":"frame","description":"Frame information for this tree item.","$ref":"Frame"},{"name":"childFrames","description":"Child frames.","optional":true,"type":"array","items":{"$ref":"FrameTree"}}]},{"id":"ScriptIdentifier","description":"Unique script identifier.","type":"string"},{"id":"TransitionType","description":"Transition type.","type":"string","enum":["link","typed","address_bar","auto_bookmark","auto_subframe","manual_subframe","generated","auto_toplevel","form_submit","reload","keyword","keyword_generated","other"]},{"id":"NavigationEntry","description":"Navigation history entry.","type":"object","properties":[{"name":"id","description":"Unique id of the navigation history entry.","type":"integer"},{"name":"url","description":"URL of the navigation history entry.","type":"string"},{"name":"userTypedURL","description":"URL that the user typed in the url bar.","type":"string"},{"name":"title","description":"Title of the navigation history entry.","type":"string"},{"name":"transitionType","description":"Transition type.","$ref":"TransitionType"}]},{"id":"ScreencastFrameMetadata","description":"Screencast frame metadata.","experimental":true,"type":"object","properties":[{"name":"offsetTop","description":"Top offset in DIP.","type":"number"},{"name":"pageScaleFactor","description":"Page scale factor.","type":"number"},{"name":"deviceWidth","description":"Device screen width in DIP.","type":"number"},{"name":"deviceHeight","description":"Device screen height in DIP.","type":"number"},{"name":"scrollOffsetX","description":"Position of horizontal scroll in CSS pixels.","type":"number"},{"name":"scrollOffsetY","description":"Position of vertical scroll in CSS pixels.","type":"number"},{"name":"timestamp","description":"Frame swap timestamp.","optional":true,"$ref":"Network.TimeSinceEpoch"}]},{"id":"DialogType","description":"Javascript dialog type.","type":"string","enum":["alert","confirm","prompt","beforeunload"]},{"id":"AppManifestError","description":"Error while paring app manifest.","type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"critical","description":"If criticial, this is a non-recoverable parse error.","type":"integer"},{"name":"line","description":"Error line.","type":"integer"},{"name":"column","description":"Error column.","type":"integer"}]},{"id":"AppManifestParsedProperties","description":"Parsed app manifest properties.","experimental":true,"type":"object","properties":[{"name":"scope","description":"Computed scope value","type":"string"}]},{"id":"LayoutViewport","description":"Layout viewport position and dimensions.","type":"object","properties":[{"name":"pageX","description":"Horizontal offset relative to the document (CSS pixels).","type":"integer"},{"name":"pageY","description":"Vertical offset relative to the document (CSS pixels).","type":"integer"},{"name":"clientWidth","description":"Width (CSS pixels), excludes scrollbar if present.","type":"integer"},{"name":"clientHeight","description":"Height (CSS pixels), excludes scrollbar if present.","type":"integer"}]},{"id":"VisualViewport","description":"Visual viewport position, dimensions, and scale.","type":"object","properties":[{"name":"offsetX","description":"Horizontal offset relative to the layout viewport (CSS pixels).","type":"number"},{"name":"offsetY","description":"Vertical offset relative to the layout viewport (CSS pixels).","type":"number"},{"name":"pageX","description":"Horizontal offset relative to the document (CSS pixels).","type":"number"},{"name":"pageY","description":"Vertical offset relative to the document (CSS pixels).","type":"number"},{"name":"clientWidth","description":"Width (CSS pixels), excludes scrollbar if present.","type":"number"},{"name":"clientHeight","description":"Height (CSS pixels), excludes scrollbar if present.","type":"number"},{"name":"scale","description":"Scale relative to the ideal viewport (size at width=device-width).","type":"number"},{"name":"zoom","description":"Page zoom factor (CSS to device independent pixels ratio).","optional":true,"type":"number"}]},{"id":"Viewport","description":"Viewport for capturing screenshot.","type":"object","properties":[{"name":"x","description":"X offset in device independent pixels (dip).","type":"number"},{"name":"y","description":"Y offset in device independent pixels (dip).","type":"number"},{"name":"width","description":"Rectangle width in device independent pixels (dip).","type":"number"},{"name":"height","description":"Rectangle height in device independent pixels (dip).","type":"number"},{"name":"scale","description":"Page scale factor.","type":"number"}]},{"id":"FontFamilies","description":"Generic font families collection.","experimental":true,"type":"object","properties":[{"name":"standard","description":"The standard font-family.","optional":true,"type":"string"},{"name":"fixed","description":"The fixed font-family.","optional":true,"type":"string"},{"name":"serif","description":"The serif font-family.","optional":true,"type":"string"},{"name":"sansSerif","description":"The sansSerif font-family.","optional":true,"type":"string"},{"name":"cursive","description":"The cursive font-family.","optional":true,"type":"string"},{"name":"fantasy","description":"The fantasy font-family.","optional":true,"type":"string"},{"name":"math","description":"The math font-family.","optional":true,"type":"string"}]},{"id":"ScriptFontFamilies","description":"Font families collection for a script.","experimental":true,"type":"object","properties":[{"name":"script","description":"Name of the script which these font families are defined for.","type":"string"},{"name":"fontFamilies","description":"Generic font families collection for the script.","$ref":"FontFamilies"}]},{"id":"FontSizes","description":"Default font sizes.","experimental":true,"type":"object","properties":[{"name":"standard","description":"Default standard font size.","optional":true,"type":"integer"},{"name":"fixed","description":"Default fixed font size.","optional":true,"type":"integer"}]},{"id":"ClientNavigationReason","experimental":true,"type":"string","enum":["formSubmissionGet","formSubmissionPost","httpHeaderRefresh","scriptInitiated","metaTagRefresh","pageBlockInterstitial","reload","anchorClick"]},{"id":"ClientNavigationDisposition","experimental":true,"type":"string","enum":["currentTab","newTab","newWindow","download"]},{"id":"InstallabilityErrorArgument","experimental":true,"type":"object","properties":[{"name":"name","description":"Argument name (e.g. name:\'minimum-icon-size-in-pixels\').","type":"string"},{"name":"value","description":"Argument value (e.g. value:\'64\').","type":"string"}]},{"id":"InstallabilityError","description":"The installability error","experimental":true,"type":"object","properties":[{"name":"errorId","description":"The error id (e.g. \'manifest-missing-suitable-icon\').","type":"string"},{"name":"errorArguments","description":"The list of error arguments (e.g. {name:\'minimum-icon-size-in-pixels\', value:\'64\'}).","type":"array","items":{"$ref":"InstallabilityErrorArgument"}}]},{"id":"ReferrerPolicy","description":"The referring-policy used for the navigation.","experimental":true,"type":"string","enum":["noReferrer","noReferrerWhenDowngrade","origin","originWhenCrossOrigin","sameOrigin","strictOrigin","strictOriginWhenCrossOrigin","unsafeUrl"]},{"id":"CompilationCacheParams","description":"Per-script compilation cache parameters for `Page.produceCompilationCache`","experimental":true,"type":"object","properties":[{"name":"url","description":"The URL of the script to produce a compilation cache entry for.","type":"string"},{"name":"eager","description":"A hint to the backend whether eager compilation is recommended.\\n(the actual compilation mode used is upon backend discretion).","optional":true,"type":"boolean"}]},{"id":"AutoResponseMode","description":"Enum of possible auto-reponse for permisison / prompt dialogs.","experimental":true,"type":"string","enum":["none","autoAccept","autoReject","autoOptOut"]},{"id":"NavigationType","description":"The type of a frameNavigated event.","experimental":true,"type":"string","enum":["Navigation","BackForwardCacheRestore"]},{"id":"BackForwardCacheNotRestoredReason","description":"List of not restored reasons for back-forward cache.","experimental":true,"type":"string","enum":["NotPrimaryMainFrame","BackForwardCacheDisabled","RelatedActiveContentsExist","HTTPStatusNotOK","SchemeNotHTTPOrHTTPS","Loading","WasGrantedMediaAccess","DisableForRenderFrameHostCalled","DomainNotAllowed","HTTPMethodNotGET","SubframeIsNavigating","Timeout","CacheLimit","JavaScriptExecution","RendererProcessKilled","RendererProcessCrashed","SchedulerTrackedFeatureUsed","ConflictingBrowsingInstance","CacheFlushed","ServiceWorkerVersionActivation","SessionRestored","ServiceWorkerPostMessage","EnteredBackForwardCacheBeforeServiceWorkerHostAdded","RenderFrameHostReused_SameSite","RenderFrameHostReused_CrossSite","ServiceWorkerClaim","IgnoreEventAndEvict","HaveInnerContents","TimeoutPuttingInCache","BackForwardCacheDisabledByLowMemory","BackForwardCacheDisabledByCommandLine","NetworkRequestDatapipeDrainedAsBytesConsumer","NetworkRequestRedirected","NetworkRequestTimeout","NetworkExceedsBufferLimit","NavigationCancelledWhileRestoring","NotMostRecentNavigationEntry","BackForwardCacheDisabledForPrerender","UserAgentOverrideDiffers","ForegroundCacheLimit","BrowsingInstanceNotSwapped","BackForwardCacheDisabledForDelegate","UnloadHandlerExistsInMainFrame","UnloadHandlerExistsInSubFrame","ServiceWorkerUnregistration","CacheControlNoStore","CacheControlNoStoreCookieModified","CacheControlNoStoreHTTPOnlyCookieModified","NoResponseHead","Unknown","ActivationNavigationsDisallowedForBug1234857","ErrorDocument","FencedFramesEmbedder","CookieDisabled","HTTPAuthRequired","CookieFlushed","WebSocket","WebTransport","WebRTC","MainResourceHasCacheControlNoStore","MainResourceHasCacheControlNoCache","SubresourceHasCacheControlNoStore","SubresourceHasCacheControlNoCache","ContainsPlugins","DocumentLoaded","DedicatedWorkerOrWorklet","OutstandingNetworkRequestOthers","RequestedMIDIPermission","RequestedAudioCapturePermission","RequestedVideoCapturePermission","RequestedBackForwardCacheBlockedSensors","RequestedBackgroundWorkPermission","BroadcastChannel","WebXR","SharedWorker","WebLocks","WebHID","WebShare","RequestedStorageAccessGrant","WebNfc","OutstandingNetworkRequestFetch","OutstandingNetworkRequestXHR","AppBanner","Printing","WebDatabase","PictureInPicture","Portal","SpeechRecognizer","IdleManager","PaymentManager","SpeechSynthesis","KeyboardLock","WebOTPService","OutstandingNetworkRequestDirectSocket","InjectedJavascript","InjectedStyleSheet","KeepaliveRequest","IndexedDBEvent","Dummy","JsNetworkRequestReceivedCacheControlNoStoreResource","WebRTCSticky","WebTransportSticky","WebSocketSticky","ContentSecurityHandler","ContentWebAuthenticationAPI","ContentFileChooser","ContentSerial","ContentFileSystemAccess","ContentMediaDevicesDispatcherHost","ContentWebBluetooth","ContentWebUSB","ContentMediaSessionService","ContentScreenReader","EmbedderPopupBlockerTabHelper","EmbedderSafeBrowsingTriggeredPopupBlocker","EmbedderSafeBrowsingThreatDetails","EmbedderAppBannerManager","EmbedderDomDistillerViewerSource","EmbedderDomDistillerSelfDeletingRequestDelegate","EmbedderOomInterventionTabHelper","EmbedderOfflinePage","EmbedderChromePasswordManagerClientBindCredentialManager","EmbedderPermissionRequestManager","EmbedderModalDialog","EmbedderExtensions","EmbedderExtensionMessaging","EmbedderExtensionMessagingForOpenPort","EmbedderExtensionSentMessageToCachedFrame"]},{"id":"BackForwardCacheNotRestoredReasonType","description":"Types of not restored reasons for back-forward cache.","experimental":true,"type":"string","enum":["SupportPending","PageSupportNeeded","Circumstantial"]},{"id":"BackForwardCacheNotRestoredExplanation","experimental":true,"type":"object","properties":[{"name":"type","description":"Type of the reason","$ref":"BackForwardCacheNotRestoredReasonType"},{"name":"reason","description":"Not restored reason","$ref":"BackForwardCacheNotRestoredReason"},{"name":"context","description":"Context associated with the reason. The meaning of this context is\\ndependent on the reason:\\n- EmbedderExtensionSentMessageToCachedFrame: the extension ID.","optional":true,"type":"string"}]},{"id":"BackForwardCacheNotRestoredExplanationTree","experimental":true,"type":"object","properties":[{"name":"url","description":"URL of each frame","type":"string"},{"name":"explanations","description":"Not restored reasons of each frame","type":"array","items":{"$ref":"BackForwardCacheNotRestoredExplanation"}},{"name":"children","description":"Array of children frame","type":"array","items":{"$ref":"BackForwardCacheNotRestoredExplanationTree"}}]}],"commands":[{"name":"addScriptToEvaluateOnLoad","description":"Deprecated, please use addScriptToEvaluateOnNewDocument instead.","experimental":true,"deprecated":true,"parameters":[{"name":"scriptSource","type":"string"}],"returns":[{"name":"identifier","description":"Identifier of the added script.","$ref":"ScriptIdentifier"}]},{"name":"addScriptToEvaluateOnNewDocument","description":"Evaluates given script in every frame upon creation (before loading frame\'s scripts).","parameters":[{"name":"source","type":"string"},{"name":"worldName","description":"If specified, creates an isolated world with the given name and evaluates given script in it.\\nThis world name will be used as the ExecutionContextDescription::name when the corresponding\\nevent is emitted.","experimental":true,"optional":true,"type":"string"},{"name":"includeCommandLineAPI","description":"Specifies whether command line API should be available to the script, defaults\\nto false.","experimental":true,"optional":true,"type":"boolean"},{"name":"runImmediately","description":"If true, runs the script immediately on existing execution contexts or worlds.\\nDefault: false.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"identifier","description":"Identifier of the added script.","$ref":"ScriptIdentifier"}]},{"name":"bringToFront","description":"Brings page to front (activates tab)."},{"name":"captureScreenshot","description":"Capture page screenshot.","parameters":[{"name":"format","description":"Image compression format (defaults to png).","optional":true,"type":"string","enum":["jpeg","png","webp"]},{"name":"quality","description":"Compression quality from range [0..100] (jpeg only).","optional":true,"type":"integer"},{"name":"clip","description":"Capture the screenshot of a given region only.","optional":true,"$ref":"Viewport"},{"name":"fromSurface","description":"Capture the screenshot from the surface, rather than the view. Defaults to true.","experimental":true,"optional":true,"type":"boolean"},{"name":"captureBeyondViewport","description":"Capture the screenshot beyond the viewport. Defaults to false.","experimental":true,"optional":true,"type":"boolean"},{"name":"optimizeForSpeed","description":"Optimize image encoding for speed, not for resulting size (defaults to false)","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"data","description":"Base64-encoded image data. (Encoded as a base64 string when passed over JSON)","type":"string"}]},{"name":"captureSnapshot","description":"Returns a snapshot of the page as a string. For MHTML format, the serialization includes\\niframes, shadow DOM, external resources, and element-inline styles.","experimental":true,"parameters":[{"name":"format","description":"Format (defaults to mhtml).","optional":true,"type":"string","enum":["mhtml"]}],"returns":[{"name":"data","description":"Serialized page data.","type":"string"}]},{"name":"clearDeviceMetricsOverride","description":"Clears the overridden device metrics.","experimental":true,"deprecated":true,"redirect":"Emulation"},{"name":"clearDeviceOrientationOverride","description":"Clears the overridden Device Orientation.","experimental":true,"deprecated":true,"redirect":"DeviceOrientation"},{"name":"clearGeolocationOverride","description":"Clears the overridden Geolocation Position and Error.","deprecated":true,"redirect":"Emulation"},{"name":"createIsolatedWorld","description":"Creates an isolated world for the given frame.","parameters":[{"name":"frameId","description":"Id of the frame in which the isolated world should be created.","$ref":"FrameId"},{"name":"worldName","description":"An optional name which is reported in the Execution Context.","optional":true,"type":"string"},{"name":"grantUniveralAccess","description":"Whether or not universal access should be granted to the isolated world. This is a powerful\\noption, use with caution.","optional":true,"type":"boolean"}],"returns":[{"name":"executionContextId","description":"Execution context of the isolated world.","$ref":"Runtime.ExecutionContextId"}]},{"name":"deleteCookie","description":"Deletes browser cookie with given name, domain and path.","experimental":true,"deprecated":true,"redirect":"Network","parameters":[{"name":"cookieName","description":"Name of the cookie to remove.","type":"string"},{"name":"url","description":"URL to match cooke domain and path.","type":"string"}]},{"name":"disable","description":"Disables page domain notifications."},{"name":"enable","description":"Enables page domain notifications."},{"name":"getAppManifest","returns":[{"name":"url","description":"Manifest location.","type":"string"},{"name":"errors","type":"array","items":{"$ref":"AppManifestError"}},{"name":"data","description":"Manifest content.","optional":true,"type":"string"},{"name":"parsed","description":"Parsed manifest properties","experimental":true,"optional":true,"$ref":"AppManifestParsedProperties"}]},{"name":"getInstallabilityErrors","experimental":true,"returns":[{"name":"installabilityErrors","type":"array","items":{"$ref":"InstallabilityError"}}]},{"name":"getManifestIcons","description":"Deprecated because it\'s not guaranteed that the returned icon is in fact the one used for PWA installation.","experimental":true,"deprecated":true,"returns":[{"name":"primaryIcon","optional":true,"type":"string"}]},{"name":"getAppId","description":"Returns the unique (PWA) app id.\\nOnly returns values if the feature flag \'WebAppEnableManifestId\' is enabled","experimental":true,"returns":[{"name":"appId","description":"App id, either from manifest\'s id attribute or computed from start_url","optional":true,"type":"string"},{"name":"recommendedId","description":"Recommendation for manifest\'s id attribute to match current id computed from start_url","optional":true,"type":"string"}]},{"name":"getAdScriptId","experimental":true,"parameters":[{"name":"frameId","$ref":"FrameId"}],"returns":[{"name":"adScriptId","description":"Identifies the bottom-most script which caused the frame to be labelled\\nas an ad. Only sent if frame is labelled as an ad and id is available.","optional":true,"$ref":"AdScriptId"}]},{"name":"getCookies","description":"Returns all browser cookies for the page and all of its subframes. Depending\\non the backend support, will return detailed cookie information in the\\n`cookies` field.","experimental":true,"deprecated":true,"redirect":"Network","returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Network.Cookie"}}]},{"name":"getFrameTree","description":"Returns present frame tree structure.","returns":[{"name":"frameTree","description":"Present frame tree structure.","$ref":"FrameTree"}]},{"name":"getLayoutMetrics","description":"Returns metrics relating to the layouting of the page, such as viewport bounds/scale.","returns":[{"name":"layoutViewport","description":"Deprecated metrics relating to the layout viewport. Is in device pixels. Use `cssLayoutViewport` instead.","deprecated":true,"$ref":"LayoutViewport"},{"name":"visualViewport","description":"Deprecated metrics relating to the visual viewport. Is in device pixels. Use `cssVisualViewport` instead.","deprecated":true,"$ref":"VisualViewport"},{"name":"contentSize","description":"Deprecated size of scrollable area. Is in DP. Use `cssContentSize` instead.","deprecated":true,"$ref":"DOM.Rect"},{"name":"cssLayoutViewport","description":"Metrics relating to the layout viewport in CSS pixels.","$ref":"LayoutViewport"},{"name":"cssVisualViewport","description":"Metrics relating to the visual viewport in CSS pixels.","$ref":"VisualViewport"},{"name":"cssContentSize","description":"Size of scrollable area in CSS pixels.","$ref":"DOM.Rect"}]},{"name":"getNavigationHistory","description":"Returns navigation history for the current page.","returns":[{"name":"currentIndex","description":"Index of the current navigation history entry.","type":"integer"},{"name":"entries","description":"Array of navigation history entries.","type":"array","items":{"$ref":"NavigationEntry"}}]},{"name":"resetNavigationHistory","description":"Resets navigation history for the current page."},{"name":"getResourceContent","description":"Returns content of the given resource.","experimental":true,"parameters":[{"name":"frameId","description":"Frame id to get resource for.","$ref":"FrameId"},{"name":"url","description":"URL of the resource to get content for.","type":"string"}],"returns":[{"name":"content","description":"Resource content.","type":"string"},{"name":"base64Encoded","description":"True, if content was served as base64.","type":"boolean"}]},{"name":"getResourceTree","description":"Returns present frame / resource tree structure.","experimental":true,"returns":[{"name":"frameTree","description":"Present frame / resource tree structure.","$ref":"FrameResourceTree"}]},{"name":"handleJavaScriptDialog","description":"Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).","parameters":[{"name":"accept","description":"Whether to accept or dismiss the dialog.","type":"boolean"},{"name":"promptText","description":"The text to enter into the dialog prompt before accepting. Used only if this is a prompt\\ndialog.","optional":true,"type":"string"}]},{"name":"navigate","description":"Navigates current page to the given URL.","parameters":[{"name":"url","description":"URL to navigate the page to.","type":"string"},{"name":"referrer","description":"Referrer URL.","optional":true,"type":"string"},{"name":"transitionType","description":"Intended transition type.","optional":true,"$ref":"TransitionType"},{"name":"frameId","description":"Frame id to navigate, if not specified navigates the top frame.","optional":true,"$ref":"FrameId"},{"name":"referrerPolicy","description":"Referrer-policy used for the navigation.","experimental":true,"optional":true,"$ref":"ReferrerPolicy"}],"returns":[{"name":"frameId","description":"Frame id that has navigated (or failed to navigate)","$ref":"FrameId"},{"name":"loaderId","description":"Loader identifier. This is omitted in case of same-document navigation,\\nas the previously committed loaderId would not change.","optional":true,"$ref":"Network.LoaderId"},{"name":"errorText","description":"User friendly error message, present if and only if navigation has failed.","optional":true,"type":"string"}]},{"name":"navigateToHistoryEntry","description":"Navigates current page to the given history entry.","parameters":[{"name":"entryId","description":"Unique id of the entry to navigate to.","type":"integer"}]},{"name":"printToPDF","description":"Print page as PDF.","parameters":[{"name":"landscape","description":"Paper orientation. Defaults to false.","optional":true,"type":"boolean"},{"name":"displayHeaderFooter","description":"Display header and footer. Defaults to false.","optional":true,"type":"boolean"},{"name":"printBackground","description":"Print background graphics. Defaults to false.","optional":true,"type":"boolean"},{"name":"scale","description":"Scale of the webpage rendering. Defaults to 1.","optional":true,"type":"number"},{"name":"paperWidth","description":"Paper width in inches. Defaults to 8.5 inches.","optional":true,"type":"number"},{"name":"paperHeight","description":"Paper height in inches. Defaults to 11 inches.","optional":true,"type":"number"},{"name":"marginTop","description":"Top margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginBottom","description":"Bottom margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginLeft","description":"Left margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginRight","description":"Right margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"pageRanges","description":"Paper ranges to print, one based, e.g., \'1-5, 8, 11-13\'. Pages are\\nprinted in the document order, not in the order specified, and no\\nmore than once.\\nDefaults to empty string, which implies the entire document is printed.\\nThe page numbers are quietly capped to actual page count of the\\ndocument, and ranges beyond the end of the document are ignored.\\nIf this results in no pages to print, an error is reported.\\nIt is an error to specify a range with start greater than end.","optional":true,"type":"string"},{"name":"headerTemplate","description":"HTML template for the print header. Should be valid HTML markup with following\\nclasses used to inject printing values into them:\\n- `date`: formatted print date\\n- `title`: document title\\n- `url`: document location\\n- `pageNumber`: current page number\\n- `totalPages`: total pages in the document\\n\\nFor example, `<span class=title></span>` would generate span containing the title.","optional":true,"type":"string"},{"name":"footerTemplate","description":"HTML template for the print footer. Should use the same format as the `headerTemplate`.","optional":true,"type":"string"},{"name":"preferCSSPageSize","description":"Whether or not to prefer page size as defined by css. Defaults to false,\\nin which case the content will be scaled to fit the paper size.","optional":true,"type":"boolean"},{"name":"transferMode","description":"return as stream","experimental":true,"optional":true,"type":"string","enum":["ReturnAsBase64","ReturnAsStream"]}],"returns":[{"name":"data","description":"Base64-encoded pdf data. Empty if |returnAsStream| is specified. (Encoded as a base64 string when passed over JSON)","type":"string"},{"name":"stream","description":"A handle of the stream that holds resulting PDF data.","experimental":true,"optional":true,"$ref":"IO.StreamHandle"}]},{"name":"reload","description":"Reloads given page optionally ignoring the cache.","parameters":[{"name":"ignoreCache","description":"If true, browser cache is ignored (as if the user pressed Shift+refresh).","optional":true,"type":"boolean"},{"name":"scriptToEvaluateOnLoad","description":"If set, the script will be injected into all frames of the inspected page after reload.\\nArgument will be ignored if reloading dataURL origin.","optional":true,"type":"string"}]},{"name":"removeScriptToEvaluateOnLoad","description":"Deprecated, please use removeScriptToEvaluateOnNewDocument instead.","experimental":true,"deprecated":true,"parameters":[{"name":"identifier","$ref":"ScriptIdentifier"}]},{"name":"removeScriptToEvaluateOnNewDocument","description":"Removes given script from the list.","parameters":[{"name":"identifier","$ref":"ScriptIdentifier"}]},{"name":"screencastFrameAck","description":"Acknowledges that a screencast frame has been received by the frontend.","experimental":true,"parameters":[{"name":"sessionId","description":"Frame number.","type":"integer"}]},{"name":"searchInResource","description":"Searches for given string in resource content.","experimental":true,"parameters":[{"name":"frameId","description":"Frame id for resource to search in.","$ref":"FrameId"},{"name":"url","description":"URL of the resource to search in.","type":"string"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"Debugger.SearchMatch"}}]},{"name":"setAdBlockingEnabled","description":"Enable Chrome\'s experimental ad filter on all sites.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to block ads.","type":"boolean"}]},{"name":"setBypassCSP","description":"Enable page Content Security Policy by-passing.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to bypass page CSP.","type":"boolean"}]},{"name":"getPermissionsPolicyState","description":"Get Permissions Policy state on given frame.","experimental":true,"parameters":[{"name":"frameId","$ref":"FrameId"}],"returns":[{"name":"states","type":"array","items":{"$ref":"PermissionsPolicyFeatureState"}}]},{"name":"getOriginTrials","description":"Get Origin Trials on given frame.","experimental":true,"parameters":[{"name":"frameId","$ref":"FrameId"}],"returns":[{"name":"originTrials","type":"array","items":{"$ref":"OriginTrial"}}]},{"name":"setDeviceMetricsOverride","description":"Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\\nwindow.innerWidth, window.innerHeight, and \\"device-width\\"/\\"device-height\\"-related CSS media\\nquery results).","experimental":true,"deprecated":true,"redirect":"Emulation","parameters":[{"name":"width","description":"Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"height","description":"Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"deviceScaleFactor","description":"Overriding device scale factor value. 0 disables the override.","type":"number"},{"name":"mobile","description":"Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\\nautosizing and more.","type":"boolean"},{"name":"scale","description":"Scale to apply to resulting view image.","optional":true,"type":"number"},{"name":"screenWidth","description":"Overriding screen width value in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"screenHeight","description":"Overriding screen height value in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"positionX","description":"Overriding view X position on screen in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"positionY","description":"Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"dontSetVisibleSize","description":"Do not set visible view size, rely upon explicit setVisibleSize call.","optional":true,"type":"boolean"},{"name":"screenOrientation","description":"Screen orientation override.","optional":true,"$ref":"Emulation.ScreenOrientation"},{"name":"viewport","description":"The viewport dimensions and scale. If not set, the override is cleared.","optional":true,"$ref":"Viewport"}]},{"name":"setDeviceOrientationOverride","description":"Overrides the Device Orientation.","experimental":true,"deprecated":true,"redirect":"DeviceOrientation","parameters":[{"name":"alpha","description":"Mock alpha","type":"number"},{"name":"beta","description":"Mock beta","type":"number"},{"name":"gamma","description":"Mock gamma","type":"number"}]},{"name":"setFontFamilies","description":"Set generic font families.","experimental":true,"parameters":[{"name":"fontFamilies","description":"Specifies font families to set. If a font family is not specified, it won\'t be changed.","$ref":"FontFamilies"},{"name":"forScripts","description":"Specifies font families to set for individual scripts.","optional":true,"type":"array","items":{"$ref":"ScriptFontFamilies"}}]},{"name":"setFontSizes","description":"Set default font sizes.","experimental":true,"parameters":[{"name":"fontSizes","description":"Specifies font sizes to set. If a font size is not specified, it won\'t be changed.","$ref":"FontSizes"}]},{"name":"setDocumentContent","description":"Sets given markup as the document\'s HTML.","parameters":[{"name":"frameId","description":"Frame id to set HTML for.","$ref":"FrameId"},{"name":"html","description":"HTML content to set.","type":"string"}]},{"name":"setDownloadBehavior","description":"Set the behavior when downloading a file.","experimental":true,"deprecated":true,"parameters":[{"name":"behavior","description":"Whether to allow all or deny all download requests, or use default Chrome behavior if\\navailable (otherwise deny).","type":"string","enum":["deny","allow","default"]},{"name":"downloadPath","description":"The default path to save downloaded files to. This is required if behavior is set to \'allow\'","optional":true,"type":"string"}]},{"name":"setGeolocationOverride","description":"Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\\nunavailable.","deprecated":true,"redirect":"Emulation","parameters":[{"name":"latitude","description":"Mock latitude","optional":true,"type":"number"},{"name":"longitude","description":"Mock longitude","optional":true,"type":"number"},{"name":"accuracy","description":"Mock accuracy","optional":true,"type":"number"}]},{"name":"setLifecycleEventsEnabled","description":"Controls whether page will emit lifecycle events.","experimental":true,"parameters":[{"name":"enabled","description":"If true, starts emitting lifecycle events.","type":"boolean"}]},{"name":"setTouchEmulationEnabled","description":"Toggles mouse event-based touch event emulation.","experimental":true,"deprecated":true,"redirect":"Emulation","parameters":[{"name":"enabled","description":"Whether the touch event emulation should be enabled.","type":"boolean"},{"name":"configuration","description":"Touch/gesture events configuration. Default: current platform.","optional":true,"type":"string","enum":["mobile","desktop"]}]},{"name":"startScreencast","description":"Starts sending each frame using the `screencastFrame` event.","experimental":true,"parameters":[{"name":"format","description":"Image compression format.","optional":true,"type":"string","enum":["jpeg","png"]},{"name":"quality","description":"Compression quality from range [0..100].","optional":true,"type":"integer"},{"name":"maxWidth","description":"Maximum screenshot width.","optional":true,"type":"integer"},{"name":"maxHeight","description":"Maximum screenshot height.","optional":true,"type":"integer"},{"name":"everyNthFrame","description":"Send every n-th frame.","optional":true,"type":"integer"}]},{"name":"stopLoading","description":"Force the page stop all navigations and pending resource fetches."},{"name":"crash","description":"Crashes renderer on the IO thread, generates minidumps.","experimental":true},{"name":"close","description":"Tries to close page, running its beforeunload hooks, if any.","experimental":true},{"name":"setWebLifecycleState","description":"Tries to update the web lifecycle state of the page.\\nIt will transition the page to the given state according to:\\nhttps://github.com/WICG/web-lifecycle/","experimental":true,"parameters":[{"name":"state","description":"Target lifecycle state","type":"string","enum":["frozen","active"]}]},{"name":"stopScreencast","description":"Stops sending each frame in the `screencastFrame`.","experimental":true},{"name":"produceCompilationCache","description":"Requests backend to produce compilation cache for the specified scripts.\\n`scripts` are appeneded to the list of scripts for which the cache\\nwould be produced. The list may be reset during page navigation.\\nWhen script with a matching URL is encountered, the cache is optionally\\nproduced upon backend discretion, based on internal heuristics.\\nSee also: `Page.compilationCacheProduced`.","experimental":true,"parameters":[{"name":"scripts","type":"array","items":{"$ref":"CompilationCacheParams"}}]},{"name":"addCompilationCache","description":"Seeds compilation cache for given url. Compilation cache does not survive\\ncross-process navigation.","experimental":true,"parameters":[{"name":"url","type":"string"},{"name":"data","description":"Base64-encoded data (Encoded as a base64 string when passed over JSON)","type":"string"}]},{"name":"clearCompilationCache","description":"Clears seeded compilation cache.","experimental":true},{"name":"setSPCTransactionMode","description":"Sets the Secure Payment Confirmation transaction mode.\\nhttps://w3c.github.io/secure-payment-confirmation/#sctn-automation-set-spc-transaction-mode","experimental":true,"parameters":[{"name":"mode","$ref":"AutoResponseMode"}]},{"name":"setRPHRegistrationMode","description":"Extensions for Custom Handlers API:\\nhttps://html.spec.whatwg.org/multipage/system-state.html#rph-automation","experimental":true,"parameters":[{"name":"mode","$ref":"AutoResponseMode"}]},{"name":"generateTestReport","description":"Generates a report for testing.","experimental":true,"parameters":[{"name":"message","description":"Message to be displayed in the report.","type":"string"},{"name":"group","description":"Specifies the endpoint group to deliver the report to.","optional":true,"type":"string"}]},{"name":"waitForDebugger","description":"Pauses page execution. Can be resumed using generic Runtime.runIfWaitingForDebugger.","experimental":true},{"name":"setInterceptFileChooserDialog","description":"Intercept file chooser requests and transfer control to protocol clients.\\nWhen file chooser interception is enabled, native file chooser dialog is not shown.\\nInstead, a protocol event `Page.fileChooserOpened` is emitted.","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"setPrerenderingAllowed","description":"Enable/disable prerendering manually.\\n\\nThis command is a short-term solution for https://crbug.com/1440085.\\nSee https://docs.google.com/document/d/12HVmFxYj5Jc-eJr5OmWsa2bqTJsbgGLKI6ZIyx0_wpA\\nfor more details.\\n\\nTODO(https://crbug.com/1440085): Remove this once Puppeteer supports tab targets.","experimental":true,"parameters":[{"name":"isAllowed","type":"boolean"}]}],"events":[{"name":"domContentEventFired","parameters":[{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"fileChooserOpened","description":"Emitted only when `page.interceptFileChooser` is enabled.","parameters":[{"name":"frameId","description":"Id of the frame containing input node.","experimental":true,"$ref":"FrameId"},{"name":"mode","description":"Input mode.","type":"string","enum":["selectSingle","selectMultiple"]},{"name":"backendNodeId","description":"Input node id. Only present for file choosers opened via an `<input type=\\"file\\">` element.","experimental":true,"optional":true,"$ref":"DOM.BackendNodeId"}]},{"name":"frameAttached","description":"Fired when frame has been attached to its parent.","parameters":[{"name":"frameId","description":"Id of the frame that has been attached.","$ref":"FrameId"},{"name":"parentFrameId","description":"Parent frame identifier.","$ref":"FrameId"},{"name":"stack","description":"JavaScript stack trace of when frame was attached, only set if frame initiated from script.","optional":true,"$ref":"Runtime.StackTrace"}]},{"name":"frameClearedScheduledNavigation","description":"Fired when frame no longer has a scheduled navigation.","deprecated":true,"parameters":[{"name":"frameId","description":"Id of the frame that has cleared its scheduled navigation.","$ref":"FrameId"}]},{"name":"frameDetached","description":"Fired when frame has been detached from its parent.","parameters":[{"name":"frameId","description":"Id of the frame that has been detached.","$ref":"FrameId"},{"name":"reason","experimental":true,"type":"string","enum":["remove","swap"]}]},{"name":"frameNavigated","description":"Fired once navigation of the frame has completed. Frame is now associated with the new loader.","parameters":[{"name":"frame","description":"Frame object.","$ref":"Frame"},{"name":"type","experimental":true,"$ref":"NavigationType"}]},{"name":"documentOpened","description":"Fired when opening document to write to.","experimental":true,"parameters":[{"name":"frame","description":"Frame object.","$ref":"Frame"}]},{"name":"frameResized","experimental":true},{"name":"frameRequestedNavigation","description":"Fired when a renderer-initiated navigation is requested.\\nNavigation may still be cancelled after the event is issued.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that is being navigated.","$ref":"FrameId"},{"name":"reason","description":"The reason for the navigation.","$ref":"ClientNavigationReason"},{"name":"url","description":"The destination URL for the requested navigation.","type":"string"},{"name":"disposition","description":"The disposition for the navigation.","$ref":"ClientNavigationDisposition"}]},{"name":"frameScheduledNavigation","description":"Fired when frame schedules a potential navigation.","deprecated":true,"parameters":[{"name":"frameId","description":"Id of the frame that has scheduled a navigation.","$ref":"FrameId"},{"name":"delay","description":"Delay (in seconds) until the navigation is scheduled to begin. The navigation is not\\nguaranteed to start.","type":"number"},{"name":"reason","description":"The reason for the navigation.","$ref":"ClientNavigationReason"},{"name":"url","description":"The destination URL for the scheduled navigation.","type":"string"}]},{"name":"frameStartedLoading","description":"Fired when frame has started loading.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that has started loading.","$ref":"FrameId"}]},{"name":"frameStoppedLoading","description":"Fired when frame has stopped loading.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that has stopped loading.","$ref":"FrameId"}]},{"name":"downloadWillBegin","description":"Fired when page is about to start a download.\\nDeprecated. Use Browser.downloadWillBegin instead.","experimental":true,"deprecated":true,"parameters":[{"name":"frameId","description":"Id of the frame that caused download to begin.","$ref":"FrameId"},{"name":"guid","description":"Global unique identifier of the download.","type":"string"},{"name":"url","description":"URL of the resource being downloaded.","type":"string"},{"name":"suggestedFilename","description":"Suggested file name of the resource (the actual name of the file saved on disk may differ).","type":"string"}]},{"name":"downloadProgress","description":"Fired when download makes progress. Last call has |done| == true.\\nDeprecated. Use Browser.downloadProgress instead.","experimental":true,"deprecated":true,"parameters":[{"name":"guid","description":"Global unique identifier of the download.","type":"string"},{"name":"totalBytes","description":"Total expected bytes to download.","type":"number"},{"name":"receivedBytes","description":"Total bytes received.","type":"number"},{"name":"state","description":"Download status.","type":"string","enum":["inProgress","completed","canceled"]}]},{"name":"interstitialHidden","description":"Fired when interstitial page was hidden"},{"name":"interstitialShown","description":"Fired when interstitial page was shown"},{"name":"javascriptDialogClosed","description":"Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) has been\\nclosed.","parameters":[{"name":"result","description":"Whether dialog was confirmed.","type":"boolean"},{"name":"userInput","description":"User input in case of prompt.","type":"string"}]},{"name":"javascriptDialogOpening","description":"Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) is about to\\nopen.","parameters":[{"name":"url","description":"Frame url.","type":"string"},{"name":"message","description":"Message that will be displayed by the dialog.","type":"string"},{"name":"type","description":"Dialog type.","$ref":"DialogType"},{"name":"hasBrowserHandler","description":"True iff browser is capable showing or acting on the given dialog. When browser has no\\ndialog handler for given target, calling alert while Page domain is engaged will stall\\nthe page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.","type":"boolean"},{"name":"defaultPrompt","description":"Default dialog prompt.","optional":true,"type":"string"}]},{"name":"lifecycleEvent","description":"Fired for top level page lifecycle events such as navigation, load, paint, etc.","parameters":[{"name":"frameId","description":"Id of the frame.","$ref":"FrameId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"Network.LoaderId"},{"name":"name","type":"string"},{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"backForwardCacheNotUsed","description":"Fired for failed bfcache history navigations if BackForwardCache feature is enabled. Do\\nnot assume any ordering with the Page.frameNavigated event. This event is fired only for\\nmain-frame history navigation where the document changes (non-same-document navigations),\\nwhen bfcache navigation fails.","experimental":true,"parameters":[{"name":"loaderId","description":"The loader id for the associated navgation.","$ref":"Network.LoaderId"},{"name":"frameId","description":"The frame id of the associated frame.","$ref":"FrameId"},{"name":"notRestoredExplanations","description":"Array of reasons why the page could not be cached. This must not be empty.","type":"array","items":{"$ref":"BackForwardCacheNotRestoredExplanation"}},{"name":"notRestoredExplanationsTree","description":"Tree structure of reasons why the page could not be cached for each frame.","optional":true,"$ref":"BackForwardCacheNotRestoredExplanationTree"}]},{"name":"loadEventFired","parameters":[{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"navigatedWithinDocument","description":"Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame.","$ref":"FrameId"},{"name":"url","description":"Frame\'s new url.","type":"string"}]},{"name":"screencastFrame","description":"Compressed image data requested by the `startScreencast`.","experimental":true,"parameters":[{"name":"data","description":"Base64-encoded compressed image. (Encoded as a base64 string when passed over JSON)","type":"string"},{"name":"metadata","description":"Screencast frame metadata.","$ref":"ScreencastFrameMetadata"},{"name":"sessionId","description":"Frame number.","type":"integer"}]},{"name":"screencastVisibilityChanged","description":"Fired when the page with currently enabled screencast was shown or hidden `.","experimental":true,"parameters":[{"name":"visible","description":"True if the page is visible.","type":"boolean"}]},{"name":"windowOpen","description":"Fired when a new window is going to be opened, via window.open(), link click, form submission,\\netc.","parameters":[{"name":"url","description":"The URL for the new window.","type":"string"},{"name":"windowName","description":"Window name.","type":"string"},{"name":"windowFeatures","description":"An array of enabled window features.","type":"array","items":{"type":"string"}},{"name":"userGesture","description":"Whether or not it was triggered by user gesture.","type":"boolean"}]},{"name":"compilationCacheProduced","description":"Issued for every compilation cache generated. Is only available\\nif Page.setGenerateCompilationCache is enabled.","experimental":true,"parameters":[{"name":"url","type":"string"},{"name":"data","description":"Base64-encoded data (Encoded as a base64 string when passed over JSON)","type":"string"}]}]},{"domain":"Performance","types":[{"id":"Metric","description":"Run-time execution metric.","type":"object","properties":[{"name":"name","description":"Metric name.","type":"string"},{"name":"value","description":"Metric value.","type":"number"}]}],"commands":[{"name":"disable","description":"Disable collecting and reporting metrics."},{"name":"enable","description":"Enable collecting and reporting metrics.","parameters":[{"name":"timeDomain","description":"Time domain to use for collecting and reporting duration metrics.","optional":true,"type":"string","enum":["timeTicks","threadTicks"]}]},{"name":"setTimeDomain","description":"Sets time domain to use for collecting and reporting duration metrics.\\nNote that this must be called before enabling metrics collection. Calling\\nthis method while metrics collection is enabled returns an error.","experimental":true,"deprecated":true,"parameters":[{"name":"timeDomain","description":"Time domain","type":"string","enum":["timeTicks","threadTicks"]}]},{"name":"getMetrics","description":"Retrieve current values of run-time metrics.","returns":[{"name":"metrics","description":"Current values for run-time metrics.","type":"array","items":{"$ref":"Metric"}}]}],"events":[{"name":"metrics","description":"Current values of the metrics.","parameters":[{"name":"metrics","description":"Current values of the metrics.","type":"array","items":{"$ref":"Metric"}},{"name":"title","description":"Timestamp title.","type":"string"}]}]},{"domain":"PerformanceTimeline","description":"Reporting of performance timeline events, as specified in\\nhttps://w3c.github.io/performance-timeline/#dom-performanceobserver.","experimental":true,"dependencies":["DOM","Network"],"types":[{"id":"LargestContentfulPaint","description":"See https://github.com/WICG/LargestContentfulPaint and largest_contentful_paint.idl","type":"object","properties":[{"name":"renderTime","$ref":"Network.TimeSinceEpoch"},{"name":"loadTime","$ref":"Network.TimeSinceEpoch"},{"name":"size","description":"The number of pixels being painted.","type":"number"},{"name":"elementId","description":"The id attribute of the element, if available.","optional":true,"type":"string"},{"name":"url","description":"The URL of the image (may be trimmed).","optional":true,"type":"string"},{"name":"nodeId","optional":true,"$ref":"DOM.BackendNodeId"}]},{"id":"LayoutShiftAttribution","type":"object","properties":[{"name":"previousRect","$ref":"DOM.Rect"},{"name":"currentRect","$ref":"DOM.Rect"},{"name":"nodeId","optional":true,"$ref":"DOM.BackendNodeId"}]},{"id":"LayoutShift","description":"See https://wicg.github.io/layout-instability/#sec-layout-shift and layout_shift.idl","type":"object","properties":[{"name":"value","description":"Score increment produced by this event.","type":"number"},{"name":"hadRecentInput","type":"boolean"},{"name":"lastInputTime","$ref":"Network.TimeSinceEpoch"},{"name":"sources","type":"array","items":{"$ref":"LayoutShiftAttribution"}}]},{"id":"TimelineEvent","type":"object","properties":[{"name":"frameId","description":"Identifies the frame that this event is related to. Empty for non-frame targets.","$ref":"Page.FrameId"},{"name":"type","description":"The event type, as specified in https://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype\\nThis determines which of the optional \\"details\\" fiedls is present.","type":"string"},{"name":"name","description":"Name may be empty depending on the type.","type":"string"},{"name":"time","description":"Time in seconds since Epoch, monotonically increasing within document lifetime.","$ref":"Network.TimeSinceEpoch"},{"name":"duration","description":"Event duration, if applicable.","optional":true,"type":"number"},{"name":"lcpDetails","optional":true,"$ref":"LargestContentfulPaint"},{"name":"layoutShiftDetails","optional":true,"$ref":"LayoutShift"}]}],"commands":[{"name":"enable","description":"Previously buffered events would be reported before method returns.\\nSee also: timelineEventAdded","parameters":[{"name":"eventTypes","description":"The types of event to report, as specified in\\nhttps://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype\\nThe specified filter overrides any previous filters, passing empty\\nfilter disables recording.\\nNote that not all types exposed to the web platform are currently supported.","type":"array","items":{"type":"string"}}]}],"events":[{"name":"timelineEventAdded","description":"Sent when a performance timeline event is added. See reportPerformanceTimeline method.","parameters":[{"name":"event","$ref":"TimelineEvent"}]}]},{"domain":"Security","description":"Security","types":[{"id":"CertificateId","description":"An internal certificate ID value.","type":"integer"},{"id":"MixedContentType","description":"A description of mixed content (HTTP resources on HTTPS pages), as defined by\\nhttps://www.w3.org/TR/mixed-content/#categories","type":"string","enum":["blockable","optionally-blockable","none"]},{"id":"SecurityState","description":"The security level of a page or resource.","type":"string","enum":["unknown","neutral","insecure","secure","info","insecure-broken"]},{"id":"CertificateSecurityState","description":"Details about the security state of the page certificate.","experimental":true,"type":"object","properties":[{"name":"protocol","description":"Protocol name (e.g. \\"TLS 1.2\\" or \\"QUIC\\").","type":"string"},{"name":"keyExchange","description":"Key Exchange used by the connection, or the empty string if not applicable.","type":"string"},{"name":"keyExchangeGroup","description":"(EC)DH group used by the connection, if applicable.","optional":true,"type":"string"},{"name":"cipher","description":"Cipher name.","type":"string"},{"name":"mac","description":"TLS MAC. Note that AEAD ciphers do not have separate MACs.","optional":true,"type":"string"},{"name":"certificate","description":"Page certificate.","type":"array","items":{"type":"string"}},{"name":"subjectName","description":"Certificate subject name.","type":"string"},{"name":"issuer","description":"Name of the issuing CA.","type":"string"},{"name":"validFrom","description":"Certificate valid from date.","$ref":"Network.TimeSinceEpoch"},{"name":"validTo","description":"Certificate valid to (expiration) date","$ref":"Network.TimeSinceEpoch"},{"name":"certificateNetworkError","description":"The highest priority network error code, if the certificate has an error.","optional":true,"type":"string"},{"name":"certificateHasWeakSignature","description":"True if the certificate uses a weak signature aglorithm.","type":"boolean"},{"name":"certificateHasSha1Signature","description":"True if the certificate has a SHA1 signature in the chain.","type":"boolean"},{"name":"modernSSL","description":"True if modern SSL","type":"boolean"},{"name":"obsoleteSslProtocol","description":"True if the connection is using an obsolete SSL protocol.","type":"boolean"},{"name":"obsoleteSslKeyExchange","description":"True if the connection is using an obsolete SSL key exchange.","type":"boolean"},{"name":"obsoleteSslCipher","description":"True if the connection is using an obsolete SSL cipher.","type":"boolean"},{"name":"obsoleteSslSignature","description":"True if the connection is using an obsolete SSL signature.","type":"boolean"}]},{"id":"SafetyTipStatus","experimental":true,"type":"string","enum":["badReputation","lookalike"]},{"id":"SafetyTipInfo","experimental":true,"type":"object","properties":[{"name":"safetyTipStatus","description":"Describes whether the page triggers any safety tips or reputation warnings. Default is unknown.","$ref":"SafetyTipStatus"},{"name":"safeUrl","description":"The URL the safety tip suggested (\\"Did you mean?\\"). Only filled in for lookalike matches.","optional":true,"type":"string"}]},{"id":"VisibleSecurityState","description":"Security state information about the page.","experimental":true,"type":"object","properties":[{"name":"securityState","description":"The security level of the page.","$ref":"SecurityState"},{"name":"certificateSecurityState","description":"Security state details about the page certificate.","optional":true,"$ref":"CertificateSecurityState"},{"name":"safetyTipInfo","description":"The type of Safety Tip triggered on the page. Note that this field will be set even if the Safety Tip UI was not actually shown.","optional":true,"$ref":"SafetyTipInfo"},{"name":"securityStateIssueIds","description":"Array of security state issues ids.","type":"array","items":{"type":"string"}}]},{"id":"SecurityStateExplanation","description":"An explanation of an factor contributing to the security state.","type":"object","properties":[{"name":"securityState","description":"Security state representing the severity of the factor being explained.","$ref":"SecurityState"},{"name":"title","description":"Title describing the type of factor.","type":"string"},{"name":"summary","description":"Short phrase describing the type of factor.","type":"string"},{"name":"description","description":"Full text explanation of the factor.","type":"string"},{"name":"mixedContentType","description":"The type of mixed content described by the explanation.","$ref":"MixedContentType"},{"name":"certificate","description":"Page certificate.","type":"array","items":{"type":"string"}},{"name":"recommendations","description":"Recommendations to fix any issues.","optional":true,"type":"array","items":{"type":"string"}}]},{"id":"InsecureContentStatus","description":"Information about insecure content on the page.","deprecated":true,"type":"object","properties":[{"name":"ranMixedContent","description":"Always false.","type":"boolean"},{"name":"displayedMixedContent","description":"Always false.","type":"boolean"},{"name":"containedMixedForm","description":"Always false.","type":"boolean"},{"name":"ranContentWithCertErrors","description":"Always false.","type":"boolean"},{"name":"displayedContentWithCertErrors","description":"Always false.","type":"boolean"},{"name":"ranInsecureContentStyle","description":"Always set to unknown.","$ref":"SecurityState"},{"name":"displayedInsecureContentStyle","description":"Always set to unknown.","$ref":"SecurityState"}]},{"id":"CertificateErrorAction","description":"The action to take when a certificate error occurs. continue will continue processing the\\nrequest and cancel will cancel the request.","type":"string","enum":["continue","cancel"]}],"commands":[{"name":"disable","description":"Disables tracking security state changes."},{"name":"enable","description":"Enables tracking security state changes."},{"name":"setIgnoreCertificateErrors","description":"Enable/disable whether all certificate errors should be ignored.","experimental":true,"parameters":[{"name":"ignore","description":"If true, all certificate errors will be ignored.","type":"boolean"}]},{"name":"handleCertificateError","description":"Handles a certificate error that fired a certificateError event.","deprecated":true,"parameters":[{"name":"eventId","description":"The ID of the event.","type":"integer"},{"name":"action","description":"The action to take on the certificate error.","$ref":"CertificateErrorAction"}]},{"name":"setOverrideCertificateErrors","description":"Enable/disable overriding certificate errors. If enabled, all certificate error events need to\\nbe handled by the DevTools client and should be answered with `handleCertificateError` commands.","deprecated":true,"parameters":[{"name":"override","description":"If true, certificate errors will be overridden.","type":"boolean"}]}],"events":[{"name":"certificateError","description":"There is a certificate error. If overriding certificate errors is enabled, then it should be\\nhandled with the `handleCertificateError` command. Note: this event does not fire if the\\ncertificate error has been allowed internally. Only one client per target should override\\ncertificate errors at the same time.","deprecated":true,"parameters":[{"name":"eventId","description":"The ID of the event.","type":"integer"},{"name":"errorType","description":"The type of the error.","type":"string"},{"name":"requestURL","description":"The url that was requested.","type":"string"}]},{"name":"visibleSecurityStateChanged","description":"The security state of the page changed.","experimental":true,"parameters":[{"name":"visibleSecurityState","description":"Security state information about the page.","$ref":"VisibleSecurityState"}]},{"name":"securityStateChanged","description":"The security state of the page changed. No longer being sent.","deprecated":true,"parameters":[{"name":"securityState","description":"Security state.","$ref":"SecurityState"},{"name":"schemeIsCryptographic","description":"True if the page was loaded over cryptographic transport such as HTTPS.","deprecated":true,"type":"boolean"},{"name":"explanations","description":"Previously a list of explanations for the security state. Now always\\nempty.","deprecated":true,"type":"array","items":{"$ref":"SecurityStateExplanation"}},{"name":"insecureContentStatus","description":"Information about insecure content on the page.","deprecated":true,"$ref":"InsecureContentStatus"},{"name":"summary","description":"Overrides user-visible description of the state. Always omitted.","deprecated":true,"optional":true,"type":"string"}]}]},{"domain":"ServiceWorker","experimental":true,"dependencies":["Target"],"types":[{"id":"RegistrationID","type":"string"},{"id":"ServiceWorkerRegistration","description":"ServiceWorker registration.","type":"object","properties":[{"name":"registrationId","$ref":"RegistrationID"},{"name":"scopeURL","type":"string"},{"name":"isDeleted","type":"boolean"}]},{"id":"ServiceWorkerVersionRunningStatus","type":"string","enum":["stopped","starting","running","stopping"]},{"id":"ServiceWorkerVersionStatus","type":"string","enum":["new","installing","installed","activating","activated","redundant"]},{"id":"ServiceWorkerVersion","description":"ServiceWorker version.","type":"object","properties":[{"name":"versionId","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"scriptURL","type":"string"},{"name":"runningStatus","$ref":"ServiceWorkerVersionRunningStatus"},{"name":"status","$ref":"ServiceWorkerVersionStatus"},{"name":"scriptLastModified","description":"The Last-Modified header value of the main script.","optional":true,"type":"number"},{"name":"scriptResponseTime","description":"The time at which the response headers of the main script were received from the server.\\nFor cached script it is the last time the cache entry was validated.","optional":true,"type":"number"},{"name":"controlledClients","optional":true,"type":"array","items":{"$ref":"Target.TargetID"}},{"name":"targetId","optional":true,"$ref":"Target.TargetID"}]},{"id":"ServiceWorkerErrorMessage","description":"ServiceWorker error message.","type":"object","properties":[{"name":"errorMessage","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"versionId","type":"string"},{"name":"sourceURL","type":"string"},{"name":"lineNumber","type":"integer"},{"name":"columnNumber","type":"integer"}]}],"commands":[{"name":"deliverPushMessage","parameters":[{"name":"origin","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"data","type":"string"}]},{"name":"disable"},{"name":"dispatchSyncEvent","parameters":[{"name":"origin","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"tag","type":"string"},{"name":"lastChance","type":"boolean"}]},{"name":"dispatchPeriodicSyncEvent","parameters":[{"name":"origin","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"tag","type":"string"}]},{"name":"enable"},{"name":"inspectWorker","parameters":[{"name":"versionId","type":"string"}]},{"name":"setForceUpdateOnPageLoad","parameters":[{"name":"forceUpdateOnPageLoad","type":"boolean"}]},{"name":"skipWaiting","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"startWorker","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"stopAllWorkers"},{"name":"stopWorker","parameters":[{"name":"versionId","type":"string"}]},{"name":"unregister","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"updateRegistration","parameters":[{"name":"scopeURL","type":"string"}]}],"events":[{"name":"workerErrorReported","parameters":[{"name":"errorMessage","$ref":"ServiceWorkerErrorMessage"}]},{"name":"workerRegistrationUpdated","parameters":[{"name":"registrations","type":"array","items":{"$ref":"ServiceWorkerRegistration"}}]},{"name":"workerVersionUpdated","parameters":[{"name":"versions","type":"array","items":{"$ref":"ServiceWorkerVersion"}}]}]},{"domain":"Storage","experimental":true,"dependencies":["Browser","Network"],"types":[{"id":"SerializedStorageKey","type":"string"},{"id":"StorageType","description":"Enum of possible storage types.","type":"string","enum":["appcache","cookies","file_systems","indexeddb","local_storage","shader_cache","websql","service_workers","cache_storage","interest_groups","shared_storage","storage_buckets","all","other"]},{"id":"UsageForType","description":"Usage for a storage type.","type":"object","properties":[{"name":"storageType","description":"Name of storage type.","$ref":"StorageType"},{"name":"usage","description":"Storage usage (bytes).","type":"number"}]},{"id":"TrustTokens","description":"Pair of issuer origin and number of available (signed, but not used) Trust\\nTokens from that issuer.","experimental":true,"type":"object","properties":[{"name":"issuerOrigin","type":"string"},{"name":"count","type":"number"}]},{"id":"InterestGroupAccessType","description":"Enum of interest group access types.","type":"string","enum":["join","leave","update","loaded","bid","win"]},{"id":"InterestGroupAd","description":"Ad advertising element inside an interest group.","type":"object","properties":[{"name":"renderUrl","type":"string"},{"name":"metadata","optional":true,"type":"string"}]},{"id":"InterestGroupDetails","description":"The full details of an interest group.","type":"object","properties":[{"name":"ownerOrigin","type":"string"},{"name":"name","type":"string"},{"name":"expirationTime","$ref":"Network.TimeSinceEpoch"},{"name":"joiningOrigin","type":"string"},{"name":"biddingUrl","optional":true,"type":"string"},{"name":"biddingWasmHelperUrl","optional":true,"type":"string"},{"name":"updateUrl","optional":true,"type":"string"},{"name":"trustedBiddingSignalsUrl","optional":true,"type":"string"},{"name":"trustedBiddingSignalsKeys","type":"array","items":{"type":"string"}},{"name":"userBiddingSignals","optional":true,"type":"string"},{"name":"ads","type":"array","items":{"$ref":"InterestGroupAd"}},{"name":"adComponents","type":"array","items":{"$ref":"InterestGroupAd"}}]},{"id":"SharedStorageAccessType","description":"Enum of shared storage access types.","type":"string","enum":["documentAddModule","documentSelectURL","documentRun","documentSet","documentAppend","documentDelete","documentClear","workletSet","workletAppend","workletDelete","workletClear","workletGet","workletKeys","workletEntries","workletLength","workletRemainingBudget"]},{"id":"SharedStorageEntry","description":"Struct for a single key-value pair in an origin\'s shared storage.","type":"object","properties":[{"name":"key","type":"string"},{"name":"value","type":"string"}]},{"id":"SharedStorageMetadata","description":"Details for an origin\'s shared storage.","type":"object","properties":[{"name":"creationTime","$ref":"Network.TimeSinceEpoch"},{"name":"length","type":"integer"},{"name":"remainingBudget","type":"number"}]},{"id":"SharedStorageReportingMetadata","description":"Pair of reporting metadata details for a candidate URL for `selectURL()`.","type":"object","properties":[{"name":"eventType","type":"string"},{"name":"reportingUrl","type":"string"}]},{"id":"SharedStorageUrlWithMetadata","description":"Bundles a candidate URL with its reporting metadata.","type":"object","properties":[{"name":"url","description":"Spec of candidate URL.","type":"string"},{"name":"reportingMetadata","description":"Any associated reporting metadata.","type":"array","items":{"$ref":"SharedStorageReportingMetadata"}}]},{"id":"SharedStorageAccessParams","description":"Bundles the parameters for shared storage access events whose\\npresence/absence can vary according to SharedStorageAccessType.","type":"object","properties":[{"name":"scriptSourceUrl","description":"Spec of the module script URL.\\nPresent only for SharedStorageAccessType.documentAddModule.","optional":true,"type":"string"},{"name":"operationName","description":"Name of the registered operation to be run.\\nPresent only for SharedStorageAccessType.documentRun and\\nSharedStorageAccessType.documentSelectURL.","optional":true,"type":"string"},{"name":"serializedData","description":"The operation\'s serialized data in bytes (converted to a string).\\nPresent only for SharedStorageAccessType.documentRun and\\nSharedStorageAccessType.documentSelectURL.","optional":true,"type":"string"},{"name":"urlsWithMetadata","description":"Array of candidate URLs\' specs, along with any associated metadata.\\nPresent only for SharedStorageAccessType.documentSelectURL.","optional":true,"type":"array","items":{"$ref":"SharedStorageUrlWithMetadata"}},{"name":"key","description":"Key for a specific entry in an origin\'s shared storage.\\nPresent only for SharedStorageAccessType.documentSet,\\nSharedStorageAccessType.documentAppend,\\nSharedStorageAccessType.documentDelete,\\nSharedStorageAccessType.workletSet,\\nSharedStorageAccessType.workletAppend,\\nSharedStorageAccessType.workletDelete, and\\nSharedStorageAccessType.workletGet.","optional":true,"type":"string"},{"name":"value","description":"Value for a specific entry in an origin\'s shared storage.\\nPresent only for SharedStorageAccessType.documentSet,\\nSharedStorageAccessType.documentAppend,\\nSharedStorageAccessType.workletSet, and\\nSharedStorageAccessType.workletAppend.","optional":true,"type":"string"},{"name":"ignoreIfPresent","description":"Whether or not to set an entry for a key if that key is already present.\\nPresent only for SharedStorageAccessType.documentSet and\\nSharedStorageAccessType.workletSet.","optional":true,"type":"boolean"}]},{"id":"StorageBucketsDurability","type":"string","enum":["relaxed","strict"]},{"id":"StorageBucket","type":"object","properties":[{"name":"storageKey","$ref":"SerializedStorageKey"},{"name":"name","description":"If not specified, it is the default bucket of the storageKey.","optional":true,"type":"string"}]},{"id":"StorageBucketInfo","type":"object","properties":[{"name":"bucket","$ref":"StorageBucket"},{"name":"id","type":"string"},{"name":"expiration","$ref":"Network.TimeSinceEpoch"},{"name":"quota","description":"Storage quota (bytes).","type":"number"},{"name":"persistent","type":"boolean"},{"name":"durability","$ref":"StorageBucketsDurability"}]},{"id":"AttributionReportingSourceType","experimental":true,"type":"string","enum":["navigation","event"]},{"id":"UnsignedInt64AsBase10","experimental":true,"type":"string"},{"id":"UnsignedInt128AsBase16","experimental":true,"type":"string"},{"id":"SignedInt64AsBase10","experimental":true,"type":"string"},{"id":"AttributionReportingFilterDataEntry","experimental":true,"type":"object","properties":[{"name":"key","type":"string"},{"name":"values","type":"array","items":{"type":"string"}}]},{"id":"AttributionReportingAggregationKeysEntry","experimental":true,"type":"object","properties":[{"name":"key","type":"string"},{"name":"value","$ref":"UnsignedInt128AsBase16"}]},{"id":"AttributionReportingSourceRegistration","experimental":true,"type":"object","properties":[{"name":"time","$ref":"Network.TimeSinceEpoch"},{"name":"expiry","description":"duration in seconds","optional":true,"type":"integer"},{"name":"eventReportWindow","description":"duration in seconds","optional":true,"type":"integer"},{"name":"aggregatableReportWindow","description":"duration in seconds","optional":true,"type":"integer"},{"name":"type","$ref":"AttributionReportingSourceType"},{"name":"sourceOrigin","type":"string"},{"name":"reportingOrigin","type":"string"},{"name":"destinationSites","type":"array","items":{"type":"string"}},{"name":"eventId","$ref":"UnsignedInt64AsBase10"},{"name":"priority","$ref":"SignedInt64AsBase10"},{"name":"filterData","type":"array","items":{"$ref":"AttributionReportingFilterDataEntry"}},{"name":"aggregationKeys","type":"array","items":{"$ref":"AttributionReportingAggregationKeysEntry"}},{"name":"debugKey","optional":true,"$ref":"UnsignedInt64AsBase10"}]},{"id":"AttributionReportingSourceRegistrationResult","experimental":true,"type":"string","enum":["success","internalError","insufficientSourceCapacity","insufficientUniqueDestinationCapacity","excessiveReportingOrigins","prohibitedByBrowserPolicy","successNoised","destinationReportingLimitReached","destinationGlobalLimitReached","destinationBothLimitsReached","reportingOriginsPerSiteLimitReached"]}],"commands":[{"name":"getStorageKeyForFrame","description":"Returns a storage key given a frame id.","parameters":[{"name":"frameId","$ref":"Page.FrameId"}],"returns":[{"name":"storageKey","$ref":"SerializedStorageKey"}]},{"name":"clearDataForOrigin","description":"Clears storage for origin.","parameters":[{"name":"origin","description":"Security origin.","type":"string"},{"name":"storageTypes","description":"Comma separated list of StorageType to clear.","type":"string"}]},{"name":"clearDataForStorageKey","description":"Clears storage for storage key.","parameters":[{"name":"storageKey","description":"Storage key.","type":"string"},{"name":"storageTypes","description":"Comma separated list of StorageType to clear.","type":"string"}]},{"name":"getCookies","description":"Returns all browser cookies.","parameters":[{"name":"browserContextId","description":"Browser context to use when called on the browser endpoint.","optional":true,"$ref":"Browser.BrowserContextID"}],"returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Network.Cookie"}}]},{"name":"setCookies","description":"Sets given cookies.","parameters":[{"name":"cookies","description":"Cookies to be set.","type":"array","items":{"$ref":"Network.CookieParam"}},{"name":"browserContextId","description":"Browser context to use when called on the browser endpoint.","optional":true,"$ref":"Browser.BrowserContextID"}]},{"name":"clearCookies","description":"Clears cookies.","parameters":[{"name":"browserContextId","description":"Browser context to use when called on the browser endpoint.","optional":true,"$ref":"Browser.BrowserContextID"}]},{"name":"getUsageAndQuota","description":"Returns usage and quota in bytes.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}],"returns":[{"name":"usage","description":"Storage usage (bytes).","type":"number"},{"name":"quota","description":"Storage quota (bytes).","type":"number"},{"name":"overrideActive","description":"Whether or not the origin has an active storage quota override","type":"boolean"},{"name":"usageBreakdown","description":"Storage usage per type (bytes).","type":"array","items":{"$ref":"UsageForType"}}]},{"name":"overrideQuotaForOrigin","description":"Override quota for the specified origin","experimental":true,"parameters":[{"name":"origin","description":"Security origin.","type":"string"},{"name":"quotaSize","description":"The quota size (in bytes) to override the original quota with.\\nIf this is called multiple times, the overridden quota will be equal to\\nthe quotaSize provided in the final call. If this is called without\\nspecifying a quotaSize, the quota will be reset to the default value for\\nthe specified origin. If this is called multiple times with different\\norigins, the override will be maintained for each origin until it is\\ndisabled (called without a quotaSize).","optional":true,"type":"number"}]},{"name":"trackCacheStorageForOrigin","description":"Registers origin to be notified when an update occurs to its cache storage list.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"trackCacheStorageForStorageKey","description":"Registers storage key to be notified when an update occurs to its cache storage list.","parameters":[{"name":"storageKey","description":"Storage key.","type":"string"}]},{"name":"trackIndexedDBForOrigin","description":"Registers origin to be notified when an update occurs to its IndexedDB.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"trackIndexedDBForStorageKey","description":"Registers storage key to be notified when an update occurs to its IndexedDB.","parameters":[{"name":"storageKey","description":"Storage key.","type":"string"}]},{"name":"untrackCacheStorageForOrigin","description":"Unregisters origin from receiving notifications for cache storage.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"untrackCacheStorageForStorageKey","description":"Unregisters storage key from receiving notifications for cache storage.","parameters":[{"name":"storageKey","description":"Storage key.","type":"string"}]},{"name":"untrackIndexedDBForOrigin","description":"Unregisters origin from receiving notifications for IndexedDB.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"untrackIndexedDBForStorageKey","description":"Unregisters storage key from receiving notifications for IndexedDB.","parameters":[{"name":"storageKey","description":"Storage key.","type":"string"}]},{"name":"getTrustTokens","description":"Returns the number of stored Trust Tokens per issuer for the\\ncurrent browsing context.","experimental":true,"returns":[{"name":"tokens","type":"array","items":{"$ref":"TrustTokens"}}]},{"name":"clearTrustTokens","description":"Removes all Trust Tokens issued by the provided issuerOrigin.\\nLeaves other stored data, including the issuer\'s Redemption Records, intact.","experimental":true,"parameters":[{"name":"issuerOrigin","type":"string"}],"returns":[{"name":"didDeleteTokens","description":"True if any tokens were deleted, false otherwise.","type":"boolean"}]},{"name":"getInterestGroupDetails","description":"Gets details for a named interest group.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"},{"name":"name","type":"string"}],"returns":[{"name":"details","$ref":"InterestGroupDetails"}]},{"name":"setInterestGroupTracking","description":"Enables/Disables issuing of interestGroupAccessed events.","experimental":true,"parameters":[{"name":"enable","type":"boolean"}]},{"name":"getSharedStorageMetadata","description":"Gets metadata for an origin\'s shared storage.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"}],"returns":[{"name":"metadata","$ref":"SharedStorageMetadata"}]},{"name":"getSharedStorageEntries","description":"Gets the entries in an given origin\'s shared storage.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"}],"returns":[{"name":"entries","type":"array","items":{"$ref":"SharedStorageEntry"}}]},{"name":"setSharedStorageEntry","description":"Sets entry with `key` and `value` for a given origin\'s shared storage.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"},{"name":"key","type":"string"},{"name":"value","type":"string"},{"name":"ignoreIfPresent","description":"If `ignoreIfPresent` is included and true, then only sets the entry if\\n`key` doesn\'t already exist.","optional":true,"type":"boolean"}]},{"name":"deleteSharedStorageEntry","description":"Deletes entry for `key` (if it exists) for a given origin\'s shared storage.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"},{"name":"key","type":"string"}]},{"name":"clearSharedStorageEntries","description":"Clears all entries for a given origin\'s shared storage.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"}]},{"name":"resetSharedStorageBudget","description":"Resets the budget for `ownerOrigin` by clearing all budget withdrawals.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"}]},{"name":"setSharedStorageTracking","description":"Enables/disables issuing of sharedStorageAccessed events.","experimental":true,"parameters":[{"name":"enable","type":"boolean"}]},{"name":"setStorageBucketTracking","description":"Set tracking for a storage key\'s buckets.","experimental":true,"parameters":[{"name":"storageKey","type":"string"},{"name":"enable","type":"boolean"}]},{"name":"deleteStorageBucket","description":"Deletes the Storage Bucket with the given storage key and bucket name.","experimental":true,"parameters":[{"name":"bucket","$ref":"StorageBucket"}]},{"name":"runBounceTrackingMitigations","description":"Deletes state for sites identified as potential bounce trackers, immediately.","experimental":true,"returns":[{"name":"deletedSites","type":"array","items":{"type":"string"}}]},{"name":"setAttributionReportingLocalTestingMode","description":"https://wicg.github.io/attribution-reporting-api/","experimental":true,"parameters":[{"name":"enabled","description":"If enabled, noise is suppressed and reports are sent immediately.","type":"boolean"}]},{"name":"setAttributionReportingTracking","description":"Enables/disables issuing of Attribution Reporting events.","experimental":true,"parameters":[{"name":"enable","type":"boolean"}]}],"events":[{"name":"cacheStorageContentUpdated","description":"A cache\'s contents have been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"storageKey","description":"Storage key to update.","type":"string"},{"name":"bucketId","description":"Storage bucket to update.","type":"string"},{"name":"cacheName","description":"Name of cache in origin.","type":"string"}]},{"name":"cacheStorageListUpdated","description":"A cache has been added/deleted.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"storageKey","description":"Storage key to update.","type":"string"},{"name":"bucketId","description":"Storage bucket to update.","type":"string"}]},{"name":"indexedDBContentUpdated","description":"The origin\'s IndexedDB object store has been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"storageKey","description":"Storage key to update.","type":"string"},{"name":"bucketId","description":"Storage bucket to update.","type":"string"},{"name":"databaseName","description":"Database to update.","type":"string"},{"name":"objectStoreName","description":"ObjectStore to update.","type":"string"}]},{"name":"indexedDBListUpdated","description":"The origin\'s IndexedDB database list has been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"storageKey","description":"Storage key to update.","type":"string"},{"name":"bucketId","description":"Storage bucket to update.","type":"string"}]},{"name":"interestGroupAccessed","description":"One of the interest groups was accessed by the associated page.","parameters":[{"name":"accessTime","$ref":"Network.TimeSinceEpoch"},{"name":"type","$ref":"InterestGroupAccessType"},{"name":"ownerOrigin","type":"string"},{"name":"name","type":"string"}]},{"name":"sharedStorageAccessed","description":"Shared storage was accessed by the associated page.\\nThe following parameters are included in all events.","parameters":[{"name":"accessTime","description":"Time of the access.","$ref":"Network.TimeSinceEpoch"},{"name":"type","description":"Enum value indicating the Shared Storage API method invoked.","$ref":"SharedStorageAccessType"},{"name":"mainFrameId","description":"DevTools Frame Token for the primary frame tree\'s root.","$ref":"Page.FrameId"},{"name":"ownerOrigin","description":"Serialized origin for the context that invoked the Shared Storage API.","type":"string"},{"name":"params","description":"The sub-parameters warapped by `params` are all optional and their\\npresence/absence depends on `type`.","$ref":"SharedStorageAccessParams"}]},{"name":"storageBucketCreatedOrUpdated","parameters":[{"name":"bucketInfo","$ref":"StorageBucketInfo"}]},{"name":"storageBucketDeleted","parameters":[{"name":"bucketId","type":"string"}]},{"name":"attributionReportingSourceRegistered","description":"TODO(crbug.com/1458532): Add other Attribution Reporting events, e.g.\\ntrigger registration.","experimental":true,"parameters":[{"name":"registration","$ref":"AttributionReportingSourceRegistration"},{"name":"result","$ref":"AttributionReportingSourceRegistrationResult"}]}]},{"domain":"SystemInfo","description":"The SystemInfo domain defines methods and events for querying low-level system information.","experimental":true,"types":[{"id":"GPUDevice","description":"Describes a single graphics processor (GPU).","type":"object","properties":[{"name":"vendorId","description":"PCI ID of the GPU vendor, if available; 0 otherwise.","type":"number"},{"name":"deviceId","description":"PCI ID of the GPU device, if available; 0 otherwise.","type":"number"},{"name":"subSysId","description":"Sub sys ID of the GPU, only available on Windows.","optional":true,"type":"number"},{"name":"revision","description":"Revision of the GPU, only available on Windows.","optional":true,"type":"number"},{"name":"vendorString","description":"String description of the GPU vendor, if the PCI ID is not available.","type":"string"},{"name":"deviceString","description":"String description of the GPU device, if the PCI ID is not available.","type":"string"},{"name":"driverVendor","description":"String description of the GPU driver vendor.","type":"string"},{"name":"driverVersion","description":"String description of the GPU driver version.","type":"string"}]},{"id":"Size","description":"Describes the width and height dimensions of an entity.","type":"object","properties":[{"name":"width","description":"Width in pixels.","type":"integer"},{"name":"height","description":"Height in pixels.","type":"integer"}]},{"id":"VideoDecodeAcceleratorCapability","description":"Describes a supported video decoding profile with its associated minimum and\\nmaximum resolutions.","type":"object","properties":[{"name":"profile","description":"Video codec profile that is supported, e.g. VP9 Profile 2.","type":"string"},{"name":"maxResolution","description":"Maximum video dimensions in pixels supported for this |profile|.","$ref":"Size"},{"name":"minResolution","description":"Minimum video dimensions in pixels supported for this |profile|.","$ref":"Size"}]},{"id":"VideoEncodeAcceleratorCapability","description":"Describes a supported video encoding profile with its associated maximum\\nresolution and maximum framerate.","type":"object","properties":[{"name":"profile","description":"Video codec profile that is supported, e.g H264 Main.","type":"string"},{"name":"maxResolution","description":"Maximum video dimensions in pixels supported for this |profile|.","$ref":"Size"},{"name":"maxFramerateNumerator","description":"Maximum encoding framerate in frames per second supported for this\\n|profile|, as fraction\'s numerator and denominator, e.g. 24/1 fps,\\n24000/1001 fps, etc.","type":"integer"},{"name":"maxFramerateDenominator","type":"integer"}]},{"id":"SubsamplingFormat","description":"YUV subsampling type of the pixels of a given image.","type":"string","enum":["yuv420","yuv422","yuv444"]},{"id":"ImageType","description":"Image format of a given image.","type":"string","enum":["jpeg","webp","unknown"]},{"id":"ImageDecodeAcceleratorCapability","description":"Describes a supported image decoding profile with its associated minimum and\\nmaximum resolutions and subsampling.","type":"object","properties":[{"name":"imageType","description":"Image coded, e.g. Jpeg.","$ref":"ImageType"},{"name":"maxDimensions","description":"Maximum supported dimensions of the image in pixels.","$ref":"Size"},{"name":"minDimensions","description":"Minimum supported dimensions of the image in pixels.","$ref":"Size"},{"name":"subsamplings","description":"Optional array of supported subsampling formats, e.g. 4:2:0, if known.","type":"array","items":{"$ref":"SubsamplingFormat"}}]},{"id":"GPUInfo","description":"Provides information about the GPU(s) on the system.","type":"object","properties":[{"name":"devices","description":"The graphics devices on the system. Element 0 is the primary GPU.","type":"array","items":{"$ref":"GPUDevice"}},{"name":"auxAttributes","description":"An optional dictionary of additional GPU related attributes.","optional":true,"type":"object"},{"name":"featureStatus","description":"An optional dictionary of graphics features and their status.","optional":true,"type":"object"},{"name":"driverBugWorkarounds","description":"An optional array of GPU driver bug workarounds.","type":"array","items":{"type":"string"}},{"name":"videoDecoding","description":"Supported accelerated video decoding capabilities.","type":"array","items":{"$ref":"VideoDecodeAcceleratorCapability"}},{"name":"videoEncoding","description":"Supported accelerated video encoding capabilities.","type":"array","items":{"$ref":"VideoEncodeAcceleratorCapability"}},{"name":"imageDecoding","description":"Supported accelerated image decoding capabilities.","type":"array","items":{"$ref":"ImageDecodeAcceleratorCapability"}}]},{"id":"ProcessInfo","description":"Represents process info.","type":"object","properties":[{"name":"type","description":"Specifies process type.","type":"string"},{"name":"id","description":"Specifies process id.","type":"integer"},{"name":"cpuTime","description":"Specifies cumulative CPU usage in seconds across all threads of the\\nprocess since the process start.","type":"number"}]}],"commands":[{"name":"getInfo","description":"Returns information about the system.","returns":[{"name":"gpu","description":"Information about the GPUs on the system.","$ref":"GPUInfo"},{"name":"modelName","description":"A platform-dependent description of the model of the machine. On Mac OS, this is, for\\nexample, \'MacBookPro\'. Will be the empty string if not supported.","type":"string"},{"name":"modelVersion","description":"A platform-dependent description of the version of the machine. On Mac OS, this is, for\\nexample, \'10.1\'. Will be the empty string if not supported.","type":"string"},{"name":"commandLine","description":"The command line string used to launch the browser. Will be the empty string if not\\nsupported.","type":"string"}]},{"name":"getFeatureState","description":"Returns information about the feature state.","parameters":[{"name":"featureState","type":"string"}],"returns":[{"name":"featureEnabled","type":"boolean"}]},{"name":"getProcessInfo","description":"Returns information about all running processes.","returns":[{"name":"processInfo","description":"An array of process info blocks.","type":"array","items":{"$ref":"ProcessInfo"}}]}]},{"domain":"Target","description":"Supports additional targets discovery and allows to attach to them.","types":[{"id":"TargetID","type":"string"},{"id":"SessionID","description":"Unique identifier of attached debugging session.","type":"string"},{"id":"TargetInfo","type":"object","properties":[{"name":"targetId","$ref":"TargetID"},{"name":"type","type":"string"},{"name":"title","type":"string"},{"name":"url","type":"string"},{"name":"attached","description":"Whether the target has an attached client.","type":"boolean"},{"name":"openerId","description":"Opener target Id","optional":true,"$ref":"TargetID"},{"name":"canAccessOpener","description":"Whether the target has access to the originating window.","experimental":true,"type":"boolean"},{"name":"openerFrameId","description":"Frame id of originating window (is only set if target has an opener).","experimental":true,"optional":true,"$ref":"Page.FrameId"},{"name":"browserContextId","experimental":true,"optional":true,"$ref":"Browser.BrowserContextID"},{"name":"subtype","description":"Provides additional details for specific target types. For example, for\\nthe type of \\"page\\", this may be set to \\"portal\\" or \\"prerender\\".","experimental":true,"optional":true,"type":"string"}]},{"id":"FilterEntry","description":"A filter used by target query/discovery/auto-attach operations.","experimental":true,"type":"object","properties":[{"name":"exclude","description":"If set, causes exclusion of mathcing targets from the list.","optional":true,"type":"boolean"},{"name":"type","description":"If not present, matches any type.","optional":true,"type":"string"}]},{"id":"TargetFilter","description":"The entries in TargetFilter are matched sequentially against targets and\\nthe first entry that matches determines if the target is included or not,\\ndepending on the value of `exclude` field in the entry.\\nIf filter is not specified, the one assumed is\\n[{type: \\"browser\\", exclude: true}, {type: \\"tab\\", exclude: true}, {}]\\n(i.e. include everything but `browser` and `tab`).","experimental":true,"type":"array","items":{"$ref":"FilterEntry"}},{"id":"RemoteLocation","experimental":true,"type":"object","properties":[{"name":"host","type":"string"},{"name":"port","type":"integer"}]}],"commands":[{"name":"activateTarget","description":"Activates (focuses) the target.","parameters":[{"name":"targetId","$ref":"TargetID"}]},{"name":"attachToTarget","description":"Attaches to the target with given id.","parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"flatten","description":"Enables \\"flat\\" access to the session via specifying sessionId attribute in the commands.\\nWe plan to make this the default, deprecate non-flattened mode,\\nand eventually retire it. See crbug.com/991325.","optional":true,"type":"boolean"}],"returns":[{"name":"sessionId","description":"Id assigned to the session.","$ref":"SessionID"}]},{"name":"attachToBrowserTarget","description":"Attaches to the browser target, only uses flat sessionId mode.","experimental":true,"returns":[{"name":"sessionId","description":"Id assigned to the session.","$ref":"SessionID"}]},{"name":"closeTarget","description":"Closes the target. If the target is a page that gets closed too.","parameters":[{"name":"targetId","$ref":"TargetID"}],"returns":[{"name":"success","description":"Always set to true. If an error occurs, the response indicates protocol error.","deprecated":true,"type":"boolean"}]},{"name":"exposeDevToolsProtocol","description":"Inject object to the target\'s main frame that provides a communication\\nchannel with browser target.\\n\\nInjected object will be available as `window[bindingName]`.\\n\\nThe object has the follwing API:\\n- `binding.send(json)` - a method to send messages over the remote debugging protocol\\n- `binding.onmessage = json => handleMessage(json)` - a callback that will be called for the protocol notifications and command responses.","experimental":true,"parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"bindingName","description":"Binding name, \'cdp\' if not specified.","optional":true,"type":"string"}]},{"name":"createBrowserContext","description":"Creates a new empty BrowserContext. Similar to an incognito profile but you can have more than\\none.","experimental":true,"parameters":[{"name":"disposeOnDetach","description":"If specified, disposes this context when debugging session disconnects.","optional":true,"type":"boolean"},{"name":"proxyServer","description":"Proxy server, similar to the one passed to --proxy-server","optional":true,"type":"string"},{"name":"proxyBypassList","description":"Proxy bypass list, similar to the one passed to --proxy-bypass-list","optional":true,"type":"string"},{"name":"originsWithUniversalNetworkAccess","description":"An optional list of origins to grant unlimited cross-origin access to.\\nParts of the URL other than those constituting origin are ignored.","optional":true,"type":"array","items":{"type":"string"}}],"returns":[{"name":"browserContextId","description":"The id of the context created.","$ref":"Browser.BrowserContextID"}]},{"name":"getBrowserContexts","description":"Returns all browser contexts created with `Target.createBrowserContext` method.","experimental":true,"returns":[{"name":"browserContextIds","description":"An array of browser context ids.","type":"array","items":{"$ref":"Browser.BrowserContextID"}}]},{"name":"createTarget","description":"Creates a new page.","parameters":[{"name":"url","description":"The initial URL the page will be navigated to. An empty string indicates about:blank.","type":"string"},{"name":"width","description":"Frame width in DIP (headless chrome only).","optional":true,"type":"integer"},{"name":"height","description":"Frame height in DIP (headless chrome only).","optional":true,"type":"integer"},{"name":"browserContextId","description":"The browser context to create the page in.","experimental":true,"optional":true,"$ref":"Browser.BrowserContextID"},{"name":"enableBeginFrameControl","description":"Whether BeginFrames for this target will be controlled via DevTools (headless chrome only,\\nnot supported on MacOS yet, false by default).","experimental":true,"optional":true,"type":"boolean"},{"name":"newWindow","description":"Whether to create a new Window or Tab (chrome-only, false by default).","optional":true,"type":"boolean"},{"name":"background","description":"Whether to create the target in background or foreground (chrome-only,\\nfalse by default).","optional":true,"type":"boolean"},{"name":"forTab","description":"Whether to create the target of type \\"tab\\".","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"targetId","description":"The id of the page opened.","$ref":"TargetID"}]},{"name":"detachFromTarget","description":"Detaches session with given id.","parameters":[{"name":"sessionId","description":"Session to detach.","optional":true,"$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"disposeBrowserContext","description":"Deletes a BrowserContext. All the belonging pages will be closed without calling their\\nbeforeunload hooks.","experimental":true,"parameters":[{"name":"browserContextId","$ref":"Browser.BrowserContextID"}]},{"name":"getTargetInfo","description":"Returns information about a target.","experimental":true,"parameters":[{"name":"targetId","optional":true,"$ref":"TargetID"}],"returns":[{"name":"targetInfo","$ref":"TargetInfo"}]},{"name":"getTargets","description":"Retrieves a list of available targets.","parameters":[{"name":"filter","description":"Only targets matching filter will be reported. If filter is not specified\\nand target discovery is currently enabled, a filter used for target discovery\\nis used for consistency.","experimental":true,"optional":true,"$ref":"TargetFilter"}],"returns":[{"name":"targetInfos","description":"The list of targets.","type":"array","items":{"$ref":"TargetInfo"}}]},{"name":"sendMessageToTarget","description":"Sends protocol message over session with given id.\\nConsider using flat mode instead; see commands attachToTarget, setAutoAttach,\\nand crbug.com/991325.","deprecated":true,"parameters":[{"name":"message","type":"string"},{"name":"sessionId","description":"Identifier of the session.","optional":true,"$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"setAutoAttach","description":"Controls whether to automatically attach to new targets which are considered to be related to\\nthis one. When turned on, attaches to all existing related targets as well. When turned off,\\nautomatically detaches from all currently attached targets.\\nThis also clears all targets added by `autoAttachRelated` from the list of targets to watch\\nfor creation of related targets.","experimental":true,"parameters":[{"name":"autoAttach","description":"Whether to auto-attach to related targets.","type":"boolean"},{"name":"waitForDebuggerOnStart","description":"Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\\nto run paused targets.","type":"boolean"},{"name":"flatten","description":"Enables \\"flat\\" access to the session via specifying sessionId attribute in the commands.\\nWe plan to make this the default, deprecate non-flattened mode,\\nand eventually retire it. See crbug.com/991325.","optional":true,"type":"boolean"},{"name":"filter","description":"Only targets matching filter will be attached.","experimental":true,"optional":true,"$ref":"TargetFilter"}]},{"name":"autoAttachRelated","description":"Adds the specified target to the list of targets that will be monitored for any related target\\ncreation (such as child frames, child workers and new versions of service worker) and reported\\nthrough `attachedToTarget`. The specified target is also auto-attached.\\nThis cancels the effect of any previous `setAutoAttach` and is also cancelled by subsequent\\n`setAutoAttach`. Only available at the Browser target.","experimental":true,"parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"waitForDebuggerOnStart","description":"Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\\nto run paused targets.","type":"boolean"},{"name":"filter","description":"Only targets matching filter will be attached.","experimental":true,"optional":true,"$ref":"TargetFilter"}]},{"name":"setDiscoverTargets","description":"Controls whether to discover available targets and notify via\\n`targetCreated/targetInfoChanged/targetDestroyed` events.","parameters":[{"name":"discover","description":"Whether to discover available targets.","type":"boolean"},{"name":"filter","description":"Only targets matching filter will be attached. If `discover` is false,\\n`filter` must be omitted or empty.","experimental":true,"optional":true,"$ref":"TargetFilter"}]},{"name":"setRemoteLocations","description":"Enables target discovery for the specified locations, when `setDiscoverTargets` was set to\\n`true`.","experimental":true,"parameters":[{"name":"locations","description":"List of remote locations.","type":"array","items":{"$ref":"RemoteLocation"}}]}],"events":[{"name":"attachedToTarget","description":"Issued when attached to target because of auto-attach or `attachToTarget` command.","experimental":true,"parameters":[{"name":"sessionId","description":"Identifier assigned to the session used to send/receive messages.","$ref":"SessionID"},{"name":"targetInfo","$ref":"TargetInfo"},{"name":"waitingForDebugger","type":"boolean"}]},{"name":"detachedFromTarget","description":"Issued when detached from target for any reason (including `detachFromTarget` command). Can be\\nissued multiple times per target if multiple sessions have been attached to it.","experimental":true,"parameters":[{"name":"sessionId","description":"Detached session identifier.","$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"receivedMessageFromTarget","description":"Notifies about a new protocol message received from the session (as reported in\\n`attachedToTarget` event).","parameters":[{"name":"sessionId","description":"Identifier of a session which sends a message.","$ref":"SessionID"},{"name":"message","type":"string"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"targetCreated","description":"Issued when a possible inspection target is created.","parameters":[{"name":"targetInfo","$ref":"TargetInfo"}]},{"name":"targetDestroyed","description":"Issued when a target is destroyed.","parameters":[{"name":"targetId","$ref":"TargetID"}]},{"name":"targetCrashed","description":"Issued when a target has crashed.","parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"status","description":"Termination status type.","type":"string"},{"name":"errorCode","description":"Termination error code.","type":"integer"}]},{"name":"targetInfoChanged","description":"Issued when some information about a target has changed. This only happens between\\n`targetCreated` and `targetDestroyed`.","parameters":[{"name":"targetInfo","$ref":"TargetInfo"}]}]},{"domain":"Tethering","description":"The Tethering domain defines methods and events for browser port binding.","experimental":true,"commands":[{"name":"bind","description":"Request browser port binding.","parameters":[{"name":"port","description":"Port number to bind.","type":"integer"}]},{"name":"unbind","description":"Request browser port unbinding.","parameters":[{"name":"port","description":"Port number to unbind.","type":"integer"}]}],"events":[{"name":"accepted","description":"Informs that port was successfully bound and got a specified connection id.","parameters":[{"name":"port","description":"Port number that was successfully bound.","type":"integer"},{"name":"connectionId","description":"Connection id to be used.","type":"string"}]}]},{"domain":"Tracing","experimental":true,"dependencies":["IO"],"types":[{"id":"MemoryDumpConfig","description":"Configuration for memory dump. Used only when \\"memory-infra\\" category is enabled.","type":"object"},{"id":"TraceConfig","type":"object","properties":[{"name":"recordMode","description":"Controls how the trace buffer stores data.","optional":true,"type":"string","enum":["recordUntilFull","recordContinuously","recordAsMuchAsPossible","echoToConsole"]},{"name":"traceBufferSizeInKb","description":"Size of the trace buffer in kilobytes. If not specified or zero is passed, a default value\\nof 200 MB would be used.","optional":true,"type":"number"},{"name":"enableSampling","description":"Turns on JavaScript stack sampling.","optional":true,"type":"boolean"},{"name":"enableSystrace","description":"Turns on system tracing.","optional":true,"type":"boolean"},{"name":"enableArgumentFilter","description":"Turns on argument filter.","optional":true,"type":"boolean"},{"name":"includedCategories","description":"Included category filters.","optional":true,"type":"array","items":{"type":"string"}},{"name":"excludedCategories","description":"Excluded category filters.","optional":true,"type":"array","items":{"type":"string"}},{"name":"syntheticDelays","description":"Configuration to synthesize the delays in tracing.","optional":true,"type":"array","items":{"type":"string"}},{"name":"memoryDumpConfig","description":"Configuration for memory dump triggers. Used only when \\"memory-infra\\" category is enabled.","optional":true,"$ref":"MemoryDumpConfig"}]},{"id":"StreamFormat","description":"Data format of a trace. Can be either the legacy JSON format or the\\nprotocol buffer format. Note that the JSON format will be deprecated soon.","type":"string","enum":["json","proto"]},{"id":"StreamCompression","description":"Compression type to use for traces returned via streams.","type":"string","enum":["none","gzip"]},{"id":"MemoryDumpLevelOfDetail","description":"Details exposed when memory request explicitly declared.\\nKeep consistent with memory_dump_request_args.h and\\nmemory_instrumentation.mojom","type":"string","enum":["background","light","detailed"]},{"id":"TracingBackend","description":"Backend type to use for tracing. `chrome` uses the Chrome-integrated\\ntracing service and is supported on all platforms. `system` is only\\nsupported on Chrome OS and uses the Perfetto system tracing service.\\n`auto` chooses `system` when the perfettoConfig provided to Tracing.start\\nspecifies at least one non-Chrome data source; otherwise uses `chrome`.","type":"string","enum":["auto","chrome","system"]}],"commands":[{"name":"end","description":"Stop trace events collection."},{"name":"getCategories","description":"Gets supported tracing categories.","returns":[{"name":"categories","description":"A list of supported tracing categories.","type":"array","items":{"type":"string"}}]},{"name":"recordClockSyncMarker","description":"Record a clock sync marker in the trace.","parameters":[{"name":"syncId","description":"The ID of this clock sync marker","type":"string"}]},{"name":"requestMemoryDump","description":"Request a global memory dump.","parameters":[{"name":"deterministic","description":"Enables more deterministic results by forcing garbage collection","optional":true,"type":"boolean"},{"name":"levelOfDetail","description":"Specifies level of details in memory dump. Defaults to \\"detailed\\".","optional":true,"$ref":"MemoryDumpLevelOfDetail"}],"returns":[{"name":"dumpGuid","description":"GUID of the resulting global memory dump.","type":"string"},{"name":"success","description":"True iff the global memory dump succeeded.","type":"boolean"}]},{"name":"start","description":"Start trace events collection.","parameters":[{"name":"categories","description":"Category/tag filter","deprecated":true,"optional":true,"type":"string"},{"name":"options","description":"Tracing options","deprecated":true,"optional":true,"type":"string"},{"name":"bufferUsageReportingInterval","description":"If set, the agent will issue bufferUsage events at this interval, specified in milliseconds","optional":true,"type":"number"},{"name":"transferMode","description":"Whether to report trace events as series of dataCollected events or to save trace to a\\nstream (defaults to `ReportEvents`).","optional":true,"type":"string","enum":["ReportEvents","ReturnAsStream"]},{"name":"streamFormat","description":"Trace data format to use. This only applies when using `ReturnAsStream`\\ntransfer mode (defaults to `json`).","optional":true,"$ref":"StreamFormat"},{"name":"streamCompression","description":"Compression format to use. This only applies when using `ReturnAsStream`\\ntransfer mode (defaults to `none`)","optional":true,"$ref":"StreamCompression"},{"name":"traceConfig","optional":true,"$ref":"TraceConfig"},{"name":"perfettoConfig","description":"Base64-encoded serialized perfetto.protos.TraceConfig protobuf message\\nWhen specified, the parameters `categories`, `options`, `traceConfig`\\nare ignored. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"tracingBackend","description":"Backend type (defaults to `auto`)","optional":true,"$ref":"TracingBackend"}]}],"events":[{"name":"bufferUsage","parameters":[{"name":"percentFull","description":"A number in range [0..1] that indicates the used size of event buffer as a fraction of its\\ntotal size.","optional":true,"type":"number"},{"name":"eventCount","description":"An approximate number of events in the trace log.","optional":true,"type":"number"},{"name":"value","description":"A number in range [0..1] that indicates the used size of event buffer as a fraction of its\\ntotal size.","optional":true,"type":"number"}]},{"name":"dataCollected","description":"Contains a bucket of collected trace events. When tracing is stopped collected events will be\\nsent as a sequence of dataCollected events followed by tracingComplete event.","parameters":[{"name":"value","type":"array","items":{"type":"object"}}]},{"name":"tracingComplete","description":"Signals that tracing is stopped and there is no trace buffers pending flush, all data were\\ndelivered via dataCollected events.","parameters":[{"name":"dataLossOccurred","description":"Indicates whether some trace data is known to have been lost, e.g. because the trace ring\\nbuffer wrapped around.","type":"boolean"},{"name":"stream","description":"A handle of the stream that holds resulting trace data.","optional":true,"$ref":"IO.StreamHandle"},{"name":"traceFormat","description":"Trace data format of returned stream.","optional":true,"$ref":"StreamFormat"},{"name":"streamCompression","description":"Compression format of returned stream.","optional":true,"$ref":"StreamCompression"}]}]},{"domain":"Fetch","description":"A domain for letting clients substitute browser\'s network layer with client code.","dependencies":["Network","IO","Page"],"types":[{"id":"RequestId","description":"Unique request identifier.","type":"string"},{"id":"RequestStage","description":"Stages of the request to handle. Request will intercept before the request is\\nsent. Response will intercept after the response is received (but before response\\nbody is received).","type":"string","enum":["Request","Response"]},{"id":"RequestPattern","type":"object","properties":[{"name":"urlPattern","description":"Wildcards (`\'*\'` -> zero or more, `\'?\'` -> exactly one) are allowed. Escape character is\\nbackslash. Omitting is equivalent to `\\"*\\"`.","optional":true,"type":"string"},{"name":"resourceType","description":"If set, only requests for matching resource types will be intercepted.","optional":true,"$ref":"Network.ResourceType"},{"name":"requestStage","description":"Stage at which to begin intercepting requests. Default is Request.","optional":true,"$ref":"RequestStage"}]},{"id":"HeaderEntry","description":"Response HTTP header entry","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"AuthChallenge","description":"Authorization challenge for HTTP status code 401 or 407.","type":"object","properties":[{"name":"source","description":"Source of the authentication challenge.","optional":true,"type":"string","enum":["Server","Proxy"]},{"name":"origin","description":"Origin of the challenger.","type":"string"},{"name":"scheme","description":"The authentication scheme used, such as basic or digest","type":"string"},{"name":"realm","description":"The realm of the challenge. May be empty.","type":"string"}]},{"id":"AuthChallengeResponse","description":"Response to an AuthChallenge.","type":"object","properties":[{"name":"response","description":"The decision on what to do in response to the authorization challenge. Default means\\ndeferring to the default behavior of the net stack, which will likely either the Cancel\\nauthentication or display a popup dialog box.","type":"string","enum":["Default","CancelAuth","ProvideCredentials"]},{"name":"username","description":"The username to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"},{"name":"password","description":"The password to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"}]}],"commands":[{"name":"disable","description":"Disables the fetch domain."},{"name":"enable","description":"Enables issuing of requestPaused events. A request will be paused until client\\ncalls one of failRequest, fulfillRequest or continueRequest/continueWithAuth.","parameters":[{"name":"patterns","description":"If specified, only requests matching any of these patterns will produce\\nfetchRequested event and will be paused until clients response. If not set,\\nall requests will be affected.","optional":true,"type":"array","items":{"$ref":"RequestPattern"}},{"name":"handleAuthRequests","description":"If true, authRequired events will be issued and requests will be paused\\nexpecting a call to continueWithAuth.","optional":true,"type":"boolean"}]},{"name":"failRequest","description":"Causes the request to fail with specified reason.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"errorReason","description":"Causes the request to fail with the given reason.","$ref":"Network.ErrorReason"}]},{"name":"fulfillRequest","description":"Provides response to the request.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"responseCode","description":"An HTTP response code.","type":"integer"},{"name":"responseHeaders","description":"Response headers.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}},{"name":"binaryResponseHeaders","description":"Alternative way of specifying response headers as a \\\\0-separated\\nseries of name: value pairs. Prefer the above method unless you\\nneed to represent some non-UTF8 values that can\'t be transmitted\\nover the protocol as text. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"body","description":"A response body. If absent, original response body will be used if\\nthe request is intercepted at the response stage and empty body\\nwill be used if the request is intercepted at the request stage. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"responsePhrase","description":"A textual representation of responseCode.\\nIf absent, a standard phrase matching responseCode is used.","optional":true,"type":"string"}]},{"name":"continueRequest","description":"Continues the request, optionally modifying some of its parameters.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"url","description":"If set, the request url will be modified in a way that\'s not observable by page.","optional":true,"type":"string"},{"name":"method","description":"If set, the request method is overridden.","optional":true,"type":"string"},{"name":"postData","description":"If set, overrides the post data in the request. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"headers","description":"If set, overrides the request headers. Note that the overrides do not\\nextend to subsequent redirect hops, if a redirect happens. Another override\\nmay be applied to a different request produced by a redirect.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}},{"name":"interceptResponse","description":"If set, overrides response interception behavior for this request.","experimental":true,"optional":true,"type":"boolean"}]},{"name":"continueWithAuth","description":"Continues a request supplying authChallengeResponse following authRequired event.","parameters":[{"name":"requestId","description":"An id the client received in authRequired event.","$ref":"RequestId"},{"name":"authChallengeResponse","description":"Response to with an authChallenge.","$ref":"AuthChallengeResponse"}]},{"name":"continueResponse","description":"Continues loading of the paused response, optionally modifying the\\nresponse headers. If either responseCode or headers are modified, all of them\\nmust be present.","experimental":true,"parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"responseCode","description":"An HTTP response code. If absent, original response code will be used.","optional":true,"type":"integer"},{"name":"responsePhrase","description":"A textual representation of responseCode.\\nIf absent, a standard phrase matching responseCode is used.","optional":true,"type":"string"},{"name":"responseHeaders","description":"Response headers. If absent, original response headers will be used.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}},{"name":"binaryResponseHeaders","description":"Alternative way of specifying response headers as a \\\\0-separated\\nseries of name: value pairs. Prefer the above method unless you\\nneed to represent some non-UTF8 values that can\'t be transmitted\\nover the protocol as text. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"}]},{"name":"getResponseBody","description":"Causes the body of the response to be received from the server and\\nreturned as a single string. May only be issued for a request that\\nis paused in the Response stage and is mutually exclusive with\\ntakeResponseBodyForInterceptionAsStream. Calling other methods that\\naffect the request or disabling fetch domain before body is received\\nresults in an undefined behavior.\\nNote that the response body is not available for redirects. Requests\\npaused in the _redirect received_ state may be differentiated by\\n`responseCode` and presence of `location` response header, see\\ncomments to `requestPaused` for details.","parameters":[{"name":"requestId","description":"Identifier for the intercepted request to get body for.","$ref":"RequestId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"takeResponseBodyAsStream","description":"Returns a handle to the stream representing the response body.\\nThe request must be paused in the HeadersReceived stage.\\nNote that after this command the request can\'t be continued\\nas is -- client either needs to cancel it or to provide the\\nresponse body.\\nThe stream only supports sequential read, IO.read will fail if the position\\nis specified.\\nThis method is mutually exclusive with getResponseBody.\\nCalling other methods that affect the request or disabling fetch\\ndomain before body is received results in an undefined behavior.","parameters":[{"name":"requestId","$ref":"RequestId"}],"returns":[{"name":"stream","$ref":"IO.StreamHandle"}]}],"events":[{"name":"requestPaused","description":"Issued when the domain is enabled and the request URL matches the\\nspecified filter. The request is paused until the client responds\\nwith one of continueRequest, failRequest or fulfillRequest.\\nThe stage of the request can be determined by presence of responseErrorReason\\nand responseStatusCode -- the request is at the response stage if either\\nof these fields is present and in the request stage otherwise.\\nRedirect responses and subsequent requests are reported similarly to regular\\nresponses and requests. Redirect responses may be distinguished by the value\\nof `responseStatusCode` (which is one of 301, 302, 303, 307, 308) along with\\npresence of the `location` header. Requests resulting from a redirect will\\nhave `redirectedRequestId` field set.","parameters":[{"name":"requestId","description":"Each request the page makes will have a unique id.","$ref":"RequestId"},{"name":"request","description":"The details of the request.","$ref":"Network.Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"Network.ResourceType"},{"name":"responseErrorReason","description":"Response error if intercepted at response stage.","optional":true,"$ref":"Network.ErrorReason"},{"name":"responseStatusCode","description":"Response code if intercepted at response stage.","optional":true,"type":"integer"},{"name":"responseStatusText","description":"Response status text if intercepted at response stage.","optional":true,"type":"string"},{"name":"responseHeaders","description":"Response headers if intercepted at the response stage.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}},{"name":"networkId","description":"If the intercepted request had a corresponding Network.requestWillBeSent event fired for it,\\nthen this networkId will be the same as the requestId present in the requestWillBeSent event.","optional":true,"$ref":"Network.RequestId"},{"name":"redirectedRequestId","description":"If the request is due to a redirect response from the server, the id of the request that\\nhas caused the redirect.","experimental":true,"optional":true,"$ref":"RequestId"}]},{"name":"authRequired","description":"Issued when the domain is enabled with handleAuthRequests set to true.\\nThe request is paused until client responds with continueWithAuth.","parameters":[{"name":"requestId","description":"Each request the page makes will have a unique id.","$ref":"RequestId"},{"name":"request","description":"The details of the request.","$ref":"Network.Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"Network.ResourceType"},{"name":"authChallenge","description":"Details of the Authorization Challenge encountered.\\nIf this is set, client should respond with continueRequest that\\ncontains AuthChallengeResponse.","$ref":"AuthChallenge"}]}]},{"domain":"WebAudio","description":"This domain allows inspection of Web Audio API.\\nhttps://webaudio.github.io/web-audio-api/","experimental":true,"types":[{"id":"GraphObjectId","description":"An unique ID for a graph object (AudioContext, AudioNode, AudioParam) in Web Audio API","type":"string"},{"id":"ContextType","description":"Enum of BaseAudioContext types","type":"string","enum":["realtime","offline"]},{"id":"ContextState","description":"Enum of AudioContextState from the spec","type":"string","enum":["suspended","running","closed"]},{"id":"NodeType","description":"Enum of AudioNode types","type":"string"},{"id":"ChannelCountMode","description":"Enum of AudioNode::ChannelCountMode from the spec","type":"string","enum":["clamped-max","explicit","max"]},{"id":"ChannelInterpretation","description":"Enum of AudioNode::ChannelInterpretation from the spec","type":"string","enum":["discrete","speakers"]},{"id":"ParamType","description":"Enum of AudioParam types","type":"string"},{"id":"AutomationRate","description":"Enum of AudioParam::AutomationRate from the spec","type":"string","enum":["a-rate","k-rate"]},{"id":"ContextRealtimeData","description":"Fields in AudioContext that change in real-time.","type":"object","properties":[{"name":"currentTime","description":"The current context time in second in BaseAudioContext.","type":"number"},{"name":"renderCapacity","description":"The time spent on rendering graph divided by render quantum duration,\\nand multiplied by 100. 100 means the audio renderer reached the full\\ncapacity and glitch may occur.","type":"number"},{"name":"callbackIntervalMean","description":"A running mean of callback interval.","type":"number"},{"name":"callbackIntervalVariance","description":"A running variance of callback interval.","type":"number"}]},{"id":"BaseAudioContext","description":"Protocol object for BaseAudioContext","type":"object","properties":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"contextType","$ref":"ContextType"},{"name":"contextState","$ref":"ContextState"},{"name":"realtimeData","optional":true,"$ref":"ContextRealtimeData"},{"name":"callbackBufferSize","description":"Platform-dependent callback buffer size.","type":"number"},{"name":"maxOutputChannelCount","description":"Number of output channels supported by audio hardware in use.","type":"number"},{"name":"sampleRate","description":"Context sample rate.","type":"number"}]},{"id":"AudioListener","description":"Protocol object for AudioListener","type":"object","properties":[{"name":"listenerId","$ref":"GraphObjectId"},{"name":"contextId","$ref":"GraphObjectId"}]},{"id":"AudioNode","description":"Protocol object for AudioNode","type":"object","properties":[{"name":"nodeId","$ref":"GraphObjectId"},{"name":"contextId","$ref":"GraphObjectId"},{"name":"nodeType","$ref":"NodeType"},{"name":"numberOfInputs","type":"number"},{"name":"numberOfOutputs","type":"number"},{"name":"channelCount","type":"number"},{"name":"channelCountMode","$ref":"ChannelCountMode"},{"name":"channelInterpretation","$ref":"ChannelInterpretation"}]},{"id":"AudioParam","description":"Protocol object for AudioParam","type":"object","properties":[{"name":"paramId","$ref":"GraphObjectId"},{"name":"nodeId","$ref":"GraphObjectId"},{"name":"contextId","$ref":"GraphObjectId"},{"name":"paramType","$ref":"ParamType"},{"name":"rate","$ref":"AutomationRate"},{"name":"defaultValue","type":"number"},{"name":"minValue","type":"number"},{"name":"maxValue","type":"number"}]}],"commands":[{"name":"enable","description":"Enables the WebAudio domain and starts sending context lifetime events."},{"name":"disable","description":"Disables the WebAudio domain."},{"name":"getRealtimeData","description":"Fetch the realtime data from the registered contexts.","parameters":[{"name":"contextId","$ref":"GraphObjectId"}],"returns":[{"name":"realtimeData","$ref":"ContextRealtimeData"}]}],"events":[{"name":"contextCreated","description":"Notifies that a new BaseAudioContext has been created.","parameters":[{"name":"context","$ref":"BaseAudioContext"}]},{"name":"contextWillBeDestroyed","description":"Notifies that an existing BaseAudioContext will be destroyed.","parameters":[{"name":"contextId","$ref":"GraphObjectId"}]},{"name":"contextChanged","description":"Notifies that existing BaseAudioContext has changed some properties (id stays the same)..","parameters":[{"name":"context","$ref":"BaseAudioContext"}]},{"name":"audioListenerCreated","description":"Notifies that the construction of an AudioListener has finished.","parameters":[{"name":"listener","$ref":"AudioListener"}]},{"name":"audioListenerWillBeDestroyed","description":"Notifies that a new AudioListener has been created.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"listenerId","$ref":"GraphObjectId"}]},{"name":"audioNodeCreated","description":"Notifies that a new AudioNode has been created.","parameters":[{"name":"node","$ref":"AudioNode"}]},{"name":"audioNodeWillBeDestroyed","description":"Notifies that an existing AudioNode has been destroyed.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"nodeId","$ref":"GraphObjectId"}]},{"name":"audioParamCreated","description":"Notifies that a new AudioParam has been created.","parameters":[{"name":"param","$ref":"AudioParam"}]},{"name":"audioParamWillBeDestroyed","description":"Notifies that an existing AudioParam has been destroyed.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"nodeId","$ref":"GraphObjectId"},{"name":"paramId","$ref":"GraphObjectId"}]},{"name":"nodesConnected","description":"Notifies that two AudioNodes are connected.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"sourceId","$ref":"GraphObjectId"},{"name":"destinationId","$ref":"GraphObjectId"},{"name":"sourceOutputIndex","optional":true,"type":"number"},{"name":"destinationInputIndex","optional":true,"type":"number"}]},{"name":"nodesDisconnected","description":"Notifies that AudioNodes are disconnected. The destination can be null, and it means all the outgoing connections from the source are disconnected.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"sourceId","$ref":"GraphObjectId"},{"name":"destinationId","$ref":"GraphObjectId"},{"name":"sourceOutputIndex","optional":true,"type":"number"},{"name":"destinationInputIndex","optional":true,"type":"number"}]},{"name":"nodeParamConnected","description":"Notifies that an AudioNode is connected to an AudioParam.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"sourceId","$ref":"GraphObjectId"},{"name":"destinationId","$ref":"GraphObjectId"},{"name":"sourceOutputIndex","optional":true,"type":"number"}]},{"name":"nodeParamDisconnected","description":"Notifies that an AudioNode is disconnected to an AudioParam.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"sourceId","$ref":"GraphObjectId"},{"name":"destinationId","$ref":"GraphObjectId"},{"name":"sourceOutputIndex","optional":true,"type":"number"}]}]},{"domain":"WebAuthn","description":"This domain allows configuring virtual authenticators to test the WebAuthn\\nAPI.","experimental":true,"types":[{"id":"AuthenticatorId","type":"string"},{"id":"AuthenticatorProtocol","type":"string","enum":["u2f","ctap2"]},{"id":"Ctap2Version","type":"string","enum":["ctap2_0","ctap2_1"]},{"id":"AuthenticatorTransport","type":"string","enum":["usb","nfc","ble","cable","internal"]},{"id":"VirtualAuthenticatorOptions","type":"object","properties":[{"name":"protocol","$ref":"AuthenticatorProtocol"},{"name":"ctap2Version","description":"Defaults to ctap2_0. Ignored if |protocol| == u2f.","optional":true,"$ref":"Ctap2Version"},{"name":"transport","$ref":"AuthenticatorTransport"},{"name":"hasResidentKey","description":"Defaults to false.","optional":true,"type":"boolean"},{"name":"hasUserVerification","description":"Defaults to false.","optional":true,"type":"boolean"},{"name":"hasLargeBlob","description":"If set to true, the authenticator will support the largeBlob extension.\\nhttps://w3c.github.io/webauthn#largeBlob\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"hasCredBlob","description":"If set to true, the authenticator will support the credBlob extension.\\nhttps://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-credBlob-extension\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"hasMinPinLength","description":"If set to true, the authenticator will support the minPinLength extension.\\nhttps://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-minpinlength-extension\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"hasPrf","description":"If set to true, the authenticator will support the prf extension.\\nhttps://w3c.github.io/webauthn/#prf-extension\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"automaticPresenceSimulation","description":"If set to true, tests of user presence will succeed immediately.\\nOtherwise, they will not be resolved. Defaults to true.","optional":true,"type":"boolean"},{"name":"isUserVerified","description":"Sets whether User Verification succeeds or fails for an authenticator.\\nDefaults to false.","optional":true,"type":"boolean"}]},{"id":"Credential","type":"object","properties":[{"name":"credentialId","type":"string"},{"name":"isResidentCredential","type":"boolean"},{"name":"rpId","description":"Relying Party ID the credential is scoped to. Must be set when adding a\\ncredential.","optional":true,"type":"string"},{"name":"privateKey","description":"The ECDSA P-256 private key in PKCS#8 format. (Encoded as a base64 string when passed over JSON)","type":"string"},{"name":"userHandle","description":"An opaque byte sequence with a maximum size of 64 bytes mapping the\\ncredential to a specific user. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"signCount","description":"Signature counter. This is incremented by one for each successful\\nassertion.\\nSee https://w3c.github.io/webauthn/#signature-counter","type":"integer"},{"name":"largeBlob","description":"The large blob associated with the credential.\\nSee https://w3c.github.io/webauthn/#sctn-large-blob-extension (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"}]}],"commands":[{"name":"enable","description":"Enable the WebAuthn domain and start intercepting credential storage and\\nretrieval with a virtual authenticator.","parameters":[{"name":"enableUI","description":"Whether to enable the WebAuthn user interface. Enabling the UI is\\nrecommended for debugging and demo purposes, as it is closer to the real\\nexperience. Disabling the UI is recommended for automated testing.\\nSupported at the embedder\'s discretion if UI is available.\\nDefaults to false.","optional":true,"type":"boolean"}]},{"name":"disable","description":"Disable the WebAuthn domain."},{"name":"addVirtualAuthenticator","description":"Creates and adds a virtual authenticator.","parameters":[{"name":"options","$ref":"VirtualAuthenticatorOptions"}],"returns":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"setResponseOverrideBits","description":"Resets parameters isBogusSignature, isBadUV, isBadUP to false if they are not present.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"isBogusSignature","description":"If isBogusSignature is set, overrides the signature in the authenticator response to be zero.\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"isBadUV","description":"If isBadUV is set, overrides the UV bit in the flags in the authenticator response to\\nbe zero. Defaults to false.","optional":true,"type":"boolean"},{"name":"isBadUP","description":"If isBadUP is set, overrides the UP bit in the flags in the authenticator response to\\nbe zero. Defaults to false.","optional":true,"type":"boolean"}]},{"name":"removeVirtualAuthenticator","description":"Removes the given authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"addCredential","description":"Adds the credential to the specified authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credential","$ref":"Credential"}]},{"name":"getCredential","description":"Returns a single credential stored in the given virtual authenticator that\\nmatches the credential ID.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credentialId","type":"string"}],"returns":[{"name":"credential","$ref":"Credential"}]},{"name":"getCredentials","description":"Returns all the credentials stored in the given virtual authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}],"returns":[{"name":"credentials","type":"array","items":{"$ref":"Credential"}}]},{"name":"removeCredential","description":"Removes a credential from the authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credentialId","type":"string"}]},{"name":"clearCredentials","description":"Clears all the credentials from the specified device.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"setUserVerified","description":"Sets whether User Verification succeeds or fails for an authenticator.\\nThe default is true.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"isUserVerified","type":"boolean"}]},{"name":"setAutomaticPresenceSimulation","description":"Sets whether tests of user presence will succeed immediately (if true) or fail to resolve (if false) for an authenticator.\\nThe default is true.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"enabled","type":"boolean"}]}],"events":[{"name":"credentialAdded","description":"Triggered when a credential is added to an authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credential","$ref":"Credential"}]},{"name":"credentialAsserted","description":"Triggered when a credential is used in a webauthn assertion.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credential","$ref":"Credential"}]}]},{"domain":"Media","description":"This domain allows detailed inspection of media elements","experimental":true,"types":[{"id":"PlayerId","description":"Players will get an ID that is unique within the agent context.","type":"string"},{"id":"Timestamp","type":"number"},{"id":"PlayerMessage","description":"Have one type per entry in MediaLogRecord::Type\\nCorresponds to kMessage","type":"object","properties":[{"name":"level","description":"Keep in sync with MediaLogMessageLevel\\nWe are currently keeping the message level \'error\' separate from the\\nPlayerError type because right now they represent different things,\\nthis one being a DVLOG(ERROR) style log message that gets printed\\nbased on what log level is selected in the UI, and the other is a\\nrepresentation of a media::PipelineStatus object. Soon however we\'re\\ngoing to be moving away from using PipelineStatus for errors and\\nintroducing a new error type which should hopefully let us integrate\\nthe error log level into the PlayerError type.","type":"string","enum":["error","warning","info","debug"]},{"name":"message","type":"string"}]},{"id":"PlayerProperty","description":"Corresponds to kMediaPropertyChange","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"PlayerEvent","description":"Corresponds to kMediaEventTriggered","type":"object","properties":[{"name":"timestamp","$ref":"Timestamp"},{"name":"value","type":"string"}]},{"id":"PlayerErrorSourceLocation","description":"Represents logged source line numbers reported in an error.\\nNOTE: file and line are from chromium c++ implementation code, not js.","type":"object","properties":[{"name":"file","type":"string"},{"name":"line","type":"integer"}]},{"id":"PlayerError","description":"Corresponds to kMediaError","type":"object","properties":[{"name":"errorType","type":"string"},{"name":"code","description":"Code is the numeric enum entry for a specific set of error codes, such\\nas PipelineStatusCodes in media/base/pipeline_status.h","type":"integer"},{"name":"stack","description":"A trace of where this error was caused / where it passed through.","type":"array","items":{"$ref":"PlayerErrorSourceLocation"}},{"name":"cause","description":"Errors potentially have a root cause error, ie, a DecoderError might be\\ncaused by an WindowsError","type":"array","items":{"$ref":"PlayerError"}},{"name":"data","description":"Extra data attached to an error, such as an HRESULT, Video Codec, etc.","type":"object"}]}],"events":[{"name":"playerPropertiesChanged","description":"This can be called multiple times, and can be used to set / override /\\nremove player properties. A null propValue indicates removal.","parameters":[{"name":"playerId","$ref":"PlayerId"},{"name":"properties","type":"array","items":{"$ref":"PlayerProperty"}}]},{"name":"playerEventsAdded","description":"Send events as a list, allowing them to be batched on the browser for less\\ncongestion. If batched, events must ALWAYS be in chronological order.","parameters":[{"name":"playerId","$ref":"PlayerId"},{"name":"events","type":"array","items":{"$ref":"PlayerEvent"}}]},{"name":"playerMessagesLogged","description":"Send a list of any messages that need to be delivered.","parameters":[{"name":"playerId","$ref":"PlayerId"},{"name":"messages","type":"array","items":{"$ref":"PlayerMessage"}}]},{"name":"playerErrorsRaised","description":"Send a list of any errors that need to be delivered.","parameters":[{"name":"playerId","$ref":"PlayerId"},{"name":"errors","type":"array","items":{"$ref":"PlayerError"}}]},{"name":"playersCreated","description":"Called whenever a player is created, or when a new agent joins and receives\\na list of active players. If an agent is restored, it will receive the full\\nlist of player ids and all events again.","parameters":[{"name":"players","type":"array","items":{"$ref":"PlayerId"}}]}],"commands":[{"name":"enable","description":"Enables the Media domain"},{"name":"disable","description":"Disables the Media domain."}]},{"domain":"DeviceAccess","experimental":true,"types":[{"id":"RequestId","description":"Device request id.","type":"string"},{"id":"DeviceId","description":"A device id.","type":"string"},{"id":"PromptDevice","description":"Device information displayed in a user prompt to select a device.","type":"object","properties":[{"name":"id","$ref":"DeviceId"},{"name":"name","description":"Display name as it appears in a device request user prompt.","type":"string"}]}],"commands":[{"name":"enable","description":"Enable events in this domain."},{"name":"disable","description":"Disable events in this domain."},{"name":"selectPrompt","description":"Select a device in response to a DeviceAccess.deviceRequestPrompted event.","parameters":[{"name":"id","$ref":"RequestId"},{"name":"deviceId","$ref":"DeviceId"}]},{"name":"cancelPrompt","description":"Cancel a prompt in response to a DeviceAccess.deviceRequestPrompted event.","parameters":[{"name":"id","$ref":"RequestId"}]}],"events":[{"name":"deviceRequestPrompted","description":"A device request opened a user prompt to select a device. Respond with the\\nselectPrompt or cancelPrompt command.","parameters":[{"name":"id","$ref":"RequestId"},{"name":"devices","type":"array","items":{"$ref":"PromptDevice"}}]}]},{"domain":"Preload","experimental":true,"types":[{"id":"RuleSetId","description":"Unique id","type":"string"},{"id":"RuleSet","description":"Corresponds to SpeculationRuleSet","type":"object","properties":[{"name":"id","$ref":"RuleSetId"},{"name":"loaderId","description":"Identifies a document which the rule set is associated with.","$ref":"Network.LoaderId"},{"name":"sourceText","description":"Source text of JSON representing the rule set. If it comes from\\n`<script>` tag, it is the textContent of the node. Note that it is\\na JSON for valid case.\\n\\nSee also:\\n- https://wicg.github.io/nav-speculation/speculation-rules.html\\n- https://github.com/WICG/nav-speculation/blob/main/triggers.md","type":"string"},{"name":"backendNodeId","description":"A speculation rule set is either added through an inline\\n`<script>` tag or through an external resource via the\\n\'Speculation-Rules\' HTTP header. For the first case, we include\\nthe BackendNodeId of the relevant `<script>` tag. For the second\\ncase, we include the external URL where the rule set was loaded\\nfrom, and also RequestId if Network domain is enabled.\\n\\nSee also:\\n- https://wicg.github.io/nav-speculation/speculation-rules.html#speculation-rules-script\\n- https://wicg.github.io/nav-speculation/speculation-rules.html#speculation-rules-header","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"url","optional":true,"type":"string"},{"name":"requestId","optional":true,"$ref":"Network.RequestId"},{"name":"errorType","description":"Error information\\n`errorMessage` is null iff `errorType` is null.","optional":true,"$ref":"RuleSetErrorType"},{"name":"errorMessage","description":"TODO(https://crbug.com/1425354): Replace this property with structured error.","deprecated":true,"optional":true,"type":"string"}]},{"id":"RuleSetErrorType","type":"string","enum":["SourceIsNotJsonObject","InvalidRulesSkipped"]},{"id":"SpeculationAction","description":"The type of preloading attempted. It corresponds to\\nmojom::SpeculationAction (although PrefetchWithSubresources is omitted as it\\nisn\'t being used by clients).","type":"string","enum":["Prefetch","Prerender"]},{"id":"SpeculationTargetHint","description":"Corresponds to mojom::SpeculationTargetHint.\\nSee https://github.com/WICG/nav-speculation/blob/main/triggers.md#window-name-targeting-hints","type":"string","enum":["Blank","Self"]},{"id":"PreloadingAttemptKey","description":"A key that identifies a preloading attempt.\\n\\nThe url used is the url specified by the trigger (i.e. the initial URL), and\\nnot the final url that is navigated to. For example, prerendering allows\\nsame-origin main frame navigations during the attempt, but the attempt is\\nstill keyed with the initial URL.","type":"object","properties":[{"name":"loaderId","$ref":"Network.LoaderId"},{"name":"action","$ref":"SpeculationAction"},{"name":"url","type":"string"},{"name":"targetHint","optional":true,"$ref":"SpeculationTargetHint"}]},{"id":"PreloadingAttemptSource","description":"Lists sources for a preloading attempt, specifically the ids of rule sets\\nthat had a speculation rule that triggered the attempt, and the\\nBackendNodeIds of <a href> or <area href> elements that triggered the\\nattempt (in the case of attempts triggered by a document rule). It is\\npossible for mulitple rule sets and links to trigger a single attempt.","type":"object","properties":[{"name":"key","$ref":"PreloadingAttemptKey"},{"name":"ruleSetIds","type":"array","items":{"$ref":"RuleSetId"}},{"name":"nodeIds","type":"array","items":{"$ref":"DOM.BackendNodeId"}}]},{"id":"PrerenderFinalStatus","description":"List of FinalStatus reasons for Prerender2.","type":"string","enum":["Activated","Destroyed","LowEndDevice","InvalidSchemeRedirect","InvalidSchemeNavigation","InProgressNavigation","NavigationRequestBlockedByCsp","MainFrameNavigation","MojoBinderPolicy","RendererProcessCrashed","RendererProcessKilled","Download","TriggerDestroyed","NavigationNotCommitted","NavigationBadHttpStatus","ClientCertRequested","NavigationRequestNetworkError","MaxNumOfRunningPrerendersExceeded","CancelAllHostsForTesting","DidFailLoad","Stop","SslCertificateError","LoginAuthRequested","UaChangeRequiresReload","BlockedByClient","AudioOutputDeviceRequested","MixedContent","TriggerBackgrounded","MemoryLimitExceeded","FailToGetMemoryUsage","DataSaverEnabled","HasEffectiveUrl","ActivatedBeforeStarted","InactivePageRestriction","StartFailed","TimeoutBackgrounded","CrossSiteRedirectInInitialNavigation","CrossSiteNavigationInInitialNavigation","SameSiteCrossOriginRedirectNotOptInInInitialNavigation","SameSiteCrossOriginNavigationNotOptInInInitialNavigation","ActivationNavigationParameterMismatch","ActivatedInBackground","EmbedderHostDisallowed","ActivationNavigationDestroyedBeforeSuccess","TabClosedByUserGesture","TabClosedWithoutUserGesture","PrimaryMainFrameRendererProcessCrashed","PrimaryMainFrameRendererProcessKilled","ActivationFramePolicyNotCompatible","PreloadingDisabled","BatterySaverEnabled","ActivatedDuringMainFrameNavigation","PreloadingUnsupportedByWebContents","CrossSiteRedirectInMainFrameNavigation","CrossSiteNavigationInMainFrameNavigation","SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation","SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation","MemoryPressureOnTrigger","MemoryPressureAfterTriggered","PrerenderingDisabledByDevTools","ResourceLoadBlockedByClient","SpeculationRuleRemoved","ActivatedWithAuxiliaryBrowsingContexts"]},{"id":"PreloadingStatus","description":"Preloading status values, see also PreloadingTriggeringOutcome. This\\nstatus is shared by prefetchStatusUpdated and prerenderStatusUpdated.","type":"string","enum":["Pending","Running","Ready","Success","Failure","NotSupported"]},{"id":"PrefetchStatus","description":"TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and\\nfilter out the ones that aren\'t necessary to the developers.","type":"string","enum":["PrefetchAllowed","PrefetchFailedIneligibleRedirect","PrefetchFailedInvalidRedirect","PrefetchFailedMIMENotSupported","PrefetchFailedNetError","PrefetchFailedNon2XX","PrefetchFailedPerPageLimitExceeded","PrefetchEvicted","PrefetchHeldback","PrefetchIneligibleRetryAfter","PrefetchIsPrivacyDecoy","PrefetchIsStale","PrefetchNotEligibleBrowserContextOffTheRecord","PrefetchNotEligibleDataSaverEnabled","PrefetchNotEligibleExistingProxy","PrefetchNotEligibleHostIsNonUnique","PrefetchNotEligibleNonDefaultStoragePartition","PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy","PrefetchNotEligibleSchemeIsNotHttps","PrefetchNotEligibleUserHasCookies","PrefetchNotEligibleUserHasServiceWorker","PrefetchNotEligibleBatterySaverEnabled","PrefetchNotEligiblePreloadingDisabled","PrefetchNotFinishedInTime","PrefetchNotStarted","PrefetchNotUsedCookiesChanged","PrefetchProxyNotAvailable","PrefetchResponseUsed","PrefetchSuccessfulButNotUsed","PrefetchNotUsedProbeFailed"]}],"commands":[{"name":"enable"},{"name":"disable"}],"events":[{"name":"ruleSetUpdated","description":"Upsert. Currently, it is only emitted when a rule set added.","parameters":[{"name":"ruleSet","$ref":"RuleSet"}]},{"name":"ruleSetRemoved","parameters":[{"name":"id","$ref":"RuleSetId"}]},{"name":"prerenderAttemptCompleted","description":"Fired when a prerender attempt is completed.","parameters":[{"name":"key","$ref":"PreloadingAttemptKey"},{"name":"initiatingFrameId","description":"The frame id of the frame initiating prerendering.","$ref":"Page.FrameId"},{"name":"prerenderingUrl","type":"string"},{"name":"finalStatus","$ref":"PrerenderFinalStatus"},{"name":"disallowedApiMethod","description":"This is used to give users more information about the name of the API call\\nthat is incompatible with prerender and has caused the cancellation of the attempt","optional":true,"type":"string"}]},{"name":"preloadEnabledStateUpdated","description":"Fired when a preload enabled state is updated.","parameters":[{"name":"disabledByPreference","type":"boolean"},{"name":"disabledByDataSaver","type":"boolean"},{"name":"disabledByBatterySaver","type":"boolean"},{"name":"disabledByHoldbackPrefetchSpeculationRules","type":"boolean"},{"name":"disabledByHoldbackPrerenderSpeculationRules","type":"boolean"}]},{"name":"prefetchStatusUpdated","description":"Fired when a prefetch attempt is updated.","parameters":[{"name":"key","$ref":"PreloadingAttemptKey"},{"name":"initiatingFrameId","description":"The frame id of the frame initiating prefetch.","$ref":"Page.FrameId"},{"name":"prefetchUrl","type":"string"},{"name":"status","$ref":"PreloadingStatus"},{"name":"prefetchStatus","$ref":"PrefetchStatus"},{"name":"requestId","$ref":"Network.RequestId"}]},{"name":"prerenderStatusUpdated","description":"Fired when a prerender attempt is updated.","parameters":[{"name":"key","$ref":"PreloadingAttemptKey"},{"name":"status","$ref":"PreloadingStatus"},{"name":"prerenderStatus","optional":true,"$ref":"PrerenderFinalStatus"},{"name":"disallowedMojoInterface","description":"This is used to give users more information about the name of Mojo interface\\nthat is incompatible with prerender and has caused the cancellation of the attempt.","optional":true,"type":"string"}]},{"name":"preloadingAttemptSourcesUpdated","description":"Send a list of sources for all preloading attempts in a document.","parameters":[{"name":"loaderId","$ref":"Network.LoaderId"},{"name":"preloadingAttemptSources","type":"array","items":{"$ref":"PreloadingAttemptSource"}}]}]},{"domain":"FedCm","description":"This domain allows interacting with the FedCM dialog.","experimental":true,"types":[{"id":"LoginState","description":"Whether this is a sign-up or sign-in action for this account, i.e.\\nwhether this account has ever been used to sign in to this RP before.","type":"string","enum":["SignIn","SignUp"]},{"id":"DialogType","description":"Whether the dialog shown is an account chooser or an auto re-authentication dialog.","type":"string","enum":["AccountChooser","AutoReauthn"]},{"id":"Account","description":"Corresponds to IdentityRequestAccount","type":"object","properties":[{"name":"accountId","type":"string"},{"name":"email","type":"string"},{"name":"name","type":"string"},{"name":"givenName","type":"string"},{"name":"pictureUrl","type":"string"},{"name":"idpConfigUrl","type":"string"},{"name":"idpSigninUrl","type":"string"},{"name":"loginState","$ref":"LoginState"},{"name":"termsOfServiceUrl","description":"These two are only set if the loginState is signUp","optional":true,"type":"string"},{"name":"privacyPolicyUrl","optional":true,"type":"string"}]}],"events":[{"name":"dialogShown","parameters":[{"name":"dialogId","type":"string"},{"name":"dialogType","$ref":"DialogType"},{"name":"accounts","type":"array","items":{"$ref":"Account"}},{"name":"title","description":"These exist primarily so that the caller can verify the\\nRP context was used appropriately.","type":"string"},{"name":"subtitle","optional":true,"type":"string"}]}],"commands":[{"name":"enable","parameters":[{"name":"disableRejectionDelay","description":"Allows callers to disable the promise rejection delay that would\\nnormally happen, if this is unimportant to what\'s being tested.\\n(step 4 of https://fedidcg.github.io/FedCM/#browser-api-rp-sign-in)","optional":true,"type":"boolean"}]},{"name":"disable"},{"name":"selectAccount","parameters":[{"name":"dialogId","type":"string"},{"name":"accountIndex","type":"integer"}]},{"name":"dismissDialog","parameters":[{"name":"dialogId","type":"string"},{"name":"triggerCooldown","optional":true,"type":"boolean"}]},{"name":"resetCooldown","description":"Resets the cooldown time, if any, to allow the next FedCM call to show\\na dialog even if one was recently dismissed by the user."}]},{"domain":"Console","description":"This domain is deprecated - use Runtime or Log instead.","deprecated":true,"dependencies":["Runtime"],"types":[{"id":"ConsoleMessage","description":"Console message.","type":"object","properties":[{"name":"source","description":"Message source.","type":"string","enum":["xml","javascript","network","console-api","storage","appcache","rendering","security","other","deprecation","worker"]},{"name":"level","description":"Message severity.","type":"string","enum":["log","warning","error","debug","info"]},{"name":"text","description":"Message text.","type":"string"},{"name":"url","description":"URL of the message origin.","optional":true,"type":"string"},{"name":"line","description":"Line number in the resource that generated this message (1-based).","optional":true,"type":"integer"},{"name":"column","description":"Column number in the resource that generated this message (1-based).","optional":true,"type":"integer"}]}],"commands":[{"name":"clearMessages","description":"Does nothing."},{"name":"disable","description":"Disables console domain, prevents further console messages from being reported to the client."},{"name":"enable","description":"Enables console domain, sends the messages collected so far to the client by means of the\\n`messageAdded` notification."}],"events":[{"name":"messageAdded","description":"Issued when new console message is added.","parameters":[{"name":"message","description":"Console message that has been added.","$ref":"ConsoleMessage"}]}]},{"domain":"Debugger","description":"Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing\\nbreakpoints, stepping through execution, exploring stack traces, etc.","dependencies":["Runtime"],"types":[{"id":"BreakpointId","description":"Breakpoint identifier.","type":"string"},{"id":"CallFrameId","description":"Call frame identifier.","type":"string"},{"id":"Location","description":"Location in the source code.","type":"object","properties":[{"name":"scriptId","description":"Script identifier as reported in the `Debugger.scriptParsed`.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","optional":true,"type":"integer"}]},{"id":"ScriptPosition","description":"Location in the source code.","experimental":true,"type":"object","properties":[{"name":"lineNumber","type":"integer"},{"name":"columnNumber","type":"integer"}]},{"id":"LocationRange","description":"Location range within one script.","experimental":true,"type":"object","properties":[{"name":"scriptId","$ref":"Runtime.ScriptId"},{"name":"start","$ref":"ScriptPosition"},{"name":"end","$ref":"ScriptPosition"}]},{"id":"CallFrame","description":"JavaScript call frame. Array of call frames form the call stack.","type":"object","properties":[{"name":"callFrameId","description":"Call frame identifier. This identifier is only valid while the virtual machine is paused.","$ref":"CallFrameId"},{"name":"functionName","description":"Name of the JavaScript function called on this call frame.","type":"string"},{"name":"functionLocation","description":"Location in the source code.","optional":true,"$ref":"Location"},{"name":"location","description":"Location in the source code.","$ref":"Location"},{"name":"url","description":"JavaScript script name or url.\\nDeprecated in favor of using the `location.scriptId` to resolve the URL via a previously\\nsent `Debugger.scriptParsed` event.","deprecated":true,"type":"string"},{"name":"scopeChain","description":"Scope chain for this call frame.","type":"array","items":{"$ref":"Scope"}},{"name":"this","description":"`this` object for this call frame.","$ref":"Runtime.RemoteObject"},{"name":"returnValue","description":"The value being returned, if the function is at return point.","optional":true,"$ref":"Runtime.RemoteObject"},{"name":"canBeRestarted","description":"Valid only while the VM is paused and indicates whether this frame\\ncan be restarted or not. Note that a `true` value here does not\\nguarantee that Debugger#restartFrame with this CallFrameId will be\\nsuccessful, but it is very likely.","experimental":true,"optional":true,"type":"boolean"}]},{"id":"Scope","description":"Scope description.","type":"object","properties":[{"name":"type","description":"Scope type.","type":"string","enum":["global","local","with","closure","catch","block","script","eval","module","wasm-expression-stack"]},{"name":"object","description":"Object representing the scope. For `global` and `with` scopes it represents the actual\\nobject; for the rest of the scopes, it is artificial transient object enumerating scope\\nvariables as its properties.","$ref":"Runtime.RemoteObject"},{"name":"name","optional":true,"type":"string"},{"name":"startLocation","description":"Location in the source code where scope starts","optional":true,"$ref":"Location"},{"name":"endLocation","description":"Location in the source code where scope ends","optional":true,"$ref":"Location"}]},{"id":"SearchMatch","description":"Search match for resource.","type":"object","properties":[{"name":"lineNumber","description":"Line number in resource content.","type":"number"},{"name":"lineContent","description":"Line with match content.","type":"string"}]},{"id":"BreakLocation","type":"object","properties":[{"name":"scriptId","description":"Script identifier as reported in the `Debugger.scriptParsed`.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","optional":true,"type":"integer"},{"name":"type","optional":true,"type":"string","enum":["debuggerStatement","call","return"]}]},{"id":"WasmDisassemblyChunk","experimental":true,"type":"object","properties":[{"name":"lines","description":"The next chunk of disassembled lines.","type":"array","items":{"type":"string"}},{"name":"bytecodeOffsets","description":"The bytecode offsets describing the start of each line.","type":"array","items":{"type":"integer"}}]},{"id":"ScriptLanguage","description":"Enum of possible script languages.","type":"string","enum":["JavaScript","WebAssembly"]},{"id":"DebugSymbols","description":"Debug symbols available for a wasm script.","type":"object","properties":[{"name":"type","description":"Type of the debug symbols.","type":"string","enum":["None","SourceMap","EmbeddedDWARF","ExternalDWARF"]},{"name":"externalURL","description":"URL of the external symbol source.","optional":true,"type":"string"}]}],"commands":[{"name":"continueToLocation","description":"Continues execution until specific location is reached.","parameters":[{"name":"location","description":"Location to continue to.","$ref":"Location"},{"name":"targetCallFrames","optional":true,"type":"string","enum":["any","current"]}]},{"name":"disable","description":"Disables debugger for given page."},{"name":"enable","description":"Enables debugger for the given page. Clients should not assume that the debugging has been\\nenabled until the result for this command is received.","parameters":[{"name":"maxScriptsCacheSize","description":"The maximum size in bytes of collected scripts (not referenced by other heap objects)\\nthe debugger can hold. Puts no limit if parameter is omitted.","experimental":true,"optional":true,"type":"number"}],"returns":[{"name":"debuggerId","description":"Unique identifier of the debugger.","experimental":true,"$ref":"Runtime.UniqueDebuggerId"}]},{"name":"evaluateOnCallFrame","description":"Evaluates expression on a given call frame.","parameters":[{"name":"callFrameId","description":"Call frame identifier to evaluate on.","$ref":"CallFrameId"},{"name":"expression","description":"Expression to evaluate.","type":"string"},{"name":"objectGroup","description":"String object group name to put result into (allows rapid releasing resulting object handles\\nusing `releaseObjectGroup`).","optional":true,"type":"string"},{"name":"includeCommandLineAPI","description":"Specifies whether command line API should be available to the evaluated expression, defaults\\nto false.","optional":true,"type":"boolean"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"throwOnSideEffect","description":"Whether to throw an exception if side effect cannot be ruled out during evaluation.","optional":true,"type":"boolean"},{"name":"timeout","description":"Terminate execution after timing out (number of milliseconds).","experimental":true,"optional":true,"$ref":"Runtime.TimeDelta"}],"returns":[{"name":"result","description":"Object wrapper for the evaluation result.","$ref":"Runtime.RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"Runtime.ExceptionDetails"}]},{"name":"getPossibleBreakpoints","description":"Returns possible locations for breakpoint. scriptId in start and end range locations should be\\nthe same.","parameters":[{"name":"start","description":"Start of range to search possible breakpoint locations in.","$ref":"Location"},{"name":"end","description":"End of range to search possible breakpoint locations in (excluding). When not specified, end\\nof scripts is used as end of range.","optional":true,"$ref":"Location"},{"name":"restrictToFunction","description":"Only consider locations which are in the same (non-nested) function as start.","optional":true,"type":"boolean"}],"returns":[{"name":"locations","description":"List of the possible breakpoint locations.","type":"array","items":{"$ref":"BreakLocation"}}]},{"name":"getScriptSource","description":"Returns source for the script with given id.","parameters":[{"name":"scriptId","description":"Id of the script to get source for.","$ref":"Runtime.ScriptId"}],"returns":[{"name":"scriptSource","description":"Script source (empty in case of Wasm bytecode).","type":"string"},{"name":"bytecode","description":"Wasm bytecode. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"}]},{"name":"disassembleWasmModule","experimental":true,"parameters":[{"name":"scriptId","description":"Id of the script to disassemble","$ref":"Runtime.ScriptId"}],"returns":[{"name":"streamId","description":"For large modules, return a stream from which additional chunks of\\ndisassembly can be read successively.","optional":true,"type":"string"},{"name":"totalNumberOfLines","description":"The total number of lines in the disassembly text.","type":"integer"},{"name":"functionBodyOffsets","description":"The offsets of all function bodies, in the format [start1, end1,\\nstart2, end2, ...] where all ends are exclusive.","type":"array","items":{"type":"integer"}},{"name":"chunk","description":"The first chunk of disassembly.","$ref":"WasmDisassemblyChunk"}]},{"name":"nextWasmDisassemblyChunk","description":"Disassemble the next chunk of lines for the module corresponding to the\\nstream. If disassembly is complete, this API will invalidate the streamId\\nand return an empty chunk. Any subsequent calls for the now invalid stream\\nwill return errors.","experimental":true,"parameters":[{"name":"streamId","type":"string"}],"returns":[{"name":"chunk","description":"The next chunk of disassembly.","$ref":"WasmDisassemblyChunk"}]},{"name":"getWasmBytecode","description":"This command is deprecated. Use getScriptSource instead.","deprecated":true,"parameters":[{"name":"scriptId","description":"Id of the Wasm script to get source for.","$ref":"Runtime.ScriptId"}],"returns":[{"name":"bytecode","description":"Script source. (Encoded as a base64 string when passed over JSON)","type":"string"}]},{"name":"getStackTrace","description":"Returns stack trace with given `stackTraceId`.","experimental":true,"parameters":[{"name":"stackTraceId","$ref":"Runtime.StackTraceId"}],"returns":[{"name":"stackTrace","$ref":"Runtime.StackTrace"}]},{"name":"pause","description":"Stops on the next JavaScript statement."},{"name":"pauseOnAsyncCall","experimental":true,"deprecated":true,"parameters":[{"name":"parentStackTraceId","description":"Debugger will pause when async call with given stack trace is started.","$ref":"Runtime.StackTraceId"}]},{"name":"removeBreakpoint","description":"Removes JavaScript breakpoint.","parameters":[{"name":"breakpointId","$ref":"BreakpointId"}]},{"name":"restartFrame","description":"Restarts particular call frame from the beginning. The old, deprecated\\nbehavior of `restartFrame` is to stay paused and allow further CDP commands\\nafter a restart was scheduled. This can cause problems with restarting, so\\nwe now continue execution immediatly after it has been scheduled until we\\nreach the beginning of the restarted frame.\\n\\nTo stay back-wards compatible, `restartFrame` now expects a `mode`\\nparameter to be present. If the `mode` parameter is missing, `restartFrame`\\nerrors out.\\n\\nThe various return values are deprecated and `callFrames` is always empty.\\nUse the call frames from the `Debugger#paused` events instead, that fires\\nonce V8 pauses at the beginning of the restarted function.","parameters":[{"name":"callFrameId","description":"Call frame identifier to evaluate on.","$ref":"CallFrameId"},{"name":"mode","description":"The `mode` parameter must be present and set to \'StepInto\', otherwise\\n`restartFrame` will error out.","experimental":true,"optional":true,"type":"string","enum":["StepInto"]}],"returns":[{"name":"callFrames","description":"New stack trace.","deprecated":true,"type":"array","items":{"$ref":"CallFrame"}},{"name":"asyncStackTrace","description":"Async stack trace, if any.","deprecated":true,"optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","deprecated":true,"optional":true,"$ref":"Runtime.StackTraceId"}]},{"name":"resume","description":"Resumes JavaScript execution.","parameters":[{"name":"terminateOnResume","description":"Set to true to terminate execution upon resuming execution. In contrast\\nto Runtime.terminateExecution, this will allows to execute further\\nJavaScript (i.e. via evaluation) until execution of the paused code\\nis actually resumed, at which point termination is triggered.\\nIf execution is currently not paused, this parameter has no effect.","optional":true,"type":"boolean"}]},{"name":"searchInContent","description":"Searches for given string in script content.","parameters":[{"name":"scriptId","description":"Id of the script to search in.","$ref":"Runtime.ScriptId"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"SearchMatch"}}]},{"name":"setAsyncCallStackDepth","description":"Enables or disables async call stacks tracking.","parameters":[{"name":"maxDepth","description":"Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\\ncall stacks (default).","type":"integer"}]},{"name":"setBlackboxPatterns","description":"Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in\\nscripts with url matching one of the patterns. VM will try to leave blackboxed script by\\nperforming \'step in\' several times, finally resorting to \'step out\' if unsuccessful.","experimental":true,"parameters":[{"name":"patterns","description":"Array of regexps that will be used to check script url for blackbox state.","type":"array","items":{"type":"string"}}]},{"name":"setBlackboxedRanges","description":"Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted\\nscripts by performing \'step in\' several times, finally resorting to \'step out\' if unsuccessful.\\nPositions array contains positions where blackbox state is changed. First interval isn\'t\\nblackboxed. Array should be sorted.","experimental":true,"parameters":[{"name":"scriptId","description":"Id of the script.","$ref":"Runtime.ScriptId"},{"name":"positions","type":"array","items":{"$ref":"ScriptPosition"}}]},{"name":"setBreakpoint","description":"Sets JavaScript breakpoint at a given location.","parameters":[{"name":"location","description":"Location to set breakpoint in.","$ref":"Location"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will only stop on the\\nbreakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"},{"name":"actualLocation","description":"Location this breakpoint resolved into.","$ref":"Location"}]},{"name":"setInstrumentationBreakpoint","description":"Sets instrumentation breakpoint.","parameters":[{"name":"instrumentation","description":"Instrumentation name.","type":"string","enum":["beforeScriptExecution","beforeScriptWithSourceMapExecution"]}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"}]},{"name":"setBreakpointByUrl","description":"Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this\\ncommand is issued, all existing parsed scripts will have breakpoints resolved and returned in\\n`locations` property. Further matching script parsing will result in subsequent\\n`breakpointResolved` events issued. This logical breakpoint will survive page reloads.","parameters":[{"name":"lineNumber","description":"Line number to set breakpoint at.","type":"integer"},{"name":"url","description":"URL of the resources to set breakpoint on.","optional":true,"type":"string"},{"name":"urlRegex","description":"Regex pattern for the URLs of the resources to set breakpoints on. Either `url` or\\n`urlRegex` must be specified.","optional":true,"type":"string"},{"name":"scriptHash","description":"Script hash of the resources to set breakpoint on.","optional":true,"type":"string"},{"name":"columnNumber","description":"Offset in the line to set breakpoint at.","optional":true,"type":"integer"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will only stop on the\\nbreakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"},{"name":"locations","description":"List of the locations this breakpoint resolved into upon addition.","type":"array","items":{"$ref":"Location"}}]},{"name":"setBreakpointOnFunctionCall","description":"Sets JavaScript breakpoint before each call to the given function.\\nIf another function was created from the same source as a given one,\\ncalling it will also trigger the breakpoint.","experimental":true,"parameters":[{"name":"objectId","description":"Function object id.","$ref":"Runtime.RemoteObjectId"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will\\nstop on the breakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"}]},{"name":"setBreakpointsActive","description":"Activates / deactivates all breakpoints on the page.","parameters":[{"name":"active","description":"New value for breakpoints active state.","type":"boolean"}]},{"name":"setPauseOnExceptions","description":"Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions,\\nor caught exceptions, no exceptions. Initial pause on exceptions state is `none`.","parameters":[{"name":"state","description":"Pause on exceptions mode.","type":"string","enum":["none","caught","uncaught","all"]}]},{"name":"setReturnValue","description":"Changes return value in top frame. Available only at return break position.","experimental":true,"parameters":[{"name":"newValue","description":"New return value.","$ref":"Runtime.CallArgument"}]},{"name":"setScriptSource","description":"Edits JavaScript source live.\\n\\nIn general, functions that are currently on the stack can not be edited with\\na single exception: If the edited function is the top-most stack frame and\\nthat is the only activation of that function on the stack. In this case\\nthe live edit will be successful and a `Debugger.restartFrame` for the\\ntop-most function is automatically triggered.","parameters":[{"name":"scriptId","description":"Id of the script to edit.","$ref":"Runtime.ScriptId"},{"name":"scriptSource","description":"New content of the script.","type":"string"},{"name":"dryRun","description":"If true the change will not actually be applied. Dry run may be used to get result\\ndescription without actually modifying the code.","optional":true,"type":"boolean"},{"name":"allowTopFrameEditing","description":"If true, then `scriptSource` is allowed to change the function on top of the stack\\nas long as the top-most stack frame is the only activation of that function.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"callFrames","description":"New stack trace in case editing has happened while VM was stopped.","deprecated":true,"optional":true,"type":"array","items":{"$ref":"CallFrame"}},{"name":"stackChanged","description":"Whether current call stack was modified after applying the changes.","deprecated":true,"optional":true,"type":"boolean"},{"name":"asyncStackTrace","description":"Async stack trace, if any.","deprecated":true,"optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","deprecated":true,"optional":true,"$ref":"Runtime.StackTraceId"},{"name":"status","description":"Whether the operation was successful or not. Only `Ok` denotes a\\nsuccessful live edit while the other enum variants denote why\\nthe live edit failed.","experimental":true,"type":"string","enum":["Ok","CompileError","BlockedByActiveGenerator","BlockedByActiveFunction","BlockedByTopLevelEsModuleChange"]},{"name":"exceptionDetails","description":"Exception details if any. Only present when `status` is `CompileError`.","optional":true,"$ref":"Runtime.ExceptionDetails"}]},{"name":"setSkipAllPauses","description":"Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc).","parameters":[{"name":"skip","description":"New value for skip pauses state.","type":"boolean"}]},{"name":"setVariableValue","description":"Changes value of variable in a callframe. Object-based scopes are not supported and must be\\nmutated manually.","parameters":[{"name":"scopeNumber","description":"0-based number of scope as was listed in scope chain. Only \'local\', \'closure\' and \'catch\'\\nscope types are allowed. Other scopes could be manipulated manually.","type":"integer"},{"name":"variableName","description":"Variable name.","type":"string"},{"name":"newValue","description":"New variable value.","$ref":"Runtime.CallArgument"},{"name":"callFrameId","description":"Id of callframe that holds variable.","$ref":"CallFrameId"}]},{"name":"stepInto","description":"Steps into the function call.","parameters":[{"name":"breakOnAsyncCall","description":"Debugger will pause on the execution of the first async task which was scheduled\\nbefore next pause.","experimental":true,"optional":true,"type":"boolean"},{"name":"skipList","description":"The skipList specifies location ranges that should be skipped on step into.","experimental":true,"optional":true,"type":"array","items":{"$ref":"LocationRange"}}]},{"name":"stepOut","description":"Steps out of the function call."},{"name":"stepOver","description":"Steps over the statement.","parameters":[{"name":"skipList","description":"The skipList specifies location ranges that should be skipped on step over.","experimental":true,"optional":true,"type":"array","items":{"$ref":"LocationRange"}}]}],"events":[{"name":"breakpointResolved","description":"Fired when breakpoint is resolved to an actual script and location.","parameters":[{"name":"breakpointId","description":"Breakpoint unique identifier.","$ref":"BreakpointId"},{"name":"location","description":"Actual breakpoint location.","$ref":"Location"}]},{"name":"paused","description":"Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria.","parameters":[{"name":"callFrames","description":"Call stack the virtual machine stopped on.","type":"array","items":{"$ref":"CallFrame"}},{"name":"reason","description":"Pause reason.","type":"string","enum":["ambiguous","assert","CSPViolation","debugCommand","DOM","EventListener","exception","instrumentation","OOM","other","promiseRejection","XHR","step"]},{"name":"data","description":"Object containing break-specific auxiliary properties.","optional":true,"type":"object"},{"name":"hitBreakpoints","description":"Hit breakpoints IDs","optional":true,"type":"array","items":{"type":"string"}},{"name":"asyncStackTrace","description":"Async stack trace, if any.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","experimental":true,"optional":true,"$ref":"Runtime.StackTraceId"},{"name":"asyncCallStackTraceId","description":"Never present, will be removed.","experimental":true,"deprecated":true,"optional":true,"$ref":"Runtime.StackTraceId"}]},{"name":"resumed","description":"Fired when the virtual machine resumed execution."},{"name":"scriptFailedToParse","description":"Fired when virtual machine fails to parse the script.","parameters":[{"name":"scriptId","description":"Identifier of the script parsed.","$ref":"Runtime.ScriptId"},{"name":"url","description":"URL or name of the script parsed (if any).","type":"string"},{"name":"startLine","description":"Line offset of the script within the resource with given URL (for script tags).","type":"integer"},{"name":"startColumn","description":"Column offset of the script within the resource with given URL.","type":"integer"},{"name":"endLine","description":"Last line of the script.","type":"integer"},{"name":"endColumn","description":"Length of the last line of the script.","type":"integer"},{"name":"executionContextId","description":"Specifies script creation context.","$ref":"Runtime.ExecutionContextId"},{"name":"hash","description":"Content hash of the script, SHA-256.","type":"string"},{"name":"executionContextAuxData","description":"Embedder-specific auxiliary data likely matching {isDefault: boolean, type: \'default\'|\'isolated\'|\'worker\', frameId: string}","optional":true,"type":"object"},{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"},{"name":"hasSourceURL","description":"True, if this script has sourceURL.","optional":true,"type":"boolean"},{"name":"isModule","description":"True, if this script is ES6 module.","optional":true,"type":"boolean"},{"name":"length","description":"This script length.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript top stack frame of where the script parsed event was triggered if available.","experimental":true,"optional":true,"$ref":"Runtime.StackTrace"},{"name":"codeOffset","description":"If the scriptLanguage is WebAssembly, the code section offset in the module.","experimental":true,"optional":true,"type":"integer"},{"name":"scriptLanguage","description":"The language of the script.","experimental":true,"optional":true,"$ref":"Debugger.ScriptLanguage"},{"name":"embedderName","description":"The name the embedder supplied for this script.","experimental":true,"optional":true,"type":"string"}]},{"name":"scriptParsed","description":"Fired when virtual machine parses script. This event is also fired for all known and uncollected\\nscripts upon enabling debugger.","parameters":[{"name":"scriptId","description":"Identifier of the script parsed.","$ref":"Runtime.ScriptId"},{"name":"url","description":"URL or name of the script parsed (if any).","type":"string"},{"name":"startLine","description":"Line offset of the script within the resource with given URL (for script tags).","type":"integer"},{"name":"startColumn","description":"Column offset of the script within the resource with given URL.","type":"integer"},{"name":"endLine","description":"Last line of the script.","type":"integer"},{"name":"endColumn","description":"Length of the last line of the script.","type":"integer"},{"name":"executionContextId","description":"Specifies script creation context.","$ref":"Runtime.ExecutionContextId"},{"name":"hash","description":"Content hash of the script, SHA-256.","type":"string"},{"name":"executionContextAuxData","description":"Embedder-specific auxiliary data likely matching {isDefault: boolean, type: \'default\'|\'isolated\'|\'worker\', frameId: string}","optional":true,"type":"object"},{"name":"isLiveEdit","description":"True, if this script is generated as a result of the live edit operation.","experimental":true,"optional":true,"type":"boolean"},{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"},{"name":"hasSourceURL","description":"True, if this script has sourceURL.","optional":true,"type":"boolean"},{"name":"isModule","description":"True, if this script is ES6 module.","optional":true,"type":"boolean"},{"name":"length","description":"This script length.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript top stack frame of where the script parsed event was triggered if available.","experimental":true,"optional":true,"$ref":"Runtime.StackTrace"},{"name":"codeOffset","description":"If the scriptLanguage is WebAssembly, the code section offset in the module.","experimental":true,"optional":true,"type":"integer"},{"name":"scriptLanguage","description":"The language of the script.","experimental":true,"optional":true,"$ref":"Debugger.ScriptLanguage"},{"name":"debugSymbols","description":"If the scriptLanguage is WebASsembly, the source of debug symbols for the module.","experimental":true,"optional":true,"$ref":"Debugger.DebugSymbols"},{"name":"embedderName","description":"The name the embedder supplied for this script.","experimental":true,"optional":true,"type":"string"}]}]},{"domain":"HeapProfiler","experimental":true,"dependencies":["Runtime"],"types":[{"id":"HeapSnapshotObjectId","description":"Heap snapshot object id.","type":"string"},{"id":"SamplingHeapProfileNode","description":"Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes.","type":"object","properties":[{"name":"callFrame","description":"Function location.","$ref":"Runtime.CallFrame"},{"name":"selfSize","description":"Allocations size in bytes for the node excluding children.","type":"number"},{"name":"id","description":"Node id. Ids are unique across all profiles collected between startSampling and stopSampling.","type":"integer"},{"name":"children","description":"Child nodes.","type":"array","items":{"$ref":"SamplingHeapProfileNode"}}]},{"id":"SamplingHeapProfileSample","description":"A single sample from a sampling profile.","type":"object","properties":[{"name":"size","description":"Allocation size in bytes attributed to the sample.","type":"number"},{"name":"nodeId","description":"Id of the corresponding profile tree node.","type":"integer"},{"name":"ordinal","description":"Time-ordered sample ordinal number. It is unique across all profiles retrieved\\nbetween startSampling and stopSampling.","type":"number"}]},{"id":"SamplingHeapProfile","description":"Sampling profile.","type":"object","properties":[{"name":"head","$ref":"SamplingHeapProfileNode"},{"name":"samples","type":"array","items":{"$ref":"SamplingHeapProfileSample"}}]}],"commands":[{"name":"addInspectedHeapObject","description":"Enables console to refer to the node with given id via $x (see Command Line API for more details\\n$x functions).","parameters":[{"name":"heapObjectId","description":"Heap snapshot object id to be accessible by means of $x command line API.","$ref":"HeapSnapshotObjectId"}]},{"name":"collectGarbage"},{"name":"disable"},{"name":"enable"},{"name":"getHeapObjectId","parameters":[{"name":"objectId","description":"Identifier of the object to get heap object id for.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"heapSnapshotObjectId","description":"Id of the heap snapshot object corresponding to the passed remote object id.","$ref":"HeapSnapshotObjectId"}]},{"name":"getObjectByHeapObjectId","parameters":[{"name":"objectId","$ref":"HeapSnapshotObjectId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"}],"returns":[{"name":"result","description":"Evaluation result.","$ref":"Runtime.RemoteObject"}]},{"name":"getSamplingProfile","returns":[{"name":"profile","description":"Return the sampling profile being collected.","$ref":"SamplingHeapProfile"}]},{"name":"startSampling","parameters":[{"name":"samplingInterval","description":"Average sample interval in bytes. Poisson distribution is used for the intervals. The\\ndefault value is 32768 bytes.","optional":true,"type":"number"},{"name":"includeObjectsCollectedByMajorGC","description":"By default, the sampling heap profiler reports only objects which are\\nstill alive when the profile is returned via getSamplingProfile or\\nstopSampling, which is useful for determining what functions contribute\\nthe most to steady-state memory usage. This flag instructs the sampling\\nheap profiler to also include information about objects discarded by\\nmajor GC, which will show which functions cause large temporary memory\\nusage or long GC pauses.","optional":true,"type":"boolean"},{"name":"includeObjectsCollectedByMinorGC","description":"By default, the sampling heap profiler reports only objects which are\\nstill alive when the profile is returned via getSamplingProfile or\\nstopSampling, which is useful for determining what functions contribute\\nthe most to steady-state memory usage. This flag instructs the sampling\\nheap profiler to also include information about objects discarded by\\nminor GC, which is useful when tuning a latency-sensitive application\\nfor minimal GC activity.","optional":true,"type":"boolean"}]},{"name":"startTrackingHeapObjects","parameters":[{"name":"trackAllocations","optional":true,"type":"boolean"}]},{"name":"stopSampling","returns":[{"name":"profile","description":"Recorded sampling heap profile.","$ref":"SamplingHeapProfile"}]},{"name":"stopTrackingHeapObjects","parameters":[{"name":"reportProgress","description":"If true \'reportHeapSnapshotProgress\' events will be generated while snapshot is being taken\\nwhen the tracking is stopped.","optional":true,"type":"boolean"},{"name":"treatGlobalObjectsAsRoots","description":"Deprecated in favor of `exposeInternals`.","deprecated":true,"optional":true,"type":"boolean"},{"name":"captureNumericValue","description":"If true, numerical values are included in the snapshot","optional":true,"type":"boolean"},{"name":"exposeInternals","description":"If true, exposes internals of the snapshot.","experimental":true,"optional":true,"type":"boolean"}]},{"name":"takeHeapSnapshot","parameters":[{"name":"reportProgress","description":"If true \'reportHeapSnapshotProgress\' events will be generated while snapshot is being taken.","optional":true,"type":"boolean"},{"name":"treatGlobalObjectsAsRoots","description":"If true, a raw snapshot without artificial roots will be generated.\\nDeprecated in favor of `exposeInternals`.","deprecated":true,"optional":true,"type":"boolean"},{"name":"captureNumericValue","description":"If true, numerical values are included in the snapshot","optional":true,"type":"boolean"},{"name":"exposeInternals","description":"If true, exposes internals of the snapshot.","experimental":true,"optional":true,"type":"boolean"}]}],"events":[{"name":"addHeapSnapshotChunk","parameters":[{"name":"chunk","type":"string"}]},{"name":"heapStatsUpdate","description":"If heap objects tracking has been started then backend may send update for one or more fragments","parameters":[{"name":"statsUpdate","description":"An array of triplets. Each triplet describes a fragment. The first integer is the fragment\\nindex, the second integer is a total count of objects for the fragment, the third integer is\\na total size of the objects for the fragment.","type":"array","items":{"type":"integer"}}]},{"name":"lastSeenObjectId","description":"If heap objects tracking has been started then backend regularly sends a current value for last\\nseen object id and corresponding timestamp. If the were changes in the heap since last event\\nthen one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event.","parameters":[{"name":"lastSeenObjectId","type":"integer"},{"name":"timestamp","type":"number"}]},{"name":"reportHeapSnapshotProgress","parameters":[{"name":"done","type":"integer"},{"name":"total","type":"integer"},{"name":"finished","optional":true,"type":"boolean"}]},{"name":"resetProfiles"}]},{"domain":"Profiler","dependencies":["Runtime","Debugger"],"types":[{"id":"ProfileNode","description":"Profile node. Holds callsite information, execution statistics and child nodes.","type":"object","properties":[{"name":"id","description":"Unique id of the node.","type":"integer"},{"name":"callFrame","description":"Function location.","$ref":"Runtime.CallFrame"},{"name":"hitCount","description":"Number of samples where this node was on top of the call stack.","optional":true,"type":"integer"},{"name":"children","description":"Child node ids.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"deoptReason","description":"The reason of being not optimized. The function may be deoptimized or marked as don\'t\\noptimize.","optional":true,"type":"string"},{"name":"positionTicks","description":"An array of source position ticks.","optional":true,"type":"array","items":{"$ref":"PositionTickInfo"}}]},{"id":"Profile","description":"Profile.","type":"object","properties":[{"name":"nodes","description":"The list of profile nodes. First item is the root node.","type":"array","items":{"$ref":"ProfileNode"}},{"name":"startTime","description":"Profiling start timestamp in microseconds.","type":"number"},{"name":"endTime","description":"Profiling end timestamp in microseconds.","type":"number"},{"name":"samples","description":"Ids of samples top nodes.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"timeDeltas","description":"Time intervals between adjacent samples in microseconds. The first delta is relative to the\\nprofile startTime.","optional":true,"type":"array","items":{"type":"integer"}}]},{"id":"PositionTickInfo","description":"Specifies a number of samples attributed to a certain source position.","type":"object","properties":[{"name":"line","description":"Source line number (1-based).","type":"integer"},{"name":"ticks","description":"Number of samples attributed to the source line.","type":"integer"}]},{"id":"CoverageRange","description":"Coverage data for a source range.","type":"object","properties":[{"name":"startOffset","description":"JavaScript script source offset for the range start.","type":"integer"},{"name":"endOffset","description":"JavaScript script source offset for the range end.","type":"integer"},{"name":"count","description":"Collected execution count of the source range.","type":"integer"}]},{"id":"FunctionCoverage","description":"Coverage data for a JavaScript function.","type":"object","properties":[{"name":"functionName","description":"JavaScript function name.","type":"string"},{"name":"ranges","description":"Source ranges inside the function with coverage data.","type":"array","items":{"$ref":"CoverageRange"}},{"name":"isBlockCoverage","description":"Whether coverage data for this function has block granularity.","type":"boolean"}]},{"id":"ScriptCoverage","description":"Coverage data for a JavaScript script.","type":"object","properties":[{"name":"scriptId","description":"JavaScript script id.","$ref":"Runtime.ScriptId"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"functions","description":"Functions contained in the script that has coverage data.","type":"array","items":{"$ref":"FunctionCoverage"}}]}],"commands":[{"name":"disable"},{"name":"enable"},{"name":"getBestEffortCoverage","description":"Collect coverage data for the current isolate. The coverage data may be incomplete due to\\ngarbage collection.","returns":[{"name":"result","description":"Coverage data for the current isolate.","type":"array","items":{"$ref":"ScriptCoverage"}}]},{"name":"setSamplingInterval","description":"Changes CPU profiler sampling interval. Must be called before CPU profiles recording started.","parameters":[{"name":"interval","description":"New sampling interval in microseconds.","type":"integer"}]},{"name":"start"},{"name":"startPreciseCoverage","description":"Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code\\ncoverage may be incomplete. Enabling prevents running optimized code and resets execution\\ncounters.","parameters":[{"name":"callCount","description":"Collect accurate call counts beyond simple \'covered\' or \'not covered\'.","optional":true,"type":"boolean"},{"name":"detailed","description":"Collect block-based coverage.","optional":true,"type":"boolean"},{"name":"allowTriggeredUpdates","description":"Allow the backend to send updates on its own initiative","optional":true,"type":"boolean"}],"returns":[{"name":"timestamp","description":"Monotonically increasing time (in seconds) when the coverage update was taken in the backend.","type":"number"}]},{"name":"stop","returns":[{"name":"profile","description":"Recorded profile.","$ref":"Profile"}]},{"name":"stopPreciseCoverage","description":"Disable precise code coverage. Disabling releases unnecessary execution count records and allows\\nexecuting optimized code."},{"name":"takePreciseCoverage","description":"Collect coverage data for the current isolate, and resets execution counters. Precise code\\ncoverage needs to have started.","returns":[{"name":"result","description":"Coverage data for the current isolate.","type":"array","items":{"$ref":"ScriptCoverage"}},{"name":"timestamp","description":"Monotonically increasing time (in seconds) when the coverage update was taken in the backend.","type":"number"}]}],"events":[{"name":"consoleProfileFinished","parameters":[{"name":"id","type":"string"},{"name":"location","description":"Location of console.profileEnd().","$ref":"Debugger.Location"},{"name":"profile","$ref":"Profile"},{"name":"title","description":"Profile title passed as an argument to console.profile().","optional":true,"type":"string"}]},{"name":"consoleProfileStarted","description":"Sent when new profile recording is started using console.profile() call.","parameters":[{"name":"id","type":"string"},{"name":"location","description":"Location of console.profile().","$ref":"Debugger.Location"},{"name":"title","description":"Profile title passed as an argument to console.profile().","optional":true,"type":"string"}]},{"name":"preciseCoverageDeltaUpdate","description":"Reports coverage delta since the last poll (either from an event like this, or from\\n`takePreciseCoverage` for the current isolate. May only be sent if precise code\\ncoverage has been started. This event can be trigged by the embedder to, for example,\\ntrigger collection of coverage data immediately at a certain point in time.","experimental":true,"parameters":[{"name":"timestamp","description":"Monotonically increasing time (in seconds) when the coverage update was taken in the backend.","type":"number"},{"name":"occasion","description":"Identifier for distinguishing coverage events.","type":"string"},{"name":"result","description":"Coverage data for the current isolate.","type":"array","items":{"$ref":"ScriptCoverage"}}]}]},{"domain":"Runtime","description":"Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects.\\nEvaluation results are returned as mirror object that expose object type, string representation\\nand unique identifier that can be used for further object reference. Original objects are\\nmaintained in memory unless they are either explicitly released or are released along with the\\nother objects in their object group.","types":[{"id":"ScriptId","description":"Unique script identifier.","type":"string"},{"id":"SerializationOptions","description":"Represents options for serialization. Overrides `generatePreview`, `returnByValue` and\\n`generateWebDriverValue`.","type":"object","properties":[{"name":"serialization","type":"string","enum":["deep","json","idOnly"]},{"name":"maxDepth","description":"Deep serialization depth. Default is full depth. Respected only in `deep` serialization mode.","optional":true,"type":"integer"},{"name":"additionalParameters","description":"Embedder-specific parameters. For example if connected to V8 in Chrome these control DOM\\nserialization via `maxNodeDepth: integer` and `includeShadowTree: \\"none\\" | \\"open\\" | \\"all\\"`.\\nValues can be only of type string or integer.","optional":true,"type":"object"}]},{"id":"DeepSerializedValue","description":"Represents deep serialized value.","type":"object","properties":[{"name":"type","type":"string","enum":["undefined","null","string","number","boolean","bigint","regexp","date","symbol","array","object","function","map","set","weakmap","weakset","error","proxy","promise","typedarray","arraybuffer","node","window"]},{"name":"value","optional":true,"type":"any"},{"name":"objectId","optional":true,"type":"string"},{"name":"weakLocalObjectReference","description":"Set if value reference met more then once during serialization. In such\\ncase, value is provided only to one of the serialized values. Unique\\nper value in the scope of one CDP call.","optional":true,"type":"integer"}]},{"id":"RemoteObjectId","description":"Unique object identifier.","type":"string"},{"id":"UnserializableValue","description":"Primitive value which cannot be JSON-stringified. Includes values `-0`, `NaN`, `Infinity`,\\n`-Infinity`, and bigint literals.","type":"string"},{"id":"RemoteObject","description":"Mirror object referencing original JavaScript object.","type":"object","properties":[{"name":"type","description":"Object type.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","bigint"]},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.\\nNOTE: If you change anything here, make sure to also update\\n`subtype` in `ObjectPreview` and `PropertyPreview` below.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error","proxy","promise","typedarray","arraybuffer","dataview","webassemblymemory","wasmvalue"]},{"name":"className","description":"Object class (constructor) name. Specified for `object` type values only.","optional":true,"type":"string"},{"name":"value","description":"Remote object value in case of primitive values or JSON values (if it was requested).","optional":true,"type":"any"},{"name":"unserializableValue","description":"Primitive value which can not be JSON-stringified does not have `value`, but gets this\\nproperty.","optional":true,"$ref":"UnserializableValue"},{"name":"description","description":"String representation of the object.","optional":true,"type":"string"},{"name":"webDriverValue","description":"Deprecated. Use `deepSerializedValue` instead. WebDriver BiDi representation of the value.","deprecated":true,"optional":true,"$ref":"DeepSerializedValue"},{"name":"deepSerializedValue","description":"Deep serialized value.","experimental":true,"optional":true,"$ref":"DeepSerializedValue"},{"name":"objectId","description":"Unique object identifier (for non-primitive values).","optional":true,"$ref":"RemoteObjectId"},{"name":"preview","description":"Preview containing abbreviated property values. Specified for `object` type values only.","experimental":true,"optional":true,"$ref":"ObjectPreview"},{"name":"customPreview","experimental":true,"optional":true,"$ref":"CustomPreview"}]},{"id":"CustomPreview","experimental":true,"type":"object","properties":[{"name":"header","description":"The JSON-stringified result of formatter.header(object, config) call.\\nIt contains json ML array that represents RemoteObject.","type":"string"},{"name":"bodyGetterId","description":"If formatter returns true as a result of formatter.hasBody call then bodyGetterId will\\ncontain RemoteObjectId for the function that returns result of formatter.body(object, config) call.\\nThe result value is json ML array.","optional":true,"$ref":"RemoteObjectId"}]},{"id":"ObjectPreview","description":"Object containing abbreviated remote object value.","experimental":true,"type":"object","properties":[{"name":"type","description":"Object type.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","bigint"]},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error","proxy","promise","typedarray","arraybuffer","dataview","webassemblymemory","wasmvalue"]},{"name":"description","description":"String representation of the object.","optional":true,"type":"string"},{"name":"overflow","description":"True iff some of the properties or entries of the original object did not fit.","type":"boolean"},{"name":"properties","description":"List of the properties.","type":"array","items":{"$ref":"PropertyPreview"}},{"name":"entries","description":"List of the entries. Specified for `map` and `set` subtype values only.","optional":true,"type":"array","items":{"$ref":"EntryPreview"}}]},{"id":"PropertyPreview","experimental":true,"type":"object","properties":[{"name":"name","description":"Property name.","type":"string"},{"name":"type","description":"Object type. Accessor means that the property itself is an accessor property.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","accessor","bigint"]},{"name":"value","description":"User-friendly property value string.","optional":true,"type":"string"},{"name":"valuePreview","description":"Nested value preview.","optional":true,"$ref":"ObjectPreview"},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error","proxy","promise","typedarray","arraybuffer","dataview","webassemblymemory","wasmvalue"]}]},{"id":"EntryPreview","experimental":true,"type":"object","properties":[{"name":"key","description":"Preview of the key. Specified for map-like collection entries.","optional":true,"$ref":"ObjectPreview"},{"name":"value","description":"Preview of the value.","$ref":"ObjectPreview"}]},{"id":"PropertyDescriptor","description":"Object property descriptor.","type":"object","properties":[{"name":"name","description":"Property name or symbol description.","type":"string"},{"name":"value","description":"The value associated with the property.","optional":true,"$ref":"RemoteObject"},{"name":"writable","description":"True if the value associated with the property may be changed (data descriptors only).","optional":true,"type":"boolean"},{"name":"get","description":"A function which serves as a getter for the property, or `undefined` if there is no getter\\n(accessor descriptors only).","optional":true,"$ref":"RemoteObject"},{"name":"set","description":"A function which serves as a setter for the property, or `undefined` if there is no setter\\n(accessor descriptors only).","optional":true,"$ref":"RemoteObject"},{"name":"configurable","description":"True if the type of this property descriptor may be changed and if the property may be\\ndeleted from the corresponding object.","type":"boolean"},{"name":"enumerable","description":"True if this property shows up during enumeration of the properties on the corresponding\\nobject.","type":"boolean"},{"name":"wasThrown","description":"True if the result was thrown during the evaluation.","optional":true,"type":"boolean"},{"name":"isOwn","description":"True if the property is owned for the object.","optional":true,"type":"boolean"},{"name":"symbol","description":"Property symbol object, if the property is of the `symbol` type.","optional":true,"$ref":"RemoteObject"}]},{"id":"InternalPropertyDescriptor","description":"Object internal property descriptor. This property isn\'t normally visible in JavaScript code.","type":"object","properties":[{"name":"name","description":"Conventional property name.","type":"string"},{"name":"value","description":"The value associated with the property.","optional":true,"$ref":"RemoteObject"}]},{"id":"PrivatePropertyDescriptor","description":"Object private field descriptor.","experimental":true,"type":"object","properties":[{"name":"name","description":"Private property name.","type":"string"},{"name":"value","description":"The value associated with the private property.","optional":true,"$ref":"RemoteObject"},{"name":"get","description":"A function which serves as a getter for the private property,\\nor `undefined` if there is no getter (accessor descriptors only).","optional":true,"$ref":"RemoteObject"},{"name":"set","description":"A function which serves as a setter for the private property,\\nor `undefined` if there is no setter (accessor descriptors only).","optional":true,"$ref":"RemoteObject"}]},{"id":"CallArgument","description":"Represents function call argument. Either remote object id `objectId`, primitive `value`,\\nunserializable primitive value or neither of (for undefined) them should be specified.","type":"object","properties":[{"name":"value","description":"Primitive value or serializable javascript object.","optional":true,"type":"any"},{"name":"unserializableValue","description":"Primitive value which can not be JSON-stringified.","optional":true,"$ref":"UnserializableValue"},{"name":"objectId","description":"Remote object handle.","optional":true,"$ref":"RemoteObjectId"}]},{"id":"ExecutionContextId","description":"Id of an execution context.","type":"integer"},{"id":"ExecutionContextDescription","description":"Description of an isolated world.","type":"object","properties":[{"name":"id","description":"Unique id of the execution context. It can be used to specify in which execution context\\nscript evaluation should be performed.","$ref":"ExecutionContextId"},{"name":"origin","description":"Execution context origin.","type":"string"},{"name":"name","description":"Human readable name describing given context.","type":"string"},{"name":"uniqueId","description":"A system-unique execution context identifier. Unlike the id, this is unique across\\nmultiple processes, so can be reliably used to identify specific context while backend\\nperforms a cross-process navigation.","experimental":true,"type":"string"},{"name":"auxData","description":"Embedder-specific auxiliary data likely matching {isDefault: boolean, type: \'default\'|\'isolated\'|\'worker\', frameId: string}","optional":true,"type":"object"}]},{"id":"ExceptionDetails","description":"Detailed information about exception (or error) that was thrown during script compilation or\\nexecution.","type":"object","properties":[{"name":"exceptionId","description":"Exception id.","type":"integer"},{"name":"text","description":"Exception text, which should be used together with exception object when available.","type":"string"},{"name":"lineNumber","description":"Line number of the exception location (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number of the exception location (0-based).","type":"integer"},{"name":"scriptId","description":"Script ID of the exception location.","optional":true,"$ref":"ScriptId"},{"name":"url","description":"URL of the exception location, to be used when the script was not reported.","optional":true,"type":"string"},{"name":"stackTrace","description":"JavaScript stack trace if available.","optional":true,"$ref":"StackTrace"},{"name":"exception","description":"Exception object if available.","optional":true,"$ref":"RemoteObject"},{"name":"executionContextId","description":"Identifier of the context where exception happened.","optional":true,"$ref":"ExecutionContextId"},{"name":"exceptionMetaData","description":"Dictionary with entries of meta data that the client associated\\nwith this exception, such as information about associated network\\nrequests, etc.","experimental":true,"optional":true,"type":"object"}]},{"id":"Timestamp","description":"Number of milliseconds since epoch.","type":"number"},{"id":"TimeDelta","description":"Number of milliseconds.","type":"number"},{"id":"CallFrame","description":"Stack entry for runtime errors and assertions.","type":"object","properties":[{"name":"functionName","description":"JavaScript function name.","type":"string"},{"name":"scriptId","description":"JavaScript script id.","$ref":"ScriptId"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"lineNumber","description":"JavaScript script line number (0-based).","type":"integer"},{"name":"columnNumber","description":"JavaScript script column number (0-based).","type":"integer"}]},{"id":"StackTrace","description":"Call frames for assertions or error messages.","type":"object","properties":[{"name":"description","description":"String label of this stack trace. For async traces this may be a name of the function that\\ninitiated the async call.","optional":true,"type":"string"},{"name":"callFrames","description":"JavaScript function name.","type":"array","items":{"$ref":"CallFrame"}},{"name":"parent","description":"Asynchronous JavaScript stack trace that preceded this stack, if available.","optional":true,"$ref":"StackTrace"},{"name":"parentId","description":"Asynchronous JavaScript stack trace that preceded this stack, if available.","experimental":true,"optional":true,"$ref":"StackTraceId"}]},{"id":"UniqueDebuggerId","description":"Unique identifier of current debugger.","experimental":true,"type":"string"},{"id":"StackTraceId","description":"If `debuggerId` is set stack trace comes from another debugger and can be resolved there. This\\nallows to track cross-debugger calls. See `Runtime.StackTrace` and `Debugger.paused` for usages.","experimental":true,"type":"object","properties":[{"name":"id","type":"string"},{"name":"debuggerId","optional":true,"$ref":"UniqueDebuggerId"}]}],"commands":[{"name":"awaitPromise","description":"Add handler to promise with given promise object id.","parameters":[{"name":"promiseObjectId","description":"Identifier of the promise.","$ref":"RemoteObjectId"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Promise result. Will contain rejected value if promise was rejected.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details if stack strace is available.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"callFunctionOn","description":"Calls function with given declaration on the given object. Object group of the result is\\ninherited from the target object.","parameters":[{"name":"functionDeclaration","description":"Declaration of the function to call.","type":"string"},{"name":"objectId","description":"Identifier of the object to call function on. Either objectId or executionContextId should\\nbe specified.","optional":true,"$ref":"RemoteObjectId"},{"name":"arguments","description":"Call arguments. All call arguments must belong to the same JavaScript world as the target\\nobject.","optional":true,"type":"array","items":{"$ref":"CallArgument"}},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object which should be sent by value.\\nCan be overriden by `serializationOptions`.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"userGesture","description":"Whether execution should be treated as initiated by user in the UI.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"},{"name":"executionContextId","description":"Specifies execution context which global object will be used to call function on. Either\\nexecutionContextId or objectId should be specified.","optional":true,"$ref":"ExecutionContextId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects. If objectGroup is not\\nspecified and objectId is, objectGroup will be inherited from object.","optional":true,"type":"string"},{"name":"throwOnSideEffect","description":"Whether to throw an exception if side effect cannot be ruled out during evaluation.","experimental":true,"optional":true,"type":"boolean"},{"name":"uniqueContextId","description":"An alternative way to specify the execution context to call function on.\\nCompared to contextId that may be reused across processes, this is guaranteed to be\\nsystem-unique, so it can be used to prevent accidental function call\\nin context different than intended (e.g. as a result of navigation across process\\nboundaries).\\nThis is mutually exclusive with `executionContextId`.","experimental":true,"optional":true,"type":"string"},{"name":"generateWebDriverValue","description":"Deprecated. Use `serializationOptions: {serialization:\\"deep\\"}` instead.\\nWhether the result should contain `webDriverValue`, serialized according to\\nhttps://w3c.github.io/webdriver-bidi. This is mutually exclusive with `returnByValue`, but\\nresulting `objectId` is still provided.","deprecated":true,"optional":true,"type":"boolean"},{"name":"serializationOptions","description":"Specifies the result serialization. If provided, overrides\\n`generatePreview`, `returnByValue` and `generateWebDriverValue`.","experimental":true,"optional":true,"$ref":"SerializationOptions"}],"returns":[{"name":"result","description":"Call result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"compileScript","description":"Compiles expression.","parameters":[{"name":"expression","description":"Expression to compile.","type":"string"},{"name":"sourceURL","description":"Source url to be set for the script.","type":"string"},{"name":"persistScript","description":"Specifies whether the compiled script should be persisted.","type":"boolean"},{"name":"executionContextId","description":"Specifies in which execution context to perform script run. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.","optional":true,"$ref":"ExecutionContextId"}],"returns":[{"name":"scriptId","description":"Id of the script.","optional":true,"$ref":"ScriptId"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"disable","description":"Disables reporting of execution contexts creation."},{"name":"discardConsoleEntries","description":"Discards collected exceptions and console API calls."},{"name":"enable","description":"Enables reporting of execution contexts creation by means of `executionContextCreated` event.\\nWhen the reporting gets enabled the event will be sent immediately for each existing execution\\ncontext."},{"name":"evaluate","description":"Evaluates expression on global object.","parameters":[{"name":"expression","description":"Expression to evaluate.","type":"string"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"includeCommandLineAPI","description":"Determines whether Command Line API should be available during the evaluation.","optional":true,"type":"boolean"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"contextId","description":"Specifies in which execution context to perform evaluation. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.\\nThis is mutually exclusive with `uniqueContextId`, which offers an\\nalternative way to identify the execution context that is more reliable\\nin a multi-process environment.","optional":true,"$ref":"ExecutionContextId"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"userGesture","description":"Whether execution should be treated as initiated by user in the UI.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"},{"name":"throwOnSideEffect","description":"Whether to throw an exception if side effect cannot be ruled out during evaluation.\\nThis implies `disableBreaks` below.","experimental":true,"optional":true,"type":"boolean"},{"name":"timeout","description":"Terminate execution after timing out (number of milliseconds).","experimental":true,"optional":true,"$ref":"TimeDelta"},{"name":"disableBreaks","description":"Disable breakpoints during execution.","experimental":true,"optional":true,"type":"boolean"},{"name":"replMode","description":"Setting this flag to true enables `let` re-declaration and top-level `await`.\\nNote that `let` variables can only be re-declared if they originate from\\n`replMode` themselves.","experimental":true,"optional":true,"type":"boolean"},{"name":"allowUnsafeEvalBlockedByCSP","description":"The Content Security Policy (CSP) for the target might block \'unsafe-eval\'\\nwhich includes eval(), Function(), setTimeout() and setInterval()\\nwhen called with non-callable arguments. This flag bypasses CSP for this\\nevaluation and allows unsafe-eval. Defaults to true.","experimental":true,"optional":true,"type":"boolean"},{"name":"uniqueContextId","description":"An alternative way to specify the execution context to evaluate in.\\nCompared to contextId that may be reused across processes, this is guaranteed to be\\nsystem-unique, so it can be used to prevent accidental evaluation of the expression\\nin context different than intended (e.g. as a result of navigation across process\\nboundaries).\\nThis is mutually exclusive with `contextId`.","experimental":true,"optional":true,"type":"string"},{"name":"generateWebDriverValue","description":"Deprecated. Use `serializationOptions: {serialization:\\"deep\\"}` instead.\\nWhether the result should contain `webDriverValue`, serialized\\naccording to\\nhttps://w3c.github.io/webdriver-bidi. This is mutually exclusive with `returnByValue`, but\\nresulting `objectId` is still provided.","deprecated":true,"optional":true,"type":"boolean"},{"name":"serializationOptions","description":"Specifies the result serialization. If provided, overrides\\n`generatePreview`, `returnByValue` and `generateWebDriverValue`.","experimental":true,"optional":true,"$ref":"SerializationOptions"}],"returns":[{"name":"result","description":"Evaluation result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"getIsolateId","description":"Returns the isolate id.","experimental":true,"returns":[{"name":"id","description":"The isolate id.","type":"string"}]},{"name":"getHeapUsage","description":"Returns the JavaScript heap usage.\\nIt is the total usage of the corresponding isolate not scoped to a particular Runtime.","experimental":true,"returns":[{"name":"usedSize","description":"Used heap size in bytes.","type":"number"},{"name":"totalSize","description":"Allocated heap size in bytes.","type":"number"}]},{"name":"getProperties","description":"Returns properties of a given object. Object group of the result is inherited from the target\\nobject.","parameters":[{"name":"objectId","description":"Identifier of the object to return properties for.","$ref":"RemoteObjectId"},{"name":"ownProperties","description":"If true, returns properties belonging only to the element itself, not to its prototype\\nchain.","optional":true,"type":"boolean"},{"name":"accessorPropertiesOnly","description":"If true, returns accessor properties (with getter/setter) only; internal properties are not\\nreturned either.","experimental":true,"optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the results.","experimental":true,"optional":true,"type":"boolean"},{"name":"nonIndexedPropertiesOnly","description":"If true, returns non-indexed properties only.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Object properties.","type":"array","items":{"$ref":"PropertyDescriptor"}},{"name":"internalProperties","description":"Internal object properties (only of the element itself).","optional":true,"type":"array","items":{"$ref":"InternalPropertyDescriptor"}},{"name":"privateProperties","description":"Object private properties.","experimental":true,"optional":true,"type":"array","items":{"$ref":"PrivatePropertyDescriptor"}},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"globalLexicalScopeNames","description":"Returns all let, const and class variables from global scope.","parameters":[{"name":"executionContextId","description":"Specifies in which execution context to lookup global scope variables.","optional":true,"$ref":"ExecutionContextId"}],"returns":[{"name":"names","type":"array","items":{"type":"string"}}]},{"name":"queryObjects","parameters":[{"name":"prototypeObjectId","description":"Identifier of the prototype to return objects for.","$ref":"RemoteObjectId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release the results.","optional":true,"type":"string"}],"returns":[{"name":"objects","description":"Array with objects.","$ref":"RemoteObject"}]},{"name":"releaseObject","description":"Releases remote object with given id.","parameters":[{"name":"objectId","description":"Identifier of the object to release.","$ref":"RemoteObjectId"}]},{"name":"releaseObjectGroup","description":"Releases all remote objects that belong to a given group.","parameters":[{"name":"objectGroup","description":"Symbolic object group name.","type":"string"}]},{"name":"runIfWaitingForDebugger","description":"Tells inspected instance to run if it was waiting for debugger to attach."},{"name":"runScript","description":"Runs script with given id in a given context.","parameters":[{"name":"scriptId","description":"Id of the script to run.","$ref":"ScriptId"},{"name":"executionContextId","description":"Specifies in which execution context to perform script run. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.","optional":true,"$ref":"ExecutionContextId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"includeCommandLineAPI","description":"Determines whether Command Line API should be available during the evaluation.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object which should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Run result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"setAsyncCallStackDepth","description":"Enables or disables async call stacks tracking.","redirect":"Debugger","parameters":[{"name":"maxDepth","description":"Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\\ncall stacks (default).","type":"integer"}]},{"name":"setCustomObjectFormatterEnabled","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"setMaxCallStackSizeToCapture","experimental":true,"parameters":[{"name":"size","type":"integer"}]},{"name":"terminateExecution","description":"Terminate current or next JavaScript execution.\\nWill cancel the termination when the outer-most script execution ends.","experimental":true},{"name":"addBinding","description":"If executionContextId is empty, adds binding with the given name on the\\nglobal objects of all inspected contexts, including those created later,\\nbindings survive reloads.\\nBinding function takes exactly one argument, this argument should be string,\\nin case of any other input, function throws an exception.\\nEach binding function call produces Runtime.bindingCalled notification.","experimental":true,"parameters":[{"name":"name","type":"string"},{"name":"executionContextId","description":"If specified, the binding would only be exposed to the specified\\nexecution context. If omitted and `executionContextName` is not set,\\nthe binding is exposed to all execution contexts of the target.\\nThis parameter is mutually exclusive with `executionContextName`.\\nDeprecated in favor of `executionContextName` due to an unclear use case\\nand bugs in implementation (crbug.com/1169639). `executionContextId` will be\\nremoved in the future.","deprecated":true,"optional":true,"$ref":"ExecutionContextId"},{"name":"executionContextName","description":"If specified, the binding is exposed to the executionContext with\\nmatching name, even for contexts created after the binding is added.\\nSee also `ExecutionContext.name` and `worldName` parameter to\\n`Page.addScriptToEvaluateOnNewDocument`.\\nThis parameter is mutually exclusive with `executionContextId`.","experimental":true,"optional":true,"type":"string"}]},{"name":"removeBinding","description":"This method does not remove binding function from global object but\\nunsubscribes current runtime agent from Runtime.bindingCalled notifications.","experimental":true,"parameters":[{"name":"name","type":"string"}]},{"name":"getExceptionDetails","description":"This method tries to lookup and populate exception details for a\\nJavaScript Error object.\\nNote that the stackTrace portion of the resulting exceptionDetails will\\nonly be populated if the Runtime domain was enabled at the time when the\\nError was thrown.","experimental":true,"parameters":[{"name":"errorObjectId","description":"The error object for which to resolve the exception details.","$ref":"RemoteObjectId"}],"returns":[{"name":"exceptionDetails","optional":true,"$ref":"ExceptionDetails"}]}],"events":[{"name":"bindingCalled","description":"Notification is issued every time when binding is called.","experimental":true,"parameters":[{"name":"name","type":"string"},{"name":"payload","type":"string"},{"name":"executionContextId","description":"Identifier of the context where the call was made.","$ref":"ExecutionContextId"}]},{"name":"consoleAPICalled","description":"Issued when console API was called.","parameters":[{"name":"type","description":"Type of the call.","type":"string","enum":["log","debug","info","error","warning","dir","dirxml","table","trace","clear","startGroup","startGroupCollapsed","endGroup","assert","profile","profileEnd","count","timeEnd"]},{"name":"args","description":"Call arguments.","type":"array","items":{"$ref":"RemoteObject"}},{"name":"executionContextId","description":"Identifier of the context where the call was made.","$ref":"ExecutionContextId"},{"name":"timestamp","description":"Call timestamp.","$ref":"Timestamp"},{"name":"stackTrace","description":"Stack trace captured when the call was made. The async stack chain is automatically reported for\\nthe following call types: `assert`, `error`, `trace`, `warning`. For other types the async call\\nchain can be retrieved using `Debugger.getStackTrace` and `stackTrace.parentId` field.","optional":true,"$ref":"StackTrace"},{"name":"context","description":"Console context descriptor for calls on non-default console context (not console.*):\\n\'anonymous#unique-logger-id\' for call on unnamed context, \'name#unique-logger-id\' for call\\non named context.","experimental":true,"optional":true,"type":"string"}]},{"name":"exceptionRevoked","description":"Issued when unhandled exception was revoked.","parameters":[{"name":"reason","description":"Reason describing why exception was revoked.","type":"string"},{"name":"exceptionId","description":"The id of revoked exception, as reported in `exceptionThrown`.","type":"integer"}]},{"name":"exceptionThrown","description":"Issued when exception was thrown and unhandled.","parameters":[{"name":"timestamp","description":"Timestamp of the exception.","$ref":"Timestamp"},{"name":"exceptionDetails","$ref":"ExceptionDetails"}]},{"name":"executionContextCreated","description":"Issued when new execution context is created.","parameters":[{"name":"context","description":"A newly created execution context.","$ref":"ExecutionContextDescription"}]},{"name":"executionContextDestroyed","description":"Issued when execution context is destroyed.","parameters":[{"name":"executionContextId","description":"Id of the destroyed context","deprecated":true,"$ref":"ExecutionContextId"},{"name":"executionContextUniqueId","description":"Unique Id of the destroyed context","experimental":true,"type":"string"}]},{"name":"executionContextsCleared","description":"Issued when all executionContexts were cleared in browser"},{"name":"inspectRequested","description":"Issued when object should be inspected (for example, as a result of inspect() command line API\\ncall).","parameters":[{"name":"object","$ref":"RemoteObject"},{"name":"hints","type":"object"},{"name":"executionContextId","description":"Identifier of the context where the call was made.","experimental":true,"optional":true,"$ref":"ExecutionContextId"}]}]},{"domain":"Schema","description":"This domain is deprecated.","deprecated":true,"types":[{"id":"Domain","description":"Description of the protocol domain.","type":"object","properties":[{"name":"name","description":"Domain name.","type":"string"},{"name":"version","description":"Domain version.","type":"string"}]}],"commands":[{"name":"getDomains","description":"Returns supported domains.","returns":[{"name":"domains","description":"List of supported domains.","type":"array","items":{"$ref":"Domain"}}]}]}]}')}},t={};function r(n){var i=t[n];if(void 0!==i)return i.exports;var o=t[n]={id:n,loaded:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.loaded=!0,o.exports}r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),r(6124);var n=r(6010);module.exports.CDP=n})(); \ No newline at end of file
diff --git a/node_modules/chrome-remote-interface/index.js b/node_modules/chrome-remote-interface/index.js
new file mode 100644
index 0000000..3a32c89
--- /dev/null
+++ b/node_modules/chrome-remote-interface/index.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const EventEmitter = require('events');
+const dns = require('dns');
+
+const devtools = require('./lib/devtools.js');
+const Chrome = require('./lib/chrome.js');
+
+// XXX reset the default that has been changed in
+// (https://github.com/nodejs/node/pull/39987) to prefer IPv4. since
+// implementations alway bind on 127.0.0.1 this solution should be fairly safe
+// (see #467)
+if (dns.setDefaultResultOrder) {
+ dns.setDefaultResultOrder('ipv4first');
+}
+
+function CDP(options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+ const notifier = new EventEmitter();
+ if (typeof callback === 'function') {
+ // allow to register the error callback later
+ process.nextTick(() => {
+ new Chrome(options, notifier);
+ });
+ return notifier.once('connect', callback);
+ } else {
+ return new Promise((fulfill, reject) => {
+ notifier.once('connect', fulfill);
+ notifier.once('error', reject);
+ new Chrome(options, notifier);
+ });
+ }
+}
+
+module.exports = CDP;
+module.exports.Protocol = devtools.Protocol;
+module.exports.List = devtools.List;
+module.exports.New = devtools.New;
+module.exports.Activate = devtools.Activate;
+module.exports.Close = devtools.Close;
+module.exports.Version = devtools.Version;
diff --git a/node_modules/chrome-remote-interface/lib/api.js b/node_modules/chrome-remote-interface/lib/api.js
new file mode 100644
index 0000000..03fc75d
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/api.js
@@ -0,0 +1,92 @@
+'use strict';
+
+function arrayToObject(parameters) {
+ const keyValue = {};
+ parameters.forEach((parameter) =>{
+ const name = parameter.name;
+ delete parameter.name;
+ keyValue[name] = parameter;
+ });
+ return keyValue;
+}
+
+function decorate(to, category, object) {
+ to.category = category;
+ Object.keys(object).forEach((field) => {
+ // skip the 'name' field as it is part of the function prototype
+ if (field === 'name') {
+ return;
+ }
+ // commands and events have parameters whereas types have properties
+ if (category === 'type' && field === 'properties' ||
+ field === 'parameters') {
+ to[field] = arrayToObject(object[field]);
+ } else {
+ to[field] = object[field];
+ }
+ });
+}
+
+function addCommand(chrome, domainName, command) {
+ const commandName = `${domainName}.${command.name}`;
+ const handler = (params, sessionId, callback) => {
+ return chrome.send(commandName, params, sessionId, callback);
+ };
+ decorate(handler, 'command', command);
+ chrome[commandName] = chrome[domainName][command.name] = handler;
+}
+
+function addEvent(chrome, domainName, event) {
+ const eventName = `${domainName}.${event.name}`;
+ const handler = (sessionId, handler) => {
+ if (typeof sessionId === 'function') {
+ handler = sessionId;
+ sessionId = undefined;
+ }
+ const rawEventName = sessionId ? `${eventName}.${sessionId}` : eventName;
+ if (typeof handler === 'function') {
+ chrome.on(rawEventName, handler);
+ return () => chrome.removeListener(rawEventName, handler);
+ } else {
+ return new Promise((fulfill, reject) => {
+ chrome.once(rawEventName, fulfill);
+ });
+ }
+ };
+ decorate(handler, 'event', event);
+ chrome[eventName] = chrome[domainName][event.name] = handler;
+}
+
+function addType(chrome, domainName, type) {
+ const typeName = `${domainName}.${type.id}`;
+ const help = {};
+ decorate(help, 'type', type);
+ chrome[typeName] = chrome[domainName][type.id] = help;
+}
+
+function prepare(object, protocol) {
+ // assign the protocol and generate the shorthands
+ object.protocol = protocol;
+ protocol.domains.forEach((domain) => {
+ const domainName = domain.domain;
+ object[domainName] = {};
+ // add commands
+ (domain.commands || []).forEach((command) => {
+ addCommand(object, domainName, command);
+ });
+ // add events
+ (domain.events || []).forEach((event) => {
+ addEvent(object, domainName, event);
+ });
+ // add types
+ (domain.types || []).forEach((type) => {
+ addType(object, domainName, type);
+ });
+ // add utility listener for each domain
+ object[domainName].on = (eventName, handler) => {
+ return object[domainName][eventName](handler);
+ };
+ });
+}
+
+module.exports.prepare = prepare;
diff --git a/node_modules/chrome-remote-interface/lib/chrome.js b/node_modules/chrome-remote-interface/lib/chrome.js
new file mode 100644
index 0000000..b093d1e
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/chrome.js
@@ -0,0 +1,314 @@
+'use strict';
+
+const EventEmitter = require('events');
+const util = require('util');
+const formatUrl = require('url').format;
+const parseUrl = require('url').parse;
+
+const WebSocket = require('ws');
+
+const api = require('./api.js');
+const defaults = require('./defaults.js');
+const devtools = require('./devtools.js');
+
+class ProtocolError extends Error {
+ constructor(request, response) {
+ let {message} = response;
+ if (response.data) {
+ message += ` (${response.data})`;
+ }
+ super(message);
+ // attach the original response as well
+ this.request = request;
+ this.response = response;
+ }
+}
+
+class Chrome extends EventEmitter {
+ constructor(options, notifier) {
+ super();
+ // options
+ const defaultTarget = (targets) => {
+ // prefer type = 'page' inspectable targets as they represents
+ // browser tabs (fall back to the first inspectable target
+ // otherwise)
+ let backup;
+ let target = targets.find((target) => {
+ if (target.webSocketDebuggerUrl) {
+ backup = backup || target;
+ return target.type === 'page';
+ } else {
+ return false;
+ }
+ });
+ target = target || backup;
+ if (target) {
+ return target;
+ } else {
+ throw new Error('No inspectable targets');
+ }
+ };
+ options = options || {};
+ this.host = options.host || defaults.HOST;
+ this.port = options.port || defaults.PORT;
+ this.secure = !!(options.secure);
+ this.useHostName = !!(options.useHostName);
+ this.alterPath = options.alterPath || ((path) => path);
+ this.protocol = options.protocol;
+ this.local = !!(options.local);
+ this.target = options.target || defaultTarget;
+ // locals
+ this._notifier = notifier;
+ this._callbacks = {};
+ this._nextCommandId = 1;
+ // properties
+ this.webSocketUrl = undefined;
+ // operations
+ this._start();
+ }
+
+ // avoid misinterpreting protocol's members as custom util.inspect functions
+ inspect(depth, options) {
+ options.customInspect = false;
+ return util.inspect(this, options);
+ }
+
+ send(method, params, sessionId, callback) {
+ // handle optional arguments
+ const optionals = Array.from(arguments).slice(1);
+ params = optionals.find(x => typeof x === 'object');
+ sessionId = optionals.find(x => typeof x === 'string');
+ callback = optionals.find(x => typeof x === 'function');
+ // return a promise when a callback is not provided
+ if (typeof callback === 'function') {
+ this._enqueueCommand(method, params, sessionId, callback);
+ return undefined;
+ } else {
+ return new Promise((fulfill, reject) => {
+ this._enqueueCommand(method, params, sessionId, (error, response) => {
+ if (error) {
+ const request = {method, params, sessionId};
+ reject(
+ error instanceof Error
+ ? error // low-level WebSocket error
+ : new ProtocolError(request, response)
+ );
+ } else {
+ fulfill(response);
+ }
+ });
+ });
+ }
+ }
+
+ close(callback) {
+ const closeWebSocket = (callback) => {
+ // don't close if it's already closed
+ if (this._ws.readyState === 3) {
+ callback();
+ } else {
+ // don't notify on user-initiated shutdown ('disconnect' event)
+ this._ws.removeAllListeners('close');
+ this._ws.once('close', () => {
+ this._ws.removeAllListeners();
+ this._handleConnectionClose();
+ callback();
+ });
+ this._ws.close();
+ }
+ };
+ if (typeof callback === 'function') {
+ closeWebSocket(callback);
+ return undefined;
+ } else {
+ return new Promise((fulfill, reject) => {
+ closeWebSocket(fulfill);
+ });
+ }
+ }
+
+ // initiate the connection process
+ async _start() {
+ const options = {
+ host: this.host,
+ port: this.port,
+ secure: this.secure,
+ useHostName: this.useHostName,
+ alterPath: this.alterPath
+ };
+ try {
+ // fetch the WebSocket debugger URL
+ const url = await this._fetchDebuggerURL(options);
+ // allow the user to alter the URL
+ const urlObject = parseUrl(url);
+ urlObject.pathname = options.alterPath(urlObject.pathname);
+ this.webSocketUrl = formatUrl(urlObject);
+ // update the connection parameters using the debugging URL
+ options.host = urlObject.hostname;
+ options.port = urlObject.port || options.port;
+ // fetch the protocol and prepare the API
+ const protocol = await this._fetchProtocol(options);
+ api.prepare(this, protocol);
+ // finally connect to the WebSocket
+ await this._connectToWebSocket();
+ // since the handler is executed synchronously, the emit() must be
+ // performed in the next tick so that uncaught errors in the client code
+ // are not intercepted by the Promise mechanism and therefore reported
+ // via the 'error' event
+ process.nextTick(() => {
+ this._notifier.emit('connect', this);
+ });
+ } catch (err) {
+ this._notifier.emit('error', err);
+ }
+ }
+
+ // fetch the WebSocket URL according to 'target'
+ async _fetchDebuggerURL(options) {
+ const userTarget = this.target;
+ switch (typeof userTarget) {
+ case 'string': {
+ let idOrUrl = userTarget;
+ // use default host and port if omitted (and a relative URL is specified)
+ if (idOrUrl.startsWith('/')) {
+ idOrUrl = `ws://${this.host}:${this.port}${idOrUrl}`;
+ }
+ // a WebSocket URL is specified by the user (e.g., node-inspector)
+ if (idOrUrl.match(/^wss?:/i)) {
+ return idOrUrl; // done!
+ }
+ // a target id is specified by the user
+ else {
+ const targets = await devtools.List(options);
+ const object = targets.find((target) => target.id === idOrUrl);
+ return object.webSocketDebuggerUrl;
+ }
+ }
+ case 'object': {
+ const object = userTarget;
+ return object.webSocketDebuggerUrl;
+ }
+ case 'function': {
+ const func = userTarget;
+ const targets = await devtools.List(options);
+ const result = func(targets);
+ const object = typeof result === 'number' ? targets[result] : result;
+ return object.webSocketDebuggerUrl;
+ }
+ default:
+ throw new Error(`Invalid target argument "${this.target}"`);
+ }
+ }
+
+ // fetch the protocol according to 'protocol' and 'local'
+ async _fetchProtocol(options) {
+ // if a protocol has been provided then use it
+ if (this.protocol) {
+ return this.protocol;
+ }
+ // otherwise user either the local or the remote version
+ else {
+ options.local = this.local;
+ return await devtools.Protocol(options);
+ }
+ }
+
+ // establish the WebSocket connection and start processing user commands
+ _connectToWebSocket() {
+ return new Promise((fulfill, reject) => {
+ // create the WebSocket
+ try {
+ if (this.secure) {
+ this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, 'wss:');
+ }
+ this._ws = new WebSocket(this.webSocketUrl, [], {
+ maxPayload: 256 * 1024 * 1024,
+ perMessageDeflate: false,
+ followRedirects: true,
+ });
+ } catch (err) {
+ // handles bad URLs
+ reject(err);
+ return;
+ }
+ // set up event handlers
+ this._ws.on('open', () => {
+ fulfill();
+ });
+ this._ws.on('message', (data) => {
+ const message = JSON.parse(data);
+ this._handleMessage(message);
+ });
+ this._ws.on('close', (code) => {
+ this._handleConnectionClose();
+ this.emit('disconnect');
+ });
+ this._ws.on('error', (err) => {
+ reject(err);
+ });
+ });
+ }
+
+ _handleConnectionClose() {
+ // make sure to complete all the unresolved callbacks
+ const err = new Error('WebSocket connection closed');
+ for (const callback of Object.values(this._callbacks)) {
+ callback(err);
+ }
+ this._callbacks = {};
+ }
+
+ // handle the messages read from the WebSocket
+ _handleMessage(message) {
+ // command response
+ if (message.id) {
+ const callback = this._callbacks[message.id];
+ if (!callback) {
+ return;
+ }
+ // interpret the lack of both 'error' and 'result' as success
+ // (this may happen with node-inspector)
+ if (message.error) {
+ callback(true, message.error);
+ } else {
+ callback(false, message.result || {});
+ }
+ // unregister command response callback
+ delete this._callbacks[message.id];
+ // notify when there are no more pending commands
+ if (Object.keys(this._callbacks).length === 0) {
+ this.emit('ready');
+ }
+ }
+ // event
+ else if (message.method) {
+ const {method, params, sessionId} = message;
+ this.emit('event', message);
+ this.emit(method, params, sessionId);
+ this.emit(`${method}.${sessionId}`, params, sessionId);
+ }
+ }
+
+ // send a command to the remote endpoint and register a callback for the reply
+ _enqueueCommand(method, params, sessionId, callback) {
+ const id = this._nextCommandId++;
+ const message = {
+ id,
+ method,
+ sessionId,
+ params: params || {}
+ };
+ this._ws.send(JSON.stringify(message), (err) => {
+ if (err) {
+ // handle low-level WebSocket errors
+ if (typeof callback === 'function') {
+ callback(err);
+ }
+ } else {
+ this._callbacks[id] = callback;
+ }
+ });
+ }
+}
+
+module.exports = Chrome;
diff --git a/node_modules/chrome-remote-interface/lib/defaults.js b/node_modules/chrome-remote-interface/lib/defaults.js
new file mode 100644
index 0000000..18778f0
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/defaults.js
@@ -0,0 +1,4 @@
+'use strict';
+
+module.exports.HOST = 'localhost';
+module.exports.PORT = 9222;
diff --git a/node_modules/chrome-remote-interface/lib/devtools.js b/node_modules/chrome-remote-interface/lib/devtools.js
new file mode 100644
index 0000000..3975a00
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/devtools.js
@@ -0,0 +1,127 @@
+'use strict';
+
+const http = require('http');
+const https = require('https');
+
+const defaults = require('./defaults.js');
+const externalRequest = require('./external-request.js');
+
+// options.path must be specified; callback(err, data)
+function devToolsInterface(path, options, callback) {
+ const transport = options.secure ? https : http;
+ const requestOptions = {
+ method: options.method,
+ host: options.host || defaults.HOST,
+ port: options.port || defaults.PORT,
+ useHostName: options.useHostName,
+ path: (options.alterPath ? options.alterPath(path) : path)
+ };
+ externalRequest(transport, requestOptions, callback);
+}
+
+// wrapper that allows to return a promise if the callback is omitted, it works
+// for DevTools methods
+function promisesWrapper(func) {
+ return (options, callback) => {
+ // options is an optional argument
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+ options = options || {};
+ // just call the function otherwise wrap a promise around its execution
+ if (typeof callback === 'function') {
+ func(options, callback);
+ return undefined;
+ } else {
+ return new Promise((fulfill, reject) => {
+ func(options, (err, result) => {
+ if (err) {
+ reject(err);
+ } else {
+ fulfill(result);
+ }
+ });
+ });
+ }
+ };
+}
+
+function Protocol(options, callback) {
+ // if the local protocol is requested
+ if (options.local) {
+ const localDescriptor = require('./protocol.json');
+ callback(null, localDescriptor);
+ return;
+ }
+ // try to fetch the protocol remotely
+ devToolsInterface('/json/protocol', options, (err, descriptor) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null, JSON.parse(descriptor));
+ }
+ });
+}
+
+function List(options, callback) {
+ devToolsInterface('/json/list', options, (err, tabs) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null, JSON.parse(tabs));
+ }
+ });
+}
+
+function New(options, callback) {
+ let path = '/json/new';
+ if (Object.prototype.hasOwnProperty.call(options, 'url')) {
+ path += `?${options.url}`;
+ }
+ options.method = options.method || 'PUT'; // see #497
+ devToolsInterface(path, options, (err, tab) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null, JSON.parse(tab));
+ }
+ });
+}
+
+function Activate(options, callback) {
+ devToolsInterface('/json/activate/' + options.id, options, (err) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null);
+ }
+ });
+}
+
+function Close(options, callback) {
+ devToolsInterface('/json/close/' + options.id, options, (err) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null);
+ }
+ });
+}
+
+function Version(options, callback) {
+ devToolsInterface('/json/version', options, (err, versionInfo) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null, JSON.parse(versionInfo));
+ }
+ });
+}
+
+module.exports.Protocol = promisesWrapper(Protocol);
+module.exports.List = promisesWrapper(List);
+module.exports.New = promisesWrapper(New);
+module.exports.Activate = promisesWrapper(Activate);
+module.exports.Close = promisesWrapper(Close);
+module.exports.Version = promisesWrapper(Version);
diff --git a/node_modules/chrome-remote-interface/lib/external-request.js b/node_modules/chrome-remote-interface/lib/external-request.js
new file mode 100644
index 0000000..656446e
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/external-request.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const dns = require('dns');
+const util = require('util');
+
+const REQUEST_TIMEOUT = 10000;
+
+// callback(err, data)
+async function externalRequest(transport, options, callback) {
+ // perform the DNS lookup manually so that the HTTP host header generated by
+ // http.get will contain the IP address, this is needed because since Chrome
+ // 66 the host header cannot contain an host name different than localhost
+ // (see https://github.com/cyrus-and/chrome-remote-interface/issues/340)
+ if (!options.useHostName) {
+ try {
+ const {address} = await util.promisify(dns.lookup)(options.host);
+ options.host = address;
+ } catch (err) {
+ callback(err);
+ return;
+ }
+ }
+ // perform the actual request
+ const request = transport.request(options, (response) => {
+ let data = '';
+ response.on('data', (chunk) => {
+ data += chunk;
+ });
+ response.on('end', () => {
+ if (response.statusCode === 200) {
+ callback(null, data);
+ } else {
+ callback(new Error(data));
+ }
+ });
+ });
+ request.setTimeout(REQUEST_TIMEOUT, () => {
+ request.abort();
+ });
+ request.on('error', callback);
+ request.end();
+}
+
+module.exports = externalRequest;
diff --git a/node_modules/chrome-remote-interface/lib/protocol.json b/node_modules/chrome-remote-interface/lib/protocol.json
new file mode 100644
index 0000000..896a371
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/protocol.json
@@ -0,0 +1,27862 @@
+{
+ "version": {
+ "major": "1",
+ "minor": "3"
+ },
+ "domains": [
+ {
+ "domain": "Accessibility",
+ "experimental": true,
+ "dependencies": [
+ "DOM"
+ ],
+ "types": [
+ {
+ "id": "AXNodeId",
+ "description": "Unique accessibility node identifier.",
+ "type": "string"
+ },
+ {
+ "id": "AXValueType",
+ "description": "Enum of possible property types.",
+ "type": "string",
+ "enum": [
+ "boolean",
+ "tristate",
+ "booleanOrUndefined",
+ "idref",
+ "idrefList",
+ "integer",
+ "node",
+ "nodeList",
+ "number",
+ "string",
+ "computedString",
+ "token",
+ "tokenList",
+ "domRelation",
+ "role",
+ "internalRole",
+ "valueUndefined"
+ ]
+ },
+ {
+ "id": "AXValueSourceType",
+ "description": "Enum of possible property sources.",
+ "type": "string",
+ "enum": [
+ "attribute",
+ "implicit",
+ "style",
+ "contents",
+ "placeholder",
+ "relatedElement"
+ ]
+ },
+ {
+ "id": "AXValueNativeSourceType",
+ "description": "Enum of possible native property sources (as a subtype of a particular AXValueSourceType).",
+ "type": "string",
+ "enum": [
+ "description",
+ "figcaption",
+ "label",
+ "labelfor",
+ "labelwrapped",
+ "legend",
+ "rubyannotation",
+ "tablecaption",
+ "title",
+ "other"
+ ]
+ },
+ {
+ "id": "AXValueSource",
+ "description": "A single source for a computed AX property.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "What type of source this is.",
+ "$ref": "AXValueSourceType"
+ },
+ {
+ "name": "value",
+ "description": "The value of this property source.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "attribute",
+ "description": "The name of the relevant attribute, if any.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "attributeValue",
+ "description": "The value of the relevant attribute, if any.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "superseded",
+ "description": "Whether this source is superseded by a higher priority source.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "nativeSource",
+ "description": "The native markup source for this value, e.g. a `<label>` element.",
+ "optional": true,
+ "$ref": "AXValueNativeSourceType"
+ },
+ {
+ "name": "nativeSourceValue",
+ "description": "The value, such as a node or node list, of the native source.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "invalid",
+ "description": "Whether the value for this property is invalid.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "invalidReason",
+ "description": "Reason for the value being invalid, if it is.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AXRelatedNode",
+ "type": "object",
+ "properties": [
+ {
+ "name": "backendDOMNodeId",
+ "description": "The BackendNodeId of the related DOM node.",
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "idref",
+ "description": "The IDRef value provided, if any.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "text",
+ "description": "The text alternative of this node in the current context.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AXProperty",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "The name of this property.",
+ "$ref": "AXPropertyName"
+ },
+ {
+ "name": "value",
+ "description": "The value of this property.",
+ "$ref": "AXValue"
+ }
+ ]
+ },
+ {
+ "id": "AXValue",
+ "description": "A single computed AX property.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "The type of this value.",
+ "$ref": "AXValueType"
+ },
+ {
+ "name": "value",
+ "description": "The computed value of this property.",
+ "optional": true,
+ "type": "any"
+ },
+ {
+ "name": "relatedNodes",
+ "description": "One or more related nodes, if applicable.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AXRelatedNode"
+ }
+ },
+ {
+ "name": "sources",
+ "description": "The sources which contributed to the computation of this property.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AXValueSource"
+ }
+ }
+ ]
+ },
+ {
+ "id": "AXPropertyName",
+ "description": "Values of AXProperty name:\n- from 'busy' to 'roledescription': states which apply to every AX node\n- from 'live' to 'root': attributes which apply to nodes in live regions\n- from 'autocomplete' to 'valuetext': attributes which apply to widgets\n- from 'checked' to 'selected': states which apply to widgets\n- from 'activedescendant' to 'owns' - relationships between elements other than parent/child/sibling.",
+ "type": "string",
+ "enum": [
+ "busy",
+ "disabled",
+ "editable",
+ "focusable",
+ "focused",
+ "hidden",
+ "hiddenRoot",
+ "invalid",
+ "keyshortcuts",
+ "settable",
+ "roledescription",
+ "live",
+ "atomic",
+ "relevant",
+ "root",
+ "autocomplete",
+ "hasPopup",
+ "level",
+ "multiselectable",
+ "orientation",
+ "multiline",
+ "readonly",
+ "required",
+ "valuemin",
+ "valuemax",
+ "valuetext",
+ "checked",
+ "expanded",
+ "modal",
+ "pressed",
+ "selected",
+ "activedescendant",
+ "controls",
+ "describedby",
+ "details",
+ "errormessage",
+ "flowto",
+ "labelledby",
+ "owns"
+ ]
+ },
+ {
+ "id": "AXNode",
+ "description": "A node in the accessibility tree.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeId",
+ "description": "Unique identifier for this node.",
+ "$ref": "AXNodeId"
+ },
+ {
+ "name": "ignored",
+ "description": "Whether this node is ignored for accessibility",
+ "type": "boolean"
+ },
+ {
+ "name": "ignoredReasons",
+ "description": "Collection of reasons why this node is hidden.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AXProperty"
+ }
+ },
+ {
+ "name": "role",
+ "description": "This `Node`'s role, whether explicit or implicit.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "chromeRole",
+ "description": "This `Node`'s Chrome raw role.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "name",
+ "description": "The accessible name for this `Node`.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "description",
+ "description": "The accessible description for this `Node`.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "value",
+ "description": "The value for this `Node`.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "properties",
+ "description": "All other properties",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AXProperty"
+ }
+ },
+ {
+ "name": "parentId",
+ "description": "ID for this node's parent.",
+ "optional": true,
+ "$ref": "AXNodeId"
+ },
+ {
+ "name": "childIds",
+ "description": "IDs for each of this node's child nodes.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AXNodeId"
+ }
+ },
+ {
+ "name": "backendDOMNodeId",
+ "description": "The backend ID for the associated DOM node, if any.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "frameId",
+ "description": "The frame ID for the frame associated with this nodes document.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables the accessibility domain."
+ },
+ {
+ "name": "enable",
+ "description": "Enables the accessibility domain which causes `AXNodeId`s to remain consistent between method calls.\nThis turns on accessibility for the page, which can impact performance until accessibility is disabled."
+ },
+ {
+ "name": "getPartialAXTree",
+ "description": "Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to get the partial accessibility tree for.",
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node to get the partial accessibility tree for.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper to get the partial accessibility tree for.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "fetchRelatives",
+ "description": "Whether to fetch this node's ancestors, siblings and children. Defaults to true.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "description": "The `Accessibility.AXNode` for this DOM node, if it exists, plus its ancestors, siblings and\nchildren, if requested.",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getFullAXTree",
+ "description": "Fetches the entire accessibility tree for the root Document",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "depth",
+ "description": "The maximum depth at which descendants of the root node should be retrieved.\nIf omitted, the full tree is returned.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "frameId",
+ "description": "The frame for whose document the AX tree should be retrieved.\nIf omited, the root frame is used.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getRootAXNode",
+ "description": "Fetches the root node.\nRequires `enable()` to have been called previously.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "The frame in whose document the node resides.\nIf omitted, the root frame is used.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "node",
+ "$ref": "AXNode"
+ }
+ ]
+ },
+ {
+ "name": "getAXNodeAndAncestors",
+ "description": "Fetches a node and all ancestors up to and including the root.\nRequires `enable()` to have been called previously.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to get.",
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node to get.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper to get.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getChildAXNodes",
+ "description": "Fetches a particular accessibility node by AXNodeId.\nRequires `enable()` to have been called previously.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "AXNodeId"
+ },
+ {
+ "name": "frameId",
+ "description": "The frame in whose document the node resides.\nIf omitted, the root frame is used.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ },
+ {
+ "name": "queryAXTree",
+ "description": "Query a DOM node's accessibility subtree for accessible name and role.\nThis command computes the name and role for all nodes in the subtree, including those that are\nignored for accessibility, and returns those that mactch the specified name and role. If no DOM\nnode is specified, or the DOM node does not exist, the command returns an error. If neither\n`accessibleName` or `role` is specified, it returns all the accessibility nodes in the subtree.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node for the root to query.",
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node for the root to query.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper for the root to query.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "accessibleName",
+ "description": "Find nodes with this computed name.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "role",
+ "description": "Find nodes with this computed role.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "description": "A list of `Accessibility.AXNode` matching the specified attributes,\nincluding nodes that are ignored for accessibility.",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "loadComplete",
+ "description": "The loadComplete event mirrors the load complete event sent by the browser to assistive\ntechnology when the web page has finished loading.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "root",
+ "description": "New document root node.",
+ "$ref": "AXNode"
+ }
+ ]
+ },
+ {
+ "name": "nodesUpdated",
+ "description": "The nodesUpdated event is sent every time a previously requested node has changed the in tree.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodes",
+ "description": "Updated node data.",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Animation",
+ "experimental": true,
+ "dependencies": [
+ "Runtime",
+ "DOM"
+ ],
+ "types": [
+ {
+ "id": "Animation",
+ "description": "Animation instance.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "`Animation`'s id.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "`Animation`'s name.",
+ "type": "string"
+ },
+ {
+ "name": "pausedState",
+ "description": "`Animation`'s internal paused state.",
+ "type": "boolean"
+ },
+ {
+ "name": "playState",
+ "description": "`Animation`'s play state.",
+ "type": "string"
+ },
+ {
+ "name": "playbackRate",
+ "description": "`Animation`'s playback rate.",
+ "type": "number"
+ },
+ {
+ "name": "startTime",
+ "description": "`Animation`'s start time.",
+ "type": "number"
+ },
+ {
+ "name": "currentTime",
+ "description": "`Animation`'s current time.",
+ "type": "number"
+ },
+ {
+ "name": "type",
+ "description": "Animation type of `Animation`.",
+ "type": "string",
+ "enum": [
+ "CSSTransition",
+ "CSSAnimation",
+ "WebAnimation"
+ ]
+ },
+ {
+ "name": "source",
+ "description": "`Animation`'s source animation node.",
+ "optional": true,
+ "$ref": "AnimationEffect"
+ },
+ {
+ "name": "cssId",
+ "description": "A unique ID for `Animation` representing the sources that triggered this CSS\nanimation/transition.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AnimationEffect",
+ "description": "AnimationEffect instance",
+ "type": "object",
+ "properties": [
+ {
+ "name": "delay",
+ "description": "`AnimationEffect`'s delay.",
+ "type": "number"
+ },
+ {
+ "name": "endDelay",
+ "description": "`AnimationEffect`'s end delay.",
+ "type": "number"
+ },
+ {
+ "name": "iterationStart",
+ "description": "`AnimationEffect`'s iteration start.",
+ "type": "number"
+ },
+ {
+ "name": "iterations",
+ "description": "`AnimationEffect`'s iterations.",
+ "type": "number"
+ },
+ {
+ "name": "duration",
+ "description": "`AnimationEffect`'s iteration duration.",
+ "type": "number"
+ },
+ {
+ "name": "direction",
+ "description": "`AnimationEffect`'s playback direction.",
+ "type": "string"
+ },
+ {
+ "name": "fill",
+ "description": "`AnimationEffect`'s fill mode.",
+ "type": "string"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "`AnimationEffect`'s target node.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "keyframesRule",
+ "description": "`AnimationEffect`'s keyframes.",
+ "optional": true,
+ "$ref": "KeyframesRule"
+ },
+ {
+ "name": "easing",
+ "description": "`AnimationEffect`'s timing function.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "KeyframesRule",
+ "description": "Keyframes Rule",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "CSS keyframed animation's name.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "keyframes",
+ "description": "List of animation keyframes.",
+ "type": "array",
+ "items": {
+ "$ref": "KeyframeStyle"
+ }
+ }
+ ]
+ },
+ {
+ "id": "KeyframeStyle",
+ "description": "Keyframe Style",
+ "type": "object",
+ "properties": [
+ {
+ "name": "offset",
+ "description": "Keyframe's time offset.",
+ "type": "string"
+ },
+ {
+ "name": "easing",
+ "description": "`AnimationEffect`'s timing function.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables animation domain notifications."
+ },
+ {
+ "name": "enable",
+ "description": "Enables animation domain notifications."
+ },
+ {
+ "name": "getCurrentTime",
+ "description": "Returns the current time of the an animation.",
+ "parameters": [
+ {
+ "name": "id",
+ "description": "Id of animation.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "currentTime",
+ "description": "Current time of the page.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "getPlaybackRate",
+ "description": "Gets the playback rate of the document timeline.",
+ "returns": [
+ {
+ "name": "playbackRate",
+ "description": "Playback rate for animations on page.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "releaseAnimations",
+ "description": "Releases a set of animations to no longer be manipulated.",
+ "parameters": [
+ {
+ "name": "animations",
+ "description": "List of animation ids to seek.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "resolveAnimation",
+ "description": "Gets the remote object of the Animation.",
+ "parameters": [
+ {
+ "name": "animationId",
+ "description": "Animation id.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "remoteObject",
+ "description": "Corresponding remote object.",
+ "$ref": "Runtime.RemoteObject"
+ }
+ ]
+ },
+ {
+ "name": "seekAnimations",
+ "description": "Seek a set of animations to a particular time within each animation.",
+ "parameters": [
+ {
+ "name": "animations",
+ "description": "List of animation ids to seek.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "currentTime",
+ "description": "Set the current time of each animation.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setPaused",
+ "description": "Sets the paused state of a set of animations.",
+ "parameters": [
+ {
+ "name": "animations",
+ "description": "Animations to set the pause state of.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "paused",
+ "description": "Paused state to set to.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setPlaybackRate",
+ "description": "Sets the playback rate of the document timeline.",
+ "parameters": [
+ {
+ "name": "playbackRate",
+ "description": "Playback rate for animations on page",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setTiming",
+ "description": "Sets the timing of an animation node.",
+ "parameters": [
+ {
+ "name": "animationId",
+ "description": "Animation id.",
+ "type": "string"
+ },
+ {
+ "name": "duration",
+ "description": "Duration of the animation.",
+ "type": "number"
+ },
+ {
+ "name": "delay",
+ "description": "Delay of the animation.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "animationCanceled",
+ "description": "Event for when an animation has been cancelled.",
+ "parameters": [
+ {
+ "name": "id",
+ "description": "Id of the animation that was cancelled.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "animationCreated",
+ "description": "Event for each animation that has been created.",
+ "parameters": [
+ {
+ "name": "id",
+ "description": "Id of the animation that was created.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "animationStarted",
+ "description": "Event for animation that has been started.",
+ "parameters": [
+ {
+ "name": "animation",
+ "description": "Animation that was started.",
+ "$ref": "Animation"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Audits",
+ "description": "Audits domain allows investigation of page violations and possible improvements.",
+ "experimental": true,
+ "dependencies": [
+ "Network"
+ ],
+ "types": [
+ {
+ "id": "AffectedCookie",
+ "description": "Information about a cookie that is affected by an inspector issue.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "The following three properties uniquely identify a cookie",
+ "type": "string"
+ },
+ {
+ "name": "path",
+ "type": "string"
+ },
+ {
+ "name": "domain",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AffectedRequest",
+ "description": "Information about a request that is affected by an inspector issue.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "requestId",
+ "description": "The unique request id.",
+ "$ref": "Network.RequestId"
+ },
+ {
+ "name": "url",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AffectedFrame",
+ "description": "Information about the frame affected by an inspector issue.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "frameId",
+ "$ref": "Page.FrameId"
+ }
+ ]
+ },
+ {
+ "id": "CookieExclusionReason",
+ "type": "string",
+ "enum": [
+ "ExcludeSameSiteUnspecifiedTreatedAsLax",
+ "ExcludeSameSiteNoneInsecure",
+ "ExcludeSameSiteLax",
+ "ExcludeSameSiteStrict",
+ "ExcludeInvalidSameParty",
+ "ExcludeSamePartyCrossPartyContext",
+ "ExcludeDomainNonASCII",
+ "ExcludeThirdPartyCookieBlockedInFirstPartySet"
+ ]
+ },
+ {
+ "id": "CookieWarningReason",
+ "type": "string",
+ "enum": [
+ "WarnSameSiteUnspecifiedCrossSiteContext",
+ "WarnSameSiteNoneInsecure",
+ "WarnSameSiteUnspecifiedLaxAllowUnsafe",
+ "WarnSameSiteStrictLaxDowngradeStrict",
+ "WarnSameSiteStrictCrossDowngradeStrict",
+ "WarnSameSiteStrictCrossDowngradeLax",
+ "WarnSameSiteLaxCrossDowngradeStrict",
+ "WarnSameSiteLaxCrossDowngradeLax",
+ "WarnAttributeValueExceedsMaxSize",
+ "WarnDomainNonASCII",
+ "WarnThirdPartyPhaseout"
+ ]
+ },
+ {
+ "id": "CookieOperation",
+ "type": "string",
+ "enum": [
+ "SetCookie",
+ "ReadCookie"
+ ]
+ },
+ {
+ "id": "CookieIssueDetails",
+ "description": "This information is currently necessary, as the front-end has a difficult\ntime finding a specific cookie. With this, we can convey specific error\ninformation without the cookie.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "cookie",
+ "description": "If AffectedCookie is not set then rawCookieLine contains the raw\nSet-Cookie header string. This hints at a problem where the\ncookie line is syntactically or semantically malformed in a way\nthat no valid cookie could be created.",
+ "optional": true,
+ "$ref": "AffectedCookie"
+ },
+ {
+ "name": "rawCookieLine",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cookieWarningReasons",
+ "type": "array",
+ "items": {
+ "$ref": "CookieWarningReason"
+ }
+ },
+ {
+ "name": "cookieExclusionReasons",
+ "type": "array",
+ "items": {
+ "$ref": "CookieExclusionReason"
+ }
+ },
+ {
+ "name": "operation",
+ "description": "Optionally identifies the site-for-cookies and the cookie url, which\nmay be used by the front-end as additional context.",
+ "$ref": "CookieOperation"
+ },
+ {
+ "name": "siteForCookies",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cookieUrl",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "request",
+ "optional": true,
+ "$ref": "AffectedRequest"
+ }
+ ]
+ },
+ {
+ "id": "MixedContentResolutionStatus",
+ "type": "string",
+ "enum": [
+ "MixedContentBlocked",
+ "MixedContentAutomaticallyUpgraded",
+ "MixedContentWarning"
+ ]
+ },
+ {
+ "id": "MixedContentResourceType",
+ "type": "string",
+ "enum": [
+ "AttributionSrc",
+ "Audio",
+ "Beacon",
+ "CSPReport",
+ "Download",
+ "EventSource",
+ "Favicon",
+ "Font",
+ "Form",
+ "Frame",
+ "Image",
+ "Import",
+ "Manifest",
+ "Ping",
+ "PluginData",
+ "PluginResource",
+ "Prefetch",
+ "Resource",
+ "Script",
+ "ServiceWorker",
+ "SharedWorker",
+ "Stylesheet",
+ "Track",
+ "Video",
+ "Worker",
+ "XMLHttpRequest",
+ "XSLT"
+ ]
+ },
+ {
+ "id": "MixedContentIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "resourceType",
+ "description": "The type of resource causing the mixed content issue (css, js, iframe,\nform,...). Marked as optional because it is mapped to from\nblink::mojom::RequestContextType, which will be replaced\nby network::mojom::RequestDestination",
+ "optional": true,
+ "$ref": "MixedContentResourceType"
+ },
+ {
+ "name": "resolutionStatus",
+ "description": "The way the mixed content issue is being resolved.",
+ "$ref": "MixedContentResolutionStatus"
+ },
+ {
+ "name": "insecureURL",
+ "description": "The unsafe http url causing the mixed content issue.",
+ "type": "string"
+ },
+ {
+ "name": "mainResourceURL",
+ "description": "The url responsible for the call to an unsafe url.",
+ "type": "string"
+ },
+ {
+ "name": "request",
+ "description": "The mixed content request.\nDoes not always exist (e.g. for unsafe form submission urls).",
+ "optional": true,
+ "$ref": "AffectedRequest"
+ },
+ {
+ "name": "frame",
+ "description": "Optional because not every mixed content issue is necessarily linked to a frame.",
+ "optional": true,
+ "$ref": "AffectedFrame"
+ }
+ ]
+ },
+ {
+ "id": "BlockedByResponseReason",
+ "description": "Enum indicating the reason a response has been blocked. These reasons are\nrefinements of the net error BLOCKED_BY_RESPONSE.",
+ "type": "string",
+ "enum": [
+ "CoepFrameResourceNeedsCoepHeader",
+ "CoopSandboxedIFrameCannotNavigateToCoopPage",
+ "CorpNotSameOrigin",
+ "CorpNotSameOriginAfterDefaultedToSameOriginByCoep",
+ "CorpNotSameSite"
+ ]
+ },
+ {
+ "id": "BlockedByResponseIssueDetails",
+ "description": "Details for a request that has been blocked with the BLOCKED_BY_RESPONSE\ncode. Currently only used for COEP/COOP, but may be extended to include\nsome CSP errors in the future.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "request",
+ "$ref": "AffectedRequest"
+ },
+ {
+ "name": "parentFrame",
+ "optional": true,
+ "$ref": "AffectedFrame"
+ },
+ {
+ "name": "blockedFrame",
+ "optional": true,
+ "$ref": "AffectedFrame"
+ },
+ {
+ "name": "reason",
+ "$ref": "BlockedByResponseReason"
+ }
+ ]
+ },
+ {
+ "id": "HeavyAdResolutionStatus",
+ "type": "string",
+ "enum": [
+ "HeavyAdBlocked",
+ "HeavyAdWarning"
+ ]
+ },
+ {
+ "id": "HeavyAdReason",
+ "type": "string",
+ "enum": [
+ "NetworkTotalLimit",
+ "CpuTotalLimit",
+ "CpuPeakLimit"
+ ]
+ },
+ {
+ "id": "HeavyAdIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "resolution",
+ "description": "The resolution status, either blocking the content or warning.",
+ "$ref": "HeavyAdResolutionStatus"
+ },
+ {
+ "name": "reason",
+ "description": "The reason the ad was blocked, total network or cpu or peak cpu.",
+ "$ref": "HeavyAdReason"
+ },
+ {
+ "name": "frame",
+ "description": "The frame that was blocked.",
+ "$ref": "AffectedFrame"
+ }
+ ]
+ },
+ {
+ "id": "ContentSecurityPolicyViolationType",
+ "type": "string",
+ "enum": [
+ "kInlineViolation",
+ "kEvalViolation",
+ "kURLViolation",
+ "kTrustedTypesSinkViolation",
+ "kTrustedTypesPolicyViolation",
+ "kWasmEvalViolation"
+ ]
+ },
+ {
+ "id": "SourceCodeLocation",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "optional": true,
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "ContentSecurityPolicyIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "blockedURL",
+ "description": "The url not included in allowed sources.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "violatedDirective",
+ "description": "Specific directive that is violated, causing the CSP issue.",
+ "type": "string"
+ },
+ {
+ "name": "isReportOnly",
+ "type": "boolean"
+ },
+ {
+ "name": "contentSecurityPolicyViolationType",
+ "$ref": "ContentSecurityPolicyViolationType"
+ },
+ {
+ "name": "frameAncestor",
+ "optional": true,
+ "$ref": "AffectedFrame"
+ },
+ {
+ "name": "sourceCodeLocation",
+ "optional": true,
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "violatingNodeId",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ },
+ {
+ "id": "SharedArrayBufferIssueType",
+ "type": "string",
+ "enum": [
+ "TransferIssue",
+ "CreationIssue"
+ ]
+ },
+ {
+ "id": "SharedArrayBufferIssueDetails",
+ "description": "Details for a issue arising from an SAB being instantiated in, or\ntransferred to a context that is not cross-origin isolated.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "sourceCodeLocation",
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "isWarning",
+ "type": "boolean"
+ },
+ {
+ "name": "type",
+ "$ref": "SharedArrayBufferIssueType"
+ }
+ ]
+ },
+ {
+ "id": "LowTextContrastIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "violatingNodeId",
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "violatingNodeSelector",
+ "type": "string"
+ },
+ {
+ "name": "contrastRatio",
+ "type": "number"
+ },
+ {
+ "name": "thresholdAA",
+ "type": "number"
+ },
+ {
+ "name": "thresholdAAA",
+ "type": "number"
+ },
+ {
+ "name": "fontSize",
+ "type": "string"
+ },
+ {
+ "name": "fontWeight",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CorsIssueDetails",
+ "description": "Details for a CORS related issue, e.g. a warning or error related to\nCORS RFC1918 enforcement.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "corsErrorStatus",
+ "$ref": "Network.CorsErrorStatus"
+ },
+ {
+ "name": "isWarning",
+ "type": "boolean"
+ },
+ {
+ "name": "request",
+ "$ref": "AffectedRequest"
+ },
+ {
+ "name": "location",
+ "optional": true,
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "initiatorOrigin",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "resourceIPAddressSpace",
+ "optional": true,
+ "$ref": "Network.IPAddressSpace"
+ },
+ {
+ "name": "clientSecurityState",
+ "optional": true,
+ "$ref": "Network.ClientSecurityState"
+ }
+ ]
+ },
+ {
+ "id": "AttributionReportingIssueType",
+ "type": "string",
+ "enum": [
+ "PermissionPolicyDisabled",
+ "UntrustworthyReportingOrigin",
+ "InsecureContext",
+ "InvalidHeader",
+ "InvalidRegisterTriggerHeader",
+ "SourceAndTriggerHeaders",
+ "SourceIgnored",
+ "TriggerIgnored",
+ "OsSourceIgnored",
+ "OsTriggerIgnored",
+ "InvalidRegisterOsSourceHeader",
+ "InvalidRegisterOsTriggerHeader",
+ "WebAndOsHeaders",
+ "NoWebOrOsSupport",
+ "NavigationRegistrationWithoutTransientUserActivation"
+ ]
+ },
+ {
+ "id": "AttributionReportingIssueDetails",
+ "description": "Details for issues around \"Attribution Reporting API\" usage.\nExplainer: https://github.com/WICG/attribution-reporting-api",
+ "type": "object",
+ "properties": [
+ {
+ "name": "violationType",
+ "$ref": "AttributionReportingIssueType"
+ },
+ {
+ "name": "request",
+ "optional": true,
+ "$ref": "AffectedRequest"
+ },
+ {
+ "name": "violatingNodeId",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "invalidParameter",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "QuirksModeIssueDetails",
+ "description": "Details for issues about documents in Quirks Mode\nor Limited Quirks Mode that affects page layouting.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "isLimitedQuirksMode",
+ "description": "If false, it means the document's mode is \"quirks\"\ninstead of \"limited-quirks\".",
+ "type": "boolean"
+ },
+ {
+ "name": "documentNodeId",
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "frameId",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "loaderId",
+ "$ref": "Network.LoaderId"
+ }
+ ]
+ },
+ {
+ "id": "NavigatorUserAgentIssueDetails",
+ "deprecated": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "location",
+ "optional": true,
+ "$ref": "SourceCodeLocation"
+ }
+ ]
+ },
+ {
+ "id": "GenericIssueErrorType",
+ "type": "string",
+ "enum": [
+ "CrossOriginPortalPostMessageError",
+ "FormLabelForNameError",
+ "FormDuplicateIdForInputError",
+ "FormInputWithNoLabelError",
+ "FormAutocompleteAttributeEmptyError",
+ "FormEmptyIdAndNameAttributesForInputError",
+ "FormAriaLabelledByToNonExistingId",
+ "FormInputAssignedAutocompleteValueToIdOrNameAttributeError",
+ "FormLabelHasNeitherForNorNestedInput",
+ "FormLabelForMatchesNonExistingIdError",
+ "FormInputHasWrongButWellIntendedAutocompleteValueError",
+ "ResponseWasBlockedByORB"
+ ]
+ },
+ {
+ "id": "GenericIssueDetails",
+ "description": "Depending on the concrete errorType, different properties are set.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "errorType",
+ "description": "Issues with the same errorType are aggregated in the frontend.",
+ "$ref": "GenericIssueErrorType"
+ },
+ {
+ "name": "frameId",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "violatingNodeId",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "violatingNodeAttribute",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "request",
+ "optional": true,
+ "$ref": "AffectedRequest"
+ }
+ ]
+ },
+ {
+ "id": "DeprecationIssueDetails",
+ "description": "This issue tracks information needed to print a deprecation message.\nhttps://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/frame/third_party/blink/renderer/core/frame/deprecation/README.md",
+ "type": "object",
+ "properties": [
+ {
+ "name": "affectedFrame",
+ "optional": true,
+ "$ref": "AffectedFrame"
+ },
+ {
+ "name": "sourceCodeLocation",
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "type",
+ "description": "One of the deprecation names from third_party/blink/renderer/core/frame/deprecation/deprecation.json5",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "BounceTrackingIssueDetails",
+ "description": "This issue warns about sites in the redirect chain of a finished navigation\nthat may be flagged as trackers and have their state cleared if they don't\nreceive a user interaction. Note that in this context 'site' means eTLD+1.\nFor example, if the URL `https://example.test:80/bounce` was in the\nredirect chain, the site reported would be `example.test`.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "trackingSites",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ClientHintIssueReason",
+ "type": "string",
+ "enum": [
+ "MetaTagAllowListInvalidOrigin",
+ "MetaTagModifiedHTML"
+ ]
+ },
+ {
+ "id": "FederatedAuthRequestIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "federatedAuthRequestIssueReason",
+ "$ref": "FederatedAuthRequestIssueReason"
+ }
+ ]
+ },
+ {
+ "id": "FederatedAuthRequestIssueReason",
+ "description": "Represents the failure reason when a federated authentication reason fails.\nShould be updated alongside RequestIdTokenStatus in\nthird_party/blink/public/mojom/devtools/inspector_issue.mojom to include\nall cases except for success.",
+ "type": "string",
+ "enum": [
+ "ShouldEmbargo",
+ "TooManyRequests",
+ "WellKnownHttpNotFound",
+ "WellKnownNoResponse",
+ "WellKnownInvalidResponse",
+ "WellKnownListEmpty",
+ "WellKnownInvalidContentType",
+ "ConfigNotInWellKnown",
+ "WellKnownTooBig",
+ "ConfigHttpNotFound",
+ "ConfigNoResponse",
+ "ConfigInvalidResponse",
+ "ConfigInvalidContentType",
+ "ClientMetadataHttpNotFound",
+ "ClientMetadataNoResponse",
+ "ClientMetadataInvalidResponse",
+ "ClientMetadataInvalidContentType",
+ "DisabledInSettings",
+ "ErrorFetchingSignin",
+ "InvalidSigninResponse",
+ "AccountsHttpNotFound",
+ "AccountsNoResponse",
+ "AccountsInvalidResponse",
+ "AccountsListEmpty",
+ "AccountsInvalidContentType",
+ "IdTokenHttpNotFound",
+ "IdTokenNoResponse",
+ "IdTokenInvalidResponse",
+ "IdTokenInvalidRequest",
+ "IdTokenInvalidContentType",
+ "ErrorIdToken",
+ "Canceled",
+ "RpPageNotVisible",
+ "SilentMediationFailure",
+ "ThirdPartyCookiesBlocked"
+ ]
+ },
+ {
+ "id": "FederatedAuthUserInfoRequestIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "federatedAuthUserInfoRequestIssueReason",
+ "$ref": "FederatedAuthUserInfoRequestIssueReason"
+ }
+ ]
+ },
+ {
+ "id": "FederatedAuthUserInfoRequestIssueReason",
+ "description": "Represents the failure reason when a getUserInfo() call fails.\nShould be updated alongside FederatedAuthUserInfoRequestResult in\nthird_party/blink/public/mojom/devtools/inspector_issue.mojom.",
+ "type": "string",
+ "enum": [
+ "NotSameOrigin",
+ "NotIframe",
+ "NotPotentiallyTrustworthy",
+ "NoApiPermission",
+ "NotSignedInWithIdp",
+ "NoAccountSharingPermission",
+ "InvalidConfigOrWellKnown",
+ "InvalidAccountsResponse",
+ "NoReturningUserFromFetchedAccounts"
+ ]
+ },
+ {
+ "id": "ClientHintIssueDetails",
+ "description": "This issue tracks client hints related issues. It's used to deprecate old\nfeatures, encourage the use of new ones, and provide general guidance.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "sourceCodeLocation",
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "clientHintIssueReason",
+ "$ref": "ClientHintIssueReason"
+ }
+ ]
+ },
+ {
+ "id": "FailedRequestInfo",
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "The URL that failed to load.",
+ "type": "string"
+ },
+ {
+ "name": "failureMessage",
+ "description": "The failure message for the failed request.",
+ "type": "string"
+ },
+ {
+ "name": "requestId",
+ "optional": true,
+ "$ref": "Network.RequestId"
+ }
+ ]
+ },
+ {
+ "id": "StyleSheetLoadingIssueReason",
+ "type": "string",
+ "enum": [
+ "LateImportRule",
+ "RequestFailed"
+ ]
+ },
+ {
+ "id": "StylesheetLoadingIssueDetails",
+ "description": "This issue warns when a referenced stylesheet couldn't be loaded.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "sourceCodeLocation",
+ "description": "Source code position that referenced the failing stylesheet.",
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "styleSheetLoadingIssueReason",
+ "description": "Reason why the stylesheet couldn't be loaded.",
+ "$ref": "StyleSheetLoadingIssueReason"
+ },
+ {
+ "name": "failedRequestInfo",
+ "description": "Contains additional info when the failure was due to a request.",
+ "optional": true,
+ "$ref": "FailedRequestInfo"
+ }
+ ]
+ },
+ {
+ "id": "InspectorIssueCode",
+ "description": "A unique identifier for the type of issue. Each type may use one of the\noptional fields in InspectorIssueDetails to convey more specific\ninformation about the kind of issue.",
+ "type": "string",
+ "enum": [
+ "CookieIssue",
+ "MixedContentIssue",
+ "BlockedByResponseIssue",
+ "HeavyAdIssue",
+ "ContentSecurityPolicyIssue",
+ "SharedArrayBufferIssue",
+ "LowTextContrastIssue",
+ "CorsIssue",
+ "AttributionReportingIssue",
+ "QuirksModeIssue",
+ "NavigatorUserAgentIssue",
+ "GenericIssue",
+ "DeprecationIssue",
+ "ClientHintIssue",
+ "FederatedAuthRequestIssue",
+ "BounceTrackingIssue",
+ "StylesheetLoadingIssue",
+ "FederatedAuthUserInfoRequestIssue"
+ ]
+ },
+ {
+ "id": "InspectorIssueDetails",
+ "description": "This struct holds a list of optional fields with additional information\nspecific to the kind of issue. When adding a new issue code, please also\nadd a new optional field to this type.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "cookieIssueDetails",
+ "optional": true,
+ "$ref": "CookieIssueDetails"
+ },
+ {
+ "name": "mixedContentIssueDetails",
+ "optional": true,
+ "$ref": "MixedContentIssueDetails"
+ },
+ {
+ "name": "blockedByResponseIssueDetails",
+ "optional": true,
+ "$ref": "BlockedByResponseIssueDetails"
+ },
+ {
+ "name": "heavyAdIssueDetails",
+ "optional": true,
+ "$ref": "HeavyAdIssueDetails"
+ },
+ {
+ "name": "contentSecurityPolicyIssueDetails",
+ "optional": true,
+ "$ref": "ContentSecurityPolicyIssueDetails"
+ },
+ {
+ "name": "sharedArrayBufferIssueDetails",
+ "optional": true,
+ "$ref": "SharedArrayBufferIssueDetails"
+ },
+ {
+ "name": "lowTextContrastIssueDetails",
+ "optional": true,
+ "$ref": "LowTextContrastIssueDetails"
+ },
+ {
+ "name": "corsIssueDetails",
+ "optional": true,
+ "$ref": "CorsIssueDetails"
+ },
+ {
+ "name": "attributionReportingIssueDetails",
+ "optional": true,
+ "$ref": "AttributionReportingIssueDetails"
+ },
+ {
+ "name": "quirksModeIssueDetails",
+ "optional": true,
+ "$ref": "QuirksModeIssueDetails"
+ },
+ {
+ "name": "navigatorUserAgentIssueDetails",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "NavigatorUserAgentIssueDetails"
+ },
+ {
+ "name": "genericIssueDetails",
+ "optional": true,
+ "$ref": "GenericIssueDetails"
+ },
+ {
+ "name": "deprecationIssueDetails",
+ "optional": true,
+ "$ref": "DeprecationIssueDetails"
+ },
+ {
+ "name": "clientHintIssueDetails",
+ "optional": true,
+ "$ref": "ClientHintIssueDetails"
+ },
+ {
+ "name": "federatedAuthRequestIssueDetails",
+ "optional": true,
+ "$ref": "FederatedAuthRequestIssueDetails"
+ },
+ {
+ "name": "bounceTrackingIssueDetails",
+ "optional": true,
+ "$ref": "BounceTrackingIssueDetails"
+ },
+ {
+ "name": "stylesheetLoadingIssueDetails",
+ "optional": true,
+ "$ref": "StylesheetLoadingIssueDetails"
+ },
+ {
+ "name": "federatedAuthUserInfoRequestIssueDetails",
+ "optional": true,
+ "$ref": "FederatedAuthUserInfoRequestIssueDetails"
+ }
+ ]
+ },
+ {
+ "id": "IssueId",
+ "description": "A unique id for a DevTools inspector issue. Allows other entities (e.g.\nexceptions, CDP message, console messages, etc.) to reference an issue.",
+ "type": "string"
+ },
+ {
+ "id": "InspectorIssue",
+ "description": "An inspector issue reported from the back-end.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "code",
+ "$ref": "InspectorIssueCode"
+ },
+ {
+ "name": "details",
+ "$ref": "InspectorIssueDetails"
+ },
+ {
+ "name": "issueId",
+ "description": "A unique id for this issue. May be omitted if no other entity (e.g.\nexception, CDP message, etc.) is referencing this issue.",
+ "optional": true,
+ "$ref": "IssueId"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getEncodedResponse",
+ "description": "Returns the response body and size if it were re-encoded with the specified settings. Only\napplies to images.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier of the network request to get content for.",
+ "$ref": "Network.RequestId"
+ },
+ {
+ "name": "encoding",
+ "description": "The encoding to use.",
+ "type": "string",
+ "enum": [
+ "webp",
+ "jpeg",
+ "png"
+ ]
+ },
+ {
+ "name": "quality",
+ "description": "The quality of the encoding (0-1). (defaults to 1)",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "sizeOnly",
+ "description": "Whether to only return the size information (defaults to false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "body",
+ "description": "The encoded body as a base64 string. Omitted if sizeOnly is true. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "originalSize",
+ "description": "Size before re-encoding.",
+ "type": "integer"
+ },
+ {
+ "name": "encodedSize",
+ "description": "Size after re-encoding.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables issues domain, prevents further issues from being reported to the client."
+ },
+ {
+ "name": "enable",
+ "description": "Enables issues domain, sends the issues collected so far to the client by means of the\n`issueAdded` event."
+ },
+ {
+ "name": "checkContrast",
+ "description": "Runs the contrast check for the target page. Found issues are reported\nusing Audits.issueAdded event.",
+ "parameters": [
+ {
+ "name": "reportAAA",
+ "description": "Whether to report WCAG AAA level issues. Default is false.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "checkFormsIssues",
+ "description": "Runs the form issues check for the target page. Found issues are reported\nusing Audits.issueAdded event.",
+ "returns": [
+ {
+ "name": "formIssues",
+ "type": "array",
+ "items": {
+ "$ref": "GenericIssueDetails"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "issueAdded",
+ "parameters": [
+ {
+ "name": "issue",
+ "$ref": "InspectorIssue"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Autofill",
+ "description": "Defines commands and events for Autofill.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "CreditCard",
+ "type": "object",
+ "properties": [
+ {
+ "name": "number",
+ "description": "16-digit credit card number.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Name of the credit card owner.",
+ "type": "string"
+ },
+ {
+ "name": "expiryMonth",
+ "description": "2-digit expiry month.",
+ "type": "string"
+ },
+ {
+ "name": "expiryYear",
+ "description": "4-digit expiry year.",
+ "type": "string"
+ },
+ {
+ "name": "cvc",
+ "description": "3-digit card verification code.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AddressField",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "address field name, for example GIVEN_NAME.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "address field name, for example Jon Doe.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Address",
+ "type": "object",
+ "properties": [
+ {
+ "name": "fields",
+ "description": "fields and values defining a test address.",
+ "type": "array",
+ "items": {
+ "$ref": "AddressField"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "trigger",
+ "description": "Trigger autofill on a form identified by the fieldId.\nIf the field and related form cannot be autofilled, returns an error.",
+ "parameters": [
+ {
+ "name": "fieldId",
+ "description": "Identifies a field that serves as an anchor for autofill.",
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "frameId",
+ "description": "Identifies the frame that field belongs to.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "card",
+ "description": "Credit card information to fill out the form. Credit card data is not saved.",
+ "$ref": "CreditCard"
+ }
+ ]
+ },
+ {
+ "name": "setAddresses",
+ "description": "Set addresses so that developers can verify their forms implementation.",
+ "parameters": [
+ {
+ "name": "addresses",
+ "type": "array",
+ "items": {
+ "$ref": "Address"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "BackgroundService",
+ "description": "Defines events for background web platform features.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "ServiceName",
+ "description": "The Background Service that will be associated with the commands/events.\nEvery Background Service operates independently, but they share the same\nAPI.",
+ "type": "string",
+ "enum": [
+ "backgroundFetch",
+ "backgroundSync",
+ "pushMessaging",
+ "notifications",
+ "paymentHandler",
+ "periodicBackgroundSync"
+ ]
+ },
+ {
+ "id": "EventMetadata",
+ "description": "A key-value pair for additional event information to pass along.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "BackgroundServiceEvent",
+ "type": "object",
+ "properties": [
+ {
+ "name": "timestamp",
+ "description": "Timestamp of the event (in seconds).",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "origin",
+ "description": "The origin this event belongs to.",
+ "type": "string"
+ },
+ {
+ "name": "serviceWorkerRegistrationId",
+ "description": "The Service Worker ID that initiated the event.",
+ "$ref": "ServiceWorker.RegistrationID"
+ },
+ {
+ "name": "service",
+ "description": "The Background Service this event belongs to.",
+ "$ref": "ServiceName"
+ },
+ {
+ "name": "eventName",
+ "description": "A description of the event.",
+ "type": "string"
+ },
+ {
+ "name": "instanceId",
+ "description": "An identifier that groups related events together.",
+ "type": "string"
+ },
+ {
+ "name": "eventMetadata",
+ "description": "A list of event-specific information.",
+ "type": "array",
+ "items": {
+ "$ref": "EventMetadata"
+ }
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key this event belongs to.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "startObserving",
+ "description": "Enables event updates for the service.",
+ "parameters": [
+ {
+ "name": "service",
+ "$ref": "ServiceName"
+ }
+ ]
+ },
+ {
+ "name": "stopObserving",
+ "description": "Disables event updates for the service.",
+ "parameters": [
+ {
+ "name": "service",
+ "$ref": "ServiceName"
+ }
+ ]
+ },
+ {
+ "name": "setRecording",
+ "description": "Set the recording state for the service.",
+ "parameters": [
+ {
+ "name": "shouldRecord",
+ "type": "boolean"
+ },
+ {
+ "name": "service",
+ "$ref": "ServiceName"
+ }
+ ]
+ },
+ {
+ "name": "clearEvents",
+ "description": "Clears all stored data for the service.",
+ "parameters": [
+ {
+ "name": "service",
+ "$ref": "ServiceName"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "recordingStateChanged",
+ "description": "Called when the recording state for the service has been updated.",
+ "parameters": [
+ {
+ "name": "isRecording",
+ "type": "boolean"
+ },
+ {
+ "name": "service",
+ "$ref": "ServiceName"
+ }
+ ]
+ },
+ {
+ "name": "backgroundServiceEventReceived",
+ "description": "Called with all existing backgroundServiceEvents when enabled, and all new\nevents afterwards if enabled and recording.",
+ "parameters": [
+ {
+ "name": "backgroundServiceEvent",
+ "$ref": "BackgroundServiceEvent"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Browser",
+ "description": "The Browser domain defines methods and events for browser managing.",
+ "types": [
+ {
+ "id": "BrowserContextID",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "WindowID",
+ "experimental": true,
+ "type": "integer"
+ },
+ {
+ "id": "WindowState",
+ "description": "The state of the browser window.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "normal",
+ "minimized",
+ "maximized",
+ "fullscreen"
+ ]
+ },
+ {
+ "id": "Bounds",
+ "description": "Browser window bounds information",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "left",
+ "description": "The offset from the left edge of the screen to the window in pixels.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "top",
+ "description": "The offset from the top edge of the screen to the window in pixels.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "width",
+ "description": "The window width in pixels.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "The window height in pixels.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "windowState",
+ "description": "The window state. Default to normal.",
+ "optional": true,
+ "$ref": "WindowState"
+ }
+ ]
+ },
+ {
+ "id": "PermissionType",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "accessibilityEvents",
+ "audioCapture",
+ "backgroundSync",
+ "backgroundFetch",
+ "clipboardReadWrite",
+ "clipboardSanitizedWrite",
+ "displayCapture",
+ "durableStorage",
+ "flash",
+ "geolocation",
+ "idleDetection",
+ "localFonts",
+ "midi",
+ "midiSysex",
+ "nfc",
+ "notifications",
+ "paymentHandler",
+ "periodicBackgroundSync",
+ "protectedMediaIdentifier",
+ "sensors",
+ "storageAccess",
+ "topLevelStorageAccess",
+ "videoCapture",
+ "videoCapturePanTiltZoom",
+ "wakeLockScreen",
+ "wakeLockSystem",
+ "windowManagement"
+ ]
+ },
+ {
+ "id": "PermissionSetting",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "granted",
+ "denied",
+ "prompt"
+ ]
+ },
+ {
+ "id": "PermissionDescriptor",
+ "description": "Definition of PermissionDescriptor defined in the Permissions API:\nhttps://w3c.github.io/permissions/#dictdef-permissiondescriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Name of permission.\nSee https://cs.chromium.org/chromium/src/third_party/blink/renderer/modules/permissions/permission_descriptor.idl for valid permission names.",
+ "type": "string"
+ },
+ {
+ "name": "sysex",
+ "description": "For \"midi\" permission, may also specify sysex control.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "userVisibleOnly",
+ "description": "For \"push\" permission, may specify userVisibleOnly.\nNote that userVisibleOnly = true is the only currently supported type.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "allowWithoutSanitization",
+ "description": "For \"clipboard\" permission, may specify allowWithoutSanitization.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "panTiltZoom",
+ "description": "For \"camera\" permission, may specify panTiltZoom.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "BrowserCommandId",
+ "description": "Browser command ids used by executeBrowserCommand.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "openTabSearch",
+ "closeTabSearch"
+ ]
+ },
+ {
+ "id": "Bucket",
+ "description": "Chrome histogram bucket.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "low",
+ "description": "Minimum value (inclusive).",
+ "type": "integer"
+ },
+ {
+ "name": "high",
+ "description": "Maximum value (exclusive).",
+ "type": "integer"
+ },
+ {
+ "name": "count",
+ "description": "Number of samples.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "Histogram",
+ "description": "Chrome histogram.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Name.",
+ "type": "string"
+ },
+ {
+ "name": "sum",
+ "description": "Sum of sample values.",
+ "type": "integer"
+ },
+ {
+ "name": "count",
+ "description": "Total number of samples.",
+ "type": "integer"
+ },
+ {
+ "name": "buckets",
+ "description": "Buckets.",
+ "type": "array",
+ "items": {
+ "$ref": "Bucket"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "setPermission",
+ "description": "Set permission settings for given origin.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "permission",
+ "description": "Descriptor of permission to override.",
+ "$ref": "PermissionDescriptor"
+ },
+ {
+ "name": "setting",
+ "description": "Setting of the permission.",
+ "$ref": "PermissionSetting"
+ },
+ {
+ "name": "origin",
+ "description": "Origin the permission applies to, all origins if not specified.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "browserContextId",
+ "description": "Context to override. When omitted, default browser context is used.",
+ "optional": true,
+ "$ref": "BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "grantPermissions",
+ "description": "Grant specific permissions to the given origin and reject all others.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "permissions",
+ "type": "array",
+ "items": {
+ "$ref": "PermissionType"
+ }
+ },
+ {
+ "name": "origin",
+ "description": "Origin the permission applies to, all origins if not specified.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "browserContextId",
+ "description": "BrowserContext to override permissions. When omitted, default browser context is used.",
+ "optional": true,
+ "$ref": "BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "resetPermissions",
+ "description": "Reset all permission management for all origins.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "browserContextId",
+ "description": "BrowserContext to reset permissions. When omitted, default browser context is used.",
+ "optional": true,
+ "$ref": "BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "setDownloadBehavior",
+ "description": "Set the behavior when downloading a file.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "behavior",
+ "description": "Whether to allow all or deny all download requests, or use default Chrome behavior if\navailable (otherwise deny). |allowAndName| allows download and names files according to\ntheir dowmload guids.",
+ "type": "string",
+ "enum": [
+ "deny",
+ "allow",
+ "allowAndName",
+ "default"
+ ]
+ },
+ {
+ "name": "browserContextId",
+ "description": "BrowserContext to set download behavior. When omitted, default browser context is used.",
+ "optional": true,
+ "$ref": "BrowserContextID"
+ },
+ {
+ "name": "downloadPath",
+ "description": "The default path to save downloaded files to. This is required if behavior is set to 'allow'\nor 'allowAndName'.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "eventsEnabled",
+ "description": "Whether to emit download events (defaults to false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "cancelDownload",
+ "description": "Cancel a download if in progress",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "guid",
+ "description": "Global unique identifier of the download.",
+ "type": "string"
+ },
+ {
+ "name": "browserContextId",
+ "description": "BrowserContext to perform the action in. When omitted, default browser context is used.",
+ "optional": true,
+ "$ref": "BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "close",
+ "description": "Close browser gracefully."
+ },
+ {
+ "name": "crash",
+ "description": "Crashes browser on the main thread.",
+ "experimental": true
+ },
+ {
+ "name": "crashGpuProcess",
+ "description": "Crashes GPU process.",
+ "experimental": true
+ },
+ {
+ "name": "getVersion",
+ "description": "Returns version information.",
+ "returns": [
+ {
+ "name": "protocolVersion",
+ "description": "Protocol version.",
+ "type": "string"
+ },
+ {
+ "name": "product",
+ "description": "Product name.",
+ "type": "string"
+ },
+ {
+ "name": "revision",
+ "description": "Product revision.",
+ "type": "string"
+ },
+ {
+ "name": "userAgent",
+ "description": "User-Agent.",
+ "type": "string"
+ },
+ {
+ "name": "jsVersion",
+ "description": "V8 version.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getBrowserCommandLine",
+ "description": "Returns the command line switches for the browser process if, and only if\n--enable-automation is on the commandline.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "arguments",
+ "description": "Commandline parameters",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getHistograms",
+ "description": "Get Chrome histograms.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "query",
+ "description": "Requested substring in name. Only histograms which have query as a\nsubstring in their name are extracted. An empty or absent query returns\nall histograms.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "delta",
+ "description": "If true, retrieve delta since last delta call.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "histograms",
+ "description": "Histograms.",
+ "type": "array",
+ "items": {
+ "$ref": "Histogram"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getHistogram",
+ "description": "Get a Chrome histogram by name.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "name",
+ "description": "Requested histogram name.",
+ "type": "string"
+ },
+ {
+ "name": "delta",
+ "description": "If true, retrieve delta since last delta call.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "histogram",
+ "description": "Histogram.",
+ "$ref": "Histogram"
+ }
+ ]
+ },
+ {
+ "name": "getWindowBounds",
+ "description": "Get position and size of the browser window.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "windowId",
+ "description": "Browser window id.",
+ "$ref": "WindowID"
+ }
+ ],
+ "returns": [
+ {
+ "name": "bounds",
+ "description": "Bounds information of the window. When window state is 'minimized', the restored window\nposition and size are returned.",
+ "$ref": "Bounds"
+ }
+ ]
+ },
+ {
+ "name": "getWindowForTarget",
+ "description": "Get the browser window that contains the devtools target.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "targetId",
+ "description": "Devtools agent host id. If called as a part of the session, associated targetId is used.",
+ "optional": true,
+ "$ref": "Target.TargetID"
+ }
+ ],
+ "returns": [
+ {
+ "name": "windowId",
+ "description": "Browser window id.",
+ "$ref": "WindowID"
+ },
+ {
+ "name": "bounds",
+ "description": "Bounds information of the window. When window state is 'minimized', the restored window\nposition and size are returned.",
+ "$ref": "Bounds"
+ }
+ ]
+ },
+ {
+ "name": "setWindowBounds",
+ "description": "Set position and/or size of the browser window.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "windowId",
+ "description": "Browser window id.",
+ "$ref": "WindowID"
+ },
+ {
+ "name": "bounds",
+ "description": "New window bounds. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined\nwith 'left', 'top', 'width' or 'height'. Leaves unspecified fields unchanged.",
+ "$ref": "Bounds"
+ }
+ ]
+ },
+ {
+ "name": "setDockTile",
+ "description": "Set dock tile details, platform-specific.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "badgeLabel",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "image",
+ "description": "Png encoded image. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "executeBrowserCommand",
+ "description": "Invoke custom browser commands used by telemetry.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "commandId",
+ "$ref": "BrowserCommandId"
+ }
+ ]
+ },
+ {
+ "name": "addPrivacySandboxEnrollmentOverride",
+ "description": "Allows a site to use privacy sandbox features that require enrollment\nwithout the site actually being enrolled. Only supported on page targets.",
+ "parameters": [
+ {
+ "name": "url",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "downloadWillBegin",
+ "description": "Fired when page is about to start a download.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that caused the download to begin.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "guid",
+ "description": "Global unique identifier of the download.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource being downloaded.",
+ "type": "string"
+ },
+ {
+ "name": "suggestedFilename",
+ "description": "Suggested file name of the resource (the actual name of the file saved on disk may differ).",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "downloadProgress",
+ "description": "Fired when download makes progress. Last call has |done| == true.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "guid",
+ "description": "Global unique identifier of the download.",
+ "type": "string"
+ },
+ {
+ "name": "totalBytes",
+ "description": "Total expected bytes to download.",
+ "type": "number"
+ },
+ {
+ "name": "receivedBytes",
+ "description": "Total bytes received.",
+ "type": "number"
+ },
+ {
+ "name": "state",
+ "description": "Download status.",
+ "type": "string",
+ "enum": [
+ "inProgress",
+ "completed",
+ "canceled"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "CSS",
+ "description": "This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles)\nhave an associated `id` used in subsequent operations on the related object. Each object type has\na specific `id` structure, and those are not interchangeable between objects of different kinds.\nCSS objects can be loaded using the `get*ForNode()` calls (which accept a DOM node id). A client\ncan also keep track of stylesheets via the `styleSheetAdded`/`styleSheetRemoved` events and\nsubsequently load the required stylesheet contents using the `getStyleSheet[Text]()` methods.",
+ "experimental": true,
+ "dependencies": [
+ "DOM",
+ "Page"
+ ],
+ "types": [
+ {
+ "id": "StyleSheetId",
+ "type": "string"
+ },
+ {
+ "id": "StyleSheetOrigin",
+ "description": "Stylesheet type: \"injected\" for stylesheets injected via extension, \"user-agent\" for user-agent\nstylesheets, \"inspector\" for stylesheets created by the inspector (i.e. those holding the \"via\ninspector\" rules), \"regular\" for regular stylesheets.",
+ "type": "string",
+ "enum": [
+ "injected",
+ "user-agent",
+ "inspector",
+ "regular"
+ ]
+ },
+ {
+ "id": "PseudoElementMatches",
+ "description": "CSS rule collection for a single pseudo style.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "pseudoType",
+ "description": "Pseudo element type.",
+ "$ref": "DOM.PseudoType"
+ },
+ {
+ "name": "pseudoIdentifier",
+ "description": "Pseudo element custom ident.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "matches",
+ "description": "Matches of CSS rules applicable to the pseudo style.",
+ "type": "array",
+ "items": {
+ "$ref": "RuleMatch"
+ }
+ }
+ ]
+ },
+ {
+ "id": "InheritedStyleEntry",
+ "description": "Inherited CSS rule collection from ancestor node.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "inlineStyle",
+ "description": "The ancestor node's inline style, if any, in the style inheritance chain.",
+ "optional": true,
+ "$ref": "CSSStyle"
+ },
+ {
+ "name": "matchedCSSRules",
+ "description": "Matches of CSS rules matching the ancestor node in the style inheritance chain.",
+ "type": "array",
+ "items": {
+ "$ref": "RuleMatch"
+ }
+ }
+ ]
+ },
+ {
+ "id": "InheritedPseudoElementMatches",
+ "description": "Inherited pseudo element matches from pseudos of an ancestor node.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "pseudoElements",
+ "description": "Matches of pseudo styles from the pseudos of an ancestor node.",
+ "type": "array",
+ "items": {
+ "$ref": "PseudoElementMatches"
+ }
+ }
+ ]
+ },
+ {
+ "id": "RuleMatch",
+ "description": "Match data for a CSS rule.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "rule",
+ "description": "CSS rule in the match.",
+ "$ref": "CSSRule"
+ },
+ {
+ "name": "matchingSelectors",
+ "description": "Matching selector indices in the rule's selectorList selectors (0-based).",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Value",
+ "description": "Data for a simple selector (these are delimited by commas in a selector list).",
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Value text.",
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "Value range in the underlying resource (if available).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "specificity",
+ "description": "Specificity of the selector.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Specificity"
+ }
+ ]
+ },
+ {
+ "id": "Specificity",
+ "description": "Specificity:\nhttps://drafts.csswg.org/selectors/#specificity-rules",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "a",
+ "description": "The a component, which represents the number of ID selectors.",
+ "type": "integer"
+ },
+ {
+ "name": "b",
+ "description": "The b component, which represents the number of class selectors, attributes selectors, and\npseudo-classes.",
+ "type": "integer"
+ },
+ {
+ "name": "c",
+ "description": "The c component, which represents the number of type selectors and pseudo-elements.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "SelectorList",
+ "description": "Selector list data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "selectors",
+ "description": "Selectors in the list.",
+ "type": "array",
+ "items": {
+ "$ref": "Value"
+ }
+ },
+ {
+ "name": "text",
+ "description": "Rule selector text.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CSSStyleSheetHeader",
+ "description": "CSS stylesheet metainformation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The stylesheet identifier.",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "frameId",
+ "description": "Owner frame identifier.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "sourceURL",
+ "description": "Stylesheet resource URL. Empty if this is a constructed stylesheet created using\nnew CSSStyleSheet() (but non-empty if this is a constructed sylesheet imported\nas a CSS module script).",
+ "type": "string"
+ },
+ {
+ "name": "sourceMapURL",
+ "description": "URL of source map associated with the stylesheet (if any).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "origin",
+ "description": "Stylesheet origin.",
+ "$ref": "StyleSheetOrigin"
+ },
+ {
+ "name": "title",
+ "description": "Stylesheet title.",
+ "type": "string"
+ },
+ {
+ "name": "ownerNode",
+ "description": "The backend id for the owner node of the stylesheet.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "disabled",
+ "description": "Denotes whether the stylesheet is disabled.",
+ "type": "boolean"
+ },
+ {
+ "name": "hasSourceURL",
+ "description": "Whether the sourceURL field value comes from the sourceURL comment.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isInline",
+ "description": "Whether this stylesheet is created for STYLE tag by parser. This flag is not set for\ndocument.written STYLE tags.",
+ "type": "boolean"
+ },
+ {
+ "name": "isMutable",
+ "description": "Whether this stylesheet is mutable. Inline stylesheets become mutable\nafter they have been modified via CSSOM API.\n`<link>` element's stylesheets become mutable only if DevTools modifies them.\nConstructed stylesheets (new CSSStyleSheet()) are mutable immediately after creation.",
+ "type": "boolean"
+ },
+ {
+ "name": "isConstructed",
+ "description": "True if this stylesheet is created through new CSSStyleSheet() or imported as a\nCSS module script.",
+ "type": "boolean"
+ },
+ {
+ "name": "startLine",
+ "description": "Line offset of the stylesheet within the resource (zero based).",
+ "type": "number"
+ },
+ {
+ "name": "startColumn",
+ "description": "Column offset of the stylesheet within the resource (zero based).",
+ "type": "number"
+ },
+ {
+ "name": "length",
+ "description": "Size of the content (in characters).",
+ "type": "number"
+ },
+ {
+ "name": "endLine",
+ "description": "Line offset of the end of the stylesheet within the resource (zero based).",
+ "type": "number"
+ },
+ {
+ "name": "endColumn",
+ "description": "Column offset of the end of the stylesheet within the resource (zero based).",
+ "type": "number"
+ },
+ {
+ "name": "loadingFailed",
+ "description": "If the style sheet was loaded from a network resource, this indicates when the resource failed to load",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "CSSRule",
+ "description": "CSS rule representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "selectorList",
+ "description": "Rule selector data.",
+ "$ref": "SelectorList"
+ },
+ {
+ "name": "nestingSelectors",
+ "description": "Array of selectors from ancestor style rules, sorted by distance from the current rule.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "origin",
+ "description": "Parent stylesheet's origin.",
+ "$ref": "StyleSheetOrigin"
+ },
+ {
+ "name": "style",
+ "description": "Associated style declaration.",
+ "$ref": "CSSStyle"
+ },
+ {
+ "name": "media",
+ "description": "Media list array (for rules involving media queries). The array enumerates media queries\nstarting with the innermost one, going outwards.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSMedia"
+ }
+ },
+ {
+ "name": "containerQueries",
+ "description": "Container query list array (for rules involving container queries).\nThe array enumerates container queries starting with the innermost one, going outwards.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSContainerQuery"
+ }
+ },
+ {
+ "name": "supports",
+ "description": "@supports CSS at-rule array.\nThe array enumerates @supports at-rules starting with the innermost one, going outwards.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSSupports"
+ }
+ },
+ {
+ "name": "layers",
+ "description": "Cascade layer array. Contains the layer hierarchy that this rule belongs to starting\nwith the innermost layer and going outwards.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSLayer"
+ }
+ },
+ {
+ "name": "scopes",
+ "description": "@scope CSS at-rule array.\nThe array enumerates @scope at-rules starting with the innermost one, going outwards.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSScope"
+ }
+ },
+ {
+ "name": "ruleTypes",
+ "description": "The array keeps the types of ancestor CSSRules from the innermost going outwards.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSRuleType"
+ }
+ }
+ ]
+ },
+ {
+ "id": "CSSRuleType",
+ "description": "Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors.\nThis list only contains rule types that are collected during the ancestor rule collection.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "MediaRule",
+ "SupportsRule",
+ "ContainerRule",
+ "LayerRule",
+ "ScopeRule",
+ "StyleRule"
+ ]
+ },
+ {
+ "id": "RuleUsage",
+ "description": "CSS coverage information.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "startOffset",
+ "description": "Offset of the start of the rule (including selector) from the beginning of the stylesheet.",
+ "type": "number"
+ },
+ {
+ "name": "endOffset",
+ "description": "Offset of the end of the rule body from the beginning of the stylesheet.",
+ "type": "number"
+ },
+ {
+ "name": "used",
+ "description": "Indicates whether the rule was actually used by some element in the page.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "SourceRange",
+ "description": "Text range within a resource. All numbers are zero-based.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "startLine",
+ "description": "Start line of range.",
+ "type": "integer"
+ },
+ {
+ "name": "startColumn",
+ "description": "Start column of range (inclusive).",
+ "type": "integer"
+ },
+ {
+ "name": "endLine",
+ "description": "End line of range",
+ "type": "integer"
+ },
+ {
+ "name": "endColumn",
+ "description": "End column of range (exclusive).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "ShorthandEntry",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Shorthand name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Shorthand value.",
+ "type": "string"
+ },
+ {
+ "name": "important",
+ "description": "Whether the property has \"!important\" annotation (implies `false` if absent).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "CSSComputedStyleProperty",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Computed style property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Computed style property value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CSSStyle",
+ "description": "CSS style representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "cssProperties",
+ "description": "CSS properties in the style.",
+ "type": "array",
+ "items": {
+ "$ref": "CSSProperty"
+ }
+ },
+ {
+ "name": "shorthandEntries",
+ "description": "Computed values for all shorthands found in the style.",
+ "type": "array",
+ "items": {
+ "$ref": "ShorthandEntry"
+ }
+ },
+ {
+ "name": "cssText",
+ "description": "Style declaration text (if available).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "Style declaration range in the enclosing stylesheet (if available).",
+ "optional": true,
+ "$ref": "SourceRange"
+ }
+ ]
+ },
+ {
+ "id": "CSSProperty",
+ "description": "CSS property declaration data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "The property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "The property value.",
+ "type": "string"
+ },
+ {
+ "name": "important",
+ "description": "Whether the property has \"!important\" annotation (implies `false` if absent).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "implicit",
+ "description": "Whether the property is implicit (implies `false` if absent).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "text",
+ "description": "The full property text as specified in the style.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "parsedOk",
+ "description": "Whether the property is understood by the browser (implies `true` if absent).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "disabled",
+ "description": "Whether the property is disabled by the user (present for source-based properties only).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "range",
+ "description": "The entire property range in the enclosing style declaration (if available).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "longhandProperties",
+ "description": "Parsed longhand components of this property if it is a shorthand.\nThis field will be empty if the given property is not a shorthand.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSProperty"
+ }
+ }
+ ]
+ },
+ {
+ "id": "CSSMedia",
+ "description": "CSS media rule descriptor.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Media query text.",
+ "type": "string"
+ },
+ {
+ "name": "source",
+ "description": "Source of the media query: \"mediaRule\" if specified by a @media rule, \"importRule\" if\nspecified by an @import rule, \"linkedSheet\" if specified by a \"media\" attribute in a linked\nstylesheet's LINK tag, \"inlineSheet\" if specified by a \"media\" attribute in an inline\nstylesheet's STYLE tag.",
+ "type": "string",
+ "enum": [
+ "mediaRule",
+ "importRule",
+ "linkedSheet",
+ "inlineSheet"
+ ]
+ },
+ {
+ "name": "sourceURL",
+ "description": "URL of the document containing the media query description.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "The associated rule (@media or @import) header range in the enclosing stylesheet (if\navailable).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the stylesheet containing this object (if exists).",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "mediaList",
+ "description": "Array of media queries.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "MediaQuery"
+ }
+ }
+ ]
+ },
+ {
+ "id": "MediaQuery",
+ "description": "Media query descriptor.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "expressions",
+ "description": "Array of media query expressions.",
+ "type": "array",
+ "items": {
+ "$ref": "MediaQueryExpression"
+ }
+ },
+ {
+ "name": "active",
+ "description": "Whether the media query condition is satisfied.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "MediaQueryExpression",
+ "description": "Media query expression descriptor.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "value",
+ "description": "Media query expression value.",
+ "type": "number"
+ },
+ {
+ "name": "unit",
+ "description": "Media query expression units.",
+ "type": "string"
+ },
+ {
+ "name": "feature",
+ "description": "Media query expression feature.",
+ "type": "string"
+ },
+ {
+ "name": "valueRange",
+ "description": "The associated range of the value text in the enclosing stylesheet (if available).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "computedLength",
+ "description": "Computed length of media query expression (if applicable).",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "CSSContainerQuery",
+ "description": "CSS container query rule descriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Container query text.",
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "The associated rule header range in the enclosing stylesheet (if\navailable).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the stylesheet containing this object (if exists).",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "name",
+ "description": "Optional name for the container.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "physicalAxes",
+ "description": "Optional physical axes queried for the container.",
+ "optional": true,
+ "$ref": "DOM.PhysicalAxes"
+ },
+ {
+ "name": "logicalAxes",
+ "description": "Optional logical axes queried for the container.",
+ "optional": true,
+ "$ref": "DOM.LogicalAxes"
+ }
+ ]
+ },
+ {
+ "id": "CSSSupports",
+ "description": "CSS Supports at-rule descriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Supports rule text.",
+ "type": "string"
+ },
+ {
+ "name": "active",
+ "description": "Whether the supports condition is satisfied.",
+ "type": "boolean"
+ },
+ {
+ "name": "range",
+ "description": "The associated rule header range in the enclosing stylesheet (if\navailable).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the stylesheet containing this object (if exists).",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ }
+ ]
+ },
+ {
+ "id": "CSSScope",
+ "description": "CSS Scope at-rule descriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Scope rule text.",
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "The associated rule header range in the enclosing stylesheet (if\navailable).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the stylesheet containing this object (if exists).",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ }
+ ]
+ },
+ {
+ "id": "CSSLayer",
+ "description": "CSS Layer at-rule descriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Layer name.",
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "The associated rule header range in the enclosing stylesheet (if\navailable).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the stylesheet containing this object (if exists).",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ }
+ ]
+ },
+ {
+ "id": "CSSLayerData",
+ "description": "CSS Layer data.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Layer name.",
+ "type": "string"
+ },
+ {
+ "name": "subLayers",
+ "description": "Direct sub-layers",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSLayerData"
+ }
+ },
+ {
+ "name": "order",
+ "description": "Layer order. The order determines the order of the layer in the cascade order.\nA higher number has higher priority in the cascade order.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "PlatformFontUsage",
+ "description": "Information about amount of glyphs that were rendered with given font.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "familyName",
+ "description": "Font's family name reported by platform.",
+ "type": "string"
+ },
+ {
+ "name": "isCustomFont",
+ "description": "Indicates if the font was downloaded or resolved locally.",
+ "type": "boolean"
+ },
+ {
+ "name": "glyphCount",
+ "description": "Amount of glyphs that were rendered with this font.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "FontVariationAxis",
+ "description": "Information about font variation axes for variable fonts",
+ "type": "object",
+ "properties": [
+ {
+ "name": "tag",
+ "description": "The font-variation-setting tag (a.k.a. \"axis tag\").",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Human-readable variation name in the default language (normally, \"en\").",
+ "type": "string"
+ },
+ {
+ "name": "minValue",
+ "description": "The minimum value (inclusive) the font supports for this tag.",
+ "type": "number"
+ },
+ {
+ "name": "maxValue",
+ "description": "The maximum value (inclusive) the font supports for this tag.",
+ "type": "number"
+ },
+ {
+ "name": "defaultValue",
+ "description": "The default value.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "FontFace",
+ "description": "Properties of a web font: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#font-descriptions\nand additional information such as platformFontFamily and fontVariationAxes.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "fontFamily",
+ "description": "The font-family.",
+ "type": "string"
+ },
+ {
+ "name": "fontStyle",
+ "description": "The font-style.",
+ "type": "string"
+ },
+ {
+ "name": "fontVariant",
+ "description": "The font-variant.",
+ "type": "string"
+ },
+ {
+ "name": "fontWeight",
+ "description": "The font-weight.",
+ "type": "string"
+ },
+ {
+ "name": "fontStretch",
+ "description": "The font-stretch.",
+ "type": "string"
+ },
+ {
+ "name": "fontDisplay",
+ "description": "The font-display.",
+ "type": "string"
+ },
+ {
+ "name": "unicodeRange",
+ "description": "The unicode-range.",
+ "type": "string"
+ },
+ {
+ "name": "src",
+ "description": "The src.",
+ "type": "string"
+ },
+ {
+ "name": "platformFontFamily",
+ "description": "The resolved platform font family",
+ "type": "string"
+ },
+ {
+ "name": "fontVariationAxes",
+ "description": "Available variation settings (a.k.a. \"axes\").",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "FontVariationAxis"
+ }
+ }
+ ]
+ },
+ {
+ "id": "CSSTryRule",
+ "description": "CSS try rule representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "origin",
+ "description": "Parent stylesheet's origin.",
+ "$ref": "StyleSheetOrigin"
+ },
+ {
+ "name": "style",
+ "description": "Associated style declaration.",
+ "$ref": "CSSStyle"
+ }
+ ]
+ },
+ {
+ "id": "CSSPositionFallbackRule",
+ "description": "CSS position-fallback rule representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "$ref": "Value"
+ },
+ {
+ "name": "tryRules",
+ "description": "List of keyframes.",
+ "type": "array",
+ "items": {
+ "$ref": "CSSTryRule"
+ }
+ }
+ ]
+ },
+ {
+ "id": "CSSKeyframesRule",
+ "description": "CSS keyframes rule representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "animationName",
+ "description": "Animation name.",
+ "$ref": "Value"
+ },
+ {
+ "name": "keyframes",
+ "description": "List of keyframes.",
+ "type": "array",
+ "items": {
+ "$ref": "CSSKeyframeRule"
+ }
+ }
+ ]
+ },
+ {
+ "id": "CSSKeyframeRule",
+ "description": "CSS keyframe rule representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "origin",
+ "description": "Parent stylesheet's origin.",
+ "$ref": "StyleSheetOrigin"
+ },
+ {
+ "name": "keyText",
+ "description": "Associated key text.",
+ "$ref": "Value"
+ },
+ {
+ "name": "style",
+ "description": "Associated style declaration.",
+ "$ref": "CSSStyle"
+ }
+ ]
+ },
+ {
+ "id": "StyleDeclarationEdit",
+ "description": "A descriptor of operation to mutate style declaration text.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier.",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "description": "The range of the style text in the enclosing stylesheet.",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "text",
+ "description": "New style text.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "addRule",
+ "description": "Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the\nposition specified by `location`.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier where a new rule should be inserted.",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "ruleText",
+ "description": "The text of a new rule.",
+ "type": "string"
+ },
+ {
+ "name": "location",
+ "description": "Text position of a new rule in the target style sheet.",
+ "$ref": "SourceRange"
+ }
+ ],
+ "returns": [
+ {
+ "name": "rule",
+ "description": "The newly created rule.",
+ "$ref": "CSSRule"
+ }
+ ]
+ },
+ {
+ "name": "collectClassNames",
+ "description": "Returns all class names from specified stylesheet.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "classNames",
+ "description": "Class name list.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "createStyleSheet",
+ "description": "Creates a new special \"via-inspector\" stylesheet in the frame with given `frameId`.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Identifier of the frame where \"via-inspector\" stylesheet should be created.",
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the created \"via-inspector\" stylesheet.",
+ "$ref": "StyleSheetId"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables the CSS agent for the given page."
+ },
+ {
+ "name": "enable",
+ "description": "Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been\nenabled until the result of this command is received."
+ },
+ {
+ "name": "forcePseudoState",
+ "description": "Ensures that the given node will have specified pseudo-classes whenever its style is computed by\nthe browser.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "The element id for which to force the pseudo state.",
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "forcedPseudoClasses",
+ "description": "Element pseudo classes to force when computing the element's style.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getBackgroundColors",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to get background colors for.",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "backgroundColors",
+ "description": "The range of background colors behind this element, if it contains any visible text. If no\nvisible text is present, this will be undefined. In the case of a flat background color,\nthis will consist of simply that color. In the case of a gradient, this will consist of each\nof the color stops. For anything more complicated, this will be an empty array. Images will\nbe ignored (as if the image had failed to load).",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "computedFontSize",
+ "description": "The computed font size for this node, as a CSS computed value string (e.g. '12px').",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "computedFontWeight",
+ "description": "The computed font weight for this node, as a CSS computed value string (e.g. 'normal' or\n'100').",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getComputedStyleForNode",
+ "description": "Returns the computed style for a DOM node identified by `nodeId`.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "computedStyle",
+ "description": "Computed style for the specified DOM node.",
+ "type": "array",
+ "items": {
+ "$ref": "CSSComputedStyleProperty"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getInlineStylesForNode",
+ "description": "Returns the styles defined inline (explicitly in the \"style\" attribute and implicitly, using DOM\nattributes) for a DOM node identified by `nodeId`.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "inlineStyle",
+ "description": "Inline style for the specified DOM node.",
+ "optional": true,
+ "$ref": "CSSStyle"
+ },
+ {
+ "name": "attributesStyle",
+ "description": "Attribute-defined element style (e.g. resulting from \"width=20 height=100%\").",
+ "optional": true,
+ "$ref": "CSSStyle"
+ }
+ ]
+ },
+ {
+ "name": "getMatchedStylesForNode",
+ "description": "Returns requested styles for a DOM node identified by `nodeId`.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "inlineStyle",
+ "description": "Inline style for the specified DOM node.",
+ "optional": true,
+ "$ref": "CSSStyle"
+ },
+ {
+ "name": "attributesStyle",
+ "description": "Attribute-defined element style (e.g. resulting from \"width=20 height=100%\").",
+ "optional": true,
+ "$ref": "CSSStyle"
+ },
+ {
+ "name": "matchedCSSRules",
+ "description": "CSS rules matching this node, from all applicable stylesheets.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "RuleMatch"
+ }
+ },
+ {
+ "name": "pseudoElements",
+ "description": "Pseudo style matches for this node.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "PseudoElementMatches"
+ }
+ },
+ {
+ "name": "inherited",
+ "description": "A chain of inherited styles (from the immediate node parent up to the DOM tree root).",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "InheritedStyleEntry"
+ }
+ },
+ {
+ "name": "inheritedPseudoElements",
+ "description": "A chain of inherited pseudo element styles (from the immediate node parent up to the DOM tree root).",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "InheritedPseudoElementMatches"
+ }
+ },
+ {
+ "name": "cssKeyframesRules",
+ "description": "A list of CSS keyframed animations matching this node.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSKeyframesRule"
+ }
+ },
+ {
+ "name": "cssPositionFallbackRules",
+ "description": "A list of CSS position fallbacks matching this node.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSPositionFallbackRule"
+ }
+ },
+ {
+ "name": "parentLayoutNodeId",
+ "description": "Id of the first parent element that does not have display: contents.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "name": "getMediaQueries",
+ "description": "Returns all media queries parsed by the rendering engine.",
+ "returns": [
+ {
+ "name": "medias",
+ "type": "array",
+ "items": {
+ "$ref": "CSSMedia"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getPlatformFontsForNode",
+ "description": "Requests information about platform fonts which we used to render child TextNodes in the given\nnode.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "fonts",
+ "description": "Usage statistics for every employed platform font.",
+ "type": "array",
+ "items": {
+ "$ref": "PlatformFontUsage"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getStyleSheetText",
+ "description": "Returns the current textual content for a stylesheet.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "text",
+ "description": "The stylesheet text.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getLayersForNode",
+ "description": "Returns all layers parsed by the rendering engine for the tree scope of a node.\nGiven a DOM element identified by nodeId, getLayersForNode returns the root\nlayer for the nearest ancestor document or shadow root. The layer root contains\nthe full layer tree for the tree scope and their ordering.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "rootLayer",
+ "$ref": "CSSLayerData"
+ }
+ ]
+ },
+ {
+ "name": "trackComputedStyleUpdates",
+ "description": "Starts tracking the given computed styles for updates. The specified array of properties\nreplaces the one previously specified. Pass empty array to disable tracking.\nUse takeComputedStyleUpdates to retrieve the list of nodes that had properties modified.\nThe changes to computed style properties are only tracked for nodes pushed to the front-end\nby the DOM agent. If no changes to the tracked properties occur after the node has been pushed\nto the front-end, no updates will be issued for the node.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "propertiesToTrack",
+ "type": "array",
+ "items": {
+ "$ref": "CSSComputedStyleProperty"
+ }
+ }
+ ]
+ },
+ {
+ "name": "takeComputedStyleUpdates",
+ "description": "Polls the next batch of computed style updates.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "The list of node Ids that have their tracked computed styles updated.",
+ "type": "array",
+ "items": {
+ "$ref": "DOM.NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setEffectivePropertyValueForNode",
+ "description": "Find a rule with the given active property for the given node and set the new value for this\nproperty",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "The element id for which to set property.",
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "propertyName",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setKeyframeKey",
+ "description": "Modifies the keyframe rule key text.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "keyText",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "keyText",
+ "description": "The resulting key text after modification.",
+ "$ref": "Value"
+ }
+ ]
+ },
+ {
+ "name": "setMediaText",
+ "description": "Modifies the rule selector.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "text",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "media",
+ "description": "The resulting CSS media rule after modification.",
+ "$ref": "CSSMedia"
+ }
+ ]
+ },
+ {
+ "name": "setContainerQueryText",
+ "description": "Modifies the expression of a container query.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "text",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "containerQuery",
+ "description": "The resulting CSS container query rule after modification.",
+ "$ref": "CSSContainerQuery"
+ }
+ ]
+ },
+ {
+ "name": "setSupportsText",
+ "description": "Modifies the expression of a supports at-rule.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "text",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "supports",
+ "description": "The resulting CSS Supports rule after modification.",
+ "$ref": "CSSSupports"
+ }
+ ]
+ },
+ {
+ "name": "setScopeText",
+ "description": "Modifies the expression of a scope at-rule.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "text",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "scope",
+ "description": "The resulting CSS Scope rule after modification.",
+ "$ref": "CSSScope"
+ }
+ ]
+ },
+ {
+ "name": "setRuleSelector",
+ "description": "Modifies the rule selector.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "selector",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "selectorList",
+ "description": "The resulting selector list after modification.",
+ "$ref": "SelectorList"
+ }
+ ]
+ },
+ {
+ "name": "setStyleSheetText",
+ "description": "Sets the new stylesheet text.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "text",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "sourceMapURL",
+ "description": "URL of source map associated with script (if any).",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setStyleTexts",
+ "description": "Applies specified style edits one after another in the given order.",
+ "parameters": [
+ {
+ "name": "edits",
+ "type": "array",
+ "items": {
+ "$ref": "StyleDeclarationEdit"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "styles",
+ "description": "The resulting styles after modification.",
+ "type": "array",
+ "items": {
+ "$ref": "CSSStyle"
+ }
+ }
+ ]
+ },
+ {
+ "name": "startRuleUsageTracking",
+ "description": "Enables the selector recording."
+ },
+ {
+ "name": "stopRuleUsageTracking",
+ "description": "Stop tracking rule usage and return the list of rules that were used since last call to\n`takeCoverageDelta` (or since start of coverage instrumentation).",
+ "returns": [
+ {
+ "name": "ruleUsage",
+ "type": "array",
+ "items": {
+ "$ref": "RuleUsage"
+ }
+ }
+ ]
+ },
+ {
+ "name": "takeCoverageDelta",
+ "description": "Obtain list of rules that became used since last call to this method (or since start of coverage\ninstrumentation).",
+ "returns": [
+ {
+ "name": "coverage",
+ "type": "array",
+ "items": {
+ "$ref": "RuleUsage"
+ }
+ },
+ {
+ "name": "timestamp",
+ "description": "Monotonically increasing time, in seconds.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setLocalFontsEnabled",
+ "description": "Enables/disables rendering of local CSS fonts (enabled by default).",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether rendering of local fonts is enabled.",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "fontsUpdated",
+ "description": "Fires whenever a web font is updated. A non-empty font parameter indicates a successfully loaded\nweb font.",
+ "parameters": [
+ {
+ "name": "font",
+ "description": "The web font that has loaded.",
+ "optional": true,
+ "$ref": "FontFace"
+ }
+ ]
+ },
+ {
+ "name": "mediaQueryResultChanged",
+ "description": "Fires whenever a MediaQuery result changes (for example, after a browser window has been\nresized.) The current implementation considers only viewport-dependent media features."
+ },
+ {
+ "name": "styleSheetAdded",
+ "description": "Fired whenever an active document stylesheet is added.",
+ "parameters": [
+ {
+ "name": "header",
+ "description": "Added stylesheet metainfo.",
+ "$ref": "CSSStyleSheetHeader"
+ }
+ ]
+ },
+ {
+ "name": "styleSheetChanged",
+ "description": "Fired whenever a stylesheet is changed as a result of the client operation.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ }
+ ]
+ },
+ {
+ "name": "styleSheetRemoved",
+ "description": "Fired whenever an active document stylesheet is removed.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the removed stylesheet.",
+ "$ref": "StyleSheetId"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "CacheStorage",
+ "experimental": true,
+ "dependencies": [
+ "Storage"
+ ],
+ "types": [
+ {
+ "id": "CacheId",
+ "description": "Unique identifier of the Cache object.",
+ "type": "string"
+ },
+ {
+ "id": "CachedResponseType",
+ "description": "type of HTTP response cached",
+ "type": "string",
+ "enum": [
+ "basic",
+ "cors",
+ "default",
+ "error",
+ "opaqueResponse",
+ "opaqueRedirect"
+ ]
+ },
+ {
+ "id": "DataEntry",
+ "description": "Data entry.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "requestURL",
+ "description": "Request URL.",
+ "type": "string"
+ },
+ {
+ "name": "requestMethod",
+ "description": "Request method.",
+ "type": "string"
+ },
+ {
+ "name": "requestHeaders",
+ "description": "Request headers",
+ "type": "array",
+ "items": {
+ "$ref": "Header"
+ }
+ },
+ {
+ "name": "responseTime",
+ "description": "Number of seconds since epoch.",
+ "type": "number"
+ },
+ {
+ "name": "responseStatus",
+ "description": "HTTP response status code.",
+ "type": "integer"
+ },
+ {
+ "name": "responseStatusText",
+ "description": "HTTP response status text.",
+ "type": "string"
+ },
+ {
+ "name": "responseType",
+ "description": "HTTP response type",
+ "$ref": "CachedResponseType"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Response headers",
+ "type": "array",
+ "items": {
+ "$ref": "Header"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Cache",
+ "description": "Cache identifier.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "cacheId",
+ "description": "An opaque unique id of the cache.",
+ "$ref": "CacheId"
+ },
+ {
+ "name": "securityOrigin",
+ "description": "Security origin of the cache.",
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key of the cache.",
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket of the cache.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "cacheName",
+ "description": "The name of the cache.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Header",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CachedResponse",
+ "description": "Cached response",
+ "type": "object",
+ "properties": [
+ {
+ "name": "body",
+ "description": "Entry content, base64-encoded. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "deleteCache",
+ "description": "Deletes a cache.",
+ "parameters": [
+ {
+ "name": "cacheId",
+ "description": "Id of cache for deletion.",
+ "$ref": "CacheId"
+ }
+ ]
+ },
+ {
+ "name": "deleteEntry",
+ "description": "Deletes a cache entry.",
+ "parameters": [
+ {
+ "name": "cacheId",
+ "description": "Id of cache where the entry will be deleted.",
+ "$ref": "CacheId"
+ },
+ {
+ "name": "request",
+ "description": "URL spec of the request.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "requestCacheNames",
+ "description": "Requests cache names.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ }
+ ],
+ "returns": [
+ {
+ "name": "caches",
+ "description": "Caches for the security origin.",
+ "type": "array",
+ "items": {
+ "$ref": "Cache"
+ }
+ }
+ ]
+ },
+ {
+ "name": "requestCachedResponse",
+ "description": "Fetches cache entry.",
+ "parameters": [
+ {
+ "name": "cacheId",
+ "description": "Id of cache that contains the entry.",
+ "$ref": "CacheId"
+ },
+ {
+ "name": "requestURL",
+ "description": "URL spec of the request.",
+ "type": "string"
+ },
+ {
+ "name": "requestHeaders",
+ "description": "headers of the request.",
+ "type": "array",
+ "items": {
+ "$ref": "Header"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "response",
+ "description": "Response read from the cache.",
+ "$ref": "CachedResponse"
+ }
+ ]
+ },
+ {
+ "name": "requestEntries",
+ "description": "Requests data from cache.",
+ "parameters": [
+ {
+ "name": "cacheId",
+ "description": "ID of cache to get entries from.",
+ "$ref": "CacheId"
+ },
+ {
+ "name": "skipCount",
+ "description": "Number of records to skip.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pageSize",
+ "description": "Number of records to fetch.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pathFilter",
+ "description": "If present, only return the entries containing this substring in the path",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "cacheDataEntries",
+ "description": "Array of object store data entries.",
+ "type": "array",
+ "items": {
+ "$ref": "DataEntry"
+ }
+ },
+ {
+ "name": "returnCount",
+ "description": "Count of returned entries from this storage. If pathFilter is empty, it\nis the count of all entries from this storage.",
+ "type": "number"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Cast",
+ "description": "A domain for interacting with Cast, Presentation API, and Remote Playback API\nfunctionalities.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "Sink",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "id",
+ "type": "string"
+ },
+ {
+ "name": "session",
+ "description": "Text describing the current session. Present only if there is an active\nsession on the sink.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Starts observing for sinks that can be used for tab mirroring, and if set,\nsinks compatible with |presentationUrl| as well. When sinks are found, a\n|sinksUpdated| event is fired.\nAlso starts observing for issue messages. When an issue is added or removed,\nan |issueUpdated| event is fired.",
+ "parameters": [
+ {
+ "name": "presentationUrl",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Stops observing for sinks and issues."
+ },
+ {
+ "name": "setSinkToUse",
+ "description": "Sets a sink to be used when the web page requests the browser to choose a\nsink via Presentation API, Remote Playback API, or Cast SDK.",
+ "parameters": [
+ {
+ "name": "sinkName",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "startDesktopMirroring",
+ "description": "Starts mirroring the desktop to the sink.",
+ "parameters": [
+ {
+ "name": "sinkName",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "startTabMirroring",
+ "description": "Starts mirroring the tab to the sink.",
+ "parameters": [
+ {
+ "name": "sinkName",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "stopCasting",
+ "description": "Stops the active Cast session on the sink.",
+ "parameters": [
+ {
+ "name": "sinkName",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "sinksUpdated",
+ "description": "This is fired whenever the list of available sinks changes. A sink is a\ndevice or a software surface that you can cast to.",
+ "parameters": [
+ {
+ "name": "sinks",
+ "type": "array",
+ "items": {
+ "$ref": "Sink"
+ }
+ }
+ ]
+ },
+ {
+ "name": "issueUpdated",
+ "description": "This is fired whenever the outstanding issue/error message changes.\n|issueMessage| is empty if there is no issue.",
+ "parameters": [
+ {
+ "name": "issueMessage",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "DOM",
+ "description": "This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object\nthat has an `id`. This `id` can be used to get additional information on the Node, resolve it into\nthe JavaScript object wrapper, etc. It is important that client receives DOM events only for the\nnodes that are known to the client. Backend keeps track of the nodes that were sent to the client\nand never sends the same node twice. It is client's responsibility to collect information about\nthe nodes that were sent to the client. Note that `iframe` owner elements will return\ncorresponding document elements as their child nodes.",
+ "dependencies": [
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "NodeId",
+ "description": "Unique DOM node identifier.",
+ "type": "integer"
+ },
+ {
+ "id": "BackendNodeId",
+ "description": "Unique DOM node identifier used to reference a node that may not have been pushed to the\nfront-end.",
+ "type": "integer"
+ },
+ {
+ "id": "BackendNode",
+ "description": "Backend node with a friendly name.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeType",
+ "description": "`Node`'s nodeType.",
+ "type": "integer"
+ },
+ {
+ "name": "nodeName",
+ "description": "`Node`'s nodeName.",
+ "type": "string"
+ },
+ {
+ "name": "backendNodeId",
+ "$ref": "BackendNodeId"
+ }
+ ]
+ },
+ {
+ "id": "PseudoType",
+ "description": "Pseudo element type.",
+ "type": "string",
+ "enum": [
+ "first-line",
+ "first-letter",
+ "before",
+ "after",
+ "marker",
+ "backdrop",
+ "selection",
+ "target-text",
+ "spelling-error",
+ "grammar-error",
+ "highlight",
+ "first-line-inherited",
+ "scrollbar",
+ "scrollbar-thumb",
+ "scrollbar-button",
+ "scrollbar-track",
+ "scrollbar-track-piece",
+ "scrollbar-corner",
+ "resizer",
+ "input-list-button",
+ "view-transition",
+ "view-transition-group",
+ "view-transition-image-pair",
+ "view-transition-old",
+ "view-transition-new"
+ ]
+ },
+ {
+ "id": "ShadowRootType",
+ "description": "Shadow root type.",
+ "type": "string",
+ "enum": [
+ "user-agent",
+ "open",
+ "closed"
+ ]
+ },
+ {
+ "id": "CompatibilityMode",
+ "description": "Document compatibility mode.",
+ "type": "string",
+ "enum": [
+ "QuirksMode",
+ "LimitedQuirksMode",
+ "NoQuirksMode"
+ ]
+ },
+ {
+ "id": "PhysicalAxes",
+ "description": "ContainerSelector physical axes",
+ "type": "string",
+ "enum": [
+ "Horizontal",
+ "Vertical",
+ "Both"
+ ]
+ },
+ {
+ "id": "LogicalAxes",
+ "description": "ContainerSelector logical axes",
+ "type": "string",
+ "enum": [
+ "Inline",
+ "Block",
+ "Both"
+ ]
+ },
+ {
+ "id": "Node",
+ "description": "DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes.\nDOMNode is a base node mirror type.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeId",
+ "description": "Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend\nwill only push node with given `id` once. It is aware of all requested nodes and will only\nfire DOM events for nodes known to the client.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "parentId",
+ "description": "The id of the parent node if any.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "The BackendNodeId for this node.",
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "nodeType",
+ "description": "`Node`'s nodeType.",
+ "type": "integer"
+ },
+ {
+ "name": "nodeName",
+ "description": "`Node`'s nodeName.",
+ "type": "string"
+ },
+ {
+ "name": "localName",
+ "description": "`Node`'s localName.",
+ "type": "string"
+ },
+ {
+ "name": "nodeValue",
+ "description": "`Node`'s nodeValue.",
+ "type": "string"
+ },
+ {
+ "name": "childNodeCount",
+ "description": "Child count for `Container` nodes.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "children",
+ "description": "Child nodes of this node when requested with children.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Node"
+ }
+ },
+ {
+ "name": "attributes",
+ "description": "Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "documentURL",
+ "description": "Document URL that `Document` or `FrameOwner` node points to.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "baseURL",
+ "description": "Base URL that `Document` or `FrameOwner` node uses for URL completion.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "publicId",
+ "description": "`DocumentType`'s publicId.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "systemId",
+ "description": "`DocumentType`'s systemId.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "internalSubset",
+ "description": "`DocumentType`'s internalSubset.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "xmlVersion",
+ "description": "`Document`'s XML version in case of XML documents.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "`Attr`'s name.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "`Attr`'s value.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "pseudoType",
+ "description": "Pseudo element type for this node.",
+ "optional": true,
+ "$ref": "PseudoType"
+ },
+ {
+ "name": "pseudoIdentifier",
+ "description": "Pseudo element identifier for this node. Only present if there is a\nvalid pseudoType.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "shadowRootType",
+ "description": "Shadow root type.",
+ "optional": true,
+ "$ref": "ShadowRootType"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame ID for frame owner elements.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "contentDocument",
+ "description": "Content document for frame owner elements.",
+ "optional": true,
+ "$ref": "Node"
+ },
+ {
+ "name": "shadowRoots",
+ "description": "Shadow root list for given element host.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Node"
+ }
+ },
+ {
+ "name": "templateContent",
+ "description": "Content document fragment for template elements.",
+ "optional": true,
+ "$ref": "Node"
+ },
+ {
+ "name": "pseudoElements",
+ "description": "Pseudo elements associated with this node.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Node"
+ }
+ },
+ {
+ "name": "importedDocument",
+ "description": "Deprecated, as the HTML Imports API has been removed (crbug.com/937746).\nThis property used to return the imported document for the HTMLImport links.\nThe property is always undefined now.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Node"
+ },
+ {
+ "name": "distributedNodes",
+ "description": "Distributed nodes for given insertion point.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "BackendNode"
+ }
+ },
+ {
+ "name": "isSVG",
+ "description": "Whether the node is SVG.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "compatibilityMode",
+ "optional": true,
+ "$ref": "CompatibilityMode"
+ },
+ {
+ "name": "assignedSlot",
+ "optional": true,
+ "$ref": "BackendNode"
+ }
+ ]
+ },
+ {
+ "id": "RGBA",
+ "description": "A structure holding an RGBA color.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "r",
+ "description": "The red component, in the [0-255] range.",
+ "type": "integer"
+ },
+ {
+ "name": "g",
+ "description": "The green component, in the [0-255] range.",
+ "type": "integer"
+ },
+ {
+ "name": "b",
+ "description": "The blue component, in the [0-255] range.",
+ "type": "integer"
+ },
+ {
+ "name": "a",
+ "description": "The alpha component, in the [0-1] range (default: 1).",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "Quad",
+ "description": "An array of quad vertices, x immediately followed by y for each point, points clock-wise.",
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "id": "BoxModel",
+ "description": "Box model.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "content",
+ "description": "Content box",
+ "$ref": "Quad"
+ },
+ {
+ "name": "padding",
+ "description": "Padding box",
+ "$ref": "Quad"
+ },
+ {
+ "name": "border",
+ "description": "Border box",
+ "$ref": "Quad"
+ },
+ {
+ "name": "margin",
+ "description": "Margin box",
+ "$ref": "Quad"
+ },
+ {
+ "name": "width",
+ "description": "Node width",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Node height",
+ "type": "integer"
+ },
+ {
+ "name": "shapeOutside",
+ "description": "Shape outside coordinates",
+ "optional": true,
+ "$ref": "ShapeOutsideInfo"
+ }
+ ]
+ },
+ {
+ "id": "ShapeOutsideInfo",
+ "description": "CSS Shape Outside details.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "bounds",
+ "description": "Shape bounds",
+ "$ref": "Quad"
+ },
+ {
+ "name": "shape",
+ "description": "Shape coordinate details",
+ "type": "array",
+ "items": {
+ "type": "any"
+ }
+ },
+ {
+ "name": "marginShape",
+ "description": "Margin shape bounds",
+ "type": "array",
+ "items": {
+ "type": "any"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Rect",
+ "description": "Rectangle.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "x",
+ "description": "X coordinate",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate",
+ "type": "number"
+ },
+ {
+ "name": "width",
+ "description": "Rectangle width",
+ "type": "number"
+ },
+ {
+ "name": "height",
+ "description": "Rectangle height",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "CSSComputedStyleProperty",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Computed style property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Computed style property value.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "collectClassNamesFromSubtree",
+ "description": "Collects class names for the node with given id and all of it's child nodes.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to collect class names.",
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "classNames",
+ "description": "Class name list.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "copyTo",
+ "description": "Creates a deep copy of the specified node and places it into the target container before the\ngiven anchor.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to copy.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "targetNodeId",
+ "description": "Id of the element to drop the copy into.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "insertBeforeNodeId",
+ "description": "Drop the copy before this node (if absent, the copy becomes the last child of\n`targetNodeId`).",
+ "optional": true,
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node clone.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "describeNode",
+ "description": "Describes node given its id, does not require domain to be enabled. Does not start tracking any\nobjects, can be used for automation.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "depth",
+ "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "node",
+ "description": "Node description.",
+ "$ref": "Node"
+ }
+ ]
+ },
+ {
+ "name": "scrollIntoViewIfNeeded",
+ "description": "Scrolls the specified rect of the given node into view if not already visible.\nNote: exactly one between nodeId, backendNodeId and objectId should be passed\nto identify the node.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "rect",
+ "description": "The rect to be scrolled into view, relative to the node's border box, in CSS pixels.\nWhen omitted, center of the node will be used, similar to Element.scrollIntoView.",
+ "optional": true,
+ "$ref": "Rect"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables DOM agent for the given page."
+ },
+ {
+ "name": "discardSearchResults",
+ "description": "Discards search results from the session with the given id. `getSearchResults` should no longer\nbe called for that search.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "searchId",
+ "description": "Unique search session identifier.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "enable",
+ "description": "Enables DOM agent for the given page.",
+ "parameters": [
+ {
+ "name": "includeWhitespace",
+ "description": "Whether to include whitespaces in the children array of returned Nodes.",
+ "experimental": true,
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "none",
+ "all"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "focus",
+ "description": "Focuses the given element.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "name": "getAttributes",
+ "description": "Returns attributes for the specified node.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to retrieve attibutes for.",
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "attributes",
+ "description": "An interleaved array of node attribute names and values.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getBoxModel",
+ "description": "Returns boxes for the given node.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "model",
+ "description": "Box model for the node.",
+ "$ref": "BoxModel"
+ }
+ ]
+ },
+ {
+ "name": "getContentQuads",
+ "description": "Returns quads that describe node position on the page. This method\nmight return multiple quads for inline nodes.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "quads",
+ "description": "Quads that describe node layout relative to viewport.",
+ "type": "array",
+ "items": {
+ "$ref": "Quad"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getDocument",
+ "description": "Returns the root DOM node (and optionally the subtree) to the caller.\nImplicitly enables the DOM domain events for the current target.",
+ "parameters": [
+ {
+ "name": "depth",
+ "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "root",
+ "description": "Resulting node.",
+ "$ref": "Node"
+ }
+ ]
+ },
+ {
+ "name": "getFlattenedDocument",
+ "description": "Returns the root DOM node (and optionally the subtree) to the caller.\nDeprecated, as it is not designed to work well with the rest of the DOM agent.\nUse DOMSnapshot.captureSnapshot instead.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "depth",
+ "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "description": "Resulting node.",
+ "type": "array",
+ "items": {
+ "$ref": "Node"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getNodesForSubtreeByStyle",
+ "description": "Finds nodes with a given computed style in a subtree.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Node ID pointing to the root of a subtree.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "computedStyles",
+ "description": "The style to filter nodes by (includes nodes if any of properties matches).",
+ "type": "array",
+ "items": {
+ "$ref": "CSSComputedStyleProperty"
+ }
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots in the same target should be traversed when returning the\nresults (default is false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "Resulting nodes.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getNodeForLocation",
+ "description": "Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is\neither returned or not.",
+ "parameters": [
+ {
+ "name": "x",
+ "description": "X coordinate.",
+ "type": "integer"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate.",
+ "type": "integer"
+ },
+ {
+ "name": "includeUserAgentShadowDOM",
+ "description": "False to skip to the nearest non-UA shadow root ancestor (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "ignorePointerEventsNone",
+ "description": "Whether to ignore pointer-events: none on elements and hit test them.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "backendNodeId",
+ "description": "Resulting node.",
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame this node belongs to.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "nodeId",
+ "description": "Id of the node at given coordinates, only when enabled and requested document.",
+ "optional": true,
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "getOuterHTML",
+ "description": "Returns node's HTML markup.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "outerHTML",
+ "description": "Outer HTML markup.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getRelayoutBoundary",
+ "description": "Returns the id of the nearest ancestor that is a relayout boundary.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node.",
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "Relayout boundary node id for the given node.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "getSearchResults",
+ "description": "Returns search results from given `fromIndex` to given `toIndex` from the search with the given\nidentifier.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "searchId",
+ "description": "Unique search session identifier.",
+ "type": "string"
+ },
+ {
+ "name": "fromIndex",
+ "description": "Start index of the search result to be returned.",
+ "type": "integer"
+ },
+ {
+ "name": "toIndex",
+ "description": "End index of the search result to be returned.",
+ "type": "integer"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "Ids of the search result nodes.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "hideHighlight",
+ "description": "Hides any highlight.",
+ "redirect": "Overlay"
+ },
+ {
+ "name": "highlightNode",
+ "description": "Highlights DOM node.",
+ "redirect": "Overlay"
+ },
+ {
+ "name": "highlightRect",
+ "description": "Highlights given rectangle.",
+ "redirect": "Overlay"
+ },
+ {
+ "name": "markUndoableState",
+ "description": "Marks last undoable state.",
+ "experimental": true
+ },
+ {
+ "name": "moveTo",
+ "description": "Moves node into the new container, places it before the given anchor.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to move.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "targetNodeId",
+ "description": "Id of the element to drop the moved node into.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "insertBeforeNodeId",
+ "description": "Drop node before this one (if absent, the moved node becomes the last child of\n`targetNodeId`).",
+ "optional": true,
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "New id of the moved node.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "performSearch",
+ "description": "Searches for a given string in the DOM tree. Use `getSearchResults` to access search results or\n`cancelSearch` to end this search session.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "query",
+ "description": "Plain text or query selector or XPath search query.",
+ "type": "string"
+ },
+ {
+ "name": "includeUserAgentShadowDOM",
+ "description": "True to search in user agent shadow DOM.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "searchId",
+ "description": "Unique search session identifier.",
+ "type": "string"
+ },
+ {
+ "name": "resultCount",
+ "description": "Number of search results.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "pushNodeByPathToFrontend",
+ "description": "Requests that the node is sent to the caller given its path. // FIXME, use XPath",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "path",
+ "description": "Path to node in the proprietary format.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node for given path.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "pushNodesByBackendIdsToFrontend",
+ "description": "Requests that a batch of nodes is sent to the caller given their backend node ids.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "backendNodeIds",
+ "description": "The array of backend node ids.",
+ "type": "array",
+ "items": {
+ "$ref": "BackendNodeId"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "The array of ids of pushed nodes that correspond to the backend ids specified in\nbackendNodeIds.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "querySelector",
+ "description": "Executes `querySelector` on a given node.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to query upon.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "selector",
+ "description": "Selector string.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "Query selector result.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "querySelectorAll",
+ "description": "Executes `querySelectorAll` on a given node.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to query upon.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "selector",
+ "description": "Selector string.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "Query selector result.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getTopLayerElements",
+ "description": "Returns NodeIds of current top layer elements.\nTop layer is rendered closest to the user within a viewport, therefore its elements always\nappear on top of all other content.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "NodeIds of top layer elements",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "redo",
+ "description": "Re-does the last undone action.",
+ "experimental": true
+ },
+ {
+ "name": "removeAttribute",
+ "description": "Removes attribute with given name from an element with given id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the element to remove attribute from.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "name",
+ "description": "Name of the attribute to remove.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeNode",
+ "description": "Removes node with given id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to remove.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "requestChildNodes",
+ "description": "Requests that children of the node with given id are returned to the caller in form of\n`setChildNodes` events where not only immediate children are retrieved, but all children down to\nthe specified depth.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to get children for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "depth",
+ "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots should be traversed when returning the sub-tree\n(default is false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "requestNode",
+ "description": "Requests that the node is sent to the caller given the JavaScript node object reference. All\nnodes that form the path from the node to the root are also sent to the client as a series of\n`setChildNodes` notifications.",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "JavaScript object id to convert into node.",
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "Node id for given object.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "resolveNode",
+ "description": "Resolves the JavaScript node object for a given NodeId or BackendNodeId.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to resolve.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Backend identifier of the node to resolve.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release multiple objects.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Execution context in which to resolve the node.",
+ "optional": true,
+ "$ref": "Runtime.ExecutionContextId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "object",
+ "description": "JavaScript object wrapper for given node.",
+ "$ref": "Runtime.RemoteObject"
+ }
+ ]
+ },
+ {
+ "name": "setAttributeValue",
+ "description": "Sets attribute for an element with given id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the element to set attribute for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "name",
+ "description": "Attribute name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Attribute value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setAttributesAsText",
+ "description": "Sets attributes on element with given id. This method is useful when user edits some existing\nattribute value and types in several attribute name/value pairs.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the element to set attributes for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "text",
+ "description": "Text with a number of attributes. Will parse this text using HTML parser.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Attribute name to replace with new attributes derived from text in case text parsed\nsuccessfully.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setFileInputFiles",
+ "description": "Sets files for the given file input element.",
+ "parameters": [
+ {
+ "name": "files",
+ "description": "Array of file paths to set.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "name": "setNodeStackTracesEnabled",
+ "description": "Sets if stack traces should be captured for Nodes. See `Node.getNodeStackTraces`. Default is disabled.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enable",
+ "description": "Enable or disable.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getNodeStackTraces",
+ "description": "Gets stack traces associated with a Node. As of now, only provides stack trace for Node creation.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to get stack traces for.",
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "creation",
+ "description": "Creation stack trace, if available.",
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ }
+ ]
+ },
+ {
+ "name": "getFileInfo",
+ "description": "Returns file information for the given\nFile wrapper.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "path",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setInspectedNode",
+ "description": "Enables console to refer to the node with given id via $x (see Command Line API for more details\n$x functions).",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "DOM node id to be accessible by means of $x command line API.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "setNodeName",
+ "description": "Sets node name for a node with given id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to set name for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "name",
+ "description": "New node's name.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "New node's id.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "setNodeValue",
+ "description": "Sets node value for a node with given id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to set value for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "value",
+ "description": "New node's value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setOuterHTML",
+ "description": "Sets node HTML markup, returns new node id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to set markup for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "outerHTML",
+ "description": "Outer HTML markup to set.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "undo",
+ "description": "Undoes the last performed action.",
+ "experimental": true
+ },
+ {
+ "name": "getFrameOwner",
+ "description": "Returns iframe node that owns iframe with the given domain.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "backendNodeId",
+ "description": "Resulting node.",
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "nodeId",
+ "description": "Id of the node at given coordinates, only when enabled and requested document.",
+ "optional": true,
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "getContainerForNode",
+ "description": "Returns the query container of the given node based on container query\nconditions: containerName, physical, and logical axes. If no axes are\nprovided, the style container is returned, which is the direct parent or the\nclosest element with a matching container-name.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "containerName",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "physicalAxes",
+ "optional": true,
+ "$ref": "PhysicalAxes"
+ },
+ {
+ "name": "logicalAxes",
+ "optional": true,
+ "$ref": "LogicalAxes"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "The container node for the given node, or null if not found.",
+ "optional": true,
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "getQueryingDescendantsForContainer",
+ "description": "Returns the descendants of a container query container that have\ncontainer queries against this container.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the container node to find querying descendants from.",
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "Descendant nodes with container queries against the given container.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "attributeModified",
+ "description": "Fired when `Element`'s attribute is modified.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node that has changed.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "name",
+ "description": "Attribute name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Attribute value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "attributeRemoved",
+ "description": "Fired when `Element`'s attribute is removed.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node that has changed.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "name",
+ "description": "A ttribute name.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "characterDataModified",
+ "description": "Mirrors `DOMCharacterDataModified` event.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node that has changed.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "characterData",
+ "description": "New text value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "childNodeCountUpdated",
+ "description": "Fired when `Container`'s child node count has changed.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node that has changed.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "childNodeCount",
+ "description": "New node count.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "childNodeInserted",
+ "description": "Mirrors `DOMNodeInserted` event.",
+ "parameters": [
+ {
+ "name": "parentNodeId",
+ "description": "Id of the node that has changed.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "previousNodeId",
+ "description": "Id of the previous sibling.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "node",
+ "description": "Inserted node data.",
+ "$ref": "Node"
+ }
+ ]
+ },
+ {
+ "name": "childNodeRemoved",
+ "description": "Mirrors `DOMNodeRemoved` event.",
+ "parameters": [
+ {
+ "name": "parentNodeId",
+ "description": "Parent id.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "nodeId",
+ "description": "Id of the node that has been removed.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "distributedNodesUpdated",
+ "description": "Called when distribution is changed.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "insertionPointId",
+ "description": "Insertion point where distributed nodes were updated.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "distributedNodes",
+ "description": "Distributed nodes for given insertion point.",
+ "type": "array",
+ "items": {
+ "$ref": "BackendNode"
+ }
+ }
+ ]
+ },
+ {
+ "name": "documentUpdated",
+ "description": "Fired when `Document` has been totally updated. Node ids are no longer valid."
+ },
+ {
+ "name": "inlineStyleInvalidated",
+ "description": "Fired when `Element`'s inline style is modified via a CSS property modification.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeIds",
+ "description": "Ids of the nodes for which the inline styles have been invalidated.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "pseudoElementAdded",
+ "description": "Called when a pseudo element is added to an element.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "parentId",
+ "description": "Pseudo element's parent element id.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "pseudoElement",
+ "description": "The added pseudo element.",
+ "$ref": "Node"
+ }
+ ]
+ },
+ {
+ "name": "topLayerElementsUpdated",
+ "description": "Called when top layer elements are changed.",
+ "experimental": true
+ },
+ {
+ "name": "pseudoElementRemoved",
+ "description": "Called when a pseudo element is removed from an element.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "parentId",
+ "description": "Pseudo element's parent element id.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "pseudoElementId",
+ "description": "The removed pseudo element id.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "setChildNodes",
+ "description": "Fired when backend wants to provide client with the missing DOM structure. This happens upon\nmost of the calls requesting node ids.",
+ "parameters": [
+ {
+ "name": "parentId",
+ "description": "Parent node id to populate with children.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "nodes",
+ "description": "Child nodes array.",
+ "type": "array",
+ "items": {
+ "$ref": "Node"
+ }
+ }
+ ]
+ },
+ {
+ "name": "shadowRootPopped",
+ "description": "Called when shadow root is popped from the element.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "hostId",
+ "description": "Host element id.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "rootId",
+ "description": "Shadow root id.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "shadowRootPushed",
+ "description": "Called when shadow root is pushed into the element.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "hostId",
+ "description": "Host element id.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "root",
+ "description": "Shadow root.",
+ "$ref": "Node"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "DOMDebugger",
+ "description": "DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript\nexecution will stop on these operations as if there was a regular breakpoint set.",
+ "dependencies": [
+ "DOM",
+ "Debugger",
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "DOMBreakpointType",
+ "description": "DOM breakpoint type.",
+ "type": "string",
+ "enum": [
+ "subtree-modified",
+ "attribute-modified",
+ "node-removed"
+ ]
+ },
+ {
+ "id": "CSPViolationType",
+ "description": "CSP Violation type.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "trustedtype-sink-violation",
+ "trustedtype-policy-violation"
+ ]
+ },
+ {
+ "id": "EventListener",
+ "description": "Object event listener.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "`EventListener`'s type.",
+ "type": "string"
+ },
+ {
+ "name": "useCapture",
+ "description": "`EventListener`'s useCapture.",
+ "type": "boolean"
+ },
+ {
+ "name": "passive",
+ "description": "`EventListener`'s passive flag.",
+ "type": "boolean"
+ },
+ {
+ "name": "once",
+ "description": "`EventListener`'s once flag.",
+ "type": "boolean"
+ },
+ {
+ "name": "scriptId",
+ "description": "Script id of the handler code.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Line number in the script (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Column number in the script (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "handler",
+ "description": "Event handler function value.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "originalHandler",
+ "description": "Event original handler function value.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Node the listener is added to (if any).",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getEventListeners",
+ "description": "Returns event listeners of the given object.",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Identifier of the object to return listeners for.",
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "depth",
+ "description": "The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false). Reports listeners for all contexts if pierce is enabled.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "listeners",
+ "description": "Array of relevant listeners.",
+ "type": "array",
+ "items": {
+ "$ref": "EventListener"
+ }
+ }
+ ]
+ },
+ {
+ "name": "removeDOMBreakpoint",
+ "description": "Removes DOM breakpoint that was set using `setDOMBreakpoint`.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to remove breakpoint from.",
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "type",
+ "description": "Type of the breakpoint to remove.",
+ "$ref": "DOMBreakpointType"
+ }
+ ]
+ },
+ {
+ "name": "removeEventListenerBreakpoint",
+ "description": "Removes breakpoint on particular DOM event.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "Event name.",
+ "type": "string"
+ },
+ {
+ "name": "targetName",
+ "description": "EventTarget interface name.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeInstrumentationBreakpoint",
+ "description": "Removes breakpoint on particular native event.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "Instrumentation name to stop on.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeXHRBreakpoint",
+ "description": "Removes breakpoint from XMLHttpRequest.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "Resource URL substring.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setBreakOnCSPViolation",
+ "description": "Sets breakpoint on particular CSP violations.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "violationTypes",
+ "description": "CSP Violations to stop upon.",
+ "type": "array",
+ "items": {
+ "$ref": "CSPViolationType"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setDOMBreakpoint",
+ "description": "Sets breakpoint on particular operation with DOM.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to set breakpoint on.",
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "type",
+ "description": "Type of the operation to stop upon.",
+ "$ref": "DOMBreakpointType"
+ }
+ ]
+ },
+ {
+ "name": "setEventListenerBreakpoint",
+ "description": "Sets breakpoint on particular DOM event.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "DOM Event name to stop on (any DOM event will do).",
+ "type": "string"
+ },
+ {
+ "name": "targetName",
+ "description": "EventTarget interface name to stop on. If equal to `\"*\"` or not provided, will stop on any\nEventTarget.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setInstrumentationBreakpoint",
+ "description": "Sets breakpoint on particular native event.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "Instrumentation name to stop on.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setXHRBreakpoint",
+ "description": "Sets breakpoint on XMLHttpRequest.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "Resource URL substring. All XHRs having this substring in the URL will get stopped upon.",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "EventBreakpoints",
+ "description": "EventBreakpoints permits setting breakpoints on particular operations and\nevents in targets that run JavaScript but do not have a DOM.\nJavaScript execution will stop on these operations as if there was a regular\nbreakpoint set.",
+ "experimental": true,
+ "commands": [
+ {
+ "name": "setInstrumentationBreakpoint",
+ "description": "Sets breakpoint on particular native event.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "Instrumentation name to stop on.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeInstrumentationBreakpoint",
+ "description": "Removes breakpoint on particular native event.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "Instrumentation name to stop on.",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "DOMSnapshot",
+ "description": "This domain facilitates obtaining document snapshots with DOM, layout, and style information.",
+ "experimental": true,
+ "dependencies": [
+ "CSS",
+ "DOM",
+ "DOMDebugger",
+ "Page"
+ ],
+ "types": [
+ {
+ "id": "DOMNode",
+ "description": "A Node in the DOM tree.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeType",
+ "description": "`Node`'s nodeType.",
+ "type": "integer"
+ },
+ {
+ "name": "nodeName",
+ "description": "`Node`'s nodeName.",
+ "type": "string"
+ },
+ {
+ "name": "nodeValue",
+ "description": "`Node`'s nodeValue.",
+ "type": "string"
+ },
+ {
+ "name": "textValue",
+ "description": "Only set for textarea elements, contains the text value.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "inputValue",
+ "description": "Only set for input elements, contains the input's associated text value.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "inputChecked",
+ "description": "Only set for radio and checkbox input elements, indicates if the element has been checked",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "optionSelected",
+ "description": "Only set for option elements, indicates if the element has been selected",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "`Node`'s id, corresponds to DOM.Node.backendNodeId.",
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "childNodeIndexes",
+ "description": "The indexes of the node's child nodes in the `domNodes` array returned by `getSnapshot`, if\nany.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "attributes",
+ "description": "Attributes of an `Element` node.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "NameValue"
+ }
+ },
+ {
+ "name": "pseudoElementIndexes",
+ "description": "Indexes of pseudo elements associated with this node in the `domNodes` array returned by\n`getSnapshot`, if any.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "layoutNodeIndex",
+ "description": "The index of the node's related layout tree node in the `layoutTreeNodes` array returned by\n`getSnapshot`, if any.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "documentURL",
+ "description": "Document URL that `Document` or `FrameOwner` node points to.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "baseURL",
+ "description": "Base URL that `Document` or `FrameOwner` node uses for URL completion.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "contentLanguage",
+ "description": "Only set for documents, contains the document's content language.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "documentEncoding",
+ "description": "Only set for documents, contains the document's character set encoding.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "publicId",
+ "description": "`DocumentType` node's publicId.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "systemId",
+ "description": "`DocumentType` node's systemId.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame ID for frame owner elements and also for the document node.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "contentDocumentIndex",
+ "description": "The index of a frame owner element's content document in the `domNodes` array returned by\n`getSnapshot`, if any.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pseudoType",
+ "description": "Type of a pseudo element node.",
+ "optional": true,
+ "$ref": "DOM.PseudoType"
+ },
+ {
+ "name": "shadowRootType",
+ "description": "Shadow root type.",
+ "optional": true,
+ "$ref": "DOM.ShadowRootType"
+ },
+ {
+ "name": "isClickable",
+ "description": "Whether this DOM node responds to mouse clicks. This includes nodes that have had click\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\nclicked.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "eventListeners",
+ "description": "Details of the node's event listeners, if any.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "DOMDebugger.EventListener"
+ }
+ },
+ {
+ "name": "currentSourceURL",
+ "description": "The selected url for nodes with a srcset attribute.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "originURL",
+ "description": "The url of the script (if any) that generates this node.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "scrollOffsetX",
+ "description": "Scroll offsets, set when this node is a Document.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "scrollOffsetY",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "InlineTextBox",
+ "description": "Details of post layout rendered text positions. The exact layout should not be regarded as\nstable and may change between versions.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "boundingBox",
+ "description": "The bounding box in document coordinates. Note that scroll offset of the document is ignored.",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "startCharacterIndex",
+ "description": "The starting index in characters, for this post layout textbox substring. Characters that\nwould be represented as a surrogate pair in UTF-16 have length 2.",
+ "type": "integer"
+ },
+ {
+ "name": "numCharacters",
+ "description": "The number of characters in this post layout textbox substring. Characters that would be\nrepresented as a surrogate pair in UTF-16 have length 2.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "LayoutTreeNode",
+ "description": "Details of an element in the DOM tree with a LayoutObject.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "domNodeIndex",
+ "description": "The index of the related DOM node in the `domNodes` array returned by `getSnapshot`.",
+ "type": "integer"
+ },
+ {
+ "name": "boundingBox",
+ "description": "The bounding box in document coordinates. Note that scroll offset of the document is ignored.",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "layoutText",
+ "description": "Contents of the LayoutText, if any.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "inlineTextNodes",
+ "description": "The post-layout inline text nodes, if any.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "InlineTextBox"
+ }
+ },
+ {
+ "name": "styleIndex",
+ "description": "Index into the `computedStyles` array returned by `getSnapshot`.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "paintOrder",
+ "description": "Global paint order index, which is determined by the stacking order of the nodes. Nodes\nthat are painted together will have the same index. Only provided if includePaintOrder in\ngetSnapshot was true.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "isStackingContext",
+ "description": "Set to true to indicate the element begins a new stacking context.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "ComputedStyle",
+ "description": "A subset of the full ComputedStyle as defined by the request whitelist.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "properties",
+ "description": "Name/value pairs of computed style properties.",
+ "type": "array",
+ "items": {
+ "$ref": "NameValue"
+ }
+ }
+ ]
+ },
+ {
+ "id": "NameValue",
+ "description": "A name/value pair.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Attribute/property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Attribute/property value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "StringIndex",
+ "description": "Index of the string in the strings table.",
+ "type": "integer"
+ },
+ {
+ "id": "ArrayOfStrings",
+ "description": "Index of the string in the strings table.",
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ },
+ {
+ "id": "RareStringData",
+ "description": "Data that is only present on rare nodes.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "index",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "value",
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ }
+ ]
+ },
+ {
+ "id": "RareBooleanData",
+ "type": "object",
+ "properties": [
+ {
+ "name": "index",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "id": "RareIntegerData",
+ "type": "object",
+ "properties": [
+ {
+ "name": "index",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "value",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Rectangle",
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "id": "DocumentSnapshot",
+ "description": "Document snapshot.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "documentURL",
+ "description": "Document URL that `Document` or `FrameOwner` node points to.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "title",
+ "description": "Document title.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "baseURL",
+ "description": "Base URL that `Document` or `FrameOwner` node uses for URL completion.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "contentLanguage",
+ "description": "Contains the document's content language.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "encodingName",
+ "description": "Contains the document's character set encoding.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "publicId",
+ "description": "`DocumentType` node's publicId.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "systemId",
+ "description": "`DocumentType` node's systemId.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame ID for frame owner elements and also for the document node.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "nodes",
+ "description": "A table with dom nodes.",
+ "$ref": "NodeTreeSnapshot"
+ },
+ {
+ "name": "layout",
+ "description": "The nodes in the layout tree.",
+ "$ref": "LayoutTreeSnapshot"
+ },
+ {
+ "name": "textBoxes",
+ "description": "The post-layout inline text nodes.",
+ "$ref": "TextBoxSnapshot"
+ },
+ {
+ "name": "scrollOffsetX",
+ "description": "Horizontal scroll offset.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "scrollOffsetY",
+ "description": "Vertical scroll offset.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "contentWidth",
+ "description": "Document content width.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "contentHeight",
+ "description": "Document content height.",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "NodeTreeSnapshot",
+ "description": "Table containing nodes.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "parentIndex",
+ "description": "Parent node index.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "nodeType",
+ "description": "`Node`'s nodeType.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "shadowRootType",
+ "description": "Type of the shadow root the `Node` is in. String values are equal to the `ShadowRootType` enum.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "nodeName",
+ "description": "`Node`'s nodeName.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ },
+ {
+ "name": "nodeValue",
+ "description": "`Node`'s nodeValue.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ },
+ {
+ "name": "backendNodeId",
+ "description": "`Node`'s id, corresponds to DOM.Node.backendNodeId.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "DOM.BackendNodeId"
+ }
+ },
+ {
+ "name": "attributes",
+ "description": "Attributes of an `Element` node. Flatten name, value pairs.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "ArrayOfStrings"
+ }
+ },
+ {
+ "name": "textValue",
+ "description": "Only set for textarea elements, contains the text value.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "inputValue",
+ "description": "Only set for input elements, contains the input's associated text value.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "inputChecked",
+ "description": "Only set for radio and checkbox input elements, indicates if the element has been checked",
+ "optional": true,
+ "$ref": "RareBooleanData"
+ },
+ {
+ "name": "optionSelected",
+ "description": "Only set for option elements, indicates if the element has been selected",
+ "optional": true,
+ "$ref": "RareBooleanData"
+ },
+ {
+ "name": "contentDocumentIndex",
+ "description": "The index of the document in the list of the snapshot documents.",
+ "optional": true,
+ "$ref": "RareIntegerData"
+ },
+ {
+ "name": "pseudoType",
+ "description": "Type of a pseudo element node.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "pseudoIdentifier",
+ "description": "Pseudo element identifier for this node. Only present if there is a\nvalid pseudoType.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "isClickable",
+ "description": "Whether this DOM node responds to mouse clicks. This includes nodes that have had click\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\nclicked.",
+ "optional": true,
+ "$ref": "RareBooleanData"
+ },
+ {
+ "name": "currentSourceURL",
+ "description": "The selected url for nodes with a srcset attribute.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "originURL",
+ "description": "The url of the script (if any) that generates this node.",
+ "optional": true,
+ "$ref": "RareStringData"
+ }
+ ]
+ },
+ {
+ "id": "LayoutTreeSnapshot",
+ "description": "Table of details of an element in the DOM tree with a LayoutObject.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeIndex",
+ "description": "Index of the corresponding node in the `NodeTreeSnapshot` array returned by `captureSnapshot`.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "styles",
+ "description": "Array of indexes specifying computed style strings, filtered according to the `computedStyles` parameter passed to `captureSnapshot`.",
+ "type": "array",
+ "items": {
+ "$ref": "ArrayOfStrings"
+ }
+ },
+ {
+ "name": "bounds",
+ "description": "The absolute position bounding box.",
+ "type": "array",
+ "items": {
+ "$ref": "Rectangle"
+ }
+ },
+ {
+ "name": "text",
+ "description": "Contents of the LayoutText, if any.",
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ },
+ {
+ "name": "stackingContexts",
+ "description": "Stacking context information.",
+ "$ref": "RareBooleanData"
+ },
+ {
+ "name": "paintOrders",
+ "description": "Global paint order index, which is determined by the stacking order of the nodes. Nodes\nthat are painted together will have the same index. Only provided if includePaintOrder in\ncaptureSnapshot was true.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "offsetRects",
+ "description": "The offset rect of nodes. Only available when includeDOMRects is set to true",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Rectangle"
+ }
+ },
+ {
+ "name": "scrollRects",
+ "description": "The scroll rect of nodes. Only available when includeDOMRects is set to true",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Rectangle"
+ }
+ },
+ {
+ "name": "clientRects",
+ "description": "The client rect of nodes. Only available when includeDOMRects is set to true",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Rectangle"
+ }
+ },
+ {
+ "name": "blendedBackgroundColors",
+ "description": "The list of background colors that are blended with colors of overlapping elements.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ },
+ {
+ "name": "textColorOpacities",
+ "description": "The list of computed text opacities.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ }
+ ]
+ },
+ {
+ "id": "TextBoxSnapshot",
+ "description": "Table of details of the post layout rendered text positions. The exact layout should not be regarded as\nstable and may change between versions.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "layoutIndex",
+ "description": "Index of the layout tree node that owns this box collection.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "bounds",
+ "description": "The absolute position bounding box.",
+ "type": "array",
+ "items": {
+ "$ref": "Rectangle"
+ }
+ },
+ {
+ "name": "start",
+ "description": "The starting index in characters, for this post layout textbox substring. Characters that\nwould be represented as a surrogate pair in UTF-16 have length 2.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "length",
+ "description": "The number of characters in this post layout textbox substring. Characters that would be\nrepresented as a surrogate pair in UTF-16 have length 2.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables DOM snapshot agent for the given page."
+ },
+ {
+ "name": "enable",
+ "description": "Enables DOM snapshot agent for the given page."
+ },
+ {
+ "name": "getSnapshot",
+ "description": "Returns a document snapshot, including the full DOM tree of the root node (including iframes,\ntemplate contents, and imported documents) in a flattened array, as well as layout and\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\nflattened.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "computedStyleWhitelist",
+ "description": "Whitelist of computed styles to return.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "includeEventListeners",
+ "description": "Whether or not to retrieve details of DOM listeners (default false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includePaintOrder",
+ "description": "Whether to determine and include the paint order index of LayoutTreeNodes (default false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeUserAgentShadowTree",
+ "description": "Whether to include UA shadow tree in the snapshot (default false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "domNodes",
+ "description": "The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.",
+ "type": "array",
+ "items": {
+ "$ref": "DOMNode"
+ }
+ },
+ {
+ "name": "layoutTreeNodes",
+ "description": "The nodes in the layout tree.",
+ "type": "array",
+ "items": {
+ "$ref": "LayoutTreeNode"
+ }
+ },
+ {
+ "name": "computedStyles",
+ "description": "Whitelisted ComputedStyle properties for each node in the layout tree.",
+ "type": "array",
+ "items": {
+ "$ref": "ComputedStyle"
+ }
+ }
+ ]
+ },
+ {
+ "name": "captureSnapshot",
+ "description": "Returns a document snapshot, including the full DOM tree of the root node (including iframes,\ntemplate contents, and imported documents) in a flattened array, as well as layout and\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\nflattened.",
+ "parameters": [
+ {
+ "name": "computedStyles",
+ "description": "Whitelist of computed styles to return.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "includePaintOrder",
+ "description": "Whether to include layout object paint orders into the snapshot.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeDOMRects",
+ "description": "Whether to include DOM rectangles (offsetRects, clientRects, scrollRects) into the snapshot",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeBlendedBackgroundColors",
+ "description": "Whether to include blended background colors in the snapshot (default: false).\nBlended background color is achieved by blending background colors of all elements\nthat overlap with the current element.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeTextColorOpacities",
+ "description": "Whether to include text color opacity in the snapshot (default: false).\nAn element might have the opacity property set that affects the text color of the element.\nThe final text color opacity is computed based on the opacity of all overlapping elements.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "documents",
+ "description": "The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.",
+ "type": "array",
+ "items": {
+ "$ref": "DocumentSnapshot"
+ }
+ },
+ {
+ "name": "strings",
+ "description": "Shared string table that all string properties refer to with indexes.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "DOMStorage",
+ "description": "Query and modify DOM storage.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "SerializedStorageKey",
+ "type": "string"
+ },
+ {
+ "id": "StorageId",
+ "description": "DOM Storage identifier.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "securityOrigin",
+ "description": "Security origin for the storage.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Represents a key by which DOM Storage keys its CachedStorageAreas",
+ "optional": true,
+ "$ref": "SerializedStorageKey"
+ },
+ {
+ "name": "isLocalStorage",
+ "description": "Whether the storage is local storage (not session storage).",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "Item",
+ "description": "DOM Storage item.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
+ "commands": [
+ {
+ "name": "clear",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables storage tracking, prevents storage events from being sent to the client."
+ },
+ {
+ "name": "enable",
+ "description": "Enables storage tracking, storage events will now be delivered to the client."
+ },
+ {
+ "name": "getDOMStorageItems",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "entries",
+ "type": "array",
+ "items": {
+ "$ref": "Item"
+ }
+ }
+ ]
+ },
+ {
+ "name": "removeDOMStorageItem",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setDOMStorageItem",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "domStorageItemAdded",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "newValue",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "domStorageItemRemoved",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "domStorageItemUpdated",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "oldValue",
+ "type": "string"
+ },
+ {
+ "name": "newValue",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "domStorageItemsCleared",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Database",
+ "experimental": true,
+ "types": [
+ {
+ "id": "DatabaseId",
+ "description": "Unique identifier of Database object.",
+ "type": "string"
+ },
+ {
+ "id": "Database",
+ "description": "Database object.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "Database ID.",
+ "$ref": "DatabaseId"
+ },
+ {
+ "name": "domain",
+ "description": "Database domain.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Database name.",
+ "type": "string"
+ },
+ {
+ "name": "version",
+ "description": "Database version.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Error",
+ "description": "Database error.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "message",
+ "description": "Error message.",
+ "type": "string"
+ },
+ {
+ "name": "code",
+ "description": "Error code.",
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables database tracking, prevents database events from being sent to the client."
+ },
+ {
+ "name": "enable",
+ "description": "Enables database tracking, database events will now be delivered to the client."
+ },
+ {
+ "name": "executeSQL",
+ "parameters": [
+ {
+ "name": "databaseId",
+ "$ref": "DatabaseId"
+ },
+ {
+ "name": "query",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "columnNames",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "values",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "any"
+ }
+ },
+ {
+ "name": "sqlError",
+ "optional": true,
+ "$ref": "Error"
+ }
+ ]
+ },
+ {
+ "name": "getDatabaseTableNames",
+ "parameters": [
+ {
+ "name": "databaseId",
+ "$ref": "DatabaseId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "tableNames",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "addDatabase",
+ "parameters": [
+ {
+ "name": "database",
+ "$ref": "Database"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "DeviceOrientation",
+ "experimental": true,
+ "commands": [
+ {
+ "name": "clearDeviceOrientationOverride",
+ "description": "Clears the overridden Device Orientation."
+ },
+ {
+ "name": "setDeviceOrientationOverride",
+ "description": "Overrides the Device Orientation.",
+ "parameters": [
+ {
+ "name": "alpha",
+ "description": "Mock alpha",
+ "type": "number"
+ },
+ {
+ "name": "beta",
+ "description": "Mock beta",
+ "type": "number"
+ },
+ {
+ "name": "gamma",
+ "description": "Mock gamma",
+ "type": "number"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Emulation",
+ "description": "This domain emulates different environments for the page.",
+ "dependencies": [
+ "DOM",
+ "Page",
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "ScreenOrientation",
+ "description": "Screen orientation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Orientation type.",
+ "type": "string",
+ "enum": [
+ "portraitPrimary",
+ "portraitSecondary",
+ "landscapePrimary",
+ "landscapeSecondary"
+ ]
+ },
+ {
+ "name": "angle",
+ "description": "Orientation angle.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "DisplayFeature",
+ "type": "object",
+ "properties": [
+ {
+ "name": "orientation",
+ "description": "Orientation of a display feature in relation to screen",
+ "type": "string",
+ "enum": [
+ "vertical",
+ "horizontal"
+ ]
+ },
+ {
+ "name": "offset",
+ "description": "The offset from the screen origin in either the x (for vertical\norientation) or y (for horizontal orientation) direction.",
+ "type": "integer"
+ },
+ {
+ "name": "maskLength",
+ "description": "A display feature may mask content such that it is not physically\ndisplayed - this length along with the offset describes this area.\nA display feature that only splits content will have a 0 mask_length.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "MediaFeature",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "VirtualTimePolicy",
+ "description": "advance: If the scheduler runs out of immediate work, the virtual time base may fast forward to\nallow the next delayed task (if any) to run; pause: The virtual time base may not advance;\npauseIfNetworkFetchesPending: The virtual time base may not advance if there are any pending\nresource fetches.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "advance",
+ "pause",
+ "pauseIfNetworkFetchesPending"
+ ]
+ },
+ {
+ "id": "UserAgentBrandVersion",
+ "description": "Used to specify User Agent Cient Hints to emulate. See https://wicg.github.io/ua-client-hints",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "brand",
+ "type": "string"
+ },
+ {
+ "name": "version",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "UserAgentMetadata",
+ "description": "Used to specify User Agent Cient Hints to emulate. See https://wicg.github.io/ua-client-hints\nMissing optional values will be filled in by the target with what it would normally use.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "brands",
+ "description": "Brands appearing in Sec-CH-UA.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "UserAgentBrandVersion"
+ }
+ },
+ {
+ "name": "fullVersionList",
+ "description": "Brands appearing in Sec-CH-UA-Full-Version-List.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "UserAgentBrandVersion"
+ }
+ },
+ {
+ "name": "fullVersion",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "platform",
+ "type": "string"
+ },
+ {
+ "name": "platformVersion",
+ "type": "string"
+ },
+ {
+ "name": "architecture",
+ "type": "string"
+ },
+ {
+ "name": "model",
+ "type": "string"
+ },
+ {
+ "name": "mobile",
+ "type": "boolean"
+ },
+ {
+ "name": "bitness",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "wow64",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "DisabledImageType",
+ "description": "Enum of image types that can be disabled.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "avif",
+ "webp"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "canEmulate",
+ "description": "Tells whether emulation is supported.",
+ "returns": [
+ {
+ "name": "result",
+ "description": "True if emulation is supported.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "clearDeviceMetricsOverride",
+ "description": "Clears the overridden device metrics."
+ },
+ {
+ "name": "clearGeolocationOverride",
+ "description": "Clears the overridden Geolocation Position and Error."
+ },
+ {
+ "name": "resetPageScaleFactor",
+ "description": "Requests that page scale factor is reset to initial values.",
+ "experimental": true
+ },
+ {
+ "name": "setFocusEmulationEnabled",
+ "description": "Enables or disables simulating a focused and active page.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether to enable to disable focus emulation.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setAutoDarkModeOverride",
+ "description": "Automatically render all web contents using a dark theme.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether to enable or disable automatic dark mode.\nIf not specified, any existing override will be cleared.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setCPUThrottlingRate",
+ "description": "Enables CPU throttling to emulate slow CPUs.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "rate",
+ "description": "Throttling rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc).",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setDefaultBackgroundColorOverride",
+ "description": "Sets or clears an override of the default background color of the frame. This override is used\nif the content does not specify one.",
+ "parameters": [
+ {
+ "name": "color",
+ "description": "RGBA of the default background color. If not specified, any existing override will be\ncleared.",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "name": "setDeviceMetricsOverride",
+ "description": "Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\nwindow.innerWidth, window.innerHeight, and \"device-width\"/\"device-height\"-related CSS media\nquery results).",
+ "parameters": [
+ {
+ "name": "width",
+ "description": "Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.",
+ "type": "integer"
+ },
+ {
+ "name": "deviceScaleFactor",
+ "description": "Overriding device scale factor value. 0 disables the override.",
+ "type": "number"
+ },
+ {
+ "name": "mobile",
+ "description": "Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\nautosizing and more.",
+ "type": "boolean"
+ },
+ {
+ "name": "scale",
+ "description": "Scale to apply to resulting view image.",
+ "experimental": true,
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "screenWidth",
+ "description": "Overriding screen width value in pixels (minimum 0, maximum 10000000).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "screenHeight",
+ "description": "Overriding screen height value in pixels (minimum 0, maximum 10000000).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "positionX",
+ "description": "Overriding view X position on screen in pixels (minimum 0, maximum 10000000).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "positionY",
+ "description": "Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "dontSetVisibleSize",
+ "description": "Do not set visible view size, rely upon explicit setVisibleSize call.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "screenOrientation",
+ "description": "Screen orientation override.",
+ "optional": true,
+ "$ref": "ScreenOrientation"
+ },
+ {
+ "name": "viewport",
+ "description": "If set, the visible area of the page will be overridden to this viewport. This viewport\nchange is not observed by the page, e.g. viewport-relative elements do not change positions.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Page.Viewport"
+ },
+ {
+ "name": "displayFeature",
+ "description": "If set, the display feature of a multi-segment screen. If not set, multi-segment support\nis turned-off.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "DisplayFeature"
+ }
+ ]
+ },
+ {
+ "name": "setScrollbarsHidden",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "hidden",
+ "description": "Whether scrollbars should be always hidden.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setDocumentCookieDisabled",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "disabled",
+ "description": "Whether document.coookie API should be disabled.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setEmitTouchEventsForMouse",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether touch emulation based on mouse input should be enabled.",
+ "type": "boolean"
+ },
+ {
+ "name": "configuration",
+ "description": "Touch/gesture events configuration. Default: current platform.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "mobile",
+ "desktop"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setEmulatedMedia",
+ "description": "Emulates the given media type or media feature for CSS media queries.",
+ "parameters": [
+ {
+ "name": "media",
+ "description": "Media type to emulate. Empty string disables the override.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "features",
+ "description": "Media features to emulate.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "MediaFeature"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setEmulatedVisionDeficiency",
+ "description": "Emulates the given vision deficiency.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Vision deficiency to emulate. Order: best-effort emulations come first, followed by any\nphysiologically accurate emulations for medically recognized color vision deficiencies.",
+ "type": "string",
+ "enum": [
+ "none",
+ "blurredVision",
+ "reducedContrast",
+ "achromatopsia",
+ "deuteranopia",
+ "protanopia",
+ "tritanopia"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setGeolocationOverride",
+ "description": "Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\nunavailable.",
+ "parameters": [
+ {
+ "name": "latitude",
+ "description": "Mock latitude",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "longitude",
+ "description": "Mock longitude",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "accuracy",
+ "description": "Mock accuracy",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setIdleOverride",
+ "description": "Overrides the Idle state.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "isUserActive",
+ "description": "Mock isUserActive",
+ "type": "boolean"
+ },
+ {
+ "name": "isScreenUnlocked",
+ "description": "Mock isScreenUnlocked",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "clearIdleOverride",
+ "description": "Clears Idle state overrides.",
+ "experimental": true
+ },
+ {
+ "name": "setNavigatorOverrides",
+ "description": "Overrides value returned by the javascript navigator object.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "platform",
+ "description": "The platform navigator.platform should return.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setPageScaleFactor",
+ "description": "Sets a specified page scale factor.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "pageScaleFactor",
+ "description": "Page scale factor.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setScriptExecutionDisabled",
+ "description": "Switches script execution in the page.",
+ "parameters": [
+ {
+ "name": "value",
+ "description": "Whether script execution should be disabled in the page.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setTouchEmulationEnabled",
+ "description": "Enables touch on platforms which do not support them.",
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether the touch event emulation should be enabled.",
+ "type": "boolean"
+ },
+ {
+ "name": "maxTouchPoints",
+ "description": "Maximum touch points supported. Defaults to one.",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setVirtualTimePolicy",
+ "description": "Turns on virtual time for all frames (replacing real-time with a synthetic time source) and sets\nthe current virtual time policy. Note this supersedes any previous time budget.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "policy",
+ "$ref": "VirtualTimePolicy"
+ },
+ {
+ "name": "budget",
+ "description": "If set, after this many virtual milliseconds have elapsed virtual time will be paused and a\nvirtualTimeBudgetExpired event is sent.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "maxVirtualTimeTaskStarvationCount",
+ "description": "If set this specifies the maximum number of tasks that can be run before virtual is forced\nforwards to prevent deadlock.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "initialVirtualTime",
+ "description": "If set, base::Time::Now will be overridden to initially return this value.",
+ "optional": true,
+ "$ref": "Network.TimeSinceEpoch"
+ }
+ ],
+ "returns": [
+ {
+ "name": "virtualTimeTicksBase",
+ "description": "Absolute timestamp at which virtual time was first enabled (up time in milliseconds).",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setLocaleOverride",
+ "description": "Overrides default host system locale with the specified one.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "locale",
+ "description": "ICU style C locale (e.g. \"en_US\"). If not specified or empty, disables the override and\nrestores default host system locale.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setTimezoneOverride",
+ "description": "Overrides default host system timezone with the specified one.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "timezoneId",
+ "description": "The timezone identifier. If empty, disables the override and\nrestores default host system timezone.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setVisibleSize",
+ "description": "Resizes the frame/viewport of the page. Note that this does not affect the frame's container\n(e.g. browser window). Can be used to produce screenshots of the specified size. Not supported\non Android.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "width",
+ "description": "Frame width (DIP).",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Frame height (DIP).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setDisabledImageTypes",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "imageTypes",
+ "description": "Image types to disable.",
+ "type": "array",
+ "items": {
+ "$ref": "DisabledImageType"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setHardwareConcurrencyOverride",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "hardwareConcurrency",
+ "description": "Hardware concurrency to report",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setUserAgentOverride",
+ "description": "Allows overriding user agent with the given string.",
+ "parameters": [
+ {
+ "name": "userAgent",
+ "description": "User agent to use.",
+ "type": "string"
+ },
+ {
+ "name": "acceptLanguage",
+ "description": "Browser langugage to emulate.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "platform",
+ "description": "The platform navigator.platform should return.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "userAgentMetadata",
+ "description": "To be sent in Sec-CH-UA-* headers and returned in navigator.userAgentData",
+ "experimental": true,
+ "optional": true,
+ "$ref": "UserAgentMetadata"
+ }
+ ]
+ },
+ {
+ "name": "setAutomationOverride",
+ "description": "Allows overriding the automation flag.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether the override should be enabled.",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "virtualTimeBudgetExpired",
+ "description": "Notification sent after the virtual time budget for the current VirtualTimePolicy has run out.",
+ "experimental": true
+ }
+ ]
+ },
+ {
+ "domain": "HeadlessExperimental",
+ "description": "This domain provides experimental commands only supported in headless mode.",
+ "experimental": true,
+ "dependencies": [
+ "Page",
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "ScreenshotParams",
+ "description": "Encoding options for a screenshot.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "format",
+ "description": "Image compression format (defaults to png).",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "jpeg",
+ "png",
+ "webp"
+ ]
+ },
+ {
+ "name": "quality",
+ "description": "Compression quality from range [0..100] (jpeg and webp only).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "optimizeForSpeed",
+ "description": "Optimize image encoding for speed, not for resulting size (defaults to false)",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "beginFrame",
+ "description": "Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a\nscreenshot from the resulting frame. Requires that the target was created with enabled\nBeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also\nhttps://goo.gle/chrome-headless-rendering for more background.",
+ "parameters": [
+ {
+ "name": "frameTimeTicks",
+ "description": "Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set,\nthe current time will be used.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "interval",
+ "description": "The interval between BeginFrames that is reported to the compositor, in milliseconds.\nDefaults to a 60 frames/second interval, i.e. about 16.666 milliseconds.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "noDisplayUpdates",
+ "description": "Whether updates should not be committed and drawn onto the display. False by default. If\ntrue, only side effects of the BeginFrame will be run, such as layout and animations, but\nany visual updates may not be visible on the display or in screenshots.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "screenshot",
+ "description": "If set, a screenshot of the frame will be captured and returned in the response. Otherwise,\nno screenshot will be captured. Note that capturing a screenshot can fail, for example,\nduring renderer initialization. In such a case, no screenshot data will be returned.",
+ "optional": true,
+ "$ref": "ScreenshotParams"
+ }
+ ],
+ "returns": [
+ {
+ "name": "hasDamage",
+ "description": "Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the\ndisplay. Reported for diagnostic uses, may be removed in the future.",
+ "type": "boolean"
+ },
+ {
+ "name": "screenshotData",
+ "description": "Base64-encoded image data of the screenshot, if one was requested and successfully taken. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables headless events for the target.",
+ "deprecated": true
+ },
+ {
+ "name": "enable",
+ "description": "Enables headless events for the target.",
+ "deprecated": true
+ }
+ ]
+ },
+ {
+ "domain": "IO",
+ "description": "Input/Output operations for streams produced by DevTools.",
+ "types": [
+ {
+ "id": "StreamHandle",
+ "description": "This is either obtained from another method or specified as `blob:<uuid>` where\n`<uuid>` is an UUID of a Blob.",
+ "type": "string"
+ }
+ ],
+ "commands": [
+ {
+ "name": "close",
+ "description": "Close the stream, discard any temporary backing storage.",
+ "parameters": [
+ {
+ "name": "handle",
+ "description": "Handle of the stream to close.",
+ "$ref": "StreamHandle"
+ }
+ ]
+ },
+ {
+ "name": "read",
+ "description": "Read a chunk of the stream",
+ "parameters": [
+ {
+ "name": "handle",
+ "description": "Handle of the stream to read.",
+ "$ref": "StreamHandle"
+ },
+ {
+ "name": "offset",
+ "description": "Seek to the specified offset before reading (if not specificed, proceed with offset\nfollowing the last read). Some types of streams may only support sequential reads.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "size",
+ "description": "Maximum number of bytes to read (left upon the agent discretion if not specified).",
+ "optional": true,
+ "type": "integer"
+ }
+ ],
+ "returns": [
+ {
+ "name": "base64Encoded",
+ "description": "Set if the data is base64-encoded",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "data",
+ "description": "Data that were read.",
+ "type": "string"
+ },
+ {
+ "name": "eof",
+ "description": "Set if the end-of-file condition occurred while reading.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "resolveBlob",
+ "description": "Return UUID of Blob object specified by a remote object id.",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Object id of a Blob object wrapper.",
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "uuid",
+ "description": "UUID of the specified Blob.",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "IndexedDB",
+ "experimental": true,
+ "dependencies": [
+ "Runtime",
+ "Storage"
+ ],
+ "types": [
+ {
+ "id": "DatabaseWithObjectStores",
+ "description": "Database with an array of object stores.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Database name.",
+ "type": "string"
+ },
+ {
+ "name": "version",
+ "description": "Database version (type is not 'integer', as the standard\nrequires the version number to be 'unsigned long long')",
+ "type": "number"
+ },
+ {
+ "name": "objectStores",
+ "description": "Object stores in this database.",
+ "type": "array",
+ "items": {
+ "$ref": "ObjectStore"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ObjectStore",
+ "description": "Object store.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Object store name.",
+ "type": "string"
+ },
+ {
+ "name": "keyPath",
+ "description": "Object store key path.",
+ "$ref": "KeyPath"
+ },
+ {
+ "name": "autoIncrement",
+ "description": "If true, object store has auto increment flag set.",
+ "type": "boolean"
+ },
+ {
+ "name": "indexes",
+ "description": "Indexes in this object store.",
+ "type": "array",
+ "items": {
+ "$ref": "ObjectStoreIndex"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ObjectStoreIndex",
+ "description": "Object store index.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Index name.",
+ "type": "string"
+ },
+ {
+ "name": "keyPath",
+ "description": "Index key path.",
+ "$ref": "KeyPath"
+ },
+ {
+ "name": "unique",
+ "description": "If true, index is unique.",
+ "type": "boolean"
+ },
+ {
+ "name": "multiEntry",
+ "description": "If true, index allows multiple entries for a key.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "Key",
+ "description": "Key.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Key type.",
+ "type": "string",
+ "enum": [
+ "number",
+ "string",
+ "date",
+ "array"
+ ]
+ },
+ {
+ "name": "number",
+ "description": "Number value.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "string",
+ "description": "String value.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "date",
+ "description": "Date value.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "array",
+ "description": "Array value.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Key"
+ }
+ }
+ ]
+ },
+ {
+ "id": "KeyRange",
+ "description": "Key range.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "lower",
+ "description": "Lower bound.",
+ "optional": true,
+ "$ref": "Key"
+ },
+ {
+ "name": "upper",
+ "description": "Upper bound.",
+ "optional": true,
+ "$ref": "Key"
+ },
+ {
+ "name": "lowerOpen",
+ "description": "If true lower bound is open.",
+ "type": "boolean"
+ },
+ {
+ "name": "upperOpen",
+ "description": "If true upper bound is open.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "DataEntry",
+ "description": "Data entry.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "description": "Key object.",
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "primaryKey",
+ "description": "Primary key object.",
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "value",
+ "description": "Value object.",
+ "$ref": "Runtime.RemoteObject"
+ }
+ ]
+ },
+ {
+ "id": "KeyPath",
+ "description": "Key path.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Key path type.",
+ "type": "string",
+ "enum": [
+ "null",
+ "string",
+ "array"
+ ]
+ },
+ {
+ "name": "string",
+ "description": "String value.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "array",
+ "description": "Array value.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "clearObjectStore",
+ "description": "Clears all entries from an object store.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database name.",
+ "type": "string"
+ },
+ {
+ "name": "objectStoreName",
+ "description": "Object store name.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "deleteDatabase",
+ "description": "Deletes a database.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database name.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "deleteObjectStoreEntries",
+ "description": "Delete a range of entries from an object store",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "type": "string"
+ },
+ {
+ "name": "objectStoreName",
+ "type": "string"
+ },
+ {
+ "name": "keyRange",
+ "description": "Range of entry keys to delete",
+ "$ref": "KeyRange"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables events from backend."
+ },
+ {
+ "name": "enable",
+ "description": "Enables events from backend."
+ },
+ {
+ "name": "requestData",
+ "description": "Requests data from object store or index.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database name.",
+ "type": "string"
+ },
+ {
+ "name": "objectStoreName",
+ "description": "Object store name.",
+ "type": "string"
+ },
+ {
+ "name": "indexName",
+ "description": "Index name, empty string for object store data requests.",
+ "type": "string"
+ },
+ {
+ "name": "skipCount",
+ "description": "Number of records to skip.",
+ "type": "integer"
+ },
+ {
+ "name": "pageSize",
+ "description": "Number of records to fetch.",
+ "type": "integer"
+ },
+ {
+ "name": "keyRange",
+ "description": "Key range.",
+ "optional": true,
+ "$ref": "KeyRange"
+ }
+ ],
+ "returns": [
+ {
+ "name": "objectStoreDataEntries",
+ "description": "Array of object store data entries.",
+ "type": "array",
+ "items": {
+ "$ref": "DataEntry"
+ }
+ },
+ {
+ "name": "hasMore",
+ "description": "If true, there are more entries to fetch in the given range.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getMetadata",
+ "description": "Gets metadata of an object store.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database name.",
+ "type": "string"
+ },
+ {
+ "name": "objectStoreName",
+ "description": "Object store name.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "entriesCount",
+ "description": "the entries count",
+ "type": "number"
+ },
+ {
+ "name": "keyGeneratorValue",
+ "description": "the current value of key generator, to become the next inserted\nkey into the object store. Valid if objectStore.autoIncrement\nis true.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "requestDatabase",
+ "description": "Requests database with given name in given frame.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database name.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "databaseWithObjectStores",
+ "description": "Database with an array of object stores.",
+ "$ref": "DatabaseWithObjectStores"
+ }
+ ]
+ },
+ {
+ "name": "requestDatabaseNames",
+ "description": "Requests database names for given security origin.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ }
+ ],
+ "returns": [
+ {
+ "name": "databaseNames",
+ "description": "Database names for origin.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Input",
+ "types": [
+ {
+ "id": "TouchPoint",
+ "type": "object",
+ "properties": [
+ {
+ "name": "x",
+ "description": "X coordinate of the event relative to the main frame's viewport in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.",
+ "type": "number"
+ },
+ {
+ "name": "radiusX",
+ "description": "X radius of the touch area (default: 1.0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "radiusY",
+ "description": "Y radius of the touch area (default: 1.0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "rotationAngle",
+ "description": "Rotation angle (default: 0.0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "force",
+ "description": "Force (default: 1.0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "tangentialPressure",
+ "description": "The normalized tangential pressure, which has a range of [-1,1] (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "tiltX",
+ "description": "The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0)",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "tiltY",
+ "description": "The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "twist",
+ "description": "The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "id",
+ "description": "Identifier used to track touch sources between events, must be unique within an event.",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "GestureSourceType",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "default",
+ "touch",
+ "mouse"
+ ]
+ },
+ {
+ "id": "MouseButton",
+ "type": "string",
+ "enum": [
+ "none",
+ "left",
+ "middle",
+ "right",
+ "back",
+ "forward"
+ ]
+ },
+ {
+ "id": "TimeSinceEpoch",
+ "description": "UTC time in seconds, counted from January 1, 1970.",
+ "type": "number"
+ },
+ {
+ "id": "DragDataItem",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "mimeType",
+ "description": "Mime type of the dragged data.",
+ "type": "string"
+ },
+ {
+ "name": "data",
+ "description": "Depending of the value of `mimeType`, it contains the dragged link,\ntext, HTML markup or any other data.",
+ "type": "string"
+ },
+ {
+ "name": "title",
+ "description": "Title associated with a link. Only valid when `mimeType` == \"text/uri-list\".",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "baseURL",
+ "description": "Stores the base URL for the contained markup. Only valid when `mimeType`\n== \"text/html\".",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "DragData",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "items",
+ "type": "array",
+ "items": {
+ "$ref": "DragDataItem"
+ }
+ },
+ {
+ "name": "files",
+ "description": "List of filenames that should be included when dropping",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "dragOperationsMask",
+ "description": "Bit field representing allowed drag operations. Copy = 1, Link = 2, Move = 16",
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "dispatchDragEvent",
+ "description": "Dispatches a drag event into the page.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the drag event.",
+ "type": "string",
+ "enum": [
+ "dragEnter",
+ "dragOver",
+ "drop",
+ "dragCancel"
+ ]
+ },
+ {
+ "name": "x",
+ "description": "X coordinate of the event relative to the main frame's viewport in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.",
+ "type": "number"
+ },
+ {
+ "name": "data",
+ "$ref": "DragData"
+ },
+ {
+ "name": "modifiers",
+ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "dispatchKeyEvent",
+ "description": "Dispatches a key event to the page.",
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the key event.",
+ "type": "string",
+ "enum": [
+ "keyDown",
+ "keyUp",
+ "rawKeyDown",
+ "char"
+ ]
+ },
+ {
+ "name": "modifiers",
+ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "timestamp",
+ "description": "Time at which the event occurred.",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "text",
+ "description": "Text as generated by processing a virtual key code with a keyboard layout. Not needed for\nfor `keyUp` and `rawKeyDown` events (default: \"\")",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "unmodifiedText",
+ "description": "Text that would have been generated by the keyboard if no modifiers were pressed (except for\nshift). Useful for shortcut (accelerator) key handling (default: \"\").",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "keyIdentifier",
+ "description": "Unique key identifier (e.g., 'U+0041') (default: \"\").",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "code",
+ "description": "Unique DOM defined string value for each physical key (e.g., 'KeyA') (default: \"\").",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "key",
+ "description": "Unique DOM defined string value describing the meaning of the key in the context of active\nmodifiers, keyboard layout, etc (e.g., 'AltGr') (default: \"\").",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "windowsVirtualKeyCode",
+ "description": "Windows virtual key code (default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "nativeVirtualKeyCode",
+ "description": "Native virtual key code (default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "autoRepeat",
+ "description": "Whether the event was generated from auto repeat (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isKeypad",
+ "description": "Whether the event was generated from the keypad (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isSystemKey",
+ "description": "Whether the event was a system key event (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "location",
+ "description": "Whether the event was from the left or right side of the keyboard. 1=Left, 2=Right (default:\n0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "commands",
+ "description": "Editing commands to send with the key event (e.g., 'selectAll') (default: []).\nThese are related to but not equal the command names used in `document.execCommand` and NSStandardKeyBindingResponding.\nSee https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h for valid command names.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "insertText",
+ "description": "This method emulates inserting text that doesn't come from a key press,\nfor example an emoji keyboard or an IME.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "text",
+ "description": "The text to insert.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "imeSetComposition",
+ "description": "This method sets the current candidate text for ime.\nUse imeCommitComposition to commit the final text.\nUse imeSetComposition with empty string as text to cancel composition.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "text",
+ "description": "The text to insert",
+ "type": "string"
+ },
+ {
+ "name": "selectionStart",
+ "description": "selection start",
+ "type": "integer"
+ },
+ {
+ "name": "selectionEnd",
+ "description": "selection end",
+ "type": "integer"
+ },
+ {
+ "name": "replacementStart",
+ "description": "replacement start",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "replacementEnd",
+ "description": "replacement end",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "dispatchMouseEvent",
+ "description": "Dispatches a mouse event to the page.",
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the mouse event.",
+ "type": "string",
+ "enum": [
+ "mousePressed",
+ "mouseReleased",
+ "mouseMoved",
+ "mouseWheel"
+ ]
+ },
+ {
+ "name": "x",
+ "description": "X coordinate of the event relative to the main frame's viewport in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.",
+ "type": "number"
+ },
+ {
+ "name": "modifiers",
+ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "timestamp",
+ "description": "Time at which the event occurred.",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "button",
+ "description": "Mouse button (default: \"none\").",
+ "optional": true,
+ "$ref": "MouseButton"
+ },
+ {
+ "name": "buttons",
+ "description": "A number indicating which buttons are pressed on the mouse when a mouse event is triggered.\nLeft=1, Right=2, Middle=4, Back=8, Forward=16, None=0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "clickCount",
+ "description": "Number of times the mouse button was clicked (default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "force",
+ "description": "The normalized pressure, which has a range of [0,1] (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "tangentialPressure",
+ "description": "The normalized tangential pressure, which has a range of [-1,1] (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "tiltX",
+ "description": "The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "tiltY",
+ "description": "The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "twist",
+ "description": "The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "deltaX",
+ "description": "X delta in CSS pixels for mouse wheel event (default: 0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "deltaY",
+ "description": "Y delta in CSS pixels for mouse wheel event (default: 0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "pointerType",
+ "description": "Pointer type (default: \"mouse\").",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "mouse",
+ "pen"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "dispatchTouchEvent",
+ "description": "Dispatches a touch event to the page.",
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the touch event. TouchEnd and TouchCancel must not contain any touch points, while\nTouchStart and TouchMove must contains at least one.",
+ "type": "string",
+ "enum": [
+ "touchStart",
+ "touchEnd",
+ "touchMove",
+ "touchCancel"
+ ]
+ },
+ {
+ "name": "touchPoints",
+ "description": "Active touch points on the touch device. One event per any changed point (compared to\nprevious touch event in a sequence) is generated, emulating pressing/moving/releasing points\none by one.",
+ "type": "array",
+ "items": {
+ "$ref": "TouchPoint"
+ }
+ },
+ {
+ "name": "modifiers",
+ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "timestamp",
+ "description": "Time at which the event occurred.",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ }
+ ]
+ },
+ {
+ "name": "cancelDragging",
+ "description": "Cancels any active dragging in the page."
+ },
+ {
+ "name": "emulateTouchFromMouseEvent",
+ "description": "Emulates touch event from the mouse event parameters.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the mouse event.",
+ "type": "string",
+ "enum": [
+ "mousePressed",
+ "mouseReleased",
+ "mouseMoved",
+ "mouseWheel"
+ ]
+ },
+ {
+ "name": "x",
+ "description": "X coordinate of the mouse pointer in DIP.",
+ "type": "integer"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the mouse pointer in DIP.",
+ "type": "integer"
+ },
+ {
+ "name": "button",
+ "description": "Mouse button. Only \"none\", \"left\", \"right\" are supported.",
+ "$ref": "MouseButton"
+ },
+ {
+ "name": "timestamp",
+ "description": "Time at which the event occurred (default: current time).",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "deltaX",
+ "description": "X delta in DIP for mouse wheel event (default: 0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "deltaY",
+ "description": "Y delta in DIP for mouse wheel event (default: 0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "modifiers",
+ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "clickCount",
+ "description": "Number of times the mouse button was clicked (default: 0).",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setIgnoreInputEvents",
+ "description": "Ignores input events (useful while auditing page).",
+ "parameters": [
+ {
+ "name": "ignore",
+ "description": "Ignores input events processing when set to true.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setInterceptDrags",
+ "description": "Prevents default drag and drop behavior and instead emits `Input.dragIntercepted` events.\nDrag and drop behavior can be directly controlled via `Input.dispatchDragEvent`.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "synthesizePinchGesture",
+ "description": "Synthesizes a pinch gesture over a time period by issuing appropriate touch events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "x",
+ "description": "X coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "scaleFactor",
+ "description": "Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out).",
+ "type": "number"
+ },
+ {
+ "name": "relativeSpeed",
+ "description": "Relative pointer speed in pixels per second (default: 800).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "gestureSourceType",
+ "description": "Which type of input events to be generated (default: 'default', which queries the platform\nfor the preferred input type).",
+ "optional": true,
+ "$ref": "GestureSourceType"
+ }
+ ]
+ },
+ {
+ "name": "synthesizeScrollGesture",
+ "description": "Synthesizes a scroll gesture over a time period by issuing appropriate touch events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "x",
+ "description": "X coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "xDistance",
+ "description": "The distance to scroll along the X axis (positive to scroll left).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "yDistance",
+ "description": "The distance to scroll along the Y axis (positive to scroll up).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "xOverscroll",
+ "description": "The number of additional pixels to scroll back along the X axis, in addition to the given\ndistance.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "yOverscroll",
+ "description": "The number of additional pixels to scroll back along the Y axis, in addition to the given\ndistance.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "preventFling",
+ "description": "Prevent fling (default: true).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "speed",
+ "description": "Swipe speed in pixels per second (default: 800).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "gestureSourceType",
+ "description": "Which type of input events to be generated (default: 'default', which queries the platform\nfor the preferred input type).",
+ "optional": true,
+ "$ref": "GestureSourceType"
+ },
+ {
+ "name": "repeatCount",
+ "description": "The number of times to repeat the gesture (default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "repeatDelayMs",
+ "description": "The number of milliseconds delay between each repeat. (default: 250).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "interactionMarkerName",
+ "description": "The name of the interaction markers to generate, if not empty (default: \"\").",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "synthesizeTapGesture",
+ "description": "Synthesizes a tap gesture over a time period by issuing appropriate touch events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "x",
+ "description": "X coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "duration",
+ "description": "Duration between touchdown and touchup events in ms (default: 50).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "tapCount",
+ "description": "Number of times to perform the tap (e.g. 2 for double tap, default: 1).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "gestureSourceType",
+ "description": "Which type of input events to be generated (default: 'default', which queries the platform\nfor the preferred input type).",
+ "optional": true,
+ "$ref": "GestureSourceType"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "dragIntercepted",
+ "description": "Emitted only when `Input.setInterceptDrags` is enabled. Use this data with `Input.dispatchDragEvent` to\nrestore normal drag and drop behavior.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "data",
+ "$ref": "DragData"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Inspector",
+ "experimental": true,
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables inspector domain notifications."
+ },
+ {
+ "name": "enable",
+ "description": "Enables inspector domain notifications."
+ }
+ ],
+ "events": [
+ {
+ "name": "detached",
+ "description": "Fired when remote debugging connection is about to be terminated. Contains detach reason.",
+ "parameters": [
+ {
+ "name": "reason",
+ "description": "The reason why connection has been terminated.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "targetCrashed",
+ "description": "Fired when debugging target has crashed"
+ },
+ {
+ "name": "targetReloadedAfterCrash",
+ "description": "Fired when debugging target has reloaded after crash"
+ }
+ ]
+ },
+ {
+ "domain": "LayerTree",
+ "experimental": true,
+ "dependencies": [
+ "DOM"
+ ],
+ "types": [
+ {
+ "id": "LayerId",
+ "description": "Unique Layer identifier.",
+ "type": "string"
+ },
+ {
+ "id": "SnapshotId",
+ "description": "Unique snapshot identifier.",
+ "type": "string"
+ },
+ {
+ "id": "ScrollRect",
+ "description": "Rectangle where scrolling happens on the main thread.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "rect",
+ "description": "Rectangle itself.",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "type",
+ "description": "Reason for rectangle to force scrolling on the main thread",
+ "type": "string",
+ "enum": [
+ "RepaintsOnScroll",
+ "TouchEventHandler",
+ "WheelEventHandler"
+ ]
+ }
+ ]
+ },
+ {
+ "id": "StickyPositionConstraint",
+ "description": "Sticky position constraints.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "stickyBoxRect",
+ "description": "Layout rectangle of the sticky element before being shifted",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "containingBlockRect",
+ "description": "Layout rectangle of the containing block of the sticky element",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "nearestLayerShiftingStickyBox",
+ "description": "The nearest sticky layer that shifts the sticky box",
+ "optional": true,
+ "$ref": "LayerId"
+ },
+ {
+ "name": "nearestLayerShiftingContainingBlock",
+ "description": "The nearest sticky layer that shifts the containing block",
+ "optional": true,
+ "$ref": "LayerId"
+ }
+ ]
+ },
+ {
+ "id": "PictureTile",
+ "description": "Serialized fragment of layer picture along with its offset within the layer.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "x",
+ "description": "Offset from owning layer left boundary",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Offset from owning layer top boundary",
+ "type": "number"
+ },
+ {
+ "name": "picture",
+ "description": "Base64-encoded snapshot data. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Layer",
+ "description": "Information about a compositing layer.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "layerId",
+ "description": "The unique id for this layer.",
+ "$ref": "LayerId"
+ },
+ {
+ "name": "parentLayerId",
+ "description": "The id of parent (not present for root).",
+ "optional": true,
+ "$ref": "LayerId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "The backend id for the node associated with this layer.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "offsetX",
+ "description": "Offset from parent layer, X coordinate.",
+ "type": "number"
+ },
+ {
+ "name": "offsetY",
+ "description": "Offset from parent layer, Y coordinate.",
+ "type": "number"
+ },
+ {
+ "name": "width",
+ "description": "Layer width.",
+ "type": "number"
+ },
+ {
+ "name": "height",
+ "description": "Layer height.",
+ "type": "number"
+ },
+ {
+ "name": "transform",
+ "description": "Transformation matrix for layer, default is identity matrix",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "name": "anchorX",
+ "description": "Transform anchor point X, absent if no transform specified",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "anchorY",
+ "description": "Transform anchor point Y, absent if no transform specified",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "anchorZ",
+ "description": "Transform anchor point Z, absent if no transform specified",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "paintCount",
+ "description": "Indicates how many time this layer has painted.",
+ "type": "integer"
+ },
+ {
+ "name": "drawsContent",
+ "description": "Indicates whether this layer hosts any content, rather than being used for\ntransform/scrolling purposes only.",
+ "type": "boolean"
+ },
+ {
+ "name": "invisible",
+ "description": "Set if layer is not visible.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "scrollRects",
+ "description": "Rectangles scrolling on main thread only.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "ScrollRect"
+ }
+ },
+ {
+ "name": "stickyPositionConstraint",
+ "description": "Sticky position constraint information",
+ "optional": true,
+ "$ref": "StickyPositionConstraint"
+ }
+ ]
+ },
+ {
+ "id": "PaintProfile",
+ "description": "Array of timings, one per paint step.",
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ }
+ ],
+ "commands": [
+ {
+ "name": "compositingReasons",
+ "description": "Provides the reasons why the given layer was composited.",
+ "parameters": [
+ {
+ "name": "layerId",
+ "description": "The id of the layer for which we want to get the reasons it was composited.",
+ "$ref": "LayerId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "compositingReasons",
+ "description": "A list of strings specifying reasons for the given layer to become composited.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "compositingReasonIds",
+ "description": "A list of strings specifying reason IDs for the given layer to become composited.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables compositing tree inspection."
+ },
+ {
+ "name": "enable",
+ "description": "Enables compositing tree inspection."
+ },
+ {
+ "name": "loadSnapshot",
+ "description": "Returns the snapshot identifier.",
+ "parameters": [
+ {
+ "name": "tiles",
+ "description": "An array of tiles composing the snapshot.",
+ "type": "array",
+ "items": {
+ "$ref": "PictureTile"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the snapshot.",
+ "$ref": "SnapshotId"
+ }
+ ]
+ },
+ {
+ "name": "makeSnapshot",
+ "description": "Returns the layer snapshot identifier.",
+ "parameters": [
+ {
+ "name": "layerId",
+ "description": "The id of the layer.",
+ "$ref": "LayerId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the layer snapshot.",
+ "$ref": "SnapshotId"
+ }
+ ]
+ },
+ {
+ "name": "profileSnapshot",
+ "parameters": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the layer snapshot.",
+ "$ref": "SnapshotId"
+ },
+ {
+ "name": "minRepeatCount",
+ "description": "The maximum number of times to replay the snapshot (1, if not specified).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "minDuration",
+ "description": "The minimum duration (in seconds) to replay the snapshot.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "clipRect",
+ "description": "The clip rectangle to apply when replaying the snapshot.",
+ "optional": true,
+ "$ref": "DOM.Rect"
+ }
+ ],
+ "returns": [
+ {
+ "name": "timings",
+ "description": "The array of paint profiles, one per run.",
+ "type": "array",
+ "items": {
+ "$ref": "PaintProfile"
+ }
+ }
+ ]
+ },
+ {
+ "name": "releaseSnapshot",
+ "description": "Releases layer snapshot captured by the back-end.",
+ "parameters": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the layer snapshot.",
+ "$ref": "SnapshotId"
+ }
+ ]
+ },
+ {
+ "name": "replaySnapshot",
+ "description": "Replays the layer snapshot and returns the resulting bitmap.",
+ "parameters": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the layer snapshot.",
+ "$ref": "SnapshotId"
+ },
+ {
+ "name": "fromStep",
+ "description": "The first step to replay from (replay from the very start if not specified).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "toStep",
+ "description": "The last step to replay to (replay till the end if not specified).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "scale",
+ "description": "The scale to apply while replaying (defaults to 1).",
+ "optional": true,
+ "type": "number"
+ }
+ ],
+ "returns": [
+ {
+ "name": "dataURL",
+ "description": "A data: URL for resulting image.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "snapshotCommandLog",
+ "description": "Replays the layer snapshot and returns canvas log.",
+ "parameters": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the layer snapshot.",
+ "$ref": "SnapshotId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "commandLog",
+ "description": "The array of canvas function calls.",
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "layerPainted",
+ "parameters": [
+ {
+ "name": "layerId",
+ "description": "The id of the painted layer.",
+ "$ref": "LayerId"
+ },
+ {
+ "name": "clip",
+ "description": "Clip rectangle.",
+ "$ref": "DOM.Rect"
+ }
+ ]
+ },
+ {
+ "name": "layerTreeDidChange",
+ "parameters": [
+ {
+ "name": "layers",
+ "description": "Layer tree, absent if not in the comspositing mode.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Layer"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Log",
+ "description": "Provides access to log entries.",
+ "dependencies": [
+ "Runtime",
+ "Network"
+ ],
+ "types": [
+ {
+ "id": "LogEntry",
+ "description": "Log entry.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "source",
+ "description": "Log entry source.",
+ "type": "string",
+ "enum": [
+ "xml",
+ "javascript",
+ "network",
+ "storage",
+ "appcache",
+ "rendering",
+ "security",
+ "deprecation",
+ "worker",
+ "violation",
+ "intervention",
+ "recommendation",
+ "other"
+ ]
+ },
+ {
+ "name": "level",
+ "description": "Log entry severity.",
+ "type": "string",
+ "enum": [
+ "verbose",
+ "info",
+ "warning",
+ "error"
+ ]
+ },
+ {
+ "name": "text",
+ "description": "Logged text.",
+ "type": "string"
+ },
+ {
+ "name": "category",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "cors"
+ ]
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp when this entry was added.",
+ "$ref": "Runtime.Timestamp"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource if known.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Line number in the resource.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "stackTrace",
+ "description": "JavaScript stack trace.",
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "networkRequestId",
+ "description": "Identifier of the network request associated with this entry.",
+ "optional": true,
+ "$ref": "Network.RequestId"
+ },
+ {
+ "name": "workerId",
+ "description": "Identifier of the worker associated with this entry.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "args",
+ "description": "Call arguments.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Runtime.RemoteObject"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ViolationSetting",
+ "description": "Violation configuration setting.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Violation type.",
+ "type": "string",
+ "enum": [
+ "longTask",
+ "longLayout",
+ "blockedEvent",
+ "blockedParser",
+ "discouragedAPIUse",
+ "handler",
+ "recurringHandler"
+ ]
+ },
+ {
+ "name": "threshold",
+ "description": "Time threshold to trigger upon.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "clear",
+ "description": "Clears the log."
+ },
+ {
+ "name": "disable",
+ "description": "Disables log domain, prevents further log entries from being reported to the client."
+ },
+ {
+ "name": "enable",
+ "description": "Enables log domain, sends the entries collected so far to the client by means of the\n`entryAdded` notification."
+ },
+ {
+ "name": "startViolationsReport",
+ "description": "start violation reporting.",
+ "parameters": [
+ {
+ "name": "config",
+ "description": "Configuration for violations.",
+ "type": "array",
+ "items": {
+ "$ref": "ViolationSetting"
+ }
+ }
+ ]
+ },
+ {
+ "name": "stopViolationsReport",
+ "description": "Stop violation reporting."
+ }
+ ],
+ "events": [
+ {
+ "name": "entryAdded",
+ "description": "Issued when new message was logged.",
+ "parameters": [
+ {
+ "name": "entry",
+ "description": "The entry.",
+ "$ref": "LogEntry"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Memory",
+ "experimental": true,
+ "types": [
+ {
+ "id": "PressureLevel",
+ "description": "Memory pressure level.",
+ "type": "string",
+ "enum": [
+ "moderate",
+ "critical"
+ ]
+ },
+ {
+ "id": "SamplingProfileNode",
+ "description": "Heap profile sample.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "size",
+ "description": "Size of the sampled allocation.",
+ "type": "number"
+ },
+ {
+ "name": "total",
+ "description": "Total bytes attributed to this sample.",
+ "type": "number"
+ },
+ {
+ "name": "stack",
+ "description": "Execution stack at the point of allocation.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SamplingProfile",
+ "description": "Array of heap profile samples.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "samples",
+ "type": "array",
+ "items": {
+ "$ref": "SamplingProfileNode"
+ }
+ },
+ {
+ "name": "modules",
+ "type": "array",
+ "items": {
+ "$ref": "Module"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Module",
+ "description": "Executable module information",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Name of the module.",
+ "type": "string"
+ },
+ {
+ "name": "uuid",
+ "description": "UUID of the module.",
+ "type": "string"
+ },
+ {
+ "name": "baseAddress",
+ "description": "Base address where the module is loaded into memory. Encoded as a decimal\nor hexadecimal (0x prefixed) string.",
+ "type": "string"
+ },
+ {
+ "name": "size",
+ "description": "Size of the module in bytes.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getDOMCounters",
+ "returns": [
+ {
+ "name": "documents",
+ "type": "integer"
+ },
+ {
+ "name": "nodes",
+ "type": "integer"
+ },
+ {
+ "name": "jsEventListeners",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "prepareForLeakDetection"
+ },
+ {
+ "name": "forciblyPurgeJavaScriptMemory",
+ "description": "Simulate OomIntervention by purging V8 memory."
+ },
+ {
+ "name": "setPressureNotificationsSuppressed",
+ "description": "Enable/disable suppressing memory pressure notifications in all processes.",
+ "parameters": [
+ {
+ "name": "suppressed",
+ "description": "If true, memory pressure notifications will be suppressed.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "simulatePressureNotification",
+ "description": "Simulate a memory pressure notification in all processes.",
+ "parameters": [
+ {
+ "name": "level",
+ "description": "Memory pressure level of the notification.",
+ "$ref": "PressureLevel"
+ }
+ ]
+ },
+ {
+ "name": "startSampling",
+ "description": "Start collecting native memory profile.",
+ "parameters": [
+ {
+ "name": "samplingInterval",
+ "description": "Average number of bytes between samples.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "suppressRandomness",
+ "description": "Do not randomize intervals between samples.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "stopSampling",
+ "description": "Stop collecting native memory profile."
+ },
+ {
+ "name": "getAllTimeSamplingProfile",
+ "description": "Retrieve native memory allocations profile\ncollected since renderer process startup.",
+ "returns": [
+ {
+ "name": "profile",
+ "$ref": "SamplingProfile"
+ }
+ ]
+ },
+ {
+ "name": "getBrowserSamplingProfile",
+ "description": "Retrieve native memory allocations profile\ncollected since browser process startup.",
+ "returns": [
+ {
+ "name": "profile",
+ "$ref": "SamplingProfile"
+ }
+ ]
+ },
+ {
+ "name": "getSamplingProfile",
+ "description": "Retrieve native memory allocations profile collected since last\n`startSampling` call.",
+ "returns": [
+ {
+ "name": "profile",
+ "$ref": "SamplingProfile"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Network",
+ "description": "Network domain allows tracking network activities of the page. It exposes information about http,\nfile, data and other requests and responses, their headers, bodies, timing, etc.",
+ "dependencies": [
+ "Debugger",
+ "Runtime",
+ "Security"
+ ],
+ "types": [
+ {
+ "id": "ResourceType",
+ "description": "Resource type as it was perceived by the rendering engine.",
+ "type": "string",
+ "enum": [
+ "Document",
+ "Stylesheet",
+ "Image",
+ "Media",
+ "Font",
+ "Script",
+ "TextTrack",
+ "XHR",
+ "Fetch",
+ "Prefetch",
+ "EventSource",
+ "WebSocket",
+ "Manifest",
+ "SignedExchange",
+ "Ping",
+ "CSPViolationReport",
+ "Preflight",
+ "Other"
+ ]
+ },
+ {
+ "id": "LoaderId",
+ "description": "Unique loader identifier.",
+ "type": "string"
+ },
+ {
+ "id": "RequestId",
+ "description": "Unique request identifier.",
+ "type": "string"
+ },
+ {
+ "id": "InterceptionId",
+ "description": "Unique intercepted request identifier.",
+ "type": "string"
+ },
+ {
+ "id": "ErrorReason",
+ "description": "Network level fetch failure reason.",
+ "type": "string",
+ "enum": [
+ "Failed",
+ "Aborted",
+ "TimedOut",
+ "AccessDenied",
+ "ConnectionClosed",
+ "ConnectionReset",
+ "ConnectionRefused",
+ "ConnectionAborted",
+ "ConnectionFailed",
+ "NameNotResolved",
+ "InternetDisconnected",
+ "AddressUnreachable",
+ "BlockedByClient",
+ "BlockedByResponse"
+ ]
+ },
+ {
+ "id": "TimeSinceEpoch",
+ "description": "UTC time in seconds, counted from January 1, 1970.",
+ "type": "number"
+ },
+ {
+ "id": "MonotonicTime",
+ "description": "Monotonically increasing time in seconds since an arbitrary point in the past.",
+ "type": "number"
+ },
+ {
+ "id": "Headers",
+ "description": "Request / response headers as keys / values of JSON object.",
+ "type": "object"
+ },
+ {
+ "id": "ConnectionType",
+ "description": "The underlying connection technology that the browser is supposedly using.",
+ "type": "string",
+ "enum": [
+ "none",
+ "cellular2g",
+ "cellular3g",
+ "cellular4g",
+ "bluetooth",
+ "ethernet",
+ "wifi",
+ "wimax",
+ "other"
+ ]
+ },
+ {
+ "id": "CookieSameSite",
+ "description": "Represents the cookie's 'SameSite' status:\nhttps://tools.ietf.org/html/draft-west-first-party-cookies",
+ "type": "string",
+ "enum": [
+ "Strict",
+ "Lax",
+ "None"
+ ]
+ },
+ {
+ "id": "CookiePriority",
+ "description": "Represents the cookie's 'Priority' status:\nhttps://tools.ietf.org/html/draft-west-cookie-priority-00",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Low",
+ "Medium",
+ "High"
+ ]
+ },
+ {
+ "id": "CookieSourceScheme",
+ "description": "Represents the source scheme of the origin that originally set the cookie.\nA value of \"Unset\" allows protocol clients to emulate legacy cookie scope for the scheme.\nThis is a temporary ability and it will be removed in the future.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Unset",
+ "NonSecure",
+ "Secure"
+ ]
+ },
+ {
+ "id": "ResourceTiming",
+ "description": "Timing information for the request.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "requestTime",
+ "description": "Timing's requestTime is a baseline in seconds, while the other numbers are ticks in\nmilliseconds relatively to this requestTime.",
+ "type": "number"
+ },
+ {
+ "name": "proxyStart",
+ "description": "Started resolving proxy.",
+ "type": "number"
+ },
+ {
+ "name": "proxyEnd",
+ "description": "Finished resolving proxy.",
+ "type": "number"
+ },
+ {
+ "name": "dnsStart",
+ "description": "Started DNS address resolve.",
+ "type": "number"
+ },
+ {
+ "name": "dnsEnd",
+ "description": "Finished DNS address resolve.",
+ "type": "number"
+ },
+ {
+ "name": "connectStart",
+ "description": "Started connecting to the remote host.",
+ "type": "number"
+ },
+ {
+ "name": "connectEnd",
+ "description": "Connected to the remote host.",
+ "type": "number"
+ },
+ {
+ "name": "sslStart",
+ "description": "Started SSL handshake.",
+ "type": "number"
+ },
+ {
+ "name": "sslEnd",
+ "description": "Finished SSL handshake.",
+ "type": "number"
+ },
+ {
+ "name": "workerStart",
+ "description": "Started running ServiceWorker.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "workerReady",
+ "description": "Finished Starting ServiceWorker.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "workerFetchStart",
+ "description": "Started fetch event.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "workerRespondWithSettled",
+ "description": "Settled fetch event respondWith promise.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "sendStart",
+ "description": "Started sending request.",
+ "type": "number"
+ },
+ {
+ "name": "sendEnd",
+ "description": "Finished sending request.",
+ "type": "number"
+ },
+ {
+ "name": "pushStart",
+ "description": "Time the server started pushing request.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "pushEnd",
+ "description": "Time the server finished pushing request.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "receiveHeadersStart",
+ "description": "Started receiving response headers.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "receiveHeadersEnd",
+ "description": "Finished receiving response headers.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "ResourcePriority",
+ "description": "Loading priority of a resource request.",
+ "type": "string",
+ "enum": [
+ "VeryLow",
+ "Low",
+ "Medium",
+ "High",
+ "VeryHigh"
+ ]
+ },
+ {
+ "id": "PostDataEntry",
+ "description": "Post data entry for HTTP request",
+ "type": "object",
+ "properties": [
+ {
+ "name": "bytes",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Request",
+ "description": "HTTP request data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "Request URL (without fragment).",
+ "type": "string"
+ },
+ {
+ "name": "urlFragment",
+ "description": "Fragment of the requested URL starting with hash, if present.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "method",
+ "description": "HTTP request method.",
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "description": "HTTP request headers.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "postData",
+ "description": "HTTP POST request data.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "hasPostData",
+ "description": "True when the request has POST data. Note that postData might still be omitted when this flag is true when the data is too long.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "postDataEntries",
+ "description": "Request body elements. This will be converted from base64 to binary",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "PostDataEntry"
+ }
+ },
+ {
+ "name": "mixedContentType",
+ "description": "The mixed content type of the request.",
+ "optional": true,
+ "$ref": "Security.MixedContentType"
+ },
+ {
+ "name": "initialPriority",
+ "description": "Priority of the resource request at the time request is sent.",
+ "$ref": "ResourcePriority"
+ },
+ {
+ "name": "referrerPolicy",
+ "description": "The referrer policy of the request, as defined in https://www.w3.org/TR/referrer-policy/",
+ "type": "string",
+ "enum": [
+ "unsafe-url",
+ "no-referrer-when-downgrade",
+ "no-referrer",
+ "origin",
+ "origin-when-cross-origin",
+ "same-origin",
+ "strict-origin",
+ "strict-origin-when-cross-origin"
+ ]
+ },
+ {
+ "name": "isLinkPreload",
+ "description": "Whether is loaded via link preload.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "trustTokenParams",
+ "description": "Set for requests when the TrustToken API is used. Contains the parameters\npassed by the developer (e.g. via \"fetch\") as understood by the backend.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TrustTokenParams"
+ },
+ {
+ "name": "isSameSite",
+ "description": "True if this resource request is considered to be the 'same site' as the\nrequest correspondinfg to the main frame.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "SignedCertificateTimestamp",
+ "description": "Details of a signed certificate timestamp (SCT).",
+ "type": "object",
+ "properties": [
+ {
+ "name": "status",
+ "description": "Validation status.",
+ "type": "string"
+ },
+ {
+ "name": "origin",
+ "description": "Origin.",
+ "type": "string"
+ },
+ {
+ "name": "logDescription",
+ "description": "Log name / description.",
+ "type": "string"
+ },
+ {
+ "name": "logId",
+ "description": "Log ID.",
+ "type": "string"
+ },
+ {
+ "name": "timestamp",
+ "description": "Issuance date. Unlike TimeSinceEpoch, this contains the number of\nmilliseconds since January 1, 1970, UTC, not the number of seconds.",
+ "type": "number"
+ },
+ {
+ "name": "hashAlgorithm",
+ "description": "Hash algorithm.",
+ "type": "string"
+ },
+ {
+ "name": "signatureAlgorithm",
+ "description": "Signature algorithm.",
+ "type": "string"
+ },
+ {
+ "name": "signatureData",
+ "description": "Signature data.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "SecurityDetails",
+ "description": "Security details about a request.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "protocol",
+ "description": "Protocol name (e.g. \"TLS 1.2\" or \"QUIC\").",
+ "type": "string"
+ },
+ {
+ "name": "keyExchange",
+ "description": "Key Exchange used by the connection, or the empty string if not applicable.",
+ "type": "string"
+ },
+ {
+ "name": "keyExchangeGroup",
+ "description": "(EC)DH group used by the connection, if applicable.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cipher",
+ "description": "Cipher name.",
+ "type": "string"
+ },
+ {
+ "name": "mac",
+ "description": "TLS MAC. Note that AEAD ciphers do not have separate MACs.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "certificateId",
+ "description": "Certificate ID value.",
+ "$ref": "Security.CertificateId"
+ },
+ {
+ "name": "subjectName",
+ "description": "Certificate subject name.",
+ "type": "string"
+ },
+ {
+ "name": "sanList",
+ "description": "Subject Alternative Name (SAN) DNS names and IP addresses.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "issuer",
+ "description": "Name of the issuing CA.",
+ "type": "string"
+ },
+ {
+ "name": "validFrom",
+ "description": "Certificate valid from date.",
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "validTo",
+ "description": "Certificate valid to (expiration) date",
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "signedCertificateTimestampList",
+ "description": "List of signed certificate timestamps (SCTs).",
+ "type": "array",
+ "items": {
+ "$ref": "SignedCertificateTimestamp"
+ }
+ },
+ {
+ "name": "certificateTransparencyCompliance",
+ "description": "Whether the request complied with Certificate Transparency policy",
+ "$ref": "CertificateTransparencyCompliance"
+ },
+ {
+ "name": "serverSignatureAlgorithm",
+ "description": "The signature algorithm used by the server in the TLS server signature,\nrepresented as a TLS SignatureScheme code point. Omitted if not\napplicable or not known.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "encryptedClientHello",
+ "description": "Whether the connection used Encrypted ClientHello",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "CertificateTransparencyCompliance",
+ "description": "Whether the request complied with Certificate Transparency policy.",
+ "type": "string",
+ "enum": [
+ "unknown",
+ "not-compliant",
+ "compliant"
+ ]
+ },
+ {
+ "id": "BlockedReason",
+ "description": "The reason why request was blocked.",
+ "type": "string",
+ "enum": [
+ "other",
+ "csp",
+ "mixed-content",
+ "origin",
+ "inspector",
+ "subresource-filter",
+ "content-type",
+ "coep-frame-resource-needs-coep-header",
+ "coop-sandboxed-iframe-cannot-navigate-to-coop-page",
+ "corp-not-same-origin",
+ "corp-not-same-origin-after-defaulted-to-same-origin-by-coep",
+ "corp-not-same-site"
+ ]
+ },
+ {
+ "id": "CorsError",
+ "description": "The reason why request was blocked.",
+ "type": "string",
+ "enum": [
+ "DisallowedByMode",
+ "InvalidResponse",
+ "WildcardOriginNotAllowed",
+ "MissingAllowOriginHeader",
+ "MultipleAllowOriginValues",
+ "InvalidAllowOriginValue",
+ "AllowOriginMismatch",
+ "InvalidAllowCredentials",
+ "CorsDisabledScheme",
+ "PreflightInvalidStatus",
+ "PreflightDisallowedRedirect",
+ "PreflightWildcardOriginNotAllowed",
+ "PreflightMissingAllowOriginHeader",
+ "PreflightMultipleAllowOriginValues",
+ "PreflightInvalidAllowOriginValue",
+ "PreflightAllowOriginMismatch",
+ "PreflightInvalidAllowCredentials",
+ "PreflightMissingAllowExternal",
+ "PreflightInvalidAllowExternal",
+ "PreflightMissingAllowPrivateNetwork",
+ "PreflightInvalidAllowPrivateNetwork",
+ "InvalidAllowMethodsPreflightResponse",
+ "InvalidAllowHeadersPreflightResponse",
+ "MethodDisallowedByPreflightResponse",
+ "HeaderDisallowedByPreflightResponse",
+ "RedirectContainsCredentials",
+ "InsecurePrivateNetwork",
+ "InvalidPrivateNetworkAccess",
+ "UnexpectedPrivateNetworkAccess",
+ "NoCorsRedirectModeNotFollow",
+ "PreflightMissingPrivateNetworkAccessId",
+ "PreflightMissingPrivateNetworkAccessName",
+ "PrivateNetworkAccessPermissionUnavailable",
+ "PrivateNetworkAccessPermissionDenied"
+ ]
+ },
+ {
+ "id": "CorsErrorStatus",
+ "type": "object",
+ "properties": [
+ {
+ "name": "corsError",
+ "$ref": "CorsError"
+ },
+ {
+ "name": "failedParameter",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "ServiceWorkerResponseSource",
+ "description": "Source of serviceworker response.",
+ "type": "string",
+ "enum": [
+ "cache-storage",
+ "http-cache",
+ "fallback-code",
+ "network"
+ ]
+ },
+ {
+ "id": "TrustTokenParams",
+ "description": "Determines what type of Trust Token operation is executed and\ndepending on the type, some additional parameters. The values\nare specified in third_party/blink/renderer/core/fetch/trust_token.idl.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "operation",
+ "$ref": "TrustTokenOperationType"
+ },
+ {
+ "name": "refreshPolicy",
+ "description": "Only set for \"token-redemption\" operation and determine whether\nto request a fresh SRR or use a still valid cached SRR.",
+ "type": "string",
+ "enum": [
+ "UseCached",
+ "Refresh"
+ ]
+ },
+ {
+ "name": "issuers",
+ "description": "Origins of issuers from whom to request tokens or redemption\nrecords.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "TrustTokenOperationType",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Issuance",
+ "Redemption",
+ "Signing"
+ ]
+ },
+ {
+ "id": "AlternateProtocolUsage",
+ "description": "The reason why Chrome uses a specific transport protocol for HTTP semantics.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "alternativeJobWonWithoutRace",
+ "alternativeJobWonRace",
+ "mainJobWonRace",
+ "mappingMissing",
+ "broken",
+ "dnsAlpnH3JobWonWithoutRace",
+ "dnsAlpnH3JobWonRace",
+ "unspecifiedReason"
+ ]
+ },
+ {
+ "id": "Response",
+ "description": "HTTP response data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "Response URL. This URL can be different from CachedResource.url in case of redirect.",
+ "type": "string"
+ },
+ {
+ "name": "status",
+ "description": "HTTP response status code.",
+ "type": "integer"
+ },
+ {
+ "name": "statusText",
+ "description": "HTTP response status text.",
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "description": "HTTP response headers.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "headersText",
+ "description": "HTTP response headers text. This has been replaced by the headers in Network.responseReceivedExtraInfo.",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "mimeType",
+ "description": "Resource mimeType as determined by the browser.",
+ "type": "string"
+ },
+ {
+ "name": "requestHeaders",
+ "description": "Refined HTTP request headers that were actually transmitted over the network.",
+ "optional": true,
+ "$ref": "Headers"
+ },
+ {
+ "name": "requestHeadersText",
+ "description": "HTTP request headers text. This has been replaced by the headers in Network.requestWillBeSentExtraInfo.",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "connectionReused",
+ "description": "Specifies whether physical connection was actually reused for this request.",
+ "type": "boolean"
+ },
+ {
+ "name": "connectionId",
+ "description": "Physical connection id that was actually used for this request.",
+ "type": "number"
+ },
+ {
+ "name": "remoteIPAddress",
+ "description": "Remote IP address.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "remotePort",
+ "description": "Remote port.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "fromDiskCache",
+ "description": "Specifies that the request was served from the disk cache.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "fromServiceWorker",
+ "description": "Specifies that the request was served from the ServiceWorker.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "fromPrefetchCache",
+ "description": "Specifies that the request was served from the prefetch cache.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "encodedDataLength",
+ "description": "Total number of bytes received for this request so far.",
+ "type": "number"
+ },
+ {
+ "name": "timing",
+ "description": "Timing information for the given request.",
+ "optional": true,
+ "$ref": "ResourceTiming"
+ },
+ {
+ "name": "serviceWorkerResponseSource",
+ "description": "Response source of response from ServiceWorker.",
+ "optional": true,
+ "$ref": "ServiceWorkerResponseSource"
+ },
+ {
+ "name": "responseTime",
+ "description": "The time at which the returned response was generated.",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "cacheStorageCacheName",
+ "description": "Cache Storage Cache Name.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "protocol",
+ "description": "Protocol used to fetch this request.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "alternateProtocolUsage",
+ "description": "The reason why Chrome uses a specific transport protocol for HTTP semantics.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "AlternateProtocolUsage"
+ },
+ {
+ "name": "securityState",
+ "description": "Security state of the request resource.",
+ "$ref": "Security.SecurityState"
+ },
+ {
+ "name": "securityDetails",
+ "description": "Security details for the request.",
+ "optional": true,
+ "$ref": "SecurityDetails"
+ }
+ ]
+ },
+ {
+ "id": "WebSocketRequest",
+ "description": "WebSocket request data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "headers",
+ "description": "HTTP request headers.",
+ "$ref": "Headers"
+ }
+ ]
+ },
+ {
+ "id": "WebSocketResponse",
+ "description": "WebSocket response data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "status",
+ "description": "HTTP response status code.",
+ "type": "integer"
+ },
+ {
+ "name": "statusText",
+ "description": "HTTP response status text.",
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "description": "HTTP response headers.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "headersText",
+ "description": "HTTP response headers text.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "requestHeaders",
+ "description": "HTTP request headers.",
+ "optional": true,
+ "$ref": "Headers"
+ },
+ {
+ "name": "requestHeadersText",
+ "description": "HTTP request headers text.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "WebSocketFrame",
+ "description": "WebSocket message data. This represents an entire WebSocket message, not just a fragmented frame as the name suggests.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "opcode",
+ "description": "WebSocket message opcode.",
+ "type": "number"
+ },
+ {
+ "name": "mask",
+ "description": "WebSocket message mask.",
+ "type": "boolean"
+ },
+ {
+ "name": "payloadData",
+ "description": "WebSocket message payload data.\nIf the opcode is 1, this is a text message and payloadData is a UTF-8 string.\nIf the opcode isn't 1, then payloadData is a base64 encoded string representing binary data.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CachedResource",
+ "description": "Information about the cached resource.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "Resource URL. This is the url of the original network request.",
+ "type": "string"
+ },
+ {
+ "name": "type",
+ "description": "Type of this resource.",
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "response",
+ "description": "Cached response data.",
+ "optional": true,
+ "$ref": "Response"
+ },
+ {
+ "name": "bodySize",
+ "description": "Cached response body size.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "Initiator",
+ "description": "Information about the request initiator.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Type of this initiator.",
+ "type": "string",
+ "enum": [
+ "parser",
+ "script",
+ "preload",
+ "SignedExchange",
+ "preflight",
+ "other"
+ ]
+ },
+ {
+ "name": "stack",
+ "description": "Initiator JavaScript stack trace, set for Script only.",
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "url",
+ "description": "Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Initiator line number, set for Parser type or for Script type (when script is importing\nmodule) (0-based).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Initiator column number, set for Parser type or for Script type (when script is importing\nmodule) (0-based).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "requestId",
+ "description": "Set if another request triggered this request (e.g. preflight).",
+ "optional": true,
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "id": "Cookie",
+ "description": "Cookie object",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Cookie name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Cookie value.",
+ "type": "string"
+ },
+ {
+ "name": "domain",
+ "description": "Cookie domain.",
+ "type": "string"
+ },
+ {
+ "name": "path",
+ "description": "Cookie path.",
+ "type": "string"
+ },
+ {
+ "name": "expires",
+ "description": "Cookie expiration date as the number of seconds since the UNIX epoch.",
+ "type": "number"
+ },
+ {
+ "name": "size",
+ "description": "Cookie size.",
+ "type": "integer"
+ },
+ {
+ "name": "httpOnly",
+ "description": "True if cookie is http-only.",
+ "type": "boolean"
+ },
+ {
+ "name": "secure",
+ "description": "True if cookie is secure.",
+ "type": "boolean"
+ },
+ {
+ "name": "session",
+ "description": "True in case of session cookie.",
+ "type": "boolean"
+ },
+ {
+ "name": "sameSite",
+ "description": "Cookie SameSite type.",
+ "optional": true,
+ "$ref": "CookieSameSite"
+ },
+ {
+ "name": "priority",
+ "description": "Cookie Priority",
+ "experimental": true,
+ "$ref": "CookiePriority"
+ },
+ {
+ "name": "sameParty",
+ "description": "True if cookie is SameParty.",
+ "experimental": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sourceScheme",
+ "description": "Cookie source scheme type.",
+ "experimental": true,
+ "$ref": "CookieSourceScheme"
+ },
+ {
+ "name": "sourcePort",
+ "description": "Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\nThis is a temporary ability and it will be removed in the future.",
+ "experimental": true,
+ "type": "integer"
+ },
+ {
+ "name": "partitionKey",
+ "description": "Cookie partition key. The site of the top-level URL the browser was visiting at the start\nof the request to the endpoint that set the cookie.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "partitionKeyOpaque",
+ "description": "True if cookie partition key is opaque.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "SetCookieBlockedReason",
+ "description": "Types of reasons why a cookie may not be stored from a response.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "SecureOnly",
+ "SameSiteStrict",
+ "SameSiteLax",
+ "SameSiteUnspecifiedTreatedAsLax",
+ "SameSiteNoneInsecure",
+ "UserPreferences",
+ "ThirdPartyBlockedInFirstPartySet",
+ "SyntaxError",
+ "SchemeNotSupported",
+ "OverwriteSecure",
+ "InvalidDomain",
+ "InvalidPrefix",
+ "UnknownError",
+ "SchemefulSameSiteStrict",
+ "SchemefulSameSiteLax",
+ "SchemefulSameSiteUnspecifiedTreatedAsLax",
+ "SamePartyFromCrossPartyContext",
+ "SamePartyConflictsWithOtherAttributes",
+ "NameValuePairExceedsMaxSize"
+ ]
+ },
+ {
+ "id": "CookieBlockedReason",
+ "description": "Types of reasons why a cookie may not be sent with a request.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "SecureOnly",
+ "NotOnPath",
+ "DomainMismatch",
+ "SameSiteStrict",
+ "SameSiteLax",
+ "SameSiteUnspecifiedTreatedAsLax",
+ "SameSiteNoneInsecure",
+ "UserPreferences",
+ "ThirdPartyBlockedInFirstPartySet",
+ "UnknownError",
+ "SchemefulSameSiteStrict",
+ "SchemefulSameSiteLax",
+ "SchemefulSameSiteUnspecifiedTreatedAsLax",
+ "SamePartyFromCrossPartyContext",
+ "NameValuePairExceedsMaxSize"
+ ]
+ },
+ {
+ "id": "BlockedSetCookieWithReason",
+ "description": "A cookie which was not stored from a response with the corresponding reason.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "blockedReasons",
+ "description": "The reason(s) this cookie was blocked.",
+ "type": "array",
+ "items": {
+ "$ref": "SetCookieBlockedReason"
+ }
+ },
+ {
+ "name": "cookieLine",
+ "description": "The string representing this individual cookie as it would appear in the header.\nThis is not the entire \"cookie\" or \"set-cookie\" header which could have multiple cookies.",
+ "type": "string"
+ },
+ {
+ "name": "cookie",
+ "description": "The cookie object which represents the cookie which was not stored. It is optional because\nsometimes complete cookie information is not available, such as in the case of parsing\nerrors.",
+ "optional": true,
+ "$ref": "Cookie"
+ }
+ ]
+ },
+ {
+ "id": "BlockedCookieWithReason",
+ "description": "A cookie with was not sent with a request with the corresponding reason.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "blockedReasons",
+ "description": "The reason(s) the cookie was blocked.",
+ "type": "array",
+ "items": {
+ "$ref": "CookieBlockedReason"
+ }
+ },
+ {
+ "name": "cookie",
+ "description": "The cookie object representing the cookie which was not sent.",
+ "$ref": "Cookie"
+ }
+ ]
+ },
+ {
+ "id": "CookieParam",
+ "description": "Cookie parameter object",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Cookie name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Cookie value.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "The request-URI to associate with the setting of the cookie. This value can affect the\ndefault domain, path, source port, and source scheme values of the created cookie.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "domain",
+ "description": "Cookie domain.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "path",
+ "description": "Cookie path.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "secure",
+ "description": "True if cookie is secure.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "httpOnly",
+ "description": "True if cookie is http-only.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sameSite",
+ "description": "Cookie SameSite type.",
+ "optional": true,
+ "$ref": "CookieSameSite"
+ },
+ {
+ "name": "expires",
+ "description": "Cookie expiration date, session cookie if not set",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "priority",
+ "description": "Cookie Priority.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "CookiePriority"
+ },
+ {
+ "name": "sameParty",
+ "description": "True if cookie is SameParty.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sourceScheme",
+ "description": "Cookie source scheme type.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "CookieSourceScheme"
+ },
+ {
+ "name": "sourcePort",
+ "description": "Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\nThis is a temporary ability and it will be removed in the future.",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "partitionKey",
+ "description": "Cookie partition key. The site of the top-level URL the browser was visiting at the start\nof the request to the endpoint that set the cookie.\nIf not set, the cookie will be set as not partitioned.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AuthChallenge",
+ "description": "Authorization challenge for HTTP status code 401 or 407.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "source",
+ "description": "Source of the authentication challenge.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "Server",
+ "Proxy"
+ ]
+ },
+ {
+ "name": "origin",
+ "description": "Origin of the challenger.",
+ "type": "string"
+ },
+ {
+ "name": "scheme",
+ "description": "The authentication scheme used, such as basic or digest",
+ "type": "string"
+ },
+ {
+ "name": "realm",
+ "description": "The realm of the challenge. May be empty.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AuthChallengeResponse",
+ "description": "Response to an AuthChallenge.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "response",
+ "description": "The decision on what to do in response to the authorization challenge. Default means\ndeferring to the default behavior of the net stack, which will likely either the Cancel\nauthentication or display a popup dialog box.",
+ "type": "string",
+ "enum": [
+ "Default",
+ "CancelAuth",
+ "ProvideCredentials"
+ ]
+ },
+ {
+ "name": "username",
+ "description": "The username to provide, possibly empty. Should only be set if response is\nProvideCredentials.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "password",
+ "description": "The password to provide, possibly empty. Should only be set if response is\nProvideCredentials.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "InterceptionStage",
+ "description": "Stages of the interception to begin intercepting. Request will intercept before the request is\nsent. Response will intercept after the response is received.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Request",
+ "HeadersReceived"
+ ]
+ },
+ {
+ "id": "RequestPattern",
+ "description": "Request pattern for interception.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "urlPattern",
+ "description": "Wildcards (`'*'` -> zero or more, `'?'` -> exactly one) are allowed. Escape character is\nbackslash. Omitting is equivalent to `\"*\"`.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "resourceType",
+ "description": "If set, only requests for matching resource types will be intercepted.",
+ "optional": true,
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "interceptionStage",
+ "description": "Stage at which to begin intercepting requests. Default is Request.",
+ "optional": true,
+ "$ref": "InterceptionStage"
+ }
+ ]
+ },
+ {
+ "id": "SignedExchangeSignature",
+ "description": "Information about a signed exchange signature.\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#rfc.section.3.1",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "label",
+ "description": "Signed exchange signature label.",
+ "type": "string"
+ },
+ {
+ "name": "signature",
+ "description": "The hex string of signed exchange signature.",
+ "type": "string"
+ },
+ {
+ "name": "integrity",
+ "description": "Signed exchange signature integrity.",
+ "type": "string"
+ },
+ {
+ "name": "certUrl",
+ "description": "Signed exchange signature cert Url.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "certSha256",
+ "description": "The hex string of signed exchange signature cert sha256.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "validityUrl",
+ "description": "Signed exchange signature validity Url.",
+ "type": "string"
+ },
+ {
+ "name": "date",
+ "description": "Signed exchange signature date.",
+ "type": "integer"
+ },
+ {
+ "name": "expires",
+ "description": "Signed exchange signature expires.",
+ "type": "integer"
+ },
+ {
+ "name": "certificates",
+ "description": "The encoded certificates.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SignedExchangeHeader",
+ "description": "Information about a signed exchange header.\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cbor-representation",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "requestUrl",
+ "description": "Signed exchange request URL.",
+ "type": "string"
+ },
+ {
+ "name": "responseCode",
+ "description": "Signed exchange response code.",
+ "type": "integer"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Signed exchange response headers.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "signatures",
+ "description": "Signed exchange response signature.",
+ "type": "array",
+ "items": {
+ "$ref": "SignedExchangeSignature"
+ }
+ },
+ {
+ "name": "headerIntegrity",
+ "description": "Signed exchange header integrity hash in the form of `sha256-<base64-hash-value>`.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "SignedExchangeErrorField",
+ "description": "Field type for a signed exchange related error.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "signatureSig",
+ "signatureIntegrity",
+ "signatureCertUrl",
+ "signatureCertSha256",
+ "signatureValidityUrl",
+ "signatureTimestamps"
+ ]
+ },
+ {
+ "id": "SignedExchangeError",
+ "description": "Information about a signed exchange response.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "message",
+ "description": "Error message.",
+ "type": "string"
+ },
+ {
+ "name": "signatureIndex",
+ "description": "The index of the signature which caused the error.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "errorField",
+ "description": "The field which caused the error.",
+ "optional": true,
+ "$ref": "SignedExchangeErrorField"
+ }
+ ]
+ },
+ {
+ "id": "SignedExchangeInfo",
+ "description": "Information about a signed exchange response.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "outerResponse",
+ "description": "The outer response of signed HTTP exchange which was received from network.",
+ "$ref": "Response"
+ },
+ {
+ "name": "header",
+ "description": "Information about the signed exchange header.",
+ "optional": true,
+ "$ref": "SignedExchangeHeader"
+ },
+ {
+ "name": "securityDetails",
+ "description": "Security details for the signed exchange header.",
+ "optional": true,
+ "$ref": "SecurityDetails"
+ },
+ {
+ "name": "errors",
+ "description": "Errors occurred while handling the signed exchagne.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "SignedExchangeError"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ContentEncoding",
+ "description": "List of content encodings supported by the backend.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "deflate",
+ "gzip",
+ "br",
+ "zstd"
+ ]
+ },
+ {
+ "id": "PrivateNetworkRequestPolicy",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Allow",
+ "BlockFromInsecureToMorePrivate",
+ "WarnFromInsecureToMorePrivate",
+ "PreflightBlock",
+ "PreflightWarn"
+ ]
+ },
+ {
+ "id": "IPAddressSpace",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Local",
+ "Private",
+ "Public",
+ "Unknown"
+ ]
+ },
+ {
+ "id": "ConnectTiming",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "requestTime",
+ "description": "Timing's requestTime is a baseline in seconds, while the other numbers are ticks in\nmilliseconds relatively to this requestTime. Matches ResourceTiming's requestTime for\nthe same request (but not for redirected requests).",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "ClientSecurityState",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "initiatorIsSecureContext",
+ "type": "boolean"
+ },
+ {
+ "name": "initiatorIPAddressSpace",
+ "$ref": "IPAddressSpace"
+ },
+ {
+ "name": "privateNetworkRequestPolicy",
+ "$ref": "PrivateNetworkRequestPolicy"
+ }
+ ]
+ },
+ {
+ "id": "CrossOriginOpenerPolicyValue",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "SameOrigin",
+ "SameOriginAllowPopups",
+ "RestrictProperties",
+ "UnsafeNone",
+ "SameOriginPlusCoep",
+ "RestrictPropertiesPlusCoep"
+ ]
+ },
+ {
+ "id": "CrossOriginOpenerPolicyStatus",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "value",
+ "$ref": "CrossOriginOpenerPolicyValue"
+ },
+ {
+ "name": "reportOnlyValue",
+ "$ref": "CrossOriginOpenerPolicyValue"
+ },
+ {
+ "name": "reportingEndpoint",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "reportOnlyReportingEndpoint",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CrossOriginEmbedderPolicyValue",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "None",
+ "Credentialless",
+ "RequireCorp"
+ ]
+ },
+ {
+ "id": "CrossOriginEmbedderPolicyStatus",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "value",
+ "$ref": "CrossOriginEmbedderPolicyValue"
+ },
+ {
+ "name": "reportOnlyValue",
+ "$ref": "CrossOriginEmbedderPolicyValue"
+ },
+ {
+ "name": "reportingEndpoint",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "reportOnlyReportingEndpoint",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "ContentSecurityPolicySource",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "HTTP",
+ "Meta"
+ ]
+ },
+ {
+ "id": "ContentSecurityPolicyStatus",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "effectiveDirectives",
+ "type": "string"
+ },
+ {
+ "name": "isEnforced",
+ "type": "boolean"
+ },
+ {
+ "name": "source",
+ "$ref": "ContentSecurityPolicySource"
+ }
+ ]
+ },
+ {
+ "id": "SecurityIsolationStatus",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "coop",
+ "optional": true,
+ "$ref": "CrossOriginOpenerPolicyStatus"
+ },
+ {
+ "name": "coep",
+ "optional": true,
+ "$ref": "CrossOriginEmbedderPolicyStatus"
+ },
+ {
+ "name": "csp",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "ContentSecurityPolicyStatus"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ReportStatus",
+ "description": "The status of a Reporting API report.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Queued",
+ "Pending",
+ "MarkedForRemoval",
+ "Success"
+ ]
+ },
+ {
+ "id": "ReportId",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "ReportingApiReport",
+ "description": "An object representing a report generated by the Reporting API.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "$ref": "ReportId"
+ },
+ {
+ "name": "initiatorUrl",
+ "description": "The URL of the document that triggered the report.",
+ "type": "string"
+ },
+ {
+ "name": "destination",
+ "description": "The name of the endpoint group that should be used to deliver the report.",
+ "type": "string"
+ },
+ {
+ "name": "type",
+ "description": "The type of the report (specifies the set of data that is contained in the report body).",
+ "type": "string"
+ },
+ {
+ "name": "timestamp",
+ "description": "When the report was generated.",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "depth",
+ "description": "How many uploads deep the related request was.",
+ "type": "integer"
+ },
+ {
+ "name": "completedAttempts",
+ "description": "The number of delivery attempts made so far, not including an active attempt.",
+ "type": "integer"
+ },
+ {
+ "name": "body",
+ "type": "object"
+ },
+ {
+ "name": "status",
+ "$ref": "ReportStatus"
+ }
+ ]
+ },
+ {
+ "id": "ReportingApiEndpoint",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "The URL of the endpoint to which reports may be delivered.",
+ "type": "string"
+ },
+ {
+ "name": "groupName",
+ "description": "Name of the endpoint group.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "LoadNetworkResourcePageResult",
+ "description": "An object providing the result of a network resource load.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "success",
+ "type": "boolean"
+ },
+ {
+ "name": "netError",
+ "description": "Optional values used for error reporting.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "netErrorName",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "httpStatusCode",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "stream",
+ "description": "If successful, one of the following two fields holds the result.",
+ "optional": true,
+ "$ref": "IO.StreamHandle"
+ },
+ {
+ "name": "headers",
+ "description": "Response headers.",
+ "optional": true,
+ "$ref": "Network.Headers"
+ }
+ ]
+ },
+ {
+ "id": "LoadNetworkResourceOptions",
+ "description": "An options object that may be extended later to better support CORS,\nCORB and streaming.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "disableCache",
+ "type": "boolean"
+ },
+ {
+ "name": "includeCredentials",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "setAcceptedEncodings",
+ "description": "Sets a list of content encodings that will be accepted. Empty list means no encoding is accepted.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "encodings",
+ "description": "List of accepted content encodings.",
+ "type": "array",
+ "items": {
+ "$ref": "ContentEncoding"
+ }
+ }
+ ]
+ },
+ {
+ "name": "clearAcceptedEncodingsOverride",
+ "description": "Clears accepted encodings set by setAcceptedEncodings",
+ "experimental": true
+ },
+ {
+ "name": "canClearBrowserCache",
+ "description": "Tells whether clearing browser cache is supported.",
+ "deprecated": true,
+ "returns": [
+ {
+ "name": "result",
+ "description": "True if browser cache can be cleared.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "canClearBrowserCookies",
+ "description": "Tells whether clearing browser cookies is supported.",
+ "deprecated": true,
+ "returns": [
+ {
+ "name": "result",
+ "description": "True if browser cookies can be cleared.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "canEmulateNetworkConditions",
+ "description": "Tells whether emulation of network conditions is supported.",
+ "deprecated": true,
+ "returns": [
+ {
+ "name": "result",
+ "description": "True if emulation of network conditions is supported.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "clearBrowserCache",
+ "description": "Clears browser cache."
+ },
+ {
+ "name": "clearBrowserCookies",
+ "description": "Clears browser cookies."
+ },
+ {
+ "name": "continueInterceptedRequest",
+ "description": "Response to Network.requestIntercepted which either modifies the request to continue with any\nmodifications, or blocks it, or completes it with the provided response bytes. If a network\nfetch occurs as a result which encounters a redirect an additional Network.requestIntercepted\nevent will be sent with the same InterceptionId.\nDeprecated, use Fetch.continueRequest, Fetch.fulfillRequest and Fetch.failRequest instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "interceptionId",
+ "$ref": "InterceptionId"
+ },
+ {
+ "name": "errorReason",
+ "description": "If set this causes the request to fail with the given reason. Passing `Aborted` for requests\nmarked with `isNavigationRequest` also cancels the navigation. Must not be set in response\nto an authChallenge.",
+ "optional": true,
+ "$ref": "ErrorReason"
+ },
+ {
+ "name": "rawResponse",
+ "description": "If set the requests completes using with the provided base64 encoded raw response, including\nHTTP status line and headers etc... Must not be set in response to an authChallenge. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "If set the request url will be modified in a way that's not observable by page. Must not be\nset in response to an authChallenge.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "method",
+ "description": "If set this allows the request method to be overridden. Must not be set in response to an\nauthChallenge.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "postData",
+ "description": "If set this allows postData to be set. Must not be set in response to an authChallenge.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "description": "If set this allows the request headers to be changed. Must not be set in response to an\nauthChallenge.",
+ "optional": true,
+ "$ref": "Headers"
+ },
+ {
+ "name": "authChallengeResponse",
+ "description": "Response to a requestIntercepted with an authChallenge. Must not be set otherwise.",
+ "optional": true,
+ "$ref": "AuthChallengeResponse"
+ }
+ ]
+ },
+ {
+ "name": "deleteCookies",
+ "description": "Deletes browser cookies with matching name and url or domain/path pair.",
+ "parameters": [
+ {
+ "name": "name",
+ "description": "Name of the cookies to remove.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "If specified, deletes all the cookies with the given name where domain and path match\nprovided URL.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "domain",
+ "description": "If specified, deletes only cookies with the exact domain.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "path",
+ "description": "If specified, deletes only cookies with the exact path.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables network tracking, prevents network events from being sent to the client."
+ },
+ {
+ "name": "emulateNetworkConditions",
+ "description": "Activates emulation of network conditions.",
+ "parameters": [
+ {
+ "name": "offline",
+ "description": "True to emulate internet disconnection.",
+ "type": "boolean"
+ },
+ {
+ "name": "latency",
+ "description": "Minimum latency from request sent to response headers received (ms).",
+ "type": "number"
+ },
+ {
+ "name": "downloadThroughput",
+ "description": "Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.",
+ "type": "number"
+ },
+ {
+ "name": "uploadThroughput",
+ "description": "Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.",
+ "type": "number"
+ },
+ {
+ "name": "connectionType",
+ "description": "Connection type if known.",
+ "optional": true,
+ "$ref": "ConnectionType"
+ }
+ ]
+ },
+ {
+ "name": "enable",
+ "description": "Enables network tracking, network events will now be delivered to the client.",
+ "parameters": [
+ {
+ "name": "maxTotalBufferSize",
+ "description": "Buffer size in bytes to use when preserving network payloads (XHRs, etc).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "maxResourceBufferSize",
+ "description": "Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "maxPostDataSize",
+ "description": "Longest post body size (in bytes) that would be included in requestWillBeSent notification",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "getAllCookies",
+ "description": "Returns all browser cookies. Depending on the backend support, will return detailed cookie\ninformation in the `cookies` field.\nDeprecated. Use Storage.getCookies instead.",
+ "deprecated": true,
+ "returns": [
+ {
+ "name": "cookies",
+ "description": "Array of cookie objects.",
+ "type": "array",
+ "items": {
+ "$ref": "Cookie"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getCertificate",
+ "description": "Returns the DER-encoded certificate.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin to get certificate for.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "tableNames",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getCookies",
+ "description": "Returns all browser cookies for the current URL. Depending on the backend support, will return\ndetailed cookie information in the `cookies` field.",
+ "parameters": [
+ {
+ "name": "urls",
+ "description": "The list of URLs for which applicable cookies will be fetched.\nIf not specified, it's assumed to be set to the list containing\nthe URLs of the page and all of its subframes.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "cookies",
+ "description": "Array of cookie objects.",
+ "type": "array",
+ "items": {
+ "$ref": "Cookie"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getResponseBody",
+ "description": "Returns content served for the given request.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier of the network request to get content for.",
+ "$ref": "RequestId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "body",
+ "description": "Response body.",
+ "type": "string"
+ },
+ {
+ "name": "base64Encoded",
+ "description": "True, if content was sent as base64.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getRequestPostData",
+ "description": "Returns post data sent with the request. Returns an error when no data was sent with the request.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier of the network request to get content for.",
+ "$ref": "RequestId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "postData",
+ "description": "Request body string, omitting files from multipart requests",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getResponseBodyForInterception",
+ "description": "Returns content served for the given currently intercepted request.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "interceptionId",
+ "description": "Identifier for the intercepted request to get body for.",
+ "$ref": "InterceptionId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "body",
+ "description": "Response body.",
+ "type": "string"
+ },
+ {
+ "name": "base64Encoded",
+ "description": "True, if content was sent as base64.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "takeResponseBodyForInterceptionAsStream",
+ "description": "Returns a handle to the stream representing the response body. Note that after this command,\nthe intercepted request can't be continued as is -- you either need to cancel it or to provide\nthe response body. The stream only supports sequential read, IO.read will fail if the position\nis specified.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "interceptionId",
+ "$ref": "InterceptionId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "stream",
+ "$ref": "IO.StreamHandle"
+ }
+ ]
+ },
+ {
+ "name": "replayXHR",
+ "description": "This method sends a new XMLHttpRequest which is identical to the original one. The following\nparameters should be identical: method, url, async, request body, extra headers, withCredentials\nattribute, user, password.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier of XHR to replay.",
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "searchInResponseBody",
+ "description": "Searches for given string in response content.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier of the network response to search.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "query",
+ "description": "String to search for.",
+ "type": "string"
+ },
+ {
+ "name": "caseSensitive",
+ "description": "If true, search is case sensitive.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isRegex",
+ "description": "If true, treats string parameter as regex.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "List of search matches.",
+ "type": "array",
+ "items": {
+ "$ref": "Debugger.SearchMatch"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setBlockedURLs",
+ "description": "Blocks URLs from loading.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "urls",
+ "description": "URL patterns to block. Wildcards ('*') are allowed.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setBypassServiceWorker",
+ "description": "Toggles ignoring of service worker for each request.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "bypass",
+ "description": "Bypass service worker and load from network.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setCacheDisabled",
+ "description": "Toggles ignoring cache for each request. If `true`, cache will not be used.",
+ "parameters": [
+ {
+ "name": "cacheDisabled",
+ "description": "Cache disabled state.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setCookie",
+ "description": "Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.",
+ "parameters": [
+ {
+ "name": "name",
+ "description": "Cookie name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Cookie value.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "The request-URI to associate with the setting of the cookie. This value can affect the\ndefault domain, path, source port, and source scheme values of the created cookie.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "domain",
+ "description": "Cookie domain.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "path",
+ "description": "Cookie path.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "secure",
+ "description": "True if cookie is secure.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "httpOnly",
+ "description": "True if cookie is http-only.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sameSite",
+ "description": "Cookie SameSite type.",
+ "optional": true,
+ "$ref": "CookieSameSite"
+ },
+ {
+ "name": "expires",
+ "description": "Cookie expiration date, session cookie if not set",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "priority",
+ "description": "Cookie Priority type.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "CookiePriority"
+ },
+ {
+ "name": "sameParty",
+ "description": "True if cookie is SameParty.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sourceScheme",
+ "description": "Cookie source scheme type.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "CookieSourceScheme"
+ },
+ {
+ "name": "sourcePort",
+ "description": "Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\nThis is a temporary ability and it will be removed in the future.",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "partitionKey",
+ "description": "Cookie partition key. The site of the top-level URL the browser was visiting at the start\nof the request to the endpoint that set the cookie.\nIf not set, the cookie will be set as not partitioned.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "success",
+ "description": "Always set to true. If an error occurs, the response indicates protocol error.",
+ "deprecated": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setCookies",
+ "description": "Sets given cookies.",
+ "parameters": [
+ {
+ "name": "cookies",
+ "description": "Cookies to be set.",
+ "type": "array",
+ "items": {
+ "$ref": "CookieParam"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setExtraHTTPHeaders",
+ "description": "Specifies whether to always send extra HTTP headers with the requests from this page.",
+ "parameters": [
+ {
+ "name": "headers",
+ "description": "Map with extra HTTP headers.",
+ "$ref": "Headers"
+ }
+ ]
+ },
+ {
+ "name": "setAttachDebugStack",
+ "description": "Specifies whether to attach a page script stack id in requests",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether to attach a page script stack for debugging purpose.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setRequestInterception",
+ "description": "Sets the requests to intercept that match the provided patterns and optionally resource types.\nDeprecated, please use Fetch.enable instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "patterns",
+ "description": "Requests matching any of these patterns will be forwarded and wait for the corresponding\ncontinueInterceptedRequest call.",
+ "type": "array",
+ "items": {
+ "$ref": "RequestPattern"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setUserAgentOverride",
+ "description": "Allows overriding user agent with the given string.",
+ "redirect": "Emulation",
+ "parameters": [
+ {
+ "name": "userAgent",
+ "description": "User agent to use.",
+ "type": "string"
+ },
+ {
+ "name": "acceptLanguage",
+ "description": "Browser langugage to emulate.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "platform",
+ "description": "The platform navigator.platform should return.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "userAgentMetadata",
+ "description": "To be sent in Sec-CH-UA-* headers and returned in navigator.userAgentData",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Emulation.UserAgentMetadata"
+ }
+ ]
+ },
+ {
+ "name": "getSecurityIsolationStatus",
+ "description": "Returns information about the COEP/COOP isolation status.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "If no frameId is provided, the status of the target is provided.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "status",
+ "$ref": "SecurityIsolationStatus"
+ }
+ ]
+ },
+ {
+ "name": "enableReportingApi",
+ "description": "Enables tracking for the Reporting API, events generated by the Reporting API will now be delivered to the client.\nEnabling triggers 'reportingApiReportAdded' for all existing reports.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enable",
+ "description": "Whether to enable or disable events for the Reporting API",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "loadNetworkResource",
+ "description": "Fetches the resource and returns the content.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Frame id to get the resource for. Mandatory for frame targets, and\nshould be omitted for worker targets.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource to get content for.",
+ "type": "string"
+ },
+ {
+ "name": "options",
+ "description": "Options for the request.",
+ "$ref": "LoadNetworkResourceOptions"
+ }
+ ],
+ "returns": [
+ {
+ "name": "resource",
+ "$ref": "LoadNetworkResourcePageResult"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "dataReceived",
+ "description": "Fired when data chunk was received over the network.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "dataLength",
+ "description": "Data chunk length.",
+ "type": "integer"
+ },
+ {
+ "name": "encodedDataLength",
+ "description": "Actual bytes received (might be less than dataLength for compressed encodings).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "eventSourceMessageReceived",
+ "description": "Fired when EventSource message is received.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "eventName",
+ "description": "Message type.",
+ "type": "string"
+ },
+ {
+ "name": "eventId",
+ "description": "Message identifier.",
+ "type": "string"
+ },
+ {
+ "name": "data",
+ "description": "Message content.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "loadingFailed",
+ "description": "Fired when HTTP request has failed to load.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "type",
+ "description": "Resource type.",
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "errorText",
+ "description": "User friendly error message.",
+ "type": "string"
+ },
+ {
+ "name": "canceled",
+ "description": "True if loading was canceled.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "blockedReason",
+ "description": "The reason why loading was blocked, if any.",
+ "optional": true,
+ "$ref": "BlockedReason"
+ },
+ {
+ "name": "corsErrorStatus",
+ "description": "The reason why loading was blocked by CORS, if any.",
+ "optional": true,
+ "$ref": "CorsErrorStatus"
+ }
+ ]
+ },
+ {
+ "name": "loadingFinished",
+ "description": "Fired when HTTP request has finished loading.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "encodedDataLength",
+ "description": "Total number of bytes received for this request.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "requestIntercepted",
+ "description": "Details of an intercepted HTTP request, which must be either allowed, blocked, modified or\nmocked.\nDeprecated, use Fetch.requestPaused instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "interceptionId",
+ "description": "Each request the page makes will have a unique id, however if any redirects are encountered\nwhile processing that fetch, they will be reported with the same id as the original fetch.\nLikewise if HTTP authentication is needed then the same fetch id will be used.",
+ "$ref": "InterceptionId"
+ },
+ {
+ "name": "request",
+ "$ref": "Request"
+ },
+ {
+ "name": "frameId",
+ "description": "The id of the frame that initiated the request.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "resourceType",
+ "description": "How the requested resource will be used.",
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "isNavigationRequest",
+ "description": "Whether this is a navigation request, which can abort the navigation completely.",
+ "type": "boolean"
+ },
+ {
+ "name": "isDownload",
+ "description": "Set if the request is a navigation that will result in a download.\nOnly present after response is received from the server (i.e. HeadersReceived stage).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "redirectUrl",
+ "description": "Redirect location, only sent if a redirect was intercepted.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "authChallenge",
+ "description": "Details of the Authorization Challenge encountered. If this is set then\ncontinueInterceptedRequest must contain an authChallengeResponse.",
+ "optional": true,
+ "$ref": "AuthChallenge"
+ },
+ {
+ "name": "responseErrorReason",
+ "description": "Response error if intercepted at response stage or if redirect occurred while intercepting\nrequest.",
+ "optional": true,
+ "$ref": "ErrorReason"
+ },
+ {
+ "name": "responseStatusCode",
+ "description": "Response code if intercepted at response stage or if redirect occurred while intercepting\nrequest or auth retry occurred.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Response headers if intercepted at the response stage or if redirect occurred while\nintercepting request or auth retry occurred.",
+ "optional": true,
+ "$ref": "Headers"
+ },
+ {
+ "name": "requestId",
+ "description": "If the intercepted request had a corresponding requestWillBeSent event fired for it, then\nthis requestId will be the same as the requestId present in the requestWillBeSent event.",
+ "optional": true,
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "requestServedFromCache",
+ "description": "Fired if request ended up loading from cache.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "requestWillBeSent",
+ "description": "Fired when page is about to send HTTP request.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Loader identifier. Empty string if the request is fetched from worker.",
+ "$ref": "LoaderId"
+ },
+ {
+ "name": "documentURL",
+ "description": "URL of the document this request is loaded for.",
+ "type": "string"
+ },
+ {
+ "name": "request",
+ "description": "Request data.",
+ "$ref": "Request"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "wallTime",
+ "description": "Timestamp.",
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "initiator",
+ "description": "Request initiator.",
+ "$ref": "Initiator"
+ },
+ {
+ "name": "redirectHasExtraInfo",
+ "description": "In the case that redirectResponse is populated, this flag indicates whether\nrequestWillBeSentExtraInfo and responseReceivedExtraInfo events will be or were emitted\nfor the request which was just redirected.",
+ "experimental": true,
+ "type": "boolean"
+ },
+ {
+ "name": "redirectResponse",
+ "description": "Redirect response data.",
+ "optional": true,
+ "$ref": "Response"
+ },
+ {
+ "name": "type",
+ "description": "Type of this resource.",
+ "optional": true,
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame identifier.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "hasUserGesture",
+ "description": "Whether the request is initiated by a user gesture. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "resourceChangedPriority",
+ "description": "Fired when resource loading priority is changed",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "newPriority",
+ "description": "New priority",
+ "$ref": "ResourcePriority"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "signedExchangeReceived",
+ "description": "Fired when a signed exchange was received over the network",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "info",
+ "description": "Information about the signed exchange response.",
+ "$ref": "SignedExchangeInfo"
+ }
+ ]
+ },
+ {
+ "name": "responseReceived",
+ "description": "Fired when HTTP response is available.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Loader identifier. Empty string if the request is fetched from worker.",
+ "$ref": "LoaderId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "type",
+ "description": "Resource type.",
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "response",
+ "description": "Response data.",
+ "$ref": "Response"
+ },
+ {
+ "name": "hasExtraInfo",
+ "description": "Indicates whether requestWillBeSentExtraInfo and responseReceivedExtraInfo events will be\nor were emitted for this request.",
+ "experimental": true,
+ "type": "boolean"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame identifier.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ]
+ },
+ {
+ "name": "webSocketClosed",
+ "description": "Fired when WebSocket is closed.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "webSocketCreated",
+ "description": "Fired upon WebSocket creation.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "url",
+ "description": "WebSocket request URL.",
+ "type": "string"
+ },
+ {
+ "name": "initiator",
+ "description": "Request initiator.",
+ "optional": true,
+ "$ref": "Initiator"
+ }
+ ]
+ },
+ {
+ "name": "webSocketFrameError",
+ "description": "Fired when WebSocket message error occurs.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "errorMessage",
+ "description": "WebSocket error message.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "webSocketFrameReceived",
+ "description": "Fired when WebSocket message is received.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "response",
+ "description": "WebSocket response data.",
+ "$ref": "WebSocketFrame"
+ }
+ ]
+ },
+ {
+ "name": "webSocketFrameSent",
+ "description": "Fired when WebSocket message is sent.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "response",
+ "description": "WebSocket response data.",
+ "$ref": "WebSocketFrame"
+ }
+ ]
+ },
+ {
+ "name": "webSocketHandshakeResponseReceived",
+ "description": "Fired when WebSocket handshake response becomes available.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "response",
+ "description": "WebSocket response data.",
+ "$ref": "WebSocketResponse"
+ }
+ ]
+ },
+ {
+ "name": "webSocketWillSendHandshakeRequest",
+ "description": "Fired when WebSocket is about to initiate handshake.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "wallTime",
+ "description": "UTC Timestamp.",
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "request",
+ "description": "WebSocket request data.",
+ "$ref": "WebSocketRequest"
+ }
+ ]
+ },
+ {
+ "name": "webTransportCreated",
+ "description": "Fired upon WebTransport creation.",
+ "parameters": [
+ {
+ "name": "transportId",
+ "description": "WebTransport identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "url",
+ "description": "WebTransport request URL.",
+ "type": "string"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "initiator",
+ "description": "Request initiator.",
+ "optional": true,
+ "$ref": "Initiator"
+ }
+ ]
+ },
+ {
+ "name": "webTransportConnectionEstablished",
+ "description": "Fired when WebTransport handshake is finished.",
+ "parameters": [
+ {
+ "name": "transportId",
+ "description": "WebTransport identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "webTransportClosed",
+ "description": "Fired when WebTransport is disposed.",
+ "parameters": [
+ {
+ "name": "transportId",
+ "description": "WebTransport identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "requestWillBeSentExtraInfo",
+ "description": "Fired when additional information about a requestWillBeSent event is available from the\nnetwork stack. Not every requestWillBeSent event will have an additional\nrequestWillBeSentExtraInfo fired for it, and there is no guarantee whether requestWillBeSent\nor requestWillBeSentExtraInfo will be fired first for the same request.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier. Used to match this information to an existing requestWillBeSent event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "associatedCookies",
+ "description": "A list of cookies potentially associated to the requested URL. This includes both cookies sent with\nthe request and the ones not sent; the latter are distinguished by having blockedReason field set.",
+ "type": "array",
+ "items": {
+ "$ref": "BlockedCookieWithReason"
+ }
+ },
+ {
+ "name": "headers",
+ "description": "Raw request headers as they will be sent over the wire.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "connectTiming",
+ "description": "Connection timing information for the request.",
+ "experimental": true,
+ "$ref": "ConnectTiming"
+ },
+ {
+ "name": "clientSecurityState",
+ "description": "The client security state set for the request.",
+ "optional": true,
+ "$ref": "ClientSecurityState"
+ },
+ {
+ "name": "siteHasCookieInOtherPartition",
+ "description": "Whether the site has partitioned cookies stored in a partition different than the current one.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "responseReceivedExtraInfo",
+ "description": "Fired when additional information about a responseReceived event is available from the network\nstack. Not every responseReceived event will have an additional responseReceivedExtraInfo for\nit, and responseReceivedExtraInfo may be fired before or after responseReceived.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier. Used to match this information to another responseReceived event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "blockedCookies",
+ "description": "A list of cookies which were not stored from the response along with the corresponding\nreasons for blocking. The cookies here may not be valid due to syntax errors, which\nare represented by the invalid cookie line string instead of a proper cookie.",
+ "type": "array",
+ "items": {
+ "$ref": "BlockedSetCookieWithReason"
+ }
+ },
+ {
+ "name": "headers",
+ "description": "Raw response headers as they were received over the wire.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "resourceIPAddressSpace",
+ "description": "The IP address space of the resource. The address space can only be determined once the transport\nestablished the connection, so we can't send it in `requestWillBeSentExtraInfo`.",
+ "$ref": "IPAddressSpace"
+ },
+ {
+ "name": "statusCode",
+ "description": "The status code of the response. This is useful in cases the request failed and no responseReceived\nevent is triggered, which is the case for, e.g., CORS errors. This is also the correct status code\nfor cached requests, where the status in responseReceived is a 200 and this will be 304.",
+ "type": "integer"
+ },
+ {
+ "name": "headersText",
+ "description": "Raw response header text as it was received over the wire. The raw text may not always be\navailable, such as in the case of HTTP/2 or QUIC.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cookiePartitionKey",
+ "description": "The cookie partition key that will be used to store partitioned cookies set in this response.\nOnly sent when partitioned cookies are enabled.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cookiePartitionKeyOpaque",
+ "description": "True if partitioned cookies are enabled, but the partition key is not serializeable to string.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "trustTokenOperationDone",
+ "description": "Fired exactly once for each Trust Token operation. Depending on\nthe type of the operation and whether the operation succeeded or\nfailed, the event is fired before the corresponding request was sent\nor after the response was received.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "status",
+ "description": "Detailed success or error status of the operation.\n'AlreadyExists' also signifies a successful operation, as the result\nof the operation already exists und thus, the operation was abort\npreemptively (e.g. a cache hit).",
+ "type": "string",
+ "enum": [
+ "Ok",
+ "InvalidArgument",
+ "MissingIssuerKeys",
+ "FailedPrecondition",
+ "ResourceExhausted",
+ "AlreadyExists",
+ "Unavailable",
+ "Unauthorized",
+ "BadResponse",
+ "InternalError",
+ "UnknownError",
+ "FulfilledLocally"
+ ]
+ },
+ {
+ "name": "type",
+ "$ref": "TrustTokenOperationType"
+ },
+ {
+ "name": "requestId",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "topLevelOrigin",
+ "description": "Top level origin. The context in which the operation was attempted.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "issuerOrigin",
+ "description": "Origin of the issuer in case of a \"Issuance\" or \"Redemption\" operation.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "issuedTokenCount",
+ "description": "The number of obtained Trust Tokens on a successful \"Issuance\" operation.",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "subresourceWebBundleMetadataReceived",
+ "description": "Fired once when parsing the .wbn file has succeeded.\nThe event contains the information about the web bundle contents.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier. Used to match this information to another event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "urls",
+ "description": "A list of URLs of resources in the subresource Web Bundle.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "subresourceWebBundleMetadataError",
+ "description": "Fired once when parsing the .wbn file has failed.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier. Used to match this information to another event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "errorMessage",
+ "description": "Error message",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "subresourceWebBundleInnerResponseParsed",
+ "description": "Fired when handling requests for resources within a .wbn file.\nNote: this will only be fired for resources that are requested by the webpage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "innerRequestId",
+ "description": "Request identifier of the subresource request",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "innerRequestURL",
+ "description": "URL of the subresource resource.",
+ "type": "string"
+ },
+ {
+ "name": "bundleRequestId",
+ "description": "Bundle request identifier. Used to match this information to another event.\nThis made be absent in case when the instrumentation was enabled only\nafter webbundle was parsed.",
+ "optional": true,
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "subresourceWebBundleInnerResponseError",
+ "description": "Fired when request for resources within a .wbn file failed.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "innerRequestId",
+ "description": "Request identifier of the subresource request",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "innerRequestURL",
+ "description": "URL of the subresource resource.",
+ "type": "string"
+ },
+ {
+ "name": "errorMessage",
+ "description": "Error message",
+ "type": "string"
+ },
+ {
+ "name": "bundleRequestId",
+ "description": "Bundle request identifier. Used to match this information to another event.\nThis made be absent in case when the instrumentation was enabled only\nafter webbundle was parsed.",
+ "optional": true,
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "reportingApiReportAdded",
+ "description": "Is sent whenever a new report is added.\nAnd after 'enableReportingApi' for all existing reports.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "report",
+ "$ref": "ReportingApiReport"
+ }
+ ]
+ },
+ {
+ "name": "reportingApiReportUpdated",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "report",
+ "$ref": "ReportingApiReport"
+ }
+ ]
+ },
+ {
+ "name": "reportingApiEndpointsChangedForOrigin",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin of the document(s) which configured the endpoints.",
+ "type": "string"
+ },
+ {
+ "name": "endpoints",
+ "type": "array",
+ "items": {
+ "$ref": "ReportingApiEndpoint"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Overlay",
+ "description": "This domain provides various functionality related to drawing atop the inspected page.",
+ "experimental": true,
+ "dependencies": [
+ "DOM",
+ "Page",
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "SourceOrderConfig",
+ "description": "Configuration data for drawing the source order of an elements children.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "parentOutlineColor",
+ "description": "the color to outline the givent element in.",
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "childOutlineColor",
+ "description": "the color to outline the child elements in.",
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "GridHighlightConfig",
+ "description": "Configuration data for the highlighting of Grid elements.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "showGridExtensionLines",
+ "description": "Whether the extension lines from grid cells to the rulers should be shown (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showPositiveLineNumbers",
+ "description": "Show Positive line number labels (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showNegativeLineNumbers",
+ "description": "Show Negative line number labels (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showAreaNames",
+ "description": "Show area name labels (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showLineNames",
+ "description": "Show line name labels (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showTrackSizes",
+ "description": "Show track size labels (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "gridBorderColor",
+ "description": "The grid container border highlight color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "cellBorderColor",
+ "description": "The cell border color (default: transparent). Deprecated, please use rowLineColor and columnLineColor instead.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "rowLineColor",
+ "description": "The row line color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "columnLineColor",
+ "description": "The column line color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "gridBorderDash",
+ "description": "Whether the grid border is dashed (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "cellBorderDash",
+ "description": "Whether the cell border is dashed (default: false). Deprecated, please us rowLineDash and columnLineDash instead.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "rowLineDash",
+ "description": "Whether row lines are dashed (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "columnLineDash",
+ "description": "Whether column lines are dashed (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "rowGapColor",
+ "description": "The row gap highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "rowHatchColor",
+ "description": "The row gap hatching fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "columnGapColor",
+ "description": "The column gap highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "columnHatchColor",
+ "description": "The column gap hatching fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "areaBorderColor",
+ "description": "The named grid areas border color (Default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "gridBackgroundColor",
+ "description": "The grid container background color (Default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "FlexContainerHighlightConfig",
+ "description": "Configuration data for the highlighting of Flex container elements.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "containerBorder",
+ "description": "The style of the container border",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "lineSeparator",
+ "description": "The style of the separator between lines",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "itemSeparator",
+ "description": "The style of the separator between items",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "mainDistributedSpace",
+ "description": "Style of content-distribution space on the main axis (justify-content).",
+ "optional": true,
+ "$ref": "BoxStyle"
+ },
+ {
+ "name": "crossDistributedSpace",
+ "description": "Style of content-distribution space on the cross axis (align-content).",
+ "optional": true,
+ "$ref": "BoxStyle"
+ },
+ {
+ "name": "rowGapSpace",
+ "description": "Style of empty space caused by row gaps (gap/row-gap).",
+ "optional": true,
+ "$ref": "BoxStyle"
+ },
+ {
+ "name": "columnGapSpace",
+ "description": "Style of empty space caused by columns gaps (gap/column-gap).",
+ "optional": true,
+ "$ref": "BoxStyle"
+ },
+ {
+ "name": "crossAlignment",
+ "description": "Style of the self-alignment line (align-items).",
+ "optional": true,
+ "$ref": "LineStyle"
+ }
+ ]
+ },
+ {
+ "id": "FlexItemHighlightConfig",
+ "description": "Configuration data for the highlighting of Flex item elements.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "baseSizeBox",
+ "description": "Style of the box representing the item's base size",
+ "optional": true,
+ "$ref": "BoxStyle"
+ },
+ {
+ "name": "baseSizeBorder",
+ "description": "Style of the border around the box representing the item's base size",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "flexibilityArrow",
+ "description": "Style of the arrow representing if the item grew or shrank",
+ "optional": true,
+ "$ref": "LineStyle"
+ }
+ ]
+ },
+ {
+ "id": "LineStyle",
+ "description": "Style information for drawing a line.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "color",
+ "description": "The color of the line (default: transparent)",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "pattern",
+ "description": "The line pattern (default: solid)",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "dashed",
+ "dotted"
+ ]
+ }
+ ]
+ },
+ {
+ "id": "BoxStyle",
+ "description": "Style information for drawing a box.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "fillColor",
+ "description": "The background color for the box (default: transparent)",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "hatchColor",
+ "description": "The hatching color for the box (default: transparent)",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "ContrastAlgorithm",
+ "type": "string",
+ "enum": [
+ "aa",
+ "aaa",
+ "apca"
+ ]
+ },
+ {
+ "id": "HighlightConfig",
+ "description": "Configuration data for the highlighting of page elements.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "showInfo",
+ "description": "Whether the node info tooltip should be shown (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showStyles",
+ "description": "Whether the node styles in the tooltip (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showRulers",
+ "description": "Whether the rulers should be shown (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showAccessibilityInfo",
+ "description": "Whether the a11y info should be shown (default: true).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showExtensionLines",
+ "description": "Whether the extension lines from node to the rulers should be shown (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "contentColor",
+ "description": "The content box highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "paddingColor",
+ "description": "The padding highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "borderColor",
+ "description": "The border highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "marginColor",
+ "description": "The margin highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "eventTargetColor",
+ "description": "The event target element highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "shapeColor",
+ "description": "The shape outside fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "shapeMarginColor",
+ "description": "The shape margin fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "cssGridColor",
+ "description": "The grid layout color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "colorFormat",
+ "description": "The color format used to format color styles (default: hex).",
+ "optional": true,
+ "$ref": "ColorFormat"
+ },
+ {
+ "name": "gridHighlightConfig",
+ "description": "The grid layout highlight configuration (default: all transparent).",
+ "optional": true,
+ "$ref": "GridHighlightConfig"
+ },
+ {
+ "name": "flexContainerHighlightConfig",
+ "description": "The flex container highlight configuration (default: all transparent).",
+ "optional": true,
+ "$ref": "FlexContainerHighlightConfig"
+ },
+ {
+ "name": "flexItemHighlightConfig",
+ "description": "The flex item highlight configuration (default: all transparent).",
+ "optional": true,
+ "$ref": "FlexItemHighlightConfig"
+ },
+ {
+ "name": "contrastAlgorithm",
+ "description": "The contrast algorithm to use for the contrast ratio (default: aa).",
+ "optional": true,
+ "$ref": "ContrastAlgorithm"
+ },
+ {
+ "name": "containerQueryContainerHighlightConfig",
+ "description": "The container query container highlight configuration (default: all transparent).",
+ "optional": true,
+ "$ref": "ContainerQueryContainerHighlightConfig"
+ }
+ ]
+ },
+ {
+ "id": "ColorFormat",
+ "type": "string",
+ "enum": [
+ "rgb",
+ "hsl",
+ "hwb",
+ "hex"
+ ]
+ },
+ {
+ "id": "GridNodeHighlightConfig",
+ "description": "Configurations for Persistent Grid Highlight",
+ "type": "object",
+ "properties": [
+ {
+ "name": "gridHighlightConfig",
+ "description": "A descriptor for the highlight appearance.",
+ "$ref": "GridHighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "id": "FlexNodeHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "flexContainerHighlightConfig",
+ "description": "A descriptor for the highlight appearance of flex containers.",
+ "$ref": "FlexContainerHighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "id": "ScrollSnapContainerHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "snapportBorder",
+ "description": "The style of the snapport border (default: transparent)",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "snapAreaBorder",
+ "description": "The style of the snap area border (default: transparent)",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "scrollMarginColor",
+ "description": "The margin highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "scrollPaddingColor",
+ "description": "The padding highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "ScrollSnapHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scrollSnapContainerHighlightConfig",
+ "description": "A descriptor for the highlight appearance of scroll snap containers.",
+ "$ref": "ScrollSnapContainerHighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "id": "HingeConfig",
+ "description": "Configuration for dual screen hinge",
+ "type": "object",
+ "properties": [
+ {
+ "name": "rect",
+ "description": "A rectangle represent hinge",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "contentColor",
+ "description": "The content box highlight fill color (default: a dark color).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "outlineColor",
+ "description": "The content box highlight outline color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "ContainerQueryHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "containerQueryContainerHighlightConfig",
+ "description": "A descriptor for the highlight appearance of container query containers.",
+ "$ref": "ContainerQueryContainerHighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the container node to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "id": "ContainerQueryContainerHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "containerBorder",
+ "description": "The style of the container border.",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "descendantBorder",
+ "description": "The style of the descendants' borders.",
+ "optional": true,
+ "$ref": "LineStyle"
+ }
+ ]
+ },
+ {
+ "id": "IsolatedElementHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "isolationModeHighlightConfig",
+ "description": "A descriptor for the highlight appearance of an element in isolation mode.",
+ "$ref": "IsolationModeHighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the isolated element to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "id": "IsolationModeHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "resizerColor",
+ "description": "The fill color of the resizers (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "resizerHandleColor",
+ "description": "The fill color for resizer handles (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "maskColor",
+ "description": "The fill color for the mask covering non-isolated elements (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "InspectMode",
+ "type": "string",
+ "enum": [
+ "searchForNode",
+ "searchForUAShadowDOM",
+ "captureAreaScreenshot",
+ "showDistances",
+ "none"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables domain notifications."
+ },
+ {
+ "name": "enable",
+ "description": "Enables domain notifications."
+ },
+ {
+ "name": "getHighlightObjectForTest",
+ "description": "For testing.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to get highlight object for.",
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "includeDistance",
+ "description": "Whether to include distance info.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeStyle",
+ "description": "Whether to include style info.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "colorFormat",
+ "description": "The color format to get config with (default: hex).",
+ "optional": true,
+ "$ref": "ColorFormat"
+ },
+ {
+ "name": "showAccessibilityInfo",
+ "description": "Whether to show accessibility info (default: true).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "highlight",
+ "description": "Highlight data for the node.",
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "name": "getGridHighlightObjectsForTest",
+ "description": "For Persistent Grid testing.",
+ "parameters": [
+ {
+ "name": "nodeIds",
+ "description": "Ids of the node to get highlight object for.",
+ "type": "array",
+ "items": {
+ "$ref": "DOM.NodeId"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "highlights",
+ "description": "Grid Highlight data for the node ids provided.",
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "name": "getSourceOrderHighlightObjectForTest",
+ "description": "For Source Order Viewer testing.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "highlight",
+ "description": "Source order highlight data for the node id provided.",
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "name": "hideHighlight",
+ "description": "Hides any highlight."
+ },
+ {
+ "name": "highlightFrame",
+ "description": "Highlights owner element of the frame with given id.\nDeprecated: Doesn't work reliablity and cannot be fixed due to process\nseparatation (the owner node might be in a different process). Determine\nthe owner node in the client and use highlightNode.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Identifier of the frame to highlight.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "contentColor",
+ "description": "The content box highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "contentOutlineColor",
+ "description": "The content box highlight outline color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "name": "highlightNode",
+ "description": "Highlights DOM node with given id or with the given JavaScript object wrapper. Either nodeId or\nobjectId must be specified.",
+ "parameters": [
+ {
+ "name": "highlightConfig",
+ "description": "A descriptor for the highlight appearance.",
+ "$ref": "HighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to highlight.",
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node to highlight.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node to be highlighted.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "selector",
+ "description": "Selectors to highlight relevant nodes.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "highlightQuad",
+ "description": "Highlights given quad. Coordinates are absolute with respect to the main frame viewport.",
+ "parameters": [
+ {
+ "name": "quad",
+ "description": "Quad to highlight",
+ "$ref": "DOM.Quad"
+ },
+ {
+ "name": "color",
+ "description": "The highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "outlineColor",
+ "description": "The highlight outline color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "name": "highlightRect",
+ "description": "Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport.",
+ "parameters": [
+ {
+ "name": "x",
+ "description": "X coordinate",
+ "type": "integer"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate",
+ "type": "integer"
+ },
+ {
+ "name": "width",
+ "description": "Rectangle width",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Rectangle height",
+ "type": "integer"
+ },
+ {
+ "name": "color",
+ "description": "The highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "outlineColor",
+ "description": "The highlight outline color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "name": "highlightSourceOrder",
+ "description": "Highlights the source order of the children of the DOM node with given id or with the given\nJavaScript object wrapper. Either nodeId or objectId must be specified.",
+ "parameters": [
+ {
+ "name": "sourceOrderConfig",
+ "description": "A descriptor for the appearance of the overlay drawing.",
+ "$ref": "SourceOrderConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to highlight.",
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node to highlight.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node to be highlighted.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "name": "setInspectMode",
+ "description": "Enters the 'inspect' mode. In this mode, elements that user is hovering over are highlighted.\nBackend then generates 'inspectNodeRequested' event upon element selection.",
+ "parameters": [
+ {
+ "name": "mode",
+ "description": "Set an inspection mode.",
+ "$ref": "InspectMode"
+ },
+ {
+ "name": "highlightConfig",
+ "description": "A descriptor for the highlight appearance of hovered-over nodes. May be omitted if `enabled\n== false`.",
+ "optional": true,
+ "$ref": "HighlightConfig"
+ }
+ ]
+ },
+ {
+ "name": "setShowAdHighlights",
+ "description": "Highlights owner element of all frames detected to be ads.",
+ "parameters": [
+ {
+ "name": "show",
+ "description": "True for showing ad highlights",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setPausedInDebuggerMessage",
+ "parameters": [
+ {
+ "name": "message",
+ "description": "The message to display, also triggers resume and step over controls.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setShowDebugBorders",
+ "description": "Requests that backend shows debug borders on layers",
+ "parameters": [
+ {
+ "name": "show",
+ "description": "True for showing debug borders",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowFPSCounter",
+ "description": "Requests that backend shows the FPS counter",
+ "parameters": [
+ {
+ "name": "show",
+ "description": "True for showing the FPS counter",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowGridOverlays",
+ "description": "Highlight multiple elements with the CSS Grid overlay.",
+ "parameters": [
+ {
+ "name": "gridNodeHighlightConfigs",
+ "description": "An array of node identifiers and descriptors for the highlight appearance.",
+ "type": "array",
+ "items": {
+ "$ref": "GridNodeHighlightConfig"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setShowFlexOverlays",
+ "parameters": [
+ {
+ "name": "flexNodeHighlightConfigs",
+ "description": "An array of node identifiers and descriptors for the highlight appearance.",
+ "type": "array",
+ "items": {
+ "$ref": "FlexNodeHighlightConfig"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setShowScrollSnapOverlays",
+ "parameters": [
+ {
+ "name": "scrollSnapHighlightConfigs",
+ "description": "An array of node identifiers and descriptors for the highlight appearance.",
+ "type": "array",
+ "items": {
+ "$ref": "ScrollSnapHighlightConfig"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setShowContainerQueryOverlays",
+ "parameters": [
+ {
+ "name": "containerQueryHighlightConfigs",
+ "description": "An array of node identifiers and descriptors for the highlight appearance.",
+ "type": "array",
+ "items": {
+ "$ref": "ContainerQueryHighlightConfig"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setShowPaintRects",
+ "description": "Requests that backend shows paint rectangles",
+ "parameters": [
+ {
+ "name": "result",
+ "description": "True for showing paint rectangles",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowLayoutShiftRegions",
+ "description": "Requests that backend shows layout shift regions",
+ "parameters": [
+ {
+ "name": "result",
+ "description": "True for showing layout shift regions",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowScrollBottleneckRects",
+ "description": "Requests that backend shows scroll bottleneck rects",
+ "parameters": [
+ {
+ "name": "show",
+ "description": "True for showing scroll bottleneck rects",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowHitTestBorders",
+ "description": "Deprecated, no longer has any effect.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "show",
+ "description": "True for showing hit-test borders",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowWebVitals",
+ "description": "Request that backend shows an overlay with web vital metrics.",
+ "parameters": [
+ {
+ "name": "show",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowViewportSizeOnResize",
+ "description": "Paints viewport size upon main frame resize.",
+ "parameters": [
+ {
+ "name": "show",
+ "description": "Whether to paint size or not.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowHinge",
+ "description": "Add a dual screen device hinge",
+ "parameters": [
+ {
+ "name": "hingeConfig",
+ "description": "hinge data, null means hideHinge",
+ "optional": true,
+ "$ref": "HingeConfig"
+ }
+ ]
+ },
+ {
+ "name": "setShowIsolatedElements",
+ "description": "Show elements in isolation mode with overlays.",
+ "parameters": [
+ {
+ "name": "isolatedElementHighlightConfigs",
+ "description": "An array of node identifiers and descriptors for the highlight appearance.",
+ "type": "array",
+ "items": {
+ "$ref": "IsolatedElementHighlightConfig"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "inspectNodeRequested",
+ "description": "Fired when the node should be inspected. This happens after call to `setInspectMode` or when\nuser manually inspects an element.",
+ "parameters": [
+ {
+ "name": "backendNodeId",
+ "description": "Id of the node to inspect.",
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ },
+ {
+ "name": "nodeHighlightRequested",
+ "description": "Fired when the node should be highlighted. This happens after call to `setInspectMode`.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "name": "screenshotRequested",
+ "description": "Fired when user asks to capture screenshot of some area on the page.",
+ "parameters": [
+ {
+ "name": "viewport",
+ "description": "Viewport to capture, in device independent pixels (dip).",
+ "$ref": "Page.Viewport"
+ }
+ ]
+ },
+ {
+ "name": "inspectModeCanceled",
+ "description": "Fired when user cancels the inspect mode."
+ }
+ ]
+ },
+ {
+ "domain": "Page",
+ "description": "Actions and events related to the inspected page belong to the page domain.",
+ "dependencies": [
+ "Debugger",
+ "DOM",
+ "IO",
+ "Network",
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "FrameId",
+ "description": "Unique frame identifier.",
+ "type": "string"
+ },
+ {
+ "id": "AdFrameType",
+ "description": "Indicates whether a frame has been identified as an ad.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "none",
+ "child",
+ "root"
+ ]
+ },
+ {
+ "id": "AdFrameExplanation",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "ParentIsAd",
+ "CreatedByAdScript",
+ "MatchedBlockingRule"
+ ]
+ },
+ {
+ "id": "AdFrameStatus",
+ "description": "Indicates whether a frame has been identified as an ad and why.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "adFrameType",
+ "$ref": "AdFrameType"
+ },
+ {
+ "name": "explanations",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AdFrameExplanation"
+ }
+ }
+ ]
+ },
+ {
+ "id": "AdScriptId",
+ "description": "Identifies the bottom-most script which caused the frame to be labelled\nas an ad.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "description": "Script Id of the bottom-most script which caused the frame to be labelled\nas an ad.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "debuggerId",
+ "description": "Id of adScriptId's debugger.",
+ "$ref": "Runtime.UniqueDebuggerId"
+ }
+ ]
+ },
+ {
+ "id": "SecureContextType",
+ "description": "Indicates whether the frame is a secure context and why it is the case.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Secure",
+ "SecureLocalhost",
+ "InsecureScheme",
+ "InsecureAncestor"
+ ]
+ },
+ {
+ "id": "CrossOriginIsolatedContextType",
+ "description": "Indicates whether the frame is cross-origin isolated and why it is the case.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Isolated",
+ "NotIsolated",
+ "NotIsolatedFeatureDisabled"
+ ]
+ },
+ {
+ "id": "GatedAPIFeatures",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "SharedArrayBuffers",
+ "SharedArrayBuffersTransferAllowed",
+ "PerformanceMeasureMemory",
+ "PerformanceProfile"
+ ]
+ },
+ {
+ "id": "PermissionsPolicyFeature",
+ "description": "All Permissions Policy features. This enum should match the one defined\nin third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "accelerometer",
+ "ambient-light-sensor",
+ "attribution-reporting",
+ "autoplay",
+ "bluetooth",
+ "browsing-topics",
+ "camera",
+ "ch-dpr",
+ "ch-device-memory",
+ "ch-downlink",
+ "ch-ect",
+ "ch-prefers-color-scheme",
+ "ch-prefers-reduced-motion",
+ "ch-rtt",
+ "ch-save-data",
+ "ch-ua",
+ "ch-ua-arch",
+ "ch-ua-bitness",
+ "ch-ua-platform",
+ "ch-ua-model",
+ "ch-ua-mobile",
+ "ch-ua-form-factor",
+ "ch-ua-full-version",
+ "ch-ua-full-version-list",
+ "ch-ua-platform-version",
+ "ch-ua-wow64",
+ "ch-viewport-height",
+ "ch-viewport-width",
+ "ch-width",
+ "clipboard-read",
+ "clipboard-write",
+ "compute-pressure",
+ "cross-origin-isolated",
+ "direct-sockets",
+ "display-capture",
+ "document-domain",
+ "encrypted-media",
+ "execution-while-out-of-viewport",
+ "execution-while-not-rendered",
+ "focus-without-user-activation",
+ "fullscreen",
+ "frobulate",
+ "gamepad",
+ "geolocation",
+ "gyroscope",
+ "hid",
+ "identity-credentials-get",
+ "idle-detection",
+ "interest-cohort",
+ "join-ad-interest-group",
+ "keyboard-map",
+ "local-fonts",
+ "magnetometer",
+ "microphone",
+ "midi",
+ "otp-credentials",
+ "payment",
+ "picture-in-picture",
+ "private-aggregation",
+ "private-state-token-issuance",
+ "private-state-token-redemption",
+ "publickey-credentials-get",
+ "run-ad-auction",
+ "screen-wake-lock",
+ "serial",
+ "shared-autofill",
+ "shared-storage",
+ "shared-storage-select-url",
+ "smart-card",
+ "storage-access",
+ "sync-xhr",
+ "unload",
+ "usb",
+ "vertical-scroll",
+ "web-share",
+ "window-management",
+ "window-placement",
+ "xr-spatial-tracking"
+ ]
+ },
+ {
+ "id": "PermissionsPolicyBlockReason",
+ "description": "Reason for a permissions policy feature to be disabled.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Header",
+ "IframeAttribute",
+ "InFencedFrameTree",
+ "InIsolatedApp"
+ ]
+ },
+ {
+ "id": "PermissionsPolicyBlockLocator",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "frameId",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "blockReason",
+ "$ref": "PermissionsPolicyBlockReason"
+ }
+ ]
+ },
+ {
+ "id": "PermissionsPolicyFeatureState",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "feature",
+ "$ref": "PermissionsPolicyFeature"
+ },
+ {
+ "name": "allowed",
+ "type": "boolean"
+ },
+ {
+ "name": "locator",
+ "optional": true,
+ "$ref": "PermissionsPolicyBlockLocator"
+ }
+ ]
+ },
+ {
+ "id": "OriginTrialTokenStatus",
+ "description": "Origin Trial(https://www.chromium.org/blink/origin-trials) support.\nStatus for an Origin Trial token.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Success",
+ "NotSupported",
+ "Insecure",
+ "Expired",
+ "WrongOrigin",
+ "InvalidSignature",
+ "Malformed",
+ "WrongVersion",
+ "FeatureDisabled",
+ "TokenDisabled",
+ "FeatureDisabledForUser",
+ "UnknownTrial"
+ ]
+ },
+ {
+ "id": "OriginTrialStatus",
+ "description": "Status for an Origin Trial.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Enabled",
+ "ValidTokenNotProvided",
+ "OSNotSupported",
+ "TrialNotAllowed"
+ ]
+ },
+ {
+ "id": "OriginTrialUsageRestriction",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "None",
+ "Subset"
+ ]
+ },
+ {
+ "id": "OriginTrialToken",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "origin",
+ "type": "string"
+ },
+ {
+ "name": "matchSubDomains",
+ "type": "boolean"
+ },
+ {
+ "name": "trialName",
+ "type": "string"
+ },
+ {
+ "name": "expiryTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "isThirdParty",
+ "type": "boolean"
+ },
+ {
+ "name": "usageRestriction",
+ "$ref": "OriginTrialUsageRestriction"
+ }
+ ]
+ },
+ {
+ "id": "OriginTrialTokenWithStatus",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "rawTokenText",
+ "type": "string"
+ },
+ {
+ "name": "parsedToken",
+ "description": "`parsedToken` is present only when the token is extractable and\nparsable.",
+ "optional": true,
+ "$ref": "OriginTrialToken"
+ },
+ {
+ "name": "status",
+ "$ref": "OriginTrialTokenStatus"
+ }
+ ]
+ },
+ {
+ "id": "OriginTrial",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "trialName",
+ "type": "string"
+ },
+ {
+ "name": "status",
+ "$ref": "OriginTrialStatus"
+ },
+ {
+ "name": "tokensWithStatus",
+ "type": "array",
+ "items": {
+ "$ref": "OriginTrialTokenWithStatus"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Frame",
+ "description": "Information about the Frame on the page.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "Frame unique identifier.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "parentId",
+ "description": "Parent frame identifier.",
+ "optional": true,
+ "$ref": "FrameId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Identifier of the loader associated with this frame.",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "name",
+ "description": "Frame's name as specified in the tag.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "Frame document's URL without fragment.",
+ "type": "string"
+ },
+ {
+ "name": "urlFragment",
+ "description": "Frame document's URL fragment including the '#'.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "domainAndRegistry",
+ "description": "Frame document's registered domain, taking the public suffixes list into account.\nExtracted from the Frame's url.\nExample URLs: http://www.google.com/file.html -> \"google.com\"\n http://a.b.co.uk/file.html -> \"b.co.uk\"",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "name": "securityOrigin",
+ "description": "Frame document's security origin.",
+ "type": "string"
+ },
+ {
+ "name": "mimeType",
+ "description": "Frame document's mimeType as determined by the browser.",
+ "type": "string"
+ },
+ {
+ "name": "unreachableUrl",
+ "description": "If the frame failed to load, this contains the URL that could not be loaded. Note that unlike url above, this URL may contain a fragment.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "adFrameStatus",
+ "description": "Indicates whether this frame was tagged as an ad and why.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "AdFrameStatus"
+ },
+ {
+ "name": "secureContextType",
+ "description": "Indicates whether the main document is a secure context and explains why that is the case.",
+ "experimental": true,
+ "$ref": "SecureContextType"
+ },
+ {
+ "name": "crossOriginIsolatedContextType",
+ "description": "Indicates whether this is a cross origin isolated context.",
+ "experimental": true,
+ "$ref": "CrossOriginIsolatedContextType"
+ },
+ {
+ "name": "gatedAPIFeatures",
+ "description": "Indicated which gated APIs / features are available.",
+ "experimental": true,
+ "type": "array",
+ "items": {
+ "$ref": "GatedAPIFeatures"
+ }
+ }
+ ]
+ },
+ {
+ "id": "FrameResource",
+ "description": "Information about the Resource on the page.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "Resource URL.",
+ "type": "string"
+ },
+ {
+ "name": "type",
+ "description": "Type of this resource.",
+ "$ref": "Network.ResourceType"
+ },
+ {
+ "name": "mimeType",
+ "description": "Resource mimeType as determined by the browser.",
+ "type": "string"
+ },
+ {
+ "name": "lastModified",
+ "description": "last-modified timestamp as reported by server.",
+ "optional": true,
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "contentSize",
+ "description": "Resource content size.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "failed",
+ "description": "True if the resource failed to load.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "canceled",
+ "description": "True if the resource was canceled during loading.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "FrameResourceTree",
+ "description": "Information about the Frame hierarchy along with their cached resources.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "frame",
+ "description": "Frame information for this tree item.",
+ "$ref": "Frame"
+ },
+ {
+ "name": "childFrames",
+ "description": "Child frames.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "FrameResourceTree"
+ }
+ },
+ {
+ "name": "resources",
+ "description": "Information about frame resources.",
+ "type": "array",
+ "items": {
+ "$ref": "FrameResource"
+ }
+ }
+ ]
+ },
+ {
+ "id": "FrameTree",
+ "description": "Information about the Frame hierarchy.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "frame",
+ "description": "Frame information for this tree item.",
+ "$ref": "Frame"
+ },
+ {
+ "name": "childFrames",
+ "description": "Child frames.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "FrameTree"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ScriptIdentifier",
+ "description": "Unique script identifier.",
+ "type": "string"
+ },
+ {
+ "id": "TransitionType",
+ "description": "Transition type.",
+ "type": "string",
+ "enum": [
+ "link",
+ "typed",
+ "address_bar",
+ "auto_bookmark",
+ "auto_subframe",
+ "manual_subframe",
+ "generated",
+ "auto_toplevel",
+ "form_submit",
+ "reload",
+ "keyword",
+ "keyword_generated",
+ "other"
+ ]
+ },
+ {
+ "id": "NavigationEntry",
+ "description": "Navigation history entry.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "Unique id of the navigation history entry.",
+ "type": "integer"
+ },
+ {
+ "name": "url",
+ "description": "URL of the navigation history entry.",
+ "type": "string"
+ },
+ {
+ "name": "userTypedURL",
+ "description": "URL that the user typed in the url bar.",
+ "type": "string"
+ },
+ {
+ "name": "title",
+ "description": "Title of the navigation history entry.",
+ "type": "string"
+ },
+ {
+ "name": "transitionType",
+ "description": "Transition type.",
+ "$ref": "TransitionType"
+ }
+ ]
+ },
+ {
+ "id": "ScreencastFrameMetadata",
+ "description": "Screencast frame metadata.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "offsetTop",
+ "description": "Top offset in DIP.",
+ "type": "number"
+ },
+ {
+ "name": "pageScaleFactor",
+ "description": "Page scale factor.",
+ "type": "number"
+ },
+ {
+ "name": "deviceWidth",
+ "description": "Device screen width in DIP.",
+ "type": "number"
+ },
+ {
+ "name": "deviceHeight",
+ "description": "Device screen height in DIP.",
+ "type": "number"
+ },
+ {
+ "name": "scrollOffsetX",
+ "description": "Position of horizontal scroll in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "scrollOffsetY",
+ "description": "Position of vertical scroll in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "timestamp",
+ "description": "Frame swap timestamp.",
+ "optional": true,
+ "$ref": "Network.TimeSinceEpoch"
+ }
+ ]
+ },
+ {
+ "id": "DialogType",
+ "description": "Javascript dialog type.",
+ "type": "string",
+ "enum": [
+ "alert",
+ "confirm",
+ "prompt",
+ "beforeunload"
+ ]
+ },
+ {
+ "id": "AppManifestError",
+ "description": "Error while paring app manifest.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "message",
+ "description": "Error message.",
+ "type": "string"
+ },
+ {
+ "name": "critical",
+ "description": "If criticial, this is a non-recoverable parse error.",
+ "type": "integer"
+ },
+ {
+ "name": "line",
+ "description": "Error line.",
+ "type": "integer"
+ },
+ {
+ "name": "column",
+ "description": "Error column.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "AppManifestParsedProperties",
+ "description": "Parsed app manifest properties.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "scope",
+ "description": "Computed scope value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "LayoutViewport",
+ "description": "Layout viewport position and dimensions.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "pageX",
+ "description": "Horizontal offset relative to the document (CSS pixels).",
+ "type": "integer"
+ },
+ {
+ "name": "pageY",
+ "description": "Vertical offset relative to the document (CSS pixels).",
+ "type": "integer"
+ },
+ {
+ "name": "clientWidth",
+ "description": "Width (CSS pixels), excludes scrollbar if present.",
+ "type": "integer"
+ },
+ {
+ "name": "clientHeight",
+ "description": "Height (CSS pixels), excludes scrollbar if present.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "VisualViewport",
+ "description": "Visual viewport position, dimensions, and scale.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "offsetX",
+ "description": "Horizontal offset relative to the layout viewport (CSS pixels).",
+ "type": "number"
+ },
+ {
+ "name": "offsetY",
+ "description": "Vertical offset relative to the layout viewport (CSS pixels).",
+ "type": "number"
+ },
+ {
+ "name": "pageX",
+ "description": "Horizontal offset relative to the document (CSS pixels).",
+ "type": "number"
+ },
+ {
+ "name": "pageY",
+ "description": "Vertical offset relative to the document (CSS pixels).",
+ "type": "number"
+ },
+ {
+ "name": "clientWidth",
+ "description": "Width (CSS pixels), excludes scrollbar if present.",
+ "type": "number"
+ },
+ {
+ "name": "clientHeight",
+ "description": "Height (CSS pixels), excludes scrollbar if present.",
+ "type": "number"
+ },
+ {
+ "name": "scale",
+ "description": "Scale relative to the ideal viewport (size at width=device-width).",
+ "type": "number"
+ },
+ {
+ "name": "zoom",
+ "description": "Page zoom factor (CSS to device independent pixels ratio).",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "Viewport",
+ "description": "Viewport for capturing screenshot.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "x",
+ "description": "X offset in device independent pixels (dip).",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y offset in device independent pixels (dip).",
+ "type": "number"
+ },
+ {
+ "name": "width",
+ "description": "Rectangle width in device independent pixels (dip).",
+ "type": "number"
+ },
+ {
+ "name": "height",
+ "description": "Rectangle height in device independent pixels (dip).",
+ "type": "number"
+ },
+ {
+ "name": "scale",
+ "description": "Page scale factor.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "FontFamilies",
+ "description": "Generic font families collection.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "standard",
+ "description": "The standard font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "fixed",
+ "description": "The fixed font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "serif",
+ "description": "The serif font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "sansSerif",
+ "description": "The sansSerif font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cursive",
+ "description": "The cursive font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "fantasy",
+ "description": "The fantasy font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "math",
+ "description": "The math font-family.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "ScriptFontFamilies",
+ "description": "Font families collection for a script.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "script",
+ "description": "Name of the script which these font families are defined for.",
+ "type": "string"
+ },
+ {
+ "name": "fontFamilies",
+ "description": "Generic font families collection for the script.",
+ "$ref": "FontFamilies"
+ }
+ ]
+ },
+ {
+ "id": "FontSizes",
+ "description": "Default font sizes.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "standard",
+ "description": "Default standard font size.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "fixed",
+ "description": "Default fixed font size.",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "ClientNavigationReason",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "formSubmissionGet",
+ "formSubmissionPost",
+ "httpHeaderRefresh",
+ "scriptInitiated",
+ "metaTagRefresh",
+ "pageBlockInterstitial",
+ "reload",
+ "anchorClick"
+ ]
+ },
+ {
+ "id": "ClientNavigationDisposition",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "currentTab",
+ "newTab",
+ "newWindow",
+ "download"
+ ]
+ },
+ {
+ "id": "InstallabilityErrorArgument",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Argument name (e.g. name:'minimum-icon-size-in-pixels').",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Argument value (e.g. value:'64').",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "InstallabilityError",
+ "description": "The installability error",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "errorId",
+ "description": "The error id (e.g. 'manifest-missing-suitable-icon').",
+ "type": "string"
+ },
+ {
+ "name": "errorArguments",
+ "description": "The list of error arguments (e.g. {name:'minimum-icon-size-in-pixels', value:'64'}).",
+ "type": "array",
+ "items": {
+ "$ref": "InstallabilityErrorArgument"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ReferrerPolicy",
+ "description": "The referring-policy used for the navigation.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "noReferrer",
+ "noReferrerWhenDowngrade",
+ "origin",
+ "originWhenCrossOrigin",
+ "sameOrigin",
+ "strictOrigin",
+ "strictOriginWhenCrossOrigin",
+ "unsafeUrl"
+ ]
+ },
+ {
+ "id": "CompilationCacheParams",
+ "description": "Per-script compilation cache parameters for `Page.produceCompilationCache`",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "The URL of the script to produce a compilation cache entry for.",
+ "type": "string"
+ },
+ {
+ "name": "eager",
+ "description": "A hint to the backend whether eager compilation is recommended.\n(the actual compilation mode used is upon backend discretion).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "AutoResponseMode",
+ "description": "Enum of possible auto-reponse for permisison / prompt dialogs.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "none",
+ "autoAccept",
+ "autoReject",
+ "autoOptOut"
+ ]
+ },
+ {
+ "id": "NavigationType",
+ "description": "The type of a frameNavigated event.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Navigation",
+ "BackForwardCacheRestore"
+ ]
+ },
+ {
+ "id": "BackForwardCacheNotRestoredReason",
+ "description": "List of not restored reasons for back-forward cache.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "NotPrimaryMainFrame",
+ "BackForwardCacheDisabled",
+ "RelatedActiveContentsExist",
+ "HTTPStatusNotOK",
+ "SchemeNotHTTPOrHTTPS",
+ "Loading",
+ "WasGrantedMediaAccess",
+ "DisableForRenderFrameHostCalled",
+ "DomainNotAllowed",
+ "HTTPMethodNotGET",
+ "SubframeIsNavigating",
+ "Timeout",
+ "CacheLimit",
+ "JavaScriptExecution",
+ "RendererProcessKilled",
+ "RendererProcessCrashed",
+ "SchedulerTrackedFeatureUsed",
+ "ConflictingBrowsingInstance",
+ "CacheFlushed",
+ "ServiceWorkerVersionActivation",
+ "SessionRestored",
+ "ServiceWorkerPostMessage",
+ "EnteredBackForwardCacheBeforeServiceWorkerHostAdded",
+ "RenderFrameHostReused_SameSite",
+ "RenderFrameHostReused_CrossSite",
+ "ServiceWorkerClaim",
+ "IgnoreEventAndEvict",
+ "HaveInnerContents",
+ "TimeoutPuttingInCache",
+ "BackForwardCacheDisabledByLowMemory",
+ "BackForwardCacheDisabledByCommandLine",
+ "NetworkRequestDatapipeDrainedAsBytesConsumer",
+ "NetworkRequestRedirected",
+ "NetworkRequestTimeout",
+ "NetworkExceedsBufferLimit",
+ "NavigationCancelledWhileRestoring",
+ "NotMostRecentNavigationEntry",
+ "BackForwardCacheDisabledForPrerender",
+ "UserAgentOverrideDiffers",
+ "ForegroundCacheLimit",
+ "BrowsingInstanceNotSwapped",
+ "BackForwardCacheDisabledForDelegate",
+ "UnloadHandlerExistsInMainFrame",
+ "UnloadHandlerExistsInSubFrame",
+ "ServiceWorkerUnregistration",
+ "CacheControlNoStore",
+ "CacheControlNoStoreCookieModified",
+ "CacheControlNoStoreHTTPOnlyCookieModified",
+ "NoResponseHead",
+ "Unknown",
+ "ActivationNavigationsDisallowedForBug1234857",
+ "ErrorDocument",
+ "FencedFramesEmbedder",
+ "CookieDisabled",
+ "HTTPAuthRequired",
+ "CookieFlushed",
+ "WebSocket",
+ "WebTransport",
+ "WebRTC",
+ "MainResourceHasCacheControlNoStore",
+ "MainResourceHasCacheControlNoCache",
+ "SubresourceHasCacheControlNoStore",
+ "SubresourceHasCacheControlNoCache",
+ "ContainsPlugins",
+ "DocumentLoaded",
+ "DedicatedWorkerOrWorklet",
+ "OutstandingNetworkRequestOthers",
+ "RequestedMIDIPermission",
+ "RequestedAudioCapturePermission",
+ "RequestedVideoCapturePermission",
+ "RequestedBackForwardCacheBlockedSensors",
+ "RequestedBackgroundWorkPermission",
+ "BroadcastChannel",
+ "WebXR",
+ "SharedWorker",
+ "WebLocks",
+ "WebHID",
+ "WebShare",
+ "RequestedStorageAccessGrant",
+ "WebNfc",
+ "OutstandingNetworkRequestFetch",
+ "OutstandingNetworkRequestXHR",
+ "AppBanner",
+ "Printing",
+ "WebDatabase",
+ "PictureInPicture",
+ "Portal",
+ "SpeechRecognizer",
+ "IdleManager",
+ "PaymentManager",
+ "SpeechSynthesis",
+ "KeyboardLock",
+ "WebOTPService",
+ "OutstandingNetworkRequestDirectSocket",
+ "InjectedJavascript",
+ "InjectedStyleSheet",
+ "KeepaliveRequest",
+ "IndexedDBEvent",
+ "Dummy",
+ "JsNetworkRequestReceivedCacheControlNoStoreResource",
+ "WebRTCSticky",
+ "WebTransportSticky",
+ "WebSocketSticky",
+ "ContentSecurityHandler",
+ "ContentWebAuthenticationAPI",
+ "ContentFileChooser",
+ "ContentSerial",
+ "ContentFileSystemAccess",
+ "ContentMediaDevicesDispatcherHost",
+ "ContentWebBluetooth",
+ "ContentWebUSB",
+ "ContentMediaSessionService",
+ "ContentScreenReader",
+ "EmbedderPopupBlockerTabHelper",
+ "EmbedderSafeBrowsingTriggeredPopupBlocker",
+ "EmbedderSafeBrowsingThreatDetails",
+ "EmbedderAppBannerManager",
+ "EmbedderDomDistillerViewerSource",
+ "EmbedderDomDistillerSelfDeletingRequestDelegate",
+ "EmbedderOomInterventionTabHelper",
+ "EmbedderOfflinePage",
+ "EmbedderChromePasswordManagerClientBindCredentialManager",
+ "EmbedderPermissionRequestManager",
+ "EmbedderModalDialog",
+ "EmbedderExtensions",
+ "EmbedderExtensionMessaging",
+ "EmbedderExtensionMessagingForOpenPort",
+ "EmbedderExtensionSentMessageToCachedFrame"
+ ]
+ },
+ {
+ "id": "BackForwardCacheNotRestoredReasonType",
+ "description": "Types of not restored reasons for back-forward cache.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "SupportPending",
+ "PageSupportNeeded",
+ "Circumstantial"
+ ]
+ },
+ {
+ "id": "BackForwardCacheNotRestoredExplanation",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Type of the reason",
+ "$ref": "BackForwardCacheNotRestoredReasonType"
+ },
+ {
+ "name": "reason",
+ "description": "Not restored reason",
+ "$ref": "BackForwardCacheNotRestoredReason"
+ },
+ {
+ "name": "context",
+ "description": "Context associated with the reason. The meaning of this context is\ndependent on the reason:\n- EmbedderExtensionSentMessageToCachedFrame: the extension ID.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "BackForwardCacheNotRestoredExplanationTree",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "URL of each frame",
+ "type": "string"
+ },
+ {
+ "name": "explanations",
+ "description": "Not restored reasons of each frame",
+ "type": "array",
+ "items": {
+ "$ref": "BackForwardCacheNotRestoredExplanation"
+ }
+ },
+ {
+ "name": "children",
+ "description": "Array of children frame",
+ "type": "array",
+ "items": {
+ "$ref": "BackForwardCacheNotRestoredExplanationTree"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "addScriptToEvaluateOnLoad",
+ "description": "Deprecated, please use addScriptToEvaluateOnNewDocument instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "scriptSource",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "identifier",
+ "description": "Identifier of the added script.",
+ "$ref": "ScriptIdentifier"
+ }
+ ]
+ },
+ {
+ "name": "addScriptToEvaluateOnNewDocument",
+ "description": "Evaluates given script in every frame upon creation (before loading frame's scripts).",
+ "parameters": [
+ {
+ "name": "source",
+ "type": "string"
+ },
+ {
+ "name": "worldName",
+ "description": "If specified, creates an isolated world with the given name and evaluates given script in it.\nThis world name will be used as the ExecutionContextDescription::name when the corresponding\nevent is emitted.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "includeCommandLineAPI",
+ "description": "Specifies whether command line API should be available to the script, defaults\nto false.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "runImmediately",
+ "description": "If true, runs the script immediately on existing execution contexts or worlds.\nDefault: false.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "identifier",
+ "description": "Identifier of the added script.",
+ "$ref": "ScriptIdentifier"
+ }
+ ]
+ },
+ {
+ "name": "bringToFront",
+ "description": "Brings page to front (activates tab)."
+ },
+ {
+ "name": "captureScreenshot",
+ "description": "Capture page screenshot.",
+ "parameters": [
+ {
+ "name": "format",
+ "description": "Image compression format (defaults to png).",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "jpeg",
+ "png",
+ "webp"
+ ]
+ },
+ {
+ "name": "quality",
+ "description": "Compression quality from range [0..100] (jpeg only).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "clip",
+ "description": "Capture the screenshot of a given region only.",
+ "optional": true,
+ "$ref": "Viewport"
+ },
+ {
+ "name": "fromSurface",
+ "description": "Capture the screenshot from the surface, rather than the view. Defaults to true.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "captureBeyondViewport",
+ "description": "Capture the screenshot beyond the viewport. Defaults to false.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "optimizeForSpeed",
+ "description": "Optimize image encoding for speed, not for resulting size (defaults to false)",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "data",
+ "description": "Base64-encoded image data. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "captureSnapshot",
+ "description": "Returns a snapshot of the page as a string. For MHTML format, the serialization includes\niframes, shadow DOM, external resources, and element-inline styles.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "format",
+ "description": "Format (defaults to mhtml).",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "mhtml"
+ ]
+ }
+ ],
+ "returns": [
+ {
+ "name": "data",
+ "description": "Serialized page data.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "clearDeviceMetricsOverride",
+ "description": "Clears the overridden device metrics.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "Emulation"
+ },
+ {
+ "name": "clearDeviceOrientationOverride",
+ "description": "Clears the overridden Device Orientation.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "DeviceOrientation"
+ },
+ {
+ "name": "clearGeolocationOverride",
+ "description": "Clears the overridden Geolocation Position and Error.",
+ "deprecated": true,
+ "redirect": "Emulation"
+ },
+ {
+ "name": "createIsolatedWorld",
+ "description": "Creates an isolated world for the given frame.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame in which the isolated world should be created.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "worldName",
+ "description": "An optional name which is reported in the Execution Context.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "grantUniveralAccess",
+ "description": "Whether or not universal access should be granted to the isolated world. This is a powerful\noption, use with caution.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "executionContextId",
+ "description": "Execution context of the isolated world.",
+ "$ref": "Runtime.ExecutionContextId"
+ }
+ ]
+ },
+ {
+ "name": "deleteCookie",
+ "description": "Deletes browser cookie with given name, domain and path.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "Network",
+ "parameters": [
+ {
+ "name": "cookieName",
+ "description": "Name of the cookie to remove.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "URL to match cooke domain and path.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables page domain notifications."
+ },
+ {
+ "name": "enable",
+ "description": "Enables page domain notifications."
+ },
+ {
+ "name": "getAppManifest",
+ "returns": [
+ {
+ "name": "url",
+ "description": "Manifest location.",
+ "type": "string"
+ },
+ {
+ "name": "errors",
+ "type": "array",
+ "items": {
+ "$ref": "AppManifestError"
+ }
+ },
+ {
+ "name": "data",
+ "description": "Manifest content.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "parsed",
+ "description": "Parsed manifest properties",
+ "experimental": true,
+ "optional": true,
+ "$ref": "AppManifestParsedProperties"
+ }
+ ]
+ },
+ {
+ "name": "getInstallabilityErrors",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "installabilityErrors",
+ "type": "array",
+ "items": {
+ "$ref": "InstallabilityError"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getManifestIcons",
+ "description": "Deprecated because it's not guaranteed that the returned icon is in fact the one used for PWA installation.",
+ "experimental": true,
+ "deprecated": true,
+ "returns": [
+ {
+ "name": "primaryIcon",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getAppId",
+ "description": "Returns the unique (PWA) app id.\nOnly returns values if the feature flag 'WebAppEnableManifestId' is enabled",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "appId",
+ "description": "App id, either from manifest's id attribute or computed from start_url",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "recommendedId",
+ "description": "Recommendation for manifest's id attribute to match current id computed from start_url",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getAdScriptId",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "$ref": "FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "adScriptId",
+ "description": "Identifies the bottom-most script which caused the frame to be labelled\nas an ad. Only sent if frame is labelled as an ad and id is available.",
+ "optional": true,
+ "$ref": "AdScriptId"
+ }
+ ]
+ },
+ {
+ "name": "getCookies",
+ "description": "Returns all browser cookies for the page and all of its subframes. Depending\non the backend support, will return detailed cookie information in the\n`cookies` field.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "Network",
+ "returns": [
+ {
+ "name": "cookies",
+ "description": "Array of cookie objects.",
+ "type": "array",
+ "items": {
+ "$ref": "Network.Cookie"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getFrameTree",
+ "description": "Returns present frame tree structure.",
+ "returns": [
+ {
+ "name": "frameTree",
+ "description": "Present frame tree structure.",
+ "$ref": "FrameTree"
+ }
+ ]
+ },
+ {
+ "name": "getLayoutMetrics",
+ "description": "Returns metrics relating to the layouting of the page, such as viewport bounds/scale.",
+ "returns": [
+ {
+ "name": "layoutViewport",
+ "description": "Deprecated metrics relating to the layout viewport. Is in device pixels. Use `cssLayoutViewport` instead.",
+ "deprecated": true,
+ "$ref": "LayoutViewport"
+ },
+ {
+ "name": "visualViewport",
+ "description": "Deprecated metrics relating to the visual viewport. Is in device pixels. Use `cssVisualViewport` instead.",
+ "deprecated": true,
+ "$ref": "VisualViewport"
+ },
+ {
+ "name": "contentSize",
+ "description": "Deprecated size of scrollable area. Is in DP. Use `cssContentSize` instead.",
+ "deprecated": true,
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "cssLayoutViewport",
+ "description": "Metrics relating to the layout viewport in CSS pixels.",
+ "$ref": "LayoutViewport"
+ },
+ {
+ "name": "cssVisualViewport",
+ "description": "Metrics relating to the visual viewport in CSS pixels.",
+ "$ref": "VisualViewport"
+ },
+ {
+ "name": "cssContentSize",
+ "description": "Size of scrollable area in CSS pixels.",
+ "$ref": "DOM.Rect"
+ }
+ ]
+ },
+ {
+ "name": "getNavigationHistory",
+ "description": "Returns navigation history for the current page.",
+ "returns": [
+ {
+ "name": "currentIndex",
+ "description": "Index of the current navigation history entry.",
+ "type": "integer"
+ },
+ {
+ "name": "entries",
+ "description": "Array of navigation history entries.",
+ "type": "array",
+ "items": {
+ "$ref": "NavigationEntry"
+ }
+ }
+ ]
+ },
+ {
+ "name": "resetNavigationHistory",
+ "description": "Resets navigation history for the current page."
+ },
+ {
+ "name": "getResourceContent",
+ "description": "Returns content of the given resource.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Frame id to get resource for.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource to get content for.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "content",
+ "description": "Resource content.",
+ "type": "string"
+ },
+ {
+ "name": "base64Encoded",
+ "description": "True, if content was served as base64.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getResourceTree",
+ "description": "Returns present frame / resource tree structure.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "frameTree",
+ "description": "Present frame / resource tree structure.",
+ "$ref": "FrameResourceTree"
+ }
+ ]
+ },
+ {
+ "name": "handleJavaScriptDialog",
+ "description": "Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).",
+ "parameters": [
+ {
+ "name": "accept",
+ "description": "Whether to accept or dismiss the dialog.",
+ "type": "boolean"
+ },
+ {
+ "name": "promptText",
+ "description": "The text to enter into the dialog prompt before accepting. Used only if this is a prompt\ndialog.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "navigate",
+ "description": "Navigates current page to the given URL.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "URL to navigate the page to.",
+ "type": "string"
+ },
+ {
+ "name": "referrer",
+ "description": "Referrer URL.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "transitionType",
+ "description": "Intended transition type.",
+ "optional": true,
+ "$ref": "TransitionType"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame id to navigate, if not specified navigates the top frame.",
+ "optional": true,
+ "$ref": "FrameId"
+ },
+ {
+ "name": "referrerPolicy",
+ "description": "Referrer-policy used for the navigation.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "ReferrerPolicy"
+ }
+ ],
+ "returns": [
+ {
+ "name": "frameId",
+ "description": "Frame id that has navigated (or failed to navigate)",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Loader identifier. This is omitted in case of same-document navigation,\nas the previously committed loaderId would not change.",
+ "optional": true,
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "errorText",
+ "description": "User friendly error message, present if and only if navigation has failed.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "navigateToHistoryEntry",
+ "description": "Navigates current page to the given history entry.",
+ "parameters": [
+ {
+ "name": "entryId",
+ "description": "Unique id of the entry to navigate to.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "printToPDF",
+ "description": "Print page as PDF.",
+ "parameters": [
+ {
+ "name": "landscape",
+ "description": "Paper orientation. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "displayHeaderFooter",
+ "description": "Display header and footer. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "printBackground",
+ "description": "Print background graphics. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "scale",
+ "description": "Scale of the webpage rendering. Defaults to 1.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "paperWidth",
+ "description": "Paper width in inches. Defaults to 8.5 inches.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "paperHeight",
+ "description": "Paper height in inches. Defaults to 11 inches.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "marginTop",
+ "description": "Top margin in inches. Defaults to 1cm (~0.4 inches).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "marginBottom",
+ "description": "Bottom margin in inches. Defaults to 1cm (~0.4 inches).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "marginLeft",
+ "description": "Left margin in inches. Defaults to 1cm (~0.4 inches).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "marginRight",
+ "description": "Right margin in inches. Defaults to 1cm (~0.4 inches).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "pageRanges",
+ "description": "Paper ranges to print, one based, e.g., '1-5, 8, 11-13'. Pages are\nprinted in the document order, not in the order specified, and no\nmore than once.\nDefaults to empty string, which implies the entire document is printed.\nThe page numbers are quietly capped to actual page count of the\ndocument, and ranges beyond the end of the document are ignored.\nIf this results in no pages to print, an error is reported.\nIt is an error to specify a range with start greater than end.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "headerTemplate",
+ "description": "HTML template for the print header. Should be valid HTML markup with following\nclasses used to inject printing values into them:\n- `date`: formatted print date\n- `title`: document title\n- `url`: document location\n- `pageNumber`: current page number\n- `totalPages`: total pages in the document\n\nFor example, `<span class=title></span>` would generate span containing the title.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "footerTemplate",
+ "description": "HTML template for the print footer. Should use the same format as the `headerTemplate`.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "preferCSSPageSize",
+ "description": "Whether or not to prefer page size as defined by css. Defaults to false,\nin which case the content will be scaled to fit the paper size.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "transferMode",
+ "description": "return as stream",
+ "experimental": true,
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "ReturnAsBase64",
+ "ReturnAsStream"
+ ]
+ }
+ ],
+ "returns": [
+ {
+ "name": "data",
+ "description": "Base64-encoded pdf data. Empty if |returnAsStream| is specified. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ },
+ {
+ "name": "stream",
+ "description": "A handle of the stream that holds resulting PDF data.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "IO.StreamHandle"
+ }
+ ]
+ },
+ {
+ "name": "reload",
+ "description": "Reloads given page optionally ignoring the cache.",
+ "parameters": [
+ {
+ "name": "ignoreCache",
+ "description": "If true, browser cache is ignored (as if the user pressed Shift+refresh).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "scriptToEvaluateOnLoad",
+ "description": "If set, the script will be injected into all frames of the inspected page after reload.\nArgument will be ignored if reloading dataURL origin.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeScriptToEvaluateOnLoad",
+ "description": "Deprecated, please use removeScriptToEvaluateOnNewDocument instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "identifier",
+ "$ref": "ScriptIdentifier"
+ }
+ ]
+ },
+ {
+ "name": "removeScriptToEvaluateOnNewDocument",
+ "description": "Removes given script from the list.",
+ "parameters": [
+ {
+ "name": "identifier",
+ "$ref": "ScriptIdentifier"
+ }
+ ]
+ },
+ {
+ "name": "screencastFrameAck",
+ "description": "Acknowledges that a screencast frame has been received by the frontend.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "sessionId",
+ "description": "Frame number.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "searchInResource",
+ "description": "Searches for given string in resource content.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Frame id for resource to search in.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource to search in.",
+ "type": "string"
+ },
+ {
+ "name": "query",
+ "description": "String to search for.",
+ "type": "string"
+ },
+ {
+ "name": "caseSensitive",
+ "description": "If true, search is case sensitive.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isRegex",
+ "description": "If true, treats string parameter as regex.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "List of search matches.",
+ "type": "array",
+ "items": {
+ "$ref": "Debugger.SearchMatch"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setAdBlockingEnabled",
+ "description": "Enable Chrome's experimental ad filter on all sites.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether to block ads.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setBypassCSP",
+ "description": "Enable page Content Security Policy by-passing.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether to bypass page CSP.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getPermissionsPolicyState",
+ "description": "Get Permissions Policy state on given frame.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "$ref": "FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "states",
+ "type": "array",
+ "items": {
+ "$ref": "PermissionsPolicyFeatureState"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getOriginTrials",
+ "description": "Get Origin Trials on given frame.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "$ref": "FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "originTrials",
+ "type": "array",
+ "items": {
+ "$ref": "OriginTrial"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setDeviceMetricsOverride",
+ "description": "Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\nwindow.innerWidth, window.innerHeight, and \"device-width\"/\"device-height\"-related CSS media\nquery results).",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "Emulation",
+ "parameters": [
+ {
+ "name": "width",
+ "description": "Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.",
+ "type": "integer"
+ },
+ {
+ "name": "deviceScaleFactor",
+ "description": "Overriding device scale factor value. 0 disables the override.",
+ "type": "number"
+ },
+ {
+ "name": "mobile",
+ "description": "Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\nautosizing and more.",
+ "type": "boolean"
+ },
+ {
+ "name": "scale",
+ "description": "Scale to apply to resulting view image.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "screenWidth",
+ "description": "Overriding screen width value in pixels (minimum 0, maximum 10000000).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "screenHeight",
+ "description": "Overriding screen height value in pixels (minimum 0, maximum 10000000).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "positionX",
+ "description": "Overriding view X position on screen in pixels (minimum 0, maximum 10000000).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "positionY",
+ "description": "Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "dontSetVisibleSize",
+ "description": "Do not set visible view size, rely upon explicit setVisibleSize call.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "screenOrientation",
+ "description": "Screen orientation override.",
+ "optional": true,
+ "$ref": "Emulation.ScreenOrientation"
+ },
+ {
+ "name": "viewport",
+ "description": "The viewport dimensions and scale. If not set, the override is cleared.",
+ "optional": true,
+ "$ref": "Viewport"
+ }
+ ]
+ },
+ {
+ "name": "setDeviceOrientationOverride",
+ "description": "Overrides the Device Orientation.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "DeviceOrientation",
+ "parameters": [
+ {
+ "name": "alpha",
+ "description": "Mock alpha",
+ "type": "number"
+ },
+ {
+ "name": "beta",
+ "description": "Mock beta",
+ "type": "number"
+ },
+ {
+ "name": "gamma",
+ "description": "Mock gamma",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setFontFamilies",
+ "description": "Set generic font families.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "fontFamilies",
+ "description": "Specifies font families to set. If a font family is not specified, it won't be changed.",
+ "$ref": "FontFamilies"
+ },
+ {
+ "name": "forScripts",
+ "description": "Specifies font families to set for individual scripts.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "ScriptFontFamilies"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setFontSizes",
+ "description": "Set default font sizes.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "fontSizes",
+ "description": "Specifies font sizes to set. If a font size is not specified, it won't be changed.",
+ "$ref": "FontSizes"
+ }
+ ]
+ },
+ {
+ "name": "setDocumentContent",
+ "description": "Sets given markup as the document's HTML.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Frame id to set HTML for.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "html",
+ "description": "HTML content to set.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setDownloadBehavior",
+ "description": "Set the behavior when downloading a file.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "behavior",
+ "description": "Whether to allow all or deny all download requests, or use default Chrome behavior if\navailable (otherwise deny).",
+ "type": "string",
+ "enum": [
+ "deny",
+ "allow",
+ "default"
+ ]
+ },
+ {
+ "name": "downloadPath",
+ "description": "The default path to save downloaded files to. This is required if behavior is set to 'allow'",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setGeolocationOverride",
+ "description": "Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\nunavailable.",
+ "deprecated": true,
+ "redirect": "Emulation",
+ "parameters": [
+ {
+ "name": "latitude",
+ "description": "Mock latitude",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "longitude",
+ "description": "Mock longitude",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "accuracy",
+ "description": "Mock accuracy",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setLifecycleEventsEnabled",
+ "description": "Controls whether page will emit lifecycle events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "If true, starts emitting lifecycle events.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setTouchEmulationEnabled",
+ "description": "Toggles mouse event-based touch event emulation.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "Emulation",
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether the touch event emulation should be enabled.",
+ "type": "boolean"
+ },
+ {
+ "name": "configuration",
+ "description": "Touch/gesture events configuration. Default: current platform.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "mobile",
+ "desktop"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "startScreencast",
+ "description": "Starts sending each frame using the `screencastFrame` event.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "format",
+ "description": "Image compression format.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "jpeg",
+ "png"
+ ]
+ },
+ {
+ "name": "quality",
+ "description": "Compression quality from range [0..100].",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "maxWidth",
+ "description": "Maximum screenshot width.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "maxHeight",
+ "description": "Maximum screenshot height.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "everyNthFrame",
+ "description": "Send every n-th frame.",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "stopLoading",
+ "description": "Force the page stop all navigations and pending resource fetches."
+ },
+ {
+ "name": "crash",
+ "description": "Crashes renderer on the IO thread, generates minidumps.",
+ "experimental": true
+ },
+ {
+ "name": "close",
+ "description": "Tries to close page, running its beforeunload hooks, if any.",
+ "experimental": true
+ },
+ {
+ "name": "setWebLifecycleState",
+ "description": "Tries to update the web lifecycle state of the page.\nIt will transition the page to the given state according to:\nhttps://github.com/WICG/web-lifecycle/",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "state",
+ "description": "Target lifecycle state",
+ "type": "string",
+ "enum": [
+ "frozen",
+ "active"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "stopScreencast",
+ "description": "Stops sending each frame in the `screencastFrame`.",
+ "experimental": true
+ },
+ {
+ "name": "produceCompilationCache",
+ "description": "Requests backend to produce compilation cache for the specified scripts.\n`scripts` are appeneded to the list of scripts for which the cache\nwould be produced. The list may be reset during page navigation.\nWhen script with a matching URL is encountered, the cache is optionally\nproduced upon backend discretion, based on internal heuristics.\nSee also: `Page.compilationCacheProduced`.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "scripts",
+ "type": "array",
+ "items": {
+ "$ref": "CompilationCacheParams"
+ }
+ }
+ ]
+ },
+ {
+ "name": "addCompilationCache",
+ "description": "Seeds compilation cache for given url. Compilation cache does not survive\ncross-process navigation.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "data",
+ "description": "Base64-encoded data (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "clearCompilationCache",
+ "description": "Clears seeded compilation cache.",
+ "experimental": true
+ },
+ {
+ "name": "setSPCTransactionMode",
+ "description": "Sets the Secure Payment Confirmation transaction mode.\nhttps://w3c.github.io/secure-payment-confirmation/#sctn-automation-set-spc-transaction-mode",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "mode",
+ "$ref": "AutoResponseMode"
+ }
+ ]
+ },
+ {
+ "name": "setRPHRegistrationMode",
+ "description": "Extensions for Custom Handlers API:\nhttps://html.spec.whatwg.org/multipage/system-state.html#rph-automation",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "mode",
+ "$ref": "AutoResponseMode"
+ }
+ ]
+ },
+ {
+ "name": "generateTestReport",
+ "description": "Generates a report for testing.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "message",
+ "description": "Message to be displayed in the report.",
+ "type": "string"
+ },
+ {
+ "name": "group",
+ "description": "Specifies the endpoint group to deliver the report to.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "waitForDebugger",
+ "description": "Pauses page execution. Can be resumed using generic Runtime.runIfWaitingForDebugger.",
+ "experimental": true
+ },
+ {
+ "name": "setInterceptFileChooserDialog",
+ "description": "Intercept file chooser requests and transfer control to protocol clients.\nWhen file chooser interception is enabled, native file chooser dialog is not shown.\nInstead, a protocol event `Page.fileChooserOpened` is emitted.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setPrerenderingAllowed",
+ "description": "Enable/disable prerendering manually.\n\nThis command is a short-term solution for https://crbug.com/1440085.\nSee https://docs.google.com/document/d/12HVmFxYj5Jc-eJr5OmWsa2bqTJsbgGLKI6ZIyx0_wpA\nfor more details.\n\nTODO(https://crbug.com/1440085): Remove this once Puppeteer supports tab targets.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "isAllowed",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "domContentEventFired",
+ "parameters": [
+ {
+ "name": "timestamp",
+ "$ref": "Network.MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "fileChooserOpened",
+ "description": "Emitted only when `page.interceptFileChooser` is enabled.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame containing input node.",
+ "experimental": true,
+ "$ref": "FrameId"
+ },
+ {
+ "name": "mode",
+ "description": "Input mode.",
+ "type": "string",
+ "enum": [
+ "selectSingle",
+ "selectMultiple"
+ ]
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Input node id. Only present for file choosers opened via an `<input type=\"file\">` element.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ },
+ {
+ "name": "frameAttached",
+ "description": "Fired when frame has been attached to its parent.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has been attached.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "parentFrameId",
+ "description": "Parent frame identifier.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "stack",
+ "description": "JavaScript stack trace of when frame was attached, only set if frame initiated from script.",
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ }
+ ]
+ },
+ {
+ "name": "frameClearedScheduledNavigation",
+ "description": "Fired when frame no longer has a scheduled navigation.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has cleared its scheduled navigation.",
+ "$ref": "FrameId"
+ }
+ ]
+ },
+ {
+ "name": "frameDetached",
+ "description": "Fired when frame has been detached from its parent.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has been detached.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "reason",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "remove",
+ "swap"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "frameNavigated",
+ "description": "Fired once navigation of the frame has completed. Frame is now associated with the new loader.",
+ "parameters": [
+ {
+ "name": "frame",
+ "description": "Frame object.",
+ "$ref": "Frame"
+ },
+ {
+ "name": "type",
+ "experimental": true,
+ "$ref": "NavigationType"
+ }
+ ]
+ },
+ {
+ "name": "documentOpened",
+ "description": "Fired when opening document to write to.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frame",
+ "description": "Frame object.",
+ "$ref": "Frame"
+ }
+ ]
+ },
+ {
+ "name": "frameResized",
+ "experimental": true
+ },
+ {
+ "name": "frameRequestedNavigation",
+ "description": "Fired when a renderer-initiated navigation is requested.\nNavigation may still be cancelled after the event is issued.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that is being navigated.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "reason",
+ "description": "The reason for the navigation.",
+ "$ref": "ClientNavigationReason"
+ },
+ {
+ "name": "url",
+ "description": "The destination URL for the requested navigation.",
+ "type": "string"
+ },
+ {
+ "name": "disposition",
+ "description": "The disposition for the navigation.",
+ "$ref": "ClientNavigationDisposition"
+ }
+ ]
+ },
+ {
+ "name": "frameScheduledNavigation",
+ "description": "Fired when frame schedules a potential navigation.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has scheduled a navigation.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "delay",
+ "description": "Delay (in seconds) until the navigation is scheduled to begin. The navigation is not\nguaranteed to start.",
+ "type": "number"
+ },
+ {
+ "name": "reason",
+ "description": "The reason for the navigation.",
+ "$ref": "ClientNavigationReason"
+ },
+ {
+ "name": "url",
+ "description": "The destination URL for the scheduled navigation.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "frameStartedLoading",
+ "description": "Fired when frame has started loading.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has started loading.",
+ "$ref": "FrameId"
+ }
+ ]
+ },
+ {
+ "name": "frameStoppedLoading",
+ "description": "Fired when frame has stopped loading.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has stopped loading.",
+ "$ref": "FrameId"
+ }
+ ]
+ },
+ {
+ "name": "downloadWillBegin",
+ "description": "Fired when page is about to start a download.\nDeprecated. Use Browser.downloadWillBegin instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that caused download to begin.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "guid",
+ "description": "Global unique identifier of the download.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource being downloaded.",
+ "type": "string"
+ },
+ {
+ "name": "suggestedFilename",
+ "description": "Suggested file name of the resource (the actual name of the file saved on disk may differ).",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "downloadProgress",
+ "description": "Fired when download makes progress. Last call has |done| == true.\nDeprecated. Use Browser.downloadProgress instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "guid",
+ "description": "Global unique identifier of the download.",
+ "type": "string"
+ },
+ {
+ "name": "totalBytes",
+ "description": "Total expected bytes to download.",
+ "type": "number"
+ },
+ {
+ "name": "receivedBytes",
+ "description": "Total bytes received.",
+ "type": "number"
+ },
+ {
+ "name": "state",
+ "description": "Download status.",
+ "type": "string",
+ "enum": [
+ "inProgress",
+ "completed",
+ "canceled"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "interstitialHidden",
+ "description": "Fired when interstitial page was hidden"
+ },
+ {
+ "name": "interstitialShown",
+ "description": "Fired when interstitial page was shown"
+ },
+ {
+ "name": "javascriptDialogClosed",
+ "description": "Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) has been\nclosed.",
+ "parameters": [
+ {
+ "name": "result",
+ "description": "Whether dialog was confirmed.",
+ "type": "boolean"
+ },
+ {
+ "name": "userInput",
+ "description": "User input in case of prompt.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "javascriptDialogOpening",
+ "description": "Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) is about to\nopen.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "Frame url.",
+ "type": "string"
+ },
+ {
+ "name": "message",
+ "description": "Message that will be displayed by the dialog.",
+ "type": "string"
+ },
+ {
+ "name": "type",
+ "description": "Dialog type.",
+ "$ref": "DialogType"
+ },
+ {
+ "name": "hasBrowserHandler",
+ "description": "True iff browser is capable showing or acting on the given dialog. When browser has no\ndialog handler for given target, calling alert while Page domain is engaged will stall\nthe page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.",
+ "type": "boolean"
+ },
+ {
+ "name": "defaultPrompt",
+ "description": "Default dialog prompt.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "lifecycleEvent",
+ "description": "Fired for top level page lifecycle events such as navigation, load, paint, etc.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Loader identifier. Empty string if the request is fetched from worker.",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "timestamp",
+ "$ref": "Network.MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "backForwardCacheNotUsed",
+ "description": "Fired for failed bfcache history navigations if BackForwardCache feature is enabled. Do\nnot assume any ordering with the Page.frameNavigated event. This event is fired only for\nmain-frame history navigation where the document changes (non-same-document navigations),\nwhen bfcache navigation fails.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "loaderId",
+ "description": "The loader id for the associated navgation.",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "frameId",
+ "description": "The frame id of the associated frame.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "notRestoredExplanations",
+ "description": "Array of reasons why the page could not be cached. This must not be empty.",
+ "type": "array",
+ "items": {
+ "$ref": "BackForwardCacheNotRestoredExplanation"
+ }
+ },
+ {
+ "name": "notRestoredExplanationsTree",
+ "description": "Tree structure of reasons why the page could not be cached for each frame.",
+ "optional": true,
+ "$ref": "BackForwardCacheNotRestoredExplanationTree"
+ }
+ ]
+ },
+ {
+ "name": "loadEventFired",
+ "parameters": [
+ {
+ "name": "timestamp",
+ "$ref": "Network.MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "navigatedWithinDocument",
+ "description": "Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "url",
+ "description": "Frame's new url.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "screencastFrame",
+ "description": "Compressed image data requested by the `startScreencast`.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "data",
+ "description": "Base64-encoded compressed image. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ },
+ {
+ "name": "metadata",
+ "description": "Screencast frame metadata.",
+ "$ref": "ScreencastFrameMetadata"
+ },
+ {
+ "name": "sessionId",
+ "description": "Frame number.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "screencastVisibilityChanged",
+ "description": "Fired when the page with currently enabled screencast was shown or hidden `.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "visible",
+ "description": "True if the page is visible.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "windowOpen",
+ "description": "Fired when a new window is going to be opened, via window.open(), link click, form submission,\netc.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "The URL for the new window.",
+ "type": "string"
+ },
+ {
+ "name": "windowName",
+ "description": "Window name.",
+ "type": "string"
+ },
+ {
+ "name": "windowFeatures",
+ "description": "An array of enabled window features.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "userGesture",
+ "description": "Whether or not it was triggered by user gesture.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "compilationCacheProduced",
+ "description": "Issued for every compilation cache generated. Is only available\nif Page.setGenerateCompilationCache is enabled.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "data",
+ "description": "Base64-encoded data (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Performance",
+ "types": [
+ {
+ "id": "Metric",
+ "description": "Run-time execution metric.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Metric name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Metric value.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disable collecting and reporting metrics."
+ },
+ {
+ "name": "enable",
+ "description": "Enable collecting and reporting metrics.",
+ "parameters": [
+ {
+ "name": "timeDomain",
+ "description": "Time domain to use for collecting and reporting duration metrics.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "timeTicks",
+ "threadTicks"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setTimeDomain",
+ "description": "Sets time domain to use for collecting and reporting duration metrics.\nNote that this must be called before enabling metrics collection. Calling\nthis method while metrics collection is enabled returns an error.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "timeDomain",
+ "description": "Time domain",
+ "type": "string",
+ "enum": [
+ "timeTicks",
+ "threadTicks"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getMetrics",
+ "description": "Retrieve current values of run-time metrics.",
+ "returns": [
+ {
+ "name": "metrics",
+ "description": "Current values for run-time metrics.",
+ "type": "array",
+ "items": {
+ "$ref": "Metric"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "metrics",
+ "description": "Current values of the metrics.",
+ "parameters": [
+ {
+ "name": "metrics",
+ "description": "Current values of the metrics.",
+ "type": "array",
+ "items": {
+ "$ref": "Metric"
+ }
+ },
+ {
+ "name": "title",
+ "description": "Timestamp title.",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "PerformanceTimeline",
+ "description": "Reporting of performance timeline events, as specified in\nhttps://w3c.github.io/performance-timeline/#dom-performanceobserver.",
+ "experimental": true,
+ "dependencies": [
+ "DOM",
+ "Network"
+ ],
+ "types": [
+ {
+ "id": "LargestContentfulPaint",
+ "description": "See https://github.com/WICG/LargestContentfulPaint and largest_contentful_paint.idl",
+ "type": "object",
+ "properties": [
+ {
+ "name": "renderTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "loadTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "size",
+ "description": "The number of pixels being painted.",
+ "type": "number"
+ },
+ {
+ "name": "elementId",
+ "description": "The id attribute of the element, if available.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "The URL of the image (may be trimmed).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "nodeId",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ },
+ {
+ "id": "LayoutShiftAttribution",
+ "type": "object",
+ "properties": [
+ {
+ "name": "previousRect",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "currentRect",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "nodeId",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ },
+ {
+ "id": "LayoutShift",
+ "description": "See https://wicg.github.io/layout-instability/#sec-layout-shift and layout_shift.idl",
+ "type": "object",
+ "properties": [
+ {
+ "name": "value",
+ "description": "Score increment produced by this event.",
+ "type": "number"
+ },
+ {
+ "name": "hadRecentInput",
+ "type": "boolean"
+ },
+ {
+ "name": "lastInputTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "sources",
+ "type": "array",
+ "items": {
+ "$ref": "LayoutShiftAttribution"
+ }
+ }
+ ]
+ },
+ {
+ "id": "TimelineEvent",
+ "type": "object",
+ "properties": [
+ {
+ "name": "frameId",
+ "description": "Identifies the frame that this event is related to. Empty for non-frame targets.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "type",
+ "description": "The event type, as specified in https://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype\nThis determines which of the optional \"details\" fiedls is present.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Name may be empty depending on the type.",
+ "type": "string"
+ },
+ {
+ "name": "time",
+ "description": "Time in seconds since Epoch, monotonically increasing within document lifetime.",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "duration",
+ "description": "Event duration, if applicable.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "lcpDetails",
+ "optional": true,
+ "$ref": "LargestContentfulPaint"
+ },
+ {
+ "name": "layoutShiftDetails",
+ "optional": true,
+ "$ref": "LayoutShift"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Previously buffered events would be reported before method returns.\nSee also: timelineEventAdded",
+ "parameters": [
+ {
+ "name": "eventTypes",
+ "description": "The types of event to report, as specified in\nhttps://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype\nThe specified filter overrides any previous filters, passing empty\nfilter disables recording.\nNote that not all types exposed to the web platform are currently supported.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "timelineEventAdded",
+ "description": "Sent when a performance timeline event is added. See reportPerformanceTimeline method.",
+ "parameters": [
+ {
+ "name": "event",
+ "$ref": "TimelineEvent"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Security",
+ "description": "Security",
+ "types": [
+ {
+ "id": "CertificateId",
+ "description": "An internal certificate ID value.",
+ "type": "integer"
+ },
+ {
+ "id": "MixedContentType",
+ "description": "A description of mixed content (HTTP resources on HTTPS pages), as defined by\nhttps://www.w3.org/TR/mixed-content/#categories",
+ "type": "string",
+ "enum": [
+ "blockable",
+ "optionally-blockable",
+ "none"
+ ]
+ },
+ {
+ "id": "SecurityState",
+ "description": "The security level of a page or resource.",
+ "type": "string",
+ "enum": [
+ "unknown",
+ "neutral",
+ "insecure",
+ "secure",
+ "info",
+ "insecure-broken"
+ ]
+ },
+ {
+ "id": "CertificateSecurityState",
+ "description": "Details about the security state of the page certificate.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "protocol",
+ "description": "Protocol name (e.g. \"TLS 1.2\" or \"QUIC\").",
+ "type": "string"
+ },
+ {
+ "name": "keyExchange",
+ "description": "Key Exchange used by the connection, or the empty string if not applicable.",
+ "type": "string"
+ },
+ {
+ "name": "keyExchangeGroup",
+ "description": "(EC)DH group used by the connection, if applicable.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cipher",
+ "description": "Cipher name.",
+ "type": "string"
+ },
+ {
+ "name": "mac",
+ "description": "TLS MAC. Note that AEAD ciphers do not have separate MACs.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "certificate",
+ "description": "Page certificate.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "subjectName",
+ "description": "Certificate subject name.",
+ "type": "string"
+ },
+ {
+ "name": "issuer",
+ "description": "Name of the issuing CA.",
+ "type": "string"
+ },
+ {
+ "name": "validFrom",
+ "description": "Certificate valid from date.",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "validTo",
+ "description": "Certificate valid to (expiration) date",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "certificateNetworkError",
+ "description": "The highest priority network error code, if the certificate has an error.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "certificateHasWeakSignature",
+ "description": "True if the certificate uses a weak signature aglorithm.",
+ "type": "boolean"
+ },
+ {
+ "name": "certificateHasSha1Signature",
+ "description": "True if the certificate has a SHA1 signature in the chain.",
+ "type": "boolean"
+ },
+ {
+ "name": "modernSSL",
+ "description": "True if modern SSL",
+ "type": "boolean"
+ },
+ {
+ "name": "obsoleteSslProtocol",
+ "description": "True if the connection is using an obsolete SSL protocol.",
+ "type": "boolean"
+ },
+ {
+ "name": "obsoleteSslKeyExchange",
+ "description": "True if the connection is using an obsolete SSL key exchange.",
+ "type": "boolean"
+ },
+ {
+ "name": "obsoleteSslCipher",
+ "description": "True if the connection is using an obsolete SSL cipher.",
+ "type": "boolean"
+ },
+ {
+ "name": "obsoleteSslSignature",
+ "description": "True if the connection is using an obsolete SSL signature.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "SafetyTipStatus",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "badReputation",
+ "lookalike"
+ ]
+ },
+ {
+ "id": "SafetyTipInfo",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "safetyTipStatus",
+ "description": "Describes whether the page triggers any safety tips or reputation warnings. Default is unknown.",
+ "$ref": "SafetyTipStatus"
+ },
+ {
+ "name": "safeUrl",
+ "description": "The URL the safety tip suggested (\"Did you mean?\"). Only filled in for lookalike matches.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "VisibleSecurityState",
+ "description": "Security state information about the page.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "securityState",
+ "description": "The security level of the page.",
+ "$ref": "SecurityState"
+ },
+ {
+ "name": "certificateSecurityState",
+ "description": "Security state details about the page certificate.",
+ "optional": true,
+ "$ref": "CertificateSecurityState"
+ },
+ {
+ "name": "safetyTipInfo",
+ "description": "The type of Safety Tip triggered on the page. Note that this field will be set even if the Safety Tip UI was not actually shown.",
+ "optional": true,
+ "$ref": "SafetyTipInfo"
+ },
+ {
+ "name": "securityStateIssueIds",
+ "description": "Array of security state issues ids.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SecurityStateExplanation",
+ "description": "An explanation of an factor contributing to the security state.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "securityState",
+ "description": "Security state representing the severity of the factor being explained.",
+ "$ref": "SecurityState"
+ },
+ {
+ "name": "title",
+ "description": "Title describing the type of factor.",
+ "type": "string"
+ },
+ {
+ "name": "summary",
+ "description": "Short phrase describing the type of factor.",
+ "type": "string"
+ },
+ {
+ "name": "description",
+ "description": "Full text explanation of the factor.",
+ "type": "string"
+ },
+ {
+ "name": "mixedContentType",
+ "description": "The type of mixed content described by the explanation.",
+ "$ref": "MixedContentType"
+ },
+ {
+ "name": "certificate",
+ "description": "Page certificate.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "recommendations",
+ "description": "Recommendations to fix any issues.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "InsecureContentStatus",
+ "description": "Information about insecure content on the page.",
+ "deprecated": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "ranMixedContent",
+ "description": "Always false.",
+ "type": "boolean"
+ },
+ {
+ "name": "displayedMixedContent",
+ "description": "Always false.",
+ "type": "boolean"
+ },
+ {
+ "name": "containedMixedForm",
+ "description": "Always false.",
+ "type": "boolean"
+ },
+ {
+ "name": "ranContentWithCertErrors",
+ "description": "Always false.",
+ "type": "boolean"
+ },
+ {
+ "name": "displayedContentWithCertErrors",
+ "description": "Always false.",
+ "type": "boolean"
+ },
+ {
+ "name": "ranInsecureContentStyle",
+ "description": "Always set to unknown.",
+ "$ref": "SecurityState"
+ },
+ {
+ "name": "displayedInsecureContentStyle",
+ "description": "Always set to unknown.",
+ "$ref": "SecurityState"
+ }
+ ]
+ },
+ {
+ "id": "CertificateErrorAction",
+ "description": "The action to take when a certificate error occurs. continue will continue processing the\nrequest and cancel will cancel the request.",
+ "type": "string",
+ "enum": [
+ "continue",
+ "cancel"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables tracking security state changes."
+ },
+ {
+ "name": "enable",
+ "description": "Enables tracking security state changes."
+ },
+ {
+ "name": "setIgnoreCertificateErrors",
+ "description": "Enable/disable whether all certificate errors should be ignored.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ignore",
+ "description": "If true, all certificate errors will be ignored.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "handleCertificateError",
+ "description": "Handles a certificate error that fired a certificateError event.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "eventId",
+ "description": "The ID of the event.",
+ "type": "integer"
+ },
+ {
+ "name": "action",
+ "description": "The action to take on the certificate error.",
+ "$ref": "CertificateErrorAction"
+ }
+ ]
+ },
+ {
+ "name": "setOverrideCertificateErrors",
+ "description": "Enable/disable overriding certificate errors. If enabled, all certificate error events need to\nbe handled by the DevTools client and should be answered with `handleCertificateError` commands.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "override",
+ "description": "If true, certificate errors will be overridden.",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "certificateError",
+ "description": "There is a certificate error. If overriding certificate errors is enabled, then it should be\nhandled with the `handleCertificateError` command. Note: this event does not fire if the\ncertificate error has been allowed internally. Only one client per target should override\ncertificate errors at the same time.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "eventId",
+ "description": "The ID of the event.",
+ "type": "integer"
+ },
+ {
+ "name": "errorType",
+ "description": "The type of the error.",
+ "type": "string"
+ },
+ {
+ "name": "requestURL",
+ "description": "The url that was requested.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "visibleSecurityStateChanged",
+ "description": "The security state of the page changed.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "visibleSecurityState",
+ "description": "Security state information about the page.",
+ "$ref": "VisibleSecurityState"
+ }
+ ]
+ },
+ {
+ "name": "securityStateChanged",
+ "description": "The security state of the page changed. No longer being sent.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "securityState",
+ "description": "Security state.",
+ "$ref": "SecurityState"
+ },
+ {
+ "name": "schemeIsCryptographic",
+ "description": "True if the page was loaded over cryptographic transport such as HTTPS.",
+ "deprecated": true,
+ "type": "boolean"
+ },
+ {
+ "name": "explanations",
+ "description": "Previously a list of explanations for the security state. Now always\nempty.",
+ "deprecated": true,
+ "type": "array",
+ "items": {
+ "$ref": "SecurityStateExplanation"
+ }
+ },
+ {
+ "name": "insecureContentStatus",
+ "description": "Information about insecure content on the page.",
+ "deprecated": true,
+ "$ref": "InsecureContentStatus"
+ },
+ {
+ "name": "summary",
+ "description": "Overrides user-visible description of the state. Always omitted.",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "ServiceWorker",
+ "experimental": true,
+ "dependencies": [
+ "Target"
+ ],
+ "types": [
+ {
+ "id": "RegistrationID",
+ "type": "string"
+ },
+ {
+ "id": "ServiceWorkerRegistration",
+ "description": "ServiceWorker registration.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "scopeURL",
+ "type": "string"
+ },
+ {
+ "name": "isDeleted",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "ServiceWorkerVersionRunningStatus",
+ "type": "string",
+ "enum": [
+ "stopped",
+ "starting",
+ "running",
+ "stopping"
+ ]
+ },
+ {
+ "id": "ServiceWorkerVersionStatus",
+ "type": "string",
+ "enum": [
+ "new",
+ "installing",
+ "installed",
+ "activating",
+ "activated",
+ "redundant"
+ ]
+ },
+ {
+ "id": "ServiceWorkerVersion",
+ "description": "ServiceWorker version.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "versionId",
+ "type": "string"
+ },
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "scriptURL",
+ "type": "string"
+ },
+ {
+ "name": "runningStatus",
+ "$ref": "ServiceWorkerVersionRunningStatus"
+ },
+ {
+ "name": "status",
+ "$ref": "ServiceWorkerVersionStatus"
+ },
+ {
+ "name": "scriptLastModified",
+ "description": "The Last-Modified header value of the main script.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "scriptResponseTime",
+ "description": "The time at which the response headers of the main script were received from the server.\nFor cached script it is the last time the cache entry was validated.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "controlledClients",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Target.TargetID"
+ }
+ },
+ {
+ "name": "targetId",
+ "optional": true,
+ "$ref": "Target.TargetID"
+ }
+ ]
+ },
+ {
+ "id": "ServiceWorkerErrorMessage",
+ "description": "ServiceWorker error message.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "errorMessage",
+ "type": "string"
+ },
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "versionId",
+ "type": "string"
+ },
+ {
+ "name": "sourceURL",
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "deliverPushMessage",
+ "parameters": [
+ {
+ "name": "origin",
+ "type": "string"
+ },
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "data",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disable"
+ },
+ {
+ "name": "dispatchSyncEvent",
+ "parameters": [
+ {
+ "name": "origin",
+ "type": "string"
+ },
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "tag",
+ "type": "string"
+ },
+ {
+ "name": "lastChance",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "dispatchPeriodicSyncEvent",
+ "parameters": [
+ {
+ "name": "origin",
+ "type": "string"
+ },
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "tag",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "enable"
+ },
+ {
+ "name": "inspectWorker",
+ "parameters": [
+ {
+ "name": "versionId",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setForceUpdateOnPageLoad",
+ "parameters": [
+ {
+ "name": "forceUpdateOnPageLoad",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "skipWaiting",
+ "parameters": [
+ {
+ "name": "scopeURL",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "startWorker",
+ "parameters": [
+ {
+ "name": "scopeURL",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "stopAllWorkers"
+ },
+ {
+ "name": "stopWorker",
+ "parameters": [
+ {
+ "name": "versionId",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "unregister",
+ "parameters": [
+ {
+ "name": "scopeURL",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "updateRegistration",
+ "parameters": [
+ {
+ "name": "scopeURL",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "workerErrorReported",
+ "parameters": [
+ {
+ "name": "errorMessage",
+ "$ref": "ServiceWorkerErrorMessage"
+ }
+ ]
+ },
+ {
+ "name": "workerRegistrationUpdated",
+ "parameters": [
+ {
+ "name": "registrations",
+ "type": "array",
+ "items": {
+ "$ref": "ServiceWorkerRegistration"
+ }
+ }
+ ]
+ },
+ {
+ "name": "workerVersionUpdated",
+ "parameters": [
+ {
+ "name": "versions",
+ "type": "array",
+ "items": {
+ "$ref": "ServiceWorkerVersion"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Storage",
+ "experimental": true,
+ "dependencies": [
+ "Browser",
+ "Network"
+ ],
+ "types": [
+ {
+ "id": "SerializedStorageKey",
+ "type": "string"
+ },
+ {
+ "id": "StorageType",
+ "description": "Enum of possible storage types.",
+ "type": "string",
+ "enum": [
+ "appcache",
+ "cookies",
+ "file_systems",
+ "indexeddb",
+ "local_storage",
+ "shader_cache",
+ "websql",
+ "service_workers",
+ "cache_storage",
+ "interest_groups",
+ "shared_storage",
+ "storage_buckets",
+ "all",
+ "other"
+ ]
+ },
+ {
+ "id": "UsageForType",
+ "description": "Usage for a storage type.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "storageType",
+ "description": "Name of storage type.",
+ "$ref": "StorageType"
+ },
+ {
+ "name": "usage",
+ "description": "Storage usage (bytes).",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "TrustTokens",
+ "description": "Pair of issuer origin and number of available (signed, but not used) Trust\nTokens from that issuer.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "issuerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "count",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "InterestGroupAccessType",
+ "description": "Enum of interest group access types.",
+ "type": "string",
+ "enum": [
+ "join",
+ "leave",
+ "update",
+ "loaded",
+ "bid",
+ "win"
+ ]
+ },
+ {
+ "id": "InterestGroupAd",
+ "description": "Ad advertising element inside an interest group.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "renderUrl",
+ "type": "string"
+ },
+ {
+ "name": "metadata",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "InterestGroupDetails",
+ "description": "The full details of an interest group.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "expirationTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "joiningOrigin",
+ "type": "string"
+ },
+ {
+ "name": "biddingUrl",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "biddingWasmHelperUrl",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "updateUrl",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "trustedBiddingSignalsUrl",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "trustedBiddingSignalsKeys",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "userBiddingSignals",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "ads",
+ "type": "array",
+ "items": {
+ "$ref": "InterestGroupAd"
+ }
+ },
+ {
+ "name": "adComponents",
+ "type": "array",
+ "items": {
+ "$ref": "InterestGroupAd"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SharedStorageAccessType",
+ "description": "Enum of shared storage access types.",
+ "type": "string",
+ "enum": [
+ "documentAddModule",
+ "documentSelectURL",
+ "documentRun",
+ "documentSet",
+ "documentAppend",
+ "documentDelete",
+ "documentClear",
+ "workletSet",
+ "workletAppend",
+ "workletDelete",
+ "workletClear",
+ "workletGet",
+ "workletKeys",
+ "workletEntries",
+ "workletLength",
+ "workletRemainingBudget"
+ ]
+ },
+ {
+ "id": "SharedStorageEntry",
+ "description": "Struct for a single key-value pair in an origin's shared storage.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "SharedStorageMetadata",
+ "description": "Details for an origin's shared storage.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "creationTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "length",
+ "type": "integer"
+ },
+ {
+ "name": "remainingBudget",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "SharedStorageReportingMetadata",
+ "description": "Pair of reporting metadata details for a candidate URL for `selectURL()`.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "eventType",
+ "type": "string"
+ },
+ {
+ "name": "reportingUrl",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "SharedStorageUrlWithMetadata",
+ "description": "Bundles a candidate URL with its reporting metadata.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "Spec of candidate URL.",
+ "type": "string"
+ },
+ {
+ "name": "reportingMetadata",
+ "description": "Any associated reporting metadata.",
+ "type": "array",
+ "items": {
+ "$ref": "SharedStorageReportingMetadata"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SharedStorageAccessParams",
+ "description": "Bundles the parameters for shared storage access events whose\npresence/absence can vary according to SharedStorageAccessType.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptSourceUrl",
+ "description": "Spec of the module script URL.\nPresent only for SharedStorageAccessType.documentAddModule.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "operationName",
+ "description": "Name of the registered operation to be run.\nPresent only for SharedStorageAccessType.documentRun and\nSharedStorageAccessType.documentSelectURL.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "serializedData",
+ "description": "The operation's serialized data in bytes (converted to a string).\nPresent only for SharedStorageAccessType.documentRun and\nSharedStorageAccessType.documentSelectURL.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "urlsWithMetadata",
+ "description": "Array of candidate URLs' specs, along with any associated metadata.\nPresent only for SharedStorageAccessType.documentSelectURL.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "SharedStorageUrlWithMetadata"
+ }
+ },
+ {
+ "name": "key",
+ "description": "Key for a specific entry in an origin's shared storage.\nPresent only for SharedStorageAccessType.documentSet,\nSharedStorageAccessType.documentAppend,\nSharedStorageAccessType.documentDelete,\nSharedStorageAccessType.workletSet,\nSharedStorageAccessType.workletAppend,\nSharedStorageAccessType.workletDelete, and\nSharedStorageAccessType.workletGet.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Value for a specific entry in an origin's shared storage.\nPresent only for SharedStorageAccessType.documentSet,\nSharedStorageAccessType.documentAppend,\nSharedStorageAccessType.workletSet, and\nSharedStorageAccessType.workletAppend.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "ignoreIfPresent",
+ "description": "Whether or not to set an entry for a key if that key is already present.\nPresent only for SharedStorageAccessType.documentSet and\nSharedStorageAccessType.workletSet.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "StorageBucketsDurability",
+ "type": "string",
+ "enum": [
+ "relaxed",
+ "strict"
+ ]
+ },
+ {
+ "id": "StorageBucket",
+ "type": "object",
+ "properties": [
+ {
+ "name": "storageKey",
+ "$ref": "SerializedStorageKey"
+ },
+ {
+ "name": "name",
+ "description": "If not specified, it is the default bucket of the storageKey.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "StorageBucketInfo",
+ "type": "object",
+ "properties": [
+ {
+ "name": "bucket",
+ "$ref": "StorageBucket"
+ },
+ {
+ "name": "id",
+ "type": "string"
+ },
+ {
+ "name": "expiration",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "quota",
+ "description": "Storage quota (bytes).",
+ "type": "number"
+ },
+ {
+ "name": "persistent",
+ "type": "boolean"
+ },
+ {
+ "name": "durability",
+ "$ref": "StorageBucketsDurability"
+ }
+ ]
+ },
+ {
+ "id": "AttributionReportingSourceType",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "navigation",
+ "event"
+ ]
+ },
+ {
+ "id": "UnsignedInt64AsBase10",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "UnsignedInt128AsBase16",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "SignedInt64AsBase10",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "AttributionReportingFilterDataEntry",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "values",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "AttributionReportingAggregationKeysEntry",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "$ref": "UnsignedInt128AsBase16"
+ }
+ ]
+ },
+ {
+ "id": "AttributionReportingSourceRegistration",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "time",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "expiry",
+ "description": "duration in seconds",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "eventReportWindow",
+ "description": "duration in seconds",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "aggregatableReportWindow",
+ "description": "duration in seconds",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "type",
+ "$ref": "AttributionReportingSourceType"
+ },
+ {
+ "name": "sourceOrigin",
+ "type": "string"
+ },
+ {
+ "name": "reportingOrigin",
+ "type": "string"
+ },
+ {
+ "name": "destinationSites",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "eventId",
+ "$ref": "UnsignedInt64AsBase10"
+ },
+ {
+ "name": "priority",
+ "$ref": "SignedInt64AsBase10"
+ },
+ {
+ "name": "filterData",
+ "type": "array",
+ "items": {
+ "$ref": "AttributionReportingFilterDataEntry"
+ }
+ },
+ {
+ "name": "aggregationKeys",
+ "type": "array",
+ "items": {
+ "$ref": "AttributionReportingAggregationKeysEntry"
+ }
+ },
+ {
+ "name": "debugKey",
+ "optional": true,
+ "$ref": "UnsignedInt64AsBase10"
+ }
+ ]
+ },
+ {
+ "id": "AttributionReportingSourceRegistrationResult",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "success",
+ "internalError",
+ "insufficientSourceCapacity",
+ "insufficientUniqueDestinationCapacity",
+ "excessiveReportingOrigins",
+ "prohibitedByBrowserPolicy",
+ "successNoised",
+ "destinationReportingLimitReached",
+ "destinationGlobalLimitReached",
+ "destinationBothLimitsReached",
+ "reportingOriginsPerSiteLimitReached"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getStorageKeyForFrame",
+ "description": "Returns a storage key given a frame id.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "storageKey",
+ "$ref": "SerializedStorageKey"
+ }
+ ]
+ },
+ {
+ "name": "clearDataForOrigin",
+ "description": "Clears storage for origin.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ },
+ {
+ "name": "storageTypes",
+ "description": "Comma separated list of StorageType to clear.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "clearDataForStorageKey",
+ "description": "Clears storage for storage key.",
+ "parameters": [
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "type": "string"
+ },
+ {
+ "name": "storageTypes",
+ "description": "Comma separated list of StorageType to clear.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getCookies",
+ "description": "Returns all browser cookies.",
+ "parameters": [
+ {
+ "name": "browserContextId",
+ "description": "Browser context to use when called on the browser endpoint.",
+ "optional": true,
+ "$ref": "Browser.BrowserContextID"
+ }
+ ],
+ "returns": [
+ {
+ "name": "cookies",
+ "description": "Array of cookie objects.",
+ "type": "array",
+ "items": {
+ "$ref": "Network.Cookie"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setCookies",
+ "description": "Sets given cookies.",
+ "parameters": [
+ {
+ "name": "cookies",
+ "description": "Cookies to be set.",
+ "type": "array",
+ "items": {
+ "$ref": "Network.CookieParam"
+ }
+ },
+ {
+ "name": "browserContextId",
+ "description": "Browser context to use when called on the browser endpoint.",
+ "optional": true,
+ "$ref": "Browser.BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "clearCookies",
+ "description": "Clears cookies.",
+ "parameters": [
+ {
+ "name": "browserContextId",
+ "description": "Browser context to use when called on the browser endpoint.",
+ "optional": true,
+ "$ref": "Browser.BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "getUsageAndQuota",
+ "description": "Returns usage and quota in bytes.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "usage",
+ "description": "Storage usage (bytes).",
+ "type": "number"
+ },
+ {
+ "name": "quota",
+ "description": "Storage quota (bytes).",
+ "type": "number"
+ },
+ {
+ "name": "overrideActive",
+ "description": "Whether or not the origin has an active storage quota override",
+ "type": "boolean"
+ },
+ {
+ "name": "usageBreakdown",
+ "description": "Storage usage per type (bytes).",
+ "type": "array",
+ "items": {
+ "$ref": "UsageForType"
+ }
+ }
+ ]
+ },
+ {
+ "name": "overrideQuotaForOrigin",
+ "description": "Override quota for the specified origin",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ },
+ {
+ "name": "quotaSize",
+ "description": "The quota size (in bytes) to override the original quota with.\nIf this is called multiple times, the overridden quota will be equal to\nthe quotaSize provided in the final call. If this is called without\nspecifying a quotaSize, the quota will be reset to the default value for\nthe specified origin. If this is called multiple times with different\norigins, the override will be maintained for each origin until it is\ndisabled (called without a quotaSize).",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "trackCacheStorageForOrigin",
+ "description": "Registers origin to be notified when an update occurs to its cache storage list.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "trackCacheStorageForStorageKey",
+ "description": "Registers storage key to be notified when an update occurs to its cache storage list.",
+ "parameters": [
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "trackIndexedDBForOrigin",
+ "description": "Registers origin to be notified when an update occurs to its IndexedDB.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "trackIndexedDBForStorageKey",
+ "description": "Registers storage key to be notified when an update occurs to its IndexedDB.",
+ "parameters": [
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "untrackCacheStorageForOrigin",
+ "description": "Unregisters origin from receiving notifications for cache storage.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "untrackCacheStorageForStorageKey",
+ "description": "Unregisters storage key from receiving notifications for cache storage.",
+ "parameters": [
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "untrackIndexedDBForOrigin",
+ "description": "Unregisters origin from receiving notifications for IndexedDB.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "untrackIndexedDBForStorageKey",
+ "description": "Unregisters storage key from receiving notifications for IndexedDB.",
+ "parameters": [
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getTrustTokens",
+ "description": "Returns the number of stored Trust Tokens per issuer for the\ncurrent browsing context.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "tokens",
+ "type": "array",
+ "items": {
+ "$ref": "TrustTokens"
+ }
+ }
+ ]
+ },
+ {
+ "name": "clearTrustTokens",
+ "description": "Removes all Trust Tokens issued by the provided issuerOrigin.\nLeaves other stored data, including the issuer's Redemption Records, intact.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "issuerOrigin",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "didDeleteTokens",
+ "description": "True if any tokens were deleted, false otherwise.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getInterestGroupDetails",
+ "description": "Gets details for a named interest group.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "details",
+ "$ref": "InterestGroupDetails"
+ }
+ ]
+ },
+ {
+ "name": "setInterestGroupTracking",
+ "description": "Enables/Disables issuing of interestGroupAccessed events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enable",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getSharedStorageMetadata",
+ "description": "Gets metadata for an origin's shared storage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "metadata",
+ "$ref": "SharedStorageMetadata"
+ }
+ ]
+ },
+ {
+ "name": "getSharedStorageEntries",
+ "description": "Gets the entries in an given origin's shared storage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "entries",
+ "type": "array",
+ "items": {
+ "$ref": "SharedStorageEntry"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setSharedStorageEntry",
+ "description": "Sets entry with `key` and `value` for a given origin's shared storage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ },
+ {
+ "name": "ignoreIfPresent",
+ "description": "If `ignoreIfPresent` is included and true, then only sets the entry if\n`key` doesn't already exist.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "deleteSharedStorageEntry",
+ "description": "Deletes entry for `key` (if it exists) for a given origin's shared storage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "clearSharedStorageEntries",
+ "description": "Clears all entries for a given origin's shared storage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "resetSharedStorageBudget",
+ "description": "Resets the budget for `ownerOrigin` by clearing all budget withdrawals.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setSharedStorageTracking",
+ "description": "Enables/disables issuing of sharedStorageAccessed events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enable",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setStorageBucketTracking",
+ "description": "Set tracking for a storage key's buckets.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "storageKey",
+ "type": "string"
+ },
+ {
+ "name": "enable",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "deleteStorageBucket",
+ "description": "Deletes the Storage Bucket with the given storage key and bucket name.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "bucket",
+ "$ref": "StorageBucket"
+ }
+ ]
+ },
+ {
+ "name": "runBounceTrackingMitigations",
+ "description": "Deletes state for sites identified as potential bounce trackers, immediately.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "deletedSites",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setAttributionReportingLocalTestingMode",
+ "description": "https://wicg.github.io/attribution-reporting-api/",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "If enabled, noise is suppressed and reports are sent immediately.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setAttributionReportingTracking",
+ "description": "Enables/disables issuing of Attribution Reporting events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enable",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "cacheStorageContentUpdated",
+ "description": "A cache's contents have been modified.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin to update.",
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key to update.",
+ "type": "string"
+ },
+ {
+ "name": "bucketId",
+ "description": "Storage bucket to update.",
+ "type": "string"
+ },
+ {
+ "name": "cacheName",
+ "description": "Name of cache in origin.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "cacheStorageListUpdated",
+ "description": "A cache has been added/deleted.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin to update.",
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key to update.",
+ "type": "string"
+ },
+ {
+ "name": "bucketId",
+ "description": "Storage bucket to update.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "indexedDBContentUpdated",
+ "description": "The origin's IndexedDB object store has been modified.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin to update.",
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key to update.",
+ "type": "string"
+ },
+ {
+ "name": "bucketId",
+ "description": "Storage bucket to update.",
+ "type": "string"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database to update.",
+ "type": "string"
+ },
+ {
+ "name": "objectStoreName",
+ "description": "ObjectStore to update.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "indexedDBListUpdated",
+ "description": "The origin's IndexedDB database list has been modified.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin to update.",
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key to update.",
+ "type": "string"
+ },
+ {
+ "name": "bucketId",
+ "description": "Storage bucket to update.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "interestGroupAccessed",
+ "description": "One of the interest groups was accessed by the associated page.",
+ "parameters": [
+ {
+ "name": "accessTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "type",
+ "$ref": "InterestGroupAccessType"
+ },
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "sharedStorageAccessed",
+ "description": "Shared storage was accessed by the associated page.\nThe following parameters are included in all events.",
+ "parameters": [
+ {
+ "name": "accessTime",
+ "description": "Time of the access.",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "type",
+ "description": "Enum value indicating the Shared Storage API method invoked.",
+ "$ref": "SharedStorageAccessType"
+ },
+ {
+ "name": "mainFrameId",
+ "description": "DevTools Frame Token for the primary frame tree's root.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "ownerOrigin",
+ "description": "Serialized origin for the context that invoked the Shared Storage API.",
+ "type": "string"
+ },
+ {
+ "name": "params",
+ "description": "The sub-parameters warapped by `params` are all optional and their\npresence/absence depends on `type`.",
+ "$ref": "SharedStorageAccessParams"
+ }
+ ]
+ },
+ {
+ "name": "storageBucketCreatedOrUpdated",
+ "parameters": [
+ {
+ "name": "bucketInfo",
+ "$ref": "StorageBucketInfo"
+ }
+ ]
+ },
+ {
+ "name": "storageBucketDeleted",
+ "parameters": [
+ {
+ "name": "bucketId",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "attributionReportingSourceRegistered",
+ "description": "TODO(crbug.com/1458532): Add other Attribution Reporting events, e.g.\ntrigger registration.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "registration",
+ "$ref": "AttributionReportingSourceRegistration"
+ },
+ {
+ "name": "result",
+ "$ref": "AttributionReportingSourceRegistrationResult"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "SystemInfo",
+ "description": "The SystemInfo domain defines methods and events for querying low-level system information.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "GPUDevice",
+ "description": "Describes a single graphics processor (GPU).",
+ "type": "object",
+ "properties": [
+ {
+ "name": "vendorId",
+ "description": "PCI ID of the GPU vendor, if available; 0 otherwise.",
+ "type": "number"
+ },
+ {
+ "name": "deviceId",
+ "description": "PCI ID of the GPU device, if available; 0 otherwise.",
+ "type": "number"
+ },
+ {
+ "name": "subSysId",
+ "description": "Sub sys ID of the GPU, only available on Windows.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "revision",
+ "description": "Revision of the GPU, only available on Windows.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "vendorString",
+ "description": "String description of the GPU vendor, if the PCI ID is not available.",
+ "type": "string"
+ },
+ {
+ "name": "deviceString",
+ "description": "String description of the GPU device, if the PCI ID is not available.",
+ "type": "string"
+ },
+ {
+ "name": "driverVendor",
+ "description": "String description of the GPU driver vendor.",
+ "type": "string"
+ },
+ {
+ "name": "driverVersion",
+ "description": "String description of the GPU driver version.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Size",
+ "description": "Describes the width and height dimensions of an entity.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "width",
+ "description": "Width in pixels.",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Height in pixels.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "VideoDecodeAcceleratorCapability",
+ "description": "Describes a supported video decoding profile with its associated minimum and\nmaximum resolutions.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "profile",
+ "description": "Video codec profile that is supported, e.g. VP9 Profile 2.",
+ "type": "string"
+ },
+ {
+ "name": "maxResolution",
+ "description": "Maximum video dimensions in pixels supported for this |profile|.",
+ "$ref": "Size"
+ },
+ {
+ "name": "minResolution",
+ "description": "Minimum video dimensions in pixels supported for this |profile|.",
+ "$ref": "Size"
+ }
+ ]
+ },
+ {
+ "id": "VideoEncodeAcceleratorCapability",
+ "description": "Describes a supported video encoding profile with its associated maximum\nresolution and maximum framerate.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "profile",
+ "description": "Video codec profile that is supported, e.g H264 Main.",
+ "type": "string"
+ },
+ {
+ "name": "maxResolution",
+ "description": "Maximum video dimensions in pixels supported for this |profile|.",
+ "$ref": "Size"
+ },
+ {
+ "name": "maxFramerateNumerator",
+ "description": "Maximum encoding framerate in frames per second supported for this\n|profile|, as fraction's numerator and denominator, e.g. 24/1 fps,\n24000/1001 fps, etc.",
+ "type": "integer"
+ },
+ {
+ "name": "maxFramerateDenominator",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "SubsamplingFormat",
+ "description": "YUV subsampling type of the pixels of a given image.",
+ "type": "string",
+ "enum": [
+ "yuv420",
+ "yuv422",
+ "yuv444"
+ ]
+ },
+ {
+ "id": "ImageType",
+ "description": "Image format of a given image.",
+ "type": "string",
+ "enum": [
+ "jpeg",
+ "webp",
+ "unknown"
+ ]
+ },
+ {
+ "id": "ImageDecodeAcceleratorCapability",
+ "description": "Describes a supported image decoding profile with its associated minimum and\nmaximum resolutions and subsampling.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "imageType",
+ "description": "Image coded, e.g. Jpeg.",
+ "$ref": "ImageType"
+ },
+ {
+ "name": "maxDimensions",
+ "description": "Maximum supported dimensions of the image in pixels.",
+ "$ref": "Size"
+ },
+ {
+ "name": "minDimensions",
+ "description": "Minimum supported dimensions of the image in pixels.",
+ "$ref": "Size"
+ },
+ {
+ "name": "subsamplings",
+ "description": "Optional array of supported subsampling formats, e.g. 4:2:0, if known.",
+ "type": "array",
+ "items": {
+ "$ref": "SubsamplingFormat"
+ }
+ }
+ ]
+ },
+ {
+ "id": "GPUInfo",
+ "description": "Provides information about the GPU(s) on the system.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "devices",
+ "description": "The graphics devices on the system. Element 0 is the primary GPU.",
+ "type": "array",
+ "items": {
+ "$ref": "GPUDevice"
+ }
+ },
+ {
+ "name": "auxAttributes",
+ "description": "An optional dictionary of additional GPU related attributes.",
+ "optional": true,
+ "type": "object"
+ },
+ {
+ "name": "featureStatus",
+ "description": "An optional dictionary of graphics features and their status.",
+ "optional": true,
+ "type": "object"
+ },
+ {
+ "name": "driverBugWorkarounds",
+ "description": "An optional array of GPU driver bug workarounds.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "videoDecoding",
+ "description": "Supported accelerated video decoding capabilities.",
+ "type": "array",
+ "items": {
+ "$ref": "VideoDecodeAcceleratorCapability"
+ }
+ },
+ {
+ "name": "videoEncoding",
+ "description": "Supported accelerated video encoding capabilities.",
+ "type": "array",
+ "items": {
+ "$ref": "VideoEncodeAcceleratorCapability"
+ }
+ },
+ {
+ "name": "imageDecoding",
+ "description": "Supported accelerated image decoding capabilities.",
+ "type": "array",
+ "items": {
+ "$ref": "ImageDecodeAcceleratorCapability"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ProcessInfo",
+ "description": "Represents process info.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Specifies process type.",
+ "type": "string"
+ },
+ {
+ "name": "id",
+ "description": "Specifies process id.",
+ "type": "integer"
+ },
+ {
+ "name": "cpuTime",
+ "description": "Specifies cumulative CPU usage in seconds across all threads of the\nprocess since the process start.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getInfo",
+ "description": "Returns information about the system.",
+ "returns": [
+ {
+ "name": "gpu",
+ "description": "Information about the GPUs on the system.",
+ "$ref": "GPUInfo"
+ },
+ {
+ "name": "modelName",
+ "description": "A platform-dependent description of the model of the machine. On Mac OS, this is, for\nexample, 'MacBookPro'. Will be the empty string if not supported.",
+ "type": "string"
+ },
+ {
+ "name": "modelVersion",
+ "description": "A platform-dependent description of the version of the machine. On Mac OS, this is, for\nexample, '10.1'. Will be the empty string if not supported.",
+ "type": "string"
+ },
+ {
+ "name": "commandLine",
+ "description": "The command line string used to launch the browser. Will be the empty string if not\nsupported.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getFeatureState",
+ "description": "Returns information about the feature state.",
+ "parameters": [
+ {
+ "name": "featureState",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "featureEnabled",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getProcessInfo",
+ "description": "Returns information about all running processes.",
+ "returns": [
+ {
+ "name": "processInfo",
+ "description": "An array of process info blocks.",
+ "type": "array",
+ "items": {
+ "$ref": "ProcessInfo"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Target",
+ "description": "Supports additional targets discovery and allows to attach to them.",
+ "types": [
+ {
+ "id": "TargetID",
+ "type": "string"
+ },
+ {
+ "id": "SessionID",
+ "description": "Unique identifier of attached debugging session.",
+ "type": "string"
+ },
+ {
+ "id": "TargetInfo",
+ "type": "object",
+ "properties": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ },
+ {
+ "name": "type",
+ "type": "string"
+ },
+ {
+ "name": "title",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "attached",
+ "description": "Whether the target has an attached client.",
+ "type": "boolean"
+ },
+ {
+ "name": "openerId",
+ "description": "Opener target Id",
+ "optional": true,
+ "$ref": "TargetID"
+ },
+ {
+ "name": "canAccessOpener",
+ "description": "Whether the target has access to the originating window.",
+ "experimental": true,
+ "type": "boolean"
+ },
+ {
+ "name": "openerFrameId",
+ "description": "Frame id of originating window (is only set if target has an opener).",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "browserContextId",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Browser.BrowserContextID"
+ },
+ {
+ "name": "subtype",
+ "description": "Provides additional details for specific target types. For example, for\nthe type of \"page\", this may be set to \"portal\" or \"prerender\".",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "FilterEntry",
+ "description": "A filter used by target query/discovery/auto-attach operations.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "exclude",
+ "description": "If set, causes exclusion of mathcing targets from the list.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "type",
+ "description": "If not present, matches any type.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "TargetFilter",
+ "description": "The entries in TargetFilter are matched sequentially against targets and\nthe first entry that matches determines if the target is included or not,\ndepending on the value of `exclude` field in the entry.\nIf filter is not specified, the one assumed is\n[{type: \"browser\", exclude: true}, {type: \"tab\", exclude: true}, {}]\n(i.e. include everything but `browser` and `tab`).",
+ "experimental": true,
+ "type": "array",
+ "items": {
+ "$ref": "FilterEntry"
+ }
+ },
+ {
+ "id": "RemoteLocation",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "host",
+ "type": "string"
+ },
+ {
+ "name": "port",
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "activateTarget",
+ "description": "Activates (focuses) the target.",
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "attachToTarget",
+ "description": "Attaches to the target with given id.",
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ },
+ {
+ "name": "flatten",
+ "description": "Enables \"flat\" access to the session via specifying sessionId attribute in the commands.\nWe plan to make this the default, deprecate non-flattened mode,\nand eventually retire it. See crbug.com/991325.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "sessionId",
+ "description": "Id assigned to the session.",
+ "$ref": "SessionID"
+ }
+ ]
+ },
+ {
+ "name": "attachToBrowserTarget",
+ "description": "Attaches to the browser target, only uses flat sessionId mode.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "sessionId",
+ "description": "Id assigned to the session.",
+ "$ref": "SessionID"
+ }
+ ]
+ },
+ {
+ "name": "closeTarget",
+ "description": "Closes the target. If the target is a page that gets closed too.",
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ }
+ ],
+ "returns": [
+ {
+ "name": "success",
+ "description": "Always set to true. If an error occurs, the response indicates protocol error.",
+ "deprecated": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "exposeDevToolsProtocol",
+ "description": "Inject object to the target's main frame that provides a communication\nchannel with browser target.\n\nInjected object will be available as `window[bindingName]`.\n\nThe object has the follwing API:\n- `binding.send(json)` - a method to send messages over the remote debugging protocol\n- `binding.onmessage = json => handleMessage(json)` - a callback that will be called for the protocol notifications and command responses.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ },
+ {
+ "name": "bindingName",
+ "description": "Binding name, 'cdp' if not specified.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "createBrowserContext",
+ "description": "Creates a new empty BrowserContext. Similar to an incognito profile but you can have more than\none.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "disposeOnDetach",
+ "description": "If specified, disposes this context when debugging session disconnects.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "proxyServer",
+ "description": "Proxy server, similar to the one passed to --proxy-server",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "proxyBypassList",
+ "description": "Proxy bypass list, similar to the one passed to --proxy-bypass-list",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "originsWithUniversalNetworkAccess",
+ "description": "An optional list of origins to grant unlimited cross-origin access to.\nParts of the URL other than those constituting origin are ignored.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "browserContextId",
+ "description": "The id of the context created.",
+ "$ref": "Browser.BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "getBrowserContexts",
+ "description": "Returns all browser contexts created with `Target.createBrowserContext` method.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "browserContextIds",
+ "description": "An array of browser context ids.",
+ "type": "array",
+ "items": {
+ "$ref": "Browser.BrowserContextID"
+ }
+ }
+ ]
+ },
+ {
+ "name": "createTarget",
+ "description": "Creates a new page.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "The initial URL the page will be navigated to. An empty string indicates about:blank.",
+ "type": "string"
+ },
+ {
+ "name": "width",
+ "description": "Frame width in DIP (headless chrome only).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Frame height in DIP (headless chrome only).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "browserContextId",
+ "description": "The browser context to create the page in.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Browser.BrowserContextID"
+ },
+ {
+ "name": "enableBeginFrameControl",
+ "description": "Whether BeginFrames for this target will be controlled via DevTools (headless chrome only,\nnot supported on MacOS yet, false by default).",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "newWindow",
+ "description": "Whether to create a new Window or Tab (chrome-only, false by default).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "background",
+ "description": "Whether to create the target in background or foreground (chrome-only,\nfalse by default).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "forTab",
+ "description": "Whether to create the target of type \"tab\".",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "targetId",
+ "description": "The id of the page opened.",
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "detachFromTarget",
+ "description": "Detaches session with given id.",
+ "parameters": [
+ {
+ "name": "sessionId",
+ "description": "Session to detach.",
+ "optional": true,
+ "$ref": "SessionID"
+ },
+ {
+ "name": "targetId",
+ "description": "Deprecated.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "disposeBrowserContext",
+ "description": "Deletes a BrowserContext. All the belonging pages will be closed without calling their\nbeforeunload hooks.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "browserContextId",
+ "$ref": "Browser.BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "getTargetInfo",
+ "description": "Returns information about a target.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "targetId",
+ "optional": true,
+ "$ref": "TargetID"
+ }
+ ],
+ "returns": [
+ {
+ "name": "targetInfo",
+ "$ref": "TargetInfo"
+ }
+ ]
+ },
+ {
+ "name": "getTargets",
+ "description": "Retrieves a list of available targets.",
+ "parameters": [
+ {
+ "name": "filter",
+ "description": "Only targets matching filter will be reported. If filter is not specified\nand target discovery is currently enabled, a filter used for target discovery\nis used for consistency.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TargetFilter"
+ }
+ ],
+ "returns": [
+ {
+ "name": "targetInfos",
+ "description": "The list of targets.",
+ "type": "array",
+ "items": {
+ "$ref": "TargetInfo"
+ }
+ }
+ ]
+ },
+ {
+ "name": "sendMessageToTarget",
+ "description": "Sends protocol message over session with given id.\nConsider using flat mode instead; see commands attachToTarget, setAutoAttach,\nand crbug.com/991325.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "message",
+ "type": "string"
+ },
+ {
+ "name": "sessionId",
+ "description": "Identifier of the session.",
+ "optional": true,
+ "$ref": "SessionID"
+ },
+ {
+ "name": "targetId",
+ "description": "Deprecated.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "setAutoAttach",
+ "description": "Controls whether to automatically attach to new targets which are considered to be related to\nthis one. When turned on, attaches to all existing related targets as well. When turned off,\nautomatically detaches from all currently attached targets.\nThis also clears all targets added by `autoAttachRelated` from the list of targets to watch\nfor creation of related targets.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "autoAttach",
+ "description": "Whether to auto-attach to related targets.",
+ "type": "boolean"
+ },
+ {
+ "name": "waitForDebuggerOnStart",
+ "description": "Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\nto run paused targets.",
+ "type": "boolean"
+ },
+ {
+ "name": "flatten",
+ "description": "Enables \"flat\" access to the session via specifying sessionId attribute in the commands.\nWe plan to make this the default, deprecate non-flattened mode,\nand eventually retire it. See crbug.com/991325.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "filter",
+ "description": "Only targets matching filter will be attached.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TargetFilter"
+ }
+ ]
+ },
+ {
+ "name": "autoAttachRelated",
+ "description": "Adds the specified target to the list of targets that will be monitored for any related target\ncreation (such as child frames, child workers and new versions of service worker) and reported\nthrough `attachedToTarget`. The specified target is also auto-attached.\nThis cancels the effect of any previous `setAutoAttach` and is also cancelled by subsequent\n`setAutoAttach`. Only available at the Browser target.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ },
+ {
+ "name": "waitForDebuggerOnStart",
+ "description": "Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\nto run paused targets.",
+ "type": "boolean"
+ },
+ {
+ "name": "filter",
+ "description": "Only targets matching filter will be attached.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TargetFilter"
+ }
+ ]
+ },
+ {
+ "name": "setDiscoverTargets",
+ "description": "Controls whether to discover available targets and notify via\n`targetCreated/targetInfoChanged/targetDestroyed` events.",
+ "parameters": [
+ {
+ "name": "discover",
+ "description": "Whether to discover available targets.",
+ "type": "boolean"
+ },
+ {
+ "name": "filter",
+ "description": "Only targets matching filter will be attached. If `discover` is false,\n`filter` must be omitted or empty.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TargetFilter"
+ }
+ ]
+ },
+ {
+ "name": "setRemoteLocations",
+ "description": "Enables target discovery for the specified locations, when `setDiscoverTargets` was set to\n`true`.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "locations",
+ "description": "List of remote locations.",
+ "type": "array",
+ "items": {
+ "$ref": "RemoteLocation"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "attachedToTarget",
+ "description": "Issued when attached to target because of auto-attach or `attachToTarget` command.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "sessionId",
+ "description": "Identifier assigned to the session used to send/receive messages.",
+ "$ref": "SessionID"
+ },
+ {
+ "name": "targetInfo",
+ "$ref": "TargetInfo"
+ },
+ {
+ "name": "waitingForDebugger",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "detachedFromTarget",
+ "description": "Issued when detached from target for any reason (including `detachFromTarget` command). Can be\nissued multiple times per target if multiple sessions have been attached to it.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "sessionId",
+ "description": "Detached session identifier.",
+ "$ref": "SessionID"
+ },
+ {
+ "name": "targetId",
+ "description": "Deprecated.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "receivedMessageFromTarget",
+ "description": "Notifies about a new protocol message received from the session (as reported in\n`attachedToTarget` event).",
+ "parameters": [
+ {
+ "name": "sessionId",
+ "description": "Identifier of a session which sends a message.",
+ "$ref": "SessionID"
+ },
+ {
+ "name": "message",
+ "type": "string"
+ },
+ {
+ "name": "targetId",
+ "description": "Deprecated.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "targetCreated",
+ "description": "Issued when a possible inspection target is created.",
+ "parameters": [
+ {
+ "name": "targetInfo",
+ "$ref": "TargetInfo"
+ }
+ ]
+ },
+ {
+ "name": "targetDestroyed",
+ "description": "Issued when a target is destroyed.",
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "targetCrashed",
+ "description": "Issued when a target has crashed.",
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ },
+ {
+ "name": "status",
+ "description": "Termination status type.",
+ "type": "string"
+ },
+ {
+ "name": "errorCode",
+ "description": "Termination error code.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "targetInfoChanged",
+ "description": "Issued when some information about a target has changed. This only happens between\n`targetCreated` and `targetDestroyed`.",
+ "parameters": [
+ {
+ "name": "targetInfo",
+ "$ref": "TargetInfo"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Tethering",
+ "description": "The Tethering domain defines methods and events for browser port binding.",
+ "experimental": true,
+ "commands": [
+ {
+ "name": "bind",
+ "description": "Request browser port binding.",
+ "parameters": [
+ {
+ "name": "port",
+ "description": "Port number to bind.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "unbind",
+ "description": "Request browser port unbinding.",
+ "parameters": [
+ {
+ "name": "port",
+ "description": "Port number to unbind.",
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "accepted",
+ "description": "Informs that port was successfully bound and got a specified connection id.",
+ "parameters": [
+ {
+ "name": "port",
+ "description": "Port number that was successfully bound.",
+ "type": "integer"
+ },
+ {
+ "name": "connectionId",
+ "description": "Connection id to be used.",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Tracing",
+ "experimental": true,
+ "dependencies": [
+ "IO"
+ ],
+ "types": [
+ {
+ "id": "MemoryDumpConfig",
+ "description": "Configuration for memory dump. Used only when \"memory-infra\" category is enabled.",
+ "type": "object"
+ },
+ {
+ "id": "TraceConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "recordMode",
+ "description": "Controls how the trace buffer stores data.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "recordUntilFull",
+ "recordContinuously",
+ "recordAsMuchAsPossible",
+ "echoToConsole"
+ ]
+ },
+ {
+ "name": "traceBufferSizeInKb",
+ "description": "Size of the trace buffer in kilobytes. If not specified or zero is passed, a default value\nof 200 MB would be used.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "enableSampling",
+ "description": "Turns on JavaScript stack sampling.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "enableSystrace",
+ "description": "Turns on system tracing.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "enableArgumentFilter",
+ "description": "Turns on argument filter.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includedCategories",
+ "description": "Included category filters.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "excludedCategories",
+ "description": "Excluded category filters.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "syntheticDelays",
+ "description": "Configuration to synthesize the delays in tracing.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "memoryDumpConfig",
+ "description": "Configuration for memory dump triggers. Used only when \"memory-infra\" category is enabled.",
+ "optional": true,
+ "$ref": "MemoryDumpConfig"
+ }
+ ]
+ },
+ {
+ "id": "StreamFormat",
+ "description": "Data format of a trace. Can be either the legacy JSON format or the\nprotocol buffer format. Note that the JSON format will be deprecated soon.",
+ "type": "string",
+ "enum": [
+ "json",
+ "proto"
+ ]
+ },
+ {
+ "id": "StreamCompression",
+ "description": "Compression type to use for traces returned via streams.",
+ "type": "string",
+ "enum": [
+ "none",
+ "gzip"
+ ]
+ },
+ {
+ "id": "MemoryDumpLevelOfDetail",
+ "description": "Details exposed when memory request explicitly declared.\nKeep consistent with memory_dump_request_args.h and\nmemory_instrumentation.mojom",
+ "type": "string",
+ "enum": [
+ "background",
+ "light",
+ "detailed"
+ ]
+ },
+ {
+ "id": "TracingBackend",
+ "description": "Backend type to use for tracing. `chrome` uses the Chrome-integrated\ntracing service and is supported on all platforms. `system` is only\nsupported on Chrome OS and uses the Perfetto system tracing service.\n`auto` chooses `system` when the perfettoConfig provided to Tracing.start\nspecifies at least one non-Chrome data source; otherwise uses `chrome`.",
+ "type": "string",
+ "enum": [
+ "auto",
+ "chrome",
+ "system"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "end",
+ "description": "Stop trace events collection."
+ },
+ {
+ "name": "getCategories",
+ "description": "Gets supported tracing categories.",
+ "returns": [
+ {
+ "name": "categories",
+ "description": "A list of supported tracing categories.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "recordClockSyncMarker",
+ "description": "Record a clock sync marker in the trace.",
+ "parameters": [
+ {
+ "name": "syncId",
+ "description": "The ID of this clock sync marker",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "requestMemoryDump",
+ "description": "Request a global memory dump.",
+ "parameters": [
+ {
+ "name": "deterministic",
+ "description": "Enables more deterministic results by forcing garbage collection",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "levelOfDetail",
+ "description": "Specifies level of details in memory dump. Defaults to \"detailed\".",
+ "optional": true,
+ "$ref": "MemoryDumpLevelOfDetail"
+ }
+ ],
+ "returns": [
+ {
+ "name": "dumpGuid",
+ "description": "GUID of the resulting global memory dump.",
+ "type": "string"
+ },
+ {
+ "name": "success",
+ "description": "True iff the global memory dump succeeded.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "start",
+ "description": "Start trace events collection.",
+ "parameters": [
+ {
+ "name": "categories",
+ "description": "Category/tag filter",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "options",
+ "description": "Tracing options",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "bufferUsageReportingInterval",
+ "description": "If set, the agent will issue bufferUsage events at this interval, specified in milliseconds",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "transferMode",
+ "description": "Whether to report trace events as series of dataCollected events or to save trace to a\nstream (defaults to `ReportEvents`).",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "ReportEvents",
+ "ReturnAsStream"
+ ]
+ },
+ {
+ "name": "streamFormat",
+ "description": "Trace data format to use. This only applies when using `ReturnAsStream`\ntransfer mode (defaults to `json`).",
+ "optional": true,
+ "$ref": "StreamFormat"
+ },
+ {
+ "name": "streamCompression",
+ "description": "Compression format to use. This only applies when using `ReturnAsStream`\ntransfer mode (defaults to `none`)",
+ "optional": true,
+ "$ref": "StreamCompression"
+ },
+ {
+ "name": "traceConfig",
+ "optional": true,
+ "$ref": "TraceConfig"
+ },
+ {
+ "name": "perfettoConfig",
+ "description": "Base64-encoded serialized perfetto.protos.TraceConfig protobuf message\nWhen specified, the parameters `categories`, `options`, `traceConfig`\nare ignored. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "tracingBackend",
+ "description": "Backend type (defaults to `auto`)",
+ "optional": true,
+ "$ref": "TracingBackend"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "bufferUsage",
+ "parameters": [
+ {
+ "name": "percentFull",
+ "description": "A number in range [0..1] that indicates the used size of event buffer as a fraction of its\ntotal size.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "eventCount",
+ "description": "An approximate number of events in the trace log.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "value",
+ "description": "A number in range [0..1] that indicates the used size of event buffer as a fraction of its\ntotal size.",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "dataCollected",
+ "description": "Contains a bucket of collected trace events. When tracing is stopped collected events will be\nsent as a sequence of dataCollected events followed by tracingComplete event.",
+ "parameters": [
+ {
+ "name": "value",
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ ]
+ },
+ {
+ "name": "tracingComplete",
+ "description": "Signals that tracing is stopped and there is no trace buffers pending flush, all data were\ndelivered via dataCollected events.",
+ "parameters": [
+ {
+ "name": "dataLossOccurred",
+ "description": "Indicates whether some trace data is known to have been lost, e.g. because the trace ring\nbuffer wrapped around.",
+ "type": "boolean"
+ },
+ {
+ "name": "stream",
+ "description": "A handle of the stream that holds resulting trace data.",
+ "optional": true,
+ "$ref": "IO.StreamHandle"
+ },
+ {
+ "name": "traceFormat",
+ "description": "Trace data format of returned stream.",
+ "optional": true,
+ "$ref": "StreamFormat"
+ },
+ {
+ "name": "streamCompression",
+ "description": "Compression format of returned stream.",
+ "optional": true,
+ "$ref": "StreamCompression"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Fetch",
+ "description": "A domain for letting clients substitute browser's network layer with client code.",
+ "dependencies": [
+ "Network",
+ "IO",
+ "Page"
+ ],
+ "types": [
+ {
+ "id": "RequestId",
+ "description": "Unique request identifier.",
+ "type": "string"
+ },
+ {
+ "id": "RequestStage",
+ "description": "Stages of the request to handle. Request will intercept before the request is\nsent. Response will intercept after the response is received (but before response\nbody is received).",
+ "type": "string",
+ "enum": [
+ "Request",
+ "Response"
+ ]
+ },
+ {
+ "id": "RequestPattern",
+ "type": "object",
+ "properties": [
+ {
+ "name": "urlPattern",
+ "description": "Wildcards (`'*'` -> zero or more, `'?'` -> exactly one) are allowed. Escape character is\nbackslash. Omitting is equivalent to `\"*\"`.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "resourceType",
+ "description": "If set, only requests for matching resource types will be intercepted.",
+ "optional": true,
+ "$ref": "Network.ResourceType"
+ },
+ {
+ "name": "requestStage",
+ "description": "Stage at which to begin intercepting requests. Default is Request.",
+ "optional": true,
+ "$ref": "RequestStage"
+ }
+ ]
+ },
+ {
+ "id": "HeaderEntry",
+ "description": "Response HTTP header entry",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AuthChallenge",
+ "description": "Authorization challenge for HTTP status code 401 or 407.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "source",
+ "description": "Source of the authentication challenge.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "Server",
+ "Proxy"
+ ]
+ },
+ {
+ "name": "origin",
+ "description": "Origin of the challenger.",
+ "type": "string"
+ },
+ {
+ "name": "scheme",
+ "description": "The authentication scheme used, such as basic or digest",
+ "type": "string"
+ },
+ {
+ "name": "realm",
+ "description": "The realm of the challenge. May be empty.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AuthChallengeResponse",
+ "description": "Response to an AuthChallenge.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "response",
+ "description": "The decision on what to do in response to the authorization challenge. Default means\ndeferring to the default behavior of the net stack, which will likely either the Cancel\nauthentication or display a popup dialog box.",
+ "type": "string",
+ "enum": [
+ "Default",
+ "CancelAuth",
+ "ProvideCredentials"
+ ]
+ },
+ {
+ "name": "username",
+ "description": "The username to provide, possibly empty. Should only be set if response is\nProvideCredentials.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "password",
+ "description": "The password to provide, possibly empty. Should only be set if response is\nProvideCredentials.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables the fetch domain."
+ },
+ {
+ "name": "enable",
+ "description": "Enables issuing of requestPaused events. A request will be paused until client\ncalls one of failRequest, fulfillRequest or continueRequest/continueWithAuth.",
+ "parameters": [
+ {
+ "name": "patterns",
+ "description": "If specified, only requests matching any of these patterns will produce\nfetchRequested event and will be paused until clients response. If not set,\nall requests will be affected.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "RequestPattern"
+ }
+ },
+ {
+ "name": "handleAuthRequests",
+ "description": "If true, authRequired events will be issued and requests will be paused\nexpecting a call to continueWithAuth.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "failRequest",
+ "description": "Causes the request to fail with specified reason.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "An id the client received in requestPaused event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "errorReason",
+ "description": "Causes the request to fail with the given reason.",
+ "$ref": "Network.ErrorReason"
+ }
+ ]
+ },
+ {
+ "name": "fulfillRequest",
+ "description": "Provides response to the request.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "An id the client received in requestPaused event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "responseCode",
+ "description": "An HTTP response code.",
+ "type": "integer"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Response headers.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "HeaderEntry"
+ }
+ },
+ {
+ "name": "binaryResponseHeaders",
+ "description": "Alternative way of specifying response headers as a \\0-separated\nseries of name: value pairs. Prefer the above method unless you\nneed to represent some non-UTF8 values that can't be transmitted\nover the protocol as text. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "body",
+ "description": "A response body. If absent, original response body will be used if\nthe request is intercepted at the response stage and empty body\nwill be used if the request is intercepted at the request stage. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "responsePhrase",
+ "description": "A textual representation of responseCode.\nIf absent, a standard phrase matching responseCode is used.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "continueRequest",
+ "description": "Continues the request, optionally modifying some of its parameters.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "An id the client received in requestPaused event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "url",
+ "description": "If set, the request url will be modified in a way that's not observable by page.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "method",
+ "description": "If set, the request method is overridden.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "postData",
+ "description": "If set, overrides the post data in the request. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "description": "If set, overrides the request headers. Note that the overrides do not\nextend to subsequent redirect hops, if a redirect happens. Another override\nmay be applied to a different request produced by a redirect.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "HeaderEntry"
+ }
+ },
+ {
+ "name": "interceptResponse",
+ "description": "If set, overrides response interception behavior for this request.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "continueWithAuth",
+ "description": "Continues a request supplying authChallengeResponse following authRequired event.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "An id the client received in authRequired event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "authChallengeResponse",
+ "description": "Response to with an authChallenge.",
+ "$ref": "AuthChallengeResponse"
+ }
+ ]
+ },
+ {
+ "name": "continueResponse",
+ "description": "Continues loading of the paused response, optionally modifying the\nresponse headers. If either responseCode or headers are modified, all of them\nmust be present.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "An id the client received in requestPaused event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "responseCode",
+ "description": "An HTTP response code. If absent, original response code will be used.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "responsePhrase",
+ "description": "A textual representation of responseCode.\nIf absent, a standard phrase matching responseCode is used.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Response headers. If absent, original response headers will be used.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "HeaderEntry"
+ }
+ },
+ {
+ "name": "binaryResponseHeaders",
+ "description": "Alternative way of specifying response headers as a \\0-separated\nseries of name: value pairs. Prefer the above method unless you\nneed to represent some non-UTF8 values that can't be transmitted\nover the protocol as text. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getResponseBody",
+ "description": "Causes the body of the response to be received from the server and\nreturned as a single string. May only be issued for a request that\nis paused in the Response stage and is mutually exclusive with\ntakeResponseBodyForInterceptionAsStream. Calling other methods that\naffect the request or disabling fetch domain before body is received\nresults in an undefined behavior.\nNote that the response body is not available for redirects. Requests\npaused in the _redirect received_ state may be differentiated by\n`responseCode` and presence of `location` response header, see\ncomments to `requestPaused` for details.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier for the intercepted request to get body for.",
+ "$ref": "RequestId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "body",
+ "description": "Response body.",
+ "type": "string"
+ },
+ {
+ "name": "base64Encoded",
+ "description": "True, if content was sent as base64.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "takeResponseBodyAsStream",
+ "description": "Returns a handle to the stream representing the response body.\nThe request must be paused in the HeadersReceived stage.\nNote that after this command the request can't be continued\nas is -- client either needs to cancel it or to provide the\nresponse body.\nThe stream only supports sequential read, IO.read will fail if the position\nis specified.\nThis method is mutually exclusive with getResponseBody.\nCalling other methods that affect the request or disabling fetch\ndomain before body is received results in an undefined behavior.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "$ref": "RequestId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "stream",
+ "$ref": "IO.StreamHandle"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "requestPaused",
+ "description": "Issued when the domain is enabled and the request URL matches the\nspecified filter. The request is paused until the client responds\nwith one of continueRequest, failRequest or fulfillRequest.\nThe stage of the request can be determined by presence of responseErrorReason\nand responseStatusCode -- the request is at the response stage if either\nof these fields is present and in the request stage otherwise.\nRedirect responses and subsequent requests are reported similarly to regular\nresponses and requests. Redirect responses may be distinguished by the value\nof `responseStatusCode` (which is one of 301, 302, 303, 307, 308) along with\npresence of the `location` header. Requests resulting from a redirect will\nhave `redirectedRequestId` field set.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Each request the page makes will have a unique id.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "request",
+ "description": "The details of the request.",
+ "$ref": "Network.Request"
+ },
+ {
+ "name": "frameId",
+ "description": "The id of the frame that initiated the request.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "resourceType",
+ "description": "How the requested resource will be used.",
+ "$ref": "Network.ResourceType"
+ },
+ {
+ "name": "responseErrorReason",
+ "description": "Response error if intercepted at response stage.",
+ "optional": true,
+ "$ref": "Network.ErrorReason"
+ },
+ {
+ "name": "responseStatusCode",
+ "description": "Response code if intercepted at response stage.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "responseStatusText",
+ "description": "Response status text if intercepted at response stage.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Response headers if intercepted at the response stage.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "HeaderEntry"
+ }
+ },
+ {
+ "name": "networkId",
+ "description": "If the intercepted request had a corresponding Network.requestWillBeSent event fired for it,\nthen this networkId will be the same as the requestId present in the requestWillBeSent event.",
+ "optional": true,
+ "$ref": "Network.RequestId"
+ },
+ {
+ "name": "redirectedRequestId",
+ "description": "If the request is due to a redirect response from the server, the id of the request that\nhas caused the redirect.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "authRequired",
+ "description": "Issued when the domain is enabled with handleAuthRequests set to true.\nThe request is paused until client responds with continueWithAuth.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Each request the page makes will have a unique id.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "request",
+ "description": "The details of the request.",
+ "$ref": "Network.Request"
+ },
+ {
+ "name": "frameId",
+ "description": "The id of the frame that initiated the request.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "resourceType",
+ "description": "How the requested resource will be used.",
+ "$ref": "Network.ResourceType"
+ },
+ {
+ "name": "authChallenge",
+ "description": "Details of the Authorization Challenge encountered.\nIf this is set, client should respond with continueRequest that\ncontains AuthChallengeResponse.",
+ "$ref": "AuthChallenge"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "WebAudio",
+ "description": "This domain allows inspection of Web Audio API.\nhttps://webaudio.github.io/web-audio-api/",
+ "experimental": true,
+ "types": [
+ {
+ "id": "GraphObjectId",
+ "description": "An unique ID for a graph object (AudioContext, AudioNode, AudioParam) in Web Audio API",
+ "type": "string"
+ },
+ {
+ "id": "ContextType",
+ "description": "Enum of BaseAudioContext types",
+ "type": "string",
+ "enum": [
+ "realtime",
+ "offline"
+ ]
+ },
+ {
+ "id": "ContextState",
+ "description": "Enum of AudioContextState from the spec",
+ "type": "string",
+ "enum": [
+ "suspended",
+ "running",
+ "closed"
+ ]
+ },
+ {
+ "id": "NodeType",
+ "description": "Enum of AudioNode types",
+ "type": "string"
+ },
+ {
+ "id": "ChannelCountMode",
+ "description": "Enum of AudioNode::ChannelCountMode from the spec",
+ "type": "string",
+ "enum": [
+ "clamped-max",
+ "explicit",
+ "max"
+ ]
+ },
+ {
+ "id": "ChannelInterpretation",
+ "description": "Enum of AudioNode::ChannelInterpretation from the spec",
+ "type": "string",
+ "enum": [
+ "discrete",
+ "speakers"
+ ]
+ },
+ {
+ "id": "ParamType",
+ "description": "Enum of AudioParam types",
+ "type": "string"
+ },
+ {
+ "id": "AutomationRate",
+ "description": "Enum of AudioParam::AutomationRate from the spec",
+ "type": "string",
+ "enum": [
+ "a-rate",
+ "k-rate"
+ ]
+ },
+ {
+ "id": "ContextRealtimeData",
+ "description": "Fields in AudioContext that change in real-time.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "currentTime",
+ "description": "The current context time in second in BaseAudioContext.",
+ "type": "number"
+ },
+ {
+ "name": "renderCapacity",
+ "description": "The time spent on rendering graph divided by render quantum duration,\nand multiplied by 100. 100 means the audio renderer reached the full\ncapacity and glitch may occur.",
+ "type": "number"
+ },
+ {
+ "name": "callbackIntervalMean",
+ "description": "A running mean of callback interval.",
+ "type": "number"
+ },
+ {
+ "name": "callbackIntervalVariance",
+ "description": "A running variance of callback interval.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "BaseAudioContext",
+ "description": "Protocol object for BaseAudioContext",
+ "type": "object",
+ "properties": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "contextType",
+ "$ref": "ContextType"
+ },
+ {
+ "name": "contextState",
+ "$ref": "ContextState"
+ },
+ {
+ "name": "realtimeData",
+ "optional": true,
+ "$ref": "ContextRealtimeData"
+ },
+ {
+ "name": "callbackBufferSize",
+ "description": "Platform-dependent callback buffer size.",
+ "type": "number"
+ },
+ {
+ "name": "maxOutputChannelCount",
+ "description": "Number of output channels supported by audio hardware in use.",
+ "type": "number"
+ },
+ {
+ "name": "sampleRate",
+ "description": "Context sample rate.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "AudioListener",
+ "description": "Protocol object for AudioListener",
+ "type": "object",
+ "properties": [
+ {
+ "name": "listenerId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ }
+ ]
+ },
+ {
+ "id": "AudioNode",
+ "description": "Protocol object for AudioNode",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "nodeType",
+ "$ref": "NodeType"
+ },
+ {
+ "name": "numberOfInputs",
+ "type": "number"
+ },
+ {
+ "name": "numberOfOutputs",
+ "type": "number"
+ },
+ {
+ "name": "channelCount",
+ "type": "number"
+ },
+ {
+ "name": "channelCountMode",
+ "$ref": "ChannelCountMode"
+ },
+ {
+ "name": "channelInterpretation",
+ "$ref": "ChannelInterpretation"
+ }
+ ]
+ },
+ {
+ "id": "AudioParam",
+ "description": "Protocol object for AudioParam",
+ "type": "object",
+ "properties": [
+ {
+ "name": "paramId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "nodeId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "paramType",
+ "$ref": "ParamType"
+ },
+ {
+ "name": "rate",
+ "$ref": "AutomationRate"
+ },
+ {
+ "name": "defaultValue",
+ "type": "number"
+ },
+ {
+ "name": "minValue",
+ "type": "number"
+ },
+ {
+ "name": "maxValue",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Enables the WebAudio domain and starts sending context lifetime events."
+ },
+ {
+ "name": "disable",
+ "description": "Disables the WebAudio domain."
+ },
+ {
+ "name": "getRealtimeData",
+ "description": "Fetch the realtime data from the registered contexts.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "realtimeData",
+ "$ref": "ContextRealtimeData"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "contextCreated",
+ "description": "Notifies that a new BaseAudioContext has been created.",
+ "parameters": [
+ {
+ "name": "context",
+ "$ref": "BaseAudioContext"
+ }
+ ]
+ },
+ {
+ "name": "contextWillBeDestroyed",
+ "description": "Notifies that an existing BaseAudioContext will be destroyed.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ }
+ ]
+ },
+ {
+ "name": "contextChanged",
+ "description": "Notifies that existing BaseAudioContext has changed some properties (id stays the same)..",
+ "parameters": [
+ {
+ "name": "context",
+ "$ref": "BaseAudioContext"
+ }
+ ]
+ },
+ {
+ "name": "audioListenerCreated",
+ "description": "Notifies that the construction of an AudioListener has finished.",
+ "parameters": [
+ {
+ "name": "listener",
+ "$ref": "AudioListener"
+ }
+ ]
+ },
+ {
+ "name": "audioListenerWillBeDestroyed",
+ "description": "Notifies that a new AudioListener has been created.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "listenerId",
+ "$ref": "GraphObjectId"
+ }
+ ]
+ },
+ {
+ "name": "audioNodeCreated",
+ "description": "Notifies that a new AudioNode has been created.",
+ "parameters": [
+ {
+ "name": "node",
+ "$ref": "AudioNode"
+ }
+ ]
+ },
+ {
+ "name": "audioNodeWillBeDestroyed",
+ "description": "Notifies that an existing AudioNode has been destroyed.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "nodeId",
+ "$ref": "GraphObjectId"
+ }
+ ]
+ },
+ {
+ "name": "audioParamCreated",
+ "description": "Notifies that a new AudioParam has been created.",
+ "parameters": [
+ {
+ "name": "param",
+ "$ref": "AudioParam"
+ }
+ ]
+ },
+ {
+ "name": "audioParamWillBeDestroyed",
+ "description": "Notifies that an existing AudioParam has been destroyed.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "nodeId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "paramId",
+ "$ref": "GraphObjectId"
+ }
+ ]
+ },
+ {
+ "name": "nodesConnected",
+ "description": "Notifies that two AudioNodes are connected.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "destinationId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceOutputIndex",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "destinationInputIndex",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "nodesDisconnected",
+ "description": "Notifies that AudioNodes are disconnected. The destination can be null, and it means all the outgoing connections from the source are disconnected.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "destinationId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceOutputIndex",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "destinationInputIndex",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "nodeParamConnected",
+ "description": "Notifies that an AudioNode is connected to an AudioParam.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "destinationId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceOutputIndex",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "nodeParamDisconnected",
+ "description": "Notifies that an AudioNode is disconnected to an AudioParam.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "destinationId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceOutputIndex",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "WebAuthn",
+ "description": "This domain allows configuring virtual authenticators to test the WebAuthn\nAPI.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "AuthenticatorId",
+ "type": "string"
+ },
+ {
+ "id": "AuthenticatorProtocol",
+ "type": "string",
+ "enum": [
+ "u2f",
+ "ctap2"
+ ]
+ },
+ {
+ "id": "Ctap2Version",
+ "type": "string",
+ "enum": [
+ "ctap2_0",
+ "ctap2_1"
+ ]
+ },
+ {
+ "id": "AuthenticatorTransport",
+ "type": "string",
+ "enum": [
+ "usb",
+ "nfc",
+ "ble",
+ "cable",
+ "internal"
+ ]
+ },
+ {
+ "id": "VirtualAuthenticatorOptions",
+ "type": "object",
+ "properties": [
+ {
+ "name": "protocol",
+ "$ref": "AuthenticatorProtocol"
+ },
+ {
+ "name": "ctap2Version",
+ "description": "Defaults to ctap2_0. Ignored if |protocol| == u2f.",
+ "optional": true,
+ "$ref": "Ctap2Version"
+ },
+ {
+ "name": "transport",
+ "$ref": "AuthenticatorTransport"
+ },
+ {
+ "name": "hasResidentKey",
+ "description": "Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "hasUserVerification",
+ "description": "Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "hasLargeBlob",
+ "description": "If set to true, the authenticator will support the largeBlob extension.\nhttps://w3c.github.io/webauthn#largeBlob\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "hasCredBlob",
+ "description": "If set to true, the authenticator will support the credBlob extension.\nhttps://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-credBlob-extension\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "hasMinPinLength",
+ "description": "If set to true, the authenticator will support the minPinLength extension.\nhttps://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-minpinlength-extension\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "hasPrf",
+ "description": "If set to true, the authenticator will support the prf extension.\nhttps://w3c.github.io/webauthn/#prf-extension\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "automaticPresenceSimulation",
+ "description": "If set to true, tests of user presence will succeed immediately.\nOtherwise, they will not be resolved. Defaults to true.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isUserVerified",
+ "description": "Sets whether User Verification succeeds or fails for an authenticator.\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "Credential",
+ "type": "object",
+ "properties": [
+ {
+ "name": "credentialId",
+ "type": "string"
+ },
+ {
+ "name": "isResidentCredential",
+ "type": "boolean"
+ },
+ {
+ "name": "rpId",
+ "description": "Relying Party ID the credential is scoped to. Must be set when adding a\ncredential.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "privateKey",
+ "description": "The ECDSA P-256 private key in PKCS#8 format. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ },
+ {
+ "name": "userHandle",
+ "description": "An opaque byte sequence with a maximum size of 64 bytes mapping the\ncredential to a specific user. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "signCount",
+ "description": "Signature counter. This is incremented by one for each successful\nassertion.\nSee https://w3c.github.io/webauthn/#signature-counter",
+ "type": "integer"
+ },
+ {
+ "name": "largeBlob",
+ "description": "The large blob associated with the credential.\nSee https://w3c.github.io/webauthn/#sctn-large-blob-extension (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Enable the WebAuthn domain and start intercepting credential storage and\nretrieval with a virtual authenticator.",
+ "parameters": [
+ {
+ "name": "enableUI",
+ "description": "Whether to enable the WebAuthn user interface. Enabling the UI is\nrecommended for debugging and demo purposes, as it is closer to the real\nexperience. Disabling the UI is recommended for automated testing.\nSupported at the embedder's discretion if UI is available.\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disable the WebAuthn domain."
+ },
+ {
+ "name": "addVirtualAuthenticator",
+ "description": "Creates and adds a virtual authenticator.",
+ "parameters": [
+ {
+ "name": "options",
+ "$ref": "VirtualAuthenticatorOptions"
+ }
+ ],
+ "returns": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ }
+ ]
+ },
+ {
+ "name": "setResponseOverrideBits",
+ "description": "Resets parameters isBogusSignature, isBadUV, isBadUP to false if they are not present.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "isBogusSignature",
+ "description": "If isBogusSignature is set, overrides the signature in the authenticator response to be zero.\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isBadUV",
+ "description": "If isBadUV is set, overrides the UV bit in the flags in the authenticator response to\nbe zero. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isBadUP",
+ "description": "If isBadUP is set, overrides the UP bit in the flags in the authenticator response to\nbe zero. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "removeVirtualAuthenticator",
+ "description": "Removes the given authenticator.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ }
+ ]
+ },
+ {
+ "name": "addCredential",
+ "description": "Adds the credential to the specified authenticator.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "credential",
+ "$ref": "Credential"
+ }
+ ]
+ },
+ {
+ "name": "getCredential",
+ "description": "Returns a single credential stored in the given virtual authenticator that\nmatches the credential ID.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "credentialId",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "credential",
+ "$ref": "Credential"
+ }
+ ]
+ },
+ {
+ "name": "getCredentials",
+ "description": "Returns all the credentials stored in the given virtual authenticator.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "credentials",
+ "type": "array",
+ "items": {
+ "$ref": "Credential"
+ }
+ }
+ ]
+ },
+ {
+ "name": "removeCredential",
+ "description": "Removes a credential from the authenticator.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "credentialId",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "clearCredentials",
+ "description": "Clears all the credentials from the specified device.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ }
+ ]
+ },
+ {
+ "name": "setUserVerified",
+ "description": "Sets whether User Verification succeeds or fails for an authenticator.\nThe default is true.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "isUserVerified",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setAutomaticPresenceSimulation",
+ "description": "Sets whether tests of user presence will succeed immediately (if true) or fail to resolve (if false) for an authenticator.\nThe default is true.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "enabled",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "credentialAdded",
+ "description": "Triggered when a credential is added to an authenticator.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "credential",
+ "$ref": "Credential"
+ }
+ ]
+ },
+ {
+ "name": "credentialAsserted",
+ "description": "Triggered when a credential is used in a webauthn assertion.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "credential",
+ "$ref": "Credential"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Media",
+ "description": "This domain allows detailed inspection of media elements",
+ "experimental": true,
+ "types": [
+ {
+ "id": "PlayerId",
+ "description": "Players will get an ID that is unique within the agent context.",
+ "type": "string"
+ },
+ {
+ "id": "Timestamp",
+ "type": "number"
+ },
+ {
+ "id": "PlayerMessage",
+ "description": "Have one type per entry in MediaLogRecord::Type\nCorresponds to kMessage",
+ "type": "object",
+ "properties": [
+ {
+ "name": "level",
+ "description": "Keep in sync with MediaLogMessageLevel\nWe are currently keeping the message level 'error' separate from the\nPlayerError type because right now they represent different things,\nthis one being a DVLOG(ERROR) style log message that gets printed\nbased on what log level is selected in the UI, and the other is a\nrepresentation of a media::PipelineStatus object. Soon however we're\ngoing to be moving away from using PipelineStatus for errors and\nintroducing a new error type which should hopefully let us integrate\nthe error log level into the PlayerError type.",
+ "type": "string",
+ "enum": [
+ "error",
+ "warning",
+ "info",
+ "debug"
+ ]
+ },
+ {
+ "name": "message",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "PlayerProperty",
+ "description": "Corresponds to kMediaPropertyChange",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "PlayerEvent",
+ "description": "Corresponds to kMediaEventTriggered",
+ "type": "object",
+ "properties": [
+ {
+ "name": "timestamp",
+ "$ref": "Timestamp"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "PlayerErrorSourceLocation",
+ "description": "Represents logged source line numbers reported in an error.\nNOTE: file and line are from chromium c++ implementation code, not js.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "file",
+ "type": "string"
+ },
+ {
+ "name": "line",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "PlayerError",
+ "description": "Corresponds to kMediaError",
+ "type": "object",
+ "properties": [
+ {
+ "name": "errorType",
+ "type": "string"
+ },
+ {
+ "name": "code",
+ "description": "Code is the numeric enum entry for a specific set of error codes, such\nas PipelineStatusCodes in media/base/pipeline_status.h",
+ "type": "integer"
+ },
+ {
+ "name": "stack",
+ "description": "A trace of where this error was caused / where it passed through.",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerErrorSourceLocation"
+ }
+ },
+ {
+ "name": "cause",
+ "description": "Errors potentially have a root cause error, ie, a DecoderError might be\ncaused by an WindowsError",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerError"
+ }
+ },
+ {
+ "name": "data",
+ "description": "Extra data attached to an error, such as an HRESULT, Video Codec, etc.",
+ "type": "object"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "playerPropertiesChanged",
+ "description": "This can be called multiple times, and can be used to set / override /\nremove player properties. A null propValue indicates removal.",
+ "parameters": [
+ {
+ "name": "playerId",
+ "$ref": "PlayerId"
+ },
+ {
+ "name": "properties",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerProperty"
+ }
+ }
+ ]
+ },
+ {
+ "name": "playerEventsAdded",
+ "description": "Send events as a list, allowing them to be batched on the browser for less\ncongestion. If batched, events must ALWAYS be in chronological order.",
+ "parameters": [
+ {
+ "name": "playerId",
+ "$ref": "PlayerId"
+ },
+ {
+ "name": "events",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerEvent"
+ }
+ }
+ ]
+ },
+ {
+ "name": "playerMessagesLogged",
+ "description": "Send a list of any messages that need to be delivered.",
+ "parameters": [
+ {
+ "name": "playerId",
+ "$ref": "PlayerId"
+ },
+ {
+ "name": "messages",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerMessage"
+ }
+ }
+ ]
+ },
+ {
+ "name": "playerErrorsRaised",
+ "description": "Send a list of any errors that need to be delivered.",
+ "parameters": [
+ {
+ "name": "playerId",
+ "$ref": "PlayerId"
+ },
+ {
+ "name": "errors",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerError"
+ }
+ }
+ ]
+ },
+ {
+ "name": "playersCreated",
+ "description": "Called whenever a player is created, or when a new agent joins and receives\na list of active players. If an agent is restored, it will receive the full\nlist of player ids and all events again.",
+ "parameters": [
+ {
+ "name": "players",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerId"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Enables the Media domain"
+ },
+ {
+ "name": "disable",
+ "description": "Disables the Media domain."
+ }
+ ]
+ },
+ {
+ "domain": "DeviceAccess",
+ "experimental": true,
+ "types": [
+ {
+ "id": "RequestId",
+ "description": "Device request id.",
+ "type": "string"
+ },
+ {
+ "id": "DeviceId",
+ "description": "A device id.",
+ "type": "string"
+ },
+ {
+ "id": "PromptDevice",
+ "description": "Device information displayed in a user prompt to select a device.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "$ref": "DeviceId"
+ },
+ {
+ "name": "name",
+ "description": "Display name as it appears in a device request user prompt.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Enable events in this domain."
+ },
+ {
+ "name": "disable",
+ "description": "Disable events in this domain."
+ },
+ {
+ "name": "selectPrompt",
+ "description": "Select a device in response to a DeviceAccess.deviceRequestPrompted event.",
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "deviceId",
+ "$ref": "DeviceId"
+ }
+ ]
+ },
+ {
+ "name": "cancelPrompt",
+ "description": "Cancel a prompt in response to a DeviceAccess.deviceRequestPrompted event.",
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "RequestId"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "deviceRequestPrompted",
+ "description": "A device request opened a user prompt to select a device. Respond with the\nselectPrompt or cancelPrompt command.",
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "devices",
+ "type": "array",
+ "items": {
+ "$ref": "PromptDevice"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Preload",
+ "experimental": true,
+ "types": [
+ {
+ "id": "RuleSetId",
+ "description": "Unique id",
+ "type": "string"
+ },
+ {
+ "id": "RuleSet",
+ "description": "Corresponds to SpeculationRuleSet",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "$ref": "RuleSetId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Identifies a document which the rule set is associated with.",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "sourceText",
+ "description": "Source text of JSON representing the rule set. If it comes from\n`<script>` tag, it is the textContent of the node. Note that it is\na JSON for valid case.\n\nSee also:\n- https://wicg.github.io/nav-speculation/speculation-rules.html\n- https://github.com/WICG/nav-speculation/blob/main/triggers.md",
+ "type": "string"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "A speculation rule set is either added through an inline\n`<script>` tag or through an external resource via the\n'Speculation-Rules' HTTP header. For the first case, we include\nthe BackendNodeId of the relevant `<script>` tag. For the second\ncase, we include the external URL where the rule set was loaded\nfrom, and also RequestId if Network domain is enabled.\n\nSee also:\n- https://wicg.github.io/nav-speculation/speculation-rules.html#speculation-rules-script\n- https://wicg.github.io/nav-speculation/speculation-rules.html#speculation-rules-header",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "url",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "requestId",
+ "optional": true,
+ "$ref": "Network.RequestId"
+ },
+ {
+ "name": "errorType",
+ "description": "Error information\n`errorMessage` is null iff `errorType` is null.",
+ "optional": true,
+ "$ref": "RuleSetErrorType"
+ },
+ {
+ "name": "errorMessage",
+ "description": "TODO(https://crbug.com/1425354): Replace this property with structured error.",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "RuleSetErrorType",
+ "type": "string",
+ "enum": [
+ "SourceIsNotJsonObject",
+ "InvalidRulesSkipped"
+ ]
+ },
+ {
+ "id": "SpeculationAction",
+ "description": "The type of preloading attempted. It corresponds to\nmojom::SpeculationAction (although PrefetchWithSubresources is omitted as it\nisn't being used by clients).",
+ "type": "string",
+ "enum": [
+ "Prefetch",
+ "Prerender"
+ ]
+ },
+ {
+ "id": "SpeculationTargetHint",
+ "description": "Corresponds to mojom::SpeculationTargetHint.\nSee https://github.com/WICG/nav-speculation/blob/main/triggers.md#window-name-targeting-hints",
+ "type": "string",
+ "enum": [
+ "Blank",
+ "Self"
+ ]
+ },
+ {
+ "id": "PreloadingAttemptKey",
+ "description": "A key that identifies a preloading attempt.\n\nThe url used is the url specified by the trigger (i.e. the initial URL), and\nnot the final url that is navigated to. For example, prerendering allows\nsame-origin main frame navigations during the attempt, but the attempt is\nstill keyed with the initial URL.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "loaderId",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "action",
+ "$ref": "SpeculationAction"
+ },
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "targetHint",
+ "optional": true,
+ "$ref": "SpeculationTargetHint"
+ }
+ ]
+ },
+ {
+ "id": "PreloadingAttemptSource",
+ "description": "Lists sources for a preloading attempt, specifically the ids of rule sets\nthat had a speculation rule that triggered the attempt, and the\nBackendNodeIds of <a href> or <area href> elements that triggered the\nattempt (in the case of attempts triggered by a document rule). It is\npossible for mulitple rule sets and links to trigger a single attempt.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "$ref": "PreloadingAttemptKey"
+ },
+ {
+ "name": "ruleSetIds",
+ "type": "array",
+ "items": {
+ "$ref": "RuleSetId"
+ }
+ },
+ {
+ "name": "nodeIds",
+ "type": "array",
+ "items": {
+ "$ref": "DOM.BackendNodeId"
+ }
+ }
+ ]
+ },
+ {
+ "id": "PrerenderFinalStatus",
+ "description": "List of FinalStatus reasons for Prerender2.",
+ "type": "string",
+ "enum": [
+ "Activated",
+ "Destroyed",
+ "LowEndDevice",
+ "InvalidSchemeRedirect",
+ "InvalidSchemeNavigation",
+ "InProgressNavigation",
+ "NavigationRequestBlockedByCsp",
+ "MainFrameNavigation",
+ "MojoBinderPolicy",
+ "RendererProcessCrashed",
+ "RendererProcessKilled",
+ "Download",
+ "TriggerDestroyed",
+ "NavigationNotCommitted",
+ "NavigationBadHttpStatus",
+ "ClientCertRequested",
+ "NavigationRequestNetworkError",
+ "MaxNumOfRunningPrerendersExceeded",
+ "CancelAllHostsForTesting",
+ "DidFailLoad",
+ "Stop",
+ "SslCertificateError",
+ "LoginAuthRequested",
+ "UaChangeRequiresReload",
+ "BlockedByClient",
+ "AudioOutputDeviceRequested",
+ "MixedContent",
+ "TriggerBackgrounded",
+ "MemoryLimitExceeded",
+ "FailToGetMemoryUsage",
+ "DataSaverEnabled",
+ "HasEffectiveUrl",
+ "ActivatedBeforeStarted",
+ "InactivePageRestriction",
+ "StartFailed",
+ "TimeoutBackgrounded",
+ "CrossSiteRedirectInInitialNavigation",
+ "CrossSiteNavigationInInitialNavigation",
+ "SameSiteCrossOriginRedirectNotOptInInInitialNavigation",
+ "SameSiteCrossOriginNavigationNotOptInInInitialNavigation",
+ "ActivationNavigationParameterMismatch",
+ "ActivatedInBackground",
+ "EmbedderHostDisallowed",
+ "ActivationNavigationDestroyedBeforeSuccess",
+ "TabClosedByUserGesture",
+ "TabClosedWithoutUserGesture",
+ "PrimaryMainFrameRendererProcessCrashed",
+ "PrimaryMainFrameRendererProcessKilled",
+ "ActivationFramePolicyNotCompatible",
+ "PreloadingDisabled",
+ "BatterySaverEnabled",
+ "ActivatedDuringMainFrameNavigation",
+ "PreloadingUnsupportedByWebContents",
+ "CrossSiteRedirectInMainFrameNavigation",
+ "CrossSiteNavigationInMainFrameNavigation",
+ "SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation",
+ "SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation",
+ "MemoryPressureOnTrigger",
+ "MemoryPressureAfterTriggered",
+ "PrerenderingDisabledByDevTools",
+ "ResourceLoadBlockedByClient",
+ "SpeculationRuleRemoved",
+ "ActivatedWithAuxiliaryBrowsingContexts"
+ ]
+ },
+ {
+ "id": "PreloadingStatus",
+ "description": "Preloading status values, see also PreloadingTriggeringOutcome. This\nstatus is shared by prefetchStatusUpdated and prerenderStatusUpdated.",
+ "type": "string",
+ "enum": [
+ "Pending",
+ "Running",
+ "Ready",
+ "Success",
+ "Failure",
+ "NotSupported"
+ ]
+ },
+ {
+ "id": "PrefetchStatus",
+ "description": "TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and\nfilter out the ones that aren't necessary to the developers.",
+ "type": "string",
+ "enum": [
+ "PrefetchAllowed",
+ "PrefetchFailedIneligibleRedirect",
+ "PrefetchFailedInvalidRedirect",
+ "PrefetchFailedMIMENotSupported",
+ "PrefetchFailedNetError",
+ "PrefetchFailedNon2XX",
+ "PrefetchFailedPerPageLimitExceeded",
+ "PrefetchEvicted",
+ "PrefetchHeldback",
+ "PrefetchIneligibleRetryAfter",
+ "PrefetchIsPrivacyDecoy",
+ "PrefetchIsStale",
+ "PrefetchNotEligibleBrowserContextOffTheRecord",
+ "PrefetchNotEligibleDataSaverEnabled",
+ "PrefetchNotEligibleExistingProxy",
+ "PrefetchNotEligibleHostIsNonUnique",
+ "PrefetchNotEligibleNonDefaultStoragePartition",
+ "PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy",
+ "PrefetchNotEligibleSchemeIsNotHttps",
+ "PrefetchNotEligibleUserHasCookies",
+ "PrefetchNotEligibleUserHasServiceWorker",
+ "PrefetchNotEligibleBatterySaverEnabled",
+ "PrefetchNotEligiblePreloadingDisabled",
+ "PrefetchNotFinishedInTime",
+ "PrefetchNotStarted",
+ "PrefetchNotUsedCookiesChanged",
+ "PrefetchProxyNotAvailable",
+ "PrefetchResponseUsed",
+ "PrefetchSuccessfulButNotUsed",
+ "PrefetchNotUsedProbeFailed"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable"
+ },
+ {
+ "name": "disable"
+ }
+ ],
+ "events": [
+ {
+ "name": "ruleSetUpdated",
+ "description": "Upsert. Currently, it is only emitted when a rule set added.",
+ "parameters": [
+ {
+ "name": "ruleSet",
+ "$ref": "RuleSet"
+ }
+ ]
+ },
+ {
+ "name": "ruleSetRemoved",
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "RuleSetId"
+ }
+ ]
+ },
+ {
+ "name": "prerenderAttemptCompleted",
+ "description": "Fired when a prerender attempt is completed.",
+ "parameters": [
+ {
+ "name": "key",
+ "$ref": "PreloadingAttemptKey"
+ },
+ {
+ "name": "initiatingFrameId",
+ "description": "The frame id of the frame initiating prerendering.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "prerenderingUrl",
+ "type": "string"
+ },
+ {
+ "name": "finalStatus",
+ "$ref": "PrerenderFinalStatus"
+ },
+ {
+ "name": "disallowedApiMethod",
+ "description": "This is used to give users more information about the name of the API call\nthat is incompatible with prerender and has caused the cancellation of the attempt",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "preloadEnabledStateUpdated",
+ "description": "Fired when a preload enabled state is updated.",
+ "parameters": [
+ {
+ "name": "disabledByPreference",
+ "type": "boolean"
+ },
+ {
+ "name": "disabledByDataSaver",
+ "type": "boolean"
+ },
+ {
+ "name": "disabledByBatterySaver",
+ "type": "boolean"
+ },
+ {
+ "name": "disabledByHoldbackPrefetchSpeculationRules",
+ "type": "boolean"
+ },
+ {
+ "name": "disabledByHoldbackPrerenderSpeculationRules",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "prefetchStatusUpdated",
+ "description": "Fired when a prefetch attempt is updated.",
+ "parameters": [
+ {
+ "name": "key",
+ "$ref": "PreloadingAttemptKey"
+ },
+ {
+ "name": "initiatingFrameId",
+ "description": "The frame id of the frame initiating prefetch.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "prefetchUrl",
+ "type": "string"
+ },
+ {
+ "name": "status",
+ "$ref": "PreloadingStatus"
+ },
+ {
+ "name": "prefetchStatus",
+ "$ref": "PrefetchStatus"
+ },
+ {
+ "name": "requestId",
+ "$ref": "Network.RequestId"
+ }
+ ]
+ },
+ {
+ "name": "prerenderStatusUpdated",
+ "description": "Fired when a prerender attempt is updated.",
+ "parameters": [
+ {
+ "name": "key",
+ "$ref": "PreloadingAttemptKey"
+ },
+ {
+ "name": "status",
+ "$ref": "PreloadingStatus"
+ },
+ {
+ "name": "prerenderStatus",
+ "optional": true,
+ "$ref": "PrerenderFinalStatus"
+ },
+ {
+ "name": "disallowedMojoInterface",
+ "description": "This is used to give users more information about the name of Mojo interface\nthat is incompatible with prerender and has caused the cancellation of the attempt.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "preloadingAttemptSourcesUpdated",
+ "description": "Send a list of sources for all preloading attempts in a document.",
+ "parameters": [
+ {
+ "name": "loaderId",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "preloadingAttemptSources",
+ "type": "array",
+ "items": {
+ "$ref": "PreloadingAttemptSource"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "FedCm",
+ "description": "This domain allows interacting with the FedCM dialog.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "LoginState",
+ "description": "Whether this is a sign-up or sign-in action for this account, i.e.\nwhether this account has ever been used to sign in to this RP before.",
+ "type": "string",
+ "enum": [
+ "SignIn",
+ "SignUp"
+ ]
+ },
+ {
+ "id": "DialogType",
+ "description": "Whether the dialog shown is an account chooser or an auto re-authentication dialog.",
+ "type": "string",
+ "enum": [
+ "AccountChooser",
+ "AutoReauthn"
+ ]
+ },
+ {
+ "id": "Account",
+ "description": "Corresponds to IdentityRequestAccount",
+ "type": "object",
+ "properties": [
+ {
+ "name": "accountId",
+ "type": "string"
+ },
+ {
+ "name": "email",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "givenName",
+ "type": "string"
+ },
+ {
+ "name": "pictureUrl",
+ "type": "string"
+ },
+ {
+ "name": "idpConfigUrl",
+ "type": "string"
+ },
+ {
+ "name": "idpSigninUrl",
+ "type": "string"
+ },
+ {
+ "name": "loginState",
+ "$ref": "LoginState"
+ },
+ {
+ "name": "termsOfServiceUrl",
+ "description": "These two are only set if the loginState is signUp",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "privacyPolicyUrl",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "dialogShown",
+ "parameters": [
+ {
+ "name": "dialogId",
+ "type": "string"
+ },
+ {
+ "name": "dialogType",
+ "$ref": "DialogType"
+ },
+ {
+ "name": "accounts",
+ "type": "array",
+ "items": {
+ "$ref": "Account"
+ }
+ },
+ {
+ "name": "title",
+ "description": "These exist primarily so that the caller can verify the\nRP context was used appropriately.",
+ "type": "string"
+ },
+ {
+ "name": "subtitle",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "parameters": [
+ {
+ "name": "disableRejectionDelay",
+ "description": "Allows callers to disable the promise rejection delay that would\nnormally happen, if this is unimportant to what's being tested.\n(step 4 of https://fedidcg.github.io/FedCM/#browser-api-rp-sign-in)",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "disable"
+ },
+ {
+ "name": "selectAccount",
+ "parameters": [
+ {
+ "name": "dialogId",
+ "type": "string"
+ },
+ {
+ "name": "accountIndex",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "dismissDialog",
+ "parameters": [
+ {
+ "name": "dialogId",
+ "type": "string"
+ },
+ {
+ "name": "triggerCooldown",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "resetCooldown",
+ "description": "Resets the cooldown time, if any, to allow the next FedCM call to show\na dialog even if one was recently dismissed by the user."
+ }
+ ]
+ },
+ {
+ "domain": "Console",
+ "description": "This domain is deprecated - use Runtime or Log instead.",
+ "deprecated": true,
+ "dependencies": [
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "ConsoleMessage",
+ "description": "Console message.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "source",
+ "description": "Message source.",
+ "type": "string",
+ "enum": [
+ "xml",
+ "javascript",
+ "network",
+ "console-api",
+ "storage",
+ "appcache",
+ "rendering",
+ "security",
+ "other",
+ "deprecation",
+ "worker"
+ ]
+ },
+ {
+ "name": "level",
+ "description": "Message severity.",
+ "type": "string",
+ "enum": [
+ "log",
+ "warning",
+ "error",
+ "debug",
+ "info"
+ ]
+ },
+ {
+ "name": "text",
+ "description": "Message text.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "URL of the message origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "line",
+ "description": "Line number in the resource that generated this message (1-based).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "column",
+ "description": "Column number in the resource that generated this message (1-based).",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "clearMessages",
+ "description": "Does nothing."
+ },
+ {
+ "name": "disable",
+ "description": "Disables console domain, prevents further console messages from being reported to the client."
+ },
+ {
+ "name": "enable",
+ "description": "Enables console domain, sends the messages collected so far to the client by means of the\n`messageAdded` notification."
+ }
+ ],
+ "events": [
+ {
+ "name": "messageAdded",
+ "description": "Issued when new console message is added.",
+ "parameters": [
+ {
+ "name": "message",
+ "description": "Console message that has been added.",
+ "$ref": "ConsoleMessage"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Debugger",
+ "description": "Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing\nbreakpoints, stepping through execution, exploring stack traces, etc.",
+ "dependencies": [
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "BreakpointId",
+ "description": "Breakpoint identifier.",
+ "type": "string"
+ },
+ {
+ "id": "CallFrameId",
+ "description": "Call frame identifier.",
+ "type": "string"
+ },
+ {
+ "id": "Location",
+ "description": "Location in the source code.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "description": "Script identifier as reported in the `Debugger.scriptParsed`.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Line number in the script (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Column number in the script (0-based).",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "ScriptPosition",
+ "description": "Location in the source code.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "lineNumber",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "LocationRange",
+ "description": "Location range within one script.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "start",
+ "$ref": "ScriptPosition"
+ },
+ {
+ "name": "end",
+ "$ref": "ScriptPosition"
+ }
+ ]
+ },
+ {
+ "id": "CallFrame",
+ "description": "JavaScript call frame. Array of call frames form the call stack.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "callFrameId",
+ "description": "Call frame identifier. This identifier is only valid while the virtual machine is paused.",
+ "$ref": "CallFrameId"
+ },
+ {
+ "name": "functionName",
+ "description": "Name of the JavaScript function called on this call frame.",
+ "type": "string"
+ },
+ {
+ "name": "functionLocation",
+ "description": "Location in the source code.",
+ "optional": true,
+ "$ref": "Location"
+ },
+ {
+ "name": "location",
+ "description": "Location in the source code.",
+ "$ref": "Location"
+ },
+ {
+ "name": "url",
+ "description": "JavaScript script name or url.\nDeprecated in favor of using the `location.scriptId` to resolve the URL via a previously\nsent `Debugger.scriptParsed` event.",
+ "deprecated": true,
+ "type": "string"
+ },
+ {
+ "name": "scopeChain",
+ "description": "Scope chain for this call frame.",
+ "type": "array",
+ "items": {
+ "$ref": "Scope"
+ }
+ },
+ {
+ "name": "this",
+ "description": "`this` object for this call frame.",
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "returnValue",
+ "description": "The value being returned, if the function is at return point.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "canBeRestarted",
+ "description": "Valid only while the VM is paused and indicates whether this frame\ncan be restarted or not. Note that a `true` value here does not\nguarantee that Debugger#restartFrame with this CallFrameId will be\nsuccessful, but it is very likely.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "Scope",
+ "description": "Scope description.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Scope type.",
+ "type": "string",
+ "enum": [
+ "global",
+ "local",
+ "with",
+ "closure",
+ "catch",
+ "block",
+ "script",
+ "eval",
+ "module",
+ "wasm-expression-stack"
+ ]
+ },
+ {
+ "name": "object",
+ "description": "Object representing the scope. For `global` and `with` scopes it represents the actual\nobject; for the rest of the scopes, it is artificial transient object enumerating scope\nvariables as its properties.",
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "name",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "startLocation",
+ "description": "Location in the source code where scope starts",
+ "optional": true,
+ "$ref": "Location"
+ },
+ {
+ "name": "endLocation",
+ "description": "Location in the source code where scope ends",
+ "optional": true,
+ "$ref": "Location"
+ }
+ ]
+ },
+ {
+ "id": "SearchMatch",
+ "description": "Search match for resource.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "lineNumber",
+ "description": "Line number in resource content.",
+ "type": "number"
+ },
+ {
+ "name": "lineContent",
+ "description": "Line with match content.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "BreakLocation",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "description": "Script identifier as reported in the `Debugger.scriptParsed`.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Line number in the script (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Column number in the script (0-based).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "type",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "debuggerStatement",
+ "call",
+ "return"
+ ]
+ }
+ ]
+ },
+ {
+ "id": "WasmDisassemblyChunk",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "lines",
+ "description": "The next chunk of disassembled lines.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "bytecodeOffsets",
+ "description": "The bytecode offsets describing the start of each line.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ScriptLanguage",
+ "description": "Enum of possible script languages.",
+ "type": "string",
+ "enum": [
+ "JavaScript",
+ "WebAssembly"
+ ]
+ },
+ {
+ "id": "DebugSymbols",
+ "description": "Debug symbols available for a wasm script.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Type of the debug symbols.",
+ "type": "string",
+ "enum": [
+ "None",
+ "SourceMap",
+ "EmbeddedDWARF",
+ "ExternalDWARF"
+ ]
+ },
+ {
+ "name": "externalURL",
+ "description": "URL of the external symbol source.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "continueToLocation",
+ "description": "Continues execution until specific location is reached.",
+ "parameters": [
+ {
+ "name": "location",
+ "description": "Location to continue to.",
+ "$ref": "Location"
+ },
+ {
+ "name": "targetCallFrames",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "any",
+ "current"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables debugger for given page."
+ },
+ {
+ "name": "enable",
+ "description": "Enables debugger for the given page. Clients should not assume that the debugging has been\nenabled until the result for this command is received.",
+ "parameters": [
+ {
+ "name": "maxScriptsCacheSize",
+ "description": "The maximum size in bytes of collected scripts (not referenced by other heap objects)\nthe debugger can hold. Puts no limit if parameter is omitted.",
+ "experimental": true,
+ "optional": true,
+ "type": "number"
+ }
+ ],
+ "returns": [
+ {
+ "name": "debuggerId",
+ "description": "Unique identifier of the debugger.",
+ "experimental": true,
+ "$ref": "Runtime.UniqueDebuggerId"
+ }
+ ]
+ },
+ {
+ "name": "evaluateOnCallFrame",
+ "description": "Evaluates expression on a given call frame.",
+ "parameters": [
+ {
+ "name": "callFrameId",
+ "description": "Call frame identifier to evaluate on.",
+ "$ref": "CallFrameId"
+ },
+ {
+ "name": "expression",
+ "description": "Expression to evaluate.",
+ "type": "string"
+ },
+ {
+ "name": "objectGroup",
+ "description": "String object group name to put result into (allows rapid releasing resulting object handles\nusing `releaseObjectGroup`).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "includeCommandLineAPI",
+ "description": "Specifies whether command line API should be available to the evaluated expression, defaults\nto false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "silent",
+ "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "returnByValue",
+ "description": "Whether the result is expected to be a JSON object that should be sent by value.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the result.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "throwOnSideEffect",
+ "description": "Whether to throw an exception if side effect cannot be ruled out during evaluation.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "timeout",
+ "description": "Terminate execution after timing out (number of milliseconds).",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Runtime.TimeDelta"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Object wrapper for the evaluation result.",
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "Runtime.ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "getPossibleBreakpoints",
+ "description": "Returns possible locations for breakpoint. scriptId in start and end range locations should be\nthe same.",
+ "parameters": [
+ {
+ "name": "start",
+ "description": "Start of range to search possible breakpoint locations in.",
+ "$ref": "Location"
+ },
+ {
+ "name": "end",
+ "description": "End of range to search possible breakpoint locations in (excluding). When not specified, end\nof scripts is used as end of range.",
+ "optional": true,
+ "$ref": "Location"
+ },
+ {
+ "name": "restrictToFunction",
+ "description": "Only consider locations which are in the same (non-nested) function as start.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "locations",
+ "description": "List of the possible breakpoint locations.",
+ "type": "array",
+ "items": {
+ "$ref": "BreakLocation"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getScriptSource",
+ "description": "Returns source for the script with given id.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script to get source for.",
+ "$ref": "Runtime.ScriptId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "scriptSource",
+ "description": "Script source (empty in case of Wasm bytecode).",
+ "type": "string"
+ },
+ {
+ "name": "bytecode",
+ "description": "Wasm bytecode. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disassembleWasmModule",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script to disassemble",
+ "$ref": "Runtime.ScriptId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "streamId",
+ "description": "For large modules, return a stream from which additional chunks of\ndisassembly can be read successively.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "totalNumberOfLines",
+ "description": "The total number of lines in the disassembly text.",
+ "type": "integer"
+ },
+ {
+ "name": "functionBodyOffsets",
+ "description": "The offsets of all function bodies, in the format [start1, end1,\nstart2, end2, ...] where all ends are exclusive.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "chunk",
+ "description": "The first chunk of disassembly.",
+ "$ref": "WasmDisassemblyChunk"
+ }
+ ]
+ },
+ {
+ "name": "nextWasmDisassemblyChunk",
+ "description": "Disassemble the next chunk of lines for the module corresponding to the\nstream. If disassembly is complete, this API will invalidate the streamId\nand return an empty chunk. Any subsequent calls for the now invalid stream\nwill return errors.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "streamId",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "chunk",
+ "description": "The next chunk of disassembly.",
+ "$ref": "WasmDisassemblyChunk"
+ }
+ ]
+ },
+ {
+ "name": "getWasmBytecode",
+ "description": "This command is deprecated. Use getScriptSource instead.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the Wasm script to get source for.",
+ "$ref": "Runtime.ScriptId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "bytecode",
+ "description": "Script source. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getStackTrace",
+ "description": "Returns stack trace with given `stackTraceId`.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "stackTraceId",
+ "$ref": "Runtime.StackTraceId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "stackTrace",
+ "$ref": "Runtime.StackTrace"
+ }
+ ]
+ },
+ {
+ "name": "pause",
+ "description": "Stops on the next JavaScript statement."
+ },
+ {
+ "name": "pauseOnAsyncCall",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "parentStackTraceId",
+ "description": "Debugger will pause when async call with given stack trace is started.",
+ "$ref": "Runtime.StackTraceId"
+ }
+ ]
+ },
+ {
+ "name": "removeBreakpoint",
+ "description": "Removes JavaScript breakpoint.",
+ "parameters": [
+ {
+ "name": "breakpointId",
+ "$ref": "BreakpointId"
+ }
+ ]
+ },
+ {
+ "name": "restartFrame",
+ "description": "Restarts particular call frame from the beginning. The old, deprecated\nbehavior of `restartFrame` is to stay paused and allow further CDP commands\nafter a restart was scheduled. This can cause problems with restarting, so\nwe now continue execution immediatly after it has been scheduled until we\nreach the beginning of the restarted frame.\n\nTo stay back-wards compatible, `restartFrame` now expects a `mode`\nparameter to be present. If the `mode` parameter is missing, `restartFrame`\nerrors out.\n\nThe various return values are deprecated and `callFrames` is always empty.\nUse the call frames from the `Debugger#paused` events instead, that fires\nonce V8 pauses at the beginning of the restarted function.",
+ "parameters": [
+ {
+ "name": "callFrameId",
+ "description": "Call frame identifier to evaluate on.",
+ "$ref": "CallFrameId"
+ },
+ {
+ "name": "mode",
+ "description": "The `mode` parameter must be present and set to 'StepInto', otherwise\n`restartFrame` will error out.",
+ "experimental": true,
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "StepInto"
+ ]
+ }
+ ],
+ "returns": [
+ {
+ "name": "callFrames",
+ "description": "New stack trace.",
+ "deprecated": true,
+ "type": "array",
+ "items": {
+ "$ref": "CallFrame"
+ }
+ },
+ {
+ "name": "asyncStackTrace",
+ "description": "Async stack trace, if any.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "asyncStackTraceId",
+ "description": "Async stack trace, if any.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Runtime.StackTraceId"
+ }
+ ]
+ },
+ {
+ "name": "resume",
+ "description": "Resumes JavaScript execution.",
+ "parameters": [
+ {
+ "name": "terminateOnResume",
+ "description": "Set to true to terminate execution upon resuming execution. In contrast\nto Runtime.terminateExecution, this will allows to execute further\nJavaScript (i.e. via evaluation) until execution of the paused code\nis actually resumed, at which point termination is triggered.\nIf execution is currently not paused, this parameter has no effect.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "searchInContent",
+ "description": "Searches for given string in script content.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script to search in.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "query",
+ "description": "String to search for.",
+ "type": "string"
+ },
+ {
+ "name": "caseSensitive",
+ "description": "If true, search is case sensitive.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isRegex",
+ "description": "If true, treats string parameter as regex.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "List of search matches.",
+ "type": "array",
+ "items": {
+ "$ref": "SearchMatch"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setAsyncCallStackDepth",
+ "description": "Enables or disables async call stacks tracking.",
+ "parameters": [
+ {
+ "name": "maxDepth",
+ "description": "Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\ncall stacks (default).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setBlackboxPatterns",
+ "description": "Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in\nscripts with url matching one of the patterns. VM will try to leave blackboxed script by\nperforming 'step in' several times, finally resorting to 'step out' if unsuccessful.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "patterns",
+ "description": "Array of regexps that will be used to check script url for blackbox state.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setBlackboxedRanges",
+ "description": "Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted\nscripts by performing 'step in' several times, finally resorting to 'step out' if unsuccessful.\nPositions array contains positions where blackbox state is changed. First interval isn't\nblackboxed. Array should be sorted.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "positions",
+ "type": "array",
+ "items": {
+ "$ref": "ScriptPosition"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setBreakpoint",
+ "description": "Sets JavaScript breakpoint at a given location.",
+ "parameters": [
+ {
+ "name": "location",
+ "description": "Location to set breakpoint in.",
+ "$ref": "Location"
+ },
+ {
+ "name": "condition",
+ "description": "Expression to use as a breakpoint condition. When specified, debugger will only stop on the\nbreakpoint if this expression evaluates to true.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "breakpointId",
+ "description": "Id of the created breakpoint for further reference.",
+ "$ref": "BreakpointId"
+ },
+ {
+ "name": "actualLocation",
+ "description": "Location this breakpoint resolved into.",
+ "$ref": "Location"
+ }
+ ]
+ },
+ {
+ "name": "setInstrumentationBreakpoint",
+ "description": "Sets instrumentation breakpoint.",
+ "parameters": [
+ {
+ "name": "instrumentation",
+ "description": "Instrumentation name.",
+ "type": "string",
+ "enum": [
+ "beforeScriptExecution",
+ "beforeScriptWithSourceMapExecution"
+ ]
+ }
+ ],
+ "returns": [
+ {
+ "name": "breakpointId",
+ "description": "Id of the created breakpoint for further reference.",
+ "$ref": "BreakpointId"
+ }
+ ]
+ },
+ {
+ "name": "setBreakpointByUrl",
+ "description": "Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this\ncommand is issued, all existing parsed scripts will have breakpoints resolved and returned in\n`locations` property. Further matching script parsing will result in subsequent\n`breakpointResolved` events issued. This logical breakpoint will survive page reloads.",
+ "parameters": [
+ {
+ "name": "lineNumber",
+ "description": "Line number to set breakpoint at.",
+ "type": "integer"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resources to set breakpoint on.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "urlRegex",
+ "description": "Regex pattern for the URLs of the resources to set breakpoints on. Either `url` or\n`urlRegex` must be specified.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "scriptHash",
+ "description": "Script hash of the resources to set breakpoint on.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Offset in the line to set breakpoint at.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "condition",
+ "description": "Expression to use as a breakpoint condition. When specified, debugger will only stop on the\nbreakpoint if this expression evaluates to true.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "breakpointId",
+ "description": "Id of the created breakpoint for further reference.",
+ "$ref": "BreakpointId"
+ },
+ {
+ "name": "locations",
+ "description": "List of the locations this breakpoint resolved into upon addition.",
+ "type": "array",
+ "items": {
+ "$ref": "Location"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setBreakpointOnFunctionCall",
+ "description": "Sets JavaScript breakpoint before each call to the given function.\nIf another function was created from the same source as a given one,\ncalling it will also trigger the breakpoint.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Function object id.",
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "condition",
+ "description": "Expression to use as a breakpoint condition. When specified, debugger will\nstop on the breakpoint if this expression evaluates to true.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "breakpointId",
+ "description": "Id of the created breakpoint for further reference.",
+ "$ref": "BreakpointId"
+ }
+ ]
+ },
+ {
+ "name": "setBreakpointsActive",
+ "description": "Activates / deactivates all breakpoints on the page.",
+ "parameters": [
+ {
+ "name": "active",
+ "description": "New value for breakpoints active state.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setPauseOnExceptions",
+ "description": "Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions,\nor caught exceptions, no exceptions. Initial pause on exceptions state is `none`.",
+ "parameters": [
+ {
+ "name": "state",
+ "description": "Pause on exceptions mode.",
+ "type": "string",
+ "enum": [
+ "none",
+ "caught",
+ "uncaught",
+ "all"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setReturnValue",
+ "description": "Changes return value in top frame. Available only at return break position.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "newValue",
+ "description": "New return value.",
+ "$ref": "Runtime.CallArgument"
+ }
+ ]
+ },
+ {
+ "name": "setScriptSource",
+ "description": "Edits JavaScript source live.\n\nIn general, functions that are currently on the stack can not be edited with\na single exception: If the edited function is the top-most stack frame and\nthat is the only activation of that function on the stack. In this case\nthe live edit will be successful and a `Debugger.restartFrame` for the\ntop-most function is automatically triggered.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script to edit.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "scriptSource",
+ "description": "New content of the script.",
+ "type": "string"
+ },
+ {
+ "name": "dryRun",
+ "description": "If true the change will not actually be applied. Dry run may be used to get result\ndescription without actually modifying the code.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "allowTopFrameEditing",
+ "description": "If true, then `scriptSource` is allowed to change the function on top of the stack\nas long as the top-most stack frame is the only activation of that function.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "callFrames",
+ "description": "New stack trace in case editing has happened while VM was stopped.",
+ "deprecated": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CallFrame"
+ }
+ },
+ {
+ "name": "stackChanged",
+ "description": "Whether current call stack was modified after applying the changes.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "asyncStackTrace",
+ "description": "Async stack trace, if any.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "asyncStackTraceId",
+ "description": "Async stack trace, if any.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Runtime.StackTraceId"
+ },
+ {
+ "name": "status",
+ "description": "Whether the operation was successful or not. Only `Ok` denotes a\nsuccessful live edit while the other enum variants denote why\nthe live edit failed.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Ok",
+ "CompileError",
+ "BlockedByActiveGenerator",
+ "BlockedByActiveFunction",
+ "BlockedByTopLevelEsModuleChange"
+ ]
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details if any. Only present when `status` is `CompileError`.",
+ "optional": true,
+ "$ref": "Runtime.ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "setSkipAllPauses",
+ "description": "Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc).",
+ "parameters": [
+ {
+ "name": "skip",
+ "description": "New value for skip pauses state.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setVariableValue",
+ "description": "Changes value of variable in a callframe. Object-based scopes are not supported and must be\nmutated manually.",
+ "parameters": [
+ {
+ "name": "scopeNumber",
+ "description": "0-based number of scope as was listed in scope chain. Only 'local', 'closure' and 'catch'\nscope types are allowed. Other scopes could be manipulated manually.",
+ "type": "integer"
+ },
+ {
+ "name": "variableName",
+ "description": "Variable name.",
+ "type": "string"
+ },
+ {
+ "name": "newValue",
+ "description": "New variable value.",
+ "$ref": "Runtime.CallArgument"
+ },
+ {
+ "name": "callFrameId",
+ "description": "Id of callframe that holds variable.",
+ "$ref": "CallFrameId"
+ }
+ ]
+ },
+ {
+ "name": "stepInto",
+ "description": "Steps into the function call.",
+ "parameters": [
+ {
+ "name": "breakOnAsyncCall",
+ "description": "Debugger will pause on the execution of the first async task which was scheduled\nbefore next pause.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "skipList",
+ "description": "The skipList specifies location ranges that should be skipped on step into.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "LocationRange"
+ }
+ }
+ ]
+ },
+ {
+ "name": "stepOut",
+ "description": "Steps out of the function call."
+ },
+ {
+ "name": "stepOver",
+ "description": "Steps over the statement.",
+ "parameters": [
+ {
+ "name": "skipList",
+ "description": "The skipList specifies location ranges that should be skipped on step over.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "LocationRange"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "breakpointResolved",
+ "description": "Fired when breakpoint is resolved to an actual script and location.",
+ "parameters": [
+ {
+ "name": "breakpointId",
+ "description": "Breakpoint unique identifier.",
+ "$ref": "BreakpointId"
+ },
+ {
+ "name": "location",
+ "description": "Actual breakpoint location.",
+ "$ref": "Location"
+ }
+ ]
+ },
+ {
+ "name": "paused",
+ "description": "Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria.",
+ "parameters": [
+ {
+ "name": "callFrames",
+ "description": "Call stack the virtual machine stopped on.",
+ "type": "array",
+ "items": {
+ "$ref": "CallFrame"
+ }
+ },
+ {
+ "name": "reason",
+ "description": "Pause reason.",
+ "type": "string",
+ "enum": [
+ "ambiguous",
+ "assert",
+ "CSPViolation",
+ "debugCommand",
+ "DOM",
+ "EventListener",
+ "exception",
+ "instrumentation",
+ "OOM",
+ "other",
+ "promiseRejection",
+ "XHR",
+ "step"
+ ]
+ },
+ {
+ "name": "data",
+ "description": "Object containing break-specific auxiliary properties.",
+ "optional": true,
+ "type": "object"
+ },
+ {
+ "name": "hitBreakpoints",
+ "description": "Hit breakpoints IDs",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "asyncStackTrace",
+ "description": "Async stack trace, if any.",
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "asyncStackTraceId",
+ "description": "Async stack trace, if any.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Runtime.StackTraceId"
+ },
+ {
+ "name": "asyncCallStackTraceId",
+ "description": "Never present, will be removed.",
+ "experimental": true,
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Runtime.StackTraceId"
+ }
+ ]
+ },
+ {
+ "name": "resumed",
+ "description": "Fired when the virtual machine resumed execution."
+ },
+ {
+ "name": "scriptFailedToParse",
+ "description": "Fired when virtual machine fails to parse the script.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Identifier of the script parsed.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "url",
+ "description": "URL or name of the script parsed (if any).",
+ "type": "string"
+ },
+ {
+ "name": "startLine",
+ "description": "Line offset of the script within the resource with given URL (for script tags).",
+ "type": "integer"
+ },
+ {
+ "name": "startColumn",
+ "description": "Column offset of the script within the resource with given URL.",
+ "type": "integer"
+ },
+ {
+ "name": "endLine",
+ "description": "Last line of the script.",
+ "type": "integer"
+ },
+ {
+ "name": "endColumn",
+ "description": "Length of the last line of the script.",
+ "type": "integer"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Specifies script creation context.",
+ "$ref": "Runtime.ExecutionContextId"
+ },
+ {
+ "name": "hash",
+ "description": "Content hash of the script, SHA-256.",
+ "type": "string"
+ },
+ {
+ "name": "executionContextAuxData",
+ "description": "Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}",
+ "optional": true,
+ "type": "object"
+ },
+ {
+ "name": "sourceMapURL",
+ "description": "URL of source map associated with script (if any).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "hasSourceURL",
+ "description": "True, if this script has sourceURL.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isModule",
+ "description": "True, if this script is ES6 module.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "length",
+ "description": "This script length.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "stackTrace",
+ "description": "JavaScript top stack frame of where the script parsed event was triggered if available.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "codeOffset",
+ "description": "If the scriptLanguage is WebAssembly, the code section offset in the module.",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "scriptLanguage",
+ "description": "The language of the script.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Debugger.ScriptLanguage"
+ },
+ {
+ "name": "embedderName",
+ "description": "The name the embedder supplied for this script.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "scriptParsed",
+ "description": "Fired when virtual machine parses script. This event is also fired for all known and uncollected\nscripts upon enabling debugger.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Identifier of the script parsed.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "url",
+ "description": "URL or name of the script parsed (if any).",
+ "type": "string"
+ },
+ {
+ "name": "startLine",
+ "description": "Line offset of the script within the resource with given URL (for script tags).",
+ "type": "integer"
+ },
+ {
+ "name": "startColumn",
+ "description": "Column offset of the script within the resource with given URL.",
+ "type": "integer"
+ },
+ {
+ "name": "endLine",
+ "description": "Last line of the script.",
+ "type": "integer"
+ },
+ {
+ "name": "endColumn",
+ "description": "Length of the last line of the script.",
+ "type": "integer"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Specifies script creation context.",
+ "$ref": "Runtime.ExecutionContextId"
+ },
+ {
+ "name": "hash",
+ "description": "Content hash of the script, SHA-256.",
+ "type": "string"
+ },
+ {
+ "name": "executionContextAuxData",
+ "description": "Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}",
+ "optional": true,
+ "type": "object"
+ },
+ {
+ "name": "isLiveEdit",
+ "description": "True, if this script is generated as a result of the live edit operation.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sourceMapURL",
+ "description": "URL of source map associated with script (if any).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "hasSourceURL",
+ "description": "True, if this script has sourceURL.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isModule",
+ "description": "True, if this script is ES6 module.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "length",
+ "description": "This script length.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "stackTrace",
+ "description": "JavaScript top stack frame of where the script parsed event was triggered if available.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "codeOffset",
+ "description": "If the scriptLanguage is WebAssembly, the code section offset in the module.",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "scriptLanguage",
+ "description": "The language of the script.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Debugger.ScriptLanguage"
+ },
+ {
+ "name": "debugSymbols",
+ "description": "If the scriptLanguage is WebASsembly, the source of debug symbols for the module.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Debugger.DebugSymbols"
+ },
+ {
+ "name": "embedderName",
+ "description": "The name the embedder supplied for this script.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "HeapProfiler",
+ "experimental": true,
+ "dependencies": [
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "HeapSnapshotObjectId",
+ "description": "Heap snapshot object id.",
+ "type": "string"
+ },
+ {
+ "id": "SamplingHeapProfileNode",
+ "description": "Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "callFrame",
+ "description": "Function location.",
+ "$ref": "Runtime.CallFrame"
+ },
+ {
+ "name": "selfSize",
+ "description": "Allocations size in bytes for the node excluding children.",
+ "type": "number"
+ },
+ {
+ "name": "id",
+ "description": "Node id. Ids are unique across all profiles collected between startSampling and stopSampling.",
+ "type": "integer"
+ },
+ {
+ "name": "children",
+ "description": "Child nodes.",
+ "type": "array",
+ "items": {
+ "$ref": "SamplingHeapProfileNode"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SamplingHeapProfileSample",
+ "description": "A single sample from a sampling profile.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "size",
+ "description": "Allocation size in bytes attributed to the sample.",
+ "type": "number"
+ },
+ {
+ "name": "nodeId",
+ "description": "Id of the corresponding profile tree node.",
+ "type": "integer"
+ },
+ {
+ "name": "ordinal",
+ "description": "Time-ordered sample ordinal number. It is unique across all profiles retrieved\nbetween startSampling and stopSampling.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "SamplingHeapProfile",
+ "description": "Sampling profile.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "head",
+ "$ref": "SamplingHeapProfileNode"
+ },
+ {
+ "name": "samples",
+ "type": "array",
+ "items": {
+ "$ref": "SamplingHeapProfileSample"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "addInspectedHeapObject",
+ "description": "Enables console to refer to the node with given id via $x (see Command Line API for more details\n$x functions).",
+ "parameters": [
+ {
+ "name": "heapObjectId",
+ "description": "Heap snapshot object id to be accessible by means of $x command line API.",
+ "$ref": "HeapSnapshotObjectId"
+ }
+ ]
+ },
+ {
+ "name": "collectGarbage"
+ },
+ {
+ "name": "disable"
+ },
+ {
+ "name": "enable"
+ },
+ {
+ "name": "getHeapObjectId",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Identifier of the object to get heap object id for.",
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "heapSnapshotObjectId",
+ "description": "Id of the heap snapshot object corresponding to the passed remote object id.",
+ "$ref": "HeapSnapshotObjectId"
+ }
+ ]
+ },
+ {
+ "name": "getObjectByHeapObjectId",
+ "parameters": [
+ {
+ "name": "objectId",
+ "$ref": "HeapSnapshotObjectId"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release multiple objects.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Evaluation result.",
+ "$ref": "Runtime.RemoteObject"
+ }
+ ]
+ },
+ {
+ "name": "getSamplingProfile",
+ "returns": [
+ {
+ "name": "profile",
+ "description": "Return the sampling profile being collected.",
+ "$ref": "SamplingHeapProfile"
+ }
+ ]
+ },
+ {
+ "name": "startSampling",
+ "parameters": [
+ {
+ "name": "samplingInterval",
+ "description": "Average sample interval in bytes. Poisson distribution is used for the intervals. The\ndefault value is 32768 bytes.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "includeObjectsCollectedByMajorGC",
+ "description": "By default, the sampling heap profiler reports only objects which are\nstill alive when the profile is returned via getSamplingProfile or\nstopSampling, which is useful for determining what functions contribute\nthe most to steady-state memory usage. This flag instructs the sampling\nheap profiler to also include information about objects discarded by\nmajor GC, which will show which functions cause large temporary memory\nusage or long GC pauses.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeObjectsCollectedByMinorGC",
+ "description": "By default, the sampling heap profiler reports only objects which are\nstill alive when the profile is returned via getSamplingProfile or\nstopSampling, which is useful for determining what functions contribute\nthe most to steady-state memory usage. This flag instructs the sampling\nheap profiler to also include information about objects discarded by\nminor GC, which is useful when tuning a latency-sensitive application\nfor minimal GC activity.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "startTrackingHeapObjects",
+ "parameters": [
+ {
+ "name": "trackAllocations",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "stopSampling",
+ "returns": [
+ {
+ "name": "profile",
+ "description": "Recorded sampling heap profile.",
+ "$ref": "SamplingHeapProfile"
+ }
+ ]
+ },
+ {
+ "name": "stopTrackingHeapObjects",
+ "parameters": [
+ {
+ "name": "reportProgress",
+ "description": "If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken\nwhen the tracking is stopped.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "treatGlobalObjectsAsRoots",
+ "description": "Deprecated in favor of `exposeInternals`.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "captureNumericValue",
+ "description": "If true, numerical values are included in the snapshot",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "exposeInternals",
+ "description": "If true, exposes internals of the snapshot.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "takeHeapSnapshot",
+ "parameters": [
+ {
+ "name": "reportProgress",
+ "description": "If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "treatGlobalObjectsAsRoots",
+ "description": "If true, a raw snapshot without artificial roots will be generated.\nDeprecated in favor of `exposeInternals`.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "captureNumericValue",
+ "description": "If true, numerical values are included in the snapshot",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "exposeInternals",
+ "description": "If true, exposes internals of the snapshot.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "addHeapSnapshotChunk",
+ "parameters": [
+ {
+ "name": "chunk",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "heapStatsUpdate",
+ "description": "If heap objects tracking has been started then backend may send update for one or more fragments",
+ "parameters": [
+ {
+ "name": "statsUpdate",
+ "description": "An array of triplets. Each triplet describes a fragment. The first integer is the fragment\nindex, the second integer is a total count of objects for the fragment, the third integer is\na total size of the objects for the fragment.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "name": "lastSeenObjectId",
+ "description": "If heap objects tracking has been started then backend regularly sends a current value for last\nseen object id and corresponding timestamp. If the were changes in the heap since last event\nthen one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event.",
+ "parameters": [
+ {
+ "name": "lastSeenObjectId",
+ "type": "integer"
+ },
+ {
+ "name": "timestamp",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "reportHeapSnapshotProgress",
+ "parameters": [
+ {
+ "name": "done",
+ "type": "integer"
+ },
+ {
+ "name": "total",
+ "type": "integer"
+ },
+ {
+ "name": "finished",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "resetProfiles"
+ }
+ ]
+ },
+ {
+ "domain": "Profiler",
+ "dependencies": [
+ "Runtime",
+ "Debugger"
+ ],
+ "types": [
+ {
+ "id": "ProfileNode",
+ "description": "Profile node. Holds callsite information, execution statistics and child nodes.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "Unique id of the node.",
+ "type": "integer"
+ },
+ {
+ "name": "callFrame",
+ "description": "Function location.",
+ "$ref": "Runtime.CallFrame"
+ },
+ {
+ "name": "hitCount",
+ "description": "Number of samples where this node was on top of the call stack.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "children",
+ "description": "Child node ids.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "deoptReason",
+ "description": "The reason of being not optimized. The function may be deoptimized or marked as don't\noptimize.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "positionTicks",
+ "description": "An array of source position ticks.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "PositionTickInfo"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Profile",
+ "description": "Profile.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodes",
+ "description": "The list of profile nodes. First item is the root node.",
+ "type": "array",
+ "items": {
+ "$ref": "ProfileNode"
+ }
+ },
+ {
+ "name": "startTime",
+ "description": "Profiling start timestamp in microseconds.",
+ "type": "number"
+ },
+ {
+ "name": "endTime",
+ "description": "Profiling end timestamp in microseconds.",
+ "type": "number"
+ },
+ {
+ "name": "samples",
+ "description": "Ids of samples top nodes.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "timeDeltas",
+ "description": "Time intervals between adjacent samples in microseconds. The first delta is relative to the\nprofile startTime.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "id": "PositionTickInfo",
+ "description": "Specifies a number of samples attributed to a certain source position.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "line",
+ "description": "Source line number (1-based).",
+ "type": "integer"
+ },
+ {
+ "name": "ticks",
+ "description": "Number of samples attributed to the source line.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "CoverageRange",
+ "description": "Coverage data for a source range.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "startOffset",
+ "description": "JavaScript script source offset for the range start.",
+ "type": "integer"
+ },
+ {
+ "name": "endOffset",
+ "description": "JavaScript script source offset for the range end.",
+ "type": "integer"
+ },
+ {
+ "name": "count",
+ "description": "Collected execution count of the source range.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "FunctionCoverage",
+ "description": "Coverage data for a JavaScript function.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "functionName",
+ "description": "JavaScript function name.",
+ "type": "string"
+ },
+ {
+ "name": "ranges",
+ "description": "Source ranges inside the function with coverage data.",
+ "type": "array",
+ "items": {
+ "$ref": "CoverageRange"
+ }
+ },
+ {
+ "name": "isBlockCoverage",
+ "description": "Whether coverage data for this function has block granularity.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "ScriptCoverage",
+ "description": "Coverage data for a JavaScript script.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "description": "JavaScript script id.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "url",
+ "description": "JavaScript script name or url.",
+ "type": "string"
+ },
+ {
+ "name": "functions",
+ "description": "Functions contained in the script that has coverage data.",
+ "type": "array",
+ "items": {
+ "$ref": "FunctionCoverage"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable"
+ },
+ {
+ "name": "enable"
+ },
+ {
+ "name": "getBestEffortCoverage",
+ "description": "Collect coverage data for the current isolate. The coverage data may be incomplete due to\ngarbage collection.",
+ "returns": [
+ {
+ "name": "result",
+ "description": "Coverage data for the current isolate.",
+ "type": "array",
+ "items": {
+ "$ref": "ScriptCoverage"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setSamplingInterval",
+ "description": "Changes CPU profiler sampling interval. Must be called before CPU profiles recording started.",
+ "parameters": [
+ {
+ "name": "interval",
+ "description": "New sampling interval in microseconds.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "start"
+ },
+ {
+ "name": "startPreciseCoverage",
+ "description": "Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code\ncoverage may be incomplete. Enabling prevents running optimized code and resets execution\ncounters.",
+ "parameters": [
+ {
+ "name": "callCount",
+ "description": "Collect accurate call counts beyond simple 'covered' or 'not covered'.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "detailed",
+ "description": "Collect block-based coverage.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "allowTriggeredUpdates",
+ "description": "Allow the backend to send updates on its own initiative",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "timestamp",
+ "description": "Monotonically increasing time (in seconds) when the coverage update was taken in the backend.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "stop",
+ "returns": [
+ {
+ "name": "profile",
+ "description": "Recorded profile.",
+ "$ref": "Profile"
+ }
+ ]
+ },
+ {
+ "name": "stopPreciseCoverage",
+ "description": "Disable precise code coverage. Disabling releases unnecessary execution count records and allows\nexecuting optimized code."
+ },
+ {
+ "name": "takePreciseCoverage",
+ "description": "Collect coverage data for the current isolate, and resets execution counters. Precise code\ncoverage needs to have started.",
+ "returns": [
+ {
+ "name": "result",
+ "description": "Coverage data for the current isolate.",
+ "type": "array",
+ "items": {
+ "$ref": "ScriptCoverage"
+ }
+ },
+ {
+ "name": "timestamp",
+ "description": "Monotonically increasing time (in seconds) when the coverage update was taken in the backend.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "consoleProfileFinished",
+ "parameters": [
+ {
+ "name": "id",
+ "type": "string"
+ },
+ {
+ "name": "location",
+ "description": "Location of console.profileEnd().",
+ "$ref": "Debugger.Location"
+ },
+ {
+ "name": "profile",
+ "$ref": "Profile"
+ },
+ {
+ "name": "title",
+ "description": "Profile title passed as an argument to console.profile().",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "consoleProfileStarted",
+ "description": "Sent when new profile recording is started using console.profile() call.",
+ "parameters": [
+ {
+ "name": "id",
+ "type": "string"
+ },
+ {
+ "name": "location",
+ "description": "Location of console.profile().",
+ "$ref": "Debugger.Location"
+ },
+ {
+ "name": "title",
+ "description": "Profile title passed as an argument to console.profile().",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "preciseCoverageDeltaUpdate",
+ "description": "Reports coverage delta since the last poll (either from an event like this, or from\n`takePreciseCoverage` for the current isolate. May only be sent if precise code\ncoverage has been started. This event can be trigged by the embedder to, for example,\ntrigger collection of coverage data immediately at a certain point in time.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "timestamp",
+ "description": "Monotonically increasing time (in seconds) when the coverage update was taken in the backend.",
+ "type": "number"
+ },
+ {
+ "name": "occasion",
+ "description": "Identifier for distinguishing coverage events.",
+ "type": "string"
+ },
+ {
+ "name": "result",
+ "description": "Coverage data for the current isolate.",
+ "type": "array",
+ "items": {
+ "$ref": "ScriptCoverage"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Runtime",
+ "description": "Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects.\nEvaluation results are returned as mirror object that expose object type, string representation\nand unique identifier that can be used for further object reference. Original objects are\nmaintained in memory unless they are either explicitly released or are released along with the\nother objects in their object group.",
+ "types": [
+ {
+ "id": "ScriptId",
+ "description": "Unique script identifier.",
+ "type": "string"
+ },
+ {
+ "id": "SerializationOptions",
+ "description": "Represents options for serialization. Overrides `generatePreview`, `returnByValue` and\n`generateWebDriverValue`.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "serialization",
+ "type": "string",
+ "enum": [
+ "deep",
+ "json",
+ "idOnly"
+ ]
+ },
+ {
+ "name": "maxDepth",
+ "description": "Deep serialization depth. Default is full depth. Respected only in `deep` serialization mode.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "additionalParameters",
+ "description": "Embedder-specific parameters. For example if connected to V8 in Chrome these control DOM\nserialization via `maxNodeDepth: integer` and `includeShadowTree: \"none\" | \"open\" | \"all\"`.\nValues can be only of type string or integer.",
+ "optional": true,
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "id": "DeepSerializedValue",
+ "description": "Represents deep serialized value.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "type": "string",
+ "enum": [
+ "undefined",
+ "null",
+ "string",
+ "number",
+ "boolean",
+ "bigint",
+ "regexp",
+ "date",
+ "symbol",
+ "array",
+ "object",
+ "function",
+ "map",
+ "set",
+ "weakmap",
+ "weakset",
+ "error",
+ "proxy",
+ "promise",
+ "typedarray",
+ "arraybuffer",
+ "node",
+ "window"
+ ]
+ },
+ {
+ "name": "value",
+ "optional": true,
+ "type": "any"
+ },
+ {
+ "name": "objectId",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "weakLocalObjectReference",
+ "description": "Set if value reference met more then once during serialization. In such\ncase, value is provided only to one of the serialized values. Unique\nper value in the scope of one CDP call.",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "RemoteObjectId",
+ "description": "Unique object identifier.",
+ "type": "string"
+ },
+ {
+ "id": "UnserializableValue",
+ "description": "Primitive value which cannot be JSON-stringified. Includes values `-0`, `NaN`, `Infinity`,\n`-Infinity`, and bigint literals.",
+ "type": "string"
+ },
+ {
+ "id": "RemoteObject",
+ "description": "Mirror object referencing original JavaScript object.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Object type.",
+ "type": "string",
+ "enum": [
+ "object",
+ "function",
+ "undefined",
+ "string",
+ "number",
+ "boolean",
+ "symbol",
+ "bigint"
+ ]
+ },
+ {
+ "name": "subtype",
+ "description": "Object subtype hint. Specified for `object` type values only.\nNOTE: If you change anything here, make sure to also update\n`subtype` in `ObjectPreview` and `PropertyPreview` below.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "array",
+ "null",
+ "node",
+ "regexp",
+ "date",
+ "map",
+ "set",
+ "weakmap",
+ "weakset",
+ "iterator",
+ "generator",
+ "error",
+ "proxy",
+ "promise",
+ "typedarray",
+ "arraybuffer",
+ "dataview",
+ "webassemblymemory",
+ "wasmvalue"
+ ]
+ },
+ {
+ "name": "className",
+ "description": "Object class (constructor) name. Specified for `object` type values only.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Remote object value in case of primitive values or JSON values (if it was requested).",
+ "optional": true,
+ "type": "any"
+ },
+ {
+ "name": "unserializableValue",
+ "description": "Primitive value which can not be JSON-stringified does not have `value`, but gets this\nproperty.",
+ "optional": true,
+ "$ref": "UnserializableValue"
+ },
+ {
+ "name": "description",
+ "description": "String representation of the object.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "webDriverValue",
+ "description": "Deprecated. Use `deepSerializedValue` instead. WebDriver BiDi representation of the value.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "DeepSerializedValue"
+ },
+ {
+ "name": "deepSerializedValue",
+ "description": "Deep serialized value.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "DeepSerializedValue"
+ },
+ {
+ "name": "objectId",
+ "description": "Unique object identifier (for non-primitive values).",
+ "optional": true,
+ "$ref": "RemoteObjectId"
+ },
+ {
+ "name": "preview",
+ "description": "Preview containing abbreviated property values. Specified for `object` type values only.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "ObjectPreview"
+ },
+ {
+ "name": "customPreview",
+ "experimental": true,
+ "optional": true,
+ "$ref": "CustomPreview"
+ }
+ ]
+ },
+ {
+ "id": "CustomPreview",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "header",
+ "description": "The JSON-stringified result of formatter.header(object, config) call.\nIt contains json ML array that represents RemoteObject.",
+ "type": "string"
+ },
+ {
+ "name": "bodyGetterId",
+ "description": "If formatter returns true as a result of formatter.hasBody call then bodyGetterId will\ncontain RemoteObjectId for the function that returns result of formatter.body(object, config) call.\nThe result value is json ML array.",
+ "optional": true,
+ "$ref": "RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "id": "ObjectPreview",
+ "description": "Object containing abbreviated remote object value.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Object type.",
+ "type": "string",
+ "enum": [
+ "object",
+ "function",
+ "undefined",
+ "string",
+ "number",
+ "boolean",
+ "symbol",
+ "bigint"
+ ]
+ },
+ {
+ "name": "subtype",
+ "description": "Object subtype hint. Specified for `object` type values only.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "array",
+ "null",
+ "node",
+ "regexp",
+ "date",
+ "map",
+ "set",
+ "weakmap",
+ "weakset",
+ "iterator",
+ "generator",
+ "error",
+ "proxy",
+ "promise",
+ "typedarray",
+ "arraybuffer",
+ "dataview",
+ "webassemblymemory",
+ "wasmvalue"
+ ]
+ },
+ {
+ "name": "description",
+ "description": "String representation of the object.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "overflow",
+ "description": "True iff some of the properties or entries of the original object did not fit.",
+ "type": "boolean"
+ },
+ {
+ "name": "properties",
+ "description": "List of the properties.",
+ "type": "array",
+ "items": {
+ "$ref": "PropertyPreview"
+ }
+ },
+ {
+ "name": "entries",
+ "description": "List of the entries. Specified for `map` and `set` subtype values only.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "EntryPreview"
+ }
+ }
+ ]
+ },
+ {
+ "id": "PropertyPreview",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Property name.",
+ "type": "string"
+ },
+ {
+ "name": "type",
+ "description": "Object type. Accessor means that the property itself is an accessor property.",
+ "type": "string",
+ "enum": [
+ "object",
+ "function",
+ "undefined",
+ "string",
+ "number",
+ "boolean",
+ "symbol",
+ "accessor",
+ "bigint"
+ ]
+ },
+ {
+ "name": "value",
+ "description": "User-friendly property value string.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "valuePreview",
+ "description": "Nested value preview.",
+ "optional": true,
+ "$ref": "ObjectPreview"
+ },
+ {
+ "name": "subtype",
+ "description": "Object subtype hint. Specified for `object` type values only.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "array",
+ "null",
+ "node",
+ "regexp",
+ "date",
+ "map",
+ "set",
+ "weakmap",
+ "weakset",
+ "iterator",
+ "generator",
+ "error",
+ "proxy",
+ "promise",
+ "typedarray",
+ "arraybuffer",
+ "dataview",
+ "webassemblymemory",
+ "wasmvalue"
+ ]
+ }
+ ]
+ },
+ {
+ "id": "EntryPreview",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "description": "Preview of the key. Specified for map-like collection entries.",
+ "optional": true,
+ "$ref": "ObjectPreview"
+ },
+ {
+ "name": "value",
+ "description": "Preview of the value.",
+ "$ref": "ObjectPreview"
+ }
+ ]
+ },
+ {
+ "id": "PropertyDescriptor",
+ "description": "Object property descriptor.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Property name or symbol description.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "The value associated with the property.",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "writable",
+ "description": "True if the value associated with the property may be changed (data descriptors only).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "get",
+ "description": "A function which serves as a getter for the property, or `undefined` if there is no getter\n(accessor descriptors only).",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "set",
+ "description": "A function which serves as a setter for the property, or `undefined` if there is no setter\n(accessor descriptors only).",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "configurable",
+ "description": "True if the type of this property descriptor may be changed and if the property may be\ndeleted from the corresponding object.",
+ "type": "boolean"
+ },
+ {
+ "name": "enumerable",
+ "description": "True if this property shows up during enumeration of the properties on the corresponding\nobject.",
+ "type": "boolean"
+ },
+ {
+ "name": "wasThrown",
+ "description": "True if the result was thrown during the evaluation.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isOwn",
+ "description": "True if the property is owned for the object.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "symbol",
+ "description": "Property symbol object, if the property is of the `symbol` type.",
+ "optional": true,
+ "$ref": "RemoteObject"
+ }
+ ]
+ },
+ {
+ "id": "InternalPropertyDescriptor",
+ "description": "Object internal property descriptor. This property isn't normally visible in JavaScript code.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Conventional property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "The value associated with the property.",
+ "optional": true,
+ "$ref": "RemoteObject"
+ }
+ ]
+ },
+ {
+ "id": "PrivatePropertyDescriptor",
+ "description": "Object private field descriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Private property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "The value associated with the private property.",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "get",
+ "description": "A function which serves as a getter for the private property,\nor `undefined` if there is no getter (accessor descriptors only).",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "set",
+ "description": "A function which serves as a setter for the private property,\nor `undefined` if there is no setter (accessor descriptors only).",
+ "optional": true,
+ "$ref": "RemoteObject"
+ }
+ ]
+ },
+ {
+ "id": "CallArgument",
+ "description": "Represents function call argument. Either remote object id `objectId`, primitive `value`,\nunserializable primitive value or neither of (for undefined) them should be specified.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "value",
+ "description": "Primitive value or serializable javascript object.",
+ "optional": true,
+ "type": "any"
+ },
+ {
+ "name": "unserializableValue",
+ "description": "Primitive value which can not be JSON-stringified.",
+ "optional": true,
+ "$ref": "UnserializableValue"
+ },
+ {
+ "name": "objectId",
+ "description": "Remote object handle.",
+ "optional": true,
+ "$ref": "RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "id": "ExecutionContextId",
+ "description": "Id of an execution context.",
+ "type": "integer"
+ },
+ {
+ "id": "ExecutionContextDescription",
+ "description": "Description of an isolated world.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "Unique id of the execution context. It can be used to specify in which execution context\nscript evaluation should be performed.",
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "origin",
+ "description": "Execution context origin.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Human readable name describing given context.",
+ "type": "string"
+ },
+ {
+ "name": "uniqueId",
+ "description": "A system-unique execution context identifier. Unlike the id, this is unique across\nmultiple processes, so can be reliably used to identify specific context while backend\nperforms a cross-process navigation.",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "name": "auxData",
+ "description": "Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}",
+ "optional": true,
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "id": "ExceptionDetails",
+ "description": "Detailed information about exception (or error) that was thrown during script compilation or\nexecution.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "exceptionId",
+ "description": "Exception id.",
+ "type": "integer"
+ },
+ {
+ "name": "text",
+ "description": "Exception text, which should be used together with exception object when available.",
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Line number of the exception location (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Column number of the exception location (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "scriptId",
+ "description": "Script ID of the exception location.",
+ "optional": true,
+ "$ref": "ScriptId"
+ },
+ {
+ "name": "url",
+ "description": "URL of the exception location, to be used when the script was not reported.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "stackTrace",
+ "description": "JavaScript stack trace if available.",
+ "optional": true,
+ "$ref": "StackTrace"
+ },
+ {
+ "name": "exception",
+ "description": "Exception object if available.",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Identifier of the context where exception happened.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "exceptionMetaData",
+ "description": "Dictionary with entries of meta data that the client associated\nwith this exception, such as information about associated network\nrequests, etc.",
+ "experimental": true,
+ "optional": true,
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "id": "Timestamp",
+ "description": "Number of milliseconds since epoch.",
+ "type": "number"
+ },
+ {
+ "id": "TimeDelta",
+ "description": "Number of milliseconds.",
+ "type": "number"
+ },
+ {
+ "id": "CallFrame",
+ "description": "Stack entry for runtime errors and assertions.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "functionName",
+ "description": "JavaScript function name.",
+ "type": "string"
+ },
+ {
+ "name": "scriptId",
+ "description": "JavaScript script id.",
+ "$ref": "ScriptId"
+ },
+ {
+ "name": "url",
+ "description": "JavaScript script name or url.",
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "description": "JavaScript script line number (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "description": "JavaScript script column number (0-based).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "StackTrace",
+ "description": "Call frames for assertions or error messages.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "description",
+ "description": "String label of this stack trace. For async traces this may be a name of the function that\ninitiated the async call.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "callFrames",
+ "description": "JavaScript function name.",
+ "type": "array",
+ "items": {
+ "$ref": "CallFrame"
+ }
+ },
+ {
+ "name": "parent",
+ "description": "Asynchronous JavaScript stack trace that preceded this stack, if available.",
+ "optional": true,
+ "$ref": "StackTrace"
+ },
+ {
+ "name": "parentId",
+ "description": "Asynchronous JavaScript stack trace that preceded this stack, if available.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "StackTraceId"
+ }
+ ]
+ },
+ {
+ "id": "UniqueDebuggerId",
+ "description": "Unique identifier of current debugger.",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "StackTraceId",
+ "description": "If `debuggerId` is set stack trace comes from another debugger and can be resolved there. This\nallows to track cross-debugger calls. See `Runtime.StackTrace` and `Debugger.paused` for usages.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "type": "string"
+ },
+ {
+ "name": "debuggerId",
+ "optional": true,
+ "$ref": "UniqueDebuggerId"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "awaitPromise",
+ "description": "Add handler to promise with given promise object id.",
+ "parameters": [
+ {
+ "name": "promiseObjectId",
+ "description": "Identifier of the promise.",
+ "$ref": "RemoteObjectId"
+ },
+ {
+ "name": "returnByValue",
+ "description": "Whether the result is expected to be a JSON object that should be sent by value.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the result.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Promise result. Will contain rejected value if promise was rejected.",
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details if stack strace is available.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "callFunctionOn",
+ "description": "Calls function with given declaration on the given object. Object group of the result is\ninherited from the target object.",
+ "parameters": [
+ {
+ "name": "functionDeclaration",
+ "description": "Declaration of the function to call.",
+ "type": "string"
+ },
+ {
+ "name": "objectId",
+ "description": "Identifier of the object to call function on. Either objectId or executionContextId should\nbe specified.",
+ "optional": true,
+ "$ref": "RemoteObjectId"
+ },
+ {
+ "name": "arguments",
+ "description": "Call arguments. All call arguments must belong to the same JavaScript world as the target\nobject.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CallArgument"
+ }
+ },
+ {
+ "name": "silent",
+ "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "returnByValue",
+ "description": "Whether the result is expected to be a JSON object which should be sent by value.\nCan be overriden by `serializationOptions`.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the result.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "userGesture",
+ "description": "Whether execution should be treated as initiated by user in the UI.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "awaitPromise",
+ "description": "Whether execution should `await` for resulting value and return once awaited promise is\nresolved.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Specifies execution context which global object will be used to call function on. Either\nexecutionContextId or objectId should be specified.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release multiple objects. If objectGroup is not\nspecified and objectId is, objectGroup will be inherited from object.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "throwOnSideEffect",
+ "description": "Whether to throw an exception if side effect cannot be ruled out during evaluation.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "uniqueContextId",
+ "description": "An alternative way to specify the execution context to call function on.\nCompared to contextId that may be reused across processes, this is guaranteed to be\nsystem-unique, so it can be used to prevent accidental function call\nin context different than intended (e.g. as a result of navigation across process\nboundaries).\nThis is mutually exclusive with `executionContextId`.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "generateWebDriverValue",
+ "description": "Deprecated. Use `serializationOptions: {serialization:\"deep\"}` instead.\nWhether the result should contain `webDriverValue`, serialized according to\nhttps://w3c.github.io/webdriver-bidi. This is mutually exclusive with `returnByValue`, but\nresulting `objectId` is still provided.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "serializationOptions",
+ "description": "Specifies the result serialization. If provided, overrides\n`generatePreview`, `returnByValue` and `generateWebDriverValue`.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "SerializationOptions"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Call result.",
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "compileScript",
+ "description": "Compiles expression.",
+ "parameters": [
+ {
+ "name": "expression",
+ "description": "Expression to compile.",
+ "type": "string"
+ },
+ {
+ "name": "sourceURL",
+ "description": "Source url to be set for the script.",
+ "type": "string"
+ },
+ {
+ "name": "persistScript",
+ "description": "Specifies whether the compiled script should be persisted.",
+ "type": "boolean"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Specifies in which execution context to perform script run. If the parameter is omitted the\nevaluation will be performed in the context of the inspected page.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script.",
+ "optional": true,
+ "$ref": "ScriptId"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables reporting of execution contexts creation."
+ },
+ {
+ "name": "discardConsoleEntries",
+ "description": "Discards collected exceptions and console API calls."
+ },
+ {
+ "name": "enable",
+ "description": "Enables reporting of execution contexts creation by means of `executionContextCreated` event.\nWhen the reporting gets enabled the event will be sent immediately for each existing execution\ncontext."
+ },
+ {
+ "name": "evaluate",
+ "description": "Evaluates expression on global object.",
+ "parameters": [
+ {
+ "name": "expression",
+ "description": "Expression to evaluate.",
+ "type": "string"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release multiple objects.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "includeCommandLineAPI",
+ "description": "Determines whether Command Line API should be available during the evaluation.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "silent",
+ "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "contextId",
+ "description": "Specifies in which execution context to perform evaluation. If the parameter is omitted the\nevaluation will be performed in the context of the inspected page.\nThis is mutually exclusive with `uniqueContextId`, which offers an\nalternative way to identify the execution context that is more reliable\nin a multi-process environment.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "returnByValue",
+ "description": "Whether the result is expected to be a JSON object that should be sent by value.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the result.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "userGesture",
+ "description": "Whether execution should be treated as initiated by user in the UI.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "awaitPromise",
+ "description": "Whether execution should `await` for resulting value and return once awaited promise is\nresolved.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "throwOnSideEffect",
+ "description": "Whether to throw an exception if side effect cannot be ruled out during evaluation.\nThis implies `disableBreaks` below.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "timeout",
+ "description": "Terminate execution after timing out (number of milliseconds).",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TimeDelta"
+ },
+ {
+ "name": "disableBreaks",
+ "description": "Disable breakpoints during execution.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "replMode",
+ "description": "Setting this flag to true enables `let` re-declaration and top-level `await`.\nNote that `let` variables can only be re-declared if they originate from\n`replMode` themselves.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "allowUnsafeEvalBlockedByCSP",
+ "description": "The Content Security Policy (CSP) for the target might block 'unsafe-eval'\nwhich includes eval(), Function(), setTimeout() and setInterval()\nwhen called with non-callable arguments. This flag bypasses CSP for this\nevaluation and allows unsafe-eval. Defaults to true.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "uniqueContextId",
+ "description": "An alternative way to specify the execution context to evaluate in.\nCompared to contextId that may be reused across processes, this is guaranteed to be\nsystem-unique, so it can be used to prevent accidental evaluation of the expression\nin context different than intended (e.g. as a result of navigation across process\nboundaries).\nThis is mutually exclusive with `contextId`.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "generateWebDriverValue",
+ "description": "Deprecated. Use `serializationOptions: {serialization:\"deep\"}` instead.\nWhether the result should contain `webDriverValue`, serialized\naccording to\nhttps://w3c.github.io/webdriver-bidi. This is mutually exclusive with `returnByValue`, but\nresulting `objectId` is still provided.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "serializationOptions",
+ "description": "Specifies the result serialization. If provided, overrides\n`generatePreview`, `returnByValue` and `generateWebDriverValue`.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "SerializationOptions"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Evaluation result.",
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "getIsolateId",
+ "description": "Returns the isolate id.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "id",
+ "description": "The isolate id.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getHeapUsage",
+ "description": "Returns the JavaScript heap usage.\nIt is the total usage of the corresponding isolate not scoped to a particular Runtime.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "usedSize",
+ "description": "Used heap size in bytes.",
+ "type": "number"
+ },
+ {
+ "name": "totalSize",
+ "description": "Allocated heap size in bytes.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "getProperties",
+ "description": "Returns properties of a given object. Object group of the result is inherited from the target\nobject.",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Identifier of the object to return properties for.",
+ "$ref": "RemoteObjectId"
+ },
+ {
+ "name": "ownProperties",
+ "description": "If true, returns properties belonging only to the element itself, not to its prototype\nchain.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "accessorPropertiesOnly",
+ "description": "If true, returns accessor properties (with getter/setter) only; internal properties are not\nreturned either.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the results.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "nonIndexedPropertiesOnly",
+ "description": "If true, returns non-indexed properties only.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Object properties.",
+ "type": "array",
+ "items": {
+ "$ref": "PropertyDescriptor"
+ }
+ },
+ {
+ "name": "internalProperties",
+ "description": "Internal object properties (only of the element itself).",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "InternalPropertyDescriptor"
+ }
+ },
+ {
+ "name": "privateProperties",
+ "description": "Object private properties.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "PrivatePropertyDescriptor"
+ }
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "globalLexicalScopeNames",
+ "description": "Returns all let, const and class variables from global scope.",
+ "parameters": [
+ {
+ "name": "executionContextId",
+ "description": "Specifies in which execution context to lookup global scope variables.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "names",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "queryObjects",
+ "parameters": [
+ {
+ "name": "prototypeObjectId",
+ "description": "Identifier of the prototype to return objects for.",
+ "$ref": "RemoteObjectId"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release the results.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "objects",
+ "description": "Array with objects.",
+ "$ref": "RemoteObject"
+ }
+ ]
+ },
+ {
+ "name": "releaseObject",
+ "description": "Releases remote object with given id.",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Identifier of the object to release.",
+ "$ref": "RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "name": "releaseObjectGroup",
+ "description": "Releases all remote objects that belong to a given group.",
+ "parameters": [
+ {
+ "name": "objectGroup",
+ "description": "Symbolic object group name.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "runIfWaitingForDebugger",
+ "description": "Tells inspected instance to run if it was waiting for debugger to attach."
+ },
+ {
+ "name": "runScript",
+ "description": "Runs script with given id in a given context.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script to run.",
+ "$ref": "ScriptId"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Specifies in which execution context to perform script run. If the parameter is omitted the\nevaluation will be performed in the context of the inspected page.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release multiple objects.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "silent",
+ "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeCommandLineAPI",
+ "description": "Determines whether Command Line API should be available during the evaluation.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "returnByValue",
+ "description": "Whether the result is expected to be a JSON object which should be sent by value.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the result.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "awaitPromise",
+ "description": "Whether execution should `await` for resulting value and return once awaited promise is\nresolved.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Run result.",
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "setAsyncCallStackDepth",
+ "description": "Enables or disables async call stacks tracking.",
+ "redirect": "Debugger",
+ "parameters": [
+ {
+ "name": "maxDepth",
+ "description": "Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\ncall stacks (default).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setCustomObjectFormatterEnabled",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setMaxCallStackSizeToCapture",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "size",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "terminateExecution",
+ "description": "Terminate current or next JavaScript execution.\nWill cancel the termination when the outer-most script execution ends.",
+ "experimental": true
+ },
+ {
+ "name": "addBinding",
+ "description": "If executionContextId is empty, adds binding with the given name on the\nglobal objects of all inspected contexts, including those created later,\nbindings survive reloads.\nBinding function takes exactly one argument, this argument should be string,\nin case of any other input, function throws an exception.\nEach binding function call produces Runtime.bindingCalled notification.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "executionContextId",
+ "description": "If specified, the binding would only be exposed to the specified\nexecution context. If omitted and `executionContextName` is not set,\nthe binding is exposed to all execution contexts of the target.\nThis parameter is mutually exclusive with `executionContextName`.\nDeprecated in favor of `executionContextName` due to an unclear use case\nand bugs in implementation (crbug.com/1169639). `executionContextId` will be\nremoved in the future.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "executionContextName",
+ "description": "If specified, the binding is exposed to the executionContext with\nmatching name, even for contexts created after the binding is added.\nSee also `ExecutionContext.name` and `worldName` parameter to\n`Page.addScriptToEvaluateOnNewDocument`.\nThis parameter is mutually exclusive with `executionContextId`.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeBinding",
+ "description": "This method does not remove binding function from global object but\nunsubscribes current runtime agent from Runtime.bindingCalled notifications.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getExceptionDetails",
+ "description": "This method tries to lookup and populate exception details for a\nJavaScript Error object.\nNote that the stackTrace portion of the resulting exceptionDetails will\nonly be populated if the Runtime domain was enabled at the time when the\nError was thrown.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "errorObjectId",
+ "description": "The error object for which to resolve the exception details.",
+ "$ref": "RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "exceptionDetails",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "bindingCalled",
+ "description": "Notification is issued every time when binding is called.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "payload",
+ "type": "string"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Identifier of the context where the call was made.",
+ "$ref": "ExecutionContextId"
+ }
+ ]
+ },
+ {
+ "name": "consoleAPICalled",
+ "description": "Issued when console API was called.",
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the call.",
+ "type": "string",
+ "enum": [
+ "log",
+ "debug",
+ "info",
+ "error",
+ "warning",
+ "dir",
+ "dirxml",
+ "table",
+ "trace",
+ "clear",
+ "startGroup",
+ "startGroupCollapsed",
+ "endGroup",
+ "assert",
+ "profile",
+ "profileEnd",
+ "count",
+ "timeEnd"
+ ]
+ },
+ {
+ "name": "args",
+ "description": "Call arguments.",
+ "type": "array",
+ "items": {
+ "$ref": "RemoteObject"
+ }
+ },
+ {
+ "name": "executionContextId",
+ "description": "Identifier of the context where the call was made.",
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Call timestamp.",
+ "$ref": "Timestamp"
+ },
+ {
+ "name": "stackTrace",
+ "description": "Stack trace captured when the call was made. The async stack chain is automatically reported for\nthe following call types: `assert`, `error`, `trace`, `warning`. For other types the async call\nchain can be retrieved using `Debugger.getStackTrace` and `stackTrace.parentId` field.",
+ "optional": true,
+ "$ref": "StackTrace"
+ },
+ {
+ "name": "context",
+ "description": "Console context descriptor for calls on non-default console context (not console.*):\n'anonymous#unique-logger-id' for call on unnamed context, 'name#unique-logger-id' for call\non named context.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "exceptionRevoked",
+ "description": "Issued when unhandled exception was revoked.",
+ "parameters": [
+ {
+ "name": "reason",
+ "description": "Reason describing why exception was revoked.",
+ "type": "string"
+ },
+ {
+ "name": "exceptionId",
+ "description": "The id of revoked exception, as reported in `exceptionThrown`.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "exceptionThrown",
+ "description": "Issued when exception was thrown and unhandled.",
+ "parameters": [
+ {
+ "name": "timestamp",
+ "description": "Timestamp of the exception.",
+ "$ref": "Timestamp"
+ },
+ {
+ "name": "exceptionDetails",
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "executionContextCreated",
+ "description": "Issued when new execution context is created.",
+ "parameters": [
+ {
+ "name": "context",
+ "description": "A newly created execution context.",
+ "$ref": "ExecutionContextDescription"
+ }
+ ]
+ },
+ {
+ "name": "executionContextDestroyed",
+ "description": "Issued when execution context is destroyed.",
+ "parameters": [
+ {
+ "name": "executionContextId",
+ "description": "Id of the destroyed context",
+ "deprecated": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "executionContextUniqueId",
+ "description": "Unique Id of the destroyed context",
+ "experimental": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "executionContextsCleared",
+ "description": "Issued when all executionContexts were cleared in browser"
+ },
+ {
+ "name": "inspectRequested",
+ "description": "Issued when object should be inspected (for example, as a result of inspect() command line API\ncall).",
+ "parameters": [
+ {
+ "name": "object",
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "hints",
+ "type": "object"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Identifier of the context where the call was made.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Schema",
+ "description": "This domain is deprecated.",
+ "deprecated": true,
+ "types": [
+ {
+ "id": "Domain",
+ "description": "Description of the protocol domain.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Domain name.",
+ "type": "string"
+ },
+ {
+ "name": "version",
+ "description": "Domain version.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getDomains",
+ "description": "Returns supported domains.",
+ "returns": [
+ {
+ "name": "domains",
+ "description": "List of supported domains.",
+ "type": "array",
+ "items": {
+ "$ref": "Domain"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/node_modules/chrome-remote-interface/lib/websocket-wrapper.js b/node_modules/chrome-remote-interface/lib/websocket-wrapper.js
new file mode 100644
index 0000000..8321446
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/websocket-wrapper.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const EventEmitter = require('events');
+
+// wrapper around the Node.js ws module
+// for use in browsers
+class WebSocketWrapper extends EventEmitter {
+ constructor(url) {
+ super();
+ this._ws = new WebSocket(url); // eslint-disable-line no-undef
+ this._ws.onopen = () => {
+ this.emit('open');
+ };
+ this._ws.onclose = () => {
+ this.emit('close');
+ };
+ this._ws.onmessage = (event) => {
+ this.emit('message', event.data);
+ };
+ this._ws.onerror = () => {
+ this.emit('error', new Error('WebSocket error'));
+ };
+ }
+
+ close() {
+ this._ws.close();
+ }
+
+ send(data, callback) {
+ try {
+ this._ws.send(data);
+ callback();
+ } catch (err) {
+ callback(err);
+ }
+ }
+}
+
+module.exports = WebSocketWrapper;
diff --git a/node_modules/chrome-remote-interface/package.json b/node_modules/chrome-remote-interface/package.json
new file mode 100644
index 0000000..dc01bf4
--- /dev/null
+++ b/node_modules/chrome-remote-interface/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "chrome-remote-interface",
+ "author": "Andrea Cardaci <cyrus.and@gmail.com>",
+ "license": "MIT",
+ "contributors": [
+ "Andrey Sidorov <sidoares@yandex.ru>",
+ "Greg Cochard <greg@gregcochard.com>"
+ ],
+ "description": "Chrome Debugging Protocol interface",
+ "keywords": [
+ "chrome",
+ "debug",
+ "protocol",
+ "remote",
+ "interface"
+ ],
+ "homepage": "https://github.com/cyrus-and/chrome-remote-interface",
+ "version": "0.33.0",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/cyrus-and/chrome-remote-interface.git"
+ },
+ "bugs": {
+ "url": "http://github.com/cyrus-and/chrome-remote-interface/issues"
+ },
+ "engine-strict": {
+ "node": ">=8"
+ },
+ "dependencies": {
+ "commander": "2.11.x",
+ "ws": "^7.2.0"
+ },
+ "files": [
+ "lib",
+ "bin",
+ "index.js",
+ "chrome-remote-interface.js",
+ "webpack.config.js"
+ ],
+ "bin": {
+ "chrome-remote-interface": "bin/client.js"
+ },
+ "main": "index.js",
+ "browser": "chrome-remote-interface.js",
+ "devDependencies": {
+ "babel-core": "^6.26.3",
+ "babel-loader": "8.x.x",
+ "babel-polyfill": "^6.26.0",
+ "babel-preset-env": "^1.7.0",
+ "eslint": "^8.8.0",
+ "json-loader": "^0.5.4",
+ "mocha": "^9.2.0",
+ "process": "^0.11.10",
+ "url": "^0.11.0",
+ "util": "^0.12.4",
+ "webpack": "^5.39.0",
+ "webpack-cli": "^4.7.2"
+ },
+ "scripts": {
+ "test": "./scripts/run-tests.sh",
+ "webpack": "webpack",
+ "prepare": "webpack"
+ }
+}
diff --git a/node_modules/chrome-remote-interface/webpack.config.js b/node_modules/chrome-remote-interface/webpack.config.js
new file mode 100644
index 0000000..0b7da3c
--- /dev/null
+++ b/node_modules/chrome-remote-interface/webpack.config.js
@@ -0,0 +1,48 @@
+'use strict';
+
+const TerserPlugin = require('terser-webpack-plugin');
+const webpack = require('webpack');
+
+function criWrapper(_, options, callback) {
+ window.criRequest(options, callback); // eslint-disable-line no-undef
+}
+
+module.exports = {
+ mode: 'production',
+ resolve: {
+ fallback: {
+ 'util': require.resolve('util/'),
+ 'url': require.resolve('url/'),
+ 'http': false,
+ 'https': false,
+ 'dns': false
+ },
+ alias: {
+ 'ws': './websocket-wrapper.js'
+ }
+ },
+ externals: [
+ {
+ './external-request.js': `var (${criWrapper})`
+ }
+ ],
+ plugins: [
+ new webpack.ProvidePlugin({
+ process: 'process/browser',
+ }),
+ ],
+ optimization: {
+ minimizer: [
+ new TerserPlugin({
+ extractComments: false,
+ })
+ ],
+ },
+ entry: ['babel-polyfill', './index.js'],
+ output: {
+ path: __dirname,
+ filename: 'chrome-remote-interface.js',
+ libraryTarget: process.env.TARGET || 'commonjs2',
+ library: 'CDP'
+ }
+};
diff --git a/node_modules/commander/History.md b/node_modules/commander/History.md
new file mode 100644
index 0000000..d60418e
--- /dev/null
+++ b/node_modules/commander/History.md
@@ -0,0 +1,298 @@
+
+2.11.0 / 2017-07-03
+==================
+
+ * Fix help section order and padding (#652)
+ * feature: support for signals to subcommands (#632)
+ * Fixed #37, --help should not display first (#447)
+ * Fix translation errors. (#570)
+ * Add package-lock.json
+ * Remove engines
+ * Upgrade package version
+ * Prefix events to prevent conflicts between commands and options (#494)
+ * Removing dependency on graceful-readlink
+ * Support setting name in #name function and make it chainable
+ * Add .vscode directory to .gitignore (Visual Studio Code metadata)
+ * Updated link to ruby commander in readme files
+
+2.10.0 / 2017-06-19
+==================
+
+ * Update .travis.yml. drop support for older node.js versions.
+ * Fix require arguments in README.md
+ * On SemVer you do not start from 0.0.1
+ * Add missing semi colon in readme
+ * Add save param to npm install
+ * node v6 travis test
+ * Update Readme_zh-CN.md
+ * Allow literal '--' to be passed-through as an argument
+ * Test subcommand alias help
+ * link build badge to master branch
+ * Support the alias of Git style sub-command
+ * added keyword commander for better search result on npm
+ * Fix Sub-Subcommands
+ * test node.js stable
+ * Fixes TypeError when a command has an option called `--description`
+ * Update README.md to make it beginner friendly and elaborate on the difference between angled and square brackets.
+ * Add chinese Readme file
+
+2.9.0 / 2015-10-13
+==================
+
+ * Add option `isDefault` to set default subcommand #415 @Qix-
+ * Add callback to allow filtering or post-processing of help text #434 @djulien
+ * Fix `undefined` text in help information close #414 #416 @zhiyelee
+
+2.8.1 / 2015-04-22
+==================
+
+ * Back out `support multiline description` Close #396 #397
+
+2.8.0 / 2015-04-07
+==================
+
+ * Add `process.execArg` support, execution args like `--harmony` will be passed to sub-commands #387 @DigitalIO @zhiyelee
+ * Fix bug in Git-style sub-commands #372 @zhiyelee
+ * Allow commands to be hidden from help #383 @tonylukasavage
+ * When git-style sub-commands are in use, yet none are called, display help #382 @claylo
+ * Add ability to specify arguments syntax for top-level command #258 @rrthomas
+ * Support multiline descriptions #208 @zxqfox
+
+2.7.1 / 2015-03-11
+==================
+
+ * Revert #347 (fix collisions when option and first arg have same name) which causes a bug in #367.
+
+2.7.0 / 2015-03-09
+==================
+
+ * Fix git-style bug when installed globally. Close #335 #349 @zhiyelee
+ * Fix collisions when option and first arg have same name. Close #346 #347 @tonylukasavage
+ * Add support for camelCase on `opts()`. Close #353 @nkzawa
+ * Add node.js 0.12 and io.js to travis.yml
+ * Allow RegEx options. #337 @palanik
+ * Fixes exit code when sub-command failing. Close #260 #332 @pirelenito
+ * git-style `bin` files in $PATH make sense. Close #196 #327 @zhiyelee
+
+2.6.0 / 2014-12-30
+==================
+
+ * added `Command#allowUnknownOption` method. Close #138 #318 @doozr @zhiyelee
+ * Add application description to the help msg. Close #112 @dalssoft
+
+2.5.1 / 2014-12-15
+==================
+
+ * fixed two bugs incurred by variadic arguments. Close #291 @Quentin01 #302 @zhiyelee
+
+2.5.0 / 2014-10-24
+==================
+
+ * add support for variadic arguments. Closes #277 @whitlockjc
+
+2.4.0 / 2014-10-17
+==================
+
+ * fixed a bug on executing the coercion function of subcommands option. Closes #270
+ * added `Command.prototype.name` to retrieve command name. Closes #264 #266 @tonylukasavage
+ * added `Command.prototype.opts` to retrieve all the options as a simple object of key-value pairs. Closes #262 @tonylukasavage
+ * fixed a bug on subcommand name. Closes #248 @jonathandelgado
+ * fixed function normalize doesn’t honor option terminator. Closes #216 @abbr
+
+2.3.0 / 2014-07-16
+==================
+
+ * add command alias'. Closes PR #210
+ * fix: Typos. Closes #99
+ * fix: Unused fs module. Closes #217
+
+2.2.0 / 2014-03-29
+==================
+
+ * add passing of previous option value
+ * fix: support subcommands on windows. Closes #142
+ * Now the defaultValue passed as the second argument of the coercion function.
+
+2.1.0 / 2013-11-21
+==================
+
+ * add: allow cflag style option params, unit test, fixes #174
+
+2.0.0 / 2013-07-18
+==================
+
+ * remove input methods (.prompt, .confirm, etc)
+
+1.3.2 / 2013-07-18
+==================
+
+ * add support for sub-commands to co-exist with the original command
+
+1.3.1 / 2013-07-18
+==================
+
+ * add quick .runningCommand hack so you can opt-out of other logic when running a sub command
+
+1.3.0 / 2013-07-09
+==================
+
+ * add EACCES error handling
+ * fix sub-command --help
+
+1.2.0 / 2013-06-13
+==================
+
+ * allow "-" hyphen as an option argument
+ * support for RegExp coercion
+
+1.1.1 / 2012-11-20
+==================
+
+ * add more sub-command padding
+ * fix .usage() when args are present. Closes #106
+
+1.1.0 / 2012-11-16
+==================
+
+ * add git-style executable subcommand support. Closes #94
+
+1.0.5 / 2012-10-09
+==================
+
+ * fix `--name` clobbering. Closes #92
+ * fix examples/help. Closes #89
+
+1.0.4 / 2012-09-03
+==================
+
+ * add `outputHelp()` method.
+
+1.0.3 / 2012-08-30
+==================
+
+ * remove invalid .version() defaulting
+
+1.0.2 / 2012-08-24
+==================
+
+ * add `--foo=bar` support [arv]
+ * fix password on node 0.8.8. Make backward compatible with 0.6 [focusaurus]
+
+1.0.1 / 2012-08-03
+==================
+
+ * fix issue #56
+ * fix tty.setRawMode(mode) was moved to tty.ReadStream#setRawMode() (i.e. process.stdin.setRawMode())
+
+1.0.0 / 2012-07-05
+==================
+
+ * add support for optional option descriptions
+ * add defaulting of `.version()` to package.json's version
+
+0.6.1 / 2012-06-01
+==================
+
+ * Added: append (yes or no) on confirmation
+ * Added: allow node.js v0.7.x
+
+0.6.0 / 2012-04-10
+==================
+
+ * Added `.prompt(obj, callback)` support. Closes #49
+ * Added default support to .choose(). Closes #41
+ * Fixed the choice example
+
+0.5.1 / 2011-12-20
+==================
+
+ * Fixed `password()` for recent nodes. Closes #36
+
+0.5.0 / 2011-12-04
+==================
+
+ * Added sub-command option support [itay]
+
+0.4.3 / 2011-12-04
+==================
+
+ * Fixed custom help ordering. Closes #32
+
+0.4.2 / 2011-11-24
+==================
+
+ * Added travis support
+ * Fixed: line-buffered input automatically trimmed. Closes #31
+
+0.4.1 / 2011-11-18
+==================
+
+ * Removed listening for "close" on --help
+
+0.4.0 / 2011-11-15
+==================
+
+ * Added support for `--`. Closes #24
+
+0.3.3 / 2011-11-14
+==================
+
+ * Fixed: wait for close event when writing help info [Jerry Hamlet]
+
+0.3.2 / 2011-11-01
+==================
+
+ * Fixed long flag definitions with values [felixge]
+
+0.3.1 / 2011-10-31
+==================
+
+ * Changed `--version` short flag to `-V` from `-v`
+ * Changed `.version()` so it's configurable [felixge]
+
+0.3.0 / 2011-10-31
+==================
+
+ * Added support for long flags only. Closes #18
+
+0.2.1 / 2011-10-24
+==================
+
+ * "node": ">= 0.4.x < 0.7.0". Closes #20
+
+0.2.0 / 2011-09-26
+==================
+
+ * Allow for defaults that are not just boolean. Default peassignment only occurs for --no-*, optional, and required arguments. [Jim Isaacs]
+
+0.1.0 / 2011-08-24
+==================
+
+ * Added support for custom `--help` output
+
+0.0.5 / 2011-08-18
+==================
+
+ * Changed: when the user enters nothing prompt for password again
+ * Fixed issue with passwords beginning with numbers [NuckChorris]
+
+0.0.4 / 2011-08-15
+==================
+
+ * Fixed `Commander#args`
+
+0.0.3 / 2011-08-15
+==================
+
+ * Added default option value support
+
+0.0.2 / 2011-08-15
+==================
+
+ * Added mask support to `Command#password(str[, mask], fn)`
+ * Added `Command#password(str, fn)`
+
+0.0.1 / 2010-01-03
+==================
+
+ * Initial release
diff --git a/node_modules/commander/LICENSE b/node_modules/commander/LICENSE
new file mode 100644
index 0000000..10f997a
--- /dev/null
+++ b/node_modules/commander/LICENSE
@@ -0,0 +1,22 @@
+(The MIT License)
+
+Copyright (c) 2011 TJ Holowaychuk <tj@vision-media.ca>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/node_modules/commander/Readme.md b/node_modules/commander/Readme.md
new file mode 100644
index 0000000..3135a94
--- /dev/null
+++ b/node_modules/commander/Readme.md
@@ -0,0 +1,351 @@
+# Commander.js
+
+
+[![Build Status](https://api.travis-ci.org/tj/commander.js.svg?branch=master)](http://travis-ci.org/tj/commander.js)
+[![NPM Version](http://img.shields.io/npm/v/commander.svg?style=flat)](https://www.npmjs.org/package/commander)
+[![NPM Downloads](https://img.shields.io/npm/dm/commander.svg?style=flat)](https://www.npmjs.org/package/commander)
+[![Join the chat at https://gitter.im/tj/commander.js](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/tj/commander.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+ The complete solution for [node.js](http://nodejs.org) command-line interfaces, inspired by Ruby's [commander](https://github.com/commander-rb/commander).
+ [API documentation](http://tj.github.com/commander.js/)
+
+
+## Installation
+
+ $ npm install commander --save
+
+## Option parsing
+
+ Options with commander are defined with the `.option()` method, also serving as documentation for the options. The example below parses args and options from `process.argv`, leaving remaining args as the `program.args` array which were not consumed by options.
+
+```js
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .option('-p, --peppers', 'Add peppers')
+ .option('-P, --pineapple', 'Add pineapple')
+ .option('-b, --bbq-sauce', 'Add bbq sauce')
+ .option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
+ .parse(process.argv);
+
+console.log('you ordered a pizza with:');
+if (program.peppers) console.log(' - peppers');
+if (program.pineapple) console.log(' - pineapple');
+if (program.bbqSauce) console.log(' - bbq');
+console.log(' - %s cheese', program.cheese);
+```
+
+ Short flags may be passed as a single arg, for example `-abc` is equivalent to `-a -b -c`. Multi-word options such as "--template-engine" are camel-cased, becoming `program.templateEngine` etc.
+
+
+## Coercion
+
+```js
+function range(val) {
+ return val.split('..').map(Number);
+}
+
+function list(val) {
+ return val.split(',');
+}
+
+function collect(val, memo) {
+ memo.push(val);
+ return memo;
+}
+
+function increaseVerbosity(v, total) {
+ return total + 1;
+}
+
+program
+ .version('0.1.0')
+ .usage('[options] <file ...>')
+ .option('-i, --integer <n>', 'An integer argument', parseInt)
+ .option('-f, --float <n>', 'A float argument', parseFloat)
+ .option('-r, --range <a>..<b>', 'A range', range)
+ .option('-l, --list <items>', 'A list', list)
+ .option('-o, --optional [value]', 'An optional value')
+ .option('-c, --collect [value]', 'A repeatable value', collect, [])
+ .option('-v, --verbose', 'A value that can be increased', increaseVerbosity, 0)
+ .parse(process.argv);
+
+console.log(' int: %j', program.integer);
+console.log(' float: %j', program.float);
+console.log(' optional: %j', program.optional);
+program.range = program.range || [];
+console.log(' range: %j..%j', program.range[0], program.range[1]);
+console.log(' list: %j', program.list);
+console.log(' collect: %j', program.collect);
+console.log(' verbosity: %j', program.verbose);
+console.log(' args: %j', program.args);
+```
+
+## Regular Expression
+```js
+program
+ .version('0.1.0')
+ .option('-s --size <size>', 'Pizza size', /^(large|medium|small)$/i, 'medium')
+ .option('-d --drink [drink]', 'Drink', /^(coke|pepsi|izze)$/i)
+ .parse(process.argv);
+
+console.log(' size: %j', program.size);
+console.log(' drink: %j', program.drink);
+```
+
+## Variadic arguments
+
+ The last argument of a command can be variadic, and only the last argument. To make an argument variadic you have to
+ append `...` to the argument name. Here is an example:
+
+```js
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .command('rmdir <dir> [otherDirs...]')
+ .action(function (dir, otherDirs) {
+ console.log('rmdir %s', dir);
+ if (otherDirs) {
+ otherDirs.forEach(function (oDir) {
+ console.log('rmdir %s', oDir);
+ });
+ }
+ });
+
+program.parse(process.argv);
+```
+
+ An `Array` is used for the value of a variadic argument. This applies to `program.args` as well as the argument passed
+ to your action as demonstrated above.
+
+## Specify the argument syntax
+
+```js
+#!/usr/bin/env node
+
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .arguments('<cmd> [env]')
+ .action(function (cmd, env) {
+ cmdValue = cmd;
+ envValue = env;
+ });
+
+program.parse(process.argv);
+
+if (typeof cmdValue === 'undefined') {
+ console.error('no command given!');
+ process.exit(1);
+}
+console.log('command:', cmdValue);
+console.log('environment:', envValue || "no environment given");
+```
+Angled brackets (e.g. `<cmd>`) indicate required input. Square brackets (e.g. `[env]`) indicate optional input.
+
+## Git-style sub-commands
+
+```js
+// file: ./examples/pm
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .command('install [name]', 'install one or more packages')
+ .command('search [query]', 'search with optional query')
+ .command('list', 'list packages installed', {isDefault: true})
+ .parse(process.argv);
+```
+
+When `.command()` is invoked with a description argument, no `.action(callback)` should be called to handle sub-commands, otherwise there will be an error. This tells commander that you're going to use separate executables for sub-commands, much like `git(1)` and other popular tools.
+The commander will try to search the executables in the directory of the entry script (like `./examples/pm`) with the name `program-command`, like `pm-install`, `pm-search`.
+
+Options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the option from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified.
+
+If the program is designed to be installed globally, make sure the executables have proper modes, like `755`.
+
+### `--harmony`
+
+You can enable `--harmony` option in two ways:
+* Use `#! /usr/bin/env node --harmony` in the sub-commands scripts. Note some os version don’t support this pattern.
+* Use the `--harmony` option when call the command, like `node --harmony examples/pm publish`. The `--harmony` option will be preserved when spawning sub-command process.
+
+## Automated --help
+
+ The help information is auto-generated based on the information commander already knows about your program, so the following `--help` info is for free:
+
+```
+ $ ./examples/pizza --help
+
+ Usage: pizza [options]
+
+ An application for pizzas ordering
+
+ Options:
+
+ -h, --help output usage information
+ -V, --version output the version number
+ -p, --peppers Add peppers
+ -P, --pineapple Add pineapple
+ -b, --bbq Add bbq sauce
+ -c, --cheese <type> Add the specified type of cheese [marble]
+ -C, --no-cheese You do not want any cheese
+
+```
+
+## Custom help
+
+ You can display arbitrary `-h, --help` information
+ by listening for "--help". Commander will automatically
+ exit once you are done so that the remainder of your program
+ does not execute causing undesired behaviours, for example
+ in the following executable "stuff" will not output when
+ `--help` is used.
+
+```js
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .option('-f, --foo', 'enable some foo')
+ .option('-b, --bar', 'enable some bar')
+ .option('-B, --baz', 'enable some baz');
+
+// must be before .parse() since
+// node's emit() is immediate
+
+program.on('--help', function(){
+ console.log(' Examples:');
+ console.log('');
+ console.log(' $ custom-help --help');
+ console.log(' $ custom-help -h');
+ console.log('');
+});
+
+program.parse(process.argv);
+
+console.log('stuff');
+```
+
+Yields the following help output when `node script-name.js -h` or `node script-name.js --help` are run:
+
+```
+
+Usage: custom-help [options]
+
+Options:
+
+ -h, --help output usage information
+ -V, --version output the version number
+ -f, --foo enable some foo
+ -b, --bar enable some bar
+ -B, --baz enable some baz
+
+Examples:
+
+ $ custom-help --help
+ $ custom-help -h
+
+```
+
+## .outputHelp(cb)
+
+Output help information without exiting.
+Optional callback cb allows post-processing of help text before it is displayed.
+
+If you want to display help by default (e.g. if no command was provided), you can use something like:
+
+```js
+var program = require('commander');
+var colors = require('colors');
+
+program
+ .version('0.1.0')
+ .command('getstream [url]', 'get stream URL')
+ .parse(process.argv);
+
+ if (!process.argv.slice(2).length) {
+ program.outputHelp(make_red);
+ }
+
+function make_red(txt) {
+ return colors.red(txt); //display the help text in red on the console
+}
+```
+
+## .help(cb)
+
+ Output help information and exit immediately.
+ Optional callback cb allows post-processing of help text before it is displayed.
+
+## Examples
+
+```js
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .option('-C, --chdir <path>', 'change the working directory')
+ .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
+ .option('-T, --no-tests', 'ignore test hook');
+
+program
+ .command('setup [env]')
+ .description('run setup commands for all envs')
+ .option("-s, --setup_mode [mode]", "Which setup mode to use")
+ .action(function(env, options){
+ var mode = options.setup_mode || "normal";
+ env = env || 'all';
+ console.log('setup for %s env(s) with %s mode', env, mode);
+ });
+
+program
+ .command('exec <cmd>')
+ .alias('ex')
+ .description('execute the given remote cmd')
+ .option("-e, --exec_mode <mode>", "Which exec mode to use")
+ .action(function(cmd, options){
+ console.log('exec "%s" using %s mode', cmd, options.exec_mode);
+ }).on('--help', function() {
+ console.log(' Examples:');
+ console.log();
+ console.log(' $ deploy exec sequential');
+ console.log(' $ deploy exec async');
+ console.log();
+ });
+
+program
+ .command('*')
+ .action(function(env){
+ console.log('deploying "%s"', env);
+ });
+
+program.parse(process.argv);
+```
+
+More Demos can be found in the [examples](https://github.com/tj/commander.js/tree/master/examples) directory.
+
+## License
+
+MIT
diff --git a/node_modules/commander/index.js b/node_modules/commander/index.js
new file mode 100644
index 0000000..d5dbe18
--- /dev/null
+++ b/node_modules/commander/index.js
@@ -0,0 +1,1137 @@
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('events').EventEmitter;
+var spawn = require('child_process').spawn;
+var path = require('path');
+var dirname = path.dirname;
+var basename = path.basename;
+var fs = require('fs');
+
+/**
+ * Expose the root command.
+ */
+
+exports = module.exports = new Command();
+
+/**
+ * Expose `Command`.
+ */
+
+exports.Command = Command;
+
+/**
+ * Expose `Option`.
+ */
+
+exports.Option = Option;
+
+/**
+ * Initialize a new `Option` with the given `flags` and `description`.
+ *
+ * @param {String} flags
+ * @param {String} description
+ * @api public
+ */
+
+function Option(flags, description) {
+ this.flags = flags;
+ this.required = ~flags.indexOf('<');
+ this.optional = ~flags.indexOf('[');
+ this.bool = !~flags.indexOf('-no-');
+ flags = flags.split(/[ ,|]+/);
+ if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift();
+ this.long = flags.shift();
+ this.description = description || '';
+}
+
+/**
+ * Return option name.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Option.prototype.name = function() {
+ return this.long
+ .replace('--', '')
+ .replace('no-', '');
+};
+
+/**
+ * Check if `arg` matches the short or long flag.
+ *
+ * @param {String} arg
+ * @return {Boolean}
+ * @api private
+ */
+
+Option.prototype.is = function(arg) {
+ return arg == this.short || arg == this.long;
+};
+
+/**
+ * Initialize a new `Command`.
+ *
+ * @param {String} name
+ * @api public
+ */
+
+function Command(name) {
+ this.commands = [];
+ this.options = [];
+ this._execs = {};
+ this._allowUnknownOption = false;
+ this._args = [];
+ this._name = name || '';
+}
+
+/**
+ * Inherit from `EventEmitter.prototype`.
+ */
+
+Command.prototype.__proto__ = EventEmitter.prototype;
+
+/**
+ * Add command `name`.
+ *
+ * The `.action()` callback is invoked when the
+ * command `name` is specified via __ARGV__,
+ * and the remaining arguments are applied to the
+ * function for access.
+ *
+ * When the `name` is "*" an un-matched command
+ * will be passed as the first arg, followed by
+ * the rest of __ARGV__ remaining.
+ *
+ * Examples:
+ *
+ * program
+ * .version('0.0.1')
+ * .option('-C, --chdir <path>', 'change the working directory')
+ * .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
+ * .option('-T, --no-tests', 'ignore test hook')
+ *
+ * program
+ * .command('setup')
+ * .description('run remote setup commands')
+ * .action(function() {
+ * console.log('setup');
+ * });
+ *
+ * program
+ * .command('exec <cmd>')
+ * .description('run the given remote command')
+ * .action(function(cmd) {
+ * console.log('exec "%s"', cmd);
+ * });
+ *
+ * program
+ * .command('teardown <dir> [otherDirs...]')
+ * .description('run teardown commands')
+ * .action(function(dir, otherDirs) {
+ * console.log('dir "%s"', dir);
+ * if (otherDirs) {
+ * otherDirs.forEach(function (oDir) {
+ * console.log('dir "%s"', oDir);
+ * });
+ * }
+ * });
+ *
+ * program
+ * .command('*')
+ * .description('deploy the given env')
+ * .action(function(env) {
+ * console.log('deploying "%s"', env);
+ * });
+ *
+ * program.parse(process.argv);
+ *
+ * @param {String} name
+ * @param {String} [desc] for git-style sub-commands
+ * @return {Command} the new command
+ * @api public
+ */
+
+Command.prototype.command = function(name, desc, opts) {
+ opts = opts || {};
+ var args = name.split(/ +/);
+ var cmd = new Command(args.shift());
+
+ if (desc) {
+ cmd.description(desc);
+ this.executables = true;
+ this._execs[cmd._name] = true;
+ if (opts.isDefault) this.defaultExecutable = cmd._name;
+ }
+
+ cmd._noHelp = !!opts.noHelp;
+ this.commands.push(cmd);
+ cmd.parseExpectedArgs(args);
+ cmd.parent = this;
+
+ if (desc) return this;
+ return cmd;
+};
+
+/**
+ * Define argument syntax for the top-level command.
+ *
+ * @api public
+ */
+
+Command.prototype.arguments = function (desc) {
+ return this.parseExpectedArgs(desc.split(/ +/));
+};
+
+/**
+ * Add an implicit `help [cmd]` subcommand
+ * which invokes `--help` for the given command.
+ *
+ * @api private
+ */
+
+Command.prototype.addImplicitHelpCommand = function() {
+ this.command('help [cmd]', 'display help for [cmd]');
+};
+
+/**
+ * Parse expected `args`.
+ *
+ * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`.
+ *
+ * @param {Array} args
+ * @return {Command} for chaining
+ * @api public
+ */
+
+Command.prototype.parseExpectedArgs = function(args) {
+ if (!args.length) return;
+ var self = this;
+ args.forEach(function(arg) {
+ var argDetails = {
+ required: false,
+ name: '',
+ variadic: false
+ };
+
+ switch (arg[0]) {
+ case '<':
+ argDetails.required = true;
+ argDetails.name = arg.slice(1, -1);
+ break;
+ case '[':
+ argDetails.name = arg.slice(1, -1);
+ break;
+ }
+
+ if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') {
+ argDetails.variadic = true;
+ argDetails.name = argDetails.name.slice(0, -3);
+ }
+ if (argDetails.name) {
+ self._args.push(argDetails);
+ }
+ });
+ return this;
+};
+
+/**
+ * Register callback `fn` for the command.
+ *
+ * Examples:
+ *
+ * program
+ * .command('help')
+ * .description('display verbose help')
+ * .action(function() {
+ * // output help here
+ * });
+ *
+ * @param {Function} fn
+ * @return {Command} for chaining
+ * @api public
+ */
+
+Command.prototype.action = function(fn) {
+ var self = this;
+ var listener = function(args, unknown) {
+ // Parse any so-far unknown options
+ args = args || [];
+ unknown = unknown || [];
+
+ var parsed = self.parseOptions(unknown);
+
+ // Output help if necessary
+ outputHelpIfNecessary(self, parsed.unknown);
+
+ // If there are still any unknown options, then we simply
+ // die, unless someone asked for help, in which case we give it
+ // to them, and then we die.
+ if (parsed.unknown.length > 0) {
+ self.unknownOption(parsed.unknown[0]);
+ }
+
+ // Leftover arguments need to be pushed back. Fixes issue #56
+ if (parsed.args.length) args = parsed.args.concat(args);
+
+ self._args.forEach(function(arg, i) {
+ if (arg.required && null == args[i]) {
+ self.missingArgument(arg.name);
+ } else if (arg.variadic) {
+ if (i !== self._args.length - 1) {
+ self.variadicArgNotLast(arg.name);
+ }
+
+ args[i] = args.splice(i);
+ }
+ });
+
+ // Always append ourselves to the end of the arguments,
+ // to make sure we match the number of arguments the user
+ // expects
+ if (self._args.length) {
+ args[self._args.length] = self;
+ } else {
+ args.push(self);
+ }
+
+ fn.apply(self, args);
+ };
+ var parent = this.parent || this;
+ var name = parent === this ? '*' : this._name;
+ parent.on('command:' + name, listener);
+ if (this._alias) parent.on('command:' + this._alias, listener);
+ return this;
+};
+
+/**
+ * Define option with `flags`, `description` and optional
+ * coercion `fn`.
+ *
+ * The `flags` string should contain both the short and long flags,
+ * separated by comma, a pipe or space. The following are all valid
+ * all will output this way when `--help` is used.
+ *
+ * "-p, --pepper"
+ * "-p|--pepper"
+ * "-p --pepper"
+ *
+ * Examples:
+ *
+ * // simple boolean defaulting to false
+ * program.option('-p, --pepper', 'add pepper');
+ *
+ * --pepper
+ * program.pepper
+ * // => Boolean
+ *
+ * // simple boolean defaulting to true
+ * program.option('-C, --no-cheese', 'remove cheese');
+ *
+ * program.cheese
+ * // => true
+ *
+ * --no-cheese
+ * program.cheese
+ * // => false
+ *
+ * // required argument
+ * program.option('-C, --chdir <path>', 'change the working directory');
+ *
+ * --chdir /tmp
+ * program.chdir
+ * // => "/tmp"
+ *
+ * // optional argument
+ * program.option('-c, --cheese [type]', 'add cheese [marble]');
+ *
+ * @param {String} flags
+ * @param {String} description
+ * @param {Function|*} [fn] or default
+ * @param {*} [defaultValue]
+ * @return {Command} for chaining
+ * @api public
+ */
+
+Command.prototype.option = function(flags, description, fn, defaultValue) {
+ var self = this
+ , option = new Option(flags, description)
+ , oname = option.name()
+ , name = camelcase(oname);
+
+ // default as 3rd arg
+ if (typeof fn != 'function') {
+ if (fn instanceof RegExp) {
+ var regex = fn;
+ fn = function(val, def) {
+ var m = regex.exec(val);
+ return m ? m[0] : def;
+ }
+ }
+ else {
+ defaultValue = fn;
+ fn = null;
+ }
+ }
+
+ // preassign default value only for --no-*, [optional], or <required>
+ if (false == option.bool || option.optional || option.required) {
+ // when --no-* we make sure default is true
+ if (false == option.bool) defaultValue = true;
+ // preassign only if we have a default
+ if (undefined !== defaultValue) self[name] = defaultValue;
+ }
+
+ // register the option
+ this.options.push(option);
+
+ // when it's passed assign the value
+ // and conditionally invoke the callback
+ this.on('option:' + oname, function(val) {
+ // coercion
+ if (null !== val && fn) val = fn(val, undefined === self[name]
+ ? defaultValue
+ : self[name]);
+
+ // unassigned or bool
+ if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) {
+ // if no value, bool true, and we have a default, then use it!
+ if (null == val) {
+ self[name] = option.bool
+ ? defaultValue || true
+ : false;
+ } else {
+ self[name] = val;
+ }
+ } else if (null !== val) {
+ // reassign
+ self[name] = val;
+ }
+ });
+
+ return this;
+};
+
+/**
+ * Allow unknown options on the command line.
+ *
+ * @param {Boolean} arg if `true` or omitted, no error will be thrown
+ * for unknown options.
+ * @api public
+ */
+Command.prototype.allowUnknownOption = function(arg) {
+ this._allowUnknownOption = arguments.length === 0 || arg;
+ return this;
+};
+
+/**
+ * Parse `argv`, settings options and invoking commands when defined.
+ *
+ * @param {Array} argv
+ * @return {Command} for chaining
+ * @api public
+ */
+
+Command.prototype.parse = function(argv) {
+ // implicit help
+ if (this.executables) this.addImplicitHelpCommand();
+
+ // store raw args
+ this.rawArgs = argv;
+
+ // guess name
+ this._name = this._name || basename(argv[1], '.js');
+
+ // github-style sub-commands with no sub-command
+ if (this.executables && argv.length < 3 && !this.defaultExecutable) {
+ // this user needs help
+ argv.push('--help');
+ }
+
+ // process argv
+ var parsed = this.parseOptions(this.normalize(argv.slice(2)));
+ var args = this.args = parsed.args;
+
+ var result = this.parseArgs(this.args, parsed.unknown);
+
+ // executable sub-commands
+ var name = result.args[0];
+
+ var aliasCommand = null;
+ // check alias of sub commands
+ if (name) {
+ aliasCommand = this.commands.filter(function(command) {
+ return command.alias() === name;
+ })[0];
+ }
+
+ if (this._execs[name] && typeof this._execs[name] != "function") {
+ return this.executeSubCommand(argv, args, parsed.unknown);
+ } else if (aliasCommand) {
+ // is alias of a subCommand
+ args[0] = aliasCommand._name;
+ return this.executeSubCommand(argv, args, parsed.unknown);
+ } else if (this.defaultExecutable) {
+ // use the default subcommand
+ args.unshift(this.defaultExecutable);
+ return this.executeSubCommand(argv, args, parsed.unknown);
+ }
+
+ return result;
+};
+
+/**
+ * Execute a sub-command executable.
+ *
+ * @param {Array} argv
+ * @param {Array} args
+ * @param {Array} unknown
+ * @api private
+ */
+
+Command.prototype.executeSubCommand = function(argv, args, unknown) {
+ args = args.concat(unknown);
+
+ if (!args.length) this.help();
+ if ('help' == args[0] && 1 == args.length) this.help();
+
+ // <cmd> --help
+ if ('help' == args[0]) {
+ args[0] = args[1];
+ args[1] = '--help';
+ }
+
+ // executable
+ var f = argv[1];
+ // name of the subcommand, link `pm-install`
+ var bin = basename(f, '.js') + '-' + args[0];
+
+
+ // In case of globally installed, get the base dir where executable
+ // subcommand file should be located at
+ var baseDir
+ , link = fs.lstatSync(f).isSymbolicLink() ? fs.readlinkSync(f) : f;
+
+ // when symbolink is relative path
+ if (link !== f && link.charAt(0) !== '/') {
+ link = path.join(dirname(f), link)
+ }
+ baseDir = dirname(link);
+
+ // prefer local `./<bin>` to bin in the $PATH
+ var localBin = path.join(baseDir, bin);
+
+ // whether bin file is a js script with explicit `.js` extension
+ var isExplicitJS = false;
+ if (exists(localBin + '.js')) {
+ bin = localBin + '.js';
+ isExplicitJS = true;
+ } else if (exists(localBin)) {
+ bin = localBin;
+ }
+
+ args = args.slice(1);
+
+ var proc;
+ if (process.platform !== 'win32') {
+ if (isExplicitJS) {
+ args.unshift(bin);
+ // add executable arguments to spawn
+ args = (process.execArgv || []).concat(args);
+
+ proc = spawn('node', args, { stdio: 'inherit', customFds: [0, 1, 2] });
+ } else {
+ proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] });
+ }
+ } else {
+ args.unshift(bin);
+ proc = spawn(process.execPath, args, { stdio: 'inherit'});
+ }
+
+ var signals = ['SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGINT', 'SIGHUP'];
+ signals.forEach(function(signal) {
+ process.on(signal, function(){
+ if ((proc.killed === false) && (proc.exitCode === null)){
+ proc.kill(signal);
+ }
+ });
+ });
+ proc.on('close', process.exit.bind(process));
+ proc.on('error', function(err) {
+ if (err.code == "ENOENT") {
+ console.error('\n %s(1) does not exist, try --help\n', bin);
+ } else if (err.code == "EACCES") {
+ console.error('\n %s(1) not executable. try chmod or run with root\n', bin);
+ }
+ process.exit(1);
+ });
+
+ // Store the reference to the child process
+ this.runningCommand = proc;
+};
+
+/**
+ * Normalize `args`, splitting joined short flags. For example
+ * the arg "-abc" is equivalent to "-a -b -c".
+ * This also normalizes equal sign and splits "--abc=def" into "--abc def".
+ *
+ * @param {Array} args
+ * @return {Array}
+ * @api private
+ */
+
+Command.prototype.normalize = function(args) {
+ var ret = []
+ , arg
+ , lastOpt
+ , index;
+
+ for (var i = 0, len = args.length; i < len; ++i) {
+ arg = args[i];
+ if (i > 0) {
+ lastOpt = this.optionFor(args[i-1]);
+ }
+
+ if (arg === '--') {
+ // Honor option terminator
+ ret = ret.concat(args.slice(i));
+ break;
+ } else if (lastOpt && lastOpt.required) {
+ ret.push(arg);
+ } else if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) {
+ arg.slice(1).split('').forEach(function(c) {
+ ret.push('-' + c);
+ });
+ } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
+ ret.push(arg.slice(0, index), arg.slice(index + 1));
+ } else {
+ ret.push(arg);
+ }
+ }
+
+ return ret;
+};
+
+/**
+ * Parse command `args`.
+ *
+ * When listener(s) are available those
+ * callbacks are invoked, otherwise the "*"
+ * event is emitted and those actions are invoked.
+ *
+ * @param {Array} args
+ * @return {Command} for chaining
+ * @api private
+ */
+
+Command.prototype.parseArgs = function(args, unknown) {
+ var name;
+
+ if (args.length) {
+ name = args[0];
+ if (this.listeners('command:' + name).length) {
+ this.emit('command:' + args.shift(), args, unknown);
+ } else {
+ this.emit('command:*', args);
+ }
+ } else {
+ outputHelpIfNecessary(this, unknown);
+
+ // If there were no args and we have unknown options,
+ // then they are extraneous and we need to error.
+ if (unknown.length > 0) {
+ this.unknownOption(unknown[0]);
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Return an option matching `arg` if any.
+ *
+ * @param {String} arg
+ * @return {Option}
+ * @api private
+ */
+
+Command.prototype.optionFor = function(arg) {
+ for (var i = 0, len = this.options.length; i < len; ++i) {
+ if (this.options[i].is(arg)) {
+ return this.options[i];
+ }
+ }
+};
+
+/**
+ * Parse options from `argv` returning `argv`
+ * void of these options.
+ *
+ * @param {Array} argv
+ * @return {Array}
+ * @api public
+ */
+
+Command.prototype.parseOptions = function(argv) {
+ var args = []
+ , len = argv.length
+ , literal
+ , option
+ , arg;
+
+ var unknownOptions = [];
+
+ // parse options
+ for (var i = 0; i < len; ++i) {
+ arg = argv[i];
+
+ // literal args after --
+ if (literal) {
+ args.push(arg);
+ continue;
+ }
+
+ if ('--' == arg) {
+ literal = true;
+ continue;
+ }
+
+ // find matching Option
+ option = this.optionFor(arg);
+
+ // option is defined
+ if (option) {
+ // requires arg
+ if (option.required) {
+ arg = argv[++i];
+ if (null == arg) return this.optionMissingArgument(option);
+ this.emit('option:' + option.name(), arg);
+ // optional arg
+ } else if (option.optional) {
+ arg = argv[i+1];
+ if (null == arg || ('-' == arg[0] && '-' != arg)) {
+ arg = null;
+ } else {
+ ++i;
+ }
+ this.emit('option:' + option.name(), arg);
+ // bool
+ } else {
+ this.emit('option:' + option.name());
+ }
+ continue;
+ }
+
+ // looks like an option
+ if (arg.length > 1 && '-' == arg[0]) {
+ unknownOptions.push(arg);
+
+ // If the next argument looks like it might be
+ // an argument for this option, we pass it on.
+ // If it isn't, then it'll simply be ignored
+ if (argv[i+1] && '-' != argv[i+1][0]) {
+ unknownOptions.push(argv[++i]);
+ }
+ continue;
+ }
+
+ // arg
+ args.push(arg);
+ }
+
+ return { args: args, unknown: unknownOptions };
+};
+
+/**
+ * Return an object containing options as key-value pairs
+ *
+ * @return {Object}
+ * @api public
+ */
+Command.prototype.opts = function() {
+ var result = {}
+ , len = this.options.length;
+
+ for (var i = 0 ; i < len; i++) {
+ var key = camelcase(this.options[i].name());
+ result[key] = key === 'version' ? this._version : this[key];
+ }
+ return result;
+};
+
+/**
+ * Argument `name` is missing.
+ *
+ * @param {String} name
+ * @api private
+ */
+
+Command.prototype.missingArgument = function(name) {
+ console.error();
+ console.error(" error: missing required argument `%s'", name);
+ console.error();
+ process.exit(1);
+};
+
+/**
+ * `Option` is missing an argument, but received `flag` or nothing.
+ *
+ * @param {String} option
+ * @param {String} flag
+ * @api private
+ */
+
+Command.prototype.optionMissingArgument = function(option, flag) {
+ console.error();
+ if (flag) {
+ console.error(" error: option `%s' argument missing, got `%s'", option.flags, flag);
+ } else {
+ console.error(" error: option `%s' argument missing", option.flags);
+ }
+ console.error();
+ process.exit(1);
+};
+
+/**
+ * Unknown option `flag`.
+ *
+ * @param {String} flag
+ * @api private
+ */
+
+Command.prototype.unknownOption = function(flag) {
+ if (this._allowUnknownOption) return;
+ console.error();
+ console.error(" error: unknown option `%s'", flag);
+ console.error();
+ process.exit(1);
+};
+
+/**
+ * Variadic argument with `name` is not the last argument as required.
+ *
+ * @param {String} name
+ * @api private
+ */
+
+Command.prototype.variadicArgNotLast = function(name) {
+ console.error();
+ console.error(" error: variadic arguments must be last `%s'", name);
+ console.error();
+ process.exit(1);
+};
+
+/**
+ * Set the program version to `str`.
+ *
+ * This method auto-registers the "-V, --version" flag
+ * which will print the version number when passed.
+ *
+ * @param {String} str
+ * @param {String} [flags]
+ * @return {Command} for chaining
+ * @api public
+ */
+
+Command.prototype.version = function(str, flags) {
+ if (0 == arguments.length) return this._version;
+ this._version = str;
+ flags = flags || '-V, --version';
+ this.option(flags, 'output the version number');
+ this.on('option:version', function() {
+ process.stdout.write(str + '\n');
+ process.exit(0);
+ });
+ return this;
+};
+
+/**
+ * Set the description to `str`.
+ *
+ * @param {String} str
+ * @return {String|Command}
+ * @api public
+ */
+
+Command.prototype.description = function(str) {
+ if (0 === arguments.length) return this._description;
+ this._description = str;
+ return this;
+};
+
+/**
+ * Set an alias for the command
+ *
+ * @param {String} alias
+ * @return {String|Command}
+ * @api public
+ */
+
+Command.prototype.alias = function(alias) {
+ var command = this;
+ if(this.commands.length !== 0) {
+ command = this.commands[this.commands.length - 1]
+ }
+
+ if (arguments.length === 0) return command._alias;
+
+ command._alias = alias;
+ return this;
+};
+
+/**
+ * Set / get the command usage `str`.
+ *
+ * @param {String} str
+ * @return {String|Command}
+ * @api public
+ */
+
+Command.prototype.usage = function(str) {
+ var args = this._args.map(function(arg) {
+ return humanReadableArgName(arg);
+ });
+
+ var usage = '[options]'
+ + (this.commands.length ? ' [command]' : '')
+ + (this._args.length ? ' ' + args.join(' ') : '');
+
+ if (0 == arguments.length) return this._usage || usage;
+ this._usage = str;
+
+ return this;
+};
+
+/**
+ * Get or set the name of the command
+ *
+ * @param {String} str
+ * @return {String|Command}
+ * @api public
+ */
+
+Command.prototype.name = function(str) {
+ if (0 === arguments.length) return this._name;
+ this._name = str;
+ return this;
+};
+
+/**
+ * Return the largest option length.
+ *
+ * @return {Number}
+ * @api private
+ */
+
+Command.prototype.largestOptionLength = function() {
+ return this.options.reduce(function(max, option) {
+ return Math.max(max, option.flags.length);
+ }, 0);
+};
+
+/**
+ * Return help for options.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Command.prototype.optionHelp = function() {
+ var width = this.largestOptionLength();
+
+ // Append the help information
+ return this.options.map(function(option) {
+ return pad(option.flags, width) + ' ' + option.description;
+ }).concat([pad('-h, --help', width) + ' ' + 'output usage information'])
+ .join('\n');
+};
+
+/**
+ * Return command help documentation.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Command.prototype.commandHelp = function() {
+ if (!this.commands.length) return '';
+
+ var commands = this.commands.filter(function(cmd) {
+ return !cmd._noHelp;
+ }).map(function(cmd) {
+ var args = cmd._args.map(function(arg) {
+ return humanReadableArgName(arg);
+ }).join(' ');
+
+ return [
+ cmd._name
+ + (cmd._alias ? '|' + cmd._alias : '')
+ + (cmd.options.length ? ' [options]' : '')
+ + ' ' + args
+ , cmd._description
+ ];
+ });
+
+ var width = commands.reduce(function(max, command) {
+ return Math.max(max, command[0].length);
+ }, 0);
+
+ return [
+ ''
+ , ' Commands:'
+ , ''
+ , commands.map(function(cmd) {
+ var desc = cmd[1] ? ' ' + cmd[1] : '';
+ return pad(cmd[0], width) + desc;
+ }).join('\n').replace(/^/gm, ' ')
+ , ''
+ ].join('\n');
+};
+
+/**
+ * Return program help documentation.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Command.prototype.helpInformation = function() {
+ var desc = [];
+ if (this._description) {
+ desc = [
+ ' ' + this._description
+ , ''
+ ];
+ }
+
+ var cmdName = this._name;
+ if (this._alias) {
+ cmdName = cmdName + '|' + this._alias;
+ }
+ var usage = [
+ ''
+ ,' Usage: ' + cmdName + ' ' + this.usage()
+ , ''
+ ];
+
+ var cmds = [];
+ var commandHelp = this.commandHelp();
+ if (commandHelp) cmds = [commandHelp];
+
+ var options = [
+ ''
+ , ' Options:'
+ , ''
+ , '' + this.optionHelp().replace(/^/gm, ' ')
+ , ''
+ ];
+
+ return usage
+ .concat(desc)
+ .concat(options)
+ .concat(cmds)
+ .join('\n');
+};
+
+/**
+ * Output help information for this command
+ *
+ * @api public
+ */
+
+Command.prototype.outputHelp = function(cb) {
+ if (!cb) {
+ cb = function(passthru) {
+ return passthru;
+ }
+ }
+ process.stdout.write(cb(this.helpInformation()));
+ this.emit('--help');
+};
+
+/**
+ * Output help information and exit.
+ *
+ * @api public
+ */
+
+Command.prototype.help = function(cb) {
+ this.outputHelp(cb);
+ process.exit();
+};
+
+/**
+ * Camel-case the given `flag`
+ *
+ * @param {String} flag
+ * @return {String}
+ * @api private
+ */
+
+function camelcase(flag) {
+ return flag.split('-').reduce(function(str, word) {
+ return str + word[0].toUpperCase() + word.slice(1);
+ });
+}
+
+/**
+ * Pad `str` to `width`.
+ *
+ * @param {String} str
+ * @param {Number} width
+ * @return {String}
+ * @api private
+ */
+
+function pad(str, width) {
+ var len = Math.max(0, width - str.length);
+ return str + Array(len + 1).join(' ');
+}
+
+/**
+ * Output help information if necessary
+ *
+ * @param {Command} command to output help for
+ * @param {Array} array of options to search for -h or --help
+ * @api private
+ */
+
+function outputHelpIfNecessary(cmd, options) {
+ options = options || [];
+ for (var i = 0; i < options.length; i++) {
+ if (options[i] == '--help' || options[i] == '-h') {
+ cmd.outputHelp();
+ process.exit(0);
+ }
+ }
+}
+
+/**
+ * Takes an argument an returns its human readable equivalent for help usage.
+ *
+ * @param {Object} arg
+ * @return {String}
+ * @api private
+ */
+
+function humanReadableArgName(arg) {
+ var nameOutput = arg.name + (arg.variadic === true ? '...' : '');
+
+ return arg.required
+ ? '<' + nameOutput + '>'
+ : '[' + nameOutput + ']'
+}
+
+// for versions before node v0.8 when there weren't `fs.existsSync`
+function exists(file) {
+ try {
+ if (fs.statSync(file).isFile()) {
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+}
+
diff --git a/node_modules/commander/package.json b/node_modules/commander/package.json
new file mode 100644
index 0000000..708f223
--- /dev/null
+++ b/node_modules/commander/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "commander",
+ "version": "2.11.0",
+ "description": "the complete solution for node.js command-line programs",
+ "keywords": [
+ "commander",
+ "command",
+ "option",
+ "parser"
+ ],
+ "author": "TJ Holowaychuk <tj@vision-media.ca>",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/tj/commander.js.git"
+ },
+ "devDependencies": {
+ "should": "^11.2.1",
+ "sinon": "^2.3.5"
+ },
+ "scripts": {
+ "test": "make test"
+ },
+ "main": "index",
+ "files": [
+ "index.js"
+ ],
+ "dependencies": {}
+}
diff --git a/node_modules/sizzle/AUTHORS.txt b/node_modules/sizzle/AUTHORS.txt
new file mode 100644
index 0000000..d510ad0
--- /dev/null
+++ b/node_modules/sizzle/AUTHORS.txt
@@ -0,0 +1,67 @@
+Authors ordered by first contribution
+
+John Resig <jeresig@gmail.com>
+Cheah Chu Yeow <chuyeow@gmail.com>
+Andrew Chalkley <andrew@chalkley.org>
+Fabio Buffoni <fabio.buffoni@bitmaster.it>
+Stefan Bauckmeier  <stefan@bauckmeier.de>
+Brandon Aaron <brandon.aaron@gmail.com>
+Anton Kovalyov <anton@kovalyov.net>
+Dušan B. Jovanovic <dbjdbj@gmail.com>
+Riccardo De Agostini <rdeago@gmail.com>
+Fabian Jakobs <fabian.jakobs@web.de>
+Karl Swedberg <kswedberg@gmail.com>
+Jake Archibald <jake.archibald@bbc.co.uk>
+Colin Snover <github.com@zetafleet.com>
+Anton Matzneller <obhvsbypqghgc@gmail.com>
+Dave Methvin <dave.methvin@gmail.com>
+Corey Frang <gnarf@gnarf.net>
+Mathias Bynens <mathias@qiwi.be>
+John Firebaugh <john_firebaugh@us.ibm.com>
+Timmy Willison <timmywillisn@gmail.com>
+Mike Sherov <mike.sherov@gmail.com>
+Rock Hymas <rock@fogcreek.com>
+Yehuda Katz <wycats@gmail.com>
+Jörn Zaefferer <joern.zaefferer@gmail.com>
+Richard Gibson <richard.gibson@gmail.com>
+Vitya Muhachev <vic99999@yandex.ru>
+Henri Wiechers <hwiechers@gmail.com>
+Alan Plum <github@ap.apsq.de>
+Chad Killingsworth <chadkillingsworth@missouristate.edu>
+Markus Staab <markus.staab@redaxo.de>
+Timo Tijhof <krinklemail@gmail.com>
+Diego Tres <diegotres@gmail.com>
+Jonathan Sampson <jjdsampson@gmail.com>
+Pascal Borreli <pascal@borreli.com>
+Daniel Herman <daniel.c.herman@gmail.com>
+Frederic Junod <frederic.junod@camptocamp.com>
+Mitch Foley <mitch@thefoley.net>
+Scott González <scott.gonzalez@gmail.com>
+Oleg Gaidarenko <markelog@gmail.com>
+Dan Burzo <danburzo@gmail.com>
+Goare Mao <mygoare@gmail.com>
+Dongseok Paeng <dongseok83.paeng@lge.com>
+Michał Gołębiowski-Owczarek <m.goleb@gmail.com>
+Philip Jägenstedt <philip@foolip.org>
+Chris Antaki <ChrisAntaki@gmail.com>
+Benjamin Tan <demoneaux@gmail.com>
+T.J. Crowder <tj.crowder@farsightsoftware.com>
+Anne-Gaelle Colom <coloma@westminster.ac.uk>
+Neftaly Hernandez <neftaly.hernandez@gmail.com>
+Jörn Wagner <joern.wagner@explicatis.com>
+Chris Rebert <code@rebertia.com>
+Colin Frick <colin@bash.li>
+John-David Dalton <john.david.dalton@gmail.com>
+Kevin Kirsche <Kev.Kirsche+GitHub@gmail.com>
+Steve Mao <maochenyan@gmail.com>
+Tom von Clef <thomas.vonclef@gmail.com>
+Josh Soref <apache@soref.com>
+Saptak Sengupta <saptak013@gmail.com>
+Jon Dufresne <jon.dufresne@gmail.com>
+Andrey Meshkov <ay.meshkov@gmail.com>
+Sébastien Règne <regseb@users.noreply.github.com>
+Andrey Meshkov <am@adguard.com>
+Siddharth Dungarwal <sd5869@gmail.com>
+wartmanm <3869625+wartmanm@users.noreply.github.com>
+Hoang <dangkyokhoang@gmail.com>
+JuanMa Ruiz <ruizjuanma@gmail.com>
diff --git a/node_modules/sizzle/LICENSE.txt b/node_modules/sizzle/LICENSE.txt
new file mode 100644
index 0000000..88fcd17
--- /dev/null
+++ b/node_modules/sizzle/LICENSE.txt
@@ -0,0 +1,36 @@
+Copyright JS Foundation and other contributors, https://js.foundation/
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/jquery/sizzle
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+All files located in the node_modules and external directories are
+externally maintained libraries used by this software which have their
+own licenses; we recommend you read them, as their terms may differ from
+the terms above.
diff --git a/node_modules/sizzle/README.md b/node_modules/sizzle/README.md
new file mode 100644
index 0000000..8ca764f
--- /dev/null
+++ b/node_modules/sizzle/README.md
@@ -0,0 +1,55 @@
+# Sizzle
+
+__A pure-JavaScript CSS selector engine designed to be easily dropped in to a host library.__
+
+- [More information](https://sizzlejs.com/)
+- [Documentation](https://github.com/jquery/sizzle/wiki/)
+- [Browser support](https://github.com/jquery/sizzle/wiki/#wiki-browsers)
+
+Contribution Guides
+---------------------------
+
+In the spirit of open source software development, jQuery always encourages community code contribution. To help you get started and before you jump into writing code, be sure to read these important contribution guidelines thoroughly:
+
+1. [Getting Involved](https://contribute.jquery.org/)
+2. [JavaScript Style Guide](https://contribute.jquery.org/style-guide/js/)
+3. [Writing Code for jQuery Organization Projects](https://contribute.jquery.org/code/)
+
+What you need to build Sizzle
+---------------------------
+
+In order to build Sizzle, you should have Node.js/npm latest and git 1.7 or later (earlier versions might work OK, but are not tested).
+
+For Windows you have to download and install [git](http://git-scm.com/downloads) and [Node.js](https://nodejs.org/download/).
+
+Mac OS users should install [Homebrew](http://mxcl.github.com/homebrew/). Once Homebrew is installed, run `brew install git` to install git,
+and `brew install node` to install Node.js.
+
+Linux/BSD users should use their appropriate package managers to install git and Node.js, or build from source
+if you swing that way. Easy-peasy.
+
+
+How to build Sizzle
+----------------------------
+
+Clone a copy of the main Sizzle git repo by running:
+
+```bash
+git clone git://github.com/jquery/sizzle.git
+```
+
+In the `sizzle/dist` folder you will find build version of sizzle along with the minified copy and associated map file.
+
+Testing
+----------------------------
+
+- Run `npm install`, it's also preferable (but not necessarily) to globally install `grunt-cli` package – `npm install -g grunt-cli`
+- Open `test/index.html` in the browser. Or run `npm test`/`grunt test` on the command line, if environment variables `BROWSER_STACK_USERNAME` and `BROWSER_STACK_ACCESS_KEY` are set up, it will attempt to use [Browserstack](https://www.browserstack.com/) service (you will need to install java on your machine so browserstack could connect to your local server), otherwise [PhantomJS](http://phantomjs.org/) will be used.
+- The actual unit tests are in the `test/unit` directory.
+
+Developing with [grunt](http://gruntjs.com)
+----------------------------
+
+- `npm run build` or `grunt` will lint, build, test, and compare the sizes of the built files.
+- `npm start` or `grunt start` can be run to re-lint, re-build, and re-test files as you change them.
+- `grunt -help` will show other available commands.
diff --git a/node_modules/sizzle/dist/sizzle.js b/node_modules/sizzle/dist/sizzle.js
new file mode 100644
index 0000000..456b233
--- /dev/null
+++ b/node_modules/sizzle/dist/sizzle.js
@@ -0,0 +1,2514 @@
+/*!
+ * Sizzle CSS Selector Engine v2.3.10
+ * https://sizzlejs.com/
+ *
+ * Copyright JS Foundation and other contributors
+ * Released under the MIT license
+ * https://js.foundation/
+ *
+ * Date: 2023-02-14
+ */
+( function( window ) {
+var i,
+ support,
+ Expr,
+ getText,
+ isXML,
+ tokenize,
+ compile,
+ select,
+ outermostContext,
+ sortInput,
+ hasDuplicate,
+
+ // Local document vars
+ setDocument,
+ document,
+ docElem,
+ documentIsHTML,
+ rbuggyQSA,
+ rbuggyMatches,
+ matches,
+ contains,
+
+ // Instance-specific data
+ expando = "sizzle" + 1 * new Date(),
+ preferredDoc = window.document,
+ dirruns = 0,
+ done = 0,
+ classCache = createCache(),
+ tokenCache = createCache(),
+ compilerCache = createCache(),
+ nonnativeSelectorCache = createCache(),
+ sortOrder = function( a, b ) {
+ if ( a === b ) {
+ hasDuplicate = true;
+ }
+ return 0;
+ },
+
+ // Instance methods
+ hasOwn = ( {} ).hasOwnProperty,
+ arr = [],
+ pop = arr.pop,
+ pushNative = arr.push,
+ push = arr.push,
+ slice = arr.slice,
+
+ // Use a stripped-down indexOf as it's faster than native
+ // https://jsperf.com/thor-indexof-vs-for/5
+ indexOf = function( list, elem ) {
+ var i = 0,
+ len = list.length;
+ for ( ; i < len; i++ ) {
+ if ( list[ i ] === elem ) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" +
+ "ismap|loop|multiple|open|readonly|required|scoped",
+
+ // Regular expressions
+
+ // http://www.w3.org/TR/css3-selectors/#whitespace
+ whitespace = "[\\x20\\t\\r\\n\\f]",
+
+ // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
+ identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace +
+ "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",
+
+ // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
+ attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
+
+ // Operator (capture 2)
+ "*([*^$|!~]?=)" + whitespace +
+
+ // "Attribute values must be CSS identifiers [capture 5]
+ // or strings [capture 3 or capture 4]"
+ "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" +
+ whitespace + "*\\]",
+
+ pseudos = ":(" + identifier + ")(?:\\((" +
+
+ // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
+ // 1. quoted (capture 3; capture 4 or capture 5)
+ "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
+
+ // 2. simple (capture 6)
+ "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
+
+ // 3. anything else (capture 2)
+ ".*" +
+ ")\\)|)",
+
+ // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+ rwhitespace = new RegExp( whitespace + "+", "g" ),
+ rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" +
+ whitespace + "+$", "g" ),
+
+ rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+ rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace +
+ "*" ),
+ rdescend = new RegExp( whitespace + "|>" ),
+
+ rpseudo = new RegExp( pseudos ),
+ ridentifier = new RegExp( "^" + identifier + "$" ),
+
+ matchExpr = {
+ "ID": new RegExp( "^#(" + identifier + ")" ),
+ "CLASS": new RegExp( "^\\.(" + identifier + ")" ),
+ "TAG": new RegExp( "^(" + identifier + "|[*])" ),
+ "ATTR": new RegExp( "^" + attributes ),
+ "PSEUDO": new RegExp( "^" + pseudos ),
+ "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" +
+ whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" +
+ whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+ "bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+
+ // For use in libraries implementing .is()
+ // We use this for POS matching in `select`
+ "needsContext": new RegExp( "^" + whitespace +
+ "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace +
+ "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+ },
+
+ rhtml = /HTML$/i,
+ rinputs = /^(?:input|select|textarea|button)$/i,
+ rheader = /^h\d$/i,
+
+ rnative = /^[^{]+\{\s*\[native \w/,
+
+ // Easily-parseable/retrievable ID or TAG or CLASS selectors
+ rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+ rsibling = /[+~]/,
+
+ // CSS escapes
+ // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+ runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ),
+ funescape = function( escape, nonHex ) {
+ var high = "0x" + escape.slice( 1 ) - 0x10000;
+
+ return nonHex ?
+
+ // Strip the backslash prefix from a non-hex escape sequence
+ nonHex :
+
+ // Replace a hexadecimal escape sequence with the encoded Unicode code point
+ // Support: IE <=11+
+ // For values outside the Basic Multilingual Plane (BMP), manually construct a
+ // surrogate pair
+ high < 0 ?
+ String.fromCharCode( high + 0x10000 ) :
+ String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+ },
+
+ // CSS string/identifier serialization
+ // https://drafts.csswg.org/cssom/#common-serializing-idioms
+ rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,
+ fcssescape = function( ch, asCodePoint ) {
+ if ( asCodePoint ) {
+
+ // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER
+ if ( ch === "\0" ) {
+ return "\uFFFD";
+ }
+
+ // Control characters and (dependent upon position) numbers get escaped as code points
+ return ch.slice( 0, -1 ) + "\\" +
+ ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
+ }
+
+ // Other potentially-special ASCII characters get backslash-escaped
+ return "\\" + ch;
+ },
+
+ // Used for iframes
+ // See setDocument()
+ // Removing the function wrapper causes a "Permission Denied"
+ // error in IE
+ unloadHandler = function() {
+ setDocument();
+ },
+
+ inDisabledFieldset = addCombinator(
+ function( elem ) {
+ return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset";
+ },
+ { dir: "parentNode", next: "legend" }
+ );
+
+// Optimize for push.apply( _, NodeList )
+try {
+ push.apply(
+ ( arr = slice.call( preferredDoc.childNodes ) ),
+ preferredDoc.childNodes
+ );
+
+ // Support: Android<4.0
+ // Detect silently failing push.apply
+ // eslint-disable-next-line no-unused-expressions
+ arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+ push = { apply: arr.length ?
+
+ // Leverage slice if possible
+ function( target, els ) {
+ pushNative.apply( target, slice.call( els ) );
+ } :
+
+ // Support: IE<9
+ // Otherwise append directly
+ function( target, els ) {
+ var j = target.length,
+ i = 0;
+
+ // Can't trust NodeList.length
+ while ( ( target[ j++ ] = els[ i++ ] ) ) {}
+ target.length = j - 1;
+ }
+ };
+}
+
+function Sizzle( selector, context, results, seed ) {
+ var m, i, elem, nid, match, groups, newSelector,
+ newContext = context && context.ownerDocument,
+
+ // nodeType defaults to 9, since context defaults to document
+ nodeType = context ? context.nodeType : 9;
+
+ results = results || [];
+
+ // Return early from calls with invalid selector or context
+ if ( typeof selector !== "string" || !selector ||
+ nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
+
+ return results;
+ }
+
+ // Try to shortcut find operations (as opposed to filters) in HTML documents
+ if ( !seed ) {
+ setDocument( context );
+ context = context || document;
+
+ if ( documentIsHTML ) {
+
+ // If the selector is sufficiently simple, try using a "get*By*" DOM method
+ // (excepting DocumentFragment context, where the methods don't exist)
+ if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) {
+
+ // ID selector
+ if ( ( m = match[ 1 ] ) ) {
+
+ // Document context
+ if ( nodeType === 9 ) {
+ if ( ( elem = context.getElementById( m ) ) ) {
+
+ // Support: IE, Opera, Webkit
+ // TODO: identify versions
+ // getElementById can match elements by name instead of ID
+ if ( elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ } else {
+ return results;
+ }
+
+ // Element context
+ } else {
+
+ // Support: IE, Opera, Webkit
+ // TODO: identify versions
+ // getElementById can match elements by name instead of ID
+ if ( newContext && ( elem = newContext.getElementById( m ) ) &&
+ contains( context, elem ) &&
+ elem.id === m ) {
+
+ results.push( elem );
+ return results;
+ }
+ }
+
+ // Type selector
+ } else if ( match[ 2 ] ) {
+ push.apply( results, context.getElementsByTagName( selector ) );
+ return results;
+
+ // Class selector
+ } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName &&
+ context.getElementsByClassName ) {
+
+ push.apply( results, context.getElementsByClassName( m ) );
+ return results;
+ }
+ }
+
+ // Take advantage of querySelectorAll
+ if ( support.qsa &&
+ !nonnativeSelectorCache[ selector + " " ] &&
+ ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) &&
+
+ // Support: IE 8 only
+ // Exclude object elements
+ ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) {
+
+ newSelector = selector;
+ newContext = context;
+
+ // qSA considers elements outside a scoping root when evaluating child or
+ // descendant combinators, which is not what we want.
+ // In such cases, we work around the behavior by prefixing every selector in the
+ // list with an ID selector referencing the scope context.
+ // The technique has to be used as well when a leading combinator is used
+ // as such selectors are not recognized by querySelectorAll.
+ // Thanks to Andrew Dupont for this technique.
+ if ( nodeType === 1 &&
+ ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) {
+
+ // Expand context for sibling selectors
+ newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
+ context;
+
+ // We can use :scope instead of the ID hack if the browser
+ // supports it & if we're not changing the context.
+ if ( newContext !== context || !support.scope ) {
+
+ // Capture the context ID, setting it first if necessary
+ if ( ( nid = context.getAttribute( "id" ) ) ) {
+ nid = nid.replace( rcssescape, fcssescape );
+ } else {
+ context.setAttribute( "id", ( nid = expando ) );
+ }
+ }
+
+ // Prefix every selector in the list
+ groups = tokenize( selector );
+ i = groups.length;
+ while ( i-- ) {
+ groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " +
+ toSelector( groups[ i ] );
+ }
+ newSelector = groups.join( "," );
+ }
+
+ try {
+ push.apply( results,
+ newContext.querySelectorAll( newSelector )
+ );
+ return results;
+ } catch ( qsaError ) {
+ nonnativeSelectorCache( selector, true );
+ } finally {
+ if ( nid === expando ) {
+ context.removeAttribute( "id" );
+ }
+ }
+ }
+ }
+ }
+
+ // All others
+ return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {function(string, object)} Returns the Object data after storing it on itself with
+ * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ * deleting the oldest entry
+ */
+function createCache() {
+ var keys = [];
+
+ function cache( key, value ) {
+
+ // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+ if ( keys.push( key + " " ) > Expr.cacheLength ) {
+
+ // Only keep the most recent entries
+ delete cache[ keys.shift() ];
+ }
+ return ( cache[ key + " " ] = value );
+ }
+ return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+ fn[ expando ] = true;
+ return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created element and returns a boolean result
+ */
+function assert( fn ) {
+ var el = document.createElement( "fieldset" );
+
+ try {
+ return !!fn( el );
+ } catch ( e ) {
+ return false;
+ } finally {
+
+ // Remove from its parent by default
+ if ( el.parentNode ) {
+ el.parentNode.removeChild( el );
+ }
+
+ // release memory in IE
+ el = null;
+ }
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied
+ */
+function addHandle( attrs, handler ) {
+ var arr = attrs.split( "|" ),
+ i = arr.length;
+
+ while ( i-- ) {
+ Expr.attrHandle[ arr[ i ] ] = handler;
+ }
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
+ */
+function siblingCheck( a, b ) {
+ var cur = b && a,
+ diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+ a.sourceIndex - b.sourceIndex;
+
+ // Use IE sourceIndex if available on both nodes
+ if ( diff ) {
+ return diff;
+ }
+
+ // Check if b follows a
+ if ( cur ) {
+ while ( ( cur = cur.nextSibling ) ) {
+ if ( cur === b ) {
+ return -1;
+ }
+ }
+ }
+
+ return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return ( name === "input" || name === "button" ) && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for :enabled/:disabled
+ * @param {Boolean} disabled true for :disabled; false for :enabled
+ */
+function createDisabledPseudo( disabled ) {
+
+ // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable
+ return function( elem ) {
+
+ // Only certain elements can match :enabled or :disabled
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled
+ if ( "form" in elem ) {
+
+ // Check for inherited disabledness on relevant non-disabled elements:
+ // * listed form-associated elements in a disabled fieldset
+ // https://html.spec.whatwg.org/multipage/forms.html#category-listed
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
+ // * option elements in a disabled optgroup
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled
+ // All such elements have a "form" property.
+ if ( elem.parentNode && elem.disabled === false ) {
+
+ // Option elements defer to a parent optgroup if present
+ if ( "label" in elem ) {
+ if ( "label" in elem.parentNode ) {
+ return elem.parentNode.disabled === disabled;
+ } else {
+ return elem.disabled === disabled;
+ }
+ }
+
+ // Support: IE 6 - 11
+ // Use the isDisabled shortcut property to check for disabled fieldset ancestors
+ return elem.isDisabled === disabled ||
+
+ // Where there is no isDisabled, check manually
+ /* jshint -W018 */
+ elem.isDisabled !== !disabled &&
+ inDisabledFieldset( elem ) === disabled;
+ }
+
+ return elem.disabled === disabled;
+
+ // Try to winnow out elements that can't be disabled before trusting the disabled property.
+ // Some victims get caught in our net (label, legend, menu, track), but it shouldn't
+ // even exist on them, let alone have a boolean value.
+ } else if ( "label" in elem ) {
+ return elem.disabled === disabled;
+ }
+
+ // Remaining elements are neither :enabled nor :disabled
+ return false;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+ return markFunction( function( argument ) {
+ argument = +argument;
+ return markFunction( function( seed, matches ) {
+ var j,
+ matchIndexes = fn( [], seed.length, argument ),
+ i = matchIndexes.length;
+
+ // Match elements found at the specified indexes
+ while ( i-- ) {
+ if ( seed[ ( j = matchIndexes[ i ] ) ] ) {
+ seed[ j ] = !( matches[ j ] = seed[ j ] );
+ }
+ }
+ } );
+ } );
+}
+
+/**
+ * Checks a node for validity as a Sizzle context
+ * @param {Element|Object=} context
+ * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
+ */
+function testContext( context ) {
+ return context && typeof context.getElementsByTagName !== "undefined" && context;
+}
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Detects XML nodes
+ * @param {Element|Object} elem An element or a document
+ * @returns {Boolean} True iff elem is a non-HTML XML node
+ */
+isXML = Sizzle.isXML = function( elem ) {
+ var namespace = elem && elem.namespaceURI,
+ docElem = elem && ( elem.ownerDocument || elem ).documentElement;
+
+ // Support: IE <=8
+ // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes
+ // https://bugs.jquery.com/ticket/4833
+ return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" );
+};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+ var hasCompare, subWindow,
+ doc = node ? node.ownerDocument || node : preferredDoc;
+
+ // Return early if doc is invalid or already selected
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) {
+ return document;
+ }
+
+ // Update global variables
+ document = doc;
+ docElem = document.documentElement;
+ documentIsHTML = !isXML( document );
+
+ // Support: IE 9 - 11+, Edge 12 - 18+
+ // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( preferredDoc != document &&
+ ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) {
+
+ // Support: IE 11, Edge
+ if ( subWindow.addEventListener ) {
+ subWindow.addEventListener( "unload", unloadHandler, false );
+
+ // Support: IE 9 - 10 only
+ } else if ( subWindow.attachEvent ) {
+ subWindow.attachEvent( "onunload", unloadHandler );
+ }
+ }
+
+ // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only,
+ // Safari 4 - 5 only, Opera <=11.6 - 12.x only
+ // IE/Edge & older browsers don't support the :scope pseudo-class.
+ // Support: Safari 6.0 only
+ // Safari 6.0 supports :scope but it's an alias of :root there.
+ support.scope = assert( function( el ) {
+ docElem.appendChild( el ).appendChild( document.createElement( "div" ) );
+ return typeof el.querySelectorAll !== "undefined" &&
+ !el.querySelectorAll( ":scope fieldset div" ).length;
+ } );
+
+ // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+
+ // Make sure the the `:has()` argument is parsed unforgivingly.
+ // We include `*` in the test to detect buggy implementations that are
+ // _selectively_ forgiving (specifically when the list includes at least
+ // one valid selector).
+ // Note that we treat complete lack of support for `:has()` as if it were
+ // spec-compliant support, which is fine because use of `:has()` in such
+ // environments will fail in the qSA path and fall back to jQuery traversal
+ // anyway.
+ support.cssHas = assert( function() {
+ try {
+ document.querySelector( ":has(*,:jqfake)" );
+ return false;
+ } catch ( e ) {
+ return true;
+ }
+ } );
+
+ /* Attributes
+ ---------------------------------------------------------------------- */
+
+ // Support: IE<8
+ // Verify that getAttribute really returns attributes and not properties
+ // (excepting IE8 booleans)
+ support.attributes = assert( function( el ) {
+ el.className = "i";
+ return !el.getAttribute( "className" );
+ } );
+
+ /* getElement(s)By*
+ ---------------------------------------------------------------------- */
+
+ // Check if getElementsByTagName("*") returns only elements
+ support.getElementsByTagName = assert( function( el ) {
+ el.appendChild( document.createComment( "" ) );
+ return !el.getElementsByTagName( "*" ).length;
+ } );
+
+ // Support: IE<9
+ support.getElementsByClassName = rnative.test( document.getElementsByClassName );
+
+ // Support: IE<10
+ // Check if getElementById returns elements by name
+ // The broken getElementById methods don't pick up programmatically-set names,
+ // so use a roundabout getElementsByName test
+ support.getById = assert( function( el ) {
+ docElem.appendChild( el ).id = expando;
+ return !document.getElementsByName || !document.getElementsByName( expando ).length;
+ } );
+
+ // ID filter and find
+ if ( support.getById ) {
+ Expr.filter[ "ID" ] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ return elem.getAttribute( "id" ) === attrId;
+ };
+ };
+ Expr.find[ "ID" ] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var elem = context.getElementById( id );
+ return elem ? [ elem ] : [];
+ }
+ };
+ } else {
+ Expr.filter[ "ID" ] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ var node = typeof elem.getAttributeNode !== "undefined" &&
+ elem.getAttributeNode( "id" );
+ return node && node.value === attrId;
+ };
+ };
+
+ // Support: IE 6 - 7 only
+ // getElementById is not reliable as a find shortcut
+ Expr.find[ "ID" ] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var node, i, elems,
+ elem = context.getElementById( id );
+
+ if ( elem ) {
+
+ // Verify the id attribute
+ node = elem.getAttributeNode( "id" );
+ if ( node && node.value === id ) {
+ return [ elem ];
+ }
+
+ // Fall back on getElementsByName
+ elems = context.getElementsByName( id );
+ i = 0;
+ while ( ( elem = elems[ i++ ] ) ) {
+ node = elem.getAttributeNode( "id" );
+ if ( node && node.value === id ) {
+ return [ elem ];
+ }
+ }
+ }
+
+ return [];
+ }
+ };
+ }
+
+ // Tag
+ Expr.find[ "TAG" ] = support.getElementsByTagName ?
+ function( tag, context ) {
+ if ( typeof context.getElementsByTagName !== "undefined" ) {
+ return context.getElementsByTagName( tag );
+
+ // DocumentFragment nodes don't have gEBTN
+ } else if ( support.qsa ) {
+ return context.querySelectorAll( tag );
+ }
+ } :
+
+ function( tag, context ) {
+ var elem,
+ tmp = [],
+ i = 0,
+
+ // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
+ results = context.getElementsByTagName( tag );
+
+ // Filter out possible comments
+ if ( tag === "*" ) {
+ while ( ( elem = results[ i++ ] ) ) {
+ if ( elem.nodeType === 1 ) {
+ tmp.push( elem );
+ }
+ }
+
+ return tmp;
+ }
+ return results;
+ };
+
+ // Class
+ Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) {
+ if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
+ return context.getElementsByClassName( className );
+ }
+ };
+
+ /* QSA/matchesSelector
+ ---------------------------------------------------------------------- */
+
+ // QSA and matchesSelector support
+
+ // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+ rbuggyMatches = [];
+
+ // qSa(:focus) reports false when true (Chrome 21)
+ // We allow this because of a bug in IE8/9 that throws an error
+ // whenever `document.activeElement` is accessed on an iframe
+ // So, we allow :focus to pass through QSA all the time to avoid the IE error
+ // See https://bugs.jquery.com/ticket/13378
+ rbuggyQSA = [];
+
+ if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) {
+
+ // Build QSA regex
+ // Regex strategy adopted from Diego Perini
+ assert( function( el ) {
+
+ var input;
+
+ // Select is set to empty string on purpose
+ // This is to test IE's treatment of not explicitly
+ // setting a boolean content attribute,
+ // since its presence should be enough
+ // https://bugs.jquery.com/ticket/12359
+ docElem.appendChild( el ).innerHTML = "<a id='" + expando + "'></a>" +
+ "<select id='" + expando + "-\r\\' msallowcapture=''>" +
+ "<option selected=''></option></select>";
+
+ // Support: IE8, Opera 11-12.16
+ // Nothing should be selected when empty strings follow ^= or $= or *=
+ // The test attribute must be unknown in Opera but "safe" for WinRT
+ // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
+ if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) {
+ rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+ }
+
+ // Support: IE8
+ // Boolean attributes and "value" are not treated correctly
+ if ( !el.querySelectorAll( "[selected]" ).length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+ }
+
+ // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
+ if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
+ rbuggyQSA.push( "~=" );
+ }
+
+ // Support: IE 11+, Edge 15 - 18+
+ // IE 11/Edge don't find elements on a `[name='']` query in some cases.
+ // Adding a temporary attribute to the document before the selection works
+ // around the issue.
+ // Interestingly, IE 10 & older don't seem to have the issue.
+ input = document.createElement( "input" );
+ input.setAttribute( "name", "" );
+ el.appendChild( input );
+ if ( !el.querySelectorAll( "[name='']" ).length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" +
+ whitespace + "*(?:''|\"\")" );
+ }
+
+ // Webkit/Opera - :checked should return selected option elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ // IE8 throws error here and will not see later tests
+ if ( !el.querySelectorAll( ":checked" ).length ) {
+ rbuggyQSA.push( ":checked" );
+ }
+
+ // Support: Safari 8+, iOS 8+
+ // https://bugs.webkit.org/show_bug.cgi?id=136851
+ // In-page `selector#id sibling-combinator selector` fails
+ if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) {
+ rbuggyQSA.push( ".#.+[+~]" );
+ }
+
+ // Support: Firefox <=3.6 - 5 only
+ // Old Firefox doesn't throw on a badly-escaped identifier.
+ el.querySelectorAll( "\\\f" );
+ rbuggyQSA.push( "[\\r\\n\\f]" );
+ } );
+
+ assert( function( el ) {
+ el.innerHTML = "<a href='' disabled='disabled'></a>" +
+ "<select disabled='disabled'><option/></select>";
+
+ // Support: Windows 8 Native Apps
+ // The type and name attributes are restricted during .innerHTML assignment
+ var input = document.createElement( "input" );
+ input.setAttribute( "type", "hidden" );
+ el.appendChild( input ).setAttribute( "name", "D" );
+
+ // Support: IE8
+ // Enforce case-sensitivity of name attribute
+ if ( el.querySelectorAll( "[name=d]" ).length ) {
+ rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
+ }
+
+ // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+ // IE8 throws error here and will not see later tests
+ if ( el.querySelectorAll( ":enabled" ).length !== 2 ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Support: IE9-11+
+ // IE's :disabled selector does not pick up the children of disabled fieldsets
+ docElem.appendChild( el ).disabled = true;
+ if ( el.querySelectorAll( ":disabled" ).length !== 2 ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Support: Opera 10 - 11 only
+ // Opera 10-11 does not throw on post-comma invalid pseudos
+ el.querySelectorAll( "*,:x" );
+ rbuggyQSA.push( ",.*:" );
+ } );
+ }
+
+ if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches ||
+ docElem.webkitMatchesSelector ||
+ docElem.mozMatchesSelector ||
+ docElem.oMatchesSelector ||
+ docElem.msMatchesSelector ) ) ) ) {
+
+ assert( function( el ) {
+
+ // Check to see if it's possible to do matchesSelector
+ // on a disconnected node (IE 9)
+ support.disconnectedMatch = matches.call( el, "*" );
+
+ // This should fail with an exception
+ // Gecko does not error, returns false instead
+ matches.call( el, "[s!='']:x" );
+ rbuggyMatches.push( "!=", pseudos );
+ } );
+ }
+
+ if ( !support.cssHas ) {
+
+ // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+
+ // Our regular `try-catch` mechanism fails to detect natively-unsupported
+ // pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`)
+ // in browsers that parse the `:has()` argument as a forgiving selector list.
+ // https://drafts.csswg.org/selectors/#relational now requires the argument
+ // to be parsed unforgivingly, but browsers have not yet fully adjusted.
+ rbuggyQSA.push( ":has" );
+ }
+
+ rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) );
+ rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) );
+
+ /* Contains
+ ---------------------------------------------------------------------- */
+ hasCompare = rnative.test( docElem.compareDocumentPosition );
+
+ // Element contains another
+ // Purposefully self-exclusive
+ // As in, an element does not contain itself
+ contains = hasCompare || rnative.test( docElem.contains ) ?
+ function( a, b ) {
+
+ // Support: IE <9 only
+ // IE doesn't have `contains` on `document` so we need to check for
+ // `documentElement` presence.
+ // We need to fall back to `a` when `documentElement` is missing
+ // as `ownerDocument` of elements within `<template/>` may have
+ // a null one - a default behavior of all modern browsers.
+ var adown = a.nodeType === 9 && a.documentElement || a,
+ bup = b && b.parentNode;
+ return a === bup || !!( bup && bup.nodeType === 1 && (
+ adown.contains ?
+ adown.contains( bup ) :
+ a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+ ) );
+ } :
+ function( a, b ) {
+ if ( b ) {
+ while ( ( b = b.parentNode ) ) {
+ if ( b === a ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ /* Sorting
+ ---------------------------------------------------------------------- */
+
+ // Document order sorting
+ sortOrder = hasCompare ?
+ function( a, b ) {
+
+ // Flag for duplicate removal
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ // Sort on method existence if only one input has compareDocumentPosition
+ var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
+ if ( compare ) {
+ return compare;
+ }
+
+ // Calculate position if both inputs belong to the same document
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ?
+ a.compareDocumentPosition( b ) :
+
+ // Otherwise we know they are disconnected
+ 1;
+
+ // Disconnected nodes
+ if ( compare & 1 ||
+ ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) {
+
+ // Choose the first element that is related to our preferred document
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( a == document || a.ownerDocument == preferredDoc &&
+ contains( preferredDoc, a ) ) {
+ return -1;
+ }
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( b == document || b.ownerDocument == preferredDoc &&
+ contains( preferredDoc, b ) ) {
+ return 1;
+ }
+
+ // Maintain original order
+ return sortInput ?
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+ 0;
+ }
+
+ return compare & 4 ? -1 : 1;
+ } :
+ function( a, b ) {
+
+ // Exit early if the nodes are identical
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ var cur,
+ i = 0,
+ aup = a.parentNode,
+ bup = b.parentNode,
+ ap = [ a ],
+ bp = [ b ];
+
+ // Parentless nodes are either documents or disconnected
+ if ( !aup || !bup ) {
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ /* eslint-disable eqeqeq */
+ return a == document ? -1 :
+ b == document ? 1 :
+ /* eslint-enable eqeqeq */
+ aup ? -1 :
+ bup ? 1 :
+ sortInput ?
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+ 0;
+
+ // If the nodes are siblings, we can do a quick check
+ } else if ( aup === bup ) {
+ return siblingCheck( a, b );
+ }
+
+ // Otherwise we need full lists of their ancestors for comparison
+ cur = a;
+ while ( ( cur = cur.parentNode ) ) {
+ ap.unshift( cur );
+ }
+ cur = b;
+ while ( ( cur = cur.parentNode ) ) {
+ bp.unshift( cur );
+ }
+
+ // Walk down the tree looking for a discrepancy
+ while ( ap[ i ] === bp[ i ] ) {
+ i++;
+ }
+
+ return i ?
+
+ // Do a sibling check if the nodes have a common ancestor
+ siblingCheck( ap[ i ], bp[ i ] ) :
+
+ // Otherwise nodes in our document sort first
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ /* eslint-disable eqeqeq */
+ ap[ i ] == preferredDoc ? -1 :
+ bp[ i ] == preferredDoc ? 1 :
+ /* eslint-enable eqeqeq */
+ 0;
+ };
+
+ return document;
+};
+
+Sizzle.matches = function( expr, elements ) {
+ return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+ setDocument( elem );
+
+ if ( support.matchesSelector && documentIsHTML &&
+ !nonnativeSelectorCache[ expr + " " ] &&
+ ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+ ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
+
+ try {
+ var ret = matches.call( elem, expr );
+
+ // IE 9's matchesSelector returns false on disconnected nodes
+ if ( ret || support.disconnectedMatch ||
+
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9
+ elem.document && elem.document.nodeType !== 11 ) {
+ return ret;
+ }
+ } catch ( e ) {
+ nonnativeSelectorCache( expr, true );
+ }
+ }
+
+ return Sizzle( expr, document, null, [ elem ] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+
+ // Set document vars if needed
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( ( context.ownerDocument || context ) != document ) {
+ setDocument( context );
+ }
+ return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+
+ // Set document vars if needed
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( ( elem.ownerDocument || elem ) != document ) {
+ setDocument( elem );
+ }
+
+ var fn = Expr.attrHandle[ name.toLowerCase() ],
+
+ // Don't get fooled by Object.prototype properties (jQuery #13807)
+ val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+ fn( elem, name, !documentIsHTML ) :
+ undefined;
+
+ return val !== undefined ?
+ val :
+ support.attributes || !documentIsHTML ?
+ elem.getAttribute( name ) :
+ ( val = elem.getAttributeNode( name ) ) && val.specified ?
+ val.value :
+ null;
+};
+
+Sizzle.escape = function( sel ) {
+ return ( sel + "" ).replace( rcssescape, fcssescape );
+};
+
+Sizzle.error = function( msg ) {
+ throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+ var elem,
+ duplicates = [],
+ j = 0,
+ i = 0;
+
+ // Unless we *know* we can detect duplicates, assume their presence
+ hasDuplicate = !support.detectDuplicates;
+ sortInput = !support.sortStable && results.slice( 0 );
+ results.sort( sortOrder );
+
+ if ( hasDuplicate ) {
+ while ( ( elem = results[ i++ ] ) ) {
+ if ( elem === results[ i ] ) {
+ j = duplicates.push( i );
+ }
+ }
+ while ( j-- ) {
+ results.splice( duplicates[ j ], 1 );
+ }
+ }
+
+ // Clear input after sorting to release objects
+ // See https://github.com/jquery/sizzle/pull/225
+ sortInput = null;
+
+ return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+ var node,
+ ret = "",
+ i = 0,
+ nodeType = elem.nodeType;
+
+ if ( !nodeType ) {
+
+ // If no nodeType, this is expected to be an array
+ while ( ( node = elem[ i++ ] ) ) {
+
+ // Do not traverse comment nodes
+ ret += getText( node );
+ }
+ } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+
+ // Use textContent for elements
+ // innerText usage removed for consistency of new lines (jQuery #11153)
+ if ( typeof elem.textContent === "string" ) {
+ return elem.textContent;
+ } else {
+
+ // Traverse its children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ ret += getText( elem );
+ }
+ }
+ } else if ( nodeType === 3 || nodeType === 4 ) {
+ return elem.nodeValue;
+ }
+
+ // Do not include comment or processing instruction nodes
+
+ return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+ // Can be adjusted by the user
+ cacheLength: 50,
+
+ createPseudo: markFunction,
+
+ match: matchExpr,
+
+ attrHandle: {},
+
+ find: {},
+
+ relative: {
+ ">": { dir: "parentNode", first: true },
+ " ": { dir: "parentNode" },
+ "+": { dir: "previousSibling", first: true },
+ "~": { dir: "previousSibling" }
+ },
+
+ preFilter: {
+ "ATTR": function( match ) {
+ match[ 1 ] = match[ 1 ].replace( runescape, funescape );
+
+ // Move the given value to match[3] whether quoted or unquoted
+ match[ 3 ] = ( match[ 3 ] || match[ 4 ] ||
+ match[ 5 ] || "" ).replace( runescape, funescape );
+
+ if ( match[ 2 ] === "~=" ) {
+ match[ 3 ] = " " + match[ 3 ] + " ";
+ }
+
+ return match.slice( 0, 4 );
+ },
+
+ "CHILD": function( match ) {
+
+ /* matches from matchExpr["CHILD"]
+ 1 type (only|nth|...)
+ 2 what (child|of-type)
+ 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+ 4 xn-component of xn+y argument ([+-]?\d*n|)
+ 5 sign of xn-component
+ 6 x of xn-component
+ 7 sign of y-component
+ 8 y of y-component
+ */
+ match[ 1 ] = match[ 1 ].toLowerCase();
+
+ if ( match[ 1 ].slice( 0, 3 ) === "nth" ) {
+
+ // nth-* requires argument
+ if ( !match[ 3 ] ) {
+ Sizzle.error( match[ 0 ] );
+ }
+
+ // numeric x and y parameters for Expr.filter.CHILD
+ // remember that false/true cast respectively to 0/1
+ match[ 4 ] = +( match[ 4 ] ?
+ match[ 5 ] + ( match[ 6 ] || 1 ) :
+ 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) );
+ match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" );
+
+ // other types prohibit arguments
+ } else if ( match[ 3 ] ) {
+ Sizzle.error( match[ 0 ] );
+ }
+
+ return match;
+ },
+
+ "PSEUDO": function( match ) {
+ var excess,
+ unquoted = !match[ 6 ] && match[ 2 ];
+
+ if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) {
+ return null;
+ }
+
+ // Accept quoted arguments as-is
+ if ( match[ 3 ] ) {
+ match[ 2 ] = match[ 4 ] || match[ 5 ] || "";
+
+ // Strip excess characters from unquoted arguments
+ } else if ( unquoted && rpseudo.test( unquoted ) &&
+
+ // Get excess from tokenize (recursively)
+ ( excess = tokenize( unquoted, true ) ) &&
+
+ // advance to the next closing parenthesis
+ ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) {
+
+ // excess is a negative index
+ match[ 0 ] = match[ 0 ].slice( 0, excess );
+ match[ 2 ] = unquoted.slice( 0, excess );
+ }
+
+ // Return only captures needed by the pseudo filter method (type and argument)
+ return match.slice( 0, 3 );
+ }
+ },
+
+ filter: {
+
+ "TAG": function( nodeNameSelector ) {
+ var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+ return nodeNameSelector === "*" ?
+ function() {
+ return true;
+ } :
+ function( elem ) {
+ return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+ };
+ },
+
+ "CLASS": function( className ) {
+ var pattern = classCache[ className + " " ];
+
+ return pattern ||
+ ( pattern = new RegExp( "(^|" + whitespace +
+ ")" + className + "(" + whitespace + "|$)" ) ) && classCache(
+ className, function( elem ) {
+ return pattern.test(
+ typeof elem.className === "string" && elem.className ||
+ typeof elem.getAttribute !== "undefined" &&
+ elem.getAttribute( "class" ) ||
+ ""
+ );
+ } );
+ },
+
+ "ATTR": function( name, operator, check ) {
+ return function( elem ) {
+ var result = Sizzle.attr( elem, name );
+
+ if ( result == null ) {
+ return operator === "!=";
+ }
+ if ( !operator ) {
+ return true;
+ }
+
+ result += "";
+
+ /* eslint-disable max-len */
+
+ return operator === "=" ? result === check :
+ operator === "!=" ? result !== check :
+ operator === "^=" ? check && result.indexOf( check ) === 0 :
+ operator === "*=" ? check && result.indexOf( check ) > -1 :
+ operator === "$=" ? check && result.slice( -check.length ) === check :
+ operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
+ operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+ false;
+ /* eslint-enable max-len */
+
+ };
+ },
+
+ "CHILD": function( type, what, _argument, first, last ) {
+ var simple = type.slice( 0, 3 ) !== "nth",
+ forward = type.slice( -4 ) !== "last",
+ ofType = what === "of-type";
+
+ return first === 1 && last === 0 ?
+
+ // Shortcut for :nth-*(n)
+ function( elem ) {
+ return !!elem.parentNode;
+ } :
+
+ function( elem, _context, xml ) {
+ var cache, uniqueCache, outerCache, node, nodeIndex, start,
+ dir = simple !== forward ? "nextSibling" : "previousSibling",
+ parent = elem.parentNode,
+ name = ofType && elem.nodeName.toLowerCase(),
+ useCache = !xml && !ofType,
+ diff = false;
+
+ if ( parent ) {
+
+ // :(first|last|only)-(child|of-type)
+ if ( simple ) {
+ while ( dir ) {
+ node = elem;
+ while ( ( node = node[ dir ] ) ) {
+ if ( ofType ?
+ node.nodeName.toLowerCase() === name :
+ node.nodeType === 1 ) {
+
+ return false;
+ }
+ }
+
+ // Reverse direction for :only-* (if we haven't yet done so)
+ start = dir = type === "only" && !start && "nextSibling";
+ }
+ return true;
+ }
+
+ start = [ forward ? parent.firstChild : parent.lastChild ];
+
+ // non-xml :nth-child(...) stores cache data on `parent`
+ if ( forward && useCache ) {
+
+ // Seek `elem` from a previously-cached index
+
+ // ...in a gzip-friendly way
+ node = parent;
+ outerCache = node[ expando ] || ( node[ expando ] = {} );
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ ( outerCache[ node.uniqueID ] = {} );
+
+ cache = uniqueCache[ type ] || [];
+ nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+ diff = nodeIndex && cache[ 2 ];
+ node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+ while ( ( node = ++nodeIndex && node && node[ dir ] ||
+
+ // Fallback to seeking `elem` from the start
+ ( diff = nodeIndex = 0 ) || start.pop() ) ) {
+
+ // When found, cache indexes on `parent` and break
+ if ( node.nodeType === 1 && ++diff && node === elem ) {
+ uniqueCache[ type ] = [ dirruns, nodeIndex, diff ];
+ break;
+ }
+ }
+
+ } else {
+
+ // Use previously-cached element index if available
+ if ( useCache ) {
+
+ // ...in a gzip-friendly way
+ node = elem;
+ outerCache = node[ expando ] || ( node[ expando ] = {} );
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ ( outerCache[ node.uniqueID ] = {} );
+
+ cache = uniqueCache[ type ] || [];
+ nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+ diff = nodeIndex;
+ }
+
+ // xml :nth-child(...)
+ // or :nth-last-child(...) or :nth(-last)?-of-type(...)
+ if ( diff === false ) {
+
+ // Use the same loop as above to seek `elem` from the start
+ while ( ( node = ++nodeIndex && node && node[ dir ] ||
+ ( diff = nodeIndex = 0 ) || start.pop() ) ) {
+
+ if ( ( ofType ?
+ node.nodeName.toLowerCase() === name :
+ node.nodeType === 1 ) &&
+ ++diff ) {
+
+ // Cache the index of each encountered element
+ if ( useCache ) {
+ outerCache = node[ expando ] ||
+ ( node[ expando ] = {} );
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ ( outerCache[ node.uniqueID ] = {} );
+
+ uniqueCache[ type ] = [ dirruns, diff ];
+ }
+
+ if ( node === elem ) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Incorporate the offset, then check against cycle size
+ diff -= last;
+ return diff === first || ( diff % first === 0 && diff / first >= 0 );
+ }
+ };
+ },
+
+ "PSEUDO": function( pseudo, argument ) {
+
+ // pseudo-class names are case-insensitive
+ // http://www.w3.org/TR/selectors/#pseudo-classes
+ // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+ // Remember that setFilters inherits from pseudos
+ var args,
+ fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+ Sizzle.error( "unsupported pseudo: " + pseudo );
+
+ // The user may use createPseudo to indicate that
+ // arguments are needed to create the filter function
+ // just as Sizzle does
+ if ( fn[ expando ] ) {
+ return fn( argument );
+ }
+
+ // But maintain support for old signatures
+ if ( fn.length > 1 ) {
+ args = [ pseudo, pseudo, "", argument ];
+ return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+ markFunction( function( seed, matches ) {
+ var idx,
+ matched = fn( seed, argument ),
+ i = matched.length;
+ while ( i-- ) {
+ idx = indexOf( seed, matched[ i ] );
+ seed[ idx ] = !( matches[ idx ] = matched[ i ] );
+ }
+ } ) :
+ function( elem ) {
+ return fn( elem, 0, args );
+ };
+ }
+
+ return fn;
+ }
+ },
+
+ pseudos: {
+
+ // Potentially complex pseudos
+ "not": markFunction( function( selector ) {
+
+ // Trim the selector passed to compile
+ // to avoid treating leading and trailing
+ // spaces as combinators
+ var input = [],
+ results = [],
+ matcher = compile( selector.replace( rtrim, "$1" ) );
+
+ return matcher[ expando ] ?
+ markFunction( function( seed, matches, _context, xml ) {
+ var elem,
+ unmatched = matcher( seed, null, xml, [] ),
+ i = seed.length;
+
+ // Match elements unmatched by `matcher`
+ while ( i-- ) {
+ if ( ( elem = unmatched[ i ] ) ) {
+ seed[ i ] = !( matches[ i ] = elem );
+ }
+ }
+ } ) :
+ function( elem, _context, xml ) {
+ input[ 0 ] = elem;
+ matcher( input, null, xml, results );
+
+ // Don't keep the element (issue #299)
+ input[ 0 ] = null;
+ return !results.pop();
+ };
+ } ),
+
+ "has": markFunction( function( selector ) {
+ return function( elem ) {
+ return Sizzle( selector, elem ).length > 0;
+ };
+ } ),
+
+ "contains": markFunction( function( text ) {
+ text = text.replace( runescape, funescape );
+ return function( elem ) {
+ return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1;
+ };
+ } ),
+
+ // "Whether an element is represented by a :lang() selector
+ // is based solely on the element's language value
+ // being equal to the identifier C,
+ // or beginning with the identifier C immediately followed by "-".
+ // The matching of C against the element's language value is performed case-insensitively.
+ // The identifier C does not have to be a valid language name."
+ // http://www.w3.org/TR/selectors/#lang-pseudo
+ "lang": markFunction( function( lang ) {
+
+ // lang value must be a valid identifier
+ if ( !ridentifier.test( lang || "" ) ) {
+ Sizzle.error( "unsupported lang: " + lang );
+ }
+ lang = lang.replace( runescape, funescape ).toLowerCase();
+ return function( elem ) {
+ var elemLang;
+ do {
+ if ( ( elemLang = documentIsHTML ?
+ elem.lang :
+ elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) {
+
+ elemLang = elemLang.toLowerCase();
+ return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+ }
+ } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 );
+ return false;
+ };
+ } ),
+
+ // Miscellaneous
+ "target": function( elem ) {
+ var hash = window.location && window.location.hash;
+ return hash && hash.slice( 1 ) === elem.id;
+ },
+
+ "root": function( elem ) {
+ return elem === docElem;
+ },
+
+ "focus": function( elem ) {
+ return elem === document.activeElement &&
+ ( !document.hasFocus || document.hasFocus() ) &&
+ !!( elem.type || elem.href || ~elem.tabIndex );
+ },
+
+ // Boolean properties
+ "enabled": createDisabledPseudo( false ),
+ "disabled": createDisabledPseudo( true ),
+
+ "checked": function( elem ) {
+
+ // In CSS3, :checked should return both checked and selected elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ var nodeName = elem.nodeName.toLowerCase();
+ return ( nodeName === "input" && !!elem.checked ) ||
+ ( nodeName === "option" && !!elem.selected );
+ },
+
+ "selected": function( elem ) {
+
+ // Accessing this property makes selected-by-default
+ // options in Safari work properly
+ if ( elem.parentNode ) {
+ // eslint-disable-next-line no-unused-expressions
+ elem.parentNode.selectedIndex;
+ }
+
+ return elem.selected === true;
+ },
+
+ // Contents
+ "empty": function( elem ) {
+
+ // http://www.w3.org/TR/selectors/#empty-pseudo
+ // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
+ // but not by others (comment: 8; processing instruction: 7; etc.)
+ // nodeType < 6 works because attributes (2) do not appear as children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ if ( elem.nodeType < 6 ) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ "parent": function( elem ) {
+ return !Expr.pseudos[ "empty" ]( elem );
+ },
+
+ // Element/input types
+ "header": function( elem ) {
+ return rheader.test( elem.nodeName );
+ },
+
+ "input": function( elem ) {
+ return rinputs.test( elem.nodeName );
+ },
+
+ "button": function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === "button" || name === "button";
+ },
+
+ "text": function( elem ) {
+ var attr;
+ return elem.nodeName.toLowerCase() === "input" &&
+ elem.type === "text" &&
+
+ // Support: IE <10 only
+ // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
+ ( ( attr = elem.getAttribute( "type" ) ) == null ||
+ attr.toLowerCase() === "text" );
+ },
+
+ // Position-in-collection
+ "first": createPositionalPseudo( function() {
+ return [ 0 ];
+ } ),
+
+ "last": createPositionalPseudo( function( _matchIndexes, length ) {
+ return [ length - 1 ];
+ } ),
+
+ "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) {
+ return [ argument < 0 ? argument + length : argument ];
+ } ),
+
+ "even": createPositionalPseudo( function( matchIndexes, length ) {
+ var i = 0;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ } ),
+
+ "odd": createPositionalPseudo( function( matchIndexes, length ) {
+ var i = 1;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ } ),
+
+ "lt": createPositionalPseudo( function( matchIndexes, length, argument ) {
+ var i = argument < 0 ?
+ argument + length :
+ argument > length ?
+ length :
+ argument;
+ for ( ; --i >= 0; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ } ),
+
+ "gt": createPositionalPseudo( function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; ++i < length; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ } )
+ }
+};
+
+Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ];
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+ Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+ Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
+ var matched, match, tokens, type,
+ soFar, groups, preFilters,
+ cached = tokenCache[ selector + " " ];
+
+ if ( cached ) {
+ return parseOnly ? 0 : cached.slice( 0 );
+ }
+
+ soFar = selector;
+ groups = [];
+ preFilters = Expr.preFilter;
+
+ while ( soFar ) {
+
+ // Comma and first run
+ if ( !matched || ( match = rcomma.exec( soFar ) ) ) {
+ if ( match ) {
+
+ // Don't consume trailing commas as valid
+ soFar = soFar.slice( match[ 0 ].length ) || soFar;
+ }
+ groups.push( ( tokens = [] ) );
+ }
+
+ matched = false;
+
+ // Combinators
+ if ( ( match = rleadingCombinator.exec( soFar ) ) ) {
+ matched = match.shift();
+ tokens.push( {
+ value: matched,
+
+ // Cast descendant combinators to space
+ type: match[ 0 ].replace( rtrim, " " )
+ } );
+ soFar = soFar.slice( matched.length );
+ }
+
+ // Filters
+ for ( type in Expr.filter ) {
+ if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] ||
+ ( match = preFilters[ type ]( match ) ) ) ) {
+ matched = match.shift();
+ tokens.push( {
+ value: matched,
+ type: type,
+ matches: match
+ } );
+ soFar = soFar.slice( matched.length );
+ }
+ }
+
+ if ( !matched ) {
+ break;
+ }
+ }
+
+ // Return the length of the invalid excess
+ // if we're just parsing
+ // Otherwise, throw an error or return tokens
+ return parseOnly ?
+ soFar.length :
+ soFar ?
+ Sizzle.error( selector ) :
+
+ // Cache the tokens
+ tokenCache( selector, groups ).slice( 0 );
+};
+
+function toSelector( tokens ) {
+ var i = 0,
+ len = tokens.length,
+ selector = "";
+ for ( ; i < len; i++ ) {
+ selector += tokens[ i ].value;
+ }
+ return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+ var dir = combinator.dir,
+ skip = combinator.next,
+ key = skip || dir,
+ checkNonElements = base && key === "parentNode",
+ doneName = done++;
+
+ return combinator.first ?
+
+ // Check against closest ancestor/preceding element
+ function( elem, context, xml ) {
+ while ( ( elem = elem[ dir ] ) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ return matcher( elem, context, xml );
+ }
+ }
+ return false;
+ } :
+
+ // Check against all ancestor/preceding elements
+ function( elem, context, xml ) {
+ var oldCache, uniqueCache, outerCache,
+ newCache = [ dirruns, doneName ];
+
+ // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
+ if ( xml ) {
+ while ( ( elem = elem[ dir ] ) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ if ( matcher( elem, context, xml ) ) {
+ return true;
+ }
+ }
+ }
+ } else {
+ while ( ( elem = elem[ dir ] ) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ outerCache = elem[ expando ] || ( elem[ expando ] = {} );
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ elem.uniqueID ] ||
+ ( outerCache[ elem.uniqueID ] = {} );
+
+ if ( skip && skip === elem.nodeName.toLowerCase() ) {
+ elem = elem[ dir ] || elem;
+ } else if ( ( oldCache = uniqueCache[ key ] ) &&
+ oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
+
+ // Assign to newCache so results back-propagate to previous elements
+ return ( newCache[ 2 ] = oldCache[ 2 ] );
+ } else {
+
+ // Reuse newcache so results back-propagate to previous elements
+ uniqueCache[ key ] = newCache;
+
+ // A match means we're done; a fail means we have to keep checking
+ if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ };
+}
+
+function elementMatcher( matchers ) {
+ return matchers.length > 1 ?
+ function( elem, context, xml ) {
+ var i = matchers.length;
+ while ( i-- ) {
+ if ( !matchers[ i ]( elem, context, xml ) ) {
+ return false;
+ }
+ }
+ return true;
+ } :
+ matchers[ 0 ];
+}
+
+function multipleContexts( selector, contexts, results ) {
+ var i = 0,
+ len = contexts.length;
+ for ( ; i < len; i++ ) {
+ Sizzle( selector, contexts[ i ], results );
+ }
+ return results;
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+ var elem,
+ newUnmatched = [],
+ i = 0,
+ len = unmatched.length,
+ mapped = map != null;
+
+ for ( ; i < len; i++ ) {
+ if ( ( elem = unmatched[ i ] ) ) {
+ if ( !filter || filter( elem, context, xml ) ) {
+ newUnmatched.push( elem );
+ if ( mapped ) {
+ map.push( i );
+ }
+ }
+ }
+ }
+
+ return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+ if ( postFilter && !postFilter[ expando ] ) {
+ postFilter = setMatcher( postFilter );
+ }
+ if ( postFinder && !postFinder[ expando ] ) {
+ postFinder = setMatcher( postFinder, postSelector );
+ }
+ return markFunction( function( seed, results, context, xml ) {
+ var temp, i, elem,
+ preMap = [],
+ postMap = [],
+ preexisting = results.length,
+
+ // Get initial elements from seed or context
+ elems = seed || multipleContexts(
+ selector || "*",
+ context.nodeType ? [ context ] : context,
+ []
+ ),
+
+ // Prefilter to get matcher input, preserving a map for seed-results synchronization
+ matcherIn = preFilter && ( seed || !selector ) ?
+ condense( elems, preMap, preFilter, context, xml ) :
+ elems,
+
+ matcherOut = matcher ?
+
+ // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+ postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+ // ...intermediate processing is necessary
+ [] :
+
+ // ...otherwise use results directly
+ results :
+ matcherIn;
+
+ // Find primary matches
+ if ( matcher ) {
+ matcher( matcherIn, matcherOut, context, xml );
+ }
+
+ // Apply postFilter
+ if ( postFilter ) {
+ temp = condense( matcherOut, postMap );
+ postFilter( temp, [], context, xml );
+
+ // Un-match failing elements by moving them back to matcherIn
+ i = temp.length;
+ while ( i-- ) {
+ if ( ( elem = temp[ i ] ) ) {
+ matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem );
+ }
+ }
+ }
+
+ if ( seed ) {
+ if ( postFinder || preFilter ) {
+ if ( postFinder ) {
+
+ // Get the final matcherOut by condensing this intermediate into postFinder contexts
+ temp = [];
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( ( elem = matcherOut[ i ] ) ) {
+
+ // Restore matcherIn since elem is not yet a final match
+ temp.push( ( matcherIn[ i ] = elem ) );
+ }
+ }
+ postFinder( null, ( matcherOut = [] ), temp, xml );
+ }
+
+ // Move matched elements from seed to results to keep them synchronized
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( ( elem = matcherOut[ i ] ) &&
+ ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) {
+
+ seed[ temp ] = !( results[ temp ] = elem );
+ }
+ }
+ }
+
+ // Add elements to results, through postFinder if defined
+ } else {
+ matcherOut = condense(
+ matcherOut === results ?
+ matcherOut.splice( preexisting, matcherOut.length ) :
+ matcherOut
+ );
+ if ( postFinder ) {
+ postFinder( null, results, matcherOut, xml );
+ } else {
+ push.apply( results, matcherOut );
+ }
+ }
+ } );
+}
+
+function matcherFromTokens( tokens ) {
+ var checkContext, matcher, j,
+ len = tokens.length,
+ leadingRelative = Expr.relative[ tokens[ 0 ].type ],
+ implicitRelative = leadingRelative || Expr.relative[ " " ],
+ i = leadingRelative ? 1 : 0,
+
+ // The foundational matcher ensures that elements are reachable from top-level context(s)
+ matchContext = addCombinator( function( elem ) {
+ return elem === checkContext;
+ }, implicitRelative, true ),
+ matchAnyContext = addCombinator( function( elem ) {
+ return indexOf( checkContext, elem ) > -1;
+ }, implicitRelative, true ),
+ matchers = [ function( elem, context, xml ) {
+ var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+ ( checkContext = context ).nodeType ?
+ matchContext( elem, context, xml ) :
+ matchAnyContext( elem, context, xml ) );
+
+ // Avoid hanging onto element (issue #299)
+ checkContext = null;
+ return ret;
+ } ];
+
+ for ( ; i < len; i++ ) {
+ if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) {
+ matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ];
+ } else {
+ matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches );
+
+ // Return special upon seeing a positional matcher
+ if ( matcher[ expando ] ) {
+
+ // Find the next relative operator (if any) for proper handling
+ j = ++i;
+ for ( ; j < len; j++ ) {
+ if ( Expr.relative[ tokens[ j ].type ] ) {
+ break;
+ }
+ }
+ return setMatcher(
+ i > 1 && elementMatcher( matchers ),
+ i > 1 && toSelector(
+
+ // If the preceding token was a descendant combinator, insert an implicit any-element `*`
+ tokens
+ .slice( 0, i - 1 )
+ .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } )
+ ).replace( rtrim, "$1" ),
+ matcher,
+ i < j && matcherFromTokens( tokens.slice( i, j ) ),
+ j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ),
+ j < len && toSelector( tokens )
+ );
+ }
+ matchers.push( matcher );
+ }
+ }
+
+ return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+ var bySet = setMatchers.length > 0,
+ byElement = elementMatchers.length > 0,
+ superMatcher = function( seed, context, xml, results, outermost ) {
+ var elem, j, matcher,
+ matchedCount = 0,
+ i = "0",
+ unmatched = seed && [],
+ setMatched = [],
+ contextBackup = outermostContext,
+
+ // We must always have either seed elements or outermost context
+ elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ),
+
+ // Use integer dirruns iff this is the outermost matcher
+ dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ),
+ len = elems.length;
+
+ if ( outermost ) {
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ outermostContext = context == document || context || outermost;
+ }
+
+ // Add elements passing elementMatchers directly to results
+ // Support: IE<9, Safari
+ // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
+ for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) {
+ if ( byElement && elem ) {
+ j = 0;
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( !context && elem.ownerDocument != document ) {
+ setDocument( elem );
+ xml = !documentIsHTML;
+ }
+ while ( ( matcher = elementMatchers[ j++ ] ) ) {
+ if ( matcher( elem, context || document, xml ) ) {
+ results.push( elem );
+ break;
+ }
+ }
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ }
+ }
+
+ // Track unmatched elements for set filters
+ if ( bySet ) {
+
+ // They will have gone through all possible matchers
+ if ( ( elem = !matcher && elem ) ) {
+ matchedCount--;
+ }
+
+ // Lengthen the array for every element, matched or not
+ if ( seed ) {
+ unmatched.push( elem );
+ }
+ }
+ }
+
+ // `i` is now the count of elements visited above, and adding it to `matchedCount`
+ // makes the latter nonnegative.
+ matchedCount += i;
+
+ // Apply set filters to unmatched elements
+ // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`
+ // equals `i`), unless we didn't visit _any_ elements in the above loop because we have
+ // no element matchers and no seed.
+ // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that
+ // case, which will result in a "00" `matchedCount` that differs from `i` but is also
+ // numerically zero.
+ if ( bySet && i !== matchedCount ) {
+ j = 0;
+ while ( ( matcher = setMatchers[ j++ ] ) ) {
+ matcher( unmatched, setMatched, context, xml );
+ }
+
+ if ( seed ) {
+
+ // Reintegrate element matches to eliminate the need for sorting
+ if ( matchedCount > 0 ) {
+ while ( i-- ) {
+ if ( !( unmatched[ i ] || setMatched[ i ] ) ) {
+ setMatched[ i ] = pop.call( results );
+ }
+ }
+ }
+
+ // Discard index placeholder values to get only actual matches
+ setMatched = condense( setMatched );
+ }
+
+ // Add matches to results
+ push.apply( results, setMatched );
+
+ // Seedless set matches succeeding multiple successful matchers stipulate sorting
+ if ( outermost && !seed && setMatched.length > 0 &&
+ ( matchedCount + setMatchers.length ) > 1 ) {
+
+ Sizzle.uniqueSort( results );
+ }
+ }
+
+ // Override manipulation of globals by nested matchers
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ outermostContext = contextBackup;
+ }
+
+ return unmatched;
+ };
+
+ return bySet ?
+ markFunction( superMatcher ) :
+ superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
+ var i,
+ setMatchers = [],
+ elementMatchers = [],
+ cached = compilerCache[ selector + " " ];
+
+ if ( !cached ) {
+
+ // Generate a function of recursive functions that can be used to check each element
+ if ( !match ) {
+ match = tokenize( selector );
+ }
+ i = match.length;
+ while ( i-- ) {
+ cached = matcherFromTokens( match[ i ] );
+ if ( cached[ expando ] ) {
+ setMatchers.push( cached );
+ } else {
+ elementMatchers.push( cached );
+ }
+ }
+
+ // Cache the compiled function
+ cached = compilerCache(
+ selector,
+ matcherFromGroupMatchers( elementMatchers, setMatchers )
+ );
+
+ // Save selector and tokenization
+ cached.selector = selector;
+ }
+ return cached;
+};
+
+/**
+ * A low-level selection function that works with Sizzle's compiled
+ * selector functions
+ * @param {String|Function} selector A selector or a pre-compiled
+ * selector function built with Sizzle.compile
+ * @param {Element} context
+ * @param {Array} [results]
+ * @param {Array} [seed] A set of elements to match against
+ */
+select = Sizzle.select = function( selector, context, results, seed ) {
+ var i, tokens, token, type, find,
+ compiled = typeof selector === "function" && selector,
+ match = !seed && tokenize( ( selector = compiled.selector || selector ) );
+
+ results = results || [];
+
+ // Try to minimize operations if there is only one selector in the list and no seed
+ // (the latter of which guarantees us context)
+ if ( match.length === 1 ) {
+
+ // Reduce context if the leading compound selector is an ID
+ tokens = match[ 0 ] = match[ 0 ].slice( 0 );
+ if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" &&
+ context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) {
+
+ context = ( Expr.find[ "ID" ]( token.matches[ 0 ]
+ .replace( runescape, funescape ), context ) || [] )[ 0 ];
+ if ( !context ) {
+ return results;
+
+ // Precompiled matchers will still verify ancestry, so step up a level
+ } else if ( compiled ) {
+ context = context.parentNode;
+ }
+
+ selector = selector.slice( tokens.shift().value.length );
+ }
+
+ // Fetch a seed set for right-to-left matching
+ i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length;
+ while ( i-- ) {
+ token = tokens[ i ];
+
+ // Abort if we hit a combinator
+ if ( Expr.relative[ ( type = token.type ) ] ) {
+ break;
+ }
+ if ( ( find = Expr.find[ type ] ) ) {
+
+ // Search, expanding context for leading sibling combinators
+ if ( ( seed = find(
+ token.matches[ 0 ].replace( runescape, funescape ),
+ rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) ||
+ context
+ ) ) ) {
+
+ // If seed is empty or no tokens remain, we can return early
+ tokens.splice( i, 1 );
+ selector = seed.length && toSelector( tokens );
+ if ( !selector ) {
+ push.apply( results, seed );
+ return results;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Compile and execute a filtering function if one is not provided
+ // Provide `match` to avoid retokenization if we modified the selector above
+ ( compiled || compile( selector, match ) )(
+ seed,
+ context,
+ !documentIsHTML,
+ results,
+ !context || rsibling.test( selector ) && testContext( context.parentNode ) || context
+ );
+ return results;
+};
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando;
+
+// Support: Chrome 14-35+
+// Always assume duplicates if they aren't passed to the comparison function
+support.detectDuplicates = !!hasDuplicate;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+// Detached nodes confoundingly follow *each other*
+support.sortDetached = assert( function( el ) {
+
+ // Should return 1, but returns 4 (following)
+ return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1;
+} );
+
+// Support: IE<8
+// Prevent attribute/property "interpolation"
+// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !assert( function( el ) {
+ el.innerHTML = "<a href='#'></a>";
+ return el.firstChild.getAttribute( "href" ) === "#";
+} ) ) {
+ addHandle( "type|href|height|width", function( elem, name, isXML ) {
+ if ( !isXML ) {
+ return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+ }
+ } );
+}
+
+// Support: IE<9
+// Use defaultValue in place of getAttribute("value")
+if ( !support.attributes || !assert( function( el ) {
+ el.innerHTML = "<input/>";
+ el.firstChild.setAttribute( "value", "" );
+ return el.firstChild.getAttribute( "value" ) === "";
+} ) ) {
+ addHandle( "value", function( elem, _name, isXML ) {
+ if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
+ return elem.defaultValue;
+ }
+ } );
+}
+
+// Support: IE<9
+// Use getAttributeNode to fetch booleans when getAttribute lies
+if ( !assert( function( el ) {
+ return el.getAttribute( "disabled" ) == null;
+} ) ) {
+ addHandle( booleans, function( elem, name, isXML ) {
+ var val;
+ if ( !isXML ) {
+ return elem[ name ] === true ? name.toLowerCase() :
+ ( val = elem.getAttributeNode( name ) ) && val.specified ?
+ val.value :
+ null;
+ }
+ } );
+}
+
+// EXPOSE
+var _sizzle = window.Sizzle;
+
+Sizzle.noConflict = function() {
+ if ( window.Sizzle === Sizzle ) {
+ window.Sizzle = _sizzle;
+ }
+
+ return Sizzle;
+};
+
+if ( typeof define === "function" && define.amd ) {
+ define( function() {
+ return Sizzle;
+ } );
+
+// Sizzle requires that there be a global window in Common-JS like environments
+} else if ( typeof module !== "undefined" && module.exports ) {
+ module.exports = Sizzle;
+} else {
+ window.Sizzle = Sizzle;
+}
+
+// EXPOSE
+
+} )( window );
diff --git a/node_modules/sizzle/dist/sizzle.min.js b/node_modules/sizzle/dist/sizzle.min.js
new file mode 100644
index 0000000..8c686df
--- /dev/null
+++ b/node_modules/sizzle/dist/sizzle.min.js
@@ -0,0 +1,3 @@
+/*! Sizzle v2.3.10 | (c) JS Foundation and other contributors | js.foundation */
+!function(e){var t,n,r,i,o,u,l,a,c,s,f,d,p,h,g,m,y,v,w,b="sizzle"+1*new Date,N=e.document,C=0,x=0,E=ae(),A=ae(),S=ae(),D=ae(),T=function(e,t){return e===t&&(f=!0),0},L={}.hasOwnProperty,q=[],I=q.pop,B=q.push,R=q.push,$=q.slice,k=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},H="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",P="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",z="\\["+M+"*("+P+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+P+"))|)"+M+"*\\]",F=":("+P+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+z+")*)|.*)\\)|)",O=new RegExp(M+"+","g"),j=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),G=new RegExp("^"+M+"*,"+M+"*"),U=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),V=new RegExp(M+"|>"),X=new RegExp(F),J=new RegExp("^"+P+"$"),K={ID:new RegExp("^#("+P+")"),CLASS:new RegExp("^\\.("+P+")"),TAG:new RegExp("^("+P+"|[*])"),ATTR:new RegExp("^"+z),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+H+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Q=/HTML$/i,W=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){d()},ue=ve(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{R.apply(q=$.call(N.childNodes),N.childNodes),q[N.childNodes.length].nodeType}catch(e){R={apply:q.length?function(e,t){B.apply(e,$.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function le(e,t,r,i){var o,l,c,s,f,h,y,v=t&&t.ownerDocument,N=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==N&&9!==N&&11!==N)return r;if(!i&&(d(t),t=t||p,g)){if(11!==N&&(f=_.exec(e)))if(o=f[1]){if(9===N){if(!(c=t.getElementById(o)))return r;if(c.id===o)return r.push(c),r}else if(v&&(c=v.getElementById(o))&&w(t,c)&&c.id===o)return r.push(c),r}else{if(f[2])return R.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return R.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!D[e+" "]&&(!m||!m.test(e))&&(1!==N||"object"!==t.nodeName.toLowerCase())){if(y=e,v=t,1===N&&(V.test(e)||U.test(e))){(v=ee.test(e)&&ge(t.parentNode)||t)===t&&n.scope||((s=t.getAttribute("id"))?s=s.replace(re,ie):t.setAttribute("id",s=b)),l=(h=u(e)).length;while(l--)h[l]=(s?"#"+s:":scope")+" "+ye(h[l]);y=h.join(",")}try{return R.apply(r,v.querySelectorAll(y)),r}catch(t){D(e,!0)}finally{s===b&&t.removeAttribute("id")}}}return a(e.replace(j,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function ce(e){return e[b]=!0,e}function se(e){var t=p.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pe(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ue(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return ce(function(t){return t=+t,ce(function(n,r){var i,o=e([],n.length,t),u=o.length;while(u--)n[i=o[u]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&void 0!==e.getElementsByTagName&&e}n=le.support={},o=le.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Q.test(t||n&&n.nodeName||"HTML")},d=le.setDocument=function(e){var t,i,u=e?e.ownerDocument||e:N;return u!=p&&9===u.nodeType&&u.documentElement?(p=u,h=p.documentElement,g=!o(p),N!=p&&(i=p.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",oe,!1):i.attachEvent&&i.attachEvent("onunload",oe)),n.scope=se(function(e){return h.appendChild(e).appendChild(p.createElement("div")),void 0!==e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),n.cssHas=se(function(){try{return p.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),n.attributes=se(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=se(function(e){return e.appendChild(p.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Z.test(p.getElementsByClassName),n.getById=se(function(e){return h.appendChild(e).id=b,!p.getElementsByName||!p.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&g)return t.getElementsByClassName(e)},y=[],m=[],(n.qsa=Z.test(p.querySelectorAll))&&(se(function(e){var t;h.appendChild(e).innerHTML="<a id='"+b+"'></a><select id='"+b+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+M+"*(?:value|"+H+")"),e.querySelectorAll("[id~="+b+"-]").length||m.push("~="),(t=p.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||m.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||m.push(".#.+[+~]"),e.querySelectorAll("\\\f"),m.push("[\\r\\n\\f]")}),se(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=p.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&m.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")})),(n.matchesSelector=Z.test(v=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&se(function(e){n.disconnectedMatch=v.call(e,"*"),v.call(e,"[s!='']:x"),y.push("!=",F)}),n.cssHas||m.push(":has"),m=m.length&&new RegExp(m.join("|")),y=y.length&&new RegExp(y.join("|")),t=Z.test(h.compareDocumentPosition),w=t||Z.test(h.contains)?function(e,t){var n=9===e.nodeType&&e.documentElement||e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},T=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e==p||e.ownerDocument==N&&w(N,e)?-1:t==p||t.ownerDocument==N&&w(N,t)?1:s?k(s,e)-k(s,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,u=[e],l=[t];if(!i||!o)return e==p?-1:t==p?1:i?-1:o?1:s?k(s,e)-k(s,t):0;if(i===o)return de(e,t);n=e;while(n=n.parentNode)u.unshift(n);n=t;while(n=n.parentNode)l.unshift(n);while(u[r]===l[r])r++;return r?de(u[r],l[r]):u[r]==N?-1:l[r]==N?1:0},p):p},le.matches=function(e,t){return le(e,null,null,t)},le.matchesSelector=function(e,t){if(d(e),n.matchesSelector&&g&&!D[t+" "]&&(!y||!y.test(t))&&(!m||!m.test(t)))try{var r=v.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){D(t,!0)}return le(t,p,null,[e]).length>0},le.contains=function(e,t){return(e.ownerDocument||e)!=p&&d(e),w(e,t)},le.attr=function(e,t){(e.ownerDocument||e)!=p&&d(e);var i=r.attrHandle[t.toLowerCase()],o=i&&L.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},le.escape=function(e){return(e+"").replace(re,ie)},le.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},le.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,s=!n.sortStable&&e.slice(0),e.sort(T),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return s=null,e},i=le.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=le.selectors={cacheLength:50,createPseudo:ce,match:K,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||le.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&le.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return K.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=u(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=le.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace(O," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),u="last"!==e.slice(-4),l="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,a){var c,s,f,d,p,h,g=o!==u?"nextSibling":"previousSibling",m=t.parentNode,y=l&&t.nodeName.toLowerCase(),v=!a&&!l,w=!1;if(m){if(o){while(g){d=t;while(d=d[g])if(l?d.nodeName.toLowerCase()===y:1===d.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[u?m.firstChild:m.lastChild],u&&v){w=(p=(c=(s=(f=(d=m)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===C&&c[1])&&c[2],d=p&&m.childNodes[p];while(d=++p&&d&&d[g]||(w=p=0)||h.pop())if(1===d.nodeType&&++w&&d===t){s[e]=[C,p,w];break}}else if(v&&(w=p=(c=(s=(f=(d=t)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===C&&c[1]),!1===w)while(d=++p&&d&&d[g]||(w=p=0)||h.pop())if((l?d.nodeName.toLowerCase()===y:1===d.nodeType)&&++w&&(v&&((s=(f=d[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]=[C,w]),d===t))break;return(w-=i)===r||w%r==0&&w/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||le.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?ce(function(e,n){var r,o=i(e,t),u=o.length;while(u--)e[r=k(e,o[u])]=!(n[r]=o[u])}):function(e){return i(e,0,n)}):i}},pseudos:{not:ce(function(e){var t=[],n=[],r=l(e.replace(j,"$1"));return r[b]?ce(function(e,t,n,i){var o,u=r(e,null,i,[]),l=e.length;while(l--)(o=u[l])&&(e[l]=!(t[l]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:ce(function(e){return function(t){return le(e,t).length>0}}),contains:ce(function(e){return e=e.replace(te,ne),function(t){return(t.textContent||i(t)).indexOf(e)>-1}}),lang:ce(function(e){return J.test(e||"")||le.error("unsupported lang: "+e),e=e.replace(te,ne).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:pe(!1),disabled:pe(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return W.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:he(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:he(function(e,t,n){for(var r=n<0?n+t:n>t?t:n;--r>=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=r.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})r.pseudos[t]=function(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}(t);for(t in{submit:!0,reset:!0})r.pseudos[t]=function(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}(t);function me(){}me.prototype=r.filters=r.pseudos,r.setFilters=new me,u=le.tokenize=function(e,t){var n,i,o,u,l,a,c,s=A[e+" "];if(s)return t?0:s.slice(0);l=e,a=[],c=r.preFilter;while(l){n&&!(i=G.exec(l))||(i&&(l=l.slice(i[0].length)||l),a.push(o=[])),n=!1,(i=U.exec(l))&&(n=i.shift(),o.push({value:n,type:i[0].replace(j," ")}),l=l.slice(n.length));for(u in r.filter)!(i=K[u].exec(l))||c[u]&&!(i=c[u](i))||(n=i.shift(),o.push({value:n,type:u,matches:i}),l=l.slice(n.length));if(!n)break}return t?l.length:l?le.error(e):A(e,a).slice(0)};function ye(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function ve(e,t,n){var r=t.dir,i=t.next,o=i||r,u=n&&"parentNode"===o,l=x++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||u)return e(t,n,i);return!1}:function(t,n,a){var c,s,f,d=[C,l];if(a){while(t=t[r])if((1===t.nodeType||u)&&e(t,n,a))return!0}else while(t=t[r])if(1===t.nodeType||u)if(f=t[b]||(t[b]={}),s=f[t.uniqueID]||(f[t.uniqueID]={}),i&&i===t.nodeName.toLowerCase())t=t[r]||t;else{if((c=s[o])&&c[0]===C&&c[1]===l)return d[2]=c[2];if(s[o]=d,d[2]=e(t,n,a))return!0}return!1}}function we(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r<i;r++)le(e,t[r],n);return n}function Ne(e,t,n,r,i){for(var o,u=[],l=0,a=e.length,c=null!=t;l<a;l++)(o=e[l])&&(n&&!n(o,r,i)||(u.push(o),c&&t.push(l)));return u}function Ce(e,t,n,r,i,o){return r&&!r[b]&&(r=Ce(r)),i&&!i[b]&&(i=Ce(i,o)),ce(function(o,u,l,a){var c,s,f,d=[],p=[],h=u.length,g=o||be(t||"*",l.nodeType?[l]:l,[]),m=!e||!o&&t?g:Ne(g,d,e,l,a),y=n?i||(o?e:h||r)?[]:u:m;if(n&&n(m,y,l,a),r){c=Ne(y,p),r(c,[],l,a),s=c.length;while(s--)(f=c[s])&&(y[p[s]]=!(m[p[s]]=f))}if(o){if(i||e){if(i){c=[],s=y.length;while(s--)(f=y[s])&&c.push(m[s]=f);i(null,y=[],c,a)}s=y.length;while(s--)(f=y[s])&&(c=i?k(o,f):d[s])>-1&&(o[c]=!(u[c]=f))}}else y=Ne(y===u?y.splice(h,y.length):y),i?i(null,u,y,a):R.apply(u,y)})}function xe(e){for(var t,n,i,o=e.length,u=r.relative[e[0].type],l=u||r.relative[" "],a=u?1:0,s=ve(function(e){return e===t},l,!0),f=ve(function(e){return k(t,e)>-1},l,!0),d=[function(e,n,r){var i=!u&&(r||n!==c)||((t=n).nodeType?s(e,n,r):f(e,n,r));return t=null,i}];a<o;a++)if(n=r.relative[e[a].type])d=[ve(we(d),n)];else{if((n=r.filter[e[a].type].apply(null,e[a].matches))[b]){for(i=++a;i<o;i++)if(r.relative[e[i].type])break;return Ce(a>1&&we(d),a>1&&ye(e.slice(0,a-1).concat({value:" "===e[a-2].type?"*":""})).replace(j,"$1"),n,a<i&&xe(e.slice(a,i)),i<o&&xe(e=e.slice(i)),i<o&&ye(e))}d.push(n)}return we(d)}function Ee(e,t){var n=t.length>0,i=e.length>0,o=function(o,u,l,a,s){var f,h,m,y=0,v="0",w=o&&[],b=[],N=c,x=o||i&&r.find.TAG("*",s),E=C+=null==N?1:Math.random()||.1,A=x.length;for(s&&(c=u==p||u||s);v!==A&&null!=(f=x[v]);v++){if(i&&f){h=0,u||f.ownerDocument==p||(d(f),l=!g);while(m=e[h++])if(m(f,u||p,l)){a.push(f);break}s&&(C=E)}n&&((f=!m&&f)&&y--,o&&w.push(f))}if(y+=v,n&&v!==y){h=0;while(m=t[h++])m(w,b,u,l);if(o){if(y>0)while(v--)w[v]||b[v]||(b[v]=I.call(a));b=Ne(b)}R.apply(a,b),s&&!o&&b.length>0&&y+t.length>1&&le.uniqueSort(a)}return s&&(C=E,c=N),w};return n?ce(o):o}l=le.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=u(e)),n=t.length;while(n--)(o=xe(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},a=le.select=function(e,t,n,i){var o,a,c,s,f,d="function"==typeof e&&e,p=!i&&u(e=d.selector||e);if(n=n||[],1===p.length){if((a=p[0]=p[0].slice(0)).length>2&&"ID"===(c=a[0]).type&&9===t.nodeType&&g&&r.relative[a[1].type]){if(!(t=(r.find.ID(c.matches[0].replace(te,ne),t)||[])[0]))return n;d&&(t=t.parentNode),e=e.slice(a.shift().value.length)}o=K.needsContext.test(e)?0:a.length;while(o--){if(c=a[o],r.relative[s=c.type])break;if((f=r.find[s])&&(i=f(c.matches[0].replace(te,ne),ee.test(a[0].type)&&ge(t.parentNode)||t))){if(a.splice(o,1),!(e=i.length&&ye(a)))return R.apply(n,i),n;break}}}return(d||l(e,p))(i,t,!g,n,!t||ee.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(T).join("")===b,n.detectDuplicates=!!f,d(),n.sortDetached=se(function(e){return 1&e.compareDocumentPosition(p.createElement("fieldset"))}),se(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&se(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||fe("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),se(function(e){return null==e.getAttribute("disabled")})||fe(H,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null});var Ae=e.Sizzle;le.noConflict=function(){return e.Sizzle===le&&(e.Sizzle=Ae),le},"function"==typeof define&&define.amd?define(function(){return le}):"undefined"!=typeof module&&module.exports?module.exports=le:e.Sizzle=le}(window);
+//# sourceMappingURL=sizzle.min.map \ No newline at end of file
diff --git a/node_modules/sizzle/dist/sizzle.min.map b/node_modules/sizzle/dist/sizzle.min.map
new file mode 100644
index 0000000..43f672d
--- /dev/null
+++ b/node_modules/sizzle/dist/sizzle.min.map
@@ -0,0 +1 @@
+{"version":3,"sources":["sizzle.js"],"names":["window","i","support","Expr","getText","isXML","tokenize","compile","select","outermostContext","sortInput","hasDuplicate","setDocument","document","docElem","documentIsHTML","rbuggyQSA","rbuggyMatches","matches","contains","expando","Date","preferredDoc","dirruns","done","classCache","createCache","tokenCache","compilerCache","nonnativeSelectorCache","sortOrder","a","b","hasOwn","hasOwnProperty","arr","pop","pushNative","push","slice","indexOf","list","elem","len","length","booleans","whitespace","identifier","attributes","pseudos","rwhitespace","RegExp","rtrim","rcomma","rleadingCombinator","rdescend","rpseudo","ridentifier","matchExpr","ID","CLASS","TAG","ATTR","PSEUDO","CHILD","bool","needsContext","rhtml","rinputs","rheader","rnative","rquickExpr","rsibling","runescape","funescape","escape","nonHex","high","String","fromCharCode","rcssescape","fcssescape","ch","asCodePoint","charCodeAt","toString","unloadHandler","inDisabledFieldset","addCombinator","disabled","nodeName","toLowerCase","dir","next","apply","call","childNodes","nodeType","e","target","els","j","Sizzle","selector","context","results","seed","m","nid","match","groups","newSelector","newContext","ownerDocument","exec","getElementById","id","getElementsByTagName","getElementsByClassName","qsa","test","testContext","parentNode","scope","getAttribute","replace","setAttribute","toSelector","join","querySelectorAll","qsaError","removeAttribute","keys","cache","key","value","cacheLength","shift","markFunction","fn","assert","el","createElement","removeChild","addHandle","attrs","handler","split","attrHandle","siblingCheck","cur","diff","sourceIndex","nextSibling","createDisabledPseudo","isDisabled","createPositionalPseudo","argument","matchIndexes","namespace","namespaceURI","documentElement","node","hasCompare","subWindow","doc","defaultView","top","addEventListener","attachEvent","appendChild","cssHas","querySelector","className","createComment","getById","getElementsByName","filter","attrId","find","getAttributeNode","elems","tag","tmp","input","innerHTML","matchesSelector","webkitMatchesSelector","mozMatchesSelector","oMatchesSelector","msMatchesSelector","disconnectedMatch","compareDocumentPosition","adown","bup","compare","sortDetached","aup","ap","bp","unshift","expr","elements","ret","attr","name","val","undefined","specified","sel","error","msg","Error","uniqueSort","duplicates","detectDuplicates","sortStable","sort","splice","textContent","firstChild","nodeValue","selectors","createPseudo","relative",">","first"," ","+","~","preFilter","excess","unquoted","nodeNameSelector","pattern","operator","check","result","type","what","_argument","last","simple","forward","ofType","_context","xml","uniqueCache","outerCache","nodeIndex","start","parent","useCache","lastChild","uniqueID","pseudo","args","setFilters","idx","matched","not","matcher","unmatched","has","text","lang","elemLang","hash","location","root","focus","activeElement","hasFocus","href","tabIndex","enabled","checked","selected","selectedIndex","empty","header","button","_matchIndexes","eq","even","odd","lt","gt","radio","checkbox","file","password","image","createInputPseudo","submit","reset","createButtonPseudo","prototype","filters","parseOnly","tokens","soFar","preFilters","cached","combinator","base","skip","checkNonElements","doneName","oldCache","newCache","elementMatcher","matchers","multipleContexts","contexts","condense","map","newUnmatched","mapped","setMatcher","postFilter","postFinder","postSelector","temp","preMap","postMap","preexisting","matcherIn","matcherOut","matcherFromTokens","checkContext","leadingRelative","implicitRelative","matchContext","matchAnyContext","concat","matcherFromGroupMatchers","elementMatchers","setMatchers","bySet","byElement","superMatcher","outermost","matchedCount","setMatched","contextBackup","dirrunsUnique","Math","random","token","compiled","_name","defaultValue","_sizzle","noConflict","define","amd","module","exports"],"mappings":";CAUA,SAAYA,GACZ,IAAIC,EACHC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EAGAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EAGAC,EAAU,SAAW,EAAI,IAAIC,KAC7BC,EAAetB,EAAOa,SACtBU,EAAU,EACVC,EAAO,EACPC,EAAaC,KACbC,EAAaD,KACbE,EAAgBF,KAChBG,EAAyBH,KACzBI,EAAY,SAAUC,EAAGC,GAIxB,OAHKD,IAAMC,IACVrB,GAAe,GAET,GAIRsB,KAAgBC,eAChBC,KACAC,EAAMD,EAAIC,IACVC,EAAaF,EAAIG,KACjBA,EAAOH,EAAIG,KACXC,EAAQJ,EAAII,MAIZC,EAAU,SAAUC,EAAMC,GAGzB,IAFA,IAAIzC,EAAI,EACP0C,EAAMF,EAAKG,OACJ3C,EAAI0C,EAAK1C,IAChB,GAAKwC,EAAMxC,KAAQyC,EAClB,OAAOzC,EAGT,OAAQ,GAGT4C,EAAW,6HAMXC,EAAa,sBAGbC,EAAa,0BAA4BD,EACxC,0CAGDE,EAAa,MAAQF,EAAa,KAAOC,EAAa,OAASD,EAG9D,gBAAkBA,EAIlB,2DAA6DC,EAAa,OAC1ED,EAAa,OAEdG,EAAU,KAAOF,EAAa,wFAOAC,EAAa,eAO3CE,EAAc,IAAIC,OAAQL,EAAa,IAAK,KAC5CM,EAAQ,IAAID,OAAQ,IAAML,EAAa,8BACtCA,EAAa,KAAM,KAEpBO,EAAS,IAAIF,OAAQ,IAAML,EAAa,KAAOA,EAAa,KAC5DQ,EAAqB,IAAIH,OAAQ,IAAML,EAAa,WAAaA,EAAa,IAAMA,EACnF,KACDS,EAAW,IAAIJ,OAAQL,EAAa,MAEpCU,EAAU,IAAIL,OAAQF,GACtBQ,EAAc,IAAIN,OAAQ,IAAMJ,EAAa,KAE7CW,GACCC,GAAM,IAAIR,OAAQ,MAAQJ,EAAa,KACvCa,MAAS,IAAIT,OAAQ,QAAUJ,EAAa,KAC5Cc,IAAO,IAAIV,OAAQ,KAAOJ,EAAa,SACvCe,KAAQ,IAAIX,OAAQ,IAAMH,GAC1Be,OAAU,IAAIZ,OAAQ,IAAMF,GAC5Be,MAAS,IAAIb,OAAQ,yDACpBL,EAAa,+BAAiCA,EAAa,cAC3DA,EAAa,aAAeA,EAAa,SAAU,KACpDmB,KAAQ,IAAId,OAAQ,OAASN,EAAW,KAAM,KAI9CqB,aAAgB,IAAIf,OAAQ,IAAML,EACjC,mDAAqDA,EACrD,mBAAqBA,EAAa,mBAAoB,MAGxDqB,EAAQ,SACRC,EAAU,sCACVC,EAAU,SAEVC,EAAU,yBAGVC,EAAa,mCAEbC,GAAW,OAIXC,GAAY,IAAItB,OAAQ,uBAAyBL,EAAa,uBAAwB,KACtF4B,GAAY,SAAUC,EAAQC,GAC7B,IAAIC,EAAO,KAAOF,EAAOpC,MAAO,GAAM,MAEtC,OAAOqC,IASNC,EAAO,EACNC,OAAOC,aAAcF,EAAO,OAC5BC,OAAOC,aAAcF,GAAQ,GAAK,MAAe,KAAPA,EAAe,SAK5DG,GAAa,sDACbC,GAAa,SAAUC,EAAIC,GAC1B,OAAKA,EAGQ,OAAPD,EACG,SAIDA,EAAG3C,MAAO,GAAI,GAAM,KAC1B2C,EAAGE,WAAYF,EAAGtC,OAAS,GAAIyC,SAAU,IAAO,IAI3C,KAAOH,GAOfI,GAAgB,WACf1E,KAGD2E,GAAqBC,GACpB,SAAU9C,GACT,OAAyB,IAAlBA,EAAK+C,UAAqD,aAAhC/C,EAAKgD,SAASC,gBAE9CC,IAAK,aAAcC,KAAM,WAI7B,IACCvD,EAAKwD,MACF3D,EAAMI,EAAMwD,KAAMzE,EAAa0E,YACjC1E,EAAa0E,YAMd7D,EAAKb,EAAa0E,WAAWpD,QAASqD,SACrC,MAAQC,GACT5D,GAASwD,MAAO3D,EAAIS,OAGnB,SAAUuD,EAAQC,GACjB/D,EAAWyD,MAAOK,EAAQ5D,EAAMwD,KAAMK,KAKvC,SAAUD,EAAQC,GACjB,IAAIC,EAAIF,EAAOvD,OACd3C,EAAI,EAGL,MAAUkG,EAAQE,KAAQD,EAAKnG,MAC/BkG,EAAOvD,OAASyD,EAAI,IAKvB,SAASC,GAAQC,EAAUC,EAASC,EAASC,GAC5C,IAAIC,EAAG1G,EAAGyC,EAAMkE,EAAKC,EAAOC,EAAQC,EACnCC,EAAaR,GAAWA,EAAQS,cAGhChB,EAAWO,EAAUA,EAAQP,SAAW,EAKzC,GAHAQ,EAAUA,MAGe,iBAAbF,IAA0BA,GACxB,IAAbN,GAA+B,IAAbA,GAA+B,KAAbA,EAEpC,OAAOQ,EAIR,IAAMC,IACL9F,EAAa4F,GACbA,EAAUA,GAAW3F,EAEhBE,GAAiB,CAIrB,GAAkB,KAAbkF,IAAqBY,EAAQtC,EAAW2C,KAAMX,IAGlD,GAAOI,EAAIE,EAAO,IAGjB,GAAkB,IAAbZ,EAAiB,CACrB,KAAOvD,EAAO8D,EAAQW,eAAgBR,IAUrC,OAAOF,EALP,GAAK/D,EAAK0E,KAAOT,EAEhB,OADAF,EAAQnE,KAAMI,GACP+D,OAYT,GAAKO,IAAgBtE,EAAOsE,EAAWG,eAAgBR,KACtDxF,EAAUqF,EAAS9D,IACnBA,EAAK0E,KAAOT,EAGZ,OADAF,EAAQnE,KAAMI,GACP+D,MAKH,CAAA,GAAKI,EAAO,GAElB,OADAvE,EAAKwD,MAAOW,EAASD,EAAQa,qBAAsBd,IAC5CE,EAGD,IAAOE,EAAIE,EAAO,KAAS3G,EAAQoH,wBACzCd,EAAQc,uBAGR,OADAhF,EAAKwD,MAAOW,EAASD,EAAQc,uBAAwBX,IAC9CF,EAKT,GAAKvG,EAAQqH,MACX1F,EAAwB0E,EAAW,QACjCvF,IAAcA,EAAUwG,KAAMjB,MAIlB,IAAbN,GAAqD,WAAnCO,EAAQd,SAASC,eAA+B,CAYpE,GAVAoB,EAAcR,EACdS,EAAaR,EASK,IAAbP,IACF1C,EAASiE,KAAMjB,IAAcjD,EAAmBkE,KAAMjB,IAAe,EAGvES,EAAaxC,GAASgD,KAAMjB,IAAckB,GAAajB,EAAQkB,aAC9DlB,KAImBA,GAAYtG,EAAQyH,SAGhCf,EAAMJ,EAAQoB,aAAc,OAClChB,EAAMA,EAAIiB,QAAS7C,GAAYC,IAE/BuB,EAAQsB,aAAc,KAAQlB,EAAMxF,IAMtCnB,GADA6G,EAASxG,EAAUiG,IACR3D,OACX,MAAQ3C,IACP6G,EAAQ7G,IAAQ2G,EAAM,IAAMA,EAAM,UAAa,IAC9CmB,GAAYjB,EAAQ7G,IAEtB8G,EAAcD,EAAOkB,KAAM,KAG5B,IAIC,OAHA1F,EAAKwD,MAAOW,EACXO,EAAWiB,iBAAkBlB,IAEvBN,EACN,MAAQyB,GACTrG,EAAwB0E,GAAU,GACjC,QACIK,IAAQxF,GACZoF,EAAQ2B,gBAAiB,QAQ9B,OAAO3H,EAAQ+F,EAASsB,QAASzE,EAAO,MAAQoD,EAASC,EAASC,GASnE,SAAShF,KACR,IAAI0G,KAEJ,SAASC,EAAOC,EAAKC,GAQpB,OALKH,EAAK9F,KAAMgG,EAAM,KAAQnI,EAAKqI,oBAG3BH,EAAOD,EAAKK,SAEXJ,EAAOC,EAAM,KAAQC,EAE/B,OAAOF,EAOR,SAASK,GAAcC,GAEtB,OADAA,EAAIvH,IAAY,EACTuH,EAOR,SAASC,GAAQD,GAChB,IAAIE,EAAKhI,EAASiI,cAAe,YAEjC,IACC,QAASH,EAAIE,GACZ,MAAQ3C,GACT,OAAO,EACN,QAGI2C,EAAGnB,YACPmB,EAAGnB,WAAWqB,YAAaF,GAI5BA,EAAK,MASP,SAASG,GAAWC,EAAOC,GAC1B,IAAI/G,EAAM8G,EAAME,MAAO,KACtBlJ,EAAIkC,EAAIS,OAET,MAAQ3C,IACPE,EAAKiJ,WAAYjH,EAAKlC,IAAQiJ,EAUhC,SAASG,GAActH,EAAGC,GACzB,IAAIsH,EAAMtH,GAAKD,EACdwH,EAAOD,GAAsB,IAAfvH,EAAEkE,UAAiC,IAAfjE,EAAEiE,UACnClE,EAAEyH,YAAcxH,EAAEwH,YAGpB,GAAKD,EACJ,OAAOA,EAIR,GAAKD,EACJ,MAAUA,EAAMA,EAAIG,YACnB,GAAKH,IAAQtH,EACZ,OAAQ,EAKX,OAAOD,EAAI,GAAK,EA6BjB,SAAS2H,GAAsBjE,GAG9B,OAAO,SAAU/C,GAKhB,MAAK,SAAUA,EASTA,EAAKgF,aAAgC,IAAlBhF,EAAK+C,SAGvB,UAAW/C,EACV,UAAWA,EAAKgF,WACbhF,EAAKgF,WAAWjC,WAAaA,EAE7B/C,EAAK+C,WAAaA,EAMpB/C,EAAKiH,aAAelE,GAI1B/C,EAAKiH,cAAgBlE,GACrBF,GAAoB7C,KAAW+C,EAG1B/C,EAAK+C,WAAaA,EAKd,UAAW/C,GACfA,EAAK+C,WAAaA,GAY5B,SAASmE,GAAwBjB,GAChC,OAAOD,GAAc,SAAUmB,GAE9B,OADAA,GAAYA,EACLnB,GAAc,SAAUhC,EAAMxF,GACpC,IAAImF,EACHyD,EAAenB,KAAQjC,EAAK9D,OAAQiH,GACpC5J,EAAI6J,EAAalH,OAGlB,MAAQ3C,IACFyG,EAAQL,EAAIyD,EAAc7J,MAC9ByG,EAAML,KAASnF,EAASmF,GAAMK,EAAML,SAYzC,SAASoB,GAAajB,GACrB,OAAOA,QAAmD,IAAjCA,EAAQa,sBAAwCb,EAI1EtG,EAAUoG,GAAOpG,WAOjBG,EAAQiG,GAAOjG,MAAQ,SAAUqC,GAChC,IAAIqH,EAAYrH,GAAQA,EAAKsH,aAC5BlJ,EAAU4B,IAAUA,EAAKuE,eAAiBvE,GAAOuH,gBAKlD,OAAQ9F,EAAMqD,KAAMuC,GAAajJ,GAAWA,EAAQ4E,UAAY,SAQjE9E,EAAc0F,GAAO1F,YAAc,SAAUsJ,GAC5C,IAAIC,EAAYC,EACfC,EAAMH,EAAOA,EAAKjD,eAAiBiD,EAAO5I,EAO3C,OAAK+I,GAAOxJ,GAA6B,IAAjBwJ,EAAIpE,UAAmBoE,EAAIJ,iBAKnDpJ,EAAWwJ,EACXvJ,EAAUD,EAASoJ,gBACnBlJ,GAAkBV,EAAOQ,GAQpBS,GAAgBT,IAClBuJ,EAAYvJ,EAASyJ,cAAiBF,EAAUG,MAAQH,IAGrDA,EAAUI,iBACdJ,EAAUI,iBAAkB,SAAUlF,IAAe,GAG1C8E,EAAUK,aACrBL,EAAUK,YAAa,WAAYnF,KASrCpF,EAAQyH,MAAQiB,GAAQ,SAAUC,GAEjC,OADA/H,EAAQ4J,YAAa7B,GAAK6B,YAAa7J,EAASiI,cAAe,aACzB,IAAxBD,EAAGZ,mBACfY,EAAGZ,iBAAkB,uBAAwBrF,SAYhD1C,EAAQyK,OAAS/B,GAAQ,WACxB,IAEC,OADA/H,EAAS+J,cAAe,oBACjB,EACN,MAAQ1E,GACT,OAAO,KAUThG,EAAQ8C,WAAa4F,GAAQ,SAAUC,GAEtC,OADAA,EAAGgC,UAAY,KACPhC,EAAGjB,aAAc,eAO1B1H,EAAQmH,qBAAuBuB,GAAQ,SAAUC,GAEhD,OADAA,EAAG6B,YAAa7J,EAASiK,cAAe,MAChCjC,EAAGxB,qBAAsB,KAAMzE,SAIxC1C,EAAQoH,uBAAyBhD,EAAQkD,KAAM3G,EAASyG,wBAMxDpH,EAAQ6K,QAAUnC,GAAQ,SAAUC,GAEnC,OADA/H,EAAQ4J,YAAa7B,GAAKzB,GAAKhG,GACvBP,EAASmK,oBAAsBnK,EAASmK,kBAAmB5J,GAAUwB,SAIzE1C,EAAQ6K,SACZ5K,EAAK8K,OAAa,GAAI,SAAU7D,GAC/B,IAAI8D,EAAS9D,EAAGS,QAASpD,GAAWC,IACpC,OAAO,SAAUhC,GAChB,OAAOA,EAAKkF,aAAc,QAAWsD,IAGvC/K,EAAKgL,KAAW,GAAI,SAAU/D,EAAIZ,GACjC,QAAuC,IAA3BA,EAAQW,gBAAkCpG,EAAiB,CACtE,IAAI2B,EAAO8D,EAAQW,eAAgBC,GACnC,OAAO1E,GAASA,UAIlBvC,EAAK8K,OAAa,GAAK,SAAU7D,GAChC,IAAI8D,EAAS9D,EAAGS,QAASpD,GAAWC,IACpC,OAAO,SAAUhC,GAChB,IAAIwH,OAAwC,IAA1BxH,EAAK0I,kBACtB1I,EAAK0I,iBAAkB,MACxB,OAAOlB,GAAQA,EAAK3B,QAAU2C,IAMhC/K,EAAKgL,KAAW,GAAI,SAAU/D,EAAIZ,GACjC,QAAuC,IAA3BA,EAAQW,gBAAkCpG,EAAiB,CACtE,IAAImJ,EAAMjK,EAAGoL,EACZ3I,EAAO8D,EAAQW,eAAgBC,GAEhC,GAAK1E,EAAO,CAIX,IADAwH,EAAOxH,EAAK0I,iBAAkB,QACjBlB,EAAK3B,QAAUnB,EAC3B,OAAS1E,GAIV2I,EAAQ7E,EAAQwE,kBAAmB5D,GACnCnH,EAAI,EACJ,MAAUyC,EAAO2I,EAAOpL,KAEvB,IADAiK,EAAOxH,EAAK0I,iBAAkB,QACjBlB,EAAK3B,QAAUnB,EAC3B,OAAS1E,GAKZ,YAMHvC,EAAKgL,KAAY,IAAIjL,EAAQmH,qBAC5B,SAAUiE,EAAK9E,GACd,YAA6C,IAAjCA,EAAQa,qBACZb,EAAQa,qBAAsBiE,GAG1BpL,EAAQqH,IACZf,EAAQyB,iBAAkBqD,QAD3B,GAKR,SAAUA,EAAK9E,GACd,IAAI9D,EACH6I,KACAtL,EAAI,EAGJwG,EAAUD,EAAQa,qBAAsBiE,GAGzC,GAAa,MAARA,EAAc,CAClB,MAAU5I,EAAO+D,EAASxG,KACF,IAAlByC,EAAKuD,UACTsF,EAAIjJ,KAAMI,GAIZ,OAAO6I,EAER,OAAO9E,GAITtG,EAAKgL,KAAc,MAAIjL,EAAQoH,wBAA0B,SAAUuD,EAAWrE,GAC7E,QAA+C,IAAnCA,EAAQc,wBAA0CvG,EAC7D,OAAOyF,EAAQc,uBAAwBuD,IAUzC5J,KAOAD,MAEOd,EAAQqH,IAAMjD,EAAQkD,KAAM3G,EAASoH,qBAI3CW,GAAQ,SAAUC,GAEjB,IAAI2C,EAOJ1K,EAAQ4J,YAAa7B,GAAK4C,UAAY,UAAYrK,EAAU,qBAC1CA,EAAU,kEAOvByH,EAAGZ,iBAAkB,wBAAyBrF,QAClD5B,EAAUsB,KAAM,SAAWQ,EAAa,gBAKnC+F,EAAGZ,iBAAkB,cAAerF,QACzC5B,EAAUsB,KAAM,MAAQQ,EAAa,aAAeD,EAAW,KAI1DgG,EAAGZ,iBAAkB,QAAU7G,EAAU,MAAOwB,QACrD5B,EAAUsB,KAAM,OAQjBkJ,EAAQ3K,EAASiI,cAAe,UAC1BhB,aAAc,OAAQ,IAC5Be,EAAG6B,YAAac,GACV3C,EAAGZ,iBAAkB,aAAcrF,QACxC5B,EAAUsB,KAAM,MAAQQ,EAAa,QAAUA,EAAa,KAC3DA,EAAa,gBAMT+F,EAAGZ,iBAAkB,YAAarF,QACvC5B,EAAUsB,KAAM,YAMXuG,EAAGZ,iBAAkB,KAAO7G,EAAU,MAAOwB,QAClD5B,EAAUsB,KAAM,YAKjBuG,EAAGZ,iBAAkB,QACrBjH,EAAUsB,KAAM,iBAGjBsG,GAAQ,SAAUC,GACjBA,EAAG4C,UAAY,oFAKf,IAAID,EAAQ3K,EAASiI,cAAe,SACpC0C,EAAM1D,aAAc,OAAQ,UAC5Be,EAAG6B,YAAac,GAAQ1D,aAAc,OAAQ,KAIzCe,EAAGZ,iBAAkB,YAAarF,QACtC5B,EAAUsB,KAAM,OAASQ,EAAa,eAKW,IAA7C+F,EAAGZ,iBAAkB,YAAarF,QACtC5B,EAAUsB,KAAM,WAAY,aAK7BxB,EAAQ4J,YAAa7B,GAAKpD,UAAW,EACc,IAA9CoD,EAAGZ,iBAAkB,aAAcrF,QACvC5B,EAAUsB,KAAM,WAAY,aAK7BuG,EAAGZ,iBAAkB,QACrBjH,EAAUsB,KAAM,YAIXpC,EAAQwL,gBAAkBpH,EAAQkD,KAAQtG,EAAUJ,EAAQI,SAClEJ,EAAQ6K,uBACR7K,EAAQ8K,oBACR9K,EAAQ+K,kBACR/K,EAAQgL,qBAERlD,GAAQ,SAAUC,GAIjB3I,EAAQ6L,kBAAoB7K,EAAQ6E,KAAM8C,EAAI,KAI9C3H,EAAQ6E,KAAM8C,EAAI,aAClB5H,EAAcqB,KAAM,KAAMW,KAItB/C,EAAQyK,QAQb3J,EAAUsB,KAAM,QAGjBtB,EAAYA,EAAU4B,QAAU,IAAIO,OAAQnC,EAAUgH,KAAM,MAC5D/G,EAAgBA,EAAc2B,QAAU,IAAIO,OAAQlC,EAAc+G,KAAM,MAIxEmC,EAAa7F,EAAQkD,KAAM1G,EAAQkL,yBAKnC7K,EAAWgJ,GAAc7F,EAAQkD,KAAM1G,EAAQK,UAC9C,SAAUY,EAAGC,GAQZ,IAAIiK,EAAuB,IAAflK,EAAEkE,UAAkBlE,EAAEkI,iBAAmBlI,EACpDmK,EAAMlK,GAAKA,EAAE0F,WACd,OAAO3F,IAAMmK,MAAWA,GAAwB,IAAjBA,EAAIjG,YAClCgG,EAAM9K,SACL8K,EAAM9K,SAAU+K,GAChBnK,EAAEiK,yBAA8D,GAAnCjK,EAAEiK,wBAAyBE,MAG3D,SAAUnK,EAAGC,GACZ,GAAKA,EACJ,MAAUA,EAAIA,EAAE0F,WACf,GAAK1F,IAAMD,EACV,OAAO,EAIV,OAAO,GAOTD,EAAYqI,EACZ,SAAUpI,EAAGC,GAGZ,GAAKD,IAAMC,EAEV,OADArB,GAAe,EACR,EAIR,IAAIwL,GAAWpK,EAAEiK,yBAA2BhK,EAAEgK,wBAC9C,OAAKG,IAgBU,GAPfA,GAAYpK,EAAEkF,eAAiBlF,KAASC,EAAEiF,eAAiBjF,GAC1DD,EAAEiK,wBAAyBhK,GAG3B,KAIG9B,EAAQkM,cAAgBpK,EAAEgK,wBAAyBjK,KAAQoK,EAOzDpK,GAAKlB,GAAYkB,EAAEkF,eAAiB3F,GACxCH,EAAUG,EAAcS,IAChB,EAOJC,GAAKnB,GAAYmB,EAAEiF,eAAiB3F,GACxCH,EAAUG,EAAcU,GACjB,EAIDtB,EACJ8B,EAAS9B,EAAWqB,GAAMS,EAAS9B,EAAWsB,GAChD,EAGe,EAAVmK,GAAe,EAAI,IAE3B,SAAUpK,EAAGC,GAGZ,GAAKD,IAAMC,EAEV,OADArB,GAAe,EACR,EAGR,IAAI2I,EACHrJ,EAAI,EACJoM,EAAMtK,EAAE2F,WACRwE,EAAMlK,EAAE0F,WACR4E,GAAOvK,GACPwK,GAAOvK,GAGR,IAAMqK,IAAQH,EAMb,OAAOnK,GAAKlB,GAAY,EACvBmB,GAAKnB,EAAW,EAEhBwL,GAAO,EACPH,EAAM,EACNxL,EACE8B,EAAS9B,EAAWqB,GAAMS,EAAS9B,EAAWsB,GAChD,EAGK,GAAKqK,IAAQH,EACnB,OAAO7C,GAActH,EAAGC,GAIzBsH,EAAMvH,EACN,MAAUuH,EAAMA,EAAI5B,WACnB4E,EAAGE,QAASlD,GAEbA,EAAMtH,EACN,MAAUsH,EAAMA,EAAI5B,WACnB6E,EAAGC,QAASlD,GAIb,MAAQgD,EAAIrM,KAAQsM,EAAItM,GACvBA,IAGD,OAAOA,EAGNoJ,GAAciD,EAAIrM,GAAKsM,EAAItM,IAO3BqM,EAAIrM,IAAOqB,GAAgB,EAC3BiL,EAAItM,IAAOqB,EAAe,EAE1B,GAGKT,GAnfCA,GAsfTyF,GAAOpF,QAAU,SAAUuL,EAAMC,GAChC,OAAOpG,GAAQmG,EAAM,KAAM,KAAMC,IAGlCpG,GAAOoF,gBAAkB,SAAUhJ,EAAM+J,GAGxC,GAFA7L,EAAa8B,GAERxC,EAAQwL,iBAAmB3K,IAC9Bc,EAAwB4K,EAAO,QAC7BxL,IAAkBA,EAAcuG,KAAMiF,OACtCzL,IAAkBA,EAAUwG,KAAMiF,IAErC,IACC,IAAIE,EAAMzL,EAAQ6E,KAAMrD,EAAM+J,GAG9B,GAAKE,GAAOzM,EAAQ6L,mBAInBrJ,EAAK7B,UAAuC,KAA3B6B,EAAK7B,SAASoF,SAC/B,OAAO0G,EAEP,MAAQzG,GACTrE,EAAwB4K,GAAM,GAIhC,OAAOnG,GAAQmG,EAAM5L,EAAU,MAAQ6B,IAASE,OAAS,GAG1D0D,GAAOnF,SAAW,SAAUqF,EAAS9D,GAUpC,OAHO8D,EAAQS,eAAiBT,IAAa3F,GAC5CD,EAAa4F,GAEPrF,EAAUqF,EAAS9D,IAG3B4D,GAAOsG,KAAO,SAAUlK,EAAMmK,IAOtBnK,EAAKuE,eAAiBvE,IAAU7B,GACtCD,EAAa8B,GAGd,IAAIiG,EAAKxI,EAAKiJ,WAAYyD,EAAKlH,eAG9BmH,EAAMnE,GAAM1G,EAAO8D,KAAM5F,EAAKiJ,WAAYyD,EAAKlH,eAC9CgD,EAAIjG,EAAMmK,GAAO9L,QACjBgM,EAEF,YAAeA,IAARD,EACNA,EACA5M,EAAQ8C,aAAejC,EACtB2B,EAAKkF,aAAciF,IACjBC,EAAMpK,EAAK0I,iBAAkByB,KAAYC,EAAIE,UAC9CF,EAAIvE,MACJ,MAGJjC,GAAO3B,OAAS,SAAUsI,GACzB,OAASA,EAAM,IAAKpF,QAAS7C,GAAYC,KAG1CqB,GAAO4G,MAAQ,SAAUC,GACxB,MAAM,IAAIC,MAAO,0CAA4CD,IAO9D7G,GAAO+G,WAAa,SAAU5G,GAC7B,IAAI/D,EACH4K,KACAjH,EAAI,EACJpG,EAAI,EAOL,GAJAU,GAAgBT,EAAQqN,iBACxB7M,GAAaR,EAAQsN,YAAc/G,EAAQlE,MAAO,GAClDkE,EAAQgH,KAAM3L,GAETnB,EAAe,CACnB,MAAU+B,EAAO+D,EAASxG,KACpByC,IAAS+D,EAASxG,KACtBoG,EAAIiH,EAAWhL,KAAMrC,IAGvB,MAAQoG,IACPI,EAAQiH,OAAQJ,EAAYjH,GAAK,GAQnC,OAFA3F,EAAY,KAEL+F,GAORrG,EAAUkG,GAAOlG,QAAU,SAAUsC,GACpC,IAAIwH,EACHyC,EAAM,GACN1M,EAAI,EACJgG,EAAWvD,EAAKuD,SAEjB,GAAMA,GAQC,GAAkB,IAAbA,GAA+B,IAAbA,GAA+B,KAAbA,EAAkB,CAIjE,GAAiC,iBAArBvD,EAAKiL,YAChB,OAAOjL,EAAKiL,YAIZ,IAAMjL,EAAOA,EAAKkL,WAAYlL,EAAMA,EAAOA,EAAK+G,YAC/CkD,GAAOvM,EAASsC,QAGZ,GAAkB,IAAbuD,GAA+B,IAAbA,EAC7B,OAAOvD,EAAKmL,eAnBZ,MAAU3D,EAAOxH,EAAMzC,KAGtB0M,GAAOvM,EAAS8J,GAqBlB,OAAOyC,IAGRxM,EAAOmG,GAAOwH,WAGbtF,YAAa,GAEbuF,aAAcrF,GAEd7B,MAAOnD,EAEP0F,cAEA+B,QAEA6C,UACCC,KAAOrI,IAAK,aAAcsI,OAAO,GACjCC,KAAOvI,IAAK,cACZwI,KAAOxI,IAAK,kBAAmBsI,OAAO,GACtCG,KAAOzI,IAAK,oBAGb0I,WACCxK,KAAQ,SAAU+C,GAWjB,OAVAA,EAAO,GAAMA,EAAO,GAAIgB,QAASpD,GAAWC,IAG5CmC,EAAO,IAAQA,EAAO,IAAOA,EAAO,IACnCA,EAAO,IAAO,IAAKgB,QAASpD,GAAWC,IAEpB,OAAfmC,EAAO,KACXA,EAAO,GAAM,IAAMA,EAAO,GAAM,KAG1BA,EAAMtE,MAAO,EAAG,IAGxByB,MAAS,SAAU6C,GAiClB,OArBAA,EAAO,GAAMA,EAAO,GAAIlB,cAEU,QAA7BkB,EAAO,GAAItE,MAAO,EAAG,IAGnBsE,EAAO,IACZP,GAAO4G,MAAOrG,EAAO,IAKtBA,EAAO,KAASA,EAAO,GACtBA,EAAO,IAAQA,EAAO,IAAO,GAC7B,GAAqB,SAAfA,EAAO,IAAiC,QAAfA,EAAO,KACvCA,EAAO,KAAWA,EAAO,GAAMA,EAAO,IAAwB,QAAfA,EAAO,KAG3CA,EAAO,IAClBP,GAAO4G,MAAOrG,EAAO,IAGfA,GAGR9C,OAAU,SAAU8C,GACnB,IAAI0H,EACHC,GAAY3H,EAAO,IAAOA,EAAO,GAElC,OAAKnD,EAAmB,MAAE8D,KAAMX,EAAO,IAC/B,MAIHA,EAAO,GACXA,EAAO,GAAMA,EAAO,IAAOA,EAAO,IAAO,GAG9B2H,GAAYhL,EAAQgE,KAAMgH,KAGnCD,EAASjO,EAAUkO,GAAU,MAG7BD,EAASC,EAAShM,QAAS,IAAKgM,EAAS5L,OAAS2L,GAAWC,EAAS5L,UAGxEiE,EAAO,GAAMA,EAAO,GAAItE,MAAO,EAAGgM,GAClC1H,EAAO,GAAM2H,EAASjM,MAAO,EAAGgM,IAI1B1H,EAAMtE,MAAO,EAAG,MAIzB0I,QAECpH,IAAO,SAAU4K,GAChB,IAAI/I,EAAW+I,EAAiB5G,QAASpD,GAAWC,IAAYiB,cAChE,MAA4B,MAArB8I,EACN,WACC,OAAO,GAER,SAAU/L,GACT,OAAOA,EAAKgD,UAAYhD,EAAKgD,SAASC,gBAAkBD,IAI3D9B,MAAS,SAAUiH,GAClB,IAAI6D,EAAUjN,EAAYoJ,EAAY,KAEtC,OAAO6D,IACJA,EAAU,IAAIvL,OAAQ,MAAQL,EAC/B,IAAM+H,EAAY,IAAM/H,EAAa,SAAarB,EACjDoJ,EAAW,SAAUnI,GACpB,OAAOgM,EAAQlH,KACY,iBAAnB9E,EAAKmI,WAA0BnI,EAAKmI,gBACd,IAAtBnI,EAAKkF,cACXlF,EAAKkF,aAAc,UACpB,OAKN9D,KAAQ,SAAU+I,EAAM8B,EAAUC,GACjC,OAAO,SAAUlM,GAChB,IAAImM,EAASvI,GAAOsG,KAAMlK,EAAMmK,GAEhC,OAAe,MAAVgC,EACgB,OAAbF,GAEFA,IAINE,GAAU,GAIU,MAAbF,EAAmBE,IAAWD,EACvB,OAAbD,EAAoBE,IAAWD,EAClB,OAAbD,EAAoBC,GAAqC,IAA5BC,EAAOrM,QAASoM,GAChC,OAAbD,EAAoBC,GAASC,EAAOrM,QAASoM,IAAW,EAC3C,OAAbD,EAAoBC,GAASC,EAAOtM,OAAQqM,EAAMhM,UAAagM,EAClD,OAAbD,GAAsB,IAAME,EAAOhH,QAAS3E,EAAa,KAAQ,KAAMV,QAASoM,IAAW,EAC9E,OAAbD,IAAoBE,IAAWD,GAASC,EAAOtM,MAAO,EAAGqM,EAAMhM,OAAS,KAAQgM,EAAQ,QAO3F5K,MAAS,SAAU8K,EAAMC,EAAMC,EAAWd,EAAOe,GAChD,IAAIC,EAAgC,QAAvBJ,EAAKvM,MAAO,EAAG,GAC3B4M,EAA+B,SAArBL,EAAKvM,OAAQ,GACvB6M,EAAkB,YAATL,EAEV,OAAiB,IAAVb,GAAwB,IAATe,EAGrB,SAAUvM,GACT,QAASA,EAAKgF,YAGf,SAAUhF,EAAM2M,EAAUC,GACzB,IAAIjH,EAAOkH,EAAaC,EAAYtF,EAAMuF,EAAWC,EACpD9J,EAAMsJ,IAAWC,EAAU,cAAgB,kBAC3CQ,EAASjN,EAAKgF,WACdmF,EAAOuC,GAAU1M,EAAKgD,SAASC,cAC/BiK,GAAYN,IAAQF,EACpB7F,GAAO,EAER,GAAKoG,EAAS,CAGb,GAAKT,EAAS,CACb,MAAQtJ,EAAM,CACbsE,EAAOxH,EACP,MAAUwH,EAAOA,EAAMtE,GACtB,GAAKwJ,EACJlF,EAAKxE,SAASC,gBAAkBkH,EACd,IAAlB3C,EAAKjE,SAEL,OAAO,EAKTyJ,EAAQ9J,EAAe,SAATkJ,IAAoBY,GAAS,cAE5C,OAAO,EAMR,GAHAA,GAAUP,EAAUQ,EAAO/B,WAAa+B,EAAOE,WAG1CV,GAAWS,EAAW,CAe1BrG,GADAkG,GADApH,GAHAkH,GAJAC,GADAtF,EAAOyF,GACYvO,KAAe8I,EAAM9I,QAId8I,EAAK4F,YAC5BN,EAAYtF,EAAK4F,eAEChB,QACF,KAAQvN,GAAW8G,EAAO,KACzBA,EAAO,GAC3B6B,EAAOuF,GAAaE,EAAO3J,WAAYyJ,GAEvC,MAAUvF,IAASuF,GAAavF,GAAQA,EAAMtE,KAG3C2D,EAAOkG,EAAY,IAAOC,EAAMtN,MAGlC,GAAuB,IAAlB8H,EAAKjE,YAAoBsD,GAAQW,IAASxH,EAAO,CACrD6M,EAAaT,IAAWvN,EAASkO,EAAWlG,GAC5C,YAyBF,GAlBKqG,IAaJrG,EADAkG,GADApH,GAHAkH,GAJAC,GADAtF,EAAOxH,GACYtB,KAAe8I,EAAM9I,QAId8I,EAAK4F,YAC5BN,EAAYtF,EAAK4F,eAEChB,QACF,KAAQvN,GAAW8G,EAAO,KAMhC,IAATkB,EAGJ,MAAUW,IAASuF,GAAavF,GAAQA,EAAMtE,KAC3C2D,EAAOkG,EAAY,IAAOC,EAAMtN,MAElC,IAAOgN,EACNlF,EAAKxE,SAASC,gBAAkBkH,EACd,IAAlB3C,EAAKjE,aACHsD,IAGGqG,KAMJL,GALAC,EAAatF,EAAM9I,KAChB8I,EAAM9I,QAIiB8I,EAAK4F,YAC5BN,EAAYtF,EAAK4F,eAEPhB,IAAWvN,EAASgI,IAG7BW,IAASxH,GACb,MASL,OADA6G,GAAQ0F,KACQf,GAAW3E,EAAO2E,GAAU,GAAK3E,EAAO2E,GAAS,KAKrEnK,OAAU,SAAUgM,EAAQlG,GAM3B,IAAImG,EACHrH,EAAKxI,EAAK8C,QAAS8M,IAAY5P,EAAK8P,WAAYF,EAAOpK,gBACtDW,GAAO4G,MAAO,uBAAyB6C,GAKzC,OAAKpH,EAAIvH,GACDuH,EAAIkB,GAIPlB,EAAG/F,OAAS,GAChBoN,GAASD,EAAQA,EAAQ,GAAIlG,GACtB1J,EAAK8P,WAAW/N,eAAgB6N,EAAOpK,eAC7C+C,GAAc,SAAUhC,EAAMxF,GAC7B,IAAIgP,EACHC,EAAUxH,EAAIjC,EAAMmD,GACpB5J,EAAIkQ,EAAQvN,OACb,MAAQ3C,IAEPyG,EADAwJ,EAAM1N,EAASkE,EAAMyJ,EAASlQ,OACbiB,EAASgP,GAAQC,EAASlQ,MAG7C,SAAUyC,GACT,OAAOiG,EAAIjG,EAAM,EAAGsN,KAIhBrH,IAIT1F,SAGCmN,IAAO1H,GAAc,SAAUnC,GAK9B,IAAIiF,KACH/E,KACA4J,EAAU9P,EAASgG,EAASsB,QAASzE,EAAO,OAE7C,OAAOiN,EAASjP,GACfsH,GAAc,SAAUhC,EAAMxF,EAASmO,EAAUC,GAChD,IAAI5M,EACH4N,EAAYD,EAAS3J,EAAM,KAAM4I,MACjCrP,EAAIyG,EAAK9D,OAGV,MAAQ3C,KACAyC,EAAO4N,EAAWrQ,MACxByG,EAAMzG,KAASiB,EAASjB,GAAMyC,MAIjC,SAAUA,EAAM2M,EAAUC,GAMzB,OALA9D,EAAO,GAAM9I,EACb2N,EAAS7E,EAAO,KAAM8D,EAAK7I,GAG3B+E,EAAO,GAAM,MACL/E,EAAQrE,SAInBmO,IAAO7H,GAAc,SAAUnC,GAC9B,OAAO,SAAU7D,GAChB,OAAO4D,GAAQC,EAAU7D,GAAOE,OAAS,KAI3CzB,SAAYuH,GAAc,SAAU8H,GAEnC,OADAA,EAAOA,EAAK3I,QAASpD,GAAWC,IACzB,SAAUhC,GAChB,OAASA,EAAKiL,aAAevN,EAASsC,IAASF,QAASgO,IAAU,KAWpEC,KAAQ/H,GAAc,SAAU+H,GAO/B,OAJMhN,EAAY+D,KAAMiJ,GAAQ,KAC/BnK,GAAO4G,MAAO,qBAAuBuD,GAEtCA,EAAOA,EAAK5I,QAASpD,GAAWC,IAAYiB,cACrC,SAAUjD,GAChB,IAAIgO,EACJ,GACC,GAAOA,EAAW3P,EACjB2B,EAAK+N,KACL/N,EAAKkF,aAAc,aAAgBlF,EAAKkF,aAAc,QAGtD,OADA8I,EAAWA,EAAS/K,iBACA8K,GAA2C,IAAnCC,EAASlO,QAASiO,EAAO,YAE3C/N,EAAOA,EAAKgF,aAAkC,IAAlBhF,EAAKuD,UAC7C,OAAO,KAKTE,OAAU,SAAUzD,GACnB,IAAIiO,EAAO3Q,EAAO4Q,UAAY5Q,EAAO4Q,SAASD,KAC9C,OAAOA,GAAQA,EAAKpO,MAAO,KAAQG,EAAK0E,IAGzCyJ,KAAQ,SAAUnO,GACjB,OAAOA,IAAS5B,GAGjBgQ,MAAS,SAAUpO,GAClB,OAAOA,IAAS7B,EAASkQ,iBACrBlQ,EAASmQ,UAAYnQ,EAASmQ,gBAC7BtO,EAAKoM,MAAQpM,EAAKuO,OAASvO,EAAKwO,WAItCC,QAAWzH,IAAsB,GACjCjE,SAAYiE,IAAsB,GAElC0H,QAAW,SAAU1O,GAIpB,IAAIgD,EAAWhD,EAAKgD,SAASC,cAC7B,MAAsB,UAAbD,KAA0BhD,EAAK0O,SACxB,WAAb1L,KAA2BhD,EAAK2O,UAGpCA,SAAY,SAAU3O,GASrB,OALKA,EAAKgF,YAEThF,EAAKgF,WAAW4J,eAGQ,IAAlB5O,EAAK2O,UAIbE,MAAS,SAAU7O,GAMlB,IAAMA,EAAOA,EAAKkL,WAAYlL,EAAMA,EAAOA,EAAK+G,YAC/C,GAAK/G,EAAKuD,SAAW,EACpB,OAAO,EAGT,OAAO,GAGR0J,OAAU,SAAUjN,GACnB,OAAQvC,EAAK8C,QAAiB,MAAGP,IAIlC8O,OAAU,SAAU9O,GACnB,OAAO2B,EAAQmD,KAAM9E,EAAKgD,WAG3B8F,MAAS,SAAU9I,GAClB,OAAO0B,EAAQoD,KAAM9E,EAAKgD,WAG3B+L,OAAU,SAAU/O,GACnB,IAAImK,EAAOnK,EAAKgD,SAASC,cACzB,MAAgB,UAATkH,GAAkC,WAAdnK,EAAKoM,MAA8B,WAATjC,GAGtD2D,KAAQ,SAAU9N,GACjB,IAAIkK,EACJ,MAAuC,UAAhClK,EAAKgD,SAASC,eACN,SAAdjD,EAAKoM,OAIuC,OAAxClC,EAAOlK,EAAKkF,aAAc,UACN,SAAvBgF,EAAKjH,gBAIRuI,MAAStE,GAAwB,WAChC,OAAS,KAGVqF,KAAQrF,GAAwB,SAAU8H,EAAe9O,GACxD,OAASA,EAAS,KAGnB+O,GAAM/H,GAAwB,SAAU8H,EAAe9O,EAAQiH,GAC9D,OAASA,EAAW,EAAIA,EAAWjH,EAASiH,KAG7C+H,KAAQhI,GAAwB,SAAUE,EAAclH,GAEvD,IADA,IAAI3C,EAAI,EACAA,EAAI2C,EAAQ3C,GAAK,EACxB6J,EAAaxH,KAAMrC,GAEpB,OAAO6J,IAGR+H,IAAOjI,GAAwB,SAAUE,EAAclH,GAEtD,IADA,IAAI3C,EAAI,EACAA,EAAI2C,EAAQ3C,GAAK,EACxB6J,EAAaxH,KAAMrC,GAEpB,OAAO6J,IAGRgI,GAAMlI,GAAwB,SAAUE,EAAclH,EAAQiH,GAM7D,IALA,IAAI5J,EAAI4J,EAAW,EAClBA,EAAWjH,EACXiH,EAAWjH,EACVA,EACAiH,IACQ5J,GAAK,GACd6J,EAAaxH,KAAMrC,GAEpB,OAAO6J,IAGRiI,GAAMnI,GAAwB,SAAUE,EAAclH,EAAQiH,GAE7D,IADA,IAAI5J,EAAI4J,EAAW,EAAIA,EAAWjH,EAASiH,IACjC5J,EAAI2C,GACbkH,EAAaxH,KAAMrC,GAEpB,OAAO6J,OAKL7G,QAAe,IAAI9C,EAAK8C,QAAc,GAG3C,IAAMhD,KAAO+R,OAAO,EAAMC,UAAU,EAAMC,MAAM,EAAMC,UAAU,EAAMC,OAAO,GAC5EjS,EAAK8C,QAAShD,GA7zCf,SAA4B6O,GAC3B,OAAO,SAAUpM,GAEhB,MAAgB,UADLA,EAAKgD,SAASC,eACEjD,EAAKoM,OAASA,GA0zCtBuD,CAAmBpS,GAExC,IAAMA,KAAOqS,QAAQ,EAAMC,OAAO,GACjCpS,EAAK8C,QAAShD,GArzCf,SAA6B6O,GAC5B,OAAO,SAAUpM,GAChB,IAAImK,EAAOnK,EAAKgD,SAASC,cACzB,OAAkB,UAATkH,GAA6B,WAATA,IAAuBnK,EAAKoM,OAASA,GAkzC/C0D,CAAoBvS,GAIzC,SAASgQ,MACTA,GAAWwC,UAAYtS,EAAKuS,QAAUvS,EAAK8C,QAC3C9C,EAAK8P,WAAa,IAAIA,GAEtB3P,EAAWgG,GAAOhG,SAAW,SAAUiG,EAAUoM,GAChD,IAAIxC,EAAStJ,EAAO+L,EAAQ9D,EAC3B+D,EAAO/L,EAAQgM,EACfC,EAASpR,EAAY4E,EAAW,KAEjC,GAAKwM,EACJ,OAAOJ,EAAY,EAAII,EAAOxQ,MAAO,GAGtCsQ,EAAQtM,EACRO,KACAgM,EAAa3S,EAAKmO,UAElB,MAAQuE,EAAQ,CAGT1C,KAAatJ,EAAQxD,EAAO6D,KAAM2L,MAClChM,IAGJgM,EAAQA,EAAMtQ,MAAOsE,EAAO,GAAIjE,SAAYiQ,GAE7C/L,EAAOxE,KAAQsQ,OAGhBzC,GAAU,GAGHtJ,EAAQvD,EAAmB4D,KAAM2L,MACvC1C,EAAUtJ,EAAM4B,QAChBmK,EAAOtQ,MACNiG,MAAO4H,EAGPrB,KAAMjI,EAAO,GAAIgB,QAASzE,EAAO,OAElCyP,EAAQA,EAAMtQ,MAAO4N,EAAQvN,SAI9B,IAAMkM,KAAQ3O,EAAK8K,SACXpE,EAAQnD,EAAWoL,GAAO5H,KAAM2L,KAAgBC,EAAYhE,MAChEjI,EAAQiM,EAAYhE,GAAQjI,MAC9BsJ,EAAUtJ,EAAM4B,QAChBmK,EAAOtQ,MACNiG,MAAO4H,EACPrB,KAAMA,EACN5N,QAAS2F,IAEVgM,EAAQA,EAAMtQ,MAAO4N,EAAQvN,SAI/B,IAAMuN,EACL,MAOF,OAAOwC,EACNE,EAAMjQ,OACNiQ,EACCvM,GAAO4G,MAAO3G,GAGd5E,EAAY4E,EAAUO,GAASvE,MAAO,IAGzC,SAASwF,GAAY6K,GAIpB,IAHA,IAAI3S,EAAI,EACP0C,EAAMiQ,EAAOhQ,OACb2D,EAAW,GACJtG,EAAI0C,EAAK1C,IAChBsG,GAAYqM,EAAQ3S,GAAIsI,MAEzB,OAAOhC,EAGR,SAASf,GAAe6K,EAAS2C,EAAYC,GAC5C,IAAIrN,EAAMoN,EAAWpN,IACpBsN,EAAOF,EAAWnN,KAClByC,EAAM4K,GAAQtN,EACduN,EAAmBF,GAAgB,eAAR3K,EAC3B8K,EAAW5R,IAEZ,OAAOwR,EAAW9E,MAGjB,SAAUxL,EAAM8D,EAAS8I,GACxB,MAAU5M,EAAOA,EAAMkD,GACtB,GAAuB,IAAlBlD,EAAKuD,UAAkBkN,EAC3B,OAAO9C,EAAS3N,EAAM8D,EAAS8I,GAGjC,OAAO,GAIR,SAAU5M,EAAM8D,EAAS8I,GACxB,IAAI+D,EAAU9D,EAAaC,EAC1B8D,GAAa/R,EAAS6R,GAGvB,GAAK9D,GACJ,MAAU5M,EAAOA,EAAMkD,GACtB,IAAuB,IAAlBlD,EAAKuD,UAAkBkN,IACtB9C,EAAS3N,EAAM8D,EAAS8I,GAC5B,OAAO,OAKV,MAAU5M,EAAOA,EAAMkD,GACtB,GAAuB,IAAlBlD,EAAKuD,UAAkBkN,EAQ3B,GAPA3D,EAAa9M,EAAMtB,KAAesB,EAAMtB,OAIxCmO,EAAcC,EAAY9M,EAAKoN,YAC5BN,EAAY9M,EAAKoN,cAEfoD,GAAQA,IAASxQ,EAAKgD,SAASC,cACnCjD,EAAOA,EAAMkD,IAASlD,MAChB,CAAA,IAAO2Q,EAAW9D,EAAajH,KACrC+K,EAAU,KAAQ9R,GAAW8R,EAAU,KAAQD,EAG/C,OAASE,EAAU,GAAMD,EAAU,GAOnC,GAHA9D,EAAajH,GAAQgL,EAGdA,EAAU,GAAMjD,EAAS3N,EAAM8D,EAAS8I,GAC9C,OAAO,EAMZ,OAAO,GAIV,SAASiE,GAAgBC,GACxB,OAAOA,EAAS5Q,OAAS,EACxB,SAAUF,EAAM8D,EAAS8I,GACxB,IAAIrP,EAAIuT,EAAS5Q,OACjB,MAAQ3C,IACP,IAAMuT,EAAUvT,GAAKyC,EAAM8D,EAAS8I,GACnC,OAAO,EAGT,OAAO,GAERkE,EAAU,GAGZ,SAASC,GAAkBlN,EAAUmN,EAAUjN,GAG9C,IAFA,IAAIxG,EAAI,EACP0C,EAAM+Q,EAAS9Q,OACR3C,EAAI0C,EAAK1C,IAChBqG,GAAQC,EAAUmN,EAAUzT,GAAKwG,GAElC,OAAOA,EAGR,SAASkN,GAAUrD,EAAWsD,EAAK3I,EAAQzE,EAAS8I,GAOnD,IANA,IAAI5M,EACHmR,KACA5T,EAAI,EACJ0C,EAAM2N,EAAU1N,OAChBkR,EAAgB,MAAPF,EAEF3T,EAAI0C,EAAK1C,KACTyC,EAAO4N,EAAWrQ,MAClBgL,IAAUA,EAAQvI,EAAM8D,EAAS8I,KACtCuE,EAAavR,KAAMI,GACdoR,GACJF,EAAItR,KAAMrC,KAMd,OAAO4T,EAGR,SAASE,GAAYzF,EAAW/H,EAAU8J,EAAS2D,EAAYC,EAAYC,GAO1E,OANKF,IAAeA,EAAY5S,KAC/B4S,EAAaD,GAAYC,IAErBC,IAAeA,EAAY7S,KAC/B6S,EAAaF,GAAYE,EAAYC,IAE/BxL,GAAc,SAAUhC,EAAMD,EAASD,EAAS8I,GACtD,IAAI6E,EAAMlU,EAAGyC,EACZ0R,KACAC,KACAC,EAAc7N,EAAQ7D,OAGtByI,EAAQ3E,GAAQ+M,GACflN,GAAY,IACZC,EAAQP,UAAaO,GAAYA,MAKlC+N,GAAYjG,IAAe5H,GAASH,EAEnC8E,EADAsI,GAAUtI,EAAO+I,EAAQ9F,EAAW9H,EAAS8I,GAG9CkF,EAAanE,EAGZ4D,IAAgBvN,EAAO4H,EAAYgG,GAAeN,MAMjDvN,EACD8N,EAQF,GALKlE,GACJA,EAASkE,EAAWC,EAAYhO,EAAS8I,GAIrC0E,EAAa,CACjBG,EAAOR,GAAUa,EAAYH,GAC7BL,EAAYG,KAAU3N,EAAS8I,GAG/BrP,EAAIkU,EAAKvR,OACT,MAAQ3C,KACAyC,EAAOyR,EAAMlU,MACnBuU,EAAYH,EAASpU,MAAWsU,EAAWF,EAASpU,IAAQyC,IAK/D,GAAKgE,GACJ,GAAKuN,GAAc3F,EAAY,CAC9B,GAAK2F,EAAa,CAGjBE,KACAlU,EAAIuU,EAAW5R,OACf,MAAQ3C,KACAyC,EAAO8R,EAAYvU,KAGzBkU,EAAK7R,KAAQiS,EAAWtU,GAAMyC,GAGhCuR,EAAY,KAAQO,KAAmBL,EAAM7E,GAI9CrP,EAAIuU,EAAW5R,OACf,MAAQ3C,KACAyC,EAAO8R,EAAYvU,MACvBkU,EAAOF,EAAazR,EAASkE,EAAMhE,GAAS0R,EAAQnU,KAAS,IAE/DyG,EAAMyN,KAAY1N,EAAS0N,GAASzR,UAOvC8R,EAAab,GACZa,IAAe/N,EACd+N,EAAW9G,OAAQ4G,EAAaE,EAAW5R,QAC3C4R,GAEGP,EACJA,EAAY,KAAMxN,EAAS+N,EAAYlF,GAEvChN,EAAKwD,MAAOW,EAAS+N,KAMzB,SAASC,GAAmB7B,GAyB3B,IAxBA,IAAI8B,EAAcrE,EAAShK,EAC1B1D,EAAMiQ,EAAOhQ,OACb+R,EAAkBxU,EAAK6N,SAAU4E,EAAQ,GAAI9D,MAC7C8F,EAAmBD,GAAmBxU,EAAK6N,SAAU,KACrD/N,EAAI0U,EAAkB,EAAI,EAG1BE,EAAerP,GAAe,SAAU9C,GACvC,OAAOA,IAASgS,GACdE,GAAkB,GACrBE,EAAkBtP,GAAe,SAAU9C,GAC1C,OAAOF,EAASkS,EAAchS,IAAU,GACtCkS,GAAkB,GACrBpB,GAAa,SAAU9Q,EAAM8D,EAAS8I,GACrC,IAAI3C,GAASgI,IAAqBrF,GAAO9I,IAAY/F,MAClDiU,EAAelO,GAAUP,SAC1B4O,EAAcnS,EAAM8D,EAAS8I,GAC7BwF,EAAiBpS,EAAM8D,EAAS8I,IAIlC,OADAoF,EAAe,KACR/H,IAGD1M,EAAI0C,EAAK1C,IAChB,GAAOoQ,EAAUlQ,EAAK6N,SAAU4E,EAAQ3S,GAAI6O,MAC3C0E,GAAahO,GAAe+N,GAAgBC,GAAYnD,QAClD,CAIN,IAHAA,EAAUlQ,EAAK8K,OAAQ2H,EAAQ3S,GAAI6O,MAAOhJ,MAAO,KAAM8M,EAAQ3S,GAAIiB,UAGrDE,GAAY,CAIzB,IADAiF,IAAMpG,EACEoG,EAAI1D,EAAK0D,IAChB,GAAKlG,EAAK6N,SAAU4E,EAAQvM,GAAIyI,MAC/B,MAGF,OAAOiF,GACN9T,EAAI,GAAKsT,GAAgBC,GACzBvT,EAAI,GAAK8H,GAGT6K,EACErQ,MAAO,EAAGtC,EAAI,GACd8U,QAAUxM,MAAgC,MAAzBqK,EAAQ3S,EAAI,GAAI6O,KAAe,IAAM,MACtDjH,QAASzE,EAAO,MAClBiN,EACApQ,EAAIoG,GAAKoO,GAAmB7B,EAAOrQ,MAAOtC,EAAGoG,IAC7CA,EAAI1D,GAAO8R,GAAqB7B,EAASA,EAAOrQ,MAAO8D,IACvDA,EAAI1D,GAAOoF,GAAY6K,IAGzBY,EAASlR,KAAM+N,GAIjB,OAAOkD,GAAgBC,GAGxB,SAASwB,GAA0BC,EAAiBC,GACnD,IAAIC,EAAQD,EAAYtS,OAAS,EAChCwS,EAAYH,EAAgBrS,OAAS,EACrCyS,EAAe,SAAU3O,EAAMF,EAAS8I,EAAK7I,EAAS6O,GACrD,IAAI5S,EAAM2D,EAAGgK,EACZkF,EAAe,EACftV,EAAI,IACJqQ,EAAY5J,MACZ8O,KACAC,EAAgBhV,EAGhB4K,EAAQ3E,GAAQ0O,GAAajV,EAAKgL,KAAY,IAAG,IAAKmK,GAGtDI,EAAkBnU,GAA4B,MAAjBkU,EAAwB,EAAIE,KAAKC,UAAY,GAC1EjT,EAAM0I,EAAMzI,OAcb,IAZK0S,IAMJ7U,EAAmB+F,GAAW3F,GAAY2F,GAAW8O,GAM9CrV,IAAM0C,GAAgC,OAAvBD,EAAO2I,EAAOpL,IAAeA,IAAM,CACzD,GAAKmV,GAAa1S,EAAO,CACxB2D,EAAI,EAMEG,GAAW9D,EAAKuE,eAAiBpG,IACtCD,EAAa8B,GACb4M,GAAOvO,GAER,MAAUsP,EAAU4E,EAAiB5O,KACpC,GAAKgK,EAAS3N,EAAM8D,GAAW3F,EAAUyO,GAAQ,CAChD7I,EAAQnE,KAAMI,GACd,MAGG4S,IACJ/T,EAAUmU,GAKPP,KAGGzS,GAAQ2N,GAAW3N,IACzB6S,IAII7O,GACJ4J,EAAUhO,KAAMI,IAgBnB,GATA6S,GAAgBtV,EASXkV,GAASlV,IAAMsV,EAAe,CAClClP,EAAI,EACJ,MAAUgK,EAAU6E,EAAa7O,KAChCgK,EAASC,EAAWkF,EAAYhP,EAAS8I,GAG1C,GAAK5I,EAAO,CAGX,GAAK6O,EAAe,EACnB,MAAQtV,IACCqQ,EAAWrQ,IAAOuV,EAAYvV,KACrCuV,EAAYvV,GAAMmC,EAAI2D,KAAMU,IAM/B+O,EAAa7B,GAAU6B,GAIxBlT,EAAKwD,MAAOW,EAAS+O,GAGhBF,IAAc5O,GAAQ8O,EAAW5S,OAAS,GAC5C2S,EAAeL,EAAYtS,OAAW,GAExC0D,GAAO+G,WAAY5G,GAUrB,OALK6O,IACJ/T,EAAUmU,EACVjV,EAAmBgV,GAGbnF,GAGT,OAAO6E,EACNzM,GAAc2M,GACdA,EAGF9U,EAAU+F,GAAO/F,QAAU,SAAUgG,EAAUM,GAC9C,IAAI5G,EACHiV,KACAD,KACAlC,EAASnR,EAAe2E,EAAW,KAEpC,IAAMwM,EAAS,CAGRlM,IACLA,EAAQvG,EAAUiG,IAEnBtG,EAAI4G,EAAMjE,OACV,MAAQ3C,KACP8S,EAAS0B,GAAmB5N,EAAO5G,KACtBmB,GACZ8T,EAAY5S,KAAMyQ,GAElBkC,EAAgB3S,KAAMyQ,IAKxBA,EAASnR,EACR2E,EACAyO,GAA0BC,EAAiBC,KAIrC3O,SAAWA,EAEnB,OAAOwM,GAYRvS,EAAS8F,GAAO9F,OAAS,SAAU+F,EAAUC,EAASC,EAASC,GAC9D,IAAIzG,EAAG2S,EAAQiD,EAAO/G,EAAM3D,EAC3B2K,EAA+B,mBAAbvP,GAA2BA,EAC7CM,GAASH,GAAQpG,EAAYiG,EAAWuP,EAASvP,UAAYA,GAM9D,GAJAE,EAAUA,MAIY,IAAjBI,EAAMjE,OAAe,CAIzB,IADAgQ,EAAS/L,EAAO,GAAMA,EAAO,GAAItE,MAAO,IAC5BK,OAAS,GAAsC,QAA/BiT,EAAQjD,EAAQ,IAAM9D,MAC5B,IAArBtI,EAAQP,UAAkBlF,GAAkBZ,EAAK6N,SAAU4E,EAAQ,GAAI9D,MAAS,CAIhF,KAFAtI,GAAYrG,EAAKgL,KAAW,GAAG0K,EAAM3U,QAAS,GAC5C2G,QAASpD,GAAWC,IAAa8B,QAAmB,IAErD,OAAOC,EAGIqP,IACXtP,EAAUA,EAAQkB,YAGnBnB,EAAWA,EAAShE,MAAOqQ,EAAOnK,QAAQF,MAAM3F,QAIjD3C,EAAIyD,EAA0B,aAAE8D,KAAMjB,GAAa,EAAIqM,EAAOhQ,OAC9D,MAAQ3C,IAAM,CAIb,GAHA4V,EAAQjD,EAAQ3S,GAGXE,EAAK6N,SAAYc,EAAO+G,EAAM/G,MAClC,MAED,IAAO3D,EAAOhL,EAAKgL,KAAM2D,MAGjBpI,EAAOyE,EACb0K,EAAM3U,QAAS,GAAI2G,QAASpD,GAAWC,IACvCF,GAASgD,KAAMoL,EAAQ,GAAI9D,OAAUrH,GAAajB,EAAQkB,aACzDlB,IACI,CAKL,GAFAoM,EAAOlF,OAAQzN,EAAG,KAClBsG,EAAWG,EAAK9D,QAAUmF,GAAY6K,IAGrC,OADAtQ,EAAKwD,MAAOW,EAASC,GACdD,EAGR,QAeJ,OAPEqP,GAAYvV,EAASgG,EAAUM,IAChCH,EACAF,GACCzF,EACD0F,GACCD,GAAWhC,GAASgD,KAAMjB,IAAckB,GAAajB,EAAQkB,aAAgBlB,GAExEC,GAMRvG,EAAQsN,WAAapM,EAAQ+H,MAAO,IAAKsE,KAAM3L,GAAYkG,KAAM,MAAS5G,EAI1ElB,EAAQqN,mBAAqB5M,EAG7BC,IAIAV,EAAQkM,aAAexD,GAAQ,SAAUC,GAGxC,OAA4E,EAArEA,EAAGmD,wBAAyBnL,EAASiI,cAAe,eAMtDF,GAAQ,SAAUC,GAEvB,OADAA,EAAG4C,UAAY,mBACiC,MAAzC5C,EAAG+E,WAAWhG,aAAc,WAEnCoB,GAAW,yBAA0B,SAAUtG,EAAMmK,EAAMxM,GAC1D,IAAMA,EACL,OAAOqC,EAAKkF,aAAciF,EAA6B,SAAvBA,EAAKlH,cAA2B,EAAI,KAOjEzF,EAAQ8C,YAAe4F,GAAQ,SAAUC,GAG9C,OAFAA,EAAG4C,UAAY,WACf5C,EAAG+E,WAAW9F,aAAc,QAAS,IACY,KAA1Ce,EAAG+E,WAAWhG,aAAc,YAEnCoB,GAAW,QAAS,SAAUtG,EAAMqT,EAAO1V,GAC1C,IAAMA,GAAyC,UAAhCqC,EAAKgD,SAASC,cAC5B,OAAOjD,EAAKsT,eAOTpN,GAAQ,SAAUC,GACvB,OAAwC,MAAjCA,EAAGjB,aAAc,eAExBoB,GAAWnG,EAAU,SAAUH,EAAMmK,EAAMxM,GAC1C,IAAIyM,EACJ,IAAMzM,EACL,OAAwB,IAAjBqC,EAAMmK,GAAkBA,EAAKlH,eACjCmH,EAAMpK,EAAK0I,iBAAkByB,KAAYC,EAAIE,UAC9CF,EAAIvE,MACJ,OAML,IAAI0N,GAAUjW,EAAOsG,OAErBA,GAAO4P,WAAa,WAKnB,OAJKlW,EAAOsG,SAAWA,KACtBtG,EAAOsG,OAAS2P,IAGV3P,IAGe,mBAAX6P,QAAyBA,OAAOC,IAC3CD,OAAQ,WACP,OAAO7P,KAIqB,oBAAX+P,QAA0BA,OAAOC,QACnDD,OAAOC,QAAUhQ,GAEjBtG,EAAOsG,OAASA,GAl8EjB,CAu8EKtG","file":"sizzle.min.js"} \ No newline at end of file
diff --git a/node_modules/sizzle/package.json b/node_modules/sizzle/package.json
new file mode 100644
index 0000000..6e252ec
--- /dev/null
+++ b/node_modules/sizzle/package.json
@@ -0,0 +1,85 @@
+{
+ "title": "Sizzle",
+ "name": "sizzle",
+ "version": "2.3.10",
+ "description": "A pure-JavaScript, bottom-up CSS selector engine designed to be easily dropped in to a host library.",
+ "keywords": [
+ "sizzle",
+ "javascript",
+ "CSS",
+ "selector",
+ "jquery"
+ ],
+ "homepage": "https://sizzlejs.com",
+ "author": {
+ "name": "JS Foundation and other contributors",
+ "url": "https://github.com/jquery/sizzle/blob/2.3.10/AUTHORS.txt"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/jquery/sizzle.git"
+ },
+ "bugs": {
+ "url": "https://github.com/jquery/sizzle/issues"
+ },
+ "license": "MIT",
+ "files": [
+ "AUTHORS.txt",
+ "LICENSE.txt",
+ "dist/sizzle.js",
+ "dist/sizzle.min.js",
+ "dist/sizzle.min.map"
+ ],
+ "main": "dist/sizzle.js",
+ "dependencies": {},
+ "devDependencies": {
+ "benchmark": "2.1.4",
+ "commitplease": "2.7.10",
+ "eslint-config-jquery": "2.0.0",
+ "grunt": "1.5.3",
+ "grunt-cli": "1.4.3",
+ "grunt-compare-size": "0.4.2",
+ "grunt-contrib-qunit": "2.0.0",
+ "grunt-contrib-uglify": "3.0.1",
+ "grunt-contrib-watch": "1.0.0",
+ "grunt-eslint": "21.0.0",
+ "grunt-git-authors": "3.2.0",
+ "grunt-jsonlint": "1.1.0",
+ "grunt-karma": "2.0.0",
+ "grunt-npmcopy": "0.1.0",
+ "gzip-js": "0.3.2",
+ "jquery": "1.9.1",
+ "karma": "1.3.0",
+ "karma-browserstack-launcher": "1.3.0",
+ "karma-chrome-launcher": "2.2.0",
+ "karma-firefox-launcher": "1.2.0",
+ "karma-html2js-preprocessor": "1.1.0",
+ "karma-phantomjs-launcher": "1.0.4",
+ "karma-qunit": "1.2.1",
+ "load-grunt-tasks": "3.5.2",
+ "phantomjs-prebuilt": "2.1.15",
+ "qunitjs": "1.23.1",
+ "requirejs": "2.3.5",
+ "requirejs-domready": "2.0.3",
+ "requirejs-text": "2.0.15"
+ },
+ "scripts": {
+ "build": "npm install && grunt",
+ "start": "grunt start",
+ "test": "grunt && grunt test"
+ },
+ "commitplease": {
+ "components": [
+ "Misc",
+ "Docs",
+ "Tests",
+ "Build",
+ "Release",
+ "Core",
+ "Tokenize",
+ "Compile",
+ "Selector",
+ "SetDocument"
+ ]
+ }
+}
diff --git a/node_modules/ws/LICENSE b/node_modules/ws/LICENSE
new file mode 100644
index 0000000..a145cd1
--- /dev/null
+++ b/node_modules/ws/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/node_modules/ws/README.md b/node_modules/ws/README.md
new file mode 100644
index 0000000..20a6114
--- /dev/null
+++ b/node_modules/ws/README.md
@@ -0,0 +1,495 @@
+# ws: a Node.js WebSocket library
+
+[![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws)
+[![CI](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster)
+[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws)
+
+ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and
+server implementation.
+
+Passes the quite extensive Autobahn test suite: [server][server-report],
+[client][client-report].
+
+**Note**: This module does not work in the browser. The client in the docs is a
+reference to a back end with the role of a client in the WebSocket
+communication. Browser clients must use the native
+[`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
+object. To make the same code work seamlessly on Node.js and the browser, you
+can use one of the many wrappers available on npm, like
+[isomorphic-ws](https://github.com/heineiuo/isomorphic-ws).
+
+## Table of Contents
+
+- [Protocol support](#protocol-support)
+- [Installing](#installing)
+ - [Opt-in for performance](#opt-in-for-performance)
+- [API docs](#api-docs)
+- [WebSocket compression](#websocket-compression)
+- [Usage examples](#usage-examples)
+ - [Sending and receiving text data](#sending-and-receiving-text-data)
+ - [Sending binary data](#sending-binary-data)
+ - [Simple server](#simple-server)
+ - [External HTTP/S server](#external-https-server)
+ - [Multiple servers sharing a single HTTP/S server](#multiple-servers-sharing-a-single-https-server)
+ - [Client authentication](#client-authentication)
+ - [Server broadcast](#server-broadcast)
+ - [echo.websocket.org demo](#echowebsocketorg-demo)
+ - [Use the Node.js streams API](#use-the-nodejs-streams-api)
+ - [Other examples](#other-examples)
+- [FAQ](#faq)
+ - [How to get the IP address of the client?](#how-to-get-the-ip-address-of-the-client)
+ - [How to detect and close broken connections?](#how-to-detect-and-close-broken-connections)
+ - [How to connect via a proxy?](#how-to-connect-via-a-proxy)
+- [Changelog](#changelog)
+- [License](#license)
+
+## Protocol support
+
+- **HyBi drafts 07-12** (Use the option `protocolVersion: 8`)
+- **HyBi drafts 13-17** (Current default, alternatively option
+ `protocolVersion: 13`)
+
+## Installing
+
+```
+npm install ws
+```
+
+### Opt-in for performance
+
+There are 2 optional modules that can be installed along side with the ws
+module. These modules are binary addons which improve certain operations.
+Prebuilt binaries are available for the most popular platforms so you don't
+necessarily need to have a C++ compiler installed on your machine.
+
+- `npm install --save-optional bufferutil`: Allows to efficiently perform
+ operations such as masking and unmasking the data payload of the WebSocket
+ frames.
+- `npm install --save-optional utf-8-validate`: Allows to efficiently check if a
+ message contains valid UTF-8.
+
+## API docs
+
+See [`/doc/ws.md`](./doc/ws.md) for Node.js-like documentation of ws classes and
+utility functions.
+
+## WebSocket compression
+
+ws supports the [permessage-deflate extension][permessage-deflate] which enables
+the client and server to negotiate a compression algorithm and its parameters,
+and then selectively apply it to the data payloads of each WebSocket message.
+
+The extension is disabled by default on the server and enabled by default on the
+client. It adds a significant overhead in terms of performance and memory
+consumption so we suggest to enable it only if it is really needed.
+
+Note that Node.js has a variety of issues with high-performance compression,
+where increased concurrency, especially on Linux, can lead to [catastrophic
+memory fragmentation][node-zlib-bug] and slow performance. If you intend to use
+permessage-deflate in production, it is worthwhile to set up a test
+representative of your workload and ensure Node.js/zlib will handle it with
+acceptable performance and memory usage.
+
+Tuning of permessage-deflate can be done via the options defined below. You can
+also use `zlibDeflateOptions` and `zlibInflateOptions`, which is passed directly
+into the creation of [raw deflate/inflate streams][node-zlib-deflaterawdocs].
+
+See [the docs][ws-server-options] for more options.
+
+```js
+const WebSocket = require('ws');
+
+const wss = new WebSocket.Server({
+ port: 8080,
+ perMessageDeflate: {
+ zlibDeflateOptions: {
+ // See zlib defaults.
+ chunkSize: 1024,
+ memLevel: 7,
+ level: 3
+ },
+ zlibInflateOptions: {
+ chunkSize: 10 * 1024
+ },
+ // Other options settable:
+ clientNoContextTakeover: true, // Defaults to negotiated value.
+ serverNoContextTakeover: true, // Defaults to negotiated value.
+ serverMaxWindowBits: 10, // Defaults to negotiated value.
+ // Below options specified as default values.
+ concurrencyLimit: 10, // Limits zlib concurrency for perf.
+ threshold: 1024 // Size (in bytes) below which messages
+ // should not be compressed.
+ }
+});
+```
+
+The client will only use the extension if it is supported and enabled on the
+server. To always disable the extension on the client set the
+`perMessageDeflate` option to `false`.
+
+```js
+const WebSocket = require('ws');
+
+const ws = new WebSocket('ws://www.host.com/path', {
+ perMessageDeflate: false
+});
+```
+
+## Usage examples
+
+### Sending and receiving text data
+
+```js
+const WebSocket = require('ws');
+
+const ws = new WebSocket('ws://www.host.com/path');
+
+ws.on('open', function open() {
+ ws.send('something');
+});
+
+ws.on('message', function incoming(data) {
+ console.log(data);
+});
+```
+
+### Sending binary data
+
+```js
+const WebSocket = require('ws');
+
+const ws = new WebSocket('ws://www.host.com/path');
+
+ws.on('open', function open() {
+ const array = new Float32Array(5);
+
+ for (var i = 0; i < array.length; ++i) {
+ array[i] = i / 2;
+ }
+
+ ws.send(array);
+});
+```
+
+### Simple server
+
+```js
+const WebSocket = require('ws');
+
+const wss = new WebSocket.Server({ port: 8080 });
+
+wss.on('connection', function connection(ws) {
+ ws.on('message', function incoming(message) {
+ console.log('received: %s', message);
+ });
+
+ ws.send('something');
+});
+```
+
+### External HTTP/S server
+
+```js
+const fs = require('fs');
+const https = require('https');
+const WebSocket = require('ws');
+
+const server = https.createServer({
+ cert: fs.readFileSync('/path/to/cert.pem'),
+ key: fs.readFileSync('/path/to/key.pem')
+});
+const wss = new WebSocket.Server({ server });
+
+wss.on('connection', function connection(ws) {
+ ws.on('message', function incoming(message) {
+ console.log('received: %s', message);
+ });
+
+ ws.send('something');
+});
+
+server.listen(8080);
+```
+
+### Multiple servers sharing a single HTTP/S server
+
+```js
+const http = require('http');
+const WebSocket = require('ws');
+const url = require('url');
+
+const server = http.createServer();
+const wss1 = new WebSocket.Server({ noServer: true });
+const wss2 = new WebSocket.Server({ noServer: true });
+
+wss1.on('connection', function connection(ws) {
+ // ...
+});
+
+wss2.on('connection', function connection(ws) {
+ // ...
+});
+
+server.on('upgrade', function upgrade(request, socket, head) {
+ const pathname = url.parse(request.url).pathname;
+
+ if (pathname === '/foo') {
+ wss1.handleUpgrade(request, socket, head, function done(ws) {
+ wss1.emit('connection', ws, request);
+ });
+ } else if (pathname === '/bar') {
+ wss2.handleUpgrade(request, socket, head, function done(ws) {
+ wss2.emit('connection', ws, request);
+ });
+ } else {
+ socket.destroy();
+ }
+});
+
+server.listen(8080);
+```
+
+### Client authentication
+
+```js
+const http = require('http');
+const WebSocket = require('ws');
+
+const server = http.createServer();
+const wss = new WebSocket.Server({ noServer: true });
+
+wss.on('connection', function connection(ws, request, client) {
+ ws.on('message', function message(msg) {
+ console.log(`Received message ${msg} from user ${client}`);
+ });
+});
+
+server.on('upgrade', function upgrade(request, socket, head) {
+ // This function is not defined on purpose. Implement it with your own logic.
+ authenticate(request, (err, client) => {
+ if (err || !client) {
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
+ socket.destroy();
+ return;
+ }
+
+ wss.handleUpgrade(request, socket, head, function done(ws) {
+ wss.emit('connection', ws, request, client);
+ });
+ });
+});
+
+server.listen(8080);
+```
+
+Also see the provided [example][session-parse-example] using `express-session`.
+
+### Server broadcast
+
+A client WebSocket broadcasting to all connected WebSocket clients, including
+itself.
+
+```js
+const WebSocket = require('ws');
+
+const wss = new WebSocket.Server({ port: 8080 });
+
+wss.on('connection', function connection(ws) {
+ ws.on('message', function incoming(data) {
+ wss.clients.forEach(function each(client) {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(data);
+ }
+ });
+ });
+});
+```
+
+A client WebSocket broadcasting to every other connected WebSocket clients,
+excluding itself.
+
+```js
+const WebSocket = require('ws');
+
+const wss = new WebSocket.Server({ port: 8080 });
+
+wss.on('connection', function connection(ws) {
+ ws.on('message', function incoming(data) {
+ wss.clients.forEach(function each(client) {
+ if (client !== ws && client.readyState === WebSocket.OPEN) {
+ client.send(data);
+ }
+ });
+ });
+});
+```
+
+### echo.websocket.org demo
+
+```js
+const WebSocket = require('ws');
+
+const ws = new WebSocket('wss://echo.websocket.org/', {
+ origin: 'https://websocket.org'
+});
+
+ws.on('open', function open() {
+ console.log('connected');
+ ws.send(Date.now());
+});
+
+ws.on('close', function close() {
+ console.log('disconnected');
+});
+
+ws.on('message', function incoming(data) {
+ console.log(`Roundtrip time: ${Date.now() - data} ms`);
+
+ setTimeout(function timeout() {
+ ws.send(Date.now());
+ }, 500);
+});
+```
+
+### Use the Node.js streams API
+
+```js
+const WebSocket = require('ws');
+
+const ws = new WebSocket('wss://echo.websocket.org/', {
+ origin: 'https://websocket.org'
+});
+
+const duplex = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' });
+
+duplex.pipe(process.stdout);
+process.stdin.pipe(duplex);
+```
+
+### Other examples
+
+For a full example with a browser client communicating with a ws server, see the
+examples folder.
+
+Otherwise, see the test cases.
+
+## FAQ
+
+### How to get the IP address of the client?
+
+The remote IP address can be obtained from the raw socket.
+
+```js
+const WebSocket = require('ws');
+
+const wss = new WebSocket.Server({ port: 8080 });
+
+wss.on('connection', function connection(ws, req) {
+ const ip = req.socket.remoteAddress;
+});
+```
+
+When the server runs behind a proxy like NGINX, the de-facto standard is to use
+the `X-Forwarded-For` header.
+
+```js
+wss.on('connection', function connection(ws, req) {
+ const ip = req.headers['x-forwarded-for'].split(',')[0].trim();
+});
+```
+
+### How to detect and close broken connections?
+
+Sometimes the link between the server and the client can be interrupted in a way
+that keeps both the server and the client unaware of the broken state of the
+connection (e.g. when pulling the cord).
+
+In these cases ping messages can be used as a means to verify that the remote
+endpoint is still responsive.
+
+```js
+const WebSocket = require('ws');
+
+function noop() {}
+
+function heartbeat() {
+ this.isAlive = true;
+}
+
+const wss = new WebSocket.Server({ port: 8080 });
+
+wss.on('connection', function connection(ws) {
+ ws.isAlive = true;
+ ws.on('pong', heartbeat);
+});
+
+const interval = setInterval(function ping() {
+ wss.clients.forEach(function each(ws) {
+ if (ws.isAlive === false) return ws.terminate();
+
+ ws.isAlive = false;
+ ws.ping(noop);
+ });
+}, 30000);
+
+wss.on('close', function close() {
+ clearInterval(interval);
+});
+```
+
+Pong messages are automatically sent in response to ping messages as required by
+the spec.
+
+Just like the server example above your clients might as well lose connection
+without knowing it. You might want to add a ping listener on your clients to
+prevent that. A simple implementation would be:
+
+```js
+const WebSocket = require('ws');
+
+function heartbeat() {
+ clearTimeout(this.pingTimeout);
+
+ // Use `WebSocket#terminate()`, which immediately destroys the connection,
+ // instead of `WebSocket#close()`, which waits for the close timer.
+ // Delay should be equal to the interval at which your server
+ // sends out pings plus a conservative assumption of the latency.
+ this.pingTimeout = setTimeout(() => {
+ this.terminate();
+ }, 30000 + 1000);
+}
+
+const client = new WebSocket('wss://echo.websocket.org/');
+
+client.on('open', heartbeat);
+client.on('ping', heartbeat);
+client.on('close', function clear() {
+ clearTimeout(this.pingTimeout);
+});
+```
+
+### How to connect via a proxy?
+
+Use a custom `http.Agent` implementation like [https-proxy-agent][] or
+[socks-proxy-agent][].
+
+## Changelog
+
+We're using the GitHub [releases][changelog] for changelog entries.
+
+## License
+
+[MIT](LICENSE)
+
+[changelog]: https://github.com/websockets/ws/releases
+[client-report]: http://websockets.github.io/ws/autobahn/clients/
+[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent
+[node-zlib-bug]: https://github.com/nodejs/node/issues/8871
+[node-zlib-deflaterawdocs]:
+ https://nodejs.org/api/zlib.html#zlib_zlib_createdeflateraw_options
+[permessage-deflate]: https://tools.ietf.org/html/rfc7692
+[server-report]: http://websockets.github.io/ws/autobahn/servers/
+[session-parse-example]: ./examples/express-session-parse
+[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent
+[ws-server-options]:
+ https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback
diff --git a/node_modules/ws/browser.js b/node_modules/ws/browser.js
new file mode 100644
index 0000000..ca4f628
--- /dev/null
+++ b/node_modules/ws/browser.js
@@ -0,0 +1,8 @@
+'use strict';
+
+module.exports = function () {
+ throw new Error(
+ 'ws does not work in the browser. Browser clients must use the native ' +
+ 'WebSocket object'
+ );
+};
diff --git a/node_modules/ws/index.js b/node_modules/ws/index.js
new file mode 100644
index 0000000..722c786
--- /dev/null
+++ b/node_modules/ws/index.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const WebSocket = require('./lib/websocket');
+
+WebSocket.createWebSocketStream = require('./lib/stream');
+WebSocket.Server = require('./lib/websocket-server');
+WebSocket.Receiver = require('./lib/receiver');
+WebSocket.Sender = require('./lib/sender');
+
+module.exports = WebSocket;
diff --git a/node_modules/ws/lib/buffer-util.js b/node_modules/ws/lib/buffer-util.js
new file mode 100644
index 0000000..6fd84c3
--- /dev/null
+++ b/node_modules/ws/lib/buffer-util.js
@@ -0,0 +1,129 @@
+'use strict';
+
+const { EMPTY_BUFFER } = require('./constants');
+
+/**
+ * Merges an array of buffers into a new buffer.
+ *
+ * @param {Buffer[]} list The array of buffers to concat
+ * @param {Number} totalLength The total length of buffers in the list
+ * @return {Buffer} The resulting buffer
+ * @public
+ */
+function concat(list, totalLength) {
+ if (list.length === 0) return EMPTY_BUFFER;
+ if (list.length === 1) return list[0];
+
+ const target = Buffer.allocUnsafe(totalLength);
+ let offset = 0;
+
+ for (let i = 0; i < list.length; i++) {
+ const buf = list[i];
+ target.set(buf, offset);
+ offset += buf.length;
+ }
+
+ if (offset < totalLength) return target.slice(0, offset);
+
+ return target;
+}
+
+/**
+ * Masks a buffer using the given mask.
+ *
+ * @param {Buffer} source The buffer to mask
+ * @param {Buffer} mask The mask to use
+ * @param {Buffer} output The buffer where to store the result
+ * @param {Number} offset The offset at which to start writing
+ * @param {Number} length The number of bytes to mask.
+ * @public
+ */
+function _mask(source, mask, output, offset, length) {
+ for (let i = 0; i < length; i++) {
+ output[offset + i] = source[i] ^ mask[i & 3];
+ }
+}
+
+/**
+ * Unmasks a buffer using the given mask.
+ *
+ * @param {Buffer} buffer The buffer to unmask
+ * @param {Buffer} mask The mask to use
+ * @public
+ */
+function _unmask(buffer, mask) {
+ // Required until https://github.com/nodejs/node/issues/9006 is resolved.
+ const length = buffer.length;
+ for (let i = 0; i < length; i++) {
+ buffer[i] ^= mask[i & 3];
+ }
+}
+
+/**
+ * Converts a buffer to an `ArrayBuffer`.
+ *
+ * @param {Buffer} buf The buffer to convert
+ * @return {ArrayBuffer} Converted buffer
+ * @public
+ */
+function toArrayBuffer(buf) {
+ if (buf.byteLength === buf.buffer.byteLength) {
+ return buf.buffer;
+ }
+
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
+}
+
+/**
+ * Converts `data` to a `Buffer`.
+ *
+ * @param {*} data The data to convert
+ * @return {Buffer} The buffer
+ * @throws {TypeError}
+ * @public
+ */
+function toBuffer(data) {
+ toBuffer.readOnly = true;
+
+ if (Buffer.isBuffer(data)) return data;
+
+ let buf;
+
+ if (data instanceof ArrayBuffer) {
+ buf = Buffer.from(data);
+ } else if (ArrayBuffer.isView(data)) {
+ buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
+ } else {
+ buf = Buffer.from(data);
+ toBuffer.readOnly = false;
+ }
+
+ return buf;
+}
+
+try {
+ const bufferUtil = require('bufferutil');
+ const bu = bufferUtil.BufferUtil || bufferUtil;
+
+ module.exports = {
+ concat,
+ mask(source, mask, output, offset, length) {
+ if (length < 48) _mask(source, mask, output, offset, length);
+ else bu.mask(source, mask, output, offset, length);
+ },
+ toArrayBuffer,
+ toBuffer,
+ unmask(buffer, mask) {
+ if (buffer.length < 32) _unmask(buffer, mask);
+ else bu.unmask(buffer, mask);
+ }
+ };
+} catch (e) /* istanbul ignore next */ {
+ module.exports = {
+ concat,
+ mask: _mask,
+ toArrayBuffer,
+ toBuffer,
+ unmask: _unmask
+ };
+}
diff --git a/node_modules/ws/lib/constants.js b/node_modules/ws/lib/constants.js
new file mode 100644
index 0000000..4082981
--- /dev/null
+++ b/node_modules/ws/lib/constants.js
@@ -0,0 +1,10 @@
+'use strict';
+
+module.exports = {
+ BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'],
+ GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
+ kStatusCode: Symbol('status-code'),
+ kWebSocket: Symbol('websocket'),
+ EMPTY_BUFFER: Buffer.alloc(0),
+ NOOP: () => {}
+};
diff --git a/node_modules/ws/lib/event-target.js b/node_modules/ws/lib/event-target.js
new file mode 100644
index 0000000..a6fbe72
--- /dev/null
+++ b/node_modules/ws/lib/event-target.js
@@ -0,0 +1,184 @@
+'use strict';
+
+/**
+ * Class representing an event.
+ *
+ * @private
+ */
+class Event {
+ /**
+ * Create a new `Event`.
+ *
+ * @param {String} type The name of the event
+ * @param {Object} target A reference to the target to which the event was
+ * dispatched
+ */
+ constructor(type, target) {
+ this.target = target;
+ this.type = type;
+ }
+}
+
+/**
+ * Class representing a message event.
+ *
+ * @extends Event
+ * @private
+ */
+class MessageEvent extends Event {
+ /**
+ * Create a new `MessageEvent`.
+ *
+ * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data
+ * @param {WebSocket} target A reference to the target to which the event was
+ * dispatched
+ */
+ constructor(data, target) {
+ super('message', target);
+
+ this.data = data;
+ }
+}
+
+/**
+ * Class representing a close event.
+ *
+ * @extends Event
+ * @private
+ */
+class CloseEvent extends Event {
+ /**
+ * Create a new `CloseEvent`.
+ *
+ * @param {Number} code The status code explaining why the connection is being
+ * closed
+ * @param {String} reason A human-readable string explaining why the
+ * connection is closing
+ * @param {WebSocket} target A reference to the target to which the event was
+ * dispatched
+ */
+ constructor(code, reason, target) {
+ super('close', target);
+
+ this.wasClean = target._closeFrameReceived && target._closeFrameSent;
+ this.reason = reason;
+ this.code = code;
+ }
+}
+
+/**
+ * Class representing an open event.
+ *
+ * @extends Event
+ * @private
+ */
+class OpenEvent extends Event {
+ /**
+ * Create a new `OpenEvent`.
+ *
+ * @param {WebSocket} target A reference to the target to which the event was
+ * dispatched
+ */
+ constructor(target) {
+ super('open', target);
+ }
+}
+
+/**
+ * Class representing an error event.
+ *
+ * @extends Event
+ * @private
+ */
+class ErrorEvent extends Event {
+ /**
+ * Create a new `ErrorEvent`.
+ *
+ * @param {Object} error The error that generated this event
+ * @param {WebSocket} target A reference to the target to which the event was
+ * dispatched
+ */
+ constructor(error, target) {
+ super('error', target);
+
+ this.message = error.message;
+ this.error = error;
+ }
+}
+
+/**
+ * This provides methods for emulating the `EventTarget` interface. It's not
+ * meant to be used directly.
+ *
+ * @mixin
+ */
+const EventTarget = {
+ /**
+ * Register an event listener.
+ *
+ * @param {String} type A string representing the event type to listen for
+ * @param {Function} listener The listener to add
+ * @param {Object} [options] An options object specifies characteristics about
+ * the event listener
+ * @param {Boolean} [options.once=false] A `Boolean`` indicating that the
+ * listener should be invoked at most once after being added. If `true`,
+ * the listener would be automatically removed when invoked.
+ * @public
+ */
+ addEventListener(type, listener, options) {
+ if (typeof listener !== 'function') return;
+
+ function onMessage(data) {
+ listener.call(this, new MessageEvent(data, this));
+ }
+
+ function onClose(code, message) {
+ listener.call(this, new CloseEvent(code, message, this));
+ }
+
+ function onError(error) {
+ listener.call(this, new ErrorEvent(error, this));
+ }
+
+ function onOpen() {
+ listener.call(this, new OpenEvent(this));
+ }
+
+ const method = options && options.once ? 'once' : 'on';
+
+ if (type === 'message') {
+ onMessage._listener = listener;
+ this[method](type, onMessage);
+ } else if (type === 'close') {
+ onClose._listener = listener;
+ this[method](type, onClose);
+ } else if (type === 'error') {
+ onError._listener = listener;
+ this[method](type, onError);
+ } else if (type === 'open') {
+ onOpen._listener = listener;
+ this[method](type, onOpen);
+ } else {
+ this[method](type, listener);
+ }
+ },
+
+ /**
+ * Remove an event listener.
+ *
+ * @param {String} type A string representing the event type to remove
+ * @param {Function} listener The listener to remove
+ * @public
+ */
+ removeEventListener(type, listener) {
+ const listeners = this.listeners(type);
+
+ for (let i = 0; i < listeners.length; i++) {
+ if (listeners[i] === listener || listeners[i]._listener === listener) {
+ this.removeListener(type, listeners[i]);
+ }
+ }
+ }
+};
+
+module.exports = EventTarget;
diff --git a/node_modules/ws/lib/extension.js b/node_modules/ws/lib/extension.js
new file mode 100644
index 0000000..87a4213
--- /dev/null
+++ b/node_modules/ws/lib/extension.js
@@ -0,0 +1,223 @@
+'use strict';
+
+//
+// Allowed token characters:
+//
+// '!', '#', '$', '%', '&', ''', '*', '+', '-',
+// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~'
+//
+// tokenChars[32] === 0 // ' '
+// tokenChars[33] === 1 // '!'
+// tokenChars[34] === 0 // '"'
+// ...
+//
+// prettier-ignore
+const tokenChars = [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
+ 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127
+];
+
+/**
+ * Adds an offer to the map of extension offers or a parameter to the map of
+ * parameters.
+ *
+ * @param {Object} dest The map of extension offers or parameters
+ * @param {String} name The extension or parameter name
+ * @param {(Object|Boolean|String)} elem The extension parameters or the
+ * parameter value
+ * @private
+ */
+function push(dest, name, elem) {
+ if (dest[name] === undefined) dest[name] = [elem];
+ else dest[name].push(elem);
+}
+
+/**
+ * Parses the `Sec-WebSocket-Extensions` header into an object.
+ *
+ * @param {String} header The field value of the header
+ * @return {Object} The parsed object
+ * @public
+ */
+function parse(header) {
+ const offers = Object.create(null);
+
+ if (header === undefined || header === '') return offers;
+
+ let params = Object.create(null);
+ let mustUnescape = false;
+ let isEscaping = false;
+ let inQuotes = false;
+ let extensionName;
+ let paramName;
+ let start = -1;
+ let end = -1;
+ let i = 0;
+
+ for (; i < header.length; i++) {
+ const code = header.charCodeAt(i);
+
+ if (extensionName === undefined) {
+ if (end === -1 && tokenChars[code] === 1) {
+ if (start === -1) start = i;
+ } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) {
+ if (end === -1 && start !== -1) end = i;
+ } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) {
+ if (start === -1) {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+
+ if (end === -1) end = i;
+ const name = header.slice(start, end);
+ if (code === 0x2c) {
+ push(offers, name, params);
+ params = Object.create(null);
+ } else {
+ extensionName = name;
+ }
+
+ start = end = -1;
+ } else {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ } else if (paramName === undefined) {
+ if (end === -1 && tokenChars[code] === 1) {
+ if (start === -1) start = i;
+ } else if (code === 0x20 || code === 0x09) {
+ if (end === -1 && start !== -1) end = i;
+ } else if (code === 0x3b || code === 0x2c) {
+ if (start === -1) {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+
+ if (end === -1) end = i;
+ push(params, header.slice(start, end), true);
+ if (code === 0x2c) {
+ push(offers, extensionName, params);
+ params = Object.create(null);
+ extensionName = undefined;
+ }
+
+ start = end = -1;
+ } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) {
+ paramName = header.slice(start, i);
+ start = end = -1;
+ } else {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ } else {
+ //
+ // The value of a quoted-string after unescaping must conform to the
+ // token ABNF, so only token characters are valid.
+ // Ref: https://tools.ietf.org/html/rfc6455#section-9.1
+ //
+ if (isEscaping) {
+ if (tokenChars[code] !== 1) {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ if (start === -1) start = i;
+ else if (!mustUnescape) mustUnescape = true;
+ isEscaping = false;
+ } else if (inQuotes) {
+ if (tokenChars[code] === 1) {
+ if (start === -1) start = i;
+ } else if (code === 0x22 /* '"' */ && start !== -1) {
+ inQuotes = false;
+ end = i;
+ } else if (code === 0x5c /* '\' */) {
+ isEscaping = true;
+ } else {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) {
+ inQuotes = true;
+ } else if (end === -1 && tokenChars[code] === 1) {
+ if (start === -1) start = i;
+ } else if (start !== -1 && (code === 0x20 || code === 0x09)) {
+ if (end === -1) end = i;
+ } else if (code === 0x3b || code === 0x2c) {
+ if (start === -1) {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+
+ if (end === -1) end = i;
+ let value = header.slice(start, end);
+ if (mustUnescape) {
+ value = value.replace(/\\/g, '');
+ mustUnescape = false;
+ }
+ push(params, paramName, value);
+ if (code === 0x2c) {
+ push(offers, extensionName, params);
+ params = Object.create(null);
+ extensionName = undefined;
+ }
+
+ paramName = undefined;
+ start = end = -1;
+ } else {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ }
+ }
+
+ if (start === -1 || inQuotes) {
+ throw new SyntaxError('Unexpected end of input');
+ }
+
+ if (end === -1) end = i;
+ const token = header.slice(start, end);
+ if (extensionName === undefined) {
+ push(offers, token, params);
+ } else {
+ if (paramName === undefined) {
+ push(params, token, true);
+ } else if (mustUnescape) {
+ push(params, paramName, token.replace(/\\/g, ''));
+ } else {
+ push(params, paramName, token);
+ }
+ push(offers, extensionName, params);
+ }
+
+ return offers;
+}
+
+/**
+ * Builds the `Sec-WebSocket-Extensions` header field value.
+ *
+ * @param {Object} extensions The map of extensions and parameters to format
+ * @return {String} A string representing the given object
+ * @public
+ */
+function format(extensions) {
+ return Object.keys(extensions)
+ .map((extension) => {
+ let configurations = extensions[extension];
+ if (!Array.isArray(configurations)) configurations = [configurations];
+ return configurations
+ .map((params) => {
+ return [extension]
+ .concat(
+ Object.keys(params).map((k) => {
+ let values = params[k];
+ if (!Array.isArray(values)) values = [values];
+ return values
+ .map((v) => (v === true ? k : `${k}=${v}`))
+ .join('; ');
+ })
+ )
+ .join('; ');
+ })
+ .join(', ');
+ })
+ .join(', ');
+}
+
+module.exports = { format, parse };
diff --git a/node_modules/ws/lib/limiter.js b/node_modules/ws/lib/limiter.js
new file mode 100644
index 0000000..3fd3578
--- /dev/null
+++ b/node_modules/ws/lib/limiter.js
@@ -0,0 +1,55 @@
+'use strict';
+
+const kDone = Symbol('kDone');
+const kRun = Symbol('kRun');
+
+/**
+ * A very simple job queue with adjustable concurrency. Adapted from
+ * https://github.com/STRML/async-limiter
+ */
+class Limiter {
+ /**
+ * Creates a new `Limiter`.
+ *
+ * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed
+ * to run concurrently
+ */
+ constructor(concurrency) {
+ this[kDone] = () => {
+ this.pending--;
+ this[kRun]();
+ };
+ this.concurrency = concurrency || Infinity;
+ this.jobs = [];
+ this.pending = 0;
+ }
+
+ /**
+ * Adds a job to the queue.
+ *
+ * @param {Function} job The job to run
+ * @public
+ */
+ add(job) {
+ this.jobs.push(job);
+ this[kRun]();
+ }
+
+ /**
+ * Removes a job from the queue and runs it if possible.
+ *
+ * @private
+ */
+ [kRun]() {
+ if (this.pending === this.concurrency) return;
+
+ if (this.jobs.length) {
+ const job = this.jobs.shift();
+
+ this.pending++;
+ job(this[kDone]);
+ }
+ }
+}
+
+module.exports = Limiter;
diff --git a/node_modules/ws/lib/permessage-deflate.js b/node_modules/ws/lib/permessage-deflate.js
new file mode 100644
index 0000000..ce91784
--- /dev/null
+++ b/node_modules/ws/lib/permessage-deflate.js
@@ -0,0 +1,518 @@
+'use strict';
+
+const zlib = require('zlib');
+
+const bufferUtil = require('./buffer-util');
+const Limiter = require('./limiter');
+const { kStatusCode, NOOP } = require('./constants');
+
+const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
+const kPerMessageDeflate = Symbol('permessage-deflate');
+const kTotalLength = Symbol('total-length');
+const kCallback = Symbol('callback');
+const kBuffers = Symbol('buffers');
+const kError = Symbol('error');
+
+//
+// We limit zlib concurrency, which prevents severe memory fragmentation
+// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913
+// and https://github.com/websockets/ws/issues/1202
+//
+// Intentionally global; it's the global thread pool that's an issue.
+//
+let zlibLimiter;
+
+/**
+ * permessage-deflate implementation.
+ */
+class PerMessageDeflate {
+ /**
+ * Creates a PerMessageDeflate instance.
+ *
+ * @param {Object} [options] Configuration options
+ * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept
+ * disabling of server context takeover
+ * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/
+ * acknowledge disabling of client context takeover
+ * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
+ * use of a custom server window size
+ * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support
+ * for, or request, a custom client window size
+ * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on
+ * deflate
+ * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
+ * inflate
+ * @param {Number} [options.threshold=1024] Size (in bytes) below which
+ * messages should not be compressed
+ * @param {Number} [options.concurrencyLimit=10] The number of concurrent
+ * calls to zlib
+ * @param {Boolean} [isServer=false] Create the instance in either server or
+ * client mode
+ * @param {Number} [maxPayload=0] The maximum allowed message length
+ */
+ constructor(options, isServer, maxPayload) {
+ this._maxPayload = maxPayload | 0;
+ this._options = options || {};
+ this._threshold =
+ this._options.threshold !== undefined ? this._options.threshold : 1024;
+ this._isServer = !!isServer;
+ this._deflate = null;
+ this._inflate = null;
+
+ this.params = null;
+
+ if (!zlibLimiter) {
+ const concurrency =
+ this._options.concurrencyLimit !== undefined
+ ? this._options.concurrencyLimit
+ : 10;
+ zlibLimiter = new Limiter(concurrency);
+ }
+ }
+
+ /**
+ * @type {String}
+ */
+ static get extensionName() {
+ return 'permessage-deflate';
+ }
+
+ /**
+ * Create an extension negotiation offer.
+ *
+ * @return {Object} Extension parameters
+ * @public
+ */
+ offer() {
+ const params = {};
+
+ if (this._options.serverNoContextTakeover) {
+ params.server_no_context_takeover = true;
+ }
+ if (this._options.clientNoContextTakeover) {
+ params.client_no_context_takeover = true;
+ }
+ if (this._options.serverMaxWindowBits) {
+ params.server_max_window_bits = this._options.serverMaxWindowBits;
+ }
+ if (this._options.clientMaxWindowBits) {
+ params.client_max_window_bits = this._options.clientMaxWindowBits;
+ } else if (this._options.clientMaxWindowBits == null) {
+ params.client_max_window_bits = true;
+ }
+
+ return params;
+ }
+
+ /**
+ * Accept an extension negotiation offer/response.
+ *
+ * @param {Array} configurations The extension negotiation offers/reponse
+ * @return {Object} Accepted configuration
+ * @public
+ */
+ accept(configurations) {
+ configurations = this.normalizeParams(configurations);
+
+ this.params = this._isServer
+ ? this.acceptAsServer(configurations)
+ : this.acceptAsClient(configurations);
+
+ return this.params;
+ }
+
+ /**
+ * Releases all resources used by the extension.
+ *
+ * @public
+ */
+ cleanup() {
+ if (this._inflate) {
+ this._inflate.close();
+ this._inflate = null;
+ }
+
+ if (this._deflate) {
+ const callback = this._deflate[kCallback];
+
+ this._deflate.close();
+ this._deflate = null;
+
+ if (callback) {
+ callback(
+ new Error(
+ 'The deflate stream was closed while data was being processed'
+ )
+ );
+ }
+ }
+ }
+
+ /**
+ * Accept an extension negotiation offer.
+ *
+ * @param {Array} offers The extension negotiation offers
+ * @return {Object} Accepted configuration
+ * @private
+ */
+ acceptAsServer(offers) {
+ const opts = this._options;
+ const accepted = offers.find((params) => {
+ if (
+ (opts.serverNoContextTakeover === false &&
+ params.server_no_context_takeover) ||
+ (params.server_max_window_bits &&
+ (opts.serverMaxWindowBits === false ||
+ (typeof opts.serverMaxWindowBits === 'number' &&
+ opts.serverMaxWindowBits > params.server_max_window_bits))) ||
+ (typeof opts.clientMaxWindowBits === 'number' &&
+ !params.client_max_window_bits)
+ ) {
+ return false;
+ }
+
+ return true;
+ });
+
+ if (!accepted) {
+ throw new Error('None of the extension offers can be accepted');
+ }
+
+ if (opts.serverNoContextTakeover) {
+ accepted.server_no_context_takeover = true;
+ }
+ if (opts.clientNoContextTakeover) {
+ accepted.client_no_context_takeover = true;
+ }
+ if (typeof opts.serverMaxWindowBits === 'number') {
+ accepted.server_max_window_bits = opts.serverMaxWindowBits;
+ }
+ if (typeof opts.clientMaxWindowBits === 'number') {
+ accepted.client_max_window_bits = opts.clientMaxWindowBits;
+ } else if (
+ accepted.client_max_window_bits === true ||
+ opts.clientMaxWindowBits === false
+ ) {
+ delete accepted.client_max_window_bits;
+ }
+
+ return accepted;
+ }
+
+ /**
+ * Accept the extension negotiation response.
+ *
+ * @param {Array} response The extension negotiation response
+ * @return {Object} Accepted configuration
+ * @private
+ */
+ acceptAsClient(response) {
+ const params = response[0];
+
+ if (
+ this._options.clientNoContextTakeover === false &&
+ params.client_no_context_takeover
+ ) {
+ throw new Error('Unexpected parameter "client_no_context_takeover"');
+ }
+
+ if (!params.client_max_window_bits) {
+ if (typeof this._options.clientMaxWindowBits === 'number') {
+ params.client_max_window_bits = this._options.clientMaxWindowBits;
+ }
+ } else if (
+ this._options.clientMaxWindowBits === false ||
+ (typeof this._options.clientMaxWindowBits === 'number' &&
+ params.client_max_window_bits > this._options.clientMaxWindowBits)
+ ) {
+ throw new Error(
+ 'Unexpected or invalid parameter "client_max_window_bits"'
+ );
+ }
+
+ return params;
+ }
+
+ /**
+ * Normalize parameters.
+ *
+ * @param {Array} configurations The extension negotiation offers/reponse
+ * @return {Array} The offers/response with normalized parameters
+ * @private
+ */
+ normalizeParams(configurations) {
+ configurations.forEach((params) => {
+ Object.keys(params).forEach((key) => {
+ let value = params[key];
+
+ if (value.length > 1) {
+ throw new Error(`Parameter "${key}" must have only a single value`);
+ }
+
+ value = value[0];
+
+ if (key === 'client_max_window_bits') {
+ if (value !== true) {
+ const num = +value;
+ if (!Number.isInteger(num) || num < 8 || num > 15) {
+ throw new TypeError(
+ `Invalid value for parameter "${key}": ${value}`
+ );
+ }
+ value = num;
+ } else if (!this._isServer) {
+ throw new TypeError(
+ `Invalid value for parameter "${key}": ${value}`
+ );
+ }
+ } else if (key === 'server_max_window_bits') {
+ const num = +value;
+ if (!Number.isInteger(num) || num < 8 || num > 15) {
+ throw new TypeError(
+ `Invalid value for parameter "${key}": ${value}`
+ );
+ }
+ value = num;
+ } else if (
+ key === 'client_no_context_takeover' ||
+ key === 'server_no_context_takeover'
+ ) {
+ if (value !== true) {
+ throw new TypeError(
+ `Invalid value for parameter "${key}": ${value}`
+ );
+ }
+ } else {
+ throw new Error(`Unknown parameter "${key}"`);
+ }
+
+ params[key] = value;
+ });
+ });
+
+ return configurations;
+ }
+
+ /**
+ * Decompress data. Concurrency limited.
+ *
+ * @param {Buffer} data Compressed data
+ * @param {Boolean} fin Specifies whether or not this is the last fragment
+ * @param {Function} callback Callback
+ * @public
+ */
+ decompress(data, fin, callback) {
+ zlibLimiter.add((done) => {
+ this._decompress(data, fin, (err, result) => {
+ done();
+ callback(err, result);
+ });
+ });
+ }
+
+ /**
+ * Compress data. Concurrency limited.
+ *
+ * @param {Buffer} data Data to compress
+ * @param {Boolean} fin Specifies whether or not this is the last fragment
+ * @param {Function} callback Callback
+ * @public
+ */
+ compress(data, fin, callback) {
+ zlibLimiter.add((done) => {
+ this._compress(data, fin, (err, result) => {
+ done();
+ callback(err, result);
+ });
+ });
+ }
+
+ /**
+ * Decompress data.
+ *
+ * @param {Buffer} data Compressed data
+ * @param {Boolean} fin Specifies whether or not this is the last fragment
+ * @param {Function} callback Callback
+ * @private
+ */
+ _decompress(data, fin, callback) {
+ const endpoint = this._isServer ? 'client' : 'server';
+
+ if (!this._inflate) {
+ const key = `${endpoint}_max_window_bits`;
+ const windowBits =
+ typeof this.params[key] !== 'number'
+ ? zlib.Z_DEFAULT_WINDOWBITS
+ : this.params[key];
+
+ this._inflate = zlib.createInflateRaw({
+ ...this._options.zlibInflateOptions,
+ windowBits
+ });
+ this._inflate[kPerMessageDeflate] = this;
+ this._inflate[kTotalLength] = 0;
+ this._inflate[kBuffers] = [];
+ this._inflate.on('error', inflateOnError);
+ this._inflate.on('data', inflateOnData);
+ }
+
+ this._inflate[kCallback] = callback;
+
+ this._inflate.write(data);
+ if (fin) this._inflate.write(TRAILER);
+
+ this._inflate.flush(() => {
+ const err = this._inflate[kError];
+
+ if (err) {
+ this._inflate.close();
+ this._inflate = null;
+ callback(err);
+ return;
+ }
+
+ const data = bufferUtil.concat(
+ this._inflate[kBuffers],
+ this._inflate[kTotalLength]
+ );
+
+ if (this._inflate._readableState.endEmitted) {
+ this._inflate.close();
+ this._inflate = null;
+ } else {
+ this._inflate[kTotalLength] = 0;
+ this._inflate[kBuffers] = [];
+
+ if (fin && this.params[`${endpoint}_no_context_takeover`]) {
+ this._inflate.reset();
+ }
+ }
+
+ callback(null, data);
+ });
+ }
+
+ /**
+ * Compress data.
+ *
+ * @param {Buffer} data Data to compress
+ * @param {Boolean} fin Specifies whether or not this is the last fragment
+ * @param {Function} callback Callback
+ * @private
+ */
+ _compress(data, fin, callback) {
+ const endpoint = this._isServer ? 'server' : 'client';
+
+ if (!this._deflate) {
+ const key = `${endpoint}_max_window_bits`;
+ const windowBits =
+ typeof this.params[key] !== 'number'
+ ? zlib.Z_DEFAULT_WINDOWBITS
+ : this.params[key];
+
+ this._deflate = zlib.createDeflateRaw({
+ ...this._options.zlibDeflateOptions,
+ windowBits
+ });
+
+ this._deflate[kTotalLength] = 0;
+ this._deflate[kBuffers] = [];
+
+ //
+ // An `'error'` event is emitted, only on Node.js < 10.0.0, if the
+ // `zlib.DeflateRaw` instance is closed while data is being processed.
+ // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong
+ // time due to an abnormal WebSocket closure.
+ //
+ this._deflate.on('error', NOOP);
+ this._deflate.on('data', deflateOnData);
+ }
+
+ this._deflate[kCallback] = callback;
+
+ this._deflate.write(data);
+ this._deflate.flush(zlib.Z_SYNC_FLUSH, () => {
+ if (!this._deflate) {
+ //
+ // The deflate stream was closed while data was being processed.
+ //
+ return;
+ }
+
+ let data = bufferUtil.concat(
+ this._deflate[kBuffers],
+ this._deflate[kTotalLength]
+ );
+
+ if (fin) data = data.slice(0, data.length - 4);
+
+ //
+ // Ensure that the callback will not be called again in
+ // `PerMessageDeflate#cleanup()`.
+ //
+ this._deflate[kCallback] = null;
+
+ this._deflate[kTotalLength] = 0;
+ this._deflate[kBuffers] = [];
+
+ if (fin && this.params[`${endpoint}_no_context_takeover`]) {
+ this._deflate.reset();
+ }
+
+ callback(null, data);
+ });
+ }
+}
+
+module.exports = PerMessageDeflate;
+
+/**
+ * The listener of the `zlib.DeflateRaw` stream `'data'` event.
+ *
+ * @param {Buffer} chunk A chunk of data
+ * @private
+ */
+function deflateOnData(chunk) {
+ this[kBuffers].push(chunk);
+ this[kTotalLength] += chunk.length;
+}
+
+/**
+ * The listener of the `zlib.InflateRaw` stream `'data'` event.
+ *
+ * @param {Buffer} chunk A chunk of data
+ * @private
+ */
+function inflateOnData(chunk) {
+ this[kTotalLength] += chunk.length;
+
+ if (
+ this[kPerMessageDeflate]._maxPayload < 1 ||
+ this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload
+ ) {
+ this[kBuffers].push(chunk);
+ return;
+ }
+
+ this[kError] = new RangeError('Max payload size exceeded');
+ this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH';
+ this[kError][kStatusCode] = 1009;
+ this.removeListener('data', inflateOnData);
+ this.reset();
+}
+
+/**
+ * The listener of the `zlib.InflateRaw` stream `'error'` event.
+ *
+ * @param {Error} err The emitted error
+ * @private
+ */
+function inflateOnError(err) {
+ //
+ // There is no need to call `Zlib#close()` as the handle is automatically
+ // closed when an error is emitted.
+ //
+ this[kPerMessageDeflate]._inflate = null;
+ err[kStatusCode] = 1007;
+ this[kCallback](err);
+}
diff --git a/node_modules/ws/lib/receiver.js b/node_modules/ws/lib/receiver.js
new file mode 100644
index 0000000..1d2af76
--- /dev/null
+++ b/node_modules/ws/lib/receiver.js
@@ -0,0 +1,607 @@
+'use strict';
+
+const { Writable } = require('stream');
+
+const PerMessageDeflate = require('./permessage-deflate');
+const {
+ BINARY_TYPES,
+ EMPTY_BUFFER,
+ kStatusCode,
+ kWebSocket
+} = require('./constants');
+const { concat, toArrayBuffer, unmask } = require('./buffer-util');
+const { isValidStatusCode, isValidUTF8 } = require('./validation');
+
+const GET_INFO = 0;
+const GET_PAYLOAD_LENGTH_16 = 1;
+const GET_PAYLOAD_LENGTH_64 = 2;
+const GET_MASK = 3;
+const GET_DATA = 4;
+const INFLATING = 5;
+
+/**
+ * HyBi Receiver implementation.
+ *
+ * @extends Writable
+ */
+class Receiver extends Writable {
+ /**
+ * Creates a Receiver instance.
+ *
+ * @param {String} [binaryType=nodebuffer] The type for binary data
+ * @param {Object} [extensions] An object containing the negotiated extensions
+ * @param {Boolean} [isServer=false] Specifies whether to operate in client or
+ * server mode
+ * @param {Number} [maxPayload=0] The maximum allowed message length
+ */
+ constructor(binaryType, extensions, isServer, maxPayload) {
+ super();
+
+ this._binaryType = binaryType || BINARY_TYPES[0];
+ this[kWebSocket] = undefined;
+ this._extensions = extensions || {};
+ this._isServer = !!isServer;
+ this._maxPayload = maxPayload | 0;
+
+ this._bufferedBytes = 0;
+ this._buffers = [];
+
+ this._compressed = false;
+ this._payloadLength = 0;
+ this._mask = undefined;
+ this._fragmented = 0;
+ this._masked = false;
+ this._fin = false;
+ this._opcode = 0;
+
+ this._totalPayloadLength = 0;
+ this._messageLength = 0;
+ this._fragments = [];
+
+ this._state = GET_INFO;
+ this._loop = false;
+ }
+
+ /**
+ * Implements `Writable.prototype._write()`.
+ *
+ * @param {Buffer} chunk The chunk of data to write
+ * @param {String} encoding The character encoding of `chunk`
+ * @param {Function} cb Callback
+ * @private
+ */
+ _write(chunk, encoding, cb) {
+ if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
+
+ this._bufferedBytes += chunk.length;
+ this._buffers.push(chunk);
+ this.startLoop(cb);
+ }
+
+ /**
+ * Consumes `n` bytes from the buffered data.
+ *
+ * @param {Number} n The number of bytes to consume
+ * @return {Buffer} The consumed bytes
+ * @private
+ */
+ consume(n) {
+ this._bufferedBytes -= n;
+
+ if (n === this._buffers[0].length) return this._buffers.shift();
+
+ if (n < this._buffers[0].length) {
+ const buf = this._buffers[0];
+ this._buffers[0] = buf.slice(n);
+ return buf.slice(0, n);
+ }
+
+ const dst = Buffer.allocUnsafe(n);
+
+ do {
+ const buf = this._buffers[0];
+ const offset = dst.length - n;
+
+ if (n >= buf.length) {
+ dst.set(this._buffers.shift(), offset);
+ } else {
+ dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset);
+ this._buffers[0] = buf.slice(n);
+ }
+
+ n -= buf.length;
+ } while (n > 0);
+
+ return dst;
+ }
+
+ /**
+ * Starts the parsing loop.
+ *
+ * @param {Function} cb Callback
+ * @private
+ */
+ startLoop(cb) {
+ let err;
+ this._loop = true;
+
+ do {
+ switch (this._state) {
+ case GET_INFO:
+ err = this.getInfo();
+ break;
+ case GET_PAYLOAD_LENGTH_16:
+ err = this.getPayloadLength16();
+ break;
+ case GET_PAYLOAD_LENGTH_64:
+ err = this.getPayloadLength64();
+ break;
+ case GET_MASK:
+ this.getMask();
+ break;
+ case GET_DATA:
+ err = this.getData(cb);
+ break;
+ default:
+ // `INFLATING`
+ this._loop = false;
+ return;
+ }
+ } while (this._loop);
+
+ cb(err);
+ }
+
+ /**
+ * Reads the first two bytes of a frame.
+ *
+ * @return {(RangeError|undefined)} A possible error
+ * @private
+ */
+ getInfo() {
+ if (this._bufferedBytes < 2) {
+ this._loop = false;
+ return;
+ }
+
+ const buf = this.consume(2);
+
+ if ((buf[0] & 0x30) !== 0x00) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'RSV2 and RSV3 must be clear',
+ true,
+ 1002,
+ 'WS_ERR_UNEXPECTED_RSV_2_3'
+ );
+ }
+
+ const compressed = (buf[0] & 0x40) === 0x40;
+
+ if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'RSV1 must be clear',
+ true,
+ 1002,
+ 'WS_ERR_UNEXPECTED_RSV_1'
+ );
+ }
+
+ this._fin = (buf[0] & 0x80) === 0x80;
+ this._opcode = buf[0] & 0x0f;
+ this._payloadLength = buf[1] & 0x7f;
+
+ if (this._opcode === 0x00) {
+ if (compressed) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'RSV1 must be clear',
+ true,
+ 1002,
+ 'WS_ERR_UNEXPECTED_RSV_1'
+ );
+ }
+
+ if (!this._fragmented) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'invalid opcode 0',
+ true,
+ 1002,
+ 'WS_ERR_INVALID_OPCODE'
+ );
+ }
+
+ this._opcode = this._fragmented;
+ } else if (this._opcode === 0x01 || this._opcode === 0x02) {
+ if (this._fragmented) {
+ this._loop = false;
+ return error(
+ RangeError,
+ `invalid opcode ${this._opcode}`,
+ true,
+ 1002,
+ 'WS_ERR_INVALID_OPCODE'
+ );
+ }
+
+ this._compressed = compressed;
+ } else if (this._opcode > 0x07 && this._opcode < 0x0b) {
+ if (!this._fin) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'FIN must be set',
+ true,
+ 1002,
+ 'WS_ERR_EXPECTED_FIN'
+ );
+ }
+
+ if (compressed) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'RSV1 must be clear',
+ true,
+ 1002,
+ 'WS_ERR_UNEXPECTED_RSV_1'
+ );
+ }
+
+ if (this._payloadLength > 0x7d) {
+ this._loop = false;
+ return error(
+ RangeError,
+ `invalid payload length ${this._payloadLength}`,
+ true,
+ 1002,
+ 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
+ );
+ }
+ } else {
+ this._loop = false;
+ return error(
+ RangeError,
+ `invalid opcode ${this._opcode}`,
+ true,
+ 1002,
+ 'WS_ERR_INVALID_OPCODE'
+ );
+ }
+
+ if (!this._fin && !this._fragmented) this._fragmented = this._opcode;
+ this._masked = (buf[1] & 0x80) === 0x80;
+
+ if (this._isServer) {
+ if (!this._masked) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'MASK must be set',
+ true,
+ 1002,
+ 'WS_ERR_EXPECTED_MASK'
+ );
+ }
+ } else if (this._masked) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'MASK must be clear',
+ true,
+ 1002,
+ 'WS_ERR_UNEXPECTED_MASK'
+ );
+ }
+
+ if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16;
+ else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64;
+ else return this.haveLength();
+ }
+
+ /**
+ * Gets extended payload length (7+16).
+ *
+ * @return {(RangeError|undefined)} A possible error
+ * @private
+ */
+ getPayloadLength16() {
+ if (this._bufferedBytes < 2) {
+ this._loop = false;
+ return;
+ }
+
+ this._payloadLength = this.consume(2).readUInt16BE(0);
+ return this.haveLength();
+ }
+
+ /**
+ * Gets extended payload length (7+64).
+ *
+ * @return {(RangeError|undefined)} A possible error
+ * @private
+ */
+ getPayloadLength64() {
+ if (this._bufferedBytes < 8) {
+ this._loop = false;
+ return;
+ }
+
+ const buf = this.consume(8);
+ const num = buf.readUInt32BE(0);
+
+ //
+ // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
+ // if payload length is greater than this number.
+ //
+ if (num > Math.pow(2, 53 - 32) - 1) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'Unsupported WebSocket frame: payload length > 2^53 - 1',
+ false,
+ 1009,
+ 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'
+ );
+ }
+
+ this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);
+ return this.haveLength();
+ }
+
+ /**
+ * Payload length has been read.
+ *
+ * @return {(RangeError|undefined)} A possible error
+ * @private
+ */
+ haveLength() {
+ if (this._payloadLength && this._opcode < 0x08) {
+ this._totalPayloadLength += this._payloadLength;
+ if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'Max payload size exceeded',
+ false,
+ 1009,
+ 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
+ );
+ }
+ }
+
+ if (this._masked) this._state = GET_MASK;
+ else this._state = GET_DATA;
+ }
+
+ /**
+ * Reads mask bytes.
+ *
+ * @private
+ */
+ getMask() {
+ if (this._bufferedBytes < 4) {
+ this._loop = false;
+ return;
+ }
+
+ this._mask = this.consume(4);
+ this._state = GET_DATA;
+ }
+
+ /**
+ * Reads data bytes.
+ *
+ * @param {Function} cb Callback
+ * @return {(Error|RangeError|undefined)} A possible error
+ * @private
+ */
+ getData(cb) {
+ let data = EMPTY_BUFFER;
+
+ if (this._payloadLength) {
+ if (this._bufferedBytes < this._payloadLength) {
+ this._loop = false;
+ return;
+ }
+
+ data = this.consume(this._payloadLength);
+ if (this._masked) unmask(data, this._mask);
+ }
+
+ if (this._opcode > 0x07) return this.controlMessage(data);
+
+ if (this._compressed) {
+ this._state = INFLATING;
+ this.decompress(data, cb);
+ return;
+ }
+
+ if (data.length) {
+ //
+ // This message is not compressed so its lenght is the sum of the payload
+ // length of all fragments.
+ //
+ this._messageLength = this._totalPayloadLength;
+ this._fragments.push(data);
+ }
+
+ return this.dataMessage();
+ }
+
+ /**
+ * Decompresses data.
+ *
+ * @param {Buffer} data Compressed data
+ * @param {Function} cb Callback
+ * @private
+ */
+ decompress(data, cb) {
+ const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
+
+ perMessageDeflate.decompress(data, this._fin, (err, buf) => {
+ if (err) return cb(err);
+
+ if (buf.length) {
+ this._messageLength += buf.length;
+ if (this._messageLength > this._maxPayload && this._maxPayload > 0) {
+ return cb(
+ error(
+ RangeError,
+ 'Max payload size exceeded',
+ false,
+ 1009,
+ 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
+ )
+ );
+ }
+
+ this._fragments.push(buf);
+ }
+
+ const er = this.dataMessage();
+ if (er) return cb(er);
+
+ this.startLoop(cb);
+ });
+ }
+
+ /**
+ * Handles a data message.
+ *
+ * @return {(Error|undefined)} A possible error
+ * @private
+ */
+ dataMessage() {
+ if (this._fin) {
+ const messageLength = this._messageLength;
+ const fragments = this._fragments;
+
+ this._totalPayloadLength = 0;
+ this._messageLength = 0;
+ this._fragmented = 0;
+ this._fragments = [];
+
+ if (this._opcode === 2) {
+ let data;
+
+ if (this._binaryType === 'nodebuffer') {
+ data = concat(fragments, messageLength);
+ } else if (this._binaryType === 'arraybuffer') {
+ data = toArrayBuffer(concat(fragments, messageLength));
+ } else {
+ data = fragments;
+ }
+
+ this.emit('message', data);
+ } else {
+ const buf = concat(fragments, messageLength);
+
+ if (!isValidUTF8(buf)) {
+ this._loop = false;
+ return error(
+ Error,
+ 'invalid UTF-8 sequence',
+ true,
+ 1007,
+ 'WS_ERR_INVALID_UTF8'
+ );
+ }
+
+ this.emit('message', buf.toString());
+ }
+ }
+
+ this._state = GET_INFO;
+ }
+
+ /**
+ * Handles a control message.
+ *
+ * @param {Buffer} data Data to handle
+ * @return {(Error|RangeError|undefined)} A possible error
+ * @private
+ */
+ controlMessage(data) {
+ if (this._opcode === 0x08) {
+ this._loop = false;
+
+ if (data.length === 0) {
+ this.emit('conclude', 1005, '');
+ this.end();
+ } else if (data.length === 1) {
+ return error(
+ RangeError,
+ 'invalid payload length 1',
+ true,
+ 1002,
+ 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
+ );
+ } else {
+ const code = data.readUInt16BE(0);
+
+ if (!isValidStatusCode(code)) {
+ return error(
+ RangeError,
+ `invalid status code ${code}`,
+ true,
+ 1002,
+ 'WS_ERR_INVALID_CLOSE_CODE'
+ );
+ }
+
+ const buf = data.slice(2);
+
+ if (!isValidUTF8(buf)) {
+ return error(
+ Error,
+ 'invalid UTF-8 sequence',
+ true,
+ 1007,
+ 'WS_ERR_INVALID_UTF8'
+ );
+ }
+
+ this.emit('conclude', code, buf.toString());
+ this.end();
+ }
+ } else if (this._opcode === 0x09) {
+ this.emit('ping', data);
+ } else {
+ this.emit('pong', data);
+ }
+
+ this._state = GET_INFO;
+ }
+}
+
+module.exports = Receiver;
+
+/**
+ * Builds an error object.
+ *
+ * @param {function(new:Error|RangeError)} ErrorCtor The error constructor
+ * @param {String} message The error message
+ * @param {Boolean} prefix Specifies whether or not to add a default prefix to
+ * `message`
+ * @param {Number} statusCode The status code
+ * @param {String} errorCode The exposed error code
+ * @return {(Error|RangeError)} The error
+ * @private
+ */
+function error(ErrorCtor, message, prefix, statusCode, errorCode) {
+ const err = new ErrorCtor(
+ prefix ? `Invalid WebSocket frame: ${message}` : message
+ );
+
+ Error.captureStackTrace(err, error);
+ err.code = errorCode;
+ err[kStatusCode] = statusCode;
+ return err;
+}
diff --git a/node_modules/ws/lib/sender.js b/node_modules/ws/lib/sender.js
new file mode 100644
index 0000000..441171c
--- /dev/null
+++ b/node_modules/ws/lib/sender.js
@@ -0,0 +1,409 @@
+/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */
+
+'use strict';
+
+const net = require('net');
+const tls = require('tls');
+const { randomFillSync } = require('crypto');
+
+const PerMessageDeflate = require('./permessage-deflate');
+const { EMPTY_BUFFER } = require('./constants');
+const { isValidStatusCode } = require('./validation');
+const { mask: applyMask, toBuffer } = require('./buffer-util');
+
+const mask = Buffer.alloc(4);
+
+/**
+ * HyBi Sender implementation.
+ */
+class Sender {
+ /**
+ * Creates a Sender instance.
+ *
+ * @param {(net.Socket|tls.Socket)} socket The connection socket
+ * @param {Object} [extensions] An object containing the negotiated extensions
+ */
+ constructor(socket, extensions) {
+ this._extensions = extensions || {};
+ this._socket = socket;
+
+ this._firstFragment = true;
+ this._compress = false;
+
+ this._bufferedBytes = 0;
+ this._deflating = false;
+ this._queue = [];
+ }
+
+ /**
+ * Frames a piece of data according to the HyBi WebSocket protocol.
+ *
+ * @param {Buffer} data The data to frame
+ * @param {Object} options Options object
+ * @param {Number} options.opcode The opcode
+ * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
+ * modified
+ * @param {Boolean} [options.fin=false] Specifies whether or not to set the
+ * FIN bit
+ * @param {Boolean} [options.mask=false] Specifies whether or not to mask
+ * `data`
+ * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
+ * RSV1 bit
+ * @return {Buffer[]} The framed data as a list of `Buffer` instances
+ * @public
+ */
+ static frame(data, options) {
+ const merge = options.mask && options.readOnly;
+ let offset = options.mask ? 6 : 2;
+ let payloadLength = data.length;
+
+ if (data.length >= 65536) {
+ offset += 8;
+ payloadLength = 127;
+ } else if (data.length > 125) {
+ offset += 2;
+ payloadLength = 126;
+ }
+
+ const target = Buffer.allocUnsafe(merge ? data.length + offset : offset);
+
+ target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
+ if (options.rsv1) target[0] |= 0x40;
+
+ target[1] = payloadLength;
+
+ if (payloadLength === 126) {
+ target.writeUInt16BE(data.length, 2);
+ } else if (payloadLength === 127) {
+ target.writeUInt32BE(0, 2);
+ target.writeUInt32BE(data.length, 6);
+ }
+
+ if (!options.mask) return [target, data];
+
+ randomFillSync(mask, 0, 4);
+
+ target[1] |= 0x80;
+ target[offset - 4] = mask[0];
+ target[offset - 3] = mask[1];
+ target[offset - 2] = mask[2];
+ target[offset - 1] = mask[3];
+
+ if (merge) {
+ applyMask(data, mask, target, offset, data.length);
+ return [target];
+ }
+
+ applyMask(data, mask, data, 0, data.length);
+ return [target, data];
+ }
+
+ /**
+ * Sends a close message to the other peer.
+ *
+ * @param {Number} [code] The status code component of the body
+ * @param {String} [data] The message component of the body
+ * @param {Boolean} [mask=false] Specifies whether or not to mask the message
+ * @param {Function} [cb] Callback
+ * @public
+ */
+ close(code, data, mask, cb) {
+ let buf;
+
+ if (code === undefined) {
+ buf = EMPTY_BUFFER;
+ } else if (typeof code !== 'number' || !isValidStatusCode(code)) {
+ throw new TypeError('First argument must be a valid error code number');
+ } else if (data === undefined || data === '') {
+ buf = Buffer.allocUnsafe(2);
+ buf.writeUInt16BE(code, 0);
+ } else {
+ const length = Buffer.byteLength(data);
+
+ if (length > 123) {
+ throw new RangeError('The message must not be greater than 123 bytes');
+ }
+
+ buf = Buffer.allocUnsafe(2 + length);
+ buf.writeUInt16BE(code, 0);
+ buf.write(data, 2);
+ }
+
+ if (this._deflating) {
+ this.enqueue([this.doClose, buf, mask, cb]);
+ } else {
+ this.doClose(buf, mask, cb);
+ }
+ }
+
+ /**
+ * Frames and sends a close message.
+ *
+ * @param {Buffer} data The message to send
+ * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
+ * @param {Function} [cb] Callback
+ * @private
+ */
+ doClose(data, mask, cb) {
+ this.sendFrame(
+ Sender.frame(data, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x08,
+ mask,
+ readOnly: false
+ }),
+ cb
+ );
+ }
+
+ /**
+ * Sends a ping message to the other peer.
+ *
+ * @param {*} data The message to send
+ * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
+ * @param {Function} [cb] Callback
+ * @public
+ */
+ ping(data, mask, cb) {
+ const buf = toBuffer(data);
+
+ if (buf.length > 125) {
+ throw new RangeError('The data size must not be greater than 125 bytes');
+ }
+
+ if (this._deflating) {
+ this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]);
+ } else {
+ this.doPing(buf, mask, toBuffer.readOnly, cb);
+ }
+ }
+
+ /**
+ * Frames and sends a ping message.
+ *
+ * @param {Buffer} data The message to send
+ * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
+ * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified
+ * @param {Function} [cb] Callback
+ * @private
+ */
+ doPing(data, mask, readOnly, cb) {
+ this.sendFrame(
+ Sender.frame(data, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x09,
+ mask,
+ readOnly
+ }),
+ cb
+ );
+ }
+
+ /**
+ * Sends a pong message to the other peer.
+ *
+ * @param {*} data The message to send
+ * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
+ * @param {Function} [cb] Callback
+ * @public
+ */
+ pong(data, mask, cb) {
+ const buf = toBuffer(data);
+
+ if (buf.length > 125) {
+ throw new RangeError('The data size must not be greater than 125 bytes');
+ }
+
+ if (this._deflating) {
+ this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]);
+ } else {
+ this.doPong(buf, mask, toBuffer.readOnly, cb);
+ }
+ }
+
+ /**
+ * Frames and sends a pong message.
+ *
+ * @param {Buffer} data The message to send
+ * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
+ * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified
+ * @param {Function} [cb] Callback
+ * @private
+ */
+ doPong(data, mask, readOnly, cb) {
+ this.sendFrame(
+ Sender.frame(data, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x0a,
+ mask,
+ readOnly
+ }),
+ cb
+ );
+ }
+
+ /**
+ * Sends a data message to the other peer.
+ *
+ * @param {*} data The message to send
+ * @param {Object} options Options object
+ * @param {Boolean} [options.compress=false] Specifies whether or not to
+ * compress `data`
+ * @param {Boolean} [options.binary=false] Specifies whether `data` is binary
+ * or text
+ * @param {Boolean} [options.fin=false] Specifies whether the fragment is the
+ * last one
+ * @param {Boolean} [options.mask=false] Specifies whether or not to mask
+ * `data`
+ * @param {Function} [cb] Callback
+ * @public
+ */
+ send(data, options, cb) {
+ const buf = toBuffer(data);
+ const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
+ let opcode = options.binary ? 2 : 1;
+ let rsv1 = options.compress;
+
+ if (this._firstFragment) {
+ this._firstFragment = false;
+ if (rsv1 && perMessageDeflate) {
+ rsv1 = buf.length >= perMessageDeflate._threshold;
+ }
+ this._compress = rsv1;
+ } else {
+ rsv1 = false;
+ opcode = 0;
+ }
+
+ if (options.fin) this._firstFragment = true;
+
+ if (perMessageDeflate) {
+ const opts = {
+ fin: options.fin,
+ rsv1,
+ opcode,
+ mask: options.mask,
+ readOnly: toBuffer.readOnly
+ };
+
+ if (this._deflating) {
+ this.enqueue([this.dispatch, buf, this._compress, opts, cb]);
+ } else {
+ this.dispatch(buf, this._compress, opts, cb);
+ }
+ } else {
+ this.sendFrame(
+ Sender.frame(buf, {
+ fin: options.fin,
+ rsv1: false,
+ opcode,
+ mask: options.mask,
+ readOnly: toBuffer.readOnly
+ }),
+ cb
+ );
+ }
+ }
+
+ /**
+ * Dispatches a data message.
+ *
+ * @param {Buffer} data The message to send
+ * @param {Boolean} [compress=false] Specifies whether or not to compress
+ * `data`
+ * @param {Object} options Options object
+ * @param {Number} options.opcode The opcode
+ * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
+ * modified
+ * @param {Boolean} [options.fin=false] Specifies whether or not to set the
+ * FIN bit
+ * @param {Boolean} [options.mask=false] Specifies whether or not to mask
+ * `data`
+ * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
+ * RSV1 bit
+ * @param {Function} [cb] Callback
+ * @private
+ */
+ dispatch(data, compress, options, cb) {
+ if (!compress) {
+ this.sendFrame(Sender.frame(data, options), cb);
+ return;
+ }
+
+ const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
+
+ this._bufferedBytes += data.length;
+ this._deflating = true;
+ perMessageDeflate.compress(data, options.fin, (_, buf) => {
+ if (this._socket.destroyed) {
+ const err = new Error(
+ 'The socket was closed while data was being compressed'
+ );
+
+ if (typeof cb === 'function') cb(err);
+
+ for (let i = 0; i < this._queue.length; i++) {
+ const callback = this._queue[i][4];
+
+ if (typeof callback === 'function') callback(err);
+ }
+
+ return;
+ }
+
+ this._bufferedBytes -= data.length;
+ this._deflating = false;
+ options.readOnly = false;
+ this.sendFrame(Sender.frame(buf, options), cb);
+ this.dequeue();
+ });
+ }
+
+ /**
+ * Executes queued send operations.
+ *
+ * @private
+ */
+ dequeue() {
+ while (!this._deflating && this._queue.length) {
+ const params = this._queue.shift();
+
+ this._bufferedBytes -= params[1].length;
+ Reflect.apply(params[0], this, params.slice(1));
+ }
+ }
+
+ /**
+ * Enqueues a send operation.
+ *
+ * @param {Array} params Send operation parameters.
+ * @private
+ */
+ enqueue(params) {
+ this._bufferedBytes += params[1].length;
+ this._queue.push(params);
+ }
+
+ /**
+ * Sends a frame.
+ *
+ * @param {Buffer[]} list The frame to send
+ * @param {Function} [cb] Callback
+ * @private
+ */
+ sendFrame(list, cb) {
+ if (list.length === 2) {
+ this._socket.cork();
+ this._socket.write(list[0]);
+ this._socket.write(list[1], cb);
+ this._socket.uncork();
+ } else {
+ this._socket.write(list[0], cb);
+ }
+ }
+}
+
+module.exports = Sender;
diff --git a/node_modules/ws/lib/stream.js b/node_modules/ws/lib/stream.js
new file mode 100644
index 0000000..19e1bff
--- /dev/null
+++ b/node_modules/ws/lib/stream.js
@@ -0,0 +1,180 @@
+'use strict';
+
+const { Duplex } = require('stream');
+
+/**
+ * Emits the `'close'` event on a stream.
+ *
+ * @param {Duplex} stream The stream.
+ * @private
+ */
+function emitClose(stream) {
+ stream.emit('close');
+}
+
+/**
+ * The listener of the `'end'` event.
+ *
+ * @private
+ */
+function duplexOnEnd() {
+ if (!this.destroyed && this._writableState.finished) {
+ this.destroy();
+ }
+}
+
+/**
+ * The listener of the `'error'` event.
+ *
+ * @param {Error} err The error
+ * @private
+ */
+function duplexOnError(err) {
+ this.removeListener('error', duplexOnError);
+ this.destroy();
+ if (this.listenerCount('error') === 0) {
+ // Do not suppress the throwing behavior.
+ this.emit('error', err);
+ }
+}
+
+/**
+ * Wraps a `WebSocket` in a duplex stream.
+ *
+ * @param {WebSocket} ws The `WebSocket` to wrap
+ * @param {Object} [options] The options for the `Duplex` constructor
+ * @return {Duplex} The duplex stream
+ * @public
+ */
+function createWebSocketStream(ws, options) {
+ let resumeOnReceiverDrain = true;
+ let terminateOnDestroy = true;
+
+ function receiverOnDrain() {
+ if (resumeOnReceiverDrain) ws._socket.resume();
+ }
+
+ if (ws.readyState === ws.CONNECTING) {
+ ws.once('open', function open() {
+ ws._receiver.removeAllListeners('drain');
+ ws._receiver.on('drain', receiverOnDrain);
+ });
+ } else {
+ ws._receiver.removeAllListeners('drain');
+ ws._receiver.on('drain', receiverOnDrain);
+ }
+
+ const duplex = new Duplex({
+ ...options,
+ autoDestroy: false,
+ emitClose: false,
+ objectMode: false,
+ writableObjectMode: false
+ });
+
+ ws.on('message', function message(msg) {
+ if (!duplex.push(msg)) {
+ resumeOnReceiverDrain = false;
+ ws._socket.pause();
+ }
+ });
+
+ ws.once('error', function error(err) {
+ if (duplex.destroyed) return;
+
+ // Prevent `ws.terminate()` from being called by `duplex._destroy()`.
+ //
+ // - If the `'error'` event is emitted before the `'open'` event, then
+ // `ws.terminate()` is a noop as no socket is assigned.
+ // - Otherwise, the error is re-emitted by the listener of the `'error'`
+ // event of the `Receiver` object. The listener already closes the
+ // connection by calling `ws.close()`. This allows a close frame to be
+ // sent to the other peer. If `ws.terminate()` is called right after this,
+ // then the close frame might not be sent.
+ terminateOnDestroy = false;
+ duplex.destroy(err);
+ });
+
+ ws.once('close', function close() {
+ if (duplex.destroyed) return;
+
+ duplex.push(null);
+ });
+
+ duplex._destroy = function (err, callback) {
+ if (ws.readyState === ws.CLOSED) {
+ callback(err);
+ process.nextTick(emitClose, duplex);
+ return;
+ }
+
+ let called = false;
+
+ ws.once('error', function error(err) {
+ called = true;
+ callback(err);
+ });
+
+ ws.once('close', function close() {
+ if (!called) callback(err);
+ process.nextTick(emitClose, duplex);
+ });
+
+ if (terminateOnDestroy) ws.terminate();
+ };
+
+ duplex._final = function (callback) {
+ if (ws.readyState === ws.CONNECTING) {
+ ws.once('open', function open() {
+ duplex._final(callback);
+ });
+ return;
+ }
+
+ // If the value of the `_socket` property is `null` it means that `ws` is a
+ // client websocket and the handshake failed. In fact, when this happens, a
+ // socket is never assigned to the websocket. Wait for the `'error'` event
+ // that will be emitted by the websocket.
+ if (ws._socket === null) return;
+
+ if (ws._socket._writableState.finished) {
+ callback();
+ if (duplex._readableState.endEmitted) duplex.destroy();
+ } else {
+ ws._socket.once('finish', function finish() {
+ // `duplex` is not destroyed here because the `'end'` event will be
+ // emitted on `duplex` after this `'finish'` event. The EOF signaling
+ // `null` chunk is, in fact, pushed when the websocket emits `'close'`.
+ callback();
+ });
+ ws.close();
+ }
+ };
+
+ duplex._read = function () {
+ if (
+ (ws.readyState === ws.OPEN || ws.readyState === ws.CLOSING) &&
+ !resumeOnReceiverDrain
+ ) {
+ resumeOnReceiverDrain = true;
+ if (!ws._receiver._writableState.needDrain) ws._socket.resume();
+ }
+ };
+
+ duplex._write = function (chunk, encoding, callback) {
+ if (ws.readyState === ws.CONNECTING) {
+ ws.once('open', function open() {
+ duplex._write(chunk, encoding, callback);
+ });
+ return;
+ }
+
+ ws.send(chunk, callback);
+ };
+
+ duplex.on('end', duplexOnEnd);
+ duplex.on('error', duplexOnError);
+ return duplex;
+}
+
+module.exports = createWebSocketStream;
diff --git a/node_modules/ws/lib/validation.js b/node_modules/ws/lib/validation.js
new file mode 100644
index 0000000..169ac6f
--- /dev/null
+++ b/node_modules/ws/lib/validation.js
@@ -0,0 +1,104 @@
+'use strict';
+
+/**
+ * Checks if a status code is allowed in a close frame.
+ *
+ * @param {Number} code The status code
+ * @return {Boolean} `true` if the status code is valid, else `false`
+ * @public
+ */
+function isValidStatusCode(code) {
+ return (
+ (code >= 1000 &&
+ code <= 1014 &&
+ code !== 1004 &&
+ code !== 1005 &&
+ code !== 1006) ||
+ (code >= 3000 && code <= 4999)
+ );
+}
+
+/**
+ * Checks if a given buffer contains only correct UTF-8.
+ * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by
+ * Markus Kuhn.
+ *
+ * @param {Buffer} buf The buffer to check
+ * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false`
+ * @public
+ */
+function _isValidUTF8(buf) {
+ const len = buf.length;
+ let i = 0;
+
+ while (i < len) {
+ if ((buf[i] & 0x80) === 0) {
+ // 0xxxxxxx
+ i++;
+ } else if ((buf[i] & 0xe0) === 0xc0) {
+ // 110xxxxx 10xxxxxx
+ if (
+ i + 1 === len ||
+ (buf[i + 1] & 0xc0) !== 0x80 ||
+ (buf[i] & 0xfe) === 0xc0 // Overlong
+ ) {
+ return false;
+ }
+
+ i += 2;
+ } else if ((buf[i] & 0xf0) === 0xe0) {
+ // 1110xxxx 10xxxxxx 10xxxxxx
+ if (
+ i + 2 >= len ||
+ (buf[i + 1] & 0xc0) !== 0x80 ||
+ (buf[i + 2] & 0xc0) !== 0x80 ||
+ (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong
+ (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF)
+ ) {
+ return false;
+ }
+
+ i += 3;
+ } else if ((buf[i] & 0xf8) === 0xf0) {
+ // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ if (
+ i + 3 >= len ||
+ (buf[i + 1] & 0xc0) !== 0x80 ||
+ (buf[i + 2] & 0xc0) !== 0x80 ||
+ (buf[i + 3] & 0xc0) !== 0x80 ||
+ (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong
+ (buf[i] === 0xf4 && buf[i + 1] > 0x8f) ||
+ buf[i] > 0xf4 // > U+10FFFF
+ ) {
+ return false;
+ }
+
+ i += 4;
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+try {
+ let isValidUTF8 = require('utf-8-validate');
+
+ /* istanbul ignore if */
+ if (typeof isValidUTF8 === 'object') {
+ isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0
+ }
+
+ module.exports = {
+ isValidStatusCode,
+ isValidUTF8(buf) {
+ return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf);
+ }
+ };
+} catch (e) /* istanbul ignore next */ {
+ module.exports = {
+ isValidStatusCode,
+ isValidUTF8: _isValidUTF8
+ };
+}
diff --git a/node_modules/ws/lib/websocket-server.js b/node_modules/ws/lib/websocket-server.js
new file mode 100644
index 0000000..fe7fdf5
--- /dev/null
+++ b/node_modules/ws/lib/websocket-server.js
@@ -0,0 +1,447 @@
+/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */
+
+'use strict';
+
+const EventEmitter = require('events');
+const http = require('http');
+const https = require('https');
+const net = require('net');
+const tls = require('tls');
+const { createHash } = require('crypto');
+
+const PerMessageDeflate = require('./permessage-deflate');
+const WebSocket = require('./websocket');
+const { format, parse } = require('./extension');
+const { GUID, kWebSocket } = require('./constants');
+
+const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
+
+const RUNNING = 0;
+const CLOSING = 1;
+const CLOSED = 2;
+
+/**
+ * Class representing a WebSocket server.
+ *
+ * @extends EventEmitter
+ */
+class WebSocketServer extends EventEmitter {
+ /**
+ * Create a `WebSocketServer` instance.
+ *
+ * @param {Object} options Configuration options
+ * @param {Number} [options.backlog=511] The maximum length of the queue of
+ * pending connections
+ * @param {Boolean} [options.clientTracking=true] Specifies whether or not to
+ * track clients
+ * @param {Function} [options.handleProtocols] A hook to handle protocols
+ * @param {String} [options.host] The hostname where to bind the server
+ * @param {Number} [options.maxPayload=104857600] The maximum allowed message
+ * size
+ * @param {Boolean} [options.noServer=false] Enable no server mode
+ * @param {String} [options.path] Accept only connections matching this path
+ * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable
+ * permessage-deflate
+ * @param {Number} [options.port] The port where to bind the server
+ * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S
+ * server to use
+ * @param {Function} [options.verifyClient] A hook to reject connections
+ * @param {Function} [callback] A listener for the `listening` event
+ */
+ constructor(options, callback) {
+ super();
+
+ options = {
+ maxPayload: 100 * 1024 * 1024,
+ perMessageDeflate: false,
+ handleProtocols: null,
+ clientTracking: true,
+ verifyClient: null,
+ noServer: false,
+ backlog: null, // use default (511 as implemented in net.js)
+ server: null,
+ host: null,
+ path: null,
+ port: null,
+ ...options
+ };
+
+ if (
+ (options.port == null && !options.server && !options.noServer) ||
+ (options.port != null && (options.server || options.noServer)) ||
+ (options.server && options.noServer)
+ ) {
+ throw new TypeError(
+ 'One and only one of the "port", "server", or "noServer" options ' +
+ 'must be specified'
+ );
+ }
+
+ if (options.port != null) {
+ this._server = http.createServer((req, res) => {
+ const body = http.STATUS_CODES[426];
+
+ res.writeHead(426, {
+ 'Content-Length': body.length,
+ 'Content-Type': 'text/plain'
+ });
+ res.end(body);
+ });
+ this._server.listen(
+ options.port,
+ options.host,
+ options.backlog,
+ callback
+ );
+ } else if (options.server) {
+ this._server = options.server;
+ }
+
+ if (this._server) {
+ const emitConnection = this.emit.bind(this, 'connection');
+
+ this._removeListeners = addListeners(this._server, {
+ listening: this.emit.bind(this, 'listening'),
+ error: this.emit.bind(this, 'error'),
+ upgrade: (req, socket, head) => {
+ this.handleUpgrade(req, socket, head, emitConnection);
+ }
+ });
+ }
+
+ if (options.perMessageDeflate === true) options.perMessageDeflate = {};
+ if (options.clientTracking) this.clients = new Set();
+ this.options = options;
+ this._state = RUNNING;
+ }
+
+ /**
+ * Returns the bound address, the address family name, and port of the server
+ * as reported by the operating system if listening on an IP socket.
+ * If the server is listening on a pipe or UNIX domain socket, the name is
+ * returned as a string.
+ *
+ * @return {(Object|String|null)} The address of the server
+ * @public
+ */
+ address() {
+ if (this.options.noServer) {
+ throw new Error('The server is operating in "noServer" mode');
+ }
+
+ if (!this._server) return null;
+ return this._server.address();
+ }
+
+ /**
+ * Close the server.
+ *
+ * @param {Function} [cb] Callback
+ * @public
+ */
+ close(cb) {
+ if (cb) this.once('close', cb);
+
+ if (this._state === CLOSED) {
+ process.nextTick(emitClose, this);
+ return;
+ }
+
+ if (this._state === CLOSING) return;
+ this._state = CLOSING;
+
+ //
+ // Terminate all associated clients.
+ //
+ if (this.clients) {
+ for (const client of this.clients) client.terminate();
+ }
+
+ const server = this._server;
+
+ if (server) {
+ this._removeListeners();
+ this._removeListeners = this._server = null;
+
+ //
+ // Close the http server if it was internally created.
+ //
+ if (this.options.port != null) {
+ server.close(emitClose.bind(undefined, this));
+ return;
+ }
+ }
+
+ process.nextTick(emitClose, this);
+ }
+
+ /**
+ * See if a given request should be handled by this server instance.
+ *
+ * @param {http.IncomingMessage} req Request object to inspect
+ * @return {Boolean} `true` if the request is valid, else `false`
+ * @public
+ */
+ shouldHandle(req) {
+ if (this.options.path) {
+ const index = req.url.indexOf('?');
+ const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
+
+ if (pathname !== this.options.path) return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Handle a HTTP Upgrade request.
+ *
+ * @param {http.IncomingMessage} req The request object
+ * @param {(net.Socket|tls.Socket)} socket The network socket between the
+ * server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Function} cb Callback
+ * @public
+ */
+ handleUpgrade(req, socket, head, cb) {
+ socket.on('error', socketOnError);
+
+ const key =
+ req.headers['sec-websocket-key'] !== undefined
+ ? req.headers['sec-websocket-key'].trim()
+ : false;
+ const version = +req.headers['sec-websocket-version'];
+ const extensions = {};
+
+ if (
+ req.method !== 'GET' ||
+ req.headers.upgrade.toLowerCase() !== 'websocket' ||
+ !key ||
+ !keyRegex.test(key) ||
+ (version !== 8 && version !== 13) ||
+ !this.shouldHandle(req)
+ ) {
+ return abortHandshake(socket, 400);
+ }
+
+ if (this.options.perMessageDeflate) {
+ const perMessageDeflate = new PerMessageDeflate(
+ this.options.perMessageDeflate,
+ true,
+ this.options.maxPayload
+ );
+
+ try {
+ const offers = parse(req.headers['sec-websocket-extensions']);
+
+ if (offers[PerMessageDeflate.extensionName]) {
+ perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
+ extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
+ }
+ } catch (err) {
+ return abortHandshake(socket, 400);
+ }
+ }
+
+ //
+ // Optionally call external client verification handler.
+ //
+ if (this.options.verifyClient) {
+ const info = {
+ origin:
+ req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
+ secure: !!(req.socket.authorized || req.socket.encrypted),
+ req
+ };
+
+ if (this.options.verifyClient.length === 2) {
+ this.options.verifyClient(info, (verified, code, message, headers) => {
+ if (!verified) {
+ return abortHandshake(socket, code || 401, message, headers);
+ }
+
+ this.completeUpgrade(key, extensions, req, socket, head, cb);
+ });
+ return;
+ }
+
+ if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
+ }
+
+ this.completeUpgrade(key, extensions, req, socket, head, cb);
+ }
+
+ /**
+ * Upgrade the connection to WebSocket.
+ *
+ * @param {String} key The value of the `Sec-WebSocket-Key` header
+ * @param {Object} extensions The accepted extensions
+ * @param {http.IncomingMessage} req The request object
+ * @param {(net.Socket|tls.Socket)} socket The network socket between the
+ * server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Function} cb Callback
+ * @throws {Error} If called more than once with the same socket
+ * @private
+ */
+ completeUpgrade(key, extensions, req, socket, head, cb) {
+ //
+ // Destroy the socket if the client has already sent a FIN packet.
+ //
+ if (!socket.readable || !socket.writable) return socket.destroy();
+
+ if (socket[kWebSocket]) {
+ throw new Error(
+ 'server.handleUpgrade() was called more than once with the same ' +
+ 'socket, possibly due to a misconfiguration'
+ );
+ }
+
+ if (this._state > RUNNING) return abortHandshake(socket, 503);
+
+ const digest = createHash('sha1')
+ .update(key + GUID)
+ .digest('base64');
+
+ const headers = [
+ 'HTTP/1.1 101 Switching Protocols',
+ 'Upgrade: websocket',
+ 'Connection: Upgrade',
+ `Sec-WebSocket-Accept: ${digest}`
+ ];
+
+ const ws = new WebSocket(null);
+ let protocol = req.headers['sec-websocket-protocol'];
+
+ if (protocol) {
+ protocol = protocol.split(',').map(trim);
+
+ //
+ // Optionally call external protocol selection handler.
+ //
+ if (this.options.handleProtocols) {
+ protocol = this.options.handleProtocols(protocol, req);
+ } else {
+ protocol = protocol[0];
+ }
+
+ if (protocol) {
+ headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
+ ws._protocol = protocol;
+ }
+ }
+
+ if (extensions[PerMessageDeflate.extensionName]) {
+ const params = extensions[PerMessageDeflate.extensionName].params;
+ const value = format({
+ [PerMessageDeflate.extensionName]: [params]
+ });
+ headers.push(`Sec-WebSocket-Extensions: ${value}`);
+ ws._extensions = extensions;
+ }
+
+ //
+ // Allow external modification/inspection of handshake headers.
+ //
+ this.emit('headers', headers, req);
+
+ socket.write(headers.concat('\r\n').join('\r\n'));
+ socket.removeListener('error', socketOnError);
+
+ ws.setSocket(socket, head, this.options.maxPayload);
+
+ if (this.clients) {
+ this.clients.add(ws);
+ ws.on('close', () => this.clients.delete(ws));
+ }
+
+ cb(ws, req);
+ }
+}
+
+module.exports = WebSocketServer;
+
+/**
+ * Add event listeners on an `EventEmitter` using a map of <event, listener>
+ * pairs.
+ *
+ * @param {EventEmitter} server The event emitter
+ * @param {Object.<String, Function>} map The listeners to add
+ * @return {Function} A function that will remove the added listeners when
+ * called
+ * @private
+ */
+function addListeners(server, map) {
+ for (const event of Object.keys(map)) server.on(event, map[event]);
+
+ return function removeListeners() {
+ for (const event of Object.keys(map)) {
+ server.removeListener(event, map[event]);
+ }
+ };
+}
+
+/**
+ * Emit a `'close'` event on an `EventEmitter`.
+ *
+ * @param {EventEmitter} server The event emitter
+ * @private
+ */
+function emitClose(server) {
+ server._state = CLOSED;
+ server.emit('close');
+}
+
+/**
+ * Handle premature socket errors.
+ *
+ * @private
+ */
+function socketOnError() {
+ this.destroy();
+}
+
+/**
+ * Close the connection when preconditions are not fulfilled.
+ *
+ * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request
+ * @param {Number} code The HTTP response status code
+ * @param {String} [message] The HTTP response body
+ * @param {Object} [headers] Additional HTTP response headers
+ * @private
+ */
+function abortHandshake(socket, code, message, headers) {
+ if (socket.writable) {
+ message = message || http.STATUS_CODES[code];
+ headers = {
+ Connection: 'close',
+ 'Content-Type': 'text/html',
+ 'Content-Length': Buffer.byteLength(message),
+ ...headers
+ };
+
+ socket.write(
+ `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
+ Object.keys(headers)
+ .map((h) => `${h}: ${headers[h]}`)
+ .join('\r\n') +
+ '\r\n\r\n' +
+ message
+ );
+ }
+
+ socket.removeListener('error', socketOnError);
+ socket.destroy();
+}
+
+/**
+ * Remove whitespace characters from both ends of a string.
+ *
+ * @param {String} str The string
+ * @return {String} A new string representing `str` stripped of whitespace
+ * characters from both its beginning and end
+ * @private
+ */
+function trim(str) {
+ return str.trim();
+}
diff --git a/node_modules/ws/lib/websocket.js b/node_modules/ws/lib/websocket.js
new file mode 100644
index 0000000..1df8967
--- /dev/null
+++ b/node_modules/ws/lib/websocket.js
@@ -0,0 +1,1195 @@
+/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */
+
+'use strict';
+
+const EventEmitter = require('events');
+const https = require('https');
+const http = require('http');
+const net = require('net');
+const tls = require('tls');
+const { randomBytes, createHash } = require('crypto');
+const { Readable } = require('stream');
+const { URL } = require('url');
+
+const PerMessageDeflate = require('./permessage-deflate');
+const Receiver = require('./receiver');
+const Sender = require('./sender');
+const {
+ BINARY_TYPES,
+ EMPTY_BUFFER,
+ GUID,
+ kStatusCode,
+ kWebSocket,
+ NOOP
+} = require('./constants');
+const { addEventListener, removeEventListener } = require('./event-target');
+const { format, parse } = require('./extension');
+const { toBuffer } = require('./buffer-util');
+
+const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
+const protocolVersions = [8, 13];
+const closeTimeout = 30 * 1000;
+
+/**
+ * Class representing a WebSocket.
+ *
+ * @extends EventEmitter
+ */
+class WebSocket extends EventEmitter {
+ /**
+ * Create a new `WebSocket`.
+ *
+ * @param {(String|URL)} address The URL to which to connect
+ * @param {(String|String[])} [protocols] The subprotocols
+ * @param {Object} [options] Connection options
+ */
+ constructor(address, protocols, options) {
+ super();
+
+ this._binaryType = BINARY_TYPES[0];
+ this._closeCode = 1006;
+ this._closeFrameReceived = false;
+ this._closeFrameSent = false;
+ this._closeMessage = '';
+ this._closeTimer = null;
+ this._extensions = {};
+ this._protocol = '';
+ this._readyState = WebSocket.CONNECTING;
+ this._receiver = null;
+ this._sender = null;
+ this._socket = null;
+
+ if (address !== null) {
+ this._bufferedAmount = 0;
+ this._isServer = false;
+ this._redirects = 0;
+
+ if (Array.isArray(protocols)) {
+ protocols = protocols.join(', ');
+ } else if (typeof protocols === 'object' && protocols !== null) {
+ options = protocols;
+ protocols = undefined;
+ }
+
+ initAsClient(this, address, protocols, options);
+ } else {
+ this._isServer = true;
+ }
+ }
+
+ /**
+ * This deviates from the WHATWG interface since ws doesn't support the
+ * required default "blob" type (instead we define a custom "nodebuffer"
+ * type).
+ *
+ * @type {String}
+ */
+ get binaryType() {
+ return this._binaryType;
+ }
+
+ set binaryType(type) {
+ if (!BINARY_TYPES.includes(type)) return;
+
+ this._binaryType = type;
+
+ //
+ // Allow to change `binaryType` on the fly.
+ //
+ if (this._receiver) this._receiver._binaryType = type;
+ }
+
+ /**
+ * @type {Number}
+ */
+ get bufferedAmount() {
+ if (!this._socket) return this._bufferedAmount;
+
+ return this._socket._writableState.length + this._sender._bufferedBytes;
+ }
+
+ /**
+ * @type {String}
+ */
+ get extensions() {
+ return Object.keys(this._extensions).join();
+ }
+
+ /**
+ * @type {Function}
+ */
+ /* istanbul ignore next */
+ get onclose() {
+ return undefined;
+ }
+
+ /* istanbul ignore next */
+ set onclose(listener) {}
+
+ /**
+ * @type {Function}
+ */
+ /* istanbul ignore next */
+ get onerror() {
+ return undefined;
+ }
+
+ /* istanbul ignore next */
+ set onerror(listener) {}
+
+ /**
+ * @type {Function}
+ */
+ /* istanbul ignore next */
+ get onopen() {
+ return undefined;
+ }
+
+ /* istanbul ignore next */
+ set onopen(listener) {}
+
+ /**
+ * @type {Function}
+ */
+ /* istanbul ignore next */
+ get onmessage() {
+ return undefined;
+ }
+
+ /* istanbul ignore next */
+ set onmessage(listener) {}
+
+ /**
+ * @type {String}
+ */
+ get protocol() {
+ return this._protocol;
+ }
+
+ /**
+ * @type {Number}
+ */
+ get readyState() {
+ return this._readyState;
+ }
+
+ /**
+ * @type {String}
+ */
+ get url() {
+ return this._url;
+ }
+
+ /**
+ * Set up the socket and the internal resources.
+ *
+ * @param {(net.Socket|tls.Socket)} socket The network socket between the
+ * server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Number} [maxPayload=0] The maximum allowed message size
+ * @private
+ */
+ setSocket(socket, head, maxPayload) {
+ const receiver = new Receiver(
+ this.binaryType,
+ this._extensions,
+ this._isServer,
+ maxPayload
+ );
+
+ this._sender = new Sender(socket, this._extensions);
+ this._receiver = receiver;
+ this._socket = socket;
+
+ receiver[kWebSocket] = this;
+ socket[kWebSocket] = this;
+
+ receiver.on('conclude', receiverOnConclude);
+ receiver.on('drain', receiverOnDrain);
+ receiver.on('error', receiverOnError);
+ receiver.on('message', receiverOnMessage);
+ receiver.on('ping', receiverOnPing);
+ receiver.on('pong', receiverOnPong);
+
+ socket.setTimeout(0);
+ socket.setNoDelay();
+
+ if (head.length > 0) socket.unshift(head);
+
+ socket.on('close', socketOnClose);
+ socket.on('data', socketOnData);
+ socket.on('end', socketOnEnd);
+ socket.on('error', socketOnError);
+
+ this._readyState = WebSocket.OPEN;
+ this.emit('open');
+ }
+
+ /**
+ * Emit the `'close'` event.
+ *
+ * @private
+ */
+ emitClose() {
+ if (!this._socket) {
+ this._readyState = WebSocket.CLOSED;
+ this.emit('close', this._closeCode, this._closeMessage);
+ return;
+ }
+
+ if (this._extensions[PerMessageDeflate.extensionName]) {
+ this._extensions[PerMessageDeflate.extensionName].cleanup();
+ }
+
+ this._receiver.removeAllListeners();
+ this._readyState = WebSocket.CLOSED;
+ this.emit('close', this._closeCode, this._closeMessage);
+ }
+
+ /**
+ * Start a closing handshake.
+ *
+ * +----------+ +-----------+ +----------+
+ * - - -|ws.close()|-->|close frame|-->|ws.close()|- - -
+ * | +----------+ +-----------+ +----------+ |
+ * +----------+ +-----------+ |
+ * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING
+ * +----------+ +-----------+ |
+ * | | | +---+ |
+ * +------------------------+-->|fin| - - - -
+ * | +---+ | +---+
+ * - - - - -|fin|<---------------------+
+ * +---+
+ *
+ * @param {Number} [code] Status code explaining why the connection is closing
+ * @param {String} [data] A string explaining why the connection is closing
+ * @public
+ */
+ close(code, data) {
+ if (this.readyState === WebSocket.CLOSED) return;
+ if (this.readyState === WebSocket.CONNECTING) {
+ const msg = 'WebSocket was closed before the connection was established';
+ return abortHandshake(this, this._req, msg);
+ }
+
+ if (this.readyState === WebSocket.CLOSING) {
+ if (
+ this._closeFrameSent &&
+ (this._closeFrameReceived || this._receiver._writableState.errorEmitted)
+ ) {
+ this._socket.end();
+ }
+
+ return;
+ }
+
+ this._readyState = WebSocket.CLOSING;
+ this._sender.close(code, data, !this._isServer, (err) => {
+ //
+ // This error is handled by the `'error'` listener on the socket. We only
+ // want to know if the close frame has been sent here.
+ //
+ if (err) return;
+
+ this._closeFrameSent = true;
+
+ if (
+ this._closeFrameReceived ||
+ this._receiver._writableState.errorEmitted
+ ) {
+ this._socket.end();
+ }
+ });
+
+ //
+ // Specify a timeout for the closing handshake to complete.
+ //
+ this._closeTimer = setTimeout(
+ this._socket.destroy.bind(this._socket),
+ closeTimeout
+ );
+ }
+
+ /**
+ * Send a ping.
+ *
+ * @param {*} [data] The data to send
+ * @param {Boolean} [mask] Indicates whether or not to mask `data`
+ * @param {Function} [cb] Callback which is executed when the ping is sent
+ * @public
+ */
+ ping(data, mask, cb) {
+ if (this.readyState === WebSocket.CONNECTING) {
+ throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
+ }
+
+ if (typeof data === 'function') {
+ cb = data;
+ data = mask = undefined;
+ } else if (typeof mask === 'function') {
+ cb = mask;
+ mask = undefined;
+ }
+
+ if (typeof data === 'number') data = data.toString();
+
+ if (this.readyState !== WebSocket.OPEN) {
+ sendAfterClose(this, data, cb);
+ return;
+ }
+
+ if (mask === undefined) mask = !this._isServer;
+ this._sender.ping(data || EMPTY_BUFFER, mask, cb);
+ }
+
+ /**
+ * Send a pong.
+ *
+ * @param {*} [data] The data to send
+ * @param {Boolean} [mask] Indicates whether or not to mask `data`
+ * @param {Function} [cb] Callback which is executed when the pong is sent
+ * @public
+ */
+ pong(data, mask, cb) {
+ if (this.readyState === WebSocket.CONNECTING) {
+ throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
+ }
+
+ if (typeof data === 'function') {
+ cb = data;
+ data = mask = undefined;
+ } else if (typeof mask === 'function') {
+ cb = mask;
+ mask = undefined;
+ }
+
+ if (typeof data === 'number') data = data.toString();
+
+ if (this.readyState !== WebSocket.OPEN) {
+ sendAfterClose(this, data, cb);
+ return;
+ }
+
+ if (mask === undefined) mask = !this._isServer;
+ this._sender.pong(data || EMPTY_BUFFER, mask, cb);
+ }
+
+ /**
+ * Send a data message.
+ *
+ * @param {*} data The message to send
+ * @param {Object} [options] Options object
+ * @param {Boolean} [options.compress] Specifies whether or not to compress
+ * `data`
+ * @param {Boolean} [options.binary] Specifies whether `data` is binary or
+ * text
+ * @param {Boolean} [options.fin=true] Specifies whether the fragment is the
+ * last one
+ * @param {Boolean} [options.mask] Specifies whether or not to mask `data`
+ * @param {Function} [cb] Callback which is executed when data is written out
+ * @public
+ */
+ send(data, options, cb) {
+ if (this.readyState === WebSocket.CONNECTING) {
+ throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
+ }
+
+ if (typeof options === 'function') {
+ cb = options;
+ options = {};
+ }
+
+ if (typeof data === 'number') data = data.toString();
+
+ if (this.readyState !== WebSocket.OPEN) {
+ sendAfterClose(this, data, cb);
+ return;
+ }
+
+ const opts = {
+ binary: typeof data !== 'string',
+ mask: !this._isServer,
+ compress: true,
+ fin: true,
+ ...options
+ };
+
+ if (!this._extensions[PerMessageDeflate.extensionName]) {
+ opts.compress = false;
+ }
+
+ this._sender.send(data || EMPTY_BUFFER, opts, cb);
+ }
+
+ /**
+ * Forcibly close the connection.
+ *
+ * @public
+ */
+ terminate() {
+ if (this.readyState === WebSocket.CLOSED) return;
+ if (this.readyState === WebSocket.CONNECTING) {
+ const msg = 'WebSocket was closed before the connection was established';
+ return abortHandshake(this, this._req, msg);
+ }
+
+ if (this._socket) {
+ this._readyState = WebSocket.CLOSING;
+ this._socket.destroy();
+ }
+ }
+}
+
+/**
+ * @constant {Number} CONNECTING
+ * @memberof WebSocket
+ */
+Object.defineProperty(WebSocket, 'CONNECTING', {
+ enumerable: true,
+ value: readyStates.indexOf('CONNECTING')
+});
+
+/**
+ * @constant {Number} CONNECTING
+ * @memberof WebSocket.prototype
+ */
+Object.defineProperty(WebSocket.prototype, 'CONNECTING', {
+ enumerable: true,
+ value: readyStates.indexOf('CONNECTING')
+});
+
+/**
+ * @constant {Number} OPEN
+ * @memberof WebSocket
+ */
+Object.defineProperty(WebSocket, 'OPEN', {
+ enumerable: true,
+ value: readyStates.indexOf('OPEN')
+});
+
+/**
+ * @constant {Number} OPEN
+ * @memberof WebSocket.prototype
+ */
+Object.defineProperty(WebSocket.prototype, 'OPEN', {
+ enumerable: true,
+ value: readyStates.indexOf('OPEN')
+});
+
+/**
+ * @constant {Number} CLOSING
+ * @memberof WebSocket
+ */
+Object.defineProperty(WebSocket, 'CLOSING', {
+ enumerable: true,
+ value: readyStates.indexOf('CLOSING')
+});
+
+/**
+ * @constant {Number} CLOSING
+ * @memberof WebSocket.prototype
+ */
+Object.defineProperty(WebSocket.prototype, 'CLOSING', {
+ enumerable: true,
+ value: readyStates.indexOf('CLOSING')
+});
+
+/**
+ * @constant {Number} CLOSED
+ * @memberof WebSocket
+ */
+Object.defineProperty(WebSocket, 'CLOSED', {
+ enumerable: true,
+ value: readyStates.indexOf('CLOSED')
+});
+
+/**
+ * @constant {Number} CLOSED
+ * @memberof WebSocket.prototype
+ */
+Object.defineProperty(WebSocket.prototype, 'CLOSED', {
+ enumerable: true,
+ value: readyStates.indexOf('CLOSED')
+});
+
+[
+ 'binaryType',
+ 'bufferedAmount',
+ 'extensions',
+ 'protocol',
+ 'readyState',
+ 'url'
+].forEach((property) => {
+ Object.defineProperty(WebSocket.prototype, property, { enumerable: true });
+});
+
+//
+// Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes.
+// See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface
+//
+['open', 'error', 'close', 'message'].forEach((method) => {
+ Object.defineProperty(WebSocket.prototype, `on${method}`, {
+ enumerable: true,
+ get() {
+ const listeners = this.listeners(method);
+ for (let i = 0; i < listeners.length; i++) {
+ if (listeners[i]._listener) return listeners[i]._listener;
+ }
+
+ return undefined;
+ },
+ set(listener) {
+ const listeners = this.listeners(method);
+ for (let i = 0; i < listeners.length; i++) {
+ //
+ // Remove only the listeners added via `addEventListener`.
+ //
+ if (listeners[i]._listener) this.removeListener(method, listeners[i]);
+ }
+ this.addEventListener(method, listener);
+ }
+ });
+});
+
+WebSocket.prototype.addEventListener = addEventListener;
+WebSocket.prototype.removeEventListener = removeEventListener;
+
+module.exports = WebSocket;
+
+/**
+ * Initialize a WebSocket client.
+ *
+ * @param {WebSocket} websocket The client to initialize
+ * @param {(String|URL)} address The URL to which to connect
+ * @param {String} [protocols] The subprotocols
+ * @param {Object} [options] Connection options
+ * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable
+ * permessage-deflate
+ * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
+ * handshake request
+ * @param {Number} [options.protocolVersion=13] Value of the
+ * `Sec-WebSocket-Version` header
+ * @param {String} [options.origin] Value of the `Origin` or
+ * `Sec-WebSocket-Origin` header
+ * @param {Number} [options.maxPayload=104857600] The maximum allowed message
+ * size
+ * @param {Boolean} [options.followRedirects=false] Whether or not to follow
+ * redirects
+ * @param {Number} [options.maxRedirects=10] The maximum number of redirects
+ * allowed
+ * @private
+ */
+function initAsClient(websocket, address, protocols, options) {
+ const opts = {
+ protocolVersion: protocolVersions[1],
+ maxPayload: 100 * 1024 * 1024,
+ perMessageDeflate: true,
+ followRedirects: false,
+ maxRedirects: 10,
+ ...options,
+ createConnection: undefined,
+ socketPath: undefined,
+ hostname: undefined,
+ protocol: undefined,
+ timeout: undefined,
+ method: undefined,
+ host: undefined,
+ path: undefined,
+ port: undefined
+ };
+
+ if (!protocolVersions.includes(opts.protocolVersion)) {
+ throw new RangeError(
+ `Unsupported protocol version: ${opts.protocolVersion} ` +
+ `(supported versions: ${protocolVersions.join(', ')})`
+ );
+ }
+
+ let parsedUrl;
+
+ if (address instanceof URL) {
+ parsedUrl = address;
+ websocket._url = address.href;
+ } else {
+ parsedUrl = new URL(address);
+ websocket._url = address;
+ }
+
+ const isUnixSocket = parsedUrl.protocol === 'ws+unix:';
+
+ if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) {
+ const err = new Error(`Invalid URL: ${websocket.url}`);
+
+ if (websocket._redirects === 0) {
+ throw err;
+ } else {
+ emitErrorAndClose(websocket, err);
+ return;
+ }
+ }
+
+ const isSecure =
+ parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:';
+ const defaultPort = isSecure ? 443 : 80;
+ const key = randomBytes(16).toString('base64');
+ const get = isSecure ? https.get : http.get;
+ let perMessageDeflate;
+
+ opts.createConnection = isSecure ? tlsConnect : netConnect;
+ opts.defaultPort = opts.defaultPort || defaultPort;
+ opts.port = parsedUrl.port || defaultPort;
+ opts.host = parsedUrl.hostname.startsWith('[')
+ ? parsedUrl.hostname.slice(1, -1)
+ : parsedUrl.hostname;
+ opts.headers = {
+ 'Sec-WebSocket-Version': opts.protocolVersion,
+ 'Sec-WebSocket-Key': key,
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ ...opts.headers
+ };
+ opts.path = parsedUrl.pathname + parsedUrl.search;
+ opts.timeout = opts.handshakeTimeout;
+
+ if (opts.perMessageDeflate) {
+ perMessageDeflate = new PerMessageDeflate(
+ opts.perMessageDeflate !== true ? opts.perMessageDeflate : {},
+ false,
+ opts.maxPayload
+ );
+ opts.headers['Sec-WebSocket-Extensions'] = format({
+ [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
+ });
+ }
+ if (protocols) {
+ opts.headers['Sec-WebSocket-Protocol'] = protocols;
+ }
+ if (opts.origin) {
+ if (opts.protocolVersion < 13) {
+ opts.headers['Sec-WebSocket-Origin'] = opts.origin;
+ } else {
+ opts.headers.Origin = opts.origin;
+ }
+ }
+ if (parsedUrl.username || parsedUrl.password) {
+ opts.auth = `${parsedUrl.username}:${parsedUrl.password}`;
+ }
+
+ if (isUnixSocket) {
+ const parts = opts.path.split(':');
+
+ opts.socketPath = parts[0];
+ opts.path = parts[1];
+ }
+
+ if (opts.followRedirects) {
+ if (websocket._redirects === 0) {
+ websocket._originalUnixSocket = isUnixSocket;
+ websocket._originalSecure = isSecure;
+ websocket._originalHostOrSocketPath = isUnixSocket
+ ? opts.socketPath
+ : parsedUrl.host;
+
+ const headers = options && options.headers;
+
+ //
+ // Shallow copy the user provided options so that headers can be changed
+ // without mutating the original object.
+ //
+ options = { ...options, headers: {} };
+
+ if (headers) {
+ for (const [key, value] of Object.entries(headers)) {
+ options.headers[key.toLowerCase()] = value;
+ }
+ }
+ } else {
+ const isSameHost = isUnixSocket
+ ? websocket._originalUnixSocket
+ ? opts.socketPath === websocket._originalHostOrSocketPath
+ : false
+ : websocket._originalUnixSocket
+ ? false
+ : parsedUrl.host === websocket._originalHostOrSocketPath;
+
+ if (!isSameHost || (websocket._originalSecure && !isSecure)) {
+ //
+ // Match curl 7.77.0 behavior and drop the following headers. These
+ // headers are also dropped when following a redirect to a subdomain.
+ //
+ delete opts.headers.authorization;
+ delete opts.headers.cookie;
+
+ if (!isSameHost) delete opts.headers.host;
+
+ opts.auth = undefined;
+ }
+ }
+
+ //
+ // Match curl 7.77.0 behavior and make the first `Authorization` header win.
+ // If the `Authorization` header is set, then there is nothing to do as it
+ // will take precedence.
+ //
+ if (opts.auth && !options.headers.authorization) {
+ options.headers.authorization =
+ 'Basic ' + Buffer.from(opts.auth).toString('base64');
+ }
+ }
+
+ let req = (websocket._req = get(opts));
+
+ if (opts.timeout) {
+ req.on('timeout', () => {
+ abortHandshake(websocket, req, 'Opening handshake has timed out');
+ });
+ }
+
+ req.on('error', (err) => {
+ if (req === null || req.aborted) return;
+
+ req = websocket._req = null;
+ emitErrorAndClose(websocket, err);
+ });
+
+ req.on('response', (res) => {
+ const location = res.headers.location;
+ const statusCode = res.statusCode;
+
+ if (
+ location &&
+ opts.followRedirects &&
+ statusCode >= 300 &&
+ statusCode < 400
+ ) {
+ if (++websocket._redirects > opts.maxRedirects) {
+ abortHandshake(websocket, req, 'Maximum redirects exceeded');
+ return;
+ }
+
+ req.abort();
+
+ let addr;
+
+ try {
+ addr = new URL(location, address);
+ } catch (err) {
+ emitErrorAndClose(websocket, err);
+ return;
+ }
+
+ initAsClient(websocket, addr, protocols, options);
+ } else if (!websocket.emit('unexpected-response', req, res)) {
+ abortHandshake(
+ websocket,
+ req,
+ `Unexpected server response: ${res.statusCode}`
+ );
+ }
+ });
+
+ req.on('upgrade', (res, socket, head) => {
+ websocket.emit('upgrade', res);
+
+ //
+ // The user may have closed the connection from a listener of the `upgrade`
+ // event.
+ //
+ if (websocket.readyState !== WebSocket.CONNECTING) return;
+
+ req = websocket._req = null;
+
+ if (res.headers.upgrade.toLowerCase() !== 'websocket') {
+ abortHandshake(websocket, socket, 'Invalid Upgrade header');
+ return;
+ }
+
+ const digest = createHash('sha1')
+ .update(key + GUID)
+ .digest('base64');
+
+ if (res.headers['sec-websocket-accept'] !== digest) {
+ abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header');
+ return;
+ }
+
+ const serverProt = res.headers['sec-websocket-protocol'];
+ const protList = (protocols || '').split(/, */);
+ let protError;
+
+ if (!protocols && serverProt) {
+ protError = 'Server sent a subprotocol but none was requested';
+ } else if (protocols && !serverProt) {
+ protError = 'Server sent no subprotocol';
+ } else if (serverProt && !protList.includes(serverProt)) {
+ protError = 'Server sent an invalid subprotocol';
+ }
+
+ if (protError) {
+ abortHandshake(websocket, socket, protError);
+ return;
+ }
+
+ if (serverProt) websocket._protocol = serverProt;
+
+ const secWebSocketExtensions = res.headers['sec-websocket-extensions'];
+
+ if (secWebSocketExtensions !== undefined) {
+ if (!perMessageDeflate) {
+ const message =
+ 'Server sent a Sec-WebSocket-Extensions header but no extension ' +
+ 'was requested';
+ abortHandshake(websocket, socket, message);
+ return;
+ }
+
+ let extensions;
+
+ try {
+ extensions = parse(secWebSocketExtensions);
+ } catch (err) {
+ const message = 'Invalid Sec-WebSocket-Extensions header';
+ abortHandshake(websocket, socket, message);
+ return;
+ }
+
+ const extensionNames = Object.keys(extensions);
+
+ if (extensionNames.length) {
+ if (
+ extensionNames.length !== 1 ||
+ extensionNames[0] !== PerMessageDeflate.extensionName
+ ) {
+ const message =
+ 'Server indicated an extension that was not requested';
+ abortHandshake(websocket, socket, message);
+ return;
+ }
+
+ try {
+ perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]);
+ } catch (err) {
+ const message = 'Invalid Sec-WebSocket-Extensions header';
+ abortHandshake(websocket, socket, message);
+ return;
+ }
+
+ websocket._extensions[PerMessageDeflate.extensionName] =
+ perMessageDeflate;
+ }
+ }
+
+ websocket.setSocket(socket, head, opts.maxPayload);
+ });
+}
+
+/**
+ * Emit the `'error'` and `'close'` event.
+ *
+ * @param {WebSocket} websocket The WebSocket instance
+ * @param {Error} The error to emit
+ * @private
+ */
+function emitErrorAndClose(websocket, err) {
+ websocket._readyState = WebSocket.CLOSING;
+ websocket.emit('error', err);
+ websocket.emitClose();
+}
+
+/**
+ * Create a `net.Socket` and initiate a connection.
+ *
+ * @param {Object} options Connection options
+ * @return {net.Socket} The newly created socket used to start the connection
+ * @private
+ */
+function netConnect(options) {
+ options.path = options.socketPath;
+ return net.connect(options);
+}
+
+/**
+ * Create a `tls.TLSSocket` and initiate a connection.
+ *
+ * @param {Object} options Connection options
+ * @return {tls.TLSSocket} The newly created socket used to start the connection
+ * @private
+ */
+function tlsConnect(options) {
+ options.path = undefined;
+
+ if (!options.servername && options.servername !== '') {
+ options.servername = net.isIP(options.host) ? '' : options.host;
+ }
+
+ return tls.connect(options);
+}
+
+/**
+ * Abort the handshake and emit an error.
+ *
+ * @param {WebSocket} websocket The WebSocket instance
+ * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to
+ * abort or the socket to destroy
+ * @param {String} message The error message
+ * @private
+ */
+function abortHandshake(websocket, stream, message) {
+ websocket._readyState = WebSocket.CLOSING;
+
+ const err = new Error(message);
+ Error.captureStackTrace(err, abortHandshake);
+
+ if (stream.setHeader) {
+ stream.abort();
+
+ if (stream.socket && !stream.socket.destroyed) {
+ //
+ // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if
+ // called after the request completed. See
+ // https://github.com/websockets/ws/issues/1869.
+ //
+ stream.socket.destroy();
+ }
+
+ stream.once('abort', websocket.emitClose.bind(websocket));
+ websocket.emit('error', err);
+ } else {
+ stream.destroy(err);
+ stream.once('error', websocket.emit.bind(websocket, 'error'));
+ stream.once('close', websocket.emitClose.bind(websocket));
+ }
+}
+
+/**
+ * Handle cases where the `ping()`, `pong()`, or `send()` methods are called
+ * when the `readyState` attribute is `CLOSING` or `CLOSED`.
+ *
+ * @param {WebSocket} websocket The WebSocket instance
+ * @param {*} [data] The data to send
+ * @param {Function} [cb] Callback
+ * @private
+ */
+function sendAfterClose(websocket, data, cb) {
+ if (data) {
+ const length = toBuffer(data).length;
+
+ //
+ // The `_bufferedAmount` property is used only when the peer is a client and
+ // the opening handshake fails. Under these circumstances, in fact, the
+ // `setSocket()` method is not called, so the `_socket` and `_sender`
+ // properties are set to `null`.
+ //
+ if (websocket._socket) websocket._sender._bufferedBytes += length;
+ else websocket._bufferedAmount += length;
+ }
+
+ if (cb) {
+ const err = new Error(
+ `WebSocket is not open: readyState ${websocket.readyState} ` +
+ `(${readyStates[websocket.readyState]})`
+ );
+ cb(err);
+ }
+}
+
+/**
+ * The listener of the `Receiver` `'conclude'` event.
+ *
+ * @param {Number} code The status code
+ * @param {String} reason The reason for closing
+ * @private
+ */
+function receiverOnConclude(code, reason) {
+ const websocket = this[kWebSocket];
+
+ websocket._closeFrameReceived = true;
+ websocket._closeMessage = reason;
+ websocket._closeCode = code;
+
+ if (websocket._socket[kWebSocket] === undefined) return;
+
+ websocket._socket.removeListener('data', socketOnData);
+ process.nextTick(resume, websocket._socket);
+
+ if (code === 1005) websocket.close();
+ else websocket.close(code, reason);
+}
+
+/**
+ * The listener of the `Receiver` `'drain'` event.
+ *
+ * @private
+ */
+function receiverOnDrain() {
+ this[kWebSocket]._socket.resume();
+}
+
+/**
+ * The listener of the `Receiver` `'error'` event.
+ *
+ * @param {(RangeError|Error)} err The emitted error
+ * @private
+ */
+function receiverOnError(err) {
+ const websocket = this[kWebSocket];
+
+ if (websocket._socket[kWebSocket] !== undefined) {
+ websocket._socket.removeListener('data', socketOnData);
+
+ //
+ // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See
+ // https://github.com/websockets/ws/issues/1940.
+ //
+ process.nextTick(resume, websocket._socket);
+
+ websocket.close(err[kStatusCode]);
+ }
+
+ websocket.emit('error', err);
+}
+
+/**
+ * The listener of the `Receiver` `'finish'` event.
+ *
+ * @private
+ */
+function receiverOnFinish() {
+ this[kWebSocket].emitClose();
+}
+
+/**
+ * The listener of the `Receiver` `'message'` event.
+ *
+ * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message
+ * @private
+ */
+function receiverOnMessage(data) {
+ this[kWebSocket].emit('message', data);
+}
+
+/**
+ * The listener of the `Receiver` `'ping'` event.
+ *
+ * @param {Buffer} data The data included in the ping frame
+ * @private
+ */
+function receiverOnPing(data) {
+ const websocket = this[kWebSocket];
+
+ websocket.pong(data, !websocket._isServer, NOOP);
+ websocket.emit('ping', data);
+}
+
+/**
+ * The listener of the `Receiver` `'pong'` event.
+ *
+ * @param {Buffer} data The data included in the pong frame
+ * @private
+ */
+function receiverOnPong(data) {
+ this[kWebSocket].emit('pong', data);
+}
+
+/**
+ * Resume a readable stream
+ *
+ * @param {Readable} stream The readable stream
+ * @private
+ */
+function resume(stream) {
+ stream.resume();
+}
+
+/**
+ * The listener of the `net.Socket` `'close'` event.
+ *
+ * @private
+ */
+function socketOnClose() {
+ const websocket = this[kWebSocket];
+
+ this.removeListener('close', socketOnClose);
+ this.removeListener('data', socketOnData);
+ this.removeListener('end', socketOnEnd);
+
+ websocket._readyState = WebSocket.CLOSING;
+
+ let chunk;
+
+ //
+ // The close frame might not have been received or the `'end'` event emitted,
+ // for example, if the socket was destroyed due to an error. Ensure that the
+ // `receiver` stream is closed after writing any remaining buffered data to
+ // it. If the readable side of the socket is in flowing mode then there is no
+ // buffered data as everything has been already written and `readable.read()`
+ // will return `null`. If instead, the socket is paused, any possible buffered
+ // data will be read as a single chunk.
+ //
+ if (
+ !this._readableState.endEmitted &&
+ !websocket._closeFrameReceived &&
+ !websocket._receiver._writableState.errorEmitted &&
+ (chunk = websocket._socket.read()) !== null
+ ) {
+ websocket._receiver.write(chunk);
+ }
+
+ websocket._receiver.end();
+
+ this[kWebSocket] = undefined;
+
+ clearTimeout(websocket._closeTimer);
+
+ if (
+ websocket._receiver._writableState.finished ||
+ websocket._receiver._writableState.errorEmitted
+ ) {
+ websocket.emitClose();
+ } else {
+ websocket._receiver.on('error', receiverOnFinish);
+ websocket._receiver.on('finish', receiverOnFinish);
+ }
+}
+
+/**
+ * The listener of the `net.Socket` `'data'` event.
+ *
+ * @param {Buffer} chunk A chunk of data
+ * @private
+ */
+function socketOnData(chunk) {
+ if (!this[kWebSocket]._receiver.write(chunk)) {
+ this.pause();
+ }
+}
+
+/**
+ * The listener of the `net.Socket` `'end'` event.
+ *
+ * @private
+ */
+function socketOnEnd() {
+ const websocket = this[kWebSocket];
+
+ websocket._readyState = WebSocket.CLOSING;
+ websocket._receiver.end();
+ this.end();
+}
+
+/**
+ * The listener of the `net.Socket` `'error'` event.
+ *
+ * @private
+ */
+function socketOnError() {
+ const websocket = this[kWebSocket];
+
+ this.removeListener('error', socketOnError);
+ this.on('error', NOOP);
+
+ if (websocket) {
+ websocket._readyState = WebSocket.CLOSING;
+ this.destroy();
+ }
+}
diff --git a/node_modules/ws/package.json b/node_modules/ws/package.json
new file mode 100644
index 0000000..832203f
--- /dev/null
+++ b/node_modules/ws/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "ws",
+ "version": "7.5.9",
+ "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
+ "keywords": [
+ "HyBi",
+ "Push",
+ "RFC-6455",
+ "WebSocket",
+ "WebSockets",
+ "real-time"
+ ],
+ "homepage": "https://github.com/websockets/ws",
+ "bugs": "https://github.com/websockets/ws/issues",
+ "repository": "websockets/ws",
+ "author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
+ "license": "MIT",
+ "main": "index.js",
+ "browser": "browser.js",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "files": [
+ "browser.js",
+ "index.js",
+ "lib/*.js"
+ ],
+ "scripts": {
+ "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
+ "integration": "mocha --throw-deprecation test/*.integration.js",
+ "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ },
+ "devDependencies": {
+ "benchmark": "^2.1.4",
+ "bufferutil": "^4.0.1",
+ "eslint": "^7.2.0",
+ "eslint-config-prettier": "^8.1.0",
+ "eslint-plugin-prettier": "^4.0.0",
+ "mocha": "^7.0.0",
+ "nyc": "^15.0.0",
+ "prettier": "^2.0.5",
+ "utf-8-validate": "^5.0.2"
+ }
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..7035632
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,5810 @@
+{
+ "name": "CockpitDevelopmentDependencies",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
+ "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.23.4",
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+ "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.23.9",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
+ "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
+ "dev": true,
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz",
+ "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^2.2.3"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz",
+ "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ }
+ },
+ "node_modules/@csstools/media-query-list-parser": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.7.tgz",
+ "integrity": "sha512-lHPKJDkPUECsyAvD60joYfDmp8UERYxHGkFfyLJFTVK/ERJe0sVlIFLXU5XFxdjNDTerp5L4KeaKG+Z5S94qxQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^2.5.0",
+ "@csstools/css-tokenizer": "^2.2.3"
+ }
+ },
+ "node_modules/@csstools/selector-specificity": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.1.tgz",
+ "integrity": "sha512-NPljRHkq4a14YzZ3YD406uaxh7s0g6eAq3L9aLOWywoqe8PkYamAvtsh7KNX6c++ihDrJ0RiU+/z7rGnhlZ5ww==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "postcss-selector-parser": "^6.0.13"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz",
+ "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+ "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.56.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
+ "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.14",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
+ "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.2",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
+ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
+ "dev": true
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@patternfly/patternfly": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-5.1.0.tgz",
+ "integrity": "sha512-wzVgL/0xPsmuRKWc6lMNEo5gDcTUtyU231eJSBTapOKXiwBOv2flvLEHPYLO6oDYXO+hwUrVgbcZFWMd1UlLwA=="
+ },
+ "node_modules/@patternfly/react-core": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.1.2.tgz",
+ "integrity": "sha512-MeSasp7PgkqlirlbbGuEj6j3KqXVoNkE3c3N6rfxTZOF025ullDJjtzf/L/Fiyht4tH1uNCtkdlpnea6jqTMPg==",
+ "dependencies": {
+ "@patternfly/react-icons": "^5.1.2",
+ "@patternfly/react-styles": "^5.1.2",
+ "@patternfly/react-tokens": "^5.1.2",
+ "focus-trap": "7.5.2",
+ "react-dropzone": "^14.2.3",
+ "tslib": "^2.5.0"
+ },
+ "peerDependencies": {
+ "react": "^17 || ^18",
+ "react-dom": "^17 || ^18"
+ }
+ },
+ "node_modules/@patternfly/react-icons": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.1.2.tgz",
+ "integrity": "sha512-hgf3OchvNyCcxqDrRJCkxauFdxENtVX2d6uTkMfOQWP3hs8hqYGHR5S0pe2teJ1SwAs2Rgtf7ezzmzKAouAjkw==",
+ "peerDependencies": {
+ "react": "^17 || ^18",
+ "react-dom": "^17 || ^18"
+ }
+ },
+ "node_modules/@patternfly/react-styles": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.1.2.tgz",
+ "integrity": "sha512-rGNo8MstZG2r3yDS1tWwYDctK1qWW5RT1UwKF1DrQfhZ8ruEEL6m2ZXXM0u62hmM3qq4Q8h5lgn/bVHBnOHSLA=="
+ },
+ "node_modules/@patternfly/react-table": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-5.1.2.tgz",
+ "integrity": "sha512-C+ctkW6oWmdVhGv1rawVlo54baSu5G3ja3ZDtBjVsgMmpsGD0GIBXpvwtFO+OJVeY7T6qXHInMyuW3QNz/0rog==",
+ "dependencies": {
+ "@patternfly/react-core": "^5.1.2",
+ "@patternfly/react-icons": "^5.1.2",
+ "@patternfly/react-styles": "^5.1.2",
+ "@patternfly/react-tokens": "^5.1.2",
+ "lodash": "^4.17.19",
+ "tslib": "^2.5.0"
+ },
+ "peerDependencies": {
+ "react": "^17 || ^18",
+ "react-dom": "^17 || ^18"
+ }
+ },
+ "node_modules/@patternfly/react-tokens": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.1.2.tgz",
+ "integrity": "sha512-hu/6kEEMnyDc4GiMiaEau3kYq0BZoB3X1tZLcNfg9zQZnOydUgaLcUgR8+IlMF/nVVIqNjZF2RA/5lmKAVz2cQ=="
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+ "dev": true
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "dev": true,
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.11.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
+ "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz",
+ "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
+ "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
+ "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz",
+ "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.filter": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz",
+ "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-array-method-boxes-properly": "^1.0.0",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz",
+ "integrity": "sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
+ "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
+ "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz",
+ "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.1.0",
+ "es-shim-unscopables": "^1.0.2"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
+ "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.2.1",
+ "get-intrinsic": "^1.2.3",
+ "is-array-buffer": "^3.0.4",
+ "is-shared-array-buffer": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ast-types-flow": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+ "dev": true
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/asynciterator.prototype": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz",
+ "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ }
+ },
+ "node_modules/attr-accept": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+ "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/autolinker": {
+ "version": "3.16.2",
+ "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-3.16.2.tgz",
+ "integrity": "sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz",
+ "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz",
+ "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
+ "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/builtin-modules": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
+ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/builtins": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
+ "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "semver": "^7.0.0"
+ }
+ },
+ "node_modules/builtins/node_modules/semver": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chrome-remote-interface": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.0.tgz",
+ "integrity": "sha512-tv/SgeBfShXk43fwFpQ9wnS7mOCPzETnzDXTNxCb6TqKOiOeIfbrJz+2NAp8GmzwizpKa058wnU1Te7apONaYg==",
+ "dev": true,
+ "dependencies": {
+ "commander": "2.11.x",
+ "ws": "^7.2.0"
+ },
+ "bin": {
+ "chrome-remote-interface": "bin/client.js"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/colord": {
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
+ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cosmiconfig": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
+ "dev": true,
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-functions-list": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.1.tgz",
+ "integrity": "sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12 || >=16"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
+ "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
+ "dev": true,
+ "dependencies": {
+ "mdn-data": "2.0.30",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "dev": true
+ },
+ "node_modules/date-fns": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
+ "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-equal": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
+ "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.5",
+ "es-get-iterator": "^1.1.3",
+ "get-intrinsic": "^1.2.2",
+ "is-arguments": "^1.1.1",
+ "is-array-buffer": "^3.0.2",
+ "is-date-object": "^1.0.5",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "isarray": "^2.0.5",
+ "object-is": "^1.1.5",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.5.1",
+ "side-channel": "^1.0.4",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.22.4",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.4.tgz",
+ "integrity": "sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "arraybuffer.prototype.slice": "^1.0.3",
+ "available-typed-arrays": "^1.0.6",
+ "call-bind": "^1.0.7",
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.6",
+ "get-intrinsic": "^1.2.4",
+ "get-symbol-description": "^1.0.2",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.1",
+ "internal-slot": "^1.0.7",
+ "is-array-buffer": "^3.0.4",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.13",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.13.1",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.5",
+ "regexp.prototype.flags": "^1.5.2",
+ "safe-array-concat": "^1.1.0",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.trim": "^1.2.8",
+ "string.prototype.trimend": "^1.0.7",
+ "string.prototype.trimstart": "^1.0.7",
+ "typed-array-buffer": "^1.0.1",
+ "typed-array-byte-length": "^1.0.0",
+ "typed-array-byte-offset": "^1.0.0",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-array-method-boxes-properly": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
+ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
+ "dev": true
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-get-iterator": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
+ "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "is-arguments": "^1.1.1",
+ "is-map": "^2.0.2",
+ "is-set": "^2.0.2",
+ "is-string": "^1.0.7",
+ "isarray": "^2.0.5",
+ "stop-iteration-iterator": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.0.17",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.17.tgz",
+ "integrity": "sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ==",
+ "dev": true,
+ "dependencies": {
+ "asynciterator.prototype": "^1.0.0",
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.4",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.2",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "globalthis": "^1.0.3",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.7",
+ "iterator.prototype": "^1.1.2",
+ "safe-array-concat": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
+ "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.2",
+ "has-tostringtag": "^1.0.0",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
+ "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.0"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz",
+ "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.19.11",
+ "@esbuild/android-arm": "0.19.11",
+ "@esbuild/android-arm64": "0.19.11",
+ "@esbuild/android-x64": "0.19.11",
+ "@esbuild/darwin-arm64": "0.19.11",
+ "@esbuild/darwin-x64": "0.19.11",
+ "@esbuild/freebsd-arm64": "0.19.11",
+ "@esbuild/freebsd-x64": "0.19.11",
+ "@esbuild/linux-arm": "0.19.11",
+ "@esbuild/linux-arm64": "0.19.11",
+ "@esbuild/linux-ia32": "0.19.11",
+ "@esbuild/linux-loong64": "0.19.11",
+ "@esbuild/linux-mips64el": "0.19.11",
+ "@esbuild/linux-ppc64": "0.19.11",
+ "@esbuild/linux-riscv64": "0.19.11",
+ "@esbuild/linux-s390x": "0.19.11",
+ "@esbuild/linux-x64": "0.19.11",
+ "@esbuild/netbsd-x64": "0.19.11",
+ "@esbuild/openbsd-x64": "0.19.11",
+ "@esbuild/sunos-x64": "0.19.11",
+ "@esbuild/win32-arm64": "0.19.11",
+ "@esbuild/win32-ia32": "0.19.11",
+ "@esbuild/win32-x64": "0.19.11"
+ }
+ },
+ "node_modules/esbuild-plugin-copy": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/esbuild-plugin-copy/-/esbuild-plugin-copy-2.1.1.tgz",
+ "integrity": "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "chokidar": "^3.5.3",
+ "fs-extra": "^10.0.1",
+ "globby": "^11.0.3"
+ },
+ "peerDependencies": {
+ "esbuild": ">= 0.14.0"
+ }
+ },
+ "node_modules/esbuild-plugin-replace": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/esbuild-plugin-replace/-/esbuild-plugin-replace-1.4.0.tgz",
+ "integrity": "sha512-lP3ZAyzyRa5JXoOd59lJbRKNObtK8pJ/RO7o6vdjwLi71GfbL32NR22ZuS7/cLZkr10/L1lutoLma8E4DLngYg==",
+ "dev": true,
+ "dependencies": {
+ "magic-string": "^0.25.7"
+ }
+ },
+ "node_modules/esbuild-sass-plugin": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.1.tgz",
+ "integrity": "sha512-mBB2aEF0xk7yo+Q9pSUh8xYED/1O2wbAM6IauGkDrqy6pl9SbJNakLeLGXiNpNujWIudu8TJTZCv2L5AQYRXtA==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.22.6",
+ "sass": "^1.7.3"
+ },
+ "peerDependencies": {
+ "esbuild": "^0.19.4"
+ }
+ },
+ "node_modules/esbuild-wasm": {
+ "version": "0.19.11",
+ "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.11.tgz",
+ "integrity": "sha512-MIhnpc1TxERUHomteO/ZZHp+kUawGEc03D/8vMHGzffLvbFLeDe6mwxqEZwlqBNY7SLWbyp6bBQAcCen8+wpjQ==",
+ "dev": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.56.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
+ "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.56.0",
+ "@humanwhocodes/config-array": "^0.11.13",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-compat-utils": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz",
+ "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "eslint": ">=6.0.0"
+ }
+ },
+ "node_modules/eslint-config-standard": {
+ "version": "17.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz",
+ "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^8.0.1",
+ "eslint-plugin-import": "^2.25.2",
+ "eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
+ "eslint-plugin-promise": "^6.0.0"
+ }
+ },
+ "node_modules/eslint-config-standard-jsx": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-11.0.0.tgz",
+ "integrity": "sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "peerDependencies": {
+ "eslint": "^8.8.0",
+ "eslint-plugin-react": "^7.28.0"
+ }
+ },
+ "node_modules/eslint-config-standard-react": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-standard-react/-/eslint-config-standard-react-13.0.0.tgz",
+ "integrity": "sha512-HrVPGj8UncHfV+BsdJTuJpVsomn6AIrke3Af2Fh4XFvQQDU+iO6N2ZL+UsC+scExft4fU3uf7fJwj7PKWnXJDA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "peerDependencies": {
+ "eslint": "^8.8.0",
+ "eslint-plugin-react": "^7.28.0",
+ "eslint-plugin-react-hooks": "^4.6.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
+ "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-es": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz",
+ "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==",
+ "dev": true,
+ "dependencies": {
+ "eslint-utils": "^2.0.0",
+ "regexpp": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=4.19.1"
+ }
+ },
+ "node_modules/eslint-plugin-es-x": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz",
+ "integrity": "sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.1.2",
+ "@eslint-community/regexpp": "^4.6.0",
+ "eslint-compat-utils": "^0.1.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ota-meshi"
+ },
+ "peerDependencies": {
+ "eslint": ">=8"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.29.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
+ "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.7",
+ "array.prototype.findlastindex": "^1.2.3",
+ "array.prototype.flat": "^1.3.2",
+ "array.prototype.flatmap": "^1.3.2",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.8.0",
+ "hasown": "^2.0.0",
+ "is-core-module": "^2.13.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.7",
+ "object.groupby": "^1.0.1",
+ "object.values": "^1.1.7",
+ "semver": "^6.3.1",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz",
+ "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.23.2",
+ "aria-query": "^5.3.0",
+ "array-includes": "^3.1.7",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "=4.7.0",
+ "axobject-query": "^3.2.1",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "es-iterator-helpers": "^1.0.15",
+ "hasown": "^2.0.0",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.7",
+ "object.fromentries": "^2.0.7"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-n": {
+ "version": "16.6.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz",
+ "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "builtins": "^5.0.1",
+ "eslint-plugin-es-x": "^7.5.0",
+ "get-tsconfig": "^4.7.0",
+ "globals": "^13.24.0",
+ "ignore": "^5.2.4",
+ "is-builtin-module": "^3.2.1",
+ "is-core-module": "^2.12.1",
+ "minimatch": "^3.1.2",
+ "resolve": "^1.22.2",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-n/node_modules/semver": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-plugin-node": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz",
+ "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==",
+ "dev": true,
+ "dependencies": {
+ "eslint-plugin-es": "^3.0.0",
+ "eslint-utils": "^2.0.0",
+ "ignore": "^5.1.1",
+ "minimatch": "^3.0.4",
+ "resolve": "^1.10.1",
+ "semver": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=5.16.0"
+ }
+ },
+ "node_modules/eslint-plugin-promise": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
+ "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.33.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz",
+ "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flatmap": "^1.3.1",
+ "array.prototype.tosorted": "^1.1.1",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.0.12",
+ "estraverse": "^5.3.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.6",
+ "object.fromentries": "^2.0.6",
+ "object.hasown": "^1.1.2",
+ "object.values": "^1.1.6",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.4",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
+ "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.5",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
+ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+ "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+ "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/file-selector": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
+ "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
+ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+ "dev": true
+ },
+ "node_modules/focus-trap": {
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz",
+ "integrity": "sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==",
+ "dependencies": {
+ "tabbable": "^6.2.0"
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
+ "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
+ "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "functions-have-names": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
+ "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
+ "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.7.2",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz",
+ "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/gettext-parser": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-8.0.0.tgz",
+ "integrity": "sha512-eFmhDi2xQ+2reMRY2AbJ2oa10uFOl1oyGbAKdCZiNOk94NJHi7aN0OBELSC9v35ZAPQdr+uRBi93/Gu4SlBdrA==",
+ "dev": true,
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "encoding": "^0.1.13",
+ "readable-stream": "^4.5.2",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/global-modules": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
+ "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
+ "dev": true,
+ "dependencies": {
+ "global-prefix": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
+ "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
+ "dev": true,
+ "dependencies": {
+ "ini": "^1.3.5",
+ "kind-of": "^6.0.2",
+ "which": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+ "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globalyzer": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
+ "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==",
+ "dev": true
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globjoin": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz",
+ "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
+ "dev": true
+ },
+ "node_modules/globrex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
+ "dev": true
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
+ "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/html-tags": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
+ "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/htmlparser": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/htmlparser/-/htmlparser-1.7.7.tgz",
+ "integrity": "sha512-zpK66ifkT0fauyFh2Mulrq4AqGTucxGtOhZ8OjkbSfcCpkqQEI8qRkY0tSQSJNAQ4HUZkgWaU4fK4EH6SVH9PQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.1.33"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/ignore": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+ "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
+ "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==",
+ "dev": true
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
+ "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.0",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/irregular-plurals": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz",
+ "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-arguments": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+ "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
+ "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true
+ },
+ "node_modules/is-async-function": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
+ "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-builtin-module": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
+ "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "builtin-modules": "^3.3.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+ "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
+ "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+ "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz",
+ "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz",
+ "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
+ "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
+ "dev": true,
+ "dependencies": {
+ "which-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz",
+ "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz",
+ "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
+ "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "reflect.getprototypeof": "^1.0.4",
+ "set-function-name": "^2.0.1"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
+ "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jed": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz",
+ "integrity": "sha512-z35ZSEcXHxLW4yumw0dF6L464NT36vmx3wxJw8MDpraBcWuNVgUPZgPJKcu1HekNgwlMFNqol7i/IpSbjhqwqA==",
+ "dev": true
+ },
+ "node_modules/js-sha1": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/js-sha1/-/js-sha1-0.7.0.tgz",
+ "integrity": "sha512-oQZ1Mo7440BfLSv9TX87VNEyU52pXPVG19F9PL3gTgNt0tVxlZ8F4O6yze3CLuLx28TxotxvlyepCNaaV0ZjMw=="
+ },
+ "node_modules/js-sha256": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz",
+ "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q=="
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/known-css-properties": {
+ "version": "0.29.0",
+ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz",
+ "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==",
+ "dev": true
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.22",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
+ "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+ "dev": true
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+ "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+ "dev": true,
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+ "dev": true
+ },
+ "node_modules/log-symbols": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
+ "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^5.3.0",
+ "is-unicode-supported": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols/node_modules/chalk": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+ "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+ "dev": true,
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
+ "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+ "dev": true,
+ "dependencies": {
+ "sourcemap-codec": "^1.4.8"
+ }
+ },
+ "node_modules/mathml-tag-names": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
+ "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.0.30",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
+ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
+ "dev": true
+ },
+ "node_modules/meow": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
+ "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
+ "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/node-watch": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.3.tgz",
+ "integrity": "sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-is": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
+ "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
+ "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz",
+ "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz",
+ "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.2.tgz",
+ "integrity": "sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==",
+ "dev": true,
+ "dependencies": {
+ "array.prototype.filter": "^1.0.3",
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.0.0"
+ }
+ },
+ "node_modules/object.hasown": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz",
+ "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz",
+ "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-scurry": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
+ "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^9.1.1 || ^10.0.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
+ "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
+ "dev": true,
+ "engines": {
+ "node": "14 || >=16.14"
+ }
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/plur": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz",
+ "integrity": "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==",
+ "dev": true,
+ "dependencies": {
+ "irregular-plurals": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.35",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
+ "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-media-query-parser": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
+ "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==",
+ "dev": true
+ },
+ "node_modules/postcss-resolve-nested-selector": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
+ "integrity": "sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==",
+ "dev": true
+ },
+ "node_modules/postcss-safe-parser": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.0.tgz",
+ "integrity": "sha512-ovehqRNVCpuFzbXoTb4qLtyzK3xn3t/CUBxOs8LsnQjQrShaB4lKiHoVqY8ANaC0hBMHq5QVWk77rwGklFUDrg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.31"
+ }
+ },
+ "node_modules/postcss-scss": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz",
+ "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss-scss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.29"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.15",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
+ "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
+ "dev": true,
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/qunit": {
+ "version": "2.20.0",
+ "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.20.0.tgz",
+ "integrity": "sha512-N8Fp1J55waE+QG1KwX2LOyqulZUToRrrPBqDOfYfuAMkEglFL15uwvmH1P4Tq/omQ/mGbBI8PEB3PhIfvUb+jg==",
+ "dev": true,
+ "dependencies": {
+ "commander": "7.2.0",
+ "node-watch": "0.7.3",
+ "tiny-glob": "0.2.9"
+ },
+ "bin": {
+ "qunit": "bin/qunit.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/qunit-tap": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/qunit-tap/-/qunit-tap-1.5.1.tgz",
+ "integrity": "sha512-ZEgx3kF/DN5WYX3qkR7Kgq/+vtnttaHkX69GFqZ3EEKyXsQ3mYDReWjyetN3jtBEl3ZvQ7DhW6rLq/9sJzoUHQ==",
+ "dev": true,
+ "engines": [
+ "node >=0.1.96",
+ "spidermonkey",
+ "rhino",
+ "phantomjs"
+ ]
+ },
+ "node_modules/qunit/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "node_modules/react-dropzone": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
+ "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
+ "dependencies": {
+ "attr-accept": "^2.2.2",
+ "file-selector": "^0.6.0",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">= 10.13"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8 || 18.0.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "node_modules/readable-stream": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
+ "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
+ "dev": true,
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz",
+ "integrity": "sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.0.0",
+ "get-intrinsic": "^1.2.3",
+ "globalthis": "^1.0.3",
+ "which-builtin-type": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "dev": true
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
+ "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "set-function-name": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/remarkable": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-2.0.1.tgz",
+ "integrity": "sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==",
+ "dependencies": {
+ "argparse": "^1.0.10",
+ "autolinker": "^3.11.0"
+ },
+ "bin": {
+ "remarkable": "bin/remarkable.js"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/remarkable/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.8",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+ "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz",
+ "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "get-intrinsic": "^1.2.2",
+ "has-symbols": "^1.0.3",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
+ "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.1.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true
+ },
+ "node_modules/sass": {
+ "version": "1.70.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.70.0.tgz",
+ "integrity": "sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
+ "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
+ "dependencies": {
+ "define-data-property": "^1.1.2",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.3",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
+ "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
+ "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sizzle": {
+ "version": "2.3.10",
+ "resolved": "https://registry.npmjs.org/sizzle/-/sizzle-2.3.10.tgz",
+ "integrity": "sha512-kPGev+SiByuzi/YPDTqCwdKLWCaN9+14ve86yH0gP6Efue04xjLYWJrcLC6y1buFyIVXkwHNXPsOTEd1MYVPbQ==",
+ "dev": true
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "deprecated": "Please use @jridgewell/sourcemap-codec instead",
+ "dev": true
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
+ "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==",
+ "dependencies": {
+ "internal-slot": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz",
+ "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "regexp.prototype.flags": "^1.5.0",
+ "set-function-name": "^2.0.0",
+ "side-channel": "^1.0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
+ "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
+ "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
+ "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stylelint": {
+ "version": "16.2.1",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.2.1.tgz",
+ "integrity": "sha512-SfIMGFK+4n7XVAyv50CpVfcGYWG4v41y6xG7PqOgQSY8M/PgdK0SQbjWFblxjJZlN9jNq879mB4BCZHJRIJ1hA==",
+ "dev": true,
+ "dependencies": {
+ "@csstools/css-parser-algorithms": "^2.5.0",
+ "@csstools/css-tokenizer": "^2.2.3",
+ "@csstools/media-query-list-parser": "^2.1.7",
+ "@csstools/selector-specificity": "^3.0.1",
+ "balanced-match": "^2.0.0",
+ "colord": "^2.9.3",
+ "cosmiconfig": "^9.0.0",
+ "css-functions-list": "^3.2.1",
+ "css-tree": "^2.3.1",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "fastest-levenshtein": "^1.0.16",
+ "file-entry-cache": "^8.0.0",
+ "global-modules": "^2.0.0",
+ "globby": "^11.1.0",
+ "globjoin": "^0.1.4",
+ "html-tags": "^3.3.1",
+ "ignore": "^5.3.0",
+ "imurmurhash": "^0.1.4",
+ "is-plain-object": "^5.0.0",
+ "known-css-properties": "^0.29.0",
+ "mathml-tag-names": "^2.1.3",
+ "meow": "^13.1.0",
+ "micromatch": "^4.0.5",
+ "normalize-path": "^3.0.0",
+ "picocolors": "^1.0.0",
+ "postcss": "^8.4.33",
+ "postcss-resolve-nested-selector": "^0.1.1",
+ "postcss-safe-parser": "^7.0.0",
+ "postcss-selector-parser": "^6.0.15",
+ "postcss-value-parser": "^4.2.0",
+ "resolve-from": "^5.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^7.1.0",
+ "supports-hyperlinks": "^3.0.0",
+ "svg-tags": "^1.0.0",
+ "table": "^6.8.1",
+ "write-file-atomic": "^5.0.1"
+ },
+ "bin": {
+ "stylelint": "bin/stylelint.mjs"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ }
+ },
+ "node_modules/stylelint-config-recommended": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.0.tgz",
+ "integrity": "sha512-jSkx290CglS8StmrLp2TxAppIajzIBZKYm3IxT89Kg6fGlxbPiTiyH9PS5YUuVAFwaJLl1ikiXX0QWjI0jmgZQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.0.0"
+ }
+ },
+ "node_modules/stylelint-config-recommended-scss": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-14.0.0.tgz",
+ "integrity": "sha512-HDvpoOAQ1RpF+sPbDOT2Q2/YrBDEJDnUymmVmZ7mMCeNiFSdhRdyGEimBkz06wsN+HaFwUh249gDR+I9JR7Onw==",
+ "dev": true,
+ "dependencies": {
+ "postcss-scss": "^4.0.9",
+ "stylelint-config-recommended": "^14.0.0",
+ "stylelint-scss": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3.3",
+ "stylelint": "^16.0.2"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/stylelint-config-standard": {
+ "version": "36.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-36.0.0.tgz",
+ "integrity": "sha512-3Kjyq4d62bYFp/Aq8PMKDwlgUyPU4nacXsjDLWJdNPRUgpuxALu1KnlAHIj36cdtxViVhXexZij65yM0uNIHug==",
+ "dev": true,
+ "dependencies": {
+ "stylelint-config-recommended": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.1.0"
+ }
+ },
+ "node_modules/stylelint-config-standard-scss": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-standard-scss/-/stylelint-config-standard-scss-13.0.0.tgz",
+ "integrity": "sha512-WaLvkP689qSYUpJQPCo30TFJSSc3VzvvoWnrgp+7PpVby5o8fRUY1cZcP0sePZfjrFl9T8caGhcKg0GO34VDiQ==",
+ "dev": true,
+ "dependencies": {
+ "stylelint-config-recommended-scss": "^14.0.0",
+ "stylelint-config-standard": "^36.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3.3",
+ "stylelint": "^16.1.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/stylelint-formatter-pretty": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-formatter-pretty/-/stylelint-formatter-pretty-4.0.0.tgz",
+ "integrity": "sha512-tVuAEhvdTcLzlupqPEPhpBoszX3hB6AnI/OSqEIZOxRatHDHSlu/MaU13MUDzEPOgdoFfDzsVqhp4j2DltaIvg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "kofi",
+ "url": "https://ko-fi.com/mrcgrtz"
+ },
+ {
+ "type": "liberapay",
+ "url": "https://liberapay.com/mrcgrtz/"
+ }
+ ],
+ "dependencies": {
+ "ansi-escapes": "^6.2.0",
+ "log-symbols": "^6.0.0",
+ "picocolors": "^1.0.0",
+ "plur": "^5.1.0",
+ "string-width": "^7.0.0",
+ "supports-hyperlinks": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": ">=16.0.0"
+ }
+ },
+ "node_modules/stylelint-formatter-pretty/node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/stylelint-formatter-pretty/node_modules/emoji-regex": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
+ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+ "dev": true
+ },
+ "node_modules/stylelint-formatter-pretty/node_modules/string-width": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz",
+ "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stylelint-formatter-pretty/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/stylelint-scss": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.1.0.tgz",
+ "integrity": "sha512-kCfK8TQzthGwb4vaZniZgxRsVbCM4ZckmT1b/H5m4FU3I8Dz0id9llKsy1NMp3XXqC8+OPD4rVKtUbSxXlJb5g==",
+ "dev": true,
+ "dependencies": {
+ "known-css-properties": "^0.29.0",
+ "postcss-media-query-parser": "^0.2.3",
+ "postcss-resolve-nested-selector": "^0.1.1",
+ "postcss-selector-parser": "^6.0.15",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.0.2"
+ }
+ },
+ "node_modules/stylelint-use-logical-spec": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/stylelint-use-logical-spec/-/stylelint-use-logical-spec-5.0.1.tgz",
+ "integrity": "sha512-UfLB4LW6iG4r3cXxjxkiHQrFyhWFqt8FpNNngD+TyvgMWSokk5TYwTvBHS3atUvZhOogllTOe/PUrGE+4z84AA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "peerDependencies": {
+ "stylelint": ">=11 < 17"
+ }
+ },
+ "node_modules/stylelint/node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/stylelint/node_modules/balanced-match": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
+ "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
+ "dev": true
+ },
+ "node_modules/stylelint/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/stylelint/node_modules/brace-expansion/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/stylelint/node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/stylelint/node_modules/flat-cache": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.0.tgz",
+ "integrity": "sha512-EryKbCE/wxpxKniQlyas6PY1I9vwtF3uCBweX+N8KYTCn3Y12RTGtQAJ/bd5pl7kxUAc8v/R3Ake/N17OZiFqA==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4",
+ "rimraf": "^5.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/stylelint/node_modules/glob": {
+ "version": "10.3.10",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
+ "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^2.3.5",
+ "minimatch": "^9.0.1",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
+ "path-scurry": "^1.10.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/stylelint/node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/stylelint/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stylelint/node_modules/rimraf": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
+ "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^10.3.7"
+ },
+ "bin": {
+ "rimraf": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/stylelint/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-hyperlinks": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz",
+ "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/svg-tags": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
+ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
+ "dev": true
+ },
+ "node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
+ },
+ "node_modules/table": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
+ "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/table/node_modules/ajv": {
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/table/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/throttle-debounce": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
+ "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==",
+ "engines": {
+ "node": ">=12.22"
+ }
+ },
+ "node_modules/tiny-glob": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
+ "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
+ "dev": true,
+ "dependencies": {
+ "globalyzer": "0.1.0",
+ "globrex": "^0.1.2"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz",
+ "integrity": "sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
+ "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
+ "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "has-proto": "^1.0.1",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+ "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
+ "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
+ "dev": true,
+ "dependencies": {
+ "function.prototype.name": "^1.1.5",
+ "has-tostringtag": "^1.0.0",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.0.5",
+ "is-finalizationregistry": "^1.0.2",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.1.4",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz",
+ "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==",
+ "dependencies": {
+ "is-map": "^2.0.1",
+ "is-set": "^2.0.1",
+ "is-weakmap": "^2.0.1",
+ "is-weakset": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz",
+ "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.6",
+ "call-bind": "^1.0.5",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "node_modules/write-file-atomic": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
+ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "7.5.9",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
+ "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xterm": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
+ "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="
+ },
+ "node_modules/xterm-addon-canvas": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/xterm-addon-canvas/-/xterm-addon-canvas-0.5.0.tgz",
+ "integrity": "sha512-QOo/eZCMrCleAgMimfdbaZCgmQRWOml63Ued6RwQ+UTPvQj3Av9QKx3xksmyYrDGRO/AVRXa9oNuzlYvLdmoLQ==",
+ "peerDependencies": {
+ "xterm": "^5.0.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..933871d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "CockpitDevelopmentDependencies",
+ "description": "Cockpit isn't a Node package, these are devel time deps, not needed to build tarball either",
+ "type": "module",
+ "private": true,
+ "dependencies": {
+ "@patternfly/patternfly": "5.1.0",
+ "@patternfly/react-core": "5.1.2",
+ "@patternfly/react-icons": "5.1.2",
+ "@patternfly/react-styles": "5.1.2",
+ "@patternfly/react-table": "5.1.2",
+ "@patternfly/react-tokens": "5.1.2",
+ "deep-equal": "2.2.3",
+ "date-fns": "3.3.1",
+ "js-sha1": "0.7.0",
+ "js-sha256": "0.11.0",
+ "json-stable-stringify-without-jsonify": "1.0.1",
+ "prop-types": "15.8.1",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "remarkable": "2.0.1",
+ "throttle-debounce": "5.0.0",
+ "uuid": "9.0.1",
+ "xterm": "5.3.0",
+ "xterm-addon-canvas": "0.5.0"
+ },
+ "devDependencies": {
+ "argparse": "2.0.1",
+ "chrome-remote-interface": "0.33.0",
+ "esbuild": "0.19.11",
+ "esbuild-plugin-copy": "2.1.1",
+ "esbuild-plugin-replace": "1.4.0",
+ "esbuild-sass-plugin": "2.16.1",
+ "esbuild-wasm": "0.19.11",
+ "eslint": "8.56.0",
+ "eslint-config-standard": "17.1.0",
+ "eslint-config-standard-jsx": "11.0.0",
+ "eslint-config-standard-react": "13.0.0",
+ "eslint-plugin-import": "2.29.1",
+ "eslint-plugin-jsx-a11y": "6.8.0",
+ "eslint-plugin-node": "11.1.0",
+ "eslint-plugin-promise": "6.1.1",
+ "eslint-plugin-react": "7.33.2",
+ "eslint-plugin-react-hooks": "4.6.0",
+ "gettext-parser": "8.0.0",
+ "htmlparser": "1.7.7",
+ "jed": "1.1.1",
+ "qunit": "2.20.0",
+ "qunit-tap": "1.5.1",
+ "sass": "1.70.0",
+ "sizzle": "2.3.10",
+ "stylelint": "16.2.1",
+ "stylelint-config-standard": "36.0.0",
+ "stylelint-config-standard-scss": "13.0.0",
+ "stylelint-formatter-pretty": "4.0.0",
+ "stylelint-use-logical-spec": "5.0.1"
+ },
+ "scripts": {
+ "eslint": "eslint --ext .js --ext .jsx pkg/ test/common/",
+ "eslint:fix": "eslint --fix --ext .js --ext .jsx pkg/ test/common/",
+ "stylelint": "stylelint pkg/*/*{.css,scss}",
+ "stylelint:fix": "stylelint --fix pkg/*/*{.css,scss}"
+ }
+}
diff --git a/pkg/Makefile.am b/pkg/Makefile.am
new file mode 100644
index 0000000..fd0bb67
--- /dev/null
+++ b/pkg/Makefile.am
@@ -0,0 +1,56 @@
+pkg_TESTS = \
+ pkg/users/test-list-public-keys.sh \
+ $(NULL)
+
+TESTS += $(pkg_TESTS)
+
+pixmapsdir = ${datarootdir}/pixmaps
+pixmaps_DATA = pkg/sosreport/cockpit-sosreport.png
+
+EXTRA_DIST += \
+ pkg/users/mock \
+ pkg/lib/qunit-template.html.in \
+ pkg/lib/cockpit-po-plugin.js \
+ $(pkg_TESTS) \
+ $(metainfo_DATA) \
+ $(pixmaps_DATA) \
+ $(NULL)
+
+if ENABLE_PCP
+pcpmanifestdir = $(datadir)/cockpit/pcp
+dist_pcpmanifest_DATA = pkg/pcp/manifest.json
+endif
+
+# one built file in dist/ which we use as dependency
+DIST_STAMP = $(srcdir)/dist/static/manifest.json
+
+# dynamic pkg → dist dependency, to rebuild the bundles if any web related file changes
+# exclude automake unit test log files
+PKG_INPUTS = $(shell find $(srcdir)/pkg -type f ! -name 'test*.trs' ! -name 'test*.log')
+
+V_BUNDLE = $(V_BUNDLE_$(V))
+V_BUNDLE_ = $(V_BUNDLE_$(AM_DEFAULT_VERBOSITY))
+V_BUNDLE_0 = @echo " BUNDLE dist";
+
+# delete the stamp first; neither webpack nor esbuild touch it if the contents didn't change,
+# but this is just a representative for all of dist/*
+$(DIST_STAMP): $(srcdir)/package-lock.json $(PKG_INPUTS)
+ @rm -f $(DIST_STAMP)
+ $(V_BUNDLE) cd $(srcdir) && NODE_ENV='$(NODE_ENV)' tools/termschutz ./build.js
+
+EXTRA_DIST += build.js files.js package.json package-lock.json
+
+# This is how the qunit tests get included. We need to prevent automake from
+# seeing them during ./autogen.sh, but need make to find them at compile time.
+# We don't run them in the pybridge case since they're part of `pytest`.
+if WITH_OLD_BRIDGE
+-include $(wildcard pkg/Makefile.qunit*)
+endif
+
+INSTALL_DATA_LOCAL_TARGETS += install-bundles
+install-bundles:
+ cd $(srcdir)/dist; find */* -type f -exec install -D -m 644 '{}' '$(abspath $(DESTDIR)$(datadir))/cockpit/{}' \;
+
+UNINSTALL_LOCAL_TARGETS += uninstall-bundles
+uninstall-bundles:
+ rm -rf $(DESTDIR)$(datadir)/cockpit
diff --git a/pkg/Makefile.qunit b/pkg/Makefile.qunit
new file mode 100644
index 0000000..794c571
--- /dev/null
+++ b/pkg/Makefile.qunit
@@ -0,0 +1,2 @@
+# We need to avoid automake seeing this, but want it evaluated at runtime
+TESTS += $(wildcard $(srcdir)/qunit/*/test-*.html)
diff --git a/pkg/apps/application-list.jsx b/pkg/apps/application-list.jsx
new file mode 100644
index 0000000..35ee253
--- /dev/null
+++ b/pkg/apps/application-list.jsx
@@ -0,0 +1,221 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React, { useState } from "react";
+import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DataList, DataListAction, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+
+import { RebootingIcon } from "@patternfly/react-icons";
+
+import { check_uninstalled_packages } from "packagekit.js";
+import * as PackageKit from "./packagekit.js";
+import { read_os_release } from "os-release.js";
+import { icon_url, show_error, launch, ProgressBar, CancelButton } from "./utils.jsx";
+import { ActionButton } from "./application.jsx";
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { useInit } from "../lib/hooks.js";
+
+const _ = cockpit.gettext;
+
+const ApplicationRow = ({ comp, progress, progress_title, action }) => {
+ const [error, setError] = useState();
+
+ const name = (
+ <Button variant="link"
+ isInline id={comp.name}
+ onClick={() => comp.installed ? launch(comp) : cockpit.location.go(comp.id)}>
+ {comp.name}
+ </Button>);
+
+ let summary_or_progress;
+ if (progress) {
+ summary_or_progress = (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <span id={comp.name + "-progress"} className="progress-title-span">{progress_title}</span>
+ <ProgressBar title={progress_title} data={progress} ariaLabelledBy={comp.name + "-progress"} />
+ </Flex>);
+ } else {
+ if (error) {
+ summary_or_progress = (
+ <div>
+ {comp.summary}
+ <Alert isInline variant='danger'
+ actionClose={<AlertActionCloseButton onClose={() => setError(null)} />}
+ title={error} />
+ </div>
+ );
+ } else {
+ summary_or_progress = comp.summary;
+ }
+ }
+
+ return (
+ <DataListItem className="app-list" aria-labelledby={comp.name}>
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell isIcon key="icon">
+ <img src={icon_url(comp.icon)} role="presentation" alt="" />
+ </DataListCell>,
+ <DataListCell width={1} key="app name">
+ {name}
+ </DataListCell>,
+ <DataListCell width={4} key="secondary content">
+ {summary_or_progress}
+ </DataListCell>,
+ ]}
+ />
+ <DataListAction aria-labelledby={comp.name} aria-label={_("Actions")}>
+ <ActionButton comp={comp} progress={progress} action={action} />
+ </DataListAction>
+ </DataListItemRow>
+ </DataListItem>
+ );
+};
+
+export const ApplicationList = ({ metainfo_db, appProgress, appProgressTitle, action }) => {
+ const [progress, setProgress] = useState(false);
+ const [dataPackagesInstalled, setDataPackagesInstalled] = useState(null);
+ const comps = [];
+ for (const id in metainfo_db.components)
+ comps.push(metainfo_db.components[id]);
+ comps.sort((a, b) => a.name.localeCompare(b.name));
+
+ function get_config(name, os_release, def) {
+ // ID is a single value, ID_LIKE is a list
+ const os_list = [os_release?.ID || "", ...(os_release?.ID_LIKE || "").split(/\s+/)];
+
+ if (cockpit.manifests.apps && cockpit.manifests.apps.config) {
+ const val = cockpit.manifests.apps.config[name];
+ if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
+ for (const os of os_list) {
+ if (val[os])
+ return val[os];
+ }
+ return def;
+ }
+ return val !== undefined ? val : def;
+ } else {
+ return def;
+ }
+ }
+
+ async function check_missing_data(packages) {
+ try {
+ const missing = await check_uninstalled_packages(packages);
+ setDataPackagesInstalled(missing.size === 0);
+ } catch (e) {
+ console.warn("Failed to check missing AppStream metadata packages:", e.toString());
+ }
+ }
+
+ useInit(async () => {
+ const os_release = await read_os_release();
+ const configPackages = get_config('appstream_config_packages', os_release, []);
+ const dataPackages = get_config('appstream_data_packages', os_release, []);
+ await check_missing_data([...dataPackages, ...configPackages]);
+ });
+
+ function refresh() {
+ read_os_release().then(os_release => {
+ const configPackages = get_config('appstream_config_packages', os_release, []);
+ const dataPackages = get_config('appstream_data_packages', os_release, []);
+ PackageKit.refresh(metainfo_db.origin_files,
+ configPackages,
+ dataPackages,
+ setProgress)
+ .finally(async () => {
+ await check_missing_data([...dataPackages, ...configPackages]);
+ setProgress(false);
+ }).catch(show_error);
+ });
+ }
+
+ let refresh_progress, refresh_button, tbody;
+ if (progress) {
+ refresh_progress = <ProgressBar id="refresh-progress" size="sm" title={_("Checking for new applications")} data={progress} />;
+ refresh_button = <CancelButton data={progress} />;
+ } else {
+ refresh_progress = null;
+ refresh_button = (
+ <Button variant="secondary" onClick={refresh} id="refresh" aria-label={ _("Update package information") }>
+ <RebootingIcon />
+ </Button>
+ );
+ }
+
+ if (comps.length) {
+ tbody = comps.map(c => <ApplicationRow comp={c} key={c.id}
+ progress={appProgress[c.id]}
+ progress_title={appProgressTitle[c.id]}
+ action={action} />);
+ }
+
+ const data_missing_msg = (dataPackagesInstalled == false && !refresh_progress)
+ ? _("Application information is missing")
+ : null;
+
+ return (
+ <Page id="list-page" data-packages-checked={dataPackagesInstalled !== null}>
+ <PageSection variant={PageSectionVariants.light}>
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <h2 className="pf-v5-u-font-size-3xl">{_("Applications")}</h2>
+ <FlexItem align={{ default: 'alignRight' }}>
+ <Flex alignItems={{ default: 'alignItemsCenter' }} spacer={{ default: 'spacerXs' }}>
+ <FlexItem>
+ {refresh_progress}
+ </FlexItem>
+ <FlexItem>
+ {refresh_button}
+ </FlexItem>
+ </Flex>
+ </FlexItem>
+ </Flex>
+ </PageSection>
+ {comps.length == 0
+ ? <EmptyStatePanel title={ _("No applications installed or available.") }
+ paragraph={data_missing_msg}
+ action={ data_missing_msg && _("Install application information")} onAction={refresh} />
+ : <PageSection>
+ <Stack hasGutter>
+ {!progress && data_missing_msg &&
+ <StackItem key="missing-meta-alert">
+ <Alert variant="warning" isInline title={data_missing_msg}
+ actionLinks={ <AlertActionLink onClick={refresh}>{_("Install")}</AlertActionLink>} />
+ </StackItem>
+ }
+ <StackItem>
+ <Card>
+ <DataList aria-label={_("Applications list")}>
+ { tbody }
+ </DataList>
+ </Card>
+ </StackItem>
+ </Stack>
+ </PageSection>
+ }
+ </Page>
+ );
+};
diff --git a/pkg/apps/application.jsx b/pkg/apps/application.jsx
new file mode 100644
index 0000000..6d11e85
--- /dev/null
+++ b/pkg/apps/application.jsx
@@ -0,0 +1,155 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { ExternalLinkAltIcon } from '@patternfly/react-icons';
+
+import * as PackageKit from "./packagekit.js";
+
+import { icon_url, launch, ProgressBar, CancelButton } from "./utils.jsx";
+
+import "./application.scss";
+
+const _ = cockpit.gettext;
+
+export const ActionButton = ({ comp, progress, action }) => {
+ function install(comp) {
+ action(PackageKit.install, comp.pkgname, _("Installing"), comp.id);
+ }
+
+ function remove(comp) {
+ action(PackageKit.remove, comp.file, _("Removing"), comp.id);
+ }
+
+ if (progress) {
+ return <CancelButton data={progress} />;
+ } else if (comp.installed) {
+ return <Button variant="danger" onClick={() => remove(comp)}>{_("Remove")}</Button>;
+ } else {
+ return <Button variant="secondary" onClick={() => install(comp)}>{_("Install")}</Button>;
+ }
+};
+
+export const Application = ({ metainfo_db, id, progress, progress_title, action }) => {
+ if (!id)
+ return null;
+
+ const comp = metainfo_db.components[id];
+
+ function render_homepage_link(urls) {
+ return urls.map((url, index) => {
+ if (url.type == 'homepage') {
+ return (
+ <Button isInline variant="link" component='a' href={url.link}
+ key={"project-url-" + index}
+ target="_blank" rel="noopener noreferrer"
+ icon={<ExternalLinkAltIcon />}
+ iconPosition="right">
+ {_("View project website")}
+ </Button>
+ );
+ } else {
+ return null;
+ }
+ });
+ }
+
+ // Render a description in the form returned by the AppsSream
+ // parser, which is a list of paragraphs and lists.
+
+ function render_description(description) {
+ if (!description)
+ return <p>{_("No description provided.")}</p>;
+
+ return description.map((paragraph, index) => {
+ if (paragraph.tag == 'ul') {
+ return <ul key={`paragraph-${index}`}>{paragraph.items.map(item => <li key={item}>{item}</li>)}</ul>;
+ } else if (paragraph.tag == 'ol') {
+ return <ol key={`paragraph-${index}`}>{paragraph.items.map(item => <li key={item}>{item}</li>)}</ol>;
+ } else {
+ return <p key={`paragraph-${index}`}>{paragraph}</p>;
+ }
+ });
+ }
+
+ // Render the icon, name, homepage link, summary, description, and screenshots of the component,
+ // plus the UI for installing and removing it.
+
+ function render_comp() {
+ if (!comp)
+ return <div>{_("Unknown application")}</div>;
+
+ let progress_or_launch;
+ if (progress) {
+ progress_or_launch = <ProgressBar title={progress_title} data={progress} />;
+ } else if (comp.installed) {
+ progress_or_launch = <Button variant="link" onClick={() => launch(comp)}>{_("Go to application")}</Button>;
+ } else {
+ progress_or_launch = null;
+ }
+
+ return (
+ <Card>
+ <CardHeader actions={{
+ actions: <>{progress_or_launch}<ActionButton comp={comp} progress={progress} action={action} /></>,
+ }}>
+ <CardTitle>
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <img src={icon_url(comp.icon)} role="presentation" alt="" />
+ <span>{comp.summary}</span>
+ </Flex>
+ </CardTitle>
+ </CardHeader>
+ <CardBody>
+ <Stack hasGutter>
+ {render_homepage_link(comp.urls)}
+ <div className="app-description">{render_description(comp.description)}</div>
+ {comp.screenshots.length
+ ? <div className="text-center">
+ { comp.screenshots.map((s, index) => <img key={`comp-${index}`} className="app-screenshot" role="presentation" alt="" src={s.full} />) }
+ </div>
+ : null}
+ </Stack>
+ </CardBody>
+ </Card>
+ );
+ }
+
+ return (
+ <Page id="app-page"
+ className="application-details">
+ <PageBreadcrumb stickyOnBreakpoint={{ default: "top" }}>
+ <Breadcrumb>
+ <BreadcrumbItem to="#/">{_("Applications")}</BreadcrumbItem>
+ <BreadcrumbItem isActive>{comp ? comp.name : id}</BreadcrumbItem>
+ </Breadcrumb>
+ </PageBreadcrumb>
+ <PageSection>
+ {render_comp()}
+ </PageSection>
+ </Page>
+ );
+};
diff --git a/pkg/apps/application.scss b/pkg/apps/application.scss
new file mode 100644
index 0000000..4a58014
--- /dev/null
+++ b/pkg/apps/application.scss
@@ -0,0 +1,45 @@
+@use "page";
+@use "../../node_modules/@patternfly/patternfly/utilities/Text/text.css";
+
+/* Application list */
+
+.app-list img {
+ inline-size: 32px;
+ block-size: 32px;
+ max-inline-size: unset;
+}
+
+.app-list .progress-title {
+ margin-inline-end: 10px;
+}
+
+.app-list .pf-v5-c-data-list__item-action button {
+ min-inline-size: 6rem;
+}
+
+.app-list .pf-v5-c-data-list__item-content {
+ /* Vertically align application info to the center */
+ align-items: center;
+}
+
+.progress-bar {
+ min-inline-size: 12rem;
+ inline-size: 20%;
+ overflow: hidden;
+}
+
+/* Application */
+
+.application-details img {
+ inline-size: 32px;
+ block-size: 32px;
+}
+
+.application-details .progress-title {
+ float: inline-start;
+ margin-inline-end: 10px;
+}
+
+.app-screenshot {
+ margin: 10px;
+}
diff --git a/pkg/apps/apps.jsx b/pkg/apps/apps.jsx
new file mode 100644
index 0000000..873e006
--- /dev/null
+++ b/pkg/apps/apps.jsx
@@ -0,0 +1,78 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import "polyfills";
+import 'cockpit-dark-theme'; // once per page
+
+import cockpit from "cockpit";
+
+import React, { useState } from "react";
+import { createRoot } from 'react-dom/client';
+
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { ApplicationList } from "./application-list.jsx";
+import { Application } from "./application.jsx";
+import { get_metainfo_db } from "./appstream.js";
+import { usePageLocation, useObject, useEvent } from "hooks";
+import { show_error } from "./utils.jsx";
+
+const App = () => {
+ const [progress, setProgress] = useState({});
+ const [progressTitle, setProgressTitle] = useState({});
+
+ function action(func, arg, progress_title, id) {
+ setProgressTitle({ ...progressTitle, [id]: progress_title });
+ func(arg, progress => setProgress({ ...progress, [id]: progress }))
+ .finally(() => setProgress({ ...progress, [id]: null }))
+ .catch(show_error);
+ }
+
+ const { path } = usePageLocation();
+
+ const metainfo_db = useObject(get_metainfo_db, null, []);
+ useEvent(metainfo_db, "changed");
+
+ if (!metainfo_db.ready)
+ return <EmptyStatePanel loading />;
+
+ if (path.length === 0) {
+ return <ApplicationList metainfo_db={metainfo_db}
+ action={action}
+ appProgress={progress}
+ appProgressTitle={progressTitle} />;
+ } else if (path.length == 1) {
+ const id = path[0];
+ return <Application metainfo_db={metainfo_db}
+ action={action}
+ progress={progress[id]}
+ progressTitle={progressTitle[id]}
+ id={id} />;
+ } else { /* redirect */
+ console.warn("not a apps location: " + path);
+ cockpit.location = '';
+ }
+};
+
+function init() {
+ const root = createRoot(document.getElementById("apps-page"));
+ root.render(<App />);
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/pkg/apps/appstream.js b/pkg/apps/appstream.js
new file mode 100644
index 0000000..ad02320
--- /dev/null
+++ b/pkg/apps/appstream.js
@@ -0,0 +1,58 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import * as python from "python.js";
+import inotify_py from "inotify.py";
+import watch_appstream_py from "./watch-appstream.py";
+
+let metainfo_db = null;
+
+export function get_metainfo_db() {
+ if (!metainfo_db) {
+ metainfo_db = cockpit.event_target({
+ ready: false,
+ components: [],
+ origin_files: []
+ });
+
+ let buf = "";
+ python.spawn([inotify_py, watch_appstream_py], [],
+ { environ: ["LANGUAGE=" + (cockpit.language || "en")] })
+ .stream(function (data) {
+ buf += data;
+ const lines = buf.split("\n");
+ buf = lines[lines.length - 1];
+ if (lines.length >= 2) {
+ const metadata = JSON.parse(lines[lines.length - 2]);
+ metainfo_db.components = metadata.components;
+ metainfo_db.origin_files = metadata.origin_files;
+ metainfo_db.ready = true;
+ metainfo_db.dispatchEvent("changed");
+ }
+ })
+ .fail(function (error) {
+ if (error != "closed") {
+ console.warn(error);
+ }
+ });
+ }
+
+ return metainfo_db;
+}
diff --git a/pkg/apps/content-security-policy.override b/pkg/apps/content-security-policy.override
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pkg/apps/content-security-policy.override
diff --git a/pkg/apps/default.png b/pkg/apps/default.png
new file mode 100644
index 0000000..fa28647
--- /dev/null
+++ b/pkg/apps/default.png
Binary files differ
diff --git a/pkg/apps/index.html b/pkg/apps/index.html
new file mode 100644
index 0000000..2c14ed3
--- /dev/null
+++ b/pkg/apps/index.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2017 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html id="applications-page">
+ <head>
+ <title translate="yes">Applications</title>
+ <meta charset="utf-8" />
+ <link href="apps.css" type="text/css" rel="stylesheet" />
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script type="text/javascript" src="../manifests.js"></script>
+ <script type="text/javascript" src="apps.js"></script>
+ </head>
+ <body class="pf-v5-m-tabular-nums">
+
+ <div class="ct-page-fill" id="apps-page">
+ </div>
+
+ </body>
+</html>
diff --git a/pkg/apps/manifest.json b/pkg/apps/manifest.json
new file mode 100644
index 0000000..56039e5
--- /dev/null
+++ b/pkg/apps/manifest.json
@@ -0,0 +1,23 @@
+{
+ "tools": {
+ "index": {
+ "label": "Applications",
+ "keywords": [
+ {
+ "matches": ["plugin", "apps", "addon", "add-on", "install", "extension"]
+ }
+ ]
+ }
+ },
+
+ "content-security-policy": "img-src *",
+
+ "config": {
+ "appstream_config_packages": {
+ "debian": ["appstream"]
+ },
+ "appstream_data_packages": {
+ "fedora": ["appstream-data"]
+ }
+ }
+}
diff --git a/pkg/apps/packagekit.js b/pkg/apps/packagekit.js
new file mode 100644
index 0000000..059233c
--- /dev/null
+++ b/pkg/apps/packagekit.js
@@ -0,0 +1,187 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import * as PK from "packagekit.js";
+
+class ProgressReporter {
+ constructor(base, range, callback) {
+ this.base = base;
+ this.range = range;
+ this.callback = callback;
+ this.progress_reporter = this.progress_reporter.bind(this);
+ }
+
+ progress_reporter(data) {
+ if (data.absolute_percentage >= 0) {
+ const newPercentage = this.base + data.absolute_percentage / 100 * this.range;
+ // PackageKit with Apt backend reports wrong percentages https://github.com/PackageKit/PackageKit/issues/516
+ // Double check here that we have an increasing only progress value
+ if (this.percentage == undefined || newPercentage >= this.percentage)
+ this.percentage = newPercentage;
+ }
+ this.callback({ percentage: this.percentage, ...data });
+ }
+}
+
+function resolve_many(method, filter, names, progress_cb) {
+ const ids = [];
+
+ return PK.cancellableTransaction(method, [filter, names], progress_cb,
+ {
+ Package: (info, package_id) => ids.push(package_id),
+ })
+ .then(() => ids);
+}
+
+function resolve(method, filter, name, progress_cb) {
+ return resolve_many(method, filter, [name], progress_cb)
+ .then(function (ids) {
+ if (ids.length === 0)
+ return Promise.reject(new PK.TransactionError("not-found", "Can't resolve package"));
+ else
+ return ids[0];
+ });
+}
+
+function reload_bridge_packages() {
+ return cockpit.dbus(null, { bus: "internal" }).call("/packages", "cockpit.Packages", "Reload", []);
+}
+
+export function install(name, progress_cb) {
+ const progress = new ProgressReporter(0, 1, progress_cb);
+
+ return resolve("Resolve", PK.Enum.FILTER_ARCH | PK.Enum.FILTER_NOT_SOURCE | PK.Enum.FILTER_NEWEST, name,
+ progress.progress_reporter)
+ .then(pkgid => {
+ progress.base = 1;
+ progress.range = 99;
+
+ return PK.cancellableTransaction("InstallPackages", [0, [pkgid]], progress.progress_reporter)
+ .then(reload_bridge_packages);
+ });
+}
+
+export function remove(name, progress_cb) {
+ const progress = new ProgressReporter(0, 1, progress_cb);
+
+ return resolve("SearchFiles", PK.Enum.FILTER_INSTALLED, name, progress.progress_reporter)
+ .then(pkgid => {
+ progress.base = 1;
+ progress.range = 99;
+
+ return PK.cancellableTransaction("RemovePackages", [0, [pkgid], true, false], progress.progress_reporter)
+ .then(reload_bridge_packages);
+ });
+}
+
+export function refresh(origin_files, config_packages, data_packages, progress_cb) {
+ const origin_pkgs = { };
+ const update_ids = [];
+
+ /* In addition to refreshing the repository metadata, we also
+ * update all packages that contain AppStream collection metadata.
+ *
+ * AppStream collection metadata is arguably part of the
+ * repository metadata and should be updated during a regular
+ * refresh of the repositories. On some distributions this is
+ * what happens, but on others (such as Fedora), the collection
+ * metadata is delivered in packages. We find them and update
+ * them explicitly.
+ *
+ * Also, we have two explicit lists of packages, and we make sure
+ * that they are installed. The first list contains packages that
+ * configure the system to retrieve AppStream data as part of
+ * repository metadata, and the second list contains packages that
+ * contain AppStream data themselves.
+ */
+ const progress = new ProgressReporter(0, 1, progress_cb);
+
+ const search_origin_file_packages = () => {
+ return PK.cancellableTransaction("SearchFiles", [PK.Enum.FILTER_INSTALLED, origin_files],
+ progress.progress_reporter,
+ {
+ Package: (info, package_id) => {
+ const pkg = package_id.split(";")[0];
+ origin_pkgs[pkg] = true;
+ },
+ });
+ };
+
+ const refresh_cache = () => {
+ progress.base = 6;
+ progress.range = 69;
+
+ return PK.cancellableTransaction("RefreshCache", [true], progress.progress_reporter);
+ };
+
+ const maybe_update_origin_file_packages = () => {
+ progress.base = 75;
+ progress.range = 5;
+
+ return PK.cancellableTransaction("GetUpdates", [0], progress.progress_reporter,
+ {
+ Package: (info, package_id) => {
+ const pkg = package_id.split(";")[0];
+ if (pkg in origin_pkgs)
+ update_ids.push(package_id);
+ },
+ })
+ .then(() => {
+ progress.base = 80;
+ progress.range = 15;
+
+ if (update_ids.length > 0)
+ return PK.cancellableTransaction("UpdatePackages", [0, update_ids],
+ progress.progress_reporter);
+ });
+ };
+
+ const ensure_packages = (pkgs, start_progress) => {
+ if (pkgs.length > 0) {
+ progress.base = start_progress;
+ progress.range = 1;
+
+ return resolve_many("Resolve",
+ PK.Enum.FILTER_ARCH | PK.Enum.FILTER_NOT_SOURCE | PK.Enum.FILTER_NEWEST | PK.Enum.FILTER_NOT_INSTALLED,
+ pkgs, progress.progress_reporter)
+ .then(ids => {
+ if (ids.length > 0) {
+ progress.base = start_progress + 1;
+ progress.range = 4;
+
+ return PK.cancellableTransaction("InstallPackages", [0, ids],
+ progress.progress_reporter)
+ .catch(ex => {
+ if (ex.code != PK.Enum.ERROR_ALREADY_INSTALLED)
+ return Promise.reject(ex);
+ });
+ }
+ });
+ } else {
+ return Promise.resolve();
+ }
+ };
+
+ return ensure_packages(config_packages, 0)
+ .then(search_origin_file_packages)
+ .then(refresh_cache)
+ .then(maybe_update_origin_file_packages)
+ .then(() => ensure_packages(data_packages, 95));
+}
diff --git a/pkg/apps/utils.jsx b/pkg/apps/utils.jsx
new file mode 100644
index 0000000..53cef78
--- /dev/null
+++ b/pkg/apps/utils.jsx
@@ -0,0 +1,99 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Progress, ProgressMeasureLocation } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
+import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export function icon_url(path_or_url) {
+ if (!path_or_url)
+ return "default.png";
+
+ if (path_or_url[0] != '/')
+ return path_or_url;
+
+ const queryobj = {
+ payload: "fsread1",
+ binary: "raw",
+ path: path_or_url,
+ };
+
+ if (path_or_url.endsWith(".svg")) {
+ queryobj.external = { "content-type": "image/svg+xml" };
+ }
+
+ const prefix = (new URL(cockpit.transport.uri("channel/" + cockpit.transport.csrf_token))).pathname;
+ const query = window.btoa(JSON.stringify(queryobj));
+ return prefix + '?' + query;
+}
+
+export const ProgressBar = ({ size, title, data, ariaLabelledBy }) => {
+ if (data.waiting) {
+ return (<Split>
+ <SplitItem className="progress-title" isFilled>
+ {_("Waiting for other programs to finish using the package manager...")}
+ </SplitItem>
+ <SplitItem>
+ <Spinner size="md" />
+ </SplitItem>
+ </Split>);
+ } else {
+ return <Progress className="progress-bar" value={data.percentage} size={size} measureLocation={ProgressMeasureLocation.inside} aria-labelledby={ariaLabelledBy} />;
+ }
+};
+
+export const CancelButton = ({ data }) => (
+ <Button variant="secondary" isDisabled={!data.cancel} onClick={data.cancel}>
+ {_("Cancel")}
+ </Button>);
+
+export const show_error = ex => {
+ if (ex.code == "cancelled")
+ return;
+
+ if (ex.code == "not-found")
+ ex.detail = _("No installation package found for this application.");
+
+ show_modal_dialog(
+ {
+ title: _("Error"),
+ body: (
+ <p>{typeof ex == 'string' ? ex : (ex.detail || ex.message)}</p>
+ )
+ },
+ {
+ cancel_button: { text: _("Close"), variant: "secondary" },
+ actions: []
+ });
+};
+
+export const launch = (comp) => {
+ for (let i = 0; i < comp.launchables.length; i++) {
+ if (comp.launchables[i].type == "cockpit-manifest") {
+ cockpit.jump([comp.launchables[i].name]);
+ return;
+ }
+ }
+};
diff --git a/pkg/apps/watch-appstream.py b/pkg/apps/watch-appstream.py
new file mode 100644
index 0000000..0b0ad24
--- /dev/null
+++ b/pkg/apps/watch-appstream.py
@@ -0,0 +1,367 @@
+import gzip
+import json
+import os
+import sys
+import traceback
+import xml.etree.ElementTree as ET
+
+# Our own little abstraction on top of inotify. This only supports
+# watching directories non-recursively, but it also supports watching
+# directories that come and go into and out of existence.
+#
+# We could use pyinotify for this, but it would only be able to
+# replace the Inotify class; we would still need all the logic in the
+# Watcher class.
+
+
+class Watcher:
+
+ def __init__(self):
+ self.inotify = Inotify()
+ self.watches = {} # path -> wd
+ self.handlers = {} # wd -> set of callbacks
+
+ def __add_watch(self, path, mask, handler):
+ if path in self.watches:
+ wd = self.watches[path]
+ self.handlers[wd] = self.handlers[wd] | frozenset([handler])
+ else:
+ wd = self.inotify.add_watch(path, mask)
+ if wd >= 0:
+ self.watches[path] = wd
+ self.handlers[wd] = frozenset([handler])
+
+ def __rem_watch(self, path, handler):
+ wd = self.watches[path]
+ self.handlers[wd] = self.handlers[wd] - frozenset([handler])
+ if len(self.handlers[wd]) == 0:
+ self.inotify.rem_watch(wd)
+ del self.handlers[wd]
+ del self.watches[path]
+
+ def watch_directory(self, path, callback):
+
+ events = (IN_CREATE |
+ IN_MOVED_TO |
+ IN_MOVED_FROM |
+ IN_DELETE_SELF |
+ IN_CLOSE_WRITE |
+ IN_DELETE |
+ IN_MOVE_SELF)
+
+ def handler(mask, name):
+ if ((mask & IN_CREATE or mask & IN_MOVED_TO) and
+ cur_wait and name == cur_wait):
+ reset()
+ elif mask & (IN_DELETE_SELF | IN_MOVE_SELF):
+ reset()
+ elif not cur_wait and len(name) > 0:
+ if mask & (IN_CLOSE_WRITE | IN_MOVED_TO | IN_DELETE | IN_MOVED_FROM):
+ callback(os.path.join(path, name))
+
+ def reset():
+ self.__rem_watch(cur_path, handler)
+ self.watch_directory(path, callback)
+
+ cur_path = path
+ cur_wait = None
+ while not os.path.exists(cur_path):
+ cur_wait = os.path.basename(cur_path)
+ cur_path = os.path.dirname(cur_path)
+
+ self.__add_watch(cur_path, events, handler)
+
+ if not cur_wait:
+ for f in os.listdir(cur_path):
+ callback(os.path.join(cur_path, f))
+
+ def run(self):
+ def event(wd, mask, name):
+ if wd in self.handlers:
+ for h in self.handlers[wd]:
+ h(mask, name)
+ self.inotify.run(event)
+
+
+lang = os.environ.get('LANGUAGE')
+
+
+def attr_lang(elt):
+ return elt.attrib.get('{http://www.w3.org/XML/1998/namespace}lang')
+
+
+def element(xml, tag):
+ if lang:
+ for elt in xml.iter(tag):
+ if attr_lang(elt) == lang:
+ return elt
+ return xml.find(tag)
+
+
+def element_value(xml, tag):
+ elt = element(xml, tag)
+ return elt.text if elt is not None else None
+
+
+def convert_description(xml, *, use_lang=True):
+ if xml is None:
+ return None
+
+ want_lang = lang if use_lang else None
+
+ # Only the following constructs are allowed, and they all appear
+ # at the top level:
+ #
+ # <p>text</p>
+ # <ul><li>text</li>...</ul>
+ # <ol><li>text</li>...</ol>
+
+ # A description can have 'lang' attributes both on the actual
+ # <description> element and on the contained <p>, <ul>, and <ol>
+ # elements, but probably not on the <li> elements.
+
+ def text(xml):
+ return " ".join(xml.itertext())
+
+ res = []
+ for c in xml:
+ if attr_lang(c) != want_lang:
+ continue
+ if c.tag == 'p':
+ res.append(text(c))
+ elif c.tag == 'ul' or c.tag == 'ol':
+ res.append({'tag': c.tag, 'items': list(map(text, c.findall('li')))})
+
+ # If we found nothing that matches lang, fall back to default
+ if lang is not None and len(res) == 0:
+ res = convert_description(xml, use_lang=False)
+
+ return res
+
+
+def convert_cached_icon(directory, origin, xml):
+ icon = xml.text
+
+ def try_size(sz):
+ path = os.path.join(directory, "..", "icons", origin, sz, icon)
+ return path if os.path.exists(path) else None
+
+ return try_size("64x64") or try_size("128x128")
+
+
+def convert_remote_icon(xml):
+ url = xml.text
+ if url.startswith(('http://', 'https://')):
+ return url
+ return None
+
+
+def convert_local_icon(xml):
+ path = xml.text
+ if path.startswith("/"):
+ return path
+ return None
+
+
+def find_and_convert_icon(directory, origin, xml):
+ if xml is None:
+ return None
+
+ # Just use the first icon.
+ icon = xml.find('icon')
+
+ if icon is not None:
+ if icon.attrib['type'] == 'cached':
+ return convert_cached_icon(directory, origin, icon)
+ elif icon.attrib['type'] == 'remote':
+ return convert_remote_icon(icon)
+ elif icon.attrib['type'] == 'local':
+ return convert_local_icon(icon)
+
+ return None
+
+
+def convert_screenshots(xml):
+ if xml is None:
+ return []
+
+ shots = []
+ for sh in xml.iter('screenshot'):
+ for img in sh.iter('image'):
+ if img.attrib['type'] == 'source':
+ shots.append({'full': img.text})
+
+ return shots
+
+
+def convert_launchables(xml):
+ ables = []
+
+ for elt in xml.iter('launchable'):
+ launchable_type = elt.attrib['type']
+ if launchable_type == "cockpit-manifest":
+ ables.append({'name': elt.text, 'type': launchable_type})
+
+ return ables
+
+
+def convert_urls(xml):
+ urls = []
+
+ for url in xml.iter('url'):
+ urls.append({'type': url.attrib['type'], 'link': url.text})
+
+ return urls
+
+
+def convert_collection_component(directory, origin, xml):
+ component_id = element_value(xml, 'id')
+ pkgname = element_value(xml, 'pkgname')
+ launchables = convert_launchables(xml)
+ urls = convert_urls(xml)
+
+ if not component_id or not pkgname or len(launchables) == 0:
+ return None
+
+ return {
+ 'id': component_id,
+ 'pkgname': pkgname,
+ 'name': element_value(xml, 'name'),
+ 'summary': element_value(xml, 'summary'),
+ 'description': convert_description(element(xml, 'description')),
+ 'icon': find_and_convert_icon(directory, origin, xml),
+ 'screenshots': convert_screenshots(element(xml, 'screenshots')),
+ 'launchables': launchables,
+ 'urls': urls
+ }
+
+
+def convert_upstream_component(file, xml):
+ if xml.tag != 'component':
+ return None
+
+ launchables = convert_launchables(xml)
+ if len(launchables) == 0:
+ return None
+
+ urls = convert_urls(xml)
+
+ return {
+ 'id': element_value(xml, 'id'),
+ 'name': element_value(xml, 'name'),
+ 'summary': element_value(xml, 'summary'),
+ 'description': convert_description(element(xml, 'description')),
+ 'icon': find_and_convert_icon(dir, '', xml),
+ 'screenshots': convert_screenshots(element(xml, 'screenshots')),
+ 'launchables': launchables,
+ 'installed': True,
+ 'file': file,
+ 'urls': urls
+ }
+
+
+class MetainfoDB:
+ def __init__(self):
+ self.dumping = False
+ self.installed_by_file = {}
+ self.available_by_file = {}
+
+ def notice_installed(self, file, xml_root):
+ if xml_root is not None:
+ comp = convert_upstream_component(file, xml_root)
+ if comp is not None:
+ self.installed_by_file[file] = comp
+ elif file in self.installed_by_file:
+ del self.installed_by_file[file]
+ if self.dumping:
+ self.dump()
+
+ def notice_available(self, file, xml_root):
+ if xml_root is not None:
+ info = {}
+ origin = xml_root.attrib['origin']
+ for xml_comp in xml_root.iter('component'):
+ try:
+ comp = convert_collection_component(os.path.dirname(file), origin, xml_comp)
+ if comp is not None:
+ if comp['id'] in info:
+ pass # warning: duplicate id
+ else:
+ info[comp['id']] = comp
+ except KeyError:
+ pass
+ self.available_by_file[file] = info
+ elif file in self.available_by_file:
+ del self.available_by_file[file]
+ if self.dumping:
+ self.dump()
+
+ def dump(self):
+ comps = {}
+ for file in self.installed_by_file:
+ comp = self.installed_by_file[file]
+ if comp['id'] in comps:
+ pass # warn dup
+ else:
+ comps[comp['id']] = comp
+ for file in self.available_by_file:
+ for comp_id in self.available_by_file[file]:
+ comp = self.available_by_file[file][comp_id]
+ if comp['id'] not in comps:
+ comps[comp['id']] = comp
+ else:
+ z = comp.copy()
+ z.update(comps[comp['id']])
+ comps[comp['id']] = z
+
+ data = {
+ 'components': comps,
+ 'origin_files': list(self.available_by_file.keys())
+ }
+
+ sys.stdout.write(json.dumps(data) + '\n')
+ sys.stdout.flush()
+
+ def start_dumping(self):
+ self.dump()
+ self.dumping = True
+
+
+def watch_db():
+ watcher = Watcher()
+
+ db = MetainfoDB()
+
+ def process_file(path, callback):
+ try:
+ if not os.path.exists(path):
+ callback(path, None)
+ elif path.endswith('.xml'):
+ callback(path, ET.parse(path).getroot())
+ elif path.endswith('.xml.gz'):
+ callback(path, ET.parse(gzip.open(path)).getroot())
+ except Exception:
+ # If we hit an exception during handling a file, pretend
+ # that it doesn't exist instead of keeping old data. This
+ # makes the behavior consistent across restarts of this
+ # watcher.
+ callback(path, None)
+ sys.stderr.write("%s: " % path)
+ sys.stderr.write("".join(traceback.format_exception_only(sys.exc_info()[0], sys.exc_info()[1])))
+ sys.stderr.flush()
+
+ def installed_callback(path):
+ process_file(path, lambda path, xml: db.notice_installed(path, xml))
+
+ def available_callback(path):
+ process_file(path, lambda path, xml: db.notice_available(path, xml))
+
+ watcher.watch_directory('/usr/share/metainfo', installed_callback)
+ watcher.watch_directory('/usr/share/swcatalog/xml', available_callback)
+ watcher.watch_directory('/usr/share/app-info/xmls', available_callback)
+ watcher.watch_directory('/var/cache/app-info/xmls', available_callback)
+ db.start_dumping()
+ watcher.run()
+
+
+watch_db()
diff --git a/pkg/base1/cockpit.js b/pkg/base1/cockpit.js
new file mode 100644
index 0000000..d09ec61
--- /dev/null
+++ b/pkg/base1/cockpit.js
@@ -0,0 +1,21 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// this registers itself as global on window.cockpit
+import cockpit from "cockpit"; // eslint-disable-line no-unused-vars
diff --git a/pkg/base1/manifest.json b/pkg/base1/manifest.json
new file mode 100644
index 0000000..765f494
--- /dev/null
+++ b/pkg/base1/manifest.json
@@ -0,0 +1,5 @@
+{
+ "version": "219",
+ "version-note": "last API change: https://github.com/cockpit-project/cockpit/pull/13482",
+ "priority": -1
+}
diff --git a/pkg/base1/test-base64.js b/pkg/base1/test-base64.js
new file mode 100644
index 0000000..fe8666f
--- /dev/null
+++ b/pkg/base1/test-base64.js
@@ -0,0 +1,75 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("base64 array", function (assert) {
+ let data = new Array(5);
+ for (let i = 0; i < 5; i++)
+ data[i] = i;
+ assert.equal(cockpit.base64_encode(data), "AAECAwQ=", "encoded from Array");
+
+ data = cockpit.base64_decode("AAECAwQFBg==");
+ assert.equal(data.length, 7, "right length");
+
+ let match = 1;
+ for (let i = 0; i < data.length; i++) {
+ if (data[i] != i) {
+ match = false;
+ break;
+ }
+ }
+
+ assert.ok(match, "right data");
+});
+
+QUnit.test("base64 arraybuffer", function (assert) {
+ const view = new Uint8Array(5);
+ for (let i = 0; i < 5; i++)
+ view[i] = i;
+ assert.equal(cockpit.base64_encode(view), "AAECAwQ=", "encoded from Uint8Array");
+
+ const data = cockpit.base64_decode("AAECAwQFBg==", Uint8Array);
+ assert.equal(data.length, 7, "right length");
+
+ let match = 1;
+ for (let i = 0; i < data.length; i++) {
+ if (data[i] != i) {
+ match = false;
+ break;
+ }
+ }
+
+ assert.ok(match, "right data");
+});
+
+QUnit.test("base64 string", function (assert) {
+ assert.equal(cockpit.base64_encode("blah"), "YmxhaA==", "encoded right");
+ assert.strictEqual(cockpit.base64_decode("YmxhaA==", String), "blah", "decoded right");
+});
+
+QUnit.test("base64 round trip", function (assert) {
+ function random_int(min, max) {
+ return Math.floor(Math.random() * (max - min)) + min;
+ }
+
+ const length = random_int(1000000, 5000000);
+ const data = new Array(length);
+ for (let i = 0; i < length; i++)
+ data[i] = random_int(0, 255);
+
+ const encoded = cockpit.base64_encode(data);
+ const decoded = cockpit.base64_decode(encoded);
+
+ assert.equal(decoded.length, length, "right length: " + length);
+
+ let match = true;
+ for (let i = 0; i < length; i++) {
+ if (data[i] != decoded[i]) {
+ match = false;
+ break;
+ }
+ }
+
+ assert.ok(match, "data correct");
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-browser-storage.js b/pkg/base1/test-browser-storage.js
new file mode 100644
index 0000000..0bc711e
--- /dev/null
+++ b/pkg/base1/test-browser-storage.js
@@ -0,0 +1,81 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+function test_storage (assert, storage, cockpitStorage) {
+ assert.expect(29);
+ storage.clear();
+ window.mock = {
+ pathname: "/cockpit+test/test"
+ };
+
+ assert.equal(cockpitStorage.prefixedKey("key1"), "cockpit+test:key1", "prefixed key has application");
+
+ /* setting */
+ cockpitStorage.setItem("key1", "value1", false);
+ assert.equal(storage.getItem("cockpit+test:key1"), "value1", "set single: application key set");
+ assert.equal(storage.getItem("key1"), null, "set single: key not set");
+ cockpitStorage.setItem("key1", "value2", true);
+ assert.equal(storage.getItem("cockpit+test:key1"), "value2", "set both: application key set");
+ assert.equal(storage.getItem("key1"), "value2", "set both: key set");
+ storage.clear();
+
+ /* getting */
+ storage.setItem("key1", "value1");
+ assert.equal(cockpitStorage.getItem("key1", false), null, "get single doesn't default to bare key");
+ assert.equal(cockpitStorage.getItem("key1", true), "value1", "get both defaults to bare key");
+ storage.setItem("cockpit+test:key1", "value2");
+ assert.equal(storage.getItem("key1"), "value1", "bare key not changed");
+ assert.equal(cockpitStorage.getItem("key1", false), "value2", "get single gets application prefixed value");
+ assert.equal(cockpitStorage.getItem("key1", true), "value2", "get both prefers application prefixed value");
+
+ /* removing */
+ cockpitStorage.removeItem("key1", false);
+ assert.equal(storage.getItem("key1"), "value1", "remove single doesn't remove bare key");
+ assert.equal(storage.getItem("cockpit+test:key1"), null, "remove single removes application prefixed key");
+ storage.setItem("cockpit+test:key1", "value1");
+ assert.equal(storage.getItem("cockpit+test:key1"), "value1", "application prefixed value reset");
+ cockpitStorage.removeItem("key1", true);
+ assert.equal(storage.getItem("key1"), null, "remove both removes bare key");
+ assert.equal(storage.getItem("cockpit+test:key1"), null, "remove both removes application prefixed key");
+ storage.clear();
+
+ /* clearing */
+ storage.setItem("key1", "value");
+ storage.setItem("key2", "value");
+ storage.setItem("cockpit+other:key1", "value");
+ storage.setItem("cockpit+other:key2", "value");
+ storage.setItem("cockpit+test:key1", "value");
+ storage.setItem("cockpit+test:key2", "value");
+
+ cockpitStorage.clear(false);
+ assert.equal(storage.getItem("key1"), "value", "clear doesn't remove bare key1");
+ assert.equal(storage.getItem("key2"), "value", "clear doesn't remove bare key2");
+ assert.equal(storage.getItem("cockpit+other:key1"), "value", "clear doesn't remove other application's key1");
+ assert.equal(storage.getItem("cockpit+other:key2"), "value", "clear doesn't remove other application's key2");
+ assert.equal(storage.getItem("cockpit+test:key1"), null, "clear doesn't remove our application's key1");
+ assert.equal(storage.getItem("cockpit+test:key2"), null, "clear doesn't remove our application's key2");
+
+ storage.setItem("cockpit+test:key1", "value");
+ storage.setItem("cockpit+test:key2", "value");
+ assert.equal(storage.getItem("cockpit+test:key1"), "value", "our application's key1 reset");
+ assert.equal(storage.getItem("cockpit+test:key2"), "value", "our application's key2 reset");
+
+ cockpitStorage.clear(true);
+ assert.equal(storage.getItem("key1"), null, "clear full removes bare key1");
+ assert.equal(storage.getItem("key2"), null, "clear full removes bare key2");
+ assert.equal(storage.getItem("cockpit+other:key1"), "value", "clear full doesn't remove other application's key1");
+ assert.equal(storage.getItem("cockpit+other:key2"), "value", "clear full doesn't remove other application's key2");
+ assert.equal(storage.getItem("cockpit+test:key1"), null, "clear full removes our application's key1");
+ assert.equal(storage.getItem("cockpit+test:key2"), null, "clear full removes our application's key2");
+}
+
+QUnit.test("local-storage", function (assert) {
+ test_storage(assert, window.localStorage, cockpit.localStorage);
+});
+
+QUnit.test("session-storage", function (assert) {
+ test_storage(assert, window.sessionStorage, cockpit.sessionStorage);
+});
+
+// Start tests after we have a user object
+cockpit.user().then(QUnit.start);
diff --git a/pkg/base1/test-cache.js b/pkg/base1/test-cache.js
new file mode 100644
index 0000000..ddd880a
--- /dev/null
+++ b/pkg/base1/test-cache.js
@@ -0,0 +1,103 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("public api", function (assert) {
+ assert.equal(typeof cockpit.cache, "function", "cockpit.cache is a function");
+});
+
+QUnit.test("single cache", function (assert) {
+ const done = assert.async();
+ assert.expect(6);
+
+ let closed = false;
+
+ function provider(result, key) {
+ assert.equal(key, "test-key-1", "provider got right key");
+ assert.equal(typeof result, "function", "provider got result function");
+
+ const timer = window.setTimeout(function() {
+ result({ myobject: "value" });
+ }, 200);
+
+ return {
+ close: function() {
+ window.clearTimeout(timer);
+ closed = true;
+ }
+ };
+ }
+
+ function consumer(value, key) {
+ assert.equal(key, "test-key-1", "consumer got right key");
+ assert.deepEqual(value, { myobject: "value" });
+
+ assert.equal(closed, false, "cache is not closed");
+ cache.close();
+ assert.equal(closed, true, "cache is closed");
+
+ done();
+ }
+
+ const cache = cockpit.cache("test-key-1", provider, consumer);
+});
+
+QUnit.test("multi cache", function (assert) {
+ const done = assert.async();
+ assert.expect(12);
+
+ let closed1 = false;
+
+ function provider1(result, key) {
+ assert.equal(key, "test-key-b", "provider1 got right key");
+ assert.equal(typeof result, "function", "provider1 got result function");
+
+ result({ myobject: "value1" });
+
+ return {
+ close: function() {
+ closed1 = true;
+ }
+ };
+ }
+
+ function provider2(result, key) {
+ assert.equal(key, "test-key-b", "provider2 got right key");
+ assert.equal(typeof result, "function", "provider2 got result function");
+
+ const timer = window.setTimeout(function() {
+ result({ myobject: "value2" });
+ }, 200);
+
+ return {
+ close: function() {
+ window.clearTimeout(timer);
+ }
+ };
+ }
+
+ function consumer1(value, key) {
+ assert.equal(key, "test-key-b", "consumer1 got right key");
+ assert.deepEqual(value, { myobject: "value1" }, "consumer1 got right value");
+ }
+
+ let count = 0;
+
+ function consumer2(value, key) {
+ assert.equal(key, "test-key-b", "consumer2 got right key");
+ count++;
+ if (count === 1) {
+ assert.deepEqual(value, { myobject: "value1" }, "consumer2 got value from producer1");
+ assert.equal(closed1, false, "cache1 is not closed");
+ cache1.close();
+ assert.equal(closed1, true, "cache1 is closed");
+ } else if (count === 2) {
+ assert.deepEqual(value, { myobject: "value2" }, "cache2 provided another value");
+ done();
+ }
+ }
+
+ const cache1 = cockpit.cache("test-key-b", provider1, consumer1);
+ cockpit.cache("test-key-b", provider2, consumer2);
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-chan.js b/pkg/base1/test-chan.js
new file mode 100644
index 0000000..143ef96
--- /dev/null
+++ b/pkg/base1/test-chan.js
@@ -0,0 +1,844 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+/* Set this to a regexp to ignore that warning once */
+function console_ignore_log(exp) {
+ const console_log = console.log;
+ console.log = function() {
+ if (!exp.exec(arguments[0]))
+ console_log.apply(console, arguments);
+ console.log = console_log;
+ };
+}
+
+/* The other end of the mock websocket */
+function MockPeer() {
+ const self = this;
+ const echos = { };
+ const nulls = { };
+ const queue = [];
+ let pending = false;
+
+ cockpit.event_target(this);
+
+ /* These are events */
+ self.onopen = function(event) { };
+ self.onrecv = function(event, channel, payload) {
+ /* A rudimentary echo channel implementation */
+ if (channel) {
+ if (channel in echos || channel in nulls)
+ queue.push([channel, payload]);
+ } else {
+ const command = JSON.parse(payload);
+ if (command.command == "open") {
+ let reply;
+ if (command.payload == "echo") {
+ echos[command.channel] = true;
+ reply = {
+ command: "ready",
+ channel: command.channel
+ };
+ } else if (command.payload == "null") {
+ nulls[command.channel] = true;
+ reply = {
+ command: "ready",
+ channel: command.channel
+ };
+ } else {
+ reply = {
+ command: "close",
+ channel: command.channel,
+ problem: "not-supported",
+ };
+ }
+ queue.push([null, JSON.stringify(reply)]);
+ } else if (command.command == "close") {
+ delete echos[command.channel];
+ delete nulls[command.channel];
+ }
+ }
+
+ if (queue.length && !pending) {
+ pending = true;
+ window.setTimeout(function() {
+ while (queue.length) {
+ self.send.apply(self, queue.shift());
+ }
+ pending = false;
+ }, 0);
+ }
+ };
+
+ /* Methods filled in by MockWebSocket */
+ self.send = function(channel, payload) { throw Error("not reached") };
+ self.close = function(options) { throw Error("not reached") };
+}
+
+window.mock = { url: "ws://url" };
+let force_default_host = null;
+let mock_peer = new MockPeer();
+
+QUnit.testDone(function() {
+ mock_peer = new MockPeer();
+ cockpit.transport.close();
+});
+
+/* Mock WebSocket */
+function MockWebSocket(url, protocol) {
+ if (typeof url != "string")
+ throw Error("WebSocket(@url) is not a string: " + typeof url);
+ if (typeof protocol != "string")
+ throw Error("WebSocket(@protocol) is not a string: " + typeof protocol);
+
+ this.onopen = function(event) { };
+ this.onclose = function(event) { };
+ this.onmessage = function(event) { };
+ this.onerror = function(event) { };
+ this.readyState = 0;
+ this.url = url;
+ this.protocol = protocol;
+ this.extensions = "";
+ this.binaryType = null;
+
+ const ws = this;
+ const mock = mock_peer;
+
+ this.send = function(data) {
+ if (typeof data != "string")
+ throw Error("WebSocket.send(@data) is not a string: " + typeof data);
+ const pos = data.indexOf("\n");
+ if (pos == -1)
+ throw Error("Invalid frame sent to WebSocket: " + data);
+ const channel = data.substring(0, pos);
+ const payload = data.substring(pos + 1);
+ window.setTimeout(function() { mock.dispatchEvent("recv", channel, payload) }, 5);
+ };
+
+ this.close = function(code, reason) {
+ if (typeof code != "number" && typeof code != "undefined")
+ throw Error("WebSocket.close(@code) is not a number: " + typeof code);
+ if (typeof reason != "string" && typeof reason != "undefined")
+ throw Error("WebSocket.close(@reason) is not a number: " + typeof string);
+ if (this.readyState > 1)
+ throw Error("WebSocket.close() called on a closed WebSocket" + this.readyState + " " + code + reason);
+ this.readyState = 3;
+ this.onclose({ name: "close", code: code || 1000, reason, wasClean: true });
+ };
+
+ /* Instantiate the global mock peer */
+ const sending = [];
+ mock.send = function(channel, payload) {
+ if (!channel)
+ channel = "";
+ const event = {
+ name: "message",
+ data: channel.toString() + "\n" + payload
+ };
+ sending.push(event);
+ window.setTimeout(function() {
+ if (ws.readyState == 1)
+ ws.onmessage(sending.shift());
+ }, 5);
+ };
+
+ mock.close = function(options) {
+ if (!options)
+ options = { };
+ window.setTimeout(function() {
+ ws.close(options.reason ? 1000 : 1011, options.reason || "");
+ }, 5);
+ };
+
+ /* Open shortly */
+ window.setTimeout(function() {
+ ws.readyState = 1;
+ mock.dispatchEvent("open");
+ ws.onopen({ name: "open" });
+ const init = {
+ command: "init",
+ version: 1,
+ "channel-seed": "test",
+ "csrf-token": "the-csrf-token",
+ user: {
+ user: "scruffy",
+ name: "Scruffy the Janitor"
+ },
+ system: {
+ version: "zero.point.zero",
+ build: "nasty stuff",
+ }
+ };
+ if (force_default_host)
+ init.host = force_default_host;
+ force_default_host = null;
+ ws.onmessage({ data: "\n" + JSON.stringify(init) });
+ }, 5);
+}
+
+WebSocket = MockWebSocket; // eslint-disable-line no-global-assign
+
+function check_transport (assert, base_url, application, socket, url_root) {
+ const old_url = window.mock.url;
+ const arr = [base_url];
+ if (base_url.slice(-1) == '/')
+ arr.push(base_url + "other");
+ else
+ arr.push(base_url + '/', base_url + '/other');
+
+ window.mock.url = null;
+ window.mock.url_root = url_root;
+ for (let i = 0; i < arr.length; i++) {
+ window.mock.pathname = arr[i];
+ assert.equal(cockpit.transport.application(), application,
+ arr[i] + " transport.application is " + socket);
+ assert.equal(cockpit.transport.uri(), "ws://" + window.location.host + socket,
+ arr[i] + " transport.uri is " + socket);
+ }
+
+ window.mock.url = old_url;
+ window.mock.url_root = null;
+ window.mock.pathname = null;
+}
+
+QUnit.test("public api", function (assert) {
+ const channel = cockpit.channel({ host: "host.example.com" });
+ assert.equal(typeof channel, "object", "cockpit.channel() constructor");
+ assert.equal(channel.options.host, "host.example.com", "channel.options is dict");
+ assert.ok(channel.id !== undefined, "channel.id is a field");
+ assert.ok(channel.toString().indexOf("host.example.com") > 0, "channel.toString()");
+ assert.equal(typeof channel.send, "function", "channel.send() is a function");
+ assert.equal(typeof channel.close, "function", "channel.close() is a function");
+ assert.strictEqual(channel.valid, true, "channel.valid is set");
+ assert.equal(typeof cockpit.logout, "function", "cockpit.logout is a function");
+ assert.equal(typeof cockpit.transport, "object", "cockpit.transport is an object");
+ assert.equal(typeof cockpit.transport.close, "function", "cockpit.transport.close is a function");
+ assert.equal(typeof cockpit.transport.options, "object", "cockpit.transport.options is a object");
+
+ if (window.location.origin)
+ assert.equal(cockpit.transport.origin, window.location.origin, "cockpit.transport.origin is correct");
+ else
+ assert.equal(typeof cockpit.transport.origin, "string", "cockpit.transport.origin is present");
+
+ check_transport(assert, '/', 'cockpit', '/cockpit/socket');
+ check_transport(assert, '/cockpit', 'cockpit', '/cockpit/socket');
+ check_transport(assert, '/cockpitother/', 'cockpit', '/cockpit/socket');
+ check_transport(assert, '/cockpita+pplication/', 'cockpit', '/cockpit/socket');
+ check_transport(assert, '/cockpit+application', 'cockpit+application', '/cockpit+application/socket');
+ check_transport(assert, '/=machine', 'cockpit+=machine', '/cockpit+=machine/socket');
+ check_transport(assert, '/url-root', 'cockpit', '/url-root/cockpit/socket', 'url-root');
+ check_transport(assert, '/url-root/cockpit', 'cockpit', '/url-root/cockpit/socket', 'url-root');
+ check_transport(assert, '/url-root/cockpit+application', 'cockpit+application',
+ '/url-root/cockpit+application/socket', 'url-root');
+ check_transport(assert, '/url-root/=machine', 'cockpit+=machine', '/url-root/cockpit+=machine/socket', 'url-root');
+});
+
+QUnit.test("open channel", function (assert) {
+ const done = assert.async();
+ assert.expect(8);
+
+ const channel = cockpit.channel({ host: "scruffy" });
+ let is_inited = false;
+ mock_peer.addEventListener("open", function(event) {
+ assert.ok(true, "websocket connected");
+ });
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ const command = JSON.parse(payload);
+ if (!is_inited) {
+ assert.equal(typeof command, "object", "valid json");
+ assert.strictEqual(chan, "", "sent with empty channel");
+ assert.equal(command.command, "init", "got init");
+ assert.equal(command.version, 1, "got init version");
+ is_inited = true;
+ } else {
+ assert.equal(command.command, "open", "right command");
+ assert.strictEqual(command.channel, channel.id, "contains right channel");
+ assert.equal(command.host, "scruffy", "host as expected");
+ done();
+ }
+ });
+});
+
+QUnit.test("multiple", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const channel = cockpit.channel({ host: "scruffy" });
+ const channelb = cockpit.channel({ host: "amy" });
+
+ const onrecv = event => {
+ mock_peer.removeEventListener("recv", onrecv);
+ assert.notStrictEqual(channel.id, channelb.id, "channels have different ids");
+ done();
+ };
+ mock_peer.addEventListener("recv", onrecv);
+});
+
+QUnit.test("open no host", function (assert) {
+ const done = assert.async();
+ assert.expect(3);
+
+ const channel = cockpit.channel({ });
+ assert.ok(channel);
+ mock_peer.addEventListener("open", function(event) {
+ assert.ok(true, "websocket connected");
+ });
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ const command = JSON.parse(payload);
+ if (command.command == "open") {
+ assert.strictEqual(command.host, undefined, "host not included");
+ done();
+ }
+ });
+});
+
+QUnit.test("open auto host", function (assert) {
+ const done = assert.async();
+ assert.expect(3);
+
+ force_default_host = "planetexpress";
+ const channel = cockpit.channel({ });
+ assert.ok(channel);
+ mock_peer.addEventListener("open", function(event) {
+ assert.ok(true, "websocket connected");
+ });
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ const command = JSON.parse(payload);
+ if (command.command == "open") {
+ assert.strictEqual(command.host, "planetexpress", "host automatically chosen");
+ done();
+ }
+ });
+});
+
+QUnit.test("send message", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const channel = cockpit.channel({ });
+ mock_peer.addEventListener("open", function(event) {
+ channel.send("Scruffy gonna die the way he lived");
+ });
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ /* Ignore the open and init messages */
+ if (!chan)
+ return;
+ assert.strictEqual(chan, channel.id, "sent with correct channel");
+ assert.equal(payload, "Scruffy gonna die the way he lived", "sent the right payload");
+ done();
+ });
+});
+
+QUnit.test("queue messages", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const sentence = [];
+ const channel = cockpit.channel({ });
+ channel.send("Scruffy");
+ channel.send("knows");
+ channel.send("he");
+ channel.send("rules");
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ if (!chan)
+ return; /* ignore control messages */
+ sentence.push(payload);
+ if (sentence.length === 4) {
+ assert.equal(sentence.join(" "), "Scruffy knows he rules", "messages queued and sent correctly");
+ done();
+ }
+ });
+});
+
+QUnit.test("receive message", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const onrecv = (event, chan, payload) => {
+ const cmd = JSON.parse(payload);
+ if (cmd.command == "open") {
+ mock_peer.removeEventListener("recv", onrecv);
+ mock_peer.send(channel.id, "Oh, marrrrmalade!");
+ }
+ };
+ mock_peer.addEventListener("recv", onrecv);
+
+ const channel = cockpit.channel({ });
+ channel.addEventListener("message", function(event, message) {
+ assert.equal(message, "Oh, marrrrmalade!", "got right message in channel");
+ done();
+ });
+});
+
+QUnit.test("close channel", function (assert) {
+ const done = assert.async(2);
+ assert.expect(4);
+
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ const cmd = JSON.parse(payload);
+ if (cmd.command == "init") {
+ return;
+ } else if (cmd.command == "open") {
+ channel.close();
+ return;
+ }
+ assert.equal(cmd.command, "close", "sent close command");
+ assert.strictEqual(cmd.channel, channel.id, "correct channel");
+ mock_peer.send("", payload);
+ done();
+ });
+ const channel = cockpit.channel({ });
+ channel.addEventListener("close", function(event, options) {
+ assert.ok(true, "triggered event");
+ assert.ok(!options.problem, "no problem");
+ done();
+ });
+});
+
+QUnit.test("close early", function (assert) {
+ const done = assert.async();
+ assert.expect(3);
+
+ const channel = cockpit.channel({ });
+ channel.addEventListener("close", function(event, options) {
+ assert.ok(true, "triggered event");
+ assert.equal(options.problem, "yo", "got problem");
+ done();
+ });
+ channel.close("yo");
+ assert.strictEqual(channel.valid, false, "no longer valid");
+});
+
+QUnit.test("close problem", function (assert) {
+ const done = assert.async();
+ assert.expect(5);
+
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ const cmd = JSON.parse(payload);
+ if (cmd.command == "init") {
+ return;
+ } else if (cmd.command == "open") {
+ channel.close({ problem: "problem" });
+ assert.strictEqual(channel.valid, false, "no longer valid");
+ return;
+ }
+ assert.equal(cmd.command, "close", "sent close command");
+ assert.equal(cmd.problem, "problem", "sent reason");
+ done();
+ });
+ const channel = cockpit.channel({ });
+ channel.addEventListener("close", function(event, options) {
+ assert.ok(true, "triggered event");
+ assert.equal(options.problem, "problem", "set");
+ });
+});
+
+QUnit.test("close problem string", function (assert) {
+ const done = assert.async();
+ assert.expect(5);
+
+ const channel = cockpit.channel({ });
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ const cmd = JSON.parse(payload);
+ if (cmd.command == "init") {
+ return;
+ } else if (cmd.command == "open") {
+ channel.close("testo");
+ assert.strictEqual(channel.valid, false, "no longer valid");
+ return;
+ }
+ assert.equal(cmd.command, "close", "sent close command");
+ assert.equal(cmd.problem, "testo", "sent reason");
+ done();
+ });
+ channel.addEventListener("close", function(event, options) {
+ assert.ok(true, "triggered event");
+ assert.equal(options.problem, "testo", "set");
+ });
+});
+
+QUnit.test("close peer", function (assert) {
+ const done = assert.async();
+ assert.expect(5);
+
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ const msg = JSON.parse(payload);
+ if (msg.command == "init")
+ return;
+ const cmd = {
+ command: "close",
+ channel: channel.id,
+ problem: "marmalade",
+ extra: 5
+ };
+ mock_peer.send("", JSON.stringify(cmd));
+ });
+
+ const channel = cockpit.channel({ });
+ const channelb = cockpit.channel({ });
+
+ channel.addEventListener("close", function(event, options) {
+ assert.ok(true, "triggered event");
+ assert.equal(options.problem, "marmalade", "received reason");
+ assert.equal(options.extra, 5, "received extra");
+ assert.strictEqual(channel.valid, false, "became invalid");
+ assert.strictEqual(channelb.valid, true, "correct channel");
+ done();
+ });
+});
+
+QUnit.test("close socket", function (assert) {
+ const done = assert.async();
+ assert.expect(4);
+
+ const channel = cockpit.channel({ });
+ const channelb = cockpit.channel({ });
+
+ channel.addEventListener("close", function(event, options) {
+ assert.equal(options.problem, "disconnected", "received reason");
+ assert.strictEqual(channel.valid, false, "channel is invalid");
+ if (!channel.valid && !channelb.valid)
+ done();
+ });
+
+ channelb.addEventListener("close", function(event, options) {
+ assert.equal(options.problem, "disconnected", "received reason");
+ assert.strictEqual(channelb.valid, false, "other channel invalid");
+ if (!channel.valid && !channelb.valid)
+ done();
+ });
+
+ mock_peer.close();
+});
+
+QUnit.test("wait ready", function (assert) {
+ const done = assert.async();
+ assert.expect(5);
+
+ const channel = cockpit.channel({ payload: "echo" });
+ channel.wait().then(function(options) {
+ assert.ok(true, "channel is ready");
+ assert.equal(typeof options, "object", "wait options");
+ assert.ok(!!options, "wait options not null");
+ assert.equal(options.command, "ready", "wait is ready");
+ assert.strictEqual(channel.valid, true, "when valid");
+ }, function() {
+ assert.ok(false, "should not fail");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test("wait close", function (assert) {
+ const done = assert.async();
+ assert.expect(6);
+
+ const channel = cockpit.channel({ payload: "unsupported" });
+ channel.wait().then(function() {
+ assert.ok(false, "should not succeed");
+ }, function(options) {
+ assert.ok(true, "channel is closed");
+ assert.equal(typeof options, "object", "wait options");
+ assert.ok(!!options, "wait options not null");
+ assert.equal(options.command, "close", "wait is close");
+ assert.equal(options.problem, "not-supported", "wait options has fields");
+ assert.strictEqual(channel.valid, false, "channel not valid");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test("wait callback", function (assert) {
+ const done = assert.async();
+ assert.expect(5);
+
+ const channel = cockpit.channel({ payload: "unsupported" });
+ channel.wait(function(options) {
+ assert.equal(typeof options, "object", "wait options");
+ assert.ok(!!options, "wait options not null");
+ assert.equal(options.command, "close", "wait is close");
+ assert.equal(options.problem, "not-supported", "wait options has fields");
+ assert.strictEqual(channel.valid, false, "channel not valid");
+ }).always(function() {
+ done();
+ });
+});
+
+QUnit.test("logout", function (assert) {
+ const done = assert.async();
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ const cmd = JSON.parse(payload);
+ if (cmd.command == "logout") {
+ mock_peer.close("disconnected");
+ assert.strictEqual(cmd.disconnect, true, "disconnect set");
+ }
+ });
+
+ let channel = cockpit.channel({ payload: "echo" });
+ let channelb = cockpit.channel({ payload: "echo" });
+
+ channel.addEventListener("close", function(event, options) {
+ assert.equal(options.problem, "disconnected", "received reason");
+ assert.strictEqual(channel.valid, false, "channel is invalid");
+ channel = null;
+ if (channel === null && channelb === null)
+ done();
+ });
+
+ channelb.addEventListener("close", function(event, options) {
+ assert.equal(options.problem, "disconnected", "received reason");
+ assert.strictEqual(channelb.valid, false, "other channel invalid");
+ channelb = null;
+ if (channel === null && channelb === null)
+ done();
+ });
+
+ cockpit.logout(false);
+});
+
+QUnit.test("droppriv", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ const cmd = JSON.parse(payload);
+ if (cmd.command == "logout") {
+ assert.strictEqual(cmd.disconnect, false, "disconnect not set");
+ done();
+ }
+ });
+
+ cockpit.drop_privileges();
+});
+
+QUnit.test("info", function (assert) {
+ const done = assert.async();
+ assert.expect(4);
+
+ let info_changed = false;
+
+ const onchanged = () => {
+ assert.strictEqual(cockpit.info.version, "zero.point.zero", "cockpit.info.version");
+ assert.strictEqual(cockpit.info.build, "nasty stuff", "cockpit.info.build");
+ info_changed = true;
+ };
+ cockpit.info.addEventListener("changed", onchanged);
+
+ const onrecv = (event, chan, payload) => {
+ const cmd = JSON.parse(payload);
+ if (cmd.command == "open") {
+ mock_peer.removeEventListener("recv", onrecv);
+ cockpit.info.removeEventListener("changed", onchanged);
+ assert.strictEqual(info_changed, true, "info changed event was called");
+ done();
+ }
+ };
+ mock_peer.addEventListener("recv", onrecv);
+
+ const channel = cockpit.channel({ host: "scruffy" });
+ assert.ok(channel);
+});
+
+QUnit.test("send after close", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ console_ignore_log(/sending message on closed.*/);
+
+ let received_message = false;
+ const channel = cockpit.channel({ });
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ if (chan)
+ received_message = true;
+ });
+
+ channel.close();
+ channel.send("Dern it.");
+
+ window.setTimeout(function() {
+ assert.ok(!received_message, "didn't send message");
+ done();
+ }, 50);
+});
+
+QUnit.test("ignore other commands", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const channel = cockpit.channel({ payload: "echo" });
+
+ console_ignore_log(/unhandled control message.*/);
+
+ mock_peer.send(0, JSON.stringify({ command: "ping" }));
+ mock_peer.send(0, JSON.stringify({ command: "unexpected" }));
+
+ window.setTimeout(function() {
+ assert.ok(channel.valid, "other messages didn't screw up channel");
+ done();
+ }, 50);
+});
+
+QUnit.test("filter message in", function (assert) {
+ const done = assert.async();
+ assert.expect(14);
+
+ let filtered = 0;
+ let filtering = true;
+ cockpit.transport.filter(function(message, channelid, control) {
+ if (!filtering)
+ return true;
+ if (message[0] == '\n') {
+ assert.strictEqual(channelid, "", "control message channel");
+ assert.equal(typeof control, "object", "control is a JSON object");
+ assert.equal(typeof control.command, "string", "control has a command");
+ return true;
+ } else {
+ assert.strictEqual(channelid, channel.id, "cockpit channel id");
+ assert.equal(control, undefined, "control is undefined");
+ filtered += 1;
+ return (filtered != 1);
+ }
+ });
+
+ let received = 0;
+ const channel = cockpit.channel({ payload: "echo" });
+ channel.addEventListener("message", function(data) {
+ received += 1;
+
+ if (received == 2) {
+ assert.equal(filtered, 3, "filtered right amount");
+ assert.equal(received, 2, "let through right amount");
+ channel.close();
+ filtering = false;
+ done();
+ }
+ });
+
+ channel.send("one");
+ channel.send("two");
+ channel.send("three");
+});
+
+QUnit.test("filter message out", function (assert) {
+ const done = assert.async();
+ assert.expect(10);
+
+ let filtered = 0;
+ let filtering = true;
+ cockpit.transport.filter(function(message, channelid, control) {
+ if (!filtering)
+ return true;
+ if (message[0] == '\n') {
+ assert.strictEqual(channelid, "", "control message channel");
+ assert.equal(typeof control, "object", "control is a JSON object");
+ assert.equal(typeof control.command, "string", "control has a command");
+ } else {
+ assert.strictEqual(channelid, channel.id, "cockpit channel id");
+ assert.equal(control, undefined, "control is undefined");
+ filtered += 1;
+
+ if (filtered != 1) {
+ channel.close();
+ filtering = false;
+ done();
+ return false;
+ }
+
+ return true;
+ }
+ return false;
+ }, true);
+
+ const channel = cockpit.channel({ payload: "null" });
+ channel.send("one");
+ channel.send("two");
+ channel.send("three");
+});
+
+QUnit.test("inject message out", function (assert) {
+ const done = assert.async();
+ assert.expect(4);
+
+ const ret = cockpit.transport.inject("bree\nyellow");
+ assert.equal(ret, false, "failure returns false");
+
+ let first = true;
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ if (first) {
+ const ret = cockpit.transport.inject("bree\nyellow");
+ assert.equal(ret, true, "returned true");
+ first = false;
+ return;
+ }
+
+ if (chan) {
+ assert.equal(chan, "bree", "right channel");
+ assert.equal(payload, "yellow", "right payload");
+ channel.close();
+ done();
+ }
+ });
+
+ const channel = cockpit.channel({ });
+});
+
+QUnit.test("inject message in", function (assert) {
+ const done = assert.async();
+ assert.expect(3);
+
+ const channel = cockpit.channel({ payload: "null" });
+ channel.addEventListener("control", function(ev, control) {
+ if (control.command == "ready") {
+ const payload = JSON.stringify({ command: "blah", blah: "marmalade", channel: channel.id });
+ const ret = cockpit.transport.inject("\n" + payload, false);
+ assert.equal(ret, true, "returned true");
+ } else {
+ assert.equal(control.command, "blah", "got right control message");
+ assert.equal(control.blah, "marmalade", "got right control data");
+ channel.close();
+ done();
+ }
+ });
+});
+
+QUnit.test("transport options", function (assert) {
+ const done = assert.async();
+ assert.expect(3);
+
+ mock_peer.addEventListener("recv", function(event, chan, payload) {
+ if (chan) {
+ assert.equal(typeof cockpit.transport.options, "object", "is an object");
+ assert.deepEqual(cockpit.transport.options, {
+ command: "init",
+ version: 1,
+ "channel-seed": "test",
+ "csrf-token": "the-csrf-token",
+ user: {
+ user: "scruffy",
+ name: "Scruffy the Janitor"
+ },
+ system: {
+ version: "zero.point.zero",
+ build: "nasty stuff",
+ }
+ }, "is correct");
+ assert.equal(cockpit.transport.csrf_token, "the-csrf-token", "got csrf token");
+ channel.close();
+ done();
+ }
+ });
+
+ const channel = cockpit.channel({ });
+ channel.send("blah");
+});
+
+QUnit.test("message", function (assert) {
+ assert.expect(4);
+ assert.strictEqual(cockpit.message("terminated"), "Your session has been terminated.", "problem code");
+ assert.strictEqual(cockpit.message({ problem: "timeout" }), "Connection has timed out.", "problem property");
+ assert.strictEqual(cockpit.message({ message: "The message", problem: "blah" }), "The message", "problem property");
+ assert.strictEqual(cockpit.message(55), "55", "invalid input");
+});
+
+window.location.hash = "";
+QUnit.start();
diff --git a/pkg/base1/test-dbus-address.js b/pkg/base1/test-dbus-address.js
new file mode 100644
index 0000000..e6284b3
--- /dev/null
+++ b/pkg/base1/test-dbus-address.js
@@ -0,0 +1,24 @@
+/* global direct_address, bus_address, */
+import { common_dbus_tests, dbus_track_tests } from "./test-dbus-common.js";
+
+import QUnit from "qunit-tests";
+
+/* no name */
+const direct_options = {
+ address: direct_address,
+ bus: "none",
+ capabilities: ["address"]
+};
+
+common_dbus_tests(direct_options, null);
+
+/* with a name */
+const address_options = {
+ address: bus_address,
+ bus: "none",
+ capabilities: ["address"]
+};
+common_dbus_tests(address_options, "com.redhat.Cockpit.DBusTests.Test");
+dbus_track_tests(address_options, "com.redhat.Cockpit.DBusTests.Test");
+
+QUnit.start();
diff --git a/pkg/base1/test-dbus-common.js b/pkg/base1/test-dbus-common.js
new file mode 100644
index 0000000..96b7351
--- /dev/null
+++ b/pkg/base1/test-dbus-common.js
@@ -0,0 +1,1016 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+function deep_update(target, data) {
+ for (const prop in data) {
+ if (Object.prototype.toString.call(data[prop]) === '[object Object]') {
+ if (!target[prop])
+ target[prop] = {};
+ deep_update(target[prop], data[prop]);
+ } else {
+ target[prop] = data[prop];
+ }
+ }
+}
+
+export function common_dbus_tests(channel_options, bus_name) { // eslint-disable-line no-unused-vars
+ QUnit.test("call method", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ assert.equal(typeof dbus.call, "function", "is a function");
+ const reply = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "HelloWorld", ["Browser-side JS"]);
+ assert.deepEqual(reply, ["Word! You said `Browser-side JS'. I'm Skeleton, btw!"], "reply");
+ });
+
+ QUnit.test("call method with timeout", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "NeverReturn",
+ [], { timeout: 10 });
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.ok([
+ "org.freedesktop.DBus.Error.Timeout",
+ "org.freedesktop.DBus.Error.NoReply"
+ ].indexOf(ex.name) >= 0);
+ }
+ });
+
+ QUnit.test("close immediately", assert => {
+ const done = assert.async();
+ assert.expect(1);
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ dbus.addEventListener("close", (_event, options) => {
+ assert.equal(options.problem, "test-code", "got right code");
+ done();
+ });
+
+ window.setTimeout(() => dbus.close("test-code"), 100);
+ });
+
+ QUnit.test("call close", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ const promise = dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["Browser-side JS"]);
+ dbus.close();
+ await promise;
+ // assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.problem, "disconnected", "got right close code");
+ }
+ });
+
+ QUnit.test("call closed", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ dbus.close("blah-blah");
+
+ try {
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "HelloWorld", ["Browser-side JS"]);
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.problem, "blah-blah", "got right close code");
+ }
+ });
+
+ QUnit.test("primitive types", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const reply = await dbus.call(
+ "/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TestPrimitiveTypes",
+ [10, true, 11, 12, 13, 14, 15, 16, 17, "a string", "/a/path", "asig", "ZWZnAA=="]);
+ assert.deepEqual(reply, [
+ 20, false, 111, 1012, 10013, 100014, 1000015, 10000016, 17.0 / Math.PI,
+ "Word! You said `a string'. Rock'n'roll!", "/modified/a/path", "assgitasig",
+ "Ynl0ZXN0cmluZyH/AA=="
+ ], "round trip");
+ });
+
+ QUnit.test.skipWithPybridge("integer bounds", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+
+ async function testNumber(type, value, valid) {
+ try {
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "TestVariant", [{ t: type, v: value }]);
+ if (!valid)
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(valid, false);
+ assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs");
+ }
+ }
+
+ await testNumber('y', 0, true);
+ await testNumber('y', 0xff, true);
+ await testNumber('y', -1, false);
+ await testNumber('y', 0xff + 1, false);
+ await testNumber('n', -300, true);
+ await testNumber('n', 300, true);
+ await testNumber('n', -0x8000 - 1, false);
+ await testNumber('n', 0x7fff + 1, false);
+ await testNumber('q', 0, true);
+ await testNumber('q', 300, true);
+ await testNumber('q', -1, false);
+ await testNumber('q', 0xffff + 1, false);
+ await testNumber('i', -0xfffff, true);
+ await testNumber('i', 0xfffff, true);
+ await testNumber('i', -0x80000000 - 1, false);
+ await testNumber('i', 0x7fffffff + 1, false);
+ await testNumber('u', 0, true);
+ await testNumber('u', 0xfffff, true);
+ await testNumber('u', -1, false);
+ await testNumber('u', 0xffffffff + 1, false);
+ await testNumber('x', -0xfffffffff, true);
+ await testNumber('x', 0xfffffffff, true);
+ await testNumber('t', 0xfffffffff, true);
+ await testNumber('t', -1, false);
+ });
+
+ QUnit.test("non-primitive types", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const reply = await dbus.call(
+ "/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "TestNonPrimitiveTypes", [
+ { one: "red", two: "blue" },
+ { first: [42, 42], second: [43, 43] },
+ [42, 'foo', 'bar'],
+ ["one", "two"],
+ ["/one", "/one/two"],
+ ["ass", "git"],
+ ["QUIA", "QkMA"]
+ ]);
+ assert.deepEqual(reply, [
+ "{'one': 'red', 'two': 'blue'}{'first': (42, 42), 'second': (43, 43)}(42, 'foo', 'bar')array_of_strings: [one, two] array_of_objpaths: [/one, /one/two] array_of_signatures: [signature 'ass', 'git'] array_of_bytestrings: [AB, BC] "
+ ], "round trip");
+ });
+
+ QUnit.test.skipWithPybridge("variants", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const reply = await dbus.call(
+ "/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "TestAsv", [{
+ one: cockpit.variant("s", "foo"),
+ two: cockpit.variant("o", "/bar"),
+ three: cockpit.variant("g", "assgit"),
+ four: cockpit.variant("y", 42),
+ five: cockpit.variant("d", 1000.0)
+ }]);
+ assert.deepEqual(reply, [
+ "{'one': <'foo'>, 'two': <objectpath '/bar'>, 'three': <signature 'assgit'>, 'four': <byte 0x2a>, 'five': <1000.0>}"
+ ], "round trip");
+ });
+
+ QUnit.test.skipWithPybridge("bad variants", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call(
+ "/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TestAsv",
+ [{
+ one: "foo",
+ two: "/bar",
+ three: "assgit",
+ four: 42,
+ five: 1000.0
+ }]);
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs", "error name");
+ assert.equal(ex.message, "Unexpected type 'string' in argument", "error message");
+ }
+ });
+
+ QUnit.test("get all", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const reply = await dbus.call("/otree/frobber", "org.freedesktop.DBus.Properties", "GetAll",
+ ["com.redhat.Cockpit.DBusTests.Frobber"]);
+ assert.deepEqual(reply, [{
+ FinallyNormalName: { t: "s", v: "There aint no place like home" },
+ ReadonlyProperty: { t: "s", v: "blah" },
+ aay: { t: "aay", v: [] },
+ ag: { t: "ag", v: [] },
+ ao: { t: "ao", v: [] },
+ as: { t: "as", v: [] },
+ ay: { t: "ay", v: "QUJDYWJjAA==" },
+ b: { t: "b", v: false },
+ d: { t: "d", v: 43 },
+ g: { t: "g", v: "" },
+ i: { t: "i", v: 0 },
+ n: { t: "n", v: 0 },
+ o: { t: "o", v: "/" },
+ q: { t: "q", v: 0 },
+ s: { t: "s", v: "" },
+ t: { t: "t", v: 0 },
+ u: { t: "u", v: 0 },
+ x: { t: "x", v: 0 },
+ y: { t: "y", v: 42 }
+ }], "reply");
+ });
+
+ QUnit.test("call unimplemented", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "UnimplementedMethod", []);
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.name, "org.freedesktop.DBus.Error.UnknownMethod", "error name");
+ assert.equal(ex.message, "Method UnimplementedMethod is not implemented on interface com.redhat.Cockpit.DBusTests.Frobber", "error message");
+ }
+ });
+
+ QUnit.test.skipWithPybridge("call bad base64", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call(
+ "/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TestPrimitiveTypes",
+ [10, true, 11, 12, 13, 14, 15, 16, 17, "a string", "/a/path", "asig", "Yooohooo!~ bad base64"]);
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs", "error name");
+ assert.equal(ex.message, "Invalid base64 in argument", "error message");
+ }
+ });
+
+ QUnit.test("call unknown", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "UnknownBlahMethod", [1]);
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.name, "org.freedesktop.DBus.Error.UnknownMethod", "error name");
+ assert.equal(ex.message, "Introspection data for method com.redhat.Cockpit.DBusTests.Frobber UnknownBlahMethod not available", "error message");
+ }
+ });
+
+ QUnit.test("signals", async assert => {
+ let received = false;
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ dbus.subscribe({
+ interface: "com.redhat.Cockpit.DBusTests.Frobber",
+ path: "/otree/frobber"
+ }, (path, iface, signal, args) => {
+ if (received)
+ return;
+ assert.equal(path, "/otree/frobber", "got right path");
+ assert.equal(iface, "com.redhat.Cockpit.DBusTests.Frobber", "got right path");
+ assert.equal(signal, "TestSignal", "signals: got right path");
+ assert.deepEqual(args, [
+ 43, ["foo", "frobber"], ["/foo", "/foo/bar"],
+ { first: [42, 42], second: [43, 43] }], "got right arguments");
+ received = true;
+ });
+
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "RequestSignalEmission", [0]);
+ assert.equal(received, true, "signal received");
+ });
+
+ QUnit.test("signal unsubscribe", async assert => {
+ let received = false;
+ const dbus = cockpit.dbus(bus_name, channel_options);
+
+ function on_signal() {
+ received = true;
+ }
+
+ const subscription = dbus.subscribe({
+ interface: "com.redhat.Cockpit.DBusTests.Frobber",
+ path: "/otree/frobber"
+ }, on_signal);
+
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "RequestSignalEmission", [0]);
+ assert.equal(received, true, "signal received");
+
+ subscription.remove();
+ received = false;
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "RequestSignalEmission", [0]);
+ assert.equal(received, false, "signal not received");
+ });
+
+ QUnit.test("with types", assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ dbus.call("/bork", "borkety.Bork", "Echo",
+ [{ one: "red", two: "blue" }, 55, 66, 32],
+ { type: "a{ss}uit" })
+ .done(function(reply, options) {
+ assert.deepEqual(reply, [{ one: "red", two: "blue" }, 55, 66, 32], "round trip");
+ assert.equal(options.type, "a{ss}uit", "got back type");
+ })
+ .always(function() {
+ assert.equal(this.state(), "resolved", "finished successfully");
+ done();
+ });
+ });
+
+ QUnit.test.skipWithPybridge("empty base64", assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ dbus.call("/bork", "borkety.Bork", "Echo",
+ [""],
+ { type: "ay" })
+ .done(function(reply, options) {
+ assert.deepEqual(reply, [""], "round trip");
+ assert.equal(options.type, "ay", "got back type");
+ })
+ .always(function() {
+ assert.equal(this.state(), "resolved", "finished successfully");
+ done();
+ });
+ });
+
+ QUnit.test.skipWithPybridge("bad object path", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("invalid/path", "borkety.Bork", "Echo", [1]);
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.problem, "protocol-error", "error name");
+ assert.equal(ex.message, "object path is invalid in dbus \"call\": invalid/path", "error message");
+ }
+ });
+
+ QUnit.test.skipWithPybridge("bad interface name", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/path", "!invalid!interface!", "Echo", [1]);
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.problem, "protocol-error", "error name");
+ assert.equal(ex.message, "interface name is invalid in dbus \"call\": !invalid!interface!", "error message");
+ }
+ });
+
+ QUnit.test.skipWithPybridge("bad method name", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/path", "borkety.Bork", "!Invalid!Method!", [1]);
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.problem, "protocol-error", "error name");
+ assert.equal(ex.message, "member name is invalid in dbus \"call\": !Invalid!Method!", "error message");
+ }
+ });
+
+ QUnit.test.skipWithPybridge("bad flags", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/path", "borkety.Bork", "Method", [1], { flags: 5 });
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.problem, "protocol-error", "error name");
+ assert.equal(ex.message, "the \"flags\" field is invalid in dbus call", "error message");
+ }
+ });
+
+ QUnit.test.skipWithPybridge("bad types", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/bork", "borkety.Bork", "Echo", [1], { type: "!!%%" });
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.problem, "protocol-error", "error name");
+ assert.equal(ex.message, "the \"type\" signature is not valid in dbus call: !!%%", "error message");
+ }
+ });
+
+ QUnit.test.skipWithPybridge("bad type invalid", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/bork", "borkety.Bork", "Echo", [1], { type: 5 }); // invalid
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.problem, "protocol-error", "error name");
+ assert.equal(ex.message, "the \"type\" field is invalid in call", "error message");
+ }
+ });
+
+ QUnit.test.skipWithPybridge("bad dict type", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "Nobody",
+ [{ "!!!": "value" }], { type: "a{is}" });
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs", "error name");
+ assert.equal(ex.message, "Unexpected key '!!!' in dict entry", "error message");
+ }
+ });
+
+ QUnit.test.skipWithPybridge("bad object path", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "Nobody",
+ ["not/a/path"], { type: "o" });
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs", "error name");
+ assert.equal(ex.message, "Invalid object path 'not/a/path'", "error message");
+ }
+ });
+
+ QUnit.test.skipWithPybridge("bad signature", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ try {
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "Nobody", ["bad signature"], { type: "g" });
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.name, "org.freedesktop.DBus.Error.InvalidArgs", "error name");
+ assert.equal(ex.message, "Invalid signature 'bad signature'", "error message");
+ }
+ });
+
+ QUnit.test("flags", assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "HelloWorld", ["test"], { flags: "" })
+ .done(function(reply, options) {
+ assert.equal(typeof options.flags, "string", "is string");
+ assert.ok(options.flags.indexOf(">") !== -1 || options.flags.indexOf("<") !== -1, "has byte order");
+ })
+ .always(function() {
+ assert.equal(this.state(), "resolved", "finished successfully");
+ done();
+ });
+ });
+
+ QUnit.test("without introspection", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const reply = await dbus.call("/bork", "borkety.Bork", "Echo");
+ assert.deepEqual(reply, [], "round trip");
+ });
+
+ QUnit.test("watch path", async assert => {
+ const cache = { };
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const onnotify = (event, data) => deep_update(cache, data);
+ dbus.addEventListener("notify", onnotify);
+
+ await dbus.watch("/otree/frobber");
+ assert.equal(typeof cache["/otree/frobber"], "object", "has path");
+ assert.deepEqual(cache["/otree/frobber"]["com.redhat.Cockpit.DBusTests.Frobber"],
+ {
+ FinallyNormalName: "There aint no place like home",
+ ReadonlyProperty: "blah",
+ aay: [],
+ ag: [],
+ ao: [],
+ as: [],
+ ay: "QUJDYWJjAA==",
+ b: false,
+ d: 43,
+ g: "",
+ i: 0,
+ n: 0,
+ o: "/",
+ q: 0,
+ s: "",
+ t: 0,
+ u: 0,
+ x: 0,
+ y: 42
+ }, "correct data");
+ dbus.removeEventListener("notify", onnotify);
+ });
+
+ QUnit.test("watch object manager", async assert => {
+ const cache = { };
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const onnotify = (event, data) => deep_update(cache, data);
+ dbus.addEventListener("notify", onnotify);
+
+ await dbus.watch({ path_namespace: "/otree" });
+ assert.deepEqual(cache, {
+ "/otree/frobber": {
+ "com.redhat.Cockpit.DBusTests.Frobber":
+ {
+ FinallyNormalName: "There aint no place like home",
+ ReadonlyProperty: "blah",
+ aay: [],
+ ag: [],
+ ao: [],
+ as: [],
+ ay: "QUJDYWJjAA==",
+ b: false,
+ d: 43,
+ g: "",
+ i: 0,
+ n: 0,
+ o: "/",
+ q: 0,
+ s: "",
+ t: 0,
+ u: 0,
+ x: 0,
+ y: 42
+ }
+ }
+ }, "correct data");
+ dbus.removeEventListener("notify", onnotify);
+ });
+
+ QUnit.test("watch change", async assert => {
+ const cache = { };
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const onnotify_cache = (event, data) => deep_update(cache, data);
+ dbus.addEventListener("notify", onnotify_cache);
+
+ let onnotify_test_called = false;
+
+ const onnotify_test = (event, data) => {
+ assert.equal(typeof cache["/otree/frobber"], "object", "has path");
+ assert.deepEqual(cache, {
+ "/otree/frobber": {
+ "com.redhat.Cockpit.DBusTests.Frobber": {
+ FinallyNormalName: "There aint no place like home",
+ ReadonlyProperty: "blah",
+ aay: [],
+ ag: [],
+ ao: [],
+ as: [],
+ ay: "QUJDYWJjAA==",
+ b: false,
+ d: 43,
+ g: "",
+ i: 0,
+ n: 0,
+ o: "/",
+ q: 0,
+ s: "",
+ t: 0,
+ u: 0,
+ x: 0,
+ y: 42
+ }
+ }
+ }, "correct data");
+ dbus.removeEventListener("notify", onnotify_cache);
+ dbus.removeEventListener("notify", onnotify_test);
+ onnotify_test_called = true;
+ };
+ dbus.addEventListener("notify", onnotify_test);
+
+ await dbus.watch("/otree/frobber");
+ assert.ok(onnotify_test_called, "onnotify_test was called");
+ });
+
+ QUnit.test("watch barrier", async assert => {
+ const cache = { };
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const onnotify = (event, data) => deep_update(cache, data);
+ dbus.addEventListener("notify", onnotify);
+
+ dbus.watch({ path_namespace: "/otree" });
+
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["Browser-side JS"]);
+ assert.deepEqual(cache["/otree/frobber"]["com.redhat.Cockpit.DBusTests.Frobber"],
+ {
+ FinallyNormalName: "There aint no place like home",
+ ReadonlyProperty: "blah",
+ aay: [],
+ ag: [],
+ ao: [],
+ as: [],
+ ay: "QUJDYWJjAA==",
+ b: false,
+ d: 43,
+ g: "",
+ i: 0,
+ n: 0,
+ o: "/",
+ q: 0,
+ s: "",
+ t: 0,
+ u: 0,
+ x: 0,
+ y: 42
+ }, "correct data");
+ dbus.removeEventListener("notify", onnotify);
+ });
+
+ QUnit.test("watch interfaces", async assert => {
+ const cache = { };
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const onnotify = (event, data) => deep_update(cache, data);
+ dbus.addEventListener("notify", onnotify);
+
+ await dbus.watch({ path_namespace: "/otree" });
+ assert.deepEqual(cache, {
+ "/otree/frobber": {
+ "com.redhat.Cockpit.DBusTests.Frobber":
+ {
+ FinallyNormalName: "There aint no place like home",
+ ReadonlyProperty: "blah",
+ aay: [],
+ ag: [],
+ ao: [],
+ as: [],
+ ay: "QUJDYWJjAA==",
+ b: false,
+ d: 43,
+ g: "",
+ i: 0,
+ n: 0,
+ o: "/",
+ q: 0,
+ s: "",
+ t: 0,
+ u: 0,
+ x: 0,
+ y: 42
+ }
+ }
+ }, "correct data");
+
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "AddAlpha", []);
+ assert.deepEqual(cache, {
+ "/otree/frobber": {
+ "com.redhat.Cockpit.DBusTests.Frobber":
+ {
+ FinallyNormalName: "There aint no place like home",
+ ReadonlyProperty: "blah",
+ aay: [],
+ ag: [],
+ ao: [],
+ as: [],
+ ay: "QUJDYWJjAA==",
+ b: false,
+ d: 43,
+ g: "",
+ i: 0,
+ n: 0,
+ o: "/",
+ q: 0,
+ s: "",
+ t: 0,
+ u: 0,
+ x: 0,
+ y: 42
+ },
+ "com.redhat.Cockpit.DBusTests.Alpha": {}
+ }
+ }, "correct data");
+
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "RemoveAlpha", []);
+ assert.deepEqual(cache, {
+ "/otree/frobber": {
+ "com.redhat.Cockpit.DBusTests.Frobber": {
+ FinallyNormalName: "There aint no place like home",
+ ReadonlyProperty: "blah",
+ aay: [],
+ ag: [],
+ ao: [],
+ as: [],
+ ay: "QUJDYWJjAA==",
+ b: false,
+ d: 43,
+ g: "",
+ i: 0,
+ n: 0,
+ o: "/",
+ q: 0,
+ s: "",
+ t: 0,
+ u: 0,
+ x: 0,
+ y: 42
+ },
+ "com.redhat.Cockpit.DBusTests.Alpha": null
+ }
+ }, "correct data");
+ dbus.removeEventListener("notify", onnotify);
+ });
+
+ QUnit.test.skipWithPybridge("path loop", async assert => {
+ const name = "yo" + new Date().getTime();
+ const cache = { };
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const onnotify = (event, data) => Object.assign(cache, data);
+ dbus.addEventListener("notify", onnotify);
+
+ await dbus.watch({ path_namespace: "/cliques/" + name });
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "CreateClique", [name]);
+ const expect = { };
+ /* The same way mock-service.c calculates the paths */
+ for (let i = 0; i < 3; i++) {
+ expect["/cliques/" + name + "/" + i] = {
+ "com.redhat.Cockpit.DBusTests.Clique": {
+ Friend: "/cliques/" + name + "/" + (i + 1) % 3
+ }
+ };
+ }
+ assert.deepEqual(cache, expect, "got all data before method reply");
+ dbus.removeEventListener("notify", onnotify);
+ });
+
+ QUnit.test.skipWithPybridge("path signal", async assert => {
+ const name = "yo" + new Date().getTime();
+ const cache = { };
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const onnotify = (event, data) => Object.assign(cache, data);
+ dbus.addEventListener("notify", onnotify);
+
+ await dbus.watch({ path: "/hidden/" + name });
+ assert.deepEqual(cache, { }, "no data yet");
+
+ dbus.subscribe({ path: "/hidden/" + name }, function(path, iface, args) {
+ assert.equal(typeof cache[path], "object", "have object");
+ assert.deepEqual(cache[path], {
+ "com.redhat.Cockpit.DBusTests.Hidden": { Name: name }
+ }, "got data before signal");
+ dbus.removeEventListener("notify", onnotify);
+ });
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "EmitHidden", [name]);
+ });
+
+ QUnit.test("proxy", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");
+ await proxy.wait();
+ assert.strictEqual(proxy.valid, true, "proxy: is valid");
+ assert.deepEqual(proxy.data, {
+ FinallyNormalName: "There aint no place like home",
+ ReadonlyProperty: "blah",
+ aay: [],
+ ag: [],
+ ao: [],
+ as: [],
+ ay: "QUJDYWJjAA==",
+ b: false,
+ d: 43,
+ g: "",
+ i: 0,
+ n: 0,
+ o: "/",
+ q: 0,
+ s: "",
+ t: 0,
+ u: 0,
+ x: 0,
+ y: 42
+ }, "correct data");
+
+ assert.strictEqual(proxy.FinallyNormalName, "There aint no place like home", "property value");
+ assert.strictEqual(proxy.ReadonlyProperty, "blah", "another property value");
+
+ assert.equal(typeof proxy.HelloWorld, "function", "has function defined");
+ const message = await proxy.HelloWorld("From a proxy");
+ assert.equal(message, "Word! You said `From a proxy'. I'm Skeleton, btw!", "method args");
+ });
+
+ QUnit.test("proxy call", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");
+
+ /* No wait */
+ const args = await proxy.call("HelloWorld", ["From a proxy"]);
+ assert.equal(args[0], "Word! You said `From a proxy'. I'm Skeleton, btw!", "method args");
+ });
+
+ QUnit.test("proxy call with timeout", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");
+
+ try {
+ await proxy.call('NeverReturn', [], { timeout: 10 });
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.ok(["org.freedesktop.DBus.Error.Timeout",
+ "org.freedesktop.DBus.Error.NoReply"].indexOf(ex.name) >= 0);
+ }
+ });
+
+ QUnit.test("proxy signal", async assert => {
+ let received = false;
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");
+
+ const onsignal = (event, name, args) => {
+ assert.equal(name, "TestSignal", "signals: got right name");
+ assert.deepEqual(args, [
+ 43, ["foo", "frobber"], ["/foo", "/foo/bar"],
+ { first: [42, 42], second: [43, 43] }], "got right arguments");
+ received = true;
+ };
+ proxy.addEventListener("signal", onsignal);
+
+ await proxy.call("RequestSignalEmission", [0]);
+ assert.equal(received, true, "signal received");
+ proxy.removeEventListener("signal", onsignal);
+ });
+
+ QUnit.test("proxy explicit notify", async assert => {
+ const done = assert.async();
+ assert.expect(1);
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");
+
+ await proxy.wait();
+ const onchanged = () => {
+ assert.equal(proxy.FinallyNormalName, "externally injected");
+ proxy.removeEventListener("changed", onchanged);
+ done();
+ };
+ proxy.addEventListener("changed", onchanged);
+
+ dbus.notify({
+ "/otree/frobber": {
+ "com.redhat.Cockpit.DBusTests.Frobber": {
+ FinallyNormalName: "externally injected"
+ }
+ }
+ });
+ });
+
+ QUnit.test("proxies", async assert => {
+ const dbus = cockpit.dbus(bus_name, channel_options);
+
+ /* Just some cleanup */
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "DeleteAllObjects", []);
+ const proxies = dbus.proxies("com.redhat.Cockpit.DBusTests.Frobber", "/otree");
+ await proxies.wait();
+
+ let added;
+ proxies.addEventListener("added", function(event, proxy) {
+ added = proxy;
+ assert.strictEqual(added.valid, true, "added objects valid");
+ });
+
+ let changed;
+ proxies.addEventListener("changed", function(event, proxy) {
+ changed = proxy;
+ });
+
+ let removed;
+ proxies.addEventListener("removed", function(event, proxy) {
+ removed = proxy;
+ });
+
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "CreateObject", ["/otree/other"]);
+ assert.equal(typeof added, "object", "got added object");
+ assert.equal(typeof changed, "object", "no changed object yet");
+ assert.equal(typeof removed, "undefined", "no removed object yet");
+ assert.equal(added.path, "/otree/other", "added object correct");
+ assert.strictEqual(added, changed, "added fires changed");
+
+ changed = null;
+ await dbus.call(added.path, added.iface, "RequestPropertyMods", []);
+ assert.strictEqual(changed, added, "change fired");
+
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "DeleteObject", ["/otree/other"]);
+ assert.strictEqual(removed, added, "removed fired");
+ assert.strictEqual(removed.valid, false, "removed is invalid");
+ dbus.close();
+ });
+}
+
+export function dbus_track_tests(channel_options, bus_name) {
+ QUnit.test("track name", async assert => {
+ const name = "yo.x" + new Date().getTime();
+
+ const dbus = cockpit.dbus(bus_name, channel_options);
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "ClaimOtherName", [name]);
+
+ const other = cockpit.dbus(name, {
+ bus: channel_options.bus,
+ address: channel_options.address,
+ track: true
+ });
+ let gone = false;
+ other.addEventListener("close", (_event, data) => {
+ assert.strictEqual(data.problem, undefined, "no problem");
+ gone = true;
+ });
+ await other.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["test"]);
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "ReleaseOtherName", [name]);
+ assert.strictEqual(gone, true, "close handler was called");
+ });
+
+ QUnit.test("no track name", async assert => {
+ const name = "yo.y" + new Date().getTime();
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "ClaimOtherName", [name]);
+
+ const other = cockpit.dbus(name, channel_options);
+ let gone = false;
+ other.addEventListener("close", () => { gone = true });
+
+ await other.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["test"]);
+ await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "ReleaseOtherName", [name]);
+ try {
+ await other.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["test"]);
+ assert.ok(false, "call after release should fail");
+ } catch (ex) {
+ assert.equal(ex.name, "org.freedesktop.DBus.Error.ServiceUnknown");
+ }
+ assert.equal(gone, false, "is not gone");
+ });
+
+ QUnit.test.skipWithPybridge("receive readable fd", async assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
+ const [fd] = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "MakeTestFd", ["readable"]);
+ assert.equal(typeof (fd.internal), 'string');
+ assert.equal(fd.payload, 'stream');
+
+ const channel = cockpit.channel(fd);
+
+ channel.onmessage = (_event, data) => {
+ assert.equal(data, 'Hello, fd');
+ channel.close();
+ done();
+ };
+ });
+
+ QUnit.test.skipWithPybridge("receive readable fd and ensure opening more than once fails", async assert => {
+ const done = assert.async();
+ assert.expect(6);
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
+ const [fd] = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "MakeTestFd", ["readable"]);
+ assert.equal(typeof (fd.internal), 'string');
+ assert.equal(fd.payload, 'stream');
+
+ const channel1 = cockpit.channel(fd);
+ assert.ok(channel1);
+ const channel2 = cockpit.channel(fd);
+
+ channel2.onclose = (_event, options) => {
+ assert.equal(options.channel, channel2.id);
+ assert.equal(options.command, 'close');
+ assert.equal(options.problem, 'not-found');
+ done();
+ };
+ });
+
+ QUnit.test.skipWithPybridge("receive readable fd and ensure writing fails", async assert => {
+ const done = assert.async();
+ assert.expect(5);
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
+ const [fd] = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "MakeTestFd", ["readable"]);
+ assert.equal(typeof (fd.internal), 'string');
+ assert.equal(fd.payload, 'stream');
+
+ const channel = cockpit.channel(fd);
+ channel.send('Hello, fd');
+
+ channel.onclose = (_event, options) => {
+ assert.equal(options.channel, channel.id);
+ assert.equal(options.command, 'close');
+ assert.equal(options.problem, 'protocol-error');
+ done();
+ };
+ });
+
+ QUnit.test.skipWithPybridge("receive writable fd", async assert => {
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
+ const [fd] = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "MakeTestFd", ["writable"]);
+ assert.equal(typeof (fd.internal), 'string');
+ assert.equal(fd.payload, 'stream');
+
+ const channel = cockpit.channel(fd);
+ channel.send('Hello, fd');
+ channel.close();
+ });
+}
diff --git a/pkg/base1/test-dbus-framed.js b/pkg/base1/test-dbus-framed.js
new file mode 100644
index 0000000..7ccb8e5
--- /dev/null
+++ b/pkg/base1/test-dbus-framed.js
@@ -0,0 +1,84 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+/* This is *NOT* a development guide, we take lots of shortcuts here */
+
+/* Top level window */
+function parent_window(assert) {
+ const done = assert.async();
+ assert.expect(4);
+ window.assert = assert; // for the child frame
+
+ document.getElementById("qunit-header").textContent = "Cockpit Parent Frame";
+ window.name = "cockpit1";
+ let initialized = false;
+ let frame;
+
+ cockpit.transport.filter((message, channel, control) => { // eslint-disable-line array-callback-return
+ if (initialized) {
+ /* Inject an unknown message that gets sent
+ * before the reply
+ */
+ const pos = message.indexOf('\n');
+ if (pos > -1) {
+ const json = JSON.parse(message.substring(pos));
+ if (json.reply) {
+ frame.postMessage(message.substring(0, pos) + '\n{"unknown":"unknown"}',
+ cockpit.transport.origin);
+ }
+ }
+ frame.postMessage(message, cockpit.transport.origin);
+ }
+ });
+
+ window.addEventListener("message", event => {
+ const message = event.data;
+ if (message.length === 0) {
+ done();
+ } else if (message.indexOf('"init"') !== -1) {
+ initialized = true;
+ frame.postMessage('\n{ "command": "init", "version": 1, "a": "b", "host" : "localhost" }',
+ cockpit.transport.origin);
+ } else {
+ const ret = cockpit.transport.inject(message);
+ if (!ret) console.error("inject failed");
+ }
+ }, false);
+
+ /* This keeps coming up in tests ... how to open the transport */
+ const chan = cockpit.channel({ payload: "resource2" });
+ chan.addEventListener("close", () => {
+ assert.equal(cockpit.transport.host, "localhost", "parent cockpit.transport.host");
+ const iframe = document.createElement("iframe");
+ iframe.setAttribute("name", "cockpit1:blah");
+ iframe.setAttribute("src", window.location.href + "?sub");
+ document.body.appendChild(iframe);
+ frame = window.frames["cockpit1:blah"];
+ });
+}
+
+function child_frame() {
+ const assert = window.parent.assert;
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" });
+ const promise = dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "HelloWorld", ["Browser-side JS"])
+ .then(reply => {
+ assert.equal(reply[0], "Word! You said `Browser-side JS'. I'm Skeleton, btw!", "reply");
+ dbus.close();
+ })
+ .always(() => {
+ assert.equal(promise.state(), "resolved", "finished successfully");
+ });
+ dbus.addEventListener("close", (ev, options) => {
+ assert.notOk(options.problem, "close cleanly");
+ cockpit.transport.close();
+ });
+}
+
+if (window.parent === window) {
+ QUnit.test("framed", parent_window);
+ QUnit.start();
+} else {
+ child_frame();
+}
diff --git a/pkg/base1/test-dbus.js b/pkg/base1/test-dbus.js
new file mode 100644
index 0000000..a4f4f05
--- /dev/null
+++ b/pkg/base1/test-dbus.js
@@ -0,0 +1,548 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+import { common_dbus_tests, dbus_track_tests } from "./test-dbus-common.js";
+
+/* with a name */
+const options = {
+ bus: "session"
+};
+common_dbus_tests(options, "com.redhat.Cockpit.DBusTests.Test");
+dbus_track_tests(options, "com.redhat.Cockpit.DBusTests.Test");
+
+QUnit.test("proxy no stutter", function (assert) {
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" });
+
+ const proxy = dbus.proxy();
+ assert.equal(proxy.iface, "com.redhat.Cockpit.DBusTests.Test", "interface auto chosen");
+ assert.equal(proxy.path, "/com/redhat/Cockpit/DBusTests/Test", "path auto chosen");
+});
+
+QUnit.test("proxies no stutter", function (assert) {
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" });
+
+ const proxies = dbus.proxies();
+ assert.equal(proxies.iface, "com.redhat.Cockpit.DBusTests.Test", "interface auto chosen");
+ assert.equal(proxies.path_namespace, "/", "path auto chosen");
+});
+
+QUnit.test("exposed client and options", function (assert) {
+ const options = { host: "localhost", bus: "session" };
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", options);
+ const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber");
+ const proxies = dbus.proxies("com.redhat.Cockpit.DBusTests.Frobber");
+
+ assert.deepEqual(dbus.options, options, "client object exposes options");
+ assert.strictEqual(proxy.client, dbus, "proxy object exposes client");
+ assert.strictEqual(proxies.client, dbus, "proxies object exposes client");
+});
+
+QUnit.test("subscriptions on closed client", function (assert) {
+ function on_signal() {
+ }
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" });
+ dbus.close();
+
+ const subscription = dbus.subscribe({
+ interface: "com.redhat.Cockpit.DBusTests.Frobber",
+ path: "/otree/frobber"
+ }, on_signal);
+ assert.ok(subscription, "can subscribe");
+
+ subscription.remove();
+ assert.ok(true, "can unsubscribe");
+});
+
+QUnit.test("watch promise recursive", function (assert) {
+ assert.expect(7);
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" });
+ const promise = dbus.watch("/otree/frobber");
+
+ const target = { };
+ const promise2 = promise.promise(target);
+ assert.strictEqual(promise2, target, "used target");
+ assert.equal(typeof promise2.done, "function", "promise2.done()");
+ assert.equal(typeof promise2.promise, "function", "promise2.promise()");
+ assert.equal(typeof promise2.remove, "function", "promise2.remove()");
+
+ const promise3 = promise2.promise();
+ assert.equal(typeof promise3.done, "function", "promise3.done()");
+ assert.equal(typeof promise3.promise, "function", "promise3.promise()");
+ assert.equal(typeof promise3.remove, "function", "promise3.remove()");
+});
+
+QUnit.test("owned messages", function (assert) {
+ const done = assert.async();
+ assert.expect(9);
+
+ const name = "yo.x" + new Date().getTime();
+ let times_changed = 0;
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" });
+ let other = null;
+ let org_owner = null;
+ function on_owner (event, owner) {
+ if (times_changed === 0) {
+ assert.strictEqual(typeof owner, "string", "initial owner string");
+ assert.ok(owner.length > 1, "initial owner not empty");
+ org_owner = owner;
+ } else if (times_changed === 1) {
+ assert.strictEqual(owner, null, "no owner");
+ } else if (times_changed === 2) {
+ // owner is the same because the server
+ // dbus connection is too.
+ assert.strictEqual(owner, org_owner, "has owner again");
+ }
+ times_changed++;
+ }
+
+ function acquire_name () {
+ dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "ClaimOtherName", [name])
+ .always(function() {
+ assert.equal(this.state(), "resolved", "name claimed");
+ if (!other) {
+ other = cockpit.dbus(name, { bus: "session" });
+ other.addEventListener("owner", on_owner);
+ release_name();
+ } else {
+ assert.strictEqual(times_changed, 3, "owner changed three times");
+ done();
+ }
+ });
+ }
+
+ function release_name () {
+ other.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "HelloWorld", ["test"])
+ .always(function() {
+ assert.equal(this.state(), "resolved", "called on other name");
+
+ dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "ReleaseOtherName", [name])
+ .always(function() {
+ assert.equal(this.state(), "resolved", "name released");
+ acquire_name();
+ });
+ });
+ }
+ acquire_name();
+});
+
+QUnit.test("owned message for absent service", assert => {
+ const done = assert.async();
+ assert.expect(1);
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.NotExisting", { bus: "session" });
+ dbus.addEventListener("owner", (_event, owner) => {
+ assert.strictEqual(owner, null, "no owner");
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("bad dbus address", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const dbus = cockpit.dbus(null, { bus: "none", address: "bad" });
+ dbus.addEventListener("close", (event, options) => {
+ assert.equal(options.problem, "protocol-error", "bad address closed");
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("bad dbus bus", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const dbus = cockpit.dbus(null, { bus: "bad" });
+ dbus.addEventListener("close", (event, options) => {
+ assert.equal(options.problem, "protocol-error", "bad bus format");
+ done();
+ });
+});
+
+QUnit.test("wait ready", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" });
+ dbus.wait().then(function(options) {
+ assert.ok(!!dbus.unique_name, "wait fills unique_name");
+ }, function() {
+ assert.ok(false, "shouldn't fail");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test("wait fail", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.NotExisting", { bus: "session" });
+ dbus.wait().then(function(options) {
+ assert.ok(false, "shouldn't succeed");
+ }, function() {
+ assert.ok(true, "should fail");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("no default name", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const dbus = cockpit.dbus(null, { bus: "session" });
+ dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "HelloWorld", ["Browser-side JS"], { name: "com.redhat.Cockpit.DBusTests.Test" })
+ .then(function(reply) {
+ assert.deepEqual(reply, ["Word! You said `Browser-side JS'. I'm Skeleton, btw!"], "replied");
+ }, function(ex) {
+ assert.ok(false, "shouldn't fail");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("no default name bad", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const dbus = cockpit.dbus(null, { bus: "session" });
+ dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "HelloWorld", ["Browser-side JS"], { name: 5 })
+ .then(function(reply) {
+ assert.ok(false, "shouldn't succeed");
+ }, function(ex) {
+ assert.equal(ex.problem, "protocol-error", "error problem");
+ assert.equal(ex.message, "the \"name\" field is invalid in dbus call", "error message");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("no default name invalid", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const dbus = cockpit.dbus(null, { bus: "session" });
+ dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "HelloWorld", ["Browser-side JS"], { name: "!invalid!" })
+ .then(function(reply) {
+ assert.ok(false, "shouldn't succeed");
+ }, function(ex) {
+ assert.equal(ex.problem, "protocol-error", "error problem");
+ assert.equal(ex.message, "the \"name\" field in dbus call is not a valid bus name: !invalid!", "error message");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("no default name missing", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const dbus = cockpit.dbus(null, { bus: "session" });
+ dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "HelloWorld", ["Browser-side JS"])
+ .then(function(reply) {
+ assert.ok(false, "shouldn't succeed");
+ }, function(ex) {
+ assert.equal(ex.problem, "protocol-error", "error problem");
+ assert.equal(ex.message, "the \"name\" field is missing in dbus call", "error message");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("no default name second", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const dbus = cockpit.dbus(null, { bus: "session" });
+ dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TellMeYourName", [],
+ { name: "com.redhat.Cockpit.DBusTests.Test" })
+ .then(function(reply) {
+ assert.deepEqual(reply, ["com.redhat.Cockpit.DBusTests.Test"], "right name");
+ return dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TellMeYourName", [],
+ { name: "com.redhat.Cockpit.DBusTests.Second" })
+ .then(function(reply) {
+ assert.deepEqual(reply, ["com.redhat.Cockpit.DBusTests.Second"], "second name");
+ }, function(ex) {
+ assert.ok(false, "shouldn't fail");
+ });
+ }, function(ex) {
+ console.log(ex);
+ assert.ok(false, "shouldn't fail");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("override default name", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" });
+ dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TellMeYourName", [])
+ .then(function(reply) {
+ assert.deepEqual(reply, ["com.redhat.Cockpit.DBusTests.Test"], "right name");
+ return dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TellMeYourName", [],
+ { name: "com.redhat.Cockpit.DBusTests.Second" })
+ .then(function(reply) {
+ assert.deepEqual(reply, ["com.redhat.Cockpit.DBusTests.Second"], "second name");
+ }, function(ex) {
+ assert.ok(false, "shouldn't fail");
+ });
+ }, function(ex) {
+ console.log(ex);
+ assert.ok(false, "shouldn't fail");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("watch no default name", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const cache = { };
+
+ const dbus = cockpit.dbus(null, { bus: "session" });
+ dbus.addEventListener("notify", function(event, data) {
+ Object.assign(cache, data);
+ });
+
+ dbus.watch({ path: "/otree/frobber", name: "com.redhat.Cockpit.DBusTests.Second" })
+ .then(function() {
+ assert.equal(typeof cache["/otree/frobber"], "object", "has path");
+ }, function(ex) {
+ assert.ok(false, "shouldn't fail");
+ })
+ .always(function() {
+ dbus.close();
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("watch missing name", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const dbus = cockpit.dbus(null, { bus: "session", other: "option" });
+ dbus.watch("/otree/frobber")
+ .then(function() {
+ assert.ok(false, "shouldn't succeed");
+ }, function(ex) {
+ assert.equal(ex.problem, "protocol-error", "error problem");
+ assert.equal(ex.message, "session: no \"name\" specified in match", "error message");
+ })
+ .always(function() {
+ dbus.close();
+ done();
+ });
+});
+
+QUnit.test.skipWithPybridge("shared client", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const dbus1 = cockpit.dbus(null, { bus: "session" });
+ const dbus2 = cockpit.dbus(null, { bus: "session" });
+
+ /* Is identical */
+ assert.strictEqual(dbus1, dbus2, "shared bus returned");
+
+ /* Closing shouldn't close shared */
+ dbus1.close();
+
+ dbus2.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
+ "HelloWorld", ["Browser-side JS"], { name: "com.redhat.Cockpit.DBusTests.Test" })
+ .then(function(reply) {
+ assert.deepEqual(reply, ["Word! You said `Browser-side JS'. I'm Skeleton, btw!"],
+ "call still works");
+ }, function(ex) {
+ assert.ok(false, "shouldn't fail");
+ })
+ .always(function() {
+ done();
+ });
+});
+
+QUnit.test("not shared option", function (assert) {
+ assert.expect(1);
+
+ const dbus1 = cockpit.dbus(null, { bus: "session" });
+ const dbus2 = cockpit.dbus(null, { bus: "session", other: "option" });
+
+ /* Should not be identical */
+ assert.notStrictEqual(dbus1, dbus2, "shared bus returned");
+
+ /* Closing shouldn't close shared */
+ dbus1.close();
+ dbus2.close();
+});
+
+QUnit.test.skipWithPybridge("emit signal type", function (assert) {
+ const done = assert.async();
+ assert.expect(4);
+
+ let received = false;
+ const dbus = cockpit.dbus(null, { bus: "session", other: "option" });
+ dbus.wait(function() {
+ dbus.subscribe({ path: "/bork", name: dbus.unique_name }, function(path, iface, signal, args) {
+ assert.equal(path, "/bork", "reflected path");
+ assert.equal(iface, "borkety.Bork", "reflected interface");
+ assert.equal(signal, "Bork", "reflected signal");
+ assert.deepEqual(args, [1, 2, 3, 4, "Bork"], "reflected arguments");
+ received = true;
+ dbus.close();
+ done();
+ });
+
+ dbus.addEventListener("close", function(event, ex) {
+ if (!received) {
+ console.log(ex);
+ assert.ok(false, "shouldn't fail");
+ done();
+ }
+ });
+
+ dbus.signal("/bork", "borkety.Bork", "Bork", [1, 2, 3, 4, "Bork"],
+ { type: "iiiis" });
+ });
+});
+
+QUnit.test.skipWithPybridge("emit signal no meta", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const dbus = cockpit.dbus(null, { bus: "session", other: "option" });
+
+ function closed(event, ex) {
+ assert.equal(ex.problem, "protocol-error", "correct problem");
+ assert.equal(ex.message, "signal argument types for signal borkety.Bork Bork unknown", "correct message");
+ dbus.removeEventListener("close", closed);
+ dbus.close();
+ done();
+ }
+
+ dbus.addEventListener("close", closed);
+ dbus.signal("/bork", "borkety.Bork", "Bork", [1, 2, 3, 4, "Bork"]);
+});
+
+async function internal_test(assert, options) {
+ const dbus = cockpit.dbus(null, options);
+ const resp = await dbus.call("/", "org.freedesktop.DBus.Introspectable", "Introspect");
+ assert.ok(String(resp[0]).indexOf("<node") !== -1, "introspected internal");
+}
+
+QUnit.test("internal dbus", async assert => internal_test(assert, { bus: "internal" }));
+
+QUnit.test.skipWithPybridge("internal dbus bus none",
+ async assert => internal_test(assert, { bus: "none" }));
+
+QUnit.test.skipWithPybridge("internal dbus bus none with address",
+ async assert => internal_test(assert, { bus: "none", address: "internal" }));
+
+QUnit.test.skipWithPybridge("separate dbus connections for channel groups", function (assert) {
+ const done = assert.async();
+ assert.expect(4);
+
+ const channel1 = cockpit.channel({ payload: 'dbus-json3', group: 'foo', bus: 'session' });
+ const channel2 = cockpit.channel({ payload: 'dbus-json3', group: 'bar', bus: 'session' });
+ const channel3 = cockpit.channel({ payload: 'dbus-json3', group: 'foo', bus: 'session' });
+ const channel4 = cockpit.channel({ payload: 'dbus-json3', group: 'baz', bus: 'session' });
+
+ Promise.all([
+ channel1.wait(), channel2.wait(), channel3.wait(), channel4.wait()
+ ]).then(function ([ready1, ready2, ready3, ready4]) {
+ assert.equal(ready1['unique-name'], ready3['unique-name']);
+ assert.notEqual(ready1['unique-name'], ready2['unique-name']);
+ assert.notEqual(ready1['unique-name'], ready4['unique-name']);
+ assert.notEqual(ready2['unique-name'], ready4['unique-name']);
+ done();
+ });
+});
+
+QUnit.test("cockpit.Config internal D-Bus API", async assert => {
+ const dbus = cockpit.dbus(null, { bus: "internal" });
+
+ // Get temp config dir to see where to place our test config
+ const reply = await dbus.call("/environment", "org.freedesktop.DBus.Properties", "Get",
+ ["cockpit.Environment", "Variables"]);
+ const configDir = reply[0].v.XDG_CONFIG_DIRS;
+ await cockpit.file(configDir + "/cockpit/cockpit.conf").replace(`
+[SomeSection]
+SomeA = one
+SomethingElse = 2
+LargeNum = 12345
+
+[Other]
+Flavor=chocolate
+Empty=
+`);
+ const proxy = dbus.proxy("cockpit.Config", "/config");
+ await proxy.wait();
+ await proxy.Reload();
+
+ // test GetString()
+ assert.equal(await proxy.GetString("SomeSection", "SomeA"), "one");
+ assert.equal(await proxy.GetString("Other", "Empty"), "");
+
+ // test GetUInt()
+
+ // this key exists, ignores default
+ assert.equal(await proxy.GetUInt("SomeSection", "SomethingElse", 10, 100, 0), 2);
+ // this key does not exist, return default
+ assert.equal(await proxy.GetUInt("SomeSection", "NotExisting", 10, 100, 0), 10);
+ // out of bounds, clamp to minimum
+ assert.equal(await proxy.GetUInt("SomeSection", "SomethingElse", 42, 50, 5), 5);
+ // out of bounds, clamp to maximum
+ assert.equal(await proxy.GetUInt("SomeSection", "LargeNum", 42, 50, 5), 50);
+ // not an integer value, returns default
+ assert.equal(await proxy.GetUInt("SomeSection", "SomeA", 10, 100, 0), 10);
+
+ // test GetString with non-existing section
+ assert.rejects(proxy.GetString("UnknownSection", "SomeKey"),
+ /key.*UnknownSection.*not exist/,
+ "unknown section raises an error");
+
+ // test GetString with non-existing key in existing section
+ assert.rejects(proxy.GetString("SomeSection", "UnknownKey"),
+ /key.*UnknownKey.*not exist/,
+ "unknown key raises an error");
+});
+
+QUnit.test("nonexisting address", async assert => {
+ const dbus = cockpit.dbus("org.freedesktop.DBus", { address: "unix:path=/nonexisting", bus: "none" });
+
+ try {
+ await dbus.call("/org/freedesktop/DBus", "org.freedesktop.DBus", "Hello", []);
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ if (await QUnit.mock_info("pybridge")) {
+ assert.equal(ex.problem, "protocol-error", "got right close code");
+ assert.equal(ex.message, "failed to connect to none bus: [Errno 2] sd_bus_start: No such file or directory",
+ "error message");
+ } else {
+ // C bridge has a weird error code
+ assert.equal(ex.problem, "internal-error", "got right close code");
+ assert.equal(ex.message, "Could not connect: No such file or directory", "error message");
+ }
+ }
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-echo.js b/pkg/base1/test-echo.js
new file mode 100644
index 0000000..d876bb2
--- /dev/null
+++ b/pkg/base1/test-echo.js
@@ -0,0 +1,125 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("basic", function (assert) {
+ const done = assert.async();
+ assert.expect(4);
+
+ const channel = cockpit.channel({ payload: "echo" });
+ let pass = 0;
+
+ const onControl = (ev, options) => {
+ if (pass === 0) {
+ assert.equal(options.command, "ready", "got ready");
+ pass += 1;
+ } else {
+ assert.equal(options.command, "done", "got done");
+ channel.close();
+ channel.removeEventListener("control", onControl);
+ done();
+ }
+ };
+ channel.addEventListener("control", onControl);
+
+ channel.addEventListener("message", (ev, payload) => {
+ assert.strictEqual(payload, "the payload", "got the right payload");
+ channel.control({ command: "done" });
+ });
+
+ assert.strictEqual(channel.binary, false, "not binary");
+ channel.send("the payload");
+});
+
+QUnit.test("binary empty", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const channel = cockpit.channel({
+ payload: "echo",
+ binary: true
+ });
+
+ const onMessage = (ev, payload) => {
+ assert.ok(payload instanceof Uint8Array, "got a byte array");
+ assert.strictEqual(payload.length, 0, "got the right payload");
+ channel.removeEventListener("message", onMessage);
+ done();
+ };
+ channel.addEventListener("message", onMessage);
+
+ channel.send("");
+});
+
+QUnit.test("binary", function (assert) {
+ const done = assert.async();
+ assert.expect(3);
+
+ const channel = cockpit.channel({ payload: "echo", binary: true });
+
+ const onMessage = (ev, payload) => {
+ assert.ok(payload instanceof Uint8Array, "got a byte array");
+
+ const array = [];
+ for (let i = 0; i < payload.length; i++)
+ array.push(payload[i]);
+ assert.deepEqual(array, [0, 1, 2, 3, 4, 5, 6, 7], "got back right data");
+
+ channel.close();
+ channel.removeEventListener("message", onMessage);
+ done();
+ };
+ channel.addEventListener("message", onMessage);
+
+ const buffer = new ArrayBuffer(8);
+ const view = new Uint8Array(buffer);
+ for (let i = 0; i < 8; i++)
+ view[i] = i;
+
+ assert.strictEqual(channel.binary, true, "binary set");
+ channel.send(buffer);
+});
+
+QUnit.test("fence", async assert => {
+ const done = assert.async();
+
+ // This is implemented in the C bridge, but not in Python.
+ if (await QUnit.mock_info("pybridge")) {
+ assert.ok(true, "skipping on python bridge, not implemented");
+ done();
+ return;
+ }
+
+ assert.expect(2);
+
+ const before = cockpit.channel({ payload: "echo" });
+ before.addEventListener("message", onMessage);
+
+ const fence = cockpit.channel({ payload: "echo", group: "fence" });
+ fence.addEventListener("message", onMessage);
+
+ const after = cockpit.channel({ payload: "echo" });
+ after.addEventListener("message", onMessage);
+
+ const received = [];
+ function onMessage(ev, payload) {
+ received.push(payload);
+ if (received.length == 3) {
+ assert.deepEqual(received, ["1", "2", "3"], "got back before and fence data");
+ fence.close();
+ } else if (received.length == 5) {
+ assert.deepEqual(received, ["1", "2", "3", "4", "5"], "got back data in right order");
+ before.close();
+ after.close();
+ done();
+ }
+ }
+
+ /* We send messages in this order, but they should echoed in numeric order */
+ before.send("1");
+ after.send("4");
+ after.send("5");
+ fence.send("2");
+ fence.send("3");
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-events.js b/pkg/base1/test-events.js
new file mode 100644
index 0000000..9b4a31f
--- /dev/null
+++ b/pkg/base1/test-events.js
@@ -0,0 +1,49 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("event dispatch", function (assert) {
+ const obj = { };
+ cockpit.event_target(obj);
+
+ let count = 0;
+ function handler(ev) {
+ if (count === 0) {
+ assert.equal(typeof ev, "object", "event is object");
+ assert.equal(ev.type, "action", "event.type is 'action'");
+ assert.equal(ev.data, "Data", "event.data is set");
+ }
+ count += 1;
+ }
+
+ const ev = new Event("action", { bubbles: false, cancelable: false });
+ ev.data = "Data";
+
+ obj.dispatchEvent(ev);
+ assert.strictEqual(count, 0, "count is zero");
+
+ obj.onaction = handler;
+ obj.dispatchEvent(ev);
+ assert.strictEqual(count, 1, "count is one");
+
+ obj.addEventListener("action", handler, true);
+ obj.dispatchEvent(ev);
+ assert.strictEqual(count, 3, "count is three");
+
+ obj.addEventListener("action", handler, true);
+ obj.dispatchEvent(ev);
+ assert.strictEqual(count, 6, "count is six");
+
+ obj.removeEventListener("action", handler, true);
+ obj.dispatchEvent(ev);
+ assert.strictEqual(count, 8, "count is eight");
+
+ obj.onaction = null;
+ obj.dispatchEvent(ev);
+ assert.strictEqual(count, 9, "count is nine");
+
+ obj.removeEventListener("action", handler, true);
+ obj.dispatchEvent(ev);
+ assert.strictEqual(count, 9, "count is still nine");
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-external.js b/pkg/base1/test-external.js
new file mode 100644
index 0000000..efaebd7
--- /dev/null
+++ b/pkg/base1/test-external.js
@@ -0,0 +1,162 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("external get", function (assert) {
+ const done = assert.async();
+ assert.expect(4);
+
+ /* The query string used to open the channel */
+ const query = window.btoa(JSON.stringify({
+ payload: "fslist1",
+ path: "/tmp",
+ watch: false
+ }));
+
+ const req = new XMLHttpRequest();
+ req.open("GET", "/cockpit/channel/" + cockpit.transport.csrf_token + '?' + query);
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ assert.equal(req.status, 200, "got right status");
+ assert.equal(req.statusText, "OK", "got right reason");
+ assert.equal(req.getResponseHeader("Content-Type"), "application/octet-stream", "default type");
+ assert.ok(req.responseText.indexOf('"present"'), "got listing");
+ done();
+ }
+ };
+ req.send();
+});
+
+QUnit.test("external fsread1", async assert => {
+ const done = assert.async();
+ assert.expect(5);
+ const resp = await cockpit.spawn(["stat", "--format", "%s", "/usr/lib/os-release"]);
+ const filesize = resp.replace(/\n$/, "");
+
+ /* The query string used to open the channel */
+ const query = window.btoa(JSON.stringify({
+ payload: "fsread1",
+ path: '/usr/lib/os-release',
+ binary: "raw",
+ external: {
+ "content-disposition": 'attachment; filename="foo"',
+ "content-type": "application/octet-stream",
+ }
+ }));
+
+ const req = new XMLHttpRequest();
+ req.open("GET", "/cockpit/channel/" + cockpit.transport.csrf_token + '?' + query);
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ assert.equal(req.status, 200, "got right status");
+ assert.equal(req.statusText, "OK", "got right reason");
+ assert.equal(req.getResponseHeader("Content-Type"), "application/octet-stream", "default type");
+ assert.equal(req.getResponseHeader("Content-Disposition"), 'attachment; filename="foo"', "default type");
+ assert.equal(req.getResponseHeader("Content-Length"), parseInt(filesize), "expected file size");
+ done();
+ }
+ };
+ req.send();
+});
+
+QUnit.test("external headers", function (assert) {
+ const done = assert.async();
+ assert.expect(3);
+
+ const query = window.btoa(JSON.stringify({
+ payload: "fslist1",
+ path: "/tmp",
+ watch: false,
+ external: {
+ "content-disposition": "my disposition; blah",
+ "content-type": "test/blah",
+ },
+ }));
+
+ const req = new XMLHttpRequest();
+ req.open("GET", "/cockpit/channel/" + cockpit.transport.csrf_token + '?' + query);
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ assert.equal(this.status, 200, "got right status");
+ assert.equal(this.getResponseHeader("Content-Type"), "test/blah", "got type");
+ assert.equal(this.getResponseHeader("Content-Disposition"), "my disposition; blah", "got disposition");
+ done();
+ }
+ };
+ req.send();
+});
+
+QUnit.test("external invalid", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const req = new XMLHttpRequest();
+ req.open("GET", "/cockpit/channel/invalid");
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ assert.equal(this.status, 404, "got not found");
+ done();
+ }
+ };
+ req.send();
+});
+
+QUnit.test("external no token", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ /* The query string used to open the channel */
+ const query = window.btoa(JSON.stringify({
+ payload: "fslist1",
+ path: "/tmp",
+ watch: false
+ }));
+
+ const req = new XMLHttpRequest();
+ req.open("GET", "/cockpit/channel/?" + query);
+ req.onreadystatechange = function() {
+ if (req.readyState == 4) {
+ assert.equal(this.status, 404, "got not found");
+ done();
+ }
+ };
+ req.send();
+});
+
+QUnit.test("external websocket", function (assert) {
+ const done = assert.async();
+ assert.expect(3);
+
+ const query = window.btoa(JSON.stringify({
+ payload: "echo"
+ }));
+
+ let count = 0;
+ const ws = new WebSocket("ws://" + window.location.host + "/cockpit/channel/" +
+ cockpit.transport.csrf_token + '?' + query, "protocol-unused");
+ ws.onopen = function() {
+ assert.ok(true, "websocket is open");
+ ws.send("oh marmalade");
+ };
+ ws.onerror = function() {
+ assert.ok(false, "websocket error");
+ };
+ ws.onmessage = function(ev) {
+ if (count === 0) {
+ assert.equal(ev.data, "oh marmalade", "got payload");
+ ws.send("another test");
+ count += 1;
+ } else {
+ assert.equal(ev.data, "another test", "got payload again");
+ ws.close(1000);
+ }
+ };
+ ws.onclose = function() {
+ done();
+ };
+});
+
+cockpit.transport.wait(function() {
+ /* Tell tap driver not to worry about HTTP failures past this point */
+ console.log("cockpittest-tap-expect-resource-error");
+ QUnit.start();
+});
diff --git a/pkg/base1/test-file.js b/pkg/base1/test-file.js
new file mode 100644
index 0000000..eab74cc
--- /dev/null
+++ b/pkg/base1/test-file.js
@@ -0,0 +1,432 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+let dir;
+
+QUnit.test("simple read", async assert => {
+ const file = cockpit.file(dir + "/foo");
+ assert.equal(file.path, dir + "/foo", "file has path");
+ assert.equal(await file.read(), "1234\n", "correct result");
+});
+
+QUnit.test("read non-existent", async assert => {
+ assert.equal(await cockpit.file(dir + "/blah").read(), null, "correct result");
+});
+
+QUnit.test("parse read", async assert => {
+ const resp = await cockpit.file(dir + "/foo.json", { syntax: JSON }).read();
+ assert.deepEqual(resp, { foo: 12 }, "correct result");
+});
+
+QUnit.test("parse read error", async assert => {
+ try {
+ await cockpit.file(dir + "/foo.bin", { syntax: JSON }).read();
+ assert.ok(false, "should have failed");
+ } catch (error) {
+ assert.ok(error instanceof SyntaxError, "got SyntaxError error");
+ }
+});
+
+QUnit.test("binary read", async assert => {
+ const resp = await cockpit.file(dir + "/foo.bin", { binary: true }).read();
+ assert.deepEqual(resp, new Uint8Array([0, 1, 2, 3]), "correct result");
+});
+
+QUnit.test("read non-regular", async assert => {
+ try {
+ await cockpit.file(dir, { binary: true }).read();
+ assert.ok(false, "should have failed");
+ } catch (error) {
+ assert.equal(error.problem, "internal-error", "got error");
+ }
+});
+
+QUnit.test("read large", async assert => {
+ const resp = await cockpit.file(dir + "/large.bin", { binary: true }).read();
+ assert.equal(resp.length, 512 * 1024, "correct result");
+});
+
+QUnit.test("read too large", async assert => {
+ try {
+ await cockpit.file(dir + "/large.bin", { binary: true, max_read_size: 8 * 1024 }).read();
+ assert.ok(false, "should have failed");
+ } catch (error) {
+ assert.equal(error.problem, "too-large", "got error");
+ }
+});
+
+/* regression: passing 'binary: false' made cockpit-ws close the whole connection */
+QUnit.test("binary false", async assert => {
+ await cockpit.file(dir + "/foo.bin", { binary: false }).read();
+ assert.ok(true, "did not crash");
+});
+
+QUnit.test("simple replace", async assert => {
+ await cockpit.file(dir + "/bar").replace("4321\n");
+ const res = await cockpit.spawn(["cat", dir + "/bar"]);
+ assert.equal(res, "4321\n", "correct content");
+});
+
+QUnit.test("stringify replace", async assert => {
+ await cockpit.file(dir + "/bar", { syntax: JSON }).replace({ foo: 4321 });
+ const res = await cockpit.spawn(["cat", dir + "/bar"]);
+ assert.deepEqual(JSON.parse(res), { foo: 4321 }, "correct content");
+});
+
+QUnit.test("stringify replace error", async assert => {
+ const cycle = { };
+ cycle.me = cycle;
+ try {
+ await cockpit.file(dir + "/bar", { syntax: JSON }).replace(cycle);
+ assert.ok(false, "should have failed");
+ } catch (error) {
+ assert.ok(error instanceof TypeError, "got stringify error");
+ }
+});
+
+QUnit.test("binary replace", async assert => {
+ await cockpit.file(dir + "/bar", { binary: true }).replace(new Uint8Array([3, 2, 1, 0]));
+ const res = await cockpit.spawn(["cat", dir + "/bar"], { binary: true });
+ assert.deepEqual(res, new Uint8Array([3, 2, 1, 0]), "correct content");
+});
+
+QUnit.test("replace large", async assert => {
+ const str = new Array(23 * 1023).join('abcdef12345');
+ await cockpit.file(dir + "/large").replace(str);
+ const res = await cockpit.spawn(["cat", dir + "/large"]);
+ assert.equal(res.length, str.length, "correct large length");
+ assert.ok(res == str, "correct large data");
+});
+
+QUnit.test("binary replace large", async assert => {
+ const data = new Uint8Array(249 * 1023);
+ const len = data.byteLength;
+ for (let i = 0; i < len; i++)
+ data[i] = i % 233;
+ await cockpit.file(dir + "/large-binary", { binary: true }).replace(data);
+ const res = await cockpit.spawn(["cat", dir + "/large-binary"], { binary: true });
+ let eq = true;
+ assert.equal(res.byteLength, 249 * 1023, "check length");
+ assert.equal(res.byteLength, data.byteLength, "correct large length");
+ for (let i = 0; i < res.byteLength; i++) {
+ if (res[i] !== data[i] || res[i] === undefined) {
+ eq = false;
+ break;
+ }
+ }
+ assert.ok(eq, "got back same data");
+});
+
+QUnit.test("remove", async assert => {
+ const exists = await cockpit.spawn(["bash", "-c", "test -f " + dir + "/bar && echo exists"]);
+ assert.equal(exists, "exists\n", "exists");
+ await cockpit.file(dir + "/bar").replace(null);
+ const res = await cockpit.spawn(["bash", "-c", "test -f " + dir + "/bar || echo gone"]);
+ assert.equal(res, "gone\n", "gone");
+});
+
+QUnit.test("abort replace", assert => {
+ const done = assert.async();
+ assert.expect(2);
+
+ const file = cockpit.file(dir + "/bar");
+
+ let n = 0;
+ function start_after_two() {
+ n += 1;
+ if (n == 2)
+ done();
+ }
+
+ file.replace("1234\n")
+ .always(function () {
+ assert.equal(this.state(), "rejected", "failed as expected");
+ start_after_two();
+ });
+
+ file.replace("abcd\n")
+ .always(function () {
+ assert.equal(this.state(), "resolved", "didn't fail");
+ start_after_two();
+ });
+});
+
+QUnit.test("replace with tag", async assert => {
+ const done = assert.async();
+ const file = cockpit.file(dir + "/barfoo");
+
+ file.read()
+ .then(async (content, tag_1) => {
+ assert.equal(content, null, "file does not exist");
+ assert.equal(tag_1, "-", "first tag is -");
+ const tag_2 = await file.replace("klmn\n", tag_1);
+ assert.notEqual(tag_2, "-", "second tag is not -");
+ const tag_3 = await file.replace("KLMN\n", tag_2);
+ assert.notEqual(tag_3, tag_2, "third tag is different");
+ try {
+ await file.replace("opqr\n", tag_2);
+ assert.ok(false, "should have failed");
+ } catch (error) {
+ assert.equal(error.problem, "change-conflict", "wrong tag is rejected");
+ done();
+ }
+ });
+});
+
+QUnit.test("modify", async assert => {
+ const file = cockpit.file(dir + "/quux");
+
+ let n = 0;
+ await file.modify(old => {
+ n += 1;
+ assert.equal(old, null, "no old content");
+ return "ABCD\n";
+ });
+ assert.equal(n, 1, "callback called once");
+
+ n = 0;
+ await file.modify(old => {
+ n += 1;
+ assert.equal(old, "ABCD\n", "correct old content");
+ return "dcba\n";
+ });
+ assert.equal(n, 1, "callback called once");
+
+ assert.equal(await cockpit.spawn(["cat", dir + "/quux"]), "dcba\n", "correct content");
+});
+
+QUnit.test("modify with conflict", async assert => {
+ const done = assert.async();
+ assert.expect(6);
+
+ const file = cockpit.file(dir + "/baz");
+
+ let n = 0;
+ file
+ .modify(old => {
+ n += 1;
+ assert.equal(old, null, "no old content");
+ return "ABCD\n";
+ })
+ .finally(() => assert.equal(n, 1, "callback called once"))
+
+ .then(async (content, tag) => {
+ let n = 0;
+ await cockpit.spawn(["bash", "-c", "sleep 1; echo XYZ > " + dir + "/baz"]);
+ await file.modify(old => {
+ n += 1;
+ if (n == 1)
+ assert.equal(old, "ABCD\n", "correct old (out-of-date) content");
+ else
+ assert.equal(old, "XYZ\n", "correct old (current) content");
+ return old.toLowerCase();
+ }, content, tag);
+
+ assert.equal(n, 2, "callback called twice");
+ const res = await cockpit.spawn(["cat", dir + "/baz"]);
+ assert.equal(res, "xyz\n", "correct content");
+ done();
+ });
+});
+
+QUnit.test("watching", assert => {
+ const done = assert.async();
+ assert.expect(7);
+
+ const file = cockpit.file(dir + "/foobar");
+ let n = 0;
+ const watch = file.watch((content, tag) => {
+ n += 1;
+ if (n == 1) {
+ assert.equal(content, null, "initially non-existent");
+ assert.equal(tag, "-", "empty tag");
+ cockpit.spawn(["bash", "-c", "cd " + dir + " && echo 1234 > foobar.tmp && mv foobar.tmp foobar"]);
+ } else if (n == 2) {
+ assert.equal(content, "1234\n", "correct new content");
+ assert.notEqual(tag, "-");
+ assert.ok(tag.length > 5, "tag has a reasonable size");
+ cockpit.spawn(["bash", "-c", "rm " + dir + "/foobar"]);
+ } else if (n == 3) {
+ assert.equal(content, null, "finally non-existent");
+ assert.equal(tag, "-", "empty tag");
+ watch.remove();
+ done();
+ }
+ });
+});
+
+QUnit.test("binary watching", assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ const file = cockpit.file(dir + "/foobar", { binary: true });
+ let n = 0;
+ const watch = file.watch((content, tag) => {
+ n += 1;
+ if (n == 1) {
+ assert.equal(content, null, "initially non-existent");
+ cockpit.spawn(["bash", "-c", "cd " + dir + " && echo '//8BAg==' | base64 -d > foobar.tmp && mv foobar.tmp foobar"]);
+ } else if (n == 2) {
+ assert.deepEqual(content, new Uint8Array([255, 255, 1, 2]), "correct new content");
+ cockpit.spawn(["bash", "-c", "rm " + dir + "/foobar"]);
+ } else if (n == 3) {
+ assert.equal(content, null, "finally non-existent");
+ watch.remove();
+ done();
+ }
+ });
+});
+
+QUnit.test("syntax watching", assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ const file = cockpit.file(dir + "/foobar.json", { syntax: JSON });
+ let n = 0;
+ const watch = file.watch((content, tag, err) => {
+ n += 1;
+ if (n == 1) {
+ assert.equal(content, null, "initially non-existent");
+ cockpit.spawn(["bash", "-c", "cd " + dir + " && echo '[ 1, 2, 3, 4 ]' > foobar.json.tmp && mv foobar.json.tmp foobar.json"]);
+ } else if (n == 2) {
+ assert.deepEqual(content, [1, 2, 3, 4], "correct new content");
+ cockpit.spawn(["bash", "-c", "echo 'hi-there-this-is-not-json' > " + dir + "/foobar.json"]);
+ } else if (n == 3) {
+ assert.ok(err instanceof SyntaxError, "got SyntaxError error");
+ watch.remove();
+ done();
+ } else
+ assert.ok(false, "not reached");
+ });
+});
+
+QUnit.test("watching without reading", assert => {
+ const done = assert.async();
+ assert.expect(7);
+
+ const file = cockpit.file(dir + "/foobar");
+ let n = 0;
+ const watch = file.watch((content, tag) => {
+ n += 1;
+ if (n == 1) {
+ assert.equal(content, null, "initially non-existent");
+ assert.equal(tag, "-", "empty tag");
+ cockpit.spawn(["bash", "-c", "cd " + dir + " && echo 1234 > foobar.tmp && mv foobar.tmp foobar"]);
+ } else if (n == 2) {
+ assert.equal(content, null, "no content as reading is disabled");
+ assert.notEqual(tag, "-");
+ assert.ok(tag.length > 5, "tag has a reasonable size");
+ cockpit.spawn(["bash", "-c", "rm " + dir + "/foobar"]);
+ } else if (n == 3) {
+ assert.equal(content, null, "finally non-existent");
+ assert.equal(tag, "-", "empty tag");
+ watch.remove();
+ done();
+ }
+ }, { read: false });
+});
+
+QUnit.test("watching directory", assert => {
+ const done = assert.async();
+ assert.expect(20);
+
+ let n = 0;
+ const watch = cockpit.channel({ payload: "fswatch1", path: dir });
+ watch.addEventListener("message", (event, payload) => {
+ const msg = JSON.parse(payload);
+ n += 1;
+
+ if (n == 1) {
+ assert.equal(msg.event, "created", "world.txt created");
+ assert.equal(msg.path, dir + "/world.txt");
+ assert.equal(msg.type, "file");
+ assert.notEqual(msg.tag, "-");
+ } else if (n == 2) {
+ assert.equal(msg.event, "changed", "world.txt changed");
+ assert.equal(msg.path, dir + "/world.txt");
+ assert.notEqual(msg.tag, "-");
+ } else if (n == 3) {
+ assert.equal(msg.event, "done-hint", "world.txt done-hint");
+ assert.equal(msg.path, dir + "/world.txt");
+ assert.notEqual(msg.tag, "-");
+
+ cockpit.spawn(["chmod", "001", `${dir}/world.txt`]);
+ } else if (n == 4) {
+ assert.equal(msg.event, "attribute-changed", "world.txt attribute-changed");
+ assert.equal(msg.path, dir + "/world.txt");
+ assert.notEqual(msg.tag, "-");
+
+ cockpit.spawn(["rm", `${dir}/world.txt`]);
+ } else if (n == 5) {
+ assert.equal(msg.event, "deleted", "world.txt deleted");
+ assert.equal(msg.path, dir + "/world.txt");
+ assert.equal(msg.tag, "-");
+
+ cockpit.spawn(["mkdir", `${dir}/somedir`]);
+ } else if (n == 6) {
+ assert.equal(msg.event, "created", "somedir created");
+ assert.equal(msg.path, dir + "/somedir");
+ assert.equal(msg.type, "directory");
+ assert.notEqual(msg.tag, "-");
+
+ watch.close();
+ done();
+ }
+ });
+
+ // trigger the first event
+ cockpit.spawn(["sh", "-c", `echo hello > ${dir}/world.txt`]);
+});
+
+QUnit.test("closing", assert => {
+ const done = assert.async();
+ assert.expect(2);
+
+ const file = cockpit.file(dir + "/foobarbaz");
+ const watch = file.watch(changed);
+
+ let n = 0;
+ function start_after_two() {
+ n += 1;
+ if (n == 2) {
+ watch.remove();
+ done();
+ }
+ }
+
+ file.read()
+ .then((content, tag) => {
+ assert.ok(false, "read didn't complete");
+ })
+ .catch(error => {
+ assert.equal(error.problem, "cancelled", "read got cancelled");
+ start_after_two();
+ });
+
+ function changed(content, tag, err) {
+ if (err) {
+ assert.equal(err.problem, "cancelled", "watch got cancelled");
+ start_after_two();
+ } else {
+ assert.ok(false, "not reached");
+ }
+ }
+
+ file.close();
+});
+
+QUnit.test("channel options", async assert => {
+ const data = await cockpit.file(dir + "/foo", { binary: true }).read();
+ assert.ok(data instanceof Uint8Array, "options applied, got binary data");
+});
+
+QUnit.test("remove testdir", async assert => {
+ await cockpit.spawn(["rm", "-rf", dir]);
+ assert.ok(true, "did not crash");
+});
+
+(async () => {
+ const resp = await cockpit.spawn(["bash", "-c", "d=$(mktemp -d); echo '1234' >$d/foo; echo '{ \"foo\": 12 }' >$d/foo.json; echo -en '\\x00\\x01\\x02\\x03' >$d/foo.bin; dd if=/dev/zero of=$d/large.bin bs=1k count=512; echo $d"]);
+ dir = resp.replace(/\n$/, "");
+ QUnit.start();
+})();
diff --git a/pkg/base1/test-format.js b/pkg/base1/test-format.js
new file mode 100644
index 0000000..0294fcd
--- /dev/null
+++ b/pkg/base1/test-format.js
@@ -0,0 +1,207 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("format", function (assert) {
+ assert.equal(cockpit.format("My $adj message with ${amount} of things", { adj: "special", amount: "lots" }),
+ "My special message with lots of things", "named keys");
+ assert.equal(cockpit.format("My $0 message with $1 of things", ["special", "lots"]),
+ "My special message with lots of things", "number keys");
+ assert.equal(cockpit.format("My $0 message with $1 of things", "special", "lots"),
+ "My special message with lots of things", "vararg keys");
+ assert.equal(cockpit.format("My $0 message with lots of things", "special"),
+ "My special message with lots of things", "vararg one key");
+ assert.equal(cockpit.format("Undefined $value", { }), "Undefined ", "missing value");
+
+ /* All falsy values except `0` should return the empty string */
+ assert.equal(cockpit.format("$0", 0), "0", "`0` as argument");
+ assert.equal(cockpit.format("$0", 0.0), "0", "`0.0` as argument");
+ assert.equal(cockpit.format("$0", false), "", "`false` as argument");
+ assert.equal(cockpit.format("$0", null), "", "`null` as argument");
+});
+
+QUnit.test("format_number", function (assert) {
+ const checks = [
+ [23.4, "23.4", "23,4"],
+ [23.46, "23.5", "23,5"],
+ [23.44, "23.4", "23,4"],
+
+ [-23.4, "-23.4", "-23,4"],
+ [-23.46, "-23.5", "-23,5"],
+ [-23.44, "-23.4", "-23,4"],
+
+ [0, "0", "0"],
+ [0.001, "0.001", "0,001"],
+ [-0.001, "-0.001", "-0,001"],
+ // smaller values get rounded up
+ [0.0003, "0.001", "0,001"],
+ [-0.0003, "-0.001", "-0,001"],
+
+ [123.0, "123", "123"],
+ [123.01, "123", "123"],
+ [-123.0, "-123", "-123"],
+ [-123.01, "-123", "-123"],
+ [null, "", ""],
+ [undefined, "", ""],
+ ];
+
+ const saved_language = cockpit.language;
+
+ assert.expect(checks.length * 3 + 7);
+
+ cockpit.language = 'en';
+ for (let i = 0; i < checks.length; i++) {
+ assert.strictEqual(cockpit.format_number(checks[i][0]), checks[i][1],
+ "format_number@en(" + checks[i][0] + ") = " + checks[i][1]);
+ }
+
+ cockpit.language = 'de';
+ for (let i = 0; i < checks.length; i++) {
+ assert.strictEqual(cockpit.format_number(checks[i][0]), checks[i][2],
+ "format_number@de(" + checks[i][0] + ") = " + checks[i][2]);
+ }
+
+ cockpit.language = 'pt_BR';
+ for (let i = 0; i < checks.length; i++) {
+ assert.strictEqual(cockpit.format_number(checks[i][0]), checks[i][2],
+ "format_number@pt_BR(" + checks[i][0] + ") = " + checks[i][2]);
+ }
+
+ /* restore this as not to break the other tests */
+ cockpit.language = saved_language;
+
+ // custom precision
+ assert.strictEqual(cockpit.format_number(1.23456, 2), "1.2", "format_number@en(precision 2)");
+ assert.strictEqual(cockpit.format_number(-1.23456, 2), "-1.2", "format_number@en(negative, precision 2)");
+ assert.strictEqual(cockpit.format_number(0.23456, 2), "0.23", "format_number@en(precision 2)");
+ assert.strictEqual(cockpit.format_number(1.23456, 4), "1.235", "format_number@en(precision 4)");
+ assert.strictEqual(cockpit.format_number(0.23456, 4), "0.2346", "format_number@en(precision 4)");
+ assert.strictEqual(cockpit.format_number(0.000123, 2), "0.01", "format_number@en(very small, precision 2)");
+ assert.strictEqual(cockpit.format_number(-0.000123, 2), "-0.01", "format_number@en(negative, very small, precision 2)");
+});
+
+QUnit.test("format_bytes", function (assert) {
+ const checks = [
+ [999, 1000, "999"],
+ [1934, undefined, "1.93 KB"],
+ [1934, 1000, "1.93 KB"],
+ [2000, 1024, "1.95 KiB"],
+ [1999, 1000, "2.00 KB"],
+ [1999, 1024, "1.95 KiB"],
+ [1000000, 1000, "1 MB"],
+ [1000001, 1000, "1.00 MB"],
+ [1000000, 1024, "977 KiB"],
+ [2000000, 1024, "1.91 MiB"],
+ [2000000, 1000, "2 MB"],
+ [2000001, 1000, "2.00 MB"],
+ [2000000, "MB", "2 MB"],
+ [2000000, "MiB", "1.91 MiB"],
+ [2000000, "KB", "2000 KB"],
+ [2000000, "KiB", "1953 KiB"],
+ [1, "KB", "0.001 KB"],
+ [0, "KB", "0 KB"],
+ [undefined, "KB", ""],
+ [null, "KB", ""],
+ ];
+
+ assert.expect(checks.length * 2 + 2);
+ for (let i = 0; i < checks.length; i++) {
+ assert.strictEqual(cockpit.format_bytes(checks[i][0], checks[i][1]), checks[i][2],
+ "format_bytes(" + checks[i][0] + ", " + String(checks[i][1]) + ") = " + checks[i][2]);
+ }
+ for (let i = 0; i < checks.length; i++) {
+ const split = checks[i][2].split(" ");
+ assert.deepEqual(cockpit.format_bytes(checks[i][0], checks[i][1], { separate: true }), split,
+ "format_bytes(" + checks[i][0] + ", " + String(checks[i][1]) + ", true) = " + split);
+ }
+
+ // backwards compatible API: format_bytes with a boolean options (used to be a single "separate" flag)
+ assert.strictEqual(cockpit.format_bytes(2500000, 1000, false), "2.50 MB");
+ assert.deepEqual(cockpit.format_bytes(2500000, 1000, true), ["2.50", "MB"]);
+});
+
+QUnit.test("get_byte_units", function (assert) {
+ const mib = 1024 * 1024;
+ const gib = mib * 1024;
+ const tib = gib * 1024;
+
+ const mib_unit = { factor: mib, name: "MiB" };
+ const gib_unit = { factor: gib, name: "GiB" };
+ const tib_unit = { factor: tib, name: "TiB" };
+
+ function selected(unit) {
+ return { factor: unit.factor, name: unit.name, selected: true };
+ }
+
+ const checks = [
+ [0 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
+ [20 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
+ [200 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
+ [2000 * mib, 1024, [selected(mib_unit), gib_unit, tib_unit]],
+ [20000 * mib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
+ [20 * gib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
+ [200 * gib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
+ [2000 * gib, 1024, [mib_unit, selected(gib_unit), tib_unit]],
+ [20000 * gib, 1024, [mib_unit, gib_unit, selected(tib_unit)]]
+ ];
+
+ assert.expect(checks.length);
+ for (let i = 0; i < checks.length; i++) {
+ assert.deepEqual(cockpit.get_byte_units(checks[i][0], checks[i][1]), checks[i][2],
+ "get_byte_units(" + checks[i][0] + ", " + checks[i][1] + ") = " + JSON.stringify(checks[i][2]));
+ }
+});
+
+QUnit.test("format_bytes_per_sec", function (assert) {
+ const checks = [
+ // default unit
+ [5, undefined, undefined, "5 B/s"],
+ [2555, undefined, undefined, "2.56 kB/s"],
+ [12345678, undefined, undefined, "12.3 MB/s"],
+ // explicit base-2 unit
+ [2555, 1024, undefined, "2.50 KiB/s"],
+ // explicit base-10 unit
+ [2555, 1000, undefined, "2.56 kB/s"],
+ [12345678, 1000, undefined, "12.3 MB/s"],
+ // explicit unit
+ [12345678, "kB/s", undefined, "12346 kB/s"],
+ [12345678, "MiB/s", undefined, "11.8 MiB/s"],
+ // custom precision
+ [2555, 1000, { precision: 2 }, "2.6 kB/s"],
+ [25555, "MB/s", { precision: 2 }, "0.026 MB/s"],
+ // significant integer digits exceed custom precision
+ [25555000, "kB/s", { precision: 2 }, "25555 kB/s"],
+ [25555678, "kB/s", { precision: 2 }, "25556 kB/s"],
+ ];
+
+ assert.expect(checks.length + 2);
+ for (let i = 0; i < checks.length; i++) {
+ assert.strictEqual(cockpit.format_bytes_per_sec(checks[i][0], checks[i][1], checks[i][2]), checks[i][3],
+ `format_bytes_per_sec(${checks[i][0]}, ${checks[i][1]}, ${checks[i][2]}) = ${checks[i][3]}`);
+ }
+
+ // separate unit
+ assert.deepEqual(cockpit.format_bytes_per_sec(2555, 1024, { separate: true }),
+ ["2.50", "KiB/s"]);
+ // backwards compatible API for separate flag
+ assert.deepEqual(cockpit.format_bytes_per_sec(2555, 1024, true),
+ ["2.50", "KiB/s"]);
+});
+
+QUnit.test("format_bits_per_sec", function (assert) {
+ const checks = [
+ [55, "55 bps"],
+ [55.23456789, "55.2 bps"],
+ [55.98765432, "56.0 bps"],
+ [2555, "2.56 Kbps"],
+ [2000, "2 Kbps"],
+ [2003, "2.00 Kbps"]
+ ];
+
+ assert.expect(checks.length);
+ for (let i = 0; i < checks.length; i++) {
+ assert.strictEqual(cockpit.format_bits_per_sec(checks[i][0]), checks[i][1],
+ "format_bits_per_sec(" + checks[i][0] + ") = " + checks[i][1]);
+ }
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-framed-cache.js b/pkg/base1/test-framed-cache.js
new file mode 100644
index 0000000..1129dcf
--- /dev/null
+++ b/pkg/base1/test-framed-cache.js
@@ -0,0 +1,92 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+/* Top level window */
+function parent_window(assert) {
+ const done = assert.async();
+ assert.expect(12);
+ window.assert = assert; // for the child frame
+
+ document.getElementById("qunit-header").textContent = "Cockpit Parent Frame";
+ let count = 0;
+ let child_done = false;
+
+ function maybe_done () {
+ if (child_done && count == 2) {
+ child_done = null;
+ done();
+ }
+ }
+
+ window.addEventListener("message", event => {
+ if (event.data == "child-done") {
+ child_done = true;
+ window.setTimeout(maybe_done, 0);
+ }
+ });
+
+ function provider(result, key) {
+ assert.equal(key, "cross-frame-cache", "parent provider got right key");
+ assert.equal(typeof result, "function", "parent provider got result function");
+ result({ myobject: "value" });
+ return {
+ close: function() {}
+ };
+ }
+
+ function consumer(value, key) {
+ count++;
+ assert.equal(key, "cross-frame-cache", "parent consumer got right key");
+ if (count === 1) {
+ assert.equal(value.myobject, "value", "parent consumer got parent value");
+ } else if (count === 2) {
+ assert.equal(value.myobject, "value2", "parent consumer got child value");
+ }
+ maybe_done();
+ }
+
+ cockpit.cache("cross-frame-cache", provider, consumer, 'parent');
+ const iframe = document.createElement("iframe");
+ iframe.setAttribute("name", "cockpit1:blah");
+ iframe.setAttribute("src", window.location.href + "?sub");
+ document.body.appendChild(iframe);
+}
+
+function child_frame() {
+ const assert = window.parent.assert;
+
+ let count = 0;
+
+ function provider(result, key) {
+ assert.equal(key, "cross-frame-cache", "child provider got right key");
+ assert.equal(typeof result, "function", "child provider got result function");
+ const timer = window.setTimeout(() => {
+ result({ myobject: "value2" });
+ window.clearTimeout(timer);
+ }, 1000);
+ return {
+ close: () => undefined,
+ };
+ }
+
+ function consumer(value, key) {
+ count++;
+ assert.equal(key, "cross-frame-cache", "child consumer got right key");
+ if (count === 1) {
+ assert.equal(value.myobject, "value", "child consumer got parent value");
+ cache.claim();
+ } else if (count == 2) {
+ assert.equal(value.myobject, "value2", "child consumer got child value");
+ window.parent.postMessage("child-done", "*");
+ }
+ }
+
+ const cache = cockpit.cache("cross-frame-cache", provider, consumer, 'child');
+}
+
+if (window.parent === window) {
+ QUnit.test("framed cache", parent_window);
+ QUnit.start();
+} else {
+ child_frame();
+}
diff --git a/pkg/base1/test-framed.js b/pkg/base1/test-framed.js
new file mode 100644
index 0000000..d8738ed
--- /dev/null
+++ b/pkg/base1/test-framed.js
@@ -0,0 +1,101 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+/* This is *NOT* a development guide, we take lots of shortcuts here */
+
+/* Top level window */
+function parent_window(assert) {
+ const done = assert.async();
+ assert.expect(7);
+ window.assert = assert; // for the child frame
+
+ document.getElementById("qunit-header").textContent = "Cockpit Parent Frame";
+ window.name = "cockpit1";
+ let initialized = false;
+ let frame;
+
+ cockpit.transport.filter(function (message, channel, control) { // eslint-disable-line array-callback-return
+ if (initialized)
+ frame.postMessage(message, cockpit.transport.origin);
+ });
+
+ window.addEventListener("message", event => {
+ const message = event.data;
+ if (message.length === 0) {
+ done();
+ } else if (message.indexOf && message.indexOf('"init"') !== -1) {
+ initialized = true;
+ frame.postMessage('\n{ "command": "init", "version": 1, "a": "b", "host" : "frame_host" }',
+ cockpit.transport.origin);
+ } else {
+ const ret = cockpit.transport.inject(message);
+ if (!ret) console.error("inject failed");
+ }
+ }, false);
+
+ /* This keeps coming up in tests ... how to open the transport */
+ const chan = cockpit.channel({ payload: "resource2" });
+ chan.addEventListener("close", () => {
+ assert.equal(cockpit.transport.host, "localhost", "parent cockpit.transport.host");
+ const iframe = document.createElement("iframe");
+ iframe.setAttribute("name", "cockpit1:blah");
+ iframe.setAttribute("src", window.location.href + "?sub");
+ document.body.appendChild(iframe);
+ frame = window.frames["cockpit1:blah"];
+ });
+}
+
+function child_frame() {
+ const assert = window.parent.assert;
+
+ let spawn_done = false;
+ let binary_done = false;
+
+ const promise = cockpit.spawn(["/bin/sh", "-c", "echo hi"], { host: "localhost" })
+ .then(resp => {
+ assert.equal(resp, "hi\n", "framed channel got output");
+ })
+ .always(() => {
+ assert.equal(promise.state(), "resolved", "framed channel closed");
+ assert.equal(cockpit.transport.host, "frame_host", "framed cockpit.transport.host");
+ spawn_done = true;
+ if (binary_done) {
+ cockpit.transport.close();
+ }
+ });
+
+ const channel = cockpit.channel({
+ payload: "echo",
+ binary: true,
+ host: "localhost"
+ });
+ channel.addEventListener("message", function(ev, payload) {
+ assert.equal(typeof payload[0], "number", "binary channel got a byte array");
+
+ let match = true;
+ for (let i = 0; i < payload.length; i++) {
+ if (payload[i] !== i)
+ match = false;
+ }
+ assert.equal(match, true, "binary channel got back right data");
+ channel.close();
+ });
+ channel.addEventListener("close", (ev, options) => {
+ assert.notOk(options.reason, "binary channel close cleanly");
+ binary_done = true;
+ if (spawn_done)
+ cockpit.transport.close();
+ });
+
+ const view = new Array(8);
+ for (let i = 0; i < 8; i++)
+ view[i] = i;
+ channel.send(view);
+}
+
+if (window.parent === window) {
+ QUnit.test("framed", parent_window);
+ QUnit.start();
+} else {
+ child_frame();
+}
diff --git a/pkg/base1/test-http.js b/pkg/base1/test-http.js
new file mode 100644
index 0000000..c968eec
--- /dev/null
+++ b/pkg/base1/test-http.js
@@ -0,0 +1,396 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+const EXPECT_MOCK_STREAM = "0 1 2 3 4 5 6 7 8 9 ";
+
+/* Set this to a regexp to ignore that warning once */
+/*
+function console_ignore_warning(exp) {
+ const console_warn = console.warn;
+ console.warn = function() {
+ if (!exp.exec(arguments[0]))
+ console_warn.apply(console, arguments);
+ console.warn = console_warn;
+ };
+}
+*/
+
+QUnit.test("public api", assert => {
+ const client = cockpit.http("/test");
+ assert.equal(typeof client, "object", "http is an object");
+ assert.equal(typeof client.get, "function", "http.get() is a function");
+ assert.equal(typeof client.post, "function", "http.post() is a function");
+});
+
+const test_server = {
+ address: window.location.hostname,
+ port: parseInt(window.location.port, 10)
+};
+
+QUnit.test("simple request", assert => {
+ const done = assert.async();
+ assert.expect(1);
+
+ cockpit.http(test_server).get("/pkg/playground/manifest.json")
+ .then(data => {
+ assert.deepEqual(JSON.parse(data), {
+ tools: {
+ index: {
+ label: "Development"
+ }
+ },
+
+ playground: {
+ "react-patterns": {
+ label: "React Patterns"
+ },
+ translate: {
+ label: "Translating"
+ },
+ exception: {
+ label: "Exceptions"
+ },
+ pkgs: {
+ label: "Packages"
+ },
+ preloaded: {
+ label: "Preloaded"
+ },
+ "notifications-receiver": {
+ label: "Notifications Receiver"
+ },
+ metrics: {
+ label: "Monitoring"
+ },
+ plot: {
+ label: "Plots"
+ },
+ service: {
+ label: "Generic Service Monitor"
+ },
+ speed: {
+ label: "Speed Tests"
+ },
+ journal: {
+ label: "Logs Box"
+ },
+ test: {
+ label: "Playground"
+ }
+ },
+ preload: ["preloaded"],
+ "content-security-policy": "img-src 'self' data:"
+ }, "returned right data");
+ })
+ .finally(done);
+});
+
+QUnit.test("with params", assert => {
+ const done = assert.async();
+ assert.expect(1);
+
+ cockpit.http(test_server)
+ .get("/mock/qs", { key: "value", name: "Scruffy the Janitor" })
+ .then(resp => assert.equal(resp, "key=value&name=Scruffy+the+Janitor", "right query string"))
+ .finally(done);
+});
+
+QUnit.test("not found", assert => {
+ const done = assert.async();
+ assert.expect(5);
+
+ cockpit.http(test_server)
+ .get("/not/found")
+ .response(status => assert.equal(status, 404, "status code"))
+ .catch((ex, data) => {
+ assert.strictEqual(ex.problem, null, "mapped to cockpit code");
+ assert.strictEqual(ex.status, 404, "has status code");
+ assert.equal(ex.message, "Not Found", "has reason");
+ assert.true(data !== undefined && data.includes('<h1>Not Found</h1>'), "got body");
+ })
+ .finally(done);
+});
+
+QUnit.test("streaming", assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ let num_chunks = 0;
+ let got = "";
+ cockpit.http(test_server)
+ .get("/mock/stream")
+ .stream(resp => {
+ got += resp;
+ num_chunks++;
+ })
+ .finally(() => {
+ assert.true(num_chunks > 1, "got at least two chunks");
+ assert.true(num_chunks <= 10, "got at most 10 chunks");
+ assert.equal(got, EXPECT_MOCK_STREAM, "stream got right data");
+ done();
+ });
+});
+
+QUnit.test("close", assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ let at = 0;
+ const http = cockpit.http(test_server);
+
+ http.get("/mock/stream")
+ .stream(resp => {
+ at += 1;
+ assert.equal(resp, "0 ", "first stream part");
+ http.close("bad-boy");
+ })
+ .catch(ex => assert.equal(ex.problem, "bad-boy", "right problem"))
+ .finally(() => {
+ assert.equal(at, 1, "stream got cancelled");
+ done();
+ });
+});
+
+QUnit.test("close all", assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ const http = cockpit.http(test_server);
+ const req = http.get("/mock/stream");
+
+ let at = 0;
+ req
+ .stream(resp => {
+ at += 1;
+ assert.equal(resp, "0 ", "first stream part");
+ http.close("bad-boy");
+ })
+ .catch(ex => assert.equal(ex.problem, "bad-boy", "right problem"))
+ .finally(() => {
+ assert.equal(at, 1, "stream got cancelled");
+ http.close("closed"); // This should be a no-op now
+ done();
+ });
+});
+
+QUnit.test("headers", assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ cockpit.http(test_server)
+ .get("/mock/headers", null, { Header1: "booo", Header2: "yay value" })
+ .response((status, headers) => {
+ assert.equal(status, 201, "status code");
+ assert.deepEqual(headers, {
+ Header1: "booo",
+ Header2: "yay value",
+ Header3: "three",
+ Header4: "marmalade",
+ "Referrer-Policy": "no-referrer",
+ "X-DNS-Prefetch-Control": "off",
+ "X-Content-Type-Options": "nosniff",
+ "X-Frame-Options": "sameorigin",
+ "Cross-Origin-Resource-Policy": "same-origin",
+ }, "got back headers");
+ })
+ .then(() => assert.ok(true, "split response succeeded"))
+ .finally(done);
+});
+
+QUnit.test("escape host header", assert => {
+ const done = assert.async();
+ assert.expect(3);
+
+ cockpit.http(test_server)
+ .get("/mock/host", null, { })
+ .response((status, headers) => {
+ assert.equal(status, 201, "status code");
+ assert.deepEqual(headers.Host, window.location.host, "got back escaped headers");
+ })
+ .then(() => assert.ok(true, "split response succeeded"))
+ .finally(done);
+});
+
+QUnit.test("connection headers", assert => {
+ const done = assert.async();
+ assert.expect(2);
+
+ cockpit.http({ port: test_server.port, headers: { Header1: "booo", Header2: "not this" } })
+ .get("/mock/headers", null, { Header2: "yay value", Header0: "extra" })
+ .response((status, headers) => {
+ assert.equal(status, 201, "status code");
+ assert.deepEqual(headers, {
+ Header0: "extra",
+ Header1: "booo",
+ Header2: "yay value",
+ Header3: "three",
+ Header4: "marmalade",
+ "Referrer-Policy": "no-referrer",
+ "X-DNS-Prefetch-Control": "off",
+ "X-Content-Type-Options": "nosniff",
+ "X-Frame-Options": "sameorigin",
+ "Cross-Origin-Resource-Policy": "same-origin",
+ }, "got back combined headers");
+ })
+ .finally(done);
+});
+
+QUnit.test("http promise recursive", assert => {
+ assert.expect(7);
+
+ const promise = cockpit.http(test_server).get("/");
+
+ const target = { };
+ const promise2 = promise.promise(target);
+ assert.strictEqual(promise2, target, "used target");
+ assert.equal(typeof promise2.done, "function", "promise2.done()");
+ assert.equal(typeof promise2.promise, "function", "promise2.promise()");
+ assert.equal(typeof promise2.input, "function", "promise2.input()");
+
+ const promise3 = promise2.promise();
+ assert.equal(typeof promise3.done, "function", "promise3.done()");
+ assert.equal(typeof promise3.promise, "function", "promise3.promise()");
+ assert.equal(typeof promise3.input, "function", "promise3.input()");
+});
+
+QUnit.test("http keep alive", async assert => {
+ assert.expect(1);
+
+ // connection sharing is not implemented in the pybridge
+ if (await QUnit.mock_info("pybridge")) {
+ assert.rejects(
+ cockpit.http({ port: test_server.port, connection: "one" }).get("/mock/connection"),
+ ex => ex.problem == "protocol-error" && ex.status == undefined,
+ "rejects connection option on python bridge");
+ return;
+ }
+
+ /*
+ * The /mock/connection handler returns an identifier that changes if
+ * a different connection is used.
+ */
+ const first = await cockpit.http({ port: test_server.port, connection: "marmalade" }).get("/mock/connection");
+ const second = await cockpit.http({ port: test_server.port, connection: "marmalade" }).get("/mock/connection");
+ assert.equal(first, second, "same connection");
+});
+
+QUnit.test("http connection different", async assert => {
+ assert.expect(1);
+
+ // connection sharing is not implemented in the pybridge
+ if (await QUnit.mock_info("pybridge")) {
+ assert.ok(true);
+ return;
+ }
+
+ /*
+ * The /mock/connection handler returns an identifier that changes if
+ * a different connection is used.
+ */
+ const first = await cockpit.http({ port: test_server.port, connection: "one" }).get("/mock/connection");
+ const second = await cockpit.http({ port: test_server.port, connection: "two" }).get("/mock/connection");
+ assert.notEqual(first, second, "different connection");
+});
+
+QUnit.test("http connection without address", async assert => {
+ assert.expect(1);
+
+ // connection sharing is not implemented in the pybridge
+ if (await QUnit.mock_info("pybridge")) {
+ assert.ok(true);
+ return;
+ }
+
+ // Able to reuse connection client info and not specify address again.
+ const first = await cockpit.http({ port: test_server.port, connection: "one" }).get("/mock/connection");
+ const second = await cockpit.http({ connection: "one" }).get("/mock/connection");
+ assert.equal(first, second, "same connection");
+});
+
+QUnit.test("no dns address", assert => {
+ assert.expect(1);
+
+ assert.rejects(cockpit.http({ port: 8080, address: "the-other-host.example.com" }).get("/"),
+ /* Unfortunately we can see either of these errors when running unit tests */
+ ex => { return ex.problem === "timeout" || ex.problem === "not-found" });
+});
+
+QUnit.test("address with params", assert => {
+ const done = assert.async();
+ assert.expect(1);
+
+ cockpit.http(test_server)
+ .get("/mock/qs", { key: "value", name: "Scruffy the Janitor" })
+ .then(resp => assert.equal(resp, "key=value&name=Scruffy+the+Janitor", "right query string"))
+ .finally(done);
+});
+
+QUnit.test("HEAD method", assert => {
+ const done = assert.async();
+ assert.expect(4);
+
+ assert.rejects(
+ cockpit.http(test_server).get("/mock/headonly"),
+ ex => ex.status == 400 && ex.reason == "Only HEAD allowed on this path",
+ "rejects GET request on /headonly path");
+
+ const InputData = "some chars";
+
+ cockpit.http(test_server).request({
+ path: "/mock/headonly",
+ method: "HEAD",
+ headers: { InputData },
+ body: "",
+ })
+ .response((status, headers) => {
+ assert.equal(status, 200);
+ assert.equal(headers.InputDataLength, InputData.length);
+ })
+ .then(data => assert.equal(data, ""))
+ .finally(done);
+});
+
+QUnit.test("wrong options", async assert => {
+ assert.rejects(
+ cockpit.http({}).get("/"),
+ // unfortunately cockpit.js does not propagate the detailed error message
+ ex => ex.problem == "protocol-error" && ex.status == undefined,
+ "rejects request without port or unix option");
+
+ assert.rejects(
+ cockpit.http({ port: 1234, unix: "/nonexisting/socket" }).get("/"),
+ ex => ex.problem == "protocol-error" && ex.status == undefined,
+ "rejects request with both port and unix option");
+
+ // This is disallowed in the pybridge, but allowed in the C bridge
+ if (await QUnit.mock_info("pybridge")) {
+ assert.rejects(
+ cockpit.http({ unix: "/nonexisting/socket", tls: {} }).get("/"),
+ ex => ex.problem == "protocol-error" && ex.status == undefined,
+ "rejects request with both unix and tls option");
+ } else {
+ assert.ok(true, "skipping on python bridge, not implemented");
+ }
+});
+
+QUnit.test("parallel stress test", async assert => {
+ // This is way too slow under valgrind
+ if (await QUnit.mock_info("skip_slow_tests")) {
+ assert.ok(true, "skipping on python bridge, not implemented");
+ return;
+ }
+
+ const num = 1000;
+ assert.expect(num + 1);
+
+ const promises = [];
+ for (let i = 0; i < num; ++i)
+ promises.push(cockpit.http(test_server).get("/mock/stream"));
+
+ const results = await Promise.all(promises);
+ assert.equal(results.length, num, "got correct number of responses");
+ for (let i = 0; i < num; ++i)
+ assert.equal(results[i], EXPECT_MOCK_STREAM);
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-journal-renderer.js b/pkg/base1/test-journal-renderer.js
new file mode 100644
index 0000000..e39555e
--- /dev/null
+++ b/pkg/base1/test-journal-renderer.js
@@ -0,0 +1,314 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import QUnit from "qunit-tests";
+import { journal } from "journal";
+
+const debug = false;
+
+function pk(label, obj) {
+ let str = label;
+ if (obj) {
+ str += ": ";
+ str += obj.toSource();
+ }
+ console.log(str);
+}
+
+function dbg(label, obj) {
+ if (debug)
+ pk(label, obj);
+}
+
+const time = 0;
+let bootid = 0;
+
+function make_entry(message) {
+ return {
+ __REALTIME_TIMESTAMP: time.toString(),
+ __CURSOR: "fake",
+ _BOOT_ID: bootid.toString(),
+ _COMM: "fake",
+ UNUSED_FIELD: "12",
+ SYSLOG_IDENTIFIER: "fake",
+ PRIORITY: "3",
+ MESSAGE: message
+ };
+}
+
+function reboot() {
+ bootid += 1;
+}
+
+let output;
+let renderer;
+let expected_day;
+
+const funcs = {
+ render_line: (ident, prio, message, count, time, cursor) => ({ message, count }),
+ render_day_header: day => ({ day }),
+ render_reboot_separator: () => ({ reboot: true }),
+
+ append: elt => {
+ dbg('append', elt);
+ output.push(elt);
+ },
+ remove_last: () => {
+ dbg('remove-last');
+ output = output.slice(0, output.length - 1);
+ },
+ prepend: elt => {
+ dbg('prepend', elt);
+ output.unshift(elt);
+ },
+ remove_first: () => {
+ dbg('remove-first');
+ output = output.slice(1, output.length);
+ }
+};
+
+function append(message) {
+ renderer.append(make_entry(message));
+}
+
+function append_flush() {
+ renderer.append_flush();
+}
+
+function prepend(message) {
+ renderer.prepend(make_entry(message));
+}
+
+function prepend_flush() {
+ renderer.prepend_flush();
+}
+
+function jexpect(assert, label, expected) {
+ function jequal(a, b) {
+ if (a.day)
+ return a.day == b.day;
+ else if (a.message)
+ return a.message == b.message && a.count == b.count;
+ else if (a.reboot)
+ return a.reboot == b.reboot;
+ else
+ return false;
+ }
+
+ function check(expected) {
+ if (output.length != expected.length)
+ return false;
+ for (let i = 0; i < output.length; i++)
+ if (!jequal(output[i], expected[i]))
+ return false;
+ return true;
+ }
+
+ assert.ok(check(expected), label);
+}
+
+const month_names = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+];
+
+QUnit.testStart(function() {
+ output = [];
+ renderer = journal.renderer(funcs);
+ const d = new Date(time);
+ expected_day = month_names[d.getMonth()] + ' ' + d.getDate().toFixed() + ', ' + d.getFullYear().toFixed();
+});
+
+QUnit.test("append", assert => {
+ append('foo');
+ append('foo');
+ append_flush();
+ jexpect(assert, 'two repeated lines',
+ [{ day: expected_day },
+ { message: 'foo', count: 2 }
+ ]);
+ append('foo');
+ append_flush();
+ jexpect(assert, 'three repeated lines after flush',
+ [{ day: expected_day },
+ { message: 'foo', count: 3 }
+ ]);
+});
+
+QUnit.test('prepend', assert => {
+ prepend('foo');
+ prepend('foo');
+ prepend_flush();
+ jexpect(assert, 'two repeated lines',
+ [{ day: expected_day },
+ { message: 'foo', count: 2 }
+ ]);
+ prepend('foo');
+ prepend_flush();
+ jexpect(assert, 'three repeated lines after flush',
+ [{ day: expected_day },
+ { message: 'foo', count: 3 }
+ ]);
+});
+
+QUnit.test('prepend after append', assert => {
+ append('foo');
+ append_flush();
+ prepend('foo');
+ prepend_flush();
+ jexpect(assert, 'two repeated lines',
+ [{ day: expected_day },
+ { message: 'foo', count: 2 }
+ ]);
+});
+
+QUnit.test('prepend after append', assert => {
+ append('foo');
+ append_flush();
+ prepend('bar');
+ prepend_flush();
+ jexpect(assert, 'two different lines',
+ [{ day: expected_day },
+ { message: 'bar', count: 1 },
+ { message: 'foo', count: 1 }
+ ]);
+});
+
+QUnit.test('append after prepend', assert => {
+ prepend('foo');
+ prepend_flush();
+ append('foo');
+ append_flush();
+ jexpect(assert, 'two repeated lines',
+ [{ day: expected_day },
+ { message: 'foo', count: 2 }
+ ]);
+});
+
+QUnit.test('append after split', assert => {
+ prepend('bar');
+ prepend('baz');
+ prepend_flush();
+ append('foo');
+ append_flush();
+ jexpect(assert, 'two different lines',
+ [{ day: expected_day },
+ { message: 'baz', count: 1 },
+ { message: 'bar', count: 1 },
+ { message: 'foo', count: 1 }
+ ]);
+});
+
+QUnit.test('append after split', assert => {
+ prepend('bar');
+ prepend('baz');
+ prepend_flush();
+ append('bar');
+ append_flush();
+ jexpect(assert, 'two repeated lines',
+ [{ day: expected_day },
+ { message: 'baz', count: 1 },
+ { message: 'bar', count: 2 },
+ ]);
+});
+
+QUnit.test('prepend after split', assert => {
+ append('foo');
+ append('bar');
+ append_flush();
+ prepend('baz');
+ prepend_flush();
+ jexpect(assert, 'two different lines',
+ [{ day: expected_day },
+ { message: 'baz', count: 1 },
+ { message: 'foo', count: 1 },
+ { message: 'bar', count: 1 },
+ ]);
+});
+
+QUnit.test('prepend after split', assert => {
+ append('foo');
+ append('bar');
+ append_flush();
+ prepend('foo');
+ prepend_flush();
+ jexpect(assert, 'two repeated lines',
+ [{ day: expected_day },
+ { message: 'foo', count: 2 },
+ { message: 'bar', count: 1 },
+ ]);
+});
+
+QUnit.test('reboot', assert => {
+ append('foo');
+ append('foo');
+ reboot();
+ append('foo');
+ append_flush();
+ jexpect(assert, 'two repeated lines before reboot',
+ [{ day: expected_day },
+ { message: 'foo', count: 2 },
+ { reboot: true },
+ { message: 'foo', count: 1 },
+ ]);
+});
+
+QUnit.test('prepend to reboot same day', assert => {
+ append('foo');
+ append('baz');
+ append_flush();
+ reboot();
+ prepend('bar');
+ prepend_flush();
+ jexpect(assert, 'different lines',
+ [{ day: expected_day },
+ { message: 'bar', count: 1 },
+ { reboot: true },
+ { message: 'foo', count: 1 },
+ { message: 'baz', count: 1 },
+ ]);
+});
+
+QUnit.test('prepend to reboot same day', assert => {
+ append('foo');
+ append('baz');
+ append_flush();
+ reboot();
+ prepend('foo');
+ prepend_flush();
+ jexpect(assert, 'repeated line',
+ [{ day: expected_day },
+ { message: 'foo', count: 1 },
+ { reboot: true },
+ { message: 'foo', count: 1 },
+ { message: 'baz', count: 1 },
+ ]);
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-locale.js b/pkg/base1/test-locale.js
new file mode 100644
index 0000000..9ae6686
--- /dev/null
+++ b/pkg/base1/test-locale.js
@@ -0,0 +1,235 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+const pig_latin = {
+ "": {
+ language: "pig",
+ "plural-forms": function(n) {
+ const plural = (n != 1);
+ return plural;
+ }
+ },
+ Control: [null, "Ontrolcay"],
+ User: [null, "Useray"],
+ Waiting: [null, "Aitingway"],
+ "$0 disk is missing": [
+ "$0 disk is missing",
+ "$0 isksbay is issingmay",
+ "$0 isksbay are issingmay"
+ ],
+ "key\u0004Control": [null, "OntrolCAY"],
+ "disk-non-rotational\u0004$0 disk is missing": [
+ "disk-non-rotational\u0004$0 disk is missing",
+ "$0 isksBAY is issingMAY",
+ "$0 isksBAY are issingMAY"
+ ]
+};
+
+const ru = {
+ "": {
+ language: "ru",
+ "plural-forms":
+ function(n) {
+ const plural = (n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
+ return plural;
+ }
+ },
+ "$0 bit": ["$0 bits", "$0 бит", "$0 бита", "$0 бит"]
+};
+
+QUnit.test("public api", function (assert) {
+ assert.equal(typeof cockpit.locale, "function", "cockpit.locale is a function");
+});
+
+QUnit.test("gettext", function (assert) {
+ cockpit.locale(null); /* clear it */
+ cockpit.locale(pig_latin);
+ assert.equal(cockpit.language, "pig", "correct lang");
+ assert.equal(cockpit.gettext("Control"), "Ontrolcay", "returned translation");
+ assert.equal(cockpit.gettext("key", "Control"), "OntrolCAY", "with context");
+ assert.equal(cockpit.gettext("Empty"), "Empty", "english default");
+ assert.equal(cockpit.gettext("verb", "Empty"), "Empty", "english default context");
+});
+
+QUnit.test("underscore", function (assert) {
+ cockpit.locale(null); /* clear it */
+ cockpit.locale(pig_latin);
+ const _ = cockpit.gettext;
+ const C_ = _;
+ assert.equal(_("Control"), "Ontrolcay", "returned translation");
+ assert.equal(_("Empty"), "Empty", "english default");
+ assert.equal(C_("key", "Control"), "OntrolCAY", "with context");
+ assert.equal(C_("verb", "Empty"), "Empty", "with context");
+});
+
+QUnit.test("ngettext simple", function (assert) {
+ cockpit.locale(null); /* clear it */
+ cockpit.locale(pig_latin);
+ assert.equal(cockpit.ngettext("$0 disk is missing", "$0 disks are missing", 0), "$0 isksbay are issingmay", "zero things");
+ assert.equal(cockpit.ngettext("$0 disk is missing", "$0 disks are missing", 1), "$0 isksbay is issingmay", "one thing");
+ assert.equal(cockpit.ngettext("$0 disk is missing", "$0 disks are missing", 5), "$0 isksbay are issingmay", "multiple things");
+ assert.equal(cockpit.ngettext("disk-non-rotational", "$0 disk is missing", "$0 disks are missing", 0),
+ "$0 isksBAY are issingMAY", "zero things context");
+ assert.equal(cockpit.ngettext("disk-non-rotational", "$0 disk is missing", "$0 disks are missing", 1),
+ "$0 isksBAY is issingMAY", "one thing context");
+ assert.equal(cockpit.ngettext("disk-non-rotational", "$0 disk is missing", "$0 disks are missing", 5),
+ "$0 isksBAY are issingMAY", "multiple things context");
+ assert.equal(cockpit.ngettext("$0 byte", "$0 bytes", 1), "$0 byte", "default one");
+ assert.equal(cockpit.ngettext("$0 byte", "$0 bytes", 2), "$0 bytes", "default multiple");
+ assert.equal(cockpit.ngettext("memory", "$0 byte", "$0 bytes", 1), "$0 byte", "default one context");
+ assert.equal(cockpit.ngettext("memory", "$0 byte", "$0 bytes", 2), "$0 bytes", "default multiple context");
+});
+
+QUnit.test("ngettext complex", function (assert) {
+ cockpit.locale(null); /* clear it */
+ cockpit.locale(ru);
+ assert.equal(cockpit.ngettext("$0 bit", "$0 bits", 0), "$0 бит", "zero things");
+ assert.equal(cockpit.ngettext("$0 bit", "$0 bits", 1), "$0 бит", "one thing");
+ assert.equal(cockpit.ngettext("$0 bit", "$0 bits", 5), "$0 бит", "multiple things");
+ assert.equal(cockpit.ngettext("$0 bit", "$0 bits", 23), "$0 бита", "genitive singular");
+ assert.equal(cockpit.ngettext("$0 byte", "$0 bytes", 1), "$0 byte", "default one");
+ assert.equal(cockpit.ngettext("$0 byte", "$0 bytes", 2), "$0 bytes", "default multiple");
+});
+
+QUnit.test("translate document", function (assert) {
+ cockpit.locale(null);
+ cockpit.locale(pig_latin);
+
+ document.getElementById('translations').innerHTML = "<span translate id='translatable-html'>Control</span>";
+
+ cockpit.translate();
+ const t = document.getElementById('translatable-html');
+ assert.equal(t.innerHTML, "Ontrolcay", "translate element");
+ assert.equal(t.hasAttribute("translate"), false, "translate element attribute removed");
+});
+
+QUnit.test("translate elements", function (assert) {
+ cockpit.locale(null);
+ cockpit.locale(pig_latin);
+
+ const div1 = document.createElement('div');
+ div1.innerHTML = "<span translate id='translatable-html'>Control</span>" +
+ "<span translate translate-context='key' id='translatable-context-html'>Control</span>";
+
+ const div2 = document.createElement('div');
+ div2.setAttribute('translate', 'translate');
+ div2.innerHTML = 'User';
+
+ const div3 = document.createElement('div');
+ div3.innerHTML = "<span><i translate>Waiting</i></span>";
+
+ const t = document.getElementById('translations');
+ t.appendChild(div1);
+ t.appendChild(div2);
+ t.appendChild(div3);
+
+ cockpit.translate(div1, div2, div3);
+ const thtml = document.getElementById('translatable-html');
+ assert.equal(thtml.innerHTML, "Ontrolcay", "translate element");
+ assert.equal(thtml.hasAttribute("translate"), false, "translate element attribute removed");
+ const tconhtml = document.getElementById('translatable-context-html');
+ assert.equal(tconhtml.innerHTML, "OntrolCAY", "translate context");
+ assert.equal(tconhtml.hasAttribute("translate"), false, "translate context attribute removed");
+});
+
+QUnit.test("translate array", function (assert) {
+ cockpit.locale(null);
+ cockpit.locale(pig_latin);
+
+ const div1 = document.createElement('div');
+ div1.innerHTML = "<span translate id='translatable-html'>Control</span>" +
+ "<span translate translate-context='key' id='translatable-context-html'>Control</span>";
+
+ const div2 = document.createElement('div');
+ div2.setAttribute('translate', 'translate');
+ div2.innerHTML = 'User';
+
+ const div3 = document.createElement('div');
+ div3.innerHTML = "<span><i translate>Waiting</i></span>";
+
+ const t = document.getElementById('translations');
+ t.appendChild(div1);
+ t.appendChild(div2);
+ t.appendChild(div3);
+
+ cockpit.translate(document.querySelector("#translations div"));
+
+ const thtml = document.getElementById('translatable-html');
+ assert.equal(thtml.innerHTML, "Ontrolcay", "translate element");
+ assert.equal(thtml.hasAttribute("translate"), false, "translate element attribute removed");
+ const tconhtml = document.getElementById('translatable-context-html');
+ assert.equal(tconhtml.innerHTML, "OntrolCAY", "translate context");
+ assert.equal(tconhtml.hasAttribute("translate"), false, "translate context attribute removed");
+});
+
+QUnit.test("translate glade", function (assert) {
+ cockpit.locale(null);
+ cockpit.locale(pig_latin);
+
+ const div = document.createElement('div');
+ div.innerHTML = "<span translate='yes' id='translatable-glade'>Control</span>" +
+ "<span translate='yes' context='key' id='translatable-glade-context'>Control</span>";
+
+ document.getElementById('translations').appendChild(div);
+
+ cockpit.translate(div);
+
+ const t = document.getElementById('translatable-glade');
+ assert.equal(t.innerHTML, "Ontrolcay", "translatable element");
+ assert.equal(t.hasAttribute("translate"), false, "translate element removed");
+ const tcon = document.getElementById('translatable-glade-context');
+ assert.equal(tcon.innerHTML, "OntrolCAY", "translate context");
+ assert.equal(tcon.hasAttribute("translate"), false, "translate context attribute removed");
+});
+
+QUnit.test("translate attributes", function (assert) {
+ cockpit.locale(null);
+ cockpit.locale(pig_latin);
+
+ const div = document.createElement('div');
+ div.innerHTML = "<span translate='title' title='Control' id='translatable-attribute'>Waiting</span>" +
+ "<div><span translate='title' translate-context='key' title='Control'" +
+ "id='translatable-attribute-context'>Waiting</span>" +
+ "<span translate='yes title' title='User' id='translatable-attribute-both'>Waiting</span></div>" +
+ "<span translate=' yes title ' title='User' id='translatable-attribute-syntax'>Waiting</span>";
+
+ document.getElementById('translations').appendChild(div);
+
+ cockpit.translate(div);
+
+ const attr = document.getElementById('translatable-attribute');
+ assert.equal(attr.getAttribute("title"), "Ontrolcay", "translate attribute");
+ assert.equal(attr.innerHTML, "Waiting", "translate attribute doesn't affect text");
+ assert.equal(attr.hasAttribute("translate"), false, "translate element removed");
+
+ const context = document.getElementById('translatable-attribute-context');
+ assert.equal(context.getAttribute("title"), "OntrolCAY", "translatable element");
+ assert.equal(context.innerHTML, "Waiting", "translate context doesn't affect text");
+ assert.equal(context.hasAttribute("translate"), false, "translate element removed");
+
+ const both = document.getElementById('translatable-attribute-both');
+ assert.equal(both.getAttribute("title"), "Useray", "translate attribute both");
+ assert.equal(both.innerHTML, "Aitingway", "translate text both");
+ assert.equal(both.hasAttribute("translate"), false, "translate removed");
+
+ const syntax = document.getElementById('translatable-attribute-both');
+ assert.equal(syntax.getAttribute("title"), "Useray", "translate syntax both");
+ assert.equal(syntax.innerHTML, "Aitingway", "translate syntax both");
+ assert.equal(syntax.hasAttribute("translate"), false, "translate removed");
+});
+
+function init() {
+ /* Area for translate tests to play in */
+ const div = document.createElement('div');
+ div.setAttribute('id', 'translations');
+ div.setAttribute('hidden', 'hidden');
+ document.querySelector("body").appendChild(div);
+
+ /* Ready to go */
+ QUnit.start();
+}
+
+if (document.readyState == "loading")
+ document.addEventListener("DOMContentLoaded", init);
+else
+ init();
diff --git a/pkg/base1/test-location.js b/pkg/base1/test-location.js
new file mode 100644
index 0000000..a760fd6
--- /dev/null
+++ b/pkg/base1/test-location.js
@@ -0,0 +1,319 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("basic", function (assert) {
+ window.location.hash = "";
+
+ assert.equal(typeof cockpit.location, "object", "cockpit.location exists");
+ assert.ok(Array.isArray(cockpit.location.path), "cockpit.location.path exists");
+ assert.equal(typeof cockpit.location.options, "object", "cockpit.location.options exists");
+ assert.equal(typeof cockpit.location.go, "function", "cockpit.location.go exists");
+ assert.equal(typeof cockpit.location.replace, "function", "cockpit.location.replace exists");
+ assert.equal(typeof cockpit.location.decode, "function", "cockpit.location.decode exists");
+ assert.equal(typeof cockpit.location.encode, "function", "cockpit.location.encode exists");
+
+ assert.deepEqual(cockpit.location.path, [], "path is empty");
+ assert.deepEqual(cockpit.location.options, { }, "options are empty");
+});
+
+QUnit.test("decode", function (assert) {
+ window.location.hash = "#/base/test";
+
+ const checks = [
+ ["#/host/path/sub?a=1&b=2", {
+ path: ["host", "path", "sub"],
+ options: { a: "1", b: "2" }
+ }
+ ],
+ ["", {
+ path: [],
+ options: { }
+ }
+ ],
+ ["#", {
+ path: [],
+ options: { }
+ }
+ ],
+ ["#/", {
+ path: [],
+ options: { }
+ }
+ ],
+ ["/horst", {
+ path: ["horst"],
+ options: { }
+ }
+ ],
+ ["//one", {
+ path: ["one"],
+ options: { }
+ }
+ ],
+ ["//one/", {
+ path: ["one"],
+ options: { }
+ }
+ ],
+ ["///two", {
+ path: ["two"],
+ options: { }
+ }
+ ],
+ ["/slash/%2f", {
+ path: ["slash", "/"],
+ options: { }
+ }
+ ],
+ ["?a=1", {
+ path: [],
+ options: { a: "1" }
+ }
+ ],
+ ["?a=1&a=2", {
+ path: [],
+ options: { a: ["1", "2"] }
+ }
+ ],
+ ["?%3f=%3d", {
+ path: [],
+ options: { "?": "=" }
+ }
+ ],
+ ["#?=", {
+ path: [],
+ options: { "": "" }
+ }
+ ],
+ ["?=", {
+ path: [],
+ options: { "": "" }
+ }
+ ],
+ ["relative/sub", {
+ path: ["base", "relative", "sub"],
+ options: { }
+ }
+ ],
+ ["./relative/sub", {
+ path: ["base", "relative", "sub"],
+ options: { }
+ }
+ ],
+ ["../relative/sub", {
+ path: ["relative", "sub"],
+ options: { }
+ }
+ ],
+ ["/top/../sub", {
+ path: ["sub"],
+ options: { }
+ }
+ ],
+ ["/top/./sub/./", {
+ path: ["top", "sub"],
+ options: { }
+ }
+ ],
+ ["relative/../sub", {
+ path: ["base", "sub"],
+ options: { }
+ }
+ ]
+ ];
+
+ assert.expect(checks.length);
+ for (let i = 0; i < checks.length; i++) {
+ const options = { };
+ const path = cockpit.location.decode(checks[i][0], options);
+ assert.deepEqual({ path, options }, checks[i][1], "decode(\"" + checks[i][0] + "\")");
+ }
+});
+
+QUnit.test("encode", function (assert) {
+ /* We don't check options here since we can't predict the order in
+ which they appear in the hash. Encoding of options is covered
+ in the "roundtrip" test.
+ */
+ const checks = [
+ ["/host/path/sub?a=1&b=2", {
+ path: ["host", "path", "sub"],
+ options: { a: "1", b: "2" }
+ }
+ ],
+ ["/one", {
+ path: ["one"],
+ options: { }
+ }
+ ],
+ ["/one/two", {
+ path: ["one", "two"],
+ options: { }
+ }
+ ],
+ ["/slash/%2F", {
+ path: ["slash", "/"],
+ options: { }
+ }
+ ],
+ ["/p?a=1", {
+ path: ["p"],
+ options: { a: "1" }
+ }
+ ],
+ ["/p?%3F=%3D", {
+ path: ["p"],
+ options: { "?": "=" }
+ }
+ ],
+ ["/p?=", {
+ path: ["p"],
+ options: { "": "" }
+ }
+ ],
+ ["/p?value=one&value=two", {
+ path: ["p"],
+ options: { value: ["one", "two"] }
+ }]
+ ];
+
+ assert.expect(checks.length);
+ for (let i = 0; i < checks.length; i++) {
+ const encoded = cockpit.location.encode(checks[i][1].path, checks[i][1].options);
+ assert.strictEqual(encoded, checks[i][0], "encode(" + JSON.stringify(checks[i][1]) + ")");
+ }
+});
+
+QUnit.test("roundtrip", function (assert) {
+ const checks = [
+ {
+ path: ["path", "sub"],
+ options: { a: "1", b: "2" }
+ },
+ {
+ path: ["päth", "süb"],
+ options: { a: "1", b: "2" }
+ },
+ {
+ path: ["/=()?", "$%&/"],
+ options: { "": "=$&%", b: "=2%34" }
+ },
+ ];
+
+ assert.expect(checks.length);
+ for (let i = 0; i < checks.length; i++) {
+ const encoded = cockpit.location.encode(checks[i].path, checks[i].options);
+ const decoded = { options: { } };
+ decoded.path = cockpit.location.decode(encoded, decoded.options);
+ assert.deepEqual(decoded, checks[i], "roundtrip(" + JSON.stringify(checks[i]) + ")");
+ }
+});
+
+QUnit.test("external change", function (assert) {
+ const location = cockpit.location;
+
+ window.location.hash = "#/a/b/c?x=1&y=2";
+
+ assert.notStrictEqual(cockpit.location, location, "cockpit.location is different object");
+ assert.deepEqual(cockpit.location.path, ["a", "b", "c"], "path is correct");
+ assert.strictEqual(cockpit.location.options.x, "1", "option x is correct");
+ assert.strictEqual(cockpit.location.options.y, "2", "option y is correct");
+});
+
+QUnit.test("internal change", function (assert) {
+ cockpit.location.go(["x", "y", "z"]);
+
+ assert.strictEqual(window.location.hash, "#/x/y/z", "hash is correct");
+ assert.deepEqual(cockpit.location.path, ["x", "y", "z"], "path is correct");
+ assert.deepEqual(cockpit.location.options, { }, "options are empty");
+});
+
+QUnit.test("string change", function (assert) {
+ cockpit.location = "/p/x/../q/r?a=b";
+
+ assert.strictEqual(window.location.hash, "#/p/q/r?a=b", "hash is correct");
+ assert.deepEqual(cockpit.location.path, ["p", "q", "r"], "path is correct");
+ assert.deepEqual(cockpit.location.options, { a: "b" }, "options are empty");
+});
+
+QUnit.test("string change", function (assert) {
+ window.location.href = "#/top/file";
+ cockpit.location = "another";
+
+ assert.strictEqual(window.location.hash, "#/top/another", "hash is correct");
+ assert.deepEqual(cockpit.location.path, ["top", "another"], "path is correct");
+});
+
+QUnit.test("change options", function (assert) {
+ window.location.hash = "";
+ assert.deepEqual(cockpit.location.path, [], "path is empty");
+ assert.deepEqual(cockpit.location.options, { }, "options are empty");
+
+ cockpit.location.go(cockpit.location.path, { x: "1" });
+ assert.deepEqual(cockpit.location.options, { x: "1" }, "options are correct");
+ assert.strictEqual(window.location.hash, "#/?x=1", "hash is correct");
+
+ cockpit.location.go(cockpit.location.path);
+ assert.deepEqual(cockpit.location.options, { }, "options are empty");
+ assert.strictEqual(window.location.hash, "#/", "hash is correct");
+});
+
+QUnit.test("test", function (assert) {
+ const done = assert.async();
+ window.location.hash = "#/hello";
+
+ let triggered = false;
+
+ assert.deepEqual(cockpit.location.path, ["hello"], "path is right");
+ const onLocationChanged = () => {
+ assert.strictEqual(window.location.hash, "#/gonna-happen", "hash has changed");
+ cockpit.removeEventListener("locationchanged", onLocationChanged);
+ triggered = true;
+ done();
+ };
+ cockpit.addEventListener("locationchanged", onLocationChanged);
+
+ cockpit.location.go(["gonna-happen"]);
+ assert.ok(!triggered, "not yet triggered");
+});
+
+QUnit.test("test", function (assert) {
+ const done = assert.async();
+ window.location.hash = "#/hello";
+
+ const location = cockpit.location;
+ assert.deepEqual(cockpit.location.path, ["hello"], "path is right");
+
+ window.setTimeout(function() {
+ location.go(["not-gonna-happen"]);
+ assert.strictEqual(window.location.hash, "#/other", "hash is correct");
+
+ cockpit.location.go(["gonna-happen"]);
+ assert.strictEqual(window.location.hash, "#/gonna-happen", "hash has changed");
+
+ done();
+ }, 100);
+
+ /* User or something else navigates */
+ window.location.hash = "#/other";
+});
+
+/* Set url_root in beforeEach so the test does not flake */
+QUnit.module("url_root tests", {
+ beforeEach: () => {
+ window.mock = { url_root: "cockpit" };
+ },
+ afterEach: () => {
+ window.mock = null;
+ }
+});
+
+QUnit.test("encode", assert => {
+ let path = cockpit.location.encode("path", "", true);
+ assert.equal(path, "/cockpit/path", "path is correct");
+ /* an existing url_root should not be prepended */
+ path = cockpit.location.encode("/cockpit/path", "", true);
+ assert.equal(path, "/cockpit/path", "path is correct");
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-metrics.js b/pkg/base1/test-metrics.js
new file mode 100644
index 0000000..e08d587
--- /dev/null
+++ b/pkg/base1/test-metrics.js
@@ -0,0 +1,195 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+function MockPeer() {
+ /*
+ * Events triggered here:
+ * opened(event, args)
+ * recv(event, payload)
+ * closed(event, problem)
+ */
+ cockpit.event_target(this);
+
+ let channel = null;
+
+ /* open: triggered when mock Channel is created */
+ this.onopened = function(event, channel, options) {
+ /* nada */
+ };
+
+ /* close event: triggered when mock Channel is closed */
+ this.onclosed = function(event, channel, options) {
+ /* nada */
+ };
+
+ /* send a message from peer back to channel */
+ this.send = function(payload) {
+ if (typeof (payload) != "string")
+ payload = String(payload);
+ if (!channel)
+ console.log("dropping message before open");
+ else if (channel.valid)
+ channel.dispatchEvent("message", payload);
+ else
+ console.log("dropping message after close");
+ };
+
+ /* send a object as JSON */
+ this.send_json = function(payload) {
+ this.send(JSON.stringify(payload));
+ };
+
+ /* peer closes the channel */
+ this.close = function(channel, options) {
+ console.assert(channel);
+ if (channel.valid) {
+ channel.valid = false;
+ channel.dispatchEvent("close", options || { });
+ }
+ };
+
+ const peer = this;
+ let last_channel = 0;
+
+ function MockChannel(options) {
+ cockpit.event_target(this);
+ this.number = last_channel++;
+ this.options = options;
+ this.valid = true;
+
+ const channel = this;
+
+ function Transport() {
+ this.close = function(problem) { console.assert(arguments.length == 1) };
+ }
+
+ this.transport = new Transport();
+
+ this.send = function(payload) {
+ console.assert(arguments.length == 1);
+ console.assert(this.valid);
+ peer.dispatchEvent("recv", channel, payload);
+ };
+
+ this.close = function(options) {
+ console.assert(arguments.length <= 1);
+ this.valid = false;
+ peer.dispatchEvent("close", channel, options || {});
+ };
+
+ QUnit.testDone(function() {
+ channel.valid = false;
+ });
+
+ peer.dispatchEvent("open", channel, options || {});
+ }
+
+ cockpit.channel = function(options) {
+ channel = new MockChannel(options);
+ return channel;
+ };
+}
+
+function MockSink(expected, callback) {
+ const self = this;
+
+ self.samples = [];
+
+ function input(beg, items, mapping) {
+ for (let i = 0; i < items.length; i++)
+ self.samples[beg + i] = items[i];
+ }
+
+ self.series = { input };
+ return self;
+}
+
+let old_channel;
+QUnit.module("Mock tests", {
+ beforeEach: () => {
+ old_channel = cockpit.channel;
+ },
+ afterEach: () => {
+ cockpit.channel = old_channel;
+ }
+});
+
+QUnit.test("non-instanced decompression", assert => {
+ assert.expect(1);
+
+ const peer = new MockPeer();
+ const sink = new MockSink();
+
+ const metrics = cockpit.metrics(1000, {
+ source: "source",
+ metrics: [{ name: "m1" }],
+ });
+ metrics.series = sink.series;
+
+ metrics.follow();
+ peer.send_json({
+ timestamp: 0,
+ now: 0,
+ interval: 1000,
+ metrics: [{ name: "m1" }]
+ });
+ peer.send_json([[10]]);
+ peer.send_json([[]]);
+
+ assert.deepEqual(sink.samples, [[10], [10]], "got correct samples");
+});
+
+QUnit.test("interval validation", assert => {
+ assert.expect(2);
+ const done = assert.async();
+ const metrics_channel = cockpit.channel({
+ payload: "metrics1",
+ source: "internal",
+ interval: -1,
+ });
+
+ metrics_channel.addEventListener("close", (_, error) => {
+ assert.equal(error.problem, "protocol-error");
+ assert.equal(error.message, 'invalid "interval" value: -1');
+ done();
+ });
+});
+
+QUnit.test("metrics validation", assert => {
+ assert.expect(2);
+ const done = assert.async();
+ const metrics_channel = cockpit.channel({
+ payload: "metrics1",
+ source: "internal",
+ interval: 1000,
+ metrics: {
+ foo: 1
+ }
+ });
+
+ metrics_channel.addEventListener("close", (_, error) => {
+ assert.equal(error.problem, "protocol-error");
+ assert.equal(error.message, 'invalid "metrics" option was specified (not an array)');
+ done();
+ });
+});
+
+QUnit.test("metrics object validation", assert => {
+ assert.expect(1);
+ const done = assert.async();
+ const metrics_channel = cockpit.channel({
+ payload: "metrics1",
+ source: "internal",
+ interval: 1000,
+ metrics: [
+ { name: "nonexistant", derive: "rate" },
+ ]
+ });
+
+ metrics_channel.addEventListener("close", (_, error) => {
+ assert.equal(error.problem, "not-supported");
+ done();
+ });
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-no-jquery.js b/pkg/base1/test-no-jquery.js
new file mode 100644
index 0000000..85ccb6a
--- /dev/null
+++ b/pkg/base1/test-no-jquery.js
@@ -0,0 +1,29 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("cockpit object without jQuery", assert => {
+ const done = assert.async();
+ assert.expect(8);
+
+ assert.equal(typeof jQuery, "undefined", "jQuery is not defined");
+ assert.equal(typeof $, "undefined", "$ is not defined");
+ assert.equal(typeof cockpit, "object", "cockpit is defined");
+ assert.notEqual(cockpit.channel, undefined, "cockpit.channel is defined");
+ assert.notEqual(cockpit.spawn, undefined, "cockpit.spawn is defined");
+
+ /* Actually try to do something useful */
+ let got_message = false;
+ const channel = cockpit.channel({ payload: "stream", spawn: ["sh", "-c", "echo hello"] });
+ channel.onmessage = ev => {
+ got_message = true;
+ assert.equal(ev.detail, "hello\n", "channel message correct");
+ channel.onmessage = null;
+ };
+ channel.onclose = ev => {
+ assert.equal(ev.detail.command, "close", "channel close data correct");
+ assert.ok(got_message, "channel got message");
+ done();
+ };
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-permissions.js b/pkg/base1/test-permissions.js
new file mode 100644
index 0000000..9563958
--- /dev/null
+++ b/pkg/base1/test-permissions.js
@@ -0,0 +1,67 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+const root_user = {
+ name: "weird-root",
+ id: 0,
+ groups: null
+};
+
+const priv_user = {
+ name: "user",
+ id: 1000,
+ groups: ["user", "agroup"]
+};
+
+let old_dbus;
+let old_is_superuser;
+
+QUnit.module("Permission tests", {
+ before: () => {
+ old_dbus = cockpit.dbus;
+ old_is_superuser = cockpit._is_superuser;
+ cockpit._is_superuser = false;
+ },
+ after: () => {
+ cockpit.dbus = old_dbus;
+ cockpit._is_superuser = old_is_superuser;
+ }
+});
+
+QUnit.test("root-all-permissions", function (assert) {
+ assert.expect(2);
+
+ const p1 = cockpit.permission({ user: priv_user });
+ assert.equal(p1.allowed, false, "not root, not allowed");
+
+ const p2 = cockpit.permission({ user: root_user });
+ assert.equal(p2.allowed, true, "is root, allowed");
+});
+
+QUnit.test("group-permissions", function (assert) {
+ assert.expect(4);
+
+ const p1 = cockpit.permission({ user: priv_user, group: "badgroup" });
+ assert.equal(p1.allowed, false, "no group, not allowed");
+
+ const p2 = cockpit.permission({ user: priv_user, group: "agroup" });
+ assert.equal(p2.allowed, true, "has group, allowed");
+
+ const p3 = cockpit.permission({ user: root_user, group: "agroup" });
+ assert.equal(p3.allowed, true, "no group but root, allowed");
+
+ const p4 = cockpit.permission({ user: { id: 0, groups: ["other"] }, group: "agroup" });
+ assert.equal(p4.allowed, true, "no group match but root, allowed");
+});
+
+QUnit.test("admin-permissions", function (assert) {
+ assert.expect(2);
+
+ const p1 = cockpit.permission({ user: priv_user, _is_superuser: false, admin: true });
+ assert.equal(p1.allowed, false, "no superuser, admin not allowed");
+
+ const p2 = cockpit.permission({ user: priv_user, _is_superuser: true, admin: true });
+ assert.equal(p2.allowed, true, "superuser, admin allowed");
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-promise.js b/pkg/base1/test-promise.js
new file mode 100644
index 0000000..7792d1d
--- /dev/null
+++ b/pkg/base1/test-promise.js
@@ -0,0 +1,21 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("should be able to dispatch es2015 promises", function (assert) {
+ // https://github.com/cockpit-project/cockpit/issues/10956
+
+ assert.expect(1);
+
+ const done = assert.async();
+ const dfd = cockpit.defer();
+
+ dfd.promise.then(() => Promise.resolve(42))
+ .then(result => {
+ assert.equal(result, 42);
+ done();
+ });
+
+ dfd.resolve();
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-protocol.js b/pkg/base1/test-protocol.js
new file mode 100644
index 0000000..8da203c
--- /dev/null
+++ b/pkg/base1/test-protocol.js
@@ -0,0 +1,117 @@
+import QUnit from "qunit-tests";
+
+function connect() {
+ const ws = new WebSocket(`ws://${window.location.host}/cockpit/socket`, "cockpit1");
+
+ const connection = {
+ oncontrol: () => {},
+ onmessage: () => {},
+ onclose: () => {},
+ send: (channel, data) => ws.send(channel + "\n" + data),
+ control: message => connection.send("", JSON.stringify(message)),
+ close: () => ws.close()
+ };
+
+ ws.onmessage = event => {
+ const message = event.data;
+
+ const pos = message.indexOf("\n");
+ if (pos < 0)
+ throw new Error("invalid message");
+
+ const channel = message.substring(0, pos);
+ const data = message.substring(pos + 1);
+
+ if (channel === "")
+ connection.oncontrol(JSON.parse(data));
+ else
+ connection.onmessage(channel, data);
+ };
+
+ ws.onclose = () => connection.onclose();
+
+ return new Promise((resolve, reject) => {
+ ws.onopen = () => resolve(connection);
+ ws.onerror = () => reject();
+ });
+}
+
+QUnit.test("first message from host is init", async function (assert) {
+ assert.expect(5);
+ const done = assert.async();
+
+ const connection = await connect();
+
+ connection.oncontrol = message => {
+ assert.strictEqual(message.command, "init");
+ assert.strictEqual(message.version, 1);
+ assert.ok("channel-seed" in message);
+ assert.ok("host" in message);
+ assert.ok("csrf-token" in message);
+
+ connection.close();
+ done();
+ };
+
+ connection.onmessage = () => { throw new Error("should not be reached") };
+});
+
+QUnit.test("host must ensure that init is the first message", async function (assert) {
+ assert.expect(2);
+ const done = assert.async(2);
+
+ const connection = await connect();
+
+ // ensure that the server closes the connection on protocol error
+ connection.onclose = () => done();
+
+ // send something first that's not "init"
+ connection.control({ command: "ping" });
+
+ connection.oncontrol = message => {
+ if (message.command === "init")
+ return;
+
+ assert.equal(message.command, "close");
+ assert.equal(message.problem, "protocol-error");
+
+ done();
+ };
+});
+
+QUnit.module("tests that need test-server warnings disabled", function (hooks) {
+ /*
+ * Some of these tests will trigger cockpit-ws or cockpit-bridge to print out
+ * warnings (on protocol errors, for example). Let the test server know that
+ * before starting the tests, so it doesn't treat those messages as fatal.
+ */
+
+ // hooks wait for the promise to be resolved before continuing
+ hooks.before(() => fetch("/mock/expect-warnings"));
+ hooks.after(() => fetch("/mock/dont-expect-warnings"));
+
+ QUnit.test("host must return an error when 'channel' is not given in 'open'", async function (assert) {
+ assert.expect(2);
+ const done = assert.async(2);
+
+ const connection = await connect();
+
+ // ensure that the server closes the connection on protocol error
+ connection.onclose = () => done();
+
+ connection.oncontrol = message => {
+ if (message.command === "init")
+ return;
+
+ assert.equal(message.command, "close");
+ assert.equal(message.problem, "protocol-error");
+
+ done();
+ };
+
+ connection.control({ command: "init", version: 1 });
+ connection.control({ command: "open", payload: "fsread", path: "/etc/passwd" });
+ });
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-series.js b/pkg/base1/test-series.js
new file mode 100644
index 0000000..96290ff
--- /dev/null
+++ b/pkg/base1/test-series.js
@@ -0,0 +1,496 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("public api", function (assert) {
+ assert.equal(typeof cockpit.grid, "function", "cockpit.grid is a function");
+ assert.equal(typeof cockpit.series, "function", "cockpit.series is a function");
+
+ const grid1 = cockpit.grid(555, 3, 8);
+ assert.strictEqual(grid1.interval, 555, "grid.interval");
+ assert.strictEqual(grid1.beg, 3, "grid.beg");
+ assert.strictEqual(grid1.end, 8, "grid.end");
+ assert.equal(typeof grid1.add, "function", "grid.add()");
+ assert.equal(typeof grid1.remove, "function", "grid.remove()");
+ assert.equal(typeof grid1.close, "function", "grid.close()");
+ assert.equal(typeof grid1.notify, "function", "grid.notify()");
+ assert.equal(typeof grid1.move, "function", "grid.move()");
+ assert.equal(typeof grid1.sync, "function", "grid.sync()");
+
+ const grid2 = cockpit.grid(555, 3);
+ assert.strictEqual(grid2.beg, 3, "not-null grid.beg");
+ assert.strictEqual(grid2.end, 3, "same grid.end");
+
+ const grid3 = cockpit.grid(555, 0);
+ assert.strictEqual(grid3.end, 0, "zero grid.end");
+
+ const sink = cockpit.series(888);
+ assert.strictEqual(sink.interval, 888, "series.interval");
+ assert.equal(typeof sink.input, "function", "series.input()");
+ assert.equal(typeof sink.load, "function", "series.load()");
+});
+
+QUnit.test("calculated row", function (assert) {
+ const grid = cockpit.grid(1000, 3, 8);
+ const calculated = grid.add(function(row, x, n) {
+ for (let i = 0; i < n; i++)
+ row[i + x] = i;
+ });
+
+ grid.notify(1, 4);
+ assert.deepEqual(calculated, [undefined, 0, 1, 2, 3], "array contents");
+});
+
+QUnit.test("calculated order", function (assert) {
+ const grid = cockpit.grid(1000, 3, 8);
+
+ const calculated = grid.add(function(row, x, n) {
+ for (let i = 0; i < n; i++)
+ row[i + x] = i;
+ });
+
+ /* Callbacks must be called in the right order for this to work */
+ const dependent = grid.add(function(row, x, n) {
+ for (let i = 0; i < n; i++)
+ row[i + x] = calculated[i + x] + 10;
+ });
+
+ grid.notify(1, 4);
+ assert.deepEqual(dependent, [undefined, 10, 11, 12, 13], "dependent array contents");
+});
+
+QUnit.test("calculated early", function (assert) {
+ const grid = cockpit.grid(1000, 3, 8);
+
+ /* Callbacks must be called in the right order for this to work */
+ const dependent = grid.add(function(row, x, n) {
+ for (let i = 0; i < n; i++)
+ row[i + x] = calculated[i + x] + 10;
+ });
+
+ /* Even though this one is added after, run first, due to early flag */
+ const calculated = grid.add(function(row, x, n) {
+ for (let i = 0; i < n; i++)
+ row[i + x] = i;
+ }, true);
+
+ grid.notify(1, 4);
+ assert.deepEqual(dependent, [undefined, 10, 11, 12, 13], "dependent array contents");
+});
+
+QUnit.test("notify limit", function (assert) {
+ const grid = cockpit.grid(1000, 5, 15);
+
+ let called = -1;
+ grid.add(function(row, x, n) {
+ called = n;
+ });
+
+ grid.notify(10, 8);
+ assert.strictEqual(called, -1, "not called out of bounds");
+
+ grid.notify(1, 0);
+ assert.strictEqual(called, -1, "not called zero length");
+
+ grid.notify(1, 20);
+ assert.strictEqual(called, 9, "truncated to right limit");
+});
+
+QUnit.test("sink row", function (assert) {
+ const grid = cockpit.grid(1000, 5, 15);
+ const sink = cockpit.series(1000);
+
+ const row1 = grid.add(sink, "one.sub.2");
+ const row2 = grid.add(sink, ["one", "sub", 2]);
+ const calc = grid.add(function(row, x, n) {
+ for (let i = 0; i < n; i++)
+ row[x + i] = row1[x + i] + row2[x + i];
+ });
+
+ let notified = null;
+ grid.addEventListener("notify", (ev, n, x) => {
+ notified = [n, x];
+ });
+
+ const items = [
+ {
+ one: { sub: [200, 201, 202], another: [20, 21, 22] },
+ two: { sub: [2000, 2001, 2002], marmalade: [0, 1, 2] }
+ },
+ {
+ one: { sub: [300, 301, 302], another: [30, 31, 32] },
+ two: { sub: [3000, 3001, 3002], marmalade: [0, 1, 2] }
+ },
+ {
+ one: { sub: [400, 401, 402], another: [40, 41, 42] },
+ two: { sub: [4000, 4001, 4002], marmalade: [0, 1, 2] }
+ }
+ ];
+
+ sink.input(7, items);
+
+ assert.deepEqual(notified, [2, 3]);
+
+ assert.deepEqual(row1, [undefined, undefined, 202, 302, 402], "row with string path");
+ assert.deepEqual(row2, [undefined, undefined, 202, 302, 402], "row with array path");
+ assert.deepEqual(calc, [undefined, undefined, 404, 604, 804], "row with calculated");
+
+ grid.close();
+});
+
+QUnit.test("sink no path", function (assert) {
+ const grid = cockpit.grid(1000, 5, 15);
+ const sink = cockpit.series(1000);
+
+ const row = grid.add(sink);
+
+ const items = [567, 768, { hello: "scruffy" }];
+
+ sink.input(8, items);
+
+ assert.deepEqual(row, [undefined, undefined, undefined, 567, 768, { hello: "scruffy" }], "row without a path");
+});
+
+QUnit.test("sink after close", function (assert) {
+ const grid = cockpit.grid(1000, 5, 15);
+ const sink = cockpit.series(1000);
+
+ const row = grid.add(sink);
+
+ const items = [1, 2, 3];
+
+ sink.input(5, items);
+ assert.deepEqual(row, [1, 2, 3], "row got values");
+
+ sink.input(8, items);
+ assert.deepEqual(row, [1, 2, 3, 1, 2, 3], "row got more values");
+
+ grid.close();
+
+ sink.input(11, items);
+ assert.deepEqual(row, [1, 2, 3, 1, 2, 3], "row got no more values");
+});
+
+QUnit.test("sink mapping", function (assert) {
+ const grid = cockpit.grid(1000, 5, 15);
+ const sink = cockpit.series(1000);
+
+ const row1 = grid.add(sink, "two.sub.1");
+ const row2 = grid.add(sink, "one.sub");
+ const row3 = grid.add(sink, "invalid");
+
+ const mapping = {
+ one: { "": 0, sub: { "": 0 }, another: { "": 1 } },
+ two: { "": 1, sub: { "": 0 }, marmalade: { "": 1 } }
+ };
+
+ const items = [
+ [
+ [[200, 201, 202], [20, 21, 22]],
+ [[2000, 2001, 2002], [0, 1, 2]]
+ ],
+ [
+ [[300, 301, 302], [30, 31, 32]],
+ [[3000, 3001, 3002], [0, 1, 2]]
+ ],
+ [
+ [[400, 401, 402], [40, 41, 42]],
+ [[4000, 4001, 4002], [0, 1, 2]]
+ ]
+ ];
+
+ sink.input(5, items, mapping);
+
+ assert.deepEqual(row1, [2001, 3001, 4001], "mapped with trailing");
+ assert.deepEqual(row2, [[200, 201, 202], [300, 301, 302], [400, 401, 402]], "mapped simply");
+ assert.deepEqual(row3, [undefined, undefined, undefined], "mapped undefined");
+
+ grid.close();
+});
+
+QUnit.test("cache simple", function (assert) {
+ const fetched = [];
+ function fetch(beg, end) {
+ fetched.push([beg, end]);
+ }
+
+ const sink = cockpit.series(1000, null, fetch);
+
+ sink.input(7, [
+ {
+ one: { sub: [200, 201, 202], another: [20, 21, 22] },
+ two: { sub: [2000, 2001, 2002], marmalade: [0, 1, 2] }
+ },
+ {
+ one: { sub: [300, 301, 302], another: [30, 31, 32] },
+ two: { sub: [3000, 3001, 3002], marmalade: [0, 1, 2] }
+ },
+ {
+ one: { sub: [400, 401, 402], another: [40, 41, 42] },
+ two: { sub: [4000, 4001, 4002], marmalade: [0, 1, 2] }
+ }
+ ]);
+
+ const grid = cockpit.grid(1000, 5, 15);
+
+ let notified = null;
+ grid.addEventListener("notify", (ev, n, x) => {
+ notified = [n, x];
+ });
+
+ const row1 = grid.add(sink, "one.sub.2");
+ const row2 = grid.add(sink, ["one", "sub", 2]);
+ const calc = grid.add(function(row, x, n) {
+ for (let i = 0; i < n; i++)
+ row[x + i] = (row1[x + i] + row2[x + i]) || undefined;
+ });
+
+ grid.sync();
+
+ assert.deepEqual(fetched, [[5, 7], [10, 15]], "fetched right data");
+ assert.deepEqual(notified, [0, 10], "notified about right indexes");
+
+ assert.deepEqual(row1, [undefined, undefined, 202, 302, 402], "row with string path");
+ assert.deepEqual(row2, [undefined, undefined, 202, 302, 402], "row with array path");
+ assert.deepEqual(calc, [undefined, undefined, 404, 604, 804, undefined,
+ undefined, undefined, undefined, undefined], "row with calculated");
+
+ grid.close();
+});
+
+QUnit.test("cache multiple", function (assert) {
+ const fetched = [];
+ function fetch(beg, end) {
+ fetched.push([beg, end]);
+ }
+
+ const sink = cockpit.series(1000, null, fetch);
+
+ sink.input(7, [{
+ one: { sub: [200, 201, 202], another: [20, 21, 22] },
+ two: { sub: [2000, 2001, 2002], marmalade: [0, 1, 2] }
+ }]);
+
+ sink.input(8, [{
+ one: { sub: [300, 301, 302], another: [30, 31, 32] },
+ two: { sub: [3000, 3001, 3002], marmalade: [0, 1, 2] }
+ }]);
+
+ sink.input(9, [{
+ one: { sub: [400, 401, 402], another: [40, 41, 42] },
+ two: { sub: [4000, 4001, 4002], marmalade: [0, 1, 2] }
+ }]);
+
+ const grid = cockpit.grid(1000, 5, 15);
+
+ let notified = null;
+ grid.addEventListener("notify", (ev, n, x) => {
+ notified = [n, x];
+ });
+
+ const row1 = grid.add(sink, "one.sub.2");
+ const row2 = grid.add(sink, ["one", "sub", 2]);
+ const calc = grid.add(function(row, x, n) {
+ for (let i = 0; i < n; i++)
+ row[x + i] = (row1[x + i] + row2[x + i]) || undefined;
+ });
+
+ grid.sync();
+
+ assert.deepEqual(fetched, [[5, 7], [10, 15]], "fetched right data");
+ assert.deepEqual(notified, [0, 10], "notified about right indexes");
+
+ assert.deepEqual(row1, [undefined, undefined, 202, 302, 402], "row with string path");
+ assert.deepEqual(row2, [undefined, undefined, 202, 302, 402], "row with array path");
+ assert.deepEqual(calc, [undefined, undefined, 404, 604, 804, undefined,
+ undefined, undefined, undefined, undefined], "row with calculated");
+
+ grid.close();
+});
+
+QUnit.test("cache overlap", function (assert) {
+ const fetched = [];
+ function fetch(beg, end) {
+ fetched.push([beg, end]);
+ }
+
+ const sink = cockpit.series(1000, null, fetch);
+ const grid = cockpit.grid(1000, 5, 15);
+ const row1 = grid.add(sink, "one.sub.2");
+
+ /* Initial state of the cache */
+ sink.input(6, [{
+ one: { sub: [200, 201, 202], another: [20, 21, 22] },
+ two: { sub: [2000, 2001, 2002], marmalade: [0, 1, 2] }
+ }, {
+ one: { sub: [200, 201, 202], another: [20, 21, 22] },
+ two: { sub: [2000, 2001, 2002], marmalade: [0, 1, 2] }
+ }]);
+
+ sink.input(8, [{
+ one: { sub: [300, 301, 302], another: [30, 31, 32] },
+ two: { sub: [3000, 3001, 3002], marmalade: [0, 1, 2] }
+ }, {
+ one: { sub: [300, 301, 302], another: [30, 31, 32] },
+ two: { sub: [3000, 3001, 3002], marmalade: [0, 1, 2] }
+ }]);
+ sink.input(10, [{
+ one: { sub: [900, 901, 902], another: [90, 91, 92] },
+ two: { sub: [9000, 9001, 9002], marmalade: [0, 1, 2] }
+ }]);
+
+ assert.deepEqual(row1, [undefined, 202, 202, 302, 302, 902], "row with initial data");
+
+ /* Overlaying the data currently throws overlapping stuff out of the cache */
+ sink.input(7, [{
+ one: { sub: [400, 401, 402], another: [40, 41, 42] },
+ two: { sub: [4000, 4001, 4002], marmalade: [0, 1, 2] }
+ }, {
+ one: { sub: [400, 401, 402], another: [40, 41, 42] },
+ two: { sub: [4000, 4001, 4002], marmalade: [0, 1, 2] }
+ }]);
+
+ const row2 = grid.add(sink, "one.sub.2");
+ grid.sync();
+
+ assert.deepEqual(row1, [undefined, 202, 402, 402, 302, 902], "row with filled data");
+ assert.deepEqual(row2, [undefined, 202, 402, 402, 302, 902], "row with overlapping data");
+
+ grid.close();
+});
+
+QUnit.test("cache limit", function (assert) {
+ const series = cockpit.series(1000, null);
+ series.limit = 5;
+ series.input(8, ["eight"]);
+ series.input(6, ["six", "seven"]);
+ series.input(9, ["nine"]);
+
+ const grid = cockpit.grid(1000, 5, 15);
+ const row = grid.add(series, null);
+ grid.sync();
+
+ assert.deepEqual(row, [undefined, "six", "seven", "eight", "nine"], "initial data correct");
+
+ /* Force an expiry by adding too much data */
+ series.input(10, ["ten", "eleven"]);
+
+ /* Should have removed some data from cache */
+ grid.move(4, 14);
+ assert.deepEqual(row, [undefined, undefined, "six", "seven", undefined, "nine", "ten", "eleven"], "expired");
+
+ /* Force further expiry */
+ series.input(3, ["three", "four", "five"]);
+
+ /* Should have removed move data from cache */
+ grid.move(3, 13);
+ assert.deepEqual(row, ["three", "four", "five", undefined, undefined, undefined,
+ undefined, "ten", "eleven"], "expired more");
+
+ grid.close();
+});
+
+QUnit.test("move", function (assert) {
+ const fetched = [];
+ function fetch(beg, end) {
+ fetched.push([beg, end]);
+ }
+
+ const sink = cockpit.series(1000, null, fetch);
+ const grid = cockpit.grid(1000, 20, 25);
+
+ const row1 = grid.add(sink, "one.sub.2");
+ const row2 = grid.add(sink, ["one", "sub", 2]);
+ const calc = grid.add(function(row, x, n) {
+ for (let i = 0; i < n; i++)
+ row[x + i] = (row1[x + i] + row2[x + i]) || undefined;
+ });
+
+ let notified = null;
+ grid.addEventListener("notify", (ev, n, x) => {
+ notified = [n, x];
+ });
+
+ sink.input(7, [{
+ one: { sub: [200, 201, 202], another: [20, 21, 22] },
+ two: { sub: [2000, 2001, 2002], marmalade: [0, 1, 2] }
+ },
+ {
+ one: { sub: [300, 301, 302], another: [30, 31, 32] },
+ two: { sub: [3000, 3001, 3002], marmalade: [0, 1, 2] }
+ },
+ {
+ one: { sub: [400, 401, 402], another: [40, 41, 42] },
+ two: { sub: [4000, 4001, 4002], marmalade: [0, 1, 2] }
+ }]);
+
+ assert.deepEqual(fetched, [], "fetched no data");
+ assert.strictEqual(notified, null, "not notified");
+
+ assert.deepEqual(row1, [], "row1 empty");
+ assert.deepEqual(row2, [], "row2 empty");
+ assert.deepEqual(calc, [], "calc empty");
+
+ grid.move(5, 15);
+
+ assert.deepEqual(fetched, [[5, 7], [10, 15]], "fetched right data");
+ assert.deepEqual(notified, [0, 10], "not notified");
+
+ assert.deepEqual(row1, [undefined, undefined, 202, 302, 402], "row1 with data");
+ assert.deepEqual(row2, [undefined, undefined, 202, 302, 402], "row2 with data");
+ assert.deepEqual(calc, [undefined, undefined, 404, 604, 804, undefined,
+ undefined, undefined, undefined, undefined], "row with calculated");
+
+ grid.close();
+});
+
+QUnit.test("move negative", function (assert) {
+ const now = Date.now();
+ const grid = cockpit.grid(1000, -20, -5);
+
+ assert.equal(grid.beg, Math.floor(now / 1000) - 20);
+ assert.equal(grid.end, Math.floor(now / 1000) - 5);
+
+ grid.move(-30, -0);
+
+ assert.equal(grid.beg, Math.floor(now / 1000) - 30);
+ assert.equal(grid.end, Math.floor(now / 1000));
+
+ grid.close();
+});
+
+QUnit.test("test", function (assert) {
+ const done = assert.async();
+ assert.expect(5);
+
+ const fetched = [];
+ function fetch(beg, end) {
+ fetched.push([beg, end]);
+ }
+
+ const series = cockpit.series(100, null, fetch);
+ const grid = cockpit.grid(100, 20, 25);
+
+ grid.add(series, []);
+
+ let count = 0;
+ grid.walk();
+
+ grid.addEventListener("notify", () => {
+ count += 1;
+
+ assert.equal(count, fetched.length, "fetched " + count);
+
+ if (count == 5) {
+ grid.close();
+ done();
+ }
+ });
+});
+
+/* mock Date.now() function that returns a constant value to avoid races
+ */
+Date.now = function() {
+ return 0;
+};
+
+QUnit.start();
diff --git a/pkg/base1/test-spawn-proc.js b/pkg/base1/test-spawn-proc.js
new file mode 100644
index 0000000..f0d5b1c
--- /dev/null
+++ b/pkg/base1/test-spawn-proc.js
@@ -0,0 +1,209 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("simple process", async assert => {
+ const resp = await cockpit.spawn(["/bin/sh", "-c", "echo hi"]);
+ assert.equal(resp, "hi\n", "returned output");
+});
+
+QUnit.test("path", async assert => {
+ const resp = await cockpit.spawn(["true"]);
+ assert.equal(resp, "", "found executable");
+});
+
+QUnit.test("directory", async assert => {
+ const resp = await cockpit.spawn(["pwd"], { directory: "/tmp" });
+ assert.equal(resp, "/tmp\n", "was right");
+});
+
+QUnit.test("error log", async assert => {
+ const resp = await cockpit.spawn(["/bin/sh", "-c", "echo hi; echo yo >&2"]);
+ assert.equal(resp, "hi\n", "produced no output");
+});
+
+QUnit.test("error output", async assert => {
+ const resp = await cockpit.spawn(["/bin/sh", "-c", "echo hi; echo yo >&2"], { err: "out" });
+ assert.equal(resp, "hi\nyo\n", "showed up");
+});
+
+QUnit.test("error message", assert => {
+ const done = assert.async();
+ assert.expect(3);
+ cockpit.spawn(["/bin/sh", "-c", "echo hi; echo yo >&2"], { err: "message" })
+ .done(function(resp, message) {
+ assert.equal(resp, "hi\n", "produced output");
+ assert.equal(message, "yo\n", "produced message");
+ })
+ .always(function() {
+ assert.equal(this.state(), "resolved", "didn't fail");
+ done();
+ });
+});
+
+QUnit.test("error message fail", assert => {
+ const done = assert.async();
+ assert.expect(3);
+ cockpit.spawn(["/bin/sh", "-c", "echo hi; echo yo >&2; exit 2"], { err: "message" })
+ .fail(function(ex, resp) {
+ assert.equal(resp, "hi\n", "produced output");
+ assert.equal(ex.message, "yo", "produced message");
+ })
+ .always(function() {
+ assert.equal(this.state(), "rejected", "didn't fail");
+ done();
+ });
+});
+
+QUnit.test("nonexisting executable", assert => {
+ assert.rejects(cockpit.spawn(["/bin/nonexistent"]),
+ ex => ex.problem == "not-found");
+});
+
+QUnit.test("permission denied", assert => {
+ assert.rejects(cockpit.spawn(["/etc/hostname"]),
+ ex => ex.problem == "access-denied");
+});
+
+QUnit.test("write eof read", async assert => {
+ const proc = cockpit.spawn(["/usr/bin/sort"]);
+ proc.input("2\n", true);
+ proc.input("3\n1\n");
+ assert.equal(await proc, "1\n2\n3\n", "output");
+});
+
+QUnit.test("stream", async assert => {
+ let streamed = 0;
+ let result = "";
+ const resp = await cockpit.spawn(["/bin/cat"])
+ .input("11\n", true)
+ .input("22\n", true)
+ .input("33\n")
+ .stream(resp => {
+ result += resp;
+ streamed += 1;
+ });
+ assert.equal(resp, "", "no then data");
+ assert.equal(result, "11\n22\n33\n", "stream data");
+ assert.ok(streamed > 0, "stream handler called");
+});
+
+QUnit.test("stream packets", async assert => {
+ let streamed = "";
+ const resp = await cockpit.spawn(["/bin/cat"])
+ .input("11\n", true)
+ .input("22\n", true)
+ .input("33\n")
+ .stream(resp => { streamed += resp });
+
+ assert.equal(resp, "", "no then data");
+ assert.equal(streamed, "11\n22\n33\n", "stream data");
+});
+
+QUnit.test("stream replaced", async assert => {
+ let first = false;
+ let second = false;
+
+ await cockpit.spawn(["/bin/cat"])
+ .input("11\n", true)
+ .input("22\n", true)
+ .input("33\n")
+ .stream(() => { first = true })
+ .stream(() => { second = true });
+
+ assert.ok(!first, "first stream handler not called");
+ assert.ok(second, "second stream handler called");
+});
+
+QUnit.test("stream partial", async assert => {
+ let streamed = "";
+ const resp = await cockpit.spawn(["/bin/cat"])
+ .input("1234")
+ .stream(chunk => {
+ if (chunk.length > 0) {
+ streamed += chunk[0];
+ return 1;
+ }
+ });
+ assert.equal(resp, "234", "right then data");
+ assert.equal(streamed, "1", "stream data");
+});
+
+QUnit.test("stream partial binary", async assert => {
+ const streamed = [];
+ const resp = await cockpit.spawn(["/bin/cat"], { binary: true })
+ .input("1234")
+ .stream(chunk => {
+ if (chunk.length > 0) {
+ streamed.push(chunk[0]);
+ return 1;
+ }
+ });
+ assert.equal(resp.length, 3, "right then data");
+ assert.deepEqual(streamed, [49], "stream data");
+});
+
+QUnit.test("script with input", async assert => {
+ const script = "#!/bin/sh\n\n# Test\n/usr/bin/sort\necho $2\necho $1";
+ const proc = cockpit.script(script, ["5", "4"]);
+ proc.input("2\n", true);
+ proc.input("3\n1\n");
+ assert.equal(await proc, "1\n2\n3\n4\n5\n", "output matched");
+});
+
+QUnit.test("script with options", async assert => {
+ const script = "#!/bin/sh\n\n# Test\n/usr/bin/sort\necho $2\necho $1 >&2";
+ const proc = cockpit.script(script, ["5", "4"], { err: "out" });
+ proc.input("2\n", true);
+ proc.input("3\n1\n");
+ assert.equal(await proc, "1\n2\n3\n4\n5\n", "output matched");
+});
+
+QUnit.test("script without args", async assert => {
+ const script = "#!/bin/sh\n\n# Test\n/usr/bin/sort >&2";
+ const proc = cockpit.script(script, { err: "out" });
+ proc.input("2\n", true);
+ proc.input("3\n1\n");
+ assert.equal(await proc, "1\n2\n3\n", "output matched");
+});
+
+QUnit.test("pty", async assert => {
+ const proc = cockpit.spawn(['sh', '-c', "tty; test -t 0"], { pty: true });
+ const output = await proc;
+ assert.equal(output.indexOf('/dev/pts'), 0, 'TTY is a pty: ' + output);
+});
+
+QUnit.test("pty window size", async assert => {
+ const proc = cockpit.spawn(['tput', 'lines', 'cols'], {
+ pty: true,
+ environ: ["TERM=vt100"],
+ window: { rows: 77, cols: 88 }
+ });
+ assert.equal(await proc, '77\r\n88\r\n', 'Correct rows and columns');
+});
+
+QUnit.test("stream large output", async assert => {
+ let lastblock = null;
+ const resp = await cockpit.spawn(["seq", "10000000"])
+ .stream(resp => {
+ if (lastblock === null)
+ assert.equal(resp.slice(0, 4), "1\n2\n", "stream data starts with first numbers");
+ lastblock = resp;
+ });
+ assert.equal(resp, "", "no then data");
+ assert.equal(lastblock.slice(-18), "\n9999999\n10000000\n", "stream data has last numbers");
+});
+
+QUnit.test("cancel process", async assert => {
+ const proc = cockpit.spawn(["sleep", "418"]);
+ await cockpit.script("until pgrep -af [s]leep.*418; do sleep 0.1; done");
+ proc.close("cancelled");
+ await cockpit.script("timeout 5 sh -ec 'while pgrep -af [s]leep.*418; do sleep 0.1; done'");
+ try {
+ await proc;
+ assert.ok(false, "proc should have failed");
+ } catch (ex) {
+ assert.equal(ex.problem, "cancelled", "proc failed with correct problem");
+ }
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-spawn.js b/pkg/base1/test-spawn.js
new file mode 100644
index 0000000..573bf94
--- /dev/null
+++ b/pkg/base1/test-spawn.js
@@ -0,0 +1,353 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+function MockPeer() {
+ /*
+ * Events triggered here:
+ * open(event, args)
+ * recv(event, payload)
+ * close(event, problem)
+ * get(event, path)
+ */
+ cockpit.event_target(this);
+
+ /* open: triggered when mock Channel is created */
+ this.onopened = function(event, channel, options) {
+ /* nada */
+ };
+
+ /* close event: triggered when mock Channel is closed */
+ this.onclosed = function(event, channel, options) {
+ /* nada */
+ };
+
+ this.oncontrol = function(event, channel, options) {
+ /* nada */
+ };
+
+ /* send a message from peer back to channel */
+ this.send = function(channel, payload) {
+ if (typeof (payload) != "string")
+ payload = String(payload);
+ window.setTimeout(function() {
+ if (channel.valid)
+ channel.dispatchEvent("message", payload);
+ else
+ console.log("dropping message after close from MockPeer");
+ }, 5);
+ };
+
+ /* peer closes the channel */
+ this.close = function(channel, options) {
+ console.assert(channel);
+ window.setTimeout(function() {
+ if (channel.valid) {
+ channel.valid = false;
+ channel.dispatchEvent("close", options || { });
+ }
+ }, 5);
+ };
+
+ const peer = this;
+ let last_channel = 0;
+
+ function MockChannel(options) {
+ cockpit.event_target(this);
+ this.number = last_channel++;
+ this.options = options;
+ this.valid = true;
+
+ const channel = this;
+
+ function Transport() {
+ this.close = function(problem) { console.assert(arguments.length == 1) };
+ }
+
+ this.transport = new Transport();
+
+ this.send = function(payload) {
+ console.assert(arguments.length == 1);
+ console.assert(this.valid);
+ window.setTimeout(function() { peer.dispatchEvent("recv", channel, payload) }, 5);
+ };
+
+ this.control = function(options) {
+ console.assert(typeof command === 'string');
+ console.assert(options !== null && typeof options === 'object');
+ console.assert(arguments.length == 1);
+ window.setTimeout(function() { peer.dispatchEvent("control", channel, options) }, 5);
+ };
+
+ this.close = function(options) {
+ console.assert(arguments.length <= 1);
+ this.valid = false;
+ window.setTimeout(function() { peer.dispatchEvent("closed", channel, options || { }) }, 5);
+ this.dispatchEvent("close", options || { });
+ };
+
+ this.buffer = function(callback) {
+ const buffers = [];
+ buffers.callback = callback;
+ buffers.squash = function squash() {
+ return buffers.join("");
+ };
+
+ this.onmessage = function(event, data) {
+ buffers.push(data);
+ if (buffers.callback) {
+ const block = buffers.squash();
+ if (block.length > 0) {
+ const consumed = buffers.callback.call(this, block);
+ if (typeof consumed !== "number" || consumed === block.length) {
+ buffers.length = 0;
+ } else {
+ buffers.length = 1;
+ buffers[0] = block.substring(consumed);
+ }
+ }
+ }
+ };
+
+ return buffers;
+ };
+
+ QUnit.testDone(function() {
+ channel.valid = false;
+ });
+
+ peer.dispatchEvent("opened", channel, options);
+ }
+
+ cockpit.channel = function(options) {
+ return new MockChannel(options);
+ };
+}
+
+QUnit.test("public api", function (assert) {
+ assert.equal(typeof cockpit.spawn, "function", "spawn is a function");
+});
+
+QUnit.test("simple request", async assert => {
+ const peer = new MockPeer();
+ peer.addEventListener("opened", function(event, channel, options) {
+ assert.deepEqual(channel.options.spawn, ["/the/path", "arg1", "arg2"], "passed spawn correctly");
+ assert.equal(channel.options.host, undefined, "had no host");
+ });
+ peer.addEventListener("recv", function(event, channel, payload) {
+ assert.equal(payload, "input", "had input");
+ this.send(channel, "output");
+ this.close(channel);
+ });
+
+ const resp = await cockpit.spawn(["/the/path", "arg1", "arg2"])
+ .input("input", true);
+ assert.deepEqual(resp, "output", "returned right json");
+});
+
+QUnit.test("input large", function (assert) {
+ const done = assert.async();
+ assert.expect(25);
+
+ const str = new Array(128 * 1024).join('abcdef12345');
+ let output = "";
+ let count = 0;
+
+ const peer = new MockPeer();
+ peer.addEventListener("recv", function(event, channel, payload) {
+ assert.ok(typeof (payload) == "string", "got payload");
+ output += payload;
+ count += 1;
+ });
+ peer.addEventListener("control", function(event, channel, options) {
+ if (options.command == "done")
+ this.close(channel);
+ });
+
+ cockpit.spawn(["/path/to/command"])
+ .input(str)
+ .always(function() {
+ assert.equal(this.state(), "resolved", "didn't fail");
+ assert.equal(str, output, "right output");
+ assert.ok(count > 1, "broken into multiple blocks");
+ done();
+ });
+});
+
+QUnit.test("binary large", function (assert) {
+ const done = assert.async();
+ assert.expect(10);
+
+ const data = new Uint8Array(249 * 1023);
+ const len = data.byteLength;
+ for (let i = 0; i < len; i++)
+ data[i] = i % 233;
+
+ let count = 0;
+
+ const peer = new MockPeer();
+ peer.addEventListener("recv", function(event, channel, payload) {
+ console.log(typeof (payload), payload.constructor);
+ assert.equal(typeof (payload), "object", "got payload");
+ assert.equal(payload.constructor, Uint8Array, "right binary array");
+ count += 1;
+ });
+ peer.addEventListener("control", function(event, channel, options) {
+ console.log("control", options);
+ if (options.command == "done")
+ this.close(channel);
+ });
+
+ cockpit.spawn(["/ptah/to/command"])
+ .input(data)
+ .always(function() {
+ assert.equal(this.state(), "resolved", "didn't fail");
+ assert.ok(count > 1, "broken into multiple blocks");
+ done();
+ });
+});
+
+QUnit.test("string command", function (assert) {
+ const done = assert.async();
+ assert.expect(2);
+
+ const peer = new MockPeer();
+ peer.addEventListener("opened", function(event, channel, options) {
+ assert.deepEqual(channel.options.spawn, ["/the/path"], "passed spawn correctly");
+ assert.equal(channel.options.host, "hostname", "had host");
+ done();
+ });
+
+ cockpit.spawn("/the/path", { host: "hostname" });
+});
+
+QUnit.test("channel options", function (assert) {
+ const done = assert.async();
+ assert.expect(1);
+
+ const peer = new MockPeer();
+ peer.addEventListener("opened", function(event, channel) {
+ assert.deepEqual(channel.options, {
+ spawn: ["/the/path", "arg"],
+ host: "the-other-host.example.com",
+ "extra-option": "zerogjuggs",
+ payload: "stream"
+ }, "sent correctly");
+ done();
+ });
+
+ /* Don't care about the result ... */
+ const options = { "extra-option": "zerogjuggs", host: "the-other-host.example.com" };
+ cockpit.spawn(["/the/path", "arg"], options);
+});
+
+QUnit.test("streaming", assert => {
+ const done = assert.async();
+ assert.expect(15);
+
+ const peer = new MockPeer();
+ peer.addEventListener("opened", function(event, channel) {
+ for (let i = 0; i < 10; i++)
+ this.send(channel, String(i));
+ this.close(channel);
+ });
+
+ let at = 0;
+ const promise = cockpit.spawn(["/unused"])
+ .stream(function(resp) {
+ assert.equal(String(at), resp, "stream got right data");
+ if (at === 0)
+ assert.strictEqual(this, promise, "stream got right this");
+ at++;
+ })
+ .done(function(resp) {
+ assert.ok(!resp, "stream didn't send data to done");
+ assert.strictEqual(this, promise, "done got right this");
+ })
+ .always(function() {
+ assert.equal(this.state(), "resolved", "split response didn't fail");
+ assert.strictEqual(this, promise, "always got right this");
+ done();
+ });
+});
+
+QUnit.test("with problem", async assert => {
+ const peer = new MockPeer();
+ peer.addEventListener("opened", (_event, channel) => {
+ peer.close(channel, { problem: "not-found" });
+ });
+
+ try {
+ await cockpit.spawn("/unused");
+ assert.ok(false, "should not be reached");
+ } catch (ex) {
+ assert.equal(ex.problem, "not-found", "got problem");
+ assert.strictEqual(ex.exit_signal, null, "got no signal");
+ assert.strictEqual(ex.exit_status, null, "got no status");
+ }
+});
+
+QUnit.test("with status", function (assert) {
+ const done = assert.async();
+ assert.expect(5);
+
+ const peer = new MockPeer();
+ peer.addEventListener("opened", function(event, channel) {
+ peer.send(channel, "the data");
+ peer.close(channel, { "exit-status": 5 });
+ });
+
+ cockpit.spawn("/unused")
+ .fail(function(ex, data) {
+ assert.strictEqual(ex.problem, null, "got null problem");
+ assert.strictEqual(ex.exit_signal, null, "got no signal");
+ assert.strictEqual(ex.exit_status, 5, "got status");
+ assert.equal(data, "the data", "got data even with exit status");
+ })
+ .always(function() {
+ assert.equal(this.state(), "rejected", "should fail");
+ done();
+ });
+});
+
+QUnit.test("with signal", function (assert) {
+ const done = assert.async();
+ assert.expect(5);
+
+ const peer = new MockPeer();
+ peer.addEventListener("opened", function(event, channel) {
+ peer.send(channel, "signal data here");
+ peer.close(channel, { "exit-signal": "TERM" });
+ });
+
+ cockpit.spawn("/unused")
+ .fail(function(ex, data) {
+ assert.strictEqual(ex.problem, null, "got null problem");
+ assert.strictEqual(ex.exit_signal, "TERM", "got signal");
+ assert.strictEqual(ex.exit_status, null, "got no status");
+ assert.equal(data, "signal data here", "got data even with signal");
+ })
+ .always(function() {
+ assert.equal(this.state(), "rejected", "should fail");
+ done();
+ });
+});
+
+QUnit.test("spawn promise recursive", function (assert) {
+ assert.expect(7);
+
+ const promise = cockpit.spawn(["/the/path", "arg1", "arg2"]);
+
+ const target = { };
+ const promise2 = promise.promise(target);
+ assert.strictEqual(promise2, target, "used target");
+ assert.equal(typeof promise2.done, "function", "promise2.done()");
+ assert.equal(typeof promise2.promise, "function", "promise2.promise()");
+ assert.equal(typeof promise2.input, "function", "promise2.input()");
+
+ const promise3 = promise2.promise();
+ assert.equal(typeof promise3.done, "function", "promise3.done()");
+ assert.equal(typeof promise3.promise, "function", "promise3.promise()");
+ assert.equal(typeof promise3.input, "function", "promise3.input()");
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-stream.js b/pkg/base1/test-stream.js
new file mode 100644
index 0000000..f8d5a15
--- /dev/null
+++ b/pkg/base1/test-stream.js
@@ -0,0 +1,94 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+const QS_REQUEST = "HEAD /mock/qs HTTP/1.0\nHOST: localhost\n\n";
+
+const test_server = {
+ address: window.location.hostname,
+ port: parseInt(window.location.port, 10)
+};
+
+QUnit.test("TCP stream port without a service", async assert => {
+ const done = assert.async();
+ assert.expect(2);
+
+ const is_pybridge = await QUnit.mock_info("pybridge");
+
+ const channel = cockpit.channel({ payload: "stream", address: "127.0.0.99", port: 2222 });
+
+ channel.addEventListener("close", (ev, options) => {
+ assert.equal(options.problem, "not-found", "channel should have failed");
+ if (is_pybridge)
+ assert.equal(options.message,
+ "[Errno 111] Connect call failed ('127.0.0.99', 2222)",
+ "detailed error message");
+ else
+ assert.equal(options.message, undefined, "C bridge does not give detailed error message");
+ done();
+ });
+});
+
+QUnit.test("TCP stream address without a port", assert => {
+ const done = assert.async();
+ assert.expect(2);
+
+ const channel = cockpit.channel({ payload: "stream", address: "127.0.0.99" });
+
+ channel.addEventListener("close", (ev, options) => {
+ assert.equal(options.problem, "protocol-error", "channel should have failed");
+ assert.equal(options.message, 'no "port" or "unix" or other address option for channel', "helpful error");
+ done();
+ });
+});
+
+QUnit.test("TCP text stream", async assert => {
+ const done = assert.async();
+ assert.expect(2);
+
+ const channel = cockpit.channel({
+ payload: "stream",
+ address: test_server.address,
+ port: test_server.port
+ });
+
+ channel.addEventListener("message", (ev, data) => {
+ assert.ok(data.startsWith("HTTP/1.1 200 OK"), "got successful HTTP response");
+ channel.close();
+ });
+
+ channel.addEventListener("close", (ev, options) => {
+ assert.equal(options.problem, undefined, "channel should have succeeded");
+ done();
+ });
+
+ channel.send(QS_REQUEST);
+ channel.control({ command: "done" });
+});
+
+QUnit.test("TCP binary stream", async assert => {
+ const done = assert.async();
+ assert.expect(2);
+
+ const channel = cockpit.channel({
+ payload: "stream",
+ binary: true,
+ address: test_server.address,
+ port: test_server.port
+ });
+
+ channel.addEventListener("message", (ev, data) => {
+ const text = new TextDecoder().decode(data);
+ assert.ok(text.startsWith("HTTP/1.1 200 OK"), "got successful HTTP response");
+ channel.close();
+ });
+
+ channel.addEventListener("close", (ev, options) => {
+ assert.equal(options.problem, undefined, "channel should have succeeded");
+ done();
+ });
+
+ channel.send(Uint8Array.from(QS_REQUEST.split('').map(c => c.charCodeAt())));
+ channel.control({ command: "done" });
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-user.js b/pkg/base1/test-user.js
new file mode 100644
index 0000000..f38c485
--- /dev/null
+++ b/pkg/base1/test-user.js
@@ -0,0 +1,38 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("load user info", async assert => {
+ const dbus = cockpit.dbus(null, { bus: "internal" });
+ const [user] = await dbus.call("/user", "org.freedesktop.DBus.Properties",
+ "GetAll", ["cockpit.User"], { type: "s" });
+ assert.ok(user.Name !== undefined, "has Name");
+ assert.equal(user.Name.t, "s", "string Name");
+ assert.ok(user.Full !== undefined, "has Full name");
+ assert.equal(user.Full.t, "s", "string Full");
+ assert.ok(user.Shell !== undefined, "has Shell");
+ assert.equal(user.Home.t, "s", "type Home");
+ assert.equal(user.Home.v.indexOf("/"), 0, "Home starts with slash");
+ assert.equal(user.Groups.t, "as", "type Groups");
+});
+
+QUnit.test("user object", async assert => {
+ const user = await cockpit.user();
+ assert.equal(typeof user.name, "string", "user name");
+ assert.equal(typeof user.full_name, "string", "user full name");
+ assert.equal(typeof user.shell, "string", "user shell");
+ assert.equal(typeof user.home, "string", "user home");
+ assert.equal(typeof user.id, "number", "user id");
+ assert.ok(Array.isArray(user.groups), "user groups");
+});
+
+QUnit.test("user environment", async assert => {
+ const data = await cockpit.spawn(["/bin/sh", "-c", "echo $USER~$SHELL~$HOME"]);
+ const parts = data.split("~");
+ assert.ok(parts[0].length > 0, "valid $USER");
+ assert.ok(parts[1].length > 0, "valid $HOME");
+ assert.equal(parts[1].indexOf("/"), 0, "$HOME starts with slash");
+ assert.ok(parts[2].length > 0, "valid $SHELL");
+ assert.equal(parts[1].indexOf("/"), 0, "$SHELL starts with slash");
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-utf8.js b/pkg/base1/test-utf8.js
new file mode 100644
index 0000000..6a09d82
--- /dev/null
+++ b/pkg/base1/test-utf8.js
@@ -0,0 +1,129 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("utf8 basic", function (assert) {
+ const str = "Base 64 \u2014 Mozilla Developer Network";
+ const expect = [66, 97, 115, 101, 32, 54, 52, 32, 226, 128, 148, 32, 77,
+ 111, 122, 105, 108, 108, 97, 32, 68, 101, 118, 101, 108,
+ 111, 112, 101, 114, 32, 78, 101, 116, 119, 111, 114, 107];
+
+ const encoded = cockpit.utf8_encoder().encode(str);
+ assert.deepEqual(encoded, expect, "encoded");
+
+ assert.equal(cockpit.utf8_decoder().decode(encoded), str, "decoded");
+});
+
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Inspired by:
+// http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
+
+// Helpers for test_utf_roundtrip.
+
+QUnit.test("utf8 round trip", function (assert) {
+ const MIN_CODEPOINT = 0;
+ const MAX_CODEPOINT = 0x10FFFF;
+ const BLOCK_SIZE = 0x1000;
+ const SKIP_SIZE = 31;
+ const encoder = cockpit.utf8_encoder();
+ const decoder = cockpit.utf8_decoder();
+
+ function cpname(n) {
+ if (n + 0 !== n)
+ return n.toString();
+ const w = (n <= 0xFFFF) ? 4 : 6;
+ return 'U+' + ('000000' + n.toString(16).toUpperCase()).slice(-w);
+ }
+
+ function genblock(from, len, skip) {
+ const block = [];
+ for (let i = 0; i < len; i += skip) {
+ let cp = from + i;
+ if (cp >= 0xD800 && cp <= 0xDFFF)
+ continue;
+ if (cp < 0x10000) {
+ block.push(String.fromCharCode(cp));
+ continue;
+ }
+ cp = cp - 0x10000;
+ block.push(String.fromCharCode(0xD800 + (cp >> 10)));
+ block.push(String.fromCharCode(0xDC00 + (cp & 0x3FF)));
+ }
+ return block.join('');
+ }
+
+ for (let i = MIN_CODEPOINT; i < MAX_CODEPOINT; i += BLOCK_SIZE) {
+ const block_tag = cpname(i) + " - " + cpname(i + BLOCK_SIZE - 1);
+ const block = genblock(i, BLOCK_SIZE, SKIP_SIZE);
+ const encoded = encoder.encode(block);
+ const decoded = decoder.decode(encoded);
+
+ const length = block.length;
+ for (let j = 0; j < length; j++) {
+ if (block[j] != decoded[j])
+ assert.deepEqual(block, decoded, "round trip " + block_tag);
+ }
+ }
+
+ assert.ok(true, "round trip all code points");
+});
+
+QUnit.test("utf8 samples", function (assert) {
+ // z, cent, CJK water, G-Clef, Private-use character
+ const sample = "z\xA2\u6C34\uD834\uDD1E\uDBFF\uDFFD";
+ const expected = [0x7A, 0xC2, 0xA2, 0xE6, 0xB0, 0xB4, 0xF0, 0x9D, 0x84, 0x9E, 0xF4, 0x8F, 0xBF, 0xBD];
+
+ const encoded = cockpit.utf8_encoder().encode(sample);
+ assert.deepEqual(encoded, expected, "encoded");
+
+ const decoded = cockpit.utf8_decoder().decode(expected);
+ assert.deepEqual(decoded, sample, "decoded");
+});
+
+QUnit.test("utf8 stream", function (assert) {
+ // z, cent, CJK water, G-Clef, Private-use character
+ const sample = "z\xA2\u6C34\uD834\uDD1E\uDBFF\uDFFD";
+ const expected = [0x7A, 0xC2, 0xA2, 0xE6, 0xB0, 0xB4, 0xF0, 0x9D, 0x84, 0x9E, 0xF4, 0x8F, 0xBF, 0xBD];
+
+ const decoder = cockpit.utf8_decoder();
+ let decoded = "";
+
+ for (let i = 0; i < expected.length; i += 2)
+ decoded += decoder.decode(expected.slice(i, i + 2), { stream: true });
+ decoded += decoder.decode();
+
+ assert.deepEqual(decoded, sample, "decoded");
+});
+
+QUnit.test("utf8 invalid", function (assert) {
+ const sample = "Base 64 \ufffd\ufffd Mozilla Developer Network";
+ const data = [66, 97, 115, 101, 32, 54, 52, 32, 226, /* 128 */ 148, 32, 77,
+ 111, 122, 105, 108, 108, 97, 32, 68, 101, 118, 101, 108,
+ 111, 112, 101, 114, 32, 78, 101, 116, 119, 111, 114, 107];
+
+ const decoded = cockpit.utf8_decoder().decode(data);
+
+ assert.deepEqual(decoded, sample, "decoded");
+});
+
+QUnit.test("utf8 fatal", function (assert) {
+ const data = [66, 97, 115, 101, 32, 54, 52, 32, 226, /* 128 */ 148, 32, 77,
+ 111, 122, 105, 108, 108, 97, 32, 68, 101, 118, 101, 108,
+ 111, 112, 101, 114, 32, 78, 101, 116, 119, 111, 114, 107];
+
+ assert.throws(function() { cockpit.utf8_decoder(true).decode(data) }, "fatal throws error");
+});
+
+QUnit.start();
diff --git a/pkg/base1/test-websocket.js b/pkg/base1/test-websocket.js
new file mode 100644
index 0000000..8f2e0fa
--- /dev/null
+++ b/pkg/base1/test-websocket.js
@@ -0,0 +1,81 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+QUnit.test("external channel websocket", function (assert) {
+ const done = assert.async();
+ assert.expect(3);
+
+ const query = window.btoa(JSON.stringify({
+ payload: "websocket-stream1",
+ address: "localhost",
+ port: parseInt(window.location.port, 10),
+ path: "/cockpit/echosocket/",
+ }));
+
+ let count = 0;
+ const ws = new WebSocket("ws://" + window.location.host + "/cockpit/channel/" +
+ cockpit.transport.csrf_token + '?' + query);
+ ws.onopen = function() {
+ assert.ok(true, "websocket is open");
+ ws.send("oh marmalade");
+ };
+ ws.onerror = function() {
+ assert.ok(false, "websocket error");
+ };
+ ws.onmessage = function(ev) {
+ if (count === 0) {
+ assert.equal(ev.data, "OH MARMALADE", "got payload");
+ ws.send("another test");
+ count += 1;
+ } else {
+ assert.equal(ev.data, "ANOTHER TEST", "got payload again");
+ ws.close(1000);
+ }
+ };
+ ws.onclose = function(ev) {
+ done();
+ };
+});
+
+QUnit.test("bad channel options websocket", function (assert) {
+ const done = assert.async();
+ const payloads = [
+ window.btoa(JSON.stringify({
+ payload: "websocket-stream1",
+ address: "localhost",
+ port: 'bad',
+ path: "/cockpit/echosocket/",
+ })),
+ window.btoa(JSON.stringify({
+ payload: "websocket-stream1",
+ address: "localhost",
+ port: parseInt(window.location.port, 10),
+ }))
+ ];
+ assert.expect(payloads.length * 3);
+ function step() {
+ const query = payloads.shift();
+ const url = "ws://" + window.location.host + "/cockpit/channel/" +
+ cockpit.transport.csrf_token + '?' + query;
+ console.log(url);
+ let ws = new WebSocket(url);
+ ws.onopen = function() {
+ assert.ok(true, "websocket opened");
+ };
+ ws.onclose = function(ev) {
+ console.log(ev);
+ assert.ok(ev.wasClean, url + "websocket unclean shutdown");
+ assert.notEqual(ev.code, 0, url + "websocket error code");
+ ws = null;
+ if (payloads.length === 0)
+ done();
+ else
+ step();
+ };
+ }
+ step();
+});
+
+cockpit.transport.wait(function() {
+ QUnit.start();
+});
diff --git a/pkg/kdump/config-client-suse.js b/pkg/kdump/config-client-suse.js
new file mode 100644
index 0000000..bbd5507
--- /dev/null
+++ b/pkg/kdump/config-client-suse.js
@@ -0,0 +1,265 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 SUSE LLC
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import { ConfigFile } from './config-client.js';
+
+/* Parse an dotenv-style config file
+ * and monitor it for changes
+ */
+export class ConfigFileSUSE extends ConfigFile {
+ /* parse lines of the config file
+ * if a line has a valid key=value format, use the key in _internal structure
+ * and also store original line, line index, value and optional line suffix / comment
+ * if value was quoted it will be stripped of quotes in `value` and `quoted` flag will
+ * be used when writing the file to keep original formatting
+ * e.g. for line 'someKey="foo" # comment'
+ * outputObject._internal["someKey"] = {
+ * index: 0,
+ * value: "foo",
+ * quoted: true,
+ * origLine: 'someKey="foo" # comment',
+ * suffix: "# comment"
+ * }
+ * skipNotify: Don't notify about changes, e.g.to avoid multiple updates when writing a file
+ */
+ _parseText(rawContent, skipNotify = false) {
+ this._dataAvailableResolve();
+
+ // clear settings if file is empty/missing
+ if (!rawContent) {
+ this._originalSettings = null;
+ this.settings = null;
+ if (!skipNotify)
+ this.dispatchEvent("kdumpConfigChanged", this.settings);
+ return;
+ }
+
+ // if nothing changed, don't bother parsing the content
+ if (rawContent == this._rawContent)
+ return;
+
+ this._rawContent = rawContent;
+
+ // this is the format expected by the UI
+ this.settings = {
+ _internal: {},
+ targets: {},
+ compression: { enabled: false, allowed: true },
+ };
+
+ this._lines = rawContent.split(/\r?\n/);
+ this._lines.forEach((line, index) => {
+ const trimmed = line.trim();
+ // if the line is empty or only a comment, skip
+ if (trimmed.indexOf("#") === 0 || trimmed.length === 0)
+ return;
+
+ // parse KEY=value or KEY="value" line
+ let parts = trimmed.match(/^([A-Z_]+)\s*=\s*(.*)$/);
+ if (parts === null) {
+ console.warn("Malformed kdump config line:", trimmed, "in", this.filename);
+ return;
+ }
+ const key = parts[1];
+ let value = parts[2];
+
+ // value might be quoted
+ let quoted = false;
+ if (value.startsWith('"')) {
+ quoted = true;
+ parts = value.match(/^"([^"]*)"\s*(.*)$/);
+ // malformed line, no ending quote?
+ if (parts === null) {
+ console.warn("Incorrectly quoted value in kdump config line:", line, "in", this.filename);
+ return;
+ }
+ } else {
+ // not quoted should be simple value but grab everything and quote on write
+ parts = value.match(/^([^#]+?)\s*(#.*)?$/);
+ if (parts === null)
+ parts = ["", ""];
+ }
+ value = parts[1];
+ const suffix = (parts[2] || "").trim();
+
+ this.settings._internal[key] = {
+ index,
+ value,
+ origLine: line,
+ quoted,
+ suffix
+ };
+ });
+
+ // make sure we copy the original keys so we overwrite the correct lines when saving
+ this._originalSettings = { };
+ Object.keys(this.settings._internal).forEach((key) => {
+ this._originalSettings[key] = { ...this.settings._internal[key] };
+ });
+
+ this._extractSettings();
+
+ if (!skipNotify)
+ this.dispatchEvent("kdumpConfigChanged", this.settings);
+ }
+
+ /* extract settings managed by cockpit from _internal into platform independent model
+ */
+ _extractSettings() {
+ // generate target(s) from KDUMP_SAVEDIR
+ if ("KDUMP_SAVEDIR" in this.settings._internal && this.settings._internal.KDUMP_SAVEDIR.value) {
+ let savedir = this.settings._internal.KDUMP_SAVEDIR.value;
+ // handle legacy "file" without prefix
+ if (savedir.startsWith("/"))
+ savedir = "file://" + savedir;
+ // server includes "username:password@" and can be empty for file://
+ const parts = savedir.match(/^(.*):\/\/([^/]*)(\/.*)$/);
+ // malformed KDUMP_SAVEDIR
+ if (parts === null) {
+ console.warn("Malformed KDUMP_SAVEDIR entry:", savedir, "in", this.filename);
+ return;
+ }
+ const [, scheme, server, path] = parts;
+ if (scheme === "file") {
+ this.settings.targets.local = {
+ type: "local",
+ path,
+ };
+ } else if (scheme === "nfs") {
+ this.settings.targets.nfs = {
+ type: scheme,
+ // on read full path is used as export
+ export: path,
+ server,
+ };
+ } else {
+ this.settings.targets[scheme] = {
+ type: scheme,
+ path,
+ server,
+ };
+ // sshkey is used by ssh and sftp/scp
+ if ("KDUMP_SSH_IDENTITY" in this.settings._internal) {
+ this.settings.targets[scheme].sshkey =
+ this.settings._internal.KDUMP_SSH_IDENTITY.value;
+ }
+ }
+ }
+
+ // default to local if no target configured
+ if (Object.keys(this.settings.targets).length === 0)
+ this.settings.targets.local = { type: "local" };
+
+ this.settings.compression.enabled = (
+ !("KDUMP_DUMPFORMAT" in this.settings._internal) ||
+ // TODO: what about other compression formats (lzo, snappy)?
+ this.settings._internal.KDUMP_DUMPFORMAT.value === "compressed"
+ );
+ }
+
+ /* update single _internal setting to given value
+ * make sure setting exists if value is not empty
+ * don't delete existing settings
+ */
+ _updateSetting(settings, key, value) {
+ if (key in settings._internal) {
+ settings._internal[key].value = value;
+ } else {
+ if (value)
+ settings._internal[key] = { value };
+ }
+ }
+
+ /* transform settings from model back to _internal format
+ * this.settings = current state from file
+ * settings = in-memory state from UI
+ */
+ _persistSettings(settings) {
+ // target
+ if (Object.keys(settings.targets).length > 0) {
+ const target = Object.values(settings.targets)[0];
+
+ if ("sshkey" in target)
+ this._updateSetting(settings, "KDUMP_SSH_IDENTITY", target.sshkey);
+
+ let savedir;
+ // default for empty path (except nfs, see below)
+ let path = target.path || "/var/crash";
+ if (path && !path.startsWith("/"))
+ path = "/" + path;
+ if (target.type === "local") {
+ savedir = "file://" + path;
+ } else if (target.type === "nfs") {
+ // override empty path default as nfs path is merged into export on read
+ if (!target.path)
+ path = "";
+ let exprt = target.export;
+ if (!exprt.startsWith("/"))
+ exprt = "/" + exprt;
+ savedir = "nfs://" + target.server + exprt + path;
+ } else {
+ savedir = target.type + "://" + target.server + path;
+ }
+ this._updateSetting(settings, "KDUMP_SAVEDIR", savedir);
+ }
+ // compression
+ if (this.settings.compression.enabled != settings.compression.enabled) {
+ if (settings.compression.enabled) {
+ this._updateSetting(settings, "KDUMP_DUMPFORMAT", "compressed");
+ } else {
+ this._updateSetting(settings, "KDUMP_DUMPFORMAT", "ELF");
+ }
+ }
+ return settings;
+ }
+
+ /* generate the config file from raw text and settings
+ */
+ generateConfig(settings) {
+ settings = this._persistSettings(settings);
+
+ const lines = this._lines.slice(0);
+
+ // we take the lines from our last read operation and modify them with the new settings
+ Object.keys(settings._internal).forEach((key) => {
+ const entry = settings._internal[key];
+
+ let value = entry.value !== undefined ? entry.value : "";
+ // quote what was quoted before + empty values + multi-word values
+ if (entry.quoted || value === "" || value.includes(" "))
+ value = '"' + value + '"';
+ let line = key + "=" + value;
+ if (entry.suffix)
+ line = line + " " + entry.suffix;
+ // this might be a new entry
+ if (!(key in this._originalSettings)) {
+ lines.push(line);
+ return;
+ }
+ // otherwise edit the old line
+ const origEntry = this._originalSettings[key];
+ lines[origEntry.index] = line;
+ });
+
+ // make sure file ends with a newline
+ if (lines[lines.length - 1] !== "")
+ lines.push("");
+ return lines.join("\n");
+ }
+}
diff --git a/pkg/kdump/config-client.js b/pkg/kdump/config-client.js
new file mode 100644
index 0000000..066c869
--- /dev/null
+++ b/pkg/kdump/config-client.js
@@ -0,0 +1,358 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+
+const deprecatedKeys = ["net", "options", "link_delay", "disk_timeout", "debug_mem_level", "blacklist"];
+const knownKeys = [
+ "raw", "nfs", "ssh", "sshkey", "path", "core_collector", "kdump_post", "kdump_pre", "extra_bins", "extra_modules",
+ "default", "force_rebuild", "override_resettable", "dracut_args", "fence_kdump_args", "fence_kdump_nodes"
+];
+// man kdump.conf suggests this as default configuration
+const defaultCoreCollector = "makedumpfile -l --message-level 7 -d 31";
+
+/* Parse an ini-style config file
+ * and monitor it for changes
+ */
+export class ConfigFile {
+ constructor(filename, superuser = false) {
+ this.filename = filename;
+ this._rawContent = undefined;
+ this._lines = [];
+ this._originalSettings = { };
+ this._dataAvailable = new Promise(resolve => { this._dataAvailableResolve = resolve });
+ this.settings = { };
+
+ cockpit.event_target(this);
+
+ this._fileHandle = cockpit.file(filename, { superuser });
+ this._fileHandle.watch((rawContent) => {
+ this._parseText(rawContent);
+ });
+ }
+
+ close() {
+ if (this._fileHandle) {
+ this._fileHandle.close();
+ this._fileHandle = undefined;
+ }
+ }
+
+ // wait for data to have been read at least once
+ wait() {
+ return this._dataAvailable;
+ }
+
+ /* parse lines of the config file
+ * if a line has a valid config key, use that as key
+ * and also store original line, line index, value and whether the line contains a comment
+ * we care about the comment since we don't want to overwrite a user comment when changing a value
+ * e.g. for line "someKey foo # comment"
+ * outputObject["someKey"] = { index: 0, value: "foo", origLine: "someKey foo # comment", hasComment: true }
+ * skipNotify: Don't notify about changes, e.g.to avoid multiple updates when writing a file
+ */
+ _parseText(rawContent, skipNotify = false) {
+ this._dataAvailableResolve();
+
+ // if nothing changed, don't bother parsing the content
+ // do proceed if the content is empty, it might be our initial read
+ if (!rawContent) {
+ this._originalSettings = null;
+ this.settings = null;
+ if (!skipNotify)
+ this.dispatchEvent("kdumpConfigChanged", this.settings);
+ return;
+ }
+
+ if (rawContent == this._rawContent)
+ return;
+
+ // if (skipNotify === undefined)
+ // skipNotify = false;
+
+ this._rawContent = rawContent;
+ // parse the config file
+ this._lines = rawContent.split(/\r?\n/);
+
+ // this is the format expected by the UI
+ this.settings = {
+ _internal: {},
+ targets: {},
+ compression: { enabled: false, allowed: false, },
+ };
+ this._lines.forEach((line, index) => {
+ const trimmed = line.trim();
+ // if the line is empty or only a comment, skip
+ if (trimmed.indexOf("#") === 0 || trimmed.length === 0)
+ return;
+
+ // we need to have a space between key and value
+ const separatorIndex = trimmed.indexOf(" ");
+ if (separatorIndex === -1)
+ return;
+ const key = trimmed.substring(0, separatorIndex);
+ let value = trimmed.substring(separatorIndex + 1).trim();
+
+ // value might have a comment at the end
+ const commentIndex = value.indexOf("#");
+ let comment;
+ if (commentIndex !== -1) {
+ comment = value.substring(commentIndex).trim();
+ value = value.substring(0, commentIndex).trim();
+ }
+ this.settings._internal[key] = {
+ index,
+ value,
+ origLine: line,
+ comment
+ };
+ });
+
+ // make sure we copy the original keys so we overwrite the correct lines when saving
+ this._originalSettings = { };
+ Object.keys(this.settings._internal).forEach((key) => {
+ this._originalSettings[key] = { ...this.settings._internal[key] };
+ });
+
+ this._extractSettings();
+
+ if (!skipNotify)
+ this.dispatchEvent("kdumpConfigChanged", this.settings);
+ }
+
+ /* extract settings managed by cockpit from _internal into platform independent model
+ */
+ _extractSettings() {
+ // "path" applies to all targets
+ const path = this.settings._internal.path || { value: "" };
+
+ Object.keys(this.settings._internal).forEach((key) => {
+ if (key === "nfs") {
+ // split nfs line into server and export parts
+ const parts = this.settings._internal.nfs.value.match(/^([^[][^:]+|\[[^\]]+\]):(.*)$/);
+ if (!parts)
+ return;
+ this.settings.targets.nfs = {
+ type: key,
+ path: path.value,
+ server: parts[1],
+ export: parts[2],
+ };
+ } else if (key === "ssh") {
+ this.settings.targets.ssh = {
+ type: key,
+ path: path.value,
+ server: this.settings._internal.ssh.value,
+ };
+ if ("sshkey" in this.settings._internal)
+ this.settings.targets.ssh.sshkey = this.settings._internal.sshkey.value;
+ } else if (key === "raw") {
+ this.settings.targets.raw = {
+ type: key,
+ partition: this.settings._internal.raw.value
+ };
+ } else {
+ // probably local, but we might also have a mount
+ // check against known keys, the ones left over may be a mount target
+ // if the key is empty or known, we don't care about it here
+ if (!key || key in knownKeys || key in deprecatedKeys)
+ return;
+ // if we have a UUID, LABEL or /dev in the value, we can be pretty sure it's a mount option
+ const value = JSON.stringify(this.settings._internal[key]).toLowerCase();
+ if (value.indexOf("uuid") > -1 || value.indexOf("label") > -1 || value.indexOf("/dev") > -1) {
+ this.settings.targets.mount = {
+ type: "mount",
+ path: path.value,
+ fsType: key,
+ partition: this.settings._internal[key].value,
+ };
+ } else {
+ // TODO: check for know filesystem types here
+ }
+ }
+ });
+
+ // default to local if no target configured
+ if (Object.keys(this.settings.targets).length === 0)
+ this.settings.targets.local = { type: "local", path: path.value };
+
+ // only allow compression if there is no core collector set or it's set to makedumpfile
+ this.settings.compression.allowed = (
+ !("core_collector" in this.settings._internal) ||
+ (this.settings._internal.core_collector.value.trim().indexOf("makedumpfile") === 0)
+ );
+ // compression is enabled if we have a core_collector command with the "-c" parameter
+ this.settings.compression.enabled = (
+ ("core_collector" in this.settings._internal) &&
+ this.settings._internal.core_collector.value &&
+ (this.settings._internal.core_collector.value.split(" ").indexOf("-c") != -1)
+ );
+
+ this.settings.core_collector = this.settings._internal?.core_collector?.value ?? defaultCoreCollector;
+ }
+
+ /* update single _internal setting to given value
+ * make sure setting exists if value is not empty
+ */
+ _updateSetting(settings, key, value) {
+ if (key in settings._internal) {
+ if (value)
+ settings._internal[key].value = value;
+ else
+ delete settings._internal[key];
+ } else {
+ if (value)
+ settings._internal[key] = { value };
+ }
+ }
+
+ /* transform settings from model back to _internal format
+ * this.settings = current state from file
+ * settings = in-memory state from UI
+ */
+ _persistSettings(settings) {
+ // target
+ if (Object.keys(settings.targets).length > 0) {
+ const target = Object.values(settings.targets)[0];
+ this._updateSetting(settings, "path", target.path);
+
+ // wipe old target settings
+ for (const key in this.settings.targets) {
+ const oldTarget = this.settings.targets[key];
+ if (oldTarget.type == "mount") {
+ delete settings._internal[oldTarget.fsType];
+ } else if (oldTarget.type == "ssh") {
+ delete settings._internal.ssh;
+ delete settings._internal.sshkey;
+ } else {
+ delete settings._internal[key];
+ }
+ }
+
+ if (target.type === "nfs") {
+ this._updateSetting(settings, "nfs", [target.server, target.export].join(":"));
+ } else if (target.type === "ssh") {
+ this._updateSetting(settings, "ssh", target.server);
+ if ("sshkey" in target)
+ this._updateSetting(settings, "sshkey", target.sshkey);
+ } else if (target.type === "raw") {
+ this._updateSetting(settings, "raw", target.partition);
+ } else if (target.type === "mount") {
+ this._updateSetting(settings, target.fsType, target.partition);
+ }
+
+ /* ssh target needs a flattened vmcore for transport */
+ if ("core_collector" in settings._internal &&
+ settings._internal.core_collector.value.includes("makedumpfile")) {
+ if (target.type === "ssh" && !settings._internal.core_collector.value.includes("-F"))
+ settings._internal.core_collector.value += " -F";
+ else if (settings._internal.core_collector.value.includes("-F"))
+ settings._internal.core_collector.value =
+ settings._internal.core_collector.value
+ .split(" ")
+ .filter(e => e != "-F")
+ .join(" ");
+ } else {
+ settings._internal.core_collector = { value: defaultCoreCollector };
+ if (target.type === "ssh") {
+ settings._internal.core_collector.value += " -F";
+ }
+ }
+ }
+ // compression
+ if (this.settings.compression.enabled != settings.compression.enabled) {
+ if (settings.compression.enabled) {
+ // enable compression
+ if ("core_collector" in settings._internal)
+ settings._internal.core_collector.value = settings._internal.core_collector.value + " -c";
+ else
+ settings._internal.core_collector = { value: defaultCoreCollector };
+ } else {
+ // disable compression
+ if ("core_collector" in this.settings._internal) {
+ // just remove all "-c" parameters
+ settings._internal.core_collector.value =
+ settings._internal.core_collector.value
+ .split(" ")
+ .filter((e) => { return (e != "-c") })
+ .join(" ");
+ } else {
+ // if we don't have anything on this in the original settings,
+ // we can get rid of the entry altogether
+ delete settings._internal.core_collector;
+ }
+ }
+ }
+ return settings;
+ }
+
+ /* generate the config file from raw text and settings
+ */
+ generateConfig(settings) {
+ settings = this._persistSettings(settings);
+
+ const lines = this._lines.slice(0);
+ const linesToDelete = [];
+ // first find the settings lines that have been disabled/deleted
+ Object.keys(this._originalSettings).forEach((key) => {
+ if (!(key in settings._internal) || !(key in settings._internal && settings._internal[key].value)) {
+ const origEntry = this._originalSettings[key];
+ // if the line had a comment, keep it, otherwise delete
+ if (origEntry.comment !== undefined)
+ lines[origEntry.index] = "#" + origEntry.origLine;
+ else
+ linesToDelete.push(origEntry.index);
+ }
+ });
+
+ // we take the lines from our last read operation and modify them with the new settings
+ Object.keys(settings._internal).forEach((key) => {
+ const entry = settings._internal[key];
+ let line = key + " " + entry.value;
+ if (entry.comment)
+ line = line + " " + entry.comment;
+ // this might be a new entry
+ if (!(key in this._originalSettings)) {
+ lines.push(line);
+ return;
+ }
+ // otherwise edit the old line
+ const origEntry = this._originalSettings[key];
+ lines[origEntry.index] = line;
+ });
+ // now delete the rows we want to delete
+ linesToDelete.sort().reverse()
+ .forEach((lineIndex) => {
+ lines.splice(lineIndex, 1);
+ });
+
+ return lines.join("\n") + "\n";
+ }
+
+ /* write settings back to file
+ * new settings that don't have a corresponding entry already have an undefined or null index
+ * returns a promise for the file operation (cockpit File)
+ */
+ write(settings) {
+ return this._fileHandle.modify((oldContent) => {
+ this._parseText(oldContent, true);
+ return this.generateConfig(settings);
+ });
+ }
+}
diff --git a/pkg/kdump/crashkernel.sh b/pkg/kdump/crashkernel.sh
new file mode 100644
index 0000000..afaf7e8
--- /dev/null
+++ b/pkg/kdump/crashkernel.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+
+# sync disks
+sync
+
+# crash the kernel
+echo 1 > /proc/sys/kernel/sysrq
+echo c > /proc/sysrq-trigger
diff --git a/pkg/kdump/index.html b/pkg/kdump/index.html
new file mode 100644
index 0000000..e1d43dd
--- /dev/null
+++ b/pkg/kdump/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2016 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="kdump-page" lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title translate="yes">Kernel dump</title>
+ <meta name="description" content="" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+ <link rel="stylesheet" href="kdump.css" />
+
+ <script type="text/javascript" src="kdump.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app"></div>
+</body>
+</html>
diff --git a/pkg/kdump/kdump-client.js b/pkg/kdump/kdump-client.js
new file mode 100644
index 0000000..56f640d
--- /dev/null
+++ b/pkg/kdump/kdump-client.js
@@ -0,0 +1,182 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import { proxy as serviceProxy } from 'service';
+import { ConfigFile } from './config-client.js';
+import { ConfigFileSUSE } from './config-client-suse.js';
+
+import crashKernelScript from './crashkernel.sh';
+import testWritableScript from './testwritable.sh';
+const _ = cockpit.gettext;
+
+/* initializes the kdump status
+ * emits "kdumpStatusChanged" when the status changes, along with a status object:
+ * {
+ * installed: true/false,
+ * enabled: true/false,
+ * state: current state,
+ * config: settings from kdump.conf
+ * target: dump target info, content depends on dump type
+ * always contains the keys:
+ * type value in ["local", "nfs", "ssh", "raw", "mount", "unknown"]
+ * multipleTargets true if the config file has more than one target defined, false otherwise
+ * }
+ *
+ */
+export class KdumpClient {
+ constructor() {
+ this.state = {
+ installed: undefined,
+ enabled: undefined,
+ state: undefined,
+ config: undefined,
+ target: undefined,
+ };
+ cockpit.event_target(this);
+
+ // listen to service status changes
+ this.kdumpService = serviceProxy("kdump");
+ this.kdumpService.addEventListener('changed', () => {
+ this.state.installed = this.kdumpService.exists;
+ this.state.enabled = this.kdumpService.enabled;
+ this.state.state = this.kdumpService.state;
+ this.dispatchEvent("kdumpStatusChanged", this.state);
+ });
+
+ // watch the config file
+ this.configClient = new ConfigFile("/etc/kdump.conf", true);
+ this._watchConfigChanges();
+
+ this.configClient.wait().then(() => {
+ // if no configuration found, try SUSE version
+ if (this.configClient.settings === null) {
+ this.configClient.close();
+ this.configClient = new ConfigFileSUSE("/etc/sysconfig/kdump", true);
+ this._watchConfigChanges();
+ }
+ });
+ }
+
+ _watchConfigChanges() {
+ // catch config changes
+ this.configClient.addEventListener('kdumpConfigChanged', () => {
+ this.state.config = this.configClient.settings;
+ this.state.target = this.targetFromSettings(this.configClient.settings);
+ this.dispatchEvent("kdumpStatusChanged", this.state);
+ });
+ }
+
+ ensureOn() {
+ // we consider the state to be "on" when it's enabled and running
+ return Promise.all([
+ this.kdumpService.enable(),
+ this.kdumpService.start()
+ ]);
+ }
+
+ ensureOff() {
+ // we consider the state to be "off" when it's disabled and stopped
+ return Promise.all([
+ this.kdumpService.stop(),
+ this.kdumpService.disable()
+ ]);
+ }
+
+ crashKernel() {
+ // crash the system kernel
+ return cockpit.script(crashKernelScript, [], { superuser: "require" });
+ }
+
+ validateSettings(settings) {
+ const target = this.targetFromSettings(settings);
+ let path;
+ if (target && target.path)
+ path = target.path;
+ // if path is invalid or we haven't set one, use default
+ if (!path)
+ path = "/var/crash";
+
+ return new Promise((resolve, reject) => {
+ if (target.type === "local") {
+ // local path, try to see if we can write
+ cockpit.script(testWritableScript, [path], { superuser: "try" })
+ .then(resolve)
+ .catch(() => reject(cockpit.format(_("Directory $0 isn't writable or doesn't exist."), path)));
+ return;
+ } else if (target.type === "nfs") {
+ if (!target.server || !target.server.trim())
+ reject(_("nfs server is empty"));
+ // IPv6 must be enclosed in square brackets
+ if (target.server.trim().match(/^\[.*[^\]]$/))
+ reject(_("nfs server is not valid IPv6"));
+ if (!target.export || !target.export.trim())
+ reject(_("nfs export is empty"));
+ } else if (target.type === "ssh") {
+ if (!target.server || !target.server.trim())
+ reject(_("ssh server is empty"));
+ if (target.sshkey && !target.sshkey.match("/.+"))
+ reject(_("ssh key isn't a path"));
+ }
+
+ /* no-op if already rejected */
+ resolve();
+ });
+ }
+
+ writeSettings(settings) {
+ return this.configClient.write(settings)
+ .then(() => {
+ // after we've written the new config, we have to restart the service to pick up changes or clean up after errors
+ if (this.kdumpService.enabled) {
+ return this.kdumpService.restart()
+ .catch(error => this.kdumpService.getRunJournal(["--output=cat", "--identifier=kdumpctl"])
+ .then(journal => {
+ error.details = journal;
+ return Promise.reject(error);
+ }, ex => {
+ console.warn("Failed to get journal of kdump.service:", ex.toString());
+ return Promise.reject(error);
+ })
+ );
+ } else {
+ return true;
+ }
+ });
+ }
+
+ exportConfig(settings) {
+ return this.configClient.generateConfig(settings).trim();
+ }
+
+ targetFromSettings(settings) {
+ const target = {
+ type: "unknown",
+ multipleTargets: false,
+ };
+
+ if (!settings || Object.keys(settings.targets).length === 0)
+ return target;
+
+ // copy first target
+ Object.assign(target, Object.values(settings.targets)[0]);
+ target.multipleTargets = Object.keys(settings.targets).length > 1;
+ return target;
+ }
+}
diff --git a/pkg/kdump/kdump-view.jsx b/pkg/kdump/kdump-view.jsx
new file mode 100644
index 0000000..bd4172d
--- /dev/null
+++ b/pkg/kdump/kdump-view.jsx
@@ -0,0 +1,639 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import cockpit from "cockpit";
+
+import React, { useEffect, useState } from "react";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Form, FormGroup, FormSection } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { CodeBlockCode } from "@patternfly/react-core/dist/esm/components/CodeBlock/index.js";
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { Title } from "@patternfly/react-core/dist/esm/components/Title/index.js";
+import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+
+import { useDialogs, DialogsContext } from "dialogs.jsx";
+import { read_os_release } from "os-release.js";
+import { fmt_to_fragments } from 'utils.jsx';
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+import { FormHelper } from "cockpit-components-form-helper";
+import { ModalError } from 'cockpit-components-inline-notification.jsx';
+import { PrivilegedButton } from "cockpit-components-privileged.jsx";
+import { ModificationsExportDialog } from "cockpit-components-modifications.jsx";
+
+const _ = cockpit.gettext;
+const DEFAULT_KDUMP_PATH = "/var/crash";
+
+const exportAnsibleTask = (settings, os_release) => {
+ const target = Object.keys(settings.targets)[0];
+ const targetSettings = settings.targets[target];
+ const kdump_core_collector = settings.core_collector;
+
+ let role_name = "linux-system-roles";
+ if (os_release.NAME === "RHEL" || os_release.ID_LIKE?.includes('rhel')) {
+ role_name = "rhel-system-roles";
+ }
+
+ let ansible = `
+---
+# Also available via https://galaxy.ansible.com/ui/standalone/roles/linux-system-roles/kdump/
+- name: install ${role_name}
+ package:
+ name: ${role_name}
+ state: present
+ delegate_to: 127.0.0.1
+ become: true
+- name: run kdump system role
+ include_role:
+ name: ${role_name}.kdump
+ vars:
+ kdump_path: ${targetSettings.path || DEFAULT_KDUMP_PATH}
+ kdump_core_collector: ${kdump_core_collector}`;
+
+ if (target === "ssh") {
+ // HACK: we should not have to specify kdump_ssh_user and kdump_ssh_user as it is in kdump_target.location
+ // https://github.com/linux-system-roles/kdump/issues/184
+ let ssh_user;
+ let ssh_server;
+ const parts = targetSettings.server.split('@');
+ if (parts.length === 1) {
+ ssh_user = "root";
+ ssh_server = parts[0];
+ } else if (parts.length === 2) {
+ ssh_user = parts[0];
+ ssh_server = parts[1];
+ } else {
+ throw new Error("ssh server contains two @ symbols");
+ }
+ ansible += `
+ kdump_target:
+ type: ssh
+ kdump_sshkey: ${targetSettings.sshkey}
+ kdump_ssh_server: ${ssh_server}
+ kdump_ssh_user: ${ssh_user}`;
+ } else if (target === "nfs") {
+ ansible += `
+ kdump_target:
+ type: nfs
+ location: ${targetSettings.server}:${targetSettings.export}
+`;
+ } else if (target !== "local") {
+ // target is unsupported
+ throw new Error("Unsupported kdump target"); // not-covered: assertion
+ }
+
+ return ansible;
+};
+
+function getLocation(target) {
+ let path = target.path || DEFAULT_KDUMP_PATH;
+
+ if (target.type === "ssh") {
+ path = `${target.server}:${path}`;
+ } else if (target.type == "nfs") {
+ path = path[0] !== '/' ? '/' + path : path;
+ path = `${target.server}:${target.export + path}`;
+ }
+
+ return path;
+}
+
+const KdumpSettingsModal = ({ settings, initialTarget, handleSave }) => {
+ const Dialogs = useDialogs();
+ const compressionAllowed = settings.compression?.allowed;
+ const [isSaving, setIsSaving] = useState(false);
+ const [error, setError] = useState(null);
+ const [isFormValid, setFormValid] = useState(true);
+ const [validationErrors, setValidationErrors] = useState({});
+
+ const [storageLocation, setStorageLocation] = useState(Object.keys(settings.targets)[0]);
+ // common options
+ const [compressionEnabled, setCompressionEnabled] = useState(settings.compression?.enabled);
+ const [directory, setDirectory] = useState(initialTarget.path || DEFAULT_KDUMP_PATH);
+ // nfs and ssh
+ const [server, setServer] = useState(settings.targets.nfs?.server || settings.targets.ssh?.server);
+ // nfs
+ const [exportPath, setExportPath] = useState(settings.targets.nfs?.export || "");
+ // ssh
+ const [sshkey, setSSHKey] = useState(settings.targets.ssh?.sshkey || "");
+
+ useEffect(() => {
+ // We can't use a ref in a functional component
+ const elem = document.querySelector("#kdump-settings-form");
+ if (elem)
+ setFormValid(elem.checkValidity());
+ }, [storageLocation, directory, sshkey, server, exportPath]);
+
+ const changeStorageLocation = target => {
+ setError(null);
+ setDirectory(DEFAULT_KDUMP_PATH);
+ setServer("");
+ setStorageLocation(target);
+ };
+
+ const changeSSHKey = value => {
+ if (value.trim() && !value.match("/.+")) {
+ setValidationErrors({ sshkey: _("SSH key isn't a path") });
+ } else {
+ setValidationErrors({});
+ }
+ setSSHKey(value);
+ };
+
+ const saveSettings = () => {
+ setError(null);
+ setIsSaving(true);
+ const newSettings = {
+ compression: {
+ allowed: compressionAllowed,
+ enabled: compressionEnabled,
+ },
+ targets: {
+ [storageLocation]: {
+ type: storageLocation,
+ // HACK: to not needlessly write a path /var/crash as this is the default,
+ // set an empty string.
+ path: directory === DEFAULT_KDUMP_PATH ? "" : directory,
+ }
+ },
+ _internal: {
+ ...settings._internal
+ }
+ };
+
+ if (storageLocation === "ssh") {
+ newSettings.targets.ssh.server = server;
+ newSettings.targets.ssh.sshkey = sshkey;
+ }
+
+ if (storageLocation === "nfs") {
+ newSettings.targets.nfs.server = server;
+ newSettings.targets.nfs.export = exportPath;
+ }
+
+ handleSave(newSettings)
+ .then(Dialogs.close)
+ .finally(() => setIsSaving(false))
+ .catch(error => {
+ if (error.details) {
+ // avoid bad summary like "systemd job RestartUnit ["kdump.service","replace"] failed with result failed"
+ // if we have a more concrete journal and trim journal's `kdump: ` prefix.
+ error.message = _("Unable to save settings");
+ error.details = <CodeBlockCode>{ error.details.replaceAll(/\nkdump: /g, "\n") }</CodeBlockCode>;
+ setError(error);
+ } else {
+ // without a journal, show the error as-is
+ setError(new Error(cockpit.format(_("Unable to save settings: $0"), String(error))));
+ }
+ });
+ };
+
+ return (
+ <Modal position="top" variant="small" id="kdump-settings-dialog" isOpen
+ title={_("Crash dump location")}
+ onClose={Dialogs.close}
+ footer={
+ <>
+ <Button variant="primary"
+ isLoading={isSaving}
+ isDisabled={isSaving || !isFormValid || Object.keys(validationErrors).length !== 0}
+ onClick={saveSettings}>
+ {_("Save changes")}
+ </Button>
+ <Button variant="link"
+ isDisabled={isSaving}
+ className="cancel"
+ onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>
+ }>
+ {error && <ModalError isExpandable
+ dialogError={error.message || error}
+ dialogErrorDetail={error.details} />}
+ <Form id="kdump-settings-form" isHorizontal>
+ <FormGroup fieldId="kdump-settings-location" label={_("Location")}>
+ <FormSelect key="location" onChange={(_, val) => changeStorageLocation(val)}
+ id="kdump-settings-location" value={storageLocation}>
+ <FormSelectOption value='local'
+ label={_("Local filesystem")} />
+ <FormSelectOption value='ssh'
+ label={_("Remote over SSH")} />
+ <FormSelectOption value='nfs'
+ label={_("Remote over NFS")} />
+ </FormSelect>
+ </FormGroup>
+
+ {storageLocation === "local" &&
+ <FormGroup fieldId="kdump-settings-local-directory" label={_("Directory")} isRequired>
+ <TextInput id="kdump-settings-local-directory" key="directory"
+ placeholder={DEFAULT_KDUMP_PATH} value={directory}
+ data-stored={directory}
+ onChange={(_event, value) => setDirectory(value)}
+ isRequired />
+ </FormGroup>
+ }
+
+ {storageLocation === "nfs" &&
+ <>
+ <FormGroup fieldId="kdump-settings-nfs-server" label={_("Server")} isRequired>
+ <TextInput id="kdump-settings-nfs-server" key="server"
+ placeholder="penguin.example.com" value={server}
+ onChange={(_event, value) => setServer(value)} isRequired />
+ </FormGroup>
+ <FormGroup fieldId="kdump-settings-nfs-export" label={_("Export")} isRequired>
+ <TextInput id="kdump-settings-nfs-export" key="export"
+ placeholder="/export/cores" value={exportPath}
+ onChange={(_event, value) => setExportPath(value)} isRequired />
+ </FormGroup>
+ <FormGroup fieldId="kdump-settings-nfs-directory" label={_("Directory")} isRequired>
+ <TextInput id="kdump-settings-nfs-directory" key="directory"
+ placeholder={DEFAULT_KDUMP_PATH} value={directory}
+ data-stored={directory}
+ onChange={(_event, value) => setDirectory(value)}
+ isRequired />
+ </FormGroup>
+ </>
+ }
+
+ {storageLocation === "ssh" &&
+ <>
+ <FormGroup fieldId="kdump-settings-ssh-server" label={_("Server")} isRequired>
+ <TextInput id="kdump-settings-ssh-server" key="server"
+ placeholder="user@server.com" value={server}
+ onChange={(_event, value) => setServer(value)} isRequired />
+ </FormGroup>
+
+ <FormGroup fieldId="kdump-settings-ssh-key" label={_("SSH key")}>
+ <TextInput id="kdump-settings-ssh-key" key="ssh"
+ placeholder="/root/.ssh/kdump_id_rsa" value={sshkey}
+ onChange={(_event, value) => changeSSHKey(value)}
+ validated={validationErrors.sshkey ? "error" : "default"} />
+ <FormHelper helperTextInvalid={validationErrors.sshkey} />
+ </FormGroup>
+
+ <FormGroup fieldId="kdump-settings-ssh-directory" label={_("Directory")} isRequired>
+ <TextInput id="kdump-settings-ssh-directory" key="directory"
+ placeholder={DEFAULT_KDUMP_PATH} value={directory}
+ data-stored={directory}
+ onChange={(_event, value) => setDirectory(value)}
+ isRequired />
+ </FormGroup>
+ </>
+ }
+
+ <FormSection>
+ <FormGroup fieldId="kdump-settings-compression" label={_("Compression")} hasNoPaddingTop>
+ <Checkbox id="kdump-settings-compression"
+ isChecked={compressionEnabled}
+ onChange={(_, c) => setCompressionEnabled(c)}
+ isDisabled={!compressionAllowed}
+ label={_("Compress crash dumps to save space")} />
+ </FormGroup>
+ </FormSection>
+ </Form>
+ </Modal>);
+};
+
+/* Show kdump status of the system and offer options to change or test the state
+ * Expected properties:
+ * kdumpActive kdump service status
+ * onSetServiceState called when the OnOff state is toggled (for kdumpActive), parameter: desired state
+ * stateChanging whether we're currently waiting for our last change to take effect
+ * onSaveSettings called with current dialog settings when the user clicks Save
+ * kdumpStatus object as described in kdump-client
+ * reservedMemory memory reserved at boot time for kdump use
+ * onCrashKernel callback to crash the kernel via kdumpClient, expects a promise
+ */
+export class KdumpPage extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor(props) {
+ super(props);
+ this.state = { os_release: null };
+
+ this.handleTestSettingsClick = this.handleTestSettingsClick.bind(this);
+ this.handleSettingsClick = this.handleSettingsClick.bind(this);
+ this.handleAutomationClick = this.handleAutomationClick.bind(this);
+ read_os_release().then(os_release => this.setState({ os_release }));
+ }
+
+ handleTestSettingsClick() {
+ // if we have multiple targets defined, the config is invalid
+ const target = this.props.kdumpStatus.target;
+ let verifyMessage;
+ if (!target.multipleTargets) {
+ const path = getLocation(target);
+ if (target.type === "local") {
+ verifyMessage = fmt_to_fragments(
+ ' ' + _("Results of the crash will be stored in $0 as $1, if kdump is properly configured."),
+ <span className="pf-v5-u-font-family-monospace-vf">{path}</span>,
+ <span className="pf-v5-u-font-family-monospace-vf">vmcore</span>);
+ } else if (target.type === "ssh" || target.type == "nfs") {
+ verifyMessage = fmt_to_fragments(
+ ' ' + _("Results of the crash will be copied through $0 to $1 as $2, if kdump is properly configured."),
+ <span className="pf-v5-u-font-family-monospace-vf">{target.type === "ssh" ? "SSH" : "NFS"}</span>,
+ <span className="pf-v5-u-font-family-monospace-vf">{path}</span>,
+ <span className="pf-v5-u-font-family-monospace-vf">vmcore</span>);
+ }
+ }
+
+ // open a dialog to confirm crashing the kernel to test the settings - then do it
+ const dialogProps = {
+ title: _("Test kdump settings"),
+ body: (<TextContent>
+ <Text component={TextVariants.p}>
+ {_("Test kdump settings by crashing the kernel. This may take a while and the system might not automatically reboot. Do not purposefully crash the system while any important task is running.")}
+ </Text>
+ {verifyMessage && <Text component={TextVariants.p}>
+ {verifyMessage}
+ </Text>}
+ </TextContent>),
+ showClose: true,
+ titleIconVariant: "warning",
+ };
+ // also test modifying properties in subsequent render calls
+ const footerProps = {
+ actions: [
+ {
+ clicked: this.props.onCrashKernel.bind(this),
+ caption: _("Crash system"),
+ style: 'danger',
+ }
+ ],
+ };
+ show_modal_dialog(dialogProps, footerProps);
+ }
+
+ handleServiceDetailsClick() {
+ cockpit.jump("/system/services#/kdump.service", cockpit.transport.host);
+ }
+
+ handleSettingsClick() {
+ const Dialogs = this.context;
+ Dialogs.show(<KdumpSettingsModal settings={this.props.kdumpStatus.config}
+ initialTarget={this.props.kdumpStatus.target}
+ handleSave={this.props.onSaveSettings} />);
+ }
+
+ handleAutomationClick() {
+ const Dialogs = this.context;
+ let enableCrashKernel = '';
+ let kdumpconf = this.props.exportConfig(this.props.kdumpStatus.config);
+ kdumpconf = kdumpconf.replaceAll('$', '\\$');
+ if (this.state.os_release.NAME?.includes('Fedora')) {
+ enableCrashKernel = `
+# A reboot will be required if crashkernel was not set before
+kdumpctl reset-crashkernel`;
+ }
+ const shell = `
+cat > /etc/kdump.conf << EOF
+${kdumpconf}
+EOF
+systemctl enable --now kdump.service
+${enableCrashKernel}
+`;
+
+ Dialogs.show(
+ <ModificationsExportDialog
+ ansible={exportAnsibleTask(this.props.kdumpStatus.config, this.state.os_release)}
+ shell={shell}
+ show
+ onClose={Dialogs.close}
+ />);
+ }
+
+ render() {
+ let kdumpLocation = (
+ <div className="dialog-wait-ct">
+ <Spinner size="md" />
+ <span>{ _("Loading...") }</span>
+ </div>
+ );
+ let targetCanChange = false;
+ if (this.props.kdumpStatus && this.props.kdumpStatus.target) {
+ // if we have multiple targets defined, the config is invalid
+ const target = this.props.kdumpStatus.target;
+ if (target.multipleTargets) {
+ kdumpLocation = _("invalid: multiple targets defined");
+ } else {
+ const locationPath = getLocation(target);
+ if (target.type == "local") {
+ kdumpLocation = cockpit.format(_("Local, $0"), locationPath);
+ targetCanChange = true;
+ } else if (target.type == "ssh") {
+ kdumpLocation = cockpit.format(_("Remote over SSH, $0"), locationPath);
+ targetCanChange = true;
+ } else if (target.type == "nfs") {
+ kdumpLocation = cockpit.format(_("Remote over NFS, $0"), locationPath);
+ targetCanChange = true;
+ } else if (target.type == "raw") {
+ kdumpLocation = _("Raw to a device");
+ } else if (target.type == "mount") {
+ /* mount targets outside of nfs are too complex for the
+ * current target dialog */
+ kdumpLocation = _("On a mounted device");
+ } else if (target.type == "ftp") {
+ kdumpLocation = _("Remote over FTP");
+ } else if (target.type == "sftp") {
+ kdumpLocation = _("Remote over SFTP");
+ } else if (target.type == "cifs") {
+ kdumpLocation = _("Remote over CIFS/SMB");
+ } else {
+ kdumpLocation = _("No configuration found");
+ }
+ }
+ }
+ // this.storeLocation(this.props.kdumpStatus.config);
+ const settingsLink = targetCanChange && <Button variant="link" isInline id="kdump-change-target" onClick={this.handleSettingsClick}>{_("Edit")}</Button>;
+ let reservedMemory;
+ if (this.props.reservedMemory === undefined) {
+ // still waiting for result
+ reservedMemory = (
+ <div className="dialog-wait-ct">
+ <Spinner size="md" />
+ <span>{ _("Reading...") }</span>
+ </div>
+ );
+ } else if (this.props.reservedMemory == 0) {
+ // nothing reserved
+ reservedMemory = <span>{_("None")} </span>;
+ } else if (this.props.reservedMemory == "error") {
+ // error while reading
+ } else {
+ // assume we have a proper value
+ // TODO: hint at using debug_mem_level to identify actual memory required?
+ reservedMemory = <span>{this.props.reservedMemory}</span>;
+ }
+
+ const serviceRunning = this.props.kdumpStatus &&
+ this.props.kdumpStatus.installed &&
+ this.props.kdumpStatus.state == "running";
+
+ let testButton;
+ if (serviceRunning) {
+ testButton = (
+ <PrivilegedButton variant="secondary" isDanger
+ excuse={ _("The user $0 is not permitted to test crash the kernel") }
+ onClick={this.handleTestSettingsClick}>
+ { _("Test configuration") }
+ </PrivilegedButton>
+ );
+ } else {
+ const tooltip = _("Test is only available while the kdump service is running.");
+ testButton = (
+ <Tooltip id="tip-test" content={tooltip}>
+ <Button variant="secondary" isDanger isAriaDisabled>
+ {_("Test configuration")}
+ </Button>
+ </Tooltip>
+ );
+ }
+
+ let automationButton = null;
+ if (this.props.kdumpStatus && this.props.kdumpStatus.config !== null && this.state.os_release !== null && targetCanChange) {
+ automationButton = (
+ <FlexItem align={{ md: 'alignRight' }}>
+ <Button id="kdump-automation-script" variant="secondary" onClick={this.handleAutomationClick}>
+ {_("View automation script")}
+ </Button>
+ </FlexItem>
+ );
+ }
+
+ let kdumpSwitch;
+ let kdumpSwitchHelper;
+ if (!this.props.kdumpCmdlineEnabled) {
+ kdumpSwitchHelper = _("Currently not supported");
+ } else {
+ kdumpSwitch = (<Switch isChecked={!!serviceRunning}
+ onChange={this.props.onSetServiceState}
+ aria-label={_("kdump status")}
+ isDisabled={this.props.stateChanging} />);
+ kdumpSwitchHelper = serviceRunning ? _("Enabled") : _("Disabled");
+ }
+
+ let alertMessage;
+ let alertDetail;
+ if (!this.props.stateChanging && this.props.kdumpStatus && this.props.kdumpStatus.installed !== undefined) {
+ if (this.props.kdumpStatus.installed) {
+ if (this.props.reservedMemory == 0) {
+ alertMessage = fmt_to_fragments(
+ _("Kernel did not boot with the $0 setting"),
+ <span className="pf-v5-u-font-family-monospace-vf">crashkernel</span>
+ );
+ alertDetail = fmt_to_fragments(
+ _("Reserve memory at boot time by setting a '$0' option on the kernel command line. For example, append '$1' to $2 in $3 or use your distribution's kernel argument editor."),
+ <span className="pf-v5-u-font-family-monospace-vf">crashkernel</span>,
+ <span className="pf-v5-u-font-family-monospace-vf">crashkernel=512M</span>,
+ <span className="pf-v5-u-font-family-monospace-vf">GRUB_CMDLINE_LINUX</span>,
+ <span className="pf-v5-u-font-family-monospace-vf">/etc/default/grub</span>
+ );
+ } else if (this.props.kdumpStatus.state == "failed") {
+ alertMessage = (
+ <>
+ {_("Service has an error")}
+ <Button variant="link" isInline className="pf-v5-u-ml-sm" onClick={this.handleServiceDetailsClick}>{_("more details")}</Button>
+ </>
+ );
+ }
+ } else {
+ alertMessage = _("Kdump service is not installed.");
+ alertDetail = fmt_to_fragments(
+ _("Install the $0 package."),
+ <span className="pf-v5-u-font-family-monospace-vf">kexec-tools</span>
+ );
+ }
+ }
+ return (
+ <Page>
+ <PageSection variant={PageSectionVariants.light}>
+ <Flex spaceItems={{ default: 'spaceItemsMd' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <Title headingLevel="h2" size="3xl">
+ {_("Kernel crash dump")}
+ </Title>
+ {kdumpSwitch}
+ <HelperText>
+ <HelperTextItem variant="indeterminate">{kdumpSwitchHelper}</HelperTextItem>
+ </HelperText>
+ {automationButton}
+ </Flex>
+ </PageSection>
+ <PageSection>
+
+ {alertMessage &&
+ <Alert variant='danger'
+ className="pf-v5-u-mb-md"
+ isLiveRegion={this.props.isLiveRegion}
+ isInline
+ title={alertMessage}>
+ {alertDetail}
+ </Alert>
+ }
+ <Card>
+ <CardTitle>
+ <Title headingLevel="h4" size="xl">
+ {_("Kdump settings")}
+ </Title>
+ </CardTitle>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Reserved memory")}</DescriptionListTerm>
+ <DescriptionListDescription>
+ {reservedMemory}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Crash dump location")}</DescriptionListTerm>
+ <DescriptionListDescription>
+ <Flex spaceItems={{ default: 'spaceItemsSm' }}>
+ <span id="kdump-target-info">{ kdumpLocation }</span>
+ {settingsLink}
+ </Flex>
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+
+ <DescriptionListGroup>
+ <DescriptionListTerm />
+ <DescriptionListDescription>
+ {testButton}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ </CardBody>
+ </Card>
+ </PageSection>
+ </Page>
+ );
+ }
+}
diff --git a/pkg/kdump/kdump.js b/pkg/kdump/kdump.js
new file mode 100644
index 0000000..f2eab37
--- /dev/null
+++ b/pkg/kdump/kdump.js
@@ -0,0 +1,129 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import 'polyfills'; // once per application
+import 'cockpit-dark-theme'; // once per page
+
+import cockpit from "cockpit";
+
+import React from "react";
+import { createRoot } from "react-dom/client";
+
+import { KdumpPage } from "./kdump-view.jsx";
+import * as kdumpClient from "./kdump-client.js";
+import { superuser } from "superuser";
+import { WithDialogs } from "dialogs.jsx";
+
+import './kdump.scss';
+
+superuser.reload_page_on_change();
+
+const initStore = function(rootElement) {
+ const root = createRoot(rootElement);
+ const dataStore = { };
+ dataStore.domRootElement = rootElement;
+
+ dataStore.kdumpClient = new kdumpClient.KdumpClient();
+
+ dataStore.saveSettings = settings =>
+ dataStore.kdumpClient.validateSettings(settings)
+ .then(() => dataStore.kdumpClient.writeSettings(settings));
+
+ dataStore.exportConfig = settings =>
+ dataStore.kdumpClient.exportConfig(settings);
+
+ // whether we're actively trying to change the state
+ dataStore.stateChanging = false;
+ function setServiceState(_event, desiredState) {
+ if (dataStore.stateChanging) {
+ console.log("already trying to change state");
+ return;
+ }
+ const promise = desiredState ? dataStore.kdumpClient.ensureOn() : dataStore.kdumpClient.ensureOff();
+ dataStore.stateChanging = true;
+ dataStore.render();
+ promise
+ .catch(error => console.warn("Failed to change kdump state:", error))
+ .finally(() => {
+ dataStore.stateChanging = false;
+ dataStore.render();
+ });
+ }
+ const render = function() {
+ root.render(<WithDialogs>{React.createElement(KdumpPage, {
+ kdumpActive: false,
+ onSetServiceState: setServiceState,
+ stateChanging: dataStore.stateChanging,
+ reservedMemory: dataStore.kdumpMemory,
+ kdumpStatus: dataStore.kdumpStatus,
+ kdumpCmdlineEnabled: dataStore.crashkernel || false,
+ onSaveSettings: dataStore.saveSettings,
+ onCrashKernel: dataStore.kdumpClient.crashKernel,
+ exportConfig: dataStore.exportConfig,
+ })}</WithDialogs>);
+ };
+ dataStore.render = render;
+
+ const crashkernelPromise = cockpit.file("/proc/cmdline").read()
+ .then(content => {
+ if (content !== null) {
+ dataStore.crashkernel = content.indexOf('crashkernel=') !== -1;
+ }
+ })
+ .catch(err => console.error("cannot read /proc/cmdline", err));
+
+ // read memory reserved for kdump from system
+ dataStore.kdumpMemory = undefined;
+ const crashsizePromise = cockpit.file("/sys/kernel/kexec_crash_size").read()
+ .then(content => {
+ const value = parseInt(content, 10);
+ if (!isNaN(value)) {
+ // if it's only a number, guess from the size what units we should use
+ // https://access.redhat.com/solutions/59432 states limit to be 896MiB and the auto at 768MiB max
+ // default unit is MiB
+ if (value >= 1024 * 1024)
+ dataStore.kdumpMemory = cockpit.format_bytes(value, 1024);
+ else if (value >= 1024)
+ dataStore.kdumpMemory = cockpit.format_bytes(value * 1024, 1024);
+ else
+ dataStore.kdumpMemory = cockpit.format_bytes(value * 1024 * 1024, 1024);
+ } else {
+ dataStore.kdumpMemory = content.trim();
+ }
+ })
+ .catch(() => { dataStore.kdumpMemory = "error" });
+
+ Promise.allSettled([crashkernelPromise, crashsizePromise]).then(render);
+
+ // catch kdump config and service changes
+ dataStore.kdumpClient.addEventListener('kdumpStatusChanged', function(event, status) {
+ dataStore.kdumpStatus = status;
+ render();
+ });
+
+ // render once
+ render();
+
+ return dataStore;
+};
+
+document.addEventListener("DOMContentLoaded", function() {
+ initStore(document.getElementById('app'));
+});
diff --git a/pkg/kdump/kdump.scss b/pkg/kdump/kdump.scss
new file mode 100644
index 0000000..260c027
--- /dev/null
+++ b/pkg/kdump/kdump.scss
@@ -0,0 +1,34 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@use "page";
+
+@import "global-variables";
+@import "@patternfly/patternfly/utilities/Text/text.scss";
+@import "@patternfly/patternfly/utilities/Spacing/spacing.scss";
+
+#kdump-settings-location {
+ inline-size: 100%;
+}
+
+// error details
+#kdump-settings-dialog .pf-v5-c-code-block__pre {
+ max-block-size: 200px;
+ overflow-y: auto;
+}
diff --git a/pkg/kdump/manifest.json b/pkg/kdump/manifest.json
new file mode 100644
index 0000000..cdc3b62
--- /dev/null
+++ b/pkg/kdump/manifest.json
@@ -0,0 +1,22 @@
+{
+ "conditions": [
+ {"path-exists": "/usr/sbin/kexec"}
+ ],
+ "tools": {
+ "index": {
+ "label": "Kernel dump",
+ "docs": [
+ {
+ "label": "Configuring kdump",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-kdump-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["kdump", "crash"]
+ }
+ ]
+ }
+ },
+ "content-security-policy": "img-src 'self' data:"
+}
diff --git a/pkg/kdump/test-config-client.js b/pkg/kdump/test-config-client.js
new file mode 100644
index 0000000..7ff9a18
--- /dev/null
+++ b/pkg/kdump/test-config-client.js
@@ -0,0 +1,82 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import QUnit from "qunit-tests";
+import cockpit from "cockpit";
+
+import * as kdump from "./config-client.js";
+
+const basicConfig = [
+ "# top comment",
+ "",
+ "foo bar",
+ " indented value",
+ "",
+ "will disappear",
+ "key value #comment"
+].join("\n");
+
+const changedConfig = [
+ "# top comment",
+ "",
+ "foo moo",
+ "indented value",
+ "",
+ "#key value #comment",
+ "hooray value",
+ "core_collector makedumpfile -l --message-level 7 -d 31",
+ ""
+].join("\n");
+
+QUnit.test("config_update", function (assert) {
+ const done = assert.async();
+ assert.expect(6);
+ const dataWasChanged = new Promise(resolve => { this.dataWasChangedResolve = resolve });
+ let config;
+ const configChanged = (event, settings) => {
+ assert.equal(settings._internal.foo.value, "moo", "value changed correctly");
+ assert.equal("key" in settings._internal, false, "setting with comment deleted correctly");
+ assert.equal("will" in settings._internal, false, "setting without comment deleted correctly");
+ assert.equal(settings._internal.hooray.value, "value", "value added correctly");
+ assert.equal(config._rawContent, changedConfig, "raw text for changed config is correct");
+ this.dataWasChangedResolve();
+ };
+
+ const filename = "cockpit_config_read";
+ const configFile = cockpit.file(filename);
+ configFile
+ .replace(basicConfig)
+ .then(() => {
+ assert.equal(configFile.path, filename, "file has correct path");
+ config = new kdump.ConfigFile(filename);
+ config.wait().then(() => {
+ config.settings._internal.foo.value = "moo";
+ delete config.settings._internal.key;
+ delete config.settings._internal.will;
+ config.settings._internal.hooray = { value: "value" };
+ config.addEventListener('kdumpConfigChanged', configChanged);
+ config.write(config.settings)
+ .then(() => dataWasChanged.then(done));
+ });
+ });
+});
+
+window.setTimeout(function() {
+ QUnit.start();
+});
diff --git a/pkg/kdump/testwritable.sh b/pkg/kdump/testwritable.sh
new file mode 100644
index 0000000..58bfa2f
--- /dev/null
+++ b/pkg/kdump/testwritable.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+set -eu
+
+tmpfile=$(mktemp $1/.XXXXXX)
+rm "$tmpfile"
diff --git a/pkg/lib/README b/pkg/lib/README
new file mode 100644
index 0000000..bb0c6ef
--- /dev/null
+++ b/pkg/lib/README
@@ -0,0 +1,5 @@
+# Cockpit shared components
+
+This directory contains React components, JavaScript modules, webpack/esbuild
+plugins, and build tools which are shared between all Cockpit projects.
+External projects usually clone this directory into their own source tree.
diff --git a/pkg/lib/_global-variables.scss b/pkg/lib/_global-variables.scss
new file mode 100644
index 0000000..e2b9935
--- /dev/null
+++ b/pkg/lib/_global-variables.scss
@@ -0,0 +1,14 @@
+@import "@patternfly/patternfly/sass-utilities/all";
+
+/*
+ * PatternFly 4 adapting the lists too early.
+ * When PF4 has a breakpoint of 768px width, it's actually 1108 for us, as the sidebar is 340px.
+ * (This does use the intended content area, but there's a mismatch between content and browser width as we use iframes.)
+ * So redefine grid breakpoints
+ */
+$pf-v5-global--breakpoint--xs: 0 !default;
+// Do not override the sm breakpoint as for width < 768px the left nav is hidden
+$pf-v5-global--breakpoint--md: 428px !default;
+$pf-v5-global--breakpoint--lg: 652px !default;
+$pf-v5-global--breakpoint--xl: 860px !default;
+$pf-v5-global--breakpoint--2xl: 1110px !default;
diff --git a/pkg/lib/cockpit-components-context-menu.jsx b/pkg/lib/cockpit-components-context-menu.jsx
new file mode 100644
index 0000000..55c55de
--- /dev/null
+++ b/pkg/lib/cockpit-components-context-menu.jsx
@@ -0,0 +1,110 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import PropTypes from "prop-types";
+
+import { Menu, MenuContent } from "@patternfly/react-core/dist/esm/components/Menu";
+
+import "context-menu.scss";
+
+/*
+ * A context menu component
+ *
+ * It requires two properties:
+ * - parentId, area in which it listens to left button click
+ * - children, a MenuList to be rendered in the context menu
+ */
+export const ContextMenu = ({ parentId, children }) => {
+ const [visible, setVisible] = React.useState(false);
+ const [event, setEvent] = React.useState(null);
+ const root = React.useRef(null);
+
+ React.useEffect(() => {
+ const _handleContextMenu = (event) => {
+ event.preventDefault();
+
+ setVisible(true);
+ setEvent(event);
+ };
+
+ const _handleClick = (event) => {
+ if (event && event.button === 0) {
+ const wasOutside = !(event.target.contains === root.current);
+
+ if (wasOutside)
+ setVisible(false);
+ }
+ };
+
+ const parent = document.getElementById(parentId);
+ parent.addEventListener('contextmenu', _handleContextMenu);
+ document.addEventListener('click', _handleClick);
+
+ return () => {
+ parent.removeEventListener('contextmenu', _handleContextMenu);
+ document.removeEventListener('click', _handleClick);
+ };
+ }, [parentId]);
+
+ React.useEffect(() => {
+ if (!event)
+ return;
+
+ const clickX = event.clientX;
+ const clickY = event.clientY;
+ const screenW = window.innerWidth;
+ const screenH = window.innerHeight;
+ const rootW = root.current.offsetWidth;
+ const rootH = root.current.offsetHeight;
+
+ const right = (screenW - clickX) > rootW;
+ const left = !right;
+ const top = (screenH - clickY) > rootH;
+ const bottom = !top;
+
+ if (right) {
+ root.current.style.left = `${clickX + 5}px`;
+ }
+
+ if (left) {
+ root.current.style.left = `${clickX - rootW - 5}px`;
+ }
+
+ if (top) {
+ root.current.style.top = `${clickY + 5}px`;
+ }
+
+ if (bottom) {
+ root.current.style.top = `${clickY - rootH - 5}px`;
+ }
+ }, [event]);
+
+ return visible &&
+ <Menu ref={root} className="contextMenu">
+ <MenuContent ref={root}>
+ {children}
+ </MenuContent>
+ </Menu>;
+};
+
+ContextMenu.propTypes = {
+ parentId: PropTypes.string.isRequired,
+ children: PropTypes.any
+};
diff --git a/pkg/lib/cockpit-components-dialog.jsx b/pkg/lib/cockpit-components-dialog.jsx
new file mode 100644
index 0000000..041a569
--- /dev/null
+++ b/pkg/lib/cockpit-components-dialog.jsx
@@ -0,0 +1,363 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import { createRoot } from "react-dom/client";
+import PropTypes from "prop-types";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { HelpIcon, ExternalLinkAltIcon } from '@patternfly/react-icons';
+
+import "cockpit-components-dialog.scss";
+
+const _ = cockpit.gettext;
+
+/*
+ * React template for a Cockpit dialog footer
+ * It can wait for an action to complete,
+ * has a 'Cancel' button and an action button (defaults to 'OK')
+ * Expected props:
+ * - cancel_clicked optional
+ * Callback called when the dialog is canceled
+ * - cancel_button optional, defaults to 'Cancel' text styled as a link
+ * - list of actions, each an object with:
+ * - clicked
+ * Callback function that is expected to return a promise.
+ * parameter: callback to set the progress text
+ * - caption optional, defaults to 'Ok'
+ * - disabled optional, defaults to false
+ * - style defaults to 'secondary', other options: 'primary', 'danger'
+ * - idle_message optional, always show this message on the last row when idle
+ * - dialog_done optional, callback when dialog is finished (param true if success, false on cancel)
+ * - set_error: required, callback to set/clear error message from actions
+ */
+class DialogFooter extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ action_in_progress: false,
+ action_in_progress_promise: null,
+ action_progress_message: '',
+ action_progress_cancel: null,
+ action_canceled: false,
+ };
+ this.update_progress = this.update_progress.bind(this);
+ this.cancel_click = this.cancel_click.bind(this);
+ }
+
+ update_progress(msg, cancel) {
+ this.setState({ action_progress_message: msg, action_progress_cancel: cancel });
+ }
+
+ action_click(handler, caption, e) {
+ this.setState({
+ action_progress_message: '',
+ action_in_progress: true,
+ action_caption_in_progress: caption,
+ action_canceled: false,
+ });
+
+ const p = handler(this.update_progress)
+ .then(() => {
+ this.props.set_error(null);
+ this.setState({ action_in_progress: false });
+ if (this.props.dialog_done)
+ this.props.dialog_done(true);
+ })
+ .catch(error => {
+ if (this.state.action_canceled) {
+ if (this.props.dialog_done)
+ this.props.dialog_done(false);
+ } else {
+ this.props.set_error(error);
+ this.setState({ action_in_progress: false });
+ }
+ /* Always log global dialog errors for easier debugging */
+ if (error)
+ console.warn(error.message || error.toString());
+ });
+
+ if (p.progress)
+ p.progress(this.update_progress);
+
+ this.setState({ action_in_progress_promise: p });
+ if (e)
+ e.stopPropagation();
+ }
+
+ cancel_click(e) {
+ this.setState({ action_canceled: true });
+
+ if (this.props.cancel_clicked)
+ this.props.cancel_clicked();
+
+ // an action might be in progress, let that handler decide what to do if they added a cancel function
+ if (this.state.action_in_progress && this.state.action_progress_cancel) {
+ this.state.action_progress_cancel();
+ return;
+ }
+ if (this.state.action_in_progress && 'cancel' in this.state.action_in_progress_promise) {
+ this.state.action_in_progress_promise.cancel();
+ return;
+ }
+
+ if (this.props.dialog_done)
+ this.props.dialog_done(false);
+ if (e)
+ e.stopPropagation();
+ }
+
+ render() {
+ const cancel_text = this.props?.cancel_button?.text ?? _("Cancel");
+ const cancel_variant = this.props?.cancel_button?.variant ?? "link";
+
+ // If an action is in progress, show the spinner with its message and disable all actions.
+ // Cancel is only enabled when the action promise has a cancel method, or we get one
+ // via the progress reporting.
+
+ let wait_element;
+ let actions_disabled;
+ let cancel_disabled;
+ if (this.state.action_in_progress) {
+ actions_disabled = true;
+ if (!(this.state.action_in_progress_promise && this.state.action_in_progress_promise.cancel) && !this.state.action_progress_cancel)
+ cancel_disabled = true;
+ wait_element = <div className="dialog-wait-ct">
+ <span>{ this.state.action_progress_message }</span>
+ </div>;
+ } else if (this.props.idle_message) {
+ wait_element = <div className="dialog-wait-ct">
+ { this.props.idle_message }
+ </div>;
+ }
+
+ const action_buttons = this.props.actions.map(action => {
+ let caption;
+ if ('caption' in action)
+ caption = action.caption;
+ else
+ caption = _("Ok");
+
+ let variant = action.style || "secondary";
+ if (variant == "primary" && action.danger)
+ variant = "danger";
+
+ return (<Button
+ key={ caption }
+ className="apply"
+ variant={ variant }
+ isLoading={ this.state.action_in_progress && this.state.action_caption_in_progress == caption }
+ isDanger={ action.danger }
+ onClick={ this.action_click.bind(this, action.clicked, caption) }
+ isDisabled={ actions_disabled || action.disabled }
+ >{ caption }</Button>
+ );
+ });
+
+ return (
+ <>
+ { this.props.extra_element }
+ { action_buttons }
+ <Button variant={cancel_variant} className="cancel" onClick={this.cancel_click} isDisabled={cancel_disabled}>{ cancel_text }</Button>
+ { wait_element }
+ </>
+ );
+ }
+}
+
+DialogFooter.propTypes = {
+ cancel_clicked: PropTypes.func,
+ cancel_button: PropTypes.object,
+ actions: PropTypes.array.isRequired,
+ dialog_done: PropTypes.func,
+ set_error: PropTypes.func.isRequired,
+};
+
+/*
+ * React template for a Cockpit dialog
+ * The primary action button is disabled while its action is in progress (waiting for promise)
+ * Removes focus on other elements on showing
+ * Expected props:
+ * - title (string)
+ * - body (react element, top element should be of class modal-body)
+ * It is recommended for information gathering dialogs to pass references
+ * to the input components to the controller. That way, the controller can
+ * extract all necessary information (e.g. for input validation) when an
+ * action is triggered.
+ * - static_error optional, always show this error after the body element
+ * - footer (react element, top element should be of class modal-footer)
+ * - id optional, id that is assigned to the top level dialog node, but not the backdrop
+ * - variant: See PF4 Modal component's 'variant' property
+ * - titleIconVariant: See PF4 Modal component's 'titleIconVariant' property
+ * - showClose optional, specifies if 'X' button for closing the dialog is present
+ */
+class Dialog extends React.Component {
+ componentDidMount() {
+ // if we used a button to open this, make sure it's not focused anymore
+ if (document.activeElement)
+ document.activeElement.blur();
+ }
+
+ render() {
+ let help = null;
+ let footer = null;
+ if (this.props.helpLink)
+ footer = <a href={this.props.helpLink} target="_blank" rel="noopener noreferrer">{_("Learn more")} <ExternalLinkAltIcon /></a>;
+
+ if (this.props.helpMessage)
+ help = <Popover
+ bodyContent={this.props.helpMessage}
+ footerContent={footer}
+ >
+ <Button variant="plain" aria-label={_("Learn more")}>
+ <HelpIcon />
+ </Button>
+ </Popover>;
+
+ const error = this.props.error || this.props.static_error;
+ const error_alert = error && <Alert variant='danger' isInline title={error} />;
+
+ return (
+ <Modal position="top" variant={this.props.variant || "medium"}
+ titleIconVariant={this.props.titleIconVariant}
+ onEscapePress={() => undefined}
+ showClose={!!this.props.showClose}
+ id={this.props.id}
+ isOpen
+ help={help}
+ footer={this.props.footer} title={this.props.title}>
+ <Stack hasGutter>
+ { error_alert }
+ <StackItem>
+ { this.props.body }
+ </StackItem>
+ </Stack>
+ </Modal>
+ );
+ }
+}
+Dialog.propTypes = {
+ // TODO: fix following by refactoring the logic showing modal dialog (recently show_modal_dialog())
+ title: PropTypes.string, // is effectively required, but show_modal_dialog() provides initially no props and resets them later.
+ body: PropTypes.element, // is effectively required, see above
+ static_error: PropTypes.string,
+ error: PropTypes.string,
+ footer: PropTypes.element, // is effectively required, see above
+ id: PropTypes.string,
+ showClose: PropTypes.bool,
+};
+
+/* Create and show a dialog
+ * For this, create a containing DOM node at the body level
+ * The returned object has the following methods:
+ * - setFooterProps replace the current footerProps and render
+ * - setProps replace the current props and render
+ * - render render again using the stored props
+ * The DOM node and React metadata are freed once the dialog has closed
+ */
+export function show_modal_dialog(props, footerProps) {
+ const dialogName = 'cockpit_modal_dialog';
+ // don't allow nested dialogs, just close whatever is open
+ const curElement = document.getElementById(dialogName);
+ let root;
+ if (curElement) {
+ root = createRoot(curElement);
+ root.unmount();
+ curElement.remove();
+ }
+ // create an element to render into
+ const rootElement = document.createElement("div");
+ root = createRoot(rootElement);
+ rootElement.id = dialogName;
+ document.body.appendChild(rootElement);
+
+ // register our own on-close callback
+ let origCallback;
+ const closeCallback = function() {
+ if (origCallback)
+ origCallback.apply(this, arguments);
+ root.unmount();
+ rootElement.remove();
+ };
+
+ const dialogObj = { };
+ let error = null;
+ dialogObj.props = props;
+ dialogObj.footerProps = null;
+ dialogObj.render = function() {
+ dialogObj.props.footer = <DialogFooter {...dialogObj.footerProps} />;
+ // Don't render if we are no longer part of the document.
+ // This would be mostly harmless except that it will remove
+ // the input focus from whatever element has it, which is
+ // unpleasant and also disrupts the tests.
+ if (rootElement.offsetParent)
+ root.render(<Dialog {...dialogObj.props} error={error} />);
+ };
+ function updateFooterAndRender() {
+ if (dialogObj.props === null || dialogObj.props === undefined)
+ dialogObj.props = { };
+ dialogObj.props.footer = <DialogFooter {...dialogObj.footerProps} />;
+ dialogObj.render();
+ }
+ dialogObj.setFooterProps = function(footerProps) {
+ dialogObj.footerProps = footerProps;
+ if (dialogObj.footerProps.dialog_done != closeCallback) {
+ origCallback = dialogObj.footerProps.dialog_done;
+ dialogObj.footerProps.dialog_done = closeCallback;
+ }
+ dialogObj.footerProps.set_error = e => {
+ error = typeof e === 'object' && e !== null ? (e.message || e.toString()) : e;
+ dialogObj.render();
+ };
+ updateFooterAndRender();
+ };
+ dialogObj.setProps = function(props) {
+ dialogObj.props = props;
+ updateFooterAndRender();
+ };
+ dialogObj.setFooterProps(footerProps);
+ dialogObj.setProps(props);
+
+ // now actually render
+ dialogObj.render();
+
+ return dialogObj;
+}
+
+export function apply_modal_dialog(event) {
+ const dialog = event.target?.closest("[role=dialog]");
+ const button = dialog?.querySelector("button.apply");
+
+ if (button) {
+ const event = new MouseEvent('click', {
+ view: window,
+ bubbles: true,
+ cancelable: true,
+ button: 0
+ });
+ button.dispatchEvent(event);
+ }
+
+ event.preventDefault();
+ return false;
+}
diff --git a/pkg/lib/cockpit-components-dialog.scss b/pkg/lib/cockpit-components-dialog.scss
new file mode 100644
index 0000000..16e42a7
--- /dev/null
+++ b/pkg/lib/cockpit-components-dialog.scss
@@ -0,0 +1,8 @@
+.pf-v5-c-modal-box__body .scroll {
+ max-block-size: calc(75vh - 10rem);
+ overflow: auto;
+}
+
+.dialog-wait-ct-spinner {
+ margin-inline-start: var(--pf-v5-global--spacer--sm);
+}
diff --git a/pkg/lib/cockpit-components-dynamic-list.jsx b/pkg/lib/cockpit-components-dynamic-list.jsx
new file mode 100644
index 0000000..12c3760
--- /dev/null
+++ b/pkg/lib/cockpit-components-dynamic-list.jsx
@@ -0,0 +1,143 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { EmptyState, EmptyStateBody } from "@patternfly/react-core/dist/esm/components/EmptyState";
+import { FormFieldGroup, FormFieldGroupHeader } from "@patternfly/react-core/dist/esm/components/Form";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText";
+
+import './cockpit-components-dynamic-list.scss';
+
+/* Dynamic list with a variable number of rows. Each row is a custom component, usually an input field(s).
+ *
+ * Props:
+ * - emptyStateString
+ * - onChange
+ * - id
+ * - itemcomponent
+ * - formclass (optional)
+ * - options (optional)
+ * - onValidationChange: A handler function which updates the parent's component's validation object.
+ * Its parameter is an array the same structure as 'validationFailed'.
+ * - validationFailed: An array where each item represents a validation error of the corresponding row component index.
+ * A row is strictly mapped to an item of the array by its index.
+ * Example: Let's have a dynamic form, where each row consists of 2 fields: name and email. Then a validation array of
+ * these rows would look like this:
+ * [
+ * { name: "Name must not be empty }, // first row
+ * { }, // second row
+ * { name: "Name cannot containt number", email: "Email must contain '@'" } // third row
+ * ]
+ */
+export class DynamicListForm extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ list: [],
+ };
+ this.keyCounter = 0;
+ this.removeItem = this.removeItem.bind(this);
+ this.addItem = this.addItem.bind(this);
+ this.onItemChange = this.onItemChange.bind(this);
+ }
+
+ removeItem(idx) {
+ const validationFailedDelta = this.props.validationFailed ? [...this.props.validationFailed] : [];
+ // We also need to remove any error messages which the item (row) may have contained
+ validationFailedDelta.splice(idx, 1);
+ this.props.onValidationChange?.(validationFailedDelta);
+
+ this.setState(state => {
+ const items = [...state.list];
+ // keep the list structure, otherwise all the indexes shift and the ID/key mapping gets broken
+ delete items[idx];
+
+ return { list: items };
+ }, () => this.props.onChange(this.state.list));
+ }
+
+ addItem() {
+ this.setState(state => {
+ return { list: [...state.list, { key: this.keyCounter++, ...this.props.default }] };
+ }, () => this.props.onChange(this.state.list));
+ }
+
+ onItemChange(idx, field, value) {
+ this.setState(state => {
+ const items = [...state.list];
+ items[idx][field] = value || null;
+ return { list: items };
+ }, () => this.props.onChange(this.state.list));
+ }
+
+ render () {
+ const { id, label, actionLabel, formclass, emptyStateString, helperText, validationFailed, onValidationChange } = this.props;
+ const dialogValues = this.state;
+ return (
+ <FormFieldGroup header={
+ <FormFieldGroupHeader
+ titleText={{ text: label }}
+ actions={<Button variant="secondary" className="btn-add" onClick={this.addItem}>{actionLabel}</Button>}
+ />
+ } className={"dynamic-form-group " + formclass}>
+ {
+ dialogValues.list.some(item => item !== undefined)
+ ? <>
+ {dialogValues.list.map((item, idx) => {
+ if (item === undefined)
+ return null;
+
+ return React.cloneElement(this.props.itemcomponent, {
+ idx,
+ item,
+ id: id + "-" + idx,
+ key: idx,
+ onChange: this.onItemChange,
+ removeitem: this.removeItem,
+ additem: this.addItem,
+ options: this.props.options,
+ validationFailed: validationFailed && validationFailed[idx],
+ onValidationChange: value => {
+ // Dynamic list consists of multiple rows. Therefore validationFailed object is presented as an array where each item represents a row
+ // Each row/item then consists of key-value pairs, which represent a field name and it's validation error
+ const delta = validationFailed ? [...validationFailed] : [];
+ // Update validation of only a single row
+ delta[idx] = value;
+
+ // If a row doesn't contain any fields with errors anymore, we delete the item of the array
+ // Deleting an item of an array replaces an item with an "empty item".
+ // This guarantees that an array of validation errors maps to the correct rows
+ if (Object.keys(delta[idx]).length == 0)
+ delete delta[idx];
+
+ onValidationChange?.(delta);
+ },
+ });
+ })
+ }
+ {helperText &&
+ <HelperText>
+ <HelperTextItem>{helperText}</HelperTextItem>
+ </HelperText>
+ }
+ </>
+ : <EmptyState>
+ <EmptyStateBody>
+ {emptyStateString}
+ </EmptyStateBody>
+ </EmptyState>
+ }
+ </FormFieldGroup>
+ );
+ }
+}
+
+DynamicListForm.propTypes = {
+ emptyStateString: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ id: PropTypes.string.isRequired,
+ itemcomponent: PropTypes.object.isRequired,
+ formclass: PropTypes.string,
+ options: PropTypes.object,
+ validationFailed: PropTypes.array,
+ onValidationChange: PropTypes.func,
+};
diff --git a/pkg/lib/cockpit-components-dynamic-list.scss b/pkg/lib/cockpit-components-dynamic-list.scss
new file mode 100644
index 0000000..2016fda
--- /dev/null
+++ b/pkg/lib/cockpit-components-dynamic-list.scss
@@ -0,0 +1,39 @@
+@import "global-variables";
+
+.dynamic-form-group {
+ .pf-v5-c-empty-state {
+ padding: 0;
+ }
+
+ .pf-v5-c-form__label {
+ // Don't allow labels to wrap
+ white-space: nowrap;
+ }
+
+ .remove-button-group {
+ // Move 'Remove' button the the end of the row
+ grid-column: -1;
+ // Move 'Remove' button to the bottom of the line so as to align with the other form fields
+ display: flex;
+ align-items: flex-end;
+ }
+
+ // Set check to the same height as input widgets and vertically align
+ .pf-v5-c-form__group-control > .pf-v5-c-check {
+ // Set height to the same as inputs
+ // Font height is font size * line height (1rem * 1.5)
+ // Widgets have 5px padding, 1px border (top & bottom): (5 + 1) * 2 = 12
+ // This all equals to 36px
+ block-size: calc(var(--pf-v5-global--FontSize--md) * var(--pf-v5-global--LineHeight--md) + 12px);
+ align-content: center;
+ }
+
+ // We use FormFieldGroup PF component for the nested look and for ability to add buttons to the header
+ // However we want to save space and not add indent to the left so we need to override it
+ .pf-v5-c-form__field-group-body {
+ // Stretch content fully
+ --pf-v5-c-form__field-group-body--GridColumn: 1 / -1;
+ // Reduce padding at the top
+ --pf-v5-c-form__field-group-body--PaddingTop: var(--pf-v5-global--spacer--xs);
+ }
+}
diff --git a/pkg/lib/cockpit-components-empty-state.css b/pkg/lib/cockpit-components-empty-state.css
new file mode 100644
index 0000000..06ba60e
--- /dev/null
+++ b/pkg/lib/cockpit-components-empty-state.css
@@ -0,0 +1,3 @@
+.pf-v5-c-empty-state .pf-v5-c-button.pf-m-primary.slim {
+ margin: 0;
+}
diff --git a/pkg/lib/cockpit-components-empty-state.jsx b/pkg/lib/cockpit-components-empty-state.jsx
new file mode 100644
index 0000000..0a78d05
--- /dev/null
+++ b/pkg/lib/cockpit-components-empty-state.jsx
@@ -0,0 +1,63 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import PropTypes from 'prop-types';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { EmptyStateActions, EmptyState, EmptyStateBody, EmptyStateFooter, EmptyStateHeader, EmptyStateIcon, EmptyStateVariant } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import "./cockpit-components-empty-state.css";
+
+export const EmptyStatePanel = ({ title, paragraph, loading, icon, action, isActionInProgress, onAction, secondary, headingLevel, titleSize }) => {
+ const slimType = title || paragraph ? "" : "slim";
+ return (
+ <EmptyState variant={EmptyStateVariant.full}>
+ <EmptyStateHeader titleText={title} headingLevel={headingLevel} icon={(loading || icon) && <EmptyStateIcon icon={loading ? Spinner : icon} />} />
+ <EmptyStateBody>
+ {paragraph}
+ </EmptyStateBody>
+ {(action || secondary) && <EmptyStateFooter>
+ { action && (typeof action == "string"
+ ? <Button variant="primary" className={slimType}
+ isLoading={isActionInProgress}
+ isDisabled={isActionInProgress}
+ onClick={onAction}>{action}</Button>
+ : action)}
+ { secondary && <EmptyStateActions>{secondary}</EmptyStateActions> }
+ </EmptyStateFooter>}
+ </EmptyState>
+ );
+};
+
+EmptyStatePanel.propTypes = {
+ loading: PropTypes.bool,
+ icon: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
+ title: PropTypes.string,
+ paragraph: PropTypes.node,
+ action: PropTypes.node,
+ isActionInProgress: PropTypes.bool,
+ onAction: PropTypes.func,
+ secondary: PropTypes.node,
+};
+
+EmptyStatePanel.defaultProps = {
+ headingLevel: "h1",
+ titleSize: "lg",
+ isActionInProgress: false,
+};
diff --git a/pkg/lib/cockpit-components-file-autocomplete.jsx b/pkg/lib/cockpit-components-file-autocomplete.jsx
new file mode 100644
index 0000000..afe4d15
--- /dev/null
+++ b/pkg/lib/cockpit-components-file-autocomplete.jsx
@@ -0,0 +1,212 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import PropTypes from "prop-types";
+import { debounce } from 'throttle-debounce';
+
+const _ = cockpit.gettext;
+
+export class FileAutoComplete extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ directory: '', // The current directory we list files/dirs from
+ displayFiles: [],
+ isOpen: false,
+ value: this.props.value || null,
+ };
+
+ this.typeaheadInputValue = "";
+ this.allowFilesUpdate = true;
+ this.updateFiles = this.updateFiles.bind(this);
+ this.finishUpdate = this.finishUpdate.bind(this);
+ this.onToggle = this.onToggle.bind(this);
+ this.clearSelection = this.clearSelection.bind(this);
+ this.onCreateOption = this.onCreateOption.bind(this);
+
+ this.onPathChange = (value) => {
+ if (!value) {
+ this.clearSelection();
+ return;
+ }
+
+ this.typeaheadInputValue = value;
+
+ const cb = (dirPath) => this.updateFiles(dirPath == '' ? '/' : dirPath);
+
+ let path = value;
+ if (value.lastIndexOf('/') == value.length - 1)
+ path = value.slice(0, value.length - 1);
+
+ const match = this.state.displayFiles
+ .find(entry => (entry.type == 'directory' && entry.path == path + '/') || (entry.type == 'file' && entry.path == path));
+
+ if (match) {
+ // If match file path is a prefix of another file, do not update current directory,
+ // since we cannot tell file/directory user wants to select
+ // https://bugzilla.redhat.com/show_bug.cgi?id=2097662
+ const isPrefix = this.state.displayFiles.filter(entry => entry.path.startsWith(value)).length > 1;
+ // If the inserted string corresponds to a directory listed in the results
+ // update the current directory and refetch results
+ if (match.type == 'directory' && !isPrefix)
+ cb(match.path);
+ else
+ this.setState({ value: match.path });
+ } else {
+ // If the inserted string's parent directory is not matching the `directory`
+ // in the state object we need to update the parent directory and recreate the displayFiles
+ const parentDir = value.slice(0, value.lastIndexOf('/'));
+
+ if (parentDir + '/' != this.state.directory) {
+ return this.updateFiles(parentDir + '/');
+ }
+ }
+ };
+ this.debouncedChange = debounce(300, this.onPathChange);
+ this.onPathChange(this.state.value);
+ }
+
+ componentWillUnmount() {
+ this.allowFilesUpdate = false;
+ }
+
+ onCreateOption(newValue) {
+ this.setState(prevState => ({
+ displayFiles: [...prevState.displayFiles, { type: "file", path: newValue }]
+ }));
+ }
+
+ updateFiles(path) {
+ if (this.state.directory == path)
+ return;
+
+ const channel = cockpit.channel({
+ payload: "fslist1",
+ path,
+ superuser: this.props.superuser,
+ watch: false,
+ });
+ const results = [];
+
+ channel.addEventListener("ready", () => {
+ this.finishUpdate(results, null, path);
+ });
+
+ channel.addEventListener("close", (ev, data) => {
+ this.finishUpdate(results, data.message, path);
+ });
+
+ channel.addEventListener("message", (ev, data) => {
+ const item = JSON.parse(data);
+ if (item && item.path && item.event == 'present') {
+ item.path = item.path + (item.type == 'directory' ? '/' : '');
+ results.push(item);
+ }
+ });
+ }
+
+ finishUpdate(results, error, directory) {
+ if (!this.allowFilesUpdate)
+ return;
+ results = results.sort((a, b) => a.path.localeCompare(b.path, { sensitivity: 'base' }));
+
+ const listItems = results.map(file => ({
+ type: file.type,
+ path: (directory == '' ? '/' : directory) + file.path
+ }));
+
+ if (directory) {
+ listItems.unshift({
+ type: "directory",
+ path: directory
+ });
+ }
+
+ if (error || !this.state.value)
+ this.props.onChange('', error);
+
+ if (!error)
+ this.setState({ displayFiles: listItems, directory });
+ this.setState({
+ error,
+ });
+ }
+
+ onToggle(_, isOpen) {
+ this.setState({ isOpen });
+ }
+
+ clearSelection() {
+ this.typeaheadInputValue = "";
+ this.updateFiles("/");
+ this.setState({
+ value: null,
+ isOpen: false
+ });
+ this.props.onChange('', null);
+ }
+
+ render() {
+ const placeholder = this.props.placeholder || _("Path to file");
+
+ const selectOptions = this.state.displayFiles
+ .map(option => <SelectOption key={option.path}
+ className={option.type}
+ value={option.path} />);
+ return (
+ <Select
+ variant="typeahead"
+ id={this.props.id}
+ isInputValuePersisted
+ onTypeaheadInputChanged={this.debouncedChange}
+ placeholderText={placeholder}
+ noResultsFoundText={this.state.error || _("No such file or directory")}
+ selections={this.state.value}
+ onSelect={(_, value) => {
+ this.setState({ value, isOpen: false });
+ this.debouncedChange(value);
+ this.props.onChange(value || '', null);
+ }}
+ onToggle={this.onToggle}
+ onClear={this.clearSelection}
+ isOpen={this.state.isOpen}
+ isCreatable={this.props.isOptionCreatable}
+ createText={_("Create")}
+ onCreateOption={this.onCreateOption}
+ menuAppendTo="parent">
+ {selectOptions}
+ </Select>
+ );
+ }
+}
+FileAutoComplete.propTypes = {
+ id: PropTypes.string,
+ placeholder: PropTypes.string,
+ superuser: PropTypes.string,
+ isOptionCreatable: PropTypes.bool,
+ onChange: PropTypes.func,
+ value: PropTypes.string,
+};
+FileAutoComplete.defaultProps = {
+ isOptionCreatable: false,
+ onChange: () => '',
+};
diff --git a/pkg/lib/cockpit-components-firewalld-request.jsx b/pkg/lib/cockpit-components-firewalld-request.jsx
new file mode 100644
index 0000000..517b8bb
--- /dev/null
+++ b/pkg/lib/cockpit-components-firewalld-request.jsx
@@ -0,0 +1,167 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React, { useState } from 'react';
+import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js";
+import { PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+
+import cockpit from 'cockpit';
+import './cockpit-components-firewalld-request.scss';
+
+const _ = cockpit.gettext;
+const firewalld = cockpit.dbus('org.fedoraproject.FirewallD1', { superuser: "try" });
+
+function debug() {
+ if (window.debugging == "all" || window.debugging?.includes("firewall"))
+ console.debug.apply(console, arguments);
+}
+
+/* React component for an info alert to enable some new service in firewalld.
+ * Use this when enabling some network-facing service. The alert will only be shown
+ * if firewalld is running, has at least one active zone, and the service is not enabled
+ * in any zone yet. It will allow the user to enable the service in any active zone,
+ * or go to the firewall page for more fine-grained configuration.
+ *
+ * Properties:
+ * - service (string, required): firewalld service name
+ * - title (string, required): Human readable/translated alert title
+ * - pageSection (bool, optional, default false): Render the alert inside a <PageSection>
+ */
+export const FirewalldRequest = ({ service, title, pageSection }) => {
+ const [zones, setZones] = useState(null);
+ const [selectedZone, setSelectedZone] = useState(null);
+ const [zoneSelectorOpened, setZoneSelectorOpened] = useState(false);
+ const [enabledAnywhere, setEnabledAnywhere] = useState(null);
+ const [enableError, setEnableError] = useState(null);
+ debug("FirewalldRequest", service, "zones", JSON.stringify(zones), "selected zone", selectedZone, "enabledAnywhere", enabledAnywhere);
+
+ if (!service)
+ return null;
+
+ // query zones on component initialization
+ if (zones === null) {
+ firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1.zone", "getActiveZones")
+ .then(([info]) => {
+ const names = Object.keys(info);
+ Promise.all(names.map(name => firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1.zone", "getZoneSettings2", [name])))
+ .then(zoneInfos => {
+ setEnabledAnywhere(!!zoneInfos.find(zoneInfo => ((zoneInfo[0].services || {}).v || []).indexOf(service) >= 0));
+ setZones(names);
+ })
+ .catch(ex => {
+ console.warn("FirewalldRequest: getZoneSettings failed:", JSON.stringify(ex));
+ setZones([]);
+ });
+
+ firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1", "getDefaultZone")
+ .then(([zone]) => setSelectedZone(zone))
+ .catch(ex => console.warn("FirewalldRequest: getDefaultZone failed:", JSON.stringify(ex)));
+ })
+ .catch(ex => {
+ // firewalld not running
+ debug("FirewalldRequest: getActiveZones failed, considering firewall inactive:", JSON.stringify(ex));
+ setZones([]);
+ });
+ }
+
+ const onAddService = () => {
+ firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1.zone", "addService",
+ [selectedZone, service, 0])
+ // permanent config
+ .then(() => firewalld.call("/org/fedoraproject/FirewallD1/config",
+ "org.fedoraproject.FirewallD1.config",
+ "getZoneByName", [selectedZone]))
+ .then(([path]) => firewalld.call(path, "org.fedoraproject.FirewallD1.config.zone", "addService", [service]))
+ // all successful, hide alert
+ .then(() => setEnabledAnywhere(true))
+ .catch(ex => {
+ // may already be enabled in permanent config, that's ok
+ if (ex.message && ex.message.indexOf("ALREADY_ENABLED") >= 0) {
+ setEnabledAnywhere(true);
+ return;
+ }
+
+ setEnableError(ex.toString());
+ setEnabledAnywhere(true);
+ console.error("Failed to enable", service, "in firewalld:", JSON.stringify(ex));
+ });
+ };
+
+ let alert;
+
+ if (enableError) {
+ alert = (
+ <Alert isInline variant="warning"
+ title={ cockpit.format(_("Failed to enable $0 in firewalld"), service) }
+ actionClose={ <AlertActionCloseButton onClose={ () => setEnableError(null) } /> }
+ actionLinks={
+ <AlertActionLink onClick={() => cockpit.jump("/network/firewall")}>
+ { _("Visit firewall") }
+ </AlertActionLink>
+ }>
+ {enableError}
+ </Alert>
+ );
+ // don't show anything if firewalld is not active, or service is already enabled somewhere
+ } else if (!zones || zones.length === 0 || !selectedZone || enabledAnywhere) {
+ return null;
+ } else {
+ alert = (
+ <Alert isInline variant="info" title={title} className="pf-v5-u-box-shadow-sm">
+ <Toolbar className="ct-alert-toolbar">
+ <ToolbarContent>
+ <ToolbarGroup spaceItems={{ default: "spaceItemsMd" }}>
+ <ToolbarItem variant="label">{ _("Zone") }</ToolbarItem>
+ <ToolbarItem>
+ <Select
+ aria-label={_("Zone")}
+ onToggle={(_event, isOpen) => setZoneSelectorOpened(isOpen)}
+ isOpen={zoneSelectorOpened}
+ onSelect={ (e, sel) => { setSelectedZone(sel); setZoneSelectorOpened(false) } }
+ selections={selectedZone}
+ toggleId={"firewalld-request-" + service}>
+ { zones.map(zone => <SelectOption key={zone} value={zone}>{zone}</SelectOption>) }
+ </Select>
+ </ToolbarItem>
+
+ <ToolbarItem>
+ <Button variant="primary" onClick={onAddService}>{ cockpit.format(_("Add $0"), service) }</Button>
+ </ToolbarItem>
+ </ToolbarGroup>
+
+ <ToolbarItem variant="separator" />
+
+ <ToolbarItem>
+ <Button variant="link" onClick={() => cockpit.jump("/network/firewall")}>
+ { _("Visit firewall") }
+ </Button>
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ </Alert>
+ );
+ }
+
+ if (pageSection)
+ return <PageSection className="ct-no-bottom-padding">{alert}</PageSection>;
+ else
+ return alert;
+};
diff --git a/pkg/lib/cockpit-components-firewalld-request.scss b/pkg/lib/cockpit-components-firewalld-request.scss
new file mode 100644
index 0000000..3c27011
--- /dev/null
+++ b/pkg/lib/cockpit-components-firewalld-request.scss
@@ -0,0 +1,12 @@
+// Required for `pf-v5-u-box-shadow-sm` as inline alert don't have a box shadow
+@use "../../node_modules/@patternfly/patternfly/utilities/BoxShadow/box-shadow.css";
+
+.pf-v5-c-page__main-section.ct-no-bottom-padding {
+ --pf-v5-c-page__main-section--PaddingBottom: 0;
+}
+
+// <Toolbar> embedded into an <Alert>
+.pf-v5-c-toolbar.ct-alert-toolbar {
+ --pf-v5-c-toolbar--BackgroundColor: transparent;
+ --pf-v5-c-toolbar--PaddingBottom: 0;
+}
diff --git a/pkg/lib/cockpit-components-form-helper.jsx b/pkg/lib/cockpit-components-form-helper.jsx
new file mode 100644
index 0000000..86e7f11
--- /dev/null
+++ b/pkg/lib/cockpit-components-form-helper.jsx
@@ -0,0 +1,43 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+
+import { FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText";
+
+export const FormHelper = ({ helperText, helperTextInvalid, variant, icon, fieldId }) => {
+ const formHelperVariant = variant || (helperTextInvalid ? "error" : "default");
+
+ if (!(helperText || helperTextInvalid))
+ return null;
+
+ return (
+ <FormHelperText>
+ <HelperText>
+ <HelperTextItem
+ id={fieldId ? (fieldId + "-helper") : undefined}
+ variant={formHelperVariant}
+ icon={icon}>
+ {formHelperVariant === "error" ? helperTextInvalid : helperText}
+ </HelperTextItem>
+ </HelperText>
+ </FormHelperText>
+ );
+};
diff --git a/pkg/lib/cockpit-components-inline-notification.css b/pkg/lib/cockpit-components-inline-notification.css
new file mode 100644
index 0000000..3c428bc
--- /dev/null
+++ b/pkg/lib/cockpit-components-inline-notification.css
@@ -0,0 +1,7 @@
+.alert-link.more-button {
+ margin-inline-start: var(--pf-v5-global--spacer--sm);
+}
+
+.notification-message {
+ white-space: pre-wrap;
+}
diff --git a/pkg/lib/cockpit-components-inline-notification.jsx b/pkg/lib/cockpit-components-inline-notification.jsx
new file mode 100644
index 0000000..94985ce
--- /dev/null
+++ b/pkg/lib/cockpit-components-inline-notification.jsx
@@ -0,0 +1,96 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React from 'react';
+import PropTypes from 'prop-types';
+import cockpit from 'cockpit';
+
+import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import './cockpit-components-inline-notification.css';
+
+const _ = cockpit.gettext;
+
+function mouseClick(fun) {
+ return function (event) {
+ if (!event || event.button !== 0)
+ return;
+ event.preventDefault();
+ return fun(event);
+ };
+}
+
+export class InlineNotification extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isDetail: false,
+ };
+
+ this.toggleDetail = this.toggleDetail.bind(this);
+ }
+
+ toggleDetail () {
+ this.setState({
+ isDetail: !this.state.isDetail,
+ });
+ }
+
+ render () {
+ const { text, detail, type, onDismiss } = this.props;
+
+ let detailButton = null;
+ if (detail) {
+ let detailButtonText = _("show more");
+ if (this.state.isDetail) {
+ detailButtonText = _("show less");
+ }
+
+ detailButton = (<Button variant="link" isInline className='alert-link more-button'
+ onClick={mouseClick(this.toggleDetail)}>{detailButtonText}</Button>);
+ }
+ const extraProps = {};
+ if (onDismiss)
+ extraProps.actionClose = <AlertActionCloseButton onClose={onDismiss} />;
+
+ return (
+ <Alert variant={type || 'danger'}
+ isLiveRegion={this.props.isLiveRegion}
+ isInline={this.props.isInline != undefined ? this.props.isInline : true}
+ title={<> {text} {detailButton} </>} {...extraProps}>
+ {this.state.isDetail && (<p>{detail}</p>)}
+ </Alert>
+ );
+ }
+}
+
+InlineNotification.propTypes = {
+ onDismiss: PropTypes.func,
+ isInline: PropTypes.bool,
+ text: PropTypes.string.isRequired, // main information to render
+ detail: PropTypes.string, // optional, more detailed information. If empty, the more/less button is not rendered.
+ type: PropTypes.string,
+};
+
+export const ModalError = ({ dialogError, dialogErrorDetail, id, isExpandable }) => {
+ return (
+ <Alert id={id} variant='danger' isInline title={dialogError} isExpandable={!!isExpandable}>
+ { typeof dialogErrorDetail === 'string' ? <p>{dialogErrorDetail}</p> : dialogErrorDetail }
+ </Alert>
+ );
+};
diff --git a/pkg/lib/cockpit-components-install-dialog.css b/pkg/lib/cockpit-components-install-dialog.css
new file mode 100644
index 0000000..5fdfb3e
--- /dev/null
+++ b/pkg/lib/cockpit-components-install-dialog.css
@@ -0,0 +1,43 @@
+.package-list-ct {
+ margin-block: 1em;
+ margin-inline: 0;
+ padding: 0;
+ max-inline-size: 110rem;
+ text-align: start;
+ box-sizing: border-box;
+}
+
+.package-list-ct li {
+ text-align: start;
+ box-sizing: border-box;
+ inline-size: 22rem;
+ padding-block: 0;
+ padding-inline: 1ex;
+ display: inline-block;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.scale-up-ct {
+ animation: dialogScaleUpCt 1s;
+}
+
+@keyframes dialogScaleUpCt {
+ 0% {
+ opacity: 0;
+ max-block-size: 0;
+ }
+
+ 25% {
+ opacity: 0;
+ }
+
+ 50% {
+ max-block-size: 100vh;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/pkg/lib/cockpit-components-install-dialog.jsx b/pkg/lib/cockpit-components-install-dialog.jsx
new file mode 100644
index 0000000..0d933ae
--- /dev/null
+++ b/pkg/lib/cockpit-components-install-dialog.jsx
@@ -0,0 +1,211 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { WarningTriangleIcon } from "@patternfly/react-icons";
+
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+import * as PK from "packagekit.js";
+
+import "cockpit-components-install-dialog.css";
+
+const _ = cockpit.gettext;
+
+// TODO - generalize this to arbitrary number of arguments (when needed)
+function format_to_fragments(fmt, arg) {
+ const index = fmt.indexOf("$0");
+ if (index >= 0)
+ return <>{fmt.slice(0, index)}{arg}{fmt.slice(index + 2)}</>;
+ else
+ return fmt;
+}
+
+/* Calling install_dialog will open a dialog that lets the user
+ * install the given package.
+ *
+ * The install_dialog function returns a promise that is fulfilled when the dialog closes after
+ * a successful installation. The promise is rejected when the user cancels the dialog.
+ *
+ * If the package is already installed before the dialog opens, we still go
+ * through all the motions and the dialog closes successfully without doing
+ * anything when the use hits "Install".
+ *
+ * You shouldn't call install_dialog unless you know that PackageKit is available.
+ * (If you do anyway, the resulting D-Bus errors will be shown to the user.)
+ */
+
+export function install_dialog(pkg, options) {
+ let data = null;
+ let error_message = null;
+ let progress_message = null;
+ let cancel = null;
+ let done = null;
+
+ if (!Array.isArray(pkg))
+ pkg = [pkg];
+
+ options = options || { };
+
+ const prom = new Promise((resolve, reject) => { done = f => { if (f) resolve(); else reject(); } });
+
+ let dialog = null;
+ function update() {
+ let extra_details = null;
+ let remove_details = null;
+ let footer_message = null;
+
+ const missing_name = <strong>{pkg.join(", ")}</strong>;
+
+ if (data && data.extra_names.length > 0)
+ extra_details = (
+ <div className="scale-up-ct">
+ {_("Additional packages:")}
+ <ul className="package-list-ct">{data.extra_names.map(id => <li key={id}>{id}</li>)}</ul>
+ </div>
+ );
+
+ if (data && data.remove_names.length > 0)
+ remove_details = (
+ <div className="scale-up-ct">
+ <WarningTriangleIcon /> {_("Removals:")}
+ <ul className="package-list">{data.remove_names.map(id => <li key={id}>{id}</li>)}</ul>
+ </div>
+ );
+
+ if (progress_message)
+ footer_message = (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <span>{ progress_message }</span>
+ <Spinner size="sm" />
+ </Flex>
+ );
+ else if (data?.download_size) {
+ footer_message = (
+ <div>
+ { format_to_fragments(_("Total size: $0"), <strong>{cockpit.format_bytes(data.download_size)}</strong>) }
+ </div>
+ );
+ }
+
+ const body = {
+ id: "dialog",
+ title: options.title || _("Install software"),
+ body: (
+ <div className="scroll">
+ <p>{ format_to_fragments(options.text || _("$0 will be installed."), missing_name) }</p>
+ { remove_details }
+ { extra_details }
+ </div>
+ ),
+ static_error: error_message,
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Install"),
+ style: "primary",
+ clicked: install_missing,
+ disabled: data == null
+ }
+ ],
+ idle_message: footer_message,
+ dialog_done: f => { if (!f && cancel) cancel(); done(f) }
+ };
+
+ if (dialog) {
+ dialog.setProps(body);
+ dialog.setFooterProps(footer);
+ } else {
+ dialog = show_modal_dialog(body, footer);
+ }
+ }
+
+ function check_missing() {
+ PK.check_missing_packages(pkg,
+ p => {
+ cancel = p.cancel;
+ let pm = null;
+ if (p.waiting)
+ pm = _("Waiting for other software management operations to finish");
+ else
+ pm = _("Checking installed software");
+ if (pm != progress_message) {
+ progress_message = pm;
+ update();
+ }
+ })
+ .then(d => {
+ if (d.unavailable_names.length > 0)
+ error_message = cockpit.format(_("$0 is not available from any repository."),
+ d.unavailable_names[0]);
+ else
+ data = d;
+ progress_message = null;
+ cancel = null;
+ update();
+ })
+ .catch(e => {
+ progress_message = null;
+ cancel = null;
+ error_message = e.toString();
+ update();
+ });
+ }
+
+ function install_missing() {
+ // We need to return a Cockpit flavoured promise since we want
+ // to use progress notifications.
+ const dfd = cockpit.defer();
+
+ PK.install_missing_packages(data,
+ p => {
+ let text = null;
+ if (p.waiting) {
+ text = _("Waiting for other software management operations to finish");
+ } else if (p.package) {
+ let fmt;
+ if (p.info == PK.Enum.INFO_DOWNLOADING)
+ fmt = _("Downloading $0");
+ else if (p.info == PK.Enum.INFO_REMOVING)
+ fmt = _("Removing $0");
+ else
+ fmt = _("Installing $0");
+ text = format_to_fragments(fmt, <strong>{p.package}</strong>);
+ }
+ dfd.notify(text, p.cancel);
+ })
+ .then(() => {
+ dfd.resolve();
+ })
+ .catch(error => {
+ dfd.reject(error);
+ });
+
+ return dfd.promise;
+ }
+
+ update();
+ check_missing();
+ return prom;
+}
diff --git a/pkg/lib/cockpit-components-listing-panel.jsx b/pkg/lib/cockpit-components-listing-panel.jsx
new file mode 100644
index 0000000..3f2b784
--- /dev/null
+++ b/pkg/lib/cockpit-components-listing-panel.jsx
@@ -0,0 +1,87 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Tab, TabTitleText, Tabs } from "@patternfly/react-core/dist/esm/components/Tabs/index.js";
+import './cockpit-components-listing-panel.scss';
+
+/* tabRenderers optional: list of tab renderers for inline expansion, array of objects with
+ * - name tab name (has to be unique in the entry, used as react key)
+ * - renderer react component
+ * - data render data passed to the tab renderer
+ */
+export class ListingPanel extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ activeTab: props.initiallyActiveTab ? props.initiallyActiveTab : 0, // currently active tab in expanded mode, defaults to first tab
+ };
+ this.handleTabClick = this.handleTabClick.bind(this);
+ }
+
+ handleTabClick(event, tabIndex) {
+ event.preventDefault();
+ if (this.state.activeTab !== tabIndex) {
+ this.setState({ activeTab: tabIndex });
+ }
+ }
+
+ render() {
+ let listingDetail;
+ if ('listingDetail' in this.props) {
+ listingDetail = (
+ <span className="ct-listing-panel-caption">
+ {this.props.listingDetail}
+ </span>
+ );
+ }
+
+ return (
+ <div className="ct-listing-panel">
+ {listingDetail && <div className="ct-listing-panel-actions pf-v5-c-tabs">
+ {listingDetail}
+ </div>}
+ {this.props.tabRenderers.length && <Tabs activeKey={this.state.activeTab} className="ct-listing-panel-tabs" mountOnEnter onSelect={this.handleTabClick}>
+ {this.props.tabRenderers.map((itm, tabIdx) => {
+ const Renderer = itm.renderer;
+ const rendererData = itm.data;
+
+ return (
+ <Tab key={tabIdx} eventKey={tabIdx} title={<TabTitleText>{itm.name}</TabTitleText>}>
+ <div className="ct-listing-panel-body" key={tabIdx} data-key={tabIdx}>
+ <Renderer {...rendererData} />
+ </div>
+ </Tab>
+ );
+ })}
+ </Tabs>}
+ </div>
+ );
+ }
+}
+ListingPanel.defaultProps = {
+ tabRenderers: [],
+};
+
+ListingPanel.propTypes = {
+ tabRenderers: PropTypes.array,
+ listingDetail: PropTypes.node,
+ initiallyActiveTab: PropTypes.number,
+};
diff --git a/pkg/lib/cockpit-components-listing-panel.scss b/pkg/lib/cockpit-components-listing-panel.scss
new file mode 100644
index 0000000..c258ae6
--- /dev/null
+++ b/pkg/lib/cockpit-components-listing-panel.scss
@@ -0,0 +1,93 @@
+.ct-listing-panel {
+ display: flex;
+ flex-wrap: wrap;
+
+ &-actions {
+ order: 2;
+ flex-grow: 1;
+ padding-block: var(--pf-v5-global--spacer--sm);
+ padding-inline: var(--pf-v5-global--spacer--md) var(--pf-v5-global--spacer--lg);
+ }
+
+ &-caption {
+ margin-inline-start: auto;
+ }
+
+ &-tabs {
+ flex-grow: 1;
+ order: 1;
+ }
+
+ .pf-v5-c-tab-content {
+ order: 3;
+ flex-basis: 100%;
+ }
+
+ &-body {
+ // Don't let PF4 automatically add a border in tables inside the body
+ --pf-v5-c-table__expandable-row--after--BorderLeftWidth: 0;
+ --pf-v5-c-table--border-width--base: 0;
+
+ // Add some sizing to the body
+ padding-block: var(--pf-v5-global--spacer--md);
+ padding-inline: var(--pf-v5-global--spacer--lg);
+ inline-size: 100%;
+
+ // Containing hack part 1
+ float: inline-start;
+
+ &::after {
+ // Containing hack part 2: Clearfix CSS hack,
+ // to allow children content to float fine without setting overflow
+ content: "";
+ clear: both;
+ display: table;
+ }
+ }
+}
+
+.ct-table {
+ > tbody > .pf-v5-c-table__expandable-row {
+ // Don't scroll table's expanded contents vertically.
+ // Instead, rely on page scrolling.
+ // Important for mobile; also useful for desktop.
+ overflow-block: visible !important;
+ max-block-size: unset !important;
+ }
+}
+
+// PF4 upstream issue to adopt expand animation:
+// https://github.com/patternfly/patternfly-design/issues/899
+
+@media not all and (prefers-reduced-motion: reduce) {
+ // Add expansion animations when prefers-reduced isn't enabled
+ .ct-table .pf-v5-c-table__expandable-row-content {
+ // Animation ends at or before 2/3 in most cases; so we extend by 1.5 to compensate
+ animation: ctListingPanelShow calc(var(--pf-v5-global--TransitionDuration) * 1.5) var(--pf-v5-global--TimingFunction);
+ }
+}
+
+@keyframes ctListingPanelShow {
+ 0% {
+ // The animation needs to flow downward to feel natural
+ transform-origin: top;
+ // Overflow will revert when done (but should be hidden during animation)
+ overflow: hidden;
+ max-block-size: 0;
+ // Padding should 'tween between 0 and the actual padding (unstated)
+ padding-block: 0;
+ }
+
+ 67% {
+ // Max height is tricky in animations, as auto doesn't work
+ // 100vh makes sense, but would cause different speeds on different devices
+ // Screens are almost all =< 12000px; data is almost always smaller
+ // we'll relax it to to 100vh at 100%, just in case.
+ max-block-size: 1200px;
+ }
+
+ 100% {
+ // Allow content to extend to the height of the screen (just in case)
+ max-block-size: 100vh;
+ }
+}
diff --git a/pkg/lib/cockpit-components-logs-panel.jsx b/pkg/lib/cockpit-components-logs-panel.jsx
new file mode 100644
index 0000000..9887252
--- /dev/null
+++ b/pkg/lib/cockpit-components-logs-panel.jsx
@@ -0,0 +1,185 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { ExclamationTriangleIcon, TimesCircleIcon } from '@patternfly/react-icons';
+
+import { journal } from "journal";
+import "journal.css";
+import "cockpit-components-logs-panel.scss";
+
+const _ = cockpit.gettext;
+
+/* JournalOutput implements the interface expected by
+ journal.renderer, and also collects the output.
+ */
+
+export class JournalOutput {
+ constructor(search_options) {
+ this.logs = [];
+ this.reboot_key = 0;
+ this.search_options = search_options || {};
+ }
+
+ onEvent(ev, cursor, full_content) {
+ // only consider primary mouse button for clicks
+ if (ev.type === 'click') {
+ if (ev.button !== 0)
+ return;
+
+ // Ignore if text is being selected - less than 3 characters most likely means misclick
+ const selection = window.getSelection().toString();
+ if (selection && selection.length > 2 && full_content.indexOf(selection) >= 0)
+ return;
+ }
+
+ // only consider enter button for keyboard events
+ if (ev.type === 'KeyDown' && ev.key !== "Enter")
+ return;
+
+ cockpit.jump("system/logs#/" + cursor + "?parent_options=" + JSON.stringify(this.search_options));
+ }
+
+ render_line(ident, prio, message, count, time, entry) {
+ let problem = false;
+ let warning = false;
+
+ if (ident === 'abrt-notification') {
+ problem = true;
+ ident = entry.PROBLEM_BINARY;
+ } else if (prio < 4) {
+ warning = true;
+ }
+
+ const full_content = [time, message, ident].join("\n");
+
+ return (
+ <div className="cockpit-logline" role="row" tabIndex="0" key={entry.__CURSOR}
+ data-cursor={entry.__CURSOR}
+ onClick={ev => this.onEvent(ev, entry.__CURSOR, full_content)}
+ onKeyDown={ev => this.onEvent(ev, entry.__CURSOR, full_content)}>
+ <div className="cockpit-log-warning" role="cell">
+ { warning
+ ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />
+ : null
+ }
+ { problem
+ ? <TimesCircleIcon className="ct-icon-times-circle" />
+ : null
+ }
+ </div>
+ <div className="cockpit-log-time" role="cell">{time}</div>
+ <span className="cockpit-log-message" role="cell">{message}</span>
+ {
+ count > 1
+ ? <div className="cockpit-log-service-container" role="cell">
+ <div className="cockpit-log-service-reduced">{ident}</div>
+ <Badge screenReaderText={_("Occurrences")} isRead key={count}>{count}</Badge>
+ </div>
+ : <div className="cockpit-log-service" role="cell">{ident}</div>
+ }
+ </div>
+ );
+ }
+
+ render_day_header(day) {
+ return <div className="panel-heading" key={day}>{day}</div>;
+ }
+
+ render_reboot_separator() {
+ return (
+ <div className="cockpit-logline" role="row" key={"reboot-" + this.reboot_key++}>
+ <div className="cockpit-log-warning" role="cell" />
+ <span className="cockpit-log-message cockpit-logmsg-reboot" role="cell">{_("Reboot")}</span>
+ </div>
+ );
+ }
+
+ prepend(item) {
+ this.logs.unshift(item);
+ }
+
+ append(item) {
+ this.logs.push(item);
+ }
+
+ remove_first() {
+ this.logs.shift();
+ }
+
+ remove_last() {
+ this.logs.pop();
+ }
+
+ limit(max) {
+ if (this.logs.length > max)
+ this.logs = this.logs.slice(0, max);
+ }
+}
+
+export class LogsPanel extends React.Component {
+ constructor() {
+ super();
+ this.state = { logs: [] };
+ }
+
+ componentDidMount() {
+ this.journalctl = journal.journalctl(this.props.match, { count: this.props.max });
+
+ const out = new JournalOutput(this.props.search_options);
+ const render = journal.renderer(out);
+
+ this.journalctl.stream((entries) => {
+ for (let i = 0; i < entries.length; i++)
+ render.prepend(entries[i]);
+ render.prepend_flush();
+ // "max + 1" since there is always a date header and we
+ // want to show "max" entries below it.
+ out.limit(this.props.max + 1);
+ this.setState({ logs: out.logs });
+ });
+ }
+
+ componentWillUnmount() {
+ this.journalctl.stop();
+ }
+
+ render() {
+ const actions = (this.state.logs.length > 0 && this.props.goto_url) && <Button variant="secondary" onClick={e => cockpit.jump(this.props.goto_url)}>{_("View all logs")}</Button>;
+
+ return (
+ <Card className="cockpit-log-panel">
+ <CardHeader actions={{ actions }}>
+ <CardTitle>{this.props.title}</CardTitle>
+ </CardHeader>
+ <CardBody className={(!this.state.logs.length && this.props.emptyMessage.length) ? "empty-message" : "contains-list"}>
+ { this.state.logs.length ? this.state.logs : this.props.emptyMessage }
+ </CardBody>
+ </Card>
+ );
+ }
+}
+LogsPanel.defaultProps = {
+ emptyMessage: [],
+};
diff --git a/pkg/lib/cockpit-components-logs-panel.scss b/pkg/lib/cockpit-components-logs-panel.scss
new file mode 100644
index 0000000..31cb0fc
--- /dev/null
+++ b/pkg/lib/cockpit-components-logs-panel.scss
@@ -0,0 +1,15 @@
+.panel-body.empty-message {
+ padding-block: 0.5rem;
+ padding-inline: 1rem;
+ text-align: center;
+}
+
+.cockpit-log-panel .panel-body {
+ padding-inline: 0;
+}
+
+.cockpit-log-panel .pf-v5-c-card__header-main > .pf-v5-c-card__title > .pf-v5-c-card__title-text {
+ padding: 0;
+ font-weight: normal;
+ font-size: var(--pf-v5-global--FontSize--2xl);
+}
diff --git a/pkg/lib/cockpit-components-modifications.css b/pkg/lib/cockpit-components-modifications.css
new file mode 100644
index 0000000..dfdaadb
--- /dev/null
+++ b/pkg/lib/cockpit-components-modifications.css
@@ -0,0 +1,28 @@
+.automation-script-modal pre {
+ max-block-size: 20em;
+ margin-block-end: 5px;
+}
+
+.automation-script-modal span.fa {
+ margin-inline-end: 5px;
+}
+
+.automation-script-modal i.fa {
+ margin-inline: 5px 2px;
+}
+
+.automation-script-modal textarea {
+ min-block-size: 15rem;
+}
+
+.automation-script-modal .ansible-docs-link > svg {
+ padding-inline-end: var(--pf-v5-global--spacer--xs);
+}
+
+.green-icon {
+ color: var(--pf-v5-global--success-color--100);
+}
+
+.pf-v5-c-card.modifications-table .pf-v5-c-card__header {
+ justify-content: space-between;
+}
diff --git a/pkg/lib/cockpit-components-modifications.jsx b/pkg/lib/cockpit-components-modifications.jsx
new file mode 100644
index 0000000..85b67c3
--- /dev/null
+++ b/pkg/lib/cockpit-components-modifications.jsx
@@ -0,0 +1,182 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DataList, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Tab, Tabs } from "@patternfly/react-core/dist/esm/components/Tabs/index.js";
+import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
+import { CheckIcon, CopyIcon, ExternalLinkAltIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
+
+import cockpit from "cockpit";
+import 'cockpit-components-modifications.css';
+
+const _ = cockpit.gettext;
+
+/* Dialog for showing scripts to modify system
+ *
+ * Enables showing shell and ansible script. Shell one is mandatory and ansible one can be omitted.
+ *
+ */
+export const ModificationsExportDialog = ({ show, onClose, shell, ansible }) => {
+ const [active_tab, setActiveTab] = React.useState("ansible");
+ const [copied, setCopied] = React.useState(false);
+ const [timeoutId, setTimeoutId] = React.useState(null);
+
+ const handleSelect = (_event, active_tab) => {
+ setCopied(false);
+ setActiveTab(active_tab);
+ if (timeoutId !== null) {
+ clearTimeout(timeoutId);
+ setTimeoutId(null);
+ }
+ };
+
+ const copyToClipboard = () => {
+ try {
+ navigator.clipboard.writeText((active_tab === "ansible" ? ansible : shell).trim())
+ .then(() => {
+ setCopied(true);
+ setTimeoutId(setTimeout(() => {
+ setCopied(false);
+ setTimeoutId(null);
+ }, 3000));
+ })
+ .catch(e => console.error('Text could not be copied: ', e ? e.toString() : ""));
+ } catch (error) {
+ console.error('Text could not be copied: ', error.toString());
+ }
+ };
+
+ const footer = (
+ <>
+ <Button variant='secondary' className="btn-clipboard" onClick={copyToClipboard} icon={copied ? <CheckIcon className="green-icon" /> : <CopyIcon />}>
+ { _("Copy to clipboard") }
+ </Button>
+ <Button variant='secondary' className='btn-cancel' onClick={onClose}>
+ { _("Close") }
+ </Button>
+ </>
+ );
+
+ return (
+ <Modal isOpen={show} className="automation-script-modal"
+ position="top" variant="medium"
+ onClose={onClose}
+ footer={footer}
+ title={_("Automation script") }>
+ <Tabs activeKey={active_tab} onSelect={handleSelect}>
+ <Tab eventKey="ansible" title={_("Ansible")}>
+ <TextArea resizeOrientation='vertical' readOnlyVariant="default" defaultValue={ansible.trim()} />
+ <div className="ansible-docs-link">
+ <OutlinedQuestionCircleIcon />
+ { _("Create new task file with this content.") }
+ <Button variant="link" component="a" href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html"
+ target="_blank" rel="noopener noreferrer"
+ icon={<ExternalLinkAltIcon />}>
+ { _("Ansible roles documentation") }
+ </Button>
+ </div>
+ </Tab>
+ <Tab eventKey="shell" title={_("Shell script")}>
+ <TextArea resizeOrientation='vertical' readOnlyVariant="default" defaultValue={shell.trim()} />
+ </Tab>
+ </Tabs>
+ </Modal>
+ );
+};
+
+ModificationsExportDialog.propTypes = {
+ shell: PropTypes.string.isRequired,
+ ansible: PropTypes.string.isRequired,
+ show: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
+
+/* Display list of modifications in human readable format
+ *
+ * Also show `View automation script` button which opens dialog in which different
+ * scripts are available. With these scripts it is possible to apply the same
+ * configurations to other machines.
+ *
+ * Pass array `entries` to show human readable messages.
+ * Pass string `shell` and `ansible` with scripts.
+ *
+ */
+export const Modifications = ({ entries, failed, permitted, title, shell, ansible }) => {
+ const [showDialog, setShowDialog] = React.useState(false);
+
+ let emptyRow = null;
+ let fail_message = permitted ? _("No system modifications") : _("The logged in user is not permitted to view system modifications");
+ fail_message = failed || fail_message;
+ if (entries === null) {
+ emptyRow = <DataListItem>
+ <DataListItemRow>
+ <DataListItemCells dataListCells={[<DataListCell key="loading">{_("Loading system modifications...")}</DataListCell>]} />
+ </DataListItemRow>
+ </DataListItem>;
+ }
+ if (entries?.length === 0) {
+ emptyRow = <DataListItem>
+ <DataListItemRow>
+ <DataListItemCells dataListCells={[<DataListCell key={fail_message}>{fail_message}</DataListCell>]} />
+ </DataListItemRow>
+ </DataListItem>;
+ }
+
+ return (
+ <>
+ <ModificationsExportDialog show={showDialog} shell={shell} ansible={ansible} onClose={() => setShowDialog(false)} />
+ <Card className="modifications-table">
+ <CardHeader>
+ <CardTitle component="h2">{title}</CardTitle>
+ { !emptyRow &&
+ <Button variant="secondary" onClick={() => setShowDialog(true)}>
+ {_("View automation script")}
+ </Button>
+ }
+ </CardHeader>
+ <CardBody className="contains-list">
+ <DataList aria-label={title} isCompact>
+ { emptyRow ||
+ entries.map(entry => <DataListItem key={entry}>
+ <DataListItemRow>
+ <DataListItemCells dataListCells={[<DataListCell key={entry}>{entry}</DataListCell>]} />
+ </DataListItemRow>
+ </DataListItem>
+ )
+ }
+ </DataList>
+ </CardBody>
+ </Card>
+ </>
+ );
+};
+
+Modifications.propTypes = {
+ failed: PropTypes.string,
+ title: PropTypes.string.isRequired,
+ permitted: PropTypes.bool.isRequired,
+ entries: PropTypes.arrayOf(PropTypes.string),
+ shell: PropTypes.string.isRequired,
+ ansible: PropTypes.string.isRequired,
+};
diff --git a/pkg/lib/cockpit-components-password.jsx b/pkg/lib/cockpit-components-password.jsx
new file mode 100644
index 0000000..d0c8cc7
--- /dev/null
+++ b/pkg/lib/cockpit-components-password.jsx
@@ -0,0 +1,188 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import cockpit from 'cockpit';
+import React, { useState } from 'react';
+import { debounce } from 'throttle-debounce';
+import { Button } from '@patternfly/react-core/dist/esm/components/Button/index.js';
+import { FormGroup, FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { InputGroup, InputGroupItem } from '@patternfly/react-core/dist/esm/components/InputGroup/index.js';
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Progress, ProgressMeasureLocation, ProgressSize } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { EyeIcon, EyeSlashIcon, HelpIcon } from '@patternfly/react-icons';
+
+import { FormHelper } from "cockpit-components-form-helper";
+
+import './cockpit-components-password.scss';
+import { Flex, FlexItem } from '@patternfly/react-core';
+
+const _ = cockpit.gettext;
+
+export function password_quality(password, force) {
+ return new Promise((resolve, reject) => {
+ cockpit.spawn('/usr/bin/pwscore', { err: "message" })
+ .input(password)
+ .done(function(content) {
+ const quality = parseInt(content, 10);
+ if (quality === 0)
+ reject(new Error(_("Password is too weak")));
+ else
+ resolve({ value: quality, message: quality === 100 ? _("Excellent password") : undefined });
+ })
+ .fail(function(ex) {
+ if (!force)
+ reject(new Error(ex.message || _("Password is not acceptable")));
+ else
+ resolve({ value: 0 });
+ });
+ });
+}
+
+const debounced_password_quality = debounce(300, (value, callback) => {
+ password_quality(value).catch(() => ({ value: 0 })).then(callback);
+});
+
+export const PasswordFormFields = ({
+ password_label, password_confirm_label,
+ password_label_info,
+ initial_password,
+ error_password, error_password_confirm,
+ idPrefix, change
+}) => {
+ const [password, setPassword] = useState(initial_password || "");
+ const [passwordConfirm, setConfirmPassword] = useState("");
+ const [passwordStrength, setPasswordStrength] = useState();
+ const [passwordMessage, setPasswordMessage] = useState("");
+ const [passwordHidden, setPasswordHidden] = useState(true);
+ const [passwordConfirmHidden, setPasswordConfirmHidden] = useState(true);
+
+ function onPasswordChanged(value) {
+ setPassword(value);
+ change("password", value);
+
+ if (value) {
+ debounced_password_quality(value, strength => {
+ setPasswordStrength(strength.value);
+ setPasswordMessage(strength.message);
+ });
+ } else {
+ setPasswordStrength();
+ setPasswordMessage("");
+ }
+ }
+
+ let variant;
+ let message;
+ let messageColor;
+ if (passwordStrength > 66) {
+ variant = "success";
+ messageColor = "pf-v5-u-success-color-200";
+ message = _("Strong password");
+ } else if (passwordStrength > 33) {
+ variant = "warning";
+ messageColor = "pf-v5-u-warning-color-200";
+ message = _("Acceptable password");
+ } else {
+ variant = "danger";
+ messageColor = "pf-v5-u-danger-color-200";
+ message = _("Weak password");
+ }
+
+ if (!passwordMessage && message)
+ setPasswordMessage(message);
+
+ let passwordStrengthValue = Number.isInteger(passwordStrength) ? Number.parseInt(passwordStrength) : -1;
+ if (password !== "" && (passwordStrengthValue >= 0 && passwordStrengthValue < 25))
+ passwordStrengthValue = 25;
+
+ return (
+ <>
+ <FormGroup label={password_label}
+ labelIcon={password_label_info &&
+ <Popover bodyContent={password_label_info}>
+ <button onClick={e => e.preventDefault()}
+ className="pf-v5-c-form__group-label-help">
+ <HelpIcon />
+ </button>
+ </Popover>
+ }
+ validated={error_password ? "warning" : "default"}
+ id={idPrefix + "-pw1-group"}
+ fieldId={idPrefix + "-pw1"}>
+ <InputGroup>
+ <InputGroupItem isFill>
+ <TextInput className="check-passwords" type={passwordHidden ? "password" : "text"} id={idPrefix + "-pw1"}
+ autoComplete="new-password" value={password} onChange={(_event, value) => onPasswordChanged(value)}
+ validated={error_password ? "warning" : "default"} />
+ </InputGroupItem>
+ <InputGroupItem>
+ <Button
+ variant="control"
+ onClick={() => setPasswordHidden(!passwordHidden)}
+ aria-label={passwordHidden ? _("Show password") : _("Hide password")}>
+ {passwordHidden ? <EyeIcon /> : <EyeSlashIcon />}
+ </Button>
+ </InputGroupItem>
+ </InputGroup>
+ {passwordStrengthValue >= 0 && <Flex spaceItems={{ default: 'spaceItemsSm' }}>
+ <FlexItem>
+ <Progress id={idPrefix + "-meter"}
+ className={"pf-v5-u-pt-xs ct-password-strength-meter " + variant}
+ title={_("password quality")}
+ size={ProgressSize.sm}
+ measureLocation={ProgressMeasureLocation.none}
+ variant={variant}
+ value={passwordStrengthValue} />
+ </FlexItem>
+ <FlexItem>
+ <div id={idPrefix + "-password-meter-message"} className={"pf-v5-c-form__helper-text " + messageColor} aria-live="polite">{passwordMessage}</div>
+ </FlexItem>
+ </Flex>}
+ {error_password && <FormHelperText>
+ <HelperText component="ul" aria-live="polite" id="password-error-message">
+ <HelperTextItem isDynamic variant="warning" component="li">
+ {error_password}
+ </HelperTextItem>
+ </HelperText>
+ </FormHelperText>}
+ </FormGroup>
+
+ {password_confirm_label && <FormGroup label={password_confirm_label}
+ id={idPrefix + "-pw2-group"}
+ fieldId={idPrefix + "-pw2"}>
+ <InputGroup>
+ <InputGroupItem isFill>
+ <TextInput type={passwordConfirmHidden ? "password" : "text"} id={idPrefix + "-pw2"} autoComplete="new-password"
+ value={passwordConfirm} onChange={(_event, value) => { setConfirmPassword(value); change("password_confirm", value) }} />
+ </InputGroupItem>
+ <InputGroupItem>
+ <Button
+ variant="control"
+ onClick={() => setPasswordConfirmHidden(!passwordConfirmHidden)}
+ aria-label={passwordConfirmHidden ? _("Show confirmation password") : _("Hide confirmation password")}>
+ {passwordConfirmHidden ? <EyeIcon /> : <EyeSlashIcon />}
+ </Button>
+ </InputGroupItem>
+ </InputGroup>
+ <FormHelper fieldId={idPrefix + "-pw2"} helperTextInvalid={error_password_confirm} />
+ </FormGroup>}
+ </>
+ );
+};
diff --git a/pkg/lib/cockpit-components-password.scss b/pkg/lib/cockpit-components-password.scss
new file mode 100644
index 0000000..e1d2d44
--- /dev/null
+++ b/pkg/lib/cockpit-components-password.scss
@@ -0,0 +1,11 @@
+@import "global-variables";
+@import "@patternfly/patternfly/utilities/Text/text.scss";
+
+.ct-password-strength-meter {
+ grid-gap: var(--pf-v5-global--spacer--xs);
+ inline-size: var(--pf-v5-global--spacer--2xl);
+
+ .pf-v5-c-progress__description, .pf-v5-c-progress__status {
+ display: none;
+ }
+}
diff --git a/pkg/lib/cockpit-components-plot.jsx b/pkg/lib/cockpit-components-plot.jsx
new file mode 100644
index 0000000..527f513
--- /dev/null
+++ b/pkg/lib/cockpit-components-plot.jsx
@@ -0,0 +1,513 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import React, { useState, useRef, useLayoutEffect } from 'react';
+import { useEvent } from "hooks.js";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Dropdown, DropdownItem, DropdownSeparator, DropdownToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+
+import { AngleLeftIcon, AngleRightIcon, SearchMinusIcon } from '@patternfly/react-icons';
+
+import * as timeformat from "timeformat";
+import '@patternfly/patternfly/patternfly-charts.scss';
+import "cockpit-components-plot.scss";
+
+const _ = cockpit.gettext;
+
+function time_ticks(data) {
+ const first_plot = data[0].data;
+ const start_ms = first_plot[0][0];
+ const end_ms = first_plot[first_plot.length - 1][0];
+
+ // Determine size between ticks
+
+ const sizes_in_seconds = [
+ 60, // minute
+ 5 * 60, // 5 minutes
+ 10 * 60, // 10 minutes
+ 30 * 60, // half hour
+ 60 * 60, // hour
+ 6 * 60 * 60, // quarter day
+ 12 * 60 * 60, // half day
+ 24 * 60 * 60, // day
+ 7 * 24 * 60 * 60, // week
+ 30 * 24 * 60 * 60, // month
+ 183 * 24 * 60 * 60, // half a year
+ 365 * 24 * 60 * 60, // year
+ 10 * 365 * 24 * 60 * 60 // 10 years
+ ];
+
+ let size;
+ for (let i = 0; i < sizes_in_seconds.length; i++) {
+ if (((end_ms - start_ms) / 1000) / sizes_in_seconds[i] < 10 || i == sizes_in_seconds.length - 1) {
+ size = sizes_in_seconds[i] * 1000;
+ break;
+ }
+ }
+
+ // Determine what to omit from the tick label. If it's all in the
+ // current year, we don't need to include the year; and if it's
+ // all happening today, we don't include the month and date.
+
+ const now_date = new Date();
+ const start_date = new Date(start_ms);
+
+ let include_year = true;
+ let include_month_and_day = true;
+
+ if (start_date.getFullYear() == now_date.getFullYear()) {
+ include_year = false;
+ if (start_date.getMonth() == now_date.getMonth() && start_date.getDate() == now_date.getDate())
+ include_month_and_day = false;
+ }
+
+ // Compute the actual ticks
+
+ const ticks = [];
+ let t = Math.ceil(start_ms / size) * size;
+ while (t < end_ms) {
+ ticks.push(t);
+ t += size;
+ }
+
+ // Render the label
+
+ function pad(n) {
+ let str = n.toFixed();
+ if (str.length == 1)
+ str = '0' + str;
+ return str;
+ }
+
+ function format_tick(val, index, ticks) {
+ const d = new Date(val);
+ let label = ' ';
+
+ if (include_month_and_day) {
+ if (include_year)
+ label += timeformat.date(d) + '\n';
+ else
+ label += timeformat.formatter({ month: "long" }).format(d) + ' ' + d.getDate().toFixed() + '\n';
+ }
+ label += pad(d.getHours()) + ':' + pad(d.getMinutes());
+
+ return label;
+ }
+
+ return {
+ ticks,
+ formatter: format_tick,
+ start: start_ms,
+ end: end_ms
+ };
+}
+
+function value_ticks(data, config) {
+ let max = config.min_max;
+ const last_plot = data[data.length - 1].data;
+ for (let i = 0; i < last_plot.length; i++) {
+ const s = last_plot[i][1] || last_plot[i][2];
+ if (s > max)
+ max = s;
+ }
+
+ // Find the highest power of the base unit that is still below
+ // MAX.
+ //
+ // For example, if the base unit is 1000 and MAX is 402,345,765
+ // this will set UNIT to 1,000,000, aka "Mega".
+ //
+ let unit = 1;
+ while (config.base_unit && max > unit * config.base_unit)
+ unit *= config.base_unit;
+
+ // Find the highest power of 10 that is below the maximum number
+ // on a tick label. If we use that as the distance between ticks,
+ // we get at most 10 ticks.
+ //
+ // To continue the example, MAX is 402,345,765 and UNIT is thus
+ // 1,000,000. The highest number on a tick label would be MAX /
+ // UNIT = 402ish. The highest power of 10 below that is 100. Thus
+ // the size between ticks is 100*UNIT = 100,000,000. Ticks would
+ // thus be "100 Mega" apart.
+ //
+ // If the highest number of would be only, say, 81, then we would get
+ // a highest power of 10, and ticks would be 10 units apart.
+ //
+ let size = Math.pow(10, Math.floor(Math.log10(max / unit))) * unit;
+
+ // Get the number of ticks to be around 4, but don't produce
+ // fractional numbers. This is done by doubling or halving the
+ // size between ticks until we get MAX / SIZE to be less than 8 or
+ // greater than 2.
+ //
+ // In the example, MAX / SIZE is already in range, so nothing
+ // changes here.
+ //
+ // If MAX / UNIT is close to the next power of ten, such as 999, we
+ // would end up with a doubled SIZE of 200,000,000.
+ //
+ // If on the other hand MAX / UNIT would be closer to the next
+ // lower power of 10, like say 110, then we would half the SIZE to
+ // get moreticks. With 110, it will happen twice and SIZE ends up
+ // being 25,000,000.
+ //
+ // However, if we only have single digit tick labels, we don't
+ // want to halve them any further, since we don't want tick labels
+ // like "0.75".
+ //
+ while (max / size > 7)
+ size *= 2;
+ while (max / size < 3 && size / unit >= 10)
+ size /= 2;
+
+ // Make a list of tick values, each SIZE apart until we are just
+ // above MAX.
+ //
+ // In the example, we get
+ //
+ // [ 0, 100000000, 200000000, 300000000, 400000000, 500000000 ]
+ //
+ const ticks = [];
+ for (let t = 0; t < max + size; t += size)
+ ticks.push(t);
+
+ if (config.pull_out_unit) {
+ const unit_str = config.formatter(unit, config.base_unit, true)[1];
+
+ return {
+ ticks,
+ formatter: (val) => config.formatter(val, unit_str, true)[0],
+ unit: unit_str,
+ max: ticks[ticks.length - 1]
+ };
+ } else {
+ return {
+ ticks,
+ formatter: config.formatter,
+ max: ticks[ticks.length - 1]
+ };
+ }
+}
+
+export const ZoomControls = ({ plot_state }) => {
+ function format_range(seconds) {
+ let n;
+ if (seconds >= 365 * 24 * 60 * 60) {
+ n = Math.ceil(seconds / (365 * 24 * 60 * 60));
+ return cockpit.format(cockpit.ngettext("$0 year", "$0 years", n), n);
+ } else if (seconds >= 30 * 24 * 60 * 60) {
+ n = Math.ceil(seconds / (30 * 24 * 60 * 60));
+ return cockpit.format(cockpit.ngettext("$0 month", "$0 months", n), n);
+ } else if (seconds >= 7 * 24 * 60 * 60) {
+ n = Math.ceil(seconds / (7 * 24 * 60 * 60));
+ return cockpit.format(cockpit.ngettext("$0 week", "$0 weeks", n), n);
+ } else if (seconds >= 24 * 60 * 60) {
+ n = Math.ceil(seconds / (24 * 60 * 60));
+ return cockpit.format(cockpit.ngettext("$0 day", "$0 days", n), n);
+ } else if (seconds >= 60 * 60) {
+ n = Math.ceil(seconds / (60 * 60));
+ return cockpit.format(cockpit.ngettext("$0 hour", "$0 hours", n), n);
+ } else {
+ n = Math.ceil(seconds / 60);
+ return cockpit.format(cockpit.ngettext("$0 minute", "$0 minutes", n), n);
+ }
+ }
+
+ const zoom_state = plot_state.zoom_state;
+
+ const [isOpen, setIsOpen] = useState(false);
+ useEvent(plot_state, "changed");
+ useEvent(zoom_state, "changed");
+
+ function range_item(seconds, title) {
+ return (
+ <DropdownItem key={title}
+ onClick={() => {
+ setIsOpen(false);
+ zoom_state.set_range(seconds);
+ }}>
+ {title}
+ </DropdownItem>
+ );
+ }
+
+ if (!zoom_state)
+ return null;
+
+ return (
+ <div>
+ <Dropdown
+ isOpen={isOpen}
+ toggle={<DropdownToggle onToggle={(_, isOpen) => setIsOpen(isOpen)}>{format_range(zoom_state.x_range)}</DropdownToggle>}
+ dropdownItems={[
+ <DropdownItem key="now" onClick={() => { zoom_state.goto_now(); setIsOpen(false) }}>
+ {_("Go to now")}
+ </DropdownItem>,
+ <DropdownSeparator key="sep" />,
+ range_item(5 * 60, _("5 minutes")),
+ range_item(60 * 60, _("1 hour")),
+ range_item(6 * 60 * 60, _("6 hours")),
+ range_item(24 * 60 * 60, _("1 day")),
+ range_item(7 * 24 * 60 * 60, _("1 week"))
+ ]} />
+ { "\n" }
+ <Button variant="secondary" onClick={() => zoom_state.zoom_out()}
+ isDisabled={!zoom_state.enable_zoom_out}>
+ <SearchMinusIcon />
+ </Button>
+ { "\n" }
+ <Button variant="secondary" onClick={() => zoom_state.scroll_left()}
+ isDisabled={!zoom_state.enable_scroll_left}>
+ <AngleLeftIcon />
+ </Button>
+ <Button variant="secondary" onClick={() => zoom_state.scroll_right()}
+ isDisabled={!zoom_state.enable_scroll_right}>
+ <AngleRightIcon />
+ </Button>
+ </div>
+ );
+};
+
+const useLayoutSize = (init_width, init_height) => {
+ const ref = useRef(null);
+ const [size, setSize] = useState({ width: init_width, height: init_height });
+ /* eslint-disable react-hooks/exhaustive-deps */
+ useLayoutEffect(() => {
+ if (ref.current) {
+ const rect = ref.current.getBoundingClientRect();
+ if (rect.width != size.width || rect.height != size.height)
+ setSize({ width: rect.width, height: rect.height });
+ }
+ });
+ /* eslint-enable */
+ return [ref, size];
+};
+
+export const SvgPlot = ({ title, config, plot_state, plot_id, className }) => {
+ const [container_ref, container_size] = useLayoutSize(0, 0);
+ const [measure_ref, measure_size] = useLayoutSize(36, 20);
+
+ useEvent(plot_state, "plot:" + plot_id);
+ useEvent(plot_state, "changed");
+ useEvent(window, "resize");
+
+ const [selection, setSelection] = useState(null);
+
+ const chart_data = plot_state.data(plot_id);
+ if (!chart_data || chart_data.length == 0)
+ return null;
+
+ const t_ticks = time_ticks(chart_data);
+ const y_ticks = value_ticks(chart_data, config);
+
+ function make_chart() {
+ const w = container_size.width;
+ const h = container_size.height;
+
+ if (w == 0 || h == 0)
+ return null;
+
+ const x_off = t_ticks.start;
+ const x_range = (t_ticks.end - t_ticks.start);
+ const y_range = y_ticks.max;
+
+ const tick_length = 5;
+ const tick_gap = 3;
+
+ // widest string plus gap plus tick
+ const m_left = Math.ceil(measure_size.width) + tick_gap + tick_length;
+
+ // half of the time label so that it pops in fully formed at the far right edge
+ const m_right = 30;
+
+ // half a line for the top-half of the top-most y-axis label
+ // plus one extra line if there is a unit or a title.
+ const m_top = (y_ticks.unit || title ? 1.5 : 0.5) * Math.ceil(measure_size.height);
+
+ // x-axis labels can be up to two lines
+ const m_bottom = tick_length + tick_gap + 2 * Math.ceil(measure_size.height);
+
+ function x_coord(x) {
+ return (x - x_off) / x_range * (w - m_left - m_right) + m_left;
+ }
+
+ function x_value(c) {
+ return (c - m_left) / (w - m_left - m_right) * x_range + x_off;
+ }
+
+ function y_coord(y) {
+ return h - Math.max(y, 0) / y_range * (h - m_top - m_bottom) - m_bottom;
+ }
+
+ function cmd(op, x, y) {
+ return op + x.toFixed() + "," + y.toFixed() + " ";
+ }
+
+ function path(data, hover_arg) {
+ let d = cmd("M", m_left, h - m_bottom);
+ for (let i = 0; i < data.length; i++) {
+ d += cmd("L", x_coord(data[i][0]), y_coord(data[i][1]));
+ }
+ d += cmd("L", w - m_right, h - m_bottom);
+ d += "z";
+
+ return (
+ <path key={hover_arg} d={d}
+ role="presentation">
+ <title>{hover_arg}</title>
+ </path>
+ );
+ }
+
+ const paths = [];
+ for (let i = chart_data.length - 1; i >= 0; i--)
+ paths.push(path(chart_data[i].data, chart_data[i].name || true));
+
+ function start_dragging(event) {
+ if (event.button !== 0)
+ return;
+
+ const bounds = container_ref.current.getBoundingClientRect();
+ const x = event.clientX - bounds.x;
+ if (x >= m_left && x < w - m_right)
+ setSelection({ start: x, stop: x, left: x, right: x });
+ }
+
+ function drag(event) {
+ const bounds = container_ref.current.getBoundingClientRect();
+ let x = event.clientX - bounds.x;
+ if (x < m_left) x = m_left;
+ if (x > w - m_right) x = w - m_right;
+ setSelection({
+ start: selection.start,
+ stop: x,
+ left: Math.min(selection.start, x),
+ right: Math.max(selection.start, x)
+ });
+ }
+
+ function stop_dragging() {
+ const left = x_value(selection.left) / 1000;
+ const right = x_value(selection.right) / 1000;
+ plot_state.zoom_state.zoom_in(right - left, right);
+ setSelection(null);
+ }
+
+ function cancel_dragging() {
+ setSelection(null);
+ }
+
+ // This is a thin transparent rectangle placed at the x-axis,
+ // on top of all the graphs. It prevents bogus hover events
+ // for parts of the graph that are zero or very very close to
+ // it.
+ const hover_guard =
+ <rect x={0} y={h - m_bottom - 1} width={w} height={5} fill="transparent" />;
+
+ return (
+ <svg width={w} height={h}
+ className="ct-plot"
+ aria-label={title}
+ // TODO: Figure out a way to handle a11y without entirely hiding the live-updating graphs
+ aria-hidden="true"
+ role="img"
+ onMouseDown={plot_state.zoom_state?.enable_zoom_in ? start_dragging : null}
+ onMouseUp={selection ? stop_dragging : null}
+ onMouseMove={selection ? drag : null}
+ onMouseLeave={cancel_dragging}>
+ <title>{title}</title>
+ <text x={0} y={-20} className="ct-plot-widest" ref={measure_ref} aria-hidden="true">{config.widest_string}</text>
+ <rect x={m_left} y={m_top} width={w - m_left - m_right} height={h - m_top - m_bottom}
+ className="ct-plot-border" />
+ { y_ticks.unit && <text x={m_left - tick_length - tick_gap} y={0.5 * m_top}
+ className="ct-plot-unit"
+ textAnchor="end">
+ {y_ticks.unit}
+ </text>
+ }
+ { title && <text x={m_left} y={0.5 * m_top} className="ct-plot-title">
+ {title}
+ </text>
+ }
+ <g className="ct-plot-lines" role="presentation">
+ { y_ticks.ticks.map((t, i) => <line key={i}
+ x1={m_left - tick_length} x2={w - m_right}
+ y1={y_coord(t)} y2={y_coord(t)} />) }
+ </g>
+ <g className="ct-plot-ticks" role="presentation">
+ { t_ticks.ticks.map((t, i) => <line key={i}
+ x1={x_coord(t)} x2={x_coord(t)}
+ y1={h - m_bottom} y2={h - m_bottom + tick_length} />) }
+ </g>
+ <g className="ct-plot-paths">
+ { paths }
+ </g>
+ { hover_guard }
+ <g className="ct-plot-axis ct-plot-axis-y" textAnchor="end">
+ { y_ticks.ticks.map((t, i) => <text key={i} x={m_left - tick_length - tick_gap} y={y_coord(t) + 5}>
+ {y_ticks.formatter(t)}
+ </text>) }
+ </g>
+ <g className="ct-plot-axis ct-plot-axis-x" textAnchor="middle">
+ { t_ticks.ticks.map((t, i) => <text key={i} y={h - m_bottom + tick_length + tick_gap}>
+ { t_ticks.formatter(t).split("\n")
+ .map((s, j) =>
+ <tspan key={i + "." + j} x={x_coord(t)} dy="1.2em">{s}</tspan>) }
+ </text>) }
+ </g>
+ { selection &&
+ <rect x={selection.left} y={m_top} width={selection.right - selection.left} height={h - m_top - m_bottom}
+ className="ct-plot-selection" /> }
+ </svg>
+ );
+ }
+
+ return (
+ <div className={className} ref={container_ref}>
+ {make_chart()}
+ </div>);
+};
+
+export const bytes_config = {
+ base_unit: 1024,
+ min_max: 10240,
+ pull_out_unit: true,
+ widest_string: "MiB",
+ formatter: cockpit.format_bytes
+};
+
+export const bytes_per_sec_config = {
+ base_unit: 1024,
+ min_max: 10240,
+ pull_out_unit: true,
+ widest_string: "MiB/s",
+ formatter: cockpit.format_bytes_per_sec
+};
+
+export const bits_per_sec_config = {
+ base_unit: 1000,
+ min_max: 10000,
+ pull_out_unit: true,
+ widest_string: "Mbps",
+ formatter: cockpit.format_bits_per_sec
+};
diff --git a/pkg/lib/cockpit-components-plot.scss b/pkg/lib/cockpit-components-plot.scss
new file mode 100644
index 0000000..d4faefa
--- /dev/null
+++ b/pkg/lib/cockpit-components-plot.scss
@@ -0,0 +1,119 @@
+// Selected set of PF chart colors to optimize for full-spectrum and colorblindness
+// using unique part of PF name and a hex color fallback
+$plotColors: (
+ blue-300 #06c,
+ green-100 #bde2b9,
+ cyan-200 #73c5c5,
+ purple-100 #b2b0ea,
+ gold-300 #f4c145,
+ orange-300 #ec7a08,
+ red-200 #a30000,
+ cyan-300 #009596,
+ black-500 #4d5258
+);
+
+.ct-plot {
+ font-family: var(--pf-v5-chart-global--FontFamily);
+
+ &-border {
+ stroke: var(--pf-v5-chart-global--Fill--Color--300);
+ fill: transparent;
+ shape-rendering: crispedges;
+ }
+
+ &-title {
+ font-size: calc(var(--pf-v5-chart-global--FontSize--md) * 1px);
+ }
+
+ // Placeholder string to stretch the column, set offscreen
+ &-widest {
+ fill: transparent;
+ }
+
+ &-axis,
+ &-unit {
+ font-size: calc(var(--pf-v5-chart-global--FontSize--xs) * 1px);
+ fill: var(--pf-v5-chart-global--Fill--Color--700);
+ letter-spacing: var(--pf-v5-chart-global--letter-spacing);
+ }
+
+ .pf-v5-theme-dark &-axis,
+ .pf-v5-theme-dark &-unit {
+ fill: var(--pf-v5-chart-global--Fill--Color--400);
+ }
+
+ &-lines,
+ &-ticks {
+ stroke: var(--pf-v5-chart-global--Fill--Color--300);
+ shape-rendering: crispedges;
+ }
+
+ &-selection {
+ fill: tan;
+ stroke: black;
+ opacity: 0.5;
+ shape-rendering: crispedges;
+ }
+
+ &-paths {
+ stroke-width: var(--pf-v5-chart-global--stroke--Width--sm);
+ shape-rendering: geometricprecision;
+
+ > path {
+ fill: var(--ct-plot-path-color);
+ stroke: var(--ct-plot-path-color);
+ }
+ }
+}
+
+.ct-plot-title {
+ fill: var(--pf-v5-global--Color--100);
+}
+
+$plotColorCurrent: 0;
+$plotColorTotal: 0;
+
+// Count up total number of plot colors
+@each $plotColor in $plotColors { $plotColorTotal: $plotColorTotal + 1; }
+
+// Iterate through colors and set each graph area to a color
+@each $plotColor, $plotColorBackup in $plotColors {
+ $plotColorCurrent: $plotColorCurrent + 1;
+ .ct-plot-paths > path:nth-last-child(#{$plotColorTotal}n + #{$plotColorCurrent}) {
+ --ct-plot-path-color: var(--pf-v5-chart-color-#{$plotColor}, #{$plotColorBackup});
+ }
+}
+
+// Make plot colors available to the entire page
+:root {
+ --ct-plot-color-total: #{$plotColorTotal};
+
+ @for $i from 1 through $plotColorTotal {
+ --ct-plot-color-#{i}: #{$plotColors[$i]};
+ }
+}
+
+[dir="rtl"] {
+ // Mirror the entire graph (placement & animation)
+ .ct-plot {
+ transform: scaleX(-1);
+
+ // Flip the text back (so it's not a mirror image)
+ text {
+ transform: scaleX(-1);
+ transform-origin: 50%;
+ transform-box: fill-box;
+ }
+
+ // Set the anchor point for y-axis and units
+ .ct-plot-axis-y text,
+ .ct-plot-unit {
+ transform-origin: 0%;
+ }
+
+ // Set the anchor point for the title
+ .ct-plot-title {
+ transform-origin: 100%;
+ }
+ }
+}
diff --git a/pkg/lib/cockpit-components-privileged.jsx b/pkg/lib/cockpit-components-privileged.jsx
new file mode 100644
index 0000000..89ce2ec
--- /dev/null
+++ b/pkg/lib/cockpit-components-privileged.jsx
@@ -0,0 +1,77 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+
+import cockpit from "cockpit";
+import { superuser } from 'superuser';
+import { useEvent } from "hooks";
+
+/**
+ * UI element wrapper for something that requires privilege. When access is not
+ * allowed, then wrap the element into a Tooltip.
+ *
+ * Note that the wrapped element itself needs to be disabled explicitly, this
+ * wrapper cannot do this (unfortunately wrapping it into a disabled span does
+ * not inherit).
+ */
+export function Privileged({ excuse, allowed, placement, tooltipId, children }) {
+ // wrap into extra <span> so that a disabled child keeps the tooltip working
+ let contents = <span id={allowed ? null : tooltipId}>{ children }</span>;
+ if (!allowed) {
+ contents = (
+ <Tooltip position={ placement || TooltipPosition.top} id={ tooltipId + "_tooltip" }
+ content={ excuse }>
+ { contents }
+ </Tooltip>);
+ }
+ return contents;
+}
+
+/**
+ * Convenience element for a Privilege wrapped Button
+ */
+export const PrivilegedButton = ({ tooltipId, placement, excuse, buttonId, onClick, ariaLabel, variant, isDanger, children }) => {
+ const [user, setUser] = useState(null);
+ useEvent(superuser, "changed");
+ useEffect(() => cockpit.user().then(user => setUser(user)));
+
+ return (
+ <Privileged allowed={ superuser.allowed } tooltipId={ tooltipId } placement={ placement }
+ excuse={ cockpit.format(excuse, user?.name ?? '') }>
+ <Button id={ buttonId } variant={ variant } isDanger={ isDanger } onClick={ onClick }
+ isInline isDisabled={ !superuser.allowed } aria-label={ ariaLabel }>
+ { children }
+ </Button>
+ </Privileged>
+ );
+};
+
+PrivilegedButton.propTypes = {
+ excuse: PropTypes.string.isRequired, // must contain a $0, replaced with user name
+ onClick: PropTypes.func,
+ variant: PropTypes.string,
+ placement: PropTypes.string, // default: top
+ buttonId: PropTypes.string,
+ tooltipId: PropTypes.string,
+ ariaLabel: PropTypes.string,
+};
diff --git a/pkg/lib/cockpit-components-shutdown.jsx b/pkg/lib/cockpit-components-shutdown.jsx
new file mode 100644
index 0000000..ee2ec7d
--- /dev/null
+++ b/pkg/lib/cockpit-components-shutdown.jsx
@@ -0,0 +1,248 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Divider } from "@patternfly/react-core/dist/esm/components/Divider/index.js";
+import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
+import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
+import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
+
+import { ServerTime } from 'serverTime.js';
+import * as timeformat from "timeformat.js";
+import { DialogsContext } from "dialogs.jsx";
+import { FormHelper } from "cockpit-components-form-helper";
+
+import "cockpit-components-shutdown.scss";
+
+const _ = cockpit.gettext;
+
+export class ShutdownModal extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor(props) {
+ super(props);
+ this.date_spawn = null;
+ this.state = {
+ error: "",
+ dateError: "",
+ message: "",
+ isOpen: false,
+ selected: "1",
+ dateObject: undefined,
+ startDate: undefined,
+ date: "",
+ time: "",
+ when: "+1",
+ formFilled: false,
+ };
+ this.onSubmit = this.onSubmit.bind(this);
+ this.updateDate = this.updateDate.bind(this);
+ this.updateTime = this.updateTime.bind(this);
+ this.calculate = this.calculate.bind(this);
+ this.dateRangeValidator = this.dateRangeValidator.bind(this);
+
+ this.server_time = new ServerTime();
+ }
+
+ componentDidMount() {
+ this.server_time.wait()
+ .then(() => {
+ const dateObject = this.server_time.utc_fake_now;
+ const date = timeformat.dateShort(dateObject);
+ const hour = this.server_time.utc_fake_now.getUTCHours();
+ const minute = this.server_time.utc_fake_now.getUTCMinutes();
+ this.setState({
+ dateObject,
+ date,
+ startDate: new Date(dateObject.toDateString()),
+ time: hour.toString().padStart(2, "0") + ":" + minute.toString().padStart(2, "0"),
+ });
+ })
+ .always(() => this.setState({ formFilled: true }));
+ }
+
+ updateDate(value, dateObject) {
+ this.setState({ date: value, dateObject }, this.calculate);
+ }
+
+ updateTime(value, hour, minute) {
+ this.setState({ time: value, hour, minute }, this.calculate);
+ }
+
+ calculate() {
+ if (this.date_spawn)
+ this.date_spawn.close("cancelled");
+
+ if (this.state.selected != "x") {
+ this.setState(prevState => ({
+ when: "+" + prevState.selected,
+ error: "",
+ dateError: "",
+ }));
+ return;
+ }
+
+ const time_error = this.state.hour === null || this.state.minute === null;
+ const date_error = !this.state.dateObject;
+
+ if (time_error && date_error) {
+ this.setState({ dateError: _("Invalid date format and invalid time format") });
+ return;
+ } else if (time_error) {
+ this.setState({ dateError: _("Invalid time format") });
+ return;
+ } else if (date_error) {
+ this.setState({ dateError: _("Invalid date format") });
+ return;
+ }
+
+ const cmd = ["date", "--date=" + (new Intl.DateTimeFormat('en-us').format(this.state.dateObject)) + " " + this.state.time, "+%s"];
+ this.date_spawn = cockpit.spawn(cmd, { err: "message" });
+ this.date_spawn.then(data => {
+ const input_timestamp = parseInt(data, 10);
+ const server_timestamp = parseInt(this.server_time.now.getTime() / 1000, 10);
+ let offset = Math.ceil((input_timestamp - server_timestamp) / 60);
+
+ /* If the time in minutes just changed, make it happen now */
+ if (offset === -1) {
+ offset = 0;
+ } else if (offset < 0) { // Otherwise it is a failure
+ this.setState({ dateError: _("Cannot schedule event in the past") });
+ return;
+ }
+
+ this.setState({
+ when: "+" + offset,
+ error: "",
+ dateError: "",
+ });
+ });
+ this.date_spawn.catch(e => {
+ if (e.problem == "cancelled")
+ return;
+ this.setState({ error: e.message });
+ });
+ this.date_spawn.finally(() => { this.date_spawn = null });
+ }
+
+ onSubmit(event) {
+ const Dialogs = this.context;
+ const arg = this.props.shutdown ? "--poweroff" : "--reboot";
+ if (!this.props.shutdown)
+ cockpit.hint("restart");
+
+ cockpit.spawn(["shutdown", arg, this.state.when, this.state.message], { superuser: true, err: "message" })
+ .then(this.props.onClose || Dialogs.close)
+ .catch(e => this.setState({ error: e }));
+
+ event.preventDefault();
+ return false;
+ }
+
+ dateRangeValidator(date) {
+ if (this.state.startDate && date < this.state.startDate) {
+ return _("Cannot schedule event in the past");
+ }
+ return '';
+ }
+
+ render() {
+ const Dialogs = this.context;
+ const options = [
+ <SelectOption value="0" key="0">{_("No delay")}</SelectOption>,
+ <Divider key="divider" component="li" />,
+ <SelectOption value="1" key="1">{_("1 minute")}</SelectOption>,
+ <SelectOption value="5" key="5">{_("5 minutes")}</SelectOption>,
+ <SelectOption value="20" key="20">{_("20 minutes")}</SelectOption>,
+ <SelectOption value="40" key="40">{_("40 minutes")}</SelectOption>,
+ <SelectOption value="60" key="60">{_("60 minutes")}</SelectOption>,
+ <Divider key="divider-2" component="li" />,
+ <SelectOption value="x" key="x">{_("Specific time")}</SelectOption>
+ ];
+
+ return (
+ <Modal isOpen position="top" variant="medium"
+ onClose={this.props.onClose || Dialogs.close}
+ id="shutdown-dialog"
+ title={this.props.shutdown ? _("Shut down") : _("Reboot")}
+ footer={<>
+ <Button variant='danger' isDisabled={this.state.error || this.state.dateError} onClick={this.onSubmit}>{this.props.shutdown ? _("Shut down") : _("Reboot")}</Button>
+ <Button variant='link' onClick={this.props.onClose || Dialogs.close}>{_("Cancel")}</Button>
+ </>}
+ >
+ <>
+ <Form isHorizontal onSubmit={this.onSubmit}>
+ <FormGroup fieldId="message" label={_("Message to logged in users")}>
+ <TextArea id="message" resizeOrientation="vertical" value={this.state.message} onChange={(_, v) => this.setState({ message: v })} />
+ </FormGroup>
+ <FormGroup fieldId="delay" label={_("Delay")}>
+ <Flex className="shutdown-delay-group" alignItems={{ default: 'alignItemsCenter' }}>
+ <Select toggleId="delay" isOpen={this.state.isOpen} selections={this.state.selected}
+ isDisabled={!this.state.formFilled}
+ className='shutdown-select-delay'
+ onToggle={(_event, o) => this.setState({ isOpen: o })} menuAppendTo="parent"
+ onSelect={(e, s) => this.setState({ selected: s, isOpen: false }, this.calculate)}>
+ {options}
+ </Select>
+ {this.state.selected === "x" && <>
+ <DatePicker aria-label={_("Pick date")}
+ buttonAriaLabel={_("Toggle date picker")}
+ className='shutdown-date-picker'
+ dateFormat={timeformat.dateShort}
+ // https://github.com/patternfly/patternfly-react/issues/9721
+ dateParse={date => {
+ const newDate = timeformat.parseShortDate(date);
+ return Number.isNaN(newDate.valueOf()) ? false : newDate;
+ }}
+ invalidFormatText=""
+ isDisabled={!this.state.formFilled}
+ locale={timeformat.dateFormatLang()}
+ weekStart={timeformat.firstDayOfWeek()}
+ onBlur={this.calculate}
+ onChange={(_, d, ds) => this.updateDate(d, ds)}
+ placeholder={timeformat.dateShortFormat()}
+ validators={[this.dateRangeValidator]}
+ value={this.state.date}
+ appendTo={() => document.body} />
+ <TimePicker time={this.state.time} is24Hour
+ className='shutdown-time-picker'
+ id="shutdown-time"
+ isDisabled={!this.state.formFilled}
+ invalidFormatErrorMessage=""
+ menuAppendTo={() => document.body}
+ onBlur={this.calculate}
+ onChange={(_, time, h, m) => this.updateTime(time, h, m) } />
+ </>}
+ </Flex>
+ <FormHelper fieldId="delay" helperTextInvalid={this.state.dateError} />
+ </FormGroup>
+ </Form>
+ {this.state.error && <Alert isInline variant='danger' title={this.state.error} />}
+ </>
+ </Modal>
+ );
+ }
+}
diff --git a/pkg/lib/cockpit-components-shutdown.scss b/pkg/lib/cockpit-components-shutdown.scss
new file mode 100644
index 0000000..80a255f
--- /dev/null
+++ b/pkg/lib/cockpit-components-shutdown.scss
@@ -0,0 +1,17 @@
+#shutdown-group {
+ overflow: visible;
+}
+
+.shutdown-delay-group {
+ // Add spacing between rows for when the flex items wrap
+ row-gap: var(--pf-v5-global--spacer--sm);
+
+ .shutdown-time-picker {
+ max-inline-size: 7rem;
+ }
+
+ .shutdown-select-delay,
+ .shutdown-date-picker {
+ max-inline-size: 10rem;
+ }
+}
diff --git a/pkg/lib/cockpit-components-table.jsx b/pkg/lib/cockpit-components-table.jsx
new file mode 100644
index 0000000..fd472ca
--- /dev/null
+++ b/pkg/lib/cockpit-components-table.jsx
@@ -0,0 +1,297 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useEffect } from 'react';
+import {
+ ExpandableRowContent,
+ Table, Thead, Tbody, Tr, Th, Td,
+ SortByDirection,
+} from '@patternfly/react-table';
+import { EmptyState, EmptyStateBody, EmptyStateFooter, EmptyStateActions } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+
+import './cockpit-components-table.scss';
+
+/* This is a wrapper around PF Table component
+ * See https://www.patternfly.org/components/table/
+ * Properties (all optional unless specified otherwise):
+ * - caption
+ * - id: optional identifier
+ * - className: additional classes added to the Table
+ * - actions: additional listing-wide actions (displayed next to the list's title)
+ * - columns: { title: string, header: boolean, sortable: boolean }[] or string[]
+ * - rows: {
+ * columns: (React.Node or string or { title: string, key: string, ...extraProps: object}}[]
+ Through extraProps the consumers can pass arbitrary properties to the <td>
+ * props: { key: string, ...extraProps: object }
+ This property is mandatory and should contain a unique `key`, all additional properties are optional.
+ Through extraProps the consumers can pass arbitrary properties to the <tr>
+ * expandedContent: (React.Node)[])
+ * selected: boolean option if the row is selected
+ * initiallyExpanded : the entry will be initially rendered as expanded, but then behaves normally
+ * }[]
+ * - emptyCaption: header caption to show if list is empty
+ * - emptyCaptionDetail: extra details to show after emptyCaption if list is empty
+ * - emptyComponent: Whole empty state component to show if the list is empty
+ * - isEmptyStateInTable: if empty state is result of a filter function this should be set, otherwise false
+ * - loading: Set to string when the content is still loading. This string is shown.
+ * - variant: For compact tables pass 'compact'
+ * - gridBreakPoint: Specifies the grid breakpoints ('', 'grid' | 'grid-md' | 'grid-lg' | 'grid-xl' | 'grid-2xl')
+ * - sortBy: { index: Number, direction: SortByDirection }
+ * - sortMethod: callback function used for sorting rows. Called with 3 parameters: sortMethod(rows, activeSortDirection, activeSortIndex)
+ * - style: object of additional css rules
+ * - afterToggle: function to be called when content is toggled
+ * - onSelect: function to be called when a checkbox is clicked. Called with 5 parameters:
+ * event, isSelected, rowIndex, rowData, extraData. rowData contains props with an id property of the clicked row.
+ * - onHeaderSelect: event, isSelected.
+ */
+export const ListingTable = ({
+ actions = [],
+ afterToggle,
+ caption = '',
+ className,
+ columns: cells = [],
+ emptyCaption = '',
+ emptyCaptionDetail,
+ emptyComponent,
+ isEmptyStateInTable = false,
+ loading = '',
+ onRowClick,
+ onSelect,
+ onHeaderSelect,
+ rows: tableRows = [],
+ showHeader = true,
+ sortBy,
+ sortMethod,
+ ...extraProps
+}) => {
+ let rows = [...tableRows];
+ const [expanded, setExpanded] = useState({});
+ const [newItems, setNewItems] = useState([]);
+ const [currentRowsKeys, setCurrentRowsKeys] = useState([]);
+ const [activeSortIndex, setActiveSortIndex] = useState(sortBy?.index ?? 0);
+ const [activeSortDirection, setActiveSortDirection] = useState(sortBy?.direction ?? SortByDirection.asc);
+ const rowKeys = rows.map(row => row.props?.key)
+ .filter(key => key !== undefined);
+ const rowKeysStr = JSON.stringify(rowKeys);
+ const currentRowsKeysStr = JSON.stringify(currentRowsKeys);
+
+ useEffect(() => {
+ // Don't highlight all when the list gets loaded
+ const _currentRowsKeys = JSON.parse(currentRowsKeysStr);
+ const _rowKeys = JSON.parse(rowKeysStr);
+
+ if (_currentRowsKeys.length !== 0) {
+ const new_keys = _rowKeys.filter(key => _currentRowsKeys.indexOf(key) === -1);
+ if (new_keys.length) {
+ setTimeout(() => setNewItems(items => items.filter(item => new_keys.indexOf(item) < 0)), 4000);
+ setNewItems(ni => [...ni, ...new_keys]);
+ }
+ }
+
+ setCurrentRowsKeys(crk => [...new Set([...crk, ..._rowKeys])]);
+ }, [currentRowsKeysStr, rowKeysStr]);
+
+ const isSortable = cells.some(col => col.sortable);
+ const isExpandable = rows.some(row => row.expandedContent);
+
+ const tableProps = {};
+
+ /* Basic table properties */
+ tableProps.className = "ct-table";
+ if (className)
+ tableProps.className = tableProps.className + " " + className;
+ if (rows.length == 0)
+ tableProps.className += ' ct-table-empty';
+
+ const header = (
+ (caption || actions.length != 0)
+ ? <header className='ct-table-header'>
+ <h3 className='ct-table-heading'> {caption} </h3>
+ {actions && <div className='ct-table-actions'> {actions} </div>}
+ </header>
+ : null
+ );
+
+ if (loading)
+ return <EmptyState>
+ <EmptyStateBody>
+ {loading}
+ </EmptyStateBody>
+ </EmptyState>;
+
+ if (rows == 0) {
+ let emptyState = null;
+ if (emptyComponent)
+ emptyState = emptyComponent;
+ else
+ emptyState = (
+ <EmptyState>
+ <EmptyStateBody>
+ <div>{emptyCaption}</div>
+ <TextContent>
+ <Text component={TextVariants.small}>
+ {emptyCaptionDetail}
+ </Text>
+ </TextContent>
+ </EmptyStateBody>
+ {actions.length > 0 &&
+ <EmptyStateFooter>
+ <EmptyStateActions>{actions}</EmptyStateActions>
+ </EmptyStateFooter>}
+ </EmptyState>
+ );
+ if (!isEmptyStateInTable)
+ return emptyState;
+
+ const emptyStateCell = (
+ [{
+ props: { colSpan: cells.length },
+ title: emptyState
+ }]
+ );
+
+ rows = [{ columns: emptyStateCell }];
+ }
+
+ const sortRows = () => {
+ const sortedRows = rows.sort((a, b) => {
+ const aitem = a.columns[activeSortIndex];
+ const bitem = b.columns[activeSortIndex];
+
+ return ((typeof aitem == 'string' ? aitem : (aitem.sortKey || aitem.title)).localeCompare(typeof bitem == 'string' ? bitem : (bitem.sortKey || bitem.title)));
+ });
+ return activeSortDirection === SortByDirection.asc ? sortedRows : sortedRows.reverse();
+ };
+
+ const onSort = (event, index, direction) => {
+ setActiveSortIndex(index);
+ setActiveSortDirection(direction);
+ };
+
+ const rowsComponents = (isSortable ? (sortMethod ? sortMethod(rows, activeSortDirection, activeSortIndex) : sortRows()) : rows).map((row, rowIndex) => {
+ const rowProps = row.props || {};
+ if (onRowClick) {
+ rowProps.isClickable = true;
+ rowProps.onRowClick = (event) => onRowClick(event, row);
+ }
+
+ if (rowProps.key && newItems.indexOf(rowProps.key) >= 0)
+ rowProps.className = (rowProps.className || "") + " ct-new-item";
+
+ const rowKey = rowProps.key || rowIndex;
+ const isExpanded = expanded[rowKey] === undefined ? !!row.initiallyExpanded : expanded[rowKey];
+ const rowPair = (
+ <React.Fragment key={rowKey + "-inner-row"}>
+ <Tr {...rowProps}>
+ {isExpandable
+ ? (row.expandedContent
+ ? <Td expand={{
+ rowIndex: rowKey,
+ isExpanded,
+ onToggle: () => {
+ if (afterToggle)
+ afterToggle(!expanded[rowKey]);
+ setExpanded({ ...expanded, [rowKey]: !expanded[rowKey] });
+ }
+ }} />
+ : <Td className="pf-v5-c-table__toggle" />)
+ : null
+ }
+ {onSelect &&
+ <Td select={{
+ rowIndex,
+ onSelect,
+ isSelected: !!row.selected,
+ props: {
+ id: rowKey
+ }
+ }} />
+ }
+ {row.columns.map((cell, cellIndex) => {
+ const { key, ...cellProps } = cell.props || {};
+ const dataLabel = typeof cells[cellIndex] == 'object' ? cells[cellIndex].title : cells[cellIndex];
+ const colKey = dataLabel || cellIndex;
+ if (cells[cellIndex]?.header)
+ return (
+ <Th key={key || `row_${rowKey}_cell_${colKey}`} dataLabel={dataLabel} {...cellProps}>
+ {typeof cell == 'object' ? cell.title : cell}
+ </Th>
+ );
+
+ return (
+ <Td key={key || `row_${rowKey}_cell_${colKey}`} dataLabel={dataLabel} {...cellProps}>
+ {typeof cell == 'object' ? cell.title : cell}
+ </Td>
+ );
+ })}
+ </Tr>
+ {row.expandedContent && <Tr id={"expanded-content" + rowIndex} isExpanded={isExpanded}>
+ <Td noPadding={row.hasPadding !== true} colSpan={row.columns.length + 1 + (onSelect ? 1 : 0)}>
+ <ExpandableRowContent>{row.expandedContent}</ExpandableRowContent>
+ </Td>
+ </Tr>}
+ </React.Fragment>
+ );
+
+ return <Tbody key={rowKey} isExpanded={row.expandedContent && isExpanded}>{rowPair}</Tbody>;
+ });
+
+ return (
+ <>
+ {header}
+ <Table {...extraProps} {...tableProps}>
+ {showHeader && <Thead>
+ <Tr>
+ {isExpandable && <Th />}
+ {!onHeaderSelect && onSelect && <Th />}
+ {onHeaderSelect && onSelect && <Th select={{
+ onSelect: onHeaderSelect,
+ isSelected: rows.every(r => r.selected)
+ }} />}
+ {cells.map((column, columnIndex) => {
+ const columnProps = column.props;
+ const sortParams = (
+ column.sortable
+ ? {
+ sort: {
+ sortBy: {
+ index: activeSortIndex,
+ direction: activeSortDirection
+ },
+ onSort,
+ columnIndex
+ }
+ }
+ : {}
+ );
+
+ return (
+ <Th key={columnIndex} {...columnProps} {...sortParams}>
+ {typeof column == 'object' ? column.title : column}
+ </Th>
+ );
+ })}
+ </Tr>
+ </Thead>}
+ {rowsComponents}
+ </Table>
+ </>
+ );
+};
diff --git a/pkg/lib/cockpit-components-table.scss b/pkg/lib/cockpit-components-table.scss
new file mode 100644
index 0000000..6854ac1
--- /dev/null
+++ b/pkg/lib/cockpit-components-table.scss
@@ -0,0 +1,106 @@
+@import "global-variables";
+
+.ct-table {
+ &.pf-m-compact {
+ > thead, > tbody {
+ > tr:not(.pf-v5-c-table__expandable-row) {
+ // We actually want the normal font size for our lists
+ --pf-v5-c-table-cell--FontSize: var(--pf-v5-global--FontSize--md);
+ }
+ }
+ }
+
+ &-header {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+
+ > :only-child {
+ flex: auto;
+ }
+ }
+
+ &-heading {
+ // Push buttons to the right by stretching the heading
+ flex: auto;
+ // Add a bit of minimum margin to the right of the heading
+ margin-inline-end: var(--pf-v5-global--spacer--md);
+ // Set a minimum height of 3rem, so when buttons wrap, there's spacing
+ min-block-size: var(--pf-v5-global--spacer--2xl);
+ // Make sure textual content is aligned to the center
+ display: flex;
+ align-items: center;
+ }
+
+ &-actions {
+ > * {
+ margin-block: var(--pf-v5-global--spacer--xs);
+ margin-inline: var(--pf-v5-global--spacer--sm) 0;
+ }
+
+ > :first-child {
+ margin-inline-start: 0;
+ }
+ }
+
+ // https://github.com/patternfly/patternfly-react/issues/5379
+ &-empty {
+ [data-label] {
+ display: revert;
+ }
+
+ [data-label]::before {
+ display: none;
+ }
+ }
+
+ // Don't wrap labels
+ [data-label]::before {
+ white-space: nowrap;
+ }
+
+ // Fix toggle button alignment
+ .pf-v5-c-table__toggle {
+ // Workaround: Chrome sometimes oddly expands the table,
+ // unless a width is set. (This affects panels the most, but not only.)
+ // As the width is smaller than the contents, and this is a table,
+ // the cell will stay at the correct width.
+ inline-size: 1px;
+ }
+
+ // Properly align actions on the end
+ > tbody > tr > td:last-child > .btn-group {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ }
+
+ // Use PF4 style headings
+ > thead th {
+ font-size: var(--pf-v5-global--FontSize--sm);
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+ }
+
+ // Adjust the padding for nested ct-tables in ct-tables
+ // FIXME: https://github.com/patternfly/patternfly/issues/4280
+ .ct-table {
+ td, th {
+ &:first-child {
+ --pf-v5-c-table--nested--first-last-child--PaddingLeft: var(--pf-v5-global--spacer--lg);
+ }
+
+ &:last-child {
+ --pf-v5-c-table--nested--first-last-child--PaddingRight: var(--pf-v5-global--spacer--lg);
+ }
+ }
+ }
+}
+
+// Special handling for rows with errors
+.pf-v5-c-table tbody tr:first-child.error {
+ &, tbody.pf-m-expanded > & {
+ background-color: var(--ct-color-list-critical-bg) !important; /* keep red background when expanded */
+ border-block-start: 1px solid var(--ct-color-list-critical-border);
+ border-block-end: 1px solid var(--ct-color-list-critical-border);
+ }
+}
diff --git a/pkg/lib/cockpit-components-terminal.jsx b/pkg/lib/cockpit-components-terminal.jsx
new file mode 100644
index 0000000..ede83ec
--- /dev/null
+++ b/pkg/lib/cockpit-components-terminal.jsx
@@ -0,0 +1,362 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import PropTypes from "prop-types";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { MenuList, MenuItem } from "@patternfly/react-core/dist/esm/components/Menu";
+import { Terminal as Term } from "xterm";
+import { CanvasAddon } from 'xterm-addon-canvas';
+
+import { ContextMenu } from "cockpit-components-context-menu.jsx";
+import cockpit from "cockpit";
+
+import "console.css";
+
+const _ = cockpit.gettext;
+
+const theme_core = {
+ yellow: "#b58900",
+ brightRed: "#cb4b16",
+ red: "#dc322f",
+ magenta: "#d33682",
+ brightMagenta: "#6c71c4",
+ blue: "#268bd2",
+ cyan: "#2aa198",
+ green: "#859900"
+};
+
+const themes = {
+ "black-theme": {
+ background: "#000000",
+ foreground: "#ffffff"
+ },
+ "dark-theme": Object.assign({}, theme_core, {
+ background: "#002b36",
+ foreground: "#fdf6e3",
+ cursor: "#eee8d5",
+ selection: "#ffffff77",
+ brightBlack: "#002b36",
+ black: "#073642",
+ brightGreen: "#586e75",
+ brightYellow: "#657b83",
+ brightBlue: "#839496",
+ brightCyan: "#93a1a1",
+ white: "#eee8d5",
+ brightWhite: "#fdf6e3"
+ }),
+ "light-theme": Object.assign({}, theme_core, {
+ background: "#fdf6e3",
+ foreground: "#002b36",
+ cursor: "#073642",
+ selection: "#00000044",
+ brightWhite: "#002b36",
+ white: "#073642",
+ brightCyan: "#586e75",
+ brightBlue: "#657b83",
+ brightYellow: "#839496",
+ brightGreen: "#93a1a1",
+ black: "#eee8d5",
+ brightBlack: "#fdf6e3"
+ }),
+ "white-theme": {
+ background: "#ffffff",
+ foreground: "#000000",
+ selection: "#00000044",
+ cursor: "#000000",
+ },
+};
+
+/*
+ * A terminal component that communicates over a cockpit channel.
+ *
+ * The only required property is 'channel', which must point to a cockpit
+ * stream channel.
+ *
+ * The size of the terminal can be set with the 'rows' and 'cols'
+ * properties. If those properties are not given, the terminal will fill
+ * its container.
+ *
+ * If the 'onTitleChanged' callback property is set, it will be called whenever
+ * the title of the terminal changes.
+ *
+ * Call focus() to set the input focus on the terminal.
+ *
+ * Also it is possible to set up theme by property 'theme'.
+ */
+export class Terminal extends React.Component {
+ constructor(props) {
+ super(props);
+ this.onChannelMessage = this.onChannelMessage.bind(this);
+ this.onChannelClose = this.onChannelClose.bind(this);
+ this.connectChannel = this.connectChannel.bind(this);
+ this.disconnectChannel = this.disconnectChannel.bind(this);
+ this.reset = this.reset.bind(this);
+ this.focus = this.focus.bind(this);
+ this.onWindowResize = this.onWindowResize.bind(this);
+ this.resizeTerminal = this.resizeTerminal.bind(this);
+ this.onFocusIn = this.onFocusIn.bind(this);
+ this.onFocusOut = this.onFocusOut.bind(this);
+ this.setText = this.setText.bind(this);
+ this.getText = this.getText.bind(this);
+ this.setTerminalTheme = this.setTerminalTheme.bind(this);
+
+ const term = new Term({
+ cols: props.cols || 80,
+ rows: props.rows || 25,
+ screenKeys: true,
+ cursorBlink: true,
+ fontSize: props.fontSize || 16,
+ fontFamily: 'Menlo, Monaco, Consolas, monospace',
+ screenReaderMode: true,
+ showPastingModal: false,
+ });
+
+ this.terminalRef = React.createRef();
+
+ term.onData(function(data) {
+ if (this.props.channel.valid)
+ this.props.channel.send(data);
+ }.bind(this));
+
+ if (props.onTitleChanged)
+ term.onTitleChange(props.onTitleChanged);
+
+ this.terminal = term;
+ this.state = {
+ showPastingModal: false,
+ cols: props.cols || 80,
+ rows: props.rows || 25
+ };
+ }
+
+ componentDidMount() {
+ this.terminal.open(this.terminalRef.current);
+ this.terminal.loadAddon(new CanvasAddon());
+ this.connectChannel();
+
+ if (!this.props.rows) {
+ window.addEventListener('resize', this.onWindowResize);
+ this.onWindowResize();
+ }
+ this.setTerminalTheme(this.props.theme || 'black-theme');
+ this.terminal.focus();
+ }
+
+ resizeTerminal(cols, rows) {
+ this.terminal.resize(cols, rows);
+ this.props.channel.control({
+ window: {
+ rows,
+ cols
+ }
+ });
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (prevProps.fontSize !== this.props.fontSize) {
+ this.terminal.options.fontSize = this.props.fontSize;
+
+ // After font size is changed, resize needs to be triggered
+ const dimensions = this.calculateDimensions();
+ if (dimensions.cols !== this.state.cols || dimensions.rows !== this.state.rows) {
+ this.onWindowResize();
+ } else {
+ // When font size changes but dimensions are the same, we need to force `resize`
+ this.resizeTerminal(dimensions.cols - 1, dimensions.rows);
+ }
+ }
+
+ if (prevState.cols !== this.state.cols || prevState.rows !== this.state.rows)
+ this.resizeTerminal(this.state.cols, this.state.rows);
+
+ if (prevProps.theme !== this.props.theme)
+ this.setTerminalTheme(this.props.theme);
+
+ if (prevProps.channel !== this.props.channel) {
+ this.terminal.reset();
+ this.disconnectChannel(prevProps.channel);
+ this.connectChannel();
+ this.props.channel.control({
+ window: {
+ rows: this.state.rows,
+ cols: this.state.cols
+ }
+ });
+ }
+ this.terminal.focus();
+ }
+
+ render() {
+ const contextMenuList = (
+ <MenuList>
+ <MenuItem className="contextMenuOption" onClick={this.getText}>
+ <div className="contextMenuName"> { _("Copy") } </div>
+ <div className="contextMenuShortcut">{ _("Ctrl+Insert") }</div>
+ </MenuItem>
+ <MenuItem className="contextMenuOption" onClick={this.setText}>
+ <div className="contextMenuName"> { _("Paste") } </div>
+ <div className="contextMenuShortcut">{ _("Shift+Insert") }</div>
+ </MenuItem>
+ </MenuList>
+ );
+
+ return (
+ <>
+ <Modal title={_("Paste error")}
+ position="top"
+ variant="small"
+ isOpen={this.state.showPastingModal}
+ onClose={() => this.setState({ showPastingModal: false })}
+ actions={[
+ <Button key="cancel" variant="secondary" onClick={() => this.setState({ showPastingModal: false })}>
+ {_("Close")}
+ </Button>
+ ]}>
+ {_("Your browser does not allow paste from the context menu. You can use Shift+Insert.")}
+ </Modal>
+ <div ref={this.terminalRef}
+ key={this.terminal}
+ className="console-ct"
+ onFocus={this.onFocusIn}
+ onContextMenu={this.contextMenu}
+ onBlur={this.onFocusOut} />
+ <ContextMenu parentId={this.props.parentId}>
+ {contextMenuList}
+ </ContextMenu>
+ </>
+ );
+ }
+
+ componentWillUnmount() {
+ this.disconnectChannel();
+ this.terminal.dispose();
+ window.removeEventListener('resize', this.onWindowResize);
+ this.onFocusOut();
+ }
+
+ setText() {
+ try {
+ navigator.clipboard.readText()
+ .then(text => this.props.channel.send(text))
+ .catch(e => this.setState({ showPastingModal: true }))
+ .finally(() => this.terminal.focus());
+ } catch (error) {
+ this.setState({ showPastingModal: true });
+ }
+ }
+
+ getText() {
+ try {
+ navigator.clipboard.writeText(this.terminal.getSelection())
+ .catch(e => console.error('Text could not be copied, use Ctrl+Insert ', e ? e.toString() : ""))
+ .finally(() => this.terminal.focus());
+ } catch (error) {
+ console.error('Text could not be copied, use Ctrl+Insert:', error.toString());
+ }
+ }
+
+ onChannelMessage(event, data) {
+ this.terminal.write(data);
+ }
+
+ onChannelClose(event, options) {
+ const term = this.terminal;
+ term.write('\x1b[31m' + (options.problem || 'disconnected') + '\x1b[m\r\n');
+ term.cursorHidden = true;
+ term.refresh(term.rows, term.rows);
+ }
+
+ connectChannel() {
+ const channel = this.props.channel;
+ if (channel?.valid) {
+ channel.addEventListener('message', this.onChannelMessage.bind(this));
+ channel.addEventListener('close', this.onChannelClose.bind(this));
+ }
+ }
+
+ disconnectChannel(channel) {
+ if (channel === undefined)
+ channel = this.props.channel;
+ if (channel) {
+ channel.removeEventListener('message', this.onChannelMessage);
+ channel.removeEventListener('close', this.onChannelClose);
+ }
+ channel.close();
+ }
+
+ reset() {
+ this.terminal.reset();
+ this.props.channel.send(String.fromCharCode(12)); // Send SIGWINCH to show prompt on attaching
+ }
+
+ focus() {
+ if (this.terminal)
+ this.terminal.focus();
+ }
+
+ calculateDimensions() {
+ const padding = 10; // Leave a bit of space around terminal
+ const realHeight = this.terminal._core._renderService.dimensions.css.cell.height;
+ const realWidth = this.terminal._core._renderService.dimensions.css.cell.width;
+ if (realHeight && realWidth && realWidth !== 0 && realHeight !== 0)
+ return {
+ rows: Math.floor((this.terminalRef.current.parentElement.clientHeight - padding) / realHeight),
+ cols: Math.floor((this.terminalRef.current.parentElement.clientWidth - padding - 12) / realWidth) // Remove 12px for scrollbar
+ };
+
+ return { rows: this.state.rows, cols: this.state.cols };
+ }
+
+ onWindowResize() {
+ this.setState(this.calculateDimensions());
+ }
+
+ setTerminalTheme(theme) {
+ this.terminal.options.theme = themes[theme];
+ }
+
+ onBeforeUnload(event) {
+ // Firefox requires this when the page is in an iframe
+ event.preventDefault();
+
+ // see "an almost cross-browser solution" at
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
+ event.returnValue = '';
+ return '';
+ }
+
+ onFocusIn() {
+ window.addEventListener('beforeunload', this.onBeforeUnload);
+ }
+
+ onFocusOut() {
+ window.removeEventListener('beforeunload', this.onBeforeUnload);
+ }
+}
+
+Terminal.propTypes = {
+ cols: PropTypes.number,
+ rows: PropTypes.number,
+ channel: PropTypes.object.isRequired,
+ onTitleChanged: PropTypes.func,
+ theme: PropTypes.string,
+ parentId: PropTypes.string.isRequired
+};
diff --git a/pkg/lib/cockpit-components-truncate.jsx b/pkg/lib/cockpit-components-truncate.jsx
new file mode 100644
index 0000000..5fb5322
--- /dev/null
+++ b/pkg/lib/cockpit-components-truncate.jsx
@@ -0,0 +1,42 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* This is our version of the PatternFly Truncate component. We have
+ it since we don't want Patternfly's unconditional tooltip.
+
+ Truncation in the middle doesn't work with Patternsfly's approach
+ in mixed RTL/LTR environments, so we only offer truncation at the
+ end.
+ */
+
+import * as React from "react";
+import './cockpit-components-truncate.scss';
+
+export const Truncate = ({
+ content,
+ ...props
+}) => {
+ return (
+ <span className="pf-v5-c-truncate ct-no-truncate-min-width" {...props}>
+ <span className="pf-v5-c-truncate__start">
+ {content}
+ </span>
+ </span>
+ );
+};
diff --git a/pkg/lib/cockpit-components-truncate.scss b/pkg/lib/cockpit-components-truncate.scss
new file mode 100644
index 0000000..e557ee1
--- /dev/null
+++ b/pkg/lib/cockpit-components-truncate.scss
@@ -0,0 +1,4 @@
+.pf-v5-c-truncate.ct-no-truncate-min-width {
+ --pf-v5-c-truncate--MinWidth: 0;
+ --pf-v5-c-truncate__start--MinWidth: 0;
+}
diff --git a/pkg/lib/cockpit-dark-theme.js b/pkg/lib/cockpit-dark-theme.js
new file mode 100644
index 0000000..00249fa
--- /dev/null
+++ b/pkg/lib/cockpit-dark-theme.js
@@ -0,0 +1,70 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+function debug() {
+ if (window.debugging == "all" || window.debugging?.includes("style")) {
+ console.debug([`cockpit-dark-theme: ${document.documentElement.id}:`, ...arguments].join(" "));
+ }
+}
+
+function changeDarkThemeClass(documentElement, dark_mode) {
+ debug(`Setting cockpit theme to ${dark_mode ? "dark" : "light"}`);
+
+ if (dark_mode) {
+ documentElement.classList.add('pf-v5-theme-dark');
+ } else {
+ documentElement.classList.remove('pf-v5-theme-dark');
+ }
+}
+
+function _setDarkMode(_style) {
+ const style = _style || localStorage.getItem('shell:style') || 'auto';
+ let dark_mode;
+ // If a user set's an explicit theme, ignore system changes.
+ if ((window.matchMedia?.('(prefers-color-scheme: dark)').matches && style === "auto") || style === "dark") {
+ dark_mode = true;
+ } else {
+ dark_mode = false;
+ }
+ changeDarkThemeClass(document.documentElement, dark_mode);
+}
+
+window.addEventListener("storage", event => {
+ if (event.key === "shell:style") {
+ debug(`Storage element 'shell:style' changed from ${event.oldValue} to ${event.newValue}`);
+
+ _setDarkMode();
+ }
+});
+
+// When changing the theme from the shell switcher the localstorage change will not fire for the same page (aka shell)
+// so we need to listen for the event on the window object.
+window.addEventListener("cockpit-style", event => {
+ const style = event.detail.style;
+ debug(`Event received from shell with 'cockpit-style' ${style}`);
+
+ _setDarkMode(style);
+});
+
+window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
+ debug(`Operating system theme preference changed to ${window.matchMedia?.('(prefers-color-scheme: dark)').matches ? "dark" : "light"}`);
+ _setDarkMode();
+});
+
+_setDarkMode();
diff --git a/pkg/lib/cockpit-po-plugin.js b/pkg/lib/cockpit-po-plugin.js
new file mode 100644
index 0000000..9c20a86
--- /dev/null
+++ b/pkg/lib/cockpit-po-plugin.js
@@ -0,0 +1,159 @@
+import fs from "fs";
+import glob from "glob";
+import path from "path";
+
+import Jed from "jed";
+import gettext_parser from "gettext-parser";
+
+const config = {};
+
+const DEFAULT_WRAPPER = 'cockpit.locale(PO_DATA);';
+
+function get_po_files() {
+ try {
+ const linguas_file = path.resolve(config.srcdir, "po/LINGUAS");
+ const linguas = fs.readFileSync(linguas_file, 'utf8').match(/\S+/g);
+ return linguas.map(lang => path.resolve(config.srcdir, 'po', lang + '.po'));
+ } catch (error) {
+ if (error.code !== 'ENOENT') {
+ throw error;
+ }
+
+ /* No LINGUAS file? Fall back to globbing.
+ * Note: we won't detect .po files being added in this case.
+ */
+ return glob.sync(path.resolve(config.srcdir, 'po/*.po'));
+ }
+}
+
+function get_plural_expr(statement) {
+ try {
+ /* Check that the plural forms isn't being sneaky since we build a function here */
+ Jed.PF.parse(statement);
+ } catch (ex) {
+ console.error("bad plural forms: " + ex.message);
+ process.exit(1);
+ }
+
+ const expr = statement.replace(/nplurals=[1-9]; plural=([^;]*);?$/, '(n) => $1');
+ if (expr === statement) {
+ console.error("bad plural forms: " + statement);
+ process.exit(1);
+ }
+
+ return expr;
+}
+
+function buildFile(po_file, subdir, webpack_module, webpack_compilation, filename, filter) {
+ return new Promise((resolve, reject) => {
+ // Read the PO file, remove fuzzy/disabled lines to avoid tripping up the validator
+ const po_data = fs.readFileSync(po_file, 'utf8')
+ .split('\n')
+ .filter(line => !line.startsWith('#~'))
+ .join('\n');
+ const parsed = gettext_parser.po.parse(po_data, { defaultCharset: 'utf8', validation: true });
+ delete parsed.translations[""][""]; // second header copy
+
+ const rtl_langs = ["ar", "fa", "he", "ur"];
+ const dir = rtl_langs.includes(parsed.headers.Language) ? "rtl" : "ltr";
+
+ // cockpit.js only looks at "plural-forms" and "language"
+ const chunks = [
+ '{\n',
+ ' "": {\n',
+ ` "plural-forms": ${get_plural_expr(parsed.headers['Plural-Forms'])},\n`,
+ ` "language": "${parsed.headers.Language}",\n`,
+ ` "language-direction": "${dir}"\n`,
+ ' }'
+ ];
+ for (const [msgctxt, context] of Object.entries(parsed.translations)) {
+ const context_prefix = msgctxt ? msgctxt + '\u0004' : ''; /* for cockpit.ngettext */
+
+ for (const [msgid, translation] of Object.entries(context)) {
+ /* Only include msgids which appear in this source directory */
+ const references = translation.comments.reference.split(/\s/);
+ if (!references.some(str => str.startsWith(`pkg/${subdir}`) || str.startsWith(config.src_directory) || str.startsWith(`pkg/lib`)))
+ continue;
+
+ if (translation.comments.flag?.match(/\bfuzzy\b/))
+ continue;
+
+ if (!references.some(filter))
+ continue;
+
+ const key = JSON.stringify(context_prefix + msgid);
+ // cockpit.js always ignores the first item
+ chunks.push(`,\n ${key}: [\n null`);
+ for (const str of translation.msgstr) {
+ chunks.push(',\n ' + JSON.stringify(str));
+ }
+ chunks.push('\n ]');
+ }
+ }
+ chunks.push('\n}');
+
+ const wrapper = config.wrapper?.(subdir) || DEFAULT_WRAPPER;
+ const output = wrapper.replace('PO_DATA', chunks.join('')) + '\n';
+
+ const out_path = path.join(subdir ? (subdir + '/') : '', filename);
+ if (webpack_compilation)
+ webpack_compilation.emitAsset(out_path, new webpack_module.sources.RawSource(output));
+ else
+ fs.writeFileSync(path.resolve(config.outdir, out_path), output);
+ return resolve();
+ });
+}
+
+function init(options) {
+ config.srcdir = process.env.SRCDIR || './';
+ config.subdirs = options.subdirs || [''];
+ config.src_directory = options.src_directory || 'src';
+ config.wrapper = options.wrapper;
+ config.outdir = options.outdir || './dist';
+}
+
+function run(webpack_module, webpack_compilation) {
+ const promises = [];
+ for (const subdir of config.subdirs) {
+ for (const po_file of get_po_files()) {
+ if (webpack_compilation)
+ webpack_compilation.fileDependencies.add(po_file);
+ const lang = path.basename(po_file).slice(0, -3);
+ promises.push(Promise.all([
+ // Separate translations for the manifest.json file and normal pages
+ buildFile(po_file, subdir, webpack_module, webpack_compilation,
+ `po.${lang}.js`, str => !str.includes('manifest.json')),
+ buildFile(po_file, subdir, webpack_module, webpack_compilation,
+ `po.manifest.${lang}.js`, str => str.includes('manifest.json'))
+ ]));
+ }
+ }
+ return Promise.all(promises);
+}
+
+export const cockpitPoEsbuildPlugin = options => ({
+ name: 'cockpitPoEsbuildPlugin',
+ setup(build) {
+ init({ ...options, outdir: build.initialOptions.outdir });
+ build.onEnd(async result => { result.errors.length === 0 && await run() });
+ },
+});
+
+export class CockpitPoWebpackPlugin {
+ constructor(options) {
+ init(options || {});
+ }
+
+ apply(compiler) {
+ compiler.hooks.thisCompilation.tap('CockpitPoWebpackPlugin', async compilation => {
+ const webpack = (await import('webpack')).default;
+ compilation.hooks.processAssets.tapPromise(
+ {
+ name: 'CockpitPoWebpackPlugin',
+ stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
+ },
+ () => run(webpack, compilation)
+ );
+ });
+ }
+}
diff --git a/pkg/lib/cockpit-rsync-plugin.js b/pkg/lib/cockpit-rsync-plugin.js
new file mode 100644
index 0000000..bb26be3
--- /dev/null
+++ b/pkg/lib/cockpit-rsync-plugin.js
@@ -0,0 +1,49 @@
+import child_process from "child_process";
+
+const config = {};
+
+function init(options) {
+ config.dest = options.dest || "";
+ config.source = options.source || "dist/";
+ config.ssh_host = process.env.RSYNC || process.env.RSYNC_DEVEL;
+
+ // ensure the target directory exists
+ if (config.ssh_host) {
+ config.rsync_dir = process.env.RSYNC ? "/usr/local/share/cockpit/" : "~/.local/share/cockpit/";
+ child_process.spawnSync("ssh", [config.ssh_host, "mkdir", "-p", config.rsync_dir], { stdio: "inherit" });
+ }
+}
+
+function run(callback) {
+ if (config.ssh_host) {
+ const proc = child_process.spawn("rsync", ["--recursive", "--info=PROGRESS2", "--delete",
+ config.source, config.ssh_host + ":" + config.rsync_dir + config.dest], { stdio: "inherit" });
+ proc.on('close', (code) => {
+ if (code !== 0) {
+ process.exit(1);
+ } else {
+ callback();
+ }
+ });
+ } else {
+ callback();
+ }
+}
+
+export const cockpitRsyncEsbuildPlugin = options => ({
+ name: 'cockpitRsyncPlugin',
+ setup(build) {
+ init(options || {});
+ build.onEnd(result => result.errors.length === 0 ? run(() => {}) : {});
+ },
+});
+
+export class CockpitRsyncWebpackPlugin {
+ constructor(options) {
+ init(options || {});
+ }
+
+ apply(compiler) {
+ compiler.hooks.afterEmit.tapAsync('WebpackHookPlugin', (_compilation, callback) => run(callback));
+ }
+}
diff --git a/pkg/lib/cockpit.js b/pkg/lib/cockpit.js
new file mode 100644
index 0000000..bef8e61
--- /dev/null
+++ b/pkg/lib/cockpit.js
@@ -0,0 +1,4444 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* eslint-disable indent,no-empty */
+
+let url_root;
+
+const meta_url_root = document.head.querySelector("meta[name='url-root']");
+if (meta_url_root) {
+ url_root = meta_url_root.content.replace(/^\/+|\/+$/g, '');
+} else {
+ // fallback for cockpit-ws < 272
+ try {
+ // Sometimes this throws a SecurityError such as during testing
+ url_root = window.localStorage.getItem('url-root');
+ } catch (e) { }
+}
+
+/* injected by tests */
+var mock = mock || { }; // eslint-disable-line no-use-before-define, no-var
+
+const cockpit = { };
+event_mixin(cockpit, { });
+
+/*
+ * The debugging property is a global that is used
+ * by various parts of the code to show/hide debug
+ * messages in the javascript console.
+ *
+ * We support using storage to get/set that property
+ * so that it carries across the various frames or
+ * alternatively persists across refreshes.
+ */
+if (typeof window.debugging === "undefined") {
+ try {
+ // Sometimes this throws a SecurityError such as during testing
+ Object.defineProperty(window, "debugging", {
+ get: function() { return window.sessionStorage.debugging || window.localStorage.debugging },
+ set: function(x) { window.sessionStorage.debugging = x }
+ });
+ } catch (e) { }
+}
+
+function in_array(array, val) {
+ const length = array.length;
+ for (let i = 0; i < length; i++) {
+ if (val === array[i])
+ return true;
+ }
+ return false;
+}
+
+function is_function(x) {
+ return typeof x === 'function';
+}
+
+function is_object(x) {
+ return x !== null && typeof x === 'object';
+}
+
+function is_plain_object(x) {
+ return is_object(x) && Object.prototype.toString.call(x) === '[object Object]';
+}
+
+/* Also works for negative zero */
+function is_negative(n) {
+ return ((n = +n) || 1 / n) < 0;
+}
+
+function invoke_functions(functions, self, args) {
+ const length = functions?.length ?? 0;
+ for (let i = 0; i < length; i++) {
+ if (functions[i])
+ functions[i].apply(self, args);
+ }
+}
+
+function iterate_data(data, callback, batch) {
+ let binary = false;
+ let len = 0;
+
+ if (!batch)
+ batch = 64 * 1024;
+
+ if (data) {
+ if (data.byteLength) {
+ len = data.byteLength;
+ binary = true;
+ } else if (data.length) {
+ len = data.length;
+ }
+ }
+
+ for (let i = 0; i < len; i += batch) {
+ const n = Math.min(len - i, batch);
+ if (binary)
+ callback(new window.Uint8Array(data.buffer, i, n));
+ else
+ callback(data.substr(i, n));
+ }
+}
+
+/* -------------------------------------------------------------------------
+ * Channels
+ *
+ * Public: https://cockpit-project.org/guide/latest/api-base1.html
+ */
+
+let default_transport = null;
+let public_transport = null;
+let reload_after_disconnect = false;
+let expect_disconnect = false;
+let init_callback = null;
+let default_host = null;
+let process_hints = null;
+let incoming_filters = null;
+let outgoing_filters = null;
+
+let transport_origin = window.location.origin;
+
+if (!transport_origin) {
+ transport_origin = window.location.protocol + "//" + window.location.hostname +
+ (window.location.port ? ':' + window.location.port : '');
+}
+
+function array_from_raw_string(str, constructor) {
+ const length = str.length;
+ const data = new (constructor || Array)(length);
+ for (let i = 0; i < length; i++)
+ data[i] = str.charCodeAt(i) & 0xFF;
+ return data;
+}
+
+function array_to_raw_string(data) {
+ const length = data.length;
+ let str = "";
+ for (let i = 0; i < length; i++)
+ str += String.fromCharCode(data[i]);
+ return str;
+}
+
+/*
+ * These are the polyfills from Mozilla. It's pretty nasty that
+ * these weren't in the typed array standardization.
+ *
+ * https://developer.mozilla.org/en-US/docs/Glossary/Base64
+ */
+
+function uint6_to_b64 (x) {
+ return x < 26 ? x + 65 : x < 52 ? x + 71 : x < 62 ? x - 4 : x === 62 ? 43 : x === 63 ? 47 : 65;
+}
+
+function base64_encode(data) {
+ if (typeof data === "string")
+ return window.btoa(data);
+ /* For when the caller has chosen to use ArrayBuffer */
+ if (data instanceof window.ArrayBuffer)
+ data = new window.Uint8Array(data);
+ const length = data.length;
+ let mod3 = 2;
+ let str = "";
+ for (let uint24 = 0, i = 0; i < length; i++) {
+ mod3 = i % 3;
+ uint24 |= data[i] << (16 >>> mod3 & 24);
+ if (mod3 === 2 || length - i === 1) {
+ str += String.fromCharCode(uint6_to_b64(uint24 >>> 18 & 63),
+ uint6_to_b64(uint24 >>> 12 & 63),
+ uint6_to_b64(uint24 >>> 6 & 63),
+ uint6_to_b64(uint24 & 63));
+ uint24 = 0;
+ }
+ }
+
+ return str.substr(0, str.length - 2 + mod3) + (mod3 === 2 ? '' : mod3 === 1 ? '=' : '==');
+}
+
+function b64_to_uint6 (x) {
+ return x > 64 && x < 91
+ ? x - 65
+ : x > 96 && x < 123
+ ? x - 71
+ : x > 47 && x < 58 ? x + 4 : x === 43 ? 62 : x === 47 ? 63 : 0;
+}
+
+function base64_decode(str, constructor) {
+ if (constructor === String)
+ return window.atob(str);
+ const ilen = str.length;
+ let eq;
+ for (eq = 0; eq < 3; eq++) {
+ if (str[ilen - (eq + 1)] != '=')
+ break;
+ }
+ const olen = (ilen * 3 + 1 >> 2) - eq;
+ const data = new (constructor || Array)(olen);
+ for (let mod3, mod4, uint24 = 0, oi = 0, ii = 0; ii < ilen; ii++) {
+ mod4 = ii & 3;
+ uint24 |= b64_to_uint6(str.charCodeAt(ii)) << 18 - 6 * mod4;
+ if (mod4 === 3 || ilen - ii === 1) {
+ for (mod3 = 0; mod3 < 3 && oi < olen; mod3++, oi++)
+ data[oi] = uint24 >>> (16 >>> mod3 & 24) & 255;
+ uint24 = 0;
+ }
+ }
+ return data;
+}
+
+window.addEventListener('beforeunload', function() {
+ expect_disconnect = true;
+}, false);
+
+function transport_debug() {
+ if (window.debugging == "all" || window.debugging?.includes("channel"))
+ console.debug.apply(console, arguments);
+}
+
+/*
+ * Extends an object to have the standard DOM style addEventListener
+ * removeEventListener and dispatchEvent methods. The dispatchEvent
+ * method has the additional capability to create a new event from a type
+ * string and arguments.
+ */
+function event_mixin(obj, handlers) {
+ Object.defineProperties(obj, {
+ addEventListener: {
+ enumerable: false,
+ value: function addEventListener(type, handler) {
+ if (handlers[type] === undefined)
+ handlers[type] = [];
+ handlers[type].push(handler);
+ }
+ },
+ removeEventListener: {
+ enumerable: false,
+ value: function removeEventListener(type, handler) {
+ const length = handlers[type] ? handlers[type].length : 0;
+ for (let i = 0; i < length; i++) {
+ if (handlers[type][i] === handler) {
+ handlers[type][i] = null;
+ break;
+ }
+ }
+ }
+ },
+ dispatchEvent: {
+ enumerable: false,
+ value: function dispatchEvent(event) {
+ let type, args;
+ if (typeof event === "string") {
+ type = event;
+ args = Array.prototype.slice.call(arguments, 1);
+
+ let detail = null;
+ if (arguments.length == 2)
+ detail = arguments[1];
+ else if (arguments.length > 2)
+ detail = args;
+
+ event = new CustomEvent(type, {
+ bubbles: false,
+ cancelable: false,
+ detail
+ });
+
+ args.unshift(event);
+ } else {
+ type = event.type;
+ args = arguments;
+ }
+ if (is_function(obj['on' + type]))
+ obj['on' + type].apply(obj, args);
+ invoke_functions(handlers[type], obj, args);
+ }
+ }
+ });
+}
+
+function calculate_application() {
+ let path = window.location.pathname || "/";
+ let _url_root = url_root;
+ if (window.mock?.pathname)
+ path = window.mock.pathname;
+ if (window.mock?.url_root)
+ _url_root = window.mock.url_root;
+
+ if (_url_root && path.indexOf('/' + _url_root) === 0)
+ path = path.replace('/' + _url_root, '') || '/';
+
+ if (path.indexOf("/cockpit/") !== 0 && path.indexOf("/cockpit+") !== 0) {
+ if (path.indexOf("/=") === 0)
+ path = "/cockpit+" + path.split("/")[1];
+ else
+ path = "/cockpit";
+ }
+
+ return path.split("/")[1];
+}
+
+function calculate_url(suffix) {
+ if (!suffix)
+ suffix = "socket";
+ const window_loc = window.location.toString();
+ let _url_root = url_root;
+
+ if (window.mock?.url)
+ return window.mock.url;
+ if (window.mock?.url_root)
+ _url_root = window.mock.url_root;
+
+ let prefix = calculate_application();
+ if (_url_root)
+ prefix = _url_root + "/" + prefix;
+
+ if (window_loc.indexOf('http:') === 0) {
+ return "ws://" + window.location.host + "/" + prefix + "/" + suffix;
+ } else if (window_loc.indexOf('https:') === 0) {
+ return "wss://" + window.location.host + "/" + prefix + "/" + suffix;
+ } else {
+ transport_debug("Cockpit must be used over http or https");
+ return null;
+ }
+}
+
+function join_data(buffers, binary) {
+ if (!binary)
+ return buffers.join("");
+
+ let total = 0;
+ const length = buffers.length;
+ for (let i = 0; i < length; i++)
+ total += buffers[i].length;
+
+ const data = window.Uint8Array ? new window.Uint8Array(total) : new Array(total);
+
+ if (data.set) {
+ for (let j = 0, i = 0; i < length; i++) {
+ data.set(buffers[i], j);
+ j += buffers[i].length;
+ }
+ } else {
+ for (let j = 0, i = 0; i < length; i++) {
+ for (let k = 0; k < buffers[i].length; k++)
+ data[i + j] = buffers[i][k];
+ j += buffers[i].length;
+ }
+ }
+
+ return data;
+}
+
+/*
+ * A WebSocket that connects to parent frame. The mechanism
+ * for doing this will eventually be documented publicly,
+ * but for now:
+ *
+ * * Forward raw cockpit1 string protocol messages via window.postMessage
+ * * Listen for cockpit1 string protocol messages via window.onmessage
+ * * Never accept or send messages to another origin
+ * * An empty string message means "close" (not completely used yet)
+ */
+function ParentWebSocket(parent) {
+ const self = this;
+ self.readyState = 0;
+
+ window.addEventListener("message", function receive(event) {
+ if (event.origin !== transport_origin || event.source !== parent)
+ return;
+ const data = event.data;
+ if (data === undefined || (data.length === undefined && data.byteLength === undefined))
+ return;
+ if (data.length === 0) {
+ self.readyState = 3;
+ self.onclose();
+ } else {
+ self.onmessage(event);
+ }
+ }, false);
+
+ self.send = function send(message) {
+ parent.postMessage(message, transport_origin);
+ };
+
+ self.close = function close() {
+ self.readyState = 3;
+ parent.postMessage("", transport_origin);
+ self.onclose();
+ };
+
+ window.setTimeout(function() {
+ self.readyState = 1;
+ self.onopen();
+ }, 0);
+}
+
+function parse_channel(data) {
+ let channel;
+
+ /* A binary message, split out the channel */
+ if (data instanceof window.ArrayBuffer) {
+ const binary = new window.Uint8Array(data);
+ const length = binary.length;
+ let pos;
+ for (pos = 0; pos < length; pos++) {
+ if (binary[pos] == 10) /* new line */
+ break;
+ }
+ if (pos === length) {
+ console.warn("binary message without channel");
+ return null;
+ } else if (pos === 0) {
+ console.warn("binary control message");
+ return null;
+ } else {
+ channel = String.fromCharCode.apply(null, binary.subarray(0, pos));
+ }
+
+ /* A textual message */
+ } else {
+ const pos = data.indexOf('\n');
+ if (pos === -1) {
+ console.warn("text message without channel");
+ return null;
+ }
+ channel = data.substring(0, pos);
+ }
+
+ return channel;
+}
+
+/* Private Transport class */
+function Transport() {
+ const self = this;
+ self.application = calculate_application();
+
+ /* We can trigger events */
+ event_mixin(self, { });
+
+ let last_channel = 0;
+ let channel_seed = "";
+
+ if (window.mock)
+ window.mock.last_transport = self;
+
+ let ws;
+ let ignore_health_check = false;
+ let got_message = false;
+
+ /* See if we should communicate via parent */
+ if (window.parent !== window && window.name.indexOf("cockpit1:") === 0)
+ ws = new ParentWebSocket(window.parent);
+
+ let check_health_timer;
+
+ if (!ws) {
+ const ws_loc = calculate_url();
+ transport_debug("connecting to " + ws_loc);
+
+ if (ws_loc) {
+ if ("WebSocket" in window) {
+ ws = new window.WebSocket(ws_loc, "cockpit1");
+ } else {
+ console.error("WebSocket not supported, application will not work!");
+ }
+ }
+
+ check_health_timer = window.setInterval(function () {
+ if (self.ready)
+ ws.send("\n{ \"command\": \"ping\" }");
+ if (!got_message) {
+ if (ignore_health_check) {
+ console.log("health check failure ignored");
+ } else {
+ console.log("health check failed");
+ self.close({ problem: "timeout" });
+ }
+ }
+ got_message = false;
+ }, 30000);
+ }
+
+ if (!ws) {
+ ws = { close: function() { } };
+ window.setTimeout(function() {
+ self.close({ problem: "no-cockpit" });
+ }, 50);
+ }
+
+ const control_cbs = { };
+ const message_cbs = { };
+ let waiting_for_init = true;
+ self.ready = false;
+
+ /* Called when ready for channels to interact */
+ function ready_for_channels() {
+ if (!self.ready) {
+ self.ready = true;
+ self.dispatchEvent("ready");
+ }
+ }
+
+ ws.onopen = function() {
+ if (ws) {
+ if (typeof ws.binaryType !== "undefined")
+ ws.binaryType = "arraybuffer";
+ ws.send("\n{ \"command\": \"init\", \"version\": 1 }");
+ }
+ };
+
+ ws.onclose = function() {
+ transport_debug("WebSocket onclose");
+ ws = null;
+ if (reload_after_disconnect) {
+ expect_disconnect = true;
+ window.location.reload(true);
+ }
+ self.close();
+ };
+
+ ws.onmessage = self.dispatch_data = function(arg) {
+ got_message = true;
+
+ /* The first line of a message is the channel */
+ const message = arg.data;
+
+ const channel = parse_channel(message);
+ if (channel === null)
+ return false;
+
+ const payload = message instanceof window.ArrayBuffer
+ ? new window.Uint8Array(message, channel.length + 1)
+ : message.substring(channel.length + 1);
+ let control;
+
+ /* A control message, always string */
+ if (!channel) {
+ transport_debug("recv control:", payload);
+ control = JSON.parse(payload);
+ } else {
+ transport_debug("recv " + channel + ":", payload);
+ }
+
+ const length = incoming_filters ? incoming_filters.length : 0;
+ for (let i = 0; i < length; i++) {
+ if (incoming_filters[i](message, channel, control) === false)
+ return false;
+ }
+
+ if (!channel)
+ process_control(control);
+ else
+ process_message(channel, payload);
+
+ return true;
+ };
+
+ self.close = function close(options) {
+ if (!options)
+ options = { problem: "disconnected" };
+ options.command = "close";
+ window.clearInterval(check_health_timer);
+ const ows = ws;
+ ws = null;
+ if (ows)
+ ows.close();
+ if (expect_disconnect)
+ return;
+ ready_for_channels(); /* ready to fail */
+
+ /* Broadcast to everyone */
+ for (const chan in control_cbs)
+ control_cbs[chan].apply(null, [options]);
+ };
+
+ self.next_channel = function next_channel() {
+ last_channel++;
+ return channel_seed + String(last_channel);
+ };
+
+ function process_init(options) {
+ if (options.problem) {
+ self.close({ problem: options.problem });
+ return;
+ }
+
+ if (options.version !== 1) {
+ console.error("received unsupported version in init message: " + options.version);
+ self.close({ problem: "not-supported" });
+ return;
+ }
+
+ if (options["channel-seed"])
+ channel_seed = String(options["channel-seed"]);
+ if (options.host)
+ default_host = options.host;
+
+ if (public_transport) {
+ public_transport.options = options;
+ public_transport.csrf_token = options["csrf-token"];
+ public_transport.host = default_host;
+ }
+
+ if (init_callback)
+ init_callback(options);
+
+ if (waiting_for_init) {
+ waiting_for_init = false;
+ ready_for_channels();
+ }
+ }
+
+ function process_control(data) {
+ const channel = data.channel;
+
+ /* Init message received */
+ if (data.command == "init") {
+ process_init(data);
+ } else if (waiting_for_init) {
+ waiting_for_init = false;
+ if (data.command != "close" || channel) {
+ console.error("received message before init: ", data.command);
+ data = { problem: "protocol-error" };
+ }
+ self.close(data);
+
+ /* Any pings get sent back as pongs */
+ } else if (data.command == "ping") {
+ data.command = "pong";
+ self.send_control(data);
+ } else if (data.command == "pong") {
+ /* Any pong commands are ignored */
+
+ } else if (data.command == "hint") {
+ if (process_hints)
+ process_hints(data);
+ } else if (channel !== undefined) {
+ const func = control_cbs[channel];
+ if (func)
+ func(data);
+ }
+ }
+
+ function process_message(channel, payload) {
+ const func = message_cbs[channel];
+ if (func)
+ func(payload);
+ }
+
+ /* The channel/control arguments is used by filters, and auto-populated if necessary */
+ self.send_data = function send_data(data, channel, control) {
+ if (!ws) {
+ return false;
+ }
+
+ const length = outgoing_filters ? outgoing_filters.length : 0;
+ for (let i = 0; i < length; i++) {
+ if (channel === undefined)
+ channel = parse_channel(data);
+ if (!channel && control === undefined)
+ control = JSON.parse(data);
+ if (outgoing_filters[i](data, channel, control) === false)
+ return false;
+ }
+
+ ws.send(data);
+ return true;
+ };
+
+ /* The control arguments is used by filters, and auto populated if necessary */
+ self.send_message = function send_message(payload, channel, control) {
+ if (channel)
+ transport_debug("send " + channel, payload);
+ else
+ transport_debug("send control:", payload);
+
+ /* A binary message */
+ if (payload.byteLength || Array.isArray(payload)) {
+ if (payload instanceof window.ArrayBuffer)
+ payload = new window.Uint8Array(payload);
+ const output = join_data([array_from_raw_string(channel), [10], payload], true);
+ return self.send_data(output.buffer, channel, control);
+
+ /* A string message */
+ } else {
+ return self.send_data(channel.toString() + "\n" + payload, channel, control);
+ }
+ };
+
+ self.send_control = function send_control(data) {
+ if (!ws && (data.command == "close" || data.command == "kill"))
+ return; /* don't complain if closed and closing */
+ if (check_health_timer &&
+ data.command == "hint" && data.hint == "ignore_transport_health_check") {
+ /* This is for us, process it directly. */
+ ignore_health_check = data.data;
+ return;
+ }
+ return self.send_message(JSON.stringify(data), "", data);
+ };
+
+ self.register = function register(channel, control_cb, message_cb) {
+ control_cbs[channel] = control_cb;
+ message_cbs[channel] = message_cb;
+ };
+
+ self.unregister = function unregister(channel) {
+ delete control_cbs[channel];
+ delete message_cbs[channel];
+ };
+}
+
+function ensure_transport(callback) {
+ if (!default_transport)
+ default_transport = new Transport();
+ const transport = default_transport;
+ if (transport.ready) {
+ callback(transport);
+ } else {
+ transport.addEventListener("ready", function() {
+ callback(transport);
+ });
+ }
+}
+
+/* Always close the transport explicitly: allows parent windows to track us */
+window.addEventListener("unload", function() {
+ if (default_transport)
+ default_transport.close();
+});
+
+function Channel(options) {
+ const self = this;
+
+ /* We can trigger events */
+ event_mixin(self, { });
+
+ let transport;
+ let ready = null;
+ let closed = null;
+ let waiting = null;
+ let received_done = false;
+ let sent_done = false;
+ let id = null;
+ const binary = (options.binary === true);
+
+ /*
+ * Queue while waiting for transport, items are tuples:
+ * [is_control ? true : false, payload]
+ */
+ const queue = [];
+
+ /* Handy for callers, but not used by us */
+ self.valid = true;
+ self.options = options;
+ self.binary = binary;
+ self.id = id;
+
+ function on_message(payload) {
+ if (received_done) {
+ console.warn("received message after done");
+ self.close("protocol-error");
+ } else {
+ self.dispatchEvent("message", payload);
+ }
+ }
+
+ function on_close(data) {
+ closed = data;
+ self.valid = false;
+ if (transport && id)
+ transport.unregister(id);
+ if (closed.message && !options.err)
+ console.warn(closed.message);
+ self.dispatchEvent("close", closed);
+ if (waiting)
+ waiting.resolve(closed);
+ }
+
+ function on_ready(data) {
+ ready = data;
+ self.dispatchEvent("ready", ready);
+ }
+
+ function on_control(data) {
+ if (data.command == "close") {
+ on_close(data);
+ return;
+ } else if (data.command == "ready") {
+ on_ready(data);
+ }
+
+ const done = data.command === "done";
+ if (done && received_done) {
+ console.warn("received two done commands on channel");
+ self.close("protocol-error");
+ } else {
+ if (done)
+ received_done = true;
+ self.dispatchEvent("control", data);
+ }
+ }
+
+ function send_payload(payload) {
+ if (!binary) {
+ if (typeof payload !== "string")
+ payload = String(payload);
+ }
+ transport.send_message(payload, id);
+ }
+
+ ensure_transport(function(trans) {
+ transport = trans;
+ if (closed)
+ return;
+
+ id = transport.next_channel();
+ self.id = id;
+
+ /* Register channel handlers */
+ transport.register(id, on_control, on_message);
+
+ /* Now open the channel */
+ const command = { };
+ for (const i in options)
+ command[i] = options[i];
+ command.command = "open";
+ command.channel = id;
+
+ if (!command.host) {
+ if (default_host)
+ command.host = default_host;
+ }
+
+ if (binary)
+ command.binary = "raw";
+ else
+ delete command.binary;
+
+ command["flow-control"] = true;
+ transport.send_control(command);
+
+ /* Now drain the queue */
+ while (queue.length > 0) {
+ const item = queue.shift();
+ if (item[0]) {
+ item[1].channel = id;
+ transport.send_control(item[1]);
+ } else {
+ send_payload(item[1]);
+ }
+ }
+ });
+
+ self.send = function send(message) {
+ if (closed)
+ console.warn("sending message on closed channel");
+ else if (sent_done)
+ console.warn("sending message after done");
+ else if (!transport)
+ queue.push([false, message]);
+ else
+ send_payload(message);
+ };
+
+ self.control = function control(options) {
+ options = options || { };
+ if (!options.command)
+ options.command = "options";
+ if (options.command === "done")
+ sent_done = true;
+ options.channel = id;
+ if (!transport)
+ queue.push([true, options]);
+ else
+ transport.send_control(options);
+ };
+
+ self.wait = function wait(callback) {
+ if (!waiting) {
+ waiting = cockpit.defer();
+ if (closed) {
+ waiting.reject(closed);
+ } else if (ready) {
+ waiting.resolve(ready);
+ } else {
+ self.addEventListener("ready", function(event, data) {
+ waiting.resolve(data);
+ });
+ self.addEventListener("close", function(event, data) {
+ waiting.reject(data);
+ });
+ }
+ }
+ const promise = waiting.promise;
+ if (callback)
+ promise.then(callback, callback);
+ return promise;
+ };
+
+ self.close = function close(options) {
+ if (closed)
+ return;
+
+ if (!options)
+ options = { };
+ else if (typeof options == "string")
+ options = { problem: options };
+ options.command = "close";
+ options.channel = id;
+
+ if (!transport)
+ queue.push([true, options]);
+ else
+ transport.send_control(options);
+ on_close(options);
+ };
+
+ self.buffer = function buffer(callback) {
+ const buffers = [];
+ buffers.callback = callback;
+ buffers.squash = function squash() {
+ return join_data(buffers, binary);
+ };
+
+ function on_message(event, data) {
+ buffers.push(data);
+ if (buffers.callback) {
+ const block = join_data(buffers, binary);
+ if (block.length > 0) {
+ const consumed = buffers.callback.call(self, block);
+ if (typeof consumed !== "number" || consumed === block.length) {
+ buffers.length = 0;
+ } else if (consumed === 0) {
+ buffers.length = 1;
+ buffers[0] = block;
+ } else if (consumed !== 0) {
+ buffers.length = 1;
+ if (block.subarray)
+ buffers[0] = block.subarray(consumed);
+ else if (block.substring)
+ buffers[0] = block.substring(consumed);
+ else
+ buffers[0] = block.slice(consumed);
+ }
+ }
+ }
+ }
+
+ function on_close() {
+ self.removeEventListener("message", on_message);
+ self.removeEventListener("close", on_close);
+ }
+
+ self.addEventListener("message", on_message);
+ self.addEventListener("close", on_close);
+
+ return buffers;
+ };
+
+ self.toString = function toString() {
+ const host = options.host || "localhost";
+ return "[Channel " + (self.valid ? id : "<invalid>") + " -> " + host + "]";
+ };
+}
+
+/* Resolve dots and double dots */
+function resolve_path_dots(parts) {
+ const out = [];
+ const length = parts.length;
+ for (let i = 0; i < length; i++) {
+ const part = parts[i];
+ if (part === "" || part == ".") {
+ continue;
+ } else if (part == "..") {
+ if (out.length === 0)
+ return null;
+ out.pop();
+ } else {
+ out.push(part);
+ }
+ }
+ return out;
+}
+
+function factory() {
+ cockpit.channel = function channel(options) {
+ return new Channel(options);
+ };
+
+ cockpit.event_target = function event_target(obj) {
+ event_mixin(obj, { });
+ return obj;
+ };
+
+ /* obsolete backwards compatible shim */
+ cockpit.extend = Object.assign;
+
+ /* These can be filled in by loading ../manifests.js */
+ cockpit.manifests = { };
+
+ /* ------------------------------------------------------------
+ * Text Encoding
+ */
+
+ function Utf8TextEncoder(constructor) {
+ const self = this;
+ self.encoding = "utf-8";
+
+ self.encode = function encode(string, options) {
+ const data = window.unescape(encodeURIComponent(string));
+ if (constructor === String)
+ return data;
+ return array_from_raw_string(data, constructor);
+ };
+ }
+
+ function Utf8TextDecoder(fatal) {
+ const self = this;
+ let buffer = null;
+ self.encoding = "utf-8";
+
+ self.decode = function decode(data, options) {
+ const stream = options?.stream;
+
+ if (data === null || data === undefined)
+ data = "";
+ if (typeof data !== "string")
+ data = array_to_raw_string(data);
+ if (buffer) {
+ data = buffer + data;
+ buffer = null;
+ }
+
+ /* We have to scan to do non-fatal and streaming */
+ const len = data.length;
+ let beg = 0;
+ let i = 0;
+ let str = "";
+
+ while (i < len) {
+ const p = data.charCodeAt(i);
+ const x = p == 255
+ ? 0
+ : p > 251 && p < 254
+ ? 6
+ : p > 247 && p < 252
+ ? 5
+ : p > 239 && p < 248
+ ? 4
+ : p > 223 && p < 240
+ ? 3
+ : p > 191 && p < 224
+ ? 2
+ : p < 128 ? 1 : 0;
+
+ let ok = (i + x <= len);
+ if (!ok && stream) {
+ buffer = data.substring(i);
+ break;
+ }
+ if (x === 0)
+ ok = false;
+ for (let j = 1; ok && j < x; j++)
+ ok = (data.charCodeAt(i + j) & 0x80) !== 0;
+
+ if (!ok) {
+ if (fatal) {
+ i = len;
+ break;
+ }
+
+ str += decodeURIComponent(window.escape(data.substring(beg, i)));
+ str += "\ufffd";
+ i++;
+ beg = i;
+ } else {
+ i += x;
+ }
+ }
+
+ str += decodeURIComponent(window.escape(data.substring(beg, i)));
+ return str;
+ };
+ }
+
+ cockpit.utf8_encoder = function utf8_encoder(constructor) {
+ return new Utf8TextEncoder(constructor);
+ };
+
+ cockpit.utf8_decoder = function utf8_decoder(fatal) {
+ return new Utf8TextDecoder(!!fatal);
+ };
+
+ cockpit.base64_encode = base64_encode;
+ cockpit.base64_decode = base64_decode;
+
+ cockpit.kill = function kill(host, group) {
+ const options = { };
+ if (host)
+ options.host = host;
+ if (group)
+ options.group = group;
+ cockpit.transport.control("kill", options);
+ };
+
+ /* Not public API ... yet? */
+ cockpit.hint = function hint(name, options) {
+ if (!default_transport)
+ return;
+ if (!options)
+ options = default_host;
+ if (typeof options == "string")
+ options = { host: options };
+ options.hint = name;
+ cockpit.transport.control("hint", options);
+ };
+
+ cockpit.transport = public_transport = {
+ wait: ensure_transport,
+ inject: function inject(message, out) {
+ if (!default_transport)
+ return false;
+ if (out === undefined || out)
+ return default_transport.send_data(message);
+ else
+ return default_transport.dispatch_data({ data: message });
+ },
+ filter: function filter(callback, out) {
+ if (out) {
+ if (!outgoing_filters)
+ outgoing_filters = [];
+ outgoing_filters.push(callback);
+ } else {
+ if (!incoming_filters)
+ incoming_filters = [];
+ incoming_filters.push(callback);
+ }
+ },
+ close: function close(problem) {
+ if (default_transport)
+ default_transport.close(problem ? { problem } : undefined);
+ default_transport = null;
+ this.options = { };
+ },
+ origin: transport_origin,
+ options: { },
+ uri: calculate_url,
+ control: function(command, options) {
+ options = { ...options, command };
+ ensure_transport(function(transport) {
+ transport.send_control(options);
+ });
+ },
+ application: function () {
+ if (!default_transport || window.mock)
+ return calculate_application();
+ return default_transport.application;
+ },
+ };
+
+ /* ------------------------------------------------------------------------------------
+ * An ordered queue of functions that should be called later.
+ */
+
+ let later_queue = [];
+ let later_timeout = null;
+
+ function later_drain() {
+ const queue = later_queue;
+ later_timeout = null;
+ later_queue = [];
+ for (;;) {
+ const func = queue.shift();
+ if (!func)
+ break;
+ func();
+ }
+ }
+
+ function later_invoke(func) {
+ if (func)
+ later_queue.push(func);
+ if (later_timeout === null)
+ later_timeout = window.setTimeout(later_drain, 0);
+ }
+
+ /* ------------------------------------------------------------------------------------
+ * Promises.
+ * Based on Q and angular promises, with some jQuery compatibility. See the angular
+ * license in COPYING.node for license lineage. There are some key differences with
+ * both Q and jQuery.
+ *
+ * * Exceptions thrown in handlers are not treated as rejections or failures.
+ * Exceptions remain actual exceptions.
+ * * Unlike jQuery callbacks added to an already completed promise don't execute
+ * immediately. Wait until control is returned to the browser.
+ */
+
+ function promise_then(state, fulfilled, rejected, updated) {
+ if (fulfilled === undefined && rejected === undefined && updated === undefined)
+ return null;
+ const result = new Deferred();
+ state.pending = state.pending || [];
+ state.pending.push([result, fulfilled, rejected, updated]);
+ if (state.status > 0)
+ schedule_process_queue(state);
+ return result.promise;
+ }
+
+ function create_promise(state) {
+ /* Like jQuery the promise object is callable */
+ const self = function Promise(target) {
+ if (target) {
+ Object.assign(target, self);
+ return target;
+ }
+ return self;
+ };
+
+ state.status = 0;
+
+ self.then = function then(fulfilled, rejected, updated) {
+ return promise_then(state, fulfilled, rejected, updated) || self;
+ };
+
+ self.catch = function catch_(callback) {
+ return promise_then(state, null, callback) || self;
+ };
+
+ self.finally = function finally_(callback, updated) {
+ return promise_then(state, function() {
+ return handle_callback(arguments, true, callback);
+ }, function() {
+ return handle_callback(arguments, false, callback);
+ }, updated) || self;
+ };
+
+ /* Basic jQuery Promise compatibility */
+ self.done = function done(fulfilled) {
+ promise_then(state, fulfilled);
+ return self;
+ };
+
+ self.fail = function fail(rejected) {
+ promise_then(state, null, rejected);
+ return self;
+ };
+
+ self.always = function always(callback) {
+ promise_then(state, callback, callback);
+ return self;
+ };
+
+ self.progress = function progress(updated) {
+ promise_then(state, null, null, updated);
+ return self;
+ };
+
+ self.state = function state_() {
+ if (state.status == 1)
+ return "resolved";
+ if (state.status == 2)
+ return "rejected";
+ return "pending";
+ };
+
+ /* Promises are recursive like jQuery */
+ self.promise = self;
+
+ return self;
+ }
+
+ function process_queue(state) {
+ const pending = state.pending;
+ state.process_scheduled = false;
+ state.pending = undefined;
+ for (let i = 0, ii = pending.length; i < ii; ++i) {
+ state.pur = true;
+ const deferred = pending[i][0];
+ const fn = pending[i][state.status];
+ if (is_function(fn)) {
+ deferred.resolve(fn.apply(state.promise, state.values));
+ } else if (state.status === 1) {
+ deferred.resolve.apply(deferred.resolve, state.values);
+ } else {
+ deferred.reject.apply(deferred.reject, state.values);
+ }
+ }
+ }
+
+ function schedule_process_queue(state) {
+ if (state.process_scheduled || !state.pending)
+ return;
+ state.process_scheduled = true;
+ later_invoke(function() { process_queue(state) });
+ }
+
+ function deferred_resolve(state, values) {
+ let then;
+ let done = false;
+ if (is_object(values[0]) || is_function(values[0]))
+ then = values[0]?.then;
+ if (is_function(then)) {
+ state.status = -1;
+ then.call(values[0], function(/* ... */) {
+ if (done)
+ return;
+ done = true;
+ deferred_resolve(state, arguments);
+ }, function(/* ... */) {
+ if (done)
+ return;
+ done = true;
+ deferred_reject(state, arguments);
+ }, function(/* ... */) {
+ deferred_notify(state, arguments);
+ });
+ } else {
+ state.values = values;
+ state.status = 1;
+ schedule_process_queue(state);
+ }
+ }
+
+ function deferred_reject(state, values) {
+ state.values = values;
+ state.status = 2;
+ schedule_process_queue(state);
+ }
+
+ function deferred_notify(state, values) {
+ const callbacks = state.pending;
+ if ((state.status <= 0) && callbacks?.length) {
+ later_invoke(function() {
+ for (let i = 0, ii = callbacks.length; i < ii; i++) {
+ const result = callbacks[i][0];
+ const callback = callbacks[i][3];
+ if (is_function(callback))
+ result.notify(callback.apply(state.promise, values));
+ else
+ result.notify.apply(result, values);
+ }
+ });
+ }
+ }
+
+ function Deferred() {
+ const self = this;
+ const state = { };
+ self.promise = state.promise = create_promise(state);
+
+ self.resolve = function resolve(/* ... */) {
+ if (arguments[0] === state.promise)
+ throw new Error("Expected promise to be resolved with other value than itself");
+ if (!state.status)
+ deferred_resolve(state, arguments);
+ return self;
+ };
+
+ self.reject = function reject(/* ... */) {
+ if (state.status)
+ return;
+ deferred_reject(state, arguments);
+ return self;
+ };
+
+ self.notify = function notify(/* ... */) {
+ deferred_notify(state, arguments);
+ return self;
+ };
+ }
+
+ function prep_promise(values, resolved) {
+ const result = cockpit.defer();
+ if (resolved)
+ result.resolve.apply(result, values);
+ else
+ result.reject.apply(result, values);
+ return result.promise;
+ }
+
+ function handle_callback(values, is_resolved, callback) {
+ let callback_output = null;
+ if (is_function(callback))
+ callback_output = callback();
+ if (callback_output && is_function(callback_output.then)) {
+ return callback_output.then(function() {
+ return prep_promise(values, is_resolved);
+ }, function() {
+ return prep_promise(arguments, false);
+ });
+ } else {
+ return prep_promise(values, is_resolved);
+ }
+ }
+
+ cockpit.when = function when(value, fulfilled, rejected, updated) {
+ const result = cockpit.defer();
+ result.resolve(value);
+ return result.promise.then(fulfilled, rejected, updated);
+ };
+
+ cockpit.resolve = function resolve(result) {
+ return cockpit.defer().resolve(result).promise;
+ };
+
+ cockpit.reject = function reject(ex) {
+ return cockpit.defer().reject(ex).promise;
+ };
+
+ cockpit.defer = function() {
+ return new Deferred();
+ };
+
+ /* ---------------------------------------------------------------------
+ * Utilities
+ */
+
+ const fmt_re = /\$\{([^}]+)\}|\$([a-zA-Z0-9_]+)/g;
+ cockpit.format = function format(fmt, args) {
+ if (arguments.length != 2 || !is_object(args) || args === null)
+ args = Array.prototype.slice.call(arguments, 1);
+
+ function replace(m, x, y) {
+ const value = args[x || y];
+
+ /* Special-case 0 (also catches 0.0). All other falsy values return
+ * the empty string.
+ */
+ if (value === 0)
+ return '0';
+
+ return value || '';
+ }
+
+ return fmt.replace(fmt_re, replace);
+ };
+
+ cockpit.format_number = function format_number(number, precision) {
+ /* We show given number of digits of precision (default 3), but avoid scientific notation.
+ * We also show integers without digits after the comma.
+ *
+ * We want to localise the decimal separator, but we never want to
+ * show thousands separators (to avoid ambiguity). For this
+ * reason, for integers and large enough numbers, we use
+ * non-localised conversions (and in both cases, show no
+ * fractional part).
+ */
+ if (precision === undefined)
+ precision = 3;
+ const lang = cockpit.language === undefined ? undefined : cockpit.language.replace('_', '-');
+ const smallestValue = 10 ** (-precision);
+
+ if (!number && number !== 0)
+ return "";
+ else if (number % 1 === 0)
+ return number.toString();
+ else if (number > 0 && number <= smallestValue)
+ return smallestValue.toLocaleString(lang);
+ else if (number < 0 && number >= -smallestValue)
+ return (-smallestValue).toLocaleString(lang);
+ else if (number > 999 || number < -999)
+ return number.toFixed(0);
+ else
+ return number.toLocaleString(lang, {
+ maximumSignificantDigits: precision,
+ minimumSignificantDigits: precision,
+ });
+ };
+
+ function format_units(number, suffixes, factor, options) {
+ // backwards compat: "options" argument position used to be a boolean flag "separate"
+ if (!is_object(options))
+ options = { separate: options };
+
+ let suffix = null;
+
+ /* Find that factor string */
+ if (!number && number !== 0) {
+ suffix = null;
+ } else if (typeof (factor) === "string") {
+ /* Prefer larger factors */
+ const keys = [];
+ for (const key in suffixes)
+ keys.push(key);
+ keys.sort().reverse();
+ for (let y = 0; y < keys.length; y++) {
+ for (let x = 0; x < suffixes[keys[y]].length; x++) {
+ if (factor == suffixes[keys[y]][x]) {
+ number = number / Math.pow(keys[y], x);
+ suffix = factor;
+ break;
+ }
+ }
+ if (suffix)
+ break;
+ }
+
+ /* @factor is a number */
+ } else if (factor in suffixes) {
+ let divisor = 1;
+ for (let i = 0; i < suffixes[factor].length; i++) {
+ const quotient = number / divisor;
+ if (quotient < factor) {
+ number = quotient;
+ suffix = suffixes[factor][i];
+ break;
+ }
+ divisor *= factor;
+ }
+ }
+
+ const string_representation = cockpit.format_number(number, options.precision);
+ let ret;
+
+ if (string_representation && suffix)
+ ret = [string_representation, suffix];
+ else
+ ret = [string_representation];
+
+ if (!options.separate)
+ ret = ret.join(" ");
+
+ return ret;
+ }
+
+ const byte_suffixes = {
+ 1000: [null, "KB", "MB", "GB", "TB", "PB", "EB", "ZB"],
+ 1024: [null, "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"]
+ };
+
+ cockpit.format_bytes = function format_bytes(number, factor, options) {
+ if (factor === undefined)
+ factor = 1000;
+ return format_units(number, byte_suffixes, factor, options);
+ };
+
+ cockpit.get_byte_units = function get_byte_units(guide_value, factor) {
+ if (factor === undefined || !(factor in byte_suffixes))
+ factor = 1000;
+
+ function unit(index) {
+ return {
+ name: byte_suffixes[factor][index],
+ factor: Math.pow(factor, index)
+ };
+ }
+
+ const units = [unit(2), unit(3), unit(4)];
+
+ // The default unit is the largest one that gives us at least
+ // two decimal digits in front of the comma.
+
+ for (let i = units.length - 1; i >= 0; i--) {
+ if (i === 0 || (guide_value / units[i].factor) >= 10) {
+ units[i].selected = true;
+ break;
+ }
+ }
+
+ return units;
+ };
+
+ const byte_sec_suffixes = {
+ 1000: ["B/s", "kB/s", "MB/s", "GB/s", "TB/s", "PB/s", "EB/s", "ZB/s"],
+ 1024: ["B/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s", "PiB/s", "EiB/s", "ZiB/s"]
+ };
+
+ cockpit.format_bytes_per_sec = function format_bytes_per_sec(number, factor, options) {
+ if (factor === undefined)
+ factor = 1000;
+ return format_units(number, byte_sec_suffixes, factor, options);
+ };
+
+ const bit_suffixes = {
+ 1000: ["bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps"]
+ };
+
+ cockpit.format_bits_per_sec = function format_bits_per_sec(number, factor, options) {
+ if (factor === undefined)
+ factor = 1000;
+ return format_units(number, bit_suffixes, factor, options);
+ };
+
+ /* ---------------------------------------------------------------------
+ * Storage Helper.
+ *
+ * Use application to prefix data stored in browser storage
+ * with helpers for compatibility.
+ */
+ function StorageHelper(storageName) {
+ const self = this;
+ let storage;
+
+ try {
+ storage = window[storageName];
+ } catch (e) { }
+
+ self.prefixedKey = function (key) {
+ return cockpit.transport.application() + ":" + key;
+ };
+
+ self.getItem = function (key, both) {
+ let value = storage.getItem(self.prefixedKey(key));
+ if (!value && both)
+ value = storage.getItem(key);
+ return value;
+ };
+
+ self.setItem = function (key, value, both) {
+ storage.setItem(self.prefixedKey(key), value);
+ if (both)
+ storage.setItem(key, value);
+ };
+
+ self.removeItem = function(key, both) {
+ storage.removeItem(self.prefixedKey(key));
+ if (both)
+ storage.removeItem(key);
+ };
+
+ /* Instead of clearing, purge anything that isn't prefixed with an application
+ * and anything prefixed with our application.
+ */
+ self.clear = function(full) {
+ let i = 0;
+ while (i < storage.length) {
+ const k = storage.key(i);
+ if (full && k.indexOf("cockpit") !== 0)
+ storage.removeItem(k);
+ else if (k.indexOf(cockpit.transport.application()) === 0)
+ storage.removeItem(k);
+ else
+ i++;
+ }
+ };
+ }
+
+ cockpit.localStorage = new StorageHelper("localStorage");
+ cockpit.sessionStorage = new StorageHelper("sessionStorage");
+
+ /* ---------------------------------------------------------------------
+ * Shared data cache.
+ *
+ * We cannot use sessionStorage when keeping lots of data in memory and
+ * sharing it between frames. It has a rather paltry limit on the amount
+ * of data it can hold ... so we use window properties instead.
+ */
+
+ function lookup_storage(win) {
+ let storage;
+ if (win.parent && win.parent !== win)
+ storage = lookup_storage(win.parent);
+ if (!storage) {
+ try {
+ storage = win["cv1-storage"];
+ if (!storage)
+ win["cv1-storage"] = storage = { };
+ } catch (ex) { }
+ }
+ return storage;
+ }
+
+ function StorageCache(org_key, provider, consumer) {
+ const self = this;
+ const key = cockpit.transport.application() + ":" + org_key;
+
+ /* For triggering events and ownership */
+ const trigger = window.sessionStorage;
+ let last;
+
+ const storage = lookup_storage(window);
+
+ let claimed = false;
+ let source;
+
+ function callback() {
+ /* Only run the callback if we have a result */
+ if (storage[key] !== undefined) {
+ const value = storage[key];
+ window.setTimeout(function() {
+ if (consumer(value, org_key) === false)
+ self.close();
+ });
+ }
+ }
+
+ function result(value) {
+ if (source && !claimed)
+ claimed = true;
+ if (!claimed)
+ return;
+
+ // use a random number to avoid races by separate instances
+ const version = Math.floor(Math.random() * 10000000) + 1;
+
+ /* Event for the local window */
+ const ev = document.createEvent("StorageEvent");
+ ev.initStorageEvent("storage", false, false, key, null,
+ version, window.location, trigger);
+
+ storage[key] = value;
+ trigger.setItem(key, version);
+ ev.self = self;
+ window.dispatchEvent(ev);
+ }
+
+ self.claim = function claim() {
+ if (source)
+ return;
+
+ /* In case we're unclaimed during the callback */
+ const claiming = { close: function() { } };
+ source = claiming;
+
+ const changed = provider(result, org_key);
+ if (source === claiming)
+ source = changed;
+ else
+ changed.close();
+ };
+
+ function unclaim() {
+ if (source?.close)
+ source.close();
+ source = null;
+
+ if (!claimed)
+ return;
+
+ claimed = false;
+
+ let current_value = trigger.getItem(key);
+ if (current_value)
+ current_value = parseInt(current_value, 10);
+ else
+ current_value = null;
+
+ if (last && last === current_value) {
+ const ev = document.createEvent("StorageEvent");
+ const version = trigger[key];
+ ev.initStorageEvent("storage", false, false, key, version,
+ null, window.location, trigger);
+ delete storage[key];
+ trigger.removeItem(key);
+ ev.self = self;
+ window.dispatchEvent(ev);
+ }
+ }
+
+ function changed(event) {
+ if (event.key !== key)
+ return;
+
+ /* check where the event came from
+ - it came from someone else:
+ if it notifies their unclaim (new value null) and we haven't already claimed, do so
+ - it came from ourselves:
+ if the new value doesn't match the actual value in the cache, and
+ we tried to claim (from null to a number), cancel our claim
+ */
+ if (event.self !== self) {
+ if (!event.newValue && !claimed) {
+ self.claim();
+ return;
+ }
+ } else if (claimed && !event.oldValue && (event.newValue !== trigger.getItem(key))) {
+ unclaim();
+ }
+
+ let new_value = null;
+ if (event.newValue)
+ new_value = parseInt(event.newValue, 10);
+ if (last !== new_value) {
+ last = new_value;
+ callback();
+ }
+ }
+
+ self.close = function() {
+ window.removeEventListener("storage", changed, true);
+ unclaim();
+ };
+
+ window.addEventListener("storage", changed, true);
+
+ /* Always clear this data on unload */
+ window.addEventListener("beforeunload", function() {
+ self.close();
+ });
+ window.addEventListener("unload", function() {
+ self.close();
+ });
+
+ if (trigger.getItem(key))
+ callback();
+ else
+ self.claim();
+ }
+
+ cockpit.cache = function cache(key, provider, consumer) {
+ return new StorageCache(key, provider, consumer);
+ };
+
+ /* ---------------------------------------------------------------------
+ * Metrics
+ *
+ * Implements the cockpit.series and cockpit.grid. Part of the metrics
+ * implementations that do not require jquery.
+ */
+
+ function SeriesSink(interval, identifier, fetch_callback) {
+ const self = this;
+
+ self.interval = interval;
+ self.limit = identifier ? 64 * 1024 : 1024;
+
+ /*
+ * The cache sits on a window, either our own or a parent
+ * window whichever we can access properly.
+ *
+ * Entries in the index are:
+ *
+ * { beg: N, items: [], mapping: { }, next: item }
+ */
+ const index = setup_index(identifier);
+
+ /*
+ * A linked list through the index, that we use for expiry
+ * of the cache.
+ */
+ let count = 0;
+ let head = null;
+ let tail = null;
+
+ function setup_index(id) {
+ if (!id)
+ return [];
+
+ /* Try and find a good place to cache data */
+ const storage = lookup_storage(window);
+
+ let index = storage[id];
+ if (!index)
+ storage[id] = index = [];
+ return index;
+ }
+
+ function search(idx, beg) {
+ let low = 0;
+ let high = idx.length - 1;
+
+ while (low <= high) {
+ const mid = (low + high) / 2 | 0;
+ const val = idx[mid].beg;
+ if (val < beg)
+ low = mid + 1;
+ else if (val > beg)
+ high = mid - 1;
+ else
+ return mid; /* key found */
+ }
+ return low;
+ }
+
+ function fetch(beg, end, for_walking) {
+ if (fetch_callback) {
+ if (!for_walking) {
+ /* Stash some fake data synchronously so that we don't ask
+ * again for the same range while they are still fetching
+ * it asynchronously.
+ */
+ stash(beg, new Array(end - beg), { });
+ }
+ fetch_callback(beg, end, for_walking);
+ }
+ }
+
+ self.load = function load(beg, end, for_walking) {
+ if (end <= beg)
+ return;
+
+ const at = search(index, beg);
+
+ const len = index.length;
+ let last = beg;
+
+ /* We do this in two phases: First, we walk the index to
+ * process what we already have and at the same time make
+ * notes about what we need to fetch. Then we go over the
+ * notes and actually fetch what we need. That way, the
+ * fetch callbacks in the second phase can modify the
+ * index data structure without disturbing the walk in the
+ * first phase.
+ */
+
+ const fetches = [];
+
+ /* Data relevant to this range can be at the found index, or earlier */
+ for (let i = at > 0 ? at - 1 : at; i < len; i++) {
+ const entry = index[i];
+ const en = entry.items.length;
+ if (!en)
+ continue;
+
+ const eb = entry.beg;
+ const b = Math.max(eb, beg);
+ const e = Math.min(eb + en, end);
+
+ if (b < e) {
+ if (b > last)
+ fetches.push([last, b]);
+ process(b, entry.items.slice(b - eb, e - eb), entry.mapping);
+ last = e;
+ } else if (i >= at) {
+ break; /* no further intersections */
+ }
+ }
+
+ for (let i = 0; i < fetches.length; i++)
+ fetch(fetches[i][0], fetches[i][1], for_walking);
+
+ if (last != end)
+ fetch(last, end, for_walking);
+ };
+
+ function stash(beg, items, mapping) {
+ if (!items.length)
+ return;
+
+ let at = search(index, beg);
+
+ const end = beg + items.length;
+
+ const len = index.length;
+ let i;
+ for (i = at > 0 ? at - 1 : at; i < len; i++) {
+ const entry = index[i];
+ const en = entry.items.length;
+ if (!en)
+ continue;
+
+ const eb = entry.beg;
+ const b = Math.max(eb, beg);
+ const e = Math.min(eb + en, end);
+
+ /*
+ * We truncate blocks that intersect with this one
+ *
+ * We could adjust them, but in general the loaders are
+ * intelligent enough to only load the required data, so
+ * not doing this optimization yet.
+ */
+
+ if (b < e) {
+ const num = e - b;
+ entry.items.splice(b - eb, num);
+ count -= num;
+ if (b - eb === 0)
+ entry.beg += (e - eb);
+ } else if (i >= at) {
+ break; /* no further intersections */
+ }
+ }
+
+ /* Insert our item into the array */
+ const entry = { beg, items, mapping };
+ if (!head)
+ head = entry;
+ if (tail)
+ tail.next = entry;
+ tail = entry;
+ count += items.length;
+ index.splice(at, 0, entry);
+
+ /* Remove any items with zero length around insertion point */
+ for (at--; at <= i; at++) {
+ const entry = index[at];
+ if (entry && !entry.items.length) {
+ index.splice(at, 1);
+ at--;
+ }
+ }
+
+ /* If our index has gotten too big, expire entries */
+ while (head && count > self.limit) {
+ count -= head.items.length;
+ head.items = [];
+ head.mapping = null;
+ head = head.next || null;
+ }
+
+ /* Remove any entries with zero length at beginning */
+ const newlen = index.length;
+ for (i = 0; i < newlen; i++) {
+ if (index[i].items.length > 0)
+ break;
+ }
+ index.splice(0, i);
+ }
+
+ /*
+ * Used to populate grids, the keys are grid ids and
+ * the values are objects: { grid, rows, notify }
+ *
+ * The rows field is an object indexed by paths
+ * container aliases, and the values are: [ row, path ]
+ */
+ const registered = { };
+
+ /* An undocumented function called by DataGrid */
+ self._register = function _register(grid, id) {
+ if (grid.interval != interval)
+ throw Error("mismatched metric interval between grid and sink");
+ let gdata = registered[id];
+ if (!gdata) {
+ gdata = registered[id] = { grid, links: [] };
+ gdata.links.remove = function remove() {
+ delete registered[id];
+ };
+ }
+ return gdata.links;
+ };
+
+ function process(beg, items, mapping) {
+ const end = beg + items.length;
+
+ for (const id in registered) {
+ const gdata = registered[id];
+ const grid = gdata.grid;
+
+ const b = Math.max(beg, grid.beg);
+ const e = Math.min(end, grid.end);
+
+ /* Does this grid overlap the bounds of item? */
+ if (b < e) {
+ /* Where in the items to take from */
+ const f = b - beg;
+
+ /* Where and how many to place */
+ const t = b - grid.beg;
+
+ /* How many to process */
+ const n = e - b;
+
+ for (let i = 0; i < n; i++) {
+ const klen = gdata.links.length;
+ for (let k = 0; k < klen; k++) {
+ const path = gdata.links[k][0];
+ const row = gdata.links[k][1];
+
+ /* Calculate the data field to fill in */
+ let data = items[f + i];
+ let map = mapping;
+ const jlen = path.length;
+ for (let j = 0; data !== undefined && j < jlen; j++) {
+ if (!data) {
+ data = undefined;
+ } else if (map !== undefined && map !== null) {
+ map = map[path[j]];
+ if (map)
+ data = data[map[""]];
+ else
+ data = data[path[j]];
+ } else {
+ data = data[path[j]];
+ }
+ }
+
+ row[t + i] = data;
+ }
+ }
+
+ /* Notify the grid, so it can call any functions */
+ grid.notify(t, n);
+ }
+ }
+ }
+
+ self.input = function input(beg, items, mapping) {
+ process(beg, items, mapping);
+ stash(beg, items, mapping);
+ };
+
+ self.close = function () {
+ for (const id in registered) {
+ const grid = registered[id];
+ if (grid?.grid)
+ grid.grid.remove_sink(self);
+ }
+ };
+ }
+
+ cockpit.series = function series(interval, cache, fetch) {
+ return new SeriesSink(interval, cache, fetch);
+ };
+
+ let unique = 1;
+
+ function SeriesGrid(interval, beg, end) {
+ const self = this;
+
+ /* We can trigger events */
+ event_mixin(self, { });
+
+ const rows = [];
+
+ self.interval = interval;
+ self.beg = 0;
+ self.end = 0;
+
+ /*
+ * Used to populate table data, the values are:
+ * [ callback, row ]
+ */
+ const callbacks = [];
+
+ const sinks = [];
+
+ let suppress = 0;
+
+ const id = "g1-" + unique;
+ unique += 1;
+
+ /* Used while walking */
+ let walking = null;
+ let offset = null;
+
+ self.notify = function notify(x, n) {
+ if (suppress)
+ return;
+ if (x + n > self.end - self.beg)
+ n = (self.end - self.beg) - x;
+ if (n <= 0)
+ return;
+ const jlen = callbacks.length;
+ for (let j = 0; j < jlen; j++) {
+ const callback = callbacks[j][0];
+ const row = callbacks[j][1];
+ callback.call(self, row, x, n);
+ }
+
+ self.dispatchEvent("notify", x, n);
+ };
+
+ self.add = function add(/* sink, path */) {
+ const row = [];
+ rows.push(row);
+
+ /* Called as add(sink, path) */
+ if (is_object(arguments[0])) {
+ const sink = arguments[0].series || arguments[0];
+
+ /* The path argument can be an array, or a dot separated string */
+ let path = arguments[1];
+ if (!path)
+ path = [];
+ else if (typeof (path) === "string")
+ path = path.split(".");
+
+ const links = sink._register(self, id);
+ if (!links.length)
+ sinks.push({ sink, links });
+ links.push([path, row]);
+
+ /* Called as add(callback) */
+ } else if (is_function(arguments[0])) {
+ const cb = [arguments[0], row];
+ if (arguments[1] === true)
+ callbacks.unshift(cb);
+ else
+ callbacks.push(cb);
+
+ /* Not called as add() */
+ } else if (arguments.length !== 0) {
+ throw Error("invalid args to grid.add()");
+ }
+
+ return row;
+ };
+
+ self.remove = function remove(row) {
+ /* Remove from the sinks */
+ let ilen = sinks.length;
+ for (let i = 0; i < ilen; i++) {
+ const jlen = sinks[i].links.length;
+ for (let j = 0; j < jlen; j++) {
+ if (sinks[i].links[j][1] === row) {
+ sinks[i].links.splice(j, 1);
+ break;
+ }
+ }
+ }
+
+ /* Remove from our list of rows */
+ ilen = rows.length;
+ for (let i = 0; i < ilen; i++) {
+ if (rows[i] === row) {
+ rows.splice(i, 1);
+ break;
+ }
+ }
+ };
+
+ self.remove_sink = function remove_sink(sink) {
+ const len = sinks.length;
+ for (let i = 0; i < len; i++) {
+ if (sinks[i].sink === sink) {
+ sinks[i].links.remove();
+ sinks.splice(i, 1);
+ break;
+ }
+ }
+ };
+
+ self.sync = function sync(for_walking) {
+ /* Suppress notifications */
+ suppress++;
+
+ /* Ask all sinks to load data */
+ const len = sinks.length;
+ for (let i = 0; i < len; i++) {
+ const sink = sinks[i].sink;
+ sink.load(self.beg, self.end, for_walking);
+ }
+
+ suppress--;
+
+ /* Notify for all rows */
+ self.notify(0, self.end - self.beg);
+ };
+
+ function move_internal(beg, end, for_walking) {
+ if (end === undefined)
+ end = beg + (self.end - self.beg);
+
+ if (end < beg)
+ beg = end;
+
+ self.beg = beg;
+ self.end = end;
+
+ if (!rows.length)
+ return;
+
+ rows.forEach(function(row) {
+ row.length = 0;
+ });
+
+ self.sync(for_walking);
+ }
+
+ function stop_walking() {
+ window.clearInterval(walking);
+ walking = null;
+ offset = null;
+ }
+
+ self.move = function move(beg, end) {
+ stop_walking();
+ /* Some code paths use now twice.
+ * They should use the same value.
+ */
+ let now = null;
+
+ /* Treat negative numbers relative to now */
+ if (beg === undefined) {
+ beg = 0;
+ } else if (is_negative(beg)) {
+ now = Date.now();
+ beg = Math.floor(now / self.interval) + beg;
+ }
+ if (end !== undefined && is_negative(end)) {
+ if (now === null)
+ now = Date.now();
+ end = Math.floor(now / self.interval) + end;
+ }
+
+ move_internal(beg, end, false);
+ };
+
+ self.walk = function walk() {
+ /* Don't overflow 32 signed bits with the interval since
+ * many browsers will mishandle it. This means that plots
+ * that would make about one step every month don't walk
+ * at all, but I guess that is ok.
+ *
+ * For example,
+ * https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
+ * says:
+ *
+ * Browsers including Internet Explorer, Chrome,
+ * Safari, and Firefox store the delay as a 32-bit
+ * signed Integer internally. This causes an Integer
+ * overflow when using delays larger than 2147483647,
+ * resulting in the timeout being executed immediately.
+ */
+
+ const start = Date.now();
+ if (self.interval > 2000000000)
+ return;
+
+ stop_walking();
+ offset = start - self.beg * self.interval;
+ walking = window.setInterval(function() {
+ const now = Date.now();
+ move_internal(Math.floor((now - offset) / self.interval), undefined, true);
+ }, self.interval);
+ };
+
+ self.close = function close() {
+ stop_walking();
+ while (sinks.length)
+ (sinks.pop()).links.remove();
+ };
+
+ self.move(beg, end);
+ }
+
+ cockpit.grid = function grid(interval, beg, end) {
+ return new SeriesGrid(interval, beg, end);
+ };
+
+ /* --------------------------------------------------------------------
+ * Basic utilities.
+ */
+
+ function BasicError(problem, message) {
+ this.problem = problem;
+ this.message = message || cockpit.message(problem);
+ this.toString = function() {
+ return this.message;
+ };
+ }
+
+ cockpit.logout = function logout(reload, reason) {
+ /* fully clear session storage */
+ cockpit.sessionStorage.clear(true);
+
+ /* Only clean application data from localStorage,
+ * except for login-data. Clear that completely */
+ cockpit.localStorage.removeItem('login-data', true);
+ cockpit.localStorage.clear(false);
+
+ if (reload !== false)
+ reload_after_disconnect = true;
+ ensure_transport(function(transport) {
+ if (!transport.send_control({ command: "logout", disconnect: true }))
+ window.location.reload(reload_after_disconnect);
+ });
+ window.sessionStorage.setItem("logout-intent", "explicit");
+ if (reason)
+ window.sessionStorage.setItem("logout-reason", reason);
+ };
+
+ /* Not public API ... yet? */
+ cockpit.drop_privileges = function drop_privileges() {
+ ensure_transport(function(transport) {
+ transport.send_control({ command: "logout", disconnect: false });
+ });
+ };
+
+ /* ---------------------------------------------------------------------
+ * User and system information
+ */
+
+ cockpit.info = { };
+ event_mixin(cockpit.info, { });
+
+ init_callback = function(options) {
+ if (options.system)
+ Object.assign(cockpit.info, options.system);
+ if (options.system)
+ cockpit.info.dispatchEvent("changed");
+ };
+
+ let the_user = null;
+ cockpit.user = function () {
+ const dfd = cockpit.defer();
+ if (!the_user) {
+ const dbus = cockpit.dbus(null, { bus: "internal" });
+ dbus.call("/user", "org.freedesktop.DBus.Properties", "GetAll",
+ ["cockpit.User"], { type: "s" })
+ .then(([user]) => {
+ the_user = {
+ id: user.Id.v,
+ name: user.Name.v,
+ full_name: user.Full.v,
+ groups: user.Groups.v,
+ home: user.Home.v,
+ shell: user.Shell.v
+ };
+ dfd.resolve(the_user);
+ })
+ .catch(ex => dfd.reject(ex))
+ .finally(() => dbus.close());
+ } else {
+ dfd.resolve(the_user);
+ }
+
+ return dfd.promise;
+ };
+
+ /* ------------------------------------------------------------------------
+ * Override for broken browser behavior
+ */
+
+ document.addEventListener("click", function(ev) {
+ if (ev.target.classList && in_array(ev.target.classList, 'disabled'))
+ ev.stopPropagation();
+ }, true);
+
+ /* ------------------------------------------------------------------------
+ * Cockpit location
+ */
+
+ /* HACK: Mozilla will unescape 'window.location.hash' before returning
+ * it, which is broken.
+ *
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=135309
+ */
+
+ let last_loc = null;
+
+ function get_window_location_hash() {
+ return (window.location.href.split('#')[1] || '');
+ }
+
+ function Location() {
+ const self = this;
+ const application = cockpit.transport.application();
+ self.url_root = url_root || "";
+
+ if (window.mock?.url_root)
+ self.url_root = window.mock.url_root;
+
+ if (application.indexOf("cockpit+=") === 0) {
+ if (self.url_root)
+ self.url_root += '/';
+ self.url_root = self.url_root + application.replace("cockpit+", '');
+ }
+
+ const href = get_window_location_hash();
+ const options = { };
+ self.path = decode(href, options);
+
+ function decode_path(input) {
+ const parts = input.split('/').map(decodeURIComponent);
+ let result, i;
+ let pre_parts = [];
+
+ if (self.url_root)
+ pre_parts = self.url_root.split('/').map(decodeURIComponent);
+
+ if (input && input[0] !== "/") {
+ result = [].concat(self.path);
+ result.pop();
+ result = result.concat(parts);
+ } else {
+ result = parts;
+ }
+
+ result = resolve_path_dots(result);
+ for (i = 0; i < pre_parts.length; i++) {
+ if (pre_parts[i] !== result[i])
+ break;
+ }
+ if (i == pre_parts.length)
+ result.splice(0, pre_parts.length);
+
+ return result;
+ }
+
+ function encode(path, options, with_root) {
+ if (typeof path == "string")
+ path = decode_path(path);
+
+ let href = "/" + path.map(encodeURIComponent).join("/");
+ if (with_root && self.url_root && href.indexOf("/" + self.url_root + "/") !== 0)
+ href = "/" + self.url_root + href;
+
+ /* Undo unnecessary encoding of these */
+ href = href.replaceAll("%40", "@");
+ href = href.replaceAll("%3D", "=");
+ href = href.replaceAll("%2B", "+");
+ href = href.replaceAll("%23", "#");
+
+ let opt;
+ const query = [];
+ function push_option(v) {
+ query.push(encodeURIComponent(opt) + "=" + encodeURIComponent(v));
+ }
+
+ if (options) {
+ for (opt in options) {
+ let value = options[opt];
+ if (!Array.isArray(value))
+ value = [value];
+ value.forEach(push_option);
+ }
+ if (query.length > 0)
+ href += "?" + query.join("&");
+ }
+ return href;
+ }
+
+ function decode(href, options) {
+ if (href[0] == '#')
+ href = href.substr(1);
+
+ const pos = href.indexOf('?');
+ const first = (pos === -1) ? href : href.substr(0, pos);
+ const path = decode_path(first);
+ if (pos !== -1 && options) {
+ href.substring(pos + 1).split("&")
+ .forEach(function(opt) {
+ const parts = opt.split('=');
+ const name = decodeURIComponent(parts[0]);
+ const value = decodeURIComponent(parts[1]);
+ if (options[name]) {
+ let last = options[name];
+ if (!Array.isArray(value))
+ last = options[name] = [last];
+ last.push(value);
+ } else {
+ options[name] = value;
+ }
+ });
+ }
+
+ return path;
+ }
+
+ function href_for_go_or_replace(/* ... */) {
+ let href;
+ if (arguments.length == 1 && arguments[0] instanceof Location) {
+ href = String(arguments[0]);
+ } else if (typeof arguments[0] == "string") {
+ const options = arguments[1] || { };
+ href = encode(decode(arguments[0], options), options);
+ } else {
+ href = encode.apply(self, arguments);
+ }
+ return href;
+ }
+
+ function replace(/* ... */) {
+ if (self !== last_loc)
+ return;
+ const href = href_for_go_or_replace.apply(self, arguments);
+ window.location.replace(window.location.pathname + '#' + href);
+ }
+
+ function go(/* ... */) {
+ if (self !== last_loc)
+ return;
+ const href = href_for_go_or_replace.apply(self, arguments);
+ window.location.hash = '#' + href;
+ }
+
+ Object.defineProperties(self, {
+ path: {
+ enumerable: true,
+ writable: false,
+ value: self.path
+ },
+ options: {
+ enumerable: true,
+ writable: false,
+ value: options
+ },
+ href: {
+ enumerable: true,
+ value: href
+ },
+ go: { value: go },
+ replace: { value: replace },
+ encode: { value: encode },
+ decode: { value: decode },
+ toString: { value: function() { return href } }
+ });
+ }
+
+ Object.defineProperty(cockpit, "location", {
+ enumerable: true,
+ get: function() {
+ if (!last_loc || last_loc.href !== get_window_location_hash())
+ last_loc = new Location();
+ return last_loc;
+ },
+ set: function(v) {
+ cockpit.location.go(v);
+ }
+ });
+
+ window.addEventListener("hashchange", function() {
+ last_loc = null;
+ let hash = window.location.hash;
+ if (hash.indexOf("#") === 0)
+ hash = hash.substring(1);
+ cockpit.hint("location", { hash });
+ cockpit.dispatchEvent("locationchanged");
+ });
+
+ /* ------------------------------------------------------------------------
+ * Cockpit jump
+ */
+
+ cockpit.jump = function jump(path, host) {
+ if (Array.isArray(path))
+ path = "/" + path.map(encodeURIComponent).join("/")
+.replaceAll("%40", "@")
+.replaceAll("%3D", "=")
+.replaceAll("%2B", "+");
+ else
+ path = "" + path;
+
+ /* When host is not given (undefined), use current transport's host. If
+ * it is null, use localhost.
+ */
+ if (host === undefined)
+ host = cockpit.transport.host;
+
+ const options = { command: "jump", location: path, host };
+ cockpit.transport.inject("\n" + JSON.stringify(options));
+ };
+
+ /* ---------------------------------------------------------------------
+ * Cockpit Page Visibility
+ */
+
+ (function() {
+ let hiddenProp;
+ let hiddenHint = false;
+
+ function visibility_change() {
+ let value = document[hiddenProp];
+ if (!hiddenProp || typeof value === "undefined")
+ value = false;
+ if (value === false)
+ value = hiddenHint;
+ if (cockpit.hidden !== value) {
+ cockpit.hidden = value;
+ cockpit.dispatchEvent("visibilitychange");
+ }
+ }
+
+ if (typeof document.hidden !== "undefined") {
+ hiddenProp = "hidden";
+ document.addEventListener("visibilitychange", visibility_change);
+ } else if (typeof document.mozHidden !== "undefined") {
+ hiddenProp = "mozHidden";
+ document.addEventListener("mozvisibilitychange", visibility_change);
+ } else if (typeof document.msHidden !== "undefined") {
+ hiddenProp = "msHidden";
+ document.addEventListener("msvisibilitychange", visibility_change);
+ } else if (typeof document.webkitHidden !== "undefined") {
+ hiddenProp = "webkitHidden";
+ document.addEventListener("webkitvisibilitychange", visibility_change);
+ }
+
+ /*
+ * Wait for changes in visibility of just our iframe. These are delivered
+ * via a hint message from the parent. For now we are the only handler of
+ * hint messages, so this is implemented rather simply on purpose.
+ */
+ process_hints = function(data) {
+ if ("hidden" in data) {
+ hiddenHint = data.hidden;
+ visibility_change();
+ }
+ };
+
+ /* The first time */
+ visibility_change();
+ }());
+
+ /* ---------------------------------------------------------------------
+ * Spawning
+ */
+
+ function ProcessError(options, name) {
+ this.problem = options.problem || null;
+ this.exit_status = options["exit-status"];
+ if (this.exit_status === undefined)
+ this.exit_status = null;
+ this.exit_signal = options["exit-signal"];
+ if (this.exit_signal === undefined)
+ this.exit_signal = null;
+ this.message = options.message;
+
+ if (this.message === undefined) {
+ if (this.problem)
+ this.message = cockpit.message(options.problem);
+ else if (this.exit_signal !== null)
+ this.message = cockpit.format(_("$0 killed with signal $1"), name, this.exit_signal);
+ else if (this.exit_status !== undefined)
+ this.message = cockpit.format(_("$0 exited with code $1"), name, this.exit_status);
+ else
+ this.message = cockpit.format(_("$0 failed"), name);
+ } else {
+ this.message = this.message.trim();
+ }
+
+ this.toString = function() {
+ return this.message;
+ };
+ }
+
+ function spawn_debug() {
+ if (window.debugging == "all" || window.debugging?.includes("spawn"))
+ console.debug.apply(console, arguments);
+ }
+
+ /* public */
+ cockpit.spawn = function(command, options) {
+ const dfd = cockpit.defer();
+
+ const args = { payload: "stream", spawn: [] };
+ if (command instanceof Array) {
+ for (let i = 0; i < command.length; i++)
+ args.spawn.push(String(command[i]));
+ } else {
+ args.spawn.push(String(command));
+ }
+ if (options !== undefined)
+ Object.assign(args, options);
+
+ spawn_debug("process spawn:", JSON.stringify(args.spawn));
+
+ const name = args.spawn[0] || "process";
+ const channel = cockpit.channel(args);
+
+ /* Callback that wants a stream response, see below */
+ const buffer = channel.buffer(null);
+
+ channel.addEventListener("close", function(event, options) {
+ const data = buffer.squash();
+ spawn_debug("process closed:", JSON.stringify(options));
+ if (data)
+ spawn_debug("process output:", data);
+ if (options.message !== undefined)
+ spawn_debug("process error:", options.message);
+
+ if (options.problem)
+ dfd.reject(new ProcessError(options, name));
+ else if (options["exit-status"] || options["exit-signal"])
+ dfd.reject(new ProcessError(options, name), data);
+ else if (options.message !== undefined)
+ dfd.resolve(data, options.message);
+ else
+ dfd.resolve(data);
+ });
+
+ const ret = dfd.promise;
+ ret.stream = function(callback) {
+ buffer.callback = callback.bind(ret);
+ return this;
+ };
+
+ ret.input = function(message, stream) {
+ if (message !== null && message !== undefined) {
+ spawn_debug("process input:", message);
+ iterate_data(message, function(data) {
+ channel.send(data);
+ });
+ }
+ if (!stream)
+ channel.control({ command: "done" });
+ return this;
+ };
+
+ ret.close = function(problem) {
+ spawn_debug("process closing:", problem);
+ if (channel.valid)
+ channel.close(problem);
+ return this;
+ };
+
+ return ret;
+ };
+
+ /* public */
+ cockpit.script = function(script, args, options) {
+ if (!options && is_plain_object(args)) {
+ options = args;
+ args = [];
+ }
+ const command = ["/bin/sh", "-c", script, "--"];
+ command.push.apply(command, args);
+ return cockpit.spawn(command, options);
+ };
+
+ function dbus_debug() {
+ if (window.debugging == "all" || window.debugging?.includes("dbus"))
+ console.debug.apply(console, arguments);
+ }
+
+ function DBusError(arg, arg1) {
+ if (typeof (arg) == "string") {
+ this.problem = arg;
+ this.name = null;
+ this.message = arg1 || cockpit.message(arg);
+ } else {
+ this.problem = null;
+ this.name = arg[0];
+ this.message = arg[1][0] || arg[0];
+ }
+ this.toString = function() {
+ return this.message;
+ };
+ }
+
+ function DBusCache() {
+ const self = this;
+
+ let callbacks = [];
+ self.data = { };
+ self.meta = { };
+
+ self.connect = function connect(path, iface, callback, first) {
+ const cb = [path, iface, callback];
+ if (first)
+ callbacks.unshift(cb);
+ else
+ callbacks.push(cb);
+ return {
+ remove: function remove() {
+ const length = callbacks.length;
+ for (let i = 0; i < length; i++) {
+ const cb = callbacks[i];
+ if (cb[0] === path && cb[1] === iface && cb[2] === callback) {
+ delete cb[i];
+ break;
+ }
+ }
+ }
+ };
+ };
+
+ function emit(path, iface, props) {
+ const copy = callbacks.slice();
+ const length = copy.length;
+ for (let i = 0; i < length; i++) {
+ const cb = copy[i];
+ if ((!cb[0] || cb[0] === path) &&
+ (!cb[1] || cb[1] === iface)) {
+ cb[2](props, path);
+ }
+ }
+ }
+
+ self.update = function update(path, iface, props) {
+ if (!self.data[path])
+ self.data[path] = { };
+ if (!self.data[path][iface])
+ self.data[path][iface] = props;
+ else
+ props = Object.assign(self.data[path][iface], props);
+ emit(path, iface, props);
+ };
+
+ self.remove = function remove(path, iface) {
+ if (self.data[path]) {
+ delete self.data[path][iface];
+ emit(path, iface, null);
+ }
+ };
+
+ self.lookup = function lookup(path, iface) {
+ if (self.data[path])
+ return self.data[path][iface];
+ return undefined;
+ };
+
+ self.each = function each(iface, callback) {
+ for (const path in self.data) {
+ for (const ifa in self.data[path]) {
+ if (ifa == iface)
+ callback(self.data[path][iface], path);
+ }
+ }
+ };
+
+ self.close = function close() {
+ self.data = { };
+ const copy = callbacks;
+ callbacks = [];
+ const length = copy.length;
+ for (let i = 0; i < length; i++)
+ copy[i].callback();
+ };
+ }
+
+ function DBusProxy(client, cache, iface, path, options) {
+ const self = this;
+ event_mixin(self, { });
+
+ let valid = false;
+ let defined = false;
+ const waits = cockpit.defer();
+
+ /* No enumeration on these properties */
+ Object.defineProperties(self, {
+ client: { value: client, enumerable: false, writable: false },
+ path: { value: path, enumerable: false, writable: false },
+ iface: { value: iface, enumerable: false, writable: false },
+ valid: { get: function() { return valid }, enumerable: false },
+ wait: {
+ enumerable: false,
+ writable: false,
+ value: function(func) {
+ if (func)
+ waits.promise.always(func);
+ return waits.promise;
+ }
+ },
+ call: {
+ value: function(name, args, options) { return client.call(path, iface, name, args, options) },
+ enumerable: false,
+ writable: false
+ },
+ data: { value: { }, enumerable: false }
+ });
+
+ if (!options)
+ options = { };
+
+ function define() {
+ if (!cache.meta[iface])
+ return;
+
+ const meta = cache.meta[iface];
+ defined = true;
+
+ Object.keys(meta.methods || { }).forEach(function(name) {
+ if (name[0].toLowerCase() == name[0])
+ return; /* Only map upper case */
+
+ /* Again, make sure these don't show up in enumerations */
+ Object.defineProperty(self, name, {
+ enumerable: false,
+ value: function() {
+ const dfd = cockpit.defer();
+ client.call(path, iface, name, Array.prototype.slice.call(arguments))
+ .done(function(reply) { dfd.resolve.apply(dfd, reply) })
+ .fail(function(ex) { dfd.reject(ex) });
+ return dfd.promise;
+ }
+ });
+ });
+
+ Object.keys(meta.properties || { }).forEach(function(name) {
+ if (name[0].toLowerCase() == name[0])
+ return; /* Only map upper case */
+
+ const config = {
+ enumerable: true,
+ get: function() { return self.data[name] },
+ set: function(v) { throw Error(name + "is not writable") }
+ };
+
+ const prop = meta.properties[name];
+ if (prop.flags && prop.flags.indexOf('w') !== -1) {
+ config.set = function(v) {
+ client.call(path, "org.freedesktop.DBus.Properties", "Set",
+ [iface, name, cockpit.variant(prop.type, v)])
+ .fail(function(ex) {
+ console.log("Couldn't set " + iface + " " + name +
+ " at " + path + ": " + ex);
+ });
+ };
+ }
+
+ /* Again, make sure these don't show up in enumerations */
+ Object.defineProperty(self, name, config);
+ });
+ }
+
+ function update(props) {
+ if (props) {
+ Object.assign(self.data, props);
+ if (!defined)
+ define();
+ valid = true;
+ } else {
+ valid = false;
+ }
+ self.dispatchEvent("changed", props);
+ }
+
+ cache.connect(path, iface, update, true);
+ update(cache.lookup(path, iface));
+
+ function signal(path, iface, name, args) {
+ self.dispatchEvent("signal", name, args);
+ if (name[0].toLowerCase() != name[0]) {
+ args = args.slice();
+ args.unshift(name);
+ self.dispatchEvent.apply(self, args);
+ }
+ }
+
+ client.subscribe({ path, interface: iface }, signal, options.subscribe !== false);
+
+ function waited(ex) {
+ if (valid)
+ waits.resolve();
+ else
+ waits.reject(ex);
+ }
+
+ /* If watching then do a proper watch, otherwise object is done */
+ if (options.watch !== false)
+ client.watch({ path, interface: iface }).always(waited);
+ else
+ waited();
+ }
+
+ function DBusProxies(client, cache, iface, path_namespace, options) {
+ const self = this;
+ event_mixin(self, { });
+
+ let waits;
+
+ Object.defineProperties(self, {
+ client: { value: client, enumerable: false, writable: false },
+ iface: { value: iface, enumerable: false, writable: false },
+ path_namespace: { value: path_namespace, enumerable: false, writable: false },
+ wait: {
+ enumerable: false,
+ writable: false,
+ value: function(func) {
+ if (func)
+ waits.always(func);
+ return waits;
+ }
+ }
+ });
+
+ /* Subscribe to signals once for all proxies */
+ const match = { interface: iface, path_namespace };
+
+ /* Callbacks added by proxies */
+ client.subscribe(match);
+
+ /* Watch for property changes */
+ if (options.watch !== false) {
+ waits = client.watch(match);
+ } else {
+ waits = cockpit.defer().resolve().promise;
+ }
+
+ /* Already added watch/subscribe, tell proxies not to */
+ options = { watch: false, subscribe: false, ...options };
+
+ function update(props, path) {
+ let proxy = self[path];
+ if (path) {
+ if (!props && proxy) {
+ delete self[path];
+ self.dispatchEvent("removed", proxy);
+ } else if (props) {
+ if (!proxy) {
+ proxy = self[path] = client.proxy(iface, path, options);
+ self.dispatchEvent("added", proxy);
+ }
+ self.dispatchEvent("changed", proxy);
+ }
+ }
+ }
+
+ cache.connect(null, iface, update, false);
+ cache.each(iface, update);
+ }
+
+ function DBusClient(name, options) {
+ const self = this;
+ event_mixin(self, { });
+
+ const args = { };
+ let track = false;
+ let owner = null;
+
+ if (options) {
+ if (options.track)
+ track = true;
+
+ delete options.track;
+ Object.assign(args, options);
+ }
+ args.payload = "dbus-json3";
+ if (name)
+ args.name = name;
+ self.options = options;
+ self.unique_name = null;
+
+ dbus_debug("dbus open: ", args);
+
+ let channel = cockpit.channel(args);
+ const subscribers = { };
+ let calls = { };
+ let cache;
+
+ /* The problem we closed with */
+ let closed;
+
+ self.constructors = { "*": DBusProxy };
+
+ /* Allows waiting on the channel if necessary */
+ self.wait = channel.wait;
+
+ function ensure_cache() {
+ if (!cache)
+ cache = new DBusCache();
+ }
+
+ function send(payload) {
+ if (channel?.valid) {
+ dbus_debug("dbus:", payload);
+ channel.send(payload);
+ return true;
+ }
+ return false;
+ }
+
+ function matches(signal, match) {
+ if (match.path && signal[0] !== match.path)
+ return false;
+ if (match.path_namespace && signal[0].indexOf(match.path_namespace) !== 0)
+ return false;
+ if (match.interface && signal[1] !== match.interface)
+ return false;
+ if (match.member && signal[2] !== match.member)
+ return false;
+ if (match.arg0 && (!signal[3] || signal[3][0] !== match.arg0))
+ return false;
+ return true;
+ }
+
+ function on_message(event, payload) {
+ dbus_debug("dbus:", payload);
+ let msg;
+ try {
+ msg = JSON.parse(payload);
+ } catch (ex) {
+ console.warn("received invalid dbus json message:", ex);
+ }
+ if (msg === undefined) {
+ channel.close({ problem: "protocol-error" });
+ return;
+ }
+ const dfd = (msg.id !== undefined) ? calls[msg.id] : undefined;
+ if (msg.reply) {
+ if (dfd) {
+ const options = { };
+ if (msg.type)
+ options.type = msg.type;
+ if (msg.flags)
+ options.flags = msg.flags;
+ dfd.resolve(msg.reply[0] || [], options);
+ delete calls[msg.id];
+ }
+ return;
+ } else if (msg.error) {
+ if (dfd) {
+ dfd.reject(new DBusError(msg.error));
+ delete calls[msg.id];
+ }
+ return;
+ }
+
+ /*
+ * The above promise resolutions or failures are triggered via
+ * later_invoke(). In order to preserve ordering guarantees we
+ * also have to process other events that way too.
+ */
+ later_invoke(function() {
+ if (msg.signal) {
+ for (const id in subscribers) {
+ const subscription = subscribers[id];
+ if (subscription.callback) {
+ if (matches(msg.signal, subscription.match))
+ subscription.callback.apply(self, msg.signal);
+ }
+ }
+ } else if (msg.notify) {
+ notify(msg.notify);
+ } else if (msg.meta) {
+ meta(msg.meta);
+ } else if (msg.owner !== undefined) {
+ self.dispatchEvent("owner", msg.owner);
+
+ /*
+ * We won't get this signal with the same
+ * owner twice so if we've seen an owner
+ * before that means it has changed.
+ */
+ if (track && owner)
+ self.close();
+
+ owner = msg.owner;
+ } else {
+ dbus_debug("received unexpected dbus json message:", payload);
+ }
+ });
+ }
+
+ function meta(data) {
+ ensure_cache();
+ Object.assign(cache.meta, data);
+ self.dispatchEvent("meta", data);
+ }
+
+ function notify(data) {
+ ensure_cache();
+ for (const path in data) {
+ for (const iface in data[path]) {
+ const props = data[path][iface];
+ if (!props)
+ cache.remove(path, iface);
+ else
+ cache.update(path, iface, props);
+ }
+ }
+ self.dispatchEvent("notify", data);
+ }
+
+ this.notify = notify;
+
+ function close_perform(options) {
+ closed = options.problem || "disconnected";
+ const outstanding = calls;
+ calls = { };
+ for (const id in outstanding) {
+ outstanding[id].reject(new DBusError(closed, options.message));
+ }
+ self.dispatchEvent("close", options);
+ }
+
+ this.close = function close(options) {
+ if (typeof options == "string")
+ options = { problem: options };
+ if (!options)
+ options = { };
+ if (channel)
+ channel.close(options);
+ else
+ close_perform(options);
+ };
+
+ function on_ready(event, message) {
+ dbus_debug("dbus ready:", options);
+ self.unique_name = message["unique-name"];
+ }
+
+ function on_close(event, options) {
+ dbus_debug("dbus close:", options);
+ channel.removeEventListener("ready", on_ready);
+ channel.removeEventListener("message", on_message);
+ channel.removeEventListener("close", on_close);
+ channel = null;
+ close_perform(options);
+ }
+
+ channel.addEventListener("ready", on_ready);
+ channel.addEventListener("message", on_message);
+ channel.addEventListener("close", on_close);
+
+ let last_cookie = 1;
+
+ this.call = function call(path, iface, method, args, options) {
+ const dfd = cockpit.defer();
+ const id = String(last_cookie);
+ last_cookie++;
+ const method_call = {
+ ...options,
+ call: [path, iface, method, args || []],
+ id
+ };
+
+ const msg = JSON.stringify(method_call);
+ if (send(msg))
+ calls[id] = dfd;
+ else
+ dfd.reject(new DBusError(closed));
+
+ return dfd.promise;
+ };
+
+ self.signal = function signal(path, iface, member, args, options) {
+ if (!channel || !channel.valid)
+ return;
+
+ const message = { ...options, signal: [path, iface, member, args || []] };
+
+ send(JSON.stringify(message));
+ };
+
+ this.subscribe = function subscribe(match, callback, rule) {
+ const subscription = {
+ match: { ...match },
+ callback
+ };
+
+ if (rule !== false)
+ send(JSON.stringify({ "add-match": subscription.match }));
+
+ let id;
+ if (callback) {
+ id = String(last_cookie);
+ last_cookie++;
+ subscribers[id] = subscription;
+ }
+
+ return {
+ remove: function() {
+ let prev;
+ if (id) {
+ prev = subscribers[id];
+ if (prev)
+ delete subscribers[id];
+ }
+ if (rule !== false && prev)
+ send(JSON.stringify({ "remove-match": prev.match }));
+ }
+ };
+ };
+
+ self.watch = function watch(path) {
+ const match = is_plain_object(path) ? { ...path } : { path: String(path) };
+
+ const id = String(last_cookie);
+ last_cookie++;
+ const dfd = cockpit.defer();
+
+ const msg = JSON.stringify({ watch: match, id });
+ if (send(msg))
+ calls[id] = dfd;
+ else
+ dfd.reject(new DBusError(closed));
+
+ const ret = dfd.promise;
+ ret.remove = function remove() {
+ if (id in calls) {
+ dfd.reject(new DBusError("cancelled"));
+ delete calls[id];
+ }
+ send(JSON.stringify({ unwatch: match }));
+ };
+ return ret;
+ };
+
+ self.proxy = function proxy(iface, path, options) {
+ if (!iface)
+ iface = name;
+ iface = String(iface);
+ if (!path)
+ path = "/" + iface.replaceAll(".", "/");
+ let Constructor = self.constructors[iface];
+ if (!Constructor)
+ Constructor = self.constructors["*"];
+ if (!options)
+ options = { };
+ ensure_cache();
+ return new Constructor(self, cache, iface, String(path), options);
+ };
+
+ self.proxies = function proxies(iface, path_namespace, options) {
+ if (!iface)
+ iface = name;
+ if (!path_namespace)
+ path_namespace = "/";
+ if (!options)
+ options = { };
+ ensure_cache();
+ return new DBusProxies(self, cache, String(iface), String(path_namespace), options);
+ };
+ }
+
+ /* Well known buses */
+ const shared_dbus = {
+ internal: null,
+ session: null,
+ system: null,
+ };
+
+ /* public */
+ cockpit.dbus = function dbus(name, options) {
+ if (!options)
+ options = { bus: "system" };
+
+ /*
+ * Figure out if this we should use a shared bus.
+ *
+ * This is only the case if a null name *and* the
+ * options are just a simple { "bus": "xxxx" }
+ */
+ const keys = Object.keys(options);
+ const bus = options.bus;
+ const shared = !name && keys.length == 1 && bus in shared_dbus;
+
+ if (shared && shared_dbus[bus])
+ return shared_dbus[bus];
+
+ const client = new DBusClient(name, options);
+
+ /*
+ * Store the shared bus for next time. Override the
+ * close function to only work when a problem is
+ * indicated.
+ */
+ if (shared) {
+ const old_close = client.close;
+ client.close = function() {
+ if (arguments.length > 0)
+ old_close.apply(client, arguments);
+ };
+ client.addEventListener("close", function() {
+ if (shared_dbus[bus] == client)
+ shared_dbus[bus] = null;
+ });
+ shared_dbus[bus] = client;
+ }
+
+ return client;
+ };
+
+ cockpit.variant = function variant(type, value) {
+ return { v: value, t: type };
+ };
+
+ cockpit.byte_array = function byte_array(string) {
+ return window.btoa(string);
+ };
+
+ /* File access
+ */
+
+ cockpit.file = function file(path, options) {
+ options = options || { };
+ const binary = options.binary;
+
+ const self = {
+ path,
+ read,
+ replace,
+ modify,
+
+ watch,
+
+ close
+ };
+
+ const base_channel_options = { ...options };
+ delete base_channel_options.syntax;
+
+ function parse(str) {
+ if (options.syntax?.parse)
+ return options.syntax.parse(str);
+ else
+ return str;
+ }
+
+ function stringify(obj) {
+ if (options.syntax?.stringify)
+ return options.syntax.stringify(obj);
+ else
+ return obj;
+ }
+
+ let read_promise = null;
+ let read_channel;
+
+ function read() {
+ if (read_promise)
+ return read_promise;
+
+ const dfd = cockpit.defer();
+ const opts = {
+ ...base_channel_options,
+ payload: "fsread1",
+ path
+ };
+
+ function try_read() {
+ read_channel = cockpit.channel(opts);
+ const content_parts = [];
+ read_channel.addEventListener("message", function (event, message) {
+ content_parts.push(message);
+ });
+ read_channel.addEventListener("close", function (event, message) {
+ read_channel = null;
+
+ if (message.problem == "change-conflict") {
+ try_read();
+ return;
+ }
+
+ read_promise = null;
+
+ if (message.problem) {
+ const error = new BasicError(message.problem, message.message);
+ fire_watch_callbacks(null, null, error);
+ dfd.reject(error);
+ return;
+ }
+
+ let content;
+ if (message.tag == "-")
+ content = null;
+ else {
+ try {
+ content = parse(join_data(content_parts, binary));
+ } catch (e) {
+ fire_watch_callbacks(null, null, e);
+ dfd.reject(e);
+ return;
+ }
+ }
+
+ fire_watch_callbacks(content, message.tag);
+ dfd.resolve(content, message.tag);
+ });
+ }
+
+ try_read();
+
+ read_promise = dfd.promise;
+ return read_promise;
+ }
+
+ let replace_channel = null;
+
+ function replace(new_content, expected_tag) {
+ const dfd = cockpit.defer();
+
+ let file_content;
+ try {
+ file_content = (new_content === null) ? null : stringify(new_content);
+ } catch (e) {
+ dfd.reject(e);
+ return dfd.promise;
+ }
+
+ if (replace_channel)
+ replace_channel.close("abort");
+
+ const opts = {
+ ...base_channel_options,
+ payload: "fsreplace1",
+ path,
+ tag: expected_tag
+ };
+ replace_channel = cockpit.channel(opts);
+
+ replace_channel.addEventListener("close", function (event, message) {
+ replace_channel = null;
+ if (message.problem) {
+ dfd.reject(new BasicError(message.problem, message.message));
+ } else {
+ fire_watch_callbacks(new_content, message.tag);
+ dfd.resolve(message.tag);
+ }
+ });
+
+ iterate_data(file_content, function(data) {
+ replace_channel.send(data);
+ });
+
+ replace_channel.control({ command: "done" });
+ return dfd.promise;
+ }
+
+ function modify(callback, initial_content, initial_tag) {
+ const dfd = cockpit.defer();
+
+ function update(content, tag) {
+ let new_content = callback(content);
+ if (new_content === undefined)
+ new_content = content;
+ replace(new_content, tag)
+ .done(function (new_tag) {
+ dfd.resolve(new_content, new_tag);
+ })
+ .fail(function (error) {
+ if (error.problem == "change-conflict")
+ read_then_update();
+ else
+ dfd.reject(error);
+ });
+ }
+
+ function read_then_update() {
+ read()
+ .done(update)
+ .fail(function (error) {
+ dfd.reject(error);
+ });
+ }
+
+ if (initial_content === undefined)
+ read_then_update();
+ else
+ update(initial_content, initial_tag);
+
+ return dfd.promise;
+ }
+
+ const watch_callbacks = [];
+ let n_watch_callbacks = 0;
+
+ let watch_channel = null;
+ let watch_tag;
+
+ function ensure_watch_channel(options) {
+ if (n_watch_callbacks > 0) {
+ if (watch_channel)
+ return;
+
+ const opts = {
+ payload: "fswatch1",
+ path,
+ superuser: base_channel_options.superuser,
+ };
+ watch_channel = cockpit.channel(opts);
+ watch_channel.addEventListener("message", function (event, message_string) {
+ let message;
+ try {
+ message = JSON.parse(message_string);
+ } catch (e) {
+ message = null;
+ }
+ if (message && message.path == path && message.tag && message.tag != watch_tag) {
+ if (options && options.read !== undefined && !options.read)
+ fire_watch_callbacks(null, message.tag);
+ else
+ read();
+ }
+ });
+ } else {
+ if (watch_channel) {
+ watch_channel.close();
+ watch_channel = null;
+ }
+ }
+ }
+
+ function fire_watch_callbacks(/* content, tag, error */) {
+ watch_tag = arguments[1] || null;
+ invoke_functions(watch_callbacks, self, arguments);
+ }
+
+ function watch(callback, options) {
+ if (callback)
+ watch_callbacks.push(callback);
+ n_watch_callbacks += 1;
+ ensure_watch_channel(options);
+
+ watch_tag = null;
+ read();
+
+ return {
+ remove: function () {
+ if (callback) {
+ const index = watch_callbacks.indexOf(callback);
+ if (index > -1)
+ watch_callbacks[index] = null;
+ }
+ n_watch_callbacks -= 1;
+ ensure_watch_channel(options);
+ }
+ };
+ }
+
+ function close() {
+ if (read_channel)
+ read_channel.close("cancelled");
+ if (replace_channel)
+ replace_channel.close("cancelled");
+ if (watch_channel)
+ watch_channel.close("cancelled");
+ }
+
+ return self;
+ };
+
+ /* ---------------------------------------------------------------------
+ * Localization
+ */
+
+ let po_data = { };
+ let po_plural;
+
+ cockpit.language = "en";
+ cockpit.language_direction = "ltr";
+ const test_l10n = window.localStorage.test_l10n;
+
+ cockpit.locale = function locale(po) {
+ let lang = cockpit.language;
+ let lang_dir = cockpit.language_direction;
+ let header;
+
+ if (po) {
+ Object.assign(po_data, po);
+ header = po[""];
+ } else if (po === null) {
+ po_data = { };
+ }
+
+ if (header) {
+ if (header["plural-forms"])
+ po_plural = header["plural-forms"];
+ if (header.language)
+ lang = header.language;
+ if (header["language-direction"])
+ lang_dir = header["language-direction"];
+ }
+
+ cockpit.language = lang;
+ cockpit.language_direction = lang_dir;
+ };
+
+ cockpit.translate = function translate(/* ... */) {
+ let what;
+
+ /* Called without arguments, entire document */
+ if (arguments.length === 0)
+ what = [document];
+
+ /* Called with a single array like argument */
+ else if (arguments.length === 1 && arguments[0].length)
+ what = arguments[0];
+
+ /* Called with 1 or more element arguments */
+ else
+ what = arguments;
+
+ /* Translate all the things */
+ const wlen = what.length;
+ for (let w = 0; w < wlen; w++) {
+ /* The list of things to translate */
+ let list = null;
+ if (what[w].querySelectorAll)
+ list = what[w].querySelectorAll("[translatable], [translate]");
+ if (!list)
+ continue;
+
+ /* Each element */
+ for (let i = 0; i < list.length; i++) {
+ const el = list[i];
+
+ let val = el.getAttribute("translate") || el.getAttribute("translatable") || "yes";
+ if (val == "no")
+ continue;
+
+ /* Each thing to translate */
+ const tasks = val.split(" ");
+ val = el.getAttribute("translate-context") || el.getAttribute("context");
+ for (let t = 0; t < tasks.length; t++) {
+ if (tasks[t] == "yes" || tasks[t] == "translate")
+ el.textContent = cockpit.gettext(val, el.textContent);
+ else if (tasks[t])
+ el.setAttribute(tasks[t], cockpit.gettext(val, el.getAttribute(tasks[t]) || ""));
+ }
+
+ /* Mark this thing as translated */
+ el.removeAttribute("translatable");
+ el.removeAttribute("translate");
+ }
+ }
+ };
+
+ cockpit.gettext = function gettext(context, string) {
+ /* Missing first parameter */
+ if (arguments.length == 1) {
+ string = context;
+ context = undefined;
+ }
+
+ const key = context ? context + '\u0004' + string : string;
+ if (po_data) {
+ const translated = po_data[key];
+ if (translated?.[1])
+ string = translated[1];
+ }
+
+ if (test_l10n === 'true')
+ return "»" + string + "«";
+
+ return string;
+ };
+
+ function imply(val) {
+ return (val === true ? 1 : val || 0);
+ }
+
+ cockpit.ngettext = function ngettext(context, string1, stringN, num) {
+ /* Missing first parameter */
+ if (arguments.length == 3) {
+ num = stringN;
+ stringN = string1;
+ string1 = context;
+ context = undefined;
+ }
+
+ const key = context ? context + '\u0004' + string1 : string1;
+ if (po_data && po_plural) {
+ const translated = po_data[key];
+ if (translated) {
+ const i = imply(po_plural(num)) + 1;
+ if (translated[i])
+ return translated[i];
+ }
+ }
+ if (num == 1)
+ return string1;
+ return stringN;
+ };
+
+ cockpit.noop = function noop(arg0, arg1) {
+ return arguments[arguments.length - 1];
+ };
+
+ /* Only for _() calls here in the cockpit code */
+ const _ = cockpit.gettext;
+
+ cockpit.message = function message(arg) {
+ if (arg.message)
+ return arg.message;
+
+ let problem = null;
+ if (arg.problem)
+ problem = arg.problem;
+ else
+ problem = arg + "";
+ if (problem == "terminated")
+ return _("Your session has been terminated.");
+ else if (problem == "no-session")
+ return _("Your session has expired. Please log in again.");
+ else if (problem == "access-denied")
+ return _("Not permitted to perform this action.");
+ else if (problem == "authentication-failed")
+ return _("Login failed");
+ else if (problem == "authentication-not-supported")
+ return _("The server refused to authenticate using any supported methods.");
+ else if (problem == "unknown-hostkey")
+ return _("Untrusted host");
+ else if (problem == "unknown-host")
+ return _("Untrusted host");
+ else if (problem == "invalid-hostkey")
+ return _("Host key is incorrect");
+ else if (problem == "internal-error")
+ return _("Internal error");
+ else if (problem == "timeout")
+ return _("Connection has timed out.");
+ else if (problem == "no-cockpit")
+ return _("Cockpit is not installed on the system.");
+ else if (problem == "no-forwarding")
+ return _("Cannot forward login credentials");
+ else if (problem == "disconnected")
+ return _("Server has closed the connection.");
+ else if (problem == "not-supported")
+ return _("Cockpit is not compatible with the software on the system.");
+ else if (problem == "no-host")
+ return _("Cockpit could not contact the given host.");
+ else if (problem == "too-large")
+ return _("Too much data");
+ else
+ return problem;
+ };
+
+ function HttpError(arg0, arg1, message) {
+ this.status = parseInt(arg0, 10);
+ this.reason = arg1;
+ this.message = message || arg1;
+ this.problem = null;
+
+ this.valueOf = function() {
+ return this.status;
+ };
+ this.toString = function() {
+ return this.status + " " + this.message;
+ };
+ }
+
+ function http_debug() {
+ if (window.debugging == "all" || window.debugging?.includes("http"))
+ console.debug.apply(console, arguments);
+ }
+
+ function find_header(headers, name) {
+ if (!headers)
+ return undefined;
+ name = name.toLowerCase();
+ for (const head in headers) {
+ if (head.toLowerCase() == name)
+ return headers[head];
+ }
+ return undefined;
+ }
+
+ function HttpClient(endpoint, options) {
+ const self = this;
+
+ self.options = options;
+ options.payload = "http-stream2";
+
+ const active_requests = [];
+
+ if (endpoint !== undefined) {
+ if (endpoint.indexOf && endpoint.indexOf("/") === 0) {
+ options.unix = endpoint;
+ } else {
+ const port = parseInt(endpoint, 10);
+ if (!isNaN(port))
+ options.port = port;
+ else
+ throw Error("The endpoint must be either a unix path or port number");
+ }
+ }
+
+ if (options.address) {
+ if (!options.capabilities)
+ options.capabilities = [];
+ options.capabilities.push("address");
+ }
+
+ function param(obj) {
+ return Object.keys(obj).map(function(k) {
+ return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]);
+ })
+.join('&')
+.split('%20')
+.join('+'); /* split/join because phantomjs */
+ }
+
+ self.request = function request(req) {
+ const dfd = cockpit.defer();
+ const ret = dfd.promise;
+
+ if (!req.path)
+ req.path = "/";
+ if (!req.method)
+ req.method = "GET";
+ if (req.params) {
+ if (req.path.indexOf("?") === -1)
+ req.path += "?" + param(req.params);
+ else
+ req.path += "&" + param(req.params);
+ }
+ delete req.params;
+
+ const input = req.body;
+ delete req.body;
+
+ const headers = req.headers;
+ delete req.headers;
+
+ Object.assign(req, options);
+
+ /* Combine the headers */
+ if (options.headers && headers)
+ req.headers = { ...options.headers, ...headers };
+ else if (options.headers)
+ req.headers = options.headers;
+ else
+ req.headers = headers;
+
+ http_debug("http request:", JSON.stringify(req));
+
+ /* We need a channel for the request */
+ const channel = cockpit.channel(req);
+
+ if (input !== undefined) {
+ if (input !== "") {
+ http_debug("http input:", input);
+ iterate_data(input, function(data) {
+ channel.send(data);
+ });
+ }
+ http_debug("http done");
+ channel.control({ command: "done" });
+ }
+
+ /* Callbacks that want to stream or get headers */
+ let streamer = null;
+ let responsers = null;
+
+ let resp = null;
+
+ const buffer = channel.buffer(function(data) {
+ /* Fire any streamers */
+ if (resp && resp.status >= 200 && resp.status <= 299 && streamer)
+ return streamer.call(ret, data);
+ return 0;
+ });
+
+ function on_control(event, options) {
+ /* Anyone looking for response details? */
+ if (options.command == "response") {
+ resp = options;
+ if (responsers) {
+ resp.headers = resp.headers || { };
+ invoke_functions(responsers, ret, [resp.status, resp.headers]);
+ }
+ }
+ }
+
+ function on_close(event, options) {
+ const pos = active_requests.indexOf(ret);
+ if (pos >= 0)
+ active_requests.splice(pos, 1);
+
+ if (options.problem) {
+ http_debug("http problem: ", options.problem);
+ dfd.reject(new BasicError(options.problem));
+ } else {
+ const body = buffer.squash();
+
+ /* An error, fail here */
+ if (resp && (resp.status < 200 || resp.status > 299)) {
+ let message;
+ const type = find_header(resp.headers, "Content-Type");
+ if (type && !channel.binary) {
+ if (type.indexOf("text/plain") === 0)
+ message = body;
+ }
+ http_debug("http status: ", resp.status);
+ dfd.reject(new HttpError(resp.status, resp.reason, message), body);
+ } else {
+ http_debug("http done");
+ dfd.resolve(body);
+ }
+ }
+
+ channel.removeEventListener("control", on_control);
+ channel.removeEventListener("close", on_close);
+ }
+
+ channel.addEventListener("control", on_control);
+ channel.addEventListener("close", on_close);
+
+ ret.stream = function(callback) {
+ streamer = callback;
+ return ret;
+ };
+ ret.response = function(callback) {
+ if (responsers === null)
+ responsers = [];
+ responsers.push(callback);
+ return ret;
+ };
+ ret.input = function(message, stream) {
+ if (message !== null && message !== undefined) {
+ http_debug("http input:", message);
+ iterate_data(message, function(data) {
+ channel.send(data);
+ });
+ }
+ if (!stream) {
+ http_debug("http done");
+ channel.control({ command: "done" });
+ }
+ return ret;
+ };
+ ret.close = function(problem) {
+ http_debug("http closing:", problem);
+ channel.close(problem);
+ return ret;
+ };
+
+ active_requests.push(ret);
+ return ret;
+ };
+
+ self.get = function get(path, params, headers) {
+ return self.request({
+ method: "GET",
+ params,
+ path,
+ body: "",
+ headers
+ });
+ };
+
+ self.post = function post(path, body, headers) {
+ headers = headers || { };
+
+ if (is_plain_object(body) || Array.isArray(body)) {
+ body = JSON.stringify(body);
+ if (find_header(headers, "Content-Type") === undefined)
+ headers["Content-Type"] = "application/json";
+ } else if (body === undefined || body === null) {
+ body = "";
+ } else if (typeof body !== "string") {
+ body = String(body);
+ }
+
+ return self.request({
+ method: "POST",
+ path,
+ body,
+ headers
+ });
+ };
+
+ self.close = function close(problem) {
+ const reqs = active_requests.slice();
+ for (let i = 0; i < reqs.length; i++)
+ reqs[i].close(problem);
+ };
+ }
+
+ /* public */
+ cockpit.http = function(endpoint, options) {
+ if (is_plain_object(endpoint) && options === undefined) {
+ options = endpoint;
+ endpoint = undefined;
+ }
+ return new HttpClient(endpoint, options || { });
+ };
+
+ /* ---------------------------------------------------------------------
+ * Permission
+ */
+
+ function check_superuser() {
+ return new Promise((resolve, reject) => {
+ const ch = cockpit.channel({ payload: "null", superuser: "require" });
+ ch.wait()
+ .then(() => resolve(true))
+ .catch(() => resolve(false))
+ .always(() => ch.close());
+ });
+ }
+
+ function Permission(options) {
+ const self = this;
+ event_mixin(self, { });
+
+ const api = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Superuser", "/superuser");
+ api.addEventListener("changed", maybe_reload);
+
+ function maybe_reload() {
+ if (api.valid && self.allowed !== null) {
+ if (self.allowed != (api.Current != "none"))
+ window.location.reload(true);
+ }
+ }
+
+ self.allowed = null;
+ self.user = options ? options.user : null; // pre-fill for unit tests
+ self.is_superuser = options ? options._is_superuser : null; // pre-fill for unit tests
+
+ let group = null;
+ let admin = false;
+
+ if (options)
+ group = options.group;
+
+ if (options?.admin)
+ admin = true;
+
+ function decide(user) {
+ if (user.id === 0)
+ return true;
+
+ if (group)
+ return !!(user.groups || []).includes(group);
+
+ if (admin)
+ return self.is_superuser;
+
+ if (user.id === undefined)
+ return null;
+
+ return false;
+ }
+
+ if (self.user && self.is_superuser !== null) {
+ self.allowed = decide(self.user);
+ } else {
+ Promise.all([cockpit.user(), check_superuser()])
+ .then(([user, is_superuser]) => {
+ self.user = user;
+ self.is_superuser = is_superuser;
+ const allowed = decide(user);
+ if (self.allowed !== allowed) {
+ self.allowed = allowed;
+ maybe_reload();
+ self.dispatchEvent("changed");
+ }
+ });
+ }
+
+ self.close = function close() {
+ /* no-op for now */
+ };
+ }
+
+ cockpit.permission = function permission(arg) {
+ return new Permission(arg);
+ };
+
+ /* ---------------------------------------------------------------------
+ * Metrics
+ *
+ */
+
+ function MetricsChannel(interval, options_list, cache) {
+ const self = this;
+ event_mixin(self, { });
+
+ if (options_list.length === undefined)
+ options_list = [options_list];
+
+ const channels = [];
+ let following = false;
+
+ self.series = cockpit.series(interval, cache, fetch_for_series);
+ self.archives = null;
+ self.meta = null;
+
+ function fetch_for_series(beg, end, for_walking) {
+ if (!for_walking)
+ self.fetch(beg, end);
+ else
+ self.follow();
+ }
+
+ function transfer(options_list, callback, is_archive) {
+ if (options_list.length === 0)
+ return;
+
+ if (!is_archive) {
+ if (following)
+ return;
+ following = true;
+ }
+
+ const options = {
+ payload: "metrics1",
+ interval,
+ source: "internal",
+ ...options_list[0]
+ };
+
+ delete options.archive_source;
+
+ const channel = cockpit.channel(options);
+ channels.push(channel);
+
+ let meta = null;
+ let last = null;
+ let beg;
+
+ channel.addEventListener("close", function(ev, close_options) {
+ if (!is_archive)
+ following = false;
+
+ if (options_list.length > 1 &&
+ (close_options.problem == "not-supported" || close_options.problem == "not-found")) {
+ transfer(options_list.slice(1), callback);
+ } else if (close_options.problem) {
+ if (close_options.problem != "terminated" &&
+ close_options.problem != "disconnected" &&
+ close_options.problem != "authentication-failed" &&
+ (close_options.problem != "not-found" || !is_archive) &&
+ (close_options.problem != "not-supported" || !is_archive)) {
+ console.warn("metrics channel failed: " + close_options.problem);
+ }
+ } else if (is_archive) {
+ if (!self.archives) {
+ self.archives = true;
+ self.dispatchEvent('changed');
+ }
+ }
+ });
+
+ channel.addEventListener("message", function(ev, payload) {
+ const message = JSON.parse(payload);
+
+ /* A meta message? */
+ const message_len = message.length;
+ if (message_len === undefined) {
+ meta = message;
+ let timestamp = 0;
+ if (meta.now && meta.timestamp)
+ timestamp = meta.timestamp + (Date.now() - meta.now);
+ beg = Math.floor(timestamp / interval);
+ callback(beg, meta, null, options_list[0]);
+
+ /* Trigger to outside interest that meta changed */
+ self.meta = meta;
+ self.dispatchEvent('changed');
+
+ /* A data message */
+ } else if (meta) {
+ /* Data decompression */
+ for (let i = 0; i < message_len; i++) {
+ const data = message[i];
+ if (last) {
+ for (let j = 0; j < last.length; j++) {
+ const dataj = data[j];
+ if (dataj === null || dataj === undefined) {
+ data[j] = last[j];
+ } else {
+ const dataj_len = dataj.length;
+ if (dataj_len !== undefined) {
+ const lastj = last[j];
+ const lastj_len = last[j].length;
+ let k;
+ for (k = 0; k < dataj_len; k++) {
+ if (dataj[k] === null)
+ dataj[k] = lastj[k];
+ }
+ for (; k < lastj_len; k++)
+ dataj[k] = lastj[k];
+ }
+ }
+ }
+ }
+ last = data;
+ }
+
+ /* Return the data */
+ callback(beg, meta, message, options_list[0]);
+
+ /* Bump timestamp for the next message */
+ beg += message_len;
+ meta.timestamp += (interval * message_len);
+ }
+ });
+ }
+
+ function drain(beg, meta, message, options) {
+ /* Generate a mapping object if necessary */
+ let mapping = meta.mapping;
+ if (!mapping) {
+ mapping = { };
+ meta.metrics.forEach(function(metric, i) {
+ const map = { "": i };
+ const name = options.metrics_path_names?.[i] ?? metric.name;
+ mapping[name] = map;
+ if (metric.instances) {
+ metric.instances.forEach(function(instance, i) {
+ if (instance === "")
+ instance = "/";
+ map[instance] = { "": i };
+ });
+ }
+ });
+ meta.mapping = mapping;
+ }
+
+ if (message)
+ self.series.input(beg, message, mapping);
+ }
+
+ self.fetch = function fetch(beg, end) {
+ const timestamp = beg * interval - Date.now();
+ const limit = end - beg;
+
+ const archive_options_list = [];
+ for (let i = 0; i < options_list.length; i++) {
+ if (options_list[i].archive_source) {
+ archive_options_list.push({
+ ...options_list[i],
+ source: options_list[i].archive_source,
+ timestamp,
+ limit
+ });
+ }
+ }
+
+ transfer(archive_options_list, drain, true);
+ };
+
+ self.follow = function follow() {
+ transfer(options_list, drain);
+ };
+
+ self.close = function close(options) {
+ const len = channels.length;
+ if (self.series)
+ self.series.close();
+
+ for (let i = 0; i < len; i++)
+ channels[i].close(options);
+ };
+ }
+
+ cockpit.metrics = function metrics(interval, options) {
+ return new MetricsChannel(interval, options);
+ };
+
+ /* ---------------------------------------------------------------------
+ * Ooops handling.
+ *
+ * If we're embedded, send oops to parent frame. Since everything
+ * could be broken at this point, just do it manually, without
+ * involving cockpit.transport or any of that logic.
+ */
+
+ cockpit.oops = function oops() {
+ if (window.parent !== window && window.name.indexOf("cockpit1:") === 0)
+ window.parent.postMessage("\n{ \"command\": \"oops\" }", transport_origin);
+ };
+
+ const old_onerror = window.onerror;
+ window.onerror = function(msg, url, line) {
+ // Errors with url == "" are not logged apparently, so let's
+ // not show the "Oops" for them either.
+ if (url != "")
+ cockpit.oops();
+ if (old_onerror)
+ return old_onerror(msg, url, line);
+ return false;
+ };
+
+ return cockpit;
+}
+
+// Register cockpit object as global, so that it can be used without ES6 modules
+// we need to do that here instead of in pkg/base1/cockpit.js, so that po.js can access cockpit already
+window.cockpit = factory();
+
+export default window.cockpit;
diff --git a/pkg/lib/console.css b/pkg/lib/console.css
new file mode 100644
index 0000000..d2ad1e8
--- /dev/null
+++ b/pkg/lib/console.css
@@ -0,0 +1,16 @@
+@import "xterm/css/xterm.css";
+
+.console-ct > .terminal {
+ display: flex;
+ block-size: 100%;
+}
+
+.terminal:focus .terminal-cursor {
+ border: none;
+ animation: blink 1s step-end infinite;
+}
+
+/* Ensure the console fits to its container (and doesn't attempt to go beyond the limits) */
+.xterm-screen, .xterm-viewport {
+ inline-size: auto !important;
+}
diff --git a/pkg/lib/context-menu.scss b/pkg/lib/context-menu.scss
new file mode 100644
index 0000000..1f1c7df
--- /dev/null
+++ b/pkg/lib/context-menu.scss
@@ -0,0 +1,20 @@
+.contextMenu {
+ position: fixed;
+ /* xterm accessibility tree has z-index 100 and we need to be in front of it
+ * to be able to handle mouse events.
+ */
+ z-index: 101;
+ /* Move the menu under the mouse */
+ transform: translate(calc(-1 * var(--pf-v5-global--spacer--sm)), calc(-1 * var(--pf-v5-global--spacer--sm)));
+
+ &Option .pf-v5-c-menu__item-text {
+ display: flex;
+ gap: var(--pf-v5-global--spacer--sm);
+ justify-content: space-between;
+ min-inline-size: 10rem;
+ }
+
+ &Shortcut {
+ opacity: 0.75;
+ }
+}
diff --git a/pkg/lib/credentials-ssh-private-keys.sh b/pkg/lib/credentials-ssh-private-keys.sh
new file mode 100644
index 0000000..e6ffc84
--- /dev/null
+++ b/pkg/lib/credentials-ssh-private-keys.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+set -u
+
+# The first thing we do is list loaded keys
+loaded=$(ssh-add -L)
+result="$?"
+
+set -e
+
+printf "$loaded"
+
+# Get info for each loaded key
+# ssh-keygen -l -f - is not
+# supported everywhere so use tempfile
+if [ $result -eq 0 ]; then
+ tempfile=$(mktemp)
+ echo "$loaded" | while read line; do
+ echo "$line" > "$tempfile"
+ printf "\v%s\v\v" "$line"
+ ssh-keygen -l -f "$tempfile" || true
+ done
+ rm $tempfile
+fi
+
+# Try to list keys in this directory
+cd "$1" || exit 0
+
+# After that each .pub file gets its on set of blocks
+for file in *.pub; do
+ printf "\v"
+ cat "$file"
+ printf "\v%s\v" "$file"
+ ssh-keygen -l -f "$file" || true
+done
diff --git a/pkg/lib/credentials-ssh-remove-key.sh b/pkg/lib/credentials-ssh-remove-key.sh
new file mode 100644
index 0000000..39ac038
--- /dev/null
+++ b/pkg/lib/credentials-ssh-remove-key.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+set -eu
+
+tempfile=$(mktemp)
+echo "$1" > "$tempfile"
+ret=0
+ssh-add -d "$tempfile" || ret=1
+rm "$tempfile"
+exit $ret
diff --git a/pkg/lib/credentials.js b/pkg/lib/credentials.js
new file mode 100644
index 0000000..d4a12aa
--- /dev/null
+++ b/pkg/lib/credentials.js
@@ -0,0 +1,344 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import lister from "credentials-ssh-private-keys.sh";
+import remove_key from "credentials-ssh-remove-key.sh";
+
+const _ = cockpit.gettext;
+
+function Keys() {
+ const self = this;
+
+ self.path = null;
+ self.items = { };
+
+ let watch = null;
+ let proc = null;
+ let timeout = null;
+
+ cockpit.event_target(this);
+
+ cockpit.user()
+ .then(user => {
+ self.path = user.home + '/.ssh';
+ refresh();
+ });
+
+ function refresh() {
+ function on_message(ev, payload) {
+ const item = JSON.parse(payload);
+ const name = item.path;
+ if (name && name.indexOf("/") === -1 && name.slice(-4) === ".pub") {
+ if (item.event === "present" || item.event === "created" ||
+ item.event === "changed" || item.event === "deleted") {
+ window.clearInterval(timeout);
+ timeout = window.setTimeout(refresh, 100);
+ }
+ }
+ }
+
+ function on_close(ev, data) {
+ watch.removeEventListener("close", on_close);
+ watch.removeEventListener("message", on_message);
+ if (!data.problem || data.problem == "not-found") {
+ watch = null; /* Watch again */
+ } else {
+ console.warn("couldn't watch " + self.path + ": " + (data.message || data.problem));
+ watch = false; /* Don't watch again */
+ }
+ }
+
+ if (watch === null) {
+ watch = cockpit.channel({ payload: "fswatch1", path: self.path });
+ watch.addEventListener("close", on_close);
+ watch.addEventListener("message", on_message);
+ }
+
+ if (proc)
+ return;
+
+ window.clearTimeout(timeout);
+ timeout = null;
+
+ proc = cockpit.script(lister, [self.path], { err: "message" });
+ proc
+ .then(data => process(data))
+ .catch(ex => console.warn("failed to list keys in home directory: " + ex.message))
+ .finally(() => {
+ proc = null;
+
+ if (!timeout)
+ timeout = window.setTimeout(refresh, 5000);
+ });
+ }
+
+ function process(data) {
+ const blocks = data.split('\v');
+ let key;
+ const items = { };
+
+ /* First block is the data from ssh agent */
+ blocks[0].trim().split("\n")
+ .forEach(function(line) {
+ key = parse_key(line, items);
+ if (key)
+ key.loaded = true;
+ });
+
+ /* Next come individual triples of blocks */
+ blocks.slice(1).forEach(function(block, i) {
+ switch (i % 3) {
+ case 0:
+ key = parse_key(block, items);
+ break;
+ case 1:
+ if (key) {
+ block = block.trim();
+ if (block.slice(-4) === ".pub")
+ key.name = block.slice(0, -4);
+ else if (block)
+ key.name = block;
+ else
+ key.agent_only = true;
+ }
+ break;
+ case 2:
+ if (key)
+ parse_info(block, key);
+ break;
+ }
+ });
+
+ self.items = items;
+ self.dispatchEvent("changed");
+ }
+
+ function parse_key(line, items) {
+ const parts = line.trim().split(" ");
+ let id, type, comment;
+
+ /* SSHv1 keys */
+ if (!isNaN(parseInt(parts[0], 10))) {
+ id = parts[2];
+ type = "RSA1";
+ comment = parts.slice(3).join(" ");
+ } else if (parts[0].indexOf("ssh-") === 0) {
+ id = parts[1];
+ type = parts[0].substring(4).toUpperCase();
+ comment = parts.slice(2).join(" ");
+ } else if (parts[0].indexOf("ecdsa-") === 0) {
+ id = parts[1];
+ type = "ECDSA";
+ comment = parts.slice(2).join(" ");
+ } else {
+ return;
+ }
+
+ let key = items[id];
+ if (!key)
+ key = items[id] = { };
+
+ key.type = type;
+ key.comment = comment;
+ key.data = line;
+ return key;
+ }
+
+ function parse_info(line, key) {
+ const parts = line.trim().split(" ")
+ .filter(n => !!n);
+
+ key.size = parseInt(parts[0], 10);
+ if (isNaN(key.size))
+ key.size = null;
+
+ key.fingerprint = parts[1];
+
+ if (!key.name && parts[2] && parts[2].indexOf("/") !== -1)
+ key.name = parts[2];
+ }
+
+ function ensure_ssh_directory(file) {
+ return cockpit.script('dir=$(dirname "$1"); test -e "$dir" || mkdir -m 700 "$dir"', [file]);
+ }
+
+ function run_keygen(file, new_type, old_pass, new_pass, two_pass) {
+ const old_exps = [/.*Enter old passphrase: $/];
+ const new_exps = [/.*Enter passphrase.*/, /.*Enter new passphrase.*/, /.*Enter same passphrase again: $/];
+ const bad_exps = [/.*failed: passphrase is too short.*/];
+
+ return new Promise((resolve, reject) => {
+ let buffer = "";
+ let sent_new = false;
+ let failure = _("No such file or directory");
+
+ if (new_pass !== two_pass) {
+ reject(new Error(_("The passwords do not match.")));
+ return;
+ }
+
+ // Exactly one of new_type or old_pass must be given
+ console.assert((new_type == null) != (old_pass == null));
+
+ const cmd = ["ssh-keygen", "-f", file];
+ if (new_type)
+ cmd.push("-t", new_type);
+ else
+ cmd.push("-p");
+
+ const proc = cockpit.spawn(cmd, { pty: true, environ: ["LC_ALL=C"], err: "out", directory: self.path });
+
+ const timeout = window.setTimeout(() => {
+ failure = _("Prompting via ssh-keygen timed out");
+ proc.close("terminated");
+ }, 10 * 1000);
+
+ proc
+ .stream(data => {
+ buffer += data;
+ if (old_pass) {
+ for (let i = 0; i < old_exps.length; i++) {
+ if (old_exps[i].test(buffer)) {
+ buffer = "";
+ failure = _("Old password not accepted");
+ proc.input(old_pass + "\n", true);
+ return;
+ }
+ }
+ }
+
+ for (let i = 0; i < new_exps.length; i++) {
+ if (new_exps[i].test(buffer)) {
+ buffer = "";
+ proc.input(new_pass + "\n", true);
+ failure = _("Failed to change password");
+ sent_new = true;
+ return;
+ }
+ }
+
+ if (sent_new) {
+ for (let i = 0; i < bad_exps.length; i++) {
+ if (bad_exps[i].test(buffer)) {
+ failure = _("New password was not accepted");
+ return;
+ }
+ }
+ }
+ })
+ .then(resolve)
+ .catch(ex => {
+ if (ex.exit_status)
+ ex = new Error(failure);
+ reject(ex);
+ })
+ .finally(() => window.clearInterval(timeout));
+ });
+ }
+
+ self.change = function change(name, old_pass, new_pass, two_pass) {
+ return run_keygen(name, null, old_pass, new_pass, two_pass);
+ };
+
+ self.create = function create(name, type, new_pass, two_pass) {
+ return ensure_ssh_directory(name)
+ .then(() => run_keygen(name, type, null, new_pass, two_pass));
+ };
+
+ self.get_pubkey = function get_pubkey(name) {
+ return cockpit.file(name + ".pub").read();
+ };
+
+ self.load = function(name, password) {
+ const ask_exp = /.*Enter passphrase for .*/;
+ const perm_exp = /.*UNPROTECTED PRIVATE KEY FILE.*/;
+ const bad_exp = /.*Bad passphrase.*/;
+
+ let buffer = "";
+ let output = "";
+ let failure = _("Not a valid private key");
+ let sent_password = false;
+
+ return new Promise((resolve, reject) => {
+ const proc = cockpit.spawn(["ssh-add", name],
+ { pty: true, environ: ["LC_ALL=C"], err: "out", directory: self.path });
+
+ const timeout = window.setTimeout(() => {
+ failure = _("Prompting via ssh-add timed out");
+ proc.close("terminated");
+ }, 10 * 1000);
+
+ proc
+ .stream(data => {
+ buffer += data;
+ output += data;
+ if (perm_exp.test(buffer)) {
+ failure = _("Invalid file permissions");
+ buffer = "";
+ } else if (ask_exp.test(buffer)) {
+ buffer = "";
+ failure = _("Password not accepted");
+ proc.input(password + "\n", true);
+ sent_password = true;
+ } else if (bad_exp.test(buffer)) {
+ buffer = "";
+ proc.input("\n", true);
+ }
+ })
+ .then(() => {
+ refresh();
+ resolve();
+ })
+ .catch(ex => {
+ console.log(output);
+ if (ex.exit_status)
+ ex = new Error(failure);
+
+ ex.sent_password = sent_password;
+ reject(ex);
+ })
+ .finally(() => window.clearInterval(timeout));
+ });
+ };
+
+ self.unload = function unload(key) {
+ const options = { pty: true, err: "message", directory: self.path };
+
+ const proc = (key.name && !key.agent_only)
+ ? cockpit.spawn(["ssh-add", "-d", key.name], options)
+ : cockpit.script(remove_key, [key.data], options);
+
+ return proc.then(refresh);
+ };
+
+ self.close = function close() {
+ if (watch)
+ watch.close();
+ if (proc)
+ proc.close();
+ window.clearTimeout(timeout);
+ timeout = null;
+ };
+}
+
+export function keys_instance() {
+ return new Keys();
+}
diff --git a/pkg/lib/ct-card.scss b/pkg/lib/ct-card.scss
new file mode 100644
index 0000000..114c659
--- /dev/null
+++ b/pkg/lib/ct-card.scss
@@ -0,0 +1,63 @@
+@use "_global-variables.scss" as *;
+
+/* Rely on the margin from the Card for spacing */
+.ct-card.pf-v5-c-card .table {
+ margin-block-end: 0;
+}
+
+// FIXME: Once PF4 provides a property for removing padding: https://github.com/patternfly/patternfly-react/issues/5606
+.ct-card.pf-v5-c-card .pf-v5-c-card__body.contains-list {
+ padding-inline: 0;
+ padding-block-end: 0;
+
+ > .pf-v5-c-table > :last-child > tr:last-child {
+ border-block-end: none;
+ }
+
+ // Remove excess padding from compact tables toggles
+ // And adjust the padding to left align the toggles with the card header
+ > .pf-v5-c-table {
+ .pf-v5-c-table__toggle {
+ padding-inline-start: 0;
+
+ > .pf-v5-c-button {
+ padding-inline-start: var(--pf-v5-global--spacer--lg);
+ }
+ }
+ }
+}
+
+.ct-card.pf-v5-c-card .pf-v5-c-card__title-text {
+ font-weight: normal;
+ font-size: var(--pf-v5-global--FontSize--2xl);
+}
+
+// Remove excess top padding from top-level empty state in cards,
+// as card headers already add enough space
+.ct-card > .pf-v5-c-card__body > .pf-v5-c-empty-state {
+ --pf-v5-c-empty-state__body--MarginTop: 0;
+ padding-block: 0 var(--pf-v5-global--spacer--md);
+}
+
+.ct-cards-grid {
+ --ct-grid-columns: 2;
+ --pf-v5-l-gallery--GridTemplateColumns: repeat(var(--ct-grid-columns), 1fr);
+
+ > .pf-v5-c-card:not(.ct-card-info) {
+ // Extend all non-info cards to be full width;
+ // let ct-card-info fit 1 column of the grid
+ grid-column: 1 / -1;
+ }
+
+ @media screen and (max-width: $pf-v5-global--breakpoint--lg) {
+ // Shrink to 1 column when space is constrained
+ --ct-grid-columns: 1;
+ }
+}
+
+// Remove redundant padding from embedded toolbars (handled by header)
+// Toolbars in card headers are not a common scenario so no need to upstream this
+.ct-card.pf-v5-c-card .pf-v5-c-toolbar,
+.ct-card.pf-v5-c-card .pf-v5-c-toolbar__content {
+ padding: 0;
+}
diff --git a/pkg/lib/dialogs.jsx b/pkg/lib/dialogs.jsx
new file mode 100644
index 0000000..b63e861
--- /dev/null
+++ b/pkg/lib/dialogs.jsx
@@ -0,0 +1,131 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* DIALOG PRESENTATION PROTOCOL
+ *
+ * Example:
+ *
+ * import { WithDialogs, useDialogs } from "dialogs.jsx";
+ *
+ * const App = () =>
+ * <WithDialogs>
+ * <Page>
+ * <ExampleButton />
+ * </Page>
+ * </WithDialogs>;
+ *
+ * const ExampleButton = () => {
+ * const Dialogs = useDialogs();
+ * return <Button onClick={() => Dialogs.show(<MyDialog />)}>Open dialog</Button>;
+ * };
+ *
+ * const MyDialog = () => {
+ * const Dialogs = useDialogs();
+ * return (
+ * <Modal title="My dialog"
+ * isOpen
+ * onClose={Dialogs.close}>
+ * <p>Hello!</p>
+ * </Modal>);
+ * };
+ *
+ * This does two things: It maintains the state of whether the dialog
+ * is open, and it does it high up in the DOM, in a stable place.
+ * Even if ExampleButton is no longer part of the DOM, the dialog will
+ * stay open and remain useable.
+ *
+ * The "WithDialogs" component enables all its children to show
+ * dialogs. Such a dialog will stay open as long as the WithDialogs
+ * component itself is mounted. Thus, you should put the WithDialogs
+ * component somewhere high up in your component tree, maybe even as
+ * the very top-most component.
+ *
+ * If your Cockpit application has multiple pages and navigation
+ * between these pages is controlled by the browser URL, then each of
+ * these pages should have its own WithDialogs wrapper. This way, a
+ * dialog opened on one page closes when the user navigates away from
+ * that page. To make sure that React maintains separate states for
+ * WithDialogs components, give them unique "key" properties.
+ *
+ * A component that wants to show a dialogs needs to get hold of the
+ * current "Dialogs" context and then call it's "show" method. For a
+ * function component the Dialogs context is returned by
+ * "useDialogs()", as shown above in the example.
+ *
+ * A class component can declare a static context type and then use
+ * "this.context" to find the Dialogs object:
+ *
+ * import { DialogsContext } from "dialogs.jsx";
+ *
+ * class ExampleButton extends React.Component {
+ * static contextType = DialogsContext;
+ *
+ * function render() {
+ * const Dialogs = this.context;
+ * return <Button onClick={() => Dialogs.show(<MyDialog />)}>Open dialog</Button>;
+ * }
+ * }
+ *
+ *
+ * - Dialogs.show(component)
+ *
+ * Calling "Dialogs.show" will render the given component as a direct
+ * child of the inner-most enclosing "WithDialogs" component. The
+ * component is of course intended to be a dialog, such as
+ * Patternfly's "Modal". There is only ever one of these; a second
+ * call to "show" will remove the previously rendered component.
+ * Passing "null" will remove the currently rendered componenet, if any.
+ *
+ * - Dialogs.close()
+ *
+ * Same as "Dialogs.show(null)".
+ */
+
+import React, { useContext, useRef, useState } from "react";
+
+export const DialogsContext = React.createContext();
+export const useDialogs = () => useContext(DialogsContext);
+
+export const WithDialogs = ({ children }) => {
+ const is_open = useRef(false); // synchronous
+ const [dialog, setDialog] = useState(null);
+
+ const Dialogs = {
+ show: component => {
+ if (component && is_open.current)
+ console.error("Dialogs.show() called for",
+ JSON.stringify(component),
+ "while a dialog is already open:",
+ JSON.stringify(dialog));
+ is_open.current = !!component;
+ setDialog(component);
+ },
+ close: () => {
+ is_open.current = false;
+ setDialog(null);
+ },
+ isActive: () => dialog !== null
+ };
+
+ return (
+ <DialogsContext.Provider value={Dialogs}>
+ {children}
+ {dialog}
+ </DialogsContext.Provider>);
+};
diff --git a/pkg/lib/esbuild-cleanup-plugin.js b/pkg/lib/esbuild-cleanup-plugin.js
new file mode 100644
index 0000000..a3a7555
--- /dev/null
+++ b/pkg/lib/esbuild-cleanup-plugin.js
@@ -0,0 +1,17 @@
+import fs from 'fs';
+import path from 'path';
+
+// always start with a fresh dist/ directory, to change between development and production, or clean up gzipped files
+export const cleanPlugin = ({ outdir = './dist', subdir = '' } = {}) => ({
+ name: 'clean-dist',
+ setup(build) {
+ build.onStart(() => {
+ try {
+ fs.rmSync(path.resolve(outdir, subdir), { recursive: true });
+ } catch (e) {
+ if (e.code !== 'ENOENT')
+ throw e;
+ }
+ });
+ }
+});
diff --git a/pkg/lib/esbuild-common.js b/pkg/lib/esbuild-common.js
new file mode 100644
index 0000000..251fac4
--- /dev/null
+++ b/pkg/lib/esbuild-common.js
@@ -0,0 +1,40 @@
+import { replace } from 'esbuild-plugin-replace';
+import { sassPlugin } from 'esbuild-sass-plugin';
+
+// List of directories to use when resolving import statements
+const nodePaths = ['pkg/lib'];
+
+export const esbuildStylesPlugins = [
+ // Redefine grid breakpoints to count with our shell
+ // See https://github.com/patternfly/patternfly-react/issues/3815 and
+ // [Redefine grid breakpoints] section in pkg/lib/_global-variables.scss for explanation
+ replace({
+ include: /\.css$/,
+ values: {
+ // Do not override the sm breakpoint as for width < 768px the left nav is hidden
+ '768px': '428px',
+ '992px': '652px',
+ '1200px': '876px',
+ '1450px': '1100px',
+ }
+ }),
+ replace({
+ include: /DataList.js$/,
+ values: {
+ 'import stylesGrid': "// HACK: revert when https://github.com/patternfly/patternfly-react/pull/8864 is released",
+ stylesGrid: 'styles',
+ }
+ }),
+ sassPlugin({
+ loadPaths: [...nodePaths, 'node_modules'],
+ quietDeps: true,
+ async transform(source, resolveDir, path) {
+ if (path.includes('patternfly-5-cockpit.scss')) {
+ return source
+ .replace(/url.*patternfly-icons-fake-path.*;/g, 'url("../base1/fonts/patternfly.woff") format("woff");')
+ .replace(/@font-face[^}]*patternfly-fonts-fake-path[^}]*}/g, '');
+ }
+ return source;
+ }
+ }),
+];
diff --git a/pkg/lib/esbuild-compress-plugin.js b/pkg/lib/esbuild-compress-plugin.js
new file mode 100644
index 0000000..b4c4180
--- /dev/null
+++ b/pkg/lib/esbuild-compress-plugin.js
@@ -0,0 +1,50 @@
+/* There is https://www.npmjs.com/package/esbuild-plugin-compress but it does
+ * not work together with our PO plugin, they are incompatible due to requiring
+ * different values for `write:`. We may be able to change our plugins to work
+ * with `write: false`, but this is easy enough to implement ourselves.
+*/
+
+import path from 'path';
+import fs from "fs";
+import util from 'node:util';
+import child_process from 'node:child_process';
+
+const NAME = 'cockpitCompressPlugin';
+
+const exec = util.promisify(child_process.execFile);
+
+const getAllFiles = function(dirPath, arrayOfFiles) {
+ const files = fs.readdirSync(dirPath);
+
+ arrayOfFiles = arrayOfFiles || [];
+
+ files.forEach(function(file) {
+ if (fs.statSync(dirPath + "/" + file).isDirectory()) {
+ arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles);
+ } else {
+ arrayOfFiles.push(path.join(dirPath, "/", file));
+ }
+ });
+
+ return arrayOfFiles;
+};
+
+export const cockpitCompressPlugin = ({ subdir = '', exclude = null } = {}) => ({
+ name: NAME,
+ setup(build) {
+ build.onEnd(async () => {
+ const gzipPromises = [];
+ const path = "./dist/" + subdir;
+
+ for await (const dirent of getAllFiles(path)) {
+ if (exclude && exclude.test(dirent))
+ continue;
+ if (dirent.endsWith('.js') || dirent.endsWith('.css')) {
+ gzipPromises.push(exec('gzip', ['-n9', dirent]));
+ }
+ }
+ await Promise.all(gzipPromises);
+ return null;
+ });
+ }
+});
diff --git a/pkg/lib/esbuild-test-html-plugin.js b/pkg/lib/esbuild-test-html-plugin.js
new file mode 100644
index 0000000..8d62384
--- /dev/null
+++ b/pkg/lib/esbuild-test-html-plugin.js
@@ -0,0 +1,28 @@
+import fs from "fs";
+import path from 'path';
+import _ from 'lodash';
+
+const srcdir = process.env.SRCDIR || '.';
+const libdir = path.resolve(srcdir, "pkg", "lib");
+
+export const cockpitTestHtmlPlugin = ({ testFiles }) => ({
+ name: 'CockpitTestHtmlPlugin',
+ setup(build) {
+ build.onEnd(async () => {
+ const data = fs.readFileSync(path.resolve(libdir, "qunit-template.html.in"), "utf8");
+ testFiles.forEach(file => {
+ const test = path.parse(file).name;
+ const output = _.template(data.toString())({
+ title: test,
+ builddir: file.split("/").map(() => "../").join(""),
+ script: test + '.js',
+ });
+ const outdir = './qunit/' + path.dirname(file);
+ const outfile = test + ".html";
+
+ fs.mkdirSync(outdir, { recursive: true });
+ fs.writeFileSync(path.resolve(outdir, outfile), output);
+ });
+ });
+ }
+});
diff --git a/pkg/lib/get-timesync-backend.py b/pkg/lib/get-timesync-backend.py
new file mode 100644
index 0000000..b3a22c2
--- /dev/null
+++ b/pkg/lib/get-timesync-backend.py
@@ -0,0 +1,61 @@
+import os
+import subprocess
+
+# get-timesync-backend - determine which NTP backend unit timedatectl
+# will likely enable.
+
+roots = ["/etc", "/run", "/usr/local", "/usr/lib", "/lib"]
+
+
+def gather_files(name, suffix):
+ # This function creates a list of files in the same order that
+ # systemd will read them in.
+ #
+ # First we collect all files in all root directories. Duplicates
+ # are avoided by only storing files with a basename that hasn't
+ # been seen yet. The roots are processed in order so that files
+ # in /etc override identically named files in /usr, for example.
+ #
+ # The files are stored in a dict with their basename (such as
+ # "10-chrony.list") as the key and their full pathname (such as
+ # "/usr/lib/systemd/ntp-units.d/10-chrony.list") as the value.
+ #
+ # This arrangement allows for easy checks for duplicate basenames
+ # while retaining access to the full pathname later when creating
+ # the final result.
+ #
+ pathname_by_basename = {}
+ for r in roots:
+ dirname = os.path.join(r, name)
+ if os.path.isdir(dirname):
+ for basename in os.listdir(dirname):
+ if basename.endswith(suffix) and basename not in pathname_by_basename:
+ pathname_by_basename[basename] = os.path.join(dirname, basename)
+
+ # Then we create a list of the full pathnames, sorted by their
+ # basenames.
+ #
+ sorted_basenames = sorted(pathname_by_basename.keys())
+ return [pathname_by_basename[basename] for basename in sorted_basenames]
+
+
+def unit_exists(unit):
+ load_state = subprocess.check_output(["systemctl", "show", "--value", "-p", "LoadState", unit],
+ universal_newlines=True).strip()
+ return load_state != "not-found" and load_state != "masked"
+
+
+def first_unit(files):
+ for f in files:
+ with open(f) as c:
+ for ll in c.readlines():
+ w = ll.strip()
+ if w != "" and not w.startswith("#") and unit_exists(w):
+ return w
+ return None
+
+
+unit = first_unit(gather_files("systemd/ntp-units.d", ".list"))
+
+if unit:
+ print(unit)
diff --git a/pkg/lib/hooks.js b/pkg/lib/hooks.js
new file mode 100644
index 0000000..10c9297
--- /dev/null
+++ b/pkg/lib/hooks.js
@@ -0,0 +1,326 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import { useState, useEffect, useRef, useReducer } from 'react';
+import deep_equal from "deep-equal";
+
+/* HOOKS
+ *
+ * These are some custom React hooks for Cockpit specific things.
+ *
+ * Overview:
+ *
+ * - usePageLocation: For following along with cockpit.location.
+ *
+ * - useLoggedInUser: For accessing information about the currently
+ * logged in user.
+ *
+ * - useFile: For reading and watching files.
+ *
+ * - useObject: For maintaining arbitrary stateful objects that get
+ * created from the properties of a component.
+ *
+ * - useEvent: For reacting to events emitted by arbitrary objects.
+ *
+ * - useInit: For running a function once.
+ *
+ * - useDeepEqualMemo: A utility hook that can help with things that
+ * need deep equal comparisons in places where React only offers
+ * Object identity comparisons, such as with useEffect.
+ */
+
+/* - usePageLocation()
+ *
+ * function Component() {
+ * const location = usePageLocation();
+ * const { path, options } = usePageLocation();
+ *
+ * ...
+ * }
+ *
+ * This returns the current value of cockpit.location and the
+ * component is re-rendered when it changes. "location" is always a
+ * valid object and never null.
+ *
+ * See https://cockpit-project.org/guide/latest/cockpit-location.html
+ */
+
+export function usePageLocation() {
+ const [location, setLocation] = useState(cockpit.location);
+
+ useEffect(() => {
+ function update() { setLocation(cockpit.location) }
+ cockpit.addEventListener("locationchanged", update);
+ return () => cockpit.removeEventListener("locationchanged", update);
+ }, []);
+
+ return location;
+}
+
+/* - useLoggedInUser()
+ *
+ * function Component() {
+ * const user_info = useLoggedInUser();
+ *
+ * ...
+ * }
+ *
+ * "user_info" is the object delivered by cockpit.user(), or null
+ * while that object is not yet available.
+ */
+
+const cockpit_user_promise = cockpit.user();
+let cockpit_user = null;
+cockpit_user_promise.then(user => { cockpit_user = user });
+
+export function useLoggedInUser() {
+ const [user, setUser] = useState(cockpit_user);
+ useEffect(() => { if (!cockpit_user) cockpit_user_promise.then(setUser); }, []);
+ return user;
+}
+
+/* - useDeepEqualMemo(value)
+ *
+ * function Component(options) {
+ * const memo_options = useDeepEqualMemo(options);
+ * useEffect(() => {
+ * const channel = cockpit.channel(..., memo_options);
+ * ...
+ * return () => channel.close();
+ * }, [memo_options]);
+ *
+ * ...
+ * }
+ *
+ * function ParentComponent() {
+ * const options = { superuser: true, host: "localhost" };
+ * return <Component options={options}/>
+ * }
+ *
+ * Sometimes a useEffect hook has a deeply nested object as one of its
+ * dependencies, such as options for a Cockpit channel. However,
+ * React will compare dependency values with Object.is, and would run
+ * the effect hook too often. In the example above, the "options"
+ * variable of Component is a different object on each render
+ * according to Object.is, but we only want to open a new channel when
+ * the value of a field such as "superuser" or "host" has actually
+ * changed.
+ *
+ * A call to useDeepEqualMemo will return some object that is deeply
+ * equal to its argument, and it will continue to return the same
+ * object (according to Object.is) until the parameter is not deeply
+ * equal to it anymore.
+ *
+ * For the example, this means that "memo_options" will always be the
+ * very same object, and the effect hook is only run once. If we
+ * would use "options" directly as a dependency of the effect hook,
+ * the channel would be closed and opened on every render. This is
+ * very inefficient, doesn't give the asynchronous channel time to do
+ * its job, and will also lead to infinite loops when events on the
+ * channel cause re-renders (which in turn will run the effect hook
+ * again, which will cause a new event, ...).
+ */
+
+export function useDeepEqualMemo(value) {
+ const ref = useRef(value);
+ if (!deep_equal(ref.current, value))
+ ref.current = value;
+ return ref.current;
+}
+
+/* - useFile(path, options)
+ * - useFileWithError(path, options)
+ *
+ * function Component() {
+ * const content = useFile("/etc/hostname", { superuser: "try" });
+ * const [content, error] = useFileWithError("/etc/hostname", { superuser: "try" });
+ *
+ * ...
+ * }
+ *
+ * The "path" and "options" parameters are passed unchanged to
+ * cockpit.file(). Thus, if you need to parse the content of the
+ * file, the best way to do that is via the "syntax" option.
+ *
+ * The "content" variable will reflect the content of the file
+ * "/etc/hostname". When the file changes on disk, the component will
+ * be re-rendered with the new content.
+ *
+ * When the file does not exist or there has been some error reading
+ * it, "content" will be false.
+ *
+ * The "error" variable will contain any errors encountered while
+ * reading the file. It is false when there are no errors.
+ *
+ * When the file does not exist, "error" will be false.
+ *
+ * The "content" and "error" variables will be null until the file has
+ * been read for the first time.
+ *
+ * useFile and useFileWithError are pretty much the same. useFile will
+ * hide the exact error from the caller, which makes it slightly
+ * cleaner to use when the exact error is not part of the UI. In the
+ * case of error, useFile will log that error to the console and
+ * return false.
+ */
+
+export function useFileWithError(path, options, hook_options) {
+ const [content_and_error, setContentAndError] = useState([null, null]);
+ const memo_options = useDeepEqualMemo(options);
+ const memo_hook_options = useDeepEqualMemo(hook_options);
+
+ useEffect(() => {
+ const handle = cockpit.file(path, memo_options);
+ handle.watch((data, tag, error) => {
+ setContentAndError([data || false, error || false]);
+ if (!data && memo_hook_options?.log_errors)
+ console.warn("Can't read " + path + ": " + (error ? error.toString() : "not found"));
+ });
+ return handle.close;
+ }, [path, memo_options, memo_hook_options]);
+
+ return content_and_error;
+}
+
+export function useFile(path, options) {
+ const [content] = useFileWithError(path, options, { log_errors: true });
+ return content;
+}
+
+/* - useObject(create, destroy, dependencies, comparators)
+ *
+ * function Component(param) {
+ * const obj = useObject(() => create_object(param),
+ * obj => obj.close(),
+ * [param], [deep_equal])
+ *
+ * ...
+ * }
+ *
+ * This will call "create_object(param)" before the first render of
+ * the component, and will call "obj.close()" after the last render.
+ *
+ * More precisely, create_object will be called as part of the first
+ * call to useObject, i.e., at the very beginning of the first render.
+ *
+ * When "param" changes compared to the previous call to useObject
+ * (according to the deep_equal function in the example above), the
+ * object will also be destroyed and a new one will be created for the
+ * new value of "param" (as part of the call to useObject).
+ *
+ * There is no time when the "obj" variable is null in the example
+ * above; the first render already has a fully created object. This
+ * is an advantage that useObject has over useEffect, which you might
+ * otherwise use to only create objects when dependencies have
+ * changed.
+ *
+ * And unlike useMemo, useObject will run a cleanup function when a
+ * component is removed. Also unlike useMemo, useObject guarantees
+ * that it will not ignore the dependencies.
+ *
+ * The dependencies are an array of values that are by default
+ * compared with Object.is. If you need to use a custom comparator
+ * function instead of Object.is, you can provide a second
+ * "comparators" array that parallels the "dependencies" array. The
+ * values at a given index in the old and new "dependencies" arrays
+ * are compared with the function at the same index in "comparators".
+ */
+
+function deps_changed(old_deps, new_deps, comps) {
+ return (!old_deps || old_deps.length != new_deps.length ||
+ old_deps.findIndex((o, i) => !(comps[i] || Object.is)(o, new_deps[i])) >= 0);
+}
+
+export function useObject(create, destroy, deps, comps) {
+ const ref = useRef(null);
+ const deps_ref = useRef(null);
+ const destroy_ref = useRef(null);
+
+ if (deps_changed(deps_ref.current, deps, comps || [])) {
+ if (ref.current && destroy)
+ destroy(ref.current);
+ ref.current = create();
+ deps_ref.current = deps;
+ }
+
+ destroy_ref.current = destroy;
+ useEffect(() => {
+ return () => destroy_ref.current?.(ref.current);
+ }, []);
+
+ return ref.current;
+}
+
+/* - useEvent(obj, event, handler)
+ *
+ * function Component(proxy) {
+ * useEvent(proxy, "changed");
+ *
+ * ...
+ * }
+ *
+ * The component will be re-rendered whenever "proxy" emits the
+ * "changed" signal. The "proxy" parameter can be null.
+ *
+ * When the optional "handler" is given, it will be called with the
+ * arguments of the event.
+ */
+
+export function useEvent(obj, event, handler) {
+ // We increase a (otherwise unused) state variable whenever the event
+ // happens. That reliably triggers a re-render.
+
+ const [, forceUpdate] = useReducer(x => x + 1, 0);
+
+ useEffect(() => {
+ function update() {
+ if (handler)
+ handler.apply(null, arguments);
+ forceUpdate();
+ }
+
+ obj?.addEventListener(event, update);
+ return () => obj?.removeEventListener(event, update);
+ }, [obj, event, handler]);
+}
+
+/* - useInit(func, deps, comps)
+ *
+ * function Component(arg) {
+ * useInit(() => {
+ * cockpit.spawn([ ..., arg ]);
+ * }, [arg]);
+ *
+ * ...
+ * }
+ *
+ * The function will be called once during the first render, and
+ * whenever "arg" changes.
+ *
+ * "useInit(func, deps, comps)" is the same as "useObject(func, null,
+ * deps, comps)" but if you want to emphasize that you just want to
+ * run a function (instead of creating a object), it is clearer to use
+ * the "useInit" name for that. Also, "deps" are optional for
+ * "useInit" and default to "[]".
+ */
+
+export function useInit(func, deps, comps, destroy = null) {
+ return useObject(func, destroy, deps || [], comps);
+}
diff --git a/pkg/lib/html2po.js b/pkg/lib/html2po.js
new file mode 100755
index 0000000..634ffc4
--- /dev/null
+++ b/pkg/lib/html2po.js
@@ -0,0 +1,235 @@
+#!/usr/bin/env node
+
+/*
+ * Extracts translatable strings from HTML files in the following forms:
+ *
+ * <tag translate>String</tag>
+ * <tag translate context="value">String</tag>
+ * <tag translate="...">String</tag>
+ * <tag translate-attr attr="String"></tag>
+ *
+ * Supports the following angular-gettext compatible forms:
+ *
+ * <translate>String</translate>
+ * <tag translate-plural="Plural">Singular</tag>
+ *
+ * Note that some of the use of the translated may not support all the strings
+ * depending on the code actually using these strings to translate the HTML.
+ */
+
+import fs from 'fs';
+import path from 'path';
+import htmlparser from 'htmlparser';
+import { ArgumentParser } from 'argparse';
+
+function fatal(message, code) {
+ console.log((filename || "html2po") + ": " + message);
+ process.exit(code || 1);
+}
+
+const parser = new ArgumentParser();
+parser.add_argument('-d', '--directory', { help: "Base directory for input files" });
+parser.add_argument('-o', '--output', { help: 'Output file', required: true });
+parser.add_argument('files', { nargs: '+', help: "One or more input files", metavar: "FILE" });
+const args = parser.parse_args();
+
+const input = args.files;
+const entries = { };
+
+/* Filename being parsed and offset of line number */
+let filename = null;
+let offsets = 0;
+
+/* The HTML parser we're using */
+const handler = new htmlparser.DefaultHandler(function(error, dom) {
+ if (error)
+ fatal(error);
+ else
+ walk(dom);
+});
+
+/* Now process each file in turn */
+step();
+
+function step() {
+ filename = input.shift();
+ if (filename === undefined) {
+ finish();
+ return;
+ }
+
+ /* Qualify the filename if necessary */
+ let full = filename;
+ if (args.directory)
+ full = path.join(args.directory, filename);
+
+ fs.readFile(full, { encoding: "utf-8" }, function(err, data) {
+ if (err)
+ fatal(err.message);
+
+ const parser = new htmlparser.Parser(handler, { includeLocation: true });
+ parser.parseComplete(data);
+ step();
+ });
+}
+
+/* Process an array of nodes */
+function walk(children) {
+ if (!children)
+ return;
+
+ children.forEach(function(child) {
+ const line = (child.location || { }).line || 0;
+ const offset = line - 1;
+
+ /* Scripts get their text processed as HTML */
+ if (child.type == 'script' && child.children) {
+ const parser = new htmlparser.Parser(handler, { includeLocation: true });
+
+ /* Make note of how far into the outer HTML file we are */
+ offsets += offset;
+
+ child.children.forEach(function(node) {
+ parser.parseChunk(node.raw);
+ });
+ parser.done();
+
+ offsets -= offset;
+
+ /* Tags get extracted as usual */
+ } else if (child.type == 'tag') {
+ tag(child);
+ }
+ });
+}
+
+/* Process a single loaded tag */
+function tag(node) {
+ let tasks, line, entry;
+ const attrs = node.attribs || { };
+ let nest = true;
+
+ /* Extract translate strings */
+ if ("translate" in attrs || "translatable" in attrs) {
+ tasks = (attrs.translate || attrs.translatable || "yes").split(" ");
+
+ /* Calculate the line location taking into account nested parsing */
+ line = (node.location || { }).line || 0;
+ line += offsets;
+
+ entry = {
+ msgctxt: attrs['translate-context'] || attrs.context,
+ msgid_plural: attrs['translate-plural'],
+ locations: [filename + ":" + line]
+ };
+
+ /* For each thing listed */
+ tasks.forEach(function(task) {
+ const copy = Object.assign({}, entry);
+
+ /* The element text itself */
+ if (task == "yes" || task == "translate") {
+ copy.msgid = extract(node.children);
+ nest = false;
+
+ /* An attribute */
+ } else if (task) {
+ copy.msgid = attrs[task];
+ }
+
+ if (copy.msgid)
+ push(copy);
+ });
+ }
+
+ /* Walk through all the children */
+ if (nest)
+ walk(node.children);
+}
+
+/* Push an entry onto the list */
+function push(entry) {
+ const key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt;
+ const prev = entries[key];
+ if (prev) {
+ prev.locations = prev.locations.concat(entry.locations);
+ } else {
+ entries[key] = entry;
+ }
+}
+
+/* Extract the given text */
+function extract(children) {
+ if (!children)
+ return null;
+
+ const str = [];
+ children.forEach(function(node) {
+ if (node.type == 'tag' && node.children)
+ str.push(extract(node.children));
+ else if (node.type == 'text' && node.data)
+ str.push(node.data);
+ });
+
+ const msgid = str.join("");
+
+ if (msgid != msgid.trim()) {
+ console.error("FATAL: string contains leading or trailing whitespace:", msgid);
+ process.exit(1);
+ }
+
+ return msgid;
+}
+
+/* Escape a string for inclusion in po file */
+function escape(string) {
+ const bs = string.split('\\')
+ .join('\\\\')
+ .split('"')
+ .join('\\"');
+ return bs.split("\n").map(function(line) {
+ return '"' + line + '"';
+ }).join("\n");
+}
+
+/* Finish by writing out the strings */
+function finish() {
+ const result = [
+ 'msgid ""',
+ 'msgstr ""',
+ '"Project-Id-Version: PACKAGE_VERSION\\n"',
+ '"MIME-Version: 1.0\\n"',
+ '"Content-Type: text/plain; charset=UTF-8\\n"',
+ '"Content-Transfer-Encoding: 8bit\\n"',
+ '"X-Generator: Cockpit html2po\\n"',
+ '',
+ ];
+
+ for (const msgid in entries) {
+ const entry = entries[msgid];
+ result.push('#: ' + entry.locations.join(" "));
+ if (entry.msgctxt)
+ result.push('msgctxt ' + escape(entry.msgctxt));
+ result.push('msgid ' + escape(entry.msgid));
+ if (entry.msgid_plural) {
+ result.push('msgid_plural ' + escape(entry.msgid_plural));
+ result.push('msgstr[0] ""');
+ result.push('msgstr[1] ""');
+ } else {
+ result.push('msgstr ""');
+ }
+ result.push('');
+ }
+
+ const data = result.join('\n');
+ if (!args.output) {
+ process.stdout.write(data);
+ process.exit(0);
+ } else {
+ fs.writeFile(args.output, data, function(err) {
+ if (err)
+ fatal(err.message);
+ process.exit(0);
+ });
+ }
+}
diff --git a/pkg/lib/inotify.py b/pkg/lib/inotify.py
new file mode 100644
index 0000000..84d6768
--- /dev/null
+++ b/pkg/lib/inotify.py
@@ -0,0 +1,72 @@
+#
+# This file is part of Cockpit.
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import ctypes
+import os
+import struct
+import sys
+
+IN_CLOSE_WRITE = 0x00000008
+IN_MOVED_FROM = 0x00000040
+IN_MOVED_TO = 0x00000080
+IN_CREATE = 0x00000100
+IN_DELETE = 0x00000200
+IN_DELETE_SELF = 0x00000400
+IN_MOVE_SELF = 0x00000800
+IN_IGNORED = 0x00008000
+
+
+class Inotify:
+ def __init__(self):
+ self._libc = ctypes.CDLL(None, use_errno=True)
+ self._get_errno_func = ctypes.get_errno
+
+ self._libc.inotify_init.argtypes = []
+ self._libc.inotify_init.restype = ctypes.c_int
+ self._libc.inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p,
+ ctypes.c_uint32]
+ self._libc.inotify_add_watch.restype = ctypes.c_int
+ self._libc.inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int]
+ self._libc.inotify_rm_watch.restype = ctypes.c_int
+
+ self.fd = self._libc.inotify_init()
+
+ def add_watch(self, path, mask):
+ path = ctypes.create_string_buffer(path.encode(sys.getfilesystemencoding()))
+ wd = self._libc.inotify_add_watch(self.fd, path, mask)
+ if wd < 0:
+ sys.stderr.write("can't add watch for %s: %s\n" % (path, os.strerror(self._get_errno_func())))
+ return wd
+
+ def rem_watch(self, wd):
+ if self._libc.inotify_rm_watch(self.fd, wd) < 0:
+ sys.stderr.write("can't remove watch: %s\n" % (os.strerror(self._get_errno_func())))
+
+ def process(self, callback):
+ buf = os.read(self.fd, 4096)
+ pos = 0
+ while pos < len(buf):
+ (wd, mask, cookie, name_len) = struct.unpack('iIII', buf[pos:pos + 16])
+ pos += 16
+ (name,) = struct.unpack('%ds' % name_len, buf[pos:pos + name_len])
+ pos += name_len
+ callback(wd, mask, name.decode().rstrip('\0'))
+
+ def run(self, callback):
+ while True:
+ self.process(callback)
diff --git a/pkg/lib/journal.css b/pkg/lib/journal.css
new file mode 100644
index 0000000..23303b1
--- /dev/null
+++ b/pkg/lib/journal.css
@@ -0,0 +1,161 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.cockpit-log-panel:empty {
+ border: none;
+}
+
+.cockpit-log-panel {
+ overflow-x: unset;
+}
+
+.cockpit-log-panel .panel-body {
+ padding: 0;
+}
+
+.cockpit-log-panel .pf-v5-c-card__body .panel-heading,
+.cockpit-log-panel .panel-body .panel-heading {
+ border-block-start: 0;
+ background-color: var(--ct-color-bg);
+ font-weight: var(--pf-v5-global--FontWeight--normal);
+ padding-block: var(--pf-v5-global--spacer--sm);
+ inline-size: auto;
+ color: var(--ct-color-list-text);
+ display: flex;
+}
+
+.cockpit-log-panel .pf-v5-c-card__body .panel-heading {
+ /* Align sub-heading within a PF4 card to the heading of the card */
+ padding-inline-start: var(--pf-v5-global--spacer--lg);
+}
+
+.cockpit-log-panel .panel-body .panel-heading:not(:first-child)::after {
+ content: "\a0";
+ display: block;
+ flex: auto;
+ background: linear-gradient(var(--ct-color-bg) 50%, var(--ct-color-border) calc(50% + 1px), var(--ct-color-bg) calc(50% + 2px));
+ margin-block: 0;
+ margin-inline: 0.5rem 0;
+}
+
+.cockpit-logline {
+ --log-icon: 24px;
+ --log-time: 3rem;
+ --log-message: 1fr;
+ --log-service-min: 0;
+ --log-service: minmax(var(--log-service-min), max-content);
+ background-color: var(--ct-color-list-bg);
+ font-size: var(--font-small);
+ padding-block: 0.5rem;
+ padding-inline: 1rem;
+ display: grid;
+ grid-template-columns: var(--log-icon) var(--log-time) var(--log-message) var(--log-service);
+ grid-gap: var(--pf-v5-global--spacer--sm);
+ align-items: baseline;
+}
+
+.cockpit-log-panel .cockpit-logline:hover {
+ background-color: var(--ct-color-list-hover-bg);
+ cursor: pointer;
+}
+
+.cockpit-log-panel .cockpit-logline:hover .cockpit-log-message:not(.cockpit-logmsg-reboot) {
+ color: var(--ct-color-list-hover-text);
+ text-decoration: underline;
+}
+
+.cockpit-log-panel .cockpit-logline + .panel-heading {
+ border-block-start-width: 1px;
+}
+
+/* Don't show headers without content */
+.cockpit-log-panel .panel-heading:last-child {
+ display: none !important;
+}
+
+.cockpit-logmsg-reboot {
+ font-style: italic;
+}
+
+.cockpit-log-warning {
+ display: flex;
+ align-self: center;
+ justify-content: center;
+}
+
+.empty-message {
+ inline-size: 100%;
+ color: var(--pf-v5-global--Color--200);
+ display: block;
+ padding-block: 0.5rem;
+ padding-inline: 1rem;
+ text-align: center;
+}
+
+.cockpit-log-time,
+.cockpit-log-service,
+.cockpit-log-service-reduced {
+ color: var(--pf-v5-global--Color--200);
+}
+
+.cockpit-log-time {
+ color: var(--pf-v5-global--Color--200);
+ font-family: monospace;
+ font-size: var(--pf-v5-global--FontSize--xs);
+ justify-self: end;
+ white-space: nowrap;
+}
+
+.cockpit-log-message,
+.cockpit-log-service,
+.cockpit-log-service-reduced {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ flex: auto;
+}
+
+.cockpit-log-message,
+.cockpit-log-service,
+.cockpit-log-service-reduced {
+ font-size: var(--pf-v5-global--FontSize--sm);
+}
+
+.cockpit-log-service-container > .pf-v5-c-badge {
+ margin-inline-start: var(--pf-v5-global--spacer--xs);
+}
+
+.cockpit-log-service-container {
+ display: flex;
+ align-items: baseline;
+}
+
+@media screen and (max-width: 428px) {
+ .cockpit-logline {
+ /* Remove space for service */
+ --log-service: 0;
+ }
+
+ .cockpit-log-service,
+ .cockpit-log-service-reduced,
+ .cockpit-log-service-container {
+ /* Move service under message */
+ grid-area: 2 / 3;
+ }
+}
diff --git a/pkg/lib/journal.js b/pkg/lib/journal.js
new file mode 100644
index 0000000..7659a77
--- /dev/null
+++ b/pkg/lib/journal.js
@@ -0,0 +1,453 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import * as timeformat from "timeformat";
+
+const _ = cockpit.gettext;
+
+export const journal = { };
+
+/**
+ * journalctl([match, ...], [options])
+ * @match: any number of journal match strings
+ * @options: an object containing further options
+ *
+ * Load and (by default) stream journal entries as
+ * json objects. This function returns a jQuery deferred
+ * object which delivers the various journal entries.
+ *
+ * The various @match strings are journalctl matches.
+ * Zero, one or more can be specified. They must be in
+ * string format, or arrays of strings.
+ *
+ * The optional @options object can contain the following:
+ * * "count": number of entries to load and/or pre-stream.
+ * Default is 10
+ * * "follow": if set to false just load entries and don't
+ * stream further journal data. Default is true.
+ * * "directory": optional directory to load journal files
+ * * "boot": when set only list entries from this specific
+ * boot id, or if null then the current boot.
+ * * "since": if specified list entries since the date/time
+ * * "until": if specified list entries until the date/time
+ * * "cursor": a cursor to start listing entries from
+ * * "after": a cursor to start listing entries after
+ * * "priority": if specified list entries below the specific priority, inclusive
+ *
+ * Returns a jQuery deferred promise. You can call these
+ * functions on the deferred to handle the responses. Note that
+ * there are additional non-jQuery methods.
+ *
+ * .done(function(entries) { }): Called when done, @entries is
+ * an array of all journal entries loaded. If .stream()
+ * has been invoked then @entries will be empty.
+ * .fail(function(ex) { }): called if the operation fails
+ * .stream(function(entries) { }): called when we receive entries
+ * entries. Called once per batch of journal @entries,
+ * whether following or not.
+ * .stop(): stop following or retrieving entries.
+ */
+
+journal.build_cmd = function build_cmd(/* ... */) {
+ const matches = [];
+ const options = { follow: true };
+ for (let i = 0; i < arguments.length; i++) {
+ const arg = arguments[i];
+ if (typeof arg == "string") {
+ matches.push(arg);
+ } else if (typeof arg == "object") {
+ if (arg instanceof Array) {
+ matches.push.apply(matches, arg);
+ } else {
+ Object.assign(options, arg);
+ break;
+ }
+ } else {
+ console.warn("journal.journalctl called with invalid argument:", arg);
+ }
+ }
+
+ if (options.count === undefined) {
+ if (options.follow)
+ options.count = 10;
+ else
+ options.count = null;
+ }
+
+ const cmd = ["journalctl", "-q"];
+ if (!options.count)
+ cmd.push("--no-tail");
+ else
+ cmd.push("--lines=" + options.count);
+
+ cmd.push("--output=" + (options.output || "json"));
+
+ if (options.directory)
+ cmd.push("--directory=" + options.directory);
+ if (options.boot)
+ cmd.push("--boot=" + options.boot);
+ else if (options.boot !== undefined)
+ cmd.push("--boot");
+ if (options.since)
+ cmd.push("--since=" + options.since);
+ if (options.until)
+ cmd.push("--until=" + options.until);
+ if (options.cursor)
+ cmd.push("--cursor=" + options.cursor);
+ if (options.after)
+ cmd.push("--after=" + options.after);
+ if (options.priority)
+ cmd.push("--priority=" + options.priority);
+ if (options.grep)
+ cmd.push("--grep=" + options.grep);
+
+ /* journalctl doesn't allow reverse and follow together */
+ if (options.reverse)
+ cmd.push("--reverse");
+ else if (options.follow)
+ cmd.push("--follow");
+
+ cmd.push("--");
+ cmd.push.apply(cmd, matches);
+ return cmd;
+};
+
+journal.journalctl = function journalctl(/* ... */) {
+ const cmd = journal.build_cmd.apply(null, arguments);
+
+ const dfd = cockpit.defer();
+ const promise = dfd.promise();
+ let buffer = "";
+ let entries = [];
+ let streamers = [];
+ let interval = null;
+
+ function fire_streamers() {
+ let ents, i;
+ if (streamers.length && entries.length > 0) {
+ ents = entries;
+ entries = [];
+ for (i = 0; i < streamers.length; i++)
+ streamers[i].apply(promise, [ents]);
+ } else {
+ window.clearInterval(interval);
+ interval = null;
+ }
+ }
+
+ const proc = cockpit.spawn(cmd, { batch: 8192, latency: 300, superuser: "try" })
+ .stream(function(data) {
+ if (buffer)
+ data = buffer + data;
+ buffer = "";
+
+ const lines = data.split("\n");
+ const last = lines.length - 1;
+ lines.forEach(function(line, i) {
+ if (i == last) {
+ buffer = line;
+ } else if (line && line.indexOf("-- ") !== 0) {
+ try {
+ entries.push(JSON.parse(line));
+ } catch (e) {
+ console.warn(e, line);
+ }
+ }
+ });
+
+ if (streamers.length && interval === null)
+ interval = window.setInterval(fire_streamers, 300);
+ })
+ .done(function() {
+ fire_streamers();
+ dfd.resolve(entries);
+ })
+ .fail(function(ex) {
+ /* The journalctl command fails when no entries are matched
+ * so we just ignore this status code */
+ if (ex.problem == "cancelled" ||
+ ex.exit_status === 1) {
+ fire_streamers();
+ dfd.resolve(entries);
+ } else {
+ dfd.reject(ex);
+ }
+ })
+ .always(function() {
+ window.clearInterval(interval);
+ });
+
+ promise.stream = function stream(callback) {
+ streamers.push(callback);
+ return this;
+ };
+ promise.stop = function stop() {
+ streamers = [];
+ promise.stopped = true;
+ proc.close("cancelled");
+ };
+ return promise;
+};
+
+journal.printable = function printable(value, key) {
+ if (value === undefined || value === null)
+ return _("[no data]");
+ else if (typeof (value) == "string")
+ return value;
+ else if (value.length !== undefined && value.length <= 1000 && key == "MESSAGE")
+ return new TextDecoder().decode(new Uint8Array(value));
+ else {
+ return _("[binary data]");
+ }
+};
+
+/* Render the journal entries by passing suitable DOM elements back to
+ the caller via the 'output_funcs'.
+
+ Rendering is context aware. It will insert 'reboot' markers, for
+ example, and collapse repeated lines. You can extend the output at
+ the bottom and also at the top.
+
+ A new renderer is created by calling 'journal.renderer' like
+ so:
+
+ const renderer = journal.renderer(funcs);
+
+ You can feed new entries into the renderer by calling various
+ methods on the returned object:
+
+ - renderer.append(journal_entry)
+ - renderer.append_flush()
+ - renderer.prepend(journal_entry)
+ - renderer.prepend_flush()
+
+ A 'journal_entry' is one element of the result array returned by a
+ call to 'Query' with the 'cockpit.journal_fields' as the fields to
+ return.
+
+ Calling 'append' will append the given entry to the end of the
+ output, naturally, and 'prepend' will prepend it to the start.
+
+ The output might lag behind what has been input via 'append' and
+ 'prepend', and you need to call 'append_flush' and 'prepend_flush'
+ respectively to ensure that the output is up-to-date. Flushing a
+ renderer does not introduce discontinuities into the output. You
+ can continue to feed entries into the renderer after flushing and
+ repeated lines will be correctly collapsed across the flush, for
+ example.
+
+ The renderer will call methods of the 'output_funcs' object to
+ produce the desired output:
+
+ - output_funcs.append(rendered)
+ - output_funcs.remove_last()
+ - output_funcs.prepend(rendered)
+ - output_funcs.remove_first()
+
+ The 'rendered' argument is the return value of one of the rendering
+ functions described below. The 'append' and 'prepend' methods
+ should add this element to the output, naturally, and 'remove_last'
+ and 'remove_first' should remove the indicated element.
+
+ If you never call 'prepend' on the renderer, 'output_func.prepend'
+ isn't called either. If you never call 'renderer.prepend' after
+ 'renderer.prepend_flush', then 'output_func.remove_first' will
+ never be called. The same guarantees exist for the 'append' family
+ of functions.
+
+ The actual rendering is also done by calling methods on
+ 'output_funcs':
+
+ - output_funcs.render_line(ident, prio, message, count, time, cursor)
+ - output_funcs.render_day_header(day)
+ - output_funcs.render_reboot_separator()
+*/
+
+journal.renderer = function renderer(output_funcs) {
+ if (!output_funcs.render_line)
+ console.error("Invalid renderer provided");
+
+ function copy_object(o) {
+ const c = { }; for (const p in o) c[p] = o[p]; return c;
+ }
+
+ // A 'entry' object describes a journal entry in formatted form.
+ // It has fields 'bootid', 'ident', 'prio', 'message', 'time',
+ // 'day', all of which are strings.
+
+ function format_entry(journal_entry) {
+ const d = journal_entry.__REALTIME_TIMESTAMP / 1000; // timestamps are in µs
+ return {
+ cursor: journal_entry.__CURSOR,
+ full: journal_entry,
+ day: timeformat.date(d),
+ time: timeformat.time(d),
+ bootid: journal_entry._BOOT_ID,
+ ident: journal_entry.SYSLOG_IDENTIFIER || journal_entry._COMM,
+ prio: journal_entry.PRIORITY,
+ message: journal.printable(journal_entry.MESSAGE, "MESSAGE")
+ };
+ }
+
+ function entry_is_equal(a, b) {
+ return (a && b &&
+ a.day == b.day &&
+ a.bootid == b.bootid &&
+ a.ident == b.ident &&
+ a.prio == b.prio &&
+ a.message == b.message);
+ }
+
+ // A state object describes a line that should be eventually
+ // output. It has an 'entry' field as per description above, and
+ // also 'count', 'last_time', and 'first_time', which record
+ // repeated entries. Additionally:
+ //
+ // line_present: When true, the line has been output already with
+ // some preliminary data. It needs to be removed before
+ // outputting more recent data.
+ //
+ // header_present: The day header has been output preliminarily
+ // before the actual log lines. It needs to be removed before
+ // prepending more lines. If both line_present and
+ // header_present are true, then the header comes first in the
+ // output, followed by the line.
+
+ function render_state_line(state) {
+ return output_funcs.render_line(state.entry.ident,
+ state.entry.prio,
+ state.entry.message,
+ state.count,
+ state.last_time,
+ state.entry.full);
+ }
+
+ // We keep the state of the first and last journal lines,
+ // respectively, in order to collapse repeated lines, and to
+ // insert reboot markers and day headers.
+ //
+ // Normally, there are two state objects, but if only a single
+ // line has been output so far, top_state and bottom_state point
+ // to the same object.
+
+ let top_state, bottom_state;
+
+ top_state = bottom_state = { };
+
+ function start_new_line() {
+ // If we now have two lines, split the state
+ if (top_state === bottom_state && top_state.entry) {
+ top_state = copy_object(bottom_state);
+ }
+ }
+
+ function top_output() {
+ if (top_state.header_present) {
+ output_funcs.remove_first();
+ top_state.header_present = false;
+ }
+ if (top_state.line_present) {
+ output_funcs.remove_first();
+ top_state.line_present = false;
+ }
+ if (top_state.entry) {
+ output_funcs.prepend(render_state_line(top_state));
+ top_state.line_present = true;
+ }
+ }
+
+ function prepend(journal_entry) {
+ const entry = format_entry(journal_entry);
+
+ if (entry_is_equal(top_state.entry, entry)) {
+ top_state.count += 1;
+ top_state.first_time = entry.time;
+ } else {
+ top_output();
+
+ if (top_state.entry) {
+ if (entry.bootid != top_state.entry.bootid)
+ output_funcs.prepend(output_funcs.render_reboot_separator());
+ if (entry.day != top_state.entry.day)
+ output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
+ }
+
+ start_new_line();
+ top_state.entry = entry;
+ top_state.count = 1;
+ top_state.first_time = top_state.last_time = entry.time;
+ top_state.line_present = false;
+ }
+ }
+
+ function prepend_flush() {
+ top_output();
+ if (top_state.entry) {
+ output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
+ top_state.header_present = true;
+ }
+ }
+
+ function bottom_output() {
+ if (bottom_state.line_present) {
+ output_funcs.remove_last();
+ bottom_state.line_present = false;
+ }
+ if (bottom_state.entry) {
+ output_funcs.append(render_state_line(bottom_state));
+ bottom_state.line_present = true;
+ }
+ }
+
+ function append(journal_entry) {
+ const entry = format_entry(journal_entry);
+
+ if (entry_is_equal(bottom_state.entry, entry)) {
+ bottom_state.count += 1;
+ bottom_state.last_time = entry.time;
+ } else {
+ bottom_output();
+
+ if (!bottom_state.entry || entry.day != bottom_state.entry.day) {
+ output_funcs.append(output_funcs.render_day_header(entry.day));
+ bottom_state.header_present = true;
+ }
+ if (bottom_state.entry && entry.bootid != bottom_state.entry.bootid)
+ output_funcs.append(output_funcs.render_reboot_separator());
+
+ start_new_line();
+ bottom_state.entry = entry;
+ bottom_state.count = 1;
+ bottom_state.first_time = bottom_state.last_time = entry.time;
+ bottom_state.line_present = false;
+ }
+ }
+
+ function append_flush() {
+ bottom_output();
+ }
+
+ return {
+ prepend,
+ prepend_flush,
+ append,
+ append_flush
+ };
+};
diff --git a/pkg/lib/long-running-process.js b/pkg/lib/long-running-process.js
new file mode 100644
index 0000000..0c085a9
--- /dev/null
+++ b/pkg/lib/long-running-process.js
@@ -0,0 +1,166 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Manage a long-running precious process that runs independently from a Cockpit
+ * session in a transient systemd service unit. See
+ * examples/long-running-process/README.md for details.
+ *
+ * The unit will run as root, on the system systemd manager, so that every privileged
+ * Cockpit session shares the same unit. The same approach works in principle on
+ * the user's systemd instance, but the current code does not support that as it
+ * is not a common use case for Cockpit.
+ */
+
+/* global cockpit */
+
+// systemd D-Bus API names
+const O_SD_OBJ = "/org/freedesktop/systemd1";
+const I_SD_MGR = "org.freedesktop.systemd1.Manager";
+const I_SD_UNIT = "org.freedesktop.systemd1.Unit";
+const I_DBUS_PROP = "org.freedesktop.DBus.Properties";
+
+/* Possible LongRunningProcess.state values */
+export const ProcessState = {
+ INIT: 'init',
+ STOPPED: 'stopped',
+ RUNNING: 'running',
+ FAILED: 'failed',
+};
+
+export class LongRunningProcess {
+ /* serviceName: systemd unit name to start or reattach to
+ * updateCallback: function that gets called whenever the state changed; first and only
+ * argument is `this` LongRunningProcess instance.
+ */
+ constructor(serviceName, updateCallback) {
+ this.systemdClient = cockpit.dbus("org.freedesktop.systemd1", { superuser: "require" });
+ this.serviceName = serviceName;
+ this.updateCallback = updateCallback;
+ this._setState(ProcessState.INIT);
+ this.startTimestamp = null; // µs since epoch
+ this.terminated = false;
+
+ // Watch for start event of the service
+ this.systemdClient.subscribe({ interface: I_SD_MGR, member: "JobNew" }, (path, iface, signal, args) => {
+ if (args[2] == this.serviceName)
+ this._checkState();
+ });
+
+ // Check if it is already running
+ this._checkState();
+ }
+
+ /* Start long-running process. Only call this in states STOPPED or FAILED.
+ * This runs as root, thus will be shared with all privileged Cockpit sessions.
+ * Return cockpit.spawn promise. You need to handle exceptions, but not success.
+ */
+ run(argv, options) {
+ if (this.state !== ProcessState.STOPPED && this.state !== ProcessState.FAILED)
+ throw new Error(`cannot start LongRunningProcess in state ${this.state}`);
+
+ // no need to directly react to this -- JobNew and _checkState() will pick up when the unit runs
+ return cockpit.spawn(["systemd-run", "--unit", this.serviceName, "--service-type=oneshot", "--no-block", "--"].concat(argv),
+ { superuser: "require", err: "message", ...options });
+ }
+
+ /* Stop long-running process while it is RUNNING, or reset a FAILED one */
+ terminate() {
+ if (this.state !== ProcessState.RUNNING && this.state !== ProcessState.FAILED)
+ throw new Error(`cannot terminate LongRunningProcess in state ${this.state}`);
+
+ /* This sends a SIGTERM to the unit, causing it to go into "failed" state. This would not
+ * happen with `systemd-run -p SuccessExitStatus=0`, but that does not yet work on older
+ * OSes with systemd ≤ 241 So let checkState() know that a failure is due to termination. */
+ this.terminated = true;
+ return this.systemdClient.call(O_SD_OBJ, I_SD_MGR, "StopUnit", [this.serviceName, "replace"], { type: "ss" });
+ }
+
+ /*
+ * below are internal private methods
+ */
+
+ _setState(state) {
+ /* PropertiesChanged often gets fired multiple times with the same values, avoid UI flicker */
+ if (state === this.state)
+ return;
+ this.state = state;
+ this.terminated = false;
+ if (this.updateCallback)
+ this.updateCallback(this);
+ }
+
+ _setStateFromProperties(activeState, stateChangeTimestamp) {
+ switch (activeState) {
+ case 'activating':
+ this.startTimestamp = stateChangeTimestamp;
+ this._setState(ProcessState.RUNNING);
+ break;
+ case 'failed':
+ this.startTimestamp = null; // TODO: can we derive this from InvocationID?
+ if (this.terminated) {
+ /* terminating causes failure; reset that and do not announce it as failed */
+ this.systemdClient.call(O_SD_OBJ, I_SD_MGR, "ResetFailedUnit", [this.serviceName], { type: "s" });
+ } else {
+ this._setState(ProcessState.FAILED);
+ }
+ break;
+ case 'inactive':
+ this._setState(ProcessState.STOPPED);
+ break;
+ case 'deactivating':
+ /* ignore these transitions */
+ break;
+ default:
+ throw new Error(`unexpected state of unit ${this.serviceName}: ${activeState}`);
+ }
+ }
+
+ // check if the transient unit for our command is running
+ _checkState() {
+ this.systemdClient.call(O_SD_OBJ, I_SD_MGR, "GetUnit", [this.serviceName], { type: "s" })
+ .then(([unitObj]) => {
+ /* Some time may pass between getting JobNew and the unit actually getting activated;
+ * we may get an inactive unit here; watch for state changes. This will also update
+ * the UI if the unit stops. */
+ this.subscription = this.systemdClient.subscribe(
+ { interface: I_DBUS_PROP, member: "PropertiesChanged" },
+ (path, iface, signal, args) => {
+ if (path === unitObj && args[1].ActiveState && args[1].StateChangeTimestamp)
+ this._setStateFromProperties(args[1].ActiveState.v, args[1].StateChangeTimestamp.v);
+ });
+
+ this.systemdClient.call(unitObj, I_DBUS_PROP, "GetAll", [I_SD_UNIT], { type: "s" })
+ .then(([props]) => this._setStateFromProperties(props.ActiveState.v, props.StateChangeTimestamp.v))
+ .catch(ex => {
+ throw new Error(`unexpected failure of GetAll(${unitObj}): ${ex.toString()}`);
+ });
+ })
+ .catch(ex => {
+ if (ex.name === "org.freedesktop.systemd1.NoSuchUnit") {
+ if (this.subscription) {
+ this.subscription.remove();
+ this.subscription = null;
+ }
+ this._setState(ProcessState.STOPPED);
+ } else {
+ throw new Error(`unexpected failure of GetUnit(${this.serviceName}): ${ex.toString()}`);
+ }
+ });
+ }
+}
diff --git a/pkg/lib/machine-info.js b/pkg/lib/machine-info.js
new file mode 100644
index 0000000..a5b2119
--- /dev/null
+++ b/pkg/lib/machine-info.js
@@ -0,0 +1,259 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+const _ = cockpit.gettext;
+
+export const cpu_ram_info = () =>
+ cockpit.spawn(["cat", "/proc/meminfo", "/proc/cpuinfo"])
+ .then(text => {
+ const info = { };
+ const memtotal_match = text.match(/MemTotal:[^0-9]*([0-9]+) [kK]B/);
+ const total_kb = memtotal_match && parseInt(memtotal_match[1], 10);
+ if (total_kb)
+ info.memory = total_kb * 1024;
+
+ const available_match = text.match(/MemAvailable:[^0-9]*([0-9]+) [kK]B/);
+ const available_kb = available_match && parseInt(available_match[1], 10);
+ if (available_kb)
+ info.available_memory = available_kb * 1024;
+
+ const swap_match = text.match(/SwapTotal:[^0-9]*([0-9]+) [kK]B/);
+ const swap_total_kb = swap_match && parseInt(swap_match[1], 10);
+ if (swap_total_kb)
+ info.swap = swap_total_kb * 1024;
+
+ let model_match = text.match(/^model name\s*:\s*(.*)$/m);
+ if (!model_match)
+ model_match = text.match(/^cpu\s*:\s*(.*)$/m); // PowerPC
+ if (!model_match)
+ model_match = text.match(/^vendor_id\s*:\s*(.*)$/m); // s390x
+ if (model_match)
+ info.cpu_model = model_match[1];
+
+ info.cpus = 0;
+ const re = /^(processor|cpu number)\s*:/gm;
+ while (re.test(text))
+ info.cpus += 1;
+ return info;
+ });
+
+// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_2.7.1.pdf
+const chassis_types = [
+ undefined,
+ _("Other"),
+ _("Unknown"),
+ _("Desktop"),
+ _("Low profile desktop"),
+ _("Pizza box"),
+ _("Mini tower"),
+ _("Tower"),
+ _("Portable"),
+ _("Laptop"),
+ _("Notebook"),
+ _("Handheld"),
+ _("Docking station"),
+ _("All-in-one"),
+ _("Sub-Notebook"),
+ _("Space-saving computer"),
+ _("Lunch box"), /* 0x10 */
+ _("Main server chassis"),
+ _("Expansion chassis"),
+ _("Sub-Chassis"),
+ _("Bus expansion chassis"),
+ _("Peripheral chassis"),
+ _("RAID chassis"),
+ _("Rack mount chassis"),
+ _("Sealed-case PC"),
+ _("Multi-system chassis"),
+ _("Compact PCI"), /* 0x1A */
+ _("Advanced TCA"),
+ _("Blade"),
+ _("Blade enclosure"),
+ _("Tablet"),
+ _("Convertible"),
+ _("Detachable"), /* 0x20 */
+ _("IoT gateway"),
+ _("Embedded PC"),
+ _("Mini PC"),
+ _("Stick PC"),
+];
+
+function parseDMIFields(text) {
+ const info = {};
+ text.split("\n").forEach(line => {
+ const sep = line.indexOf(':');
+ if (sep <= 0)
+ return;
+ const file = line.slice(0, sep);
+ const key = file.slice(file.lastIndexOf('/') + 1);
+ let value = line.slice(sep + 1);
+
+ // clean up after lazy OEMs
+ if (value.match(/to be filled by o\.?e\.?m\.?/i))
+ value = "";
+
+ info[key] = value;
+
+ if (key === "chassis_type")
+ info[key + "_str"] = chassis_types[parseInt(value)] || chassis_types[2]; // fall back to "Unknown"
+ });
+ return info;
+}
+
+export function dmi_info() {
+ // the grep often/usually exits with 2, that's okay as long as we find *some* information
+ return cockpit.script("grep -r . /sys/class/dmi/id || true", null,
+ { err: "message", superuser: "try" })
+ .then((output) => parseDMIFields(output));
+}
+
+// decode a binary Uint8Array with a trailing null byte
+function decode_proc_str(s) {
+ return cockpit.utf8_decoder().decode(s.slice(0, -1));
+}
+
+export function devicetree_info() {
+ let model, serial;
+
+ return Promise.all([
+ // these succeed with content === null if files are absent
+ cockpit.file("/proc/device-tree/model", { binary: true }).read()
+ .then(content => { model = content ? decode_proc_str(content) : null }),
+ cockpit.file("/proc/device-tree/serial-number", { binary: true }).read()
+ .then(content => { serial = content ? decode_proc_str(content) : null }),
+ ])
+ .then(() => ({ model, serial }));
+}
+
+/* we expect udev db paragraphs like this:
+ *
+ P: /devices/virtual/mem/null
+ N: null
+ E: DEVMODE=0666
+ E: DEVNAME=/dev/null
+ E: SUBSYSTEM=mem
+*/
+
+const udevPathRE = /^P: (.*)$/;
+const udevPropertyRE = /^E: (\w+)=(.*)$/;
+
+function parseUdevDB(text) {
+ const info = {};
+ text.split("\n\n").forEach(paragraph => {
+ let syspath = null;
+ const props = {};
+
+ paragraph = paragraph.trim();
+ if (!paragraph)
+ return;
+
+ paragraph.split("\n").forEach(line => {
+ let match = line.match(udevPathRE);
+ if (match) {
+ syspath = match[1];
+ } else {
+ match = line.match(udevPropertyRE);
+ if (match)
+ props[match[1]] = match[2];
+ }
+ });
+
+ if (syspath)
+ info[syspath] = props;
+ else
+ console.log("udev database paragraph is missing P:", paragraph);
+ });
+ return info;
+}
+
+export function udev_info() {
+ return cockpit.spawn(["udevadm", "info", "--export-db"], { err: "message" })
+ .then(output => parseUdevDB(output));
+}
+
+const memoryRE = /^([ \w]+): (.*)/;
+
+// Process the dmidecode output and create a mapping of locator to DIMM properties
+function parseMemoryInfo(text) {
+ const info = {};
+ text.split("\n\n").forEach(paragraph => {
+ let locator = null;
+ let bankLocator = null;
+ const props = {};
+ paragraph = paragraph.trim();
+ if (!paragraph)
+ return;
+
+ paragraph.split("\n").forEach(line => {
+ line = line.trim();
+ const match = line.match(memoryRE);
+ if (match)
+ props[match[1]] = match[2];
+ });
+
+ locator = props.Locator;
+ bankLocator = props['Bank Locator'];
+ if (locator)
+ info[bankLocator + locator] = props;
+ });
+ return processMemory(info);
+}
+
+// Select the useful properties to display
+function processMemory(info) {
+ const memoryArray = [];
+
+ for (const dimm in info) {
+ const memoryProperty = info[dimm];
+
+ let memorySize = memoryProperty.Size || _("Unknown");
+ if (memorySize.includes("MB")) {
+ const memorySizeValue = parseInt(memorySize, 10);
+ memorySize = cockpit.format(_("$0 GiB"), memorySizeValue / 1024);
+ }
+
+ let memoryTechnology = memoryProperty["Memory technology"];
+ if (!memoryTechnology || memoryTechnology == "<OUT OF SPEC>")
+ memoryTechnology = _("Unknown");
+
+ let memoryRank = memoryProperty.Rank || _("Unknown");
+ if (memoryRank == 1)
+ memoryRank = _("Single rank");
+ if (memoryRank == 2)
+ memoryRank = _("Dual rank");
+
+ memoryArray.push({
+ locator: (memoryProperty['Bank Locator'] + ': ' + memoryProperty.Locator) || _("Unknown"),
+ technology: memoryTechnology,
+ type: memoryProperty.Type || _("Unknown"),
+ size: memorySize,
+ state: memoryProperty["Total Width"] == "Unknown" ? _("Absent") : _("Present"),
+ rank: memoryRank,
+ speed: memoryProperty.Speed || _("Unknown")
+ });
+ }
+
+ return memoryArray;
+}
+
+export function memory_info() {
+ return cockpit.spawn(["dmidecode", "-t", "memory"], { environ: ["LC_ALL=C"], err: "message", superuser: "try" })
+ .then(output => parseMemoryInfo(output));
+}
diff --git a/pkg/lib/manifest2po.js b/pkg/lib/manifest2po.js
new file mode 100755
index 0000000..8300af4
--- /dev/null
+++ b/pkg/lib/manifest2po.js
@@ -0,0 +1,179 @@
+#!/usr/bin/env node
+
+/*
+ * Extracts translatable strings from manifest.json files.
+ *
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { ArgumentParser } from 'argparse';
+
+function fatal(message, code) {
+ console.log((filename || "manifest2po") + ": " + message);
+ process.exit(code || 1);
+}
+
+const parser = new ArgumentParser();
+parser.add_argument('-d', '--directory', { help: "Base directory for input files" });
+parser.add_argument('-o', '--output', { help: 'Output file', required: true });
+parser.add_argument('files', { nargs: '+', help: "One or more input files", metavar: "FILE" });
+const args = parser.parse_args();
+
+const input = args.files;
+const entries = { };
+
+/* Filename being parsed */
+let filename = null;
+
+/* Now process each file in turn */
+step();
+
+function step() {
+ filename = input.shift();
+ if (filename === undefined) {
+ finish();
+ return;
+ }
+
+ if (path.basename(filename) != "manifest.json")
+ return step();
+
+ /* Qualify the filename if necessary */
+ let full = filename;
+ if (args.directory)
+ full = path.join(args.directory, filename);
+
+ fs.readFile(full, { encoding: "utf-8" }, function(err, data) {
+ if (err)
+ fatal(err.message);
+
+ // There are variables which when not substituted can cause JSON.parse to fail
+ // Dummy replace them. None variable is going to be translated anyway
+ const safe_data = data.replace(/@.+?@/gi, 1);
+ process_manifest(JSON.parse(safe_data));
+
+ return step();
+ });
+}
+
+function process_manifest(manifest) {
+ if (manifest.menu)
+ process_menu(manifest.menu);
+ if (manifest.tools)
+ process_menu(manifest.tools);
+ if (manifest.bridges)
+ process_bridges(manifest.bridges);
+ if (manifest.docs)
+ process_docs(manifest.docs);
+}
+
+function process_keywords(keywords) {
+ keywords.forEach(v => {
+ v.matches.forEach(keyword =>
+ push({
+ msgid: keyword,
+ locations: [filename + ":0"]
+ })
+ );
+ });
+}
+
+function process_docs(docs) {
+ docs.forEach(doc => {
+ push({
+ msgid: doc.label,
+ locations: [filename + ":0"]
+ });
+ });
+}
+
+function process_menu(menu) {
+ for (const m in menu) {
+ if (menu[m].label) {
+ push({
+ msgid: menu[m].label,
+ locations: [filename + ":0"]
+ });
+ }
+ if (menu[m].keywords)
+ process_keywords(menu[m].keywords);
+ if (menu[m].docs)
+ process_docs(menu[m].docs);
+ }
+}
+
+function process_bridges(bridges) {
+ for (const b in bridges) {
+ if (bridges[b].label) {
+ push({
+ msgid: bridges[b].label,
+ locations: [filename + ":0"]
+ });
+ }
+ }
+}
+
+/* Push an entry onto the list */
+function push(entry) {
+ const key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt;
+ const prev = entries[key];
+ if (prev) {
+ prev.locations = prev.locations.concat(entry.locations);
+ } else {
+ entries[key] = entry;
+ }
+}
+
+/* Escape a string for inclusion in po file */
+function escape(string) {
+ const bs = string.split('\\')
+ .join('\\\\')
+ .split('"')
+ .join('\\"');
+ return bs.split("\n").map(function(line) {
+ return '"' + line + '"';
+ }).join("\n");
+}
+
+/* Finish by writing out the strings */
+function finish() {
+ const result = [
+ 'msgid ""',
+ 'msgstr ""',
+ '"Project-Id-Version: PACKAGE_VERSION\\n"',
+ '"MIME-Version: 1.0\\n"',
+ '"Content-Type: text/plain; charset=UTF-8\\n"',
+ '"Content-Transfer-Encoding: 8bit\\n"',
+ '"X-Generator: Cockpit manifest2po\\n"',
+ '',
+ ];
+
+ for (const msgid in entries) {
+ const entry = entries[msgid];
+ result.push('#: ' + entry.locations.join(" "));
+ if (entry.msgctxt)
+ result.push('msgctxt ' + escape(entry.msgctxt));
+ result.push('msgid ' + escape(entry.msgid));
+ if (entry.msgid_plural) {
+ result.push('msgid_plural ' + escape(entry.msgid_plural));
+ result.push('msgstr[0] ""');
+ result.push('msgstr[1] ""');
+ } else {
+ result.push('msgstr ""');
+ }
+ result.push('');
+ }
+
+ const data = result.join('\n');
+ if (!args.output) {
+ process.stdout.write(data);
+ process.exit(0);
+ } else {
+ fs.writeFile(args.output, data, function(err) {
+ if (err)
+ fatal(err.message);
+ process.exit(0);
+ });
+ }
+}
diff --git a/pkg/lib/menu-select-widget.scss b/pkg/lib/menu-select-widget.scss
new file mode 100644
index 0000000..81778df
--- /dev/null
+++ b/pkg/lib/menu-select-widget.scss
@@ -0,0 +1,35 @@
+// FIXME: Remove this custom implementation once a component exists upstream.
+// PF overrides to fake a multiselect widget (as one does not currently exist in PF4).
+// A menu gives us the interaction we want, but the styling is a bit off.
+// Therefore, we're changing the visuals here locally.
+// PF4 upstream request for multi-select @ https://github.com/patternfly/patternfly/issues/4027
+.ct-menu-select-widget.pf-v5-c-menu {
+ // Divider is silly between the widgets in this context
+ .pf-v5-c-divider {
+ display: none;
+
+ + .pf-v5-c-menu__content {
+ // There should be minimal space between the widgets (replacing the divider)
+ margin-block-start: var(--pf-v5-global--spacer--sm);
+ }
+ }
+
+ .pf-v5-c-menu__content {
+ // An overflow multi-select widget needs an outline
+ border: 1px solid var(--pf-v5-global--BorderColor--100);
+ }
+
+ // Search should not be inset when there's no border containing it
+ .pf-v5-c-menu__search {
+ padding: 0;
+ }
+
+ // Keep the background on a selected item even when it doesn't have
+ // focus, allowing keyboard control to have the only background color
+ // when active but also keep the background color when the list loses
+ // focus (such as when the keyboard or mouse navigates outside,
+ // including initial rendering of the list.
+ .pf-v5-c-menu__list:not(:focus-within) .pf-m-selected {
+ background-color: var(--pf-v5-c-menu__list-item--hover--BackgroundColor);
+ }
+}
diff --git a/pkg/lib/notifications.js b/pkg/lib/notifications.js
new file mode 100644
index 0000000..115f12b
--- /dev/null
+++ b/pkg/lib/notifications.js
@@ -0,0 +1,167 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* NOTIFICATIONS
+
+A page can broadcast notifications to the rest of Cockpit. For
+example, the "Software updates" page can send out a notification when
+it detects that software updates are available. The shell will then
+highlight the menu entry for "Software updates" and the "System"
+overview page will also mention it in its "Operating system" section.
+
+The details are all still experimental and subject to change.
+
+As a first step, there are only simple "page status" notifications.
+When we address "event" style notifications, page status notifications
+might become a special case of them. Or not.
+
+A page status is either null, or a JSON value with the following
+fields:
+
+ - type (string, optional)
+
+ If specified, one of "info", "warning", "error". The shell will put
+ an appropriate icon next to the navigation entry for this page, for
+ example.
+
+ Omitting 'type' means that the page has no special status and is the
+ same as using "null" as the whole status value. This can be used to
+ broadcast values in the 'details' field to other pages without
+ forcing an icon into the navigation menu.
+
+ - title (string, optional)
+
+ A short, human readable, localized description of the status,
+ suitable for a tooltip.
+
+ - details (JSON value, optional)
+
+ An arbitrary value. The "System" overview page might monitor a
+ couple of pages for their status and it will use 'details' to display
+ a richer version of the status than possible with just type and
+ title. The recognized properties are:
+
+ * icon: custom icon name (defaults to standard icon corresponding to type)
+ * pficon: PatternFly icon name; e.g. "enhancement", "bug", "security", "spinner", "check";
+ see get_pficon() in pkg/systemd/page-status.jsx
+ * link: custom link target (defaults to page name); if false, the
+ notification will not be a link
+
+Usage:
+
+ import { page_status } from "notifications";
+
+ - page_status.set_own(STATUS)
+
+ Sets the status of the page making the call, completely overwriting
+ the current status. For example,
+
+ page_status.set_own({
+ type: "info",
+ title: _("Software updates available"),
+ details: {
+ num_updates: 10,
+ num_security_updates: 5
+ }
+ });
+
+ page_status.set_own({
+ type: null
+ title: _("System is up to date"),
+ details: {
+ last_check: 81236457
+ }
+ });
+
+ Calling this function with the same STATUS value multiple times is
+ cheap: only the first call will actually broadcast the new status.
+
+ - page_status.get(PAGE, [HOST])
+
+ Retrieves the current status of page PAGE of HOST. When HOST is
+ omitted, it defaults to the default host of the calling page.
+
+ PAGE is the same string that Cockpit uses in its URLs to identify a
+ page, such as "system/terminal" or "storage".
+
+ Until the page_status object is fully initialized (see 'valid'
+ below), this function will return 'undefined'.
+
+ - page_status.addEventListener("changed", event => { ... })
+
+ The "changed" event is emitted whenever any page status changes.
+
+ - page_status.valid
+
+ The page_status objects needs to initialize itself asynchronously and
+ 'valid' is false until this is done. When 'valid' changes to true, a
+ "changed" event is emitted.
+
+*/
+
+import cockpit from "cockpit";
+import deep_equal from "deep-equal";
+
+class PageStatus {
+ constructor() {
+ cockpit.event_target(this);
+ window.addEventListener("storage", event => {
+ if (event.key == "cockpit:page_status") {
+ this.dispatchEvent("changed");
+ }
+ });
+
+ this.cur_own = null;
+
+ this.valid = false;
+ cockpit.transport.wait(() => {
+ this.valid = true;
+ this.dispatchEvent("changed");
+ });
+ }
+
+ get(page, host) {
+ let page_status;
+
+ if (!this.valid)
+ return undefined;
+
+ if (host === undefined)
+ host = cockpit.transport.host;
+
+ try {
+ page_status = JSON.parse(sessionStorage.getItem("cockpit:page_status"));
+ } catch {
+ return null;
+ }
+
+ if (page_status?.[host])
+ return page_status[host][page] || null;
+ return null;
+ }
+
+ set_own(status) {
+ if (!deep_equal(status, this.cur_own)) {
+ this.cur_own = status;
+ cockpit.transport.control("notify", { page_status: status });
+ }
+ }
+}
+
+export const page_status = new PageStatus();
diff --git a/pkg/lib/os-release.js b/pkg/lib/os-release.js
new file mode 100644
index 0000000..d9aca1c
--- /dev/null
+++ b/pkg/lib/os-release.js
@@ -0,0 +1,38 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+function parse_simple_vars(text) {
+ const res = { };
+ for (const l of text.split('\n')) {
+ const pos = l.indexOf('=');
+ if (pos > 0) {
+ const name = l.substring(0, pos);
+ let val = l.substring(pos + 1);
+ if (val[0] == '"' && val[val.length - 1] == '"')
+ val = val.substring(1, val.length - 1);
+ res[name] = val;
+ }
+ }
+ return res;
+}
+
+/* Return /etc/os-release as object */
+export const read_os_release = () => cockpit.file("/etc/os-release", { syntax: { parse: parse_simple_vars } }).read();
diff --git a/pkg/lib/packagekit.js b/pkg/lib/packagekit.js
new file mode 100644
index 0000000..c11cd32
--- /dev/null
+++ b/pkg/lib/packagekit.js
@@ -0,0 +1,528 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017, 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import { superuser } from 'superuser';
+
+const _ = cockpit.gettext;
+
+// see https://github.com/PackageKit/PackageKit/blob/main/lib/packagekit-glib2/pk-enum.h
+export const Enum = {
+ EXIT_SUCCESS: 1,
+ EXIT_FAILED: 2,
+ EXIT_CANCELLED: 3,
+ ROLE_REFRESH_CACHE: 13,
+ ROLE_UPDATE_PACKAGES: 22,
+ INFO_UNKNOWN: -1,
+ INFO_LOW: 3,
+ INFO_ENHANCEMENT: 4,
+ INFO_NORMAL: 5,
+ INFO_BUGFIX: 6,
+ INFO_IMPORTANT: 7,
+ INFO_SECURITY: 8,
+ INFO_DOWNLOADING: 10,
+ INFO_UPDATING: 11,
+ INFO_INSTALLING: 12,
+ INFO_REMOVING: 13,
+ INFO_REINSTALLING: 19,
+ INFO_DOWNGRADING: 20,
+ STATUS_WAIT: 1,
+ STATUS_DOWNLOAD: 8,
+ STATUS_INSTALL: 9,
+ STATUS_UPDATE: 10,
+ STATUS_CLEANUP: 11,
+ STATUS_SIGCHECK: 14,
+ STATUS_WAITING_FOR_LOCK: 30,
+ FILTER_INSTALLED: (1 << 2),
+ FILTER_NOT_INSTALLED: (1 << 3),
+ FILTER_NEWEST: (1 << 16),
+ FILTER_ARCH: (1 << 18),
+ FILTER_NOT_SOURCE: (1 << 21),
+ ERROR_ALREADY_INSTALLED: 9,
+ TRANSACTION_FLAG_SIMULATE: (1 << 2),
+};
+
+export const transactionInterface = "org.freedesktop.PackageKit.Transaction";
+
+let _dbus_client = null;
+
+/**
+ * Get PackageKit D-Bus client
+ *
+ * This will get lazily initialized and re-initialized after PackageKit
+ * disconnects (due to a crash or idle timeout).
+ */
+function dbus_client() {
+ if (_dbus_client === null) {
+ _dbus_client = cockpit.dbus("org.freedesktop.PackageKit", { superuser: "try", track: true });
+ _dbus_client.addEventListener("close", () => {
+ console.log("PackageKit went away from D-Bus");
+ _dbus_client = null;
+ });
+ }
+
+ return _dbus_client;
+}
+
+// Reconnect when privileges change
+superuser.addEventListener("changed", () => { _dbus_client = null });
+
+/**
+ * Call a PackageKit method
+ */
+export function call(objectPath, iface, method, args, opts) {
+ return dbus_client().call(objectPath, iface, method, args, opts);
+}
+
+/**
+ * Figure out whether PackageKit is available and usable
+ */
+export function detect() {
+ function dbus_detect() {
+ return call("/org/freedesktop/PackageKit", "org.freedesktop.DBus.Properties",
+ "Get", ["org.freedesktop.PackageKit", "VersionMajor"])
+ .then(() => true,
+ () => false);
+ }
+
+ return cockpit.spawn(["findmnt", "-T", "/usr", "-n", "-o", "VFS-OPTIONS"])
+ .then(options => {
+ if (options.split(",").indexOf("ro") >= 0)
+ return false;
+ else
+ return dbus_detect();
+ })
+ .catch(dbus_detect);
+}
+
+/**
+ * Watch a running PackageKit transaction
+ *
+ * transactionPath (string): D-Bus object path of the PackageKit transaction
+ * signalHandlers, notifyHandler: As in method #transaction
+ * Returns: If notifyHandler is set, Cockpit promise that resolves when the watch got set up
+ */
+export function watchTransaction(transactionPath, signalHandlers, notifyHandler) {
+ const subscriptions = [];
+ let notifyReturn;
+ const client = dbus_client();
+
+ // Listen for PackageKit crashes while the transaction runs
+ function onClose(event, ex) {
+ console.warn("PackageKit went away during transaction", transactionPath, ":", JSON.stringify(ex));
+ if (signalHandlers.ErrorCode)
+ signalHandlers.ErrorCode("close", _("PackageKit crashed"));
+ if (signalHandlers.Finished)
+ signalHandlers.Finished(Enum.EXIT_FAILED);
+ }
+ client.addEventListener("close", onClose);
+
+ if (signalHandlers) {
+ Object.keys(signalHandlers).forEach(handler => subscriptions.push(
+ client.subscribe({ interface: transactionInterface, path: transactionPath, member: handler },
+ (path, iface, signal, args) => signalHandlers[handler](...args)))
+ );
+ }
+
+ if (notifyHandler) {
+ notifyReturn = client.watch(transactionPath);
+ subscriptions.push(notifyReturn);
+ client.addEventListener("notify", reply => {
+ const iface = reply?.detail?.[transactionPath]?.[transactionInterface];
+ if (iface)
+ notifyHandler(iface, transactionPath);
+ });
+ }
+
+ // unsubscribe when transaction finished
+ subscriptions.push(client.subscribe(
+ { interface: transactionInterface, path: transactionPath, member: "Finished" },
+ () => {
+ subscriptions.map(s => s.remove());
+ client.removeEventListener("close", onClose);
+ })
+ );
+
+ return notifyReturn;
+}
+
+/**
+ * Run a PackageKit transaction
+ *
+ * method (string): D-Bus method name on the https://www.freedesktop.org/software/PackageKit/gtk-doc/Transaction.html interface
+ * If undefined, only a transaction will be created without calling a method on it
+ * arglist (array): "in" arguments of @method
+ * signalHandlers (object): maps PackageKit.Transaction signal names to handlers
+ * notifyHandler (function): handler for https://cockpit-project.org/guide/latest/cockpit-dbus.html#cockpit-dbus-onnotify
+ * signals, called on property changes with (changed_properties, transaction_path)
+ * Returns: Promise that resolves with transaction path on success, or rejects on an error
+ *
+ * Note that most often you don't really need the transaction path, but want to
+ * listen to the "Finished" signal.
+ *
+ * Example:
+ * transaction("GetUpdates", [0], {
+ * Package: (info, packageId, _summary) => { ... },
+ * ErrorCode: (code, details) => { ... },
+ * },
+ * changedProps => { ... } // notify handler
+ * )
+ * .then(transactionPath => { ... })
+ * .catch(ex => { handle exception });
+ */
+export function transaction(method, arglist, signalHandlers, notifyHandler) {
+ return call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "CreateTransaction", [])
+ .then(([transactionPath]) => {
+ if (!signalHandlers && !notifyHandler)
+ return transactionPath;
+
+ const watchPromise = watchTransaction(transactionPath, signalHandlers, notifyHandler) || Promise.resolve();
+ return watchPromise.then(() => {
+ if (method) {
+ return call(transactionPath, transactionInterface, method, arglist)
+ .then(() => transactionPath);
+ } else {
+ return transactionPath;
+ }
+ });
+ });
+}
+
+export class TransactionError extends Error {
+ constructor(code, detail) {
+ super(detail);
+ this.detail = detail;
+ this.code = code;
+ }
+}
+
+/**
+ * Run a long cancellable PackageKit transaction
+ *
+ * method (string): D-Bus method name on the https://www.freedesktop.org/software/PackageKit/gtk-doc/Transaction.html interface
+ * arglist (array): "in" arguments of @method
+ * progress_cb: Callback that receives a {waiting, percentage, cancel} object regularly; if cancel is not null, it can
+ * be called to cancel the current transaction. if wait is true, PackageKit is waiting for its lock (i. e.
+ * on another package operation)
+ * signalHandlers, notifyHandler: As in method #transaction, but ErrorCode and Finished are handled internally
+ * Returns: Promise that resolves when the transaction finished successfully, or rejects with TransactionError
+ * on failure.
+ */
+export function cancellableTransaction(method, arglist, progress_cb, signalHandlers) {
+ if (signalHandlers?.ErrorCode || signalHandlers?.Finished)
+ throw Error("cancellableTransaction handles ErrorCode and Finished signals internally");
+
+ return new Promise((resolve, reject) => {
+ let cancelled = false;
+ let status;
+ let allow_wait_status = false;
+ const progress_data = {
+ waiting: false,
+ absolute_percentage: 0,
+ cancel: null
+ };
+
+ function changed(props, transaction_path) {
+ function cancel() {
+ call(transaction_path, transactionInterface, "Cancel", []);
+ cancelled = true;
+ }
+
+ if (progress_cb) {
+ if ("Status" in props)
+ status = props.Status;
+ progress_data.waiting = allow_wait_status && (status === Enum.STATUS_WAIT || status === Enum.STATUS_WAITING_FOR_LOCK);
+ if ("AllowCancel" in props)
+ progress_data.cancel = props.AllowCancel ? cancel : null;
+ if ("Percentage" in props && props.Percentage <= 100)
+ progress_data.absolute_percentage = props.Percentage;
+
+ progress_cb(progress_data);
+ }
+ }
+
+ // We ignore STATUS_WAIT and friends during the first second of a transaction. They
+ // are always reported briefly even when a transaction doesn't really need to wait.
+ window.setTimeout(() => {
+ allow_wait_status = true;
+ changed({});
+ }, 1000);
+
+ transaction(method, arglist,
+ Object.assign({
+ // avoid calling progress_cb after ending the transaction, to avoid flickering cancel buttons
+ ErrorCode: (code, detail) => {
+ progress_cb = null;
+ reject(new TransactionError(cancelled ? "cancelled" : code, detail));
+ },
+ Finished: exit => {
+ progress_cb = null;
+ resolve(exit);
+ },
+ }, signalHandlers || {}),
+ changed)
+ .catch(ex => {
+ progress_cb = null;
+ reject(ex);
+ });
+ });
+}
+
+/**
+ * Check Red Hat subscription-manager if if this is an unregistered RHEL
+ * system. If subscription-manager is not installed or required (not a
+ * Red Hat product), nothing happens.
+ *
+ * callback: Called with a boolean (true: registered, false: not registered)
+ * after querying subscription-manager once, and whenever the value
+ * changes.
+ */
+export function watchRedHatSubscription(callback) {
+ const sm = cockpit.dbus("com.redhat.RHSM1");
+
+ function check() {
+ sm.call(
+ "/com/redhat/RHSM1/Entitlement", "com.redhat.RHSM1.Entitlement", "GetStatus", ["", ""])
+ .then(result => {
+ const status = JSON.parse(result[0]);
+ callback(status.valid);
+ })
+ .catch(ex => console.warn("Failed to query RHEL subscription status:", JSON.stringify(ex)));
+ }
+
+ // check if subscription is required on this system, i.e. whether there are any installed products
+ sm.call("/com/redhat/RHSM1/Products", "com.redhat.RHSM1.Products", "ListInstalledProducts", ["", {}, ""])
+ .then(result => {
+ const products = JSON.parse(result[0]);
+ if (products.length === 0)
+ return;
+
+ // check if this is an unregistered RHEL system
+ sm.subscribe(
+ {
+ path: "/com/redhat/RHSM1/Entitlement",
+ interface: "com.redhat.RHSM1.Entitlement",
+ member: "EntitlementChanged"
+ },
+ () => check()
+ );
+
+ check();
+ })
+ .catch(ex => {
+ if (ex.problem != "not-found")
+ console.warn("Failed to query RHSM products:", JSON.stringify(ex));
+ });
+}
+
+/* Support for installing missing packages.
+ *
+ * First call check_missing_packages to determine whether something
+ * needs to be installed, then call install_missing_packages to
+ * actually install them.
+ *
+ * check_missing_packages resolves to an object that can be passed to
+ * install_missing_packages. It contains these fields:
+ *
+ * - missing_names: Packages that were requested, are currently not installed,
+ * and can be installed.
+ *
+ * - missing_ids: The full PackageKit IDs corresponding to missing_names
+ *
+ * - unavailable_names: Packages that were requested, are currently not installed,
+ * but can't be found in any repository.
+ *
+ * If unavailable_names is empty, a simulated installation of the missing packages
+ * is done and the result also contains these fields:
+ *
+ * - extra_names: Packages that need to be installed as dependencies of
+ * missing_names.
+ *
+ * - remove_names: Packages that need to be removed.
+ *
+ * - download_size: Bytes that need to be downloaded.
+ */
+
+export function check_missing_packages(names, progress_cb) {
+ const data = {
+ missing_ids: [],
+ missing_names: [],
+ unavailable_names: [],
+ };
+
+ if (names.length === 0)
+ return Promise.resolve(data);
+
+ function refresh() {
+ return cancellableTransaction("RefreshCache", [false], progress_cb);
+ }
+
+ function resolve() {
+ const installed_names = { };
+
+ return cancellableTransaction("Resolve",
+ [Enum.FILTER_ARCH | Enum.FILTER_NOT_SOURCE | Enum.FILTER_NEWEST, names],
+ progress_cb,
+ {
+ Package: (info, package_id) => {
+ const parts = package_id.split(";");
+ const repos = parts[3].split(":");
+ if (repos.indexOf("installed") >= 0) {
+ installed_names[parts[0]] = true;
+ } else {
+ data.missing_ids.push(package_id);
+ data.missing_names.push(parts[0]);
+ }
+ },
+ })
+ .then(() => {
+ names.forEach(name => {
+ if (!installed_names[name] && data.missing_names.indexOf(name) == -1)
+ data.unavailable_names.push(name);
+ });
+ return data;
+ });
+ }
+
+ function simulate(data) {
+ data.install_ids = [];
+ data.remove_ids = [];
+ data.extra_names = [];
+ data.remove_names = [];
+
+ if (data.missing_ids.length > 0 && data.unavailable_names.length === 0) {
+ return cancellableTransaction("InstallPackages",
+ [Enum.TRANSACTION_FLAG_SIMULATE, data.missing_ids],
+ progress_cb,
+ {
+ Package: (info, package_id) => {
+ const name = package_id.split(";")[0];
+ if (info == Enum.INFO_REMOVING) {
+ data.remove_ids.push(package_id);
+ data.remove_names.push(name);
+ } else if (info == Enum.INFO_INSTALLING ||
+ info == Enum.INFO_UPDATING) {
+ data.install_ids.push(package_id);
+ if (data.missing_names.indexOf(name) == -1)
+ data.extra_names.push(name);
+ }
+ }
+ })
+ .then(() => {
+ data.missing_names.sort();
+ data.extra_names.sort();
+ data.remove_names.sort();
+ return data;
+ });
+ } else {
+ return data;
+ }
+ }
+
+ function get_details(data) {
+ data.download_size = 0;
+ if (data.install_ids.length > 0) {
+ return cancellableTransaction("GetDetails",
+ [data.install_ids],
+ progress_cb,
+ {
+ Details: details => {
+ if (details.size)
+ data.download_size += details.size.v;
+ }
+ })
+ .then(() => data);
+ } else {
+ return data;
+ }
+ }
+
+ return refresh().then(resolve)
+ .then(simulate)
+ .then(get_details);
+}
+
+/* Check a list of packages whether they are installed.
+ *
+ * This is a lightweight version of check_missing_packages() which does not
+ * refresh, simulates, or retrieves details. It just checks which of the given package
+ * names are already installed, and returns a Set of the missing ones.
+ */
+export function check_uninstalled_packages(names) {
+ const uninstalled = new Set(names);
+
+ if (names.length === 0)
+ return Promise.resolve(uninstalled);
+
+ return cancellableTransaction("Resolve",
+ [Enum.FILTER_ARCH | Enum.FILTER_NOT_SOURCE | Enum.FILTER_INSTALLED, names],
+ null, // don't need progress, this is fast
+ {
+ Package: (info, package_id) => {
+ const parts = package_id.split(";");
+ uninstalled.delete(parts[0]);
+ },
+ })
+ .then(() => uninstalled);
+}
+
+/* Carry out what check_missing_packages has planned.
+ *
+ * In addition to the usual "waiting", "percentage", and "cancel"
+ * fields, the object reported by progress_cb also includes "info" and
+ * "package" from the "Package" signal.
+ */
+
+export function install_missing_packages(data, progress_cb) {
+ if (!data || data.missing_ids.length === 0)
+ return Promise.resolve();
+
+ let last_progress, last_info, last_name;
+
+ function report_progess() {
+ progress_cb({
+ waiting: last_progress.waiting,
+ percentage: last_progress.percentage,
+ cancel: last_progress.cancel,
+ info: last_info,
+ package: last_name
+ });
+ }
+
+ return cancellableTransaction("InstallPackages", [0, data.missing_ids],
+ p => {
+ last_progress = p;
+ report_progess();
+ },
+ {
+ Package: (info, id) => {
+ last_info = info;
+ last_name = id.split(";")[0];
+ report_progess();
+ }
+ });
+}
+
+/**
+ * Get the used backendName in PackageKit.
+ */
+export function getBackendName() {
+ return call("/org/freedesktop/PackageKit", "org.freedesktop.DBus.Properties",
+ "Get", ["org.freedesktop.PackageKit", "BackendName"]);
+}
diff --git a/pkg/lib/page.scss b/pkg/lib/page.scss
new file mode 100644
index 0000000..3da4f61
--- /dev/null
+++ b/pkg/lib/page.scss
@@ -0,0 +1,197 @@
+@use "@patternfly/patternfly/base/patternfly-themes.scss";
+@use "@patternfly/patternfly/patternfly-theme-dark.scss";
+@use "./patternfly/patternfly-5-overrides.scss";
+@import "global-variables";
+
+/* Globally resize headings */
+h1 {
+ --ct-heading-font-size: var(--pf-v5-global--FontSize--4xl);
+}
+
+h2 {
+ --ct-heading-font-size: var(--pf-v5-global--FontSize--3xl);
+}
+
+h3 {
+ --ct-heading-font-size: var(--pf-v5-global--FontSize--2xl);
+}
+
+h4 {
+ --ct-heading-font-size: var(--pf-v5-global--FontSize--lg);
+}
+
+// Only apply a custom font size when a heading does NOT have a PF4 class
+h1, h2, h3, h4 {
+ &:not([class*="pf-"]):not([data-pf-content="true"]) {
+ font-size: var(--ct-heading-font-size);
+ }
+}
+
+/* End of headings resize */
+
+a {
+ cursor: pointer;
+}
+
+.disabled {
+ pointer-events: auto;
+}
+
+.btn {
+ min-block-size: 26px;
+ min-inline-size: 26px;
+}
+
+.btn.disabled, .pf-v5-c-button.disabled {
+ pointer-events: auto;
+}
+
+.btn.disabled:hover, .pf-v5-c-button.disabled:hover {
+ z-index: auto;
+}
+
+.btn-group {
+ /* Fix button groups from wrapping in narrow widths */
+ display: inline-flex;
+}
+
+a.disabled {
+ cursor: not-allowed !important;
+ text-decoration: none;
+ pointer-events: none;
+ color: #8b8d8f;
+}
+
+a.disabled:hover {
+ text-decoration: none;
+}
+
+.highlight-ct {
+ background-color: var(--ct-color-link-hover-bg);
+}
+
+.curtains-ct {
+ inline-size: 100%;
+}
+
+/* Animation of new items */
+.ct-new-item {
+ animation: ctNewRow 4s ease-in;
+}
+
+:root {
+ --ct-animation-new-background: #fdf4dd;
+}
+
+.pf-v5-theme-dark {
+ --ct-animation-new-background: #353428;
+}
+
+/* Animation background is instantly yellow and fades out halfway through */
+@keyframes ctNewRow {
+ 0% {
+ background-color: var(--ct-animation-new-background);
+ }
+
+ 50% {
+ background-color: var(--ct-animation-new-background);
+ }
+}
+
+/* Dialog patterns */
+
+.dialog-wait-ct {
+ /* Right align footer idle messages after the buttons */
+ margin-inline-start: auto;
+ display: flex;
+ column-gap: var(--pf-v5-global--spacer--sm);
+ align-items: center;
+}
+
+:root {
+ /* Cockpit custom colors */
+ --ct-color-light-red: #f8cccc;
+ --ct-color-red-hat-red : #e00;
+
+ // Blend between --pf-v5-global--palette--black-200 and 300
+ --ct-global--palette--black-250: #e6e6e6;
+
+ /* Semantic colors */
+ --ct-color-fg: var(--pf-v5-global--color--100);
+ --ct-color-bg: var(--pf-v5-global--BackgroundColor--100);
+ --ct-color-text: var(--ct-color-fg);
+
+ --ct-color-link : var(--pf-v5-global--active-color--100);
+ --ct-color-link-visited: var(--pf-v5-global--active-color--100);
+
+ --ct-color-subtle-copy: var(--pf-v5-global--disabled-color--100);
+
+ // General border color (semantic shortcut, instead of specifying the color directly)
+ --ct-color-border: var(--pf-v5-global--BorderColor--100);
+
+ // Used for highlighting link blocks (with a light background blue)
+ --ct-color-link-hover-bg : var(--pf-v5-global--palette--light-blue-100);
+
+ /* Colors used for custom lists */
+ // as seen in Journal, Listing, Table widgets and pages like Machines, Updates, Services
+ --ct-color-list-text : var(--ct-color-text);
+ --ct-color-list-link : var(--ct-color-link);
+ --ct-color-list-bg : var(--ct-color-bg);
+ --ct-color-list-border : var(--ct-color-border);
+ --ct-color-list-hover-text : var(--ct-color-link);
+ --ct-color-list-hover-bg : var(--pf-v5-global--BackgroundColor--150);
+ --ct-color-list-hover-border : var(--pf-v5-global--BackgroundColor--150);
+ --ct-color-list-hover-icon : var(--pf-v5-global--palette--light-blue-400);
+ --ct-color-list-selected-text : var(--ct-color-link);
+ --ct-color-list-selected-bg : var(--pf-v5-global--BackgroundColor--150);
+ --ct-color-list-selected-border : var(--pf-v5-global--BackgroundColor--150);
+ --ct-color-list-active-text : var(--pf-v5-global--palette--blue-500);
+ --ct-color-list-active-bg : var(--ct-color-bg);
+ --ct-color-list-active-border : var(--ct-color-list-border);
+ --ct-color-list-critical-bg : var(--pf-v5-global--palette--red-50);
+ --ct-color-list-critical-border : #e6bcbc; // red-500 mixed with white @ 50%
+ --ct-color-list-critical-alert-text: var(--pf-v5-global--palette--red-200);
+}
+
+.pf-v5-theme-dark {
+ --ct-color-list-critical-bg : #261213; // red-100 mixed with black-850 @ 20%
+ --ct-color-list-critical-border : var(--pf-v5-global--danger-color--200);
+ --ct-color-list-critical-alert-text: var(--pf-v5-global--palette--red-8888);
+}
+
+[hidden] { display: none !important; }
+
+// Let PF4 handle the scrolling through page component otherwise we might get double scrollbar
+html:not(.index-page) body {
+ overflow-block: hidden;
+
+ // Ensure UI fills the entire page (and does not run over)
+ .ct-page-fill {
+ block-size: 100% !important;
+ }
+}
+
+.ct-icon-info-circle {
+ color: var(--pf-v5-global--info-color--100);
+}
+
+.ct-icon-exclamation-triangle {
+ color: var(--pf-v5-global--warning-color--100);
+}
+
+.ct-icon-times-circle {
+ color: var(--pf-v5-global--danger-color--100);
+}
+
+// Action buttons in headers add extra space. Offset that with a negative margin
+// to compensate, so headings are always the same height regardless of action
+// buttons or not.
+.pf-v5-c-page__main-breadcrumb .pf-v5-c-button {
+ --offset: calc(-1 * var(--pf-v5-global--spacer--sm));
+ margin-block: var(--offset);
+}
+
+// To be used only from testlib.py for pixel-tests
+main.pixel-test {
+ overflow-y: clip;
+}
diff --git a/pkg/lib/pam_user_parser.js b/pkg/lib/pam_user_parser.js
new file mode 100644
index 0000000..278f710
--- /dev/null
+++ b/pkg/lib/pam_user_parser.js
@@ -0,0 +1,95 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+function parse_passwd_content(content) {
+ if (!content) {
+ console.warn("Couldn't read /etc/passwd");
+ return [];
+ }
+
+ const ret = [];
+ const lines = content.split('\n');
+
+ for (let i = 0; i < lines.length; i++) {
+ if (!lines[i])
+ continue;
+ const column = lines[i].split(':');
+ ret.push({
+ name: column[0],
+ password: column[1],
+ uid: parseInt(column[2], 10),
+ gid: parseInt(column[3], 10),
+ gecos: (column[4] || '').replace(/,*$/, ''),
+ home: column[5] || '',
+ shell: column[6] || '',
+ });
+ }
+
+ return ret;
+}
+
+export const etc_passwd_syntax = {
+ parse: parse_passwd_content
+};
+
+function parse_group_content(content) {
+ // /etc/group file is used to set only secondary groups of users. The primary group is saved in /etc/passwd-
+ content = (content || "").trim();
+ if (!content) {
+ console.warn("Couldn't read /etc/group");
+ return [];
+ }
+
+ const ret = [];
+ const lines = content.split('\n');
+
+ for (let i = 0; i < lines.length; i++) {
+ if (!lines[i])
+ continue;
+ const column = lines[i].split(':');
+ ret.push({
+ name: column[0],
+ password: column[1],
+ gid: parseInt(column[2], 10),
+ userlist: column[3].split(','),
+ });
+ }
+
+ return ret;
+}
+
+export const etc_group_syntax = {
+ parse: parse_group_content
+};
+
+function parse_shells_content(content) {
+ content = (content || "").trim();
+ if (!content) {
+ console.warn("Couldn't read /etc/shells");
+ return [];
+ }
+
+ const lines = content.split('\n');
+
+ return lines.filter(line => !line.includes("#") && line.trim());
+}
+
+export const etc_shells_syntax = {
+ parse: parse_shells_content
+};
diff --git a/pkg/lib/patternfly/_fonts.scss b/pkg/lib/patternfly/_fonts.scss
new file mode 100644
index 0000000..841eee7
--- /dev/null
+++ b/pkg/lib/patternfly/_fonts.scss
@@ -0,0 +1,38 @@
+@mixin printRedHatFont(
+ $weightValue: 400,
+ $weightName: "Regular",
+ $familyName: "RedHatText",
+ $style: "normal",
+ $relative: true
+) {
+ $filePath: "../../static/fonts" + "/" + $familyName + "-" + $weightName;
+
+ @font-face {
+ font-family: $familyName;
+ src: url("#{$filePath}.woff2") format("woff2");
+ font-style: #{$style};
+ font-weight: $weightValue;
+ text-rendering: optimizelegibility;
+ }
+}
+
+@include printRedHatFont(700, "Bold", $familyName: "RedHatDisplay");
+@include printRedHatFont(700, "BoldItalic", $style: "italic", $familyName: "RedHatDisplay");
+@include printRedHatFont(900, "Black", $familyName: "RedHatDisplay");
+@include printRedHatFont(900, "BlackItalic", $style: "italic", $familyName: "RedHatDisplay");
+@include printRedHatFont(300, "Italic", $style: "italic", $familyName: "RedHatDisplay");
+@include printRedHatFont(400, "Medium", $familyName: "RedHatDisplay");
+@include printRedHatFont(400, "MediumItalic", $style: "italic", $familyName: "RedHatDisplay");
+@include printRedHatFont(300, "Regular", $familyName: "RedHatDisplay");
+@include printRedHatFont(700, "Bold");
+@include printRedHatFont(700, "BoldItalic", $style: "italic");
+@include printRedHatFont(400, "Italic", $style: "italic");
+@include printRedHatFont(700, "Medium");
+@include printRedHatFont(700, "MediumItalic", $style: "italic");
+@include printRedHatFont(400, "Regular");
+@include printRedHatFont(700, "Bold", $familyName: "RedHatMono");
+@include printRedHatFont(700, "BoldItalic", $style: "italic", $familyName: "RedHatMono");
+@include printRedHatFont(400, "Italic", $style: "italic", $familyName: "RedHatMono");
+@include printRedHatFont(500, "Medium", $familyName: "RedHatMono");
+@include printRedHatFont(500, "MediumItalic", $style: "italic", $familyName: "RedHatMono");
+@include printRedHatFont(400, "Regular", $familyName: "RedHatMono");
diff --git a/pkg/lib/patternfly/patternfly-5-cockpit.scss b/pkg/lib/patternfly/patternfly-5-cockpit.scss
new file mode 100644
index 0000000..8008e20
--- /dev/null
+++ b/pkg/lib/patternfly/patternfly-5-cockpit.scss
@@ -0,0 +1,9 @@
+/* Set fake font and icon path variables */
+$pf-v5-global--font-path: "patternfly-fonts-fake-path";
+$pf-v5-global--fonticon-path: "patternfly-icons-fake-path";
+$pf-v5-global--disable-fontawesome: true !default; // Disable Font Awesome 5 Free
+
+@import "@patternfly/patternfly/patternfly-base.scss";
+
+/* Import our own fonts since the PF4 font-face rules are filtered out in build.js */
+@import "./fonts";
diff --git a/pkg/lib/patternfly/patternfly-5-overrides.scss b/pkg/lib/patternfly/patternfly-5-overrides.scss
new file mode 100644
index 0000000..8cbf65f
--- /dev/null
+++ b/pkg/lib/patternfly/patternfly-5-overrides.scss
@@ -0,0 +1,391 @@
+/*** PF5 overrides ***/
+// Pull in variables (for breakpoints)
+@use "global-variables" as *;
+
+// PF Select is deprecated - no issue reported upstream - this needs to be removed from our codebase
+// Make select have the expected width
+.pf-v5-c-select[data-popper-reference-hidden="false"] {
+ inline-size: auto;
+}
+
+/* WORKAROUND: Navigation problems with Tertiary Nav widget on mobile */
+/* See: https://github.com/patternfly/patternfly-design/issues/840 */
+/* Helper mod to wrap pf-v5-c-nav__tertiary */
+.ct-m-nav__tertiary-wrap {
+ .pf-v5-c-nav__list {
+ flex-wrap: wrap;
+ }
+
+ .pf-v5-c-nav__scroll-button {
+ display: none;
+ }
+}
+
+/* Helper mod to center pf-v5-c-nav__tertiary when it wraps */
+.ct-m-nav__tertiary-center {
+ .pf-v5-c-nav__list {
+ justify-content: center;
+ }
+}
+
+/* Fix overflow issue with tabs, especially seen in small sizes, like mobile
+seen in:
+- https://github.com/cockpit-project/cockpit-podman/pull/897#issuecomment-1127637202
+- https://github.com/patternfly/patternfly/issues/1625
+- https://github.com/patternfly/patternfly/pull/2757
+- https://github.com/patternfly/patternfly/issues/4800
+- https://github.com/patternfly/patternfly-design/issues/840
+- https://github.com/patternfly/patternfly-design/issues/1034
+- https://github.com/cockpit-project/cockpit-podman/issues/845
+
+This disables the large and halfway useless overflow buttons and causes the tabs
+to wrap around when there isn't enough space.
+*/
+.pf-v5-c-tabs__list {
+ flex-wrap: wrap;
+}
+
+/* Fix select menu rendering */
+ul.pf-v5-c-select__menu {
+ /* Don't get too tall */
+ max-block-size: min(20rem, 50vh);
+ /* Don't have a horizontal scrollbar */
+ overflow-y: auto;
+}
+
+/* Adjust padding on form selects to resemble PF non-form selects */
+/* (This can be seen when the longest text is selected on a non-stretched select) */
+/* Upstream: https://github.com/patternfly/patternfly/issues/4387 */
+/* Cockpit-Podman: https://github.com/cockpit-project/cockpit-podman/issues/755 */
+select.pf-v5-c-form-control {
+ --pf-v5-c-form-control--PaddingRight: 41px;
+ --pf-v5-c-form-control--PaddingLeft: 8px;
+
+ // Firefox's select text has additional padding (4px)
+ @-moz-document url-prefix() {
+ --pf-v5-c-form-control--PaddingRight: 37px;
+ --pf-v5-c-form-control--PaddingLeft: 4px;
+ }
+}
+
+// Workaround the transparent background for HTML select options
+// https://github.com/patternfly/patternfly/issues/5695
+.pf-v5-c-form-control option {
+ background-color: var(--pf-v5-c-form-control--BackgroundColor);
+ color: var(--pf-v5-c-form-control--Color);
+}
+
+// The default gap between the rows in horizontal lists is too large
+.pf-v5-c-description-list.pf-m-horizontal-on-sm,
+.pf-v5-c-description-list.pf-m-horizontal {
+ --pf-v5-c-description-list--RowGap: 1rem;
+}
+
+.pf-v5-c-description-list {
+ // When using horizontal ruler inside description list it's just for the spacing - don't show it
+ > hr {
+ border-block-start: none;
+ }
+}
+
+.pf-v5-c-modal-box.pf-m-align-top {
+ // We utilize custom footers in dialogs
+ // Make sure that the buttons always appear in the next line from the inline alerts
+ .pf-v5-c-modal-box__footer {
+ flex-wrap: wrap;
+ row-gap: var(--pf-v5-global--spacer--sm);
+
+ > div:not(.pf-v5-c-button):not(.dialog-wait-ct) {
+ flex: 0 0 100%;
+ }
+ }
+}
+
+// don't hide long dialog titles
+.pf-v5-c-modal-box__title-text {
+ white-space: normal;
+}
+
+.pf-v5-c-card {
+ // https://github.com/patternfly/patternfly/issues/3959
+ --pf-v5-c-card__header-toggle--MarginTop: 0;
+
+ .pf-v5-c-card__header:not(.ct-card-expandable-header),
+ .pf-v5-c-card__header:not(.ct-card-expandable-header) .pf-v5-c-card__header-main {
+ // upstream fix (pending): https://github.com/patternfly/patternfly/pull/3714
+ display: flex;
+ flex-wrap: wrap;
+ row-gap: var(--pf-v5-global--spacer--sm);
+ justify-content: space-between;
+ }
+
+ .pf-v5-c-card__header:not(.ct-card-expandable-header) {
+ > .pf-v5-c-card__actions {
+ flex-wrap: wrap;
+ row-gap: var(--pf-v5-global--spacer--sm);
+
+ // PF4 CardActions act up when using buttons while the title is large of font
+ // https://github.com/patternfly/patternfly/issues/3713
+ // https://github.com/patternfly/patternfly/issues/4362
+ margin: unset;
+ padding-inline: var(--pf-v5-c-card__actions--PaddingLeft) unset;
+ }
+ }
+}
+
+// Add some spacing to nested form groups - PF4 does not support these yet
+// https://github.com/patternfly/patternfly-design/issues/1012
+.pf-v5-c-form__group-control {
+ .pf-v5-c-form__group, .pf-v5-c-form__section {
+ padding-block-start: var(--pf-v5-global--spacer--md);
+ }
+}
+
+// Alerts use elements that have fonts set in other frameworks (including PF3);
+// generally, this is an H4 that often has a font size and sometimes family set.
+// Therefore, it should inherit from the alert font set at the pf-v5-c-alert level.
+// https://github.com/patternfly/patternfly/issues/4206
+.pf-v5-c-alert__title {
+ font-size: inherit;
+ font-family: inherit;
+}
+
+.pf-v5-c-toolbar {
+ // Make summary content use the same vertical space as the filter toggle,
+ // when possible.
+ // https://github.com/patternfly/patternfly-design/issues/1055
+ &.ct-compact {
+ @media screen and (max-width: $pf-v5-global--breakpoint--lg - 1) {
+ display: flex;
+ flex-wrap: wrap;
+
+ > .pf-v5-c-toolbar__content:first-child {
+ flex: auto;
+ }
+
+ .pf-v5-c-toolbar__content-section {
+ inline-size: auto;
+ }
+ }
+ }
+}
+
+// When there is an Alert above the Form add some spacing
+.pf-v5-c-modal-box .pf-v5-c-alert + .pf-v5-c-form {
+ padding-block-start: var(--pf-v5-global--FontSize--sm);
+}
+
+// HACK: Not possible to specify text, so needs some hacks, see https://github.com/patternfly/patternfly-react/issues/6140
+.pf-v5-c-toolbar__toggle {
+ .pf-v5-c-button.pf-m-plain {
+ color: var(--pf-v5-c-button--m-link--Color);
+
+ .pf-v5-c-button__icon {
+ margin-inline-end: var(--pf-v5-global--spacer--sm);
+ }
+ }
+}
+
+// Flex should use gap, not a margin hack
+// https://github.com/patternfly/patternfly/issues/4523
+// Override default spacing from lg -> md
+.pf-v5-l-flex {
+ gap: var(--pf-v5-l-flex--spacer-base);
+
+ &:not([class*="pf-m-space-items-"]) {
+ > * {
+ --pf-v5-l-flex--spacer--column: 0;
+ }
+ gap: var(--pf-v5-l-flex--spacer--md);
+ }
+
+ // Negate the margin hack used by immediate flex children
+ // (except for nested flex, as we want to mind the gap)
+ > :not(.pf-v5-l-flex) {
+ --pf-v5-l-flex--spacer-base: 0;
+ }
+
+ // Undo all spacer modification adjustments
+ &[class*="pf-m-space-items-"] {
+ > * {
+ --pf-v5-l-flex--spacer--column: 0;
+ }
+ }
+
+ // Re-add spacer modification adjustments on the flex layout widget
+ // (using class attribute matching for handling breakpoint -on- also)
+ @each $size in (none, xs, sm, md, lg, xl, 2xl, 3xl, 4xl) {
+ &[class*="pf-m-space-items-#{$size}"] {
+ --pf-v5-l-flex--spacer-base: var(--pf-v5-l-flex--spacer--#{$size});
+ }
+ }
+}
+
+// Realign the radio and checks: https://github.com/patternfly/patternfly/issues/5802
+.pf-v5-c-radio,
+.pf-v5-c-check {
+ // `baseline` is different in Firefox than Chrome & WebKit; use `normal`
+ align-items: normal;
+
+ // Remove incorrect PF settings on the children
+ &__label,
+ &__input {
+ margin-block: auto;
+ align-self: unset;
+ transform: unset;
+ }
+
+ // Slightly shift the radio and check widgets
+ &__input {
+ // Shift up the checks/radios for (most) browsers
+ transform: translateY(-1px);
+ // Mozilla doesn't need the translation, so undo it
+ -moz-transform: none; // stylelint-disable property-no-vendor-prefix
+ // If the size is not specified, browsers may size it between 12px - 16px; so let's set it to the font size (which winds up 16px)
+ block-size: var(--pf-v5-global--FontSize--md);
+ // Use the height for width too
+ aspect-ratio: 1;
+ }
+}
+
+// InputMenus now use the PF Panel component which mistakenly uses position:
+// relative, when it needs to be set to absolute.
+// Additionally, it needs to be full width to properly align to the widget the
+// popover panel describes.
+// https://github.com/patternfly/patternfly-react/issues/7592
+.pf-v5-c-search-input__menu.pf-v5-c-panel {
+ position: absolute;
+ inline-size: 100%;
+}
+
+// Breadcrumb links should have the correct pointing hand cursor.
+//
+// PatternFly requires a "to" attribute for an actual link, but we use some
+// funky onClick JS for navigating and override it with a className.
+//
+// Therefore, instead of having a proper <a href="..."> being rendered, we need
+// to override the link. This is a problem with a (correct) assumption in PF
+// and our (incorrect) way of not using links (but using JavaScript) for
+// linking.
+//
+// Nevertheless, Cockpit needs to be adapted for this to work as expected.
+.pf-v5-c-breadcrumb__link {
+ cursor: pointer;
+}
+
+//Page headers are inconsistent with shadows and borders
+// https://github.com/patternfly/patternfly/issues/5184
+.pf-v5-c-page__main-group,
+.pf-v5-c-page__main-nav,
+.pf-v5-c-page__main-section.pf-m-light:not(:last-child) {
+ z-index: var(--pf-v5-c-page--section--m-shadow-bottom--ZIndex);
+ box-shadow: var(--pf-v5-c-page--section--m-shadow-bottom--BoxShadow);
+}
+
+// Dark mode fixes for several PF components
+.pf-v5-theme-dark {
+ // Change background color behind cards
+ // (matches PF surge website; PF doesn't specify otherwise)
+ .pf-v5-c-page__main-section {
+ --pf-v5-c-page__main-section--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-300);
+ }
+
+ // Adapt breadcrumb bar to be similar color as PF website
+ // (We use header bars in slightly different ways from PF)
+ // https://github.com/patternfly/patternfly/issues/5301
+ .pf-v5-c-page__main-breadcrumb {
+ --pf-v5-c-page__main-breadcrumb--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-100);
+ background-color: var(--pf-v5-global--BackgroundColor--dark-100);
+ }
+
+ // Fix input group background and borders
+ // (Looks fixed in PF5, but not in PF4)
+ .pf-v5-c-text-input-group {
+ background-color: var(--pf-v5-global--BackgroundColor--400);
+
+ .pf-v5-c-text-input-group__text {
+ &::before {
+ border-block-start-color: transparent;
+ border-inline-end-color: transparent;
+ border-inline-start-color: transparent;
+ }
+
+ &:is(:focus,:hover)::after {
+ border-block-end-color: var(--pf-v5-global--active-color--100);
+ }
+
+ &:not(:focus):not(:hover)::after {
+ border-block-end-color: var(--pf-v5-global--BorderColor--400);
+ }
+ }
+ }
+
+ // FIXME: https://github.com/patternfly/patternfly/issues/5278
+ .pf-v5-c-modal-box .pf-v5-c-table {
+ background-color: inherit;
+ }
+}
+
+// Fix icons in buttons
+.pf-v5-c-button__icon.pf-m-start {
+ margin-inline: 0 var(--pf-v5-c-button__icon--m-start--MarginRight);
+}
+
+// Drop side padding in mobile mode,
+// intended mainly for PF PageSection elements (pf-v5-c-page__main-section).
+// It's similar to adding padding={{ default: 'noPadding', sm: 'padding' }},
+// except this only affects the sides, not the top and bottom.
+@media screen and (max-width: $pf-v5-global--breakpoint--sm) {
+ .pf-v5-c-page__main > section.pf-v5-c-page__main-section:not(.pf-m-padding) {
+ padding-inline: 0;
+ }
+}
+
+// Patch tabular number 0s to not have the slash inside
+// https://github.com/RedHatOfficial/RedHatFont/issues/53
+// https://github.com/patternfly/patternfly/issues/5308
+@font-face {
+ /* red-hat-text-regular */
+ unicode-range: U+0030;
+ font-family: RedHatText;
+ font-style: normal;
+ font-weight: 400;
+ src: url(data:font/woff2;base64,d09GMgABAAAAAAe8ABAAAAAAHEQAAAdfAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGVA/RkZUTRwaGBuaeBxuBmAAgkIRCAqBAIEICwwAATYCJAMUBCAFk0wHNRsqG1FUjgoEfz9ujIGNa3r1k8ghpztyImrCBYeonxUhtHdiJ+VxOsW1XvMb+MfL1IXbjodirvftJrlLmXk8oXAIGtz3ZWMrDLGtwvP7dO1vUK18MEuOuwr1HGbdxvxtJkjRURZ0AACm/+Q57n/s4iCgJJL8gTb8spMpWXyTfaB1cheGpDVP+q9RXC7EbPF87t79W+ptEWcSRJxLBcrqiyc9mJ/A5OTLl8rkeDKpTCaVSqRSeRKJRCKRJ6lV11TVDu9HyuJ0/Y9TL5b/hy5+R0aejKwkK8mXBOLAMYRbAP/3rn1aYAcsaY7cXWsUktB1QLOX5Gj3FzgHqMqYN5+yBWRFikmVJYERwEKRA5YVBoWr1JXdhjkRb+jfXuUqsphizGjMnud5RoEapc7yJhDeW1SeZtBWnpvP6D8mOcO8dhBSraNojaD2yybgbQPwhjbA2xMCHdEVJEokAu2t9Yp/EWNaoYXAQK3LPs8IEysenyvh2++a4Tsfb0To2HHUVRk68cYrZfpQy05dn0A66VSbbF6hjflJMm9Q4RWVFEBMaI5CFkyaph/NGFGHKzOpeYPeeusrrfoL30CHAjIyjDLJn/GXjQJSMhSG16nl6boWG2QPw4vDk5q39d2BXlhvqrHx5IoCRAZcxvNOMgGQdGa3It3wh0L1cq0yZS93LQasSKAXZKHF5T/jpNed7iewLTa3BLO3HQ1chV4/PqWW5xsv9scjxfLEvg6W66ymBWQLnF4HwG05Qpjl+0Mt3yccNMDFALN63HnhmwhUAJgBUEJ01QXSVgLLZUjnGeAUbel0vddcbCr2SsAlQulrmmtzEqdHsVAOfeWleo/JPLQOamTU66LjeNhzp6KqAKB1MLTQBx8Uae18DIfaBXcA43mTUQeLmrBAwtF8Fy1NAslad5ibzH4kuMkgqErYAITmyx40CQlJ7dTem8p1V1IvnRFDsnzeUQe+aPuhHgG95VfxBtGxZoZJIF8r8p9XmYjEGxId7kNBPkX6ZksLGbHULnNBv5t2vKjt1nwQy2RXecnhRbVxpSMPi83HqMSQdRjE6NC0oEOdCTlVp3RmQebQbDTfd+E4OkYMPkagMB7MMPPbJzLYBXAVDQJYK3WrCOgXKXJ5hgdPgi+g90EfCHrjAAyGAxYFWZXHcGXhJ9Tg7OiH21m2gmfWFKi20Wo8Fn1TaG9iJiMNhT01isE6Qvcm9RoGQS/YLXVkx2XaQv4maOZD2wWPr8rbmZXzhscBbT4r2qJHotccXba3jCDkSZftjYHIKk5GPyXQfSgKNpUYOdkzRxW9rW7DaFEgoHJdZoaGC2BoFF4xMOqN5kS0/PNMJBgD6B4HQxayhVWEIHBcS5MOpQdgb4JQPebjAXMo3pabQwRzohBGV30Go/tBZr+aP5dVaWTQLzQE6snLfFBLG3ktDbDB3oQ4jHosgoyqKDMaRfEYTk7gKBgLbFo3Jg7V1Irfb3Wuo8B9gPiWAENpKFBgIAEWaqFSZiiY6kuBqS2vuH+AaBCASCSFGoe/icLdLQrRYgBUoD1KDAXTAZQASFkXTvv+yB20DbvmmL/ndTqfMVDiT7SITwPFfydBBs3A8LD73aoGzdv55V+pTP9sAhCIvP2w2u7IgN+p+LZo4exWW8+1pJYgkAtG5QiyRQ4LqoD/PmITkA9s5V6sEqh0pYFECCwFwNC4RAD2+YcAoWOMJUBqxioIUBgUVwlQ6hmvAZVOmQI0dM/ZBGjalwNqXYqLEKCdLkUbAdq723oBHfStHhGgo97VF0AndzuEAJ0NaP4EdDdaT9MiynZ4Avgg9I3OkDrHLCjMj01QGhttUOkfX6BhZHaFpidagdrgYo9vu8DL0D6G90voYGp1HDqaWL2CTjF8/IPOZjef/gu6W66bVsgVfikhX4CRupq6TeRZwEUeuRzkksTIPSUXcdkYOVJhgZxSIssCjBVK2wwGX4gFKhadLZcy4LM7L+qOxxOyhaQk6Ejnchm2Atvk4XJp9Wykhn90HbqUvDyHbUGlLZ8X/RB+yEFlzV/sENPhgW+dyD7HbRtnvgK64z9z/WD4IdzyL/vrCb5KUqRgL+V9gGn0v/D+GnY0nbhh2vk9osu2G1CDTZtcqzd2+tEFO4khorWB4FwD7QhObbCD7uVubobyQza9yZpYiWo0ZM6BRlOJ/k62KJ+tiHYGkk+RUutIGj2xdQeb8gqJOq9Tr3dzHPI6XKhEaJ0NeZLzsG+S4qLA/IQTxDHLLORUvlCOrzI5QwlPrpjyThGjIGgmECZItiwss4xT+CKps5O+SKGEZEm4yHduMQitPPLg2zjbRkKQJVtsYxZYSVeC5uvSOZDPcOecCUs5sLfwTQescCutiZZg9H66MvflaXyLncfLRP+Q9s7YYoJBaHRhkz5y+KMHkrxNcmEXqS7LMFm5svK7cxOE8r9LB9albLEE+80Cl7eGlTrlqR0nS1dQgkQ3bYuWJLrmZTiraE+GjP8IPrtDF85Ce3Y081ATaMVR2G+FkHpE1ksdy3WWFgAA);
+}
+
+@font-face {
+ /* red-hat-text-italic */
+ unicode-range: U+0030;
+ font-family: RedHatText;
+ font-style: italic;
+ font-weight: 400;
+ src: url(data:font/woff2;base64,d09GMgABAAAAAARQAAsAAAAADYwAAAQEAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgkIRCAqBKIE/CwwAATYCJAMUBCAFkz0HIBvTDBGVnFUBfhzYbVYrgpXBW2mcjbMQzKj2CJ7Xi/85N8mCj8I31EGoJLfybs1VZl6ZyBK4Vh2jT3nLs5sj4dUngEuZ1e+dluoRtq0zKIlQcJ/3KFXzHQpsqVVIHAiJKjWtCRXZx1eZiYvUsXGRKq/wZqa/mnANebooLlGQzkvbYH3zQmMn02IhKYosT9HMsus4eAn7YwxsuHz9BsIUU7K//3+D7O+YisshfkyPYohAhgwDBFggExggNwS5XGBKp5Ns77wjgCgJZxAJSUd/+EtkvGcTotPRBgWoIcdUbAJoACBJzValtf/57znl3Xdr570fp36y5XnFPKe8Vz//gw/i1Pe/mp7yyQfnv/fhbu++FwW2/uD992tAS899ft8Trzjx6K1fv+LYrcLjHn6ydsziJQfvs/T+vXbd9uCNvNW7bXOQc2rfHfXM6M2tz2QH3HJDdeOhZ6/bcNr60+5qHVurzlm/8V2ceq8KTOj8lrZLLwhEuvjgRyZds3N7wW+VevYjfH7H+xfAF1/9ejG89NDNSUUCgYkJtRFiwh566jiLuf8msSHpIqvTNiYIFPpLICEElgAYEmcJwLYeEiA0YowASSmWC5AZEGcIkOseLwlQaMZfApR0TeMFKNs2/SRARTvbX4DquPJK1Fyav4G6PsXlaOhVfIymS8s90dK//MXJ3XSvDJeJvIoH8BD6RAtJI2YiMyfWITcqrkehX3yGkmGpjLIH0kJUDMrWS6vK41GLYdmdqJtS7IqGCcVdaMaw4ie0zCrfelfdjCoT/mNmMPqRhFKFoYAn4EN3lRy6ymXQGkHdzZk0qOVSDE28sMpAUpCmwthIibhcJeFtfkk4UoOOS9ucfNVJoSCkBNJCfGRp0GM6WHHSAca2e5XjNc9wTbxfzu6Ea9L+bzesFFy9ku2ECd4U8btM2SS6R3W0Dm7Nvct9MWBCd3CGAGwwMUlgN3wZ49tTAJ/DEwjFDmZ29mJ3cIrNNpFQ8POCMF6hO2OBiJHTJl2hWM1IqAmEdGPw2aecpAiDHtYuOAoWBOgaRC0j30/SgKTns3uMlCTSsaA5LIXzPVMpqbNQ3zXGyb+rCAq66SboZlBgH0TKIZRZb8TDpTTgfRmXZlLpYu2obzb24skIX0S9eU9FapWzKOozbZflzSW6GXkjQoskWjn00StBUChOXManswhawW0pKUkYMcWhCC3HQCq5Tpb2YA+VVMJP/LjFHXYFe5++22b/HhYBgX2DvWDJMKYOJMjwm/C3tCQUO7kgcYS2wn4xVlSi3TUef1ruwTWOT/dsRyfVaLGfUW7bTbBL7u/Spt0zJvsrQ671aEScUIObukdTouZX0C1qsUxUSreOW5fgH/+/9g2dIwMA);
+}
+
+@font-face {
+ /* red-hat-text-medium */
+ unicode-range: U+0030;
+ font-family: RedHatText;
+ font-style: normal;
+ font-weight: 700;
+ src: url(data:font/woff2;base64,d09GMgABAAAAAARYAAsAAAAADewAAAQLAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgkIRCAqBBIEUCwwAATYCJAMUBCAFlEEHIBssDRHF5AH4ceCUZY9pfajQk3kIirq5CjGXcNU3dzw879+/rX1uIKlLfhd0AMukgzVCNamGesTI7ZlyoyVrc1NnSh8mwlFTCbdmKtP+Dit8tlI4uJx+ywNd5wGHgUayB46pjw3cEzAMA9obaA2wrQFmmmYhpw1jFzajZpAvnYxflbQxTcLQpXLM0Ca6k6JAlq87j+w4K3uxFI1geN+hw/TSSqvs7/9fkv0drbEL4rd0DbUEMmSoJkA3mUA1+dRCPvkEWuVy9Wvk3gogChCASEg2jMM3kQFbCpHLeQX5QRHkQwN0AuQDQJKKgBaFvt38Uaz7eMt7a7/Z9PEZ8aON78yfr31ftGdVrYts/HLjRxs+3vR+/avCjj48fmJf0pGHJ47HC07O3LomD7Q8+b81b41tT7+YcqBX76kHP6toeve6uOr7HbdHcPpTA4FI2z6r8ajDzNduv61o9gG+OvXRHvj62x9Z3dxvaeV5iEISCMRN8R8greRLWf67/xgxnHRSmzRJXYH8ykkgIQR6AagXFwRgsnMChLLRQYCkYEwRIFMtDguQT9n4VoD8iqfSAhRQOvUVoKDJWWEBCimZbRCgsJLZWQGK2JHvOwGK0qrU0zRQTIX+TAPF7ShYAw2UUPX+QQOlPK2B0h43qH8Zn1vtTryFcQacQ2iQEpCUyBDIdMocyKdBLkN+VfIbFFDn1ISCzqQTFFLjmYspTNgFRVLn7i4U1ap5UEyz3oPiqfNYGErocG9CKX8rC6U9rcsvKCO/kQYSqRuJuDwMtTW1taAjjw3t2SxoSmBoS0r4bCaGejLMk5AUVORhLKV0NTS4CPNkDHWmRKTxbV7zrA2Hg5iIED5xz1gixkpA9eTYMhodPRv3toVTwYZzfnC4qGIhd21cYi8zNAv28+xKENSUf4kTUDV3CeiKHtmuZ5iVq4VkIvBLjqMuL1k33vskVyY0SJAOHQrQWp1NH+/QrQwsLOn0E/yJTk3V+ls1HV21l+kE5fySUAsfcTET7pUUkoghMeVgReYSIGkgqh/yvZMCCzNqeIwJLkmIVKFwqAij/piFK0VURf4O8Tvu43iIgnSaBx0kHOxCkGz4FBoi5j/kigfE2TnGwkg58w7pYGap20if9/p69PIrmKo/EcXZ83nb778FJJpPOBNISDCEbOjCr5wnOFjXszsvzLpQFCxKMUkkxZQ6hYTqEpKrYWNsCYyvHC4RuAopKPidh9FUe5gtE2nhvsDPoN1DZrBablAY/vtRWGYtRDj3dii4i2KFX/5YnaLQ3Jx/M14CbaSX7fC6iCkQYjcpW2xHgXndGplBaGzmiD8/n4udCiLBhyIarSIVSpzlI9gYQiEnAv1uYLpLNP0kBmgE4daZBPYHprlOLAA=);
+}
+
+@font-face {
+ /* red-hat-text-medium-italic */
+ unicode-range: U+0030;
+ font-family: RedHatText;
+ font-style: italic;
+ font-weight: 700;
+ src: url(data:font/woff2;base64,d09GMgABAAAAAASEAAsAAAAADlwAAAQ2AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgkIRCAqBFIEsCwwAATYCJAMUBCAFlSEHIBukDRGVnM0Bfh7YtpMt1pMMwRCUu0ZjuTUEq7QbeQVD9K0cz/Ot8btvZnf/YKZJNFnHE01D/RnzSKWKhUyUChVONznY33vvR2dGwQ2aI5GoNmehLcEOiWwABJeyW3OVmVcm8ohUq47RZ7FbwPRAePVny2WpHmHbKIFFSYTiZgv0eNbhCK663oXEgZAouis1niAM8d0RrIJ/++6lfvsW4tlZz+XCcgddeYS86ClXUGEcuZ9pztoaHSVD7Uom1PoYXN4HzmM1HDvo8UJstrl+/duh/ooIdyFfyySwWFBRMV/ALlUwX2cxOp1g8////ez/7wTIYNOHFBRPpuM3UT2dduT/fwTQg7HosBaHAOMBUJSJ41s7f/UIRt8fohuPkYqTk0DjIYx+wDSdIBXHF4dHTScnx41XjWvbCeXg09bl7Kq2spLoZndx7KxpLys+bE+xPzlSvtEU9Kdaw+XrzSFfCjx7S7xYdVVkdxPPNt0V6Tn3pXNM35O5+5Lj8TVY8f+1pJQVQcrNGpprY9Ik7deGqZ/h49BxJ+DT3euFXl45vWgKCHjCmw1FF5wzw5Or//U/Ro5RHsGeErZC0JurgIII9gFYngkI4ITnAmJa5AKKIccFVEvTCwGdufkT0JtQdggYzCr5AkacqAYBzaT6BgLGmFS/CBjrdk8VMM7s/k7AeDMHpoAJbo+kCZhoXuMJmGx828MUE9q1+VPNbbuqdGPwlM8QK4OHYnwOQ3UoSdDZnlXozS1EGGwsFhjxtDRBs7B2yceo12BsltZPGGdzPw7jre+/YEKWDgaYaOfIK0w2u7lgirlNLucvmGpSt7lM7qfkWAGGRgZGhtBDQEM3moI2CEMXpUxIkxiaqrFAplRBDQHGchVPX5/lsEBN6JEyib5sun/VmWE4kkNiintWMinWBKkq1j5mPQKNN61wQ1xoZ4PJlTQWfjfEOg5qkYlI9NKWD4LKvsIBWNs3wdfAg/b9JD/GK04tASqxsZdO7i0AsMW2oxcemJdGiAQY6v2hj4/5jub2DnznuBqjbimBQcMNRmM/O3D5FqNuVZcqy4v1JSg8sy8eXD/7opUqTiaFNzdUr4ltdQWPsnLjOwlH+QU1zSZWiSQ6EEejweUpky0p6SA48swT/xBwKuiT9El+l5vYD1LSkIqGHupwVyFgqz9rKPLSzaGjvds6iLOcPkV66UGKvJMJY/hKQzuzbm7RfuSNODEixDT0CVekwI2Yun7uzTyIAlGRSAvNsUrPEhHr4Wac1Xe2cgCFYQwS3sbkcQPzJRV0vfftYZIvhIHN7j0prlgOThTI+EtwCbhkFD29IrGHWGFLMysqF5fCO59XB/DG+Xle3pOQIjH2k9PYLoHSvy+RUVwIR/xlEFc+BxEXVNQ/eHIo4f1HsH/OR5zIVm52mVCpjKZqyJHY+j1zNf8f/y+v5iopAAAAAA==);
+}
+
+// Fix for https://github.com/patternfly/patternfly/issues/5855
+.pf-v5-u-font-weight-light {
+ font-weight: 300;
+}
+
+// Workaround for https://github.com/patternfly/patternfly/issues/5865
+.pf-v5-c-card.pf-m-selectable, .pf-v5-c-card.pf-m-clickable {
+ isolation: auto;
+}
diff --git a/pkg/lib/plot.js b/pkg/lib/plot.js
new file mode 100644
index 0000000..19c8d7f
--- /dev/null
+++ b/pkg/lib/plot.js
@@ -0,0 +1,574 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+
+/* The public API here is not general and a bit weird. It can only do
+ * what Cockpit itself needs right now, and what can be easily
+ * implemented without touching the older Metrics_series classes.
+ *
+ * The basic idea is that you create a global PlotState object and
+ * keep it alive for the lifetime of the page. Then you instantiate
+ * ZoomControl and SvgPlot components as needed.
+ *
+ * To control what to plot, you call some methods on the PlotState
+ * object:
+ *
+ * - plot_state.plot_single(id, metric_options)
+ *
+ * - plot_state.plot_instances(id, metric_options, instances, reset)
+ *
+ * You need to figure out the rest of the details from the existing
+ * users, unfortunately.
+ */
+
+class Metrics_series {
+ constructor(desc, opts, grid, plot_data, interval) {
+ cockpit.event_target(this);
+ this.desc = desc;
+ this.options = opts;
+ this.grid = grid;
+ this.plot_data = plot_data;
+ this.interval = interval;
+ this.channel = null;
+ this.chanopts_list = [];
+ }
+
+ stop() {
+ if (this.channel)
+ this.channel.close();
+ }
+
+ remove_series() {
+ const pos = this.plot_data.indexOf(this.options);
+ if (pos >= 0)
+ this.plot_data.splice(pos, 1);
+ }
+
+ remove() {
+ this.stop();
+ this.remove_series();
+ this.dispatchEvent("removed");
+ }
+
+ build_metric(n) {
+ return { name: n, units: this.desc.units, derive: this.desc.derive };
+ }
+
+ check_archives() {
+ if (this.channel.archives)
+ this.dispatchEvent("changed");
+ }
+}
+
+class Metrics_sum_series extends Metrics_series {
+ constructor(desc, opts, grid, plot_data, interval) {
+ super(desc, opts, grid, plot_data, interval);
+ if (this.desc.direct) {
+ this.chanopts_list.push({
+ source: 'direct',
+ archive_source: 'pcp-archive',
+ metrics: this.desc.direct.map(this.build_metric, this),
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+ if (this.desc.pmcd) {
+ this.chanopts_list.push({
+ source: 'pmcd',
+ metrics: this.desc.pmcd.map(this.build_metric, this),
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+ if (this.desc.internal) {
+ this.chanopts_list.push({
+ source: 'internal',
+ metrics: this.desc.internal.map(this.build_metric, this),
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+ }
+
+ flat_sum(val) {
+ if (!val)
+ return 0;
+ if (val.length !== undefined) {
+ let sum = 0;
+ for (let i = 0; i < val.length; i++)
+ sum += this.flat_sum(val[i]);
+ return sum;
+ }
+ return val;
+ }
+
+ reset_series() {
+ if (this.channel)
+ this.channel.close();
+
+ this.channel = cockpit.metrics(this.interval, this.chanopts_list);
+
+ const metrics_row = this.grid.add(this.channel, []);
+ const factor = this.desc.factor || 1;
+ const threshold = this.desc.threshold || null;
+ const offset = this.desc.offset || 0;
+ this.options.data = this.grid.add((row, x, n) => {
+ for (let i = 0; i < n; i++) {
+ const value = offset + this.flat_sum(metrics_row[x + i]) * factor;
+ if (threshold !== null)
+ row[x + i] = [(this.grid.beg + x + i) * this.interval, Math.abs(value) > threshold ? value : null, threshold];
+ else
+ row[x + i] = [(this.grid.beg + x + i) * this.interval, value];
+ }
+ });
+
+ this.channel.addEventListener("changed", this.check_archives.bind(this));
+ this.check_archives();
+ }
+}
+
+class Metrics_stacked_instances_series extends Metrics_series {
+ constructor(desc, opts, grid, plot_data, interval) {
+ super(desc, opts, grid, plot_data, interval);
+ this.instances = { };
+ this.last_instance = null;
+ if (this.desc.direct) {
+ this.chanopts_list.push({
+ source: 'direct',
+ archive_source: 'pcp-archive',
+ metrics: [this.build_metric(this.desc.direct)],
+ metrics_path_names: ['a'],
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+ if (this.desc.pmcd) {
+ this.chanopts_list.push({
+ source: 'pmcd',
+ metrics: this.desc.pmcd.map(this.build_metric, this),
+ metrics_path_names: ['a'],
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+
+ if (this.desc.internal) {
+ this.chanopts_list.push({
+ source: 'internal',
+ metrics: [this.build_metric(this.desc.internal)],
+ metrics_path_names: ['a'],
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+ }
+
+ reset_series() {
+ if (this.channel)
+ this.channel.close();
+ this.channel = cockpit.metrics(this.interval, this.chanopts_list);
+ this.channel.addEventListener("changed", this.check_archives.bind(this));
+ this.check_archives();
+ for (const name in this.instances)
+ this.instances[name].reset();
+ }
+
+ add_instance(name, selector) {
+ if (this.instances[name])
+ return;
+
+ const instance_data = Object.assign({ selector }, this.options);
+ const factor = this.desc.factor || 1;
+ const threshold = this.desc.threshold || 0;
+ const last = this.last_instance;
+ let metrics_row;
+
+ function reset() {
+ metrics_row = this.grid.add(this.channel, ['a', name]);
+ instance_data.data = this.grid.add((row, x, n) => {
+ for (let i = 0; i < n; i++) {
+ const value = (metrics_row[x + i] || 0) * factor;
+ const ts = (this.grid.beg + x + i) * this.interval;
+ let floor = 0;
+
+ if (last) {
+ if (last.data[x + i][1])
+ floor = last.data[x + i][1];
+ else
+ floor = last.data[x + i][2];
+ }
+
+ if (Math.abs(value) > threshold) {
+ row[x + i] = [ts, floor + value, floor];
+ if (row[x + i - 1] && row[x + i - 1][1] === null)
+ row[x + i - 1][1] = row[x + i - 1][2];
+ } else {
+ row[x + i] = [ts, null, floor];
+ if (row[x + i - 1] && row[x + i - 1][1] !== null)
+ row[x + i - 1][1] = row[x + i - 1][2];
+ }
+ }
+ });
+ }
+
+ function remove() {
+ this.grid.remove(metrics_row);
+ this.grid.remove(instance_data.data);
+ const pos = this.plot_data.indexOf(instance_data);
+ if (pos >= 0)
+ this.plot_data.splice(pos, 1);
+ }
+
+ instance_data.reset = reset.bind(this);
+ instance_data.remove = remove.bind(this);
+ instance_data.name = name;
+ this.last_instance = instance_data;
+ this.instances[name] = instance_data;
+ instance_data.reset();
+ this.plot_data.push(instance_data);
+ this.grid.sync();
+ }
+
+ clear_instances() {
+ for (const i in this.instances)
+ this.instances[i].remove();
+ this.instances = { };
+ this.last_instance = null;
+ }
+}
+
+class Plot {
+ constructor(element, x_range_seconds, x_stop_seconds) {
+ cockpit.event_target(this);
+
+ this.series = [];
+ this.plot_data = [];
+
+ this.interval = Math.ceil(x_range_seconds / 1000) * 1000;
+ this.grid = null;
+
+ this.sync_suppressed = 0;
+ this.archives = false;
+
+ this.reset(x_range_seconds, x_stop_seconds);
+ }
+
+ refresh() {
+ this.dispatchEvent("plot", this.plot_data);
+ }
+
+ start_walking() {
+ this.grid.walk();
+ }
+
+ stop_walking() {
+ this.grid.move(this.grid.beg, this.grid.end);
+ }
+
+ reset(x_range_seconds, x_stop_seconds) {
+ // Fill the plot with about 1000 samples, but don't sample
+ // faster than once per second.
+ //
+ // TODO - do this based on the actual size of the plot.
+ this.interval = Math.ceil(x_range_seconds / 1000) * 1000;
+
+ const x_offset = (x_stop_seconds !== undefined)
+ ? (new Date().getTime()) - x_stop_seconds * 1000
+ : 0;
+
+ const beg = -Math.ceil((x_range_seconds * 1000 + x_offset) / this.interval);
+ const end = -Math.floor(x_offset / this.interval);
+
+ if (this.grid && this.grid.interval == this.interval) {
+ this.grid.move(beg, end);
+ } else {
+ if (this.grid)
+ this.grid.close();
+ this.grid = cockpit.grid(this.interval, beg, end);
+ this.sync_suppressed++;
+ for (let i = 0; i < this.series.length; i++) {
+ this.series[i].stop();
+ this.series[i].interval = this.interval;
+ this.series[i].grid = this.grid;
+ this.series[i].reset_series();
+ }
+ this.sync_suppressed--;
+ this.sync();
+
+ this.grid.addEventListener("notify", (event, index, count) => {
+ this.refresh();
+ });
+ }
+ }
+
+ sync() {
+ if (this.sync_suppressed === 0)
+ this.grid.sync();
+ }
+
+ destroy() {
+ this.grid.close();
+ for (let i = 0; i < this.series.length; i++)
+ this.series[i].stop();
+
+ this.options = { };
+ this.series = [];
+ this.plot_data = [];
+ }
+
+ check_archives() {
+ if (!this.archives) {
+ this.archives = true;
+ this.dispatchEvent('changed');
+ }
+ }
+
+ add_metrics_sum_series(desc, opts) {
+ const sum_series = new Metrics_sum_series(desc, opts, this.grid, this.plot_data, this.interval);
+
+ sum_series.addEventListener("removed", this.refresh.bind(this));
+ sum_series.addEventListener("changed", this.check_archives.bind(this));
+ sum_series.reset_series();
+ sum_series.check_archives();
+
+ this.series.push(sum_series);
+ this.sync();
+ this.plot_data.push(opts);
+
+ return sum_series;
+ }
+
+ add_metrics_stacked_instances_series(desc, opts) {
+ const stacked_series = new Metrics_stacked_instances_series(desc, opts, this.grid, this.plot_data, this.interval);
+
+ stacked_series.addEventListener("removed", this.refresh.bind(this));
+ stacked_series.addEventListener("changed", this.check_archives.bind(this));
+ stacked_series.reset_series();
+ stacked_series.check_archives();
+
+ this.series.push(stacked_series);
+ this.sync_suppressed++;
+ for (const name in stacked_series.instances)
+ stacked_series.instances[name].reset();
+ this.sync_suppressed--;
+ this.sync();
+
+ return stacked_series;
+ }
+}
+
+class ZoomState {
+ constructor(reset_callback) {
+ cockpit.event_target(this);
+
+ this.reset_callback = reset_callback;
+
+ this.x_range = 5 * 60;
+ this.x_stop = undefined;
+ this.history = [];
+
+ this.enable_zoom_in = false;
+ this.enable_zoom_out = true;
+ this.enable_scroll_left = true;
+ this.enable_scroll_right = false;
+ }
+
+ reset() {
+ const plot_min_x_range = 5 * 60;
+
+ if (this.x_range < plot_min_x_range) {
+ this.x_stop += (plot_min_x_range - this.x_range) / 2;
+ this.x_range = plot_min_x_range;
+ }
+ if (this.x_stop >= (new Date()).getTime() / 1000 - 10)
+ this.x_stop = undefined;
+
+ this.reset_callback(this.x_range, this.x_stop);
+
+ this.enable_zoom_in = (this.x_range > plot_min_x_range);
+ this.enable_scroll_right = (this.x_stop !== undefined);
+
+ this.dispatchEvent("changed");
+ }
+
+ set_range(x_range) {
+ this.history = [];
+ this.x_range = x_range;
+ this.reset();
+ }
+
+ zoom_in(x_range, x_stop) {
+ this.history.push(this.x_range);
+ this.x_range = x_range;
+ this.x_stop = x_stop;
+ this.reset();
+ }
+
+ zoom_out() {
+ const plot_zoom_steps = [
+ 5 * 60,
+ 60 * 60,
+ 6 * 60 * 60,
+ 24 * 60 * 60,
+ 7 * 24 * 60 * 60,
+ 30 * 24 * 60 * 60,
+ 365 * 24 * 60 * 60
+ ];
+
+ let r = this.history.pop();
+ if (r === undefined) {
+ let i;
+ for (i = 0; i < plot_zoom_steps.length - 1; i++) {
+ if (plot_zoom_steps[i] > this.x_range)
+ break;
+ }
+ r = plot_zoom_steps[i];
+ }
+ if (this.x_stop !== undefined)
+ this.x_stop += (r - this.x_range) / 2;
+ this.x_range = r;
+ this.reset();
+ }
+
+ goto_now() {
+ this.x_stop = undefined;
+ this.reset();
+ }
+
+ scroll_left() {
+ const step = this.x_range / 10;
+ if (this.x_stop === undefined)
+ this.x_stop = (new Date()).getTime() / 1000;
+ this.x_stop -= step;
+ this.reset();
+ }
+
+ scroll_right() {
+ const step = this.x_range / 10;
+ if (this.x_stop !== undefined) {
+ this.x_stop += step;
+ this.reset();
+ }
+ }
+}
+
+class SinglePlotState {
+ constructor() {
+ this._plot = new Plot(null, 300);
+ this._plot.start_walking();
+ }
+
+ plot_single(metric) {
+ if (this._stacked_instances_series) {
+ this._stacked_instances_series.clear_instances();
+ this._stacked_instances_series.remove();
+ this._stacked_instances_series = null;
+ }
+ if (!this._sum_series) {
+ this._sum_series = this._plot.add_metrics_sum_series(metric, { });
+ }
+ }
+
+ plot_instances(metric, insts, reset) {
+ if (this._sum_series) {
+ this._sum_series.remove();
+ this._sum_series = null;
+ }
+ if (!this._stacked_instances_series) {
+ this._stacked_instances_series = this._plot.add_metrics_stacked_instances_series(metric, { });
+ } else if (reset) {
+ // We can't remove individual instances, only clear the
+ // whole thing (because of limitations of Metrics_stacked_instances_series above).
+ // So we do that, but only when there is at least one instance
+ // that needs to be removed. That avoids a lot of events and React warnings.
+ if (Object.keys(this._stacked_instances_series.instances).some(old => insts.indexOf(old) == -1))
+ this._stacked_instances_series.clear_instances();
+ }
+
+ for (let i = 0; i < insts.length; i++) {
+ this._stacked_instances_series.add_instance(insts[i]);
+ }
+ }
+
+ destroy() {
+ this._plot.destroy();
+ }
+}
+
+export class PlotState {
+ constructor() {
+ cockpit.event_target(this);
+ this.plots = { };
+ this.zoom_state = null;
+ }
+
+ _reset_plots(x_range, x_stop) {
+ for (const id in this.plots) {
+ const p = this.plots[id]._plot;
+ p.stop_walking();
+ p.reset(x_range, x_stop);
+ p.refresh();
+ if (x_stop === undefined)
+ p.start_walking();
+ }
+ }
+
+ _check_archives(plot) {
+ if (!this.zoom_state && plot.archives) {
+ this.zoom_state = new ZoomState(this._reset_plots.bind(this));
+ this.dispatchEvent("changed");
+ }
+ }
+
+ _get(id) {
+ if (this.plots[id])
+ return this.plots[id];
+
+ const ps = new SinglePlotState();
+ ps._plot.addEventListener("changed", () => this._check_archives(ps._plot));
+ this._check_archives(ps._plot);
+ ps._plot.addEventListener("plot", (event, data) => {
+ ps.data = data;
+ this.dispatchEvent("plot:" + id);
+ });
+
+ this.plots[id] = ps;
+ return ps;
+ }
+
+ plot_single(id, metric) {
+ const ps = this._get(id);
+ ps.plot_single(metric);
+ }
+
+ plot_instances(id, metric, insts, reset) {
+ this._get(id).plot_instances(metric, insts, reset);
+ }
+
+ data(id) {
+ return this.plots[id] && this.plots[id].data;
+ }
+}
diff --git a/pkg/lib/polyfills.js b/pkg/lib/polyfills.js
new file mode 100644
index 0000000..4c00bea
--- /dev/null
+++ b/pkg/lib/polyfills.js
@@ -0,0 +1,25 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ This file contains various polyfills and other compatibility hacks
+ */
+
+// Don't complain about extending native data types -- that's what polyfills do
+/* eslint-disable no-extend-native */
diff --git a/pkg/lib/python.js b/pkg/lib/python.js
new file mode 100644
index 0000000..c229065
--- /dev/null
+++ b/pkg/lib/python.js
@@ -0,0 +1,30 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+const pyinvoke = ["sh", "-ec", "exec $(command -v /usr/libexec/platform-python || command -v python3) -c \"$@\"", "--"];
+
+export function spawn (script_pieces, args, options) {
+ const script = (typeof script_pieces == "string")
+ ? script_pieces
+ : script_pieces.join("\n");
+
+ return cockpit.spawn(pyinvoke.concat([script]).concat(args), options);
+}
diff --git a/pkg/lib/qunit-template.html.in b/pkg/lib/qunit-template.html.in
new file mode 100644
index 0000000..0482817
--- /dev/null
+++ b/pkg/lib/qunit-template.html.in
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+ This file is part of Cockpit.
+
+ Copyright (C) 2014 Red Hat, Inc.
+
+ Cockpit is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ Cockpit is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title><%= title %></title>
+ <link rel="stylesheet" href="<%= title %>.css" type="text/css" />
+ <script src="<%= builddir %>dist/base1/cockpit.js"></script>
+ <script src="<%= script %>"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <h1 id="qunit-header"><%= title %></h1>
+ <h2 id="qunit-banner"></h2><div id="qunit-testrunner-toolbar"></div>
+ <h2 id="qunit-userAgent"></h2><ol id="qunit-tests"></ol>
+ <div id="qunit-fixture">test markup, will be hidden</div>
+ <div id="done-flag" style="display: none;">Done</div>
+</body>
+</html>
diff --git a/pkg/lib/qunit-tests.js b/pkg/lib/qunit-tests.js
new file mode 100644
index 0000000..6f6e69d
--- /dev/null
+++ b/pkg/lib/qunit-tests.js
@@ -0,0 +1,90 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+import QUnit from "qunit/qunit/qunit.js";
+import qunitTap from "qunit-tap/lib/qunit-tap.js";
+import "qunit/qunit/qunit.css";
+
+QUnit.mock_info = async key => {
+ const response = await fetch(`http://${window.location.hostname}:${window.location.port}/mock/info`);
+ return (await response.json())[key];
+};
+
+// Convenience for skipping tests that the python bridge can't yet
+// handle.
+
+let is_pybridge = null;
+
+QUnit.test.skipWithPybridge = async (name, callback) => {
+ if (is_pybridge === null)
+ is_pybridge = await QUnit.mock_info("pybridge");
+
+ if (is_pybridge)
+ QUnit.test.skip(name, callback);
+ else
+ QUnit.test(name, callback);
+};
+
+/* Always use explicit start */
+QUnit.config.autostart = false;
+
+let qunit_started = false;
+
+QUnit.moduleStart(() => {
+ qunit_started = true;
+});
+
+window.setTimeout(() => {
+ if (!qunit_started) {
+ console.log("QUnit not started by test");
+ console.log("cockpittest-tap-error");
+ }
+}, 20000);
+
+/* QUnit-Tap writes the summary line right after this function returns.
+* Delay printing the end marker until after that summary is out.
+*/
+QUnit.done(() => window.setTimeout(() => console.log("cockpittest-tap-done"), 0));
+
+/* Now initialize qunit-tap
+ *
+ * When not running under a tap driver this stuff will just show up in
+ * the console. We print out a special canary at the end of the tests
+ * so that the tap driver can know when the testing is done.
+ *
+ * In addition double check for a test file that doesn't properly call
+ * QUnit.start() after its done setting up its tests.
+ *
+ * We also want to insert the current test name into all tap lines.
+ */
+const tap_regex = /^((not )?ok [0-9]+ (- )?)(.*)$/;
+qunitTap(QUnit, function() {
+ if (arguments.length == 1 && QUnit.config.current) {
+ const match = tap_regex.exec(arguments[0]);
+ if (match) {
+ console.log(match[1] + QUnit.config.current.testName + ": " + match[4]);
+ return;
+ }
+ }
+ console.log.apply(console, arguments);
+});
+
+export default QUnit;
diff --git a/pkg/lib/serverTime.js b/pkg/lib/serverTime.js
new file mode 100644
index 0000000..ae3023b
--- /dev/null
+++ b/pkg/lib/serverTime.js
@@ -0,0 +1,768 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import cockpit from "cockpit";
+import React, { useState } from "react";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { CloseIcon, ExclamationCircleIcon, InfoCircleIcon, PlusIcon } from "@patternfly/react-icons";
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+import { useObject, useEvent } from "hooks.js";
+
+import * as service from "service.js";
+import * as timeformat from "timeformat.js";
+import * as python from "python.js";
+import get_timesync_backend_py from "./get-timesync-backend.py";
+
+import { superuser } from "superuser.js";
+
+import "serverTime.scss";
+
+const _ = cockpit.gettext;
+
+export function ServerTime() {
+ const self = this;
+ cockpit.event_target(self);
+
+ function emit_changed() {
+ self.dispatchEvent("changed");
+ }
+
+ let time_offset = null;
+ let remote_offset = null;
+
+ let client = null;
+ let timedate = null;
+
+ function connect() {
+ if (client) {
+ timedate.removeEventListener("changed", emit_changed);
+ client.close();
+ }
+ client = cockpit.dbus('org.freedesktop.timedate1', { superuser: "try" });
+ timedate = client.proxy();
+ timedate.addEventListener("changed", emit_changed);
+ client.subscribe({
+ interface: "org.freedesktop.DBus.Properties",
+ member: "PropertiesChanged"
+ }, ntp_updated);
+ }
+
+ const timedate1_service = service.proxy("dbus-org.freedesktop.timedate1.service");
+ const timesyncd_service = service.proxy("systemd-timesyncd.service");
+ const chronyd_service = service.proxy("chronyd.service");
+
+ timesyncd_service.addEventListener("changed", emit_changed);
+ chronyd_service.addEventListener("changed", emit_changed);
+
+ /*
+ * The time we return from here as its UTC time set to the
+ * server time. This is the only way to get predictable
+ * behavior.
+ */
+ Object.defineProperty(self, 'utc_fake_now', {
+ enumerable: true,
+ get: function get() {
+ const offset = time_offset + remote_offset;
+ return new Date(offset + (new Date()).valueOf());
+ }
+ });
+
+ Object.defineProperty(self, 'now', {
+ enumerable: true,
+ get: function get() {
+ return new Date(time_offset + (new Date()).valueOf());
+ }
+ });
+
+ self.format = function format(and_time) {
+ const options = { dateStyle: "medium", timeStyle: and_time ? "short" : undefined, timeZone: "UTC" };
+ return timeformat.formatter(options).format(self.utc_fake_now);
+ };
+
+ const updateInterval = window.setInterval(emit_changed, 30000);
+
+ self.wait = function wait() {
+ if (remote_offset === null)
+ return self.update();
+ return Promise.resolve();
+ };
+
+ self.update = function update() {
+ return cockpit.spawn(["date", "+%s:%z"], { err: "message" })
+ .then(data => {
+ const parts = data.trim().split(":");
+ const timems = parseInt(parts[0], 10) * 1000;
+ let tzmin = parseInt(parts[1].slice(-2), 10);
+ const tzhour = parseInt(parts[1].slice(0, -2));
+ if (tzhour < 0)
+ tzmin = -tzmin;
+ const offsetms = (tzhour * 3600000) + tzmin * 60000;
+ const now = new Date();
+ time_offset = (timems - now.valueOf());
+ remote_offset = offsetms;
+ emit_changed();
+ })
+ .catch(ex => console.log("Couldn't calculate server time offset: " + cockpit.message(ex)));
+ };
+
+ /* There is no way to make sense of this date without a round trip to the
+ * server, as the timezone is really server specific. */
+ self.change_time = (datestr, timestr) => cockpit.spawn(["date", "--date=" + datestr + " " + timestr, "+%s"])
+ .then(data => {
+ const seconds = parseInt(data.trim(), 10);
+ return timedate.call('SetTime', [seconds * 1000 * 1000, false, true])
+ .then(self.update);
+ });
+
+ self.bump_time = function (millis) {
+ return timedate.call('SetTime', [millis, true, true]);
+ };
+
+ self.get_time_zone = function () {
+ return timedate.Timezone;
+ };
+
+ self.set_time_zone = function (tz) {
+ return timedate.call('SetTimezone', [tz, true]);
+ };
+
+ self.poll_ntp_synchronized = () => client.call(
+ timedate.path, "org.freedesktop.DBus.Properties", "Get", ["org.freedesktop.timedate1", "NTPSynchronized"])
+ .then(result => {
+ const ifaces = { "org.freedesktop.timedate1": { NTPSynchronized: result[0].v } };
+ const data = { };
+ data[timedate.path] = ifaces;
+ client.notify(data);
+ })
+ .catch(error => {
+ if (error.name != "org.freedesktop.DBus.Error.UnknownProperty" &&
+ error.problem != "not-found")
+ console.log("can't get NTPSynchronized property", error);
+ });
+
+ let ntp_waiting_value = null;
+ let ntp_waiting_resolve = null;
+
+ function ntp_updated(path, iface, member, args) {
+ if (!ntp_waiting_resolve || !args[1].NTP)
+ return;
+ if (ntp_waiting_value !== args[1].NTP.v)
+ console.warn("Unexpected value of NTP");
+ ntp_waiting_resolve();
+ ntp_waiting_resolve = null;
+ }
+
+ self.set_ntp = function set_ntp(val) {
+ const promise = new Promise((resolve, reject) => {
+ ntp_waiting_resolve = resolve;
+ });
+ ntp_waiting_value = val;
+ client.call(timedate.path,
+ "org.freedesktop.DBus.Properties", "Get", ["org.freedesktop.timedate1", "NTP"])
+ .then(result => {
+ // Check if don't want to enable enabled or disable disabled
+ if (result[0].v === val) {
+ ntp_waiting_resolve();
+ ntp_waiting_resolve = null;
+ return;
+ }
+ timedate.call('SetNTP', [val, true])
+ .catch(e => {
+ ntp_waiting_resolve();
+ ntp_waiting_resolve = null;
+ console.error("Failed to call SetNTP:", e.message); // not-covered: OS error
+ });
+ });
+ return promise;
+ };
+
+ self.get_ntp_active = function () {
+ return timedate.NTP;
+ };
+
+ self.get_ntp_supported = function () {
+ return timedate.CanNTP;
+ };
+
+ self.get_ntp_status = function () {
+ const status = {
+ initialized: false,
+ active: false,
+ synch: false,
+ service: null,
+ server: null,
+ sub_status: null
+ };
+
+ // flag for tests that timedated/timesyncd proxies got initialized
+ if (timedate.CanNTP !== undefined &&
+ timedate1_service.unit && timedate1_service.unit.Id &&
+ timesyncd_service.exists !== null &&
+ chronyd_service.exists !== null)
+ status.initialized = true;
+
+ status.active = timedate.NTP;
+ status.synch = timedate.NTPSynchronized;
+
+ const timesyncd_server_regex = /.*time server (.*)\./i;
+
+ const timesyncd_status = (timesyncd_service.state == "running" &&
+ timesyncd_service.service?.StatusText);
+
+ if (timesyncd_service.state == "running")
+ status.service = "systemd-timesyncd.service";
+ else if (chronyd_service.state == "running")
+ status.service = "chronyd.service";
+
+ if (timesyncd_status) {
+ const match = timesyncd_status.match(timesyncd_server_regex);
+ if (match)
+ status.server = match[1];
+ else if (timesyncd_status != "Idle." && timesyncd_status !== "")
+ status.sub_status = timesyncd_status;
+ }
+
+ return status;
+ };
+
+ function get_timesync_backend() {
+ return python.spawn(get_timesync_backend_py, [], { superuser: "try", err: "message" })
+ .then(data => {
+ const unit = data.trim();
+ if (unit == "systemd-timesyncd.service")
+ return "timesyncd";
+ else if (unit == "chrony.service" || unit == "chronyd.service")
+ return "chronyd";
+ else
+ return null;
+ });
+ }
+
+ function get_custom_ntp_timesyncd() {
+ const custom_ntp_config_file = cockpit.file("/etc/systemd/timesyncd.conf.d/50-cockpit.conf",
+ { superuser: "try" });
+
+ const result = {
+ backend: "timesyncd",
+ enabled: false,
+ servers: []
+ };
+
+ return custom_ntp_config_file.read()
+ .then(function(text) {
+ let ntp_line = "";
+ if (text) {
+ result.enabled = true;
+ text.split("\n").forEach(function(line) {
+ if (line.indexOf("NTP=") === 0) {
+ ntp_line = line.slice(4);
+ result.enabled = true;
+ } else if (line.indexOf("#NTP=") === 0) {
+ ntp_line = line.slice(5);
+ result.enabled = false;
+ }
+ });
+
+ result.servers = ntp_line.split(" ").filter(function(val) {
+ return val !== "";
+ });
+ if (result.servers.length === 0)
+ result.enabled = false;
+ }
+ return result;
+ })
+ .catch(function(error) {
+ console.warn("failed to load time servers", error);
+ return result;
+ });
+ }
+
+ function set_custom_ntp_timesyncd(config) {
+ const custom_ntp_config_file = cockpit.file("/etc/systemd/timesyncd.conf.d/50-cockpit.conf",
+ { superuser: true });
+
+ const text = `# This file is automatically generated by Cockpit\n\n[Time]\n${config.enabled ? "" : "#"}NTP=${config.servers.join(" ")}\n`;
+
+ return cockpit.spawn(["mkdir", "-p", "/etc/systemd/timesyncd.conf.d"], { superuser: true })
+ .then(() => custom_ntp_config_file.replace(text));
+ }
+
+ const chronyd_sourcedir = "/etc/chrony/sources.d";
+ const chronyd_sources_enabled = chronyd_sourcedir + "/cockpit.sources";
+ const chronyd_sources_disabled = chronyd_sourcedir + "/cockpit.disabled";
+
+ function get_custom_ntp_chronyd() {
+ const enabled_file = cockpit.file(chronyd_sources_enabled, { superuser: "try" });
+ const disabled_file = cockpit.file(chronyd_sources_disabled, { superuser: "try" });
+
+ function parse_servers(data) {
+ if (!data)
+ return [];
+ const servers = [];
+ data.split("\n").forEach(function(line) {
+ const parts = line.split(" ");
+ if (parts[0] == "server")
+ servers.push(parts[1]);
+ });
+ return servers;
+ }
+
+ return enabled_file.read()
+ .then(data => {
+ if (data) {
+ return {
+ backend: "chronyd",
+ enabled: true,
+ servers: parse_servers(data)
+ };
+ } else {
+ return disabled_file.read()
+ .then(data => {
+ return {
+ backend: "chronyd",
+ enabled: false,
+ servers: parse_servers(data)
+ };
+ });
+ }
+ });
+ }
+
+ function set_custom_ntp_chronyd(config) {
+ const enabled_file = cockpit.file(chronyd_sources_enabled, { superuser: true });
+ const disabled_file = cockpit.file(chronyd_sources_disabled, { superuser: true });
+
+ const text = "# This file is automatically generated by Cockpit\n\n" + config.servers.map(s => `server ${s}\n`).join("");
+
+ // HACK - https://bugzilla.redhat.com/show_bug.cgi?id=2168863
+ function ensure_sourcedir() {
+ function add_sourcedir(data) {
+ const line = "sourcedir " + chronyd_sourcedir;
+ if (data && data.indexOf(line) == -1)
+ data += "\n# Added by Cockpit\n" + line + "\n";
+ return data;
+ }
+ return cockpit.file("/etc/chrony.conf", { superuser: true }).modify(add_sourcedir);
+ }
+
+ return cockpit.spawn(["mkdir", "-p", chronyd_sourcedir], { superuser: true })
+ .then(() => {
+ if (config.enabled)
+ return enabled_file.replace(text).then(() => disabled_file.replace(null)).then(ensure_sourcedir);
+ else
+ return disabled_file.replace(text).then(() => enabled_file.replace(null));
+ });
+ }
+
+ self.get_custom_ntp = function () {
+ return get_timesync_backend().then(backend => {
+ if (backend == "timesyncd") {
+ return get_custom_ntp_timesyncd();
+ } else if (backend == "chronyd") {
+ return get_custom_ntp_chronyd();
+ } else {
+ return Promise.resolve({ backend: null, servers: [], enabled: false });
+ }
+ });
+ };
+
+ self.set_custom_ntp = function (config) {
+ if (config.backend == "timesyncd") {
+ return set_custom_ntp_timesyncd(config);
+ } else if (config.backend == "chronyd") {
+ return set_custom_ntp_chronyd(config);
+ } else {
+ return Promise.resolve();
+ }
+ };
+
+ self.get_timezones = function() {
+ return cockpit.spawn(["/usr/bin/timedatectl", "list-timezones"])
+ .then(content => content.split('\n').filter(tz => tz != ""));
+ };
+
+ /* NTPSynchronized needs to be polled so we just do that
+ * always.
+ */
+
+ const ntp_poll_interval = window.setInterval(function() {
+ self.poll_ntp_synchronized();
+ }, 5000);
+
+ self.close = function close() {
+ window.clearInterval(updateInterval);
+ window.clearInterval(ntp_poll_interval);
+ client.close();
+ };
+
+ connect();
+ superuser.addEventListener("reconnect", connect);
+ self.update();
+}
+
+export function ServerTimeConfig() {
+ const server_time = useObject(() => new ServerTime(),
+ st => st.close(),
+ []);
+ useEvent(server_time, "changed");
+
+ const ntp = server_time.get_ntp_status();
+
+ const tz = server_time.get_time_zone();
+ const systime_button = (
+ <Button variant="link" id="system_information_systime_button"
+ onClick={ () => change_systime_dialog(server_time, tz) }
+ data-timedated-initialized={ntp?.initialized}
+ isInline isDisabled={!superuser.allowed || !tz}>
+ { server_time.format(true) }
+ </Button>);
+
+ let ntp_status = null;
+ if (ntp?.active) {
+ let icon; let header; let body = ""; let footer = null;
+ if (ntp.synch) {
+ icon = <InfoCircleIcon className="ct-info-circle" />;
+ header = _("Synchronized");
+ if (ntp.server)
+ body = <div>{cockpit.format(_("Synchronized with $0"), ntp.server)}</div>;
+ } else {
+ if (ntp.server) {
+ icon = <Spinner size="md" />;
+ header = _("Synchronizing");
+ body = <div>{cockpit.format(_("Trying to synchronize with $0"), ntp.server)}</div>;
+ } else {
+ icon = <ExclamationCircleIcon className="ct-exclamation-circle" />;
+ header = _("Not synchronized");
+ if (ntp.service) {
+ footer = (
+ <Button variant="link"
+ onClick={() => cockpit.jump("/system/services#/" +
+ encodeURIComponent(ntp.service))}>
+ {_("Log messages")}
+ </Button>);
+ }
+ }
+ }
+
+ if (ntp.sub_status) {
+ body = <>{body}<div>{ntp.sub_status}</div></>;
+ }
+
+ ntp_status = (
+ <Popover headerContent={header} bodyContent={body} footerContent={footer}>
+ {icon}
+ </Popover>);
+ }
+
+ return (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ {systime_button}
+ {ntp_status}
+ </Flex>
+ );
+}
+
+function Validated({ errors, error_key, children }) {
+ const error = errors?.[error_key];
+ // We need to always render the <div> for the has-error
+ // class so that the input field keeps the focus when
+ // errors are cleared. Otherwise the DOM changes enough
+ // for the Browser to remove focus.
+ return (
+ <div className={error ? "ct-validation-wrapper has-error" : "ct-validation-wrapper"}>
+ { children }
+ { error ? <span className="help-block dialog-error">{error}</span> : null }
+ </div>
+ );
+}
+
+function ValidatedInput({ errors, error_key, children }) {
+ const error = errors?.[error_key];
+ return (
+ <span className={error ? "ct-validation-wrapper has-error" : "ct-validation-wrapper"}>
+ { children }
+ </span>
+ );
+}
+
+function ChangeSystimeBody({ state, errors, change }) {
+ const [zonesOpen, setZonesOpen] = useState(false);
+ const [modeOpen, setModeOpen] = useState(false);
+
+ const {
+ time_zone, time_zones,
+ mode,
+ manual_date, manual_time,
+ ntp_supported, custom_ntp
+ } = state;
+
+ function add_server(event, index) {
+ custom_ntp.servers.splice(index + 1, 0, "");
+ change("custom_ntp", custom_ntp);
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ }
+
+ function remove_server(event, index) {
+ custom_ntp.servers.splice(index, 1);
+ change("custom_ntp", custom_ntp);
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ }
+
+ function change_server(event, index, value) {
+ custom_ntp.servers[index] = value;
+ change("custom_ntp", custom_ntp);
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ }
+
+ const ntp_servers = (
+ custom_ntp.servers.map((s, i) => (
+ <Flex className="ntp-server-input-group" spaceItems={{ default: 'spaceItemsSm' }} key={i}>
+ <FlexItem grow={{ default: 'grow' }}>
+ <TextInput value={s} placeholder={_("NTP server")} aria-label={_("NTP server")}
+ onChange={(event, value) => change_server(event, i, value)} />
+ </FlexItem>
+ <Button variant="secondary" onClick={event => add_server(event, i)}
+ icon={ <PlusIcon /> } />
+ <Button variant="secondary" onClick={event => remove_server(event, i)}
+ icon={ <CloseIcon /> } isDisabled={i === custom_ntp.servers.length - 1} />
+ </Flex>
+ ))
+ );
+
+ const mode_options = [
+ <SelectOption key="manual_time" value="manual_time">{_("Manually")}</SelectOption>,
+ <SelectOption key="ntp_time" value="ntp_time" isDisabled={!ntp_supported}>{_("Automatically using NTP")}</SelectOption>
+ ];
+
+ if (custom_ntp.backend)
+ mode_options.push(
+ <SelectOption key="ntp_time_custom" value="ntp_time_custom" isDisabled={!ntp_supported}>
+ { custom_ntp.backend == "chronyd"
+ ? _("Automatically using additional NTP servers")
+ : _("Automatically using specific NTP servers")
+ }
+ </SelectOption>);
+
+ return (
+ <Form isHorizontal>
+ <FormGroup fieldId="systime-timezones" label={_("Time zone")}>
+ <Validated errors={errors} error_key="time_zone">
+ <Select id="systime-timezones" variant="typeahead"
+ isOpen={zonesOpen} onToggle={(_, isOpen) => setZonesOpen(isOpen)}
+ selections={time_zone}
+ onSelect={(event, value) => { setZonesOpen(false); change("time_zone", value) }}
+ menuAppendTo="parent">
+ { time_zones.map(tz => <SelectOption key={tz} value={tz}>{tz.replaceAll("_", " ")}</SelectOption>) }
+ </Select>
+ </Validated>
+ </FormGroup>
+ <FormGroup fieldId="change_systime" label={_("Set time")} isStack>
+ <Select id="change_systime"
+ isOpen={modeOpen} onToggle={(_, isOpen) => setModeOpen(isOpen)}
+ selections={mode} onSelect={(event, value) => { setModeOpen(false); change("mode", value) }}
+ menuAppendTo="parent">
+ { mode_options }
+ </Select>
+ { mode == "manual_time" &&
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} id="systime-manual-row">
+ <ValidatedInput errors={errors} error_key="manual_date">
+ <DatePicker id="systime-date-input"
+ aria-label={_("Pick date")}
+ buttonAriaLabel={_("Toggle date picker")}
+ dateFormat={timeformat.dateShort}
+ dateParse={timeformat.parseShortDate}
+ invalidFormatText=""
+ locale={timeformat.dateFormatLang()}
+ weekStart={timeformat.firstDayOfWeek()}
+ placeholder={timeformat.dateShortFormat()}
+ onChange={(_, d) => change("manual_date", d)}
+ value={manual_date}
+ appendTo={() => document.body} />
+ </ValidatedInput>
+ <ValidatedInput errors={errors} error_key="manual_time">
+ <TimePicker id="systime-time-input"
+ className="ct-serverTime-time-picker"
+ time={manual_time}
+ is24Hour
+ menuAppendTo={() => document.body}
+ invalidFormatErrorMessage=""
+ onChange={(e, time, h, m, s, valid) => change("manual_time", time, valid) } />
+ </ValidatedInput>
+ <Validated errors={errors} error_key="manual_date" />
+ <Validated errors={errors} error_key="manual_time" />
+ </Flex>
+ }
+ { mode == "ntp_time_custom" &&
+ <Validated errors={errors} error_key="ntp_servers">
+ <div id="systime-ntp-servers">
+ { ntp_servers }
+ </div>
+ </Validated>
+ }
+ </FormGroup>
+ </Form>
+ );
+}
+
+function has_errors(errors) {
+ for (const field in errors) {
+ if (errors[field])
+ return true;
+ }
+ return false;
+}
+
+function change_systime_dialog(server_time, timezone) {
+ let dlg = null;
+ const state = {
+ time_zone: timezone,
+ time_zones: null,
+ mode: null,
+ ntp_supported: server_time.get_ntp_supported(),
+ custom_ntp: null,
+ manual_time_valid: true,
+ };
+ let errors = { };
+
+ function get_current_time() {
+ state.manual_date = server_time.format();
+
+ const minutes = server_time.utc_fake_now.getUTCMinutes();
+ // normalize to two digits
+ const minutes_str = (minutes < 10) ? "0" + minutes.toString() : minutes.toString();
+ state.manual_time = `${server_time.utc_fake_now.getUTCHours()}:${minutes_str}`;
+ }
+
+ function change(field, value, isValid) {
+ state[field] = value;
+ errors = { };
+
+ if (field == "mode" && value == "manual_time")
+ get_current_time();
+
+ if (field == "manual_time")
+ state.manual_time_valid = value && isValid;
+
+ update();
+ }
+
+ function validate() {
+ errors = { };
+
+ if (state.time_zone == "")
+ errors.time_zone = _("Invalid timezone");
+
+ if (state.mode == "manual_time") {
+ const new_date = new Date(state.manual_date);
+ if (isNaN(new_date.getTime()) || new_date.getTime() < 0)
+ errors.manual_date = _("Invalid date format");
+
+ if (!state.manual_time_valid)
+ errors.manual_time = _("Invalid time format");
+ }
+
+ if (state.mode == "ntp_time_custom") {
+ if (state.custom_ntp.servers.filter(s => !!s).length == 0)
+ errors.ntp_servers = _("Need at least one NTP server");
+ }
+
+ return !has_errors(errors);
+ }
+
+ function apply() {
+ return server_time.set_time_zone(state.time_zone)
+ .then(() => {
+ if (state.mode == "manual_time") {
+ return server_time.set_ntp(false)
+ .then(() => server_time.change_time(state.manual_date,
+ state.manual_time));
+ } else {
+ // Switch off NTP, write the config file, and switch NTP back on
+ state.custom_ntp.enabled = (state.mode == "ntp_time_custom");
+ state.custom_ntp.servers = state.custom_ntp.servers.filter(s => !!s);
+ return server_time.set_ntp(false)
+ .then(() => server_time.set_custom_ntp(state.custom_ntp))
+ .then(() => server_time.set_ntp(true));
+ }
+ });
+ }
+
+ function update() {
+ const props = {
+ id: "system_information_change_systime",
+ title: _("Change system time"),
+ body: <ChangeSystimeBody state={state} errors={errors} change={change} />
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Change"),
+ style: "primary",
+ clicked: () => {
+ if (validate()) {
+ return apply();
+ } else {
+ update();
+ return Promise.reject();
+ }
+ }
+ }
+ ]
+ };
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ Promise.all([server_time.get_custom_ntp(), server_time.get_timezones()])
+ .then(([custom_ntp, time_zones]) => {
+ if (custom_ntp.servers.length == 0)
+ custom_ntp.servers = [""];
+ state.custom_ntp = custom_ntp;
+ state.time_zones = time_zones;
+ if (server_time.get_ntp_active()) {
+ if (custom_ntp.enabled)
+ state.mode = "ntp_time_custom";
+ else
+ state.mode = "ntp_time";
+ } else {
+ state.mode = "manual_time";
+ get_current_time();
+ }
+ update();
+ });
+}
diff --git a/pkg/lib/serverTime.scss b/pkg/lib/serverTime.scss
new file mode 100644
index 0000000..454ca7c
--- /dev/null
+++ b/pkg/lib/serverTime.scss
@@ -0,0 +1,7 @@
+.ct-serverTime-time-picker {
+ max-inline-size: 7rem;
+}
+
+.ntp-server-input-group:not(:last-child) {
+ margin-block-end: var(--pf-v5-global--spacer--sm);
+}
diff --git a/pkg/lib/service.js b/pkg/lib/service.js
new file mode 100644
index 0000000..3ef1878
--- /dev/null
+++ b/pkg/lib/service.js
@@ -0,0 +1,344 @@
+import cockpit from "cockpit";
+
+/* SERVICE MANAGEMENT API
+ *
+ * The "service" module lets you monitor and manage a
+ * system service on localhost in a simple way.
+ *
+ * It mainly exists because talking to the systemd D-Bus API is
+ * not trivial enough to do it directly.
+ *
+ * - proxy = service.proxy(name)
+ *
+ * Create a proxy that represents the service named NAME.
+ *
+ * The proxy has properties and methods (described below) that
+ * allow you to monitor the state of the service, and perform
+ * simple actions on it.
+ *
+ * Initially, any of the properties can be "null" until their
+ * actual values have been retrieved in the background.
+ *
+ * - proxy.addEventListener('changed', event => { ... })
+ *
+ * The 'changed' event is emitted whenever one of the properties
+ * of the proxy changes.
+ *
+ * - proxy.exists
+ *
+ * A boolean that tells whether the service is known or not. A
+ * proxy with 'exists == false' will have 'state == undefined' and
+ * 'enabled == undefined'.
+ *
+ * - proxy.state
+ *
+ * Either 'undefined' when the state can't be retrieved, or a
+ * string that has one of the values "starting", "running",
+ * "stopping", "stopped", or "failed".
+ *
+ * - proxy.enabled
+ *
+ * Either 'undefined' when the value can't be retrieved, or a
+ * boolean that tells whether the service is started 'enabled'.
+ * What it means exactly for a service to be enabled depends on
+ * the service, but an enabled service is usually started on boot,
+ * no matter whether other services need it or not. A disabled
+ * service is usually only started when it is needed by some other
+ * service.
+ *
+ * - proxy.unit
+ * - proxy.details
+ *
+ * The raw org.freedesktop.systemd1.Unit and type-specific D-Bus
+ * interface proxies for the service.
+ *
+ * - proxy.service
+ *
+ * The deprecated name for proxy.details
+ *
+ * - promise = proxy.start()
+ *
+ * Start the service. The return value is a standard jQuery
+ * promise as returned from DBusClient.call.
+ *
+ * - promise = proxy.restart()
+ *
+ * Restart the service.
+ *
+ * - promise = proxy.tryRestart()
+ *
+ * Try to restart the service if it's running or starting
+ *
+ * - promise = proxy.stop()
+ *
+ * Stop the service.
+ *
+ * - promise = proxy.enable()
+ *
+ * Enable the service.
+ *
+ * - promise = proxy.disable()
+ *
+ * Disable the service.
+ *
+ * - journal = proxy.getRunJournal(options)
+ *
+ * Return the journal of the current (if running) or recent (if failed/stopped) service run,
+ * similar to `systemctl status`. `options` is an optional array that gets appended to the `journalctl` call.
+ */
+
+let systemd_client;
+let systemd_manager;
+
+function wait_valid(proxy, callback) {
+ proxy.wait(() => {
+ if (proxy.valid)
+ callback();
+ });
+}
+
+function with_systemd_manager(done) {
+ if (!systemd_manager) {
+ // cached forever, only used for reading/watching; no superuser
+ systemd_client = cockpit.dbus("org.freedesktop.systemd1");
+ systemd_manager = systemd_client.proxy("org.freedesktop.systemd1.Manager",
+ "/org/freedesktop/systemd1");
+ wait_valid(systemd_manager, () => {
+ systemd_manager.Subscribe()
+ .catch(error => {
+ if (error.name != "org.freedesktop.systemd1.AlreadySubscribed" &&
+ error.name != "org.freedesktop.DBus.Error.FileExists")
+ console.warn("Subscribing to systemd signals failed", error);
+ });
+ });
+ }
+ wait_valid(systemd_manager, done);
+}
+
+export function proxy(name, kind) {
+ const self = {
+ exists: null,
+ state: null,
+ enabled: null,
+
+ wait,
+
+ start,
+ stop,
+ restart,
+ tryRestart,
+
+ enable,
+ disable,
+
+ getRunJournal,
+ };
+
+ cockpit.event_target(self);
+
+ let unit, details;
+ let wait_promise_resolve;
+ const wait_promise = new Promise(resolve => { wait_promise_resolve = resolve });
+
+ if (name.indexOf(".") == -1)
+ name = name + ".service";
+ if (kind === undefined)
+ kind = "Service";
+
+ function update_from_unit() {
+ self.exists = (unit.LoadState != "not-found" || unit.ActiveState != "inactive");
+
+ if (unit.ActiveState == "activating")
+ self.state = "starting";
+ else if (unit.ActiveState == "deactivating")
+ self.state = "stopping";
+ else if (unit.ActiveState == "active" || unit.ActiveState == "reloading")
+ self.state = "running";
+ else if (unit.ActiveState == "failed")
+ self.state = "failed";
+ else if (unit.ActiveState == "inactive" && self.exists)
+ self.state = "stopped";
+ else
+ self.state = undefined;
+
+ if (unit.UnitFileState == "enabled" || unit.UnitFileState == "linked")
+ self.enabled = true;
+ else if (unit.UnitFileState == "disabled" || unit.UnitFileState == "masked")
+ self.enabled = false;
+ else
+ self.enabled = undefined;
+
+ self.unit = unit;
+
+ self.dispatchEvent("changed");
+ wait_promise_resolve();
+ }
+
+ function update_from_details() {
+ self.details = details;
+ self.service = details;
+ self.dispatchEvent("changed");
+ }
+
+ with_systemd_manager(function () {
+ systemd_manager.LoadUnit(name)
+ .then(path => {
+ unit = systemd_client.proxy('org.freedesktop.systemd1.Unit', path);
+ unit.addEventListener('changed', update_from_unit);
+ wait_valid(unit, update_from_unit);
+
+ details = systemd_client.proxy('org.freedesktop.systemd1.' + kind, path);
+ details.addEventListener('changed', update_from_details);
+ wait_valid(details, update_from_details);
+ })
+ .catch(() => {
+ self.exists = false;
+ self.dispatchEvent('changed');
+ });
+ });
+
+ function refresh() {
+ if (!unit || !details)
+ return Promise.resolve();
+
+ function refresh_interface(path, iface) {
+ return systemd_client.call(path, "org.freedesktop.DBus.Properties", "GetAll", [iface])
+ .then(([result]) => {
+ const props = { };
+ for (const p in result)
+ props[p] = result[p].v;
+ systemd_client.notify({ [unit.path]: { [iface]: props } });
+ })
+ .catch(error => console.log(error));
+ }
+
+ return Promise.allSettled([
+ refresh_interface(unit.path, "org.freedesktop.systemd1.Unit"),
+ refresh_interface(details.path, "org.freedesktop.systemd1." + kind),
+ ]);
+ }
+
+ function on_job_new_removed_refresh(event, number, path, unit_id, result) {
+ if (unit_id == name)
+ refresh();
+ }
+
+ /* HACK - https://bugs.freedesktop.org/show_bug.cgi?id=69575
+ *
+ * We need to explicitly get new property values when getting
+ * a UnitNew signal since UnitNew doesn't carry them.
+ * However, reacting to UnitNew with GetAll could lead to an
+ * infinite loop since systemd emits a UnitNew in reaction to
+ * GetAll for units that it doesn't want to keep loaded, such
+ * as units without unit files.
+ *
+ * So we ignore UnitNew and instead assume that the unit state
+ * only changes in interesting ways when there is a job for it
+ * or when the daemon is reloaded (or when we get a property
+ * change notification, of course).
+ */
+
+ // This is what we want to do:
+ // systemd_manager.addEventListener("UnitNew", function (event, unit_id, path) {
+ // if (unit_id == name)
+ // refresh();
+ // });
+
+ // This is what we have to do:
+ systemd_manager.addEventListener("Reloading", (event, reloading) => {
+ if (!reloading)
+ refresh();
+ });
+
+ systemd_manager.addEventListener("JobNew", on_job_new_removed_refresh);
+ systemd_manager.addEventListener("JobRemoved", on_job_new_removed_refresh);
+
+ function wait(callback) {
+ wait_promise.then(callback);
+ }
+
+ /* Actions
+ *
+ * We don't call methods on the persistent systemd_client, as that does not have superuser
+ */
+
+ function call_manager(dbus, method, args) {
+ return dbus.call("/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ method, args);
+ }
+
+ function call_manager_with_job(method, args) {
+ return new Promise((resolve, reject) => {
+ const dbus = cockpit.dbus("org.freedesktop.systemd1", { superuser: "try" });
+ let pending_job_path;
+
+ const subscription = dbus.subscribe(
+ { interface: "org.freedesktop.systemd1.Manager", member: "JobRemoved" },
+ (_path, _iface, _signal, [_number, path, _unit_id, result]) => {
+ if (path == pending_job_path) {
+ subscription.remove();
+ dbus.close();
+ refresh().then(() => {
+ if (result === "done")
+ resolve();
+ else
+ reject(new Error(`systemd job ${method} ${JSON.stringify(args)} failed with result ${result}`));
+ });
+ }
+ });
+
+ call_manager(dbus, method, args)
+ .then(([path]) => { pending_job_path = path })
+ .catch(() => {
+ dbus.close();
+ reject();
+ });
+ });
+ }
+
+ function call_manager_with_reload(method, args) {
+ const dbus = cockpit.dbus("org.freedesktop.systemd1", { superuser: "try" });
+ return call_manager(dbus, method, args)
+ .then(() => call_manager(dbus, "Reload", []))
+ .then(refresh)
+ .finally(dbus.close);
+ }
+
+ function start() {
+ return call_manager_with_job("StartUnit", [name, "replace"]);
+ }
+
+ function stop() {
+ return call_manager_with_job("StopUnit", [name, "replace"]);
+ }
+
+ function restart() {
+ return call_manager_with_job("RestartUnit", [name, "replace"]);
+ }
+
+ function tryRestart() {
+ return call_manager_with_job("TryRestartUnit", [name, "replace"]);
+ }
+
+ function enable() {
+ return call_manager_with_reload("EnableUnitFiles", [[name], false, false]);
+ }
+
+ function disable() {
+ return call_manager_with_reload("DisableUnitFiles", [[name], false]);
+ }
+
+ function getRunJournal(options) {
+ if (!details || !details.ExecMainStartTimestamp)
+ return Promise.reject(new Error("getRunJournal(): unit is not known"));
+
+ // collect the service journal since start time; property is μs, journal wants s
+ const startTime = Math.floor(details.ExecMainStartTimestamp / 1000000);
+ return cockpit.spawn(
+ ["journalctl", "--unit", name, "--since=@" + startTime.toString()].concat(options || []),
+ { superuser: "try", error: "message" });
+ }
+
+ return self;
+}
diff --git a/pkg/lib/superuser.js b/pkg/lib/superuser.js
new file mode 100644
index 0000000..5753b2c
--- /dev/null
+++ b/pkg/lib/superuser.js
@@ -0,0 +1,126 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+/* import { superuser } from "superuser";
+ *
+ * The "superuser" object indicates whether or not the current page
+ * can open superuser channels.
+ *
+ * - superuser.allowed
+ *
+ * This is true when the page can open superuser channels, and false
+ * otherwise. This field might be "null" while the page or the Cockpit
+ * session itself is still initializing.
+ *
+ * UI elements that trigger actions that need administrative access
+ * should be hidden when the "allowed" field is false or null. (If
+ * those elements also show information, such as with checkboxes or
+ * toggle buttons, disable them instead of hiding.)
+ *
+ * UI elements that alert the user that they don't have administrative
+ * access should be shown when the "allowed" field is exactly false,
+ * but not when it is null.
+ *
+ * - superuser.addEventListener("changed", () => ...)
+ *
+ * The event handler is called whenever superuser.allowed has changed.
+ * A page should update its appearance according to superuser.allowed.
+ *
+ * - superuser.addEventListener("reconnect", () => ...)
+ *
+ * The event handler is called whenever channels should be re-opened
+ * that use the "superuser" option.
+ *
+ * The difference between "reconnect" and "connect" is that the
+ * "reconnect" signal does not trigger when superuser.allowed goes
+ * from "null" to its first real value. You don't need to re-open
+ * channels in this case, and it happens on every page load, so this
+ * is important to avoid.
+ *
+ * - superuser.reload_page_on_change()
+ *
+ * Calling this function instructs the "superuser" object to reload
+ * the page whenever "superuser.allowed" changes. This is a (bad)
+ * alternative to re-initializing the page and intended to be used
+ * only to help with the transition.
+ *
+ * Even if you are using "superuser.reload_page_on_change" to avoid having
+ * to re-initialize your page dynamically, you should still use the
+ * "changed" event to update the page appearance since
+ * "superuser.allowed" might still change a couple of times right
+ * after page reload.
+ */
+
+function Superuser() {
+ const proxy = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Superuser", "/superuser");
+ let reload_on_change = false;
+
+ const compute_allowed = () => {
+ if (!proxy.valid || proxy.Current == "init")
+ return null;
+ return proxy.Current != "none";
+ };
+
+ const self = {
+ allowed: compute_allowed(),
+ reload_page_on_change
+ };
+
+ cockpit.event_target(self);
+
+ function changed(allowed) {
+ if (self.allowed != allowed) {
+ if (self.allowed != null && reload_on_change) {
+ window.location.reload(true);
+ } else {
+ const prev = self.allowed;
+ self.allowed = allowed;
+ self.dispatchEvent("changed");
+ if (prev != null)
+ self.dispatchEvent("reconnect");
+ }
+ }
+ }
+
+ proxy.wait(() => {
+ if (!proxy.valid) {
+ // Fall back to cockpit.permissions
+ const permission = cockpit.permission({ admin: true });
+ const update = () => {
+ changed(permission.allowed);
+ };
+ permission.addEventListener("changed", update);
+ update();
+ }
+ });
+
+ proxy.addEventListener("changed", () => {
+ changed(compute_allowed());
+ });
+
+ function reload_page_on_change() {
+ reload_on_change = true;
+ }
+
+ return self;
+}
+
+export const superuser = Superuser();
diff --git a/pkg/lib/table.css b/pkg/lib/table.css
new file mode 100644
index 0000000..63277cf
--- /dev/null
+++ b/pkg/lib/table.css
@@ -0,0 +1,138 @@
+.panel .table {
+ font-size: var(--pf-v5-global--FontSize-s);
+}
+
+/* Panels don't draw borders between them */
+.panel > .table > tbody:first-child td {
+ border-block-start: 1px solid rgb(221 221 221);
+}
+
+/* Table headers should not generate a double border */
+.panel .table thead tr th {
+ border-block-end: none;
+}
+
+/* Fix panel heading alignment & mobile layout */
+
+.panel-heading {
+ align-items: center;
+ background: #f5f5f5;
+ display: flex;
+ flex-wrap: wrap;
+ /* (28px small size widget height) + (0.5rem * 2) */
+ min-block-size: calc(28px + 1rem);
+ padding-block: 0.5rem;
+ padding-inline: 1rem;
+ position: relative;
+ z-index: 100;
+}
+
+.panel-title {
+ font: inherit;
+ margin: 0;
+ padding: 0;
+}
+
+.panel-title > a {
+ color: var(--ct-color-link);
+ display: inline-block;
+}
+
+.panel-title > a:hover,
+.panel-title > a:focus {
+ color: var(--alert-info-text);
+}
+
+/* Allow children in the title to wrap */
+.panel-title > h3,
+.panel-title > a,
+.panel-title > div,
+.panel-title > span {
+ flex-shrink: 1;
+ word-break: break-all;
+}
+
+.panel-heading > :last-child:not(:first-child),
+.panel-heading > .panel-heading-actions {
+ flex: auto;
+ text-align: end;
+}
+
+@media screen and (max-width: 640px) {
+ /* Remove _most_ of the gaps on the sides of small screens */
+ /* to maximize space, but still keep the boxy panel look */
+ .col-md-12 > .panel {
+ margin-inline: -10px;
+ }
+
+ .panel {
+ /* Background fade */
+ --hi-color: #d1d1d1;
+ --hi-color2: var(--ct-global--palette--black-250);
+ --bg-color: var(--ct-color-bg);
+ --hi-width: 20px;
+ --hi-width3: calc(var(--hi-width) * 3);
+ --transparent: rgb(255 255 255 / 0%); /* WebKit needs white transparent */
+ max-inline-size: 100vw;
+ overflow-x: auto;
+ position: relative;
+ background-image:
+ linear-gradient(to left, var(--bg-color) var(--hi-width), var(--transparent) var(--hi-width3)),
+ linear-gradient(to left, var(--hi-color) 1px, var(--transparent) 2px, var(--hi-color2) 4px, var(--bg-color) var(--hi-width)),
+ linear-gradient(to right, var(--bg-color) var(--hi-width), var(--transparent) var(--hi-width3)),
+ linear-gradient(to right, var(--hi-color) 1px, var(--transparent) 2px, var(--hi-color2) 4px, var(--bg-color) var(--hi-width));
+ background-attachment: local, scroll, local, scroll;
+ background-position: right, right, left, left;
+ background-repeat: no-repeat;
+ background-size: var(--hi-width3) 100%;
+ }
+
+ .panel > .panel-heading {
+ position: sticky;
+ inset-inline-start: 0;
+ inset-block-start: 0;
+ }
+
+ .panel .table thead th {
+ white-space: nowrap;
+ }
+
+ .panel .table:not(:hover):not(:focus):not(:active) {
+ background: transparent;
+ }
+
+ .panel .table thead:not(:hover):not(:focus):not(:active) {
+ background: transparent;
+ }
+}
+
+.pf-v5-c-table__tr.pf-m-clickable:hover > td,
+.pf-v5-c-table__tr.pf-m-clickable:hover > th {
+ /* PF5 has no hover background color; we have to force the override for hover colors */
+ background-color: var(--ct-color-list-hover-bg) !important;
+ color: var(--ct-color-list-hover-text) !important;
+}
+
+/* Override patternfly to fit buttons and such */
+.table > thead > tr > th,
+.table > tbody > tr > td {
+ padding: 0.5rem;
+ vertical-align: baseline;
+}
+
+/* Override the heavy patternfly headers */
+.table > thead {
+ background-image: none;
+ background-color: var(--ct-color-bg);
+}
+
+/* Make things line up */
+.table tbody tr > :first-child,
+.table thead tr > :first-child {
+ padding-inline-start: 1rem;
+}
+
+.table tbody tr > :last-child,
+.table thead tr > :last-child {
+ padding-inline-end: 1rem;
+}
diff --git a/pkg/lib/timeformat.js b/pkg/lib/timeformat.js
new file mode 100644
index 0000000..4995e31
--- /dev/null
+++ b/pkg/lib/timeformat.js
@@ -0,0 +1,66 @@
+/* Wrappers around Intl.DateTimeFormat and date-fns which use Cockpit's current locale, and define a few standard formats.
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
+ *
+ * Time stamps are given in milliseconds since the epoch.
+ */
+import cockpit from "cockpit";
+import { parse, formatDistanceToNow } from 'date-fns';
+import * as locales from 'date-fns/locale';
+
+// this needs to be dynamic, as some pages don't initialize cockpit.language right away
+export const dateFormatLang = () => cockpit.language.replace('_', '-');
+
+const dateFormatLangDateFns = () => {
+ if (cockpit.language == "en") return "enUS";
+ else return cockpit.language.replace('_', '');
+};
+
+// general Intl.DateTimeFormat formatter object
+export const formatter = options => new Intl.DateTimeFormat(dateFormatLang(), options);
+
+// common formatters; try to use these as much as possible, for UI consistency
+// 07:41 AM
+export const time = t => formatter({ timeStyle: "short" }).format(t);
+// 7:41:26 AM
+export const timeSeconds = t => formatter({ timeStyle: "medium" }).format(t);
+// June 30, 2021
+export const date = t => formatter({ dateStyle: "long" }).format(t);
+// 06/30/2021
+export const dateShort = t => formatter().format(t);
+// Jun 30, 2021, 7:41 AM
+export const dateTime = t => formatter({ dateStyle: "medium", timeStyle: "short" }).format(t);
+// Jun 30, 2021, 7:41:23 AM
+export const dateTimeSeconds = t => formatter({ dateStyle: "medium", timeStyle: "medium" }).format(t);
+// Jun 30, 7:41 AM
+export const dateTimeNoYear = t => formatter({ month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(t);
+// Wednesday, June 30, 2021
+export const weekdayDate = t => formatter({ dateStyle: "full" }).format(t);
+
+// The following options are helpful for placeholders
+// yyyy/mm/dd
+export const dateShortFormat = () => locales[dateFormatLangDateFns()].formatLong.date({ width: 'short' });
+// about 1 hour [ago]
+export const distanceToNow = (t, addSuffix) => formatDistanceToNow(t, { locale: locales[dateFormatLangDateFns()], addSuffix });
+
+// Parse a string localized date like 30.06.21 to a Date Object
+export function parseShortDate(dateStr) {
+ const parsed = parse(dateStr, dateShortFormat(), new Date());
+
+ // Strip time which may cause bugs in calendar
+ const timePortion = parsed.getTime() % (3600 * 1000 * 24);
+ return new Date(parsed - timePortion);
+}
+
+/***
+ * sorely missing from Intl: https://github.com/tc39/ecma402/issues/6
+ * based on https://github.com/unicode-cldr/cldr-core/blob/master/supplemental/weekData.json#L59
+ * However, we don't have translations for most locales, and cockpit.language does not even contain
+ * the country in most cases, so this is just an approximation.
+ * Most locales start the week on Monday (day 1), so default to that and enumerate the others.
+ */
+
+const first_dow_sun = ['en', 'ja', 'ko', 'pt', 'pt_BR', 'sv', 'zh_CN', 'zh_TW'];
+
+export function firstDayOfWeek() {
+ return first_dow_sun.indexOf(cockpit.language) >= 0 ? 0 : 1;
+}
diff --git a/pkg/lib/utils.jsx b/pkg/lib/utils.jsx
new file mode 100644
index 0000000..f5b5a4e
--- /dev/null
+++ b/pkg/lib/utils.jsx
@@ -0,0 +1,33 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+
+export function fmt_to_fragments(fmt) {
+ const args = Array.prototype.slice.call(arguments, 1);
+
+ function replace(part) {
+ if (part[0] == "$") {
+ return args[parseInt(part.slice(1))];
+ } else
+ return part;
+ }
+
+ return React.createElement.apply(null, [React.Fragment, { }].concat(fmt.split(/(\$[0-9]+)/g).map(replace)));
+}
diff --git a/pkg/metrics/index.html b/pkg/metrics/index.html
new file mode 100644
index 0000000..3fd14f9
--- /dev/null
+++ b/pkg/metrics/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this package; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="metrics-page" lang="en">
+<head>
+ <title translate="yes">Metrics and history</title>
+ <meta charset="utf-8" />
+ <meta name="description" content="" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+ <link rel="stylesheet" href="index.css" />
+
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script type="text/javascript" src="../manifests.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script type="text/javascript" src="index.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app"></div>
+</body>
+</html>
diff --git a/pkg/metrics/index.js b/pkg/metrics/index.js
new file mode 100644
index 0000000..bb7737b
--- /dev/null
+++ b/pkg/metrics/index.js
@@ -0,0 +1,30 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import 'cockpit-dark-theme'; // once per page
+
+import React from 'react';
+import { createRoot } from "react-dom/client";
+import { Application } from './metrics.jsx';
+
+document.addEventListener("DOMContentLoaded", function () {
+ const root = createRoot(document.getElementById("app"));
+ root.render(React.createElement(Application, {}));
+});
diff --git a/pkg/metrics/manifest.json b/pkg/metrics/manifest.json
new file mode 100644
index 0000000..6b3d3ec
--- /dev/null
+++ b/pkg/metrics/manifest.json
@@ -0,0 +1,11 @@
+{
+ "parent": {
+ "component": "system",
+ "docs": [
+ {
+ "label": "Performance Co-Pilot",
+ "url": "https://pcp.readthedocs.io/en/latest/"
+ }
+ ]
+ }
+}
diff --git a/pkg/metrics/metrics.jsx b/pkg/metrics/metrics.jsx
new file mode 100644
index 0000000..34bcd35
--- /dev/null
+++ b/pkg/metrics/metrics.jsx
@@ -0,0 +1,2022 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, createRef } from 'react';
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js";
+import { Icon } from "@patternfly/react-core/dist/esm/components/Icon/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Page, PageGroup, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Progress, ProgressVariant } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+import { Table, Thead, Td, Th, Tr, Tbody, TableGridBreakpoint, TableVariant, TableText } from '@patternfly/react-table';
+import {
+ AngleRightIcon, AngleDownIcon, ExclamationTriangleIcon, ExclamationCircleIcon, CogIcon, ExternalLinkAltIcon,
+ ResourcesFullIcon, ResourcesAlmostFullIcon, ResourcesAlmostEmptyIcon
+} from '@patternfly/react-icons';
+
+import cockpit from 'cockpit';
+import * as machine_info from "../lib/machine-info.js";
+import * as packagekit from "packagekit.js";
+import * as service from "service";
+import * as timeformat from "timeformat";
+import { superuser } from "superuser";
+import { journal } from "journal";
+import { useObject, useEvent, useInit } from "hooks.js";
+import { WithDialogs, useDialogs } from "dialogs.jsx";
+
+import { EmptyStatePanel } from "../lib/cockpit-components-empty-state.jsx";
+import { JournalOutput } from "cockpit-components-logs-panel.jsx";
+import { install_dialog } from "cockpit-components-install-dialog.jsx";
+import { ModalError } from "cockpit-components-inline-notification.jsx";
+import { FirewalldRequest } from "cockpit-components-firewalld-request.jsx";
+
+import "./metrics.scss";
+import "journal.css";
+
+const MSEC_PER_H = 3600000;
+const INTERVAL = 5000;
+const SAMPLES_PER_H = MSEC_PER_H / INTERVAL;
+const SAMPLES_PER_MIN = SAMPLES_PER_H / 60;
+const SVG_YMAX = (SAMPLES_PER_MIN - 1).toString();
+const LOAD_HOURS = 12;
+const _ = cockpit.gettext;
+
+// format Date as YYYY-MM-DD HH:mm:ss UTC which is human friendly and systemd compatible
+const formatUTC_ISO = t => `${t.getUTCFullYear()}-${t.getUTCMonth() + 1}-${t.getUTCDate()} ${t.getUTCHours()}:${t.getUTCMinutes()}:${t.getUTCSeconds()} UTC`;
+
+// podman's containers cgroup
+const podmanCgroupRe = /libpod-(?<containerid>[a-z|0-9]{64})\.scope$/;
+// cgroup userid
+const useridCgroupRe = /user-(?<userid>\d+).slice/;
+
+// keep track of maximum values for unbounded data, so that we can normalize it properly
+// pre-init them to avoid inflating noise
+let scaleSatCPU = 4;
+let scaleUseDisks = 10000; // KB/s
+let scaleUseNetwork = 100000; // B/s
+
+let numCpu = 1;
+let memTotal; // bytes
+let swapTotal; // bytes, can be undefined
+
+const machine_info_promise = machine_info.cpu_ram_info();
+machine_info_promise.then(info => {
+ numCpu = info.cpus;
+ memTotal = info.memory;
+ swapTotal = info.swap;
+});
+
+// round up to the nearest number that has all zeroes except for the first digit
+// avoids over-aggressive scaling, but needs scaling more often
+const scaleForValue = x => {
+ const scale = Math.pow(10, Math.floor(Math.log10(x)));
+ // this can be tweaked towards "less rescaling" with an additional scalar, like "x * 1.5 / scale"
+ return Math.ceil(x / scale) * scale;
+};
+
+const RESOURCES = {
+ use_cpu: {
+ name: _("CPU usage"),
+ event_description: _("CPU"),
+ // all in msec/s
+ normalize: ([nice, user, sys]) => (nice + user + sys) / 1000 / numCpu,
+ format: ([nice, user, sys]) => `${_("nice")}: ${Math.round(nice / 10)}%, ${_("user")}: ${Math.round(user / 10)}%, ${_("sys")}: ${Math.round(sys / 10)}%`,
+ },
+ sat_cpu: {
+ name: _("Load"),
+ event_description: _("Load"),
+ // unitless, unbounded, dynamic scaling for normalization
+ normalize: load => Math.min(load, scaleSatCPU) / scaleSatCPU,
+ format: load => cockpit.format_number(load),
+ },
+ use_memory: {
+ name: _("Memory usage"),
+ event_description: _("Memory"),
+ // assume used == total - available
+ normalize: ([totalKiB, availKiB]) => 1 - (availKiB / totalKiB),
+ format: ([totalKiB, availKiB]) => `${cockpit.format_bytes((totalKiB - availKiB) * 1024)} / ${cockpit.format_bytes(totalKiB * 1024)}`,
+ },
+ sat_memory: {
+ name: _("Swap out"),
+ event_description: _("Swap"),
+ // page/s, unbounded, and mostly 0; just categorize into "nothing" (most of the time),
+ // "a little" (< 1000 pages), and "a lot" (> 1000 pages)
+ normalize: swapout => swapout > 1000 ? 1 : (swapout > 1 ? 0.3 : 0),
+ format: swapout => cockpit.format(cockpit.ngettext("$0 page", "$0 pages", Math.floor(swapout)), Math.floor(swapout)),
+ },
+ use_disks: {
+ name: _("Disk I/O"),
+ event_description: _("Disk I/O"),
+ // KiB/s, unbounded, dynamic scaling for normalization
+ normalize: KiBps => KiBps / scaleUseDisks,
+ format: KiBps => cockpit.format_bytes_per_sec(KiBps * 1024),
+ },
+ use_network: {
+ name: _("Network I/O"),
+ event_description: _("Network I/O"),
+ // B/s, unbounded, dynamic scaling for normalization
+ normalize: bps => bps / scaleUseNetwork,
+ format: bps => cockpit.format_bytes_per_sec(bps),
+ },
+};
+
+const CURRENT_METRICS = [
+ { name: "cpu.basic.user", derive: "rate" },
+ { name: "cpu.basic.system", derive: "rate" },
+ { name: "cpu.basic.nice", derive: "rate" },
+ { name: "memory.used" },
+ { name: "memory.swap-used" },
+ { name: "disk.all.read", units: "bytes", derive: "rate" },
+ { name: "disk.all.written", units: "bytes", derive: "rate" },
+ { name: "network.interface.rx", units: "bytes", derive: "rate" },
+ { name: "network.interface.tx", units: "bytes", derive: "rate" },
+ { name: "cgroup.cpu.usage", derive: "rate" },
+ { name: "cgroup.memory.usage" },
+ { name: "cpu.core.user", derive: "rate" },
+ { name: "cpu.core.system", derive: "rate" },
+ { name: "cpu.core.nice", derive: "rate" },
+ { name: "disk.dev.read", units: "bytes", derive: "rate" },
+ { name: "disk.dev.written", units: "bytes", derive: "rate" },
+];
+
+const CPU_TEMPERATURE_METRICS = [
+ { name: "cpu.temperature" },
+];
+
+const PRIVILEGED_METRICS = [
+ { name: "disk.cgroup.read", units: "bytes", derive: "rate" },
+ { name: "disk.cgroup.written", units: "bytes", derive: "rate" },
+];
+
+const HISTORY_METRICS = [
+ // CPU utilization
+ { name: "kernel.all.cpu.nice", derive: "rate" },
+ { name: "kernel.all.cpu.user", derive: "rate" },
+ { name: "kernel.all.cpu.sys", derive: "rate" },
+
+ // CPU saturation
+ { name: "kernel.all.load" },
+
+ // memory utilization (unit: KiB)
+ { name: "mem.physmem" },
+ // mem.util.used is useless, it includes cache (unit: KiB)
+ { name: "mem.util.available" },
+
+ // memory saturation
+ { name: "swap.pagesout", derive: "rate" },
+
+ // disk utilization; despite the name, the unit is in KiB! (pminfo -d -F disk.all.total_bytes)
+ { name: "disk.all.total_bytes", derive: "rate" },
+
+ // network utilization
+ { name: "network.interface.total.bytes", derive: "rate", "omit-instances": ["lo"] },
+];
+
+function debug() {
+ if (window.debugging == "all" || window.debugging?.includes("metrics"))
+ console.debug.apply(console, arguments);
+}
+
+// metrics channel samples are compressed, see
+// https://github.com/cockpit-project/cockpit/blob/main/doc/protocol.md#payload-metrics1
+// samples is the compressed metrics channel value, state the last valid values (initialize once to empty array)
+function decompress_samples(samples, state) {
+ samples.forEach((sample, i) => {
+ if (sample instanceof Array) {
+ if (!state[i]) // uninitialized, create empty array
+ state[i] = [];
+ sample.forEach((inst, k) => {
+ if (typeof inst === 'number')
+ state[i][k] = inst;
+ });
+ } else if (typeof sample === 'number') {
+ state[i] = sample;
+ }
+ });
+}
+
+function make_rows(rows, rowProps, columnLabels) {
+ return rows.map((columns, rowIndex) =>
+ <Tr key={"row-" + rowIndex} {...rowProps?.(columns)}>
+ {columns.map((column, columnIndex) =>
+ <Td data-label={columnLabels?.[columnIndex]} key={"column-" + columnIndex}>
+ {column}
+ </Td>
+ )}
+ </Tr>
+ );
+}
+
+class CurrentMetrics extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.metrics_channel = null;
+ this.temperature_channel = null;
+ this.privileged_channel = null;
+ this.samples = [];
+ this.temperatureSamples = [];
+ this.privilegedSamples = [];
+ this.netInterfacesNames = [];
+ this.cgroupCPUNames = [];
+ this.cgroupMemoryNames = [];
+ this.cgroupDiskNames = [];
+ this.disksNames = [];
+ this.cpuTemperature = null;
+ this.cpuTemperatureColors = {
+ textColor: "",
+ iconColor: "",
+ icon: null,
+ };
+
+ this.state = {
+ userid: null,
+ memUsed: 0, // bytes
+ swapUsed: null, // bytes
+ cpuUsed: 0, // percentage
+ cpuCoresUsed: [], // [ percentage ]
+ loadAvg: null, // [ 1min, 5min, 15min ]
+ disksRead: 0, // B/s
+ disksWritten: 0, // B/s
+ mounts: [], // [{ target (string), use (percent), avail (bytes) }]
+ netInterfacesRx: [],
+ netInterfacesTx: [],
+ topServicesCPU: [], // [ { name, percent } ]
+ topServicesMemory: [], // [ { name, bytes } ]
+ topServicesDiskIO: [], // [ [ name, read, write ] ]
+ podNameMapping: {}, // { uid -> containerid -> name }
+ };
+
+ this.onVisibilityChange = this.onVisibilityChange.bind(this);
+ this.onMetricsUpdate = this.onMetricsUpdate.bind(this);
+ this.onTemperatureUpdate = this.onTemperatureUpdate.bind(this);
+ this.onPrivilegedMetricsUpdate = this.onPrivilegedMetricsUpdate.bind(this);
+ this.updateMounts = this.updateMounts.bind(this);
+ this.updateLoad = this.updateLoad.bind(this);
+
+ cockpit.addEventListener("visibilitychange", this.onVisibilityChange);
+ this.onVisibilityChange();
+
+ // regularly update info about filesystems
+ this.updateMounts();
+
+ // there is no internal metrics channel for load yet; see https://github.com/cockpit-project/cockpit/pull/14510
+ this.updateLoad();
+ }
+
+ componentDidMount() {
+ superuser.addEventListener("changed", () => this.setState({ podNameMapping: {} }));
+ cockpit.user().then(user => this.setState({ userid: user.id }));
+ }
+
+ onVisibilityChange() {
+ if (cockpit.hidden && this.temperature_channel !== null) {
+ this.temperature_channel.removeEventListener("message", this.onTemperatureUpdate);
+ this.temperature_channel.close();
+ this.temperature_channel = null;
+ }
+
+ if (cockpit.hidden && this.privileged_channel !== null) {
+ this.privileged_channel.removeEventListener("message", this.onPrivilegedMetricsUpdate);
+ this.privileged_channel.close();
+ this.privileged_channel = null;
+ }
+
+ if (cockpit.hidden && this.metrics_channel !== null) {
+ this.metrics_channel.removeEventListener("message", this.onMetricsUpdate);
+ this.metrics_channel.close();
+ this.metrics_channel = null;
+ return;
+ }
+
+ if (!cockpit.hidden && (this.temperature_channel === null)) {
+ this.temperature_channel = cockpit.channel({ payload: "metrics1", source: "internal", interval: INTERVAL, metrics: CPU_TEMPERATURE_METRICS });
+ this.temperature_channel.addEventListener("close", (ev, error) => console.warn("CPU temperature metric closed:", error));
+ this.temperature_channel.addEventListener("message", this.onTemperatureUpdate);
+ }
+
+ // requires sudo access in order to sample every cgroup
+ // limited access only allows sampling of current user's cgroups
+ if (!cockpit.hidden && (this.privileged_channel === null)) {
+ this.privileged_channel = cockpit.channel({ superuser: "try", payload: "metrics1", source: "internal", interval: INTERVAL, metrics: PRIVILEGED_METRICS });
+ this.privileged_channel.addEventListener("message", this.onPrivilegedMetricsUpdate);
+ }
+
+ if (!cockpit.hidden && this.metrics_channel === null) {
+ this.metrics_channel = cockpit.channel({ payload: "metrics1", source: "internal", interval: INTERVAL, metrics: CURRENT_METRICS });
+ this.metrics_channel.addEventListener("message", this.onMetricsUpdate);
+ }
+ }
+
+ /* Return Set of mount points which should not be shown in Disks card */
+ hideMounts(procMounts) {
+ const result = new Set();
+ procMounts.trim().split("\n")
+ .forEach(line => {
+ // looks like this: /dev/loop1 /var/mnt iso9660 ro,relatime,nojoliet,check=s,map=n,blocksize=2048 0 0
+ const fields = line.split(' ');
+ const options = fields[3].split(',');
+
+ /* hide read-only loop mounts; these are often things like snaps or iso images
+ * which are always at 100% capacity, but are uninteresting for disk usage alerts */
+ if ((fields[0].indexOf("/loop") >= 0 && options.indexOf('ro') >= 0))
+ result.add(fields[1]);
+ /* hide flatpaks */
+ if ((fields[0].indexOf('revokefs-fuse') >= 0 && fields[1].indexOf('flatpak') >= 0))
+ result.add(fields[1]);
+ });
+ return result;
+ }
+
+ updateMounts() {
+ Promise.all([
+ /* df often exits with non-zero if it encounters any filesystem it can't read;
+ but that's fine, get info about all the others */
+ cockpit.script("df --local --exclude-type=tmpfs --exclude-type=devtmpfs --block-size=1 --output=target,size,avail,pcent || true",
+ { err: "message" }),
+ cockpit.file("/proc/mounts").read()
+ ])
+ .then(([df_out, mounts_out]) => {
+ const hide = this.hideMounts(mounts_out);
+
+ // skip first line with the headings
+ const mounts = [];
+ df_out.trim()
+ .split("\n")
+ .slice(1)
+ .forEach(s => {
+ const fields = s.split(/ +/);
+ if (fields.length != 4) {
+ console.warn("Invalid line in df:", s);
+ return;
+ }
+
+ if (hide.has(fields[0]))
+ return;
+ mounts.push({
+ target: fields[0],
+ size: Number(fields[1]),
+ avail: Number(fields[2]),
+ use: Number(fields[3].slice(0, -1)), /* strip off '%' */
+ });
+ });
+
+ debug("df parsing done:", JSON.stringify(mounts));
+ this.setState({ mounts });
+
+ // update it again regularly
+ window.setTimeout(this.updateMounts, 10000);
+ })
+ .catch(ex => {
+ console.warn("Failed to run df or read /proc/mounts:", ex.toString());
+ this.setState({ mounts: [] });
+ });
+ }
+
+ updateLoad() {
+ cockpit.file("/proc/loadavg").read()
+ .then(content => {
+ // format: three load averages, then process counters; e.g.: 0.67 1.00 0.78 2/725 87151
+ this.setState({ loadAvg: content.split(' ').slice(0, 3) });
+ // update it again regularly
+ window.setTimeout(this.updateLoad, 5000);
+ })
+ .catch(ex => {
+ console.warn("Failed to read /proc/loadavg:", ex.toString());
+ this.setState({ loadAvg: null });
+ });
+ }
+
+ onTemperatureUpdate(event, message) {
+ debug("current CPU temperature message", message);
+ const data = JSON.parse(message);
+
+ if (!Array.isArray(data)) {
+ return;
+ }
+
+ data.forEach(temperatureSamples => decompress_samples(temperatureSamples, this.temperatureSamples));
+
+ if (this.temperatureSamples[0].length > 0) {
+ this.cpuTemperature = Math.round(Math.max(...this.temperatureSamples[0]));
+ } else {
+ // close the channel when bridge couldn't sample temperature
+ this.temperature_channel.close("No samples received");
+ return;
+ }
+
+ if (this.cpuTemperature <= 80) {
+ this.cpuTemperatureColors.textColor = "";
+ this.cpuTemperatureColors.iconColor = "";
+ this.cpuTemperatureColors.icon = null;
+ } else if (this.cpuTemperature < 95) {
+ this.cpuTemperatureColors.textColor = "text-color-warning";
+ this.cpuTemperatureColors.iconColor = "icon-color-warning";
+ this.cpuTemperatureColors.icon = <ExclamationTriangleIcon />;
+ } else {
+ this.cpuTemperatureColors.textColor = "text-color-critical";
+ this.cpuTemperatureColors.iconColor = "icon-color-critical";
+ this.cpuTemperatureColors.icon = <ExclamationCircleIcon />;
+ }
+ }
+
+ onMetricsUpdate(event, message) {
+ debug("current metrics message", message);
+ const data = JSON.parse(message);
+
+ // reset state on meta messages
+ if (!Array.isArray(data)) {
+ this.samples = [];
+ console.assert(data.metrics[7].name === 'network.interface.rx');
+ this.netInterfacesNames = data.metrics[7].instances.slice();
+ console.assert(data.metrics[9].name === 'cgroup.cpu.usage');
+ this.cgroupCPUNames = data.metrics[9].instances.slice();
+ this.cgroupMemoryNames = data.metrics[10].instances.slice();
+ console.assert(data.metrics[14].name === 'disk.dev.read');
+ this.disksNames = data.metrics[14].instances.slice();
+ debug("metrics message was meta, new net instance names", JSON.stringify(this.netInterfacesNames));
+ return;
+ }
+
+ data.forEach(samples => decompress_samples(samples, this.samples));
+
+ const newState = {};
+ // CPU metrics are in ms/s; divide by 10 to get percentage
+ if (typeof this.samples[0] === 'number') {
+ const cpu = Math.round((this.samples[0] + this.samples[1] + this.samples[2]) / 10 / numCpu);
+ newState.cpuUsed = cpu;
+ }
+
+ newState.memUsed = this.samples[3];
+ newState.swapUsed = this.samples[4];
+
+ if (typeof this.samples[5] === 'number')
+ newState.disksRead = this.samples[5];
+ if (typeof this.samples[6] === 'number')
+ newState.disksWritten = this.samples[6];
+
+ newState.netInterfacesRx = this.samples[7];
+ newState.netInterfacesTx = this.samples[8];
+
+ // Collect CPU cores
+ newState.cpuCoresUsed = [];
+ if (this.samples[11] && this.samples[11].length == this.samples[12].length && this.samples[12].length == this.samples[13].length) {
+ for (let i = 0; i < this.samples[11].length; i++) {
+ // CPU cores metrics are in ms/s; divide by 10 to get percentage
+ newState.cpuCoresUsed[i] = Math.round((this.samples[11][i] + this.samples[12][i] + this.samples[13][i]) / 10);
+ }
+ }
+
+ // return [ { [key, value, is_user, is_container, userid | cgroup] } ] list of the biggest n values
+ const n_biggest = (names, values, n) => {
+ const merged = [];
+ names.forEach((k, i) => {
+ const v = values[i];
+ // filter out invalid values, the empty (root) cgroup, non-services
+ if (k.endsWith('.service') && typeof v === 'number' && v != 0) {
+ const is_user = k.match(/^user.*user@\d+\.service.+/);
+ const label = k.replace(/.*\//, '').replace(/\.service$/, '');
+ // only keep cgroup basenames, and drop redundant .service suffix
+ merged.push([label, v, is_user, false, k]);
+ }
+ // filter out podman containers, but only for the logged in
+ // user or root user if the user is privileged. Other users
+ // containers will show up under the user@uid cgroup
+ const matches = k.match(podmanCgroupRe);
+ if (matches && v) {
+ let is_user = false;
+ let uid = 0;
+ const containerid = matches.groups.containerid;
+ const umatches = k.match(useridCgroupRe);
+ if (umatches) {
+ is_user = true;
+ uid = parseInt(umatches.groups.userid);
+ }
+
+ if (uid === 0 || this.state.userid == uid) {
+ merged.push([containerid, v, is_user, true, uid]);
+ }
+ }
+ });
+ merged.sort((a, b) => b[1] - a[1]);
+ return merged.slice(0, n);
+ };
+
+ // top 5 CPU and memory consuming systemd units
+ const topServicesCPU = n_biggest(this.cgroupCPUNames, this.samples[9], 5);
+ newState.topServicesCPU = topServicesCPU.map(
+ ([key, value, is_user, is_container, userid]) => this.cgroupRow(key, is_user, is_container, userid, Number(value / 10 / numCpu).toFixed(1)) // usec/s → percent
+ );
+
+ const topServicesMemory = n_biggest(this.cgroupMemoryNames, this.samples[10], 5);
+ newState.topServicesMemory = topServicesMemory.map(
+ ([key, value, is_user, is_container, userid]) => this.cgroupRow(key, is_user, is_container, userid, cockpit.format_bytes(value))
+ );
+
+ const notMappedContainers = topServicesMemory.concat(topServicesCPU).filter(([key, value, is_user, is_container, userid]) => is_container && this.getCachedPodName(userid, key) === undefined);
+ if (notMappedContainers.length !== 0) {
+ this.update_podman_name_mapping(notMappedContainers);
+ }
+ this.setState(newState);
+ }
+
+ onPrivilegedMetricsUpdate(event, message) {
+ debug("process metrics message", message);
+ const data = JSON.parse(message);
+
+ if (!Array.isArray(data)) {
+ this.cgroupDiskNames = data.metrics[0].instances.slice();
+ return;
+ }
+
+ data.forEach(privilegedSamples => decompress_samples(privilegedSamples, this.privilegedSamples));
+
+ // return [ name, read, write, isUser, isContainer, uid | cgroup ]
+ const n_biggest = (n, names, valuesA, valuesB) => {
+ const merged = [];
+ const userSlices = {};
+ names.forEach((name, i) => {
+ // filter out invalid values, the empty (root) cgroup, non-services
+ if (name.endsWith('.service')) {
+ // only keep cgroup basenames, and drop redundant .service suffix
+ const label = name.replace(/.*\//, '').replace(/\.service$/, '');
+ const isUser = name.match(useridCgroupRe);
+ merged.push([label, valuesA[i], valuesB[i], isUser, false, name]);
+ return;
+ }
+
+ // filter out podman containers, but only for the logged in
+ // user or root user if the user is privileged. Other users
+ // containers will show up under the user@uid cgroup
+ const matches = name.match(/libpod-(?<containerid>[a-z|0-9]{64})\.scope/);
+ if (matches && valuesA[i] !== undefined && valuesB[i] !== undefined) {
+ const containerid = matches.groups.containerid;
+ const umatches = name.match(useridCgroupRe);
+ const isUser = !!umatches;
+ const uid = parseInt(umatches?.groups.userid) || 0;
+
+ if (uid === 0 || this.state.userid == uid) {
+ merged.push([containerid, valuesA[i], valuesB[i], isUser, true, uid]);
+ return;
+ }
+ }
+
+ // combine user slices into user@ID
+ // { name: [ read, write ] }
+ const umatches = name.match(useridCgroupRe);
+ if (umatches) {
+ if (userSlices[umatches.groups.userid] === undefined) {
+ userSlices[umatches.groups.userid] = [valuesA[i], valuesB[i]];
+ } else {
+ userSlices[umatches.groups.userid][0] += valuesA[i];
+ userSlices[umatches.groups.userid][1] += valuesB[i];
+ }
+ }
+ });
+
+ Object.keys(userSlices).forEach((key) => {
+ merged.push(["user@" + key, userSlices[key][0], userSlices[key][1], false, false]);
+ });
+
+ // sort by overall (read + write) disk usage
+ merged.sort((a, b) => (b[1] + b[2]) - (a[1] + a[2]));
+ return merged.slice(0, n);
+ };
+
+ const newState = {};
+
+ const topServicesDiskIO = n_biggest(5, this.cgroupDiskNames, this.privilegedSamples[0], this.privilegedSamples[1]);
+ newState.topServicesDiskIO = topServicesDiskIO.filter(([_, read, write, ..._rest]) => read !== 0 || write !== 0).map(([name, read, write, isUser, isContainer, uid]) => {
+ return this.cgroupRow(name, isUser, isContainer, uid, read > 1 ? cockpit.format_bytes_per_sec(read) : 0, write > 1 ? cockpit.format_bytes_per_sec(write) : 0);
+ });
+
+ this.setState(newState);
+ }
+
+ getCachedPodName = (uid, containerid) => this.state.podNameMapping[uid] && this.state.podNameMapping[uid][containerid];
+
+ cgroupRow = (name, is_user, is_container, uid, ...values) => {
+ const podman_installed = cockpit.manifests?.podman;
+
+ const cgroupClickHandler = (name, isUser, isContainer, uid) => {
+ if (isContainer) {
+ const containerName = this.getCachedPodName(uid, name);
+ if (containerName) {
+ cockpit.jump("/podman#/?name=" + containerName);
+ } else {
+ cockpit.jump("/podman");
+ }
+ } else {
+ cockpit.jump("/system/services#/" + name + ".service" + (isUser ? "?owner=user" : ""));
+ }
+ };
+
+ let name_text = (
+ <Button variant="link" isInline component="a" key={name}
+ onClick={() => cgroupClickHandler(name, is_user, is_container, uid)}
+ isDisabled={is_container && !podman_installed}>
+ <TableText wrapModifier="truncate">
+ {is_container ? _("pod") + " " + (this.getCachedPodName(uid, name) || name.substr(0, 12)) : name}
+ </TableText>
+ </Button>
+ );
+ if (is_container && !podman_installed) {
+ name_text = (
+ <Tooltip content={_("cockpit-podman is not installed")} key={name + "_tooltip"}>
+ <div>
+ {name_text}
+ </div>
+ </Tooltip>);
+ }
+
+ const values_text = values.map((value, idx) => {
+ return <TableText key={idx} wrapModifier="nowrap">{value}</TableText>;
+ });
+
+ return [name_text, ...values_text];
+ };
+
+ /**
+ * Look up the container names using podman ps for the given cgroups.
+ */
+ update_podman_name_mapping = cgroups => {
+ // New mapping state
+ const podNameMapping = {};
+
+ const promises = cgroups.map(([containerid, value, is_user, is_container, userid]) => {
+ if (!(userid in podNameMapping)) {
+ podNameMapping[userid] = {};
+ }
+ // Always initialize the cache for when we hit an error.
+ podNameMapping[userid][containerid] = null;
+
+ if ((userid === 0 && !superuser.allowed) && userid !== this.state.userid) {
+ return null;
+ }
+ return cockpit.spawn(["podman", "ps", "--format", "json"], { superuser: userid === 0 ? "required" : null })
+ .then(result => [result, userid]);
+ }).filter(prom => prom !== null);
+
+ Promise.all(promises).then(results => {
+ for (const [output, uid] of results) {
+ try {
+ const containers = JSON.parse(output);
+ for (const container of containers) {
+ podNameMapping[uid][container.Id] = container.Names[0];
+ }
+ } catch (err) {
+ console.error("podman ps outputs invalid JSON", err.toString());
+ }
+ }
+ })
+ .catch(err => console.error("could not obtain podman names:", err))
+ .finally(() => this.setState(prevState => ({ podNameMapping: { ...prevState.podNameMapping, ...podNameMapping } })));
+ };
+
+ render() {
+ const memUsedFraction = memTotal ? this.state.memUsed / memTotal : 0;
+ const memAvail = memTotal ? (memTotal - this.state.memUsed) : 0;
+ const num_cpu_str = cockpit.format(cockpit.ngettext("$0 CPU", "$0 CPUs", numCpu), numCpu);
+
+ const netIO = this.netInterfacesNames.map((iface, i) => [
+ <Button variant="link" isInline onClick={() => cockpit.jump(`/network#/${iface}`) } key={iface}>{iface}</Button>,
+ this.state.netInterfacesRx[i] >= 1 ? cockpit.format_bytes_per_sec(this.state.netInterfacesRx[i]) : "0",
+ this.state.netInterfacesTx[i] >= 1 ? cockpit.format_bytes_per_sec(this.state.netInterfacesTx[i]) : "0",
+ ]);
+
+ let swapProgress;
+
+ if (swapTotal) {
+ const swapUsedFraction = this.state.swapUsed / swapTotal;
+ const swapAvail = swapTotal - this.state.swapUsed;
+ swapProgress = (
+ <Tooltip content={ cockpit.format(_("$0 total"), cockpit.format_bytes(swapTotal)) } position="bottom">
+ <Progress
+ id="current-swap-usage"
+ title={ _("Swap") }
+ value={this.state.swapUsed}
+ className="pf-m-sm"
+ min={0} max={swapTotal}
+ variant={swapUsedFraction > 0.9 ? ProgressVariant.danger : swapUsedFraction >= 0.8 ? ProgressVariant.warning : null}
+ label={ cockpit.format(_("$0 available"), cockpit.format_bytes(swapAvail)) } />
+ </Tooltip>);
+ }
+
+ let cores = null;
+ let topCore = null;
+ let allCpus = null;
+ let cpu_label = null;
+ if (this.state.cpuCoresUsed.length > 1) {
+ const top_cores = this.state.cpuCoresUsed.map((v, i) => [i, v]).sort((a, b) => b[1] - a[1])
+ .slice(0, 16);
+ cores = (<Grid className='cpu-all' component='dl'>
+ {top_cores.map(c =>
+ <React.Fragment key={c[0]}>
+ <GridItem component='dt'>{ cockpit.format(_("Core $0"), c[0]) }</GridItem>
+ <GridItem component='dd'>{c[1]}%</GridItem>
+ </React.Fragment>)
+ }
+ </Grid>);
+
+ cpu_label = (
+ <Flex spaceItems={{ default: 'spaceItemsNone' }} justifyContent={{ default: 'justifyContentFlexEnd' }}>
+ <FlexItem>&nbsp;{ cockpit.format(_("average: $0%"), this.state.cpuUsed) }</FlexItem>
+ <FlexItem>&nbsp;{ cockpit.format(_("max: $0%"), top_cores[0][1]) }</FlexItem>
+ </Flex>);
+
+ topCore = <Progress
+ aria-label={_("Current top CPU usage")}
+ id="current-top-cpu-usage"
+ value={top_cores[0][1]}
+ className="current-top-cpu-usage pf-m-sm"
+ min={0} max={100}
+ variant={ top_cores[0][1] > 90 ? ProgressVariant.danger : top_cores[0][1] >= 80 ? ProgressVariant.warning : ProgressVariant.info }
+ measureLocation="none" />;
+
+ allCpus = (
+ <Popover minWidth={0} aria-label={ _("View all CPUs") } bodyContent={cores}>
+ <Button variant="link" className='pf-v5-u-font-size-sm'>{ _("View all CPUs") }</Button>
+ </Popover>);
+ } else {
+ cpu_label = this.state.cpuUsed + '%';
+ }
+
+ const disksUsage = (this.disksNames.length > 0 && this.samples[14] && this.samples[15])
+ ? (
+ this.disksNames.map((name, i) => [
+ name,
+ this.samples[14][i] >= 1 ? cockpit.format_bytes_per_sec(this.samples[14][i]) : "0",
+ this.samples[15][i] >= 1 ? cockpit.format_bytes_per_sec(this.samples[15][i]) : "0",
+ ])
+ )
+ : [];
+
+ let allDisks = null;
+ const rowPropsDisks = row => ({ 'device-name': row[0] });
+ const diskColumns = [_("Device"), _("Read"), _("Write")];
+ if (disksUsage.length > 1) {
+ const disksTableContent = (
+ <Table
+ variant={TableVariant.compact}
+ gridBreakPoint={TableGridBreakpoint.gridLg}
+ borders={false}
+ aria-label={ _("Disks usage") }>
+ <Thead>
+ <Tr>{diskColumns.map(col => <Th key={col}>{col}</Th>)}</Tr>
+ </Thead>
+ <Tbody className="pf-v5-m-tabular-nums disks-nowrap">
+ {make_rows(disksUsage, rowPropsDisks, diskColumns)}
+ </Tbody>
+ </Table>
+ );
+
+ allDisks = (
+ <Popover minWidth={0} aria-label={ _("View all disks") } bodyContent={disksTableContent}>
+ <Button variant="link" className='pf-v5-u-font-size-sm'>{ _("View per-disk throughput") }</Button>
+ </Popover>
+ );
+ }
+
+ // first element is the jump button, key is interface name
+ const rowPropsIface = row => ({ 'data-interface': row[0].key });
+ const rowPropsDiskIO = row => ({ 'cgroup-name': row[0] });
+ const topServicesCPUColumns = [_("Service"), "%"];
+ const topServicesMemoryColumns = [_("Service"), _("Used")];
+ const ifaceColumns = [_("Interface"), _("In"), _("Out")];
+
+ return (
+ <Gallery className="current-metrics" hasGutter>
+ <Card id="current-metrics-card-cpu">
+ <CardHeader className='align-baseline'>
+ <CardTitle>{ _("CPU") }</CardTitle>
+ { this.cpuTemperature !== null &&
+ <span className="temperature">
+ <span className={this.cpuTemperatureColors.iconColor}>
+ {this.cpuTemperatureColors.icon}
+ </span>
+ &nbsp;
+ <span className={this.cpuTemperatureColors.textColor}>
+ { cockpit.format("$0 °C", this.cpuTemperature) }
+ </span>
+ </span> }
+ </CardHeader>
+ <CardBody>
+ <div className="progress-stack-no-space">
+ <Progress
+ id="current-cpu-usage"
+ value={this.state.cpuUsed}
+ className="current-cpu-usage pf-m-sm"
+ min={0} max={100}
+ variant={ this.state.cpuUsed > 90 ? ProgressVariant.danger : this.state.cpuUsed >= 80 ? ProgressVariant.warning : null }
+ title={ num_cpu_str }
+ label={ cpu_label } />
+ {topCore}
+ {allCpus}
+ </div>
+
+ { this.state.loadAvg &&
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Load") }</DescriptionListTerm>
+ <DescriptionListDescription id="load-avg">
+ <Flex spaceItems={{ default: 'spaceItemsXs' }}>
+ <FlexItem>{ _("1 min") }:&nbsp;{ this.state.loadAvg[0] },</FlexItem>
+ <FlexItem>{ _("5 min") }:&nbsp;{ this.state.loadAvg[1] },</FlexItem>
+ <FlexItem>{ _("15 min") }:&nbsp;{ this.state.loadAvg[2] }</FlexItem>
+ </Flex>
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList> }
+
+ { this.state.topServicesCPU.length > 0 &&
+ <Table
+ variant={TableVariant.compact}
+ gridBreakPoint={TableGridBreakpoint.none}
+ borders={false}
+ aria-label={ _("Top 5 CPU services") }>
+ <Thead>
+ <Tr>
+ <Th width={80}>{_("Service")}</Th>
+ <Th>%</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {make_rows(this.state.topServicesCPU, undefined, topServicesCPUColumns)}
+ </Tbody>
+ </Table> }
+ </CardBody>
+ </Card>
+
+ <Card>
+ <CardTitle>{ _("Memory") }</CardTitle>
+ <CardBody>
+ <div className="progress-stack">
+ <Tooltip
+ content={ cockpit.format(_("$0 total"), cockpit.format_bytes(memTotal)) }
+ position="bottom">
+ <Progress
+ id="current-memory-usage"
+ title={ _("RAM") }
+ value={memTotal ? this.state.memUsed : undefined}
+ className="pf-m-sm"
+ min={0} max={memTotal}
+ variant={memUsedFraction > 0.9 ? ProgressVariant.danger : memUsedFraction >= 0.8 ? ProgressVariant.warning : null}
+ label={ memAvail ? cockpit.format(_("$0 available"), cockpit.format_bytes(memAvail)) : "" } />
+ </Tooltip>
+ {swapProgress}
+ </div>
+
+ { this.state.topServicesMemory.length > 0 &&
+ <Table
+ variant={TableVariant.compact}
+ gridBreakPoint={TableGridBreakpoint.none}
+ borders={false}
+ aria-label={ _("Top 5 memory services") }>
+ <Thead>
+ <Tr>
+ <Th width={80}>{_("Service")}</Th>
+ <Th>{_("Used")}</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {make_rows(this.state.topServicesMemory, undefined, topServicesMemoryColumns)}
+ </Tbody>
+ </Table> }
+ </CardBody>
+ </Card>
+
+ <Card id="current-metrics-card-disks">
+ <CardTitle>{ _("Disks") }</CardTitle>
+ <CardBody>
+ <DescriptionList isHorizontal columnModifier={{ default: '2Col' }}>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Read") }</DescriptionListTerm>
+ <DescriptionListDescription id="current-disks-read">{ this.state.disksRead >= 1 ? cockpit.format_bytes_per_sec(this.state.disksRead) : "0" }</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Write") }</DescriptionListTerm>
+ <DescriptionListDescription id="current-disks-write">{ this.state.disksWritten >= 1 ? cockpit.format_bytes_per_sec(this.state.disksWritten) : "0" }</DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ <div className="all-disks-no-gap">
+ {allDisks}
+ </div>
+ <div id="current-disks-usage" className="progress-stack"> {
+ this.state.mounts.map(info => {
+ let progress = (
+ <Progress
+ data-disk-usage-target={info.target}
+ value={info.use} min={0} max={100}
+ className="pf-m-sm"
+ variant={info.use > 90 ? ProgressVariant.danger : info.use >= 80 ? ProgressVariant.warning : null}
+ title={info.target}
+ label={ cockpit.format(_("$0 free"), cockpit.format_bytes(info.avail, 1000)) } />
+ );
+ if (cockpit.manifests?.storage)
+ progress = <Button variant="link" isInline onClick={() => cockpit.jump("/storage") }>{progress}</Button>;
+
+ return (
+ <Tooltip
+ key={info.target}
+ content={ cockpit.format(_("$0 total"), cockpit.format_bytes(info.size, 1000)) }
+ position="bottom">
+ {progress}
+ </Tooltip>
+ );
+ })
+ }
+ </div>
+ { this.state.topServicesDiskIO.length > 0 &&
+ <Table
+ variant={TableVariant.compact}
+ gridBreakPoint={TableGridBreakpoint.none}
+ borders={false}
+ aria-label={ _("Top 5 disk usage services") }>
+ <Thead>
+ <Tr>
+ <Th width={50}>{_("Service")}</Th>
+ <Th>{_("Read")}</Th>
+ <Th>{_("Write")}</Th>
+ </Tr>
+ </Thead>
+ <Tbody className="pf-v5-m-tabular-nums">
+ {make_rows(this.state.topServicesDiskIO, rowPropsDiskIO, [_("Service"), _("Read"), _("Write")])}
+ </Tbody>
+ </Table> }
+ </CardBody>
+ </Card>
+
+ <Card className="current-metrics-network">
+ <CardTitle>{ _("Network") }</CardTitle>
+ <CardBody>
+ <Table
+ variant={TableVariant.compact}
+ // FIXME: If we can make the table less wide, then we can switch from gridLg to none
+ // and (possibly) dropping (at least some of) the font size overrides
+ // this would require breaking out the units/s into its own row
+ gridBreakPoint={TableGridBreakpoint.gridLg}
+ borders={false}
+ aria-label={ _("Network usage") }>
+ <Thead>
+ <Tr>{ifaceColumns.map(col => <Th key={col}>{col}</Th>)}</Tr>
+ </Thead>
+ <Tbody className="network-nowrap-shrink">
+ {make_rows(netIO, rowPropsIface, ifaceColumns)}
+ </Tbody>
+ </Table>
+ </CardBody>
+ </Card>
+ </Gallery>
+ );
+ }
+}
+
+const SvgGraph = ({ data, resource, have_sat }) => {
+ const dataPoints = key => (
+ "0 0, " + // start polygon at (0, 0)
+ data.map((samples, index) => (samples && typeof samples[key] === 'number') ? parseInt(samples[key] * 100).toString() + "% " + (((index / SVG_YMAX) * 100).toString() + "%") : "").join(", ") +
+ ", 0 " + ((data.length - 1) / SVG_YMAX) * 100 + "%" // close polygon
+ );
+
+ return (
+ <>
+ <div
+ className="polygon polygon-use"
+ style={{ "--points": dataPoints("use_" + resource) }}
+ points={dataPoints("use_" + resource)}
+ />
+ { have_sat && <div
+ className="polygon polygon-sat"
+ style={{ "--points": dataPoints("sat_" + resource) }}
+ points={dataPoints("sat_" + resource)}
+ /> }
+ </>
+ );
+};
+
+class MetricsMinute extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ logs: null,
+ logsUrl: null,
+ };
+ this.onHover = this.onHover.bind(this);
+ this.findLogs = this.findLogs.bind(this);
+
+ this.spikesButtonRef = createRef();
+ }
+
+ componentDidMount() {
+ if (this.props.isExpanded && this.props.events)
+ this.findLogs(this.props.events.start - 4, this.props.events.end + 4); // +- 20s
+ }
+
+ onHover(ev) {
+ // FIXME - throttle debounce this
+ const bounds = ev.target.getBoundingClientRect();
+ const offsetY = (ev.clientY - bounds.y) / bounds.height;
+ const indexOffset = Math.floor((1 - offsetY) * SAMPLES_PER_MIN);
+ const sample = this.props.rawData[indexOffset];
+ if (!sample)
+ return;
+
+ const time = this.props.startTime + this.props.minute * 60000 + indexOffset * INTERVAL;
+ let tooltip = timeformat.timeSeconds(time) + "\n\n";
+ Object.entries(sample).forEach(([t, v]) => {
+ if (v !== null && v !== undefined)
+ tooltip += `${RESOURCES[t].name}: ${RESOURCES[t].format(v)}\n`;
+ });
+ ev.target.setAttribute("title", tooltip);
+ }
+
+ findLogs(start, end) {
+ const timestamp = this.props.startTime + (this.props.minute * 60000);
+ const start_minute = Math.floor(start / SAMPLES_PER_MIN);
+ const start_second = (start - (start_minute * SAMPLES_PER_MIN)) * (60 / SAMPLES_PER_MIN);
+ const end_minute = Math.floor(end / SAMPLES_PER_MIN);
+ const end_second = (end - (end_minute * SAMPLES_PER_MIN)) * (60 / SAMPLES_PER_MIN);
+
+ const time = new Date(timestamp);
+ time.setUTCMinutes(start_minute);
+ time.setUTCSeconds(start_second);
+ const since = formatUTC_ISO(time);
+
+ time.setUTCMinutes(end_minute);
+ time.setUTCSeconds(end_second);
+ const until = formatUTC_ISO(time);
+
+ const match = { priority: "info", since, until, follow: false, count: 10 };
+ const journalctl = journal.journalctl(match);
+
+ const out = new JournalOutput(match);
+ out.render_day_header = () => { return null };
+ const render = journal.renderer(out);
+
+ journalctl.stream(entries => {
+ entries.forEach(entry => render.prepend(entry));
+ render.prepend_flush();
+ })
+ .then(() => {
+ let logsUrl;
+ if (out.logs.length === 0) {
+ // without logs, increase verbosity and time range (-15 mins to + 1 min)
+ const since = formatUTC_ISO(new Date(timestamp - 15 * 60000));
+ const until = formatUTC_ISO(new Date(timestamp + 60000));
+ logsUrl = `/system/logs/#/?priority=debug&since=${encodeURIComponent(since)}&until=${encodeURIComponent(until)}&follow=false`;
+ } else {
+ // with logs, show the exact minute and same log level as on the metrics page
+ logsUrl = `/system/logs/#/?priority=info&since=${encodeURIComponent(since)}&until=${encodeURIComponent(until)}&follow=false`;
+ }
+
+ this.setState({ logs: out.logs, logsUrl });
+ });
+ }
+
+ render() {
+ const first = this.props.data.find(i => i !== null);
+
+ const graphs = Object.keys(this.props.selectedVisibility).filter(itm => this.props.selectedVisibility[itm]).map(resource => {
+ // not all resources have a saturation metric
+ let have_sat = !!RESOURCES["sat_" + resource];
+
+ // If there is no swap, don't render it
+ if (resource === "memory" && !swapTotal)
+ have_sat = false;
+
+ let graph = null;
+ if (this.props.events) {
+ // render full SVG graphs for "expanded" minutes with events
+ graph = <SvgGraph key={resource} data={this.props.data} resource={resource} have_sat={have_sat} />;
+ } else if (first) {
+ // render simple bars for "compressed" minutes without events
+ graph = <>
+ <div className="polygon-use compressed" style={{ "--utilization": first["use_" + resource] || 0 }} />
+ { have_sat && <div className="polygon-sat compressed" style={{ "--saturation": first["sat_" + resource] || 0 }} /> }
+ </>;
+ }
+
+ return (
+ <div
+ key={ resource + this.props.startTime + this.props.minute }
+ className={ ("metrics-data metrics-data-" + resource) + (first ? " valid-data" : " empty-data") + (have_sat ? " have-saturation" : "") }
+ aria-hidden="true"
+ { ...(this.props.isExpanded && { onMouseMove: this.onHover }) }
+ >
+ {graph}
+ </div>
+ );
+ });
+
+ let desc;
+ if (this.props.isExpanded && this.props.events) {
+ const timestamp = this.props.startTime + (this.props.minute * 60000);
+
+ const logsPanel = (
+ <>
+ {(this.state.logs?.length && this.state.logsUrl) && <Button variant="secondary" onClick={e => cockpit.jump(this.state.logsUrl)}>{_("View detailed logs")}</Button>}
+ <div className="cockpit-log-panel">
+ {this.state.logs?.length ? this.state.logs : _("No log entries")}
+ </div>
+ </>
+ );
+
+ const resourceDesc = (
+ <span className="type">
+ {this.props.events.events.map(t => RESOURCES[t].event_description).join(", ")}
+ </span>
+ );
+ desc = (
+ <>
+ <div className="metrics-events">
+ <time>{ timeformat.time(timestamp) }</time>
+ <span className="spikes_count" />
+ {this.state.logs?.length > 0
+ ? <Button
+ ref={this.spikesButtonRef}
+ variant="link" isInline
+ className="spikes_info">
+ {resourceDesc}
+ </Button>
+ : <span className="spikes_info">{resourceDesc}</span>}
+ </div>
+ {this.state.logs?.length > 0 && <Popover position="right" hasAutoWidth className="metrics-events-popover" bodyContent={logsPanel} triggerRef={this.spikesButtonRef} />}
+ </>
+ );
+ }
+
+ return (
+ <div className="metrics-minute" data-minute={this.props.minute}>
+ { this.props.isExpanded && desc }
+ <div className="metrics-graphs">
+ { graphs }
+ </div>
+ </div>
+ );
+ }
+}
+
+class MetricsHour extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ minuteGraphs: [],
+ minute_events: {},
+ isHourExpanded: false,
+ dataItems: 0,
+ };
+
+ this.updateGraphs = this.updateGraphs.bind(this);
+ }
+
+ componentDidMount() {
+ this.updateGraphs(this.props.data, this.props.startTime, this.props.selectedVisibility);
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (this.state.dataItems !== nextProps.data.length ||
+ this.state.isHourExpanded !== nextState.isHourExpanded ||
+ this.props.startTime !== nextProps.startTime ||
+ Object.keys(this.props.selectedVisibility).some(itm => this.props.selectedVisibility[itm] != nextProps.selectedVisibility[itm])) {
+ this.updateGraphs(nextProps.data, nextProps.startTime, nextProps.selectedVisibility, nextState.isHourExpanded);
+ return false;
+ }
+
+ return true;
+ }
+
+ // data: type → SAMPLES_PER_H objects from startTime
+ updateGraphs(data, startTime, selectedVisibility, isHourExpanded) {
+ const filteredData = data.map(sample => Object.keys(sample)
+ .filter(key => selectedVisibility[key.split("_")[1]])
+ .reduce((cur, key) => Object.assign(cur, { [key]: sample[key] }), {}));
+ // Normalize data
+ const normData = filteredData.map(sample => {
+ if (sample === null)
+ return null;
+ const n = {};
+ for (const type in sample)
+ n[type] = (sample[type] !== null && sample[type] !== undefined) ? RESOURCES[type].normalize(sample[type]) : null;
+ return n;
+ });
+
+ // Count minutes to render
+ let minutes = 60;
+ if (this.props.clipLeading) {
+ // When clipping of empty leading minutes is allowed, find the highest 5 minute interval with valid data
+ let m = 55;
+ for (; m >= 0; m = m - 5) {
+ const dataOffset = m * SAMPLES_PER_MIN;
+ const dataSlice = normData.slice(dataOffset, dataOffset + SAMPLES_PER_MIN * 5);
+ if (dataSlice.some(i => i !== null && i !== undefined))
+ break;
+ }
+ minutes = m + 5;
+ }
+
+ // Compute spike events
+ const minute_events = {};
+ for (const type in RESOURCES) {
+ let prev_val = data[0] ? data[0][type] : null;
+ normData.forEach((samples, i) => {
+ if (samples === null)
+ return;
+ const value = samples[type];
+ // either high enough slope, or crossing the 80% threshold
+ if (prev_val !== null && (value - prev_val > 0.25 || (prev_val < 0.75 && value >= 0.8))) {
+ const minute = Math.floor(i / SAMPLES_PER_MIN);
+ if (minute_events[minute] === undefined)
+ minute_events[minute] = { events: [], start: i - 1 };
+
+ minute_events[minute].end = i;
+
+ // For every minute show each type of event max once
+ if (minute_events[minute].events.indexOf(type) === -1)
+ minute_events[minute].events.push(type);
+ }
+ prev_val = value;
+ });
+ }
+
+ const minuteGraphs = [];
+
+ for (let minute = minutes - 1; minute >= 0; --minute) {
+ const dataOffset = minute * SAMPLES_PER_MIN;
+ const dataSlice = normData.slice(dataOffset, dataOffset + SAMPLES_PER_MIN);
+ const rawSlice = this.props.data.slice(dataOffset, dataOffset + SAMPLES_PER_MIN);
+ minuteGraphs.push(
+ <MetricsMinute
+ isExpanded={isHourExpanded}
+ key={minute}
+ minute={minute}
+ data={dataSlice}
+ rawData={rawSlice}
+ events={minute_events[minute]}
+ startTime={this.props.startTime}
+ selectedVisibility={selectedVisibility} />
+ );
+ }
+
+ this.setState((_, prevProps) => ({
+ isHourExpanded,
+ minute_events,
+ minuteGraphs,
+ dataItems: prevProps.data.length
+ }));
+ }
+
+ render() {
+ const hourDesc = (
+ <HourDescription
+ minute_events={this.state.minute_events}
+ onToggleHourExpanded={isHourExpanded => this.setState({ isHourExpanded })}
+ startTime={this.props.startTime}
+ isHourExpanded={this.state.isHourExpanded} />
+ );
+
+ return (
+ <div id={ "metrics-hour-" + this.props.startTime.toString() }
+ className={"metrics-hour" + (!this.state.isHourExpanded ? " metrics-hour-compressed" : "")}>
+ {hourDesc}
+ {!this.state.isHourExpanded ? <div className="metrics-minutes">{this.state.minuteGraphs}</div> : this.state.minuteGraphs}
+ </div>
+ );
+ }
+}
+
+const HourDescription = ({ minute_events, isHourExpanded, onToggleHourExpanded, startTime }) => {
+ const event_types = {};
+ Object.keys(RESOURCES).forEach(t => { event_types[t] = 0 });
+ Object.values(minute_events).forEach(event => { event.events.forEach(t => { event_types[t] += 1 }) });
+ const spikes = Object.values(event_types).reduce((acc, event_type_count) => acc + event_type_count, 0);
+ return (
+ <span className={"metrics-events" + (isHourExpanded ? " metrics-events-hour-header-expanded" : "")}>
+ {spikes > 0 &&
+ <Button variant="plain" className="metrics-events-expander" onClick={() => onToggleHourExpanded(!isHourExpanded)} icon={isHourExpanded ? <AngleDownIcon /> : <Icon shouldMirrorRTL><AngleRightIcon /></Icon>} />}
+ <time>{ timeformat.time(startTime) }</time>
+ <Flex flexWrap={{ default: 'nowrap' }} spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsBaseline' }} className="spikes_count">
+ {spikes >= 10 && <ResourcesFullIcon color="var(--resource-icon-color-full)" />}
+ {spikes >= 5 && spikes < 10 && <ResourcesAlmostFullIcon color="var(--resource-icon-color-middle)" />}
+ {spikes < 5 && spikes > 0 && <ResourcesAlmostEmptyIcon color="var(--resource-icon-color-empty)" />}
+ <FlexItem>
+ {spikes ? cockpit.format(cockpit.ngettext("$0 spike", "$0 spikes", spikes), spikes) : _("No events")}
+ </FlexItem>
+ </Flex>
+ <span className="spikes_info">
+ {spikes > 0 && Object.entries(event_types)
+ .filter(([_, count]) => count > 0)
+ .map(([event_type, count]) => cockpit.format("$0 $1", count, RESOURCES[event_type].event_description))
+ .join(", ")}
+ </span>
+ </span>
+ );
+};
+
+// null means "not initialized yet"
+const invalidService = proxy => proxy.state === null;
+const runningService = proxy => ['running', 'starting'].indexOf(proxy.state) >= 0;
+
+const wait_cond = (cond, objects) => {
+ return new Promise((resolve, reject) => {
+ const check = () => {
+ if (cond()) {
+ objects.forEach(o => o.removeEventListener("changed", check));
+ resolve();
+ }
+ };
+ objects.forEach(o => o.addEventListener("changed", check));
+ check();
+ });
+};
+
+const PCPConfigDialog = ({
+ firewalldRequest,
+ needsLogout, setNeedsLogout,
+ s_pmlogger, s_pmproxy, s_redis, s_redis_server
+}) => {
+ const Dialogs = useDialogs();
+ const dialogInitialProxyValue = runningService(s_pmproxy) && (runningService(s_redis) || runningService(s_redis_server));
+ const [dialogError, setDialogError] = useState(null);
+ const [dialogLoggerValue, setDialogLoggerValue] = useState(runningService(s_pmlogger));
+ const [dialogProxyValue, setDialogProxyValue] = useState(dialogInitialProxyValue);
+ const [pending, setPending] = useState(false);
+ const [packagekitExists, setPackagekitExists] = useState(null);
+
+ useInit(() => packagekit.detect().then(setPackagekitExists));
+
+ const handleInstall = () => {
+ // when enabling services, install missing packages on demand
+ const missing = [];
+ if (dialogLoggerValue && !s_pmlogger.exists)
+ missing.push("cockpit-pcp");
+ if (dialogProxyValue && !(s_redis.exists || s_redis_server.exists))
+ missing.push("redis");
+ if (missing.length > 0) {
+ debug("PCPConfig: missing packages", JSON.stringify(missing), ", offering install");
+ Dialogs.close();
+ return install_dialog(missing)
+ .then(() => {
+ debug("PCPConfig: package installation successful");
+ if (missing.indexOf("cockpit-pcp") >= 0)
+ setNeedsLogout(true);
+ return wait_cond(() => (s_pmlogger.exists &&
+ (!dialogProxyValue || (s_pmproxy.exists && (s_redis.exists || s_redis_server.exists)))),
+ [s_pmlogger, s_pmproxy, s_redis, s_redis_server]);
+ });
+ } else
+ return Promise.resolve();
+ };
+
+ const handleSave = () => {
+ debug("PCPConfig handleSave(): dialogLoggerValue", dialogLoggerValue, "dialogInitialProxyValue", dialogInitialProxyValue, "dialogProxyValue", dialogProxyValue);
+
+ handleInstall()
+ .then(() => {
+ setPending(true);
+
+ let real_redis;
+ let redis_name;
+ if (s_redis_server.exists) {
+ real_redis = s_redis_server;
+ redis_name = "redis-server.service";
+ } else {
+ real_redis = s_redis;
+ redis_name = "redis.service";
+ }
+
+ const redis_enable_cmd = `mkdir -p /etc/systemd/system/pmproxy.service.wants; ln -sf ../${redis_name} /etc/systemd/system/pmproxy.service.wants/${redis_name}`;
+ const redis_disable_cmd = `rm -f /etc/systemd/system/pmproxy.service.wants/${redis_name}; rmdir -p /etc/systemd/system/pmproxy.service.wants 2>/dev/null || true`;
+ let action;
+
+ // enable/disable does a daemon-reload, which interferes with start on some distros; so don't run them in parallel
+ if (dialogLoggerValue)
+ action = s_pmlogger.start().then(() => s_pmlogger.enable());
+ else
+ action = s_pmlogger.stop().finally(() => s_pmlogger.disable());
+
+ if (dialogProxyValue !== null && dialogInitialProxyValue !== dialogProxyValue) {
+ if (dialogProxyValue === true) {
+ // pmproxy.service needs to (re)start *after* redis to recognize it
+ action = action
+ .then(() => real_redis.start())
+ .then(() => s_pmproxy.restart())
+ // turn redis into a dependency, as the metrics API requires it
+ .then(() => cockpit.script(redis_enable_cmd, { superuser: "require", err: "message" }))
+ .then(() => s_pmproxy.enable());
+ } else {
+ // don't stop redis here -- it's a shared service, other things may be using it
+ action = action
+ .then(() => s_pmproxy.stop())
+ .then(() => cockpit.script(redis_disable_cmd, { superuser: "require", err: "message" }))
+ .then(() => s_pmproxy.disable());
+ }
+ }
+
+ action
+ .then(() => {
+ Dialogs.close();
+ if (dialogProxyValue && !dialogInitialProxyValue && firewalldRequest)
+ firewalldRequest({ service: "pmproxy", title: _("Open the pmproxy service in the firewall to share metrics.") });
+ else
+ firewalldRequest(null);
+ })
+ .catch(err => { setPending(false); setDialogError(err.toString()) });
+ })
+ .catch(() => null); // ignore cancel in install dialog
+ };
+
+ return (
+ <Modal position="top" variant="small" isOpen
+ id="pcp-settings-modal"
+ onClose={Dialogs.close}
+ title={ _("Metrics settings") }
+ description={
+ <div className="pcp-settings-modal-text">
+ { _("Performance Co-Pilot collects and analyzes performance metrics from your system.") }
+
+ <Button component="a" variant="link" href="https://cockpit-project.org/guide/latest/feature-pcp.html"
+ isInline
+ target="_blank" rel="noopener noreferrer"
+ icon={<ExternalLinkAltIcon />}>
+ { _("Read more...") }
+ </Button>
+ </div>
+ }
+ footer={<>
+ <Button variant='primary' onClick={handleSave} isDisabled={pending} isLoading={pending}>
+ { _("Save") }
+ </Button>
+ <Button variant='link' className='btn-cancel' onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>
+ }>
+
+ <Stack hasGutter>
+ { dialogError && <ModalError dialogError={ _("Failed to configure PCP") } dialogErrorDetail={dialogError} /> }
+ <StackItem>
+ <Switch id="switch-pmlogger"
+ isChecked={dialogLoggerValue}
+ isDisabled={!s_pmlogger.exists && !packagekitExists}
+ label={
+ <Flex spaceItems={{ modifier: 'spaceItemsXl' }}>
+ <FlexItem>{ _("Collect metrics") }</FlexItem>
+ <TextContent>
+ <Text component={TextVariants.small}>(pmlogger.service)</Text>
+ </TextContent>
+ </Flex>
+ }
+ onChange={(_event, enable) => {
+ // pmproxy needs pmlogger, auto-disable it
+ setDialogLoggerValue(enable);
+ if (!enable)
+ setDialogProxyValue(false);
+ }} />
+
+ <Switch id="switch-pmproxy"
+ isChecked={dialogProxyValue}
+ label={
+ <Flex spaceItems={{ modifier: 'spaceItemsXl' }}>
+ <FlexItem>{ _("Export to network") }</FlexItem>
+ <TextContent>
+ <Text component={TextVariants.small}>(pmproxy.service)</Text>
+ </TextContent>
+ </Flex>
+ }
+ isDisabled={ !dialogLoggerValue }
+ onChange={(_event, enable) => setDialogProxyValue(enable)} />
+ </StackItem>
+ </Stack>
+ </Modal>);
+};
+
+const PCPConfig = ({ buttonVariant, firewalldRequest, needsLogout, setNeedsLogout }) => {
+ const Dialogs = useDialogs();
+
+ const s_pmlogger = useObject(() => service.proxy("pmlogger.service"), null, []);
+ const s_pmproxy = useObject(() => service.proxy("pmproxy.service"), null, []);
+ // redis.service on Fedora/RHEL, redis-server.service on Debian/Ubuntu with an Alias=redis
+ const s_redis = useObject(() => service.proxy("redis.service"), null, []);
+ const s_redis_server = useObject(() => service.proxy("redis-server.service"), null, []);
+
+ useEvent(superuser, "changed");
+ useEvent(s_pmlogger, "changed");
+ useEvent(s_pmproxy, "changed");
+ useEvent(s_redis, "changed");
+ useEvent(s_redis_server, "changed");
+
+ debug("PCPConfig s_pmlogger.state", s_pmlogger.state, "needs logout", needsLogout);
+ debug("PCPConfig s_pmproxy state", s_pmproxy.state, "redis exists", s_redis.exists, "state", s_redis.state, "redis-server exists", s_redis_server.exists, "state", s_redis_server.state);
+
+ if (!superuser.allowed)
+ return null;
+
+ function show_dialog() {
+ Dialogs.show(<PCPConfigDialog firewalldRequest={firewalldRequest}
+ needsLogout={needsLogout} setNeedsLogout={setNeedsLogout}
+ s_pmlogger={s_pmlogger}
+ s_pmproxy={s_pmproxy}
+ s_redis={s_redis} s_redis_server={s_redis_server} />);
+ }
+
+ return (
+ <Button variant={buttonVariant} icon={<CogIcon />}
+ isDisabled={ invalidService(s_pmlogger) || invalidService(s_pmproxy) || invalidService(s_redis) || invalidService(s_redis_server) }
+ onClick={show_dialog}>
+ { _("Metrics settings") }
+ </Button>);
+};
+
+class MetricsHistory extends React.Component {
+ constructor(props) {
+ super(props);
+ // metrics data: hour timestamp → array of SAMPLES_PER_H objects of { type → value } or null
+ this.data = {};
+ // timestamp of the most recent sample that we got (for auto-refresh)
+ this.most_recent = 0;
+ // Oldest read data
+ this.oldest_timestamp = 0;
+ // Timestamp representing today midnight to calculate other days for date select
+ this.today_midnight = null;
+ this.columns = [["cpu", _("CPU")], ["memory", _("Memory")], ["disks", _("Disk I/O")], ["network", _("Network")]];
+
+ this.state = {
+ hours: [], // available hours for rendering in descending order
+ loading: true, // show loading indicator
+ metricsAvailable: true,
+ pmLoggerState: null,
+ error: null,
+ isDatepickerOpened: false,
+ selectedDate: null,
+ packagekitExists: false,
+ isBeibootBridge: false,
+ selectedVisibility: this.columns.reduce((a, v) => ({ ...a, [v[0]]: true }), {})
+ };
+
+ this.handleMoreData = this.handleMoreData.bind(this);
+ this.handleToggle = this.handleToggle.bind(this);
+ this.handleSelect = this.handleSelect.bind(this);
+ this.handleInstall = this.handleInstall.bind(this);
+
+ /* supervise pmlogger.service, to diagnose missing history */
+ this.pmlogger_service = service.proxy("pmlogger.service");
+ this.pmlogger_service.addEventListener("changed", () => {
+ if (!invalidService(this.pmlogger_service) && this.pmlogger_service.state !== this.state.pmLoggerState) {
+ // when it got enabled while the page is running (e.g. through Settings dialog), start data collection
+ if (!this.state.metricsAvailable && runningService(this.pmlogger_service))
+ this.initialLoadData();
+ this.setState({ pmLoggerState: this.pmlogger_service.state });
+ }
+ });
+
+ // FIXME: load less up-front, load more when scrolling
+ machine_info_promise.then(() => this.initialLoadData());
+
+ cockpit.addEventListener("visibilitychange", () => {
+ // update history metrics when in auto-update mode
+ if (!cockpit.hidden && this.history_refresh_timer)
+ this.load_data(this.most_recent);
+ });
+ }
+
+ // load and render the last 24 hours (plus current one) initially; this needs numCpu initialized for correct scaling
+ initialLoadData() {
+ cockpit.spawn(["date", "+%s"])
+ .then(out => {
+ const now = parseInt(out.trim()) * 1000;
+ const current_hour = Math.floor(now / MSEC_PER_H) * MSEC_PER_H;
+ this.most_recent = current_hour;
+ this.today_midnight = new Date(current_hour).setHours(0, 0, 0, 0);
+
+ const selectedDate = parseInt(cockpit.location.options.date) || this.today_midnight;
+
+ if (selectedDate !== this.today_midnight)
+ this.load_data(selectedDate, 24 * SAMPLES_PER_H, true);
+ else
+ this.load_data(current_hour - LOAD_HOURS * MSEC_PER_H, undefined, true);
+
+ this.setState({
+ metricsAvailable: true,
+ selectedDate,
+ });
+ })
+ .catch(ex => this.setState({ error: ex.toString() }));
+ }
+
+ async componentDidMount() {
+ const packagekitExists = await packagekit.detect();
+ // HACK: See https://github.com/cockpit-project/cockpit/issues/19143
+ let cmdline = "";
+ try {
+ cmdline = await cockpit.file("/proc/self/cmdline").read();
+ } catch (_ex) {}
+
+ const isBeibootBridge = cmdline?.includes("ic# cockpit-bridge");
+
+ this.setState({ packagekitExists, isBeibootBridge });
+ }
+
+ handleMoreData() {
+ this.load_data(this.oldest_timestamp - (LOAD_HOURS * MSEC_PER_H), LOAD_HOURS * SAMPLES_PER_H, true);
+ }
+
+ handleToggle(_, isOpen) {
+ this.setState({ isDatepickerOpened: isOpen });
+ }
+
+ handleSelect(e, sel) {
+ // Stop fetching of new data
+ if (this.history_refresh_timer !== null) {
+ window.clearTimeout(this.history_refresh_timer);
+ this.history_refresh_timer = null;
+ }
+
+ this.oldest_timestamp = 0;
+
+ cockpit.location.go([], Object.assign(cockpit.location.options, { date: sel }));
+ this.setState({
+ selectedDate: sel,
+ isDatepickerOpened: false,
+ hours: [],
+ }, () => this.load_data(sel, sel === this.today_midnight ? undefined : 24 * SAMPLES_PER_H, true));
+ }
+
+ handleInstall() {
+ install_dialog("cockpit-pcp")
+ .then(() => this.props.setNeedsLogout(true))
+ .catch(() => null); // ignore cancel
+ }
+
+ load_data(load_timestamp, limit, show_spinner) {
+ if (show_spinner)
+ this.setState({ loading: true });
+
+ this.oldest_timestamp = this.oldest_timestamp > load_timestamp || this.oldest_timestamp === 0 ? load_timestamp : this.oldest_timestamp;
+ let current_hour; // hour of timestamp, from most recent meta message
+ let hour_index; // index within data[current_hour] array
+ const current_sample = []; // last valid value, for decompression
+ const new_hours = new Set(); // newly seen hours during this load
+ this.history_refresh_timer = null;
+
+ const metrics = cockpit.channel({
+ payload: "metrics1",
+ interval: INTERVAL,
+ source: "pcp-archive",
+ timestamp: load_timestamp,
+ limit,
+ metrics: HISTORY_METRICS,
+ });
+
+ metrics.addEventListener("message", (event, message) => {
+ debug("history metrics message", message);
+ message = JSON.parse(message);
+
+ const init_current_hour = () => {
+ if (!this.data[current_hour])
+ this.data[current_hour] = [];
+
+ // When limit is considered only add hours in this time range
+ if (!limit || load_timestamp + (limit * INTERVAL) >= current_hour)
+ new_hours.add(current_hour);
+ };
+
+ // meta message
+ if (!Array.isArray(message)) {
+ current_hour = Math.floor(message.timestamp / MSEC_PER_H) * MSEC_PER_H;
+ init_current_hour();
+ hour_index = Math.floor((message.timestamp - current_hour) / INTERVAL);
+ console.assert(hour_index < SAMPLES_PER_H);
+
+ debug("message is metadata; time stamp", message.timestamp, "=", timeformat.dateTime(message.timestamp), "for current_hour", current_hour, "=", timeformat.dateTime(current_hour), "hour_index", hour_index);
+ return;
+ }
+
+ debug("message is", message.length, "samples data for current hour", current_hour, "=", timeformat.dateTime(current_hour));
+
+ message.forEach((samples, i) => {
+ decompress_samples(samples, current_sample);
+
+ /* don't overwrite existing data with null data; this often happens at the first
+ * data point when "rate" metrics cannot be calculated yet */
+ if (typeof current_sample[0] !== 'number' && this.data[current_hour][hour_index]) {
+ debug("load_data", load_timestamp, ": ignoring sample #", i, ":", JSON.stringify(current_sample), "current data sample", JSON.stringify(this.data[current_hour][hour_index]));
+ return;
+ }
+
+ // TODO: eventually track/display this by-interface?
+ const use_network = current_sample[8].reduce((acc, cur) => acc + cur, 0);
+ const sat_cpu = typeof current_sample[3][1] === 'number' ? current_sample[3][1] : null; // instances: (15min, 1min, 5min), pick 1min
+
+ this.data[current_hour][hour_index] = {
+ use_cpu: typeof current_sample[2] === 'number' ? [current_sample[0], current_sample[1], current_sample[2]] : null,
+ sat_cpu,
+ use_memory: typeof current_sample[5] === 'number' ? [current_sample[4], current_sample[5]] : null,
+ sat_memory: current_sample[6],
+ use_disks: current_sample[7],
+ use_network,
+ };
+
+ // keep track of maximums of unbounded values, for dynamic scaling
+ if (sat_cpu > scaleSatCPU)
+ scaleSatCPU = scaleForValue(sat_cpu);
+ if (current_sample[7] > scaleUseDisks)
+ scaleUseDisks = scaleForValue(current_sample[7]);
+ if (use_network > scaleUseNetwork)
+ scaleUseNetwork = scaleForValue(use_network);
+
+ if (++hour_index === SAMPLES_PER_H) {
+ current_hour += MSEC_PER_H;
+ hour_index = 0;
+ init_current_hour();
+ debug("hour overflow, advancing to", current_hour, "=", timeformat.dateTime(current_hour));
+ }
+ });
+
+ // update most recent sample timestamp
+ this.most_recent = Math.max(this.most_recent, current_hour + (hour_index - 5) * INTERVAL);
+ debug("most recent timestamp is now", this.most_recent, "=", timeformat.dateTime(this.most_recent));
+ });
+
+ metrics.addEventListener("close", (event, message) => {
+ if (message.problem) {
+ debug("could not load metrics:", message.problem);
+ this.setState({
+ loading: false,
+ metricsAvailable: false,
+ });
+ } else {
+ debug("loaded metrics for timestamp", timeformat.dateTime(load_timestamp), "new hours", JSON.stringify(Array.from(new_hours)));
+ new_hours.forEach(hour => debug("hour", hour, "data", JSON.stringify(this.data[hour])));
+
+ const hours = Array.from(new Set([...this.state.hours, ...new_hours]));
+ // sort in descending order
+ hours.sort((a, b) => b - a);
+ // re-render
+ this.setState({ hours, loading: false });
+
+ // trigger automatic update every minute when visible
+ if (!limit) {
+ this.history_refresh_timer = window.setTimeout(() => {
+ if (!cockpit.hidden)
+ this.load_data(this.most_recent);
+ }, 60000);
+ }
+ }
+
+ metrics.close();
+ });
+ }
+
+ render() {
+ if (this.props.needsLogout)
+ return <EmptyStatePanel
+ icon={ExclamationCircleIcon}
+ title={_("You need to relogin to be able to see metrics history")}
+ action={<Button onClick={() => cockpit.logout(true)}>{_("Log out")}</Button>}
+ />;
+
+ // on a single machine, cockpit-pcp depends on pcp; but this may not be the case in the beiboot scenario,
+ // so additionally check if pcp is available on the logged in target machine
+ if ((cockpit.manifests && !cockpit.manifests.pcp) || this.pmlogger_service.exists === false)
+ return <EmptyStatePanel
+ icon={ExclamationCircleIcon}
+ title={_("Package cockpit-pcp is missing for metrics history")}
+ action={this.state.isBeibootBridge === true
+ // See https://github.com/cockpit-project/cockpit/issues/19143
+ ? <Text>{ _("Installation not supported without installed cockpit package") }</Text>
+ : this.state.packagekitExists && <Button onClick={this.handleInstall}>{_("Install cockpit-pcp")}</Button>}
+ />;
+
+ if (!this.state.metricsAvailable) {
+ let action;
+ let paragraph;
+
+ if (this.pmlogger_service.state === 'stopped') {
+ paragraph = _("pmlogger.service is not running");
+ action = <PCPConfig buttonVariant="primary"
+ firewalldRequest={this.props.firewalldRequest}
+ needsLogout={this.props.needsLogout}
+ setNeedsLogout={this.props.setNeedsLogout} />;
+ } else {
+ if (this.pmlogger_service.state === 'failed')
+ paragraph = _("pmlogger.service has failed");
+ else /* running, or initialization hangs */
+ paragraph = _("pmlogger.service is failing to collect data");
+ action = <Button variant="link" onClick={() => cockpit.jump("/system/services#/pmlogger.service") }>{_("Troubleshoot")}</Button>;
+ }
+
+ return <EmptyStatePanel
+ icon={ExclamationCircleIcon}
+ title={_("Metrics history could not be loaded")}
+ paragraph={paragraph}
+ action={action}
+ />;
+ }
+
+ if (this.state.error)
+ return <EmptyStatePanel
+ icon={ExclamationCircleIcon}
+ title={_("Error has occurred")}
+ paragraph={this.state.error}
+ />;
+
+ let nodata_alert = null;
+ const lastHourIndex = this.state.hours.length - 1;
+ if (!this.state.loading && this.state.hours.length > 0 && this.oldest_timestamp < this.state.hours[lastHourIndex]) {
+ let t1, t2;
+ if (this.state.hours[lastHourIndex] - this.oldest_timestamp < 24 * MSEC_PER_H) {
+ t1 = timeformat.time(this.oldest_timestamp);
+ t2 = timeformat.time(this.state.hours[lastHourIndex]);
+ } else {
+ t1 = timeformat.dateTime(this.oldest_timestamp);
+ t2 = timeformat.dateTime(this.state.hours[lastHourIndex]);
+ }
+ nodata_alert = <Alert className="nodata" variant="info" isInline title={ cockpit.format(_("No data available between $0 and $1"), t1, t2) } />;
+ }
+
+ if (!this.state.loading && this.state.hours.length === 0)
+ nodata_alert = <EmptyStatePanel icon={ExclamationCircleIcon} title={_("No data available")} />;
+
+ // generate selection of last 14 days
+ const options = Array(15).fill()
+ .map((_undef, i) => {
+ const date = this.today_midnight - i * 86400000;
+ const text = i == 0 ? _("Today") : timeformat.weekdayDate(date);
+ return <SelectOption key={date} value={date}>{text}</SelectOption>;
+ });
+
+ function Label(props) {
+ return (
+ <div className={"metrics-label metrics-label-graph" + (props.items.length > 1 ? " have-saturation" : "")}>
+ <span>{props.label}</span>
+ <TextContent className="metrics-sublabels">
+ { props.items.map(i => <Text component={TextVariants.small} key={i}>{i}</Text>) }
+ </TextContent>
+ </div>
+ );
+ }
+
+ const columnVisibilityMenuItems = this.columns.map(itm => {
+ return (
+ <SelectOption
+ key={itm[0]}
+ value={itm[1]}
+ inputId={'column-visibility-option-' + itm[0]} />
+ );
+ });
+ const selections = (
+ this.columns
+ .filter(itm => this.state.selectedVisibility[itm[0]])
+ .map(itm => itm[1])
+ );
+
+ return (
+ <div className="metrics" style={{ "--graph-cnt": selections.length }}>
+ <PageGroup stickyOnBreakpoint={{ default: 'top' }}>
+ <section className="metrics-heading">
+ <Flex className="metrics-selectors" spaceItems={{ default: 'spaceItemsSm' }}>
+ <Select
+ className="select-min metrics-label"
+ aria-label={_("Jump to")}
+ onToggle={this.handleToggle}
+ onSelect={this.handleSelect}
+ isOpen={this.state.isDatepickerOpened}
+ selections={this.state.selectedDate}
+ toggleId="date-picker-select-toggle"
+ >
+ {options}
+ </Select>
+ <Select
+ toggleAriaLabel={_("Graph visibility options menu")}
+ className="select-min metrics-label"
+ variant="checkbox"
+ isCheckboxSelectionBadgeHidden
+ isOpen={!!this.state.isOpenColumnVisibility}
+ onSelect={(_, selection) => {
+ const s = this.columns.find(itm => itm[1] == selection);
+ this.setState(prevState => ({
+ selectedVisibility: { ...prevState.selectedVisibility, [s[0]]: !prevState.selectedVisibility[s[0]] }
+ }));
+ }}
+ onToggle={() => this.setState({ isOpenColumnVisibility: !this.state.isOpenColumnVisibility })}
+ placeholderText={_("Graph visibility")}
+ selections={selections}>
+ {columnVisibilityMenuItems}
+ </Select>
+ </Flex>
+ <Stack className="metrics-label-graph-mobile">
+ {[["cpu", _("CPU usage/load")], ["memory", _("Memory usage/swap")], ["disks", _("Disk I/O")], ["network", _("Network")]]
+ .filter(itm => this.state.selectedVisibility[itm[0]])
+ .map(itm => (
+ <Flex key={itm[0]} flexWrap={{ default: 'nowrap' }} spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsBaseline' }}>
+ <div className={"square label-" + itm[0]} />
+ <FlexItem>{itm[1]}</FlexItem>
+ </Flex>
+ ))}
+ </Stack>
+ <div className="metrics-graphs metrics-heading-graphs">
+ {this.state.selectedVisibility.cpu && <Label label={_("CPU")} items={[_("Usage"), _("Load")]} />}
+ {this.state.selectedVisibility.memory && <Label label={_("Memory")} items={[_("Usage"), ...swapTotal ? [_("Swap")] : []]} />}
+ {this.state.selectedVisibility.disks && <Label label={_("Disk I/O")} items={[_("Usage")]} />}
+ {this.state.selectedVisibility.network && <Label label={_("Network")} items={[_("Usage")]} />}
+ </div>
+ </section>
+ </PageGroup>
+ <PageSection className="metrics-history-section" variant={PageSectionVariants.light}>
+ <>
+ { this.state.hours.length > 0 &&
+ <Card isPlain>
+ <CardBody className="metrics-history">
+ { this.state.hours.map((time, i) => {
+ const showHeader = i == 0 || timeformat.date(time) != timeformat.date(this.state.hours[i - 1]);
+
+ return (
+ <React.Fragment key={timeformat.dateTime(time)}>
+ {showHeader && <TextContent><Text component={TextVariants.h3} className="metrics-time"><time>{ timeformat.date(time) }</time></Text></TextContent>}
+ <MetricsHour key={time} startTime={parseInt(time)}
+ selectedVisibility={this.state.selectedVisibility}
+ data={this.data[time]} clipLeading={i == 0} />
+ </React.Fragment>
+ );
+ })}
+ </CardBody>
+ </Card> }
+ {nodata_alert}
+ <div className="bottom-panel">
+ { this.state.loading
+ ? <EmptyStatePanel loading title={_("Loading...")} />
+ : <Button onClick={this.handleMoreData}>{_("Load earlier data")}</Button> }
+ </div>
+ </>
+ </PageSection>
+ </div>
+ );
+ }
+}
+
+export const Application = () => {
+ const [firewalldRequest, setFirewalldRequest] = useState(null);
+ const [needsLogout, setNeedsLogout] = useState(false);
+
+ return (
+ <WithDialogs>
+ <Page additionalGroupedContent={
+ <PageSection id="metrics-header-section" variant={PageSectionVariants.light} type='breadcrumb'>
+ <Flex>
+ <FlexItem>
+ <Breadcrumb>
+ <BreadcrumbItem onClick={() => cockpit.jump("/system")} className="pf-v5-c-breadcrumb__link">{_("Overview")}</BreadcrumbItem>
+ <BreadcrumbItem isActive>{_("Metrics and history")}</BreadcrumbItem>
+ </Breadcrumb>
+ </FlexItem>
+ <FlexItem align={{ default: 'alignRight' }}>
+ <PCPConfig buttonVariant="secondary"
+ firewalldRequest={setFirewalldRequest}
+ needsLogout={needsLogout}
+ setNeedsLogout={setNeedsLogout} />
+ </FlexItem>
+ </Flex>
+ </PageSection>
+ }>
+ { firewalldRequest &&
+ <FirewalldRequest service={firewalldRequest.service} title={firewalldRequest.title} pageSection /> }
+ <PageSection>
+ <CurrentMetrics />
+ </PageSection>
+ <MetricsHistory firewalldRequest={setFirewalldRequest}
+ needsLogout={needsLogout}
+ setNeedsLogout={setNeedsLogout} />
+ </Page>
+ </WithDialogs>);
+};
diff --git a/pkg/metrics/metrics.scss b/pkg/metrics/metrics.scss
new file mode 100644
index 0000000..8276b1a
--- /dev/null
+++ b/pkg/metrics/metrics.scss
@@ -0,0 +1,852 @@
+@use "page";
+@use "ct-card";
+@import "global-variables";
+@import "@patternfly/patternfly/utilities/Text/text.scss";
+
+// utilities for `pf-u...` classes
+@import "@patternfly/patternfly/utilities/Spacing/spacing.css";
+
+// colors for PF charts
+@import "@patternfly/patternfly/patternfly-charts.css";
+@import "@patternfly/patternfly/patternfly-charts-theme-dark.css";
+
+#app {
+ section.pf-v5-c-page__main-breadcrumb {
+ padding-block-end: var(--pf-v5-c-page__main-breadcrumb--PaddingTop);
+ }
+}
+
+html {
+ --metrics-heading-time-color: var(--pf-v5-global--BackgroundColor--light-100);
+ --resource-icon-color-empty: var(--pf-v5-global--palette--black-400);
+ --resource-icon-color-middle: var(--pf-v5-global--palette--orange-200);
+ --resource-icon-color-full: var(--pf-v5-global--palette--orange-400);
+}
+
+.pf-v5-theme-dark {
+ --metrics-heading-time-color: var(--pf-v5-global--BackgroundColor--300);
+ --metrics-events-border-color: #000;
+ --resource-icon-color-empty: var(--pf-v5-global--palette--black-400);
+ --resource-icon-color-middle: var(--pf-v5-global--palette--orange-500);
+ --resource-icon-color-full: var(--pf-v5-global--palette--orange-200);
+
+ // Adjust alert color to seperate itself from the history section background
+ .pf-v5-c-alert {
+ --pf-v5-c-alert--m-inline--m-info--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-400);
+ }
+}
+
+// Remove doubled-up padding on "main" sections, when two+ sections are used.
+.pf-v5-c-page__main-section + .pf-v5-c-page__main-section {
+ padding-block-start: 0;
+}
+
+// FIXME: More styles from ct-system-overview
+.current-metrics {
+ --card-width: 200px;
+ --pf-v5-l-gallery--GridTemplateColumns: repeat(auto-fit, minmax(var(--card-width), 1fr));
+
+ // the cards already have padding, align the "top 5" tables with the other content
+ .pf-v5-c-table {
+ --pf-v5-c-table--m-compact--cell--first-last-child--PaddingLeft: 0;
+ --pf-v5-c-table--m-compact--cell--first-last-child--PaddingRight: 0;
+
+ tr > * {
+ --pf-v5-c-table--cell--PaddingTop: var(--pf-v5-global--spacer--xs);
+ --pf-v5-c-table--cell--PaddingBottom: var(--pf-v5-global--spacer--xs);
+ }
+ }
+
+ .pf-v5-c-card {
+ // Make sure the cards do not get taller than 50% of the screen
+ // ...and when they do, they should scroll
+ @media screen and (orientation: landscape) {
+ max-block-size: 50vh;
+ overflow: auto;
+ }
+
+ &__title-text {
+ font-size: var(--pf-v5-global--FontSize--xl);
+ font-weight: var(--pf-v5-global--FontWeight--normal);
+ }
+
+ // Let the children get flexy
+ &__body {
+ display: flex;
+ flex-direction: column;
+
+ // Strip the padding from the first item,
+ // including table headings
+ > :first-child,
+ > :first-child > thead > tr > th {
+ padding-block-start: 0;
+ }
+
+ .pf-v5-c-table thead th:not(:first-child),
+ .pf-v5-c-table tbody td:not(:first-child) {
+ text-align: end;
+ }
+ }
+
+ .align-baseline {
+ align-items: baseline;
+ }
+
+ .text-color-warning {
+ color: var(--pf-v5-global--warning-color--200);
+ }
+
+ .icon-color-warning {
+ color: var(--pf-v5-global--warning-color--100);
+ }
+
+ .text-color-critical {
+ color: var(--pf-v5-global--danger-color--200);
+ }
+
+ .icon-color-critical {
+ color: var(--pf-v5-global--danger-color--100);
+ }
+
+ .progress-stack {
+ display: grid;
+ padding-block-end: var(--pf-v5-global--spacer--sm);
+ grid-gap: var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--md);
+
+ &:not(:first-child) {
+ padding-block-start: var(--pf-v5-global--spacer--sm);
+ }
+ }
+
+ .progress-stack-no-space {
+ display: grid;
+ padding-block-end: var(--pf-v5-global--spacer--sm);
+ grid-gap: 0;
+
+ &:not(:first-child) {
+ padding-block-start: var(--pf-v5-global--spacer--sm);
+ }
+
+ // avoid the "x" icon on high CPU usage
+ .pf-v5-c-progress__status-icon {
+ display: none;
+ }
+
+ // don't center the "View all CPUs" button
+ button {
+ padding-inline-start: 0;
+ inline-size: fit-content;
+ }
+ }
+
+ // Stretch the progress stack and description list so they fill up remaining space.
+ // This pushes the (unflexing) tables to the bottom.
+ .progress-stack,
+ .pf-v5-c-description-list {
+ flex: 1 1 0;
+ align-content: start;
+ }
+
+ // When progress stack and description list are used sequentially,
+ // give the most space to the latter of the two elements.
+ .progress-stack + .pf-v5-c-description-list,
+ .pf-v5-c-description-list + .progress-stack {
+ flex-basis: 100%;
+ }
+
+ // WebKit is a little wonky with flex sizing; it needs specific instructions here
+ .pf-v5-c-description-list:first-child {
+ flex-grow: 0;
+ }
+
+ .pf-v5-c-description-list__description {
+ font-size: var(--pf-v5-global--FontSize--sm);
+ }
+ }
+
+ // Shrink progress bars and their gap a little
+ .pf-v5-c-progress {
+ grid-gap: 0;
+ --pf-v5-c-progress__bar--Height: var(--pf-v5-global--spacer--xs);
+ // PF4 uses end; it probably should arguably use last baseline.
+ // (perhaps end may work better for non-text content?)
+ align-items: last baseline;
+
+ + .pf-v5-c-progress {
+ // Add a tiny gap for sandwiched usage bars (as in CPU)
+ padding-block-start: 1px;
+ }
+ }
+
+ // Only underline the description
+ .pf-v5-c-button.pf-m-link.pf-m-inline:hover {
+ text-decoration: none;
+
+ .pf-v5-c-progress__description {
+ text-decoration: var(--pf-v5-c-button--m-link--m-inline--hover--TextDecoration);
+ }
+ }
+
+ // Match font weight with description list terms
+ .pf-v5-c-progress__description {
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+ }
+
+ // Recolor text
+ .pf-m-link {
+ &, &:hover {
+ .pf-v5-c-progress__measure {
+ color: var(--pf-v5-global--Color--100);
+ }
+ }
+ }
+
+ // default is way too wide for the cards
+ .pf-v5-c-description-list {
+ --pf-v5-c-description-list--m-horizontal__term--width: minmax(0, auto);
+ --pf-v5-c-description-list--m-horizontal__description--width: minmax(0, auto);
+ }
+
+ // Don't wrap network in / out data
+ // Shrink network cards and data a little more
+ // (as space is costly and they use a lot)
+ // FIXME: Let's fix the content (split off units) and drop this when we can
+ .network-nowrap-shrink {
+ td {
+ white-space: nowrap;
+ font-size: var(--pf-v5-global--FontSize--xs);
+ }
+ }
+
+ // Shrink percentage column to allow for more service text
+ th[data-label="%"],
+ td[data-label="%"] {
+ .pf-v5-c-table__text {
+ --pf-v5-c-table--cell--MinWidth: 0;
+ display: inline;
+ }
+ inline-size: 2rem;
+ min-inline-size: 0;
+ }
+
+ .pf-m-grid-lg.pf-v5-c-table {
+ tbody:first-of-type {
+ border-block-start: none;
+ }
+
+ tbody tr:not(.pf-v5-c-table__expandable-row) {
+ --pf-v5-c-table-tr--responsive--PaddingTop: var(--pf-v5-global--spacer--sm);
+ --pf-v5-c-table-tr--responsive--PaddingRight: 0;
+ --pf-v5-c-table-tr--responsive--PaddingBottom: var(--pf-v5-global--spacer--sm);
+ --pf-v5-c-table-tr--responsive--PaddingLeft: 0;
+ --pf-v5-c-table-tr--responsive--border-width--base: 1px;
+
+ &:last-child {
+ border-block-end: none;
+ }
+
+ > * {
+ --pf-v5-c-table--cell--responsive--PaddingTop: 0;
+ --pf-v5-c-table--cell--responsive--PaddingBottom: 0;
+ }
+ }
+ }
+
+ .all-disks-no-gap {
+ // don't center the "View all disks" button
+ button {
+ padding-inline-start: 0;
+ inline-size: fit-content;
+ }
+ }
+}
+
+.pf-v5-c-popover {
+ thead th:not(:first-child) {
+ text-align: end;
+ }
+
+ .disks-nowrap {
+ td {
+ white-space: nowrap;
+ }
+
+ td:not(:first-child) {
+ text-align: end;
+ }
+ }
+}
+
+@media (min-width: 641px) and (max-width: 1200px) {
+ .current-metrics {
+ --card-width: 200px;
+ }
+
+ .current-metrics .pf-v5-c-card__body {
+ overflow-block: auto;
+ max-block-size: 50vw;
+ }
+
+ // Trim down the gutter space between cards
+ .current-metrics.pf-v5-l-gallery.pf-m-gutter {
+ grid-gap: var(--pf-v5-global--spacer--sm);
+ }
+
+ // Shave off a few pixels from cards
+ .pf-v5-c-card__header, .pf-v5-c-card__title, .pf-v5-c-card__body, .pf-v5-c-card__footer {
+ --pf-v5-c-card--first-child--PaddingTop: var(--pf-v5-global--spacer--sm);
+ --pf-v5-c-card__title--not--last-child--PaddingBottom: var(--pf-v5-global--spacer--sm);
+ --pf-v5-c-card--child--PaddingTop: var(--pf-v5-global--spacer--sm);
+ --pf-v5-c-card--child--PaddingRight: var(--pf-v5-global--spacer--md);
+ --pf-v5-c-card--child--PaddingBottom: var(--pf-v5-global--spacer--sm);
+ --pf-v5-c-card--child--PaddingLeft: var(--pf-v5-global--spacer--md);
+ }
+}
+
+// Change progress layout in medium sizes
+@media (min-width: 460px) and (max-width: 1200px) {
+ // Stack description lists (instead of grid)
+ .pf-v5-c-description-list__group {
+ display: block;
+ }
+}
+
+// Stretch network card and reflow items
+@media (min-width: 660px) and (max-width: 860px) {
+ .current-metrics-network {
+ grid-column: 1 / -1;
+
+ // Reflow interfaces horizontally, with wrapping
+ .pf-v5-c-table > tbody {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
+ grid-gap: 0 var(--pf-v5-global--spacer--lg);
+ }
+ }
+}
+
+$graphs: cpu, memory, disks, network;
+
+.metrics {
+ --cpu-color: var(--pf-v5-chart-color-purple-200);
+ --cpu-color-extreme: var(--pf-v5-chart-color-purple-300);
+
+ --memory-color: var(--pf-v5-chart-color-green-200);
+ --memory-color-extreme: var(--pf-v5-chart-color-green-300);
+
+ --disks-color: var(--pf-v5-chart-color-gold-300);
+ --disks-color-extreme: var(--pf-v5-chart-color-gold-400);
+
+ --network-color: var(--pf-v5-chart-color-black-300);
+ --network-color-extreme: var(--pf-v5-chart-color-black-400);
+
+ .pf-v5-theme-dark & {
+ --cpu-color: var(--pf-v5-chart-color-purple-500);
+ --cpu-color-extreme: var(--pf-v5-chart-color-purple-300);
+
+ --memory-color: var(--pf-v5-chart-color-green-500);
+ --memory-color-extreme: var(--pf-v5-chart-color-green-400);
+
+ --disks-color: var(--pf-v5-chart-color-gold-500);
+ --disks-color-extreme: var(--pf-v5-chart-color-gold-300);
+
+ --network-color: var(--pf-v5-chart-color-black-400);
+ --network-color-extreme: var(--pf-v5-chart-color-black-300);
+ }
+
+ --column-size: 10vw;
+ --half-column-size: 8vw;
+ --data-min-height: 5px;
+
+ --graph-column-size: 60%;
+
+ &-hour-compressed {
+ --data-min-height: 1px;
+ display: flex;
+ background-color: var(--pf-v5-global--BackgroundColor--light-300);
+ }
+
+ &-heading,
+ &-hour:not(.metrics-hour-compressed) .metrics-minute {
+ background-color: var(--pf-v5-global--BackgroundColor--light-300);
+ display: flex;
+ }
+
+ &-heading,
+ &-heading-graphs {
+ align-items: center;
+ }
+
+ &-graphs {
+ flex-basis: var(--graph-column-size);
+ margin-inline-start: auto;
+ display: grid;
+ grid-template-columns: repeat(var(--graph-cnt), minmax(var(--column-size), 1fr));
+
+ .metrics-data {
+ min-block-size: var(--data-min-height);
+ }
+ }
+
+ &-sublabels.pf-v5-c-content {
+ small {
+ font-size: var(--pf-v5-global--FontSize--xs);
+ margin-block-end: var(--pf-v5-global--spacer--xs) !important;
+ }
+ display: flex;
+ }
+
+ &-label-graph {
+ text-align: end;
+
+ & .metrics-sublabels {
+ justify-content: flex-end;
+ }
+ }
+
+ &-label-graph.have-saturation {
+ text-align: center;
+
+ & .metrics-sublabels {
+ justify-content: space-evenly;
+ }
+ }
+
+ &-minutes {
+ flex-basis: var(--graph-column-size);
+ align-self: end;
+ }
+
+ &-events-hour-header-expanded {
+ inline-size: calc(100% - var(--graph-column-size));
+ }
+
+ &-selectors,
+ &-events-wrapper {
+ flex-basis: calc(100% - var(--graph-column-size));
+ inline-size: 100%;
+ }
+
+ &-events {
+ display: flex;
+ column-gap: var(--pf-v5-global--spacer--sm);
+ flex-basis: calc(100% - var(--graph-column-size));
+ align-items: baseline;
+ font-size: var(--pf-v5-global--FontSize--sm);
+
+ &-expander {
+ min-inline-size: 3rem;
+ }
+
+ time:first-child {
+ margin-inline-start: calc(3rem + var(--pf-v5-global--spacer--sm));
+
+ + .spikes_count {
+ padding-inline-start: calc(var(--pf-v5-global--spacer--xl) + var(--pf-v5-global--spacer--md));
+ color: var(--pf-v5-global--Color--200);
+ }
+ }
+
+ .spikes_count {
+ > div {
+ white-space: nowrap;
+ align-items: baseline;
+ }
+
+ svg {
+ align-self: center;
+ }
+ }
+
+ > * {
+ line-height: var(--pf-v5-global--LineHeight--md);
+ }
+
+ > *:not(.metrics-events-expander):not(:last-child) {
+ flex: 1;
+ white-space: nowrap;
+ }
+
+ > *:last-child {
+ flex-basis: 50%;
+ }
+
+ // Just embolden the top level time events
+ button + time {
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+ }
+
+ // Make messages such as `Loading...` or `No logs found` centered
+ .cockpit-log-panel {
+ display: flex;
+ flex-wrap: wrap;
+ // justify-content: center;
+
+ .cockpit-logline {
+ flex: 0 0 100%;
+ }
+ }
+ }
+
+ &-time,
+ &-heading {
+ padding-inline: var(--pf-v5-c-page__main-section--PaddingLeft) var(--pf-v5-c-page__main-section--PaddingRight);
+ }
+
+ &-time {
+ background: var(--metrics-heading-time-color);
+ padding-block: var(--pf-v5-global--spacer--md) var(--pf-v5-global--spacer--sm);
+
+ // Time has tabular numbers, but it's not what we want for a heading
+ time {
+ font-variant: none !important;
+ }
+ }
+
+ &-history-section {
+ padding: 0;
+ }
+
+ &-history {
+ background-color: var(--pf-v5-global--BackgroundColor--light-300);
+ padding: 0;
+ }
+
+ &-data {
+ --gradient-fragment: var(--color) 80%, var(--color-extreme) 90%;
+ --gradient: linear-gradient(to right, var(--gradient-fragment));
+ --gradient-flip: linear-gradient(to left, var(--gradient-fragment));
+ display: grid;
+ contain: strict;
+ margin-block: 0;
+ margin-inline: var(--pf-v5-global--spacer--sm);
+ // Make SVG and generated before/after line position relative to metrics-data
+ position: relative;
+ grid-template-areas: "utilization";
+ // Make sure rows expand (WebKit needs this)
+ grid-template-rows: 1fr;
+
+ // vertical ruler line for areas with data, and horizontal tick mark for utilization half
+ &.valid-data::before {
+ content: "";
+ position: absolute;
+ z-index: 1;
+ inline-size: 100%;
+ inset-block: 0;
+ border-inline-end: 1px solid rgb(0 0 0 / 30%);
+ }
+
+ &.valid-data.have-saturation::before {
+ inline-size: 50%;
+ }
+
+ // horizontal tick mark for saturation half, for resources that have it
+ &.valid-data.have-saturation::after {
+ content: "";
+ position: absolute;
+ z-index: 1;
+ inline-size: 50%;
+ inset-inline-start: 50%;
+ }
+
+ .polygon {
+ // WebKit has an issue with normal positioning (height exceeds bounds), but absolute works fine
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ clip-path: polygon(var(--points));
+ background: var(--gradient);
+ block-size: 100%;
+ inline-size: 100%;
+ }
+
+ @each $graph in $graphs {
+ &-#{$graph} {
+ --color: var(--#{$graph}-color);
+ --color-extreme: var(--#{$graph}-color-extreme);
+ }
+ }
+
+ /* we always need to flip the polygons in Y direction, as the SVG
+ * coordinate system's Y goes downwards, and the computed SVGs are relative
+ * to (0,0), with a non-constant max-Y.
+ * Flip them in X direction for RTL. */
+
+ .polygon.polygon-use {
+ grid-area: utilization;
+ // horizontally the use graphs go to the left on LTR, to the right on RTL
+ transform: scale(-1, -1);
+ [dir="rtl"] & { transform: scale(1, -1); }
+ }
+
+ .polygon.polygon-sat {
+ grid-area: saturation;
+ opacity: 0.7;
+ // horizontally the load graphs go to the right on LTR, to the left on RTL
+ transform: scale(1, -1);
+ [dir="rtl"] & { transform: scale(-1, -1); }
+ }
+
+ .compressed.polygon-use {
+ background: var(--gradient-flip);
+ clip-path: inset(0 0 0 calc(100% - var(--utilization) * 100%));
+ [dir="rtl"] & { transform: scale(-1, 1); }
+ }
+
+ .compressed.polygon-sat {
+ background: var(--gradient);
+ clip-path: inset(0 calc(100% - var(--saturation) * 100%) 0 0);
+ opacity: 0.7;
+ [dir="rtl"] & { transform: scale(-1, 1); }
+ }
+ }
+
+ &-data.have-saturation {
+ grid-template-areas: "utilization saturation";
+ }
+
+ // vertical dotted line through graph center, for areas without data
+ &-data::after {
+ content: "";
+ z-index: -1;
+ inset-block-start: 0;
+ inset-block-end: 0;
+ position: absolute;
+ }
+
+ &-data.empty-data::after {
+ position: initial;
+ border-inline-end: 1px dotted rgb(0 0 0 / 30%);
+ }
+
+ &-data.empty-data.have-saturation::after {
+ position: absolute;
+ border-inline-start: 1px dotted rgb(0 0 0 / 30%);
+ border-inline-end: none;
+ inset-inline-start: 50%;
+ }
+
+ &-hour {
+ padding-block: 0;
+ padding-inline: var(--pf-v5-global--spacer--sm);
+ }
+
+ &-hour:not(.metrics-hour-compressed) .metrics-minute,
+ &-hour.metrics-hour-compressed .metrics-minute:last-of-type {
+ .valid-data.have-saturation::after,
+ .valid-data::before {
+ // Fake a border via clip-path, to set it on top, instead of after
+ background: rgb(120 120 120 / 30%);
+ pointer-events: none;
+ clip-path: inset(calc(100% - 1px) 0 0);
+ }
+ }
+
+ .pf-v5-c-card__body:first-child {
+ padding-block-start: 0;
+ }
+}
+
+// Graph column labels are the widest part; scale down the labels for narrow widths
+@media (min-width: 800px) and (max-width: 1200px) {
+ .metrics-label-graph {
+ font-size: var(--pf-v5-global--FontSize--sm);
+ }
+}
+
+@media (min-width: 800px) {
+ .metrics-label-graph-mobile {
+ display: none;
+ }
+}
+
+@media (max-width: 800px) {
+ .metrics {
+ --graph-column-size: 40%;
+
+ time:first-child {
+ + .spikes_count {
+ padding-inline-start: 0;
+ }
+ }
+ }
+
+ .metrics-heading > .metrics-graphs,
+ .metrics-minute > .metrics-events > .spikes_count {
+ display: none;
+ }
+
+ .metrics-label-graph-mobile {
+ padding-inline-start: var(--pf-v5-global--spacer--sm);
+ padding-block-start: var(--pf-v5-global--spacer--sm);
+ font-size: var(--pf-v5-global--FontSize--sm);
+ }
+
+ .square {
+ inline-size: 1rem;
+ block-size: 1rem;
+ }
+
+ @each $graph in $graphs {
+ .label-#{$graph} {
+ background-color: var(--#{$graph}-color);
+ }
+ }
+
+ .metrics-hour > .metrics-events {
+ .spikes_info,
+ .spikes_count svg {
+ display: none;
+ }
+ }
+}
+
+.bottom-panel {
+ display: flex;
+ justify-content: space-around;
+ padding: 1em;
+}
+
+.nodata {
+ margin: 1em;
+}
+
+.select-min {
+ inline-size: min-content;
+}
+
+#pcp-settings-modal {
+ .pcp-settings-modal-text {
+ // "Read more..." link
+ a {
+ padding-inline-start: 1em;
+ white-space: nowrap;
+ }
+ }
+
+ // leave some space for the focus rings
+ .pf-v5-c-switch {
+ margin-block-end: 10px;
+ }
+}
+
+.cpu-all {
+ --pf-v5-l-grid--m-gutter--GridGap: var(--pf-v5-global--spacer--xs) var(--pf-v5-global--spacer--md);
+ // Mixing font families need to be aligned properly (to the baseline)
+ align-items: baseline;
+ // In monospaced text, "100%" is 4 characters
+ grid-template-columns: 1fr minmax(4ch, auto);
+
+ > .pf-v5-l-grid__item {
+ // Don't use PF's hardcoded 12-column grid
+ grid-column: auto;
+ }
+
+ > dd {
+ // TODO: Use tabular numbers instead of monospace,
+ // after new font usage is enabled
+ font-family: var(--pf-v5-global--FontFamily--monospace);
+ // Aligning percentages to the right, especially when monospaced,
+ // makes it easier to see high percentages
+ text-align: end;
+ }
+}
+
+.current-cpu-usage:not(:last-child) {
+ // Bump CPU average above CPU max
+ z-index: 1;
+
+ // Make the background transparent, so CPU max shows through
+ .pf-v5-c-progress__bar {
+ --pf-v5-c-progress__bar--BackgroundColor: transparent;
+ --pf-v5-c-progress__bar--before--Opacity: 0;
+ }
+}
+
+.current-top-cpu-usage {
+ // Nudge max to be at the same place as average
+ margin-block-start: -5px;
+
+ // Lighten up the default bar
+ &:not(.pf-m-danger):not(.pf-m-warning) {
+ .pf-v5-c-progress__indicator {
+ opacity: 0.33;
+ }
+ }
+}
+
+// FIXME: We probably want to adapt some of these for the general log message widget
+.cockpit-logline {
+ padding-block: 0.25rem;
+ padding-inline: 0;
+}
+
+.cockpit-log-message {
+ text-align: start;
+ overflow-wrap: anywhere;
+ white-space: break-spaces;
+ // Clamping is supported in all browsers,
+ // but it needs special webkit prefix (yes, in all browsers)
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ display: webkit-box; /* stylelint-disable-line */
+ color: var(--pf-v5-global--active-color--100);
+}
+
+// Log warning icon is incorrectly aligned (especially when messages wrap);
+// this fixes it by realigning and offsetting
+.cockpit-log-warning {
+ transform: translateY(calc((var(--pf-v5-global--spacer--md) - 22px) / -2));
+ margin: 0;
+ margin-inline-end: var(--pf-v5-global--spacer--xs);
+ align-self: baseline;
+}
+
+.metrics-events-popover {
+ .cockpit-log-panel {
+ overflow: auto;
+ // Remove extra whitespace; the modal already has some
+ padding: 0;
+ margin: 0;
+ }
+
+ .pf-v5-c-popover__content {
+ // Remove extra whitespace in the modal
+ --pf-v5-c-popover--c-button--sibling--PaddingRight: 0;
+ }
+}
+
+@media (min-width: 801px) {
+ .metrics-events-popover {
+ .cockpit-log-panel {
+ // Limit the size of the popover to make it more elegant
+ max-block-size: 65vw;
+ }
+
+ .pf-v5-c-popover__content {
+ // Limit the width of the popover in desktop mode
+ max-inline-size: max(30rem, 40vw);
+ }
+ }
+}
+
+@media (max-width: 800px) {
+ .metrics-events-popover {
+ // Make the popover into a sheet from the bottom in mobile mode
+ inset-block: auto 0 !important;
+ inset-inline: 0 !important;
+ transform: none !important;
+ max-block-size: 100%;
+ min-block-size: 25vh;
+
+ .pf-v5-c-popover__content {
+ // Limit the width of the popover in desktop mode
+ max-inline-size: max(100vw);
+ }
+
+ // As a sheet at the bottom, it doesn't point to anything
+ .pf-v5-c-popover__arrow {
+ display: none;
+ }
+ }
+}
diff --git a/pkg/networkmanager/bond.jsx b/pkg/networkmanager/bond.jsx
new file mode 100644
index 0000000..bdbf11c
--- /dev/null
+++ b/pkg/networkmanager/bond.jsx
@@ -0,0 +1,231 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useContext } from 'react';
+import cockpit from 'cockpit';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { ExternalLinkSquareAltIcon, HelpIcon } from '@patternfly/react-icons';
+
+import { MacMenu, MemberInterfaceChoices, NetworkModal, Name, dialogSave } from './dialogs-common.jsx';
+import { ModelContext } from './model-context.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+import { v4 as uuidv4 } from 'uuid';
+import {
+ member_connection_for_interface,
+ member_interface_choices,
+} from './interfaces.js';
+
+const _ = cockpit.gettext;
+
+export const bond_mode_choices =
+ [
+ { choice: 'balance-rr', title: _("Round robin") },
+ { choice: 'active-backup', title: _("Active backup") },
+ { choice: 'balance-xor', title: _("XOR") },
+ { choice: 'broadcast', title: _("Broadcast") },
+ { choice: '802.3ad', title: _("802.3ad") },
+ { choice: 'balance-tlb', title: _("Adaptive transmit load balancing") },
+ { choice: 'balance-alb', title: _("Adaptive load balancing") }
+ ];
+
+const bond_monitoring_choices =
+ [
+ { choice: 'mii', title: _("MII (recommended)") },
+ { choice: 'arp', title: _("ARP") }
+ ];
+
+export const BondDialog = ({ connection, dev, settings }) => {
+ const Dialogs = useDialogs();
+ const idPrefix = "network-bond-settings";
+ const model = useContext(ModelContext);
+ const options = settings.bond.options;
+
+ const memberChoicesInit = {};
+
+ member_interface_choices(model, connection).forEach((iface) => {
+ memberChoicesInit[iface.Name] = !!member_connection_for_interface(connection, iface);
+ });
+
+ const [dialogError, setDialogError] = useState(undefined);
+ const [iface, setIface] = useState(settings.connection.interface_name);
+ const [linkDownDelay, setLinkDownDelay] = useState(options.downdelay || "0");
+ const [linkMonitoring, setLinkMonitoring] = useState(options.arp_interval ? "arp" : "mii");
+ const [linkMonitoringInterval, setLinkMonitoringInterval] = useState(options.miimon || options.arp_interval || "100");
+ const [linkUpDelay, setLinkUpDelay] = useState(options.updelay || "0");
+ const [mac, setMAC] = useState((settings.ethernet && settings.ethernet.assigned_mac_address) || "");
+ const [memberChoices, setMemberChoices] = useState(memberChoicesInit);
+ const [mode, setMode] = useState(options.mode);
+ const [monitoringTargets, setMonitoringTargets] = useState(options.arp_ip_target);
+ const [primary, setPrimary] = useState(undefined);
+
+ const onSubmit = (ev) => {
+ const createSettingsObj = () => ({
+ ...settings,
+ connection: {
+ ...settings.connection,
+ id: iface,
+ interface_name: iface,
+ },
+ ethernet: {
+ assigned_mac_address: mac
+ },
+ bond: {
+ ...settings.bond,
+ interface_name: iface,
+ options: {
+ ...settings.bond.options,
+ mode,
+ ...(linkMonitoring == 'mii' && {
+ miimon: linkMonitoringInterval,
+ updelay: linkUpDelay,
+ downdelay: linkDownDelay,
+ }),
+ ...(linkMonitoring === 'arp' && {
+ arp_interval: linkMonitoringInterval,
+ arp_ip_target: monitoringTargets,
+ }),
+ ...(mode == "active-backup" && { primary })
+ }
+ }
+ });
+
+ dialogSave({
+ model,
+ dev,
+ connection,
+ members: memberChoices,
+ membersInit: memberChoicesInit,
+ settings: createSettingsObj(),
+ setDialogError,
+ onClose: Dialogs.close,
+ });
+
+ // Prevent dialog from closing because of <form> onsubmit event
+ if (event)
+ event.preventDefault();
+
+ return false;
+ };
+
+ return (
+ <NetworkModal dialogError={dialogError}
+ idPrefix={idPrefix}
+ onSubmit={onSubmit}
+ title={!connection ? _("Add bond") : _("Edit bond settings")}
+ help={
+ <Popover
+ headerContent={_("Network bond")}
+ id="bond-help"
+ bodyContent={
+ <div>
+ {_("A network bond combines multiple network interfaces into one logical interface with higher throughput or redundancy.")}
+ </div>
+ }
+ footerContent={
+ <Button component='a'
+ rel="noopener noreferrer" target="_blank"
+ variant='link'
+ isInline
+ icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
+ href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-network-bonds-using-the-web-console_system-management-using-the-rhel-8-web-console">
+ {_("Learn more")}
+ </Button>
+ }
+ >
+ <Button id="bond-help-popup-button" variant="plain" aria-label="Help">
+ <HelpIcon />
+ </Button>
+ </Popover>
+ }
+ isCreateDialog={!connection}
+ >
+ <>
+ <Name idPrefix={idPrefix} iface={iface} setIface={setIface} />
+ <FormGroup label={_("Interfaces")} fieldId={idPrefix + "-interface-members-list"} hasNoPaddingTop>
+ <MemberInterfaceChoices idPrefix={idPrefix} memberChoices={memberChoices} setMemberChoices={setMemberChoices} model={model} group={connection} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-mac-input"} label={_("MAC")}>
+ <MacMenu idPrefix={idPrefix} model={model} mac={mac} setMAC={setMAC} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-mode-select"} label={_("Mode")}>
+ <FormSelect id={idPrefix + "-mode-select"} onChange={(_, val) => setMode(val)}
+ value={mode}>
+ {bond_mode_choices.map(choice => <FormSelectOption value={choice.choice} label={choice.title} key={choice.choice} />)}
+ </FormSelect>
+ </FormGroup>
+ {mode == "active-backup" && <FormGroup fieldId={idPrefix + "-primary-select"} label={_("Primary")}>
+ <FormSelect id={idPrefix + "-primary-select"} onChange={(_, val) => setPrimary(val)}
+ value={primary}>
+ <>
+ <FormSelectOption key='-' value={null} label='-' />
+ {Object.keys(memberChoices)
+ .filter(iface => memberChoices[iface])
+ .map(iface => <FormSelectOption key={iface} label={iface} value={iface} />)}
+ </>
+ </FormSelect>
+ </FormGroup>}
+ <FormGroup fieldId={idPrefix + "-link-monitoring-select"} label={_("Link monitoring")}>
+ <FormSelect id={idPrefix + "-link-monitoring-select"} onChange={(_, val) => setLinkMonitoring(val)}
+ value={linkMonitoring}>
+ {bond_monitoring_choices.map(choice => <FormSelectOption value={choice.choice} label={choice.title} key={choice.choice} />)}
+ </FormSelect>
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-link-monitoring-interval-input"} label={_("Monitoring interval")}>
+ <TextInput id={idPrefix + "-link-monitoring-interval-input"} className="network-number-field" value={linkMonitoringInterval} onChange={(_event, value) => setLinkMonitoringInterval(value)} />
+ </FormGroup>
+ {linkMonitoring == 'mii' && <>
+ <FormGroup fieldId={idPrefix + "-link-up-delay-input"} label={_("Link up delay")}>
+ <TextInput id={idPrefix + "-link-up-delay-input"} className="network-number-field" value={linkUpDelay} onChange={(_event, value) => setLinkUpDelay(value)} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-link-down-delay-input"} label={_("Link down delay")}>
+ <TextInput id={idPrefix + "-link-down-delay-input"} className="network-number-field" value={linkDownDelay} onChange={(_event, value) => setLinkDownDelay(value)} />
+ </FormGroup>
+ </>}
+ {linkMonitoring == 'arp' && <FormGroup fieldId={idPrefix + "-monitoring-targets-input"} label={_("Monitoring targets")}>
+ <TextInput id={idPrefix + "-monitoring-targets-input"} value={monitoringTargets} onChange={(_event, value) => setMonitoringTargets(value)} />
+ </FormGroup>}
+ </>
+ </NetworkModal>
+ );
+};
+
+export const getGhostSettings = ({ newIfaceName }) => {
+ return (
+ {
+ connection: {
+ id: newIfaceName,
+ autoconnect: true,
+ type: "bond",
+ uuid: uuidv4(),
+ interface_name: newIfaceName,
+ },
+ bond: {
+ options: {
+ mode: "active-backup"
+ },
+ interface_name: newIfaceName
+ }
+ }
+ );
+};
diff --git a/pkg/networkmanager/bridge.jsx b/pkg/networkmanager/bridge.jsx
new file mode 100644
index 0000000..9800585
--- /dev/null
+++ b/pkg/networkmanager/bridge.jsx
@@ -0,0 +1,147 @@
+/*
+
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useContext } from 'react';
+import cockpit from 'cockpit';
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+import { MemberInterfaceChoices, NetworkModal, Name, dialogSave } from './dialogs-common.jsx';
+import { ModelContext } from './model-context.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+import { v4 as uuidv4 } from 'uuid';
+import {
+ member_connection_for_interface,
+ member_interface_choices,
+} from './interfaces.js';
+
+const _ = cockpit.gettext;
+
+export const BridgeDialog = ({ connection, dev, settings }) => {
+ const Dialogs = useDialogs();
+ const idPrefix = "network-bridge-settings";
+ const model = useContext(ModelContext);
+ const memberChoicesInit = {};
+
+ member_interface_choices(model, connection).forEach((iface) => {
+ memberChoicesInit[iface.Name] = !!member_connection_for_interface(connection, iface);
+ });
+
+ const [dialogError, setDialogError] = useState(undefined);
+ const [iface, setIface] = useState(settings.connection.interface_name);
+ const [stp, setStp] = useState(!!settings.bridge.stp);
+ const [priority, setPriority] = useState(settings.bridge.priority || 10);
+ const [forwardDelay, setForwardDelay] = useState(settings.bridge.forward_delay || 10);
+ const [helloTime, setHelloTime] = useState(settings.bridge.hello_time || 10);
+ const [maxAge, setMaxAge] = useState(settings.bridge.max_age || 10);
+ const [memberChoices, setMemberChoices] = useState(memberChoicesInit);
+
+ const onSubmit = (ev) => {
+ const createSettingsObj = () => ({
+ ...settings,
+ connection: {
+ ...settings.connection,
+ id: iface,
+ interface_name: iface,
+ },
+ bridge: {
+ ...settings.bridge,
+ stp,
+ ...(stp && { priority, forward_delay: forwardDelay, hello_time: helloTime, max_age: maxAge }),
+ interface_name: iface,
+ }
+ });
+
+ dialogSave({
+ model,
+ dev,
+ connection,
+ members: memberChoices,
+ membersInit: memberChoicesInit,
+ settings: createSettingsObj(),
+ setDialogError,
+ onClose: Dialogs.close,
+ });
+
+ // Prevent dialog from closing because of <form> onsubmit event
+ if (event)
+ event.preventDefault();
+
+ return false;
+ };
+
+ return (
+ <NetworkModal dialogError={dialogError}
+ idPrefix="network-bridge-settings"
+ onSubmit={onSubmit}
+ title={!connection ? _("Add bridge") : _("Edit bridge settings")}
+ isCreateDialog={!connection}
+ >
+ <>
+ <Name idPrefix={idPrefix} iface={iface} setIface={setIface} />
+ <FormGroup label={_("Ports")} fieldId={idPrefix + "-interface-members-list"} hasNoPaddingTop>
+ <MemberInterfaceChoices idPrefix={idPrefix} memberChoices={memberChoices} setMemberChoices={setMemberChoices} model={model} group={connection} />
+ </FormGroup>
+ <FormGroup label={_("Options")} fieldId={idPrefix + "-stp-enabled-input"} hasNoPaddingTop>
+ <Checkbox id={idPrefix + "-stp-enabled-input"} isChecked={stp} onChange={(_, s) => setStp(s)} label={_("Spanning tree protocol (STP)")} />
+ {stp && <>
+ <FormGroup fieldId="network-bridge-stp-settings-priority-input" label={_("STP priority")}>
+ <TextInput id="network-bridge-stp-settings-priority-input" className="network-number-field" value={priority} onChange={(_event, value) => setPriority(value)} />
+ </FormGroup>
+ <FormGroup fieldId="network-bridge-stp-settings-forward-delay-input" label={_("STP forward delay")}>
+ <TextInput id="network-bridge-stp-settings-forward-delay-input" className="network-number-field" value={forwardDelay} onChange={(_event, value) => setForwardDelay(value)} />
+ </FormGroup>
+ <FormGroup fieldId="network-bridge-stp-settings-hello-time-input" label={_("STP hello time")}>
+ <TextInput id="network-bridge-stp-settings-hello-time-input" className="network-number-field" value={helloTime} onChange={(_event, value) => setHelloTime(value)} />
+ </FormGroup>
+ <FormGroup fieldId="network-bridge-stp-settings-max-age-input" label={_("STP maximum message age")}>
+ <TextInput id="network-bridge-stp-settings-max-age-input" className="network-number-field" value={maxAge} onChange={(_event, value) => setMaxAge(value)} />
+ </FormGroup>
+ </>}
+ </FormGroup>
+ </>
+ </NetworkModal>
+ );
+};
+
+export const getGhostSettings = ({ newIfaceName }) => {
+ return (
+ {
+ connection: {
+ id: newIfaceName,
+ autoconnect: true,
+ type: "bridge",
+ uuid: uuidv4(),
+ interface_name: newIfaceName
+ },
+ bridge: {
+ interface_name: newIfaceName,
+ stp: false,
+ priority: 32768,
+ forward_delay: 15,
+ hello_time: 2,
+ max_age: 20,
+ ageing_time: 300
+ }
+ }
+ );
+};
diff --git a/pkg/networkmanager/bridgeport.jsx b/pkg/networkmanager/bridgeport.jsx
new file mode 100644
index 0000000..1ca0dff
--- /dev/null
+++ b/pkg/networkmanager/bridgeport.jsx
@@ -0,0 +1,91 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useContext } from 'react';
+import cockpit from 'cockpit';
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+import { NetworkModal, dialogSave } from './dialogs-common.jsx';
+import { ModelContext } from './model-context.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+export const BridgePortDialog = ({ connection, dev, settings }) => {
+ const Dialogs = useDialogs();
+ const idPrefix = "network-bridge-port-settings";
+ const model = useContext(ModelContext);
+
+ let config = settings.bridge_port;
+
+ if (!config)
+ config = config = { };
+
+ const [priority, setPriority] = useState(config.priority);
+ const [hairPin, setHairPin] = useState(config.hairpin_mode);
+ const [pathCost, setPathCost] = useState(config.path_cost);
+ const [dialogError, setDialogError] = useState(undefined);
+
+ const onSubmit = (ev) => {
+ const createSettingsObj = () => ({
+ ...settings,
+ bridge_port: {
+ ...settings.bridge_port,
+ priority: parseInt(priority, 10),
+ path_cost: parseInt(pathCost, 10),
+ hairpin_mode: hairPin,
+ }
+ });
+
+ dialogSave({
+ model,
+ dev,
+ connection,
+ settings: createSettingsObj(),
+ setDialogError,
+ onClose: Dialogs.close,
+ });
+
+ // Prevent dialog from closing because of <form> onsubmit event
+ if (event)
+ event.preventDefault();
+
+ return false;
+ };
+
+ return (
+ <NetworkModal dialogError={dialogError}
+ idPrefix={idPrefix}
+ onSubmit={onSubmit}
+ title={_("Bridge port settings")}
+ >
+ <FormGroup fieldId={idPrefix + "-prio-input"} label={_("Priority")}>
+ <TextInput id={idPrefix + "-prio-input"} value={priority} onChange={(_event, value) => setPriority(value)} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-path-cost-input"} label={_("Path cost")}>
+ <TextInput id={idPrefix + "-path-cost-input"} value={pathCost} onChange={(_event, value) => setPathCost(value)} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-hairPin-mode-input"}>
+ <Checkbox id={idPrefix + "-hairPin-mode-input"} isChecked={hairPin} onChange={(_, hp) => setHairPin(hp)} label={_("Hair pin mode")} />
+ </FormGroup>
+ </NetworkModal>
+ );
+};
diff --git a/pkg/networkmanager/dialogs-common.jsx b/pkg/networkmanager/dialogs-common.jsx
new file mode 100644
index 0000000..7bb0f4a
--- /dev/null
+++ b/pkg/networkmanager/dialogs-common.jsx
@@ -0,0 +1,331 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useContext, useEffect, useState } from 'react';
+import cockpit from 'cockpit';
+import * as packagekit from 'packagekit.js';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+import { BondDialog, getGhostSettings as getBondGhostSettings } from './bond.jsx';
+import { BridgeDialog, getGhostSettings as getBridgeGhostSettings } from './bridge.jsx';
+import { BridgePortDialog } from './bridgeport.jsx';
+import { IpSettingsDialog } from './ip-settings.jsx';
+import { TeamDialog, getGhostSettings as getTeamGhostSettings } from './team.jsx';
+import { TeamPortDialog } from './teamport.jsx';
+import { VlanDialog, getGhostSettings as getVlanGhostSettings } from './vlan.jsx';
+import { WireGuardDialog, getWireGuardGhostSettings } from './wireguard.jsx';
+import { MtuDialog } from './mtu.jsx';
+import { MacDialog } from './mac.jsx';
+import { ModalError } from 'cockpit-components-inline-notification.jsx';
+import { ModelContext } from './model-context.jsx';
+import { useDialogs } from "dialogs.jsx";
+import { install_dialog } from "cockpit-components-install-dialog.jsx";
+import { read_os_release } from "os-release.js";
+
+import {
+ apply_group_member,
+ syn_click,
+ with_checkpoint, with_settings_checkpoint,
+ connection_devices,
+ settings_applier,
+ show_unexpected_error,
+} from './interfaces.js';
+
+const _ = cockpit.gettext;
+// nm-dbus-interface.h
+const NM_CAPABILITY_TEAM = 1;
+
+export const MacMenu = ({ idPrefix, model, mac, setMAC }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [optionsMap, setOptionsMap] = useState([]);
+
+ useEffect(() => {
+ const optionsMapInit = [];
+
+ model.list_interfaces().forEach(iface => {
+ if (iface.Device && iface.Device.HwAddress && iface.Device.HwAddress !== "00:00:00:00:00:00") {
+ optionsMapInit.push({
+ toString: () => cockpit.format("$0 ($1)", iface.Device.HwAddress, iface.Name),
+ value: iface.Device.HwAddress
+ });
+ }
+ });
+ optionsMapInit.push(
+ { toString: () => _("Permanent"), value: "permanent" },
+ { toString: () => _("Preserve"), value: "preserve" },
+ { toString: () => _("Random"), value: "random" },
+ { toString: () => _("Stable"), value: "stable" },
+ );
+ setOptionsMap(optionsMapInit);
+ }, [model]);
+
+ const clearSelection = () => {
+ setMAC(undefined);
+ setIsOpen(false);
+ };
+
+ const onSelect = (_, selection) => {
+ if (typeof selection == 'object')
+ setMAC(selection.value);
+ else
+ setMAC(selection);
+ setIsOpen(false);
+ };
+
+ const onCreateOption = newValue => {
+ setOptionsMap([...optionsMap, { value: newValue, toString: () => newValue }]);
+ };
+
+ return (
+ <Select createText={_("Use")}
+ isCreatable
+ isOpen={isOpen}
+ menuAppendTo={() => document.body}
+ onClear={clearSelection}
+ onCreateOption={onCreateOption}
+ onSelect={onSelect}
+ onToggle={(_event, value) => setIsOpen(value)}
+ selections={optionsMap.find(option => option.value == mac)}
+ variant="typeahead"
+ toggleId={idPrefix + "-mac-input"}
+ >
+ {optionsMap.map((option, index) => (
+ <SelectOption key={index}
+ value={option}
+ />
+ ))}
+ </Select>
+ );
+};
+
+export const MemberInterfaceChoices = ({ idPrefix, memberChoices, setMemberChoices, model, group }) => {
+ return (
+ <Stack id={idPrefix + "-interface-members-list"}>
+ {Object.keys(memberChoices).map((iface, idx) => (
+ <Checkbox data-iface={iface}
+ id={idPrefix + "-interface-members-" + iface}
+ isChecked={memberChoices[iface]}
+ key={iface}
+ label={iface}
+ onChange={(_event, checked) => setMemberChoices({ ...memberChoices, [iface]: checked })}
+ />
+ ))}
+ </Stack>
+ );
+};
+
+export const Name = ({ idPrefix, iface, setIface }) => {
+ return (
+ <FormGroup fieldId={idPrefix + "-interface-name-input"} label={_("Name")}>
+ <TextInput id={idPrefix + "-interface-name-input"} value={iface} onChange={(_event, value) => setIface(value)} />
+ </FormGroup>
+ );
+};
+
+export const NetworkModal = ({ dialogError, help, idPrefix, title, onSubmit, children, isFormHorizontal, isCreateDialog, submitDisabled = false }) => {
+ const Dialogs = useDialogs();
+
+ return (
+ <Modal id={idPrefix + "-dialog"} position="top" variant="medium"
+ isOpen
+ help={help}
+ onClose={Dialogs.close}
+ title={title}
+ footer={
+ <>
+ <Button variant='primary' id={idPrefix + "-save"} onClick={onSubmit} isDisabled={submitDisabled}>
+ {isCreateDialog ? _("Add") : _("Save")}
+ </Button>
+ <Button variant='link' id={idPrefix + "-cancel"} onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>
+ }
+ >
+ <Form id={idPrefix + "-body"} onSubmit={onSubmit} isHorizontal={isFormHorizontal !== false}>
+ {dialogError && <ModalError id={idPrefix + "-error"} dialogError={_("Failed to save settings")} dialogErrorDetail={dialogError} />}
+ {children}
+ </Form>
+ </Modal>
+ );
+};
+
+export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) => {
+ const Dialogs = useDialogs();
+ const model = useContext(ModelContext);
+
+ if (type == "team" && !model.get_manager().Capabilities.includes(NM_CAPABILITY_TEAM))
+ return null;
+
+ const con = iface && iface.MainConnection;
+ const dev = iface && iface.Device;
+
+ const getName = () => {
+ let name;
+ // Find the first free interface name
+ for (let i = 0; i < 100; i++) {
+ name = type + i;
+ if (!model.find_interface(name))
+ break;
+ }
+ return name;
+ };
+
+ const newIfaceName = !iface ? getName() : undefined;
+
+ let settings = connectionSettings;
+ if (!settings) {
+ if (type == 'bond') settings = getBondGhostSettings({ newIfaceName });
+ if (type == 'vlan') settings = getVlanGhostSettings();
+ if (type == 'team') settings = getTeamGhostSettings({ newIfaceName });
+ if (type == 'bridge') settings = getBridgeGhostSettings({ newIfaceName });
+ if (type == 'wg') settings = getWireGuardGhostSettings({ newIfaceName });
+ }
+
+ const properties = { connection: con, dev, settings };
+
+ async function resolveDeps(type) {
+ if (type === 'wg') {
+ try {
+ await cockpit.script("command -v wg");
+ } catch {
+ const packagekitExits = await packagekit.detect();
+ const os_release = await read_os_release();
+
+ // RHEL/CentOS 8 does not have wireguard-tools
+ if (packagekitExits && os_release.PLATFORM_ID !== "platform:el8")
+ await install_dialog("wireguard-tools");
+ }
+ }
+ }
+
+ function show() {
+ let dlg = null;
+ if (type == 'bond')
+ dlg = <BondDialog {...properties} />;
+ else if (type == 'vlan')
+ dlg = <VlanDialog {...properties} />;
+ else if (type == 'team')
+ dlg = <TeamDialog {...properties} />;
+ else if (type == 'bridge')
+ dlg = <BridgeDialog {...properties} />;
+ else if (type == 'wg')
+ dlg = <WireGuardDialog {...properties} />;
+ else if (type == 'mtu')
+ dlg = <MtuDialog {...properties} />;
+ else if (type == 'mac')
+ dlg = <MacDialog {...properties} />;
+ else if (type == 'teamport')
+ dlg = <TeamPortDialog {...properties} />;
+ else if (type == 'bridgeport')
+ dlg = <BridgePortDialog {...properties} />;
+ else if (type == 'ipv4')
+ dlg = <IpSettingsDialog topic="ipv4" {...properties} />;
+ else if (type == 'ipv6')
+ dlg = <IpSettingsDialog topic="ipv6" {...properties} />;
+
+ if (dlg)
+ resolveDeps(type)
+ .then(() => Dialogs.show(dlg))
+ .catch(err => console.error("NetworkAction Dialog failed:", err)); // not-covered: OS error
+ }
+
+ return (
+ <>
+ <Button id={"networking-" + (!iface ? "add-" : "edit-") + type}
+ isInline={!!iface}
+ onClick={syn_click(model, show)}
+ variant={!iface ? "secondary" : "link"}>
+ {buttonText || _("edit")}
+ </Button>
+ </>
+ );
+};
+
+function reactivateConnection({ con, dev }) {
+ if (con.Settings.connection.interface_name &&
+ con.Settings.connection.interface_name != dev.Interface) {
+ return dev.disconnect()
+ .then(() => con.activate(null, null))
+ .catch(show_unexpected_error);
+ } else {
+ return con.activate(dev, null)
+ .catch(show_unexpected_error);
+ }
+}
+
+export const dialogSave = ({ model, dev, connection, members, membersInit, settings, setDialogError, onClose }) => {
+ const apply_settings = settings_applier(model, dev, connection);
+ const iface = settings.connection.interface_name;
+ const type = settings.connection.type;
+ const membersChanged = members ? Object.keys(membersInit).some(iface => membersInit[iface] != members[iface]) : false;
+
+ model.set_operation_in_progress(true);
+
+ const modify = () => {
+ return ((members !== undefined)
+ ? apply_group_member(members,
+ model,
+ apply_settings,
+ connection,
+ settings,
+ type)
+ : apply_settings(settings))
+ .then(() => {
+ onClose();
+ if (connection)
+ cockpit.location.go([iface]);
+ if (connection && dev && dev.ActiveConnection && dev.ActiveConnection.Connection === connection)
+ return reactivateConnection({ con: connection, dev });
+ })
+ .catch(ex => setDialogError(typeof ex === 'string' ? ex : ex.message))
+ .then(() => model.set_operation_in_progress(false));
+ };
+ if (connection) {
+ with_settings_checkpoint(model, modify,
+ {
+ ...(type != 'vlan' && {
+ devices: (membersChanged ? [] : connection_devices(connection))
+ }),
+ hack_does_add_or_remove: type == 'vlan' || membersChanged,
+ rollback_on_failure: type !== 'vlan' && membersChanged
+ });
+ } else {
+ try {
+ with_checkpoint(
+ model,
+ modify,
+ {
+ fail_text: cockpit.format(_("Creating this $0 will break the connection to the server, and will make the administration UI unavailable."), type == 'vlan' ? 'VLAN' : type),
+ anyway_text: _("Create it"),
+ hack_does_add_or_remove: true,
+ rollback_on_failure: type != 'vlan',
+ });
+ } catch (e) {
+ setDialogError(typeof e === 'string' ? e : e.message);
+ }
+ }
+};
diff --git a/pkg/networkmanager/firewall-client.js b/pkg/networkmanager/firewall-client.js
new file mode 100644
index 0000000..0a7a848
--- /dev/null
+++ b/pkg/networkmanager/firewall-client.js
@@ -0,0 +1,547 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import * as service from 'service';
+import { debounce } from 'throttle-debounce';
+import * as utils from './utils.js';
+
+const firewall = {
+ owner: undefined,
+ installed: true,
+ enabled: false,
+ readonly: true,
+ services: {},
+ enabledServices: new Set(),
+ /* Dictionary where zone ID is the key and zone information, as fetched by
+ * fetchZoneInfos, the value */
+ zones: {},
+ activeZones: new Set(),
+ /* Zones predefined by firewalld, from untrusted to trusted */
+ predefinedZones: ['drop', 'block', 'public', 'external',
+ 'dmz', 'work', 'home', 'internal', 'trusted'],
+ defaultZone: null,
+ availableInterfaces: [],
+ ready: false
+};
+
+cockpit.event_target(firewall);
+
+utils.list_interfaces().then(interfaces => {
+ firewall.availableInterfaces = interfaces;
+});
+
+const firewalld_service = service.proxy('firewalld');
+let firewalld_dbus = null;
+
+function debug() {
+ if (window.debugging == "all" || window.debugging?.includes("firewalld")) // not-covered: debugging
+ console.debug("firewalld:", ...arguments); // not-covered: debugging
+}
+
+firewall.debouncedGetZones = debounce(300, () => {
+ getZones()
+ .then(() => getServices())
+ .then(() => firewall.debouncedEvent('changed'))
+ .catch(error => console.warn(error));
+});
+
+/* As certain dbus signal callbacks might change the firewall frequently
+ * in a short period of time, prevent rapid succession of renders by
+ * debouncing the ('changed') event */
+firewall.debouncedEvent = debounce(300, event => firewall.dispatchEvent(event));
+
+/* As a service might be removed from multiple zones at the same time,
+ * prevent rapid succession of GetServices call */
+firewall.debouncedGetServices = debounce(300, () => {
+ getServices().then(() => firewall.debouncedEvent('changed'));
+});
+
+function initFirewalldDbus() {
+ debug("initializing D-Bus connection");
+ if (firewalld_dbus)
+ firewalld_dbus.close();
+
+ firewalld_dbus = cockpit.dbus('org.fedoraproject.FirewallD1', { superuser: "try" });
+
+ firewalld_dbus.addEventListener('owner', (event, owner) => {
+ if (firewall.owner === owner)
+ return;
+ debug("owner changed:", JSON.stringify(owner));
+
+ firewall.owner = owner;
+ firewall.enabled = !!owner;
+
+ firewall.zones = {};
+ firewall.activeZones = new Set();
+ firewall.services = {};
+ firewall.enabledServices = new Set();
+
+ if (!firewall.enabled) {
+ firewall.ready = true;
+ firewall.dispatchEvent('changed');
+ return;
+ }
+
+ getZones()
+ .then(() => getServices())
+ .catch(error => console.warn(error))
+ .then(() => { firewall.ready = true })
+ .then(() => firewall.debouncedEvent('changed'));
+ });
+
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.zone',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'ServiceAdded'
+ }, (path, iface, signal, args) => {
+ const zone = args[0];
+ const service = args[1];
+ fetchZoneInfos([zone])
+ .then(() => fetchServiceInfos([service]))
+ .then(info => firewall.enabledServices.add(info[0].id))
+ .then(() => firewall.debouncedEvent('changed'))
+ .catch(error => console.warn(error));
+ });
+
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.zone',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'ServiceRemoved'
+ }, (path, iface, signal, args) => {
+ const zone = args[0];
+ const service = args[1];
+
+ firewall.zones[zone].services = firewall.zones[zone].services.filter(s => s !== service);
+ firewall.enabledServices.delete(service);
+ firewall.debouncedGetServices();
+ });
+
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.zone',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'PortAdded'
+ }, (path, iface, signal, args) => {
+ const zone = args[0];
+ const port = args[1];
+ const protocol = args[2];
+ if (!firewall.zones[zone].ports.some(p => p.port === port && p.protocol === protocol)) {
+ firewall.zones[zone].ports.push({ port, protocol });
+ firewall.debouncedEvent('changed');
+ }
+ });
+
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.zone',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'PortRemoved'
+ }, (path, iface, signal, args) => {
+ const zone = args[0];
+ const port = args[1];
+ const protocol = args[2];
+ firewall.zones[zone].ports = firewall.zones[zone].ports
+ .filter(p => p.port !== port || p.protocol !== protocol);
+ firewall.debouncedEvent('changed');
+ });
+
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'Reloaded'
+ }, () => firewall.debouncedGetZones());
+
+ /* There are two APIs available, changeZoneOf(Interface|Source) and
+ * add(Interface|Source). Listen to both of them for any background changes
+ * to zones. */
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.zone',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'ZoneOfInterfaceChanged'
+ }, () => firewall.debouncedGetZones());
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.zone',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'ZoneOfSourceChanged'
+ }, () => firewall.debouncedGetZones());
+
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.zone',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'InterfaceAdded'
+ }, () => firewall.debouncedGetZones());
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.zone',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'SourceAdded'
+ }, () => firewall.debouncedGetZones());
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.zone',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'InterfaceRemoved'
+ }, () => firewall.debouncedGetZones());
+ firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.zone',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'SourceRemoved'
+ }, () => firewall.debouncedGetZones());
+}
+
+firewalld_service.addEventListener('changed', () => {
+ const installed = !!firewalld_service.exists;
+ const is_running = firewalld_service.state === 'running';
+
+ // we get lots of these events, for internal property changes; filter interesting changes
+ if (is_running === firewalld_service.prev_running && firewall.installed === installed)
+ return;
+ firewall.installed = installed;
+ firewalld_service.prev_running = is_running;
+
+ debug("systemd service changed: exists", firewalld_service.exists, "state", firewalld_service.state,
+ "firewall.enabled:", JSON.stringify(firewall.enabled));
+
+ /* HACK: cockpit.dbus() remains dead for non-activatable names, so reinitialize it if the service gets enabled and started
+ * See https://github.com/cockpit-project/cockpit/pull/9125 */
+ if (!firewall.enabled && is_running) {
+ debug("reinitializing D-Bus connection after unit got started");
+ initFirewalldDbus();
+ }
+
+ firewall.dispatchEvent('changed');
+});
+
+function getZones() {
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1.zone',
+ 'getActiveZones', [])
+ .then(reply => fetchZoneInfos(Object.keys(reply[0])))
+ .then(zones => {
+ firewall.activeZones = new Set(zones.map(z => z.id));
+ debug("getActiveZones succeeded:", JSON.stringify([...firewall.activeZones]));
+ })
+ .then(() => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1',
+ 'getDefaultZone', []))
+ .then(reply => {
+ firewall.defaultZone = reply[0];
+ })
+ .then(() => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1.zone',
+ 'getZones', []))
+ .then(reply => fetchZoneInfos(reply[0]));
+}
+
+function getServices() {
+ if (firewall.readonly)
+ return Promise.resolve();
+ firewall.enabledServices = new Set();
+ return Promise.all([...firewall.activeZones].map(z => {
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1.zone',
+ 'getServices', [z])
+ .then(reply => fetchServiceInfos(reply[0]))
+ .then(services => {
+ const promises = [];
+ for (const s of services) {
+ firewall.enabledServices.add(s.id);
+ if (s.includes.length)
+ promises.push(fetchServiceInfos(s.includes));
+ }
+ return Promise.all(promises);
+ });
+ }));
+}
+
+function fetchServiceInfos(services) {
+ return Promise.all(services.map(service => {
+ if (firewall.services[service])
+ return firewall.services[service];
+
+ let info;
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1',
+ 'getServiceSettings2', [service])
+ .then(([{ short, description, ports }]) => {
+ short = short ? short.v : "";
+ description = description ? description.v : "";
+ ports = ports ? ports.v : [];
+ info = {
+ id: service,
+ name: short,
+ description,
+ ports: ports.map(p => ({ port: p[0], protocol: p[1] })),
+ includes: [],
+ };
+
+ firewall.services[service] = info;
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
+ 'org.fedoraproject.FirewallD1.config',
+ 'getServiceByName', [service]);
+ })
+ .then(path => firewalld_dbus.call(path[0],
+ 'org.fedoraproject.FirewallD1.config.service',
+ 'getSettings2', []))
+ .then(reply => {
+ if (reply[0].includes) {
+ info.includes = reply[0].includes.v;
+ firewall.services[service] = info;
+ }
+ return info;
+ })
+ .catch(error => {
+ if (error.name === 'org.freedesktop.DBus.Error.UnknownMethod')
+ return info;
+ return Promise.reject(error);
+ });
+ }));
+}
+
+function fetchZoneInfos(zones) {
+ return Promise.all(zones.map(zone => {
+ if (firewall.readonly) {
+ const info = {
+ id: zone,
+ name: zone,
+ description: null,
+ target: null,
+ services: [],
+ ports: [],
+ interfaces: [],
+ source: [],
+ };
+ firewall.zones[zone] = info;
+ return info;
+ }
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1.zone',
+ 'getZoneSettings2', [zone])
+ .then(([zoneInfo]) => {
+ const info = {
+ id: zone,
+ name: (zoneInfo.short || {}).v,
+ description: (zoneInfo.description || {}).v,
+ target: (zoneInfo.target || {}).v,
+ services: ((zoneInfo.services || {}).v || []),
+ ports: ((zoneInfo.ports || {}).v || []).map(p => ({ port: p[0], protocol: p[1] })),
+ interfaces: ((zoneInfo.interfaces || {}).v || []),
+ source: ((zoneInfo.sources || {}).v || []),
+ };
+ firewall.zones[zone] = info;
+ return info;
+ });
+ }));
+}
+
+initFirewalldDbus();
+
+cockpit.spawn(['sh', '-c', 'pkcheck --action-id org.fedoraproject.FirewallD1.all --process $$ --allow-user-interaction 2>&1'], { superuser: "try" })
+ .then(() => {
+ firewall.readonly = false;
+ firewall.debouncedEvent('changed');
+ firewall.debouncedGetZones();
+ })
+ .catch(error => {
+ console.log("pkcheck failed", error);
+
+ // Fall back to cockpit.permissions, "pkcheck" might not be available,
+ // always allow edits by admins
+ const permission = cockpit.permission({ admin: true });
+ const update_permissions = () => {
+ firewall.readonly = !permission.allowed;
+ firewall.debouncedEvent('changed');
+ firewall.debouncedGetZones();
+ };
+ permission.addEventListener("changed", update_permissions);
+ });
+
+firewall.enable = () => firewalld_service.enable().then(() => firewalld_service.start());
+firewall.disable = () => firewalld_service.disable().then(() => firewalld_service.stop());
+
+firewall.getAvailableServices = () => {
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1',
+ 'listServices', [])
+ .then(reply => fetchServiceInfos(reply[0]))
+ .catch(error => console.warn(error));
+};
+
+/*
+ * Only call this after defining a new service, as it will remove existing
+ * non-permanent configurations.
+ */
+firewall.reload = () => {
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1',
+ 'reload', [])
+ .catch(error => console.warn(error));
+};
+
+/*
+ * Remove a service from the specified zone (i.e., close its ports).
+ *
+ * Returns a promise that resolves when the service is removed.
+ */
+firewall.removeService = (zone, service) => {
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1.zone',
+ 'removeService', [zone, service])
+ .then(reply => firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
+ 'org.fedoraproject.FirewallD1.config',
+ 'getZoneByName', [zone]))
+ .then(path => firewalld_dbus.call(path[0], 'org.fedoraproject.FirewallD1.config.zone',
+ 'removeService', [service]));
+};
+
+/*
+ * Create new firewalld service.
+ *
+ * Returns a promise that resolves when the service is created.
+ * It will also reload firewalld and enable the new service.
+ */
+firewall.createService = (service, ports, zones, desc = "") => {
+ const subscription = firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1',
+ path: '/org/fedoraproject/FirewallD1',
+ member: 'Reloaded'
+ }, () => {
+ firewall.addServices(zones, [service]);
+ subscription.remove();
+ });
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
+ 'org.fedoraproject.FirewallD1.config',
+ 'addService2', [service, { description: { t: 's', v: desc }, ports: { t: 'a(ss)', v: ports } }])
+ .then(() => firewall.reload());
+};
+
+/*
+ * Edit firewalld service.
+ *
+ * Returns a promise that resolves when the service is edited.
+ */
+firewall.editService = (service, ports, desc = "") => {
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
+ 'org.fedoraproject.FirewallD1.config',
+ 'getServiceByName', [service])
+ .then(path => firewalld_dbus.call(path[0],
+ 'org.fedoraproject.FirewallD1.config.service',
+ 'update2', [{ description: { t: 's', v: desc }, ports: { t: 'a(ss)', v: ports } }])
+ .then(() => {
+ // No signal for updated service so we need to manually update it
+ firewall.services[service].description = desc;
+ firewall.services[service].ports = ports.map(port => ({ port: port[0], protocol: port[1] }));
+ firewall.debouncedEvent('changed');
+ return firewall.reload();
+ })
+ );
+};
+
+/*
+ * Add a predefined firewalld service to the specified zone (i.e., open its
+ * ports).
+ *
+ * Returns a promise that resolves when the service is added.
+ */
+firewall.addService = (zone, service) => {
+ return firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1.zone',
+ 'addService', [zone, service, 0])
+ .then(reply => firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
+ 'org.fedoraproject.FirewallD1.config',
+ 'getZoneByName', [zone]))
+ .then(path => firewalld_dbus.call(path[0], 'org.fedoraproject.FirewallD1.config.zone',
+ 'addService', [service]));
+};
+
+/*
+ * Like addService(), but adds multiple predefined firewalld services at once
+ * to the specified zones.
+ *
+ * Returns a promise that resolves when all services are added.
+ */
+firewall.addServices = (zone, services) =>
+ Promise.all(services.map(s => firewall.addService(zone, s)));
+
+firewall.removeServiceFromZones = (zones, service) =>
+ Promise.all(zones.map(z => firewall.removeService(z, service)));
+
+firewall.activateZone = (zone, interfaces, sources) => {
+ let promises = interfaces.map(i => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1.zone',
+ 'addInterface', [zone, i]));
+
+ promises = promises.concat(sources.map(s => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1.zone',
+ 'addSource', [zone, s])));
+ let p = Promise.all(promises).then(() => firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
+ 'org.fedoraproject.FirewallD1.config',
+ 'getZoneByName', [zone]));
+ p = p.then(path => {
+ /* Once this signal is received, it's safe to actually emit the changed
+ * signal and thus update the UI */
+ const subscription = firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.config.zone',
+ path: path[0],
+ member: 'Updated'
+ }, (path, iface, signal, args) => {
+ getZones().then(() => getServices());
+ subscription.remove();
+ });
+
+ return firewalld_dbus.call(path[0],
+ 'org.fedoraproject.FirewallD1.config.zone',
+ 'update2', [{ interfaces: { t: 'as', v: interfaces }, sources: { t: 'as', v: sources } }]);
+ });
+ return p;
+};
+
+/*
+ * A zone is considered deactivated when it has no interfaces or sources.
+ */
+firewall.deactiveateZone = (zone) => {
+ const zoneObject = firewall.zones[zone];
+ let promises = zoneObject.interfaces.map(i => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1.zone',
+ 'removeInterface', [zone, i]));
+ promises = promises.concat(zoneObject.source.map(s => firewalld_dbus.call('/org/fedoraproject/FirewallD1',
+ 'org.fedoraproject.FirewallD1.zone',
+ 'removeSource', [zone, s])));
+ let p = Promise.all(promises).then(() => firewalld_dbus.call('/org/fedoraproject/FirewallD1/config',
+ 'org.fedoraproject.FirewallD1.config',
+ 'getZoneByName', [zone]));
+ p = p.then(path => {
+ /* Once this signal is received, it's safe to actually emit the changed
+ * signal and thus update the UI */
+ const subscription = firewalld_dbus.subscribe({
+ interface: 'org.fedoraproject.FirewallD1.config.zone',
+ path: path[0],
+ member: 'Updated'
+ }, (path, iface, signal, args) => {
+ firewall.activeZones.delete(args[0]);
+ getZones().then(() => getServices());
+ subscription.remove();
+ });
+
+ return firewalld_dbus.call(path[0],
+ 'org.fedoraproject.FirewallD1.config.zone',
+ 'update2', [{ interfaces: { t: 'as', v: [] }, sources: { t: 'as', v: [] } }]);
+ });
+
+ return p.catch(error => console.warn(error));
+};
+
+export default firewall;
diff --git a/pkg/networkmanager/firewall-switch.jsx b/pkg/networkmanager/firewall-switch.jsx
new file mode 100644
index 0000000..e27177a
--- /dev/null
+++ b/pkg/networkmanager/firewall-switch.jsx
@@ -0,0 +1,85 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import cockpit from "cockpit";
+import React from "react";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+
+const _ = cockpit.gettext;
+
+export class FirewallSwitch extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ pendingTarget: null /* `null` for not pending */
+ };
+ this.onSwitchChanged = this.onSwitchChanged.bind(this);
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ if (props.firewall.enabled == state.pendingTarget) {
+ return {
+ pendingTarget: null
+ };
+ }
+ return null;
+ }
+
+ onSwitchChanged(_event, value) {
+ this.setState({ pendingTarget: value });
+
+ if (value)
+ this.props.firewall.enable();
+ else
+ this.props.firewall.disable();
+ }
+
+ render() {
+ const enabled = this.state.pendingTarget !== null ? this.state.pendingTarget : this.props.firewall.enabled;
+ let firewallOnOff;
+
+ if (this.props.firewall.readonly) {
+ firewallOnOff = <Tooltip id="tip-auth"
+ content={ _("You are not authorized to modify the firewall.") }>
+ <Switch isChecked={enabled}
+ id='networking-firewall-switch'
+ className='networking-firewall-switch'
+ onChange={this.onSwitchChanged}
+ aria-label={enabled ? _("Not authorized to disable the firewall") : _("Not authorized to enable the firewall")}
+ isDisabled />
+ </Tooltip>;
+ } else {
+ firewallOnOff = <Switch isChecked={enabled}
+ id='networking-firewall-switch'
+ className='networking-firewall-switch'
+ isDisabled={!!this.state.pendingTarget}
+ onChange={this.onSwitchChanged}
+ aria-label={enabled ? _("Disable the firewall") : _("Enable the firewall")} />;
+ }
+ return (
+ <>
+ {firewallOnOff}
+ <HelperText>
+ <HelperTextItem variant="indeterminate">{enabled ? _("Enabled") : _("Disabled")}</HelperTextItem>
+ </HelperText>
+ </>
+ );
+ }
+}
diff --git a/pkg/networkmanager/firewall.html b/pkg/networkmanager/firewall.html
new file mode 100644
index 0000000..4035878
--- /dev/null
+++ b/pkg/networkmanager/firewall.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2017 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+ <title translate="yes">Firewall</title>
+ <meta charset="utf-8" />
+
+ <link href="firewall.css" type="text/css" rel="stylesheet" />
+
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+ <script src="firewall.js"></script>
+ </head>
+
+ <body class="pf-v5-m-tabular-nums">
+ <div id="firewall" class="ct-page-fill"></div>
+ </body>
+</html>
diff --git a/pkg/networkmanager/firewall.jsx b/pkg/networkmanager/firewall.jsx
new file mode 100644
index 0000000..a0acddc
--- /dev/null
+++ b/pkg/networkmanager/firewall.jsx
@@ -0,0 +1,1096 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import 'cockpit-dark-theme'; // once per page
+import cockpit from "cockpit";
+import React, { useState } from 'react';
+import { createRoot } from "react-dom/client";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { DataList, DataListCell, DataListCheck, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
+import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Form, FormGroup, FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { Title } from "@patternfly/react-core/dist/esm/components/Title/index.js";
+import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js";
+import { Page, PageBreadcrumb, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+
+import firewall from "./firewall-client.js";
+import { FormHelper } from "cockpit-components-form-helper";
+import { ListingTable } from 'cockpit-components-table.jsx';
+import { ModalError } from "cockpit-components-inline-notification.jsx";
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { FirewallSwitch } from "./firewall-switch.jsx";
+
+import { superuser } from "superuser";
+import { WithDialogs, DialogsContext } from "dialogs.jsx";
+
+import "./networking.scss";
+
+const _ = cockpit.gettext;
+
+superuser.reload_page_on_change();
+
+const upperCaseFirstLetter = text => text[0].toUpperCase() + text.slice(1);
+
+const DeleteDropdown = ({ items, id }) => {
+ const [isActionsKebabOpen, setActionsKebabOpen] = useState(false);
+
+ const dropdown_items = items.map(item => <DropdownItem key={item.text}
+ className={item.danger ? "pf-m-danger" : ""}
+ aria-label={item.ariaLabel}
+ onClick={item.handleClick}>
+ {item.text}
+ </DropdownItem>);
+
+ return (<Dropdown toggle={<KebabToggle onToggle={(_event, isOpen) => setActionsKebabOpen(isOpen)} id={id || null} />}
+ isOpen={isActionsKebabOpen}
+ isPlain
+ position="right"
+ dropdownItems={dropdown_items} />);
+};
+
+function serviceRow(props) {
+ let tcp = props.service.ports.filter(p => p.protocol.toUpperCase() == 'TCP');
+ let udp = props.service.ports.filter(p => p.protocol.toUpperCase() == 'UDP');
+
+ for (const s of props.service.includes) {
+ if (firewall.services[s]) {
+ tcp = tcp.concat(firewall.services[s].ports.filter(p => p.protocol.toUpperCase() == 'TCP'));
+ udp = udp.concat(firewall.services[s].ports.filter(p => p.protocol.toUpperCase() == 'UDP'));
+ }
+ }
+
+ function onRemoveService(event) {
+ props.onRemoveService(props.service.id);
+ event.stopPropagation();
+ }
+
+ function onEditService(event) {
+ props.onEditService(props.service.id);
+ event.stopPropagation();
+ }
+
+ const columns = [
+ {
+ title: props.service.id, header: true
+ },
+ {
+ title: <div key={props.service.id + "tcp"}>
+ { tcp.map(p => p.port).join(', ') }
+ </div>
+ },
+ {
+ title: <div key={props.service.id + "udp"}>
+ { udp.map(p => p.port).join(', ') }
+ </div>
+ },
+ ];
+
+ if (!props.readonly) {
+ // Only allow editing manually created services - no name is a decent (and only) indicator
+ const items = [];
+ if (!props.service.name)
+ items.push({ text: _("Edit"), ariaLabel: cockpit.format(_("Edit service $0"), props.service.id), handleClick: onEditService });
+
+ items.push({ text: _("Delete"), danger: true, ariaLabel: cockpit.format(_("Remove service $0"), props.service.id), handleClick: onRemoveService });
+
+ columns.push({
+ title: <DeleteDropdown items={items} id={props.service.key} />
+ });
+ }
+
+ let description, includes;
+ if (props.service.description)
+ description = <p>{props.service.description}</p>;
+
+ if (props.service.includes.length > 0) {
+ includes = <>
+ <h5>Included Services</h5>
+ <ul>{props.service.includes.map(s => {
+ const service = firewall.services[s];
+ if (service && service.description)
+ return <li key={service.id}><strong>{service.id}</strong>: {service.description}</li>;
+ else
+ return null;
+ })} </ul></>;
+ }
+
+ return ({
+ props: { key: props.service.id, 'data-row-id': props.service.id },
+ columns,
+ hasPadding: true,
+ expandedContent: <>{description}{includes}</>,
+ });
+}
+
+function portRow(props) {
+ const columns = [
+ {
+ title: <i key={props.zone.id + "-additional-ports"}>{ _("Additional ports") }</i>
+ },
+ {
+ title: props.zone.ports
+ .filter(p => p.protocol === "tcp")
+ .map(p => p.port)
+ .join(", ")
+ },
+ {
+ title: props.zone.ports
+ .filter(p => p.protocol === "udp")
+ .map(p => p.port)
+ .join(", ")
+ },
+ ];
+ return ({
+ props: { key: props.zone.id + "-ports", 'data-row-id': props.zone.id + "-ports" },
+ columns
+ });
+}
+
+function ZoneSection(props) {
+ function onRemoveZone(event) {
+ event.stopPropagation();
+ props.onRemoveZone(props.zone.id);
+ }
+
+ const deleteButton = (<DeleteDropdown items={[{ text: _("Delete"), danger: true, ariaLabel: cockpit.format(_("Remove zone $0"), props.zone.id), handleClick: onRemoveZone }]} id={`dropdown-${props.zone.id}`} />);
+ const addServiceAction = (
+ <Button variant="primary" onClick={() => props.openServicesDialog(props.zone.id, props.zone.id)} className="add-services-button" aria-label={cockpit.format(_("Add services to zone $0"), props.zone.id)}>
+ {_("Add services")}
+ </Button>
+ );
+
+ const actions = !firewall.readonly && <div className="zone-section-buttons">{addServiceAction}{deleteButton}</div>;
+
+ return <Card className="zone-section" data-id={props.zone.id}>
+ <CardHeader actions={{ actions }} className="zone-section-heading">
+ <Flex alignItems={{ default: 'alignSelfBaseline' }} spaceItems={{ default: 'spaceItemsXl' }}>
+ <CardTitle component="h2">
+ { cockpit.format(_("$0 zone"), upperCaseFirstLetter(props.zone.name || props.zone.id)) }
+ </CardTitle>
+ <Flex>
+ { props.zone.interfaces.length > 0 &&
+ <span>
+ <strong>{cockpit.ngettext("Interface", "Interfaces", props.zone.interfaces.length)}</strong> {props.zone.interfaces.join(", ")}
+ </span>
+ }
+ <span>
+ <strong>{_("Allowed addresses")}</strong> {props.zone.source.length ? props.zone.source.join(", ") : _("Entire subnet")}
+ </span>
+ </Flex>
+ </Flex>
+ </CardHeader>
+ {(props.zone.services.length > 0 || props.zone.ports.length > 0) &&
+ <CardBody className="contains-list">
+ <ListingTable columns={[{ title: _("Service"), props: { width: 40 } }, { title: _("TCP"), props: { width: 30 } }, { title: _("UDP"), props: { width: 30 } }, { title: "", props: { width: 10 } }]}
+ id={props.zone.id}
+ aria-label={props.zone.id}
+ variant="compact"
+ emptyCaption={_("There are no active services in this zone")}
+ rows={
+ props.zone.services.map(s => {
+ if (s in firewall.services) {
+ return serviceRow({
+ key: firewall.services[s].id,
+ service: firewall.services[s],
+ onRemoveService: service => props.onRemoveService(props.zone.id, service),
+ onEditService: service => props.onEditService(props.zone, firewall.services[service]),
+ readonly: firewall.readonly,
+ });
+ } else {
+ return null;
+ }
+ }).concat(
+ props.zone.ports.length > 0
+ ? portRow({
+ key: props.zone.id + "-ports",
+ zone: props.zone,
+ readonly: firewall.readonly
+ })
+ : [])
+ .filter(Boolean)}
+
+ />
+ </CardBody>}
+ </Card>;
+}
+
+class SearchInput extends React.Component {
+ constructor(props) {
+ super(props);
+ this.onValueChanged = this.onValueChanged.bind(this);
+ this.state = { value: props.value || "" };
+ }
+
+ onValueChanged(value) {
+ this.setState({ value });
+
+ if (this.timer)
+ window.clearTimeout(this.timer);
+
+ this.timer = window.setTimeout(() => {
+ this.props.onChange(value);
+ this.timer = null;
+ }, 300);
+ }
+
+ render() {
+ return (
+ <Toolbar className="filter-services-toolbar">
+ <ToolbarContent>
+ <ToolbarItem variant="label">
+ {_("Filter services")}
+ </ToolbarItem>
+ <ToolbarItem>
+ <TextInput type="search"
+ id={this.props.id}
+ onChange={(_event, value) => this.onValueChanged(value)}
+ value={this.state.value}
+ />
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ );
+ }
+}
+
+const renderPorts = service => {
+ const tcpPorts = [];
+ const udpPorts = [];
+ function addPorts(ports) {
+ for (const port of ports) {
+ if (port.protocol === "tcp")
+ tcpPorts.push(port.port);
+ else
+ udpPorts.push(port.port);
+ }
+ }
+ addPorts(service.ports);
+ for (const s of service.includes)
+ addPorts(firewall.services[s].ports);
+
+ return (
+ <div className="service-list-item-text">
+ { tcpPorts.length > 0 && <span className="service-ports tcp"><strong>TCP: </strong>{ tcpPorts.join(', ') }</span> }
+ { udpPorts.length > 0 && <span className="service-ports udp"><strong>UDP: </strong>{ udpPorts.join(', ') }</span> }
+ </div>
+ );
+};
+
+class AddEditServicesModal extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ services: null,
+ selected: new Set(),
+ filter: "",
+ custom: !!props.custom_id,
+ generate_custom_id: !props.custom_id,
+ tcp_error: "",
+ udp_error: "",
+ avail_services: null,
+ custom_id: props.custom_id || "",
+ custom_description: props.custom_description || "",
+ custom_tcp_ports: props.custom_tcp_ports || [],
+ custom_udp_ports: props.custom_udp_ports || [],
+ custom_tcp_value: props.custom_tcp_value || "",
+ custom_udp_value: props.custom_udp_value || "",
+ dialogError: null,
+ dialogErrorDetail: null,
+ };
+ this.save = this.save.bind(this);
+ this.edit = this.edit.bind(this);
+ this.checkNullValues = this.checkNullValues.bind(this);
+ this.onFilterChanged = this.onFilterChanged.bind(this);
+ this.onToggleService = this.onToggleService.bind(this);
+ this.setId = this.setId.bind(this);
+ this.setDescription = this.setDescription.bind(this);
+ this.getName = this.getName.bind(this);
+ this.validate = this.validate.bind(this);
+ this.createPorts = this.createPorts.bind(this);
+ this.parseServices = this.parseServices.bind(this);
+ this.onToggleType = this.onToggleType.bind(this);
+ this.getCustomId = this.getCustomId.bind(this);
+ }
+
+ createPorts() {
+ const ret = [];
+ this.state.custom_tcp_ports.forEach(port => ret.push([port, 'tcp']));
+ this.state.custom_udp_ports.forEach(port => ret.push([port, 'udp']));
+ return ret;
+ }
+
+ getCustomId() {
+ return "custom--" + (
+ this.state.custom_tcp_ports.map(port => this.getName(port, "tcp"))
+ .concat(this.state.custom_udp_ports.map(port => this.getName(port, "udp")))
+ .join('-')
+ );
+ }
+
+ checkNullValues() {
+ return (!this.state.custom_tcp_value && !this.state.custom_udp_value);
+ }
+
+ edit(event) {
+ const Dialogs = this.context;
+ firewall.editService(this.props.custom_id, this.createPorts(), this.state.custom_description)
+ .then(() => Dialogs.close())
+ .catch(error => {
+ this.setState({
+ dialogError: _("Failed to edit service"),
+ dialogErrorDetail: error.name + ": " + error.message,
+ });
+ });
+
+ if (event)
+ event.preventDefault();
+ return false;
+ }
+
+ save(event) {
+ const Dialogs = this.context;
+ let p;
+ if (this.state.custom) {
+ const custom_id = this.state.custom_id === "" ? this.getCustomId() : this.state.custom_id;
+ p = firewall.createService(custom_id, this.createPorts(), this.props.zoneId, this.state.custom_description);
+ } else {
+ p = firewall.addServices(this.props.zoneId, [...this.state.selected]);
+ }
+ p.then(() => Dialogs.close())
+ .catch(error => {
+ this.setState(prevState => ({
+ dialogError: prevState.custom ? _("Failed to add port") : _("Failed to add service"),
+ dialogErrorDetail: error.name + ": " + error.message,
+ }));
+ });
+
+ if (event)
+ event.preventDefault();
+ return false;
+ }
+
+ onToggleService(event, serviceId) {
+ const service = serviceId;
+ const enabled = event.target.checked;
+
+ this.setState(oldState => {
+ const selected = new Set(oldState.selected);
+
+ if (enabled)
+ selected.add(service);
+ else
+ selected.delete(service);
+
+ return {
+ selected
+ };
+ });
+ }
+
+ /* Create list of services from /etc/services type file
+ *
+ * Return dictionary of services:
+ * - key => port number or port alias (80/http)
+ * - item => dictionary with 3 compulsory items:
+ * - name => port alias (http)
+ * - port => port number (80)
+ * - type => list of types (tcp/udp...)
+ * - description => _may be not present_ (Web Server)
+ */
+ parseServices(content) {
+ if (!content) {
+ console.warn("Couldn't read /etc/services");
+ return [];
+ }
+
+ const ret = {};
+ content.split('\n').forEach(line => {
+ if (!line || line.startsWith("#"))
+ return;
+ const m = line.match(/^(\S+)\s+(\d+)\/(\S+).*?(#(.*))?$/);
+ const new_port = { name: m[1], port: m[2], type: [m[3]] };
+ if (m.length > 5 && m[5])
+ new_port.description = m[5].trim();
+ if (ret[m[1]])
+ ret[m[1]].type.push(new_port.type[0]);
+ else
+ ret[m[1]] = new_port;
+ if (ret[m[2]])
+ ret[m[2]].type.push(new_port.type[0]);
+ else
+ ret[m[2]] = new_port;
+ });
+ return ret;
+ }
+
+ setId(value) {
+ this.setState({
+ custom_id: value,
+ generate_custom_id: value.length === 0,
+ });
+ }
+
+ setDescription(value) {
+ this.setState({
+ custom_description: value
+ });
+ }
+
+ getName(port, type) {
+ const known = this.state.avail_services[port];
+ if (known && known.type.includes(type))
+ return known.name;
+ else
+ return port;
+ }
+
+ getPortNumber(port, type, avail) {
+ if (!avail) {
+ const num_p = Number(port);
+ if (isNaN(num_p))
+ return [0, _("Unknown service name")];
+ else if (num_p <= 0 || num_p > 65535)
+ return [0, _("Invalid port number")];
+ else
+ return [port, ""];
+ } else {
+ return [avail.port, ""];
+ }
+ }
+
+ validate(event, value) {
+ let error = "";
+ let targets = ['tcp', 'custom_tcp_ports', 'tcp_error', 'custom_tcp_value'];
+ if (event.target.id === "udp-ports")
+ targets = ['udp', 'custom_udp_ports', 'udp_error', 'custom_udp_value'];
+ const new_ports = [];
+ const event_id = event.target.id;
+
+ this.setState(oldState => {
+ const ports = value.split(',');
+ ports.forEach((port) => {
+ port = port.trim();
+ if (!port)
+ return;
+ let ports;
+ if (port.indexOf("-") > -1) {
+ ports = port.split("-");
+ if (ports.length != 2) {
+ error = _("Invalid range");
+ return;
+ }
+ [ports[0], error] = this.getPortNumber(ports[0], targets[0], oldState.avail_services[ports[0]]);
+ if (!error) {
+ [ports[1], error] = this.getPortNumber(ports[1], targets[0], oldState.avail_services[ports[1]]);
+ if (!error) {
+ if (Number(ports[0]) >= Number(ports[1]))
+ error = _("Range must be strictly ordered");
+ else
+ new_ports.push(ports[0] + "-" + ports[1]);
+ }
+ }
+ } else {
+ [ports, error] = this.getPortNumber(port, targets[0], oldState.avail_services[port]);
+ if (!error)
+ new_ports.push(ports);
+ }
+ });
+ const newState = {
+ [targets[1]]: new_ports,
+ [targets[2]]: error,
+ [targets[3]]: value
+ };
+
+ let all_ports;
+ if (event_id === "udp-ports") {
+ const old_ports = oldState.custom_tcp_ports.map(port => this.getName(port, "tcp"));
+ all_ports = old_ports.concat(new_ports.map(port => this.getName(port, "udp")));
+ } else {
+ const old_ports = oldState.custom_udp_ports.map(port => this.getName(port, "udp"));
+ all_ports = new_ports.map(port => this.getName(port, "tcp")).concat(old_ports);
+ }
+
+ if (oldState.generate_custom_id) {
+ if (all_ports.length > 0)
+ newState.custom_id = "custom--" + all_ports.join('-');
+ else
+ newState.custom_id = "";
+ }
+
+ return newState;
+ });
+ }
+
+ onToggleType(event) {
+ this.setState({
+ custom: event.target.value === "ports"
+ });
+ }
+
+ componentDidMount() {
+ firewall.getAvailableServices()
+ .then(services => this.setState({ services }));
+ cockpit.file('/etc/services').read()
+ .then(content => this.setState({
+ avail_services: this.parseServices(content)
+ }));
+ }
+
+ onFilterChanged(value) {
+ this.setState({ filter: value.toLowerCase() });
+ }
+
+ render() {
+ const Dialogs = this.context;
+ let services;
+ if (this.state.filter && this.state.services && !isNaN(this.state.filter))
+ services = this.state.services.filter(s => {
+ for (const port of s.ports)
+ if (port.port === this.state.filter)
+ return true;
+ return false;
+ });
+ else if (this.state.filter && this.state.services)
+ services = this.state.services.filter(s => s.id.indexOf(this.state.filter) > -1);
+ else
+ services = this.state.services;
+
+ // hide services which have been enabled in the zone
+ if (services)
+ services = services.filter(s => firewall.zones[this.props.zoneId].services.indexOf(s.id) === -1);
+
+ let addText = "";
+ let titleText = "";
+ if (this.props.custom_id) {
+ addText = _("Edit service");
+ titleText = cockpit.format(_("Edit custom service in $0 zone"), this.props.zoneName);
+ } else {
+ addText = this.state.custom ? _("Add ports") : _("Add services");
+ titleText = this.state.custom ? cockpit.format(_("Add ports to $0 zone"), this.props.zoneName) : cockpit.format(_("Add services to $0 zone"), this.props.zoneName);
+ }
+
+ return (
+ <Modal id="add-services-dialog" isOpen
+ position="top" variant="medium"
+ onClose={Dialogs.close}
+ title={titleText}
+ footer={<>
+ { !this.state.custom ||
+ <Alert variant="warning"
+ isInline
+ title={_("Adding custom ports will reload firewalld. A reload will result in the loss of any runtime-only configuration!")} />
+ }
+ <Button variant='primary' isDisabled={(this.state.custom && this.checkNullValues()) || (!this.state.custom && !this.state.selected.size)} onClick={this.props.custom_id ? this.edit : this.save} aria-label={titleText}>
+ {addText}
+ </Button>
+ <Button variant='link' className='btn-cancel' onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>}
+ >
+ <Form isHorizontal onSubmit={this.props.custom_id ? this.edit : this.save}>
+ {
+ this.state.dialogError && <ModalError dialogError={this.state.dialogError} dialogErrorDetail={this.state.dialogErrorDetail} />
+ }
+ { !!this.props.custom_id ||
+ <FormGroup className="add-services-dialog-type" isInline>
+ <Radio name="type"
+ id="add-services-dialog--services"
+ value="services"
+ isChecked={!this.state.custom}
+ onChange={this.onToggleType}
+ label={_("Services")} />
+ <Radio name="type"
+ id="add-services-dialog--ports"
+ value="ports"
+ isChecked={this.state.custom}
+ onChange={this.onToggleType}
+ isDisabled={this.state.avail_services == null}
+ label={_("Custom ports")} />
+ </FormGroup>
+ }
+ { this.state.custom ||
+ <div>
+ { services
+ ? (
+ <>
+ <SearchInput id="filter-services-input"
+ value={this.state.filter}
+ onChange={this.onFilterChanged} />
+ <DataList className="service-list" isCompact>
+ {services.map(s => (
+ <DataListItem key={s.id} aria-labelledby={s.id}>
+ <DataListItemRow>
+ <DataListCheck aria-labelledby={s.id}
+ isChecked={this.state.selected.has(s.id)}
+ onChange={(event, value) => this.onToggleService(event, s.id)}
+ id={"firewall-service-" + s.id}
+ name={s.id + "-checkbox"} />
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell key="service-list-item">
+ <label htmlFor={"firewall-service-" + s.id}
+ className="service-list-iteam-heading">
+ {s.id}
+ </label>
+ {renderPorts(s)}
+ </DataListCell>,
+ ]} />
+ </DataListItemRow>
+ </DataListItem>
+ ))}
+ </DataList>
+ </>
+ )
+ : (
+ <EmptyStatePanel loading />
+ )}
+ </div>
+ }
+ { !this.state.custom ||
+ <>
+ <FormGroup label="TCP">
+ <TextInput id="tcp-ports" type="text" onChange={this.validate}
+ validated={this.state.tcp_error ? "error" : "default"}
+ isDisabled={this.state.avail_services == null}
+ value={this.state.custom_tcp_value}
+ placeholder={_("Example: 22,ssh,8080,5900-5910")} />
+ <FormHelper helperTextInvalid={this.state.tcp_error} helperText={_("Comma-separated ports, ranges, and services are accepted")} />
+ </FormGroup>
+
+ <FormGroup label="UDP">
+ <TextInput id="udp-ports" type="text" onChange={this.validate}
+ validated={this.state.udp_error ? "error" : "default"}
+ isDisabled={this.state.avail_services == null}
+ value={this.state.custom_udp_value}
+ placeholder={_("Example: 88,2019,nfs,rsync")} />
+ <FormHelper helperTextInvalid={this.state.udp_error} helperText={_("Comma-separated ports, ranges, and services are accepted")} />
+ </FormGroup>
+
+ <FormGroup label={_("ID")}>
+ <TextInput id="service-name" onChange={(_event, value) => this.setId(value)} isDisabled={!!this.props.custom_id || this.state.avail_services == null}
+ value={this.state.custom_id} />
+ <FormHelper helperText={_("If left empty, ID will be generated based on associated port services and port numbers")} />
+ </FormGroup>
+
+ <FormGroup label={_("Description")}>
+ <TextInput id="service-description" onChange={(_event, value) => this.setDescription(value)} isDisabled={this.state.avail_services == null}
+ value={this.state.custom_description} />
+ </FormGroup>
+ </>
+ }
+ </Form>
+ </Modal>
+ );
+ }
+}
+
+class ActivateZoneModal extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor() {
+ super();
+
+ this.state = {
+ ipRange: "ip-entire-subnet",
+ ipRangeValue: null,
+ zone: null,
+ interfaces: new Set(),
+ dialogError: null,
+ dialogErrorDetail: null,
+ };
+ this.onFirewallChanged = this.onFirewallChanged.bind(this);
+ this.onInterfaceChange = this.onInterfaceChange.bind(this);
+ this.onChange = this.onChange.bind(this);
+ this.save = this.save.bind(this);
+ }
+
+ componentDidMount() {
+ firewall.addEventListener("changed", this.onFirewallChanged);
+ }
+
+ componentWillUnmount() {
+ firewall.removeEventListener("changed", this.onFirewallChanged);
+ }
+
+ onFirewallChanged() {
+ this.setState({});
+ }
+
+ onInterfaceChange(event) {
+ const int = event.target.value;
+ const enabled = event.target.checked;
+ this.setState(state => {
+ const interfaces = new Set(state.interfaces);
+ if (enabled)
+ interfaces.add(int);
+ else
+ interfaces.delete(int);
+ return { interfaces };
+ });
+ }
+
+ onChange(key, value) {
+ this.setState({ [key]: value });
+ }
+
+ save(event) {
+ const Dialogs = this.context;
+ let p;
+ if (firewall.zones[this.state.zone].services.indexOf("cockpit") === -1)
+ p = firewall.addService(this.state.zone, "cockpit");
+ else
+ p = Promise.resolve();
+
+ const sources = this.state.ipRange === "ip-range" ? this.state.ipRangeValue.split(",").map(ip => ip.trim()) : [];
+ p.then(() =>
+ firewall.activateZone(this.state.zone, [...this.state.interfaces], sources)
+ .then(Dialogs.close)
+ .catch(error => {
+ this.setState({
+ dialogError: _("Failed to add zone"),
+ dialogErrorDetail: error.name + ": " + error.message,
+ });
+ }));
+
+ if (event)
+ event.preventDefault();
+ return false;
+ }
+
+ render() {
+ const Dialogs = this.context;
+ const zones = Object.keys(firewall.zones).filter(z => firewall.zones[z].target === "default" && !firewall.activeZones.has(z));
+ const customZones = zones.filter(z => firewall.predefinedZones.indexOf(z) === -1);
+ const interfaces = firewall.availableInterfaces.filter(i => {
+ let inZone = false;
+ firewall.activeZones.forEach(z => {
+ inZone |= firewall.zones[z].interfaces.indexOf(i.device) !== -1;
+ });
+ return !inZone;
+ });
+ // https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceCapabilities
+ const NM_DEVICE_CAP_IS_SOFTWARE = 4;
+ const virtualDevices = interfaces.filter(i => (i.capabilities & NM_DEVICE_CAP_IS_SOFTWARE) !== 0 && i.device !== "lo").sort((a, b) => a.device.localeCompare(b.device));
+ const physicalDevices = interfaces.filter(i => ((i.capabilities & NM_DEVICE_CAP_IS_SOFTWARE) === 0) && i.device !== "lo").sort((a, b) => a.device.localeCompare(b.device));
+ return (
+ <Modal id="add-zone-dialog" isOpen
+ position="top" variant="medium"
+ onClose={Dialogs.close}
+ title={_("Add zone")}
+ footer={<>
+ <Button variant="primary" onClick={this.save} isDisabled={this.state.zone === null ||
+ (this.state.interfaces.size === 0 && this.state.ipRange === "ip-entire-subnet") ||
+ (this.state.ipRange === "ip-range" && !this.state.ipRangeValue)}>
+ { _("Add zone") }
+ </Button>
+ <Button variant="link" className="btn-cancel" onClick={Dialogs.close}>
+ { _("Cancel") }
+ </Button>
+ </>}
+ >
+ <Form isHorizontal onSubmit={this.save}>
+ {
+ this.state.dialogError && <ModalError dialogError={this.state.dialogError} dialogErrorDetail={this.state.dialogErrorDetail} />
+ }
+ <FormGroup label={ _("Trust level") } className="add-zone-zones">
+ <Flex>
+ <FlexItem className="add-zone-zones-firewalld">
+ <legend>{ _("Sorted from least to most trusted") }</legend>
+ { zones.filter(z => firewall.predefinedZones.indexOf(z) !== -1).sort((a, b) => firewall.predefinedZones.indexOf(a) - firewall.predefinedZones.indexOf(b))
+ .map(z =>
+ <Radio key={z} id={z} name="zone" value={z}
+ isChecked={this.state.zone == z}
+ onChange={e => this.onChange("zone", e.target.value)}
+ label={ firewall.zones[z].id } />
+ )}
+ </FlexItem>
+ <FlexItem className="add-zone-zones-custom">
+ { customZones.length > 0 && <legend>{ _("Custom zones") }</legend> }
+ { customZones.map(z =>
+ <Radio key={z} id={z} name="zone" value={z}
+ isChecked={this.state.zone == z}
+ onChange={e => this.onChange("zone", e.target.value)}
+ label={ firewall.zones[z].id } />
+ )}
+ </FlexItem>
+ </Flex>
+ </FormGroup>
+
+ <FormGroup label={ _("Description") }>
+ <p id="add-zone-description-readonly">
+ { (this.state.zone && firewall.zones[this.state.zone].description) || _("No description available") }
+ </p>
+ </FormGroup>
+
+ <FormGroup label={ _("Included services") } hasNoPaddingTop>
+ <div id="add-zone-services-readonly">
+ { (this.state.zone && firewall.zones[this.state.zone].services.join(", ")) || _("None") }
+ </div>
+ <FormHelper helperText={_("The cockpit service is automatically included")} />
+ </FormGroup>
+
+ <FormGroup label={ _("Interfaces") } hasNoPaddingTop isInline>
+ { physicalDevices.map(i =>
+ <Checkbox key={i.device}
+ id={i.device}
+ value={i.device}
+ onChange={(event, value) => this.onInterfaceChange(event)}
+ isChecked={this.state.interfaces.has(i.device)}
+ label={i.device} />) }
+ { virtualDevices.map(i =>
+ <Checkbox key={i.device}
+ id={i.device}
+ value={i.device}
+ onChange={(event, value) => this.onInterfaceChange(event)}
+ isChecked={this.state.interfaces.has(i.device)}
+ label={i.device} />) }
+ </FormGroup>
+
+ <FormGroup label={ _("Allowed addresses") } hasNoPaddingTop isInline>
+ <Radio name="add-zone-ip"
+ isChecked={this.state.ipRange == "ip-entire-subnet"}
+ value="ip-entire-subnet"
+ id="ip-entire-subnet"
+ onChange={e => this.onChange("ipRange", e.target.value)}
+ label={ _("Entire subnet") } />
+ <Radio name="add-zone-ip"
+ isChecked={this.state.ipRange == "ip-range"}
+ value="ip-range"
+ id="ip-range"
+ onChange={e => this.onChange("ipRange", e.target.value)}
+ label={ _("Range") } />
+ {this.state.ipRange === "ip-range" && (
+ <>
+ <TextInput id="add-zone-ip" onChange={(_event, value) => this.onChange("ipRangeValue", value)} />
+ <FormHelperText>{_("IP address with routing prefix. Separate multiple values with a comma. Example: 192.0.2.0/24, 2001:db8::/32")}</FormHelperText>
+ </>
+ )}
+ </FormGroup>
+ </Form>
+ </Modal>
+ );
+ }
+}
+
+function DeleteConfirmationModal(props) {
+ return (
+ <Modal id="delete-confirmation-dialog" isOpen
+ position="top" variant="medium"
+ onClose={props.onCancel}
+ title={props.title}
+ footer={<>
+ <Button variant="danger" onClick={props.onDelete} aria-label={cockpit.format(_("Confirm removal of $0"), props.target)}>
+ { _("Delete") }
+ </Button>
+ <Button variant="link" className="btn-cancel" onClick={props.onCancel}>
+ { _("Cancel") }
+ </Button>
+ </>}
+ >
+ {props.body && <Alert variant="warning" isInline title={props.body} />}
+ </Modal>
+ );
+}
+
+export class Firewall extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor() {
+ super();
+
+ this.state = {
+ firewall,
+ pendingTarget: null /* `null` for not pending */
+ };
+
+ this.onFirewallChanged = this.onFirewallChanged.bind(this);
+ this.openServicesDialog = this.openServicesDialog.bind(this);
+ this.openAddZoneDialog = this.openAddZoneDialog.bind(this);
+ this.onRemoveZone = this.onRemoveZone.bind(this);
+ this.onRemoveService = this.onRemoveService.bind(this);
+ this.onEditService = this.onEditService.bind(this);
+ }
+
+ onFirewallChanged() {
+ this.setState((prevState) => {
+ if (prevState.pendingTarget === firewall.enabled)
+ return { firewall, pendingTarget: null };
+
+ return { firewall };
+ });
+ }
+
+ onRemoveZone(zone) {
+ const Dialogs = this.context;
+ let body;
+ if (firewall.zones[zone].services.indexOf("cockpit") !== -1)
+ body = _("This zone contains the cockpit service. Make sure that this zone does not apply to your current web console connection.");
+ else
+ body = _("Removing the zone will remove all services within it.");
+ Dialogs.show(<DeleteConfirmationModal title={ cockpit.format(_("Remove zone $0"), zone) }
+ body={body}
+ target={zone}
+ onCancel={Dialogs.close}
+ onDelete={ () => {
+ firewall.deactiveateZone(zone);
+ Dialogs.close();
+ }} />
+ );
+ }
+
+ onRemoveService(zone, service) {
+ const Dialogs = this.context;
+ if (service === 'cockpit') {
+ const body = _("Removing the cockpit service might result in the web console becoming unreachable. Make sure that this zone does not apply to your current web console connection.");
+ Dialogs.show(<DeleteConfirmationModal title={ cockpit.format(_("Remove $0 service from $1 zone"), service, zone) }
+ body={body}
+ target={service}
+ onCancel={Dialogs.close}
+ onDelete={ () => {
+ firewall.removeService(zone, service);
+ Dialogs.close();
+ }} />
+ );
+ } else {
+ firewall.removeService(zone, service);
+ }
+ }
+
+ onEditService(zone, service) {
+ const tcp_ports = [];
+ const udp_ports = [];
+ service.ports.forEach(port => {
+ if (port.protocol === "tcp")
+ tcp_ports.push(port.port);
+ else
+ udp_ports.push(port.port);
+ });
+
+ const zone_name = zone.name ? zone.name : upperCaseFirstLetter(zone.id);
+
+ const Dialogs = this.context;
+ Dialogs.show(<AddEditServicesModal zoneId={zone.id} zoneName={zone_name} custom_id={service.id}
+ custom_tcp_ports={tcp_ports} custom_udp_ports={udp_ports} custom_description={service.description}
+ custom_tcp_value={tcp_ports.join(", ")} custom_udp_value={udp_ports.join(", ")} />);
+ }
+
+ componentDidMount() {
+ firewall.addEventListener("changed", this.onFirewallChanged);
+ }
+
+ componentWillUnmount() {
+ firewall.removeEventListener("changed", this.onFirewallChanged);
+ }
+
+ openServicesDialog(zoneId, zoneName) {
+ const Dialogs = this.context;
+ Dialogs.show(<AddEditServicesModal zoneId={zoneId} zoneName={zoneName} />);
+ }
+
+ openAddZoneDialog() {
+ const Dialogs = this.context;
+ Dialogs.show(<ActivateZoneModal />);
+ }
+
+ render() {
+ function go_up(event) {
+ cockpit.jump("/network", cockpit.transport.host);
+ }
+
+ if (!this.state.firewall.installed) {
+ return <EmptyStatePanel title={ _("Firewall is not available") }
+ paragraph={ cockpit.format(_("Please install the $0 package"), "firewalld") }
+ icon={ ExclamationCircleIcon }
+ />;
+ }
+
+ if (!this.state.firewall.ready)
+ return <EmptyStatePanel loading />;
+
+ const addZoneAction = (
+ <Button variant="primary" onClick={this.openAddZoneDialog} id="add-zone-button" aria-label={_("Add a new zone")}>
+ {_("Add new zone")}
+ </Button>
+ );
+
+ const zones = [...this.state.firewall.activeZones].sort((z1, z2) =>
+ z1 === firewall.defaultZone ? -1 : z2 === firewall.defaultZone ? 1 : 0
+ ).map(id => this.state.firewall.zones[id]);
+
+ const enabled = this.state.firewall.enabled;
+
+ return (
+ <Page>
+ <PageBreadcrumb stickyOnBreakpoint={{ default: "top" }}>
+ <Breadcrumb>
+ <BreadcrumbItem onClick={go_up} className="pf-v5-c-breadcrumb__link">{_("Networking")}</BreadcrumbItem>
+ <BreadcrumbItem isActive>{_("Firewall")}</BreadcrumbItem>
+ </Breadcrumb>
+ </PageBreadcrumb>
+ <PageSection id="firewall-heading" variant={PageSectionVariants.light} className="firewall-heading">
+ <Flex alignItems={{ default: 'alignItemsCenter' }} justifyContent={{ default: 'justifyContentSpaceBetween' }}>
+ <Flex alignItems={{ default: 'alignItemsCenter' }} id="firewall-heading-title-group">
+ <Title headingLevel="h2" size="3xl">
+ {_("Firewall")}
+ </Title>
+ <FirewallSwitch firewall={firewall} />
+ <p>{_("Incoming requests are blocked by default. Outgoing requests are not blocked.")}</p>
+ </Flex>
+ { enabled && !firewall.readonly && <span className="btn-group">{addZoneAction}</span> }
+ </Flex>
+ </PageSection>
+ <PageSection id="zones-listing">
+ { enabled && <Stack hasGutter>
+ {
+ zones.map(z => <ZoneSection key={z.id}
+ zone={z}
+ openServicesDialog={this.openServicesDialog}
+ readonly={this.state.firewall.readonly}
+ onRemoveZone={this.onRemoveZone}
+ onEditService={this.onEditService}
+ onRemoveService={this.onRemoveService} />
+ )
+ }
+ </Stack> }
+ </PageSection>
+ </Page>
+ );
+ }
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.title = cockpit.gettext(document.title);
+ const root = createRoot(document.getElementById("firewall"));
+ root.render(<WithDialogs><Firewall /></WithDialogs>);
+});
diff --git a/pkg/networkmanager/helpers.js b/pkg/networkmanager/helpers.js
new file mode 100644
index 0000000..28b0deb
--- /dev/null
+++ b/pkg/networkmanager/helpers.js
@@ -0,0 +1,58 @@
+import cockpit from "cockpit";
+
+export class UsageMonitor {
+ constructor() {
+ this.channel = cockpit.metrics(
+ 1000,
+ [
+ {
+ source: "direct",
+ metrics: [
+ {
+ name: "network.interface.in.bytes",
+ units: "bytes",
+ derive: "rate"
+ },
+ {
+ name: "network.interface.out.bytes",
+ units: "bytes",
+ derive: "rate"
+ },
+ ],
+ metrics_path_names: ["rx", "tx"]
+ },
+ {
+ source: "internal",
+ metrics: [
+ {
+ name: "network.interface.rx",
+ units: "bytes",
+ derive: "rate"
+ },
+ {
+ name: "network.interface.tx",
+ units: "bytes",
+ derive: "rate"
+ },
+ ],
+ metrics_path_names: ["rx", "tx"]
+ }
+ ]
+ );
+
+ this.grid = cockpit.grid(1000, -1, -0);
+ this.samples = { };
+
+ this.channel.follow();
+ this.grid.walk();
+ }
+
+ add(iface) {
+ if (!this.samples[iface]) {
+ this.samples[iface] = [
+ this.grid.add(this.channel, ["rx", iface]),
+ this.grid.add(this.channel, ["tx", iface])
+ ];
+ }
+ }
+}
diff --git a/pkg/networkmanager/index.html b/pkg/networkmanager/index.html
new file mode 100644
index 0000000..a2ca810
--- /dev/null
+++ b/pkg/networkmanager/index.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html id="networkmanager-page">
+<head>
+ <title translate="yes">Networking</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="networkmanager.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../manifests.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+ <script src="networkmanager.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+
+ <div id="network-page" class="ct-page-fill network-page">
+ </div>
+
+</body>
+</html>
diff --git a/pkg/networkmanager/interfaces.js b/pkg/networkmanager/interfaces.js
new file mode 100644
index 0000000..181362a
--- /dev/null
+++ b/pkg/networkmanager/interfaces.js
@@ -0,0 +1,1822 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React from "react";
+import cockpit from 'cockpit';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+
+import { fmt_to_fragments } from 'utils.jsx';
+import * as utils from './utils.js';
+import { v4 as uuidv4 } from 'uuid';
+
+import "./networking.scss";
+
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+function show_error_dialog(title, message) {
+ const props = {
+ id: "error-popup",
+ title,
+ body: <p>{message}</p>
+ };
+
+ const footer = {
+ actions: [],
+ cancel_button: { text: _("Close"), variant: "secondary" }
+ };
+
+ show_modal_dialog(props, footer);
+}
+
+export function show_unexpected_error(error) {
+ show_error_dialog(_("Unexpected error"), error.message || error);
+}
+
+function show_breaking_change_dialog({ fail_text, anyway_text, action }) {
+ const props = {
+ titleIconVariant: "warning",
+ id: "confirm-breaking-change-popup",
+ title: _("Connection will be lost"),
+ body: <p>{fail_text}</p>
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: anyway_text,
+ clicked: action,
+ style: "danger",
+ }
+ ],
+ cancel_button: { text: _("Keep connection"), variant: "secondary" }
+ };
+
+ show_modal_dialog(props, footer);
+}
+
+export function connection_settings(c) {
+ if (c && c.Settings && c.Settings.connection) {
+ return c.Settings.connection;
+ } else {
+ // It is a programming error if we ever access a Connection
+ // object that doesn't have it's settings yet, and we expect
+ // each Connection object to have "connection" settings.
+ console.warn("Incomplete 'Connection' object accessed", c);
+ // HACK - phantomjs console.trace() prints nothing
+ try { throw new Error() } catch (e) { console.log(e.stack) }
+ return { };
+ }
+}
+
+/* NetworkManagerModel
+ *
+ * The NetworkManager model maintains a mostly-read-only data
+ * structure that represents the state of the NetworkManager service
+ * on a given machine.
+ *
+ * The data structure consists of JavaScript values such as objects,
+ * arrays, and strings that point at each other. It might have
+ * cycles. In general, it follows the NetworkManager D-Bus API but
+ * tries to hide annoyances such as endian issues.
+ *
+ * For example,
+ *
+ * const manager = model.get_manager();
+ * manager.Devices[0].ActiveConnection.Ipv4Config.Addresses[0][0]
+ *
+ * is the first IPv4 address of the first device as a string.
+ *
+ * The model initializes itself asynchronously and emits the 'changed'
+ * event whenever anything changes. If you only access the data
+ * structure from within the 'changed' event handler, you should
+ * always see it in a complete state.
+ *
+ * In other words, any change in the data structure from one 'changed'
+ * event to the next represents a real change in the state of
+ * NetworkManager.
+ *
+ * When a new model is created, its main 'manager' object starts out
+ * as 'null'. The first 'changed' event signals that initialization
+ * is complete and that the whole data structure is now stable and
+ * reachable from the 'manager' object.
+ *
+ * Methods are invoked directly on the objects in the data structure.
+ * For example,
+ *
+ * manager.Devices[0].disconnect();
+ * manager.Devices[0].ActiveConnection.deactivate();
+ *
+ * TODO - document the details of the data structure.
+ */
+
+/* HACK
+ *
+ * NetworkManager doesn't implement the standard o.fd.DBus.Properties
+ * interface.
+ *
+ * 1) NM does not emit the PropertiesChanged signal on the
+ * o.fd.DBus.Properties interface but rather on its own interfaces
+ * like o.fd.NetworkManager.Device.Wired.
+ *
+ * 2) NM does not always emit the PropertiesChanged signal on the
+ * interface whose properties have changed. For example, when a
+ * property on o.fd.NM.Device changes, this might be notified by a
+ * PropertiesChanged signal on the o.fd.NM.Device.Wired interface
+ * for the same object path.
+ *
+ * https://bugzilla.gnome.org/show_bug.cgi?id=729826
+ *
+ * We cope with this here by merging all properties of all interfaces
+ * for a given object path. This is appropriate and nice for
+ * NetworkManager, and we should probably keep it that way even if
+ * NetworkManager would use a standard o.fd.DBus.Properties API.
+ */
+
+export function NetworkManagerModel() {
+ /*
+ * The NetworkManager model doesn't need proxies in its DBus client.
+ * It uses the 'raw' dbus events and methods and constructs its own data
+ * structure. This has the advantage of avoiding wasting
+ * resources for maintaining the unused proxies, avoids some code
+ * complexity, and allows to do the right thing with the
+ * peculiarities of the NetworkManager API.
+ */
+
+ const self = this;
+ cockpit.event_target(self);
+
+ const client = cockpit.dbus("org.freedesktop.NetworkManager", { superuser: "try" });
+ self.client = client;
+
+ /* resolved once first stage of initialization is done */
+ self.preinit = new Promise((resolve, reject) => {
+ client.call("/org/freedesktop/NetworkManager",
+ "org.freedesktop.DBus.Properties", "Get",
+ ["org.freedesktop.NetworkManager", "State"], { flags: "" })
+ .then((reply, options) => {
+ if (options.flags) {
+ if (options.flags.indexOf(">") !== -1)
+ utils.set_byteorder("be");
+ else if (options.flags.indexOf("<") !== -1)
+ utils.set_byteorder("le");
+ resolve();
+ }
+ })
+ .catch(complain);
+ });
+
+ /* Mostly generic D-Bus stuff. */
+
+ const objects = { };
+
+ self.set_curtain = (state) => {
+ self.curtain = state;
+ self.dispatchEvent("changed");
+ };
+
+ /* This is a test helper so that we wait for operations to finish before moving forward with the test */
+ self.set_operation_in_progress = (value) => {
+ self.operationInProgress = value;
+ self.dispatchEvent("changed");
+ };
+
+ function complain() {
+ self.ready = false;
+ console.warn.apply(console, arguments);
+ }
+
+ function conv_Object(type) {
+ return function (path) {
+ return get_object(path, type);
+ };
+ }
+
+ function conv_Array(conv) {
+ return function (elts) {
+ return elts.map(conv);
+ };
+ }
+
+ function priv(obj) {
+ return obj[' priv'];
+ }
+
+ let outstanding_refreshes = 0;
+
+ function push_refresh() {
+ outstanding_refreshes += 1;
+ }
+
+ function pop_refresh() {
+ outstanding_refreshes -= 1;
+ if (outstanding_refreshes === 0)
+ export_model();
+ }
+
+ function get_object(path, type) {
+ if (path == "/")
+ return null;
+ function Constructor() {
+ this[' priv'] = { };
+ priv(this).type = type;
+ priv(this).path = path;
+ for (const p in type.props)
+ this[p] = type.props[p].def;
+ }
+ if (!objects[path]) {
+ Constructor.prototype = type.prototype;
+ objects[path] = new Constructor();
+ if (type.refresh)
+ type.refresh(objects[path]);
+ if (type.exporters && type.exporters[0])
+ type.exporters[0](objects[path]);
+ }
+ return objects[path];
+ }
+
+ function peek_object(path) {
+ return objects[path] || null;
+ }
+
+ function drop_object(path) {
+ const obj = objects[path];
+ if (obj) {
+ if (priv(obj).type.drop)
+ priv(obj).type.drop(obj);
+ delete objects[path];
+ export_model();
+ }
+ }
+
+ function set_object_properties(obj, props) {
+ const decl = priv(obj).type.props;
+ for (const p in decl) {
+ let val = props[decl[p].prop || p];
+ if (val !== undefined) {
+ if (decl[p].conv)
+ val = decl[p].conv(val);
+ if (val !== obj[p]) {
+ obj[p] = val;
+ if (decl[p].trigger)
+ decl[p].trigger(obj);
+ }
+ }
+ }
+ }
+
+ function remove_signatures(props_with_sigs) {
+ const props = { };
+ for (const p in props_with_sigs) {
+ if (props_with_sigs[p]) {
+ props[p] = props_with_sigs[p].v;
+ }
+ }
+ return props;
+ }
+
+ function objpath(obj) {
+ if (obj && priv(obj).path)
+ return priv(obj).path;
+ else
+ return "/";
+ }
+
+ function call_object_method(obj, iface, method) {
+ return client.call(objpath(obj), iface, method, Array.prototype.slice.call(arguments, 3));
+ }
+
+ const interface_types = { };
+ let max_export_phases = 0;
+ let export_pending;
+
+ function set_object_types(all_types) {
+ all_types.forEach(function (type) {
+ if (type.exporters && type.exporters.length > max_export_phases)
+ max_export_phases = type.exporters.length;
+ type.interfaces.forEach(function (iface) {
+ interface_types[iface] = type;
+ });
+ });
+ }
+
+ function signal_emitted(path, iface, signal, args) {
+ const obj = peek_object(path);
+
+ if (obj) {
+ const type = priv(obj).type;
+
+ if (signal == "PropertiesChanged") {
+ push_refresh();
+ set_object_properties(obj, remove_signatures(args[0]));
+ pop_refresh();
+ } else if (type.signals && type.signals[signal])
+ type.signals[signal](obj, args);
+ }
+ }
+
+ function interface_properties(path, iface, props) {
+ const type = interface_types[iface];
+ if (type)
+ set_object_properties(get_object(path, type), props);
+ }
+
+ function interface_removed(path, iface) {
+ /* For NetworkManager we can make this assumption */
+ drop_object(path);
+ }
+
+ let export_model_promise = null;
+ let export_model_promise_resolve = null;
+
+ function export_model() {
+ function doit() {
+ for (let phase = 0; phase < max_export_phases; phase++) {
+ for (const path in objects) {
+ const obj = objects[path];
+ const exp = priv(obj).type.exporters;
+ if (exp && exp[phase])
+ exp[phase](obj);
+ }
+ }
+
+ self.ready = true;
+ self.dispatchEvent('changed');
+ if (export_model_promise) {
+ export_model_promise_resolve();
+ export_model_promise = null;
+ export_model_promise_resolve = null;
+ }
+ }
+
+ if (!export_pending) {
+ export_pending = true;
+ window.setTimeout(function () { export_pending = false; doit() }, 300);
+ }
+ }
+
+ self.synchronize = function synchronize() {
+ if (outstanding_refreshes === 0) {
+ return Promise.resolve();
+ } else {
+ if (!export_model_promise)
+ export_model_promise = new Promise(resolve => { export_model_promise_resolve = resolve });
+ return export_model_promise;
+ }
+ };
+
+ let subscription;
+ let watch;
+
+ function onNotifyEventHandler(event, data) {
+ Object.keys(data).forEach(path => {
+ const interfaces = data[path];
+
+ Object.keys(interfaces).forEach(iface => {
+ const props = interfaces[iface];
+
+ if (props)
+ interface_properties(path, iface, props);
+ else
+ interface_removed(path, iface);
+ });
+ });
+ }
+
+ self.preinit.then(() => {
+ subscription = client.subscribe({ }, signal_emitted);
+ client.addEventListener("notify", onNotifyEventHandler);
+ watch = client.watch({ path_namespace: "/org/freedesktop" });
+ client.addEventListener("owner", (event, owner) => {
+ if (owner) {
+ watch.remove();
+ watch = client.watch({ path_namespace: "/org/freedesktop" });
+ }
+ });
+ });
+
+ self.close = function close() {
+ subscription.remove();
+ watch.remove();
+ client.removeEventListener("notify", onNotifyEventHandler);
+ client.close("unused");
+ };
+
+ /* NetworkManager specific data conversions and utility functions.
+ */
+
+ function ip4_address_from_nm(addr) {
+ return [utils.ip4_to_text(addr[0]),
+ utils.ip_prefix_to_text(addr[1]),
+ utils.ip4_to_text(addr[2], true)
+ ];
+ }
+
+ function ip4_address_to_nm(addr) {
+ return [utils.ip4_from_text(addr[0]),
+ utils.ip4_prefix_from_text(addr[1]),
+ utils.ip4_from_text(addr[2], true)
+ ];
+ }
+
+ function ip4_route_from_nm(addr) {
+ return [utils.ip4_to_text(addr[0]),
+ utils.ip_prefix_to_text(addr[1]),
+ utils.ip4_to_text(addr[2], true),
+ utils.ip_metric_to_text(addr[3])
+ ];
+ }
+
+ function ip4_route_to_nm(addr) {
+ return [utils.ip4_from_text(addr[0]),
+ utils.ip4_prefix_from_text(addr[1]),
+ utils.ip4_from_text(addr[2], true),
+ utils.ip_metric_from_text(addr[3])
+ ];
+ }
+ function ip6_address_from_nm(addr) {
+ return [utils.ip6_to_text(addr[0]),
+ utils.ip_prefix_to_text(addr[1]),
+ utils.ip6_to_text(addr[2], true)
+ ];
+ }
+
+ function ip6_address_to_nm(addr) {
+ return [utils.ip6_from_text(addr[0]),
+ parseInt(addr[1], 10) || 64,
+ utils.ip6_from_text(addr[2], true)
+ ];
+ }
+
+ function ip6_route_from_nm(addr) {
+ return [utils.ip6_to_text(addr[0]),
+ utils.ip_prefix_to_text(addr[1]),
+ utils.ip6_to_text(addr[2], true),
+ utils.ip_metric_to_text(addr[3]),
+ ];
+ }
+
+ function ip6_route_to_nm(addr) {
+ return [utils.ip6_from_text(addr[0]),
+ utils.ip_prefix_from_text(addr[1]),
+ utils.ip6_from_text(addr[2], true),
+ utils.ip_metric_from_text(addr[3])
+ ];
+ }
+
+ function settings_from_nm(settings) {
+ function get(first, second, def) {
+ if (settings[first] && settings[first][second])
+ return settings[first][second].v;
+ else
+ return def;
+ }
+
+ function get_ip(first, addr_from_nm, route_from_nm, ip_to_text) {
+ return {
+ method: get(first, "method", "auto"),
+ ignore_auto_dns: get(first, "ignore-auto-dns", false),
+ ignore_auto_routes: get(first, "ignore-auto-routes", false),
+ addresses: get(first, "addresses", []).map(addr_from_nm),
+ dns: get(first, "dns", []).map(ip_to_text),
+ dns_search: get(first, "dns-search", []),
+ routes: get(first, "routes", []).map(route_from_nm)
+ };
+ }
+
+ const result = {
+ connection: {
+ type: get("connection", "type"),
+ uuid: get("connection", "uuid"),
+ interface_name: get("connection", "interface-name"),
+ timestamp: get("connection", "timestamp", 0),
+ id: get("connection", "id", _("Unknown")),
+ autoconnect: get("connection", "autoconnect", true),
+ autoconnect_members:
+ get("connection", "autoconnect-slaves", -1),
+ member_type: get("connection", "slave-type"),
+ group: get("connection", "master")
+ }
+ };
+
+ if (!settings.connection.master) {
+ result.ipv4 = get_ip("ipv4", ip4_address_from_nm, ip4_route_from_nm, utils.ip4_to_text);
+ result.ipv6 = get_ip("ipv6", ip6_address_from_nm, ip6_route_from_nm, utils.ip6_to_text);
+ }
+
+ if (settings["802-3-ethernet"]) {
+ result.ethernet = {
+ mtu: get("802-3-ethernet", "mtu"),
+ assigned_mac_address: get("802-3-ethernet", "assigned-mac-address")
+ };
+ }
+
+ if (settings.bond) {
+ /* Options are documented as part of the Linux bonding driver.
+ https://www.kernel.org/doc/Documentation/networking/bonding.txt
+ */
+ result.bond = {
+ options: { ...get("bond", "options", { }) },
+ interface_name: get("bond", "interface-name")
+ };
+ }
+
+ function JSON_parse_carefully(str) {
+ try {
+ return JSON.parse(str);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ if (settings.team) {
+ result.team = {
+ config: JSON_parse_carefully(get("team", "config", "{}")),
+ interface_name: get("team", "interface-name")
+ };
+ }
+
+ if (settings["team-port"] || result.connection.member_type == "team") {
+ result.team_port = { config: JSON_parse_carefully(get("team-port", "config", "{}")), };
+ }
+
+ if (settings.bridge) {
+ result.bridge = {
+ interface_name: get("bridge", "interface-name"),
+ stp: get("bridge", "stp", true),
+ priority: get("bridge", "priority", 32768),
+ forward_delay: get("bridge", "forward-delay", 15),
+ hello_time: get("bridge", "hello-time", 2),
+ max_age: get("bridge", "max-age", 20),
+ ageing_time: get("bridge", "ageing-time", 300)
+ };
+ }
+
+ if (settings["bridge-port"] || result.connection.member_type == "bridge") {
+ result.bridge_port = {
+ priority: get("bridge-port", "priority", 32),
+ path_cost: get("bridge-port", "path-cost", 100),
+ hairpin_mode: get("bridge-port", "hairpin-mode", false)
+ };
+ }
+
+ if (settings.vlan) {
+ result.vlan = {
+ parent: get("vlan", "parent"),
+ id: get("vlan", "id"),
+ interface_name: get("vlan", "interface-name")
+ };
+ }
+
+ if (settings.wireguard) {
+ result.wireguard = {
+ listen_port: get("wireguard", "listen-port", 0),
+ peers: get("wireguard", "peers", []).map(peer => ({
+ publicKey: peer['public-key'].v,
+ endpoint: peer.endpoint?.v, // enpoint of a peer is optional
+ allowedIps: peer['allowed-ips']?.v
+ })),
+ };
+ }
+
+ return result;
+ }
+
+ function settings_to_nm(settings, orig) {
+ const result = JSON.parse(JSON.stringify(orig || { }));
+
+ function set(first, second, sig, val, def) {
+ if (val === undefined)
+ val = def;
+ if (!result[first])
+ result[first] = { };
+ if (val !== undefined)
+ result[first][second] = cockpit.variant(sig, val);
+ else
+ delete result[first][second];
+ }
+
+ function set_ip(first, addrs_sig, addr_to_nm, routes_sig, route_to_nm, ips_sig, ip_from_text) {
+ set(first, "method", 's', settings[first].method);
+ set(first, "ignore-auto-dns", 'b', settings[first].ignore_auto_dns);
+ set(first, "ignore-auto-routes", 'b', settings[first].ignore_auto_routes);
+
+ const addresses = settings[first].addresses;
+ if (addresses)
+ set(first, "addresses", addrs_sig, addresses.map(addr_to_nm));
+
+ const dns = settings[first].dns;
+ if (dns)
+ set(first, "dns", ips_sig, dns.map(ip_from_text));
+ set(first, "dns-search", 'as', settings[first].dns_search);
+
+ const routes = settings[first].routes;
+ if (routes)
+ set(first, "routes", routes_sig, routes.map(route_to_nm));
+
+ // Never pass "address-labels" back to NetworkManager. It
+ // is documented as "internal only", but needs to somehow
+ // stay in sync with "addresses". By not passing it back
+ // we don't have to worry about that.
+ //
+ delete result[first]["address-labels"];
+ }
+
+ set("connection", "id", 's', settings.connection.id);
+ set("connection", "autoconnect", 'b', settings.connection.autoconnect);
+ set("connection", "autoconnect-slaves", 'i', settings.connection.autoconnect_members);
+ set("connection", "uuid", 's', settings.connection.uuid);
+ set("connection", "interface-name", 's', settings.connection.interface_name);
+ set("connection", "type", 's', settings.connection.type);
+ set("connection", "slave-type", 's', settings.connection.member_type);
+ set("connection", "master", 's', settings.connection.group);
+
+ if (settings.ipv4)
+ set_ip("ipv4", 'aau', ip4_address_to_nm, 'aau', ip4_route_to_nm, 'au', utils.ip4_from_text);
+ else
+ delete result.ipv4;
+
+ if (settings.ipv6)
+ set_ip("ipv6", 'a(ayuay)', ip6_address_to_nm, 'a(ayuayu)', ip6_route_to_nm, 'aay', utils.ip6_from_text);
+ else
+ delete result.ipv6;
+
+ if (settings.bond) {
+ set("bond", "options", 'a{ss}', settings.bond.options);
+ set("bond", "interface-name", 's', settings.bond.interface_name);
+ } else
+ delete result.bond;
+
+ if (settings.team) {
+ set("team", "config", 's', JSON.stringify(settings.team.config));
+ set("team", "interface-name", 's', settings.team.interface_name);
+ } else
+ delete result.team;
+
+ if (settings.team_port)
+ set("team-port", "config", 's', JSON.stringify(settings.team_port.config));
+ else
+ delete result["team-port"];
+
+ if (settings.bridge) {
+ set("bridge", "interface-name", 's', settings.bridge.interface_name);
+ set("bridge", "stp", 'b', settings.bridge.stp);
+ set("bridge", "priority", 'u', settings.bridge.priority);
+ set("bridge", "forward-delay", 'u', settings.bridge.forward_delay);
+ set("bridge", "hello-time", 'u', settings.bridge.hello_time);
+ set("bridge", "max-age", 'u', settings.bridge.max_age);
+ set("bridge", "ageing-time", 'u', settings.bridge.ageing_time);
+ } else
+ delete result.bridge;
+
+ if (settings.bridge_port) {
+ set("bridge-port", "priority", 'u', settings.bridge_port.priority);
+ set("bridge-port", "path-cost", 'u', settings.bridge_port.path_cost);
+ set("bridge-port", "hairpin-mode", 'b', settings.bridge_port.hairpin_mode);
+ } else
+ delete result["bridge-port"];
+
+ if (settings.vlan) {
+ set("vlan", "parent", 's', settings.vlan.parent);
+ set("vlan", "id", 'u', settings.vlan.id);
+ set("vlan", "interface-name", 's', settings.vlan.interface_name);
+ // '1' is the default, but we need to set it explicitly anyway.
+ set("vlan", "flags", 'u', 1);
+ } else
+ delete result.vlan;
+
+ if (settings.ethernet) {
+ set("802-3-ethernet", "mtu", 'u', settings.ethernet.mtu);
+ set("802-3-ethernet", "assigned-mac-address", 's', settings.ethernet.assigned_mac_address);
+ // Delete cloned-mac-address so that assigned-mac-address gets used.
+ delete result["802-3-ethernet"]["cloned-mac-address"];
+ } else
+ delete result["802-3-ethernet"];
+
+ if (settings.wireguard) {
+ set("wireguard", "private-key", "s", settings.wireguard.private_key);
+ set("wireguard", "listen-port", "u", settings.wireguard.listen_port);
+ set("wireguard", "peers", "aa{sv}", settings.wireguard.peers.map(peer => {
+ return {
+ "public-key": {
+ t: "s",
+ v: peer.publicKey
+ },
+ ...peer.endpoint
+ ? {
+ endpoint: {
+ t: "s",
+ v: peer.endpoint
+ }
+ }
+ : {},
+ "allowed-ips": {
+ t: "as",
+ v: peer.allowedIps
+ }
+ };
+ }));
+ } else {
+ delete result.wireguard;
+ }
+
+ return result;
+ }
+
+ function device_type_to_symbol(type) {
+ // This returns a string that is suitable for the connection.type field of
+ // Connection.Settings, except for "ethernet".
+ switch (type) {
+ case 0: return 'unknown';
+ case 1: return 'ethernet'; // 802-3-ethernet
+ case 2: return '802-11-wireless';
+ case 3: return 'unused1';
+ case 4: return 'unused2';
+ case 5: return 'bluetooth';
+ case 6: return '802-11-olpc-mesh';
+ case 7: return 'wimax';
+ case 8: return 'modem';
+ case 9: return 'infiniband';
+ case 10: return 'bond';
+ case 11: return 'vlan';
+ case 12: return 'adsl';
+ case 13: return 'bridge';
+ case 14: return 'generic';
+ case 15: return 'team';
+ case 16: return 'tun';
+ case 17: return 'ip_tunnel';
+ case 18: return 'macvlan';
+ case 19: return 'vxlan';
+ case 20: return 'veth';
+ case 21: return 'macsec';
+ case 22: return 'dummy';
+ case 23: return 'ppp';
+ case 24: return 'ovs_interface';
+ case 25: return 'ovs_port';
+ case 26: return 'ovs_bridge';
+ case 27: return 'wpan';
+ case 28: return '6lowpan';
+ case 29: return 'wireguard';
+ case 30: return 'wifi_p2p';
+ case 31: return 'vrf';
+ case 32: return 'loopback';
+ default: return '';
+ }
+ }
+
+ function device_state_to_text(state) {
+ switch (state) {
+ // NM_DEVICE_STATE_UNKNOWN
+ case 0: return "?";
+ // NM_DEVICE_STATE_UNMANAGED
+ case 10: return "";
+ // NM_DEVICE_STATE_UNAVAILABLE
+ case 20: return _("Not available");
+ // NM_DEVICE_STATE_DISCONNECTED
+ case 30: return _("Inactive");
+ // NM_DEVICE_STATE_PREPARE
+ case 40: return _("Preparing");
+ // NM_DEVICE_STATE_CONFIG
+ case 50: return _("Configuring");
+ // NM_DEVICE_STATE_NEED_AUTH
+ case 60: return _("Authenticating");
+ // NM_DEVICE_STATE_IP_CONFIG
+ case 70: return _("Configuring IP");
+ // NM_DEVICE_STATE_IP_CHECK
+ case 80: return _("Checking IP");
+ // NM_DEVICE_STATE_SECONDARIES
+ case 90: return _("Waiting");
+ // NM_DEVICE_STATE_ACTIVATED
+ case 100: return _("Active");
+ // NM_DEVICE_STATE_DEACTIVATING
+ case 110: return _("Deactivating");
+ // NM_DEVICE_STATE_FAILED
+ case 120: return _("Failed");
+ default: return "";
+ }
+ }
+
+ const connections_by_uuid = { };
+
+ function set_settings(obj, settings) {
+ if (obj.Settings && obj.Settings.connection && obj.Settings.connection.uuid)
+ delete connections_by_uuid[obj.Settings.connection.uuid];
+ obj.Settings = settings;
+ if (settings && settings.connection && settings.connection.uuid)
+ connections_by_uuid[settings.connection.uuid] = obj;
+ }
+
+ function refresh_settings(obj) {
+ push_refresh();
+ client.call(objpath(obj), "org.freedesktop.NetworkManager.Settings.Connection", "GetSettings")
+ .then(function(reply) {
+ const result = reply[0];
+ if (result) {
+ priv(obj).orig = result;
+ set_settings(obj, settings_from_nm(result));
+ }
+ })
+ .catch(complain)
+ .finally(pop_refresh);
+ }
+
+ function refresh_udev(obj) {
+ if (obj.Udi.indexOf("/sys/") !== 0)
+ return;
+
+ push_refresh();
+ cockpit.spawn(["udevadm", "info", obj.Udi], { err: 'message' })
+ .then(function(res) {
+ const props = { };
+ function snarf_prop(line, env, prop) {
+ const prefix = "E: " + env + "=";
+ if (line.indexOf(prefix) === 0) {
+ props[prop] = line.substr(prefix.length);
+ }
+ }
+ res.split('\n').forEach(function(line) {
+ snarf_prop(line, "ID_MODEL_FROM_DATABASE", "IdModel");
+ snarf_prop(line, "ID_VENDOR_FROM_DATABASE", "IdVendor");
+ });
+ set_object_properties(obj, props);
+ })
+ .catch(function(ex) {
+ /* udevadm info exits with 4 when device doesn't exist */
+ if (ex.exit_status !== 4) {
+ console.warn(ex.message);
+ console.warn(ex);
+ }
+ })
+ .finally(pop_refresh);
+ }
+
+ function handle_updated(obj) {
+ refresh_settings(obj);
+ }
+
+ /* NetworkManager specific object types, used by the generic D-Bus
+ * code and using the data conversion functions.
+ */
+
+ const type_Ipv4Config = {
+ interfaces: [
+ "org.freedesktop.NetworkManager.IP4Config"
+ ],
+
+ props: {
+ Addresses: { conv: conv_Array(ip4_address_from_nm), def: [] }
+ }
+ };
+
+ const type_Ipv6Config = {
+ interfaces: [
+ "org.freedesktop.NetworkManager.IP6Config"
+ ],
+
+ props: {
+ Addresses: { conv: conv_Array(ip6_address_from_nm), def: [] }
+ }
+ };
+
+ const type_Connection = {
+ interfaces: [
+ "org.freedesktop.NetworkManager.Settings.Connection"
+ ],
+
+ props: {
+ Unsaved: { }
+ },
+
+ signals: {
+ Updated: handle_updated
+ },
+
+ refresh: refresh_settings,
+
+ drop: function (obj) {
+ set_settings(obj, null);
+ },
+
+ prototype: {
+ copy_settings: function () {
+ return JSON.parse(JSON.stringify(this.Settings));
+ },
+
+ apply_settings: function (settings) {
+ const self = this;
+ try {
+ return call_object_method(self,
+ "org.freedesktop.NetworkManager.Settings.Connection", "Update",
+ settings_to_nm(settings, priv(self).orig))
+ .then(() => {
+ set_settings(self, settings);
+ });
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ },
+
+ activate: function (dev, specific_object) {
+ return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
+ "org.freedesktop.NetworkManager", "ActivateConnection",
+ objpath(this), objpath(dev), objpath(specific_object))
+ .then(([active_connection]) => active_connection);
+ },
+
+ delete_: function () {
+ return call_object_method(this, "org.freedesktop.NetworkManager.Settings.Connection", "Delete")
+ .then(() => undefined);
+ }
+ },
+
+ exporters: [
+ function (obj) {
+ obj.Groups = [];
+ obj.Members = [];
+ obj.Interfaces = [];
+ },
+
+ null,
+
+ null,
+
+ // Needs: type_Interface.Connections
+ //
+ // Sets: type_Connection.Members
+ // type_Connection.Groups
+ //
+ function (obj) {
+ // Most of the time, a connection has zero or one groups,
+ // but when a connection refers to its group by interface
+ // name, we might end up with more than one group
+ // connection so we just collect them all.
+ //
+ // TODO - Nail down how NM really handles this.
+
+ function check_con(con) {
+ const group_settings = connection_settings(con);
+ const my_settings = connection_settings(obj);
+ if (group_settings.type == my_settings.member_type) {
+ obj.Groups.push(con);
+ con.Members.push(obj);
+ }
+ }
+
+ const cs = connection_settings(obj);
+ if (cs.member_type) {
+ const group = connections_by_uuid[cs.group];
+ if (group) {
+ obj.Groups.push(group);
+ group.Members.push(obj);
+ } else {
+ const iface = peek_interface(cs.group);
+ if (iface) {
+ iface.Connections.forEach(check_con);
+ }
+ }
+ }
+ }
+ ]
+
+ };
+
+ const type_ActiveConnection = {
+ interfaces: [
+ "org.freedesktop.NetworkManager.Connection.Active"
+ ],
+
+ props: {
+ Connection: { conv: conv_Object(type_Connection) },
+ Ip4Config: { conv: conv_Object(type_Ipv4Config) },
+ Ip6Config: { conv: conv_Object(type_Ipv6Config) }
+ // See below for "Group"
+ },
+
+ prototype: {
+ deactivate: function() {
+ return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
+ "org.freedesktop.NetworkManager", "DeactivateConnection",
+ objpath(this))
+ .then(() => undefined);
+ }
+ }
+ };
+
+ const type_Device = {
+ interfaces: [
+ "org.freedesktop.NetworkManager.Device",
+ "org.freedesktop.NetworkManager.Device.Wired",
+ "org.freedesktop.NetworkManager.Device.Bond",
+ "org.freedesktop.NetworkManager.Device.Team",
+ "org.freedesktop.NetworkManager.Device.Bridge",
+ "org.freedesktop.NetworkManager.Device.Vlan"
+ ],
+
+ props: {
+ DeviceType: { conv: device_type_to_symbol },
+ Interface: { },
+ StateText: { prop: "State", conv: device_state_to_text, def: _("Unknown") },
+ State: { },
+ HwAddress: { },
+ AvailableConnections: { conv: conv_Array(conv_Object(type_Connection)), def: [] },
+ ActiveConnection: { conv: conv_Object(type_ActiveConnection) },
+ Ip4Config: { conv: conv_Object(type_Ipv4Config) },
+ Ip6Config: { conv: conv_Object(type_Ipv6Config) },
+ Udi: { trigger: refresh_udev },
+ IdVendor: { def: "" },
+ IdModel: { def: "" },
+ Driver: { def: "" },
+ Carrier: { def: true },
+ Speed: { },
+ Managed: { def: false },
+ // See below for "Members"
+ },
+
+ prototype: {
+ activate: function(connection, specific_object) {
+ return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
+ "org.freedesktop.NetworkManager", "ActivateConnection",
+ objpath(connection), objpath(this), objpath(specific_object))
+ .then(([active_connection]) => active_connection);
+ },
+
+ activate_with_settings: function(settings, specific_object) {
+ try {
+ return call_object_method(get_object("/org/freedesktop/NetworkManager", type_Manager),
+ "org.freedesktop.NetworkManager", "AddAndActivateConnection",
+ settings_to_nm(settings), objpath(this), objpath(specific_object))
+ .then(([path, active_connection]) => active_connection);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ },
+
+ disconnect: function () {
+ return call_object_method(this, 'org.freedesktop.NetworkManager.Device', 'Disconnect')
+ .then(() => undefined);
+ }
+ }
+ };
+
+ // The 'Interface' type does not correspond to any NetworkManager
+ // object or interface. We use it to represent a network device
+ // that might or might not actually be known to the kernel, such
+ // as the interface of a bond that is currently down.
+ //
+ // This is a HACK: NetworkManager should export Device nodes for
+ // these.
+
+ const type_Interface = {
+ interfaces: [],
+
+ exporters: [
+ function (obj) {
+ obj.Device = null;
+ obj._NonDeviceConnections = [];
+ obj.Connections = [];
+ obj.MainConnection = null;
+ },
+
+ null,
+
+ // Needs: type_Interface.Device
+ // type_Interface._NonDeviceConnections
+ //
+ // Sets: type_Connection.Interfaces
+ // type_Interface.Connections
+ // type_Interface.MainConnection
+
+ function (obj) {
+ if (!obj.Device && obj._NonDeviceConnections.length === 0) {
+ drop_object(priv(obj).path);
+ return;
+ }
+
+ function consider_for_main(con) {
+ if (!obj.MainConnection ||
+ connection_settings(obj.MainConnection).timestamp < connection_settings(con).timestamp) {
+ obj.MainConnection = con;
+ }
+ }
+
+ obj.Connections = obj._NonDeviceConnections;
+
+ if (obj.Device) {
+ obj.Device.AvailableConnections.forEach(function (con) {
+ if (obj.Connections.indexOf(con) == -1)
+ obj.Connections.push(con);
+ });
+ }
+
+ obj.Connections.forEach(function (con) {
+ consider_for_main(con);
+ con.Interfaces.push(obj);
+ });
+
+ // Explicitly prefer the active connection. The
+ // active connection should have the most recent
+ // timestamp, but only when the activation was
+ // successful. Also, there don't seem to be change
+ // notifications when the timestamp changes.
+
+ if (obj.Device && obj.Device.ActiveConnection && obj.Device.ActiveConnection.Connection) {
+ obj.MainConnection = obj.Device.ActiveConnection.Connection;
+ }
+ }
+ ]
+
+ };
+
+ function get_interface(iface) {
+ const obj = get_object(":interface:" + iface, type_Interface);
+ obj.Name = iface;
+ return obj;
+ }
+
+ function peek_interface(iface) {
+ return peek_object(":interface:" + iface);
+ }
+
+ const type_Settings = {
+ interfaces: [
+ "org.freedesktop.NetworkManager.Settings"
+ ],
+
+ props: {
+ Connections: { conv: conv_Array(conv_Object(type_Connection)), def: [] }
+ },
+
+ prototype: {
+ add_connection: function (conf) {
+ return call_object_method(this,
+ 'org.freedesktop.NetworkManager.Settings',
+ 'AddConnection',
+ settings_to_nm(conf, { }))
+ .then(([path]) => get_object(path, type_Connection));
+ }
+ },
+
+ exporters: [
+ null,
+
+ // Sets: type_Interface._NonDeviceConnections
+ //
+ function (obj) {
+ if (obj.Connections) {
+ obj.Connections.forEach(function (con) {
+ function add_to_interface(name) {
+ if (name) {
+ const cons = get_interface(name)._NonDeviceConnections;
+ if (cons.indexOf(con) == -1)
+ cons.push(con);
+ }
+ }
+
+ if (con.Settings) {
+ if (con.Settings.connection)
+ add_to_interface(con.Settings.connection.interface_name);
+ if (con.Settings.bond)
+ add_to_interface(con.Settings.bond.interface_name);
+ if (con.Settings.team)
+ add_to_interface(con.Settings.team.interface_name);
+ if (con.Settings.bridge)
+ add_to_interface(con.Settings.bridge.interface_name);
+ if (con.Settings.vlan)
+ add_to_interface(con.Settings.vlan.interface_name);
+ }
+ });
+ }
+ }
+ ]
+ };
+
+ const type_Manager = {
+ interfaces: [
+ "org.freedesktop.NetworkManager"
+ ],
+
+ props: {
+ Capabilities: { def: [] },
+ Version: { },
+ Devices: {
+ conv: conv_Array(conv_Object(type_Device)),
+ def: []
+ },
+ ActiveConnections: { conv: conv_Array(conv_Object(type_ActiveConnection)), def: [] }
+ },
+
+ prototype: {
+ checkpoint_create: function (devices, timeout) {
+ return call_object_method(this,
+ 'org.freedesktop.NetworkManager',
+ 'CheckpointCreate',
+ devices.map(objpath),
+ timeout,
+ 0)
+ .then(([checkpoint]) => checkpoint)
+ .catch(function (error) {
+ if (error.name != "org.freedesktop.DBus.Error.UnknownMethod")
+ console.warn(error.message || error);
+ });
+ },
+
+ checkpoint_destroy: function (checkpoint) {
+ if (checkpoint) {
+ return call_object_method(this,
+ 'org.freedesktop.NetworkManager',
+ 'CheckpointDestroy',
+ checkpoint)
+ .then(() => undefined);
+ } else
+ return Promise.resolve();
+ },
+
+ checkpoint_rollback: function (checkpoint) {
+ if (checkpoint) {
+ return call_object_method(this,
+ 'org.freedesktop.NetworkManager',
+ 'CheckpointRollback',
+ checkpoint)
+ .then(([result]) => result);
+ } else
+ return Promise.resolve();
+ }
+ },
+
+ exporters: [
+ null,
+
+ // Sets: type_Interface.Device
+ //
+ function (obj) {
+ obj.Devices.forEach(function (dev) {
+ if (dev.Interface) {
+ const iface = get_interface(dev.Interface);
+ iface.Device = dev;
+ }
+ });
+ }
+ ]
+ };
+
+ /* Now create the cyclic declarations.
+ */
+ type_ActiveConnection.props.Group = { conv: conv_Object(type_Device) };
+ type_Device.props.Members = { conv: conv_Array(conv_Object(type_Device)), def: [] };
+
+ /* Accessing the model.
+ */
+
+ self.list_interfaces = function list_interfaces() {
+ const result = [];
+ for (const path in objects) {
+ const obj = objects[path];
+ if (priv(obj).type === type_Interface)
+ result.push(obj);
+ }
+ return result.sort(function (a, b) { return a.Name.localeCompare(b.Name) });
+ };
+
+ self.find_interface = peek_interface;
+
+ self.get_manager = function () {
+ return get_object("/org/freedesktop/NetworkManager",
+ type_Manager);
+ };
+
+ self.get_settings = function () {
+ return get_object("/org/freedesktop/NetworkManager/Settings",
+ type_Settings);
+ };
+
+ /* Initialization.
+ */
+
+ set_object_types([type_Manager,
+ type_Settings,
+ type_Device,
+ type_Ipv4Config,
+ type_Ipv6Config,
+ type_Connection,
+ type_ActiveConnection
+ ]);
+
+ get_object("/org/freedesktop/NetworkManager", type_Manager);
+ get_object("/org/freedesktop/NetworkManager/Settings", type_Settings);
+
+ self.ready = undefined;
+ self.operationInProgress = undefined;
+ self.curtain = undefined;
+ return self;
+}
+
+export function syn_click(model, fun) {
+ return function() {
+ const self = this;
+ const self_args = arguments;
+ return model.synchronize().then(function() {
+ fun.apply(self, self_args);
+ });
+ };
+}
+
+export function is_managed(dev) {
+ // Never let the user manage loopback devices, nothing good can come from that.
+ return dev.State != 10 && dev.DeviceType != "loopback" && dev.Interface != "lo";
+}
+
+function render_interface_link(iface) {
+ return <Button variant='link' tabindex="0"
+ isInline
+ onClick={() => cockpit.location.go([iface])}>{iface}
+ </Button>;
+}
+
+export function device_state_text(dev) {
+ if (!dev)
+ return _("Inactive");
+ if (dev.State == 100 && dev.Carrier === false)
+ return _("No carrier");
+ if (!is_managed(dev)) {
+ if (!dev.ActiveConnection &&
+ (!dev.Ip4Config || dev.Ip4Config.Addresses.length === 0) &&
+ (!dev.Ip6Config || dev.Ip6Config.Addresses.length === 0))
+ return _("Inactive");
+ }
+ return dev.StateText;
+}
+
+export function array_join(elts, sep) {
+ const result = [];
+ for (let i = 0; i < elts.length; i++) {
+ result.push(elts[i]);
+ if (i < elts.length - 1)
+ result.push(sep);
+ }
+ return result;
+}
+
+export function render_active_connection(dev, with_link, hide_link_local) {
+ const parts = [];
+
+ if (!dev)
+ return "";
+
+ const con = dev.ActiveConnection;
+
+ if (con && con.Group) {
+ return fmt_to_fragments(_("Part of $0"), with_link ? render_interface_link(con.Group.Interface) : con.Group.Interface);
+ }
+
+ const ip4config = con ? con.Ip4Config : dev.Ip4Config;
+ if (ip4config) {
+ ip4config.Addresses.forEach(function (a) {
+ parts.push(a[0] + "/" + a[1]);
+ });
+ }
+
+ function is_ipv6_link_local(addr) {
+ return (addr.indexOf("fe8") === 0 ||
+ addr.indexOf("fe9") === 0 ||
+ addr.indexOf("fea") === 0 ||
+ addr.indexOf("feb") === 0);
+ }
+
+ const ip6config = con ? con.Ip6Config : dev.Ip6Config;
+ if (ip6config) {
+ ip6config.Addresses.forEach(function (a) {
+ if (!(hide_link_local && is_ipv6_link_local(a[0])))
+ parts.push(a[0] + "/" + a[1]);
+ });
+ }
+
+ return parts.join(", ");
+}
+
+/* Resource usage monitoring
+*/
+
+export function complete_settings(settings, device) {
+ if (!device) {
+ console.warn("No device to complete settings", JSON.stringify(settings));
+ return;
+ }
+
+ settings.connection.id = device.Interface;
+ settings.connection.uuid = uuidv4();
+
+ if (device.DeviceType == 'ethernet') {
+ settings.connection.type = '802-3-ethernet';
+ settings.ethernet = { };
+ } else {
+ // The remaining types are identical between Device and Settings, see
+ // device_type_to_symbol.
+ settings.connection.type = device.DeviceType;
+ }
+}
+
+export function settings_applier(model, device, connection) {
+ /* If we have a connection, we can just update it.
+ * Otherwise if the settings has TYPE set, we can add
+ * them as a stand-alone object. Otherwise, we
+ * activate the device with the settings which causes
+ * NM to fill in the type and other details.
+ *
+ * HACK - The activation is a hack, we would rather
+ * just have NM fill in the details and not activate
+ * the connection. See complete_settings above that
+ * can do some of this completion.
+ *
+ * https://bugzilla.gnome.org/show_bug.cgi?id=775226
+ */
+
+ return function (settings) {
+ if (connection) {
+ return connection.apply_settings(settings);
+ } else if (settings.connection.type) {
+ return model.get_settings().add_connection(settings);
+ } else if (device) {
+ return device.activate_with_settings(settings);
+ } else {
+ cockpit.warn("No way to apply settings", connection, settings);
+ return Promise.resolve();
+ }
+ };
+}
+
+export function choice_title(choices, choice, def) {
+ for (let i = 0; i < choices.length; i++) {
+ if (choices[i].choice == choice)
+ return choices[i].title;
+ }
+ return def;
+}
+
+/* Support for automatically rolling back changes that break the
+ * connection to the server.
+ *
+ * The basic idea is to perform the following steps:
+ *
+ * 1) Create a checkpoint with automatic rollback
+ * 2) Make the change
+ * 3) Destroy the checkpoint
+ *
+ * If step 2 breaks the connection, step 3 won't happen and the
+ * checkpoint will roll back after some time. This is supposed to
+ * restore connectivity, so steps 2 and 3 will complete at that time,
+ * and step 3 will fail because the checkpoint doesn't exist anymore.
+ *
+ * The failure of step 3 is our indication that the connection was
+ * temporarily broken, and we inform the user about that.
+ *
+ * Usually, step 2 completes successfully also for a change that
+ * breaks the connection, and connectivity is only lost after some
+ * delay. Thus, we also delay step 3 by a short amount (settle_time,
+ * below).
+ *
+ * For a change that _doesn't_ break connectivity, this whole process
+ * is inherently a race: Steps 2 and 3 need to complete before the
+ * checkpoint created in step 1 reaches its timeout.
+ *
+ * It is better to wait a bit longer for salvation after making a
+ * mistake than to have many of your legitimate changes be cancelled
+ * by an impatient nanny mechanism. Thus, we use a rather long
+ * checkpoint rollback timeout (rollback_time, below).
+ *
+ * For a good change, all three steps usually happen quickly, and the
+ * time we wait between steps 2 and 3 doesn't need to be very long
+ * either, apparently. Thus, we delay any indication that something
+ * might be wrong by a short delay (curtain_time, below), and most
+ * changes can thus be made without the "Testing connection" curtain
+ * coming up.
+ *
+ * Some changes will be rolled back although the user really wants to
+ * make them. For example, the user might want to change the IP
+ * address of the machine, and although this will disconnect Cockpit,
+ * the user can connect again on the new address.
+ *
+ * In order to give the user the option to avoid this unwanted
+ * rollback, we let him/her do the same change without a checkpoint
+ * directly from the dialog that explains the problem.
+ */
+
+/* To avoid interference, we switch off the global transport health
+ * check while a checkpoint exists. For example, if the rollback
+ * takes a really long time, Cockpit would otherwise disconnect itself
+ * forcefully and the user would not get to see the dialog with the
+ * "Do it anyway" button. This dialog is the only way to make certain
+ * changes, and it is thus important to show it if at all possible.
+ */
+
+/* Considerations for choosing the times below
+ *
+ * curtain_time too short: Curtain comes up too often for good changes.
+ *
+ * curtain_time too long: User is left with a broken UI for a
+ * significant time in the case of a mistake.
+ *
+ * settle_time too short: Some bad changes that take time to have any
+ * effect will be let through.
+ *
+ * settle_time too high: All operations take a long time and the race
+ * between Cockpit destroying the checkpoint
+ * and NetworkManager rolling it back (see
+ * above) gets tighter. The curtain
+ * needs to come up to prevent the user from
+ * interacting with the page. Thus
+ * settle_time should be shorter than
+ * curtain_time.
+ *
+ * rollback_time too short: Good changes that take a long time to complete
+ * (on a loaded machine, say) are cancelled spuriously.
+ *
+ * rollback_time too long: The user has to wait a long time before
+ * his/her mistake is corrected and might
+ * consider Cockpit to be dead already.
+ * Also, the network connection machinery in
+ * the kernels and browsers must recover
+ * after no packages have been flowing for
+ * this much time. Windows seems to have
+ * less patience than Linux in this regard.
+ */
+const curtain_time = 1.5;
+let settle_time = 1.0;
+const rollback_time = 7.0;
+
+export function with_checkpoint(model, modify, options) {
+ const manager = model.get_manager();
+
+ let curtain_timeout;
+ let curtain_title_timeout;
+
+ function show_curtain() {
+ cockpit.hint("ignore_transport_health_check", { data: true });
+ curtain_timeout = window.setTimeout(function () {
+ curtain_timeout = null;
+ model.set_curtain('testing');
+ }, curtain_time * 1000);
+ curtain_title_timeout = window.setTimeout(function () {
+ curtain_title_timeout = null;
+ model.set_curtain('restoring');
+ }, rollback_time * 1000);
+ }
+
+ function hide_curtain() {
+ if (curtain_timeout)
+ window.clearTimeout(curtain_timeout);
+ curtain_timeout = null;
+ if (curtain_title_timeout)
+ window.clearTimeout(curtain_title_timeout);
+ cockpit.hint("ignore_transport_health_check", { data: false });
+
+ model.set_curtain(undefined);
+ }
+
+ // HACK - Let's not use checkpoints for changes that involve
+ // adding or removing connections.
+ //
+ // https://bugzilla.redhat.com/show_bug.cgi?id=1378393
+ // https://bugzilla.redhat.com/show_bug.cgi?id=1398316
+ //
+ // We also switch off checkpoints for most of the integration
+ // tests.
+
+ if (options.hack_does_add_or_remove || window.cockpit_tests_disable_checkpoints) {
+ modify();
+ return;
+ }
+
+ if (window.cockpit_tests_checkpoints_settle_time)
+ settle_time = window.cockpit_tests_checkpoints_settle_time;
+
+ manager.checkpoint_create(options.devices || [], rollback_time)
+ .then(function (cp) {
+ if (!cp) {
+ modify();
+ return;
+ }
+
+ show_curtain();
+ modify()
+ .then(function () {
+ window.setTimeout(function () {
+ manager.checkpoint_destroy(cp)
+ .catch(function () {
+ show_breaking_change_dialog({
+ ...options,
+ action: syn_click(model, modify)
+ });
+ })
+ .finally(hide_curtain);
+ }, settle_time * 1000);
+ })
+ .catch(function () {
+ hide_curtain();
+
+ // HACK
+ //
+ // We want to avoid rollbacks for operations that don't actually change anything when they
+ // fail. Rollback are always disruptive and always seem to reconnect all the included
+ // devices, even if nothing has actually changed. Thus, if you give invalid input to
+ // NetworkManager and receive an error in a settings dialog, rolling back the checkpoint
+ // would cause a temporary disconnection on the interface.
+ //
+ // https://bugzilla.redhat.com/show_bug.cgi?id=1427187
+
+ if (options.rollback_on_failure)
+ manager.checkpoint_rollback(cp);
+ else
+ manager.checkpoint_destroy(cp);
+ });
+ });
+}
+
+export function with_settings_checkpoint(model, modify, options) {
+ with_checkpoint(model, modify,
+ {
+ ...options,
+ fail_text: _("Changing the settings will break the connection to the server, and will make the administration UI unavailable."),
+ anyway_text: _("Change the settings"),
+ });
+}
+
+export function connection_devices(con) {
+ const devices = [];
+
+ if (con)
+ con.Interfaces.forEach(function (iface) { if (iface.Device) devices.push(iface.Device); });
+
+ return devices;
+}
+
+export function is_interface_connection(iface, connection) {
+ return connection && connection.Interfaces.indexOf(iface) != -1;
+}
+
+export function is_interesting_interface(iface) {
+ return !iface.Device || is_managed(iface.Device);
+}
+
+export function member_connection_for_interface(group, iface) {
+ return group?.Members.find(s => is_interface_connection(iface, s));
+}
+
+export function member_interface_choices(model, group) {
+ return model.list_interfaces().filter(function (iface) {
+ return !is_interface_connection(iface, group) && is_interesting_interface(iface);
+ });
+}
+
+export function free_member_connection(con) {
+ const cs = connection_settings(con);
+ if (cs.member_type) {
+ delete cs.member_type;
+ delete cs.group;
+ delete con.Settings.team_port;
+ delete con.Settings.bridge_port;
+ return con.apply_settings(con.Settings).then(() => { con.activate(null, null) });
+ }
+}
+
+export function set_member(model, group_connection, group_settings, member_type,
+ iface_name, val) {
+ const iface = model.find_interface(iface_name);
+ if (!iface)
+ return false;
+
+ const main_connection = iface.MainConnection;
+
+ if (val) {
+ /* Turn the main_connection into a member for group.
+ */
+
+ const group_iface = group_connection
+ ? group_connection.Interfaces[0].Name
+ : group_settings.connection.interface_name;
+
+ if (!group_iface)
+ return false;
+
+ let member_settings;
+ if (main_connection) {
+ member_settings = main_connection.Settings;
+
+ if (member_settings.connection.group == group_settings.connection.uuid ||
+ member_settings.connection.group == group_settings.connection.id ||
+ member_settings.connection.group == group_iface)
+ return Promise.resolve();
+
+ member_settings.connection.member_type = member_type;
+ member_settings.connection.group = group_iface;
+ member_settings.connection.autoconnect = true;
+ delete member_settings.ipv4;
+ delete member_settings.ipv6;
+ delete member_settings.team_port;
+ delete member_settings.bridge_port;
+ } else {
+ member_settings = {
+ connection:
+ {
+ autoconnect: true,
+ interface_name: iface.Name,
+ member_type,
+ group: group_iface
+ }
+ };
+ complete_settings(member_settings, iface.Device);
+ }
+
+ return settings_applier(model, iface.Device, main_connection)(member_settings).then(function () {
+ // If the group already exists, activate or deactivate the member immediately so that
+ // the settings actually apply and the interface becomes a member. Otherwise we
+ // activate it later when the group is created.
+ if (group_connection) {
+ const group_dev = group_connection.Interfaces[0].Device;
+ if (group_dev && group_dev.ActiveConnection)
+ return main_connection.activate(iface.Device);
+ else if (iface.Device.ActiveConnection)
+ return iface.Device.ActiveConnection.deactivate();
+ }
+ });
+ } else {
+ /* Free the main_connection from being a member if it is our member. If there is
+ * no main_connection, we don't need to do anything.
+ */
+ if (main_connection && main_connection.Groups.indexOf(group_connection) != -1) {
+ free_member_connection(main_connection);
+ }
+ }
+
+ return true;
+}
+
+export function apply_group_member(choices, model, apply_group, group_connection, group_settings, member_type) {
+ const active_settings = [];
+
+ if (!group_connection) {
+ if (group_settings.bond &&
+ group_settings.bond.options &&
+ group_settings.bond.options.primary) {
+ const iface = model.find_interface(group_settings.bond.options.primary);
+ if (iface && iface.MainConnection)
+ active_settings.push(iface.MainConnection.Settings);
+ } else {
+ Object.keys(choices)
+ .filter(choice => choices[choice])
+ .forEach(choice => {
+ const iface = model.find_interface(choice);
+ if (iface && iface.Device && iface.Device.ActiveConnection && iface.Device.ActiveConnection.Connection) {
+ active_settings.push(iface.Device.ActiveConnection.Connection.Settings);
+ }
+ });
+ }
+
+ if (active_settings.length == 1) {
+ group_settings.ipv4 = JSON.parse(JSON.stringify(active_settings[0].ipv4));
+ group_settings.ipv6 = JSON.parse(JSON.stringify(active_settings[0].ipv6));
+ }
+
+ group_settings.connection.autoconnect_members = 1;
+ }
+
+ /* For bonds, the order in which members are added to their group matters since the first members gets to
+ * set the MAC address of the bond, which matters for DHCP. We leave it to NetworkManager to determine
+ * the order in which members are added so that the order is consistent with what happens when the bond is
+ * activated the next time, such as after a reboot.
+ */
+
+ function set_all_members() {
+ const deferreds = Object.keys(choices).map(iface => {
+ return model.synchronize().then(function () {
+ return set_member(model, group_connection, group_settings, member_type,
+ iface, choices[iface]);
+ });
+ });
+ return Promise.all(deferreds);
+ }
+
+ return set_all_members().then(function () {
+ return apply_group(group_settings);
+ });
+}
+
+export function init() {
+ cockpit.translate();
+}
diff --git a/pkg/networkmanager/ip-settings.jsx b/pkg/networkmanager/ip-settings.jsx
new file mode 100644
index 0000000..b7f7b2f
--- /dev/null
+++ b/pkg/networkmanager/ip-settings.jsx
@@ -0,0 +1,403 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useContext, useEffect } from 'react';
+import cockpit from 'cockpit';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { FormFieldGroup, FormFieldGroupHeader, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { Grid } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+
+import { PlusIcon, TrashIcon } from '@patternfly/react-icons';
+
+import { NetworkModal, dialogSave } from './dialogs-common.jsx';
+import { ModelContext } from './model-context.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+const ip_method_choices = [
+ { choice: 'auto', title: _("Automatic") },
+ { choice: 'dhcp', title: _("Automatic (DHCP only)") },
+ { choice: 'link-local', title: _("Link local") },
+ { choice: 'manual', title: _("Manual") },
+ { choice: 'ignore', title: _("Ignore") },
+ { choice: 'shared', title: _("Shared") },
+ { choice: 'disabled', title: _("Disabled") }
+];
+
+const supported_ipv4_methods = ['auto', 'link-local', 'manual', 'shared', 'disabled'];
+// NM only supports a subset of IPv4 and IPv6 methods for wireguard
+// See: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/1.42.8/src/libnm-core-impl/nm-setting-wireguard.c#L1723
+const wg_supported_ipv4_methods = ['manual', 'disabled'];
+const wg_supported_ipv6_methods = ['link-local', 'manual', 'ignored', 'disabled'];
+
+export function get_ip_method_choices(topic, device_type) {
+ if (topic === 'ipv4') {
+ if (device_type === 'wireguard')
+ return ip_method_choices.filter(item => wg_supported_ipv4_methods.includes(item.choice));
+ return ip_method_choices.filter(item => supported_ipv4_methods.includes(item.choice));
+ }
+
+ if (device_type === 'wireguard')
+ return ip_method_choices.filter(item => wg_supported_ipv6_methods.includes(item.choice));
+
+ // IPv6 supports all the choices
+ return ip_method_choices;
+}
+
+export const IpSettingsDialog = ({ topic, connection, dev, settings }) => {
+ const Dialogs = useDialogs();
+ const idPrefix = "network-ip-settings";
+ const model = useContext(ModelContext);
+
+ const params = settings[topic];
+ const [addresses, setAddresses] = useState(params.addresses ? params.addresses.map(addr => ({ address: addr[0], netmask: addr[1], gateway: addr[2] })) : []);
+ const [dialogError, setDialogError] = useState(undefined);
+ const [dns, setDns] = useState(params.dns || []);
+ const [dnsSearch, setDnsSearch] = useState(params.dns_search || []);
+ const [ignoreAutoDns, setIgnoreAutoDns] = useState(params.ignore_auto_dns);
+ const [ignoreAutoRoutes, setIgnoreAutoRoutes] = useState(params.ignore_auto_routes);
+ const [method, setMethod] = useState(params.method);
+ const [routes, setRoutes] = useState(params.routes ? params.routes.map(addr => ({ address: addr[0], netmask: addr[1], gateway: addr[2], metric: addr[3] })) : []);
+
+ // The link local, shared, and disabled methods can't take any
+ // addresses, dns servers, or dns search domains. Routes,
+ // however, are ok, even for "disabled" and "ignored". But
+ // since that doesn't make sense, we remove routes as well for
+ // these methods.
+ const isOff = (method == "disabled" || method == "ignore");
+ const canHaveExtra = !(method == "link-local" || method == "shared" || isOff);
+
+ // The auto_*_btns only make sense when the address method
+ // is "auto" or "dhcp".
+ const canAuto = (method == "auto" || method == "dhcp");
+
+ useEffect(() => {
+ // The manual method needs at least one address
+ if (method == 'manual' && addresses.length == 0)
+ setAddresses([{ address: "", netmask: "", gateway: "" }]);
+
+ if (!canHaveExtra) {
+ setAddresses([]);
+ setDns([]);
+ setDnsSearch([]);
+ }
+
+ if (isOff)
+ setRoutes([]);
+ }, [method, addresses.length, canHaveExtra, isOff]);
+
+ const onSubmit = (ev) => {
+ const createSettingsObj = () => ({
+ ...settings,
+ [topic]: {
+ ...settings[topic],
+ method,
+ addresses: addresses.map(addr => [addr.address, addr.netmask, addr.gateway]),
+ dns,
+ dns_search: dnsSearch,
+ routes: routes.map(route => [route.address, route.netmask, route.gateway, route.metric]),
+ ignore_auto_dns: ignoreAutoDns,
+ ignore_auto_routes: ignoreAutoRoutes,
+ }
+ });
+
+ dialogSave({
+ model,
+ dev,
+ connection,
+ settings: createSettingsObj(),
+ setDialogError,
+ onClose: Dialogs.close,
+ });
+
+ // Prevent dialog from closing because of <form> onsubmit event
+ if (event)
+ event.preventDefault();
+
+ return false;
+ };
+ const addressIpv4Helper = (address) => {
+ const config = { address, netmask: '', gateway: '' };
+ const split = address.split('.');
+
+ if (split.length !== 4)
+ return config;
+
+ config.gateway = `${split[0]}.${split[1]}.${split[2]}.${split[3] === "1" ? "254" : "1"}`;
+ if (split[0] >= 0 && split[0] <= 127) {
+ return { ...config, netmask: "255.0.0.0" };
+ } else if (split[0] >= 128 && split[0] <= 191) {
+ return { ...config, netmask: "255.255.0.0" };
+ } else if (split[0] <= 192 && split[0] <= 223) {
+ return { ...config, netmask: "255.255.255.0" };
+ } else return { ...config, gateway: '' };
+ };
+
+ return (
+ <NetworkModal dialogError={dialogError}
+ idPrefix={idPrefix}
+ onSubmit={onSubmit}
+ title={topic == "ipv4" ? _("IPv4 settings") : _("IPv6 settings")}
+ isFormHorizontal={false}
+ >
+ <FormFieldGroup
+ data-field='addresses'
+ header={
+ <FormFieldGroupHeader
+ titleText={{ text: _("Addresses") }}
+ actions={
+ <Flex>
+ <FormSelect className="network-ip-settings-method"
+ id={idPrefix + "-select-method"}
+ aria-label={_("Select method")}
+ onChange={(_, val) => setMethod(val)}
+ value={method}>
+ {get_ip_method_choices(topic, dev.DeviceType).map(choice => <FormSelectOption value={choice.choice} label={choice.title} key={choice.choice} />)}
+ </FormSelect>
+ <Tooltip content={_("Add address")}>
+ <Button variant="secondary"
+ isDisabled={!canHaveExtra}
+ onClick={() => setAddresses([...addresses, { address: "", netmask: "", gateway: "" }])}
+ id={idPrefix + "-address-add"}
+ aria-label={_("Add address")}>
+ <PlusIcon />
+ </Button>
+ </Tooltip>
+ </Flex>
+ }
+ />
+ }
+ >
+ {addresses.map((address, i) => {
+ const prefixText = (topic == "ipv4") ? _("Prefix length or netmask") : _("Prefix length");
+
+ return (
+ <Grid key={i} hasGutter>
+ <FormGroup fieldId={idPrefix + "-address-" + i} label={_("Address")} className="pf-m-4-col-on-sm">
+ <TextInput id={idPrefix + "-address-" + i} value={address.address} onChange={(_event, value) => setAddresses(
+ addresses.map((item, index) =>
+ i === index
+ ? addressIpv4Helper(value)
+ : item
+ ))} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-netmask-" + i} label={prefixText} className="pf-m-4-col-on-sm">
+ <TextInput id={idPrefix + "-netmask-" + i} value={address.netmask} onChange={(_event, value) => setAddresses(
+ addresses.map((item, index) =>
+ i === index
+ ? { ...item, netmask: value }
+ : item
+ ))} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-gateway-" + i} label={_("Gateway")} className="pf-m-4-col-on-sm">
+ <TextInput id={idPrefix + "-gateway-" + i} value={address.gateway} onChange={(_event, value) => setAddresses(
+ addresses.map((item, index) =>
+ i === index
+ ? { ...item, gateway: value }
+ : item
+ ))} />
+ </FormGroup>
+ <FormGroup className="pf-m-1-col-on-sm remove-button-group">
+ <Button variant='plain'
+ isDisabled={method == 'manual' && i == 0}
+ onClick={() => setAddresses(addresses.filter((_, index) => index !== i))}
+ aria-label={_("Remove item")}
+ icon={<TrashIcon />} />
+ </FormGroup>
+ </Grid>
+ );
+ })}
+ </FormFieldGroup>
+ <FormFieldGroup
+ data-field='dns'
+ header={
+ <FormFieldGroupHeader
+ titleText={{ text: _("DNS") }}
+ actions={
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <Switch
+ isChecked={!ignoreAutoDns}
+ isDisabled={!canAuto}
+ onChange={(_event, value) => setIgnoreAutoDns(!value)}
+ label={_("Automatic")} />
+ <Tooltip content={_("Add DNS server")}>
+ <Button variant="secondary"
+ isDisabled={!canHaveExtra}
+ onClick={() => setDns([...dns, ""])}
+ id={idPrefix + "-dns-add"}
+ aria-label={_("Add DNS server")}>
+ <PlusIcon />
+ </Button>
+ </Tooltip>
+ </Flex>
+ }
+ />
+ }
+ >
+ {dns.map((server, i) => {
+ return (
+ <Grid key={i} hasGutter>
+ <FormGroup fieldId={idPrefix + "-dns-server-" + i} label={_("Server")}>
+ <TextInput id={idPrefix + "-dns-server-" + i} value={server} onChange={(_event, value) => setDns(
+ dns.map((item, index) =>
+ i === index
+ ? value
+ : item
+ ))} />
+ </FormGroup>
+ <FormGroup className="pf-m-1-col-on-sm remove-button-group">
+ <Button variant='plain'
+ size="sm"
+ onClick={() => setDns(dns.filter((_, index) => index !== i))}
+ aria-label={_("Remove item")}
+ icon={<TrashIcon />} />
+ </FormGroup>
+ </Grid>
+ );
+ })}
+ </FormFieldGroup>
+ <FormFieldGroup
+ data-field='dns_search'
+ header={
+ <FormFieldGroupHeader
+ titleText={{ text: _("DNS search domains") }}
+ actions={
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <Switch
+ isChecked={!ignoreAutoDns}
+ isDisabled={!canAuto}
+ onChange={(_event, value) => setIgnoreAutoDns(!value)}
+ label={_("Automatic")} />
+ <Tooltip content={_("Add search domain")}>
+ <Button variant="secondary"
+ isDisabled={!canHaveExtra}
+ onClick={() => setDnsSearch([...dnsSearch, ""])}
+ id={idPrefix + "-dns-search-add"}
+ aria-label={_("Add search domain")}>
+ <PlusIcon />
+ </Button>
+ </Tooltip>
+ </Flex>
+ }
+ />
+ }
+ >
+ {dnsSearch.map((domain, i) => {
+ return (
+ <Grid key={i} hasGutter>
+ <FormGroup fieldId={idPrefix + "-search-domain-" + i} label={_("Search domain")}>
+ <TextInput id={idPrefix + "-search-domain-" + i} value={domain} onChange={(_event, value) => setDnsSearch(
+ dnsSearch.map((item, index) =>
+ i === index
+ ? value
+ : item
+ ))} />
+ </FormGroup>
+ <FormGroup className="pf-m-1-col-on-sm remove-button-group">
+ <Button variant='plain'
+ size="sm"
+ onClick={() => setDnsSearch(dnsSearch.filter((_, index) => index !== i))}
+ aria-label={_("Remove item")}
+ icon={<TrashIcon />} />
+ </FormGroup>
+ </Grid>
+ );
+ })}
+ </FormFieldGroup>
+ <FormFieldGroup
+ data-field='routes'
+ header={
+ <FormFieldGroupHeader
+ titleText={{ text: _("Routes") }}
+ actions={
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <Switch
+ isChecked={!ignoreAutoRoutes}
+ isDisabled={!canAuto}
+ onChange={(_event, value) => setIgnoreAutoRoutes(!value)}
+ label={_("Automatic")} />
+ <Tooltip content={_("Add route")}>
+ <Button variant="secondary"
+ isDisabled={isOff}
+ onClick={() => setRoutes([...routes, { address: "", netmask: "", gateway: "", metric: "" }])}
+ id={idPrefix + "-route-add"}
+ aria-label={_("Add route")}>
+ <PlusIcon />
+ </Button>
+ </Tooltip>
+ </Flex>
+ }
+ />
+ }
+ >
+ {routes.map((route, i) => {
+ return (
+ <Grid key={i} hasGutter>
+ <FormGroup fieldId={idPrefix + "-route-address-" + i} label={_("Address")} className="pf-m-3-col-on-sm">
+ <TextInput id={idPrefix + "-route-address-" + i} value={route.address} onChange={(_event, value) => setRoutes(
+ routes.map((item, index) =>
+ i === index
+ ? { ...item, address: value }
+ : item
+ ))} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-route-netmask-" + i} label={_("Prefix length or netmask")} className="pf-m-4-col-on-sm">
+ <TextInput id={idPrefix + "-route-netmask-" + i} value={route.netmask} onChange={(_event, value) => setRoutes(
+ routes.map((item, index) =>
+ i === index
+ ? { ...item, netmask: value }
+ : item
+ ))} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-route-gateway-" + i} label={_("Gateway")} className="pf-m-3-col-on-sm">
+ <TextInput id={idPrefix + "-route-gateway-" + i} value={route.gateway} onChange={(_event, value) => setRoutes(
+ routes.map((item, index) =>
+ i === index
+ ? { ...item, gateway: value }
+ : item
+ ))} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-route-metric-" + i} label={_("Metric")} className="pf-m-2-col-on-sm">
+ <TextInput id={idPrefix + "-route-metric-" + i} value={route.metric} onChange={(_event, value) => setRoutes(
+ routes.map((item, index) =>
+ i === index
+ ? { ...item, metric: value }
+ : item
+ ))} />
+ </FormGroup>
+ <FormGroup className="pf-m-1-col-on-sm remove-button-group">
+ <Button variant='plain'
+ size="sm"
+ onClick={() => setRoutes(routes.filter((_, index) => index !== i))}
+ aria-label={_("Remove item")}
+ icon={<TrashIcon />} />
+ </FormGroup>
+ </Grid>
+ );
+ })}
+ </FormFieldGroup>
+ </NetworkModal>
+ );
+};
diff --git a/pkg/networkmanager/mac.jsx b/pkg/networkmanager/mac.jsx
new file mode 100644
index 0000000..36e2933
--- /dev/null
+++ b/pkg/networkmanager/mac.jsx
@@ -0,0 +1,78 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useContext } from 'react';
+import cockpit from 'cockpit';
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+
+import { MacMenu, NetworkModal, dialogSave } from './dialogs-common.jsx';
+import { ModelContext } from './model-context.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+export const MacDialog = ({ connection, dev, settings }) => {
+ const Dialogs = useDialogs();
+ const idPrefix = "network-mac-settings";
+ const model = useContext(ModelContext);
+
+ const [mac, setMAC] = useState((settings.ethernet && settings.ethernet.assigned_mac_address) || "");
+ const [dialogError, setDialogError] = useState(undefined);
+
+ const onSubmit = (ev) => {
+ const createSettingsObj = () => ({
+ ...settings,
+ ethernet: {
+ assigned_mac_address: mac
+ },
+ });
+
+ if (!mac) {
+ setDialogError(_("Enter a valid MAC address"));
+ return;
+ }
+
+ dialogSave({
+ model,
+ dev,
+ connection,
+ settings: createSettingsObj(),
+ setDialogError,
+ onClose: Dialogs.close,
+ });
+
+ // Prevent dialog from closing because of <form> onsubmit event
+ if (event)
+ event.preventDefault();
+
+ return false;
+ };
+
+ return (
+ <NetworkModal dialogError={dialogError}
+ idPrefix={idPrefix}
+ onSubmit={onSubmit}
+ title={_("Ethernet MAC")}
+ >
+ <FormGroup fieldId={idPrefix + "-mac-input"} label={_("MAC")}>
+ <MacMenu idPrefix={idPrefix} model={model} mac={mac} setMAC={setMAC} />
+ </FormGroup>
+ </NetworkModal>
+ );
+};
diff --git a/pkg/networkmanager/manifest.json b/pkg/networkmanager/manifest.json
new file mode 100644
index 0000000..8ef35f8
--- /dev/null
+++ b/pkg/networkmanager/manifest.json
@@ -0,0 +1,43 @@
+{
+ "name": "network",
+ "conditions": [
+ {"path-exists": "/usr/share/dbus-1/system.d/org.freedesktop.NetworkManager.conf"}
+ ],
+ "menu": {
+ "index": {
+ "label": "Networking",
+ "order": 40,
+ "docs": [
+ {
+ "label": "Managing networking bonds",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-network-bonds-using-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing networking teams",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-network-teams-using-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing networking bridges",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-network-bridges-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing VLANs",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-vlans-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing firewall",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing_firewall_using_the_web_console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["network", "interface", "bridge", "vlan", "bond", "team", "port", "mac", "ipv4", "ipv6"]
+ },
+ {
+ "matches": ["firewall", "firewalld", "zone", "tcp", "udp"],
+ "goto": "/network/firewall"
+ }
+ ]
+ }
+ }
+}
diff --git a/pkg/networkmanager/model-context.jsx b/pkg/networkmanager/model-context.jsx
new file mode 100644
index 0000000..0bca8f5
--- /dev/null
+++ b/pkg/networkmanager/model-context.jsx
@@ -0,0 +1,3 @@
+import React from 'react';
+
+export const ModelContext = React.createContext(undefined);
diff --git a/pkg/networkmanager/mtu.jsx b/pkg/networkmanager/mtu.jsx
new file mode 100644
index 0000000..d09ade8
--- /dev/null
+++ b/pkg/networkmanager/mtu.jsx
@@ -0,0 +1,97 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useContext } from 'react';
+import cockpit from 'cockpit';
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+import { NetworkModal, dialogSave } from './dialogs-common.jsx';
+import { ModelContext } from './model-context.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+export const MtuDialog = ({ connection, dev, settings }) => {
+ const Dialogs = useDialogs();
+ const idPrefix = "network-mtu-settings";
+ const model = useContext(ModelContext);
+
+ const [dialogError, setDialogError] = useState(undefined);
+ const [mode, setMode] = useState(!settings.ethernet.mtu ? "auto" : "custom");
+ const [mtu, setMtu] = useState(settings.ethernet.mtu ? settings.ethernet.mtu : '');
+
+ const onSubmit = (ev) => {
+ const mtuNew = mode == 'auto' ? 0 : parseInt(mtu, 10);
+ if (isNaN(mtuNew) || mtuNew < 0) {
+ setDialogError(_("MTU must be a positive number"));
+ return;
+ }
+ const createSettingsObj = () => ({
+ ...settings,
+ ethernet: {
+ ...settings.ethernet,
+ mtu: mtuNew,
+ }
+ });
+
+ dialogSave({
+ model,
+ dev,
+ connection,
+ settings: createSettingsObj(),
+ setDialogError,
+ onClose: Dialogs.close,
+ });
+
+ // Prevent dialog from closing because of <form> onsubmit event
+ if (event)
+ event.preventDefault();
+
+ return false;
+ };
+
+ return (
+ <NetworkModal dialogError={dialogError}
+ idPrefix={idPrefix}
+ onSubmit={onSubmit}
+ title={_("Ethernet MTU")}
+ >
+ <>
+ <Radio id={idPrefix + "-auto"}
+ isChecked={mode == "auto"}
+ label={_("Automatic")}
+ name="mtu-mode"
+ onChange={() => setMode("auto")}
+ value="auto" />
+ <Radio id={idPrefix + "-custom"}
+ isChecked={mode == "custom"}
+ label={
+ <>
+ <span>{_("Set to")}</span>
+ <TextInput id={idPrefix + "-input"} value={mtu} onChange={(_event, value) => setMtu(value)} className="mtu-label-input" />
+ </>
+ }
+ name="mtu-mode"
+ onChange={() => setMode("custom")}
+ value="custom" />
+ </>
+ </NetworkModal>
+ );
+};
diff --git a/pkg/networkmanager/network-interface-members.jsx b/pkg/networkmanager/network-interface-members.jsx
new file mode 100644
index 0000000..df322ee
--- /dev/null
+++ b/pkg/networkmanager/network-interface-members.jsx
@@ -0,0 +1,211 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import cockpit from "cockpit";
+import React, { useState, useContext } from "react";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { Dropdown, DropdownItem, DropdownToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import { MinusIcon } from '@patternfly/react-icons';
+
+import { ListingTable } from "cockpit-components-table.jsx";
+import { ModelContext } from './model-context.jsx';
+import { useEvent } from "hooks";
+
+import {
+ connection_settings,
+ device_state_text,
+ free_member_connection,
+ is_interesting_interface,
+ set_member,
+ show_unexpected_error,
+ syn_click,
+ with_checkpoint,
+ is_managed,
+} from './interfaces.js';
+import { fmt_to_fragments } from 'utils.jsx';
+
+const _ = cockpit.gettext;
+
+export const NetworkInterfaceMembers = ({
+ members,
+ memberIfaces,
+ interfaces,
+ iface,
+ usage_monitor,
+ privileged
+}) => {
+ const model = useContext(ModelContext);
+ const [isOpen, setIsOpen] = useState(false);
+ useEvent(usage_monitor.grid, "notify");
+
+ function renderMemberRows() {
+ const rows = [];
+
+ members.forEach(iface => {
+ const member_con = iface.MainConnection;
+ const dev = iface.Device;
+ const isActive = (dev && dev.State == 100 && dev.Carrier === true);
+ const onoff = (
+ <Switch
+ aria-label={cockpit.format(_("Switch of $0"), iface.Name)}
+ isDisabled={!privileged}
+ isChecked={!!(dev && dev.ActiveConnection)}
+ onChange={(_event, val) => {
+ if (val) {
+ with_checkpoint(
+ model,
+ () => member_con.activate(dev).catch(show_unexpected_error),
+ {
+ devices: dev ? [dev] : [],
+ fail_text: fmt_to_fragments(_("Switching on $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
+ anyway_text: cockpit.format(_("Switch on $0"), iface.Name)
+ });
+ } else if (dev) {
+ with_checkpoint(
+ model,
+ () => dev.disconnect().catch(show_unexpected_error),
+ {
+ devices: [dev],
+ fail_text: fmt_to_fragments(_("Switching off $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
+ anyway_text: cockpit.format(_("Switch off $0"), iface.Name)
+ });
+ }
+ } } />
+ );
+
+ const row = ({
+ columns: [
+ { title: (!dev || is_managed(dev)) ? <Button variant="link" isInline onClick={() => cockpit.location.go([iface.Name])}>{iface.Name}</Button> : iface.Name },
+ // Will add traffic info right after
+ {
+ title: (
+ <div className="btn-group">
+ {onoff}
+ {privileged && <Button variant="secondary"
+ size="sm"
+ onClick={syn_click(model, () => {
+ with_checkpoint(
+ model,
+ () => free_member_connection(member_con).catch(show_unexpected_error),
+ {
+ devices: dev ? [dev] : [],
+ fail_text: fmt_to_fragments(_("Removing $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
+ anyway_text: cockpit.format(_("Remove $0"), iface.Name),
+ hack_does_add_or_remove: true
+ });
+ return false;
+ })}>
+ <MinusIcon />
+ </Button>}
+ </div>
+ ),
+ props: { className: "pf-v5-c-table__action" }
+ },
+ ],
+ props: {
+ key: iface.Name,
+ "data-interface": encodeURIComponent(iface.Name),
+ "data-sample-id": isActive ? encodeURIComponent(iface.Name) : null,
+ "data-row-id": iface.Name,
+ }
+ });
+
+ if (isActive) {
+ const samples = usage_monitor.samples[iface.Name];
+ row.columns.splice(1, 0, { title: samples ? cockpit.format_bits_per_sec(samples[1][0] * 8) : "" });
+ row.columns.splice(2, 0, { title: samples ? cockpit.format_bits_per_sec(samples[0][0] * 8) : "" });
+ } else {
+ row.columns.splice(1, 0, { title: device_state_text() });
+ row.columns.splice(1, 0, { title: "" });
+ }
+
+ rows.push(row);
+ });
+ return rows;
+ }
+
+ const main_connection = iface.MainConnection;
+ const cs = iface.MainConnection && connection_settings(iface.MainConnection);
+
+ const dropdownItems = (
+ interfaces
+ .filter(i => {
+ return (is_interesting_interface(i) &&
+ !memberIfaces[i.Name] &&
+ i != iface);
+ })
+ .map(iface => {
+ const onClick = () => {
+ with_checkpoint(
+ model,
+ () => {
+ return set_member(model, main_connection, main_connection.Settings,
+ cs.type, iface.Name, true)
+ .catch(show_unexpected_error);
+ },
+ {
+ devices: iface.Device ? [iface.Device] : [],
+ fail_text: fmt_to_fragments(_("Adding $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{iface.Name}</b>),
+ anyway_text: cockpit.format(_("Add $0"), iface.Name),
+ hack_does_add_or_remove: true
+ }
+ );
+ };
+
+ return (
+ <DropdownItem onClick={syn_click(model, onClick)}
+ key={"add-member-" + iface.Name}
+ component="button">
+ {iface.Name}
+ </DropdownItem>
+ );
+ })
+ );
+
+ const add_btn = (
+ <Dropdown onSelect={() => setIsOpen(false)}
+ toggle={
+ <DropdownToggle id="add-member" onToggle={(_, isOpen) => setIsOpen(isOpen)}>
+ {_("Add member")}
+ </DropdownToggle>
+ }
+ isOpen={isOpen}
+ position="right"
+ dropdownItems={dropdownItems} />
+ );
+
+ return (
+ <Card id="network-interface-members" className="network-interface-members">
+ <CardHeader actions={{ actions: add_btn }}>
+ <CardTitle component="h2">{_("Interface members")}</CardTitle>
+ </CardHeader>
+ <ListingTable aria-label={_("Interface members")}
+ className="networking-interface-members"
+ variant='compact'
+ columns={[
+ { title: (cs && cs.type == "bond") ? _("Interfaces") : _("Ports"), props: { width: 25 } },
+ { title: _("Sending"), props: { width: 25 } },
+ { title: _("Receiving"), props: { width: 25 } },
+ { title: "" },
+ ]}
+ rows={renderMemberRows()} />
+ </Card>
+ );
+};
diff --git a/pkg/networkmanager/network-interface.jsx b/pkg/networkmanager/network-interface.jsx
new file mode 100644
index 0000000..ba21cde
--- /dev/null
+++ b/pkg/networkmanager/network-interface.jsx
@@ -0,0 +1,756 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import cockpit from "cockpit";
+import React, { useContext } from "react";
+import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+import { Page, PageBreadcrumb, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+
+import { Privileged } from "cockpit-components-privileged.jsx";
+
+import { ModelContext } from './model-context.jsx';
+import { NetworkInterfaceMembers } from "./network-interface-members.jsx";
+import { NetworkAction } from './dialogs-common.jsx';
+import { NetworkPlots } from "./plots";
+import { fmt_to_fragments } from 'utils.jsx';
+
+import {
+ array_join,
+ choice_title,
+ complete_settings,
+ connection_settings,
+ free_member_connection,
+ is_managed,
+ render_active_connection,
+ settings_applier,
+ show_unexpected_error,
+ syn_click,
+ with_checkpoint,
+} from './interfaces.js';
+import {
+ team_runner_choices,
+ team_watch_choices,
+} from './team.jsx';
+import {
+ bond_mode_choices,
+} from './bond.jsx';
+
+import { get_ip_method_choices } from './ip-settings.jsx';
+
+const _ = cockpit.gettext;
+
+export const NetworkInterfacePage = ({
+ privileged,
+ operationInProgress,
+ usage_monitor,
+ plot_state,
+ interfaces,
+ iface
+}) => {
+ const model = useContext(ModelContext);
+
+ const dev_name = iface.Name;
+ const dev = iface.Device;
+ const isManaged = iface && (!dev || is_managed(dev));
+
+ let ghostSettings = null;
+ let connectionSettings = null;
+
+ if (iface) {
+ if (iface.MainConnection) {
+ connectionSettings = iface.MainConnection.Settings;
+ } else {
+ ghostSettings = createGhostConnectionSettings();
+ connectionSettings = ghostSettings;
+ }
+ }
+
+ function deleteConnections() {
+ function deleteConnectionAndMembers(con) {
+ return Promise.all(con.Members.map(s => free_member_connection(s))).then(() => con.delete_());
+ }
+
+ function deleteConnections(cons) {
+ return Promise.all(cons.map(deleteConnectionAndMembers));
+ }
+
+ function deleteIfaceConnections(iface) {
+ return deleteConnections(iface.Connections);
+ }
+
+ const location = cockpit.location;
+
+ function modify() {
+ return deleteIfaceConnections(iface)
+ .then(function () {
+ location.go("/");
+ })
+ .catch(show_unexpected_error);
+ }
+
+ if (iface) {
+ with_checkpoint(model, modify,
+ {
+ devices: dev ? [dev] : [],
+ fail_text: fmt_to_fragments(_("Deleting $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
+ anyway_text: cockpit.format(_("Delete $0"), dev_name),
+ hack_does_add_or_remove: true,
+ rollback_on_failure: true
+ });
+ }
+ }
+
+ function connect() {
+ if (!(iface.MainConnection || (dev && ghostSettings)))
+ return;
+
+ function fail(error) {
+ show_unexpected_error(error);
+ }
+
+ function modify() {
+ if (iface.MainConnection) {
+ return iface.MainConnection.activate(dev, null).catch(fail);
+ } else {
+ return dev.activate_with_settings(ghostSettings, null).catch(fail);
+ }
+ }
+
+ with_checkpoint(model, modify,
+ {
+ devices: dev ? [dev] : [],
+ fail_text: fmt_to_fragments(_("Switching on $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
+ anyway_text: cockpit.format(_("Switch on $0"), dev_name)
+ });
+ }
+
+ function disconnect() {
+ if (!dev) {
+ console.log("Trying to switch off without a device?");
+ return;
+ }
+
+ function modify () {
+ return dev.disconnect()
+ .catch(error => show_unexpected_error(error));
+ }
+
+ with_checkpoint(model, modify,
+ {
+ devices: [dev],
+ fail_text: fmt_to_fragments(_("Switching off $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
+ anyway_text: cockpit.format(_("Switch off $0"), dev_name)
+ });
+ }
+
+ function renderDesc() {
+ let desc, cs;
+ if (dev) {
+ if (dev.DeviceType == 'ethernet' || dev.IdVendor || dev.IdModel) {
+ desc = cockpit.format("$IdVendor $IdModel $Driver", dev);
+ } else if (dev.DeviceType == 'bond') {
+ desc = _("Bond");
+ } else if (dev.DeviceType == 'team') {
+ desc = _("Team");
+ } else if (dev.DeviceType == 'vlan') {
+ desc = _("VLAN");
+ } else if (dev.DeviceType == 'bridge') {
+ desc = _("Bridge");
+ } else if (dev.Driver == 'wireguard') {
+ desc = "WireGuard";
+ } else
+ desc = cockpit.format(_("Unknown \"$0\""), dev.DeviceType);
+ } else if (iface) {
+ cs = connection_settings(iface.Connections[0]);
+ if (cs.type == "bond")
+ desc = _("Bond");
+ else if (cs.type == "team")
+ desc = _("Team");
+ else if (cs.type == "vlan")
+ desc = _("VLAN");
+ else if (cs.type == "bridge")
+ desc = _("Bridge");
+ else if (cs.type == "wireguard")
+ desc = "WireGuard";
+ else if (cs.type)
+ desc = cockpit.format(_("Unknown \"$0\""), cs.type);
+ else
+ desc = _("Unknown");
+ } else
+ desc = _("Unknown");
+
+ return desc;
+ }
+
+ function renderMac() {
+ let mac;
+ if (dev &&
+ dev.HwAddress) {
+ mac = dev.HwAddress;
+ } else if (iface &&
+ iface.MainConnection &&
+ iface.MainConnection.Settings &&
+ iface.MainConnection.Settings.ethernet &&
+ iface.MainConnection.Settings.ethernet.assigned_mac_address) {
+ mac = iface.MainConnection.Settings.ethernet.assigned_mac_address;
+ }
+
+ const can_edit_mac = (privileged && iface && iface.MainConnection &&
+ (connection_settings(iface.MainConnection).type == "802-3-ethernet" ||
+ connection_settings(iface.MainConnection).type == "bond"));
+
+ let mac_desc;
+ if (can_edit_mac) {
+ mac_desc = (
+ <NetworkAction type="mac" iface={iface} buttonText={mac} connectionSettings={iface.MainConnection.Settings} />
+ );
+ } else {
+ mac_desc = mac;
+ }
+
+ return mac_desc;
+ }
+
+ function renderCarrierStatusRow() {
+ if (dev && dev.Carrier !== undefined) {
+ return (
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Carrier")}</DescriptionListTerm>
+ <DescriptionListDescription>
+ {dev.Carrier ? (dev.Speed ? cockpit.format_bits_per_sec(dev.Speed * 1e6) : _("Yes")) : _("No")}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ );
+ } else
+ return null;
+ }
+
+ function renderActiveStatusRow() {
+ let state;
+
+ if (iface.MainConnection && iface.MainConnection.Groups.length > 0)
+ return null;
+
+ if (!dev)
+ state = _("Inactive");
+ else if (isManaged && dev.State != 100)
+ state = dev.StateText;
+ else
+ state = null;
+
+ const activeConnection = render_active_connection(dev, true, false);
+ return (
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Status")}</DescriptionListTerm>
+ <DescriptionListDescription className="networking-interface-status">
+ {activeConnection}
+ {state ? <span>{state}</span> : null}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ );
+ }
+
+ function renderConnectionSettingsRows(con, settings) {
+ if (!isManaged || !settings)
+ return [];
+
+ let group_settings = null;
+ if (con && con.Groups.length > 0)
+ group_settings = con.Groups[0].Settings;
+
+ function renderIpSettings(topic) {
+ const params = settings[topic];
+ const parts = [];
+
+ if (params.method != "manual")
+ parts.push(choice_title(get_ip_method_choices(topic), params.method, _("Unknown configuration")));
+
+ const addr_is_extra = (params.method != "manual");
+ const addrs = [];
+ params.addresses.forEach(function (a) {
+ let addr = a[0] + "/" + a[1];
+ if (a[2] && a[2] != "0.0.0.0" && a[2] != "0:0:0:0:0:0:0:0")
+ addr += " via " + a[2];
+ addrs.push(addr);
+ });
+ if (addrs.length > 0)
+ parts.push(cockpit.format(addr_is_extra ? _("Additional address $val") : _("Address $val"),
+ { val: addrs.join(", ") }));
+
+ const dns_is_extra = (!params["ignore-auto-dns"] && params.method != "manual");
+ if (params.dns.length > 0)
+ parts.push(cockpit.format(dns_is_extra ? _("Additional DNS $val") : _("DNS $val"),
+ { val: params.dns.join(", ") }));
+ if (params.dns_search.length > 0)
+ parts.push(cockpit.format(dns_is_extra ? _("Additional DNS search domains $val") : _("DNS search domains $val"),
+ { val: params.dns_search.join(", ") }));
+
+ return parts;
+ }
+
+ function renderAutoconnectRow() {
+ if (settings.connection.autoconnect !== undefined) {
+ return (
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("General")}</DescriptionListTerm>
+ <DescriptionListDescription>
+ <Checkbox id="autoreconnect" isDisabled={!privileged}
+ onChange={(_event, checked) => {
+ settings.connection.autoconnect = checked;
+ settings_applier(self.model, dev, con)(settings);
+ }}
+ isChecked={settings.connection.autoconnect}
+ label={_("Connect automatically")} />
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ );
+ }
+ }
+
+ function renderSettingsRow(title, rows, configure) {
+ const link_text = [];
+ for (let i = 0; i < rows.length; i++) {
+ link_text.push(rows[i]);
+ if (i < rows.length - 1)
+ link_text.push(<br key={"break-" + i} />);
+ }
+
+ return (
+ <DescriptionListGroup>
+ <DescriptionListTerm>{title}</DescriptionListTerm>
+ <DescriptionListDescription>
+ {link_text.length
+ ? <span className="network-interface-settings-text">
+ {link_text}
+ </span>
+ : null}
+ {privileged
+ ? (typeof configure === 'function' ? <Button variant="link" isInline onClick={syn_click(model, configure)}>{_("edit")}</Button> : configure)
+ : null}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ );
+ }
+
+ function renderIpSettingsRow(topic, title) {
+ if (!settings[topic])
+ return null;
+
+ const configure = <NetworkAction type={topic} iface={iface} connectionSettings={settings} />;
+ return renderSettingsRow(title, renderIpSettings(topic), configure);
+ }
+
+ function renderMtuSettingsRow() {
+ const rows = [];
+ const options = settings.ethernet;
+
+ if (!options)
+ return null;
+
+ function addRow(fmt, args) {
+ rows.push(cockpit.format(fmt, args));
+ }
+
+ if (options.mtu)
+ addRow("$mtu", options);
+ else
+ addRow(_("Automatic"), options);
+
+ const configure = <NetworkAction type="mtu" iface={iface} connectionSettings={settings} />;
+ return renderSettingsRow(_("MTU"), rows, configure);
+ }
+
+ function render_connection_link(con, key) {
+ return <span key={key}>
+ {
+ array_join(
+ con.Interfaces.map(iface =>
+ <Button variant="link" key={iface.Name}
+ isInline
+ onClick={() => cockpit.location.go([iface.Name])}>{iface.Name}</Button>),
+ ", ")
+ }
+ </span>;
+ }
+
+ function render_group() {
+ if (con && con.Groups.length > 0) {
+ return (
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Group")}</DescriptionListTerm>
+ <DescriptionListDescription>
+ {array_join(con.Groups.map(render_connection_link), ", ")}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ );
+ } else
+ return null;
+ }
+
+ function renderBondSettingsRow() {
+ const parts = [];
+ const rows = [];
+
+ if (!settings.bond)
+ return null;
+
+ const options = settings.bond.options;
+
+ parts.push(choice_title(bond_mode_choices, options.mode, options.mode));
+ if (options.arp_interval)
+ parts.push(_("ARP monitoring"));
+
+ if (parts.length > 0)
+ rows.push(parts.join(", "));
+
+ const configure = <NetworkAction type="bond" iface={iface} connectionSettings={settings} />;
+ return renderSettingsRow(_("Bond"), rows, configure);
+ }
+
+ function renderTeamSettingsRow() {
+ const parts = [];
+ const rows = [];
+
+ if (!settings.team)
+ return null;
+
+ const config = settings.team.config;
+
+ if (config === null)
+ parts.push(_("Broken configuration"));
+ else {
+ if (config.runner)
+ parts.push(choice_title(team_runner_choices, config.runner.name, config.runner.name));
+ if (config.link_watch && config.link_watch.name != "ethtool")
+ parts.push(choice_title(team_watch_choices, config.link_watch.name, config.link_watch.name));
+ }
+
+ if (parts.length > 0)
+ rows.push(parts.join(", "));
+
+ const configure = <NetworkAction type="team" iface={iface} connectionSettings={settings} />;
+ return renderSettingsRow(_("Team"), rows, configure);
+ }
+
+ function renderTeamPortSettingsRow() {
+ const parts = [];
+ const rows = [];
+
+ if (!settings.team_port)
+ return null;
+
+ /* Only "activebackup" and "lacp" team ports have
+ * something to configure.
+ */
+ if (!group_settings ||
+ !group_settings.team ||
+ !group_settings.team.config ||
+ !group_settings.team.config.runner ||
+ !(group_settings.team.config.runner.name == "activebackup" ||
+ group_settings.team.config.runner.name == "lacp"))
+ return null;
+
+ const config = settings.team_port.config;
+
+ if (config === null)
+ parts.push(_("Broken configuration"));
+
+ if (parts.length > 0)
+ rows.push(parts.join(", "));
+
+ const configure = <NetworkAction type="teamport" iface={iface} connectionSettings={settings} />;
+ return renderSettingsRow(_("Team port"), rows, configure);
+ }
+
+ function renderBridgeSettingsRow() {
+ const rows = [];
+ const options = settings.bridge;
+
+ if (!options)
+ return null;
+
+ function addRow(fmt, args) {
+ rows.push(cockpit.format(fmt, args));
+ }
+
+ if (options.stp) {
+ addRow(_("Spanning tree protocol"));
+ if (options.priority != 32768)
+ addRow(_("Priority $priority"), options);
+ if (options.forward_delay != 15)
+ addRow(_("Forward delay $forward_delay"), options);
+ if (options.hello_time != 2)
+ addRow(_("Hello time $hello_time"), options);
+ if (options.max_age != 20)
+ addRow(_("Maximum message age $max_age"), options);
+ }
+
+ const configure = <NetworkAction type="bridge" iface={iface} connectionSettings={settings} />;
+ return renderSettingsRow(_("Bridge"), rows, configure);
+ }
+
+ function renderBridgePortSettingsRow() {
+ const rows = [];
+ const options = settings.bridge_port;
+
+ if (!options)
+ return null;
+
+ function addRow(fmt, args) {
+ rows.push(cockpit.format(fmt, args));
+ }
+
+ if (options.priority != 32)
+ addRow(_("Priority $priority"), options);
+ if (options.path_cost != 100)
+ addRow(_("Path cost $path_cost"), options);
+ if (options.hairpin_mode)
+ addRow(_("Hairpin mode"));
+
+ const configure = <NetworkAction type="bridgeport" iface={iface} connectionSettings={settings} />;
+ return renderSettingsRow(_("Bridge port"), rows, configure);
+ }
+
+ function renderVlanSettingsRow() {
+ const rows = [];
+ const options = settings.vlan;
+
+ if (!options)
+ return null;
+
+ function addRow(fmt, args) {
+ rows.push(cockpit.format(fmt, args));
+ }
+
+ addRow(_("Parent $parent"), options);
+ addRow(_("ID $id"), options);
+
+ const configure = <NetworkAction type="vlan" iface={iface} connectionSettings={settings} />;
+ return renderSettingsRow(_("VLAN"), rows, configure);
+ }
+
+ function renderWireGuardSettingsRow() {
+ const rows = [];
+ const options = settings.wireguard;
+
+ if (!options) {
+ return null;
+ }
+
+ const configure = <NetworkAction type="wg" iface={iface} connectionSettings={settings} />;
+
+ return renderSettingsRow(_("WireGuard"), rows, configure);
+ }
+
+ return [
+ render_group(),
+ renderAutoconnectRow(),
+ renderIpSettingsRow("ipv4", _("IPv4")),
+ renderIpSettingsRow("ipv6", _("IPv6")),
+ renderMtuSettingsRow(),
+ renderVlanSettingsRow(),
+ renderBridgeSettingsRow(),
+ renderBridgePortSettingsRow(),
+ renderBondSettingsRow(),
+ renderTeamSettingsRow(),
+ renderTeamPortSettingsRow(),
+ renderWireGuardSettingsRow(),
+ ];
+ }
+
+ function renderConnectionMembers(con) {
+ const memberIfaces = { };
+ const members = { };
+
+ const rx_plot_data = {
+ direct: "network.interface.in.bytes",
+ internal: "network.interface.rx",
+ units: "bytes",
+ derive: "rate",
+ factor: 8
+ };
+
+ const tx_plot_data = {
+ direct: "network.interface.out.bytes",
+ internal: "network.interface.tx",
+ units: "bytes",
+ derive: "rate",
+ factor: 8
+ };
+
+ const cs = con && connection_settings(con);
+ if (!con || (cs.type != "bond" && cs.type != "team" && cs.type != "bridge")) {
+ plot_state.plot_instances('rx', rx_plot_data, [dev_name], true);
+ plot_state.plot_instances('tx', tx_plot_data, [dev_name], true);
+ return null;
+ }
+
+ const plot_ifaces = [];
+
+ con.Members.forEach(member_con => {
+ member_con.Interfaces.forEach(iface => {
+ if (iface.MainConnection != member_con)
+ return;
+
+ const dev = iface.Device;
+
+ /* Unmanaged devices shouldn't show up as members
+ * but let's not take any chances.
+ */
+ if (dev && !is_managed(dev))
+ return;
+
+ plot_ifaces.push(iface.Name);
+ usage_monitor.add(iface.Name);
+ members[iface.Name] = iface;
+ memberIfaces[iface.Name] = true;
+ });
+ });
+
+ plot_state.plot_instances('rx', rx_plot_data, plot_ifaces, true);
+ plot_state.plot_instances('tx', tx_plot_data, plot_ifaces, true);
+
+ const sorted_members = Object.keys(members).sort()
+ .map(name => members[name]);
+
+ return (
+ <NetworkInterfaceMembers members={sorted_members}
+ memberIfaces={memberIfaces}
+ interfaces={interfaces}
+ iface={iface}
+ usage_monitor={usage_monitor}
+ privileged={privileged} />
+ );
+ }
+
+ function createGhostConnectionSettings() {
+ const settings = {
+ connection: {
+ interface_name: iface.Name
+ },
+ ipv4: {
+ method: "auto",
+ addresses: [],
+ dns: [],
+ dns_search: [],
+ routes: []
+ },
+ ipv6: {
+ method: "auto",
+ addresses: [],
+ dns: [],
+ dns_search: [],
+ routes: []
+ }
+ };
+ complete_settings(settings, dev);
+ return settings;
+ }
+
+ /* Disable the On/Off button for interfaces that we don't know about at all,
+ and for devices that NM declares to be unavailable. Neither can be activated.
+ */
+
+ let onoff;
+ if (isManaged) {
+ onoff = (
+ <Privileged allowed={privileged}
+ tooltipId="interface-switch"
+ excuse={ _("Not permitted to configure network devices") }>
+ <Switch id="interface-switch"
+ isChecked={!!(dev && dev.ActiveConnection)}
+ isDisabled={!iface || (dev && dev.State == 20) || !privileged}
+ onChange={(_event, enable) => enable ? connect() : disconnect()}
+ aria-label={_("Enable or disable the device")} />
+ </Privileged>
+ );
+ }
+
+ const isDeletable = (iface && !dev) || (dev && (dev.DeviceType == 'bond' ||
+ dev.DeviceType == 'team' ||
+ dev.DeviceType == 'vlan' ||
+ dev.DeviceType == 'bridge' ||
+ dev.DeviceType == 'wireguard'));
+
+ const settingsRows = renderConnectionSettingsRows(iface.MainConnection, connectionSettings)
+ .map((component, idx) => <React.Fragment key={idx}>{component}</React.Fragment>);
+
+ return (
+ <Page id="network-interface"
+ data-test-wait={operationInProgress}>
+ <PageBreadcrumb stickyOnBreakpoint={{ default: "top" }}>
+ <Breadcrumb>
+ <BreadcrumbItem to='#/'>
+ {_("Networking")}
+ </BreadcrumbItem>
+ <BreadcrumbItem isActive>
+ {dev_name}
+ </BreadcrumbItem>
+ </Breadcrumb>
+ </PageBreadcrumb>
+ <PageSection variant={PageSectionVariants.light}>
+ <NetworkPlots plot_state={plot_state} />
+ </PageSection>
+ <PageSection>
+ <Gallery hasGutter>
+ <Card className="network-interface-details">
+ <CardHeader actions={{
+ actions: (
+ <>
+ {isDeletable && isManaged &&
+ <Button variant="danger"
+ onClick={syn_click(model, deleteConnections)}
+ id="network-interface-delete">
+ {_("Delete")}
+ </Button>}
+ {onoff}
+ </>
+ ),
+ }}>
+ <CardTitle className="network-interface-details-title">
+ <span id="network-interface-name">{dev_name}</span>
+ <span id="network-interface-hw">{renderDesc()}</span>
+ <span id="network-interface-mac">{renderMac()}</span>
+ </CardTitle>
+ </CardHeader>
+ <CardBody>
+ <DescriptionList id="network-interface-settings" className="network-interface-settings pf-m-horizontal-on-sm">
+ {renderActiveStatusRow()}
+ {renderCarrierStatusRow()}
+ {settingsRows}
+ </DescriptionList>
+ </CardBody>
+ { !isManaged
+ ? <CardBody>
+ {_("This device cannot be managed here.")}
+ </CardBody>
+ : null
+ }
+ </Card>
+ {renderConnectionMembers(iface.MainConnection)}
+ </Gallery>
+ </PageSection>
+ </Page>
+ );
+};
diff --git a/pkg/networkmanager/network-main.jsx b/pkg/networkmanager/network-main.jsx
new file mode 100644
index 0000000..b623889
--- /dev/null
+++ b/pkg/networkmanager/network-main.jsx
@@ -0,0 +1,215 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from 'react';
+import { useEvent } from "hooks";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+
+import { FirewallSwitch } from "./firewall-switch.jsx";
+import { ListingTable } from "cockpit-components-table.jsx";
+import { NetworkAction } from "./dialogs-common.jsx";
+import { LogsPanel } from "cockpit-components-logs-panel.jsx";
+import { NetworkPlots } from "./plots";
+
+import firewall from './firewall-client.js';
+import {
+ device_state_text,
+ is_managed,
+ render_active_connection,
+} from './interfaces.js';
+
+const _ = cockpit.gettext;
+
+export const NetworkPage = ({ privileged, operationInProgress, usage_monitor, plot_state, interfaces }) => {
+ useEvent(firewall, "changed");
+ useEvent(usage_monitor.grid, "notify");
+
+ const managed = [];
+ const unmanaged = [];
+ const plot_ifaces = [];
+
+ interfaces.forEach(iface => {
+ function hasGroup(iface) {
+ return ((iface.Device &&
+ iface.Device.ActiveConnection &&
+ iface.Device.ActiveConnection.Group &&
+ iface.Device.ActiveConnection.Group.Members.length > 0) ||
+ (iface.MainConnection &&
+ iface.MainConnection.Groups.length > 0));
+ }
+
+ // Skip loopback
+ if (iface.Name == "lo" || (iface.Device && iface.Device.DeviceType == 'loopback'))
+ return;
+
+ // Skip members
+ if (hasGroup(iface))
+ return;
+
+ const dev = iface.Device;
+ const show_traffic = (dev && (dev.State == 100 || dev.State == 10) && dev.Carrier === true);
+
+ plot_ifaces.push(iface.Name);
+ usage_monitor.add(iface.Name);
+
+ const activeConnection = render_active_connection(dev, false, true);
+ const row = {
+ columns: [
+ { title: (!dev || is_managed(dev)) ? <Button variant="link" isInline onClick={() => cockpit.location.go([iface.Name])}>{iface.Name}</Button> : iface.Name },
+ { title: activeConnection },
+ ],
+ props: {
+ key: iface.Name,
+ "data-interface": encodeURIComponent(iface.Name),
+ "data-sample-id": show_traffic ? encodeURIComponent(iface.Name) : null,
+ "data-row-id": iface.Name,
+ }
+ };
+
+ if (show_traffic) {
+ const samples = usage_monitor.samples[iface.Name];
+ row.columns.push({ title: samples ? cockpit.format_bits_per_sec(samples[1][0] * 8) : "" });
+ row.columns.push({ title: samples ? cockpit.format_bits_per_sec(samples[0][0] * 8) : "" });
+ } else {
+ row.columns.push({ title: device_state_text(dev), props: { colSpan: 2 } });
+ }
+
+ if (!dev || is_managed(dev)) {
+ managed.push(row);
+ } else {
+ unmanaged.push(row);
+ }
+ });
+
+ const rx_plot_data = {
+ direct: "network.interface.in.bytes",
+ internal: "network.interface.rx",
+ units: "bytes",
+ derive: "rate",
+ threshold: 200,
+ factor: 8
+ };
+
+ const tx_plot_data = {
+ direct: "network.interface.out.bytes",
+ internal: "network.interface.tx",
+ units: "bytes",
+ derive: "rate",
+ threshold: 200,
+ factor: 8
+ };
+
+ plot_state.plot_instances('rx', rx_plot_data, plot_ifaces);
+ plot_state.plot_instances('tx', tx_plot_data, plot_ifaces);
+
+ /* Start of properties for the LogsPanel component */
+ const match = [
+ "_SYSTEMD_UNIT=NetworkManager.service", "+",
+ "_SYSTEMD_UNIT=firewalld.service"
+ ];
+ const search_options = {
+ prio: "debug",
+ _SYSTEMD_UNIT: "NetworkManager.service,firewalld.service"
+ };
+ const url = "/system/logs/#/?prio=debug&_SYSTEMD_UNIT=NetworkManager.service,firewalld.service";
+ /* End of properties for the LogsPanel component */
+
+ const actions = privileged && (
+ <>
+ <NetworkAction buttonText={_("Add VPN")} type='wg' />
+ <NetworkAction buttonText={_("Add bond")} type='bond' />
+ <NetworkAction buttonText={_("Add team")} type='team' />
+ <NetworkAction buttonText={_("Add bridge")} type='bridge' />
+ <NetworkAction buttonText={_("Add VLAN")} type='vlan' />
+ </>
+ );
+
+ return (
+ <Page data-test-wait={operationInProgress} id="networking">
+ <PageSection id="networking-graphs" className="networking-graphs" variant={PageSectionVariants.light}>
+ <NetworkPlots plot_state={plot_state} />
+ </PageSection>
+ <PageSection>
+ <Gallery hasGutter>
+ {firewall.installed && <Card id="networking-firewall-summary">
+ <CardHeader actions={{
+ actions: <Button variant="secondary" id="networking-firewall-link"
+ component="a"
+ onClick={() => cockpit.jump("/network/firewall", cockpit.transport.host)}>
+ {_("Edit rules and zones")}
+ </Button>,
+ }}>
+ <Flex spaceItems={{ default: 'spaceItemsMd' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <CardTitle component="h2">{_("Firewall")}</CardTitle>
+ <FirewallSwitch firewall={firewall} />
+ </Flex>
+ </CardHeader>
+ <CardBody>
+ <Button variant="link"
+ component="a"
+ isInline
+ onClick={() => cockpit.jump("/network/firewall", cockpit.transport.host)}>
+ {cockpit.format(cockpit.ngettext("$0 active zone", "$0 active zones", firewall.activeZones.size), firewall.activeZones.size)}
+ </Button>
+ </CardBody>
+ </Card>}
+ <Card id="networking-interfaces">
+ <CardHeader actions={{ actions }}>
+ <CardTitle component="h2">{_("Interfaces")}</CardTitle>
+ </CardHeader>
+ <ListingTable aria-label={_("Managed interfaces")}
+ variant='compact'
+ columns={[
+ { title: _("Name"), header: true, props: { width: 25 } },
+ { title: _("IP address"), props: { width: 25 } },
+ { title: _("Sending"), props: { width: 25 } },
+ { title: _("Receiving"), props: { width: 25 } },
+ ]}
+ rows={managed} />
+ </Card>
+ {unmanaged.length > 0 &&
+ <Card id="networking-unmanaged-interfaces">
+ <CardHeader>
+ <CardTitle component="h2">{_("Unmanaged interfaces")}</CardTitle>
+ </CardHeader>
+ <ListingTable aria-label={_("Unmanaged interfaces")}
+ variant='compact'
+ columns={[
+ { title: _("Name"), header: true, props: { width: 25 } },
+ { title: _("IP address"), props: { width: 25 } },
+ { title: _("Sending"), props: { width: 25 } },
+ { title: _("Receiving"), props: { width: 25 } },
+ ]}
+ rows={unmanaged} />
+ </Card>}
+ <LogsPanel title={_("Network logs")} match={match}
+ max={10} search_options={search_options}
+ goto_url={url}
+ className="contains-list" />
+ </Gallery>
+ </PageSection>
+ </Page>
+ );
+};
diff --git a/pkg/networkmanager/networking.scss b/pkg/networkmanager/networking.scss
new file mode 100644
index 0000000..e0fefdd
--- /dev/null
+++ b/pkg/networkmanager/networking.scss
@@ -0,0 +1,316 @@
+@use "ct-card";
+@use "page";
+@import "global-variables";
+
+#firewall,
+#network-page {
+ .pf-v5-c-card {
+ @extend .ct-card;
+ }
+}
+
+#networking, #network-interface {
+ .pf-v5-l-gallery {
+ --pf-v5-l-gallery--GridTemplateColumns: 1fr;
+ }
+}
+
+// General networking page overview
+.network-page {
+ .cockpit-log-panel {
+ max-inline-size: 100vw;
+ }
+}
+
+#network-interface {
+ @at-root {
+ #network-interface-name {
+ font-weight: var(--pf-v5-c-card__title--FontWeight);
+ }
+
+ .network-interface-status > span {
+ overflow-wrap: anywhere;
+ }
+ }
+}
+
+.network-number-field {
+ max-inline-size: 4em;
+}
+
+.network-graph {
+ block-size: 180px;
+}
+
+.pf-v5-c-page__main-breadcrumb + .networking-graphs {
+ // Remove the top padding when following a breadcrumb
+ padding-block-start: 0;
+}
+
+// Constrain widths of networking headers (on both the main page and in network-interface-members)
+th {
+ &.networking-speed {
+ inline-size: 20%;
+ }
+
+ &.networking-spacer {
+ inline-size: var(--pf-v5-global--spacer--4xl);
+ }
+
+ &.networking-action {
+ inline-size: var(--pf-v5-global--spacer--3xl);
+ }
+}
+
+.network-interface-details {
+ &-title {
+ display: grid;
+ grid-auto-flow: column;
+ grid-gap: var(--pf-v5-global--spacer--md);
+ }
+
+ &-delete {
+ margin-inline-end: var(--pf-v5-global--spacer--lg);
+ }
+
+ // Seems to be used for the "general" checkbox
+ .networking-controls {
+ label {
+ font-weight: inherit;
+ }
+
+ input {
+ margin-inline-start: 0;
+ }
+ }
+}
+
+#networking-interfaces, #networking-unmanaged-interfaces {
+ th > .pf-m-link {
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+
+ // Preserve the same baseline as other elements, so items align
+ display: inline;
+ // Expand the link to the container, for easier clickability
+ inline-size: 100%;
+ }
+}
+
+.network-interface-members {
+ .pf-v5-c-switch {
+ margin-inline-end: var(--pf-v5-global--spacer--md);
+ }
+
+ .pf-v5-c-table tbody > tr > * {
+ vertical-align: middle;
+ }
+
+ .btn-group {
+ padding-block: 0.5rem;
+ }
+}
+
+// If the button is not the first item in the DescriptionListDescription add left spacing
+.network-interface-settings dd button:not(:first-child) {
+ margin-inline-start: var(--pf-v5-global--spacer--sm);
+}
+
+// The modal body, by default, overflows to accomodate extra content. This is
+// normall good. However, it also can trim things that flow outside, such as
+// the focus ring of the MTU custom input. Since we know the modal won't have
+// much vertical content, we can disable the overflow to let the focus ring be
+// fully visible. (Otherwise the bottommost pixels are removed.)
+#network-mtu-settings-dialog .pf-v5-c-modal-box__body {
+ overflow: visible;
+}
+
+// Use a grid to lay out elements. This could've been a flex, but a grid lets
+// us size elements from the parent element instead of having to specify
+// individual elements
+#network-mtu-settings-custom + .pf-v5-c-radio__label {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: baseline;
+ gap: var(--pf-v5-global--spacer--md);
+}
+
+// Set the width of the input element; with a little extra space
+.mtu-label-input > input {
+ inline-size: 7ch;
+ font-variant: tabular-nums;
+}
+
+// Temporary curtain to hide the content as it loads
+#testing-connection-curtain {
+ z-index: 2000;
+}
+
+.pf-v5-l-flex > .network-ip-settings-method {
+ inline-size: 12rem;
+}
+
+#network-ip-settings-body {
+ .pf-v5-c-form__label {
+ // Don't allow labels to wrap
+ white-space: nowrap;
+ }
+
+ .remove-button-group {
+ // Move 'Remove' button the the end of the row
+ grid-column: -1;
+ // Move 'Remove' button to the bottom of the line so as to align with the other form fields
+ display: flex;
+ align-items: flex-end;
+ }
+}
+
+/********** Firewall section **********/
+
+#add-services-dialog {
+ .service-list {
+ border: 1px solid var(--pf-v5-c-data-list--BorderTopColor);
+ block-size: 30rem;
+ // full height minus UI, for mobile and small desktops
+ max-block-size: calc(100vh - 20rem);
+ overflow-y: auto;
+ }
+
+ .service-list-item-heading {
+ font-size: 1.2em;
+ margin: 0;
+ }
+
+ .service-list-item-text {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .service-ports {
+ opacity: 0.75;
+
+ &:first-of-type {
+ margin-inline-end: 1em;
+ }
+ }
+
+ .add-services-dialog-type {
+ display: flex;
+ }
+
+ .has-error {
+ animation: 300ms error-slide-down ease-in-out;
+ color: #c00;
+ padding: 0;
+
+ &:empty {
+ display: none;
+ }
+ }
+}
+
+#firewall {
+ block-size: 100%;
+
+ .ct-table tbody tr:first-of-type td:nth-child(2) {
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+ }
+}
+
+@media screen and (max-width: $pf-v5-global--breakpoint--md) {
+ .zone-section-heading.pf-v5-c-card__header {
+ padding-inline-start: var(--pf-v5-global--spacer--md);
+ }
+}
+
+#delete-confirmation-dialog {
+ .delete-confirmation-body {
+ display: flex;
+ }
+}
+
+#add-zone-dialog legend {
+ color: var(--ct-color-subtle-copy);
+ font-size: var(--pf-v5-global--FontSize--sm);
+}
+
+.add-zone-zones legend {
+ line-height: 3;
+}
+
+#add-zone-dialog .add-zone-zones .pf-v5-c-radio__label {
+ text-transform: capitalize;
+}
+
+/* Move firewalld zones higher in z-index (so lines can go behind) */
+.add-zone-zones-firewalld {
+ input {
+ position: relative;
+ z-index: 2;
+ inline-size: 16px;
+ block-size: 16px;
+ }
+
+ > label {
+ /* FIXME: Add lines behind the radio buttons */
+ &::after {
+ border-block-end: 1px solid #d1d1d1;
+ content: "";
+ }
+
+ /* Start line at the midpoint for the first radio */
+ &:first-of-type::after {
+ inset-inline-start: 50%;
+ }
+
+ /* End line at the midpoint for the last radio */
+ &:last-of-type::after {
+ inset-inline-end: 50%;
+ }
+ }
+}
+
+/* Display labels below buttons */
+.add-zone-zones-firewalld, .add-zone-zones-custom {
+ > label.radio {
+ display: inline-flex;
+ }
+}
+
+#add-zone-description-readonly {
+ padding-block: 0.5rem 0;
+ padding-inline: 0;
+ color: var(--ct-color-subtle-copy);
+}
+
+#add-zone-services-readonly legend {
+ padding: 0;
+ line-height: 1;
+}
+
+// Animation for Firewall's add service dialog,
+// as a sudden transition would be (otherwise) too jarring
+@keyframes error-slide-down {
+ 0% {
+ line-height: 0;
+ block-size: 0;
+ opacity: 0;
+ overflow: hidden;
+ }
+
+ 100% {
+ line-height: inherit;
+ block-size: auto;
+ opacity: 1;
+ }
+}
+
+.pf-v5-c-dropdown__menu-item.pf-m-danger {
+ color: var(--pf-v5-global--danger-color--200);
+}
+
+// Remove the default top padding from the toolbar
+.filter-services-toolbar {
+ padding-block-start: 0;
+}
+
+/* End Firewall specific CSS */
diff --git a/pkg/networkmanager/networkmanager.jsx b/pkg/networkmanager/networkmanager.jsx
new file mode 100644
index 0000000..ef4f3c4
--- /dev/null
+++ b/pkg/networkmanager/networkmanager.jsx
@@ -0,0 +1,156 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import cockpit from "cockpit";
+import 'cockpit-dark-theme'; // once per page
+import React, { useRef } from 'react';
+import { createRoot } from "react-dom/client";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { ExclamationCircleIcon } from "@patternfly/react-icons";
+
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { ModelContext } from './model-context.jsx';
+import { NetworkInterfacePage } from './network-interface.jsx';
+import { NetworkPage } from './network-main.jsx';
+import { UsageMonitor } from './helpers.js';
+
+import * as service from 'service.js';
+import { init as initDialogs, NetworkManagerModel } from './interfaces.js';
+import { superuser } from 'superuser';
+import { PlotState } from 'plot';
+
+import { useObject, useEvent, usePageLocation } from "hooks";
+import { WithDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+const App = () => {
+ const nmService = useObject(() => service.proxy("NetworkManager"),
+ null,
+ []);
+ useEvent(nmService, "changed");
+
+ const model = useObject(() => new NetworkManagerModel(), null, []);
+ useEvent(model, "changed");
+
+ const nmRunning_ref = useRef(undefined);
+ useEvent(model.client, "owner", (event, owner) => { nmRunning_ref.current = owner !== null });
+
+ const { path } = usePageLocation();
+
+ useEvent(superuser, "changed");
+
+ const usage_monitor = useObject(() => new UsageMonitor(), null, []);
+ const plot_state_main = useObject(() => new PlotState(), null, []);
+ const plot_state_iface = useObject(() => new PlotState(), null, []);
+
+ if (model.curtain == 'testing' || model.curtain == 'restoring') {
+ return <EmptyStatePanel loading title={model.curtain == 'testing' ? _("Testing connection") : _("Restoring connection")} />;
+ }
+
+ if (model.ready === undefined)
+ return <EmptyStatePanel loading />;
+
+ /* Show EmptyStatePanel when nm is not running */
+ if (!nmRunning_ref.current) {
+ if (nmService.enabled) {
+ return (
+ <div id="networking-nm-crashed">
+ <EmptyStatePanel icon={ ExclamationCircleIcon }
+ title={ _("NetworkManager is not running") }
+ action={ name ? _("Start service") : null }
+ onAction={ nmService.start }
+ secondary={
+ <Button component="a"
+ variant="secondary"
+ onClick={() => cockpit.jump("/system/services#/NetworkManager.service", cockpit.transport.host)}>
+ {_("Troubleshoot…")}
+ </Button>
+ } />
+ </div>
+ );
+ } else if (!nmService.exists) {
+ return (
+ <div id="networking-nm-not-found">
+ <EmptyStatePanel icon={ ExclamationCircleIcon }
+ title={ _("NetworkManager is not installed") } />
+
+ </div>
+ );
+ } else {
+ return (
+ <div id="networking-nm-disabled">
+ <EmptyStatePanel icon={ ExclamationCircleIcon }
+ title={ _("Network devices and graphs require NetworkManager") }
+ action={ name ? _("Enable service") : null }
+ onAction={() => {
+ nmService.enable();
+ nmService.start();
+ }} />
+
+ </div>
+ );
+ }
+ }
+
+ const interfaces = model.list_interfaces();
+
+ /* At this point NM is running and the model is ready */
+ if (path.length == 0) {
+ return (
+ <ModelContext.Provider value={model}>
+ <WithDialogs key="1">
+ <NetworkPage privileged={superuser.allowed}
+ operationInProgress={model.operationInProgress}
+ usage_monitor={usage_monitor}
+ plot_state={plot_state_main}
+ interfaces={interfaces} />
+ </WithDialogs>
+ </ModelContext.Provider>
+ );
+ } else if (path.length == 1) {
+ const iface = interfaces.find(iface => iface.Name == path[0]);
+
+ if (iface) {
+ return (
+ <ModelContext.Provider value={model}>
+ <WithDialogs key="2">
+ <NetworkInterfacePage privileged={superuser.allowed}
+ operationInProgress={model.operationInProgress}
+ usage_monitor={usage_monitor}
+ plot_state={plot_state_iface}
+ interfaces={interfaces}
+ iface={iface} />
+ </WithDialogs>
+ </ModelContext.Provider>
+ );
+ }
+ }
+
+ return null;
+};
+
+function init() {
+ initDialogs();
+ const root = createRoot(document.getElementById("network-page"));
+ root.render(<App />);
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/pkg/networkmanager/plots.js b/pkg/networkmanager/plots.js
new file mode 100644
index 0000000..a2780a6
--- /dev/null
+++ b/pkg/networkmanager/plots.js
@@ -0,0 +1,49 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+
+import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
+import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js";
+import { ZoomControls, SvgPlot, bits_per_sec_config } from "cockpit-components-plot.jsx";
+
+import cockpit from "cockpit";
+const _ = cockpit.gettext;
+
+export const NetworkPlots = ({ plot_state }) => {
+ return (
+ <>
+ <Split>
+ <SplitItem isFilled />
+ <SplitItem><ZoomControls plot_state={plot_state} /></SplitItem>
+ </Split>
+ <Grid sm={12} md={6} lg={6} hasGutter>
+ <GridItem>
+ <SvgPlot className="network-graph"
+ title={_("Transmitting")} config={bits_per_sec_config}
+ plot_state={plot_state} plot_id='tx' />
+ </GridItem>
+ <GridItem>
+ <SvgPlot className="network-graph"
+ title={_("Receiving")} config={bits_per_sec_config}
+ plot_state={plot_state} plot_id='rx' />
+ </GridItem>
+ </Grid>
+ </>);
+};
diff --git a/pkg/networkmanager/team.jsx b/pkg/networkmanager/team.jsx
new file mode 100644
index 0000000..fc96133
--- /dev/null
+++ b/pkg/networkmanager/team.jsx
@@ -0,0 +1,228 @@
+/*
+
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useContext } from 'react';
+import cockpit from 'cockpit';
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+import { MemberInterfaceChoices, NetworkModal, Name, dialogSave } from './dialogs-common.jsx';
+import { ModelContext } from './model-context.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+import { v4 as uuidv4 } from 'uuid';
+import {
+ member_connection_for_interface,
+ member_interface_choices,
+} from './interfaces.js';
+
+const _ = cockpit.gettext;
+
+export const team_runner_choices =
+ [
+ { choice: 'roundrobin', title: _("Round robin") },
+ { choice: 'activebackup', title: _("Active backup") },
+ { choice: 'loadbalance', title: _("Load balancing") },
+ { choice: 'broadcast', title: _("Broadcast") },
+ { choice: 'lacp', title: _("802.3ad LACP") },
+ ];
+
+export const team_balancer_choices =
+ [
+ { choice: 'none', title: _("Passive") },
+ { choice: 'basic', title: _("Active") }
+ ];
+
+export const team_watch_choices =
+ [
+ { choice: 'ethtool', title: _("Ethtool") },
+ { choice: 'arp-ping', title: _("ARP ping") },
+ { choice: 'nsna-ping', title: _("NSNA ping") }
+ ];
+
+export const TeamDialog = ({ connection, dev, settings }) => {
+ const Dialogs = useDialogs();
+ const idPrefix = "network-team-settings";
+ const model = useContext(ModelContext);
+ const config = settings.team.config || {};
+ if (!config.runner)
+ config.runner = { };
+ if (!config.runner.name)
+ config.runner.name = "activebackup";
+ if (!config.link_watch)
+ config.link_watch = { };
+ if (!config.link_watch.name)
+ config.link_watch.name = "ethtool";
+ if (config.link_watch.interval === undefined)
+ config.link_watch.interval = 100;
+ if (config.link_watch.delay_up === undefined)
+ config.link_watch.delay_up = 0;
+ if (config.link_watch.delay_down === undefined)
+ config.link_watch.delay_down = 0;
+
+ const memberChoicesInit = {};
+
+ member_interface_choices(model, connection).forEach((iface) => {
+ memberChoicesInit[iface.Name] = !!member_connection_for_interface(connection, iface);
+ });
+
+ const [balancer, setBalancer] = useState(config.balancer);
+ const [dialogError, setDialogError] = useState(undefined);
+ const [iface, setIface] = useState(settings.connection.interface_name);
+ const [linkDownDelay, setLinkDownDelay] = useState(config.link_watch.delay_down);
+ const [linkWatch, setLinkWatch] = useState(config.link_watch.name);
+ const [linkUpDelay, setLinkUpDelay] = useState(config.link_watch.delay_up);
+ const [memberChoices, setMemberChoices] = useState(memberChoicesInit);
+ const [runner, setRunner] = useState(config.runner.name);
+ const [pingTarget, setPingTarget] = useState(config.link_watch.target_host);
+ const [pingInterval, setPingInterval] = useState(config.link_watch.interval);
+ const [primary, setPrimary] = useState(undefined);
+
+ const onSubmit = (ev) => {
+ const createSettingsObj = () => ({
+ ...settings,
+ connection: {
+ ...settings.connection,
+ id: iface,
+ interface_name: iface,
+ },
+ team: {
+ ...settings.team,
+ interface_name: iface,
+ config: {
+ ...settings.team.config,
+ link_watch: {
+ name: linkWatch,
+ ...(linkWatch == 'ethtool' && {
+ delay_up: linkUpDelay,
+ delay_down: linkDownDelay,
+ }),
+ ...(linkWatch != 'ethtool' && {
+ interval: pingInterval,
+ target_host: pingTarget,
+ }),
+ },
+ runner: {
+ ...config.runner,
+ name: runner,
+ ...((runner == "loadbalance" || runner == "lacp") && { tx_balancer: balancer == 'none' ? {} : balancer })
+ }
+ }
+ }
+ });
+
+ dialogSave({
+ model,
+ dev,
+ connection,
+ members: memberChoices,
+ membersInit: memberChoicesInit,
+ settings: createSettingsObj(),
+ setDialogError,
+ onClose: Dialogs.close,
+ });
+
+ // Prevent dialog from closing because of <form> onsubmit event
+ if (event)
+ event.preventDefault();
+
+ return false;
+ };
+
+ return (
+ <NetworkModal dialogError={dialogError}
+ idPrefix={idPrefix}
+ onSubmit={onSubmit}
+ title={!connection ? _("Add team") : _("Edit team settings")}
+ isCreateDialog={!connection}
+ >
+ <>
+ <Name idPrefix={idPrefix} iface={iface} setIface={setIface} />
+ <FormGroup label={_("Ports")} fieldId={idPrefix + "-interface-members-list"} hasNoPaddingTop>
+ <MemberInterfaceChoices idPrefix={idPrefix} memberChoices={memberChoices} setMemberChoices={setMemberChoices} model={model} group={connection} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-runner-select"} label={_("Runner")}>
+ <FormSelect id={idPrefix + "-runner-select"} onChange={(_, val) => setRunner(val)}
+ value={runner}>
+ {team_runner_choices.map(choice => <FormSelectOption value={choice.choice} label={choice.title} key={choice.choice} />)}
+ </FormSelect>
+ </FormGroup>
+ {(runner == "loadbalance" || runner == "lacp") && <FormGroup fieldId={idPrefix + "-balancer-select"} label={_("Balancer")}>
+ <FormSelect id={idPrefix + "-balancer-select"} onChange={(_, val) => setBalancer(val)}
+ value={balancer}>
+ {team_balancer_choices.map(choice => <FormSelectOption value={choice.choice} label={choice.title} key={choice.choice} />)}
+ </FormSelect>
+ </FormGroup>}
+ {runner == "activebackup" && <FormGroup fieldId={idPrefix + "-primary-select"} label={_("Primary")}>
+ <FormSelect id={idPrefix + "-primary-select"} onChange={(_, val) => setPrimary(val)}
+ value={primary}>
+ <>
+ <FormSelectOption key='-' value={null} label='-' />
+ {Object.keys(memberChoices)
+ .filter(iface => memberChoices[iface])
+ .map(iface => <FormSelectOption key={iface} label={iface} value={iface} />)}
+ </>
+ </FormSelect>
+ </FormGroup>}
+ <FormGroup fieldId={idPrefix + "-link-watch-select"} label={_("Link watch")}>
+ <FormSelect id={idPrefix + "-link-watch-select"} onChange={(_, val) => setLinkWatch(val)}
+ value={linkWatch}>
+ {team_watch_choices.map(choice => <FormSelectOption value={choice.choice} label={choice.title} key={choice.choice} />)}
+ </FormSelect>
+ </FormGroup>
+ {linkWatch == 'ethtool' && <>
+ <FormGroup fieldId={idPrefix + "-link-up-delay-input"} label={_("Link up delay")}>
+ <TextInput id={idPrefix + "-link-up-delay-input"} className="network-number-field" value={linkUpDelay} onChange={(_event, value) => setLinkUpDelay(value)} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-link-down-delay-input"} label={_("Link down delay")}>
+ <TextInput id={idPrefix + "-link-down-delay-input"} className="network-number-field" value={linkDownDelay} onChange={(_event, value) => setLinkDownDelay(value)} />
+ </FormGroup>
+ </>}
+ {linkWatch != 'ethtool' && <>
+ <FormGroup fieldId={idPrefix + "-ping-interval-input"} label={_("Ping interval")}>
+ <TextInput id={idPrefix + "-ping-interval-input"} className="network-number-field" value={pingInterval} onChange={(_event, value) => setPingInterval(value)} />
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-ping-target-input"} label={_("Ping target")}>
+ <TextInput id={idPrefix + "-ping-target-input"} value={pingTarget} onChange={(_event, value) => setPingTarget(value)} />
+ </FormGroup>
+ </>}
+ </>
+ </NetworkModal>
+ );
+};
+
+export const getGhostSettings = ({ newIfaceName }) => {
+ return (
+ {
+ connection: {
+ id: newIfaceName,
+ autoconnect: true,
+ type: "team",
+ uuid: uuidv4(),
+ interface_name: newIfaceName,
+ },
+ team: {
+ config: {},
+ interface_name: newIfaceName
+ }
+ }
+ );
+};
diff --git a/pkg/networkmanager/teamport.jsx b/pkg/networkmanager/teamport.jsx
new file mode 100644
index 0000000..cb16682
--- /dev/null
+++ b/pkg/networkmanager/teamport.jsx
@@ -0,0 +1,99 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useContext } from 'react';
+import cockpit from 'cockpit';
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+import { NetworkModal, dialogSave } from './dialogs-common.jsx';
+import { ModelContext } from './model-context.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+export const TeamPortDialog = ({ connection, dev, settings }) => {
+ const Dialogs = useDialogs();
+ const idPrefix = "network-team-port-settings";
+ const model = useContext(ModelContext);
+
+ const group_settings = connection.Groups[0].Settings;
+ const group_config = group_settings.team.config;
+ const teamMode = group_config.runner.name;
+ let config = settings.team_port.config;
+
+ if (!config)
+ config = config = { };
+
+ const [priority, setPriority] = useState(teamMode == 'activebackup' ? config.prio : config.lacp_prio);
+ const [sticky, setSticky] = useState(config.sticky);
+ const [key, setKey] = useState(config.lacp_key);
+ const [dialogError, setDialogError] = useState(undefined);
+
+ const onSubmit = (ev) => {
+ const createSettingsObj = () => ({
+ ...settings,
+ team_port: {
+ config: {
+ ...config,
+ ...(teamMode == 'activebackup' && { prio: priority, sticky }),
+ ...(teamMode == 'lacp' && { lacp_prio: priority, lacp_key: key }),
+ }
+ }
+ });
+
+ dialogSave({
+ model,
+ dev,
+ connection,
+ settings: createSettingsObj(),
+ setDialogError,
+ onClose: Dialogs.close,
+ });
+
+ // Prevent dialog from closing because of <form> onsubmit event
+ if (event)
+ event.preventDefault();
+
+ return false;
+ };
+
+ return (
+ <NetworkModal dialogError={dialogError}
+ idPrefix={idPrefix}
+ onSubmit={onSubmit}
+ title={_("Team port settings")}
+ >
+ <FormGroup fieldId={idPrefix + "-" + teamMode + "-prio-input"} label={_("Priority")}>
+ <TextInput id={idPrefix + "-" + teamMode + "-prio-input"} value={priority} onChange={(_event, value) => setPriority(value)} />
+ </FormGroup>
+ {teamMode == 'activebackup'
+ ? <FormGroup fieldId={idPrefix + "-activebackup-sticky-input"}>
+ <Checkbox id={idPrefix + "-activebackup-sticky-input"} isChecked={sticky} onChange={(_, s) => setSticky(s)} label={_("Sticky")} />
+ </FormGroup>
+ : null}
+ {teamMode == 'lacp'
+ ? <FormGroup fieldId={idPrefix + "-" + teamMode + "-key-input"} label={_("LACP key")}>
+ <TextInput id={idPrefix + "-" + teamMode + "-key-input"} value={key} onChange={(_event, value) => setKey(value)} />
+ </FormGroup>
+ : null}
+ </NetworkModal>
+ );
+};
diff --git a/pkg/networkmanager/test-utils.js b/pkg/networkmanager/test-utils.js
new file mode 100644
index 0000000..27263a4
--- /dev/null
+++ b/pkg/networkmanager/test-utils.js
@@ -0,0 +1,318 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import * as utils from "./utils.js";
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+
+function assert_throws(assert, func, checks) {
+ assert.expect(checks.length);
+
+ checks.forEach(function(c) {
+ assert.throws(function() {
+ func(c);
+ });
+ });
+}
+
+QUnit.test("ip_prefix_from_text", function (assert) {
+ const checks = [
+ ["0", 0],
+ ["12", 12],
+ [" 12 ", 12]
+ ];
+
+ assert.expect(checks.length);
+
+ checks.forEach(function(c) {
+ assert.strictEqual(utils.ip_prefix_from_text(c[0]), c[1]);
+ });
+});
+
+QUnit.test("ip_prefix_from_text invalids", function (assert) {
+ const checks = [
+ "",
+ "-1",
+ "foo",
+ "1foo",
+ "1.5",
+ "1 2 3"
+ ];
+
+ assert_throws(assert, utils.ip_prefix_from_text, checks);
+});
+
+QUnit.test("ip_metric_from_text", function (assert) {
+ const checks = [
+ ["", 0],
+ ["0", 0],
+ ["12", 12],
+ [" 12 ", 12]
+ ];
+
+ assert.expect(checks.length);
+
+ checks.forEach(function(c) {
+ assert.strictEqual(utils.ip_metric_from_text(c[0]), c[1]);
+ });
+});
+
+QUnit.test("ip_metric_from_text invalids", function (assert) {
+ const checks = [
+ "-1",
+ "foo",
+ "1foo",
+ "1.5",
+ "1 2 3"
+ ];
+
+ assert_throws(assert, utils.ip_metric_from_text, checks);
+});
+
+QUnit.test("ip4_to/from_text be", function (assert) {
+ const checks = [
+ ["0.0.0.0", 0x00000000],
+ ["255.255.255.255", 0xFFFFFFFF],
+ ["1.2.3.4", 0x01020304],
+ [" 1.2.3.4 ", 0x01020304],
+ [" 1 . 2 . 3. 4 ", 0x01020304]
+ ];
+
+ assert.expect(2 * checks.length);
+
+ utils.set_byteorder("be");
+ checks.forEach(function(c) {
+ assert.strictEqual(utils.ip4_to_text(c[1]), c[0].replaceAll(" ", ""));
+ assert.strictEqual(utils.ip4_from_text(c[0]), c[1]);
+ });
+});
+
+QUnit.test("ip4_to/from_text le", function (assert) {
+ const checks = [
+ ["0.0.0.0", 0x00000000],
+ ["255.255.255.255", 0xFFFFFFFF],
+ ["1.2.3.4", 0x04030201],
+ [" 1.2.3.4 ", 0x04030201],
+ [" 1 . 2 . 3. 4 ", 0x04030201]
+ ];
+
+ assert.expect(2 * checks.length);
+
+ utils.set_byteorder("le");
+ checks.forEach(function(c) {
+ assert.strictEqual(utils.ip4_to_text(c[1]), c[0].replaceAll(" ", ""));
+ assert.strictEqual(utils.ip4_from_text(c[0]), c[1]);
+ });
+});
+
+QUnit.test("ip4_from_text invalids", function (assert) {
+ const checks = [
+ "",
+ "0",
+ "0.0",
+ "0.0.0",
+ "0.0.0.0.0",
+ "-1.2.3.4",
+ "foo",
+ "1.foo.3.4",
+ "1foo.2.3.4",
+ "1.2.3.400",
+ "1,2,3,4",
+ "1 1.2.3 3.4"
+ ];
+
+ assert_throws(assert, utils.ip4_from_text, checks);
+});
+
+QUnit.test("ip4_to_text zero", function (assert) {
+ utils.set_byteorder("be");
+ assert.strictEqual(utils.ip4_to_text(0, true), "");
+});
+
+QUnit.test("ip4_from_text empty", function (assert) {
+ utils.set_byteorder("be");
+ assert.strictEqual(utils.ip4_from_text("", true), 0);
+});
+
+QUnit.test("ip4_to/from_text invalid byteorder", function (assert) {
+ utils.set_byteorder(undefined);
+ assert.throws(function() { utils.ip4_from_text("1.2.3.4") });
+ assert.throws(function() { utils.ip4_to_text(0x01020304) });
+});
+
+QUnit.test("ip4_prefix_from_text", function (assert) {
+ const checks = [
+ "0.0.0.0",
+
+ " 128.0.0.0",
+ "192.0.0.0 ",
+ "224. 0. 0.0",
+ "240. 0.0 .0",
+ "248.0.0.0",
+ "252. 0.0.0",
+ "254.0.0.0",
+ "255.0.0.0",
+
+ "255.128.0.0",
+ "255.192.0.0",
+ "255.224.0.0",
+ "255.240.0.0",
+ "255.248.0.0",
+ "255.252.0.0",
+ "255.254.0.0",
+ "255.255.0.0",
+
+ "255.255.128.0",
+ "255.255.192.0",
+ "255.255.224.0",
+ "255.255.240.0",
+ "255.255.248.0",
+ "255.255.252.0",
+ "255.255.254.0",
+ "255.255.255.0",
+
+ "255.255.255.128",
+ "255.255.255.192",
+ "255.255.255.224",
+ "255.255.255.240",
+ "255.255.255.248",
+ "255.255.255.252",
+ "255.255.255.254",
+ "255.255.255.255"
+ ];
+
+ assert.expect(checks.length);
+
+ checks.forEach(function(c, i) {
+ assert.strictEqual(utils.ip4_prefix_from_text(c), i);
+ });
+});
+
+QUnit.test("ip4_prefix_from_text invalids", function (assert) {
+ const checks = [
+ "",
+ "-1",
+ "foo",
+ "1foo",
+ "1.5",
+
+ "0.0",
+ "0.0.0",
+ "0.0.0.0.0",
+ "1.2.3.4",
+ "255.255.255.8",
+ "255.192.0.10"
+ ];
+
+ assert_throws(assert, utils.ip4_prefix_from_text, checks);
+});
+
+QUnit.test("ip6_to/from_text", function (assert) {
+ const checks = [
+ [[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+ "0:0:0:0:0:0:0:0"
+ ],
+ [[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F],
+ "1:203:405:607:809:a0b:c0d:e0f"
+ ],
+ [[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F],
+ " 1: 203 : 405: 607: 809:a0b :c0d:e0f"
+ ],
+ ];
+
+ assert.expect(2 * checks.length);
+
+ checks.forEach(function(c) {
+ assert.strictEqual(utils.ip6_to_text(cockpit.base64_encode(c[0])), c[1].replaceAll(" ", ""));
+ assert.deepEqual(cockpit.base64_decode(utils.ip6_from_text(c[1])), c[0]);
+ });
+});
+
+QUnit.test("ip6_from_text abbrevs", function (assert) {
+ const checks = [
+ ["::",
+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+ ],
+ ["::1",
+ [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
+ ],
+ ["1::",
+ [0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
+ ],
+ ["1:2:3::2:1",
+ [0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01],
+ ],
+ ["2001::1",
+ [0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
+ ],
+ ];
+
+ checks.forEach(function(c) {
+ assert.deepEqual(cockpit.base64_decode(utils.ip6_from_text(c[0])), c[1]);
+ });
+});
+
+QUnit.test("ip6_from_text invalids", function (assert) {
+ const checks = [
+ "",
+ "0",
+ "0:0",
+ "0:0:0",
+ "0:0:0:0",
+ "0:0:0:0:0",
+ "0:0:0:0:0:0",
+ "0:0:0:0:0:0:0",
+ "0:0:0:0:0:0:0:0:0",
+ "foo",
+ "1:2:3:four:5:6:7:8",
+ "1:2:3:-4:5:6:7:8",
+ "1:2:3:4.0:5:6:7:8",
+ "1:2:3:4foo:5:6:7:8",
+ "1:2:3:10000:5:6:7:8",
+ "1::4::8",
+ "::8::",
+ "1:2:3:4 4:5:6:7:8",
+ ];
+
+ assert_throws(assert, utils.ip6_from_text, checks);
+});
+
+QUnit.test("ip6_to_text zero", function (assert) {
+ const zero = [0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0
+ ];
+ assert.strictEqual(utils.ip6_to_text(cockpit.base64_encode(zero), true), "");
+});
+
+QUnit.test("ip6_from_text empty", function (assert) {
+ const zero = [0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0
+ ];
+ assert.deepEqual(cockpit.base64_decode(utils.ip6_from_text("", true)), zero);
+});
+
+QUnit.start();
diff --git a/pkg/networkmanager/utils.js b/pkg/networkmanager/utils.js
new file mode 100644
index 0000000..7fdc91a
--- /dev/null
+++ b/pkg/networkmanager/utils.js
@@ -0,0 +1,247 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+const _ = cockpit.gettext;
+
+/* NetworkManager specific data conversions and utility functions.
+ */
+
+let byteorder;
+
+export function set_byteorder(bo) {
+ byteorder = bo;
+}
+
+export function ip_prefix_to_text(num) {
+ return num.toString();
+}
+
+export function ip_prefix_from_text(text) {
+ if (/^[0-9]+$/.test(text.trim()))
+ return parseInt(text, 10);
+
+ throw cockpit.format(_("Invalid prefix $0"), text);
+}
+
+export function ip_metric_to_text(num) {
+ return num.toString();
+}
+
+export function ip_metric_from_text(text) {
+ if (text === "")
+ return 0;
+
+ if (/^[0-9]+$/.test(text.trim()))
+ return parseInt(text, 10);
+
+ throw cockpit.format(_("Invalid metric $0"), text);
+}
+
+function toDec(n) {
+ return n.toString(10);
+}
+
+function bytes_from_nm32(num) {
+ const bytes = [];
+ if (byteorder == "be") {
+ for (let i = 3; i >= 0; i--) {
+ bytes[i] = num & 0xFF;
+ num = num >>> 8;
+ }
+ } else if (byteorder == "le") {
+ for (let i = 0; i < 4; i++) {
+ bytes[i] = num & 0xFF;
+ num = num >>> 8;
+ }
+ } else {
+ throw new Error("byteorder is unset or has invalid value " + JSON.stringify(byteorder));
+ }
+ return bytes;
+}
+
+export function ip4_to_text(num, zero_is_empty) {
+ if (num === 0 && zero_is_empty)
+ return "";
+ return bytes_from_nm32(num).map(toDec)
+ .join('.');
+}
+
+export function ip4_from_text(text, empty_is_zero) {
+ function invalid() {
+ throw cockpit.format(_("Invalid address $0"), text);
+ }
+
+ if (text === "" && empty_is_zero)
+ return 0;
+
+ const parts = text.split('.');
+ if (parts.length != 4)
+ invalid();
+
+ const bytes = parts.map(function(s) {
+ if (/^[0-9]+$/.test(s.trim()))
+ return parseInt(s, 10);
+ else
+ return invalid();
+ });
+
+ let num = 0;
+ function shift(b) {
+ if (isNaN(b) || b < 0 || b > 0xFF)
+ invalid();
+ num = 0x100 * num + b;
+ }
+
+ if (byteorder == "be") {
+ for (let i = 0; i < 4; i++) {
+ shift(bytes[i]);
+ }
+ } else if (byteorder == "le") {
+ for (let i = 3; i >= 0; i--) {
+ shift(bytes[i]);
+ }
+ } else {
+ throw new Error("byteorder is unset or has invalid value " + JSON.stringify(byteorder));
+ }
+
+ return num;
+}
+
+const text_to_prefix_bits = {
+ 255: 8, 254: 7, 252: 6, 248: 5, 240: 4, 224: 3, 192: 2, 128: 1, 0: 0
+};
+
+export function ip4_prefix_from_text(text) {
+ function invalid() {
+ throw cockpit.format(_("Invalid prefix or netmask $0"), text);
+ }
+
+ if (/^[0-9]+$/.test(text.trim()))
+ return parseInt(text, 10);
+ const parts = text.split('.');
+ if (parts.length != 4)
+ invalid();
+ let prefix = 0;
+ let i;
+ for (i = 0; i < 4; i++) {
+ const p = text_to_prefix_bits[parts[i].trim()];
+ if (p !== undefined) {
+ prefix += p;
+ if (p < 8)
+ break;
+ } else
+ invalid();
+ }
+ for (i += 1; i < 4; i++) {
+ if (/^0+$/.test(parts[i].trim()) === false)
+ invalid();
+ }
+ return prefix;
+}
+
+export function ip6_to_text(data, zero_is_empty) {
+ const parts = [];
+ const bytes = cockpit.base64_decode(data);
+ for (let i = 0; i < 8; i++)
+ parts[i] = ((bytes[2 * i] << 8) + bytes[2 * i + 1]).toString(16);
+ const result = parts.join(':');
+ if (result == "0:0:0:0:0:0:0:0" && zero_is_empty)
+ return "";
+ return result;
+}
+
+export function ip6_from_text(text, empty_is_zero) {
+ function invalid() {
+ throw cockpit.format(_("Invalid address $0"), text);
+ }
+
+ if (text === "" && empty_is_zero)
+ return cockpit.base64_encode([0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ ]);
+
+ const parts = text.split(':');
+ if (parts.length < 1 || parts.length > 8)
+ invalid();
+
+ if (parts[0] === "")
+ parts[0] = "0";
+ if (parts[parts.length - 1] === "")
+ parts[parts.length - 1] = "0";
+
+ const bytes = [];
+ let empty_seen = false;
+ let j = 0;
+ for (let i = 0; i < parts.length; i++, j++) {
+ if (parts[i] === "") {
+ if (empty_seen)
+ invalid();
+ empty_seen = true;
+ while (j < i + (8 - parts.length)) {
+ bytes[2 * j] = bytes[2 * j + 1] = 0;
+ j++;
+ }
+ } else {
+ if (!/^[0-9a-fA-F]+$/.test(parts[i].trim()))
+ invalid();
+ const n = parseInt(parts[i], 16);
+ if (isNaN(n) || n < 0 || n > 0xFFFF)
+ invalid();
+ bytes[2 * j] = n >> 8;
+ bytes[2 * j + 1] = n & 0xFF;
+ }
+ }
+ if (j != 8)
+ invalid();
+
+ return cockpit.base64_encode(bytes);
+}
+
+export function list_interfaces() {
+ return new Promise((resolve, reject) => {
+ const client = cockpit.dbus("org.freedesktop.NetworkManager");
+ client.call('/org/freedesktop/NetworkManager',
+ 'org.freedesktop.NetworkManager',
+ 'GetAllDevices', [])
+ .then(reply => {
+ Promise.all(reply[0].map(device => {
+ return Promise.all([
+ client.call(device,
+ 'org.freedesktop.DBus.Properties',
+ 'Get', ['org.freedesktop.NetworkManager.Device', 'Interface'])
+ .then(reply => reply[0]),
+ client.call(device,
+ 'org.freedesktop.DBus.Properties',
+ 'Get', ['org.freedesktop.NetworkManager.Device', 'Capabilities'])
+ .then(reply => reply[0])
+ ]);
+ }))
+ .then(interfaces => {
+ client.close();
+ resolve(interfaces.map(i => {
+ return { device: i[0].v, capabilities: i[1].v };
+ }));
+ })
+ .catch(e => console.warn(JSON.stringify(e)));
+ })
+ .catch(e => console.warn(JSON.stringify(e)));
+ });
+}
diff --git a/pkg/networkmanager/vlan.jsx b/pkg/networkmanager/vlan.jsx
new file mode 100644
index 0000000..2270ca2
--- /dev/null
+++ b/pkg/networkmanager/vlan.jsx
@@ -0,0 +1,134 @@
+/*
+
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useContext } from 'react';
+import cockpit from 'cockpit';
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+import { Name, NetworkModal, dialogSave } from './dialogs-common.jsx';
+import { ModelContext } from './model-context.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+import { v4 as uuidv4 } from 'uuid';
+import {
+ is_interface_connection,
+ is_interesting_interface,
+} from './interfaces.js';
+
+const _ = cockpit.gettext;
+
+export const VlanDialog = ({ connection, dev, settings }) => {
+ const Dialogs = useDialogs();
+ const idPrefix = "network-vlan-settings";
+ const model = useContext(ModelContext);
+ const parentChoices = [];
+ model.list_interfaces().forEach(iface => {
+ if (!is_interface_connection(iface, connection) &&
+ is_interesting_interface(iface))
+ parentChoices.push(iface.Name);
+ });
+
+ const [dialogError, setDialogError] = useState(undefined);
+ const [parent, setParent] = useState(settings.vlan.parent || parentChoices[0]);
+ const [vlanId, setVlanId] = useState(settings.vlan.id || 1);
+ const [iface, setIface] = useState(settings.vlan.interface_name || (parent + "." + vlanId));
+
+ const onSubmit = (ev) => {
+ const createSettingsObj = () => ({
+ ...settings,
+ connection: {
+ ...settings.connection,
+ id: iface,
+ interface_name: iface,
+ },
+ vlan: {
+ ...settings.vlan,
+ parent,
+ id: parseInt(vlanId, 10),
+ interface_name: iface,
+ }
+ });
+
+ dialogSave({
+ model,
+ dev,
+ connection,
+ settings: createSettingsObj(),
+ setDialogError,
+ onClose: Dialogs.close,
+ });
+
+ // Prevent dialog from closing because of <form> onsubmit event
+ if (event)
+ event.preventDefault();
+
+ return false;
+ };
+
+ return (
+ <NetworkModal dialogError={dialogError}
+ idPrefix={idPrefix}
+ onSubmit={onSubmit}
+ title={!connection ? _("Add VLAN") : _("Edit VLAN settings")}
+ isCreateDialog={!connection}
+ >
+ <>
+ <FormGroup fieldId={idPrefix + "-parent-select"} label={_("Parent")}>
+ <FormSelect id={idPrefix + "-parent-select"} onChange={(_, value) => {
+ setParent(value);
+ if (iface == (parent + "." + vlanId))
+ setIface(value + "." + vlanId);
+ }}
+ value={parent}>
+ {parentChoices.map(choice => <FormSelectOption value={choice} label={choice} key={choice} />)}
+ </FormSelect>
+ </FormGroup>
+ <FormGroup fieldId={idPrefix + "-vlan-id-input"} label={_("VLAN ID")}>
+ <TextInput id={idPrefix + "-vlan-id-input"} value={vlanId} onChange={(_event, value) => {
+ setVlanId(value);
+ if (iface == (parent + "." + vlanId))
+ setIface(parent + "." + value);
+ }} />
+ </FormGroup>
+ <Name idPrefix={idPrefix} iface={iface} setIface={setIface} />
+ </>
+ </NetworkModal>
+ );
+};
+
+export const getGhostSettings = () => {
+ return (
+ {
+ connection: {
+ id: "",
+ autoconnect: true,
+ type: "vlan",
+ uuid: uuidv4(),
+ interface_name: ""
+ },
+ vlan: {
+ interface_name: "",
+ parent: ""
+ }
+ }
+ );
+};
diff --git a/pkg/networkmanager/wireguard.jsx b/pkg/networkmanager/wireguard.jsx
new file mode 100644
index 0000000..d9b56b4
--- /dev/null
+++ b/pkg/networkmanager/wireguard.jsx
@@ -0,0 +1,364 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useContext, useEffect, useState } from 'react';
+import cockpit from 'cockpit';
+import { Button } from '@patternfly/react-core/dist/esm/components/Button/index.js';
+import { ClipboardCopy } from '@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js';
+import { EmptyState, EmptyStateBody } from '@patternfly/react-core/dist/esm/components/EmptyState/index.js';
+import { FormGroup, FormFieldGroup, FormFieldGroupHeader, FormHelperText } from '@patternfly/react-core/dist/esm/components/Form/index.js';
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Grid } from '@patternfly/react-core/dist/esm/layouts/Grid/index.js';
+import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText/index';
+import { InputGroup } from '@patternfly/react-core/dist/esm/components/InputGroup/index.js';
+import { Popover } from '@patternfly/react-core/dist/esm/components/Popover/index.js';
+import { Radio } from '@patternfly/react-core/dist/esm/components/Radio/index.js';
+import { Text } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput/index.js';
+import { HelpIcon, TrashIcon } from '@patternfly/react-icons';
+
+import { Name, NetworkModal, dialogSave } from "./dialogs-common";
+import { ModelContext } from './model-context';
+import { useDialogs } from 'dialogs.jsx';
+
+import './wireguard.scss';
+
+const _ = cockpit.gettext;
+
+function addressesToString(addresses) {
+ return addresses.map(address => address[0] + "/" + address[1]).join(", ");
+}
+
+function stringToAddresses(str) {
+ return str.split(/[\s,]+/).map(strAddress => {
+ const parts = strAddress.split("/");
+ if (parts.length > 2) {
+ throw new Error(_("Addresses are not formatted correctly"));
+ }
+ const [ip, prefix] = parts;
+ const defaultPrefix = "32";
+ // Gateway usually conflicts with routing that NetworkManager configures for WireGuard interfaces
+ // So it should not be set in that case or more accurately set to 0 i.e. "0.0.0.0" in string format
+ const gateway = "0.0.0.0";
+ return [ip, prefix ?? defaultPrefix, gateway];
+ });
+}
+
+export function WireGuardDialog({ settings, connection, dev }) {
+ const Dialogs = useDialogs();
+ const idPrefix = "network-wireguard-settings";
+ const model = useContext(ModelContext);
+
+ const [iface, setIface] = useState(settings.connection.interface_name);
+ const [isPrivKeyGenerated, setIsPrivKeyGenerated] = useState(true);
+ const [generatedPrivateKey, setGeneratedPrivateKey] = useState("");
+ const [pastedPrivateKey, setPastedPrivatedKey] = useState("");
+ const [publicKey, setPublicKey] = useState("");
+ const [listenPort, setListenPort] = useState(settings.wireguard.listen_port);
+ const [addresses, setAddresses] = useState(addressesToString(settings.ipv4.addresses));
+ const [dialogError, setDialogError] = useState("");
+ const [peers, setPeers] = useState(settings.wireguard.peers.map(peer => ({ ...peer, allowedIps: peer.allowedIps?.join(",") ?? '' })));
+
+ // Additional check for `wg` after install_dialog for non-packagekit and el8 environments
+ useEffect(() => {
+ async function checkWireguardPackage() {
+ try {
+ await cockpit.script("command -v wg");
+ } catch (e) {
+ setDialogError(_("wireguard-tools package is not installed"));
+ }
+ }
+
+ checkWireguardPackage();
+ }, []);
+
+ useEffect(() => {
+ if (!connection)
+ generatePrivateKey();
+ }, [connection]);
+
+ useEffect(() => {
+ async function getPrivateKey() {
+ const objpath = connection[" priv"].path;
+ const [result] = await model.client.call(objpath, "org.freedesktop.NetworkManager.Settings.Connection", "GetSecrets", ["wireguard"]);
+ setGeneratedPrivateKey(result.wireguard["private-key"].v);
+ }
+
+ if (connection?.[" priv"].path) {
+ getPrivateKey();
+ }
+ }, [model, connection]);
+
+ useEffect(() => {
+ const privateKey = isPrivKeyGenerated ? generatedPrivateKey : pastedPrivateKey;
+ if (privateKey === "") {
+ setPublicKey("");
+ return;
+ }
+
+ async function getPublicKey() {
+ try {
+ const key = await cockpit.spawn(["wg", "pubkey"], { err: 'message' }).input(privateKey.trim());
+ setPublicKey(key.trim());
+ } catch (e) {
+ console.error("Failed to call wg pubkey:", e.message);
+ setPublicKey('');
+ }
+ }
+
+ getPublicKey();
+ }, [isPrivKeyGenerated, generatedPrivateKey, pastedPrivateKey]);
+
+ async function generatePrivateKey() {
+ try {
+ const key = await cockpit.spawn(["wg", "genkey"]);
+ setGeneratedPrivateKey(key.trim());
+ } catch (e) {
+ setDialogError(e.message);
+ }
+ }
+
+ function onSubmit() {
+ const private_key = isPrivKeyGenerated ? generatedPrivateKey : pastedPrivateKey;
+
+ // Validate Addresses before submit
+ // Also validate listenPort as PF TextInput[type=number] accepts normal text as well on firefox
+ // See - https://github.com/patternfly/patternfly-react/issues/9391
+ let addr, peersArr;
+ const listen_port = Number(listenPort);
+ try {
+ addr = stringToAddresses(addresses);
+
+ if (isNaN(listen_port)) {
+ throw new Error(_("Listen port must be a number"));
+ }
+
+ peersArr = peers.map((peer, index) => {
+ if (peer.endpoint?.trim()) {
+ const parts = peer.endpoint.split(":");
+ if (parts.length !== 2) {
+ throw cockpit.format(_("Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. 1.2.3.4:51820 or example.com:51820"), index + 1);
+ }
+ const [, port] = parts;
+ if (port == '' || isNaN(Number(port))) {
+ throw cockpit.format(_("Peer #$0 has invalid endpoint port. Port must be a number."), index + 1);
+ }
+ }
+ return ({ ...peer, allowedIps: peer.allowedIps.trim().split(',') });
+ });
+ } catch (e) {
+ setDialogError(typeof e === 'string' ? e : e.message);
+ return;
+ }
+
+ function createSettingsObj() {
+ return {
+ ...settings,
+ connection: {
+ ...settings.connection,
+ id: `con-${iface}`,
+ interface_name: iface,
+ type: 'wireguard'
+ },
+ wireguard: {
+ private_key,
+ listen_port,
+ peers: peersArr,
+ },
+ ipv4: {
+ addresses: addr,
+ method: 'manual',
+ dns: [],
+ dns_search: []
+ }
+ };
+ }
+
+ dialogSave({
+ connection,
+ dev,
+ model,
+ settings: createSettingsObj(),
+ onClose: Dialogs.close,
+ setDialogError
+ });
+ }
+
+ return (
+ <NetworkModal
+ title={!connection ? _("Add WireGuard VPN") : _("Edit WireGuard VPN")}
+ onSubmit={onSubmit}
+ dialogError={dialogError}
+ idPrefix={idPrefix}
+ submitDisabled={!iface || !addresses || !generatedPrivateKey}
+ isCreateDialog={!connection}
+ >
+ <Name idPrefix={idPrefix} iface={iface} setIface={setIface} />
+ <FormGroup label={_("Private key")} fieldId={idPrefix + '-private-key-input'} isInline hasNoPaddingTop>
+ <Radio label={_("Generated")} name="private-key" id={idPrefix + '-generated-key'} defaultChecked onChange={() => { setIsPrivKeyGenerated(true) }} />
+ <Radio label={_("Paste existing key")} name="private-key" id={idPrefix + '-paste-key'} onChange={() => { setIsPrivKeyGenerated(false) }} />
+
+ {isPrivKeyGenerated
+ ? <InputGroup className='pf-v5-u-pt-sm'>
+ <Flex className='pf-v5-u-w-100' spaceItems={{ default: 'spaceItemsSm' }}>
+ <FlexItem grow={{ default: 'grow' }}>
+ <ClipboardCopy isReadOnly id={idPrefix + '-private-key-input'} className='pf-v5-u-font-family-monospace pf-v5-u-w-100'>{generatedPrivateKey}</ClipboardCopy>
+ </FlexItem>
+ {connection && <FlexItem>
+ <Button variant='secondary' onClick={generatePrivateKey}>{_("Regenerate")}</Button>
+ </FlexItem>}
+ </Flex>
+ </InputGroup>
+ : <InputGroup className='pf-v5-u-pt-sm'>
+ <TextInput id={idPrefix + '-private-key-input'}
+ className='pf-v5-u-font-family-monospace'
+ value={pastedPrivateKey}
+ onChange={(_, val) => setPastedPrivatedKey(val)}
+ isDisabled={isPrivKeyGenerated}
+ />
+ </InputGroup>}
+ </FormGroup>
+ <FormGroup label={_("Public key")}>
+ {(isPrivKeyGenerated || publicKey)
+ ? <ClipboardCopy isReadOnly className='pf-v5-u-font-family-monospace' id={idPrefix + '-public-key'}>{publicKey}</ClipboardCopy>
+ : <Flex className='placeholder-text' alignItems={{ default: 'alignItemsCenter' }}><Text>{_("Public key will be generated when a valid private key is entered")}</Text></Flex>}
+ </FormGroup>
+ <FormGroup label={_("Listen port")} fieldId={idPrefix + '-listen-port-input'}>
+ <Flex>
+ <TextInput id={idPrefix + '-listen-port-input'} className='network-number-field' value={listenPort} onChange={(_, val) => { setListenPort(val) }} />
+ {!parseInt(listenPort) && <FormHelperText>
+ <HelperText>
+ <HelperTextItem>{_("Will be set to \"Automatic\"")}</HelperTextItem>
+ </HelperText>
+ </FormHelperText>}
+ </Flex>
+ </FormGroup>
+ <FormGroup label={_("IPv4 addresses")} fieldId={idPrefix + '-addresses-input'}>
+ <TextInput id={idPrefix + '-addresses-input'} value={addresses} onChange={(_, val) => { setAddresses(val) }} placeholder="Example, 10.0.0.1/24, 1.2.3.4/24" />
+ <FormHelperText>
+ <HelperText>
+ <HelperTextItem>{_("Multiple addresses can be specified using commas or spaces as delimiters.")}</HelperTextItem>
+ </HelperText>
+ </FormHelperText>
+ </FormGroup>
+ <FormFieldGroup
+ header={
+ <FormFieldGroupHeader
+ className='pf-v5-u-align-items-center'
+ titleText={{
+ text: (
+ <Flex className='pf-m-space-items-none'>
+ <FlexItem>
+ <Text>{_("Peers")}</Text>
+ </FlexItem>
+ <FlexItem>
+ <Popover
+ bodyContent={
+ <p>{_("Peers are other machines that connect with this one. Public keys from other machines will be shared with each other.")}</p>
+ }
+ footerContent={
+ <p>{_("Endpoint acting as a \"server\" need to be specified as host:port, otherwise it can be left empty.")}</p>
+ }
+ >
+ <Button variant='plain'><HelpIcon /></Button>
+ </Popover>
+ </FlexItem>
+ </Flex>
+ )
+ }}
+ actions={
+ <Button
+ variant='secondary'
+ onClick={() => setPeers(peers => [...peers, { publicKey: '', endpoint: '', allowedIps: '' }])}
+ >
+ {_("Add peer")}
+ </Button>
+ }
+ />
+ }
+ className='dynamic-form-group'
+ >
+ {(peers.length !== 0)
+ ? peers.map((peer, i) => (
+ <Grid key={i} hasGutter id={idPrefix + '-peer-' + i}>
+ <FormGroup className='pf-m-6-col-on-md' label={_("Public key")} fieldId={idPrefix + '-publickey-peer-' + i}>
+ <TextInput
+ value={peer.publicKey}
+ onChange={(_, val) => {
+ setPeers(peers => peers.map((peer, index) => i === index ? { ...peer, publicKey: val } : peer));
+ }}
+ id={idPrefix + '-publickey-peer-' + i}
+ />
+ </FormGroup>
+ <FormGroup className='pf-m-3-col-on-md' label={_("Endpoint")} fieldId={idPrefix + '-endpoint-peer-' + i}>
+ <TextInput
+ value={peer.endpoint}
+ onChange={(_, val) => {
+ setPeers(peers => peers.map((peer, index) => i === index ? { ...peer, endpoint: val } : peer));
+ }}
+ id={idPrefix + '-endpoint-peer-' + i}
+ />
+ </FormGroup>
+ <FormGroup className='pf-m-3-col-on-md' label={_("Allowed IPs")} fieldId={idPrefix + '-allowedips-peer-' + i}>
+ <TextInput
+ value={peer.allowedIps}
+ onChange={(_, val) => {
+ setPeers(peers => peers.map((peer, index) => i === index ? { ...peer, allowedIps: val } : peer));
+ }}
+ id={idPrefix + '-allowedips-peer-' + i}
+ />
+ </FormGroup>
+ <FormGroup className='pf-m-1-col-on-md remove-button-group'>
+ <Button
+ variant='plain'
+ id={idPrefix + '-btn-close-peer-' + i}
+ onClick={() => {
+ setPeers(peers => peers.filter((_, index) => i !== index));
+ }}
+ >
+ <TrashIcon />
+ </Button>
+ </FormGroup>
+ </Grid>
+ ))
+ : <EmptyState>
+ <EmptyStateBody>{_("No peers added.")}</EmptyStateBody>
+ </EmptyState>
+ }
+ </FormFieldGroup>
+ </NetworkModal>
+ );
+}
+
+export function getWireGuardGhostSettings({ newIfaceName }) {
+ return {
+ connection: {
+ id: `con-${newIfaceName}`,
+ interface_name: newIfaceName
+ },
+ wireguard: {
+ listen_port: 0,
+ private_key: "",
+ peers: []
+ },
+ ipv4: {
+ addresses: []
+ }
+ };
+}
diff --git a/pkg/networkmanager/wireguard.scss b/pkg/networkmanager/wireguard.scss
new file mode 100644
index 0000000..1d1a709
--- /dev/null
+++ b/pkg/networkmanager/wireguard.scss
@@ -0,0 +1,39 @@
+@import "global-variables.scss";
+@import "@patternfly/patternfly/utilities/Text/text.scss";
+@import "@patternfly/patternfly/utilities/Spacing/spacing.scss";
+@import "@patternfly/patternfly/utilities/Sizing/sizing.scss";
+@import "@patternfly/patternfly/utilities/Flex/flex.scss";
+
+.placeholder-text {
+ color: var(--pf-v5-global--Color--200);
+ block-size: 2.25rem;
+}
+
+.dynamic-form-group {
+ .pf-v5-c-empty-state {
+ padding: 0;
+ }
+
+ .pf-v5-c-form__field-group-body {
+ .pf-v5-c-form__group {
+ display: block;
+ }
+
+ .remove-button-group {
+ // Move 'Remove' button the the end of the row
+ grid-column: -1;
+ // Move 'Remove' button to the bottom of the line so as to align with the other form fields
+ display: flex;
+ align-items: flex-end;
+ }
+ }
+
+ // We use FormFieldGroup PF component for the nested look and for ability to add buttons to the header
+ // However we want to save space and not add indent to the left so we need to override it
+ .pf-v5-c-form__field-group-body {
+ // Stretch content fully
+ --pf-v5-c-form__field-group-body--GridColumn: 1 / -1;
+ // Reduce padding at the top
+ --pf-v5-c-form__field-group-body--PaddingTop: var(--pf-v5-global--spacer--xs);
+ }
+}
diff --git a/pkg/packagekit/autoupdates.jsx b/pkg/packagekit/autoupdates.jsx
new file mode 100644
index 0000000..6f271b1
--- /dev/null
+++ b/pkg/packagekit/autoupdates.jsx
@@ -0,0 +1,413 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React, { useState } from "react";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
+import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
+
+import { install_dialog } from "cockpit-components-install-dialog.jsx";
+import { useDialogs } from "dialogs.jsx";
+import { useInit } from "hooks";
+
+const _ = cockpit.gettext;
+
+function debug() {
+ if (window.debugging == "all" || window.debugging?.includes("packagekit"))
+ console.debug.apply(console, arguments);
+}
+
+/**
+ * Package manager specific implementations; PackageKit does not cover
+ * automatic updates, so we have to implement dnf-automatic and
+ * unattended-upgrades configuration ourselves
+ */
+
+class ImplBase {
+ constructor() {
+ this.supported = true; // false if system was customized in a way that we cannot parse
+ this.enabled = null; // boolean
+ this.type = null; // "all" or "security"
+ this.day = null; // systemd.time(7) day of week (e. g. "mon"), or empty for daily
+ this.time = null; // systemd.time(7) time (e. g. "06:00") or empty for "any time"
+ this.installed = null; // boolean
+ this.packageName = null; // name of the package providing automatic updates
+ }
+
+ // Init data members. Return a promise that resolves when done.
+ getConfig() {
+ throw new Error("abstract method");
+ }
+
+ // Update configuration for given non-null values, and update member variables on success;
+ // return a promise that resolves when done, or fails when configuration writing fails
+ setConfig(enabled, type, day, time) {
+ throw new Error("abstract method", enabled, type, day, time);
+ }
+}
+
+class DnfImpl extends ImplBase {
+ getConfig() {
+ return new Promise((resolve, reject) => {
+ this.packageName = "dnf-automatic";
+
+ // - dnf has two ways to enable automatic updates: Either by enabling dnf-automatic-install.timer
+ // or by setting "apply_updates = yes" in the config file and enabling dnf-automatic.timer
+ // - the config file determines whether to apply security updates only
+ // - by default this runs every day (OnUnitInactiveSec=1d), but the timer can be changed with a timer unit
+ // drop-in, so get the last line
+ cockpit.script("set -e; if rpm -q " + this.packageName + " >/dev/null; then echo installed; fi; " +
+ "if grep -q '^[ \\t]*upgrade_type[ \\t]*=[ \\t]*security' /etc/dnf/automatic.conf; then echo security; fi; " +
+ "TIMER=dnf-automatic-install.timer; " +
+ "if systemctl --quiet is-enabled dnf-automatic-install.timer 2>/dev/null; then echo enabled; " +
+ "elif systemctl --quiet is-enabled dnf-automatic.timer 2>/dev/null && grep -q '^[ \t]*apply_updates[ \t]*=[ \t]*yes' " +
+ " /etc/dnf/automatic.conf; then echo enabled; TIMER=dnf-automatic.timer; " +
+ "fi; " +
+ 'OUT=$(systemctl cat $TIMER 2>/dev/null || true); ' +
+ 'echo "$OUT" | grep "^OnUnitInactiveSec= *[^ ]" | tail -n1; ' +
+ 'echo "$OUT" | grep "^OnCalendar= *[^ ]" | tail -n1; ',
+ [], { err: "message" })
+ .then(output => {
+ this.installed = (output.indexOf("installed\n") >= 0);
+ this.enabled = (output.indexOf("enabled\n") >= 0);
+ this.type = (output.indexOf("security\n") >= 0) ? "security" : "all";
+
+ // if we have OnCalendar=, use that (we disable OnUnitInactiveSec= in our drop-in)
+ const calIdx = output.indexOf("OnCalendar=");
+ if (calIdx >= 0) {
+ this.parseCalendar(output.substr(calIdx).split('\n')[0].split("=")[1]);
+ } else {
+ if (output.indexOf("InactiveSec=1d\n") >= 0)
+ this.day = this.time = "";
+ else if (this.installed)
+ this.supported = false;
+ }
+
+ debug(`dnf getConfig: supported ${this.supported}, enabled ${this.enabled}, type ${this.type}, day ${this.day}, time ${this.time}, installed ${this.installed}; raw response '${output}'`);
+ resolve();
+ })
+ .catch(error => {
+ console.error("dnf getConfig failed:", error);
+ this.supported = false;
+ resolve();
+ });
+ });
+ }
+
+ parseCalendar(spec) {
+ // see systemd.time(7); we only support what we write, otherwise we treat it as custom config and "unsupported"
+ const daysOfWeek = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
+ const validTime = /^((|0|1)[0-9]|2[0-3]):[0-5][0-9]$/;
+
+ const words = spec.trim().toLowerCase()
+ .split(/\s+/);
+
+ // check if we have a day of week
+ if (daysOfWeek.indexOf(words[0]) >= 0) {
+ this.day = words.shift();
+ } else if (words[0] === '*-*-*') {
+ this.day = ""; // daily with "all matches" date specification
+ words.shift();
+ } else {
+ this.day = ""; // daily without date specification
+ }
+
+ // now there should only be a time left
+ if (words.length == 1 && validTime.test(words[0]))
+ this.time = words[0].replace(/^0+/, "");
+ else
+ this.supported = false;
+ }
+
+ setConfig(enabled, type, day, time) {
+ const timerConf = "/etc/systemd/system/dnf-automatic-install.timer.d/time.conf";
+ let script = "set -e; ";
+
+ if (type !== null) {
+ const value = (type == "security") ? "security" : "default";
+
+ // normally upgrade_type = should already be in the file, so replace that line;
+ // if it's not already present, append it to the file
+ script += "sed -i '/\\bupgrade_type\\b[ \\t]*=/ { h; s/^.*$/upgrade_type = " + value + "/ }; " +
+ "$ { x; /^$/ { s//upgrade_type = " + value + "/; H }; x }' /etc/dnf/automatic.conf; ";
+ }
+
+ // if we enable through Cockpit, make sure that starting the timer doesn't start the .service right away,
+ // due to the packaged default OnBootSec=1h; just set a reasonable initial time which will trigger the code below
+ if (enabled && !this.enabled && !this.time && !this.day)
+ time = "6:00";
+
+ if (time !== null || day !== null) {
+ if (day === "" && time === "") {
+ // restore defaults
+ script += "rm -f " + timerConf + "; ";
+ } else {
+ if (day == null)
+ day = this.day;
+ if (time == null)
+ time = this.time;
+ script += "mkdir -p /etc/systemd/system/dnf-automatic-install.timer.d; ";
+ script += "printf '[Timer]\\nOnBootSec=\\nOnCalendar=" + day + " " + time + "\\n' > " + timerConf + "; ";
+ script += "systemctl daemon-reload; ";
+ }
+ }
+
+ if (enabled !== null) {
+ const rebootConf = "/etc/systemd/system/dnf-automatic-install.service.d/autoreboot.conf";
+
+ script += "systemctl " + (enabled ? "enable" : "disable") + " --now dnf-automatic-install.timer; ";
+
+ if (enabled) {
+ /* dnf 4.15+ supports automatic reboots; check if the config option exists, and if so, change the
+ default to "when-needed"; but be strict about the format, to avoid changing a customized setting */
+ script += "if grep '^[[:space:]]*reboot\\b' /etc/dnf/automatic.conf; then ";
+ script += " sed -i 's/^reboot = never$/reboot = when-needed/' /etc/dnf/automatic.conf; ";
+ // and drop the previous hack on upgrades */
+ script += " rm -f " + rebootConf + "; ";
+ /* HACK for older dnf: enable automatic reboots after updating; dnf-automatic does not leave a log
+ file behind for deciding whether it actually installed anything, so resort to grepping the journal
+ for the last run (https://bugzilla.redhat.com/show_bug.cgi?id=1491190) */
+ script += "else ";
+ script += " mkdir -p /etc/systemd/system/dnf-automatic-install.service.d; ";
+ script += " printf '[Service]\\nExecStartPost=/bin/sh -ec \"" +
+ "if systemctl status --no-pager --lines=100 dnf-automatic-install.service| grep -q ===========$$; then " +
+ "shutdown -r +5 rebooting after applying package updates; fi\"\\n' > " + rebootConf + "; ";
+ script += " systemctl daemon-reload; ";
+ script += "fi";
+ } else {
+ // also make sure that the legacy unit name is disabled; this can fail if the unit does not exist
+ script += "systemctl disable --now dnf-automatic.timer 2>/dev/null || true; ";
+ script += "rm -f " + rebootConf + "; ";
+ }
+ }
+
+ debug(`setConfig(${enabled}, "${type}", "${day}", "${time}"): script "${script}"`);
+
+ return cockpit.script(script, [], { superuser: "require" })
+ .then(() => {
+ debug("dnf setConfig: configuration updated successfully");
+ if (enabled !== null)
+ this.enabled = enabled;
+ if (type !== null)
+ this.type = type;
+ if (day !== null)
+ this.day = day;
+ if (time !== null)
+ this.time = time;
+ })
+ .catch(error => console.error("dnf setConfig failed:", error.toString()));
+ }
+}
+
+// Returns a promise for instantiating "backend"; this will never fail, if
+// automatic updates are not supported, backend will be null.
+export function getBackend(packagekit_backend, forceReinit) {
+ if (!getBackend.promise || forceReinit) {
+ debug("getBackend() called first time or forceReinit passed, initializing promise");
+ getBackend.promise = new Promise((resolve, reject) => {
+ const backend = (packagekit_backend === "dnf") ? new DnfImpl() : undefined;
+ // TODO: apt backend
+ if (backend)
+ backend.getConfig().then(() => resolve(backend));
+ else
+ resolve(null);
+ });
+ }
+ return getBackend.promise;
+}
+
+const AutoUpdatesDialog = ({ backend }) => {
+ const Dialogs = useDialogs();
+ const [pending, setPending] = useState(false);
+ const [enabled, setEnabled] = useState(backend.enabled);
+ const [type, setType] = useState(backend.type);
+ const [day, setDay] = useState(backend.day);
+ const [time, setTime] = useState(backend.time && backend.time.padStart(5, "0"));
+
+ function save(event) {
+ setPending(true);
+ backend.setConfig(enabled, type, day, time)
+ .finally(Dialogs.close);
+
+ if (event)
+ event.preventDefault();
+ return false;
+ }
+
+ return (
+ <Modal position="top" variant="small" id="automatic-updates-dialog" isOpen
+ title={_("Automatic updates")}
+ onClose={Dialogs.close}
+ footer={
+ <>
+ <Button variant="primary"
+ isLoading={pending}
+ isDisabled={pending}
+ onClick={save}>
+ {_("Save changes")}
+ </Button>
+ <Button variant="link"
+ isDisabled={pending}
+ onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>
+ }>
+ <Form isHorizontal onSubmit={save}>
+ <FormGroup fieldId="type" label={_("Type")} hasNoPaddingTop>
+ <Radio isChecked={!enabled}
+ onChange={() => { setEnabled(false); setType(null) }}
+ isDisabled={pending}
+ label={_("No updates")}
+ id="no-updates"
+ name="type" />
+ <Radio isChecked={enabled && type === "security"}
+ onChange={() => { setEnabled(true); setType("security") }}
+ isDisabled={pending}
+ label={_("Security updates only")}
+ id="security-updates"
+ name="type" />
+ <Radio isChecked={enabled && type === "all"}
+ onChange={() => { setEnabled(true); setType("all") }}
+ isDisabled={pending}
+ label={_("All updates")}
+ id="all-updates"
+ name="type" />
+ </FormGroup>
+
+ {enabled &&
+ <>
+ <FormGroup fieldId="when" label={_("When")}>
+ <Flex className="auto-update-group">
+ <FormSelect id="auto-update-day"
+ isDisabled={pending}
+ value={day == "" ? "everyday" : day}
+ onChange={(_, d) => setDay(d == "everyday" ? "" : d) }>
+ <FormSelectOption value="everyday" label={_("every day")} />
+ <FormSelectOption value="mon" label={_("Mondays")} />
+ <FormSelectOption value="tue" label={_("Tuesdays")} />
+ <FormSelectOption value="wed" label={_("Wednesdays")} />
+ <FormSelectOption value="thu" label={_("Thursdays")} />
+ <FormSelectOption value="fri" label={_("Fridays")} />
+ <FormSelectOption value="sat" label={_("Saturdays")} />
+ <FormSelectOption value="sun" label={_("Sundays")} />
+ </FormSelect>
+
+ <span className="auto-conf-text">{_("at")}</span>
+
+ <TimePicker time={time} is24Hour
+ menuAppendTo={() => document.body}
+ id="auto-update-time" isDisabled={pending}
+ invalidFormatErrorMessage={_("Invalid time format")}
+ onChange={(_, time) => setTime(time)} />
+ </Flex>
+ </FormGroup>
+
+ <Alert variant="info" title={_("This host will reboot after updates are installed.")} isInline />
+ </>}
+ </Form>
+ </Modal>);
+};
+
+export const AutoUpdates = ({ privileged, packagekit_backend }) => {
+ const Dialogs = useDialogs();
+ const [backend, setBackend] = useState(null);
+ useInit(() => getBackend(packagekit_backend).then(setBackend));
+
+ if (!backend)
+ return null;
+
+ let state = null;
+ if (!backend.enabled)
+ state = _("Disabled");
+ if (!backend.installed)
+ state = _("Not set up");
+
+ const days = {
+ "": _("every day"),
+ mon: _("every Monday"),
+ tue: _("every Tuesday"),
+ wed: _("every Wednesday"),
+ thu: _("every Thursday"),
+ fri: _("every Friday"),
+ sat: _("every Saturday"),
+ sun: _("every Sunday")
+ };
+
+ let desc = null;
+
+ if (backend.enabled && backend.supported) {
+ const day = days[backend.day];
+ const time = backend.time;
+ desc = backend.type == "security"
+ ? cockpit.format(_("Security updates will be applied $0 at $1"), day, time)
+ : cockpit.format(_("Updates will be applied $0 at $1"), day, time);
+ }
+
+ if (privileged && backend.installed && !backend.supported)
+ return (
+ <div id="autoupdates-settings">
+ <Alert isInline
+ variant="info"
+ className="autoupdates-card-error"
+ title={_("Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install.timer. Please remove custom overrides to configure automatic updates.")} />
+ </div>
+ );
+
+ return (
+ <div id="autoupdates-settings">
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <Flex grow={{ default: 'grow' }} alignItems={{ default: 'alignItemsBaseline' }}>
+ <FlexItem>
+ <b>{_("Automatic updates")}</b>
+ </FlexItem>
+ <FlexItem>
+ {state}
+ </FlexItem>
+ </Flex>
+ <Flex>
+ <Button variant="secondary"
+ isDisabled={!privileged}
+ size="sm"
+ onClick={() => {
+ if (!backend.installed) {
+ install_dialog(backend.packageName)
+ .then(() => {
+ getBackend(packagekit_backend, true).then(b => {
+ setBackend(b);
+ Dialogs.show(<AutoUpdatesDialog backend={b} />);
+ });
+ }, () => null);
+ } else {
+ Dialogs.show(<AutoUpdatesDialog backend={backend} />);
+ }
+ }}>
+ {!backend.installed ? _("Enable") : _("Edit")}
+ </Button>
+ </Flex>
+ </Flex>
+ {desc}
+ </div>);
+};
diff --git a/pkg/packagekit/callTracer.py b/pkg/packagekit/callTracer.py
new file mode 100644
index 0000000..0d06ebd
--- /dev/null
+++ b/pkg/packagekit/callTracer.py
@@ -0,0 +1,19 @@
+import json
+
+from tracer.query import Query
+
+q = Query()
+applications = q.affected_applications().get()
+
+
+def filter_by_type(apps, type_str):
+ return [app.name for app in apps if app.type == type_str]
+
+
+dump_obj = {}
+# ignore type "session" for cockpit use case
+dump_obj["reboot"] = filter_by_type(applications, "static")
+dump_obj["daemons"] = filter_by_type(applications, "daemon")
+dump_obj["manual"] = filter_by_type(applications, "application")
+
+print(json.dumps(dump_obj))
diff --git a/pkg/packagekit/history.jsx b/pkg/packagekit/history.jsx
new file mode 100644
index 0000000..6a5e681
--- /dev/null
+++ b/pkg/packagekit/history.jsx
@@ -0,0 +1,120 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import PropTypes from "prop-types";
+
+import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+import { BundleIcon } from "@patternfly/react-icons";
+import { ListingTable } from "cockpit-components-table.jsx";
+import * as timeformat from "timeformat.js";
+
+import cockpit from "cockpit";
+
+const _ = cockpit.gettext;
+
+function formatPkgs(pkgs) {
+ const names = Object.keys(pkgs).filter(i => i != "_time");
+ names.sort();
+ return names.map(n => {
+ const tooltipRef = React.useRef(null);
+
+ return (
+ <React.Fragment key={n}>
+ <li ref={tooltipRef}>{n}</li>
+ <Tooltip triggerRef={tooltipRef} content={ n + " " + pkgs[n] } />
+ </React.Fragment>
+ );
+ });
+}
+
+export const PackageList = ({ packages }) => packages ? <ul className='flow-list'>{formatPkgs(packages)}</ul> : null;
+
+export class History extends React.Component {
+ /* Some PackageKit transactions come in pairs with identical package list,
+ * but different versions. This is an internal technicality, merge them
+ * together for presentation.
+ *
+ * Returns a time sorted (descending) list of objects like
+ * { time: timestamp, num_packages: 2, packages: {names...}}
+ */
+ mergeHistory() {
+ const history = [];
+ let prevTime, prevPackages;
+
+ for (let i = 0; i < this.props.packagekit.length; ++i) {
+ const packages = Object.keys(this.props.packagekit[i]).filter(i => i != "_time");
+ const time = this.props.packagekit[i]._time;
+ packages.sort();
+
+ if (prevTime && (time - prevTime) <= 600000 /* 10 mins */ &&
+ prevPackages.toString() == packages.toString())
+ history.pop();
+
+ history.push({ time, packages: this.props.packagekit[i], num_packages: packages.length });
+
+ if (history.length === 3)
+ break;
+
+ prevPackages = packages;
+ prevTime = time;
+ }
+
+ return history;
+ }
+
+ render() {
+ const history = this.mergeHistory();
+ if (history.length === 0)
+ return null;
+
+ const rows = history.map((update, index) => {
+ const pkgcount = (
+ <div className="list-view-pf-additional-info-item">
+ <BundleIcon />
+ { cockpit.format(cockpit.ngettext("$0 package", "$0 packages", update.num_packages), update.num_packages) }
+ </div>);
+
+ const expandedContent = <PackageList packages={update.packages} />;
+
+ return ({
+ props: { key: update.time },
+ columns: [
+ { title: timeformat.dateTime(update.time), props: { className: "history-time" } },
+ { title: pkgcount, props: { className: "history-pkgcount" } },
+ ],
+ initiallyExpanded: index == 0,
+ hasPadding: true,
+ expandedContent
+ });
+ });
+
+ return (
+ <ListingTable aria-label={_("Updates history")}
+ showHeader={false}
+ className="updates-history"
+ columns={[_("Time"), _("History package count")]}
+ rows={rows} />
+ );
+ }
+}
+
+History.propTypes = {
+ packagekit: PropTypes.arrayOf(PropTypes.object).isRequired,
+};
diff --git a/pkg/packagekit/index.html b/pkg/packagekit/index.html
new file mode 100644
index 0000000..f4f8a98
--- /dev/null
+++ b/pkg/packagekit/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2017 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="packagekit-page">
+
+<head>
+ <title translate="yes">Software updates</title>
+ <meta charset="utf-8" />
+
+ <link href="updates.css" rel="stylesheet" />
+
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="updates.js"></script>
+ <script src="po.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app"></div>
+</body>
+
+</html>
diff --git a/pkg/packagekit/kpatch.jsx b/pkg/packagekit/kpatch.jsx
new file mode 100644
index 0000000..109271a
--- /dev/null
+++ b/pkg/packagekit/kpatch.jsx
@@ -0,0 +1,393 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import PropTypes from "prop-types";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { Form } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Text, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+import { InfoIcon, InfoCircleIcon } from "@patternfly/react-icons";
+
+import cockpit from "cockpit";
+import { proxy as serviceProxy } from "service";
+import { check_missing_packages } from "packagekit.js";
+import { install_dialog } from "cockpit-components-install-dialog.jsx";
+import { read_os_release } from "os-release.js";
+
+const _ = cockpit.gettext;
+
+export class KpatchSettings extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {
+ loaded: false,
+ showLoading: null, // true: show spinner during initialization; false: hide
+ auto: null, // `dnf kpatch` is set to `auto`
+ enabled: null, // kpatch.service is enabled
+ missing: [], // missing packages from `kpatch`, `kpatch-dnf`
+ unavailable: [], // unavailable packages from `kpatch`, `kpatch-dnf`
+
+ // Modal states
+ error: "",
+ updating: false,
+ showModal: false,
+ applyCheckbox: false, // state of the checkbox
+ justCurrent: null, // radio state
+
+ kernelName: "", // uname -r
+ patchName: null, // kpatch-patch name
+ patchInstalled: null, // kpatch-patch installed
+ patchUnavailable: null, // kpatch-patch available
+ };
+
+ this.kpatchService = serviceProxy("kpatch");
+
+ this.checkSetup = this.checkSetup.bind(this);
+ this.handleChange = this.handleChange.bind(this);
+ this.onClose = this.onClose.bind(this);
+ this.handleInstall = this.handleInstall.bind(this);
+
+ // only show a spinner during loading on RHEL (the only place where we expect this to work)
+ read_os_release().then(os_release => this.setState({ showLoading: os_release && os_release.ID == 'rhel' }));
+ }
+
+ // Only current patches or also future ones
+ current(enabled, installed, unavailable) {
+ return enabled && (installed || unavailable);
+ }
+
+ componentDidMount() {
+ check_missing_packages(["kpatch", "kpatch-dnf"])
+ .then(d =>
+ this.checkSetup().then(() =>
+ this.setState({
+ loaded: true,
+ unavailable: d.unavailable_names || [],
+ missing: d.missing_names || [],
+ })
+ )
+ )
+ .catch(e => console.log("Could not determine kpatch availability:", JSON.stringify(e)));
+
+ this.kpatchService.addEventListener('changed', () => {
+ this.setState(state => {
+ const current = this.current(this.kpatchService.enabled, state.patchInstalled, state.patchUnavailable);
+ return ({
+ enabled: this.kpatchService.enabled,
+ justCurrent: current && !state.auto,
+ applyCheckbox: current,
+ });
+ });
+ });
+ }
+
+ checkSetup() {
+ // TODO - replace both with `dnf kpatch status` once https://github.com/dynup/kpatch-dnf/pull/8 lands
+ const kpatch_promise = cockpit.file("/etc/dnf/plugins/kpatch.conf").read()
+ .then(data => {
+ if (data) {
+ const auto = /autoupdate\s*=\s*True/i.test(data);
+ this.setState((state, _) => {
+ const current = this.current(state.enabled, state.patchInstalled, state.patchUnavailable);
+ return ({
+ auto: !!auto,
+ justCurrent: current && !auto,
+ applyCheckbox: current,
+ });
+ });
+ }
+ })
+ .catch(() => true); // Ignore errors, most likely just does not exist
+
+ const uname_promise = cockpit.spawn(["uname", "-r"])
+ .then(data => {
+ const fields = data.split("-");
+ const kpp_kernel_version = fields[0].replaceAll(".", "_");
+ let release = fields[1].split(".");
+ release = release.slice(0, release.length - 2); // remove el8.x86_64
+ const kpp_kernel_release = release.join("_");
+ const patch_name = ["kpatch-patch", kpp_kernel_version, kpp_kernel_release].join("-");
+ return check_missing_packages([patch_name])
+ .then(d =>
+ this.setState((state, _) => {
+ const installed = (d.unavailable_names || []).length === 0 && (d.missing_names || []).length === 0;
+ const unavailable = (d.unavailable_names || []).length > 0;
+ const current = this.current(state.enabled, installed, unavailable);
+ return ({
+ kernelName: data,
+ patchName: patch_name,
+ patchInstalled: installed,
+ patchUnavailable: unavailable,
+ justCurrent: current && !state.auto,
+ applyCheckbox: current,
+ });
+ })
+ );
+ })
+ .catch(err => console.error("Could not determine kpatch packages:", err)); // not-covered: OS error
+
+ return Promise.allSettled([kpatch_promise, uname_promise]);
+ }
+
+ handleInstall() {
+ this.setState({ updating: true });
+ install_dialog(this.state.missing)
+ .then(() => this.setState({ missing: [], updating: false }))
+ .catch(() => this.setState({ updating: false }));
+ }
+
+ onClose() {
+ this.setState((state, _) => {
+ const current = this.current(state.enabled, state.patchInstalled, state.patchUnavailable);
+ return ({
+ justCurrent: current && !state.auto,
+ applyCheckbox: current,
+ showModal: false,
+ error: "",
+ });
+ });
+ }
+
+ handleChange() {
+ this.setState({ updating: true });
+
+ if (this.state.applyCheckbox) {
+ let install;
+ if (this.state.justCurrent) {
+ install = new Promise((resolve, reject) => {
+ cockpit.spawn(["dnf", "-y", "kpatch", "manual"], { superuser: "require", err: "message" })
+ .then(() => {
+ if (!this.state.patchUnavailable && !this.state.patchInstalled)
+ // TODO - replace with `dnf kpatch install` once https://github.com/dynup/kpatch-dnf/pull/8 lands
+ cockpit.spawn(["dnf", "-y", "install", this.state.patchName], { superuser: "require", err: "message" }).then(resolve)
+ .catch(reject);
+ else
+ resolve();
+ })
+ .catch(reject);
+ });
+ } else {
+ install = cockpit.spawn(["dnf", "-y", "kpatch", "auto"], { superuser: "require", err: "message" });
+ }
+ install
+ .then(() =>
+ this.kpatchService.enable().then(() =>
+ this.kpatchService.start().then(() =>
+ this.setState({ showModal: false, error: "" })
+ )
+ )
+ )
+ .catch(e => this.setState({ error: e.toString() }))
+ .finally(() => this.checkSetup().then(() => this.setState({ updating: false })));
+ } else {
+ cockpit.spawn(["dnf", "-y", "kpatch", "manual"], { superuser: "require", err: "message" })
+ .then(() =>
+ this.kpatchService.disable().then(() =>
+ this.kpatchService.stop().then(() =>
+ this.setState({ showModal: false, error: "" })
+ )
+ )
+ )
+ .catch(e => this.setState({ error: e.toString() }))
+ .finally(() => this.checkSetup().then(() => this.setState({ updating: false })));
+ }
+ }
+
+ render() {
+ // don't show anything during initial detection
+ if ((this.state.loaded === false || this.state.patchName === null) && !this.state.showLoading)
+ return null;
+
+ // Not supported on this system
+ if (this.state.unavailable.length > 0 && !this.state.showLoading)
+ return null;
+ let state;
+ let actionText = _("Edit");
+ let action = () => this.setState({ showModal: true });
+
+ if (this.state.loaded === false || this.state.patchName === null) {
+ // Not yet recognized
+ state = <Spinner size="md" />;
+ } else if (this.state.unavailable.length > 0) {
+ state = <Popover headerContent={ _("Unavailable packages") } bodyContent={ this.state.unavailable.join(", ") }>
+ <span>
+ { _("Not available") }
+ &nbsp;
+ <InfoCircleIcon className="ct-info-circle" />
+ </span>
+ </Popover>;
+ } else if (this.state.missing.length > 0) {
+ state = _("Not installed");
+ actionText = _("Install");
+ action = this.handleInstall;
+ } else if (!this.state.enabled) {
+ state = _("Disabled");
+ actionText = _("Enable");
+ } else {
+ state = _("Enabled");
+ }
+
+ const kernel_name = this.state.kernelName ? " (" + this.state.kernelName + ")" : "";
+ const error = this.state.error ? <Alert variant='danger' isInline title={this.state.error} /> : null;
+
+ const body = <Form><Checkbox id="apply-kpatch"
+ isChecked={this.state.applyCheckbox}
+ label={_("Apply kernel live patches")}
+ onChange={(_event, checked) => this.setState({ applyCheckbox: checked })}
+ body={<>
+ <Radio id="current-future"
+ label={_("for current and future kernels")}
+ onChange={() => this.setState({ justCurrent: false })}
+ isDisabled={!this.state.applyCheckbox}
+ isChecked={!this.state.justCurrent} />
+ <Radio id="current-only"
+ label={_("for current kernel only") + kernel_name}
+ onChange={() => this.setState({ justCurrent: true })}
+ isDisabled={!this.state.applyCheckbox}
+ isChecked={this.state.justCurrent} />
+ </>}
+ /></Form>;
+
+ return (<>
+ <div id="kpatch-settings">
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <Flex grow={{ default: 'grow' }} alignItems={{ default: 'alignItemsBaseline' }}>
+ <FlexItem>
+ <b>{_("Kernel live patching")}</b>
+ </FlexItem>
+ <FlexItem>
+ {state}
+ </FlexItem>
+ </Flex>
+ <Flex>
+ <Button variant="secondary"
+ size="sm"
+ isDisabled={!this.props.privileged || this.state.updating || !this.state.loaded || this.state.unavailable.length > 0}
+ onClick={action}>
+ {actionText}
+ </Button>
+ </Flex>
+ </Flex>
+ </div>
+ <Modal position="top" variant="small" id="kpatch-setup" isOpen={this.state.showModal}
+ title={_("Kernel live patch settings")}
+ onClose={ this.onClose }
+ footer={
+ <>
+ <Button variant="primary"
+ isLoading={ this.state.updating }
+ isDisabled={ this.state.updating }
+ onClick={ this.handleChange }>
+ {_("Save")}
+ </Button>
+ <Button variant="link"
+ isDisabled={ this.state.updating }
+ onClick={ this.onClose }>
+ {_("Cancel")}
+ </Button>
+ </>
+ }>
+ <>
+ {error}
+ {body}
+ </>
+ </Modal>
+ </>);
+ }
+}
+
+KpatchSettings.propTypes = {
+ privileged: PropTypes.bool,
+};
+
+export class KpatchStatus extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {
+ loaded: [],
+ installed: [],
+ changelog: null, // FIXME - load changelog
+ };
+ }
+
+ componentDidMount() {
+ cockpit.spawn(["kpatch", "list"], { superuser: "try", err: "ignore", environ: ["LC_MESSAGES=C"] })
+ .then(m => {
+ const parts = m.trim().split("\n\n");
+ if (parts.length !== 2 ||
+ !parts[0].startsWith("Loaded patch modules:") ||
+ !parts[1].startsWith("Installed patch modules:")) {
+ console.warn("Unexpected output from `kpatch list`", m);
+ return;
+ }
+
+ const loaded = parts[0].split("\n")
+ .slice(1)
+ .map(i => i.split(" ")[0]);
+ const installed = parts[1].split("\n")
+ .slice(1)
+ .map(i => i.split(" ")[0]);
+ this.setState({ loaded, installed });
+ })
+ .catch(() => true); // Ignore errors
+ }
+
+ render() {
+ let text = [];
+ text = this.state.loaded.map(i =>
+ <Text key={i} component={TextVariants.p}>
+ { cockpit.format(_("Kernel live patch $0 is active"), i) }
+ </Text>
+ );
+
+ if (text.length === 0)
+ text = this.state.installed.map(i =>
+ <Text key={i} component={TextVariants.p}>
+ { cockpit.format(_("Kernel live patch $0 is installed"), i) }
+ </Text>
+ );
+
+ if (text.length > 0)
+ return (
+ <Split hasGutter>
+ <SplitItem>
+ <InfoIcon />
+ </SplitItem>
+ <SplitItem isFilled>
+ <Stack>
+ {text}
+ </Stack>
+ </SplitItem>
+ </Split>
+ );
+
+ return null;
+ }
+}
diff --git a/pkg/packagekit/manifest.json b/pkg/packagekit/manifest.json
new file mode 100644
index 0000000..8decc72
--- /dev/null
+++ b/pkg/packagekit/manifest.json
@@ -0,0 +1,29 @@
+{
+ "name": "updates",
+ "priority": 0,
+ "conditions": [
+ {"path-exists": "/lib/systemd/system/packagekit.service"},
+ {"path-not-exists": "/sysroot/ostree"}
+ ],
+
+ "tools": {
+ "index": {
+ "label": "Software updates",
+ "docs": [
+ {
+ "label": "Managing software updates",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-software-updates-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["package", "packagekit", "dnf", "yum", "apt-get", "security"]
+ }
+ ]
+ }
+ },
+
+ "preload": [ "index" ],
+
+ "content-security-policy": "default-src 'self'; img-src 'self' data:; font-src 'self' data:;"
+}
diff --git a/pkg/packagekit/mock-updates.js b/pkg/packagekit/mock-updates.js
new file mode 100644
index 0000000..1937b21
--- /dev/null
+++ b/pkg/packagekit/mock-updates.js
@@ -0,0 +1,111 @@
+/*
+ * Mock "available updates" entries for interactively testing layout changes with large updates
+ * To use it, import it in updates.jsx:
+ *
+ * import { injectMockUpdates } from "./mock-updates.js";
+ *
+ * and call it in loadUpdates()'s then Handler:
+ *
+ * .then(() => {
+ * injectMockUpdates(updates);
+ * let pkg_ids = Object.keys(updates);
+ */
+
+export function injectMockUpdates(updates) {
+ // some security updates
+ updates["security-crit;2.3-4"] = {
+ name: "security-crit",
+ version: "2.3-4",
+ bug_urls: [],
+ cve_urls: ["https://cve.example.com?name=CVE-2014-123456", "https://cve.example.com?name=CVE-2017-9999"],
+ vendor_urls: ["https://access.redhat.com/security/updates/classification/#critical", "critical",
+ "https://access.redhat.example.com/errata/RHSA-2000:0001", "https://access.redhat.example.com/errata/RHSA-2000:0002"],
+ severity: 8,
+ description: "This will wreck your data center!",
+ };
+ updates["security-low;1-2+sec1"] = {
+ name: "security-low",
+ version: "1-2+sec1",
+ bug_urls: [],
+ cve_urls: ["https://cve.example.com?name=CVE-2014-54321"],
+ vendor_urls: ["https://access.redhat.com/security/updates/classification/#bogus", "bogus",
+ "https://access.redhat.com/security/updates/classification/#low", "low"],
+ severity: 8,
+ description: "Mostly Harmless",
+ };
+ updates["security-imp;5-2"] = {
+ name: "security-imp",
+ version: "5-2",
+ bug_urls: [],
+ vendor_urls: ["https://access.redhat.com/security/updates/classification/#low", "low",
+ "https://access.redhat.com/security/updates/classification/#important", "important"],
+ severity: 8,
+ description: "This update will make you sleep more peacefully.",
+ };
+ updates["security-mod;12.3-4"] = {
+ name: "security-mod",
+ version: "12.3-4",
+ bug_urls: [],
+ vendor_urls: ["https://access.redhat.com/security/updates/classification/#moderate", "moderate"],
+ severity: 8,
+ description: "This update will make you sleep more peacefully.",
+ };
+ // no vendor URLs, default severity
+ updates["security-default;42"] = {
+ name: "security-default",
+ version: "42",
+ cve_urls: ["https://cve.example.com?name=CVE-2014-54321"],
+ vendor_urls: [],
+ severity: 8,
+ description: "Yet another weakness fixed.",
+ };
+
+ // source with many binaries
+ for (let i = 1; i < 50; ++i) {
+ const name = `manypkgs${i}`;
+ updates[name + ";1-1"] = {
+ name,
+ version: "1-1",
+ bug_urls: [],
+ cve_urls: [],
+ severity: 4,
+ description: "Make [everything](http://everything.example.com) *better*\n\n * more packages\n * more `bugs`\n * more fun!",
+ markdown: true,
+ };
+ }
+
+ // long changelog
+ updates["verbose;1-1"] = {
+ name: "verbose",
+ version: "1-1",
+ bug_urls: [],
+ cve_urls: [],
+ severity: 6,
+ description: ("Some longish explanation of some boring technical change. " +
+ "This is total technobabble gibberish for layman users.\n\n").repeat(30)
+ };
+
+ updates["verbose-md;1-1"] = {
+ name: "verbose-md",
+ version: "1-1",
+ bug_urls: [],
+ cve_urls: [],
+ severity: 6,
+ description: ("Some longish explanation of some *boring* technical change. " +
+ "This is total technobabble gibberish for layman users.\n\n").repeat(30),
+ markdown: true,
+ };
+
+ // many bug fixes
+ const bugs = [];
+ for (let i = 10000; i < 10025; ++i)
+ bugs.push("http://bugzilla.example.com/" + i);
+ updates["buggy;1-1"] = {
+ name: "buggy",
+ version: "1-1",
+ bug_urls: bugs,
+ cve_urls: [],
+ severity: 6,
+ description: "This is FUBAR",
+ };
+}
diff --git a/pkg/packagekit/pf-security.woff b/pkg/packagekit/pf-security.woff
new file mode 100644
index 0000000..f17f7fc
--- /dev/null
+++ b/pkg/packagekit/pf-security.woff
Binary files differ
diff --git a/pkg/packagekit/updates.jsx b/pkg/packagekit/updates.jsx
new file mode 100644
index 0000000..365e254
--- /dev/null
+++ b/pkg/packagekit/updates.jsx
@@ -0,0 +1,1650 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import 'polyfills'; // once per application
+import 'cockpit-dark-theme'; // once per page
+
+import cockpit from "cockpit";
+import React from "react";
+import { createRoot } from 'react-dom/client';
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { CodeBlock, CodeBlockCode } from "@patternfly/react-core/dist/esm/components/CodeBlock/index.js";
+import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js";
+import { LabelGroup } from "@patternfly/react-core/dist/esm/components/Label/index.js";
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Progress, ProgressSize } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import { Text, TextContent, TextList, TextListItem, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+
+import {
+ BugIcon,
+ CheckIcon,
+ EnhancementIcon,
+ ExclamationCircleIcon,
+ ExclamationTriangleIcon,
+ RebootingIcon,
+ RedoIcon,
+ ProcessAutomationIcon,
+ SecurityIcon,
+} from "@patternfly/react-icons";
+import { cellWidth, TableText } from "@patternfly/react-table";
+import { Remarkable } from "remarkable";
+
+import { AutoUpdates, getBackend } from "./autoupdates.jsx";
+import { KpatchSettings, KpatchStatus } from "./kpatch.jsx";
+import { History, PackageList } from "./history.jsx";
+import { page_status } from "notifications";
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { ListingTable } from 'cockpit-components-table.jsx';
+import { ModalError } from 'cockpit-components-inline-notification.jsx';
+import { ShutdownModal } from 'cockpit-components-shutdown.jsx';
+import { WithDialogs } from "dialogs.jsx";
+
+import { superuser } from 'superuser';
+import * as PK from "packagekit.js";
+import * as timeformat from "timeformat.js";
+
+import * as python from "python.js";
+import callTracerScript from './callTracer.py';
+
+import "./updates.scss";
+
+const _ = cockpit.gettext;
+
+// "available" heading is built dynamically
+const STATE_HEADINGS = {};
+const PK_STATUS_STRINGS = {};
+const PK_STATUS_LOG_STRINGS = {};
+const packageSummaries = {};
+
+const UPDATES = {
+ ALL: 0,
+ SECURITY: 1,
+ KPATCHES: 2,
+};
+
+function init() {
+ STATE_HEADINGS.loading = _("Loading available updates, please wait...");
+ STATE_HEADINGS.locked = _("Some other program is currently using the package manager, please wait...");
+ STATE_HEADINGS.refreshing = _("Refreshing package information");
+ STATE_HEADINGS.uptodate = _("System is up to date");
+ STATE_HEADINGS.applying = _("Applying updates");
+ STATE_HEADINGS.updateError = _("Applying updates failed");
+ STATE_HEADINGS.loadError = _("Loading available updates failed");
+
+ PK_STATUS_STRINGS[PK.Enum.STATUS_DOWNLOAD] = _("Downloading");
+ PK_STATUS_STRINGS[PK.Enum.STATUS_INSTALL] = _("Installing");
+ PK_STATUS_STRINGS[PK.Enum.STATUS_UPDATE] = _("Updating");
+ PK_STATUS_STRINGS[PK.Enum.STATUS_CLEANUP] = _("Setting up");
+ PK_STATUS_STRINGS[PK.Enum.STATUS_SIGCHECK] = _("Verifying");
+
+ PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_DOWNLOAD] = _("Downloaded");
+ PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_INSTALL] = _("Installed");
+ PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_UPDATE] = _("Updated");
+ PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_CLEANUP] = _("Set up");
+ PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_SIGCHECK] = _("Verified");
+}
+
+// parse CVEs from an arbitrary text (changelog) and return URL array
+function parseCVEs(text) {
+ if (!text)
+ return [];
+
+ const cves = text.match(/CVE-\d{4}-\d+/g);
+ if (!cves)
+ return [];
+ return cves.map(n => "https://www.cve.org/CVERecord?id=" + n);
+}
+
+function deduplicate(list) {
+ const d = { };
+ list.forEach(i => { if (i) d[i] = true; });
+ const result = Object.keys(d);
+ result.sort();
+ return result;
+}
+
+// Insert comma strings in between elements of the list. Unlike list.join(",")
+// this does not stringify the elements, which we need to keep as JSX objects.
+function insertCommas(list) {
+ if (list.length <= 1)
+ return list;
+ return list.reduce((prev, cur) => [prev, ", ", cur]);
+}
+
+// Fedora changelogs are a wild mix of enumerations or not, headings, etc.
+// Remove that formatting to avoid an untidy updates overview list
+function cleanupChangelogLine(text) {
+ if (!text)
+ return text;
+
+ // enumerations
+ text = text.replace(/^[-* ]*/, "");
+
+ // headings
+ text = text.replace(/^=+\s+/, "").replace(/=+\s*$/, "");
+
+ return text.trim();
+}
+
+// Replace cockpit-wsinstance-https@[long_id] with a shorter string
+function shortenCockpitWsInstance(list) {
+ list = [...list];
+
+ list.forEach((item, idx) => {
+ if (item.startsWith("cockpit-wsinstance-https"))
+ list[idx] = "cockpit-wsinstance-https@.";
+ });
+
+ return list;
+}
+
+function count_security_updates(updates) {
+ let num_security = 0;
+ for (const u in updates)
+ if (updates[u].severity === PK.Enum.INFO_SECURITY)
+ ++num_security;
+ return num_security;
+}
+
+function isKpatchPackage(name) {
+ return name.startsWith("kpatch-patch");
+}
+
+function count_kpatch_updates(updates) {
+ let num_kpatches = 0;
+ for (const u in updates)
+ if (isKpatchPackage(updates[u].name))
+ ++num_kpatches;
+ return num_kpatches;
+}
+
+function find_highest_severity(updates) {
+ let max = PK.Enum.INFO_LOW;
+ for (const u in updates)
+ if (updates[u].severity > max)
+ max = updates[u].severity;
+ return max;
+}
+
+/**
+ * Get appropriate icon for an update severity
+ *
+ * info: An Enum.INFO_* level
+ * secSeverity: If given, further classification of the severity of Enum.INFO_SECURITY from the vendor_urls;
+ * e. g. "critical", see https://access.redhat.com/security/updates/classification
+ * Returns: Icon JSX object
+ *
+ */
+function getSeverityIcon(info, secSeverity) {
+ let classes = "severity-icon";
+ if (secSeverity)
+ classes += " severity-" + secSeverity;
+ if (info == PK.Enum.INFO_SECURITY)
+ return <SecurityIcon aria-label={ secSeverity || _("security") } className={classes} />;
+ else if (info >= PK.Enum.INFO_NORMAL)
+ return <BugIcon className={classes} aria-label={ _("bug fix") } />;
+ else
+ return <EnhancementIcon className={classes} aria-label={ _("enhancement") } />;
+}
+
+function getPageStatusSeverityIcon(severity) {
+ if (severity == PK.Enum.INFO_SECURITY)
+ return "security";
+ else if (severity >= PK.Enum.INFO_NORMAL)
+ return "bug";
+ else
+ return "enhancement";
+}
+
+function getSeverityURL(urls) {
+ if (!urls)
+ return null;
+
+ // in ascending severity
+ const knownLevels = ["low", "moderate", "important", "critical"];
+ let highestIndex = -1;
+ let highestURL = null;
+
+ // search URLs for highest valid severity; by all means we expect an update to have at most one, but for paranoia..
+ urls.forEach(value => {
+ if (value.startsWith("https://access.redhat.com/security/updates/classification/#")) {
+ const i = knownLevels.indexOf(value.slice(value.indexOf("#") + 1));
+ if (i > highestIndex) {
+ highestIndex = i;
+ highestURL = value;
+ }
+ }
+ });
+ return highestURL;
+}
+
+// Overrides the link_open function to apply our required HTML attributes
+function customRemarkable() {
+ const remarkable = new Remarkable();
+
+ const orig_link_open = remarkable.renderer.rules.link_open;
+ remarkable.renderer.rules.link_open = function() {
+ let result = orig_link_open.apply(null, arguments);
+
+ const parser = new DOMParser();
+ const htmlDocument = parser.parseFromString(result, "text/html");
+ const links = htmlDocument.getElementsByTagName("a");
+ if (links.length === 1) {
+ const href = links[0].getAttribute("href");
+ result = `<a rel="noopener noreferrer" target="_blank" href="${href}">`;
+ }
+ return result;
+ };
+ return remarkable;
+}
+
+function updateItem(remarkable, info, pkgNames, key) {
+ let bugs = null;
+ if (info.bug_urls && info.bug_urls.length) {
+ // we assume a bug URL ends with a number; if not, show the complete URL
+ bugs = insertCommas(info.bug_urls.map(url => (
+ <a key={url} rel="noopener noreferrer" target="_blank" href={url}>
+ {url.match(/[0-9]+$/) || url}
+ </a>)
+ ));
+ }
+
+ let cves = null;
+ if (info.cve_urls && info.cve_urls.length) {
+ cves = insertCommas(info.cve_urls.map(url => (
+ <a key={url} href={url} rel="noopener noreferrer" target="_blank">
+ {url.match(/[^/=]+$/)}
+ </a>)
+ ));
+ }
+
+ let errata = null;
+ if (info.vendor_urls) {
+ errata = insertCommas(info.vendor_urls.filter(url => url.indexOf("/errata/") > 0).map(url => (
+ <a key={url} href={url} rel="noopener noreferrer" target="_blank">
+ {url.match(/[^/=]+$/)}
+ </a>)
+ ));
+ if (!errata.length)
+ errata = null; // simpler testing below
+ }
+
+ let secSeverityURL = getSeverityURL(info.vendor_urls);
+ const secSeverity = secSeverityURL ? secSeverityURL.slice(secSeverityURL.indexOf("#") + 1) : null;
+ const icon = getSeverityIcon(info.severity, secSeverity);
+ let type;
+ if (info.severity === PK.Enum.INFO_SECURITY) {
+ if (secSeverityURL)
+ secSeverityURL = <a rel="noopener noreferrer" target="_blank" href={secSeverityURL}>{secSeverity}</a>;
+ type = (
+ <>
+ <Tooltip id="tip-severity" content={ secSeverity || _("security") }>
+ <span>
+ {icon}
+ { (info.cve_urls && info.cve_urls.length > 0) ? info.cve_urls.length : "" }
+ </span>
+ </Tooltip>
+ </>);
+ } else {
+ const tip = (info.severity >= PK.Enum.INFO_NORMAL) ? _("bug fix") : _("enhancement");
+ type = (
+ <Tooltip id="tip-severity" content={tip}>
+ <span>
+ {icon}
+ { bugs ? info.bug_urls.length : "" }
+ </span>
+ </Tooltip>
+ );
+ }
+
+ const pkgList = pkgNames.map((n, index) => (
+ <Tooltip key={n.name + n.arch} id="tip-summary" content={packageSummaries[n.name] + " (" + n.arch + ")"}>
+ <span>{n.name + (index !== (pkgNames.length - 1) ? ", " : "")}</span>
+ </Tooltip>)
+ );
+ const pkgs = pkgList;
+ const pkgsTruncated = pkgList.slice(0, 4);
+
+ if (pkgList.length > 4)
+ pkgsTruncated.push(<span key="more">…</span>);
+
+ if (pkgNames.some(pkg => isKpatchPackage(pkg.name)))
+ pkgsTruncated.push(
+ <LabelGroup key={`${key}-kpatches-labelgroup`} className="kpatches-labelgroup">
+ {" "}<Badge color="blue" variant="filled">{_("patches")}</Badge>
+ </LabelGroup>
+ );
+
+ let descriptionFirstLine = (info.description || "").trim();
+ if (descriptionFirstLine.indexOf("\n") >= 0)
+ descriptionFirstLine = descriptionFirstLine.slice(0, descriptionFirstLine.indexOf("\n"));
+ descriptionFirstLine = cleanupChangelogLine(descriptionFirstLine);
+ let description;
+ if (info.markdown) {
+ descriptionFirstLine = <span dangerouslySetInnerHTML={{ __html: remarkable.render(descriptionFirstLine) }} />;
+ description = <div dangerouslySetInnerHTML={{ __html: remarkable.render(info.description) }} />;
+ } else {
+ description = <div className="changelog">{info.description}</div>;
+ }
+
+ const expandedContent = (
+ <Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
+ <DescriptionList>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Packages")}</DescriptionListTerm>
+ <DescriptionListDescription>{pkgs}</DescriptionListDescription>
+ </DescriptionListGroup>
+ { cves
+ ? <DescriptionListGroup>
+ <DescriptionListTerm>{_("CVE")}</DescriptionListTerm>
+ <DescriptionListDescription>{cves}</DescriptionListDescription>
+ </DescriptionListGroup>
+ : null }
+ { secSeverityURL
+ ? <DescriptionListGroup>
+ <DescriptionListTerm>{_("Severity")}</DescriptionListTerm>
+ <DescriptionListDescription className="severity">{secSeverityURL}</DescriptionListDescription>
+ </DescriptionListGroup>
+ : null }
+ { errata
+ ? <DescriptionListGroup>
+ <DescriptionListTerm>{_("Errata")}</DescriptionListTerm>
+ <DescriptionListDescription>{errata}</DescriptionListDescription>
+ </DescriptionListGroup>
+ : null }
+ { bugs
+ ? <DescriptionListGroup>
+ <DescriptionListTerm>{_("Bugs")}</DescriptionListTerm>
+ <DescriptionListDescription>{bugs}</DescriptionListDescription>
+ </DescriptionListGroup>
+ : null }
+ </DescriptionList>
+ <TextContent>{description}</TextContent>
+ </Flex>
+ );
+
+ return {
+ columns: [
+ { title: pkgsTruncated },
+ { title: <TableText wrapModifier="truncate">{info.version}</TableText>, props: { className: "version" } },
+ { title: <TableText wrapModifier="nowrap">{type}</TableText>, props: { className: "type" } },
+ { title: descriptionFirstLine, props: { className: "changelog" } },
+ ],
+ props: {
+ key,
+ className: info.severity === PK.Enum.INFO_SECURITY ? ["error"] : [],
+ },
+ hasPadding: true,
+ expandedContent,
+ };
+}
+
+const UpdatesList = ({ updates }) => {
+ const remarkable = customRemarkable();
+ const update_ids = [];
+
+ // PackageKit doesn"t expose source package names, so group packages with the same version and changelog
+ // create a reverse version+changes → [id] map on iteration
+ const sameUpdate = {};
+ const packageNames = {};
+ Object.keys(updates).forEach(id => {
+ const u = updates[id];
+ // did we already see the same version and description? then merge
+ const hash = u.version + u.description;
+ const seenId = sameUpdate[hash];
+ if (seenId) {
+ packageNames[seenId].push({ name: u.name, arch: u.arch });
+ } else {
+ // this is a new update
+ sameUpdate[hash] = id;
+ packageNames[id] = [{ name: u.name, arch: u.arch }];
+ update_ids.push(id);
+ }
+ });
+
+ // sort security first
+ update_ids.sort((a, b) => {
+ if (updates[a].severity === PK.Enum.INFO_SECURITY && updates[b].severity !== PK.Enum.INFO_SECURITY)
+ return -1;
+ if (updates[a].severity !== PK.Enum.INFO_SECURITY && updates[b].severity === PK.Enum.INFO_SECURITY)
+ return 1;
+ return a.localeCompare(b);
+ });
+
+ return (
+ <ListingTable aria-label={_("Available updates")}
+ gridBreakPoint='grid-lg'
+ columns={[
+ { title: _("Name"), transforms: [cellWidth(40)] },
+ { title: _("Version"), transforms: [cellWidth(15)] },
+ { title: _("Severity"), transforms: [cellWidth(15)] },
+ { title: _("Details"), transforms: [cellWidth(30)] },
+ ]}
+ rows={update_ids.map(id => updateItem(remarkable, updates[id], packageNames[id].sort((a, b) => a.name > b.name), id))} />
+ );
+};
+
+class RestartServices extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ dialogError: undefined,
+ restartInProgress: false,
+ };
+
+ this.dialogErrorSet = this.dialogErrorSet.bind(this);
+ this.dialogErrorDismiss = this.dialogErrorDismiss.bind(this);
+ this.restart = this.restart.bind(this);
+ }
+
+ dialogErrorSet(text, detail) {
+ this.setState({ dialogError: text, dialogErrorDetail: detail });
+ }
+
+ dialogErrorDismiss() {
+ this.setState({ dialogError: undefined });
+ }
+
+ restart() {
+ // make sure cockpit package is the last to restart
+ const daemons = this.props.tracerPackages.daemons.sort((a, b) => {
+ if (a.includes("cockpit") && b.includes("cockpit"))
+ return 0;
+ if (a.includes("cockpit"))
+ return 1;
+ return a.localeCompare(b);
+ });
+ const restarts = daemons.map(service => cockpit.spawn(["systemctl", "restart", service + ".service"], { superuser: "required", err: "message" }));
+ this.setState({ restartInProgress: true });
+ Promise.all(restarts)
+ .then(() => {
+ this.props.onValueChanged({ tracerPackages: { reboot: this.props.tracerPackages.reboot, daemons: [], manual: this.props.tracerPackages.manual } });
+ if (this.props.state === "updateSuccess")
+ this.props.loadUpdates();
+ this.setState({ restartInProgress: false });
+ this.props.close();
+ })
+ .catch(ex => {
+ this.dialogErrorSet(_("Failed to restart service"), ex.message);
+ // call Tracer again to see what services remain
+ this.props.callTracer(null);
+ });
+ }
+
+ render() {
+ let body;
+ if (this.props.tracerRunning) {
+ body = (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <Spinner size="sm" />
+ <p>{_("Reloading the state of remaining services")}</p>
+ </Flex>
+ );
+ } else if (this.props.tracerPackages.daemons.length > 0) {
+ body = (<>
+ {cockpit.ngettext("The following service will be restarted:", "The following services will be restarted:", this.props.tracerPackages.daemons.length)}
+ <TwoColumnContent list={this.props.tracerPackages.daemons} flexClassName="restart-services-modal-body" />
+ </>);
+ }
+
+ return (
+ <Modal id="restart-services-modal" isOpen
+ position="top"
+ variant="medium"
+ onClose={this.props.close}
+ title={_("Restart services")}
+ footer={
+ <>
+ {this.props.tracerPackages.daemons.includes("cockpit") &&
+ <Alert variant="warning"
+ title={_("Web Console will restart")}
+ isInline>
+ <p>
+ {_("When the Web Console is restarted, you will no longer see progress information. However, the update process will continue in the background. Reconnect to continue watching the update process.")}
+ </p>
+ </Alert>}
+ <Button variant='primary'
+ isDisabled={ this.state.restartInProgress }
+ onClick={ this.restart }>
+ {_("Restart services")}
+ </Button>
+ <Button variant='link' className='btn-cancel' onClick={ this.props.close }>
+ {_("Cancel")}
+ </Button>
+ </>
+ }>
+ <Stack hasGutter>
+ {this.state.dialogError && <ModalError dialogError={this.state.dialogError} dialogErrorDetail={this.state.dialogErrorDetail} />}
+ <StackItem>{body}</StackItem>
+ </Stack>
+ </Modal>
+ );
+ }
+}
+
+const formatPackageId = packageId => {
+ const pfields = packageId.split(";");
+ return pfields[0] + " " + pfields[1] + " (" + pfields[2] + ")";
+};
+
+// actions is a chronological list of { status, packageId } events that happen during applying updates
+// status: see PK_STATUS_* at https://github.com/PackageKit/PackageKit/blob/main/lib/packagekit-glib2/pk-enum.h
+const ApplyUpdates = ({ transactionProps, actions, onCancel, rebootAfter, setRebootAfter }) => {
+ const remain = transactionProps.RemainingTime
+ ? timeformat.distanceToNow(new Date().valueOf() + transactionProps.RemainingTime * 1000)
+ : null;
+
+ let percentage = transactionProps.Percentage || 0;
+ // PackageKit sets this to 101 initially
+ if (percentage > 100)
+ percentage = 0;
+
+ // scroll update log to the bottom, if it already is (almost) at the bottom
+ const log = document.getElementById("update-log");
+ if (log) {
+ if (log.scrollHeight - log.clientHeight <= log.scrollTop + 2)
+ log.scrollTop = log.scrollHeight;
+ }
+
+ const cancelButton = transactionProps.AllowCancel
+ ? <Button variant="secondary" onClick={onCancel} size="sm">{_("Cancel")}</Button>
+ : null;
+
+ if (actions.length === 0 && percentage === 0) {
+ return <EmptyStatePanel title={ _("Initializing...") }
+ headingLevel="h5"
+ titleSize="4xl"
+ secondary={cancelButton}
+ loading
+ />;
+ }
+
+ const lastAction = actions[actions.length - 1];
+ // when resuming an upgrade, we did not get any Package signal yet; fall back to LastPackage
+ const curPackage = formatPackageId(lastAction?.packageId || transactionProps.LastPackage || "");
+ return (
+ <div className="progress-main-view">
+ <Grid hasGutter>
+ <GridItem span="9">
+ <div className="progress-description">
+ <Spinner size="md" />
+ <strong>{ PK_STATUS_STRINGS[lastAction?.status] || PK_STATUS_STRINGS[PK.Enum.STATUS_UPDATE] }</strong>
+ &nbsp;{curPackage}
+ </div>
+ <Progress title={remain}
+ value={percentage}
+ size={ProgressSize.sm}
+ className="pf-v5-u-mb-xs" />
+ </GridItem>
+
+ <GridItem span="3">{cancelButton}</GridItem>
+
+ <GridItem span="12">
+ <Switch id="reboot-after" isChecked={rebootAfter}
+ label={ _("Reboot after completion") }
+ onChange={setRebootAfter} />
+ </GridItem>
+
+ <GridItem span="12" className="update-log">
+ <ExpandableSection toggleText={_("View update log")} onToggle={() => {
+ // always scroll down on expansion
+ const log = document.getElementById("update-log");
+ log.scrollTop = log.scrollHeight;
+ }}>
+ <div id="update-log" className="update-log-content">
+ <table>
+ <tbody>
+ { actions.slice(0, -1).map((action, i) => (
+ <tr key={action.packageId + i}>
+ <th>{PK_STATUS_LOG_STRINGS[action.status] || PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_UPDATE]}</th>
+ <td>{formatPackageId(action.packageId)}</td>
+ </tr>)) }
+ </tbody>
+ </table>
+ </div>
+ </ExpandableSection>
+ </GridItem>
+ </Grid>
+ </div>
+ );
+};
+
+const TwoColumnContent = ({ list, flexClassName }) => {
+ const half = Math.round(list.length / 2);
+ const col1 = list.slice(0, half);
+ const col2 = list.slice(half);
+ return (
+ <Flex className={flexClassName}>
+ <FlexItem flex={{ default: 'flex_1' }}>
+ <TextContent>
+ <TextList>
+ {col1.map(item => (<TextListItem key={item}>{item}</TextListItem>))}
+ </TextList>
+ </TextContent>
+ </FlexItem>
+ {col2.length > 0 && <FlexItem flex={{ default: 'flex_1' }}>
+ <TextContent>
+ <TextList>
+ {col2.map(item => (<TextListItem key={item}>{item}</TextListItem>))}
+ </TextList>
+ </TextContent>
+ </FlexItem>}
+ </Flex>
+ );
+};
+
+const TwoColumnTitle = ({ icon, str }) => {
+ return (<>
+ {icon}
+ <span className="update-success-table-title">
+ {str}
+ </span>
+ </>);
+};
+
+const UpdateSuccess = ({ onIgnore, openServiceRestartDialog, openRebootDialog, restart, manual, reboot, tracerAvailable, history }) => {
+ if (!tracerAvailable) {
+ return (<>
+ <EmptyStatePanel icon={RebootingIcon}
+ title={ _("Update was successful") }
+ headingLevel="h5"
+ titleSize="4xl"
+ paragraph={ _("Updated packages may require a reboot to take effect.") }
+ secondary={
+ <>
+ <Button id="reboot-system" variant="primary" onClick={openRebootDialog}>{_("Reboot system...")}</Button>
+ <Button id="ignore" variant="link" onClick={onIgnore}>{_("Ignore")}</Button>
+ </>
+ } />
+ <div className="flow-list-blank-slate">
+ <ExpandableSection toggleText={_("Package information")}>
+ <PackageList packages={history[0]} />
+ </ExpandableSection>
+ </div>
+ </>);
+ }
+
+ const entries = [];
+ if (reboot.length > 0) {
+ entries.push({
+ columns: [
+ {
+ title: <TwoColumnTitle icon={<RebootingIcon />}
+ str={cockpit.format(cockpit.ngettext("$0 package needs a system reboot",
+ "$0 packages need a system reboot",
+ reboot.length),
+ reboot.length)} />
+ },
+ ],
+ props: { key: "reboot", id: "reboot-row" },
+ hasPadding: true,
+ expandedContent: <TwoColumnContent list={reboot} />,
+ });
+ }
+
+ if (restart.length > 0) {
+ entries.push({
+ columns: [
+ {
+ title: <TwoColumnTitle icon={<ProcessAutomationIcon />}
+ str={cockpit.format(cockpit.ngettext("$0 service needs to be restarted",
+ "$0 services need to be restarted",
+ restart.length),
+ restart.length)} />
+ },
+ ],
+ props: { key: "service", id: "service-row" },
+ hasPadding: true,
+ expandedContent: <TwoColumnContent list={restart} />,
+ });
+ }
+
+ if (manual.length > 0) {
+ entries.push({
+ columns: [
+ {
+ title: <TwoColumnTitle icon={<ProcessAutomationIcon />}
+ str={_("Some software needs to be restarted manually")} />
+ }
+ ],
+ props: { key: "manual", id: "manual-row" },
+ hasPadding: true,
+ expandedContent: <TwoColumnContent list={manual} />,
+ });
+ }
+
+ const showReboot = reboot.length > 0 || manual.length > 0;
+
+ return (<>
+ <EmptyStatePanel title={ _("Update was successful") }
+ headingLevel="h5"
+ titleSize="4xl"
+ secondary={
+ <>
+ { entries.length > 0 && <ListingTable aria-label={_("Update Success Table")}
+ columns={[{ title: _("Info") }]}
+ showHeader={false}
+ className="updates-success-table"
+ rows={entries} /> }
+ <div className="update-success-actions">
+ { showReboot && <Button id="reboot-system" variant="primary" onClick={openRebootDialog}>{_("Reboot system...")}</Button> }
+ { restart.length > 0 && <Button id="choose-service" variant={showReboot ? "secondary" : "primary"} onClick={openServiceRestartDialog}>{_("Restart services...")}</Button> }
+ { reboot.length > 0 || restart.length > 0 || manual.length > 0
+ ? <Button id="ignore" variant="link" onClick={onIgnore}>{_("Ignore")}</Button>
+ : <Button id="ignore" variant="primary" onClick={onIgnore}>{_("Continue")}</Button> }
+ </div>
+ </>
+ } />
+ <div className="flow-list-blank-slate">
+ <ExpandableSection toggleText={_("Package information")}>
+ <PackageList packages={history[0]} />
+ </ExpandableSection>
+ </div>
+ </>);
+};
+
+const UpdatesStatus = ({ updates, highestSeverity, timeSinceRefresh, tracerPackages, onValueChanged }) => {
+ const numUpdates = Object.keys(updates).length;
+ const numSecurity = count_security_updates(updates);
+ const numRestartServices = tracerPackages.daemons.length;
+ const numManualSoftware = tracerPackages.manual.length;
+ const numRebootPackages = tracerPackages.reboot.length;
+ let lastChecked;
+ // PackageKit returns G_MAXUINT if the db was never checked.
+ if (timeSinceRefresh !== null && timeSinceRefresh !== 2 ** 32 - 1)
+ lastChecked = cockpit.format(_("Last checked: $0"), timeformat.distanceToNow(new Date().valueOf() - timeSinceRefresh * 1000, true));
+
+ const notifications = [];
+ if (numUpdates > 0) {
+ if (numUpdates == numSecurity) {
+ const stateStr = cockpit.ngettext("$0 security fix available", "$0 security fixes available", numSecurity);
+ notifications.push({
+ id: "security-updates-available",
+ stateStr: cockpit.format(stateStr, numSecurity),
+ icon: getSeverityIcon(highestSeverity),
+ secondary: <Text id="last-checked" component={TextVariants.small}>{lastChecked}</Text>
+ });
+ } else {
+ let stateStr = cockpit.ngettext("$0 update available", "$0 updates available", numUpdates);
+ if (numSecurity > 0)
+ stateStr += cockpit.ngettext(", including $1 security fix", ", including $1 security fixes", numSecurity);
+ notifications.push({
+ id: "updates-available",
+ stateStr: cockpit.format(stateStr, numUpdates, numSecurity),
+ icon: getSeverityIcon(highestSeverity),
+ secondary: <Text id="last-checked" component={TextVariants.small}>{lastChecked}</Text>
+ });
+ }
+ } else if (!numRestartServices && !numRebootPackages && !numManualSoftware) {
+ notifications.push({
+ id: "system-up-to-date",
+ stateStr: STATE_HEADINGS.uptodate,
+ icon: <CheckIcon color="green" />,
+ secondary: <Text id="last-checked" component={TextVariants.small}>{lastChecked}</Text>
+ });
+ }
+
+ if (numRebootPackages > 0) {
+ const stateStr = cockpit.ngettext("$0 package needs a system reboot", "$0 packages need a system reboot", numRebootPackages);
+ notifications.push({
+ id: "packages-need-reboot",
+ stateStr: cockpit.format(stateStr, numRebootPackages),
+ icon: <RebootingIcon />,
+ secondary: <Button variant="danger" onClick={() => onValueChanged("showRebootSystemDialog", true)}>
+ {_("Reboot system...")}
+ </Button>
+ });
+ }
+
+ if (numRestartServices > 0) {
+ const stateStr = cockpit.ngettext("$0 service needs to be restarted", "$0 services need to be restarted", numRestartServices);
+ notifications.push({
+ id: "services-need-restart",
+ stateStr: cockpit.format(stateStr, numRestartServices),
+ icon: <ProcessAutomationIcon />,
+ secondary: <Button variant="primary" onClick={() => onValueChanged("showRestartServicesDialog", true)}>
+ {_("Restart services...")}
+ </Button>
+ });
+ }
+
+ if (numManualSoftware > 0) {
+ notifications.push({
+ id: "processes-need-restart",
+ stateStr: _("Some software needs to be restarted manually"),
+ icon: <ProcessAutomationIcon />,
+ secondary: <Text component={TextVariants.small}>{tracerPackages.manual.join(", ")}</Text>
+ });
+ }
+
+ return (<Stack hasGutter>
+ { notifications.map(notification => (
+ <StackItem key={notification.id}>
+ <Flex flexWrap={{ default: 'nowrap' }} id={notification.id}>
+ <FlexItem>
+ {notification.icon}
+ </FlexItem>
+ <FlexItem>
+ <Stack>
+ <StackItem>
+ <Text component={TextVariants.p}>{notification.stateStr}</Text>
+ </StackItem>
+ <StackItem>
+ { notification.secondary }
+ </StackItem>
+ </Stack>
+ </FlexItem>
+ </Flex>
+ </StackItem>
+ ))}
+ </Stack>);
+};
+
+class CardsPage extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ autoupdates_backend: undefined,
+ };
+ }
+
+ componentDidMount() {
+ getBackend(this.props.backend).then(b => { this.setState({ autoupdates_backend: b }) });
+ }
+
+ render() {
+ const cardContents = [];
+ let settingsContent = null;
+ const statusContent = <Stack hasGutter>
+ <UpdatesStatus key="updates-status"
+ updates={this.props.updates}
+ onValueChanged={this.props.onValueChanged}
+ tracerPackages={this.props.tracerPackages}
+ highestSeverity={this.props.highestSeverity}
+ timeSinceRefresh={this.props.timeSinceRefresh} />
+ <KpatchStatus />
+ </Stack>;
+
+ if (this.state.autoupdates_backend) {
+ settingsContent = <Stack hasGutter>
+ <AutoUpdates privileged={this.props.privileged} packagekit_backend={this.props.backend} />
+ <KpatchSettings privileged={this.props.privileged} />
+ </Stack>;
+ }
+
+ cardContents.push({
+ id: "status",
+ className: settingsContent !== null ? "ct-card-info" : "",
+ title: _("Status"),
+ actions: (<Tooltip content={_("Check for updates")}>
+ <Button variant="secondary" onClick={this.props.handleRefresh}><RedoIcon /></Button>
+ </Tooltip>),
+ body: statusContent,
+ });
+
+ if (settingsContent !== null) {
+ cardContents.push({
+ id: "settings",
+ className: "ct-card-info",
+ title: _("Settings"),
+ body: settingsContent,
+ });
+ }
+
+ if (this.props.state === "available") { // automatic updates are not tracked by PackageKit, hide history when they are enabled
+ cardContents.push({
+ id: "available-updates",
+ title: _("Available updates"),
+ actions: (<div className="pk-updates--header--actions">
+ {this.props.cockpitUpdate &&
+ <Flex flex={{ default: 'inlineFlex' }} className="cockpit-update-warning">
+ <FlexItem>
+ <ExclamationTriangleIcon className="ct-icon-exclamation-triangle cockpit-update-warning-icon" />
+ <strong className="cockpit-update-warning-text">
+ <span className="pf-screen-reader">{_("Danger alert:")}</span>
+ {_("Web Console will restart")}
+ </strong>
+ </FlexItem>
+ <FlexItem>
+ <Popover aria-label="More information popover"
+ bodyContent={_("When the Web Console is restarted, you will no longer see progress information. However, the update process will continue in the background. Reconnect to continue watching the update process.")}>
+ <Button variant="link" isInline>{_("More info...")}</Button>
+ </Popover>
+ </FlexItem>
+ </Flex>}
+ {this.props.applyKpatches}
+ {this.props.applySecurity}
+ {this.props.applyAll}
+ </div>),
+ containsList: true,
+ body: <UpdatesList updates={this.props.updates} />
+ });
+ }
+
+ if ((!this.state.autoupdates_backend || !this.state.autoupdates_backend.enabled) && this.props.history.length > 0) { // automatic updates are not tracked by PackageKit, hide history when they are enabled
+ cardContents.push({
+ id: "update-history",
+ title: _("Update history"),
+ containsList: true,
+ body: <History packagekit={this.props.history} />
+ });
+ }
+
+ return cardContents.map(card => {
+ return (
+ <Card key={card.id} className={card.className} id={card.id}>
+ <CardHeader actions={{ actions: card.actions }}>
+ <CardTitle component="h2">{card.title}</CardTitle>
+ </CardHeader>
+ <CardBody className={card.containsList ? "contains-list" : null}>
+ {card.body}
+ </CardBody>
+ </Card>
+ );
+ });
+ }
+}
+
+class OsUpdates extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ state: "loading",
+ errorMessages: [],
+ updates: {},
+ timeSinceRefresh: null,
+ loadPercent: null,
+ cockpitUpdate: false,
+ haveOsRepo: null,
+ applyTransaction: null,
+ applyTransactionProps: {},
+ applyActions: [],
+ history: [],
+ unregistered: false,
+ privileged: false,
+ autoUpdatesEnabled: undefined,
+ tracerPackages: { daemons: [], manual: [], reboot: [] },
+ tracerAvailable: false,
+ tracerRunning: false,
+ showRestartServicesDialog: false,
+ showRebootSystemDialog: false,
+ backend: "",
+ rebootAfterSuccess: false,
+ };
+ this.handleLoadError = this.handleLoadError.bind(this);
+ this.handleRefresh = this.handleRefresh.bind(this);
+ this.loadUpdates = this.loadUpdates.bind(this);
+ this.onValueChanged = this.onValueChanged.bind(this);
+
+ superuser.addEventListener("changed", () => {
+ this.setState({ privileged: superuser.allowed });
+ // get out of error state when switching from unprivileged to privileged
+ if (superuser.allowed && this.state.state.indexOf("Error") >= 0)
+ this.loadUpdates();
+ });
+ }
+
+ onValueChanged(key, value) {
+ this.setState({ [key]: value });
+ }
+
+ componentDidMount() {
+ this._mounted = true;
+ this.callTracer(null);
+
+ PK.getBackendName().then(([prop]) => this.setState({ backend: prop.v }));
+
+ // check if there is an upgrade in progress already; if so, switch to "applying" state right away
+ PK.call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "GetTransactionList", [])
+ .then(([transactions]) => {
+ if (!this._mounted)
+ return;
+
+ const promises = transactions.map(transactionPath => PK.call(
+ transactionPath, "org.freedesktop.DBus.Properties", "Get", [PK.transactionInterface, "Role"]));
+
+ Promise.all(promises)
+ .then(roles => {
+ // any transaction with UPDATE_PACKAGES role?
+ for (let idx = 0; idx < roles.length; ++idx) {
+ if (roles[idx][0].v === PK.Enum.ROLE_UPDATE_PACKAGES) {
+ this.watchUpdates(transactions[idx]);
+ return;
+ }
+ }
+
+ // no running updates found, proceed to showing available updates
+ this.initialLoadOrRefresh();
+ })
+ .catch(ex => {
+ console.warn("GetTransactionList: failed to read PackageKit transaction roles:", ex.message);
+ // be robust, try to continue with loading updates anyway
+ this.initialLoadOrRefresh();
+ });
+ })
+ .catch(this.handleLoadError);
+ }
+
+ componentWillUnmount() {
+ this._mounted = false;
+ }
+
+ callTracer(state) {
+ this.setState({ tracerRunning: true });
+ python.spawn(callTracerScript, null, { err: "message", superuser: "require" })
+ .then(output => {
+ const tracerPackages = JSON.parse(output);
+ // Filter out duplicates
+ tracerPackages.reboot = [...new Set(shortenCockpitWsInstance(tracerPackages.reboot))];
+ tracerPackages.daemons = [...new Set(shortenCockpitWsInstance(tracerPackages.daemons))];
+ tracerPackages.manual = [...new Set(shortenCockpitWsInstance(tracerPackages.manual))];
+ const nextState = { tracerAvailable: true, tracerRunning: false, tracerPackages };
+ if (state)
+ nextState.state = state;
+
+ this.setState(nextState);
+ })
+ .catch((exception, data) => {
+ // common cases: this platform does not have tracer installed
+ if (!exception.message?.includes("ModuleNotFoundError") &&
+ // or supported (like on Arch)
+ !exception.message?.includes("UnsupportedDistribution") &&
+ // or polkit does not allow it
+ exception.problem !== "access-denied" &&
+ // or unprivileged session
+ exception.problem !== "authentication-failed" &&
+ // or the session goes away while checking
+ exception.problem !== "terminated")
+ console.error(`Tracer failed: "${JSON.stringify(exception)}", data: "${JSON.stringify(data)}"`);
+ // When tracer fails, act like it's not available (demand reboot after every update)
+ const nextState = { tracerAvailable: false, tracerRunning: false, tracerPackages: { reboot: [], daemons: [], manual: [] } };
+ if (state)
+ nextState.state = state;
+ this.setState(nextState);
+ });
+ }
+
+ handleLoadError(ex) {
+ console.warn("loading available updates failed:", JSON.stringify(ex));
+
+ if (!this._mounted)
+ return;
+
+ if (ex.problem === "not-found" || ex.name?.includes("DBus.Error.ServiceUnknown"))
+ ex = _("PackageKit is not installed");
+ this.state.errorMessages.push(ex.detail || ex.message || ex);
+ this.setState({ state: "loadError" });
+ }
+
+ removeHeading(text) {
+ // on Debian the update_text starts with "== version ==" which is
+ // redundant; we don't want Markdown headings in the table
+ if (text)
+ return text.trim().replace(/^== .* ==\n/, "")
+ .trim();
+ return text;
+ }
+
+ loadUpdateDetails(pkg_ids) {
+ const limit = 500; // Load iteratively to avoid exceeding cockpit-ws frame size
+ PK.cancellableTransaction("GetUpdateDetail", [pkg_ids.slice(0, limit)], null, {
+ UpdateDetail: (packageId, updates, obsoletes, vendor_urls, bug_urls, cve_urls, restart,
+ update_text, changelog /* state, issued, updated */) => {
+ const u = this.state.updates[packageId];
+ if (!u) {
+ console.warn("Mismatching update:", packageId);
+ return;
+ }
+
+ u.vendor_urls = vendor_urls;
+ // HACK: bug_urls and cve_urls also contain titles, in a not-quite-predictable order; ignore them,
+ // only pick out http[s] URLs (https://bugs.freedesktop.org/show_bug.cgi?id=104552)
+ if (bug_urls)
+ bug_urls = bug_urls.filter(url => url.match(/^https?:\/\//));
+ if (cve_urls)
+ cve_urls = cve_urls.filter(url => url.match(/^https?:\/\//));
+
+ u.description = this.removeHeading(update_text) || changelog;
+ if (update_text)
+ u.markdown = true;
+ u.bug_urls = deduplicate(bug_urls);
+ // many backends don't support proper severities; parse CVEs from description as a fallback
+ u.cve_urls = deduplicate(cve_urls && cve_urls.length > 0 ? cve_urls : parseCVEs(u.description));
+ if (u.cve_urls && u.cve_urls.length > 0)
+ u.severity = PK.Enum.INFO_SECURITY;
+ u.vendor_urls = vendor_urls || [];
+ // u.restart = restart; // broken (always "1") at least in Fedora
+
+ this.setState(prevState => ({ updates: prevState.updates }));
+ },
+ })
+ .then(() => {
+ if (pkg_ids.length <= limit)
+ this.setState({ state: "available" });
+ else
+ this.loadUpdateDetails(pkg_ids.slice(limit));
+ })
+ .catch(ex => {
+ console.warn("GetUpdateDetail failed:", JSON.stringify(ex));
+ // still show available updates, with reduced detail
+ this.setState({ state: "available" });
+ });
+ }
+
+ loadUpdates() {
+ const updates = {};
+ let cockpitUpdate = false;
+
+ this.setState({ state: "loading" });
+
+ // check if there is an available version of coreutils; this is a heuristics for unregistered RHEL
+ // systems to see if they need a subscription to get "proper" OS updates
+ let have_coreutils = false;
+ PK.cancellableTransaction(
+ "Resolve",
+ [PK.Enum.FILTER_ARCH | PK.Enum.FILTER_NEWEST | PK.Enum.FILTER_NOT_INSTALLED, ["coreutils"]],
+ null,
+ {
+ Package: (info, package_id) => { have_coreutils = true }
+ })
+ .then(() => this.setState({ haveOsRepo: have_coreutils }),
+ ex => console.warn("Resolving coreutils failed:", JSON.stringify(ex)))
+ .then(() => PK.cancellableTransaction(
+ "GetUpdates", [0],
+ data => this.setState({ state: data.waiting ? "locked" : "loading" }),
+ {
+ Package: (info, packageId, _summary) => {
+ const id_fields = packageId.split(";");
+ packageSummaries[id_fields[0]] = _summary;
+ // HACK: dnf backend yields wrong severity with PK < 1.2.4 (https://github.com/PackageKit/PackageKit/issues/268)
+ if (info < PK.Enum.INFO_LOW || info > PK.Enum.INFO_SECURITY)
+ info = PK.Enum.INFO_NORMAL;
+ updates[packageId] = { name: id_fields[0], version: id_fields[1], severity: info, arch: id_fields[2] };
+ if (id_fields[0] == "cockpit-ws")
+ cockpitUpdate = true;
+ // Arch Linux has no cockpit-ws package
+ if (id_fields[0] == "cockpit" && this.state.backend === "alpm")
+ cockpitUpdate = true;
+ },
+ }))
+ .then(() => {
+ // get the details for all packages
+ const pkg_ids = Object.keys(updates);
+ if (pkg_ids.length) {
+ this.setState({ updates, cockpitUpdate }, () => {
+ this.loadUpdateDetails(pkg_ids);
+ });
+ } else {
+ this.setState({ updates: {}, state: "uptodate" });
+ }
+ this.loadHistory();
+ })
+ .catch(this.handleLoadError);
+ }
+
+ loadHistory() {
+ const history = [];
+
+ // would be nice to filter only for "update-packages" role, but can't here
+ PK.transaction("GetOldTransactions", [0], {
+ Transaction: (objPath, timeSpec, succeeded, role, duration, data) => {
+ if (role !== PK.Enum.ROLE_UPDATE_PACKAGES)
+ return;
+ // data looks like:
+ // downloading\tbash-completion;1:2.6-1.fc26;noarch;updates-testing
+ // updating\tbash-completion;1:2.6-1.fc26;noarch;updates-testing
+ const pkgs = { _time: Date.parse(timeSpec) };
+ let empty = true;
+ data.split("\n").forEach(line => {
+ const fields = line.trim().split("\t");
+ if (fields.length >= 2) {
+ const pkgId = fields[1].split(";");
+ pkgs[pkgId[0]] = pkgId[1];
+ empty = false;
+ }
+ });
+ if (!empty)
+ history.unshift(pkgs); // PK reports in time-ascending order, but we want the latest first
+ },
+
+ // only update the state once to avoid flicker
+ Finished: () => {
+ if (history.length > 0)
+ this.setState({ history });
+ }
+ })
+ .catch(ex => console.warn("Failed to load old transactions:", ex));
+ }
+
+ initialLoadOrRefresh() {
+ PK.watchRedHatSubscription(registered => this.setState({ unregistered: !registered }));
+
+ cockpit.addEventListener("visibilitychange", () => {
+ if (!cockpit.hidden)
+ this.loadOrRefresh(false);
+ });
+
+ if (!cockpit.hidden)
+ this.loadOrRefresh(true);
+ else
+ this.loadUpdates();
+ }
+
+ loadOrRefresh(always_load) {
+ PK.call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "GetTimeSinceAction",
+ [PK.Enum.ROLE_REFRESH_CACHE])
+ .then(([seconds]) => {
+ this.setState({ timeSinceRefresh: seconds });
+
+ // automatically trigger refresh for ≥ 1 day or if never refreshed
+ if (seconds >= 24 * 3600 || seconds < 0)
+ this.handleRefresh();
+ else if (always_load)
+ this.loadUpdates();
+ })
+ .catch(this.handleLoadError);
+ }
+
+ watchUpdates(transactionPath) {
+ this.setState({ state: "applying", applyTransaction: transactionPath, applyTransactionProps: {}, applyActions: [] });
+
+ return PK.watchTransaction(transactionPath,
+ {
+ ErrorCode: (code, details) => this.state.errorMessages.push(details),
+
+ Finished: exit => {
+ this.setState({ applyTransaction: null, applyTransactionProps: {}, applyActions: [] });
+
+ if (exit === PK.Enum.EXIT_SUCCESS) {
+ if (this.state.tracerAvailable) {
+ this.setState({ state: "loading", loadPercent: null });
+ this.callTracer("updateSuccess");
+ } else {
+ this.setState({ state: "updateSuccess", loadPercent: null });
+ }
+ this.loadHistory();
+ } else if (exit === PK.Enum.EXIT_CANCELLED) {
+ if (this.state.tracerAvailable) {
+ this.setState({ state: "loading", loadPercent: null });
+ this.callTracer(null);
+ }
+ this.loadUpdates();
+ } else {
+ // normally we get FAILED here with ErrorCodes; handle unexpected errors to allow for some debugging
+ if (exit !== PK.Enum.EXIT_FAILED)
+ this.state.errorMessages.push(cockpit.format(_("PackageKit reported error code $0"), exit));
+ this.setState({ state: "updateError" });
+ }
+ },
+
+ // not working/being used in at least Fedora
+ RequireRestart: (type, packageId) => console.log("update RequireRestart", type, packageId),
+
+ Package: (status, packageId) => this.setState(old =>
+ ({ applyActions: [...old.applyActions, { status, packageId }] })
+ ),
+ },
+
+ notify => this.setState(old =>
+ ({ applyTransactionProps: { ...old.applyTransactionProps, ...notify } })
+ )
+ )
+ .catch(ex => {
+ this.state.errorMessages.push(ex);
+ this.setState({ state: "updateError" });
+ });
+ }
+
+ applyUpdates(type) {
+ let ids = Object.keys(this.state.updates);
+ if (type === UPDATES.SECURITY)
+ ids = ids.filter(id => this.state.updates[id].severity === PK.Enum.INFO_SECURITY);
+ if (type === UPDATES.KPATCHES) {
+ ids = ids.filter(id => isKpatchPackage(this.state.updates[id].name));
+ }
+
+ PK.transaction()
+ .then(transactionPath => {
+ this.watchUpdates(transactionPath)
+ .then(() => {
+ PK.call(transactionPath, PK.transactionInterface, "UpdatePackages", [0, ids])
+ .catch(ex => {
+ // We get more useful error messages through ErrorCode or "PackageKit has crashed", so only
+ // show this if we don't have anything else
+ if (this.state.errorMessages.length === 0)
+ this.state.errorMessages.push(ex.message);
+ this.setState({ state: "updateError" });
+ });
+ });
+ })
+ .catch(ex => {
+ this.state.errorMessages.push(ex.message);
+ this.setState({ state: "updateError" });
+ });
+ }
+
+ renderContent() {
+ let applySecurity, applyKpatches, applyAll;
+
+ /* On unregistered RHEL systems we need some heuristics: If the "main" OS repos (which provide coreutils) require
+ * a subscription, then point this out and don't show available updates, even if there are some auxiliary
+ * repositories enabled which don't require subscriptions. But there are a lot of cases (cloud repos, nightly internal
+ * repos) which don't need a subscription, there it would just be confusing */
+ if (this.state.unregistered && this.state.haveOsRepo === false) {
+ page_status.set_own({
+ type: "warning",
+ title: _("Not registered"),
+ details: {
+ link: "subscriptions",
+ }
+ });
+
+ return <EmptyStatePanel
+ title={_("This system is not registered")}
+ headingLevel="h5"
+ titleSize="4xl"
+ paragraph={ _("To get software updates, this system needs to be registered with Red Hat, either using the Red Hat Customer Portal or a local subscription server.") }
+ icon={ExclamationCircleIcon}
+ action={ _("Register…") }
+ onAction={ () => cockpit.jump("/subscriptions", cockpit.transport.host) }
+ />;
+ }
+
+ switch (this.state.state) {
+ case "loading":
+ case "refreshing":
+ case "locked":
+ page_status.set_own({
+ type: null,
+ title: _("Checking for package updates..."),
+ details: {
+ link: false,
+ pficon: "spinner",
+ }
+ });
+
+ if (this.state.loadPercent)
+ return <Progress value={this.state.loadPercent} title={STATE_HEADINGS[this.state.state]} />;
+ else
+ return <EmptyStatePanel loading title={ _("Checking software status")}
+ headingLevel="h5"
+ titleSize="4xl"
+ paragraph={STATE_HEADINGS[this.state.state]}
+ />;
+
+ case "available":
+ {
+ const num_updates = Object.keys(this.state.updates).length;
+ const num_security_updates = count_security_updates(this.state.updates);
+ const num_kpatches = count_kpatch_updates(this.state.updates);
+ const highest_severity = find_highest_severity(this.state.updates);
+
+ applyAll = (
+ <Button id={num_updates == num_security_updates ? "install-security" : "install-all"} variant="primary" onClick={ () => this.applyUpdates(UPDATES.ALL) }>
+ { num_updates == num_security_updates
+ ? _("Install security updates")
+ : _("Install all updates") }
+ </Button>);
+
+ if (num_security_updates > 0 && num_updates > num_security_updates) {
+ applySecurity = (
+ <Button id="install-security" variant="secondary" onClick={ () => this.applyUpdates(UPDATES.SECURITY) }>
+ {_("Install security updates")}
+ </Button>);
+ }
+
+ if (num_kpatches > 0) {
+ applyKpatches = (
+ <Button id="install-kpatches" variant="secondary" onClick={ () => this.applyUpdates(UPDATES.KPATCHES) }>
+ {_("Install kpatch updates")}
+ </Button>);
+ }
+
+ let text;
+ if (highest_severity == PK.Enum.INFO_SECURITY)
+ text = _("Security updates available");
+ else if (highest_severity >= PK.Enum.INFO_NORMAL)
+ text = _("Bug fix updates available");
+ else if (highest_severity >= PK.Enum.INFO_LOW)
+ text = _("Enhancement updates available");
+ else
+ text = _("Updates available");
+
+ page_status.set_own({
+ type: num_security_updates > 0 ? "warning" : "info",
+ title: text,
+ details: {
+ pficon: getPageStatusSeverityIcon(highest_severity)
+ }
+ });
+
+ return (
+ <>
+ <PageSection>
+ <Gallery className='ct-cards-grid' hasGutter>
+ <CardsPage handleRefresh={this.handleRefresh}
+ applySecurity={applySecurity}
+ applyAll={applyAll}
+ applyKpatches={applyKpatches}
+ highestSeverity={highest_severity}
+ onValueChanged={this.onValueChanged}
+ {...this.state} />
+ </Gallery>
+ </PageSection>
+ { this.state.showRestartServicesDialog &&
+ <RestartServices tracerPackages={this.state.tracerPackages}
+ close={() => this.setState({ showRestartServicesDialog: false })}
+ state={this.state.state}
+ callTracer={(state) => this.callTracer(state)}
+ onValueChanged={delta => this.setState(delta)}
+ loadUpdates={this.loadUpdates} />
+ }
+ { this.state.showRebootSystemDialog &&
+ <ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
+ }
+ </>
+ );
+ }
+
+ case "loadError":
+ case "updateError":
+ page_status.set_own({
+ type: "error",
+ title: STATE_HEADINGS[this.state.state],
+ });
+ return (
+ <Stack>
+ <EmptyStatePanel title={ STATE_HEADINGS[this.state.state] }
+ icon={ ExclamationCircleIcon }
+ paragraph={
+ <TextContent>
+ <Text component={TextVariants.p}>
+ {_("Please resolve the issue and reload this page.")}
+ </Text>
+ </TextContent>
+ }
+ />
+ <CodeBlock className='pf-v5-u-mx-auto error-log'>
+ <CodeBlockCode>
+ {this.state.errorMessages
+ .filter((m, index) => index == 0 || m != this.state.errorMessages[index - 1])
+ .map(m => <span key={m}>{m}</span>)}
+ </CodeBlockCode>
+ </CodeBlock>
+ </Stack>
+ );
+
+ case "applying":
+ page_status.set_own(null);
+ return <ApplyUpdates transactionProps={this.state.applyTransactionProps}
+ actions={this.state.applyActions}
+ onCancel={ () => PK.call(this.state.applyTransaction, PK.transactionInterface, "Cancel", []) }
+ rebootAfter={this.state.rebootAfterSuccess}
+ setRebootAfter={ (_event, enabled) => this.setState({ rebootAfterSuccess: enabled }) }
+ />;
+
+ case "updateSuccess": {
+ if (this.state.rebootAfterSuccess) {
+ this.setState({ state: "restart" });
+ cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require" });
+ return null;
+ }
+
+ let warningTitle;
+ if (!this.state.tracerAvailable) {
+ warningTitle = _("Reboot recommended");
+ } else {
+ if (this.state.tracerPackages.reboot.length > 0)
+ warningTitle = cockpit.ngettext("A package needs a system reboot for the updates to take effect:",
+ "Some packages need a system reboot for the updates to take effect:",
+ this.state.tracerPackages.reboot.length);
+ else if (this.state.tracerPackages.daemons.length > 0)
+ warningTitle = cockpit.ngettext("A service needs to be restarted for the updates to take effect:",
+ "Some services need to be restarted for the updates to take effect:",
+ this.state.tracerPackages.daemons.length);
+ else if (this.state.tracerPackages.manual.length > 0)
+ warningTitle = _("Some software needs to be restarted manually");
+ }
+
+ if (warningTitle) {
+ page_status.set_own({
+ type: "warning",
+ title: warningTitle
+ });
+ }
+
+ return (
+ <>
+ <UpdateSuccess onIgnore={this.loadUpdates}
+ openServiceRestartDialog={() => this.setState({ showRestartServicesDialog: true })}
+ openRebootDialog={() => this.setState({ showRebootSystemDialog: true })}
+ restart={this.state.tracerPackages.daemons}
+ manual={this.state.tracerPackages.manual}
+ reboot={this.state.tracerPackages.reboot}
+ tracerAvailable={this.state.tracerAvailable}
+ history={this.state.history} />
+ { this.state.showRebootSystemDialog &&
+ <ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
+ }
+ { this.state.showRestartServicesDialog &&
+ <RestartServices tracerPackages={this.state.tracerPackages}
+ close={() => this.setState({ showRestartServicesDialog: false })}
+ state={this.state.state}
+ callTracer={(state) => this.callTracer(state)}
+ onValueChanged={delta => this.setState(delta)}
+ loadUpdates={this.loadUpdates} />
+ }
+ </>
+ );
+ }
+
+ case "restart":
+ page_status.set_own(null);
+ return <EmptyStatePanel loading title={ _("Restarting") }
+ headingLevel="h5"
+ titleSize="4xl"
+ paragraph={ _("Your server will close the connection soon. You can reconnect after it has restarted.") }
+ />;
+
+ case "uptodate":
+ {
+ page_status.set_own({
+ title: STATE_HEADINGS[this.state.state],
+ details: {
+ link: false,
+ pficon: "check",
+ }
+ });
+
+ return (
+ <>
+ <PageSection>
+ <Gallery className='ct-cards-grid' hasGutter>
+ <CardsPage onValueChanged={this.onValueChanged} handleRefresh={this.handleRefresh} {...this.state} />
+ </Gallery>
+ { this.state.showRestartServicesDialog &&
+ <RestartServices tracerPackages={this.state.tracerPackages}
+ close={() => this.setState({ showRestartServicesDialog: false })}
+ state={this.state.state}
+ callTracer={(state) => this.callTracer(state)}
+ onValueChanged={delta => this.setState(delta)}
+ loadUpdates={this.loadUpdates} />
+ }
+ { this.state.showRebootSystemDialog &&
+ <ShutdownModal onClose={() => this.setState({ showRebootSystemDialog: false })} />
+ }
+ </PageSection>
+ </>
+ );
+ }
+
+ default:
+ page_status.set_own(null);
+ return null;
+ }
+ }
+
+ handleRefresh() {
+ this.setState({ state: "refreshing", loadPercent: null });
+ PK.cancellableTransaction("RefreshCache", [true], data => this.setState({ loadPercent: data.percentage }))
+ .then(() => {
+ if (this._mounted === false)
+ return;
+
+ this.setState({ timeSinceRefresh: 0 });
+ this.loadUpdates();
+ })
+ .catch(this.handleLoadError);
+ }
+
+ render() {
+ let content = this.renderContent();
+ if (!["available", "uptodate"].includes(this.state.state))
+ content = <PageSection variant={PageSectionVariants.light}>{content}</PageSection>;
+
+ return (
+ <WithDialogs>
+ <Page>
+ {content}
+ </Page>
+ </WithDialogs>
+ );
+ }
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.title = cockpit.gettext(document.title);
+ init();
+ const root = createRoot(document.getElementById('app'));
+ root.render(<OsUpdates />);
+});
diff --git a/pkg/packagekit/updates.scss b/pkg/packagekit/updates.scss
new file mode 100644
index 0000000..00718ef
--- /dev/null
+++ b/pkg/packagekit/updates.scss
@@ -0,0 +1,275 @@
+@use "ct-card";
+@use "page";
+
+@import "@patternfly/patternfly/utilities/Spacing/spacing.css";
+
+/* Style the list cards as ct-cards */
+.pf-v5-c-page__main-section .pf-v5-c-card {
+ @extend .ct-card;
+}
+
+.pf-v5-c-table tr:nth-child(1) {
+ > td, th {
+ --pf-v5-c-table--cell--PaddingTop: var(--pf-v5-global--spacer--sm);
+ --pf-v5-c-table--cell--PaddingBottom: var(--pf-v5-global--spacer--sm);
+
+ padding-block: var(--pf-v5-c-table--cell--PaddingTop) var(--pf-v5-c-table--cell--PaddingBottom);
+
+ padding-inline: var(--pf-v5-c-table--cell--PaddingLeft) var(--pf-v5-c-table--cell--PaddingRight);
+ }
+}
+
+.kpatches-labelgroup ul.pf-v5-c-label-group__list,
+.kpatches-labelgroup li.pf-v5-c-label-group__list-item:last-child, {
+ margin-block-end: 0;
+}
+
+.pk-updates--header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ padding-block: 1.5rem 1rem;
+ padding-inline: 0;
+
+ &--actions {
+ > .pf-v5-c-button,
+ > .btn-group {
+ margin-block: 5px 0;
+ margin-inline: 0 5px;
+
+ &:last-child {
+ margin-inline-end: 0;
+ }
+ }
+ }
+}
+
+@media screen and (min-width: 640px) {
+ .pk-updates--header--auto {
+ justify-content: flex-start;
+ }
+}
+
+// Make header's content bold
+.pf-v5-c-table tr:nth-child(1) td:nth-child(2) {
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+}
+
+.ct-table tr {
+ .severity-icon {
+ margin-inline-end: var(--pf-v5-global--spacer--xs);
+ }
+
+ td.changelog {
+ vertical-align: top;
+
+ * {
+ display: inline;
+ font: inherit;
+ background: inherit;
+ color: inherit;
+ }
+
+ &, p {
+ max-inline-size: 60vw;
+ margin-block-end: 0; // counter-act <Markdown>
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ th:last-child,
+ td.changelog:last-child {
+ text-align: start;
+ }
+}
+
+.severity-critical {
+ color: #a30000;
+}
+
+.severity-important {
+ color: #ec7a08;
+}
+
+.severity-low {
+ color: var(--ct-color-subtle-copy);
+}
+
+div.changelog {
+ max-block-size: 20em;
+ overflow: auto;
+ white-space: pre-wrap;
+}
+
+/* Hide changelog summaries in narrow widths */
+@media screen and (max-width: 80ch) {
+ table.ct-table {
+ /* Hide changelog header (there's no class) & data */
+ thead th:last-child,
+ td.changelog {
+ display: none;
+ }
+ }
+}
+
+/* don't let the install progress bar get too wide */
+.progress-main-view {
+ max-inline-size: 60rem;
+ margin-block: 10ex 0;
+ margin-inline: auto;
+
+ .pf-v5-l-grid {
+ align-items: end;
+ }
+}
+
+/* workaround font not supporting tabular numbers yet https://github.com/cockpit-project/cockpit/issues/15090 */
+.pf-v5-c-progress__status {
+ min-inline-size: 3ch;
+}
+
+/* Add some space between the spinner and the text */
+.progress-description > svg {
+ margin-inline-end: var(--pf-v5-global--spacer--sm);
+}
+
+.flow-list-blank-slate {
+ margin-block: 0;
+ margin-inline: auto;
+ max-inline-size: 69rem;
+ text-align: center;
+}
+
+.flow-list {
+ padding: 0;
+ text-align: start;
+ box-sizing: border-box;
+
+ li {
+ text-align: start;
+ box-sizing: border-box;
+ inline-size: 22rem;
+ padding-block: 0;
+ padding-inline: 0 1ex;
+ display: inline-block;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+}
+
+// prevent overflowo on small screens
+.error-log {
+ max-inline-size: 100%;
+}
+
+.update-log {
+ text-align: center;
+
+ th {
+ text-align: start;
+ padding-inline-end: 3ex;
+ }
+
+ td {
+ text-align: start;
+ }
+}
+
+.update-log-content {
+ margin-block: 0;
+ margin-inline: 10ex;
+ block-size: 13em;
+ overflow-y: auto;
+}
+
+table.header-buttons {
+ margin-block-end: 20px; /* replacement for h2's margin-bottom */
+ td {
+ vertical-align: middle;
+ }
+
+ h2 {
+ /* vertical default ones break vertical alignment */
+ margin-block: 0;
+ margin-inline: 0 4rem;
+ }
+}
+
+#auto-update-day {
+ max-inline-size: 10rem;
+}
+
+#auto-update-time {
+ max-inline-size: 7rem;
+}
+
+.auto-update-group {
+ // Add spacing between rows for when the flex items wrap
+ row-gap: var(--pf-v5-global--spacer--sm);
+}
+
+.pk-updates .pf-v5-c-description-list + div {
+ padding-block-start: var(--pf-v5-global--spacer--md);
+}
+
+.restart-services-modal-body {
+ padding-block: 1em;
+ padding-inline: 0;
+}
+
+.pf-v5-c-content ul {
+ list-style-type: none;
+}
+
+.updates-history {
+ // Constrain time's width
+ .history-time {
+ inline-size: 0;
+ white-space: nowrap;
+ }
+
+ // Fix alignment of the package count's icon
+ .list-view-pf-additional-info-item {
+ align-items: baseline;
+
+ > svg {
+ margin-inline-end: 1ex;
+ }
+ }
+}
+
+.updates-success-table {
+ margin-block-end: 1em;
+}
+
+.update-success-actions > button {
+ margin-block: var(--pf-v5-global--spacer--xs);
+ margin-inline: var(--pf-v5-global--spacer--sm) 0;
+}
+
+.update-success-table-title {
+ padding-inline-start: var(--pf-v5-global--spacer--sm);
+}
+
+.cockpit-update-warning {
+ margin-inline-end: var(--pf-v5-global--spacer--md);
+}
+
+.cockpit-update-warning-icon {
+ margin-inline-end: var(--pf-v5-global--spacer--sm);
+}
+
+.cockpit-update-warning-text {
+ color: var(--pf-v5-global--warning-color--200);
+}
+
+.autoupdates-card-error {
+ margin-block-end: var(--pf-v5-global--spacer--md);
+}
+
+.ct-info-circle {
+ color: var(--pf-v5-global--info-color--100);
+}
diff --git a/pkg/pcp/manifest.json b/pkg/pcp/manifest.json
new file mode 100644
index 0000000..eb12c1a
--- /dev/null
+++ b/pkg/pcp/manifest.json
@@ -0,0 +1,11 @@
+{
+ "requires": {
+ "cockpit": "239"
+ },
+ "bridges": [
+ {
+ "match": { "payload": "metrics1" },
+ "spawn": [ "${libexecdir}/cockpit-pcp" ]
+ }
+ ]
+}
diff --git a/pkg/playground/exception.html b/pkg/playground/exception.html
new file mode 100644
index 0000000..00f8dc0
--- /dev/null
+++ b/pkg/playground/exception.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Javascript exceptions</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div id="internal" class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h2>Exception</h2>
+
+ <p>Clicking this button should make a javascript exception happen.</p>
+
+ <button id="exception">Exception</button>
+ </section>
+ </main>
+ </div>
+ <script src="exception.js"></script>
+</body>
+</html>
diff --git a/pkg/playground/exception.js b/pkg/playground/exception.js
new file mode 100644
index 0000000..542d444
--- /dev/null
+++ b/pkg/playground/exception.js
@@ -0,0 +1,15 @@
+/* An unhandled javascript exception */
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import cockpit from "cockpit";
+
+const button = document.getElementById("exception");
+button.addEventListener("click", function() {
+ const obj = { };
+ window.setTimeout(function() {
+ obj[0].value = 1;
+ }, 0);
+});
+
+cockpit.transport.wait(function() {
+ document.body.removeAttribute("hidden");
+});
diff --git a/pkg/playground/hammer.gif b/pkg/playground/hammer.gif
new file mode 100644
index 0000000..5588eb8
--- /dev/null
+++ b/pkg/playground/hammer.gif
Binary files differ
diff --git a/pkg/playground/index.html b/pkg/playground/index.html
new file mode 100644
index 0000000..81672d1
--- /dev/null
+++ b/pkg/playground/index.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html id="playground-page">
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Development Playground</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="index.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../manifests.js"></script>
+ <script src="index.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h2>Development Playground</h2>
+ <ul id="nav"></ul>
+ </section>
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h2>Page Status</h2>
+ <label>Type <input id="type" /></label>
+ <label>Title <input id="title" /></label>
+ <button id="set-status" class="pf-v5-c-button pf-m-primary" type="button">Set</button>
+ <button id="clear-status" class="pf-v5-c-button pf-m-secondary" type="button">Clear</button>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/pkg/playground/index.js b/pkg/playground/index.js
new file mode 100644
index 0000000..5aa6aa4
--- /dev/null
+++ b/pkg/playground/index.js
@@ -0,0 +1,38 @@
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import "../../node_modules/@patternfly/patternfly/components/Button/button.css";
+import 'cockpit-dark-theme'; // once per page
+import cockpit from "cockpit";
+import { page_status } from "notifications";
+
+import "../lib/page.scss";
+
+function id(sel) {
+ return document.getElementById(sel);
+}
+
+function init() {
+ const entries = cockpit.manifests.playground.playground;
+ const nav = id("nav");
+
+ for (const p in entries) {
+ const entry = entries[p];
+ const li = document.createElement("li");
+ const a = document.createElement("a");
+ li.appendChild(a);
+ a.appendChild(document.createTextNode(entry.label || p));
+ a.onclick = () => { cockpit.jump("/playground/" + (entry.path || p)) };
+ nav.appendChild(li);
+ }
+
+ id("set-status").onclick = event => {
+ page_status.set_own({ type: id("type").value, title: id("title").value });
+ };
+
+ id("clear-status").onclick = event => {
+ page_status.set_own(null);
+ };
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ cockpit.transport.wait(init);
+});
diff --git a/pkg/playground/journal.html b/pkg/playground/journal.html
new file mode 100644
index 0000000..56c34f7
--- /dev/null
+++ b/pkg/playground/journal.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Journal Box</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="journal.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="journal.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <div id="journal-box"></div>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/pkg/playground/journal.jsx b/pkg/playground/journal.jsx
new file mode 100644
index 0000000..ea2b6ca
--- /dev/null
+++ b/pkg/playground/journal.jsx
@@ -0,0 +1,37 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import { createRoot } from 'react-dom/client';
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import "page.scss";
+
+import { LogsPanel } from "cockpit-components-logs-panel.jsx";
+
+document.addEventListener("DOMContentLoaded", function() {
+ const cur_unit_id = "certmonger.service";
+ const match = [
+ "_SYSTEMD_UNIT=" + cur_unit_id, "+",
+ "COREDUMP_UNIT=" + cur_unit_id, "+",
+ "UNIT=" + cur_unit_id
+ ];
+ const root = createRoot(document.getElementById('journal-box'));
+ root.render(<LogsPanel title="Logs!" match={match} max={10} />);
+});
diff --git a/pkg/playground/manifest.json b/pkg/playground/manifest.json
new file mode 100644
index 0000000..fadfbe9
--- /dev/null
+++ b/pkg/playground/manifest.json
@@ -0,0 +1,48 @@
+{
+ "tools": {
+ "index": {
+ "label": "Development"
+ }
+ },
+
+ "playground": {
+ "react-patterns": {
+ "label": "React Patterns"
+ },
+ "translate": {
+ "label": "Translating"
+ },
+ "exception": {
+ "label": "Exceptions"
+ },
+ "pkgs": {
+ "label": "Packages"
+ },
+ "preloaded": {
+ "label": "Preloaded"
+ },
+ "notifications-receiver": {
+ "label": "Notifications Receiver"
+ },
+ "metrics": {
+ "label": "Monitoring"
+ },
+ "plot": {
+ "label": "Plots"
+ },
+ "service": {
+ "label": "Generic Service Monitor"
+ },
+ "speed": {
+ "label": "Speed Tests"
+ },
+ "test": {
+ "label": "Playground"
+ },
+ "journal": {
+ "label": "Logs Box"
+ }
+ },
+ "preload": [ "preloaded" ],
+ "content-security-policy": "img-src 'self' data:"
+}
diff --git a/pkg/playground/metrics.html b/pkg/playground/metrics.html
new file mode 100644
index 0000000..2554d48
--- /dev/null
+++ b/pkg/playground/metrics.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Monitoring</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="metrics.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="metrics.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>Monitoring</h1>
+ <button id="reload">Reload</button>
+ <div id="results"></div>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/pkg/playground/metrics.js b/pkg/playground/metrics.js
new file mode 100644
index 0000000..24718ef
--- /dev/null
+++ b/pkg/playground/metrics.js
@@ -0,0 +1,20 @@
+import cockpit from "cockpit";
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+
+const metrics = [{ name: "block.device.read" }];
+
+const channel = cockpit.channel({
+ payload: "metrics1",
+ source: "internal",
+ metrics,
+ interval: 1000
+});
+
+channel.addEventListener("close", (event, message) => console.log(message));
+channel.addEventListener("message", (event, message) => console.log(message));
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.body.removeAttribute("hidden");
+ document.getElementById("reload").addEventListener("click", () => cockpit.logout(true));
+});
diff --git a/pkg/playground/notifications-receiver.html b/pkg/playground/notifications-receiver.html
new file mode 100644
index 0000000..e005e7e
--- /dev/null
+++ b/pkg/playground/notifications-receiver.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Notifications Receiver</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="notifications-receiver.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <h1>Notifications Receiver</h1>
+
+ <span id="received-type"></span> / <span id="received-title"></span>
+
+</body>
+
+</html>
diff --git a/pkg/playground/notifications-receiver.js b/pkg/playground/notifications-receiver.js
new file mode 100644
index 0000000..6e90596
--- /dev/null
+++ b/pkg/playground/notifications-receiver.js
@@ -0,0 +1,24 @@
+import { page_status } from "notifications";
+
+function id(sel) {
+ return document.getElementById(sel);
+}
+
+function update() {
+ const status = page_status.get("playground");
+
+ if (status) {
+ id("received-type").innerText = status.type;
+ id("received-title").innerText = status.title;
+ } else if (status !== undefined) {
+ id("received-type").innerText = "-";
+ id("received-title").innerText = "-";
+ }
+}
+
+function init () {
+ page_status.addEventListener("changed", update);
+ update();
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/pkg/playground/pkgs.html b/pkg/playground/pkgs.html
new file mode 100644
index 0000000..b70d2a7
--- /dev/null
+++ b/pkg/playground/pkgs.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Packages</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="pkgs.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <button id="reload">Reload</button>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/pkg/playground/pkgs.js b/pkg/playground/pkgs.js
new file mode 100644
index 0000000..a766b42
--- /dev/null
+++ b/pkg/playground/pkgs.js
@@ -0,0 +1,42 @@
+import cockpit from "cockpit";
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+
+document.addEventListener("DOMContentLoaded", () => {
+ const proxy = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Packages", "/packages");
+
+ let manifests;
+
+ function update(str) {
+ const new_m = JSON.parse(str);
+
+ if (manifests) {
+ for (const p in new_m) {
+ if (!manifests[p])
+ console.log("ADD", p);
+ else if (manifests[p].checksum != new_m[p].checksum)
+ console.log("CHG", p);
+ }
+ for (const p in manifests) {
+ if (!new_m[p])
+ console.log("REM", p);
+ }
+ }
+
+ manifests = new_m;
+ }
+
+ const debug_manifest_changes = false;
+
+ proxy.wait(function () {
+ document.body.removeAttribute("hidden");
+ if (debug_manifest_changes) {
+ update(proxy.Manifests);
+ proxy.addEventListener("changed", () => update(proxy.Manifests));
+ }
+ document.getElementById("reload").addEventListener("click", () => {
+ proxy.Reload()
+ .catch(error => console.log("ERROR", error));
+ });
+ });
+});
diff --git a/pkg/playground/plot.css b/pkg/playground/plot.css
new file mode 100644
index 0000000..87a61eb
--- /dev/null
+++ b/pkg/playground/plot.css
@@ -0,0 +1,3 @@
+.mem-graph {
+ block-size: 180px;
+}
diff --git a/pkg/playground/plot.html b/pkg/playground/plot.html
new file mode 100644
index 0000000..4583551
--- /dev/null
+++ b/pkg/playground/plot.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Plots</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="plot.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="plot.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div id="plot-direct"></div>
+ <div id="plot-pmcd"></div>
+</body>
+</html>
diff --git a/pkg/playground/plot.js b/pkg/playground/plot.js
new file mode 100644
index 0000000..28ed469
--- /dev/null
+++ b/pkg/playground/plot.js
@@ -0,0 +1,38 @@
+import './plot.css';
+
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+
+import { PlotState } from "plot.js";
+import { SvgPlot, bytes_config } from "cockpit-components-plot.jsx";
+
+const direct_metric = {
+ direct: ["mem.util.available"],
+ units: "bytes"
+};
+
+const pmcd_metric = {
+ pmcd: ["mem.util.available"],
+ units: "bytes"
+};
+
+document.addEventListener("DOMContentLoaded", function() {
+ const plot_state = new PlotState();
+ plot_state.plot_single('direct', direct_metric);
+ plot_state.plot_single('pmcd', pmcd_metric);
+
+ // For the tests
+ window.plot_state = plot_state;
+
+ createRoot(document.getElementById('plot-direct')).render(
+ <SvgPlot className="mem-graph"
+ title="Direct" config={bytes_config}
+ plot_state={plot_state} plot_id="direct" />
+ );
+
+ createRoot(document.getElementById('plot-pmcd')).render(
+ <SvgPlot className="mem-graph"
+ title="PMCD" config={bytes_config}
+ plot_state={plot_state} plot_id="pmcd" />
+ );
+});
diff --git a/pkg/playground/preloaded.html b/pkg/playground/preloaded.html
new file mode 100644
index 0000000..8526b65
--- /dev/null
+++ b/pkg/playground/preloaded.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Preloaded Page</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="preloaded.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <h1>Preloaded</h1>
+
+ <div id="path"></div>
+ <pre id="host"></pre>
+ <pre id="release"></pre>
+
+</body>
+
+</html>
diff --git a/pkg/playground/preloaded.js b/pkg/playground/preloaded.js
new file mode 100644
index 0000000..d360096
--- /dev/null
+++ b/pkg/playground/preloaded.js
@@ -0,0 +1,43 @@
+import cockpit from "cockpit";
+
+// This is the basic structure of a preloaded page. It has a two
+// phase initialization: phase 1 while it is still invisible, and
+// phase 2 when it becomes visible.
+//
+// Elements on the page (including the body) are made visible only
+// once the page itself is visible. Otherwise layout might go wrong
+// and not recover automatically.
+
+function init_1() {
+ return (cockpit.spawn(["hostname"])
+ .then(data => {
+ document.getElementById("host").innerText = data.trim();
+ }));
+}
+
+function init_2() {
+ return (cockpit.file("/etc/os-release").read()
+ .then(data => {
+ document.getElementById("release").innerText = data;
+ }));
+}
+
+function navigate() {
+ document.getElementById("path").innerText = cockpit.location.path.join("/");
+ document.body.removeAttribute("hidden");
+}
+
+function maybe_phase_2() {
+ cockpit.removeEventListener("visibilitychange", maybe_phase_2);
+ if (cockpit.hidden) {
+ cockpit.addEventListener("visibilitychange", maybe_phase_2);
+ } else {
+ init_2().then(navigate);
+ }
+}
+
+function init() {
+ init_1().then(maybe_phase_2);
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/pkg/playground/react-demo-cards.jsx b/pkg/playground/react-demo-cards.jsx
new file mode 100644
index 0000000..79ebc36
--- /dev/null
+++ b/pkg/playground/react-demo-cards.jsx
@@ -0,0 +1,68 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import { createRoot } from 'react-dom/client';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardFooter, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { Gallery, GalleryItem } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+
+const CardsDemo = () => {
+ const cards = [
+ <Card isCompact key="card1">
+ <CardBody>I'm a card in a gallery</CardBody>
+ </Card>,
+ <Card isCompact key="card2">
+ <CardBody>I'm a card in a gallery</CardBody>
+ <CardFooter>I have a footer</CardFooter>
+ </Card>,
+ <Card isCompact key="card3">
+ <CardBody>I'm a card in a gallery</CardBody>
+ </Card>,
+ <Card isCompact key="card4">
+ <CardTitle>I have a header too</CardTitle>
+ <CardBody>I'm a card in a gallery</CardBody>
+ </Card>,
+ <Card key="card5">
+ <CardHeader actions={{
+ actions: <><input type="checkbox" />
+ <Button className="btn">click</Button></>,
+ }} />
+ <CardTitle>This is a card header</CardTitle>
+ <CardBody>I'm a card in a gallery</CardBody>
+ </Card>,
+ <GalleryItem key="card6">
+ I'm not a card, but I'm in the gallery too, as a generic GalleryItem.
+ </GalleryItem>,
+ ];
+ return (
+ <>
+ <h4>PF4 cards arranged using a PF4 Gallery</h4>
+ <Gallery hasGutter>
+ { cards }
+ </Gallery>
+ </>
+ );
+};
+
+export function showCardsDemo(rootElement) {
+ const root = createRoot(rootElement);
+ root.render(<CardsDemo />);
+}
diff --git a/pkg/playground/react-demo-dialog.jsx b/pkg/playground/react-demo-dialog.jsx
new file mode 100644
index 0000000..c4fa18f
--- /dev/null
+++ b/pkg/playground/react-demo-dialog.jsx
@@ -0,0 +1,40 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+/* Sample dialog body
+ */
+export class PatternDialogBody extends React.Component {
+ selectChanged(value) {
+ console.log("new value: " + value);
+ }
+
+ render() {
+ return (
+ <Form isHorizontal>
+ <FormGroup fieldId="control-1" label='Label'>
+ <TextInput id="control-1" />
+ </FormGroup>
+ </Form>
+ );
+ }
+}
diff --git a/pkg/playground/react-demo-file-autocomplete.jsx b/pkg/playground/react-demo-file-autocomplete.jsx
new file mode 100644
index 0000000..3c1afa2
--- /dev/null
+++ b/pkg/playground/react-demo-file-autocomplete.jsx
@@ -0,0 +1,33 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import { createRoot } from 'react-dom/client';
+
+import { FileAutoComplete } from "cockpit-components-file-autocomplete.jsx";
+
+export function showFileAcDemo(rootElement) {
+ const root = createRoot(rootElement);
+ root.render(<FileAutoComplete id='file-autocomplete-widget' />);
+}
+
+export function showFileAcDemoPreselected(rootElement) {
+ const root = createRoot(rootElement);
+ root.render(<FileAutoComplete id='file-autocomplete-widget-preselected' value="/home/admin/newdir/file1" />);
+}
diff --git a/pkg/playground/react-patterns.html b/pkg/playground/react-patterns.html
new file mode 100644
index 0000000..6b0ac1e
--- /dev/null
+++ b/pkg/playground/react-patterns.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit React Patterns Usage</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="react-patterns.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="react-patterns.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h3>Select file</h3>
+ <div id="demo-file-ac"></div>
+ <div id="demo-file-ac-preselected"></div>
+ </section>
+
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h3>Dialogs</h3>
+ <button id="demo-show-dialog" class="pf-v5-c-button pf-m-secondary">Show Dialog</button>
+ <button id="demo-show-error-dialog" class="pf-v5-c-button pf-m-secondary">Show Error-Dialog</button>
+ <div id="demo-dialog-result"></div>
+ </section>
+
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h3>Cards</h3>
+ <div id="demo-cards"></div>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/pkg/playground/react-patterns.js b/pkg/playground/react-patterns.js
new file mode 100644
index 0000000..10bbe1d
--- /dev/null
+++ b/pkg/playground/react-patterns.js
@@ -0,0 +1,129 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import "../../node_modules/@patternfly/patternfly/components/Page/page.css";
+
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+
+import { PatternDialogBody } from "./react-demo-dialog.jsx";
+import { showCardsDemo } from "./react-demo-cards.jsx";
+import { showFileAcDemo, showFileAcDemoPreselected } from "./react-demo-file-autocomplete.jsx";
+
+/* -----------------------------------------------------------------------------
+ Modal Dialog
+ -----------------------------------------------------------------------------
+ */
+
+let lastAction = "";
+
+const onDialogStandardClicked = function(mode) {
+ lastAction = mode;
+ const dfd = cockpit.defer();
+ dfd.notify("Starting something long");
+ if (mode == 'steps') {
+ let count = 0;
+ const interval = window.setInterval(function() {
+ count += 1;
+ dfd.notify("Step " + count);
+ }, 500);
+ window.setTimeout(function() {
+ window.clearTimeout(interval);
+ dfd.resolve();
+ }, 5000);
+ dfd.promise.cancel = function() {
+ window.clearTimeout(interval);
+ dfd.notify("Canceling");
+ window.setTimeout(function() {
+ dfd.reject("Action canceled");
+ }, 1000);
+ };
+ } else if (mode == 'reject') {
+ dfd.reject("Some error occurred");
+ } else {
+ dfd.resolve();
+ }
+ return dfd.promise;
+};
+
+const onDialogDone = function(success) {
+ const result = success ? "successful" : "Canceled";
+ const action = success ? lastAction : "no action";
+ document.getElementById("demo-dialog-result").textContent = "Dialog closed: " + result + "(" + action + ")";
+};
+
+const onStandardDemoClicked = (staticError) => {
+ const dialogProps = {
+ title: "This shouldn't be seen",
+ body: React.createElement(PatternDialogBody, { clickNested: onStandardDemoClicked }),
+ static_error: staticError,
+ };
+ // also test modifying properties in subsequent render calls
+ const footerProps = {
+ actions: [
+ {
+ clicked: onDialogStandardClicked.bind(null, 'standard action'),
+ caption: "OK",
+ style: 'primary',
+ },
+ {
+ clicked: onDialogStandardClicked.bind(null, 'dangerous action'),
+ caption: "Danger",
+ style: 'danger',
+ },
+ {
+ clicked: onDialogStandardClicked.bind(null, 'steps'),
+ caption: "Wait",
+ },
+ ],
+ dialog_done: onDialogDone,
+ };
+ const dialogObj = show_modal_dialog(dialogProps, footerProps);
+ // if this failed, exit (trying to create a nested dialog)
+ if (!dialogObj)
+ return;
+ footerProps.actions.push(
+ {
+ clicked: onDialogStandardClicked.bind(null, 'reject'),
+ caption: "Error",
+ style: 'primary',
+ });
+ dialogObj.setFooterProps(footerProps);
+ dialogProps.title = "Example React Dialog";
+ dialogObj.setProps(dialogProps);
+};
+
+document.addEventListener("DOMContentLoaded", function() {
+ document.getElementById('demo-show-dialog').addEventListener("click", onStandardDemoClicked.bind(null, null), false);
+ document.getElementById('demo-show-error-dialog').addEventListener("click", onStandardDemoClicked.bind(null, 'Some static error'), false);
+
+ /* -----------------------------------------------------------------------------
+ Listing Pattern
+ -----------------------------------------------------------------------------
+ */
+ // File autocomplete
+ showFileAcDemo(document.getElementById('demo-file-ac'));
+ showFileAcDemoPreselected(document.getElementById('demo-file-ac-preselected'));
+
+ // Cards
+ showCardsDemo(document.getElementById('demo-cards'));
+});
diff --git a/pkg/playground/service.html b/pkg/playground/service.html
new file mode 100644
index 0000000..a1cf397
--- /dev/null
+++ b/pkg/playground/service.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Generic Service Monitor</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="service.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <table>
+ <tbody>
+ <tr><td>Exists</td><td id="exists"></td></tr>
+ <tr><td>State</td><td id="state"></td></tr>
+ <tr><td>Enabled</td><td id="enabled"></td></tr>
+ </tbody>
+ </table>
+
+ <button id="start">Start</button>
+ <button id="stop">Stop</button>
+ <button id="enable">Enable</button>
+ <button id="disable">Disable</button>
+</body>
+</html>
diff --git a/pkg/playground/service.js b/pkg/playground/service.js
new file mode 100644
index 0000000..4d092dc
--- /dev/null
+++ b/pkg/playground/service.js
@@ -0,0 +1,42 @@
+import cockpit from "cockpit";
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+
+import * as service from "service";
+
+document.addEventListener("DOMContentLoaded", () => {
+ let proxy;
+
+ function navigate() {
+ proxy = service.proxy(cockpit.location.path[0] || "");
+
+ function show() {
+ function s(t) {
+ document.getElementById(t).textContent = JSON.stringify(proxy[t]);
+ }
+ s('exists');
+ s('state');
+ s('enabled');
+ }
+
+ proxy.addEventListener("changed", show);
+ show();
+
+ document.body.removeAttribute("hidden");
+ }
+
+ function b(t) {
+ document.getElementById(t).addEventListener("click", () => {
+ proxy[t]()
+ .catch(error => console.error("action", t, "failed:", JSON.stringify(error)));
+ });
+ }
+
+ b('start');
+ b('stop');
+ b('enable');
+ b('disable');
+
+ cockpit.addEventListener("locationchanged", navigate);
+ navigate();
+});
diff --git a/pkg/playground/speed.css b/pkg/playground/speed.css
new file mode 100644
index 0000000..b24e6d6
--- /dev/null
+++ b/pkg/playground/speed.css
@@ -0,0 +1,5 @@
+#spawn-output {
+ min-inline-size: 70ex;
+ min-block-size: 1rem;
+ max-block-size: 20rem;
+}
diff --git a/pkg/playground/speed.html b/pkg/playground/speed.html
new file mode 100644
index 0000000..2238460
--- /dev/null
+++ b/pkg/playground/speed.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit Speed Tests</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="speed.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="speed.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>Speed Tests</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label">Speed: </label></td>
+ <td><label id="speed"></label></td>
+ </tr>
+ <tr>
+ <td><label class="control-label">Process ID: </label></td>
+ <td><label id="pid"></label></td>
+ </tr>
+ <tr>
+ <td><label class="control-label">Bridge Memory: </label></td>
+ <td><label id="memory"></label></td>
+ </tr>
+ </tbody>
+ </table>
+ <p><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="stop">Stop</button></p>
+
+ <h2>Echo Tests</h2>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label" for="message">Message</label></td>
+ <td><input class="form-control" id="message" value="100000" /></td>
+ </tr>
+ <tr>
+ <td><label class="control-label" for="batch">Batch</label></td>
+ <td><input class="form-control" id="batch" value="1" /></td>
+ </tr>
+ <tr>
+ <td><label class="control-label" for="interval">Interval</label></td>
+ <td><input class="form-control" id="interval" value="100" /></td>
+ </tr>
+ <tr>
+ <td><label class="control-label" for="binary">Binary</label></td>
+ <td><input type="checkbox" id="binary" /></td>
+ </tr>
+ <tr>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="echo-normal">Normal Channel</button></td>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="echo-sideband">Sideband Channel</button></td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>Read Test</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label" for="message">Path</label></td>
+ <td><input class="form-control" id="read-path" value="/dev/sda" /></td>
+ </tr>
+ <tr>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="read-normal">Normal Channel</button></td>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="read-sideband">Sideband Channel</button></td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>Download Test</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label" for="message">Path</label></td>
+ <td><input class="form-control" id="download-path" value="/dev/sda" /></td>
+ </tr>
+ <tr>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="download-external">External Download</button></td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>spawn test</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label" for="spawn">command</label></td>
+ <td><input class="form-control" id="spawn-command" value="seq 1000000" /></td>
+ </tr>
+ <tr>
+ <td><label class="control-label" for="spawn-result">result</label></td>
+ <td><label class="form-control" id="spawn-result"></label></td>
+ </tr>
+ <tr>
+ <td><label class="control-label" for="spawn-output">output block</label></td>
+ <td><pre id="spawn-output"></pre></td>
+ </tr>
+ <tr>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="spawn">spawn command</button></td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ <section class="pf-v5-c-page__main-section pf-m-light">
+ <h1>spawn input test</h1>
+ <table>
+ <tbody>
+ <tr>
+ <td><label class="control-label" for="spawn-input-result">result</label></td>
+ <td><label class="form-control" id="spawn-input-result"></label></td>
+ </tr>
+ <tr>
+ <td><button class="pf-v5-c-button pf-m-secondary pf-m-primary" id="spawn-input">spawn command</button></td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/pkg/playground/speed.js b/pkg/playground/speed.js
new file mode 100644
index 0000000..f5af53d
--- /dev/null
+++ b/pkg/playground/speed.js
@@ -0,0 +1,281 @@
+import cockpit from "cockpit";
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import "../../node_modules/@patternfly/patternfly/components/Button/button.css";
+import "../../node_modules/@patternfly/patternfly/components/Page/page.css";
+import "./speed.css";
+
+let channel = null;
+let websocket = null;
+let timer = null;
+let start = null;
+let total = 0;
+let proc = null;
+let close_problem;
+
+function update() {
+ const element = document.getElementById("speed");
+ if (channel || websocket) {
+ element.textContent = cockpit.format_bytes_per_sec((total * 1000) / (Date.now() - start));
+ console.log(total);
+ } else {
+ element.textContent = "";
+ }
+
+ const memory = document.getElementById("memory");
+ const pid = document.getElementById("pid");
+ if (!proc) {
+ proc = cockpit.script("echo $PPID && cat /proc/$PPID/statm");
+ proc.then(function(data) {
+ const parts = data.split("\n");
+ pid.textContent = parts[0];
+ memory.textContent = parts[1];
+ proc = null;
+ }, function(ex) {
+ memory.textContent = String(ex);
+ proc = null;
+ });
+ }
+}
+
+function echo(ev) {
+ stop();
+
+ const sideband = ev.target.id == "echo-sideband";
+
+ function generate(length, binary) {
+ if (binary)
+ return new window.ArrayBuffer(length);
+ else
+ return (new Array(length)).join("x");
+ }
+
+ const length = parseInt(document.getElementById("message").value, 10);
+ const batch = parseInt(document.getElementById("batch").value, 10);
+ const interval = parseInt(document.getElementById("interval").value, 10);
+
+ if (isNaN(length) || isNaN(interval) || isNaN(batch)) {
+ window.alert("Bad value");
+ return;
+ }
+
+ const binary = document.getElementById.checked;
+ const options = { payload: "echo" };
+ const input = generate(length, binary);
+ start = new Date();
+ total = 0;
+
+ if (sideband) {
+ if (binary)
+ options.binary = "raw";
+
+ websocket = new window.WebSocket(cockpit.transport.uri("channel/" + cockpit.transport.csrf_token) +
+ "?" + window.btoa(JSON.stringify(options)));
+ websocket.binaryType = 'arraybuffer';
+
+ websocket.onopen = function() {
+ for (let i = 0; i < batch; i++)
+ websocket.send(input);
+ timer = window.setInterval(function() {
+ for (let i = 0; i < batch; i++)
+ websocket.send(input);
+ }, interval);
+ };
+
+ websocket.onmessage = function(event) {
+ if (binary)
+ total += event.data.byteLength;
+ else
+ total += event.data.length;
+ };
+
+ websocket.onclose = function(event) {
+ if (websocket)
+ window.alert("channel closed");
+ stop();
+ };
+ } else {
+ if (binary)
+ options.binary = true;
+
+ channel = cockpit.channel(options);
+
+ channel.addEventListener("message", function(event, data) {
+ total += data.length;
+ });
+ channel.addEventListener("close", function(event, options) {
+ if (options.problem)
+ window.alert(options.problem);
+ stop();
+ });
+
+ for (let i = 0; i < batch; i++)
+ channel.send(input);
+
+ timer = window.setInterval(function() {
+ for (let i = 0; i < batch; i++)
+ channel.send(input);
+ }, interval);
+ }
+}
+
+function read(ev) {
+ stop();
+
+ const sideband = ev.target.id == "read-sideband";
+ const path = document.getElementById("read-path");
+
+ const options = {
+ payload: "fsread1",
+ path: path.value,
+ max_read_size: 100 * 1024 * 1024 * 1024,
+ binary: sideband ? "raw" : true,
+ };
+
+ start = Date.now();
+ total = 0;
+
+ if (sideband) {
+ websocket = new window.WebSocket(cockpit.transport.uri("channel/" + cockpit.transport.csrf_token) +
+ "?" + window.btoa(JSON.stringify(options)));
+ websocket.binaryType = 'arraybuffer';
+ websocket.onmessage = function(event) {
+ total += event.data.byteLength;
+ };
+ websocket.onclose = function(event) {
+ if (websocket)
+ window.alert("channel closed");
+ stop();
+ };
+ } else {
+ channel = cockpit.channel(options);
+ channel.addEventListener("message", function(event, data) {
+ total += data.length;
+ });
+ channel.addEventListener("close", function(event, options) {
+ if (options.problem)
+ window.alert(options.problem);
+ stop();
+ });
+ }
+}
+
+function download(ev) {
+ stop();
+
+ const path = document.getElementById("download-path");
+
+ const options = {
+ binary: "raw",
+ max_read_size: 100 * 1024 * 1024 * 1024,
+ external: {
+ "content-disposition": 'attachment; filename="download"',
+ "content-type": "application/octet-stream"
+ }
+ };
+
+ /* Allow use of HTTP URLs */
+ if (path.value.indexOf("http") === 0) {
+ const url = new URL(path.value);
+ options.payload = "http-stream2";
+ options.address = url.hostname;
+ options.port = parseInt(url.port, 10);
+ options.path = url.pathname;
+ options.method = "GET";
+ } else {
+ options.payload = "fsread1";
+ options.path = path.value;
+ }
+
+ console.log("Download", options);
+
+ start = Date.now();
+ total = 0;
+
+ const prefix = (new URL(cockpit.transport.uri("channel/" + cockpit.transport.csrf_token))).pathname;
+ const query = window.btoa(JSON.stringify(options));
+ window.open(prefix + "?" + query);
+}
+
+function spawn() {
+ stop();
+
+ document.getElementById("spawn-result").textContent = "Running...";
+ const command = document.getElementById("spawn-command").value;
+
+ start = Date.now();
+ total = 0;
+ close_problem = "terminated";
+
+ console.log("spawning", command);
+ channel = cockpit.script(command, { err: "message" });
+ channel.stream(data => {
+ console.log("spawn: stream block length", data.length);
+ total += data.length;
+ document.getElementById("spawn-output").textContent = data;
+ });
+ channel.then(() => {
+ console.log("spawn: command finished successfully");
+ stop();
+ document.getElementById("spawn-result").textContent = "success";
+ });
+ channel.catch(ex => {
+ console.log("spawn: command failed", JSON.stringify(ex));
+ stop();
+ document.getElementById("spawn-result").textContent = `failed with exit code ${ex.exit_status}: ${ex.message}`;
+ });
+}
+
+function spawn_input() {
+ stop();
+
+ const el_result = document.getElementById("spawn-input-result");
+ el_result.textContent = "Running...";
+
+ start = Date.now();
+ total = 0;
+ channel = cockpit.spawn(["dd", "of=/tmp/spawninput"], { err: "message" });
+ const chunk = 'a'.repeat(65536);
+ for (let i = 0; i < 1000; i++) {
+ channel.input(chunk, true);
+ total += chunk.length;
+ }
+ channel.input(); // send EOF
+ channel
+ .then(() => {
+ el_result.textContent = "success";
+ })
+ .catch(ex => {
+ el_result.textContent = `failed with exit code ${ex.exit_status}: ${ex.message}`;
+ })
+ .finally(stop);
+}
+
+function stop() {
+ update();
+
+ if (channel)
+ channel.close(close_problem);
+ channel = null;
+ close_problem = undefined;
+ const ws = websocket;
+ websocket = null;
+ if (ws)
+ ws.close();
+
+ window.clearInterval(timer);
+ timer = null;
+}
+
+cockpit.transport.wait(function() {
+ document.getElementById("echo-normal").addEventListener("click", echo);
+ document.getElementById("echo-sideband").addEventListener("click", echo);
+ document.getElementById("read-normal").addEventListener("click", read);
+ document.getElementById("read-sideband").addEventListener("click", read);
+ document.getElementById("download-external").addEventListener("click", download);
+ document.getElementById("spawn").addEventListener("click", spawn);
+ document.getElementById("spawn-input").addEventListener("click", spawn_input);
+ document.getElementById("stop").addEventListener("click", stop);
+ window.setInterval(update, 500);
+ document.body.removeAttribute("hidden");
+});
diff --git a/pkg/playground/test.html b/pkg/playground/test.html
new file mode 100644
index 0000000..478bd8e
--- /dev/null
+++ b/pkg/playground/test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit playground</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="test.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="test.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section id="internal" class="pf-v5-c-page__main-section pf-m-light">
+ <img src="hammer.gif" id="hammer" />
+ <div id="nav"></div>
+ <button class="pf-v5-c-button pf-m-secondary" id="go-down">Go down</button>
+ <br/>
+ <br/>
+ <div class="cockpit-internal-reauthorize">
+ <button class="pf-v5-c-button pf-m-secondary">Privileged Action</button>
+ <span></span>
+ </div>
+ <div class="super-channel">
+ <button class="pf-v5-c-button pf-m-secondary">Superuser</button>
+ <span></span>
+ </div>
+ <div class="lock-channel">
+ <button class="pf-v5-c-button pf-m-secondary">Lock /tmp/playground-test-lock</button>
+ <span></span>
+ </div>
+ <button class="pf-v5-c-button pf-m-secondary" id="modify-file">Increment /tmp/counter atomically</button>
+ <div id="file-content"></div>
+ <div id="file-error"></div>
+ <br/>
+ <div>
+ <button class="pf-v5-c-button pf-m-secondary" id="load-file">Load /tmp/counter</button>
+ <button class="pf-v5-c-button pf-m-secondary" id="save-file">Overwrite /tmp/counter</button>
+ <button class="pf-v5-c-button pf-m-secondary" id="delete-file">Delete /tmp/counter</button>
+ </div>
+ <textarea id="edit-file"></textarea>
+ <br/>
+ Visibility: <label id="hidden"></label>
+ </section>
+ </main>
+ </div>
+</body>
+</html>
diff --git a/pkg/playground/test.js b/pkg/playground/test.js
new file mode 100644
index 0000000..a654d5e
--- /dev/null
+++ b/pkg/playground/test.js
@@ -0,0 +1,130 @@
+import cockpit from "cockpit";
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import "../../node_modules/@patternfly/patternfly/components/Button/button.css";
+import "../../node_modules/@patternfly/patternfly/components/Page/page.css";
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.getElementById("hammer").addEventListener("click", e => e.target.setAttribute("hidden", "hidden"));
+
+ document.querySelector(".cockpit-internal-reauthorize .pf-v5-c-button").addEventListener("click", () => {
+ document.querySelector(".cockpit-internal-reauthorize span").textContent = "checking...";
+ cockpit.script("pkcheck --action-id org.freedesktop.policykit.exec --process $$ -u 2>&1", { superuser: "try" })
+ .stream(data => console.debug(data))
+ .then(() => {
+ document.querySelector(".cockpit-internal-reauthorize span").textContent = "result: authorized";
+ })
+ .catch(() => {
+ document.querySelector(".cockpit-internal-reauthorize span").textContent = "result: access-denied";
+ });
+ });
+
+ document.querySelector(".super-channel .pf-v5-c-button").addEventListener("click", () => {
+ document.querySelector(".super-channel span").textContent = "checking...";
+ cockpit.spawn(["id"], { superuser: true })
+ .then(data => {
+ console.log("done");
+ document.querySelector(".super-channel span").textContent = "result: " + data;
+ })
+ .catch(ex => {
+ console.log("fail");
+ document.querySelector(".super-channel span").textContent = "result: " + ex.problem;
+ });
+ });
+
+ document.querySelector(".lock-channel .pf-v5-c-button").addEventListener("click", () => {
+ document.querySelector(".lock-channel span").textContent = "locking...";
+ cockpit.spawn(["flock", "-o", "/tmp/playground-test-lock", "-c", "echo locked; sleep infinity"],
+ { superuser: "try", err: "message" })
+ .stream(data => {
+ document.querySelector(".lock-channel span").textContent = data;
+ })
+ .catch(ex => {
+ document.querySelector(".lock-channel span").textContent = "failed: " + ex.toString();
+ });
+ });
+
+ function update_nav() {
+ document.getElementById("nav").textContent = '';
+ const path = ["top"].concat(cockpit.location.path);
+ const e_nav = document.getElementById("nav");
+ path.forEach((p, i) => {
+ if (i < path.length - 1) {
+ const e_link = document.createElement("a");
+ e_link.setAttribute("tabindex", "0");
+ e_link.textContent = p;
+ e_link.addEventListener("click", () => cockpit.location.go(path.slice(1, i + 1)));
+ e_nav.append(e_link, " >> ");
+ } else {
+ const e_span = document.createElement("span");
+ e_span.textContent = p;
+ e_nav.appendChild(e_span);
+ }
+ });
+ }
+
+ cockpit.addEventListener('locationchanged', update_nav);
+ update_nav();
+
+ document.getElementById('go-down').addEventListener("click", () => {
+ const len = cockpit.location.path.length;
+ cockpit.location.go(cockpit.location.path.concat(len.toString()), { length: len.toString() });
+ });
+
+ const counter = cockpit.file("/tmp/counter", { syntax: JSON });
+
+ function normalize_counter(obj) {
+ obj = obj || { };
+ obj.counter = obj.counter || 0;
+ return obj;
+ }
+
+ function complain(error) {
+ document.getElementById('file-error').textContent = error.toString();
+ }
+
+ function changed(content, tag, error) {
+ if (error)
+ return complain(error);
+ document.getElementById('file-content').textContent = normalize_counter(content).counter;
+ document.getElementById('file-error').textContent = "";
+ }
+
+ counter.watch(changed);
+
+ document.getElementById('modify-file').addEventListener("click", () => {
+ counter
+ .modify(obj => {
+ obj = normalize_counter(obj);
+ obj.counter += 1;
+ return obj;
+ })
+ .catch(complain);
+ });
+
+ function load_file() {
+ cockpit.file("/tmp/counter").read()
+ .then(content => {
+ document.getElementById('edit-file').value = content;
+ });
+ }
+
+ function save_file() {
+ cockpit.file("/tmp/counter").replace(document.getElementById('edit-file').value);
+ }
+
+ document.getElementById('load-file').addEventListener("click", load_file);
+ document.getElementById('save-file').addEventListener("click", save_file);
+ load_file();
+
+ document.getElementById('delete-file').addEventListener("click", () => cockpit.spawn(["rm", "-f", "/tmp/counter"]));
+
+ document.body.removeAttribute("hidden");
+
+ function show_hidden() {
+ document.getElementById("hidden").textContent = cockpit.hidden ? "hidden" : "visible";
+ }
+
+ cockpit.addEventListener("visibilitychange", show_hidden);
+ show_hidden();
+});
diff --git a/pkg/playground/translate.html b/pkg/playground/translate.html
new file mode 100644
index 0000000..bf2bc64
--- /dev/null
+++ b/pkg/playground/translate.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Cockpit playground</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <script src="../base1/cockpit.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="pf-v5-c-page">
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section id="internal" class="pf-v5-c-page__main-section pf-m-light">
+ <h2>HTML translations</h2>
+
+ <p>For translating HTML use these forms:</p>
+
+ <p>
+ <code>&lt;span translate&gt;Ready&lt;/span&gt;</code>
+ = <span translate="yes" id="translate-html">Ready</span>
+ </p>
+
+ <p>
+ <code>&lt;span translate translate-context="verb"&gt;Ready&lt;/span&gt;</code>
+ = <span translate="yes" translate-context="verb" id="translate-html-context">Ready</span>
+ </p>
+
+ <p>
+ <code>&lt;span translate="yes"&gt;Not ready&lt;/span&gt;</code>
+ = <span translate="yes" id="translate-html-yes">Not ready</span>
+ </p>
+
+ <p>
+ <code>&lt;span translate="title" title="Unavailable"&gt;Cancel&lt;/span&gt;</code>
+ = <span translate="title" title="Unavailable" id="translate-html-title">Cancel</span>
+ </p>
+
+ <p>
+ <code>&lt;span translate="yes title" title="Unavailable"&gt;Cancel&lt;/span&gt;</code>
+ = <span translate="yes title" title="Unavailable" id="translate-html-yes-title">Cancel</span>
+ </p>
+
+ <p>Note that we do <b>not support</b>:</p>
+ <ul>
+ <li>Interpolation of variables.</li>
+ <li>Pluralization</li>
+ <li>The <code>&lt;translate&gt;</code> element</li>
+ </ul>
+
+ <hr />
+
+ <h2>Old Glade style</h2>
+
+ <p>The old Glade style is not recommended:</p>
+
+ <p>
+ <code>&lt;span translate="yes"&gt;Empty&lt;/span&gt;</code>
+ = <span translate="yes" id="translatable-glade">Empty</span>
+ </p>
+
+ <p>
+ <code>&lt;span translate="yes" context="verb"&gt;Empty&lt;/span&gt;</code>
+ = <span translate="yes" context="verb" id="translatable-glade-context">Empty</span>
+ </p>
+
+ <p>Note that we do <b>not support</b>:</p>
+ <ul>
+ <li>Interpolation of variables.</li>
+ <li>Translatable attributes.</li>
+ <li>Pluralization</li>
+ <li>The <code>&lt;translate&gt;</code> element</li>
+ </ul>
+
+ <hr />
+
+ <h2>Code Translations</h2>
+
+ <p>For translating in javascript code, use these forms:</p>
+
+ <p>
+ <code>_("Empty")</code>
+ = <span id="underscore-empty"></span>
+ </p>
+
+ <p>
+ <code>_("verb", "Empty")</code>
+ = <span id="underscore-context-empty"></span>
+ </p>
+
+ <p>
+ <code>C_("verb", "Empty")</code>
+ = <span id="cunderscore-context-empty"></span>
+ </p>
+
+ <p>
+ <code>cockpit.gettext("Control")</code>
+ = <span id="gettext-control"></span>
+ </p>
+
+ <p>
+ <code>cockpit.gettext("key", "Control")</code>
+ = <span id="gettext-context-control"></span>
+ </p>
+
+ <p>
+ <code>cockpit.ngettext("$0 disk is missing", "$0 disks are missing", 1)</code>
+ = <span id="ngettext-disks-1"></span>
+ </p>
+
+ <p>
+ <code>cockpit.ngettext("$0 disk is missing", "$0 disks are missing", 2)</code>
+ = <span id="ngettext-disks-2"></span>
+ </p>
+
+ <p>
+ <code>cockpit.ngettext("disk-non-rotational", "$0 disk is missing", "$0 disks are missing", 1)</code>
+ = <span id="ngettext-context-disks-1"></span>
+ </p>
+
+ <p>
+ <code>cockpit.ngettext("disk-non-rotational", "$0 disk is missing", "$0 disks are missing", 2)</code>
+ = <span id="ngettext-context-disks-2"></span>
+ </p>
+
+ <p>Note that we do <b>not support</b>:</p>
+ <ul>
+ <li>Extraction of single quoted strings.</li>
+ </ul>
+ </section>
+ </main>
+ </div>
+ <script src="translate.js"></script>
+ <!-- Bring in initial translations -->
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+ <!-- Override translations from here -->
+ <script src="po.extra.js"></script>
+</body>
+</html>
diff --git a/pkg/playground/translate.js b/pkg/playground/translate.js
new file mode 100644
index 0000000..e194ffc
--- /dev/null
+++ b/pkg/playground/translate.js
@@ -0,0 +1,39 @@
+import cockpit from "cockpit";
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+
+const _ = cockpit.gettext;
+const C_ = cockpit.gettext;
+
+document.addEventListener("DOMContentLoaded", () => {
+ cockpit.translate();
+
+ let text = _("Empty");
+ document.getElementById("underscore-empty").textContent = text;
+
+ text = _("verb", "Empty");
+ document.getElementById("underscore-context-empty").textContent = text;
+
+ text = C_("verb", "Empty");
+ document.getElementById("cunderscore-context-empty").textContent = text;
+
+ text = cockpit.gettext("Control");
+ document.getElementById("gettext-control").textContent = text;
+
+ text = cockpit.gettext("key", "Control");
+ document.getElementById("gettext-context-control").textContent = text;
+
+ text = cockpit.ngettext("$0 disk is missing", "$0 disks are missing", 1);
+ document.getElementById("ngettext-disks-1").textContent = text;
+
+ text = cockpit.ngettext("$0 disk is missing", "$0 disks are missing", 2);
+ document.getElementById("ngettext-disks-2").textContent = text;
+
+ text = cockpit.ngettext("disk-non-rotational", "$0 disk is missing", "$0 disks are missing", 1);
+ document.getElementById("ngettext-context-disks-1").textContent = text;
+
+ text = cockpit.ngettext("disk-non-rotational", "$0 disk is missing", "$0 disks are missing", 2);
+ document.getElementById("ngettext-context-disks-2").textContent = text;
+
+ cockpit.transport.wait(() => document.body.removeAttribute("hidden"));
+});
diff --git a/pkg/ruff.toml b/pkg/ruff.toml
new file mode 100644
index 0000000..90be695
--- /dev/null
+++ b/pkg/ruff.toml
@@ -0,0 +1,9 @@
+extend = "../pyproject.toml"
+
+[lint]
+ignore = [
+ "E501", # https://github.com/charliermarsh/ruff/issues/3206#issuecomment-1562681390
+
+ "EXE001", # Shebang is present but file is not executable
+ "F821", # Undefined name
+]
diff --git a/pkg/selinux/index.html b/pkg/selinux/index.html
new file mode 100644
index 0000000..a1330aa
--- /dev/null
+++ b/pkg/selinux/index.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2016 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="selinux-page" lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title translate="yes">SELinux troubleshoot</title>
+ <meta name="description" content="" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+ <link rel="stylesheet" href="selinux.css" />
+
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script type="text/javascript" src="selinux.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app"></div>
+</body>
+</html>
diff --git a/pkg/selinux/manifest.json b/pkg/selinux/manifest.json
new file mode 100644
index 0000000..30b5cdf
--- /dev/null
+++ b/pkg/selinux/manifest.json
@@ -0,0 +1,15 @@
+{
+ "conditions": [
+ {"path-exists": "/sys/fs/selinux"}
+ ],
+ "tools": {
+ "index": {
+ "label": "SELinux",
+ "keywords": [
+ {
+ "matches": ["setroubleshoot", "semanage", "avc"]
+ }
+ ]
+ }
+ }
+}
diff --git a/pkg/selinux/selinux-client.js b/pkg/selinux/selinux-client.js
new file mode 100644
index 0000000..88070a9
--- /dev/null
+++ b/pkg/selinux/selinux-client.js
@@ -0,0 +1,236 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+
+/* until we have a good dbus interface to get selinux status updates,
+ * we resort to polling
+ */
+
+// how often to check the status [milliseconds]
+const pollingInterval = 10000;
+
+const statusCommand = "sestatus";
+
+// currentStatus reflects the status of SELinux on the system
+const status = {
+ enabled: undefined,
+ enforcing: false,
+ configEnforcing: false, // configured mode at boot time
+ shell: "",
+ ansible: "",
+ modifications: null,
+ permitted: true,
+ failed: false,
+};
+
+/* initializes the selinux status updating, returns initial status
+ * statusChangedCallback parameters (status, errorMessage)
+ * status with the following properties:
+ * - enabled: undefined (couldn't get info), true (enabled) or false (disabled)
+ * cannot be changed without a reboot
+ * - enforcing: boolean (current selinux mode of the system, false if permissive or selinux disabled)
+ * - configEnforcing: boolean (mode the system is configured to boot in, may differ from current mode)
+ * - shell: Output of `semanage export`
+ * - ansible: Ansible script for setting up local modifications
+ * - modifications: List of all local modifications in selinux policy
+ * - permitted: Set to false if user is not allowed to see local modifications
+ * - failed: Reading of modifications failed in unexpected way
+ * errorMessage: optional, if getting the status failed, here will be additional info
+ *
+ * Since we're screenscraping we need to run this in LC_ALL=C mode
+ */
+export function init(statusChangedCallback) {
+ const refreshInfo = function() {
+ cockpit.spawn(statusCommand, { err: 'message', environ: ["LC_ALL=C"], superuser: "try" }).then(
+ function(output) {
+ /* parse output that looks like this:
+ * SELinux status: enabled
+ * SELinuxfs mount: /sys/fs/selinux
+ * SELinux root directory: /etc/selinux
+ * Loaded policy name: targeted
+ * Current mode: enforcing
+ * Mode from config file: enforcing
+ * Policy MLS status: enabled
+ * Policy deny_unknown status: allowed
+ * Max kernel policy version: 30
+ * We want the lines 'SELinux status', 'Current mode' and 'Mode from config file'
+ */
+
+ const lines = output.split("\n");
+ lines.forEach(function(itm) {
+ const items = itm.trim().split(":");
+ if (items.length !== 2)
+ return;
+ const key = items[0].trim();
+ const value = items[1].trim();
+ if (key == "SELinux status") {
+ status.enabled = (value == "enabled");
+ } else if (key == "Current mode") {
+ status.enforcing = (value == "enforcing");
+ } else if (key == "Mode from config file") {
+ if (value == 'error (Permission denied)') {
+ status.configEnforcing = undefined;
+ } else {
+ status.configEnforcing = (value == "enforcing");
+ }
+ }
+ });
+ if (statusChangedCallback)
+ statusChangedCallback(status, undefined);
+ },
+ function(error) {
+ if (status === undefined)
+ return;
+ if (statusChangedCallback) {
+ status.enabled = undefined;
+ statusChangedCallback(status, error.message);
+ }
+ }
+ );
+ };
+
+ let polling = null;
+
+ function setupPolling() {
+ if (cockpit.hidden) {
+ window.clearInterval(polling);
+ polling = null;
+ } else if (polling === null) {
+ polling = window.setInterval(refreshInfo, pollingInterval);
+ refreshInfo();
+ getModifications(statusChangedCallback);
+ }
+ }
+
+ cockpit.addEventListener("visibilitychange", setupPolling);
+ setupPolling();
+
+ /* The first time */
+ if (polling === null)
+ refreshInfo();
+
+ return status;
+}
+
+function parseBoolean(result, item) {
+ // Example:
+ // authlogin_nsswitch_use_ldap (on , on) Allow authlogin to nsswitch use ldap
+ // Split by first ')', as the name cannot contain ')'
+ if (item) {
+ const match = item.match(/(\S*)\s*\((\S*)\s*,.*\)\s*(.*)/);
+ if (match) {
+ let description = match[3];
+ let enable_val = "--on";
+ if (match[2] !== "on") {
+ enable_val = "--off";
+ description = description.replace("Allow", "Disallow");
+ }
+ // We want to support Ansible Core, the `seboolean:` module is not a builtin
+ const ansible = `
+- name: ${description}
+ command: semanage boolean -m ${enable_val} ${match[1]}
+`;
+ result.push({ description, ansible });
+ }
+ }
+ return result;
+}
+
+export function getModifications(statusChangedCallback) {
+ // List of items we know how to parse
+ const manageditems_callbacks = [["boolean", parseBoolean]];
+ const manageditems = manageditems_callbacks.map(item => item[0]);
+
+ // Building a query to get information from semanage
+ // Use `semanage export` to show shell script (and parse types we yet don't parse explicitly)
+ // Use `semanage <type> --list -C` to get better readable and parsable local changes
+ // Use `echo '~~~~~'` as separator, so we don't need to execute multiple commands
+ let script = "semanage export";
+ manageditems.forEach(item => { script += " && echo '~~~~~' && semanage " + item + " --list -C --noheading" });
+ cockpit.script(script, [], { err: 'message', environ: ["LC_MESSAGES=C"], superuser: "try" })
+ .then(output => {
+ output = output.split("~~~~~");
+ status.shell = output[0];
+ status.modifications = [];
+ status.ansible = "";
+
+ for (let i = 1; i < output.length; i++) {
+ const parsed = output[i].trim().split("\n")
+ .reduce(manageditems_callbacks[i - 1][1], []);
+ parsed.forEach(p => {
+ status.modifications.push(p.description);
+ status.ansible += p.ansible;
+ });
+ }
+
+ const shell_rules = {};
+ // As long as we don't parse all items, we need to get some from general export
+ // Once we can parse all types, this can be dropped
+ status.modifications.push(...(output[0].split("\n").reduce(function (result, mod) {
+ mod = mod.trim();
+ if (mod === "")
+ return result;
+
+ const items = mod.split(" ");
+
+ // Skip enumeration of types, e.g. 'boolean -D'
+ if (items.length === 2 && items[1] == "-D")
+ return result;
+
+ if (manageditems.indexOf(items[0]) < 0) {
+ if (items[0] in shell_rules)
+ shell_rules[items[0]].push(" semanage " + mod);
+ else
+ shell_rules[items[0]] = [" semanage " + mod];
+ result.push(mod);
+ }
+ return result;
+ }, [])));
+
+ // Create shell rule for every ansible group separately
+ Object.keys(shell_rules).forEach(t => {
+ const rules = shell_rules[t].join("\n");
+ status.ansible += `
+- name: Set up ${t} customizations
+ shell: |
+ semanage ${t} -D
+${rules}
+`;
+ });
+
+ statusChangedCallback(status, undefined);
+ })
+ .catch(e => {
+ status.modifications = [];
+ if (e.message.indexOf("ValueError:") >= 0) {
+ status.permitted = false;
+ statusChangedCallback(status, undefined);
+ } else {
+ status.failed = true;
+ statusChangedCallback(status, e.message);
+ }
+ });
+}
+
+// returns a promise of the command used to set enforcing mode
+export function setEnforcing(enforcingMode) {
+ const command = ["setenforce", (enforcingMode ? "1" : "0")];
+ return cockpit.spawn(command, { superuser: true, err: "message" });
+}
diff --git a/pkg/selinux/selinux.js b/pkg/selinux/selinux.js
new file mode 100644
index 0000000..cfbb23e
--- /dev/null
+++ b/pkg/selinux/selinux.js
@@ -0,0 +1,306 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import 'cockpit-dark-theme'; // once per page
+
+import React from "react";
+import { createRoot } from "react-dom/client";
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+
+import * as troubleshootClient from "./setroubleshoot-client.js";
+import * as selinuxClient from "./selinux-client.js";
+import { SETroubleshootPage } from "./setroubleshoot-view.jsx";
+import { superuser } from 'superuser';
+
+import "./setroubleshoot.scss";
+
+const _ = cockpit.gettext;
+
+superuser.reload_page_on_change();
+
+const initStore = function(rootElement) {
+ const dataStore = { };
+ dataStore.domRootElement = rootElement;
+
+ dataStore.entries = [];
+
+ // connected to the dbus api of setroubleshootd
+ dataStore.connected = false;
+
+ // currently trying to connect / used for timer
+ dataStore.connecting = null;
+
+ // did we have a connection error?
+ dataStore.error = null;
+
+ dataStore.client = troubleshootClient.client;
+
+ dataStore.selinuxStatusError = undefined;
+
+ const selinuxStatusChanged = function(status, errorMessage) {
+ dataStore.selinuxStatus = status;
+ if (errorMessage !== undefined)
+ dataStore.selinuxStatusError = errorMessage;
+ dataStore.render();
+ };
+ const selinuxStatusDismissError = function() {
+ dataStore.selinuxStatusError = undefined;
+ dataStore.render();
+ };
+ const selinuxChangeMode = function(_event, newMode) {
+ selinuxClient.setEnforcing(newMode).then(
+ function() {
+ dataStore.selinuxStatus.enforcing = newMode;
+ dataStore.render();
+ },
+ function(error) {
+ dataStore.selinuxStatusError = cockpit.format(_("Error while setting SELinux mode: '$0'"), error.message);
+ dataStore.render();
+ }
+ );
+ };
+ dataStore.selinuxStatus = selinuxClient.init(selinuxStatusChanged);
+
+ // run a fix and update the entries accordingly
+ const runFix = function(alertId, analysisId, fixId, runCommand) {
+ let idx;
+ for (idx = dataStore.entries.length - 1; idx >= 0; --idx) {
+ if (dataStore.entries[idx].key == alertId)
+ break;
+ }
+ if (idx < 0) {
+ console.log("Unable to find alert entry for element requesting fix: " + alertId + " (" + analysisId + ").");
+ return;
+ }
+ dataStore.entries[idx].details.pluginAnalysis[fixId].fix = {
+ plugin: analysisId,
+ running: true,
+ result: null,
+ success: false,
+ };
+ dataStore.render();
+ const promise = runCommand
+ ? cockpit.script(runCommand, { err: "message", superuser: "require" })
+ : dataStore.client.runFix(alertId, analysisId);
+
+ promise
+ .then(output => {
+ dataStore.entries[idx].details.pluginAnalysis[fixId].fix = {
+ plugin: analysisId,
+ running: false,
+ result: output,
+ success: true,
+ };
+ selinuxClient.getModifications(selinuxStatusChanged);
+ dataStore.render();
+ })
+ .catch(error => {
+ dataStore.entries[idx].details.pluginAnalysis[fixId].fix = {
+ plugin: analysisId,
+ running: false,
+ result: error.toString(),
+ success: false,
+ };
+ dataStore.render();
+ });
+ };
+
+ /* Delete an alert via the client
+ * if it goes wrong, show an error
+ * remove the entry if successful
+ * This function will only be called if the backend functionality is actually present
+ */
+ const deleteAlert = function(alertId) {
+ return dataStore.client.capabilities.deleteAlert(alertId)
+ .then(() => {
+ let idx;
+ for (idx = dataStore.entries.length - 1; idx >= 0; --idx) {
+ if (dataStore.entries[idx].key == alertId)
+ break;
+ }
+ if (idx < 0)
+ return;
+ dataStore.entries.splice(idx, 1);
+ dataStore.render();
+ })
+ .catch(error => {
+ dataStore.error = error.toString();
+ dataStore.render();
+ });
+ };
+
+ const dismissError = function() {
+ dataStore.error = null;
+ dataStore.render();
+ };
+
+ const render = function() {
+ if (!dataStore.reactRoot)
+ dataStore.reactRoot = createRoot(rootElement);
+ const enableDeleteAlert = ('capabilities' in dataStore.client && 'deleteAlert' in dataStore.client.capabilities);
+ dataStore.reactRoot.render(React.createElement(SETroubleshootPage, {
+ connected: dataStore.connected,
+ connecting: dataStore.connecting,
+ error: dataStore.error,
+ dismissError,
+ entries: dataStore.entries,
+ runFix,
+ deleteAlert: enableDeleteAlert ? deleteAlert : undefined,
+ selinuxStatus: dataStore.selinuxStatus,
+ selinuxStatusError: dataStore.selinuxStatusError,
+ changeSelinuxMode: selinuxChangeMode,
+ dismissStatusError: selinuxStatusDismissError,
+ }));
+ };
+ dataStore.render = render;
+
+ /* Update an alert entry if it exists, otherwise create one
+ Details: if undefined, we don't have info on them yet,
+ while null means an error occurred while retrieving them
+ The function doesn't trigger a render
+ */
+ const maybeUpdateAlert = function(localId, description, count, details) {
+ // if we already know about this alert, ignore unless the repetition count changed
+ // we start at the back because that's where we push new entries
+ // if we receive an alert multiple times, this is where it will be
+ for (let idx = dataStore.entries.length - 1; idx >= 0; --idx) {
+ if (dataStore.entries[idx].key == localId) {
+ if (description === undefined || count === undefined) {
+ dataStore.entries[idx].details = details;
+ return;
+ }
+ // don't update newer information
+ // this can happen in cases of highly frequent updates
+ if (dataStore.entries[idx].count <= count) {
+ // don't tamper with the status of a fix being run
+ // new alerts might be coming in while a fix is running and we don't want
+ // to lose the progress or result
+
+ // only allow details to be null if the count has increased
+ if ((details !== undefined) || (dataStore.entries[idx].count < count)) {
+ dataStore.entries[idx].details = details;
+ }
+ dataStore.entries[idx].description = description;
+ dataStore.entries[idx].count = count;
+ }
+ return;
+ }
+ }
+ // nothing found, so we create a new entry
+ dataStore.entries.push({ key: localId, description, count, details, fix: null });
+ };
+
+ /* Add a list of messages and triggers getting details for each of them
+ The list is added without details at first (if it's a new entry) to preserve the order
+ */
+ const handleMultipleMessages = function(entries) {
+ const detailPromises = [];
+ for (let idxEntry = 0; idxEntry != entries.length; ++idxEntry) {
+ const entry = entries[idxEntry];
+ maybeUpdateAlert(entry.localId, entry.summary, entry.reportCount, undefined);
+ detailPromises.push(dataStore.getAlertDetails(entry.localId));
+ }
+
+ Promise.all(detailPromises).then(render);
+ };
+
+ dataStore.handleAlert = function(level, localId) {
+ // right now the level is unused, since we can't access it for existing alerts
+
+ // we receive the item details in added delayed fashion, render only once we have the full info
+ dataStore.getAlertDetails(localId);
+ render();
+ };
+
+ const getAlertDetails = function(id) {
+ return dataStore.client.getAlert(id)
+ .then(details => {
+ maybeUpdateAlert(id, details.summary, details.reportCount, details);
+ })
+ .catch(() => {
+ maybeUpdateAlert(id, undefined, undefined, null);
+ });
+ };
+ dataStore.getAlertDetails = getAlertDetails;
+
+ const setDisconnected = function() {
+ dataStore.connected = false;
+ render();
+ };
+
+ const setErrorIfNotConnected = function() {
+ if (dataStore.connecting === null)
+ return;
+ dataStore.error = _("Not connected");
+ render();
+ };
+
+ dataStore.connectionTimeout = 5000;
+
+ function capablitiesChanged(capabilities) {
+ dataStore.capabilities = capabilities;
+ render();
+ }
+
+ // try to connect
+ dataStore.tryConnect = function() {
+ if (dataStore.connecting === null) {
+ dataStore.connecting = window.setTimeout(setErrorIfNotConnected, dataStore.connectionTimeout);
+ render();
+ // initialize our setroubleshootd client
+ dataStore.client.init(capablitiesChanged)
+ .then(capablitiesChanged => {
+ dataStore.connected = true;
+ window.clearTimeout(dataStore.connecting);
+ dataStore.connecting = null;
+ dataStore.error = null; // reset "not connected"
+ render();
+ // now register a callback to get new entries and get all existing ones
+ // the order is important, since we don't want to miss an entry
+ dataStore.client.handleAlert(dataStore.handleAlert);
+ dataStore.client.getAlerts()
+ .then(handleMultipleMessages)
+ .catch(() => {
+ console.error("Unable to get setroubleshootd messages");
+ setDisconnected();
+ });
+ })
+ .catch(() => {
+ dataStore.connected = false;
+ window.clearTimeout(dataStore.connecting);
+ dataStore.connecting = null;
+ render();
+ });
+ }
+ };
+
+ // render once initially
+ render();
+
+ // try to connect immediately
+ dataStore.tryConnect();
+
+ return dataStore;
+};
+
+document.addEventListener("DOMContentLoaded", function() {
+ initStore(document.getElementById('app'));
+});
diff --git a/pkg/selinux/setroubleshoot-client.js b/pkg/selinux/setroubleshoot-client.js
new file mode 100644
index 0000000..5d221f8
--- /dev/null
+++ b/pkg/selinux/setroubleshoot-client.js
@@ -0,0 +1,179 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+const _ = cockpit.gettext;
+
+export const client = {};
+
+const busName = "org.fedoraproject.Setroubleshootd";
+const dbusInterface = "org.fedoraproject.SetroubleshootdIface";
+const dbusPath = "/org/fedoraproject/Setroubleshootd";
+
+const busNameFixit = "org.fedoraproject.SetroubleshootFixit";
+const dbusInterfaceFixit = busNameFixit;
+const dbusPathFixit = "/org/fedoraproject/SetroubleshootFixit/object";
+
+client.init = function(capabilitiesChangedCallback) {
+ client.connected = false;
+ const dbusClientSeTroubleshoot = cockpit.dbus(busName, { superuser: "try" });
+ client.proxy = dbusClientSeTroubleshoot.proxy(dbusInterface, dbusPath);
+
+ client.proxyFixit = cockpit.dbus(busNameFixit, { superuser: "try" }).proxy(dbusInterfaceFixit, dbusPathFixit);
+
+ const connectPromise = new Promise((resolve, reject) => {
+ client.proxy.wait(() => {
+ // HACK setroubleshootd seems to drop connections if we don't start explicitly
+ client.proxy.call("start", [])
+ .then(() => {
+ client.connected = true;
+ resolve();
+ })
+ .catch(ex => reject(new Error(_("Unable to start setroubleshootd"))));
+ });
+ });
+
+ client.alertCallback = null;
+
+ function handleSignal(event, name, args) {
+ if (client.alertCallback && name == "alert") {
+ const level = args[0];
+ const localId = args[1];
+ client.alertCallback(level, localId);
+ }
+ }
+
+ // register to receive calls whenever a new alert becomes available
+ // signature for the alert callback: (level, localId)
+ client.handleAlert = function(callback) {
+ // if we didn't listen to events before, do so now
+ if (!client.alertCallback) {
+ client.proxy.addEventListener("signal", handleSignal);
+ }
+ client.alertCallback = callback;
+ };
+
+ // returns a promise
+ client.getAlerts = function(since) {
+ return new Promise((resolve, reject) => {
+ let call;
+ if (since !== undefined)
+ call = client.proxy.call("get_all_alerts_since", [since]);
+ else
+ call = client.proxy.call("get_all_alerts", []);
+ call
+ .then(result => {
+ resolve(result[0].map(entry => ({
+ localId: entry[0],
+ summary: entry[1],
+ reportCount: entry[2],
+ })));
+ })
+ .catch(ex => reject(ex));
+ });
+ };
+
+ /* Return an alert with summary, audit events, fix suggestions (by id)
+ localId: an alert id
+ summary: a brief description of an alert. E.g.
+ "SELinux is preventing /usr/bin/bash from ioctl access on the unix_stream_socket unix_stream_socket."
+ reportCount: count of reports of this alert
+ auditEvent: an array of audit events (AVC, SYSCALL) connected to the alert
+ pluginAnalysis: an array of plugin analysis structure
+ ifText
+ thenText
+ doText
+ analysisId: plugin id. It can be used in org.fedoraproject.SetroubleshootFixit.run_fix()
+ fixable: True when an alert is fixable by a plugin
+ reportBug: True when an alert should be reported
+ firstSeen: when the alert was seen for the first time, timestamp in ms
+ lastSeen: when the alert was seen for the last time, timestamp in ms
+ level: "green", "yellow" or "red"
+ */
+ client.getAlert = localId => client.proxy.call("get_alert", [localId])
+ .then(result => {
+ const details = {
+ localId: result[0],
+ summary: result[1],
+ reportCount: result[2],
+ auditEvent: result[3],
+ pluginAnalysis: result[4],
+ firstSeen: result[5] / 1000,
+ lastSeen: result[6] / 1000,
+ level: result[7],
+ };
+ // cleanup analysis
+ details.pluginAnalysis = details.pluginAnalysis.map(itm => ({
+ ifText: itm[0],
+ thenText: itm[1],
+ doText: itm[2],
+ analysisId: itm[3],
+ fixable: itm[4],
+ reportBug: itm[5],
+ }));
+ return details;
+ })
+ .catch(ex => {
+ console.warn("Unable to get alert for id", localId, ":", JSON.stringify(ex));
+ return Promise.reject(new Error(`Unable to get alert for id ${localId}: ${ex.toString()}`));
+ });
+
+ /* Run a fix via SetroubleshootFixit
+ The analysisId is given as part of pluginAnalysis entries in alert details
+ */
+ client.runFix = (alertId, analysisId) => client.proxyFixit.call("run_fix", [alertId, analysisId])
+ .then(result => result[0])
+ .catch(ex => {
+ console.warn("Unable to run fix:", JSON.stringify(ex));
+ return Promise.reject(new Error(cockpit.format(_("Unable to run fix: $0"), ex.toString())));
+ });
+
+ /* Delete an alert from the database (will be removed for all users), returns true on success
+ * Only assign this to the client variable if the dbus interface actually supports the operation
+ */
+ const deleteAlert = localId => client.proxy.call("delete_alert", [localId])
+ .then(success => {
+ if (!success)
+ return Promise.reject(new Error(cockpit.format(_("Failed to delete alert: $0"), localId)));
+ })
+ .catch(ex => {
+ console.warn("Unable to delete alert with id", localId, ":", JSON.stringify(ex));
+ return Promise.reject(new Error(cockpit.format(_("Failed to delete alert: $0"), ex.toString())));
+ });
+
+ // earlier versions of the dbus interface don't support alert deletion/dismissal
+ // HACK https://bugzilla.redhat.com/show_bug.cgi?id=1306700
+ // once every client we ship to handles these features, we can remove the capabilities check
+ client.capabilities = { };
+
+ // wait for metadata - if this has the method delete_alert, we can use that
+ dbusClientSeTroubleshoot.addEventListener("meta", function(event, meta) {
+ if (dbusInterface in meta && 'methods' in meta[dbusInterface] && 'delete_alert' in meta[dbusInterface].methods)
+ client.capabilities.deleteAlert = deleteAlert;
+ else
+ delete client.capabilities.deleteAlert;
+
+ if (capabilitiesChangedCallback)
+ capabilitiesChangedCallback(client.capabilities);
+ });
+
+ // connect to dbus and start setroubleshootd
+ return connectPromise;
+};
diff --git a/pkg/selinux/setroubleshoot-view.jsx b/pkg/selinux/setroubleshoot-view.jsx
new file mode 100644
index 0000000..33118ac
--- /dev/null
+++ b/pkg/selinux/setroubleshoot-view.jsx
@@ -0,0 +1,468 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import React from "react";
+import { Alert, AlertActionCloseButton, AlertGroup } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Divider } from "@patternfly/react-core/dist/esm/components/Divider/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
+import { ExclamationCircleIcon, ExclamationTriangleIcon, InfoCircleIcon } from "@patternfly/react-icons";
+
+import { Modifications } from "cockpit-components-modifications.jsx";
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { ListingTable } from "cockpit-components-table.jsx";
+import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
+import * as timeformat from 'timeformat';
+
+const _ = cockpit.gettext;
+
+/* Show details for an alert, including possible solutions
+ * Props correspond to an item in the setroubleshoot dataStore
+ */
+class SELinuxEventDetails extends React.Component {
+ runFix(itmIdx, runCommand) {
+ const localId = this.props.details.localId;
+ const analysisId = this.props.details.pluginAnalysis[itmIdx].analysisId;
+ this.props.runFix(localId, analysisId, itmIdx, runCommand);
+ }
+
+ render() {
+ if (!this.props.details) {
+ // details should be requested by default, so we just need to wait for them
+ if (this.props.details === undefined)
+ return <EmptyStatePanel loading title={ _("Waiting for details...") } />;
+ else
+ return <EmptyStatePanel icon={ExclamationCircleIcon} title={ _("Unable to get alert details.") } />;
+ }
+
+ const self = this;
+ const fixEntries = this.props.details.pluginAnalysis.map(function(itm, itmIdx) {
+ let fixit = null;
+ let fixit_command = null;
+ let msg = null;
+
+ /* some plugins like catchall_sebool don't report fixable as they offer multiple solutions;
+ * we can offer to run a single setsebool command for convenience */
+ let fixable = itm.fixable;
+ if (!fixable && itm.doText && itm.doText.startsWith("setsebool") && itm.doText.indexOf("\n") < 0) {
+ fixable = true;
+ fixit_command = itm.doText;
+ }
+
+ if (fixable) {
+ if ((itm.fix) && (itm.fix.plugin == itm.analysisId)) {
+ if (itm.fix.running) {
+ msg = (
+ <div>
+ <div className="spinner spinner-xs setroubleshoot-progress-spinner" />
+ <span className="setroubleshoot-progress-message"> { _("Applying solution...") }</span>
+ </div>
+ );
+ } else {
+ if (itm.fix.success) {
+ msg = (
+ <Alert isInline variant="success" title={ _("Solution applied successfully") }>
+ {itm.fix.result}
+ </Alert>
+ );
+ } else {
+ msg = (
+ <Alert isInline variant="danger" title={ _("Solution failed") }>
+ {itm.fix.result}
+ </Alert>
+ );
+ }
+ }
+ }
+ if (!itm.fix) {
+ fixit = (
+ <div className="setroubleshoot-listing-action">
+ <Button variant="secondary" onClick={ self.runFix.bind(self, itmIdx, fixit_command) }>
+ { _("Apply this solution") }
+ </Button>
+ </div>
+ );
+ }
+ } else {
+ fixit = (
+ <div className="setroubleshoot-listing-action">
+ <span>{ _("Unable to apply this solution automatically") }</span>
+ </div>
+ );
+ }
+
+ let doElement = "";
+
+ // One line usually means one command
+ if (itm.doText && itm.doText.indexOf("\n") < 0)
+ doElement = <TextArea aria-label={_("solution")} readOnlyVariant="default" defaultValue={itm.doText} />;
+
+ // There can be text with commands. Command always starts on a new line with '#'
+ // Group subsequent commands into one `<TextArea>` element.
+ if (itm.doText && itm.doText.indexOf("\n") >= 0) {
+ const parts = [];
+ const lines = itm.doText.split("\n");
+ let lastCommand = false;
+ lines.forEach(l => {
+ if (l[0] == "#") { // command
+ if (lastCommand) // When appending command remove "# ". Only the first command keeps it and it is removed later on
+ parts[parts.length - 1] += ("\n" + l.substr(2));
+ else
+ parts.push(l);
+ lastCommand = true;
+ } else {
+ parts.push(l);
+ lastCommand = false;
+ }
+ });
+ doElement = parts.map((p, index) => p[0] == "#"
+ ? <TextArea aria-label={_("solution")}
+ readOnlyVariant="plain"
+ key={p}
+ defaultValue={p.substr(2)} />
+ : <span key={p}>{p}</span>);
+ }
+
+ return (
+ <StackItem key={itm.analysisId + (itm.ifText || "") + (itm.doText || "")}>
+ <div className="selinux-details">
+ <div>
+ <div>
+ <span>{itm.ifText}</span>
+ </div>
+ <div>
+ {itm.thenText}
+ </div>
+ <ExpandableSection toggleText={_("solution details")}>
+ {doElement}
+ </ExpandableSection>
+ {msg}
+ </div>
+ {fixit}
+ </div>
+ {itmIdx != self.props.details.pluginAnalysis.length - 1 && <Divider />}
+ </StackItem>
+ );
+ });
+ return <Stack hasGutter>{fixEntries}</Stack>;
+ }
+}
+
+/* Show the audit log events for an alert */
+const SELinuxEventLog = ({ details }) => {
+ if (!details) {
+ // details should be requested by default, so we just need to wait for them
+ if (details === undefined)
+ return <EmptyStatePanel loading title={ _("Waiting for details...") } />;
+ else
+ return <EmptyStatePanel icon={ExclamationCircleIcon} title={ _("Unable to get alert details.") } />;
+ }
+
+ const logEntries = details.auditEvent.map((itm, idx) => {
+ // use the alert id and index in the event log array as the data key for react
+ // if the log becomes dynamic, the entire log line might need to be considered as the key
+ return <div key={ details.localId + "." + idx }>{itm}</div>;
+ });
+ return <div className="setroubleshoot-log">{logEntries}</div>;
+};
+
+/* Component to show a dismissable error, message as child text
+ * dismissError callback function triggered when the close button is pressed
+ */
+class DismissableError extends React.Component {
+ constructor(props) {
+ super(props);
+ this.handleDismissError = this.handleDismissError.bind(this);
+ }
+
+ handleDismissError(e) {
+ // only consider primary mouse button
+ if (!e || e.button !== 0)
+ return;
+ if (this.props.dismissError)
+ this.props.dismissError();
+ e.stopPropagation();
+ }
+
+ render() {
+ return (
+ <Alert isInline
+ variant='danger' title={this.props.children}
+ actionClose={<AlertActionCloseButton onClose={this.handleDismissError} />} />
+ );
+ }
+}
+
+/* Component to show selinux status and offer an option to change it
+ * selinuxStatus status of selinux on the system, properties as defined in selinux-client.js
+ * selinuxStatusError error message from reading or setting selinux status/mode
+ * changeSelinuxMode function to use for changing the selinux enforcing mode
+ * dismissError function to dismiss the error message
+ */
+class SELinuxStatus extends React.Component {
+ render() {
+ const errorMessage = this.props.selinuxStatusError
+ ? <DismissableError dismissError={this.props.dismissError}>{this.props.selinuxStatusError}</DismissableError>
+ : null;
+
+ if (this.props.selinuxStatus.enabled === undefined) {
+ // we don't know the current state
+ return (
+ <div>
+ {errorMessage}
+ <h3>{_("SELinux system status is unknown.")}</h3>
+ </div>
+ );
+ } else if (!this.props.selinuxStatus.enabled) {
+ // selinux is disabled on the system, not much we can do
+ return (
+ <div>
+ {errorMessage}
+ <h3>{_("SELinux is disabled on the system.")}</h3>
+ </div>
+ );
+ }
+ const configUnknown = (this.props.selinuxStatus.configEnforcing === undefined);
+ let note = null;
+ if (configUnknown)
+ note = _("The configured state is unknown, it might change on the next boot.");
+ else if (!configUnknown && this.props.selinuxStatus.enforcing !== this.props.selinuxStatus.configEnforcing)
+ note = _("Setting deviates from the configured state and will revert on the next boot.");
+
+ return (
+ <Stack hasGutter className="selinux-policy-ct">
+ <Flex spaceItems={{ default: 'spaceItemsMd' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <h2>{_("SELinux policy")}</h2>
+ <Switch isChecked={this.props.selinuxStatus.enforcing}
+ label={_("Enforcing")}
+ labelOff={_("Permissive")}
+ onChange={this.props.changeSelinuxMode} />
+ </Flex>
+ { note !== null &&
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <InfoCircleIcon />
+ <FlexItem>{ note }</FlexItem>
+ </Flex>
+ }
+ {errorMessage}
+ </Stack>
+ );
+ }
+}
+
+/* The listing only shows if we have a connection to the dbus API
+ * Otherwise we have blank slate: trying to connect, error
+ * Expected properties:
+ * connected true if the client is connected to setroubleshoot-server via dbus
+ * error error message to show (in EmptyState if not connected, as a dismissable alert otherwise
+ * dismissError callback, triggered for the dismissable error in connected state
+ * deleteAlert callback, triggered with an alert id as parameter to trigger deletion
+ * entries setroubleshoot entries
+ * - runFix function to run fix
+ * - details fix details as provided by the setroubleshoot client
+ * - description brief description of the error
+ * - count how many times (>= 1) this alert occurred
+ * selinuxStatus status of selinux on the system, properties as defined in selinux-client.js
+ * selinuxStatusError error message from reading or setting selinux status/mode
+ * changeSelinuxMode function to use for changing the selinux enforcing mode
+ * dismissStatusError function that is triggered to dismiss the selinux status error
+ */
+export class SETroubleshootPage extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { selected: {} };
+ this.handleDismissError = this.handleDismissError.bind(this);
+ this.onSelect = this.onSelect.bind(this);
+ }
+
+ handleDismissError(e) {
+ // only consider primary mouse button
+ if (!e || e.button !== 0)
+ return;
+ if (this.props.dismissError)
+ this.props.dismissError();
+ e.stopPropagation();
+ }
+
+ onSelect(_, isSelected, rowId) {
+ const selected = Object.assign(this.state.selected);
+ selected[this.props.entries[rowId].key] = isSelected;
+ this.setState({ selected });
+ }
+
+ render() {
+ // if selinux is disabled, we only show EmptyState
+ if (this.props.selinuxStatus.enabled === false) {
+ return <EmptyStatePanel icon={ ExclamationCircleIcon } title={ _("SELinux is disabled on the system") } />;
+ }
+ const self = this;
+ const title = _("SELinux access control errors");
+ const emptyCaption = _("No SELinux alerts.");
+ let emptyState;
+ let entries;
+ if (!this.props.connected) {
+ if (this.props.connecting) {
+ emptyState = <EmptyStatePanel paragraph={ _("Connecting to SETroubleshoot daemon...") } loading />;
+ } else {
+ // if we don't have setroubleshoot-server, be more subtle about saying that
+ emptyState = <EmptyStatePanel icon={ InfoCircleIcon }
+ paragraph={_("Install setroubleshoot-server to troubleshoot SELinux events.")} />;
+ }
+ } else {
+ entries = this.props.entries.map(function(itm, index) {
+ itm.runFix = self.props.runFix;
+ let listingDetail;
+ if (itm.details && 'firstSeen' in itm.details) {
+ if (itm.details.reportCount >= 2) {
+ listingDetail = cockpit.format(_("Occurred between $0 and $1"),
+ timeformat.dateTime(itm.details.firstSeen),
+ timeformat.dateTime(itm.details.lastSeen)
+ );
+ } else {
+ listingDetail = cockpit.format(_("Occurred $0"), timeformat.dateTime(itm.details.firstSeen));
+ }
+ }
+ const tabRenderers = [
+ {
+ name: _("Solutions"),
+ renderer: SELinuxEventDetails,
+ data: itm,
+ },
+ {
+ name: _("Audit log"),
+ renderer: SELinuxEventLog,
+ data: itm,
+ },
+ ];
+ // if the alert has level "red", it's critical
+ const criticalAlert = (itm.details && 'level' in itm.details && itm.details.level == "red")
+ ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle pf-v5-c-icon pf-m-lg" />
+ : null;
+ const columns = [
+ { title: criticalAlert },
+ { title: itm.description }
+ ];
+ if (itm.count > 1) {
+ columns.push({ title: <Badge isRead>{itm.count}</Badge>, props: { className: "pf-v5-c-table__action" } });
+ } else {
+ columns.push({ title: <span />, props: { className: "pf-v5-c-table__action" } });
+ }
+ return ({
+ props: { key: itm.details ? itm.details.localId : index },
+ selected: self.state.selected[itm.details ? itm.details.localId : index],
+ disableSelection: !itm.details,
+ columns,
+ expandedContent: <ListingPanel tabRenderers={tabRenderers}
+ listingDetail={listingDetail} />
+ });
+ });
+ }
+ let selectedCnt = 0;
+ for (const k in this.state.selected) if (this.state.selected[k]) selectedCnt++;
+ const onDeleteClick = () => {
+ for (const k in this.state.selected)
+ if (this.state.selected[k])
+ this.props.deleteAlert(k).then(() => this.setState(prevState => ({ selected: { ...prevState.selected, [k]: false } })));
+ };
+ const actions = (
+ !emptyState
+ ? <Button className="selinux-alert-dismiss"
+ variant="danger"
+ onClick={onDeleteClick}
+ isDisabled={ !this.props.deleteAlert || !selectedCnt}>
+ {selectedCnt ? cockpit.format(cockpit.ngettext("Dismiss $0 alert", "Dismiss $0 alerts", selectedCnt), selectedCnt) : _("Dismiss selected alerts")}
+ </Button>
+ : null
+ );
+ const troubleshooting = (
+ <Card>
+ <CardHeader actions={{ actions }}>
+ <CardTitle component="h2">{title}</CardTitle>
+ </CardHeader>
+ <CardBody className="contains-list">
+ {!emptyState
+ ? <ListingTable aria-label={ title }
+ id="selinux-alerts"
+ onSelect={this.onSelect}
+ gridBreakPoint=''
+ emptyCaption={ emptyCaption }
+ columns={[{ title: _("Alert") }, { title: _("Error message"), header: true }, { title: _("Occurrences") }]}
+ showHeader={false}
+ variant="compact"
+ rows={entries} />
+ : emptyState}
+ </CardBody>
+ </Card>
+ );
+
+ const modifications = (
+ <Modifications
+ title={ _("System modifications") }
+ permitted={ this.props.selinuxStatus.permitted }
+ shell={ "semanage import <<EOF\n" + this.props.selinuxStatus.shell.trim() + "\nEOF" }
+ ansible={ this.props.selinuxStatus.ansible }
+ entries={ this.props.selinuxStatus.modifications }
+ failed={this.props.selinuxStatus.failed ? _("Error running semanage to discover system modifications") : null}
+ />
+ );
+
+ let errorMessage;
+ if (this.props.error) {
+ errorMessage = (
+ <AlertGroup isToast>
+ <Alert
+ isLiveRegion
+ variant='danger' title={this.props.error}
+ actionClose={<AlertActionCloseButton onClose={this.handleDismissError} />} />
+ </AlertGroup>
+ );
+ }
+
+ return (
+ <>
+ {errorMessage}
+ <Page>
+ <PageSection padding={{ default: "padding" }} variant={PageSectionVariants.light}>
+ <SELinuxStatus
+ selinuxStatus={this.props.selinuxStatus}
+ selinuxStatusError={this.props.selinuxStatusError}
+ changeSelinuxMode={this.props.changeSelinuxMode}
+ dismissError={this.props.dismissStatusError}
+ />
+ </PageSection>
+ <PageSection>
+ <Stack hasGutter>
+ <StackItem>{modifications}</StackItem>
+ <StackItem>{troubleshooting}</StackItem>
+ </Stack>
+ </PageSection>
+ </Page>
+ </>
+ );
+ }
+}
diff --git a/pkg/selinux/setroubleshoot.scss b/pkg/selinux/setroubleshoot.scss
new file mode 100644
index 0000000..f4a05e5
--- /dev/null
+++ b/pkg/selinux/setroubleshoot.scss
@@ -0,0 +1,67 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+@use "ct-card";
+@use "page";
+
+#app .pf-v5-c-card {
+ @extend .ct-card;
+}
+
+.setroubleshoot-log {
+ font-family: monospace;
+}
+
+.setroubleshoot-progress-spinner {
+ display: inline-block;
+}
+
+.setroubleshoot-progress-message {
+ vertical-align: top;
+}
+
+/* Table cells should collapse on mobile */
+.ct-table.pf-v5-c-table tbody > tr > * {
+ word-break: break-word;
+}
+
+.selinux-details {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--md);
+
+ > div:first-child {
+ inline-size: 70%;
+ flex: auto;
+ }
+
+ + hr {
+ margin-block-start: var(--pf-v5-global--spacer--sm);
+ }
+
+ .pf-v5-c-expandable-section__content {
+ margin-block-start: unset;
+ }
+}
+
+.selinux-details textarea {
+ margin-block-start: var(--pf-v5-global--spacer--xs);
+ resize: none;
+}
diff --git a/pkg/shell/active-pages-modal.jsx b/pkg/shell/active-pages-modal.jsx
new file mode 100644
index 0000000..205471a
--- /dev/null
+++ b/pkg/shell/active-pages-modal.jsx
@@ -0,0 +1,115 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import React, { useState } from "react";
+import { ListingTable } from "cockpit-components-table.jsx";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Label } from "@patternfly/react-core/dist/esm/components/Label/index.js";
+import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { useDialogs } from "dialogs.jsx";
+import { useInit } from "hooks";
+
+const _ = cockpit.gettext;
+
+export const ActivePagesDialog = ({ frames }) => {
+ function get_pages() {
+ const result = [];
+ for (const address in frames.iframes) {
+ for (const component in frames.iframes[address]) {
+ const iframe = frames.iframes[address][component];
+ result.push({
+ frame: iframe,
+ component,
+ address,
+ name: iframe.getAttribute("name"),
+ active: iframe.getAttribute("data-active") === 'true',
+ selected: iframe.getAttribute("data-active") === 'true',
+ displayName: address === "localhost" ? "/" + component : address + ":/" + component
+ });
+ }
+ }
+
+ // sort the frames by displayName, active ones first
+ result.sort(function(a, b) {
+ return (a.active ? -2 : 0) + (b.active ? 2 : 0) +
+ ((a.displayName < b.displayName) ? -1 : 0) + ((b.displayName < a.displayName) ? 1 : 0);
+ });
+
+ return result;
+ }
+
+ const Dialogs = useDialogs();
+ const init_pages = useInit(get_pages, [frames]);
+ const [pages, setPages] = useState(init_pages);
+
+ function onRemove() {
+ pages.forEach(element => {
+ if (element.selected)
+ frames.remove(element.host, element.component);
+ });
+ Dialogs.close();
+ }
+
+ const rows = pages.map(page => {
+ const columns = [{
+ title: <Split>
+ <SplitItem isFilled>
+ {page.displayName}
+ </SplitItem>
+ <SplitItem>
+ {page.active && <Label color="blue">{_("active")}</Label>}
+ </SplitItem>
+ </Split>,
+ }];
+ return ({
+ props: {
+ key: page.name,
+ 'data-row-id': page.name
+ },
+ columns,
+ selected: page.selected,
+ });
+ });
+
+ return (
+ <Modal isOpen position="top" variant="small"
+ id="active-pages-dialog"
+ onClose={Dialogs.close}
+ title={_("Active pages")}
+ footer={<>
+ <Button variant='primary' onClick={onRemove}>{_("Close selected pages")}</Button>
+ <Button variant='link' onClick={Dialogs.close}>{_("Cancel")}</Button>
+ </>}
+ >
+ <ListingTable showHeader={false}
+ columns={[{ title: _("Page name") }]}
+ aria-label={_("Active pages")}
+ emptyCaption={ _("There are currently no active pages") }
+ onSelect={(_event, isSelected, rowIndex) => {
+ const new_pages = [...pages];
+ new_pages[rowIndex].selected = isSelected;
+ setPages(new_pages);
+ }}
+ rows={rows} />
+ </Modal>
+ );
+};
diff --git a/pkg/shell/base_index.js b/pkg/shell/base_index.js
new file mode 100644
index 0000000..c067a22
--- /dev/null
+++ b/pkg/shell/base_index.js
@@ -0,0 +1,927 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import { createRoot } from "react-dom/client";
+
+import { TimeoutModal } from "./shell-modals.jsx";
+
+const shell_embedded = window.location.pathname.indexOf(".html") !== -1;
+const _ = cockpit.gettext;
+
+function component_checksum(machine, component) {
+ const parts = component.split("/");
+ const pkg = parts[0];
+ if (machine.manifests && machine.manifests[pkg] && machine.manifests[pkg][".checksum"])
+ return "$" + machine.manifests[pkg][".checksum"];
+}
+
+function Frames(index, setupIdleResetTimers) {
+ const self = this;
+ let language = document.cookie.replace(/(?:(?:^|.*;\s*)CockpitLang\s*=\s*([^;]*).*$)|^.*$/, "$1");
+ if (!language)
+ language = navigator.language.toLowerCase(); // Default to Accept-Language header
+
+ /* Lists of frames, by host */
+ self.iframes = { };
+
+ function remove_frame(frame) {
+ frame.remove();
+ }
+
+ self.remove = function remove(machine, component) {
+ let address;
+ if (typeof machine == "string")
+ address = machine;
+ else if (machine)
+ address = machine.address;
+ if (!address)
+ address = "localhost";
+ const list = self.iframes[address] || { };
+ if (!component)
+ delete self.iframes[address];
+ Object.keys(list).forEach(function(key) {
+ if (!component || component == key) {
+ remove_frame(list[key]);
+ delete list[component];
+ }
+ });
+ };
+
+ function frame_ready(frame, count) {
+ let ready = false;
+
+ window.clearTimeout(frame.timer);
+ frame.timer = null;
+
+ try {
+ if (frame.contentWindow.document && frame.contentWindow.document.body)
+ ready = frame.contentWindow.document.body.offsetWidth > 0 && frame.contentWindow.document.body.offsetHeight > 0;
+ } catch (ex) {
+ ready = true;
+ }
+
+ if (!count)
+ count = 0;
+ count += 1;
+ if (count > 50)
+ ready = true;
+
+ if (ready) {
+ if (frame.getAttribute("data-ready") != "1") {
+ frame.setAttribute("data-ready", "1");
+ if (count > 0)
+ index.navigate();
+ }
+ if (frame.contentWindow && setupIdleResetTimers)
+ setupIdleResetTimers(frame.contentWindow);
+
+ if (frame.contentDocument && frame.contentDocument.documentElement) {
+ frame.contentDocument.documentElement.lang = language;
+ if (cockpit.language_direction)
+ frame.contentDocument.documentElement.dir = cockpit.language_direction;
+ }
+ } else {
+ frame.timer = window.setTimeout(function() {
+ frame_ready(frame, count + 1);
+ }, 100);
+ }
+ }
+
+ self.lookup = function lookup(machine, component, hash) {
+ let host;
+ let address;
+ let new_frame = false;
+
+ if (typeof machine == "string") {
+ address = host = machine;
+ } else if (machine) {
+ host = machine.connection_string;
+ address = machine.address;
+ }
+
+ if (!host)
+ host = "localhost";
+ if (!address)
+ address = host;
+
+ let list = self.iframes[address];
+ if (!list)
+ self.iframes[address] = list = { };
+
+ const name = "cockpit1:" + host + "/" + component;
+ let frame = list[component];
+ if (frame && frame.getAttribute("name") != name) {
+ remove_frame(frame);
+ frame = null;
+ }
+
+ /* A preloaded frame */
+ if (!frame) {
+ const wind = window.frames[name];
+ if (wind)
+ frame = wind.frameElement;
+ if (frame) {
+ const src = frame.getAttribute('src');
+ frame.url = src.split("#")[0];
+ list[component] = frame;
+ }
+ }
+
+ /* Need to create a new frame */
+ if (!frame) {
+ new_frame = true;
+ frame = document.createElement("iframe");
+ frame.setAttribute("class", "container-frame");
+ frame.setAttribute("name", name);
+ frame.setAttribute("data-host", host);
+ frame.style.display = "none";
+
+ let base, checksum;
+ if (machine) {
+ if (machine.manifests && machine.manifests[".checksum"])
+ checksum = "$" + machine.manifests[".checksum"];
+ else
+ checksum = machine.checksum;
+ }
+
+ if (checksum && checksum == component_checksum(machine, component)) {
+ if (host === "localhost")
+ base = "..";
+ else
+ base = "../../" + checksum;
+ } else {
+ /* If we don't have any checksums, or if the component specifies a different
+ checksum than the machine, load it via a non-caching @<host> path. This
+ makes sure that we get the right files, and also that we don't poisen the
+ cache with wrong files.
+
+ We can't use a $<component-checksum> path since cockpit-ws only knows how to
+ route the machine checksum.
+
+ TODO - make it possible to use $<component-checksum>.
+ */
+ base = "../../@" + host;
+ }
+
+ frame.url = base + "/" + component;
+ if (component.indexOf("/") === -1)
+ frame.url += "/index";
+ frame.url += ".html";
+ }
+
+ if (!hash)
+ hash = "/";
+ const src = frame.url + "#" + hash;
+ if (frame.getAttribute('src') != src) {
+ if (frame.contentWindow) {
+ // This prevents the browser from creating a new
+ // history entry. It would do that whenever the "src"
+ // of a frame is changed and the window location is
+ // not consistent with the new "src" value.
+ //
+ // This matters when a "jump" command changes both the
+ // the current frame and the hash of the new frame.
+ frame.contentWindow.location.replace(src);
+ }
+ frame.setAttribute('src', src);
+ }
+
+ /* Store frame only when fully setup */
+ if (new_frame) {
+ list[component] = frame;
+ document.getElementById("content").appendChild(frame);
+
+ const style = localStorage.getItem('shell:style') || 'auto';
+ let dark_mode;
+ // If a user set's an explicit theme, ignore system changes.
+ if ((window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && style === "auto") || style === "dark") {
+ dark_mode = true;
+ } else {
+ dark_mode = false;
+ }
+
+ // The new iframe is shown before any HTML/CSS is ready and loaded,
+ // explicitly set a dark background so we don't see any white flashes
+ if (dark_mode && frame.contentDocument && frame.contentDocument.documentElement) {
+ // --pf-v5-global--BackgroundColor--dark-300
+ const dark_mode_background = '#1b1d21';
+ frame.contentDocument.documentElement.style.background = dark_mode_background;
+ } else {
+ frame.contentDocument.documentElement.style.background = 'white';
+ }
+ }
+ frame_ready(frame);
+ return frame;
+ };
+}
+
+function Router(index) {
+ const self = this;
+
+ let unique_id = 0;
+ const origin = cockpit.transport.origin;
+ const source_by_seed = { };
+ const source_by_name = { };
+
+ cockpit.transport.filter(function(message, channel, control) {
+ /* Only control messages with a channel are forwardable */
+ if (control) {
+ if (control.channel !== undefined) {
+ for (const seed in source_by_seed) {
+ const source = source_by_seed[seed];
+ if (!source.window.closed)
+ source.window.postMessage(message, origin);
+ }
+ } else if (control.command == "hint") {
+ /* This is where we handle hint messages directed at
+ * the shell. Right now, there aren't any.
+ */
+ }
+
+ /* Forward message to relevant frame */
+ } else if (channel) {
+ const pos = channel.indexOf('!');
+ if (pos !== -1) {
+ const seed = channel.substring(0, pos + 1);
+ const source = source_by_seed[seed];
+ if (source) {
+ if (!source.window.closed)
+ source.window.postMessage(message, origin);
+ return false; /* Stop delivery */
+ }
+ }
+ }
+
+ /* Still deliver the message locally */
+ return true;
+ }, false);
+
+ function perform_jump(child, control) {
+ const current_frame = index.current_frame();
+ if (child !== window) {
+ if (!current_frame || current_frame.contentWindow != child)
+ return;
+ }
+ let str = control.location || "";
+ if (str[0] != "/")
+ str = "/" + str;
+ if (control.host)
+ str = "/@" + encodeURIComponent(control.host) + str;
+ index.jump(str);
+ }
+
+ function perform_track(child) {
+ const current_frame = index.current_frame();
+ /* Note that we ignore tracknig for old shell code */
+ if (current_frame && current_frame.contentWindow === child &&
+ child.name && child.name.indexOf("/shell/shell") === -1) {
+ let hash = child.location.hash;
+ if (hash.indexOf("#") === 0)
+ hash = hash.substring(1);
+ if (hash === "/")
+ hash = "";
+ /* The browser has already pushed an appropriate entry to
+ the history, so let's just replace it with our custom
+ state object.
+ */
+ const state = Object.assign({}, index.retrieve_state(), { hash });
+ index.navigate(state, true);
+ }
+ }
+
+ function on_unload(ev) {
+ let source;
+ if (ev.target.defaultView)
+ source = source_by_name[ev.target.defaultView.name];
+ else if (ev.view)
+ source = source_by_name[ev.view.name];
+ if (source)
+ unregister(source);
+ }
+
+ function on_hashchange(ev) {
+ const source = source_by_name[ev.target.name];
+ if (source)
+ perform_track(source.window);
+ }
+
+ function on_load(ev) {
+ const source = source_by_name[ev.target.contentWindow.name];
+ if (source)
+ perform_track(source.window);
+ }
+
+ function unregister(source) {
+ const child = source.window;
+ cockpit.kill(null, child.name);
+ const frame = child.frameElement;
+ if (frame)
+ frame.removeEventListener("load", on_load);
+ /* This is often invalid when the window is closed */
+ if (child.removeEventListener) {
+ child.removeEventListener("unload", on_unload);
+ child.removeEventListener("hashchange", on_hashchange);
+ }
+ delete source_by_seed[source.channel_seed];
+ delete source_by_name[source.name];
+ }
+
+ function register(child) {
+ let host, page;
+ const name = child.name || "";
+ if (name.indexOf("cockpit1:") === 0) {
+ const parts = name.substring(9).split("/");
+ host = parts[0];
+ page = parts.slice(1).join("/");
+ }
+ if (!name || !host || !page) {
+ console.warn("invalid child window name", child, name);
+ return;
+ }
+
+ unique_id += 1;
+ const seed = (cockpit.transport.options["channel-seed"] || "undefined:") + unique_id + "!";
+ const source = {
+ name,
+ window: child,
+ channel_seed: seed,
+ default_host: host,
+ page,
+ inited: false,
+ };
+ source_by_seed[seed] = source;
+ source_by_name[name] = source;
+
+ const frame = child.frameElement;
+ frame.addEventListener("load", on_load);
+ child.addEventListener("unload", on_unload);
+ child.addEventListener("hashchange", on_hashchange);
+
+ /*
+ * Setting the "data-loaded" attribute helps the testsuite
+ * know when it can switch into the frame and inject its
+ * own additions.
+ */
+ frame.setAttribute('data-loaded', '1');
+
+ perform_track(child);
+
+ index.navigate();
+ return source;
+ }
+
+ function message_handler(event) {
+ if (event.origin !== origin)
+ return;
+
+ let data = event.data;
+ const child = event.source;
+ if (!child)
+ return;
+
+ /* If it's binary data just send it.
+ * TODO: Once we start restricting what frames can
+ * talk to which hosts, we need to parse control
+ * messages here, and cross check channels */
+ if (data instanceof window.ArrayBuffer) {
+ cockpit.transport.inject(data, true);
+ return;
+ }
+
+ if (typeof data !== "string")
+ return;
+
+ let source, control;
+
+ /*
+ * On Internet Explorer we see Access Denied when non Cockpit
+ * frames send messages (such as Javascript console). This also
+ * happens when the window is closed.
+ */
+ try {
+ source = source_by_name[child.name];
+ } catch (ex) {
+ console.log("received message from child with in accessible name: ", ex);
+ return;
+ }
+
+ /* Closing the transport */
+ if (data.length === 0) {
+ if (source)
+ unregister(source);
+ return;
+ }
+
+ /* A control message */
+ if (data[0] == '\n') {
+ control = JSON.parse(data.substring(1));
+ if (control.command === "init") {
+ if (source)
+ unregister(source);
+ if (control.problem) {
+ console.warn("child frame failed to init: " + control.problem);
+ source = null;
+ } else {
+ source = register(child);
+ }
+ if (source) {
+ const reply = {
+ ...cockpit.transport.options,
+ command: "init",
+ host: source.default_host,
+ "channel-seed": source.channel_seed,
+ };
+ child.postMessage("\n" + JSON.stringify(reply), origin);
+ source.inited = true;
+
+ /* If this new frame is not the current one, tell it */
+ if (child.frameElement != index.current_frame())
+ self.hint(child.frameElement.contentWindow, { hidden: true });
+ }
+ } else if (control.command === "jump") {
+ perform_jump(child, control);
+ return;
+ } else if (control.command === "hint") {
+ if (control.hint == "restart") {
+ /* watchdog handles current host for now */
+ if (control.host != cockpit.transport.host)
+ index.expect_restart(control.host);
+ } else
+ cockpit.hint(control.hint, control);
+ return;
+ } else if (control.command == "oops") {
+ index.show_oops();
+ return;
+ } else if (control.command == "notify") {
+ if (source)
+ index.handle_notifications(source.default_host, source.page, control);
+ return;
+
+ /* Only control messages with a channel are forwardable */
+ } else if (control.channel === undefined && (control.command !== "logout" && control.command !== "kill")) {
+ return;
+
+ /* Add the child's group to all open channel messages */
+ } else if (control.command == "open") {
+ control.group = child.name;
+ data = "\n" + JSON.stringify(control);
+ }
+ }
+
+ if (!source) {
+ console.warn("child frame " + child.name + " sending data without init");
+ return;
+ }
+
+ /* Everything else gets forwarded */
+ cockpit.transport.inject(data, true);
+ }
+
+ self.start = function start(messages) {
+ window.addEventListener("message", message_handler, false);
+ for (let i = 0, len = messages.length; i < len; i++)
+ message_handler(messages[i]);
+ };
+
+ self.hint = function hint(child, data) {
+ const source = source_by_name[child.name];
+ /* This is often invalid when the window is closed */
+ if (source && source.inited && !source.window.closed) {
+ data.command = "hint";
+ const message = "\n" + JSON.stringify(data);
+ source.window.postMessage(message, origin);
+ }
+ };
+}
+
+/*
+ * New instances of Index must be created by new_index_from_proto
+ * and the caller must include a navigation function in the given
+ * prototype. That function will be called by Frames and
+ * Router to actually perform any navigation action.
+ *
+ * Emits "disconnect" and "expect_restart" signals, that should be
+ * handled by the caller.
+ */
+function Index() {
+ const self = this;
+ let current_frame;
+
+ cockpit.event_target(self);
+
+ if (typeof self.navigate !== "function")
+ throw Error("Index requires a prototype with a navigate function");
+
+ /* Session timing out after inactivity */
+ let session_final_timer = null;
+ let session_timeout = 0;
+ let current_idle_time = 0;
+ let final_countdown = 30000; // last 30 seconds
+ let title = "";
+ const standard_login = window.localStorage['standard-login'];
+
+ self.has_oops = false;
+
+ function sessionTimeout() {
+ current_idle_time += 5000;
+ if (!session_final_timer && current_idle_time >= session_timeout - final_countdown) {
+ title = document.title;
+ sessionFinalTimeout();
+ }
+ }
+
+ let session_timeout_dialog_root = null;
+
+ function updateFinalCountdown() {
+ const remaining_secs = Math.floor(final_countdown / 1000);
+ const timeout_text = cockpit.format(_("You will be logged out in $0 seconds."), remaining_secs);
+ document.title = "(" + remaining_secs + ") " + title;
+ if (!session_timeout_dialog_root)
+ session_timeout_dialog_root = createRoot(document.getElementById('session-timeout-dialog'));
+ session_timeout_dialog_root.render(React.createElement(TimeoutModal, {
+ onClose: () => {
+ window.clearTimeout(session_final_timer);
+ session_final_timer = null;
+ document.title = title;
+ resetTimer();
+ session_timeout_dialog_root.unmount();
+ session_timeout_dialog_root = null;
+ final_countdown = 30000;
+ },
+ text: timeout_text,
+ }));
+ }
+
+ function sessionFinalTimeout() {
+ final_countdown -= 1000;
+ if (final_countdown > 0) {
+ updateFinalCountdown();
+ session_final_timer = window.setTimeout(sessionFinalTimeout, 1000);
+ } else {
+ cockpit.logout(true, _("You have been logged out due to inactivity."));
+ }
+ }
+
+ /* Auto-logout idle timer */
+ function resetTimer(ev) {
+ if (!session_final_timer) {
+ current_idle_time = 0;
+ }
+ }
+
+ function setupIdleResetTimers(win) {
+ win.addEventListener("mousemove", resetTimer, false);
+ win.addEventListener("mousedown", resetTimer, false);
+ win.addEventListener("keypress", resetTimer, false);
+ win.addEventListener("touchmove", resetTimer, false);
+ win.addEventListener("scroll", resetTimer, false);
+ }
+
+ cockpit.dbus(null, { bus: "internal" }).call("/config", "cockpit.Config", "GetUInt", ["Session", "IdleTimeout", 0, 240, 0], [])
+ .then(result => {
+ session_timeout = result[0] * 60000;
+ if (session_timeout > 0 && standard_login) {
+ setupIdleResetTimers(window);
+ window.setInterval(sessionTimeout, 5000);
+ }
+ })
+ .catch(e => {
+ if (e.message.indexOf("GetUInt not available") === -1)
+ console.warn(e.message);
+ });
+
+ self.frames = new Frames(self, setupIdleResetTimers);
+ self.router = new Router(self);
+
+ /* Watchdog for disconnect */
+ const watchdog = cockpit.channel({ payload: "null" });
+ watchdog.addEventListener("close", (event, options) => {
+ const watchdog_problem = options.problem || "disconnected";
+ console.warn("transport closed: " + watchdog_problem);
+ self.dispatchEvent("disconnect", watchdog_problem);
+ });
+
+ const old_onerror = window.onerror;
+ window.onerror = function cockpit_error_handler(msg, url, line) {
+ // Errors with url == "" are not logged apparently, so let's
+ // not show the "Oops" for them either.
+ if (url != "")
+ self.show_oops();
+ if (old_onerror)
+ return old_onerror(msg, url, line);
+ return false;
+ };
+
+ /*
+ * Navigation is driven by state objects, which are used with pushState()
+ * and friends. The state is the canonical navigation location, and not
+ * the URL. Only when no state has been pushed or we are arriving from
+ * a link, do we parse the state from the URL.
+ *
+ * Each state object has:
+ * host: a machine host
+ * component: the stripped component to load
+ * hash: the hash to pass to the component
+ * sidebar: set to true to hint that we want a component with a sidebar
+ *
+ * If state.sidebar is set, and no component has yet been chosen for the
+ * given state, then we try to find one that would show a sidebar.
+ */
+
+ /* Encode navigate state into a string
+ * If with_root is true the configured
+ * url root will be added to the generated
+ * url. with_root should be used when
+ * navigating to a new url or updating
+ * history, but is not needed when simply
+ * generating a string for a link.
+ */
+ function encode(state, sidebar, with_root) {
+ const path = [];
+ if (state.host && (sidebar || state.host !== "localhost"))
+ path.push("@" + state.host);
+ if (state.component)
+ path.push.apply(path, state.component.split("/"));
+ let string = cockpit.location.encode(path, null, with_root);
+ if (state.hash && state.hash !== "/")
+ string += "#" + state.hash;
+ return string;
+ }
+
+ /* Decodes navigate state from a string */
+ function decode(string) {
+ const state = { version: "v1", hash: "" };
+ const pos = string.indexOf("#");
+ if (pos !== -1) {
+ state.hash = string.substring(pos + 1);
+ string = string.substring(0, pos);
+ }
+ if (string[0] != '/')
+ string = "/" + string;
+ const path = cockpit.location.decode(string);
+ if (path[0] && path[0][0] == "@") {
+ state.host = path.shift().substring(1);
+ state.sidebar = true;
+ } else {
+ state.host = "localhost";
+ }
+ if (path.length && path[path.length - 1] == "index")
+ path.pop();
+ state.component = path.join("/");
+ return state;
+ }
+
+ self.retrieve_state = function() {
+ let state = window.history.state;
+ if (!state || state.version !== "v1") {
+ if (shell_embedded)
+ state = decode("/" + window.location.hash);
+ else
+ state = decode(window.location.pathname + window.location.hash);
+ }
+ return state;
+ };
+
+ function lookup_component_hash(address, component) {
+ if (!address)
+ address = "localhost";
+
+ const list = self.frames.iframes[address];
+ const iframe = list ? list[component] : undefined;
+
+ if (iframe) {
+ const src = iframe.getAttribute('src');
+ if (src)
+ return src.split("#")[1];
+ }
+
+ return null;
+ }
+
+ self.preload_frames = function (host, manifests) {
+ for (const c in manifests) {
+ const preload = manifests[c].preload;
+ if (preload && preload.length) {
+ for (const p of preload) {
+ if (p == "index")
+ self.frames.lookup(host, c);
+ else
+ self.frames.lookup(host, c + "/" + p);
+ }
+ }
+ }
+ };
+
+ /* Jumps to a given navigate state */
+ self.jump = function (state, replace) {
+ if (typeof (state) === "string")
+ state = decode(state);
+
+ const current = self.retrieve_state();
+
+ /* Make sure we have the data we need */
+ if (!state.host)
+ state.host = current.host || "localhost";
+
+ // When switching hosts, check if we left from some page
+ if (!state.component && state.host !== current.host) {
+ const host_frames = self.frames.iframes[state.host] || {};
+ const active = Object.keys(host_frames)
+ .filter(k => host_frames[k].getAttribute('data-active') === 'true');
+ if (active.length > 0)
+ state.component = active[0];
+ }
+
+ if (!("component" in state))
+ state.component = current.component || "";
+
+ const history = window.history;
+ const frame_change = (state.host !== current.host ||
+ state.component !== current.component);
+
+ if (frame_change && !state.hash)
+ state.hash = lookup_component_hash(state.host, state.component);
+
+ const target = shell_embedded ? window.location : encode(state, null, true);
+
+ if (replace) {
+ history.replaceState(state, "", target);
+ return false;
+ }
+
+ if (frame_change || state.hash !== current.hash) {
+ history.pushState(state, "", target);
+ document.getElementById("nav-system").classList.remove("interact");
+ self.navigate(state, true);
+ return true;
+ }
+
+ return false;
+ };
+
+ /* Build an href for use in an <a> */
+ self.href = function (state, sidebar) {
+ return encode(state, sidebar);
+ };
+
+ self.show_oops = function () {
+ self.has_oops = true;
+ self.dispatchEvent("update");
+ };
+
+ self.current_frame = function (frame) {
+ if (frame !== undefined) {
+ if (current_frame !== frame) {
+ if (current_frame && current_frame.contentWindow)
+ self.router.hint(current_frame.contentWindow, { hidden: true });
+ if (frame && frame.contentWindow)
+ self.router.hint(frame.contentWindow, { hidden: false });
+ }
+ current_frame = frame;
+ }
+ return current_frame;
+ };
+
+ self.start = function() {
+ /* window.messages is initialized in shell/indexes.jsx */
+ const messages = window.messages;
+ if (messages)
+ messages.cancel();
+ self.router.start(messages || []);
+ };
+
+ self.ready = function () {
+ window.addEventListener("popstate", ev => {
+ self.navigate(ev.state, true);
+ });
+
+ self.navigate(null, true);
+ cockpit.translate();
+ document.body.removeAttribute("hidden");
+ };
+
+ self.expect_restart = function (host) {
+ self.dispatchEvent("expect_restart", host);
+ };
+}
+
+function CompiledComponents() {
+ const self = this;
+ self.items = {};
+
+ self.load = function(manifests, section) {
+ Object.entries(manifests || { }).forEach(([name, manifest]) => {
+ Object.entries(manifest[section] || { }).forEach(([prop, info]) => {
+ const item = {
+ section,
+ label: cockpit.gettext(info.label) || prop,
+ order: info.order === undefined ? 1000 : info.order,
+ docs: info.docs,
+ keywords: info.keywords || [{ matches: [] }],
+ keyword: { score: -1 }
+ };
+
+ // Always first keyword should be page name
+ const page_name = item.label.toLowerCase();
+ if (item.keywords[0].matches.indexOf(page_name) < 0)
+ item.keywords[0].matches.unshift(page_name);
+
+ // Keywords from manifest have different defaults than are usual
+ item.keywords.forEach(i => {
+ i.weight = i.weight || 3;
+ i.translate = i.translate === undefined ? true : i.translate;
+ });
+
+ if (info.path)
+ item.path = info.path.replace(/\.html$/, "");
+ else
+ item.path = name + "/" + prop;
+
+ /* Split out any hash in the path */
+ const pos = item.path.indexOf("#");
+ if (pos !== -1) {
+ item.hash = item.path.substr(pos + 1);
+ item.path = item.path.substr(0, pos);
+ }
+
+ /* Fix component for compatibility and normalize it */
+ if (item.path.indexOf("/") === -1)
+ item.path = name + "/" + item.path;
+ if (item.path.slice(-6) == "/index")
+ item.path = item.path.slice(0, -6);
+ self.items[item.path] = item;
+ });
+ });
+ };
+
+ self.ordered = function(section) {
+ const list = [];
+ for (const x in self.items) {
+ if (!section || self.items[x].section === section)
+ list.push(self.items[x]);
+ }
+ list.sort(function(a, b) {
+ let ret = a.order - b.order;
+ if (ret === 0)
+ ret = a.label.localeCompare(b.label);
+ return ret;
+ });
+ return list;
+ };
+
+ self.search = function(prop, value) {
+ for (const x in self.items) {
+ if (self.items[x][prop] === value)
+ return self.items[x];
+ }
+ };
+}
+
+function follow(arg) {
+ /* A promise of some sort */
+ if (arguments.length == 1 && typeof arg.then == "function") {
+ arg.then(function() { console.log.apply(console, arguments) },
+ function() { console.error.apply(console, arguments) });
+ if (typeof arg.stream == "function")
+ arg.stream(function() { console.log.apply(console, arguments) });
+ }
+}
+
+let zz_value;
+
+/* For debugging utility in the index window */
+Object.defineProperties(window, {
+ cockpit: { value: cockpit },
+ zz: {
+ get: function() { return zz_value },
+ set: function(val) { zz_value = val; follow(val) }
+ }
+});
+
+export function new_index_from_proto(proto) {
+ const o = new Object(proto); // eslint-disable-line no-new-object
+ Index.call(o);
+ return o;
+}
+
+export function new_compiled() {
+ return new CompiledComponents();
+}
diff --git a/pkg/shell/credentials.jsx b/pkg/shell/credentials.jsx
new file mode 100644
index 0000000..9bae3b4
--- /dev/null
+++ b/pkg/shell/credentials.jsx
@@ -0,0 +1,324 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React, { useState } from "react";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { ClipboardCopy, ClipboardCopyVariant } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js";
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { ActionGroup, Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { InfoCircleIcon } from '@patternfly/react-icons';
+
+import * as credentials from "credentials";
+import { FileAutoComplete } from "cockpit-components-file-autocomplete.jsx";
+import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
+import { ListingTable } from 'cockpit-components-table.jsx';
+import { ModalError } from 'cockpit-components-inline-notification.jsx';
+import { useEvent, useObject } from 'hooks';
+import { useDialogs } from "dialogs.jsx";
+
+import "./credentials.scss";
+
+const _ = cockpit.gettext;
+
+export const CredentialsModal = () => {
+ const Dialogs = useDialogs();
+ const keys = useObject(() => credentials.keys_instance(), null, []);
+ const [addNewKey, setAddNewKey] = useState(false);
+ const [dialogError, setDialogError] = useState();
+ const [unlockKey, setUnlockKey] = useState();
+
+ useEvent(keys, "changed");
+
+ if (!keys)
+ return null;
+
+ function onToggleKey(id, enable) {
+ const key = keys.items[id];
+
+ if (!key || !key.name)
+ return;
+
+ /* Key needs to be loaded, show load UI */
+ if (enable && !key.loaded) {
+ setUnlockKey(key.name);
+ /* Key needs to be unloaded, do that directly */
+ } else if (!enable && key.loaded) {
+ keys.unload(key).catch(ex => setDialogError(ex.message));
+ }
+ }
+
+ return (
+ <>
+ <Modal isOpen position="top" variant="medium"
+ onClose={Dialogs.close}
+ title={_("SSH keys")}
+ id="credentials-modal"
+ footer={<Button variant='secondary' onClick={Dialogs.close}>{_("Close")}</Button>}
+ >
+ <Stack hasGutter>
+ {dialogError && <ModalError dialogError={dialogError} />}
+ <Flex justifyContent={{ default: 'justifyContentSpaceBetween' }}>
+ <FlexItem>{_("Use the following keys to authenticate against other systems")}</FlexItem>
+ <Button variant='secondary'
+ id="ssh-file-add-custom"
+ onClick={() => setAddNewKey(true)}>
+ {_("Add key")}
+ </Button>
+ </Flex>
+ {addNewKey && <AddNewKey load={keys.load} unlockKey={setUnlockKey} onClose={() => setAddNewKey(false)} />}
+ <ListingTable
+ aria-label={ _("SSH keys") }
+ gridBreakPoint=''
+ id="credential-keys"
+ showHeader={false}
+ variant="compact"
+ columns={ [
+ { title: _("Name"), header: true },
+ { title: _("Toggle") },
+ ] }
+ rows={ Object.keys(keys.items).map((currentKeyId, index) => {
+ const currentKey = keys.items[currentKeyId] || { name: 'test' };
+ const tabRenderers = [
+ {
+ data: { currentKey },
+ name: _("Details"),
+ renderer: KeyDetails,
+ },
+ {
+ data: { currentKey },
+ name: _("Public key"),
+ renderer: PublicKey,
+ },
+ {
+ data: { currentKey, change: keys.change, setDialogError },
+ name: _("Password"),
+ renderer: KeyPassword,
+ },
+ ];
+ const expandedContent = (
+ <ListingPanel tabRenderers={tabRenderers} />
+ );
+
+ return ({
+ columns: [
+ {
+ title: currentKey.name || currentKey.comment,
+ },
+ {
+ title: <Switch aria-label={_("Use key")}
+ isChecked={!!currentKey.loaded}
+ key={"switch-" + index}
+ onChange={(_event, value) => onToggleKey(currentKeyId, value)} />,
+ }
+ ],
+ expandedContent,
+ props: { key: currentKey.fingerprint, 'data-name': currentKey.name || currentKey.comment, 'data-loaded': !!currentKey.loaded },
+ });
+ })} />
+ </Stack>
+ </Modal>
+ {unlockKey && <UnlockKey keyName={unlockKey} load={keys.load} onClose={() => { setUnlockKey(undefined); setAddNewKey(false) }} />}
+ </>
+ );
+};
+
+const AddNewKey = ({ load, unlockKey, onClose }) => {
+ const [addNewKeyLoading, setAddNewKeyLoading] = useState(false);
+ const [newKeyPath, setNewKeyPath] = useState("");
+ const [newKeyPathError, setNewKeyPathError] = useState();
+
+ const addCustomKey = () => {
+ setAddNewKeyLoading(true);
+ load(newKeyPath)
+ .then(onClose)
+ .catch(ex => {
+ if (!ex.sent_password)
+ setNewKeyPathError(ex.message);
+ else
+ unlockKey(newKeyPath);
+ })
+ .finally(() => setAddNewKeyLoading(false));
+ };
+
+ return (
+ <Grid hasGutter>
+ <GridItem span={9} id="ssh-file-add-key">
+ <FileAutoComplete onChange={setNewKeyPath}
+ placeholder={_("Path to file")}
+ superuser="try" />
+ {newKeyPathError && <HelperText className="pf-v5-c-form__helper-text">
+ <HelperTextItem variant="error">{newKeyPathError}</HelperTextItem>
+ </HelperText>}
+ </GridItem>
+ <GridItem span={1}>
+ <Button id="ssh-file-add"
+ isDisabled={addNewKeyLoading || !newKeyPath}
+ isLoading={addNewKeyLoading}
+ onClick={addCustomKey}
+ variant="secondary">
+ {_("Add")}
+ </Button>
+ </GridItem>
+ </Grid>
+ );
+};
+
+const KeyDetails = ({ currentKey }) => {
+ return (
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Comment")}</DescriptionListTerm>
+ <DescriptionListDescription>{currentKey.comment}</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Type")}</DescriptionListTerm>
+ <DescriptionListDescription>{currentKey.type}</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Fingerprint")}</DescriptionListTerm>
+ <DescriptionListDescription>{currentKey.fingerprint}</DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ );
+};
+
+const PublicKey = ({ currentKey }) => {
+ return (
+ <ClipboardCopy isReadOnly hoverTip={_("Copy")} clickTip={_("Copied")} variant={ClipboardCopyVariant.expansion}>
+ {currentKey.data.trim()}
+ </ClipboardCopy>
+ );
+};
+
+const KeyPassword = ({ currentKey, change, setDialogError }) => {
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [inProgress, setInProgress] = useState(undefined);
+ const [newPassword, setNewPassword] = useState('');
+ const [oldPassword, setOldPassword] = useState('');
+
+ function changePassword() {
+ if (!currentKey || !currentKey.name)
+ return;
+
+ setInProgress(true);
+ setDialogError();
+
+ if (oldPassword === undefined || newPassword === undefined || confirmPassword === undefined)
+ setDialogError("Invalid password fields");
+
+ change(currentKey.name, oldPassword, newPassword, confirmPassword)
+ .then(() => {
+ setOldPassword('');
+ setNewPassword('');
+ setConfirmPassword('');
+ setInProgress(false);
+ })
+ .catch(ex => {
+ setDialogError(ex.message);
+ setInProgress(undefined);
+ });
+ }
+
+ const changePasswordBtn = (
+ <Button variant="primary"
+ id={(currentKey.name || currentKey.comment) + "-change-password"}
+ isDisabled={inProgress}
+ isLoading={inProgress}
+ onClick={() => changePassword()}>{_("Change password")}</Button>
+ );
+
+ return (
+ <Form onSubmit={e => { e.preventDefault(); return false }} isHorizontal>
+ {inProgress === false && <HelperText>
+ <HelperTextItem variant="success" hasIcon>
+ {_("Password changed successfully")}
+ </HelperTextItem>
+ </HelperText>}
+ <FormGroup label={_("Password")} fieldId={(currentKey.name || currentKey.comment) + "-old-password"} type="password">
+ <TextInput type="password" id={(currentKey.name || currentKey.comment) + "-old-password"} value={oldPassword} onChange={(_event, value) => setOldPassword(value)} />
+ </FormGroup>
+ <FormGroup label={_("New password")} fieldId={(currentKey.name || currentKey.comment) + "-new-password"} type="password">
+ <TextInput type="password" id={(currentKey.name || currentKey.comment) + "-new-password"} value={newPassword} onChange={(_event, value) => setNewPassword(value)} />
+ </FormGroup>
+ <FormGroup label={_("Confirm password")} fieldId={(currentKey.name || currentKey.comment) + "-confirm-password"} type="password">
+ <TextInput type="password" id={(currentKey.name || currentKey.comment) + "-confirm-password"} value={confirmPassword} onChange={(_event, value) => setConfirmPassword(value)} />
+ </FormGroup>
+ <ActionGroup>
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <Popover
+ aria-label={_("Password tip")}
+ bodyContent={_("Tip: Make your key password match your login password to automatically authenticate against other systems.")}
+ >
+ <button className="pf-v5-c-form__group-label-help ct-icon-info-circle"
+ onClick={e => e.preventDefault()}
+ type="button">
+ <InfoCircleIcon />
+ </button>
+ </Popover>
+ {changePasswordBtn}
+ </Flex>
+ </ActionGroup>
+ </Form>
+ );
+};
+
+const UnlockKey = ({ keyName, load, onClose }) => {
+ const [password, setPassword] = useState();
+ const [dialogError, setDialogError] = useState();
+
+ function load_key() {
+ if (!keyName)
+ return;
+
+ load(keyName, password)
+ .then(onClose)
+ .catch(ex => {
+ setDialogError(ex.message);
+ console.warn("loading key failed: ", ex.message);
+ });
+ }
+
+ return (
+ <Modal isOpen position="top" variant="small"
+ onClose={onClose}
+ title={cockpit.format(_("Unlock key $0"), keyName)}
+ footer={
+ <>
+ <Button variant="primary" id={keyName + "-unlock"} isDisabled={!keyName} onClick={load_key}>{_("Unlock")}</Button>
+ <Button variant='link' onClick={onClose}>{_("Cancel")}</Button>
+ </>
+ }>
+ <Form onSubmit={e => { e.preventDefault(); return false }} isHorizontal>
+ {dialogError && <ModalError dialogError={dialogError} />}
+ <FormGroup label={_("Password")} fieldId={keyName + "-password"} type="password">
+ <TextInput type="password" id={keyName + "-password"} value={password} onChange={(_event, value) => setPassword(value)} />
+ </FormGroup>
+ </Form>
+ </Modal>
+ );
+};
diff --git a/pkg/shell/credentials.scss b/pkg/shell/credentials.scss
new file mode 100644
index 0000000..e4a84c2
--- /dev/null
+++ b/pkg/shell/credentials.scss
@@ -0,0 +1,22 @@
+#credentials-modal {
+ // SSH keys don't have spaces, allow these two wrap
+ .pf-v5-c-clipboard-copy__expandable-content {
+ word-wrap: anywhere;
+ }
+
+ .pf-v5-c-table__toggle + th {
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+ }
+
+ td:last-child {
+ inline-size: 1%;
+ }
+
+ // Remove excess padding from compact tables toggles
+ // And adjust the padding to left align the toggles with the card header
+ .pf-v5-c-table {
+ .pf-v5-c-table__toggle {
+ padding-inline-start: 0;
+ }
+ }
+}
diff --git a/pkg/shell/failures.jsx b/pkg/shell/failures.jsx
new file mode 100644
index 0000000..44d8a78
--- /dev/null
+++ b/pkg/shell/failures.jsx
@@ -0,0 +1,81 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { ClipboardCopy } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js";
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { ExclamationCircleIcon } from "@patternfly/react-icons";
+
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+
+const _ = cockpit.gettext;
+
+export const EarlyFailure = ({ ca_cert_url }) => {
+ return (
+ <Page>
+ <PageSection variant={PageSectionVariants.light}>
+ <EmptyStatePanel icon={ExclamationCircleIcon}
+ title={ _("Connection failed") }
+ paragraph={
+ <>
+ <Stack hasGutter>
+ <div>{_("There was an unexpected error while connecting to the machine.")}</div>
+ <div>{_("Messages related to the failure might be found in the journal:")}</div>
+ <ClipboardCopy isReadOnly hoverTip={_("Copy")} clickTip={_("Copied")}>journalctl -u cockpit</ClipboardCopy>
+ {ca_cert_url && <div id="safari-cert-help">
+ <div>{_("Safari users need to import and trust the certificate of the self-signing CA:")}</div>
+ <Button variant="link" component="a" id="safari-cert" href={ca_cert_url} download>ca.cer</Button>
+ </div>}
+ </Stack>
+ </>
+ } />
+ </PageSection>
+ </Page>
+ );
+};
+
+export const EarlyFailureReady = ({ loading, title, paragraph, reconnect, troubleshoot, onTroubleshoot, watchdog_problem, navigate }) => {
+ const onReconnect = () => {
+ if (watchdog_problem) {
+ cockpit.sessionStorage.clear();
+ window.location.reload(true);
+ } else {
+ navigate(null, true);
+ }
+ };
+
+ return (
+ <Page>
+ <PageSection variant={PageSectionVariants.light}>
+ <EmptyStatePanel icon={!loading ? ExclamationCircleIcon : undefined}
+ loading={loading}
+ title={title}
+ action={<>
+ {reconnect && <Button id="machine-reconnect" onClick={onReconnect}>{_("Reconnect")}</Button>}
+ {troubleshoot && <Button id="machine-troubleshoot" onClick={onTroubleshoot}>{_("Log in")}</Button>}
+ </>}
+ paragraph={paragraph} />
+ </PageSection>
+ </Page>
+ );
+};
diff --git a/pkg/shell/hosts.jsx b/pkg/shell/hosts.jsx
new file mode 100644
index 0000000..23425a6
--- /dev/null
+++ b/pkg/shell/hosts.jsx
@@ -0,0 +1,248 @@
+import cockpit from "cockpit";
+
+import React from 'react';
+import ReactDOM from "react-dom";
+import PropTypes from 'prop-types';
+import { PageSidebar } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+import { EditIcon, MinusIcon, CaretUpIcon, CaretDownIcon } from '@patternfly/react-icons';
+
+import 'polyfills';
+import { CockpitNav, CockpitNavItem } from "./nav.jsx";
+import { HostModal } from "./hosts_dialog.jsx";
+
+const _ = cockpit.gettext;
+const hosts_sel = document.getElementById("nav-hosts");
+
+class HostsSelector extends React.Component {
+ constructor() {
+ super();
+ this.el = document.createElement("div");
+ this.el.className = "view-hosts";
+ }
+
+ componentDidMount() {
+ hosts_sel.appendChild(this.el);
+ }
+
+ componentWillUnmount() {
+ hosts_sel.removeChild(this.el);
+ }
+
+ render() {
+ const { children } = this.props;
+ return ReactDOM.createPortal(children, this.el);
+ }
+}
+
+function HostLine({ host, user }) {
+ return (
+ <>
+ <span id="current-username" className="username">{user}</span>
+ {user && <span className="at">@</span>}
+ <span className="hostname">{host}</span>
+ </>
+ );
+}
+
+export class CockpitHosts extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ opened: false,
+ editing: false,
+ current_user: "",
+ current_key: props.machine.key,
+ show_modal: false,
+ edit_machine: null,
+ };
+
+ this.toggleMenu = this.toggleMenu.bind(this);
+ this.filterHosts = this.filterHosts.bind(this);
+ this.onAddNewHost = this.onAddNewHost.bind(this);
+ this.onEditHosts = this.onEditHosts.bind(this);
+ this.onHostEdit = this.onHostEdit.bind(this);
+ this.onRemove = this.onRemove.bind(this);
+ }
+
+ componentDidMount() {
+ cockpit.user().then(user => {
+ this.setState({ current_user: user.name || "" });
+ });
+ }
+
+ static getDerivedStateFromProps(nextProps, prevState) {
+ if (nextProps.machine.key !== prevState.current_key) {
+ document.getElementById(nextProps.selector).classList.toggle("interact", false);
+ return {
+ current_key: nextProps.machine.key,
+ opened: false,
+ editing: false,
+ };
+ }
+ return null;
+ }
+
+ toggleMenu() {
+ document.getElementById(this.props.selector).classList.toggle("interact", !this.state.opened);
+
+ this.setState(s => {
+ return (
+ {
+ opened: !s.opened,
+ editing: false,
+ }
+ );
+ });
+ }
+
+ onAddNewHost() {
+ this.setState({ show_modal: true });
+ }
+
+ onHostEdit(event, machine) {
+ this.setState({ show_modal: true, edit_machine: machine });
+ }
+
+ onEditHosts() {
+ this.setState(s => { return { editing: !s.editing } });
+ }
+
+ onRemove(event, machine) {
+ event.preventDefault();
+
+ if (this.props.machine === machine) {
+ // Removing machine underneath ourself - jump to localhost
+ const addr = this.props.hostAddr({ host: "localhost" }, true);
+ this.props.jump(addr);
+ }
+
+ if (this.props.machines.list.length <= 2)
+ this.setState({ editing: false });
+ this.props.machines.change(machine.key, { visible: false });
+ }
+
+ filterHosts(host, term) {
+ if (!term)
+ return host;
+ const new_host = Object.assign({}, host);
+ term = term.toLowerCase();
+
+ if (host.label.toLowerCase().indexOf(term) > -1)
+ new_host.keyword = host.label.toLowerCase();
+
+ const user = host.user || this.state.current_user;
+ if (user.toLowerCase().indexOf(term) > -1)
+ new_host.keyword = user.toLowerCase() + " @";
+
+ if (new_host.keyword)
+ return new_host;
+ return null;
+ }
+
+ // HACK: using HTML rather than Select PF4 component as:
+ // 1. It does not change the arrow when opened/closed
+ // 2. It closes the dropdown even when trying to search... and cannot tell it not to
+ render() {
+ const hostAddr = this.props.hostAddr;
+ const editing = this.state.editing;
+ const groups = [{
+ name: _("Hosts"),
+ items: this.props.machines.list,
+ }];
+ const render = (m, term) => <CockpitNavItem
+ term={term}
+ keyword={m.keyword}
+ to={hostAddr({ host: m.address }, true)}
+ active={m === this.props.machine}
+ key={m.key}
+ name={m.label}
+ header={(m.user ? m.user : this.state.current_user) + " @"}
+ status={m.state === "failed" ? { type: "error", title: _("Connection error") } : null}
+ className={m.state}
+ jump={this.props.jump}
+ actions={<>
+ <Tooltip content={_("Edit")} position="right">
+ <Button isDisabled={m.address === "localhost"} className="nav-action" hidden={!editing} onClick={e => this.onHostEdit(e, m)} key={m.label + "edit"} variant="secondary"><EditIcon /></Button>
+ </Tooltip>
+ <Tooltip content={_("Remove")} position="right">
+ <Button isDisabled={m.address === "localhost"} onClick={e => this.onRemove(e, m)} className="nav-action" hidden={!editing} key={m.label + "remove"} variant="danger"><MinusIcon /></Button>
+ </Tooltip>
+ </>}
+ />;
+ const label = this.props.machine.label || "";
+ const user = this.props.machine.user || this.state.current_user;
+ return (
+ <>
+ <div className="ct-switcher">
+ <div className="pf-v5-c-select pf-m-dark">
+ <button onClick={this.toggleMenu} id="host-toggle" aria-labelledby="host-toggle" aria-expanded={(this.state.opened ? "true" : "false")} aria-haspopup="listbox" type="button" className="ct-nav-toggle pf-v5-c-select__toggle pf-m-plain">
+ <span className="pf-v5-c-select__toggle-wrapper desktop_v">
+ <span className="pf-v5-c-select__toggle-text">
+ <HostLine user={user} host={label} />
+ </span>
+ </span>
+ <CaretUpIcon
+ className={`pf-v5-c-select__toggle-arrow mobile_v pf-v5-c-icon pf-m-lg ${this.state.opened ? 'clicked' : ''}`}
+ aria-hidden="true"
+ />
+ <span className="pf-v5-c-select__toggle-wrapper mobile_v">
+ {_("Host")}
+ </span>
+ <CaretDownIcon
+ className={`pf-v5-c-select__toggle-arrow desktop_v pf-v5-c-icon ${this.state.opened ? 'clicked' : ''}`}
+ aria-hidden="true"
+ />
+
+ </button>
+ </div>
+
+ { this.state.opened &&
+ <HostsSelector>
+ <PageSidebar isSidebarOpen={this.props.opened} theme="dark" className={"sidebar-hosts" + (this.state.editing ? " edit-hosts" : "")}>
+ <CockpitNav
+ selector={this.props.selector}
+ groups={groups}
+ item_render={render}
+ sorting={(a, b) => true}
+ filtering={this.filterHosts}
+ current={label}
+ jump={() => console.error("internal error: jump not supported in hosts selector")}
+ />
+ <div className="nav-hosts-actions">
+ {this.props.machines.list.length > 1 && <Button variant="secondary" onClick={this.onEditHosts}>{this.state.editing ? _("Stop editing hosts") : _("Edit hosts")}</Button>}
+ <Button variant="secondary" onClick={this.onAddNewHost}>{_("Add new host")}</Button>
+ </div>
+ </PageSidebar>
+ </HostsSelector>
+ }
+ </div>
+ {this.state.show_modal &&
+ <HostModal machines_ins={this.props.machines}
+ onClose={() => this.setState({ show_modal: false, edit_machine: null })}
+ address={this.state.edit_machine ? this.state.edit_machine.address : null}
+ caller_callback={this.state.edit_machine
+ ? (new_connection_string) => {
+ const parts = this.props.machines.split_connection_string(new_connection_string);
+ if (this.state.edit_machine == this.props.machine && parts.address != this.state.edit_machine.address) {
+ const addr = this.props.hostAddr({ host: parts.address }, true);
+ this.props.jump(addr);
+ }
+ return Promise.resolve();
+ }
+ : null } />
+ }
+ </>
+ );
+ }
+}
+
+CockpitHosts.propTypes = {
+ machine: PropTypes.object.isRequired,
+ machines: PropTypes.object.isRequired,
+ selector: PropTypes.string.isRequired,
+ hostAddr: PropTypes.func.isRequired,
+ jump: PropTypes.func.isRequired,
+};
diff --git a/pkg/shell/hosts_dialog.jsx b/pkg/shell/hosts_dialog.jsx
new file mode 100644
index 0000000..2662455
--- /dev/null
+++ b/pkg/shell/hosts_dialog.jsx
@@ -0,0 +1,1074 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import { get_init_superuser_for_options } from "./machines/machines";
+import * as credentials from "credentials";
+
+import ssh_show_default_key_sh from "./machines/ssh-show-default-key.sh";
+import ssh_add_key_sh from "./machines/ssh-add-key.sh";
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { ClipboardCopy } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js";
+import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { OutlinedQuestionCircleIcon } from "@patternfly/react-icons";
+
+import { FormHelper } from "cockpit-components-form-helper";
+import { ModalError } from "cockpit-components-inline-notification.jsx";
+
+const _ = cockpit.gettext;
+
+export const codes = {
+ "no-cockpit": "not-supported",
+ "not-supported": "not-supported",
+ "protocol-error": "not-supported",
+ "authentication-not-supported": "change-auth",
+ "authentication-failed": "change-auth",
+ "no-forwarding": "change-auth",
+ "unknown-hostkey": "unknown-hostkey",
+ "invalid-hostkey": "invalid-hostkey",
+ "not-found": "add-machine",
+ "unknown-host": "unknown-host"
+};
+
+function full_address(machines_ins, address) {
+ const machine = machines_ins.lookup(address);
+ if (machine && machine.address !== "localhost")
+ return machine.connection_string;
+
+ return address;
+}
+
+function is_method_supported(methods, method) {
+ const result = methods[method];
+ return result ? result !== "no-server-support" : false;
+}
+
+function prevent_default(callback) {
+ return event => {
+ callback();
+ event.preventDefault();
+ return false;
+ };
+}
+
+class NotSupported extends React.Component {
+ render() {
+ return (
+ <Modal id="hosts_setup_server_dialog" isOpen
+ position="top" variant="medium"
+ onClose={this.props.onClose}
+ title={_("Cockpit is not installed")}
+ footer={<>
+ <Button variant="link" className="btn-cancel" onClick={this.props.onClose}>
+ { _("Close") }
+ </Button>
+ </>}
+ >
+ <Stack hasGutter>
+ { this.props.dialogError && <ModalError dialogError={this.props.dialogError} />}
+ <p>{cockpit.format(_("A compatible version of Cockpit is not installed on $0."), this.props.full_address)}</p>
+ </Stack>
+ </Modal>
+ );
+ }
+}
+
+class AddMachine extends React.Component {
+ constructor(props) {
+ super(props);
+
+ let address_parts = null;
+ if (this.props.full_address)
+ address_parts = this.props.machines_ins.split_connection_string(this.props.full_address);
+
+ let host_address = "";
+ let host_user = "";
+ if (address_parts) {
+ host_address = address_parts.address;
+ if (address_parts.port)
+ host_address += ":" + address_parts.port;
+ host_user = address_parts.user;
+ }
+
+ let color = props.machines_ins.unused_color();
+ let old_machine = null;
+ if (props.old_address)
+ old_machine = props.machines_ins.lookup(props.old_address);
+ if (old_machine)
+ color = this.rgb2Hex(old_machine.color);
+
+ this.state = {
+ user: host_user || "",
+ address: host_address || "",
+ color,
+ addressError: "",
+ inProgress: false,
+ old_machine,
+ userChanged: false,
+ };
+
+ this.onAddressChange = this.onAddressChange.bind(this);
+ this.onAddHost = this.onAddHost.bind(this);
+ }
+
+ rgb2Hex(c) {
+ function toHex(d) {
+ return ("0" + (parseInt(d, 10).toString(16)))
+ .slice(-2);
+ }
+
+ if (c[0] === "#")
+ return c;
+
+ const colors = /rgb\((\d*), (\d*), (\d*)\)/.exec(c);
+ return "#" + toHex(colors[1]) + toHex(colors[2]) + toHex(colors[3]);
+ }
+
+ onAddressChange() {
+ let error = "";
+ if (this.state.address.search(/\s+/) !== -1)
+ error = _("The IP address or hostname cannot contain whitespace.");
+ else {
+ const machine = this.props.machines_ins.lookup(this.state.address);
+ const machine_address = machine ? full_address(this.props.machines_ins, machine.address) : undefined;
+ if (machine && machine.on_disk && machine_address != this.props.old_address) {
+ if (machine.visible)
+ error = _("This machine has already been added.");
+ else if (!this.state.userChanged)
+ this.setState({ user: machine.user, color: this.rgb2Hex(machine.color) });
+ } else if (this.state.old_machine && !machine && !this.state.userChanged) { // When editing host by changing its address generate new color
+ this.setState((_, prevProps) => ({ color: prevProps.machines_ins.unused_color(), userChanged: true }));
+ }
+ }
+
+ this.setState({ addressError: error });
+
+ return error;
+ }
+
+ onAddHost() {
+ const parts = this.props.machines_ins.split_connection_string(this.state.address);
+ // user in "User name:" field wins over user in connection string
+ const address = this.props.machines_ins.generate_connection_string(this.state.user || parts.user, parts.port, parts.address);
+
+ if (this.onAddressChange())
+ return;
+
+ this.props.setAddress(address);
+
+ if (this.state.old_machine && address === this.state.old_machine.connection_string) {
+ this.props.setError(null);
+ this.setState({ inProgress: true });
+ this.props.run(this.props.machines_ins.change(this.state.old_machine.key, { color: this.state.color }))
+ .catch(ex => {
+ this.setState({ inProgress: false });
+ throw ex;
+ });
+ return;
+ }
+
+ this.props.setError(null);
+ this.setState({ inProgress: true });
+
+ this.props.setGoal(() => {
+ const parts = this.props.machines_ins.split_connection_string(this.state.address);
+ // user in "User name:" field wins over user in connection string
+ const address = this.props.machines_ins.generate_connection_string(this.state.user || parts.user, parts.port, parts.address);
+
+ return new Promise((resolve, reject) => {
+ this.props.machines_ins.add(address, this.state.color)
+ .then(() => {
+ // When changing address of machine, hide the old one
+ if (this.state.old_machine && this.state.old_machine != this.props.machines_ins.lookup(address)) {
+ this.props.machines_ins.change(this.state.old_machine.key, { visible: false })
+ .then(resolve);
+ } else {
+ resolve();
+ }
+ })
+ .catch(ex => {
+ ex.message = cockpit.format(_("Failed to add machine: $0"), cockpit.message(ex));
+ this.setState({ dialogError: cockpit.message(ex), inProgress: false });
+ reject(ex);
+ });
+ });
+ });
+
+ this.props.run(this.props.try2Connect(address), ex => {
+ if (ex.problem === "no-host") {
+ let host_id_port = address;
+ let port = "22";
+ const port_index = host_id_port.lastIndexOf(":");
+ if (port_index === -1)
+ host_id_port = address + ":22";
+ else
+ port = host_id_port.substr(port_index + 1);
+
+ ex.message = cockpit.format(_("Unable to contact the given host $0. Make sure it has ssh running on port $1, or specify another port in the address."), host_id_port, port);
+ ex.problem = "not-found";
+ }
+ this.setState({ inProgress: false });
+ this.props.setError(ex);
+ });
+ }
+
+ render() {
+ const invisible = this.props.machines_ins.addresses.filter(addr => {
+ const m = this.props.machines_ins.lookup(addr);
+ return !m || !m.visible;
+ });
+
+ const callback = this.onAddHost;
+ const title = this.state.old_machine ? _("Edit host") : _("Add new host");
+ const submitText = this.state.old_machine ? _("Set") : _("Add");
+
+ const body = <Form isHorizontal onSubmit={prevent_default(callback)}>
+ <FormGroup label={_("Host")}>
+ <TextInput id="add-machine-address" onChange={(_event, address) => this.setState({ address })}
+ validated={this.state.addressError ? "error" : "default"} onBlur={this.onAddressChange}
+ isDisabled={this.props.old_address === "localhost"} list="options" value={this.state.address} />
+ <datalist id="options">
+ {invisible.map(a => <option key={a} value={a} />)}
+ </datalist>
+ <FormHelper helperTextInvalid={this.state.addressError} helperText={_("Can be a hostname, IP address, alias name, or ssh:// URI")} />
+ </FormGroup>
+ <FormGroup label={_("User name")}>
+ <TextInput id="add-machine-user" onChange={(_event, value) => this.setState({ user: value, userChanged: true })}
+ isDisabled={this.props.old_address === "localhost"} value={this.state.user} />
+ <FormHelper helperText={_("When empty, connect with the current user")} />
+ </FormGroup>
+ <FormGroup label={_("Color")}>
+ <input type="color" value={this.state.color} onChange={(e) => this.setState({ color: e.target.value }) } />
+ </FormGroup>
+ </Form>;
+
+ return (
+ <Modal id="hosts_setup_server_dialog" isOpen
+ position="top" variant="medium"
+ onClose={this.props.onClose}
+ title={title}
+ footer={<>
+ <Button variant="primary" onClick={callback} isLoading={this.state.inProgress}
+ isDisabled={this.state.address === "" || this.state.addressError !== "" || this.state.inProgress}>
+ { submitText }
+ </Button>
+ <Button variant="link" className="btn-cancel" onClick={this.props.onClose}>
+ { _("Cancel") }
+ </Button>
+ </>}
+ >
+ <Stack hasGutter>
+ { this.props.dialogError && <ModalError dialogError={this.props.dialogError} />}
+ {body}
+ </Stack>
+ </Modal>
+ );
+ }
+}
+
+class MachinePort extends React.Component {
+ constructor(props) {
+ super(props);
+
+ const machine = props.machines_ins.lookup(props.full_address);
+ if (!machine) {
+ props.onClose();
+ return;
+ }
+
+ this.state = {
+ port: machine.port,
+ };
+
+ this.onChangePort = this.onChangePort.bind(this);
+ }
+
+ onChangePort() {
+ const promise = new Promise((resolve, reject) => {
+ const parts = this.props.machines_ins.split_connection_string(this.props.full_address);
+ parts.port = this.state.port;
+ const address = this.props.machines_ins.generate_connection_string(parts.user,
+ parts.port,
+ parts.address);
+ const self = this;
+
+ function update_host(ex) {
+ self.props.setAddress(address);
+ self.props.machines_ins.change(parts.address, { port: parts.port })
+ .then(() => {
+ // We failed before so try to connect again now that the machine is saved
+ if (ex) {
+ self.props.try2Connect(address)
+ .then(self.props.complete)
+ .catch(reject);
+ } else {
+ resolve();
+ }
+ })
+ .catch(ex => reject(cockpit.format(_("Failed to edit machine: $0"), cockpit.message(ex))));
+ }
+
+ this.props.try2Connect(address)
+ .then(update_host)
+ .catch(ex => {
+ // any other error means progress, so save
+ if (ex.problem !== 'no-host')
+ update_host(ex);
+ else
+ reject(ex);
+ });
+ });
+
+ this.props.run(promise);
+ }
+
+ render() {
+ const callback = this.onChangePort;
+ const title = cockpit.format(_("Could not contact $0"), this.props.full_address);
+ const submitText = _("Update");
+
+ const body = <>
+ <p>
+ <span>{cockpit.format(_("Unable to contact $0."), this.props.full_address)}</span>
+ <span>{_("Is sshd running on a different port?")}</span>
+ </p>
+ <Form isHorizontal onSubmit={prevent_default(callback)}>
+ <FormGroup label={_("Port")}>
+ <TextInput id="edit-machine-port" onChange={(_event, value) => this.setState({ port: value })} />
+ </FormGroup>
+ </Form>
+ </>;
+
+ return (
+ <Modal id="hosts_setup_server_dialog" isOpen
+ position="top" variant="medium"
+ onClose={this.props.onClose}
+ title={title}
+ footer={<>
+ <Button variant="primary" onClick={callback} isLoading={this.state.inProgress}
+ isDisabled={this.state.inProgress}>
+ { submitText }
+ </Button>
+ <Button variant="link" className="btn-cancel" onClick={this.props.onClose}>
+ { _("Cancel") }
+ </Button>
+ </>}
+ >
+ <Stack hasGutter>
+ { this.props.dialogError && <ModalError dialogError={this.props.dialogError} />}
+ {body}
+ </Stack>
+ </Modal>
+ );
+ }
+}
+
+class HostKey extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ inProgress: false,
+ verifyExpanded: false,
+ error_options: props.error_options,
+ };
+
+ this.onAddKey = this.onAddKey.bind(this);
+ }
+
+ componentDidMount() {
+ if (!this.props.error_options || !this.props.error_options["host-key"]) {
+ const options = {};
+ let match_problem = this.props.template;
+ if (this.props.template == "unknown-host") {
+ options.session = "private";
+ match_problem = "unknown-hostkey";
+ }
+
+ this.props.try2Connect(this.props.full_address, options)
+ .then(this.props.complete)
+ .catch(ex => {
+ if (ex.problem !== match_problem) {
+ this.props.setError(ex);
+ } else {
+ this.setState({ error_options: ex });
+ }
+ });
+ }
+ }
+
+ onAddKey() {
+ this.setState({ inProgress: true });
+
+ const key = this.state.error_options["host-key"];
+ const machine = this.props.machines_ins.lookup(this.props.full_address);
+ let q;
+ if (!machine || machine.on_disk) {
+ q = this.props.machines_ins.add_key(key);
+ } else {
+ // When machine isn't saved to disk don't save the key either
+ q = this.props.machines_ins.change(this.props.full_address, { host_key: key });
+ }
+
+ this.props.run(q.then(() => {
+ return this.props.try2Connect(this.props.full_address, {})
+ .catch(ex => {
+ if ((ex.problem == "invalid-hostkey" || ex.problem == "unknown-hostkey") && machine && !machine.on_disk)
+ this.props.machines_ins.change(this.props.full_address, { host_key: null });
+ else {
+ this.setState({ inProgress: false });
+ throw ex;
+ }
+ });
+ }));
+ }
+
+ render() {
+ let key_type = "";
+ let fp = "";
+ if (this.state.error_options && this.state.error_options["host-key"]) {
+ key_type = this.state.error_options["host-key"].split(" ")[1];
+ fp = this.state.error_options["host-fingerprint"];
+ }
+
+ const callback = this.onAddKey;
+ const title = cockpit.format(this.props.template === "invalid-hostkey" ? _("$0 key changed") : _("New host: $0"),
+ this.props.host);
+ const submitText = _("Trust and add host");
+ let unknown = false;
+ let body = null;
+ if (!key_type) {
+ unknown = true;
+ } else if (this.props.template === "invalid-hostkey") {
+ body = <>
+ <Alert variant='danger' isInline title={_("Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.")} />
+ <p>{_("To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:")}</p>
+ <ClipboardCopy isReadOnly hoverTip={_("Copy")} clickTip={_("Copied")} className="hostkey-fingerprint pf-v5-u-font-family-monospace">{fp}</ClipboardCopy>
+ <p className="hostkey-type">({key_type})</p>
+ <p>{cockpit.format(_("To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:"), this.props.host)}</p>
+ <ClipboardCopy isReadOnly hoverTip={_("Copy")} clickTip={_("Copied")} className="hostkey-verify-help-cmds pf-v5-u-font-family-monospace">ssh-keyscan -t {key_type} localhost | ssh-keygen -lf -</ClipboardCopy>
+ <p>{_("The resulting fingerprint is fine to share via public methods, including email.")}</p>
+ <p>{_("If the fingerprint matches, click 'Trust and add host'. Otherwise, do not connect and contact your administrator.")}</p>
+ </>;
+ } else {
+ const fingerprint_help = <Popover bodyContent={
+ _("The resulting fingerprint is fine to share via public methods, including email. If you are asking someone else to do the verification for you, they can send the results using any method.")}>
+ <OutlinedQuestionCircleIcon />
+ </Popover>;
+ body = <>
+ <p>{cockpit.format(_("You are connecting to $0 for the first time."), this.props.host)}</p>
+ <ExpandableSection toggleText={ _("Verify fingerprint") }
+ isExpanded={this.state.verifyExpanded}
+ onToggle={(_ev, verifyExpanded) => this.setState({ verifyExpanded }) }>
+ <div>{_("Run this command over a trusted network or physically on the remote machine:")}</div>
+ <ClipboardCopy isReadOnly hoverTip={_("Copy")} clickTip={_("Copied")} className="hostkey-verify-help hostkey-verify-help-cmds pf-v5-u-font-family-monospace">ssh-keyscan -t {key_type} localhost | ssh-keygen -lf -</ClipboardCopy>
+ <div>{_("The fingerprint should match:")} {fingerprint_help}</div>
+ <ClipboardCopy isReadOnly hoverTip={_("Copy")} clickTip={_("Copied")} className="hostkey-verify-help hostkey-fingerprint pf-v5-u-font-family-monospace">{fp}</ClipboardCopy>
+ </ExpandableSection>
+ <Alert variant='warning' isInline isPlain title={_("Malicious pages on a remote machine may affect other connected hosts")} />
+ </>;
+ }
+
+ return (
+ <Modal id="hosts_setup_server_dialog" isOpen
+ position="top" variant="medium"
+ onClose={this.props.onClose}
+ title={title}
+ footer={<>
+ { unknown ||
+ <Button variant="primary" onClick={callback} isLoading={this.state.inProgress}
+ isDisabled={this.state.inProgress}>
+ { submitText }
+ </Button>
+ }
+ <Button variant="link" className="btn-cancel" onClick={this.props.onClose}>
+ { _("Cancel") }
+ </Button>
+ </>}
+ >
+ <Stack hasGutter>
+ { this.props.dialogError && <ModalError dialogError={this.props.dialogError} />}
+ {body}
+ </Stack>
+ </Modal>
+ );
+ }
+}
+
+class ChangeAuth extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ auth: "password",
+ auto_login: false,
+ custom_password: "",
+ custom_password_error: "",
+ locked_identity_password: "",
+ locked_identity_password_error: "",
+ login_setup_new_key_password: "",
+ login_setup_new_key_password_error: "",
+ login_setup_new_key_password2: "",
+ login_setup_new_key_password2_error: "",
+ user: "",
+ default_ssh_key: null,
+ identity_path: null,
+ inProgress: true, // componentDidMount changes to false once loaded
+ };
+
+ this.keys = null;
+ if (credentials)
+ this.keys = credentials.keys_instance();
+
+ this.getSupports = this.getSupports.bind(this);
+ this.updateIdentity = this.updateIdentity.bind(this);
+ this.login = this.login.bind(this);
+ this.maybe_create_key = this.maybe_create_key.bind(this);
+ this.authorize_key = this.authorize_key.bind(this);
+ this.maybe_unlock_key = this.maybe_unlock_key.bind(this);
+ }
+
+ updateIdentity() {
+ let identity_path = null;
+ if (this.props.error_options && this.props.error_options.error && this.props.error_options.error.startsWith("locked identity"))
+ identity_path = this.props.error_options.error.split(": ")[1];
+
+ const default_ssh_key = this.state.default_ssh_key;
+ if (default_ssh_key && default_ssh_key.encrypted)
+ default_ssh_key.unaligned_passphrase = identity_path && identity_path === default_ssh_key.name;
+
+ this.setState({ identity_path, default_ssh_key });
+ }
+
+ componentDidMount() {
+ cockpit.user()
+ .then(user =>
+ cockpit.script(ssh_show_default_key_sh, [], { })
+ .then(data => {
+ let default_ssh_key = null;
+ const info = data.split("\n");
+ if (info[0])
+ default_ssh_key = { name: info[0], exists: true, encrypted: info[1] === "encrypted" };
+ else
+ default_ssh_key = { name: user.home + "/.ssh/id_rsa", type: "rsa", exists: false };
+
+ return this.setState({ inProgress: false, default_ssh_key, user }, this.updateIdentity);
+ })
+ )
+ .catch(ex => { this.setState({ inProgress: false }); this.props.setError(ex) });
+
+ if (!this.props.error_options || this.props.error_options["auth-method-results"] === null) {
+ this.props.try2Connect(this.props.full_address)
+ .then(this.props.complete)
+ .catch(ex => {
+ this.setState({ inProgress: false });
+ this.props.setError(ex);
+ });
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.keys)
+ this.keys.close();
+ this.keys = null;
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.error_options !== this.props.error_options)
+ this.updateIdentity();
+ }
+
+ getSupports() {
+ let methods = null;
+ let available = null;
+
+ let offer_login_password = false;
+ let offer_key_password = false;
+
+ if (this.props.error_options) {
+ available = {};
+
+ methods = this.props.error_options["auth-method-results"];
+ if (methods) {
+ for (const method in methods) {
+ if (is_method_supported(methods, method)) {
+ available[method] = true;
+ }
+ }
+ }
+
+ offer_login_password = !!available.password;
+ offer_key_password = this.state.identity_path !== null;
+ } else {
+ offer_login_password = true;
+ offer_key_password = false;
+ }
+
+ return {
+ offer_login_password,
+ offer_key_password,
+ };
+ }
+
+ maybe_create_key(passphrase) {
+ if (!this.state.default_ssh_key.exists)
+ return this.keys.create(this.state.default_ssh_key.name, this.state.default_ssh_key.type, passphrase, passphrase);
+ else
+ return Promise.resolve();
+ }
+
+ authorize_key(host) {
+ return this.keys.get_pubkey(this.state.default_ssh_key.name)
+ .then(data => cockpit.script(ssh_add_key_sh, [data.trim()], { host, err: "message" }));
+ }
+
+ maybe_unlock_key() {
+ const { offer_login_password, offer_key_password } = this.getSupports();
+ const both = offer_login_password && offer_key_password;
+
+ if ((both && this.state.auth === "key") || (!both && offer_key_password))
+ return this.keys.load(this.state.identity_path, this.state.locked_identity_password);
+ else
+ return Promise.resolve();
+ }
+
+ login() {
+ const options = {};
+ const user = this.props.machines_ins.split_connection_string(this.props.full_address).user || "";
+ const do_key_password_change = this.state.auto_login && this.state.default_ssh_key.unaligned_passphrase;
+
+ let custom_password_error = "";
+ let locked_identity_password_error = "";
+ let login_setup_new_key_password_error = "";
+ let login_setup_new_key_password2_error = "";
+
+ const { offer_login_password, offer_key_password } = this.getSupports();
+ const both = offer_login_password && offer_key_password;
+
+ if ((both && this.state.auth === "password") || (!both && offer_login_password)) {
+ if (!this.state.custom_password)
+ custom_password_error = _("The password can not be empty");
+
+ options.password = this.state.custom_password;
+ options.session = 'shared';
+ if (!user) {
+ /* we don't want to save the default user for everyone
+ * so we pass current user as an option, but make sure the
+ * session isn't private
+ */
+ if (this.state.user && this.state.user.name)
+ options.user = this.state.user.name;
+ options["temp-session"] = false; /* Compatibility option */
+ }
+ }
+
+ if ((offer_key_password && !(both && this.state.auth === "password")) && !this.state.locked_identity_password)
+ locked_identity_password_error = _("The key password can not be empty");
+
+ if (this.state.auto_login && !do_key_password_change && this.state.login_setup_new_key_password !== this.state.login_setup_new_key_password2)
+ login_setup_new_key_password2_error = _("The key passwords do not match");
+
+ if (do_key_password_change && !this.state.login_setup_new_key_password)
+ login_setup_new_key_password_error = _("The new key password can not be empty");
+
+ if (do_key_password_change && this.state.login_setup_new_key_password !== this.state.login_setup_new_key_password2)
+ login_setup_new_key_password2_error = _("The key passwords do not match");
+
+ if (custom_password_error || locked_identity_password_error || login_setup_new_key_password_error || login_setup_new_key_password2_error) {
+ this.setState({
+ custom_password_error,
+ locked_identity_password_error,
+ login_setup_new_key_password_error,
+ login_setup_new_key_password2_error,
+ });
+ return;
+ }
+
+ this.setState({ inProgress: true });
+ const machine = this.props.machines_ins.lookup(this.props.full_address);
+
+ this.props.run(this.maybe_unlock_key()
+ .then(() => {
+ return this.props.try2Connect(this.props.full_address, options)
+ .then(() => {
+ if (machine)
+ return this.props.machines_ins.change(machine.address, { user });
+ else
+ return Promise.resolve();
+ })
+ .then(() => {
+ if (do_key_password_change)
+ return this.keys.change(this.state.default_ssh_key.name, this.state.locked_identity_password, this.state.login_setup_new_key_password, this.state.login_setup_new_key_password);
+ else if (this.state.auto_login)
+ return this.maybe_create_key(this.state.login_setup_new_key_password)
+ .then(() => this.authorize_key(this.props.full_address));
+ else
+ return Promise.resolve();
+ });
+ })
+ .catch(ex => {
+ this.setState({ inProgress: false });
+ throw ex;
+ }));
+ }
+
+ render() {
+ const { offer_login_password, offer_key_password } = this.getSupports();
+ const both = offer_login_password && offer_key_password;
+
+ let offer_key_setup = true;
+ let show_password_advice = true;
+ if (!this.state.default_ssh_key)
+ offer_key_setup = false;
+ else if (this.state.default_ssh_key.unaligned_passphrase)
+ offer_key_setup = (both && this.state.auth === "key") || (!both && offer_key_password);
+ else if (this.state.identity_path) {
+ // This is a locked, non-default identity that will never
+ // be loaded into the agent, so there is no point in
+ // offering to change the passphrase.
+ show_password_advice = false;
+ offer_key_setup = false;
+ }
+
+ const callback = this.login;
+ const title = cockpit.format(_("Log in to $0"), this.props.full_address);
+ const submitText = _("Log in");
+ let statement = "";
+
+ if (!offer_login_password && !offer_key_password)
+ statement = <p>{cockpit.format(_("Unable to log in to $0. The host does not accept password login or any of your SSH keys."), this.props.full_address)}</p>;
+ else if (offer_login_password && !offer_key_password)
+ statement = <p>{cockpit.format(_("Unable to log in to $0 using SSH key authentication. Please provide the password. You may want to set up your SSH keys for automatic login."), this.props.full_address)}</p>;
+ else if (offer_key_password && !offer_login_password)
+ statement = <>
+ <p>{cockpit.format(_("The SSH key for logging in to $0 is protected by a password, and the host does not allow logging in with a password. Please provide the password of the key at $1."), this.props.full_address, this.state.identity_path)}</p>
+ {show_password_advice && <span className="password-change-advice">{_("You may want to change the password of the key for automatic login.")}</span>}
+ </>;
+ else if (both)
+ statement = <>
+ <p>{cockpit.format(_("The SSH key for logging in to $0 is protected. You can log in with either your login password or by providing the password of the key at $1."), this.props.full_address, this.state.identity_path)}</p>
+ {show_password_advice && <span className="password-change-advice">{_("You may want to change the password of the key for automatic login.")}</span>}
+ </>;
+
+ let auto_text = null;
+ let auto_details = null;
+ if (this.state.default_ssh_key) {
+ const lmach = this.props.machines_ins.lookup(null);
+ const key = this.state.default_ssh_key.name;
+ const luser = this.state.user.name;
+ const lhost = lmach ? lmach.label || lmach.address : "localhost";
+ const afile = "~/.ssh/authorized_keys";
+ const ruser = this.props.machines_ins.split_connection_string(this.props.full_address).user || this.state.user.name;
+ const rhost = this.props.machines_ins.split_connection_string(this.props.full_address).address;
+ if (!this.state.default_ssh_key.exists) {
+ auto_text = _("Create a new SSH key and authorize it");
+ auto_details = <>
+ <p>{cockpit.format(_("A new SSH key at $0 will be created for $1 on $2 and it will be added to the $3 file of $4 on $5."), key, luser, lhost, afile, ruser, rhost)}</p>
+ <FormGroup label={_("Key password")}>
+ <TextInput id="login-setup-new-key-password" onChange={(_event, value) => this.setState({ login_setup_new_key_password: value })}
+ type="password" value={this.state.login_setup_new_key_password} validated={this.state.login_setup_new_key_password_error ? "error" : "default"} />
+ <FormHelper helperTextInvalid={this.state.login_setup_new_key_password_error} />
+ </FormGroup>
+ <FormGroup label={_("Confirm key password")}>
+ <TextInput id="login-setup-new-key-password2" onChange={(_event, value) => this.setState({ login_setup_new_key_password2: value })}
+ type="password" value={this.state.login_setup_new_key_password2} validated={this.state.login_setup_new_key_password2_error ? "error" : "default"} />
+ <FormHelper helperTextInvalid={this.state.login_setup_new_key_password2_error} />
+ </FormGroup>
+ <p>{cockpit.format(_("In order to allow log in to $0 as $1 without password in the future, use the login password of $2 on $3 as the key password, or leave the key password blank."), rhost, ruser, luser, lhost)}</p>
+ </>;
+ } else if (this.state.default_ssh_key.unaligned_passphrase) {
+ auto_text = cockpit.format(_("Change the password of $0"), key);
+ auto_details = <>
+ <p>{cockpit.format(_("By changing the password of the SSH key $0 to the login password of $1 on $2, the key will be automatically made available and you can log in to $3 without password in the future."), key, luser, lhost, afile, rhost)}</p>
+ <FormGroup label={_("New key password")}>
+ <TextInput id="login-setup-new-key-password" onChange={(_event, value) => this.setState({ login_setup_new_key_password: value })}
+ type="password" value={this.state.login_setup_new_key_password} validated={this.state.login_setup_new_key_password_error ? "error" : "default"} />
+ <FormHelper helperTextInvalid={this.state.login_setup_new_key_password_error} />
+ </FormGroup>
+ <FormGroup label={_("Confirm new key password")} validated={this.state.login_setup_new_key_password2_error ? "error" : "default"}>
+ <TextInput id="login-setup-new-key-password2" onChange={(_event, value) => this.setState({ login_setup_new_key_password2: value })}
+ type="password" value={this.state.login_setup_new_key_password2} validated={this.state.login_setup_new_key_password2_error ? "error" : "default"} />
+
+ <FormHelper helperTextInvalid={this.state.login_setup_new_key_password2_error} />
+ </FormGroup>
+ <p>{cockpit.format(_("In order to allow log in to $0 as $1 without password in the future, use the login password of $2 on $3 as the key password, or leave the key password blank."), rhost, ruser, luser, lhost)}</p>
+ </>;
+ } else {
+ auto_text = _("Authorize SSH key");
+ auto_details = <>
+ <p>{cockpit.format(_("The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."), key, luser, lhost, afile, ruser, rhost)}</p>
+ <p>{_("This will allow you to log in without password in the future.")}</p>
+ </>;
+ }
+ }
+
+ const body = <>
+ {statement}
+ <br />
+ {(offer_login_password || offer_key_password) &&
+ <Form isHorizontal onSubmit={prevent_default(callback)}>
+ {both &&
+ <FormGroup label={_("Authentication")} isInline hasNoPaddingTop>
+ <Radio isChecked={this.state.auth === "password"}
+ onChange={() => this.setState({ auth: "password" })}
+ id="auth-password"
+ value="password"
+ label={_("Password")} />
+ <Radio isChecked={this.state.auth === "key"}
+ onChange={() => this.setState({ auth: "key" })}
+ id="auth-key"
+ value="key"
+ label={_("SSH key")} />
+ </FormGroup>
+ }
+ {((both && this.state.auth === "password") || (!both && offer_login_password)) &&
+ <FormGroup label={_("Password")}>
+ <TextInput id="login-custom-password" onChange={(_event, value) => this.setState({ custom_password: value })}
+ type="password" value={this.state.custom_password} validated={this.state.custom_password_error ? "error" : "default"} />
+ <FormHelper helperTextInvalid={this.state.custom_password_error} />
+ </FormGroup>
+ }
+ {((both && this.state.auth === "key") || (!both && offer_key_password)) &&
+ <FormGroup label={_("Key password")}>
+ <TextInput id="locked-identity-password" onChange={(_event, value) => this.setState({ locked_identity_password: value })}
+ type="password" autoComplete="new-password" value={this.state.locked_identity_password} validated={this.state.locked_identity_password_error ? "error" : "default"} />
+ <FormHelper
+ helperText={cockpit.format(_("The SSH key $0 will be made available for the remainder of the session and will be available for login to other hosts as well."), this.state.identity_path)}
+ helperTextInvalid={this.state.locked_identity_password_error} />
+ </FormGroup>
+ }
+ {offer_key_setup &&
+ <FormGroup label={ _("Automatic login") } hasNoPaddingTop isInline>
+ <Checkbox onChange={(_event, checked) => this.setState({ auto_login: checked })}
+ isChecked={this.state.auto_login} id="login-setup-keys"
+ label={auto_text} body={this.state.auto_login ? auto_details : null} />
+ </FormGroup>
+ }
+ </Form>
+ }
+ </>;
+
+ return (
+ <Modal id="hosts_setup_server_dialog" isOpen
+ position="top" variant="medium"
+ onClose={this.props.onClose}
+ title={title}
+ footer={<>
+ <Button variant="primary" onClick={callback} isLoading={this.state.inProgress}
+ isDisabled={this.state.inProgress || (!offer_login_password && !offer_key_password) || !this.state.default_ssh_key || !this.props.error_options}>
+ { submitText }
+ </Button>
+ <Button variant="link" className="btn-cancel" onClick={this.props.onClose}>
+ { _("Cancel") }
+ </Button>
+ </>}
+ >
+ <Stack hasGutter>
+ { this.props.dialogError && <ModalError dialogError={this.props.dialogError} />}
+ {body}
+ </Stack>
+ </Modal>
+ );
+ }
+}
+
+export class HostModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ current_template: this.props.template || "add-machine",
+ address: full_address(props.machines_ins, props.address),
+ old_address: full_address(props.machines_ins, props.address),
+ error_options: null,
+ dialogError: "", // Error to be shown in the modal
+ };
+
+ this.promise_callback = null;
+
+ this.addressOrLabel = this.addressOrLabel.bind(this);
+ this.changeContent = this.changeContent.bind(this);
+ this.try2Connect = this.try2Connect.bind(this);
+ this.setGoal = this.setGoal.bind(this);
+ this.setError = this.setError.bind(this);
+ this.setAddress = this.setAddress.bind(this);
+ this.run = this.run.bind(this);
+ this.complete = this.complete.bind(this);
+ }
+
+ addressOrLabel() {
+ const machine = this.props.machines_ins.lookup(this.state.address);
+ let host = this.props.machines_ins.split_connection_string(this.state.address).address;
+ if (machine && machine.label)
+ host = machine.label;
+ return host;
+ }
+
+ changeContent(template, error_options) {
+ if (this.state.current_template !== template)
+ this.setState({ current_template: template, error_options });
+ }
+
+ try2Connect(address, options) {
+ return new Promise((resolve, reject) => {
+ const conn_options = { ...options, payload: "echo", host: address };
+
+ conn_options["init-superuser"] = get_init_superuser_for_options(conn_options);
+
+ const machine = this.props.machines_ins.lookup(address);
+ if (machine && machine.host_key && !machine.on_disk) {
+ conn_options['temp-session'] = false; // Compatibility option
+ conn_options.session = 'shared';
+ conn_options['host-key'] = machine.host_key;
+ }
+
+ const client = cockpit.channel(conn_options);
+ client.send("x");
+ client.addEventListener("message", () => {
+ resolve();
+ client.close();
+ });
+ client.addEventListener("close", (event, options) => {
+ reject(options);
+ });
+ });
+ }
+
+ complete() {
+ if (this.promise_callback)
+ this.promise_callback().then(this.props.onClose);
+ else
+ this.props.onClose();
+ }
+
+ setGoal(callback) {
+ this.promise_callback = callback;
+ }
+
+ setError(error) {
+ if (error === null)
+ return this.setState({ dialogError: null });
+
+ let template = null;
+ if (error.problem && error.command === "close")
+ template = codes[error.problem];
+
+ if (template && this.state.current_template !== template)
+ this.changeContent(template, error);
+ else
+ this.setState({ error_options: error, dialogError: cockpit.message(error) });
+ }
+
+ setAddress(address) {
+ this.setState({ address });
+ }
+
+ run(promise, failure_callback) {
+ return new Promise((resolve, reject) => {
+ const promise_funcs = [];
+ const self = this;
+
+ function next(i) {
+ promise_funcs[i]()
+ .then(val => {
+ i = i + 1;
+ if (i < promise_funcs.length) {
+ next(i);
+ } else {
+ resolve();
+ self.props.onClose();
+ }
+ })
+ .catch(ex => {
+ if (failure_callback)
+ failure_callback(ex);
+ else
+ self.setError(ex);
+ });
+ }
+
+ promise_funcs.push(() => { return promise });
+
+ if (this.promise_callback)
+ promise_funcs.push(this.promise_callback);
+
+ if (this.props.caller_callback)
+ promise_funcs.push(() => this.props.caller_callback(this.state.address));
+
+ next(0);
+ });
+ }
+
+ render() {
+ const template = this.state.current_template;
+
+ const props = {
+ template,
+ host: this.addressOrLabel(),
+ full_address: this.state.address,
+ old_address: this.state.old_address,
+ address_data: this.props.machines_ins.split_connection_string(this.state.address),
+ error_options: this.state.error_options,
+ dialogError: this.state.dialogError,
+ machines_ins: this.props.machines_ins,
+ onClose: this.props.onClose,
+ run: this.run,
+ setGoal: this.setGoal,
+ setError: this.setError,
+ setAddress: this.setAddress,
+ try2Connect: this.try2Connect,
+ complete: this.complete,
+ };
+
+ if (template === "add-machine")
+ return <AddMachine {...props} />;
+ else if (template === "unknown-hostkey" || template === "unknown-host" || template === "invalid-hostkey")
+ return <HostKey {...props} />;
+ else if (template === "change-auth")
+ return <ChangeAuth {...props} />;
+ else if (template === "change-port")
+ return <MachinePort {...props} />;
+ else if (template === "not-supported")
+ return <NotSupported {...props} />;
+
+ console.error("Unknown template:", template);
+ return null;
+ }
+}
+
+HostModal.propTypes = {
+ machines_ins: PropTypes.object.isRequired,
+ onClose: PropTypes.func.isRequired,
+ caller_callback: PropTypes.func,
+ address: PropTypes.string,
+ template: PropTypes.string,
+};
diff --git a/pkg/shell/images/bg-plain.jpg b/pkg/shell/images/bg-plain.jpg
new file mode 100644
index 0000000..a874f34
--- /dev/null
+++ b/pkg/shell/images/bg-plain.jpg
Binary files differ
diff --git a/pkg/shell/images/cockpit-icon.svg b/pkg/shell/images/cockpit-icon.svg
new file mode 100644
index 0000000..d6adfb5
--- /dev/null
+++ b/pkg/shell/images/cockpit-icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs/><g fill="#fff" class="ct-icon"><path class="ct-icon-circle" d="M16 0A16 16 0 000 16a16 16 0 0016 16 16 16 0 0016-16A16 16 0 0016 0zm0 2.5A13.5 13.5 0 0129.5 16 13.5 13.5 0 0116 29.5 13.5 13.5 0 012.5 16 13.5 13.5 0 0116 2.5z"/><path class="ct-icon-plane" d="M21.26 10c-.664-.024-1.67.498-2.575 1.398l-1.951 1.94-5.846-1.963-1.14 1.177 4.408 3.35-.986.98c-.349.346-.64.71-.87 1.066l-2.652-.197-.648.656 2.641 1.956L13.571 23l.652-.653-.196-2.679c.334-.22.677-.494 1.005-.822l1.038-1.031 3.382 4.442 1.177-1.14-1.973-5.875 1.89-1.879c1.207-1.2 1.763-2.603 1.248-3.149-.13-.136-.313-.205-.534-.213z"/></g></svg>
diff --git a/pkg/shell/images/server-error.png b/pkg/shell/images/server-error.png
new file mode 100644
index 0000000..3ac6db5
--- /dev/null
+++ b/pkg/shell/images/server-error.png
Binary files differ
diff --git a/pkg/shell/images/server-large.png b/pkg/shell/images/server-large.png
new file mode 100644
index 0000000..7d429b3
--- /dev/null
+++ b/pkg/shell/images/server-large.png
Binary files differ
diff --git a/pkg/shell/images/server-small.png b/pkg/shell/images/server-small.png
new file mode 100644
index 0000000..2837870
--- /dev/null
+++ b/pkg/shell/images/server-small.png
Binary files differ
diff --git a/pkg/shell/index.html b/pkg/shell/index.html
new file mode 100644
index 0000000..96a76a8
--- /dev/null
+++ b/pkg/shell/index.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html id="shell-page" class="index-page pf-v5-theme-dark">
+ <head>
+ <title>Cockpit</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="shell.css" rel="stylesheet" />
+ <link href="../../static/branding.css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../manifests.js"></script>
+ <!-- HACK: C bridge loads translations via glob and Python via manifest.js -->
+ <script src="../*/po.manifest.js"></script>
+ <script src="../*/po.js"></script>
+ <script src="po.js"></script>
+ </head>
+ <body class="pf-v5-m-tabular-nums" hidden="true">
+ <div id="main" class="page">
+ <a class="screenreader-text skiplink desktop_v" href="#content" translate="yes">Skip to content</a>
+ <a class="screenreader-text skiplink desktop_v" href="#hosts-sel" translate="yes">Skip main navigation</a>
+
+ <div id="sidebar-toggle" class="pf-v5-c-select pf-m-dark sidebar-toggle">
+ </div>
+
+ <div id="nav-system" class="area-ct-subnav nav-system-menu sidebar interact">
+ <nav id="host-apps" class="host-apps">
+ <!-- Navigation goes here !-->
+ </nav>
+ </div>
+
+ <nav id="hosts-sel" class="navbar navbar-default navbar-pf navbar-pf-vertical" tabindex="-1">
+ <!-- Hosts selector goes here !-->
+ </nav>
+
+ <div id="nav-hosts" class="area-ct-subnav nav-hosts-menu sidebar">
+ <!-- Hosts go here !-->
+ </div>
+
+ <div id="topnav" class="header">
+ <!-- Top navigation goes here !-->
+ </div>
+
+
+ <div id="troubleshoot-dialog"></div>
+ <div id="session-timeout-dialog"></div>
+
+ <div id="content" role="main" class="area-ct-content" tabindex="-1">
+ <!-- This is where the iframes appear -->
+ <div id="early-failure-ready" class="curtains-ct" hidden="true"></div>
+ </div>
+ </div>
+
+ <div id="early-failure" class="early-failure" hidden="true">
+ </div>
+
+ <script src="shell.js"></script>
+ </body>
+</html>
diff --git a/pkg/shell/indexes.jsx b/pkg/shell/indexes.jsx
new file mode 100644
index 0000000..2639765
--- /dev/null
+++ b/pkg/shell/indexes.jsx
@@ -0,0 +1,623 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import 'cockpit-dark-theme'; // once per page
+import cockpit from "cockpit";
+import React from "react";
+import { createRoot } from "react-dom/client";
+
+import { CockpitNav, CockpitNavItem, SidebarToggle } from "./nav.jsx";
+import { TopNav } from ".//topnav.jsx";
+import { CockpitHosts } from "./hosts.jsx";
+import { codes, HostModal } from "./hosts_dialog.jsx";
+import { EarlyFailure, EarlyFailureReady } from './failures.jsx';
+import { WithDialogs } from "dialogs.jsx";
+
+import * as base_index from "./base_index";
+
+const _ = cockpit.gettext;
+
+function MachinesIndex(index_options, machines, loader) {
+ if (!index_options)
+ index_options = {};
+
+ const root = id => createRoot(document.getElementById(id));
+
+ // Document is guaranteed to be loaded at this point.
+ const sidebar_toggle_root = root('sidebar-toggle');
+ const early_failure_root = root('early-failure');
+ const early_failure_ready_root = root('early-failure-ready');
+ const topnav_root = root('topnav');
+ const hosts_sel_root = root('hosts-sel');
+ let host_apps_root = null;
+
+ const page_status = { };
+ sessionStorage.removeItem("cockpit:page_status");
+
+ index_options.navigate = function (state, sidebar) {
+ return navigate(state, sidebar);
+ };
+ index_options.handle_notifications = function (host, page, data) {
+ if (data.page_status !== undefined) {
+ if (!page_status[host])
+ page_status[host] = { };
+ page_status[host][page] = data.page_status;
+ sessionStorage.setItem("cockpit:page_status", JSON.stringify(page_status));
+ // Just for triggering an "updated" event
+ machines.overlay(host, { });
+ }
+ };
+
+ const index = base_index.new_index_from_proto(index_options);
+
+ /* Restarts */
+ index.addEventListener("expect_restart", (ev, host) => loader.expect_restart(host));
+
+ /* Disconnection Dialog */
+ let watchdog_problem = null;
+ index.addEventListener("disconnect", (ev, problem) => {
+ watchdog_problem = problem;
+ show_disconnected();
+ });
+
+ index.addEventListener("update", () => {
+ update_topbar();
+ });
+
+ /* Is troubleshooting dialog open */
+ let troubleshooting_opened = false;
+
+ sidebar_toggle_root.render(<SidebarToggle />);
+
+ // Focus with skiplinks
+ const skiplinks = document.getElementsByClassName("skiplink");
+ Array.from(skiplinks).forEach(skiplink => {
+ skiplink.addEventListener("click", ev => {
+ document.getElementById(ev.target.hash.substring(1)).focus();
+ return false;
+ });
+ });
+
+ let current_user = "";
+ cockpit.user().then(user => {
+ current_user = user.name || "";
+ });
+
+ /* Navigation */
+ let ready = false;
+ function on_ready() {
+ ready = true;
+ index.ready();
+ }
+
+ function preload_frames () {
+ for (const m of machines.list)
+ index.preload_frames(m, m.manifests);
+ }
+
+ /* When the machine list is ready we start processing navigation */
+ machines.addEventListener("ready", on_ready);
+ machines.addEventListener("removed", (ev, machine) => {
+ index.frames.remove(machine);
+ update_machines();
+ });
+ ["added", "updated"].forEach(evn => {
+ machines.addEventListener(evn, (ev, machine) => {
+ if (!machine.visible)
+ index.frames.remove(machine);
+ else if (machine.problem)
+ index.frames.remove(machine);
+
+ update_machines();
+ preload_frames();
+ if (ready)
+ navigate();
+ });
+ });
+
+ if (machines.ready)
+ on_ready();
+
+ function show_disconnected() {
+ if (!ready) {
+ document.getElementById("early-failure-ready").setAttribute("hidden", "hidden");
+ document.getElementById("early-failure").removeAttribute("hidden");
+
+ const ca_cert_url = window.sessionStorage.getItem("CACertUrl");
+ early_failure_root.render(<EarlyFailure ca_cert_url={
+ (window.navigator.userAgent.indexOf("Safari") >= 0 && ca_cert_url) ? ca_cert_url : undefined
+ } />);
+ document.getElementById("main").setAttribute("hidden", "hidden");
+ document.body.removeAttribute("hidden");
+ return;
+ }
+
+ const current_frame = index.current_frame();
+
+ if (current_frame)
+ current_frame.setAttribute("hidden", "hidden");
+
+ document.getElementById("early-failure").setAttribute("hidden", "hidden");
+ document.getElementById("early-failure-ready").removeAttribute("hidden");
+
+ early_failure_ready_root.render(
+ <EarlyFailureReady title={_("Disconnected")}
+ reconnect
+ watchdog_problem={watchdog_problem}
+ navigate={navigate}
+ paragraph={cockpit.message(watchdog_problem)} />);
+ }
+
+ /* Handles navigation */
+ function navigate(state, reconnect) {
+ /* If this is a watchdog problem or we are troubleshooting
+ * let the dialog handle it */
+ if (watchdog_problem || troubleshooting_opened)
+ return;
+
+ if (!state)
+ state = index.retrieve_state();
+ let machine = machines.lookup(state.host);
+
+ /* No such machine */
+ if (!machine) {
+ machine = {
+ key: state.host,
+ address: state.host,
+ label: state.host,
+ state: "failed",
+ problem: "not-found",
+ };
+
+ /* Asked to reconnect to the machine */
+ } else if (!machine.visible) {
+ machine.state = "failed";
+ machine.problem = "not-found";
+ } else if (reconnect) {
+ loader.connect(state.host);
+ }
+
+ const compiled = compile(machine);
+ if (machine.manifests && !state.component)
+ state.component = choose_component(state, compiled);
+
+ update_navbar(machine, state, compiled);
+ update_topbar(machine, state, compiled);
+ update_frame(machine, state, compiled);
+
+ /* Just replace the state, and URL */
+ index.jump(state, true);
+ }
+
+ function choose_component(state, compiled) {
+ /* Go for the first item */
+ const menu_items = compiled.ordered("menu");
+ if (menu_items.length > 0 && menu_items[0])
+ return menu_items[0].path;
+
+ return "system";
+ }
+
+ function update_topbar(machine, state, compiled) {
+ if (!state)
+ state = index.retrieve_state();
+
+ if (!machine)
+ machine = machines.lookup(state.host);
+
+ if (!compiled)
+ compiled = compile(machine);
+
+ topnav_root.render(
+ <WithDialogs>
+ <TopNav index={index} state={state} machine={machine} compiled={compiled} />
+ </WithDialogs>);
+ }
+
+ function update_navbar(machine, state, compiled) {
+ if (!state)
+ state = index.retrieve_state();
+
+ if (!machine)
+ machine = machines.lookup(state.host);
+
+ if (!machine || machine.state != "connected") {
+ if (host_apps_root) {
+ host_apps_root.unmount();
+ host_apps_root = null;
+ }
+ return;
+ }
+
+ if (!compiled)
+ compiled = compile(machine);
+
+ if (machine.address !== "localhost") {
+ document.getElementById("main").style.setProperty('--ct-color-host-accent', machine.color);
+ } else {
+ // Remove property to fall back to default accent color
+ document.getElementById("main").style.removeProperty('--ct-color-host-accent');
+ }
+
+ const component_manifest = find_component(state, compiled);
+
+ // Filtering of navigation by term
+ function keyword_filter(item, term) {
+ function keyword_relevance(current_best, item) {
+ const translate = item.translate || false;
+ const weight = item.weight || 0;
+ let score;
+ let _m = "";
+ let best = { score: -1 };
+ item.matches.forEach(m => {
+ if (translate)
+ _m = _(m);
+ score = -1;
+ // Best score when starts in translate language
+ if (translate && _m.indexOf(term) == 0)
+ score = 4 + weight;
+ // Second best score when starts in English
+ else if (m.indexOf(term) == 0)
+ score = 3 + weight;
+ // Substring consider only when at least 3 letters were used
+ else if (term.length >= 3) {
+ if (translate && _m.indexOf(term) >= 0)
+ score = 2 + weight;
+ else if (m.indexOf(term) >= 0)
+ score = 1 + weight;
+ }
+ if (score > best.score) {
+ best = { keyword: m, score };
+ }
+ });
+ if (best.score > current_best.score) {
+ current_best = { keyword: best.keyword, score: best.score, goto: item.goto || null };
+ }
+ return current_best;
+ }
+
+ const new_item = Object.assign({}, item);
+ new_item.keyword = { score: -1 };
+ if (!term)
+ return new_item;
+ const best_keyword = new_item.keywords.reduce(keyword_relevance, { score: -1 });
+ if (best_keyword.score > -1) {
+ new_item.keyword = best_keyword;
+ return new_item;
+ }
+ return null;
+ }
+
+ // Rendering of separate navigation menu items
+ function nav_item(component, term) {
+ const active = component_manifest === component.path;
+
+ // Parse path
+ let path = component.path;
+ let hash = component.hash;
+ if (component.keyword.goto) {
+ if (component.keyword.goto[0] === "/")
+ path = component.keyword.goto.substr(1);
+ else
+ hash = component.keyword.goto;
+ }
+
+ // Parse page status
+ let status = null;
+ if (page_status[machine.key])
+ status = page_status[machine.key][component.path];
+
+ return React.createElement(CockpitNavItem, {
+ key: component.label,
+ name: component.label,
+ active,
+ status,
+ keyword: component.keyword.keyword,
+ term,
+ to: index.href({ host: machine.address, component: path, hash }),
+ jump: index.jump,
+ });
+ }
+
+ const groups = [
+ {
+ name: _("Apps"),
+ items: compiled.ordered("dashboard"),
+ }, {
+ name: _("System"),
+ items: compiled.ordered("menu"),
+ }, {
+ name: _("Tools"),
+ items: compiled.ordered("tools"),
+ }
+ ].filter(i => i.items.length > 0);
+
+ if (compiled.items.apps && groups.length === 3)
+ groups[0].action = { label: _("Edit"), path: index.href({ host: machine.address, component: compiled.items.apps.path }) };
+
+ if (!host_apps_root)
+ host_apps_root = root('host-apps');
+ host_apps_root.render(
+ React.createElement(CockpitNav, {
+ groups,
+ selector: "host-apps",
+ item_render: nav_item,
+ filtering: keyword_filter,
+ sorting: (a, b) => { return b.keyword.score - a.keyword.score },
+ current: state.component,
+ jump: index.jump,
+ }));
+
+ update_machines(state, machine);
+ }
+
+ function update_machines(state, machine) {
+ if (!state)
+ state = index.retrieve_state();
+
+ if (!machine)
+ machine = machines.lookup(state.host);
+
+ hosts_sel_root.render(
+ React.createElement(CockpitHosts, {
+ machine: machine || {},
+ machines,
+ selector: "nav-hosts",
+ hostAddr: index.href,
+ jump: index.jump,
+ }));
+ }
+
+ function update_title(label, machine) {
+ if (label)
+ label += " - ";
+ else
+ label = "";
+ let suffix = index.default_title;
+
+ if (machine) {
+ if (machine.address === "localhost") {
+ const compiled = compile(machine);
+ if (compiled.ordered("menu").length || compiled.ordered("tools").length)
+ suffix = (machine.user || current_user) + "@" + machine.label;
+ } else {
+ suffix = (machine.user || current_user) + "@" + machine.label;
+ }
+ }
+
+ document.title = label + suffix;
+ }
+
+ function find_component(state, compiled) {
+ let component = state.component;
+ // If `state.component` is not known to any manifest, find where it comes from
+ if (compiled.items[state.component] === undefined) {
+ let s = state.component;
+ while (s && compiled.items[s] === undefined)
+ s = s.substring(0, s.lastIndexOf("/"));
+ component = s;
+ }
+
+ // Still don't know where it comes from, check for parent
+ if (!component) {
+ const comp = cockpit.manifests[state.component];
+ if (comp && comp.parent)
+ return comp.parent.component;
+ }
+
+ return component;
+ }
+
+ let troubleshoot_dialog_root = null;
+
+ function update_frame(machine, state, compiled) {
+ function render_troubleshoot() {
+ troubleshooting_opened = true;
+ const template = codes[machine.problem] || "change-port";
+ if (!troubleshoot_dialog_root)
+ troubleshoot_dialog_root = root('troubleshoot-dialog');
+ troubleshoot_dialog_root.render(React.createElement(HostModal, {
+ template,
+ address: machine.address,
+ machines_ins: machines,
+ onClose: () => {
+ troubleshoot_dialog_root.unmount();
+ troubleshoot_dialog_root = null;
+ troubleshooting_opened = false;
+ navigate(null, true);
+ }
+ }));
+ }
+
+ let current_frame = index.current_frame();
+
+ if (machine.state != "connected") {
+ if (current_frame)
+ current_frame.setAttribute("hidden", "hidden");
+ current_frame = null;
+ index.current_frame(current_frame);
+
+ const connecting = (machine.state == "connecting");
+ let title, message;
+ if (machine.restarting) {
+ title = _("The machine is rebooting");
+ message = "";
+ } else if (connecting) {
+ title = _("Connecting to the machine");
+ message = "";
+ } else {
+ title = _("Not connected to host");
+ if (machine.problem == "not-found") {
+ message = _("Cannot connect to an unknown host");
+ } else {
+ const error = machine.problem || machine.state;
+ if (error)
+ message = cockpit.message(error);
+ else
+ message = "";
+ }
+ }
+
+ let troubleshooting = false;
+
+ if (!machine.restarting && (machine.problem === "no-host" || !!codes[machine.problem])) {
+ troubleshooting = true;
+ }
+
+ const restarting = !!machine.restarting;
+ const reconnect = !connecting && machine.problem != "not-found" && !troubleshooting;
+
+ document.querySelector("#early-failure-ready").removeAttribute("hidden");
+ early_failure_ready_root.render(
+ <EarlyFailureReady loading={connecting || restarting}
+ title={title}
+ reconnect={reconnect}
+ troubleshoot={troubleshooting}
+ onTroubleshoot={render_troubleshoot}
+ watchdog_problem={watchdog_problem}
+ navigate={navigate}
+ paragraph={message} />);
+
+ update_title(null, machine);
+
+ /* Fall through when connecting, and allow frame to load at same time */
+ if (!connecting)
+ return;
+ }
+
+ let hash = state.hash;
+ let component = state.component;
+
+ /* Old cockpit packages, used to be in shell/shell.html */
+ if (machine && compiled.compat) {
+ const compat = compiled.compat[component];
+ if (compat) {
+ component = "shell/shell";
+ hash = compat;
+ }
+ }
+
+ const frame = component ? index.frames.lookup(machine, component, hash) : undefined;
+ if (frame != current_frame) {
+ if (current_frame) {
+ current_frame.style.display = "none";
+ // Reset 'data-active' only on the same host
+ if (frame.getAttribute('data-host') === current_frame.getAttribute('data-host'))
+ current_frame.setAttribute('data-active', 'false');
+ }
+ index.current_frame(frame);
+ }
+
+ if (machine.state == "connected") {
+ document.querySelector("#early-failure-ready").setAttribute("hidden", "hidden");
+ frame.style.display = "block";
+ frame.setAttribute('data-active', 'true');
+ frame.removeAttribute("hidden");
+
+ const component_manifest = find_component(state, compiled);
+ const item = compiled.items[component_manifest];
+ const label = item ? item.label : "";
+ update_title(label, machine);
+ if (label)
+ frame.setAttribute('title', label);
+ }
+ }
+
+ function compatibility(machine) {
+ if (!machine.manifests || machine.address === "localhost")
+ return null;
+
+ const shell = machine.manifests.shell || { };
+ const menu = shell.menu || { };
+ const tools = shell.tools || { };
+
+ const mapping = { };
+
+ /* The following were included in shell/shell.html in old versions */
+ if ("_host_" in menu)
+ mapping["system/host"] = "/server";
+ if ("_init_" in menu)
+ mapping["system/init"] = "/services";
+ if ("_network_" in menu)
+ mapping["network/interfaces"] = "/networking";
+ if ("_storage_" in menu)
+ mapping["storage/devices"] = "/storage";
+ if ("_users_" in tools)
+ mapping["users/local"] = "/accounts";
+
+ /* For Docker we have to guess ... some heuristics */
+ if ("_storage_" in menu || "_init_" in menu)
+ mapping["docker/containers"] = "/containers";
+
+ return mapping;
+ }
+
+ function compile(machine) {
+ const compiled = base_index.new_compiled();
+ compiled.load(machine.manifests, "tools");
+ compiled.load(machine.manifests, "dashboard");
+ compiled.load(machine.manifests, "menu");
+ compiled.compat = compatibility(machine);
+ return compiled;
+ }
+
+ cockpit.transport.wait(function() {
+ index.start();
+ });
+}
+
+function message_queue(event) {
+ window.messages.push(event);
+}
+
+/* When we're being loaded into the index window we have additional duties */
+if (document.documentElement.classList.contains("index-page")) {
+ /* Indicates to child frames that we are a cockpit1 router frame */
+ window.name = "cockpit1";
+
+ /* The same thing as above, but compatibility with old cockpit */
+ window.options = { sink: true, protocol: "cockpit1" };
+
+ /* Tell the pages about our features. */
+ window.features = {
+ navbar_is_for_current_machine: true
+ };
+
+ /* While the index is initializing, snag any messages we receive from frames */
+ window.messages = [];
+
+ window.messages.cancel = function() {
+ window.removeEventListener("message", message_queue, false);
+ window.messages = null;
+ };
+
+ let language = document.cookie.replace(/(?:(?:^|.*;\s*)CockpitLang\s*=\s*([^;]*).*$)|^.*$/, "$1");
+ if (!language)
+ language = navigator.language.toLowerCase(); // Default to Accept-Language header
+
+ document.documentElement.lang = language;
+ if (cockpit.language_direction)
+ document.documentElement.dir = cockpit.language_direction;
+
+ window.addEventListener("message", message_queue, false);
+}
+
+export function machines_index(options, machines_ins, loader) {
+ return new MachinesIndex(options, machines_ins, loader);
+}
diff --git a/pkg/shell/machines/machines.js b/pkg/shell/machines/machines.js
new file mode 100644
index 0000000..b48ae95
--- /dev/null
+++ b/pkg/shell/machines/machines.js
@@ -0,0 +1,792 @@
+import cockpit from "cockpit";
+
+import ssh_add_key_sh from "./ssh-add-key.sh";
+
+const mod = { };
+
+/*
+ * We share the Machines state between multiple frames. Only
+ * one frame has the job of loading the state, usually index.js
+ * The Loader code below does all the loading.
+ *
+ * The data is stored in sessionStorage in a JSON object, like this
+ * {
+ * content: name → info dict from bridge's /machines Machines property
+ * overlay: extra data to augment and override on top of content
+ * }
+ *
+ * This uses sessionStorage rather than cockpit.sessionStorage
+ * because we don't ever want to write unprefixed keys.
+ */
+
+const key = cockpit.sessionStorage.prefixedKey("v2-machines.json");
+const session_prefix = cockpit.sessionStorage.prefixedKey("v1-session-machine");
+
+function generate_session_key(host) {
+ return session_prefix + "/" + host;
+}
+
+export function host_superuser_storage_key(host) {
+ if (!host)
+ host = cockpit.transport.host;
+
+ const local_key = window.localStorage.getItem("superuser-key");
+ if (host == "localhost")
+ return local_key;
+ else if (host.indexOf("@") >= 0)
+ return "superuser:" + host;
+ else if (local_key)
+ return local_key + "@" + host;
+ else
+ return null;
+}
+
+export function get_init_superuser_for_options(options) {
+ let value = null;
+ const key = host_superuser_storage_key(options.host);
+ if (key)
+ value = window.localStorage.getItem(key);
+
+ /* When connecting, we can optionally try to start a privileged
+ * bridge immediately. However, it is quite likely that that
+ * needs a password and if we don't have one, it will likely fail.
+ * That would be okay, but sudo is very noisy about failures and
+ * might send nasty emails to your parents. For that reason we
+ * pass "init-superuser": "none" here when there is no password.
+ *
+ * The downside is that if sudo is configured to not require a
+ * password, we could start it successfully immediately as part
+ * of the connection process, which would be convenient. However,
+ * if sudo works without password, gaining admin privs is just a
+ * single click, and the convenience loss is not that of a big deal,
+ * hopefully.
+ */
+
+ if (value == "sudo" && !options.password)
+ value = "none";
+
+ return value;
+}
+
+function Machines() {
+ const self = this;
+
+ cockpit.event_target(self);
+
+ let flat = null;
+ self.ready = false;
+
+ /* parsed machine data */
+ const machines = { };
+
+ /* Data shared between Machines() instances */
+ let last = {
+ content: null,
+ overlay: {
+ localhost: {
+ visible: true,
+ manifests: cockpit.manifests
+ }
+ }
+ };
+
+ function storage(ev) {
+ if (ev.key === key && ev.storageArea === window.sessionStorage)
+ refresh(JSON.parse(ev.newValue || "null"));
+ }
+
+ window.addEventListener("storage", storage);
+
+ window.setTimeout(function() {
+ const value = window.sessionStorage.getItem(key);
+ if (!self.ready && value)
+ refresh(JSON.parse(value));
+ });
+
+ let timeout = null;
+
+ function sync(machine, values, overlay) {
+ const desired = { ...values, ...overlay };
+ for (const prop in desired) {
+ if (machine[prop] !== desired[prop])
+ machine[prop] = desired[prop];
+ }
+ for (const prop in machine) {
+ if (machine[prop] !== desired[prop])
+ delete machine[prop];
+ }
+ return machine;
+ }
+
+ function refresh(shared, push) {
+ if (!shared)
+ return;
+
+ last = shared;
+ flat = null;
+
+ if (push && !timeout) {
+ timeout = window.setTimeout(function() {
+ timeout = null;
+ window.sessionStorage.setItem(key, JSON.stringify(last));
+ }, 10);
+ }
+
+ const hosts = { };
+ const content = shared.content || { };
+ const overlay = shared.overlay || { };
+ for (const host in content)
+ hosts[host] = true;
+ for (const host in overlay)
+ hosts[host] = true;
+
+ const events = [];
+
+ for (const host in hosts) {
+ const old_machine = machines[host] || { };
+ const old_conns = old_machine.connection_string;
+
+ /* Invert logic for color, always respect what's on disk */
+ if (content[host] && content[host].color && overlay[host])
+ delete overlay[host].color;
+
+ const machine = sync(old_machine, content[host], overlay[host]);
+
+ /* Fill in defaults */
+ machine.key = host;
+ if (!machine.address)
+ machine.address = host;
+
+ machine.connection_string = self.generate_connection_string(machine.user,
+ machine.port,
+ machine.address);
+
+ if (!machine.label) {
+ if (host == "localhost" || host == "localhost.localdomain") {
+ const application = cockpit.transport.application();
+ if (application.indexOf('cockpit+=') === 0)
+ machine.label = application.replace('cockpit+=', '');
+ else
+ machine.label = window.location.hostname;
+ } else {
+ machine.label = host;
+ }
+ }
+ if (!machine.avatar)
+ machine.avatar = "../shell/images/server-small.png";
+
+ events.push([host in machines ? "updated" : "added",
+ [machine, host, old_conns]]);
+ machines[host] = machine;
+ }
+
+ /* Remove any lost hosts */
+ for (const host in machines) {
+ if (!(host in hosts)) {
+ const machine = machines[host];
+ delete machines[host];
+ delete overlay[host];
+ events.push(["removed", [machine, host]]);
+ }
+ }
+
+ /* Fire off all events */
+ const len = events.length;
+ for (let i = 0; i < len; i++) {
+ self.dispatchEvent(events[i][0], ...events[i][1]);
+ }
+ }
+
+ function update_session_machine(machine, host, values) {
+ /* We don't save the whole machine object */
+ const skey = generate_session_key(host);
+ const data = { ...machine, ...values };
+ window.sessionStorage.setItem(skey, JSON.stringify(data));
+ self.overlay(host, values);
+ return cockpit.when([]);
+ }
+
+ function update_saved_machine(host, values) {
+ // wrap values in variants for D-Bus call; at least values.port can
+ // be int or string, so stringify everything but the "visible" boolean
+ const values_variant = {};
+ for (const prop in values) {
+ if (values[prop] !== null) {
+ if (prop == "visible")
+ values_variant[prop] = cockpit.variant('b', values[prop]);
+ else
+ values_variant[prop] = cockpit.variant('s', values[prop].toString());
+ }
+ }
+
+ // FIXME: investigate re-using the proxy from Loader (runs in different frame/scope)
+ const bridge = cockpit.dbus(null, { bus: "internal", superuser: "require" });
+ const mod =
+ bridge.call("/machines", "cockpit.Machines", "Update", ["99-webui.json", host, values_variant])
+ .catch(error => {
+ // avoid make noise when we are not superuser
+ if (error.problem !== "access-denied")
+ console.error("failed to call cockpit.Machines.Update(): ", JSON.stringify(error));
+ })
+ .then(() => self.overlay(host, values));
+
+ return mod;
+ }
+
+ self.set_ready = function ready() {
+ if (!self.ready) {
+ self.ready = true;
+ self.dispatchEvent("ready");
+ }
+ };
+
+ self.add_key = function(host_key) {
+ return cockpit.script(ssh_add_key_sh, [host_key.trim(), "known_hosts"], { err: "message" });
+ };
+
+ self.add = function add(connection_string, color) {
+ let values = self.split_connection_string(connection_string);
+ const host = values.address;
+
+ values = {
+ visible: true,
+ color: color || self.unused_color(),
+ ...values
+ };
+
+ const machine = self.lookup(host);
+ if (machine)
+ machine.on_disk = true;
+
+ return self.change(values.address, values);
+ };
+
+ self.unused_color = function unused_color() {
+ const len = mod.colors.length;
+ for (let i = 0; i < len; i++) {
+ if (!color_in_use(mod.colors[i]))
+ return mod.colors[i];
+ }
+ return "gray";
+ };
+
+ function color_in_use(color) {
+ const norm = mod.colors.parse(color);
+ for (const key in machines) {
+ const machine = machines[key];
+ if (machine.color && mod.colors.parse(machine.color) == norm)
+ return true;
+ }
+ return false;
+ }
+
+ function merge(item, values) {
+ for (const prop in values) {
+ if (values[prop] === null)
+ delete item[prop];
+ else
+ item[prop] = values[prop];
+ }
+ }
+
+ self.change = function change(host, values) {
+ const machine = self.lookup(host);
+
+ if (machine && !machine.on_disk)
+ return update_session_machine(machine, host, values);
+ else
+ return update_saved_machine(host, values);
+ };
+
+ self.data = function data(content) {
+ const changes = {};
+
+ for (const host in content) {
+ changes[host] = { ...last.overlay[host] };
+ merge(changes[host], { on_disk: true });
+ }
+
+ /* It's a full reload, so data not
+ * present is no longer from disk
+ */
+ for (const host in machines) {
+ if (content && !content[host]) {
+ changes[host] = { ...last.overlay[host] };
+ merge(changes[host], { on_disk: null });
+ }
+ }
+
+ refresh({
+ content,
+ overlay: { ...last.overlay, ...changes },
+ }, true);
+ };
+
+ self.overlay = function overlay(host, values) {
+ const address = self.split_connection_string(host).address;
+ const changes = { };
+ changes[address] = { ...last.overlay[address] };
+ merge(changes[address], values);
+ refresh({
+ content: last.content,
+ overlay: { ...last.overlay, ...changes }
+ }, true);
+ };
+
+ Object.defineProperty(self, "list", {
+ enumerable: true,
+ get: function get() {
+ if (!flat) {
+ flat = [];
+ for (const key in machines) {
+ if (machines[key].visible)
+ flat.push(machines[key]);
+ }
+ flat.sort(function(m1, m2) {
+ return m1.label.localeCompare(m2.label);
+ });
+ }
+ return flat;
+ }
+ });
+
+ Object.defineProperty(self, "addresses", {
+ enumerable: true,
+ get: function get() {
+ return Object.keys(machines);
+ }
+ });
+
+ self.lookup = function lookup(address) {
+ const parts = self.split_connection_string(address);
+ return machines[parts.address || "localhost"] || null;
+ };
+
+ self.generate_connection_string = function (user, port, addr) {
+ let address = addr;
+ if (user)
+ address = user + "@" + address;
+
+ if (port)
+ address = address + ":" + port;
+
+ return address;
+ };
+
+ self.split_connection_string = function(conn_to) {
+ const parts = {};
+ let user_spot = -1;
+ let port_spot = -1;
+
+ if (conn_to) {
+ if (conn_to.substring(0, 6) === "ssh://")
+ conn_to = conn_to.substring(6);
+ user_spot = conn_to.lastIndexOf('@');
+ port_spot = conn_to.lastIndexOf(':');
+ }
+
+ if (user_spot > 0) {
+ parts.user = conn_to.substring(0, user_spot);
+ conn_to = conn_to.substring(user_spot + 1);
+ port_spot = conn_to.lastIndexOf(':');
+ }
+
+ if (port_spot > -1) {
+ const port = parseInt(conn_to.substring(port_spot + 1), 10);
+ if (!isNaN(port)) {
+ parts.port = port;
+ conn_to = conn_to.substring(0, port_spot);
+ }
+ }
+
+ parts.address = conn_to;
+ return parts;
+ };
+
+ self.close = function close() {
+ window.removeEventListener("storage", storage);
+ };
+}
+
+function Loader(machines, session_only) {
+ const self = this;
+
+ /* Have we loaded from cockpit session */
+ let session_loaded = false;
+
+ /* echo channels to each machine */
+ const channels = { };
+ const channels_listeners_message = { };
+ const channels_listeners_close = { };
+
+ /* hostnamed proxies to each machine, if hostnamed available */
+ const proxies = { };
+ const proxies_listeners_changed = { };
+
+ /* clients for the bridge D-Bus API */
+ const bridge_dbus = { };
+
+ function process_session_key(key, value) {
+ const parts = key.split("/");
+ if (parts[0] == session_prefix &&
+ parts.length === 2) {
+ const host = parts[1];
+ if (value) {
+ const values = JSON.parse(value);
+ const machine = machines.lookup(host);
+ if (!machine || !machine.on_disk)
+ machines.overlay(host, values);
+ else if (!machine.visible)
+ machines.change(host, { visible: true });
+ self.connect(host);
+ }
+ }
+ }
+
+ function load_from_session_storage() {
+ session_loaded = true;
+ for (let i = 0; i < window.sessionStorage.length; i++) {
+ const k = window.sessionStorage.key(i);
+ process_session_key(k, window.sessionStorage.getItem(k));
+ }
+ }
+
+ function process_session_machines(ev) {
+ if (ev.storageArea === window.sessionStorage)
+ process_session_key(ev.key || "", ev.newValue);
+ }
+ window.addEventListener("storage", process_session_machines);
+
+ function state(host, value, problem) {
+ const values = { state: value, problem };
+ if (value == "connected") {
+ values.restarting = false;
+ } else if (problem) {
+ values.manifests = null;
+ values.checksum = null;
+ if (problem == "authentication-failed" || problem == "authentication-not-supported")
+ values.restarting = false;
+ }
+ machines.overlay(host, values);
+ }
+
+ machines.addEventListener("added", updated);
+ machines.addEventListener("updated", updated);
+ machines.addEventListener("removed", removed);
+
+ function updated(ev, machine, host, old_conns) {
+ if (!machine) {
+ machine = machines.lookup(host);
+ if (!machine)
+ return;
+ }
+
+ let props = proxies[host];
+ if (!props || !props.valid)
+ props = { };
+
+ const overlay = { };
+
+ if (!machine.color)
+ overlay.color = machines.unused_color();
+
+ const label = props.PrettyHostname || props.StaticHostname || props.Hostname;
+ if (label && label !== machine.label)
+ overlay.label = label;
+
+ const os = props.OperatingSystemPrettyName;
+ if (os && os != machine.os)
+ overlay.os = props.OperatingSystemPrettyName;
+
+ if (Object.keys(overlay).length > 0)
+ machines.overlay(host, overlay);
+
+ /* Don't automatically reconnect failed machines, and don't
+ * automatically connect to new machines. The navigation will
+ * explicitly connect as necessary.
+ */
+ if (machine.visible) {
+ if (old_conns && machine.connection_string != old_conns) {
+ cockpit.kill(old_conns);
+ self.disconnect(host);
+ self.connect(host);
+ }
+ } else {
+ self.disconnect(host);
+ }
+ }
+
+ function removed(ev, machine, host) {
+ self.disconnect(host);
+ }
+
+ self.connect = function connect(host) {
+ const machine = machines.lookup(host);
+ if (!machine)
+ return;
+
+ let channel = channels[host];
+ if (channel)
+ return;
+
+ const options = {
+ host: machine.connection_string,
+ payload: "echo",
+ };
+
+ options["init-superuser"] = get_init_superuser_for_options(options);
+
+ if (!machine.on_disk && machine.host_key) {
+ options['temp-session'] = false; /* Compatibility option */
+ options.session = 'shared';
+ options['host-key'] = machine.host_key;
+ }
+
+ channel = cockpit.channel(options);
+ channels[host] = channel;
+
+ const local = host === "localhost";
+
+ /* Request is null, and message is true when connected */
+ let request = null;
+ let open = local;
+
+ let url;
+ if (!machine.manifests) {
+ if (machine.checksum)
+ url = "../../" + machine.checksum + "/manifests.json";
+ else
+ url = "../../@" + encodeURI(machine.connection_string) + "/manifests.json";
+ }
+
+ function whirl() {
+ if (!request && open)
+ state(host, "connected", null);
+ else
+ state(host, "connecting", null);
+ }
+
+ /* Here we load the machine manifests, and expect them before going to "connected" */
+ function request_manifest() {
+ request = new XMLHttpRequest();
+ request.responseType = "json";
+ request.open("GET", url, true);
+ request.addEventListener("load", () => {
+ const overlay = { manifests: request.response };
+ const etag = request.getResponseHeader("ETag");
+ if (etag) /* and remove quotes */
+ overlay.checksum = etag.replace(/^"(.+)"$/, '$1');
+ machines.overlay(host, overlay);
+
+ request = null;
+ whirl();
+ });
+ request.addEventListener("error", () => {
+ console.warn("failed to load manifests from " + machine.connection_string);
+ request = null;
+ whirl();
+ });
+ request.send();
+ }
+
+ /* Try to get change notifications via the internal
+ /packages D-Bus interface of the bridge. Not all
+ bridges support this API, so we still get the first
+ version of the manifests via HTTP in request_manifest.
+ */
+
+ function watch_manifests() {
+ const dbus = cockpit.dbus(null, {
+ bus: "internal",
+ host: machine.connection_string
+ });
+ bridge_dbus[host] = dbus;
+ dbus.subscribe({
+ path: "/packages",
+ interface: "org.freedesktop.DBus.Properties",
+ member: "PropertiesChanged"
+ },
+ function (path, iface, mamber, args) {
+ if (args[0] == "cockpit.Packages") {
+ if (args[1].Manifests) {
+ const manifests = JSON.parse(args[1].Manifests.v);
+ machines.overlay(host, { manifests });
+ }
+ }
+ });
+
+ /* Tell the bridge to reload the packages, but only if
+ it hasn't just started. Thus, nothing happens on
+ the first login, but if you reload the shell, we
+ will also reload the packages.
+ */
+ dbus.call("/packages", "cockpit.Packages", "ReloadHint", []);
+ }
+
+ function request_hostname() {
+ if (!machine.static_hostname) {
+ const proxy = cockpit.dbus("org.freedesktop.hostname1",
+ { host: machine.connection_string }).proxy();
+ proxies[host] = proxy;
+ proxy.wait(function() {
+ proxies_listeners_changed[host] = () => updated(null, null, host);
+ proxy.addEventListener("changed", proxies_listeners_changed[host]);
+ updated(null, null, host);
+ });
+ }
+ }
+
+ /* Send a message to the server and get back a message once connected */
+ if (!local) {
+ channel.send("x");
+
+ channels_listeners_message[host] = () => {
+ open = true;
+ if (url)
+ request_manifest();
+ watch_manifests();
+ request_hostname();
+ whirl();
+ };
+ channel.addEventListener("message", channels_listeners_message[host]);
+
+ channels_listeners_close[host] = (ev, options) => {
+ const m = machines.lookup(host);
+ open = false;
+ // reset to clean state when removing machine (orderly disconnect), otherwise mark as failed
+ if (!options.problem && m && !m.visible)
+ state(host, null, null);
+ else
+ state(host, "failed", options.problem || "disconnected");
+ if (m && m.restarting) {
+ window.setTimeout(function() {
+ self.connect(host);
+ }, 10000);
+ }
+ self.disconnect(host);
+ };
+ channel.addEventListener("close", channels_listeners_close[host]);
+ } else {
+ if (url)
+ request_manifest();
+ watch_manifests();
+ request_hostname();
+ }
+
+ /* In case already ready, for example when local */
+ whirl();
+ };
+
+ self.disconnect = function disconnect(host) {
+ if (host === "localhost")
+ return;
+
+ const channel = channels[host];
+ delete channels[host];
+ if (channel) {
+ channel.close();
+ channel.removeEventListener("message", channels_listeners_message[host]);
+ channel.removeEventListener("close", channels_listeners_close[host]);
+ }
+
+ const proxy = proxies[host];
+ delete proxies[host];
+ if (proxy) {
+ proxy.client.close();
+ proxy.removeEventListener("changed", proxies_listeners_changed[host]);
+ }
+
+ const dbus = bridge_dbus[host];
+ delete bridge_dbus[host];
+ if (dbus) {
+ dbus.close();
+ }
+ };
+
+ self.expect_restart = function expect_restart(host) {
+ const parts = machines.split_connection_string(host);
+ machines.overlay(parts.address, {
+ restarting: true,
+ problem: null
+ });
+ };
+
+ self.close = function close() {
+ machines.removeEventListener("added", updated);
+ machines.removeEventListener("changed", updated);
+ machines.removeEventListener("removed", removed);
+ machines = null;
+
+ window.removeEventListener("storage", process_session_machines);
+ const hosts = Object.keys(channels);
+ hosts.forEach(self.disconnect);
+ };
+
+ if (!session_only) {
+ const proxy = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Machines", "/machines");
+ proxy.addEventListener("changed", data => {
+ // unwrap variants from D-Bus call
+ const wrapped = proxy.Machines;
+ const data_unwrap = {};
+ for (const host in wrapped) {
+ const host_props = {};
+ for (const prop in wrapped[host])
+ host_props[prop] = wrapped[host][prop].v;
+ data_unwrap[host] = host_props;
+ }
+
+ machines.data(data_unwrap);
+ if (!session_loaded)
+ load_from_session_storage();
+ machines.set_ready();
+ });
+ } else {
+ load_from_session_storage();
+ machines.data({});
+ machines.set_ready();
+ }
+}
+
+mod.instance = function instance(loader) {
+ return new Machines();
+};
+
+mod.loader = function loader(machines, session_only) {
+ return new Loader(machines, session_only);
+};
+
+mod.colors = [
+ "#0099d3",
+ "#67d300",
+ "#d39e00",
+ "#d3007c",
+ "#00d39f",
+ "#00d1d3",
+ "#00618a",
+ "#4c8a00",
+ "#8a6600",
+ "#9b005b",
+ "#008a55",
+ "#008a8a",
+ "#00b9ff",
+ "#7dff00",
+ "#ffbe00",
+ "#ff0096",
+ "#00ffc0",
+ "#00fdff",
+ "#023448",
+ "#264802",
+ "#483602",
+ "#590034",
+ "#024830",
+ "#024848"
+];
+
+mod.colors.parse = function parse_color(input) {
+ const div = document.createElement('div');
+ div.style.color = input;
+ const style = window.getComputedStyle(div, null);
+ return style.getPropertyValue("color") || div.style.color;
+};
+
+export const machines = mod;
diff --git a/pkg/shell/machines/ssh-add-key.sh b/pkg/shell/machines/ssh-add-key.sh
new file mode 100755
index 0000000..3fb53f9
--- /dev/null
+++ b/pkg/shell/machines/ssh-add-key.sh
@@ -0,0 +1,24 @@
+#! /bin/sh
+
+set -euf
+
+d=$HOME/.ssh
+p=${2:-authorized_keys}
+f=$d/$p
+
+if ! test -f "$f"; then
+ mkdir -m 700 -p "$d"
+ touch "$f"
+ chmod 600 "$f"
+fi
+
+while read l; do
+ if [ "$l" = "$1" ]; then
+ exit 0
+ fi
+done <"$f"
+
+# Add newline if necessary
+! test -s "$f" || tail -c1 < "$f" | read -r _ || echo >> "$f"
+
+echo "$1" >>"$f"
diff --git a/pkg/shell/machines/ssh-show-default-key.sh b/pkg/shell/machines/ssh-show-default-key.sh
new file mode 100755
index 0000000..a7198b3
--- /dev/null
+++ b/pkg/shell/machines/ssh-show-default-key.sh
@@ -0,0 +1,16 @@
+#! /bin/sh
+
+set -euf
+
+# Print the name of default key, if any.
+
+for f in id_dsa id_ecdsa id_ecdsa_sk id_ed25519 id_ed25519_sk id_rsa; do
+ p=$HOME/.ssh/$f
+ if test -f "$p"; then
+ echo "$p"
+ if ! ssh-keygen -y -P "" -f "$p" >/dev/null 2>/dev/null; then
+ echo "encrypted"
+ fi
+ exit 0
+ fi
+done
diff --git a/pkg/shell/machines/test-machines.js b/pkg/shell/machines/test-machines.js
new file mode 100644
index 0000000..2d3d5c5
--- /dev/null
+++ b/pkg/shell/machines/test-machines.js
@@ -0,0 +1,209 @@
+import cockpit from "cockpit";
+import QUnit from "qunit-tests";
+import { machines } from "./machines.js";
+
+const dbus = cockpit.dbus(null, { bus: "internal" });
+let configDir;
+
+QUnit.module("machines.d parsing tests", {
+ beforeEach: () => cockpit.spawn(["mkdir", "-p", configDir + "/cockpit/machines.d"]),
+ afterEach: () => cockpit.spawn(["rm", "-rf", configDir + "/cockpit/machines.d"]),
+ after: () => cockpit.spawn(["rm", "-r", configDir]),
+});
+
+/***
+ * Tests for parsing on-disk JSON configuration
+ */
+
+async function machinesParseTest(assert, machines_defs, expectedProperty) {
+ for (const fname in machines_defs) {
+ let path = fname;
+ if (fname.indexOf('/') < 0)
+ path = configDir + "/cockpit/machines.d/" + fname;
+ await cockpit.file(path).replace(machines_defs[fname]);
+ }
+
+ const reply = await dbus.call("/machines", "org.freedesktop.DBus.Properties", "Get",
+ ["cockpit.Machines", "Machines"], { type: "ss" });
+ assert.equal(reply[0].t, "a{sa{sv}}", "expected return type");
+ assert.deepEqual(reply[0].v, expectedProperty, "expected property value");
+}
+
+QUnit.test("no machine definitions", async assert => machinesParseTest(assert, {}, {}));
+
+QUnit.test("empty json", async assert => machinesParseTest(assert, { "01.json": "" }, {}));
+
+QUnit.test("two definitions", async assert => machinesParseTest(
+ assert,
+ {
+ "01.json": '{"green": {"visible": true, "address": "1.2.3.4"}, ' +
+ ' "9.8.7.6": {"visible": false, "address": "9.8.7.6", "user": "admin"}}'
+ },
+ {
+ green: {
+ address: { t: "s", v: "1.2.3.4" },
+ visible: { t: "b", v: true }
+ },
+ "9.8.7.6": {
+ address: { t: "s", v: "9.8.7.6" },
+ user: { t: "s", v: "admin" },
+ visible: { t: "b", v: false }
+ }
+ })
+);
+
+QUnit.test("invalid json", async assert => machinesParseTest(assert, { "01.json": '{"green":' }, {}));
+
+QUnit.test.skipWithPybridge("invalid data types", async assert => machinesParseTest(assert, { "01.json": '{"green": []}' }, {}));
+
+QUnit.test.skipWithPybridge("merge several JSON files", async assert => machinesParseTest(
+ assert,
+ /* 99-webui.json changes a property in green, adds a
+ * property to blue, and adds an entire new host yellow */
+ {
+ "01-green.json": '{"green": {"visible": true, "address": "1.2.3.4"}}',
+ "02-blue.json": '{"blue": {"address": "9.8.7.6"}}',
+ "09-webui.json": '{"green": {"visible": false}, ' +
+ ' "blue": {"user": "joe"}, ' +
+ ' "yellow": {"address": "fe80::1", "user": "sue"}}'
+ },
+ {
+ green: {
+ address: { t: "s", v: "1.2.3.4" },
+ visible: { t: "b", v: false }
+ },
+ blue: {
+ address: { t: "s", v: "9.8.7.6" },
+ user: { t: "s", v: "joe" },
+ },
+ yellow: {
+ address: { t: "s", v: "fe80::1" },
+ user: { t: "s", v: "sue" },
+ }
+ }
+));
+
+QUnit.test.skipWithPybridge("merge JSON files with errors", async assert => machinesParseTest(
+ assert,
+ {
+ "01-valid.json": '{"green": {"visible": true, "address": "1.2.3.4"}}',
+ "02-syntax.json": '[a',
+ "03-toptype.json": '["green"]',
+ "04-toptype.json": '{"green": ["visible"]}',
+ "05-valid.json": '{"blue": {"address": "fe80::1"}}',
+ // goodprop should still be considered
+ "06-proptype.json": '{"green": {"visible": [], "address": {"bar": null}, "goodprop": "yeah"}}',
+ "07-valid.json": '{"green": {"user": "joe"}}',
+ "08-empty.json": ''
+ },
+ {
+ green: {
+ address: { t: "s", v: "1.2.3.4" },
+ visible: { t: "b", v: true },
+ user: { t: "s", v: "joe" },
+ goodprop: { t: "s", v: "yeah" },
+ },
+ blue: {
+ address: { t: "s", v: "fe80::1" }
+ }
+ }
+));
+
+/***
+ * Tests for Update()
+ */
+
+async function machinesUpdateTest(assert, origJson, host, props, expectedJson) {
+ const f = configDir + "/cockpit/machines.d/99-webui.json";
+
+ await cockpit.file(f).replace(origJson);
+ const reply = await dbus.call("/machines", "cockpit.Machines", "Update",
+ ["99-webui.json", host, props], { type: "ssa{sv}" });
+ assert.deepEqual(reply, [], "no expected return value");
+ const content = await cockpit.file(f, { syntax: JSON }).read();
+ assert.deepEqual(content, expectedJson, "expected file content");
+}
+
+QUnit.test("no config files", async assert => machinesUpdateTest(
+ assert,
+ null,
+ "green",
+ { visible: cockpit.variant('b', true), address: cockpit.variant('s', "1.2.3.4") },
+ { green: { address: "1.2.3.4", visible: true } }
+));
+
+QUnit.test("add host to existing map", async assert => machinesUpdateTest(
+ assert,
+ '{"green": {"visible": true, "address": "1.2.3.4"}}',
+ "blue",
+ { visible: cockpit.variant('b', false), address: cockpit.variant('s', "9.8.7.6") },
+ {
+ green: { address: "1.2.3.4", visible: true },
+ blue: { address: "9.8.7.6", visible: false }
+ }
+));
+
+QUnit.test("change bool host property", async assert => machinesUpdateTest(
+ assert,
+ '{"green": {"visible": true, "address": "1.2.3.4"}}',
+ "green",
+ { visible: cockpit.variant('b', false) },
+ { green: { address: "1.2.3.4", visible: false } }
+));
+
+QUnit.test("change string host property", async assert => machinesUpdateTest(
+ assert,
+ '{"green": {"visible": true, "address": "1.2.3.4"}}',
+ "green",
+ { address: cockpit.variant('s', "fe80::1") },
+ { green: { address: "fe80::1", visible: true } }
+));
+
+QUnit.test("add host property", async assert => machinesUpdateTest(
+ assert,
+ '{"green": {"visible": true, "address": "1.2.3.4"}}',
+ "green",
+ { color: cockpit.variant('s', "pitchblack") },
+ { green: { address: "1.2.3.4", visible: true, color: "pitchblack" } }
+));
+
+QUnit.test("Update() only writes delta", async assert => {
+ await cockpit.file(configDir + "/cockpit/machines.d/01-green.json")
+ .replace('{"green": {"address": "1.2.3.4"}, "blue": {"address": "fe80::1"}}');
+ return machinesUpdateTest(
+ assert,
+ null,
+ "green",
+ { color: cockpit.variant('s', "pitchblack") },
+ { green: { color: "pitchblack" } });
+});
+
+QUnit.test("updating and existing delta file", async assert => {
+ await cockpit.file(configDir + "/cockpit/machines.d/01-green.json")
+ .replace('{"green": {"address": "1.2.3.4"}, "blue": {"address": "fe80::1"}}');
+
+ return machinesUpdateTest(
+ assert,
+ '{"green": {"address": "9.8.7.6", "user": "joe"}}',
+ "green",
+ { color: cockpit.variant('s', "pitchblack") },
+ { green: { address: "9.8.7.6", user: "joe", color: "pitchblack" } });
+});
+
+QUnit.test("colors.parse()", assert => {
+ const colors = [
+ ["#960064", "rgb(150, 0, 100)"],
+ ["rgb(150, 0, 100)", "rgb(150, 0, 100)"],
+ ["#ccc", "rgb(204, 204, 204)"],
+ ];
+ assert.expect(colors.length);
+ colors.forEach(color => assert.equal(machines.colors.parse(color[0]), color[1], "parsed color " + color[0]));
+});
+
+/* The test cockpit-bridge gets started with temp $XDG_CONFIG_DIRS instead of defaulting to /etc/.
+ * Read it from the bridge so that we can put our test files into it. */
+const proxy = dbus.proxy("cockpit.Environment", "/environment");
+proxy.wait(() => {
+ configDir = proxy.Variables.XDG_CONFIG_DIRS;
+ QUnit.start();
+});
diff --git a/pkg/shell/manifest.json b/pkg/shell/manifest.json
new file mode 100644
index 0000000..a3f965f
--- /dev/null
+++ b/pkg/shell/manifest.json
@@ -0,0 +1,58 @@
+{
+ "requires": {
+ "cockpit": "239"
+ },
+ "locales": {
+ "cs-cz": "čeština",
+ "de-de": "Deutsch",
+ "en-us": "English",
+ "es-es": "español",
+ "fi-fi": "suomi",
+ "fr-fr": "français",
+ "he-il": "עברית",
+ "it-it": "italiano",
+ "ja-jp": "日本語",
+ "ka-ge": "ქართული",
+ "ko-kr": "한국어",
+ "nb-no": "norsk bokmål",
+ "nl-nl": "Nederlands",
+ "pl-pl": "polski",
+ "pt-br": "português (Brasil)",
+ "ru-ru": "русский",
+ "sk-sk": "slovenčina",
+ "sv-se": "svenska",
+ "tr-tr": "Türkçe",
+ "uk-ua": "Українська",
+ "zh-cn": "中文(中国)"
+ },
+ "docs": [
+ {
+ "label": "Web Console",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/index"
+ }
+ ],
+ "bridges": [
+ {
+ "privileged": true,
+ "environ": [
+ "SUDO_ASKPASS=${libexecdir}/cockpit-askpass"
+ ],
+ "spawn": [
+ "sudo",
+ "-k",
+ "-A",
+ "cockpit-bridge",
+ "--privileged"
+ ]
+ },
+ {
+ "privileged": true,
+ "spawn": [
+ "pkexec",
+ "--disable-internal-agent",
+ "cockpit-bridge",
+ "--privileged"
+ ]
+ }
+ ]
+}
diff --git a/pkg/shell/nav.jsx b/pkg/shell/nav.jsx
new file mode 100644
index 0000000..2030793
--- /dev/null
+++ b/pkg/shell/nav.jsx
@@ -0,0 +1,250 @@
+import cockpit from "cockpit";
+
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Nav } from "@patternfly/react-core/dist/esm/components/Nav/index.js";
+import { SearchInput } from "@patternfly/react-core/dist/esm/components/SearchInput/index.js";
+import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+import { ContainerNodeIcon, ExclamationCircleIcon, ExclamationTriangleIcon, InfoCircleIcon } from '@patternfly/react-icons';
+
+const _ = cockpit.gettext;
+
+export const SidebarToggle = () => {
+ const [active, setActive] = useState(false);
+
+ useEffect(() => {
+ /* This is a HACK for catching lost clicks on the pages which live in iframes so as to close dropdown menus on the shell.
+ * Note: Clicks on an <iframe> element won't trigger document.documentElement listeners, because it's literally different page with different security domain.
+ * However, when clicking on an iframe moves focus to its content's window that triggers the main window.blur event.
+ * Addionally, when clicking on an element in the same iframe make sure to unset the 'active' state of the 'System' dropdown selector.
+ */
+ const handleClickOutside = (ev) => {
+ if (ev.target.id == "nav-system-item")
+ return;
+
+ setActive(false);
+ };
+
+ ["blur", "click"].map(ev_type => window.addEventListener(ev_type, handleClickOutside));
+
+ return () => {
+ ["blur", "click"].map(ev_type => window.removeEventListener(ev_type, handleClickOutside));
+ };
+ }, []);
+
+ useEffect(() => {
+ document.getElementById("nav-system").classList.toggle("interact", active);
+ }, [active]);
+
+ return (
+ <Button className={"pf-v5-c-select__toggle ct-nav-toggle " + (active ? "active" : "")}
+ id="nav-system-item" variant="plain"
+ onClick={() => setActive(!active)}>
+ <ContainerNodeIcon className="pf-v5-c-icon pf-m-lg" />
+ {_("System")}
+ </Button>
+ );
+};
+
+export class CockpitNav extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ search: "",
+ current: props.current,
+ };
+
+ this.clearSearch = this.clearSearch.bind(this);
+ }
+
+ componentDidMount() {
+ const self = this;
+ const sel = this.props.selector;
+ // Click on active menu item (when using arrows to navigate through menu)
+ function clickActiveItem() {
+ const cur = document.activeElement;
+ if (cur.nodeName === "INPUT") {
+ const el = document.querySelector("#" + sel + " li:first-of-type a");
+ if (el)
+ el.click();
+ } else {
+ cur.click();
+ }
+ }
+
+ // Move focus to next item in menu (when using arrows to navigate through menu)
+ // With arguments it is possible to change direction
+ function focusNextItem(begin, step) {
+ const cur = document.activeElement;
+ const all = Array.from(document.querySelectorAll("#" + sel + " li a"));
+ if (cur.nodeName === "INPUT" && all) {
+ if (begin < 0)
+ begin = all.length - 1;
+ all[begin].focus();
+ } else {
+ let i = all.findIndex(item => item === cur);
+ i += step;
+ if (i < 0 || i >= all.length)
+ document.querySelector("#" + sel + " .pf-v5-c-text-input-group__text-input").focus();
+ else
+ all[i].focus();
+ }
+ }
+
+ function navigate_apps(ev) {
+ if (ev.keyCode === 13) // Enter
+ clickActiveItem();
+ else if (ev.keyCode === 40) // Arrow Down
+ focusNextItem(0, 1);
+ else if (ev.keyCode === 38) // Arrow Up
+ focusNextItem(-1, -1);
+ else if (ev.keyCode === 27) { // Escape - clean selection
+ self.setState({ search: "" });
+ document.querySelector("#" + sel + " .pf-v5-c-text-input-group__text-input").focus();
+ }
+ }
+
+ document.getElementById(sel).addEventListener("keyup", navigate_apps);
+ document.getElementById(sel).addEventListener("change", navigate_apps);
+ }
+
+ static getDerivedStateFromProps(nextProps, prevState) {
+ if (nextProps.current !== prevState.current)
+ return {
+ current: nextProps.current,
+ search: "",
+ };
+ return null;
+ }
+
+ clearSearch() {
+ this.setState({ search: "" });
+ }
+
+ render() {
+ const groups = [];
+ const term = this.state.search.toLowerCase();
+ this.props.groups.forEach(g => {
+ const new_items = g.items.map(i => this.props.filtering(i, term)).filter(Boolean);
+ new_items.sort(this.props.sorting);
+ if (new_items.length > 0)
+ groups.push({ name: g.name, items: new_items, action: g.action });
+ });
+
+ return (
+ <>
+ <SearchInput placeholder={_("Search")} value={this.state.search} onChange={(_, search) => this.setState({ search })} onClear={() => this.setState({ search: "" })} className="search" />
+ <Nav onSelect={this.onSelect} theme="dark">
+ { groups.map(g =>
+ <section className="pf-v5-c-nav__section" aria-labelledby={"section-title-" + g.name} key={g.name}>
+ <div className="nav-group-heading">
+ <h2 className="pf-v5-c-nav__section-title" id={"section-title-" + g.name}>{g.name}</h2>
+ { g.action &&
+ <a className="pf-v5-c-nav__section-title nav-item" href={g.action.path} onClick={ ev => {
+ this.props.jump(g.action.path);
+ ev.preventDefault();
+ }}>{g.action.label}</a>
+ }
+ </div>
+ <ul className="pf-v5-c-nav__list">
+ {g.items.map(i => this.props.item_render(i, this.state.search.toLowerCase()))}
+ </ul>
+ </section>
+ )}
+ { groups.length < 1 && <span className="non-menu-item no-results">{_("No results found")}</span> }
+ { this.state.search !== "" && <span className="non-menu-item"><Button variant="link" onClick={this.clearSearch} className="nav-item-hint">{_("Clear search")}</Button></span> }
+ </Nav>
+ </>
+ );
+ }
+}
+
+CockpitNav.propTypes = {
+ groups: PropTypes.array.isRequired,
+ selector: PropTypes.string.isRequired,
+ item_render: PropTypes.func.isRequired,
+ current: PropTypes.string.isRequired,
+ filtering: PropTypes.func.isRequired,
+ sorting: PropTypes.func.isRequired,
+ jump: PropTypes.func.isRequired,
+};
+
+function PageStatus({ status, name }) {
+ // Generate name for the status
+ let desc = name.toLowerCase().split(" ");
+ desc.push(status.type);
+ desc = desc.join("-");
+
+ return (
+ <Tooltip id={desc + "-tooltip"} content={status.title}
+ position={TooltipPosition.right}>
+ <span id={desc} className="nav-item-status">
+ {status.type == "error"
+ ? <ExclamationCircleIcon color="#f54f42" />
+ : status.type == "warning"
+ ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" color="#f0ab00" />
+ : <InfoCircleIcon color="#73bcf7" />}
+ </span>
+ </Tooltip>
+ );
+}
+
+function FormattedText({ keyword, term }) {
+ function split_text(text, term) {
+ const b = text.toLowerCase().indexOf(term);
+ const e = b + term.length;
+ return [text.substring(0, b), text.substring(b, e), text.substring(e, text.length)];
+ }
+
+ const s = split_text(keyword, term);
+ return (
+ <>{s[0]}<mark>{s[1]}</mark>{s[2]}</>
+ );
+}
+
+export function CockpitNavItem(props) {
+ const s = props.status;
+ const name_matches = props.keyword === props.name.toLowerCase();
+ let header_matches = false;
+ if (props.header)
+ header_matches = props.keyword === props.header.toLowerCase();
+
+ const classes = props.className ? [props.className] : [];
+ classes.push("pf-v5-c-nav__item", "nav-item");
+
+ return (
+ <li className={classes.join(" ")}>
+ <a className={"pf-v5-c-nav__link" + (props.active ? " pf-m-current" : "")}
+ aria-current={props.active && "page"}
+ href={cockpit.location.encode(props.to, {}, true)} onClick={ev => {
+ props.jump(props.to);
+ ev.preventDefault();
+ }}>
+ { props.header && <span className="nav-item-hint">{header_matches ? <FormattedText keyword={props.header} term={props.term} /> : props.header}</span> }
+ <span className="nav-item-name">
+ { name_matches ? <FormattedText keyword={props.name} term={props.term} /> : props.name }
+ </span>
+ {s && s.type && <PageStatus status={s} name={props.name} />}
+ { !name_matches && !header_matches && props.keyword && <span className="nav-item-hint nav-item-hint-contains">{_("Contains:")} <FormattedText keyword={props.keyword} term={props.term} /></span> }
+ </a>
+ <span className="nav-item-actions nav-host-action-buttons">
+ {props.actions}
+ </span>
+ </li>
+ );
+}
+
+CockpitNavItem.propTypes = {
+ name: PropTypes.string.isRequired,
+ to: PropTypes.string.isRequired,
+ jump: PropTypes.func,
+ status: PropTypes.object,
+ active: PropTypes.bool,
+ keyword: PropTypes.string,
+ term: PropTypes.string,
+ header: PropTypes.string,
+ actions: PropTypes.node,
+};
diff --git a/pkg/shell/nav.scss b/pkg/shell/nav.scss
new file mode 100644
index 0000000..a175edf
--- /dev/null
+++ b/pkg/shell/nav.scss
@@ -0,0 +1,824 @@
+@import "global-variables";
+
+/* Navigation base, used for both desktop & mobile navigation */
+
+$phone-tiny: 360px;
+$phone: 767px;
+$desktop: $phone + 1px;
+
+:root {
+ --ct-color-nav-menu: var(--pf-v5-global--BackgroundColor--dark-300);
+ --ct-color-nav-highlight: var(--pf-v5-global--BackgroundColor--dark--400);
+ --ct-color-nav-highlight-text: var(--pf-v5-global--BackgroundColor--light-100);
+ --ct-color-nav-search: #1b1b1b;
+}
+
+.area-ct-subnav {
+ background: var(--ct-color-nav-menu);
+ display: flex;
+ flex-direction: column;
+ position: relative;
+
+ // Overrides for dark theme (as PF's dark theme is currently odd with nav)
+ .pf-v5-theme-dark & {
+ --ct-color-nav-menu: var(--pf-v5-global--BackgroundColor--150);
+ --ct-color-nav-search: var(--pf-v5-global--BackgroundColor--200);
+ --ct-color-nav-highlight: var(--pf-v5-global--palette--gold-600);
+ --ct-color-nav-highlight-text: var(--pf-v5-global--palette--gold-300);
+
+ .pf-v5-c-nav__link {
+ --pf-v5-c-nav__link--hover--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-300);
+ --pf-v5-c-nav__link--m-current--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-400);
+ }
+
+ .search .pf-v5-c-text-input-group__text-input {
+ border-color: var(--pf-v5-global--BorderColor--300);
+ }
+
+ .sidebar-hosts .pf-v5-c-nav__item {
+ --pf-v5-c-nav__link--hover--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-300);
+ --pf-v5-c-nav__link--m-current--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-400);
+ }
+
+ .nav-item-hint {
+ color: var(--pf-v5-global--palette--black-300);
+ }
+ }
+
+ mark {
+ font-weight: bold;
+ background: var(--ct-color-nav-highlight);
+ color: var(--ct-color-nav-highlight-text);
+ }
+
+ // https://github.com/patternfly/patternfly-design/issues/1112
+ .search {
+ background: transparent !important;
+ padding-block: var(--pf-v5-global--spacer--md) var(--pf-v5-global--spacer--sm);
+ padding-inline: var(--pf-v5-global--spacer--md);
+
+ > .pf-v5-c-text-input-group__main {
+ background: var(--ct-color-nav-search);
+ }
+
+ .pf-v5-c-text-input-group__text::after,
+ .pf-v5-c-text-input-group__text::before {
+ border: none;
+ }
+
+ // Adjust search icon
+ .pf-v5-c-text-input-group__icon {
+ padding-block: var(--pf-v5-global--spacer--md) var(--pf-v5-global--spacer--sm) !important;
+ padding-inline: var(--pf-v5-global--spacer--md) !important;
+ color: var(--pf-v5-global--palette--black-200);
+ inset-inline-start: var(--pf-v5-global--spacer--sm);
+ }
+
+ .pf-v5-c-text-input-group__text-input,
+ .pf-v5-c-text-input-group__text-input::placeholder {
+ color: var(--pf-v5-global--palette--black-100);
+ }
+
+ .pf-v5-c-text-input-group__text-input {
+ // Reserve space for the × icon
+ padding-inline: var(--pf-v5-global--spacer--xl) 0;
+ // Stroke outside, based on PF search dark mode mockups
+ border: 1px solid var(--pf-v5-global--palette--black-700);
+ }
+
+ // Reposition the × icon over the entry & recolor it
+ .pf-v5-c-text-input-group__utilities {
+ align-self: center;
+ justify-content: center;
+ position: absolute;
+ inset-inline-end: var(--pf-v5-global--spacer--sm);
+ inline-size: var(--pf-v5-global--spacer--xl);
+
+ // Recolor × icon
+ > .pf-v5-c-button {
+ color: var(--pf-v5-global--palette--black-400);
+
+ &:hover {
+ color: var(--pf-v5-global--palette--black-200);
+ }
+ }
+ }
+ }
+
+ .nav-item-hint {
+ color: var(--pf-v5-global--palette--black-400);
+ display: inline-block;
+ font-size: var(--pf-v5-global--FontSize--sm);
+ font-weight: var(--pf-v5-global--FontWeight--normal);
+ inline-size: 100%;
+ }
+
+ .pf-m-current .nav-item-hint {
+ color: var(--pf-v5-global--palette--black-300);
+ }
+
+ .non-menu-item .nav-item-hint {
+ color: var(--pf-v5-global--primary-color--light-100);
+ }
+
+ // Add hover effect for clear search link
+ .non-menu-item:hover .nav-item-hint {
+ text-decoration: underline;
+ }
+
+ .non-menu-item {
+ color: var(--pf-v5-c-nav__link--Color);
+ display: flex;
+ justify-content: center;
+ }
+
+ .no-results {
+ padding-block: var(--pf-v5-global--spacer--md);
+ padding-inline: 0;
+ }
+}
+
+.area-ct-content {
+ position: relative;
+ z-index: 1;
+ display: block;
+
+ > iframe {
+ block-size: 100%;
+ position: absolute;
+ }
+}
+
+.screenreader-text {
+ position: absolute;
+ inset-inline-start: -999px;
+ inline-size: 1px;
+ block-size: 1px;
+ inset-block-start: auto;
+
+ &:focus {
+ display: inline-block;
+ block-size: auto;
+ inline-size: auto;
+ position: static;
+ padding-block: 19px;
+ padding-inline: 0;
+ inline-size: 100%;
+ text-align: center;
+ background-color: var(--pf-v5-global--BackgroundColor--dark-300);
+ color: white;
+ }
+}
+
+[dir="rtl"] {
+ // Not setting this breaks pixel tests
+ .screenreader-text {
+ inset-inline-end: -999px;
+ }
+}
+
+/* Desktop navigation */
+@media (min-width: $desktop) {
+ // Adapt PF4's new header select for 2-up
+ .ct-topnav-content .pf-v5-c-toolbar__item:not(.super-user-indicator) {
+ // Remove the margin spacing between each
+ margin-inline-end: 0;
+
+ // Collapse the 2 side border lines into 1
+ + .pf-v5-c-toolbar__item button::before {
+ margin-inline-start: -1px;
+ }
+ }
+
+ .mobile_v {
+ display: none !important;
+ }
+
+ .area-ct-subnav {
+ // Make a separator between host selector & search
+ border-block-start: 1px solid var(--pf-v5-global--Color--300);
+ }
+
+ .view-hosts .sidebar-hosts {
+ animation: navHostsSlide 300ms ease-out;
+ transform-origin: top;
+ position: absolute;
+ inset: 0;
+ background: var(--pf-v5-global--BackgroundColor--dark-200);
+
+ .pf-v5-theme-dark & {
+ background: var(--pf-v5-global--BackgroundColor--dark-300);
+ }
+
+ z-index: 399; // Modals have 400 and modals should be in front of host switcher
+
+ &.edit-hosts {
+ .pf-v5-c-nav__list {
+ .pf-v5-c-button {
+ animation: navHostsEditButtonsAppear 400ms;
+ animation-timing-function: ease-in-out;
+ }
+ }
+ }
+ }
+
+ .header {
+ border-inline-start: 1px solid var(--pf-v5-global--BackgroundColor--dark-100);
+ background: linear-gradient(to right, var(--pf-v5-global--BackgroundColor--dark-100) -1rem, var(--pf-v5-global--BackgroundColor--dark-300) 1rem);
+
+ .credential-unlocked {
+ /* AAA contrast: simulate #a1a1a1 on #151515 */
+ opacity: 0.6;
+ }
+
+ .pf-v5-c-dropdown__menu {
+ // Correct the offset
+ margin-block-start: -4px;
+ }
+ }
+
+ .pf-v5-theme-dark {
+ > .pf-v5-c-select__toggle {
+ background-color: var(--pf-v5-global--BackgroundColor--dark-300);
+ }
+ }
+
+ .ct-switcher {
+ background-color: var(--pf-v5-global--BackgroundColor--dark-300);
+
+ > .pf-v5-c-select > button {
+ padding-inline-start: var(--pf-v5-global--spacer--lg);
+ }
+ }
+
+ .nav-hosts-menu {
+ display: none;
+
+ &.interact {
+ display: block;
+ }
+ }
+
+ .pf-v5-c-select__toggle-arrow {
+ font-size: 1.3rem;
+ transition-duration: 0.3s;
+ }
+
+ .clicked {
+ transform: rotate(180deg);
+ }
+}
+
+/* Mobile navigation */
+@media (max-width: $phone) {
+ :root {
+ --nav-icon-size: 4rem;
+ }
+
+ .desktop_v {
+ display: none !important;
+ }
+
+ .area-ct-subnav {
+ align-self: flex-end;
+ z-index: 2;
+
+ &:not(.interact) {
+ display: none;
+ }
+ }
+
+ .area-ct-subnav {
+ transform-origin: bottom;
+ animation: navHostsSlide 200ms ease-out;
+ }
+
+ .header {
+ .pf-v5-c-toolbar__item {
+ margin-inline-end: 0;
+ }
+ }
+
+ .nav-system-menu {
+ inset-inline: 0;
+ block-size: auto;
+ overflow: auto;
+ max-block-size: calc(100vh - var(--nav-icon-size));
+ max-inline-size: 29rem;
+ display: block;
+ }
+
+ .nav-hosts-menu {
+ position: absolute;
+ inset-block-end: var(--nav-icon-size);
+ inset-inline-end: var(--pf-v5-global--spacer--md);
+ max-inline-size: 100vw;
+ }
+
+ .pf-m-dark.pf-v5-c-select {
+ .pf-v5-c-select__toggle {
+ color: var(--pf-v5-global--Color--light-100) !important;
+ background-color: transparent;
+ }
+ }
+
+ .ct-switcher > .pf-v5-c-select {
+ .pf-v5-c-select__toggle {
+ display: block;
+ inline-size: auto;
+ }
+
+ .pf-v5-c-select__toggle-arrow {
+ transform: scale(1.5);
+ margin: unset;
+ margin-block-start: 4px;
+ transition-duration: 0.3s;
+ }
+
+ .clicked {
+ transform : rotate(180deg) scale(1.5);
+ }
+
+ > button {
+ padding-inline-start: 0;
+ }
+ }
+
+ .menu-toggle,
+ .sidebar-toggle .fa {
+ display: block;
+ transform: scale(1.5);
+ border-radius: 0 !important;
+ }
+
+ // Override some PF4isms to make sure the host switcher is 100% tall
+ .navbar-pf {
+ .ct-switcher {
+ display: flex;
+ }
+ }
+}
+
+@media (max-width: $phone-tiny) {
+ // Don't have a gap for the hosts selection
+ .nav-hosts-menu {
+ inset-inline-end: 0;
+ }
+
+ // Make the hosts selection fill the width of the page
+ .view-hosts .sidebar-hosts .pf-v5-c-page__sidebar-body {
+ inline-size: 100vw;
+ }
+}
+
+.navbar.navbar-pf-vertical {
+ border-block-start: 0;
+}
+
+.ct-switcher {
+ block-size: 100%;
+
+ > .pf-v5-c-select {
+ block-size: 100%;
+
+ .pf-v5-c-select__toggle-text {
+ text-align: start;
+ }
+ }
+
+ .username {
+ display: inline-block;
+ }
+
+ .at {
+ opacity: 0.8;
+ }
+
+ .hostname {
+ display: block;
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
+
+.pf-v5-theme-dark {
+ .pf-v5-c-select__toggle {
+ > * {
+ color: var(--pf-v5-global--Color--light-100);
+ }
+ }
+}
+
+.pf-v5-c-select__toggle {
+ block-size: 100%;
+}
+
+.nav-action {
+ margin-block: 0 !important;
+ margin-inline: auto 0 !important;
+}
+
+.host-apps {
+ flex: 1 1 0;
+ overflow-x: hidden;
+ overflow-y: auto;
+ position: relative;
+ scrollbar-color: var(--pf-v5-global--Color--400) var(--pf-v5-global--BackgroundColor--dark-200);
+
+ // In mobile, make the search at the top and the rest scroll
+ @media (max-width: $phone) {
+ display: grid;
+ grid-template-rows: auto 1fr;
+ max-block-size: calc(100vh - var(--nav-icon-size));
+ position: sticky;
+ inset-block-start: 0;
+
+ > .pf-v5-c-nav {
+ overflow: auto;
+ }
+ }
+}
+
+// Support links in headings
+.nav-group-heading {
+ display: flex;
+ justify-content: space-between;
+
+ > .pf-v5-c-nav__section-title:not(a) {
+ flex: auto;
+ }
+
+ a {
+ color: var(--pf-v5-global--link--Color--light);
+ font-weight: normal;
+
+ &:hover {
+ color: var(--pf-v5-global--link--Color--light--hover);
+ }
+ }
+}
+
+.nav-system-menu {
+ // Flow items, if wider (useful for mobile)
+ .pf-v5-c-nav__list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(10rem, auto));
+ gap: var(--pf-v5-c-nav__item--MarginTop) 0;
+ }
+
+ // PF uses margin for non-first elements; it really should use gap in the
+ // parent instead; fixing it with a local override, specific for our menu, as
+ // we really need it for mobile. It changes desktop too, but does the right
+ // thing there (but it's not visabily noticable in desktop).
+ .nav-item {
+ --pf-v5-c-nav__item--MarginTop: 0;
+ }
+
+ // Give additional style to individual menu items
+ .pf-v5-c-nav__link {
+ display: grid;
+ grid-template: "name status" "match match";
+ grid-template-columns: 1fr auto;
+ justify-content: space-between;
+ gap: 0 var(--pf-v5-global--spacer--md);
+ padding-inline-end: var(--pf-v5-global--spacer--md);
+ // In mobile, sometimes items wrap; neighbors should also fill the space
+ block-size: 100%;
+
+ > .nav-item-name {
+ grid-area: name;
+ }
+
+ > .nav-item-status {
+ grid-area: status;
+ }
+
+ > .nav-item-hint {
+ grid-area: match;
+ }
+
+ > :empty {
+ display: none;
+ }
+
+ // Add a background to shine through the icon's gaps
+ // (for better contrast, even when hovering / selected)
+ .nav-item-status {
+ position: relative;
+
+ > svg {
+ // Set position for the icon sandwiching
+ position: relative;
+ // Add a shadow around the icon
+ // Move the icon up the stack
+ z-index: 1;
+ }
+
+ // Fill the interior gaps
+ &::before {
+ position: absolute;
+ content: "";
+ background-color: var(--pf-v5-c-nav__link--Color);
+ border-radius: 3px;
+ inset-block: 6px;
+ inset-inline: 5px;
+ }
+ }
+ }
+}
+
+.view-hosts .sidebar-hosts {
+ .pf-v5-c-nav__list {
+ overflow-y: auto;
+ }
+
+ .pf-v5-c-page__sidebar-body {
+ display: grid;
+ grid-template-rows: max-content max-content max-content;
+
+ @media (min-width: $desktop) {
+ max-block-size: 100%;
+ }
+
+ @media (max-width: $phone) {
+ // Don't run off the top of the page in mobile
+ max-block-size: calc(100vh - var(--nav-icon-size));
+ }
+ }
+
+ .pf-v5-c-nav {
+ overflow: auto;
+ }
+
+ .nav-hosts-actions {
+ --button-margin-x: 1rem;
+ --button-margin-y: 0.75rem;
+ display: grid;
+ gap: var(--pf-v5-global--spacer--sm);
+ margin-block: var(--button-margin-y);
+ margin-inline: var(--button-margin-x);
+
+ > button {
+ html:not(.pf-v5-theme-dark) & {
+ color: var(--pf-v5-global--BackgroundColor--200);
+
+ &::after {
+ border-color: var(--pf-v5-global--BackgroundColor--200);
+ }
+ }
+ }
+ }
+
+ .nav-item {
+ --pf-v5-c-nav--m-dark__item--m-current--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-400);
+ display: grid;
+ grid-template-columns: 1fr auto;
+
+ > .pf-v5-c-nav__link {
+ flex-direction: column;
+ justify-content: center;
+ word-break: break-word;
+ }
+ }
+
+ .nav-item-actions {
+ display: flex;
+ gap: var(--pf-v5-global--spacer--xs);
+ padding-block: var(--pf-v5-global--spacer--xs);
+ padding-inline: var(--pf-v5-global--spacer--sm);
+ // Pull the background color over
+ background: var(--pf-v5-c-nav__link--BackgroundColor);
+
+ @media (min-width: $desktop) {
+ // Desktop has limited horizontal space, so stack these
+ // Ironically, mobile has more space here (due to being a popup)
+ flex-direction: column;
+ }
+
+ > button {
+ --pf-v5-c-button--m-secondary--Color: rgb(255 255 255 / 75%);
+ --pf-v5-c-button--m-secondary--BorderColor: rgb(255 255 255 / 75%);
+ --pf-v5-c-button--m-secondary--hover--Color: rgb(255 255 255 / 100%);
+ --pf-v5-c-button--m-secondary--hover--BorderColor: rgb(255 255 255 / 100%);
+
+ // Simulate PF4's disabled icon, but with alpha
+ --pf-v5-c-button--disabled--BackgroundColor: rgb(255 255 255 / 17%);
+ --pf-v5-c-button--disabled--Color: rgb(0 7 13 / 45%);
+ margin: 0;
+ }
+ }
+
+ // Use the "current" background for actions color when item is selected
+ .pf-m-current + .nav-item-actions {
+ background-color: var(--pf-v5-c-nav__link--m-current--BackgroundColor);
+ }
+}
+
+.ct-topnav-content {
+ margin-inline-start: auto;
+}
+
+// Rework navigation toggles in desktop and (especially) mobile modes
+.super-user-indicator > button,
+.ct-nav-toggle:not(.pf-v5-c-dropdown) {
+ color: var(--pf-v5-global--Color--light-100) !important;
+ background: transparent;
+}
+
+.ct-nav-toggle:not(.pf-v5-c-dropdown) {
+ &:hover, &:active, &.active, &.interact, &[aria-expanded="true"] {
+ text-decoration: none;
+ // approximate --pf-v5-global--BackgroundColor--dark-400, but with opacity
+ background: rgb(249 252 255 / 32%) !important;
+
+ .hostname {
+ text-decoration: underline;
+ }
+ }
+
+ &:focus {
+ text-decoration: none;
+ outline: 1px dotted var(--pf-v5-global--BackgroundColor--light-100);
+ }
+
+ // The way the event is handled, it checks for the button, but not the icon. It should bubble, but doesn't. We can work around this by passing mouse events through.
+ svg {
+ pointer-events: none;
+ }
+}
+
+// In mobile view keep the non PF4 of displaying hover / focus state in the masthead / toolbar items
+// This can go away once we move the host selector inside the Masthead
+// https://github.com/patternfly/patternfly/issues/4524
+@media (max-width: $phone) {
+ .ct-nav-toggle.pf-v5-c-dropdown {
+ &:hover, &:active, &.active, &.interact, &[aria-expanded="true"], &.pf-m-expanded {
+ text-decoration: none;
+ // approximate --pf-v5-global--BackgroundColor--dark-400, but with opacity
+ background: rgb(249 252 255 / 32%);
+
+ .hostname {
+ text-decoration: underline;
+ }
+ }
+
+ &:focus {
+ text-decoration: none;
+ outline: 1px dotted var(--pf-v5-global--BackgroundColor--light-100);
+ }
+ }
+
+ // Hide border from navigation items for mobile
+ .ct-header-item:not(:hover) button:not(:hover)::before {
+ border: none;
+ }
+}
+
+.ct-locked > .ct-lock-wrapper > svg {
+ margin-inline-end: var(--pf-v5-global--spacer--sm);
+ color: var(--pf-v5-global--palette--gold-400);
+}
+
+.super-user-indicator {
+ @media (min-width: $desktop) {
+ > .ct-locked {
+ > .ct-lock-wrapper {
+ background: var(--pf-v5-global--BackgroundColor--dark-200);
+ border-radius: var(--pf-v5-global--BorderRadius--sm);
+ padding-block: var(--pf-v5-global--spacer--xs);
+ padding-inline: var(--pf-v5-global--spacer--sm);
+
+ > svg {
+ color: var(--pf-v5-global--palette--gold-300);
+ }
+ }
+
+ &:hover, &:focus {
+ background: none;
+
+ > .ct-lock-wrapper {
+ background: var(--pf-v5-global--BackgroundColor--dark-400);
+ }
+ }
+ }
+ }
+
+ > .ct-unlocked {
+ &:hover, &:focus {
+ text-decoration: underline;
+ }
+
+ svg {
+ display: none;
+ }
+ }
+}
+
+// Mobile sizes
+@media (max-width: $phone) {
+ #host-toggle,
+ #nav-system-item,
+ .ct-nav-toggle > button {
+ // Stretch to navbar
+ block-size: 100%;
+ // Don't stretch to fill content; make optimal width same as height
+ inline-size: var(--pf-v5-global--spacer--4xl);
+ // Leave enough space for 5 items (4 + spinner)
+ max-inline-size: calc(100vw / 5);
+ display: grid !important;
+ grid-template-rows: 28px 1fr;
+ justify-content: center;
+ justify-items: center;
+ color: var(--pf-v5-global--Color--light-100);
+ padding-block: var(--pf-v5-global--spacer--sm);
+ padding-inline: var(--pf-v5-global--spacer--xs);
+ align-items: center;
+
+ > .pf-v5-c-select__toggle-wrapper {
+ flex: none;
+ max-inline-size: 100%;
+ }
+
+ // Remove the toggled outline
+ .pf-v5-c-select__toggle::before {
+ display: none !important;
+ }
+ // Don't show dropdown icon
+ .pf-v5-c-dropdown__toggle-icon {
+ display: none;
+ }
+
+ .pf-v5-c-dropdown__toggle-image {
+ align-self: center;
+ margin: 0 !important;
+ }
+
+ .pf-v5-c-select__toggle-arrow {
+ // This is here because Chrome is weird sometimes...
+ padding-block: 2px 5px;
+ padding-inline: 0;
+ }
+ }
+
+ .ct-nav-toggle .pf-v5-c-dropdown__menu {
+ inset-block: auto 100%;
+ }
+}
+
+// HACK: Don't span the navigation as wide or tall as possible
+.nav-hosts-menu {
+ block-size: auto;
+
+ .pf-v5-c-page__sidebar {
+ inline-size: unset;
+ }
+}
+
+/* Dark mode adjustments */
+
+.pf-v5-theme-dark {
+ // Change sidebar background color when dark
+ nav,
+ .ct-switcher > .pf-v5-c-select {
+ --ct-color-nav-menu: var(--pf-v5-global--BackgroundColor--dark-100);
+ --pf-v5-global--BackgroundColor--300: var(--pf-v5-global--BackgroundColor--dark-200);
+ }
+
+ // Redefine the background color for current links
+ .area-ct-subnav .pf-v5-c-nav__item {
+ > .pf-m-current, .pf-m-current + .nav-item-actions {
+ --pf-v5-c-nav__link--m-current--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-200);
+ }
+ }
+
+ // Add a border to the right of the sidebar (without affecting flow)
+ .ct-switcher > .pf-v5-c-select,
+ .area-ct-subnav.nav-system-menu.sidebar,
+ .navbar.navbar-default navbar-pf.navbar-pf-vertical {
+ &::after {
+ pointer-events: none;
+ border: 1px solid #444548;
+ border-width: 0;
+ border-inline-end: 1px;
+ position: absolute;
+ inset: 0;
+ content: "";
+ }
+ }
+}
+
+/* Navigation animation */
+
+@keyframes navHostsSlide {
+ 0% { opacity: 0.25; transform: scaleY(0); }
+ 25% { opacity: 0.5; transform: scaleY(0.25); }
+ 100% { opacity: 1; transform: scaleY(1); }
+}
+
+@keyframes navHostsEditButtonsAppear {
+ 0% { opacity: 0; max-block-size: 0; max-inline-size: 0; }
+ 50% { opacity: 0; max-block-size: 0; max-inline-size: 0; }
+ 75% { opacity: 0; max-block-size: 100%; max-inline-size: 100%; }
+ 100% { opacity: 1; max-block-size: 100%; max-inline-size: 100%; }
+}
diff --git a/pkg/shell/shell-modals.jsx b/pkg/shell/shell-modals.jsx
new file mode 100644
index 0000000..433b5ca
--- /dev/null
+++ b/pkg/shell/shell-modals.jsx
@@ -0,0 +1,203 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React, { useState } from "react";
+import { AboutModal } from "@patternfly/react-core/dist/esm/components/AboutModal/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Divider } from "@patternfly/react-core/dist/esm/components/Divider/index.js";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Menu, MenuContent, MenuSearch, MenuSearchInput, MenuItem, MenuList } from "@patternfly/react-core/dist/esm/components/Menu/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { Text, TextContent, TextList, TextListItem, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+import { SearchIcon } from '@patternfly/react-icons';
+
+import { useInit } from "hooks";
+import { useDialogs } from "dialogs.jsx";
+
+import "menu-select-widget.scss";
+
+const _ = cockpit.gettext;
+
+export const AboutCockpitModal = () => {
+ const Dialogs = useDialogs();
+ const [packages, setPackages] = useState(null);
+
+ useInit(() => {
+ const packages = [];
+ const cmd = "(set +e; rpm -qa --qf '%{NAME} %{VERSION}\\n'; dpkg-query -f '${Package} ${Version}\n' --show; pacman -Q) 2> /dev/null | grep cockpit | sort";
+ cockpit.spawn(["bash", "-c", cmd], [], { err: "message" })
+ .then(pkgs =>
+ pkgs.trim().split("\n")
+ .forEach(p => {
+ const parts = p.split(" ");
+ packages.push({ name: parts[0], version: parts[1] });
+ })
+ )
+ .catch(error => console.error("Could not read packages versions:", error))
+ .finally(() => setPackages(packages));
+ });
+
+ return (
+ <AboutModal
+ isOpen
+ onClose={Dialogs.close}
+ id="about-cockpit-modal"
+ trademark={_("Licensed under GNU LGPL version 2.1")}
+ productName={_("Web Console")}
+ brandImageSrc="../shell/images/cockpit-icon.svg"
+ brandImageAlt={_("Web console logo")}
+ >
+ <TextContent>
+ <Text component={TextVariants.p}>
+ {_("Cockpit is an interactive Linux server admin interface.")}
+ </Text>
+ <Text component={TextVariants.p}>
+ <Text component={TextVariants.a} href="https://cockpit-project.org" target="_blank" rel="noopener noreferrer">
+ {_("Project website")}
+ </Text>
+ </Text>
+ <TextList component="dl">
+ {packages === null && <span>{_("Loading packages...")}</span>}
+ {packages !== null && packages.map(p =>
+ <React.Fragment key={p.name}>
+ <TextListItem key={p.name} component="dt">{p.name}</TextListItem>
+ <TextListItem component="dd">{p.version}</TextListItem>
+ </React.Fragment>
+ )}
+ </TextList>
+ </TextContent>
+ </AboutModal>
+ );
+};
+
+export const LangModal = () => {
+ const language = document.cookie.replace(/(?:(?:^|.*;\s*)CockpitLang\s*=\s*([^;]*).*$)|^.*$/, "$1") || "en-us";
+
+ const Dialogs = useDialogs();
+ const [selected, setSelected] = useState(language);
+ const [searchInput, setSearchInput] = useState("");
+
+ function onSelect() {
+ if (!selected)
+ return;
+
+ const cookie = "CockpitLang=" + encodeURIComponent(selected) + "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT";
+ document.cookie = cookie;
+ window.localStorage.setItem("cockpit.lang", selected);
+ window.location.reload(true);
+ }
+
+ const manifest = cockpit.manifests.shell || { };
+
+ return (
+ <Modal isOpen position="top" variant="small"
+ id="display-language-modal"
+ className="display-language-modal"
+ onClose={Dialogs.close}
+ title={_("Display language")}
+ footer={<>
+ <Button variant='primary' onClick={onSelect}>{_("Select")}</Button>
+ <Button variant='link' onClick={Dialogs.close}>{_("Cancel")}</Button>
+ </>}
+ >
+ <Flex direction={{ default: 'column' }}>
+ <p>{_("Choose the language to be used in the application")}</p>
+ <Menu id="display-language-list"
+ isPlain
+ isScrollable
+ className="ct-menu-select-widget"
+ onSelect={(_, selected) => setSelected(selected)}
+ activeItemId={selected}
+ selected={selected}>
+ <MenuSearch>
+ <MenuSearchInput>
+ <TextInput
+ value={searchInput}
+ aria-label={_("Filter menu items")}
+ customIcon={<SearchIcon />}
+ type="search"
+ onChange={(_event, value) => setSearchInput(value)}
+ />
+ </MenuSearchInput>
+ </MenuSearch>
+ <Divider />
+ <MenuContent>
+ <MenuList>
+ {
+ (() => {
+ const filteredLocales = Object.keys(manifest.locales || {})
+ .filter(key => !searchInput || manifest.locales[key].toLowerCase().includes(searchInput.toString().toLowerCase()));
+
+ if (filteredLocales.length === 0) {
+ return (
+ <MenuItem isDisabled>
+ {_("No languages match")}
+ </MenuItem>
+ );
+ }
+ return filteredLocales.map(key => {
+ return (
+ <MenuItem itemId={key} key={key} data-value={key}>
+ {manifest.locales[key]}
+ </MenuItem>
+ );
+ });
+ })()
+ }
+ </MenuList>
+ </MenuContent>
+ </Menu>
+ </Flex>
+ </Modal>
+ );
+};
+
+export function TimeoutModal(props) {
+ return (
+ <Modal isOpen position="top" variant="medium"
+ showClose={false}
+ title={_("Session is about to expire")}
+ id="session-timeout-modal"
+ footer={<Button variant='primary' onClick={props.onClose}>{_("Continue session")}</Button>}
+ >
+ {props.text}
+ </Modal>
+ );
+}
+
+export function OopsModal(props) {
+ const Dialogs = useDialogs();
+ return (
+ <Modal isOpen position="top" variant="medium"
+ onClose={Dialogs.close}
+ title={_("Unexpected error")}
+ footer={<Button variant='secondary' onClick={Dialogs.close}>{_("Close")}</Button>}
+ >
+ {_("Cockpit had an unexpected internal error.")}
+ <br />
+ <br />
+ <span>{("You can try restarting Cockpit by pressing refresh in your browser. The javascript console contains details about this error") + " ("}
+ <b>{_("Ctrl-Shift-I")}</b>
+ {" " + _("in most browsers") + ")."}
+ </span>
+ </Modal>
+ );
+}
diff --git a/pkg/shell/shell.html b/pkg/shell/shell.html
new file mode 100644
index 0000000..6a6b000
--- /dev/null
+++ b/pkg/shell/shell.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Things have moved</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="stylesheet" href="index.css" />
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="curtains-ct">
+ <h1>Things have moved</h1>
+ <p>This old version of Cockpit doesn't know where to find default pages. Use the navigation menus to help it find its way.</p>
+ </div>
+</body>
+</html>
diff --git a/pkg/shell/shell.js b/pkg/shell/shell.js
new file mode 100644
index 0000000..28dd749
--- /dev/null
+++ b/pkg/shell/shell.js
@@ -0,0 +1,34 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+
+import { machines } from "./machines/machines.js";
+import * as indexes from "./indexes.jsx";
+
+import "./shell.scss";
+
+const machines_inst = machines.instance();
+const loader = machines.loader(machines_inst);
+
+const options = {
+ default_title: "Cockpit",
+};
+
+indexes.machines_index(options, machines_inst, loader);
diff --git a/pkg/shell/shell.scss b/pkg/shell/shell.scss
new file mode 100644
index 0000000..32d0048
--- /dev/null
+++ b/pkg/shell/shell.scss
@@ -0,0 +1,273 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+@use "page";
+@use "table";
+@use "nav";
+@import "@patternfly/patternfly/components/Select/select.css";
+@import "@patternfly/patternfly/utilities/Text/text.css";
+@import "@patternfly/patternfly/components/Icon/icon.css";
+
+:root {
+ --ct-color-host-accent: var(--pf-v5-global--active-color--100);
+ --ct-topnav-background: var(--pf-v5-global--BackgroundColor--dark-100);
+}
+
+.pf-v5-theme-dark {
+ --ct-topnav-background: var(--pf-v5-global--BackgroundColor--dark-200);
+}
+
+/* Hacks on top for now */
+
+html.index-page body {
+ overflow: hidden;
+}
+
+.hide-before::before {
+ display: none;
+}
+
+#machine-change-auth > p {
+ margin-block-end: 5px;
+}
+
+// Fix fingerprint layout
+.add-host-fingerprint {
+ margin-block-start: var(--pf-v5-global--spacer--md);
+
+ td {
+ padding: 0;
+ line-height: inherit;
+ block-size: auto;
+ vertical-align: middle !important;
+
+ &:first-child {
+ padding-inline-end: var(--pf-v5-global--spacer--md);
+ }
+ }
+}
+
+.machine-key {
+ margin: 0;
+ white-space: pre-line;
+ padding-block: var(--pf-v5-global--spacer--xs);
+ padding-inline: var(--pf-v5-global--spacer--sm);
+}
+
+.login-setup-checkbox-wrapper {
+ // Use a flex layout to better vertically align the contents to the grid
+ display: flex;
+}
+
+#edit-machine-port {
+ inline-size: 100px;
+}
+
+#connecting {
+ block-size: 100%;
+ background-color: var(--ct-global--palette--black-250);
+ text-align: center;
+ padding-block-start: 120px;
+}
+
+/* System information */
+
+#systime-date-input,
+#systime-time-hours,
+#systime-time-minutes {
+ display: inline;
+}
+
+/* index page */
+
+.display-language-modal {
+ /* Do not let the language menu expand to the end of the page */
+ .pf-v5-c-menu__content {
+ max-block-size: 20rem;
+ overflow: auto;
+ }
+}
+
+iframe.container-frame {
+ display: none;
+ border: none;
+ inline-size: 100%;
+}
+
+// Page layout
+
+$phone: 767px;
+$desktop: $phone + 1px;
+
+.page {
+ --nav-width: 15rem;
+ display: grid;
+ inline-size: 100%;
+ block-size: 100%;
+
+ // Set masthead and toolbar to transparent, so the background shows through
+ // (including the accent line)
+
+ .pf-v5-c-masthead {
+ --pf-v5-c-masthead--BackgroundColor: transparent;
+ }
+
+ .pf-v5-c-toolbar {
+ --pf-v5-c-toolbar--BackgroundColor: transparent;
+ }
+
+ @media (max-width: $phone) {
+ grid-template-areas: "main main main" "sidebar switcher header";
+ grid-template-rows: 1fr 4rem;
+ grid-template-columns: 1fr auto auto;
+ overflow: hidden;
+
+ .area-ct-subnav {
+ grid-area: main;
+ }
+
+ .sidebar-toggle {
+ grid-area: sidebar;
+ display: flex;
+ background: var(--pf-v5-global--BackgroundColor--dark-100);
+ }
+
+ > .header,
+ > .navbar,
+ > .sidebar-toggle {
+ // Line on bottom, Base color (with subtle glow)
+ background: linear-gradient(
+ to top,
+ var(--ct-color-host-accent),
+ var(--ct-color-host-accent) 0.1875rem,
+ transparent 0.1875rem
+ ),
+ linear-gradient(
+ to top,
+ var(--ct-color-host-accent) -1.5rem,
+ var(--ct-topnav-background) 0.75rem
+ );
+ }
+
+ // Remove excess padding from masthead in mobile
+ .pf-v5-c-masthead {
+ --pf-v5-c-masthead--inset: 0;
+ }
+ }
+
+ @media (min-width: $desktop) {
+ grid-template-areas: "switcher header" "sidebar main";
+ grid-template-rows: max-content 1fr;
+ grid-template-columns: minmax(min-content, var(--nav-width)) 1fr;
+
+ > .navbar {
+ max-inline-size: var(--nav-width);
+ }
+
+ .sidebar {
+ grid-area: sidebar;
+ }
+
+ .sidebar-toggle {
+ grid-area: none;
+ display: none;
+ }
+
+ > .header {
+ // Shadow to the left, Line on top, Base color (with subtle glow)
+ background: linear-gradient(
+ to right,
+ var(--ct-topnav-background) -1rem,
+ transparent 1rem
+ ),
+ linear-gradient(
+ to bottom,
+ var(--ct-color-host-accent),
+ var(--ct-color-host-accent) 0.1875rem,
+ transparent 0.1875rem
+ ),
+ linear-gradient(
+ to bottom,
+ var(--ct-color-host-accent) -1.5rem,
+ var(--ct-topnav-background) 0.75rem
+ );
+ }
+ }
+
+ // Shrink nav for VMs @ 1024×768 (and below)
+ @media (max-width: 1024px) and (max-height: 768px) and (orientation: landscape) {
+ --nav-width: 14rem;
+ }
+
+ .area-ct-content {
+ grid-area: main;
+ }
+
+ .header {
+ grid-area: header;
+ }
+}
+
+.hostkey-type {
+ font-size: small;
+}
+
+.hostkey-verify-help {
+ margin-block-end: 1.5em;
+}
+
+/* Alert fixups */
+.modal-content .pf-v5-c-alert {
+ text-align: start;
+ margin-block-end: 24px;
+}
+
+.early-failure {
+ block-size: 100%;
+}
+
+// Header menu buttons don't need a background color (especially in dark mode)
+.pf-v5-c-toolbar .pf-v5-c-dropdown__toggle {
+ --pf-v5-c-dropdown__toggle--BackgroundColor: transparent;
+}
+
+// Header menu buttons don't need a background color (especially in dark mode)
+.pf-v5-c-toolbar .pf-v5-c-dropdown__toggle {
+ --pf-v5-c-dropdown__toggle--BackgroundColor: transparent;
+}
+
+// PF hardcodes header colors to dark, which does funky things
+// ...so swap them back properly for dropdown menus
+.pf-v5-c-masthead .pf-v5-c-dropdown__menu {
+ --pf-v5-global--BackgroundColor--100: var(--pf-v5-global--BackgroundColor--light-100);
+ --pf-v5-global--Color--100: var(--pf-v5-global--Color--dark-100);
+}
+
+// Customize the about screen with the Cockpit logo
+.pf-v5-c-about-modal-box {
+ --pf-v5-c-about-modal-box--BackgroundImage: linear-gradient(#000d, #000d), url(../shell/images/cockpit-icon.svg);
+ --pf-v5-c-about-modal-box--BackgroundPosition: max(25rem, 143%) 10rem;
+ --pf-v5-c-about-modal-box--BackgroundSize--min-width: 786px;
+
+ [dir="rtl"] {
+ --pf-v5-c-about-modal-box--BackgroundPosition: calc(100% - 30rem) 10rem;
+ }
+}
diff --git a/pkg/shell/superuser.jsx b/pkg/shell/superuser.jsx
new file mode 100644
index 0000000..e2b0500
--- /dev/null
+++ b/pkg/shell/superuser.jsx
@@ -0,0 +1,349 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React, { useState } from "react";
+import { useObject, useInit, useEvent } from "hooks";
+import { useDialogs } from "dialogs.jsx";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { ModalError } from 'cockpit-components-inline-notification.jsx';
+import { host_superuser_storage_key } from './machines/machines';
+import { LockIcon } from '@patternfly/react-icons';
+
+const _ = cockpit.gettext;
+
+function sudo_polish(msg) {
+ if (!msg)
+ return msg;
+
+ msg = msg.replace(/^\[sudo] /, "");
+ msg = msg[0].toUpperCase() + msg.slice(1);
+
+ return msg;
+}
+
+const UnlockDialog = ({ proxy, host }) => {
+ const D = useDialogs();
+ useInit(init, [proxy, host]);
+
+ const [methods, setMethods] = useState(null);
+ const [method, setMethod] = useState(false);
+ const [busy, setBusy] = useState(false);
+ const [cancel, setCancel] = useState(() => D.close);
+ const [prompt, setPrompt] = useState(null);
+ const [message, setMessage] = useState(null);
+ const [error, setError] = useState(null);
+ const [errorVariant, setErrorVariant] = useState(null);
+ const [value, setValue] = useState("");
+
+ function start(method) {
+ setBusy(true);
+ setCancel(() => () => {
+ proxy.Stop();
+ D.close();
+ });
+
+ let did_prompt = false;
+
+ const onprompt = (event, message, prompt, def, echo, error) => {
+ setBusy(false);
+ setPrompt({
+ message: sudo_polish(message),
+ prompt: sudo_polish(prompt),
+ echo
+ });
+ setValue(def);
+
+ if (error) {
+ setError(sudo_polish(error));
+ setErrorVariant(did_prompt ? 'danger' : 'warning');
+ }
+
+ did_prompt = true;
+ };
+
+ proxy.addEventListener("Prompt", onprompt);
+ proxy.Start(method)
+ .then(() => {
+ proxy.removeEventListener("Prompt", onprompt);
+
+ const key = host_superuser_storage_key(host);
+ if (key)
+ window.localStorage.setItem(key, method);
+ if (did_prompt) {
+ D.close();
+ } else {
+ setBusy(false);
+ setPrompt(null);
+ setMessage(_("You now have administrative access."));
+ setCancel(() => D.close);
+ }
+ })
+ .catch(err => {
+ console.warn(err);
+ proxy.removeEventListener("Prompt", onprompt);
+ if (err && err.message != "cancelled") {
+ setBusy(false);
+ setPrompt(null);
+ setError(sudo_polish(err.toString()));
+ setCancel(() => D.close);
+ } else
+ D.close();
+ });
+ }
+
+ function init() {
+ return proxy.Stop().finally(() => {
+ if (proxy.Methods) {
+ const ids = Object.keys(proxy.Methods);
+ if (ids.length == 0)
+ start("sudo");
+ else if (ids.length == 1)
+ start(ids[0]);
+ else {
+ setMethods(ids);
+ setMethod(ids[0]);
+ }
+ } else
+ start("sudo");
+ });
+ }
+
+ const validated = errorVariant == "danger" ? "error" : errorVariant;
+
+ let title = null;
+ let title_icon = null;
+ let body = null;
+ let footer = null;
+
+ if (prompt) {
+ if (!prompt.message && !prompt.prompt) {
+ prompt.message = _("Please authenticate to gain administrative access");
+ prompt.prompt = _("Password");
+ }
+
+ const apply = () => {
+ proxy.Answer(value);
+ setError(null);
+ setBusy(true);
+ };
+
+ title = _("Switch to administrative access");
+ body = (
+ <Form isHorizontal onSubmit={event => { apply(); event.preventDefault(); return false }}>
+ { error && <Alert variant={errorVariant || 'danger'} isInline title={error} /> }
+ { prompt.message && <span>{prompt.message}</span> }
+ <FormGroup
+ fieldId="switch-to-admin-access-password"
+ label={prompt.prompt}
+ validated={!error ? "default" : validated || "error"}
+ >
+ <TextInput
+ autoFocus // eslint-disable-line jsx-a11y/no-autofocus
+ id="switch-to-admin-access-password"
+ isDisabled={busy}
+ onChange={(_event, value) => setValue(value)}
+ type={!prompt.echo ? 'password' : 'text'}
+ validated={!error ? "default" : validated || "error"}
+ value={value}
+ />
+ </FormGroup>
+ </Form>
+ );
+
+ footer = (
+ <>
+ <Button variant='primary' onClick={apply} isDisabled={busy} isLoading={busy}>
+ {_("Authenticate")}
+ </Button>
+ <Button variant='link' className='btn-cancel' onClick={cancel}>
+ {_("Cancel")}
+ </Button>
+ </>);
+ } else if (message) {
+ title = _("Administrative access");
+ body = <p>{message}</p>;
+ footer = (
+ <Button variant="secondary" className='btn-cancel' onClick={cancel}>
+ {_("Close")}
+ </Button>);
+ } else if (error) {
+ title_icon = "danger";
+ title = _("Problem becoming administrator");
+ body = <p>{error}</p>;
+ footer = (
+ <Button variant="secondary" className='btn-cancel' onClick={cancel}>
+ {_("Close")}
+ </Button>);
+ } else if (methods) {
+ title = _("Switch to administrative access");
+ body = (
+ <Form isHorizontal>
+ <FormGroup fieldId="switch-to-admin-access-bridge-select"
+ label={_("Method")}>
+ <FormSelect id="switch-to-admin-access-bridge-select" value={method} onChange={(_, method) => setMethod(method)} isDisabled={busy}>
+ { methods.map(m => <FormSelectOption value={m} key={m}
+ label={_(proxy.Methods[m].v.label.v)} />) }
+ </FormSelect>
+ </FormGroup>
+ </Form>);
+
+ footer = (
+ <>
+ <Button variant='primary' onClick={() => start(method)} isDisabled={busy} isLoading={busy}>
+ {_("Authenticate")}
+ </Button>
+ <Button variant='link' className='btn-cancel' onClick={cancel}>
+ {_("Cancel")}
+ </Button>
+ </>);
+ }
+
+ if (body === null)
+ return null;
+
+ return (
+ <Modal isOpen
+ position="top"
+ variant="medium"
+ onClose={cancel}
+ title={title}
+ titleIconVariant={title_icon}
+ footer={footer}>
+ {body}
+ </Modal>
+ );
+};
+
+const LockDialog = ({ proxy, host }) => {
+ const D = useDialogs();
+ const [error, setError] = useState(null);
+
+ const apply = () => {
+ setError(null);
+ proxy.Stop()
+ .then(() => {
+ const key = host_superuser_storage_key(host);
+ if (key)
+ window.localStorage.setItem(key, "none");
+ D.close();
+ })
+ .catch(err => {
+ setError(err.toString());
+ });
+ };
+
+ const footer = (
+ <>
+ <Button variant='primary' onClick={apply}>
+ {_("Limit access")}
+ </Button>
+ <Button variant='link' className='btn-cancel' onClick={D.close}>
+ {_("Cancel")}
+ </Button>
+ </>
+ );
+
+ return (
+ <Modal isOpen
+ position="top" variant="medium"
+ onClose={D.close}
+ footer={footer}
+ title={_("Switch to limited access")}>
+ <Stack hasGutter>
+ {error && <ModalError dialogError={error} />}
+ <StackItem>
+ <p>{_("Limited access mode restricts administrative privileges. Some parts of the web console will have reduced functionality.")}</p>
+ <p>{_("Your browser will remember your access level across sessions.")}</p>
+ </StackItem>
+ </Stack>
+ </Modal>
+ );
+};
+
+const SuperuserDialogs = ({ superuser_proxy, host, create_trigger }) => {
+ const D = useDialogs();
+ useEvent(superuser_proxy, "changed",
+ () => {
+ const key = host_superuser_storage_key(host);
+ if (key) {
+ // Reset wanted state if we fail to gain admin privs.
+ // Failing to gain admin privs might take a noticeable
+ // time, and we don't want to suffer through the
+ // associated intermediate UI state on every login.
+ const want = window.localStorage.getItem(key);
+ if (superuser_proxy.Current == "none" && superuser_proxy.Current != want)
+ window.localStorage.setItem(key, superuser_proxy.Current);
+ }
+ });
+
+ if (!superuser_proxy || !superuser_proxy.valid)
+ return;
+
+ const show = (superuser_proxy.Current != "root" && superuser_proxy.Current != "init" &&
+ (superuser_proxy.Bridges?.length ?? 0) > 0);
+ const unlocked = superuser_proxy.Current != "none";
+
+ function unlock() {
+ D.show(<UnlockDialog proxy={superuser_proxy} host={host} />);
+ }
+
+ function lock() {
+ D.show(<LockDialog proxy={superuser_proxy} host={host} />);
+ }
+
+ if (!show)
+ return null;
+
+ return create_trigger(unlocked, unlocked ? lock : unlock);
+};
+
+export const SuperuserIndicator = ({ proxy, host }) => {
+ function create_trigger(unlocked, onclick) {
+ return (
+ <Button variant="link" onClick={onclick} className={unlocked ? "ct-unlocked" : "ct-locked"}>
+ <span className="ct-lock-wrapper">
+ {!unlocked && <LockIcon />}
+ {unlocked ? _("Administrative access") : _("Limited access")}
+ </span>
+ </Button>
+ );
+ }
+
+ return <SuperuserDialogs superuser_proxy={proxy}
+ host={host}
+ create_trigger={create_trigger} />;
+};
+
+export const SuperuserButton = () => {
+ const proxy = useObject(() => cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Superuser", "/superuser"));
+
+ const create_trigger = (unlocked, onclick) =>
+ <Button onClick={onclick}>
+ {unlocked ? _("Switch to limited access") : _("Turn on administrative access")}
+ </Button>;
+
+ return <SuperuserDialogs superuser_proxy={proxy} create_trigger={create_trigger} />;
+};
diff --git a/pkg/shell/topnav.jsx b/pkg/shell/topnav.jsx
new file mode 100644
index 0000000..1ad8c3d
--- /dev/null
+++ b/pkg/shell/topnav.jsx
@@ -0,0 +1,289 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Dropdown, DropdownGroup, DropdownItem, DropdownPosition, DropdownSeparator, DropdownToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+import { Masthead, MastheadContent } from "@patternfly/react-core/dist/esm/components/Masthead/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { ToggleGroup, ToggleGroupItem } from "@patternfly/react-core/dist/esm/components/ToggleGroup/index.js";
+import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js";
+import { CogIcon, ExternalLinkAltIcon, HelpIcon } from '@patternfly/react-icons';
+
+import { ActivePagesDialog } from "./active-pages-modal.jsx";
+import { CredentialsModal } from './credentials.jsx';
+import { AboutCockpitModal, LangModal, OopsModal } from "./shell-modals.jsx";
+import { SuperuserIndicator } from "./superuser.jsx";
+import { read_os_release } from "os-release.js";
+import { DialogsContext } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+export class TopNav extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor(props) {
+ super(props);
+
+ let hash = props.state.hash;
+ let component = props.state.component;
+
+ if (props.machine && props.compiled.compat && props.compiled.compat[component]) { // Old cockpit packages used to be in shell/shell.html
+ hash = props.compiled.compat[component];
+ component = "shell/shell";
+ }
+ const frame = component ? props.index.frames.lookup(props.machine, component, hash) : undefined;
+
+ this.state = {
+ component,
+ frame,
+ docsOpened: false,
+ menuOpened: false,
+ showActivePages: false,
+ osRelease: {},
+ theme: localStorage.getItem('shell:style') || 'auto',
+ };
+
+ this.superuser_connection = null;
+ this.superuser = null;
+
+ read_os_release().then(os => this.setState({ osRelease: os || {} }));
+
+ this.handleClickOutside = () => this.setState({ menuOpened: false, docsOpened: false });
+ }
+
+ componentDidMount() {
+ /* This is a HACK for catching lost clicks on the pages which live in iframes so as to close dropdown menus on the shell.
+ * Note: Clicks on an <iframe> element won't trigger document.documentElement listeners, because it's literally different page with different security domain.
+ * However, when clicking on an iframe moves focus to its content's window that triggers the main window.blur event.
+ */
+ window.addEventListener("blur", this.handleClickOutside);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("blur", this.handleClickOutside);
+ }
+
+ static getDerivedStateFromProps(nextProps, prevState) {
+ let hash = nextProps.state.hash;
+ let component = nextProps.state.component;
+
+ if (nextProps.machine && nextProps.compiled.compat && nextProps.compiled.compat[component]) { // Old cockpit packages used to be in shell/shell.html
+ hash = nextProps.compiled.compat[component];
+ component = "shell/shell";
+ }
+
+ if (component !== prevState.component) {
+ const frame = component ? nextProps.index.frames.lookup(nextProps.machine, component, hash) : undefined;
+ return {
+ frame,
+ component,
+ };
+ }
+
+ return null;
+ }
+
+ handleModeClick = (event, isSelected) => {
+ const theme = event.currentTarget.id;
+ this.setState({ theme });
+
+ const styleEvent = new CustomEvent("cockpit-style", {
+ detail: {
+ style: theme,
+ }
+ });
+ window.dispatchEvent(styleEvent);
+
+ localStorage.setItem("shell:style", theme);
+ };
+
+ render() {
+ const Dialogs = this.context;
+ const connected = this.props.machine.state === "connected";
+
+ let docs = [];
+
+ if (!this.superuser_connection || (this.superuser_connection.options.host !=
+ this.props.machine.connection_string)) {
+ if (this.superuser_connection) {
+ this.superuser_connection.close();
+ this.superuser_connection = null;
+ }
+
+ if (connected) {
+ this.superuser_connection = cockpit.dbus(null, { bus: "internal", host: this.props.machine.connection_string });
+ this.superuser = this.superuser_connection.proxy("cockpit.Superuser", "/superuser");
+ }
+ }
+
+ const item = this.props.compiled.items[this.props.state.component];
+ if (item && item.docs)
+ docs = item.docs;
+
+ // Check for parent as well
+ if (docs.length === 0) {
+ const comp = cockpit.manifests[this.props.state.component];
+ if (comp && comp.parent && comp.parent.docs)
+ docs = comp.parent.docs;
+ }
+
+ const docItems = [];
+
+ if (this.state.osRelease.DOCUMENTATION_URL)
+ docItems.push(<DropdownItem key="os-doc" href={this.state.osRelease.DOCUMENTATION_URL} target="blank" rel="noopener noreferrer" icon={<ExternalLinkAltIcon />}>
+ {cockpit.format(_("$0 documentation"), this.state.osRelease.NAME)}
+ </DropdownItem>);
+
+ // global documentation for cockpit as a whole
+ (cockpit.manifests.shell?.docs ?? []).forEach(doc => {
+ docItems.push(<DropdownItem key={doc.label} href={doc.url} target="blank" rel="noopener noreferrer" icon={<ExternalLinkAltIcon />}>
+ {doc.label}
+ </DropdownItem>);
+ });
+
+ if (docs.length > 0)
+ docItems.push(<DropdownSeparator key="separator" />);
+
+ docs.forEach(e => {
+ docItems.push(<DropdownItem key={e.label} href={e.url} target="blank" rel="noopener noreferrer" icon={<ExternalLinkAltIcon />}>
+ {_(e.label)}
+ </DropdownItem>);
+ });
+
+ docItems.push(<DropdownSeparator key="separator1" />);
+ docItems.push(<DropdownItem key="about" component="button"
+ onClick={() => Dialogs.show(<AboutCockpitModal />)}>
+ {_("About Web Console")}
+ </DropdownItem>);
+
+ const manifest = cockpit.manifests.shell || { };
+
+ const main_menu = [
+ <div id="super-user-indicator-mobile" className="mobile_v" key="superusermobile">
+ <SuperuserIndicator proxy={this.superuser} host={this.props.machine.connection_string} />
+ </div>,
+ <DropdownSeparator key="separator2" className="mobile_v" />,
+ <DropdownGroup label={_("Style")} key="dark-switcher">
+ <DropdownItem key="dark-switcher-menu" component="div" isPlainText>
+ <ToggleGroup key="dark-switcher-togglegroup">
+ <ToggleGroupItem key="dark-switcher-auto" buttonId="auto" text={_("Default")}
+ isSelected={this.state.theme === "auto"}
+ onChange={this.handleModeClick} />
+ <ToggleGroupItem key="dark-switcher-light" buttonId="light" text={_("Light")}
+ isSelected={this.state.theme === "light"}
+ onChange={this.handleModeClick} />
+ <ToggleGroupItem key="dark-switcher-dark" buttonId="dark" text={_("Dark")}
+ isSelected={this.state.theme === "dark"}
+ onChange={this.handleModeClick} />
+ </ToggleGroup>
+ </DropdownItem>
+ </DropdownGroup>,
+ <DropdownSeparator key="separatorDark" />,
+ ];
+
+ if (manifest.locales)
+ main_menu.push(<DropdownItem key="languages" className="display-language-menu" component="button"
+ onClick={() => Dialogs.show(<LangModal />)}>
+ {_("Display language")}
+ </DropdownItem>);
+
+ if (this.state.showActivePages)
+ main_menu.push(
+ <DropdownItem key="frames" id="active-pages" component="button"
+ onClick={() => Dialogs.show(<ActivePagesDialog frames={this.props.index.frames} />)}>
+ {_("Active pages")}
+ </DropdownItem>);
+
+ main_menu.push(
+ <DropdownItem key="creds" id="sshkeys" component="button"
+ onClick={() => Dialogs.show(<CredentialsModal />)}>
+ {_("SSH keys")}
+ </DropdownItem>,
+ <DropdownSeparator key="separator3" />,
+ <DropdownItem key="logout" id="logout" component="button" onClick={cockpit.logout}>
+ {_("Log out")}
+ </DropdownItem>,
+ );
+
+ return (
+ <Masthead>
+ <MastheadContent>
+ <Toolbar id="toolbar" isFullHeight isStatic>
+ <ToolbarContent className="ct-topnav-content">
+ {(connected && this.state.frame && !this.state.frame.getAttribute("data-ready")) &&
+ <ToolbarItem id="machine-spinner">
+ <Spinner size="lg" style={{ "--pf-v5-c-spinner--Color": "#fff", "--pf-v5-c-spinner--diameter": "2rem" }} />
+ </ToolbarItem>
+ }
+ { connected &&
+ <ToolbarItem id="super-user-indicator" className="super-user-indicator desktop_v">
+ <SuperuserIndicator proxy={this.superuser} host={this.props.machine.connection_string} />
+ </ToolbarItem>
+ }
+ { this.props.index.has_oops &&
+ <ToolbarItem>
+ <Button id="navbar-oops" variant="link" size="lg" isDanger
+ onClick={() => Dialogs.show(<OopsModal />)}>{_("Ooops!")}</Button>
+ </ToolbarItem>
+ }
+ <ToolbarItem>
+ <Dropdown
+ onSelect={() => {
+ this.setState(prevState => { return { docsOpened: !prevState.docsOpened } });
+ document.getElementById("toggle-docs").focus();
+ }}
+ toggle={
+ <DropdownToggle id="toggle-docs" icon={<HelpIcon className="toggle-docs-icon pf-v5-c-icon pf-m-lg" />} onToggle={(_event, isOpen) => { this.setState({ docsOpened: isOpen }) }}>
+ {_("Help")}
+ </DropdownToggle>
+ }
+ isOpen={this.state.docsOpened}
+ dropdownItems={docItems}
+ position={DropdownPosition.right}
+ isFullHeight
+ className="ct-header-item ct-nav-toggle"
+ />
+ </ToolbarItem>
+ <ToolbarItem>
+ <Dropdown
+ onSelect={() => {
+ this.setState(prevState => { return { menuOpened: !prevState.menuOpened } });
+ document.getElementById("toggle-menu").focus();
+ }}
+ toggle={
+ <DropdownToggle id="toggle-menu" icon={<CogIcon className="pf-v5-c-icon pf-m-lg" />} onToggle={(_event, isOpen) => this.setState({ menuOpened: isOpen, showActivePages: _event.altKey }) }>
+ {_("Session")}
+ </DropdownToggle>
+ }
+ isOpen={this.state.menuOpened}
+ dropdownItems={main_menu}
+ position={DropdownPosition.right}
+ isFullHeight
+ className="ct-header-item ct-nav-toggle"
+ />
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ </MastheadContent>
+ </Masthead>
+ );
+ }
+}
diff --git a/pkg/sosreport/cockpit-sosreport.png b/pkg/sosreport/cockpit-sosreport.png
new file mode 100644
index 0000000..9f3923e
--- /dev/null
+++ b/pkg/sosreport/cockpit-sosreport.png
Binary files differ
diff --git a/pkg/sosreport/index.html b/pkg/sosreport/index.html
new file mode 100644
index 0000000..1d770cc
--- /dev/null
+++ b/pkg/sosreport/index.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2021 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html>
+ <head>
+ <title translate="yes">Diagnostic reports</title>
+ <meta charset="utf-8" />
+ <link href="sosreport.css" rel="stylesheet" />
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script type="text/javascript" src="sosreport.js"></script>
+ </head>
+ <body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app"></div>
+ </body>
+</html>
diff --git a/pkg/sosreport/manifest.json b/pkg/sosreport/manifest.json
new file mode 100644
index 0000000..6f33ae2
--- /dev/null
+++ b/pkg/sosreport/manifest.json
@@ -0,0 +1,12 @@
+{
+ "tools": {
+ "index": {
+ "label": "Diagnostic reports",
+ "keywords": [
+ {
+ "matches": ["sos"]
+ }
+ ]
+ }
+ }
+}
diff --git a/pkg/sosreport/sosreport.jsx b/pkg/sosreport/sosreport.jsx
new file mode 100644
index 0000000..f99a1e3
--- /dev/null
+++ b/pkg/sosreport/sosreport.jsx
@@ -0,0 +1,532 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import './sosreport.scss';
+import "polyfills";
+import 'cockpit-dark-theme'; // once per page
+
+import React, { useState } from "react";
+import { createRoot } from 'react-dom/client';
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { CodeBlockCode } from "@patternfly/react-core/dist/esm/components/CodeBlock/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Label, LabelGroup } from "@patternfly/react-core/dist/esm/components/Label/index.js";
+import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { InputGroup } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
+
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { ListingTable } from "cockpit-components-table.jsx";
+
+import cockpit from "cockpit";
+import { superuser } from "superuser";
+import { useObject, useEvent } from "hooks";
+
+import { SuperuserButton } from "../shell/superuser.jsx";
+
+import { fmt_to_fragments } from "utils.jsx";
+import * as timeformat from "timeformat";
+import { WithDialogs, useDialogs } from "dialogs.jsx";
+import { FormHelper } from "cockpit-components-form-helper";
+
+const _ = cockpit.gettext;
+
+function sosLister() {
+ const self = {
+ ready: false,
+ problem: null,
+ reports: {}
+ };
+
+ cockpit.event_target(self);
+
+ function emit_changed() {
+ self.dispatchEvent("changed");
+ }
+
+ function parse_report_name(path) {
+ const basename = path.replace(/.*\//, "");
+ const archive_rx = /^(secured-)?sosreport-(.*)\.tar\.[^.]+(\.gpg)?$/;
+ const m = basename.match(archive_rx);
+ if (m) {
+ let name = m[2];
+ let obfuscated = false;
+ if (name.endsWith("-obfuscated")) {
+ obfuscated = true;
+ name = name.replace(/-obfuscated$/, "");
+ }
+
+ return {
+ encrypted: !!m[1],
+ obfuscated,
+ name
+ };
+ }
+ }
+
+ function update_reports() {
+ cockpit.script('find /var/tmp -maxdepth 1 -name \'*sosreport-*.tar.*\' -print0 | xargs -0 -r stat --printf="%n\\r%W\\n"', { superuser: true, err: "message" })
+ .then(output => {
+ const reports = { };
+ const lines = output.split("\n");
+ for (const line of lines) {
+ const [path, date] = line.split("\r");
+ const report = parse_report_name(path);
+ if (report) {
+ report.date = Number(date);
+ reports[path] = report;
+ }
+ }
+ self.reports = reports;
+ self.ready = true;
+ emit_changed();
+ })
+ .catch(err => {
+ self.problem = err.problem || err.message;
+ self.ready = true;
+ emit_changed();
+ });
+ }
+
+ let watch = null;
+
+ function restart() {
+ if (superuser.allowed === null)
+ return;
+
+ if (watch)
+ watch.close("cancelled");
+ self.ready = false;
+ self.problem = null;
+ watch = null;
+
+ watch = cockpit.channel({ payload: "fswatch1", path: "/var/tmp", superuser: true });
+ watch.addEventListener("message", (event, payload) => {
+ const msg = JSON.parse(payload);
+ if (msg.event != "present" && parse_report_name(msg.path))
+ update_reports();
+ });
+
+ update_reports();
+ }
+
+ restart();
+ superuser.addEventListener("changed", restart);
+ return self;
+}
+
+function sosCreate(args, setProgress, setError, setErrorDetail) {
+ let output = "";
+ let plugins_count = 0;
+ const progress_regex = /Running ([0-9]+)\/([0-9]+):/; // Only for sos < 3.6
+ const finishing_regex = /Finishing plugins.*\[Running: (.*)\]/;
+ const starting_regex = /Starting ([0-9]+)\/([0-9]+).*\[Running: (.*)\]/;
+
+ // TODO - Use a real API instead of scraping stdout once such an API exists
+ const task = cockpit.spawn(["sos", "report", "--batch"].concat(args),
+ { superuser: true, err: "out", pty: true });
+
+ task.stream(text => {
+ let p = 0;
+ let m;
+
+ output += text;
+ const lines = output.split("\n");
+ for (let i = lines.length - 1; i >= 0; i--) {
+ if ((m = starting_regex.exec(lines[i]))) {
+ plugins_count = parseInt(m[2], 10);
+ p = ((parseInt(m[1], 10) - m[3].split(" ").length) / plugins_count) * 100;
+ break;
+ } else if ((m = finishing_regex.exec(lines[i]))) {
+ if (!plugins_count)
+ p = 100;
+ else
+ p = ((plugins_count - m[1].split(" ").length) / plugins_count) * 100;
+ break;
+ } else if ((m = progress_regex.exec(lines[i]))) {
+ p = (parseInt(m[1], 10) / parseInt(m[2], 10)) * 100;
+ break;
+ }
+ }
+
+ setProgress(p);
+ });
+
+ task.catch(error => {
+ // easier investigation of failures, errors in pty mode may be hard to see
+ if (error.problem !== 'cancelled')
+ console.error("Failed to call sos report:", JSON.stringify(error));
+ setError(error.toString() || _("sos report failed"));
+ setErrorDetail(output);
+ });
+
+ return task;
+}
+
+function sosDownload(path) {
+ const basename = path.replace(/.*\//, "");
+ const query = window.btoa(JSON.stringify({
+ payload: "fsread1",
+ binary: "raw",
+ path,
+ superuser: true,
+ max_read_size: 150 * 1024 * 1024,
+ external: {
+ "content-disposition": 'attachment; filename="' + basename + '"',
+ "content-type": "application/x-xz, application/octet-stream"
+ }
+ }));
+ const prefix = (new URL(cockpit.transport.uri("channel/" + cockpit.transport.csrf_token))).pathname;
+ const url = prefix + '?' + query;
+ return new Promise((resolve, reject) => {
+ // We download via a hidden iframe to get better control over the error cases
+ const iframe = document.createElement("iframe");
+ iframe.setAttribute("src", url);
+ iframe.setAttribute("hidden", "hidden");
+ iframe.addEventListener("load", () => {
+ const title = iframe.contentDocument.title;
+ if (title) {
+ reject(title);
+ } else {
+ resolve();
+ }
+ });
+ document.body.appendChild(iframe);
+ });
+}
+
+function sosRemove(path) {
+ return cockpit.script(cockpit.format("rm -f '$0' '$0'.*", path), { superuser: true, err: "message" });
+}
+
+const SOSDialog = () => {
+ const Dialogs = useDialogs();
+ const [label, setLabel] = useState("");
+ const [passphrase, setPassphrase] = useState("");
+ const [showPassphrase, setShowPassphrase] = useState(false);
+ const [obfuscate, setObfuscate] = useState(false);
+ const [verbose, setVerbose] = useState(false);
+ const [task, setTask] = useState(null);
+ const [progress, setProgress] = useState(null);
+ const [error, setError] = useState(null);
+ const [errorDetail, setErrorDetail] = useState(null);
+
+ function run() {
+ setError(null);
+ setProgress(null);
+
+ const args = [];
+
+ if (label) {
+ args.push("--label");
+ args.push(label);
+ }
+
+ if (passphrase) {
+ args.push("--encrypt-pass");
+ args.push(passphrase);
+ }
+
+ if (obfuscate) {
+ args.push("--clean");
+ }
+
+ if (verbose) {
+ args.push("-v");
+ }
+
+ const task = sosCreate(args, setProgress, err => { if (err == "cancelled") Dialogs.close(); else setError(err); },
+ setErrorDetail);
+ setTask(task);
+ task.then(Dialogs.close);
+ task.finally(() => setTask(null));
+ }
+
+ const actions = [];
+ actions.push(<Button key="run" isLoading={!!task} isDisabled={!!task} onClick={run}>
+ {_("Run report")}
+ </Button>);
+ if (task)
+ actions.push(<Button key="stop" variant="secondary" onClick={() => task.close("cancelled")}>
+ {_("Stop report")}
+ </Button>);
+ else
+ actions.push(<Button key="cancel" variant="link" onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>);
+
+ return <Modal id="sos-dialog"
+ position="top"
+ variant="medium"
+ isOpen
+ onClose={Dialogs.close}
+ footer={
+ <>
+ {actions}
+ {progress ? <span>{cockpit.format(_("Progress: $0"), progress.toFixed() + "%")}</span> : null}
+ </>
+ }
+ title={ _("Run new report") }>
+ { error
+ ? <>
+ <Alert variant="warning" isInline title={error}>
+ <CodeBlockCode>{errorDetail}</CodeBlockCode>
+ </Alert>
+ <br />
+ </>
+ : null }
+ <p>{ _("SOS reporting collects system information to help with diagnosing problems.") }</p>
+ <p>{ _("This information is stored only on the system.") }</p>
+ <br />
+ <Form isHorizontal>
+ <FormGroup label={_("Report label")}>
+ <TextInput id="sos-dialog-ti-1" value={label} onChange={(_event, value) => setLabel(value)} />
+ </FormGroup>
+ <FormGroup label={_("Encryption passphrase")}>
+ <InputGroup>
+ <TextInput type={showPassphrase ? "text" : "password"} value={passphrase} onChange={(_event, value) => setPassphrase(value)}
+ id="sos-dialog-ti-2" autoComplete="new-password" />
+ <Button variant="control" onClick={() => setShowPassphrase(!showPassphrase)}>
+ { showPassphrase ? <EyeSlashIcon /> : <EyeIcon /> }
+ </Button>
+ </InputGroup>
+ <FormHelper helperText={_("Leave empty to skip encryption")} />
+ </FormGroup>
+ <FormGroup label={_("Options")} hasNoPaddingTop>
+ <Checkbox label={_("Obfuscate network addresses, hostnames, and usernames")}
+ id="sos-dialog-cb-1" isChecked={obfuscate} onChange={(_, o) => setObfuscate(o)} />
+ <Checkbox label={_("Use verbose logging")}
+ id="sos-dialog-cb-2" isChecked={verbose} onChange={(_, v) => setVerbose(v)} />
+ </FormGroup>
+ </Form>
+ </Modal>;
+};
+
+const SOSRemoveDialog = ({ path }) => {
+ const Dialogs = useDialogs();
+ const [task, setTask] = useState(null);
+ const [error, setError] = useState(null);
+
+ function remove() {
+ setError(null);
+ setTask(sosRemove(path)
+ .then(Dialogs.close)
+ .catch(err => {
+ setTask(null);
+ setError(err.toString());
+ }));
+ }
+
+ return (
+ <Modal id="sos-remove-dialog"
+ position="top"
+ variant="medium"
+ isOpen
+ onClose={Dialogs.close}
+ title={_("Delete report permanently?")}
+ titleIconVariant="warning"
+ actions={[
+ <Button key="apply"
+ variant="danger"
+ onClick={remove}
+ isLoading={!!task}
+ isDisabled={!!task}>
+ {_("Delete")}
+ </Button>,
+ <Button key="cancel"
+ onClick={Dialogs.close}
+ isDisabled={!!task}
+ variant="link">
+ {_("Cancel")}
+ </Button>
+ ]}>
+ { error && <><Alert variant="warning" isInline title={error} /><br /></> }
+ <p>{fmt_to_fragments(_("The file $0 will be deleted."), <b>{path}</b>)}</p>
+ </Modal>);
+};
+
+const SOSErrorDialog = ({ error }) => {
+ const Dialogs = useDialogs();
+
+ return (
+ <Modal id="sos-error-dialog"
+ position="top"
+ variant="medium"
+ isOpen
+ onClose={Dialogs.close}
+ title={ _("Error") }>
+ <p>{error}</p>
+ </Modal>);
+};
+
+const Menu = ({ items }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+ <Dropdown onSelect={() => setIsOpen(!isOpen)}
+ toggle={<KebabToggle onToggle={(_, isOpen) => setIsOpen(isOpen)} />}
+ isOpen={isOpen}
+ isPlain
+ position="right"
+ dropdownItems={items} />
+ );
+};
+
+const MenuItem = ({ onClick, onlyNarrow, children }) => (
+ <DropdownItem className={onlyNarrow ? "show-only-when-narrow" : null}
+ onKeyDown={onClick}
+ onClick={onClick}>
+ {children}
+ </DropdownItem>
+);
+
+const SOSBody = () => {
+ const Dialogs = useDialogs();
+ const lister = useObject(sosLister, obj => obj.close, []);
+ useEvent(lister, "changed");
+
+ const superuser_proxy = useObject(() => cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Superuser",
+ "/superuser"),
+ obj => obj.close(),
+ []);
+ useEvent(superuser_proxy, "changed");
+
+ if (!lister.ready)
+ return <EmptyStatePanel loading />;
+
+ if (lister.problem) {
+ if (lister.problem == "access-denied")
+ return (
+ <EmptyStatePanel
+ title={_("Administrative access required")}
+ paragraph={_("Administrative access is required to create and access reports.")}
+ action={<SuperuserButton />} />);
+ else
+ return <EmptyStatePanel title={lister.problem} />;
+ }
+
+ function run_report() {
+ Dialogs.show(<SOSDialog />);
+ }
+
+ function make_report_row(path) {
+ const report = lister.reports[path];
+
+ function download() {
+ sosDownload(path).catch(err => Dialogs.show(<SOSErrorDialog error={err.toString()} />));
+ }
+
+ function remove() {
+ Dialogs.show(<SOSRemoveDialog path={path} />);
+ }
+
+ const labels = [];
+ if (report.encrypted)
+ labels.push(<Label key="enc" color="orange">
+ {_("Encrypted")}
+ </Label>);
+ if (report.obfuscated)
+ labels.push(<Label key="obf" color="gray">
+ {_("Obfuscated")}
+ </Label>);
+
+ const action = (
+ <Button variant="secondary" className="show-only-when-wide"
+ onClick={download}>
+ {_("Download")}
+ </Button>);
+ const menu = <Menu items={[
+ <MenuItem key="download"
+ onlyNarrow
+ onClick={download}>
+ {_("Download")}
+ </MenuItem>,
+ <MenuItem key="remove"
+ onClick={remove}>
+ {_("Delete")}
+ </MenuItem>
+ ]} />;
+
+ return {
+ props: { key: path },
+ columns: [
+ report.name,
+ timeformat.distanceToNow(new Date(report.date * 1000), true),
+ { title: <LabelGroup>{labels}</LabelGroup> },
+ {
+ title: <>{action}{menu}</>,
+ props: { className: "pf-v5-c-table__action table-row-action" }
+ },
+ ]
+ };
+ }
+
+ return (
+ <PageSection>
+ <Card className="ct-card">
+ <CardHeader actions={{
+ actions: <Button id="create-button" variant="primary" onClick={run_report}>
+ {_("Run report")}
+ </Button>,
+ }}>
+ <CardTitle component="h2">{_("Reports")}</CardTitle>
+ </CardHeader>
+ <CardBody className="contains-list">
+ <ListingTable emptyCaption={_("No system reports.")}
+ columns={ [
+ { title: _("Report") },
+ { title: _("Created") },
+ { title: _("Attributes") },
+ ] }
+ rows={Object
+ .keys(lister.reports)
+ .sort((a, b) => lister.reports[b].date - lister.reports[a].date)
+ .map(make_report_row)} />
+ </CardBody>
+ </Card>
+ </PageSection>);
+};
+
+const SOSPage = () => {
+ return (
+ <WithDialogs>
+ <Page>
+ <PageSection padding={{ default: "padding" }} variant={PageSectionVariants.light}>
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <h2 className="pf-v5-u-font-size-3xl">{_("System diagnostics")}</h2>
+ </Flex>
+ </PageSection>
+ <SOSBody />
+ </Page>
+ </WithDialogs>);
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+ cockpit.translate();
+ const root = createRoot(document.getElementById('app'));
+ root.render(<SOSPage />);
+});
diff --git a/pkg/sosreport/sosreport.png b/pkg/sosreport/sosreport.png
new file mode 100644
index 0000000..df1440f
--- /dev/null
+++ b/pkg/sosreport/sosreport.png
Binary files differ
diff --git a/pkg/sosreport/sosreport.scss b/pkg/sosreport/sosreport.scss
new file mode 100644
index 0000000..51918b7
--- /dev/null
+++ b/pkg/sosreport/sosreport.scss
@@ -0,0 +1,25 @@
+@use "page";
+@use "ct-card.scss";
+@use "../../node_modules/@patternfly/patternfly/utilities/Text/text.css";
+@import "global-variables";
+
+.table-row-action {
+ text-align: end;
+ white-space: nowrap !important;
+}
+
+.hidden {
+ display: none;
+}
+
+@media (max-width: $pf-v5-global--breakpoint--md - 1) {
+ .show-only-when-wide {
+ display: none !important;
+ }
+}
+
+@media (min-width: $pf-v5-global--breakpoint--md) {
+ .show-only-when-narrow {
+ display: none !important;
+ }
+}
diff --git a/pkg/static/login.html b/pkg/static/login.html
new file mode 100644
index 0000000..d912019
--- /dev/null
+++ b/pkg/static/login.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <title>Loading...</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="robots" content="noindex" />
+ <meta insert="dynamic_content_here" />
+ <script type="text/javascript">/*insert_translations_here*/</script>
+ <script type="text/javascript" src="cockpit/static/login.js"></script>
+ <link href="cockpit/static/login.css" type="text/css" rel="stylesheet" />
+ <link href="cockpit/static/branding.css" type="text/css" rel="stylesheet" />
+</head>
+
+<body class="login-pf">
+ <div id="banner" class="pf-v5-c-alert pf-m-info pf-m-inline dialog-error" aria-label="inline danger alert" hidden="true">
+ <svg fill="currentColor" viewBox="0 0 448 512" aria-hidden="true">
+ <path d="M224 512c35.32 0 63.97-28.65 63.97-64H160.03c0 35.35 28.65 64 63.97 64zm215.39-149.71c-19.32-20.76-55.47-51.99-55.47-154.29 0-77.7-54.48-139.9-127.94-155.16V32c0-17.67-14.32-32-31.98-32s-31.98 14.33-31.98 32v20.84C118.56 68.1 64.08 130.3 64.08 208c0 102.3-36.15 133.53-55.47 154.29-6 6.45-8.66 14.16-8.61 21.71.11 16.4 12.98 32 32.1 32h383.8c19.12 0 32-15.6 32.1-32 .05-7.55-2.61-15.27-8.61-21.71z" />
+ </svg>
+ <span id="banner-message" class="pf-v5-c-alert__title"></span>
+ </div>
+
+ <span id="badge"></span>
+
+ <div class="container" id="main">
+ <h1 id="brand" class="hide-before"></h1>
+
+ <div id="error-group" class="pf-v5-c-alert pf-m-danger pf-m-inline dialog-error noscript" aria-label="inline danger alert">
+ <svg fill="currentColor" viewBox="0 0 512 512" aria-hidden="true">
+ <path d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h28.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z" />
+ </svg>
+ <h2 id="login-error-message" class="pf-v5-c-alert__title">
+ <span class="noscript" translate="yes">Please enable JavaScript to use the Web Console.</span>
+ </h2>
+ </div>
+
+ <div class="unsupported-browser" id="unsupported-browser" hidden="true">
+ <h2 class="unsupported-browser-heading" translate="yes">A modern browser is required for security, reliability, and performance.</h2>
+ <div class="browser-recommendations">
+ <div class="browser-download">
+ <h3 translate="yes">Download a new browser for free</h3>
+ <ul>
+ <li><a href="https://firefox.com/">Mozilla Firefox</a> / Linux, Windows, macOS</li>
+ <li><a href="https://www.google.com/chrome/">Google Chrome</a> / Linux, Windows, macOS</li>
+ </ul>
+ </div>
+ <div class="browser-bundled">
+ <h3 translate="yes">Or use a bundled browser</h3>
+ <ul>
+ <li><a href="https://www.microsoft.com/">Microsoft Edge</a> / Windows</li>
+ <li><a href="https://www.apple.com/safari/">Apple Safari</a> / macOS</li>
+ </ul>
+ </div>
+ </div>
+ <details id="login-override">
+ <summary class="pf-v5-c-expandable-section">
+ <svg height="16" width="16" viewBox="0 0 16 16" id="option-caret" class="caret caret-right" aria-hidden="true">
+ <polygon fill="#ffffff" points="4,0 4,14 12,7"></polygon>
+ </svg>
+ <span id="bypass-browser-check" translate="yes">Bypass browser check</span>
+ </summary>
+ <div id="login-override-content"></div>
+ </details>
+ </div>
+
+ <div id="info-group" class="pf-v5-c-alert pf-m-info pf-m-inline dialog-error" aria-label="inline danger alert" hidden="true">
+ <svg fill="currentColor" viewBox="0 0 512 512" aria-hidden="true">
+ <path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z" />
+ </svg>
+ <h2 id="login-info-message" class="pf-v5-c-alert__title"></h2>
+ </div>
+
+ <div id="login" class="login-area" hidden="true">
+ <form onsubmit="return false">
+
+ <div id="hostkey-group" class="form-group" hidden="true">
+ <h1 id="hostkey-title"></h1>
+ <div id="hostkey-warning-group" class="pf-v5-c-alert pf-m-warning pf-m-inline dialog-error" aria-label="inline warning alert" hidden="true">
+ <svg fill="currentColor" viewBox="0 0 576 512" aria-hidden="true"><path d="M569.52 440.01c18.46 32-4.71 71.99-41.58 71.99H48.05c-36.93 0-60-40.05-41.57-71.99L246.42 24c18.47-32.01 64.72-31.96 83.16 0L569.52 440zM288 354a46 46 0 100 92 46 46 0 000-92zm-43.67-165.35l7.41 136A12 12 0 00263.74 336h48.54a12 12 0 0011.98-11.35l7.42-136A12 12 0 00319.7 176h-63.38a12 12 0 00-11.98 12.65z"/></svg>
+ <h2 translate="yes" class="pf-v5-c-alert__title">Changed keys are often the result of an operating system reinstallation. However, an unexpected change may indicate a third-party attempt to intercept your connection.</h2>
+ </div>
+ <p id="hostkey-message-1"></p>
+ <p translate="yes">To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:</p>
+ <pre id="hostkey-fingerprint"></pre>
+ <p id="hostkey-type"></p>
+ <p id="hostkey-verify-help-1"></p>
+ <pre id="hostkey-verify-help-cmds"></pre>
+ <p translate="yes">The resulting fingerprint is fine to share via public methods, including email.</p>
+ <p translate="yes">If the fingerprint matches, click "Accept key and log in". Otherwise, do not log in and contact your administrator.</p>
+ </div>
+
+ <div id="user-group" class="form-group">
+ <label for="login-user-input" class="control-label" translate="yes">User name</label>
+ <input type="text" class="form-control" id="login-user-input" autocorrect="off" autocapitalize="none" autofocus="true" autocomplete="username" />
+ </div>
+
+ <div id="password-group" class="form-group">
+ <label for="login-password-input" class="control-label" translate="yes">Password</label>
+ <div class="password-with-toggle">
+ <input type="password" class="form-control" id="login-password-input" autocomplete="current-password" />
+ <button type="button" id="login-password-toggle" class="pf-v5-c-button pf-m-control login-password-toggle" aria-label="Show password">
+ <svg fill="currentColor" aria-hidden="true" viewBox="0 0 640 512">
+ <path class="password-show" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"/>
+ <path class="password-hide" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"/>
+ </svg>
+ </button>
+ </div>
+ </div>
+
+ <div id="conversation-group" class="form-group" hidden="true">
+ <div id="conversation-message"></div>
+ <label id="conversation-prompt" for="conversation-input"></label>
+ <input type="password" class="form-control" id="conversation-input" autocomplete="one-time-code" />
+ </div>
+
+ <div id="option-group">
+ <a href="#" id="show-other-login-options">
+ <svg height="16" width="16" viewBox="0 0 16 16" id="option-caret" class="caret caret-right" aria-hidden="true">
+ <polygon fill="#ffffff" points="4,0 4,14 12,7"></polygon>
+ </svg><span id="show-other-login-options-text" translate="yes">Other options</span>
+ <!-- Above: Span needs to be immediately next to SVG to prevent whitespace in the link -->
+ </a>
+ </div>
+
+ <div id="server-group" class="form-group" hidden="true">
+ <label title="Log in to another system. Leave blank to log in to the local system." for="server-field" id="server-field-label" class="control-label" translate="yes">Connect to</label>
+ <div class="server-box">
+ <input type="text" class="form-control" id="server-field" placeholder=" " />
+ <span class="input-clear" id="server-clear" aria-hidden="true">
+ <svg fill="currentColor" viewBox="0 0 352 512"><path d="m242.72 256 100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.29 12.28-32.19 0-44.48z"/></svg>
+ </span>
+ </div>
+ </div>
+
+ <div class="form-group login-actions">
+ <button class="pf-v5-c-button pf-m-primary login-button" id="login-button" type="submit">
+ <span class="spinner"></span>
+ <span id="login-button-text" class="button-text" translate="yes">Log in</span>
+ </button>
+ <a id="get-out-link" href="/" translate="yes">Cancel</a>
+ </div>
+ </form>
+ </div>
+
+ <div id="login-wait-validating" hidden="true">
+ <span class="help-block" translate="yes">Validating authentication token</span>
+ <span class="spinner col-xs-15"></span>
+ </div>
+
+ <div id="login-fatal" hidden="true">
+ <div id="login-fatal-message"></div>
+ <a id="login-again" class="pf-v5-c-button pf-m-primary" href="#" translate="yes" hidden="true">Try again</a>
+ </div>
+ </div>
+
+ <div class="details" id="login-details" hidden="true">
+ <p>
+ <label class="control-label"><span translate="yes">Server</span>: <b id="server-name"></b></label>
+ </p>
+ <p id="login-note" class="login-note"></p>
+ </div>
+
+ <div class="container" id="recent-hosts" hidden="true">
+ <h1 translate="yes">Recent hosts</h1>
+ <div class="server-box" id="recent-hosts-list">
+ <!-- Here show up recent hosts -->
+ </div>
+ </div>
+
+ <script type="text/javascript">
+ /* Hide everything classed as "noscript" */
+ document.querySelectorAll('.noscript').forEach(function(element){
+ element.hidden = true;
+ });
+ </script>
+</body>
+</html>
diff --git a/pkg/static/login.js b/pkg/static/login.js
new file mode 100644
index 0000000..3748114
--- /dev/null
+++ b/pkg/static/login.js
@@ -0,0 +1,1047 @@
+import "./login.scss";
+
+(function(console) {
+ let localStorage;
+
+ /* Some browsers fail localStorage access due to corruption, preventing Cockpit login */
+ try {
+ localStorage = window.localStorage;
+ window.localStorage.removeItem('url-root');
+ window.localStorage.removeItem('standard-login');
+ } catch (ex) {
+ localStorage = window.sessionStorage;
+ console.warn(String(ex));
+ }
+
+ /* Dark mode */
+ const theme = localStorage.getItem('shell:style') || 'auto';
+ if ((window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && theme === "auto") || theme === "dark") {
+ document.documentElement.classList.add('pf-v5-theme-dark');
+ } else {
+ document.documentElement.classList.remove('pf-v5-theme-dark');
+ }
+
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
+ if ((event.matches && theme === "auto") || theme === "dark") {
+ document.documentElement.classList.add('pf-v5-theme-dark');
+ } else {
+ document.documentElement.classList.remove('pf-v5-theme-dark');
+ }
+ });
+
+ let url_root;
+ const environment = window.environment || { };
+ const oauth = environment.OAuth || null;
+ if (oauth) {
+ if (!oauth.TokenParam)
+ oauth.TokenParam = "access_token";
+ if (!oauth.ErrorParam)
+ oauth.ErrorParam = "error_description";
+ }
+
+ const fmt_re = /\$\{([^}]+)\}|\$([a-zA-Z0-9_]+)/g;
+ function format(fmt /* ... */) {
+ const args = Array.prototype.slice.call(arguments, 1);
+ return fmt.replace(fmt_re, function(m, x, y) { return args[x || y] || "" });
+ }
+
+ function gettext(key) {
+ if (window.cockpit_po) {
+ const translated = window.cockpit_po[key];
+ if (translated && translated[1])
+ return translated[1];
+ }
+ return key;
+ }
+
+ function translate() {
+ if (!document.querySelectorAll)
+ return;
+ const list = document.querySelectorAll("[translate]");
+ for (let i = 0; i < list.length; i++)
+ list[i].textContent = gettext(list[i].textContent);
+ }
+
+ const _ = gettext;
+
+ let login_path, application, org_login_path, org_application;
+ const qs_re = /[?&]?([^=]+)=([^&]*)/g;
+ let oauth_redirect_to = null;
+
+ function QueryParams(qs) {
+ qs = qs.split('+').join(' ');
+
+ const params = {};
+
+ for (;;) {
+ const tokens = qs_re.exec(qs);
+ if (!tokens)
+ break;
+ params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]);
+ }
+ return params;
+ }
+
+ if (!console)
+ console = function() { };
+
+ function id(name) {
+ return document.getElementById(name);
+ }
+
+ // Hide an element (or set of elements) based on a boolean
+ // true: element is hidden, false: element is shown
+ function hideToggle(elements, toggle) {
+ // If it's a single selector, convert it to an array for the loop
+ if (typeof elements === "string")
+ elements = [elements];
+
+ // >= 1 arguments (of type element or string (for CSS selectors))
+ // (passed in "arguments" isn't a a true array, so forEach wouldn't always work)
+ for (let i = 0; i < elements.length; i++) {
+ if (typeof elements[i] === "string") {
+ // Support CSS selectors as a string
+ const els = document.querySelectorAll(elements[i]);
+
+ if (els)
+ els.forEach(function(element) {
+ if (element.hidden !== !!toggle)
+ element.hidden = !!toggle;
+ });
+ } else {
+ // Hide specific elements
+ if (elements[i].hidden !== !!toggle)
+ elements[i].hidden = !!toggle;
+ }
+ }
+ }
+
+ // Show >=1 arguments (element or CSS selector)
+ function show() {
+ hideToggle(arguments, false);
+ }
+
+ // Hide >=1 arguments (element or CSS selector)
+ function hide() {
+ hideToggle(arguments, true);
+ }
+
+ function show_captured_stderr(msg) {
+ if (window.console)
+ console.warn("stderr:", msg);
+
+ hide("#login-wait-validating");
+
+ hide("#login", "#login-details");
+ show("#login-fatal");
+
+ id("login-again").onclick = () => { hide('#login-fatal'); show_login() };
+ show("#login-again");
+
+ const el = id("login-fatal-message");
+ el.textContent = "";
+ el.appendChild(document.createTextNode(msg));
+ }
+
+ function fatal(msg) {
+ if (window.console)
+ console.warn("fatal:", msg);
+
+ hide("#login-again", "#login-wait-validating");
+
+ if (oauth_redirect_to) {
+ id("login-again").href = oauth_redirect_to;
+ show("#login-again");
+ }
+
+ hide("#login", "#login-details");
+ show("#login-fatal");
+
+ const el = id("login-fatal-message");
+ el.textContent = "";
+ el.appendChild(document.createTextNode(msg));
+ }
+
+ function brand(_id, def) {
+ const elt = id(_id);
+ const style = (elt && window.getComputedStyle) ? window.getComputedStyle(elt, ":before") : null;
+
+ if (!style)
+ return;
+
+ let content = style.content;
+ if (content && content != "none" && content != "normal") {
+ const len = content.length;
+ if ((content[0] === '"' || content[0] === '\'') &&
+ len > 2 && content[len - 1] === content[0])
+ content = content.substr(1, len - 2);
+ elt.innerHTML = content || def;
+ } else {
+ elt.removeAttribute("class");
+ }
+ }
+
+ function requisites() {
+ function showBypass(bypass) {
+ if (bypass) {
+ // Selectively show and hide elements
+ show("#login", "#login-details", "#login-override");
+ hide("#get-out-link");
+
+ // Reparent login form to the expander
+ id("login-override-content").appendChild(id("login"));
+
+ // Change the state of the button from primary to warning
+ id("login-button").classList.add("pf-m-warning");
+
+ // Render a "helper text" warning above the log in button
+ document.querySelector("#login .login-actions").insertAdjacentHTML(
+ "beforebegin",
+ "<div class='pf-v5-c-helper-text pf-m-warning' id='bypass-warning'>" +
+ _("Cockpit might not render correctly in your browser") +
+ "</div>"
+ );
+ } else {
+ hide("#login", "#login-details", "#login-override");
+ }
+ }
+
+ function disableLogin(name, bypass) {
+ if (name === "supports")
+ name = "@supports API";
+ const errorString = format(_("This web browser is too old to run the Web Console (missing $0)"), name);
+
+ if (window.console)
+ console.warn(errorString);
+ id("login-error-message").textContent = errorString;
+ show("#unsupported-browser", "#error-group");
+ document.body.classList.add("unsupported-browser");
+
+ showBypass(bypass);
+ }
+
+ function req(name, obj) {
+ let ret;
+ try {
+ ret = (obj && obj[name]);
+ } catch (ex) {
+ fatal(format(_("The web browser configuration prevents Cockpit from running (inaccessible $0)"), name));
+ throw ex;
+ }
+ if (ret === undefined) {
+ disableLogin(name);
+ return false;
+ }
+ return true;
+ }
+
+ function css() {
+ /*
+ * Be certain to use parenthesis when checking CSS strings
+ * as Edge is oddly particular.
+ *
+ * Instead of "display: inline", use:
+ * "(display: inline)"
+ * or
+ * "display", "inline"
+ */
+ const args = [].join.call(arguments, ": ");
+
+ if (!window.CSS || !window.CSS.supports.apply(this, arguments)) {
+ disableLogin(args, "bypass");
+ return false;
+ }
+ return true;
+ }
+
+ const hard_req = req("WebSocket", window) &&
+ req("XMLHttpRequest", window) &&
+ req("sessionStorage", window) &&
+ req("JSON", window) &&
+ req("defineProperty", Object) &&
+ req("console", window) &&
+ req("pushState", window.history) &&
+ req("textContent", document) &&
+ req("replaceAll", String.prototype) &&
+ req("finally", Promise.prototype) &&
+ req("supports", window.CSS);
+
+ if (hard_req) {
+ css("display", "flex") &&
+ css("display", "grid") &&
+ css("selector(test)") &&
+ css("selector(:is(*):where(*))");
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ function trim(s) {
+ return s.replace(/^\s+|\s+$/g, '');
+ }
+
+ /* Sets values for application, url_root and login_path */
+ function setup_path_globals (path) {
+ const parser = document.createElement('a');
+ // send_login_html() sets <base> to UrlRoot
+ const base = document.baseURI;
+
+ path = path || "/";
+ parser.href = base;
+ if (parser.pathname != "/") {
+ url_root = parser.pathname.replace(/^\/+|\/+$/g, '');
+ // deprecated: for connecting to cockpit.js < 272
+ localStorage.setItem('url-root', url_root);
+ if (url_root && path.indexOf('/' + url_root) === 0)
+ path = path.replace('/' + url_root, '') || '/';
+ }
+
+ if (path.indexOf("/=") === 0) {
+ environment.hostname = path.substring(2).split("/")[0];
+ id("server-field").value = environment.hostname;
+ toggle_options(null, true);
+ path = "/cockpit+" + path.split("/")[1];
+ } else if (path.indexOf("/cockpit/") !== 0 && path.indexOf("/cockpit+") !== 0) {
+ path = "/cockpit";
+ }
+
+ application = path.split("/")[1];
+ login_path = "/" + application + "/login";
+ if (url_root)
+ login_path = "/" + url_root + login_path;
+
+ org_application = application;
+ org_login_path = login_path;
+ }
+
+ function toggle_options(ev, show) {
+ // On keypress, only accept spacebar (enter acts as a click)
+ if (ev && ev.type === 'keypress' && ev.key !== ' ')
+ return;
+ // Stop the <a>'s click handler, otherwise it causes a page reload
+ if (ev && ev.type === 'click')
+ ev.preventDefault();
+
+ if (show === undefined)
+ show = id("server-group").hidden;
+
+ hideToggle("#server-group", !show);
+
+ id("option-group").setAttribute("data-state", show);
+ }
+
+ function toggle_password(event) {
+ const input = id("login-password-input");
+
+ input.setAttribute("type", (input.getAttribute("type") === "password") ? "text" : "password");
+ event.stopPropagation();
+ }
+
+ function boot() {
+ window.onload = null;
+
+ translate();
+ if (window.cockpit_po && window.cockpit_po[""]) {
+ document.documentElement.lang = window.cockpit_po[""].language;
+ if (window.cockpit_po[""]["language-direction"])
+ document.documentElement.dir = window.cockpit_po[""]["language-direction"];
+ }
+
+ setup_path_globals(window.location.pathname);
+
+ /* Determine if we are nested or not, and switch styles */
+ if (window.location.pathname.indexOf("/" + url_root + "/cockpit/") === 0 ||
+ window.location.pathname.indexOf("/" + url_root + "/cockpit+") === 0)
+ document.documentElement.setAttribute("class", "inline");
+
+ // Setup title
+ let title = environment.page.title;
+ if (environment.is_cockpit_client)
+ title = _("Login");
+ if (!title || application.indexOf("cockpit+=") === 0)
+ title = environment.hostname;
+ document.title = title;
+
+ if (application.indexOf("cockpit+=") === 0) {
+ hide("#brand", "#badge");
+ } else {
+ brand("badge", "");
+ brand("brand", "Cockpit");
+ }
+
+ if (!requisites())
+ return;
+
+ if (environment.banner) {
+ show("#banner");
+ id("banner-message").textContent = environment.banner.trimEnd();
+ }
+
+ id("bypass-browser-check").addEventListener("click", toggle_options);
+ id("bypass-browser-check").addEventListener("keypress", toggle_options);
+ id("show-other-login-options").addEventListener("click", toggle_options);
+ id("show-other-login-options").addEventListener("keypress", toggle_options);
+ id("server-clear").addEventListener("click", function () {
+ const el = id("server-field");
+ el.value = "";
+ el.focus();
+ });
+
+ const logout_intent = window.sessionStorage.getItem("logout-intent") == "explicit";
+ if (logout_intent)
+ window.sessionStorage.removeItem("logout-intent");
+
+ const logout_reason = window.sessionStorage.getItem("logout-reason");
+ if (logout_reason)
+ window.sessionStorage.removeItem("logout-reason");
+
+ /* Try automatic/kerberos authentication? */
+ if (oauth) {
+ hide("#login-details", "#login");
+ if (logout_intent) {
+ build_oauth_redirect_to();
+ id("login-again").textContent = _("Login again");
+ fatal(_("Logout successful"));
+ } else {
+ oauth_auto_login();
+ }
+ } else if (logout_intent) {
+ show_login(logout_reason);
+ } else if (need_host()) {
+ show_login();
+ } else {
+ standard_auto_login();
+ }
+ }
+
+ function standard_auto_login() {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", login_path, true);
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ run(JSON.parse(xhr.responseText));
+ } else if (xhr.status == 401) {
+ show_login();
+ } else if (xhr.statusText) {
+ fatal(decodeURIComponent(xhr.statusText));
+ } else if (xhr.status === 0) {
+ show_login();
+ } else {
+ fatal(format(_("$0 error"), xhr.status));
+ }
+ }
+ };
+ xhr.send();
+ }
+
+ function build_oauth_redirect_to() {
+ const url_parts = window.location.href.split('#', 2);
+ oauth_redirect_to = oauth.URL;
+ if (oauth.URL.indexOf("?") > -1)
+ oauth_redirect_to += "&";
+ else
+ oauth_redirect_to += "?";
+ oauth_redirect_to += "redirect_uri=" + encodeURIComponent(url_parts[0]);
+ }
+
+ function oauth_auto_login() {
+ const parser = document.createElement('a');
+ if (!oauth.URL)
+ return fatal(_("Cockpit authentication is configured incorrectly."));
+
+ const query = (!window.location.search && window.location.hash)
+ ? QueryParams(window.location.hash.slice(1))
+ : QueryParams(window.location.search);
+
+ /* Not all providers allow hashes in redirect urls */
+
+ build_oauth_redirect_to();
+
+ if (query[oauth.TokenParam]) {
+ if (window.sessionStorage.getItem('login-wanted')) {
+ parser.href = window.sessionStorage.getItem('login-wanted');
+ setup_path_globals(parser.pathname);
+ }
+
+ const token_val = query[oauth.TokenParam];
+ show("#login-wait-validating");
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", login_path, true);
+ xhr.setRequestHeader("Authorization", "Bearer " + token_val);
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ run(JSON.parse(xhr.responseText));
+ } else {
+ const prompt_data = get_prompt_from_challenge(xhr.getResponseHeader("WWW-Authenticate"), xhr.responseText);
+ if (prompt_data)
+ show_converse(prompt_data);
+ else
+ fatal(decodeURIComponent(xhr.statusText));
+ }
+ }
+ };
+ xhr.send();
+ } else if (query[oauth.ErrorParam]) {
+ fatal(query[oauth.ErrorParam]);
+ } else {
+ /* Store url we originally wanted in case we
+ * had to strip a hash or query params
+ */
+ window.sessionStorage.setItem('login-wanted',
+ window.location.href);
+ window.location = oauth_redirect_to;
+ }
+ }
+
+ function clear_errors() {
+ hide("#error-group");
+ id("login-error-message").textContent = "";
+ }
+
+ function clear_info() {
+ hide("#info-group");
+ id("login-info-message").textContent = "";
+ }
+
+ function login_failure(msg, form) {
+ clear_errors();
+ if (msg) {
+ /* OAuth failures are always fatal */
+ if (oauth) {
+ fatal(msg);
+ } else {
+ show_form(form || "login");
+ id("login-error-message").textContent = msg;
+ show("#error-group");
+ }
+ }
+ }
+
+ function login_info(msg) {
+ clear_info();
+ if (msg) {
+ id("login-info-message").textContent = msg;
+ show("#info-group");
+ }
+ }
+
+ function host_failure(msg) {
+ const host = id("server-field").value;
+ if (!host) {
+ login_failure(msg);
+ } else {
+ clear_errors();
+ id("login-error-message").textContent = msg;
+ show("#error-group");
+ toggle_options(null, true);
+ show_form("login");
+ }
+ }
+
+ function login_note(msg) {
+ const el = id("login-note");
+ if (msg) {
+ show(el);
+ el.textContent = msg;
+ } else {
+ el.innerHTML = '&nbsp;';
+ }
+ }
+
+ function need_host() {
+ return environment.page.require_host &&
+ org_application.indexOf("cockpit+=") === -1;
+ }
+
+ function get_recent_hosts() {
+ let hosts = [];
+ try {
+ hosts = JSON.parse(localStorage.getItem("cockpit-client-sessions") || "[]");
+ } catch (e) {
+ console.log("Failed to parse 'cockpit-client-sessions':", e);
+ }
+
+ return hosts;
+ }
+
+ function call_login() {
+ login_failure(null);
+ const user = trim(id("login-user-input").value);
+ if (user === "" && !environment.is_cockpit_client) {
+ login_failure(_("User name cannot be empty"));
+ } else if (need_host() && id("server-field").value === "") {
+ login_failure(_("Please specify the host to connect to"));
+ } else {
+ const machine = id("server-field").value;
+ if (machine) {
+ application = "cockpit+=" + machine;
+ login_path = org_login_path.replace("/" + org_application + "/", "/" + application + "/");
+ id("brand").style.display = "none";
+ id("badge").style.visibility = "hidden";
+ } else {
+ application = org_application;
+ login_path = org_login_path;
+ brand("badge", "");
+ brand("brand", "Cockpit");
+ }
+
+ id("server-name").textContent = machine || environment.hostname;
+ id("login-button").removeEventListener("click", call_login);
+
+ const password = id("login-password-input").value;
+
+ const superuser_key = "superuser:" + user + (machine ? ":" + machine : "");
+ const superuser = localStorage.getItem(superuser_key) || "none";
+ localStorage.setItem("superuser-key", superuser_key);
+ localStorage.setItem(superuser_key, superuser);
+
+ /* Keep information if login page was used */
+ localStorage.setItem('standard-login', true);
+
+ const headers = {
+ Authorization: "Basic " + window.btoa(utf8(user + ":" + password)),
+ "X-Superuser": superuser,
+ };
+ // allow unknown remote hosts with interactive logins with "Connect to:"
+ if (machine)
+ headers["X-SSH-Connect-Unknown-Hosts"] = "yes";
+
+ send_login_request("GET", headers, false);
+ }
+ }
+
+ function render_recent_hosts() {
+ const hosts = get_recent_hosts();
+
+ const list = id("recent-hosts-list");
+ list.innerHTML = "";
+ hosts.forEach(host => {
+ const wrapper = document.createElement("div");
+ wrapper.classList.add("host-line");
+
+ const b1 = document.createElement("button");
+ b1.textContent = host;
+ b1.classList.add("pf-v5-c-button", "pf-m-tertiary", "host-name");
+ b1.addEventListener("click", () => {
+ id("server-field").value = host;
+ call_login();
+ });
+
+ const b2 = document.createElement("button");
+ b2.title = _("Remove host");
+ b2.ariaLabel = b2.title;
+ b2.classList.add("host-remove");
+ b2.addEventListener("click", () => {
+ const i = hosts.indexOf(host);
+ hosts.splice(i, 1);
+ localStorage.setItem('cockpit-client-sessions', JSON.stringify(hosts));
+ render_recent_hosts();
+ });
+
+ wrapper.append(b1, b2);
+ list.append(wrapper);
+ });
+ hideToggle("#recent-hosts", hosts.length == 0);
+ }
+
+ function show_form(form) {
+ const connectable = environment.page.connect;
+ let expanded = id("option-group").getAttribute("data-state");
+
+ hide("#login-wait-validating");
+ show("#login");
+ hideToggle("#login-details", environment.is_cockpit_client);
+ hideToggle("#server-field-label", environment.is_cockpit_client);
+ if (environment.is_cockpit_client) {
+ const brand = id("brand");
+ brand.textContent = _("Connect to:");
+ brand.classList.add("text-brand");
+ }
+
+ hideToggle(["#user-group", "#password-group"], form != "login" || environment.is_cockpit_client);
+ hideToggle("#conversation-group", form != "conversation");
+ hideToggle("#hostkey-group", form != "hostkey");
+
+ id("login-button-text").textContent = (form == "hostkey") ? _("Accept key and log in") : _("Log in");
+ if (form != "login")
+ id("login-password-input").value = '';
+
+ if (environment.page.require_host) {
+ hide("#option-group");
+ expanded = true;
+ } else {
+ hideToggle("#option-group", !connectable || form != "login");
+ }
+
+ if (!connectable || form != "login") {
+ hide("#server-group");
+ } else {
+ hideToggle("#server-group", !expanded);
+ }
+
+ id("login-button").removeAttribute('disabled');
+ id("login-button").removeAttribute('spinning');
+ id("login-button").classList.remove("pf-m-danger");
+ id("login-button").classList.add("pf-m-primary");
+ hide("#get-out-link");
+
+ if (form == "login")
+ id("login-button").addEventListener("click", call_login);
+
+ if (environment.is_cockpit_client) {
+ render_recent_hosts();
+ document.body.classList.add("cockpit-client");
+ }
+ }
+
+ function show_login(message) {
+ /* Show the login screen */
+ login_info(message);
+ id("server-name").textContent = document.title;
+ login_note(_("Log in with your server user account."));
+ id("login-user-input").addEventListener("keydown", function(e) {
+ login_failure(null);
+ clear_info();
+ if (e.which == 13)
+ id("login-password-input").focus();
+ }, false);
+
+ const do_login = function(e) {
+ login_failure(null);
+ if (e.which == 13)
+ call_login();
+ };
+
+ id("login-password-input").addEventListener("keydown", do_login);
+ id("login-password-toggle").addEventListener("click", toggle_password);
+
+ show_form("login");
+
+ if (!environment.is_cockpit_client) {
+ id("login-user-input").focus();
+ } else if (environment.page.require_host) {
+ id("server-field").focus();
+ }
+ }
+
+ function get_known_hosts_db() {
+ try {
+ return JSON.parse(localStorage.getItem("known_hosts") || "{ }");
+ } catch (ex) {
+ console.warn("Can't parse known_hosts database in localStorage", ex);
+ return { };
+ }
+ }
+
+ function set_known_hosts_db(db) {
+ try {
+ localStorage.setItem("known_hosts", JSON.stringify(db));
+ } catch (ex) {
+ console.warn("Can't write known_hosts database to localStorage", ex);
+ }
+ }
+
+ function do_hostkey_verification(data) {
+ const key_db = get_known_hosts_db();
+ const key = data["host-key"];
+ const key_key = key.split(" ")[0];
+ const key_type = key.split(" ")[1];
+
+ if (key_db[key_key] == key) {
+ converse(data.id, data.default);
+ return;
+ }
+
+ if (key_db[key_key]) {
+ id("hostkey-title").textContent = format(_("$0 key changed"), id("server-field").value);
+ show("#hostkey-warning-group");
+ id("hostkey-message-1").textContent = "";
+ } else {
+ id("hostkey-title").textContent = _("New host");
+ hide("#hostkey-warning-group");
+ id("hostkey-message-1").textContent = format(_("You are connecting to $0 for the first time."), id("server-field").value);
+ }
+
+ id("hostkey-verify-help-1").textContent = format(_("To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:"), id("server-field").value);
+ id("hostkey-verify-help-cmds").textContent = format("ssh-keyscan$0 localhost | ssh-keygen -lf -",
+ key_type ? " -t " + key_type : "");
+
+ id("hostkey-fingerprint").textContent = data.default;
+
+ if (key_type) {
+ id("hostkey-type").textContent = format("($0)", key_type);
+ show("#hostkey-type");
+ } else {
+ hide("#hostkey-type");
+ }
+
+ login_failure("");
+
+ function call_converse() {
+ id("login-button").removeEventListener("click", call_converse);
+ login_failure(null, "hostkey");
+ key_db[key_key] = key;
+ set_known_hosts_db(key_db);
+ converse(data.id, data.default);
+ }
+
+ id("login-button").addEventListener("click", call_converse);
+
+ show_form("hostkey");
+ show("#get-out-link");
+
+ if (key_db[key_key]) {
+ id("login-button").classList.add("pf-m-danger");
+ id("login-button").classList.remove("pf-m-primary");
+ }
+ }
+
+ function show_converse(prompt_data) {
+ if (prompt_data["host-key"]) {
+ do_hostkey_verification(prompt_data);
+ return;
+ }
+
+ const type = prompt_data.echo ? "text" : "password";
+ id("conversation-prompt").textContent = prompt_data.prompt;
+
+ const em = id("conversation-message");
+ const msg = prompt_data.error || prompt_data.message;
+ if (msg) {
+ em.textContent = msg;
+ show(em);
+ } else {
+ hide(em);
+ }
+
+ const ei = id("conversation-input");
+ ei.value = "";
+ if (prompt_data.default)
+ ei.value = prompt_data.default;
+ ei.setAttribute('type', type);
+
+ login_failure("");
+
+ function call_converse() {
+ id("conversation-input").removeEventListener("keydown", key_down);
+ id("login-button").removeEventListener("click", call_converse);
+ login_failure(null, "conversation");
+ converse(prompt_data.id, id("conversation-input").value);
+ }
+
+ function key_down(e) {
+ login_failure(null, "conversation");
+ if (e.which == 13) {
+ call_converse();
+ }
+ }
+
+ id("conversation-input").addEventListener("keydown", key_down);
+ id("login-button").addEventListener("click", call_converse);
+ show_form("conversation");
+ ei.focus();
+ }
+
+ function utf8(str) {
+ return window.unescape(encodeURIComponent(str));
+ }
+
+ function get_prompt_from_challenge (header, body) {
+ if (!header)
+ return null;
+
+ const parts = header.split(' ');
+ if (parts[0].toLowerCase() !== 'x-conversation' && parts.length != 3)
+ return null;
+
+ const id = parts[1];
+ let prompt;
+ try {
+ prompt = window.atob(parts[2]);
+ } catch (err) {
+ if (window.console)
+ console.error("Invalid prompt data", err);
+ return null;
+ }
+
+ let resp;
+ try {
+ resp = JSON.parse(body);
+ } catch (err) {
+ if (window.console)
+ console.log("Got invalid JSON response for prompt data", err);
+ resp = {};
+ }
+
+ resp.id = id;
+ resp.prompt = prompt;
+ return resp;
+ }
+
+ function send_login_request(method, headers, is_conversation) {
+ id("login-button").setAttribute('disabled', "true");
+ id("login-button").setAttribute('spinning', "true");
+ const xhr = new XMLHttpRequest();
+ xhr.open(method, login_path, true);
+
+ for (const k in headers)
+ xhr.setRequestHeader(k, headers[k]);
+
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState != 4) {
+ return;
+ }
+ if (xhr.status == 200) {
+ const resp = JSON.parse(xhr.responseText);
+ run(resp);
+ } else if (xhr.status == 401) {
+ const challenge = xhr.getResponseHeader("WWW-Authenticate");
+ if (challenge && challenge.toLowerCase().indexOf("x-conversation") === 0) {
+ const prompt_data = get_prompt_from_challenge(challenge, xhr.responseText);
+ if (prompt_data)
+ show_converse(prompt_data);
+ else
+ fatal(_("Internal error: Invalid challenge header"));
+ } else {
+ if (window.console)
+ console.log(xhr.statusText);
+ if (xhr.statusText.startsWith("captured-stderr:")) {
+ show_captured_stderr(decodeURIComponent(xhr.statusText.replace(/^captured-stderr:/, '')));
+ } else if (xhr.statusText.indexOf("authentication-not-supported") > -1) {
+ const user = trim(id("login-user-input").value);
+ fatal(format(_("The server refused to authenticate '$0' using password authentication, and no other supported authentication methods are available."), user));
+ } else if (xhr.statusText.indexOf("terminated") > -1) {
+ login_failure(_("Authentication failed: Server closed connection"));
+ } else if (xhr.statusText.indexOf("no-host") > -1) {
+ host_failure(_("Unable to connect to that address"));
+ } else if (xhr.statusText.indexOf("unknown-hostkey") > -1) {
+ host_failure(_("Refusing to connect. Hostkey is unknown"));
+ } else if (xhr.statusText.indexOf("unknown-host") > -1) {
+ host_failure(_("Refusing to connect. Host is unknown"));
+ } else if (xhr.statusText.indexOf("invalid-hostkey") > -1) {
+ host_failure(_("Refusing to connect. Hostkey does not match"));
+ } else if (is_conversation) {
+ login_failure(_("Authentication failed"));
+ } else {
+ login_failure(_("Wrong user name or password"));
+ }
+ }
+ } else if (xhr.status == 403) {
+ login_failure(_(decodeURIComponent(xhr.statusText)) || _("Permission denied"));
+ } else if (xhr.statusText) {
+ fatal(decodeURIComponent(xhr.statusText));
+ } else {
+ fatal(format(_("$0 error"), xhr.status));
+ }
+ };
+ xhr.send();
+ }
+
+ function converse(id, msg) {
+ const headers = {
+ Authorization: "X-Conversation " + id + " " + window.btoa(utf8(msg))
+ };
+ send_login_request("GET", headers, true);
+ }
+
+ function login_reload (wanted) {
+ // Force a reload if not triggered below
+ // because only the hash part of the url
+ // changed
+ let timer = window.setTimeout(function() {
+ timer = null;
+ window.location.reload(true);
+ }, 100);
+
+ if (wanted && wanted != window.location.href)
+ window.location = wanted;
+
+ // cancel forced reload if we are reloading
+ window.onbeforeunload = function() {
+ if (timer)
+ window.clearTimeout(timer);
+ timer = null;
+ };
+ }
+
+ function clear_storage (storage, prefix, full) {
+ let i = 0;
+ while (i < storage.length) {
+ const k = storage.key(i);
+ if (full && k.indexOf("cockpit") !== 0)
+ storage.removeItem(k);
+ else if (k.indexOf(prefix) === 0)
+ storage.removeItem(k);
+ else
+ i++;
+ }
+ }
+
+ function setup_localstorage (response) {
+ /* Clear anything not prefixed with
+ * different application from sessionStorage
+ */
+ clear_storage(window.sessionStorage, application, true);
+
+ /* Clear anything prefixed with our application
+ * and login-data, but not other non-application values.
+ */
+ localStorage.removeItem('login-data');
+ clear_storage(localStorage, application, false);
+
+ if (response && response["login-data"]) {
+ const str = JSON.stringify(response["login-data"]);
+ /* login-data is tied to the auth cookie, since
+ * cookies are available after the page
+ * session ends login-data should be too.
+ */
+ localStorage.setItem(application + 'login-data', str);
+ /* Backwards compatibility for packages that aren't application prefixed */
+ localStorage.setItem('login-data', str);
+ }
+
+ /* URL Root is set by cockpit ws and shouldn't be prefixed
+ * by application
+ * deprecated: for connecting to cockpit.js < 272
+ */
+ if (url_root)
+ localStorage.setItem('url-root', url_root);
+
+ const ca_cert_url = environment.CACertUrl;
+ if (ca_cert_url)
+ window.sessionStorage.setItem('CACertUrl', ca_cert_url);
+ }
+
+ function run(response) {
+ let wanted = window.sessionStorage.getItem('login-wanted');
+ const machine = id("server-field").value;
+
+ /* When using cockpit client remember all the addresses being used */
+ if (machine && environment.is_cockpit_client) {
+ const hosts = get_recent_hosts();
+ if (hosts.indexOf(machine) < 0) {
+ hosts.push(machine);
+ localStorage.setItem('cockpit-client-sessions', JSON.stringify(hosts));
+ }
+ }
+
+ if (machine && application != org_application) {
+ wanted = "/=" + machine;
+ if (url_root)
+ wanted = "/" + url_root + wanted;
+ }
+
+ /* clean up sessionStorage. clear anything that isn't prefixed
+ * with an application and anything prefixed with our application.
+ */
+ clear_storage(window.sessionStorage, application, false);
+
+ setup_localstorage(response);
+ login_reload(wanted);
+ }
+
+ window.onload = boot;
+})(window.console);
diff --git a/pkg/static/login.scss b/pkg/static/login.scss
new file mode 100644
index 0000000..33e6645
--- /dev/null
+++ b/pkg/static/login.scss
@@ -0,0 +1,1043 @@
+/* Login page is standalone, all related CSS is here */
+
+@font-face {
+ font-family: RedHatText;
+ font-style: normal;
+ font-weight: 400;
+ src: url(fonts/RedHatText-Regular.woff2) format("woff2");
+}
+
+@font-face {
+ font-family: RedHatText;
+ font-style: normal;
+ font-weight: 700;
+ src: url(fonts/RedHatText-Medium.woff2) format("woff2");
+}
+
+*,
+::after,
+::before {
+ box-sizing: border-box;
+}
+
+[hidden]:not([hidden="false"]) {
+ display: none !important;
+}
+
+html {
+ font-family: sans-serif;
+ block-size: 100%;
+ background: #000;
+}
+
+body {
+ margin: 0;
+ font-family: RedHatText, Helvetica, Arial, sans-serif;
+ background-color: #333;
+ color: #fff;
+ line-height: 1.5;
+}
+
+h1, h2, h3, h4 {
+ font-size: 1.125rem;
+ font-weight: 400;
+ margin: 0;
+}
+
+h1 {
+ font-size: 1.75rem;
+ line-height: 1.3;
+ padding-block: 0 1rem;
+ padding-inline: 0;
+}
+
+h2 {
+ font-size: 1.3rem;
+}
+
+pre {
+ white-space: pre-wrap;
+}
+
+.pf-v5-c-button,
+label {
+ font-weight: 600;
+}
+
+.pf-v5-c-button,
+img {
+ vertical-align: middle;
+}
+
+#option-group,
+.pf-v5-c-button,
+.input-clear,
+button {
+ cursor: pointer;
+}
+
+#option-group svg,
+.input-clear {
+ opacity: 0.7;
+}
+
+a,
+summary {
+ color: #06c;
+ text-decoration: none;
+}
+
+.pf-v5-c-button:focus,
+.host-remove:focus,
+button:focus,
+a:focus {
+ outline: -webkit-focus-ring-color auto 5px;
+ outline: dotted thin;
+ outline-offset: -2px;
+}
+
+a:focus,
+a:hover {
+ color: #fff;
+ text-decoration: underline;
+}
+
+img {
+ border: 0;
+}
+
+button,
+input,
+select,
+textarea {
+ font-family: inherit;
+ margin: 0;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+button {
+ overflow: visible;
+ border-radius: 0.1875rem;
+ border: none;
+}
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+p {
+ margin-block: 0 1rem;
+ margin-inline: 0;
+}
+
+.container {
+ margin-inline: auto;
+}
+
+/* Bump options & connection away from user/pass */
+#option-group,
+#option-group[hidden] + #server-group {
+ margin-block-start: 1rem;
+}
+
+#option-group {
+ margin-block-end: 1rem;
+}
+
+.form-group:not(:first-child) {
+ margin-block-start: 1rem;
+}
+
+.login-actions {
+ display: flex;
+ align-items: baseline;
+ grid-gap: 1rem;
+}
+
+label {
+ display: inline-block;
+}
+
+#option-group,
+.control-label {
+ font-size: 0.875rem;
+}
+
+[role="form"] .control-label,
+form .control-label {
+ display: flex;
+ align-items: center;
+ min-block-size: 2rem;
+ padding-block: 0 0.5rem;
+ padding-inline: 0;
+}
+
+.form-control {
+ color: #151515;
+}
+
+.pf-v5-c-button.pf-m-control,
+.form-control[type="password"],
+.form-control[type="text"] {
+ display: block;
+ inline-size: 100%;
+ padding-block: 0.25rem;
+ padding-inline: 0.5rem;
+ background-color: #fff;
+ background-image: none;
+ border: 1px solid #ededed;
+ border-block-end-color: #72767b;
+ border-radius: 1px;
+ transition: border-color 0.15s ease-in-out;
+ color: #151515;
+}
+
+.pf-v5-c-button.pf-m-control:hover,
+.form-control[type="password"]:hover,
+.form-control[type="text"]:hover,
+.form-control[type="password"]:focus,
+.form-control[type="text"]:focus {
+ border-block-end-color: #06c;
+}
+
+.pf-v5-c-button.pf-m-control:focus,
+.form-control[type="password"]:focus,
+.form-control[type="text"]:focus {
+ padding-block-end: calc(0.25rem - 1px);
+ border-block-end-width: 2px;
+}
+
+.pf-v5-c-button.pf-m-control:focus,
+.form-control:focus {
+ outline: 0;
+}
+
+.form-control::placeholder {
+ color: #999;
+ font-style: italic;
+ opacity: 1;
+}
+
+.checkbox-row {
+ margin-block: 1rem 0;
+ margin-inline: 0;
+ display: flex;
+ align-items: baseline;
+}
+
+.checkbox-row > input[type="checkbox"] {
+ inline-size: 1rem;
+ block-size: 1rem;
+ align-self: flex-start;
+ margin-block-start: calc((1.5rem - 1rem) / 2);
+ margin-inline-end: 0.5rem;
+}
+
+label.checkbox {
+ font: inherit;
+}
+
+.help-block {
+ display: block;
+ margin-block: 5px 1rem;
+ color: #737373;
+}
+
+.pf-v5-c-button,
+.caret {
+ display: inline-block;
+}
+
+.pf-v5-c-button[spinning] .spinner {
+ display: inline-flex;
+}
+
+.form-group::after {
+ clear: both;
+ margin-block-end: 1rem;
+}
+
+.pf-v5-c-button {
+ text-align: center;
+ background-image: none;
+ border: 1px solid transparent;
+ white-space: nowrap;
+ padding-block: 0.375rem;
+ padding-inline: 1rem;
+ border-radius: 3px;
+ font-size: inherit;
+ font-weight: 400;
+ user-select: none;
+}
+
+.pf-v5-c-button:focus,
+.pf-v5-c-button:hover {
+ text-decoration: none;
+}
+
+.pf-v5-c-button:active {
+ outline: 0;
+ background-image: none;
+}
+
+.pf-m-primary {
+ background-color: #06c;
+ border-color: #06c;
+ color: #fff;
+}
+
+.pf-m-primary:active,
+.pf-m-primary:not([disabled]):focus,
+.pf-m-primary:not([disabled]):hover {
+ background-color: #004080;
+ border-color: #004080;
+ color: #fff;
+ outline: none;
+}
+
+.pf-m-tertiary {
+ background-color: transparent;
+ border-color: #151515;
+ color: #151515;
+}
+
+.pf-m-tertiary:active,
+.pf-m-tertiary:focus,
+.pf-m-tertiary:hover {
+ background-color: transparent;
+ border-color: #151515;
+ color: #151515;
+ border-width: 2px;
+}
+
+.pf-m-danger {
+ background-color: #c9190b;
+ border-color: #c9190b;
+ color: #fff;
+}
+
+.pf-m-danger:hover,
+.pf-m-danger:focus {
+ background-color: #a30000;
+ border-color: #a30000;
+ outline: none;
+}
+
+.pf-v5-c-button.pf-m-warning {
+ background-color: #f0ab00;
+ border-color: #f0ab00;
+ color: black;
+}
+
+.pf-v5-c-button.pf-m-warning:hover,
+.pf-v5-c-button.pf-m-warning:focus {
+ background-color: #c58c00;
+ border-color: #c58c00;
+ color: black;
+}
+
+.login-pf {
+ block-size: 100%;
+}
+
+.login-pf #brand img {
+ display: block;
+ margin-block: 0;
+ margin-inline: auto;
+ max-inline-size: 100%;
+}
+
+.unsupported-browser #brand {
+ display: none;
+}
+
+.login-pf #banner {
+ margin-block: 1rem 0.5rem;
+ margin-inline: 0;
+ grid-area: banner;
+ inline-size: 100%;
+}
+
+#banner-message {
+ white-space: pre-wrap;
+ max-block-size: 12em;
+ overflow: auto;
+}
+
+.login-pf .container {
+ background-color: rgb(255 255 255 / 50%);
+ background: white;
+ color: #333;
+ padding-block: 3rem 2rem;
+ padding-inline: 3rem;
+ inline-size: 100%;
+}
+
+#main {
+ grid-area: login;
+}
+
+.login-pf .container .details p:first-child {
+ border-block-start: 1px solid #474747;
+ padding-block-start: 1.5rem;
+ margin-block-start: 1.5rem;
+}
+
+.login-pf .details p {
+ margin: 0;
+}
+
+.login-pf .details p + p {
+ margin-block-start: 0.5rem;
+}
+
+.login-pf .container .control-label {
+ font-weight: 600;
+}
+
+.login-pf .container .help-block {
+ color: #fff;
+}
+
+.login-pf .container .form-group:last-child,
+.login-pf .container .form-group:last-child .help-block:last-child {
+ margin-block-end: 0;
+}
+
+.spinner {
+ animation: 0.6s linear infinite rotation;
+ border: 4px solid rgb(0 0 0 / 25%);
+ border-block-start-color: rgb(0 0 0 / 75%);
+ border-radius: 100%;
+ block-size: 1.5rem;
+ inline-size: 1.5rem;
+}
+
+/* Alerts */
+.pf-v5-c-alert {
+ color: #151515;
+ position: relative;
+ grid-template-columns: max-content 1fr max-content;
+ grid-template-rows: 1fr auto;
+ grid-template-areas:
+ "icon title action"
+ ". content content";
+ background-color: #fff;
+ margin-block: 0 1.5rem;
+ margin-inline: 0;
+ display: grid;
+ border: 3px solid #009596;
+ border-width: 2px 0 0;
+ box-shadow: rgb(3 3 3 / 16%) 0 0.5rem 1rem 0, rgb(3 3 3 / 8%) 0 0 0.5rem 0;
+}
+
+.pf-v5-c-alert.pf-m-inline {
+ box-shadow: none;
+}
+
+.pf-v5-c-alert > svg {
+ grid-area: icon;
+ block-size: 1.125rem;
+ inline-size: 1.125rem;
+ margin-block: 1.25rem 1rem;
+ margin-inline: 1rem;
+ float: inline-start;
+ color: #009596;
+}
+
+@supports (display: grid) {
+ .pf-v5-c-alert > svg {
+ float: none;
+ margin-inline-end: 0;
+ }
+}
+
+.pf-v5-c-alert__title {
+ grid-area: title;
+ font-size: 1rem;
+ margin: 1rem;
+}
+
+.pf-v5-c-alert.pf-m-inline.pf-m-danger {
+ background: #faeae8;
+ border-color: #c9190b;
+}
+
+.pf-v5-c-alert.pf-m-danger > svg {
+ color: #c9190b;
+}
+
+.pf-v5-c-alert.pf-m-danger .pf-v5-c-alert__title {
+ color: #a30000;
+}
+
+.pf-v5-c-alert.pf-m-inline.pf-m-warning {
+ background: #fdf7e7;
+ border-color: #f0ab00;
+}
+
+.pf-v5-c-alert.pf-m-warning > svg {
+ color: #f0ab00;
+}
+
+.pf-v5-c-alert.pf-m-warning .pf-v5-c-alert__title {
+ color: #795600;
+}
+
+.pf-v5-c-alert.pf-m-inline.pf-m-info {
+ background: #e7f1fa;
+ border-color: #73bcf7;
+}
+
+.pf-v5-c-alert.pf-m-info > svg {
+ color: #73bcf7;
+}
+
+.pf-v5-c-alert.pf-m-info .pf-v5-c-alert__title {
+ color: #004368;
+}
+
+#server-group::before {
+ clear: both;
+ margin-block-start: 5px;
+}
+
+.login-fatal {
+ font-size: 130%;
+}
+
+.unsupported-browser ul {
+ display: inline-block;
+ margin-block: 0;
+ margin-inline: auto;
+ text-align: start;
+}
+
+.unsupported-browser a {
+ font-weight: 700;
+}
+
+.input-clear,
+.inline .container .help-block {
+ color: #000;
+}
+
+.caret,
+.server-box {
+ position: relative;
+}
+
+.conversation-prompt {
+ white-space: normal;
+ word-wrap: break-word;
+}
+
+.control-label {
+ white-space: nowrap;
+}
+
+.spinner {
+ border-color: rgb(255 255 255 / 75%) rgb(255 255 255 / 25%) rgb(255 255 255 / 25%);
+}
+
+#hostkey-fingerprint {
+ font-size: large;
+ font-weight: bold;
+ margin-block-end: 0;
+}
+
+#hostkey-type {
+ font-size: small;
+}
+
+#hostkey-verify-help-cmds {
+ border-radius: 0.25rem;
+ padding-block: 0.5rem;
+ padding-inline: 1rem;
+ background: rgb(150 150 150 / 20%);
+}
+
+.pf-v5-c-button .spinner,
+.pf-v5-c-button[spinning] .button-text,
+.hide-before::before {
+ display: none;
+}
+
+/* Hide inline login screen by default */
+.inline #badge,
+.inline #brand,
+.inline #login-details {
+ display: none;
+}
+
+.inline body {
+ background: 0 0 !important;
+ color: #000;
+}
+
+.caret {
+ vertical-align: middle;
+ margin-block: calc((1.5rem - 16px) / 2);
+ margin-inline: 0;
+ margin-inline-end: 0.25rem;
+ transition: all 300ms;
+}
+
+[data-state="true"] svg.caret {
+ transform: rotate(90deg) translateX(-3px);
+ transform-origin: 0.5rem;
+}
+
+.input-clear {
+ display: flex;
+ padding-block: 0.5rem;
+ padding-inline: 0.75rem;
+ position: absolute;
+ inset-block: 0;
+ inset-inline-end: 0;
+}
+
+.input-clear > svg {
+ block-size: 1rem;
+ inline-size: auto;
+}
+
+/* Only show clear icon when the field has text */
+.form-control:placeholder-shown + .input-clear {
+ display: none;
+}
+
+/* Reserve a little space for the clear icon in the input */
+.server-box > .form-control {
+ padding-inline-end: 2rem;
+}
+
+#option-group:hover svg,
+.input-clear:hover {
+ opacity: 1;
+}
+
+#option-group div {
+ margin-inline-start: -3px;
+ margin-block: 3px 1rem;
+}
+
+.login-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ block-size: 2.5rem;
+ flex-basis: 100%;
+}
+
+#hostkey-group:not([hidden]) ~ .login-actions > .login-button {
+ flex-basis: max-content;
+}
+
+.pf-v5-c-button[disabled] {
+ background-color: #333;
+ background-image: none;
+ border-color: #555;
+ cursor: default;
+}
+
+/*** Media breakpoints ***/
+
+/* Optimize for mobile phones in portrait mode */
+@media (max-width: 480px) {
+ .login-pf {
+ display: flex;
+ flex-direction: column-reverse;
+ position: relative;
+ }
+
+ .row {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .login-pf .container {
+ inline-size: 100%;
+ padding: 2rem;
+ }
+
+ .login-pf #badge {
+ max-inline-size: calc(100vw - 2rem);
+ }
+
+ /* Brand container */
+ .login-pf #brand {
+ font-size: inherit;
+ background-position: 50% 50%;
+ }
+
+ .details {
+ text-align: center;
+ }
+}
+
+/* Large phones and desktops */
+@media (min-width: 481px) {
+ .unsupported-browser-heading {
+ margin-block: 2rem 1rem;
+ margin-inline: 0;
+ }
+
+ .login-pf #brand {
+ padding: 0;
+ margin-block-end: 1rem;
+ }
+}
+
+/* Desktop only */
+@media (min-width: 1024px) {
+ .control-label {
+ text-align: end;
+ }
+
+ .login-pf #brand img {
+ margin: 0;
+ text-align: start;
+ }
+
+ .login-pf .container .login-area {
+ border-inline-end: 1px solid #474747;
+ }
+}
+
+/* PF4-ish style overrides (depending on grid & CSS variables */
+
+.row {
+ display: flex;
+ flex-flow: column;
+ margin: 0;
+}
+
+body.login-pf {
+ color: #151515;
+ display: grid;
+ grid-template-areas: "banner" "login" "details" "recent" "logo";
+ grid-template-rows: repeat(3, auto) 1fr;
+ background-position: 0 0 !important;
+ padding: 0;
+}
+
+.login-pf .container .details p:first-child {
+ border-block-start: 0;
+ padding-block-start: 0;
+ margin-block-start: 0;
+}
+
+.login-button {
+ inline-size: 100%;
+}
+
+.login-pf .details {
+ grid-area: details;
+ padding-block: 1.5rem;
+ padding-inline: 3rem;
+ inline-size: 100%;
+ background: #ededed;
+}
+
+#recent-hosts {
+ grid-area: recent;
+ margin-block-start: 1rem;
+ padding-block: 3rem 1.5rem;
+ padding-inline: 1.5rem;
+}
+
+#recent-hosts > h1 {
+ padding-inline-start: 1.5rem;
+}
+
+.login-pf #badge {
+ grid-area: logo;
+}
+
+.login-pf .container .login-area {
+ border: none;
+ inline-size: auto;
+}
+
+details > summary {
+ display: block;
+ padding-block: 0.5rem;
+ padding-inline: 0;
+ cursor: pointer;
+}
+
+details > summary:hover {
+ text-decoration: underline;
+}
+
+details[open] > summary > .caret {
+ transform: rotate(90deg) translateX(-3px);
+}
+
+.login-pf #brand {
+ text-transform: initial;
+}
+
+#brand strong,
+#brand b {
+ font-weight: 600;
+}
+
+.login-pf #brand.text-brand {
+ inline-size: auto;
+ background: none;
+}
+
+a:focus,
+a:hover {
+ color: #004080;
+}
+
+.caret polygon {
+ fill: #06c;
+}
+
+.host-line {
+ display: grid;
+ grid: 1fr / 1fr auto;
+ border: 1px solid #d2d2d2;
+ border-width: 1px 0;
+}
+
+.host-line + .host-line {
+ border-block-start-width: 0;
+}
+
+.host-name {
+ flex: auto;
+ text-align: start;
+ color: #06c;
+ border: none;
+ /* As we're making the list flush to the card, we should indent the contents to align */
+ padding-block: 0.5rem;
+ padding-inline: 1.5rem 1rem;
+}
+
+.host-name:hover {
+ color: #004080;
+ text-decoration: underline;
+}
+
+.host-remove {
+ background: transparent;
+ margin-inline-end: 1.5rem;
+ padding-block: 0.375rem;
+ padding-inline: 1rem;
+ /* Allow ::before to size relative to .host-remove */
+ position: relative;
+}
+
+/* As masking would hide outline, use ::before for the × SVG */
+.host-remove::before {
+ /* ::before pseudoelement needs special CSS to be visible */
+ content: "";
+ display: block;
+ position: absolute;
+ inset: 0;
+ background-color: #151515;
+ border: none;
+ mask: center / contain no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 352 512'%3E%3Cpath d='m242.7 256 100-100a31.5 31.5 0 0 0 0-44.5l-22.2-22.3a31.5 31.5 0 0 0-44.4 0L176 189.2 76 89.3a31.5 31.5 0 0 0-44.5 0L9.2 111.5a31.5 31.5 0 0 0 0 44.4l100 100.1-100 100a31.5 31.5 0 0 0 0 44.5l22.3 22.3a31.5 31.5 0 0 0 44.4 0l100.1-100 100 100a31.5 31.5 0 0 0 44.5 0l22.3-22.2a31.5 31.5 0 0 0 0-44.5L242.8 256z'/%3E%3C/svg%3E");
+ mask-size: 1rem 1rem;
+ opacity: 0.6;
+}
+
+.host-remove:focus::before,
+.host-remove:hover::before {
+ opacity: 1;
+}
+
+/* Mobile-specific */
+@media (max-width: 1023px) {
+ body.login-pf {
+ justify-items: center;
+ }
+
+ #badge {
+ background-position: center;
+ margin: 2rem;
+ }
+}
+
+/* Desktop-specific */
+@media (min-width: 1024px) {
+ body.login-pf {
+ grid-template-areas:
+ ". banner banner ."
+ ". . . ."
+ ". login logo ."
+ ". details logo ."
+ ". recent logo ."
+ ". . . .";
+ grid-template-rows: auto 1fr minmax(min-content, max-content) repeat(2, auto) 1fr;
+ grid-template-columns: minmax(0, 1fr) repeat(2, minmax(auto, 34rem)) minmax(0, 1fr);
+ grid-gap: 0 4rem;
+ }
+}
+
+.unsupported-browser ul {
+ color: #555;
+ margin-block: 0 1rem;
+ margin-inline: 0;
+}
+
+.browser-recommendations {
+ margin-block: 1rem 0;
+ margin-inline: 0;
+}
+
+.browser-recommendations h3 {
+ font: inherit;
+ margin-block-start: 0;
+}
+
+.dialog-error {
+ margin-block: 0 1rem;
+ margin-inline: 0;
+}
+
+#login-again {
+ display: block;
+ margin-block-start: 1rem;
+}
+
+.password-with-toggle {
+ display: grid;
+ grid-template-columns: 1fr auto;
+}
+
+.pf-v5-c-button.login-password-toggle {
+ display: flex;
+ align-items: center;
+ border-inline-start-width: 0;
+}
+
+.login-password-toggle > svg {
+ block-size: 1rem;
+ inline-size: auto;
+}
+
+/* Show the appropriate eye icon based on the password text status */
+input[type="text"] + .login-password-toggle .password-show,
+input[type="password"] + .login-password-toggle .password-hide {
+ display: none;
+}
+
+.pf-v5-c-helper-text {
+ margin-block-start: 1rem;
+}
+
+.pf-v5-c-helper-text.pf-m-warning {
+ color: #795600;
+}
+
+/* Dark */
+.pf-v5-theme-dark {
+ .login-pf .container,
+ .login-pf .details {
+ z-index: 2;
+ }
+
+ .login-pf .container {
+ background: #26292d;
+ color: #f0f0f0;
+ }
+
+ .login-pf .details {
+ background: #151515;
+ color: #f0f0f0;
+ }
+
+ .pf-v5-c-button.pf-m-control,
+ .form-control[type="password"],
+ .form-control[type="text"] {
+ background: #393f44;
+ border-color: #393f44;
+ border-block-end-color: #6c6f72;
+ color: #f0f0f0;
+ }
+
+ a:active,
+ a:focus,
+ a:hover,
+ a,
+ summary {
+ color: #8ac0f6;
+ }
+
+ .input-clear,
+ .inline .container .help-block {
+ color: #f0f0f0;
+ }
+
+ .host-line {
+ border-color: #34373b;
+ }
+
+ /* The × icon's focus ring */
+ .host-remove:focus {
+ outline-color: #f1f1f1;
+ }
+
+ /* The × icon */
+ .host-remove::before {
+ background-color: #f1f1f1;
+ }
+
+ .unsupported-browser ul {
+ color: #999;
+ }
+
+ .pf-v5-c-alert.pf-m-inline.pf-m-danger {
+ background: #1e2125;
+ border-color: #fe5142;
+ }
+
+ .pf-v5-c-alert.pf-m-danger > svg,
+ .pf-v5-c-alert.pf-m-danger .pf-v5-c-alert__title {
+ color: #fe5142;
+ }
+
+ .pf-v5-c-button[disabled] {
+ background-color: #444548;
+ border-color: #444548;
+ color: #c6c7c8;
+ }
+
+ /* Screen the background and logo darker */
+ .login-pf::after {
+ /* Pass through mouse events */
+ pointer-events: none;
+ background: #000;
+ opacity: 0.66;
+ display: block;
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ }
+
+ .pf-v5-c-helper-text.pf-m-warning {
+ color: #f0ab00;
+ }
+}
+
+/* Animation */
+
+@keyframes rotation {
+ from {
+ transform: rotate(0);
+ }
+
+ to {
+ transform: rotate(359deg);
+ }
+}
diff --git a/pkg/static/manifest.json b/pkg/static/manifest.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/pkg/static/manifest.json
@@ -0,0 +1 @@
+{}
diff --git a/pkg/storaged/anaconda.jsx b/pkg/storaged/anaconda.jsx
new file mode 100644
index 0000000..ffc54b1
--- /dev/null
+++ b/pkg/storaged/anaconda.jsx
@@ -0,0 +1,166 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import client from "./client.js";
+
+import { decode_filename } from "./utils.js";
+import { parse_subvol_from_options } from "./btrfs/utils.jsx";
+
+function parse_parent_from_options(options) {
+ const parent_match = options.match(/x-parent=(?<parent>[\w\\-]+)/);
+ if (parent_match) {
+ return parent_match.groups.parent;
+ } else
+ return null;
+}
+
+function uuid_equal(a, b) {
+ return a.replace("-", "").toUpperCase() == b.replace("-", "").toUpperCase();
+}
+
+function device_name(block) {
+ // Prefer symlinks in /dev/stratis/.
+ return (block.Symlinks.map(decode_filename).find(n => n.indexOf("/dev/stratis/") == 0) ||
+ decode_filename(block.PreferredDevice));
+}
+
+export function remember_passphrase(block, passphrase) {
+ if (!client.in_anaconda_mode())
+ return;
+
+ if (!window.isSecureContext)
+ return;
+
+ try {
+ const passphrases = JSON.parse(window.sessionStorage.getItem("cockpit_passphrases")) || { };
+ passphrases[device_name(block)] = passphrase;
+ window.sessionStorage.setItem("cockpit_passphrases", JSON.stringify(passphrases));
+ } catch {
+ console.warn("Can't record passphrases");
+ }
+}
+
+export function export_mount_point_mapping() {
+ if (!client.in_anaconda_mode())
+ return;
+
+ function tab_info(config, for_parent) {
+ let dir;
+ let subvols;
+
+ for (const c of config) {
+ if (c[0] == "fstab") {
+ const o = decode_filename(c[1].opts.v);
+ if (for_parent && !uuid_equal(parse_parent_from_options(o), for_parent))
+ continue;
+
+ const d = client.strip_mount_point_prefix(decode_filename(c[1].dir.v));
+ if (d) {
+ const sv = parse_subvol_from_options(o);
+ if (sv) {
+ if (sv.pathname) {
+ if (!subvols)
+ subvols = { };
+ subvols[sv.pathname] = { dir: d };
+ }
+ } else if (!dir) {
+ dir = d;
+ }
+ }
+ }
+ }
+
+ if (dir || subvols)
+ return {
+ type: "filesystem",
+ dir,
+ subvolumes: subvols
+ };
+
+ for (const c of config) {
+ if (c[0] == "crypttab") {
+ const o = decode_filename(c[1].options.v);
+ if (for_parent && !uuid_equal(parse_parent_from_options(o), for_parent))
+ continue;
+
+ const device = decode_filename(c[1].device.v);
+ let content_info;
+ if (device.startsWith("UUID=")) {
+ content_info = tab_info(config, device.substr(5));
+ }
+
+ return {
+ type: "crypto",
+ content: content_info,
+ };
+ }
+ }
+ }
+
+ function block_info(block) {
+ if (block.IdUsage == "filesystem") {
+ return tab_info(block.Configuration);
+ } else if (block.IdUsage == "other" && block.IdType == "swap") {
+ return {
+ type: "swap",
+ };
+ } else if (block.IdUsage == "crypto") {
+ const cleartext_block = client.blocks_cleartext[block.path];
+
+ let content_info;
+ if (cleartext_block)
+ content_info = block_info(cleartext_block);
+ else {
+ const block_crypto = client.blocks_crypto[block.path];
+ if (block_crypto)
+ content_info = tab_info(block_crypto.ChildConfiguration, block.IdUUID);
+ }
+
+ if (content_info) {
+ return {
+ type: "crypto",
+ cleartext_device: cleartext_block && device_name(cleartext_block),
+ content: content_info,
+ };
+ }
+ }
+ }
+
+ const mpm = { };
+ for (const p in client.blocks) {
+ const b = client.blocks[p];
+ mpm[device_name(b)] = block_info(b);
+ }
+
+ // Add inactive logical volumes
+ for (const vg_p in client.vgroups) {
+ const vg = client.vgroups[vg_p];
+ for (const lv of client.vgroups_lvols[vg_p] || []) {
+ const b = client.lvols_block[lv.path];
+ const dev_name = "/dev/" + vg.Name + "/" + lv.Name;
+ if (!b && !mpm[dev_name]) {
+ const info = tab_info(lv.ChildConfiguration, lv.UUID);
+ if (info)
+ mpm[dev_name] = info;
+ }
+ }
+ }
+
+ window.sessionStorage.setItem("cockpit_mount_points", JSON.stringify(mpm));
+}
diff --git a/pkg/storaged/block/create-pages.jsx b/pkg/storaged/block/create-pages.jsx
new file mode 100644
index 0000000..2463b2d
--- /dev/null
+++ b/pkg/storaged/block/create-pages.jsx
@@ -0,0 +1,129 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import client from "../client";
+
+import { get_fstab_config } from "../filesystem/utils.jsx";
+
+import { make_partition_table_page } from "../partitions/partition-table.jsx";
+import { make_legacy_vdo_page } from "../legacy-vdo/legacy-vdo.jsx";
+
+import { make_unrecognized_data_card } from "./unrecognized-data.jsx";
+import { make_unformatted_data_card } from "./unformatted-data.jsx";
+import { make_locked_encrypted_data_card } from "../crypto/locked-encrypted-data.jsx";
+import { make_filesystem_card } from "../filesystem/filesystem.jsx";
+import { make_lvm2_physical_volume_card } from "../lvm2/physical-volume.jsx";
+import { make_mdraid_disk_card } from "../mdraid/mdraid-disk.jsx";
+import { make_stratis_blockdev_card } from "../stratis/blockdev.jsx";
+import { make_swap_card } from "../swap/swap.jsx";
+import { make_encryption_card } from "../crypto/encryption.jsx";
+import { make_btrfs_device_card } from "../btrfs/device.jsx";
+import { make_btrfs_filesystem_card } from "../btrfs/filesystem.jsx";
+import { make_btrfs_subvolume_pages } from "../btrfs/volume.jsx";
+
+import { new_page } from "../pages.jsx";
+
+/* CARD must have page_name, page_location, and page_size set.
+ */
+
+export function make_block_page(parent, block, card) {
+ let is_crypto = block.IdUsage == 'crypto';
+ let content_block = is_crypto ? client.blocks_cleartext[block.path] : block;
+ const fstab_config = get_fstab_config(content_block || block, true);
+
+ const block_stratis_blockdev = client.blocks_stratis_blockdev[block.path];
+ const block_stratis_stopped_pool = client.blocks_stratis_stopped_pool[block.path];
+ const legacy_vdo = client.legacy_vdo_overlay.find_by_backing_block(block);
+
+ const is_stratis = ((content_block && content_block.IdUsage == "raid" && content_block.IdType == "stratis") ||
+ (block_stratis_blockdev && client.stratis_pools[block_stratis_blockdev.Pool]) ||
+ block_stratis_stopped_pool);
+
+ const is_btrfs = (fstab_config.length > 0 &&
+ (fstab_config[2].indexOf("subvol=") >= 0 || fstab_config[2].indexOf("subvolid=") >= 0));
+
+ const block_btrfs_blockdev = content_block && client.blocks_fsys_btrfs[content_block.path];
+ const single_device_volume = block_btrfs_blockdev && block_btrfs_blockdev.data.num_devices === 1;
+
+ if (client.blocks_ptable[block.path]) {
+ make_partition_table_page(parent, block, card);
+ return;
+ }
+
+ if (legacy_vdo) {
+ make_legacy_vdo_page(parent, legacy_vdo, block, card);
+ return;
+ }
+
+ // Adjust for encryption leaking out of Stratis
+ if (is_crypto && is_stratis) {
+ is_crypto = false;
+ content_block = block;
+ }
+
+ if (is_crypto)
+ card = make_encryption_card(card, block);
+
+ if (!content_block) {
+ if (!is_crypto) {
+ // can not happen unless there is a bug in the code above.
+ console.error("Assertion failure: is_crypto == false");
+ }
+ if (fstab_config.length > 0 && !is_btrfs) {
+ card = make_filesystem_card(card, block, null, fstab_config);
+ } else {
+ card = make_locked_encrypted_data_card(card, block);
+ }
+ } else {
+ const is_filesystem = content_block.IdUsage == 'filesystem';
+ const block_pvol = client.blocks_pvol[content_block.path];
+ const block_swap = client.blocks_swap[content_block.path];
+
+ if (block_btrfs_blockdev) {
+ if (single_device_volume)
+ card = make_btrfs_filesystem_card(card, block, content_block);
+ else
+ card = make_btrfs_device_card(card, block, content_block, block_btrfs_blockdev);
+ } else if (is_filesystem) {
+ card = make_filesystem_card(card, block, content_block, fstab_config);
+ } else if ((content_block.IdUsage == "raid" && content_block.IdType == "LVM2_member") ||
+ (block_pvol && client.vgroups[block_pvol.VolumeGroup])) {
+ card = make_lvm2_physical_volume_card(card, block, content_block);
+ } else if (is_stratis) {
+ card = make_stratis_blockdev_card(card, block, content_block);
+ } else if ((content_block.IdUsage == "raid") ||
+ (client.mdraids[content_block.MDRaidMember])) {
+ card = make_mdraid_disk_card(card, block, content_block);
+ } else if (block_swap ||
+ (content_block.IdUsage == "other" && content_block.IdType == "swap")) {
+ card = make_swap_card(card, block, content_block);
+ } else if (client.blocks_available[content_block.path]) {
+ card = make_unformatted_data_card(card, block, content_block);
+ } else {
+ card = make_unrecognized_data_card(card, block, content_block);
+ }
+ }
+
+ if (card) {
+ const page = new_page(parent, card);
+ if (block_btrfs_blockdev && single_device_volume)
+ make_btrfs_subvolume_pages(page, block_btrfs_blockdev);
+ return page;
+ }
+}
diff --git a/pkg/storaged/block/format-dialog.jsx b/pkg/storaged/block/format-dialog.jsx
new file mode 100644
index 0000000..67761cf
--- /dev/null
+++ b/pkg/storaged/block/format-dialog.jsx
@@ -0,0 +1,669 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import client from "../client.js";
+
+import {
+ edit_crypto_config, parse_options, unparse_options, extract_option,
+ get_parent_blocks, is_netdev,
+ decode_filename, encode_filename, block_name,
+ get_active_usage, reload_systemd, teardown_active_usage,
+ validate_fsys_label,
+} from "../utils.js";
+
+import React from "react";
+import { FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { HelperText, HelperTextItem, } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { ExclamationTriangleIcon, InfoCircleIcon } from "@patternfly/react-icons";
+
+import {
+ dialog_open,
+ TextInput, PassInput, CheckBoxes, SelectOne, SizeSlider,
+ BlockingMessage, TeardownMessage,
+ init_active_usage_processes
+} from "../dialog.jsx";
+
+import { get_fstab_config, is_valid_mount_point } from "../filesystem/utils.jsx";
+import { init_existing_passphrase, unlock_with_type } from "../crypto/keyslots.jsx";
+import { job_progress_wrapper } from "../jobs-panel.jsx";
+import { at_boot_input, mount_options } from "../filesystem/mounting-dialog.jsx";
+import { remember_passphrase } from "../anaconda.jsx";
+
+const _ = cockpit.gettext;
+
+export function initial_tab_options(client, block, for_fstab) {
+ const options = { };
+
+ // "nofail" is the default for new filesystems with Cockpit so
+ // that a failure to mount one of them will not prevent
+ // Cockpit from starting. This allows people to debug and fix
+ // these failures with Cockpit itself.
+ //
+ // In Anaconda mode however, we don't make "nofail" the
+ // default since people will be creating the core filesystems
+ // like "/", "/var", etc.
+
+ if (!client.in_anaconda_mode())
+ options.nofail = true;
+
+ get_parent_blocks(client, block.path).forEach(p => {
+ if (is_netdev(client, p)) {
+ options._netdev = true;
+ }
+ // HACK - https://bugzilla.redhat.com/show_bug.cgi?id=1589541
+ if (client.legacy_vdo_overlay.find_by_block(client.blocks[p])) {
+ options._netdev = true;
+ options["x-systemd.device-timeout=0"] = true;
+ if (for_fstab)
+ options["x-systemd.requires=vdo.service"] = true;
+ }
+ });
+
+ return Object.keys(options).join(",");
+}
+
+export function initial_crypto_options(client, block) {
+ return initial_tab_options(client, block, false);
+}
+
+export function initial_mount_options(client, block) {
+ return initial_tab_options(client, block, true);
+}
+
+export const mount_explanation = {
+ local:
+ <FormHelperText>
+ <HelperText>
+ <HelperTextItem hasIcon>
+ {_("Mounts before services start")}
+ </HelperTextItem>
+ <HelperTextItem hasIcon>
+ {_("Appropriate for critical mounts, such as /var")}
+ </HelperTextItem>
+ <HelperTextItem hasIcon icon={<ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />}>
+ {_("Boot fails if filesystem does not mount, preventing remote access")}
+ </HelperTextItem>
+ </HelperText>
+ </FormHelperText>,
+ nofail:
+ <FormHelperText>
+ <HelperText>
+ <HelperTextItem hasIcon>
+ {_("Mounts in parallel with services")}
+ </HelperTextItem>
+ <HelperTextItem hasIcon icon={<InfoCircleIcon className="ct-icon-info-circle" />}>
+ {_("Boot still succeeds when filesystem does not mount")}
+ </HelperTextItem>
+ </HelperText>
+ </FormHelperText>,
+ netdev:
+ <FormHelperText>
+ <HelperText>
+ <HelperTextItem hasIcon>
+ {_("Mounts in parallel with services, but after network is available")}
+ </HelperTextItem>
+ <HelperTextItem hasIcon icon={<InfoCircleIcon className="ct-icon-info-circle" />}>
+ {_("Boot still succeeds when filesystem does not mount")}
+ </HelperTextItem>
+ </HelperText>
+ </FormHelperText>,
+ never:
+ <FormHelperText>
+ <HelperText>
+ <HelperTextItem hasIcon>
+ {_("Does not mount during boot")}
+ </HelperTextItem>
+ <HelperTextItem hasIcon>
+ {_("Useful for mounts that are optional or need interaction (such as passphrases)")}
+ </HelperTextItem>
+ </HelperText>
+ </FormHelperText>,
+};
+
+export function format_dialog(client, path, start, size, enable_dos_extended) {
+ const block = client.blocks[path];
+ if (block.IdUsage == "crypto") {
+ cockpit.spawn(["cryptsetup", "luksDump", decode_filename(block.Device)], { superuser: true })
+ .then(output => {
+ if (output.indexOf("Keyslots:") >= 0) // This is what luksmeta-monitor-hack looks for
+ return 2;
+ else
+ return 1;
+ })
+ .catch(() => {
+ return false;
+ })
+ .then(version => {
+ return format_dialog_internal(client, path, start, size, enable_dos_extended, version);
+ });
+ } else {
+ return format_dialog_internal(client, path, start, size, enable_dos_extended);
+ }
+}
+
+function find_root_fsys_block() {
+ const root = client.anaconda?.mount_point_prefix || "/";
+ for (const p in client.blocks) {
+ if (client.blocks_fsys[p] && client.blocks_fsys[p].MountPoints.map(decode_filename).indexOf(root) >= 0)
+ return client.blocks[p];
+ if (client.blocks[p].Configuration.find(c => c[0] == "fstab" && decode_filename(c[1].dir.v) == root))
+ return client.blocks[p];
+ }
+ return null;
+}
+
+function format_dialog_internal(client, path, start, size, enable_dos_extended, old_luks_version) {
+ const block = client.blocks[path];
+ const block_part = client.blocks_part[path];
+ const block_ptable = client.blocks_ptable[path] || client.blocks_ptable[block_part?.Table];
+
+ const offer_keep_keys = block.IdUsage == "crypto";
+ const unlock_before_format = offer_keep_keys && !client.blocks_cleartext[path];
+
+ const create_partition = (start !== undefined);
+
+ let title;
+ if (create_partition)
+ title = cockpit.format(_("Create partition on $0"), block_name(block));
+ else
+ title = cockpit.format(_("Format $0"), block_name(block));
+
+ function is_filesystem(vals) {
+ return vals.type != "empty" && vals.type != "dos-extended" && vals.type != "biosboot" && vals.type != "swap";
+ }
+
+ function add_fsys(storaged_name, entry) {
+ if (storaged_name === true ||
+ (client.fsys_info && client.fsys_info[storaged_name] && client.fsys_info[storaged_name].can_format)) {
+ filesystem_options.push(entry);
+ }
+ }
+
+ const filesystem_options = [];
+ add_fsys("xfs", { value: "xfs", title: "XFS" });
+ add_fsys("ext4", { value: "ext4", title: "EXT4" });
+ add_fsys("btrfs", { value: "btrfs", title: "BTRFS" });
+ add_fsys("vfat", { value: "vfat", title: "VFAT" });
+ add_fsys("ntfs", { value: "ntfs", title: "NTFS" });
+ add_fsys("swap", { value: "swap", title: "Swap" });
+ if (client.in_anaconda_mode()) {
+ if (block_ptable && block_ptable.Type == "gpt" && !client.anaconda.efi)
+ add_fsys(true, { value: "biosboot", title: "BIOS boot partition" });
+ if (block_ptable && client.anaconda.efi)
+ add_fsys(true, { value: "efi", title: "EFI system partition" });
+ }
+ add_fsys(true, { value: "empty", title: _("No filesystem") });
+ if (create_partition && enable_dos_extended)
+ add_fsys(true, { value: "dos-extended", title: _("Extended partition") });
+
+ function is_supported(type) {
+ return filesystem_options.find(o => o.value == type);
+ }
+
+ let default_type = null;
+ if (block.IdUsage == "filesystem" && is_supported(block.IdType))
+ default_type = block.IdType;
+ else {
+ const root_block = find_root_fsys_block();
+ if (root_block && is_supported(root_block.IdType)) {
+ default_type = root_block.IdType;
+ } else if (client.anaconda?.default_fsys_type && is_supported(client.anaconda.default_fsys_type)) {
+ default_type = client.anaconda.default_fsys_type;
+ } else {
+ default_type = "ext4";
+ }
+ }
+
+ function is_encrypted(vals) {
+ return vals.crypto && vals.crypto !== "none";
+ }
+
+ function add_crypto_type(value, title, recommended) {
+ if ((client.manager.SupportedEncryptionTypes && client.manager.SupportedEncryptionTypes.indexOf(value) != -1) ||
+ value == "luks1") {
+ crypto_types.push({
+ value,
+ title: title + (recommended ? " " + _("(recommended)") : "")
+ });
+ }
+ }
+
+ const crypto_types = [{ value: "none", title: _("No encryption") }];
+ if (offer_keep_keys) {
+ if (old_luks_version)
+ crypto_types.push({
+ value: " keep",
+ title: cockpit.format(_("Reuse existing encryption ($0)"), "LUKS" + old_luks_version)
+ });
+ else
+ crypto_types.push({ value: " keep", title: _("Reuse existing encryption") });
+ }
+ add_crypto_type("luks1", "LUKS1", false);
+ add_crypto_type("luks2", "LUKS2", true);
+
+ const usage = get_active_usage(client, create_partition ? null : path, _("format"), _("delete"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), block_name(block)),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ const crypto_config = block.Configuration.find(c => c[0] == "crypttab");
+ let crypto_options;
+ if (crypto_config) {
+ crypto_options = (decode_filename(crypto_config[1].options.v)
+ .split(",")
+ .filter(function (s) { return s.indexOf("x-parent") !== 0 })
+ .join(","));
+ } else {
+ crypto_options = initial_crypto_options(client, block);
+ }
+
+ const crypto_split_options = parse_options(crypto_options);
+ extract_option(crypto_split_options, "noauto");
+ extract_option(crypto_split_options, "nofail");
+ extract_option(crypto_split_options, "_netdev");
+ const crypto_extra_options = unparse_options(crypto_split_options);
+
+ let [, old_dir, old_opts] = get_fstab_config(block, true);
+ if (old_opts == undefined)
+ old_opts = initial_mount_options(client, block);
+
+ old_dir = client.strip_mount_point_prefix(old_dir);
+ if (old_dir === false)
+ return Promise.reject(_("This device can not be used for the installation target."));
+
+ // Strip out btrfs subvolume mount options
+ const split_options = parse_options(old_opts).filter(opt => !(opt.startsWith('subvol=') || opt.startsWith('subvolid=')));
+ extract_option(split_options, "noauto");
+ const opt_ro = extract_option(split_options, "ro");
+ const opt_never_auto = extract_option(split_options, "x-cockpit-never-auto");
+ const opt_nofail = extract_option(split_options, "nofail");
+ const opt_netdev = extract_option(split_options, "_netdev");
+ const extra_options = unparse_options(split_options);
+
+ let existing_passphrase_type = null;
+
+ let at_boot;
+ if (opt_never_auto)
+ at_boot = "never";
+ else if (opt_netdev)
+ at_boot = "netdev";
+ else if (opt_nofail)
+ at_boot = "nofail";
+ else
+ at_boot = "local";
+
+ let action_variants = [
+ { tag: null, Title: create_partition ? _("Create and mount") : _("Format and mount") },
+ { tag: "nomount", Title: create_partition ? _("Create only") : _("Format only") }
+ ];
+
+ const action_variants_for_empty = [
+ { tag: "nomount", Title: create_partition ? _("Create") : _("Format") }
+ ];
+
+ let action_variants_for_swap = [
+ { tag: null, Title: create_partition ? _("Create and start") : _("Format and start") },
+ { tag: "nomount", Title: create_partition ? _("Create only") : _("Format only") }
+ ];
+
+ if (client.in_anaconda_mode()) {
+ action_variants = action_variants_for_swap = [
+ { tag: "nomount", Title: create_partition ? _("Create") : _("Format") }
+ ];
+ }
+
+ const dlg = dialog_open({
+ Title: title,
+ Teardown: TeardownMessage(usage),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ validate: (name, vals) => validate_fsys_label(name, vals.type),
+ visible: is_filesystem
+ }),
+ TextInput("mount_point", _("Mount point"),
+ {
+ visible: is_filesystem,
+ value: old_dir || "",
+ validate: (val, values, variant) => {
+ return is_valid_mount_point(client,
+ block,
+ client.add_mount_point_prefix(val),
+ variant == "nomount");
+ }
+ }),
+ SelectOne("type", _("Type"),
+ {
+ value: default_type,
+ choices: filesystem_options
+ }),
+ SizeSlider("size", _("Size"),
+ {
+ value: size,
+ max: size,
+ round: 1024 * 1024,
+ visible: function () {
+ return create_partition;
+ }
+ }),
+ CheckBoxes("erase", _("Overwrite"),
+ {
+ fields: [
+ { tag: "on", title: _("Overwrite existing data with zeros (slower)") }
+ ],
+ }),
+ SelectOne("crypto", _("Encryption"),
+ {
+ choices: crypto_types,
+ value: offer_keep_keys ? " keep" : "none",
+ visible: vals => vals.type != "dos-extended" && vals.type != "biosboot" && vals.type != "efi",
+ nested_fields: [
+ PassInput("passphrase", _("Passphrase"),
+ {
+ validate: function (phrase, vals) {
+ if (vals.crypto != " keep" && phrase === "")
+ return _("Passphrase cannot be empty");
+ },
+ visible: vals => is_encrypted(vals) && vals.crypto != " keep",
+ new_password: true
+ }),
+ PassInput("passphrase2", _("Confirm"),
+ {
+ validate: function (phrase2, vals) {
+ if (vals.crypto != " keep" && phrase2 != vals.passphrase)
+ return _("Passphrases do not match");
+ },
+ visible: vals => is_encrypted(vals) && vals.crypto != " keep",
+ new_password: true
+ }),
+ CheckBoxes("store_passphrase", "",
+ {
+ visible: vals => is_encrypted(vals) && vals.crypto != " keep",
+ value: {
+ on: false,
+ },
+ fields: [
+ { title: _("Store passphrase"), tag: "on" }
+ ]
+ }),
+ PassInput("old_passphrase", _("Passphrase"),
+ {
+ validate: function (phrase) {
+ if (phrase === "")
+ return _("Passphrase cannot be empty");
+ },
+ visible: vals => vals.crypto == " keep" && vals.needs_explicit_passphrase,
+ explanation: _("The disk needs to be unlocked before formatting. Please provide a existing passphrase.")
+ }),
+ TextInput("crypto_options", _("Encryption options"),
+ {
+ visible: is_encrypted,
+ value: crypto_extra_options
+ })
+ ]
+ }),
+ at_boot_input(at_boot, is_filesystem),
+ mount_options(opt_ro, extra_options, is_filesystem),
+ ],
+ update: function (dlg, vals, trigger) {
+ if (trigger == "at_boot")
+ dlg.set_options("at_boot", { explanation: mount_explanation[vals.at_boot] });
+ else if (trigger == "type") {
+ if (dlg.get_value("type") == "empty") {
+ dlg.update_actions({ Variants: action_variants_for_empty });
+ } else if (dlg.get_value("type") == "swap") {
+ dlg.update_actions({ Variants: action_variants_for_swap });
+ } else {
+ dlg.update_actions({ Variants: action_variants });
+ }
+ if (vals.type == "efi" && !vals.mount_point)
+ dlg.set_values({ mount_point: "/boot/efi" });
+ }
+ },
+ Action: {
+ Variants: action_variants,
+ Danger: (create_partition ? null : _("Formatting erases all data on a storage device.")),
+ wrapper: job_progress_wrapper(client, block.path, client.blocks_cleartext[block.path]?.path),
+ disable_on_error: usage.Teardown,
+ action: function (vals) {
+ const mount_now = vals.variant != "nomount";
+ let type = vals.type;
+ let partition_type = "";
+
+ if (type == "efi") {
+ type = "vfat";
+ partition_type = block_ptable.Type == "dos" ? "0xEF" : "c12a7328-f81f-11d2-ba4b-00a0c93ec93b";
+ }
+
+ if (type == "biosboot") {
+ type = "empty";
+ partition_type = "21686148-6449-6e6f-744e-656564454649";
+ }
+
+ if (type == "swap") {
+ partition_type = (block_ptable && block_ptable.Type == "dos"
+ ? "0x82"
+ : "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f");
+ }
+
+ const options = {
+ 'tear-down': { t: 'b', v: true }
+ };
+ if (vals.erase.on)
+ options.erase = { t: 's', v: "zero" };
+ if (vals.name)
+ options.label = { t: 's', v: vals.name };
+
+ // HACK - https://bugzilla.redhat.com/show_bug.cgi?id=1516041
+ if (client.legacy_vdo_overlay.find_by_block(block)) {
+ options['no-discard'] = { t: 'b', v: true };
+ }
+
+ const keep_keys = is_encrypted(vals) && offer_keep_keys && vals.crypto == " keep";
+
+ const config_items = [];
+ let new_crypto_options;
+ if (is_encrypted(vals)) {
+ let opts = [];
+ if (is_filesystem(vals)) {
+ if (!mount_now || vals.at_boot == "never")
+ opts.push("noauto");
+ if (vals.at_boot == "nofail")
+ opts.push("nofail");
+ if (vals.at_boot == "netdev")
+ opts.push("_netdev");
+ }
+
+ opts = opts.concat(parse_options(vals.crypto_options));
+ new_crypto_options = { t: 'ay', v: encode_filename(unparse_options(opts)) };
+ const item = {
+ options: new_crypto_options,
+ "track-parents": { t: 'b', v: true }
+ };
+
+ if (!keep_keys) {
+ if (vals.store_passphrase.on) {
+ item["passphrase-contents"] = { t: 'ay', v: encode_filename(vals.passphrase) };
+ } else {
+ item["passphrase-contents"] = { t: 'ay', v: encode_filename("") };
+ }
+ config_items.push(["crypttab", item]);
+ options["encrypt.passphrase"] = { t: 's', v: vals.passphrase };
+ options["encrypt.type"] = { t: 's', v: vals.crypto };
+ }
+ }
+
+ let mount_point;
+
+ if (is_filesystem(vals)) {
+ const mount_options = [];
+ if (!mount_now || vals.at_boot == "never") {
+ mount_options.push("noauto");
+ }
+ if (vals.mount_options?.ro)
+ mount_options.push("ro");
+ if (vals.at_boot == "never")
+ mount_options.push("x-cockpit-never-auto");
+ if (vals.at_boot == "nofail")
+ mount_options.push("nofail");
+ if (vals.at_boot == "netdev")
+ mount_options.push("_netdev");
+ if (vals.mount_options?.extra)
+ mount_options.push(vals.mount_options.extra);
+ if (type == "btrfs")
+ mount_options.push("subvol=/");
+
+ mount_point = vals.mount_point;
+ if (mount_point != "") {
+ if (mount_point[0] != "/")
+ mount_point = "/" + mount_point;
+ mount_point = client.add_mount_point_prefix(mount_point);
+
+ config_items.push(["fstab", {
+ dir: { t: 'ay', v: encode_filename(mount_point) },
+ type: { t: 'ay', v: encode_filename("auto") },
+ opts: { t: 'ay', v: encode_filename(mount_options.join(",") || "defaults") },
+ freq: { t: 'i', v: 0 },
+ passno: { t: 'i', v: 0 },
+ "track-parents": { t: 'b', v: true }
+ }]);
+ }
+ }
+
+ if (type == "swap") {
+ config_items.push(["fstab", {
+ dir: { t: 'ay', v: encode_filename("none") },
+ type: { t: 'ay', v: encode_filename("swap") },
+ opts: { t: 'ay', v: encode_filename(mount_now ? "defaults" : "noauto") },
+ freq: { t: 'i', v: 0 },
+ passno: { t: 'i', v: 0 },
+ "track-parents": { t: 'b', v: true }
+ }]);
+ }
+
+ if (config_items.length > 0)
+ options["config-items"] = { t: 'a(sa{sv})', v: config_items };
+
+ function maybe_unlock() {
+ const content_block = client.blocks_cleartext[path];
+ if (content_block)
+ return content_block;
+
+ return (unlock_with_type(client, block, vals.old_passphrase, existing_passphrase_type)
+ .catch(error => {
+ dlg.set_values({ needs_explicit_passphrase: true });
+ return Promise.reject(error);
+ })
+ .then(() => client.blocks_cleartext[path]));
+ }
+
+ function format() {
+ if (create_partition) {
+ if (type == "dos-extended")
+ return block_ptable.CreatePartition(start, vals.size, "0x05", "", { });
+ else
+ return block_ptable.CreatePartitionAndFormat(start, vals.size, partition_type, "", { },
+ type, options);
+ } else if (keep_keys) {
+ return (edit_crypto_config(block,
+ (config, commit) => {
+ config.options = new_crypto_options;
+ return commit();
+ })
+ .then(() => maybe_unlock())
+ .then(content_block => {
+ return content_block.Format(type, options);
+ }));
+ } else {
+ return block.Format(type, options)
+ .then(() => {
+ if (partition_type != "" && block_part)
+ return block_part.SetType(partition_type, {});
+ });
+ }
+ }
+
+ function block_fsys_for_block(path) {
+ if (keep_keys) {
+ const content_block = client.blocks_cleartext[path];
+ return client.blocks_fsys[content_block.path];
+ } else if (is_encrypted(vals))
+ return (client.blocks_cleartext[path] &&
+ client.blocks_fsys[client.blocks_cleartext[path].path]);
+ else
+ return client.blocks_fsys[path];
+ }
+
+ function block_swap_for_block(path) {
+ if (keep_keys) {
+ const content_block = client.blocks_cleartext[path];
+ return client.blocks_swap[content_block.path];
+ } else if (is_encrypted(vals))
+ return (client.blocks_cleartext[path] &&
+ client.blocks_swap[client.blocks_cleartext[path].path]);
+ else
+ return client.blocks_swap[path];
+ }
+
+ function block_crypto_for_block(path) {
+ return client.blocks_crypto[path];
+ }
+
+ async function maybe_mount(new_path) {
+ const path = new_path || block.path;
+ const new_block = await client.wait_for(() => client.blocks[path]);
+
+ if (is_encrypted(vals))
+ remember_passphrase(new_block, vals.passphrase);
+
+ if (is_filesystem(vals) && mount_now) {
+ const block_fsys = await client.wait_for(() => block_fsys_for_block(path));
+ await client.mount_at(client.blocks[block_fsys.path], mount_point);
+ }
+ if (type == "swap" && mount_now) {
+ const block_swap = await client.wait_for(() => block_swap_for_block(path));
+ await block_swap.Start({});
+ }
+ if (is_encrypted(vals) && !mount_now) {
+ const block_crypto = await client.wait_for(() => block_crypto_for_block(path));
+ await block_crypto.Lock({ });
+ }
+ }
+
+ return teardown_active_usage(client, usage)
+ .then(reload_systemd)
+ .then(format)
+ .then(new_path => reload_systemd().then(() => new_path))
+ .then(maybe_mount);
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage),
+ unlock_before_format
+ ? init_existing_passphrase(block, true, type => { existing_passphrase_type = type })
+ : null
+ ]
+ });
+}
diff --git a/pkg/storaged/block/other.jsx b/pkg/storaged/block/other.jsx
new file mode 100644
index 0000000..a564a3f
--- /dev/null
+++ b/pkg/storaged/block/other.jsx
@@ -0,0 +1,67 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client.js";
+
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+
+import { StorageCard, StorageDescription, new_card } from "../pages.jsx";
+import { block_name, should_ignore } from "../utils.js";
+import { partitionable_block_actions } from "../partitions/actions.jsx";
+import { OtherIcon } from "../icons/gnome-icons.jsx";
+
+import { make_block_page } from "../block/create-pages.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_other_page(parent, block) {
+ if (should_ignore(client, block.path))
+ return;
+
+ const other_card = new_card({
+ title: _("Block device"),
+ next: null,
+ page_block: block,
+ page_icon: OtherIcon,
+ for_summary: true,
+ job_path: block.path,
+ component: OtherCard,
+ props: { block },
+ actions: partitionable_block_actions(block),
+ });
+
+ make_block_page(parent, block, other_card);
+}
+
+const OtherCard = ({ card, block }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Device number")}
+ value={(block.DeviceNumber >> 8) + ":" + (block.DeviceNumber & 0xFF)} />
+ <StorageDescription title={_("Device file")} value={block_name(block)} />
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/block/resize.jsx b/pkg/storaged/block/resize.jsx
new file mode 100644
index 0000000..77cf023
--- /dev/null
+++ b/pkg/storaged/block/resize.jsx
@@ -0,0 +1,656 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import cockpit from "cockpit";
+import client from "../client.js";
+
+import {
+ block_name, get_active_usage, teardown_active_usage,
+ undo_temporary_teardown, is_mounted_synch, get_partitions
+} from "../utils.js";
+import {
+ existing_passphrase_fields, init_existing_passphrase,
+ request_passphrase_on_error_handler
+} from "../crypto/keyslots.jsx";
+import {
+ dialog_open, SizeSlider, BlockingMessage, TeardownMessage, SelectSpaces,
+ init_active_usage_processes
+} from "../dialog.jsx";
+import { std_reply } from "../stratis/utils.jsx";
+import { pvs_to_spaces } from "../lvm2/utils.jsx";
+
+const _ = cockpit.gettext;
+
+export function check_unused_space(path) {
+ const block = client.blocks[path];
+ const lvm2 = client.blocks_lvm2[path];
+ const lvol = lvm2 && client.lvols[lvm2.LogicalVolume];
+ const part = client.blocks_part[path];
+
+ let size, min_change;
+
+ if (lvol) {
+ size = lvol.Size;
+ min_change = client.vgroups[lvol.VolumeGroup].ExtentSize;
+ } else if (part) {
+ size = part.Size;
+ min_change = 1024 * 1024;
+ } else {
+ return null;
+ }
+
+ if (size != block.Size) {
+ // Let's ignore inconsistent lvol,part/block combinations.
+ // These happen during a resize and the inconsistency will
+ // eventually go away.
+ return null;
+ }
+
+ let content_path = null;
+ let crypto_overhead = 0;
+
+ const crypto = client.blocks_crypto[block.path];
+ const cleartext = client.blocks_cleartext[block.path];
+ if (crypto) {
+ if (crypto.MetadataSize !== undefined && cleartext) {
+ content_path = cleartext.path;
+ crypto_overhead = crypto.MetadataSize;
+ }
+ } else {
+ content_path = path;
+ }
+
+ const fsys = client.blocks_fsys[content_path];
+ const content_block = client.blocks[content_path];
+ const vdo = content_block ? client.legacy_vdo_overlay.find_by_backing_block(content_block) : null;
+ const stratis_bdev = client.blocks_stratis_blockdev[content_path];
+
+ if (fsys && fsys.Size && (size - fsys.Size - crypto_overhead) > min_change && fsys.Resize) {
+ return {
+ volume_size: size - crypto_overhead,
+ content_size: fsys.Size
+ };
+ }
+
+ if (vdo && (size - vdo.physical_size - crypto_overhead) > min_change) {
+ return {
+ volume_size: size - crypto_overhead,
+ content_size: vdo.physical_size
+ };
+ }
+
+ if (stratis_bdev && (size - Number(stratis_bdev.TotalPhysicalSize) - crypto_overhead) > min_change) {
+ return {
+ volume_size: size - crypto_overhead,
+ content_size: Number(stratis_bdev.TotalPhysicalSize)
+ };
+ }
+
+ return null;
+}
+
+function lvol_or_part_and_fsys_resize(client, lvol_or_part, size, offline, passphrase, pvs) {
+ let fsys;
+ let crypto_overhead;
+ let vdo;
+ let stratis_bdev;
+ let orig_size;
+ let block;
+
+ if (lvol_or_part.iface == "org.freedesktop.UDisks2.LogicalVolume") {
+ orig_size = lvol_or_part.Size;
+ block = client.lvols_block[lvol_or_part.path];
+ if (!block)
+ return lvol_or_part.Resize(size, { });
+ } else {
+ orig_size = lvol_or_part.Size;
+ block = client.blocks[lvol_or_part.path];
+ }
+
+ const crypto = client.blocks_crypto[block.path];
+ if (crypto) {
+ const cleartext = client.blocks_cleartext[block.path];
+ if (!cleartext)
+ return;
+ fsys = client.blocks_fsys[cleartext.path];
+ vdo = client.legacy_vdo_overlay.find_by_backing_block(cleartext);
+ stratis_bdev = client.blocks_stratis_blockdev[cleartext.path];
+ crypto_overhead = crypto.MetadataSize;
+ } else {
+ fsys = client.blocks_fsys[block.path];
+ vdo = client.legacy_vdo_overlay.find_by_backing_block(block);
+ stratis_bdev = client.blocks_stratis_blockdev[block.path];
+ crypto_overhead = 0;
+ }
+
+ function fsys_resize() {
+ if (fsys) {
+ // When growing a filesystem, always grow it to fill its
+ // block device. This is always the right thing to do in
+ // Cockpit.
+ //
+ const resize_size = (size > orig_size) ? 0 : size - crypto_overhead;
+
+ // HACK - https://bugzilla.redhat.com/show_bug.cgi?id=1934567
+ //
+ // block_fsys.MountedAt might be out of synch with reality
+ // here if resizing the crypto card accidentally
+ // triggered an unmount. Thus, we check synchronously
+ // whether or not we should be doing a offline resize or
+ // not.
+ //
+ // Another option for us would be to just mount the
+ // filesystem back if that's what we expect, to undo the
+ // bug mentioned above. But let's be a bit more passive
+ // here and hope the bug gets fixed eventually.
+ return (is_mounted_synch(client.blocks[fsys.path])
+ .then(is_mounted => {
+ // When doing an offline resize, we need to first repair the filesystem.
+ if (!is_mounted) {
+ return (fsys.Repair({ })
+ .then(() => fsys.Resize(resize_size, { })));
+ } else {
+ return fsys.Resize(resize_size, { });
+ }
+ }));
+ } else if (vdo) {
+ if (size - crypto_overhead > vdo.physical_size)
+ return vdo.grow_physical();
+ else if (size - crypto_overhead < vdo.physical_size)
+ return Promise.reject(_("VDO backing devices can not be made smaller"));
+ else
+ return Promise.resolve();
+ } else if (stratis_bdev) {
+ const delta = size - crypto_overhead - Number(stratis_bdev.TotalPhysicalSize);
+ if (delta > 0) {
+ const pool = client.stratis_pools[stratis_bdev.Pool];
+ return pool.GrowPhysicalDevice(stratis_bdev.Uuid).then(std_reply);
+ } else if (delta < 0)
+ // This shouldn't happen. But if it does, continuing is harmful, so we throw an error.
+ return Promise.reject(_("Stratis blockdevs can not be made smaller")); // not-covered: safety check
+ else
+ return Promise.resolve();
+ } else if (client.blocks_available[block.path]) {
+ // Growing or shrinking unformatted data, nothing to do
+ return Promise.resolve();
+ } else if (size < orig_size) {
+ // This shouldn't happen. But if it does, continuing is harmful, so we throw an error.
+ return Promise.reject(_("Unrecognized data can not be made smaller here.")); // not-covered: safety check
+ } else {
+ // Growing unrecognized content, nothing to do.
+ return Promise.resolve();
+ }
+ }
+
+ function crypto_resize() {
+ // When growing a LUKS device, always grow it to fill its
+ // block device. This is always the right thing to do in
+ // Cockpit.
+ //
+ const resize_size = (size > orig_size) ? 0 : size - crypto_overhead;
+
+ if (crypto) {
+ const opts = { };
+ if (passphrase)
+ opts.passphrase = { t: "s", v: passphrase };
+ return crypto.Resize(resize_size, opts);
+ } else {
+ return Promise.resolve();
+ }
+ }
+
+ function lvol_or_part_resize() {
+ if (size != orig_size) {
+ // Both LogicalVolume and Partition have a Resize method
+ // with the same signature, so this will work on both.
+ return lvol_or_part.Resize(size, { pvs: pvs ? { t: 'ao', v: pvs } : undefined });
+ } else
+ return Promise.resolve();
+ }
+
+ if (size < orig_size) {
+ return fsys_resize().then(crypto_resize)
+ .then(lvol_or_part_resize);
+ } else if (size >= orig_size) {
+ return lvol_or_part_resize().then(crypto_resize)
+ .then(fsys_resize);
+ }
+}
+
+export function get_resize_info(client, block, to_fit) {
+ let info, shrink_excuse, grow_excuse;
+
+ if (block) {
+ if (block.IdUsage == 'crypto' && client.blocks_crypto[block.path]) {
+ const cleartext = client.blocks_cleartext[block.path];
+
+ if (!cleartext) {
+ info = { };
+ shrink_excuse = grow_excuse = _("Unlock before resizing");
+ } else {
+ return get_resize_info(client, cleartext, to_fit);
+ }
+ } else if (block.IdUsage == 'filesystem') {
+ info = client.fsys_info && client.fsys_info[block.IdType];
+
+ if (!info) {
+ info = { };
+ shrink_excuse = grow_excuse = cockpit.format(_("$0 can not be resized here"),
+ block.IdType);
+ } else {
+ if (!info.can_shrink && !info.can_grow) {
+ shrink_excuse = grow_excuse = cockpit.format(_("$0 can not be resized"),
+ block.IdType);
+ } else {
+ if (!info.can_shrink)
+ shrink_excuse = cockpit.format(_("$0 can not be made smaller"),
+ block.IdType);
+ if (!info.can_grow)
+ grow_excuse = cockpit.format(_("$0 can not be made larger"),
+ block.IdType);
+ }
+ }
+ } else if (client.blocks_stratis_blockdev[block.path] && client.features.stratis_grow_blockdevs) {
+ info = {
+ can_shrink: false,
+ can_grow: true,
+ grow_needs_unmount: false
+ };
+ shrink_excuse = _("Stratis blockdevs can not be made smaller");
+ } else if (block.IdUsage == 'raid') {
+ info = { };
+ shrink_excuse = grow_excuse = _("Physical volumes can not be resized here");
+ } else if (client.legacy_vdo_overlay.find_by_backing_block(block)) {
+ info = {
+ can_shrink: false,
+ can_grow: true,
+ grow_needs_unmount: false
+ };
+ shrink_excuse = _("VDO backing devices can not be made smaller");
+ } else if (client.blocks_swap[block.path]) {
+ info = {
+ can_shrink: false,
+ can_grow: false,
+ };
+ shrink_excuse = grow_excuse = _("Swap can not be resized here");
+ } else if (client.blocks_available[block.path]) {
+ info = {
+ can_shrink: true,
+ can_grow: true,
+ shrink_needs_unmount: false,
+ grow_needs_unmount: false,
+ };
+ } else {
+ info = {
+ can_shrink: false,
+ can_grow: true,
+ grow_needs_unmount: true
+ };
+ shrink_excuse = _("Unrecognized data can not be made smaller here");
+ }
+ if (to_fit) {
+ // Shrink to fit doesn't need to resize the content
+ shrink_excuse = null;
+ }
+ } else {
+ info = { };
+ shrink_excuse = grow_excuse = _("Activate before resizing");
+ }
+
+ return { info, shrink_excuse, grow_excuse };
+}
+
+export function free_space_after_part(client, part) {
+ const parts = get_partitions(client, client.blocks[part.Table]);
+
+ function find_it(parts) {
+ for (const p of parts) {
+ if (p.type == "free" && p.start == part.Offset + part.Size)
+ return p.size;
+ if (p.type == "container") {
+ const s = find_it(p.partitions);
+ if (s)
+ return s;
+ }
+ }
+ return false;
+ }
+
+ return find_it(parts) || 0;
+}
+
+export function grow_dialog(client, lvol_or_part, info, to_fit) {
+ let title, block, name, orig_size, max_size, allow_infinite, round_size;
+ let has_subvols, subvols, pvs_as_spaces, initial_pvs;
+
+ function compute_max_size(spaces) {
+ const layout = lvol_or_part.Layout;
+ const pvs = spaces.map(s => s.pvol);
+ const n_pvs = pvs.length;
+ const sum = pvs.reduce((sum, pv) => sum + pv.FreeSize, 0);
+ const min = Math.min.apply(null, pvs.map(pv => pv.FreeSize));
+
+ if (!has_subvols) {
+ return sum;
+ } else if (layout == "raid0") {
+ return n_pvs * min;
+ } else if (layout == "raid1") {
+ return min;
+ } else if (layout == "raid10") {
+ return (n_pvs / 2) * min;
+ } else if ((layout == "raid4" || layout == "raid5")) {
+ return (n_pvs - 1) * min;
+ } else if (layout == "raid6") {
+ return (n_pvs - 2) * min;
+ } else
+ return 0; // not-covered: internal error
+ }
+
+ if (lvol_or_part.iface == "org.freedesktop.UDisks2.LogicalVolume") {
+ const vgroup = client.vgroups[lvol_or_part.VolumeGroup];
+ const pool = client.lvols[lvol_or_part.ThinPool];
+
+ pvs_as_spaces = pvs_to_spaces(client, client.vgroups_pvols[vgroup.path].filter(pvol => pvol.FreeSize > 0));
+ subvols = client.lvols_stripe_summary[lvol_or_part.path];
+ has_subvols = subvols && (lvol_or_part.Layout == "mirror" || lvol_or_part.Layout.indexOf("raid") == 0);
+
+ if (!has_subvols)
+ initial_pvs = pvs_as_spaces;
+ else {
+ initial_pvs = [];
+
+ // Step 1: Find the spaces that are already used for a
+ // subvolume. If a subvolume uses more than one, prefer the
+ // one with more available space.
+ for (const sv of subvols) {
+ let sel = null;
+ for (const p in sv) {
+ for (const spc of pvs_as_spaces)
+ if (spc.block.path == p && (!sel || sel.size < spc.size))
+ sel = spc;
+ }
+ if (sel)
+ initial_pvs.push(sel);
+ }
+
+ // Step 2: Select missing one randomly.
+ for (const pv of pvs_as_spaces) {
+ if (initial_pvs.indexOf(pv) == -1 && initial_pvs.length < subvols.length)
+ initial_pvs.push(pv);
+ }
+ }
+
+ title = _("Grow logical volume");
+ block = client.lvols_block[lvol_or_part.path];
+ name = lvol_or_part.Name;
+ orig_size = lvol_or_part.Size;
+ max_size = pool ? pool.Size * 3 : lvol_or_part.Size + compute_max_size(initial_pvs);
+ allow_infinite = !!pool;
+ round_size = vgroup.ExtentSize;
+ } else {
+ has_subvols = false;
+ title = _("Grow partition");
+ block = client.blocks[lvol_or_part.path];
+ name = block_name(block);
+ orig_size = lvol_or_part.Size;
+ max_size = lvol_or_part.Size + free_space_after_part(client, lvol_or_part);
+ allow_infinite = false;
+ round_size = 1024 * 1024;
+ }
+
+ const usage = get_active_usage(client,
+ block && info.grow_needs_unmount ? block.path : null,
+ _("grow"), null,
+ true);
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), name),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ let grow_size;
+ const size_fields = [];
+ if (!to_fit) {
+ if ((has_subvols || lvol_or_part.Layout == "linear") && pvs_as_spaces.length > 1)
+ size_fields.push(
+ SelectSpaces("pvs", _("Physical Volumes"),
+ {
+ spaces: pvs_as_spaces,
+ value: initial_pvs,
+ min_selected: subvols.length,
+ validate: val => {
+ if (has_subvols && subvols.length != val.length)
+ return cockpit.format(_("Exactly $0 physical volumes must be selected"),
+ subvols.length);
+ }
+ }));
+ size_fields.push(
+ SizeSlider("size", _("Size"),
+ {
+ value: orig_size,
+ min: orig_size,
+ max: max_size,
+ allow_infinite,
+ round: round_size,
+ }));
+ } else {
+ grow_size = block.Size;
+ }
+
+ let recovered_passphrase;
+ let passphrase_fields = [];
+ if (block && block.IdType == "crypto_LUKS" && block.IdVersion == 2)
+ passphrase_fields = existing_passphrase_fields(_("Resizing an encrypted filesystem requires unlocking the disk. Please provide a current disk passphrase."));
+
+ function prepare_pvs(pvs) {
+ if (!pvs)
+ return pvs;
+
+ pvs = pvs.map(spc => spc.block.path);
+
+ if (!has_subvols)
+ return pvs;
+
+ const subvol_pvs = [];
+
+ // Step 1: Find PVs that are already used by a subvolume
+ subvols.forEach((sv, idx) => {
+ subvol_pvs[idx] = null;
+ for (const pv in sv) {
+ if (pvs.indexOf(pv) >= 0 && subvol_pvs.indexOf(pv) == -1) {
+ subvol_pvs[idx] = pv;
+ break;
+ }
+ }
+ });
+
+ // Step 2: Use the rest for the leftover subvolumes
+ subvols.forEach((sv, idx) => {
+ if (!subvol_pvs[idx]) {
+ for (const pv of pvs) {
+ if (subvol_pvs.indexOf(pv) == -1) {
+ subvol_pvs[idx] = pv;
+ break;
+ }
+ }
+ }
+ });
+
+ return subvol_pvs;
+ }
+
+ if (!usage.Teardown && size_fields.length + passphrase_fields.length === 0) {
+ return lvol_or_part_and_fsys_resize(client, lvol_or_part, grow_size, info.grow_needs_unmount,
+ null, prepare_pvs(initial_pvs));
+ }
+
+ const dlg = dialog_open({
+ Title: title,
+ Teardown: TeardownMessage(usage),
+ Body: has_subvols && <div><p>{cockpit.format(_("Exactly $0 physical volumes need to be selected, one for each stripe of the logical volume."), subvols.length)}</p><br /></div>,
+ Fields: size_fields.concat(passphrase_fields),
+ update: (dlg, vals, trigger) => {
+ if (vals.pvs) {
+ const max = lvol_or_part.Size + compute_max_size(vals.pvs);
+ if (vals.size > max)
+ dlg.set_values({ size: max });
+ dlg.set_options("size", { max });
+ }
+ },
+ Action: {
+ Title: _("Grow"),
+ disable_on_error: usage.Teardown,
+ action: function (vals) {
+ return teardown_active_usage(client, usage)
+ .then(function () {
+ return (lvol_or_part_and_fsys_resize(client, lvol_or_part,
+ to_fit ? grow_size : vals.size,
+ info.grow_needs_unmount,
+ vals.passphrase || recovered_passphrase,
+ prepare_pvs(vals.pvs))
+ .then(() => undo_temporary_teardown(client, usage))
+ .catch(request_passphrase_on_error_handler(dlg, vals, recovered_passphrase, block)));
+ });
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage),
+ passphrase_fields.length
+ ? init_existing_passphrase(block, false, pp => { recovered_passphrase = pp })
+ : null
+ ]
+ });
+}
+
+export function shrink_dialog(client, lvol_or_part, info, to_fit) {
+ let title, block, name, orig_size, round_size;
+
+ if (lvol_or_part.iface == "org.freedesktop.UDisks2.LogicalVolume") {
+ const vgroup = client.vgroups[lvol_or_part.VolumeGroup];
+
+ title = _("Shrink logical volume");
+ block = client.lvols_block[lvol_or_part.path];
+ name = lvol_or_part.Name;
+ orig_size = lvol_or_part.Size;
+ round_size = vgroup.ExtentSize;
+ } else {
+ title = _("Shrink partition");
+ block = client.blocks[lvol_or_part.path];
+ name = block_name(block);
+ orig_size = lvol_or_part.Size;
+ round_size = 1024 * 1024;
+ }
+
+ const usage = get_active_usage(client,
+ block && !to_fit && info.shrink_needs_unmount ? block.path : null,
+ _("shrink"), null,
+ true);
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), name),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ let shrink_size;
+ let size_fields = [];
+ if (!to_fit) {
+ size_fields = [
+ SizeSlider("size", _("Size"),
+ {
+ value: orig_size,
+ max: orig_size,
+ round: round_size,
+ })
+ ];
+ } else {
+ const crypto = client.blocks_crypto[block.path];
+ const cleartext = client.blocks_cleartext[block.path];
+ let content_path = null;
+ let crypto_overhead = 0;
+
+ if (crypto) {
+ if (crypto.MetadataSize !== undefined && cleartext) {
+ content_path = cleartext.path;
+ crypto_overhead = crypto.MetadataSize;
+ }
+ } else {
+ content_path = block.path;
+ }
+
+ const fsys = client.blocks_fsys[content_path];
+ if (fsys)
+ shrink_size = fsys.Size + crypto_overhead;
+
+ const vdo = client.legacy_vdo_overlay.find_by_backing_block(client.blocks[content_path]);
+ if (vdo)
+ shrink_size = vdo.physical_size + crypto_overhead;
+
+ const stratis_bdev = client.blocks_stratis_blockdev[content_path];
+ if (stratis_bdev)
+ shrink_size = Number(stratis_bdev.TotalPhysicalSize) + crypto_overhead;
+
+ if (shrink_size === undefined) {
+ console.warn("Couldn't determine size to shrink to."); // not-covered: safety check
+ return; // not-covered: safety check
+ }
+ }
+
+ let recovered_passphrase;
+ let passphrase_fields = [];
+ if (block && block.IdType == "crypto_LUKS" && block.IdVersion == 2)
+ passphrase_fields = existing_passphrase_fields(_("Resizing an encrypted filesystem requires unlocking the disk. Please provide a current disk passphrase."));
+
+ if (usage.length == 0 && size_fields.length + passphrase_fields.length === 0) {
+ return lvol_or_part_and_fsys_resize(client, lvol_or_part, shrink_size, false, null);
+ }
+
+ const dlg = dialog_open({
+ Title: title,
+ Teardown: TeardownMessage(usage),
+ Fields: size_fields.concat(passphrase_fields),
+ Action: {
+ Title: _("Shrink"),
+ disable_on_error: usage.Teardown,
+ action: function (vals) {
+ return teardown_active_usage(client, usage)
+ .then(function () {
+ return (lvol_or_part_and_fsys_resize(client, lvol_or_part,
+ to_fit ? shrink_size : vals.size,
+ to_fit ? false : info.shrink_needs_unmount,
+ vals.passphrase || recovered_passphrase)
+ .then(() => undo_temporary_teardown(client, usage))
+ .catch(request_passphrase_on_error_handler(dlg, vals, recovered_passphrase, block)));
+ });
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage),
+ passphrase_fields.length
+ ? init_existing_passphrase(block, false, pp => { recovered_passphrase = pp })
+ : null
+ ]
+ });
+}
diff --git a/pkg/storaged/block/unformatted-data.jsx b/pkg/storaged/block/unformatted-data.jsx
new file mode 100644
index 0000000..7c4e5ed
--- /dev/null
+++ b/pkg/storaged/block/unformatted-data.jsx
@@ -0,0 +1,39 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import client from "../client";
+
+import { StorageCard, new_card } from "../pages.jsx";
+import { format_dialog } from "./format-dialog.jsx";
+import { std_lock_action } from "../crypto/actions.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_unformatted_data_card(next, backing_block, content_block) {
+ return new_card({
+ title: _("Unformatted data"),
+ next,
+ component: StorageCard,
+ actions: [
+ std_lock_action(backing_block, content_block),
+ { title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true },
+ ]
+ });
+}
diff --git a/pkg/storaged/block/unrecognized-data.jsx b/pkg/storaged/block/unrecognized-data.jsx
new file mode 100644
index 0000000..f5056d6
--- /dev/null
+++ b/pkg/storaged/block/unrecognized-data.jsx
@@ -0,0 +1,57 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { StorageCard, StorageDescription, new_card } from "../pages.jsx";
+import { format_dialog } from "./format-dialog.jsx";
+import { std_lock_action } from "../crypto/actions.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_unrecognized_data_card(next, backing_block, content_block) {
+ return new_card({
+ title: _("Unrecognized data"),
+ next,
+ component: UnrecognizedDataCard,
+ props: { backing_block, content_block },
+ actions: [
+ std_lock_action(backing_block, content_block),
+ { title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true },
+ ]
+ });
+}
+
+export const UnrecognizedDataCard = ({ card, backing_block, content_block }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Usage")} value={content_block.IdUsage || "-"} />
+ <StorageDescription title={_("Type")} value={content_block.IdType || "-"} />
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/btrfs/device.jsx b/pkg/storaged/btrfs/device.jsx
new file mode 100644
index 0000000..3f31724
--- /dev/null
+++ b/pkg/storaged/btrfs/device.jsx
@@ -0,0 +1,97 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { StorageCard, StorageDescription, new_card, register_crossref } from "../pages.jsx";
+import { StorageUsageBar } from "../storage-controls.jsx";
+import { std_lock_action } from "../crypto/actions.jsx";
+import { format_dialog } from "../block/format-dialog.jsx";
+import { btrfs_device_usage } from "./utils.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_btrfs_device_card(next, backing_block, content_block, block_btrfs) {
+ const label = block_btrfs && block_btrfs.data.label;
+ const uuid = block_btrfs && block_btrfs.data.uuid;
+ const use = btrfs_device_usage(client, uuid, block_btrfs.path);
+
+ const btrfs_card = new_card({
+ title: _("btrfs device"),
+ location: label || uuid,
+ next,
+ component: BtrfsDeviceCard,
+ props: { backing_block, content_block },
+ actions: btrfs_device_actions(backing_block, content_block),
+ });
+
+ register_crossref({
+ key: uuid,
+ card: btrfs_card,
+ size: <StorageUsageBar stats={use} short />,
+ });
+
+ return btrfs_card;
+}
+
+export const BtrfsDeviceCard = ({ card, backing_block, content_block }) => {
+ const block_btrfs = client.blocks_fsys_btrfs[content_block.path];
+ const uuid = block_btrfs && block_btrfs.data.uuid;
+ const label = block_btrfs && block_btrfs.data.label;
+ const use = btrfs_device_usage(client, uuid, block_btrfs.path);
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("btrfs volume")}>
+ {uuid
+ ? <Button variant="link" isInline role="link"
+ onClick={() => cockpit.location.go(["btrfs-volume", uuid])}>
+ {label || uuid}
+ </Button>
+ : "-"
+ }
+ </StorageDescription>
+ <StorageDescription title={_("UUID")} value={content_block.IdUUID} />
+ { block_btrfs &&
+ <StorageDescription title={_("Usage")}>
+ <StorageUsageBar key="s" stats={use} />
+ </StorageDescription>
+ }
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>);
+};
+
+export function btrfs_device_actions(backing_block, content_block) {
+ if (backing_block && content_block)
+ return [
+ std_lock_action(backing_block, content_block),
+ { title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true },
+ ];
+ else
+ return [];
+}
diff --git a/pkg/storaged/btrfs/filesystem.jsx b/pkg/storaged/btrfs/filesystem.jsx
new file mode 100644
index 0000000..d062d7a
--- /dev/null
+++ b/pkg/storaged/btrfs/filesystem.jsx
@@ -0,0 +1,92 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hopeg that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import {
+ new_card, ChildrenTable, StorageCard, StorageDescription
+} from "../pages.jsx";
+import { StorageUsageBar, StorageLink } from "../storage-controls.jsx";
+import { btrfs_device_usage, btrfs_is_volume_mounted } from "./utils.jsx";
+import { btrfs_device_actions } from "./device.jsx";
+import { rename_dialog } from "./volume.jsx";
+
+const _ = cockpit.gettext;
+
+/**
+ * For single btrfs volumes we show the data as a filesystem card with the
+ * subvolumes directly undernearth. This differentiates from multi device
+ * volumes, there they are shown under a different card.
+ */
+export function make_btrfs_filesystem_card(next, backing_block, content_block) {
+ return new_card({
+ title: _("btrfs filesystem"),
+ next,
+ actions: btrfs_device_actions(backing_block, content_block),
+ component: BtrfsFilesystemCard,
+ props: { backing_block, content_block },
+ });
+}
+
+const BtrfsFilesystemCard = ({ card, backing_block, content_block }) => {
+ const block_btrfs = client.blocks_fsys_btrfs[content_block.path];
+ const uuid = block_btrfs && block_btrfs.data.uuid;
+ const label = block_btrfs && block_btrfs.data.label;
+ const use = btrfs_device_usage(client, uuid, block_btrfs.path);
+
+ // Changing the label is only supported when the device is not mounted
+ // otherwise we will get btrfs filesystem error ERROR: device /dev/vda5 is
+ // mounted, use mount point. This is a libblockdev/udisks limitation as it
+ // only passes the device and not the mountpoint when the device is mounted.
+ // https://github.com/storaged-project/libblockdev/issues/966
+ const is_mounted = btrfs_is_volume_mounted(client, [backing_block]);
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Label")}
+ value={label}
+ action={
+ <StorageLink onClick={() => rename_dialog(block_btrfs, label)}
+ excuse={is_mounted ? _("Btrfs volume is mounted") : null}>
+ {_("edit")}
+ </StorageLink>}
+ />
+ <StorageDescription title={_("UUID")} value={content_block.IdUUID} />
+ { block_btrfs &&
+ <StorageDescription title={_("Usage")}>
+ <StorageUsageBar key="s" stats={use} />
+ </StorageDescription>
+ }
+ </DescriptionList>
+ </CardBody>
+ <CardBody className="contains-list">
+ <ChildrenTable emptyCaption={_("No subvolumes")}
+ aria-label={_("btrfs subvolumes")}
+ page={card.page} />
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/btrfs/subvolume.jsx b/pkg/storaged/btrfs/subvolume.jsx
new file mode 100644
index 0000000..7c9d470
--- /dev/null
+++ b/pkg/storaged/btrfs/subvolume.jsx
@@ -0,0 +1,343 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { StorageCard, StorageDescription, new_card, new_page, navigate_away_from_card } from "../pages.jsx";
+import { StorageUsageBar } from "../storage-controls.jsx";
+import {
+ encode_filename, get_fstab_config_with_client, reload_systemd, extract_option, parse_options,
+ flatten, teardown_active_usage,
+} from "../utils.js";
+import { btrfs_usage, validate_subvolume_name } from "./utils.jsx";
+import { at_boot_input, mounting_dialog, mount_options } from "../filesystem/mounting-dialog.jsx";
+import {
+ dialog_open, TextInput,
+ TeardownMessage, init_active_usage_processes,
+} from "../dialog.jsx";
+import { check_mismounted_fsys, MismountAlert } from "../filesystem/mismounting.jsx";
+import {
+ is_mounted, is_valid_mount_point, mount_point_text, MountPoint, edit_mount_point
+} from "../filesystem/utils.jsx";
+import client, { btrfs_poll } from "../client.js";
+
+const _ = cockpit.gettext;
+
+function subvolume_unmount(volume, subvol, forced_options) {
+ const block = client.blocks[volume.path];
+ mounting_dialog(client, block, "unmount", forced_options, subvol);
+}
+
+function subvolume_mount(volume, subvol, forced_options) {
+ const block = client.blocks[volume.path];
+ mounting_dialog(client, block, "mount", forced_options, subvol);
+}
+
+function get_mount_point_in_parent(volume, subvol) {
+ const block = client.blocks[volume.path];
+ const subvols = client.uuids_btrfs_subvols[volume.data.uuid];
+ if (!subvols)
+ return null;
+
+ for (const p of subvols) {
+ const has_parent_subvol = (p.pathname == "/" && subvol.pathname !== "/") ||
+ (subvol.pathname.substring(0, p.pathname.length) == p.pathname &&
+ subvol.pathname[p.pathname.length] == "/");
+ if (has_parent_subvol && is_mounted(client, block, p)) {
+ const [, pmp, opts] = get_fstab_config_with_client(client, block, false, p);
+ const opt_ro = extract_option(parse_options(opts), "ro");
+ if (!opt_ro) {
+ if (p.pathname == "/")
+ return pmp + "/" + subvol.pathname;
+ else
+ return pmp + subvol.pathname.substring(p.pathname.length);
+ }
+ }
+ }
+ return null;
+}
+
+function set_mount_options(subvol, block, vals) {
+ const mount_options = [];
+ const mount_now = vals.variant != "nomount";
+
+ if (!mount_now || vals.at_boot == "never") {
+ mount_options.push("noauto");
+ }
+ if (vals.mount_options?.ro)
+ mount_options.push("ro");
+ if (vals.at_boot == "never")
+ mount_options.push("x-cockpit-never-auto");
+ if (vals.at_boot == "nofail")
+ mount_options.push("nofail");
+ if (vals.at_boot == "netdev")
+ mount_options.push("_netdev");
+
+ const name = (subvol.pathname == "/" ? vals.name : subvol.pathname + "/" + vals.name);
+ mount_options.push("subvol=" + name);
+ if (vals.mount_options?.extra)
+ mount_options.push(vals.mount_options.extra);
+
+ let mount_point = vals.mount_point;
+ if (mount_point[0] != "/")
+ mount_point = "/" + mount_point;
+ mount_point = client.add_mount_point_prefix(mount_point);
+
+ const config =
+ ["fstab",
+ {
+ dir: { t: 'ay', v: encode_filename(mount_point) },
+ type: { t: 'ay', v: encode_filename("btrfs") },
+ opts: { t: 'ay', v: encode_filename(mount_options.join(",") || "defaults") },
+ freq: { t: 'i', v: 0 },
+ passno: { t: 'i', v: 0 },
+ "track-parents": { t: 'b', v: true }
+ }
+ ];
+
+ return block.AddConfigurationItem(config, {})
+ .then(reload_systemd)
+ .then(() => {
+ if (mount_now) {
+ return client.mount_at(block, mount_point);
+ } else
+ return Promise.resolve();
+ });
+}
+
+function subvolume_create(volume, subvol, parent_dir) {
+ const block = client.blocks[volume.path];
+
+ let action_variants = [
+ { tag: null, Title: _("Create and mount") },
+ { tag: "nomount", Title: _("Create only") }
+ ];
+
+ if (client.in_anaconda_mode()) {
+ action_variants = [
+ { tag: "nomount", Title: _("Create") }
+ ];
+ }
+
+ dialog_open({
+ Title: _("Create subvolume"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ validate: name => validate_subvolume_name(name)
+ }),
+ TextInput("mount_point", _("Mount Point"),
+ {
+ validate: (val, _values, variant) => {
+ return is_valid_mount_point(client,
+ block,
+ client.add_mount_point_prefix(val),
+ variant == "nomount");
+ }
+ }),
+ mount_options(false, false),
+ at_boot_input("local"),
+ ],
+ Action: {
+ Variants: action_variants,
+ action: async function (vals) {
+ // HACK: cannot use block_btrfs.CreateSubvolume as it always creates a subvolume relative to MountPoints[0] which
+ // makes it impossible to handle a situation where we have multiple subvolumes mounted.
+ // https://github.com/storaged-project/udisks/issues/1242
+ await cockpit.spawn(["btrfs", "subvolume", "create", `${parent_dir}/${vals.name}`], { superuser: "require", err: "message" });
+ await btrfs_poll();
+ if (vals.mount_point !== "") {
+ await set_mount_options(subvol, block, vals);
+ }
+ }
+ }
+ });
+}
+
+function subvolume_delete(volume, subvol, mount_point_in_parent, card) {
+ const block = client.blocks[volume.path];
+ const subvols = client.uuids_btrfs_subvols[volume.data.uuid];
+
+ function get_direct_subvol_children(subvol) {
+ function is_direct_parent(sv) {
+ return (sv.pathname.length > subvol.pathname.length &&
+ sv.pathname.substring(0, subvol.pathname.length) == subvol.pathname &&
+ sv.pathname[subvol.pathname.length] == "/" &&
+ sv.pathname.substring(subvol.pathname.length + 1).indexOf("/") == -1);
+ }
+
+ return subvols.filter(is_direct_parent);
+ }
+
+ function get_subvol_children(subvol) {
+ // The deepest nested children must come first
+ const direct_children = get_direct_subvol_children(subvol);
+ return flatten(direct_children.map(get_subvol_children)).concat(direct_children);
+ }
+
+ const all_subvols = get_subvol_children(subvol).concat([subvol]);
+ const configs_to_remove = [];
+ const paths_to_delete = [];
+ const usage = [];
+
+ for (const sv of all_subvols) {
+ const [config, mount_point] = get_fstab_config_with_client(client, block, false, sv);
+ const fs_is_mounted = is_mounted(client, block, sv);
+
+ usage.push({
+ level: 0,
+ usage: fs_is_mounted ? 'mounted' : 'none',
+ block,
+ name: sv.pathname,
+ location: mount_point,
+ actions: fs_is_mounted ? [_("unmount"), _("delete")] : [_("delete")],
+ blocking: false,
+ });
+
+ if (config)
+ configs_to_remove.push(config);
+
+ paths_to_delete.push(mount_point_in_parent + sv.pathname.substring(subvol.pathname.length));
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Permanently delete subvolume $0?"), subvol.pathname),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Title: _("Delete"),
+ Danger: _("Deleting erases all data on this subvolume and all it's children."),
+ action: async function () {
+ await teardown_active_usage(client, usage);
+ for (const c of configs_to_remove)
+ await block.RemoveConfigurationItem(c, {});
+ await cockpit.spawn(["btrfs", "subvolume", "delete"].concat(paths_to_delete),
+ { superuser: true, err: "message" });
+ await btrfs_poll();
+ navigate_away_from_card(card);
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+}
+
+export function make_btrfs_subvolume_page(parent, volume, subvol) {
+ const actions = [];
+
+ const use = btrfs_usage(client, volume);
+ const block = client.blocks[volume.path];
+ const fstab_config = get_fstab_config_with_client(client, block, false, subvol);
+ const [, mount_point, opts] = fstab_config;
+ const opt_ro = extract_option(parse_options(opts), "ro");
+ const mismount_warning = check_mismounted_fsys(block, block, fstab_config, subvol);
+ const mounted = is_mounted(client, block, subvol);
+ const mp_text = mount_point_text(mount_point, mounted);
+ if (mp_text == null)
+ return null;
+ const forced_options = [`subvol=${subvol.pathname}`];
+ const mount_point_in_parent = get_mount_point_in_parent(volume, subvol);
+
+ if (client.in_anaconda_mode()) {
+ actions.push({
+ title: _("Edit mount point"),
+ action: () => edit_mount_point(block, forced_options, subvol),
+ });
+ }
+
+ if (mounted) {
+ actions.push({
+ title: _("Unmount"),
+ action: () => subvolume_unmount(volume, subvol, forced_options),
+ });
+ } else {
+ actions.push({
+ title: _("Mount"),
+ action: () => subvolume_mount(volume, subvol, forced_options),
+ });
+ }
+
+ // If the current subvolume is mounted rw with an fstab entry or any parent
+ // subvolume is mounted rw with an fstab entry allow subvolume creation.
+ let create_excuse = "";
+ if (!mount_point_in_parent) {
+ if (!mounted)
+ create_excuse = _("Subvolume needs to be mounted");
+ else if (opt_ro)
+ create_excuse = _("Subvolume needs to be mounted writable");
+ }
+
+ actions.push({
+ title: _("Create subvolume"),
+ excuse: create_excuse,
+ action: () => subvolume_create(volume, subvol, (mounted && !opt_ro) ? mount_point : mount_point_in_parent),
+ });
+
+ let delete_excuse = "";
+ if (!mount_point_in_parent) {
+ delete_excuse = _("At least one parent needs to be mounted writable");
+ }
+
+ // Don't show deletion for the root subvolume as it can never be deleted.
+ if (subvol.id !== 5)
+ actions.push({
+ danger: true,
+ title: _("Delete"),
+ excuse: delete_excuse,
+ action: () => subvolume_delete(volume, subvol, mount_point_in_parent, card),
+ });
+
+ const card = new_card({
+ title: _("btrfs subvolume"),
+ next: null,
+ page_location: ["btrfs", volume.data.uuid, subvol.pathname],
+ page_name: subvol.pathname,
+ page_size: is_mounted && <StorageUsageBar stats={use} short />,
+ location: mp_text,
+ component: BtrfsSubvolumeCard,
+ has_warning: !!mismount_warning,
+ props: { subvol, mount_point, mismount_warning, block, fstab_config, forced_options },
+ actions,
+ });
+ new_page(parent, card);
+}
+
+const BtrfsSubvolumeCard = ({ card, subvol, mismount_warning, block, fstab_config, forced_options }) => {
+ return (
+ <StorageCard card={card} alert={mismount_warning &&
+ <MismountAlert warning={mismount_warning}
+ fstab_config={fstab_config}
+ backing_block={block} content_block={block} subvol={subvol} />}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Name")} value={subvol.pathname} />
+ <StorageDescription title={_("ID")} value={subvol.id} />
+ <StorageDescription title={_("Mount point")}>
+ <MountPoint fstab_config={fstab_config}
+ backing_block={block} content_block={block}
+ forced_options={forced_options} subvol={subvol} />
+ </StorageDescription>
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>);
+};
diff --git a/pkg/storaged/btrfs/utils.jsx b/pkg/storaged/btrfs/utils.jsx
new file mode 100644
index 0000000..402ded0
--- /dev/null
+++ b/pkg/storaged/btrfs/utils.jsx
@@ -0,0 +1,90 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import cockpit from "cockpit";
+
+import { decode_filename } from "../utils.js";
+
+const _ = cockpit.gettext;
+
+/*
+ * Calculate the usage based on the data from `btrfs filesystem show` which has
+ * been made available to client.uuids_btrfs_usage. The size/usage is provided
+ * per block device.
+ */
+export function btrfs_device_usage(client, uuid, path) {
+ const block = client.blocks[path];
+ const device = block && block.Device;
+ const uuid_usage = client.uuids_btrfs_usage[uuid];
+ if (uuid_usage && device) {
+ const usage = uuid_usage[decode_filename(device)];
+ if (usage) {
+ return [usage, block.Size];
+ }
+ }
+ return [0, block.Size];
+}
+
+/**
+ * Calculate the overal btrfs "volume" usage. UDisks only knows the usage per block.
+ */
+export function btrfs_usage(client, volume) {
+ const block_fsys = client.blocks_fsys[volume.path];
+ const mount_point = block_fsys && block_fsys.MountPoints[0];
+ let use = mount_point && client.fsys_sizes.data[decode_filename(mount_point)];
+ if (!use)
+ use = [volume.data.used, client.uuids_btrfs_blocks[volume.data.uuid].reduce((sum, b) => sum + b.Size, 0)];
+ return use;
+}
+
+/**
+ * Is the btrfs volume mounted anywhere
+ */
+export function btrfs_is_volume_mounted(client, block_devices) {
+ for (const block_device of block_devices) {
+ const block_fs = client.blocks_fsys[block_device.path];
+ if (block_fs && block_fs.MountPoints.length > 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+export function parse_subvol_from_options(options) {
+ const subvol = { };
+ const subvolid_match = options.match(/subvolid=(?<subvolid>\d+)/);
+ const subvol_match = options.match(/subvol=(?<subvol>[\w\\/]+)/);
+ if (subvolid_match)
+ subvol.id = subvolid_match.groups.subvolid;
+ if (subvol_match)
+ subvol.pathname = subvol_match.groups.subvol;
+
+ if (subvolid_match || subvol_match)
+ return subvol;
+ else
+ return null;
+}
+
+export function validate_subvolume_name(name) {
+ if (name === "")
+ return _("Name cannot be empty.");
+ if (name.length > 255)
+ return _("Name cannot be longer than 255 characters.");
+ if (name.includes('/'))
+ return cockpit.format(_("Name cannot contain the character '/'."));
+}
diff --git a/pkg/storaged/btrfs/volume.jsx b/pkg/storaged/btrfs/volume.jsx
new file mode 100644
index 0000000..9d533f1
--- /dev/null
+++ b/pkg/storaged/btrfs/volume.jsx
@@ -0,0 +1,202 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hopeg that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardHeader, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { VolumeIcon } from "../icons/gnome-icons.jsx";
+
+import {
+ new_card, new_page, PAGE_CATEGORY_VIRTUAL,
+ get_crossrefs, ChildrenTable, PageTable, StorageCard, StorageDescription
+} from "../pages.jsx";
+import { StorageUsageBar, StorageLink } from "../storage-controls.jsx";
+import { fmt_size_long, validate_fsys_label, decode_filename, should_ignore } from "../utils.js";
+import { btrfs_usage, btrfs_is_volume_mounted, parse_subvol_from_options } from "./utils.jsx";
+import { dialog_open, TextInput } from "../dialog.jsx";
+import { make_btrfs_subvolume_page } from "./subvolume.jsx";
+import { btrfs_device_actions } from "./device.jsx";
+
+const _ = cockpit.gettext;
+
+/*
+ * Udisks is a disk/block library so it manages that, btrfs turns this a bit
+ * around and has one "volume" which can have multiple blocks by a unique uuid.
+ *
+ * Cockpit shows Btrfs as following:
+ *
+ * -> btrfs subvolume
+ * -> btrfs volume
+ * -> btrfs device
+ * -> block device
+ */
+export function make_btrfs_volume_page(parent, uuid) {
+ const block_devices = client.uuids_btrfs_blocks[uuid];
+ const block_btrfs = client.blocks_fsys_btrfs[block_devices[0].path];
+ const volume = client.uuids_btrfs_volume[uuid];
+ const use = btrfs_usage(client, volume);
+
+ if (block_devices.some(blk => should_ignore(client, blk.path)))
+ return;
+
+ // Single-device btrfs volumes are shown directly on the page of
+ // their device; they don't get a standalone "btrfs volume" page.
+ if (block_btrfs.data.num_devices === 1)
+ return;
+
+ const name = block_btrfs.data.label || uuid;
+ const btrfs_volume_card = new_card({
+ title: _("btrfs volume"),
+ next: null,
+ page_location: ["btrfs-volume", uuid],
+ page_name: name,
+ page_icon: VolumeIcon,
+ page_category: PAGE_CATEGORY_VIRTUAL,
+ page_size: use[1],
+ component: BtrfsVolumeCard,
+ props: { block_devices, uuid, use },
+ });
+
+ const subvolumes_card = make_btrfs_subvolumes_card(btrfs_volume_card, null, null);
+
+ const subvolumes_page = new_page(parent, subvolumes_card);
+ make_btrfs_subvolume_pages(subvolumes_page, volume);
+}
+
+export function rename_dialog(block_btrfs, label) {
+ dialog_open({
+ Title: _("Change label"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ validate: name => validate_fsys_label(name, "btrfs"),
+ value: label
+ })
+ ],
+ Action: {
+ Title: _("Save"),
+ action: function (vals) {
+ return block_btrfs.SetLabel(vals.name, {});
+ }
+ }
+ });
+}
+
+const BtrfsVolumeCard = ({ card, block_devices, uuid, use }) => {
+ const block_btrfs = client.blocks_fsys_btrfs[block_devices[0].path];
+ const label = block_btrfs.data.label || "-";
+
+ // Changing the label is only supported when the device is not mounted
+ // otherwise we will get btrfs filesystem error ERROR: device /dev/vda5 is
+ // mounted, use mount point. This is a libblockdev/udisks limitation as it
+ // only passes the device and not the mountpoint when the device is mounted.
+ // https://github.com/storaged-project/libblockdev/issues/966
+ const is_mounted = btrfs_is_volume_mounted(client, block_devices);
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Label")}
+ value={label}
+ action={
+ <StorageLink onClick={() => rename_dialog(block_btrfs, label)}
+ excuse={is_mounted ? _("Btrfs volume is mounted") : null}>
+ {_("edit")}
+ </StorageLink>}
+ />
+ <StorageDescription title={_("UUID")} value={uuid} />
+ <StorageDescription title={_("Capacity")} value={fmt_size_long(use[1])} />
+ <StorageDescription title={_("Usage")}>
+ <StorageUsageBar key="s" stats={use} />
+ </StorageDescription>
+ </DescriptionList>
+ </CardBody>
+ <CardHeader><strong>{_("btrfs devices")}</strong></CardHeader>
+ <CardBody className="contains-list">
+ <PageTable emptyCaption={_("No devices found")}
+ aria-label={_("btrfs device")}
+ crossrefs={get_crossrefs(uuid)} />
+ </CardBody>
+ </StorageCard>
+ );
+};
+
+export function make_btrfs_subvolumes_card(next, block, backing_block) {
+ return new_card({
+ title: _("btrfs subvolumes"),
+ next,
+ actions: btrfs_device_actions(block, backing_block),
+ component: BtrfsSubVolumesCard,
+ });
+}
+
+const BtrfsSubVolumesCard = ({ card }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody className="contains-list">
+ <ChildrenTable emptyCaption={_("No subvolumes")}
+ aria-label={_("btrfs subvolumes")}
+ page={card.page} />
+ </CardBody>
+ </StorageCard>
+ );
+};
+
+export function make_btrfs_subvolume_pages(parent, volume) {
+ const subvols = client.uuids_btrfs_subvols[volume.data.uuid];
+ if (subvols) {
+ for (const subvol of subvols) {
+ make_btrfs_subvolume_page(parent, volume, subvol);
+ }
+ } else {
+ const block = client.blocks[volume.path];
+ /*
+ * Try to show subvolumes based on fstab entries, this is a bit tricky
+ * as mounts where subvolid cannot be shown userfriendly.
+ */
+ let has_root = false;
+ for (const config of block.Configuration) {
+ if (config[0] == "fstab") {
+ const opts = config[1].opts;
+ if (!opts)
+ continue;
+
+ const fstab_subvol = parse_subvol_from_options(decode_filename(opts.v));
+
+ if (fstab_subvol === null)
+ continue;
+
+ if (fstab_subvol.pathname === "/")
+ has_root = true;
+
+ if (fstab_subvol.pathname)
+ make_btrfs_subvolume_page(parent, volume, fstab_subvol);
+ }
+ }
+
+ if (!has_root) {
+ // Always show the root subvolume even when the volume is not mounted.
+ make_btrfs_subvolume_page(parent, volume, { pathname: "/", id: 5 });
+ }
+ }
+}
diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js
new file mode 100644
index 0000000..6769316
--- /dev/null
+++ b/pkg/storaged/client.js
@@ -0,0 +1,1767 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import * as PK from 'packagekit.js';
+import { superuser } from 'superuser';
+
+import * as utils from './utils.js';
+
+import * as python from "python.js";
+import { read_os_release } from "os-release.js";
+
+import inotify_py from "inotify.py";
+import mount_users_py from "./mount-users.py";
+import nfs_mounts_py from "./nfs/nfs-mounts.py";
+import vdo_monitor_py from "./legacy-vdo/vdo-monitor.py";
+import stratis2_set_key_py from "./stratis/stratis2-set-key.py";
+import stratis3_set_key_py from "./stratis/stratis3-set-key.py";
+
+import { reset_pages } from "./pages.jsx";
+import { make_overview_page } from "./overview/overview.jsx";
+import { export_mount_point_mapping } from "./anaconda.jsx";
+
+import deep_equal from "deep-equal";
+
+/* STORAGED CLIENT
+ */
+
+function debug() {
+ if (window.debugging == "all" || window.debugging?.includes("storaged")) // not-covered: debugging
+ console.debug.apply(console, arguments); // not-covered: debugging
+}
+
+const client = {
+ busy: 0
+};
+
+cockpit.event_target(client);
+
+client.run = (func) => {
+ const prom = func();
+ if (prom) {
+ client.busy += 1;
+ return prom.finally(() => {
+ client.busy -= 1;
+ client.dispatchEvent("changed");
+ });
+ }
+};
+
+/* Superuser
+ */
+
+client.superuser = superuser;
+client.superuser.reload_page_on_change();
+client.superuser.addEventListener("changed", () => client.dispatchEvent("changed"));
+
+/* Metrics
+ */
+
+function instance_sampler(metrics, source) {
+ let instances;
+ const self = {
+ data: { },
+ close
+ };
+
+ cockpit.event_target(self);
+
+ function handle_meta(msg) {
+ self.data = { };
+ instances = [];
+ for (let m = 0; m < msg.metrics.length; m++) {
+ instances[m] = msg.metrics[m].instances;
+ for (let i = 0; i < instances[m].length; i++)
+ self.data[instances[m][i]] = [];
+ }
+ if (Object.keys(self.data).length > 100) {
+ close();
+ self.data = { };
+ }
+ }
+
+ function handle_data(msg) {
+ let changed = false;
+ for (let s = 0; s < msg.length; s++) {
+ const metrics = msg[s];
+ for (let m = 0; m < metrics.length; m++) {
+ const inst = metrics[m];
+ for (let i = 0; i < inst.length; i++) {
+ if (inst[i] !== null && inst[i] != self.data[instances[m][i]][m]) {
+ changed = true;
+ self.data[instances[m][i]][m] = inst[i];
+ }
+ }
+ }
+ }
+ if (changed)
+ self.dispatchEvent('changed');
+ }
+
+ const channel = cockpit.channel({
+ payload: "metrics1",
+ source: source || "internal",
+ metrics
+ });
+ channel.addEventListener("closed", function (event, error) {
+ console.log("closed", error);
+ });
+ channel.addEventListener("message", function (event, message) {
+ const msg = JSON.parse(message);
+ if (msg.length)
+ handle_data(msg);
+ else
+ handle_meta(msg);
+ });
+
+ function close() {
+ channel.close();
+ }
+
+ return self;
+}
+
+/* D-Bus proxies
+ */
+
+client.time_offset = undefined; /* Number of milliseconds that the server is ahead of us. */
+client.features = undefined;
+
+client.storaged_client = undefined;
+
+function proxy(iface, path) {
+ return client.storaged_client.proxy("org.freedesktop.UDisks2." + iface,
+ "/org/freedesktop/UDisks2/" + path,
+ { watch: true });
+}
+
+function proxies(iface) {
+ /* We create the proxies here with 'watch' set to false and
+ * establish a general watch for all of them. This is more
+ * efficient since it reduces the number of D-Bus calls done
+ * by the cache.
+ */
+ return client.storaged_client.proxies("org.freedesktop.UDisks2." + iface,
+ "/org/freedesktop/UDisks2",
+ { watch: false });
+}
+
+client.call = function call(path, iface, method, args, options) {
+ return client.storaged_client.call(path, "org.freedesktop.UDisks2." + iface, method, args, options);
+};
+
+function init_proxies () {
+ client.mdraids = proxies("MDRaid");
+ client.vgroups = proxies("VolumeGroup");
+ client.lvols = proxies("LogicalVolume");
+ client.drives = proxies("Drive");
+ client.drives_ata = proxies("Drive.Ata");
+ client.blocks = proxies("Block");
+ client.blocks_ptable = proxies("PartitionTable");
+ client.blocks_part = proxies("Partition");
+ client.blocks_lvm2 = proxies("Block.LVM2");
+ client.blocks_pvol = proxies("PhysicalVolume");
+ client.blocks_fsys = proxies("Filesystem");
+ client.blocks_crypto = proxies("Encrypted");
+ client.blocks_swap = proxies("Swapspace");
+ client.iscsi_sessions = proxies("ISCSI.Session");
+ client.vdo_vols = proxies("VDOVolume");
+ client.blocks_fsys_btrfs = proxies("Filesystem.BTRFS");
+ client.jobs = proxies("Job");
+
+ return client.storaged_client.watch({ path_namespace: "/org/freedesktop/UDisks2" });
+}
+
+/* Monitors
+ */
+
+client.fsys_sizes = instance_sampler([{ name: "mount.used" },
+ { name: "mount.total" }
+]);
+
+client.swap_sizes = instance_sampler([{ name: "swapdev.length" },
+ { name: "swapdev.free" },
+], "direct");
+
+/* Derived indices.
+ */
+
+function is_multipath_master(block) {
+ // The master has "mpath" in its device mapper UUID. In the
+ // future, storaged will hopefully provide this information
+ // directly.
+ if (block.Symlinks && block.Symlinks.length) {
+ for (let i = 0; i < block.Symlinks.length; i++)
+ if (utils.decode_filename(block.Symlinks[i]).indexOf("/dev/disk/by-id/dm-uuid-mpath-") === 0)
+ return true;
+ }
+ return false;
+}
+
+export async function btrfs_poll() {
+ const usage_regex = /used\s+(?<used>\d+)\s+path\s+(?<device>[\w/]+)/;
+ if (!client.uuids_btrfs_subvols)
+ client.uuids_btrfs_subvols = { };
+ if (!client.uuids_btrfs_usage)
+ client.uuids_btrfs_usage = { };
+ if (!client.uuids_btrfs_default_subvol)
+ client.uuids_btrfs_default_subvol = { };
+ if (!client.uuids_btrfs_volume)
+ return;
+
+ if (!client.superuser.allowed) {
+ return;
+ }
+
+ const uuids_subvols = { };
+ const uuids_usage = { };
+ const btrfs_default_subvol = { };
+ for (const uuid of Object.keys(client.uuids_btrfs_volume)) {
+ const blocks = client.uuids_btrfs_blocks[uuid];
+ if (!blocks)
+ continue;
+
+ // In multi device setups MountPoints can be on either of the block devices, so try them all.
+ const MountPoints = blocks.map(block => {
+ return client.blocks_fsys[block.path];
+ }).map(block_fsys => block_fsys.MountPoints).reduce((accum, current) => accum.concat(current));
+ const mp = MountPoints[0];
+ if (mp) {
+ const mount_point = utils.decode_filename(mp);
+ try {
+ // HACK: UDisks GetSubvolumes method uses `subvolume list -p` which
+ // does not show the full subvolume path which we want to show in the UI
+ //
+ // $ btrfs subvolume list -p /run/butter
+ // ID 256 gen 7 parent 5 top level 5 path one
+ // ID 257 gen 7 parent 256 top level 256 path two
+ // ID 258 gen 7 parent 257 top level 257 path two/three/four
+ //
+ // $ btrfs subvolume list -ap /run/butter
+ // ID 256 gen 7 parent 5 top level 5 path <FS_TREE>/one
+ // ID 257 gen 7 parent 256 top level 256 path one/two
+ // ID 258 gen 7 parent 257 top level 257 path <FS_TREE>/one/two/three/four
+ const output = await cockpit.spawn(["btrfs", "subvolume", "list", "-ap", mount_point], { superuser: true, err: "message" });
+ const subvols = [{ pathname: "/", id: 5 }];
+ for (const line of output.split("\n")) {
+ const m = line.match(/ID (\d+).*parent (\d+).*path (<FS_TREE>\/)?(.*)/);
+ if (m)
+ subvols.push({ pathname: m[4], id: Number(m[1]), parent: Number(m[2]) });
+ }
+ uuids_subvols[uuid] = subvols;
+ } catch (err) {
+ console.warn(`unable to obtain subvolumes for mount point ${mount_point}`, err);
+ }
+
+ // HACK: Obtain the default subvolume, required for mounts in which do not specify a subvol and subvolid.
+ // In the future can be obtained via UDisks, it requires the btrfs partition to be mounted somewhere.
+ // https://github.com/storaged-project/udisks/commit/b6966b7076cd837f9d307eef64beedf01bc863ae
+ try {
+ const output = await cockpit.spawn(["btrfs", "subvolume", "get-default", mount_point], { superuser: true, err: "message" });
+ const id_match = output.match(/ID (\d+).*/);
+ if (id_match)
+ btrfs_default_subvol[uuid] = Number(id_match[1]);
+ } catch (err) {
+ console.warn(`unable to obtain default subvolume for mount point ${mount_point}`, err);
+ }
+
+ // HACK: UDisks should expose a better btrfs API with btrfs device information
+ // https://github.com/storaged-project/udisks/issues/1232
+ // TODO: optimise into just parsing one `btrfs filesystem show`?
+ try {
+ const usage_output = await cockpit.spawn(["btrfs", "filesystem", "show", "--raw", uuid], { superuser: true, err: "message" });
+ const usages = {};
+ for (const line of usage_output.split("\n")) {
+ const match = usage_regex.exec(line);
+ if (match) {
+ const { used, device } = match.groups;
+ usages[device] = used;
+ }
+ }
+ uuids_usage[uuid] = usages;
+ } catch (err) {
+ console.warn(`btrfs filesystem show ${uuid}`, err);
+ }
+ } else {
+ uuids_subvols[uuid] = null;
+ uuids_usage[uuid] = null;
+ }
+ }
+
+ if (!deep_equal(client.uuids_btrfs_subvols, uuids_subvols) || !deep_equal(client.uuids_btrfs_usage, uuids_usage) ||
+ !deep_equal(client.uuids_btrfs_default_subvol, btrfs_default_subvol)) {
+ debug("btrfs_pol new subvols:", uuids_subvols);
+ client.uuids_btrfs_subvols = uuids_subvols;
+ client.uuids_btrfs_usage = uuids_usage;
+ debug("btrfs_pol usage:", uuids_usage);
+ client.uuids_btrfs_default_subvol = btrfs_default_subvol;
+ debug("btrfs_pol default subvolumes:", btrfs_default_subvol);
+ client.update();
+ }
+}
+
+function btrfs_findmnt_poll() {
+ if (!client.btrfs_mounts)
+ client.btrfs_mounts = { };
+
+ const update_btrfs_mounts = output => {
+ const btrfs_mounts = {};
+ try {
+ // Extract the data into a { uuid: { subvolid: { subvol, target } } }
+ const mounts = JSON.parse(output);
+ if ("filesystems" in mounts) {
+ for (const fs of mounts.filesystems) {
+ const subvolid_match = fs.options.match(/subvolid=(?<subvolid>\d+)/);
+ const subvol_match = fs.options.match(/subvol=(?<subvol>[\w\\/]+)/);
+ if (!subvolid_match && !subvol_match) {
+ console.warn("findmnt entry without subvol and subvolid", fs);
+ break;
+ }
+
+ const { subvolid } = subvolid_match.groups;
+ const { subvol } = subvol_match.groups;
+ const subvolume = {
+ pathname: subvol,
+ id: subvolid,
+ mount_points: [fs.target],
+ };
+
+ if (!(fs.uuid in btrfs_mounts)) {
+ btrfs_mounts[fs.uuid] = { };
+ }
+
+ // We need to handle multiple mounts, they are listed seperate.
+ if (subvolid in btrfs_mounts[fs.uuid]) {
+ btrfs_mounts[fs.uuid][subvolid].mount_points.push(fs.target);
+ } else {
+ btrfs_mounts[fs.uuid][subvolid] = subvolume;
+ }
+ }
+ }
+ } catch (exc) {
+ if (exc.message)
+ console.error("unable to parse findmnt JSON output", exc);
+ }
+
+ // Update client state
+ if (!deep_equal(client.btrfs_mounts, btrfs_mounts)) {
+ client.btrfs_mounts = btrfs_mounts;
+ debug("btrfs_findmnt_poll mounts:", client.btrfs_mounts);
+ client.update();
+ }
+ };
+
+ const findmnt_poll = () => {
+ return cockpit.spawn(["findmnt", "--type", "btrfs", "--mtab", "--poll"], { superuser: "try", err: "message" }).stream(() => {
+ cockpit.spawn(["findmnt", "--type", "btrfs", "--mtab", "-o", "UUID,OPTIONS,TARGET", "--json"],
+ { superuser: "try", err: "message" }).then(output => update_btrfs_mounts(output)).catch(err => {
+ // When there are no btrfs filesystems left this can fail and thus we need to manually reset the mount info.
+ client.btrfs_mounts = {};
+ client.update();
+ if (err.message) {
+ console.error("findmnt exited with an error", err);
+ }
+ });
+ }).catch(err => {
+ console.error("findmnt --poll exited with an error", err);
+ throw new Error("findmnt --poll stopped working");
+ });
+ };
+
+ // This fails when no btrfs filesystem is found with the --mtab option and exits with 1, so that is kinda useless, however without --mtab
+ // we don't get a nice flat structure. So we ignore the errors
+ cockpit.spawn(["findmnt", "--type", "btrfs", "--mtab", "-o", "UUID,OPTIONS,SOURCE,TARGET", "--json"],
+ { superuser: "try", err: "message" }).then(output => {
+ update_btrfs_mounts(output);
+ findmnt_poll();
+ }).catch(err => {
+ // only log error when there is a real issue.
+ if (client.superuser.allowed && err.message) {
+ console.error(`unable to run findmnt ${err}`);
+ }
+ findmnt_poll();
+ });
+}
+
+function btrfs_start_polling() {
+ debug("starting polling for btrfs subvolumes");
+ window.setInterval(btrfs_poll, 5000);
+ client.uuids_btrfs_subvols = { };
+ client.uuids_btrfs_usage = { };
+ client.uuids_btrfs_default_subvol = { };
+ client.btrfs_mounts = { };
+ btrfs_poll();
+ btrfs_findmnt_poll();
+}
+
+function update_indices() {
+ let path, block, mdraid, vgroup, pvol, lvol, pool, blockdev, fsys, part, i;
+
+ client.broken_multipath_present = false;
+ client.drives_multipath_blocks = { };
+ client.drives_block = { };
+ for (path in client.drives) {
+ client.drives_multipath_blocks[path] = [];
+ }
+ for (path in client.blocks) {
+ block = client.blocks[path];
+ if (!client.blocks_part[path] && client.drives_multipath_blocks[block.Drive] !== undefined) {
+ if (is_multipath_master(block))
+ client.drives_block[block.Drive] = block;
+ else
+ client.drives_multipath_blocks[block.Drive].push(block);
+ }
+ }
+ for (path in client.drives_multipath_blocks) {
+ /* If there is no multipath master and only a single
+ * member, then this is actually a normal singlepath
+ * device.
+ */
+
+ if (!client.drives_block[path] && client.drives_multipath_blocks[path].length == 1) {
+ client.drives_block[path] = client.drives_multipath_blocks[path][0];
+ client.drives_multipath_blocks[path] = [];
+ } else {
+ client.drives_multipath_blocks[path].sort(utils.block_cmp);
+ if (!client.drives_block[path])
+ client.broken_multipath_present = true;
+ }
+ }
+
+ client.mdraids_block = { };
+ for (path in client.blocks) {
+ block = client.blocks[path];
+ if (block.MDRaid != "/")
+ client.mdraids_block[block.MDRaid] = block;
+ }
+
+ client.mdraids_members = { };
+ for (path in client.mdraids) {
+ client.mdraids_members[path] = [];
+ }
+ for (path in client.blocks) {
+ block = client.blocks[path];
+ if (client.mdraids_members[block.MDRaidMember] !== undefined)
+ client.mdraids_members[block.MDRaidMember].push(block);
+ }
+ for (path in client.mdraids_members) {
+ client.mdraids_members[path].sort(utils.block_cmp);
+ }
+
+ client.slashdevs_block = { };
+ function enter_slashdev(block, enc) {
+ client.slashdevs_block[utils.decode_filename(enc)] = block;
+ }
+ for (path in client.blocks) {
+ block = client.blocks[path];
+ enter_slashdev(block, block.Device);
+ enter_slashdev(block, block.PreferredDevice);
+ for (i = 0; i < block.Symlinks.length; i++)
+ enter_slashdev(block, block.Symlinks[i]);
+ }
+
+ client.uuids_mdraid = { };
+ for (path in client.mdraids) {
+ mdraid = client.mdraids[path];
+ client.uuids_mdraid[mdraid.UUID] = mdraid;
+ }
+
+ client.vgnames_vgroup = { };
+ for (path in client.vgroups) {
+ vgroup = client.vgroups[path];
+ client.vgnames_vgroup[vgroup.Name] = vgroup;
+ }
+
+ const vgroups_with_dm_pvs = { };
+
+ client.vgroups_pvols = { };
+ for (path in client.vgroups) {
+ client.vgroups_pvols[path] = [];
+ }
+ for (path in client.blocks_pvol) {
+ pvol = client.blocks_pvol[path];
+ if (client.vgroups_pvols[pvol.VolumeGroup] !== undefined) {
+ client.vgroups_pvols[pvol.VolumeGroup].push(pvol);
+ {
+ // HACK - this is needed below to deal with a UDisks2 bug.
+ // https://github.com/storaged-project/udisks/pull/1206
+ const block = client.blocks[path];
+ if (block && utils.decode_filename(block.Device).indexOf("/dev/dm-") == 0)
+ vgroups_with_dm_pvs[pvol.VolumeGroup] = true;
+ }
+ }
+ }
+ function cmp_pvols(a, b) {
+ return utils.block_cmp(client.blocks[a.path], client.blocks[b.path]);
+ }
+ for (path in client.vgroups_pvols) {
+ client.vgroups_pvols[path].sort(cmp_pvols);
+ }
+
+ client.vgroups_lvols = { };
+ for (path in client.vgroups) {
+ client.vgroups_lvols[path] = [];
+ }
+ for (path in client.lvols) {
+ lvol = client.lvols[path];
+ if (client.vgroups_lvols[lvol.VolumeGroup] !== undefined)
+ client.vgroups_lvols[lvol.VolumeGroup].push(lvol);
+ }
+ for (path in client.vgroups_lvols) {
+ client.vgroups_lvols[path].sort(function (a, b) { return a.Name.localeCompare(b.Name) });
+ }
+
+ client.lvols_block = { };
+ for (path in client.blocks_lvm2) {
+ client.lvols_block[client.blocks_lvm2[path].LogicalVolume] = client.blocks[path];
+ }
+
+ client.lvols_pool_members = { };
+ for (path in client.lvols) {
+ if (client.lvols[path].Type == "pool")
+ client.lvols_pool_members[path] = [];
+ }
+ for (path in client.lvols) {
+ lvol = client.lvols[path];
+ if (client.lvols_pool_members[lvol.ThinPool] !== undefined)
+ client.lvols_pool_members[lvol.ThinPool].push(lvol);
+ }
+ for (path in client.lvols_pool_members) {
+ client.lvols_pool_members[path].sort(function (a, b) { return a.Name.localeCompare(b.Name) });
+ }
+
+ function summarize_stripe(lv_size, segments) {
+ const pvs = { };
+ let total_size = 0;
+ for (const [, size, pv] of segments) {
+ if (!pvs[pv])
+ pvs[pv] = 0;
+ pvs[pv] += size;
+ total_size += size;
+ }
+ if (total_size < lv_size)
+ pvs["/"] = lv_size - total_size;
+ return pvs;
+ }
+
+ client.lvols_stripe_summary = { };
+ client.lvols_status = { };
+ for (path in client.lvols) {
+ const struct = client.lvols[path].Structure;
+ const lvol = client.lvols[path];
+
+ // HACK - UDisks2 can't find the PVs of a segment when they
+ // are on a device mapper device.
+ //
+ // https://github.com/storaged-project/udisks/pull/1206
+
+ if (vgroups_with_dm_pvs[lvol.VolumeGroup])
+ continue;
+
+ let summary;
+ let status = "";
+ if (lvol.Layout != "thin" && struct && struct.segments) {
+ summary = summarize_stripe(struct.size.v, struct.segments.v);
+ if (summary["/"])
+ status = "partial";
+ } else if (struct && struct.data && struct.metadata &&
+ (struct.data.v.length == struct.metadata.v.length || struct.metadata.v.length == 0)) {
+ summary = [];
+ const n_total = struct.data.v.length;
+ let n_missing = 0;
+ for (let i = 0; i < n_total; i++) {
+ const data_lv = struct.data.v[i];
+ const metadata_lv = struct.metadata.v[i] || { size: { v: 0 }, segments: { v: [] } };
+
+ if (!data_lv.segments || (metadata_lv && !metadata_lv.segments)) {
+ summary = undefined;
+ break;
+ }
+
+ const s = summarize_stripe(data_lv.size.v + metadata_lv.size.v,
+ data_lv.segments.v.concat(metadata_lv.segments.v));
+ if (s["/"])
+ n_missing += 1;
+
+ summary.push(s);
+ }
+ if (n_missing > 0) {
+ status = "partial";
+ if (lvol.Layout == "raid1") {
+ if (n_total - n_missing >= 1)
+ status = "degraded";
+ }
+ if (lvol.Layout == "raid10") {
+ // This is correct for two-way mirroring, which is
+ // the only setup supported by lvm2.
+ if (n_missing > n_total / 2) {
+ // More than half of the PVs are gone -> at
+ // least one mirror has definitely lost both
+ // halves.
+ status = "partial";
+ } else if (n_missing > 1) {
+ // Two or more PVs are lost -> one mirror
+ // might have lost both halves
+ status = "degraded-maybe-partial";
+ } else {
+ // Only one PV is missing -> no mirror has
+ // lost both halves.
+ status = "degraded";
+ }
+ }
+ if (lvol.Layout == "raid4" || lvol.Layout == "raid5") {
+ if (n_missing <= 1)
+ status = "degraded";
+ }
+ if (lvol.Layout == "raid6") {
+ if (n_missing <= 2)
+ status = "degraded";
+ }
+ }
+ }
+ if (summary) {
+ client.lvols_stripe_summary[path] = summary;
+ client.lvols_status[path] = status;
+ }
+ }
+
+ client.stratis_poolnames_pool = { };
+ for (path in client.stratis_pools) {
+ pool = client.stratis_pools[path];
+ client.stratis_poolnames_pool[pool.Name] = pool;
+ }
+
+ client.stratis_pooluuids_pool = { };
+ for (path in client.stratis_pools) {
+ pool = client.stratis_pools[path];
+ client.stratis_pooluuids_pool[pool.Uuid] = pool;
+ }
+
+ client.stratis_pool_blockdevs = { };
+ for (path in client.stratis_pools) {
+ client.stratis_pool_blockdevs[path] = [];
+ }
+ for (path in client.stratis_blockdevs) {
+ blockdev = client.stratis_blockdevs[path];
+ if (client.stratis_pools[blockdev.Pool] !== undefined)
+ client.stratis_pool_blockdevs[blockdev.Pool].push(blockdev);
+ }
+
+ client.stratis_pool_filesystems = { };
+ for (path in client.stratis_pools) {
+ client.stratis_pool_filesystems[path] = [];
+ }
+ for (path in client.stratis_filesystems) {
+ fsys = client.stratis_filesystems[path];
+ if (client.stratis_pools[fsys.Pool] !== undefined)
+ client.stratis_pool_filesystems[fsys.Pool].push(fsys);
+ }
+
+ client.blocks_stratis_fsys = { };
+ for (path in client.stratis_filesystems) {
+ fsys = client.stratis_filesystems[path];
+ block = client.slashdevs_block[fsys.Devnode];
+ if (block)
+ client.blocks_stratis_fsys[block.path] = fsys;
+ }
+
+ client.blocks_stratis_blockdev = { };
+ for (path in client.stratis_blockdevs) {
+ block = client.slashdevs_block[client.stratis_blockdevs[path].PhysicalPath];
+ if (block)
+ client.blocks_stratis_blockdev[block.path] = client.stratis_blockdevs[path];
+ }
+
+ client.blocks_stratis_stopped_pool = { };
+ client.stratis_stopped_pool_key_description = { };
+ client.stratis_stopped_pool_clevis_info = { };
+ for (const uuid in client.stratis_manager.StoppedPools) {
+ const devs = client.stratis_manager.StoppedPools[uuid].devs.v;
+ for (const d of devs) {
+ block = client.slashdevs_block[d.devnode];
+ if (block)
+ client.blocks_stratis_stopped_pool[block.path] = uuid;
+ }
+ const kinfo = client.stratis_manager.StoppedPools[uuid].key_description;
+ if (kinfo &&
+ kinfo.t == "(bv)" &&
+ kinfo.v[0] &&
+ kinfo.v[1].t == "(bs)" &&
+ kinfo.v[1].v[0]) {
+ client.stratis_stopped_pool_key_description[uuid] = kinfo.v[1].v[1];
+ }
+ const cinfo = client.stratis_manager.StoppedPools[uuid].clevis_info;
+ if (cinfo &&
+ cinfo.t == "(bv)" &&
+ cinfo.v[0] &&
+ cinfo.v[1].t == "(b(ss))" &&
+ cinfo.v[1].v[0]) {
+ client.stratis_stopped_pool_clevis_info[uuid] = cinfo.v[1].v[1];
+ }
+ }
+
+ client.stratis_pool_stats = { };
+ for (path in client.stratis_pools) {
+ const pool = client.stratis_pools[path];
+ const filesystems = client.stratis_pool_filesystems[path];
+
+ const fsys_offsets = [];
+ let fsys_total_used = 0;
+ let fsys_total_size = 0;
+ filesystems.forEach(fs => {
+ fsys_offsets.push(fsys_total_used);
+ fsys_total_used += fs.Used[0] ? Number(fs.Used[1]) : 0;
+ fsys_total_size += Number(fs.Size);
+ });
+
+ const overhead = pool.TotalPhysicalUsed[0] ? (Number(pool.TotalPhysicalUsed[1]) - fsys_total_used) : 0;
+ const pool_total = Number(pool.TotalPhysicalSize) - overhead;
+ let pool_free = pool_total - fsys_total_size;
+
+ // leave some margin since the above computation does not seem to
+ // be exactly right when snapshots are involved.
+ pool_free -= filesystems.length * 1024 * 1024;
+
+ client.stratis_pool_stats[path] = {
+ fsys_offsets,
+ fsys_total_used,
+ fsys_total_size,
+ pool_total,
+ pool_free,
+ };
+ }
+
+ client.blocks_cleartext = { };
+ for (path in client.blocks) {
+ block = client.blocks[path];
+ if (block.CryptoBackingDevice != "/")
+ client.blocks_cleartext[block.CryptoBackingDevice] = block;
+ }
+
+ client.blocks_partitions = { };
+ for (path in client.blocks_ptable) {
+ client.blocks_partitions[path] = [];
+ }
+ for (path in client.blocks_part) {
+ part = client.blocks_part[path];
+ if (client.blocks_partitions[part.Table] !== undefined)
+ client.blocks_partitions[part.Table].push(part);
+ }
+ for (path in client.blocks_partitions) {
+ client.blocks_partitions[path].sort(function (a, b) { return a.Offset - b.Offset });
+ }
+
+ client.iscsi_sessions_drives = { };
+ client.drives_iscsi_session = { };
+ for (path in client.drives) {
+ const block = client.drives_block[path];
+ if (!block)
+ continue;
+ for (const session_path in client.iscsi_sessions) {
+ const session = client.iscsi_sessions[session_path];
+ for (i = 0; i < block.Symlinks.length; i++) {
+ if (utils.decode_filename(block.Symlinks[i]).includes(session.data.target_name)) {
+ client.drives_iscsi_session[path] = session;
+ if (!client.iscsi_sessions_drives[session_path])
+ client.iscsi_sessions_drives[session_path] = [];
+ client.iscsi_sessions_drives[session_path].push(client.drives[path]);
+ }
+ }
+ }
+ }
+
+ client.blocks_available = { };
+ for (path in client.blocks) {
+ block = client.blocks[path];
+ if (utils.is_available_block(client, block))
+ client.blocks_available[path] = true;
+ }
+
+ client.path_jobs = { };
+ function enter_job(job) {
+ if (!job.Objects || !job.Objects.length)
+ return;
+ job.Objects.forEach(p => {
+ if (!client.path_jobs[p])
+ client.path_jobs[p] = [];
+ client.path_jobs[p].push(job);
+ });
+ }
+ for (path in client.jobs) {
+ enter_job(client.jobs[path]);
+ }
+
+ // UDisks API does not provide a btrfs volume abstraction so we keep track of
+ // volume's by uuid in an object. uuid => [org.freedesktop.UDisks2.Filesystem.BTRFS]
+ // https://github.com/storaged-project/udisks/issues/1232
+ const old_uuids = client.uuids_btrfs_volume;
+ let need_poll = false;
+ client.uuids_btrfs_volume = { };
+ client.uuids_btrfs_blocks = { };
+ for (const p in client.blocks_fsys_btrfs) {
+ const bfs = client.blocks_fsys_btrfs[p];
+ const uuid = bfs.data.uuid;
+ const block_fsys = client.blocks_fsys[p];
+ if ((block_fsys && block_fsys.MountPoints.length > 0) || !client.uuids_btrfs_volume[uuid]) {
+ client.uuids_btrfs_volume[uuid] = bfs;
+ if (!old_uuids || !old_uuids[uuid])
+ need_poll = true;
+ }
+ if (!client.uuids_btrfs_blocks[uuid])
+ client.uuids_btrfs_blocks[uuid] = [];
+ client.uuids_btrfs_blocks[uuid].push(client.blocks[p]);
+ }
+
+ if (need_poll) {
+ btrfs_poll();
+ }
+}
+
+client.update = (first_time) => {
+ if (first_time)
+ client.ready = true;
+ if (client.ready) {
+ update_indices();
+ reset_pages();
+ make_overview_page();
+ export_mount_point_mapping();
+ client.dispatchEvent("changed");
+ }
+};
+
+function init_model(callback) {
+ function pull_time() {
+ return cockpit.spawn(["date", "+%s"])
+ .then(function (now) {
+ client.time_offset = parseInt(now, 10) * 1000 - new Date().getTime();
+ });
+ }
+
+ function enable_udisks_features() {
+ if (!client.manager.valid)
+ return Promise.resolve();
+ if (!client.manager.EnableModules)
+ return Promise.resolve();
+ return client.manager.EnableModules(true).then(
+ function() {
+ client.manager_lvm2 = proxy("Manager.LVM2", "Manager");
+ client.manager_iscsi = proxy("Manager.ISCSI.Initiator", "Manager");
+ client.manager_btrfs = proxy("Manager.BTRFS", "Manager");
+ return Promise.allSettled([client.manager_lvm2.wait(), client.manager_iscsi.wait(), client.manager_btrfs.wait()])
+ .then(() => {
+ client.features.lvm2 = client.manager_lvm2.valid;
+ client.features.iscsi = (client.manager_iscsi.valid &&
+ client.manager_iscsi.SessionsSupported !== false);
+ client.features.btrfs = client.manager_btrfs.valid;
+ if (client.features.btrfs)
+ btrfs_start_polling();
+ });
+ }, function(error) {
+ console.warn("Can't enable storaged modules", error.toString());
+ return Promise.resolve();
+ });
+ }
+
+ function enable_lvm_create_vdo_feature() {
+ return cockpit.spawn(["vdoformat", "--version"], { err: "ignore" })
+ .then(() => { client.features.lvm_create_vdo = true; return Promise.resolve() })
+ .catch(() => Promise.resolve());
+ }
+
+ function enable_legacy_vdo_features() {
+ return client.legacy_vdo_overlay.start().then(
+ function (success) {
+ // hack here
+ client.features.legacy_vdo = success;
+ return Promise.resolve();
+ },
+ function () {
+ return Promise.resolve();
+ });
+ }
+
+ function enable_clevis_features() {
+ return cockpit.script("type clevis-luks-bind", { err: "ignore" }).then(
+ function () {
+ client.features.clevis = true;
+ return Promise.resolve();
+ },
+ function () {
+ return Promise.resolve();
+ });
+ }
+
+ function enable_nfs_features() {
+ // mount.nfs might be in */sbin but that isn't always in
+ // $PATH, such as when connecting from CentOS to another
+ // machine via SSH as non-root.
+ const std_path = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
+ return cockpit.script("type mount.nfs", { err: "message", environ: [std_path] }).then(
+ function () {
+ client.features.nfs = true;
+ client.nfs.start();
+ return Promise.resolve();
+ },
+ function () {
+ return Promise.resolve();
+ });
+ }
+
+ function enable_pk_features() {
+ return PK.detect().then(function (available) { client.features.packagekit = available });
+ }
+
+ function enable_stratis_feature() {
+ return client.stratis_start().catch(error => {
+ if (error.problem != "not-found")
+ console.warn("Failed to start Stratis support", error);
+ return Promise.resolve();
+ });
+ }
+
+ function enable_features() {
+ client.features = { };
+ return (enable_udisks_features()
+ .then(enable_clevis_features)
+ .then(enable_nfs_features)
+ .then(enable_pk_features)
+ .then(enable_stratis_feature)
+ .then(enable_lvm_create_vdo_feature)
+ .then(enable_legacy_vdo_features));
+ }
+
+ function query_fsys_info() {
+ const info = {};
+ return Promise.all(client.manager.SupportedFilesystems.map(fs =>
+ client.manager.CanFormat(fs).then(canformat_result => {
+ info[fs] = {
+ can_format: canformat_result[0],
+ can_shrink: false,
+ can_grow: false
+ };
+ return client.manager.CanResize(fs)
+ .then(canresize_result => {
+ // We assume that all filesystems support
+ // offline shrinking/growing if they
+ // support shrinking or growing at all.
+ // The actual resizing utility will
+ // temporarily mount the fs if necessary,
+ if (canresize_result[0]) {
+ info[fs].can_shrink = !!(canresize_result[1] & 2);
+ info[fs].shrink_needs_unmount = !(canresize_result[1] & 8);
+ info[fs].can_grow = !!(canresize_result[1] & 4);
+ info[fs].grow_needs_unmount = !(canresize_result[1] & 16);
+ }
+ })
+ // ignore unsupported filesystems
+ .catch(() => {});
+ }))
+ ).then(() => info);
+ }
+
+ try {
+ client.anaconda = JSON.parse(window.sessionStorage.getItem("cockpit_anaconda"));
+ if (client.anaconda)
+ console.log("ANACONDA", client.anaconda);
+ } catch {
+ console.warn("Can't parse cockpit_anaconda configuration as JSON");
+ client.anaconda = null;
+ }
+
+ pull_time().then(() => {
+ read_os_release().then(os_release => {
+ client.os_release = os_release;
+
+ enable_features().then(() => {
+ query_fsys_info().then((fsys_info) => {
+ client.fsys_info = fsys_info;
+
+ client.storaged_client.addEventListener('notify', () => client.update());
+
+ client.update(true);
+ callback();
+ });
+ });
+ });
+ });
+}
+
+client.older_than = function older_than(version) {
+ return utils.compare_versions(this.manager.Version, version) < 0;
+};
+
+/* Mount users
+ */
+
+client.find_mount_users = (target, is_mounted) => {
+ if (is_mounted === undefined || is_mounted)
+ return python.spawn(mount_users_py, ["users", target], { superuser: "try", err: "message" }).then(JSON.parse);
+ else
+ return Promise.resolve([]);
+};
+
+client.stop_mount_users = (users) => {
+ if (users && users.length > 0) {
+ return python.spawn(mount_users_py, ["stop", JSON.stringify(users)],
+ { superuser: "try", err: "message" });
+ } else
+ return Promise.resolve();
+};
+
+/* Direct mounting and unmounting
+ *
+ * We don't use UDisks2 for most of our mounting and unmounting in
+ * order to get better control over which entry from fstab is
+ * selected. Once UDisks2 allows that control, we can switch back to
+ * it.
+ *
+ * But note that these functions still require an fstab entry.
+ */
+
+client.mount_at = (block, target) => {
+ const entry = block.Configuration.find(c => c[0] == "fstab" && utils.decode_filename(c[1].dir.v) == target);
+ if (entry)
+ return cockpit.script('set -e; mkdir -p "$2"; mount "$1" "$2" -o "$3"',
+ [utils.decode_filename(block.Device), target, utils.get_block_mntopts(entry[1])],
+ { superuser: true, err: "message" });
+ else
+ return Promise.reject(cockpit.format("Internal error: No fstab entry for $0 and $1",
+ utils.decode_filename(block.Device),
+ target));
+};
+
+client.unmount_at = (target, users) => {
+ return client.stop_mount_users(users).then(() => cockpit.spawn(["umount", target],
+ { superuser: true, err: "message" }));
+};
+
+/* NFS mounts
+ */
+
+function nfs_mounts() {
+ const self = {
+ entries: [],
+ fsys_sizes: { },
+
+ start,
+
+ get_fsys_size,
+ entry_users,
+
+ update_entry,
+ add_entry,
+ remove_entry,
+
+ mount_entry,
+ unmount_entry,
+ stop_and_unmount_entry,
+ stop_and_remove_entry,
+
+ find_entry
+ };
+
+ function spawn_nfs_mounts(args) {
+ return python.spawn([inotify_py, nfs_mounts_py], args, { superuser: "try", err: "message" });
+ }
+
+ function start() {
+ let buf = "";
+ spawn_nfs_mounts(["monitor"])
+ .stream(function (output) {
+ buf += output;
+ const lines = buf.split("\n");
+ buf = lines[lines.length - 1];
+ if (lines.length >= 2) {
+ self.entries = JSON.parse(lines[lines.length - 2]);
+ self.fsys_sizes = { };
+ client.update();
+ }
+ })
+ .catch(function (error) {
+ if (error != "closed") {
+ console.warn(error);
+ }
+ });
+ }
+
+ function get_fsys_size(entry) {
+ const path = entry.fields[1];
+ if (self.fsys_sizes[path])
+ return self.fsys_sizes[path];
+
+ if (self.fsys_sizes[path] === false)
+ return null;
+
+ self.fsys_sizes[path] = false;
+ cockpit.spawn(["stat", "-f", "-c", "[ %S, %f, %b ]", path], { err: "message" })
+ .then(function (output) {
+ const data = JSON.parse(output);
+ self.fsys_sizes[path] = [(data[2] - data[1]) * data[0], data[2] * data[0]];
+ client.update();
+ })
+ .catch(function () {
+ self.fsys_sizes[path] = [0, 0];
+ client.update();
+ });
+
+ return null;
+ }
+
+ function update_entry(entry, new_fields) {
+ return spawn_nfs_mounts(["update", JSON.stringify(entry), JSON.stringify(new_fields)]);
+ }
+
+ function add_entry(fields) {
+ return spawn_nfs_mounts(["add", JSON.stringify(fields)]);
+ }
+
+ function remove_entry(entry) {
+ return spawn_nfs_mounts(["remove", JSON.stringify(entry)]);
+ }
+
+ function mount_entry(entry) {
+ return spawn_nfs_mounts(["mount", JSON.stringify(entry)]);
+ }
+
+ function unmount_entry(entry) {
+ return spawn_nfs_mounts(["unmount", JSON.stringify(entry)]);
+ }
+
+ function stop_and_unmount_entry(users, entry) {
+ return client.stop_mount_users(users).then(() => unmount_entry(entry));
+ }
+
+ function stop_and_remove_entry(users, entry) {
+ return client.stop_mount_users(users).then(() => remove_entry(entry));
+ }
+
+ function entry_users(entry) {
+ return client.find_mount_users(entry.fields[1], entry.mounted);
+ }
+
+ function find_entry(remote, local) {
+ for (let i = 0; i < self.entries.length; i++) {
+ if (self.entries[i].fields[0] == remote && self.entries[i].fields[1] == local)
+ return self.entries[i];
+ }
+ }
+
+ return self;
+}
+
+client.nfs = nfs_mounts();
+
+/* Legacy VDO CLI (RHEL 8), unsupported; newer versions use VDO through LVM API */
+
+function legacy_vdo_overlay() {
+ const self = {
+ start,
+
+ volumes: [],
+
+ by_name: { },
+ by_dev: { },
+ by_backing_dev: { },
+
+ find_by_block,
+ find_by_backing_block,
+
+ create
+ };
+
+ function cmd(args) {
+ return cockpit.spawn(["vdo"].concat(args),
+ {
+ superuser: true,
+ err: "message"
+ });
+ }
+
+ function update(data) {
+ self.by_name = { };
+ self.by_dev = { };
+ self.by_backing_dev = { };
+
+ self.volumes = data.map(function (vol, index) {
+ const name = vol.name;
+
+ function volcmd(args) {
+ return cmd(args.concat(["--name", name]));
+ }
+
+ const v = {
+ name,
+ broken: vol.broken,
+ dev: "/dev/mapper/" + name,
+ backing_dev: vol.device,
+ logical_size: vol.logical_size,
+ physical_size: vol.physical_size,
+ index_mem: vol.index_mem,
+ compression: vol.compression,
+ deduplication: vol.deduplication,
+ activated: vol.activated,
+
+ set_compression: function(val) {
+ return volcmd([val ? "enableCompression" : "disableCompression"]);
+ },
+
+ set_deduplication: function(val) {
+ return volcmd([val ? "enableDeduplication" : "disableDeduplication"]);
+ },
+
+ set_activate: function(val) {
+ return volcmd([val ? "activate" : "deactivate"]);
+ },
+
+ start: function() {
+ return volcmd(["start"]);
+ },
+
+ stop: function() {
+ return volcmd(["stop"]);
+ },
+
+ remove: function() {
+ return volcmd(["remove"]);
+ },
+
+ force_remove: function() {
+ return volcmd(["remove", "--force"]);
+ },
+
+ grow_physical: function() {
+ return volcmd(["growPhysical"]);
+ },
+
+ grow_logical: function(lsize) {
+ return volcmd(["growLogical", "--vdoLogicalSize", lsize + "B"]);
+ }
+ };
+
+ self.by_name[v.name] = v;
+ self.by_dev[v.dev] = v;
+ self.by_backing_dev[v.backing_dev] = v;
+
+ return v;
+ });
+
+ client.update();
+ }
+
+ function start() {
+ let buf = "";
+
+ return cockpit.spawn(["/bin/sh", "-c", "head -1 $(command -v vdo || echo /dev/null)"],
+ { err: "ignore" })
+ .then(function (shebang) {
+ if (shebang != "") {
+ self.python = shebang.replace(/#! */, "").trim("\n");
+ cockpit.spawn([self.python, "--", "-"], { superuser: "try", err: "message" })
+ .input(inotify_py + vdo_monitor_py)
+ .stream(function (output) {
+ buf += output;
+ const lines = buf.split("\n");
+ buf = lines[lines.length - 1];
+ if (lines.length >= 2) {
+ update(JSON.parse(lines[lines.length - 2]));
+ }
+ })
+ .catch(function (error) {
+ if (error != "closed") {
+ console.warn(error);
+ }
+ });
+ return true;
+ } else {
+ return false;
+ }
+ });
+ }
+
+ function some(array, func) {
+ let i;
+ for (i = 0; i < array.length; i++) {
+ const val = func(array[i]);
+ if (val)
+ return val;
+ }
+ return null;
+ }
+
+ function find_by_block(block) {
+ function check(encoded) { return self.by_dev[utils.decode_filename(encoded)] }
+ return check(block.Device) || some(block.Symlinks, check);
+ }
+
+ function find_by_backing_block(block) {
+ function check(encoded) { return self.by_backing_dev[utils.decode_filename(encoded)] }
+ return check(block.Device) || some(block.Symlinks, check);
+ }
+
+ function create(options) {
+ const args = ["create", "--name", options.name,
+ "--device", utils.decode_filename(options.block.PreferredDevice)];
+ if (options.logical_size !== undefined)
+ args.push("--vdoLogicalSize", options.logical_size + "B");
+ if (options.index_mem !== undefined)
+ args.push("--indexMem", options.index_mem / (1024 * 1024 * 1024));
+ if (options.compression !== undefined)
+ args.push("--compression", options.compression ? "enabled" : "disabled");
+ if (options.deduplication !== undefined)
+ args.push("--deduplication", options.deduplication ? "enabled" : "disabled");
+ if (options.emulate_512 !== undefined)
+ args.push("--emulate512", options.emulate_512 ? "enabled" : "disabled");
+ return cmd(args);
+ }
+
+ return self;
+}
+
+client.legacy_vdo_overlay = legacy_vdo_overlay();
+
+/* Stratis */
+
+client.stratis_start = () => {
+ return stratis2_start()
+ .catch(error => {
+ if (error.problem == "not-found")
+ return stratis3_start();
+ return Promise.reject(error);
+ });
+};
+
+// We need to use the same revision for all interfaces, mixing them is
+// not allowed. If we need to bump it, it should be bumped here for all
+// of them at the same time.
+//
+const stratis3_interface_revision = "r5";
+
+function stratis3_start() {
+ const stratis = cockpit.dbus("org.storage.stratis3", { superuser: "try" });
+ client.stratis_manager = stratis.proxy("org.storage.stratis3.Manager." + stratis3_interface_revision,
+ "/org/storage/stratis3");
+
+ // The rest of the code expects these to be initialized even if no
+ // stratisd is found.
+ client.stratis_pools = { };
+ client.stratis_blockdevs = { };
+ client.stratis_filesystems = { };
+ client.stratis_manager.StoppedPools = {};
+
+ return client.stratis_manager.wait()
+ .then(() => {
+ client.stratis_store_passphrase = (desc, passphrase) => {
+ return python.spawn(stratis3_set_key_py, [desc], { superuser: true })
+ .input(passphrase);
+ };
+
+ client.stratis_start_pool = (uuid, unlock_method) => {
+ return client.stratis_manager.StartPool(uuid, "uuid", [!!unlock_method, unlock_method || ""]);
+ };
+
+ client.stratis_create_pool = (name, devs, key_desc, clevis_info) => {
+ return client.stratis_manager.CreatePool(name,
+ devs,
+ key_desc ? [true, key_desc] : [false, ""],
+ clevis_info ? [true, clevis_info] : [false, ["", ""]]);
+ };
+
+ client.stratis_set_overprovisioning = (pool, flag) => {
+ // DBusProxy is smart enough to allow
+ // "pool.Overprovisioning = flag" to just work,
+ // but we want to catch any error ourselves, and
+ // we want to wait for the method call to
+ // complete.
+ return stratis.call(pool.path, "org.freedesktop.DBus.Properties", "Set",
+ ["org.storage.stratis3.pool." + stratis3_interface_revision,
+ "Overprovisioning",
+ cockpit.variant("b", flag)
+ ]);
+ };
+
+ client.stratis_list_keys = () => {
+ return client.stratis_manager.ListKeys();
+ };
+
+ client.stratis_create_filesystem = (pool, name, size) => {
+ return pool.CreateFilesystems([[name, size ? [true, size.toString()] : [false, ""]]]);
+ };
+
+ client.features.stratis = true;
+ client.features.stratis_crypto_binding = true;
+ client.features.stratis_encrypted_caches = true;
+ client.features.stratis_managed_fsys_sizes = true;
+ client.features.stratis_grow_blockdevs = true;
+ client.stratis_pools = client.stratis_manager.client.proxies("org.storage.stratis3.pool." +
+ stratis3_interface_revision,
+ "/org/storage/stratis3",
+ { watch: false });
+ client.stratis_blockdevs = client.stratis_manager.client.proxies("org.storage.stratis3.blockdev." +
+ stratis3_interface_revision,
+ "/org/storage/stratis3",
+ { watch: false });
+ client.stratis_filesystems = client.stratis_manager.client.proxies("org.storage.stratis3.filesystem." +
+ stratis3_interface_revision,
+ "/org/storage/stratis3",
+ { watch: false });
+
+ return stratis.watch({ path_namespace: "/org/storage/stratis3" }).then(() => {
+ client.stratis_manager.client.addEventListener('notify', (event, data) => {
+ client.update();
+ });
+ });
+ });
+}
+
+function stratis2_start() {
+ const stratis = cockpit.dbus("org.storage.stratis2", { superuser: "try" });
+ client.stratis_manager = stratis.proxy("org.storage.stratis2.Manager.r1",
+ "/org/storage/stratis2");
+
+ // The rest of the code expects these to be initialized even if no
+ // stratisd is found.
+ client.stratis_pools = { };
+ client.stratis_blockdevs = { };
+ client.stratis_filesystems = { };
+ client.stratis_manager.StoppedPools = {};
+
+ return client.stratis_manager.wait()
+ .then(() => {
+ if (utils.compare_versions(client.stratis_manager.Version, "2.4") < 0)
+ return Promise.reject(new Error("stratisd too old, need at least version 2.4"));
+
+ client.stratis_store_passphrase = (desc, passphrase) => {
+ return python.spawn(stratis2_set_key_py, [desc], { superuser: true })
+ .input(passphrase);
+ };
+
+ client.stratis_start_pool = (uuid) => {
+ return client.stratis_manager.UnlockPool(uuid);
+ };
+
+ client.stratis_create_pool = (name, devs, key_desc) => {
+ return client.stratis_manager.CreatePool(name, [false, 0],
+ devs,
+ key_desc ? [true, key_desc] : [false, ""]);
+ };
+
+ client.stratis_create_filesystem = (pool, name) => {
+ return pool.CreateFilesystems([name]);
+ };
+
+ client.stratis_list_keys = () => {
+ return client.stratis_manager.client.call(client.stratis_manager.path,
+ "org.storage.stratis2.FetchProperties.r2",
+ "GetProperties", [["KeyList"]])
+ .then(([result]) => {
+ if (result.KeyList && result.KeyList[0])
+ return result.KeyList[1].v;
+ else
+ return [];
+ });
+ };
+
+ client.features.stratis = true;
+ client.stratis_pools = client.stratis_manager.client.proxies("org.storage.stratis2.pool.r1",
+ "/org/storage/stratis2",
+ { watch: false });
+ client.stratis_blockdevs = client.stratis_manager.client.proxies("org.storage.stratis2.blockdev.r2",
+ "/org/storage/stratis2",
+ { watch: false });
+ client.stratis_filesystems = client.stratis_manager.client.proxies("org.storage.stratis2.filesystem",
+ "/org/storage/stratis2",
+ { watch: false });
+
+ return stratis.watch({ path_namespace: "/org/storage/stratis2" }).then(() => {
+ client.stratis_manager.client.addEventListener('notify', (event, data) => {
+ client.update();
+ stratis2_fixup_pool_notifications(data);
+ });
+
+ // We need to explicitly retrieve the values of
+ // the "FetchProperties". We do this whenever a
+ // new object appears, when something happens that
+ // might have changed them (for example when a
+ // filesystem has been added to a pool we fetch
+ // the properties of the pool), and also every 30
+ // seconds, just to be safe.
+ //
+ // See https://github.com/stratis-storage/stratisd/issues/2148
+
+ client.stratis_pools.addEventListener('added', (event, proxy) => {
+ stratis2_fetch_pool_properties(proxy);
+ // A entry might have disappeared from LockedPoolsWithDevs
+ stratis2_fetch_manager_properties(client.stratis_manager);
+ });
+
+ client.stratis_blockdevs.addEventListener('added', (event, proxy) => {
+ stratis2_fetch_pool_properties_by_path(proxy.Pool);
+ stratis2_fetch_blockdev_properties(proxy);
+ });
+
+ client.stratis_blockdevs.addEventListener('removed', (event, proxy) => {
+ stratis2_fetch_pool_properties_by_path(proxy.Pool);
+ });
+
+ client.stratis_filesystems.addEventListener('added', (event, proxy) => {
+ stratis2_fetch_filesystem_properties(proxy);
+ stratis2_fetch_pool_properties_by_path(proxy.Pool);
+ });
+
+ client.stratis_filesystems.addEventListener('removed', (event, proxy) => {
+ stratis2_fetch_pool_properties_by_path(proxy.Pool);
+ });
+
+ stratis2_start_polling();
+ return true;
+ });
+ })
+ .catch(err => {
+ if (err.problem == "not-found")
+ err.message = "The name org.storage.stratis2 can not be activated on D-Bus.";
+ return Promise.reject(err);
+ });
+}
+
+function stratis2_fetch_properties(proxy, props) {
+ const stratis = client.stratis_manager.client;
+
+ return stratis.call(proxy.path, "org.storage.stratis2.FetchProperties.r4", "GetProperties", [props])
+ .then(([result]) => {
+ const values = { };
+ for (const p of props) {
+ if (result[p] && result[p][0])
+ values[p] = result[p][1].v;
+ }
+ return values;
+ })
+ .catch(error => {
+ console.warn("Failed to fetch properties:", proxy.path, props, error);
+ return { };
+ });
+}
+
+function stratis2_fetch_manager_properties(proxy) {
+ stratis2_fetch_properties(proxy, ["LockedPoolsWithDevs"]).then(values => {
+ if (values.LockedPoolsWithDevs) {
+ proxy.StoppedPools = { };
+ for (const uuid in values.LockedPoolsWithDevs) {
+ const l = values.LockedPoolsWithDevs[uuid];
+ proxy.StoppedPools[uuid] = {
+ devs: l.devs,
+ key_description: { t: "(bv)", v: [true, l.key_description] },
+ };
+ }
+ client.update();
+ }
+ });
+}
+
+function stratis2_fetch_pool_properties(proxy) {
+ if (!proxy.TotalPhysicalUsed)
+ proxy.TotalPhysicalUsed = [false, ""];
+ if (!proxy.KeyDescription)
+ proxy.KeyDescription = [false, ""];
+ stratis2_fetch_properties(proxy, ["TotalPhysicalSize", "TotalPhysicalUsed", "KeyDescription"]).then(values => {
+ if (values.TotalPhysicalSize)
+ proxy.TotalPhysicalSize = values.TotalPhysicalSize;
+ if (values.TotalPhysicalUsed)
+ proxy.TotalPhysicalUsed = [true, values.TotalPhysicalUsed];
+ if (values.KeyDescription)
+ proxy.KeyDescription = [true, values.KeyDescription];
+ client.update();
+ });
+}
+
+function stratis2_fetch_pool_properties_by_path(path) {
+ const pool = client.stratis_pools[path];
+ if (pool)
+ stratis2_fetch_pool_properties(pool);
+}
+
+function stratis2_fetch_filesystem_properties(proxy) {
+ if (!proxy.Used)
+ proxy.Used = [false, ""];
+ stratis2_fetch_properties(proxy, ["Used"]).then(values => {
+ if (values.Used)
+ proxy.Used = [true, values.Used];
+ });
+}
+
+function stratis2_fetch_blockdev_properties(proxy) {
+ stratis2_fetch_properties(proxy, ["TotalPhysicalSize"]).then(values => {
+ if (values.TotalPhysicalSize)
+ proxy.TotalPhysicalSize = values.TotalPhysicalSize;
+ client.update();
+ });
+}
+
+function stratis2_poll() {
+ stratis2_fetch_manager_properties(client.stratis_manager);
+
+ for (const path in client.stratis_pools)
+ stratis2_fetch_pool_properties(client.stratis_pools[path]);
+
+ for (const path in client.stratis_filesystems)
+ stratis2_fetch_filesystem_properties(client.stratis_filesystems[path]);
+
+ for (const path in client.stratis_blockdevs)
+ stratis2_fetch_blockdev_properties(client.stratis_blockdevs[path]);
+}
+
+function stratis2_start_polling() {
+ stratis2_poll();
+ window.setInterval(stratis2_poll, 30000);
+}
+
+function stratis2_fixup_pool_notifications(data) {
+ const fixup_data = { };
+ let have_fixup = false;
+
+ // When renaming a pool, stratisd 2.4.2 sends out notifications
+ // with wrong interface names and forgets about notifications for
+ // Devnode properties.
+ //
+ // https://github.com/stratis-storage/stratisd/issues/2731
+
+ for (const path in data) {
+ if (client.stratis_pools[path]) {
+ for (const iface in data[path]) {
+ if (iface == "org.storage.stratis2.filesystem") {
+ const props = data[path][iface];
+ if (props && props.Name) {
+ // The pool at 'path' got renamed.
+ fixup_data[path] = { "org.storage.stratis2.pool.r1": { Name: props.Name } };
+ for (const fsys of client.stratis_pool_filesystems[path]) {
+ fixup_data[fsys.path] = {
+ "org.storage.stratis2.filesystem": {
+ Devnode: "/dev/stratis/" + props.Name + "/" + fsys.Name
+ }
+ };
+ }
+ have_fixup = true;
+ }
+ }
+ }
+ }
+ }
+
+ if (have_fixup) {
+ const stratis = client.stratis_manager.client;
+ stratis.notify(fixup_data);
+ }
+}
+
+function init_client(manager, callback) {
+ if (client.manager)
+ return;
+
+ client.storaged_client = manager.client;
+ client.manager = manager;
+
+ init_proxies().then(() => init_model(callback));
+}
+
+client.init = function init_storaged(callback) {
+ const udisks = cockpit.dbus("org.freedesktop.UDisks2", { superuser: "try" });
+ const udisks_manager = udisks.proxy("org.freedesktop.UDisks2.Manager",
+ "/org/freedesktop/UDisks2/Manager", { watch: true });
+
+ udisks_manager.wait().then(() => init_client(udisks_manager, callback))
+ .catch(ex => {
+ console.warn("client.init(): udisks manager proxy failed:", JSON.stringify(ex));
+ client.features = false;
+ callback();
+ });
+
+ udisks_manager.addEventListener("changed", () => init_client(udisks_manager, callback));
+};
+
+client.wait_for = function wait_for(cond) {
+ return new Promise(resolve => {
+ function check() {
+ const res = cond();
+ if (res) {
+ client.removeEventListener("changed", check);
+ resolve(res);
+ }
+ }
+
+ client.addEventListener("changed", check);
+ check();
+ });
+};
+
+function try_fields(dict, fields, def) {
+ for (let i = 0; i < fields.length; i++)
+ if (fields[i] && dict[fields[i]])
+ return dict[fields[i]];
+ return def;
+}
+
+client.get_config = (name, def) => {
+ if (cockpit.manifests.storage && cockpit.manifests.storage.config) {
+ const val = cockpit.manifests.storage.config[name];
+ if (typeof val === 'object' && val !== null)
+ return try_fields(val, [client.os_release.PLATFORM_ID, client.os_release.ID], def);
+ else
+ return val !== undefined ? val : def;
+ } else {
+ return def;
+ }
+};
+
+client.in_anaconda_mode = () => !!client.anaconda;
+
+client.strip_mount_point_prefix = (dir) => {
+ const mpp = client.anaconda?.mount_point_prefix;
+
+ if (dir && mpp) {
+ if (dir.indexOf(mpp) != 0)
+ return false;
+
+ dir = dir.substr(mpp.length);
+ if (dir == "")
+ dir = "/";
+ }
+
+ return dir;
+};
+
+client.add_mount_point_prefix = (dir) => {
+ const mpp = client.anaconda?.mount_point_prefix;
+ if (mpp && dir != "") {
+ if (dir == "/")
+ dir = mpp;
+ else
+ dir = mpp + dir;
+ }
+ return dir;
+};
+
+client.should_ignore_device = (devname) => {
+ return client.anaconda?.available_devices && client.anaconda.available_devices.indexOf(devname) == -1;
+};
+
+client.should_ignore_block = (block) => {
+ return client.should_ignore_device(utils.decode_filename(block.PreferredDevice));
+};
+
+export default client;
diff --git a/pkg/storaged/crypto/actions.jsx b/pkg/storaged/crypto/actions.jsx
new file mode 100644
index 0000000..d6139c8
--- /dev/null
+++ b/pkg/storaged/crypto/actions.jsx
@@ -0,0 +1,76 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import client from "../client";
+
+import { get_existing_passphrase, unlock_with_type } from "./keyslots.jsx";
+import { set_crypto_auto_option } from "../utils.js";
+import { dialog_open, PassInput } from "../dialog.jsx";
+import { remember_passphrase } from "../anaconda.jsx";
+
+const _ = cockpit.gettext;
+
+export function unlock(block) {
+ const crypto = client.blocks_crypto[block.path];
+ if (!crypto)
+ return;
+
+ function unlock_with_passphrase() {
+ const crypto = client.blocks_crypto[block.path];
+ if (!crypto)
+ return;
+
+ dialog_open({
+ Title: _("Unlock"),
+ Fields: [
+ PassInput("passphrase", _("Passphrase"), {})
+ ],
+ Action: {
+ Title: _("Unlock"),
+ action: async function (vals) {
+ await crypto.Unlock(vals.passphrase, {});
+ remember_passphrase(block, vals.passphrase);
+ await set_crypto_auto_option(block, true);
+ }
+ }
+ });
+ }
+
+ return get_existing_passphrase(block, true).then(type => {
+ return (unlock_with_type(client, block, null, type)
+ .then(() => set_crypto_auto_option(block, true))
+ .catch(() => unlock_with_passphrase()));
+ });
+}
+
+export function lock(block) {
+ const crypto = client.blocks_crypto[block.path];
+ if (!crypto)
+ return;
+
+ return crypto.Lock({}).then(() => set_crypto_auto_option(block, false));
+}
+
+export function std_lock_action(backing_block, content_block) {
+ if (backing_block == content_block)
+ return null;
+
+ return { title: _("Lock"), action: () => lock(backing_block) };
+}
diff --git a/pkg/storaged/crypto/clevis-luks-passphrase.sh b/pkg/storaged/crypto/clevis-luks-passphrase.sh
new file mode 100755
index 0000000..ad41a2e
--- /dev/null
+++ b/pkg/storaged/crypto/clevis-luks-passphrase.sh
@@ -0,0 +1,65 @@
+#! /bin/sh
+
+set -eu
+
+# clevis-luks-passphrase [--type] DEV
+#
+# Try to recover a passphrase via clevis that will open DEV, and
+# output it on stdout. Such a passphrase can be used for things like
+# resizing of LUKSv2 containers, or adding/changing keys.
+#
+# If "--type" is given, this tool writes just "clevis" to stdout when
+# a passphrase is found, instead of the actual passphrase. This is
+# useful to limit exposure of the passphrase when all you want to know
+# is whether "clevis luks unlock" can be expected to succeed.
+
+opt_type=no
+if [ $# -gt 1 ] && [ "$1" = "--type" ]; then
+ opt_type=yes
+ shift
+fi
+
+if [ $# -ne 1 ]; then
+ echo "usage: $0 [--type] DEV" >&2
+ exit 1
+fi
+
+DEV="$1"
+
+if cryptsetup isLuks --type luks1 "$DEV"; then
+
+ # The UUID that clevis uses for its luksmeta slots,
+ # see for example /usr/bin/clevis-luks-bind
+ #
+ CLEVIS_UUID=cb6e8904-81ff-40da-a84a-07ab9ab5715e
+
+ luksmeta test -d "$DEV" 2>/dev/null || exit 0
+
+ luksmeta show -d "$DEV" | while read slot state uuid; do
+ if [ "$state" = "active" ] && [ "$uuid" = "$CLEVIS_UUID" ]; then
+ if pp=$(luksmeta load -d "$DEV" -s "$slot" | clevis decrypt); then
+ if [ "$opt_type" = yes ]; then
+ echo clevis
+ else
+ printf '%s\n' "$pp"
+ fi
+ break
+ fi
+ fi
+ done
+
+elif cryptsetup isLuks --type luks2 "$DEV"; then
+ for id in `cryptsetup luksDump "$DEV" | sed -rn 's|^\s+([0-9]+): clevis|\1|p'`; do
+ tok=`cryptsetup token export --token-id "$id" "$DEV"`
+ jwe=`printf '%s\n' "$tok" | jose fmt -j- -Og jwe -o- | jose jwe fmt -i- -c`
+
+ if pt=`printf '%s' "$jwe" | clevis decrypt`; then
+ if [ "$opt_type" = yes ]; then
+ echo clevis
+ else
+ printf '%s\n' "$pt"
+ fi
+ break
+ fi
+ done
+fi
diff --git a/pkg/storaged/crypto/encryption.jsx b/pkg/storaged/crypto/encryption.jsx
new file mode 100644
index 0000000..4245de3
--- /dev/null
+++ b/pkg/storaged/crypto/encryption.jsx
@@ -0,0 +1,243 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { useObject, useEvent } from "hooks";
+import * as python from "python.js";
+import * as timeformat from "timeformat.js";
+
+import { dialog_open, TextInput, PassInput } from "../dialog.jsx";
+import { block_name, encode_filename, decode_filename, parse_options, unparse_options, extract_option, edit_crypto_config } from "../utils.js";
+import { StorageCard, StorageDescription, new_card } from "../pages.jsx";
+import luksmeta_monitor_hack_py from "./luksmeta-monitor-hack.py";
+import { is_mounted } from "../filesystem/utils.jsx";
+import { StorageLink } from "../storage-controls.jsx";
+import { CryptoKeyslots } from "./keyslots.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_encryption_card(next, block) {
+ return new_card({
+ title: _("Encryption"),
+ next,
+ type_extra: _("encrypted"),
+ component: EncryptionCard,
+ props: { block },
+ });
+}
+
+function monitor_luks(block) {
+ const self = {
+ stop,
+
+ luks_version: null,
+ slots: null,
+ slot_error: null,
+ max_slots: null,
+ };
+
+ cockpit.event_target(self);
+
+ const dev = decode_filename(block.Device);
+ const channel = python.spawn(luksmeta_monitor_hack_py, [dev], { superuser: true });
+ let buf = "";
+
+ channel.stream(output => {
+ buf += output;
+ const lines = buf.split("\n");
+ buf = lines[lines.length - 1];
+ if (lines.length >= 2) {
+ const data = JSON.parse(lines[lines.length - 2]);
+ self.slots = data.slots;
+ self.luks_version = data.version;
+ self.max_slots = data.max_slots;
+ self.dispatchEvent("changed");
+ }
+ });
+
+ channel.catch(err => {
+ self.slots = [];
+ self.slot_error = err;
+ self.dispatchEvent("changed");
+ });
+
+ function stop() {
+ channel.close();
+ }
+
+ return self;
+}
+
+function parse_tag_mtime(tag) {
+ if (tag && tag.indexOf("1:") == 0) {
+ try {
+ const parts = tag.split("-")[1].split(".");
+ // s:ns → ms
+ const mtime = parseInt(parts[0]) * 1000 + parseInt(parts[1]) * 1e-6;
+ return cockpit.format(_("Last modified: $0"), timeformat.dateTime(mtime));
+ } catch {
+ return null;
+ }
+ } else
+ return null;
+}
+
+function monitor_mtime(path) {
+ const self = {
+ stop,
+
+ mtime: 0
+ };
+
+ cockpit.event_target(self);
+
+ let file = null;
+ if (path) {
+ file = cockpit.file(path, { superuser: true });
+ file.watch((_, tag) => { self.mtime = parse_tag_mtime(tag); self.dispatchEvent("changed") },
+ { read: false });
+ }
+
+ function stop() {
+ if (file)
+ file.close();
+ }
+
+ return self;
+}
+
+const EncryptionCard = ({ card, block }) => {
+ const luks_info = useObject(() => monitor_luks(block),
+ m => m.stop(),
+ [block]);
+ useEvent(luks_info, "changed");
+
+ let old_options, passphrase_path;
+ const old_config = block.Configuration.find(c => c[0] == "crypttab");
+ if (old_config) {
+ old_options = (decode_filename(old_config[1].options.v)
+ .split(",")
+ .filter(function (s) { return s.indexOf("x-parent") !== 0 })
+ .join(","));
+ passphrase_path = decode_filename(old_config[1]["passphrase-path"].v);
+ }
+
+ const stored_passphrase_info = useObject(() => monitor_mtime(passphrase_path),
+ m => m.stop(),
+ [passphrase_path]);
+ useEvent(stored_passphrase_info, "changed");
+
+ const split_options = parse_options(old_options);
+ let opt_noauto = extract_option(split_options, "noauto");
+ const extra_options = unparse_options(split_options);
+
+ function edit_stored_passphrase() {
+ edit_crypto_config(block, function (config, commit) {
+ dialog_open({
+ Title: _("Stored passphrase"),
+ Fields: [
+ PassInput("passphrase", _("Stored passphrase"),
+ {
+ value: (config && config['passphrase-contents']
+ ? decode_filename(config['passphrase-contents'].v)
+ : "")
+ })
+ ],
+ Action: {
+ Title: _("Save"),
+ action: function (vals) {
+ config["passphrase-contents"] = {
+ t: 'ay',
+ v: encode_filename(vals.passphrase)
+ };
+ delete config["passphrase-path"];
+ return commit();
+ }
+ }
+ });
+ });
+ }
+
+ function edit_options() {
+ const fsys_config = client.blocks_crypto[block.path]?.ChildConfiguration.find(c => c[0] == "fstab");
+ const content_block = client.blocks_cleartext[block.path];
+ const is_fsys = fsys_config || (content_block && content_block.IdUsage == "filesystem");
+
+ edit_crypto_config(block, function (config, commit) {
+ dialog_open({
+ Title: _("Encryption options"),
+ Fields: [
+ TextInput("options", "", { value: extra_options }),
+ ],
+ isFormHorizontal: false,
+ Action: {
+ Title: _("Save"),
+ action: function (vals) {
+ let opts = [];
+ if (is_fsys && content_block)
+ opt_noauto = !is_mounted(client, content_block);
+ if (opt_noauto)
+ opts.push("noauto");
+ opts = opts.concat(parse_options(vals.options));
+ config.options = {
+ t: 'ay',
+ v: encode_filename(unparse_options(opts))
+ };
+ return commit();
+ }
+ }
+ });
+ });
+ }
+
+ const cleartext = client.blocks_cleartext[block.path];
+
+ const option_parts = [];
+ if (extra_options)
+ option_parts.push(extra_options);
+ const options = option_parts.join(", ");
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm wide-label">
+ <StorageDescription title={_("Encryption type")}>
+ { luks_info.luks_version ? "LUKS" + luks_info.luks_version : "-" }
+ </StorageDescription>
+ <StorageDescription title={_("Cleartext device")}>
+ {cleartext ? block_name(cleartext) : "-"}
+ </StorageDescription>
+ <StorageDescription title={_("Stored passphrase")}
+ value={ passphrase_path ? stored_passphrase_info.mtime || _("yes") : _("none") }
+ action={<StorageLink onClick={edit_stored_passphrase}>{_("edit")}</StorageLink>} />
+ <StorageDescription title={_("Options")}
+ value={options || _("none")}
+ action={<StorageLink onClick={edit_options}>{_("edit")}</StorageLink>} />
+ </DescriptionList>
+ </CardBody>
+ <CryptoKeyslots client={client} block={block}
+ slots={luks_info.slots} slot_error={luks_info.slot_error}
+ max_slots={luks_info.max_slots} />
+ </StorageCard>);
+};
diff --git a/pkg/storaged/crypto/keyslots.jsx b/pkg/storaged/crypto/keyslots.jsx
new file mode 100644
index 0000000..9a6962d
--- /dev/null
+++ b/pkg/storaged/crypto/keyslots.jsx
@@ -0,0 +1,797 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { DataList, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
+import { Text, TextContent, TextList, TextListItem, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+import { TextInput as TextInputPF } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { EditIcon, MinusIcon, PlusIcon } from "@patternfly/react-icons";
+import { EmptyState, EmptyStateBody } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
+
+import { check_missing_packages, install_missing_packages, Enum as PkEnum } from "packagekit";
+import { fmt_to_fragments } from "utils.jsx";
+import { remember_passphrase } from "../anaconda.jsx";
+
+import {
+ dialog_open,
+ SelectOneRadio, TextInput, PassInput, Skip
+} from "../dialog.jsx";
+import { decode_filename, encode_filename, get_block_mntopts, block_name, for_each_async, get_children, parse_options, unparse_options, edit_crypto_config } from "../utils.js";
+import { StorageButton } from "../storage-controls.jsx";
+
+import clevis_luks_passphrase_sh from "./clevis-luks-passphrase.sh";
+import { validate_url, get_tang_adv, TangKeyVerification } from "./tang.jsx";
+
+const _ = cockpit.gettext;
+
+/* Clevis operations
+ */
+
+function clevis_add(block, pin, cfg, passphrase) {
+ const dev = decode_filename(block.Device);
+ return cockpit.spawn(["clevis", "luks", "bind", "-f", "-k", "-", "-d", dev, pin, JSON.stringify(cfg)],
+ { superuser: true, err: "message" }).input(passphrase);
+}
+
+function clevis_remove(block, key) {
+ // clevis-luks-unbind needs a tty on stdin for some reason.
+ return cockpit.spawn(["clevis", "luks", "unbind", "-d", decode_filename(block.Device), "-s", key.slot, "-f"],
+ { superuser: true, pty: true, err: "message" });
+}
+
+export function clevis_recover_passphrase(block, just_type) {
+ const dev = decode_filename(block.Device);
+ const args = [];
+ if (just_type)
+ args.push("--type");
+ args.push(dev);
+ return cockpit.script(clevis_luks_passphrase_sh, args,
+ { superuser: true, err: "message" })
+ .then(output => output.trim());
+}
+
+function clevis_unlock(block) {
+ const dev = decode_filename(block.Device);
+ const clear_dev = "luks-" + block.IdUUID;
+ return cockpit.spawn(["clevis", "luks", "unlock", "-d", dev, "-n", clear_dev],
+ { superuser: true });
+}
+
+export async function unlock_with_type(client, block, passphrase, passphrase_type) {
+ const crypto = client.blocks_crypto[block.path];
+ if (passphrase) {
+ await crypto.Unlock(passphrase, {});
+ remember_passphrase(block, passphrase);
+ } else if (passphrase_type == "stored") {
+ await crypto.Unlock("", {});
+ } else if (passphrase_type == "clevis") {
+ await clevis_unlock(block);
+ } else {
+ // This should always be caught and should never show up in the UI
+ throw new Error("No passphrase");
+ }
+}
+
+/* Passphrase operations
+ */
+
+function passphrase_add(block, new_passphrase, old_passphrase) {
+ const dev = decode_filename(block.Device);
+ return cockpit.spawn(["cryptsetup", "luksAddKey", dev],
+ { superuser: true, err: "message" }).input(old_passphrase + "\n" + new_passphrase);
+}
+
+function passphrase_change(block, key, new_passphrase, old_passphrase) {
+ const dev = decode_filename(block.Device);
+ return cockpit.spawn(["cryptsetup", "luksChangeKey", dev, "--key-slot", key.slot.toString()],
+ { superuser: true, err: "message" }).input(old_passphrase + "\n" + new_passphrase + "\n");
+}
+
+function slot_remove(block, slot, passphrase) {
+ const dev = decode_filename(block.Device);
+ const opts = { superuser: true, err: "message" };
+ const cmd = ["cryptsetup", "luksKillSlot", dev, slot.toString()];
+ if (passphrase === false) {
+ cmd.splice(2, 0, "-q");
+ opts.pty = true;
+ }
+
+ const spawn = cockpit.spawn(cmd, opts);
+ if (passphrase !== false)
+ spawn.input(passphrase + "\n");
+
+ return spawn;
+}
+
+function passphrase_test(block, passphrase) {
+ const dev = decode_filename(block.Device);
+ return (cockpit.spawn(["cryptsetup", "luksOpen", "--test-passphrase", dev],
+ { superuser: true, err: "message" }).input(passphrase)
+ .then(() => true)
+ .catch(() => false));
+}
+
+/* Dialogs
+ */
+
+export function existing_passphrase_fields(explanation) {
+ return [
+ Skip("medskip", { visible: vals => vals.needs_explicit_passphrase }),
+ PassInput("passphrase", _("Disk passphrase"),
+ {
+ visible: vals => vals.needs_explicit_passphrase,
+ validate: val => !val.length && _("Passphrase cannot be empty"),
+ explanation
+ })
+ ];
+}
+
+function get_stored_passphrase(block, just_type) {
+ const pub_config = block.Configuration.find(c => c[0] == "crypttab");
+ if (pub_config && pub_config[1]["passphrase-path"] && decode_filename(pub_config[1]["passphrase-path"].v) != "") {
+ if (just_type)
+ return Promise.resolve("stored");
+ return block.GetSecretConfiguration({}).then(function (items) {
+ for (let i = 0; i < items.length; i++) {
+ if (items[i][0] == 'crypttab' && items[i][1]['passphrase-contents'])
+ return decode_filename(items[i][1]['passphrase-contents'].v);
+ }
+ return "";
+ });
+ }
+}
+
+export function get_existing_passphrase(block, just_type) {
+ return clevis_recover_passphrase(block, just_type).then(passphrase => {
+ return passphrase || get_stored_passphrase(block, just_type);
+ });
+}
+
+export function request_passphrase_on_error_handler(dlg, vals, recovered_passphrase, block) {
+ return function (error) {
+ if (vals.passphrase === undefined && block) {
+ return (passphrase_test(block, recovered_passphrase)
+ .then(good => {
+ if (!good)
+ dlg.set_values({ needs_explicit_passphrase: true });
+ return Promise.reject(error);
+ }));
+ } else
+ return Promise.reject(error);
+ };
+}
+
+export function init_existing_passphrase(block, just_type, callback) {
+ return {
+ title: _("Unlocking disk"),
+ func: dlg => {
+ return get_existing_passphrase(block, just_type).then(passphrase => {
+ if (!passphrase)
+ dlg.set_values({ needs_explicit_passphrase: true });
+ if (callback)
+ callback(passphrase);
+ return passphrase;
+ });
+ }
+ };
+}
+
+/* Getting the system ready for NBDE on the root filesystem.
+
+ We need the clevis module in the initrd. If it is not there, the
+ clevis-dracut package should be installed and the initrd needs to
+ be regenerated. We do this only after the user has agreed to it.
+
+ The kernel command line needs to have rd.neednet=1 in it. We just
+ do this unconditionally because it's so fast.
+*/
+
+function ensure_package_installed(steps, progress, package_name) {
+ function status_callback(progress) {
+ return p => {
+ let text = null;
+ if (p.waiting) {
+ text = _("Waiting for other software management operations to finish");
+ } else if (p.package) {
+ let fmt;
+ if (p.info == PkEnum.INFO_DOWNLOADING)
+ fmt = _("Downloading $0");
+ else if (p.info == PkEnum.INFO_REMOVING)
+ fmt = _("Removing $0");
+ else
+ fmt = _("Installing $0");
+ text = cockpit.format(fmt, p.package);
+ }
+ progress(text, p.cancel);
+ };
+ }
+
+ progress(cockpit.format(_("Checking for $0 package"), package_name), null);
+ return check_missing_packages([package_name], null)
+ .then(data => {
+ progress(null, null);
+ if (data.missing_names.length + data.unavailable_names.length > 0)
+ steps.push({
+ title: cockpit.format(_("The $0 package must be installed."), package_name),
+ func: progress => {
+ if (data.remove_names.length > 0)
+ return Promise.reject(cockpit.format(_("Installing $0 would remove $1."), name, data.remove_names[0]));
+ else if (data.unavailable_names.length > 0)
+ return Promise.reject(cockpit.format(_("The $0 package is not available from any repository."), name));
+ else
+ return install_missing_packages(data, status_callback(progress));
+ }
+ });
+ })
+ .catch(error => {
+ // Something wrong with PackageKit, maybe it is not even
+ // installed. Let's show the error during fixing.
+ progress(null, null);
+ steps.push({
+ title: cockpit.format(_("The $0 package must be installed."), package_name),
+ func: progress => {
+ if (error.problem == "not-found") {
+ return Promise.reject(cockpit.format(_("Error installing $0: PackageKit is not installed"), package_name));
+ } else {
+ return Promise.reject(cockpit.format(_("Unexpected PackageKit error during installation of $0: $1"), package_name, error.toString())); // not-covered: OS error
+ }
+ }
+ });
+ });
+}
+
+function ensure_initrd_clevis_support(steps, progress, package_name) {
+ const task = cockpit.spawn(["lsinitrd", "-m"], { superuser: true, err: "message" });
+ progress(_("Checking for NBDE support in the initrd"), () => task.close());
+ return task.then(data => {
+ progress(null, null);
+ if (data.indexOf("clevis") < 0) {
+ return ensure_package_installed(steps, progress, package_name)
+ .then(() => {
+ steps.push({
+ title: _("The initrd must be regenerated."),
+ func: progress => {
+ // dracut doesn't react to SIGINT, so let's not enable our Cancel button
+ progress(_("Regenerating initrd"), null);
+ return cockpit.spawn(["dracut", "--force", "--regenerate-all"],
+ { superuser: true, err: "message" });
+ }
+ });
+ });
+ }
+ });
+}
+
+function ensure_root_nbde_support(steps, progress) {
+ progress(_("Adding rd.neednet=1 to kernel command line"), null);
+ return cockpit.spawn(["grubby", "--update-kernel=ALL", "--args=rd.neednet=1"],
+ { superuser: true, err: "message" })
+ .then(() => ensure_initrd_clevis_support(steps, progress, "clevis-dracut"));
+}
+
+function ensure_fstab_option(steps, progress, client, block, option) {
+ const cleartext = client.blocks_cleartext[block.path];
+ const crypto = client.blocks_crypto[block.path];
+ const fsys_config = cleartext
+ ? cleartext.Configuration.find(c => c[0] == "fstab")
+ : crypto?.ChildConfiguration.find(c => c[0] == "fstab");
+ const fsys_options = fsys_config && parse_options(get_block_mntopts(fsys_config[1]));
+
+ if (!fsys_options || fsys_options.indexOf(option) >= 0)
+ return Promise.resolve();
+
+ const new_fsys_options = fsys_options.concat([option]);
+ const new_fsys_config = [
+ "fstab",
+ Object.assign({ }, fsys_config[1],
+ {
+ opts: {
+ t: 'ay',
+ v: encode_filename(unparse_options(new_fsys_options))
+ }
+ })
+ ];
+ progress(cockpit.format(_("Adding \"$0\" to filesystem options"), option), null);
+ return block.UpdateConfigurationItem(fsys_config, new_fsys_config, { });
+}
+
+function ensure_crypto_option(steps, progress, client, block, option) {
+ const crypto_config = block.Configuration.find(c => c[0] == "crypttab");
+ const crypto_options = crypto_config && parse_options(decode_filename(crypto_config[1].options.v));
+ if (!crypto_options || crypto_options.indexOf(option) >= 0)
+ return Promise.resolve();
+
+ const new_crypto_options = crypto_options.concat([option]);
+ progress(cockpit.format(_("Adding \"$0\" to encryption options"), option), null);
+ return edit_crypto_config(block, (config, commit) => {
+ config.options = { t: 'ay', v: encode_filename(unparse_options(new_crypto_options)) };
+ return commit();
+ });
+}
+
+function ensure_systemd_unit_enabled(steps, progress, name, package_name) {
+ progress(cockpit.format(_("Enabling $0"), name));
+ return cockpit.spawn(["systemctl", "is-enabled", name], { err: "message" })
+ .catch((err, output) => {
+ if (err && (output == "" || output.trim() == "not-found") && package_name) {
+ // We assume that installing the package will enable the unit.
+ return ensure_package_installed(steps, progress, package_name);
+ } else
+ return cockpit.spawn(["systemctl", "enable", name],
+ { superuser: true, err: "message" });
+ });
+}
+
+function ensure_non_root_nbde_support(steps, progress, client, block) {
+ return ensure_systemd_unit_enabled(steps, progress, "remote-cryptsetup.target")
+ .then(() => ensure_systemd_unit_enabled(steps, progress, "clevis-luks-askpass.path", "clevis-systemd"))
+ .then(() => ensure_fstab_option(steps, progress, client, block, "_netdev"))
+ .then(() => ensure_crypto_option(steps, progress, client, block, "_netdev"));
+}
+
+function contains_rootfs(client, path) {
+ const block = client.blocks[path];
+ const crypto = client.blocks_crypto[path];
+ let fsys_config = null;
+
+ if (block)
+ fsys_config = block.Configuration.find(c => c[0] == "fstab");
+ if (!fsys_config && crypto)
+ fsys_config = crypto.ChildConfiguration.find(c => c[0] == "fstab");
+
+ if (fsys_config) {
+ const dir = decode_filename(fsys_config[1].dir.v);
+ return dir == "/";
+ }
+
+ return get_children(client, path).some(p => contains_rootfs(client, p));
+}
+
+function ensure_nbde_support(steps, progress, client, block) {
+ if (contains_rootfs(client, block.path)) {
+ if (client.get_config("nbde_root_help", false)) {
+ steps.is_root = true;
+ return ensure_root_nbde_support(steps, progress);
+ } else
+ return Promise.resolve();
+ } else
+ return ensure_non_root_nbde_support(steps, progress, client, block);
+}
+
+function ensure_nbde_support_dialog(steps, client, block, url, adv, old_key, existing_passphrase) {
+ const dlg = dialog_open({
+ Title: _("Add Network Bound Disk Encryption"),
+ Body: (
+ <TextContent>
+ <Text compmonent={TextVariants.p}>
+ { steps.is_root
+ ? _("The system does not currently support unlocking the root filesystem with a Tang keyserver.")
+ : _("The system does not currently support unlocking a filesystem with a Tang keyserver during boot.")
+ }
+ </Text>
+ <Text compmonent={TextVariants.p}>
+ {_("These additional steps are necessary:")}
+ </Text>
+ <TextList>
+ { steps.map((s, i) => <TextListItem key={i}>{s.title}</TextListItem>) }
+ </TextList>
+ </TextContent>),
+ Fields: existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase.")),
+ Action: {
+ Title: _("Fix NBDE support"),
+ action: (vals, progress) => {
+ return for_each_async(steps, s => s.func(progress))
+ .then(() => {
+ steps = [];
+ progress(_("Adding key"), null);
+ return add_or_update_tang(dlg, vals, block,
+ url, adv, old_key,
+ vals.passphrase || existing_passphrase);
+ });
+ }
+ }
+ });
+}
+
+function add_dialog(client, block) {
+ let recovered_passphrase;
+
+ dialog_open({
+ Title: _("Add key"),
+ Fields: [
+ SelectOneRadio("type", _("Key source"),
+ {
+ value: "luks-passphrase",
+ visible: vals => client.features.clevis,
+ widest_title: _("Repeat passphrase"),
+ choices: [
+ { value: "luks-passphrase", title: _("Passphrase") },
+ { value: "tang", title: _("Tang keyserver") }
+ ]
+ }),
+ Skip("medskip"),
+ PassInput("new_passphrase", _("New passphrase"),
+ {
+ visible: vals => !client.features.clevis || vals.type == "luks-passphrase",
+ validate: val => !val.length && _("Passphrase cannot be empty"),
+ new_password: true
+ }),
+ PassInput("new_passphrase2", _("Repeat passphrase"),
+ {
+ visible: vals => !client.features.clevis || vals.type == "luks-passphrase",
+ validate: (val, vals) => {
+ return (vals.new_passphrase.length &&
+ vals.new_passphrase != val &&
+ _("Passphrases do not match"));
+ },
+ new_password: true
+ }),
+ TextInput("tang_url", _("Keyserver address"),
+ {
+ visible: vals => client.features.clevis && vals.type == "tang",
+ validate: validate_url
+ })
+ ].concat(existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase."))),
+ Action: {
+ Title: _("Add"),
+ action: function (vals, progress) {
+ const existing_passphrase = vals.passphrase || recovered_passphrase;
+ if (!client.features.clevis || vals.type == "luks-passphrase") {
+ return passphrase_add(block, vals.new_passphrase, existing_passphrase);
+ } else {
+ return get_tang_adv(vals.tang_url)
+ .then(adv => {
+ edit_tang_adv(client, block, null,
+ vals.tang_url, adv, existing_passphrase);
+ });
+ }
+ }
+ },
+ Inits: [
+ init_existing_passphrase(block, false, pp => { recovered_passphrase = pp })
+ ]
+ });
+}
+
+function edit_passphrase_dialog(block, key) {
+ dialog_open({
+ Title: _("Change passphrase"),
+ Fields: [
+ PassInput("old_passphrase", _("Old passphrase"),
+ { validate: val => !val.length && _("Passphrase cannot be empty") }),
+ Skip("medskip"),
+ PassInput("new_passphrase", _("New passphrase"),
+ {
+ validate: val => !val.length && _("Passphrase cannot be empty"),
+ new_password: true
+ }),
+ PassInput("new_passphrase2", _("Repeat passphrase"),
+ {
+ validate: (val, vals) => vals.new_passphrase.length && vals.new_passphrase != val && _("Passphrases do not match"),
+ new_password: true
+ })
+ ],
+ Action: {
+ Title: _("Save"),
+ action: vals => passphrase_change(block, key, vals.new_passphrase, vals.old_passphrase)
+ }
+ });
+}
+
+function edit_clevis_dialog(client, block, key) {
+ let recovered_passphrase;
+
+ dialog_open({
+ Title: _("Edit Tang keyserver"),
+ Fields: [
+ TextInput("tang_url", _("Keyserver address"),
+ {
+ validate: validate_url,
+ value: key.url
+ })
+ ].concat(existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase."))),
+ Action: {
+ Title: _("Save"),
+ action: function (vals) {
+ const existing_passphrase = vals.passphrase || recovered_passphrase;
+ return get_tang_adv(vals.tang_url).then(adv => {
+ edit_tang_adv(client, block, key, vals.tang_url, adv, existing_passphrase);
+ });
+ }
+ },
+ Inits: [
+ init_existing_passphrase(block, false, pp => { recovered_passphrase = pp })
+ ]
+ });
+}
+
+function add_or_update_tang(dlg, vals, block, url, adv, old_key, passphrase) {
+ return clevis_add(block, "tang", { url, adv }, vals.passphrase || passphrase).then(() => {
+ if (old_key)
+ return clevis_remove(block, old_key);
+ })
+ .catch(request_passphrase_on_error_handler(dlg, vals, passphrase, block));
+}
+
+function edit_tang_adv(client, block, key, url, adv, passphrase) {
+ const dlg = dialog_open({
+ Title: _("Verify key"),
+ Body: <TangKeyVerification url={url} adv={adv} />,
+ Fields: existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase.")),
+ Action: {
+ Title: _("Trust key"),
+ action: function (vals, progress) {
+ if (key) {
+ return add_or_update_tang(dlg, vals, block,
+ url, adv, key,
+ passphrase);
+ } else {
+ const steps = [];
+ return ensure_nbde_support(steps, progress, client, block)
+ .then(() => {
+ if (steps.length > 0)
+ ensure_nbde_support_dialog(steps, client, block, url,
+ adv, key, passphrase);
+ else {
+ progress(null, null);
+ return add_or_update_tang(dlg, vals, block,
+ url, adv, key,
+ passphrase);
+ }
+ });
+ }
+ }
+ }
+ });
+}
+
+const RemovePassphraseField = (tag, key, dev) => {
+ function validate(val) {
+ if (val === "")
+ return _("Passphrase can not be empty");
+ }
+
+ return {
+ tag,
+ title: null,
+ options: { validate },
+ initial_value: "",
+ bare: true,
+
+ render: (val, change, validated, error) => {
+ return (
+ <Stack hasGutter>
+ <p>{ fmt_to_fragments(_("Passphrase removal may prevent unlocking $0."), <b>{dev}</b>) }</p>
+ <Checkbox id="force-remove-passphrase"
+ isChecked={val !== false}
+ label={_("Confirm removal with an alternate passphrase")}
+ onChange={(_event, checked) => change(checked ? "" : false)}
+ body={val === false
+ ? <p className="slot-warning">
+ {_("Removing a passphrase without confirmation of another passphrase may prevent unlocking or key management, if other passphrases are forgotten or lost.")}
+ </p>
+ : <FormGroup label={_("Passphrase from any other key slot")} fieldId="remove-passphrase">
+ <TextInputPF id="remove-passphrase" type="password" value={val} onChange={(_event, value) => change(value)} />
+ </FormGroup>
+ }
+ />
+ </Stack>
+ );
+ }
+ };
+};
+
+function remove_passphrase_dialog(block, key) {
+ dialog_open({
+ Title: cockpit.format(_("Remove passphrase in key slot $0?"), key.slot),
+ Fields: [
+ RemovePassphraseField("passphrase", key, block_name(block))
+ ],
+ isFormHorizontal: false,
+ Action: {
+ DangerButton: true,
+ Title: _("Remove"),
+ action: function (vals) {
+ return slot_remove(block, key.slot, vals.passphrase);
+ }
+ }
+ });
+}
+
+const RemoveClevisField = (tag, key, dev) => {
+ return {
+ tag,
+ title: null,
+ options: { },
+ initial_value: "",
+ bare: true,
+
+ render: (val, change) => {
+ return (
+ <div data-field={tag}>
+ <p>{ fmt_to_fragments(_("Remove $0?"), <b>{key.url}</b>) }</p>
+ <p className="slot-warning">{ fmt_to_fragments(_("Keyserver removal may prevent unlocking $0."), <b>{dev}</b>) }</p>
+ </div>
+ );
+ }
+ };
+};
+
+function remove_clevis_dialog(client, block, key) {
+ dialog_open({
+ Title: _("Remove Tang keyserver?"),
+ Fields: [
+ RemoveClevisField("keyserver", key, block_name(block))
+ ],
+ Action: {
+ DangerButton: true,
+ Title: _("Remove"),
+ action: function () {
+ return clevis_remove(block, key);
+ }
+ }
+ });
+}
+
+export class CryptoKeyslots extends React.Component {
+ render() {
+ const { client, block, slots, slot_error, max_slots } = this.props;
+
+ if ((slots == null && slot_error == null) || slot_error == "not-found")
+ return null;
+
+ function decode_clevis_slot(slot) {
+ if (slot.ClevisConfig) {
+ const clevis = JSON.parse(slot.ClevisConfig.v);
+ if (clevis.pin && clevis.pin == "tang" && clevis.tang) {
+ return {
+ slot: slot.Index.v,
+ type: "tang",
+ url: clevis.tang.url
+ };
+ } else {
+ return {
+ slot: slot.Index.v,
+ type: "unknown",
+ pin: clevis.pin
+ };
+ }
+ } else {
+ return {
+ slot: slot.Index.v,
+ type: "luks-passphrase"
+ };
+ }
+ }
+
+ const keys = slots ? slots.map(decode_clevis_slot).filter(k => !!k) : [];
+
+ let table;
+ if (keys.length == 0) {
+ let text;
+ if (slot_error) {
+ if (slot_error.problem == "access-denied")
+ text = _("The currently logged in user is not permitted to see information about keys.");
+ else
+ text = slot_error.toString();
+ } else {
+ text = _("No keys added");
+ }
+ table = <EmptyState>
+ <EmptyStateBody>
+ {text}
+ </EmptyStateBody>
+ </EmptyState>;
+ } else {
+ const rows = [];
+
+ const add_row = (slot, type, desc, edit, edit_excuse, remove) => {
+ rows.push(
+ <DataListItem key={slot}>
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell key="key-type">
+ { type }
+ </DataListCell>,
+ <DataListCell key="desc" isFilled={false}>
+ { desc }
+ </DataListCell>,
+ <DataListCell key="key-slot">
+ { cockpit.format(_("Slot $0"), slot) }
+ </DataListCell>,
+ <DataListCell key="text-right" isFilled={false} alignRight>
+ <StorageButton onClick={edit}
+ ariaLabel={_("Edit")}
+ excuse={(keys.length == max_slots)
+ ? _("Editing a key requires a free slot")
+ : null}>
+ <EditIcon />
+ </StorageButton>
+ { "\n" }
+ <StorageButton onClick={remove}
+ ariaLabel={_("Remove")}
+ excuse={keys.length == 1 ? _("The last key slot can not be removed") : null}>
+ <MinusIcon />
+ </StorageButton>
+ </DataListCell>,
+ ]}
+ />
+ </DataListItemRow>
+ </DataListItem>
+ );
+ };
+
+ keys.sort((a, b) => a.slot - b.slot).forEach(key => {
+ if (key.type == "luks-passphrase") {
+ add_row(key.slot,
+ _("Passphrase"), "",
+ () => edit_passphrase_dialog(block, key), null,
+ () => remove_passphrase_dialog(block, key));
+ } else if (key.type == "tang") {
+ add_row(key.slot,
+ _("Keyserver"), key.url,
+ () => edit_clevis_dialog(client, block, key), null,
+ () => remove_clevis_dialog(client, block, key));
+ } else {
+ add_row(key.slot,
+ _("Unknown type"), "",
+ null, _("Key slots with unknown types can not be edited here"),
+ () => remove_clevis_dialog(client, block, key));
+ }
+ });
+
+ table = <DataList isCompact className="crypto-keyslots-list" aria-label={_("Keys")}>
+ {rows}
+ </DataList>;
+ }
+
+ const remaining = max_slots - keys.length;
+
+ return (
+ <>
+ <CardHeader actions={{
+ actions: <>
+ <span className="key-slot-panel-remaining">
+ { remaining < 6 ? (remaining ? cockpit.format(cockpit.ngettext("$0 slot remains", "$0 slots remain", remaining), remaining) : _("No available slots")) : null }
+ </span>
+ <StorageButton onClick={() => add_dialog(client, block)}
+ ariaLabel={_("Add")}
+ excuse={(keys.length == max_slots)
+ ? _("No free key slots")
+ : null}>
+ <PlusIcon />
+ </StorageButton>
+ </>,
+ }}>
+ <CardTitle component="h2">{_("Keys")}</CardTitle>
+ </CardHeader>
+ <CardBody id="encryption-keys" className="contains-list">
+ {table}
+ </CardBody>
+ </>
+ );
+ }
+}
diff --git a/pkg/storaged/crypto/locked-encrypted-data.jsx b/pkg/storaged/crypto/locked-encrypted-data.jsx
new file mode 100644
index 0000000..155aade
--- /dev/null
+++ b/pkg/storaged/crypto/locked-encrypted-data.jsx
@@ -0,0 +1,41 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import client from "../client";
+
+import { StorageCard, new_card } from "../pages.jsx";
+import { format_dialog } from "../block/format-dialog.jsx";
+import { unlock } from "./actions.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_locked_encrypted_data_card(next, block) {
+ return new_card({
+ title: _("Locked data"),
+ next,
+ page_block: block,
+ component: StorageCard,
+ props: { block },
+ actions: [
+ { title: _("Unlock"), action: () => unlock(block) },
+ { title: _("Format"), action: () => format_dialog(client, block.path), danger: true },
+ ]
+ });
+}
diff --git a/pkg/storaged/crypto/luksmeta-monitor-hack.py b/pkg/storaged/crypto/luksmeta-monitor-hack.py
new file mode 100755
index 0000000..36c50fb
--- /dev/null
+++ b/pkg/storaged/crypto/luksmeta-monitor-hack.py
@@ -0,0 +1,152 @@
+#! /usr/bin/python3
+
+# This simulates the org.freedesktop.UDisks.Encrypted.Slots property
+# et al for versions of UDisks that don't have them yet.
+
+import atexit
+import base64
+import json
+import os
+import re
+import signal
+import subprocess
+import sys
+
+
+def b64_decode(data):
+ # The data we get doesn't seem to have any padding, but the base64
+ # module requires it. So we add it back. Can't anyone agree on
+ # anything? Not even base64?
+ return base64.urlsafe_b64decode((data + '=' * ((4 - len(data) % 4) % 4)).encode('ascii', 'ignore'))
+
+
+def get_clevis_config_from_protected_header(protected_header):
+ header = b64_decode(protected_header).decode("utf-8")
+ header_object = json.loads(header)
+ clevis = header_object.get("clevis", None)
+ if clevis is None:
+ return None
+
+ pin = clevis.get("pin", None)
+ if pin == "tang":
+ return clevis
+ elif pin == "sss":
+ subpins = {}
+ jwes = clevis["sss"]["jwe"]
+ for jwe in jwes:
+ subconf = get_clevis_config_from_jwe(jwe)
+ subpin = subconf["pin"]
+ if subpin not in subpins:
+ subpins[subpin] = [subconf[subpin]]
+ else:
+ subpins[subpin].append(subconf[subpin])
+ return {"pin": "sss", "sss": {"t": clevis["sss"]["t"], "pins": subpins}}
+ else:
+ return {"pin": pin, pin: {}}
+
+
+def get_clevis_config_from_jwe(jwe):
+ return get_clevis_config_from_protected_header(jwe.split(".")[0])
+
+
+def info(dev):
+ slots = {}
+ version = 1
+ max_slots = 8
+
+ try:
+ result = subprocess.check_output(["cryptsetup", "luksDump", dev], stderr=subprocess.PIPE)
+ except subprocess.CalledProcessError:
+ return {"version": version, "slots": [], "max_slots": max_slots}
+
+ in_luks2_slot_section = False
+ in_luks2_token_section = False
+ for line in result.splitlines():
+ if not (line.startswith((b' ', b'\t'))):
+ in_luks2_slot_section = False
+ in_luks2_token_section = False
+ if line == b"Keyslots:":
+ in_luks2_slot_section = True
+ version = 2
+ max_slots = 32
+ elif line == b"Tokens:":
+ in_luks2_token_section = True
+
+ if in_luks2_slot_section:
+ match = re.match(b" ([0-9]+): luks2$", line)
+ else:
+ match = re.match(b"Key Slot ([0-9]+): ENABLED$", line)
+ if match:
+ slot = int(match.group(1))
+ entry = {"Index": {"v": slot}}
+ if version == 1:
+ try:
+ luksmeta = subprocess.check_output(["luksmeta", "load", "-d", dev, "-s", str(slot),
+ "-u", "cb6e8904-81ff-40da-a84a-07ab9ab5715e"],
+ stderr=subprocess.PIPE)
+ entry["ClevisConfig"] = {
+ "v": json.dumps(get_clevis_config_from_jwe(luksmeta.decode("utf-8")))
+ }
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ pass
+ if slot not in slots:
+ slots[slot] = entry
+
+ if in_luks2_token_section:
+ match = re.match(b" ([0-9]+): clevis$", line)
+ if match:
+ try:
+ token = subprocess.check_output(["cryptsetup", "token", "export", dev, "--token-id", match.group(1)],
+ stderr=subprocess.PIPE)
+ token_object = json.loads(token.decode("utf-8"))
+ if token_object.get("type") == "clevis":
+ config = json.dumps(get_clevis_config_from_protected_header(token_object["jwe"]["protected"]))
+ for slot_str in token_object.get("keyslots", []):
+ slot = int(slot_str)
+ slots[slot] = {"Index": {"v": slot},
+ "ClevisConfig": {"v": config}}
+ except subprocess.CalledProcessError:
+ pass
+
+ return {"version": version, "slots": list(slots.values()), "max_slots": max_slots}
+
+
+def monitor(dev):
+ mon = None
+
+ # We have to kill the udevadm process explicitly when Cockpit
+ # kills us. It will eventually exit on its own since its stdout
+ # will be closed when we exit, but that will only happen when it
+ # actually writes something.
+
+ def killmon():
+ if mon:
+ mon.terminate()
+
+ def sigexit(_signo, _stack):
+ killmon()
+ os._exit(0)
+
+ atexit.register(killmon)
+ signal.signal(signal.SIGTERM, sigexit)
+ signal.signal(signal.SIGINT, sigexit)
+ signal.signal(signal.SIGHUP, sigexit)
+
+ path = subprocess.check_output(["udevadm", "info", "-q", "path", dev]).rstrip(b"\n")
+ mon = subprocess.Popen(["stdbuf", "-o", "L", "udevadm", "monitor", "-u", "-s", "block"],
+ stdout=subprocess.PIPE)
+
+ old_infos = info(dev)
+ sys.stdout.write(json.dumps(old_infos) + "\n")
+ sys.stdout.flush()
+ while True:
+ line = mon.stdout.readline()
+ if path in line:
+ new_infos = info(dev)
+ if new_infos != old_infos:
+ sys.stdout.write(json.dumps(new_infos) + "\n")
+ sys.stdout.flush()
+ old_infos = new_infos
+
+
+monitor(sys.argv[1])
diff --git a/pkg/storaged/crypto/tang.jsx b/pkg/storaged/crypto/tang.jsx
new file mode 100644
index 0000000..1ba65a3
--- /dev/null
+++ b/pkg/storaged/crypto/tang.jsx
@@ -0,0 +1,133 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { ClipboardCopy } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+
+import sha1 from "js-sha1";
+import sha256 from "js-sha256";
+import stable_stringify from "json-stable-stringify-without-jsonify";
+
+const _ = cockpit.gettext;
+
+export function validate_url(url) {
+ if (url.length === 0)
+ return _("Address cannot be empty");
+ if (!parse_url(url))
+ return _("Address is not a valid URL");
+}
+
+export function get_tang_adv(url) {
+ return cockpit.spawn(["curl", "-sSf", url + "/adv"], { err: "message" })
+ .then(JSON.parse)
+ .catch(error => {
+ return Promise.reject(error.toString().replace(/^curl: \([0-9]+\) /, ""));
+ });
+}
+
+function parse_url(url) {
+ // clevis-encrypt-tang defaults to "http://" (via curl), so we do the same here.
+ if (!/^[a-zA-Z]+:\/\//.test(url))
+ url = "http://" + url;
+ try {
+ return new URL(url);
+ } catch (e) {
+ if (e instanceof TypeError)
+ return null;
+ throw e;
+ }
+}
+
+function tang_adv_payload(adv) {
+ return JSON.parse(cockpit.utf8_decoder().decode(cockpit.base64_decode(adv.payload)));
+}
+
+function jwk_b64_encode(bytes) {
+ // Use the urlsafe character set, and strip the padding.
+ return cockpit.base64_encode(bytes).replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=+$/, '');
+}
+
+function compute_thp(jwk) {
+ const REQUIRED_ATTRS = {
+ RSA: ['kty', 'p', 'd', 'q', 'dp', 'dq', 'qi', 'oth'],
+ EC: ['kty', 'crv', 'x', 'y'],
+ oct: ['kty', 'k'],
+ };
+
+ if (!jwk.kty)
+ return "(no key type attribute=";
+ if (!REQUIRED_ATTRS[jwk.kty])
+ return cockpit.format("(unknown keytype $0)", jwk.kty);
+
+ const req = REQUIRED_ATTRS[jwk.kty];
+ const norm = { };
+ req.forEach(k => { if (k in jwk) norm[k] = jwk[k]; });
+ return {
+ sha256: jwk_b64_encode(sha256.digest(stable_stringify(norm))),
+ sha1: jwk_b64_encode(sha1.digest(stable_stringify(norm)))
+ };
+}
+
+function compute_sigkey_thps(adv) {
+ function is_signing_key(jwk) {
+ if (!jwk.use && !jwk.key_ops)
+ return true;
+ if (jwk.use == "sig")
+ return true;
+ if (jwk.key_ops && jwk.key_ops.indexOf("verify") >= 0)
+ return true;
+ return false;
+ }
+
+ return adv.keys.filter(is_signing_key).map(compute_thp);
+}
+
+export const TangKeyVerification = ({ url, adv }) => {
+ const parsed = parse_url(url);
+ const cmd = cockpit.format("ssh $0 tang-show-keys $1", parsed.hostname, parsed.port);
+ const sigkey_thps = compute_sigkey_thps(tang_adv_payload(adv));
+
+ return (
+ <TextContent>
+ <Text component={TextVariants.p}>{_("Check the key hash with the Tang server.")}</Text>
+
+ <Text component={TextVariants.h3}>{_("How to check")}</Text>
+ <span>{_("In a terminal, run: ")}</span>
+ <ClipboardCopy hoverTip={_("Copy to clipboard")}
+ clickTip={_("Successfully copied to clipboard!")}
+ variant="inline-compact"
+ isCode>
+ {cmd}
+ </ClipboardCopy>
+ <Text component={TextVariants.p}>
+ {_("Check that the SHA-256 or SHA-1 hash from the command matches this dialog.")}
+ </Text>
+
+ <Text component={TextVariants.h3}>{_("SHA-256")}</Text>
+ { sigkey_thps.map(s => <Text key={s.sha256} component={TextVariants.pre}>{s.sha256}</Text>) }
+
+ <Text component={TextVariants.h3}>{_("SHA-1")}</Text>
+ { sigkey_thps.map(s => <Text key={s.sha1} component={TextVariants.pre}>{s.sha1}</Text>) }
+ </TextContent>);
+};
diff --git a/pkg/storaged/dialog.jsx b/pkg/storaged/dialog.jsx
new file mode 100644
index 0000000..c76a1b0
--- /dev/null
+++ b/pkg/storaged/dialog.jsx
@@ -0,0 +1,1374 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* STORAGE DIALOGS
+
+ To show a modal dialog, make a call like this:
+
+ dialog_show({ Title: _("What is your name?"),
+ Fields: [
+ TextInput("name", _("Name"),
+ { validate: val => (val == ""? _("Name can't be empty") : null) })
+ ]
+ Action: {
+ Title: _("Ok"),
+ action: vals => { console.log("Hello, " + vals.name + "!"); }
+ }
+ });
+
+ The call to dialog_show will open the dialog and return
+ immediately. Later, when the user clicks on "Ok", the "action"
+ function will be called with the values of the dialog fields. The
+ action function usually returns a promise, although it does not in
+ the example above. When that promise resolves, the dialog is
+ closed. When the promise is rejected, it's error is displayed in
+ the dialog, and the dialog stays open.
+
+ Fields are described by calling functions such as TextInput. A
+ number of generic ones are defined here, and you can define more
+ specialized ones yourself.
+
+ They are all called like this:
+
+ FieldFunction(tag, title, { option: value, ... })
+
+ The "tag" is used to uniquely identify this field in the dialog.
+ The action function will receive the values of all fields in an
+ object, and the tag of a field is the key in that object, for
+ example. The tag is also used to interact with a field from tests.
+
+ ACTION FUNCTIONS
+
+ The action function is called like this:
+
+ action(values, progress_callback)
+
+ The "values" parameter contains the validated values of the dialog
+ fields and the "progress_callback" can be called by the action function
+ to update the progress information in the dialog while it runs.
+
+ The progress callback should be called like this:
+
+ progress_callback(message, cancel_callback)
+
+ The "message" will be displayed in the dialog and if "cancel_callback" is
+ not null, the Cancel button in the dialog will be enabled and
+ "cancel_callback" will be called when the user clicks it.
+
+ The return value of the action function is normally a promise. When
+ it is resolved, the dialog is closed. When it is rejected the value
+ given in the rejection is displayed as an error in the dialog.
+
+ If the error value is a string, it is displayed as a global failure
+ message. When it is an object, it contains errors for individual
+ fields in this form:
+
+ { tag1: message, tag2: message }
+
+ As a special case, when "message" is "true", the field is rendered
+ as having an error (with a red outline, say), but without any
+ directly associated text. The idea is that a group of fields is in
+ error, and the error message for all of them is shown below the last
+ one in the group.
+
+ COMMON FIELD OPTIONS
+
+ Each field function describes its options. However, there are some
+ options that apply to all fields:
+
+ - value
+
+ The initial value of the field.
+
+ - visible: vals -> boolean
+
+ This function determines whether the field is shown or not.
+
+ - validate: (val, vals) -> null-or-error-string (or promise)
+
+ The validate function receives the current value of the field and
+ should return "null" (or something falsey) when that value is
+ acceptable. Otherwise, it should return a suitable error message.
+
+ The second argument has all values of all fields, in case you need
+ to look at more than one field.
+
+ It is permissible to overwrite fields of "vals" to change the final
+ value of a field.
+
+ The validate function can also return a promise which resolves to
+ null or an error message. If that promise is rejected, that error
+ is shown globally in the dialog as if the action function had
+ failed.
+
+ The validate function will only be called for currently visible
+ fields.
+
+ - widest_title
+
+ This is a hack to force the column of titles to be a certain
+ minimum width, namely the width of the widest_title. This matters
+ when there are rows that are only sometimes visible and the layout
+ would jump around when they change visibility.
+
+ Technically, the first column of a row shows the "title" but is as
+ wide as its "widest_title". The idea is that you put the widest
+ title of all fields in the widest_title option of one of the rows
+ that are always visible.
+
+ - explanation
+
+ A test to show below the field, as an explanation.
+
+ RUNNING TASKS AND DYNAMIC UPDATES
+
+ The dialog_show function returns an object that can be used to interact
+ with the dialog in various ways while it is open.
+
+ dlg = dialog_show(...)
+
+ One can run asynchronous tasks:
+
+ dlg.run("title", promise)
+
+ This will disable the footer buttons and wait for promise to be resolved
+ or rejected while showing "title" and a spinner.
+
+ One can set field values and options:
+
+ dlg.set_values({ tag1: value1, tag2: value2, ... })
+ dlg.set_options(tag, { opt1: value1, opt2: value2, ... })
+
+ It is also possible to specify a "update" function when creating the dialog:
+
+ dialog_show({ ...
+ update: function (dlg, vals, trigger) { }
+ ... })
+
+ This function is called whenever the values of fields are changed. The
+ "trigger" argument is the tag of the field that has just been changed.
+
+ DEFINING NEW FIELD TYPES
+
+ To define a new field type, just define a new function that follows
+ a few rules. Here is TextInput:
+
+ export const TextInput = (tag, title, options) => {
+ return {
+ tag: tag,
+ title: title,
+ options: options,
+ initial_value: "",
+
+ render: (val, change) =>
+ <input data-field={tag}
+ className="form-control" type="text" value={val}
+ onChange={event => change(event.target.value)}/>
+ }
+ }
+
+ As you can see, a field function should return an object with a
+ couple of fields. The "tag", "title", and "options" field just
+ store the parameters to the field function. The rest are these:
+
+ - initial_value
+
+ This is the initial value of the field.
+
+ - render: (val, change) -> React components
+
+ This should render the value part of the field, that is, the second
+ column in the table layout. The title is in the first column and
+ is rendered by the generic dialog machinery.
+
+ The "val" parameter is the current value and you should make sure
+ that the DOM element really shows that value, and not something
+ that might have left behind by previous user interactions.
+
+ The "change" parameter is a function that should be called with a
+ new value for the field whenever the user has interacted with it.
+
+ For the benefits of the integration tests, the DOM elements should
+ also contain "data-field" and maybe a "data-field-type" attributes. The
+ "data-field" value should be that tag of the field, and
+ "data-field-type" type is used by the tests to know how to interact
+ with the field. If you find to need it, just pick a reasonable value
+ and extend the test suite to handle it.
+
+ This function is not called at all for invisible fields.
+ */
+
+import cockpit from "cockpit";
+
+import React, { useState } from "react";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { DataList, DataListCell, DataListCheck, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
+import { Select as TypeAheadSelect, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Slider } from "@patternfly/react-core/dist/esm/components/Slider/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { Split } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
+import { TextInput as TextInputPF4 } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
+import { ExclamationTriangleIcon, InfoIcon, HelpIcon, EyeIcon, EyeSlashIcon } from "@patternfly/react-icons";
+import { InputGroup } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js";
+
+import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
+import { ListingTable } from "cockpit-components-table.jsx";
+import { FormHelper } from "cockpit-components-form-helper";
+
+import { fmt_size, block_name, format_size_and_text, format_delay, for_each_async } from "./utils.js";
+import { fmt_to_fragments } from "utils.jsx";
+import client from "./client.js";
+
+const _ = cockpit.gettext;
+
+function make_rows(fields, values, errors, onChange) {
+ return fields.map((f, i) => <Row key={i} field={f} values={values} errors={errors} onChange={onChange} />)
+ .filter(r => r);
+}
+
+function is_visible(field, values) {
+ return !field.options || field.options.visible == undefined || field.options.visible(values);
+}
+
+const Row = ({ field, values, errors, onChange }) => {
+ const { tag, title, options } = field;
+
+ if (!is_visible(field, values))
+ return null;
+
+ const error = errors && errors[tag];
+ const explanation = options && options.explanation;
+ const validated = (tag && errors && errors[tag]) ? 'error' : 'default';
+
+ function change(val) {
+ values[tag] = val;
+ onChange(tag);
+ }
+
+ const field_elts = field.render(values[tag], change, validated, error);
+ const nested_elts = (options && options.nested_fields
+ ? make_rows(options.nested_fields, values, errors, onChange)
+ : []);
+
+ if (title || title == "") {
+ let titleLabel = title;
+
+ if (options.widest_title)
+ titleLabel = (
+ <>
+ <div className="widest-title">{options.widest_title}</div>
+ <div>{title}</div>
+ </>
+ );
+ return (
+ <FormGroup label={titleLabel} hasNoPaddingTop={field.hasNoPaddingTop}>
+ { field_elts }
+ { nested_elts }
+ <FormHelper helperText={explanation} helperTextInvalid={validated && error} />
+ </FormGroup>
+ );
+ } else if (!field.bare) {
+ return (
+ <FormGroup validated={validated} hasNoPaddingTop={field.hasNoPaddingTop}>
+ { field_elts }
+ { nested_elts }
+ <FormHelper helperText={explanation} helperTextInvalid={validated && error} />
+ </FormGroup>
+ );
+ } else
+ return field_elts;
+};
+
+const Body = ({ body, teardown, fields, values, errors, isFormHorizontal, onChange }) => {
+ let error_alert = null;
+
+ if (errors && errors.toString() != "[object Object]") {
+ // This is a global error from a failed action
+ error_alert = <Alert variant='danger' isInline title={errors.toString()} />;
+ errors = null;
+ }
+
+ return (
+ <>
+ { error_alert }
+ { body || null }
+ { fields.length > 0
+ ? <Form onSubmit={apply_modal_dialog}
+ isHorizontal={isFormHorizontal !== false}>
+ { make_rows(fields, values, errors, onChange) }
+ </Form>
+ : null }
+ { teardown }
+ </>
+ );
+};
+
+function flatten_fields(fields) {
+ return fields.reduce(
+ (acc, val) => acc.concat([val]).concat(val.options && val.options.nested_fields
+ ? flatten_fields(val.options.nested_fields)
+ : []),
+ []);
+}
+
+export const dialog_open = (def) => {
+ const nested_fields = def.Fields || [];
+ const fields = flatten_fields(nested_fields);
+ const values = { };
+ let errors = null;
+
+ fields.forEach(f => { values[f.tag] = f.initial_value });
+
+ // We reconstruct the body every time the values change so that it
+ // will be re-rendered. This could be done with some state in the
+ // Body component maybe, but we also want the values up here so
+ // that we can pass them to validate and the action function.
+
+ const update = () => {
+ dlg.setProps(props());
+ };
+
+ const props = () => {
+ return {
+ id: "dialog",
+ title: def.Title,
+ titleIconVariant: (def.Action && (def.Action.Danger || def.Action.DangerButton)) ? "warning" : null,
+ body: <Body body={def.Body}
+ teardown={def.Teardown}
+ fields={nested_fields}
+ values={values}
+ errors={errors}
+ isFormHorizontal={def.isFormHorizontal}
+ onChange={trigger => {
+ errors = null;
+ if (def.update)
+ def.update(self, values, trigger);
+ update();
+ }} />
+ };
+ };
+
+ const update_footer = (running_title, running_promise) => {
+ dlg.setFooterProps(footer_props(running_title, running_promise));
+ };
+
+ function run_action(progress_callback, variant) {
+ const func = () => {
+ return validate(variant)
+ .then(() => {
+ const visible_values = { variant };
+ fields.forEach(f => {
+ if (is_visible(f, values))
+ visible_values[f.tag] = values[f.tag];
+ });
+ if (def.Action.wrapper)
+ return def.Action.wrapper(visible_values, progress_callback,
+ def.Action.action);
+ else
+ return def.Action.action(visible_values, progress_callback);
+ })
+ .catch(errs => {
+ if (errs && errs.toString() != "[object Object]") {
+ // Log errors from failed actions, for debugging and
+ // to allow the test suite to catch known issues.
+ console.warn(errs.toString());
+ }
+ errors = errs;
+ update();
+ update_footer();
+ return Promise.reject();
+ });
+ };
+ return client.run(func);
+ }
+
+ const footer_props = (running_title, running_promise) => {
+ const actions = [];
+
+ function add_action(variant) {
+ actions.push({
+ caption: variant.Title,
+ style: actions.length == 0 ? "primary" : "secondary",
+ danger: def.Action.Danger || def.Action.DangerButton,
+ disabled: running_promise != null || (def.Action.disable_on_error &&
+ errors && errors.toString() != "[object Object]"),
+ clicked: progress_callback => run_action(progress_callback, variant.tag),
+ });
+ }
+
+ if (def.Action) {
+ if (def.Action.Title) {
+ add_action({
+ Title: def.Action.Title,
+ tag: null,
+ });
+ }
+
+ if (def.Action.Variants) {
+ for (const v of def.Action.Variants) {
+ add_action(v);
+ }
+ }
+ }
+
+ const extra = (
+ <div>
+ { def.Action && def.Action.Danger
+ ? <HelperText><HelperTextItem variant="error">{def.Action.Danger} </HelperTextItem></HelperText>
+ : null
+ }
+ </div>);
+
+ return {
+ idle_message: (running_promise
+ ? <>
+ <span>{running_title}</span>
+ <Spinner className="dialog-wait-ct-spinner" size="md" />
+ </>
+ : null),
+ extra_element: extra,
+ actions,
+ cancel_button: def.Action ? {} : { text: _("Close"), variant: "secondary" }
+ };
+ };
+
+ const validate = (variant) => {
+ return Promise.all(fields.map(f => {
+ if (is_visible(f, values) && f.options && f.options.validate)
+ return f.options.validate(values[f.tag], values, variant);
+ else
+ return null;
+ })).then(results => {
+ const errors = { };
+ fields.forEach((f, i) => { if (results[i]) errors[f.tag] = results[i]; });
+ if (Object.keys(errors).length > 0)
+ return Promise.reject(errors);
+ });
+ };
+
+ const dlg = show_modal_dialog(props(), footer_props(null, null));
+
+ const self = {
+ run: (title, promise) => {
+ update_footer(title, promise);
+ promise.then(
+ () => {
+ update_footer(null, null);
+ },
+ (errs) => {
+ if (errs) {
+ errors = errs;
+ update();
+ }
+ update_footer(null, null);
+ });
+ },
+
+ set_values: (new_vals) => {
+ Object.assign(values, new_vals);
+ update();
+ },
+
+ get_value: (tag) => {
+ return values[tag];
+ },
+
+ update_actions: (new_actions) => {
+ Object.assign(def.Action, new_actions);
+ update_footer(null, null);
+ },
+
+ set_nested_values: (key, new_vals) => {
+ const updated = values[key];
+ Object.assign(updated, new_vals);
+ values[key] = updated;
+ update();
+ },
+
+ get_options: (tag) => {
+ for (const f of fields) {
+ if (f.tag == tag) {
+ return f.options;
+ }
+ }
+ },
+
+ set_options: (tag, new_options) => {
+ fields.forEach(f => {
+ if (f.tag == tag) {
+ Object.assign(f.options, new_options);
+ update();
+ }
+ });
+ },
+
+ set_attribute: (name, value) => {
+ def[name] = value;
+ update();
+ },
+
+ add_danger: (danger) => {
+ def.Action.Danger = <>{def.Action.Danger} {danger}</>;
+ update();
+ },
+
+ close: () => {
+ dlg.footerProps.dialog_done();
+ }
+ };
+
+ for_each_async(def.Inits || [],
+ init => {
+ if (init) {
+ const promise = init.func(self);
+ self.run(init.title, promise);
+ return promise;
+ } else
+ return Promise.resolve();
+ });
+
+ return self;
+};
+
+/* GENERIC FIELD TYPES
+ */
+
+export const TextInput = (tag, title, options) => {
+ return {
+ tag,
+ title,
+ options,
+ initial_value: options.value || "",
+
+ render: (val, change, validated) =>
+ <TextInputPF4 data-field={tag} data-field-type="text-input"
+ validated={validated}
+ aria-label={title}
+ value={val}
+ isDisabled={options.disabled}
+ onChange={(_event, value) => change(value)} />
+ };
+};
+
+const PassInputElement = ({ tag, title, options, val, change, validated }) => {
+ const [show, setShow] = useState(false);
+
+ return (
+ <InputGroup>
+ <TextInputPF4 data-field={tag} data-field-type="text-input"
+ validated={validated}
+ disabled={options.disabled}
+ aria-label={title}
+ autoComplete={options.new_password ? "new-password" : null}
+ type={show ? "text" : "password"}
+ value={val}
+ onChange={(_event, value) => change(value)} />
+ <Button variant="control"
+ onClick={() => setShow(!show)}
+ isDisabled={options.disabled}>
+ { show ? <EyeSlashIcon /> : <EyeIcon /> }
+ </Button>
+ </InputGroup>);
+};
+
+export const PassInput = (tag, title, options) => {
+ return {
+ tag,
+ title,
+ options,
+ initial_value: options.value || "",
+
+ render: (val, change, validated) =>
+ <PassInputElement tag={tag}
+ title={title}
+ options={options}
+ val={val}
+ change={change}
+ validated={validated} />
+ };
+};
+
+const TypeAheadSelectElement = ({ options, change }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [value, setValue] = useState(options.value);
+
+ return (
+ <TypeAheadSelect
+ variant="typeahead"
+ isCreatable
+ createText={_("Use")}
+ id="nfs-path-on-server"
+ isOpen={isOpen}
+ selections={value}
+ onToggle={(_event, isOpen) => setIsOpen(isOpen)}
+ onSelect={(event, value) => { setValue(value); change(value) }}
+ onClear={() => setValue(false)}
+ isDisabled={options.disabled}>
+ {options.choices.map(entry => <SelectOption key={entry} value={entry} />)}
+ </TypeAheadSelect>
+ );
+};
+
+export const ComboBox = (tag, title, options) => {
+ return {
+ tag,
+ title,
+ options,
+ initial_value: options.value || "",
+
+ render: (val, change, validated) => {
+ return <div data-field={tag} data-field-type="combobox">
+ <TypeAheadSelectElement options={options} change={change} />
+ </div>;
+ }
+ };
+};
+
+export const SelectOne = (tag, title, options) => {
+ return {
+ tag,
+ title,
+ options,
+ initial_value: options.value || options.choices[0].value,
+
+ render: (val, change, validated) => {
+ return (
+ <div data-field={tag} data-field-type="select" data-value={val}>
+ <FormSelect value={val} aria-label={tag}
+ validated={validated}
+ onChange={(_, value) => change(value)}>
+ { options.choices.map(c => <FormSelectOption value={c.value} isDisabled={c.disabled}
+ key={c.title} label={c.title} />) }
+ </FormSelect>
+ </div>
+ );
+ }
+ };
+};
+
+export const SelectOneRadio = (tag, title, options) => {
+ return {
+ tag,
+ title,
+ options,
+ initial_value: options.value || options.choices[0].value,
+ hasNoPaddingTop: true,
+
+ render: (val, change) => {
+ return (
+ <Split hasGutter data-field={tag} data-field-type="select-radio">
+ { options.choices.map(c => (
+ <Radio key={c.value} isChecked={val == c.value} data-data={c.value}
+ id={tag + '.' + c.value}
+ onChange={() => change(c.value)} label={c.title} />))
+ }
+ </Split>
+ );
+ }
+ };
+};
+
+export const SelectOneRadioVertical = (tag, title, options) => {
+ return {
+ tag,
+ title,
+ options,
+ initial_value: options.value || options.choices[0].value,
+ hasNoPaddingTop: true,
+
+ render: (val, change) => {
+ return (
+ <div data-field={tag} data-field-type="select-radio">
+ { options.choices.map(c => (
+ <Radio key={c.value} isChecked={val == c.value} data-data={c.value}
+ id={tag + '.' + c.value}
+ onChange={() => change(c.value)} label={c.title} />))
+ }
+ </div>
+ );
+ }
+ };
+};
+
+export const SelectRow = (tag, headers, options) => {
+ return {
+ tag,
+ title: null,
+ options,
+ initial_value: options.value || options.choices[0].value,
+
+ render: (val, change) => {
+ return (
+ <table data-field={tag} data-field-type=" select-row" className="dialog-select-row-table">
+ <thead>
+ <tr>{headers.map(h => <th key={h}>{h}</th>)}</tr>
+ </thead>
+ <tbody>
+ { options.choices.map(row => {
+ return (
+ <tr key={row.value}
+ onMouseDown={ev => { if (ev && ev.button === 0) change(row.value); }}
+ className={row.value == val ? "highlight-ct" : ""}>
+ {row.columns.map(c => <td key={c}>{c}</td>)}
+ </tr>
+ );
+ })
+ }
+ </tbody>
+ </table>
+ );
+ }
+ };
+};
+
+function nice_block_name(block) {
+ return block_name(client.blocks[block.CryptoBackingDevice] || block);
+}
+
+export const SelectSpaces = (tag, title, options) => {
+ return {
+ tag,
+ title,
+ options,
+ initial_value: options.value || [],
+ hasNoPaddingTop: options.spaces.length == 0,
+
+ render: (val, change) => {
+ if (options.spaces.length === 0)
+ return <span className="text-danger">{options.empty_warning}</span>;
+
+ return (
+ <DataList isCompact
+ data-field={tag} data-field-type="select-spaces">
+ { options.spaces.map(spc => {
+ const selected = (val.indexOf(spc) >= 0);
+ const block = spc.block ? nice_block_name(spc.block) : "";
+ const desc = block === spc.desc ? "" : spc.desc;
+
+ const on_change = (_event, checked) => {
+ // Be careful to keep "val" in the same order as "options.spaces".
+ if (checked && !selected)
+ change(options.spaces.filter(v => val.indexOf(v) >= 0 || v == spc));
+ else if (!checked && selected)
+ change(val.filter(v => (v != spc)));
+ };
+
+ const datalistcells = (
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell key="select-space-name" className="select-space-name">
+ {format_size_and_text(spc.size, desc)}
+ </DataListCell>,
+ <DataListCell alignRight isFilled={false} key="select-space-details" className="select-space-details">
+ {block}
+ </DataListCell>,
+ ]}
+ />);
+
+ return (
+ <DataListItem key={spc.block ? spc.block.Device : spc.desc}>
+ <DataListItemRow>
+ <DataListCheck id={(spc.block ? spc.block.Device : spc.desc) + "-row-checkbox"}
+ isDisabled={options.min_selected &&
+ selected && val.length <= options.min_selected}
+ isChecked={selected} onChange={on_change} />
+ <label htmlFor={(spc.block ? spc.block.Device : spc.desc) + "-row-checkbox"}
+ className='data-list-row-checkbox-label'>
+ {datalistcells}
+ </label>
+ </DataListItemRow>
+ </DataListItem>
+ );
+ })
+ }
+ </DataList>
+ );
+ }
+ };
+};
+
+export const SelectSpace = (tag, title, options) => {
+ return {
+ tag,
+ title,
+ options,
+ initial_value: null,
+
+ render: (val, change) => {
+ if (options.spaces.length === 0)
+ return <span className="text-danger">{options.empty_warning}</span>;
+
+ return (
+ <DataList isCompact
+ data-field={tag} data-field-type="select-spaces">
+ { options.spaces.map(spc => {
+ const block = spc.block ? nice_block_name(spc.block) : "";
+ const desc = block === spc.desc ? "" : spc.desc;
+ const on_change = (event) => {
+ if (event.target.checked)
+ change(spc);
+ };
+
+ return (
+ <DataListItem key={spc.block ? spc.block.Device : spc.desc}>
+ <DataListItemRow>
+ <div className="pf-v5-c-data-list__item-control">
+ <div className="pf-v5-c-data-list__check">
+ <input type='radio' value={desc} name='space' checked={val == spc} onChange={on_change} />
+ </div>
+ </div>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell key="select-space-name" className="select-space-name">
+ {format_size_and_text(spc.size, desc)}
+ </DataListCell>,
+ <DataListCell alignRight isFilled={false} key="select-space-details" className="select-space-details">
+ {block}
+ </DataListCell>,
+ ]}
+ />
+ </DataListItemRow>
+ </DataListItem>
+ );
+ })
+ }
+ </DataList>
+ );
+ }
+ };
+};
+
+const CheckBoxComponent = ({ tag, val, title, tooltip, update_function }) => {
+ return (
+ <Checkbox data-field={tag} data-field-type="checkbox"
+ id={tag}
+ isChecked={val}
+ label={
+ <>
+ {title}
+ { tooltip && <Popover bodyContent={tooltip}>
+ <Button className="dialog-item-tooltip" variant="link">
+ <HelpIcon />
+ </Button>
+ </Popover>
+ }
+ </>
+ }
+ onChange={(_, v) => update_function(v)} />
+ );
+};
+
+export const CheckBoxes = (tag, title, options) => {
+ return {
+ tag,
+ title,
+ options,
+ initial_value: options.value || { },
+ hasNoPaddingTop: true,
+
+ render: (val, change) => {
+ const fieldset = options.fields.map(field => {
+ const ftag = tag + "." + field.tag;
+ const fval = (val[field.tag] !== undefined) ? val[field.tag] : false;
+ function fchange(newval) {
+ val[field.tag] = newval;
+ change(val);
+ }
+
+ if (field.type === undefined || field.type == "checkbox")
+ return <CheckBoxComponent key={`checkbox-${ftag}`}
+ tag={ftag}
+ val={fval}
+ title={field.title}
+ tooltip={field.tooltip}
+ options={options}
+ update_function={fchange} />;
+ else if (field.type == "checkboxWithInput")
+ return <TextInputCheckedComponent key={`checkbox-with-text-${ftag}`}
+ tag={ftag}
+ val={fval}
+ title={field.title}
+ update_function={fchange} />;
+ else
+ return null;
+ });
+
+ if (options.fields.length == 1)
+ return fieldset;
+
+ return <>{ fieldset }</>;
+ }
+ };
+};
+
+const TextInputCheckedComponent = ({ tag, val, title, update_function }) => {
+ return (
+ <div data-field={tag} data-field-type="text-input-checked" key={tag}>
+ <Checkbox isChecked={val !== false}
+ id={tag}
+ label={title}
+ onChange={(_event, checked) => update_function(checked ? "" : false)} />
+ {val !== false && <TextInputPF4 id={tag + "-input"} value={val} onChange={(_event, value) => update_function(value)} />}
+ </div>
+ );
+};
+
+export const Skip = (className, options) => {
+ return {
+ tag: false,
+ title: null,
+ options,
+ initial_value: false,
+
+ render: () => {
+ return <div className={className} />;
+ }
+ };
+};
+
+export const Message = (text, options) => {
+ return {
+ options,
+
+ render: () => <HelperText><HelperTextItem icon={<InfoIcon />}>{text}</HelperTextItem></HelperText>,
+ };
+};
+
+function size_slider_round(value, round) {
+ if (round) {
+ if (typeof round == "function")
+ value = round(value);
+ else
+ value = Math.round(value / round) * round;
+ } else {
+ // Only produce integers by default
+ value = Math.round(value);
+ }
+ return value;
+}
+
+class SizeSliderElement extends React.Component {
+ constructor(props) {
+ super();
+ this.units = cockpit.get_byte_units(props.value || props.max);
+ this.state = { unit: this.units.find(u => u.selected).factor };
+ }
+
+ render() {
+ const { val, max, round, onChange, tag } = this.props;
+ const min = this.props.min || 0;
+ const { unit } = this.state;
+
+ const change_slider = (_event, f) => {
+ onChange(Math.max(min, size_slider_round(f, round)));
+ };
+
+ const change_text = (value) => {
+ /* We keep the literal string as the value and only
+ * interpret it below in the validate function inside
+ * SizeSlider. This allows people to freely interact with
+ * the text input without getting the text changed all the
+ * time by rounding, etc.
+ */
+ onChange({ text: value, unit });
+ };
+
+ let slider_val, text_val;
+ if (val.text && val.unit) {
+ slider_val = Number(val.text) * val.unit;
+ text_val = val.text;
+ } else {
+ slider_val = val;
+ text_val = cockpit.format_number(val / unit);
+ }
+
+ const change_unit = (_, u) => {
+ if (val.unit)
+ onChange({ text: val.text, unit: Number(u) });
+ else
+ onChange(val / unit * Number(u));
+ this.setState({ unit: Number(u) });
+ };
+
+ return (
+ <Grid hasGutter className="size-slider">
+ <GridItem span={12} sm={8}>
+ <Slider showBoundaries={false} min={min} max={max} step={(max - min) / 500}
+ value={slider_val} onChange={change_slider} />
+ </GridItem>
+ <GridItem span={6} sm={2}>
+ <TextInputPF4 className="size-text" value={text_val} onChange={(_event, value) => change_text(value)} />
+ </GridItem>
+ <GridItem span={6} sm={2}>
+ <FormSelect className="size-unit" value={unit} aria-label={tag} onChange={change_unit}>
+ { this.units.map(u => <FormSelectOption value={u.factor} key={u.name} label={u.name} />) }
+ </FormSelect>
+ </GridItem>
+ </Grid>
+ );
+ }
+}
+
+export const SizeSlider = (tag, title, options) => {
+ const validate = (val, vals) => {
+ let msg = null;
+
+ if (val.text && val.unit) {
+ // Convert to number.
+ const unit = val.unit;
+
+ val = Number(val.text) * unit;
+
+ // As a special case, if the user types something that
+ // looks like the maximum (or minimum) when formatted,
+ // always use exactly the maximum (or minimum). Otherwise
+ // we have the confusing possibility that with the exact
+ // same string in the text input, the size is sometimes
+ // too large (or too small) and sometimes not.
+
+ const sanitize = (limit) => {
+ const fmt = cockpit.format_number(limit / unit);
+ const parse = +fmt * unit;
+
+ if (val == parse)
+ val = limit;
+ };
+
+ sanitize(all_options.min || 0);
+ sanitize(all_options.max);
+
+ val = size_slider_round(val, all_options.round);
+ vals[tag] = val;
+ }
+
+ if (isNaN(val))
+ msg = _("Size must be a number");
+ else if (val === 0)
+ msg = _("Size cannot be zero");
+ else if (val < 0)
+ msg = _("Size cannot be negative");
+ else if (!options.allow_infinite && val > options.max)
+ msg = _("Size is too large");
+ else if (options.min !== undefined && val < options.min)
+ msg = cockpit.format(_("Size must be at least $0"), fmt_size(options.min));
+ else if (options.validate)
+ msg = options.validate(val, vals);
+
+ return msg;
+ };
+
+ /* This object might be mutated by dialog.set_options(), so we
+ have to use it below for the 'max' option in order to pick up
+ changes to it.
+ */
+ const all_options = Object.assign({ }, options, { validate });
+
+ return {
+ tag,
+ title,
+ options: all_options,
+ initial_value: options.value || options.max || 0,
+
+ render: (val, change) => {
+ return (
+ <div data-field={tag} data-field-type="size-slider">
+ <SizeSliderElement val={val}
+ max={all_options.max}
+ min={all_options.min}
+ round={all_options.round}
+ tag={tag}
+ onChange={change} />
+ </div>
+ );
+ }
+ };
+};
+
+export const BlockingMessage = (usage) => {
+ const usage_desc = {
+ pvol: _("physical volume of LVM2 volume group"),
+ "mdraid-member": _("member of MDRAID device"),
+ vdo: _("backing device for VDO device"),
+ "stratis-pool-member": _("member of Stratis pool"),
+ mounted: _("Filesystem outside the target"),
+ "btrfs-device": _("device of btrfs volume"),
+ };
+
+ const rows = [];
+ usage.forEach(use => {
+ if (use.blocking && use.block) {
+ const name = teardown_block_name(use);
+ rows.push({
+ columns: [name, use.location || "-", usage_desc[use.usage] || "-"]
+ });
+ }
+ });
+
+ return (
+ <div>
+ <HelperText><HelperTextItem variant="warning">{_("This device is currently in use.")}</HelperTextItem></HelperText>
+ <ListingTable variant='compact'
+ columns={[
+ { title: _("Device") },
+ { title: _("Location") },
+ { title: _("Use") }
+ ]}
+ rows={rows} />
+ </div>);
+};
+
+const UsersPopover = ({ users }) => {
+ const max = 10;
+ const services = users.filter(u => u.unit);
+ const processes = users.filter(u => u.pid);
+
+ return (
+ <Popover
+ appendTo={document.body}
+ bodyContent={
+ <>
+ { services.length > 0
+ ? <>
+ <p><b>{_("Services using the location")}</b></p>
+ <List>
+ { services.slice(0, max).map((u, i) => <ListItem key={i}>{u.unit.replace(/\.service$/, "")}</ListItem>) }
+ { services.length > max ? <ListItem key={max}>...</ListItem> : null }
+ </List>
+ </>
+ : null
+ }
+ { services.length > 0 && processes.length > 0
+ ? <br />
+ : null
+ }
+ { processes.length > 0
+ ? <>
+ <p><b>{_("Processes using the location")}</b></p>
+ <List>
+ { processes.slice(0, max).map((u, i) => <ListItem key={i}>{u.comm} (user: {u.user}, pid: {u.pid})</ListItem>) }
+ { processes.length > max ? <ListItem key={max}>...</ListItem> : null }
+ </List>
+ </>
+ : null
+ }
+ </>}>
+ <Button variant="link" style={{ visibility: users.length == 0 ? "hidden" : null }}>
+ <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" /> { "\n" }
+ {_("Currently in use")}
+ </Button>
+ </Popover>);
+};
+
+function is_expected_unmount(usage, expect_single_unmount) {
+ return (expect_single_unmount && usage.length == 1 &&
+ usage[0].usage == "mounted" && usage[0].location == expect_single_unmount);
+}
+
+const teardown_block_name = use => {
+ const block_stratis = client.blocks_stratis_fsys[use.block.path];
+ const block_btrfs = client.blocks_fsys_btrfs[use.block.path];
+ let name;
+ if (block_stratis) {
+ name = block_stratis.Devnode;
+ } else if (block_btrfs && use.name) {
+ name = use.name;
+ } else {
+ name = block_name(client.blocks[use.block.CryptoBackingDevice] || use.block);
+ }
+
+ return name;
+};
+
+export const TeardownMessage = (usage, expect_single_unmount) => {
+ if (usage.length == 0)
+ return null;
+
+ if (is_expected_unmount(usage, expect_single_unmount))
+ return <StopProcessesMessage mount_point={expect_single_unmount} users={usage[0].users} />;
+
+ const rows = [];
+ usage.forEach((use, index) => {
+ if (use.block) {
+ const name = teardown_block_name(use);
+ let location = use.location;
+ if (use.usage == "mounted") {
+ location = client.strip_mount_point_prefix(location);
+ if (location === false)
+ location = _("(Not part of target)");
+ }
+ rows.push({
+ columns: [name,
+ location || "-",
+ use.actions.length ? use.actions.join(", ") : "-",
+ {
+ title: <UsersPopover users={use.users || []} />,
+ props: { className: "pf-v5-u-text-align-right" }
+ }
+ ]
+ });
+ }
+ });
+
+ return (
+ <div className="modal-footer-teardown">
+ <p>{_("These changes will be made:")}</p>
+ <ListingTable variant='compact'
+ columns={[
+ { title: _("Device") },
+ { title: _("Location") },
+ { title: _("Action") },
+ { title: "" }
+ ]}
+ rows={rows} />
+ </div>);
+};
+
+export function teardown_danger_message(usage, expect_single_unmount) {
+ if (is_expected_unmount(usage, expect_single_unmount))
+ return stop_processes_danger_message(usage[0].users);
+
+ const usage_with_users = usage.filter(u => u.users);
+ const n_processes = usage_with_users.reduce((sum, u) => sum + u.users.filter(u => u.pid).length, 0);
+ const n_services = usage_with_users.reduce((sum, u) => sum + u.users.filter(u => u.unit).length, 0);
+ if (n_processes > 0 && n_services > 0) {
+ return _("Related processes and services will be forcefully stopped.");
+ } else if (n_processes > 0) {
+ return _("Related processes will be forcefully stopped.");
+ } else if (n_services > 0) {
+ return _("Related services will be forcefully stopped.");
+ } else {
+ return null;
+ }
+}
+
+export function init_active_usage_processes(client, usage, expect_single_unmount) {
+ return {
+ title: _("Checking related processes"),
+ func: dlg => {
+ return for_each_async(usage, u => {
+ if (u.usage == "mounted") {
+ return client.find_mount_users(u.location)
+ .then(users => {
+ u.users = users;
+ });
+ } else
+ return Promise.resolve();
+ }).then(() => {
+ dlg.set_attribute("Teardown", TeardownMessage(usage, expect_single_unmount));
+ const msg = teardown_danger_message(usage, expect_single_unmount);
+ if (msg)
+ dlg.add_danger(msg);
+ });
+ }
+ };
+}
+
+export const StopProcessesMessage = ({ mount_point, users }) => {
+ if (!users || users.length == 0)
+ return null;
+
+ const process_rows = users.filter(u => u.pid).map(u => {
+ return {
+ columns: [
+ u.pid,
+ { title: u.cmd.substr(0, 100), props: { modifier: "breakWord" } },
+ u.user || "-",
+ { title: format_delay(-u.since * 1000), props: { modifier: "nowrap" } }
+ ]
+ };
+ });
+
+ const service_rows = users.filter(u => u.unit).map(u => {
+ return {
+ columns: [
+ { title: u.unit.replace(/\.service$/, ""), props: { modifier: "breakWord" } },
+ { title: u.cmd.substr(0, 100), props: { modifier: "breakWord" } },
+ { title: u.desc || "", props: { modifier: "breakWord" } },
+ { title: format_delay(-u.since * 1000), props: { modifier: "nowrap" } }
+ ]
+ };
+ });
+
+ // If both tables are shown, we press the columns into a uniform
+ // width to reduce the visual mess.
+ const colprops = (process_rows.length > 0 && service_rows.length > 0) ? { width: 25 } : { };
+
+ return (
+ <div className="modal-footer-teardown">
+ { process_rows.length > 0
+ ? <>
+ <p>{fmt_to_fragments(_("The mount point $0 is in use by these processes:"), <b>{mount_point}</b>)}</p>
+ <ListingTable variant='compact'
+ columns={
+ [
+ { title: _("PID"), props: colprops },
+ { title: _("Command"), props: colprops },
+ { title: _("User"), props: colprops },
+ { title: _("Runtime"), props: colprops }
+ ]
+ }
+ rows={process_rows} />
+ </>
+ : null
+ }
+ { process_rows.length > 0 && service_rows.length > 0
+ ? <br />
+ : null
+ }
+ { service_rows.length > 0
+ ? <>
+ <p>{fmt_to_fragments(_("The mount point $0 is in use by these services:"), <b>{mount_point}</b>)}</p>
+ <ListingTable variant='compact'
+ columns={
+ [
+ { title: _("Service"), props: colprops },
+ { title: _("Command"), props: colprops },
+ { title: _("Description"), props: colprops },
+ { title: _("Runtime"), props: colprops }
+ ]
+ }
+ rows={service_rows} />
+ </>
+ : null
+ }
+ </div>);
+};
+
+export const stop_processes_danger_message = (users) => {
+ const n_processes = users.filter(u => u.pid).length;
+ const n_services = users.filter(u => u.unit).length;
+
+ if (n_processes > 0 && n_services > 0)
+ return _("The listed processes and services will be forcefully stopped.");
+ else if (n_processes > 0)
+ return _("The listed processes will be forcefully stopped.");
+ else if (n_services > 0)
+ return _("The listed services will be forcefully stopped.");
+ else
+ return null;
+};
diff --git a/pkg/storaged/drive/drive.jsx b/pkg/storaged/drive/drive.jsx
new file mode 100644
index 0000000..c6fdd96
--- /dev/null
+++ b/pkg/storaged/drive/drive.jsx
@@ -0,0 +1,142 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+
+import { HDDIcon, SSDIcon, MediaDriveIcon } from "../icons/gnome-icons.jsx";
+import { StorageCard, StorageDescription, new_card, new_page } from "../pages.jsx";
+import { block_name, drive_name, format_temperature, fmt_size_long, should_ignore } from "../utils.js";
+import { make_block_page } from "../block/create-pages.jsx";
+import { partitionable_block_actions } from "../partitions/actions.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_drive_page(parent, drive) {
+ let block = client.drives_block[drive.path];
+
+ if (!block) {
+ // A drive without a primary block device might be
+ // a unconfigured multipath device. Try to hobble
+ // along here by arbitrarily picking one of the
+ // multipath devices.
+ block = client.drives_multipath_blocks[drive.path][0];
+ }
+
+ if (!block)
+ return;
+
+ if (should_ignore(client, block.path))
+ return;
+
+ let cls;
+ if (client.drives_iscsi_session[drive.path])
+ cls = "iscsi";
+ else if (drive.MediaRemovable || drive.Media)
+ cls = "media";
+ else
+ cls = (drive.RotationRate === 0) ? "ssd" : "hdd";
+
+ const drive_title = {
+ media: _("Media drive"),
+ ssd: _("Solid State Drive"),
+ hdd: _("Hard Disk Drive"),
+ iscsi: _("iSCSI Drive"),
+ };
+
+ const drive_icon = {
+ media: MediaDriveIcon,
+ ssd: SSDIcon,
+ hdd: HDDIcon,
+ };
+
+ const drive_card = new_card({
+ title: drive_title[cls] || _("Drive"),
+ next: null,
+ page_block: block,
+ page_icon: drive_icon[cls],
+ for_summary: true,
+ id_extra: drive_name(drive),
+ job_path: drive.path,
+ component: DriveCard,
+ props: { drive },
+ actions: block.Size > 0 ? partitionable_block_actions(block) : [],
+ });
+
+ if (block.Size > 0) {
+ make_block_page(parent, block, drive_card);
+ } else {
+ new_page(parent, drive_card);
+ }
+}
+
+const DriveCard = ({ card, page, drive }) => {
+ const block = client.drives_block[drive.path];
+ const drive_ata = client.drives_ata[drive.path];
+ const multipath_blocks = client.drives_multipath_blocks[drive.path];
+
+ let assessment = null;
+ if (drive_ata) {
+ assessment = (
+ <StorageDescription title={_("Assessment")}>
+ <Flex spaceItems={{ default: 'spaceItemsXs' }}>
+ { drive_ata.SmartFailing
+ ? <span className="cockpit-disk-failing">{_("Disk is failing")}</span>
+ : <span>{_("Disk is OK")}</span>
+ }
+ { drive_ata.SmartTemperature > 0
+ ? <span>({format_temperature(drive_ata.SmartTemperature)})</span>
+ : null
+ }
+ </Flex>
+ </StorageDescription>);
+ }
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Vendor")} value={drive.Vendor} />
+ <StorageDescription title={_("Model")} value={drive.Model} />
+ <StorageDescription title={_("Firmware version")} value={drive.Revision} />
+ <StorageDescription title={_("Serial number")} value={drive.Serial} />
+ <StorageDescription title={_("World wide name")} value={drive.WWN} />
+ <StorageDescription title={_("Capacity")}>
+ {drive.Size
+ ? fmt_size_long(drive.Size)
+ : _("No media inserted")
+ }
+ </StorageDescription>
+ { assessment }
+ <StorageDescription title={_("Device file")}
+ value={block ? block_name(block) : "-"} />
+ { multipath_blocks.length > 0 &&
+ <StorageDescription title={_("Multipathed devices")}
+ value={multipath_blocks.map(block_name).join(" ")} />
+ }
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/filesystem/filesystem.jsx b/pkg/storaged/filesystem/filesystem.jsx
new file mode 100644
index 0000000..ff8b70f
--- /dev/null
+++ b/pkg/storaged/filesystem/filesystem.jsx
@@ -0,0 +1,166 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { useEvent } from "hooks";
+
+import {
+ block_name, fmt_size, validate_fsys_label
+} from "../utils.js";
+import {
+ dialog_open, TextInput,
+} from "../dialog.jsx";
+import { StorageLink, StorageUsageBar, StorageSize } from "../storage-controls.jsx";
+import { StorageCard, StorageDescription, new_card, useIsNarrow } from "../pages.jsx";
+
+import { format_dialog } from "../block/format-dialog.jsx";
+import { is_mounted, MountPoint, mount_point_text, edit_mount_point } from "./utils.jsx";
+import { mounting_dialog } from "./mounting-dialog.jsx";
+import { check_mismounted_fsys, MismountAlert } from "./mismounting.jsx";
+
+const _ = cockpit.gettext;
+
+/* This page is used in a variety of cases, which can be distinguished
+ * by looking at the "backing_block" and "content_block" parameters:
+ *
+ * not-encrypted: backing_block == content_block,
+ * content_block != null
+ *
+ * encrypted and unlocked: backing_block != content_block,
+ * backing_block != null,
+ * content_block != null
+ *
+ * encrypted and locked: backing_block != null,
+ * content_block == null
+ *
+ * "backing_block" is always non-null and always refers to the block
+ * device that we want to talk about in the UI. "content_block" (when
+ * non-null) is the block that we need to use for filesystem related
+ * actions, such as mounting. It's the one with the
+ * "o.fd.UDisks2.Filesystem" interface.
+ *
+ * When "content_block" is null, then "backing_block" is a locked LUKS
+ * device, but we could figure out the fstab entry for the filesystem
+ * that's on it.
+ */
+
+const MountPointUsageBar = ({ mount_point, block, short }) => {
+ useEvent(client.fsys_sizes, "changed");
+ const narrow = useIsNarrow();
+
+ const stats = client.fsys_sizes.data[mount_point];
+ if (stats)
+ return <StorageUsageBar stats={stats} critical={0.95} block={block_name(block)} short={short} />;
+ else if (short && !narrow)
+ return <StorageSize size={block.Size} />;
+ else
+ return fmt_size(block.Size);
+};
+
+export function make_filesystem_card(next, backing_block, content_block, fstab_config) {
+ const [, mount_point] = fstab_config;
+ const mismount_warning = check_mismounted_fsys(backing_block, content_block, fstab_config);
+ const mounted = content_block && is_mounted(client, content_block);
+
+ const mp_text = mount_point_text(mount_point, mounted);
+ if (mp_text == null)
+ return null;
+
+ return new_card({
+ title: content_block ? cockpit.format(_("$0 filesystem"), content_block.IdType) : _("Filesystem"),
+ location: mp_text,
+ next,
+ page_size: <MountPointUsageBar mount_point={mount_point} block={backing_block} short />,
+ has_warning: !!mismount_warning,
+ component: FilesystemCard,
+ props: { backing_block, content_block, fstab_config, mismount_warning },
+ actions: [
+ client.in_anaconda_mode() &&
+ { title: _("Edit mount point"), action: () => edit_mount_point(content_block || backing_block) },
+ content_block && mounted
+ ? { title: _("Unmount"), action: () => mounting_dialog(client, content_block, "unmount") }
+ : { title: _("Mount"), action: () => mounting_dialog(client, content_block || backing_block, "mount") },
+ { title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true },
+ ]
+ });
+}
+
+export const FilesystemCard = ({
+ card, backing_block, content_block, fstab_config, mismount_warning
+}) => {
+ function rename_dialog() {
+ // assert(content_block)
+ const block_fsys = client.blocks_fsys[content_block.path];
+
+ dialog_open({
+ Title: _("Filesystem name"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ validate: name => validate_fsys_label(name, content_block.IdType),
+ value: content_block.IdLabel
+ })
+ ],
+ Action: {
+ Title: _("Save"),
+ action: function (vals) {
+ return block_fsys.SetLabel(vals.name, {});
+ }
+ }
+ });
+ }
+
+ const [, mount_point] = fstab_config;
+ const mounted = content_block && is_mounted(client, content_block);
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Name")}
+ value={content_block?.IdLabel || "-"}
+ action={<StorageLink onClick={rename_dialog}
+ excuse={!content_block ? _("Filesystem is locked") : null}>
+ {_("edit")}
+ </StorageLink>} />
+ <StorageDescription title={_("Mount point")}>
+ <MountPoint fstab_config={fstab_config}
+ backing_block={backing_block} content_block={content_block} />
+ </StorageDescription>
+ { mounted &&
+ <StorageDescription title={_("Usage")}>
+ <MountPointUsageBar mount_point={mount_point} block={backing_block} />
+ </StorageDescription>
+ }
+ </DescriptionList>
+ </CardBody>
+ { mismount_warning &&
+ <CardBody>
+ <MismountAlert warning={mismount_warning}
+ fstab_config={fstab_config}
+ backing_block={backing_block} content_block={content_block} />
+ </CardBody>
+ }
+ </StorageCard>);
+};
diff --git a/pkg/storaged/filesystem/mismounting.jsx b/pkg/storaged/filesystem/mismounting.jsx
new file mode 100644
index 0000000..0fb5fb7
--- /dev/null
+++ b/pkg/storaged/filesystem/mismounting.jsx
@@ -0,0 +1,231 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client.js";
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+
+import {
+ encode_filename,
+ parse_options, unparse_options, extract_option, reload_systemd,
+ set_crypto_auto_option, get_mount_points,
+} from "../utils.js";
+import { StorageButton } from "../storage-controls.jsx";
+
+import { mounting_dialog } from "./mounting-dialog.jsx";
+import { get_cryptobacking_noauto } from "./utils.jsx";
+
+const _ = cockpit.gettext;
+
+export function check_mismounted_fsys(backing_block, content_block, fstab_config, subvol) {
+ if (client.in_anaconda_mode())
+ return;
+
+ const block_fsys = content_block && client.blocks_fsys[content_block.path];
+ const [, dir, opts] = fstab_config;
+
+ if (!(block_fsys || dir))
+ return;
+
+ const mounted_at = get_mount_points(client, block_fsys, subvol);
+ const split_options = parse_options(opts);
+ const opt_noauto = extract_option(split_options, "noauto");
+ const opt_noauto_intent = extract_option(split_options, "x-cockpit-never-auto");
+ const opt_systemd_automount = split_options.indexOf("x-systemd.automount") >= 0;
+ const is_mounted = mounted_at.indexOf(dir) >= 0;
+ const other_mounts = mounted_at.filter(m => m != dir);
+ const crypto_backing_noauto = get_cryptobacking_noauto(client, backing_block);
+
+ let type;
+ if (dir) {
+ if (!is_mounted && other_mounts.length > 0) {
+ if (!opt_noauto)
+ type = "change-mount-on-boot";
+ else
+ type = "mounted-no-config";
+ } else if (crypto_backing_noauto && !opt_noauto)
+ type = "locked-on-boot-mount";
+ else if (!is_mounted && !opt_noauto)
+ type = "mount-on-boot";
+ else if (is_mounted && opt_noauto && !opt_noauto_intent && !opt_systemd_automount)
+ type = "no-mount-on-boot";
+ } else if (other_mounts.length > 0) {
+ // We don't complain about the rootfs, it's probably
+ // configured somewhere else, like in the bootloader.
+ if (other_mounts[0] != "/")
+ type = "mounted-no-config";
+ }
+
+ if (type)
+ return { warning: "mismounted-fsys", type, other: other_mounts[0] };
+}
+
+export const MismountAlert = ({ warning, fstab_config, forced_options, backing_block, content_block, subvol }) => {
+ if (!warning)
+ return null;
+
+ const { type, other } = warning;
+ const [old_config, old_dir, old_opts, old_parents] = fstab_config;
+ const split_options = parse_options(old_opts);
+ extract_option(split_options, "noauto");
+ const opt_ro = extract_option(split_options, "ro");
+ const opt_nofail = extract_option(split_options, "nofail");
+ const opt_netdev = extract_option(split_options, "_netdev");
+ const split_options_for_fix_config = split_options.slice();
+ if (forced_options)
+ for (const opt of forced_options)
+ extract_option(split_options, opt);
+
+ function fix_config() {
+ let opts = [];
+ if (type == "mount-on-boot")
+ opts.push("noauto");
+ if (type == "locked-on-boot-mount") {
+ opts.push("noauto");
+ opts.push("x-cockpit-never-auto");
+ }
+ if (opt_ro)
+ opts.push("ro");
+ if (opt_nofail)
+ opts.push("nofail");
+ if (opt_netdev)
+ opts.push("_netdev");
+ if (subvol) {
+ opts.push(`subvol=${subvol.pathname}`);
+ }
+
+ // Add the forced options, but only to new entries. We
+ // don't want to modify existing entries beyond what we
+ // say on the button.
+ if (!old_config && forced_options)
+ opts = opts.concat(forced_options);
+
+ const new_opts = unparse_options(opts.concat(split_options_for_fix_config));
+ let all_new_opts;
+ if (new_opts && old_parents)
+ all_new_opts = new_opts + "," + old_parents;
+ else if (new_opts)
+ all_new_opts = new_opts;
+ else
+ all_new_opts = old_parents;
+
+ let new_dir = old_dir;
+ if (type == "change-mount-on-boot" || type == "mounted-no-config")
+ new_dir = other;
+
+ const new_config = [
+ "fstab", {
+ fsname: old_config ? old_config[1].fsname : undefined,
+ dir: { t: 'ay', v: encode_filename(new_dir) },
+ type: { t: 'ay', v: encode_filename("auto") },
+ opts: { t: 'ay', v: encode_filename(all_new_opts || "defaults") },
+ freq: { t: 'i', v: 0 },
+ passno: { t: 'i', v: 0 },
+ "track-parents": { t: 'b', v: !old_config }
+ }];
+
+ function fixup_crypto_backing() {
+ if (!backing_block)
+ return;
+ if (type == "no-mount-on-boot")
+ return set_crypto_auto_option(backing_block, true);
+ if (type == "locked-on-boot-mount")
+ return set_crypto_auto_option(backing_block, false);
+ }
+
+ function fixup_fsys() {
+ if (old_config)
+ return backing_block.UpdateConfigurationItem(old_config, new_config, {}).then(reload_systemd);
+ else
+ return backing_block.AddConfigurationItem(new_config, {}).then(reload_systemd);
+ }
+
+ return fixup_fsys().then(fixup_crypto_backing);
+ }
+
+ function fix_mount() {
+ const crypto_backing_crypto = client.blocks_crypto[backing_block.path];
+
+ function do_mount() {
+ if (!content_block)
+ mounting_dialog(client, backing_block, "mount", forced_options);
+ else
+ return client.mount_at(content_block, old_dir);
+ }
+
+ function do_unmount(dir) {
+ return client.unmount_at(dir)
+ .then(() => {
+ if (backing_block != content_block)
+ return crypto_backing_crypto.Lock({});
+ });
+ }
+
+ if (type == "change-mount-on-boot")
+ return client.unmount_at(other).then(() => client.mount_at(content_block, old_dir));
+ else if (type == "mount-on-boot")
+ return do_mount();
+ else if (type == "no-mount-on-boot")
+ return do_unmount(old_dir);
+ else if (type == "mounted-no-config")
+ return do_unmount(other);
+ else if (type == "locked-on-boot-mount") {
+ if (backing_block != content_block)
+ return set_crypto_auto_option(backing_block, true);
+ }
+ }
+
+ let text;
+ let fix_config_text;
+ let fix_mount_text;
+
+ if (type == "change-mount-on-boot") {
+ text = cockpit.format(_("The filesystem is currently mounted on $0 but will be mounted on $1 on the next boot."), other, old_dir);
+ fix_config_text = cockpit.format(_("Mount automatically on $0 on boot"), other);
+ fix_mount_text = cockpit.format(_("Mount on $0 now"), old_dir);
+ } else if (type == "mount-on-boot") {
+ text = _("The filesystem is currently not mounted but will be mounted on the next boot.");
+ fix_config_text = _("Do not mount automatically on boot");
+ fix_mount_text = _("Mount now");
+ } else if (type == "no-mount-on-boot") {
+ text = _("The filesystem is currently mounted but will not be mounted after the next boot.");
+ fix_config_text = _("Mount also automatically on boot");
+ fix_mount_text = _("Unmount now");
+ } else if (type == "mounted-no-config") {
+ text = cockpit.format(_("The filesystem is currently mounted on $0 but will not be mounted after the next boot."), other);
+ fix_config_text = cockpit.format(_("Mount automatically on $0 on boot"), other);
+ fix_mount_text = _("Unmount now");
+ } else if (type == "locked-on-boot-mount") {
+ text = _("The filesystem is configured to be automatically mounted on boot but its encryption container will not be unlocked at that time.");
+ fix_config_text = _("Do not mount automatically on boot");
+ fix_mount_text = _("Unlock automatically on boot");
+ }
+
+ return (
+ <Alert variant="warning" isInline
+ title={_("Inconsistent filesystem mount")}>
+ {text}
+ <div className="storage-alert-actions">
+ <StorageButton onClick={fix_config}>{fix_config_text}</StorageButton>
+ { fix_mount_text && <StorageButton onClick={fix_mount}>{fix_mount_text}</StorageButton> }
+ </div>
+ </Alert>);
+};
diff --git a/pkg/storaged/filesystem/mounting-dialog.jsx b/pkg/storaged/filesystem/mounting-dialog.jsx
new file mode 100644
index 0000000..f70e8a8
--- /dev/null
+++ b/pkg/storaged/filesystem/mounting-dialog.jsx
@@ -0,0 +1,362 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import client from "../client.js";
+
+import {
+ encode_filename,
+ parse_options, unparse_options, extract_option, reload_systemd,
+ set_crypto_options, is_mounted_synch,
+ get_active_usage, teardown_active_usage,
+} from "../utils.js";
+
+import {
+ dialog_open,
+ TextInput, PassInput, CheckBoxes, SelectOne,
+ TeardownMessage,
+ init_active_usage_processes
+} from "../dialog.jsx";
+import { init_existing_passphrase, unlock_with_type } from "../crypto/keyslots.jsx";
+import { initial_tab_options, mount_explanation } from "../block/format-dialog.jsx";
+
+import {
+ is_mounted, get_fstab_config,
+ is_valid_mount_point
+} from "./utils.jsx";
+
+const _ = cockpit.gettext;
+
+export const mount_options = (opt_ro, extra_options, is_visible) => {
+ return CheckBoxes("mount_options", _("Mount options"),
+ {
+ visible: vals => !client.in_anaconda_mode() && (!is_visible || is_visible(vals)),
+ value: {
+ ro: opt_ro,
+ extra: extra_options || false
+ },
+ fields: [
+ { title: _("Mount read only"), tag: "ro" },
+ { title: _("Custom mount options"), tag: "extra", type: "checkboxWithInput" },
+ ]
+ });
+};
+
+export const at_boot_input = (at_boot, is_visible) => {
+ return SelectOne("at_boot", _("At boot"),
+ {
+ visible: vals => !client.in_anaconda_mode() && (!is_visible || is_visible(vals)),
+ value: at_boot,
+ explanation: mount_explanation[at_boot],
+ choices: [
+ {
+ value: "local",
+ title: _("Mount before services start"),
+ },
+ {
+ value: "nofail",
+ title: _("Mount without waiting, ignore failure"),
+ },
+ {
+ value: "netdev",
+ title: _("Mount after network becomes available, ignore failure"),
+ },
+ {
+ value: "never",
+ title: _("Do not mount"),
+ },
+ ]
+ });
+};
+
+export function mounting_dialog(client, block, mode, forced_options, subvol) {
+ const block_fsys = client.blocks_fsys[block.path];
+ const [old_config, old_dir, old_opts, old_parents] = get_fstab_config(block, true, subvol);
+ const options = old_config ? old_opts : initial_tab_options(client, block, true);
+
+ const old_dir_for_display = client.strip_mount_point_prefix(old_dir);
+ if (old_dir_for_display === false)
+ return Promise.reject(_("This device can not be used for the installation target."));
+
+ const split_options = parse_options(options);
+ extract_option(split_options, "noauto");
+ const opt_never_auto = extract_option(split_options, "x-cockpit-never-auto");
+ const opt_ro = extract_option(split_options, "ro");
+ const opt_nofail = extract_option(split_options, "nofail");
+ const opt_netdev = extract_option(split_options, "_netdev");
+ if (forced_options)
+ for (const opt of forced_options)
+ extract_option(split_options, opt);
+ const extra_options = unparse_options(split_options);
+
+ const is_filesystem_mounted = is_mounted(client, block, subvol);
+
+ function maybe_update_config(new_dir, new_opts, passphrase, passphrase_type) {
+ let new_config = null;
+ let all_new_opts;
+
+ if (new_opts && old_parents)
+ all_new_opts = new_opts + "," + old_parents;
+ else if (new_opts)
+ all_new_opts = new_opts;
+ else
+ all_new_opts = old_parents;
+
+ if (new_dir != "") {
+ if (new_dir[0] != "/")
+ new_dir = "/" + new_dir;
+ new_config = [
+ "fstab", {
+ fsname: old_config ? old_config[1].fsname : undefined,
+ dir: { t: 'ay', v: encode_filename(new_dir) },
+ type: { t: 'ay', v: encode_filename("auto") },
+ opts: { t: 'ay', v: encode_filename(all_new_opts || "defaults") },
+ freq: { t: 'i', v: 0 },
+ passno: { t: 'i', v: 0 },
+ "track-parents": { t: 'b', v: !old_config }
+ }];
+ }
+
+ function undo() {
+ if (!old_config && new_config)
+ return block.RemoveConfigurationItem(new_config, {});
+ else if (old_config && !new_config)
+ return block.AddConfigurationItem(old_config, {});
+ else if (old_config && new_config && (new_dir != old_dir || new_opts != old_opts)) {
+ return block.UpdateConfigurationItem(new_config, old_config, {});
+ }
+ }
+
+ function get_block_fsys() {
+ if (block_fsys)
+ return Promise.resolve(block_fsys);
+ else
+ return client.wait_for(() => (client.blocks_cleartext[block.path] &&
+ client.blocks_fsys[client.blocks_cleartext[block.path].path]));
+ }
+
+ function maybe_mount() {
+ if (mode == "mount" || (mode == "update" && is_filesystem_mounted)) {
+ return (get_block_fsys()
+ .then(block_fsys => {
+ const block = client.blocks[block_fsys.path];
+ return (client.mount_at(block, new_dir)
+ .catch(error => {
+ // systemd might have mounted the filesystem for us after
+ // unlocking, because fstab told it to. Ignore any error
+ // from mounting in that case. This only happens when this
+ // code runs to fix a inconsistent mount.
+ return (is_mounted_synch(block)
+ .then(mounted_at => {
+ if (mounted_at == new_dir)
+ return;
+ return (undo()
+ .then(() => {
+ if (is_filesystem_mounted)
+ return client.mount_at(block, old_dir);
+ })
+ .catch(ignored_error => {
+ console.warn("Error during undo:", ignored_error);
+ })
+ .then(() => Promise.reject(error)));
+ }));
+ }));
+ }));
+ } else
+ return Promise.resolve();
+ }
+
+ function maybe_unlock() {
+ const crypto = client.blocks_crypto[block.path];
+ if (mode == "mount" && crypto) {
+ return (unlock_with_type(client, block, passphrase, passphrase_type)
+ .catch(error => {
+ dlg.set_values({ needs_explicit_passphrase: true });
+ return Promise.reject(error);
+ }));
+ } else
+ return Promise.resolve();
+ }
+
+ function maybe_lock() {
+ if (mode == "unmount" && !subvol) {
+ const crypto_backing = client.blocks[block.CryptoBackingDevice];
+ const crypto_backing_crypto = crypto_backing && client.blocks_crypto[crypto_backing.path];
+ if (crypto_backing_crypto) {
+ return crypto_backing_crypto.Lock({});
+ } else
+ return Promise.resolve();
+ }
+ }
+
+ // We need to reload systemd twice: Once at the beginning so
+ // that it is up to date with whatever is currently in fstab,
+ // and once at the end to make it see our changes. Otherwise
+ // systemd might do some uexpected mounts/unmounts behind our
+ // backs.
+
+ return (reload_systemd()
+ .then(() => teardown_active_usage(client, usage))
+ .then(maybe_unlock)
+ .then(() => {
+ if (!old_config && new_config)
+ return (block.AddConfigurationItem(new_config, {})
+ .then(maybe_mount));
+ else if (old_config && !new_config)
+ return block.RemoveConfigurationItem(old_config, {});
+ else if (old_config && new_config)
+ return (block.UpdateConfigurationItem(old_config, new_config, {})
+ .then(maybe_mount));
+ else if (new_config && !is_mounted(client, block))
+ return maybe_mount();
+ })
+ .then(maybe_lock)
+ .then(reload_systemd));
+ }
+
+ let at_boot;
+ if (opt_never_auto)
+ at_boot = "never";
+ else if (opt_netdev)
+ at_boot = "netdev";
+ else if (opt_nofail)
+ at_boot = "nofail";
+ else
+ at_boot = "local";
+
+ let fields = null;
+ if (mode == "mount" || mode == "update") {
+ fields = [
+ TextInput("mount_point", _("Mount point"),
+ {
+ value: old_dir_for_display,
+ validate: val => is_valid_mount_point(client,
+ block,
+ client.add_mount_point_prefix(val),
+ mode == "update" && !is_filesystem_mounted,
+ true,
+ subvol)
+ }),
+ mount_options(opt_ro, extra_options),
+ at_boot_input(at_boot),
+ ];
+
+ if (block.IdUsage == "crypto" && mode == "mount")
+ fields = fields.concat([
+ PassInput("passphrase", _("Passphrase"),
+ {
+ visible: vals => vals.needs_explicit_passphrase,
+ validate: val => !val.length && _("Passphrase cannot be empty"),
+ })
+ ]);
+ }
+
+ const mode_title = {
+ mount: _("Mount filesystem"),
+ unmount: _("Unmount filesystem $0"),
+ update: _("Mount configuration")
+ };
+
+ const mode_action = {
+ mount: _("Mount"),
+ unmount: _("Unmount"),
+ update: _("Save")
+ };
+
+ function do_unmount() {
+ let opts = [];
+ opts.push("noauto");
+ if (opt_ro)
+ opts.push("ro");
+ if (opt_never_auto)
+ opts.push("x-cockpit-never-auto");
+ if (opt_nofail)
+ opts.push("nofail");
+ if (opt_netdev)
+ opts.push("_netdev");
+ if (forced_options)
+ opts = opts.concat(forced_options);
+ if (extra_options)
+ opts = opts.concat(extra_options);
+ return (maybe_set_crypto_options(null, false, null, null)
+ .then(() => maybe_update_config(old_dir, unparse_options(opts))));
+ }
+
+ let passphrase_type;
+
+ function maybe_set_crypto_options(readonly, auto, nofail, netdev) {
+ if (client.blocks_crypto[block.path]) {
+ return set_crypto_options(block, readonly, auto, nofail, netdev);
+ } else if (client.blocks_crypto[block.CryptoBackingDevice]) {
+ return set_crypto_options(client.blocks[block.CryptoBackingDevice], readonly, auto, nofail, netdev);
+ } else
+ return Promise.resolve();
+ }
+
+ const usage = get_active_usage(client, block.path, null, null, false, subvol);
+
+ const dlg = dialog_open({
+ Title: cockpit.format(mode_title[mode], old_dir_for_display),
+ Fields: fields,
+ Teardown: TeardownMessage(usage, old_dir),
+ update: function (dlg, vals, trigger) {
+ if (trigger == "at_boot")
+ dlg.set_options("at_boot", { explanation: mount_explanation[vals.at_boot] });
+ },
+ Action: {
+ Title: mode_action[mode],
+ disable_on_error: usage.Teardown,
+ action: function (vals) {
+ if (mode == "unmount") {
+ return do_unmount();
+ } else if (mode == "mount" || mode == "update") {
+ let opts = [];
+ if ((mode == "update" && !is_filesystem_mounted) || vals.at_boot == "never")
+ opts.push("noauto");
+ if (vals.mount_options?.ro)
+ opts.push("ro");
+ if (vals.at_boot == "never")
+ opts.push("x-cockpit-never-auto");
+ if (vals.at_boot == "nofail")
+ opts.push("nofail");
+ if (vals.at_boot == "netdev")
+ opts.push("_netdev");
+ if (forced_options)
+ opts = opts.concat(forced_options);
+ if (vals.mount_options?.extra)
+ opts = opts.concat(parse_options(vals.mount_options.extra));
+ return (maybe_update_config(client.add_mount_point_prefix(vals.mount_point),
+ unparse_options(opts),
+ vals.passphrase,
+ passphrase_type)
+ .then(() => maybe_set_crypto_options(vals.mount_options?.ro,
+ opts.indexOf("noauto") == -1,
+ vals.at_boot == "nofail",
+ vals.at_boot == "netdev")));
+ }
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage, old_dir),
+ (block.IdUsage == "crypto" && mode == "mount")
+ ? init_existing_passphrase(block, true, type => { passphrase_type = type })
+ : null
+ ]
+ });
+}
diff --git a/pkg/storaged/filesystem/utils.jsx b/pkg/storaged/filesystem/utils.jsx
new file mode 100644
index 0000000..ba4ac40
--- /dev/null
+++ b/pkg/storaged/filesystem/utils.jsx
@@ -0,0 +1,244 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+
+import cockpit from "cockpit";
+import client from "../client.js";
+
+import {
+ block_name, decode_filename,
+ parse_options, extract_option,
+ get_fstab_config_with_client,
+ find_children_for_mount_point,
+ get_mount_points,
+} from "../utils.js";
+import { parse_subvol_from_options } from "../btrfs/utils.jsx";
+import { StorageLink } from "../storage-controls.jsx";
+
+import { mounting_dialog } from "./mounting-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+/* is mounted with an entry in fstab */
+export function is_mounted(client, block, subvol) {
+ const block_fsys = client.blocks_fsys[block.path];
+ const mounted_at = get_mount_points(client, block_fsys, subvol);
+ const [, dir] = get_fstab_config(block, false, subvol);
+ if (dir) {
+ return mounted_at.indexOf(dir) >= 0;
+ } else
+ return null;
+}
+
+export function get_fstab_config(block, also_child_config, subvol) {
+ return get_fstab_config_with_client(client, block, also_child_config, subvol);
+}
+
+function nice_block_name(block) {
+ return block_name(client.blocks[block.CryptoBackingDevice] || block);
+}
+
+function find_blocks_for_mount_point(client, mount_point, self_block, self_subvol) {
+ function same_btrfs_volume(a, b) {
+ return (client.blocks_fsys_btrfs[a.path] &&
+ client.blocks_fsys_btrfs[b.path] &&
+ client.blocks_fsys_btrfs[a.path].data.uuid == client.blocks_fsys_btrfs[b.path].data.uuid);
+ }
+
+ function same_btrfs_subvol(a, b) {
+ return (a && b &&
+ ((a.pathname && a.pathname == b.pathname) ||
+ (a.id && a.id == b.id)));
+ }
+
+ function is_self(b, subvol) {
+ if (self_subvol)
+ return same_btrfs_volume(b, self_block) && same_btrfs_subvol(subvol, self_subvol);
+ else
+ return self_block && (b == self_block || client.blocks[b.CryptoBackingDevice] == self_block);
+ }
+
+ function fmt_block_and_subvol(block, subvol) {
+ if (subvol)
+ return cockpit.format(_("btrfs subvolume $0 of $1"),
+ subvol.pathname || subvol.id,
+ block.IdLabel || block.IdUUID);
+ else
+ return nice_block_name(block);
+ }
+
+ const blocks = [];
+ const seen_uuids = {};
+
+ for (const p in client.blocks) {
+ const b = client.blocks[p];
+ for (const c of b.Configuration) {
+ if (c[0] == "fstab") {
+ let dir = decode_filename(c[1].dir.v);
+ if (dir[0] != "/")
+ dir = "/" + dir;
+ const subvol = parse_subvol_from_options(decode_filename(c[1].opts.v));
+ if (dir == mount_point && !is_self(b, subvol)) {
+ if (!seen_uuids[b.IdUUID]) {
+ seen_uuids[b.IdUUID] = true;
+ blocks.push(fmt_block_and_subvol(b, subvol));
+ }
+ }
+ }
+ }
+ }
+
+ return blocks;
+}
+
+export function is_valid_mount_point(client, block, val, format_only, for_fstab, subvol) {
+ if (val === "") {
+ if (!format_only || for_fstab)
+ return _("Mount point cannot be empty");
+ return null;
+ }
+
+ const other_blocks = find_blocks_for_mount_point(client, val, block, subvol);
+ if (other_blocks.length > 0)
+ return cockpit.format(_("Mount point is already used for $0"), other_blocks.join(", "));
+
+ if (!format_only) {
+ const children = find_children_for_mount_point(client, val, block);
+ if (Object.keys(children).length > 0)
+ return <>
+ {_("Filesystems are already mounted below this mountpoint.")}
+ {Object.keys(children).map(m => <div key={m}>
+ {cockpit.format("• $0 on $1", nice_block_name(children[m]),
+ client.strip_mount_point_prefix(m))}
+ </div>)}
+ {_("Please unmount them first.")}
+ </>;
+ }
+}
+
+export function get_cryptobacking_noauto(client, block) {
+ const crypto_backing = block.IdUsage == "crypto" ? block : client.blocks[block.CryptoBackingDevice];
+ if (!crypto_backing)
+ return false;
+
+ const crypto_config = crypto_backing.Configuration.find(c => c[0] == "crypttab");
+ if (!crypto_config)
+ return false;
+
+ const crypto_options = decode_filename(crypto_config[1].options.v).split(",")
+ .map(o => o.trim());
+ return crypto_options.indexOf("noauto") >= 0;
+}
+
+export function edit_mount_point(block, forced_options, subvol) {
+ mounting_dialog(client, block, "update", forced_options, subvol);
+}
+
+export const MountPoint = ({ fstab_config, forced_options, backing_block, content_block, subvol }) => {
+ const is_filesystem_mounted = content_block && is_mounted(client, content_block, subvol);
+ const [, old_dir, old_opts] = fstab_config;
+ const split_options = parse_options(old_opts);
+ extract_option(split_options, "noauto");
+ const opt_ro = extract_option(split_options, "ro");
+ const opt_never_auto = extract_option(split_options, "x-cockpit-never-auto");
+ const opt_nofail = extract_option(split_options, "nofail");
+ const opt_netdev = extract_option(split_options, "_netdev");
+ if (forced_options)
+ for (const opt of forced_options)
+ extract_option(split_options, opt);
+
+ let mount_point_text = null;
+ if (old_dir) {
+ mount_point_text = client.strip_mount_point_prefix(old_dir);
+ let opt_texts = [];
+ if (opt_ro)
+ opt_texts.push(_("read only"));
+ if (opt_never_auto)
+ opt_texts.push(_("never mount at boot"));
+ else if (opt_netdev)
+ opt_texts.push(_("after network"));
+ else if (opt_nofail)
+ opt_texts.push(_("ignore failure"));
+ else
+ opt_texts.push(_("stop boot on failure"));
+ opt_texts = opt_texts.concat(split_options);
+ if (opt_texts.length && !client.in_anaconda_mode()) {
+ mount_point_text = cockpit.format("$0 ($1)", mount_point_text, opt_texts.join(", "));
+ }
+ }
+
+ let extra_text = null;
+ if (client.in_anaconda_mode()) {
+ if (!old_dir)
+ extra_text = _("The filesystem has no assigned mount point.");
+ } else {
+ if (!is_filesystem_mounted) {
+ if (!old_dir)
+ extra_text = _("The filesystem has no permanent mount point.");
+ else
+ extra_text = _("The filesystem is not mounted.");
+ } else if (backing_block != content_block) {
+ if (!opt_never_auto)
+ extra_text = _("The filesystem will be unlocked and mounted on the next boot. This might require inputting a passphrase.");
+ }
+ }
+
+ if (!mount_point_text) {
+ mount_point_text = extra_text;
+ extra_text = null;
+ }
+
+ if (extra_text)
+ extra_text = <><br />{extra_text}</>;
+
+ return (
+ <>
+ <Flex>
+ { mount_point_text &&
+ <FlexItem>{ mount_point_text }</FlexItem>
+ }
+ <FlexItem>
+ <StorageLink onClick={() => edit_mount_point(content_block || backing_block,
+ forced_options, subvol)}>
+ {_("edit")}
+ </StorageLink>
+ </FlexItem>
+ </Flex>
+ { extra_text }
+ </>);
+};
+
+export const mount_point_text = (mount_point, mounted) => {
+ let mp_text;
+ if (mount_point) {
+ mp_text = client.strip_mount_point_prefix(mount_point);
+ if (mp_text == false)
+ return null;
+ if (!mounted && !client.in_anaconda_mode())
+ mp_text = mp_text + " " + _("(not mounted)");
+ } else {
+ if (client.in_anaconda_mode())
+ mp_text = _("(no assigned mount point)");
+ else
+ mp_text = _("(not mounted)");
+ }
+ return mp_text;
+};
diff --git a/pkg/storaged/icons/gnome-icons.jsx b/pkg/storaged/icons/gnome-icons.jsx
new file mode 100644
index 0000000..d34042b
--- /dev/null
+++ b/pkg/storaged/icons/gnome-icons.jsx
@@ -0,0 +1,55 @@
+/* Icons from the GNOME projetc, http://www.gnome.org
+
+ This work is licenced under the terms of either the GNU LGPL v3 or
+ Creative Commons Attribution-Share Alike 3.0 United States License.
+
+ To view a copy of the CC-BY-SA licence, visit
+
+ http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative
+
+ Commons, 171 Second Street, Suite 300, San Francisco, California 94105, USA.
+*/
+
+import React from "react";
+
+export const NetworkIcon = () => {
+ // network-wired-symbolic.svg
+ return <svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 6 0 c -0.554688 0 -1 0.445312 -1 1 v 3 c 0 0.554688 0.445312 1 1 1 h 1 v 2 h -7 v 2 h 2 v 2 h -1 c -0.554688 0 -1 0.445312 -1 1 v 3 c 0 0.554688 0.445312 1 1 1 h 4 c 0.554688 0 1 -0.445312 1 -1 v -3 c 0 -0.554688 -0.445312 -1 -1 -1 h -1 v -2 h 8 v 2 h -1 c -0.554688 0 -1 0.445312 -1 1 v 3 c 0 0.554688 0.445312 1 1 1 h 4 c 0.554688 0 1 -0.445312 1 -1 v -3 c 0 -0.554688 -0.445312 -1 -1 -1 h -1 v -2 h 2 v -2 h -7 v -2 h 1 c 0.554688 0 1 -0.445312 1 -1 v -3 c 0 -0.554688 -0.445312 -1 -1 -1 z m 0 0" fill="#2e3436" />
+ </svg>;
+};
+
+export const VolumeIcon = () => {
+ // drive-multidisk-symbolic.svg
+ return <svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 4 0 c -1.644531 0 -3 1.355469 -3 3 v 6 c 0 1.644531 1.355469 3 3 3 h 5 c 1.644531 0 3 -1.355469 3 -3 v -6 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 5 c 0.570312 0 0.886719 0.441406 1 1 v 5 c 0 0.472656 -0.429688 1 -1 1 h -5 c -0.554688 0 -1 -0.445312 -1 -1 v -5 c 0 -0.554688 0.445312 -1 1 -1 z m 2.503906 1.003906 c -1.375 0 -2.515625 1.128906 -2.5 2.5 l -0.003906 2.496094 h 2.5 c 1.371094 0 2.5 -1.125 2.5 -2.496094 c 0 -1.375 -1.128906 -2.5 -2.5 -2.5 z m 6.496094 1.175782 v 7.820312 c 0 0.472656 -0.429688 1 -1 1 h -8 c 0 1.644531 1.355469 3 3 3 h 5 c 1.644531 0 3 -1.355469 3 -3 v -6 c 0 -1.292969 -0.839844 -2.40625 -2 -2.820312 z m -6.492188 0.320312 c 0.550782 0 1 0.449219 1 1 s -0.449218 1 -1 1 c -0.554687 0 -1 -0.449219 -1 -1 s 0.445313 -1 1 -1 z m 0 0" fill="#2e3436" />
+ </svg>;
+};
+
+export const HDDIcon = () => {
+ // drive-harddisk-symbolic.svg
+ return <svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 4 0 c -1.644531 0 -3 1.355469 -3 3 v 10 c 0 1.644531 1.355469 3 3 3 h 8 c 1.644531 0 3 -1.355469 3 -3 v -10 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 8 c 0.570312 0 1 0.429688 1 1 v 9 c 0 0.570312 -0.429688 1 -1 1 h -8 c -0.554688 0 -1 -0.445312 -1 -1 v -9 c 0 -0.554688 0.445312 -1 1 -1 z m 4 1 c -2.210938 0 -4 1.789062 -4 4 v 4 h 4 c 2.5 0 4 -1.789062 4 -4 s -1.789062 -4 -4 -4 z m 0 2 c 1.105469 0 2 0.894531 2 2 s -0.894531 2 -2 2 s -2 -0.894531 -2 -2 s 0.894531 -2 2 -2 z m 0 0" fill="#2e3436" />
+ </svg>;
+};
+
+export const SSDIcon = () => {
+ // drive-harddisk-solidstate-symbolic.svg
+ return <svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 4 0 c -1.644531 0 -3 1.355469 -3 3 v 10 c 0 1.644531 1.355469 3 3 3 h 8 c 1.644531 0 3 -1.355469 3 -3 v -10 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 8 c 0.570312 0 1 0.429688 1 1 v 9 c 0 0.570312 -0.429688 1 -1 1 h -8 c -0.554688 0 -1 -0.445312 -1 -1 v -9 c 0 -0.554688 0.445312 -1 1 -1 z m 1 1 v 1 h -1 v 6 h 2 v 1 h 1 v -1 h 1 v 1 h 1 v -1 h 1 v 1 h 1 v -1 h 1 v -6 h -2 v -1 h -1 v 1 h -1 v -1 h -1 v 1 h -1 v -1 z m 0 0" fill="#2e3436" />
+ </svg>;
+};
+
+export const MediaDriveIcon = () => {
+ // drive-removable-media-symbolic.svg
+ return <svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 3 1 c -1.644531 0 -3 1.355469 -3 3 v 8 c 0 1.644531 1.355469 3 3 3 h 10 c 1.644531 0 3 -1.355469 3 -3 v -8 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 10 c 0.570312 0 1 0.429688 1 1 v 7 c 0 0.554688 -0.445312 1 -1 1 h -10 c -0.554688 0 -1 -0.445312 -1 -1 v -7 c 0 -0.570312 0.5 -1 1 -1 z m 0.5 7 c -0.277344 0 -0.5 0.222656 -0.5 0.5 s 0.222656 0.5 0.5 0.5 h 9 c 0.277344 0 0.5 -0.222656 0.5 -0.5 s -0.222656 -0.5 -0.5 -0.5 z m 0 0" fill="#2e3434" />
+ </svg>;
+};
+
+export const OtherIcon = () => {
+ // drive-harddisk-symbolic.svg with the internals removed
+ return <svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="M 4,0 C 2.355469,0 1,1.355469 1,3 v 10 c 0,1.644531 1.355469,3 3,3 h 8 c 1.644531,0 3,-1.355469 3,-3 V 3 C 15,1.355469 13.644531,0 12,0 Z m 0,2 h 8 c 0.570312,0 1,0.429688 1,1 v 9 c 0,0.570312 -0.429688,1 -1,1 H 4 C 3.445312,13 3,12.554688 3,12 V 3 C 3,2.445312 3.445312,2 4,2 Z" fill="#2e3436" />
+ </svg>;
+};
diff --git a/pkg/storaged/index.html b/pkg/storaged/index.html
new file mode 100644
index 0000000..59e2ec5
--- /dev/null
+++ b/pkg/storaged/index.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<!--
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html>
+<head>
+ <title translate="yes">Storage</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="storaged.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="../manifests.js"></script>
+ <script src="po.js"></script>
+ <script src="storaged.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="storage">
+ </div>
+</body>
+</html>
diff --git a/pkg/storaged/iscsi/create-dialog.jsx b/pkg/storaged/iscsi/create-dialog.jsx
new file mode 100644
index 0000000..519f6f8
--- /dev/null
+++ b/pkg/storaged/iscsi/create-dialog.jsx
@@ -0,0 +1,181 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import client from "../client.js";
+
+import { dialog_open, TextInput, PassInput, SelectRow } from "../dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export function iscsi_discover() {
+ dialog_open({
+ Title: _("Add iSCSI portal"),
+ Fields: [
+ TextInput("address", _("Server address"),
+ { validate: val => val === "" ? _("Server address cannot be empty.") : null, }),
+ TextInput("username", _("Username"), { }),
+ PassInput("password", _("Password"), { })
+ ],
+ Action: {
+ Title: _("Next"),
+ action: (vals, progress_callback) => new Promise((resolve, reject) => {
+ const options = { };
+ if (vals.username || vals.password) {
+ options.username = { t: 's', v: vals.username };
+ options.password = { t: 's', v: vals.password };
+ }
+
+ let cancelled = false;
+ client.manager_iscsi.call('DiscoverSendTargets',
+ [vals.address,
+ 0,
+ options
+ ])
+ .then(function (results) {
+ if (!cancelled) {
+ resolve();
+ iscsi_add(vals, results[0]);
+ }
+ })
+ .catch(function (error) {
+ if (cancelled)
+ return;
+
+ // HACK - https://github.com/storaged-project/udisks/issues/26
+ if (error.message.indexOf("initiator failed authorization") != -1)
+ error = {
+ username: true, // make it red without text below
+ password: _("Invalid username or password")
+ };
+ else if (error.message.indexOf("cannot resolve host name") != -1)
+ error = {
+ address: _("Unknown host name")
+ };
+ else if (error.message.indexOf("connection login retries") != -1)
+ error = {
+ address: _("Unable to reach server")
+ };
+
+ reject(error);
+ });
+
+ progress_callback(null, function () {
+ cancelled = true;
+ reject();
+ });
+ }),
+ }
+ });
+}
+
+function iscsi_login(target, cred_vals) {
+ const options = {
+ 'node.startup': { t: 's', v: "automatic" }
+ };
+ if (cred_vals.username || cred_vals.password) {
+ options.username = { t: 's', v: cred_vals.username };
+ options.password = { t: 's', v: cred_vals.password };
+ }
+ return client.manager_iscsi.call('Login',
+ [target[0],
+ target[1],
+ target[2],
+ target[3],
+ target[4],
+ options
+ ]);
+}
+
+function iscsi_add(discover_vals, nodes) {
+ dialog_open({
+ Title: cockpit.format(_("Available targets on $0"),
+ discover_vals.address),
+ Fields: [
+ SelectRow("target", [_("Name"), _("Address"), _("Port")],
+ {
+ choices: nodes.map(n => ({
+ columns: [n[0], n[2], n[3]],
+ value: n
+ }))
+ })
+ ],
+ Action: {
+ Title: _("Add"),
+ action: function (vals) {
+ return iscsi_login(vals.target, discover_vals)
+ .catch(err => {
+ if (err.message.indexOf("authorization") != -1)
+ iscsi_add_with_creds(discover_vals, vals);
+ else
+ return Promise.reject(err);
+ });
+ }
+ }
+ });
+}
+
+function iscsi_add_with_creds(discover_vals, login_vals) {
+ dialog_open({
+ Title: _("Authentication required"),
+ Fields: [
+ TextInput("username", _("Username"),
+ { value: discover_vals.username }),
+ PassInput("password", _("Password"),
+ { value: discover_vals.password })
+ ],
+ Action: {
+ Title: _("Add"),
+ action: function (vals) {
+ return iscsi_login(login_vals.target, vals)
+ .catch(err => {
+ // HACK - https://github.com/storaged-project/udisks/issues/26
+ if (err.message.indexOf("authorization") != -1)
+ err = {
+ username: true, // makes it red without text below
+ password: _("Invalid username or password")
+ };
+ return Promise.reject(err);
+ });
+ }
+ }
+ });
+}
+
+export function iscsi_change_name() {
+ return client.manager_iscsi.call('GetInitiatorName')
+ .then(function (results) {
+ const name = results[0];
+ dialog_open({
+ Title: _("Change iSCSI initiator name"),
+ Fields: [
+ TextInput("name", _("Name"), { value: name })
+ ],
+ Action: {
+ Title: _("Change"),
+ action: function (vals) {
+ return client.manager_iscsi.call('SetInitiatorName',
+ [vals.name,
+ { }
+ ]);
+ }
+ }
+ });
+ });
+}
diff --git a/pkg/storaged/iscsi/session.jsx b/pkg/storaged/iscsi/session.jsx
new file mode 100644
index 0000000..c35a2d8
--- /dev/null
+++ b/pkg/storaged/iscsi/session.jsx
@@ -0,0 +1,98 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { NetworkIcon } from "../icons/gnome-icons.jsx";
+import {
+ new_page, new_card, PAGE_CATEGORY_NETWORK,
+ StorageDescription, ChildrenTable, StorageCard
+} from "../pages.jsx";
+
+import { make_drive_page } from "../drive/drive.jsx";
+
+const _ = cockpit.gettext;
+
+async function disconnect(session, goto_page) {
+ const loc = cockpit.location;
+ await session.Logout({ 'node.startup': { t: 's', v: "manual" } });
+ loc.go(goto_page.location);
+}
+
+export function make_iscsi_session_page(parent, session) {
+ const session_card = new_card({
+ title: _("iSCSI portal"),
+ next: null,
+ page_location: ["iscsi", session.data.target_name],
+ page_name: session.data.target_name,
+ page_icon: NetworkIcon,
+ page_categroy: PAGE_CATEGORY_NETWORK,
+ component: ISCSISessionCard,
+ props: { session },
+ actions: [
+ {
+ title: _("Disconnect"),
+ action: () => disconnect(session, parent),
+ danger: true
+ },
+ ]
+ });
+
+ const drives_card = new_card({
+ title: _("iSCSI drives"),
+ next: session_card,
+ component: ISCSIDrivesCard,
+ props: { session },
+ });
+
+ const p = new_page(parent, drives_card);
+
+ if (client.iscsi_sessions_drives[session.path])
+ client.iscsi_sessions_drives[session.path].forEach(d => make_drive_page(p, d));
+}
+
+const ISCSIDrivesCard = ({ card, session }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody className="contains-list">
+ <ChildrenTable emptyCaption={_("No drives found")}
+ aria-label={_("iSCSI drives")}
+ page={card.page} />
+ </CardBody>
+ </StorageCard>
+ );
+};
+
+const ISCSISessionCard = ({ card, session }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Address")} value={session.data.address} />
+ <StorageDescription title={_("Target")} value={session.data.target_name} />
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/jobs-panel.jsx b/pkg/storaged/jobs-panel.jsx
new file mode 100644
index 0000000..1be1e54
--- /dev/null
+++ b/pkg/storaged/jobs-panel.jsx
@@ -0,0 +1,232 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DataList, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+
+import { StorageButton } from "./storage-controls.jsx";
+import { block_name, mdraid_name, lvol_name, format_delay } from "./utils.js";
+
+const _ = cockpit.gettext;
+
+/* Human readable descriptions of the symbolic "Operation"
+ * property of job objects. These are from the storaged
+ * documentation at
+ *
+ * http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Job.html
+ */
+
+const descriptions = {
+ 'ata-smart-selftest': _("SMART self-test of $target"),
+ 'drive-eject': _("Ejecting $target"),
+ 'encrypted-unlock': _("Unlocking $target"),
+ 'encrypted-lock': _("Locking $target"),
+ 'encrypted-modify': _("Modifying $target"),
+ 'encrypted-resize': _("Resizing $target"),
+ 'swapspace-start': _("Starting swapspace $target"),
+ 'swapspace-stop': _("Stopping swapspace $target"),
+ 'filesystem-mount': _("Mounting $target"),
+ 'filesystem-unmount': _("Unmounting $target"),
+ 'filesystem-modify': _("Modifying $target"),
+ 'filesystem-resize': _("Resizing $target"),
+ 'filesystem-check': _("Checking $target"),
+ 'filesystem-repair': _("Repairing $target"),
+ 'format-erase': _("Erasing $target"),
+ 'format-mkfs': _("Creating filesystem on $target"),
+ 'loop-setup': _("Setting up loop device $target"),
+ 'partition-modify': _("Modifying $target"),
+ 'partition-delete': _("Deleting $target"),
+ 'partition-create': _("Creating partition $target"),
+ cleanup: _("Cleaning up for $target"),
+ 'ata-secure-erase': _("Securely erasing $target"),
+ 'ata-enhanced-secure-erase': _("Very securely erasing $target"),
+ 'md-raid-stop': _("Stopping MDRAID device $target"),
+ 'md-raid-start': _("Starting MDRAID device $target"),
+ 'md-raid-fault-device': _("Marking $target as faulty"),
+ 'md-raid-remove-device': _("Removing $target from MDRAID device"),
+ 'md-raid-create': _("Creating MDRAID device $target"),
+ 'mdraid-check-job': _("Checking MDRAID device $target"),
+ 'mdraid-repair-job': _("Checking and repairing MDRAID device $target"),
+ 'mdraid-recover-job': _("Recovering MDRAID device $target"),
+ 'mdraid-sync-job': _("Synchronizing MDRAID device $target"),
+ 'lvm-lvol-delete': _("Deleting $target"),
+ 'lvm-lvol-activate': _("Activating $target"),
+ 'lvm-lvol-deactivate': _("Deactivating $target"),
+ 'lvm-lvol-snapshot': _("Creating snapshot of $target"),
+ 'lvm-vg-create': _("Creating LVM2 volume group $target"),
+ 'lvm-vg-delete': _("Deleting LVM2 volume group $target"),
+ 'lvm-vg-add-device': _("Adding physical volume to $target"),
+ 'lvm-vg-rem-device': _("Removing physical volume from $target"),
+ 'lvm-vg-empty-device': _("Emptying $target"),
+ 'lvm-vg-create-volume': _("Creating logical volume $target"),
+ 'lvm-vg-rename': _("Renaming $target"),
+ 'lvm-vg-resize': _("Resizing $target")
+};
+
+function make_description(client, job) {
+ let fmt = descriptions[job.Operation];
+ if (!fmt)
+ fmt = _("Operation '$operation' on $target");
+
+ const target = job.Objects.map(function (path) {
+ if (client.blocks[path])
+ return block_name(client.blocks[client.blocks[path].CryptoBackingDevice] || client.blocks[path]);
+ else if (client.mdraids[path])
+ return mdraid_name(client.mdraids[path]);
+ else if (client.vgroups[path])
+ return client.vgroups[path].Name;
+ else if (client.lvols[path])
+ return lvol_name(client.lvols[path]);
+ else
+ return _("unknown target");
+ }).join(", ");
+
+ return cockpit.format(fmt, { operation: job.Operation, target });
+}
+
+class JobRow extends React.Component {
+ render() {
+ const job = this.props.job;
+
+ function cancel() {
+ return job.Cancel({});
+ }
+
+ let remaining = null;
+ if (job.ExpectedEndTime > 0) {
+ const d = job.ExpectedEndTime / 1000 - this.props.now;
+ if (d > 0)
+ remaining = format_delay(d);
+ }
+
+ return (
+ <DataListItem>
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell key="spinner" isFilled={false}>
+ <Spinner size="lg" />
+ </DataListCell>,
+ <DataListCell key="desc" className="job-description" isFilled={false}>
+ {make_description(this.props.client, job)}
+ </DataListCell>,
+ <DataListCell key="progress" isFilled={false}>
+ {job.ProgressValid && (job.Progress * 100).toFixed() + "%"}
+ </DataListCell>,
+ <DataListCell key="remaining" isFilled={false}>
+ {remaining}
+ </DataListCell>,
+ <DataListCell key="job-action" isFilled={false} alignRight>
+ { job.Cancelable ? <StorageButton onClick={cancel}>{_("Cancel")}</StorageButton> : null }
+ </DataListCell>,
+ ]}
+ />
+ </DataListItemRow>
+ </DataListItem>
+ );
+ }
+}
+
+export class JobsPanel extends React.Component {
+ constructor() {
+ super();
+ this.reminder = null;
+ }
+
+ componentWillUnmount() {
+ if (this.reminder) {
+ window.clearTimeout(this.reminder);
+ this.reminder = null;
+ }
+ }
+
+ render() {
+ const client = this.props.client;
+ const path_jobs = client.path_jobs[this.props.path] || [];
+ const server_now = new Date().getTime() + client.time_offset;
+
+ function cmp_job(job_a, job_b) {
+ return job_a.StartTime - job_b.StartTime;
+ }
+
+ function job_is_stable(job) {
+ const age_ms = server_now - job.StartTime / 1000;
+ if (age_ms >= 2000)
+ return true;
+
+ if (job.ExpectedEndTime > 0 && (job.ExpectedEndTime / 1000 - server_now) >= 2000)
+ return true;
+
+ return false;
+ }
+
+ let jobs = [];
+ let have_reminder = false;
+ for (const j of path_jobs) {
+ if (job_is_stable(j)) {
+ jobs.push(j);
+ } else if (!have_reminder) {
+ // If there is a unstable job, we have to check again in a bit since being
+ // stable or not depends on the current time.
+ if (this.reminder)
+ window.clearTimeout(this.reminder);
+ this.reminder = window.setTimeout(() => { this.setState({}) }, 1000);
+ have_reminder = true;
+ }
+ }
+
+ if (jobs.length === 0)
+ return null;
+
+ jobs = jobs.sort(cmp_job);
+
+ return (
+ <CardBody className="contains-list">
+ <DataList isCompact aria-label={_("Jobs")}>
+ { jobs.map(j => <JobRow key={j.path} client={client} job={j} now={server_now} />) }
+ </DataList>
+ </CardBody>
+ );
+ }
+}
+
+export function job_progress_wrapper(client, path1, path2) {
+ return function (vals, progress_callback, action_function) {
+ function client_changed() {
+ const jobs = client.path_jobs[path1] || client.path_jobs[path2];
+ if (jobs && jobs.length > 0) {
+ const job = jobs[0];
+ let desc = make_description(client, job);
+ if (job.ProgressValid)
+ desc += cockpit.format(" ($0%)", (job.Progress * 100).toFixed());
+ progress_callback(desc, job.Cancelable ? () => job.Cancel({}) : null);
+ } else {
+ progress_callback(null, null);
+ }
+ }
+
+ client.addEventListener("changed", client_changed);
+ return action_function(vals, progress_callback)
+ .finally(() => { client.removeEventListener("changed", client_changed) });
+ };
+}
diff --git a/pkg/storaged/legacy-vdo/legacy-vdo.jsx b/pkg/storaged/legacy-vdo/legacy-vdo.jsx
new file mode 100644
index 0000000..198c390
--- /dev/null
+++ b/pkg/storaged/legacy-vdo/legacy-vdo.jsx
@@ -0,0 +1,348 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Card, CardBody } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { block_short_name, get_active_usage, teardown_active_usage, fmt_size, decode_filename, reload_systemd } from "../utils.js";
+import {
+ dialog_open, SizeSlider, BlockingMessage, TeardownMessage, init_active_usage_processes
+} from "../dialog.jsx";
+import { StorageButton, StorageOnOff } from "../storage-controls.jsx";
+
+import { StorageCard, new_page, new_card } from "../pages.jsx";
+import { make_block_page } from "../block/create-pages.jsx";
+
+import inotify_py from "inotify.py";
+import vdo_monitor_py from "./vdo-monitor.py";
+
+const _ = cockpit.gettext;
+
+export function make_legacy_vdo_page(parent, vdo, backing_block, next_card) {
+ const block = client.slashdevs_block[vdo.dev];
+
+ function stop() {
+ const usage = get_active_usage(client, block ? block.path : "/", _("stop"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), vdo.name),
+ Body: BlockingMessage(usage),
+ });
+ return;
+ }
+
+ if (usage.Teardown) {
+ dialog_open({
+ Title: cockpit.format(_("Confirm stopping of $0"),
+ vdo.name),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Title: _("Stop"),
+ action: function () {
+ return teardown_active_usage(client, usage)
+ .then(function () {
+ return vdo.stop();
+ });
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+ } else {
+ return vdo.stop();
+ }
+ }
+
+ function delete_() {
+ const usage = get_active_usage(client, block ? block.path : "/", _("delete"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), vdo.name),
+ Body: BlockingMessage(usage),
+ });
+ return;
+ }
+
+ function wipe_with_teardown(block) {
+ return block.Format("empty", { 'tear-down': { t: 'b', v: true } }).then(reload_systemd);
+ }
+
+ function teardown_configs() {
+ if (block) {
+ return wipe_with_teardown(block);
+ } else {
+ return vdo.start()
+ .then(function () {
+ return client.wait_for(() => client.slashdevs_block[vdo.dev])
+ .then(function (block) {
+ return wipe_with_teardown(block)
+ .catch(error => {
+ // systemd might have mounted it, let's try unmounting
+ const block_fsys = client.blocks_fsys[block.path];
+ if (block_fsys) {
+ return block_fsys.Unmount({})
+ .then(() => wipe_with_teardown(block));
+ } else {
+ return Promise.reject(error);
+ }
+ });
+ });
+ });
+ }
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Permanently delete $0?"), vdo.name),
+ Body: TeardownMessage(usage),
+ Action: {
+ Title: _("Delete"),
+ Danger: _("Deleting erases all data on a VDO device."),
+ action: function () {
+ return (teardown_active_usage(client, usage)
+ .then(teardown_configs)
+ .then(function () {
+ return vdo.remove();
+ }));
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+ }
+
+ const vdo_card = new_card({
+ title: cockpit.format(_("VDO device $0"), vdo.name),
+ next: next_card,
+ page_location: ["vdo", vdo.name],
+ page_name: block_short_name(backing_block),
+ page_size: vdo.logical_size,
+ job_path: backing_block.path,
+ component: VDODetails,
+ props: { client, vdo },
+ actions: [
+ (block
+ ? { title: _("Stop"), action: stop }
+ : { title: _("Start"), action: () => vdo.start() }
+ ),
+ { title: _("Delete"), action: delete_, danger: true }
+ ],
+ });
+
+ if (block) {
+ make_block_page(parent, block, vdo_card);
+ } else {
+ new_page(parent, vdo_card);
+ }
+}
+
+class VDODetails extends React.Component {
+ constructor() {
+ super();
+ this.poll_path = null;
+ this.state = { stats: null };
+ }
+
+ ensure_polling(enable) {
+ const client = this.props.client;
+ const vdo = this.props.vdo;
+ const block = client.slashdevs_block[vdo.dev];
+ const path = enable && block ? vdo.dev : null;
+
+ let buf = "";
+
+ if (this.poll_path === path)
+ return;
+
+ if (this.poll_path) {
+ this.poll_process.close();
+ this.setState({ stats: null });
+ }
+
+ if (path)
+ this.poll_process = cockpit.spawn([client.legacy_vdo_overlay.python, "--", "-", path], { superuser: true })
+ .input(inotify_py + vdo_monitor_py)
+ .stream((data) => {
+ buf += data;
+ const lines = buf.split("\n");
+ buf = lines[lines.length - 1];
+ if (lines.length >= 2) {
+ this.setState({ stats: JSON.parse(lines[lines.length - 2]) });
+ }
+ });
+ this.poll_path = path;
+ }
+
+ componentDidMount() {
+ this.ensure_polling(true);
+ }
+
+ componentDidUpdate() {
+ this.ensure_polling(true);
+ }
+
+ componentWillUnmount() {
+ this.ensure_polling(false);
+ }
+
+ render() {
+ const client = this.props.client;
+ const vdo = this.props.vdo;
+ const block = client.slashdevs_block[vdo.dev];
+ const backing_block = client.slashdevs_block[vdo.backing_dev];
+
+ function force_delete() {
+ return vdo.force_remove();
+ }
+
+ if (vdo.broken) {
+ return (
+ <Card>
+ <Alert variant='danger' isInline
+ title={_("The creation of this VDO device did not finish and the device can't be used.")}
+ actionClose={<StorageButton onClick={force_delete}>
+ {_("Remove device")}
+ </StorageButton>} />
+ </Card>
+ );
+ }
+
+ const alerts = [];
+ if (backing_block && backing_block.Size > vdo.physical_size)
+ alerts.push(
+ <Alert variant='warning' isInline key="unused"
+ actionClose={<StorageButton onClick={vdo.grow_physical}>{_("Grow to take all space")}</StorageButton>}
+ title={_("This VDO device does not use all of its backing device.")}>
+ { cockpit.format(_("Only $0 of $1 are used."),
+ fmt_size(vdo.physical_size),
+ fmt_size(backing_block.Size))}
+ </Alert>);
+
+ function grow_logical() {
+ dialog_open({
+ Title: cockpit.format(_("Grow logical size of $0"), vdo.name),
+ Fields: [
+ SizeSlider("lsize", _("Logical size"),
+ {
+ max: 5 * vdo.logical_size,
+ min: vdo.logical_size,
+ round: 512,
+ value: vdo.logical_size,
+ allow_infinite: true
+ })
+ ],
+ Action: {
+ Title: _("Grow"),
+ action: function (vals) {
+ if (vals.lsize > vdo.logical_size)
+ return vdo.grow_logical(vals.lsize).then(() => {
+ if (block && block.IdUsage == "filesystem")
+ return cockpit.spawn(["fsadm", "resize",
+ decode_filename(block.Device)],
+ { superuser: true, err: "message" });
+ });
+ }
+ }
+ });
+ }
+
+ function fmt_perc(num) {
+ if (num || num == 0)
+ return num + "%";
+ else
+ return "--";
+ }
+
+ const stats = this.state.stats;
+
+ const header = (
+ <StorageCard card={this.props.card} alerts={alerts}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Device file")}</DescriptionListTerm>
+ <DescriptionListDescription>{vdo.dev}</DescriptionListDescription>
+ </DescriptionListGroup>
+
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Physical")}</DescriptionListTerm>
+ <DescriptionListDescription>
+ { stats
+ ? cockpit.format(_("$0 data + $1 overhead used of $2 ($3)"),
+ fmt_size(stats.dataBlocksUsed * stats.blockSize),
+ fmt_size(stats.overheadBlocksUsed * stats.blockSize),
+ fmt_size(vdo.physical_size),
+ fmt_perc(stats.usedPercent))
+ : fmt_size(vdo.physical_size)
+ }
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Logical")}</DescriptionListTerm>
+ <DescriptionListDescription>
+ { stats
+ ? cockpit.format(_("$0 used of $1 ($2 saved)"),
+ fmt_size(stats.logicalBlocksUsed * stats.blockSize),
+ fmt_size(vdo.logical_size),
+ fmt_perc(stats.savingPercent))
+ : fmt_size(vdo.logical_size)
+ }
+ &nbsp; <StorageButton onClick={grow_logical}>{_("Grow")}</StorageButton>
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Index memory")}</DescriptionListTerm>
+ <DescriptionListDescription>{fmt_size(vdo.index_mem * 1024 * 1024 * 1024)}</DescriptionListDescription>
+ </DescriptionListGroup>
+
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Compression")}</DescriptionListTerm>
+ <DescriptionListDescription>
+ <StorageOnOff state={vdo.compression}
+ aria-label={_("Use compression")}
+ onChange={() => vdo.set_compression(!vdo.compression)} />
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Deduplication")}</DescriptionListTerm>
+ <DescriptionListDescription>
+ <StorageOnOff state={vdo.deduplication}
+ aria-label={_("Use deduplication")}
+ onChange={() => vdo.set_deduplication(!vdo.deduplication)} />
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>
+ );
+
+ return header;
+ }
+}
diff --git a/pkg/storaged/legacy-vdo/vdo-monitor.py b/pkg/storaged/legacy-vdo/vdo-monitor.py
new file mode 100644
index 0000000..36d1d11
--- /dev/null
+++ b/pkg/storaged/legacy-vdo/vdo-monitor.py
@@ -0,0 +1,132 @@
+#! /usr/bin/python3
+
+import json
+import os
+import sys
+import time
+
+from vdo.statistics import NotAvailable, Samples, VDOStatistics
+from vdo.vdomgmnt import Configuration, SizeString
+
+
+class Watcher:
+ def __init__(self, path):
+ self.inotify = Inotify()
+ self.path = path
+ self.wd = -1
+ self.setup()
+
+ def setup(self):
+ self.cur_path = self.path
+ self.cur_wait = None
+ while not os.path.exists(self.cur_path):
+ self.cur_wait = os.path.basename(self.cur_path)
+ self.cur_path = os.path.dirname(self.cur_path)
+
+ events = (IN_CREATE |
+ IN_MOVED_TO |
+ IN_MOVED_FROM |
+ IN_CLOSE_WRITE |
+ IN_MOVE_SELF)
+ self.wd = self.inotify.add_watch(self.cur_path, events)
+
+ def process(self, callback=None):
+ def event(wd, mask, name):
+ want_callback = self.cur_path == self.path
+ if self.cur_wait and name == self.cur_wait:
+ self.inotify.rem_watch(self.wd)
+ self.setup()
+ elif mask & IN_IGNORED:
+ self.setup()
+ want_callback = want_callback or self.cur_path == self.path
+ if want_callback and callback:
+ callback()
+ self.inotify.process(event)
+
+
+# Converts NotAvailable to None, recursively, and other things. The goal is to make OBJ serializable.
+def wash(obj):
+ if isinstance(obj, NotAvailable):
+ return None
+ elif isinstance(obj, SizeString):
+ return int(obj)
+ elif isinstance(obj, dict):
+ return {key: wash(obj[key]) for key in obj.keys()}
+ elif isinstance(obj, list):
+ return list(map(wash, obj))
+ else:
+ return obj
+
+
+def dump_washed(obj):
+ sys.stdout.write(json.dumps(wash(obj)) + "\n")
+ sys.stdout.flush()
+
+
+def monitor_config():
+ def query():
+ try:
+ conf = Configuration("/etc/vdoconf.yml")
+ return [{"name": vdo.getName(),
+ "broken": vdo.unrecoverablePreviousOperationFailure,
+ "device": vdo.device,
+ "logical_size": vdo.logicalSize,
+ "physical_size": vdo.physicalSize,
+ "index_mem": vdo.indexMemory,
+ "activated": vdo.activated,
+ "compression": vdo.enableCompression,
+ "deduplication": vdo.enableDeduplication}
+ for vdo in conf.getAllVdos().values()]
+ except Exception as e:
+ sys.stderr.write(str(e) + "\n")
+ return []
+
+ def event():
+ dump_washed(query())
+
+ watcher = Watcher("/etc/vdoconf.yml")
+ event()
+ while True:
+ watcher.process(event)
+
+
+def monitor_volume(dev):
+
+ monitored_fields = ['blockSize',
+ 'dataBlocksUsed', 'overheadBlocksUsed',
+ 'logicalBlocksUsed',
+ 'usedPercent', 'savingPercent'
+ ]
+
+ # Older versions let us use a string directly, newer versions want
+ # it to be pre-processed.
+ try:
+ dev = Samples.samplingDevice(dev, dev)
+ except AttributeError:
+ pass
+
+ def sample():
+ try:
+ stats = Samples.assay([VDOStatistics()], dev, mustBeVDO=False).samples[0].sample
+ return {key: stats.get(key) for key in monitored_fields}
+ except Exception as e:
+ # Ignore errors from non-existing devices. These happen
+ # briefly when a VDO volume is being stopped or deleted
+ # and the monitor hasn't been killed yet.
+ if "[Errno 2]" not in str(e):
+ raise
+ return {}
+
+ prev = None
+ while True:
+ data = sample()
+ if data != prev:
+ dump_washed(data)
+ prev = data
+ time.sleep(2)
+
+
+if len(sys.argv) == 1:
+ monitor_config()
+else:
+ monitor_volume(sys.argv[1])
diff --git a/pkg/storaged/logs-panel.jsx b/pkg/storaged/logs-panel.jsx
new file mode 100644
index 0000000..033abef
--- /dev/null
+++ b/pkg/storaged/logs-panel.jsx
@@ -0,0 +1,41 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { LogsPanel } from "cockpit-components-logs-panel.jsx";
+
+const _ = cockpit.gettext;
+
+export class StorageLogsPanel extends React.Component {
+ render() {
+ const match = [
+ "_SYSTEMD_UNIT=storaged.service", "+",
+ "_SYSTEMD_UNIT=udisks2.service", "+",
+ "_SYSTEMD_UNIT=dm-event.service", "+",
+ "_SYSTEMD_UNIT=smartd.service", "+",
+ "_SYSTEMD_UNIT=multipathd.service"
+ ];
+
+ const search_options = { prio: "debug", _SYSTEMD_UNIT: "storaged.service,udisks2.service,dm-event.service,smartd.service,multipathd.service" };
+ const url = "/system/logs/#/?prio=debug&_SYSTEMD_UNIT=storaged.service,udisks2.service,dm-event.service,smartd.service,multipathd.service";
+ return <LogsPanel title={_("Storage logs")} match={match} max={10} search_options={search_options} goto_url={url} className="contains-list" />;
+ }
+}
diff --git a/pkg/storaged/lvm2/block-logical-volume.jsx b/pkg/storaged/lvm2/block-logical-volume.jsx
new file mode 100644
index 0000000..7331d86
--- /dev/null
+++ b/pkg/storaged/lvm2/block-logical-volume.jsx
@@ -0,0 +1,410 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hopeg that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { ExclamationTriangleIcon, ExclamationCircleIcon } from "@patternfly/react-icons";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+
+import { StorageButton, StorageLink } from "../storage-controls.jsx";
+
+import { check_unused_space, get_resize_info, grow_dialog, shrink_dialog } from "../block/resize.jsx";
+import { StorageCard, StorageDescription, new_card, navigate_to_new_card_location, navigate_away_from_card } from "../pages.jsx";
+import { block_name, fmt_size, get_active_usage, teardown_active_usage, reload_systemd } from "../utils.js";
+import {
+ dialog_open, TextInput, SelectSpaces, BlockingMessage, TeardownMessage,
+ init_active_usage_processes
+} from "../dialog.jsx";
+
+import { lvm2_create_snapshot_action } from "./volume-group.jsx";
+import { pvs_to_spaces } from "./utils.jsx";
+
+const _ = cockpit.gettext;
+
+export function lvol_rename(lvol) {
+ dialog_open({
+ Title: _("Rename logical volume"),
+ Fields: [
+ TextInput("name", _("Name"),
+ { value: lvol.Name })
+ ],
+ Action: {
+ Title: _("Rename"),
+ action: function (vals) {
+ return lvol.Rename(vals.name, { });
+ }
+ }
+ });
+}
+
+export function lvol_delete(lvol, card) {
+ const vgroup = client.vgroups[lvol.VolumeGroup];
+ const block = client.lvols_block[lvol.path];
+ const usage = get_active_usage(client, block ? block.path : lvol.path, _("delete"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), lvol.Name),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Permanently delete logical volume $0/$1?"), vgroup.Name, lvol.Name),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Danger: _("Deleting a logical volume will delete all data in it."),
+ Title: _("Delete"),
+ action: async function () {
+ await teardown_active_usage(client, usage);
+ await lvol.Delete({ 'tear-down': { t: 'b', v: true } });
+ await reload_systemd();
+ navigate_away_from_card(card);
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+}
+
+function repair(lvol) {
+ const vgroup = lvol && client.vgroups[lvol.VolumeGroup];
+ if (!vgroup)
+ return;
+
+ const summary = client.lvols_stripe_summary[lvol.path];
+ const missing = summary.reduce((sum, sub) => sum + (sub["/"] ?? 0), 0);
+
+ function usable(pvol) {
+ // must have some free space and not already used for a
+ // subvolume other than those that need to be repaired.
+ return pvol.FreeSize > 0 && !summary.some(sub => !sub["/"] && sub[pvol.path]);
+ }
+
+ const pvs_as_spaces = pvs_to_spaces(client, client.vgroups_pvols[vgroup.path].filter(usable));
+ const available = pvs_as_spaces.reduce((sum, spc) => sum + spc.size, 0);
+
+ if (available < missing) {
+ dialog_open({
+ Title: cockpit.format(_("Unable to repair logical volume $0"), lvol.Name),
+ Body: <p>{cockpit.format(_("There is not enough space available that could be used for a repair. At least $0 are needed on physical volumes that are not already used for this logical volume."),
+ fmt_size(missing))}</p>
+ });
+ return;
+ }
+
+ function enough_space(pvs) {
+ const selected = pvs.reduce((sum, pv) => sum + pv.size, 0);
+ if (selected < missing)
+ return cockpit.format(_("An additional $0 must be selected"), fmt_size(missing - selected));
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Repair logical volume $0"), lvol.Name),
+ Body: <div><p>{cockpit.format(_("Select the physical volumes that should be used to repair the logical volume. At leat $0 are needed."),
+ fmt_size(missing))}</p><br /></div>,
+ Fields: [
+ SelectSpaces("pvs", _("Physical Volumes"),
+ {
+ spaces: pvs_as_spaces,
+ validate: enough_space
+ }),
+ ],
+ Action: {
+ Title: _("Repair"),
+ action: function (vals) {
+ return lvol.Repair(vals.pvs.map(spc => spc.block.path), { });
+ }
+ }
+ });
+}
+
+function deactivate(lvol, block) {
+ const vgroup = client.vgroups[lvol.VolumeGroup];
+ const usage = get_active_usage(client, block.path, _("deactivate"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), lvol.Name),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Deactivate logical volume $0/$1?"), vgroup.Name, lvol.Name),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Title: _("Deactivate"),
+ action: async function () {
+ await teardown_active_usage(client, usage);
+ await lvol.Deactivate({ });
+ await reload_systemd();
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+}
+
+export function make_block_logical_volume_card(next, vgroup, lvol, block) {
+ const unused_space_warning = check_unused_space(block.path);
+ const unused_space = !!unused_space_warning;
+ const status_code = client.lvols_status[lvol.path];
+ const pool = client.lvols[lvol.ThinPool];
+
+ let { info, shrink_excuse, grow_excuse } = get_resize_info(client, block, unused_space);
+
+ if (!unused_space && !grow_excuse && !pool && vgroup.FreeSize == 0) {
+ grow_excuse = _("Not enough space to grow");
+ }
+
+ let repair_action = null;
+ if (status_code == "degraded" || status_code == "degraded-maybe-partial")
+ repair_action = { title: _("Repair"), action: () => repair(lvol) };
+
+ const card = new_card({
+ title: _("LVM2 logical volume"),
+ next,
+ page_location: ["vg", vgroup.Name, lvol.Name],
+ page_name: lvol.Name,
+ page_size: block.Size,
+ for_summary: true,
+ has_warning: !!unused_space_warning || !!repair_action,
+ has_danger: status_code == "partial",
+ job_path: lvol.path,
+ component: LVM2LogicalVolumeCard,
+ props: { vgroup, lvol, block, unused_space_warning, resize_info: info },
+ actions: [
+ (!unused_space &&
+ {
+ title: _("Shrink"),
+ action: () => shrink_dialog(client, lvol, info),
+ excuse: shrink_excuse,
+ }),
+ (!unused_space &&
+ {
+ title: _("Grow"),
+ action: () => grow_dialog(client, lvol, info),
+ excuse: grow_excuse,
+ }),
+ {
+ title: _("Deactivate"),
+ action: () => deactivate(lvol, block),
+ },
+ lvm2_create_snapshot_action(lvol),
+ repair_action,
+ {
+ title: _("Delete"),
+ action: () => lvol_delete(lvol, card),
+ danger: true,
+ },
+ ],
+ });
+ return card;
+}
+
+const LVM2LogicalVolumeCard = ({ card, vgroup, lvol, block, unused_space_warning, resize_info }) => {
+ const unused_space = !!unused_space_warning;
+
+ function rename() {
+ dialog_open({
+ Title: _("Rename logical volume"),
+ Fields: [
+ TextInput("name", _("Name"),
+ { value: lvol.Name })
+ ],
+ Action: {
+ Title: _("Rename"),
+ action: async function (vals) {
+ await lvol.Rename(vals.name, { });
+ navigate_to_new_card_location(card, ["vg", vgroup.Name, vals.name]);
+ }
+ }
+ });
+ }
+
+ function shrink_to_fit() {
+ return shrink_dialog(client, lvol, resize_info, true);
+ }
+
+ function grow_to_fit() {
+ return grow_dialog(client, lvol, resize_info, true);
+ }
+
+ const layout_desc = {
+ raid0: _("Striped (RAID 0)"),
+ raid1: _("Mirrored (RAID 1)"),
+ raid10: _("Striped and mirrored (RAID 10)"),
+ raid4: _("Dedicated parity (RAID 4)"),
+ raid5: _("Distributed parity (RAID 5)"),
+ raid6: _("Double distributed parity (RAID 6)")
+ };
+
+ const layout = lvol.Layout;
+
+ return (
+ <StorageCard card={card}
+ alert={unused_space &&
+ <Alert variant="warning"
+ isInline
+ title={_("This logical volume is not completely used by its content.")}>
+ {cockpit.format(_("Volume size is $0. Content size is $1."),
+ fmt_size(unused_space_warning.volume_size),
+ fmt_size(unused_space_warning.content_size))}
+ <div className='storage-alert-actions'>
+ <StorageButton onClick={shrink_to_fit}>{_("Shrink volume")}</StorageButton>
+ <StorageButton onClick={grow_to_fit}>{_("Grow content")}</StorageButton>
+ </div>
+ </Alert>}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Name")} value={lvol.Name}
+ action={<StorageLink onClick={rename}>
+ {_("edit")}
+ </StorageLink>} />
+ { !unused_space &&
+ <StorageDescription title={_("Size")} value={fmt_size(lvol.Size)} />
+ }
+ { (layout && layout != "linear") &&
+ <StorageDescription title={_("Layout")} value={layout_desc[layout] || layout} />
+ }
+ <StructureDescription client={client} lvol={lvol} />
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>);
+};
+
+export const StructureDescription = ({ client, lvol }) => {
+ const vgroup = client.vgroups[lvol.VolumeGroup];
+ const pvs = (vgroup && client.vgroups_pvols[vgroup.path]) || [];
+
+ if (!lvol.Structure || pvs.length <= 1)
+ return null;
+
+ let status = null;
+ const status_code = client.lvols_status[lvol.path];
+ if (status_code == "partial") {
+ status = _("This logical volume has lost some of its physical volumes and can no longer be used. You need to delete it and create a new one to take its place.");
+ } else if (status_code == "degraded") {
+ status = _("This logical volume has lost some of its physical volumes but has not lost any data yet. You should repair it to restore its original redundancy.");
+ } else if (status_code == "degraded-maybe-partial") {
+ status = _("This logical volume has lost some of its physical volumes but might not have lost any data yet. You might be able to repair it.");
+ }
+
+ function nice_block_name(block) {
+ return block_name(client.blocks[block.CryptoBackingDevice] || block);
+ }
+
+ function pvs_box(used, block_path) {
+ if (block_path != "/") {
+ const block = client.blocks[block_path];
+ return <div key={block_path} className="storage-pvs-pv-box">
+ <div className="storage-stripe-pv-box-dev">
+ {block ? nice_block_name(block).replace("/dev/", "") : "???"}
+ </div>
+ <div>{fmt_size(used)}</div>
+ </div>;
+ } else {
+ return <div key={block_path} className="storage-pvs-pv-box">
+ <div className="storage-pvs-pv-box-dev">
+ { status_code == "degraded"
+ ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />
+ : <ExclamationCircleIcon className="ct-icon-times-circle" />
+ }
+ </div>
+ <div>{fmt_size(used)}</div>
+ </div>;
+ }
+ }
+
+ if (lvol.Layout == "linear") {
+ const pvs = client.lvols_stripe_summary[lvol.path];
+ if (!pvs)
+ return null;
+
+ const stripe = Object.keys(pvs).map((path, i) =>
+ <FlexItem key={i} className="storage-pvs-box">
+ {pvs_box(pvs[path], path)}
+ </FlexItem>);
+
+ return (
+ <StorageDescription title={_("Physical volumes")}>
+ <Flex spaceItems={{ default: "spaceItemsNone" }}
+ alignItems={{ default: "alignItemsStretch" }}>
+ {stripe}
+ </Flex>
+ {status}
+ </StorageDescription>);
+ }
+
+ function stripe_box(used, block_path) {
+ if (block_path != "/") {
+ const block = client.blocks[block_path];
+ return <div key={block_path} className="storage-stripe-pv-box">
+ <div className="storage-stripe-pv-box-dev">
+ {block ? nice_block_name(block).replace("/dev/", "") : "???"}
+ </div>
+ <div>{fmt_size(used)}</div>
+ </div>;
+ } else {
+ return <div key={block_path} className="storage-stripe-pv-box">
+ <div className="storage-stripe-pv-box-dev">
+ { status_code == "degraded"
+ ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />
+ : <ExclamationCircleIcon className="ct-icon-times-circle" />
+ }
+ </div>
+ <div>{fmt_size(used)}</div>
+ </div>;
+ }
+ }
+
+ if (lvol.Layout == "mirror" || lvol.Layout.indexOf("raid") == 0) {
+ const summary = client.lvols_stripe_summary[lvol.path];
+ if (!summary)
+ return null;
+
+ const stripes = summary.map((pvs, i) =>
+ <FlexItem key={i} className="storage-stripe-box">
+ {Object.keys(pvs).map(path => stripe_box(pvs[path], path))}
+ </FlexItem>);
+
+ return (
+ <>
+ <StorageDescription title={_("Stripes")}>
+ <Flex alignItems={{ default: "alignItemsStretch" }}>{stripes}</Flex>
+ {status}
+ {lvol.SyncRatio != 1.0
+ ? <div>{cockpit.format(_("$0 synchronized"), lvol.SyncRatio * 100 + "%")}</div>
+ : null}
+ </StorageDescription>
+ </>);
+ }
+
+ return null;
+};
diff --git a/pkg/storaged/lvm2/create-dialog.jsx b/pkg/storaged/lvm2/create-dialog.jsx
new file mode 100644
index 0000000..cd19a36
--- /dev/null
+++ b/pkg/storaged/lvm2/create-dialog.jsx
@@ -0,0 +1,71 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import client from "../client.js";
+
+import { dialog_open, TextInput, SelectSpaces } from "../dialog.jsx";
+import { validate_lvm2_name, get_available_spaces, prepare_available_spaces } from "../utils.js";
+
+const _ = cockpit.gettext;
+
+export function create_vgroup() {
+ function find_vgroup(name) {
+ for (const p in client.vgroups) {
+ if (client.vgroups[p].Name == name)
+ return client.vgroups[p];
+ }
+ return null;
+ }
+
+ let name;
+ for (let i = 0; i < 1000; i++) {
+ name = "vgroup" + i.toFixed();
+ if (!find_vgroup(name))
+ break;
+ }
+
+ dialog_open({
+ Title: _("Create volume group"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ value: name,
+ validate: validate_lvm2_name
+ }),
+ SelectSpaces("disks", _("Disks"),
+ {
+ empty_warning: _("No disks are available."),
+ validate: function (disks) {
+ if (disks.length === 0)
+ return _("At least one disk is needed.");
+ },
+ spaces: get_available_spaces(client)
+ })
+ ],
+ Action: {
+ Title: _("Create"),
+ action: function (vals) {
+ return prepare_available_spaces(client, vals.disks).then(paths => {
+ client.manager_lvm2.VolumeGroupCreate(vals.name, paths, { });
+ });
+ }
+ }
+ });
+}
diff --git a/pkg/storaged/lvm2/create-logical-volume-dialog.jsx b/pkg/storaged/lvm2/create-logical-volume-dialog.jsx
new file mode 100644
index 0000000..b0496b3
--- /dev/null
+++ b/pkg/storaged/lvm2/create-logical-volume-dialog.jsx
@@ -0,0 +1,298 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import * as PK from "packagekit.js";
+
+import { dialog_open, TextInput, SelectOne, Message, SelectSpaces, SelectOneRadioVertical, SizeSlider, CheckBoxes } from "../dialog.jsx";
+import { validate_lvm2_name } from "../utils.js";
+
+import { pvs_to_spaces, next_default_logical_volume_name } from "./utils.jsx";
+
+const _ = cockpit.gettext;
+
+function install_package(name, progress) {
+ return PK.check_missing_packages([name], p => progress(_("Checking installed software"), p.cancel))
+ .then(data => {
+ if (data.unavailable_names.length > 0)
+ return Promise.reject(new Error(
+ cockpit.format(_("$0 is not available from any repository."), data.unavailable_names[0])));
+ // let's be cautious here, we really don't expect removals
+ if (data.remove_names.length > 0)
+ return Promise.reject(new Error(
+ cockpit.format(_("Installing $0 would remove $1."), name, data.remove_names[0])));
+
+ return PK.install_missing_packages(data, p => progress(_("Installing packages"), p.cancel));
+ });
+}
+
+export function create_logical_volume(client, vgroup) {
+ if (vgroup.FreeSize == 0)
+ return;
+
+ const pvs_as_spaces = pvs_to_spaces(client, client.vgroups_pvols[vgroup.path].filter(pvol => pvol.FreeSize > 0));
+
+ const can_do_layouts = !!vgroup.CreatePlainVolumeWithLayout && pvs_as_spaces.length > 1;
+
+ const purposes = [
+ {
+ value: "block",
+ title: _("Block device for filesystems"),
+ },
+ { value: "pool", title: _("Pool for thinly provisioned volumes") }
+ /* Not implemented
+ { value: "cache", Title: _("Cache") }
+ */
+ ];
+
+ const layouts = [
+ {
+ value: "linear",
+ title: _("Linear"),
+ min_pvs: 1,
+ },
+ {
+ value: "raid0",
+ title: _("Striped (RAID 0)"),
+ min_pvs: 2,
+ },
+ {
+ value: "raid1",
+ title: _("Mirrored (RAID 1)"),
+ min_pvs: 2,
+ },
+ {
+ value: "raid10",
+ title: _("Striped and mirrored (RAID 10)"),
+ min_pvs: 4,
+ },
+ {
+ value: "raid5",
+ title: _("Distributed parity (RAID 5)"),
+ min_pvs: 3,
+ },
+ {
+ value: "raid6",
+ title: _("Double distributed parity (RAID 6)"),
+ min_pvs: 5,
+ }
+ ];
+
+ const vdo_package = client.get_config("vdo_package", null);
+ const need_vdo_install = vdo_package && !(client.features.lvm_create_vdo || client.features.legacy_vdo);
+
+ if (client.features.lvm_create_vdo || client.features.legacy_vdo || vdo_package)
+ purposes.push({ value: "vdo", title: _("VDO filesystem volume (compression/deduplication)") });
+
+ /* For layouts with redundancy, CreatePlainVolumeWithLayout will
+ * create as many subvolumes as there are selected PVs. This has
+ * the nice effect of making the calculation of the maximum size of
+ * such a volume trivial.
+ */
+
+ function max_size(vals) {
+ const layout = vals.layout;
+ const pvs = vals.pvs.map(s => s.pvol);
+ const n_pvs = pvs.length;
+ const sum = pvs.reduce((sum, pv) => sum + pv.FreeSize, 0);
+ const min = Math.min.apply(null, pvs.map(pv => pv.FreeSize));
+
+ function metasize(datasize) {
+ const default_regionsize = 2 * 1024 * 1024;
+ const regions = Math.ceil(datasize / default_regionsize);
+ const bytes = 2 * 4096 + Math.ceil(regions / 8);
+ return vgroup.ExtentSize * Math.ceil(bytes / vgroup.ExtentSize);
+ }
+
+ if (layout == "linear") {
+ return sum;
+ } else if (layout == "raid0" && n_pvs >= 2) {
+ return n_pvs * min;
+ } else if (layout == "raid1" && n_pvs >= 2) {
+ return min - metasize(min);
+ } else if (layout == "raid10" && n_pvs >= 4) {
+ return Math.floor(n_pvs / 2) * (min - metasize(min));
+ } else if ((layout == "raid4" || layout == "raid5") && n_pvs >= 3) {
+ return (n_pvs - 1) * (min - metasize(min));
+ } else if (layout == "raid6" && n_pvs >= 5) {
+ return (n_pvs - 2) * (min - metasize(min));
+ } else
+ return 0; // not-covered: internal error
+ }
+
+ const layout_descriptions = {
+ linear: _("Data will be stored on the selected physical volumes without any additional redundancy or performance improvements."),
+ raid0: _("Data will be stored on the selected physical volumes in an alternating fashion to improve performance. At least two volumes need to be selected."),
+ raid1: _("Data will be stored as two or more copies on the selected physical volumes, to improve reliability. At least two volumes need to be selected."),
+ raid10: _("Data will be stored as two copies and also in an alternating fashion on the selected physical volumes, to improve both reliability and performance. At least four volumes need to be selected."),
+ raid4: _("Data will be stored on the selected physical volumes so that one of them can be lost without affecting the data. At least three volumes need to be selected."),
+ raid5: _("Data will be stored on the selected physical volumes so that one of them can be lost without affecting the data. Data is also stored in an alternating fashion to improve performance. At least three volumes need to be selected."),
+ raid6: _("Data will be stored on the selected physical volumes so that up to two of them can be lost at the same time without affecting the data. Data is also stored in an alternating fashion to improve performance. At least five volumes need to be selected."),
+ };
+
+ function compute_layout_choices(pvs) {
+ return layouts.filter(l => l.min_pvs <= pvs.length);
+ }
+
+ for (const lay of layouts)
+ lay.disabled = pvs_as_spaces.length < lay.min_pvs;
+
+ function min_pvs_explanation(pvs, min) {
+ if (pvs.length <= min)
+ return cockpit.format(_("All $0 selected physical volumes are needed for the choosen layout."),
+ pvs.length);
+ return null;
+ }
+
+ dialog_open({
+ Title: _("Create logical volume"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ value: next_default_logical_volume_name(client, vgroup, "lvol"),
+ validate: validate_lvm2_name
+ }),
+ SelectOne("purpose", _("Purpose"),
+ {
+ value: "block",
+ choices: purposes
+ }),
+ Message(cockpit.format(_("The $0 package will be installed to create VDO devices."), vdo_package),
+ {
+ visible: vals => vals.purpose === 'vdo' && need_vdo_install,
+ }),
+ SelectSpaces("pvs", _("Physical Volumes"),
+ {
+ spaces: pvs_as_spaces,
+ value: pvs_as_spaces,
+ visible: vals => can_do_layouts && vals.purpose === 'block',
+ min_selected: 1,
+ validate: (val, vals) => {
+ if (vals.layout == "raid10" && (vals.pvs.length % 2) !== 0)
+ return _("RAID10 needs an even number of physical volumes");
+ },
+ explanation: min_pvs_explanation(pvs_as_spaces, 1)
+ }),
+ SelectOneRadioVertical("layout", _("Layout"),
+ {
+ value: "linear",
+ choices: compute_layout_choices(pvs_as_spaces),
+ visible: vals => can_do_layouts && vals.purpose === 'block',
+ explanation: layout_descriptions.linear
+ }),
+ SizeSlider("size", _("Size"),
+ {
+ visible: vals => vals.purpose !== 'vdo',
+ max: vgroup.FreeSize,
+ round: vgroup.ExtentSize
+ }),
+ /* VDO parameters */
+ SizeSlider("vdo_psize", _("Size"),
+ {
+ visible: vals => vals.purpose === 'vdo',
+ min: 5 * 1024 * 1024 * 1024,
+ max: vgroup.FreeSize,
+ round: vgroup.ExtentSize
+ }),
+ SizeSlider("vdo_lsize", _("Logical size"),
+ {
+ visible: vals => vals.purpose === 'vdo',
+ value: vgroup.FreeSize,
+ // visually point out that this can be over-provisioned
+ max: vgroup.FreeSize * 3,
+ allow_infinite: true,
+ round: vgroup.ExtentSize
+ }),
+
+ CheckBoxes("vdo_options", _("Options"),
+ {
+ visible: vals => vals.purpose === 'vdo',
+ fields: [
+ {
+ tag: "compression",
+ title: _("Compression"),
+ tooltip: _("Save space by compressing individual blocks with LZ4")
+ },
+ {
+ tag: "deduplication",
+ title: _("Deduplication"),
+ tooltip: _("Save space by storing identical data blocks just once")
+ },
+ ],
+ value: {
+ compression: true,
+ deduplication: true,
+ }
+ }),
+ ],
+ update: (dlg, vals, trigger) => {
+ if (vals.purpose == 'block' && (trigger == "layout" || trigger == "pvs" || trigger == "purpose")) {
+ for (const lay of layouts) {
+ if (lay.value == vals.layout) {
+ dlg.set_options("pvs", {
+ min_selected: lay.min_pvs,
+ explanation: min_pvs_explanation(vals.pvs, lay.min_pvs)
+ });
+ }
+ }
+ dlg.set_options("layout",
+ {
+ choices: compute_layout_choices(vals.pvs),
+ explanation: layout_descriptions[vals.layout]
+ });
+ const max = max_size(vals);
+ const old_max = dlg.get_options("size").max;
+ if (vals.size > max || vals.size == old_max)
+ dlg.set_values({ size: max });
+ dlg.set_options("size", { max });
+ } else if (trigger == "purpose") {
+ dlg.set_options("size", { max: vgroup.FreeSize });
+ }
+ },
+ Action: {
+ Title: _("Create"),
+ action: (vals, progress) => {
+ if (vals.purpose == "block") {
+ if (!can_do_layouts)
+ return vgroup.CreatePlainVolume(vals.name, vals.size, { });
+ else {
+ return vgroup.CreatePlainVolumeWithLayout(vals.name, vals.size, vals.layout,
+ vals.pvs.map(spc => spc.block.path),
+ { });
+ }
+ } else if (vals.purpose == "pool")
+ return vgroup.CreateThinPoolVolume(vals.name, vals.size, { });
+ else if (vals.purpose == "vdo") {
+ return (need_vdo_install ? install_package(vdo_package, progress) : Promise.resolve())
+ .then(() => {
+ progress(_("Creating VDO device")); // not cancellable any more
+ return vgroup.CreateVDOVolume(
+ // HACK: emulate lvcreate's automatic pool name creation until
+ // https://github.com/storaged-project/udisks/issues/939
+ vals.name, next_default_logical_volume_name(client, vgroup, "vpool"),
+ vals.vdo_psize, vals.vdo_lsize,
+ 0, // default index memory
+ vals.vdo_options.compression, vals.vdo_options.deduplication,
+ "auto", { });
+ });
+ }
+ }
+ }
+ });
+}
diff --git a/pkg/storaged/lvm2/inactive-logical-volume.jsx b/pkg/storaged/lvm2/inactive-logical-volume.jsx
new file mode 100644
index 0000000..3cc5654
--- /dev/null
+++ b/pkg/storaged/lvm2/inactive-logical-volume.jsx
@@ -0,0 +1,47 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import {
+ StorageCard, new_page, new_card
+} from "../pages.jsx";
+
+import { lvol_delete } from "./block-logical-volume.jsx";
+import { lvm2_create_snapshot_action } from "./volume-group.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_inactive_logical_volume_page(parent, vgroup, lvol, next_card) {
+ const inactive_card = new_card({
+ title: _("Inactive logical volume"),
+ next: next_card,
+ page_location: ["vg", vgroup.Name, lvol.Name],
+ page_name: lvol.Name,
+ page_size: lvol.Size,
+ component: StorageCard,
+ actions: [
+ { title: _("Activate"), action: () => lvol.Activate({}) },
+ lvm2_create_snapshot_action(lvol),
+ { title: _("Delete"), action: () => lvol_delete(lvol, inactive_card), danger: true },
+ ]
+ });
+
+ new_page(parent, inactive_card);
+}
diff --git a/pkg/storaged/lvm2/physical-volume.jsx b/pkg/storaged/lvm2/physical-volume.jsx
new file mode 100644
index 0000000..d3f2c14
--- /dev/null
+++ b/pkg/storaged/lvm2/physical-volume.jsx
@@ -0,0 +1,128 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { StorageCard, StorageDescription, new_card, register_crossref } from "../pages.jsx";
+import { format_dialog } from "../block/format-dialog.jsx";
+import { std_lock_action } from "../crypto/actions.jsx";
+import { StorageUsageBar } from "../storage-controls.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_lvm2_physical_volume_card(next, backing_block, content_block) {
+ const block_pvol = client.blocks_pvol[content_block.path];
+ const vgroup = block_pvol && client.vgroups[block_pvol.VolumeGroup];
+
+ const pv_card = new_card({
+ title: _("LVM2 physical volume"),
+ location: vgroup ? vgroup.Name : null,
+ next,
+ page_size: (block_pvol
+ ? <StorageUsageBar stats={[block_pvol.Size - block_pvol.FreeSize, block_pvol.Size]} short />
+ : backing_block.Size),
+ component: LVM2PhysicalVolumeCard,
+ props: { backing_block, content_block },
+ actions: [
+ std_lock_action(backing_block, content_block),
+ { title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true },
+ ]
+ });
+
+ function pvol_remove() {
+ return vgroup.RemoveDevice(block_pvol.path, true, {});
+ }
+
+ function pvol_empty_and_remove() {
+ return (vgroup.EmptyDevice(block_pvol.path, {})
+ .then(function() {
+ vgroup.RemoveDevice(block_pvol.path, true, {});
+ }));
+ }
+
+ if (vgroup) {
+ const pvols = client.vgroups_pvols[vgroup.path] || [];
+ let remove_action = null;
+ let remove_excuse = null;
+
+ if (vgroup.MissingPhysicalVolumes && vgroup.MissingPhysicalVolumes.length > 0) {
+ remove_excuse = _("Volume group is missing physical volumes");
+ } else if (pvols.length === 1) {
+ remove_excuse = _("Last cannot be removed");
+ } else if (block_pvol.FreeSize < block_pvol.Size) {
+ if (block_pvol.Size <= vgroup.FreeSize)
+ remove_action = pvol_empty_and_remove;
+ else
+ remove_excuse = _("Not enough free space");
+ } else {
+ remove_action = pvol_remove;
+ }
+
+ register_crossref({
+ key: vgroup,
+ card: pv_card,
+ actions: [
+ {
+ title: _("Remove"),
+ action: remove_action,
+ excuse: remove_excuse,
+ },
+ ],
+ size: <StorageUsageBar stats={[block_pvol.Size - block_pvol.FreeSize, block_pvol.Size]} short />,
+ });
+ }
+
+ return pv_card;
+}
+
+export const LVM2PhysicalVolumeCard = ({ card, backing_block, content_block }) => {
+ const block_pvol = client.blocks_pvol[content_block.path];
+ const vgroup = block_pvol && client.vgroups[block_pvol.VolumeGroup];
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Volume group")}>
+ {vgroup
+ ? <Button variant="link" isInline role="link"
+ onClick={() => cockpit.location.go(["vg", vgroup.Name])}>
+ {vgroup.Name}
+ </Button>
+ : "-"
+ }
+ </StorageDescription>
+ <StorageDescription title={_("UUID")} value={content_block.IdUUID} />
+ { block_pvol &&
+ <StorageDescription title={_("Usage")}>
+ <StorageUsageBar key="s"
+ stats={[block_pvol.Size - block_pvol.FreeSize,
+ block_pvol.Size]} />
+ </StorageDescription>
+ }
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>);
+};
diff --git a/pkg/storaged/lvm2/thin-pool-logical-volume.jsx b/pkg/storaged/lvm2/thin-pool-logical-volume.jsx
new file mode 100644
index 0000000..0154ea7
--- /dev/null
+++ b/pkg/storaged/lvm2/thin-pool-logical-volume.jsx
@@ -0,0 +1,149 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { StorageCard, StorageDescription, ChildrenTable, new_page, new_card } from "../pages.jsx";
+import { fmt_size, validate_lvm2_name } from "../utils.js";
+import { dialog_open, TextInput, SizeSlider } from "../dialog.jsx";
+import { StorageLink } from "../storage-controls.jsx";
+import { grow_dialog } from "../block/resize.jsx";
+
+import { next_default_logical_volume_name } from "./utils.jsx";
+import { lvol_rename, lvol_delete } from "./block-logical-volume.jsx";
+import { make_lvm2_logical_volume_page } from "./volume-group.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_thin_pool_logical_volume_page(parent, vgroup, lvol) {
+ function create_thin() {
+ dialog_open({
+ Title: _("Create thin volume"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ value: next_default_logical_volume_name(client, vgroup, "lvol"),
+ validate: validate_lvm2_name
+ }),
+ SizeSlider("size", _("Size"),
+ {
+ value: lvol.Size,
+ max: lvol.Size * 3,
+ allow_infinite: true,
+ round: vgroup.ExtentSize
+ })
+ ],
+ Action: {
+ Title: _("Create"),
+ action: function (vals) {
+ return vgroup.CreateThinVolume(vals.name, vals.size, lvol.path, { });
+ }
+ }
+ });
+ }
+
+ const pool_card = make_lvm2_thin_pool_card(null, vgroup, lvol);
+
+ const thin_vols_card = new_card({
+ title: _("Thinly provisioned LVM2 logical volumes"),
+ next: pool_card,
+ page_location: ["vg", vgroup.Name, lvol.Name],
+ page_name: lvol.Name,
+ page_size: lvol.Size,
+ component: LVM2ThinPoolLogicalVolumeCard,
+ props: { vgroup, lvol },
+ actions: [
+ {
+ title: _("Create new thinly provisioned logical volume"),
+ action: create_thin,
+ tag: "pool",
+ },
+ ]
+ });
+
+ const p = new_page(parent, thin_vols_card);
+
+ client.lvols_pool_members[lvol.path].forEach(member_lvol => {
+ make_lvm2_logical_volume_page(p, vgroup, member_lvol);
+ });
+}
+
+function make_lvm2_thin_pool_card(next, vgroup, lvol) {
+ let grow_excuse = null;
+ if (vgroup.FreeSize == 0)
+ grow_excuse = _("Not enough space");
+
+ const card = new_card({
+ title: _("Pool for thinly provisioned LVM2 logical volumes"),
+ next,
+ component: LVM2ThinPoolCard,
+ props: { vgroup, lvol },
+ actions: [
+ {
+ title: _("Grow"),
+ action: () => grow_dialog(client, lvol, { }),
+ excuse: grow_excuse,
+ },
+ {
+ title: _("Delete"),
+ action: () => lvol_delete(lvol, card),
+ danger: true,
+ },
+ ],
+ });
+ return card;
+}
+
+function perc(ratio) {
+ return (ratio * 100).toFixed(0) + "%";
+}
+
+export const LVM2ThinPoolLogicalVolumeCard = ({ card, vgroup, lvol }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody className="contains-list">
+ <ChildrenTable emptyCaption={_("No logical volumes")}
+ aria-label={_("Thinly provisioned LVM2 logical volumes")}
+ page={card.page} />
+ </CardBody>
+ </StorageCard>);
+};
+
+export const LVM2ThinPoolCard = ({ card, vgroup, lvol }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Name")}
+ value={lvol.Name}
+ action={<StorageLink onClick={() => lvol_rename(lvol)}>
+ {_("edit")}
+ </StorageLink>} />
+ <StorageDescription title={_("Size")} value={fmt_size(lvol.Size)} />
+ <StorageDescription title={_("Data used")} value={perc(lvol.DataAllocatedRatio)} />
+ <StorageDescription title={_("Metadata used")} value={perc(lvol.MetadataAllocatedRatio)} />
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>);
+};
diff --git a/pkg/storaged/lvm2/unsupported-logical-volume.jsx b/pkg/storaged/lvm2/unsupported-logical-volume.jsx
new file mode 100644
index 0000000..8c3b31a
--- /dev/null
+++ b/pkg/storaged/lvm2/unsupported-logical-volume.jsx
@@ -0,0 +1,68 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+
+import {
+ StorageCard, new_page, new_card
+} from "../pages.jsx";
+
+import { lvol_delete } from "./block-logical-volume.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_unsupported_logical_volume_page(parent, vgroup, lvol, next_card) {
+ const unsupported_card = new_card({
+ title: _("Unsupported logical volume"),
+ next: next_card,
+ page_location: ["vg", vgroup.Name, lvol.Name],
+ page_name: lvol.Name,
+ page_size: lvol.Size,
+ component: LVM2UnsupportedLogicalVolumeCard,
+ props: { vgroup, lvol },
+ actions: [
+ { title: _("Deactivate"), action: () => lvol.Deactivate({}) },
+ { title: _("Delete"), action: () => lvol_delete(lvol, unsupported_card), danger: true },
+ ]
+ });
+
+ // FIXME: it would be nice to log unsupported volumes with
+ // "console.error" so that our tests will detect them more
+ // readily. Unfortunately, when a logical volume gets activated,
+ // its block device will only appear on D-Bus a little while
+ // later, and the logical volume is thus considered unsupported
+ // for the little while.
+ //
+ // This also leads to potential flicker in the UI, so it would be
+ // nice to remove this intermediate state also for that reason.
+
+ new_page(parent, unsupported_card);
+}
+
+const LVM2UnsupportedLogicalVolumeCard = ({ card, vgroup, lvol }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <p>{_("INTERNAL ERROR - This logical volume is marked as active and should have an associated block device. However, no such block device could be found.")}</p>
+ </CardBody>
+ </StorageCard>);
+};
diff --git a/pkg/storaged/lvm2/utils.jsx b/pkg/storaged/lvm2/utils.jsx
new file mode 100644
index 0000000..ca0e2d7
--- /dev/null
+++ b/pkg/storaged/lvm2/utils.jsx
@@ -0,0 +1,60 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import { get_block_link_parts } from "../utils.js";
+
+export function check_partial_lvols(client, path, enter_warning) {
+ if (client.lvols_status[path] && client.lvols_status[path] != "") {
+ enter_warning(path, {
+ warning: "partial-lvol",
+ danger: client.lvols_status[path] != "degraded"
+ });
+ }
+}
+
+export function pvs_to_spaces(client, pvs) {
+ return pvs.map(pvol => {
+ const block = client.blocks[pvol.path];
+ const parts = get_block_link_parts(client, pvol.path);
+ const text = cockpit.format(parts.format, parts.link);
+ return { type: 'block', block, size: pvol.FreeSize, desc: text, pvol };
+ });
+}
+
+export function next_default_logical_volume_name(client, vgroup, prefix) {
+ function find_lvol(name) {
+ const lvols = client.vgroups_lvols[vgroup.path];
+ for (let i = 0; i < lvols.length; i++) {
+ if (lvols[i].Name == name)
+ return lvols[i];
+ }
+ return null;
+ }
+
+ let name;
+ for (let i = 0; i < 1000; i++) {
+ name = prefix + i.toFixed();
+ if (!find_lvol(name))
+ break;
+ }
+
+ return name;
+}
diff --git a/pkg/storaged/lvm2/vdo-pool.jsx b/pkg/storaged/lvm2/vdo-pool.jsx
new file mode 100644
index 0000000..03f8011
--- /dev/null
+++ b/pkg/storaged/lvm2/vdo-pool.jsx
@@ -0,0 +1,95 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { StorageOnOff } from "../storage-controls.jsx";
+
+import { grow_dialog } from "../block/resize.jsx";
+import { StorageCard, StorageDescription, new_card } from "../pages.jsx";
+import { fmt_size } from "../utils.js";
+
+const _ = cockpit.gettext;
+
+export function make_vdo_pool_card(next, vgroup, lvol) {
+ const vdo_iface = client.vdo_vols[lvol.path];
+ const vdo_pool_vol = client.lvols[vdo_iface.VDOPool];
+
+ if (!vdo_pool_vol)
+ return null;
+
+ return new_card({
+ title: _("LVM2 VDO pool"),
+ next,
+ component: LVM2VDOPoolCard,
+ props: { vgroup, lvol, vdo_iface, vdo_pool_vol },
+ actions: [
+ {
+ title: _("Grow"),
+ action: () => grow_dialog(client, vdo_pool_vol, { }),
+ }
+ ],
+ });
+}
+
+const LVM2VDOPoolCard = ({ card, vgroup, lvol, vdo_iface, vdo_pool_vol }) => {
+ function toggle_compression() {
+ const new_state = !vdo_iface.Compression;
+ return vdo_iface.EnableCompression(new_state, {})
+ .then(() => client.wait_for(() => vdo_iface.Compression === new_state));
+ }
+
+ function toggle_deduplication() {
+ const new_state = !vdo_iface.Deduplication;
+ return vdo_iface.EnableDeduplication(new_state, {})
+ .then(() => client.wait_for(() => vdo_iface.Deduplication === new_state));
+ }
+
+ function perc(ratio) {
+ return (ratio * 100).toFixed(0) + "%";
+ }
+
+ const used_pct = perc(vdo_iface.UsedSize / vdo_pool_vol.Size);
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Name")} value={vdo_pool_vol.Name} />
+ <StorageDescription title={_("Size")} value={fmt_size(vdo_pool_vol.Size)} />
+ <StorageDescription title={_("Data used")}>
+ {fmt_size(vdo_iface.UsedSize)} ({used_pct})
+ </StorageDescription>
+ <StorageDescription title={_("Metadata used")} value={perc(lvol.MetadataAllocatedRatio)} />
+ <StorageDescription title={_("Compression")}>
+ <StorageOnOff state={vdo_iface.Compression} aria-label={_("Use compression")}
+ onChange={toggle_compression} />
+ </StorageDescription>
+ <StorageDescription title={_("Deduplication")}>
+ <StorageOnOff state={vdo_iface.Deduplication} aria-label={_("Use deduplication")}
+ onChange={toggle_deduplication} />
+ </StorageDescription>
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>);
+};
diff --git a/pkg/storaged/lvm2/volume-group.jsx b/pkg/storaged/lvm2/volume-group.jsx
new file mode 100644
index 0000000..a8f6e7c
--- /dev/null
+++ b/pkg/storaged/lvm2/volume-group.jsx
@@ -0,0 +1,398 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { CardHeader, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { useObject } from "hooks";
+
+import { VolumeIcon } from "../icons/gnome-icons.jsx";
+import { StorageButton, StorageLink } from "../storage-controls.jsx";
+import {
+ StorageCard, StorageDescription, ChildrenTable, PageTable,
+ new_page, new_card, get_crossrefs, PAGE_CATEGORY_VIRTUAL,
+ navigate_to_new_card_location, navigate_away_from_card
+} from "../pages.jsx";
+import {
+ fmt_size_long, get_active_usage, teardown_active_usage, for_each_async,
+ validate_lvm2_name,
+ get_available_spaces, prepare_available_spaces,
+ reload_systemd, should_ignore,
+} from "../utils.js";
+
+import {
+ dialog_open, SelectSpaces, TextInput,
+ BlockingMessage, TeardownMessage,
+ init_active_usage_processes
+} from "../dialog.jsx";
+
+import { create_logical_volume } from "./create-logical-volume-dialog.jsx";
+import { make_block_logical_volume_card } from "./block-logical-volume.jsx";
+import { make_vdo_pool_card } from "./vdo-pool.jsx";
+import { make_thin_pool_logical_volume_page } from "./thin-pool-logical-volume.jsx";
+import { make_inactive_logical_volume_page } from "./inactive-logical-volume.jsx";
+import { make_unsupported_logical_volume_page } from "./unsupported-logical-volume.jsx";
+import { make_block_page } from "../block/create-pages.jsx";
+
+const _ = cockpit.gettext;
+
+function vgroup_rename(client, vgroup, card) {
+ dialog_open({
+ Title: _("Rename volume group"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ value: vgroup.Name,
+ validate: validate_lvm2_name
+ })
+ ],
+ Action: {
+ Title: _("Rename"),
+ action: async function (vals) {
+ await vgroup.Rename(vals.name, { });
+ navigate_to_new_card_location(card, ["vg", vals.name]);
+ }
+ }
+ });
+}
+
+function vgroup_delete(client, vgroup, card) {
+ const usage = get_active_usage(client, vgroup.path, _("delete"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"),
+ vgroup.Name),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Permanently delete $0?"), vgroup.Name),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Danger: _("Deleting erases all data on a volume group."),
+ Title: _("Delete"),
+ disable_on_error: usage.Teardown,
+ action: async function () {
+ await teardown_active_usage(client, usage);
+ await vgroup.Delete(true, { 'tear-down': { t: 'b', v: true } });
+ await reload_systemd();
+ navigate_away_from_card(card);
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+}
+
+function create_snapshot(lvol) {
+ dialog_open({
+ Title: _("Create snapshot"),
+ Fields: [
+ TextInput("name", _("Name"),
+ { validate: validate_lvm2_name }),
+ ],
+ Action: {
+ Title: _("Create"),
+ action: function (vals) {
+ return lvol.CreateSnapshot(vals.name, vals.size || 0, { });
+ }
+ }
+ });
+}
+
+export function lvm2_create_snapshot_action(lvol) {
+ if (!client.lvols[lvol.ThinPool])
+ return null;
+
+ return { title: _("Create snapshot"), action: () => create_snapshot(lvol) };
+}
+
+function make_generic_logical_volume_card(next, vgroup, lvol) {
+ let result = next;
+ if (client.vdo_vols[lvol.path])
+ result = make_vdo_pool_card(result, vgroup, lvol);
+ return result;
+}
+
+export function make_lvm2_logical_volume_page(parent, vgroup, lvol) {
+ const generic_card = make_generic_logical_volume_card(null, vgroup, lvol);
+
+ if (lvol.Type == "pool") {
+ make_thin_pool_logical_volume_page(parent, vgroup, lvol);
+ } else {
+ const block = client.lvols_block[lvol.path];
+ if (block) {
+ const lv_card = make_block_logical_volume_card(generic_card, vgroup, lvol, block);
+ make_block_page(parent, block, lv_card);
+ } else {
+ // If we can't find the block for a active volume, UDisks2
+ // or something below is probably misbehaving, and we show
+ // it as "unsupported".
+ if (lvol.Active) {
+ make_unsupported_logical_volume_page(parent, vgroup, lvol, generic_card);
+ } else {
+ make_inactive_logical_volume_page(parent, vgroup, lvol, generic_card);
+ }
+ }
+ }
+}
+
+function make_logical_volume_pages(parent, vgroup) {
+ const isVDOPool = lvol => Object.keys(client.vdo_vols).some(v => client.vdo_vols[v].VDOPool == lvol.path);
+
+ (client.vgroups_lvols[vgroup.path] || []).forEach(lvol => {
+ // We ignore volumes in a thin pool; they appear as children
+ // of their pool.
+ //
+ // We ignore old-style snapshots because Cockpit would need to
+ // treat them specially, and we haven't bothered to write the
+ // code for that.
+ //
+ // We ignore vdo pools; they appear as a card for their
+ // single contained logical volume.
+ //
+ if (lvol.ThinPool == "/" && lvol.Origin == "/" && !isVDOPool(lvol))
+ make_lvm2_logical_volume_page(parent, vgroup, lvol);
+ });
+}
+
+function add_disk(vgroup) {
+ function filter_inside_vgroup(spc) {
+ let block = spc.block;
+ if (client.blocks_part[block.path])
+ block = client.blocks[client.blocks_part[block.path].Table];
+ const lvol = (block &&
+ client.blocks_lvm2[block.path] &&
+ client.lvols[client.blocks_lvm2[block.path].LogicalVolume]);
+ return !lvol || lvol.VolumeGroup != vgroup.path;
+ }
+
+ dialog_open({
+ Title: _("Add disks"),
+ Fields: [
+ SelectSpaces("disks", _("Disks"),
+ {
+ empty_warning: _("No disks are available."),
+ validate: function(disks) {
+ if (disks.length === 0)
+ return _("At least one disk is needed.");
+ },
+ spaces: get_available_spaces(client).filter(filter_inside_vgroup)
+ })
+ ],
+ Action: {
+ Title: _("Add"),
+ action: function(vals) {
+ return prepare_available_spaces(client, vals.disks).then(paths =>
+ Promise.all(paths.map(p => vgroup.AddDevice(p, {}))));
+ }
+ }
+ });
+}
+
+export function make_lvm2_volume_group_page(parent, vgroup) {
+ const has_missing_pvs = vgroup.MissingPhysicalVolumes && vgroup.MissingPhysicalVolumes.length > 0;
+
+ let lvol_excuse = null;
+ if (has_missing_pvs)
+ lvol_excuse = _("Volume group is missing physical volumes");
+ else if (vgroup.FreeSize == 0)
+ lvol_excuse = _("No free space");
+
+ if (should_ignore(client, vgroup.path))
+ return;
+
+ const vgroup_card = new_card({
+ title: _("LVM2 volume group"),
+ next: null,
+ page_location: ["vg", vgroup.Name],
+ page_name: vgroup.Name,
+ page_icon: VolumeIcon,
+ page_category: PAGE_CATEGORY_VIRTUAL,
+ page_size: vgroup.Size,
+ job_path: vgroup.path,
+ component: LVM2VolumeGroupCard,
+ props: { vgroup },
+ actions: [
+ {
+ title: _("Add physical volume"),
+ action: () => add_disk(vgroup),
+ tag: "pvols",
+ },
+ {
+ title: _("Delete group"),
+ action: () => vgroup_delete(client, vgroup, vgroup_card),
+ danger: true,
+ tag: "group",
+ },
+ ],
+ });
+
+ const lvols_card = new_card({
+ title: _("LVM2 logical volumes"),
+ next: vgroup_card,
+ has_warning: has_missing_pvs,
+ component: LVM2LogicalVolumesCard,
+ props: { vgroup },
+ actions: [
+ {
+ title: _("Create new logical volume"),
+ action: () => create_logical_volume(client, vgroup),
+ excuse: lvol_excuse,
+ tag: "group",
+ },
+ ],
+ });
+
+ const vgroup_page = new_page(parent, lvols_card);
+ make_logical_volume_pages(vgroup_page, vgroup);
+}
+
+function vgroup_poller(vgroup) {
+ let timer = null;
+
+ if (vgroup.NeedsPolling) {
+ timer = window.setInterval(() => { vgroup.Poll() }, 2000);
+ }
+
+ function stop() {
+ if (timer)
+ window.clearInterval(timer);
+ }
+
+ return {
+ stop
+ };
+}
+
+const LVM2LogicalVolumesCard = ({ card, vgroup }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody className="contains-list">
+ <ChildrenTable emptyCaption={_("No logical volumes")}
+ aria-label={_("LVM2 logical volumes")}
+ page={card.page} />
+ </CardBody>
+ </StorageCard>
+ );
+};
+
+const LVM2VolumeGroupCard = ({ card, vgroup }) => {
+ const has_missing_pvs = vgroup.MissingPhysicalVolumes && vgroup.MissingPhysicalVolumes.length > 0;
+
+ useObject(() => vgroup_poller(vgroup),
+ poller => poller.stop(),
+ [vgroup]);
+
+ function is_partial_linear_lvol(block) {
+ const lvm2 = client.blocks_lvm2[block.path];
+ const lvol = lvm2 && client.lvols[lvm2.LogicalVolume];
+ return lvol && lvol.Layout == "linear" && client.lvols_status[lvol.path] == "partial";
+ }
+
+ function remove_missing() {
+ /* Calling vgroup.RemoveMissingPhysicalVolumes will
+ implicitly delete all partial, linear logical volumes.
+ Instead of allowing this, we explicitly delete these
+ volumes before calling RemoveMissingPhysicalVolumes.
+ This allows us to kill processes that keep them busy
+ and remove their fstab entries.
+
+ RemoveMissingPhysicalVolumes leaves non-linear volumes
+ alone, even if they can't be repaired anymore. This is
+ a bit inconsistent, but *shrug*.
+ */
+
+ let usage = get_active_usage(client, vgroup.path, _("delete"));
+ usage = usage.filter(u => u.block && is_partial_linear_lvol(u.block));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"),
+ vgroup.Name),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ dialog_open({
+ Title: _("Remove missing physical volumes?"),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Title: _("Remove"),
+ action: function () {
+ return teardown_active_usage(client, usage)
+ .then(function () {
+ return for_each_async(usage,
+ u => {
+ const lvm2 = client.blocks_lvm2[u.block.path];
+ const lvol = lvm2 && client.lvols[lvm2.LogicalVolume];
+ return lvol.Delete({ 'tear-down': { t: 'b', v: true } });
+ })
+ .then(() => vgroup.RemoveMissingPhysicalVolumes({}));
+ });
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+ }
+
+ const alerts = [];
+ if (has_missing_pvs)
+ alerts.push(
+ <Alert variant='warning' isInline key="missing"
+ actionClose={<StorageButton onClick={remove_missing}>{_("Dismiss")}</StorageButton>}
+ title={_("This volume group is missing some physical volumes.")}>
+ {vgroup.MissingPhysicalVolumes.map(uuid => <div key={uuid}>{uuid}</div>)}
+ </Alert>);
+
+ return (
+ <StorageCard card={card} alerts={alerts}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Name")}
+ value={vgroup.Name}
+ action={<StorageLink onClick={() => vgroup_rename(client, vgroup, card)}
+ excuse={has_missing_pvs && _("A volume group with missing physical volumes can not be renamed.")}>
+ {_("edit")}
+ </StorageLink>} />
+ <StorageDescription title={_("UUID")} value={vgroup.UUID} />
+ <StorageDescription title={_("Capacity")} value={fmt_size_long(vgroup.Size)} />
+ </DescriptionList>
+ </CardBody>
+ <CardHeader><strong>{_("Physical volumes")}</strong></CardHeader>
+ <CardBody className="contains-list">
+ <PageTable emptyCaption={_("No physical volumes found")}
+ aria-label={_("LVM2 physical volumes")}
+ crossrefs={get_crossrefs(vgroup)} />
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/manifest.json b/pkg/storaged/manifest.json
new file mode 100644
index 0000000..82c6c36
--- /dev/null
+++ b/pkg/storaged/manifest.json
@@ -0,0 +1,75 @@
+{
+ "name": "storage",
+ "requires": {
+ "cockpit": "266"
+ },
+ "conditions": [
+ {"path-exists": "/usr/share/dbus-1/system.d/org.freedesktop.UDisks2.conf"}
+ ],
+
+ "menu": {
+ "index": {
+ "label": "Storage",
+ "order": 30,
+ "docs": [
+ {
+ "label": "Managing partitions",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-partitions-using-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing NFS mounts",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-nfs-mounts-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing RAIDs",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-redundant-arrays-of-independent-disks-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing LVMs",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/using-the-web-console-for-configuring-lvm-logical-volumes_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing physical drives",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/using-the-web-console-for-changing-physical-drives-in-volume-groups_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Managing VDOs",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/using-the-web-console-for-managing-virtual-data-optimizer-volumes_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Using LUKS encryption",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/locking-data-with-luks-password-in-the-rhel-web-console_system-management-using-the-rhel-8-web-console"
+ },
+ {
+ "label": "Using Tang server",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/configuring-automated-unlocking-using-a-tang-key-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["filesystem", "partition", "nfs", "raid", "volume", "disk", "vdo", "iscsi", "drive", "mount", "unmount", "udisks", "mkfs", "format", "fstab", "lvm2", "luks", "encryption", "nbde", "tang"]
+ }
+ ]
+ }
+ },
+
+ "config": {
+ "nfs_client_package": {
+ "rhel": "nfs-utils", "fedora": "nfs-utils",
+ "opensuse": "nfs-client", "opensuse-leap": "nfs-client",
+ "debian": "nfs-common", "ubuntu": "nfs-common",
+ "arch": "nfs-utils"
+ },
+ "vdo_package": { "rhel": "vdo", "centos": "vdo" },
+ "stratis_package": { "fedora": "stratisd",
+ "centos": "stratisd",
+ "arch": "stratisd",
+ "platform:el9": "stratisd"
+ },
+ "nbde_root_help": { "fedora": true,
+ "centos": true,
+ "rhel": true
+ }
+ },
+ "content-security-policy": "img-src 'self' data:"
+}
diff --git a/pkg/storaged/mdraid/create-dialog.jsx b/pkg/storaged/mdraid/create-dialog.jsx
new file mode 100644
index 0000000..19f0495
--- /dev/null
+++ b/pkg/storaged/mdraid/create-dialog.jsx
@@ -0,0 +1,122 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import client from "../client";
+
+import { mdraid_name, validate_mdraid_name, get_available_spaces, prepare_available_spaces } from "../utils.js";
+import { dialog_open, TextInput, SelectOne, SelectSpaces } from "../dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export function create_mdraid() {
+ function mdraid_exists(name) {
+ for (const p in client.mdraids) {
+ if (mdraid_name(client.mdraids[p]) == name)
+ return true;
+ }
+ return false;
+ }
+
+ let name;
+ for (let i = 0; i < 1000; i++) {
+ name = "raid" + i.toFixed();
+ if (!mdraid_exists(name))
+ break;
+ }
+
+ dialog_open({
+ Title: _("Create RAID device"),
+ Fields: [
+ TextInput("name", _("Name"), {
+ value: name,
+ validate: validate_mdraid_name,
+ }),
+ SelectOne("level", _("RAID level"),
+ {
+ value: "raid5",
+ choices: [
+ {
+ value: "raid0",
+ title: _("RAID 0 (stripe)")
+ },
+ {
+ value: "raid1",
+ title: _("RAID 1 (mirror)")
+ },
+ {
+ value: "raid4",
+ title: _("RAID 4 (dedicated parity)")
+ },
+ {
+ value: "raid5",
+ title: _("RAID 5 (distributed parity)")
+ },
+ {
+ value: "raid6",
+ title: _("RAID 6 (double distributed parity)")
+ },
+ {
+ value: "raid10",
+ title: _("RAID 10 (stripe of mirrors)")
+ }
+ ]
+ }),
+ SelectOne("chunk", _("Chunk size"),
+ {
+ value: "512",
+ visible: function (vals) {
+ return vals.level != "raid1";
+ },
+ choices: [
+ { value: "4", title: _("4 KiB") },
+ { value: "8", title: _("8 KiB") },
+ { value: "16", title: _("16 KiB") },
+ { value: "32", title: _("32 KiB") },
+ { value: "64", title: _("64 KiB") },
+ { value: "128", title: _("128 KiB") },
+ { value: "512", title: _("512 KiB") },
+ { value: "1024", title: _("1 MiB") },
+ { value: "2048", title: _("2 MiB") }
+ ]
+ }),
+ SelectSpaces("disks", _("Disks"),
+ {
+ empty_warning: _("No disks are available."),
+ validate: function (disks, vals) {
+ const disks_needed = vals.level == "raid6" ? 4 : 2;
+ if (disks.length < disks_needed)
+ return cockpit.format(cockpit.ngettext("At least $0 disk is needed.", "At least $0 disks are needed.", disks_needed),
+ disks_needed);
+ },
+ spaces: get_available_spaces(client)
+ })
+ ],
+ Action: {
+ Title: _("Create"),
+ action: function (vals) {
+ return prepare_available_spaces(client, vals.disks).then(paths => {
+ return client.manager.MDRaidCreate(paths, vals.level,
+ vals.name, (vals.chunk || 0) * 1024,
+ { });
+ });
+ }
+ }
+ });
+}
diff --git a/pkg/storaged/mdraid/mdraid-disk.jsx b/pkg/storaged/mdraid/mdraid-disk.jsx
new file mode 100644
index 0000000..21d0e4d
--- /dev/null
+++ b/pkg/storaged/mdraid/mdraid-disk.jsx
@@ -0,0 +1,135 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { StorageCard, StorageDescription, new_card, register_crossref } from "../pages.jsx";
+import { format_dialog } from "../block/format-dialog.jsx";
+import { block_short_name, fmt_size, mdraid_name } from "../utils.js";
+import { std_lock_action } from "../crypto/actions.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_mdraid_disk_card(next, backing_block, content_block) {
+ const mdraid = client.mdraids[content_block.MDRaidMember];
+ const mdraid_block = mdraid && client.mdraids_block[mdraid.path];
+
+ const disk_card = new_card({
+ title: _("MDRAID disk"),
+ next,
+ location: mdraid_block ? block_short_name(mdraid_block) : (mdraid ? mdraid_name(mdraid) : null),
+ component: MDRaidDiskCard,
+ props: { backing_block, content_block, mdraid },
+ actions: [
+ std_lock_action(backing_block, content_block),
+ { title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true },
+ ]
+ });
+
+ if (mdraid) {
+ const members = client.mdraids_members[mdraid.path] || [];
+ let n_spares = 0;
+ let n_recovering = 0;
+ mdraid.ActiveDevices.forEach(function(as) {
+ if (as[2].indexOf("spare") >= 0) {
+ if (as[1] < 0)
+ n_spares += 1;
+ else
+ n_recovering += 1;
+ }
+ });
+
+ const active_state = mdraid.ActiveDevices.find(as => as[0] == content_block.path);
+
+ const state_text = (state) => {
+ return {
+ faulty: _("Failed"),
+ in_sync: _("In sync"),
+ spare: active_state[1] < 0 ? _("Spare") : _("Recovering"),
+ write_mostly: _("Write-mostly"),
+ blocked: _("Blocked")
+ }[state] || cockpit.format(_("Unknown ($0)"), state);
+ };
+
+ const slot = active_state && active_state[1] >= 0 && active_state[1].toString();
+ let states = active_state && active_state[2].map(state_text).join(", ");
+
+ if (slot)
+ states = cockpit.format(_("Slot $0"), slot) + ", " + states;
+
+ const is_in_sync = (active_state && active_state[2].indexOf("in_sync") >= 0);
+ const is_recovering = (active_state && active_state[2].indexOf("spare") >= 0 && active_state[1] >= 0);
+
+ let remove_excuse = null;
+ if (!mdraid_block)
+ remove_excuse = _("The MDRAID device must be running");
+ else if ((is_in_sync && n_recovering > 0) || is_recovering)
+ remove_excuse = _("MDRAID device is recovering");
+ else if (is_in_sync && n_spares < 1)
+ remove_excuse = _("Need a spare disk");
+ else if (members.length <= 1)
+ remove_excuse = _("Last disk can not be removed");
+
+ let remove_action = null;
+ if (mdraid.Level != "raid0")
+ remove_action = {
+ title: _("Remove"),
+ action: () => mdraid.RemoveDevice(content_block.path, { wipe: { t: 'b', v: true } }),
+ excuse: remove_excuse
+ };
+
+ register_crossref({
+ key: mdraid,
+ card: disk_card,
+ actions: [
+ remove_action
+ ],
+ size: fmt_size(content_block.Size),
+ extra: states,
+ });
+ }
+
+ return disk_card;
+}
+
+export const MDRaidDiskCard = ({ card, backing_block, content_block, mdraid }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("MDRAID device")}>
+ {mdraid
+ ? <Button variant="link" isInline role="link"
+ onClick={() => cockpit.location.go(["mdraid", mdraid.UUID])}>
+ {mdraid_name(mdraid)}
+ </Button>
+ : "-"
+ }
+ </StorageDescription>
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/mdraid/mdraid.jsx b/pkg/storaged/mdraid/mdraid.jsx
new file mode 100644
index 0000000..d366d95
--- /dev/null
+++ b/pkg/storaged/mdraid/mdraid.jsx
@@ -0,0 +1,307 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { CardHeader, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { VolumeIcon } from "../icons/gnome-icons.jsx";
+import { StorageButton, StorageLink } from "../storage-controls.jsx";
+import {
+ StorageCard, StorageDescription, PageTable,
+ new_page, new_card, PAGE_CATEGORY_VIRTUAL,
+ get_crossrefs, navigate_away_from_card
+} from "../pages.jsx";
+import { make_block_page } from "../block/create-pages.jsx";
+import {
+ block_short_name, mdraid_name, encode_filename, decode_filename,
+ fmt_size, fmt_size_long, get_active_usage, teardown_active_usage,
+ get_available_spaces, prepare_available_spaces,
+ reload_systemd, should_ignore,
+} from "../utils.js";
+
+import {
+ dialog_open, SelectSpaces,
+ BlockingMessage, TeardownMessage,
+ init_active_usage_processes
+} from "../dialog.jsx";
+
+import { partitionable_block_actions } from "../partitions/actions.jsx";
+
+const _ = cockpit.gettext;
+
+function mdraid_start(mdraid) {
+ return mdraid.Start({ "start-degraded": { t: 'b', v: true } });
+}
+
+function mdraid_stop(mdraid) {
+ const block = client.mdraids_block[mdraid.path];
+ const usage = get_active_usage(client, block ? block.path : "", _("stop"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), mdraid_name(mdraid)),
+ Body: BlockingMessage(usage),
+ });
+ return;
+ }
+
+ if (usage.Teardown) {
+ dialog_open({
+ Title: cockpit.format(_("Confirm stopping of $0"),
+ mdraid_name(mdraid)),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Title: _("Stop device"),
+ action: function () {
+ return teardown_active_usage(client, usage)
+ .then(function () {
+ return mdraid.Stop({});
+ });
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+ return;
+ }
+
+ return mdraid.Stop({});
+}
+
+function mdraid_delete(mdraid, block, card) {
+ function delete_() {
+ if (mdraid.Delete)
+ return mdraid.Delete({ 'tear-down': { t: 'b', v: true } }).then(reload_systemd);
+
+ // If we don't have a Delete method, we simulate
+ // it by stopping the array and wiping all
+ // members.
+
+ function wipe_members() {
+ return Promise.all(client.mdraids_members[mdraid.path].map(member => member.Format('empty', { })));
+ }
+
+ if (mdraid.ActiveDevices && mdraid.ActiveDevices.length > 0)
+ return mdraid.Stop({}).then(wipe_members);
+ else
+ return wipe_members();
+ }
+
+ const usage = get_active_usage(client, block ? block.path : "", _("delete"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), mdraid_name(mdraid)),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Permanently delete $0?"), mdraid_name(mdraid)),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Title: _("Delete"),
+ Danger: _("Deleting erases all data on a MDRAID device."),
+ action: async function () {
+ await teardown_active_usage(client, usage);
+ await delete_();
+ navigate_away_from_card(card);
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+}
+
+function add_disk(mdraid) {
+ function filter_inside_mdraid(spc) {
+ let block = spc.block;
+ if (client.blocks_part[block.path])
+ block = client.blocks[client.blocks_part[block.path].Table];
+ return block && block.MDRaid != mdraid.path;
+ }
+
+ function rescan(path) {
+ // mdraid often forgets to trigger udev, let's do it explicitly
+ return client.wait_for(() => client.blocks[path]).then(block => block.Rescan({ }));
+ }
+
+ dialog_open({
+ Title: _("Add disks"),
+ Fields: [
+ SelectSpaces("disks", _("Disks"),
+ {
+ empty_warning: _("No disks are available."),
+ validate: function (disks) {
+ if (disks.length === 0)
+ return _("At least one disk is needed.");
+ },
+ spaces: get_available_spaces(client).filter(filter_inside_mdraid)
+ })
+ ],
+ Action: {
+ Title: _("Add"),
+ action: function(vals) {
+ return prepare_available_spaces(client, vals.disks).then(paths =>
+ Promise.all(paths.map(p => mdraid.AddDevice(p, {}).then(() => rescan(p)))));
+ }
+ }
+ });
+}
+
+function missing_bitmap(mdraid) {
+ return (mdraid.Level != "raid0" &&
+ client.mdraids_members[mdraid.path].some(m => m.Size > 100 * 1024 * 1024 * 1024) &&
+ mdraid.BitmapLocation && decode_filename(mdraid.BitmapLocation) == "none");
+}
+
+export function make_mdraid_page(parent, mdraid) {
+ const block = client.mdraids_block[mdraid.path];
+
+ if (block && should_ignore(client, block.path))
+ return;
+
+ if (!block && client.in_anaconda_mode())
+ return;
+
+ let add_excuse = false;
+ if (!block)
+ add_excuse = _("MDRAID device must be running");
+
+ const mdraid_card = new_card({
+ title: _("MDRAID device"),
+ next: null,
+ page_location: ["mdraid", mdraid.UUID],
+ page_name: block ? block_short_name(block) : mdraid_name(mdraid),
+ page_icon: VolumeIcon,
+ page_category: PAGE_CATEGORY_VIRTUAL,
+ page_size: mdraid.Size,
+ type_extra: !block && _("stopped"),
+ id_extra: block && _("MDRAID device"),
+ for_summary: true,
+ has_warning: mdraid.Degraded > 0 || missing_bitmap(mdraid),
+ job_path: mdraid.path,
+ component: MDRaidCard,
+ props: { mdraid, block },
+ actions: [
+ (!block &&
+ {
+ title: _("Start"),
+ action: () => mdraid_start(mdraid),
+ tag: "device"
+ }),
+ (mdraid.Level != "raid0" &&
+ {
+ title: _("Add disk"),
+ action: () => add_disk(mdraid),
+ excuse: add_excuse,
+ tag: "disks",
+ }),
+ ...(block ? partitionable_block_actions(block, "device") : []),
+ {
+ title: _("Delete"),
+ action: () => mdraid_delete(mdraid, block, mdraid_card),
+ danger: true,
+ },
+ ],
+ });
+
+ if (!block) {
+ new_page(parent, mdraid_card);
+ } else
+ make_block_page(parent, block, mdraid_card);
+}
+
+const MDRaidCard = ({ card, mdraid, block }) => {
+ function format_level(str) {
+ return {
+ raid0: _("RAID 0"),
+ raid1: _("RAID 1"),
+ raid4: _("RAID 4"),
+ raid5: _("RAID 5"),
+ raid6: _("RAID 6"),
+ raid10: _("RAID 10")
+ }[str] || cockpit.format(_("RAID ($0)"), str);
+ }
+
+ let level = format_level(mdraid.Level);
+ if (mdraid.NumDevices > 0)
+ level += ", " + cockpit.format(_("$0 disks"), mdraid.NumDevices);
+ if (mdraid.ChunkSize > 0)
+ level += ", " + cockpit.format(_("$0 chunk size"), fmt_size(mdraid.ChunkSize));
+
+ const alerts = [];
+ if (mdraid.Degraded > 0) {
+ const text = cockpit.format(
+ cockpit.ngettext("$0 disk is missing", "$0 disks are missing", mdraid.Degraded),
+ mdraid.Degraded
+ );
+ alerts.push(
+ <Alert isInline variant="danger" key="degraded"
+ title={_("The MDRAID device is in a degraded state")}>
+ {text}
+ </Alert>);
+ }
+
+ function fix_bitmap() {
+ return mdraid.SetBitmapLocation(encode_filename("internal"), { });
+ }
+
+ if (missing_bitmap(mdraid)) {
+ alerts.push(
+ <Alert isInline variant="warning" key="bitmap"
+ title={_("This MDRAID device has no write-intent bitmap. Such a bitmap can reduce sychronization times significantly.")}>
+ <div className="storage-alert-actions">
+ <StorageButton onClick={fix_bitmap}>{_("Add a bitmap")}</StorageButton>
+ </div>
+ </Alert>);
+ }
+
+ return (
+ <StorageCard card={card} alerts={alerts}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Name")} value={mdraid_name(mdraid)} />
+ <StorageDescription title={_("RAID level")} value={level} />
+ <StorageDescription title={_("State")} value={block ? _("Running") : _("Not running")}
+ action={block && <StorageLink onClick={() => mdraid_stop(mdraid)}>
+ {_("Stop")}
+ </StorageLink>} />
+ <StorageDescription title={_("UUID")} value={mdraid.UUID} />
+ <StorageDescription title={_("Device")} value={block ? decode_filename(block.PreferredDevice) : "-"} />
+ <StorageDescription title={_("Capacity")} value={fmt_size_long(mdraid.Size)} />
+ </DescriptionList>
+ </CardBody>
+ <CardHeader><strong>{_("Disks")}</strong></CardHeader>
+ <CardBody className="contains-list">
+ <PageTable emptyCaption={_("No disks found")}
+ aria-label={_("MDRAID disks")}
+ crossrefs={get_crossrefs(mdraid)} />
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/mount-users.py b/pkg/storaged/mount-users.py
new file mode 100644
index 0000000..88460f6
--- /dev/null
+++ b/pkg/storaged/mount-users.py
@@ -0,0 +1,164 @@
+#! /usr/bin/python3
+
+import json
+import os
+import pwd
+import signal
+import subprocess
+import sys
+import time
+
+# mount-users -- Find and terminate processes that keep a mount busy
+
+
+def fuser(mount_point):
+
+ import dbus
+ bus = dbus.SystemBus()
+ systemd_manager = dbus.Interface(bus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1'),
+ dbus_interface='org.freedesktop.systemd1.Manager')
+
+ results = {}
+
+ def get_cmdline(pid):
+ with open(f"/proc/{pid}/cmdline") as f:
+ return " ".join(f.read().split("\0"))
+
+ def get_stat(pid):
+ with open(f"/proc/{pid}/stat") as f:
+ stat = f.read()
+ # Field two is everything between the first "(" and the last ")", the rest is space separated.
+ comm_start = stat.index("(") + 1
+ comm_end = stat.rindex(")")
+ return ([stat[0:comm_start - 1].strip(), stat[comm_start:comm_end], *list(filter(lambda f: f != '', stat[comm_end + 1:-1].split(' ')))])
+
+ def get_loginuser(pid):
+ uid = os.stat("/proc/%s" % pid).st_uid
+ try:
+ return pwd.getpwuid(uid).pw_name
+ except OSError:
+ return uid
+
+ def check(path, pid):
+ t = os.readlink(path)
+ if t == mount_point or t.startswith(mount_point + "/"):
+ unit = systemd_manager.GetUnitByPID(int(pid))
+ if unit not in results:
+ unit_obj = bus.get_object('org.freedesktop.systemd1', unit)
+ unit_id = unit_obj.Get("org.freedesktop.systemd1.Unit", "Id",
+ dbus_interface="org.freedesktop.DBus.Properties")
+ if unit_id.endswith(".scope"):
+ stat = get_stat(pid)
+ start = int(stat[21]) / os.sysconf('SC_CLK_TCK')
+ results[pid] = {"pid": int(pid),
+ "cmd": get_cmdline(pid),
+ "comm": stat[1],
+ "user": get_loginuser(pid),
+ "since": time.clock_gettime(time.CLOCK_MONOTONIC) - start}
+ else:
+ desc = unit_obj.Get("org.freedesktop.systemd1.Unit", "Description",
+ dbus_interface="org.freedesktop.DBus.Properties")
+ timestamp = unit_obj.Get("org.freedesktop.systemd1.Unit", "ActiveEnterTimestamp",
+ dbus_interface="org.freedesktop.DBus.Properties")
+ results[unit] = {"unit": unit_id,
+ "cmd": get_cmdline(pid),
+ "desc": desc,
+ "user": get_loginuser(pid),
+ "since": time.time() - timestamp / 1e6}
+ return True
+ return False
+
+ def checkdir(path, pid):
+ for f in os.listdir(path):
+ if check(os.path.join(path, f), pid):
+ return True
+ return False
+
+ my_pid = os.getpid()
+
+ for p in os.listdir("/proc/"):
+ if not p.isdigit():
+ continue
+ if int(p) == my_pid:
+ continue
+ proc = "/proc/%s/" % p
+ try:
+ if check(proc + "exe", p):
+ continue
+ if check(proc + "root", p):
+ continue
+ if check(proc + "cwd", p):
+ continue
+ if checkdir(proc + "fd", p):
+ continue
+ if checkdir(proc + "map_files", p):
+ continue
+ except OSError:
+ pass
+
+ return list(results.values())
+
+
+def users(mount_point):
+ data = fuser(mount_point)
+ sys.stdout.write(json.dumps(data) + "\n")
+ sys.stdout.flush()
+
+
+def stop_pids(pids):
+ def sendsig(pid, sig):
+ try:
+ os.kill(pid, sig)
+ return True
+ except Exception:
+ return False
+
+ def sendsigs(pids, sig):
+ for p in pids:
+ sendsig(p, sig)
+
+ def pollpids(pids, count, interval):
+ while count > 0:
+ i = 0
+ while i < len(pids):
+ if not sendsig(pids[i], 0):
+ pids.pop(i)
+ else:
+ i += 1
+ if len(pids) == 0:
+ break
+ count -= 1
+ time.sleep(interval)
+
+ sendsigs(pids, signal.SIGTERM)
+ sendsigs(pids, signal.SIGHUP)
+ pollpids(pids, 10, 0.1)
+ pollpids(pids, 19, 1)
+ sendsigs(pids, signal.SIGKILL)
+ pollpids(pids, 10, 0.1)
+
+
+def stop_units(units):
+ if len(units) > 0:
+ subprocess.check_call(['systemctl', 'stop', *units])
+
+
+def stop(users):
+ stop_pids([u["pid"] for u in users if "pid" in u])
+ stop_units([u["unit"] for u in users if "unit" in u])
+
+
+def dispatch(argv):
+ if argv[1] == "users":
+ users(argv[2])
+ elif argv[1] == "stop":
+ stop(json.loads(argv[2]))
+
+
+try:
+ dispatch(sys.argv)
+except subprocess.CalledProcessError as e:
+ sys.exit(e.returncode)
+except Exception as e:
+ sys.stderr.write(str(e) + "\n")
+ sys.exit(1)
diff --git a/pkg/storaged/multipath.jsx b/pkg/storaged/multipath.jsx
new file mode 100644
index 0000000..140d240
--- /dev/null
+++ b/pkg/storaged/multipath.jsx
@@ -0,0 +1,77 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Alert, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+
+import { get_multipathd_service } from "./utils.js";
+import { dialog_open } from "./dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export class MultipathAlert extends React.Component {
+ constructor() {
+ super();
+ this.multipathd_service = get_multipathd_service();
+ this.on_multipathd_changed = () => { this.setState({}) };
+ }
+
+ componentDidMount() {
+ this.multipathd_service.addEventListener("changed", this.on_multipathd_changed);
+ }
+
+ componentWillUnmount() {
+ this.multipathd_service.removeEventListener("changed", this.on_multipathd_changed);
+ }
+
+ render() {
+ const { client } = this.props;
+
+ // When in doubt, assume everything is alright
+ const multipathd_running = !this.multipathd_service.state || this.multipathd_service.state === "running";
+ const multipath_broken = client.broken_multipath_present === true;
+
+ function activate(event) {
+ if (!event || event.button !== 0)
+ return;
+ cockpit.spawn(["mpathconf", "--enable", "--with_multipathd", "y"],
+ { superuser: "try" })
+ .catch(function (error) {
+ dialog_open({
+ Title: _("Error"),
+ Body: error.toString()
+ });
+ });
+ }
+
+ if (multipath_broken && !multipathd_running)
+ return (
+ <StackItem>
+ <Alert isInline variant='danger'
+ actionClose={<AlertActionLink variant='secondary' onClick={activate}>{_("Start multipath")}</AlertActionLink>}
+ title={_("There are devices with multiple paths on the system, but the multipath service is not running.")}
+ />
+ </StackItem>
+ );
+ return null;
+ }
+}
diff --git a/pkg/storaged/nfs/nfs-mounts.py b/pkg/storaged/nfs/nfs-mounts.py
new file mode 100755
index 0000000..7b8fd97
--- /dev/null
+++ b/pkg/storaged/nfs/nfs-mounts.py
@@ -0,0 +1,245 @@
+#! /usr/bin/python3
+
+# nfs-mounts -- Monitor and manage NFS mounts
+#
+# This is similar to how UDisks2 monitors and manages block device
+# mounts, but for NFS. This might be moved into UDisks2, or not.
+
+# We monitor all NFS remotes listed in /etc/fstab and in /proc/self/mounts.
+# If a entry from mtab is also found in fstab, we report only the
+# fstab entry and mark it is "mounted".
+
+import json
+import os
+import re
+import select
+import subprocess
+import sys
+
+
+class Watcher:
+ def __init__(self, path):
+ self.inotify = Inotify()
+ self.path = path
+ self.setup()
+
+ def setup(self):
+ self.wd = self.inotify.add_watch(self.path, IN_CLOSE_WRITE)
+
+ def process(self, callback=None):
+ def event(_wd, mask, name):
+ if callback:
+ callback()
+ if mask & IN_IGNORED:
+ self.setup()
+ self.inotify.process(event)
+
+
+def field_escape(field):
+ return field.replace("\\", "\\134").replace(" ", "\\040").replace("\t", "\\011")
+
+
+def field_unescape(field):
+ return re.sub("\\\\([0-7]{1,3})", lambda m: chr(int(m.group(1), 8)), field)
+
+
+def parse_tab(name):
+ entries = []
+ with open(name, "r") as f:
+ for line in f:
+ sline = line.strip()
+ if sline == "" or sline[0] == "#":
+ continue
+ fields = list(map(field_unescape, re.split("[ \t]+", sline)))
+ if len(fields) > 2 and ":" in fields[0] and fields[2].startswith("nfs"):
+ entries.append(fields)
+ return entries
+
+
+def index_tab(tab):
+ by_remote = {}
+ for t in tab:
+ if t[0] not in by_remote:
+ by_remote[t[0]] = []
+ by_remote[t[0]].append(t)
+ return by_remote
+
+
+def modify_tab(name, modify):
+ with open(name) as f:
+ lines = f.read().splitlines()
+
+ new_lines = []
+ for line in lines:
+ sline = line.strip()
+ if sline == "" or sline[0] == "#":
+ new_lines.append(line)
+ else:
+ fields = list(map(field_unescape, re.split("[ \t]+", sline)))
+ if len(fields) > 0 and ":" in fields[0]:
+ new_fields = modify(fields)
+ if new_fields:
+ if new_fields == fields:
+ new_lines.append(line)
+ else:
+ new_lines.append(" ".join(map(field_escape, new_fields)))
+ else:
+ new_lines.append(line)
+ new_fields = modify(None)
+ if new_fields:
+ new_lines.append(" ".join(map(field_escape, new_fields)))
+
+ with open(name + ".tmp", "w") as f:
+ f.write("\n".join(new_lines) + "\n")
+ f.flush()
+ os.fsync(f.fileno())
+ os.rename(name + ".tmp", name)
+
+
+fstab = []
+fstab_by_remote = {}
+
+mtab = []
+mtab_by_remote = {}
+
+
+def process_fstab():
+ global fstab, fstab_by_remote
+ fstab = parse_tab("/etc/fstab")
+ fstab_by_remote = index_tab(fstab)
+
+
+def process_mtab():
+ global mtab, mtab_by_remote
+ mtab = parse_tab("/proc/self/mounts")
+ mtab_by_remote = index_tab(mtab)
+
+
+def find_in_tab(tab_by_remote, fields):
+ for t in tab_by_remote.get(fields[0], []):
+ if t[0] == fields[0] and t[1] == fields[1]:
+ return t
+ return None
+
+
+def report():
+ data = []
+ for f in fstab:
+ m = find_in_tab(mtab_by_remote, f)
+ data.append({"fstab": True, "fields": f, "mounted": m is not None})
+ for m in mtab:
+ if not find_in_tab(fstab_by_remote, m):
+ data.append({"fstab": False, "fields": m, "mounted": True})
+ sys.stdout.write(json.dumps(data) + "\n")
+ sys.stdout.flush()
+
+
+def monitor():
+ process_mtab()
+ process_fstab()
+ report()
+ fstab_watcher = Watcher("/etc/fstab")
+ mtab_file = open("/proc/self/mounts", "r")
+ while True:
+ (r, w, x) = select.select([fstab_watcher.inotify.fd], [], [mtab_file])
+ if mtab_file in x:
+ process_mtab()
+ report()
+ if fstab_watcher.inotify.fd in r:
+ fstab_watcher.process()
+ process_fstab()
+ report()
+
+
+def mkdir_if_necessary(path):
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+
+def rmdir_maybe(path):
+ try:
+ os.rmdir(path)
+ except OSError:
+ pass
+
+
+def update(entry, new_fields):
+ old_fields = entry["fields"]
+ if old_fields[1] != new_fields[1]:
+ mkdir_if_necessary(new_fields[1])
+ if entry["mounted"]:
+ if (new_fields[0] == old_fields[0] and
+ new_fields[1] == old_fields[1] and
+ new_fields[2] == old_fields[2]):
+ remount(new_fields)
+ else:
+ try:
+ unmount(entry)
+ if old_fields[1] != new_fields[1]:
+ rmdir_maybe(old_fields[1])
+ except subprocess.CalledProcessError:
+ pass
+ mount({"fields": new_fields})
+ else:
+ if old_fields[1] != new_fields[1]:
+ rmdir_maybe(old_fields[1])
+ modify_tab("/etc/fstab", lambda fields: new_fields if fields == old_fields else fields)
+
+
+def add(new_fields):
+ mkdir_if_necessary(new_fields[1])
+ mount({"fields": new_fields})
+ modify_tab("/etc/fstab", lambda fields: new_fields if fields is None else fields)
+
+
+def remove(entry):
+ old_fields = entry["fields"]
+ if entry["mounted"]:
+ unmount(entry)
+ rmdir_maybe(old_fields[1])
+ modify_tab("/etc/fstab", lambda fields: None if fields == old_fields else fields)
+
+
+def mount(entry):
+ fields = entry["fields"]
+ mkdir_if_necessary(fields[1])
+ subprocess.check_call(["mount",
+ "-t", fields[2],
+ "-o", fields[3],
+ fields[0],
+ fields[1]])
+
+
+def remount(fields):
+ subprocess.check_call(["mount",
+ "-o", "remount," + fields[3],
+ fields[0],
+ fields[1]])
+
+
+def unmount(entry):
+ subprocess.check_call(["umount", entry["fields"][1]])
+
+
+def dispatch(argv):
+ if argv[1] == "monitor":
+ monitor()
+ elif argv[1] == "update":
+ update(json.loads(argv[2]), json.loads(argv[3]))
+ elif argv[1] == "add":
+ add(json.loads(argv[2]))
+ elif argv[1] == "remove":
+ remove(json.loads(argv[2]))
+ elif argv[1] == "mount":
+ mount(json.loads(argv[2]))
+ elif argv[1] == "unmount":
+ unmount(json.loads(argv[2]))
+
+
+try:
+ dispatch(sys.argv)
+except subprocess.CalledProcessError as e:
+ sys.exit(e.returncode)
+except Exception as e:
+ sys.stderr.write(str(e) + "\n")
+ sys.exit(1)
diff --git a/pkg/storaged/nfs/nfs.jsx b/pkg/storaged/nfs/nfs.jsx
new file mode 100644
index 0000000..ccd5407
--- /dev/null
+++ b/pkg/storaged/nfs/nfs.jsx
@@ -0,0 +1,337 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { NetworkIcon } from "../icons/gnome-icons.jsx";
+import {
+ dialog_open, TextInput, ComboBox, CheckBoxes,
+ StopProcessesMessage, stop_processes_danger_message
+} from "../dialog.jsx";
+
+import { StorageUsageBar } from "../storage-controls.jsx";
+import {
+ StorageCard, StorageDescription,
+ new_page, new_card, PAGE_CATEGORY_NETWORK,
+ navigate_to_new_card_location, navigate_away_from_card
+} from "../pages.jsx";
+import { parse_options, unparse_options, extract_option } from "../utils.js";
+
+const _ = cockpit.gettext;
+
+function nfs_busy_dialog(dialog_title, entry, error, action_title, action) {
+ function show(users) {
+ if (users.length === 0) {
+ dialog_open({
+ Title: dialog_title,
+ Body: error.toString()
+ });
+ } else {
+ dialog_open({
+ Title: dialog_title,
+ Teardown: <StopProcessesMessage users={users} />,
+ Action: {
+ DangerButton: true,
+ Danger: stop_processes_danger_message(users),
+ Title: action_title,
+ action: function () {
+ return action(users);
+ }
+ }
+ });
+ }
+ }
+
+ client.nfs.entry_users(entry)
+ .then(function (users) {
+ show(users);
+ })
+ .catch(function () {
+ show([]);
+ });
+}
+
+function get_exported_directories(server) {
+ return cockpit.spawn(["showmount", "-e", "--no-headers", server], { err: "message" })
+ .then(function (output) {
+ const dirs = [];
+ output.split("\n").forEach(function (line) {
+ const d = line.split(" ")[0];
+ if (d)
+ dirs.push(d);
+ });
+ return dirs;
+ });
+}
+
+export function nfs_fstab_dialog(entry, card) {
+ const mount_options = entry ? entry.fields[3] : "defaults";
+ const split_options = parse_options(mount_options == "defaults" ? "" : mount_options);
+ const opt_auto = !extract_option(split_options, "noauto");
+ const opt_ro = extract_option(split_options, "ro");
+ const extra_options = unparse_options(split_options);
+
+ function mounting_options(vals) {
+ let opts = [];
+ if (!vals.mount_options.auto)
+ opts.push("noauto");
+ if (vals.mount_options.ro)
+ opts.push("ro");
+ if (vals.mount_options.extra !== false)
+ opts = opts.concat(parse_options(vals.mount_options.extra));
+ return unparse_options(opts);
+ }
+
+ function show(busy) {
+ let alert = null;
+ if (busy)
+ alert = <>
+ <Alert isInline variant="danger" title={_("This NFS mount is in use and only its options can be changed.")} />
+ <br />
+ </>;
+
+ let server_to_check = null;
+ let server_check_timeout = null;
+
+ function check_server(dlg, server, delay) {
+ if (server_check_timeout)
+ window.clearTimeout(server_check_timeout);
+ server_to_check = server;
+ server_check_timeout = window.setTimeout(() => {
+ server_check_timeout = null;
+ dlg.set_options("remote", { choices: [] });
+ get_exported_directories(server).then(choices => {
+ if (server == server_to_check)
+ dlg.set_options("remote", { choices });
+ });
+ }, delay);
+ }
+
+ const dlg = dialog_open({
+ Title: entry ? _("NFS mount") : _("New NFS mount"),
+ Body: alert,
+ Fields: [
+ TextInput("server", _("Server address"),
+ {
+ value: entry ? entry.fields[0].split(":")[0] : "",
+ validate: function (val) {
+ if (val === "")
+ return _("Server cannot be empty.");
+ },
+ disabled: busy
+ }),
+ ComboBox("remote", _("Path on server"),
+ {
+ value: entry ? entry.fields[0].split(":")[1] : "",
+ validate: function (val) {
+ if (val === "")
+ return _("Path on server cannot be empty.");
+ if (val[0] !== "/")
+ return _("Path on server must start with \"/\".");
+ },
+ disabled: busy,
+ choices: [],
+ }),
+ TextInput("dir", _("Local mount point"),
+ {
+ value: entry ? entry.fields[1] : "",
+ validate: function (val) {
+ if (val === "")
+ return _("Mount point cannot be empty.");
+ if (val[0] !== "/")
+ return _("Mount point must start with \"/\".");
+ },
+ disabled: busy
+ }),
+ CheckBoxes("mount_options", _("Mount options"),
+ {
+ fields: [
+ { title: _("Mount at boot"), tag: "auto" },
+ { title: _("Mount read only"), tag: "ro" },
+ { title: _("Custom mount options"), tag: "extra", type: "checkboxWithInput" },
+ ],
+ value: {
+ auto: opt_auto,
+ ro: opt_ro,
+ extra: extra_options === "" ? false : extra_options
+ }
+ },
+ ),
+ ],
+ update: (dlg, vals, trigger) => {
+ if (trigger === "server")
+ check_server(dlg, vals.server, 500);
+ },
+ Action: {
+ Title: entry ? _("Save") : _("Add"),
+ action: async function (vals) {
+ const fields = [
+ vals.server + ":" + vals.remote,
+ vals.dir,
+ entry ? entry.fields[2] : "nfs",
+ mounting_options(vals) || "defaults"
+ ];
+ if (entry) {
+ await client.nfs.update_entry(entry, fields);
+ if (entry.fields[0] != fields[0] || entry.fields[1] != fields[1])
+ navigate_to_new_card_location(card, ["nfs", fields[0], fields[1]]);
+ } else
+ await client.nfs.add_entry(fields);
+ }
+ }
+ });
+
+ if (entry && !busy)
+ check_server(dlg, entry.fields[0].split(":")[0], 0);
+ }
+
+ if (entry) {
+ client.nfs.entry_users(entry)
+ .then(function (users) {
+ show(users.length > 0);
+ })
+ .catch(function () {
+ show(false);
+ });
+ } else
+ show(false);
+}
+
+function checked(error_title, promise) {
+ promise.catch(error => {
+ dialog_open({
+ Title: error_title,
+ Body: error.toString()
+ });
+ });
+}
+
+function mount(entry) {
+ checked("Could not mount the filesystem",
+ client.nfs.mount_entry(entry));
+}
+
+function unmount(entry, card) {
+ client.nfs.unmount_entry(entry)
+ .then(function () {
+ if (!entry.fstab)
+ navigate_away_from_card(card);
+ })
+ .catch(function (error) {
+ nfs_busy_dialog(_("Unable to unmount filesystem"),
+ entry, error,
+ _("Stop and unmount"),
+ function (users) {
+ return client.nfs.stop_and_unmount_entry(users, entry)
+ .then(function () {
+ if (!entry.fstab)
+ navigate_away_from_card(card);
+ });
+ });
+ });
+}
+
+function edit(entry, card) {
+ nfs_fstab_dialog(entry, card);
+}
+
+function remove(entry, card) {
+ client.nfs.remove_entry(entry)
+ .then(function () {
+ navigate_away_from_card(card);
+ })
+ .catch(function (error) {
+ nfs_busy_dialog(_("Unable to remove mount"),
+ entry, error,
+ _("Stop and remove"),
+ function (users) {
+ return client.nfs.stop_and_remove_entry(users, entry)
+ .then(function () {
+ navigate_away_from_card(card);
+ });
+ });
+ });
+}
+
+const NfsEntryUsageBar = ({ entry, not_mounted_text, short }) => {
+ if (entry.mounted)
+ return <StorageUsageBar stats={client.nfs.get_fsys_size(entry)} critical={0.95} block={entry.fields[1]}
+ short={short} />;
+ else
+ return not_mounted_text;
+};
+
+export function make_nfs_page(parent, entry) {
+ if (client.in_anaconda_mode())
+ return;
+
+ const remote = entry.fields[0];
+ const local = entry.fields[1];
+ let mount_point = local;
+ if (!entry.mounted)
+ mount_point += " " + _("(not mounted)");
+
+ const nfs_card = new_card({
+ title: _("NFS mount"),
+ location: mount_point,
+ next: null,
+ page_location: ["nfs", remote, local],
+ page_name: remote,
+ page_icon: NetworkIcon,
+ page_category: PAGE_CATEGORY_NETWORK,
+ page_size: <NfsEntryUsageBar key="size" entry={entry} not_mounted_text={null} short />,
+ component: NfsCard,
+ props: { entry },
+ actions: [
+ (entry.mounted
+ ? { title: _("Unmount"), action: () => unmount(entry, nfs_card) }
+ : { title: _("Mount"), action: () => mount(entry) }),
+ (entry.fstab
+ ? { title: _("Edit"), action: () => edit(entry, nfs_card) }
+ : null),
+ (entry.fstab
+ ? { title: _("Remove"), action: () => remove(entry, nfs_card), danger: true }
+ : null),
+ ]
+ });
+
+ new_page(parent, nfs_card);
+}
+
+const NfsCard = ({ card, entry }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Server")} value={entry.fields[0]} />
+ <StorageDescription title={_("Mount point")} value={entry.fields[1]} />
+ <StorageDescription title={_("Size")}>
+ <NfsEntryUsageBar entry={entry} not_mounted_text="--" />
+ </StorageDescription>
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/overview/overview.jsx b/pkg/storaged/overview/overview.jsx
new file mode 100644
index 0000000..839e9b2
--- /dev/null
+++ b/pkg/storaged/overview/overview.jsx
@@ -0,0 +1,203 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { install_dialog } from "cockpit-components-install-dialog.jsx";
+
+import { Card, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { DropdownGroup, DropdownList } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
+
+import { StoragePlots } from "../plot.jsx";
+import { StorageMenuItem, StorageBarMenu } from "../storage-controls.jsx";
+import { dialog_open } from "../dialog.jsx";
+import { StorageLogsPanel } from "../logs-panel.jsx";
+
+import { create_mdraid } from "../mdraid/create-dialog.jsx";
+import { create_vgroup } from "../lvm2/create-dialog.jsx";
+import { create_stratis_pool } from "../stratis/create-dialog.jsx";
+import { iscsi_change_name, iscsi_discover } from "../iscsi/create-dialog.jsx";
+import { get_other_devices } from "../utils.js";
+
+import { new_page, new_card, StorageCard, ChildrenTable } from "../pages.jsx";
+import { make_drive_page } from "../drive/drive.jsx";
+import { make_lvm2_volume_group_page } from "../lvm2/volume-group.jsx";
+import { make_mdraid_page } from "../mdraid/mdraid.jsx";
+import { make_stratis_pool_page } from "../stratis/pool.jsx";
+import { make_stratis_stopped_pool_page } from "../stratis/stopped-pool.jsx";
+import { make_nfs_page, nfs_fstab_dialog } from "../nfs/nfs.jsx";
+import { make_iscsi_session_page } from "../iscsi/session.jsx";
+import { make_other_page } from "../block/other.jsx";
+import { make_btrfs_volume_page } from "../btrfs/volume.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_overview_page() {
+ const overview_card = new_card({
+ title: _("Storage"),
+ page_location: [],
+ page_name: _("Storage"),
+ component: OverviewCard
+ });
+
+ const overview_page = new_page(null, overview_card);
+
+ Object.keys(client.iscsi_sessions).forEach(p => make_iscsi_session_page(overview_page, client.iscsi_sessions[p]));
+ Object.keys(client.drives).forEach(p => {
+ if (!client.drives_iscsi_session[p])
+ make_drive_page(overview_page, client.drives[p]);
+ });
+ Object.keys(client.vgroups).forEach(p => make_lvm2_volume_group_page(overview_page, client.vgroups[p]));
+ Object.keys(client.mdraids).forEach(p => make_mdraid_page(overview_page, client.mdraids[p]));
+ Object.keys(client.stratis_pools).map(p => make_stratis_pool_page(overview_page, client.stratis_pools[p]));
+ Object.keys(client.stratis_manager.StoppedPools).map(uuid => make_stratis_stopped_pool_page(overview_page, uuid));
+ client.nfs.entries.forEach(e => make_nfs_page(overview_page, e));
+ get_other_devices(client).map(p => make_other_page(overview_page, client.blocks[p]));
+ Object.keys(client.uuids_btrfs_volume).forEach(uuid => make_btrfs_volume_page(overview_page, uuid));
+}
+
+const OverviewCard = ({ card, plot_state }) => {
+ function menu_item(feature, title, action) {
+ const feature_enabled = !feature || feature.is_enabled();
+ const required_package = feature && feature.package;
+
+ if (!feature_enabled && !(required_package && client.features.packagekit))
+ return null;
+
+ function install_then_action() {
+ if (!feature_enabled) {
+ install_dialog(required_package, feature.dialog_options).then(
+ () => {
+ feature.enable()
+ .then(action)
+ .catch(error => {
+ dialog_open({
+ Title: _("Error"),
+ Body: error.toString()
+ });
+ });
+ },
+ () => null /* ignore cancel */);
+ } else {
+ action();
+ }
+ }
+
+ return <StorageMenuItem key={title} onClick={install_then_action}>{title}</StorageMenuItem>;
+ }
+
+ const lvm2_feature = {
+ is_enabled: () => client.features.lvm2
+ };
+
+ const stratis_feature = {
+ is_enabled: () => client.features.stratis,
+ package: client.get_config("stratis_package", false),
+ enable: () => {
+ return cockpit.spawn(["systemctl", "start", "stratisd"], { superuser: true })
+ .then(() => client.stratis_start());
+ },
+
+ dialog_options: {
+ title: _("Install Stratis support"),
+ text: _("The $0 package must be installed to create Stratis pools.")
+ }
+ };
+
+ const nfs_feature = {
+ is_enabled: () => client.features.nfs,
+ package: client.get_config("nfs_client_package", false),
+ enable: () => {
+ client.features.nfs = true;
+ client.nfs.start();
+ return Promise.resolve();
+ },
+
+ dialog_options: {
+ title: _("Install NFS support")
+ }
+ };
+
+ const iscsi_feature = {
+ is_enabled: () => client.features.iscsi,
+ };
+
+ const local_menu_items = [
+ menu_item(null, _("Create MDRAID device"), () => create_mdraid()),
+ menu_item(lvm2_feature, _("Create LVM2 volume group"), () => create_vgroup()),
+ menu_item(stratis_feature, _("Create Stratis pool"), () => create_stratis_pool()),
+ ].filter(item => !!item);
+
+ const net_menu_items = [
+ !client.in_anaconda_mode() && menu_item(nfs_feature, _("New NFS mount"), () => nfs_fstab_dialog(null, null)),
+ menu_item(iscsi_feature, _("Change iSCSI initiater name"), () => iscsi_change_name()),
+ menu_item(iscsi_feature, _("Add iSCSI portal"), () => iscsi_discover()),
+ ].filter(item => !!item);
+
+ const groups = [];
+
+ if (local_menu_items.length > 0)
+ groups.push(
+ <DropdownGroup key="local" label={_("Local storage")}>
+ <DropdownList>
+ {local_menu_items}
+ </DropdownList>
+ </DropdownGroup>);
+
+ if (net_menu_items.length > 0)
+ groups.push(
+ <DropdownGroup key="net" label={_("Networked storage")}>
+ <DropdownList>
+ {net_menu_items}
+ </DropdownList>
+ </DropdownGroup>);
+
+ const actions = <StorageBarMenu label={_("Create storage device")} menuItems={groups} />;
+
+ return (
+ <Stack hasGutter>
+ { !client.in_anaconda_mode() &&
+ <StackItem>
+ <Card>
+ <CardBody>
+ <StoragePlots plot_state={plot_state} />
+ </CardBody>
+ </Card>
+ </StackItem>
+ }
+ <StackItem>
+ <StorageCard card={card} actions={actions}>
+ <CardBody className="contains-list">
+ <ChildrenTable emptyCaption={_("No storage found")}
+ aria-label={_("Storage")}
+ show_icons
+ page={card.page} />
+ </CardBody>
+ </StorageCard>
+ </StackItem>
+ { !client.in_anaconda_mode() &&
+ <StackItem>
+ <StorageLogsPanel />
+ </StackItem>
+ }
+ </Stack>);
+};
diff --git a/pkg/storaged/pages.jsx b/pkg/storaged/pages.jsx
new file mode 100644
index 0000000..496851c
--- /dev/null
+++ b/pkg/storaged/pages.jsx
@@ -0,0 +1,850 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React, { useState, useRef } from "react";
+import client from "./client";
+import { useEvent } from "hooks.js";
+
+import { AlertGroup } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Card, CardHeader, CardTitle, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DropdownGroup, DropdownList } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
+import { Bullseye } from "@patternfly/react-core/dist/esm/layouts/Bullseye/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table';
+import { EmptyState, EmptyStateBody } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
+import { ExclamationTriangleIcon, ExclamationCircleIcon } from "@patternfly/react-icons";
+import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+
+import { decode_filename, block_short_name, fmt_size } from "./utils.js";
+import { StorageButton, StorageBarMenu, StorageMenuItem, StorageSize } from "./storage-controls.jsx";
+import { MultipathAlert } from "./multipath.jsx";
+import { JobsPanel } from "./jobs-panel.jsx";
+import { Truncate } from "../lib/cockpit-components-truncate.jsx";
+
+const _ = cockpit.gettext;
+
+/* PAGES and CARDS
+
+ This is the central hub of the Storage application. It implements a
+ couple of concepts: "page", "card", "crossref", and "action". It
+ also implements the React components that construct the actual UI
+ out of them.
+
+ On every change of the client, a tree of pages is constructed. Each
+ page has a list of cards on it. The cards each have a list of
+ actions. In addition to that, there are a few crossrefs (cross
+ references) between pages that don't follow the tree structure.
+
+ The tree of pages starts with "make_overview_page" in
+ overview/overview.jsx. That function creates the data structures
+ that represent the overview page (using functions exported from
+ this file), and is also responsible for kicking of the creation of
+ its child pages. (And each page construction function of course
+ also creates the cards and actions for that page).
+
+ Pages can be shown by themselves, or be part of various tables in
+ other pages. For example, each row in the table in the overview
+ page represents another page, and clicking on it will navigate to
+ that page. (In fact, the overview table shows the whole tree. Other
+ apges show subsets of the tree in their tables.)
+
+ Most of the complications with pages and cards are there to support
+ putting pages into tables. Without having to do that, we wouldn't
+ need the pages and cards abstractions at all.
+
+ A page derives its "ID", "Type", "Location", and "Size" properties
+ from its cards in various ad-hoc and arguably too complex
+ ways. This is seems necessary since the cards that end up together
+ on a page can vary significantly depending on the actual storage
+ configuration of the machine.
+
+ For example, a MDRAID device might have a encrypted LVM2 physical
+ volume directly on it, or it might have a partition table with a
+ PV, in which case the card for the PV appears on another page
+ together with a card for the partition.
+
+ Cards are constructed with a call like this:
+
+ const card = new_card({
+ next: next_card,
+ title: ...,
+ component: CardComponent,
+ props: { react_props... }
+ });
+
+ A page is constructed with a call like this:
+
+ new_page(parent_page, first_card, { options... });
+
+ A call to "new_page" will implicitly build the tree by inserting
+ the new_page into the children of "parent_page".
+
+ The main React component for this application is StoragePage. It
+ will find the correct page for the browser URL and display it,
+ together with all the trimmings like the breadcrumb trail.
+
+ The page will be filled with the React components specified by its
+ cards. Such a component should use StorageCard to get the standard
+ look of all cards, and to work well with the test helpers.
+
+ Tables of pages are handled by the PageTable component exported
+ from this file or, more likely, its specialized version
+ ChildrenTable.
+
+ A ChildrenTable shows all direct and indirect children of a given
+ page, by using a PageTable appropriately. A raw PageTable can also
+ show cross references.
+
+ A cross reference associates a page with an arbitrary key. For
+ example, a page that represents a LVM2 physical volume will
+ associate itself with the DBusProxy for its volume group. The page
+ for the volume group will then retrieve all pages associated with
+ its DBusProxy and put them into a table to show all its physical
+ volumes.
+
+ When a page appears in such a crossref table, it has dedicated
+ actions, a dedicated size, and generally looks different from when
+ it appears in a "normal" childrens table. This is another source of
+ complication of how cards and pages interact.
+*/
+
+let pages = null;
+let crossrefs = null;
+
+export const PAGE_CATEGORY_PHYSICAL = 1;
+export const PAGE_CATEGORY_VIRTUAL = 2;
+export const PAGE_CATEGORY_NETWORK = 3;
+
+export function reset_pages() {
+ pages = new Map();
+ crossrefs = new Map();
+}
+
+function name_from_card(card) {
+ if (!card)
+ return null;
+ return name_from_card(card.next) || card.page_name;
+}
+
+function icon_from_card(card) {
+ if (!card)
+ return null;
+ return icon_from_card(card.next) || card.page_icon;
+}
+
+function category_from_card(card) {
+ if (!card)
+ return null;
+ return category_from_card(card.next) || card.page_category;
+}
+
+function key_from_card(card) {
+ if (!card)
+ return null;
+ return key_from_card(card.next) || card.page_key;
+}
+
+function location_from_card(card) {
+ if (!card)
+ return null;
+ return location_from_card(card.next) || card.page_location;
+}
+
+function size_from_card(card) {
+ if (!card)
+ return null;
+ if (card.page_size)
+ return card.page_size;
+ return size_from_card(card.next);
+}
+
+export function new_page(parent, card, options) {
+ const page = {
+ location: location_from_card(card),
+ name: name_from_card(card),
+ icon: icon_from_card(card),
+ category: category_from_card(card),
+ key: key_from_card(card),
+ parent,
+ children: [],
+ card,
+ options: options || {},
+ };
+ page.columns = [
+ card.title,
+ card.location,
+ size_from_card(card),
+ ];
+ if (parent)
+ parent.children.push(page);
+ while (card) {
+ card.page = page;
+ card = card.next;
+ }
+ if (page.location) {
+ pages.set(JSON.stringify(page.location), page);
+ if (page.location.length == 0) {
+ // This is the Overview page. Make it the parent of the
+ // special "not found" page (but don't make the "not
+ // found" page a child of the Overview...)
+ not_found_page.parent = page;
+ }
+ }
+ return page;
+}
+
+export function new_card({
+ title, location, next,
+ type_extra, id_extra,
+ page_name, page_icon, page_category, page_key, page_location, page_size,
+ page_block,
+ for_summary,
+ has_warning, has_danger, job_path,
+ component, props,
+ actions,
+}) {
+ if (page_block) {
+ page_location = [block_location(page_block)];
+ page_name = block_short_name(page_block);
+ page_size = page_block.Size;
+ job_path = page_block.path;
+ }
+ return {
+ title,
+ location,
+ next,
+ type_extra,
+ page_name,
+ page_icon,
+ page_category: page_category || PAGE_CATEGORY_PHYSICAL,
+ page_key,
+ page_location,
+ page_size,
+ for_summary,
+ component,
+ props,
+ has_warning,
+ has_danger,
+ job_path,
+ actions: actions ? actions.filter(a => !!a) : null,
+ id_extra,
+ };
+}
+
+export function register_crossref(crossref) {
+ const val = crossrefs.get(crossref.key) || [];
+ crossref.actions = crossref.actions ? crossref.actions.filter(a => !!a) : null;
+ val.push(crossref);
+ crossrefs.set(crossref.key, val);
+}
+
+export function get_crossrefs(key) {
+ return crossrefs.get(key);
+}
+
+/* Getting the page for a navigation location.
+ *
+ * We have a special "not found" page that is returned when there is
+ * no real page at the given location.
+ */
+
+const NotFoundCard = ({ card }) => {
+ return <span>{_("Not found")}</span>;
+};
+
+const not_found_page = new_page(null, new_card({ page_name: _("Not found"), component: NotFoundCard }));
+
+export function get_page_from_location(location) {
+ return pages?.get(JSON.stringify(location)) || not_found_page;
+}
+
+/* Common UI things
+ */
+
+export function navigate_away_from_card(card) {
+ if (!card)
+ return;
+
+ const loc = cockpit.location;
+ const page = card.page;
+ if (page.parent && JSON.stringify(loc.path) == JSON.stringify(page.location))
+ loc.go(page.parent.location);
+}
+
+export function navigate_to_new_card_location(card, location) {
+ if (!card)
+ return;
+
+ const loc = cockpit.location;
+ const page = card.page;
+ if (JSON.stringify(loc.path) == JSON.stringify(page.location))
+ loc.go(location);
+}
+
+function make_menu_item(action) {
+ return <StorageMenuItem key={action.title} onClick={() => action.action(true)}
+ danger={action.danger} excuse={action.excuse}>
+ {action.title}
+ </StorageMenuItem>;
+}
+
+function make_page_kebab(page) {
+ const items = [];
+
+ function card_item_group(card) {
+ const a = card.actions || [];
+ if (a.length > 0) {
+ let result = <DropdownList key={items.length}>{a.map(make_menu_item)}</DropdownList>;
+ if (card.title) {
+ result = <DropdownGroup key={items.length} label={card.title} className="storage-menu-title">
+ {result}
+ </DropdownGroup>;
+ }
+ return result;
+ } else
+ return null;
+ }
+
+ let c = page.card;
+ while (c) {
+ const g = card_item_group(c);
+ if (g)
+ items.push(g);
+ c = c.next;
+ }
+
+ if (items.length == 0)
+ return null;
+
+ return <StorageBarMenu menuItems={items} isKebab />;
+}
+
+function make_actions_kebab(actions) {
+ if (!actions || actions.length == 0)
+ return null;
+
+ return <StorageBarMenu menuItems={actions.map(make_menu_item)} isKebab />;
+}
+
+const ActionButtons = ({ card }) => {
+ const narrow = useIsNarrow();
+
+ function for_menu(action) {
+ // Determine whether a action should get a button or be in the
+ // menu
+
+ // In a narrow layout, everything goes to the menu
+ if (narrow)
+ return true;
+
+ // Everything that is dangerous goes to the menu
+ if (action.danger)
+ return true;
+
+ return false;
+ }
+
+ const buttons = [];
+ const items = [];
+
+ if (!card.actions)
+ return null;
+
+ for (const a of card.actions) {
+ if (for_menu(a))
+ items.push(make_menu_item(a));
+ else
+ buttons.push(
+ <StorageButton key={a.title} onClick={() => a.action(false)}
+ kind={a.danger ? "danger" : null} excuse={a.excuse}>
+ {a.title}
+ </StorageButton>);
+ }
+
+ if (items.length > 0)
+ buttons.push(<StorageBarMenu key="menu" menuItems={items} isKebab />);
+
+ return buttons;
+};
+
+function page_type_extra(page) {
+ const extra = [];
+ let c = page.card;
+ while (c) {
+ if (c.type_extra)
+ extra.push(c.type_extra);
+ c = c.next;
+ }
+ return extra;
+}
+
+function page_type(page) {
+ const type = page.card.title;
+ const extra = page_type_extra(page);
+ if (extra.length > 0)
+ return type + " (" + extra.join(", ") + ")";
+ else
+ return type;
+}
+
+// PAGE_BLOCK_SUMMARY
+//
+// Describe a page in a way that is useful to identify it when
+// deciding which block device to format, or which block devices to
+// make a volume group out of, for example. The block device itself
+// (such as /dev/sda5) should not be part of the description; it is in
+// another table column already.
+//
+// The first card on the page that has the "for_summary" flag set
+// provides the description, and the type extra of all cards
+// leading to it are added. The description is either the title
+// of the card, or its id_extra.
+//
+// For more context, the description for the parent of a page is also
+// added.
+//
+// Thus, we end up with things like "Partition - MDRAID device".
+
+function page_block_summary_1(page) {
+ let description = null;
+ const extra = [];
+ for (let card = page.card; card; card = card.next) {
+ if (card.for_summary) {
+ description = card.id_extra || card.title;
+ break;
+ }
+ if (card.type_extra)
+ extra.push(card.type_extra);
+ }
+
+ if (description && extra.length > 0)
+ description += " (" + extra.join(", ") + ")";
+
+ return description;
+}
+
+function page_block_summary(page) {
+ const desc1 = page_block_summary_1(page);
+ const desc2 = page.parent && page.parent.parent && page_block_summary_1(page.parent);
+ if (desc1 && desc2)
+ return desc1 + " - " + desc2;
+ else
+ return desc1 || desc2;
+}
+
+let narrow_query = null;
+
+export const useIsNarrow = (onChange) => {
+ if (!narrow_query) {
+ const val = window.getComputedStyle(window.document.body).getPropertyValue("--pf-v5-global--breakpoint--md");
+ narrow_query = window.matchMedia(`(max-width: ${val})`);
+ }
+ useEvent(narrow_query, "change", onChange);
+
+ return narrow_query.matches;
+};
+
+export const PageTable = ({ emptyCaption, aria_label, pages, crossrefs, sorted, show_icons }) => {
+ const [collapsed, setCollapsed] = useState(true);
+ const firstKeys = useRef(false);
+ const narrow = useIsNarrow(() => { firstKeys.current = false });
+
+ let rows = [];
+ const row_keys = new Set();
+
+ function make_row(page, crossref, level, border, key) {
+ function card_has_danger(card) {
+ if (card)
+ return card.has_danger || card_has_danger(card.next);
+ else
+ return false;
+ }
+
+ function card_has_warning(card) {
+ if (card)
+ return card.has_warning || card_has_warning(card.next);
+ else
+ return false;
+ }
+
+ function card_has_job(card) {
+ if (card)
+ return client.path_jobs[card.job_path] || card_has_job(card.next);
+ else
+ return false;
+ }
+
+ let info = null;
+ if (card_has_job(page.card))
+ info = <>{"\n"}<Spinner isInline size="md" /></>;
+ if (card_has_danger(page.card))
+ info = <>{"\n"}<ExclamationCircleIcon className="ct-icon-times-circle" />{info}</>;
+ else if (card_has_warning(page.card))
+ info = <>{"\n"}<ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />{info}</>;
+
+ const icon = (show_icons && page.icon) ? <page.icon /> : null;
+ const name = crossref ? page.name : page_display_name(page);
+ const type = crossref ? page_block_summary(page) : page_type(page);
+ const location = crossref ? crossref.extra : page.columns[1];
+ let size = crossref ? crossref.size : page.columns[2];
+ const actions = crossref ? make_actions_kebab(crossref.actions) : make_page_kebab(page);
+
+ if (typeof size === "number") {
+ if (narrow)
+ size = <span className="usage-text">{fmt_size(size)}</span>;
+ else
+ size = <StorageSize size={size} />;
+ }
+
+ function onClick(event) {
+ if (!event || event.button !== 0)
+ return;
+
+ if (page.location)
+ cockpit.location.go(page.location);
+ }
+
+ function is_clickable(element) {
+ return element.classList.contains("pf-m-clickable");
+ }
+
+ function next_clickable_sibling(element) {
+ do {
+ const next = element.nextElementSibling;
+ if (next && is_clickable(next))
+ return next;
+ element = next;
+ } while (element);
+
+ return null;
+ }
+
+ function previous_clickable_sibling(element) {
+ do {
+ const prev = element.previousElementSibling;
+ if (prev && is_clickable(prev))
+ return prev;
+ element = prev;
+ } while (element);
+
+ return null;
+ }
+
+ function onRowKeyDown(event) {
+ const { code, target } = event;
+
+ if (target.nodeName == "TR") {
+ if (code == "Space" || code == "Enter") {
+ if (page.location)
+ cockpit.location.go(page.location);
+ event.preventDefault();
+ }
+ if (code == "ArrowDown") {
+ const next = next_clickable_sibling(target);
+ if (next)
+ next.focus();
+ event.preventDefault();
+ }
+ if (code == "ArrowUp") {
+ const prev = previous_clickable_sibling(target);
+ if (prev)
+ prev.focus();
+ event.preventDefault();
+ }
+ }
+ }
+
+ const is_new = firstKeys.current != false && !firstKeys.current.has(key);
+ row_keys.add(key);
+
+ if (narrow) {
+ rows.push(
+ <Card key={key} onClick={onClick}
+ className={"ct-small-table-card" +
+ (page.location ? " ct-clickable-card" : null) +
+ (is_new ? " ct-new-item" : "")}
+ data-test-row-name={page.name}
+ data-test-row-location={page.columns[1]}>
+ <CardBody>
+ <Split hasGutter>
+ { icon && <SplitItem>{icon}</SplitItem> }
+ <SplitItem isFilled><strong><Truncate content={name} /></strong>{info}</SplitItem>
+ <SplitItem>{actions}</SplitItem>
+ </Split>
+ <Split hasGutter isWrappable>
+ <SplitItem>{type}</SplitItem>
+ <SplitItem isFilled>{location}</SplitItem>
+ <SplitItem isFilled className="pf-v5-u-text-align-right">{size}</SplitItem>
+ </Split>
+ </CardBody>
+ </Card>);
+ } else {
+ const cols = [
+ <Td key="1" onClick={onClick}>
+ <div className={"content-level-" + level}>
+ <Truncate content={name} />
+ {info}
+ </div>
+ </Td>,
+ <Td key="2" onClick={onClick} modifier="nowrap">{type}</Td>,
+ <Td key="3" onClick={onClick} modifier="nowrap">{location}</Td>,
+ <Td key="4" onClick={onClick} className="storage-size-column">{size}</Td>,
+ <Td key="5" className="pf-v5-c-table__action">{actions || <div /> }</Td>,
+ ];
+ if (show_icons)
+ cols.unshift(<Td key="0" onClick={onClick} className="storage-device-icon">{icon}</Td>);
+
+ rows.push(
+ <Tr key={key}
+ className={(border ? "" : " remove-border") +
+ (is_new ? " ct-new-item" : "")}
+ data-test-row-name={page.name} data-test-row-location={page.columns[1]}
+ isClickable={!!page.location}
+ onKeyDown={onRowKeyDown}>
+ {cols}
+ </Tr>);
+ }
+ }
+
+ function page_compare(a, b) {
+ if (a.category < b.category)
+ return -1;
+ else if (a.category > b.category)
+ return 1;
+ else
+ return a.name.localeCompare(b.name);
+ }
+
+ function sort(things, accessor, sorted) {
+ if (sorted === false)
+ return things;
+ // HACK: Use toSorted() once enough users upgrade to at least Firefox ESR 115
+ // See: https://github.com/cockpit-project/cockpit/issues/19848
+ return things.slice().sort((a, b) => page_compare(accessor(a), accessor(b)));
+ }
+
+ function make_page_rows(pages, level, last_has_border, key, sorted) {
+ const sorted_pages = sort(pages, p => p, sorted);
+ for (const p of sorted_pages) {
+ const is_last = (level == 0 || p == sorted_pages[pages.length - 1]);
+ const p_key = key + ":" + (p.key || p.name);
+ make_row(p, null, level, is_last && p.children.length == 0 && last_has_border, p_key);
+ make_page_rows(p.children, level + 1, is_last && last_has_border, p_key, p.options.sorted);
+ }
+ }
+
+ function make_crossref_rows(crossrefs) {
+ for (const c of sort(crossrefs, c => c.card.page, sorted))
+ make_row(c.card.page, c, 0, true, c.card.page.name);
+ }
+
+ if (pages)
+ make_page_rows(pages, 0, true, "", sorted);
+ else if (crossrefs)
+ make_crossref_rows(crossrefs);
+
+ if (firstKeys.current === false)
+ firstKeys.current = row_keys;
+ else {
+ firstKeys.current.forEach(v => {
+ if (!row_keys.has(v))
+ firstKeys.current.delete(v);
+ });
+ }
+
+ if (rows.length == 0) {
+ return <EmptyState>
+ <EmptyStateBody>
+ {emptyCaption}
+ </EmptyStateBody>
+ </EmptyState>;
+ }
+
+ let show_all_button = null;
+ if (rows.length > 50 && collapsed) {
+ show_all_button = (
+ <Bullseye>
+ <Button variant='link'
+ onKeyDown={ev => ev.key === "Enter" && setCollapsed(false)}
+ onClick={() => setCollapsed(false)}>
+ {cockpit.format(_("Show all $0 rows"), rows.length)}
+ </Button>
+ </Bullseye>);
+ rows = rows.slice(0, 50);
+ }
+
+ return (
+ <div>
+ { narrow
+ ? rows
+ : <Table aria-label={aria_label}
+ variant="compact">
+ { pages &&
+ <Thead>
+ <Tr>
+ { show_icons && <Th /> }
+ <Th>{_("ID")}</Th>
+ <Th>{_("Type")}</Th>
+ <Th>{_("Location")}</Th>
+ <Th className="storage-size-column-header">{_("Size")}</Th>
+ <Th />
+ </Tr>
+ </Thead>
+ }
+ <Tbody>
+ {rows}
+ </Tbody>
+ </Table>
+ }
+ {show_all_button}
+ </div>);
+};
+
+export const ChildrenTable = ({ emptyCaption, aria_label, page, show_icons }) => {
+ return <PageTable emptyCaption={emptyCaption}
+ aria_label={aria_label}
+ pages={page.children}
+ sorted={page.options.sorted}
+ show_icons={show_icons} />;
+};
+
+function page_id_extra(page) {
+ let extra = "";
+ let c = page.card;
+ while (c) {
+ if (c.id_extra)
+ extra += " " + c.id_extra;
+ c = c.next;
+ }
+ return extra;
+}
+
+function page_display_name(page) {
+ let name = page.name;
+ const extra = page_id_extra(page);
+ if (extra)
+ name = name + " - " + extra;
+ return name;
+}
+
+const PageCardStackItems = ({ page, plot_state }) => {
+ const items = [];
+ let c = page.card;
+ while (c) {
+ items.push(<React.Fragment key={items.length}>
+ <StackItem>
+ <c.component card={c} plot_state={plot_state} {...c.props} />
+ </StackItem>
+ </React.Fragment>);
+ c = c.next;
+ }
+
+ return items.reverse();
+};
+
+export function block_location(block) {
+ return decode_filename(block.PreferredDevice).replace(/^\/dev\//, "");
+}
+
+const StorageBreadcrumb = ({ page }) => {
+ const parent_crumbs = [];
+ let pp = page.parent;
+ while (pp) {
+ parent_crumbs.unshift(
+ <BreadcrumbItem key={pp.name} to={"#" + cockpit.location.encode(pp.location)}>
+ {page_display_name(pp)}
+ </BreadcrumbItem>
+ );
+ pp = pp.parent;
+ }
+
+ return (
+ <Breadcrumb>
+ { parent_crumbs }
+ <BreadcrumbItem isActive>{page_display_name(page)}</BreadcrumbItem>
+ </Breadcrumb>);
+};
+
+export const StorageCard = ({ card, alert, alerts, actions, children }) => {
+ return (
+ <Card data-test-card-title={card.title}>
+ { (client.in_anaconda_mode() && card.page.parent && !card.next) &&
+ <CardBody>
+ <StorageBreadcrumb page={card.page} />
+ </CardBody>
+ }
+ <CardHeader actions={{ actions: actions || <ActionButtons card={card} /> }}>
+ <CardTitle>{card.title}</CardTitle>
+ </CardHeader>
+ {(alert || (alerts && alerts.length > 0)) && <CardBody><AlertGroup>{alert}{alerts}</AlertGroup></CardBody>}
+ {children}
+ <JobsPanel client={client} path={card.job_path} />
+ </Card>);
+};
+
+export const StorageDescription = ({ title, value, action, children }) => {
+ if (!value && !action && !children)
+ return null;
+
+ let content;
+ if (action && value) {
+ content = (
+ <Flex>
+ <FlexItem data-test-value>{value}</FlexItem>
+ <FlexItem data-test-action>{action}</FlexItem>
+ </Flex>);
+ } else {
+ content = value || action;
+ }
+
+ return (
+ <DescriptionListGroup data-test-desc-title={title}>
+ <DescriptionListTerm>{title}</DescriptionListTerm>
+ <DescriptionListDescription data-test-value={!(action && value)}>
+ {content}{children}
+ </DescriptionListDescription>
+ </DescriptionListGroup>);
+};
+
+export const StoragePage = ({ location, plot_state }) => {
+ const page = get_page_from_location(location);
+
+ return (
+ <Page id="storage">
+ { (!client.in_anaconda_mode() && page.parent) &&
+ <PageBreadcrumb stickyOnBreakpoint={{ default: "top" }}>
+ <StorageBreadcrumb page={page} />
+ </PageBreadcrumb>
+ }
+ <PageSection isFilled={false} padding={client.in_anaconda_mode() ? { default: "noPadding" } : {}}>
+ <Stack hasGutter>
+ <MultipathAlert client={client} />
+ <PageCardStackItems page={page} plot_state={plot_state} noarrow />
+ </Stack>
+ </PageSection>
+ </Page>
+ );
+};
diff --git a/pkg/storaged/partitions/actions.jsx b/pkg/storaged/partitions/actions.jsx
new file mode 100644
index 0000000..dd936c9
--- /dev/null
+++ b/pkg/storaged/partitions/actions.jsx
@@ -0,0 +1,40 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import { format_disk } from "./format-disk-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export function partitionable_block_actions(block, tag) {
+ const excuse = block.ReadOnly ? _("Device is read-only") : null;
+
+ return [
+ (block.Size > 0
+ ? {
+ title: _("Create partition table"),
+ action: () => format_disk(block),
+ danger: true,
+ excuse,
+ tag
+ }
+ : null)
+ ];
+}
diff --git a/pkg/storaged/partitions/format-disk-dialog.jsx b/pkg/storaged/partitions/format-disk-dialog.jsx
new file mode 100644
index 0000000..f1dede3
--- /dev/null
+++ b/pkg/storaged/partitions/format-disk-dialog.jsx
@@ -0,0 +1,90 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import client from "../client";
+
+import {
+ dialog_open,
+ SelectOne, CheckBoxes,
+ BlockingMessage, TeardownMessage,
+ init_active_usage_processes
+} from "../dialog.jsx";
+import { get_active_usage, block_name, teardown_active_usage, reload_systemd } from "../utils.js";
+import { job_progress_wrapper } from "../jobs-panel.jsx";
+
+const _ = cockpit.gettext;
+
+export function format_disk(block) {
+ const usage = get_active_usage(client, block.path, _("initialize"), _("delete"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), block_name(block)),
+ Body: BlockingMessage(usage),
+ });
+ return;
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Initialize disk $0"), block_name(block)),
+ Teardown: TeardownMessage(usage),
+ Fields: [
+ SelectOne("type", _("Partitioning"),
+ {
+ value: "gpt",
+ choices: [
+ { value: "dos", title: _("Compatible with all systems and devices (MBR)") },
+ {
+ value: "gpt",
+ title: _("Compatible with modern system and hard disks > 2TB (GPT)")
+ },
+ { value: "empty", title: _("No partitioning") }
+ ]
+ }),
+ CheckBoxes("erase", _("Overwrite"),
+ {
+ fields: [
+ { tag: "on", title: _("Overwrite existing data with zeros (slower)") }
+ ],
+ }),
+ ],
+ Action: {
+ Title: _("Initialize"),
+ Danger: _("Initializing erases all data on a disk."),
+ wrapper: job_progress_wrapper(client, block.path),
+ disable_on_error: usage.Teardown,
+ action: function (vals) {
+ const options = {
+ 'tear-down': { t: 'b', v: true }
+ };
+ if (vals.erase.on)
+ options.erase = { t: 's', v: "zero" };
+ return teardown_active_usage(client, usage)
+ .then(function () {
+ return block.Format(vals.type, options);
+ })
+ .then(reload_systemd);
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+}
diff --git a/pkg/storaged/partitions/partition-table.jsx b/pkg/storaged/partitions/partition-table.jsx
new file mode 100644
index 0000000..e2873b6
--- /dev/null
+++ b/pkg/storaged/partitions/partition-table.jsx
@@ -0,0 +1,113 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+
+import { get_partitions } from "../utils.js";
+import { StorageCard, ChildrenTable, new_page, new_card } from "../pages.jsx";
+import { format_dialog } from "../block/format-dialog.jsx";
+import { make_block_page } from "../block/create-pages.jsx";
+
+import { make_partition_card, delete_partition } from "./partition.jsx";
+
+const _ = cockpit.gettext;
+
+function make_partition_pages(parent, block) {
+ const block_ptable = client.blocks_ptable[block.path];
+ let counter = 0;
+
+ function make_free_space_page(parent, start, size, enable_dos_extended) {
+ counter++;
+ const card = new_card({
+ page_name: _("Free space"),
+ page_key: "free-space-" + counter,
+ page_size: size,
+ actions: [
+ {
+ title: _("Create partition"),
+ action: () => format_dialog(client, block.path, start, size,
+ enable_dos_extended),
+ }
+ ],
+ });
+ new_page(parent, card);
+ }
+
+ function make_extended_partition_page(parent, partition) {
+ const card = new_card({
+ page_name: _("Extended partition"),
+ page_size: partition.size,
+ actions: [
+ { title: _("Delete"), action: () => delete_partition(partition.block, card), danger: true },
+ ]
+ });
+ const page = new_page(parent, card);
+ process_partitions(page, partition.partitions, false);
+ }
+
+ function process_partitions(parent, partitions, enable_dos_extended) {
+ let i, p;
+ for (i = 0; i < partitions.length; i++) {
+ p = partitions[i];
+ if (p.type == 'free')
+ make_free_space_page(parent, p.start, p.size, enable_dos_extended);
+ else if (p.type == 'container')
+ make_extended_partition_page(parent, p);
+ else {
+ const card = make_partition_card(null, p.block);
+ make_block_page(parent, p.block, card);
+ }
+ }
+ }
+
+ process_partitions(parent, get_partitions(client, block),
+ block_ptable.Type == 'dos');
+}
+
+export function make_partition_table_page(parent, block, next_card) {
+ const block_ptable = client.blocks_ptable[block.path];
+
+ const parts_card = new_card({
+ title: (block_ptable.Type
+ ? cockpit.format(_("$0 partitions"), block_ptable.Type.toLocaleUpperCase())
+ : _("Partitions")),
+ next: next_card,
+ component: PartitionsCard,
+ props: { },
+ });
+
+ const p = new_page(parent, parts_card, { sorted: false });
+ make_partition_pages(p, block);
+}
+
+const PartitionsCard = ({ card }) => {
+ return (
+ <StorageCard card={card}>
+ <CardBody className="contains-list">
+ <ChildrenTable emptyCaption={_("No partitions found")}
+ aria-label={_("Partitions")}
+ page={card.page} />
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/partitions/partition.jsx b/pkg/storaged/partitions/partition.jsx
new file mode 100644
index 0000000..ec8a8e9
--- /dev/null
+++ b/pkg/storaged/partitions/partition.jsx
@@ -0,0 +1,240 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { StorageButton, StorageLink } from "../storage-controls.jsx";
+import {
+ dialog_open,
+ SelectOne, TextInput,
+ init_active_usage_processes, BlockingMessage, TeardownMessage
+} from "../dialog.jsx";
+import { block_name, fmt_size, get_active_usage, teardown_active_usage, reload_systemd } from "../utils.js";
+import { check_unused_space, get_resize_info, free_space_after_part, grow_dialog, shrink_dialog } from "../block/resize.jsx";
+import { StorageCard, StorageDescription, new_card, navigate_away_from_card } from "../pages.jsx";
+
+const _ = cockpit.gettext;
+
+export function delete_partition(block, card) {
+ const block_part = client.blocks_part[block.path];
+ const name = block_name(block);
+ const usage = get_active_usage(client, block.path, _("delete"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"), name),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Permanently delete $0?"), name),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Danger: _("Deleting a partition will delete all data in it."),
+ Title: _("Delete"),
+ action: async function () {
+ await teardown_active_usage(client, usage);
+ await block_part.Delete({ 'tear-down': { t: 'b', v: true } });
+ await reload_systemd();
+ navigate_away_from_card(card);
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+}
+
+export function make_partition_card(next, block) {
+ const block_part = client.blocks_part[block.path];
+ const unused_space_warning = check_unused_space(block.path);
+ const unused_space = !!unused_space_warning;
+ let { info, shrink_excuse, grow_excuse } = get_resize_info(client, block, unused_space);
+
+ if (!unused_space_warning && !grow_excuse && free_space_after_part(client, block_part) == 0) {
+ grow_excuse = _("No free space after this partition");
+ }
+
+ const type = block_part.Type.replace(/^0x/, "").toLowerCase();
+ let type_extra;
+ if (type == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" || type == "ef")
+ type_extra = _("EFI system partition");
+ else if (type == "21686148-6449-6e6f-744e-656564454649")
+ type_extra = _("BIOS boot partition");
+
+ const card = new_card({
+ title: _("Partition"),
+ type_extra,
+ next,
+ page_block: block,
+ for_summary: true,
+ has_warning: !!unused_space_warning,
+ component: PartitionCard,
+ props: { block, unused_space_warning, resize_info: info },
+ actions: [
+ (!unused_space &&
+ {
+ title: _("Shrink"),
+ action: () => shrink_dialog(client, block_part, info),
+ excuse: shrink_excuse,
+ }),
+ (!unused_space &&
+ {
+ title: _("Grow"),
+ action: () => grow_dialog(client, block_part, info),
+ excuse: grow_excuse,
+ }),
+ {
+ title: _("Delete"),
+ action: () => delete_partition(block, card),
+ danger: true,
+ },
+ ],
+ });
+ return card;
+}
+
+const gpt_type_names = {
+ "21686148-6449-6e6f-744e-656564454649": _("BIOS boot partition"),
+ "c12a7328-f81f-11d2-ba4b-00a0c93ec93b": _("EFI system partition"),
+ "9e1a2d38-c612-4316-aa26-8b49521e5a8b": _("PowerPC PReP boot partition"),
+ "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f": _("Linux swap space"),
+ "0fc63daf-8483-4772-8e79-3d69d8477de4": _("Linux filesystem data"),
+ "e6d6d379-f507-44c2-a23c-238f2a3df928": _("Logical Volume Manager partition"),
+};
+
+const mbr_type_names = {
+ ef: _("EFI system partition"),
+ 82: _("Linux swap space"),
+ 83: _("Linux filesystem data"),
+ "8e": _("Logical Volume Manager partition"),
+};
+
+const PartitionCard = ({ card, block, unused_space_warning, resize_info }) => {
+ const block_part = client.blocks_part[block.path];
+ const unused_space = !!unused_space_warning;
+ const block_ptable = client.blocks_ptable[block_part.Table];
+ const type_names = block_ptable?.Type == "dos" ? mbr_type_names : gpt_type_names;
+ const type = block_part.Type.replace(/^0x/, "").toLowerCase();
+
+ function shrink_to_fit() {
+ return shrink_dialog(client, block_part, resize_info, true);
+ }
+
+ function grow_to_fit() {
+ return grow_dialog(client, block_part, resize_info, true);
+ }
+
+ function set_type_dialog() {
+ const choices = Object.keys(type_names).map(k => ({ value: k, title: type_names[k] }));
+ choices.push({ title: _("Custom"), value: "custom" });
+
+ function validate_type(val) {
+ if (block_ptable.Type == "dos") {
+ const hex_rx = /^[a-fA-F0-9]{2}$/;
+ if (!hex_rx.test(val))
+ return _("Type must contain exactly two hexadecimal characters (0 to 9, A to F).");
+ } else {
+ /* We let people use any 128 bit value as a UUID, even
+ those that are not defined by RFC 4122. This is
+ what the rest of the storage stack does as well,
+ and the "BIOS boot" UUID is in fact invalid
+ according to RFC 4122.
+
+ But we do insist that the dashes are in the right
+ place, because UDisks2 does as well.
+ */
+ const uuid_rx_1 = /^[a-fA-F0-9-]*$/;
+ if (!uuid_rx_1.test(val))
+ return _("Type can only contain the characters 0 to 9, A to F, and \"-\".");
+ const uuid_rx_2 = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/;
+ if (!uuid_rx_2.test(val))
+ return _("Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN.");
+ }
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Set partition type of $0"), block_name(block)),
+ Fields: [
+ SelectOne("type", _("Type"),
+ {
+ value: (type_names[type] ? type : "custom"),
+ choices,
+ }),
+ TextInput("custom", _("Custom type"),
+ {
+ value: type,
+ validate: validate_type,
+ visible: vals => vals.type == "custom",
+ }),
+ ],
+ Action: {
+ Danger: !client.in_anaconda_mode() && _("Changing partition types might prevent the system from booting."),
+ Title: _("Save"),
+ action: async function (vals) {
+ let t = vals.type == "custom" ? vals.custom : vals.type;
+ if (block_ptable?.Type == "dos")
+ t = "0x" + t;
+ await block_part.SetType(t, { });
+ }
+ },
+ });
+ }
+
+ return (
+ <StorageCard card={card}
+ alert={unused_space &&
+ <Alert variant="warning" isInline
+ title={_("This partition is not completely used by its content.")}>
+ {cockpit.format(_("Partition size is $0. Content size is $1."),
+ fmt_size(unused_space_warning.volume_size),
+ fmt_size(unused_space_warning.content_size))}
+ <div className='storage-alert-actions'>
+ <StorageButton onClick={shrink_to_fit}>
+ {_("Shrink partition")}
+ </StorageButton>
+ <StorageButton onClick={grow_to_fit}>
+ {_("Grow content")}
+ </StorageButton>
+ </div>
+ </Alert>}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Name")} value={block_part.Name || "-"} />
+ <StorageDescription title={_("UUID")} value={block_part.UUID} />
+ <StorageDescription title={_("Type")}
+ value={type_names[type] || type}
+ action={<StorageLink onClick={set_type_dialog}>
+ {_("edit")}
+ </StorageLink>} />
+ { !unused_space &&
+ <StorageDescription title={_("Size")} value={fmt_size(block_part.Size)} />
+ }
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>);
+};
diff --git a/pkg/storaged/plot.jsx b/pkg/storaged/plot.jsx
new file mode 100644
index 0000000..c41629a
--- /dev/null
+++ b/pkg/storaged/plot.jsx
@@ -0,0 +1,106 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import React from "react";
+
+import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
+import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js";
+import { ZoomControls, SvgPlot, bytes_per_sec_config } from "cockpit-components-plot.jsx";
+
+import { decode_filename, get_other_devices } from "./utils.js";
+
+const _ = cockpit.gettext;
+
+const single_read_metric = {
+ direct: ["disk.all.read_bytes"],
+ internal: ["disk.all.read"],
+ units: "bytes",
+ derive: "rate",
+ threshold: 1000
+};
+
+const single_write_metric = {
+ direct: ["disk.all.write_bytes"],
+ internal: ["disk.all.written"],
+ units: "bytes",
+ derive: "rate",
+ threshold: 1000
+};
+
+const instances_read_metric = {
+ direct: "disk.dev.read_bytes",
+ internal: "block.device.read",
+ units: "bytes",
+ derive: "rate",
+ threshold: 1000
+};
+
+const instances_write_metric = {
+ direct: "disk.dev.write_bytes",
+ internal: "block.device.written",
+ units: "bytes",
+ derive: "rate",
+ threshold: 1000
+};
+
+export function update_plot_state(ps, client) {
+ const devs = [];
+
+ // show all top-level block objects: Drives and Other devices
+ const blocks = Object.keys(client.drives).map(p => client.drives_block[p])
+ .concat(get_other_devices(client).map(p => client.blocks[p]));
+
+ blocks.forEach(block => {
+ const dev = block && decode_filename(block.Device).replace(/^\/dev\//, "");
+ if (dev)
+ devs.push(dev);
+ });
+
+ if (devs.length > 10) {
+ ps.plot_single('read', single_read_metric);
+ ps.plot_single('write', single_write_metric);
+ } else {
+ ps.plot_instances('read', instances_read_metric, devs);
+ ps.plot_instances('write', instances_write_metric, devs);
+ }
+}
+
+export const StoragePlots = ({ plot_state }) => {
+ return (
+ <>
+ <Split>
+ <SplitItem isFilled />
+ <SplitItem><ZoomControls plot_state={plot_state} /></SplitItem>
+ </Split>
+ <Grid sm={12} md={6} lg={6} hasGutter>
+ <GridItem>
+ <SvgPlot className="storage-graph"
+ title={_("Reading")} config={bytes_per_sec_config}
+ plot_state={plot_state} plot_id='read' />
+ </GridItem>
+ <GridItem>
+ <SvgPlot className="storage-graph"
+ title={_("Writing")} config={bytes_per_sec_config}
+ plot_state={plot_state} plot_id='write' />
+ </GridItem>
+ </Grid>
+ </>);
+};
diff --git a/pkg/storaged/storage-controls.jsx b/pkg/storaged/storage-controls.jsx
new file mode 100644
index 0000000..6396acd
--- /dev/null
+++ b/pkg/storaged/storage-controls.jsx
@@ -0,0 +1,269 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState } from 'react';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Dropdown, DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
+import { MenuToggle } from '@patternfly/react-core/dist/esm/components/MenuToggle/index.js';
+import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import { BarsIcon, EllipsisVIcon } from '@patternfly/react-icons';
+
+import cockpit from "cockpit";
+import * as utils from "./utils.js";
+import client from "./client.js";
+
+import { dialog_open } from "./dialog.jsx";
+
+const _ = cockpit.gettext;
+
+/* StorageControl - a button or similar that triggers
+ * a privileged action.
+ *
+ * It can be disabled and will show a tooltip then. It will
+ * automatically disappear when the logged in user doesn't
+ * have permission.
+ *
+ * Properties:
+ *
+ * - excuse: If set, the button/link is disabled and will show the
+ * excuse in a tooltip.
+ */
+
+class StorageControl extends React.Component {
+ render() {
+ const excuse = this.props.excuse;
+ if (!client.superuser.allowed)
+ return <div />;
+
+ if (excuse) {
+ return (
+ <Tooltip id="tip-storage" content={excuse}
+ position={this.props.excuse_placement || TooltipPosition.top}>
+ <span>
+ { this.props.content(excuse) }
+ </span>
+ </Tooltip>
+ );
+ } else {
+ return this.props.content();
+ }
+ }
+}
+
+function checked(callback, setSpinning, excuse) {
+ return function (event) {
+ if (!event)
+ return;
+
+ // only consider primary mouse button for clicks
+ if (event.type === 'click' && event.button !== 0)
+ return;
+
+ // only consider enter button for keyboard events
+ if (event.type === 'KeyDown' && event.key !== "Enter")
+ return;
+
+ event.stopPropagation();
+
+ if (excuse) {
+ dialog_open({
+ Title: _("Sorry"),
+ Body: excuse
+ });
+ return;
+ }
+
+ const promise = client.run(callback);
+ if (promise) {
+ if (setSpinning)
+ setSpinning(true);
+ promise.finally(() => {
+ if (setSpinning)
+ setSpinning(false);
+ });
+ promise.catch(function (error) {
+ console.warn(error.toString());
+ dialog_open({
+ Title: _("Error"),
+ Body: error.toString()
+ });
+ });
+ }
+ };
+}
+
+export const StorageButton = ({ id, kind, excuse, onClick, children, ariaLabel, spinner }) => {
+ const [spinning, setSpinning] = useState(false);
+
+ return <StorageControl excuse={excuse}
+ content={excuse => (
+ <Button id={id}
+ aria-label={ariaLabel}
+ onClick={checked(onClick, setSpinning)}
+ variant={kind || "secondary"}
+ isDisabled={!!excuse || (spinner && spinning)}
+ style={excuse ? { pointerEvents: 'none' } : null}
+ isLoading={spinner ? spinning : undefined}>
+ {children}
+ </Button>
+ )} />;
+};
+
+export const StorageLink = ({ id, excuse, onClick, children }) => (
+ <StorageControl excuse={excuse}
+ content={excuse => (
+ <Button onClick={checked(onClick)}
+ style={excuse ? { pointerEvents: 'none' } : null}
+ variant="link"
+ isInline
+ isDisabled={!!excuse}>
+ {children}
+ </Button>
+ )} />
+);
+
+// StorageOnOff - OnOff switch for asynchronous actions.
+//
+
+export class StorageOnOff extends React.Component {
+ constructor() {
+ super();
+ this.state = { promise: null };
+ }
+
+ render() {
+ const self = this;
+
+ function onChange(_event, val) {
+ const promise = self.props.onChange(val);
+ if (promise) {
+ promise.catch(error => {
+ dialog_open({
+ Title: _("Error"),
+ Body: error.toString()
+ });
+ })
+ .finally(() => { self.setState({ promise: null }) });
+ }
+
+ self.setState({ promise, promise_goal_state: val });
+ }
+
+ return (
+ <StorageControl excuse={this.props.excuse}
+ content={(excuse) => (
+ <Switch isChecked={this.state.promise
+ ? this.state.promise_goal_state
+ : this.props.state}
+ aria-label={this.props['aria-label']}
+ isDisabled={!!(excuse || this.state.promise)}
+ onChange={onChange} />
+ )} />
+ );
+ }
+}
+
+/* Render a usage bar showing props.stats[0] out of props.stats[1]
+ * bytes in use. If the ratio is above props.critical, the bar will be
+ * in a dangerous color.
+ */
+
+export const StorageUsageBar = ({ stats, critical, block, offset, total, short }) => {
+ if (!stats)
+ return null;
+
+ const fraction = stats[0] / stats[1];
+ const off_fraction = offset / stats[1];
+ const total_fraction = total / stats[1];
+ const labelText = utils.format_fsys_usage(stats[0], stats[1]);
+
+ return (
+ <div>
+ <span className="usage-text pf-v5-u-text-nowrap">
+ {labelText}
+ </span>
+ <div className={"usage-bar" + (fraction > critical ? " usage-bar-danger" : "") + (short ? " usage-bar-short" : "")}
+ role="progressbar"
+ aria-valuemin="0" aria-valuemax={stats[1]} aria-valuenow={stats[0]}
+ aria-label={cockpit.format(_("Usage of $0"), block)}
+ aria-valuetext={labelText}>
+ <div className="usage-bar-indicator usage-bar-other" aria-hidden="true" style={{ width: total_fraction * 100 + "%" }} />
+ <div className="usage-bar-indicator" style={{ insetInlineStart: off_fraction * 100 + "%", width: fraction * 100 + "%" }} />
+ </div>
+ </div>);
+};
+
+/* Render a static size that goes well with a short StorageusageBar in
+ the same table column, and also works well with the tests.
+*/
+
+export const StorageSize = ({ size }) => {
+ return (
+ <div>
+ <span className="usage-text pf-v5-u-text-nowrap">
+ {utils.fmt_size(size)}
+ </span>
+ <div className="usage-bar usage-bar-short usage-bar-empty" />
+ </div>);
+};
+
+export const StorageMenuItem = ({ onClick, danger, excuse, children }) => (
+ <DropdownItem className={danger && !excuse ? " delete-resource-dangerous" : ""}
+ description={excuse}
+ isDisabled={!!excuse}
+ onClick={checked(onClick, null, excuse)}>
+ {children}
+ </DropdownItem>
+);
+
+export const StorageBarMenu = ({ label, isKebab, menuItems }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ if (!client.superuser.allowed)
+ return null;
+
+ const onToggleClick = (event) => {
+ setIsOpen(!isOpen);
+ };
+
+ const onSelect = (event) => {
+ setIsOpen(false);
+ };
+
+ const toggle = ref => <MenuToggle ref={ref}
+ variant="plain"
+ className={isKebab ? "" : "pf-m-primary"}
+ onClick={onToggleClick}
+ isExpanded={isOpen}
+ aria-label={label}>
+ {isKebab ? <EllipsisVIcon /> : <BarsIcon color="white" />}
+ </MenuToggle>;
+
+ return (
+ <Dropdown isOpen={isOpen}
+ onSelect={onSelect}
+ onOpenChange={isOpen => setIsOpen(isOpen)}
+ toggle={toggle}
+ popperProps={{ position: "right" }}
+ shouldFocusToggleOnSelect>
+ {menuItems}
+ </Dropdown>);
+};
diff --git a/pkg/storaged/storage.scss b/pkg/storaged/storage.scss
new file mode 100644
index 0000000..cf82fc1
--- /dev/null
+++ b/pkg/storaged/storage.scss
@@ -0,0 +1,362 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+@use "ct-card";
+@use "page";
+@use "table";
+@use "journal";
+@import "global-variables";
+@import "@patternfly/patternfly/components/Card/card.scss";
+@import "@patternfly/patternfly/components/Progress/progress.scss";
+@import "@patternfly/patternfly/utilities/Flex/flex.scss";
+@import "@patternfly/patternfly/utilities/Text/text.scss";
+@import "@patternfly/patternfly/utilities/Alignment/alignment.scss";
+
+#storage .pf-v5-c-card {
+ @extend .ct-card;
+}
+
+.ct-card.pf-v5-c-card .pf-v5-c-card__title-text.ct-card-small-title {
+ font-size: var(--pf-v5-global--FontSize--xl);
+}
+
+// Only used in iSCSI dialog
+.dialog-select-row-table {
+ inline-size: 100%;
+
+ td {
+ text-align: start;
+ padding-block: 0.75em;
+ padding-inline: 0;
+ vertical-align: top;
+ border: 1px solid #d1d1d1;
+
+ &:first-child {
+ border-inline-end-width: 0;
+ }
+
+ &:last-child {
+ border-inline-start-width: 0;
+ }
+ }
+
+ th {font-weight: bold;
+ text-align: start;
+ color: var(--pf-v5-global--palette--black-800);
+ padding-block: 0.75em;
+ padding-inline: 0;
+ }
+
+ td, th {
+ &:first-child {
+ padding-inline-start: 0.75em;
+ }
+
+ &:last-child {
+ padding-inline-end: 0.75em;
+ }
+ }
+}
+
+div.progress {
+ inline-size: 100%;
+ block-size: var(--pf-v5-global--FontSize-xs);
+}
+
+td.job-description {
+ inline-size: 50%;
+}
+
+td.job-action {
+ text-align: end;
+}
+
+.storage-graph {
+ block-size: 180px;
+}
+
+.dialog-item-tooltip {
+ margin-inline-start: 5px;
+ padding: 0;
+ white-space: nowrap;
+}
+
+.modal-footer-teardown {
+ text-align: start;
+}
+
+.pf-v5-c-modal-box__body table {
+ --pf-v5-c-table--m-compact--cell--first-last-child--PaddingLeft: 0;
+}
+
+* + .modal-footer-teardown {
+ padding-block-start: var(--pf-v5-global--spacer--xl);
+}
+
+// FIXME: This is visual only; it needs to be fixed for a11y reasons, likely at the JSX level
+div[class*="content-level-"] {
+ --multiplier: 0;
+ --offset: calc(var(--pf-v5-global--spacer--md) * var(--multiplier));
+
+ padding-inline-start: var(--offset);
+ white-space: nowrap;
+}
+
+@for $i from 1 through 10 {
+ div.content-level-#{$i} {
+ --multiplier: #{$i};
+ }
+}
+
+// FIXME: Is this used?
+a.disabled {
+ color: var(--pf-v5-global--palette--light-blue-200);
+ text-decoration: none;
+ cursor: not-allowed;
+}
+
+.widest-title {
+ visibility: hidden;
+ block-size: 0;
+}
+
+.sigkey-hash {
+ font-family: monospace;
+ font-size: 140%;
+}
+
+.pf-v5-c-modal-box h3 {
+ margin-block-start: 0;
+ line-height: 24px;
+}
+
+.delete-resource-dangerous {
+ color: var(--pf-v5-global--danger-color--200);
+}
+
+.pf-v5-c-modal-box .slot-warning {
+ color: var(--pf-v5-global--danger-color--200);
+}
+
+// This is needed to avoid showing scrollbar in dialog
+.size-slider {
+ padding-block-end: var(--pf-v5-global--spacer--xs);
+}
+
+.panel-heading {
+ position: static;
+}
+
+.storage-alert-actions button {
+ margin-block: 0.5rem 0.2rem;
+ margin-inline: 0 0.2rem;
+}
+
+// FIXME: There's probably a better way to do this.
+// We wrap the data list row with a label to make checkboxes clickable from the whole row
+// we need this HACK in order to make alignRight property take effect
+.data-list-row-checkbox-label {
+ display: flex;
+ flex-wrap: wrap;
+ flex-grow: 1;
+}
+
+.crypto-keyslots-list {
+ border: none;
+}
+
+// So that "Stored passphrase" fits
+.pf-v5-c-description-list.wide-label {
+ --pf-v5-c-description-list--m-horizontal__term--width: minmax(0, 18ch);
+}
+
+.pf-v5-c-description-list .pf-v5-c-progress {
+ max-inline-size: 30ch;
+}
+
+.usage-bar {
+ --bg-color: var(--pf-v5-global--primary-color--100);
+ display: inline-block;
+ inline-size: 20em;
+ block-size: 1rem;
+ margin-inline-start: 1em;
+ position: relative;
+ inset-block-start: 0.15rem; // XXX - center it
+ inset-inline-start: 0;
+
+ &-short {
+ inline-size: 3.5em;
+ }
+
+ &-danger {
+ --bg-color: var(--pf-v5-global--danger-color--100);
+ }
+
+ &-empty {
+ --bg-color: none;
+ }
+
+ &-indicator {
+ display: inline-block;
+ background: var(--bg-color);
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ block-size: 100%;
+ }
+
+ // Hatched effect for progressbars that are parts of a group, but not the main
+ &-other {
+ // Use the page's default background color
+ --bg-light: var(--pf-v5-global--BackgroundColor--100);
+ // Alternate with the used color of the progress bar
+ --bg-dark: var(--bg-color);
+ // Repeat a gradient with hard stops, a perfect slant, and a consistency
+ // with all "other" hatched backgrounds
+ background: repeating-linear-gradient(
+ -45deg,
+ var(--bg-light),
+ var(--bg-light) 25%,
+ var(--bg-dark) 25%,
+ var(--bg-dark) 50%,
+ var(--bg-light) 50%
+ ) top left;
+ background-size: var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--sm);
+ // Overlay at 1/3 to mix the light and dark against the trough color
+ opacity: 0.333;
+ }
+
+ &::before {
+ display: inline-block;
+ content: "";
+ background: var(--bg-color);
+ opacity: 0.2;
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ inline-size: 100%;
+ block-size: 100%;
+ }
+}
+
+.storage-pvs-box {
+ border: 1px solid var(--pf-v5-global--palette--purple-400);
+ background: var(--pf-v5-global--palette--purple-100);
+
+ .pf-v5-theme-dark & {
+ border-color: var(--pf-v5-global--palette--purple-300);
+ background: var(--pf-v5-global--palette--purple-500);
+ }
+}
+
+.storage-pvs-pv-box {
+ padding: 0.3em;
+
+ &:not(:last-child) {
+ border-inline-end: 1px solid var(--pf-v5-global--palette--purple-400);
+ }
+
+ .pf-v5-theme-dark & {
+ border-color: var(--pf-v5-global--palette--purple-300);
+ }
+}
+
+.storage-pvs-pv-box-dev {
+ font-size: 120%;
+ font-weight: bold;
+}
+
+.storage-stripe-box {
+ border: 1px solid var(--pf-v5-global--palette--purple-400);
+ background: var(--pf-v5-global--palette--purple-100);
+
+ .pf-v5-theme-dark & {
+ border-color: var(--pf-v5-global--palette--purple-300);
+ background: var(--pf-v5-global--palette--purple-500);
+ }
+}
+
+.storage-stripe-pv-box {
+ padding: 0.3em;
+}
+
+.storage-stripe-pv-box:not(:last-child) {
+ border-block-end: 1px solid var(--pf-v5-global--palette--purple-400);
+
+ .pf-v5-theme-dark & {
+ border-color: var(--pf-v5-global--palette--purple-300);
+ }
+}
+
+.storage-stripe-pv-box-dev {
+ // FIXME: Font size should be a PF size, not a relative percentage
+ font-size: 120%;
+ font-weight: bold;
+}
+
+.remove-border {
+ border-block-end: 0 !important;
+}
+
+.ct-small-table-card {
+ border-block-start: var(--pf-v5-global--BorderWidth--sm) solid var(--pf-v5-global--BorderColor--100);
+}
+
+.ct-clickable-card:hover {
+ background: var(--pf-v5-global--BackgroundColor--150);
+ box-shadow: var(--pf-v5-global--BoxShadow--md);
+ cursor: pointer;
+}
+
+.storage-menu-title {
+ text-align: start;
+ white-space: nowrap !important;
+}
+
+.pf-v5-c-table__tbody .storage-device-icon {
+ --icon-size: 18px;
+ // Ensure the icon's cell size is as minimal as the icon allows
+ // (WebKit needs a size; 0 doesn't work for it)
+ inline-size: 1px;
+ // Remove unnecessary padding
+ padding-inline-end: 0;
+
+ > svg {
+ // Bump the icon down to align with text, based on the difference between icon and text size, just for the top
+ inset-block-start: calc((var(--pf-v5-global--spacer--md) * var(--pf-v5-global--LineHeight--md) - var(--icon-size)) / 2);
+ position: relative;
+
+ path {
+ fill: var(--pf-v5-global--Color--200);
+ }
+ }
+}
+
+.storage-size-column {
+ text-align: end;
+ white-space: nowrap !important;
+}
+
+.storage-size-column-header {
+ text-align: end;
+ // Align the "Size" header with the text of the usage bars.
+ // var(--pf-v5-global--spacer--sm): padding of compact td cells
+ // 3.5em: width of short progress bars
+ // 1em: margin between text and progress bar
+ padding-inline-end: calc(var(--pf-v5-global--spacer--sm) + 3.5em + 1em) !important;
+}
diff --git a/pkg/storaged/storaged.jsx b/pkg/storaged/storaged.jsx
new file mode 100644
index 0000000..100f92c
--- /dev/null
+++ b/pkg/storaged/storaged.jsx
@@ -0,0 +1,100 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import 'polyfills'; // once per application
+import 'cockpit-dark-theme'; // once per page
+
+import cockpit from "cockpit";
+import React from "react";
+import { createRoot } from 'react-dom/client';
+import { ExclamationCircleIcon } from "@patternfly/react-icons";
+
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { PlotState } from "plot.js";
+
+import client from "./client";
+import { update_plot_state } from "./plot.jsx";
+import { StoragePage } from "./pages.jsx";
+
+import "./storage.scss";
+
+const _ = cockpit.gettext;
+
+class Application extends React.Component {
+ constructor() {
+ super();
+ this.state = { inited: false, slow_init: false, path: cockpit.location.path };
+ this.plot_state = new PlotState();
+ this.on_client_changed = () => { if (!client.busy) this.setState({}); };
+ this.on_navigate = () => { this.setState({ path: cockpit.location.path }) };
+ }
+
+ componentDidMount() {
+ client.addEventListener("changed", this.on_client_changed);
+ cockpit.addEventListener("locationchanged", this.on_navigate);
+ client.init(() => { this.setState({ inited: true }) });
+ window.setTimeout(() => { if (!this.state.inited) this.setState({ slow_init: true }); }, 1000);
+ }
+
+ componentWillUnmount() {
+ client.removeEventListener("changed", this.on_client_changed);
+ cockpit.removeEventListener("locationchanged", this.on_navigate);
+ }
+
+ render() {
+ const { inited, slow_init, path } = this.state;
+
+ if (!inited) {
+ if (slow_init)
+ return <EmptyStatePanel loading title={ _("Loading...") } />;
+ else
+ return null;
+ }
+
+ if (client.features == false || client.older_than("2.6"))
+ return <EmptyStatePanel icon={ExclamationCircleIcon} title={ _("Storage can not be managed on this system.") } />;
+
+ // We maintain the plot state here so that the plots stay
+ // alive no matter what page is shown.
+ update_plot_state(this.plot_state, client);
+
+ return <StoragePage location={path} plot_state={this.plot_state} />;
+ }
+}
+
+function init() {
+ const root = createRoot(document.getElementById('storage'));
+ root.render(<Application />);
+ document.body.removeAttribute("hidden");
+
+ window.addEventListener('beforeunload', event => {
+ if (client.busy) {
+ // Firefox requires this when the page is in an iframe
+ event.preventDefault();
+
+ // see "an almost cross-browser solution" at
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
+ event.returnValue = '';
+ return '';
+ }
+ });
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/pkg/storaged/stratis/blockdev.jsx b/pkg/storaged/stratis/blockdev.jsx
new file mode 100644
index 0000000..117f268
--- /dev/null
+++ b/pkg/storaged/stratis/blockdev.jsx
@@ -0,0 +1,94 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import { StorageCard, StorageDescription, new_card, register_crossref } from "../pages.jsx";
+import { format_dialog } from "../block/format-dialog.jsx";
+import { fmt_size } from "../utils.js";
+import { std_lock_action } from "../crypto/actions.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_stratis_blockdev_card(next, backing_block, content_block) {
+ const blockdev = client.blocks_stratis_blockdev[content_block.path];
+ const pool = blockdev && client.stratis_pools[blockdev.Pool];
+ const stopped_pool = client.blocks_stratis_stopped_pool[content_block.path];
+
+ const blockdev_card = new_card({
+ title: _("Stratis block device"),
+ location: pool ? pool.Name : stopped_pool,
+ next,
+ component: StratisBlockdevCard,
+ props: { backing_block, content_block, pool, stopped_pool },
+ actions: [
+ std_lock_action(backing_block, content_block),
+ { title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true },
+ ]
+ });
+
+ if (pool || stopped_pool) {
+ let extra;
+ if (blockdev && blockdev.Tier == 0)
+ extra = _("data");
+ else if (blockdev && blockdev.Tier == 1)
+ extra = _("cache");
+ else
+ extra = null;
+
+ register_crossref({
+ key: pool || stopped_pool,
+ card: blockdev_card,
+ actions: [],
+ size: blockdev ? fmt_size(Number(blockdev.TotalPhysicalSize)) : fmt_size(content_block.Size),
+ extra,
+ });
+ }
+
+ return blockdev_card;
+}
+
+export const StratisBlockdevCard = ({ card, backing_block, content_block, pool, stopped_pool }) => {
+ const pool_name = pool ? pool.Name : stopped_pool;
+ const pool_uuid = pool ? pool.Uuid : stopped_pool;
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Stratis pool")}>
+ {(pool || stopped_pool)
+ ? <Button variant="link" isInline role="link"
+ onClick={() => cockpit.location.go(["pool", pool_uuid])}>
+ {pool_name}
+ </Button>
+ : "-"
+ }
+ </StorageDescription>
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/stratis/create-dialog.jsx b/pkg/storaged/stratis/create-dialog.jsx
new file mode 100644
index 0000000..ebb4def
--- /dev/null
+++ b/pkg/storaged/stratis/create-dialog.jsx
@@ -0,0 +1,164 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import client from "../client.js";
+
+import { dialog_open, TextInput, CheckBoxes, PassInput, SelectSpaces } from "../dialog.jsx";
+import { decode_filename, get_available_spaces, prepare_available_spaces } from "../utils.js";
+
+import { validate_pool_name, std_reply, get_unused_keydesc, with_stored_passphrase, confirm_tang_trust } from "./utils.jsx";
+import { validate_url, get_tang_adv } from "../crypto/tang.jsx";
+
+const _ = cockpit.gettext;
+
+export function create_stratis_pool() {
+ function find_pool(name) {
+ for (const p in client.stratis_pools) {
+ if (client.stratis_pools[p].Name == name)
+ return client.stratis_pools[p];
+ }
+ return null;
+ }
+
+ let name;
+ for (let i = 0; i < 1000; i++) {
+ name = "pool" + i.toFixed();
+ if (!find_pool(name))
+ break;
+ }
+
+ dialog_open({
+ Title: _("Create Stratis pool"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ value: name,
+ validate: name => validate_pool_name(null, name)
+ }),
+ SelectSpaces("disks", _("Block devices"),
+ {
+ empty_warning: _("No block devices are available."),
+ validate: function (disks) {
+ if (disks.length === 0)
+ return _("At least one block device is needed.");
+ },
+ spaces: get_available_spaces(client)
+ }),
+ CheckBoxes("encrypt_pass", client.features.stratis_crypto_binding ? _("Options") : "",
+ {
+ fields: [
+ {
+ tag: "on",
+ title: (client.features.stratis_crypto_binding
+ ? _("Encrypt data with a passphrase")
+ : _("Encrypt data"))
+ }
+ ],
+ nested_fields: [
+ PassInput("passphrase", _("Passphrase"),
+ {
+ validate: function (phrase) {
+ if (phrase === "")
+ return _("Passphrase cannot be empty");
+ },
+ visible: vals => vals.encrypt_pass.on,
+ new_password: true
+ }),
+ PassInput("passphrase2", _("Confirm"),
+ {
+ validate: function (phrase2, vals) {
+ if (phrase2 != vals.passphrase)
+ return _("Passphrases do not match");
+ },
+ visible: vals => vals.encrypt_pass.on,
+ new_password: true
+ })
+ ]
+ }),
+ CheckBoxes("encrypt_tang", "",
+ {
+ visible: () => client.features.stratis_crypto_binding,
+ fields: [
+ { tag: "on", title: _("Encrypt data with a Tang keyserver") }
+ ],
+ nested_fields: [
+ TextInput("tang_url", _("Keyserver address"),
+ {
+ validate: validate_url,
+ visible: vals => vals.encrypt_tang && vals.encrypt_tang.on
+ }),
+ ]
+ }),
+ CheckBoxes("managed", "",
+ {
+ visible: () => client.features.stratis_managed_fsys_sizes,
+ fields: [
+ {
+ tag: "on",
+ title: _("Manage filesystem sizes"),
+ tooltip: _("When this option is checked, the new pool will not allow overprovisioning. You need to specify a maximum size for each filesystem that is created in the pool. Filesystems can not be made larger after creation. Snapshots are fully allocated on creation. The sum of all maximum sizes can not exceed the size of the pool. The advantage of this is that filesystems in this pool can not run out of space in a surprising way. The disadvantage is that you need to know the maximum size for each filesystem in advance and creation of snapshots is limited.")
+ }
+ ]
+ })
+ ],
+ Action: {
+ Title: _("Create"),
+ action: function (vals) {
+ return prepare_available_spaces(client, vals.disks).then(function (paths) {
+ const devs = paths.map(p => decode_filename(client.blocks[p].PreferredDevice));
+
+ function create(key_desc, adv) {
+ let clevis_info = null;
+ if (adv)
+ clevis_info = ["tang", JSON.stringify({ url: vals.tang_url, adv })];
+ return client.stratis_create_pool(vals.name, devs, key_desc, clevis_info)
+ .then(std_reply)
+ .then(result => {
+ if (vals.managed && vals.managed.on && result[0]) {
+ const path = result[1][0];
+ return client.wait_for(() => client.stratis_pools[path])
+ .then(pool => {
+ return client.stratis_set_overprovisioning(pool, false);
+ });
+ }
+ });
+ }
+
+ function create2(adv) {
+ if (vals.encrypt_pass.on) {
+ return get_unused_keydesc(client, vals.name)
+ .then(keydesc => with_stored_passphrase(client, keydesc, vals.passphrase,
+ () => create(keydesc, adv)));
+ } else {
+ return create(false, adv);
+ }
+ }
+
+ if (vals.encrypt_tang && vals.encrypt_tang.on) {
+ return get_tang_adv(vals.tang_url)
+ .then(adv => confirm_tang_trust(vals.tang_url, adv, () => create2(adv)));
+ } else {
+ return create2(false);
+ }
+ });
+ }
+ }
+ });
+}
diff --git a/pkg/storaged/stratis/filesystem.jsx b/pkg/storaged/stratis/filesystem.jsx
new file mode 100644
index 0000000..2ac8a94
--- /dev/null
+++ b/pkg/storaged/stratis/filesystem.jsx
@@ -0,0 +1,242 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+
+import {
+ dialog_open, TextInput, BlockingMessage, TeardownMessage,
+ init_active_usage_processes,
+} from "../dialog.jsx";
+import { StorageUsageBar, StorageLink } from "../storage-controls.jsx";
+import {
+ StorageCard, StorageDescription,
+ new_page, new_card,
+ navigate_away_from_card, navigate_to_new_card_location,
+} from "../pages.jsx";
+import {
+ MountPoint, edit_mount_point,
+ is_valid_mount_point, is_mounted,
+ get_fstab_config, mount_point_text,
+} from "../filesystem/utils.jsx";
+import { MismountAlert, check_mismounted_fsys } from "../filesystem/mismounting.jsx";
+import { mounting_dialog, at_boot_input, mount_options } from "../filesystem/mounting-dialog.jsx";
+import { fmt_size, get_active_usage, teardown_active_usage } from "../utils.js";
+import { std_reply, validate_fs_name, set_mount_options, destroy_filesystem } from "./utils.jsx";
+import { mount_explanation } from "../block/format-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export function make_stratis_filesystem_page(parent, pool, fsys,
+ offset, forced_options, managed_fsys_sizes) {
+ const filesystems = client.stratis_pool_filesystems[pool.path];
+ const stats = client.stratis_pool_stats[pool.path];
+ const block = client.slashdevs_block[fsys.Devnode];
+
+ if (!block)
+ return;
+
+ const fstab_config = get_fstab_config(block);
+ const [, mount_point] = fstab_config;
+ const fs_is_mounted = is_mounted(client, block);
+
+ const mismount_warning = check_mismounted_fsys(block, block, fstab_config);
+
+ function mount() {
+ return mounting_dialog(client, block, "mount", forced_options);
+ }
+
+ function unmount() {
+ return mounting_dialog(client, block, "unmount", forced_options);
+ }
+
+ function snapshot_fsys() {
+ if (managed_fsys_sizes && stats.pool_free < Number(fsys.Size)) {
+ dialog_open({
+ Title: _("Not enough space"),
+ Body: cockpit.format(_("There is not enough space in the pool to make a snapshot of this filesystem. At least $0 are required but only $1 are available."),
+ fmt_size(Number(fsys.Size)), fmt_size(stats.pool_free))
+ });
+ return;
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Create a snapshot of filesystem $0"), fsys.Name),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ value: "",
+ validate: name => validate_fs_name(null, name, filesystems)
+ }),
+ TextInput("mount_point", _("Mount point"),
+ {
+ validate: (val, values, variant) => {
+ return is_valid_mount_point(client,
+ null,
+ client.add_mount_point_prefix(val),
+ variant == "nomount");
+ }
+ }),
+ mount_options(false, false),
+ at_boot_input("nofail"),
+ ],
+ update: function (dlg, vals, trigger) {
+ if (trigger == "at_boot")
+ dlg.set_options("at_boot", { explanation: mount_explanation[vals.at_boot] });
+ },
+ Action: {
+ Title: _("Create snapshot and mount"),
+ Variants: [{ tag: "nomount", Title: _("Create snapshot only") }],
+ action: function (vals) {
+ return pool.SnapshotFilesystem(fsys.path, vals.name)
+ .then(std_reply)
+ .then(result => {
+ if (result[0])
+ return set_mount_options(result[1], vals, forced_options);
+ else
+ return Promise.resolve();
+ });
+ }
+ }
+ });
+ }
+
+ function delete_fsys() {
+ const usage = get_active_usage(client, block.path, _("delete"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"),
+ fsys.Name),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Confirm deletion of $0"), fsys.Name),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Danger: _("Deleting a filesystem will delete all data in it."),
+ Title: _("Delete"),
+ action: async function () {
+ await teardown_active_usage(client, usage);
+ await destroy_filesystem(fsys);
+ navigate_away_from_card(fsys_card);
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+ }
+
+ const mp_text = mount_point_text(mount_point, fs_is_mounted);
+ if (mp_text == null)
+ return null;
+
+ const fsys_card = new_card({
+ title: _("Stratis filesystem"),
+ location: mp_text,
+ next: null,
+ page_location: ["pool", pool.Name, fsys.Name],
+ page_name: fsys.Name,
+ page_size: (!managed_fsys_sizes
+ ? <StorageUsageBar stats={[Number(fsys.Used[0] && Number(fsys.Used[1])), stats.pool_total]}
+ critical={1} total={stats.fsys_total_used} offset={offset} short />
+ : <StorageUsageBar stats={[Number(fsys.Used[0] && Number(fsys.Used[1])), Number(fsys.Size)]}
+ critical={0.95} short />),
+ has_warning: !!mismount_warning,
+ component: StratisFilesystemCard,
+ props: { pool, fsys, fstab_config, forced_options, managed_fsys_sizes, mismount_warning, offset },
+ actions: [
+ client.in_anaconda_mode() &&
+ { title: _("Edit mount point"), action: () => edit_mount_point(block, forced_options) },
+ (fs_is_mounted
+ ? { title: _("Unmount"), action: unmount }
+ : { title: _("Mount"), action: mount }),
+ { title: _("Snapshot"), action: snapshot_fsys },
+ { title: _("Delete"), action: delete_fsys, danger: true },
+ ]
+ });
+
+ new_page(parent, fsys_card);
+}
+
+const StratisFilesystemCard = ({
+ card, pool, fsys, fstab_config, forced_options, managed_fsys_sizes, mismount_warning, offset,
+}) => {
+ const filesystems = client.stratis_pool_filesystems[pool.path];
+ const stats = client.stratis_pool_stats[pool.path];
+ const block = client.slashdevs_block[fsys.Devnode];
+
+ function rename_fsys() {
+ dialog_open({
+ Title: _("Rename filesystem"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ value: fsys.Name,
+ validate: name => validate_fs_name(fsys, name, filesystems)
+ })
+ ],
+ Action: {
+ Title: _("Rename"),
+ action: async function (vals) {
+ await fsys.SetName(vals.name).then(std_reply);
+ navigate_to_new_card_location(card, ["pool", pool.Name, vals.name]);
+ }
+ }
+ });
+ }
+
+ return (
+ <StorageCard card={card}
+ alert={mismount_warning &&
+ <MismountAlert warning={mismount_warning}
+ fstab_config={fstab_config} forced_options={forced_options}
+ backing_block={block} content_block={block} />}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Name")}
+ value={fsys.Name}
+ action={<StorageLink onClick={rename_fsys}>
+ {_("edit")}
+ </StorageLink>} />
+ <StorageDescription title={_("Mount point")}>
+ <MountPoint fstab_config={fstab_config} forced_options={forced_options}
+ backing_block={block} content_block={block} />
+ </StorageDescription>
+ <StorageDescription title={_("Usage")}>
+ {(!managed_fsys_sizes
+ ? <StorageUsageBar stats={[Number(fsys.Used[0] && Number(fsys.Used[1])), stats.pool_total]}
+ critical={1} total={stats.fsys_total_used} offset={offset} />
+ : <StorageUsageBar stats={[Number(fsys.Used[0] && Number(fsys.Used[1])), Number(fsys.Size)]}
+ critical={0.95} />)
+ }
+ </StorageDescription>
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/stratis/pool.jsx b/pkg/storaged/stratis/pool.jsx
new file mode 100644
index 0000000..bf90a70
--- /dev/null
+++ b/pkg/storaged/stratis/pool.jsx
@@ -0,0 +1,575 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { CardHeader, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+
+import { VolumeIcon } from "../icons/gnome-icons.jsx";
+import { fmt_to_fragments } from "utils.jsx";
+
+import { StorageButton, StorageUsageBar, StorageLink } from "../storage-controls.jsx";
+import {
+ StorageCard, StorageDescription, ChildrenTable, PageTable,
+ new_page, new_card, PAGE_CATEGORY_VIRTUAL,
+ get_crossrefs, navigate_away_from_card
+} from "../pages.jsx";
+import {
+ get_active_usage, teardown_active_usage, for_each_async,
+ get_available_spaces, prepare_available_spaces,
+ decode_filename, should_ignore,
+} from "../utils.js";
+
+import {
+ dialog_open, SelectSpaces, TextInput, PassInput, SelectOne, SizeSlider,
+ BlockingMessage, TeardownMessage,
+ init_active_usage_processes
+} from "../dialog.jsx";
+
+import { validate_url, get_tang_adv } from "../crypto/tang.jsx";
+import { is_valid_mount_point } from "../filesystem/utils.jsx";
+import { mount_explanation } from "../block/format-dialog.jsx";
+import { at_boot_input, mount_options } from "../filesystem/mounting-dialog.jsx";
+
+import {
+ validate_pool_name, std_reply, with_keydesc, with_stored_passphrase,
+ confirm_tang_trust, get_unused_keydesc,
+ validate_fs_name, set_mount_options, destroy_filesystem
+} from "./utils.jsx";
+import { make_stratis_filesystem_page } from "./filesystem.jsx";
+
+const _ = cockpit.gettext;
+
+const fsys_min_size = 512 * 1024 * 1024;
+
+function destroy_pool(pool) {
+ return for_each_async(client.stratis_pool_filesystems[pool.path], fsys => destroy_filesystem(fsys))
+ .then(() => client.stratis_manager.DestroyPool(pool.path).then(std_reply));
+}
+
+function create_fs(pool) {
+ const filesystems = client.stratis_pool_filesystems[pool.path];
+ const stats = client.stratis_pool_stats[pool.path];
+ const forced_options = ["x-systemd.requires=stratis-fstab-setup@" + pool.Uuid + ".service"];
+ const managed_fsys_sizes = client.features.stratis_managed_fsys_sizes && !pool.Overprovisioning;
+
+ let action_variants;
+ if (!client.in_anaconda_mode()) {
+ action_variants = [
+ { tag: null, Title: _("Create and mount") },
+ { tag: "nomount", Title: _("Create only") },
+ ];
+ } else {
+ action_variants = [
+ { tag: "nomount", Title: _("Create") },
+ ];
+ }
+
+ dialog_open({
+ Title: _("Create filesystem"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ validate: name => validate_fs_name(null, name, filesystems)
+ }),
+ SizeSlider("size", _("Size"),
+ {
+ visible: () => managed_fsys_sizes,
+ min: fsys_min_size,
+ max: stats.pool_free,
+ round: 512
+ }),
+ TextInput("mount_point", _("Mount point"),
+ {
+ validate: (val, values, variant) => {
+ return is_valid_mount_point(client,
+ null,
+ client.add_mount_point_prefix(val),
+ variant == "nomount");
+ }
+ }),
+ mount_options(false, false),
+ at_boot_input(client.in_anaconda_mode() ? "local" : "nofail"),
+ ],
+ update: function (dlg, vals, trigger) {
+ if (trigger == "at_boot")
+ dlg.set_options("at_boot", { explanation: mount_explanation[vals.at_boot] });
+ },
+ Action: {
+ Variants: action_variants,
+ action: function (vals) {
+ return client.stratis_create_filesystem(pool, vals.name, vals.size)
+ .then(std_reply)
+ .then(result => {
+ if (result[0])
+ return set_mount_options(result[1][0][0], vals, forced_options);
+ else
+ return Promise.resolve();
+ });
+ }
+ }
+ });
+}
+
+function delete_pool(pool, card) {
+ const usage = get_active_usage(client, pool.path, _("delete"));
+
+ if (usage.Blocking) {
+ dialog_open({
+ Title: cockpit.format(_("$0 is in use"),
+ pool.Name),
+ Body: BlockingMessage(usage)
+ });
+ return;
+ }
+
+ dialog_open({
+ Title: cockpit.format(_("Permanently delete $0?"), pool.Name),
+ Teardown: TeardownMessage(usage),
+ Action: {
+ Danger: _("Deleting a Stratis pool will erase all data it contains."),
+ Title: _("Delete"),
+ action: async function () {
+ await teardown_active_usage(client, usage);
+ await destroy_pool(pool);
+ navigate_away_from_card(card);
+ }
+ },
+ Inits: [
+ init_active_usage_processes(client, usage)
+ ]
+ });
+}
+
+function rename_pool(pool) {
+ dialog_open({
+ Title: _("Rename Stratis pool"),
+ Fields: [
+ TextInput("name", _("Name"),
+ {
+ value: pool.Name,
+ validate: name => validate_pool_name(pool, name)
+ })
+ ],
+ Action: {
+ Title: _("Rename"),
+ action: function (vals) {
+ return pool.SetName(vals.name).then(std_reply);
+ }
+ }
+ });
+}
+
+function add_disks(pool) {
+ const blockdevs = client.stratis_pool_blockdevs[pool.path] || [];
+
+ with_keydesc(client, pool, (keydesc, keydesc_set) => {
+ const ask_passphrase = keydesc && !keydesc_set;
+
+ dialog_open({
+ Title: _("Add block devices"),
+ Fields: [
+ SelectOne("tier", _("Tier"),
+ {
+ choices: [
+ { value: "data", title: _("Data") },
+ {
+ value: "cache",
+ title: _("Cache"),
+ disabled: pool.Encrypted && !client.features.stratis_encrypted_caches
+ }
+ ]
+ }),
+ PassInput("passphrase", _("Passphrase"),
+ {
+ visible: () => ask_passphrase,
+ validate: val => !val.length && _("Passphrase cannot be empty"),
+ }),
+ SelectSpaces("disks", _("Block devices"),
+ {
+ empty_warning: _("No disks are available."),
+ validate: function(disks) {
+ if (disks.length === 0)
+ return _("At least one disk is needed.");
+ },
+ spaces: get_available_spaces(client)
+ })
+ ],
+ Action: {
+ Title: _("Add"),
+ action: function(vals) {
+ return prepare_available_spaces(client, vals.disks)
+ .then(paths => {
+ const devs = paths.map(p => decode_filename(client.blocks[p].PreferredDevice));
+
+ function add() {
+ if (vals.tier == "data") {
+ return pool.AddDataDevs(devs).then(std_reply);
+ } else if (vals.tier == "cache") {
+ const has_cache = blockdevs.some(bd => bd.Tier == 1);
+ const method = has_cache ? "AddCacheDevs" : "InitCache";
+ return pool[method](devs).then(std_reply);
+ }
+ }
+
+ if (ask_passphrase) {
+ return with_stored_passphrase(client, keydesc, vals.passphrase, add);
+ } else
+ return add();
+ });
+ }
+ }
+ });
+ });
+}
+
+function make_stratis_filesystem_pages(parent, pool) {
+ const filesystems = client.stratis_pool_filesystems[pool.path];
+ const stats = client.stratis_pool_stats[pool.path];
+ const forced_options = ["x-systemd.requires=stratis-fstab-setup@" + pool.Uuid + ".service"];
+ const managed_fsys_sizes = client.features.stratis_managed_fsys_sizes && !pool.Overprovisioning;
+
+ filesystems.forEach((fs, i) => make_stratis_filesystem_page(parent, pool, fs,
+ stats.fsys_offsets[i],
+ forced_options,
+ managed_fsys_sizes));
+}
+
+export function make_stratis_pool_page(parent, pool) {
+ const degraded_ops = pool.AvailableActions && pool.AvailableActions !== "fully_operational";
+ const blockdevs = client.stratis_pool_blockdevs[pool.path] || [];
+ const can_grow =
+ (client.features.stratis_grow_blockdevs &&
+ blockdevs.some(bd => bd.NewPhysicalSize[0] && Number(bd.NewPhysicalSize[1]) > Number(bd.TotalPhysicalSize)));
+ const managed_fsys_sizes = client.features.stratis_managed_fsys_sizes && !pool.Overprovisioning;
+ const stats = client.stratis_pool_stats[pool.path];
+
+ const use = pool.TotalPhysicalUsed[0] && [Number(pool.TotalPhysicalUsed[1]), Number(pool.TotalPhysicalSize)];
+
+ if (should_ignore(client, pool.path))
+ return;
+
+ const pool_card = new_card({
+ title: pool.Encrypted ? _("Encrypted Stratis pool") : _("Stratis pool"),
+ next: null,
+ page_location: ["pool", pool.Uuid],
+ page_name: pool.Name,
+ page_icon: VolumeIcon,
+ page_category: PAGE_CATEGORY_VIRTUAL,
+ page_size: ((!managed_fsys_sizes && use)
+ ? <StorageUsageBar key="s" stats={use} short />
+ : Number(pool.TotalPhysicalSize)),
+ component: StratisPoolCard,
+ props: { pool, degraded_ops, can_grow, managed_fsys_sizes, stats },
+ actions: [
+ {
+ title: _("Add block devices"),
+ action: () => add_disks(pool),
+ },
+ {
+ title: _("Delete pool"),
+ action: () => delete_pool(pool, pool_card),
+ danger: true,
+ },
+ ],
+ });
+
+ const fsys_card = new_card({
+ title: _("Stratis filesystems"),
+ next: pool_card,
+ has_warning: degraded_ops || can_grow,
+ component: StratisFilesystemsCard,
+ props: { pool, degraded_ops, can_grow, managed_fsys_sizes, stats },
+ actions: [
+ {
+ title: _("Create new filesystem"),
+ action: () => create_fs(pool),
+ excuse: (managed_fsys_sizes && stats.pool_free < fsys_min_size
+ ? _("Not enough space")
+ : null),
+ },
+ ],
+
+ });
+
+ const p = new_page(parent, fsys_card);
+ make_stratis_filesystem_pages(p, pool);
+}
+
+const StratisFilesystemsCard = ({ card, pool, degraded_ops, can_grow, managed_fsys_sizes, stats }) => {
+ const blockdevs = client.stratis_pool_blockdevs[pool.path] || [];
+
+ function grow_blockdevs() {
+ return for_each_async(blockdevs, bd => pool.GrowPhysicalDevice(bd.Uuid));
+ }
+
+ const alerts = [];
+ if (can_grow) {
+ alerts.push(
+ <Alert isInline key="unused"
+ variant="warning"
+ title={_("This pool does not use all the space on its block devices.")}>
+ {_("Some block devices of this pool have grown in size after the pool was created. The pool can be safely grown to use the newly available space.")}
+ <div className="storage-alert-actions">
+ <StorageButton onClick={grow_blockdevs}>
+ {_("Grow the pool to take all space")}
+ </StorageButton>
+ </div>
+ </Alert>);
+ }
+
+ if (degraded_ops) {
+ const goToStratisLogs = () => cockpit.jump("/system/logs/#/?prio=warn&_SYSTEMD_UNIT=stratisd.service");
+ alerts.push(
+ <Alert isInline key="degraded"
+ variant="warning"
+ title={_("This pool is in a degraded state.")}>
+ <div className="storage-alert-actions">
+ <Button variant="link" isInline onClick={goToStratisLogs}>
+ {_("View logs")}
+ </Button>
+ </div>
+ </Alert>);
+ }
+
+ return (
+ <StorageCard card={card} alerts={alerts}>
+ <CardBody className="contains-list">
+ <ChildrenTable emptyCaption={_("No filesystems")}
+ aria-label={_("Stratis filesystems pool")}
+ page={card.page} />
+ </CardBody>
+ </StorageCard>
+ );
+};
+
+const StratisPoolCard = ({ card, pool, degraded_ops, can_grow, managed_fsys_sizes, stats }) => {
+ const key_desc = (pool.Encrypted &&
+ pool.KeyDescription[0] &&
+ pool.KeyDescription[1][1]);
+ const can_tang = (client.features.stratis_crypto_binding &&
+ pool.Encrypted &&
+ pool.ClevisInfo[0] && // pool has consistent clevis config
+ (!pool.ClevisInfo[1][0] || pool.ClevisInfo[1][1][0] == "tang")); // not bound or bound to "tang"
+ const tang_url = can_tang && pool.ClevisInfo[1][0] ? JSON.parse(pool.ClevisInfo[1][1][1]).url : null;
+
+ function add_passphrase() {
+ dialog_open({
+ Title: _("Add passphrase"),
+ Fields: [
+ PassInput("passphrase", _("Passphrase"),
+ { validate: val => !val.length && _("Passphrase cannot be empty") }),
+ PassInput("passphrase2", _("Confirm"),
+ { validate: (val, vals) => vals.passphrase.length && vals.passphrase != val && _("Passphrases do not match") })
+ ],
+ Action: {
+ Title: _("Save"),
+ action: vals => {
+ return get_unused_keydesc(client, pool.Name)
+ .then(keydesc => {
+ return with_stored_passphrase(client, keydesc, vals.passphrase,
+ () => pool.BindKeyring(keydesc))
+ .then(std_reply);
+ });
+ }
+ }
+ });
+ }
+
+ function change_passphrase() {
+ with_keydesc(client, pool, (keydesc, keydesc_set) => {
+ dialog_open({
+ Title: _("Change passphrase"),
+ Fields: [
+ PassInput("old_passphrase", _("Old passphrase"),
+ {
+ visible: vals => !keydesc_set,
+ validate: val => !val.length && _("Passphrase cannot be empty")
+ }),
+ PassInput("new_passphrase", _("New passphrase"),
+ { validate: val => !val.length && _("Passphrase cannot be empty") }),
+ PassInput("new_passphrase2", _("Confirm"),
+ { validate: (val, vals) => vals.new_passphrase.length && vals.new_passphrase != val && _("Passphrases do not match") })
+ ],
+ Action: {
+ Title: _("Save"),
+ action: vals => {
+ function rebind() {
+ return get_unused_keydesc(client, pool.Name)
+ .then(new_keydesc => {
+ return with_stored_passphrase(client, new_keydesc, vals.new_passphrase,
+ () => pool.RebindKeyring(new_keydesc))
+ .then(std_reply);
+ });
+ }
+
+ if (vals.old_passphrase) {
+ return with_stored_passphrase(client, keydesc, vals.old_passphrase, rebind);
+ } else {
+ return rebind();
+ }
+ }
+ }
+ });
+ });
+ }
+
+ function remove_passphrase() {
+ dialog_open({
+ Title: _("Remove passphrase?"),
+ Body: <div>
+ <p className="slot-warning">{ fmt_to_fragments(_("Passphrase removal may prevent unlocking $0."), <b>{pool.Name}</b>) }</p>
+ </div>,
+ Action: {
+ DangerButton: true,
+ Title: _("Remove"),
+ action: function (vals) {
+ return pool.UnbindKeyring().then(std_reply);
+ }
+ }
+ });
+ }
+
+ function add_tang() {
+ return with_keydesc(client, pool, (keydesc, keydesc_set) => {
+ dialog_open({
+ Title: _("Add Tang keyserver"),
+ Fields: [
+ TextInput("tang_url", _("Keyserver address"),
+ {
+ validate: validate_url
+ }),
+ PassInput("passphrase", _("Pool passphrase"),
+ {
+ visible: () => !keydesc_set,
+ validate: val => !val.length && _("Passphrase cannot be empty"),
+ explanation: _("Adding a keyserver requires unlocking the pool. Please provide the existing pool passphrase.")
+ })
+ ],
+ Action: {
+ Title: _("Save"),
+ action: function (vals, progress) {
+ return get_tang_adv(vals.tang_url)
+ .then(adv => {
+ function bind() {
+ return pool.BindClevis("tang", JSON.stringify({ url: vals.tang_url, adv }))
+ .then(std_reply);
+ }
+ confirm_tang_trust(vals.tang_url, adv,
+ () => {
+ if (vals.passphrase)
+ return with_stored_passphrase(client, keydesc,
+ vals.passphrase, bind);
+ else
+ return bind();
+ });
+ });
+ }
+ }
+ });
+ });
+ }
+
+ function remove_tang() {
+ dialog_open({
+ Title: _("Remove Tang keyserver?"),
+ Body: <div>
+ <p>{ fmt_to_fragments(_("Remove $0?"), <b>{tang_url}</b>) }</p>
+ <p className="slot-warning">{ fmt_to_fragments(_("Keyserver removal may prevent unlocking $0."), <b>{pool.Name}</b>) }</p>
+ </div>,
+ Action: {
+ DangerButton: true,
+ Title: _("Remove"),
+ action: function (vals) {
+ return pool.UnbindClevis().then(std_reply);
+ }
+ }
+ });
+ }
+
+ const use = pool.TotalPhysicalUsed[0] && [Number(pool.TotalPhysicalUsed[1]), Number(pool.TotalPhysicalSize)];
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Name")}
+ value={pool.Name}
+ action={<StorageLink onClick={() => rename_pool(pool)}>
+ {_("edit")}
+ </StorageLink>} />
+ <StorageDescription title={_("UUID")} value={pool.Uuid} />
+ { !managed_fsys_sizes && use &&
+ <StorageDescription title={_("Usage")}>
+ <StorageUsageBar stats={use} critical={0.95} />
+ </StorageDescription>
+ }
+ { pool.Encrypted && client.features.stratis_crypto_binding &&
+ <StorageDescription title={_("Passphrase")}>
+ <Flex>
+ { !key_desc
+ ? <FlexItem><StorageLink onClick={add_passphrase}>{_("Add passphrase")}</StorageLink></FlexItem>
+ : <>
+ <FlexItem><StorageLink onClick={change_passphrase}>{_("Change")}</StorageLink></FlexItem>
+ <FlexItem>
+ <StorageLink onClick={remove_passphrase}
+ excuse={!tang_url ? _("This passphrase is the only way to unlock the pool and can not be removed.") : null}>
+ {_("Remove")}
+ </StorageLink>
+ </FlexItem>
+ </>
+ }
+ </Flex>
+ </StorageDescription>
+ }
+ { can_tang &&
+ <StorageDescription title={_("Keyserver")}>
+ <Flex>
+ { tang_url == null
+ ? <FlexItem><StorageLink onClick={add_tang}>{_("Add keyserver")}</StorageLink></FlexItem>
+ : <>
+ <FlexItem>{ tang_url }</FlexItem>
+ <FlexItem>
+ <StorageLink onClick={remove_tang}
+ excuse={!key_desc ? _("This keyserver is the only way to unlock the pool and can not be removed.") : null}>
+ {_("Remove")}
+ </StorageLink>
+ </FlexItem>
+ </>
+ }
+ </Flex>
+ </StorageDescription>
+ }
+ </DescriptionList>
+ </CardBody>
+ <CardHeader><strong>{_("Block devices")}</strong></CardHeader>
+ <CardBody className="contains-list">
+ <PageTable emptyCaption={_("No block devices found")}
+ aria-label={_("Stratis block devices")}
+ crossrefs={get_crossrefs(pool)} />
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/stratis/stopped-pool.jsx b/pkg/storaged/stratis/stopped-pool.jsx
new file mode 100644
index 0000000..5787d75
--- /dev/null
+++ b/pkg/storaged/stratis/stopped-pool.jsx
@@ -0,0 +1,146 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardHeader, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
+
+import { VolumeIcon } from "../icons/gnome-icons.jsx";
+import {
+ StorageCard, StorageDescription, PageTable,
+ new_page, new_card, PAGE_CATEGORY_VIRTUAL,
+ get_crossrefs
+} from "../pages.jsx";
+import { dialog_open, PassInput } from "../dialog.jsx";
+import { std_reply, with_stored_passphrase } from "./utils.jsx";
+
+const _ = cockpit.gettext;
+
+function start_pool(uuid, show_devs) {
+ const devs = client.stratis_manager.StoppedPools[uuid].devs.v.map(d => d.devnode).sort();
+ const key_desc = client.stratis_stopped_pool_key_description[uuid];
+ const clevis_info = client.stratis_stopped_pool_clevis_info[uuid];
+
+ function start(unlock_method) {
+ return client.stratis_start_pool(uuid, unlock_method).then(std_reply);
+ }
+
+ function unlock_with_keydesc(key_desc) {
+ dialog_open({
+ Title: _("Unlock encrypted Stratis pool"),
+ Body: (show_devs &&
+ <>
+ <p>{_("Provide the passphrase for the pool on these block devices:")}</p>
+ <List>{devs.map(d => <ListItem key={d}>{d}</ListItem>)}</List>
+ <br />
+ </>),
+ Fields: [
+ PassInput("passphrase", _("Passphrase"), { })
+ ],
+ Action: {
+ Title: _("Unlock"),
+ action: function(vals) {
+ return with_stored_passphrase(client, key_desc, vals.passphrase,
+ () => start("keyring"));
+ }
+ }
+ });
+ }
+
+ function unlock_with_keyring() {
+ return (client.stratis_list_keys()
+ .catch(() => [{ }])
+ .then(keys => {
+ if (keys.indexOf(key_desc) >= 0)
+ return start("keyring");
+ else
+ unlock_with_keydesc(key_desc);
+ }));
+ }
+
+ if (!key_desc && !clevis_info) {
+ // Not an encrypted pool, just start it
+ return start();
+ } else if (key_desc && clevis_info) {
+ return start("clevis").catch(unlock_with_keyring);
+ } else if (!key_desc && clevis_info) {
+ return start("clevis");
+ } else if (key_desc && !clevis_info) {
+ return unlock_with_keyring();
+ }
+}
+
+export function make_stratis_stopped_pool_page(parent, uuid) {
+ if (client.in_anaconda_mode())
+ return;
+
+ const pool_card = new_card({
+ title: _("Stratis pool"),
+ type_extra: _("stopped"),
+ next: null,
+ page_location: ["pool", uuid],
+ page_name: uuid,
+ page_icon: VolumeIcon,
+ page_category: PAGE_CATEGORY_VIRTUAL,
+ component: StoppedStratisPoolCard,
+ props: { uuid },
+ actions: [
+ { title: _("Start"), action: from_menu => start_pool(uuid, from_menu), },
+ ],
+ });
+
+ new_page(parent, pool_card);
+}
+
+const StoppedStratisPoolCard = ({ card, uuid }) => {
+ const key_desc = client.stratis_stopped_pool_key_description[uuid];
+ const clevis_info = client.stratis_stopped_pool_clevis_info[uuid];
+
+ const encrypted = key_desc || clevis_info;
+ const can_tang = encrypted && (!clevis_info || clevis_info[0] == "tang");
+ const tang_url = (can_tang && clevis_info) ? JSON.parse(clevis_info[1]).url : null;
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("UUID")} value={uuid} />
+ { encrypted && client.features.stratis_crypto_binding &&
+ <StorageDescription title={_("Passphrase")}>
+ { key_desc ? cockpit.format(_("using key description $0"), key_desc) : _("none") }
+ </StorageDescription>
+ }
+ { can_tang && client.features.stratis_crypto_binding &&
+ <StorageDescription title={_("Keyserver")} value={ tang_url || _("none") } />
+ }
+ </DescriptionList>
+ </CardBody>
+ <CardHeader><strong>{_("Block devices")}</strong></CardHeader>
+ <CardBody className="contains-list">
+ <PageTable emptyCaption={_("No block devices found")}
+ aria-label={_("Stratis block devices")}
+ crossrefs={get_crossrefs(uuid)} />
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/stratis/stratis2-set-key.py b/pkg/storaged/stratis/stratis2-set-key.py
new file mode 100644
index 0000000..87e83ac
--- /dev/null
+++ b/pkg/storaged/stratis/stratis2-set-key.py
@@ -0,0 +1,10 @@
+import sys
+
+import dbus
+
+reply = dbus.SystemBus().call_blocking('org.storage.stratis2', '/org/storage/stratis2',
+ 'org.storage.stratis2.Manager.r1', 'SetKey',
+ 'shb', [sys.argv[1], 0, False])
+if reply[1] != 0:
+ sys.stderr.write(reply[2] + '\n')
+ sys.exit(1)
diff --git a/pkg/storaged/stratis/stratis3-set-key.py b/pkg/storaged/stratis/stratis3-set-key.py
new file mode 100644
index 0000000..5084718
--- /dev/null
+++ b/pkg/storaged/stratis/stratis3-set-key.py
@@ -0,0 +1,10 @@
+import sys
+
+import dbus
+
+reply = dbus.SystemBus().call_blocking('org.storage.stratis3', '/org/storage/stratis3',
+ 'org.storage.stratis3.Manager.r0', 'SetKey',
+ 'shb', [sys.argv[1], 0, False])
+if reply[1] != 0:
+ sys.stderr.write(reply[2] + '\n')
+ sys.exit(1)
diff --git a/pkg/storaged/stratis/utils.jsx b/pkg/storaged/stratis/utils.jsx
new file mode 100644
index 0000000..e2278dd
--- /dev/null
+++ b/pkg/storaged/stratis/utils.jsx
@@ -0,0 +1,187 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { for_each_async, reload_systemd, encode_filename } from "../utils.js";
+import { dialog_open } from "../dialog.jsx";
+import { TangKeyVerification } from "../crypto/tang.jsx";
+
+const _ = cockpit.gettext;
+
+export function validate_pool_name(pool, name) {
+ if (name == "")
+ return _("Name can not be empty.");
+ if ((!pool || name != pool.Name) && client.stratis_poolnames_pool[name])
+ return _("A pool with this name exists already.");
+}
+
+export function std_reply(result, code, message) {
+ if (code)
+ return Promise.reject(message);
+ else
+ return Promise.resolve(result);
+}
+
+export function with_keydesc(client, pool, func) {
+ if (!pool.KeyDescription ||
+ !pool.KeyDescription[0] ||
+ !pool.KeyDescription[1][0]) {
+ return func(false);
+ } else {
+ const keydesc = pool.KeyDescription[1][1];
+ return client.stratis_list_keys()
+ .catch(() => []) // not-covered: internal error
+ .then(keys => func(keydesc, keys.indexOf(keydesc) >= 0));
+ }
+}
+
+export function with_stored_passphrase(client, keydesc, passphrase, func) {
+ return client.stratis_store_passphrase(keydesc, passphrase)
+ .then(func)
+ .finally(() => {
+ return client.stratis_manager.UnsetKey(keydesc)
+ .then(std_reply)
+ .catch(ex => { console.warn("Failed to remove passphrase from key ring", ex.toString()) }); // not-covered: internal error
+ });
+}
+
+export function get_unused_keydesc(client, desc_prefix) {
+ return client.stratis_list_keys()
+ .catch(() => []) // not-covered: internal error
+ .then(keys => {
+ let desc;
+ for (let i = 0; i < 1000; i++) {
+ desc = desc_prefix + (i > 0 ? "." + i.toFixed() : "");
+ if (keys.indexOf(desc) == -1)
+ break;
+ }
+ return desc;
+ });
+}
+
+export function confirm_tang_trust(url, adv, action) {
+ dialog_open({
+ Title: _("Verify key"),
+ Body: <TangKeyVerification url={url} adv={adv} />,
+ Action: {
+ Title: _("Trust key"),
+ action
+ }
+ });
+}
+
+export function check_stratis_warnings(client, enter_warning) {
+ if (!client.features.stratis_grow_blockdevs)
+ return;
+
+ for (const p in client.stratis_pools) {
+ const blockdevs = client.stratis_pool_blockdevs[p] || [];
+ const pool = client.stratis_pools[p];
+ if (blockdevs.some(bd => bd.NewPhysicalSize[0] && Number(bd.NewPhysicalSize[1]) > Number(bd.TotalPhysicalSize)))
+ enter_warning(p, { warning: "unused-blockdevs" });
+ if (pool.AvailableActions && pool.AvailableActions !== "fully_operational")
+ enter_warning(p, { warning: "not-fully-operational" });
+ }
+}
+
+function teardown_block(block) {
+ return for_each_async(block.Configuration, c => block.RemoveConfigurationItem(c, {}));
+}
+
+export function destroy_filesystem(fsys) {
+ const block = client.slashdevs_block[fsys.Devnode];
+ const pool = client.stratis_pools[fsys.Pool];
+
+ return teardown_block(block).then(() => pool.DestroyFilesystems([fsys.path]).then(std_reply));
+}
+
+export function validate_fs_name(fsys, name, filesystems) {
+ if (name == "")
+ return _("Name can not be empty.");
+ if (!fsys || name != fsys.Name) {
+ for (const fs of filesystems) {
+ if (fs.Name == name)
+ return _("A filesystem with this name exists already in this pool.");
+ }
+ }
+}
+
+export function set_mount_options(path, vals, forced_options) {
+ let mount_options = [];
+
+ if (vals.variant == "nomount" || vals.at_boot == "never")
+ mount_options.push("noauto");
+ if (vals.mount_options?.ro)
+ mount_options.push("ro");
+ if (vals.at_boot == "never")
+ mount_options.push("x-cockpit-never-auto");
+ if (vals.at_boot == "nofail")
+ mount_options.push("nofail");
+ if (vals.at_boot == "netdev")
+ mount_options.push("_netdev");
+ if (vals.mount_options?.extra)
+ mount_options.push(vals.mount_options.extra);
+
+ mount_options = mount_options.concat(forced_options);
+
+ let mount_point = vals.mount_point;
+ if (mount_point == "")
+ return Promise.resolve();
+ if (mount_point[0] != "/")
+ mount_point = "/" + mount_point;
+ mount_point = client.add_mount_point_prefix(mount_point);
+
+ const config =
+ ["fstab",
+ {
+ dir: { t: 'ay', v: encode_filename(mount_point) },
+ type: { t: 'ay', v: encode_filename("auto") },
+ opts: { t: 'ay', v: encode_filename(mount_options.join(",") || "defaults") },
+ freq: { t: 'i', v: 0 },
+ passno: { t: 'i', v: 0 },
+ }
+ ];
+
+ function udisks_block_for_stratis_fsys() {
+ const fsys = client.stratis_filesystems[path];
+ return fsys && client.slashdevs_block[fsys.Devnode];
+ }
+
+ return client.wait_for(udisks_block_for_stratis_fsys)
+ .then(block => {
+ // HACK - need a explicit "change" event
+ return block.Rescan({})
+ .then(() => {
+ return client.wait_for(() => client.blocks_fsys[block.path])
+ .then(fsys => {
+ return block.AddConfigurationItem(config, {})
+ .then(reload_systemd)
+ .then(() => {
+ if (vals.variant != "nomount")
+ return client.mount_at(block, mount_point);
+ else
+ return Promise.resolve();
+ });
+ });
+ });
+ });
+}
diff --git a/pkg/storaged/swap/swap.jsx b/pkg/storaged/swap/swap.jsx
new file mode 100644
index 0000000..de588df
--- /dev/null
+++ b/pkg/storaged/swap/swap.jsx
@@ -0,0 +1,130 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import client from "../client";
+
+import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { useEvent } from "hooks";
+
+import { StorageCard, StorageDescription, new_card } from "../pages.jsx";
+import { format_dialog } from "../block/format-dialog.jsx";
+import {
+ fmt_size, decode_filename, encode_filename,
+ parse_options, unparse_options, extract_option,
+} from "../utils.js";
+import { std_lock_action } from "../crypto/actions.jsx";
+
+const _ = cockpit.gettext;
+
+async function set_swap_noauto(block, noauto) {
+ for (const conf of block.Configuration) {
+ if (conf[0] == "fstab") {
+ const options = parse_options(decode_filename(conf[1].opts.v));
+ extract_option(options, "defaults");
+ extract_option(options, "noauto");
+ if (noauto)
+ options.push("noauto");
+ if (options.length == 0)
+ options.push("defaults");
+ const new_conf = [
+ "fstab",
+ Object.assign({ }, conf[1],
+ {
+ opts: {
+ t: 'ay',
+ v: encode_filename(unparse_options(options))
+ }
+ })
+ ];
+ await block.UpdateConfigurationItem(conf, new_conf, { });
+ return;
+ }
+ }
+
+ await block.AddConfigurationItem(
+ ["fstab", {
+ dir: { t: 'ay', v: encode_filename("none") },
+ type: { t: 'ay', v: encode_filename("swap") },
+ opts: { t: 'ay', v: encode_filename(noauto ? "noauto" : "defaults") },
+ freq: { t: 'i', v: 0 },
+ passno: { t: 'i', v: 0 },
+ "track-parents": { t: 'b', v: true }
+ }], { });
+}
+
+export function make_swap_card(next, backing_block, content_block) {
+ const block_swap = client.blocks_swap[content_block.path];
+
+ async function start() {
+ await block_swap.Start({});
+ await set_swap_noauto(content_block, false);
+ }
+
+ async function stop() {
+ await block_swap.Stop({});
+ await set_swap_noauto(content_block, true);
+ }
+
+ return new_card({
+ title: _("Swap"),
+ next,
+ component: SwapCard,
+ props: { block: content_block, block_swap },
+ actions: [
+ std_lock_action(backing_block, content_block),
+ (block_swap && block_swap.Active
+ ? { title: _("Stop"), action: stop }
+ : null),
+ (block_swap && !block_swap.Active
+ ? { title: _("Start"), action: start }
+ : null),
+ { title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true },
+ ]
+ });
+}
+
+export const SwapCard = ({ card, block, block_swap }) => {
+ const is_active = block_swap && block_swap.Active;
+ let used;
+
+ useEvent(client.swap_sizes, "changed");
+
+ if (is_active) {
+ const samples = client.swap_sizes.data[decode_filename(block.Device)];
+ if (samples)
+ used = fmt_size(samples[0] - samples[1]);
+ else
+ used = _("Unknown");
+ } else {
+ used = "-";
+ }
+
+ return (
+ <StorageCard card={card}>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ <StorageDescription title={_("Used")} value={used} />
+ </DescriptionList>
+ </CardBody>
+ </StorageCard>
+ );
+};
diff --git a/pkg/storaged/test-util.js b/pkg/storaged/test-util.js
new file mode 100644
index 0000000..2a53d3d
--- /dev/null
+++ b/pkg/storaged/test-util.js
@@ -0,0 +1,88 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import * as utils from "./utils.js";
+import QUnit from "qunit-tests";
+
+QUnit.test("format_delay", function (assert) {
+ const checks = [
+ [3000, "less than a minute"],
+ [60000, "1 minute"],
+ [15550000, "about 4 hours"],
+ ];
+
+ assert.expect(checks.length);
+ for (let i = 0; i < checks.length; i++) {
+ assert.strictEqual(utils.format_delay(checks[i][0]), checks[i][1],
+ "format_delay(" + checks[i][0] + ") = " + checks[i][1]);
+ }
+});
+
+QUnit.test("compare_versions", function (assert) {
+ const checks = [
+ ["", "", 0],
+ ["0", "0", 0],
+ ["1", "0", 1],
+ ["0", "1", -1],
+ ["2", "1.9", 1],
+ ["2.0", "2", 1],
+ ["2.1.6", "2.5", -1],
+ ["2..6", "2.0.6", 0],
+ ];
+
+ function sign(n) {
+ return (n === 0) ? 0 : (n < 0) ? -1 : 1;
+ }
+
+ assert.expect(checks.length);
+ for (let i = 0; i < checks.length; i++) {
+ assert.strictEqual(sign(utils.compare_versions(checks[i][0], checks[i][1])), checks[i][2],
+ "compare_versions(" + checks[i][0] + ", " + checks[i][1] + ") = " + checks[i][2]);
+ }
+});
+
+QUnit.test("mdraid_name_nohostnamed", function (assert) {
+ utils.mock_hostnamed({ StaticHostname: undefined });
+ assert.strictEqual(utils.mdraid_name({ Name: "somehost:mydev" }), "mydev", "remote host name is skipped when hostnamed is not available");
+ utils.mock_hostnamed(null);
+});
+
+QUnit.test("mdraid_name_remote", function (assert) {
+ utils.mock_hostnamed({ StaticHostname: "sweethome" });
+ assert.strictEqual(utils.mdraid_name({ Name: "somehost:mydev" }), "mydev (from somehost)", "expected name for remote host");
+ utils.mock_hostnamed(null);
+});
+
+QUnit.test("mdraid_name_local_static", function (assert) {
+ utils.mock_hostnamed({ StaticHostname: "sweethome" });
+ assert.strictEqual(utils.mdraid_name({ Name: "sweethome:mydev" }), "mydev", "expected name for static local host");
+ utils.mock_hostnamed(null);
+});
+
+QUnit.test("mdraid_name_local_transient", function (assert) {
+ utils.mock_hostnamed({ Hostname: "sweethome" });
+ assert.strictEqual(utils.mdraid_name({ Name: "sweethome:mydev" }), "mydev", "expected name for transient local host");
+ utils.mock_hostnamed(null);
+});
+
+/* Wait until the hostnamed dbus proxy is actually ready; otherwise the test
+ * finishes and kills the bridge before it can respond to the dbus channel open
+ * request for the hostnamed connection, which can cause hangs in
+ * ./test-server due to timing out that queued request. */
+utils.hostnamed.wait(QUnit.start);
diff --git a/pkg/storaged/utils.js b/pkg/storaged/utils.js
new file mode 100644
index 0000000..b57925b
--- /dev/null
+++ b/pkg/storaged/utils.js
@@ -0,0 +1,1140 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import * as service from "service";
+import * as timeformat from "timeformat";
+
+const _ = cockpit.gettext;
+const C_ = cockpit.gettext;
+
+/* UTILITIES
+ */
+
+export function compare_versions(a, b) {
+ function to_ints(str) {
+ return str.split(".").map(function (s) { return s ? parseInt(s, 10) : 0 });
+ }
+
+ const a_ints = to_ints(a);
+ const b_ints = to_ints(b);
+ const len = Math.min(a_ints.length, b_ints.length);
+ let i;
+
+ for (i = 0; i < len; i++) {
+ if (a_ints[i] == b_ints[i])
+ continue;
+ return a_ints[i] - b_ints[i];
+ }
+
+ return a_ints.length - b_ints.length;
+}
+
+export function parse_options(options) {
+ if (options)
+ return (options.split(",")
+ .map(function (s) { return s.trim() })
+ .filter(function (s) { return s != "" }));
+ else
+ return [];
+}
+
+export function unparse_options(split) {
+ return split.join(",");
+}
+
+export function extract_option(split, opt) {
+ const index = split.indexOf(opt);
+ if (index >= 0) {
+ split.splice(index, 1);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+export function edit_crypto_config(block, modify) {
+ let old_config, new_config;
+
+ function commit() {
+ new_config[1]["track-parents"] = { t: 'b', v: true };
+ if (old_config)
+ return block.UpdateConfigurationItem(old_config, new_config, { });
+ else
+ return block.AddConfigurationItem(new_config, { });
+ }
+
+ return block.GetSecretConfiguration({}).then(
+ function (items) {
+ old_config = items.find(c => c[0] == "crypttab");
+ new_config = ["crypttab", old_config ? Object.assign({ }, old_config[1]) : { }];
+
+ // UDisks insists on always having a "passphrase-contents" field when
+ // adding a crypttab entry, but doesn't include one itself when returning
+ // an entry without a stored passphrase.
+ //
+ if (!new_config[1]['passphrase-contents'])
+ new_config[1]['passphrase-contents'] = { t: 'ay', v: encode_filename("") };
+
+ return modify(new_config[1], commit);
+ });
+}
+
+export function set_crypto_options(block, readonly, auto, nofail, netdev) {
+ return edit_crypto_config(block, (config, commit) => {
+ const opts = config.options ? parse_options(decode_filename(config.options.v)) : [];
+ if (readonly !== null) {
+ extract_option(opts, "readonly");
+ if (readonly)
+ opts.push("readonly");
+ }
+ if (auto !== null) {
+ extract_option(opts, "noauto");
+ if (!auto)
+ opts.push("noauto");
+ }
+ if (nofail !== null) {
+ extract_option(opts, "nofail");
+ if (nofail)
+ opts.push("nofail");
+ }
+ if (netdev !== null) {
+ extract_option(opts, "_netdev");
+ if (netdev)
+ opts.push("_netdev");
+ }
+ config.options = { t: 'ay', v: encode_filename(unparse_options(opts)) };
+ return commit();
+ });
+}
+
+export function set_crypto_auto_option(block, flag) {
+ return set_crypto_options(block, null, flag, null, null);
+}
+
+export let hostnamed = cockpit.dbus("org.freedesktop.hostname1").proxy();
+
+// for unit tests
+let orig_hostnamed;
+
+export function mock_hostnamed(value) {
+ if (value) {
+ orig_hostnamed = hostnamed;
+ hostnamed = value;
+ } else {
+ hostnamed = orig_hostnamed;
+ }
+}
+
+export function flatten(array_of_arrays) {
+ if (array_of_arrays.length > 0)
+ return Array.prototype.concat.apply([], array_of_arrays);
+ else
+ return [];
+}
+
+export function decode_filename(encoded) {
+ return cockpit.utf8_decoder().decode(cockpit.base64_decode(encoded).slice(0, -1));
+}
+
+export function encode_filename(decoded) {
+ return cockpit.base64_encode(cockpit.utf8_encoder().encode(decoded)
+ .concat([0]));
+}
+
+export function get_block_mntopts(config) {
+ // treat an absent field as "default", like util-linux
+ return (config.opts ? decode_filename(config.opts.v) : "defaults");
+}
+
+export function fmt_size(bytes) {
+ return cockpit.format_bytes(bytes);
+}
+
+export function fmt_size_long(bytes) {
+ const with_decimal_unit = cockpit.format_bytes(bytes, 1000);
+ const with_binary_unit = cockpit.format_bytes(bytes, 1024);
+ /* Translators: Used in "..." */
+ return with_decimal_unit + ", " + with_binary_unit + ", " + bytes + " " + C_("format-bytes", "bytes");
+}
+
+export function fmt_rate(bytes_per_sec) {
+ return cockpit.format_bytes_per_sec(bytes_per_sec);
+}
+
+export function format_temperature(kelvin) {
+ const celsius = kelvin - 273.15;
+ const fahrenheit = 9.0 * celsius / 5.0 + 32.0;
+ return celsius.toFixed(1) + "° C / " + fahrenheit.toFixed(1) + "° F";
+}
+
+export function format_fsys_usage(used, total) {
+ let text = "";
+ let parts = cockpit.format_bytes(total, undefined, { separate: true, precision: 2 });
+ text = " / " + parts.join(" ");
+ const unit = parts[1];
+
+ parts = cockpit.format_bytes(used, unit, { separate: true, precision: 2 });
+ return parts[0] + text;
+}
+
+export function format_delay(d) {
+ return timeformat.distanceToNow(new Date().valueOf() + d);
+}
+
+export function format_size_and_text(size, text) {
+ return fmt_size(size) + " " + text;
+}
+
+export function validate_mdraid_name(name) {
+ return validate_lvm2_name(name);
+}
+
+export function validate_lvm2_name(name) {
+ if (name === "")
+ return _("Name cannot be empty.");
+ if (name.length > 127)
+ return _("Name cannot be longer than 127 characters.");
+ const m = name.match(/[^a-zA-Z0-9+._-]/);
+ if (m) {
+ if (m[0].search(/\s+/) === -1)
+ return cockpit.format(_("Name cannot contain the character '$0'."), m[0]);
+ else
+ return cockpit.format(_("Name cannot contain whitespace."), m[0]);
+ }
+}
+
+export function validate_fsys_label(label, type) {
+ const fs_label_max = {
+ xfs: 12,
+ ext4: 16,
+ vfat: 11,
+ ntfs: 128,
+ btrfs: 256,
+ };
+
+ const limit = fs_label_max[type.replace("luks+", "")];
+ const bytes = cockpit.utf8_encoder().encode(label);
+ if (limit && bytes.length > limit) {
+ // Let's not confuse people with encoding issues unless
+ // they use funny characters.
+ if (bytes.length == label.length)
+ return cockpit.format(_("Name cannot be longer than $0 characters"), limit);
+ else
+ return cockpit.format(_("Name cannot be longer than $0 bytes"), limit);
+ }
+}
+
+export function block_name(block) {
+ return decode_filename(block.PreferredDevice);
+}
+
+export function block_short_name(block) {
+ return block_name(block).replace(/^\/dev\//, "");
+}
+
+export function mdraid_name(mdraid) {
+ if (!mdraid.Name)
+ return "";
+
+ const parts = mdraid.Name.split(":");
+
+ if (parts.length != 2)
+ return mdraid.Name;
+
+ /* Check the static (from /etc/hostname) and transient (acquired from DHCP server via
+ * NetworkManager → hostnamed, may not exist) host name -- if either one matches, we
+ * consider the RAID a local one and just show the device name.
+ * Otherwise it's a remote one, and include the host in the name.
+ *
+ * However: if we call hostnamed too early, before the dbus.proxy() promise is
+ * fulfilled, it will not be valid yet (hostnamed properties are undefined);
+ * it's too inconvenient to make this function asynchronous, so just don't
+ * show the host name in this case. */
+ if (hostnamed.StaticHostname === undefined || parts[0] == hostnamed.StaticHostname || parts[0] == hostnamed.Hostname)
+ return parts[1];
+ else
+ return cockpit.format(_("$name (from $host)"),
+ {
+ name: parts[1],
+ host: parts[0]
+ });
+}
+
+export function lvol_name(lvol) {
+ let type;
+ if (lvol.Type == "pool")
+ type = _("Pool for thin logical volumes");
+ else if (lvol.ThinPool != "/")
+ type = _("Thin logical volume");
+ else if (lvol.Origin != "/")
+ type = _("Logical volume (snapshot)");
+ else
+ type = _("Logical volume");
+ return cockpit.format('$0 "$1"', type, lvol.Name);
+}
+
+export function drive_name(drive) {
+ const name_parts = [];
+ if (drive.Vendor)
+ name_parts.push(drive.Vendor);
+ if (drive.Model)
+ name_parts.push(drive.Model);
+
+ let name = name_parts.join(" ");
+ if (drive.Serial)
+ name += " (" + drive.Serial + ")";
+ else if (drive.WWN)
+ name += " (" + drive.WWN + ")";
+
+ return name;
+}
+
+export function get_block_link_parts(client, path) {
+ let is_part, is_crypt, is_lvol;
+
+ while (true) {
+ if (client.blocks_part[path] && client.blocks_ptable[client.blocks_part[path].Table]) {
+ is_part = true;
+ path = client.blocks_part[path].Table;
+ } else if (client.blocks[path] && client.blocks[client.blocks[path].CryptoBackingDevice]) {
+ is_crypt = true;
+ path = client.blocks[path].CryptoBackingDevice;
+ } else
+ break;
+ }
+
+ if (client.blocks_lvm2[path] && client.lvols[client.blocks_lvm2[path].LogicalVolume])
+ is_lvol = true;
+
+ const block = client.blocks[path];
+ if (!block)
+ return;
+
+ let location, link;
+ if (client.mdraids[block.MDRaid]) {
+ location = ["mdraid", client.mdraids[block.MDRaid].UUID];
+ link = cockpit.format(_("MDRAID device $0"), mdraid_name(client.mdraids[block.MDRaid]));
+ } else if (client.blocks_lvm2[path] &&
+ client.lvols[client.blocks_lvm2[path].LogicalVolume] &&
+ client.vgroups[client.lvols[client.blocks_lvm2[path].LogicalVolume].VolumeGroup]) {
+ const target = client.vgroups[client.lvols[client.blocks_lvm2[path].LogicalVolume].VolumeGroup].Name;
+ location = ["vg", target];
+ link = cockpit.format(_("LVM2 volume group $0"), target);
+ } else {
+ const vdo = client.legacy_vdo_overlay.find_by_block(block);
+ if (vdo) {
+ location = ["vdo", vdo.name];
+ link = cockpit.format(_("VDO device $0"), vdo.name);
+ } else {
+ location = [block_short_name(block)];
+ if (client.drives[block.Drive])
+ link = drive_name(client.drives[block.Drive]);
+ else
+ link = block_name(block);
+ }
+ }
+
+ // Partitions of logical volumes are shown as just logical volumes.
+ let format;
+ if (is_lvol && is_crypt)
+ format = _("Encrypted logical volume of $0");
+ else if (is_part && is_crypt)
+ format = _("Encrypted partition of $0");
+ else if (is_lvol)
+ format = _("Logical volume of $0");
+ else if (is_part)
+ format = _("Partition of $0");
+ else if (is_crypt)
+ format = _("Encrypted $0");
+ else
+ format = "$0";
+
+ return {
+ location,
+ format,
+ link
+ };
+}
+
+export function go_to_block(client, path) {
+ const parts = get_block_link_parts(client, path);
+ cockpit.location.go(parts.location);
+}
+
+export function get_partitions(client, block) {
+ const partitions = client.blocks_partitions[block.path];
+
+ function process_level(level, container_start, container_size) {
+ let n;
+ let last_end = container_start;
+ const total_end = container_start + container_size;
+ let block, start, size, is_container, is_contained;
+
+ const result = [];
+
+ function append_free_space(start, size) {
+ // There is a lot of rounding and aligning going on in
+ // the storage stack. All of udisks2, libblockdev,
+ // and libparted seem to contribute their own ideas of
+ // where a partition really should start.
+ //
+ // The start of partitions are aggressively rounded
+ // up, sometimes twice, but the end is not aligned in
+ // the same way. This means that a few megabytes of
+ // free space will show up between partitions.
+ //
+ // We hide these small free spaces because they are
+ // unexpected and can't be used for anything anyway.
+ //
+ // "Small" is anything less than 3 MiB, which seems to
+ // work okay. (The worst case is probably creating
+ // the first logical partition inside a extended
+ // partition with udisks+libblockdev. It leads to a 2
+ // MiB gap.)
+
+ if (size >= 3 * 1024 * 1024) {
+ result.push({ type: 'free', start, size });
+ }
+ }
+
+ for (n = 0; n < partitions.length; n++) {
+ block = client.blocks[partitions[n].path];
+ start = partitions[n].Offset;
+ size = partitions[n].Size;
+ is_container = partitions[n].IsContainer;
+ is_contained = partitions[n].IsContained;
+
+ if (block === null)
+ continue;
+
+ if (level === 0 && is_contained)
+ continue;
+
+ if (level == 1 && !is_contained)
+ continue;
+
+ if (start < container_start || start + size > container_start + container_size)
+ continue;
+
+ append_free_space(last_end, start - last_end);
+ if (is_container) {
+ result.push({
+ type: 'container',
+ block,
+ size,
+ partitions: process_level(level + 1, start, size)
+ });
+ } else {
+ result.push({ type: 'block', block });
+ }
+ last_end = start + size;
+ }
+
+ append_free_space(last_end, total_end - last_end);
+
+ return result;
+ }
+
+ return process_level(0, 0, block.Size);
+}
+
+export function is_available_block(client, block, honor_ignore_hint) {
+ const block_ptable = client.blocks_ptable[block.path];
+ const block_part = client.blocks_part[block.path];
+ const block_pvol = client.blocks_pvol[block.path];
+
+ function has_fs_label() {
+ if (!block.IdUsage)
+ return false;
+ // Devices with a LVM2_member label need to actually be
+ // associated with a volume group.
+ if (block.IdType == 'LVM2_member' && (!block_pvol || !client.vgroups[block_pvol.VolumeGroup]))
+ return false;
+ return true;
+ }
+
+ function is_mpath_member() {
+ if (!client.drives[block.Drive])
+ return false;
+ if (!client.drives_block[block.Drive]) {
+ // Broken multipath drive
+ return true;
+ }
+ const members = client.drives_multipath_blocks[block.Drive];
+ for (let i = 0; i < members.length; i++) {
+ if (members[i] == block)
+ return true;
+ }
+ return false;
+ }
+
+ function is_vdo_backing_dev() {
+ return !!client.legacy_vdo_overlay.find_by_backing_block(block);
+ }
+
+ function is_swap() {
+ return !!block && client.blocks_swap[block.path];
+ }
+
+ return (!(block.HintIgnore && honor_ignore_hint) &&
+ block.Size > 0 &&
+ !has_fs_label() &&
+ !is_mpath_member() &&
+ !is_vdo_backing_dev() &&
+ !is_swap() &&
+ !block_ptable &&
+ !(block_part && block_part.IsContainer) &&
+ !should_ignore(client, block.path));
+}
+
+export function get_available_spaces(client) {
+ function make(path) {
+ const block = client.blocks[path];
+ const parts = get_block_link_parts(client, path);
+ const text = cockpit.format(parts.format, parts.link);
+ return { type: 'block', block, size: block.Size, desc: text };
+ }
+
+ const spaces = Object.keys(client.blocks).filter(p => is_available_block(client, client.blocks[p], true))
+ .sort(make_block_path_cmp(client))
+ .map(make);
+
+ function add_free_spaces(block) {
+ const parts = get_partitions(client, block);
+ let i, p, link_parts, text;
+ for (i in parts) {
+ p = parts[i];
+ if (p.type == 'free') {
+ link_parts = get_block_link_parts(client, block.path);
+ text = cockpit.format(link_parts.format, link_parts.link);
+ spaces.push({
+ type: 'free',
+ block,
+ start: p.start,
+ size: p.size,
+ desc: cockpit.format(_("unpartitioned space on $0"), text)
+ });
+ }
+ }
+ }
+
+ for (const p in client.blocks_ptable)
+ add_free_spaces(client.blocks[p]);
+
+ return spaces;
+}
+
+export function prepare_available_spaces(client, spcs) {
+ function prepare(spc) {
+ if (spc.type == 'block')
+ return Promise.resolve(spc.block.path);
+ else if (spc.type == 'free') {
+ const block_ptable = client.blocks_ptable[spc.block.path];
+ return block_ptable.CreatePartition(spc.start, spc.size, "", "", { });
+ }
+ }
+
+ return Promise.all(spcs.map(prepare));
+}
+
+export function is_snap(client, block) {
+ const block_fsys = client.blocks_fsys[block.path];
+ return block_fsys && block_fsys.MountPoints.map(decode_filename).some(mp => mp.indexOf("/snap/") == 0 || mp.indexOf("/var/lib/snapd/snap/") == 0);
+}
+
+export function get_other_devices(client) {
+ return Object.keys(client.blocks).filter(path => {
+ const block = client.blocks[path];
+ const block_part = client.blocks_part[path];
+ const block_lvm2 = client.blocks_lvm2[path];
+
+ return ((!block_part || block_part.Table == "/") &&
+ block.Drive == "/" &&
+ block.CryptoBackingDevice == "/" &&
+ block.MDRaid == "/" &&
+ (!block_lvm2 || block_lvm2.LogicalVolume == "/") &&
+ !block.HintIgnore &&
+ block.Size > 0 &&
+ !client.legacy_vdo_overlay.find_by_block(block) &&
+ !client.blocks_stratis_fsys[block.path] &&
+ !is_snap(client, block) &&
+ !should_ignore(client, block.path));
+ });
+}
+
+/* Comparison function for sorting lists of block devices.
+
+ We sort by major:minor numbers to get the expected order when
+ there are more than 10 devices of a kind. For example, if you
+ have 20 loopback devices named loop0 to loop19, sorting them
+ alphabetically would put them in the wrong order
+
+ loop0, loop1, loop10, loop11, ..., loop2, ...
+
+ Sorting by major:minor is an easy way to do the right thing.
+*/
+
+export function block_cmp(a, b) {
+ return a.DeviceNumber - b.DeviceNumber;
+}
+
+export function make_block_path_cmp(client) {
+ return function(path_a, path_b) {
+ return block_cmp(client.blocks[path_a], client.blocks[path_b]);
+ };
+}
+
+let multipathd_service;
+
+export function get_multipathd_service () {
+ if (!multipathd_service)
+ multipathd_service = service.proxy("multipathd");
+ return multipathd_service;
+}
+
+function get_parent(client, path) {
+ if (client.blocks_part[path] && client.blocks[client.blocks_part[path].Table])
+ return client.blocks_part[path].Table;
+ if (client.blocks[path] && client.blocks[client.blocks[path].CryptoBackingDevice])
+ return client.blocks[path].CryptoBackingDevice;
+ if (client.blocks[path] && client.drives[client.blocks[path].Drive])
+ return client.blocks[path].Drive;
+ if (client.blocks[path] && client.mdraids[client.blocks[path].MDRaid])
+ return client.blocks[path].MDRaid;
+ if (client.blocks_lvm2[path] && client.lvols[client.blocks_lvm2[path].LogicalVolume])
+ return client.blocks_lvm2[path].LogicalVolume;
+ if (client.lvols[path] && client.vgroups[client.lvols[path].VolumeGroup])
+ return client.lvols[path].VolumeGroup;
+ if (client.blocks_stratis_fsys[path])
+ return client.blocks_stratis_fsys[path].Pool;
+ if (client.vgroups[path])
+ return path;
+ if (client.stratis_pools[path])
+ return path;
+}
+
+function get_direct_parent_blocks(client, path) {
+ let parent = get_parent(client, path);
+ if (!parent)
+ return [];
+ if (client.blocks[parent])
+ return [parent];
+ if (client.mdraids[parent])
+ return client.mdraids_members[parent].map(function (m) { return m.path });
+ if (client.lvols[parent])
+ parent = client.lvols[parent].VolumeGroup;
+ if (client.vgroups[parent])
+ return client.vgroups_pvols[parent].map(function (pv) { return pv.path });
+ if (client.stratis_pools[parent])
+ return client.stratis_pool_blockdevs[parent].map(bd => client.slashdevs_block[bd.Devnode].path);
+ return [];
+}
+
+export function get_parent_blocks(client, path) {
+ const direct_parents = get_direct_parent_blocks(client, path);
+ const direct_and_indirect_parents = flatten(direct_parents.map(function (p) {
+ return get_parent_blocks(client, p);
+ }));
+ return [path].concat(direct_and_indirect_parents);
+}
+
+export function is_netdev(client, path) {
+ const block = client.blocks[path];
+ const drive = block && client.drives[block.Drive];
+ if (drive && drive.Vendor == "LIO-ORG")
+ return true;
+ if (block && block.Major == 43) // NBD
+ return true;
+ return false;
+}
+
+export function should_ignore(client, path) {
+ if (!client.in_anaconda_mode())
+ return false;
+
+ const parents = get_direct_parent_blocks(client, path);
+ if (parents.length == 0) {
+ const b = client.blocks[path];
+ return b && client.should_ignore_block(b);
+ } else {
+ return parents.some(p => should_ignore(client, p));
+ }
+}
+
+/* GET_CHILDREN gets the direct children of the storage object at
+ PATH, like the partitions of a partitioned block device, or the
+ volume group of a physical volume. By calling GET_CHILDREN
+ recursively, you can traverse the whole storage hierachy from
+ hardware drives at the bottom to filesystems at the top.
+
+ GET_CHILDREN_FOR_TEARDOWN is similar but doesn't consider things
+ like volume groups to be children of their physical volumes. This
+ is appropriate for teardown processing, where tearing down a
+ physical volume does not imply tearing down the whole volume groups
+ with everything that it contains.
+*/
+
+function get_children_for_teardown(client, path) {
+ const children = [];
+
+ if (client.blocks_cleartext[path]) {
+ children.push(client.blocks_cleartext[path].path);
+ }
+
+ if (client.blocks_ptable[path]) {
+ client.blocks_partitions[path].forEach(function (part) {
+ if (!part.IsContainer)
+ children.push(part.path);
+ });
+ }
+
+ if (client.blocks_part[path] && client.blocks_part[path].IsContainer) {
+ const ptable_path = client.blocks_part[path].Table;
+ client.blocks_partitions[ptable_path].forEach(function (part) {
+ if (part.IsContained)
+ children.push(part.path);
+ });
+ }
+
+ if (client.vgroups[path]) {
+ client.vgroups_lvols[path].forEach(function (lvol) {
+ if (client.lvols_block[lvol.path])
+ children.push(client.lvols_block[lvol.path].path);
+ });
+ }
+
+ if (client.lvols_pool_members[path]) {
+ for (const lvol of client.lvols_pool_members[path]) {
+ const block = client.lvols_block[lvol.path];
+ if (block)
+ children.push(block.path);
+ }
+ }
+
+ if (client.stratis_pools[path]) {
+ client.stratis_pool_filesystems[path].forEach(function (fsys) {
+ const block = client.slashdevs_block[fsys.Devnode];
+ if (block)
+ children.push(block.path);
+ });
+ }
+
+ return children;
+}
+
+export function get_children(client, path) {
+ const children = get_children_for_teardown(client, path);
+
+ if (client.blocks[path]) {
+ const mdraid = client.blocks[path].MDRaidMember;
+ if (mdraid != "/")
+ children.push(mdraid);
+ }
+
+ if (client.blocks_pvol[path]) {
+ const vgroup = client.blocks_pvol[path].VolumeGroup;
+ if (vgroup != "/")
+ children.push(vgroup);
+ }
+
+ if (client.blocks_stratis_blockdev[path]) {
+ const pool = client.blocks_stratis_blockdev[path].Pool;
+ if (pool != "/")
+ children.push(pool);
+ }
+
+ return children;
+}
+
+export function find_children_for_mount_point(client, mount_point, self) {
+ const children = {};
+
+ function is_self(b) {
+ return self && (b == self || client.blocks[b.CryptoBackingDevice] == self);
+ }
+
+ for (const p in client.blocks) {
+ const b = client.blocks[p];
+ const fs = client.blocks_fsys[p];
+
+ if (is_self(b))
+ continue;
+
+ if (fs) {
+ for (const mp of fs.MountPoints) {
+ const mpd = decode_filename(mp);
+ if (mpd.length > mount_point.length && mpd.indexOf(mount_point) == 0 && mpd[mount_point.length] == "/")
+ children[mpd] = b;
+ }
+ }
+ }
+
+ return children;
+}
+
+export function get_fstab_config_with_client(client, block, also_child_config, subvol) {
+ function match(c) {
+ if (c[0] != "fstab")
+ return false;
+ if (subvol !== undefined) {
+ if (!c[1].opts)
+ return false;
+
+ const opts = decode_filename(c[1].opts.v).split(",");
+ if (opts.indexOf("subvolid=" + subvol.id) >= 0)
+ return true;
+ if (opts.indexOf("subvol=" + subvol.pathname) >= 0)
+ return true;
+
+ // btrfs mounted without subvol argument.
+ const btrfs_volume = client.blocks_fsys_btrfs[block.path];
+ const default_subvolid = client.uuids_btrfs_default_subvol[btrfs_volume.data.uuid];
+ if (default_subvolid === subvol.id && !opts.find(o => o.indexOf("subvol=") >= 0 || o.indexOf("subvolid=") >= 0))
+ return true;
+
+ return false;
+ }
+ return true;
+ }
+
+ let config = block.Configuration.find(match);
+
+ if (!config && also_child_config && client.blocks_crypto[block.path])
+ config = client.blocks_crypto[block.path]?.ChildConfiguration.find(c => c[0] == "fstab");
+
+ if (config && decode_filename(config[1].type.v) != "swap") {
+ const mnt_opts = get_block_mntopts(config[1]).split(",");
+ let dir = decode_filename(config[1].dir.v);
+ let opts = mnt_opts
+ .filter(function (s) { return s.indexOf("x-parent") !== 0 })
+ .join(",");
+ const parents = mnt_opts
+ .filter(function (s) { return s.indexOf("x-parent") === 0 })
+ .join(",");
+ if (dir[0] != "/")
+ dir = "/" + dir;
+ if (opts == "defaults")
+ opts = "";
+ return [config, dir, opts, parents];
+ } else
+ return [];
+}
+
+export function get_active_usage(client, path, top_action, child_action, is_temporary, subvol) {
+ function get_usage(usage, path, level) {
+ const block = client.blocks[path];
+ const fsys = client.blocks_fsys[path];
+ const swap = client.blocks_swap[path];
+ const mdraid = block && client.mdraids[block.MDRaidMember];
+ const pvol = client.blocks_pvol[path];
+ const vgroup = pvol && client.vgroups[pvol.VolumeGroup];
+ const vdo = block && client.legacy_vdo_overlay.find_by_backing_block(block);
+ const stratis_blockdev = block && client.blocks_stratis_blockdev[path];
+ const stratis_pool = stratis_blockdev && client.stratis_pools[stratis_blockdev.Pool];
+ const btrfs_volume = client.blocks_fsys_btrfs[path];
+
+ get_children_for_teardown(client, path).map(p => get_usage(usage, p, level + 1));
+
+ function get_actions(teardown_action) {
+ const actions = [];
+ if (teardown_action)
+ actions.push(teardown_action);
+ const global_action = (level == 0 || (block && client.blocks[block.CryptoBackingDevice] && level == 1)) ? top_action : child_action || top_action;
+ if (global_action)
+ actions.push(global_action);
+ return actions;
+ }
+
+ function enter_unmount(block, location, is_top) {
+ const [, mount_point] = get_fstab_config_with_client(client, block);
+ const has_fstab_entry = is_temporary && location == mount_point;
+
+ for (const u of usage) {
+ if (u.usage == 'mounted' && u.location == location) {
+ if (is_top) {
+ u.actions = get_actions(_("unmount"));
+ u.set_noauto = false;
+ }
+ return;
+ }
+ }
+ usage.push({
+ level,
+ block,
+ usage: 'mounted',
+ location,
+ has_fstab_entry,
+ set_noauto: !is_top && !is_temporary,
+ actions: (is_top ? get_actions(_("unmount")) : [_("unmount")]).concat(has_fstab_entry ? [_("mount")] : []),
+ blocking: client.strip_mount_point_prefix(location) === false,
+ });
+ }
+
+ // HACK: get_active_usage is used for mounting and formatting so we use the absence of the subvol argument
+ // to figure out that we want to format this device.
+ // This is separate from the if's below as we also always have to umount the filesystem.
+
+ // We allow a btrfs volume with one device to be formatted as this
+ // looks the most like a normal filesystem use case.
+ if (btrfs_volume && btrfs_volume.data.num_devices !== 1 && !subvol) {
+ usage.push({
+ level,
+ usage: 'btrfs-device',
+ block,
+ btrfs_volume,
+ location: btrfs_volume.data.label || btrfs_volume.data.uuid,
+ actions: get_actions(_("remove from btrfs volume")),
+ blocking: true,
+ });
+ }
+
+ const mount_points = get_mount_points(client, fsys, subvol);
+ if (mount_points.length > 0) {
+ mount_points.forEach(mp => {
+ const children = find_children_for_mount_point(client, mp, null);
+ for (const c in children)
+ enter_unmount(children[c], c, false);
+ enter_unmount(block, mp, true);
+ });
+ } else if (swap) {
+ if (swap.Active) {
+ usage.push({
+ level,
+ usage: 'swap',
+ block,
+ actions: get_actions(_("stop")),
+ });
+ }
+ } else if (mdraid) {
+ const active_state = mdraid.ActiveDevices.find(as => as[0] == block.path);
+ usage.push({
+ level,
+ usage: 'mdraid-member',
+ block,
+ mdraid,
+ location: mdraid_name(mdraid),
+ actions: get_actions(_("remove from MDRAID")),
+ blocking: !(active_state && active_state[1] < 0)
+ });
+ } else if (vgroup) {
+ usage.push({
+ level,
+ usage: 'pvol',
+ block,
+ vgroup,
+ pvol,
+ location: vgroup.Name,
+ actions: get_actions(_("remove from LVM2")),
+ blocking: pvol.FreeSize != pvol.Size
+ });
+ } else if (vdo) {
+ usage.push({
+ level,
+ usage: 'vdo-backing',
+ block,
+ vdo,
+ location: vdo.name,
+ blocking: true
+ });
+ } else if (stratis_pool) {
+ usage.push({
+ level,
+ usage: 'stratis-pool-member',
+ block,
+ stratis_pool,
+ location: stratis_pool.Name,
+ blocking: true
+ });
+ } else if (block && !client.blocks_cleartext[block.path]) {
+ usage.push({
+ level,
+ usage: 'none',
+ block,
+ actions: get_actions(null),
+ blocking: false
+ });
+ }
+
+ return usage;
+ }
+
+ let usage = [];
+ get_usage(usage, path, 0);
+
+ if (usage.length == 1 && usage[0].level == 0 && usage[0].usage == "none")
+ usage = [];
+
+ usage.Blocking = usage.some(u => u.blocking);
+ usage.Teardown = usage.some(u => !u.blocking);
+
+ return usage;
+}
+
+async function set_fsys_noauto(client, block, mount_point) {
+ for (const conf of block.Configuration) {
+ if (conf[0] == "fstab" &&
+ decode_filename(conf[1].dir.v) == mount_point) {
+ const options = parse_options(get_block_mntopts(conf[1]));
+ if (options.indexOf("noauto") >= 0)
+ continue;
+ options.push("noauto");
+ const new_conf = [
+ "fstab",
+ Object.assign({ }, conf[1],
+ {
+ opts: {
+ t: 'ay',
+ v: encode_filename(unparse_options(options))
+ }
+ })
+ ];
+ await block.UpdateConfigurationItem(conf, new_conf, { });
+ }
+ }
+
+ const crypto_backing = client.blocks[block.CryptoBackingDevice];
+ if (crypto_backing) {
+ const crypto_backing_crypto = client.blocks_crypto[crypto_backing.path];
+ await set_crypto_auto_option(crypto_backing, false);
+ if (crypto_backing_crypto)
+ await crypto_backing_crypto.Lock({});
+ }
+}
+
+export function teardown_active_usage(client, usage) {
+ // The code below is complicated by the fact that the last
+ // physical volume of a volume group can not be removed
+ // directly (even if it is completely empty). We want to
+ // remove the whole volume group instead in this case.
+ //
+ // However, we might be removing the last two (or more)
+ // physical volumes here, and it is easiest to catch this
+ // condition upfront by reshuffling the data structures.
+
+ async function unmount(mounteds) {
+ for (const m of mounteds) {
+ await client.unmount_at(m.location, m.users);
+ if (m.set_noauto)
+ await set_fsys_noauto(client, m.block, m.location);
+ }
+ }
+
+ async function stop_swap(swaps) {
+ for (const s of swaps) {
+ await client.blocks_swap[s.block.path].Stop({});
+ }
+ }
+
+ function mdraid_remove(members) {
+ return Promise.all(members.map(m => m.mdraid.RemoveDevice(m.block.path, { wipe: { t: 'b', v: true } })));
+ }
+
+ function pvol_remove(pvols) {
+ const by_vgroup = { };
+ pvols.forEach(function (p) {
+ if (!by_vgroup[p.vgroup.path])
+ by_vgroup[p.vgroup.path] = [];
+ by_vgroup[p.vgroup.path].push(p.block);
+ });
+
+ function handle_vg(p) {
+ const vg = client.vgroups[p];
+ const pvs = by_vgroup[p];
+ // If we would remove all physical volumes of a volume
+ // group, remove the whole volume group instead.
+ if (pvs.length == client.vgroups_pvols[p].length) {
+ return vg.Delete(true, { 'tear-down': { t: 'b', v: true } }).then(reload_systemd);
+ } else {
+ return Promise.all(pvs.map(pv => vg.RemoveDevice(pv.path, true, {})));
+ }
+ }
+
+ return Promise.all(Object.keys(by_vgroup).map(handle_vg));
+ }
+
+ return Promise.all(Array.prototype.concat(
+ unmount(usage.filter(function(use) { return use.usage == "mounted" })),
+ stop_swap(usage.filter(function(use) { return use.usage == "swap" })),
+ mdraid_remove(usage.filter(function(use) { return use.usage == "mdraid-member" })),
+ pvol_remove(usage.filter(function(use) { return use.usage == "pvol" }))
+ ));
+}
+
+export async function undo_temporary_teardown(client, usage) {
+ for (let i = usage.length - 1; i >= 0; i--) {
+ const u = usage[i];
+ if (u.usage == "mounted" && u.has_fstab_entry) {
+ await client.mount_at(u.block, u.location);
+ }
+ }
+}
+
+// TODO - generalize this to arbitrary number of arguments (when needed)
+export function fmt_to_array(fmt, arg) {
+ const index = fmt.indexOf("$0");
+ if (index >= 0)
+ return [fmt.slice(0, index), arg, fmt.slice(index + 2)];
+ else
+ return [fmt];
+}
+
+export function reload_systemd() {
+ return cockpit.spawn(["systemctl", "daemon-reload"], { superuser: "require", err: "message" });
+}
+
+export function is_mounted_synch(block) {
+ return (cockpit.spawn(["findmnt", "-n", "-o", "TARGET", "-S", decode_filename(block.Device)],
+ { superuser: true, err: "message" })
+ .then(data => data.trim())
+ .catch(() => false));
+}
+
+export function for_each_async(arr, func) {
+ return arr.reduce((promise, elt) => promise.then(() => func(elt)), Promise.resolve());
+}
+
+/*
+ * Get mount points for a given org.freedesktop.UDisks2.Filesystem object
+ *
+ * This generalises getting the given MountPoints of a Filesystem for btrfs and
+ * other filesystems, for btrfs we want to know if a subvolume is mounted
+ * anywhere. UDisks is currently not aware of subvolumes and it's Filesystem
+ * object gives us the MountPoints for all subvolumes while we want it per
+ * subvolume.
+ *
+ * @param {Object} block_fsys
+ * @param {Object|null} subvol
+ * @returns {Array} an array of MountPoints
+ */
+export function get_mount_points(client, block_fsys, subvol) {
+ let mounted_at = [];
+
+ if (subvol && block_fsys) {
+ const btrfs_volume = client.blocks_fsys_btrfs[block_fsys.path];
+ const volume_mounts = client.btrfs_mounts[btrfs_volume.data.uuid];
+ if (volume_mounts)
+ mounted_at = subvol.id in volume_mounts ? volume_mounts[subvol.id].mount_points : [];
+ } else {
+ mounted_at = block_fsys ? block_fsys.MountPoints.map(decode_filename) : [];
+ }
+
+ return mounted_at;
+}
diff --git a/pkg/systemd/README-realmd.md b/pkg/systemd/README-realmd.md
new file mode 100644
index 0000000..cb7018e
--- /dev/null
+++ b/pkg/systemd/README-realmd.md
@@ -0,0 +1,111 @@
+Developing the realmd component
+-------------------------------
+
+This adds functionality to Cockpit to join an AD or IPA domain.
+
+Some features of Cockpit require a domain to test. Cockpit should work
+with either Active Directory or IPA.
+
+### Running a test domain
+
+To contribute to this component, run a test domain which ends
+up being rather easy. Install the stuff in [test/README.md](../../test/README.md)
+near the top. And then do the following:
+
+ $ bots/vm-run --network services
+
+In that VM, start `/root/run-freeipa` to start an IPA domain controller, or
+`/root/run-samba-domain` for a Samba AD domain controller. Now in
+another terminal do the following:
+
+ $ sudo /bin/sh -c "echo -e 'domain cockpit.lan\nsearch cockpit.lan\nnameserver 10.111.112.100\n' > /etc/resolv.conf"
+
+Make sure this works:
+
+ $ realm discover cockpit.lan
+
+And now you're ready to use the feature. For IPA there's an account called
+"admin" with the password "foobarfoo", for Samba AD it is user "Administrator"
+with password "foobarFoo123".
+
+To test your DNS, the following should succeed without any error messages
+on your server with cockpit:
+
+ $ host cockpit.lan
+
+Now verify that you can authenticate against the IPA server. See password
+above.
+
+ $ kinit admin@COCKPIT.LAN
+ Password for admin@COCKPIT.LAN:
+
+## Running a Microsoft AD server in AWS
+
+If you want to test against Microsoft Active Directory instead of Samba or FreeIPA, the
+simplest way is to start a temporary
+[managed AD in AWS](https://docs.aws.amazon.com/directoryservice/latest/admin-guide/directory_microsoft_ad.html)
+(or another cloud provider). Just follow the few
+[setup steps](https://docs.aws.amazon.com/directoryservice/latest/admin-guide/ms_ad_getting_started_create_directory.html):
+Select the smallest edition, specify a directory DNS name (e. g. `ad.cockpit.lan`) and a password for the `Admin` user,
+and about 20 minutes later the domain server should be set up. The details page
+of the created domain shows the DNS server's IP; put that into `/etc/resolv.conf`.
+
+From then on, joining that domain with `realm` or Cockpit works in the same way.
+
+## Setting up Single Sign on
+
+Cockpit can perform single sign on authentication via Kerberos. To test and
+work on this feature, you must have a domain on your network. See section
+above if you do not.
+
+Use the following guide to configure things, with more troubleshooting advice
+below:
+
+https://cockpit-project.org/guide/latest/sso.html
+
+**BUG:** The host name of the computer Cockpit is running on should end with
+the domain name. If it does not, then rename the computer Cockpit is running on:
+[realmd bug](https://bugzilla.redhat.com/show_bug.cgi?id=1144343)
+
+ $ sudo hostnamectl set-hostname my-server.domain.com
+
+**BUG:** If your domain is an IPA domain, then you need to explicitly add a service
+before Cockpit can be used with Single Sign on. The following must be done on
+the computer running Cockpit.
+[realmd bug](https://bugzilla.redhat.com/show_bug.cgi?id=1144292)
+
+ $ sudo -s
+ # kinit admin@COCKPIT.LAN
+ # ipa service-add --ok-as-delegate=true --ok-to-auth-as-delegate=true --force HTTP/my-server.cockpit.lan@COCKPIT.LAN
+ # ipa-getkeytab -q -s services.cockpit.lan -p HTTP/my-server.cockpit.lan -k /etc/krb5.keytab
+
+Now when you go to your cockpit instance you should be able to log in without
+authenticating. Make sure to use the full hostname that you set above, the one
+that includes the domain name.
+
+If you want to use Cockpit to connect to a second server make sure that second
+server is joined to a domain, and that you can ssh into it using GSSAPI authentication
+with the domain user:
+
+ $ ssh -o PreferredAuthentications=gssapi-with-mic admin@my-server2.domain.com
+
+If you thought that was nasty and tiresome, it's because it is at present :S
+
+## Using delegated credentials
+
+Cockpit can delegate forwardable credentials. Make sure to specify you want them
+during kinit:
+
+ $ kinit -f admin@COCKPIT.LAN
+ $ klist -f
+ Default principal: admin@COCKPIT.LAN
+ ...
+ Flags: FIA
+
+Use the IPA GUI to setup "Trusted for delegation" for the host and service that
+Cockpit is running on. Make sure to tell the browser to delegate credentials
+as seen in the guide:
+
+https://cockpit-project.org/guide/latest/sso.html
+
+Ze goggles, zey do nothing!
diff --git a/pkg/systemd/abrtLog.jsx b/pkg/systemd/abrtLog.jsx
new file mode 100644
index 0000000..d582484
--- /dev/null
+++ b/pkg/systemd/abrtLog.jsx
@@ -0,0 +1,330 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import React from 'react';
+import { Accordion, AccordionContent, AccordionItem, AccordionToggle } from "@patternfly/react-core/dist/esm/components/Accordion/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Tab, Tabs } from "@patternfly/react-core/dist/esm/components/Tabs/index.js";
+import { GalleryItem } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+
+import { ListingTable } from 'cockpit-components-table.jsx';
+import { ReportingTable } from "./reporting.jsx";
+import { journal } from "journal";
+
+const _ = cockpit.gettext;
+
+const Table = ({ lines, delimiter }) => {
+ return (
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ { lines.map((line, idx) => {
+ const group = typeof line === 'string' ? line.split(delimiter) : line;
+ const term = group.shift();
+
+ return (
+ <DescriptionListGroup key={term + idx}>
+ <DescriptionListTerm>{typeof term === 'string' ? term.trim() : term}</DescriptionListTerm>
+ <DescriptionListDescription>{group.length > 1 ? group.join(" ") : group[0]}</DescriptionListDescription>
+ </DescriptionListGroup>
+ );
+ })}
+ </DescriptionList>
+ );
+};
+
+function get_all_keys_from_frames(thread) {
+ let all_keys = [];
+ thread.forEach(t => { all_keys = all_keys.concat(Object.keys(t)) });
+ const unique = [...new Set(all_keys)];
+
+ const desired_ordered_of_keys = ['function_name', 'file_name', 'address', 'build_id', 'build_id_offset'];
+ const all_ordered_keys = [];
+
+ desired_ordered_of_keys.forEach(key => {
+ if (unique.indexOf(key) !== -1)
+ all_ordered_keys.push(key);
+ });
+ unique.forEach(key => {
+ if (desired_ordered_of_keys.indexOf(key) === -1)
+ all_ordered_keys.push(key);
+ });
+
+ return all_ordered_keys;
+}
+
+const CrashTable = ({ thread }) => {
+ const all_keys = get_all_keys_from_frames(thread);
+
+ return (
+ <ListingTable
+ gridBreakPoint='grid-lg'
+ variant="compact"
+ columns={[
+ { title: _("Frame number") },
+ ...all_keys.map((key, i) => key.replace(/_/g, ' ')),
+ ]}
+ rows={thread.map((frame, i) => { return { columns: [i, ...all_keys.map(key => frame[key] || "")] } })} />
+ );
+};
+
+function render_table_eq(val) {
+ const rows = val.split("\n");
+ return <Table lines={rows} delimiter="=" />;
+}
+
+function render_table_co(val) {
+ const rows = val.split("\n");
+ return <Table lines={rows} delimiter=":" />;
+}
+
+function render_dso_list(val) {
+ const rows = val.split("\n");
+
+ return <ListingTable
+ gridBreakPoint='grid-md'
+ className="table-hide-labels"
+ variant="compact"
+ showHeader={false}
+ columns={new Array(rows[0].split(" ").length)}
+ rows={rows.map((row, i) => { return { columns: row.split(" ") } })}
+ />;
+}
+
+function render_m(val) {
+ const rows = val.replace(/ +/g, ':').split("\n");
+
+ return <ListingTable
+ gridBreakPoint='grid-md'
+ className="table-hide-labels"
+ variant="compact"
+ showHeader={false}
+ columns={new Array(rows[0].split(" ").length)}
+ rows={rows.map((row, i) => { return { columns: row.split(" ") } })}
+ />;
+}
+
+function render_cgroups(val) {
+ const rows = val.split("\n");
+ const columns = [_("Hierarchy ID"), _("Controller"), _("Path")];
+
+ return <ListingTable
+ gridBreakPoint='grid-lg'
+ variant="compact"
+ showHeader={false}
+ columns={columns}
+ rows={rows.map((row, i) => { return { columns: row.split(":") } })}
+ />;
+}
+
+function render_limits(val) {
+ const rows = val.split('\n').map(row => row.replace(/ +/g, ':'));
+ const columns = rows.shift();
+
+ return <ListingTable aria-label={_("Limits")}
+ gridBreakPoint='grid-lg'
+ variant="compact"
+ columns={columns.split(":")}
+ rows={rows.map((row, i) => { return { columns: row.split(":") } })}
+ />;
+}
+
+function render_open_fds(val) {
+ // File descriptor is described by 5 lines, in each, except the first one,
+ // we want to create an empty first column
+ const rows = [];
+ let term;
+ let value;
+ val.split('\n').forEach((line, i) => {
+ if (i % 5 == 0) {
+ if (term !== undefined && value !== undefined)
+ rows.push([term, <Stack key={term}>{value.map(itm => itm ? <StackItem key={itm}>{itm}</StackItem> : null)}</Stack>]);
+
+ term = line.split(":")[0];
+ value = [line.split(":")[1]];
+ } else {
+ value.push(line);
+ }
+ });
+
+ if (term !== undefined && value !== undefined)
+ rows.push([term, <Stack key={term}>{value.map(itm => <StackItem key={itm}>{itm}</StackItem>)}</Stack>]);
+
+ return <Table lines={rows} />;
+}
+
+function render_multiline(val) {
+ return <span className="multiline">{val}</span>;
+}
+
+// ABRT details to ignore
+const ignore_fields = ["journald_cursor", "cpuinfo"];
+
+// A map of ABRT's problems items and it's callback for rendering
+const problem_render_callbacks = {
+ os_info: render_table_eq,
+ environ: render_table_eq,
+ cgroup: render_cgroups,
+ namespaces: render_table_co,
+ maps: render_m,
+ mountinfo: render_m,
+ limits: render_limits,
+ dso_list: render_dso_list,
+ proc_pid_status: render_table_co,
+ open_fds: render_open_fds,
+ var_log_messages: render_multiline,
+ 'not-reportable': render_multiline,
+ exploitable: render_multiline,
+ suspend_stats: render_table_co,
+ dmesg: render_multiline,
+ container_rootfs: render_multiline,
+ docker_inspect: render_multiline
+};
+
+export class AbrtLogDetails extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ active_acc: "",
+ active_tab: "general",
+ details: {},
+ allThreads: false
+ };
+
+ this.handleSelect = this.handleSelect.bind(this);
+ this.handleToggle = this.handleToggle.bind(this);
+ this.onDelete = this.onDelete.bind(this);
+ this.renderBacktrace = this.renderBacktrace.bind(this);
+ }
+
+ async componentDidMount() {
+ const details = await this.props.service.GetProblemData(this.props.problem.path);
+ this.setState({ details });
+ }
+
+ handleSelect(event, active_tab) {
+ this.setState({ active_tab });
+ }
+
+ handleToggle(id) {
+ if (id === this.state.active_acc)
+ this.setState({ active_acc: '' });
+ else
+ this.setState({ active_acc: id });
+ }
+
+ onDelete(ev) {
+ this.props.service.DeleteProblems([this.props.problem.path]).then(() => this.props.reloadProblems(ev));
+ }
+
+ renderBacktrace(val) {
+ const content = [];
+ const items = JSON.parse(val) || {};
+
+ const threads = items.stacktrace;
+ let crash_thread = null;
+ const other_threads = [];
+ Object.values(threads).forEach(thread => {
+ if (thread.crash_thread && thread.frames)
+ crash_thread = thread.frames;
+ else if (thread.frames)
+ other_threads.push(thread.frames);
+ });
+
+ delete items.stacktrace;
+ const rows = Object.keys(items).map(k => k + ":" + items[k]);
+ content.push(<Table key="info" lines={rows} delimiter=":" />);
+ content.push(<CrashTable key="crash" thread={crash_thread} />);
+
+ if (other_threads.length !== 0) {
+ if (this.state.allThreads) {
+ other_threads.forEach((thread, i) => {
+ content.push(<CrashTable key={i} thread={thread} />);
+ });
+ } else {
+ content.push(<Button key="all-threads" variant="link" onClick={() => this.setState({ allThreads: true })}>{_("Show all threads")}</Button>);
+ }
+ }
+ return content;
+ }
+
+ render() {
+ const general = Object.keys(this.props.entry).filter(k => k !== 'MESSAGE' && k.indexOf('PROBLEM_') !== 0);
+ general.sort();
+
+ const detail_keys = Object.keys(problem_render_callbacks);
+ detail_keys.push("core_backtrace");
+ const info = Object.keys(this.state.details).filter(k => detail_keys.indexOf(k) < 0 && ignore_fields.indexOf(k) < 0);
+ info.sort();
+
+ const details = detail_keys.filter(d => this.state.details[d]);
+ details.sort();
+
+ return (
+ <>
+ <h1 id="entry-heading">{this.props.entry.PROBLEM_BINARY}</h1>
+ <GalleryItem id="abrt-reporting">
+ <ReportingTable problem={this.props.problem} />
+ </GalleryItem>
+ <GalleryItem id="abrt-details">
+ <Card>
+ <CardHeader actions={{ actions: <><Button variant="danger" onClick={this.onDelete}>{_("Delete")}</Button></> }}>
+
+ <CardTitle component="h2">{_("Extended information")}</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <Tabs activeKey={this.state.active_tab} onSelect={this.handleSelect}>
+ <Tab eventKey="general" title={_("General")}>
+ <Table lines={general.map(key => [key, journal.printable(this.props.entry[key], key)])} />
+ </Tab>
+ <Tab eventKey="info" title={_("Problem info")}>
+ <Table lines={info.map(key => [key, journal.printable(this.state.details[key][2], key)])} />
+ </Tab>
+ <Tab eventKey="details" title={_("Problem details")}>
+ <Accordion asDefinitionList>
+ {details.map(d =>
+ <AccordionItem key={d}>
+ <AccordionToggle
+ onClick={() => this.handleToggle(d)}
+ isExpanded={this.state.active_acc === d}
+ id={d}
+ >
+ {d}
+ </AccordionToggle>
+ <AccordionContent isHidden={this.state.active_acc !== d}>
+ { d === "core_backtrace"
+ ? this.renderBacktrace(this.state.details[d][2])
+ : problem_render_callbacks[d](this.state.details[d][2])
+ }
+ </AccordionContent>
+ </AccordionItem>
+ )}
+ </Accordion>
+ </Tab>
+ </Tabs>
+ </CardBody>
+ </Card>
+ </GalleryItem>
+ </>
+ );
+ }
+}
diff --git a/pkg/systemd/busnames.js b/pkg/systemd/busnames.js
new file mode 100644
index 0000000..4f68799
--- /dev/null
+++ b/pkg/systemd/busnames.js
@@ -0,0 +1,11 @@
+const s_bus = {
+ BUS_NAME: "org.freedesktop.systemd1",
+ O_MANAGER: "/org/freedesktop/systemd1",
+ I_MANAGER: "org.freedesktop.systemd1.Manager",
+ I_PROPS: "org.freedesktop.DBus.Properties",
+ I_UNIT: "org.freedesktop.systemd1.Unit",
+ I_TIMER: "org.freedesktop.systemd1.Timer",
+ I_SOCKET: "org.freedesktop.systemd1.Socket",
+};
+
+export default s_bus;
diff --git a/pkg/systemd/hw-detect.js b/pkg/systemd/hw-detect.js
new file mode 100644
index 0000000..925116d
--- /dev/null
+++ b/pkg/systemd/hw-detect.js
@@ -0,0 +1,172 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import cockpit from "cockpit";
+
+import * as machine_info from "machine-info.js";
+const _ = cockpit.gettext;
+
+// map an info.system key to a /sys/class/dmi/id/* attribute name
+const InfoDMIKey = {
+ version: "product_version",
+ name: "product_name",
+ alt_version: "board_vendor",
+ alt_name: "board_name",
+ type: "chassis_type_str",
+ bios_vendor: "bios_vendor",
+ bios_version: "bios_version",
+ bios_date: "bios_date",
+};
+
+const getDMI = info => machine_info.dmi_info()
+ .then(fields => {
+ Object.keys(InfoDMIKey).forEach(key => {
+ info.system[key] = fields[InfoDMIKey[key]];
+ });
+ return true;
+ });
+
+const getDeviceTree = info => machine_info.devicetree_info()
+ .then(fields => {
+ // if getDMI sets a field first, let that win
+ if (fields.model && !info.system.name)
+ info.system.name = fields.model;
+ return true;
+ });
+
+// Add info.pci [{slot, cls, vendor, model}] list
+function findPCI(udevdb, info) {
+ for (const syspath in udevdb) {
+ const props = udevdb[syspath];
+ if (props.SUBSYSTEM === "pci")
+ info.pci.push({
+ slot: props.PCI_SLOT_NAME || syspath.split("/").pop() || "",
+ cls: props.ID_PCI_CLASS_FROM_DATABASE || props.PCI_CLASS.toString() || "",
+ vendor: props.ID_VENDOR_FROM_DATABASE || "",
+ model: props.ID_MODEL_FROM_DATABASE || props.PCI_ID || ""
+ });
+ }
+}
+
+function findMemoryDevices(udevdb, info) {
+ const memoryArray = [];
+ const dmipath = '/devices/virtual/dmi/id';
+ if (!(dmipath in udevdb))
+ return;
+
+ const props = udevdb[dmipath];
+ // Systemd now exposes memory information in udev, introduced in systemd => 248
+ // https://github.com/systemd/systemd/blob/main/NEWS#L1713
+ if (!('MEMORY_ARRAY_NUM_DEVICES' in props)) {
+ return;
+ }
+
+ const devices = parseInt(props.MEMORY_ARRAY_NUM_DEVICES, 10);
+ for (let slot = 0; slot < devices; slot++) {
+ let memorySize = parseInt(props[`MEMORY_DEVICE_${slot}_SIZE`], 10);
+ if (memorySize) {
+ memorySize = cockpit.format_bytes(memorySize, 1024);
+ } else {
+ memorySize = _("Unknown");
+ }
+
+ let memoryRank = props[`MEMORY_DEVICE_${slot}_RANK`];
+ if (memoryRank == 1) {
+ memoryRank = _("Single rank");
+ } else if (memoryRank == 2) {
+ memoryRank = _("Dual rank");
+ } else {
+ memoryRank = _("Unknown");
+ }
+
+ let speed = props[`MEMORY_DEVICE_${slot}_SPEED_MTS`];
+ if (speed) {
+ speed += ' MT/s';
+ } else {
+ speed = _("Unknown");
+ }
+
+ let locator = _("Unknown");
+ if (props[`MEMORY_DEVICE_${slot}_BANK_LOCATOR`] && props[`MEMORY_DEVICE_${slot}_LOCATOR`]) {
+ locator = props[`MEMORY_DEVICE_${slot}_BANK_LOCATOR`] + ': ' + props[`MEMORY_DEVICE_${slot}_LOCATOR`];
+ }
+
+ memoryArray.push({
+ locator,
+ technology: props[`MEMORY_DEVICE_${slot}_MEMORY_TECHNOLOGY`] || _("Unknown"),
+ type: props[`MEMORY_DEVICE_${slot}_TYPE`] || _("Unknown"),
+ size: memorySize,
+ state: props[`MEMORY_DEVICE_${slot}_TOTAL_WIDTH`] ? _("Present") : _("Absent"),
+ rank: memoryRank,
+ speed,
+ });
+ }
+
+ info.memory = memoryArray;
+}
+
+export default function detect() {
+ const info = { system: {}, pci: [], memory: [] };
+ const tasks = [];
+
+ tasks.push(machine_info.cpu_ram_info()
+ .then(result => {
+ info.system.cpu_model = result.cpu_model || _("unknown");
+ info.system.nproc = result.cpus;
+ return true;
+ }));
+
+ tasks.push(getDMI(info)
+ .catch(error => {
+ console.warn("Failed to get DMI information:", error.toString());
+ return true;
+ }));
+
+ tasks.push(getDeviceTree(info)
+ .catch(error => {
+ console.debug("Failed to get DeviceTree information:", error.toString());
+ return true;
+ }));
+
+ tasks.push(machine_info.udev_info()
+ .then(result => {
+ findPCI(result, info);
+ findMemoryDevices(result, info);
+ return true;
+ })
+ .catch(error => {
+ console.warn("Failed to get udev information:", error.toString());
+ return true;
+ }));
+
+ // Fallback if systemd < 248
+ if (info.memory.length === 0) {
+ tasks.push(machine_info.memory_info()
+ .then(result => {
+ info.memory = result;
+ return true;
+ })
+ .catch(error => {
+ console.warn("Failed to get dmidecode information: ", error.toString());
+ return true;
+ }));
+ }
+
+ // return info after all task promises got done
+ return Promise.all(tasks).then(() => info);
+}
diff --git a/pkg/systemd/hwinfo.html b/pkg/systemd/hwinfo.html
new file mode 100644
index 0000000..65ea5da
--- /dev/null
+++ b/pkg/systemd/hwinfo.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html id="system-hwinfo-page">
+<head>
+ <title translate="yes">Hardware information</title>
+ <meta charset="utf-8" />
+ <link href="hwinfo.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="hwinfo"></div>
+ <script src="hwinfo.js"></script>
+</body>
+</html>
diff --git a/pkg/systemd/hwinfo.jsx b/pkg/systemd/hwinfo.jsx
new file mode 100644
index 0000000..783e92b
--- /dev/null
+++ b/pkg/systemd/hwinfo.jsx
@@ -0,0 +1,352 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import 'cockpit-dark-theme'; // once per page
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import 'polyfills'; // once per application
+
+import cockpit from "cockpit";
+import React from "react";
+import { createRoot } from 'react-dom/client';
+
+import * as timeformat from 'timeformat';
+
+import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DataList, DataListAction, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { EmptyState } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Text, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import { ExternalLinkAltIcon } from "@patternfly/react-icons";
+import { SortByDirection } from "@patternfly/react-table";
+import { ListingTable } from "cockpit-components-table.jsx";
+import { WithDialogs, useDialogs } from "dialogs.jsx";
+
+import kernelopt_sh from "./kernelopt.sh";
+import detect from "./hw-detect.js";
+
+import { superuser } from "superuser";
+import { PrivilegedButton } from "cockpit-components-privileged.jsx";
+import { useInit } from "hooks";
+
+import "./hwinfo.scss";
+
+const _ = cockpit.gettext;
+
+const SystemInfo = ({ info, onSecurityClick }) => {
+ if ((!info.name || !info.version) && info.alt_name && info.alt_version) {
+ info.name = info.alt_name;
+ info.version = info.alt_version;
+ }
+
+ const mitigations = (
+ <PrivilegedButton variant="link" buttonId="cpu_mitigations" tooltipId="tip-cpu-security"
+ excuse={ _("The user $0 is not permitted to change cpu security mitigations") }
+ onClick={ onSecurityClick }>
+ { _("Mitigations") }
+ </PrivilegedButton>
+ );
+
+ const bios_date = Date.parse(info.bios_date); // NaN for undefined, null, or invalid dates
+
+ return (
+ <Flex id="hwinfo-system-info-list" direction={{ default: 'column', sm: 'row' }}>
+ <FlexItem className="hwinfo-system-info-list-item" flex={{ default: 'flex_1' }}>
+ <DescriptionList className="pf-m-horizontal-on-md">
+ { info.type &&
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Type") }</DescriptionListTerm>
+ <DescriptionListDescription>{ info.type }</DescriptionListDescription>
+ </DescriptionListGroup> }
+ { info.name &&
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Name") }</DescriptionListTerm>
+ <DescriptionListDescription>{ info.name }</DescriptionListDescription>
+ </DescriptionListGroup> }
+ { info.version &&
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Version") }</DescriptionListTerm>
+ <DescriptionListDescription>{ info.version }</DescriptionListDescription>
+ </DescriptionListGroup> }
+ </DescriptionList>
+ </FlexItem>
+ <FlexItem className="hwinfo-system-info-list-item" flex={{ default: 'flex_1' }}>
+ <DescriptionList className="pf-m-horizontal-on-md">
+ { info.bios_vendor && <>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("BIOS") }</DescriptionListTerm>
+ <DescriptionListDescription>{ info.bios_vendor }</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("BIOS version") }</DescriptionListTerm>
+ <DescriptionListDescription>{ info.bios_version }</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("BIOS date") }</DescriptionListTerm>
+ <DescriptionListDescription>{ bios_date ? timeformat.date(bios_date) : info.bios_date }</DescriptionListDescription>
+ </DescriptionListGroup>
+ </> }
+ { info.nproc !== undefined && <>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("CPU") }</DescriptionListTerm>
+ <DescriptionListDescription>{ (info.nproc > 1) ? `${info.nproc}x ${info.cpu_model}` : info.cpu_model }</DescriptionListDescription>
+ </DescriptionListGroup>
+ { onSecurityClick !== undefined && <DescriptionListGroup>
+ <DescriptionListTerm>{ _("CPU security") }</DescriptionListTerm>
+ <DescriptionListDescription>{ mitigations }</DescriptionListDescription>
+ </DescriptionListGroup>}
+ </> }
+ </DescriptionList>
+ </FlexItem>
+ </Flex>
+ );
+};
+
+function availableMitigations() {
+ if (availableMitigations.cachedMitigations !== undefined)
+ return Promise.resolve(availableMitigations.cachedMitigations);
+ /* nosmt */
+ const promises = [cockpit.spawn(["lscpu"], { environ: ["LC_ALL=C.UTF-8"], }), cockpit.file("/proc/cmdline").read()];
+ return Promise.all(promises).then(values => {
+ let threads_per_core;
+ try {
+ threads_per_core = Number(values[0].split('\n')
+ .find(l => l.indexOf('Thread(s) per core:') !== -1)
+ .split(':')[1]);
+ } catch (e) {
+ console.warn(e);
+ return { available: false };
+ }
+ /* "nosmt" and "nosmt=force" are valid */
+ const nosmt_enabled = (values[1].indexOf("nosmt") !== -1 && values[1].indexOf("nosmt=") === -1) || values[1].indexOf("nosmt=force") !== -1;
+ /* available if threads>1 and the cmdline is valid */
+ const nosmt_available = threads_per_core > 1 && (values[1].indexOf("nosmt=") === -1 || values[1].indexOf("nosmt=force") !== -1);
+ const mitigations_match = values[1].match(/\bmitigations=(\S*)\b/);
+
+ availableMitigations.cachedMitigations = {
+ available: nosmt_available,
+ nosmt_enabled,
+ mitigations_arg: mitigations_match ? mitigations_match[1] : undefined,
+ };
+ return availableMitigations.cachedMitigations;
+ });
+}
+
+const CPUSecurityMitigationsDialog = () => {
+ const [nosmt, setNoSMT] = React.useState(undefined);
+ const [alert, setAlert] = React.useState(undefined);
+ const [rebooting, setRebooting] = React.useState(false);
+
+ useInit(() => {
+ availableMitigations().then(({ available, nosmt_enabled }) => {
+ setNoSMT(nosmt_enabled);
+ });
+ });
+
+ const saveAndReboot = () => {
+ let options = [];
+ if (nosmt) {
+ options = ['set', 'nosmt'];
+ } else {
+ // this may either be an argument of its own, or part of mitigations=
+ const ma = availableMitigations.cachedMitigations.mitigations_arg;
+ if (ma && ma.indexOf("nosmt") >= 0) {
+ const new_args = ma.split(',').filter(opt => opt != 'nosmt');
+ options = ['set', 'mitigations=' + new_args.join(',')];
+ } else {
+ options = ['remove', 'nosmt'];
+ }
+ }
+
+ cockpit.script(kernelopt_sh, options, { superuser: "require", err: "message" })
+ .then(() => {
+ cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require", err: "message" })
+ .catch(error => { setRebooting(false); setAlert(error.message) });
+ })
+ .catch(error => { setRebooting(false); setAlert(error.message) });
+ setRebooting(true);
+ };
+
+ const Dialogs = useDialogs();
+ const rows = [];
+ if (nosmt !== undefined) {
+ rows.push(
+ <DataListItem key="nosmt">
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell key="primary content">
+ <span>
+ <div className='nosmt-heading'>{ _("Disable simultaneous multithreading") } (nosmt)</div>
+ <small className='nosmt-read-more-link'>
+ <a href="https://access.redhat.com/security/vulnerabilities/L1TF" target="_blank" rel="noopener noreferrer">
+ <ExternalLinkAltIcon /> { _("Read more...") }
+ </a>
+ </small>
+ </span>
+ </DataListCell>,
+ ]}
+ />
+ <DataListAction>
+ <div id="nosmt-switch">
+ <Switch isDisabled={rebooting}
+ onChange={(_event, value) => setNoSMT(value)}
+ isChecked={nosmt} />
+ </div>
+ </DataListAction>
+ </DataListItemRow>
+ </DataListItem>
+ );
+ }
+
+ const footer = (
+ <>
+ <Button variant='danger' isDisabled={rebooting || nosmt === undefined} onClick={saveAndReboot}>
+ { _("Save and reboot") }
+ </Button>
+ <Button variant='link' className='btn-cancel' isDisabled={rebooting} onClick={Dialogs.close}>
+ { _("Cancel") }
+ </Button>
+ </>
+ );
+
+ return (
+ <Modal isOpen id="cpu-mitigations-dialog"
+ position="top" variant="medium"
+ footer={footer}
+ onClose={Dialogs.close}
+ title={ _("CPU security toggles") }>
+ <>
+ <Text className='cpu-mitigations-dialog-info' component={TextVariants.p}>
+ { _("Software-based workarounds help prevent CPU security issues. These mitigations have the side effect of reducing performance. Change these settings at your own risk.") }
+ </Text>
+ <DataList>
+ { rows }
+ </DataList>
+ { alert !== undefined &&
+ <Alert variant="danger"
+ actionClose={<AlertActionCloseButton onClose={() => setAlert(undefined)} />}
+ title={alert} />}
+ </>
+ </Modal>
+ );
+};
+
+const HardwareInfo = ({ info }) => {
+ const [mitigationsAvailable, setMitigationsAvailable] = React.useState(false);
+
+ useInit(() => {
+ availableMitigations().then(({ available }) => setMitigationsAvailable(available));
+ });
+
+ const Dialogs = useDialogs();
+ let pci = null;
+ let memory = null;
+
+ if (info.pci.length > 0) {
+ const sortedPci = info.pci.concat();
+
+ pci = (
+ <ListingTable aria-label={ _("PCI") }
+ sortBy={{ index: 0, direction: SortByDirection.asc }}
+ columns={ [
+ { title: _("Class"), sortable: true },
+ { title: _("Model"), sortable: true },
+ { title: _("Vendor"), sortable: true },
+ { title: _("Slot"), sortable: true }
+ ] }
+ rows={ sortedPci.map(dev => ({
+ props: { key: dev.slot },
+ columns: [dev.cls, dev.model, dev.vendor, dev.slot]
+ }))} />
+ );
+ }
+
+ if (info.memory.length > 0) {
+ memory = (
+ <ListingTable aria-label={ _("Memory") }
+ columns={ [_("ID"), _("Memory technology"), _("Type"), _("Size"), _("State"), _("Rank"), _("Speed")]}
+ rows={ info.memory.map(dimm => ({
+ props: { key: dimm.locator },
+ columns: [dimm.locator, dimm.technology, dimm.type, dimm.size, dimm.state, dimm.rank, dimm.speed]
+ })) } />
+ );
+ } else if (!superuser.allowed) {
+ memory = (<EmptyState>
+ {_("Viewing memory information requires administrative access.")}
+ </EmptyState>);
+ }
+
+ return (
+ <Page>
+ <PageBreadcrumb stickyOnBreakpoint={{ default: "top" }}>
+ <Breadcrumb>
+ <BreadcrumbItem onClick={ () => cockpit.jump("/system", cockpit.transport.host)} className="pf-v5-c-breadcrumb__link">{ _("Overview") }</BreadcrumbItem>
+ <BreadcrumbItem isActive>{ _("Hardware information") }</BreadcrumbItem>
+ </Breadcrumb>
+ </PageBreadcrumb>
+ <PageSection>
+ <Gallery hasGutter>
+ <Card>
+ <CardHeader>
+ <CardTitle component="h2">{_("System information")}</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <SystemInfo info={info.system}
+ onSecurityClick={mitigationsAvailable
+ ? () => Dialogs.show(<CPUSecurityMitigationsDialog />)
+ : undefined } />
+ </CardBody>
+ </Card>
+ <Card id="pci-listing">
+ <CardHeader>
+ <CardTitle component="h2">{_("PCI")}</CardTitle>
+ </CardHeader>
+ <CardBody className="contains-list">
+ { pci }
+ </CardBody>
+ </Card>
+ <Card id="memory-listing">
+ <CardHeader>
+ <CardTitle component="h2">{_("Memory")}</CardTitle>
+ </CardHeader>
+ <CardBody className="contains-list">
+ { memory }
+ </CardBody>
+ </Card>
+ </Gallery>
+ </PageSection>
+ </Page>
+ );
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.title = cockpit.gettext(document.title);
+ detect().then(info => {
+ const root = createRoot(document.getElementById('hwinfo'));
+ root.render(<WithDialogs><HardwareInfo info={info} /></WithDialogs>);
+ });
+});
diff --git a/pkg/systemd/hwinfo.scss b/pkg/systemd/hwinfo.scss
new file mode 100644
index 0000000..4b2cab7
--- /dev/null
+++ b/pkg/systemd/hwinfo.scss
@@ -0,0 +1,30 @@
+@use "ct-card";
+@use "page";
+
+#hwinfo {
+ /* Style the list cards as ct-cards */
+ .pf-v5-c-page__main-section .pf-v5-c-card {
+ @extend .ct-card;
+ }
+
+ .pf-v5-l-gallery {
+ --pf-v5-l-gallery--GridTemplateColumns: 1fr;
+ }
+}
+
+#hwinfo-system-info-list {
+ /* When the flex wraps make have the same gap as description list items have between them */
+ row-gap: 1rem;
+}
+
+.nosmt-read-more-link {
+ display: block;
+}
+
+.nosmt-heading {
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+}
+
+.cpu-mitigations-dialog-info {
+ margin-block-end: var(--pf-v5-global--spacer--md);
+}
diff --git a/pkg/systemd/index.html b/pkg/systemd/index.html
new file mode 100644
index 0000000..5eeb8d8
--- /dev/null
+++ b/pkg/systemd/index.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html id="system-overview-page">
+<head>
+ <meta charset="utf-8" />
+ <title>Overview</title>
+ <meta name="description" content="" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="stylesheet" href="overview.css" />
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script type="text/javascript" src="../base1/po.js"></script>
+ <script type="text/javascript" src="overview.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script src="../manifests.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="overview"></div>
+</body>
+</html>
diff --git a/pkg/systemd/kernelopt.sh b/pkg/systemd/kernelopt.sh
new file mode 100755
index 0000000..c60747d
--- /dev/null
+++ b/pkg/systemd/kernelopt.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+# Helper to add, modify, and remove a kernel command line option. This supports
+# grub and zipl, i. e. x86, arm64, and s390x. Either grubby (Fedora, RHEL) or
+# update-grub (Debian, Ubuntu) needs to be available.
+#
+# Copyright (C) 2019 Red Hat, Inc
+set -eu
+
+error() {
+ echo "$1" >&2
+ exit 1
+}
+
+grub() {
+ key="${2%=*}" # split off optional =value
+
+ # For the non-BLS case, or if someone overrides those with grub2-mkconfig
+ # or update-grub, change it in /etc/default/grub
+ if [ -e /etc/default/grub ]; then
+ if [ "$1" = set ]; then
+ # replace existing argument, otherwise append it
+ sed -i.bak -r "/^[[:space:]]*GRUB_CMDLINE_LINUX\b/ { s/$key(=[^[:space:]\"]*)?/$2/g; t; s/\"$/ $2\"/ }" /etc/default/grub
+ else
+ sed -i.bak -r "/^[[:space:]]*GRUB_CMDLINE_LINUX\b/ s/$key(=[^[:space:]\"]*)?//g" /etc/default/grub
+ fi
+ fi
+
+ # on Fedora and RHEL, use grubby; this covers grub and BLS; s390x's zipl also supports BLS there
+ if type grubby >/dev/null 2>&1; then
+ if [ "$1" = set ]; then
+ grubby --args="$2" --update-kernel=ALL
+ else
+ grubby --remove-args="$2" --update-kernel=ALL
+ fi
+
+ # on Debian/Ubuntu, use update-grub, which reads from /etc/default/grub
+ elif [ -e /etc/default/grub ] && type update-grub >/dev/null 2>&1; then
+ update-grub
+
+ # on OSTree, the kernel config is inside the image
+ elif cur=$(rpm-ostree kargs 2>&1); then
+ if [ "$1" = set ]; then
+ # replace if already present; can happen in the middle (must be separated by space) or at the beginning of line
+ if [ "${cur% $key *}" != "$cur" ] || [ "${cur% $key=*}" != "$cur" ] || [ "${cur#${key}[ =]}" != "$cur" ]; then
+ rpm-ostree kargs --replace="$2"
+ else
+ rpm-ostree kargs --append="$2"
+ fi
+ else
+ rpm-ostree kargs --delete="$key"
+ fi
+ else
+ error "No supported grub update mechanism found (grubby, update-grub, or rpm-ostree)"
+ fi
+}
+
+update_zipl() {
+ if type zipl >/dev/null 2>&1; then
+ zipl
+ fi
+}
+
+#
+# main
+#
+
+if [ -z "${2:-}" -o -n "${3:-}" ] || [ "$1" != "set" -a "$1" != "remove" ]; then
+ error "Usage: '$0 set <option>[=<value>]' or '$0 remove <option>'"
+fi
+
+grub "$1" "$2"
+update_zipl
diff --git a/pkg/systemd/logDetails.jsx b/pkg/systemd/logDetails.jsx
new file mode 100644
index 0000000..ab8a553
--- /dev/null
+++ b/pkg/systemd/logDetails.jsx
@@ -0,0 +1,208 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import { journal } from "journal";
+import * as timeformat from "timeformat";
+
+import React from 'react';
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { AbrtLogDetails } from "./abrtLog.jsx";
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Gallery, GalleryItem } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+
+const _ = cockpit.gettext;
+
+const LogDetails = ({ entry }) => {
+ const general = Object.keys(entry).filter(k => k !== 'MESSAGE');
+ general.sort();
+
+ const id = entry.PROBLEM_BINARY || entry.UNIT || entry.SYSLOG_IDENTIFIER || "";
+ let service = entry.USER_UNIT || entry.COREDUMP_USER_UNIT || entry._SYSTEMD_USER_UNIT || "";
+ const is_user = !!service;
+ service = service || entry.UNIT || entry.COREDUMP_UNIT || entry._SYSTEMD_UNIT || "";
+
+ // Only show redirect for unit types we show
+ if (["service", "target", "socket", "timer", "path"].indexOf(service.split(".").slice(-1)[0]) === -1)
+ service = undefined;
+
+ const actions = service && (
+ <Button variant="link" onClick={() => cockpit.jump("/system/services#/" + service + (is_user ? "?owner=user" : "")) }>
+ {cockpit.format(_("Go to $0"), service)}
+ </Button>
+ );
+
+ return (
+ <GalleryItem>
+ <Card>
+ <CardHeader actions={{ actions }}>
+ <h2 id="entry-heading">{id}</h2>
+ </CardHeader>
+ <CardTitle>{journal.printable(entry.MESSAGE, "MESSAGE")}</CardTitle>
+ <CardBody>
+ <DescriptionList className="pf-m-horizontal-on-sm">
+ { general.map(key =>
+ <DescriptionListGroup key={key}>
+ <DescriptionListTerm>{key}</DescriptionListTerm>
+ <DescriptionListDescription>{journal.printable(entry[key], key)}</DescriptionListDescription>
+ </DescriptionListGroup>
+ )}
+ </DescriptionList>
+ </CardBody>
+ </Card>
+ </GalleryItem>
+ );
+};
+
+function get_problems(service) {
+ return service.wait()
+ .then(() => {
+ return service.GetProblems(0, {})
+ .then(paths => {
+ const proxies = paths.map(p => service.client.proxy("org.freedesktop.Problems2.Entry", p));
+ return Promise.all(proxies.map(p => p.wait()))
+ .then(() => {
+ const result = { };
+ for (let i = 0; i < paths.length; i++)
+ result[paths[i]] = proxies[i];
+ return result;
+ });
+ });
+ });
+}
+
+export class LogEntry extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ error: "",
+ entry: null,
+ loading: true,
+ problemPath: null,
+ abrtService: null,
+ };
+
+ this.loadProblem = this.loadProblem.bind(this);
+ this.goHome = this.goHome.bind(this);
+ this.problems_client = null;
+ }
+
+ loadProblem(entry) {
+ if (this.problems_client)
+ this.problems_client.close();
+ this.problems_client = cockpit.dbus('org.freedesktop.problems', { superuser: "try" });
+
+ const service = this.problems_client.proxy('org.freedesktop.Problems2', '/org/freedesktop/Problems2');
+ get_problems(service)
+ .then(problems => {
+ const fields = [entry.PROBLEM_DIR, entry.PROBLEM_DUPHASH, entry.PROBLEM_UUID];
+ let path = null;
+ Object.keys(problems).some(pth => {
+ const p = problems[pth];
+ if (p && (fields.indexOf(p.ID) > 0 || fields.indexOf(p.UUID) || fields.indexOf(p.Duphash))) {
+ path = p;
+ return true;
+ } else {
+ return false;
+ }
+ });
+
+ this.setState({ entry, loading: false, error: "", problemPath: path, abrtService: service });
+ })
+ .catch(err => this.setState({ entry, loading: false, error: err.toString() }));
+ }
+
+ componentDidMount() {
+ const cursor = cockpit.location.path[0];
+ journal.journalctl({ cursor, count: 1, follow: false })
+ .then(entries => {
+ if (entries.length >= 1 && entries[0].__CURSOR == cursor) {
+ if (entries[0].SYSLOG_IDENTIFIER === "abrt-notification" || entries[0]._SYSTEMD_UNIT === "abrt-notification")
+ this.loadProblem(entries[0]);
+ else
+ this.setState({ entry: entries[0], loading: false, error: "" });
+ } else
+ this.setState({ entry: null, loading: false, error: _("Journal entry not found") });
+ })
+ .catch(error => this.setState({ entry: null, loading: false, error }));
+ }
+
+ componentWillUnmount() {
+ if (this.problems_client) {
+ this.problems_client.close();
+ this.problems_client = null;
+ }
+ }
+
+ goHome(ev) {
+ ev.preventDefault();
+ let parent_options = {};
+ if (cockpit.location.options.parent_options)
+ parent_options = JSON.parse(cockpit.location.options.parent_options);
+ cockpit.location.go('/', parent_options);
+ }
+
+ render() {
+ let breadcrumb = _("Journal entry");
+ let content = null;
+
+ if (this.state.error)
+ content = <EmptyStatePanel icon={ExclamationCircleIcon} title={this.state.error} />;
+ else if (this.state.loading)
+ content = <EmptyStatePanel loading title={ _("Loading...") } />;
+ else if (this.state.entry) {
+ const entry = this.state.entry;
+ const date = timeformat.dateTimeSeconds(entry.__REALTIME_TIMESTAMP / 1000);
+
+ if (this.state.problemPath) {
+ breadcrumb = cockpit.format(_("$0: crash at $1"), entry.PROBLEM_BINARY, date);
+ content = <AbrtLogDetails problem={this.state.problemPath}
+ entry={entry}
+ service={this.state.abrtService}
+ reloadProblems={this.goHome} />;
+ } else {
+ breadcrumb = cockpit.format(_("Entry at $0"), date);
+ content = <LogDetails entry={entry} />;
+ }
+ }
+
+ return (
+ <Page id="log-details">
+ <PageBreadcrumb stickyOnBreakpoint={{ default: "top" }}>
+ <Breadcrumb>
+ <BreadcrumbItem onClick={this.goHome} className="pf-v5-c-breadcrumb__link">{_("Logs")}</BreadcrumbItem>
+ <BreadcrumbItem isActive>
+ {breadcrumb}
+ </BreadcrumbItem>
+ </Breadcrumb>
+ </PageBreadcrumb>
+ <PageSection>
+ <Gallery hasGutter>
+ {content}
+ </Gallery>
+ </PageSection>
+ </Page>
+ );
+ }
+}
diff --git a/pkg/systemd/logs.html b/pkg/systemd/logs.html
new file mode 100644
index 0000000..d0c996a
--- /dev/null
+++ b/pkg/systemd/logs.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+This file is part of Cockpit.
+
+Copyright (C) 2015 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="system-logs-page">
+
+<head>
+ <title translate="yes">Journal</title>
+ <meta charset="utf-8" />
+ <link href="logs.css" rel="stylesheet" />
+ <script type="text/javascript" src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="logs">
+ </div>
+
+ <script type="text/javascript" src="logs.js"></script>
+</body>
+
+</html>
diff --git a/pkg/systemd/logs.jsx b/pkg/systemd/logs.jsx
new file mode 100644
index 0000000..8774016
--- /dev/null
+++ b/pkg/systemd/logs.jsx
@@ -0,0 +1,431 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import 'cockpit-dark-theme'; // once per page
+
+import cockpit from "cockpit";
+import React, { useState, useEffect } from 'react';
+import { createRoot } from 'react-dom/client';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { ClipboardCopy } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js";
+import { Divider } from "@patternfly/react-core/dist/esm/components/Divider/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { SearchInput } from "@patternfly/react-core/dist/esm/components/SearchInput/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, ToolbarToggleGroup } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js";
+import {
+ ExternalLinkSquareAltIcon,
+ FilterIcon,
+ HelpIcon,
+} from '@patternfly/react-icons';
+
+import {
+ checkJournalctlGrep,
+ getGrepFiltersFromOptions,
+ getOptionsFromTextInput,
+} from "./logsHelpers.js";
+import { JournalBox } from "./logsJournal.jsx";
+import { LogEntry } from "./logDetails.jsx";
+
+import { usePageLocation } from "hooks";
+
+import "./logs.scss";
+
+const _ = cockpit.gettext;
+
+const timeFilterOptions = [
+ { key: "boot", value: 0, toString: () => _("Current boot"), },
+ { key: "boot", value: "-1", toString: () => _("Previous boot") },
+ { key: "since", value: "-24hours", toString: () => _("Last 24 hours"), default: true },
+ { key: "since", value: "-7days", toString: () => _("Last 7 days") },
+];
+
+const journalPrioOptions = [
+ { value: "emerg", toString: () => _("Only emergency") },
+ { value: "alert", toString: () => _("Alert and above") },
+ { value: "crit", toString: () => _("Critical and above") },
+ { value: "err", toString: () => _("Error and above") },
+ { value: "warning", toString: () => _("Warning and above") },
+ { value: "notice", toString: () => _("Notice and above") },
+ { value: "info", toString: () => _("Info and above") },
+ { value: "debug", toString: () => _("Debug and above") },
+];
+
+const getPrioFilterOption = options => {
+ if (options.priority || options.prio)
+ return journalPrioOptions.find(option => option.value == options.priority || option.value == options.prio);
+
+ return journalPrioOptions.find(option => option.value == "err");
+};
+
+const getTimeFilterOption = options => {
+ if (options.boot)
+ return timeFilterOptions.find(option => option.key == 'boot' && option.value == options.boot);
+ else if (options.since)
+ return timeFilterOptions.find(option => option.key == 'since' && option.value == options.since);
+ return timeFilterOptions.find(option => 'default' in option); // Use the default key
+};
+
+export const LogsPage = () => {
+ const { path, options } = usePageLocation();
+ let follow = !(options.follow && options.follow === "false");
+
+ if (options.boot && options.boot !== "0") // Don't follow if specific boot is picked
+ follow = false;
+
+ // If priority not specified use err
+ if (!options.priority && !options.prio)
+ options.priority = 'err';
+
+ const full_grep = getGrepFiltersFromOptions({ options })[0];
+
+ /* Initial state */
+ const [currentIdentifiers, setCurrentIdentifiers] = useState(undefined);
+ const [dataFollowing, setDataFollowing] = useState(follow);
+ const [filteredQuery, setFilteredQuery] = useState(undefined);
+ const [isOpenPrioFilter, setIsOpenPrioFilter] = useState(false);
+ const [isOpenTimeFilter, setIsOpenTimeFilter] = useState(false);
+ // `prio` is a legacy name. Accept it, but don't generate it
+ const [journalPrio, setJournalPrio] = useState(getPrioFilterOption(options));
+ const [identifiersFilter, setIdentifiersFilter] = useState(options.tag || _("All"));
+ const [showTextSearch, setShowTextSearch] = useState(false);
+ const [textFilter, setTextFilter] = useState(full_grep);
+ const [timeFilter, setTimeFilter] = useState(getTimeFilterOption(options));
+ const [updateIdentifiersList, setUpdateIdentifiersList] = useState(true);
+
+ useEffect(() => {
+ checkJournalctlGrep(setShowTextSearch);
+
+ function onNavigate() {
+ const { options, path } = cockpit.location;
+ const full_grep = getGrepFiltersFromOptions({ options })[0];
+
+ if (path.length == 1) return;
+
+ setJournalPrio(getPrioFilterOption(options));
+ setIdentifiersFilter(options.tag || _("All"));
+ setTextFilter(full_grep);
+ setTimeFilter(getTimeFilterOption(options));
+ }
+
+ cockpit.addEventListener("locationchanged", onNavigate);
+ return () => cockpit.removeEventListener("locationchanged", onNavigate);
+ }, []);
+
+ if (path.length == 1) {
+ return <LogEntry />;
+ } else if (path.length > 1) { /* redirect */
+ console.warn("not a journal location: " + path);
+ cockpit.location = '';
+ }
+
+ const updateUrl = (options) => {
+ cockpit.location.go([], options);
+ };
+
+ const onJournalPrioChange = (value) => {
+ setUpdateIdentifiersList(false);
+
+ updateUrl(Object.assign(options, { priority: value }));
+ };
+
+ const onIdentifiersFilterChange = (value) => {
+ setUpdateIdentifiersList(false);
+
+ if (value == _("All")) {
+ delete options.tag;
+ updateUrl(Object.assign(options));
+ } else {
+ updateUrl(Object.assign(options, { tag: value }));
+ }
+ };
+
+ const onTextFilterChange = (value) => {
+ setUpdateIdentifiersList(true);
+
+ updateUrl(Object.assign(getOptionsFromTextInput(value)));
+ };
+
+ const onTimeFilterChange = (newTimeFilter) => {
+ setUpdateIdentifiersList(true);
+
+ if (newTimeFilter.key == 'boot' && newTimeFilter.value !== "0") // Don't follow if specific boot is picked
+ setDataFollowing(false);
+ else if (options.boot && options.boot !== "0" && newTimeFilter.key !== "boot") // Start following is specific boot is removed
+ setDataFollowing(true);
+
+ // Remove all parameters which can be set up using filters
+ delete options.boot;
+ delete options.since;
+
+ cockpit.location.go([], Object.assign(options, { [newTimeFilter.key]: newTimeFilter.value }));
+ };
+
+ return (
+ <Page>
+ <PageSection id="journal" padding={{ default: 'noPadding' }}>
+ <Toolbar>
+ <ToolbarContent>
+ <ToolbarToggleGroup className="pf-v5-u-flex-wrap pf-v5-u-flex-grow-1 pf-v5-u-align-items-flex-start" toggleIcon={<><span className="pf-v5-c-button__icon pf-m-start"><FilterIcon /></span>{_("Toggle filters")}</>} breakpoint="lg">
+ <ToolbarGroup>
+ <ToolbarItem>
+ <Select toggleId="logs-predefined-filters"
+ isOpen={isOpenTimeFilter}
+ onToggle={(_, isOpen) => setIsOpenTimeFilter(isOpen)}
+ onSelect={(e, selection) => {
+ setIsOpenTimeFilter(false);
+ onTimeFilterChange(selection);
+ }}
+ selections={timeFilter}
+ placeholderText={_("Time")}>
+ {timeFilterOptions.map(option => <SelectOption key={option.value}
+ value={option} />)}
+ </Select>
+ </ToolbarItem>
+
+ <ToolbarItem variant="label">
+ {_("Priority")}
+ </ToolbarItem>
+ <ToolbarItem>
+ <Select toggleId="journal-prio-menu"
+ isOpen={isOpenPrioFilter}
+ onToggle={(_, isOpen) => setIsOpenPrioFilter(isOpen)}
+ onSelect={(e, selection) => {
+ setIsOpenPrioFilter(false);
+ onJournalPrioChange(selection.value);
+ }}
+ selections={journalPrio}>
+ {journalPrioOptions.map(option => <SelectOption key={option.value} value={option} />)}
+ </Select>
+ </ToolbarItem>
+
+ <ToolbarItem variant="label">
+ {_("Identifier")}
+ </ToolbarItem>
+ <ToolbarItem id="journal-identifier-menu">
+ <IdentifiersFilter currentIdentifiers={currentIdentifiers}
+ onIdentifiersFilterChange={onIdentifiersFilterChange}
+ identifiersFilter={identifiersFilter} />
+ </ToolbarItem>
+ </ToolbarGroup>
+
+ <ToolbarGroup>
+ {showTextSearch &&
+ <>
+ <ToolbarItem variant="label">
+ {_("Filters")}
+ </ToolbarItem>
+ <ToolbarItem className="text-search">
+ <TextFilter id="journal-grep"
+ key={textFilter}
+ textFilter={textFilter}
+ onTextFilterChange={onTextFilterChange}
+ filteredQuery={filteredQuery} />
+ </ToolbarItem>
+ </>}
+ <ToolbarItem variant="separator" />
+
+ <ToolbarItem>
+ <Button id="journal-follow"
+ variant="secondary"
+ isDisabled={options.boot && options.boot !== "0"}
+ onClick={() => {
+ // Reset time filter if following mode is now selected but we are on a specific boot
+ if (!dataFollowing && timeFilter && timeFilter.key == "boot" && timeFilter.value !== "0") {
+ setTimeFilter(undefined);
+ }
+
+ setDataFollowing(!dataFollowing);
+ }
+ }
+ data-following={dataFollowing}>
+ {dataFollowing ? _("Pause") : _("Resume")}
+ </Button>
+ </ToolbarItem>
+ </ToolbarGroup>
+ </ToolbarToggleGroup>
+ </ToolbarContent>
+ </Toolbar>
+
+ </PageSection>
+ <PageSection padding={{ default: 'noPadding' }}
+ variant={PageSectionVariants.light}
+ id="journal-box">
+ <JournalBox dataFollowing={dataFollowing}
+ defaultSince={timeFilter ? timeFilter.value : getTimeFilterOption({}).value}
+ setCurrentIdentifiers={setCurrentIdentifiers}
+ setFilteredQuery={setFilteredQuery}
+ updateIdentifiersList={updateIdentifiersList}
+ setUpdateIdentifiersList={setUpdateIdentifiersList} />
+ </PageSection>
+ </Page>
+ );
+};
+
+const IdentifiersFilter = ({ identifiersFilter, onIdentifiersFilterChange, currentIdentifiers }) => {
+ const [isOpenIdentifiersFilter, setIsOpenIdentifiersFilter] = useState(false);
+
+ let identifiersArray;
+ if (currentIdentifiers !== undefined) {
+ identifiersArray = [
+ <SelectOption key="all" value={_("All")} />,
+ <Divider component="li" key="divider" />
+ ];
+ identifiersArray = identifiersArray.concat(
+ Array.from(currentIdentifiers)
+ .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
+ .map(unit => <SelectOption key={unit} value={unit} />)
+ );
+ } else {
+ identifiersArray = [
+ <SelectOption key={identifiersFilter} value={identifiersFilter} isDisabled />
+ ];
+ }
+
+ /* The noResultsFoundText is not shown because of https://github.com/patternfly/patternfly-react/issues/6005 */
+ return (
+ <Select {...(currentIdentifiers === undefined && { loadingVariant: 'spinner' })}
+ onToggle={(_, isOpen) => setIsOpenIdentifiersFilter(isOpen)}
+ onSelect={(e, selection) => {
+ setIsOpenIdentifiersFilter(false);
+ onIdentifiersFilterChange(selection);
+ }}
+ isOpen={isOpenIdentifiersFilter}
+ noResultsFoundText={_("No results found")}
+ onClear={() => {
+ setIsOpenIdentifiersFilter(false);
+ onIdentifiersFilterChange(_("All"));
+ }}
+ selections={identifiersFilter}
+ typeAheadAriaLabel={_("Select a identifier")}
+ variant="typeahead">
+ {identifiersArray}
+ </Select>
+ );
+};
+
+const TextFilter = ({ textFilter, onTextFilterChange, filteredQuery }) => {
+ const [unsubmittedTextFilter, setUnsubmittedTextFilter] = useState(textFilter);
+ const sinceUntilBody = _("Date specifications should be of the format YYYY-MM-DD hh:mm:ss. Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. 'now' refers to the current time. Finally, relative times may be specified, prefixed with '-' or '+'");
+
+ const sinceLabel = (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <FlexItem>{_("Since")}</FlexItem>
+ <Popover headerContent={_("Start showing entries on or newer than the specified date.")}
+ showClose={false}
+ bodyContent={sinceUntilBody}>
+ <HelpIcon />
+ </Popover>
+ </Flex>
+ );
+
+ const untilLabel = (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <FlexItem>{_("Until")}</FlexItem>
+ <Popover headerContent={_("Start showing entries on or older than the specified date.")}
+ showClose={false}
+ bodyContent={sinceUntilBody}>
+ <HelpIcon />
+ </Popover>
+ </Flex>
+ );
+
+ const bootLabel = (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <FlexItem>{_("Boot")}</FlexItem>
+ <Popover headerContent={_("Show messages from a specific boot.")}
+ showClose={false}
+ bodyContent={_("This will add a match for '_BOOT_ID='. If not specified the logs for the current boot will be shown. If the boot ID is omitted, a positive offset will look up the boots starting from the beginning of the journal, and an equal-or-less-than zero offset will look up boots starting from the end of the journal. Thus, 1 means the first boot found in the journal in chronological order, 2 the second and so on; while -0 is the last boot, -1 the boot before last, and so on.")}>
+ <HelpIcon />
+ </Popover>
+ </Flex>
+ );
+
+ const serviceLabel = (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <FlexItem>{_("Unit")}</FlexItem>
+ <Popover headerContent={_("Show messages for the specified systemd unit.")}
+ showClose={false}
+ bodyContent={_("This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to find all possible messages for the given unit. Can contain more units separated by comma. ")}>
+ <HelpIcon />
+ </Popover>
+ </Flex>
+ );
+
+ const freeTextLabel = (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <FlexItem>{_("Free-form search")}</FlexItem>
+ <Popover headerContent={_("Show messages containing given string.")}
+ showClose={false}
+ bodyContent={_("Any text string in the logs messages can be filtered. The string can also be in the form of a regular expression. Also supports filtering by message log fields. These are space separated values, in form FIELD=VALUE, where value can be comma separated list of possible values.")}>
+ <HelpIcon />
+ </Popover>
+ </Flex>
+ );
+
+ const searchInputAttributes = [
+ { attr: "since", display: sinceLabel },
+ { attr: "until", display: untilLabel },
+ { attr: "boot", display: bootLabel },
+ { attr: "unit", display: serviceLabel },
+ { attr: "priority" }, // Hide this with CSS
+ { attr: "tag" }, // Hide this with CSS
+ ];
+
+ return (
+ <SearchInput attributes={searchInputAttributes}
+ hasWordsAttrLabel={freeTextLabel}
+ advancedSearchDelimiter=":"
+ id="journal-grep"
+ onClear={() => { onTextFilterChange(""); setUnsubmittedTextFilter("") }}
+ placeholder={_("Type to filter")}
+ value={unsubmittedTextFilter}
+ onChange={(_, val) => setUnsubmittedTextFilter(val)}
+ resetButtonLabel={_("Reset")}
+ submitSearchButtonLabel={_("Search")}
+ formAdditionalItems={<Stack hasGutter>
+ <Button variant="link" component="a" isInline
+ href="https://www.freedesktop.org/software/systemd/man/journalctl.html"
+ icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
+ target="blank" rel="noopener noreferrer">
+ {_("journalctl manpage")}
+ </Button>
+ <ClipboardCopy clickTip={_("Successfully copied to clipboard")}
+ isReadOnly
+ hoverTip={_("Copy to clipboard")}
+ id="journal-cmd-copy"
+ isCode>
+ {filteredQuery}
+ </ClipboardCopy>
+ </Stack>}
+ onSearch={() => onTextFilterChange(unsubmittedTextFilter)} />
+ );
+};
+
+function init() {
+ const root = createRoot(document.getElementById('logs'));
+ root.render(<LogsPage />);
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/pkg/systemd/logs.scss b/pkg/systemd/logs.scss
new file mode 100644
index 0000000..291c275
--- /dev/null
+++ b/pkg/systemd/logs.scss
@@ -0,0 +1,148 @@
+@use "../lib/table.css";
+@use "../lib/journal.css";
+@use "./system-global.scss";
+@import "global-variables.scss";
+@import "@patternfly/patternfly/utilities/Flex/flex.scss";
+
+// https://github.com/patternfly/patternfly-react/issues/5993
+.pf-v5-c-popover.pf-m-width-auto {
+ --pf-v5-c-popover--MaxWidth: min(300px, 90%);
+}
+
+#journal {
+ grid-template-rows: auto 1fr;
+
+ .cockpit-log-panel {
+ border: none;
+ }
+
+ .pf-v5-c-page__main {
+ // Constrain page to viewport height, so journal can overflow
+ max-block-size: 100vh;
+ }
+
+ // Static width for the service selector as it likes to resize a lot while loading
+ #journal-identifier-menu ul.pf-v5-c-select__menu {
+ inline-size: 10rem;
+ }
+
+ // Long names without spaces do not wrap; triggers overflow and overlaps with check icon
+ #journal-identifier-menu .pf-v5-c-select__menu-item {
+ white-space: normal;
+ }
+}
+
+#log-details {
+ .pf-v5-l-gallery {
+ --pf-v5-l-gallery--GridTemplateColumns: 1fr;
+ }
+
+ .pf-v5-c-page__main-breadcrumb {
+ padding: var(--pf-v5-global--gutter);
+ }
+
+ .pf-v5-c-card__title,
+ .multiline {
+ word-break: break-all;
+ white-space: pre-wrap !important;
+ }
+
+ .multiline {
+ font-family: monospace, monospace;
+ }
+
+ .pf-v5-l-split {
+ padding-block-end: var(--pf-v5-global--gutter);
+ align-items: center;
+ }
+
+ .pf-v5-c-description-list {
+ --pf-v5-c-description-list--m-horizontal__term--width: 25ch;
+ }
+
+ // For abrt log details: add some gutter between the tabs and their content
+ .pf-v5-c-tab-content > .pf-v5-c-description-list {
+ padding-block-start: var(--pf-v5-global--spacer--md);
+ }
+
+ .table-hide-labels {
+ [data-label] {
+ display: revert;
+ }
+
+ [data-label]::before {
+ display: none;
+ }
+ }
+
+ // Let the description list set the color, not the wrapper accordion component
+ .pf-v5-c-accordion__expanded-content {
+ color: unset;
+ }
+}
+
+#journal-box {
+ flex: auto;
+
+ .panel-heading {
+ position: sticky;
+ inset-block-start: 0;
+ color: var(--pf-v5-global--Color--300);
+ background-color: var(--pf-v5-global--BackgroundColor--100);
+ border: none;
+ font-size: var(--pf-v5-global--FontSize--md);
+ font-family: var(--pf-v5-global--FontFamily--heading--sans-serif);
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+ padding-block: var(--pf-v5-global--spacer--lg) var(--pf-v5-global--spacer--sm);
+ padding-inline: var(--pf-v5-global--spacer--lg);
+ }
+}
+
+.pf-v5-theme-dark {
+ #journal-box .panel-heading {
+ color: var(--pf-v5-global--Color--400);
+ }
+}
+
+/* Set min width for services in the journal view */
+#journal .cockpit-logline {
+ --log-service-min: 8rem;
+}
+
+#accordion-markup {
+ margin-block-end: 0;
+}
+
+.pf-v5-c-toolbar {
+ --pf-v5-c-toolbar--BackgroundColor: var(--pf-v5-c-page__main-section--BackgroundColor);
+
+ // Make toolbar stretch to all the available space and wrap up to two lines
+ .pf-v5-c-toolbar__group:nth-child(3) {
+ flex-grow: 1;
+ }
+
+ // Make text filter stretch to all the available space
+ .pf-v5-c-toolbar__item.text-search, #journal-grep {
+ flex-grow: 1;
+ }
+
+ .text-help {
+ padding-inline-start: var(--pf-v5-global--spacer--xs);
+ }
+
+ // Hide filters from advanced search dropdown entries which already exist in the toolbar
+ #journal-grep .pf-v5-c-panel__main-body {
+ .pf-v5-c-form__group:nth-child(5), .pf-v5-c-form__group:nth-child(6) {
+ display: none;
+ }
+ }
+
+ .pf-v5-c-toolbar__expandable-content.pf-m-expanded .pf-v5-c-divider {
+ display: none;
+ }
+
+ // FIXME: When porting the selects to the PF5 select implementation drop this
+ .pf-v5-c-toolbar__item {
+ align-self: center;
+ }
+}
diff --git a/pkg/systemd/logsHelpers.js b/pkg/systemd/logsHelpers.js
new file mode 100644
index 0000000..2973534
--- /dev/null
+++ b/pkg/systemd/logsHelpers.js
@@ -0,0 +1,143 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import cockpit from "cockpit";
+import { journal } from "journal";
+
+// Sometimes `journalctl` can be compiled without `PCRE2` which means
+// that `--grep` is not usable. Hide the search box in such cases.
+export function checkJournalctlGrep(setShowTextSearch) {
+ cockpit.spawn(["journalctl", "--version"])
+ .then(m => {
+ if (m.indexOf("-PCRE2") !== -1) {
+ setShowTextSearch(false);
+ } else {
+ setShowTextSearch(true);
+ }
+ });
+}
+
+// Build the journalctl query for the inline help popover
+export const getFilteredQuery = ({ match, options }) => {
+ const cmd = journal.build_cmd(match, options);
+ const filtered_cmd = cmd.filter(i => i !== "-q" && i !== "--output=json");
+ if (filtered_cmd[filtered_cmd.length - 1] == "--")
+ filtered_cmd.pop();
+
+ return filtered_cmd.join(" ");
+};
+
+export const getGrepFiltersFromOptions = ({ options }) => {
+ const grep = options.grep || "";
+ let full_grep = "";
+ const match = [];
+
+ // `prio` is a legacy name. Accept it, but don't generate it
+ const prio_level = options.priority || options.prio || "err";
+ full_grep += "priority:" + prio_level + " ";
+
+ if (options.service) {
+ options.service.split(",").forEach(s => {
+ if (!s.endsWith(".service"))
+ s = s + ".service";
+ match.push(...['_SYSTEMD_UNIT=' + s, "+", "COREDUMP_UNIT=" + s, "+", "UNIT=" + s]);
+ });
+ full_grep += "service:" + options.service + " ";
+ } else if (options["user-service"]) {
+ options["user-service"].split(",").forEach(s => {
+ if (!s.endsWith(".service"))
+ s = s + ".service";
+ match.push(...['_SYSTEMD_USER_UNIT=' + s, "+", "COREDUMP_USER_UNIT=" + s, "+", "USER_UNIT=" + s]);
+ });
+ full_grep += "user-service:" + options["user-service"] + " ";
+ }
+
+ if (options.tag && options.tag !== "*") {
+ match.push('SYSLOG_IDENTIFIER=' + options.tag);
+ full_grep += "identifier:" + options.tag + " ";
+ }
+
+ if (options.boot)
+ full_grep += "boot:" + options.boot + " ";
+
+ if (options.since)
+ full_grep += "since:" + options.since.replaceAll(" ", "\\ ") + " ";
+
+ if (options.until)
+ full_grep += "until:" + options.until.replaceAll(" ", "\\ ") + " ";
+
+ // Other filters may be passed as well
+ Object.keys(options).forEach(k => {
+ if (k === k.toUpperCase() && options[k]) {
+ options[k].split(",").forEach(v => match.push(k + "=" + v));
+ full_grep += k + '=' + options[k] + " ";
+ }
+ });
+
+ full_grep += grep;
+
+ return [full_grep, match];
+};
+
+const split_search = (text) => {
+ let last_i = 0;
+ const words = [];
+
+ // Add space so the following loop can always pick group on space (without trailing
+ // space the last group would not be recognized and it would need a special check)
+ if (text.length && text[text.length - 1] !== " ")
+ text += " ";
+
+ for (let i = 1; i < text.length; i++) {
+ if (text[i] === " " && text[i - 1] !== "\\") {
+ words.push(text.substring(last_i, i).replaceAll("\\ ", " "));
+ last_i = i + 1;
+ }
+ }
+ return words;
+};
+
+export const getOptionsFromTextInput = (value) => {
+ const new_items = {};
+ const values = split_search(value)
+ .filter(item => {
+ let s = item.split("=");
+ if (s.length === 2 && s[0] === s[0].toUpperCase()) {
+ new_items[s[0]] = s[1];
+ return false;
+ }
+
+ const well_know_keys = ["since", "until", "boot", "priority", "follow", "service", "identifier"];
+ const map_keys = (key) => {
+ if (key === "identifier")
+ return "tag";
+ if (key == "service")
+ return "unit";
+ return key;
+ };
+ s = item.split(/:(.*)/);
+ if (s.length >= 2 && well_know_keys.includes(s[0])) {
+ new_items[map_keys(s[0])] = s[1];
+ return false;
+ }
+
+ return true;
+ });
+ new_items.grep = values.join(" ");
+ return new_items;
+};
diff --git a/pkg/systemd/logsJournal.jsx b/pkg/systemd/logsJournal.jsx
new file mode 100644
index 0000000..bce02ea
--- /dev/null
+++ b/pkg/systemd/logsJournal.jsx
@@ -0,0 +1,343 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import { journal } from "journal";
+import { superuser } from "superuser";
+
+import React from 'react';
+import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { JournalOutput } from "cockpit-components-logs-panel.jsx";
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+
+import { getGrepFiltersFromOptions, getFilteredQuery } from "./logsHelpers.js";
+
+// We open a couple of long-running channels with { superuser: "try" },
+// so we need to reload the page if the access level changes.
+superuser.reload_page_on_change();
+
+const _ = cockpit.gettext;
+// Stop stream when entries > QUERY_MORE after clicking 'Load earlier entries' button
+const QUERY_MORE = 1000;
+// Stop stream when entries > QUERY_COUNT
+const QUERY_COUNT = 5000;
+
+export class JournalBox extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ cursor: undefined,
+ loading: true,
+ logs: [],
+ streamFinished: false,
+ didntReachStart: true,
+ };
+
+ this.appendEntries = this.appendEntries.bind(this);
+ this.currentServices = new Set();
+ this.followingProcs = [];
+ this.loadServiceFilters = this.loadServiceFilters.bind(this);
+ this.prependEntries = this.prependEntries.bind(this);
+ this.procs = [];
+ this.updateQuery = this.updateQuery.bind(this);
+ this.queryError = this.queryError.bind(this);
+
+ this.options = cockpit.location.options;
+ }
+
+ componentDidMount() {
+ cockpit.addEventListener("locationchanged", this.updateQuery);
+ this.updateQuery();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.dataFollowing != this.props.dataFollowing) {
+ if (this.props.dataFollowing) {
+ const cursor = document.querySelector(".cockpit-logline");
+ if (cursor)
+ this.follow(cursor.getAttribute("data-cursor"));
+ else
+ this.follow();
+ } else {
+ this.stopFollowing();
+ }
+ }
+ }
+
+ updateQuery() {
+ this.stop();
+
+ this.options = cockpit.location.options;
+ this.match = getGrepFiltersFromOptions({ options: this.options })[1];
+ const { dataFollowing, defaultSince, updateIdentifiersList, setFilteredQuery } = this.props;
+ const { priority, grep, boot, since, until } = this.options;
+ let last = dataFollowing ? null : 1;
+ let count = 0;
+ let oldest = null;
+ const all = boot === undefined && since === undefined && until === undefined;
+
+ this.out = new JournalOutput(this.options);
+ this.renderer = journal.renderer(this.out);
+
+ const tags_match = [];
+ this.match.forEach(field => {
+ if (!field.startsWith("SYSLOG_IDENTIFIER"))
+ tags_match.push(field);
+ });
+
+ const journalctlOptions = {
+ boot,
+ follow: false, /* follow: Show only the most recent journal entries, and continuously print new entries as they are appended to the journal. */
+ grep,
+ priority,
+ reverse: true, /* reverse: Reverse output so that the newest entries are displayed first */
+ since: since || defaultSince,
+ until
+ };
+
+ setFilteredQuery(getFilteredQuery({ match: this.match, options: journalctlOptions }));
+
+ if (updateIdentifiersList)
+ this.loadServiceFilters(tags_match, journalctlOptions);
+
+ this.setState({ loading: true, didntReachStart: false, streamFinished: false, logs: [] });
+
+ const promise = journal.journalctl(this.match, journalctlOptions)
+ .fail(this.queryError)
+ .stream(entries => {
+ if (!last) {
+ last = entries[0].__CURSOR;
+ this.follow(last);
+ }
+ count += entries.length;
+ this.appendEntries(entries);
+ oldest = entries[entries.length - 1].__CURSOR;
+ if (count >= QUERY_COUNT) {
+ this.setState({ didntReachStart: true, cursor: oldest });
+ promise.stop();
+ }
+ })
+ .done(() => {
+ this.setState({ streamFinished: true });
+
+ if (!last && !promise.stopped) {
+ const journalctlOptions = {
+ boot,
+ count: 0,
+ follow: true,
+ grep,
+ priority,
+ since,
+ until,
+ };
+ this.followingProcs.push(journal.journalctl(this.match, journalctlOptions)
+ .fail(this.queryError)
+ .stream(entries => {
+ this.prependEntries(entries);
+ }));
+ }
+ if (!all)
+ this.setState({ didntReachStart: true, cursor: oldest });
+ })
+ .always(() => this.setState({ loading: false }));
+ this.procs.push(promise);
+ }
+
+ queryError(error) {
+ this.setState({ error: cockpit.message(error) });
+ }
+
+ prependEntries(entries) {
+ let newServices = false;
+
+ for (let i = 0; i < entries.length; i++) {
+ const serviceTag = entries[i].SYSLOG_IDENTIFIER;
+ this.renderer.prepend(entries[i]);
+ if (!this.currentServices.has(serviceTag)) {
+ this.currentServices.add(serviceTag);
+ newServices = true;
+ }
+ }
+ if (newServices) {
+ this.props.setCurrentIdentifiers(this.currentServices);
+ }
+ this.renderer.prepend_flush();
+
+ this.setState({ logs: this.out.logs, loading: false });
+ }
+
+ appendEntries(entries) {
+ for (let i = 0; i < entries.length; i++)
+ this.renderer.append(entries[i]);
+ this.renderer.append_flush();
+
+ this.setState({ logs: this.out.logs, loading: false });
+ }
+
+ follow(cursor) {
+ const { priority, until, grep } = this.options;
+
+ const journalctlOptions = {
+ count: 0,
+ cursor: cursor || null,
+ follow: true,
+ grep,
+ priority,
+ until,
+ };
+ this.followingProcs.push(journal.journalctl(this.match, journalctlOptions)
+ .fail(this.queryError)
+ .stream(entries => {
+ if (entries[0].__CURSOR == cursor)
+ entries.shift();
+ this.prependEntries(entries);
+ }));
+ }
+
+ loadServiceFilters(match, options) {
+ // Ideally this would use `--output cat --output-fields SYSLOG_IDENTIFIER` and do
+ // without `sh -ec`, grep, sort, replaceAll and all of those ugly stuff
+ // For that we however need newer systemd that includes https://github.com/systemd/systemd/issues/13937
+ this.currentServices = new Set();
+ const service_options = Object.assign({ output: "verbose" }, options);
+ let cmd = journal.build_cmd(match, service_options);
+
+ cmd = cmd.map(i => i.replaceAll(" ", "\\ ")).join(" ");
+ cmd = "set -o pipefail; " + cmd + " | grep SYSLOG_IDENTIFIER= | sort -u";
+ cockpit.spawn(["/bin/bash", "-ec", cmd], { superuser: "try", err: "message" })
+ .then(entries => {
+ entries.split("\n").forEach(entry => {
+ if (entry)
+ this.currentServices.add(entry.substr(entry.indexOf('=') + 1));
+ });
+ })
+ .catch(e => {
+ // grep returns `1` when nothing to match, but in that case message is empty
+ if (e.message)
+ console.log("Failed to load services:", e.message);
+ })
+ .finally(() => {
+ this.props.setCurrentIdentifiers(this.currentServices);
+ });
+ }
+
+ stop() {
+ this.procs.forEach(proc => proc.stop());
+ this.followingProcs.forEach(proc => proc.stop());
+ }
+
+ stopFollowing() {
+ this.followingProcs.forEach(proc => proc.stop());
+ }
+
+ render() {
+ const { priority, grep } = this.options;
+ const noLogs = !this.state.logs.length;
+ let error = null;
+ if (this.state.error)
+ error = (
+ <Alert variant="danger"
+ isInline
+ actionClose={<AlertActionCloseButton onClose={() => this.setState({ error: undefined })} />}
+ title={_("Failed to fetch logs")}>
+ {this.state.error}
+ </Alert>
+ );
+
+ if (!this.state.logs.length && this.state.loading)
+ return (
+ <>
+ {error}
+ <EmptyStatePanel loading title={_("Loading...")} />
+ </>
+ );
+
+ /* Journalctl command stream finished and there are not more entries to query */
+ if (!this.state.logs.length && !this.state.didntReachStart && this.state.streamFinished) {
+ return (
+ <div id="start-box" className="journal-start">
+ {error}
+ <EmptyStatePanel action={_("Clear all filters")}
+ icon={ExclamationCircleIcon}
+ onAction={() => cockpit.location.go('/')}
+ paragraph={_("Can not find any logs using the current combination of filters.")}
+ title={_("No logs found")}
+ loading={false} />
+ </div>
+ );
+ }
+ const loadEarlier = (
+ /* Show 'Load earlier entries' button if we didn't reach start yet */
+ this.state.didntReachStart
+ ? <EmptyStatePanel action={_("Load earlier entries")}
+ actionInProgressText={_("Loading earlier entries")}
+ icon={noLogs ? ExclamationCircleIcon : undefined}
+ isActionInProgress={this.state.loading}
+ onAction={() => {
+ let count = 0;
+ this.setState({ loading: true });
+
+ const journalctlOptions = {
+ cursor: this.state.cursor,
+ follow: false,
+ grep,
+ priority,
+ reverse: true,
+ };
+ this.setState({ didntReachStart: false });
+ const promise = journal.journalctl(this.match, journalctlOptions)
+ .fail(this.queryError)
+ .stream(entries => {
+ if (entries[0].__CURSOR == this.state.cursor)
+ entries.shift();
+ count += entries.length;
+ this.appendEntries(entries);
+ if (count >= QUERY_MORE) {
+ const stopped = entries[entries.length - 1].__CURSOR;
+ this.setState({ didntReachStart: true, cursor: stopped, loading: false });
+ promise.stop();
+ }
+ })
+ .done(() => {
+ this.setState({ streamFinished: true, loading: false });
+ });
+ this.procs.push(promise);
+ }}
+ paragraph={noLogs ? _("You may try to load older entries.") : ""}
+ title={noLogs ? _("No logs found") : ""}
+ loading={false} />
+ : null
+ );
+
+ return (
+ <>
+ {error}
+ {this.state.logs.length
+ ? <div id="journal-logs" className="panel panel-default cockpit-log-panel" role="table">
+ {this.state.logs}
+ </div>
+ : null}
+ <div id="start-box" className="journal-start">
+ {loadEarlier}
+ </div>
+ </>
+ );
+ }
+}
diff --git a/pkg/systemd/manifest.json b/pkg/systemd/manifest.json
new file mode 100644
index 0000000..4f8ad90
--- /dev/null
+++ b/pkg/systemd/manifest.json
@@ -0,0 +1,88 @@
+{
+ "name": "system",
+
+ "requires": {
+ "cockpit": "265"
+ },
+
+ "menu": {
+ "index": {
+ "label": "Overview",
+ "order": 10,
+ "docs": [
+ {
+ "label": "Configuring system settings",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/getting-started-with-the-rhel-8-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["time", "date", "restart", "shut", "domain", "machine", "operating system", "os", "asset tag", "ssh", "power", "version", "host"]
+ },
+ {
+ "matches": ["hardware", "mitigation", "pci", "memory", "cpu", "bios", "ram", "dimm", "serial"],
+ "goto": "/system/hwinfo"
+ },
+ {
+ "matches": ["graphs", "metrics", "history", "pcp", "cpu", "memory", "disks", "network", "cgroups", "performance"],
+ "goto": "/metrics"
+ }
+ ]
+ },
+ "services": {
+ "label": "Services",
+ "order": 100,
+ "docs": [
+ {
+ "label": "Managing services",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-services-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["service", "systemd", "target", "socket", "timer", "path", "unit", "systemctl"]
+ },
+ {
+ "matches": ["boot", "mask", "unmask", "restart", "enable", "disable"],
+ "weight": 1
+ }
+ ]
+ },
+ "logs": {
+ "label": "Logs",
+ "order": 20,
+ "docs": [
+ {
+ "label": "Reviewing logs",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/reviewing-logs_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["journal", "warning", "error", "debug"]
+ },
+ {
+ "matches": ["abrt", "crash", "coredump"],
+ "goto": "?tag=abrt-notification"
+ }
+ ]
+ }
+ },
+
+ "tools": {
+ "terminal": {
+ "label": "Terminal",
+ "keywords": [
+ {
+ "matches": ["console", "command", "bash", "shell"]
+ }
+ ]
+ }
+ },
+
+ "libexecdir": "${libexecdir}",
+
+ "preload": [ "index", "services" ],
+
+ "content-security-policy": "img-src 'self' data:"
+}
diff --git a/pkg/systemd/overview-cards/configurationCard.jsx b/pkg/systemd/overview-cards/configurationCard.jsx
new file mode 100644
index 0000000..85f9c84
--- /dev/null
+++ b/pkg/systemd/overview-cards/configurationCard.jsx
@@ -0,0 +1,306 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React, { useState } from 'react';
+import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Form, FormGroup, FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { HelperText, HelperTextItem, } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+import host_keys_script from "./ssh-list-host-keys.sh";
+import cockpit from "cockpit";
+import { superuser } from "superuser";
+import { useObject, useEvent } from "hooks.js";
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { ServerTimeConfig } from 'serverTime.js';
+import { RealmdClient, RealmButton } from "./realmd.jsx";
+import { TunedPerformanceProfile } from './tuned-dialog.jsx';
+import { CryptoPolicyRow } from './cryptoPolicies.jsx';
+import { useDialogs } from "dialogs.jsx";
+import { useInit } from "hooks";
+
+import "./configurationCard.scss";
+
+const _ = cockpit.gettext;
+
+export const ConfigurationCard = ({ hostname }) => {
+ const Dialogs = useDialogs();
+ const realmd_client = useObject(() => new RealmdClient(), null, []);
+ useEvent(realmd_client, "changed");
+
+ const hostname_button = (superuser.allowed && realmd_client.allowHostnameChange())
+ ? (
+ <Button id="system_information_hostname_button" variant="link"
+ onClick={() => Dialogs.show(<PageSystemInformationChangeHostname />)}
+ isInline aria-label="edit hostname">
+ {hostname !== "" ? _("edit") : _("Set hostname")}
+ </Button>)
+ : null;
+
+ return (
+ <>
+ <Card className="system-configuration">
+ <CardTitle>{_("Configuration")}</CardTitle>
+ <CardBody>
+ <table className="pf-v5-c-table pf-m-grid-md pf-m-compact">
+ <tbody className="pf-v5-c-table__tbody">
+ <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th" scope="row">{_("Hostname")}</th>
+ <td className="pf-v5-c-table__td">
+ {hostname && <span id="system_information_hostname_text">{hostname}</span>}
+ <span>{hostname_button}</span>
+ </td>
+ </tr>
+
+ <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th" scope="row">{_("System time")}</th>
+ <td className="pf-v5-c-table__td"><ServerTimeConfig /></td>
+ </tr>
+
+ <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th" scope="row">{_("Domain")}</th>
+ <td className="pf-v5-c-table__td"><RealmButton realmd_client={realmd_client} /></td>
+ </tr>
+
+ <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th" scope="row">{_("Performance profile")}</th>
+ <td className="pf-v5-c-table__td"><TunedPerformanceProfile /></td>
+ </tr>
+
+ <CryptoPolicyRow />
+
+ <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th" scope="row">{_("Secure shell keys")}</th>
+ <td className="pf-v5-c-table__td">
+ <Button variant="link" isInline id="system-ssh-keys-link"
+ onClick={() => Dialogs.show(<SystemInformationSshKeys />)}>
+ {_("Show fingerprints")}
+ </Button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </CardBody>
+ </Card>
+ </>
+ );
+};
+
+const SystemInformationSshKeys = () => {
+ const Dialogs = useDialogs();
+ const [keys, setKeys] = useState([]);
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(true);
+
+ function keysUpdate() {
+ cockpit.script(host_keys_script, [], { superuser: "try", err: "message" })
+ .then(data => {
+ const seen = {};
+ const keys = {};
+
+ data.trim().split("\n")
+ .forEach(line => {
+ if (!line)
+ return;
+
+ const parts = line.trim().split(" ");
+ const fp = parts[1];
+ if (!seen[fp]) {
+ seen[fp] = fp;
+ let title = parts[parts.length - 1];
+ if (title) {
+ const m = title.match(/^\((.*)\)$/);
+ if (m && m[1])
+ title = m[1];
+ }
+ if (!keys[title])
+ keys[title] = [];
+ keys[title].push(fp);
+ }
+ });
+
+ let arr = Object.keys(keys);
+ arr.sort();
+ arr = arr.map(function(k) {
+ return { title: k, fps: keys[k] };
+ });
+
+ setKeys(arr);
+ setLoading(false);
+ setError("");
+ })
+ .catch(function(ex) {
+ setLoading(false);
+ setError(cockpit.format(_("failed to list ssh host keys: $0"), ex.message));
+ });
+ }
+
+ function create_keyUpdater() {
+ /*
+ * Yes, we do refresh the keys while the dialog is open.
+ * It may occur that sshd is not running at the point when
+ * we try, or in rare cases the keys may change.
+ */
+ keysUpdate();
+ return window.setInterval(keysUpdate, 10 * 1000);
+ }
+
+ function destroy_keyUpdater(interval) {
+ window.clearInterval(interval);
+ }
+
+ useObject(create_keyUpdater, destroy_keyUpdater, []);
+
+ let body = null;
+ if (error)
+ body = <Alert variant='danger' isInline title={_("Loading of SSH keys failed")}>
+ <p>{_("Error message")}: {error}</p>
+ </Alert>;
+ else if (loading)
+ body = <EmptyStatePanel loading title={ _("Loading keys...") } />;
+ else if (!keys.length)
+ body = <EmptyStatePanel title={ _("No host keys found.") } />;
+ else
+ body = <List isPlain isBordered>
+ {keys.map(key =>
+ <ListItem key={key.title}>
+ <h4>{key.title}</h4>
+ {key.fps.map((fp, i) => <div key={i}><small>{fp}</small></div>)}
+ </ListItem>
+ )}
+ </List>;
+
+ return (
+ <Modal isOpen position="top" variant="medium"
+ onClose={Dialogs.close}
+ id="system_information_ssh_keys"
+ title={_("Machine SSH key fingerprints")}
+ footer={<>
+ <Button variant='secondary' onClick={Dialogs.close}>{_("Close")}</Button>
+ </>}
+ >
+ {body}
+ </Modal>
+ );
+};
+
+const PageSystemInformationChangeHostname = () => {
+ const Dialogs = useDialogs();
+ const [update_from_pretty, set_update_from_pretty] = useState(true);
+ const [init_hostname, set_init_hostname] = useState("");
+ const [hostname, set_hostname] = useState("");
+ const [proxy, set_proxy] = useState(null);
+ const [pretty, set_pretty] = useState("");
+ const [init_pretty, set_init_pretty] = useState("");
+ const [error, set_error] = useState([]);
+
+ useInit(() => {
+ const client = cockpit.dbus('org.freedesktop.hostname1', { superuser: "try" });
+ const hostname_proxy = client.proxy();
+
+ hostname_proxy.wait()
+ .then(() => {
+ const initial_hostname = hostname_proxy.StaticHostname || "";
+ const initial_pretty_hostname = hostname_proxy.PrettyHostname || "";
+
+ set_proxy(hostname_proxy);
+ set_hostname(initial_hostname);
+ set_init_hostname(initial_hostname);
+ set_pretty(initial_pretty_hostname);
+ set_init_pretty(initial_pretty_hostname);
+ });
+ });
+
+ function onPrettyChanged(value) {
+ // Whenever the pretty host name has changed (e.g. the user has edited it), we compute a new
+ // simple host name (e.g. 7bit ASCII, no special chars/spaces, lower case) from it
+
+ set_pretty(value);
+
+ if (update_from_pretty) {
+ const old_hostname = hostname;
+ const first_dot = old_hostname.indexOf(".");
+ let new_hostname = value
+ .toLowerCase()
+ .replace(/['".]+/g, "")
+ .replace(/[^a-zA-Z0-9]+/g, "-");
+ new_hostname = new_hostname.substr(0, 64);
+ if (first_dot >= 0)
+ new_hostname = new_hostname + old_hostname.substr(first_dot);
+ set_hostname(new_hostname);
+ }
+ }
+
+ function onHostnameChanged(value) {
+ const error = [];
+ if (value.length > 64)
+ error.push(_("Real host name must be 64 characters or less"));
+ if (value.match(/[.a-z0-9-]*/)[0] !== value || value.indexOf("..") !== -1)
+ error.push(_("Real host name can only contain lower-case characters, digits, dashes, and periods (with populated subdomains)"));
+
+ set_hostname(value);
+ set_update_from_pretty(false);
+ set_error(error);
+ }
+
+ function onSubmit(event) {
+ const one = proxy.call("SetStaticHostname", [hostname, true]);
+ const two = proxy.call("SetPrettyHostname", [pretty, true]);
+
+ Promise.all([one, two]).then(Dialogs.close);
+
+ if (event)
+ event.preventDefault();
+ return false;
+ }
+
+ const disabled = error.length || (init_hostname == hostname && init_pretty == pretty);
+ return (
+ <Modal isOpen position="top" variant="medium"
+ onClose={Dialogs.close}
+ id="system_information_change_hostname"
+ title={_("Change host name")}
+ footer={<>
+ <Button variant='primary' isDisabled={disabled} onClick={onSubmit}>{_("Change")}</Button>
+ <Button variant='link' onClick={Dialogs.close}>{_("Cancel")}</Button>
+ </>}
+ >
+ <Form isHorizontal onSubmit={onSubmit}>
+ <FormGroup fieldId="sich-pretty-hostname" label={_("Pretty host name")}>
+ <TextInput id="sich-pretty-hostname" value={pretty} onChange={(_event, value) => onPrettyChanged(value)} />
+ </FormGroup>
+ <FormGroup fieldId="sich-hostname" label={_("Real host name")}>
+ <TextInput id="sich-hostname" value={hostname} onChange={(_event, value) => onHostnameChanged(value)} validated={error.length ? "error" : "default"} />
+ {error.length > 0 && <FormHelperText>
+ <HelperText>
+ {error.map((err, i) =>
+ <HelperTextItem key={i} variant="error">
+ {err}
+ </HelperTextItem>
+ )}
+ </HelperText>
+ </FormHelperText>}
+ </FormGroup>
+ </Form>
+ </Modal>
+ );
+};
diff --git a/pkg/systemd/overview-cards/configurationCard.scss b/pkg/systemd/overview-cards/configurationCard.scss
new file mode 100644
index 0000000..43bf683
--- /dev/null
+++ b/pkg/systemd/overview-cards/configurationCard.scss
@@ -0,0 +1,14 @@
+#system_information_hostname_text + span {
+ font: inherit;
+ margin-inline-start: 0.5em;
+}
+
+#systime-time-hours,
+#systime-time-minutes {
+ display: inline;
+ inline-size: 3em;
+}
+
+.system-configuration .pf-v5-c-button.pf-m-link:disabled {
+ color: inherit;
+}
diff --git a/pkg/systemd/overview-cards/cryptoPolicies.jsx b/pkg/systemd/overview-cards/cryptoPolicies.jsx
new file mode 100644
index 0000000..0b07c6f
--- /dev/null
+++ b/pkg/systemd/overview-cards/cryptoPolicies.jsx
@@ -0,0 +1,245 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React, { useState, useEffect } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { ExclamationTriangleIcon, ExternalLinkSquareAltIcon, HelpIcon } from '@patternfly/react-icons';
+
+import { ModalError } from 'cockpit-components-inline-notification.jsx';
+import { PrivilegedButton } from "cockpit-components-privileged.jsx";
+import { ProfilesMenuDialogBody } from "./profiles-menu-dialog-body.jsx";
+import { useDialogs } from "dialogs.jsx";
+
+import "./cryptoPolicies.scss";
+
+const _ = cockpit.gettext;
+
+const displayProfileText = profile => profile === "DEFAULT" ? _("Default") : profile;
+const isInconsistentPolicy = (policy, fipsEnabled) => policy === "FIPS" !== fipsEnabled;
+
+export const CryptoPolicyRow = () => {
+ const Dialogs = useDialogs();
+ const [currentCryptoPolicy, setCurrentCryptoPolicy] = useState(null);
+ const [fipsEnabled, setFipsEnabled] = useState(null);
+ const [shaSubPolicyAvailable, setShaSubPolicyAvailable] = useState(null);
+
+ useEffect(() => {
+ cockpit.file("/proc/sys/crypto/fips_enabled").read()
+ .then(content => setFipsEnabled(content ? content.trim() === "1" : false));
+ cockpit.file("/etc/crypto-policies/state/current")
+ .watch(content => setCurrentCryptoPolicy(content ? content.trim() : null));
+ // RHEL-8-8 has no SHA1 subpolicy
+ cockpit.file("/usr/share/crypto-policies/policies/modules/SHA1.pmod").read()
+ .then(content => setShaSubPolicyAvailable(content ? content.trim() : false));
+ }, []);
+
+ if (!currentCryptoPolicy) {
+ return null;
+ }
+
+ return (
+ <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th" scope="row">{_("Cryptographic policy")}</th>
+ <td className="pf-v5-c-table__td">
+ <PrivilegedButton variant="link" buttonId="crypto-policy-button" tooltipId="tip-crypto-policy"
+ excuse={ _("The user $0 is not permitted to change cryptographic policies") }
+ onClick={() => Dialogs.show(<CryptoPolicyDialog
+ currentCryptoPolicy={currentCryptoPolicy}
+ setCurrentCryptoPolicy={setCurrentCryptoPolicy}
+ fipsEnabled={fipsEnabled}
+ shaSubPolicyAvailable={shaSubPolicyAvailable} />)}>
+ {displayProfileText(currentCryptoPolicy)}
+ </PrivilegedButton>
+ </td>
+ </tr>
+ );
+};
+
+const setPolicy = (policy, setError, setInProgress) => {
+ setInProgress(true);
+
+ let promise;
+ if (policy === "FIPS") {
+ promise = cockpit.spawn(["fips-mode-setup", "--enable"], { superuser: "require", err: "message" });
+ } else {
+ promise = cockpit.spawn(["fips-mode-setup", "--disable"], { superuser: "require", err: "message" }).then(() =>
+ cockpit.spawn(["update-crypto-policies", "--set", policy], { superuser: "require", err: "message" }));
+ }
+
+ promise.then(() => cockpit.spawn(["shutdown", "--reboot", "now"], { superuser: "require", err: "message" }))
+ .catch(error => setError(error))
+ .finally(() => setInProgress(false));
+};
+
+const CryptoPolicyDialog = ({
+ currentCryptoPolicy,
+ fipsEnabled,
+ reApply,
+ shaSubPolicyAvailable,
+}) => {
+ const Dialogs = useDialogs();
+ const [error, setError] = useState();
+ const [inProgress, setInProgress] = useState(false);
+ const [selected, setSelected] = useState(currentCryptoPolicy);
+
+ // Found in /usr/share/crypto-policies/policies/
+ const cryptopolicies = {
+ DEFAULT: _("Recommended, secure settings for current threat models."),
+ "DEFAULT:SHA1": _("DEFAULT with SHA-1 signature verification allowed."),
+ LEGACY: _("Higher interoperability at the cost of an increased attack surface."),
+ "LEGACY:AD-SUPPORT": _("LEGACY with Active Directory interoperability."),
+ FIPS: (<Flex alignItems={{ default: 'alignItemsCenter' }}>
+ {_("Only use approved and allowed algorithms when booting in FIPS mode.")}
+ <Button component='a'
+ rel="noopener noreferrer" target="_blank"
+ variant='link'
+ isInline
+ icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
+ href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/security_hardening/assembly_installing-a-rhel-8-system-with-fips-mode-enabled_security-hardening">
+ {_("Learn more")}
+ </Button>
+ </Flex>),
+ "FIPS:OSPP": _("FIPS with further Common Criteria restrictions."),
+ FUTURE: _("Protects from anticipated near-term future attacks at the expense of interoperability."),
+ };
+
+ const policies = Object.keys(cryptopolicies)
+ .filter(pol => pol.endsWith(':SHA1') ? shaSubPolicyAvailable : true)
+ .map(policy => ({
+ name: policy,
+ title: displayProfileText(policy),
+ description: cryptopolicies[policy],
+ active: !isInconsistentPolicy(policy, fipsEnabled) && policy === currentCryptoPolicy,
+ inconsistent: isInconsistentPolicy(policy, fipsEnabled) && policy === currentCryptoPolicy,
+ recommended: policy === 'DEFAULT',
+ }));
+
+ // Custom profile
+ if (!(currentCryptoPolicy in cryptopolicies)) {
+ policies.push({
+ name: currentCryptoPolicy,
+ title: displayProfileText(currentCryptoPolicy),
+ description: _("Custom cryptographic policy"),
+ active: !isInconsistentPolicy(currentCryptoPolicy, fipsEnabled),
+ inconsistent: isInconsistentPolicy(currentCryptoPolicy, fipsEnabled),
+ recommended: false,
+ });
+ }
+
+ const help = (
+ <Popover
+ id="crypto-policies-help"
+ bodyContent={
+ <div>
+ {_("Cryptographic Policies is a system component that configures the core cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos protocols.")}
+ </div>
+ }
+ footerContent={
+ <Button component='a'
+ rel="noopener noreferrer" target="_blank"
+ variant='link'
+ isInline
+ icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
+ href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening">
+ {_("Learn more")}
+ </Button>
+ }
+ >
+ <Button variant="plain" aria-label={_("Help")}>
+ <HelpIcon />
+ </Button>
+ </Popover>
+ );
+
+ return (
+ <Modal position="top" variant="medium"
+ className="ct-m-stretch-body"
+ isOpen
+ help={help}
+ onClose={Dialogs.close}
+ id="crypto-policy-dialog"
+ title={_("Change cryptographic policy")}
+ footer={
+ <>
+ {inProgress &&
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ {_("Applying new policy... This may take a few minutes.")}
+ </Flex>}
+ <Button id="crypto-policy-save-reboot" variant='primary' onClick={() => setPolicy(selected, setError, setInProgress)}
+ isDisabled={inProgress} isLoading={inProgress}
+ >
+ {reApply ? _("Reapply and reboot") : _("Apply and reboot")}
+ </Button>
+ <Button variant='link' onClick={Dialogs.close} isDisabled={inProgress}>
+ {_("Cancel")}
+ </Button>
+ </>
+ }
+ >
+ {error && <ModalError dialogError={typeof error == 'string' ? error : error.message} />}
+ {currentCryptoPolicy && <ProfilesMenuDialogBody active_profile={currentCryptoPolicy}
+ change_selected={setSelected}
+ isDisabled={inProgress}
+ profiles={policies} />}
+ </Modal>
+ );
+};
+
+export const CryptoPolicyStatus = () => {
+ const Dialogs = useDialogs();
+ const [currentCryptoPolicy, setCurrentCryptoPolicy] = useState(null);
+ const [fipsEnabled, setFipsEnabled] = useState(null);
+
+ useEffect(() => {
+ if (currentCryptoPolicy === null) {
+ cockpit.file("/etc/crypto-policies/state/current")
+ .watch(content => setCurrentCryptoPolicy(content ? content.trim().split(':', 1)[0] : undefined));
+ }
+
+ cockpit.file("/proc/sys/crypto/fips_enabled").read()
+ .then(content => setFipsEnabled(content ? content.trim() === "1" : false));
+ }, [currentCryptoPolicy]);
+
+ if (isInconsistentPolicy(currentCryptoPolicy, fipsEnabled)) {
+ return (
+ <li className="system-health-crypto-policies">
+ <Flex spacer={{ default: 'spaceItemsSm' }} flexWrap={{ default: 'nowrap' }}>
+ <FlexItem><ExclamationTriangleIcon className="crypto-policies-health-card-icon" /></FlexItem>
+ <div>
+ <div id="inconsistent_crypto_policy">
+ {currentCryptoPolicy === "FIPS" ? _("FIPS is not properly enabled") : _("Cryptographic policy is inconsistent")}
+ </div>
+ <Button isInline variant="link" className="pf-v5-u-font-size-sm"
+ onClick={() => Dialogs.show(<CryptoPolicyDialog currentCryptoPolicy={currentCryptoPolicy}
+ fipsEnabled={fipsEnabled}
+ reApply />)}>
+ {_("Review cryptographic policy")}
+ </Button>
+ </div>
+ </Flex>
+ </li>
+ );
+ }
+
+ return null;
+};
diff --git a/pkg/systemd/overview-cards/cryptoPolicies.scss b/pkg/systemd/overview-cards/cryptoPolicies.scss
new file mode 100644
index 0000000..148e393
--- /dev/null
+++ b/pkg/systemd/overview-cards/cryptoPolicies.scss
@@ -0,0 +1,3 @@
+.crypto-policies-health-card-icon {
+ color: var(--pf-v5-global--Color--200);
+}
diff --git a/pkg/systemd/overview-cards/healthCard.jsx b/pkg/systemd/overview-cards/healthCard.jsx
new file mode 100644
index 0000000..4e9c5c6
--- /dev/null
+++ b/pkg/systemd/overview-cards/healthCard.jsx
@@ -0,0 +1,47 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { Card, CardBody, CardFooter, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+
+import cockpit from "cockpit";
+import { PageStatusNotifications } from "../page-status.jsx";
+import { InsightsStatus } from "./insights.jsx";
+import { ShutDownStatus } from "./shutdownStatus.jsx";
+import LastLogin from "./lastLogin.jsx";
+import { CryptoPolicyStatus } from "./cryptoPolicies.jsx";
+
+import "./healthCard.scss";
+
+const _ = cockpit.gettext;
+
+export const HealthCard = () =>
+ <Card className="system-health">
+ <CardTitle>{_("Health")}</CardTitle>
+ <CardBody>
+ <ul className="system-health-events">
+ <PageStatusNotifications />
+ <InsightsStatus />
+ <CryptoPolicyStatus />
+ <ShutDownStatus />
+ <LastLogin />
+ </ul>
+ </CardBody>
+ <CardFooter />
+ </Card>;
diff --git a/pkg/systemd/overview-cards/healthCard.scss b/pkg/systemd/overview-cards/healthCard.scss
new file mode 100644
index 0000000..0988652
--- /dev/null
+++ b/pkg/systemd/overview-cards/healthCard.scss
@@ -0,0 +1,29 @@
+.system-health {
+ &-events {
+ > li {
+ // Better align system health icons
+ display: flex;
+ // Align icons vertically to text
+ align-items: center;
+ justify-content: flex-start;
+
+ + li {
+ margin-block-start: var(--pf-v5-global--spacer--md);
+ }
+
+ .spinner {
+ margin-inline-start: 0;
+ }
+
+ .pf-v5-c-spinner {
+ // PF's spinner is 18px while the icons are 16px,
+ // so redefine it here to make text to the right line up
+ --pf-v5-c-spinner--m-md--diameter: 16px;
+ }
+
+ > :not(a):last-child {
+ flex: auto;
+ }
+ }
+ }
+}
diff --git a/pkg/systemd/overview-cards/insights-poll-hack.sh b/pkg/systemd/overview-cards/insights-poll-hack.sh
new file mode 100644
index 0000000..111d38d
--- /dev/null
+++ b/pkg/systemd/overview-cards/insights-poll-hack.sh
@@ -0,0 +1,44 @@
+#! /bin/sh
+
+# Poll until /var/lib/insights/insights-details.json is 5 minutes
+# older than /etc/insights-client/.lastupload, then exit.
+
+# Calling "insights-details --check-results" only returns results
+# corresponding to the most recent upload some time after the upload,
+# but we don't know when exactly. We assume that it will not take
+# more than 5 minutes. However, we also want the results as soon as
+# they are available so we poll a couple of time before the 5 minutes
+# are up.
+
+# We poll fast for the first minute, and then slow down. Also, there
+# is a absolute limit on how often we poll, in case something is wrong
+# with the time stamps (such as .lastupload being from 5 years in
+# future).
+
+set -eu
+
+details_out_of_date ()
+{
+ if ! [ -f /etc/insights-client/.lastupload ]; then
+ return 1
+ fi
+ if ! [ -f /var/lib/insights/insights-details.json ]; then
+ return 0
+ fi
+ last_upload=$(stat -c "%Y" /etc/insights-client/.lastupload)
+ details=$(stat -c "%Y" /var/lib/insights/insights-details.json)
+ [ $details -lt $(expr $last_upload + 300) ]
+}
+
+tries=0
+while [ $tries -lt 20 ] && details_out_of_date; do
+ # We let insights-client write to /dev/null so that it doesn't
+ # crash should our stdout be closed.
+ insights-client --check-results >/dev/null
+ if [ $tries -lt 5 ]; then
+ sleep 10
+ else
+ sleep 60
+ fi
+ tries=$(expr $tries + 1)
+done
diff --git a/pkg/systemd/overview-cards/insights.jsx b/pkg/systemd/overview-cards/insights.jsx
new file mode 100644
index 0000000..1d83777
--- /dev/null
+++ b/pkg/systemd/overview-cards/insights.jsx
@@ -0,0 +1,236 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { CheckIcon, ExclamationTriangleIcon, ExternalLinkAltIcon } from '@patternfly/react-icons';
+
+import cockpit from "cockpit";
+import * as service from "service.js";
+import { superuser } from "superuser";
+
+import insights_poll_hack_sh from "./insights-poll-hack.sh";
+
+import "./insights.scss";
+
+const _ = cockpit.gettext;
+
+const InsightsIcon = ({ critical, important, moderate, low }) => {
+ const vars = {
+ "--critical": critical,
+ "--important": important,
+ "--moderate": moderate,
+ "--low": low
+ };
+ return (
+ <span className="insights-icon" style={vars}>
+ <span className="insights-icon-critical" />
+ <span className="insights-icon-important" />
+ <span className="insights-icon-moderate" />
+ <span className="insights-icon-low" />
+ </span>);
+};
+
+export class InsightsStatus extends React.Component {
+ constructor() {
+ super();
+ this.state = { };
+
+ this.subman_supports_insights = (cockpit.manifests.subscriptions &&
+ cockpit.manifests.subscriptions.features &&
+ cockpit.manifests.subscriptions.features.insights);
+
+ this.insights_client_timer = service.proxy("insights-client.timer");
+ this.insights_client_timer.addEventListener("changed", () => this.setState({}));
+
+ superuser.addEventListener("changed", () => {
+ if (this.is_mounted)
+ this.setup_watches();
+ });
+ }
+
+ setup_watches() {
+ if (superuser.allowed == null)
+ return;
+
+ const watch = (name, state, parser) => {
+ return cockpit.file(name, { superuser: "try", syntax: { parse: parser } }).watch((data, tag, error) => {
+ if (error)
+ console.warn("Parse error", name, error.toString());
+ this.setState({ [state]: data });
+ });
+ };
+
+ if (this.id_watch)
+ this.id_watch.remove();
+
+ this.id_watch = watch("/var/lib/insights/host-details.json", "id",
+ data => JSON.parse(data).results[0].id);
+
+ if (this.hits_watch)
+ this.hits_watch.remove();
+
+ this.hits_watch = watch("/var/lib/insights/insights-details.json", "hits",
+ data => {
+ const json = JSON.parse(data);
+ const n_hits = json.length;
+ const n_hits_by_risk = [0, 0, 0, 0];
+ json.forEach(r => {
+ const risk_index =
+ Math.max(1, Math.min(5, Math.floor(r.rule.total_risk || 0))) - 1;
+ n_hits_by_risk[risk_index] += 1;
+ });
+ return {
+ n: n_hits,
+ n_by_risk: n_hits_by_risk
+ };
+ });
+
+ if (this.upload_watch)
+ this.upload_watch.remove();
+
+ // Let's try to keep the results up-to-date
+ this.upload_watch = cockpit.file("/etc/insights-client/.lastupload").watch(data => {
+ if (this.pollster) {
+ this.pollster.close();
+ this.pollster = null;
+ }
+ if (data)
+ this.pollster = cockpit.script(insights_poll_hack_sh, [], { superuser: true });
+ });
+ }
+
+ close_watches() {
+ if (this.id_watch)
+ this.id_watch.remove();
+ this.id_watch = null;
+
+ if (this.hits_watch)
+ this.hits_watch.remove();
+ this.hits_watch = null;
+
+ if (this.upload_watch)
+ this.upload_watch.remove();
+ this.upload_watch = null;
+ }
+
+ componentDidMount() {
+ this.is_mounted = true;
+ this.setup_watches();
+ }
+
+ componentWillUnmount() {
+ this.close_watches();
+ this.is_mounted = false;
+ }
+
+ render() {
+ if (!this.insights_client_timer) {
+ // Not mounted yet
+ return null;
+ }
+
+ if (!this.insights_client_timer.exists) {
+ // insights-client is not installed
+ return null;
+ }
+
+ if (!this.insights_client_timer.enabled) {
+ // machine is not registered with Insights
+ if (this.subman_supports_insights) {
+ // subscriptions page can register us
+ return (
+ <li className="system-health-insights">
+ <Flex flexWrap={{ default: 'nowrap' }} spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <ExclamationTriangleIcon className="ct-exclamation-triangle" />
+ <Button isInline variant="link" component="a" onClick={ev => { ev.preventDefault(); cockpit.jump("/subscriptions") }}>
+ {_("Not connected to Insights")}
+ </Button>
+ </Flex>
+ </li>
+ );
+ } else
+ return null;
+ }
+
+ let url;
+ if (this.state.id)
+ url = "https://console.redhat.com/insights/inventory/" + this.state.id;
+ else
+ url = "https://console.redhat.com/insights";
+
+ let icon, text;
+ if (this.state.hits) {
+ const n = this.state.hits.n;
+ if (n == 0) {
+ icon = <CheckIcon className="ct-check-circle" />;
+ text = _("No rule hits");
+ } else {
+ const by_risk = this.state.hits.n_by_risk;
+ icon = <InsightsIcon critical={by_risk[3]}
+ important={by_risk[2]}
+ moderate={by_risk[1]}
+ low={by_risk[0]} />;
+
+ // We do this all explicitly and in a long
+ // winded way so that the translation
+ // machinery gets to see all the strings.
+ if (by_risk[3]) {
+ text = cockpit.format(cockpit.ngettext("$0 critical hit",
+ "$0 hits, including critical",
+ n),
+ n);
+ } else if (by_risk[2]) {
+ text = cockpit.format(cockpit.ngettext("$0 important hit",
+ "$0 hits, including important",
+ n),
+ n);
+ } else if (by_risk[1]) {
+ text = cockpit.format(cockpit.ngettext("$0 moderate hit",
+ "$0 hits, including moderate",
+ n),
+ n);
+ } else {
+ text = cockpit.format(cockpit.ngettext("$0 low severity hit",
+ "$0 low severity hits",
+ n),
+ n);
+ }
+ }
+ } else {
+ // Couldn't parse the results at all, be quiet
+ return null;
+ }
+
+ return (
+ <li className="system-health-insights">
+ <Flex flexWrap={{ default: 'nowrap' }} spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ {icon}
+ <Button isInline variant="link" component='a' href={url}
+ target="_blank" rel="noopener noreferrer"
+ icon={<ExternalLinkAltIcon />}
+ iconPosition="right">
+ {_("Insights: ")} {text}
+ </Button>
+ </Flex>
+ </li>
+ );
+ }
+}
diff --git a/pkg/systemd/overview-cards/insights.scss b/pkg/systemd/overview-cards/insights.scss
new file mode 100644
index 0000000..d7d35c5
--- /dev/null
+++ b/pkg/systemd/overview-cards/insights.scss
@@ -0,0 +1,61 @@
+.insights-icon {
+ inline-size: auto;
+ block-size: 1.25rem;
+ flex-basis: 22px;
+ margin-inline-end: 0.5rem;
+ justify-self: center;
+
+ /* JavaScript-set numbers; default to 0 */
+ --critical: 0;
+ --important: 0;
+ --moderate: 0;
+ --low: 0;
+ /* Add the numbers together, making calculations easier later on */
+ --total: calc(var(--critical) + var(--important) + var(--moderate) + var(--low));
+ border-radius: 3px;
+ border: 1px solid var(--pf-v5-global--BorderColor--200);
+ /* Use flex here (with auto below) so everything always adds up properly */
+ display: flex;
+ flex-direction: column;
+ inline-size: 22px;
+ block-size: 22px;
+ /* Inner gap between colors and border */
+ padding: 1px;
+ /* The top border offset is provided by the border of the first block */
+ padding-block-start: 0;
+
+ > * {
+ /* If there's a value, set to 1px. Otherwise, divide by 0 and it becomes invalid */
+ --pixel-onoff: calc(var(--severity) / var(--severity) * 1px);
+ /* Hit severity is mapped to the --servity variable in blocks below. */
+ block-size: calc(var(--severity) / var(--total) * 100%);
+ /* Make it stretchy so the math always works out. ("Fudge factor") */
+ flex: auto;
+ /* Anti-alias the edges ever-so-slightly */
+ border-radius: 1px;
+ /* If there's a value, then there's a gap at top */
+ margin-block-start: var(--pixel-onoff);
+ /* If there's a value, always round height up to 1px minimum */
+ min-block-size: var(--pixel-onoff);
+ }
+
+ &-critical {
+ --severity: var(--critical);
+ background: #c9190b;
+ }
+
+ &-important {
+ --severity: var(--important);
+ background: #ec7a08;
+ }
+
+ &-moderate {
+ --severity: var(--moderate);
+ background: #f0ab00;
+ }
+
+ &-low {
+ --severity: var(--low);
+ background: #d2d2d2;
+ }
+}
diff --git a/pkg/systemd/overview-cards/lastLogin.jsx b/pkg/systemd/overview-cards/lastLogin.jsx
new file mode 100644
index 0000000..609e439
--- /dev/null
+++ b/pkg/systemd/overview-cards/lastLogin.jsx
@@ -0,0 +1,145 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React, { useState, useEffect } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { ExclamationTriangleIcon, UserIcon } from "@patternfly/react-icons";
+
+import * as timeformat from "timeformat";
+
+import cockpit from "cockpit";
+
+import './lastLogin.scss';
+
+const _ = cockpit.gettext;
+
+// Do the full combinatorial thing to improve translatability
+const generate_line = (host, line) => {
+ let message = "";
+ if (host && line) {
+ message = cockpit.format(_("from <host> on <terminal>", "from $0 on $1"), host, line);
+ } else if (host) {
+ message = cockpit.format(_("from <host>", "from $0"), host);
+ } else if (line) {
+ message = cockpit.format(_("on <terminal>", "on $0"), line);
+ }
+ return message;
+};
+
+const getFormattedDateTime = (time) => {
+ const now = new Date();
+ const date = new Date(time);
+ if (date.getFullYear() == now.getFullYear()) {
+ return timeformat.dateTimeNoYear(date);
+ }
+ return timeformat.dateTime(date);
+};
+
+const LastLogin = () => {
+ const [messages, setLoginMessages] = useState(null);
+ const [name, setName] = useState(null);
+
+ useEffect(() => {
+ if (messages === null) {
+ const bridge = cockpit.dbus(null, { bus: "internal" });
+ bridge.call("/LoginMessages", "cockpit.LoginMessages", "Get", [])
+ .then(reply => {
+ const obj = JSON.parse(reply[0]);
+ if (obj.version == 1) {
+ setLoginMessages(obj);
+ } else {
+ // empty reply is okay -- older bridges just don't send that information
+ if (obj.version !== undefined)
+ console.error("unknown login-messages:", reply[0]);
+ }
+ })
+ .catch(error => {
+ console.error("failed to fetch login messages:", error);
+ });
+ }
+ if (name === null) {
+ cockpit.user().then(user => setName(user.name));
+ }
+ }, [messages, name]);
+
+ if (messages === null || !messages['last-login-time']) {
+ return null;
+ }
+
+ let icon = null;
+ let headerText = null;
+ let underlineText = null;
+ let headerClass = "pf-v5-u-text-break-word";
+ let underlineClass = "pf-v5-u-text-break-word";
+ const lastLoginText = _("Last successful login:") + " " + getFormattedDateTime(messages['last-login-time'] * 1000);
+ const failedLogins = messages['fail-count'];
+
+ if (failedLogins) {
+ let iconClass = "system-information-failed-login-warning-icon";
+ if (failedLogins > 5) {
+ iconClass = "system-information-failed-login-danger-icon";
+ headerClass += " system-information-failed-login-danger";
+ } else {
+ headerClass += " system-information-failed-login-warning";
+ }
+
+ icon = <ExclamationTriangleIcon className={iconClass} />;
+ headerText = cockpit.format(cockpit.ngettext("$0 failed login attempt", "$0 failed login attempts", failedLogins), failedLogins);
+ underlineText = getFormattedDateTime(messages['last-login-time'] * 1000) + " " + generate_line(messages['last-login-host'], messages['last-login-line']);
+ } else {
+ icon = <UserIcon className="system-information-last-login-icon" />;
+ headerText = lastLoginText;
+ underlineClass += " ct-grey-text pf-v5-u-font-size-sm";
+ underlineText = generate_line(messages['last-login-host'], messages['last-login-line']);
+ }
+
+ return (
+ <li className="last-login" id="page_status_last_login">
+ <Flex spacer={{ default: 'spaceItemsSm' }} flexWrap={{ default: 'nowrap' }}>
+ <FlexItem>{icon}</FlexItem>
+ <div>
+ <div id="system_last_login"
+ className={headerClass}
+ >
+ {headerText}
+ </div>
+ <div id="system_last_login_from"
+ className={underlineClass}
+ >
+ {underlineText}
+ </div>
+ {failedLogins &&
+ <div id="system_last_login_success" className="pf-v5-u-text-break-word">
+ {lastLoginText}
+ </div>
+ }
+ {name &&
+ <Button variant="link" isInline
+ className="pf-v5-u-font-size-sm"
+ onClick={() => cockpit.jump("/users#/" + name)}>
+ {_("View login history")}
+ </Button>
+ }
+ </div>
+ </Flex>
+ </li>
+ );
+};
+
+export default LastLogin;
diff --git a/pkg/systemd/overview-cards/lastLogin.scss b/pkg/systemd/overview-cards/lastLogin.scss
new file mode 100644
index 0000000..a603931
--- /dev/null
+++ b/pkg/systemd/overview-cards/lastLogin.scss
@@ -0,0 +1,28 @@
+@import "global-variables";
+@import "@patternfly/patternfly/utilities/Text/text.scss";
+
+.system-information-failed-login-warning-icon {
+ color: var(--pf-v5-global--warning-color--100);
+}
+
+.system-information-failed-login-warning {
+ color: var(--pf-v5-global--warning-color--200);
+ font-weight: bold;
+}
+
+.system-information-failed-login-danger-icon {
+ color: var(--pf-v5-global--danger-color--100);
+}
+
+.system-information-failed-login-danger {
+ color: var(--pf-v5-global--danger-color--200);
+ font-weight: bold;
+}
+
+.system-information-last-login-icon {
+ color: var(--pf-v5-global--Color--200);
+}
+
+.ct-grey-text {
+ color: var(--pf-v5-global--Color--200);
+}
diff --git a/pkg/systemd/overview-cards/motdCard.jsx b/pkg/systemd/overview-cards/motdCard.jsx
new file mode 100644
index 0000000..c4e554a
--- /dev/null
+++ b/pkg/systemd/overview-cards/motdCard.jsx
@@ -0,0 +1,123 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState } from 'react';
+
+import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
+import { EditIcon } from '@patternfly/react-icons';
+import { ModalError } from 'cockpit-components-inline-notification.jsx';
+import { superuser } from "superuser";
+import { useDialogs } from "dialogs.jsx";
+import { useInit } from "hooks";
+
+import cockpit from "cockpit";
+
+import './motdCard.scss';
+
+const _ = cockpit.gettext;
+
+const MotdEditDialog = ({ text }) => {
+ const Dialogs = useDialogs();
+ const [value, setValue] = useState(text);
+ const [error, setError] = useState(null);
+ const [errorDetail, setErrorDetail] = useState(null);
+
+ return (
+ <Modal position="top"
+ variant="medium" isOpen
+ id="motd-box-edit-modal"
+ onClose={Dialogs.close}
+ title={_("Edit /etc/motd")}
+ footer={
+ <>
+ <Button variant='primary'
+ onClick={() => cockpit.file("/etc/motd", { superuser: "try", err: "message" })
+ .replace(value)
+ .then(Dialogs.close)
+ .catch(exc => {
+ setError(_("Failed to save changes in /etc/motd"));
+ setErrorDetail(exc.message);
+ })}>
+ {_("Save changes")}
+ </Button>
+ <Button variant='link'
+ onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>
+ }>
+
+ <Stack hasGutter>
+ {error &&
+ <ModalError dialogError={error}
+ dialogErrorDetail={errorDetail} />}
+ <TextArea resizeOrientation="vertical"
+ value={value}
+ onChange={(_event, value) => setValue(value)} />
+ </Stack>
+ </Modal>);
+};
+
+export const MotdCard = () => {
+ const Dialogs = useDialogs();
+ const [motdText, setMotdText] = useState("");
+ const [motdVisible, setMotdVisible] = useState(false);
+
+ useInit(() => {
+ cockpit.file("/etc/motd").watch(content => {
+ /* trim initial empty lines and trailing space, but keep initial spaces to not break ASCII art */
+ if (content)
+ content = content.trimRight().replace(/^\s*\n/, '');
+ if (content && content != cockpit.localStorage.getItem('dismissed-motd')) {
+ setMotdText(content);
+ setMotdVisible(true);
+ } else {
+ setMotdVisible(false);
+ }
+ });
+ });
+
+ function hideAlert() {
+ setMotdVisible(false);
+ cockpit.localStorage.setItem('dismissed-motd', motdText);
+ }
+
+ if (!motdVisible)
+ return null;
+
+ const actionClose = <>
+ {superuser.allowed &&
+ <Button variant="plain"
+ id="motd-box-edit"
+ onClick={() => Dialogs.show(<MotdEditDialog text={motdText} />)}
+ aria-label={_("Edit motd")}>
+ <EditIcon />
+ </Button>}
+ <AlertActionCloseButton onClose={hideAlert} />
+ </>;
+
+ return <Alert id="motd-box" isInline className="motd-box"
+ variant="custom"
+ title={<pre id="motd">{motdText}</pre>}
+ actionClose={actionClose} />;
+};
diff --git a/pkg/systemd/overview-cards/motdCard.scss b/pkg/systemd/overview-cards/motdCard.scss
new file mode 100644
index 0000000..8003d19
--- /dev/null
+++ b/pkg/systemd/overview-cards/motdCard.scss
@@ -0,0 +1,15 @@
+#motd {
+ --motd--font-size: var(--pf-v5-global--FontSize--sm);
+ background-color: transparent;
+ border: none;
+ font-size: var(--motd--font-size);
+ // Account for the difference in font sizes, so the text lines up
+ margin: 0;
+ margin-block-start: calc(var(--pf-v5-c-alert__title--FontSize) - var(--motd--font-size));
+ padding: 0;
+ white-space: pre-wrap;
+}
+
+#motd-box {
+ margin-block-end: 0;
+}
diff --git a/pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx b/pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx
new file mode 100644
index 0000000..1dbbd7c
--- /dev/null
+++ b/pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx
@@ -0,0 +1,87 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import PropTypes from "prop-types";
+
+import { Label, LabelGroup } from "@patternfly/react-core/dist/esm/components/Label/index.js";
+import { Menu, MenuContent, MenuItem, MenuList } from "@patternfly/react-core/dist/esm/components/Menu/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+
+import "menu-select-widget.scss";
+
+const _ = cockpit.gettext;
+
+/* dialog body with list of profiles
+ * Expected props:
+ * - active_profile (key of the active profile)
+ * - change_selected callback, called with profile name each time the selected entry changes
+ * - profiles (array of entries)
+ * - name (string, key)
+ * - recommended (boolean)
+ * - active (boolean)
+ * - title (string)
+ * - description (string)
+ */
+export const ProfilesMenuDialogBody = ({ active_profile, profiles, change_selected, isDisabled }) => {
+ const [selected_profile, setSelectedProfile] = React.useState(active_profile);
+
+ const menuProfiles = profiles.map((itm) => {
+ return (
+ <MenuItem itemId={itm.name} key={itm.name} data-value={itm.name}
+ description={itm.description} isDisabled={isDisabled}
+ isActive={itm.active}>
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <p>{ itm.title }</p>
+ <FlexItem>
+ <LabelGroup>
+ {itm.recommended && <Label color="green" variant='filled'>{_("recommended")}</Label>}
+ {itm.active && <Label color="blue" variant='filled'>{_("active")}</Label>}
+ {itm.inconsistent && <Label color="orange" variant='filled'>{_("inconsistent")}</Label>}
+ </LabelGroup>
+ </FlexItem>
+ </Flex>
+ </MenuItem>
+ );
+ });
+ return (
+ <Menu className="ct-menu-select-widget"
+ isPlain
+ isScrollable
+ onSelect={(_, selected) => {
+ setSelectedProfile(selected);
+ change_selected(selected);
+ }}
+ selected={selected_profile}>
+ <MenuContent>
+ <MenuList>
+ {menuProfiles}
+ </MenuList>
+ </MenuContent>
+ </Menu>
+ );
+};
+
+ProfilesMenuDialogBody.propTypes = {
+ active_profile: PropTypes.string.isRequired,
+ change_selected: PropTypes.func.isRequired,
+ profiles: PropTypes.array.isRequired,
+ isDisabled: PropTypes.bool,
+};
diff --git a/pkg/systemd/overview-cards/realmd.jsx b/pkg/systemd/overview-cards/realmd.jsx
new file mode 100644
index 0000000..2ed3af0
--- /dev/null
+++ b/pkg/systemd/overview-cards/realmd.jsx
@@ -0,0 +1,489 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useEffect } from 'react';
+
+import { Alert, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { CodeBlockCode } from "@patternfly/react-core/dist/esm/components/CodeBlock/index.js";
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { CheckIcon, ExclamationCircleIcon, InProgressIcon } from "@patternfly/react-icons";
+
+import cockpit from "cockpit";
+import { Privileged } from "cockpit-components-privileged.jsx";
+import { superuser } from "superuser";
+import { useEvent } from "hooks.js";
+import { FormHelper } from "cockpit-components-form-helper";
+import { install_dialog } from "cockpit-components-install-dialog.jsx";
+import * as packagekit from "packagekit.js";
+import { useDialogs } from "dialogs.jsx";
+
+import "./realmd.scss";
+
+const _ = cockpit.gettext;
+
+const MANAGER = "/org/freedesktop/realmd";
+const PROVIDER = "org.freedesktop.realmd.Provider";
+const KERBEROS = "org.freedesktop.realmd.Kerberos";
+const KERBEROS_MEMBERSHIP = "org.freedesktop.realmd.KerberosMembership";
+const REALM = "org.freedesktop.realmd.Realm";
+
+export class RealmdClient {
+ constructor() {
+ this.onClose = this.onClose.bind(this);
+ this.onRealmsChanged = this.onRealmsChanged.bind(this);
+ this.joined = [];
+ this.detected = null;
+ this.error = null;
+ this.install_realmd = false;
+ this.callSerial = 1;
+
+ cockpit.event_target(this);
+ this.initProxy();
+ superuser.addEventListener("changed", () => this.initProxy());
+ }
+
+ onClose(ev, options) {
+ if (options.problem === "not-found") {
+ // see if we can install it
+ packagekit.detect().then(exists => {
+ if (exists) {
+ this.error = _("Joining a domain requires installation of realmd");
+ this.install_realmd = true;
+ } else {
+ this.error = _("Cannot join a domain because realmd is not available on this system");
+ }
+ this.dispatchEvent("changed");
+ });
+ } else {
+ this.error = cockpit.message(options);
+ this.dispatchEvent("changed");
+ }
+ this.dbus_realmd.removeEventListener("close", this.onClose);
+ this.dbus_realmd.close();
+ this.dbus_realmd = null;
+ }
+
+ onRealmsChanged() {
+ this.joined = [];
+ for (const path in this.realms) {
+ const realm = this.realms[path];
+ if (realm.Configured)
+ this.joined.push(realm);
+ }
+
+ this.dispatchEvent("changed");
+ }
+
+ initProxy() {
+ // Ignore intermediate states of superuser.allowed to
+ // avoid initializing the proxy twice during page
+ // load. This is less wasteful and helps the tests avoid
+ // race conditions. We are guaranteed to see a real "true"
+ // or "false" value eventually.
+ //
+ if (superuser.allowed === null)
+ return;
+
+ if (this.dbus_realmd) {
+ this.dbus_realmd.removeEventListener("close", this.onClose);
+ this.dbus_realmd.close();
+ }
+
+ this.error = null;
+ this.dbus_realmd = cockpit.dbus("org.freedesktop.realmd", { superuser: "try" });
+ this.dbus_realmd.watch(MANAGER);
+ this.dbus_realmd.addEventListener("close", this.onClose);
+
+ this.realms = this.dbus_realmd.proxies(REALM, MANAGER);
+ this.realms.addEventListener("changed", this.onRealmsChanged);
+ }
+
+ checkRealm(name) {
+ return this.dbus_realmd.call(MANAGER, PROVIDER, "Discover", [name, {}])
+ .then(([relevance, realms]) => {
+ if (realms.length == 0)
+ return { result: false };
+
+ // the first realm
+ const path = realms[0];
+ const realm = this.dbus_realmd.proxy(REALM, path);
+ const kerberos_membership = this.dbus_realmd.proxy(KERBEROS_MEMBERSHIP, path);
+ return Promise.allSettled([realm.wait(), kerberos_membership.wait()])
+ .then(() => { return { result: true, realm, kerberos_membership } });
+ });
+ }
+
+ join(realm, kerberosMembership, user, password) {
+ const id = "cockpit-" + this.callSerial;
+ this.callSerial += 1;
+ let diagnostics = "";
+
+ const options = { operation: cockpit.variant('s', id) };
+ const diagnostics_sub = this.dbus_realmd.subscribe({ member: "Diagnostics" }, (path, iface, signal, args) => {
+ if (args[1] === id) {
+ diagnostics += args[0];
+ }
+ });
+
+ if (kerberosMembership.valid) {
+ const credentials = ["password", "administrator", cockpit.variant('(ss)', [user, password])];
+ return kerberosMembership.call("Join", [credentials, options])
+ .then(() => this.installWSCredentials(realm, user, password))
+ .catch(ex => {
+ if (ex.name == "org.freedesktop.realmd.Error.Cancelled")
+ return Promise.resolve();
+ ex.diagnostics = diagnostics;
+ return Promise.reject(ex);
+ })
+ .finally(() => diagnostics_sub.remove());
+ } else {
+ return Promise.reject(new Error(_("Joining this domain is not supported")));
+ }
+ }
+
+ leave(realm) {
+ return this.cleanupWSCredentials(realm)
+ .then(() => realm.Deconfigure({ operation: cockpit.variant('s', "cockpit-leave-domain") }));
+ }
+
+ installWSCredentials(realm, user, password) {
+ // skip this on remote ssh hosts, only set up ws hosts
+ if (cockpit.transport.host !== "localhost")
+ return true;
+
+ const server_sw = find_detail(realm, "server-software");
+ if (server_sw !== "ipa") {
+ console.log("installing ws credentials not supported for server software", server_sw);
+ return true;
+ }
+
+ const kerberos = this.dbus_realmd.proxy(KERBEROS, realm.path);
+ return kerberos.wait()
+ .then(() => {
+ const helper = cockpit.manifests.system.libexecdir + "/cockpit-certificate-helper";
+ const proc = cockpit.spawn([helper, "ipa", "request", kerberos.RealmName, user],
+ { superuser: "require", err: "message" });
+ proc.input(password);
+ proc.catch(ex => console.warn("Failed to run", helper, "ipa request:", ex.toString()));
+ return proc;
+ })
+ .catch(() => true); // no Kerberos domain? nevermind then
+ }
+
+ cleanupWSCredentials(realm) {
+ // skip this on remote ssh hosts, only set up ws hosts
+ if (cockpit.transport.host !== "localhost")
+ return Promise.resolve();
+
+ const server_sw = find_detail(realm, "server-software");
+ if (server_sw !== "ipa") {
+ console.log("cleaning up ws credentials not supported for server software", server_sw);
+ return Promise.resolve();
+ }
+
+ const kerberos = this.dbus_realmd.proxy(KERBEROS, realm.path);
+ return kerberos.wait()
+ .then(() => {
+ const helper = cockpit.manifests.system.libexecdir + "/cockpit-certificate-helper";
+ return cockpit.spawn([helper, "ipa", "cleanup", kerberos.RealmName],
+ { superuser: "require", err: "message" })
+ .catch(ex => {
+ console.log("Failed to clean up SPN from /etc/cockpit/krb5.keytab:", JSON.stringify(ex));
+ return true;
+ });
+ })
+ .catch(() => true); // no Kerberos domain? nevermind then
+ }
+
+ allowHostnameChange() {
+ return this.joined.length === 0;
+ }
+
+ installPackage() {
+ if (this.install_realmd) {
+ return install_dialog("realmd")
+ .then(() => {
+ this.install_realmd = false;
+ this.initProxy();
+ return true;
+ })
+ .catch(() => false); // dialog cancelled
+ }
+
+ return null;
+ }
+}
+
+/*
+ * The realmd dbus interface has an a(ss) Details
+ * property. Lookup the right value for the given
+ * field key.
+ */
+function find_detail(realm, field) {
+ let result = null;
+ if (realm && realm.Details) {
+ realm.Details.forEach(value => {
+ if (value[0] === field)
+ result = value[1];
+ });
+ }
+ return result;
+}
+
+const LeaveDialog = ({ realmd_client }) => {
+ const Dialogs = useDialogs();
+ const [expanded, setExpanded] = useState(false);
+ const [pending, setPending] = useState(false);
+ const [error, setError] = useState(null);
+ const realm = realmd_client.joined[0];
+
+ const onLeave = () => {
+ setPending(true);
+ realmd_client.leave(realm)
+ .then(Dialogs.close)
+ .catch(err => {
+ console.warn("Failed to leave domain:", err.toString());
+ setPending(false);
+ setError(err);
+ });
+ };
+
+ return (
+ <Modal id="realms-leave-dialog" isOpen position="top" variant="medium"
+ onClose={Dialogs.close}
+ footer={
+ <>
+ { error && <Alert variant="danger" isInline className="realms-op-error" title={error.toString()} /> }
+ <Button variant="secondary" isDisabled={pending} onClick={Dialogs.close}>{ _("Close") }</Button>
+ </>
+ }
+ title={ _("dialog-title", "Domain") }>
+ <DescriptionList isHorizontal>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Domain") }</DescriptionListTerm>
+ <DescriptionListDescription id="realms-op-info-domain">
+ { realm && realm.Name }
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Login format") }</DescriptionListTerm>
+ <DescriptionListDescription id="realms-op-info-login-format">{
+ realm && realm.LoginFormats && realm.LoginFormats.length > 0
+ ? realm.LoginFormats[0].replace("%U", "username")
+ : null
+ }</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Server software") }</DescriptionListTerm>
+ <DescriptionListDescription id="realms-op-info-server-sw">
+ { find_detail(realm, "server-software") }
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Client software") }</DescriptionListTerm>
+ <DescriptionListDescription id="realms-op-info-client-sw">
+ { find_detail(realm, "client-software") }
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+
+ <ExpandableSection toggleText={ _("Leave domain") } isExpanded={expanded}
+ onToggle={ e => setExpanded(e) }>
+ <Alert variant="warning" isInline
+ title={ realm && realm.Name ? cockpit.format(_("Leave $0"), realm.Name) : _("Leave domain") }
+ actionLinks={
+ <Button variant="danger" id="realms-op-leave" isDisabled={pending} onClick={onLeave}>{ _("Leave domain") }</Button>
+ }>
+ { _("After leaving the domain, only users with local credentials will be able to log into this machine. This may also affect other services as DNS resolution settings and the list of trusted CAs may change.") }
+ </Alert>
+ </ExpandableSection>
+ </Modal>);
+};
+
+let domainValidateTimeout;
+
+const JoinDialog = ({ realmd_client }) => {
+ const Dialogs = useDialogs();
+ const [pending, setPending] = useState(false);
+ const [address, setAddress] = useState("");
+ const [addressValid, setAddressValid] = useState(null); // success, error, unsupported, default (for pending check)
+ const [admin, setAdmin] = useState("");
+ const [adminPassword, setAdminPassword] = useState("");
+ const [realm, setRealm] = useState(null);
+ const [kerberosMembership, setKerberosMembership] = useState(null);
+ const [error, setError] = useState(null);
+ const [diagnosticsExpanded, setDiagnosticsExpanded] = useState(false);
+
+ const checkAddress = name => {
+ setAddressValid("default");
+
+ realmd_client.checkRealm(name)
+ .then(reply => {
+ if (reply.result) {
+ setRealm(reply.realm);
+
+ // handle initial auto-detection
+ if (name == "") {
+ if (!address)
+ setAddress(reply.realm.Name);
+ }
+
+ if (reply.kerberos_membership && reply.kerberos_membership.valid) {
+ setAddressValid("success");
+ if (!admin && reply.kerberos_membership.SuggestedAdministrator)
+ setAdmin(reply.kerberos_membership.SuggestedAdministrator);
+ setKerberosMembership(reply.kerberos_membership);
+ } else {
+ setAddressValid("unsupported");
+ }
+ } else {
+ // error_detect will not show the validation error, but trigger data-discover=done
+ setAddressValid(name ? "error" : "error_detect");
+ }
+ })
+ .catch(err => console.error("checkRealm failed", JSON.stringify(err)));
+ };
+
+ const validateAddress = value => {
+ setAddress(value);
+ setAddressValid(null);
+ window.clearTimeout(domainValidateTimeout);
+ if (value)
+ domainValidateTimeout = window.setTimeout(() => checkAddress(value), 1000);
+ };
+
+ const onJoin = () => {
+ setError(null);
+ setDiagnosticsExpanded(null);
+ setPending(true);
+ realmd_client.join(realm, kerberosMembership, admin, adminPassword)
+ .then(Dialogs.close)
+ .catch(err => {
+ setPending(false);
+ setError(err);
+ });
+ };
+
+ // initial auto-detection of domain name
+ useEffect(() => checkAddress(""), []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const join_disabled = pending || addressValid !== "success" || !admin || !kerberosMembership;
+
+ const DOMAIN_VALID_HELPER_TEXT = {
+ default: _("Validating address"),
+ success: _("Contacted domain"),
+ error: _("Domain could not be contacted"),
+ unsupported: _("Domain is not supported"),
+ };
+
+ const DOMAIN_VALID_HELPER_ICON = {
+ default: <InProgressIcon />,
+ success: <CheckIcon />,
+ error: <ExclamationCircleIcon />,
+ unsupported: <ExclamationCircleIcon />,
+ };
+
+ const domainHelperText = DOMAIN_VALID_HELPER_TEXT[addressValid];
+ const domainHelperIcon = DOMAIN_VALID_HELPER_ICON[addressValid];
+
+ let errorAlert;
+ if (error) {
+ const details = (error.diagnostics && diagnosticsExpanded)
+ ? <CodeBlockCode className="realms-op-diagnostics">{error.diagnostics}</CodeBlockCode>
+ : null;
+ const actionLink = (error.diagnostics && !diagnosticsExpanded)
+ ? <AlertActionLink onClick={ () => setDiagnosticsExpanded(true) }>{ _("Details") }</AlertActionLink>
+ : null;
+ errorAlert = <Alert variant="danger" isInline className="realms-op-error" actionLinks={actionLink} title={error.toString()}>{details}</Alert>;
+ }
+
+ return (
+ <Modal id="realms-join-dialog" isOpen position="top" variant="medium"
+ onClose={Dialogs.close}
+ footer={
+ <>
+ { errorAlert }
+ <Button variant="primary"
+ isDisabled={join_disabled}
+ onClick={onJoin}
+ isLoading={pending}
+ spinnerAriaValueText={ pending ? _("Joining") : null }>
+ { _("Join") }
+ </Button>
+ <Button variant="link" isDisabled={pending} onClick={Dialogs.close}>{ _("Cancel") }</Button>
+ { pending && <span className="realms-op-wait-message">{ _("This may take a while") }</span> }
+ </>
+ }
+ title={ _("dialog-title", "Join a domain") }>
+ <Form isHorizontal onSubmit={onJoin}>
+ <FormGroup label={ _("Domain address") } fieldId="realms-op-address" validated={addressValid}>
+ <TextInput id="realms-op-address" placeholder="domain.example.com"
+ data-discover={ (!addressValid || addressValid == "default") ? null : "done" }
+ value={address} onChange={(_event, value) => validateAddress(value)} isDisabled={pending} />
+ </FormGroup>
+
+ <FormGroup label={ _("Domain administrator name") } fieldId="realms-op-admin">
+ <TextInput id="realms-op-admin" placeholder="admin" value={admin} onChange={(_event, value) => setAdmin(value)} isDisabled={pending} />
+ </FormGroup>
+ <FormGroup label={ _("Domain administrator password") } fieldId="realms-op-admin-password">
+ <TextInput id="realms-op-admin-password" type="password" value={adminPassword} onChange={(_event, value) => setAdminPassword(value)} isDisabled={pending} />
+ </FormGroup>
+ <FormHelper fieldId="realms-op-address" helperText={domainHelperText} helperTextInvalid={addressValid == "error" && domainHelperText} icon={domainHelperIcon} />
+ </Form>
+ </Modal>);
+};
+
+export const RealmButton = ({ realmd_client }) => {
+ const Dialogs = useDialogs();
+ useEvent(realmd_client, "changed");
+ useEvent(superuser, "changed");
+
+ const buttonTooltip = superuser.allowed ? realmd_client.error : _("Not permitted to configure realms");
+ const buttonText = !realmd_client.install_realmd ? (realmd_client.joined.length ? realmd_client.joined.map(r => r.Name).join(", ") : _("Join domain")) : _("Install realmd support");
+ const buttonDisabled = !superuser.allowed || (realmd_client.error && !realmd_client.install_realmd);
+
+ const onClicked = () => {
+ // handle on-demand realmd package install
+ const install_promise = realmd_client.installPackage();
+ if (install_promise) {
+ // after installation, proceed to join dialog
+ install_promise.then(success => success && onClicked());
+ return null;
+ }
+ realmd_client.joined.length > 0
+ ? Dialogs.show(<LeaveDialog realmd_client={realmd_client} />)
+ : Dialogs.show(<JoinDialog realmd_client={realmd_client} />);
+ };
+
+ return (
+ <Privileged allowed={ superuser.allowed && !realmd_client.error }
+ tooltipId="system_information_domain_tooltip"
+ excuse={ buttonTooltip }>
+ <Button id="system_information_domain_button" variant="link"
+ onClick={onClicked}
+ isInline isDisabled={buttonDisabled} aria-label={buttonText}>
+ { buttonText }
+ </Button>
+ </Privileged>);
+};
diff --git a/pkg/systemd/overview-cards/realmd.scss b/pkg/systemd/overview-cards/realmd.scss
new file mode 100644
index 0000000..051d699
--- /dev/null
+++ b/pkg/systemd/overview-cards/realmd.scss
@@ -0,0 +1,13 @@
+.realms-op-diagnostics {
+ max-block-size: 200px;
+}
+
+.realms-op-wait-message {
+ margin-inline-start: 10px;
+ float: inline-end;
+ margin-block-start: 3px;
+}
+
+#realms-leave-dialog .pf-v5-c-expandable-section {
+ margin-block-start: var(--pf-v5-global--spacer--md);
+}
diff --git a/pkg/systemd/overview-cards/shutdownStatus.jsx b/pkg/systemd/overview-cards/shutdownStatus.jsx
new file mode 100644
index 0000000..16f9959
--- /dev/null
+++ b/pkg/systemd/overview-cards/shutdownStatus.jsx
@@ -0,0 +1,114 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React, { useState, useEffect } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { PowerOffIcon, RedoIcon } from "@patternfly/react-icons";
+
+import * as timeformat from "timeformat";
+
+import cockpit from "cockpit";
+
+import "./shutdownStatus.scss";
+
+const _ = cockpit.gettext;
+
+const getScheduledShutdown = (setShutdownTime, setShutdownType) => {
+ const client = cockpit.dbus("org.freedesktop.login1");
+ return client.call("/org/freedesktop/login1", "org.freedesktop.DBus.Properties", "Get",
+ ["org.freedesktop.login1.Manager", "ScheduledShutdown"], { type: "ss" })
+ .then(([result]) => {
+ setShutdownType(result.v[0]);
+ setShutdownTime(result.v[1]);
+ })
+ .catch(err => console.warn("Failed to get ScheduledShutdown property", err.toString()));
+};
+
+const cancelShutdownAction = () => {
+ const client = cockpit.dbus("org.freedesktop.login1", { superuser: "try" });
+ return client.call("/org/freedesktop/login1", "org.freedesktop.login1.Manager", "CancelScheduledShutdown")
+ .then(([cancelled]) => {
+ if (!cancelled) {
+ console.warn("Unable to cancel shutdown");
+ }
+ })
+ .catch(err => console.warn("Failed to cancel shutdown", err.toString()));
+};
+
+export const ShutDownStatus = () => {
+ const [shutdownType, setShutdownType] = useState(null);
+ const [shutdownTime, setShutdownTime] = useState(0);
+
+ useEffect(() => {
+ getScheduledShutdown(setShutdownTime, setShutdownType);
+ // logind does not have a propertieschanged mechanism https://github.com/systemd/systemd/issues/22244
+ cockpit.file("/run/systemd/shutdown/scheduled").watch(() => {
+ getScheduledShutdown(setShutdownTime, setShutdownType);
+ });
+ }, []);
+
+ // We only care about these two types
+ if (shutdownType !== "poweroff" && shutdownType !== "reboot") {
+ // don't log undefined
+ if (shutdownType !== null && shutdownType !== "") {
+ console.log(`unsupported shutdown type ${shutdownType}`);
+ }
+ return null;
+ }
+
+ const date = new Date(shutdownTime / 1000);
+ const now = new Date();
+ let displayDate = null;
+ if (date.getFullYear() == now.getFullYear()) {
+ displayDate = timeformat.dateTimeNoYear(date);
+ } else {
+ displayDate = timeformat.dateTime(date);
+ }
+
+ let text;
+ let cancelText;
+ let icon;
+ if (shutdownType === "poweroff") {
+ icon = <PowerOffIcon className="shutdown-status-poweroff-icon" />;
+ text = _("Scheduled poweroff at $0");
+ cancelText = _("Cancel poweroff");
+ } else {
+ icon = <RedoIcon className="reboot-status-poweroff-icon" />;
+ text = _("Scheduled reboot at $0");
+ cancelText = _("Cancel reboot");
+ }
+
+ return (
+ <li id="system-health-shutdown-status">
+ <Flex spacer={{ default: 'spaceItemsSm' }} flexWrap={{ default: 'nowrap' }}>
+ <FlexItem>{icon}</FlexItem>
+ <Flex id="system-health-shutdown-status-text" direction={{ default: 'column' }}>
+ {cockpit.format(text, displayDate)}
+ <FlexItem>
+ <Button variant="link" isInline
+ id="system-health-shutdown-status-cancel-btn"
+ className="pf-v5-u-font-size-sm"
+ onClick={cancelShutdownAction}>
+ {cancelText}
+ </Button>
+ </FlexItem>
+ </Flex>
+ </Flex>
+ </li>);
+};
diff --git a/pkg/systemd/overview-cards/shutdownStatus.scss b/pkg/systemd/overview-cards/shutdownStatus.scss
new file mode 100644
index 0000000..82fe3f6
--- /dev/null
+++ b/pkg/systemd/overview-cards/shutdownStatus.scss
@@ -0,0 +1,3 @@
+.shutdown-status-poweroff-icon, .reboot-status-poweroff-icon {
+ color: var(--pf-v5-global--Color--200);
+}
diff --git a/pkg/systemd/overview-cards/ssh-list-host-keys.sh b/pkg/systemd/overview-cards/ssh-list-host-keys.sh
new file mode 100644
index 0000000..56cf170
--- /dev/null
+++ b/pkg/systemd/overview-cards/ssh-list-host-keys.sh
@@ -0,0 +1,58 @@
+#!/bin/sh
+
+set -eu
+port="22"
+host="localhost"
+
+parse_addr() {
+ if [ -n "$1" ]; then
+ case "$1" in
+ *:*)
+ t="$1"
+ p=${t##*:}
+ h=${t%:*}
+ ;;
+ *)
+ p="$1"
+ h="localhost"
+ ;;
+ esac
+
+ # checks that port is an integer
+ if [ "$p" -eq "$p" ] 2>/dev/null; then
+ port="$p"
+ host="$h"
+ if [ "$host" = "[::]" ]; then
+ host="::"
+ fi
+ fi
+ fi
+}
+
+# Try to find where sshd might be listening
+
+# Check sshd_config, only works for root
+config=$(sshd -T | grep "listenaddress " | cut -d' ' -f2-)
+echo "$config" | while IFS='\n' read line; do
+ parse_addr "$line"
+done
+
+# Check with systemd
+systemd=$(systemctl show --property=Listen sshd.socket || true)
+echo "$systemd" | while IFS='=' read -r name value; do
+ if [ "$name" = "ListenStream" ]; then
+ parse_addr "$value"
+ fi
+done
+
+keys=$(ssh-keyscan -t dsa,ecdsa,ed25519,rsa -p "$port" "$host" || true)
+if [ -n "$keys" ]; then
+ # Some versions of ssh-keygen don't support -f reading from stdin
+ # so write a tmpfile
+ tmp=$(mktemp)
+ echo "$keys" > "$tmp"
+
+ # Not all ssh-keygen version support -E in those cases just output the default
+ (ssh-keygen -l -f "$tmp" -E md5 && ssh-keygen -l -f "$tmp" -E sha256) || ssh-keygen -l -f "$tmp"
+ rm "$tmp"
+fi
diff --git a/pkg/systemd/overview-cards/systemInformationCard.jsx b/pkg/systemd/overview-cards/systemInformationCard.jsx
new file mode 100644
index 0000000..a949d90
--- /dev/null
+++ b/pkg/systemd/overview-cards/systemInformationCard.jsx
@@ -0,0 +1,145 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardFooter, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+
+import cockpit from "cockpit";
+import * as machine_info from "machine-info.js";
+import * as timeformat from "timeformat.js";
+
+import "./systemInformationCard.scss";
+
+const _ = cockpit.gettext;
+
+export class SystemInformationCard extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ machineID: null,
+ model: null,
+ assetTag: null,
+ systemUptime: null,
+ };
+ this.getSystemUptime = this.getSystemUptime.bind(this);
+ }
+
+ componentDidMount() {
+ this.getDMIInfo();
+ this.getMachineId();
+ this.getSystemUptime();
+
+ this.uptimeTimer = setInterval(this.getSystemUptime, 60000);
+ }
+
+ componentWillUnmount() {
+ clearInterval(this.uptimeTimer); // not-covered: callers currently never unmount this
+ }
+
+ getMachineId() {
+ const machine_id = cockpit.file("/etc/machine-id");
+
+ machine_id.read()
+ .then(machineID => this.setState({ machineID }))
+ .catch(ex => console.error("Error reading machine id", ex.toString())) // not-covered: OS error
+ .finally(machine_id.close);
+ }
+
+ getDMIInfo() {
+ machine_info.dmi_info()
+ .then(fields => {
+ let vendor = fields.sys_vendor;
+ let name = fields.product_name;
+ if (!vendor || !name) {
+ vendor = fields.board_vendor;
+ name = fields.board_name;
+ }
+ if (!vendor || !name)
+ this.setState({ model: null });
+ else
+ this.setState({ model: vendor + " " + name });
+
+ this.setState({ assetTag: fields.product_serial || fields.chassis_serial });
+ })
+ .catch(ex => {
+ // try DeviceTree
+ machine_info.devicetree_info() // not-covered: coverage only runs on x86_64
+ .then(fields => this.setState({ assetTag: fields.serial, model: fields.model }))
+ .catch(dmiex => {
+ console.debug("couldn't read dmi info: " + ex.toString());
+ console.debug("couldn't read DeviceTree info: " + dmiex.toString());
+ this.setState({ assetTag: null, model: null });
+ });
+ });
+ }
+
+ getSystemUptime() {
+ cockpit.file("/proc/uptime").read()
+ .then(content => {
+ const uptime = parseFloat(content.split(' ')[0]);
+ const bootTime = new Date().valueOf() - uptime * 1000;
+ this.setState({ systemUptime: timeformat.distanceToNow(bootTime) });
+ })
+ .catch(ex => console.error("Error reading system uptime", ex.toString())); // not-covered: OS error
+ }
+
+ render() {
+ return (
+ <Card className="system-information">
+ <CardTitle>{_("System information")}</CardTitle>
+ <CardBody>
+ <table className="pf-v5-c-table pf-m-grid-md pf-m-compact">
+ <tbody className="pf-v5-c-table__tbody">
+ {this.state.model && <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th" scope="row">{_("Model")}</th>
+ <td className="pf-v5-c-table__td">
+ <div id="system_information_hardware_text">{this.state.model}</div>
+ </td>
+ </tr>}
+ {this.state.assetTag && <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th" scope="row">{_("Asset tag")}</th>
+ <td className="pf-v5-c-table__td">
+ <div id="system_information_asset_tag_text">{this.state.assetTag}</div>
+ </td>
+ </tr>}
+ <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th system-information-machine-id" scope="row">{_("Machine ID")}</th>
+ <td className="pf-v5-c-table__td">
+ <div id="system_machine_id">{this.state.machineID}</div>
+ </td>
+ </tr>
+ <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th system-information-uptime" scope="row">{_("Uptime")}</th>
+ <td className="pf-v5-c-table__td">
+ <div id="system_uptime">{this.state.systemUptime}</div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </CardBody>
+ <CardFooter>
+ <Button isInline variant="link" component="a" onClick={ev => { ev.preventDefault(); cockpit.jump("/system/hwinfo", cockpit.transport.host) }}>
+ {_("View hardware details")}
+ </Button>
+ </CardFooter>
+ </Card>
+ );
+ }
+}
diff --git a/pkg/systemd/overview-cards/systemInformationCard.scss b/pkg/systemd/overview-cards/systemInformationCard.scss
new file mode 100644
index 0000000..7d09aa0
--- /dev/null
+++ b/pkg/systemd/overview-cards/systemInformationCard.scss
@@ -0,0 +1,16 @@
+#system_machine_id {
+ overflow: visible;
+ white-space: normal;
+ // Some browsers don't support anywhere yet, so we provide break-word as a fallback
+ overflow-wrap: break-word;
+ overflow-wrap: anywhere;
+ // WebKit & Blink browsers don't support overflow-wrap for non-words (yet?),
+ // so use a nonstandard break-all in this special case
+ word-break: break-all;
+}
+
+.system-information {
+ &-machine-id {
+ white-space: nowrap;
+ }
+}
diff --git a/pkg/systemd/overview-cards/tuned-dialog.jsx b/pkg/systemd/overview-cards/tuned-dialog.jsx
new file mode 100644
index 0000000..97847dd
--- /dev/null
+++ b/pkg/systemd/overview-cards/tuned-dialog.jsx
@@ -0,0 +1,326 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React, { useState, useEffect, useCallback } from 'react';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+import { ExternalLinkSquareAltIcon, HelpIcon } from '@patternfly/react-icons';
+
+import * as service from "service";
+import { EmptyStatePanel } from 'cockpit-components-empty-state.jsx';
+import { ModalError } from 'cockpit-components-inline-notification.jsx';
+import { ProfilesMenuDialogBody } from './profiles-menu-dialog-body.jsx';
+import { superuser } from 'superuser';
+import { useObject, useEvent } from "hooks";
+import { useDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+export const TunedPerformanceProfile = () => {
+ const Dialogs = useDialogs();
+ const [btnText, setBtnText] = useState();
+ const [state, setState] = useState();
+ const [status, setStatus] = useState();
+
+ const tunedService = useObject(() => service.proxy("tuned.service"),
+ null,
+ []);
+ const tuned = useObject(() => cockpit.dbus("com.redhat.tuned", { superuser: "try" }),
+ obj => obj.close(),
+ [superuser.allowed, tunedService.state]);
+
+ const poll = useCallback(() => {
+ return Promise.all([
+ tuned.call('/Tuned', 'com.redhat.tuned.control', 'is_running', []),
+ tuned.call('/Tuned', 'com.redhat.tuned.control', 'active_profile', []),
+ tuned.call('/Tuned', 'com.redhat.tuned.control', 'recommend_profile', [])
+ ])
+ .then(([[is_running], [active_result], [recommended]]) => {
+ const active = is_running ? active_result : "none";
+ return ({ state: "running", active, recommended });
+ })
+ .catch((ex) => {
+ if (!tunedService.exists)
+ return ({ state: "not-installed" });
+ else if (tunedService.state != "running")
+ return ({ state: "not-running" });
+ else
+ return Promise.reject(ex);
+ });
+ }, [tunedService, tuned]);
+
+ const updateButton = useCallback(() => {
+ return poll()
+ .then(res => {
+ const { state, active, recommended } = res;
+ let status;
+
+ if (state == "not-installed")
+ status = _("Tuned is not available");
+ else if (state == "not-running")
+ status = _("Tuned is not running");
+ else if (active == "none")
+ status = _("Tuned is off");
+ else if (active == recommended)
+ status = _("This system is using the recommended profile");
+ else
+ status = _("This system is using a custom profile");
+
+ setBtnText(state == "running" ? active : _("none"));
+ setState(state);
+ setStatus(status);
+ })
+ .catch((ex) => {
+ console.warn("failed to poll tuned", ex);
+
+ setBtnText("error");
+ setStatus(_("Communication with tuned has failed"));
+ });
+ }, [poll, setBtnText, setState, setStatus]);
+
+ useEvent(superuser, "changed");
+ useEvent(tunedService, "changed", () => updateButton());
+
+ useEffect(() => {
+ updateButton();
+ }, [updateButton]);
+
+ const showDialog = () => {
+ Dialogs.show(<TunedDialog updateButton={updateButton}
+ poll={poll}
+ tunedDbus={tuned} tunedService={tunedService} />);
+ };
+
+ return (
+ <Tooltip id="tuned-status-tooltip" content={status}>
+ <Button id="tuned-status-button"
+ isAriaDisabled={btnText == "error" || state == "not-installed" || !superuser.allowed}
+ isInline
+ onClick={showDialog}
+ variant='link'>
+ {btnText}
+ </Button>
+ </Tooltip>
+ );
+};
+
+const TunedDialog = ({
+ updateButton,
+ poll,
+ tunedDbus,
+ tunedService,
+}) => {
+ const Dialogs = useDialogs();
+ const [activeProfile, setActiveProfile] = useState();
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState();
+ const [profiles, setProfiles] = useState([]);
+ const [selected, setSelected] = useState();
+
+ /* Tuned doesn't implement the DBus.Properties interface, so
+ * we occasionally poll for what we need.
+ *
+ * Tuned doesn't auto-activate on the bus, so we have to start
+ * it explicitly when opening the dialog.
+ */
+
+ const setProfile = () => {
+ const setService = () => {
+ /* When the profile is none we disable tuned */
+ const enable = (selected != "none");
+ const action = enable ? "start" : "stop";
+ return tunedDbus.call('/Tuned', 'com.redhat.tuned.control', action, [])
+ .then(results => {
+ /* Yup this is how tuned returns failures */
+ if (!results[0]) {
+ console.warn("Failed to " + action + " tuned:", results);
+ if (results[1])
+ return Promise.reject(results[1]);
+ else if (enable)
+ return Promise.reject(cockpit.format(_("Failed to enable tuned")));
+ else
+ return Promise.reject(cockpit.format(_("Failed to disable tuned")));
+ }
+
+ /* Now tell systemd about this change */
+ if (enable && !tunedService.enabled)
+ return tunedService.enable();
+ else if (!enable && tunedService.enabled)
+ return tunedService.disable();
+ else
+ return null;
+ });
+ };
+
+ let promise;
+
+ if (selected == "none") {
+ promise = tunedDbus.call("/Tuned", 'com.redhat.tuned.control', 'disable', [])
+ .then(results => {
+ /* Yup this is how tuned returns failures */
+ if (!results[0]) {
+ console.warn("Failed to disable tuned profile:", results);
+ return Promise.reject(_("Failed to disable tuned profile"));
+ }
+
+ updateButton();
+ });
+ } else {
+ promise = tunedDbus.call('/Tuned', 'com.redhat.tuned.control', 'switch_profile', [selected])
+ .then(results => {
+ /* Yup this is how tuned returns failures */
+ if (!results[0][0]) {
+ console.warn("Failed to switch profile:", results);
+ return Promise.reject(results[0][1] || _("Failed to switch profile"));
+ }
+
+ updateButton();
+ });
+ }
+
+ return promise
+ .then(setService)
+ .then(Dialogs.close)
+ .catch(setError);
+ };
+
+ useEffect(() => {
+ const withInfo = (active, recommended, profiles) => {
+ const model = [];
+ profiles.forEach(p => {
+ let name, desc;
+ if (typeof p === "string") {
+ name = p;
+ desc = "";
+ } else {
+ name = p[0];
+ desc = p[1];
+ }
+ if (name != "none") {
+ model.push({
+ name,
+ title: name,
+ description: desc,
+ active: name == active,
+ recommended: name == recommended,
+ });
+ }
+ });
+
+ model.unshift({
+ name: "none",
+ title: _("None"),
+ description: _("Disable tuned"),
+ active: active == "none",
+ recommended: recommended == "none",
+ });
+
+ setProfiles(model);
+ setActiveProfile(active);
+ setSelected(active);
+ };
+
+ const withTuned = () => {
+ const tunedProfiles = () => {
+ return tunedDbus.call('/Tuned', 'com.redhat.tuned.control', 'profiles2', [])
+ .then((result) => result[0])
+ .catch(ex => {
+ return tunedDbus.call('/Tuned', 'com.redhat.tuned.control', 'profiles', [])
+ .then((result) => result[0]);
+ });
+ };
+
+ return poll()
+ .then(res => {
+ const { state, active, recommended } = res;
+ if (state != "running") {
+ setError(_("Tuned has failed to start"));
+ return;
+ }
+ return tunedProfiles()
+ .then(profiles => {
+ return withInfo(active, recommended, profiles);
+ })
+ .catch(setError);
+ })
+ .catch(setError);
+ };
+
+ tunedService.start()
+ .then(updateButton)
+ .then(withTuned)
+ .catch(setError)
+ .finally(() => setLoading(false));
+ }, [updateButton, poll, tunedService, tunedDbus]);
+
+ const help = (
+ <Popover
+ id="tuned-help"
+ bodyContent={
+ <div>
+ {_("Tuned is a service that monitors your system and optimizes the performance under certain workloads. The core of Tuned are profiles, which tune your system for different use cases.")}
+ </div>
+ }
+ footerContent={
+ <Button component='a'
+ rel="noopener noreferrer" target="_blank"
+ variant='link'
+ isInline
+ icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
+ href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/managing_systems_using_the_rhel_8_web_console/index#optimizing-the-system-performance-using-the-web-console_system-management-using-the-RHEL-8-web-console">
+ {_("Learn more")}
+ </Button>
+ }
+ >
+ <Button variant="plain" aria-label={_("Help")}>
+ <HelpIcon />
+ </Button>
+ </Popover>
+ );
+
+ return (
+ <Modal position="top" variant="medium"
+ className="ct-m-stretch-body"
+ isOpen
+ help={help}
+ onClose={Dialogs.close}
+ title={_("Change performance profile")}
+ footer={
+ <>
+ <Button variant='primary' isDisabled={!selected} onClick={setProfile}>
+ {_("Change profile")}
+ </Button>
+ <Button variant='link' onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>
+ }
+ >
+ {error && <ModalError dialogError={typeof error == 'string' ? error : error.message} />}
+ {loading && <EmptyStatePanel loading />}
+ {activeProfile && <ProfilesMenuDialogBody active_profile={activeProfile}
+ change_selected={setSelected}
+ profiles={profiles} />}
+ </Modal>
+ );
+};
diff --git a/pkg/systemd/overview-cards/usageCard.jsx b/pkg/systemd/overview-cards/usageCard.jsx
new file mode 100644
index 0000000..79fd734
--- /dev/null
+++ b/pkg/systemd/overview-cards/usageCard.jsx
@@ -0,0 +1,166 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardFooter, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { Progress, ProgressMeasureLocation, ProgressVariant } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
+
+import * as machine_info from "machine-info.js";
+import cockpit from "cockpit";
+
+import "./usageCard.scss";
+
+const _ = cockpit.gettext;
+
+const METRICS_SPEC = {
+ payload: "metrics1",
+ source: "internal",
+ interval: 3000,
+ metrics: [
+ { name: "cpu.basic.user", derive: "rate" },
+ { name: "cpu.basic.system", derive: "rate" },
+ { name: "cpu.basic.nice", derive: "rate" },
+ { name: "memory.used" },
+ ]
+};
+
+export class UsageCard extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.metrics_channel = null;
+ this.samples = [];
+
+ this.state = {
+ memTotal: 0, // bytes
+ memUsed: 0, // bytes
+ memUsedText: " ",
+ numCpu: 1, // number
+ cpuUsed: 0, // percentage
+ };
+
+ machine_info.cpu_ram_info()
+ .then(info => this.setState({
+ memTotal: info.memory,
+ numCpu: info.cpus,
+ }));
+
+ this.onVisibilityChange = this.onVisibilityChange.bind(this);
+ this.onMetricsUpdate = this.onMetricsUpdate.bind(this);
+
+ cockpit.addEventListener("visibilitychange", this.onVisibilityChange);
+ this.onVisibilityChange();
+ }
+
+ onVisibilityChange() {
+ if (cockpit.hidden && this.metrics_channel !== null) {
+ this.metrics_channel.removeEventListener("message", this.onMetricsUpdate);
+ this.metrics_channel.close();
+ this.metrics_channel = null;
+ return;
+ }
+
+ if (!cockpit.hidden && this.metrics_channel === null) {
+ this.metrics_channel = cockpit.channel(METRICS_SPEC);
+ this.metrics_channel.addEventListener("closed", (ev, error) => console.error("metrics closed:", error));
+ this.metrics_channel.addEventListener("message", this.onMetricsUpdate);
+ }
+ }
+
+ onMetricsUpdate(event, message) {
+ const data = JSON.parse(message);
+
+ // reset state on meta messages
+ if (!Array.isArray(data)) {
+ this.samples = [];
+ return;
+ }
+
+ // decompress
+ data.forEach(samples => {
+ samples.forEach((sample, i) => {
+ if (sample !== null)
+ this.samples[i] = sample;
+ });
+ });
+
+ // CPU metrics are in ms/s; divide by 10 to get percentage
+ if (this.samples[0] !== false) {
+ const cpu = Math.round((this.samples[0] + this.samples[1] + this.samples[2]) / 10 / this.state.numCpu);
+ this.setState({ cpuUsed: cpu });
+ }
+
+ let used_text;
+ if (this.state.memTotal) {
+ const [total_fmt, unit] = cockpit.format_bytes(this.state.memTotal, 1024, { separate: true, precision: 2 });
+ const used_fmt = cockpit.format_bytes(this.samples[3], unit, { separate: true, precision: 2 })[0];
+ used_text = cockpit.format("$0 / $1 $2", used_fmt, total_fmt, unit);
+ } else {
+ used_text = " ";
+ }
+
+ this.setState({ memUsed: this.samples[3], memUsedText: used_text });
+ }
+
+ render() {
+ const fraction = this.state.memTotal ? this.state.memUsed / this.state.memTotal : 0;
+ const cores_str = cockpit.format(cockpit.ngettext("of $0 CPU", "of $0 CPUs", this.state.numCpu), this.state.numCpu);
+
+ return (
+ <Card className="system-usage">
+ <CardTitle>{_("Usage")}</CardTitle>
+ <CardBody>
+ <table className="pf-v5-c-table pf-m-grid-md pf-m-compact">
+ <tbody className="pf-v5-c-table__tbody">
+ <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th" id="system-usage-cpu-progress" scope="row">{_("CPU")}</th>
+ <td className="pf-v5-c-table__td">
+ <Progress value={this.state.cpuUsed}
+ className="pf-m-sm"
+ min={0} max={100}
+ variant={ this.state.cpuUsed > 90 ? ProgressVariant.danger : null }
+ label={ this.state.cpuUsed + '% ' + cores_str }
+ aria-labelledby="system-usage-cpu-progress"
+ measureLocation={ProgressMeasureLocation.outside} />
+ </td>
+ </tr>
+ <tr className="pf-v5-c-table__tr">
+ <th className="pf-v5-c-table__th" id="system-usage-memory-progress" scope="row">{_("Memory")}</th>
+ <td className="pf-v5-c-table__td">
+ <Progress value={this.state.memUsed}
+ className="pf-m-sm"
+ min={0} max={this.state.memTotal}
+ variant={fraction > 0.9 ? ProgressVariant.danger : null}
+ aria-labelledby="system-usage-memory-progress"
+ label={this.state.memUsedText}
+ measureLocation={ProgressMeasureLocation.outside} />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </CardBody>
+ <CardFooter>
+ <Button isInline variant="link" component="a" onClick={ev => { ev.preventDefault(); cockpit.jump("/metrics", cockpit.transport.host) }}>
+ {_("View metrics and history")}
+ </Button>
+ </CardFooter>
+ </Card>
+ );
+ }
+}
diff --git a/pkg/systemd/overview-cards/usageCard.scss b/pkg/systemd/overview-cards/usageCard.scss
new file mode 100644
index 0000000..a014f4e
--- /dev/null
+++ b/pkg/systemd/overview-cards/usageCard.scss
@@ -0,0 +1,11 @@
+.system-usage {
+ .pf-v5-c-progress {
+ /* FIXME */
+ // This is a hacky, simple approach to make usage bars align.
+ // If the text is too short, there's too much space on the right.
+ // If the text is too long, it flows over and the bar is smaller.
+ // It's better than doing nothing, however...
+ // A more proper fix may require reworking the HTML a bit.
+ grid-template-columns: 1fr minmax(50%, auto);
+ }
+}
diff --git a/pkg/systemd/overview.jsx b/pkg/systemd/overview.jsx
new file mode 100644
index 0000000..e780a1c
--- /dev/null
+++ b/pkg/systemd/overview.jsx
@@ -0,0 +1,187 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import 'polyfills';
+import 'cockpit-dark-theme'; // once per page
+import cockpit from "cockpit";
+
+import React from 'react';
+import { createRoot } from "react-dom/client";
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+import { Dropdown, DropdownItem, DropdownPosition, DropdownToggle, DropdownToggleAction } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+
+import { superuser } from "superuser";
+
+import { SystemInformationCard } from './overview-cards/systemInformationCard.jsx';
+import { ConfigurationCard } from './overview-cards/configurationCard.jsx';
+import { HealthCard } from './overview-cards/healthCard.jsx';
+import { MotdCard } from './overview-cards/motdCard.jsx';
+import { UsageCard } from './overview-cards/usageCard.jsx';
+import { SuperuserAlert } from './superuser-alert.jsx';
+import { SuperuserIndicator } from "../shell/superuser.jsx";
+import { ShutdownModal } from 'cockpit-components-shutdown.jsx';
+import { WithDialogs, DialogsContext } from "dialogs.jsx";
+
+import "./overview.scss";
+
+const _ = cockpit.gettext;
+
+class OverviewPage extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ actionIsOpen: false,
+ privileged: true,
+ };
+ this.hostnameMonitor = this.hostnameMonitor.bind(this);
+ this.onPermissionChanged = this.onPermissionChanged.bind(this);
+
+ this.superuser = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Superuser", "/superuser");
+ }
+
+ componentDidMount() {
+ this.hostnameMonitor();
+ superuser.addEventListener("changed", this.onPermissionChanged);
+ this.onPermissionChanged();
+ }
+
+ componentWillUnmount() {
+ superuser.removeEventListener("changed", this.onPermissionChanged);
+ }
+
+ onPermissionChanged() {
+ this.setState({ privileged: superuser.allowed });
+ }
+
+ hostname_text() {
+ if (!this.state.hostnameData)
+ return undefined;
+
+ const pretty_hostname = this.state.hostnameData.PrettyHostname;
+ const static_hostname = this.state.hostnameData.StaticHostname;
+ let str = this.state.hostnameData.Hostname;
+
+ if (pretty_hostname && static_hostname && static_hostname != pretty_hostname)
+ str = pretty_hostname + " (" + static_hostname + ")";
+ else if (static_hostname)
+ str = static_hostname;
+
+ return str || '';
+ }
+
+ hostnameMonitor() {
+ this.client = cockpit.dbus('org.freedesktop.hostname1');
+ this.hostname_proxy = this.client.proxy('org.freedesktop.hostname1',
+ '/org/freedesktop/hostname1');
+ this.hostname_proxy.addEventListener("changed", data => {
+ this.setState({ hostnameData: data.detail });
+ });
+ }
+
+ render() {
+ const Dialogs = this.context;
+ const { actionIsOpen } = this.state;
+ const dropdownItems = [
+ <DropdownItem key="reboot" id="reboot"
+ onClick={() => Dialogs.show(<ShutdownModal />)}
+ component="button">
+ {_("Reboot")}
+ </DropdownItem>,
+ <DropdownItem key="shutdown" id="shutdown"
+ onClick={() => Dialogs.show(<ShutdownModal shutdown />)}
+ component="button">
+ {_("Shutdown")}
+ </DropdownItem>,
+ ];
+
+ let headerActions = null;
+ if (this.state.privileged)
+ headerActions = (
+ <Dropdown onSelect={() => this.setState({ actionIsOpen: true })}
+ toggle={
+ <DropdownToggle
+ splitButtonItems={[
+ <DropdownToggleAction id='reboot-button'
+ key='reboot-button'
+ onClick={() => Dialogs.show(<ShutdownModal />)}>
+ {_("Reboot")}
+ </DropdownToggleAction>
+ ]}
+ toggleVariant="secondary"
+ splitButtonVariant="action"
+ onToggle={(_event, isOpen) => this.setState({ actionIsOpen: isOpen })}
+ id="shutdown-group"
+ />
+ }
+ isOpen={actionIsOpen}
+ position={DropdownPosition.right}
+ dropdownItems={dropdownItems}
+ />);
+
+ const show_superuser = (
+ cockpit.transport.host && cockpit.transport.host != "localhost" &&
+ !(window.parent.name == "cockpit1" && window.parent.features &&
+ window.parent.features.navbar_is_for_current_machine));
+
+ return (
+ <Page>
+ <PageSection variant={PageSectionVariants.light} padding={{ default: 'noPadding' }}>
+ <SuperuserAlert />
+ </PageSection>
+ <PageSection variant={PageSectionVariants.light} className='ct-overview-header' padding={{ default: 'padding' }}>
+ <div className='ct-overview-header-hostname'>
+ <h1>
+ {this.hostname_text()}
+ </h1>
+ {this.state.hostnameData &&
+ this.state.hostnameData.OperatingSystemPrettyName &&
+ <div className="ct-overview-header-subheading" id="system_information_os_text">{cockpit.format(_("running $0"), this.state.hostnameData.OperatingSystemPrettyName)}</div>}
+ </div>
+ <div className='ct-overview-header-actions'>
+ { show_superuser && <SuperuserIndicator proxy={this.superuser} /> }
+ { "\n" }
+ { headerActions }
+ </div>
+ </PageSection>
+ <PageSection variant={PageSectionVariants.default}>
+ <Gallery className='ct-system-overview' hasGutter>
+ <MotdCard />
+ <HealthCard />
+ <UsageCard />
+ <SystemInformationCard />
+ <ConfigurationCard hostname={this.hostname_text()} />
+ </Gallery>
+ </PageSection>
+ </Page>
+ );
+ }
+}
+
+function init() {
+ cockpit.translate();
+ const root = createRoot(document.getElementById("overview"));
+ root.render(<WithDialogs><OverviewPage /></WithDialogs>);
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/pkg/systemd/overview.scss b/pkg/systemd/overview.scss
new file mode 100644
index 0000000..9cd3fc3
--- /dev/null
+++ b/pkg/systemd/overview.scss
@@ -0,0 +1,278 @@
+@use "./system-global.scss";
+/* System Time Modal dialog needs table.css */
+@use "../lib/table.css";
+@import "global-variables";
+@import "@patternfly/patternfly/components/Table/table.scss";
+
+.ct-limited-access-alert {
+ --pf-v5-c-alert--GridTemplateColumns: auto auto 1fr;
+
+ // Fix vertical alignment
+ // Unset the H4 line-height (as PF3/Bootstrap/etc. sets it; PF4 doesn't)
+ > .pf-v5-c-alert__title {
+ line-height: unset;
+ }
+
+ // Deconstruct nicely on small screen sizes (especially mobile)
+ // This will not be needed in a future PF4 update
+ //
+ // References:
+ // - https://github.com/cockpit-project/cockpit/issues/14106
+ // - https://github.com/patternfly/patternfly/issues/3125
+ // - https://github.com/patternfly/patternfly/pull/2921
+ //
+ // When we have the upcoming version of PF4 in Cockpit, we should drop this code
+ // (and adjust things for the button to show up on the side of desktop mode instead)
+ @media (max-width: $pf-v5-global--breakpoint--lg) {
+ grid-template-areas: "icon title" ". content" ". action";
+ grid-gap: var(--pf-v5-global--spacer--sm) 0;
+ }
+
+ @media (max-width: 320px) {
+ // Allow the action button to have a bit more space on iPhone SE sized phones
+ grid-template-areas: "icon title" ". content" "action action";
+ }
+
+ // Set the right padding so that the button aligns with the other alerts in the page on the side
+ padding-inline-end: var(--pf-v5-c-page__main-section--PaddingRight);
+
+ > .pf-v5-c-alert__action {
+ margin-inline: var(--pf-v5-global--spacer--md) 0;
+ }
+
+ // Align left content with the rest of the page
+ @media (min-width: $pf-v5-global--breakpoint--xl) {
+ padding-inline-start: var(--pf-v5-global--spacer--lg);
+ }
+}
+
+.ct-overview-header {
+ align-items: center;
+ display: flex;
+ flex: none;
+
+ &,
+ &-hostname {
+ flex-wrap: wrap;
+ }
+
+ &-actions,
+ &-hostname {
+ box-sizing: border-box;
+ display: flex;
+ }
+
+ &-hostname {
+ align-items: baseline;
+ flex: auto;
+ /* Collapse down to 15rem, to help preserve button on the right */
+ flex-basis: 15rem;
+
+ > h1 {
+ font-size: var(--pf-v5-global--FontSize--2xl) !important;
+ }
+ }
+
+ &-hostname > h1,
+ &-subheading {
+ padding-inline-end: 1rem;
+ }
+
+ &-actions {
+ align-items: center;
+ }
+
+ &-subheading {
+ font-size: var(--pf-v5-global--FontSize--md);
+ }
+}
+
+.pf-v5-l-gallery.ct-system-overview {
+ --cards: 2;
+ --pf-v5-l-gallery--GridTemplateColumns: repeat(var(--cards), 1fr);
+
+ // Small mobile: Reduce spacing
+ @media (max-width: 320px) {
+ // --pf-v5-l-gallery--m-gutter--GridGap: 0.25rem;
+ }
+
+ // Mobile: reduce to 1 card wide and minimize spacing
+ @media (max-width: 680px) {
+ --pf-v5-l-gallery--m-gutter--GridGap: var(--pf-v5-global--spacer--sm);
+ --cards: 1;
+ }
+
+ // Large desktop: Jump up to 4 cards wide
+ @media (min-width: 1400px) {
+ --cards: 4;
+ }
+
+ // Extra large desktop: Let cards align to the left at an optimal size
+ @media (min-width: 110rem) {
+ --pf-v5-l-gallery--GridTemplateColumns: repeat(auto-fill, minmax(min-content, 26rem));
+ }
+
+ // VMs @ 1024x768; add a little leeway For titlebars, start bar, etc.
+ @media (orientation: landscape) and (min-width: 684px) and (max-width: 832px) and (max-height: 703px) {
+ --pf-v5-l-gallery--m-gutter--GridGap: var(--pf-v5-global--spacer--sm);
+
+ // Also skim off some vertical space for the cards
+ .pf-v5-c-card {
+ --pf-v5-c-card--first-child--PaddingTop: var(--pf-v5-global--spacer--md);
+ --pf-v5-c-card--child--PaddingRight: var(--pf-v5-global--spacer--md);
+ }
+ }
+
+ .motd-box {
+ grid-column: 1 / -1;
+ }
+
+ .pf-v5-c-card {
+ &__title-text {
+ font-size: var(--pf-v5-global--FontSize--xl);
+ font-weight: var(--pf-v5-global--FontWeight--normal);
+ }
+
+ &__body {
+ &:last-child .pf-v5-c-table:last-child tr:last-child {
+ /* Remove the border of tables when it's the last item in a card and there isn't a card footer */
+ border-block-end: none;
+ }
+
+ p {
+ + p,
+ + button {
+ margin-block-start: calc(var(--pf-v5-global--LineHeight--md) * 1rem);
+ }
+ }
+
+ td {
+ vertical-align: middle;
+ }
+
+ th {
+ font-size: var(--pf-v5-global--FontSize--sm);
+ }
+ }
+ }
+
+ .pf-v5-c-progress {
+ &__status {
+ display: flex;
+ align-items: baseline;
+
+ &-icon {
+ display: flex;
+ align-self: center;
+ }
+ }
+ }
+
+ .pf-m-compact {
+ th, td {
+ &:first-child {
+ :not([dir="rtl"]) & {
+ padding-inline-start: 0;
+ }
+
+ [dir="rtl"] & {
+ padding-inline-end: 0;
+ }
+ }
+
+ &:last-child {
+ :not([dir="rtl"]) & {
+ padding-inline-end: 0;
+ }
+
+ [dir="rtl"] & {
+ padding-inline-start: 0;
+ }
+ }
+ }
+ }
+}
+
+@media (max-width: 779px) {
+ /* Reduce gutter & padding on smaller widths, for desktop & mobile */
+
+ .pf-v5-l-gallery.ct-system-overview {
+ --pf-v5-l-gallery--m-gutter--GridGap: calc(var(--pf-v5-global--gutter--md)/2);
+ }
+
+ .pf-v5-c-card {
+ --pf-v5-c-card--first-child--PaddingTop: var(--pf-v5-global--spacer--md);
+ --pf-v5-c-card--child--PaddingRight: var(--pf-v5-global--spacer--md);
+ --pf-v5-c-card--child--PaddingBottom: var(--pf-v5-global--spacer--md);
+ --pf-v5-c-card--child--PaddingLeft: var(--pf-v5-global--spacer--md);
+ --pf-v5-c-card__title--not-last-child--PaddingBottom: var(--pf-v5-global--spacer--sm);
+ }
+}
+
+@media (min-width: 780px) {
+ /* Embiggen subheading & card headings when there's space */
+
+ .pf-v5-l-gallery.ct-system-overview .pf-v5-c-card__title-text {
+ font-size: var(--pf-v5-global--FontSize--2xl);
+ }
+
+ .ct-overview-header-subheading {
+ font-size: var(--pf-v5-global--FontSize--lg);
+ }
+}
+
+.pf-v5-c-table tr > * {
+ vertical-align: top;
+}
+
+.ct-inline-list .pf-v5-c-list.pf-m-inline {
+ display: inline-flex;
+ margin-inline-start: 1rem;
+}
+
+/* Add a subtle dropshadow to the alerts, to separate them from the background, similar to the cards on the page */
+.pf-v5-c-page__main-section:not(.ct-overview-header),
+.pf-v5-l-gallery {
+ > .pf-v5-c-alert {
+ box-shadow: var(--pf-v5-global--BoxShadow--sm);
+
+ // Default PF4 blue and background grey are too close in shade
+ // So: Lighten up the blue to provide a touch more contrast
+ // (Based on default's light green, but in a blue shade)
+ &.pf-m-info {
+ --pf-v5-c-alert--BackgroundColor: #f1f8fe;
+ }
+ }
+}
+
+#motd-box > .pf-v5-c-alert {
+ /* Spacing between the MOTD is handled by the .pf-v5-l-gallery grid */
+ margin-block-end: 0;
+}
+
+.pf-v5-c-alert .pf-v5-c-alert__description a {
+ padding-inline-start: 15px;
+}
+
+// Stretch modal content to fill body
+// This redirects scrolling from the modal body to the select widget
+.ct-m-stretch-body {
+ // Use flex to let children fully expand to the content
+ .pf-v5-c-modal-box__body {
+ &, > :only-child {
+ // Let children fully stretch to content
+ display: flex;
+ }
+
+ > :only-child,
+ .pf-v5-c-menu__content {
+ // Get squishy with children and the menu content
+ flex: auto;
+ }
+
+ .pf-v5-c-menu__content {
+ // Relax the height constraint
+ --pf-v5-c-menu__content--MaxHeight: 100%;
+ }
+ }
+}
diff --git a/pkg/systemd/page-status.jsx b/pkg/systemd/page-status.jsx
new file mode 100644
index 0000000..95be593
--- /dev/null
+++ b/pkg/systemd/page-status.jsx
@@ -0,0 +1,117 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import React from "react";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import {
+ BugIcon,
+ CheckIcon,
+ EnhancementIcon,
+ ExclamationCircleIcon,
+ InfoCircleIcon,
+ SecurityIcon,
+ ExclamationTriangleIcon,
+} from '@patternfly/react-icons';
+
+import "./page-status.scss";
+
+import { page_status } from "notifications";
+
+function icon_for_type(type) {
+ if (type == "error")
+ return <ExclamationCircleIcon className="ct-exclamation-circle" />;
+ else if (type == "warning")
+ return <ExclamationTriangleIcon className="ct-exclamation-triangle" />;
+ else
+ return <InfoCircleIcon className="ct-info-circle" />;
+}
+
+function get_pficon(name) {
+ // set data-pficon for the tests
+ if (name == "security")
+ return <SecurityIcon data-pficon={name} />;
+ if (name == "enhancement")
+ return <EnhancementIcon data-pficon={name} />;
+ if (name == "bug")
+ return <BugIcon data-pficon={name} className="page-status-bug-icon" />;
+ if (name == "check")
+ return <CheckIcon color="green" data-pficon={name} />;
+ if (name == "spinner")
+ return <Spinner size="md" data-pficon={name} />;
+
+ throw new Error(`get_pficon(): unknown icon name ${name}`);
+}
+
+export class PageStatusNotifications extends React.Component {
+ constructor() {
+ super();
+ this.state = { };
+ this.on_page_status_changed = () => this.setState({ });
+ }
+
+ componentDidMount() {
+ page_status.addEventListener("changed", this.on_page_status_changed);
+ }
+
+ componentWillUnmount() {
+ page_status.removeEventListener("changed", this.on_page_status_changed);
+ }
+
+ render() {
+ // Explicit allowlist for now, until we can get a dynamic list
+ return ["system/services", "updates"].map(page => {
+ const status = page_status.get(page);
+ if (status && (status.type || status.details) && status.title) {
+ let action;
+ if (status.details && status.details.link !== undefined) {
+ if (status.details.link)
+ action = <Button variant="link" isInline component="a"
+ onClick={ ev => { ev.preventDefault(); cockpit.jump("/" + status.details.link) } }>{status.title}</Button>;
+ else
+ action = <span>{status.title}</span>; // no link
+ } else {
+ action = <Button variant="link" isInline component="a"
+ onClick={ ev => { ev.preventDefault(); cockpit.jump("/" + page) } }>{status.title}</Button>;
+ }
+
+ let icon;
+ if (status.details && status.details.icon)
+ icon = <span className={status.details.icon} />;
+ else if (status.details && status.details.pficon)
+ icon = get_pficon(status.details.pficon);
+ else
+ icon = icon_for_type(status.type);
+ return (
+ <li id={ "page_status_notification_" + page.replace('/', '_') } key={page}>
+ <Flex flexWrap={{ default: 'nowrap' }} spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ {icon}
+ {action}
+ </Flex>
+ </li>
+ );
+ } else {
+ return null;
+ }
+ });
+ }
+}
diff --git a/pkg/systemd/page-status.scss b/pkg/systemd/page-status.scss
new file mode 100644
index 0000000..b2cf079
--- /dev/null
+++ b/pkg/systemd/page-status.scss
@@ -0,0 +1,3 @@
+.page-status-bug-icon {
+ color: var(--pf-v5-global--Color--200);
+}
diff --git a/pkg/systemd/reporting.jsx b/pkg/systemd/reporting.jsx
new file mode 100644
index 0000000..18fd4e9
--- /dev/null
+++ b/pkg/systemd/reporting.jsx
@@ -0,0 +1,445 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { ExternalLinkAltIcon } from "@patternfly/react-icons";
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+import { useInit } from "hooks";
+
+import './reporting.scss';
+
+const _ = cockpit.gettext;
+
+const TaskState = Object.freeze({
+ READY: 0,
+ RUNNING: 1,
+ COMPLETED: 2,
+ ERROR: 3,
+ CANCELED: 4,
+});
+
+const PromptType = Object.freeze({
+ ASK: 0,
+ ASK_YES_NO: 1,
+ ASK_YES_NO_YESFOREVER: 2,
+ ASK_YES_NO_SAVE: 3,
+ ASK_PASSWORD: 4,
+});
+
+const ProblemState = Object.freeze({
+ REPORTABLE: 0,
+ REPORTING: 1,
+ REPORTED: 2,
+ UNREPORTABLE: 3,
+});
+
+const client = cockpit.dbus("org.freedesktop.problems", { superuser: "try" });
+
+// For one-off fetches of properties to avoid setting up a cache for everything.
+function get_problem_properties(problem) {
+ function executor(resolve, reject) {
+ client.wait().then(() => resolve(client));
+ }
+
+ return new Promise(executor)
+ .then(() => client.call(problem,
+ "org.freedesktop.DBus.Properties",
+ "GetAll", ["org.freedesktop.Problems2.Entry"]));
+}
+
+const FAFWorkflowRow = ({ problem }) => {
+ const [problemState, setProblemState] = React.useState(ProblemState.REPORTABLE);
+ const [process, setProcess] = React.useState(null);
+ const [reportLinks, setReportLinks] = React.useState([]);
+ const [message, setMessage] = React.useState("");
+
+ const updateStatusFromBus = () => {
+ get_problem_properties(problem.path)
+ .catch(exception => {
+ setProblemState(ProblemState.UNREPORTABLE);
+ console.error(cockpit.format("Getting properties for problem $0 failed: $1", problem.path, exception));
+ })
+ .then((properties) => {
+ if (!properties) {
+ return;
+ }
+
+ if (!properties[0].CanBeReported.v) {
+ setProblemState(ProblemState.UNREPORTABLE);
+
+ return;
+ }
+
+ const reportLinks = [];
+ let reported = false;
+
+ for (const report of properties[0].Reports.v) {
+ if (report[0] === "ABRT Server") {
+ if ("URL" in report[1]) {
+ reportLinks.push(report[1].URL.v.v);
+ }
+ reported = true;
+ }
+ }
+
+ if (reported) {
+ setProblemState(ProblemState.REPORTED);
+ setReportLinks(reportLinks);
+ }
+ });
+ };
+
+ useInit(() => {
+ updateStatusFromBus();
+ });
+
+ const onCancelButtonClick = _event => process.close("canceled");
+
+ const onReportButtonClick = _event => {
+ setMessage(_("Waiting to start…"));
+ setProblemState(ProblemState.UNREPORTABLE);
+ const process = cockpit.spawn(["reporter-ureport", "-d", problem.ID],
+ {
+ err: "out",
+ superuser: "true",
+ })
+ .stream((data) => setMessage(data))
+ .then(() => setProblemState(ProblemState.REPORTED))
+ .catch(exception => {
+ setProblemState(ProblemState.REPORTABLE);
+
+ if (exception.exit_signal != null) {
+ console.error(cockpit.format("reporter-ureport was killed with signal $0", exception.exit_signal));
+ }
+ })
+ .finally(() => updateStatusFromBus());
+
+ setProblemState(ProblemState.REPORTING);
+ setProcess(process);
+ };
+
+ return <WorkflowRow label={_("Report to ABRT Analytics")}
+ message={message}
+ onCancelButtonClick={onCancelButtonClick}
+ onReportButtonClick={onReportButtonClick}
+ problemState={problemState}
+ reportLinks={reportLinks}
+ />;
+};
+
+const BusWorkflowRow = ({ problem, client, workflow }) => {
+ const [message, setMessage] = React.useState("");
+ const [problemState, setProblemState] = React.useState(ProblemState.REPORTABLE);
+ const [reportLinks, setReportLinks] = React.useState([]);
+ const [task, setTask] = React.useState(null);
+ const label = workflow[1];
+ const inputRef = React.createRef();
+
+ const updateStatusFromBus = () => {
+ const on_get_properties = properties => {
+ if (!properties[0].CanBeReported.v) {
+ setProblemState(ProblemState.UNREPORTABLE);
+ return;
+ }
+
+ const reportLinks = [];
+ let reported = false;
+
+ for (const report of properties[0].Reports.v) {
+ if (!("WORKFLOW" in report[1])) {
+ continue;
+ }
+ if (workflow[0] !== report[1].WORKFLOW.v.v) {
+ continue;
+ }
+ if (report[0] === "ABRT Server" || report[0] === "uReport") {
+ continue;
+ }
+ if ("URL" in report[1]) {
+ reportLinks.push(report[1].URL.v.v);
+ }
+
+ reported = true;
+ }
+
+ if (reported) {
+ setProblemState(ProblemState.REPORTED);
+ setReportLinks(reportLinks);
+ }
+ };
+ const on_get_properties_rejected = exception => {
+ setProblemState(ProblemState.UNREPORTABLE);
+
+ console.error(cockpit.format("Getting properties for problem $0 failed: $1", problem.path, exception));
+ };
+
+ get_problem_properties(problem.path).then(on_get_properties, on_get_properties_rejected);
+ };
+
+ useInit(() => {
+ updateStatusFromBus();
+ });
+
+ const createTask = client => {
+ return client.call("/org/freedesktop/reportd/Service",
+ "org.freedesktop.reportd.Service", "CreateTask",
+ [workflow[0], problem.path])
+ .then(result => onCreateTask(result[0], client));
+ };
+
+ const onCancelButtonClick = _event => task.Cancel();
+
+ const onCreateTask = (object_path, client) => {
+ const task_proxy = client.proxy("org.freedesktop.reportd.Task", object_path);
+
+ task_proxy
+ .wait()
+ .then((_object_path) => {
+ task_proxy.addEventListener("changed", (_event, data) => {
+ switch (data.Status) {
+ case TaskState.RUNNING:
+ // To avoid a needless D-Bus round trip.
+ return;
+ case TaskState.CANCELED:
+ setMessage(_("Reporting was canceled"));
+ // falls through
+ case TaskState.ERROR:
+ setProblemState(ProblemState.REPORTABLE);
+ break;
+ case TaskState.COMPLETED:
+ setProblemState(ProblemState.REPORTED);
+ break;
+ default:
+ break;
+ }
+
+ updateStatusFromBus();
+ });
+ task_proxy.addEventListener("Prompt", (_event, object_path, message, type) => {
+ setMessage(_("Waiting for input…"));
+ const task_prompt = client.proxy("org.freedesktop.reportd.Task.Prompt", object_path);
+ const props = {
+ body: <p>{message}</p>,
+ };
+ const footerProps = {
+ actions: [],
+ cancel_clicked: () => {
+ task_proxy.Cancel();
+ },
+ };
+
+ switch (type) {
+ case PromptType.ASK:
+ case PromptType.ASK_PASSWORD:
+ props.body = (
+ <div>
+ <p>{message}</p>
+ <input className="full-width"
+ ref={inputRef}
+ type={type == PromptType.ASK_PASSWORD ? "password" : "text"} />
+ </div>
+ );
+ footerProps.actions.push(
+ {
+ caption: _("Send"),
+ clicked: () => {
+ return task_prompt.wait().then(() => {
+ task_prompt.Input = inputRef.current.value;
+ task_prompt.Commit();
+ });
+ },
+ style: "primary",
+ }
+ );
+ break;
+ case PromptType.ASK_YES_NO_YESFOREVER:
+ case PromptType.ASK_YES_NO:
+ case PromptType.ASK_YES_NO_SAVE:
+ footerProps.actions.push(
+ {
+ caption: _("Yes"),
+ clicked: () => {
+ return task_prompt.wait().then(() => {
+ task_prompt.Response = true;
+ task_prompt.Commit();
+ });
+ },
+ },
+ {
+ caption: _("No"),
+ clicked: (callback) => {
+ return task_prompt.wait().then(() => {
+ task_prompt.Response = false;
+ task_prompt.Commit();
+ });
+ },
+ },
+ );
+ }
+
+ show_modal_dialog(props, footerProps);
+ });
+ task_proxy.addEventListener("Progress", (_event, message) => {
+ if (/^\.+$/.exec(message) === null) {
+ // abrt-retrace-client starts printing dots if the last message it receives is repeated
+ setMessage(message);
+ }
+ });
+
+ setTask(task_proxy);
+ task_proxy.Start().catch(ex => {
+ /* GLib encodes errors for transport over the wire,
+ * but we don’t have a good way of decoding them without calling into GIO.
+ *
+ * https://developer-old.gnome.org/gio/stable/gio-GDBusError.html#g-dbus-error-encode-gerror
+ *
+ * 19 is G_IO_ERROR_CANCELLED. No need to handle user cancellations.
+ */
+ if (/Code19/.exec(ex.name) != null) {
+ return;
+ }
+
+ console.error(cockpit.format("reportd task for workflow $0 did not finish: $1", workflow[0], (ex.problem || ex.message)));
+ setMessage(_("Reporting failed"));
+ });
+ })
+ .catch(ex => console.error(cockpit.format("Setting up a D-Bus proxy for $0 failed: $1", object_path, ex)));
+ };
+
+ const onReportButtonClick = (_event) => {
+ setMessage(_("Waiting to start…"));
+ setProblemState(ProblemState.REPORTING);
+
+ client.wait()
+ .catch(exception => console.error(cockpit.format("Channel for reportd D-Bus client closed: $0", exception.problem || exception.message)))
+ .then(() => createTask(client))
+ .catch(exception => {
+ const newMessage = cockpit.format("reportd task could not be created: $0", (exception.problem || exception.message));
+ setMessage(newMessage);
+ setProblemState(ProblemState.REPORTABLE);
+ console.error(newMessage);
+ });
+ };
+
+ return <WorkflowRow label={label}
+ message={message}
+ onCancelButtonClick={onCancelButtonClick}
+ onReportButtonClick={onReportButtonClick}
+ problemState={problemState}
+ reportLinks={reportLinks}
+ />;
+};
+
+const WorkflowRow = ({ message, problemState, reportLinks, label, onReportButtonClick, onCancelButtonClick }) => {
+ let status = message;
+
+ if (problemState === ProblemState.REPORTED) {
+ if (reportLinks.length === 1) {
+ status = (
+ <a href={reportLinks[0]} rel="noopener noreferrer" target="_blank">
+ <ExternalLinkAltIcon />{_("View report")}
+ </a>
+ );
+ } else if (reportLinks.length > 1) {
+ const reportLinksComps = reportLinks.map((reportLink, index) => [
+ index > 0 && ", ",
+ <a key={index.toString()} href={reportLink} rel="noopener noreferrer" target="_blank">
+ <ExternalLinkAltIcon /> {index + 1}
+ </a>
+ ]);
+ status = <p>{_("Reports:")} {reportLinksComps}</p>;
+ } else {
+ status = _("Reported; no links available");
+ }
+ }
+
+ let button = null;
+ if (problemState === ProblemState.REPORTING) {
+ button = (
+ <Button key={"cancel_" + label}
+ variant="secondary"
+ onClick={onCancelButtonClick}>
+ {_("Cancel")}
+ </Button>
+ );
+ } else {
+ button = (
+ <Button key={"report_" + label}
+ variant="primary"
+ isDisabled={problemState !== ProblemState.REPORTABLE}
+ onClick={problemState === ProblemState.REPORTABLE ? onReportButtonClick : undefined}>
+ {_("Report")}
+ </Button>
+ );
+ }
+
+ return (
+ <Split hasGutter>
+ <SplitItem>{label}</SplitItem>
+ {problemState === ProblemState.REPORTING && (
+ <SplitItem>
+ <Spinner size="md" />
+ </SplitItem>
+ )}
+ <SplitItem isFilled>{status}</SplitItem>
+ <SplitItem>{button}</SplitItem>
+ </Split>
+ );
+};
+
+const reportd_client = cockpit.dbus("org.freedesktop.reportd", { superuser: "try" });
+
+export const ReportingTable = ({ problem }) => {
+ const [workflows, setWorkflows] = React.useState([]);
+
+ useInit(() => {
+ reportd_client
+ .wait()
+ .then(() => getWorkflows(reportd_client),
+ exception => console.error(cockpit.format("Channel for reportd D-Bus client closed: $0", exception.problem || exception.message)));
+ });
+
+ const getWorkflows = client => {
+ client.call("/org/freedesktop/reportd/Service", "org.freedesktop.reportd.Service", "GetWorkflows", [problem.path])
+ .then((args, _options) => setWorkflows(args[0]),
+ exception => console.error(cockpit.format("Failed to get workflows for problem $0: $1", problem.path, (exception.problem || exception.message))));
+ };
+
+ return (
+ <Card>
+ <CardTitle component="h2">{_("Crash reporting")}</CardTitle>
+ <CardBody>
+ <FAFWorkflowRow problem={problem} />
+ {
+ workflows.map((workflow, index) => [
+ <BusWorkflowRow key={index.toString()}
+ problem={problem}
+ client={reportd_client}
+ workflow={workflow} />
+ ])
+ }
+ </CardBody>
+ </Card>
+ );
+};
diff --git a/pkg/systemd/reporting.scss b/pkg/systemd/reporting.scss
new file mode 100644
index 0000000..560231f
--- /dev/null
+++ b/pkg/systemd/reporting.scss
@@ -0,0 +1,5 @@
+@use "../lib/ct-card.scss";
+
+#log-details .pf-v5-c-card {
+ @extend .ct-card;
+}
diff --git a/pkg/systemd/service-details.jsx b/pkg/systemd/service-details.jsx
new file mode 100644
index 0000000..8e6a080
--- /dev/null
+++ b/pkg/systemd/service-details.jsx
@@ -0,0 +1,730 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Dropdown, DropdownItem, DropdownSeparator, KebabToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
+import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+import { Card, CardHeader, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
+import {
+ AsleepIcon,
+ BanIcon, ErrorCircleOIcon, OnRunningIcon, OffIcon,
+ ExclamationCircleIcon,
+ OkIcon, UserIcon, ThumbtackIcon,
+} from "@patternfly/react-icons";
+
+import cockpit from "cockpit";
+import s_bus from "./busnames.js";
+import { systemd_client, MAX_UINT64 } from "./services.jsx";
+import * as timeformat from "timeformat";
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { useDialogs, DialogsContext } from "dialogs.jsx";
+import { ModalError } from 'cockpit-components-inline-notification.jsx';
+
+import './service-details.scss';
+
+const _ = cockpit.gettext;
+const METRICS_POLL_DELAY = 30000; // 30s
+
+/*
+ * React template for showing basic dialog for confirming action
+ * Required props:
+ * - title
+ * Title of the dialog
+ * - message
+ * Message in the dialog
+ * - close
+ * Action to be executed when Cancel button is selected.
+ * Optional props:
+ * - confirmText
+ * Text of the button for confirming the action
+ * - confirmAction
+ * Action to be executed when the action is confirmed
+ */
+const ServiceConfirmDialog = ({ id, title, message, confirmText, confirmAction }) => {
+ const Dialogs = useDialogs();
+ return (
+ <Modal id={id} isOpen
+ position="top" variant="medium"
+ onClose={Dialogs.close}
+ title={title}
+ footer={
+ <>
+ { confirmText && confirmAction &&
+ <Button variant='danger' onClick={confirmAction}>
+ {confirmText}
+ </Button>
+ }
+ <Button variant='link' className='btn-cancel' onClick={Dialogs.close}>
+ { _("Cancel") }
+ </Button>
+ </>
+ }>
+ {message}
+ </Modal>
+ );
+};
+
+/*
+ * React template for showing possible service action (in a kebab menu)
+ * Required props:
+ * - masked
+ * Unit is masked
+ * - active
+ * Unit is active (running)
+ * - failed
+ * Unit has failed
+ * - isPinned
+ * Unit is pinned
+ * - canReload
+ * Unit can be reloaded
+ * - actionCallback
+ * Method for calling unit methods like `UnitStart`
+ * - fileActionCallback
+ * Method for calling unit file methods like `EnableUnitFiles`
+ * - deleteActionCallback
+ * Method for calling deleting the systemd unit
+ * - pinUnitCallback
+ * Method to pin unit
+ * - disabled
+ * Button is disabled
+ */
+const ServiceActions = ({ masked, active, failed, canReload, actionCallback, deleteActionCallback, fileActionCallback, disabled, isPinned, pinUnitCallback }) => {
+ const Dialogs = useDialogs();
+ const [isActionOpen, setIsActionOpen] = useState(false);
+
+ const actions = [];
+
+ // If masked, only show unmasking and nothing else
+ if (masked) {
+ actions.push(
+ <DropdownItem key="unmask" onClick={() => fileActionCallback("UnmaskUnitFiles", undefined)}>{ _("Allow running (unmask)") }</DropdownItem>
+ );
+ } else { // All cases when not masked
+ if (active) {
+ if (canReload) {
+ actions.push(
+ <DropdownItem key="reload" onClick={() => actionCallback("ReloadUnit")}>{ _("Reload") }</DropdownItem>
+ );
+ }
+ actions.push(
+ <DropdownItem key="restart" onClick={() => actionCallback("RestartUnit")}>{ _("Restart") }</DropdownItem>
+ );
+ actions.push(
+ <DropdownItem key="stop" onClick={() => actionCallback("StopUnit")}>{ _("Stop") }</DropdownItem>,
+ );
+ } else {
+ actions.push(
+ <DropdownItem key="start" onClick={() => actionCallback("StartUnit")}>{ _("Start") }</DropdownItem>
+ );
+ }
+
+ if (deleteActionCallback) {
+ actions.push(<DropdownSeparator key="delete-divider" />);
+ actions.push(
+ <DropdownItem key="delete" className="pf-m-danger" onClick={() => deleteActionCallback()}>{ _("Delete") }</DropdownItem>
+ );
+ }
+
+ if (actions.length > 0) {
+ actions.push(
+ <DropdownSeparator key="divider" />
+ );
+ }
+
+ if (failed)
+ actions.push(
+ <DropdownItem key="reset" onClick={() => actionCallback("ResetFailedUnit", []) }>{ _("Clear 'Failed to start'") }</DropdownItem>
+ );
+
+ const confirm = () => {
+ Dialogs.show(<ServiceConfirmDialog id="mask-service"
+ title={ _("Mask service") }
+ message={ _("Masking service prevents all dependent units from running. This can have bigger impact than anticipated. Please confirm that you want to mask this unit.")}
+ confirmText={ _("Mask service") }
+ confirmAction={() => {
+ fileActionCallback("MaskUnitFiles", false);
+ if (failed)
+ actionCallback("ResetFailedUnit", []);
+ Dialogs.close();
+ }} />);
+ };
+
+ actions.push(
+ <DropdownItem key="mask" onClick={confirm}>{ _("Disallow running (mask)") }</DropdownItem>
+ );
+
+ actions.push(<DropdownSeparator key="pin-divider" />);
+ actions.push(
+ <DropdownItem key="pin" onClick={() => pinUnitCallback() }>{isPinned ? _("Unpin unit") : _("Pin unit")}</DropdownItem>
+ );
+ }
+
+ return (
+ <Dropdown id="service-actions" title={ _("Additional actions") }
+ toggle={<KebabToggle isDisabled={disabled}
+ onToggle={(_, isOpen) => setIsActionOpen(isOpen)} />}
+ isOpen={isActionOpen}
+ isPlain
+ onSelect={() => setIsActionOpen(!isActionOpen)}
+ position='right'
+ dropdownItems={actions} />
+ );
+};
+
+/*
+ * React template for a service details
+ * Shows current status and information about the service.
+ * Enables user to control this unit like starting, enabling, etc. the service.
+ * Required props:
+ * - unit
+ * as returned from systemd org.freedesktop.systemd1.{Unit,Socket}
+ * D-Bus interface, but with unwrapped variants, and with additional "path"
+ * property and addTimerProperties()
+ * - permitted
+ * True if user can control this unit
+ * - systemdManager
+ * Callback for displaying errors
+ * - isValid
+ * Method for finding if unit is valid
+ */
+export class ServiceDetails extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ waitsAction: false,
+ waitsFileAction: false,
+ unit_properties: {},
+ showDeleteDialog: false,
+ unitPaths: [],
+ isPinned: this.props.pinnedUnits.includes(this.props.unit.Id),
+ };
+
+ this.onOnOffSwitch = this.onOnOffSwitch.bind(this);
+ this.unitAction = this.unitAction.bind(this);
+ this.unitFileAction = this.unitFileAction.bind(this);
+ this.deleteAction = this.deleteAction.bind(this);
+ this.deleteTimer = this.deleteTimer.bind(this);
+ this.pinUnit = this.pinUnit.bind(this);
+
+ this.unitType = props.unit.Id.split('.').slice(-1)[0];
+ this.unitTypeCapitalized = this.unitType.charAt(0).toUpperCase() + this.unitType.slice(1);
+ this.doMemoryCurrentPolling = this.doMemoryCurrentPolling.bind(this);
+
+ // MemoryCurrent property does not emit a changed signal - do polling for this property
+ if (props.unit.ActiveState == "active") {
+ this.doMemoryCurrentPolling();
+ this.interval = setInterval(this.doMemoryCurrentPolling, METRICS_POLL_DELAY);
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ // If unit became active start property polling and if got inactive stop
+ if (this.props.unit.ActiveState === 'active' && !this.interval) {
+ this.doMemoryCurrentPolling();
+ this.interval = setInterval(this.doMemoryCurrentPolling, METRICS_POLL_DELAY);
+ }
+ if (this.props.unit.ActiveState === 'inactive' && this.interval) {
+ this.doMemoryCurrentPolling();
+ clearInterval(this.interval);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.interval)
+ clearInterval(this.interval);
+ }
+
+ static getDerivedStateFromProps(nextProps, prevState) {
+ return {
+ waitsAction: nextProps.loadingUnits,
+ waitsFileAction: nextProps.loadingUnits,
+ };
+ }
+
+ show_note(note) {
+ const Dialogs = this.context;
+ Dialogs.show(<ServiceConfirmDialog title={_("Note")} message={note} />);
+ }
+
+ show_error(error) {
+ const Dialogs = this.context;
+ Dialogs.show(<ServiceConfirmDialog title={_("Error")} message={error} />);
+ }
+
+ doMemoryCurrentPolling() {
+ systemd_client[this.props.owner].call(this.props.unit.path,
+ "org.freedesktop.DBus.Properties", "Get",
+ ["org.freedesktop.systemd1." + this.unitTypeCapitalized, 'MemoryCurrent'])
+ .then(result => {
+ this.addUnitProperties(
+ "MemoryCurrent",
+ result[0] && result[0].v > 0 && result[0].v < MAX_UINT64 ? result[0].v : null,
+ );
+ }, ex => console.log(ex.message));
+ }
+
+ addUnitProperties(prop, value) {
+ if (prop == "MemoryCurrent" && this.state.unit_properties.MemoryCurrent !== value)
+ this.setState({ unit_properties: Object.assign(this.state.unit_properties, { [prop]: value }) });
+ }
+
+ onOnOffSwitch() {
+ if (this.props.unit.UnitFileState === "enabled") {
+ let promise;
+ if (this.props.unit.ActiveState === "active" || this.props.unit.ActiveState === "activating")
+ promise = this.unitAction("StopUnit");
+ else if (this.props.unit.ActiveState === "failed")
+ promise = this.unitAction("ResetFailedUnit", []);
+ else
+ promise = Promise.resolve();
+
+ promise.then(() => this.unitFileAction("DisableUnitFiles", undefined));
+ } else {
+ this.unitFileAction("EnableUnitFiles", false)
+ .then(() => {
+ if (this.props.unit.ActiveState !== "active" && this.props.unit.ActiveState !== "activating")
+ this.unitAction("StartUnit");
+ });
+ }
+ }
+
+ unitAction(method, extra_args, catchExc = true) {
+ if (extra_args === undefined)
+ extra_args = ["fail"];
+ this.setState({ waitsAction: true });
+ const promise = systemd_client[this.props.owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, method, [this.props.unit.Names[0]].concat(extra_args));
+ if (catchExc) {
+ return promise.catch(error => {
+ this.show_error(error.toString());
+ this.setState({ waitsAction: false });
+ });
+ } else {
+ return promise;
+ }
+ }
+
+ pinUnit() {
+ const newPinned = this.state.isPinned
+ ? this.props.pinnedUnits.filter(unitId => unitId != this.props.unit.Id)
+ : [...this.props.pinnedUnits, this.props.unit.Id];
+
+ localStorage.setItem('systemd:pinnedUnits', JSON.stringify(newPinned));
+ this.setState(prevState => ({ isPinned: !prevState.isPinned }));
+ dispatchEvent(new Event('storage'));
+ }
+
+ unitFileAction(method, force, catchExc = true) {
+ this.setState({ waitsFileAction: true });
+ const args = [[this.props.unit.Names[0]], false];
+ if (force !== undefined)
+ args.push(force == "true");
+ const promise = systemd_client[this.props.owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, method, args)
+ .then(([results]) => {
+ if (results.length == 2 && !results[0])
+ this.show_note(_("This unit is not designed to be enabled explicitly."));
+ /* Executing daemon reload after file operations is necessary -
+ * see https://github.com/systemd/systemd/blob/main/src/systemctl/systemctl.c [enable_unit function]
+ */
+ return systemd_client[this.props.owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "Reload", null);
+ });
+ if (catchExc) {
+ return promise.catch(error => {
+ this.show_error(error.toString());
+ this.setState({ waitsFileAction: false });
+ });
+ } else {
+ return promise;
+ }
+ }
+
+ deleteAction() {
+ this.getUnitPaths().then(unitPaths => {
+ this.setState({ showDeleteDialog: true, unitPaths });
+ });
+ }
+
+ async getUnitPaths() {
+ const paths = [this.props.unit.FragmentPath];
+
+ await Promise.all(this.props.unit.Triggers.map(async trigger => {
+ // Getting dbus properties from a non-loaded unit is not possible so resort to systemctl show
+ const unitPath = await cockpit.spawn(["systemctl", "show", "--value",
+ "--property", "FragmentPath", trigger]);
+ paths.push(unitPath.trim());
+ })).catch(err => console.error("failed to look up unit details:", err.toString()));
+
+ return paths;
+ }
+
+ deleteTimer() {
+ // Stop timer so we don't get race conditions when the unit is gone.
+ if (this.interval) {
+ clearInterval(this.interval);
+ this.interval = null;
+ }
+
+ const promises = [];
+ if (this.props.unit.ActiveState === "active" || this.props.unit.ActiveState === "activating")
+ promises.push(this.unitAction("StopUnit", undefined, false));
+ if (this.props.unit.ActiveState === "failed")
+ promises.push(this.unitAction("ResetFailedUnit", undefined, false));
+ if (this.props.unit.UnitFileState === "enabled")
+ promises.push(this.unitFileAction("DisableUnitFiles", undefined, false));
+
+ return Promise.all(promises).then(() => {
+ const deletions = this.state.unitPaths.filter(path => path.startsWith("/etc/systemd/system"))
+ .map(path => cockpit.file(path, { superuser: "required" }).replace(null));
+
+ // Reload after unit/timer removal
+ return Promise.all(deletions).then(() =>
+ systemd_client[this.props.owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "Reload", null)
+ .then(() => cockpit.jump("/system/services#/?type=timer"))
+ );
+ });
+ }
+
+ render() {
+ const active = this.props.unit.ActiveState === "active" || this.props.unit.ActiveState === "activating";
+ const enabled = this.props.unit.UnitFileState === "enabled";
+ const isStatic = this.props.unit.UnitFileState !== "disabled" && !enabled;
+ const failed = this.props.unit.ActiveState === "failed";
+ const masked = this.props.unit.LoadState === "masked";
+ const unit = this.state.unit_properties;
+ const showAction = this.props.permitted || this.props.owner == "user";
+ const isCustom = this.props.unit.FragmentPath.startsWith("/etc/systemd/system") && !masked;
+ const isTimer = (this.unitType === "timer");
+
+ let status = [];
+
+ if (masked) {
+ status.push(
+ <div key="masked" className="status-masked">
+ <BanIcon className="status-icon" />
+ <span className="status">{ _("Masked") }</span>
+ <span className="side-note font-xs">{ _("Forbidden from running") }</span>
+ </div>
+ );
+ }
+
+ if (!enabled && !active && !masked && !isStatic) {
+ status.push(
+ <div key="disabled" className="status-disabled">
+ <OffIcon className="status-icon" />
+ <span className="status">{ _("Disabled") }</span>
+ </div>
+ );
+ }
+
+ if (failed) {
+ status.push(
+ <div key="failed" className="status-failed">
+ <ErrorCircleOIcon className="status-icon" />
+ <span className="status">{ _("Failed to start") }</span>
+ { showAction &&
+ <Button variant="secondary" className="action-button" onClick={() => this.unitAction("StartUnit") }>{ _("Start service") }</Button>
+ }
+ </div>
+ );
+ }
+
+ if (!status.length) {
+ if (active) {
+ status.push(
+ <div key="running" className="status-running">
+ <OnRunningIcon className="status-icon" />
+ <span className="status">{ _("Running") }</span>
+ <span className="side-note font-xs">{ _("Active since ") + timeformat.dateTime(this.props.unit.ActiveEnterTimestamp / 1000) }</span>
+ </div>
+ );
+ } else {
+ status.push(
+ <div key="stopped" className="status-stopped">
+ <OffIcon className="status-icon" />
+ <span className="status">{ _("Not running") }</span>
+ </div>
+ );
+ }
+ }
+
+ if (isStatic && !masked) {
+ status.unshift(
+ <div key="static" className="status-static">
+ <AsleepIcon className="status-icon" />
+ <span className="status">{ _("Static") }</span>
+ { this.props.unit.WantedBy && this.props.unit.WantedBy.length > 0 &&
+ <>
+ <span className="side-note font-xs">{ _("Required by ") }</span>
+ <ul className="comma-list">
+ {this.props.unit.WantedBy.map(unit => <li className="font-xs" key={unit}><a href={"#/" + unit}>{unit}</a></li>)}
+ </ul>
+ </>
+ }
+ </div>
+ );
+ }
+
+ if (!showAction && this.props.owner !== 'user') {
+ status.unshift(
+ <div key="readonly" className="status-readonly">
+ <UserIcon className="status-icon" />
+ <span className="status">{ _("Read-only") }</span>
+ <span className="side-note font-xs">{ _("Requires administration access to edit") }</span>
+ </div>
+ );
+ }
+
+ if (enabled) {
+ status.push(
+ <div key="enabled" className="status-enabled">
+ <OkIcon className="status-icon" />
+ <span className="status">{ _("Automatically starts") }</span>
+ </div>
+ );
+ }
+
+ if (this.props.unit.NextRunTime || this.props.unit.LastTriggerTime) {
+ status.push(
+ <div className="service-unit-triggers" key="triggers">
+ {this.props.unit.NextRunTime && <div className="service-unit-next-trigger">{cockpit.format("Next run: $0", this.props.unit.NextRunTime)}</div>}
+ {this.props.unit.LastTriggerTime && <div className="service-unit-last-trigger">{cockpit.format("Last trigger: $0", this.props.unit.LastTriggerTime)}</div>}
+ </div>
+ );
+ }
+
+ /* If there is some ongoing action just show spinner */
+ if (this.state.waitsAction || this.state.waitsFileAction) {
+ status = [
+ <div key="updating" className="status-updating">
+ <Spinner size="md" className="status-icon" />
+ <span className="status">{ _("Updating status...") }</span>
+ </div>
+ ];
+ }
+
+ const tooltipMessage = enabled ? _("Stop and disable") : _("Start and enable");
+ const hasLoadError = this.props.unit.LoadState !== "loaded" && this.props.unit.LoadState !== "masked";
+
+ if (hasLoadError) {
+ const path = "/system/services" + (this.props.owner === "user" ? "#/?owner=user" : ""); // not-covered: OS error
+ const loadError = this.props.unit.LoadError ? this.props.unit.LoadError[1] : null; // not-covered: OS error
+ const title = loadError || _("Failed to load unit"); // not-covered: OS error
+
+ return <EmptyStatePanel
+ icon={ExclamationCircleIcon}
+ title={title}
+ paragraph={this.props.unitId}
+ action={
+ <Button variant="link"
+ component="a"
+ onClick={() => cockpit.jump(path, cockpit.transport.host)}>
+ {_("View all services")}
+ </Button>
+ }
+ />;
+ }
+
+ // These are relevant for socket and timer activated services
+ const triggerRelationships = [
+ { Name: _("Triggers"), Units: this.props.unit.Triggers },
+ { Name: _("Triggered by"), Units: this.props.unit.TriggeredBy },
+ ];
+
+ const relationships = [
+ { Name: _("Requires"), Units: this.props.unit.Requires },
+ { Name: _("Requisite"), Units: this.props.unit.Requisite },
+ { Name: _("Wants"), Units: this.props.unit.Wants },
+ { Name: _("Binds to"), Units: this.props.unit.BindsTo },
+ { Name: _("Part of"), Units: this.props.unit.PartOf },
+ { Name: _("Required by"), Units: this.props.unit.RequiredBy },
+ { Name: _("Requisite of"), Units: this.props.unit.RequisiteOf },
+ { Name: _("Wanted by"), Units: this.props.unit.WantedBy },
+ { Name: _("Bound by"), Units: this.props.unit.BoundBy },
+ { Name: _("Consists of"), Units: this.props.unit.ConsistsOf },
+ { Name: _("Conflicts"), Units: this.props.unit.Conflicts },
+ { Name: _("Conflicted by"), Units: this.props.unit.ConflictedBy },
+ { Name: _("Before"), Units: this.props.unit.Before },
+ { Name: _("After"), Units: this.props.unit.After },
+ { Name: _("On failure"), Units: this.props.unit.OnFailure },
+ { Name: _("Propagates reload to"), Units: this.props.unit.PropagatesReloadTo },
+ { Name: _("Reload propagated from"), Units: this.props.unit.ReloadPropagatedFrom },
+ { Name: _("Joins namespace of"), Units: this.props.unit.JoinsNamespaceOf }
+ ];
+
+ const relationshipsToList = rels => {
+ return rels.filter(rel => rel.Units && rel.Units.length > 0)
+ .map(rel =>
+ <DescriptionListGroup key={rel.Name}>
+ <DescriptionListTerm>{rel.Name}</DescriptionListTerm>
+ <DescriptionListDescription id={rel.Name.split(" ").join("")}>
+ <ul className="comma-list">
+ {rel.Units.map(unit => <li key={unit}><Button isInline variant="link" component="a" href={"#/" + unit + (this.props.owner === "user" ? "?owner=user" : "")} isDisabled={!this.props.isValid(unit)}>{unit}</Button></li>)}
+ </ul>
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ );
+ };
+
+ const triggerRelationshipsList = relationshipsToList(triggerRelationships);
+
+ const extraRelationshipsList = relationshipsToList(relationships);
+
+ const conditions = this.props.unit.Conditions;
+ const notMetConditions = [];
+ if (conditions)
+ conditions.forEach(condition => {
+ if (condition[4] < 0)
+ notMetConditions.push(cockpit.format(_("Condition $0=$1 was not met"), condition[0], condition[3]));
+ });
+
+ return (
+ <Card id="service-details-unit" className="ct-card">
+ { this.state.showDeleteDialog &&
+ <DeleteModal
+ name={this.props.unit.Description}
+ handleCancel={() => this.setState({ showDeleteDialog: false })}
+ handleDelete={this.deleteTimer}
+ reason={<Flex flex={{ default: 'column' }}>
+ <p>{_("Deletion will remove the following files:")}</p>
+ <List>
+ {this.state.unitPaths.map(item => <ListItem key={item}>{item}</ListItem>)}
+ </List>
+ </Flex>
+ }
+ />
+ }
+ <CardHeader>
+ <Flex className="service-top-panel" spaceItems={{ default: 'spaceItemsMd' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <CardTitle component="h2" className="service-name">{this.props.unit.Description}</CardTitle>
+ {this.state.isPinned &&
+ <Tooltip content={_("Pinned unit")}>
+ <ThumbtackIcon className='service-thumbtack-icon' />
+ </Tooltip>}
+ { showAction &&
+ <>
+ { !masked && !isStatic &&
+ <Tooltip id="switch-unit-state" content={tooltipMessage} position={TooltipPosition.right}>
+ <Switch isChecked={enabled}
+ aria-label={tooltipMessage}
+ isDisabled={this.state.waitsAction || this.state.waitsFileAction}
+ onChange={this.onOnOffSwitch} />
+ </Tooltip>
+ }
+ <ServiceActions { ...{ active, failed, enabled, masked } } canReload={this.props.unit.CanReload}
+ actionCallback={this.unitAction} fileActionCallback={this.unitFileAction}
+ deleteActionCallback={isCustom && isTimer ? this.deleteAction : null}
+ disabled={this.state.waitsAction || this.state.waitsFileAction}
+ isPinned={this.state.isPinned} pinUnitCallback={this.pinUnit} />
+ </>
+ }
+ </Flex>
+ </CardHeader>
+ <CardBody>
+ <Stack hasGutter>
+ <DescriptionList isHorizontal>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Status") }</DescriptionListTerm>
+ <DescriptionListDescription id="statuses">
+ { status }
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Path") }</DescriptionListTerm>
+ <DescriptionListDescription id="path">{this.props.unit.FragmentPath}</DescriptionListDescription>
+ </DescriptionListGroup>
+ {unit.MemoryCurrent
+ ? <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Memory") }</DescriptionListTerm>
+ <DescriptionListDescription id="memory">{cockpit.format_bytes(unit.MemoryCurrent)}</DescriptionListDescription>
+ </DescriptionListGroup>
+ : null}
+ {this.props.unit.Listen && this.props.unit.Listen.length && <DescriptionListGroup>
+ <DescriptionListTerm>{ _("Listen") }</DescriptionListTerm>
+ <DescriptionListDescription id="listen">
+ {cockpit.format("$0 ($1)", this.props.unit.Listen[0][1], this.props.unit.Listen[0][0])}
+ </DescriptionListDescription>
+ </DescriptionListGroup>}
+ { notMetConditions.length > 0 &&
+ <DescriptionListGroup>
+ <DescriptionListTerm className="failed">{ _("Condition failed") }</DescriptionListTerm>
+ <DescriptionListDescription id="condition">
+ {notMetConditions.map(cond => <div key={cond}>{cond}</div>)}
+ </DescriptionListDescription>
+ </DescriptionListGroup>
+ }
+ {triggerRelationshipsList}
+ </DescriptionList>
+ {extraRelationshipsList.length
+ ? <ExpandableSection id="service-details-show-relationships" toggleText={triggerRelationshipsList.length ? _("Show more relationships") : _("Show relationships")}>
+ <DescriptionList isHorizontal>
+ {extraRelationshipsList}
+ </DescriptionList>
+ </ExpandableSection>
+ : null}
+ </Stack>
+ </CardBody>
+ </Card>
+ );
+ }
+}
+ServiceDetails.propTypes = {
+ unit: PropTypes.object.isRequired,
+ // not required: can be null initially, we don't wait for the proxy
+ permitted: PropTypes.bool,
+ isValid: PropTypes.func.isRequired,
+};
+
+const DeleteModal = ({ reason, name, handleCancel, handleDelete }) => {
+ const [inProgress, setInProgress] = useState(false);
+ const [dialogError, setDialogError] = useState(undefined);
+ return (
+ <Modal isOpen
+ showClose={false}
+ position="top" variant="medium"
+ onClose={handleCancel}
+ title={cockpit.format(_("Confirm deletion of $0"), name)}
+ titleIconVariant="warning"
+ footer={<>
+ <Button id="delete-timer-modal-btn" variant="danger" isDisabled={inProgress} isLoading={inProgress}
+ onClick={() => { setInProgress(true); handleDelete().catch(exc => { setDialogError(exc.message); setInProgress(false) }) }}
+ >
+ {_("Delete")}
+ </Button>
+ <Button variant="link" isDisabled={inProgress} onClick={handleCancel}>{_("Cancel")}</Button>
+ </>}
+ >
+ <Stack hasGutter>
+ {dialogError && <ModalError dialogError={_("Timer deletion failed")} dialogErrorDetail={dialogError} />}
+ {reason}
+ </Stack>
+ </Modal>
+ );
+};
diff --git a/pkg/systemd/service-details.scss b/pkg/systemd/service-details.scss
new file mode 100644
index 0000000..c46427b
--- /dev/null
+++ b/pkg/systemd/service-details.scss
@@ -0,0 +1,72 @@
+@use "ct-card";
+@use "../lib/journal.css";
+@use "_global-variables.scss";
+
+#service-details .pf-v5-c-card {
+ @extend .ct-card;
+}
+
+.comma-list {
+ display: inline;
+ list-style: none;
+ padding-inline-start: 0;
+}
+
+.comma-list li {
+ display: inline-block;
+}
+
+.comma-list li:not(:last-child)::after {
+ content: ",";
+ margin-inline-end: 0.5em;
+}
+
+/* Rely on flex for spacing only */
+#service-actions button {
+ padding-inline-start: 0;
+}
+
+.service-thumbtack-icon {
+ color: var(--pf-v5-global--icon--Color--light);
+}
+
+.action-button {
+ margin-inline-start: 2rem;
+}
+
+.status-icon::before {
+ color: inherit;
+}
+
+.status-icon {
+ margin-inline-end: 0.5rem;
+ color: var(--pf-v5-global--icon--Color--light);
+}
+
+.status-running > .status-icon,
+.status-enabled > .status-icon {
+ /* mid-way between PF4 success-100 & 200; then rounded to AA contrast */
+ color: #6da000;
+}
+
+.status-failed > .status-icon,
+.status-failed > .status {
+ color: var(--pf-v5-global--danger-color--200);
+}
+
+.side-note {
+ color: var(--pf-v5-global--Color--300);
+ padding-inline-start: 1em;
+}
+
+.pf-v5-theme-dark .side-note {
+ color: var(--pf-v5-global--Color--200);
+}
+
+.font-xs {
+ font-size: var(--pf-v5-global--FontSize--xs);
+}
+
+.pf-v5-c-dropdown__menu-item.pf-m-danger {
+ color: var(--pf-v5-global--danger-color--200);
+}
diff --git a/pkg/systemd/service-tabs.jsx b/pkg/systemd/service-tabs.jsx
new file mode 100644
index 0000000..398cb0c
--- /dev/null
+++ b/pkg/systemd/service-tabs.jsx
@@ -0,0 +1,72 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Nav, NavItem, NavList } from "@patternfly/react-core/dist/esm/components/Nav/index.js";
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+
+import cockpit from "cockpit";
+
+const _ = cockpit.gettext;
+
+export const service_tabs_suffixes = ["service", "target", "socket", "timer", "path"];
+
+/*
+ * React component showing services tabs
+ * Required props:
+ * - onChange:
+ * When different tab is selected this callback is called
+ */
+export function ServiceTabs({ onChange, activeTab, tabErrors }) {
+ const service_tabs = {
+ service: _("Services"),
+ target: _("Targets"),
+ socket: _("Sockets"),
+ timer: _("Timers"),
+ path: _("Paths")
+ };
+
+ const [activeItem, setActiveItem] = useState(activeTab);
+
+ return (
+ <Nav variant="tertiary" id="services-filter"
+ onSelect={(_event, result) => { setActiveItem(result.itemId); onChange(result.itemId) }}>
+ <NavList>
+ {Object.keys(service_tabs).map(key => {
+ return (
+ <NavItem itemId={key}
+ key={key}
+ preventDefault
+ isActive={activeItem == key}>
+ <Button variant="link" component="a" style={{ "--pf-v5-c-button--m-link--Color": "var(--pf-v5-global--Color--200)", "--pf-v5-c-nav__link--m-current--Color": "var(--pf-v5-global--Color--100)", "--pf-v5-c-nav__link--hover--Color": "var(--pf-v5-global--Color--200)" }}>
+ {service_tabs[key]}
+ {tabErrors[key] ? <ExclamationCircleIcon className="ct-exclamation-circle" /> : null}
+ </Button>
+ </NavItem>
+ );
+ })}
+ </NavList>
+ </Nav>
+ );
+}
+ServiceTabs.propTypes = {
+ onChange: PropTypes.func.isRequired,
+};
diff --git a/pkg/systemd/service.jsx b/pkg/systemd/service.jsx
new file mode 100644
index 0000000..37de4f8
--- /dev/null
+++ b/pkg/systemd/service.jsx
@@ -0,0 +1,171 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useRef, useState } from "react";
+import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
+import { PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { ServiceDetails } from "./service-details.jsx";
+import { LogsPanel } from "cockpit-components-logs-panel.jsx";
+import { superuser } from 'superuser';
+import { WithDialogs } from "dialogs.jsx";
+
+import cockpit from "cockpit";
+import { useObject } from "hooks";
+
+import s_bus from "./busnames.js";
+
+const _ = cockpit.gettext;
+
+function debug() {
+ if (window.debugging == "all" || window.debugging?.includes("service-details")) // not-covered: debugging
+ console.debug.apply(console, arguments); // not-covered: debugging
+}
+
+export const Service = ({ dbusClient, owner, unitId, unitIsValid, addTimerProperties, pinnedUnits }) => {
+ const _path = useRef(null);
+ const [error, setError] = useState(null);
+ const [reloading, setReloading] = useState(false);
+ const [unitProps, setUnitProps] = useState(null);
+
+ const updateProperties = () => {
+ const path = _path.current?.path;
+ const promises = [dbusClient.call(path, s_bus.I_PROPS, "GetAll", [s_bus.I_UNIT])];
+
+ if (unitId.endsWith(".timer"))
+ promises.push(dbusClient.call(path, s_bus.I_PROPS, "GetAll", [s_bus.I_TIMER]));
+ else if (unitId.endsWith(".socket"))
+ promises.push(dbusClient.call(path, s_bus.I_PROPS, "GetAll", [s_bus.I_SOCKET]));
+
+ Promise.all(promises)
+ .then(replies => {
+ const unit_props = replies[0][0];
+
+ // unwrap variants
+ for (const key in unit_props)
+ unit_props[key] = unit_props[key].v;
+
+ if (unitId.endsWith(".timer"))
+ addTimerProperties(replies[1][0], unit_props);
+ else if (unitId.endsWith(".socket"))
+ unit_props.Listen = replies[1][0].Listen.v;
+
+ unit_props.path = path;
+
+ debug("Service detail", unitId, "updated properties:", JSON.stringify(unit_props));
+ setUnitProps(unit_props);
+ setError(null);
+ })
+ .catch(ex => setError(ex.toString())); // not-covered: unexpected OS error
+ };
+
+ // load object path and set up PropertiesChanged subscription whenever unitId changes
+ useObject(
+ () => {
+ dbusClient.call(s_bus.O_MANAGER, s_bus.I_MANAGER, "LoadUnit", [unitId])
+ .then(([path]) => {
+ debug("Service detail", unitId, "loaded path", path);
+
+ _path.current?.propSub.remove();
+ const propSub = dbusClient.subscribe(
+ { path, interface: s_bus.I_PROPS, member: "PropertiesChanged" }, () => {
+ debug("Service detail", unitId, "PropertiesChanged; path", path);
+ updateProperties();
+ });
+
+ _path.current = { unitId, path, propSub };
+
+ // initial load
+ updateProperties();
+ })
+ .catch(ex => setError(ex.toString())); // not-covered: unexpected OS error
+ },
+ null, [unitId]);
+
+ useObject(
+ () => dbusClient.subscribe(
+ { interface: s_bus.I_MANAGER, member: "Reloading" },
+ (_, _i, _s, [is_reloading]) => {
+ debug("Service detail", unitId, "Reloading", is_reloading, "current unit", _path.current?.path);
+ setReloading(is_reloading);
+ if (!is_reloading && _path.current)
+ updateProperties();
+ }),
+ sub => sub.remove(), []);
+
+ /* We need this *only* to pick up failed Conditions after attempting to start a service, as the unit immediately gets
+ * unloaded again and we don't get a PropertiesChanged signal. */
+ useObject(
+ () => dbusClient.subscribe(
+ { interface: s_bus.I_MANAGER, member: "JobRemoved" }, (_p, _i, _s, [_job_id, _job_path, unit, result]) => {
+ if (result === "done" && unitId === _path.current?.unitId) {
+ debug("Service detail", unitId, "JobRemoved", _path.current?.path);
+ updateProperties();
+ }
+ }),
+ sub => sub.remove(), []);
+
+ // render
+ if (error)
+ return <EmptyStatePanel title={_("Loading unit failed")} icon={ExclamationCircleIcon} paragraph={error} />; // not-covered: unexpected OS error
+
+ if (unitProps === null)
+ return <EmptyStatePanel loading title={_("Loading...")} paragraph={unitId} />;
+
+ // resolve Alias name to primary ID
+ const cur_unit_id = unitProps.Id;
+
+ const unit_type = owner == "system" ? "UNIT" : "USER_UNIT";
+ const match = [
+ "_SYSTEMD_" + unit_type + "=" + cur_unit_id, "+",
+ "COREDUMP_" + unit_type + "=" + cur_unit_id, "+",
+ unit_type + "=" + cur_unit_id,
+ ];
+ const service_type = owner == "system" ? "service" : "user-service";
+ const url = "/system/logs/#/?prio=debug&" + service_type + "=" + cur_unit_id;
+ const load_state = unitProps.LoadState;
+
+ return (
+ <WithDialogs>
+ <PageBreadcrumb stickyOnBreakpoint={{ default: "top" }}>
+ <Breadcrumb>
+ <BreadcrumbItem to={"#" + cockpit.location.href.replace(/\/[^?]*/, '')}>{_("Services")}</BreadcrumbItem>
+ <BreadcrumbItem isActive>
+ {cur_unit_id}
+ </BreadcrumbItem>
+ </Breadcrumb>
+ </PageBreadcrumb>
+ <PageSection id="service-details">
+ <Stack hasGutter>
+ <ServiceDetails unit={unitProps}
+ owner={owner}
+ permitted={superuser.allowed}
+ loadingUnits={reloading}
+ isValid={unitIsValid}
+ pinnedUnits={pinnedUnits} />
+ {(load_state === "loaded" || load_state === "masked") &&
+ <LogsPanel title={_("Service logs")} match={match} emptyMessage={_("No log entries")} max={10} goto_url={url} search_options={{ prio: "debug", [service_type]: cur_unit_id }} />}
+ </Stack>
+ </PageSection>
+ </WithDialogs>
+ );
+};
diff --git a/pkg/systemd/services-list.jsx b/pkg/systemd/services-list.jsx
new file mode 100644
index 0000000..fbf9601
--- /dev/null
+++ b/pkg/systemd/services-list.jsx
@@ -0,0 +1,153 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2010 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
+import { ListingTable } from 'cockpit-components-table.jsx';
+import { ExclamationCircleIcon, SearchIcon, ThumbtackIcon } from '@patternfly/react-icons';
+
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+
+import cockpit from "cockpit";
+
+const _ = cockpit.gettext;
+
+export const ServicesList = ({ units, isTimer, filtersRef }) => {
+ let columns;
+ if (!isTimer) {
+ columns = [
+ { title: _("Unit"), header: true },
+ { title: _("State") },
+ ];
+ } else {
+ columns = [
+ { title: _("Unit"), header: true },
+ { title: _("Trigger"), props: { width: 20 } },
+ { title: _("State") },
+ ];
+ }
+ return (
+ <ListingTable aria-label={_("Systemd units")}
+ columns={columns}
+ gridBreakPoint={isTimer ? "grid-xl" : "grid-lg"}
+ showHeader={false}
+ id="services-list"
+ rows={ units.map(unit => getServicesRow({ key: unit[0], isTimer, shortId: unit[0], ...unit[1] })) }
+ emptyComponent={<EmptyStatePanel icon={SearchIcon}
+ paragraph={_("No results match the filter criteria. Clear all filters to show results.")}
+ action={<Button id="clear-all-filters" onClick={() => { filtersRef.current() }} isInline variant='link'>{_("Clear all filters")}</Button>}
+ title={_("No matching results")} /> }
+ className="services-list" />
+ );
+};
+
+const getServicesRow = ({ Id, shortId, AutomaticStartup, UnitFileState, LoadState, HasFailed, IsPinned, CombinedState, LastTriggerTime, NextRunTime, Description, isTimer }) => {
+ let displayName = shortId;
+ // Remove ".service" from services as this is not necessary
+ if (shortId.endsWith(".service"))
+ displayName = shortId.substring(0, shortId.length - 8);
+ const props = { displayName, Description };
+
+ const enabled = UnitFileState && UnitFileState.includes("enabled");
+ const disabled = UnitFileState && UnitFileState.includes("disabled");
+ const isStatic = UnitFileState && UnitFileState == "static";
+ const masked = LoadState && LoadState.includes("masked");
+ let unitFileState;
+ if (enabled || disabled)
+ unitFileState = <Badge className="service-unit-file-state" isRead={!enabled}>{AutomaticStartup}</Badge>;
+ else
+ unitFileState = <span className="service-unit-file-state service-unit-file-state-non-badge">{AutomaticStartup}</span>;
+ let tooltipMessage = "";
+ if (enabled)
+ tooltipMessage = _("Automatically starts");
+ else if (disabled)
+ tooltipMessage = _("Does not automatically start");
+ else if (masked)
+ tooltipMessage = _("Forbidden from running");
+ else if (isStatic)
+ tooltipMessage = _("Cannot be enabled");
+
+ const columns = [
+ {
+ title: (
+ <div className='service-unit-first-column'>
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <Button className='service-unit-id'
+ isInline
+ component="a"
+ onClick={() => cockpit.location.go([shortId], cockpit.location.options)}
+ variant='link'>
+ {props.displayName}
+ </Button>
+ {IsPinned &&
+ <Tooltip content={_("Pinned unit")}>
+ <ThumbtackIcon className='service-thumbtack-icon-color' />
+ </Tooltip>}
+ </Flex>
+ {props.Description != shortId && <div className='service-unit-description'>{props.Description}</div>}
+ </div>
+ )
+ },
+ {
+ title: (
+ <Flex
+ id={cockpit.format("$0-service-unit-state", Id)}
+ flexWrap={{ default: 'wrap', md: 'nowrap' }}
+ justifyContent={{ default: 'justifyContentFlexStart', [isTimer ? 'xl' : 'lg']: 'justifyContentFlexEnd' }}
+ className='service-unit-status-flex-container'>
+ <FlexItem className={"service-unit-status" + (HasFailed ? " service-unit-status-failed" : "")}>
+ {HasFailed && <ExclamationCircleIcon className='ct-exclamation-circle' />}
+ {CombinedState}
+ </FlexItem>
+ {tooltipMessage ? <Tooltip id="switch-unit-state" content={tooltipMessage} position={TooltipPosition.left}>{unitFileState}</Tooltip> : unitFileState}
+ </Flex>
+ ),
+ props: {
+ className: 'service-unit-second-column'
+ }
+ },
+ ];
+ if (isTimer) {
+ columns.splice(
+ 1, 0,
+ {
+ title: (
+ <div className="service-unit-triggers">
+ {NextRunTime && <div className="service-unit-next-trigger">{cockpit.format("Next run: $0", NextRunTime)}</div>}
+ {LastTriggerTime && <div className="service-unit-last-trigger">{cockpit.format("Last trigger: $0", LastTriggerTime)}</div>}
+ </div>
+ )
+ }
+ );
+ }
+
+ return {
+ props: {
+ 'data-goto-unit': shortId,
+ id: shortId,
+ key: shortId,
+ className: HasFailed ? "service-unit-failed" : "",
+ },
+ columns
+ };
+};
diff --git a/pkg/systemd/services.html b/pkg/systemd/services.html
new file mode 100644
index 0000000..b9076a9
--- /dev/null
+++ b/pkg/systemd/services.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html id="system-services-page">
+<head>
+ <title translate="yes">Services</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="services.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="services.js"></script>
+ <script src="po.js"></script>
+</head>
+
+<body class="pf-v5-m-tabular-nums" id="services-page">
+ <div class="ct-page-fill" id="services"></div>
+</body>
+</html>
diff --git a/pkg/systemd/services.jsx b/pkg/systemd/services.jsx
new file mode 100644
index 0000000..8637762
--- /dev/null
+++ b/pkg/systemd/services.jsx
@@ -0,0 +1,943 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import 'polyfills'; // once per application
+import 'cockpit-dark-theme'; // once per page
+
+import React, { useState, useEffect, useCallback } from "react";
+import { createRoot } from 'react-dom/client';
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Card } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { SearchInput } from "@patternfly/react-core/dist/esm/components/SearchInput/index.js";
+import { ToggleGroup, ToggleGroupItem } from "@patternfly/react-core/dist/esm/components/ToggleGroup/index.js";
+import { Toolbar, ToolbarContent, ToolbarFilter, ToolbarItem, ToolbarToggleGroup } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js";
+import { ExclamationCircleIcon, FilterIcon } from '@patternfly/react-icons';
+
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { Service } from "./service.jsx";
+import { ServiceTabs, service_tabs_suffixes } from "./service-tabs.jsx";
+import { ServicesList } from "./services-list.jsx";
+import { CreateTimerDialog } from "./timer-dialog.jsx";
+import { page_status } from "notifications";
+import * as python from "python";
+import * as timeformat from "timeformat";
+import cockpit from "cockpit";
+import { superuser } from 'superuser';
+import { useEvent, usePageLocation } from "hooks";
+import { WithDialogs } from "dialogs.jsx";
+
+import s_bus from "./busnames.js";
+import "./services.scss";
+
+const _ = cockpit.gettext;
+
+// As long as we have long-running superuser channels, we need to
+// reload the page when the access level changes.
+//
+superuser.reload_page_on_change();
+
+export const systemd_client = {
+ system: cockpit.dbus(s_bus.BUS_NAME, { bus: "system", superuser: "try" }),
+ user: cockpit.dbus(s_bus.BUS_NAME, { bus: "session" }),
+};
+const timedate_client = cockpit.dbus('org.freedesktop.timedate1');
+export let clock_realtime_now = 0; // ms since epoch; call updateTime() before using
+let monotonic_timer_base = null; // µs
+
+export const MAX_UINT64 = 2 ** 64 - 1;
+
+function debug() {
+ if (window.debugging == "all" || window.debugging?.includes("services"))
+ console.debug.apply(console, arguments);
+}
+
+export function updateTime() {
+ // To correctly interpret monotonic timers, we need to read CLOCK_MONOTONIC. This cannot be read with shell tools
+ python.spawn("import time; print(int(time.clock_gettime(time.CLOCK_REALTIME) * 1000000), int(time.clock_gettime(time.CLOCK_MONOTONIC) * 1000000))",
+ null, { err: "message" })
+ .then(output => {
+ const [realtime_us, monotonic_us] = output.split(' ').map(t => parseInt(t));
+ clock_realtime_now = realtime_us / 1000;
+ monotonic_timer_base = realtime_us - monotonic_us;
+ debug("Read clocks with Python; realtime", clock_realtime_now, "monotonic base", monotonic_timer_base);
+ })
+ .catch(ex => {
+ /* If Python is not available, fall back to reading CLOCK_BOOTTIME from /proc/timer-list.
+ * This is readable by root only */
+ console.log("Failed to read clocks with Python, using fallback:", ex.toString());
+ Promise.allSettled([
+ cockpit.spawn(["date", "+%s"]),
+ cockpit.file("/proc/timer_list", { superuser: "try" }).read(),
+ ])
+ .then(([date_result, timer_list_result]) => {
+ if (date_result.status === "fulfilled") {
+ clock_realtime_now = parseInt(date_result.value) * 1000;
+ } else {
+ console.warn("Failed to read realtime clock:", date_result.reason.toString());
+ return;
+ }
+
+ // third line is "now at N nsecs"
+ if (timer_list_result.status === "fulfilled") {
+ const match = /now at (\d+) nsecs/.exec(timer_list_result.value);
+ if (match)
+ monotonic_timer_base = clock_realtime_now * 1000 - parseInt(match[1]) / 1000;
+
+ debug("Read clocks with fallback; realtime", clock_realtime_now, "monotonic base", monotonic_timer_base);
+ } else {
+ console.log("Failed to read /proc/timer_list:", timer_list_result.reason.toString());
+ }
+ });
+ });
+}
+
+/* Notes about the systemd D-Bus API
+ *
+ * - Loading all units, fetching their properties, and listening to JobNew/JobRemoved is
+ * expensive, so the services list does not do that. For 90% of what the list shows we
+ * only need two calls: ListUnits() for enough information about all units which are in
+ * systemd's brain; and ListUnitFiles() to add the inert ones (disabled, stopped, not a
+ * dependency of anything). The only exception are timers, where we have to get the
+ * properties of the Timers interface to show their last and next run. This information
+ * is collected in listUnits().
+ *
+ * - To keep up with changes, we listen to two signals: PropertiesChanged (which also gets
+ * fired when a unit gets loaded) for run state chanes, and Reloading for file state
+ * changes (like enabling/disabling).
+ *
+ * - When loading an unloaded unit, PropertiesChanged will be fired, but unfortunately in a
+ * rather useless way: it does not contain all properties (thus it needs a GetAll),
+ * and it usually happens for the Timer interface first (when we don't yet have an ID) and
+ * for the Unit interface later; due to that, we track them in two separate dicts.
+ *
+ * - The unit details view does its own independent API communication and state
+ * management. It needs to fetch/interpret a lot of unit properties which are not part
+ * of ListUnits(), but it only needs to do that for a single unit.
+ *
+ * - ListUnitFiles will return unit files that are aliases for other unit files, but
+ * ListUnits will not return aliases.
+ *
+ * - Methods like EnableUnitFiles only change the state of files on disk. A Reload is
+ * necessary to update the state of loaded units.
+ *
+ * - The unit file state as returned by ListUnitFiles is not necessarily the same as the
+ * UnitFileState property of a loaded unit. ListUnitFiles reflects the state of the
+ * files on disk, while a loaded unit is only updated to that state via an explicit
+ * Reload. Thus, be careful to only use the UnitFileState as returned by ListUnitFiles
+ * for unloaded units. Loaded units should use the PropertiesChanged value to reflect
+ * runtime reality.
+ *
+ * A few historical notes which don't apply to the current code, but could be useful in
+ * the future:
+ *
+ *
+ * - A unit that isn't currently loaded has no object path. If you need one, do
+ * LoadUnit(); doing so will emit UnitNew.
+ *
+ * - One can use an object path for a unit that isn't currently loaded. Doing so will load
+ * the unit (and emit UnitNew).
+ *
+ * - JobNew and JobRemoved signals don't include the object path of the affected units,
+ * but we can get those by listening to UnitNew.
+ *
+ * - There might be UnitNew signals for units that are never returned by ListUnits or
+ * ListUnitFiles. These are units that are mentioned in Requires, After, etc or that
+ * people try to load via LoadUnit but that don't actually exist.
+ *
+ * - The "Names" property of a unit only includes those aliases that are currently loaded,
+ * not all. To get all possible aliases, one needs to call ListUnitFiles and match
+ * units via their object path.
+ *
+ * - The unit file state of a alias as returned by ListUnitFiles is always the same as the
+ * unit file state of the primary unit file.
+ *
+ * - A Reload will emit UnitRemoved/UnitNew signals for all units, and no
+ * PropertiesChanges signal for the properties that have changed because of the reload,
+ * such as UnitFileState.
+ */
+
+class ServicesPageBody extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ /* State related to the toolbar components */
+ isFullyLoaded: false,
+ error: null,
+ };
+
+ /* data storage
+ *
+ * do not keep as state, as that requires too much copying, and it's easy to miss updates due to setState()
+ * coalescing; whenever these change, you need to force a state update to re-render
+ */
+
+ /* loaded units: ListUnits()/PropertiesChanged for Unit interface; object path → {
+ Id,
+ Description, LoadState, ActiveState,
+ UnitFileState, // if unit has a file and got a PropertiesChanged
+ } */
+ this.units = {};
+ // for <Service unitIsValid >
+ this.knownIds = new Set();
+
+ // active timers: object path → { LastTriggerTime, NextRunTime } (formatted strings)
+ // lazily initialized when actually showing the Timers tab
+ this.timers = null;
+
+ /* ListUnitFiles() result; updated with daemon reload
+ name/id (e.g. foo.service) → { Id, UnitFileState, ActiveState ("inactive" or empty for aliases) } */
+ this.unit_files = {};
+
+ // other state which should not cause re-renders
+ this.seenActiveStates = new Set();
+ this.seenUnitFileStates = new Set();
+ this.reloading = false;
+
+ this.filtersRef = React.createRef();
+
+ // Possible LoadState values: stub, loaded, not-found, bad-setting, error, merged, masked
+ // See: typedef enum UnitLoadStateState https://github.com/systemd/systemd/blob/main/src/basic/unit-def.h
+ this.loadState = {
+ stub: _("Stub"),
+ loaded: "",
+ "not-found": _("Not found"),
+ "bad-setting": _("Bad setting"),
+ error: _("Error"),
+ merged: _("Merged"),
+ masked: "", // We present the masked from the unitFileState
+ };
+
+ // Possible ActiveState values: active, reloading, inactive, failed, activating, deactivating, maintenance
+ // See: typedef enum UnitActiveState https://github.com/systemd/systemd/blob/main/src/basic/unit-def.h
+ this.activeState = {
+ active: _("Running"),
+ reloading: _("Reloading"),
+ inactive: _("Not running"),
+ failed: _("Failed to start"),
+ activating: _("Running"),
+ deactivating: _("Not running"),
+ maintenance: _("Maintenance"),
+ };
+
+ // Possible UnitFileState values: enabled, enabled-runtime, linked, linked-runtime, alias, masked, masked-runtime, static, disabled, invalid, indirect, generated, transient, bad
+ // See: typedef enum UnitFileState https://github.com/systemd/systemd/blob/main/src/basic/unit-file.h
+ this.unitFileState = {
+ enabled: _("Enabled"),
+ "enabled-runtime": _("Enabled"),
+ disabled: _("Disabled"),
+ linked: _("Linked"),
+ "linked-runtime": _("Linked"),
+ alias: _("Alias"),
+ masked: _("Masked"),
+ "masked-runtime": _("Masked"),
+ static: _("Static"),
+ invalid: _("Invalid"),
+ indirect: _("Indirect"),
+ generated: _("Generated"),
+ transient: _("Transient"),
+ bad: _("Bad"),
+ };
+
+ this.listUnits = this.listUnits.bind(this);
+ this.loadPinnedUnits = this.loadPinnedUnits.bind(this);
+ this.onOptionsChanged = this.onOptionsChanged.bind(this);
+ this.compareUnits = this.compareUnits.bind(this);
+ }
+
+ onOptionsChanged(options) {
+ const currentOptions = { ...cockpit.location.options, ...options };
+
+ if (!currentOptions.activestate || options.activestate == "[]")
+ delete currentOptions.activestate;
+
+ if (!currentOptions.filestate || options.filestate == "[]")
+ delete currentOptions.filestate;
+
+ if (!currentOptions.name)
+ delete currentOptions.name;
+
+ cockpit.location.go(cockpit.location.path, currentOptions);
+ }
+
+ componentDidMount() {
+ systemd_client[this.props.owner].wait(() => {
+ this.systemd_subscription = systemd_client[this.props.owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "Subscribe", null)
+ .finally(this.listUnits)
+ .catch(error => {
+ if (error.name != "org.freedesktop.systemd1.AlreadySubscribed" &&
+ error.name != "org.freedesktop.DBus.Error.FileExists")
+ this.setState({ error: cockpit.format(_("Subscribing to systemd signals failed: $0"), error.toString()) });
+ });
+ })
+ .catch(ex => this.setState({ error: cockpit.format(_("Connecting to dbus failed: $0"), ex.toString()) }));
+
+ cockpit.addEventListener("visibilitychange", () => {
+ if (!cockpit.hidden) {
+ debug("visibilitychange to visible; fully loaded", this.state.isFullyLoaded);
+ /* If the page had only been fetched in the background we need to properly initialize the state now
+ * else just trigger an re-render since we are receiving signals while running in the background and
+ * we update the state but don't re-render
+ */
+ if (!this.state.isFullyLoaded)
+ this.listUnits();
+ else
+ this.setState({});
+ } else {
+ debug("visibilitychange to hidden");
+ }
+ });
+
+ /* Start listening to signals for updates
+ * - when in the middle of reload mute all signals
+ * - We don't need to listen to 'UnitFilesChanged' signal since every time we
+ * perform some file operation we do call Reload which issues 'Reloading' signal
+ */
+ systemd_client[this.props.owner].subscribe({
+ interface: s_bus.I_PROPS,
+ member: "PropertiesChanged"
+ }, async (path, _iface, _signal, [iface, props]) => {
+ if (this.props.isLoading || this.reloading)
+ return;
+
+ // ignore uninteresting unit types
+ if (!this.isUnitHandled(path))
+ return;
+
+ // ignore when timers did not yet get shown
+ if (iface === s_bus.I_TIMER && this.timers !== null) {
+ if (!this.timers[path])
+ this.timers[path] = {};
+ this.addTimerProperties(props, this.timers[path]);
+ debug("timer PropertiesChanged on", path, JSON.stringify(this.timers[path]));
+ return;
+ }
+
+ // ignore uninteresting interfaces
+ if (iface !== s_bus.I_UNIT)
+ return;
+
+ let unit = this.units[path];
+
+ if (!unit) {
+ // this happens when starting an unloaded unit; unfortunately Units props is very incomplete, so we need a GetAll
+ debug("unit PropertiesChanged on previously unloaded unit", path);
+ try {
+ [props] = await systemd_client[this.props.owner].call(path, s_bus.I_PROPS, "GetAll", [s_bus.I_UNIT]);
+ } catch (ex) { // not-covered: OS error
+ console.warn("GetAll Unit for unknown unit", path, "failed:", ex.toString()); // not-covered: OS error
+ return; // not-covered: OS error
+ }
+ unit = {};
+ this.units[path] = unit;
+ }
+
+ // unwrap variants
+ for (const prop of ["ActiveState", "LoadState", "Description", "Id", "UnitFileState"]) {
+ if (props[prop])
+ unit[prop] = props[prop].v;
+ }
+ this.knownIds.add(unit.Id);
+ debug("unit PropertiesChanged on", path, "complete:", JSON.stringify(unit));
+
+ this.processFailedUnits();
+ this.setState({ });
+ });
+
+ // handle transient units
+ systemd_client[this.props.owner].subscribe({ interface: s_bus.I_MANAGER, member: "UnitRemoved" }, (_path, _iface, _signal, [_id, objpath]) => {
+ // during daemon reload we get tons of these, ignore
+ if (this.reloading)
+ return;
+
+ if (this.units[objpath]?.UnitFileState === 'transient') {
+ debug("UnitRemoved of transient", objpath);
+ this.knownIds.delete(this.units[objpath]?.Id);
+ delete this.units[objpath];
+ this.processFailedUnits();
+ this.setState({ });
+ }
+ });
+
+ systemd_client[this.props.owner].subscribe({ interface: s_bus.I_MANAGER, member: "Reloading" }, (_path, _iface, _signal, [reloading]) => {
+ this.reloading = reloading;
+ debug("Reloading", reloading);
+ if (!reloading && !this.props.isLoading)
+ this.listUnits();
+ });
+
+ addEventListener('storage', this.loadPinnedUnits);
+ this.loadPinnedUnits();
+
+ this.timedated_subscription = timedate_client.subscribe({
+ path_namespace: "/org/freedesktop/timedate1",
+ interface: s_bus.I_PROPS,
+ member: "PropertiesChanged"
+ }, updateTime);
+ updateTime();
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (cockpit.hidden)
+ return false;
+
+ return true;
+ }
+
+ loadPinnedUnits() {
+ try {
+ this.setState({ pinnedUnits: JSON.parse(localStorage.getItem('systemd:pinnedUnits')) || [] });
+ } catch (err) {
+ console.warn("exception while parsing systemd:pinnedUnits", err);
+ this.setState({ pinnedUnits: [] });
+ }
+ }
+
+ /**
+ * Return if the unit ID or path specified by @name is handled
+ */
+ isUnitHandled(name) {
+ return service_tabs_suffixes.some(suffix => name.endsWith(suffix));
+ }
+
+ /* When the page is running in the background, fetch only information about failed units
+ * in order to update the 'Page Status'. */
+ listFailedUnits() {
+ return systemd_client[this.props.owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "ListUnitsFiltered", [["failed"]])
+ .then(([failed]) => {
+ const units = {};
+ failed.forEach(([
+ Id, Description, LoadState, ActiveState, _substate, _followUnit, ObjectPath,
+ _is_job_queued, _job_type, _job_path
+ ]) => {
+ if (!this.isUnitHandled(Id))
+ return;
+
+ units[ObjectPath] = { Id, Description, LoadState, ActiveState };
+ });
+
+ this.units = units;
+ this.processFailedUnits();
+ })
+ .catch(ex => console.warn('ListUnitsFiltered failed: ', ex.toString())); // not-covered: OS error
+ }
+
+ isTemplate(id) {
+ const tp = id.indexOf("@");
+ const sp = id.lastIndexOf(".");
+ return (tp != -1 && (tp + 1 == sp || tp + 1 == id.length));
+ }
+
+ listUnits() {
+ if (cockpit.hidden)
+ return this.listFailedUnits();
+
+ // Reinitialize the state variables for the units
+ this.props.setIsLoading(true);
+
+ const dbus = systemd_client[this.props.owner];
+ const units = {};
+
+ Promise.all([
+ dbus.call(s_bus.O_MANAGER, s_bus.I_MANAGER, "ListUnits", null),
+ dbus.call(s_bus.O_MANAGER, s_bus.I_MANAGER, "ListUnitFiles", null)
+ ])
+ .then(([[unitsResults], [unitFilesResults]]) => {
+ this.knownIds = new Set();
+
+ // ListUnits is the primary source of information
+ unitsResults.forEach(([
+ Id, Description, LoadState, ActiveState, _substate, _followUnit, ObjectPath,
+ _is_job_queued, _job_type, _job_path
+ ]) => {
+ if (!this.isUnitHandled(Id))
+ return;
+
+ // We should ignore 'not-found' units when setting the seenActiveStates
+ if (LoadState !== 'not-found')
+ this.seenActiveStates.add(ActiveState);
+
+ units[ObjectPath] = { Id, Description, LoadState, ActiveState };
+ this.knownIds.add(Id);
+ });
+
+ // unloaded, but available unit files
+ const unit_files = {};
+ unitFilesResults.forEach(([UnitFilePath, UnitFileState]) => {
+ const Id = UnitFilePath.split('/').pop();
+ if (!this.isUnitHandled(Id) | this.isTemplate(Id))
+ return;
+
+ this.seenUnitFileStates.add(UnitFileState);
+ // there is not enough information to link this to the primary unit which declared the alias
+ // name; that requires a LoadUnit() + Get("Names") + reverse lookup; the details page has the
+ // correct information, so skip the status for aliases
+ // for other units, we default to "inactive"; loaded units will override that
+ const ActiveState = (UnitFileState === "alias") ? undefined : "inactive";
+ unit_files[Id] = { Id, UnitFileState, ActiveState };
+ });
+
+ this.units = units;
+ this.unit_files = unit_files;
+ this.processFailedUnits();
+ this.setState({ isFullyLoaded: true });
+ })
+ .catch(ex => this.setState({ error: cockpit.format(_("Listing units failed: $0"), ex.toString()) })) // not-covered: OS error
+ .finally(() => this.props.setIsLoading(false));
+ }
+
+ loadTimers() {
+ const dbus = systemd_client[this.props.owner];
+ const promises = [];
+ this.timers = {};
+ debug("Loading all timers");
+
+ Object.entries(this.units).forEach(([path, unit]) => {
+ if (unit.Id.endsWith(".timer")) {
+ promises.push(dbus.call(path, s_bus.I_PROPS, "GetAll", [s_bus.I_TIMER])
+ .then(([props]) => {
+ this.timers[path] = {};
+ this.addTimerProperties(props, this.timers[path]);
+ })
+ .catch(ex => console.warn("Loading timer information for", path, "failed:", ex.toString()))); // not-covered: OS error
+ }
+ });
+ Promise.allSettled(promises).then(() => this.setState({}));
+ }
+
+ /**
+ * Sort units by alphabetically - failed units go on the top of the list
+ */
+ compareUnits(unit_a_t, unit_b_t) {
+ const unit_a = unit_a_t[1];
+ const unit_b = unit_b_t[1];
+ const failed_a = unit_a.HasFailed ? 1 : 0;
+ const failed_b = unit_b.HasFailed ? 1 : 0;
+ const pinned_a = this.state.pinnedUnits.includes(unit_a.Id) ? 1 : 0;
+ const pinned_b = this.state.pinnedUnits.includes(unit_b.Id) ? 1 : 0;
+
+ if (!unit_a || !unit_b)
+ return false;
+
+ if (failed_a != failed_b)
+ return failed_b - failed_a;
+ else if (pinned_a != pinned_b)
+ return pinned_b - pinned_a;
+ else
+ return unit_a_t[0].localeCompare(unit_b_t[0]);
+ }
+
+ addTimerProperties(timer_props, unit) {
+ const last_trigger_usec = timer_props.LastTriggerUSec.v;
+ // systemd puts -1 into an unsigned int type for the various *USec* properties
+ // JS rounds these to a float which is > MAX_UINT64, but the comparison works
+ if (last_trigger_usec > 0 && last_trigger_usec < MAX_UINT64)
+ unit.LastTriggerTime = timeformat.dateTime(last_trigger_usec / 1000);
+ else
+ unit.LastTriggerTime = _("unknown");
+
+ const next_realtime = timer_props.NextElapseUSecRealtime?.v;
+ const next_monotonic = timer_props.NextElapseUSecMonotonic?.v;
+ let next_run_time = null;
+ if (next_realtime > 0 && next_realtime < MAX_UINT64)
+ next_run_time = next_realtime;
+ else if (next_monotonic > 0 && next_monotonic < MAX_UINT64 && monotonic_timer_base !== null)
+ next_run_time = next_monotonic + monotonic_timer_base;
+
+ unit.NextRunTime = next_run_time ? timeformat.dateTime(next_run_time / 1000) : _("unknown");
+ }
+
+ /* Add some computed properties into a unit object - does not call setState */
+ updateComputedProperties(unit) {
+ unit.HasFailed = unit.ActiveState == "failed" || (
+ unit.LoadState && unit.LoadState !== "loaded" && unit.LoadState !== "masked");
+
+ unit.CombinedState = this.activeState[unit.ActiveState] || unit.ActiveState;
+ if (unit.LoadState && unit.LoadState !== "loaded" && unit.LoadState !== "masked")
+ unit.CombinedState = cockpit.format("$0 ($1)", unit.CombinedState, this.loadState[unit.LoadState]);
+
+ unit.AutomaticStartup = this.unitFileState[unit.UnitFileState] || unit.UnitFileState;
+
+ unit.IsPinned = this.state.pinnedUnits.includes(unit.Id);
+ }
+
+ processFailedUnits() {
+ const failed = new Set();
+ const tabErrors = { };
+
+ Object.values(this.units).forEach(u => {
+ if (u.ActiveState == "failed" && u.LoadState != "not-found") {
+ const suffix = u.Id.substr(u.Id.lastIndexOf('.') + 1);
+ if (service_tabs_suffixes.includes(suffix)) {
+ tabErrors[suffix] = true;
+ failed.add(u.Id);
+ }
+ }
+ });
+ this.props.setTabErrors(tabErrors);
+
+ if (failed.size > 0) {
+ page_status.set_own({
+ type: "error",
+ title: cockpit.format(cockpit.ngettext("$0 service has failed",
+ "$0 services have failed",
+ failed.size), failed.size),
+ details: [...failed]
+ });
+ } else {
+ page_status.set_own(null);
+ }
+ }
+
+ // compute filtered and sorted list of [Id, unit]
+ computeSelectedUnits() {
+ const unitType = '.' + this.props.activeTab;
+ const options = cockpit.location.options;
+ const currentTextFilter = decodeURIComponent(options.name || '').toLowerCase();
+ const filters = {
+ activeState: JSON.parse(options.activestate || '[]'),
+ fileState: JSON.parse(options.filestate || '[]')
+ };
+ const selectedUnits = [];
+ const ids = new Set();
+
+ [...Object.entries(this.units), ...Object.entries(this.unit_files)].forEach(([idx, unit]) => {
+ if (!unit.Id?.endsWith(unitType))
+ return;
+
+ if (unit.LoadState === "not-found")
+ return;
+
+ // avoid showing unloaded units when there is a loaded one
+ if (ids.has(unit.Id))
+ return;
+ ids.add(unit.Id);
+
+ const UnitFileState = unit.UnitFileState ?? this.unit_files[unit.Id]?.UnitFileState;
+
+ if (currentTextFilter && !((unit.Description && unit.Description.toLowerCase().includes(currentTextFilter)) ||
+ unit.Id.toLowerCase().includes(currentTextFilter)))
+ return;
+
+ if (filters.fileState.length && this.unitFileState[UnitFileState] &&
+ !filters.fileState.includes(this.unitFileState[UnitFileState]))
+ return;
+
+ if (filters.activeState.length && this.activeState[unit.ActiveState] &&
+ !filters.activeState.includes(this.activeState[unit.ActiveState]))
+ return;
+
+ const augmentedUnit = { ...unit, UnitFileState, ...this.timers?.[idx] };
+ this.updateComputedProperties(augmentedUnit);
+ selectedUnits.push([unit.Id, augmentedUnit]);
+ });
+
+ selectedUnits.sort(this.compareUnits);
+
+ // lazy-load timers
+ if (this.props.activeTab === 'timer' && this.timers === null)
+ this.loadTimers();
+
+ return selectedUnits;
+ }
+
+ render() {
+ if (this.state.error)
+ return <EmptyStatePanel title={_("Loading of units failed")} icon={ExclamationCircleIcon} paragraph={this.state.error} />;
+
+ /* Navigation: unit details page with a path, service list without;
+ * the details page does its own loading, we don't need to wait for isFullyLoaded */
+ const path = cockpit.location.path;
+ if (path.length == 1) {
+ const unit_id = path[0];
+
+ return <Service unitIsValid={unitId => this.unit_files[unitId] || this.knownIds.has(unitId) }
+ owner={this.props.owner}
+ key={unit_id}
+ unitId={unit_id}
+ dbusClient={systemd_client[this.props.owner]}
+ addTimerProperties={this.addTimerProperties}
+ pinnedUnits={this.state.pinnedUnits}
+ />;
+ }
+
+ if (!this.state.isFullyLoaded)
+ return <EmptyStatePanel loading title={_("Loading...")} paragraph={_("Listing units")} />;
+
+ const fileStateDropdownOptions = [
+ { value: 'enabled', label: _("Enabled") },
+ { value: 'disabled', label: _("Disabled") },
+ { value: 'static', label: _("Static") },
+ ];
+ this.seenUnitFileStates.forEach(unitFileState => {
+ if (!['enabled', 'disabled', 'static'].includes(unitFileState.split('-runtime')[0])) {
+ fileStateDropdownOptions.push({ value: unitFileState, label: this.unitFileState[unitFileState] });
+ }
+ });
+ const activeStateDropdownOptions = [
+ { value: 'active', label: _("Running") },
+ { value: 'inactive', label: _("Not running") },
+ ];
+ this.seenActiveStates.forEach(activeState => {
+ if (!['active', 'activating', 'inactive', 'deactivating'].includes(activeState)) {
+ activeStateDropdownOptions.push({ value: activeState, label: this.activeState[activeState] });
+ }
+ });
+ const activeTab = this.props.activeTab;
+
+ return (
+ <PageSection>
+ <Card isCompact>
+ <ServicesPageFilters activeStateDropdownOptions={activeStateDropdownOptions}
+ fileStateDropdownOptions={fileStateDropdownOptions}
+ filtersRef={this.filtersRef}
+ loadingUnits={this.props.isLoading}
+ options={cockpit.location.options}
+ onOptionsChanged={this.onOptionsChanged}
+ />
+ <ServicesList key={cockpit.format("$0-list", activeTab)}
+ isTimer={activeTab == 'timer'}
+ filtersRef={this.filtersRef}
+ units={this.computeSelectedUnits()} />
+ </Card>
+ </PageSection>
+ );
+ }
+}
+
+const ServicesPageFilters = ({
+ activeStateDropdownOptions,
+ fileStateDropdownOptions,
+ filtersRef,
+ loadingUnits,
+ options,
+ onOptionsChanged,
+}) => {
+ const { activestate, filestate, name } = options;
+ const [activeStateFilterIsOpen, setActiveStateFilterIsOpen] = useState(false);
+ const [currentTextFilter, setCurrentTextFilter] = useState(decodeURIComponent(name || ""));
+ const [fileStateFilterIsOpen, setFileStateFilterIsOpen] = useState(false);
+ const [filters, setFilters] = useState({
+ activeState: JSON.parse(activestate || '[]'),
+ fileState: JSON.parse(filestate || '[]'),
+ });
+
+ useEffect(() => {
+ const _filters = { activeState: JSON.parse(options.activestate || '[]'), fileState: JSON.parse(options.filestate || '[]') };
+
+ setCurrentTextFilter(decodeURIComponent(options.name || ""));
+ setFilters(_filters);
+ }, [options, setCurrentTextFilter, setFilters]);
+
+ useEffect(() => {
+ const _options = {};
+
+ _options.activestate = JSON.stringify(filters.activeState);
+ _options.filestate = JSON.stringify(filters.fileState);
+ _options.name = encodeURIComponent(currentTextFilter);
+
+ onOptionsChanged(_options);
+ }, [filters, currentTextFilter, onOptionsChanged]);
+
+ const onSelect = (type, event, selection) => {
+ const checked = event.target.checked;
+
+ setFilters({ ...filters, [type]: checked ? [...filters[type], selection] : filters[type].filter(value => value !== selection) });
+ };
+
+ const onActiveStateSelect = (event, selection) => {
+ onSelect('activeState', event, selection);
+ };
+
+ const onFileStateSelect = (event, selection) => {
+ onSelect('fileState', event, selection);
+ };
+
+ const getFilterLabelKey = (typeLabel) => {
+ if (typeLabel == 'Active state')
+ return 'activeState';
+ else if (typeLabel == 'File state')
+ return 'fileState';
+ };
+
+ const onDeleteChip = useCallback((typeLabel = '', id = '') => {
+ const type = getFilterLabelKey(typeLabel);
+
+ if (type) {
+ setFilters({ ...filters, [type]: filters[type].filter(s => s !== id) });
+ } else {
+ setFilters({
+ activeState: [],
+ fileState: []
+ });
+ }
+ }, [filters]);
+
+ const onDeleteChipGroup = (typeLabel) => {
+ const type = getFilterLabelKey(typeLabel);
+
+ setFilters({ ...filters, [type]: [] });
+ };
+
+ const onClearAllFilters = useCallback(() => {
+ setCurrentTextFilter('');
+ onDeleteChip();
+ }, [setCurrentTextFilter, onDeleteChip]);
+
+ const onTextFilterChanged = textFilter => {
+ setCurrentTextFilter(textFilter);
+ };
+
+ /* Make onClearAllFilters global so at to let it be used by the parent component */
+ useEffect(() => {
+ filtersRef.current = onClearAllFilters;
+ }, [filtersRef, onClearAllFilters]);
+
+ const toolbarItems = <>
+ <ToolbarToggleGroup toggleIcon={<><span className="pf-v5-c-button__icon pf-m-start"><FilterIcon /></span>{_("Toggle filters")}</>} breakpoint="sm"
+ variant="filter-group" alignment={{ default: 'alignLeft' }}>
+ <ToolbarItem variant="search-filter">
+ <SearchInput id="services-text-filter"
+ className="services-text-filter"
+ placeholder={_("Filter by name or description")}
+ value={currentTextFilter}
+ onChange={(_, val) => onTextFilterChanged(val)}
+ onClear={() => onTextFilterChanged('')} />
+ </ToolbarItem>
+ <ToolbarFilter chips={filters.activeState}
+ deleteChip={onDeleteChip}
+ deleteChipGroup={onDeleteChipGroup}
+ categoryName={_("Active state")}>
+ <Select aria-label={_("Active state")}
+ toggleId="services-dropdown-active-state"
+ variant="checkbox"
+ onToggle={(_, isOpen) => setActiveStateFilterIsOpen(isOpen)}
+ onSelect={onActiveStateSelect}
+ selections={filters.activeState}
+ isOpen={activeStateFilterIsOpen}
+ placeholderText={_("Active state")}>
+ {activeStateDropdownOptions.map(option => <SelectOption key={option.value}
+ value={option.label} />)}
+ </Select>
+ </ToolbarFilter>
+ <ToolbarFilter chips={filters.fileState}
+ deleteChip={onDeleteChip}
+ deleteChipGroup={onDeleteChipGroup}
+ categoryName={_("File state")}>
+ <Select aria-label={_("File state")}
+ toggleId="services-dropdown-file-state"
+ variant="checkbox"
+ onToggle={(_, isOpen) => setFileStateFilterIsOpen(isOpen)}
+ onSelect={onFileStateSelect}
+ selections={filters.fileState}
+ isOpen={fileStateFilterIsOpen}
+ placeholderText={_("File state")}>
+ {fileStateDropdownOptions.map(option => <SelectOption key={option.value}
+ value={option.label} />)}
+ </Select>
+ </ToolbarFilter>
+ </ToolbarToggleGroup>
+ </>;
+
+ return (
+ <Toolbar data-loading={loadingUnits}
+ clearAllFilters={onClearAllFilters}
+ className="pf-m-sticky-top ct-compact services-toolbar"
+ id="services-toolbar"
+ numberOfFiltersText={n => cockpit.format("$0 filters applied")}>
+ <ToolbarContent>{toolbarItems}</ToolbarContent>
+ </Toolbar>
+ );
+};
+
+const ServicesPage = () => {
+ const [tabErrors, setTabErrors] = useState({});
+ const [loggedUser, setLoggedUser] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ cockpit.user()
+ .then(user => setLoggedUser(user.name))
+ .catch(ex => console.warn(ex.message));
+ }, []);
+
+ /* Listen for permission changes for "Create timer" button */
+ useEvent(superuser, "changed");
+ // trigger re-renders when location changes (e.g. through changing filters)
+ usePageLocation();
+
+ const options = cockpit.location.options;
+ const activeTab = options.type || 'service';
+ const owner = options.owner || 'system';
+ const setOwner = (owner) => cockpit.location.go(cockpit.location.path, { ...cockpit.location.options, owner });
+
+ if (owner !== 'system' && owner !== 'user') {
+ console.warn("not a valid location: " + JSON.stringify(cockpit.location));
+ cockpit.location = '';
+ return;
+ }
+
+ return (
+ <WithDialogs>
+ <Page>
+ {cockpit.location.path.length == 0 &&
+ <PageSection variant={PageSectionVariants.light} type="nav" className="services-header">
+ <Flex>
+ <ServiceTabs activeTab={activeTab}
+ tabErrors={tabErrors}
+ onChange={activeTab => {
+ cockpit.location.go(cockpit.location.path, { ...cockpit.location.options, type: activeTab });
+ }} />
+ <FlexItem align={{ default: 'alignRight' }}>
+ {loggedUser && loggedUser !== 'root' && <ToggleGroup>
+ <ToggleGroupItem isSelected={owner == "system"}
+ buttonId="system"
+ text={_("System")}
+ onChange={() => setOwner("system")} />
+ <ToggleGroupItem isSelected={owner == "user"}
+ buttonId="user"
+ text={_("User")}
+ onChange={() => setOwner("user")} />
+ </ToggleGroup>}
+ </FlexItem>
+ {activeTab == "timer" && owner == "system" && superuser.allowed && <CreateTimerDialog isLoading={isLoading} owner={owner} />}
+ </Flex>
+ </PageSection>}
+ <ServicesPageBody
+ key={owner}
+ activeTab={activeTab}
+ owner={owner}
+ privileged={superuser.allowed}
+ setTabErrors={setTabErrors}
+ isLoading={isLoading}
+ setIsLoading={setIsLoading}
+ />
+ </Page>
+ </WithDialogs>
+ );
+};
+
+function init() {
+ const root = createRoot(document.getElementById('services'));
+ root.render(<ServicesPage />);
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/pkg/systemd/services.scss b/pkg/systemd/services.scss
new file mode 100644
index 0000000..303e869
--- /dev/null
+++ b/pkg/systemd/services.scss
@@ -0,0 +1,102 @@
+@use "./system-global.scss";
+@import "global-variables";
+
+// The service list lacks a top border.
+// However, the toolbar also lacks a border and is sticky, so add a bottom
+// border to the toolbar to separate the toolbar and list.
+.services-toolbar {
+ border-block-end: 1px solid var(--pf-v5-global--BorderColor--100);
+}
+
+// Magic pixie dust to make the list a bit faster and more furious
+.services-list tbody {
+ // Skip a lot of computations
+ contain: content;
+}
+
+.service-unit-failed {
+ background-color: var(--ct-color-list-critical-bg);
+
+ .service-unit-status {
+ color: var(--ct-color-list-critical-alert-text);
+ }
+}
+
+.service-unit-status {
+ white-space: nowrap;
+
+ &-failed {
+ color: red;
+ }
+}
+
+.service-unit-file-state {
+ display: inline-block;
+ font-size: var(--pf-v5-global--FontSize--sm);
+ min-inline-size: 5rem;
+ text-align: center;
+}
+
+.service-unit-file-state:not(.pf-v5-c-badge) {
+ text-align: center;
+ opacity: 0.8;
+}
+
+// Add some spacing between the tab label and the icon
+.pf-v5-c-nav__link > .ct-exclamation-circle {
+ margin-inline-start: 0.5rem;
+}
+
+.services-header.pf-v5-c-page__main-nav {
+ padding-block-end: 1rem;
+ padding-inline-end: 0;
+
+ > .pf-v5-l-flex {
+ // Add spacing between the tabs and the 'Create timer' button in mobile
+ row-gap: var(--pf-v5-global--spacer--sm);
+ // Align 'Create timer' button with the right side of the services list card
+ margin-inline-end: var(--pf-v5-c-page__main-section--PaddingRight);
+ }
+}
+
+// Add some spacing between the icon and the status string in the list
+.service-unit-status-failed > .ct-exclamation-circle {
+ margin-inline-end: 0.5rem;
+}
+
+.service-unit-first-column {
+ min-inline-size: 20rem;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(5rem, 25rem));
+ grid-gap: var(--pf-v5-global--spacer--sm);
+
+ .service-unit-description {
+ font-weight: bold;
+ }
+}
+
+.service-thumbtack-icon-color {
+ color: var(--pf-v5-global--icon--Color--light);
+}
+
+.service-unit-triggers {
+ min-inline-size: 20ch;
+}
+
+.pf-v5-c-table .service-unit-second-column {
+ --pf-v5-c-table--cell--Width: 20%;
+}
+
+// Don't show labels from mobile mode
+.pf-v5-c-table [data-label]::before {
+ display: none;
+}
+
+.pf-v5-c-table [data-label] {
+ display: revert;
+}
+
+// FIXME: When porting the selects to the PF5 select implementation drop this
+.pf-v5-c-toolbar__item {
+ align-self: center;
+}
diff --git a/pkg/systemd/superuser-alert.jsx b/pkg/systemd/superuser-alert.jsx
new file mode 100644
index 0000000..2814a6b
--- /dev/null
+++ b/pkg/systemd/superuser-alert.jsx
@@ -0,0 +1,41 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from 'react';
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+
+import { LockIcon } from '@patternfly/react-icons';
+
+import { SuperuserButton } from "../shell/superuser.jsx";
+import { superuser } from "superuser.js";
+
+const _ = cockpit.gettext;
+
+export const SuperuserAlert = () => {
+ if (superuser.allowed)
+ return null;
+
+ return <Alert className="ct-limited-access-alert"
+ variant="warning" isInline
+ customIcon={<LockIcon />}
+ actionClose={<SuperuserButton />}
+ title={_("Web console is running in limited access mode.")} />;
+};
diff --git a/pkg/systemd/system-global.scss b/pkg/systemd/system-global.scss
new file mode 100644
index 0000000..e0245bb
--- /dev/null
+++ b/pkg/systemd/system-global.scss
@@ -0,0 +1,17 @@
+@use "page";
+
+.ct-check-circle {
+ color: var(--pf-v5-global--success-color--100);
+}
+
+.ct-info-circle {
+ color: var(--pf-v5-global--info-color--100);
+}
+
+.ct-exclamation-triangle {
+ color: var(--pf-v5-global--warning-color--100);
+}
+
+.ct-exclamation-circle {
+ color: var(--pf-v5-global--danger-color--100);
+}
diff --git a/pkg/systemd/terminal.html b/pkg/systemd/terminal.html
new file mode 100644
index 0000000..2872e4a
--- /dev/null
+++ b/pkg/systemd/terminal.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html id="system-terminal-page">
+<head>
+ <title translate="yes">Terminal</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="terminal.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums" hidden="true">
+ <div class="ct-page-fill" id="terminal"></div>
+ <script src="terminal.js"></script>
+</body>
+</html>
diff --git a/pkg/systemd/terminal.jsx b/pkg/systemd/terminal.jsx
new file mode 100644
index 0000000..c96539b
--- /dev/null
+++ b/pkg/systemd/terminal.jsx
@@ -0,0 +1,202 @@
+import cockpit from "cockpit";
+import 'cockpit-dark-theme'; // once per page
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+
+import React from "react";
+import { createRoot } from "react-dom/client";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { NumberInput } from "@patternfly/react-core/dist/esm/components/NumberInput/index.js";
+import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js";
+
+import "./terminal.scss";
+
+import { Terminal } from "cockpit-components-terminal.jsx";
+
+const _ = cockpit.gettext;
+
+(function() {
+ cockpit.translate();
+
+ /*
+ * A terminal component for the cockpit user.
+ *
+ * Uses the Terminal component from base1 internally, but adds a header
+ * with title and Reset button.
+ *
+ * Spawns the user's shell in the user's home directory.
+ */
+ class UserTerminal extends React.Component {
+ createChannel(user) {
+ return cockpit.channel({
+ payload: "stream",
+ spawn: [user.shell || "/bin/bash"],
+ environ: [
+ "TERM=xterm-256color",
+ ],
+ directory: user.home || "/",
+ pty: true
+ });
+ }
+
+ constructor(props) {
+ super(props);
+
+ let theme = localStorage.getItem('terminal:theme');
+ let size = localStorage.getItem('terminal:font-size');
+ // HACK: Try to read the configuration from localStorage, if it does not exists fall back
+ // to the old configuration stored in a browser's cookie. After enough time has been
+ // passed this can be dropped.
+ if (theme === null || theme === "") {
+ theme = document.cookie.replace(/(?:(?:^|.*;\s*)theme_cookie\s*=\s*([^;]*).*$)|^.*$/, "$1");
+ if (theme !== "") {
+ localStorage.setItem('terminal:theme', theme);
+ this.invalidateCookie("theme_cookie");
+ }
+ }
+ if (size === null || size === "") {
+ size = document.cookie.replace(/(?:(?:^|.*;\s*)size_cookie\s*=\s*([^;]*).*$)|^.*$/, "$1");
+ if (size !== "") {
+ localStorage.setItem('terminal:font-size', size);
+ this.invalidateCookie("size_cookie");
+ }
+ }
+
+ this.state = {
+ title: 'Terminal',
+ theme: theme || "black-theme",
+ size: parseInt(size) || 16,
+ };
+ this.onTitleChanged = this.onTitleChanged.bind(this);
+ this.onResetClick = this.onResetClick.bind(this);
+ this.onThemeChanged = this.onThemeChanged.bind(this);
+ this.onPlus = this.onPlus.bind(this);
+ this.onMinus = this.onMinus.bind(this);
+
+ this.terminalRef = React.createRef();
+ this.resetButtonRef = React.createRef();
+
+ this.minSize = 6;
+ this.maxSize = 40;
+ }
+
+ async componentDidMount() {
+ const user = await cockpit.user();
+ this.setState({ user, channel: this.createChannel(user) });
+ }
+
+ onTitleChanged(title) {
+ this.setState({ title });
+ }
+
+ invalidateCookie(key) {
+ const cookie = key + "=''" +
+ "; path=/; Max-Age=0;";
+ document.cookie = cookie;
+ }
+
+ onPlus() {
+ this.setState((state, _) => {
+ localStorage.setItem('terminal:font-size', state.size + 1);
+ return { size: state.size + 1 };
+ });
+ }
+
+ onMinus() {
+ this.setState((state, _) => {
+ localStorage.setItem('terminal:font-size', state.size - 1);
+ return { size: state.size - 1 };
+ });
+ }
+
+ onThemeChanged(_, value) {
+ this.setState({ theme: value });
+ localStorage.setItem('terminal:theme', value);
+ }
+
+ onResetClick(event) {
+ if (event.button !== 0)
+ return;
+
+ if (!this.state.channel.valid && this.state.user)
+ this.setState(prevState => ({ channel: this.createChannel(prevState.user) }));
+ else
+ this.terminalRef.current.reset();
+
+ // don't focus the button, but keep it on the terminal
+ this.resetButtonRef.current.blur();
+ this.terminalRef.current.focus();
+ }
+
+ render() {
+ const terminal = this.state.channel
+ ? <Terminal ref={this.terminalRef}
+ channel={this.state.channel}
+ theme={this.state.theme}
+ fontSize={this.state.size}
+ parentId="the-terminal"
+ onTitleChanged={this.onTitleChanged} />
+ : <span>Loading...</span>;
+
+ return (
+ <div className="console-ct-container">
+ <div className="terminal-group">
+ <tt className="terminal-title">{this.state.title}</tt>
+ <Toolbar id="toolbar">
+ <ToolbarContent>
+ <ToolbarGroup>
+ <ToolbarItem variant="label" id="size-select">
+ {_("Font size")}
+ </ToolbarItem>
+ <ToolbarItem>
+ <NumberInput
+ className="font-size"
+ value={this.state.size}
+ min={this.minSize}
+ max={this.maxSize}
+ onMinus={this.onMinus}
+ onPlus={this.onPlus}
+ inputAriaLabel={_("Font size")}
+ minusBtnAriaLabel={_("Decrease by one")}
+ plusBtnAriaLabel={_("Increase by one")}
+ widthChars={2}
+ />
+ </ToolbarItem>
+ </ToolbarGroup>
+ <ToolbarGroup>
+ <ToolbarItem variant="label" id="theme-select">
+ {_("Appearance")}
+ </ToolbarItem>
+ <ToolbarItem>
+ <FormSelect onChange={this.onThemeChanged}
+ aria-labelledby="theme-select"
+ value={this.state.theme}>
+ <FormSelectOption value='black-theme' label={_("Black")} />
+ <FormSelectOption value='dark-theme' label={_("Dark")} />
+ <FormSelectOption value='light-theme' label={_("Light")} />
+ <FormSelectOption value='white-theme' label={_("White")} />
+ </FormSelect>
+ </ToolbarItem>
+ </ToolbarGroup>
+ <ToolbarItem>
+ <button ref={this.resetButtonRef}
+ className="pf-v5-c-button pf-m-secondary terminal-reset"
+ onClick={this.onResetClick}>{_("Reset")}</button>
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ </div>
+ <div className={"terminal-body " + this.state.theme} id="the-terminal">
+ {terminal}
+ </div>
+ </div>
+ );
+ }
+ }
+ UserTerminal.displayName = "UserTerminal";
+
+ const root = createRoot(document.getElementById('terminal'));
+ root.render(<UserTerminal />);
+
+ /* And show the body */
+ document.body.removeAttribute("hidden");
+}());
diff --git a/pkg/systemd/terminal.scss b/pkg/systemd/terminal.scss
new file mode 100644
index 0000000..9e97914
--- /dev/null
+++ b/pkg/systemd/terminal.scss
@@ -0,0 +1,80 @@
+@use "../lib/console.css";
+@use "page";
+@import "global-variables";
+
+.console-ct-container {
+ block-size: 100%;
+ display: grid;
+ grid-template-rows: auto 1fr;
+ overflow: hidden;
+}
+
+.console-ct-container .terminal-body,
+.console-ct-container .console-ct {
+ inset: 0;
+}
+
+.console-ct-container .terminal-group {
+ border-block-end: 1px solid var(--ct-color-border);
+}
+
+.xterm-accessibility {
+ /* Allow for scrollbar clicking */
+ /* (scrollbars are 18px or less across common OSes) */
+ inset-inline-end: 18px !important;
+}
+
+.console-ct-container .terminal-body {
+ padding: 0;
+ position: relative;
+}
+
+.console-ct-container .console-ct {
+ position: absolute;
+}
+
+.black-theme {
+ background-color: #000;
+}
+
+.dark-theme {
+ background-color: #002b36;
+}
+
+.light-theme {
+ background-color: #fdf6e3;
+}
+
+.white-theme {
+ background-color: #fff;
+}
+
+.terminal-group {
+ display: flex;
+ row-gap: 0.5rem;
+ justify-content: space-between;
+ align-content: center;
+ align-items: baseline;
+ flex-wrap: wrap;
+
+ /* Move the padding handling to the container element outside of the toolbar */
+ padding: var(--pf-v5-global--spacer--md);
+
+ > .pf-v5-c-toolbar {
+ padding: 0;
+
+ > .pf-v5-c-toolbar__content {
+ padding: 0;
+
+ select {
+ min-inline-size: 5rem;
+ }
+ }
+ padding: 0;
+ }
+}
+
+// HACK for https://forum.patternfly.org/t/read-only-number-input/422
+.font-size .pf-v5-c-form-control[readonly] {
+ cursor: default;
+}
diff --git a/pkg/systemd/timer-dialog-helpers.js b/pkg/systemd/timer-dialog-helpers.js
new file mode 100644
index 0000000..4189c83
--- /dev/null
+++ b/pkg/systemd/timer-dialog-helpers.js
@@ -0,0 +1,91 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import s_bus from "./busnames.js";
+import { systemd_client, clock_realtime_now } from "./services.jsx";
+
+export function create_timer({ name, description, command, delay, delayUnit, delayNumber, repeat, repeatPatterns, specificTime, owner }) {
+ const timer_unit = {};
+ const repeat_array = repeatPatterns;
+ timer_unit.name = name.replace(/\s/g, '');
+ timer_unit.Description = description;
+ timer_unit.Command = command;
+ timer_unit.boot_time = delayNumber;
+ timer_unit.boot_time_unit = delayUnit;
+
+ function month_day_str(d) {
+ const month_str = (d.getMonth() + 1).toString();
+ const day_str = (d.getDate()).toString();
+ return `${month_str.padStart(2, '0')}-${day_str.padStart(2, '0')}`;
+ }
+
+ if (delay == "specific-time" && repeat == "no") {
+ const today = new Date(clock_realtime_now);
+ timer_unit.OnCalendar = `OnCalendar=${today.getFullYear()}-${month_day_str(today)} ${specificTime}:00`;
+ } else if (repeat == "minutely") {
+ timer_unit.repeat_second = repeat_array.map(item => Number(item.second));
+ timer_unit.OnCalendar = "OnCalendar=*-*-* *:*:" + timer_unit.repeat_second.join(",");
+ } else if (repeat == "hourly") {
+ timer_unit.repeat_minute = repeat_array.map(item => Number(item.minute));
+ timer_unit.OnCalendar = "OnCalendar=*-*-* *:" + timer_unit.repeat_minute.join(",");
+ } else if (repeat == "daily") {
+ timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=*-*-* ${item.time}:00`);
+ } else if (repeat == "weekly") {
+ timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=${item.day} *-*-* ${item.time}:00`);
+ } else if (repeat == "monthly") {
+ timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=*-*-${item.day} ${item.time}:00`);
+ } else if (repeat == "yearly") {
+ timer_unit.OnCalendar = repeat_array.map(item => `OnCalendar=*-${month_day_str(new Date(item.date))} ${item.time}:00`);
+ }
+ if (repeat != "hourly" && repeat != "minutely" && delay == "specific-time")
+ timer_unit.OnCalendar = timer_unit.OnCalendar.toString().replaceAll(",", "\n");
+ return create_timer_file({ timer_unit, delay, owner });
+}
+
+function create_timer_file({ timer_unit, delay, owner }) {
+ const unit = "[Unit]\nDescription=";
+ const service = "\n[Service]\nExecStart=";
+ const timer = "\n[Timer]\n";
+ const install = "[Install]\nWantedBy=timers.target\n";
+ const service_file = unit + timer_unit.Description + service + timer_unit.Command + "\n";
+ let timer_file = " ";
+ if (delay == "system-boot") {
+ const boottimer = timer + "OnBootSec=" + timer_unit.boot_time + timer_unit.boot_time_unit + "\n";
+ timer_file = unit + timer_unit.Description + boottimer;
+ } else if (timer_unit.OnCalendar) {
+ const calendartimer = timer + timer_unit.OnCalendar + "\n";
+ timer_file = unit + timer_unit.Description + calendartimer;
+ }
+ timer_file += install;
+ // writing to file
+ const service_path = "/etc/systemd/system/" + timer_unit.name + ".service";
+ const timer_path = "/etc/systemd/system/" + timer_unit.name + ".timer";
+ return cockpit.file(service_path, { superuser: 'try' }).replace(service_file)
+ .then(() => cockpit.file(timer_path, { superuser: 'try' }).replace(timer_file))
+ .then(tag => systemd_client[owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "EnableUnitFiles", [[timer_unit.name + ".timer"], false, false]))
+ /* Executing daemon reload after file operations is necessary -
+ * see https://github.com/systemd/systemd/blob/main/src/systemctl/systemctl.c [enable_unit function] */
+ .then(() => systemd_client[owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "Reload", null))
+ .then(() => {
+ // start calendar timers
+ if (timer_unit.OnCalendar)
+ return systemd_client[owner].call(s_bus.O_MANAGER, s_bus.I_MANAGER, "StartUnit", [timer_unit.name + ".timer", "replace"]);
+ });
+}
diff --git a/pkg/systemd/timer-dialog.jsx b/pkg/systemd/timer-dialog.jsx
new file mode 100644
index 0000000..96a2ed8
--- /dev/null
+++ b/pkg/systemd/timer-dialog.jsx
@@ -0,0 +1,397 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { InputGroup } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
+import { MinusIcon, PlusIcon } from '@patternfly/react-icons';
+
+import { FormHelper } from "cockpit-components-form-helper";
+import { ModalError } from 'cockpit-components-inline-notification.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+import { updateTime } from './services.jsx';
+import { create_timer } from './timer-dialog-helpers.js';
+import * as timeformat from "timeformat.js";
+
+import "./timers.scss";
+
+const _ = cockpit.gettext;
+
+export const CreateTimerDialog = ({ owner, isLoading }) => {
+ const Dialogs = useDialogs();
+ return (
+ <Button key='create-timer-action'
+ variant="secondary"
+ id="create-timer"
+ isDisabled={isLoading}
+ onClick={() => {
+ updateTime();
+ Dialogs.show(<CreateTimerDialogBody owner={owner} />);
+ }}>
+ {_("Create timer")}
+ </Button>
+ );
+};
+
+const CreateTimerDialogBody = ({ owner }) => {
+ const Dialogs = useDialogs();
+ const [command, setCommand] = useState('');
+ const [delay, setDelay] = useState('specific-time');
+ const [delayNumber, setDelayNumber] = useState(0);
+ const [delayUnit, setDelayUnit] = useState('sec');
+ const [description, setDescription] = useState('');
+ const [dialogError, setDialogError] = useState(undefined);
+ const [inProgress, setInProgress] = useState(false);
+ const [name, setName] = useState('');
+ const [repeat, setRepeat] = useState('no');
+ const [repeatPatterns, setRepeatPatterns] = useState([]);
+ const [specificTime, setSpecificTime] = useState("00:00");
+ const [isSpecificTimeOpen, setSpecificTimeOpen] = useState(false);
+ const [submitted, setSubmitted] = useState(false);
+ const [commandNotFound, setCommandNotFound] = useState(false);
+ const validationFailed = {};
+
+ if (!name.trim().length || !/^[a-zA-Z0-9:_.@-]+$/.test(name))
+ validationFailed.name = true;
+ if (!description.trim().length)
+ validationFailed.description = true;
+ if (!command.trim().length || commandNotFound)
+ validationFailed.command = true;
+ if (!/^[0-9]+$/.test(delayNumber))
+ validationFailed.delayNumber = true;
+
+ const timePicker = (idx) => (
+ <TimePicker className="create-timer-time-picker"
+ time={repeatPatterns[idx].time || "00:00"}
+ is24Hour
+ isOpen={repeatPatterns[idx].isOpen || false}
+ setIsOpen={isOpen => {
+ const arr = JSON.parse(JSON.stringify(repeatPatterns));
+ arr[idx].isOpen = isOpen;
+ setRepeatPatterns(arr);
+ }}
+ menuAppendTo={() => document.body}
+ onChange={(_, time) => {
+ const arr = JSON.parse(JSON.stringify(repeatPatterns));
+ arr[idx].time = time;
+ setRepeatPatterns(arr);
+ }}
+ />
+ );
+
+ function onSubmit(event) {
+ setSubmitted(true);
+
+ if (event)
+ event.preventDefault();
+
+ if (Object.keys(validationFailed).length)
+ return false;
+
+ setInProgress(true);
+
+ // Verify if the command exists
+ const command_parts = command.split(" ");
+ cockpit.spawn(["test", "-f", command_parts[0]], { err: "ignore" })
+ .then(() => {
+ create_timer({ name, description, command, delay, delayUnit, delayNumber, repeat, specificTime, repeatPatterns, owner })
+ .then(Dialogs.close, exc => {
+ setDialogError(exc.message);
+ setInProgress(false);
+ });
+ })
+ .catch(() => {
+ setCommandNotFound(true);
+ setInProgress(false);
+ });
+
+ return false;
+ }
+
+ return (
+ <Modal id="timer-dialog"
+ className="timer-dialog" position="top" variant="medium" isOpen onClose={Dialogs.close}
+ title={cockpit.format(_("Create timer"), name)}
+ footer={
+ <>
+ <Button variant='primary'
+ id="timer-save-button"
+ isLoading={inProgress}
+ isDisabled={inProgress || commandNotFound}
+ onClick={onSubmit}>
+ {_("Save")}
+ </Button>
+ <Button variant='link' onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>
+ }>
+ {dialogError && <ModalError dialogError={_("Timer creation failed")} dialogErrorDetail={dialogError} />}
+ <Form isHorizontal onSubmit={onSubmit}>
+ <FormGroup label={_("Name")}
+ fieldId="servicename">
+ <TextInput id='servicename'
+ value={name}
+ validated={submitted && validationFailed.name ? "error" : "default"}
+ onChange={(_event, value) => setName(value)} />
+ <FormHelper fieldId="servicename"
+ helperTextInvalid={submitted && validationFailed.name && (!name.trim().length ? _("This field cannot be empty") : _("Only alphabets, numbers, : , _ , . , @ , - are allowed"))} />
+ </FormGroup>
+ <FormGroup label={_("Description")}
+ fieldId="description">
+ <TextInput id='description'
+ value={description}
+ validated={submitted && validationFailed.description ? "error" : "default"}
+ onChange={(_event, value) => setDescription(value)} />
+ <FormHelper fieldId="description" helperTextInvalid={submitted && validationFailed.description && _("This field cannot be empty")} />
+ </FormGroup>
+ <FormGroup label={_("Command")}
+ fieldId="command">
+ <TextInput id='command'
+ value={command}
+ validated={submitted && validationFailed.command ? "error" : "default"}
+ onChange={(_event, str) => { setCommandNotFound(false); setCommand(str) }} />
+ <FormHelper fieldId="command"
+ helperTextInvalid={submitted && validationFailed.command && (commandNotFound ? _("Command not found") : _("This field cannot be empty"))} />
+ </FormGroup>
+ <FormGroup label={_("Trigger")} hasNoPaddingTop>
+ <Flex>
+ <Radio value="specific-time"
+ id="specific-time"
+ name="boot-or-specific-time"
+ onChange={() => setDelay("specific-time")}
+ isChecked={delay == "specific-time"}
+ label={_("At specific time")} />
+ <Radio value="system-boot"
+ id="system-boot"
+ name="boot-or-specific-time"
+ onChange={() => setDelay("system-boot")}
+ isChecked={delay == "system-boot"}
+ label={_("After system boot")} />
+ </Flex>
+ { delay == "system-boot" &&
+ <FormGroup className="delay-group"
+ label={_("Delay")}>
+ <Flex>
+ <TextInput className="delay-number"
+ value={delayNumber}
+ validated={submitted && validationFailed.delayNumber ? "error" : "default"}
+ onChange={(_event, value) => setDelayNumber(value)} />
+ <FormSelect className="delay-unit"
+ value={delayUnit}
+ onChange={(_, val) => setDelayUnit(val)}
+ aria-label={_("Delay")}>
+ <FormSelectOption value="sec" label={_("Seconds")} />
+ <FormSelectOption value="min" label={_("Minutes")} />
+ <FormSelectOption value="hr" label={_("Hours")} />
+ <FormSelectOption value="weeks" label={_("Weeks")} />
+ </FormSelect>
+ </Flex>
+ <FormHelper helperTextInvalid={submitted && validationFailed.delayNumber && _("Delay must be a number")} />
+ </FormGroup> }
+ { delay == "specific-time" &&
+ <>
+ <FormGroup label={_("Repeat")}>
+ <FormSelect value={repeat}
+ id="drop-repeat"
+ onChange={(_, value) => {
+ if (value == repeat)
+ return;
+
+ setRepeat(value);
+ if (value == "minutely")
+ setRepeatPatterns([{ key: 0, second: "0" }]);
+ else if (value == "hourly")
+ setRepeatPatterns([{ key: 0, minute: "0" }]);
+ else if (value == "daily")
+ setRepeatPatterns([{ key: 0, time: "00:00" }]);
+ else if (value == "weekly")
+ setRepeatPatterns([{ key: 0, day: "mon", time: "00:00" }]);
+ else if (value == "monthly")
+ setRepeatPatterns([{ key: 0, day: 1, time: "00:00" }]);
+ else if (value == "yearly")
+ setRepeatPatterns([{ key: 0, date: undefined, time: "00:00" }]);
+ }}
+ aria-label={_("Repeat")}>
+ <FormSelectOption value="no" label={_("Don't repeat")} />
+ <FormSelectOption value="minutely" label={_("Minutely")} />
+ <FormSelectOption value="hourly" label={_("Hourly")} />
+ <FormSelectOption value="daily" label={_("Daily")} />
+ <FormSelectOption value="weekly" label={_("Weekly")} />
+ <FormSelectOption value="monthly" label={_("Monthly")} />
+ <FormSelectOption value="yearly" label={_("Yearly")} />
+ </FormSelect>
+ </FormGroup>
+ {repeat == "no" &&
+ <FormGroup label={_("Run at")}>
+ <TimePicker className="create-timer-time-picker specific-no-repeat"
+ isOpen={isSpecificTimeOpen} setIsOpen={setSpecificTimeOpen}
+ menuAppendTo={() => document.body} time={specificTime} is24Hour onChange={(_, val) => setSpecificTime(val)} />
+ </FormGroup>}
+ {repeatPatterns.map((item, idx) => {
+ let label;
+ if (repeat == "minutely")
+ label = _("At second");
+ else if (repeat == "hourly")
+ label = _("At minute");
+ else if (repeat == "daily")
+ label = _("Run at");
+ else if (repeat == "weekly" || repeat == "monthly" || repeat == "yearly")
+ label = _("Run on");
+
+ let helperTextInvalid;
+ const min = repeatPatterns[idx].minute;
+ const validationFailedMinute = !(/^[0-9]+$/.test(min) && min <= 59 && min >= 0);
+
+ if (submitted && repeat == 'hourly' && validationFailedMinute) {
+ helperTextInvalid = _("Minute needs to be a number between 0-59");
+ }
+
+ const sec = repeatPatterns[idx].second;
+ const validationFailedSecond = !(/^[0-9]+$/.test(sec) && sec <= 59 && sec >= 0);
+
+ if (submitted && repeat == 'minutely' && validationFailedSecond) {
+ helperTextInvalid = _("Second needs to be a number between 0-59");
+ }
+
+ return (
+ <FormGroup label={label} key={item.key}>
+ <Flex className="specific-repeat-group" data-index={idx}>
+ {repeat == "minutely" &&
+ <TextInput className='delay-number'
+ id={repeat}
+ value={repeatPatterns[idx].second}
+ onChange={(_event, second) => {
+ const arr = [...repeatPatterns];
+ arr[idx].second = second;
+ setRepeatPatterns(arr);
+ }}
+ validated={submitted && validationFailedSecond ? "error" : "default"} />
+ }
+ {repeat == "hourly" && <>
+ <TextInput className='delay-number'
+ value={repeatPatterns[idx].minute}
+ onChange={(_event, minute) => {
+ const arr = [...repeatPatterns];
+ arr[idx].minute = minute;
+ setRepeatPatterns(arr);
+ }}
+ validated={submitted && validationFailedMinute ? "error" : "default"} />
+ </>}
+ {repeat == "daily" && timePicker(idx)}
+ {repeat == "weekly" && <>
+ <FormSelect value={repeatPatterns[idx].day}
+ className="week-days"
+ onChange={(_, day) => {
+ const arr = [...repeatPatterns];
+ arr[idx].day = day;
+ setRepeatPatterns(arr);
+ }}
+ aria-label={_("Repeat weekly")}>
+ <FormSelectOption value="mon" label={_("Mondays")} />
+ <FormSelectOption value="tue" label={_("Tuesdays")} />
+ <FormSelectOption value="wed" label={_("Wednesdays")} />
+ <FormSelectOption value="thu" label={_("Thursdays")} />
+ <FormSelectOption value="fri" label={_("Fridays")} />
+ <FormSelectOption value="sat" label={_("Saturdays")} />
+ <FormSelectOption value="sun" label={_("Sundays")} />
+ </FormSelect>
+ {timePicker(idx)}
+ </>}
+ {repeat == "monthly" && <>
+ <FormSelect value={repeatPatterns[idx].day}
+ className="month-days"
+ onChange={(_, day) => {
+ const arr = [...repeatPatterns];
+ arr[idx].day = day;
+ setRepeatPatterns(arr);
+ }}
+ aria-label={_("Repeat monthly")}>
+ {[_("1st"), _("2nd"), _("3rd"), _("4th"), _("5th"),
+ _("6th"), _("7th"), _("8th"), _("9th"), _("10th"),
+ _("11th"), _("12th"), _("13th"), _("14th"), _("15th"),
+ _("16th"), _("17th"), _("18th"), _("19th"), _("20th"),
+ _("21th"), _("22th"), _("23th"), _("24th"), _("25th"),
+ _("26th"), _("27th"), _("28th"), _("29th"), _("30th"), _("31st")
+ ].map((day, index) => <FormSelectOption key={day} value={index + 1} label={day} />)}
+ </FormSelect>
+ {timePicker(idx)}
+ </>}
+ {repeat == "yearly" && <>
+ <DatePicker aria-label={_("Pick date")}
+ buttonAriaLabel={_("Toggle date picker")}
+ locale={timeformat.dateFormatLang()}
+ weekStart={timeformat.firstDayOfWeek()}
+ onChange={(_, str, data) => {
+ const arr = [...repeatPatterns];
+ arr[idx].date = str;
+ setRepeatPatterns(arr);
+ }}
+ appendTo={() => document.body} />
+ {timePicker(idx)}
+ </>}
+ {repeat !== "no" && <FlexItem align={{ default: 'alignRight' }}>
+ <InputGroup>
+ <Button aria-label={_("Remove")}
+ variant="secondary"
+ isDisabled={repeatPatterns.length == 1}
+ onClick={() => setRepeatPatterns(repeatPatterns.filter((item, item_idx) => idx != item_idx))}>
+ <MinusIcon />
+ </Button>
+ <Button aria-label={_("Add")}
+ variant="secondary"
+ onClick={() => {
+ if (repeat == "minutely")
+ setRepeatPatterns([...repeatPatterns, { key: repeatPatterns.length, second: "0" }]);
+ else if (repeat == "hourly")
+ setRepeatPatterns([...repeatPatterns, { key: repeatPatterns.length, minute: "0" }]);
+ else if (repeat == "daily")
+ setRepeatPatterns([...repeatPatterns, { key: repeatPatterns.length, time: "00:00" }]);
+ else if (repeat == "weekly")
+ setRepeatPatterns([...repeatPatterns, { key: repeatPatterns.length, day: "mon", time: "00:00" }]);
+ else if (repeat == "monthly")
+ setRepeatPatterns([...repeatPatterns, { key: repeatPatterns.length, day: 1, time: "00:00" }]);
+ else if (repeat == "yearly")
+ setRepeatPatterns([...repeatPatterns, { key: repeatPatterns.length, date: undefined, time: "00:00" }]);
+ }}>
+ <PlusIcon />
+ </Button>
+ </InputGroup>
+ </FlexItem>}
+ </Flex>
+ <FormHelper helperTextInvalid={helperTextInvalid} />
+ </FormGroup>
+ );
+ })}
+ </>}
+ </FormGroup>
+ </Form>
+ </Modal>
+ );
+};
diff --git a/pkg/systemd/timers.scss b/pkg/systemd/timers.scss
new file mode 100644
index 0000000..4c6a0eb
--- /dev/null
+++ b/pkg/systemd/timers.scss
@@ -0,0 +1,15 @@
+.timer-dialog {
+ // Limit the width of the components so that they fit in a single row
+ .pf-v5-c-date-picker:not(.create-timer-time-picker), select {
+ inline-size: 10rem;
+ max-inline-size: 10rem;
+ }
+
+ .create-timer-time-picker, .delay-number {
+ inline-size: 6rem;
+ }
+
+ .specific-repeat-group, .delay-group {
+ row-gap: var(--pf-v5-global--spacer--sm);
+ }
+}
diff --git a/pkg/users/account-create-dialog.js b/pkg/users/account-create-dialog.js
new file mode 100644
index 0000000..175a82f
--- /dev/null
+++ b/pkg/users/account-create-dialog.js
@@ -0,0 +1,590 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React from 'react';
+
+import { Bullseye } from "@patternfly/react-core/dist/esm/layouts/Bullseye/index.js";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { has_errors, is_valid_char_name } from "./dialog-utils.js";
+import { passwd_change } from "./password-dialogs.js";
+import { FormHelper } from "cockpit-components-form-helper";
+import { password_quality, PasswordFormFields } from "cockpit-components-password.jsx";
+import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
+import { HelpIcon } from '@patternfly/react-icons';
+
+const _ = cockpit.gettext;
+
+function get_default_home_dir(base_home_dir, user_name) {
+ return base_home_dir && user_name
+ ? base_home_dir + '/' + user_name
+ : "";
+}
+
+function AccountCreateBody({ state, errors, change, shells }) {
+ const {
+ real_name, user_name,
+ locked, change_passw_force,
+ shell,
+ } = state;
+
+ // We want to let user know that password and confirmation password do not match without them having to constantly click on "Create" to validate the form.
+ // But we also don't want to show an error message while they are typing the confirmation password, only when they are finished typing it.
+ // To solve the issue of telling that user is finished writing the confirmation password, let's do the following:
+ // The dialog does not validate if passwords match until:
+ // 1. they are at least the same length (which signals user has finished typing)
+ // OR
+ // 2. user submits the form (which also signals they are finished typing)
+ // Once that happens, and passwords do not match, the confirm password is validated after each keystroke.
+ let dynamic_password_confirm_error;
+ if (state.password_confirm_dirty)
+ dynamic_password_confirm_error = validate_password_confirm(state.password_confirm, state.password);
+
+ return (
+ <Form isHorizontal onSubmit={apply_modal_dialog}>
+ <FormGroup label={_("Full name")}
+ fieldId="accounts-create-real-name">
+ <TextInput id="accounts-create-real-name"
+ validated={(errors?.real_name) ? "error" : "default"}
+ value={real_name} onChange={(_event, value) => change("real_name", value)} />
+ <FormHelper fieldId="accounts-create-real-name" helperTextInvalid={errors?.real_name} />
+ </FormGroup>
+
+ <FormGroup label={_("User name")}
+ fieldId="accounts-create-user-name">
+ <TextInput id="accounts-create-user-name"
+ validated={(errors?.user_name) ? "error" : "default"}
+ value={user_name} onChange={(_event, value) => change("user_name", value)} />
+ <FormHelper fieldId="accounts-create-user-name" helperTextInvalid={errors?.user_name} />
+ </FormGroup>
+
+ <FormGroup label={_("Home directory")}
+ fieldId="accounts-create-user-home-dir">
+ <TextInput id="accounts-create-user-home-dir"
+ onChange={(_event, value) => change("home_dir", value)}
+ placeholder={_("Path to directory")}
+ value={state.home_dir} />
+ <FormHelper fieldId="accounts-create-user-home-dir" helperTextInvalid={errors?.home_dir} />
+ </FormGroup>
+
+ <FormGroup label={_("Shell")}
+ fieldId="accounts-create-user-shell">
+ <FormSelect
+ data-selected={shell}
+ id="accounts-create-user-shell"
+ onChange={(_, selection) => { change("shell", selection) }}
+ value={shell}>
+ { shells.map(shell_path => <FormSelectOption key={shell_path} value={shell_path} label={shell_path} />) }
+ </FormSelect>
+ </FormGroup>
+
+ <FormGroup label={_("User ID")}
+ fieldId="accounts-create-user-uid">
+ <TextInput id="accounts-create-user-uid"
+ onChange={(_event, value) => change("uid", value)}
+ value={state.uid} />
+ <FormHelper fieldId="accounts-create-user-uid" helperTextInvalid={errors?.uid} />
+ </FormGroup>
+
+ <FormGroup label={_("Authentication")} fieldId="accounts-create-locked" hasNoPaddingTop>
+ <Radio id="account-use-password"
+ label={_("Use password")}
+ isChecked={!locked} onChange={(_, checked) => change("locked", !checked)}
+ description={
+ <Checkbox id="accounts-create-force-password-change"
+ className="pf-v5-u-mb-xs"
+ label={_("Require password change on first login")}
+ isChecked={change_passw_force} onChange={(_event, checked) => change("change_passw_force", checked)} />
+ } />
+
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <FlexItem spacer={{ default: 'spacerNone' }}>
+ <Radio id="accounts-create-locked"
+ isChecked={locked} onChange={(_, checked) => change("locked", checked)}
+ label={_("Disallow password authentication")} />
+ </FlexItem>
+
+ <FlexItem spacer={{ default: 'spacerNone' }}>
+ <Popover bodyContent={_("Other authentication methods are still available even when interactive password authentication is not allowed.")}
+ showClose={false}>
+ <HelpIcon />
+ </Popover>
+ </FlexItem>
+ </Flex>
+ </FormGroup>
+
+ <PasswordFormFields password_label={_("Password")}
+ password_confirm_label={_("Confirm password")}
+ error_password={errors?.password}
+ error_password_confirm={dynamic_password_confirm_error || errors?.password_confirm}
+ idPrefix="accounts-create-password"
+ change={change} />
+ </Form>
+ );
+}
+
+function validate_username(username, accounts) {
+ if (!username)
+ return _("No user name specified");
+
+ for (let i = 0; i < username.length; i++) {
+ if (!is_valid_char_name(username[i]))
+ return _("The user name can only consist of letters from a-z, digits, dots, dashes and underscores.");
+ }
+
+ for (let k = 0; k < accounts.length; k++) {
+ if (accounts[k].name == username)
+ return _("This user name already exists");
+ }
+
+ return null;
+}
+
+function validate_real_name(real_name) {
+ if (!real_name)
+ return _("No real name specified");
+
+ const real_name_chars = Array.from(real_name);
+ if (real_name_chars.includes(':'))
+ return _("The full name must not contain colons.");
+}
+
+function validate_uid(uid, accounts, min_uid, max_uid, change) {
+ if (!uid)
+ return undefined;
+
+ const uid_number = Number(uid);
+ if (!Number.isInteger(uid_number) || uid_number < 0)
+ return _("User ID must be a positive integer");
+
+ if (min_uid && uid_number < min_uid)
+ return cockpit.format(_("User ID must not be lower than $0"), min_uid);
+
+ if (max_uid && uid_number > max_uid)
+ return cockpit.format(_("User ID must not be higher than $0"), max_uid);
+
+ if (accounts.some(account => account.uid === uid_number)) {
+ change("uid_exists", true);
+ return _("User ID is already used by another user");
+ }
+}
+
+function validate_home_dir(dir, directoryExpected) {
+ return cockpit.spawn(["test", "!", directoryExpected ? "-d" : "-f", dir], { superuser: "require" });
+}
+
+function validate_password(password) {
+ if (!password)
+ return _("Empty password");
+
+ return null;
+}
+
+function validate_password_confirm(password_confirm, password) {
+ if (password_confirm !== password)
+ return _("The passwords do not match");
+
+ return null;
+}
+
+function suggest_username(realname) {
+ function remove_diacritics(str) {
+ const translate_table = {
+ a: '[àáâãäå]',
+ ae: 'æ',
+ c: '[čç]',
+ d: 'ď',
+ e: '[èéêë]',
+ i: '[íìïî]',
+ l: '[ĺľ]',
+ n: '[ňñ]',
+ o: '[òóôõö]',
+ oe: 'œ',
+ r: '[ŕř]',
+ s: 'š',
+ t: 'ť',
+ u: '[ùúůûűü]',
+ y: '[ýÿ]',
+ z: 'ž',
+ };
+ for (const i in translate_table)
+ str = str.replace(new RegExp(translate_table[i], 'g'), i);
+
+ for (let k = 0; k < str.length;) {
+ if (!is_valid_char_name(str[k]))
+ str = str.substr(0, k) + str.substr(k + 1);
+ else
+ k++;
+ }
+
+ return str;
+ }
+
+ let result = "";
+ const name = realname.split(' ');
+
+ if (name.length === 1)
+ result = name[0].toLowerCase();
+ else if (name.length > 1)
+ result = name[0][0].toLowerCase() + name[name.length - 1].toLowerCase();
+
+ return remove_diacritics(result);
+}
+
+export function account_create_dialog(accounts, min_uid, max_uid, shells) {
+ let dlg = null;
+
+ const used_ids = accounts.map(a => a.uid);
+ const uid = Math.max(min_uid, Math.max(...used_ids.filter(id => id < max_uid)) + 1);
+
+ const state = {
+ dialogLoading: true,
+ real_name: "",
+ user_name: "",
+ password: "",
+ password_confirm: "",
+ password_confirm_dirty: false,
+ locked: false,
+ confirm_weak: false,
+ change_passw_force: false,
+ base_home_dir: null,
+ shell: "",
+ uid,
+ uid_exists: false,
+ min_uid,
+ max_uid,
+ home_dir: "",
+ home_dir_dirty: false,
+ };
+ let errors = { };
+
+ let old_password = null;
+ let user_name_dirty = false;
+
+ function get_defaults() {
+ return cockpit.spawn(["useradd", "-D"], { superuser: "require", err: "message" })
+ .catch(e => console.warn("Could not get useradd defaults: ", e.message))
+ .then(defaults => {
+ let shell = "";
+ let base_home_dir = null;
+ defaults.split("\n").forEach(item => {
+ if (item.indexOf("SHELL=") === 0) {
+ shell = item.split("=")[1] || "/bin/bash";
+ } else if (item.indexOf("HOME=") === 0) {
+ base_home_dir = item.split("=")[1] || "";
+ }
+ });
+ change("shell", shell);
+ change("base_home_dir", base_home_dir);
+ })
+ .finally(() => change("dialogLoading", false));
+ }
+
+ function change(field, value) {
+ state[field] = value;
+ errors = { };
+
+ if (field == "user_name") {
+ user_name_dirty = true;
+ if (!state.home_dir_dirty)
+ state.home_dir = get_default_home_dir(state.base_home_dir, value);
+ }
+
+ if (!user_name_dirty && field == "real_name") {
+ const suggested_username = suggest_username(state.real_name);
+ state.user_name = suggested_username;
+ if (!state.home_dir_dirty)
+ state.home_dir = get_default_home_dir(state.base_home_dir, suggested_username);
+ }
+
+ if (state.password != old_password) {
+ state.confirm_weak = false;
+ old_password = state.password;
+ }
+
+ if (field == "change_passw_force")
+ state.locked = false;
+
+ // Once password and confirm password are the same length, validate them after each keystroke
+ if (field == "password_confirm" && value.length >= state.password.length)
+ state.password_confirm_dirty = true;
+
+ if (field == "locked")
+ state.change_passw_force = false;
+
+ if (field == "uid")
+ state.uid_exists = false;
+
+ if (field == "home_dir") {
+ state.home_dir_dirty = true;
+ state.home_dir_exists = false;
+ state.home_dir_is_file = false;
+ }
+
+ update();
+ }
+
+ function validate(force_weak, force_home, force_uid, real_name, user_name, password, password_confirm, uid, accounts, min_uid, max_uid, change) {
+ const errs = { };
+
+ errs.real_name = validate_real_name(real_name);
+ errs.password = validate_password(password);
+ errs.password_confirm = validate_password_confirm(password_confirm, password);
+
+ if (password.length > 256)
+ errs.password = _("Password is longer than 256 characters");
+
+ errs.user_name = validate_username(user_name, accounts);
+
+ const promises = [];
+ // only evaluate password score if no other password error si present
+ if (!errs.password) {
+ promises.push(
+ password_quality(password, force_weak)
+ .catch(ex => {
+ errs.password = (ex.message || ex.toString()).replaceAll("\n", " "); // not-covered: OS error
+ })
+ );
+ }
+ if (!force_uid)
+ errs.uid = validate_uid(uid, accounts, min_uid, max_uid, change);
+
+ promises.push(
+ validate_home_dir(state.home_dir, false)
+ .catch(() => {
+ errs.home_dir = cockpit.format(_("$0 is an existing file"), state.home_dir);
+ state.home_dir_is_file = true;
+ })
+ );
+
+ if (!force_home) {
+ promises.push(
+ validate_home_dir(state.home_dir, true)
+ .catch(() => {
+ errs.home_dir = cockpit.format(_("The home directory $0 already exists. Its ownership will be changed to the new user."), state.home_dir);
+ state.home_dir_exists = true;
+ })
+ );
+ }
+
+ return Promise.all(promises)
+ .then(() => {
+ errors = errs;
+ return !has_errors(errs);
+ });
+ }
+
+ function create(real_name, user_name, password, locked, uid, force_change, home_dir, force_home, force_uid) {
+ const prog = ["useradd", "--create-home", "-s", state.shell];
+ if (real_name) {
+ prog.push('-c');
+ prog.push(real_name);
+ }
+
+ if (uid) {
+ prog.push('-u');
+ prog.push(uid);
+ }
+
+ if (force_uid)
+ prog.push('-o'); // Create user with non-unique user-specified UID, useful at certain use cases
+
+ if (home_dir) {
+ prog.push('-d');
+ prog.push(home_dir);
+ }
+
+ prog.push(user_name);
+ return cockpit.spawn(prog, { superuser: "require", err: "message" })
+ .then(() => passwd_change(user_name, password))
+ .then(() => {
+ if (locked)
+ return cockpit.spawn([
+ "usermod",
+ user_name,
+ "--lock"
+ ], { superuser: "require", err: "message" });
+ if (force_change)
+ return cockpit.spawn([
+ "passwd",
+ "-e",
+ user_name
+ ], { superuser: "require", err: "message" });
+ })
+ .then(() => {
+ if (force_home) {
+ return cockpit.spawn(["id", user_name, "--group"], { superuser: "require", err: "message" })
+ .then(gid => cockpit.spawn(["chown", "-hR", `${user_name}:${gid.trim()}`, home_dir], { superuser: "require", err: "message" }));
+ }
+ });
+ }
+
+ function passwd_check(force_weak, force_home, force_uid, real_name, user_name, password, password_confirm, locked, home_dir, force_passwd_change, uid, accounts, min_uid, max_uid, change) {
+ return validate(force_weak, force_home, force_uid, real_name, user_name, password, password_confirm, uid, accounts, min_uid, max_uid, change).then(valid => {
+ if (valid)
+ return create(real_name, user_name, password, locked, uid, force_passwd_change, home_dir, force_home, force_uid);
+ else {
+ if (!errors.real_name && !errors.user_name && !errors.home_dir && !errors.uid && !errors.password_confirm && state.password.length <= 256)
+ state.confirm_weak = true;
+
+ // Once the form is submitted and passwords do not match, validate confirm password after each keystroke
+ if (errors.password_confirm)
+ state.password_confirm_dirty = true;
+
+ update();
+ return Promise.reject();
+ }
+ });
+ }
+
+ function update() {
+ const props = {
+ id: "accounts-create-dialog",
+ title: _("Create new account"),
+ };
+ if (state.dialogLoading) {
+ props.body = (
+ <Bullseye>
+ <Spinner />
+ </Bullseye>
+ );
+ } else {
+ props.body = <AccountCreateBody state={state} errors={errors} change={change} shells={shells} />;
+ }
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Create"),
+ style: "primary",
+ clicked: () => passwd_check(
+ false, // force weak password was NOT clicked
+ false, // force user with existing home directory was NOT clicked
+ false, // force user with non-unique UID was NOT clicked
+ state.real_name,
+ state.user_name,
+ state.password,
+ state.password_confirm,
+ state.locked,
+ state.home_dir,
+ state.change_passw_force,
+ state.uid,
+ accounts,
+ state.min_uid,
+ state.max_uid,
+ change
+ ),
+ disabled: state.confirm_weak || state.uid_exists || state.home_dir_exists || state.home_dir_is_file
+ }
+ ]
+ };
+ if (state.home_dir_exists) {
+ footer.actions.push(
+ {
+ caption: _("Create and change ownership of home directory"),
+ style: "warning",
+ clicked: () => passwd_check(
+ false, // force weak password was NOT clicked
+ true, // force user with existing home directory was WAS clicked
+ false, // force user with non-unique UID was NOT clicked
+ state.real_name,
+ state.user_name,
+ state.password,
+ state.password_confirm,
+ state.locked,
+ state.home_dir,
+ state.change_passw_force,
+ state.uid,
+ accounts,
+ state.min_uid,
+ state.max_uid,
+ change
+ ),
+ }
+ );
+ }
+ if (state.confirm_weak) {
+ footer.actions.push(
+ {
+ caption: _("Create account with weak password"),
+ style: "warning",
+ clicked: () => passwd_check(
+ true, // force weak password was WAS clicked
+ false, // force user with existing home directory was NOT clicked
+ false, // force user with non-unique UID was NOT clicked
+ state.real_name,
+ state.user_name,
+ state.password,
+ state.password_confirm,
+ state.locked,
+ state.home_dir,
+ state.change_passw_force,
+ state.uid,
+ accounts,
+ state.min_uid,
+ state.max_uid,
+ change
+ ),
+ }
+ );
+ }
+ if (state.uid_exists) {
+ footer.actions.push(
+ {
+ caption: _("Create account with non-unique UID"),
+ style: "warning",
+ clicked: () => passwd_check(
+ false, // force weak password was NOT clicked
+ false, // force user with existing home directory was NOT clicked
+ true, // force user with non-unique UID was WAS clicked
+ state.real_name,
+ state.user_name,
+ state.password,
+ state.password_confirm,
+ state.locked,
+ state.home_dir,
+ state.change_passw_force,
+ state.uid,
+ accounts,
+ state.min_uid,
+ state.max_uid,
+ change
+ ),
+ }
+ );
+ }
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ update();
+ get_defaults();
+}
diff --git a/pkg/users/account-details.js b/pkg/users/account-details.js
new file mode 100644
index 0000000..6b46153
--- /dev/null
+++ b/pkg/users/account-details.js
@@ -0,0 +1,477 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useEffect, useRef } from 'react';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { EmptyState, EmptyStateActions, EmptyStateFooter, EmptyStateHeader, EmptyStateIcon, EmptyStateVariant } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { Label, LabelGroup } from "@patternfly/react-core/dist/esm/components/Label/index.js";
+import { Page, PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { ExclamationCircleIcon, HelpIcon, UndoIcon } from '@patternfly/react-icons';
+
+import cockpit from 'cockpit';
+import { superuser } from "superuser";
+import * as timeformat from "timeformat.js";
+import { apply_modal_dialog } from "cockpit-components-dialog.jsx";
+
+import { show_unexpected_error } from "./dialog-utils.js";
+import { delete_account_dialog } from "./delete-account-dialog.js";
+import { account_expiration_dialog, password_expiration_dialog } from "./expiration-dialogs.js";
+import { account_shell_dialog } from "./shell-dialog.js";
+import { set_password_dialog, reset_password_dialog } from "./password-dialogs.js";
+import { AccountLogs } from "./account-logs-panel.jsx";
+import { AuthorizedKeys } from "./authorized-keys-panel.js";
+import { get_locked } from "./utils.js";
+
+const _ = cockpit.gettext;
+
+function get_expire(name) {
+ function parse_expire(data) {
+ let account_expiration = '';
+ let account_date = null;
+
+ let password_expiration = '';
+ let password_days = null;
+
+ data.split('\n').forEach(line => {
+ const fields = line.split(': ');
+ if (fields[0] && fields[0].indexOf("Password expires") === 0) {
+ if (fields[1].indexOf("never") === 0) {
+ password_expiration = _("Never expire password");
+ } else if (fields[1].indexOf("password must be changed") === 0) {
+ password_expiration = _("Password must be changed");
+ } else {
+ password_expiration = cockpit.format(_("Require password change on $0"), timeformat.date(new Date(fields[1])));
+ }
+ } else if (fields[0] && fields[0].indexOf("Account expires") === 0) {
+ if (fields[1].indexOf("never") === 0) {
+ account_expiration = _("Never expire account");
+ } else {
+ account_date = new Date(fields[1] + " 12:00:00 UTC");
+ account_expiration = cockpit.format(_("Expire account on $0"), timeformat.date(new Date(fields[1])));
+ }
+ } else if (fields[0] && fields[0].indexOf("Maximum number of days between password change") === 0) {
+ password_days = fields[1];
+ }
+ });
+
+ return {
+ account_text: account_expiration,
+ account_date,
+ password_text: password_expiration,
+ password_days
+ };
+ }
+
+ return cockpit.spawn(["chage", "-l", name],
+ { environ: ["LC_ALL=C"], err: "message", superuser: "try" })
+ .catch(() => "")
+ .then(parse_expire);
+}
+
+export function AccountDetails({ accounts, groups, current_user, user, shells }) {
+ const [expiration, setExpiration] = useState(null);
+ useEffect(() => {
+ get_expire(user).then(setExpiration);
+
+ // Watch `/var/run/utmp` to register when user logs in or out
+ const handle = cockpit.file("/var/run/utmp", { superuser: "try", binary: true });
+ handle.watch(() => {
+ get_expire(user).then(setExpiration);
+ }, { read: false });
+ return handle.close;
+ }, [user, accounts]);
+
+ const [edited_real_name, set_edited_real_name] = useState(null);
+ const [committing_real_name, set_committing_real_name] = useState(false);
+
+ const [edited_locked, set_edited_locked] = useState(null);
+
+ function change_real_name() {
+ if (!edited_real_name)
+ return;
+
+ set_committing_real_name(true);
+
+ // TODO: unwanted chars check
+ cockpit.spawn(["/usr/sbin/usermod", user, "--comment", edited_real_name],
+ { superuser: "try", err: "message" })
+ .then(() => {
+ set_edited_real_name(null);
+ set_committing_real_name(false);
+ })
+ .catch(error => {
+ set_edited_real_name(null);
+ set_committing_real_name(false);
+ show_unexpected_error(error);
+ });
+ }
+
+ function change_locked(value, dont_retry_if_stuck) {
+ set_edited_locked(value);
+
+ cockpit.spawn(["/usr/sbin/usermod", user, value ? "--lock" : "--unlock"],
+ { superuser: "require", err: "message" })
+ .then(() => {
+ get_locked(user)
+ .then(locked => {
+ /* if we care about what the lock state should be and it doesn't match, try to change again
+ this is a workaround for different ways of handling a locked account
+ https://github.com/cockpit-project/cockpit/issues/1216
+ https://bugzilla.redhat.com/show_bug.cgi?id=853153
+ */
+ if (locked != value && !dont_retry_if_stuck) {
+ console.log("Account locked state doesn't match desired value, trying again.");
+ // only retry once to avoid uncontrolled recursion
+ change_locked(value, true);
+ } else
+ set_edited_locked(null);
+ });
+ })
+ .catch(error => {
+ set_edited_locked(null);
+ show_unexpected_error(error);
+ });
+ }
+
+ function logout_account() {
+ cockpit.spawn(["loginctl", "terminate-user", user],
+ { superuser: "try", err: "message" })
+ .then(() => {
+ get_expire(user).then(setExpiration);
+ })
+ .catch(show_unexpected_error);
+ }
+
+ if (!accounts.length) {
+ return (
+ <EmptyState variant={EmptyStateVariant.sm}>
+ <EmptyStateHeader titleText={_("Loading...")} headingLevel="h1" />
+ <EmptyStateFooter><Spinner size="xl" /></EmptyStateFooter>
+ </EmptyState>
+ );
+ }
+
+ const account = accounts.find(acc => acc.name == user);
+
+ if (!account) {
+ return (
+ <EmptyState variant={EmptyStateVariant.sm} id="account-failure">
+ <EmptyStateHeader titleText={<>{_("Account not available or cannot be edited.")}</>} icon={<EmptyStateIcon icon={ExclamationCircleIcon} />} headingLevel="h1" />
+ <EmptyStateFooter>
+ <EmptyStateActions>
+ <Breadcrumb>
+ <BreadcrumbItem to="#/">{_("Back to accounts")}</BreadcrumbItem>
+ </Breadcrumb>
+ </EmptyStateActions>
+ </EmptyStateFooter>
+ </EmptyState>
+ );
+ }
+
+ if (!expiration)
+ return null;
+
+ const self_mod_allowed = (user == current_user || !!superuser.allowed);
+
+ let title_name = account.gecos;
+ if (title_name)
+ title_name = title_name.split(',')[0];
+ else
+ title_name = account.name;
+
+ let last_login;
+ if (account.loggedIn)
+ last_login = _("Logged in");
+ else if (!account.lastLogin)
+ last_login = _("Never");
+ else
+ last_login = timeformat.dateTime(new Date(account.lastLogin));
+
+ const actions = superuser.allowed && (
+ <>
+ <Button variant="secondary" onClick={() => logout_account()} id="account-logout"
+ isDisabled={!account.loggedIn || account.uid == 0}>
+ {_("Terminate session")}
+ </Button>
+ { "\n" }
+ <Button isDisabled={account.uid == 0} variant="danger" id="account-delete"
+ onClick={() => delete_account_dialog(account)}>
+ {_("Delete")}
+ </Button>
+ </>
+ );
+
+ return (
+ <Page id="account">
+ <PageBreadcrumb stickyOnBreakpoint={{ default: "top" }}>
+ <Breadcrumb>
+ <BreadcrumbItem to="#/">{_("Accounts")}</BreadcrumbItem>
+ <BreadcrumbItem isActive>{title_name}</BreadcrumbItem>
+ </Breadcrumb>
+ </PageBreadcrumb>
+ <PageSection>
+ <Gallery hasGutter>
+ <Card className="account-details" id="account-details">
+ <CardHeader actions={{ actions }}>
+ <CardTitle id="account-title" component="h2">{title_name}</CardTitle>
+ </CardHeader>
+ <CardBody>
+ <Form isHorizontal onSubmit={apply_modal_dialog}>
+ <FormGroup fieldId="account-real-name" hasNoPaddingTop={!superuser.allowed} label={_("Full name")}>
+ { superuser.allowed
+ ? <TextInput id="account-real-name"
+ isDisabled={committing_real_name || account.uid == 0}
+ value={edited_real_name !== null ? edited_real_name : account.gecos}
+ onKeyDown={event => {
+ if (event.key == "Enter") {
+ event.target.blur();
+ }
+ }}
+ onChange={(_event, value) => set_edited_real_name(value)}
+ onBlur={() => change_real_name()} />
+ : <output id="account-real-name">{account.gecos}</output>}
+ </FormGroup>
+ <FormGroup fieldId="account-user-name" hasNoPaddingTop label={_("User name")}>
+ <output id="account-user-name">{account.name}</output>
+ </FormGroup>
+ <AccountGroupsSelect key={account.name} loggedIn={account.loggedIn} name={account.name} groups={groups} />
+ <FormGroup fieldId="account-last-login" hasNoPaddingTop label={_("Last login")}>
+ <output id="account-last-login">{last_login}</output>
+ </FormGroup>
+ <FormGroup fieldId="account-locked" label={_("Options")} hasNoPaddingTop>
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <FlexItem spacer={{ default: 'spacerNone' }}>
+ <Checkbox id="account-locked"
+ isDisabled={!superuser.allowed || edited_locked != null || user == current_user}
+ isChecked={edited_locked != null ? edited_locked : account.isLocked}
+ onChange={(_event, checked) => change_locked(checked)}
+ label={_("Disallow interactive password")} />
+ </FlexItem>
+
+ <Popover bodyContent={_("Other authentication methods are still available even when interactive password authentication is not allowed.")}
+ showClose={false}>
+ <HelpIcon />
+ </Popover>
+ <span id="account-expiration-text">
+ {expiration.account_text}
+ </span>
+ <Button onClick={() => account_expiration_dialog(account, expiration.account_date)}
+ isDisabled={!superuser.allowed}
+ variant="link"
+ isInline
+ id="account-expiration-button">
+ {_("edit")}
+ </Button>
+ </Flex>
+ </FormGroup>
+ { self_mod_allowed &&
+ <FormGroup fieldId="account-set-password" label={_("Password")}>
+ <div className="account-column-one">
+ { self_mod_allowed &&
+ <Button variant="secondary" id="account-set-password"
+ onClick={() => set_password_dialog(account, current_user)}>
+ {_("Set password")}
+ </Button>
+ }
+ { "\n" }
+ { superuser.allowed &&
+ <Button variant="secondary" id="password-reset-button"
+ onClick={() => reset_password_dialog(account)}>
+ {_("Force change")}
+ </Button>
+ }
+ </div>
+ <Flex flex={{ default: 'inlineFlex' }}>
+ <span id="password-expiration-text">
+ {expiration.password_text}
+ </span>
+ <Button onClick={() => password_expiration_dialog(account, expiration.password_days)}
+ isDisabled={!superuser.allowed}
+ variant="link"
+ isInline
+ id="password-expiration-button">
+ {_("edit")}
+ </Button>
+ </Flex>
+ </FormGroup>
+ }
+ { account.home && <FormGroup fieldId="account-home-dir" hasNoPaddingTop label={_("Home directory")}>
+ <output id="account-home-dir">{account.home}</output>
+ </FormGroup> }
+ { account.shell && <FormGroup fieldId="account-shell" hasNoPaddingTop label={_("Shell")}>
+ <Flex flex={{ default: 'inlineFlex' }}>
+ <output id="account-shell">{account.shell}</output>
+ <Button onClick={() => account_shell_dialog(account, shells)}
+ isDisabled={!superuser.allowed}
+ variant="link"
+ isInline
+ id="change-shell-button">
+ {_("change")}
+ </Button>
+ </Flex>
+ </FormGroup> }
+ </Form>
+ </CardBody>
+ </Card>
+ <AuthorizedKeys name={account.name} home={account.home} allow_mods={self_mod_allowed} />
+ <AccountLogs name={account.name} />
+ </Gallery>
+ </PageSection>
+ </Page>
+ );
+}
+
+export const AccountGroupsSelect = ({ name, loggedIn, groups, setError }) => {
+ const [isOpenGroup, setIsOpenGroup] = useState(false);
+ const [selected, setSelected] = useState();
+ const [primaryGroupName, setPrimaryGroupName] = useState();
+ const [loading, setLoading] = useState(true);
+ const [history, setHistory] = useState([]);
+ const previousValue = useRef(null);
+
+ useEffect(() => {
+ const usedGroups = groups.filter(group => group.userlist.includes(name));
+ const primaryGroup = groups.find(group => group.userlistPrimary.includes(name));
+ const _primaryGroupName = primaryGroup?.name;
+ const _selected = usedGroups.map(group => group.name);
+ if (primaryGroup)
+ _selected.push(_primaryGroupName);
+
+ previousValue.current = _selected;
+ setSelected(_selected);
+ setLoading(false);
+ setPrimaryGroupName(_primaryGroupName);
+ }, [groups, setSelected, name, previousValue]);
+
+ const undoGroupChanges = () => {
+ const undoItem = history[history.length - 1];
+ if (undoItem.type === 'added') {
+ removeGroup(undoItem.name, true).then(() => setHistory(history.slice(0, -1)));
+ } else if (undoItem.type === 'removed') {
+ addGroup(undoItem.name, true).then(() => setHistory(history.slice(0, -1)));
+ }
+ };
+
+ const removeGroup = (group, isUndo) => {
+ if (!isUndo)
+ setHistory([...history, { type: 'removed', name: group }]);
+
+ setLoading(true);
+ return cockpit.spawn(["gpasswd", "-d", name, group], { superuser: "require", err: "message" })
+ .then(() => {
+ setIsOpenGroup(false);
+ }, show_unexpected_error);
+ };
+
+ const addGroup = (group, isUndo) => {
+ if (!isUndo)
+ setHistory([...history, { type: 'added', name: group }]);
+
+ setLoading(true);
+ return cockpit.spawn(["gpasswd", "-a", name, group], { superuser: "require", err: "message" })
+ .then(() => {
+ setIsOpenGroup(false);
+ }, show_unexpected_error);
+ };
+
+ const onSelectGroup = (event, selection) => {
+ if (selected.includes(selection)) {
+ removeGroup(selection);
+ } else {
+ addGroup(selection);
+ }
+ };
+
+ const chipGroupComponent = () => {
+ return (
+ <LabelGroup numLabels={10}>
+ {(selected || []).map((currentLabel, index) => {
+ const optional = currentLabel !== primaryGroupName && superuser.allowed
+ ? {
+ onClose: ev => {
+ ev.stopPropagation();
+ removeGroup(currentLabel);
+ }
+ }
+ : {};
+
+ return (
+ <Label key={currentLabel}
+ color={groups.find(group => group.name === currentLabel).isAdmin ? "gold" : "cyan"}
+ {...optional}
+ >
+ {currentLabel}
+ </Label>
+ );
+ })}
+ </LabelGroup>
+ );
+ };
+
+ return (
+ <FormGroup
+ fieldId="account-groups"
+ id="account-groups-form-group"
+ label={_("Groups")}
+ validated={history.length > 0 ? "warning" : "default"}
+ >
+ {superuser.allowed
+ ? <Select
+ chipGroupComponent={chipGroupComponent()}
+ isDisabled={!superuser.allowed || loading}
+ isOpen={isOpenGroup}
+ onSelect={onSelectGroup}
+ onToggle={(_, isOpen) => setIsOpenGroup(isOpen)}
+ selections={selected}
+ toggleId="account-groups"
+ variant="typeaheadmulti"
+ >
+ {groups.map((option, index) => (
+ <SelectOption
+ isDisabled={option.name == primaryGroupName}
+ key={index}
+ value={option.name}
+ />
+ ))}
+ </Select>
+ : chipGroupComponent()}
+ {(history.length > 0)
+ ? <HelperText className="pf-v5-c-form__helper-text">
+ <Flex>
+ {loggedIn && <HelperTextItem id="account-groups-helper" variant="warning">{_("The user must log out and log back in for the new configuration to take effect.")}</HelperTextItem>}
+ {history.length > 0 && <Button variant="link" id="group-undo-btn" isInline icon={<UndoIcon />} onClick={undoGroupChanges}>{_("Undo")}</Button>}
+ </Flex>
+ </HelperText>
+ : ''
+ }
+ </FormGroup>
+ );
+};
diff --git a/pkg/users/account-logs-panel.jsx b/pkg/users/account-logs-panel.jsx
new file mode 100644
index 0000000..9d4da82
--- /dev/null
+++ b/pkg/users/account-logs-panel.jsx
@@ -0,0 +1,88 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React, { useState } from 'react';
+
+import { Card, CardBody, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { ListingTable } from 'cockpit-components-table.jsx';
+
+import * as timeformat from "timeformat.js";
+import { useInit } from "hooks";
+
+const _ = cockpit.gettext;
+
+export function AccountLogs({ name }) {
+ const [logins, setLogins] = useState([]);
+ useInit(() => {
+ cockpit.spawn(["last", "--time-format", "iso", "-n", 25, name], { environ: ["LC_ALL=C"] })
+ .then(data => {
+ let logins = [];
+ data.split('\n').forEach(line => {
+ // Exclude still logged in and non user lines
+ if (!line.includes(name) || line.includes('still')) {
+ return;
+ }
+ // Exclude tmux/screen lines
+ if (line.includes('tmux') || line.includes('screen')) {
+ return;
+ }
+
+ // format:
+ // admin web console ::ffff:172.27.0. 2021-09-24T09:02:13+00:00 - 2021-09-24T09:04:20+00:00 (00:02)
+ const lines = line.split(/ +/);
+ const ended = new Date(lines[lines.length - 2]);
+ const started = new Date(lines[lines.length - 4]);
+ const from = lines[lines.length - 5];
+ if (isNaN(started.getTime()) || isNaN(ended.getTime())) {
+ return;
+ }
+
+ logins.push({
+ started,
+ ended,
+ from
+ });
+ });
+
+ // Only show 15 login lines
+ logins = logins.slice(0, 15);
+ setLogins(logins);
+ })
+ .catch(ex => console.error("Failed to call last:", ex)); // not-covered: OS error
+ }, [name]);
+
+ return (
+ <Card id="account-logs">
+ <CardTitle component="h2">{_("Login history")}</CardTitle>
+ <CardBody className="contains-list">
+ <ListingTable variant="compact" aria-label={ _("Login history list") }
+ columns={ [
+ { title: _("Started") },
+ { title: _("Ended") },
+ { title: _("From") },
+ ] }
+ rows={ logins.map((line, index) => ({
+ props: { key: index },
+ columns: [timeformat.dateTime(line.started), timeformat.dateTime(line.ended), line.from]
+ }))} />
+ </CardBody>
+ </Card>
+ );
+}
diff --git a/pkg/users/accounts-list.js b/pkg/users/accounts-list.js
new file mode 100644
index 0000000..abd3971
--- /dev/null
+++ b/pkg/users/accounts-list.js
@@ -0,0 +1,489 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React, { useState, useEffect, useRef } from 'react';
+import { superuser } from "superuser";
+
+import { admins } from './users.js';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
+import { Card, CardExpandableContent, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { Dropdown, DropdownItem, DropdownSeparator, KebabToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { Label } from "@patternfly/react-core/dist/esm/components/Label/index.js";
+import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+import { SearchInput } from "@patternfly/react-core/dist/esm/components/SearchInput/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js";
+import * as timeformat from "timeformat.js";
+import { EmptyStatePanel } from 'cockpit-components-empty-state.jsx';
+import { ListingTable } from 'cockpit-components-table.jsx';
+import { SearchIcon } from '@patternfly/react-icons';
+import { SortByDirection } from "@patternfly/react-table";
+import { account_create_dialog } from "./account-create-dialog.js";
+import { delete_account_dialog } from "./delete-account-dialog.js";
+import { group_create_dialog } from "./group-create-dialog.js";
+import { lockAccountDialog } from "./lock-account-dialog.js";
+import { logoutAccountDialog } from "./logout-account-dialog.js";
+import { GroupActions } from "./group-actions.jsx";
+
+import { usePageLocation } from "hooks";
+
+const _ = cockpit.gettext;
+
+const UserActions = ({ account }) => {
+ const [isKebabOpen, setKebabOpen] = useState(false);
+
+ const actions = [
+ <DropdownItem key="edit-user"
+ onClick={ev => { ev.preventDefault(); cockpit.location.go(account.name) }}>
+ {_("Edit user")}
+ </DropdownItem>,
+ ];
+
+ superuser.allowed && actions.push(
+ <DropdownSeparator key="separator-0" />,
+ <DropdownItem key="log-user-out"
+ isDisabled={account.uid === 0 || !account.loggedIn}
+ onClick={() => { setKebabOpen(false); logoutAccountDialog(account) }}>
+ {_("Log user out")}
+ </DropdownItem>,
+ <DropdownSeparator key="separator-1" />,
+ <DropdownItem key="lock-account"
+ isDisabled={account.isLocked}
+ onClick={() => { setKebabOpen(false); lockAccountDialog(account) }}>
+ {_("Lock account")}
+ </DropdownItem>,
+ <DropdownItem key="delete-account"
+ isDisabled={account.uid === 0}
+ className={account.uid === 0 ? "" : "delete-resource-red"}
+ onClick={() => { setKebabOpen(false); delete_account_dialog(account) }}>
+ {_("Delete account")}
+ </DropdownItem>,
+ );
+
+ const kebab = (
+ <Dropdown toggle={<KebabToggle onToggle={(_, isOpen) => setKebabOpen(isOpen)} />}
+ isPlain
+ isOpen={isKebabOpen}
+ position="right"
+ id="accounts-actions"
+ menuAppendTo={document.body}
+ dropdownItems={actions} />
+ );
+ return kebab;
+};
+
+const getGroupRow = (group, accounts, isUserCreatedGroup) => {
+ let groupColorClass;
+ if (group.isAdmin)
+ groupColorClass = "group-gold";
+ else if (group.members > 0)
+ groupColorClass = "group-cyan";
+ else
+ groupColorClass = "group-grey";
+
+ const columns = [
+ {
+ sortKey: group.name,
+ title: (
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <div className={"dot " + groupColorClass} />
+ <FlexItem>{group.name}</FlexItem>
+ </Flex>
+ ),
+ props: { width: 20, },
+ },
+ {
+ title: group.gid,
+ props: { width: 10, },
+ },
+ {
+ title: group.members,
+ props: { width: 20, },
+ },
+ {
+ title: (
+ <TextContent>
+ <Text component={TextVariants.p}>
+ {(group.userlistPrimary.concat(group.userlist))
+ .map(account => {
+ if (accounts.map(account => account.name).includes(account))
+ return <Text key={account} component={TextVariants.a} href={"#" + account}>{account}</Text>;
+ else
+ return account;
+ })
+ .reduce((acc, curr) => [...acc, ", ", curr], [])
+ .slice(1)}
+ </Text>
+ </TextContent>
+ ),
+ props: { width: 50, },
+ },
+ ];
+
+ if (superuser.allowed) {
+ columns.push(
+ {
+ title: <GroupActions group={group} accounts={accounts} isUserCreatedGroup={isUserCreatedGroup} />,
+ props: { className: "pf-v5-c-table__action" }
+ }
+ );
+ }
+
+ return { columns, props: { key: group.name } };
+};
+
+const getAccountRow = (account, current, groups) => {
+ const userGroups = groups.filter(group => group.gid === account.gid || group.userlist.find(accountName => accountName === account.name));
+ const userGroupLabels = userGroups.map(group => {
+ const color = group.isAdmin ? "gold" : "cyan";
+ return (
+ <Label key={group.name} variant="filled" color={color}>
+ {!group.isAdmin ? group.name : ("admin" + " (" + group.name + ")") }
+ </Label>
+ );
+ });
+
+ let loginText = "";
+ let loginSortKey = null;
+ if (account.loggedIn) {
+ loginText = _("Logged in");
+ loginSortKey = "logged in";
+ } else if (!account.lastLogin) {
+ loginText = _("Never logged in");
+ loginSortKey = "never";
+ } else {
+ loginSortKey = new Date(account.lastLogin);
+ loginText = timeformat.dateTime(loginSortKey);
+ }
+
+ const columns = [
+ {
+ title: (
+ <span>
+ <a href={"#/" + account.name}>{account.name}</a>
+ {current && <Badge id="current-account-badge">{_("Your account")}</Badge>}
+ </span>
+ ),
+ sortKey: account.name,
+ props: { width: 25, },
+ },
+ {
+ title: account.gecos.split(',')[0],
+ props: { width: 20, },
+ },
+ {
+ title: account.uid,
+ props: { width: 10, },
+ },
+ {
+ title: loginText,
+ sortKey: loginSortKey,
+ props: { width: 25, },
+ },
+ {
+ title: (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }}>
+ {userGroupLabels}
+ </Flex>
+ ),
+ props: { width: 20 },
+ },
+ {
+ title: <UserActions account={account} />,
+ props: { className: "pf-v5-c-table__action" }
+ },
+ ];
+
+ return { columns, props: { key: account.name } };
+};
+
+const mapGroupsToAccount = (accounts, groups) => {
+ return accounts.map(account => {
+ const accountGroups = [];
+ groups.forEach(group => {
+ if (group.userlist.find(accountName => accountName === account.name))
+ accountGroups.push(group.name);
+ });
+ account.groups = accountGroups;
+
+ return account;
+ });
+};
+
+const GroupsList = ({ groups, accounts, isExpanded, setIsExpanded, min_gid, max_gid }) => {
+ const { options } = usePageLocation();
+ const [currentTextFilter, setCurrentTextFilter] = useState(options.group || '');
+ const columns = [
+ { title: _("Group name"), sortable: true },
+ { title: _("ID"), sortable: true },
+ { title: _("# of users"), sortable: true },
+ { title: _("Accounts") },
+ ];
+ const filtered_groups = groups.filter(group => {
+ if (currentTextFilter !== "" &&
+ (group.name.toLowerCase().indexOf(currentTextFilter.toLowerCase()) === -1) &&
+ (group.gid.toString().indexOf(currentTextFilter.toLowerCase()) === -1))
+ return false;
+
+ return true;
+ });
+ const ref = useRef();
+
+ useEffect(() => {
+ if (ref.current != currentTextFilter) {
+ ref.current = currentTextFilter;
+ const newOptions = { ...options };
+ if (currentTextFilter) {
+ newOptions.group = currentTextFilter;
+ } else {
+ delete newOptions.group;
+ }
+ cockpit.location.go([], newOptions);
+ } else {
+ setCurrentTextFilter(options.group || "");
+ }
+ }, [currentTextFilter, options]);
+
+ const sortRows = (rows, direction, idx) => {
+ // GID and members columns are numeric
+ const isNumeric = idx == 1 || idx == 2;
+ const sortedRows = rows.sort((a, b) => {
+ const aitem = a.columns[idx].sortKey || a.columns[idx].title;
+ const bitem = b.columns[idx].sortKey || b.columns[idx].title;
+ const aname = a.columns[0].sortKey;
+ const bname = b.columns[0].sortKey;
+
+ // administrator groups are always first
+ if (admins.includes(aname))
+ return direction === SortByDirection.asc ? -1 : 1;
+ if (admins.includes(bname))
+ return direction === SortByDirection.asc ? 1 : -1;
+
+ if (isNumeric)
+ return bitem - aitem;
+ else
+ return aitem.localeCompare(bitem);
+ });
+ return direction === SortByDirection.asc ? sortedRows : sortedRows.reverse();
+ };
+
+ const tableToolbar = (
+ <Toolbar>
+ <ToolbarContent className="groups-toolbar-header">
+ {isExpanded && <ToolbarItem>
+ <SearchInput id="groups-filter"
+ placeholder={_("Search for name or ID")}
+ value={currentTextFilter}
+ onChange={(_, val) => setCurrentTextFilter(val)}
+ onClear={() => setCurrentTextFilter('')} />
+ </ToolbarItem>}
+ { superuser.allowed &&
+ <>
+ {isExpanded && <ToolbarItem variant="separator" />}
+ <ToolbarItem align={{ md: 'alignRight' }}>
+ <Button variant="secondary" id="groups-create" onClick={() => group_create_dialog(groups, setIsExpanded, min_gid, max_gid)}>
+ {_("Create new group")}
+ </Button>
+ </ToolbarItem>
+ </>
+ }
+ </ToolbarContent>
+ </Toolbar>
+ );
+
+ return (
+ <Card className="ct-card" isExpanded={isExpanded}>
+ <CardHeader actions={{ actions: tableToolbar, hasNoOffset: true }}
+ className="ct-card-expandable-header"
+ onExpand={() => setIsExpanded(!isExpanded)}
+ toggleButtonProps={{
+ id: 'groups-view-toggle',
+ 'aria-label': _("Groups"),
+ 'aria-expanded': isExpanded
+ }}>
+ <CardTitle className="pf-v5-l-flex pf-m-align-items-center pf-m-space-items-md">
+ <Text component={TextVariants.h2}>{_("Groups")}</Text>
+ {(!isExpanded && !groups.length) && <HelperText> <HelperTextItem variant="indeterminate">{_("Loading...")}</HelperTextItem></HelperText>}
+ {(!isExpanded && filtered_groups.length > 0) && <>
+ {filtered_groups.slice(0, 3)
+ .map(group => {
+ const color = group.isAdmin ? "gold" : "cyan";
+ return (
+ <Label key={group.name} variant="filled" color={color}>
+ {group.name + ": " + (group.userlistPrimary.length + group.userlist.length)}
+ </Label>
+ );
+ })}
+ {filtered_groups.length > 3 && <Button key="more" className="group-more-btn" isInline variant='link' onClick={() => setIsExpanded(!isExpanded)}>
+ {cockpit.format(_("$0 more..."), filtered_groups.length - 3)}
+ </Button>}
+ </>}
+ </CardTitle>
+ </CardHeader>
+ <CardExpandableContent>
+ <ListingTable columns={columns}
+ id="groups-list"
+ rows={ filtered_groups.map(a => getGroupRow(a, accounts)) }
+ loading={ groups.length && accounts.length ? '' : _("Loading...") }
+ sortMethod={sortRows}
+ emptyComponent={<EmptyStatePanel title={_("No matching results")} icon={SearchIcon} />}
+ variant="compact" sortBy={{ index: 2, direction: SortByDirection.asc }} />
+ </CardExpandableContent>
+ </Card>
+ );
+};
+
+const AccountsList = ({ accounts, current_user, groups, min_uid, max_uid, shells }) => {
+ const { options } = usePageLocation();
+ const [currentTextFilter, setCurrentTextFilter] = useState(options.user || '');
+ const filtered_accounts = accounts.filter(account => {
+ if (currentTextFilter !== "" &&
+ (account.name.toLowerCase().indexOf(currentTextFilter.toLowerCase()) === -1) &&
+ (account.gecos.toLowerCase().indexOf(currentTextFilter.toLowerCase()) === -1) &&
+ (account.uid.toString().indexOf(currentTextFilter.toLowerCase()) === -1) &&
+ (!account.groups.find(group => group.toLowerCase().indexOf(currentTextFilter.toLowerCase()) !== -1)))
+ return false;
+
+ return true;
+ });
+ const ref = useRef();
+
+ useEffect(() => {
+ if (ref.current != currentTextFilter) {
+ ref.current = currentTextFilter;
+ const newOptions = { ...options };
+ if (currentTextFilter) {
+ newOptions.user = currentTextFilter;
+ } else {
+ delete newOptions.user;
+ }
+ cockpit.location.go([], newOptions);
+ } else {
+ setCurrentTextFilter(options.user || "");
+ }
+ }, [currentTextFilter, options]);
+
+ const columns = [
+ { title: _("Username"), sortable: true },
+ { title: _("Full name"), sortable: true },
+ { title: _("ID"), sortable: true },
+ { title: _("Last active"), sortable: true },
+ { title: _("Group") },
+ ];
+
+ const sortRows = (rows, direction, idx) => {
+ const sortedRows = rows.sort((a, b) => {
+ const aitem = a.columns[idx];
+ const bitem = b.columns[idx];
+ const aname = a.columns[0];
+ const bname = b.columns[0];
+
+ // current user is always first
+ if (aname.sortKey === current_user)
+ return direction === SortByDirection.asc ? -1 : 1;
+ if (bname.sortKey === current_user)
+ return direction === SortByDirection.asc ? 1 : -1;
+ // sorting last login
+ if (idx === 3) {
+ if (aitem.sortKey === "logged in")
+ return -1;
+ if (bitem.sortKey === "logged in")
+ return 1;
+ if (aitem.sortKey === "never")
+ return 1;
+ if (bitem.sortKey === "never")
+ return -1;
+
+ return bitem.sortKey - aitem.sortKey;
+ }
+
+ if (idx == 2)
+ return bitem.title - aitem.title;
+ return ((typeof aitem == 'string' ? aitem : (aitem.sortKey || aitem.title)).localeCompare(typeof bitem == 'string' ? bitem : (bitem.sortKey || bitem.title)));
+ });
+ return direction === SortByDirection.asc ? sortedRows : sortedRows.reverse();
+ };
+
+ const tableToolbar = (
+ <Toolbar>
+ <ToolbarContent className="accounts-toolbar-header">
+ <ToolbarItem>
+ <SearchInput id="accounts-filter"
+ placeholder={_("Search for name, group or ID")}
+ value={currentTextFilter}
+ onChange={(_, val) => setCurrentTextFilter(val)}
+ onClear={() => setCurrentTextFilter('')} />
+ </ToolbarItem>
+ { superuser.allowed &&
+ <>
+ <ToolbarItem variant="separator" />
+ <ToolbarItem align={{ md: 'alignRight' }}>
+ <Button id="accounts-create" onClick={() => account_create_dialog(accounts, min_uid, max_uid, shells)}>
+ {_("Create new account")}
+ </Button>
+ </ToolbarItem>
+ </>
+ }
+ </ToolbarContent>
+ </Toolbar>
+ );
+
+ return (
+ <Card className="ct-card">
+ <CardHeader actions={{ actions: tableToolbar }}>
+ <CardTitle component="h2">{_("Accounts")}</CardTitle>
+ </CardHeader>
+ <ListingTable columns={columns}
+ id="accounts-list"
+ isEmptyStateInTable={currentTextFilter !== "" && filtered_accounts.length !== accounts.length}
+ rows={ filtered_accounts.map(a => getAccountRow(a, current_user === a.name, groups)) }
+ loading={ accounts.length ? '' : _("Loading...") }
+ sortMethod={sortRows}
+ emptyComponent={<EmptyStatePanel title={_("No matching results")} icon={SearchIcon} />}
+ variant="compact" sortBy={{ index: 0, direction: SortByDirection.asc }} />
+ </Card>
+
+ );
+};
+
+export const AccountsMain = ({ accountsInfo, current_user, groups, isGroupsExpanded, setIsGroupsExpanded, min_gid, max_gid, min_uid, max_uid, shells }) => {
+ const accounts = mapGroupsToAccount(accountsInfo, groups).filter(account => {
+ if ((account.uid < 1000 && account.uid !== 0) ||
+ account.shell.match(/^(\/usr)?\/sbin\/nologin/) ||
+ account.shell === '/bin/false')
+ return false;
+ return true;
+ });
+
+ return (
+ <Page id="accounts">
+ <PageSection>
+ <Stack hasGutter>
+ <GroupsList accounts={accounts} groups={groups} isExpanded={isGroupsExpanded} setIsExpanded={setIsGroupsExpanded} min_gid={min_gid} max_gid={max_gid} />
+ <AccountsList accounts={accounts} current_user={current_user} groups={groups} shells={shells} min_uid={min_uid} max_uid={max_uid} />
+ </Stack>
+ </PageSection>
+ </Page>
+ );
+};
diff --git a/pkg/users/authorized-keys-panel.js b/pkg/users/authorized-keys-panel.js
new file mode 100644
index 0000000..6cbe093
--- /dev/null
+++ b/pkg/users/authorized-keys-panel.js
@@ -0,0 +1,182 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React, { useState } from 'react';
+import { useObject, useEvent } from 'hooks.js';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { Dropdown, KebabToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+import { OverflowMenu, OverflowMenuContent, OverflowMenuControl, OverflowMenuDropdownItem, OverflowMenuGroup, OverflowMenuItem } from "@patternfly/react-core/dist/esm/components/OverflowMenu/index.js";
+import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+import { ListingTable } from 'cockpit-components-table.jsx';
+import { show_unexpected_error } from "./dialog-utils.js";
+import * as authorized_keys from './authorized-keys.js';
+
+const _ = cockpit.gettext;
+
+function AddAuthorizedKeyDialogBody({ state, change }) {
+ const { text } = state;
+
+ return (
+ <TextArea id="authorized-keys-text"
+ placeholder={_("Paste the contents of your public SSH key file here")}
+ className="form-control"
+ value={text} onChange={(_event, value) => change("text", value)} />
+ );
+}
+
+function add_authorized_key_dialog(keys) {
+ let dlg = null;
+ const state = {
+ text: ""
+ };
+
+ function change(field, value) {
+ state[field] = value;
+ update();
+ }
+
+ function update() {
+ const props = {
+ id: "add-authorized-key-dialog",
+ title: _("Add public key"),
+ body: <AddAuthorizedKeyDialogBody state={state} change={change} />
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Add"),
+ style: "primary",
+ clicked: () => {
+ return keys.add_key(state.text);
+ }
+ }
+ ]
+ };
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ update();
+}
+
+export function AuthorizedKeys({ name, home, allow_mods }) {
+ const manager = useObject(() => authorized_keys.instance(name, home),
+ manager => manager.close(),
+ [name, home]);
+ useEvent(manager, "changed");
+ const [openedMenu, setOpenedMenu] = useState([]);
+
+ function remove_key(raw) {
+ manager.remove_key(raw).catch(show_unexpected_error);
+ }
+
+ const { state, keys } = manager;
+ let error = "";
+
+ if (state == "access-denied")
+ error = _("You do not have permission to view the authorized public keys for this account.");
+ else if (state == "failed")
+ error = _("Failed to load authorized keys.");
+ else if (state == "ready")
+ error = _("There are no authorized public keys for this account.");
+ else
+ return null;
+
+ const actions = allow_mods && (
+ <Button variant="secondary" id="authorized-key-add" onClick={() => add_authorized_key_dialog(manager)}>
+ {_("Add key")}
+ </Button>
+ );
+
+ return (
+ <Card id="account-authorized-keys">
+ <CardHeader actions={{ actions }}>
+ <CardTitle component="h2">{_("Authorized public SSH keys")}</CardTitle>
+ </CardHeader>
+ <ListingTable
+ aria-label={ _("Authorized public SSH keys") }
+ id="account-authorized-keys-list"
+ showHeader={false}
+ emptyCaption={error}
+ columns={ [
+ { title: _("Name"), header: true },
+ { title: _("Fingerprint") },
+ { title: "" },
+ ] }
+ rows={keys.map(k => {
+ return {
+ columns: [
+ { title: k.comment || _("Unnamed"), props: { width: 20 } },
+ { title: k.fp || _("Invalid key"), props: { width: 80 } },
+ {
+ title: <OverflowMenu breakpoint="lg">
+ <OverflowMenuContent>
+ <OverflowMenuGroup groupType="button">
+ <OverflowMenuItem>
+ <Button key={k.fp} variant="secondary" onClick={() => remove_key(k.raw)}>
+ {_("Remove")}
+ </Button>
+ </OverflowMenuItem>
+ </OverflowMenuGroup>
+ </OverflowMenuContent>
+ <OverflowMenuControl>
+ <Dropdown position="right"
+ onSelect={() => {
+ if (openedMenu.indexOf(k.fp) >= 0)
+ setOpenedMenu(openedMenu.filter(m => m !== k.fp));
+ else
+ setOpenedMenu([...openedMenu, k.fp]);
+ }}
+ toggle={
+ <KebabToggle
+ onToggle={(_event, open) => {
+ if (open)
+ setOpenedMenu([...openedMenu, k.fp]);
+ else
+ setOpenedMenu(openedMenu.filter(m => m !== k.fp));
+ }}
+ />
+ }
+ isOpen={openedMenu.indexOf(k.fp) >= 0}
+ isPlain
+ dropdownItems={[<OverflowMenuDropdownItem key="delete" isShared onClick={() => remove_key(k.raw)}>
+ {_("Remove")}
+ </OverflowMenuDropdownItem>]}
+ />
+ </OverflowMenuControl>
+ </OverflowMenu>,
+ props: { className: "pf-v5-c-table__action" }
+ }
+ ],
+ props: { key: k.fp }
+ };
+ })} />
+ </Card>
+ );
+}
diff --git a/pkg/users/authorized-keys.js b/pkg/users/authorized-keys.js
new file mode 100644
index 0000000..1debf0c
--- /dev/null
+++ b/pkg/users/authorized-keys.js
@@ -0,0 +1,138 @@
+import cockpit from "cockpit";
+
+import lister from "./ssh-list-public-keys.sh";
+import adder from "./ssh-add-public-key.sh";
+
+const _ = cockpit.gettext;
+
+function AuthorizedKeys (user_name, home_dir) {
+ const self = this;
+ const dir = home_dir + "/.ssh";
+ const filename = dir + "/authorized_keys";
+ let file = null;
+ let watch = null;
+ let last_tag = null;
+
+ cockpit.event_target(self);
+
+ self.keys = [];
+ self.state = "loading";
+
+ function process_failure (ex) {
+ self.keys = [];
+ if (ex.problem == "access-denied") {
+ self.state = ex.problem;
+ } else if (ex.problem == "not-found") {
+ self.state = "ready";
+ } else {
+ self.state = "failed";
+ console.warn("Error processing authentication keys: " + ex);
+ }
+ self.dispatchEvent("changed");
+ }
+
+ function update_keys(keys, tag) {
+ if (tag !== last_tag)
+ return;
+
+ self.keys = keys;
+ self.state = "ready";
+ self.dispatchEvent("changed");
+ }
+
+ /*
+ * Splits up a strings like:
+ *
+ * 2048 SHA256:AAAAB3NzaC1yc2EAAAADAQ Comment Here (RSA)
+ * 2048 SHA256:AAAAB3NzaC1yc2EAAAADAQ (RSA)
+ */
+ const PUBKEY_RE = /^(\S+)\s+(\S+)\s+(.*)\((\S+)\)$/;
+
+ function parse_pubkeys(input) {
+ const keys = [];
+
+ return cockpit.script(lister)
+ .input(input + "\n")
+ .then(output => {
+ const lines = output.split("\n");
+
+ for (let i = 0; i + 1 < lines.length; i += 2) {
+ const obj = { raw: lines[i + 1] };
+ keys.push(obj);
+ const match = lines[i].trim().match(PUBKEY_RE);
+ obj.valid = !!match && !!obj.raw;
+ if (match) {
+ obj.size = match[1];
+ obj.fp = match[2];
+ obj.comment = match[3].trim();
+ if (obj.comment == "authorized_keys" || obj.comment == "no comment")
+ obj.comment = null;
+ obj.algorithm = match[4];
+
+ /* Old ssh-keygen versions need us to find the comment ourselves */
+ if (!obj.comment && obj.raw)
+ obj.comment = obj.raw
+ .split(" ")
+ .splice(0, 2)
+ .join(" ") || null;
+ }
+ }
+ return keys;
+ })
+ .catch(ex => { // not-covered: OS error
+ cockpit.warn("Failed to list public keys:", ex.toString()); // not-covered: OS error
+ return []; // not-covered: OS error
+ });
+ }
+
+ function parse_keys(content, tag, ex) {
+ last_tag = tag;
+
+ if (ex)
+ return process_failure(ex);
+
+ if (!content)
+ return update_keys([], tag);
+
+ parse_pubkeys(content)
+ .then(keys => update_keys(keys, tag));
+ }
+
+ self.add_key = function(key) {
+ return parse_pubkeys(key)
+ .then(keys => {
+ const obj = keys[0];
+ if (obj?.valid) {
+ return cockpit
+ .script(adder, [user_name, home_dir], { superuser: "try", err: "message" })
+ .input(obj.raw + "\n")
+ // eslint-disable-next-line prefer-promise-reject-errors
+ .catch(ex => Promise.reject(_("Error saving authorized keys: ") + ex)); // not-covered: OS error
+ } else {
+ return Promise.reject(_("The key you provided was not valid."));
+ }
+ });
+ };
+
+ // don't use cockpit.file.modify() here, as that doesn't preserve permissions
+ // (https://github.com/cockpit-project/cockpit/issues/18033)
+ self.remove_key = key => cockpit.spawn(
+ ["sed", "-i", "\\!^" + key + "$!d", filename],
+ { superuser: "try", err: "message" }
+ );
+
+ self.close = function() {
+ if (watch)
+ watch.remove();
+
+ if (file)
+ file.close();
+ };
+
+ file = cockpit.file(filename, { superuser: 'try' });
+ watch = file.watch(parse_keys);
+}
+
+export function instance(user_name, home_dir) {
+ return new AuthorizedKeys(user_name, home_dir);
+}
diff --git a/pkg/users/delete-account-dialog.js b/pkg/users/delete-account-dialog.js
new file mode 100644
index 0000000..6762103
--- /dev/null
+++ b/pkg/users/delete-account-dialog.js
@@ -0,0 +1,86 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React from 'react';
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
+
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+function DeleteAccountDialogBody({ state, change }) {
+ const { delete_files } = state;
+
+ return (
+ <Checkbox id="account-confirm-delete-files"
+ label={_("Delete files")}
+ isChecked={delete_files} onChange={(_event, checked) => change("delete_files", checked)} />
+ );
+}
+
+export function delete_account_dialog(account) {
+ let dlg = null;
+
+ const state = {
+ delete_files: false
+ };
+
+ function change(field, value) {
+ state[field] = value;
+ update();
+ }
+
+ function update() {
+ const props = {
+ id: "account-confirm-delete-dialog",
+ title: cockpit.format(_("Delete $0"), account.name),
+ body: <DeleteAccountDialogBody state={state} change={change} />
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Delete"),
+ style: "danger",
+ clicked: () => {
+ const prog = ["/usr/sbin/userdel"];
+ if (state.delete_files)
+ prog.push("-r");
+ prog.push(account.name);
+
+ return cockpit.spawn(prog, { superuser: "require", err: "message" })
+ .then(function () {
+ cockpit.location.go("/");
+ });
+ }
+ }
+ ]
+ };
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ update();
+}
diff --git a/pkg/users/delete-group-dialog.js b/pkg/users/delete-group-dialog.js
new file mode 100644
index 0000000..357bba9
--- /dev/null
+++ b/pkg/users/delete-group-dialog.js
@@ -0,0 +1,66 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React from 'react';
+import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { Text, TextContent } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export function delete_group_dialog(group) {
+ const props = {
+ id: "group-confirm-delete-dialog",
+ title: cockpit.format(_("Permanently delete $0 group?"), group.name),
+ body: group.userlistPrimary.length > 0
+ ? <Stack hasGutter>
+ <TextContent>
+ <Text>
+ {_("This group is the primary group for the following users:")}
+ </Text>
+ </TextContent>
+ <List>
+ {group.userlistPrimary.map(account => <ListItem className='list-item' key={account}>{account}</ListItem>)}
+ </List>
+ </Stack>
+ : null
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: group.userlistPrimary.length > 0 ? _("Force delete") : _("Delete"),
+ style: "danger",
+ clicked: () => {
+ const prog = ["groupdel"];
+ if (group.userlistPrimary.length > 0)
+ prog.push("-f");
+ prog.push(group.name);
+
+ return cockpit.spawn(prog, { superuser: "require", err: "message" });
+ }
+ }
+ ]
+ };
+
+ show_modal_dialog(props, footer);
+}
diff --git a/pkg/users/dialog-utils.js b/pkg/users/dialog-utils.js
new file mode 100644
index 0000000..56f5856
--- /dev/null
+++ b/pkg/users/dialog-utils.js
@@ -0,0 +1,73 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React from 'react';
+
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export function Validated({ errors, error_key, children }) {
+ const error = errors?.[error_key];
+ // We need to always render the <div> for the has-error
+ // class so that the input field keeps the focus when
+ // errors are cleared. Otherwise the DOM changes enough
+ // for the Browser to remove focus.
+ return (
+ <div className={error ? "ct-validation-wrapper has-error" : "ct-validation-wrapper"}>
+ { children }
+ { error ? <span className="help-block dialog-error">{error}</span> : null }
+ </div>
+ );
+}
+
+export function has_errors(errors) {
+ for (const field in errors) {
+ if (errors[field])
+ return true;
+ }
+ return false;
+}
+
+function show_error_dialog(title, message) {
+ const props = {
+ id: "error-popup",
+ title,
+ body: <p>{message}</p>
+ };
+
+ const footer = {
+ actions: [],
+ cancel_button: { text: _("Close"), variant: "secondary" }
+ };
+
+ show_modal_dialog(props, footer);
+}
+
+export function show_unexpected_error(error) {
+ show_error_dialog(_("Unexpected error"), error.message || error);
+}
+
+export function is_valid_char_name(c) {
+ return (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') ||
+ c == '.' || c == '_' || c == '-';
+}
diff --git a/pkg/users/expiration-dialogs.js b/pkg/users/expiration-dialogs.js
new file mode 100644
index 0000000..b521937
--- /dev/null
+++ b/pkg/users/expiration-dialogs.js
@@ -0,0 +1,242 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React from 'react';
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
+
+import { has_errors } from "./dialog-utils.js";
+import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
+import * as timeformat from "timeformat.js";
+import { FormHelper } from "cockpit-components-form-helper";
+
+const _ = cockpit.gettext;
+
+function AccountExpirationDialogBody({ state, errors, change, validate, update }) {
+ const { mode, before, date } = state;
+
+ return (
+ <Form className="expiration-modal" onSubmit={apply_modal_dialog}>
+ <FormGroup validated={errors?.date ? "error" : "default"}>
+ <Radio id="account-expiration-never" name="mode" value="never"
+ label={_("Never expire account")}
+ isChecked={mode == "never"} onChange={() => change("mode", "never")} />
+ <Radio id="account-expiration-expires" name="mode" value="expires"
+ label={
+ <Flex>
+ <span>{before}</span>
+ <DatePicker aria-label={_("Pick date")}
+ buttonAriaLabel={_("Toggle date picker")}
+ locale={timeformat.dateFormatLang()}
+ weekStart={timeformat.firstDayOfWeek()}
+ onChange={(_, str) => change("date", str)}
+ onBlur={() => { validate(); update() }}
+ invalidFormatText=""
+ id="account-expiration-input"
+ value={date}
+ appendTo={() => document.body}
+ isDisabled={mode !== "expires"} />
+ </Flex>
+ }
+ isChecked={mode == "expires"} onChange={() => change("mode", "expires")} />
+ <FormHelper helperTextInvalid={errors?.date} />
+ </FormGroup>
+ </Form>
+ );
+}
+
+export function account_expiration_dialog(account, expire_date) {
+ let dlg = null;
+
+ const parts = _("Expire account on");
+
+ const state = {
+ mode: expire_date ? "expires" : "never",
+ before: parts,
+ date: expire_date?.toISOString().substr(0, 10) ?? ""
+ };
+
+ let errors = { };
+
+ function change(field, value) {
+ state[field] = value;
+ errors = { };
+ update();
+ }
+
+ // Datepicker does not provide information about the validity of the date so we need to do it here
+ // https://github.com/patternfly/patternfly-react/issues/5564
+ function validate() {
+ errors = { };
+
+ if (state.mode == "expires") {
+ if (!state.date)
+ errors.date = _("Please specify an expiration date");
+ else {
+ const date = new Date(state.date + "T12:00:00Z");
+ if (isNaN(date.getTime()) || date.getTime() < 0)
+ errors.date = _("Invalid expiration date");
+ }
+ }
+
+ return !has_errors(errors);
+ }
+
+ function update() {
+ const props = {
+ id: "account-expiration",
+ title: _("Account expiration"),
+ body: <AccountExpirationDialogBody state={state} errors={errors} change={change} validate={validate} update={update} />
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Change"),
+ style: "primary",
+ clicked: () => {
+ if (validate()) {
+ const prog = ["/usr/sbin/usermod", "-e"];
+ if (state.mode == "expires") {
+ const date = new Date(state.date + "T12:00:00Z");
+ prog.push(date.toISOString().substr(0, 10));
+ } else
+ prog.push("");
+ prog.push(account.name);
+ return cockpit.spawn(prog, { superuser: true, err: "message" });
+ } else {
+ update();
+ return Promise.reject();
+ }
+ }
+ }
+ ]
+ };
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ update();
+}
+
+function PasswordExpirationDialogBody({ state, errors, change }) {
+ const { mode, before, after, days } = state;
+
+ return (
+ <Form className="expiration-modal" onSubmit={apply_modal_dialog}>
+ <FormGroup>
+ <Radio id="password-expiration-never" name="mode" value="never"
+ label={_("Never expire password")}
+ isChecked={mode == "never"} onChange={() => change("mode", "never")} />
+ <Radio id="password-expiration-expires" name="mode" value="expires"
+ label={<>
+ <span id="password-expiration-before">{before}</span>
+ <TextInput className="size-text-ct" id="password-expiration-input"
+ validated={(errors?.days) ? "error" : "default"}
+ value={days} onChange={(_event, value) => change("days", value)} isDisabled={mode != "expires"} />
+ <span id="password-expiration-after">{after}</span>
+ </>}
+ isChecked={mode == "expires"} onChange={() => change("mode", "expires")} />
+ <FormHelper helperTextInvalid={errors?.days} />
+ </FormGroup>
+ </Form>
+ );
+}
+
+export function password_expiration_dialog(account, expire_days) {
+ let dlg = null;
+
+ /* TRANSLATORS: This is split up and therefore cannot use ngettext plurals */
+ const parts = _("Require password change every $0 days").split("$0");
+
+ if (parseInt(expire_days) >= 99999)
+ expire_days = null;
+
+ const state = {
+ mode: expire_days ? "expires" : "never",
+ before: parts[0],
+ after: parts[1],
+ days: expire_days || ""
+ };
+
+ let errors = { };
+
+ function change(field, value) {
+ state[field] = value;
+ update();
+ }
+
+ function validate() {
+ errors = { };
+
+ if (state.mode == "expires") {
+ const days = parseInt(state.days);
+ if (isNaN(days) || days < 0)
+ errors.days = _("Invalid number of days");
+ }
+
+ return !has_errors(errors);
+ }
+
+ function update() {
+ const props = {
+ id: "password-expiration",
+ title: _("Password expiration"),
+ body: <PasswordExpirationDialogBody state={state} errors={errors} change={change} />,
+ variant: "small"
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Change"),
+ style: "primary",
+ clicked: () => {
+ if (validate()) {
+ const days = state.mode == "expires" ? parseInt(state.days) : 99999;
+ return cockpit.spawn(["passwd", "-x", String(days), account.name],
+ { superuser: true, err: "message" });
+ } else {
+ update();
+ return Promise.reject();
+ }
+ }
+ }
+ ]
+ };
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ update();
+}
diff --git a/pkg/users/group-actions.jsx b/pkg/users/group-actions.jsx
new file mode 100644
index 0000000..8d858f0
--- /dev/null
+++ b/pkg/users/group-actions.jsx
@@ -0,0 +1,57 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React, { useState } from 'react';
+
+import { Dropdown, DropdownItem, DropdownSeparator, KebabToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+
+import { delete_group_dialog } from "./delete-group-dialog.js";
+import { rename_group_dialog } from "./rename-group-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export const GroupActions = ({ group, accounts }) => {
+ const [isKebabOpen, setKebabOpen] = useState(false);
+
+ if (!group.isUserCreatedGroup)
+ return null;
+
+ const actions = [
+ <DropdownItem key="rename-group"
+ onClick={() => { setKebabOpen(false); rename_group_dialog(group.name) }}>
+ {_("Rename group")}
+ </DropdownItem>,
+ <DropdownSeparator key="separator" />,
+ <DropdownItem key="delete-group"
+ className="delete-resource-red"
+ onClick={() => { setKebabOpen(false); delete_group_dialog(group) }}>
+ {_("Delete group")}
+ </DropdownItem>
+ ];
+
+ const kebab = (
+ <Dropdown toggle={<KebabToggle onToggle={(_, isOpen) => setKebabOpen(isOpen)} />}
+ isPlain
+ isOpen={isKebabOpen}
+ position="right"
+ dropdownItems={actions} />
+ );
+ return kebab;
+};
diff --git a/pkg/users/group-create-dialog.js b/pkg/users/group-create-dialog.js
new file mode 100644
index 0000000..c2d87c1
--- /dev/null
+++ b/pkg/users/group-create-dialog.js
@@ -0,0 +1,157 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React from 'react';
+
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
+import { FormHelper } from "cockpit-components-form-helper";
+
+import { has_errors, is_valid_char_name } from "./dialog-utils.js";
+
+const _ = cockpit.gettext;
+
+function GroupCreateBody({ state, errors, change }) {
+ const {
+ name, id,
+ } = state;
+
+ return (
+ <Form isHorizontal onSubmit={apply_modal_dialog}>
+ <FormGroup label={_("Name")}
+ fieldId="groups-create-name">
+ <TextInput id="groups-create-name"
+ validated={(errors?.name) ? "error" : "default"}
+ value={name} onChange={(_event, value) => change("name", value)} />
+ <FormHelper fieldId="groups-create-name" helperTextInvalid={errors?.name} />
+ </FormGroup>
+
+ <FormGroup label={_("ID")}
+ hasNoPaddingTop
+ isStack
+ fieldId="groups-create-id">
+ <TextInput id="groups-create-id"
+ validated={(errors?.id) ? "error" : "default"}
+ value={id} onChange={(_event, value) => change("id", value)} />
+ <FormHelper fieldId="groups-create-id" helperTextInvalid={errors?.id} />
+ </FormGroup>
+ </Form>
+ );
+}
+
+function validate_name(name, groups) {
+ if (!name)
+ return _("No group name specified");
+
+ for (let i = 0; i < name.length; i++) {
+ if (!is_valid_char_name(name[i]))
+ return _("The group name can only consist of letters from a-z, digits, dots, dashes and underscores");
+ }
+
+ for (let k = 0; k < groups.length; k++) {
+ if (groups[k].name == name)
+ return _("A group with this name already exists");
+ }
+
+ return null;
+}
+
+function validate_group(id, groups) {
+ if (!id)
+ return _("No ID specified");
+
+ const id_num = parseInt(id);
+ if (id_num.toString() !== id || id_num < 0)
+ return _("The group ID must be positive integer");
+
+ return null;
+}
+
+export function group_create_dialog(groups, setGroupsCardExpanded, min_gid, max_gid) {
+ let dlg = null;
+ const state = {
+ name: "",
+ id: "",
+ };
+ let errors = { };
+
+ const gids = groups
+ .filter(g => g.name !== 'nobody')
+ .map(group => group.gid);
+
+ change("id", (Math.max(min_gid, Math.max(...gids.filter(id => id < max_gid)) + 1) + 1).toString());
+
+ function change(field, value) {
+ state[field] = value;
+ errors = { };
+
+ update();
+ }
+
+ function validate(name, id) {
+ const errs = { };
+
+ errs.name = validate_name(name, groups);
+ errs.id = validate_group(id, groups);
+ errors = errs;
+
+ return !has_errors(errs);
+ }
+
+ function create(name, id) {
+ const valid = validate(name, id);
+ if (valid) {
+ const group_add_cmd = ["groupadd", name, "-g", id];
+
+ return cockpit.spawn(group_add_cmd, { superuser: "require", err: "message" });
+ } else {
+ update();
+ return Promise.reject();
+ }
+ }
+
+ function update() {
+ const props = {
+ id: "groups-create-dialog",
+ title: _("Create new group"),
+ body: <GroupCreateBody state={state} errors={errors} change={change} />
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Create"),
+ style: "primary",
+ clicked: () => create(state.name, state.id).then(() => setGroupsCardExpanded(true))
+ }
+ ]
+ };
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ update();
+}
diff --git a/pkg/users/index.html b/pkg/users/index.html
new file mode 100644
index 0000000..71282a6
--- /dev/null
+++ b/pkg/users/index.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<html id="users-page">
+<head>
+ <title translate="yes">Local accounts</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link href="users.css" type="text/css" rel="stylesheet" />
+ <script src="../base1/cockpit.js"></script>
+ <script src="../base1/po.js"></script>
+ <script src="po.js"></script>
+ <script src="users.js"></script>
+</head>
+<body class="pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="page">
+ </div>
+</body>
+</html>
diff --git a/pkg/users/lock-account-dialog.js b/pkg/users/lock-account-dialog.js
new file mode 100644
index 0000000..3528999
--- /dev/null
+++ b/pkg/users/lock-account-dialog.js
@@ -0,0 +1,46 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export const lockAccountDialog = (account) => {
+ const props = {
+ id: "account-confirm-lock-dialog",
+ title: cockpit.format(_("Lock $0"), account.name),
+ };
+
+ const footer = {
+ actions: [
+ {
+ style: "danger",
+ caption: _("Lock"),
+ clicked: () => {
+ return cockpit.spawn(["/usr/sbin/usermod", account.name, "--lock"], { superuser: "require", err: "message" })
+ .catch(err => console.warn("Failed to log user out", err));
+ },
+ }
+ ]
+ };
+
+ show_modal_dialog(props, footer);
+};
diff --git a/pkg/users/logout-account-dialog.js b/pkg/users/logout-account-dialog.js
new file mode 100644
index 0000000..07936d9
--- /dev/null
+++ b/pkg/users/logout-account-dialog.js
@@ -0,0 +1,46 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+export const logoutAccountDialog = (account) => {
+ const props = {
+ id: "account-confirm-logout-dialog",
+ title: cockpit.format(_("Logout $0"), account.name),
+ };
+
+ const footer = {
+ actions: [
+ {
+ style: "primary",
+ caption: _("Log out"),
+ clicked: () => {
+ return cockpit.spawn(["loginctl", "terminate-user", account.name], { superuser: "try", err: "message" })
+ .catch(err => console.warn("Failed to log user out", err));
+ },
+ }
+ ]
+ };
+
+ show_modal_dialog(props, footer);
+};
diff --git a/pkg/users/manifest.json b/pkg/users/manifest.json
new file mode 100644
index 0000000..4ee89f2
--- /dev/null
+++ b/pkg/users/manifest.json
@@ -0,0 +1,19 @@
+{
+ "menu": {
+ "index": {
+ "label": "Accounts",
+ "order": 70,
+ "docs": [
+ {
+ "label": "Managing user accounts",
+ "url": "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console/managing-user-accounts-in-the-web-console_system-management-using-the-rhel-8-web-console"
+ }
+ ],
+ "keywords": [
+ {
+ "matches": ["user", "password", "useradd", "passwd", "username", "login", "access", "roles", "ssh", "keys"]
+ }
+ ]
+ }
+ }
+}
diff --git a/pkg/users/mock/ssh/1-input b/pkg/users/mock/ssh/1-input
new file mode 100644
index 0000000..6f050d4
--- /dev/null
+++ b/pkg/users/mock/ssh/1-input
@@ -0,0 +1,7 @@
+
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDpAQgUA3efyrzjxijOvvEPT4wtIk2GGVW8/Lw6tFa98yNpwT9xOZZnnyzlFwiCwjeBCqo8I7xwIPZVaoTb9yEuygUQj0FRi7CQfo6l63rgz3dXWMg09PYQgF3MX2Vo2fgMtsQgaZaHNJ+Mqew8Toe9dIQKup0gPrOw6mtAltmWUfDpDO33eljQV/BnjvHd3zoWw+EyJRUZ213FzfXnVVCOdY9sbIOT86rl6MpoGyCa+9OY6cSpVeFkCUwNplaYwGC4wBY74d0ZGI9GnSDEj1mU4XOSEWXSMFUxNcMkrIukZaNYzDYDsbgJJMy+M/DLfyrxQXpMe6MQQ5jhYXBm5NfF
+
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDpAQgUA3efyrzjxijOvvEPT4wtIk2GGVW8/Lw6tFa98yNpwT9xOZZnnyzlFwiCwjeBCqo8I7xwIPZVaoTb9yEuygUQj0FRi7CQfo6l63rgz3dXWMg09PYQgF3MX2Vo2fgMtsQgaZaHNJ+Mqew8Toe9dIQKup0gPrOw6mtAltmWUfDpDO33eljQV/BnjvHd3zoWw+EyJRUZ213FzfXnVVCOdY9sbIOT86rl6MpoGyCa+9OY6cSpVeFkCUwNplaYwGC4wBY74d0ZGI9GnSDEj1mU4XOSEWXSMFUxNcMkrIukZaNYzDYDsbgJJMy+M/DLfyrxQXpMe6MQQ5jhYXBm5NfF Comment Here
+# Comment here
+
+
diff --git a/pkg/users/mock/ssh/1-output b/pkg/users/mock/ssh/1-output
new file mode 100644
index 0000000..de99b7d
--- /dev/null
+++ b/pkg/users/mock/ssh/1-output
@@ -0,0 +1,4 @@
+2048 FINGERPRINT no comment (RSA)
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDpAQgUA3efyrzjxijOvvEPT4wtIk2GGVW8/Lw6tFa98yNpwT9xOZZnnyzlFwiCwjeBCqo8I7xwIPZVaoTb9yEuygUQj0FRi7CQfo6l63rgz3dXWMg09PYQgF3MX2Vo2fgMtsQgaZaHNJ+Mqew8Toe9dIQKup0gPrOw6mtAltmWUfDpDO33eljQV/BnjvHd3zoWw+EyJRUZ213FzfXnVVCOdY9sbIOT86rl6MpoGyCa+9OY6cSpVeFkCUwNplaYwGC4wBY74d0ZGI9GnSDEj1mU4XOSEWXSMFUxNcMkrIukZaNYzDYDsbgJJMy+M/DLfyrxQXpMe6MQQ5jhYXBm5NfF
+2048 FINGERPRINT no comment (RSA)
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDpAQgUA3efyrzjxijOvvEPT4wtIk2GGVW8/Lw6tFa98yNpwT9xOZZnnyzlFwiCwjeBCqo8I7xwIPZVaoTb9yEuygUQj0FRi7CQfo6l63rgz3dXWMg09PYQgF3MX2Vo2fgMtsQgaZaHNJ+Mqew8Toe9dIQKup0gPrOw6mtAltmWUfDpDO33eljQV/BnjvHd3zoWw+EyJRUZ213FzfXnVVCOdY9sbIOT86rl6MpoGyCa+9OY6cSpVeFkCUwNplaYwGC4wBY74d0ZGI9GnSDEj1mU4XOSEWXSMFUxNcMkrIukZaNYzDYDsbgJJMy+M/DLfyrxQXpMe6MQQ5jhYXBm5NfF Comment Here
diff --git a/pkg/users/mock/ssh/2-input b/pkg/users/mock/ssh/2-input
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pkg/users/mock/ssh/2-input
diff --git a/pkg/users/mock/ssh/2-output b/pkg/users/mock/ssh/2-output
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pkg/users/mock/ssh/2-output
diff --git a/pkg/users/mock/ssh/3-input b/pkg/users/mock/ssh/3-input
new file mode 100644
index 0000000..2c50121
--- /dev/null
+++ b/pkg/users/mock/ssh/3-input
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDpAQgUA3efyrzjxijOvvEPT4wtIk2GGVW8/Lw6tFa98yNpwT9xOZZnnyzlFwiCwjeBCqo8I7xwIPZVaoTb9yEuygUQj0FRi7CQfo6l63rgz3dXWMg09PYQgF3MX2Vo2fgMtsQgaZaHNJ+Mqew8Toe9dIQKup0gPrOw6mtAltmWUfDpDO33eljQV/BnjvHd3zoWw+EyJRUZ213FzfXnVVCOdY9sbIOT86rl6MpoGyCa+9OY6cSpVeFkCUwNplaYwGC4wBY74d0ZGI9GnSDEj1mU4XOSEWXSMFUxNcMkrIukZaNYzDYDsbgJJMy+M/DLfyrxQXpMe6MQQ5jhYXBm5NfF Comment Here \ No newline at end of file
diff --git a/pkg/users/mock/ssh/3-output b/pkg/users/mock/ssh/3-output
new file mode 100644
index 0000000..65ffe82
--- /dev/null
+++ b/pkg/users/mock/ssh/3-output
@@ -0,0 +1,2 @@
+2048 FINGERPRINT no comment (RSA)
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDpAQgUA3efyrzjxijOvvEPT4wtIk2GGVW8/Lw6tFa98yNpwT9xOZZnnyzlFwiCwjeBCqo8I7xwIPZVaoTb9yEuygUQj0FRi7CQfo6l63rgz3dXWMg09PYQgF3MX2Vo2fgMtsQgaZaHNJ+Mqew8Toe9dIQKup0gPrOw6mtAltmWUfDpDO33eljQV/BnjvHd3zoWw+EyJRUZ213FzfXnVVCOdY9sbIOT86rl6MpoGyCa+9OY6cSpVeFkCUwNplaYwGC4wBY74d0ZGI9GnSDEj1mU4XOSEWXSMFUxNcMkrIukZaNYzDYDsbgJJMy+M/DLfyrxQXpMe6MQQ5jhYXBm5NfF Comment Here
diff --git a/pkg/users/mock/ssh/4-input b/pkg/users/mock/ssh/4-input
new file mode 100644
index 0000000..f6ea049
--- /dev/null
+++ b/pkg/users/mock/ssh/4-input
@@ -0,0 +1 @@
+foobar \ No newline at end of file
diff --git a/pkg/users/mock/ssh/4-output b/pkg/users/mock/ssh/4-output
new file mode 100644
index 0000000..627a821
--- /dev/null
+++ b/pkg/users/mock/ssh/4-output
@@ -0,0 +1,2 @@
+
+foobar
diff --git a/pkg/users/password-dialogs.js b/pkg/users/password-dialogs.js
new file mode 100644
index 0000000..089abac
--- /dev/null
+++ b/pkg/users/password-dialogs.js
@@ -0,0 +1,289 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React from 'react';
+import { superuser } from "superuser";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+import { has_errors } from "./dialog-utils.js";
+import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
+import { password_quality, PasswordFormFields } from "cockpit-components-password.jsx";
+import { FormHelper } from "cockpit-components-form-helper";
+
+const _ = cockpit.gettext;
+
+function passwd_self(old_pass, new_pass) {
+ const old_exps = [
+ /Current password: $/,
+ /Current Password: $/,
+ /.*\(current\) UNIX password: $/,
+ ];
+ const new_exps = [
+ /.*New password: $/,
+ /.*Retype new password: $/,
+ /.*Enter new \w*\s?password: $/,
+ /.*Retype new \w*\s?password: $/
+ ];
+ const bad_exps = [
+ /.*BAD PASSWORD:.*/
+ ];
+ const too_new_exps = [
+ /.*must wait longer to change.*/
+ ];
+
+ return new Promise((resolve, reject) => {
+ let buffer = "";
+ let sent_new = false;
+ let failure = _("Old password not accepted");
+
+ const timeout = window.setTimeout(function() {
+ failure = _("Prompting via passwd timed out");
+ proc.close("timeout");
+ }, 10 * 1000);
+
+ const proc = cockpit.spawn(["passwd"], { pty: true, environ: ["LC_ALL=C"], err: "out" })
+ .always(function() {
+ window.clearInterval(timeout);
+ })
+ .done(function() {
+ resolve();
+ })
+ .fail(function(ex) {
+ if (ex.exit_status || ex.problem == "timeout")
+ ex = new Error(failure);
+ reject(ex);
+ })
+ .stream(function(data) {
+ buffer += data;
+
+ for (let i = 0; i < too_new_exps.length; i++) {
+ if (too_new_exps[i].test(buffer)) {
+ failure = _("You must wait longer to change your password");
+ }
+ }
+
+ if (sent_new) {
+ for (let i = 0; i < bad_exps.length; i++) {
+ if (bad_exps[i].test(buffer)) {
+ failure = _("New password was not accepted");
+ }
+ }
+ }
+
+ for (let i = 0; i < old_exps.length; i++) {
+ if (old_exps[i].test(buffer)) {
+ buffer = "";
+ this.input(old_pass + "\n", true);
+ return;
+ }
+ }
+
+ for (let i = 0; i < new_exps.length; i++) {
+ if (new_exps[i].test(buffer)) {
+ buffer = "";
+ this.input(new_pass + "\n", true);
+ failure = _("Failed to change password");
+ sent_new = true;
+ return;
+ }
+ }
+ });
+ });
+}
+
+export function passwd_change(user, new_pass) {
+ return new Promise((resolve, reject) => {
+ cockpit.spawn(["chpasswd"], { superuser: "require", err: "out" })
+ .input(user + ":" + new_pass)
+ .done(function() {
+ resolve();
+ })
+ .fail(function(ex, response) {
+ if (ex.exit_status) {
+ console.log(ex);
+ if (response)
+ ex = new Error(response);
+ else
+ ex = new Error(_("Failed to change password"));
+ }
+ reject(ex);
+ });
+ });
+}
+
+function SetPasswordDialogBody({ state, errors, change }) {
+ const { need_old, password_old, current_user } = state;
+
+ return (
+ <Form isHorizontal onSubmit={apply_modal_dialog}>
+ { need_old &&
+ <>
+ <input hidden disabled value={current_user} />
+ <FormGroup label={_("Old password")}
+ fieldId="account-set-password-old">
+ <TextInput className="check-passwords" type="password" id="account-set-password-old"
+ autoComplete="current-password" value={password_old} onChange={(_event, value) => change("password_old", value)} />
+ <FormHelper helperTextInvalid={errors?.password_old} />
+ </FormGroup>
+ </> }
+ <PasswordFormFields password_label={_("New password")}
+ password_confirm_label={_("Confirm new password")}
+ error_password={errors?.password}
+ error_password_confirm={errors?.password_confirm}
+ idPrefix="account-set-password"
+ change={change} />
+ </Form>
+ );
+}
+
+export function set_password_dialog(account, current_user) {
+ let dlg = null;
+
+ const change_self = (account.name == current_user && !superuser.allowed);
+
+ const state = {
+ need_old: change_self,
+ current_user,
+ password_old: "",
+ password: "",
+ password_confirm: "",
+ confirm_weak: false,
+ };
+
+ let errors = { };
+
+ let old_password = null;
+
+ function change(field, value) {
+ state[field] = value;
+
+ if (state.password != old_password) {
+ state.confirm_weak = false;
+ old_password = state.password;
+ errors = { };
+ }
+
+ update();
+ }
+
+ function validate(force, password, password_confirm) {
+ const errs = { };
+
+ if (password != password_confirm)
+ errs.password_confirm = _("The passwords do not match");
+
+ if (password.length > 256)
+ errs.password = _("Password is longer than 256 characters");
+
+ return password_quality(password, force)
+ .catch(ex => {
+ errs.password = (ex.message || ex.toString()).replaceAll("\n", " ");
+ })
+ .then(() => {
+ errors = errs;
+ return !has_errors(errs);
+ });
+ }
+
+ function passwd_check(force, password, password_confirm, password_old) {
+ return validate(force, password, password_confirm).then(valid => {
+ if (valid) {
+ if (change_self)
+ return passwd_self(password_old, password);
+ else
+ return passwd_change(account.name, password);
+ } else {
+ if (!errors.password_confirm && state.password.length <= 256) {
+ state.confirm_weak = true;
+ }
+ update();
+ return Promise.reject();
+ }
+ });
+ }
+
+ function update() {
+ const props = {
+ id: "account-set-password-dialog",
+ title: _("Set password"),
+ body: <SetPasswordDialogBody state={state} errors={errors} change={change} />
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Set password"),
+ style: "primary",
+ clicked: () => {
+ return passwd_check(false, state.password, state.password_confirm, state.password_old);
+ },
+ disabled: state.confirm_weak
+ }
+ ]
+ };
+ if (state.confirm_weak) {
+ footer.actions.push(
+ {
+ caption: _("Set weak password"),
+ style: "warning",
+ clicked: () => {
+ return passwd_check(true, state.password, state.password_confirm, state.password_old);
+ }
+ }
+ );
+ }
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ update();
+}
+
+export function reset_password_dialog(account) {
+ const msg = cockpit.format(_("The account '$0' will be forced to change their password on next login"),
+ account.name);
+
+ const props = {
+ id: "password-reset",
+ title: _("Force password change"),
+ body: <p>{msg}</p>
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Reset password"),
+ style: "primary",
+ clicked: () => {
+ return cockpit.spawn(["passwd", "-e", account.name],
+ { superuser: true, err: "message" });
+ }
+ }
+ ]
+ };
+
+ show_modal_dialog(props, footer);
+}
diff --git a/pkg/users/rename-group-dialog.jsx b/pkg/users/rename-group-dialog.jsx
new file mode 100644
index 0000000..645e601
--- /dev/null
+++ b/pkg/users/rename-group-dialog.jsx
@@ -0,0 +1,85 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React from 'react';
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+
+import { apply_modal_dialog, show_modal_dialog } from "cockpit-components-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+function RenameGroupDialogBody({ state, change }) {
+ const { name } = state;
+
+ return (
+ <Form isHorizontal onSubmit={apply_modal_dialog}>
+ <FormGroup fieldId="group-name" label={_("New name")}>
+ <TextInput id="group-name" onChange={(_event, val) => change("name", val)} value={name} />
+ </FormGroup>
+ <HelperText>
+ <HelperTextItem variant="warning">{_("Renaming a group may affect sudo and similar rules")}</HelperTextItem>
+ </HelperText>
+ </Form>
+ );
+}
+
+export function rename_group_dialog(group) {
+ let dlg = null;
+
+ const state = {
+ name: group
+ };
+
+ function change(field, value) {
+ state[field] = value;
+ update();
+ }
+
+ function update() {
+ const props = {
+ id: "group-confirm-rename-dialog",
+ title: cockpit.format(_("Rename group $0"), group),
+ body: <RenameGroupDialogBody state={state} change={change} />,
+ variant: 'small',
+ titleIconVariant: 'warning',
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Rename"),
+ style: "primary",
+ clicked: () => cockpit.spawn(["groupmod", group, "--new-name", state.name], { superuser: "require", err: "message" }).then(dlg.close)
+ }
+ ]
+ };
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ update();
+}
diff --git a/pkg/users/shell-dialog.js b/pkg/users/shell-dialog.js
new file mode 100644
index 0000000..deecd26
--- /dev/null
+++ b/pkg/users/shell-dialog.js
@@ -0,0 +1,102 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import React from 'react';
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/index.js";
+
+import { has_errors } from "./dialog-utils.js";
+import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
+
+const _ = cockpit.gettext;
+
+function ShellDialogBody({ state, errors, change, shells }) {
+ const { shell } = state;
+ return (
+ <Form className="shell-modal" onSubmit={apply_modal_dialog}>
+ <FormGroup fieldId="edit-user-shell">
+ <FormSelect
+ data-selected={shell}
+ id="edit-user-shell"
+ onChange={(_, selection) => { change("shell", selection) }}
+ value={shell}>
+ { shells.map(shell_path => <FormSelectOption key={shell_path} value={shell_path} label={shell_path} />) }
+ </FormSelect>
+ </FormGroup>
+ </Form>
+ );
+}
+
+export function account_shell_dialog(account, shells) {
+ let dlg = null;
+
+ const state = {
+ shell: account.shell,
+ };
+
+ let errors = { };
+
+ function change(field, value) {
+ state[field] = value;
+ update();
+ }
+
+ function validate() {
+ errors = { };
+
+ return !has_errors(errors);
+ }
+
+ function update() {
+ const props = {
+ id: "shell-dialog",
+ title: _("Change shell"),
+ body: <ShellDialogBody state={state} errors={errors} change={change} shells={shells} />,
+ variant: "small"
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Change"),
+ style: "primary",
+ clicked: () => {
+ if (validate()) {
+ return cockpit.spawn(["usermod", "--shell", state.shell, account.name],
+ { superuser: true, err: "message" });
+ } else {
+ update();
+ return Promise.reject();
+ }
+ }
+ }
+ ]
+ };
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ update();
+}
diff --git a/pkg/users/ssh-add-public-key.sh b/pkg/users/ssh-add-public-key.sh
new file mode 100644
index 0000000..2fa435c
--- /dev/null
+++ b/pkg/users/ssh-add-public-key.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+set -euf
+mkdir -p "$2/.ssh"
+cd "$2/.ssh"
+
+chown $1 .
+
+touch authorized_keys 2> /dev/null || true
+chown $1 authorized_keys 2> /dev/null || true
+
+sed -i -e '$a\' authorized_keys
+cat >> authorized_keys
+
+chown $1 authorized_keys 2> /dev/null || true
+chmod 600 authorized_keys
diff --git a/pkg/users/ssh-list-public-keys.sh b/pkg/users/ssh-list-public-keys.sh
new file mode 100644
index 0000000..014590f
--- /dev/null
+++ b/pkg/users/ssh-list-public-keys.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+dir=$(mktemp -d)
+cd "$dir"
+
+process()
+{
+ if [ -z "$1" ]; then
+ return
+ fi
+ case "$1" in
+ \#*)
+ ;;
+ *)
+ echo "$1"> authorized_keys
+ echo "$(LC_ALL=C ssh-keygen -l -f authorized_keys)"
+ echo "$1"
+ ;;
+ esac
+}
+
+sed -e '$a\' | while read -r line; do
+ process "$line"
+done
+
+rm -f "$dir/authorized_keys"
+rmdir "$dir"
diff --git a/pkg/users/test-list-public-keys.sh b/pkg/users/test-list-public-keys.sh
new file mode 100755
index 0000000..be7c210
--- /dev/null
+++ b/pkg/users/test-list-public-keys.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+set -euf
+cd $(dirname $0)
+
+echo "1..4"
+
+# Normalize across various ssh-keygen versions
+normalize()
+{
+ sed -e 's/\([0-9]\+ \)[A-Za-z0-9:]\+ \+/\1FINGERPRINT /' \
+ -e 's/authorized_keys is not a public key file.//' \
+ -e 's/FINGERPRINT Comment Here (RSA)/FINGERPRINT no comment (RSA)/' \
+ -e 's/FINGERPRINT authorized_keys (RSA)/FINGERPRINT no comment (RSA)/'
+}
+
+test_compare()
+{
+ input="mock/ssh/$1-input"
+ if /bin/sh ./ssh-list-public-keys.sh < $input | normalize | diff -U3 mock/ssh/$1-output - >&2; then
+ echo "ok $1 $input"
+ else
+ echo "not ok $1 $input"
+ fi
+}
+
+for n in 1 2 3 4; do
+ test_compare $n
+done
diff --git a/pkg/users/users.js b/pkg/users/users.js
new file mode 100755
index 0000000..9278fe9
--- /dev/null
+++ b/pkg/users/users.js
@@ -0,0 +1,216 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import '../lib/patternfly/patternfly-5-cockpit.scss';
+import 'polyfills'; // once per application
+import 'cockpit-dark-theme'; // once per page
+
+import React, { useMemo, useState } from 'react';
+import { createRoot } from 'react-dom/client';
+
+import cockpit from 'cockpit';
+import { superuser } from "superuser";
+import { usePageLocation, useLoggedInUser, useFile, useInit } from "hooks.js";
+import { etc_passwd_syntax, etc_group_syntax, etc_shells_syntax } from "pam_user_parser.js";
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+
+import { get_locked } from "./utils.js";
+import { AccountsMain } from "./accounts-list.js";
+import { AccountDetails } from "./account-details.js";
+
+import "./users.scss";
+
+superuser.reload_page_on_change();
+
+export const admins = ['sudo', 'root', 'wheel'];
+const sortGroups = groups => {
+ return groups.sort((a, b) => {
+ if (a.isAdmin)
+ return -1;
+ if (b.isAdmin)
+ return 1;
+ if (a.members === b.members)
+ return a.name.localeCompare(b.name);
+ else
+ return b.members - a.members;
+ });
+};
+
+function AccountsPage() {
+ const [isGroupsExpanded, setIsGroupsExpanded] = useState(false);
+ const { path } = usePageLocation();
+ const accounts = useFile("/etc/passwd", { syntax: etc_passwd_syntax });
+ const groups = useFile("/etc/group", { syntax: etc_group_syntax });
+ const shells = useFile("/etc/shells", { syntax: etc_shells_syntax });
+ const current_user_info = useLoggedInUser();
+
+ // Handle the case where logindef == null, i.e. the file does not exist.
+ // While that's unusual, "empty /etc" is a goal, and it shouldn't crash the page.
+ const [min_gid, setMinGid] = useState(500);
+ const [max_gid, setMaxGid] = useState(60000);
+ const [min_uid, setMinUid] = useState(500);
+ const [max_uid, setMaxUid] = useState(60000);
+ const [details, setDetails] = useState(null);
+
+ useInit(() => {
+ // Watch `/var/run/utmp` to register when user logs in or out
+ const handleUtmp = cockpit.file("/var/run/utmp", { superuser: "try", binary: true });
+ handleUtmp.watch(() => getLogins().then(setDetails), { read: false });
+
+ // Watch /etc/shadow to register lock/unlock/expire changes; but avoid reading it, it's sensitive data
+ const handleShadow = cockpit.file("/etc/shadow", { superuser: "try" });
+ handleShadow.watch(() => getLogins().then(setDetails), { read: false });
+
+ const handleLogindef = cockpit.file("/etc/login.defs", { superuser: true });
+ handleLogindef.watch((logindef) => {
+ if (logindef === null)
+ return;
+
+ const minGid = parseInt(logindef.match(/^GID_MIN\s+(\d+)/m)[1]);
+ const maxGid = parseInt(logindef.match(/^GID_MAX\s+(\d+)/m)[1]);
+ const minUid = parseInt(logindef.match(/^UID_MIN\s+(\d+)/m)[1]);
+ const maxUid = parseInt(logindef.match(/^UID_MAX\s+(\d+)/m)[1]);
+
+ if (minGid)
+ setMinGid(minGid);
+ if (maxGid)
+ setMaxGid(maxGid);
+ if (minUid)
+ setMinUid(minUid);
+ if (maxUid)
+ setMaxUid(maxUid);
+ });
+
+ return [handleUtmp, handleShadow, handleLogindef];
+ }, [], null, handles => handles.forEach(handle => handle.close()));
+
+ // lastlog uses same sorting as /etc/passwd therefore arrays can be combined based on index
+ const accountsInfo = useMemo(() => {
+ if (accounts && details)
+ return accounts.map((account, i) => {
+ return Object.assign({}, account, details[i]);
+ });
+ else
+ return [];
+ }, [accounts, details]);
+
+ const groupsExtraInfo = useMemo(() => sortGroups(
+ (groups || []).map(group => {
+ const userlistPrimary = accountsInfo.filter(account => account.gid === group.gid).map(account => account.name);
+ const userlist = group.userlist.filter(el => el !== "");
+ return ({
+ ...group,
+ userlistPrimary,
+ userlist,
+ members: userlist.length + userlistPrimary.length,
+ isAdmin: admins.includes(group.name),
+ isUserCreatedGroup: group.gid >= min_gid && group.gid <= max_gid
+ });
+ })
+ ), [groups, accountsInfo, min_gid, max_gid]);
+
+ if (groupsExtraInfo.length == 0 || accountsInfo.length == 0) {
+ return <EmptyStatePanel loading />;
+ } else if (path.length === 0) {
+ return (
+ <AccountsMain
+ accountsInfo={accountsInfo}
+ current_user={current_user_info?.name}
+ groups={groupsExtraInfo || []}
+ isGroupsExpanded={isGroupsExpanded}
+ setIsGroupsExpanded={setIsGroupsExpanded}
+ min_gid={min_gid}
+ max_gid={max_gid}
+ min_uid={min_uid}
+ max_uid={max_uid}
+ shells={shells}
+ />
+ );
+ } else if (path.length === 1) {
+ return (
+ <AccountDetails accounts={accountsInfo} groups={groupsExtraInfo}
+ current_user={current_user_info?.name} user={path[0]} shells={shells} />
+ );
+ } else return null;
+}
+
+async function getLogins() {
+ let lastlog = "";
+ try {
+ lastlog = await cockpit.spawn(["lastlog"], { environ: ["LC_ALL=C"] });
+ } catch (err) {
+ console.warn("Unexpected error when getting last login information", err);
+ }
+
+ let currentLogins = [];
+ try {
+ const w = await cockpit.spawn(["w", "-sh"], { environ: ["LC_ALL=C"] });
+ currentLogins = w.split('\n').slice(0, -1).map(line => line.split(/ +/)[0]);
+ } catch (err) {
+ console.warn("Unexpected error when getting logged in accounts", err);
+ }
+
+ // shadow-utils passwd supports an --all flag which is lacking on RHEL and
+ // stable Fedora releases. Available at least on Fedora since
+ // shadow-utils-4.14.0-5.fc40 (currently known as rawhide).
+ const locked_users_map = {};
+ try {
+ const locked_statuses = await cockpit.spawn(["passwd", "-S", "--all"], { superuser: "require", err: "message", environ: ["LC_ALL=C"] });
+ // Slice off the last empty line
+ for (const line of locked_statuses.trim().split('\n')) {
+ const username = line.split(" ")[0];
+ const status = line.split(" ")[1];
+ locked_users_map[username] = status == "L";
+ }
+ } catch (err) {
+ // Only warn when it is unrelated to --all.
+ if (err.message && !err.message.includes("bad argument --all")) {
+ console.warn("Unexpected error when getting locked account information", err);
+ }
+ }
+
+ // drop header and last empty line with slice
+ const promises = lastlog.split('\n').slice(1, -1).map(async line => {
+ const splitLine = line.split(/[ \t]+/);
+ const name = splitLine[0];
+ // Fallback on passwd -S for Fedora and RHEL
+ const isLocked = locked_users_map[name] ?? await get_locked(name);
+
+ if (line.indexOf('**Never logged in**') > -1) {
+ return { name, loggedIn: false, lastLogin: null, isLocked };
+ }
+
+ const loggedIn = currentLogins.includes(name);
+
+ const date_fields = splitLine.slice(-5);
+ // this is impossible to parse with Date() (e.g. Firefox does not work with all time zones), so call `date` to parse it
+ return cockpit.spawn(["date", "+%s", "-d", date_fields.join(' ')], { environ: ["LC_ALL=C"], err: "out" })
+ .then(out => ({ name, loggedIn, lastLogin: parseInt(out) * 1000, isLocked }))
+ .catch(e => console.warn(`Failed to parse date from lastlog line '${line}': ${e.toString()}`));
+ });
+
+ return Promise.all(promises);
+}
+
+function init() {
+ const root = createRoot(document.getElementById("page"));
+ root.render(<AccountsPage />);
+ document.body.removeAttribute("hidden");
+}
+
+document.addEventListener("DOMContentLoaded", init);
diff --git a/pkg/users/users.scss b/pkg/users/users.scss
new file mode 100644
index 0000000..08a0b94
--- /dev/null
+++ b/pkg/users/users.scss
@@ -0,0 +1,123 @@
+@use "ct-card";
+@use "page";
+
+// Import utilities for `pf-u...` classes
+@import "@patternfly/patternfly/utilities/Spacing/spacing.css";
+@import "global-variables.scss";
+
+#account .pf-v5-c-card {
+ @extend .ct-card;
+}
+
+.account-details .pf-v5-c-card__header {
+ margin-block-end: var(--pf-v5-global--spacer--lg);
+}
+
+#account .pf-v5-l-gallery {
+ --pf-v5-l-gallery--GridTemplateColumns: 1fr;
+}
+
+#account .pf-v5-c-page__main-section,
+#account .pf-v5-c-page__main-nav {
+ padding: var(--pf-v5-global--gutter);
+}
+
+.delete-resource-red {
+ color: var(--pf-v5-global--danger-color--200);
+}
+
+#account-details div.checkbox:first-child {
+ margin-block-start: 0;
+}
+
+#account-details div.checkbox:last-child {
+ margin-block-end: 0;
+}
+
+.size-text-ct {
+ display: inline;
+ inline-size: 8em;
+ text-align: start;
+ margin: 2px;
+}
+
+@media (min-width: 500px) {
+ .modal-sm-ct {
+ inline-size: 32rem;
+ }
+}
+
+.account-column-one {
+ min-inline-size: 280px;
+ display: inline-block;
+}
+
+.expiration-modal .pf-v5-c-form__group-control {
+ .pf-v5-c-radio:first-child {
+ padding-block-end: var(--pf-v5-global--spacer--sm);
+ }
+ // Work around for https://github.com/patternfly/patternfly/issues/4061
+ .pf-v5-c-radio:nth-child(2) input {
+ align-self: center;
+ }
+}
+
+.help-block {
+ white-space: pre-wrap;
+}
+
+.outline-question-circle-icon {
+ margin-inline-start: var(--pf-v5-global--spacer--sm);
+}
+
+.accounts-toolbar-header > .pf-v5-c-toolbar__content-section {
+ row-gap: var(--pf-v5-global--spacer--sm);
+}
+
+.dot {
+ block-size: 20px;
+ inline-size: 20px;
+ border-radius: 50%;
+ display: inline-block;
+}
+
+// Iterate through supported PF color names for light and dark modes
+@each $color in cyan gold {
+ .group-#{"" + $color} {
+ background-color: var(--pf-v5-global--palette--#{$color}-100);
+
+ .pf-v5-theme-dark & {
+ background-color: var(--pf-v5-global--palette--#{$color}-300);
+ }
+ }
+}
+// PF adapts this color properly across light and dark
+.group-grey {
+ background-color: var(--pf-v5-global--BorderColor--100);
+}
+
+.group-more-btn {
+ font-size: var(--pf-v5-global--FontSize--sm);
+}
+
+#groups-list td:first-child {
+ vertical-align: middle;
+}
+
+#groups-create, #account-create {
+ min-inline-size: 11rem;
+}
+
+#group-confirm-delete-dialog .list-item {
+ margin-inline-end: var(--pf-v5-global--spacer--lg);
+}
+
+// We cannot use the PF helper pf-v5-u-ml-lg here, as it doesn't support RTL yet
+#current-account-badge {
+ margin-inline-start: var(--pf-v5-global--spacer--lg);
+}
+
+// Pretty sure paths are LTR, even in an RTL display
+output {
+ direction: ltr;
+}
diff --git a/pkg/users/utils.js b/pkg/users/utils.js
new file mode 100644
index 0000000..7b2efed
--- /dev/null
+++ b/pkg/users/utils.js
@@ -0,0 +1,10 @@
+import cockpit from 'cockpit';
+
+export const get_locked = name =>
+ cockpit.spawn(["passwd", "-S", name], { environ: ["LC_ALL=C"], superuser: "require" })
+ .then(content => {
+ const status = content.split(" ")[1];
+ // libuser uses "LK", shadow-utils use "L".
+ return status == "LK" || status == "L";
+ })
+ .catch(() => null);
diff --git a/plans/all.fmf b/plans/all.fmf
new file mode 100644
index 0000000..0e297d4
--- /dev/null
+++ b/plans/all.fmf
@@ -0,0 +1,27 @@
+adjust+:
+ when: revdeps == yes
+ enabled: false
+
+discover:
+ how: fmf
+execute:
+ how: tmt
+
+# Let's handle them upstream only, don't break Fedora/RHEL reverse dependency gating
+environment:
+ TEST_AUDIT_NO_SELINUX: 1
+
+/basic:
+ summary: Run tests for basic packages
+ discover+:
+ test: /test/browser/basic
+
+/network:
+ summary: Run tests for cockpit-networkmanager
+ discover+:
+ test: /test/browser/network
+
+/optional:
+ summary: Run tests for optional packages
+ discover+:
+ test: /test/browser/optional
diff --git a/plans/podman.fmf b/plans/podman.fmf
new file mode 100644
index 0000000..ae58b13
--- /dev/null
+++ b/plans/podman.fmf
@@ -0,0 +1,29 @@
+# reverse dependency test
+enabled: false
+
+adjust+:
+ when: revdeps == yes
+ enabled: true
+
+discover:
+ how: fmf
+ url: https://github.com/cockpit-project/cockpit-podman
+ ref: "main"
+execute:
+ how: tmt
+
+# This has to duplicate cockpit-podman's plan structure; see https://github.com/teemtee/tmt/issues/1770
+/podman-system:
+ summary: Run cockpit-podman system tests
+ discover+:
+ test: /test/browser/system
+
+/podman-user:
+ summary: Run cockpit-podman user tests
+ discover+:
+ test: /test/browser/user
+
+/podman-misc:
+ summary: Run other cockpit-podman tests
+ discover+:
+ test: /test/browser/other
diff --git a/po/LINGUAS b/po/LINGUAS
new file mode 100644
index 0000000..be60997
--- /dev/null
+++ b/po/LINGUAS
@@ -0,0 +1,20 @@
+cs
+de
+es
+fi
+fr
+he
+it
+ja
+ka
+ko
+nb_NO
+nl
+pl
+pt_BR
+ru
+sk
+sv
+tr
+uk
+zh_CN
diff --git a/po/Makefile.am b/po/Makefile.am
new file mode 100755
index 0000000..57102f5
--- /dev/null
+++ b/po/Makefile.am
@@ -0,0 +1,58 @@
+# The concrete set of linguas we are using
+LINGUAS = $(shell cat $(srcdir)/po/LINGUAS)
+
+# The full list of various input and output file types
+PO_LINGUAS = $(addprefix po/,$(LINGUAS))
+PO_INPUTS = $(addsuffix .po,$(PO_LINGUAS))
+
+# Extract translate attribute, Glade style, angular-gettext HTML translations
+po/cockpit.html.pot: $(srcdir)/package-lock.json
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ $(srcdir)/pkg/lib/html2po.js -d $(srcdir) -o $@ \
+ $$(cd $(srcdir) && find pkg/ -name '*.html')
+
+# Extract cockpit style javascript translations
+po/cockpit.js.pot:
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ xgettext --default-domain=cockpit --output=- --language=C --keyword= \
+ --keyword=_:1,1t --keyword=_:1c,2,2t --keyword=C_:1c,2 \
+ --keyword=N_ --keyword=NC_:1c,2 \
+ --keyword=gettext:1,1t --keyword=gettext:1c,2,2t \
+ --keyword=ngettext:1,2,3t --keyword=ngettext:1c,2,3,4t \
+ --keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \
+ --from-code=UTF-8 --directory=$(srcdir) \
+ $$( cd $(srcdir) && find pkg/ ! -name 'test-*' -name '*.js' -o -name '*.jsx') | \
+ sed '/^#/ s/, c-format//' > $@
+
+po/cockpit.manifest.pot: $(srcdir)/package-lock.json
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ $(srcdir)/pkg/lib/manifest2po.js -d $(srcdir) -o $@ \
+ $$(cd $(srcdir) && find pkg/ -name 'manifest.json')
+
+po/cockpit.appstream.pot:
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ GETTEXTDATADIRS=$(srcdir)/po xgettext --output=$@ --directory=$(srcdir) \
+ $$(cd $(srcdir) && find pkg/ src/ -name '*.appdata.xml.in' -o -name '*.metainfo.xml.in')
+
+po/cockpit.polkit.pot:
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ GETTEXTDATADIRS=$(srcdir)/po xgettext --output=$@ --directory=$(srcdir) \
+ $$(cd $(srcdir) && find src/ -name '*.policy.in')
+
+# Combine the above pot files into one
+po/cockpit.pot: po/cockpit.html.pot po/cockpit.js.pot po/cockpit.manifest.pot po/cockpit.appstream.pot po/cockpit.polkit.pot
+ $(AM_V_GEN) mkdir -p $(dir $@) && \
+ msgcat --sort-output --output-file=$@ $^
+
+CLEANFILES += \
+ dist/shell/*.po \
+ src/ws/*.po \
+ po/cockpit*.pot \
+ $(NULL)
+
+EXTRA_DIST += \
+ $(PO_INPUTS) \
+ po/LINGUAS \
+ po/its/polkit.its \
+ po/its/polkit.loc \
+ $(NULL)
diff --git a/po/cs.po b/po/cs.po
new file mode 100644
index 0000000..4b26873
--- /dev/null
+++ b/po/cs.po
@@ -0,0 +1,13854 @@
+# Zdenek <chmelarz@gmail.com>, 2016. #zanata
+# Daniel Rusek <mail@asciiwolf.com>, 2017. #zanata
+# Jiri Eischmann <jiri@eischmann.cz>, 2017. #zanata
+# Vladi Nodzak <gamesvladi+fedora@gmail.com>, 2017. #zanata
+# Zdenek <chmelarz@gmail.com>, 2017. #zanata
+# Pavel Borecki <pavel.borecki@gmail.com>, 2018. #zanata, 2020.
+# Zdenek <chmelarz@gmail.com>, 2018. #zanata
+# Pavel Borecki <pavel.borecki@gmail.com>, 2019. #zanata, 2020.
+# Matej Marusak <mmarusak@redhat.com>, 2020. #zanata
+# Anonymous <noreply@weblate.org>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-12-31 14:36+0000\n"
+"Last-Translator: Weblate Translation Memory <noreply-mt-weblate-translation-"
+"memory@weblate.org>\n"
+"Language-Team: Czech <https://translate.fedoraproject.org/projects/cockpit/"
+"main/cs/>\n"
+"Language: cs\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n"
+"X-Generator: Weblate 5.3.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "počet uživatelů"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 procesor"
+msgstr[1] "$0 procesory"
+msgstr[2] "$0 procesorů"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 aktivní zóna"
+msgstr[1] "$0 aktivní zóny"
+msgstr[2] "$0 aktivních zón"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 k dispozici"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "$0 souborových systémů nelze zvětšit."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "$0 souborových systémů nelze zmenšit."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "$0 souborovým systémům zde nelze změnit velikost."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "$0 souborovým systémům zde nelze změnit velikost."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 velikost bloku dat"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 kritický zásah"
+msgstr[1] "$0 zásahy, včetně kritických"
+msgstr[2] "$0 zásahů, včetně kritických"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 data + $1 režie využito z $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 den"
+msgstr[1] "$0 dny"
+msgstr[2] "$0 dnů"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disk chybí"
+msgstr[1] "$0 disky chybí"
+msgstr[2] "$0 disků chybí"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disk chybí"
+msgstr[1] "$0 disky chybí"
+msgstr[2] "$0 disků chybí"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 disky"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "Dokumentace k $0"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 chyba"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 skončilo s kódem $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 se nezdařilo"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 nepodařený pokus o přihlášení"
+msgstr[1] "$0 nepodařené pokusy o přihlášení"
+msgstr[2] "$0 nepodařených pokusů o přihlášení"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "$0 souborový systém"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 volné"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 hodina"
+msgstr[1] "$0 hodiny"
+msgstr[2] "$0 hodin"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 důležitý zásah"
+msgstr[1] "$0 zásahy, včetně důležitých"
+msgstr[2] "$0 zásahů, včetně důležitých"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 je existující soubor"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 je používáno"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 není k dispozici z žádného z repozitářů."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 klíč změněn"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 vynuceně ukončeno signálem $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 zásah s nízkou prioritou"
+msgstr[1] "$0 zásahy s nízkou prioritou"
+msgstr[2] "$0 zásahů s nízkou prioritou"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minuta"
+msgstr[1] "$0 minuty"
+msgstr[2] "$0 minut"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 zásah střední závažnosti"
+msgstr[1] "$0 zásahy, včetně střední závažnosti"
+msgstr[2] "$0 zásahů, včetně střední závažnosti"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 měsíc"
+msgstr[1] "$0 měsíce"
+msgstr[2] "$0 měsíců"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 další…"
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 balíček"
+msgstr[1] "$0 balíčky"
+msgstr[2] "$0 balíčků"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 balíček vyžaduje restart systému"
+msgstr[1] "$0 balíčky vyžadují restart systému"
+msgstr[2] "$0 balíčků vyžaduje restart systému"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 stránka"
+msgstr[1] "$0 stránky"
+msgstr[2] "$0 stránek"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "oddíl"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "Je k dispozici $0 oprava zabezpečení"
+msgstr[1] "Jsou k dispozici $0 opravy zabezpečení"
+msgstr[2] "Je k dispozici $0 oprav zabezpečení"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 služba zhavarovala"
+msgstr[1] "$0 služby zhavarovaly"
+msgstr[2] "$0 služeb zhavarovalo"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "Je třeba zrestartovat $0 službu"
+msgstr[1] "Je třeba zrestartovat $0 služby"
+msgstr[2] "Je třeba zrestartovat $0 služeb"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 slot zbývá"
+msgstr[1] "$0 sloty zbývají"
+msgstr[2] "$0 slotů zbývá"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 špička"
+msgstr[1] "$0 špičky"
+msgstr[2] "$0 špiček"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0 synchronizováno"
+
+# auto translated by TM merge from project: retrace-server, version: master,
+# DocId: retrace-server
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 celkem"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 aktualizace k dispozici"
+msgstr[1] "$0 aktualizace k dispozici"
+msgstr[2] "$0 aktualizací k dispozici"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "použito $0 z $1 ($2 ušetřeno)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 týden"
+msgstr[1] "$0 týdny"
+msgstr[2] "$0 týdnů"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 bude nainstalováno."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 rok"
+msgstr[1] "$0 roky"
+msgstr[2] "$0 let"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "zóna $0"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: pád v $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (z $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Vytváří se oddíl $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Místní přípojný bod"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Nepřipojeno"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(doporučeno)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", včetně $1 opravy zabezpečení"
+msgstr[1] ", včetně $1 oprav zabezpečení"
+msgstr[2] ", včetně $1 oprav zabezpečení"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 den"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 hodina"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 minuta"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 minuta"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 týden"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14."
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 min"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 minut"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 minut"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4."
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 min"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minut"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5."
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 hodin"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 minut"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9."
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Na $0 není nainstalovaná kompatibilní verze Cockpit."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "Souborový systém s tímto názvem už v tomto fondu existuje."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "Skupina s takovým názvem už existuje"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Z důvodu zabezpečení, spolehlivosti a rychlosti je zapotřebí moderní "
+"prohlížeč."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Síťové spřažení spojuje vícero síťových rozhraní do jednoho logického "
+"rozhraní s vyšší propustností nebo odolností proti výpadku."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Na $0 bude vytvořen nový SSH klíč $1 na $2 a bude přidán do souboru $3 $4 na "
+"$5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] "Aby se uplatnil, aktualizovaný balíček potřebuje restart systému:"
+msgstr[1] "Aby se uplatnily, aktualizované balíčky potřebují restart systému:"
+msgstr[2] "Aby se uplatnily, aktualizované balíčky potřebují restart systému:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Fond s takovým názvem už existuje."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] "Aby se aktualizace uplatnily, je třeba restartovat službu:"
+msgstr[1] "Aby se aktualizace uplatnily, je třeba restartovat některé služby:"
+msgstr[2] "Aby se aktualizace uplatnily, je třeba restartovat některé služby:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "Skupinu svazků, které chybí fyzické svazky, není možné přejmenovat."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP monitorování"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "O webové konzoli"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Chybí"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Přijmout klíč a přihlásit se"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "Přijatelné heslo"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Skončení platnosti účtu"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Účet není k dispozici nebo ho není možné upravovat."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Účty"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Akce"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Akce"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Aktivovat"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr "Aktivujte před změnou velikosti"
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Aktivuje se $target"
+
+# auto translated by TM merge from project: ibus-libpinyin, version: master,
+# DocId: ibus-libpinyin
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Aktivní"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Aktivní záloha"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Aktivní stránky"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Aktivní od "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Aktivní stav"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Přizpůsobující se rozkládání zátěže"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Přizpůsobující se rozkládání přenosové zátěže odesílání"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Přidat"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Přidat $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "Přidat DNS server"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Přidat šifrování disku vázané na síť"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Přidat Tang server s klíči"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Přidat VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "Přidat VPN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "Přidat WireGuard VPN"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "Přidat bitovou mapu"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Přidat novou zónu"
+
+# auto translated by TM merge from project: libvirt, version: master, DocId:
+# libvirt
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "Přidat adresu"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Přidat blokové zařízení"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Přidat sloučení linek"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Přidat síťový most"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Přidat disk"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Přidat disky"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Přidat iSCSI portál"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Přidat klíč"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "Přidat server s klíči"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Přidat člena"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Přidat nového hostitele"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Přidat novou zónu"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "Přidat heslovou frázi"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "Přidat protějšek"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "Physical volume"
+msgid "Add physical volume"
+msgstr "Fyzický svazek"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Přidat porty"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Přidat porty do zóny $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Přidat veřejnou část klíče"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "Přidat trasu"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "Přidat prohledávanou doménu"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Přidat služby"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Přidat služby do zóny $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Přidat služby do zóny $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Přidat tým"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Přidat zónu"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "Přidává se „$0“ do možností šifrování"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Přidává se „$0“ do možností souborového systému"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Přidání $0 přeruší spojení se serverem a znepřístupní tak rozhraní pro jeho "
+"správu."
+
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Přidání serveru s klíči vyžaduje odemknutí fondu. Zadejte stávající heslovou "
+"frázi k fondu."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Přidání uživatelsky určených portů znovunačte firewalld. To povede ke ztrátě "
+"jakéhokoli nastavení, které bylo učiněno pouze za běhu a neuloženo!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Přidání klíče"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Přidává se fyzický svazek do $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Přidává se rd.neednet=1 do parametrů zavádění jádra"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Další DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Další DNS domény k prohledávání $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Další akce"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Další adresa $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Další balíčky:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Další porty"
+
+# auto translated by TM merge from project: firewalld, version: master, DocId:
+# po/firewalld
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Adresa"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Adresa $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Adresu je třeba vyplnit"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Do adresy nebyla vyplněná platná URL"
+
+# auto translated by TM merge from project: Spacewalk Web UI, version: master,
+# DocId: java/code/src/com/redhat/rhn/frontend/strings/java/StringResource
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Adresy"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "Adresy nemají správný formát"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Správa pomocí webové konzole Cockpit"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Přístup na úrovni správce"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr "Abyste mohli vytvářet výkazy, je třeba mít oprávnění na úrovni správy."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Je vyžadován přístup na úrovni správce"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Pokročilé TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Po"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Po opuštění domény se do tohoto stroje budou moci přihlásit jen ti "
+"uživatelé, kteří mají účet přímo na něm. Může to postihnout také ostatní "
+"služby, jako je nastavení DNS překladu a může se změnit seznam důvěryhodných "
+"cert. autorit."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Po startu systému"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Výstraha"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Výstraha a závažnější"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Alternativní název"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Vše"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr "Všech $0 vybraných fyzických svazků je třeba pro zvolené rozvržení."
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Všechny aktualizace"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Vše-v-jednom"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Umožnit spouštění (odmaskovat)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "IP adresy, které umožněny"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Adresy, které umožněny"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "Je třeba vybrat další $0"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Dokumentace k Ansible rolím"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"V hlášeních v záznamu událostí (log) je možné filtrovat podle libovolného "
+"textového řetězce. Řetězec také může mít podobu regulárního výrazu. Také je "
+"podporováno filtrování podle kolonek zprávy záznamu událostí. Zde se jedná o "
+"mezerami oddělované hodnoty v podobě KOLONKA=HODNOTA, kde hodnota může být "
+"čárkou oddělovaný seznam možných hodnot."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Vzhled"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "Informace o aplikaci chybí"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Aplikace"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Seznam aplikací"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Použít a restartovat"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Aplikovat záplaty pro jádro systému za chodu"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Použít toto řešení"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Uplatňuje se nová zásada… Toto může zabrat několik minut."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Aplikace řešení…"
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Aplikují se aktualizace"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Aplikace aktualizací se nezdařila"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "Příhodné pro kriticky důležité přípojné body, jako např. /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Aplikace"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Posouzení"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Inventární štítek"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "Při startu systému"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Je vyžadován alespoň $0 disk."
+msgstr[1] "Jsou vyžadovány alespoň $0 disky."
+msgstr[2] "Je vyžadováno alespoň $0 disků."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "Je vyžadováno alespoň jedno blokové zařízení."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Je vyžadován alespoň jeden disk."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "V minutě"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "V sekundě"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "V uvedený čas"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Atributy"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Záznam auditu"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Ověřit se"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Ověřování se"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Ověření se"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Ověření se nezdařilo"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Ověření se nezdařilo: Server přerušil spojení"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Pro provádění privilegovaných úloh pomocí webové konzole Cockpit je třeba "
+"ověřit se"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Nutné ověření se"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Pověřit SSH klíč"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Pověřené veřejné SSH klíče"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automaticky"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automaticky (pouze DHCP)"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Automatické přihlášení"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Automatické aktualizace"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Spouští se automaticky"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Automatické použití NTP"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Automatické použití dalších NTP serverů"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Automatické použití uvedených NTP serverů"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Automatizační skript"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Cíle dostupné na $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Dostupné aktualizace"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "Grow partition"
+msgid "BIOS boot partition"
+msgstr "Zvětšit oddíl"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "Datum vydání BIOS"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "Verze BIOS"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Zpět k účtům"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Chybné"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Chybné nastavení"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Rozkládání zátěže"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Před"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Navazuje se na"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Černá"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blade server"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Skříň se šachtami pro blade servery"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "Blokové zařízení"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Blokové zařízení pro souborové systémy"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Bloková zařízení"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Blokované"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Vazba"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Zavedení systému"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"Pokud souborový systém nebude připojen, start operačního systému se nezdaří "
+"a nebude tak možný vzdálený přístup"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr ""
+"Start operačního systému se zdaří i když souborový systém nebude připojen"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Spojeno"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Most"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Port mostu"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Nastavení portu mostu"
+
+# auto translated by TM merge from project: tvtime, version: 1.0.7, DocId:
+# tvtime
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Vysílání"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Chybné nastavení"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Jsou k dispozici aktualizace opravující zabezpečení"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Chyby"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Skříň rozšíření sběrnice"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Změnou hesla k SSH klíči $0 na přihlašovací heslo uživatele $1 na $2, bude "
+"klíč automaticky zpřístupněn a na $3 se příště přihlásíte už bez hesla."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " Obejít kontrolu prohlížeče "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "Procesor"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Zabezpečení procesoru"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "Přepínače zabezpečení procesoru"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Využití procesoru"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "Využití/vytížení procesoru"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Mezipaměť"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr ""
+"Může být název stroje, IP adresa, alternativní název, nebo ssh:// URI adresa"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr ""
+"Při použití stávající kombinace filtrů se nedaří nalézt žádné záznamy "
+"událostí."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Storno"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Zrušit vypínání"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Zrušit restartování"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Není možné zapnout"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Nelze se připojit k neznámému hostiteli"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Nedaří přeposlat přístupové údaje"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr "Nelze přidat do domény, protože na tomto systému chybí nástroj realmd"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Nelze naplánovat událost v minulosti"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Kapacita"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Nosný signál"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Změnit"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "Změnit zásadu pro šifrování"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Změnit název stroje"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Změnit název iSCSI iniciátoru"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Změnit název iSCSI iniciátoru"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change shell"
+msgid "Change label"
+msgstr "Změnit shell"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Změnit heslovou frázi"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Změnit heslo"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Změnit profil výkonu"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Změnit profil"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "Změnit shell"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Změnit systémový čas"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Změnit heslo $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Změnit nastavení"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Změněné klíče jsou často výsledkem přeinstalace operačního systému. Nicméně, "
+"neočekávaná změna může značit pokus třetí strany o vložení se do vaší "
+"komunikace."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Změna nastavení přeruší spojení se serverem a znepřístupní tak uživatelské "
+"rozhraní pro jeho správu."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Zkontrolovat aktualizace"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"Zkontrolujte, že se SHA-256 nebo SHA-1 otisk z příkazu shoduje s tímto "
+"dialogem."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "Zkontrolovat otisk klíče vůči Tang serveru."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Kontroluje se $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Kontroluje se IP adresa"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "Kontroluje se RAID zařízení $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Kontroluje a opravuje se RAID zařízení $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Zjišťování pro $0 balíček"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Zjišťování přítomnosti podpory NBDE v initrd"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Hledají se nové aplikace"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Zjišťování aktualizací balíčků…"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Zjišťuje se nainstalovaný sofware"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Zjišťování souvisejících procesů"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Kontroluje se stav software"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Vyberte jazyk aplikace"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Velikost bloku"
+
+# auto translated by TM merge from project: libosinfo, version: master, DocId:
+# libosinfo
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Třída"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Čištění pro $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Vyčistit „Nepodařilo se spustit“"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Vyčistit všechny filtry"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Vyčistit vyhledávání"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Znakové zařízení"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Klientský software"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Zavřít"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Zavřít vybrané stránky"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Cockpit ověřování není nastaveno správně."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "Nastavování NetworkManager a Firewalld v pomocí Cockpit"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit se nepodařilo daný stroj kontaktovat."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "V Cockpit došlo k neočekávané vnitřní chybě."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit je správce serveru, který usnadňuje správu Linuxových serverů "
+"prostřednictvím webového prohlížeče. Není žádným problémem přecházet mezi "
+"terminálem a webovým nástrojem. Služba spuštěná přes Cockpit může být "
+"zastavena v terminálu. Podobně, pokud dojde k chybě v terminálu, je toto "
+"vidět v rozhraní žurnálu v Cockpit."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit je interaktivní rozhraní pro správu linuxového serveru."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit není kompatibilní se softwarem v systému."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit není nainstalován"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit není v systému nainstalován."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit je ideální nástroj pro nové správce serverů, neboť jim umožňuje "
+"snadno provádět jednoduché úkoly, jako je správa úložišť, kontrola žurnálu "
+"či spouštění a zastavování služeb. Můžete současně sledovat a spravovat "
+"několik serverů najednou. Stačí je přidat jedním kliknutím a vaše stroje se "
+"budou starat o své kamarády."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr ""
+"Může se stávat, že ve vámi používaném webovém prohlížeči nebude Cockpit "
+"správně zobrazován"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Shromáždit a zabalit data pro diagnostiku a podporu"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Shromáždit výpisy pádů jádra systému"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Shromažďovat metriky"
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Barva"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "Jsou přijímány čárkou oddělované porty, rozsahy a služby"
+
+# auto translated by TM merge from project: SETroubleShoot, version: master,
+# DocId: framework/po/setroubleshoot
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Příkaz"
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Příkaz nenalezen"
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Komentář"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Komunikace s procesem služby tuned se nezdařila"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Compact PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Kompatibilní se všemi systémy a zařízeními (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Kompatibilní s moderním systémem a pevnými disky > 2TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Komprimovat výpisy paměti z havárií pro úsporu místa"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Komprese"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Podmínka $0=$1 nebyla splněna"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Podmínka nebyla úspěšná"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Nastavení"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Nastavuje se"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Nastavuje se IP adresa"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Nastavuje se kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Probíhá nastavení systému"
+
+# auto translated by TM merge from project: ibus-input-pad, version: head,
+# DocId: ibus-input-pad
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Potvrdit"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Potvrďte smazání $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Potvrdit heslo ke klíči"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Potvrdit nové heslo ke klíči"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Potvrdit nové heslo"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Potvrzení hesla"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Potvrdit odebrání $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Potvrdit odebrání pomocí alternativní heslové fráze"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Potvrďte zastavení $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "V konfliktu s"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Konflikty"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Připojit se automaticky"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Připojit se k"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Připojit se k:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Připojování k procesu služby SETroubleshoot…"
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Připojení k dbus se nezdařilo: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Připojování ke stroji"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Chyba připojení"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Připojení se nezdařilo"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Překročen časový limit připojení."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "Spojení bude ztraceno"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Sestává se z"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Připojená doména"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Obsahuje:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Pokračovat"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Pokračovat v relaci"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Ovládání"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Ovládání"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Řadič"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Počítač 2v1"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Zkopírováno"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Zkopírovat"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Zkopírovat do schránky"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Jádro $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "Nedaří se kontaktovat $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Umístění výpisu paměti pádu"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Nahlašování pádů"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Zhavarovat systém"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Vytvořit"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "Vytvořit LVM2 skupinu svazků"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "Vytvořit RAID zařízení"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Vytvořit RAID zařízení"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Vytvořit Stratis fond úložiště"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Vytvořit nový SSH klíč a pověřit ho"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Pořídit zachycený stav souborového systému $0"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Vytvořit účet s identifikátorem, který už je použit"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Vytvořit účet se snadno prolomitelným heslem"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Vytvořit a změnit vlastnictví domovské složky"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Vytvořit a připojit"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create and mount"
+msgid "Create and start"
+msgstr "Vytvořit a připojit"
+
+# auto translated by TM merge from project: usermode, version: default, DocId:
+# usermode
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Vytvořit souborový systém"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Vytvořit to"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Vytvořit logický svazek"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Vytvořit nový účet"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Vytvořit nový souborový systém"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Vytvořit novou skupinu"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Vytvořit nový logický svazek"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Vytvořit nový soubor s úlohou s tímto obsahem."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "Vytvořit nový logický svazek"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "Pouze vytvořit"
+
+# auto translated by TM merge from project: anaconda, version: rhel6-branch,
+# DocId: anaconda
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Vytvořit oddíl"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Vytvořit oddíl na $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Vytvořit tabulku rozdělení na oddíly"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Pořídit zachycený stav"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Pořídit zachycený stav a připojit ho"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Pouze pořídit zachycený stav"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create storage volume"
+msgid "Create storage device"
+msgstr "Vytvořit svazek úložiště"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Vytvořit svazek"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Vytvořit tence poskytovaný (thin provided) svazek"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Vytvořit časovač"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Vytvořit skupinu svazků"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Vytvořeno"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Vytváří se LVM2 skupina svazků $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "Vytváří se RAID zařízení $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "Vytváří se VDO zařízení"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Vytváří se souborový systém na $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Vytváří se logický svazek $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Vytváří se oddíl $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Pořizování zachyceného stavu $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Vytvoření tohoto $0 přeruší spojení se serverem a znepřístupní tak rozhraní "
+"pro jeho správu."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Kritické a závažnější"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Kryptografické zásady je systémová součást, která nastavuje hlavní "
+"kryptografické podsystémy – pokrývá protokoly TLS, IPSec, SSH, DNSSec a "
+"Kerberos."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "Zásada šifrování"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "Zásada šifrování není konzistentní"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Od tohoto spuštění systému"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Stávající vytížení procesoru"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Nyní využito"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Currently in use"
+msgid "Currently not supported"
+msgstr "Nyní využito"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "uživatelsky určené"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "Uživatelsky určená zásada šifrování"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Uživatelsky určené předvolby připojení"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Uživatelsky určené porty"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom ports"
+msgid "Custom type"
+msgstr "Uživatelsky určené porty"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Uživatelsky určené zóny"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "VÝCHOZÍ s umožněným ověřením podpisu SHA-1."
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS prohledávané domény"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "Prohledávat DNS domény $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Denně"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Výstraha na nebezpečí:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Tmavý"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Data"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Využito dat"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"Data budou uložena jako dvě kopie a také ve střídajícím se způsobu na "
+"označených fyzických svazcích, což zlepší jak spolehlivost, tak výkon. Je "
+"třeba vybrat alespoň 4 svazky."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"Pro posílení spolehlivosti, data budou uložena jako dvě či více kopií na "
+"vybraných fyzických svazcích. Je třeba vybrat alespoň dva svazky."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+"Pro zlepšení výkonu budou data na vybraných fyzických svazcích uložena "
+"střídavě. Je třeba vybrat alespoň dva svazky."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"Data budou na vybraných fyzických svazcích uložena tak, že jeden z nich bude "
+"možné ztratit, aniž by to postihlo data. Je třeba vybrat alespoň tři svazky."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+"Data budou uložena na vybraných fyzických svazcích tak, že bude možné jeden "
+"z nich ztratit, aniž by to postihlo data. Bude také zlepšen výkon střídavým "
+"ukládáním dat. Je třeba vybrat alespoň tři svazky."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+"Data budou na vybraných fyzických svazcích uložena tak, že bude možné "
+"ztratit až dva naráz, aniž by to postihlo data. Pro zlepšení výkonu budou "
+"data také ukládána střídavě. Je třeba vybrat alespoň pět svazků."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+"Data budou na vybraných fyzických svazcích uložena bez jakékoli dodatečné "
+"odolnosti či zlepšení výkonu."
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Zadání datumů by mělo být ve formátu RRRR-MM-DD hh:mm:ss. Alternativně je "
+"možné použít ještě řetězce „yesterday“ (včera), „today“ (dnes), "
+"„tomorrow“ (zítra). „now“ (nyní) odkazuje na aktuální dobu. Dále je možné "
+"zadávat vztažené (relativní) časy, předeslané předponou „-“ nebo „+“"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Deaktivovat"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Repair logical volume $0"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Opravit logický svazek $0"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Deaktivuje se"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Deaktivuje se $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Ladící a závažnější"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Snížit o jedno"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "Vyhrazená parita (RAID 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Deduplikace"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Výchozí"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Prodleva"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Je třeba, aby prodleva byla číslo"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Smazat"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Smazat $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Smazat účet"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Smazat soubory"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Smazat skupinu"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Smazat"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Smazat výkaz bez možnosti obnovení?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Smazání $0 přeruší spojení se serverem a znepřístupní tak rozhraní pro jeho "
+"správu."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Maže se $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Maže se LVM2 skupina svazků $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "Smazání Stratis fondu vymaže veškerá data v něm."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "Smazání souborového systému vymaže veškerá data v něm."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "Smazání logického svazku vymaže veškerá data na něm."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "Smazání oddílu vymaže veškerá data v něm."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting erases all data on a RAID device."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Smazání vymaže veškerá data na RAID zařízení."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "Smazání vymaže veškerá data na VDO zařízení."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "Smazání vymaže veškerá data ve skupině svazků."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting erases all data on a volume group."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "Smazání vymaže veškerá data ve skupině svazků."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "Smazání odebere následující soubory:"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Popis"
+
+# auto translated by TM merge from project: Fedora Websites, version:
+# arm.fedoraproject.org, DocId: po/arm.fedoraproject.org
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Desktop"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Odpojitelné"
+
+# auto translated by TM merge from project: blivet-gui, version: master,
+# DocId: po/blivet-gui
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Podrobnosti"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Vývoj"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Zařízení"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Soubor představující zařízení"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Zařízení je pouze pro čtení"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Název služby"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Diagnostická hlášení"
+
+# auto translated by TM merge from project: usermode, version: default, DocId:
+# usermode
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Složka"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Složka $0 není zapisovatelná nebo neexistuje."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Vypnout souběžné vícevláknové zpracovávání (SMT)"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Vypnout bránu firewall"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Vypnout proces služby tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Vypnuto"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Neumožnit přihlašování zadáním hesla"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Neumožnit přihlašování heslem"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Nepovolit spuštění (maskovat)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Odpojit"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Odpojeno"
+
+# auto translated by TM merge from project: virt-manager, version: master,
+# DocId: virt-manager
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Diskový vstup/výst."
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Disk je OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Disk selhává"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Heslová fráze k disku"
+
+# auto translated by TM merge from project: blivet-gui, version: master,
+# DocId: po/blivet-gui
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Disky"
+
+# auto translated by TM merge from project: blivet-gui, version: master,
+# DocId: po/blivet-gui
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Využití disku"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "Zahodit"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Zahodit $0 výstrahu"
+msgstr[1] "Zahodit $0 výstrahy"
+msgstr[2] "Zahodit $0 výstrah"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Zahodit označené výstrahy"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Jazyk zobrazení"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "Distribuovaná parita (RAID 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "Nepřipojovat"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Nepřipojovat automaticky při startu systému"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Dokovací stanice"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Nespouštět automaticky"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "Nepřipojuje se při startu systému"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Doména"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Doména"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Adresa domény"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Uživatelské jméno správce domény"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Heslo správce domény"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Doménu se nedaří kontaktovat"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Doména není podporována"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Neopakovat"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "Dvojitá distribuovaná parita (RAID 6)"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Stáhnout"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Zdarma si stáhnout nový prohlížeč"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Staženo"
+
+# auto translated by TM merge from project: Fedora Websites, version:
+# spins.fedoraproject.org, DocId: po/spins.fedoraproject.org
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Stahuje se"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Stahuje se $0"
+
+# auto translated by TM merge from project: udisks, version: udisks, DocId:
+# udisks2
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Jednotka"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Dual rank"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Rozšířený oddíl"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Upravit"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Upravit soubor /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Upravit Tang server s klíči"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "Upravit nastavení VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "Upravit WireGuard VPN"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "Upravit nastavení spřažení"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "Upravit nastavení mostu"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Upravit uživatelsky určenou službu v zóně $0"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Upravit hostitele"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Upravit hostitele"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Upravit motd"
+
+# auto translated by TM merge from project: anaconda, version: rhel6-branch,
+# DocId: anaconda
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Přípojný bod"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Upravit pravidla a zóny"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Upravit službu"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Upravit službu $0"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "Upravit nastavení týmu"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Upravit uživatele"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "K upravení klíče je zapotřebí volného slotu"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Vysouvání $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "Jednodeskový počítač"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Prázdný"
+
+# auto translated by TM merge from project: udisks, version: udisks, DocId:
+# udisks2
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Prázdný"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Prázdné heslo"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Vyprazdňuje se $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Povolit"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Povolit nebo zakázat zařízení"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Zapnout službu"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Zapnout bránu firewall"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Povoleno"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "Povoluje se $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Šifrovat data"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Šifrovat data pomocí Tang serveru s klíči"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "Šifrovat data pomocí heslové fráze"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Šifrováno"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Šifrované $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted Stratis pool $0"
+msgid "Encrypted Stratis pool"
+msgstr "Šifrovaný Stratis fond na $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Šifrovaný logický svazek na $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Šifrovaný oddíl na $0"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Šifrování"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Volby šifrování"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Šifrovací heslová fráze"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Typ šifrování"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Ukončeno"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "Koncový bod"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"Je třeba zadat koncový bod sloužící jako „server“ v podobě stroj:port, jinak "
+"je možné ponechat nevyplněné."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Vynucující"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Jsou k dispozici vylepšující aktualizace"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Zadejte platnou MAC adresu"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Celá podsíť"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Položka na $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Vymazává se $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Errata"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Chyba"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Chyba a závažnější"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Došlo k chybě"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "Chyba při instalování $0: PackageKit není nainstalovaný"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Chybové hlášení"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "Chyba při spouštění semanage pro objevení změn v systému"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Chyba při ukládání pověřených klíčů: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Chyba při nastavování SELinux režimu: „$0“"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC adresa"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "MTU ethernetu"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "Je třeba vybrat přesně $0 fyzických svazků"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+"Je třeba vybrat přesně $0 fyzických svazků – jeden pro každý z proužků "
+"logického svazku."
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Příklad: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Příklad: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Skvělé heslo"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Rozšiřující šasi"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Ukončit platnost účtu v"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Ukončit platnost účtu v $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Exportovat"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Exportovat do sítě"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Rozšířené informace"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Rozšířený oddíl"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS není řádně zapnuté"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS s dalšími omezeními běžnými kritérii."
+
+# auto translated by TM merge from project: retrace-server, version: master,
+# DocId: retrace-server
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Neúspěšné"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Nepodařilo se přidat stroj: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Přidání portu se nezdařilo"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Službu se nepodařilo přidat"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Zónu se nepodařilo přidat"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Nepodařilo se změnit heslo"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "Nepodařilo se nastavit PCP"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Nepodařilo se smazat výstrahu: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Nepodařilo se zakázat proces služby tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Nepodařilo se vypnout profil tuned"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Nepodařilo se upravit stroj: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Službu se nepodařilo upravit"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "Nepodařilo se povolit $0 ve firewalld"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Nepodařilo se zapnout tuned"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Nepodařilo se získat záznamy událostí"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Nepodařilo se načíst pověřené klíče."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Jednotku (unit) se nepodařilo načíst"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"Nepodařilo se zpracovat jednotkové soubory pro dnf-automatic.timer nebo dnf-"
+"automatic-install.timer. Pokud chcete nastavit automatické aktualizace, "
+"odeberte uživatelsky určená přepsání."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Službu se nepodařilo restartovat"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Nepodařilo se uložit změny v /etc/motd"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Nastavení se nepodařilo uložit"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Nepodařilo se spustit"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Nepodařilo se přepnout profil"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Stav souboru"
+
+# auto translated by TM merge from project: usermode, version: default, DocId:
+# usermode
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Souborový systém"
+
+# auto translated by TM merge from project: comps, version: master, DocId:
+# comps
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "Souborový systém je uzamčen"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Název souborového systému"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Vytváří se souborový systém na $target"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "Do tohoto přípojného bodu už jsou připojené souborové systémy."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Filtrovat podle názvu nebo popisu"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Filtrovat položky nabídky"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Filtrovat služby"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Filtry"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Otisk"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Brána firewall"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Brána firewall není k dispozici"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Verze firmware"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "Opravit podporu pro NBDE"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Velikost písmen"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Spuštění zakázáno"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Vynutit změnu"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Vynutit smazání"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Vynutit změnu hesla"
+
+# auto translated by TM merge from project: blivet-gui, version: f23-branch,
+# DocId: blivet-gui
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Formát"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Naformátovat $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Naformátovat a připojit"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format and mount"
+msgid "Format and start"
+msgstr "Naformátovat a připojit"
+
+# auto translated by TM merge from project: blivet-gui, version: f23-branch,
+# DocId: blivet-gui
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Pouze naformátovat"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "Formátování vymaže všechna data na úložném zařízení."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Prodleva přeposlání $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Číslo snímku"
+
+# auto translated by TM merge from project: anaconda, version: rhel6-branch,
+# DocId: anaconda
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Volné místo"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Vyhledávání volnou formou"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Pátky"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "Z"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Celé jméno"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Brána"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Obecné"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Vytvořeno"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Jít na $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Přejít na aplikaci"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Přejít na nyní"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Viditelnost grafu"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Nabídka možností viditelnosti grafu"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Skupina"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Název skupiny"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Skupiny"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Zvětšit"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Zvětšit obsah"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Zvětšit logickou velikost $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Zvětšit logický svazek"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "Zvětšit oddíl"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "Zvětšit fond tak, aby využil veškerý dostupný prostor"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Zvětšit a využít veškerý dostupný prostor"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Hair pin režim"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Hair pin režim"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Pro držení v rukou"
+
+# auto translated by TM merge from project: udisks, version: udisks, DocId:
+# udisks2
+#: pkg/storaged/drive/drive.jsx:64
+#, fuzzy
+#| msgid "Hard Disk"
+msgid "Hard Disk Drive"
+msgstr "Pevný disk"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Informace o hardware"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Celkový stav"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Oznamovací čas $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Nápověda"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "Skrýt potvrzení hesla"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "Skrýt heslo"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "Identifikátor hierarchie"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "Výborná interoperabilita na úkor zvýšenému vystavení se útokům."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Počet historie balíčku"
+
+# auto translated by TM merge from project: usermode, version: default, DocId:
+# usermode
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Domovská složka"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Počítač"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Klíč stroje není správný"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Název stroje"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Hostitelé"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Každou hodinu"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Hodin"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "Jak zkontrolovat"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "Identif."
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "Identif. $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+"INTERNAL ERROR - Tento logický svazek je označen jako aktivní a měl by mít "
+"přiřazené blokové zařízení. Žádné takové blokové zařízení však nebylo "
+"nalezeno."
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP adresa"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"IP adresa se směrovací předponou (prefix). Vícero hodnot oddělujte čárkou. "
+"Příklad: 192.0.2.0/24, 2001:db8::/32"
+
+# auto translated by TM merge from project: firewalld, version: master, DocId:
+# po/firewalld
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "IPv4 adresy"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "Nastavení IPv4"
+
+# auto translated by TM merge from project: firewalld, version: master, DocId:
+# po/firewalld
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "Nastavení IPv6"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Identifikátor"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Pokud nevyplněno, identifikátor bude vytvořen na základě souvisejících "
+"služeb na portu a čísel portů"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Pokud se otisk shoduje, klikněte na „Přijmout klíč a přihlásit se“. V "
+"opačném případě se nepřipojujte a obraťte se na správce."
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Pokud se otisk shoduje, klikněte na „Důvěřovat a přidat stroj“. V opačném "
+"případě se nepřipojujte a obraťte se na správce."
+
+# auto translated by TM merge from project: authconfig, version: master,
+# DocId: po/authconfig
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ignorovat"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "V"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "V terminálu spusťte: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Aby se napříště bylo možné přihlašovat k ${rhost} jako ${ruser} bez hesla, "
+"použijte přihlašovací heslo uživatele ${luser} na ${lhost} jako heslo ke "
+"klíči, nebo heslo ke klíči nevyplňujte."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "Synchronní"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Neaktivní"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "Vytvořit logický svazek"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Obsažené služby"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"Ve výchozím stavu jsou příchozí požadavky blokovány. Ty odchozí nejsou."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Nekonzistentní připojení souborového systému"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Navýšit o jednu"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Paměť indexu"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Nepřímé"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Informace"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Informace a závažnější"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "inicializovat"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Inicializovat disk $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "Inicializace vymaže veškerá data na disku."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Inicializace…"
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Vhledy: "
+
+# auto translated by TM merge from project: dnf, version: master, DocId: dnf
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Nainstalovat"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Nainstalovat podporu pro NFS"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Nainstalovat podporu pro Stratis"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Nainstalovat všechny aktualizace"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "Nainstalovat informaci o aplikaci"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Nainstalovat cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Nainstalovat kpatch aktualizace"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "Nainstalovat podporu pro realmd"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Nainstalovat aktualizace zabezpečení"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr "Nainstalovat setroubleshoot-server pro řešení potíží s SELinux."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Nainstalovat software"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Nainstalujte balíček $0"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr "Instalace není podporována bez nainstalovaného balíčku cockpit"
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Nainstalováno"
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Instaluje se"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Instaluje se $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "Instalace $0 by odebrala $1."
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Instalují se balíčky"
+
+# auto translated by TM merge from project: firewalld, version: master, DocId:
+# po/firewalld
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Rozhraní"
+msgstr[1] "Rozhraní"
+msgstr[2] "Rozhraní"
+
+# auto translated by TM merge from project: firewalld, version: master, DocId:
+# po/firewalld
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Čísla rozhraní"
+
+# auto translated by TM merge from project: firewalld, version: master, DocId:
+# po/firewalld
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Rozhraní"
+
+# auto translated by TM merge from project: Pulseaudio, version: 6.0, DocId:
+# pulseaudio.pot
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Vnitřní chyba"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Vnitřní chyba: neplatná hlavička výzvy"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Neplatné"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Neplatná adresa $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Neplatný formát data"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Neplatný formát data a času"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Neplatné datum skončení platnosti"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Neplatná souborová práva"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Neplatný klíč"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Neplatná metrika $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Neplatný počet dnů"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Neplatné číslo portu"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Neplatná předpona $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Neplatná předpona nebo maska sítě $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Neplatný rozsah"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Neplatný formát času"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Neplatné časové pásmo"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Neplatné uživatelské jméno a heslo"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "Brána Internetu věcí (IoT)"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "Není sshd spuštěný na jiném portu?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Úlohy"
+
+# auto translated by TM merge from project: Fedora Websites, version:
+# alt.fedoraproject.org, DocId: po/alt.fedoraproject.org
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Spojit"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Přidat do domény"
+
+# auto translated by TM merge from project: authconfig, version: master,
+# DocId: po/authconfig
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Přidat do domény"
+
+# auto translated by TM merge from project: Fedora Websites, version:
+# alt.fedoraproject.org, DocId: po/alt.fedoraproject.org
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Přidává se"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Přidání do domény vyžaduje instalaci nástroje realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Přidání do této domény není podporováno"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Připojuje jmenný prostor od"
+
+# auto translated by TM merge from project: Fedora Websites, version:
+# spins.fedoraproject.org, DocId: po/spins.fedoraproject.org
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Žurnál"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Položka žurnálu"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Položka žurnálu nenalezena"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Přeskočit na"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit není nainstalován"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Vyzkoušet nastavení kdump"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Zachovat spojení"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Výpis pádu jádra systému"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Výpis paměti jádra"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "Záplata za chodu pro jádro systému $0 je aktivní"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Záplata za chodu pro jádro systému $0 je nainstalovaná"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Nastavení záplatování jádra systému za chodu"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Záplatování jádra systému za chodu"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Heslo ke klíči"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Sloty klíčů s neznámým typem zde není možné upravovat"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Zdroj klíče"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Klíče"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Server s klíči"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Adresa serveru s klíči"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "Odebrání serveru s klíči může zabránit odemčení $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP klíč"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "PŮVODNÍ s interoperabilitou s Active Directory."
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "VDO pool"
+msgid "LVM2 VDO pool"
+msgstr "VDO fond"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Logický svazek"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Logické svazky"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volume"
+msgstr "Fyzický svazek"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volumes"
+msgstr "Fyzický svazek"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM2 skupina svazků"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "LVM2 skupina svazků $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Notebook"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Uplynulých 24 hodin"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Uplynulých 7 dnů"
+
+# auto translated by TM merge from project: libvirt, version: master, DocId:
+# libvirt
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Naposledy aktivní"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "Poslední zbývající slot klíče není možné odebrat"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Naposledy zkontrolováno: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "Poslední zbývající slot klíče není možné odebrat"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Poslední přihlášení"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Naposledy zkontrolováno: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Poslední úspěšné přihlášení:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "Rozvržení"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Další informace naleznete"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "Opustit $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Opustit doménu"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Pokud nechcete šifrovat, nevyplňujte"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Licencováno pod GNU LGPL verze 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Světlý"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Omezit přístup"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Omezený přístup"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"Režim omezeného přístupu omezuje oprávnění pro správu. Některé části webové "
+"konzole budou poskytovat méně funkcí."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Limity"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "Lineární"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Prodleva neaktivní linky"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Lokální propoj"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Monitorování linky"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Prodleva aktivní linky"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Hlídání linky"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Odkazováno"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem $0"
+msgid "Linux filesystem data"
+msgstr "Odpojit souborový systém $0"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Odkládací prostor"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Očekávat spojení"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "Port na kterém očekávat spojení"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "Je třeba, aby port, na kterém očekávat spojení, bylo číslo"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Vypisují se jednotky (unit)"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Vypsání jednotek se nezdařilo: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Načíst"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Rozkládání zátěže"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Načíst dřívější data"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Načíst dřívější položky"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Načtení dostupných aktualizací se nezdařilo"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Načítání dostupných aktualizací, čekejte…"
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Načítají se dřívější položky"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Načítání klíčů…"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Načítání SSH klíčů se nezdařilo"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Načítání jednotek se nezdařilo"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Načítání balíčků…"
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Načítání modifikací systému…"
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Načítání jednotek se nezdařilo"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Načítání…"
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Místní účty"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Místní souborový systém"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Místní přípojný bod"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "No storage"
+msgid "Local storage"
+msgstr "Žádné úložiště"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "místně v $0"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Umístění"
+
+# auto translated by TM merge from project: authconfig, version: master,
+# DocId: po/authconfig
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Uzamknout"
+
+# auto translated by TM merge from project: authconfig, version: master,
+# DocId: po/authconfig
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "Uzamknout $0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Uzamknout účet"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgid "Locked"
+msgid "Locked data"
+msgstr "Uzamčeno"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Uzamyká se $target"
+
+# auto translated by TM merge from project: python-fedora, version: 0.4.0,
+# DocId: python-fedora
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Přihlásit se"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Přihlásit se k $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Přihlášení pomocí uživatelského účtu na tomto serveru."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Zprávy záznamu událostí"
+
+# auto translated by TM merge from project: im-chooser , version: master,
+# DocId: im-chooser
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Odhlásit"
+
+# auto translated by TM merge from project: im-chooser , version: master,
+# DocId: im-chooser
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Odhlásit uživatele"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Přihlášeni"
+
+# auto translated by TM merge from project: blivet-gui, version: f23-branch,
+# DocId: blivet-gui
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Logický"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Logický svazek (zachycený stav)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Logická velikost"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Logický svazek"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Logický svazek (zachycený stav)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Logický svazek na $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Přihlásit"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Znovu přihlásit"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Přihlášení se nezdařilo"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Formát přihlašování"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Historie přihlášení"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Seznam historie přihlašování"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "Odhlásit $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Odhlášení úspěšné"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Záznamy událostí"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Nízký desktop"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Kufříkový počítač"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+# auto translated by TM merge from project: anaconda, version: master, DocId:
+# main
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "RAID zařízení"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "RAID zařízení $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Zařízení je pouze pro čtení"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "Služba je spuštěná"
+
+# auto translated by TM merge from project: anaconda, version: master, DocId:
+# main
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "RAID zařízení"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Přidat disky"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (doporučeno)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "Je třeba, aby MTU bylo kladné číslo"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Identif. stroje"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Otisky SSH klíče stroje"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Hlavní skříň serveru"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Údržba"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+"Škodlivé stránky na vzdáleném stroji mohou postihnout ostatní připojené "
+"stroje"
+
+# auto translated by TM merge from project: usermode, version: default, DocId:
+# usermode
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "Spravovat velikosti souborových systémů"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Spravovat úložiště"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Spravovaná rozhraní"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "Správa LVM svazků"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "Správa NFS připojení"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "Správa RAID polí"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "Správa VDO optimalizátorů"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "Správa VLAN sítí"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Správa brány firewall"
+
+# auto translated by TM merge from project: Fedora Release Notes, version:
+# f24, DocId: pot/Networking
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Správa síťových spřažení"
+
+# auto translated by TM merge from project: Fedora Release Notes, version:
+# f24, DocId: pot/Networking
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Správa síťových mostů"
+
+# auto translated by TM merge from project: Fedora Release Notes, version:
+# f24, DocId: pot/Networking
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Správa sdružených síťových připojení"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Správa oddílů"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Správa fyzických disků"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Správa služeb"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Správa aktualizací software"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Správa uživatelských účtů"
+
+# auto translated by TM merge from project: Entangle, version: master, DocId:
+# entangle
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Ruční"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Ručně"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Označování $target jako vadný"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Maskovat službu"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Maskováno"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Maskování služby zabrání všem závislým jednotkám ve spouštění. To může mít "
+"větší dopad, než zamýšleno. Potvrďte, že chcete tuto jednotku maskovat."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Maximální stáří zpráv $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Mechanika optických disků"
+
+# auto translated by TM merge from project: virt-manager, version: master,
+# DocId: virt-manager
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Paměť"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Technologie paměti"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Využití paměti"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Využití paměti / odkládacího prostoru (swap)"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Sloučeno"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Zpráva přihlášeným uživatelům"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "Zprávy týkající se nezdaru se mohou nacházet v žurnálu:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Využito metadat"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Metoda"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Metrika"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Metriky a historie"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Nepodařilo se načíst historické metriky"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Nastavení metrik"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Mini věž"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Je třeba, aby minuta bylo číslo z rozmezí 0-59"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "Po minutách"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minut"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr "Zrcadlené (RAID 1)"
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Zmírnění dopadu"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Režim"
+
+# auto translated by TM merge from project: Cockpit, version: master, DocId:
+# cockpit
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Model"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Upravuje se $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Pondělky"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Interval monitorování"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Cíle monitorování"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Každý měsíc"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Více informací…"
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Připojit (mount)"
+
+# auto translated by TM merge from project: anaconda, version: rhel6-branch,
+# DocId: anaconda
+#: pkg/storaged/btrfs/subvolume.jsx:149
+#, fuzzy
+#| msgid "Mount point"
+msgid "Mount Point"
+msgstr "Přípojný bod"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "Připojit až bude síť k dispozici, ignorovat nezdar"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Připojovat také automaticky při startu systému"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Připojit při startu"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Při startu systému automaticky připojit do $0"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Připojit před spuštěním služeb"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Nastavení připojení (mount)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Připojit souborový systém"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Připojit nyní"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Připojit na $0 nyní"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Předvolby připojení"
+
+# auto translated by TM merge from project: anaconda, version: rhel6-branch,
+# DocId: anaconda
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Přípojný bod"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Je třeba vyplnit přípojný bod"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Přípojný bod je třeba vyplnit."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Přípojný bod už je používán pro $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Je třeba, aby popis přípojného bodu začínal na „/“."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Připojit pouze pro čtení"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Připojit bez čekání, ignorovat nezdar"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Připojování $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Připojit před spuštěním služeb"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Bude připojeno souběžně se službami"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr "Připojuje souběžně se službami, ale až poté, co je k dispozici síť"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Skříň pro více systémů"
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Multipathed devices"
+msgid "Multipathed devices"
+msgstr "Zařízení s vícero cestami (multipath)"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr "Je možné zadat vícero adres (oddělovaných čárkami nebo mezerami)."
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS připojení"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP server"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Název"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Název je třeba vyplnit."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Název je třeba vyplnit."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Název nemůže být delší než $0 bajtů"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Název nemůže být delší než $0 znaků"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Název nemůže být delší než 127 znaků."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Název nemůže být delší než 127 znaků."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Název nemůže obsahovat znak „$0“."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "Název nemůže obsahovat znak „$0“."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Název nemůže obsahovat prázdný znak."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr "Potřebujete rezervní disk"
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Je třeba alespoň jeden NTP server"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Síť"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Síťový vstup/výstup"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Síťová spřažení"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Síťová zařízení a grafy vyžadují NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Záznamy událostí sítě"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Využití sítě"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager není nainstalovaný"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager není spuštěný"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Využití sítě"
+
+# auto translated by TM merge from project: Fedora Release Notes, version:
+# f24, DocId: pot/Networking
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Síť"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Nikdy"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Účet s neomezenou dobou platnosti"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Heslo platí napořád"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Nikdy nepříhlášen(a)"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Nové NFS připojení"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Nový hostitel"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "Nový hostitel: $0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Nové heslo ke klíči"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Nový název"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Nová heslová fráze"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Nové heslo"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Nové heslo nebylo přijato"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Další"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Ne"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "Nezadán žádný identifikátor"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Žádné výstrahy SELinux."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Nejsou nainstalované ani dostupné žádné aplikace."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Nejsou k dispozici žádné sloty"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Nejsou k dispozici žádná bloková zařízení."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "Nenalezeno žádné zařízení pro zavádění"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Bez signálu"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Nenalezeno žádné nastavení"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Nejsou k dispozici žádná data"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Nejsou k dispozici žádná data z rozmezí $0 až $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Bez prodlevy"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Není k dispozici žádný popis"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Není poskytnut žádný popis."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "Nenalezeno žádné zařízení pro zavádění"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Nejsou k dispozici žádné disky."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Nenalezeny žádné záznamy událostí"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "Nenalezeny žádné výsledky"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Bez šifrování"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Žádné události"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Žádný souborový systém"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Žádné souborové systémy"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Žádné volné sloty klíčů"
+
+# auto translated by TM merge from project: anaconda, version: rhel6-branch,
+# DocId: anaconda
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Žádné volné místo"
+
+# auto translated by TM merge from project: anaconda, version: rhel6-branch,
+# DocId: anaconda
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "Za tímto oddílem už není žádné volné místo"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Nezadán žádný název skupiny"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Nenalezeny žádné klíče stroje."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Pro tuto aplikaci nebyl nalezen žádný instalační balíček."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Nepřidány žádné klíče"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "Neshoduje se s žádnými jazyky"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Žádné položky záznamu událostí"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Žádné logické svazky"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Nenalezeny žádné záznamy událostí"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Žádné shodující se výsledky"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Není vloženo žádné médium"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Žádný oddíl"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "Žádný oddíl"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "Nepřidány žádné protějšky."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "Fyzické svazky"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Není zadán skutečný název"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Nenalezeny žádné výsledky"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Kritériím filtru neodpovídají žádné výsledky. Pro zobrazení výsledků "
+"vyčistěte veškeré filtry."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Žádné zásahy pravidla"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No storage"
+msgid "No storage found"
+msgstr "Žádné úložiště"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Žádné logické svazky"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Žádný takový soubor nebo složka"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Žádné modifikace systému"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Žádné výkazy o systému."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Žádné aktualizace"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Nebylo zadáno uživatelské jméno"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Žádný"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Není platná soukromá část klíče"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Nemáte oprávnění pro vypnutí brány firewall"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Nemáte oprávnění pro zapnutí brány firewall"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Není k dispozici"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Nepřipojeno"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Nepřipojeno k Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Nepřipojeno k hostiteli"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space"
+msgid "Not enough free space"
+msgstr "Nedostatek prostoru"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "Nedostatek prostoru"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "Pro zvětšení není k dispozici dostatek prostoru."
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Nenalezeno"
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Nenainstalováno"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "Not permitted to configure realms"
+msgid "Not permitted to configure network devices"
+msgstr "Nemáte oprávnění nastavovat realmy"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Nemáte oprávnění nastavovat realmy"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Neoprávněni k provedení této akce."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Nepřipraveno"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Nezaregistrováno"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Není spuštěné"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Nenastaveno"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Nesynchronizováno"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Poznámka"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Notebook"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Oznámení a závažnější"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Znečitelnit síťové adresy, názvy strojů a uživatelská jména"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Znečitelněno"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Vyskytlo se $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Vyskytlo se mezi $0 a $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Výskyty"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "OK"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Původní heslová fráze"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Původní heslo"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Původní heslo nebylo přijato"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "Na připojeném zařízení"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Při nezdaru"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Jakmile bude Cockpit nainstalovaný, zapněte ho pomocí příkazu „systemctl "
+"enable --now cockpit.socket“."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Je použito pouze $0 z $1."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr ""
+"Dovolena jsou pouze písmena a číslice, dále ještě znaky : , _ , . , @ , -"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Pouze nouzové"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"Při startu systému ve FIPS režimu použít pouze schválené a povolené "
+"algoritmy."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Jejda!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr ""
+"Pokud chcete sdílet metriky, otevřete na bráně firewall komunikaci pro "
+"službu pmproxy."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Operace „$operation“ na $target"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Přepínače"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Nebo použijte přibalený prohlížeč"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Ostatní"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"I když není umožněno přihlašování se heslem, ostatní metody ověřování se "
+"jsou pořád k dispozici."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Ostatní volby"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Výstup"
+
+# auto translated by TM merge from project: Fedora Release Notes, version:
+# f23, DocId: pot/Overview
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Přehled"
+
+# auto translated by TM merge from project: Fedora Release Notes, version:
+# f23, DocId: pot/Overview
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Přepsat"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Přepsat existující data nulami (pomalejší)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "Identif. procesu"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Chybí balíček cockpit-pcp a proto není k dispozici historie metrik"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Informace o balíčku"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit zhavaroval"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit není nainstalovaný"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit ohlásilo chybový kód $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Balíčky"
+
+# #-#-#-#-# cs.po (PACKAGE VERSION) #-#-#-#-#
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+# #-#-#-#-# cs.po (PACKAGE VERSION) #-#-#-#-#
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Název stránky"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Nadřazené"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Nadřazené $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Součástí"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Součástí $0"
+
+# auto translated by TM merge from project: blivet-gui, version: f23-branch,
+# DocId: blivet-gui
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Oddíl"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Oddíl na $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "Velikost oddílu je $0. Velikost obsahu na něm je $1."
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Vytváření oddílů"
+
+# auto translated by TM merge from project: blivet-gui, version: f23-branch,
+# DocId: blivet-gui
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Oddíly"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Pasivní"
+
+# auto translated by TM merge from project: anaconda, version: f22-branch,
+# DocId: anaconda
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Heslo"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "Heslovou frázi je třeba vyplnit"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Heslovou frázi je třeba vyplnit"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Heslová fráze z libovolného jiného slotu s klíčem"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Odebrání heslové fráze může bránit odemknutí $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Zadání heslové fráze se neshodují"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Heslo"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Heslo úspěšně změněno"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Skončení platnosti hesla"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "Heslo nemůže být delší než 256 znaků"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Heslo není přijatelné"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Heslo je příliš slabé"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Heslo je třeba změnit"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Heslo nebylo přijato"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Nápověda pro heslo"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Vložit"
+
+# auto translated by TM merge from project: Pulseaudio, version: 6.0, DocId:
+# pulseaudio.pot
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Chyba vkládání"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "Vložit existující klíč"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Sem vložte obsah veřejné části svého ssh klíče"
+
+# auto translated by TM merge from project: selinux (policycoreutils),
+# version: master, DocId: policycoreutils
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Popis umístění"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Náklady trasy"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Náklady trasy $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Popis umístění na serveru"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Popis umístění na serveru je třeba vyplnit."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Je třeba, aby popis umístění na serveru začínal na „/“."
+
+# auto translated by TM merge from project: usermode, version: default, DocId:
+# usermode
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Popis umístění složky"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Popis umístění serveru"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Popisy umístění"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Pozastavit"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+"Protějšek č.$0 nemá platný port koncového bodu. Je třeba, aby port bylo "
+"číslo."
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"Protějšek č.$0 nemá platný port koncového bodu. Je třeba, aby bylo zadáno "
+"jako stroj:port, např. 1.2.3.4:51820 nebo example.com:51820"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "Protějšky"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"Protějšky jsou ostatní stroje, které se připojují k tomuto. Veřejné klíč z "
+"ostatních strojů budou sdíleny mezi sebou."
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot shromažďuje a analyzuje výkonnostní metriky z vašeho "
+"systému."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Profil výkonnosti"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Skříň periferií"
+
+# auto translated by TM merge from project: firewalld, version: master, DocId:
+# po/firewalld
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Trvalá"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "Nenávratně smazat skupinu $0?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "Nenávratně smazat $0?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Nenávratně smazat $0?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete subvolume $0?"
+msgstr "Nenávratně smazat $0?"
+
+# auto translated by TM merge from project: Linux PAM, version: master, DocId:
+# po/Linux-PAM
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Přístup zamítnut"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Permisivní"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Fyzické"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "Fyzické svazky"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Fyzické svazky"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "Zde není možné měnit velikost fyzických svazků."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Vyberte datum"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Připnout jednotku"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Interval pro ping"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Cíl pro ping"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Připnutá jednotka"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Velikost „krabice od pizzy“"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Pro získání přístupu na úrovni správce se prosím ověřte"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Pokud chcete používat webovou konzoli, zapněte podporu pro JavaScript."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Nainstalujte balíček $0"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Vyřešte problém a načtěte tuto stránku znovu."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Zadejte datum skončení platnosti"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Zadejte stroj ke kterému se připojit"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "Prosím nejprve je odpojte."
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Fond pro thin logické svazky"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Fond pro tence poskytované svazky"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Fond pro tence poskytované svazky"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "Heslová fráze k fondu"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Port"
+
+# auto translated by TM merge from project: PulseAudio for RHEL, version: 6.0,
+# DocId: pulseaudio.pot
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Přenosný"
+
+# auto translated by TM merge from project: firewalld, version: master, DocId:
+# po/firewalld
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Porty"
+
+# auto translated by TM merge from project: anaconda, version: rhel6-branch,
+# DocId: anaconda
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Vytvořit oddíl"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Délka předpony"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Délka předpony nebo maska sítě"
+
+# auto translated by TM merge from project: dnf, version: master, DocId: dnf
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Příprava"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Přítomno"
+
+# auto translated by TM merge from project: anaconda, version: master, DocId:
+# main
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Zachovat"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Hezký název stroje"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Předchozí zavedení"
+
+# auto translated by TM merge from project: blivet-gui, version: f23-branch,
+# DocId: blivet-gui
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Primární"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Priorita"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Priorita $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "Soukromý klíč"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "Problém s tím, stát se správcem"
+
+# auto translated by TM merge from project: libreport, version: master, DocId:
+# libreport
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Podrobnosti o problému"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Informace o problému"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Procesy využívající toto umístění"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Stav postupu: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Webové stránky projektu"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Časový limit výzvy prostřednictvím hesla překročen"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Časový limit výzvy prostřednictvím ssh-add překročen"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Časový limit výzvy prostřednictvím ssh-keygen překročen"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Propaguje načíst znovu k"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Chrání před očekávanými nedalekými budoucími útoky za cenu horší "
+"interoperability."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "Zadejte heslovou frázi pro fond na těchto blokových zařízeních:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Veřejná část klíče"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr "Veřejný klíč bude vytvořen v okamžiku zadání platného soukromého"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Účel"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (proužkování)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (zrcadlení)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (proužkování zrcadel)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (vyhrazená parita)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (distribuovaná parita)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (dvojitá distribuovaná parita)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID skříň"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID úroveň"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr "Pro RAID 10 je třeba sudý počet fyzických svazků"
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "Operační paměť"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Skříň do stojanu"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Náhodné"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Rozsah"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "Je třeba, aby rozsah byl striktně řazený"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Rank"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Neupravované na zařízení"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Čtení"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Zjistit více…"
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Pouze pro čtení"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Načítání"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Načítání…"
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Připraveno"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Připraveno"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Skutečný název stroje"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Skutečný název stroje může obsahovat pouze malá písmena (bez diakritiky), "
+"číslice, spojovníky a tečky (u použitých subdomén)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Je třeba, aby skutečný název stroje byl nanejvýš 64 znaků dlouhý"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Znovu uplatnit a restartovat"
+
+# auto translated by TM merge from project: system-config-kdump, version:
+# master, DocId: system-config-kdump
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Restartovat"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Po dokončení restartovat"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Doporučen restart"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Restartovat systém…"
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Příchozí"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Nedávní hostitelé"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Doporučeno, bezpečné nastavení pro stávající modely ohrožení."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Znovu připojit"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Zotavování"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "Zotavování RAID zařízení $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Obnovují se informace o balíčcích"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Odmítá se připojit. Stroj není znám"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Odmítá se připojit. Klíč stroje neodpovídá"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Odmítá se připojit. Klíč stroje není znám"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "Znovu vytvořit"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "Znovuvytváření initrd"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Zaregistrovat…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "Související procesy a služby budou vynuceně zastaveny."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "Související procesy budou vynuceně zastaveny."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "Související služby budou vynuceně zastaveny."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Načíst znovu"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Znovu načíst propagováno z"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Opětovné načítání"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Znovunačítání stavu zbývajících služeb"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "Vzdálené přes CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "Vzdálené přes FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Vzdálené přes NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "Vzdálené přes NFS"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "Vzdálené přes SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Vzdálené přes SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "Vzdálené přes SSH"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Odebrání:"
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Odebrat"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Odebrat $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Odebrat službu $0 ze zóny $1"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Odebrat $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Odebrat Tang server s klíči?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Odebrat zařízení"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Odebrat hostitele"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Odebrat položku"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "Odebrat chybějící fyzické svazky?"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "Odebrat heslovou frázi v slotu na klíč $0?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "Odebrat heslovou frázi?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Odebrat službu $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Odebrat zónu $0"
+
+# auto translated by TM merge from project: dnf, version: master, DocId: dnf
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Odebírá se"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Odebírá se $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Odebrání $0 přeruší spojení se serverem a znepřístupní tak rozhraní pro jeho "
+"správu."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "Odebírání $target z RAID zařízení"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"Odebrání heslové fráze bez potvrzení jiné heslové fráze může bránit v "
+"odemknutí nebo správě klíčů, pokud jsou ostatní heslové fráze zapomenuty "
+"nebo ztraceny."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Odebírání fyzického svazku z $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Odebrání služby cockpit může vést k tomu, že webová konzole přestane být "
+"dostupná. Ověřte, že se tato zóna nevztahuje na vaše stávající spojení s "
+"touto webovou konzolí."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "Odebrání zóny odebere také všechny služby, které obsahuje."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Přejmenovat"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Přejmenovat Stratis fond"
+
+# auto translated by TM merge from project: usermode, version: default, DocId:
+# usermode
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Přejmenovat souborový systém"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Přejmenovat skupinu"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "Přejmenovat skupinu $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Přejmenovat logický svazek"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Přejmenovat skupinu svazkůÚložiště"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Přejmenovává se $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "Přejmenování skupiny může ovlivnit sudo a podobná pravidla"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr "Opravit"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+msgid "Repair logical volume $0"
+msgstr "Opravit logický svazek $0"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Opravuje se $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Opakovat"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Opakovat každý měsíc"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Zopakovat heslovou frázi"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Opakovat každý týden"
+
+# auto translated by TM merge from project: abrt, version: rhel7, DocId: abrt
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Nahlásit"
+
+# auto translated by TM merge from project: abrt, version: rhel7, DocId: abrt
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Štítek výkazu"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Hlásit do ABRT analýzy"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Nahlášeno; nejsou k dispozici žádné odkazy"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Nahlašování se nezdařilo"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Nahlašování bylo zrušeno"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Výkazy"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Hlášení:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Vyžadovat změnu hesla každých $0 dnů"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Vyžadovat změnu hesla na $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Při prvním přihlášení vyžadovat změnu hesla"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Vyžadováno"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Vyžadováno "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Vyžaduje"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Pro úpravu jsou vyžadována oprávnění správce systému"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Závislost"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Závislost pro"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Vyhrazená paměť"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Reset"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Resetovat heslo"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Mění se velikost $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Změna velikost šifrovaného souborového systému vyžaduje jeho odemčení. "
+"Zadejte heslovou frázi."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Restartovat"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Restartovat služby"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Restartovat služby…"
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Restartuje se"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Obnovování spojení"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"Pokud je správně nastavený kdump, budou výsledky zkopírovány prostřednictvím "
+"$0 na $1 jako $2."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr "Pokud je správně nastavený kdump, budou výsledky uloženy v $0 jako $1."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Obnovit chod"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Použít stávající šifrování"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Použít stávající šifrování ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "Zkontrolovat pravidla pro šifrování"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Vyhodnocování záznamu událostí"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Round robin"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Trasy"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Spustit v"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Vytvořit nový výkaz"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Spustit na"
+
+# auto translated by TM merge from project: abrt, version: rhel7, DocId: abrt
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Vytvořit výkaz"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+"Na vzdáleném stroji spusťte – přes důvěryhodnou síť nebo fyzicky přímo na "
+"něm – tento příkaz:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Spouštěč"
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Spuštěné"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Běhové prostředí"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "Chyby řízení přístup SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux je na tomto systému vypnutý"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux je na tomto systému vypnutý."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux zásada"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "Systémový stav SELinux není znám."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "Řešení problémů s SELinux"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "SMART samotest $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"SOS hlášení shromažďuje systémové informace napomáhající diagnostice "
+"problémů."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH klíč"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "V kolonce ssh klíč není vyplněné jeho umístění"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH klíče"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "STP prodleva přeposílání"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "STP uvítací doba"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP maximální stáří zprávy"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP priorita"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Uživatelé prohlížeče Safari budou potřebovat naimportovat si certifikát samu "
+"sebe podepisující certifikační autority a nastavit ji jako důvěryhodnou:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Soboty"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Uložit"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Uložit a restartovat"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Uložit změny"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Šetřit prostorem komprimováním jednotlivých bloků pomocí LZ4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Šetřit prostorem tím, že stejné bloky dat budou ukládány pouze jednou"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Uložení nové heslové fráze vyžaduje odemknutí disku. Zadejte stávající "
+"heslovou frázi k disku."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Naplánované vypnutí v $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Naplánovaný restart v $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "Počítač se zapečetěnou skříní"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Hledat"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Prohledávané domény"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Hledat název nebo identif."
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Hledat jméno, skupinu nebo identif."
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "Je třeba, aby sekunda bylo číslo z rozmezí 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Sekund"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Klíče zabezpečeného shellu"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Bezpečné vymazávání $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Nastavení SELinux a řešení problémů"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Jsou k dispozici aktualizace zabezpečení"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Pouze aktualizace zabezpečení"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Aktualizace zabezpečení budou aplikovány $0 v $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Vybrat"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Vybrat identifikátor"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Vybrat metodu"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+"Vyberte fyzické svazky, které mají být použity pro opravu logického svazku. "
+"Je zapotřebí alespoň $0."
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Odeslat"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Odchozí"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Sériové číslo"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Server"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Adresa serveru"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Adresu serveru je třeba vyplnit."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Server je třeba vyplnit."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Server zavřel spojení."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Serverový software"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Služba"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Služba má chybu"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Záznamy událostí služby"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Služby"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Služby využívající toto umístění"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Sezení"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Platnost relace brzy skončí"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Nastavit"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Nastavit název stroje"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Vytvořit oddíl na $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Nastavit heslo"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Nastavit čas"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Nastavit na"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Nastavit"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Nastavit snadno prolomitelné heslo"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Nastavení se odchyluje od nastaveného stavu a bude navráceno při příštím "
+"restartu."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Nastavování"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Nastavování zařízení zpětné smyčky $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Nastavení"
+
+# auto translated by TM merge from project: dnf, version: master, DocId: dnf
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Závažnost"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Sdílené"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Shell"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Shellový skript"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "Zobrazit všechna vlákna"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Zobrazit všechna vlákna"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Show confirmation password"
+msgstr "Zobrazit potvrzení hesla"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Zobrazit otisky"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Zobrazit zprávy obsahující daný řetězec."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Zobrazit zprávy pro zadanou systemd jednotku."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Zobrazit zprávy z konkrétního startu systému."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Zobrazit další vztahy"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "Zobrazit heslo"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Zobrazit vztahy"
+
+# auto translated by TM merge from project: anaconda, version: master, DocId:
+# main
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Zmenšit"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Zmenšit logický svazek"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "Zmenšit oddíl"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Zmenšit oddíl"
+
+# auto translated by TM merge from project: virt-manager, version: master,
+# DocId: virt-manager
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Vypnout"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Vypnout"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Od"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Single rank"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Velikost"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "Velikost nemůže být záporná"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "Velikost nemůže být nula"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Příliš velké"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Je třeba, aby velikost byla číslo"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Je třeba, aby velikost byla alespoň $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Přeskočit hlavní navigaci"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Přeskočit na obsah"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Slot"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Slot $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Zachycený stav"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Sokety"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Aktualizace software"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Softwarová ošetření problémů zabezpečení procesoru, snižující riziko jejich "
+"zneužití. Mají negativní dopad na výkonnost. Nastavení měníte na vlastní "
+"nebezpeční."
+
+#: pkg/storaged/drive/drive.jsx:63
+#, fuzzy
+#| msgid "Solid-State Disk"
+msgid "Solid State Drive"
+msgstr "Jednotka bez pohyblivých částí"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Řešení úspěšně uplatněno"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Řešení se nezdařilo"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Řešení"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"Velikost některých blokových zařízení byla po vytvoření fondu zvětšena. Fond "
+"je možné bezpečně zvětšit a využít tak nově dostupné místo."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"Správa balíčků je v tuto chvíli používána nějakým jiným programem, vyčkejte…"
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Některý software je třeba restartovat ručně"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr "Promiňte!"
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Seřazeno od nejméně po nejvíce důvěryhodné"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Prostorově úsporný počítač"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Spanning tree protocol"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Spanning tree protocol (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Náhradní"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Konkrétní čas"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Rychlost"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Stabilní"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Spustit"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Spustit a zapnout"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Spustit multipath"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Spustit službu"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr "Začít zobrazovat položky od zadaného data výše."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "Začít zobrazovat položky od zadaného data dál do minulosti."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Spuštěno"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "Spouštění RAID zařízení $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Spouštění odkládacího prostoru $target"
+
+# auto translated by TM merge from project: selinux (policycoreutils),
+# version: master, DocId: policycoreutils
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Stav"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Statické"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Stav"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Počítač v klíčence"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Lepkavé"
+
+# auto translated by TM merge from project: Fedora Media Writer, version:
+# master, DocId: mediawriter
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Zastavit"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Zastavit a vypnout"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Zastavit a odebrat"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Zastavit a odpojit"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Zastavit zařízení"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Zastavit upravování hostitelů"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Zastavit výkaz"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "Zastavování RAID zařízení $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Zastavování odkládacího prostoru $target"
+
+# auto translated by TM merge from project: virt-manager, version: master,
+# DocId: virt-manager
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Úložiště"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "Úložiště není na tomto systému možné spravovat."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Záznamy událostí úložiště"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Uložit heslovou frázi"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Uložená heslová fráze"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "$0 blokové zařízení"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "Add block devices"
+msgid "Stratis block devices"
+msgstr "Přidat blokové zařízení"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Bloková zařízení Stratis není možné zmenšovat"
+
+# auto translated by TM merge from project: usermode, version: default, DocId:
+# usermode
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystem"
+msgstr "Vytvořit souborový systém"
+
+# auto translated by TM merge from project: usermode, version: default, DocId:
+# usermode
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems"
+msgstr "Vytvořit souborový systém"
+
+# auto translated by TM merge from project: usermode, version: default, DocId:
+# usermode
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Vytvořit souborový systém"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Stratis fond"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr "Proužkované (RAID 0)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr "Proužkované a zrcadlené (RAID 10)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr "Proužky"
+
+#: pkg/lib/cockpit-components-password.jsx:97
+msgid "Strong password"
+msgstr "Odolné heslo"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Pahýl"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Styl"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Zmenšená skříň"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Zmenšený notebook"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "Přihlášení se k odběru systemd signálů se nezdařilo: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "Svazek se nepodařilo vytvořit"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Úspěšně zkopírováno do schránky"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Úspěšně zkopírováno do schránky!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Neděle"
+
+# auto translated by TM merge from project: udisks, version: udisks, DocId:
+# udisks2
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Odkládací oddíl"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "$0 souborovým systémům zde nelze změnit velikost."
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Odstránkováno"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Vypnout $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Vypnout $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Zapnout $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Přepnout na přístup správce"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Přepnout na omezený přístup"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Vypnutí $0 přeruší spojení se serverem a znepřístupní tak rozhraní pro jeho "
+"správu."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Zapnutí $0 přeruší spojení se serverem a znepřístupní tak rozhraní pro jeho "
+"správu."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Synchronizováno"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Synchronizováno s $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Synchronizuje se"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "Synchronizuje se RAID zařízení $target"
+
+# auto translated by TM merge from project: selinux (policycoreutils),
+# version: master, DocId: policycoreutils
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Systém"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Diagnostika systému"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Informace o systému"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Systém je aktuální"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Modifikace systému"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Systémový čas"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemd jednotky"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+# auto translated by TM merge from project: virt-manager, version: master,
+# DocId: virt-manager
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tablet"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang server s klíči"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Cíl"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Cíle"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Tým"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Port týmu"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Nastavení portu týmu"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminál"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Ukončit sezení"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Vyzkoušet nastavení"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Test je k dispozici pouze pokud je spuštěná služba kdump."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Vyzkoušet nastavení kdump"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Vyzkoušet nastavení kdump vyvoláním havárie jádra systému. Může to chvíli "
+"trvat a může se stá, že systém se nerestartuje automaticky. Nevyvolávejte "
+"pád systému pokud je spuštěná nějaká důležitá úloha."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Zkouška spojení"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "Balíček $0 není k dispozici z žádného z repozitářů."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr ""
+"Pro vytváření Stratis fondu je třeba, aby byl nainstalovaný balíček $0."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "Je třeba, aby byl nainstalovaný balíček $0."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "Pro vytváření VDO zařízení bude nainstalován balíček $0."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP adresa nebo název stroje nemůže obsahovat prázdný znak."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "RAID pole je v degradovaném stavu"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr ""
+"Aby bylo možné odebírat disky je třeba, aby RAID zařízení bylo spuštěné."
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+"SSH klíč $0 uživatele $1 na $2 bude přidán do souboru $3 uživatele $4 na $5."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"SSH klíč $0 bude zpřístupněn po celou relaci a bude k dispozici také pro "
+"přihlašování se k ostatním strojům."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"SSH klíč pro přihlašování se k $0 je chráněn. Můžete se buď přihlásit svým "
+"přihlašovacím heslem nebo zadáním hesla ke klíči na $1."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"SSH klíč pro přihlašování se k $0 je chráněn. Můžete se buď přihlásit svým "
+"přihlašovacím heslem nebo zadáním hesla ke klíči na $1."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr "Účet „$0“ bude při příštím přihlášení vyzván k vynucené změně hesla"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Služba cockpit je obsažena automaticky"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr "Nastavený stav není znám, může se změnit při příštím startu."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr "Vytváření tohoto VDO zařízení není dokončeno a proto ho nelze použít."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr "Stávající uživatel není oprávněn zobrazovat informace o klíčích."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"Aby ho bylo možné naformátovat, disk je třeba odemknout. Zadejte stávající "
+"heslovou frázi."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "Soubor $0 bude smazán."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "Souborový systém nemá trvalý přípojný bod."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "Souborový systém nemá trvalý přípojný bod."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Souborový systém je nastavený tak, aby byl automaticky připojován při startu "
+"systému, ale šifrovaný kontejner, ve kterém se nachází, nebude v tu dobu "
+"odemčený."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Souborový systém je nyní připojený, ale po příštím startu systému už nebude."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Souborový systém je nyní připojený v $0, ale po příštím startu systému bude "
+"připojený v $1."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Souborový systém je nyní připojený v $0, ale po příštím startu systému už "
+"nebude připojený vůbec."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Souborový systém nyní není připojený, ale po příštím startu systému už bude."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Souborový systém není připojený."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"Souborový systém bude odemčen při příštím startu systému. To může vyžadovat "
+"zadání heslové fráze."
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "Otisk by se měl shodovat:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Je třeba restartovat následující službu:"
+msgstr[1] "Je třeba restartovat následující služby:"
+msgstr[2] "Je třeba restartovat následující služby:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "Je třeba, aby celé jméno neobsahovalo dvojtečky."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "Je třeba, aby identif. skupiny bylo celé kladné číslo"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Název skupiny se může sestávat pouze z písmen a-z (bez diakritiky), číslic, "
+"teček, spojovníků a podtržítek"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"Domovská složka $0 už existuje. Její vlastnictví bude změněno na nového "
+"uživatele."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "Je třeba znovu vytvořit initrd."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Heslo ke klíči je třeba vyplnit"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Zadání hesla ke klíči se neshodují"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "Zadaný klíč není platný."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Poslední zbývající slot klíče není možné odebrat"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "Uvedené procesy a služby budou vynuceně ukončeny."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "Uvedené procesy budou vynuceně zastaveny."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "Uvedené služby budou vynuceně zastaveny."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "Přihlášený uživatel není oprávněn zobrazovat modifikace systému"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "Stroj se restartuje"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "Přípojný bod $0 je používán následujícími procesy:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "Přípojný bod $0 je používán následujícími službami:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Nové heslo ke klíči je třeba vyplnit"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Heslo je třeba vyplnit"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Zadání hesla se neshodují"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Zadání hesla se neshodují."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr "Výsledný otisk je možné sdílet veřejnými způsoby, včetně e-mailu."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"Výsledný otisk je možné bez problémů sdílet prostřednictvím veřejných metod, "
+"včetně e-mailu. Pokud někoho jiného požádáte, aby pro vás ověřil, může "
+"výsledky poslat libovolnou metodou."
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Server odmítl ověřit „$0“ pomocí ověření heslem a nejsou k dispozici žádné "
+"další metody ověření."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr "Server odmítl ověřit u všech podporovaných metod."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"Systém v tuto chvíli nepodporuje odemykání souborového systému pomocí Tang "
+"serveru s klíči v průběhu zavádění systému."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"Systém v tuto chvíli nepodporuje odemykání kořenového souborového systému "
+"pomocí Tang serveru s klíči."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+"Uživatel $0 není oprávněn měnit zmírňování dopadu chyb zabezpečení procesoru"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "Uživatel $0 nemá oprávnění měnit pravidla pro šifrování"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "Uživatel $0 nemá oprávnění způsobovat testovací pády jádra systému"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"Aby se nová nastavení projevila je třeba, aby se uživatel odhlásil a "
+"přihlásil znovu."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Uživatelské jméno se může sestávat pouze z písmen a-z (bez diakritiky), "
+"číslic, teček, spojovníků a podtržítek."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"Nastavení webového prohlížeče brání ve spuštění Cockpit (nepřístupné $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "V tuto chvíli zde nejsou žádné aktivní stránky"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"V systému jsou zařízení s vícero cestami, ale služba multipath není spuštěná."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "V této zóně se nenacházejí žádné aktivní služby"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Pro tento účet nejsou žádné pověřené klíče."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+"Není k dispozici dostatek prostoru, který by bylo možné použít pro opravu. "
+"Na fyzických svazcích je zapotřebí přinejmenším $0, které ještě nejsou "
+"použity pro logický svazek."
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"Ve fondu není dostatek místo pro pořízení zachyceného stavu souborového "
+"systému. Je zapotřebí alespoň $, ale k dispozici je jen $1."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Při připojování ke stroji došlo k neočekávané chybě."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Jsou nezbytné tyto další kroky:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Budou učiněny tyto změny:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Tenký logický svazek"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Fond pro tence poskytované svazky"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+#, fuzzy
+#| msgid ""
+#| "This RAID array has no write-intent bitmap. Such a bitmap can reduce "
+#| "sychronization times significantly."
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"Toto RAID pole nemá bitovou mapu pro write-intent. Taková mapa významně "
+"zkrátit časy potřebné k synchronizaci."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr "Toto NFS připojení je používáno a měnit je možné jen jeho volby."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Toto VDO zařízení nepoužívá ze svého podkladového zařízení vše."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Toto zařízení zde nelze spravovat."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Toto zařízení zde nelze spravovat."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "Toto zařízení je v tuto chvíli používáno."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Tuto kolonku je třeba vyplnit"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Tato skupina je primární skupinou pro následující uživatele:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "Tento hostitel se po instalaci aktualizací restartuje."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "Tato informace je uložena pouze lokálně na systému."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Tento server s klíči je jediný způsob jak fond odemknout a není ho proto "
+"možné odebrat."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+"Tento logický svazek ztratil některé své fyzické svazky a už ho není možné "
+"používat. Je třeba ho smazat a vytvořit nový na jeho místě."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+"Tento logický svazek ztratil některé své fyzické svazky, ale zatím ještě "
+"nedošlo ke ztrátě žádných dat. Měli byste ho opravit a obnovit tak jeho "
+"původní odolnost."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+"Tento logický svazek ztratil některé své fyzické svazky, ale nejspíš ještě "
+"neztratil žádná data. Měli byste být schopní ho opravit."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr ""
+"Tento logický svazek není zcela využíván obsahem, který se na něm nachází."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Tento stroj už byl přidán."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Toto může chvíli trvat"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "Tento oddíl není zcela využíván obsahem, který se na něm nachází."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Tato heslová fráze je jediným způsobem jak fond odemknout a proto jí není "
+"možné odebrat."
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr "Tento fond nevyužívá ze svých podkladových zařízení všechen prostor."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "Tento fond se nachází v degradovaném stavu."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Tento systém není zaregistrován"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Tento systém používá uživatelsky určený profil"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Tento systém používá doporučený profil"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"Tento nástroj nastavuje pravidla pro SELinux a může pomoci s porozuměním a "
+"řešením porušení pravidel."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"Tento nástroj nastavuje systém pro zapisování výpisů pádů jádra na disk."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"Tento nástroj vytváří archiv nastavení a diagnostických informací z běžícího "
+"systému. Archiv je možné uložit lokálně nebo centrálně pro účely sledování "
+"či záznamu nebo je možné ho poslat zástupcům technické podpory, vývojářům "
+"nebo správcům systémů aby pomohli s hledáním technických selhání a laděním."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"Tento nástroj spravuje místní úložiště, jako například souborové systémy, "
+"LVM2 skupiny svazků a NFS připojení."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"Tento nástroj spravuje síťování jako například spřažení, mosty, spojení, "
+"VLAN sítě a brány firewall pomocí NetworkManager a Firewalld. NetworkManager "
+"není kompatibilní s Ubuntu ve výchozím stavu používaným systemd-networkd a "
+"skripty ifupdown v distribuci Debian."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Tato jednotka není navržena k tomu, aby byla výslovně zapnuta."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Toto uživatelské jméno už existuje"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr "Této skupině svazků chybí některé fyzické svazky."
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Tento webový prohlížeč je příliš starý pro spuštění Web Console (chybí $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Toto přidá shodu pro „_BOOT_ID=“. Pokud není určeno, budou zobrazeny záznamy "
+"událostí pro stávající start systému. Pokud je identifikátor startu "
+"vynechán, kladný posun prohledá starty počínaje od začátku žurnálu a posun "
+"rovný nebo menší než nula prohledá starty počínaje od konce žurnálu. Proto 1 "
+"znamená první start, nalezený v žurnálu v chronologickém pořadí, 2 druhé a "
+"tak dále; zatímco -0 je poslední start, -1 start před tím posledním, a tak "
+"dále."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Toto přidá shodu pro „_SYSTEMD_UNIT=“, „COREDUMP_UNIT=“ a „UNIT=“ a pomůže "
+"tak nalezení všech případných zpráv pro danou jednotku. Může obsahovat "
+"vícero jednotek, oddělovaných čárkou. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Toto vám umožní se napříště přihlašovat bez hesla."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Tato zóna obsahuje službu cockpit. Ověřte, že se tato zóna nevztahuje na "
+"vaše stávající spojení s touto webovou konzolí."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Čtvrtky"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Tier"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Čas"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Časová zóna"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Vytvoření časovače se nezdařilo"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "Smazání časovače se nezdařilo"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Časovače"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Tip: Nastavte si heslo ke klíči stejné jako pro přihlašování a budete se "
+"vůči ostatním systémům ověřovat automaticky."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Abyste zajistili, že do vašeho připojení není zasahováno záškodnickou třetí "
+"stranou, ověřte otisk klíče hostitele:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Pro získávání aktualizací software je třeba systém zaregistrovat u Red Hat, "
+"buď pomocí Red Hat portálu pro zákazníky nebo místního serveru předplatného."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Pokud chcete otisk ověřit, spusťte následující na $0 když jste fyzicky u "
+"stroje nebo prostřednictvím důvěryhodné sítě:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Dnes"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Přepnout"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Přepnout volič datumů"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Přepnout filtry"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Příliš mnoho dat"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Nástroje"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "5 služeb nejvíce vytěžujících procesor"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "5 služeb nejvíce vytěžujících úložiště"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "5 služeb zabírajících nejvíce paměti"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Celková velikost: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Věž"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Přechodné"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Přenáší se"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Spouštěč"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Spuštěno na základě"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Spouštěče"
+
+# auto translated by TM merge from project: SETroubleShoot, version: master,
+# DocId: framework/po/setroubleshoot
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Řešit potíže"
+
+# auto translated by TM merge from project: SETroubleShoot, version: master,
+# DocId: framework/po/setroubleshoot
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Řešit potíže…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "Důvěřovat a přidat hostitele"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Důvěryhodný klíč"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Stupeň důvěryhodnosti"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Zkusit znovu"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Pokus o synchronizaci se $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Úterky"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Spuštění procesu služby tuned se nezdařilo"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned je služba která monitoruje váš systém a optimalizuje jeho výkon pod "
+"určitými zátěžemi. Jádrem Tuned jsou profily, který ladí váš systém pro "
+"různé případy použití."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned není k dispozici"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned není spuštěné"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned je vypnuté"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Zapnout přístup na úrovni správce"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Typ"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Název nemůže obsahovat znak „$0“."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Filtrujte psaním"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Nedaří se automaticky uplatnit toto řešení"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "K této adrese se nedaří připojit"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "Nedaří se kontaktovat $0."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Nepodařilo se kontaktovat uvedený stroj $0. Ověřte, že je ssh spuštěno na "
+"portu $1 nebo v jeho adrese zadejte jiný port."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Nedaří se získat podrobnosti o výstraze."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Nepodařilo se přihlásit k $0 pomocí ověření se SSH klíčem. Prosím zadejte "
+"heslo. Pokud se chcete přihlašovat automaticky, nastavte SSH klíče."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Nedaří se přihlásit k $0. Hostitel nepřijímá přihlášení heslem nebo žádný z "
+"vašich SSH klíčů."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Server se nedaří dosáhnout"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Účet se nedaří odebrat"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+msgid "Unable to repair logical volume $0"
+msgstr "Nepodařilo se opravit logický svazek $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "Nedaří se spustit opravu: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "Nastavení se nedaří uložit"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "Nastavení se nedaří uložit: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Nedaří se spustit setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Nedaří se odpojit souborový systém"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Nedostupné"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Nedostupné balíčky"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Zpět"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "Neočekávaná chyba PackageKit v průběhu instalace $0: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Neočekávaná chyba"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Nerozpoznaná data"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Jednotka"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Neznámé"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Neznámé „$0“"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Neznámé ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Neznámá aplikace"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Neznámé nastavení"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Neznámý název stroje"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Neznámý název služby"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Neznámý typ"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Odemknout"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Automaticky odemknout při startu systému"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr "Odemknout před změnou velikosti"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Odemknout šifrovaný Stratis fond"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "Odemknout klíč $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Odemyká se $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Odemykání disku"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Nespravovaná rozhraní"
+
+# auto translated by TM merge from project: blivet-gui, version: f23-branch,
+# DocId: blivet-gui
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Odpojit"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "Odpojit souborový systém $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Odpojit nyní"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Odpojování $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Bez názvu"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "Zrušit připnutí jednotky"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Nerozpoznaná data"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "Unrecognized data can not be made smaller here."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Nerozpoznaná data zde nelze zmenšit."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Nerozpoznaná data zde nelze zmenšit."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Nepodporovaný svazek"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "Dokud"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Nedůvěryhodný stroj"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Aktualizovat"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Tabulka úspěchů aktualizací"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Historie aktualizací"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Aktualizovat informace o balíčcích"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "Aktualizace byla úspěšná"
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Aktualizováno"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr "Aby se uplatnily, aktualizovaný balíček může vyžadovat restart."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Jsou k dispozici aktualizace"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Historie aktualizací"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Aktualizace bude aplikovány $0 v $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Aktualizuje se"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Aktualizace stavu…"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Doba chodu od spuštění"
+
+# auto translated by TM merge from project: audit-viewer, version: default,
+# DocId: audit-viewer
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Použití"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "Využití $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Použít"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Komprimovat"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Deduplikovat"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Použít klíč"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Použít heslo"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Pro ověřování vůči ostatním systémům použít následující klíče"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Zaznamenávat podrobněji"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Využito"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"Užitečné pro připojení která jsou volitelná nebo vyžadují interakci (jako "
+"např. heslovou frázi)"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Uživatel"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "Identif. uživatele"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "Identif. uživatele už je používán jiným uživatelem"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "Je třeba, aby identif. uživatele bylo celé kladné číslo"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "Je třeba, aby identif. nebyl vyšší než $0"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "Identif. uživatele nemůže být delší než $0"
+
+# #-#-#-#-# cs.po (PACKAGE VERSION) #-#-#-#-#
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+# #-#-#-#-# cs.po (PACKAGE VERSION) #-#-#-#-#
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Uživatelské jméno"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Uživatelské jméno je třeba vyplnit"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Uživatelské jméno"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "S použitím LUKS šifrování"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "S použitím Tang serveru"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "Zařízení, která jsou podkladem pro VDO, není možné zmenšovat"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO zařízení $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "Svazek VDO souborového systému (komprese/deduplikace)"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "Identif. VLAN"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Ověřuje se adresa"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Kontroluje se ověřovací token"
+
+# auto translated by TM merge from project: libosinfo, version: master, DocId:
+# libosinfo
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Výrobce"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Ověřeno"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "Ověřit otisk"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Ověřit klíč"
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Ověřuje se"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Verze"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Velmi jisté vymazávání $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Zobrazit všechny procesory"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Zobrazit všechny disky"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Zobrazit všechny záznamy událostí"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Zobrazit všechny služby"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Zobrazit automatizační skript"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Zobrazit podrobné záznamy událostí"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Zobrazit podrobnosti o hardware"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Zobrazit historii přihlášení"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "Zobrazit záznamy událostí"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Zobrazit metriky a jejich historii"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Zobrazit toky dat pro jednotlivé disky"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Webové stránky projektu"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Zobrazit hlášení"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Zobrazit záznamy událostí při aktualizacích"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr ""
+"Zobrazování informaci o paměti vyžaduje oprávnění na úrovni správy systému."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Jít na bránu firewall"
+
+# auto translated by TM merge from project: anaconda, version: master, DocId:
+# main
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Skupina oddílů"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "This volume group is missing some physical volumes."
+msgid "Volume group is missing physical volumes"
+msgstr "Této skupině svazků chybí některé fyzické svazky."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Velikost svazku je $0. Velikost obsahu na něm je $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Čeká se"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Čeká se na podrobnosti…"
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Čeká se na vstup…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr "Čeká se až ostatní programy dokončí použití správy balíčků…"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Čeká se na dokončení ostatních operací správy balíčků"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Čeká se na spuštění…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Vyžadováno"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Vyžaduje"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Varování a závažnější"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+msgid "Weak password"
+msgstr "Snadno prolomitelné heslo"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Webová konzole"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Webová konzole pro linuxové servery"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Webová konzole se restartuje"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Webová konzole je spuštěná v režimu omezeného přístupu."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Logo webové konzole"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Středy"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Každý týden"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Týdny"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Když"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "Pokud nevyplněno, připojit se jako stávající uživatel"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Když je webová konzole restartována, už neuvidíte informaci o průběhu. "
+"Nicméně aktualizační proces bude pokračovat na pozadí. Pokud chcete sledovat "
+"proces aktualizace, znovu se připojte."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"Pokud je tato volba zaškrtnuta, nový fond neumožní přidělování nad rámec "
+"kapacit (overprovisioning). Pro každý ze souborových systémů, vytvářených ve "
+"fondu, je třeba zadat jakou velikost má mít nejvýše. Poté, co budou "
+"vytvořeny, už nebude možné souborové systémy zvětšovat. Zachycené stavy jsou "
+"vytvářeny v plné velikosti. Součet všech maximálních velikostí nemůže "
+"překračovat velikost fondu. Výhodou toho je, že souborovým systémům ve fondu "
+"nemůže dojít neočekávaně velikost. Nevýhodou je, že je potřeba znát "
+"maximální velikost pro každý ze souborových systémů předen a vytváření "
+"zachycených stavů je omezené."
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Bílá"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "Bude nastaveno na „Automaticky“"
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "World wide name"
+msgid "World wide name"
+msgstr "World wide name"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Zápis"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Převážně zápis"
+
+# auto translated by TM merge from project: asknot-ng, version: 0.1, DocId:
+# locale/asknot-ng
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Zapisování"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Chybné uživatelské jméno nebo heslo"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Každý rok s ním v tu dobu n"
+
+# auto translated by TM merge from project: selinux (policycoreutils),
+# version: master, DocId: policycoreutils
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Ano"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "K $0 se připojujete poprvé."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Nemáte oprávnění upravovat bránu firewall."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr "Nemáte oprávnění zobrazovat pověřené klíče pro tento účet."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Byli jste odhlášeni z důvodu nečinnosti."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Můžete zkusit načíst starší položky."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+"Pro automatické přihlašování se nejspíš bude třeba změnit heslo ke klíči."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Pro změnu hesla je třeba vyčkat déle"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr ""
+"Pro zobrazení historie metrik je třeba, abyste se odhlásili a znovu "
+"přihlásili"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Nyní máte přístup na úrovni správy systému."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Budete odhlášení za $0 sekund."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Váš účet"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"Vámi využívaný prohlížeč neumožňuje vkládání z kontextové nabídky. Náhradně "
+"je možné použít Shift+Insert."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "Váš prohlížeč si bude úroveň přístupu pamatovat i pro příště."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Server brzy zavře spojení. Po dokončení jeho restartu se budete moci opět "
+"připojit."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Vaše sezení bylo ukončeno."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Platnost vašeho sezení skončila. Přihlaste se znovu."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Zóna"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[binární data]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[žádná data]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "přístup"
+
+# auto translated by TM merge from project: libvirt, version: master, DocId:
+# libvirt
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "aktivní"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "doplněk"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "doplněk"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "po síti"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "aplikace"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "inventární štítek"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "v"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "průměr: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "zařízení, na kterém je VDO zařízení založeno"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "spřažení"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "boot"
+
+# auto translated by TM merge from project: libguestfs, version: master,
+# DocId: po/libguestfs
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "most"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "Ostatní zařízení"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "Ostatní zařízení"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs subvolume"
+msgstr "Svazek úložiště"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Úložné svazky"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "Svazek úložiště"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "oprava chyby"
+
+# auto translated by TM merge from project: Blivet, version: master, DocId:
+# blivet
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "bajtů"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "Mezipaměť"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "řídící skupiny"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "změnit"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman není nainstalován"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "příkaz"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "konzole"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "výpis paměti"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "procesor"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "pád"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "$0 data"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "datum"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Deaktivovat"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "ladění"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "smazat"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "vypnout"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "disk"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "disky"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "doména"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "jednotka"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "upravit"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "zapnout"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted"
+msgid "encrypted"
+msgstr "Šifrováno"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "šifrování"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "vylepšení"
+
+# auto translated by TM merge from project: SSSD, version: master, DocId:
+# po/sssd
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "chyba"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "každý pátek"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "každé pondělí"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "každý úterý"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "každou neděli"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "každý čtvrtek"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "každé úterý"
+
+# auto translated by TM merge from project: docbook-locales, version: master,
+# DocId: locale
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "každou středu"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "každý den"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "rozšíření"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "nepodařilo se vypsat ssh klíče stroje: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "souborový systém"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "brána firewall"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "pro stávající a budoucí jádra systému"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "pouze pro stávající jádro systému"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "formátovat"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "z $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "z $0 na $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "grafy"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "zvětšit"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "hardware"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "historie"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "stroj"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "iSCSI cíl"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "iSCSI cíle"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "Přidat iSCSI portál"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "ignorovat nezdar"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "ve většině prohlížečů"
+
+# auto translated by TM merge from project: comps, version: master, DocId:
+# po/comps
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "nekonzistentní"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "inicializovat"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "nainstalovat"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "rozhraní"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "neplatné: definováno vícero cílů"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "žurnál"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "manuálová stránka ke journalctl"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "stav kdump"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "klíče"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "přihlášení"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac adresa"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "stroj"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "maska"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "max: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "member of RAID device"
+msgid "member of MDRAID device"
+msgstr "člen RAID zařízení"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "člen Stratis fondu"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "paměť"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "metriky"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "omezení vlivu"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+# auto translated by TM merge from project: Fedora Websites, version:
+# getfedora.org, DocId: po/getfedora
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "další podrobnosti"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "mount"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "síť"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "nikdy nepřipojovat při startu systému"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "není vyplněná kolonka nfs exportu"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "není vyplněná kolonka nfs server"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "nfs server není platná IPv6"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "pořadí přednosti (nice)"
+
+# auto translated by TM merge from project: anaconda, version: rhel8-alpha-
+# branch, DocId: anaconda
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "nic"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "z $0 procesoru"
+msgstr[1] "ze $0 procesorů"
+msgstr[2] "z $0 procesorů"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "na $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "operační systém"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "os"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "balíček"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "oddíl"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "heslo"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "odolnost hesla"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "záplaty"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "popis umístění"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "výkon"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "fyzický svazek LVM2 skupiny svazků"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "zásuvný modul"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "služba pmlogger.service zhavarovala"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "službě pmlogger.service se nedaří shromažďovat data"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "služba pmlogger.service není spuštěná"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "pod"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "port"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "napájení"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "operační paměť"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "pouze pro čtení"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "doporučeno"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "odebrat z LVM2"
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "remove from RAID"
+msgid "remove from MDRAID"
+msgstr "odebrat z RAID"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "odebrat z LVM2"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "restartovat"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "role"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "spuštěné $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "zabezpečení"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "sériové"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "služba"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "shell"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "zobrazit méně"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "zobrazit více"
+
+# auto translated by TM merge from project: anaconda, version: master, DocId:
+# main
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "zmenšit"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "vyp"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "patice"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "řešení"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "podrobnosti řešení"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "Nahlašování problému se nezdařilo"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "v kolonce ssh klíč není vyplněné jeho umístění"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "není vyplněná kolonka ssh server"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "zastavit"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "při nezdaru zastavit start systému"
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+#, fuzzy
+#| msgid "Stopped"
+msgid "stopped"
+msgstr "Zastaveno"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "cíl"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "team"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "čas"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "časovač"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+# auto translated by TM merge from project: selinux (policycoreutils),
+# version: master, DocId: policycoreutils
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "jednotka"
+
+# auto translated by TM merge from project: Spacewalk Backend, version:
+# master, DocId: client/rhel/rhn-client-tools/po/rhn-client-tools
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "neznámý"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "neznámý cíl"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "zrušit maskování"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "odpojit (unmount)"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "nerozdělené místo na $0"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "uživatel"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "uživatelské jméno"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "s použitím popisu klíče $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "verze"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "svazek"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "varování"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "balíček wireguard-tools není nainstalovaný"
+
+# auto translated by TM merge from project: Pulseaudio, version: master,
+# DocId: pulseaudio.pot
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "ano"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "zóna"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Služba kdump není nainstalovaná. Zajistěte, aby byl nainstalován balíček "
+#~ "kexec-tools."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Není vyhrazena žádná paměť. K příkazovému řádku jádra připojte volbu "
+#~ "crashkernel (např. v /etc/default/grub) pro vyhrazení paměti při startu. "
+#~ "Příklad: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Služba je spuštěná"
+
+#~ msgid "Service is starting"
+#~ msgstr "Služba se spouští"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Služba je zastavená"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Služba se zastavuje"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Toto vyzkouší nastavení kdump vyvoláním havárie jádra."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "v parametrech zavádění jádra není nastaveno crashkernel"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (šifrované)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 Stratis fond"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 mezipaměť"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 neznámé úrovně (tier)"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 volné"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "Před odebráním tohoto disku je třeba přidat náhradní."
+
+#~ msgid "Backing device"
+#~ msgstr "Podkladové zařízení"
+
+#~ msgid "Block"
+#~ msgstr "Blok"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Kapacita"
+
+# auto translated by TM merge from project: comps, version: master, DocId:
+# po/comps
+#~ msgid "Content"
+#~ msgstr "Obsah"
+
+#~ msgid "Create devices"
+#~ msgstr "Vytvořit zařízení"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Zařízení"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Soubor představující zařízení"
+
+#~ msgid "Devices"
+#~ msgstr "Zařízení"
+
+#~ msgid "Drives"
+#~ msgstr "Jednotky"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr "Šifrované svazky je třeba před změnou velikosti odemknout."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Souborový systém (šifrované)"
+
+# auto translated by TM merge from project: comps, version: master, DocId:
+# comps
+#~ msgid "Filesystems"
+#~ msgstr "Souborové systémy"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#~ msgid "Free"
+#~ msgstr "Volno"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Uvolněte prostor v této skupině: zmenšete nebo smažte nějaké logické "
+#~ "svazky nebo přidejte fyzický svazek."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Neaktivní svazek"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Server s klíči"
+
+#~ msgid "LVM2 member"
+#~ msgstr "Člen LVM2"
+
+#~ msgid "Locked devices"
+#~ msgstr "Uzamčená zařízení"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Uzamčená šifrovaná data"
+
+# auto translated by TM merge from project: Cockpit, version: master, DocId:
+# cockpit
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Model"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS připojení"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "Podpora pro NFS není nainstalovaná"
+
+#~ msgid ""
+#~ "New logical volumes can not be created while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "V okamžiku, kdy skupině svazků chybí fyzické svazky, není možné vytvářet "
+#~ "nové logické."
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Nejsou nastavená žádná NFS připojení"
+
+#~ msgid "No devices"
+#~ msgstr "Žádná zařízení"
+
+#~ msgid "No drives attached"
+#~ msgstr "Nejsou připojené žádné jednotky"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Nejsou nastavené žádné iSCSI cíle"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Nedostatek prostoru pro nové souborové systémy"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Ostatní data"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Blokové zařízení rozdělené na oddíly"
+
+# auto translated by TM merge from project: anaconda, version: f22-branch,
+# DocId: anaconda
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Heslová fráze"
+
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "V okamžiku, kdy skupině svazků chybí fyzické svazky, není možné odebírat "
+#~ "další."
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#~ msgid "Pool"
+#~ msgstr "Fond"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Fond pro thin svazky"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID úroveň"
+
+#~ msgid "RAID member"
+#~ msgstr "Člen RAID"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Vyjímatelná jednotka"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Zobrazit $0 zařízení"
+#~ msgstr[1] "Zobrazit všechna $0 zařízení"
+#~ msgstr[2] "Zobrazit všech $0 zařízení"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Zobrazit $0 jednotku"
+#~ msgstr[1] "Zobrazit všechny $0 jednotky"
+#~ msgstr[2] "Zobrazit všech $0 jednotek"
+
+#~ msgid "Show all"
+#~ msgstr "Zobrazit vše"
+
+# auto translated by TM merge from project: dnf, version: master, DocId: dnf
+#~ msgid "Source"
+#~ msgstr "Zdroj"
+
+#~ msgid "Start pool"
+#~ msgstr "Spustit fond"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Pokud chcete vidět souborové systémy, spusťte fond."
+
+# auto translated by TM merge from project: selinux (policycoreutils),
+# version: master, DocId: policycoreutils
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Stav"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Stratis fond zastaven"
+
+# auto translated by TM merge from project: firewalld, version: master, DocId:
+# po/firewalld
+#~ msgid "Stratis member"
+#~ msgstr "Člen Stratis"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Stratis fond $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "Podpora je nainstalovaná."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "Aby bylo možné přidávat náhradní disky je třeba, aby RAID zařízení bylo "
+#~ "spuštěné."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "Poslední zbývající disk RAID zařízení není možné odebrat."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr ""
+#~ "Poslední zbývající fyzický svazek skupiny svazků není možné odebrat."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Pro odebrání tohoto fyzického svazku není k dispozici dostatek volného "
+#~ "místa. Je třeba alespoň $0 dalšího volného prostoru."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "Disk není možné vyjmout v průběhu zotavování zařízení."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr ""
+#~ "Než bude možné změnit jeho velikost je třeba, aby tento svazek byl "
+#~ "aktivován."
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Nerozpoznaná data"
+
+# auto translated by TM merge from project: audit-viewer, version: default,
+# DocId: audit-viewer
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Použití"
+
+#~ msgid "Used for"
+#~ msgstr "Použito pro"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "Podklad pro VDO"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO zařízení"
+
+# auto translated by TM merge from project: anaconda, version: master, DocId:
+# main
+#~ msgid "Volume"
+#~ msgstr "Svazek"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automaticky (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Přijmout klíč a připojit se"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Zde není možné upravovat velikost šifrovaných svazků."
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Použít Tang server s klíči"
+
+#~ msgid "Use a passphrase"
+#~ msgstr "Použít heslovou frázi"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitová mapa"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Tento fond zde není možné odemknout protože popis jeho klíče není v "
+#~ "očekávaném formátu."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Přepnout bitovou mapu"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Ověřte, že otisk klíče z Tang serveru odpovídá jednomu z následujících:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Zkontrolovat ručně s SSH: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Číslo a typ portu si neodpovídají"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Uzamčený šifrovaný Stratis fond"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Chyba při mazání výstrahy: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Nedaří se získat výstrahu: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 bajtů binárních dat]"
+
+# auto translated by TM merge from project: virt-manager, version: master,
+# DocId: virt-manager
+#~ msgid "Disk I/O spike"
+#~ msgstr "Špička diskového vst/výst."
+
+#~ msgid "Event"
+#~ msgstr "Událost"
+
+#~ msgid "Event logs"
+#~ msgstr "Záznamy událostí"
+
+#~ msgid "Load spike"
+#~ msgstr "Špička vytížení"
+
+#~ msgid "Memory spike"
+#~ msgstr "Špička využití paměti"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Špička vstupu/výstupu sítě"
+
+#~ msgid "ssh key"
+#~ msgstr "ssh klíč"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Když je tato volba zaškrtnuta, souborový systém nebude připojován v "
+#~ "průběhu příštího startu systému, i když předtím byl. Toto je užitečné "
+#~ "pokud není připojování při spouštění systému možné, jako například když "
+#~ "je zapotřebí heslo pro odemknutí souborového systému, ale start systému "
+#~ "probíhá bez přítomnosti člověka."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Nikdy nepřipojovat při startu systému"
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#~ msgid "Unit not found"
+#~ msgstr "Jednotka nenalezena"
+
+#~ msgid "Validating key"
+#~ msgstr "Ověřuje se klíč"
+
+#, fuzzy
+#~| msgid "Delete $0 volume"
+#~| msgid_plural "Delete $0 volumes"
+#~ msgid "Delete $0 group"
+#~ msgstr "Smazat $0 svazek"
+
+#~ msgid "Container administrator"
+#~ msgstr "Správce kontejneru"
+
+#~ msgid "Image builder"
+#~ msgstr "Tvorba obrazů"
+
+#~ msgid "Roles"
+#~ msgstr "Role"
+
+#~ msgid "Server administrator"
+#~ msgstr "Správce serveru"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Unixová skupina: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Úspěšně zkopírováno do klávesnice"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Diagnostická hlášení"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Výpis paměti jádra"
+
+#~ msgid "Software Updates"
+#~ msgstr "Aktualizace software"
+
+#~ msgid "Update log"
+#~ msgstr "Záznam událostí aktualizací"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "cíl výpisu nfs nemá podobu server:umisteni"
+
+#~ msgid "$0 Zone"
+#~ msgstr "Zóna $0"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Diagnostická hlášení"
+
+# auto translated by TM merge from project: appstream-glib, version: master,
+# DocId: appstream-glib
+#~ msgid "Done!"
+#~ msgstr "Hotovo!"
+
+#~ msgid "Download report"
+#~ msgstr "Stáhnout si výkaz"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Nebyl vytvořen žádný archiv."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Potvrďte smazání $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "Vytvořený archiv obsahuje data považovaná za citlivá a jeho obsah by měl "
+#~ "být předtím, než bude předán třetí straně zrevidován organizací, která ho "
+#~ "vytvořila."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Tento nástroj shromáždí informace o nastavení systému a diagnostice pro "
+#~ "použití při analýze problémů se systémem."
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Tento balíček není kompatibilní s touto verzí Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Tento balíček vyžaduje Cockpit verze %s a novější"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Metriky výkonu"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "pouze pro čtení"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB celkem"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgid "Apply"
+#~ msgstr "Použít"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Nemáte oprávnění pro odebrání zóny $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Pokud chcete heslo použít i tak, klikněte znovu na $0."
+
+#~ msgid "Access"
+#~ msgstr "Přístup"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "DISK SELHÁVÁ"
+
+#, fuzzy
+#~| msgid "Logical size"
+#~ msgid "Logical Size"
+#~ msgstr "Logická velikost"
+
+#~ msgid "Security updates "
+#~ msgstr "Aktualizace zabezpečení "
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#~ msgid "Updates "
+#~ msgstr "Aktualizace "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Volitelné)"
+
+#~ msgid "Active since"
+#~ msgstr "Aktivní od"
+
+#~ msgid "Affected locations"
+#~ msgstr "Umístění, kterých se týká"
+
+#~ msgid "Process"
+#~ msgstr "Proces"
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Remove and delete"
+#~ msgstr "Zastavit a smazat"
+
+#~ msgid "Remove and format"
+#~ msgstr "Odebrat a zformátovat"
+
+#, fuzzy
+#~| msgid "Remove and format"
+#~ msgid "Remove and grow"
+#~ msgstr "Odebrat a zformátovat"
+
+#, fuzzy
+#~| msgid "Remove and format"
+#~ msgid "Remove and initialize"
+#~ msgstr "Odebrat a zformátovat"
+
+#, fuzzy
+#~| msgid "Remove and format"
+#~ msgid "Remove and shrink"
+#~ msgstr "Odebrat a zformátovat"
+
+#, fuzzy
+#~| msgid "Remove device"
+#~ msgid "Remove and stop device"
+#~ msgstr "Odebrat zařízení"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Souborový systém je používán přihlašovacími sezeními a systémovými "
+#~ "službami. Pokračování je zastaví."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Souborový systém je používán přihlašovacími sezeními. Pokračování je "
+#~ "zastaví."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Souborový systém je používán systémovými službami. Pokračování je zastaví."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Na tomto zařízení se nacházejí souborové systémy, které jsou v tuto "
+#~ "chvíli používány. Pokračování je odpojí."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Toto zařízení je v tuto chvíli používáno ve skupinách svazků."
+
+#, fuzzy
+#~| msgid ""
+#~| "This device is currently used for volume groups. Proceeding will remove "
+#~| "it from its volume groups."
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Toto zařízení je v tuto chvíli používáno ve skupinách svazků. Pokračování "
+#~ "ho odebere z jeho skupin svazků."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Toto zařízení je v tuto chvíli používáno pro RAID zařízení."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Toto zařízení je v tuto chvíli používáno pro RAID zařízení. Pokračování "
+#~ "ho z nich odebere."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Toto zařízení je v tuto chvíli používáno ve skupinách svazků."
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Unmount and delete"
+#~ msgstr "Zastavit a smazat"
+
+#~ msgid "Unmount and format"
+#~ msgstr "Odpojit a zformátovat"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and grow"
+#~ msgstr "Odpojit nyní"
+
+#, fuzzy
+#~| msgid "Unmount and format"
+#~ msgid "Unmount and initialize"
+#~ msgstr "Odpojit a zformátovat"
+
+#, fuzzy
+#~| msgid "Unmount and format"
+#~ msgid "Unmount and shrink"
+#~ msgstr "Odpojit a zformátovat"
+
+#, fuzzy
+#~| msgid "On a mounted device"
+#~ msgid "Unmount and stop device"
+#~ msgstr "Na připojeném zařízení"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 z $1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "Blokované"
+
+#~ msgid "Blockdev of Stratis pool $0"
+#~ msgstr "Blokové zařízení Stratis fondu $0"
+
+#~ msgid "Blockdev of locked Stratis pool $0"
+#~ msgstr "Blokové zařízení zamčeného Stratis fondu $0"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Fyzický svazek od $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Člen RAID zařízení $0"
+
+#~ msgid "Menu"
+#~ msgstr "Nabídka"
+
+#~ msgid "VDO backing"
+#~ msgstr "Podklad pro VDO"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Vytvořit fond úložiště"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "Předvolby připojení"
+
+#, fuzzy
+#~| msgid "Reset Storage Pool"
+#~ msgid "Stratis Pool"
+#~ msgstr "Resetovat fond úložišť"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Je zapotřebí disk."
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#~ msgid "Disk"
+#~ msgstr "Disk"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Pouze pro staré aplikace. Snižuje výkon."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Nainstalovat podporu pro VDO"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Použít emulaci 512 bajtů"
+
+#~ msgid "System services"
+#~ msgstr "Systémové služby"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Vytvořit diagnostické hlášení "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr "Souběžné spojení s více než {{ limit }} stroji není podporováno."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "Sjednocené přihlašování (SSO), založené na Kerberos"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Přihlásit na {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Zadání nového hesla ke klíči se neshodují"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "Pokud chcete otisk ověřit, spusťte následující na {{host}} když jste "
+#~ "fyzicky u stroje nebo prostřednictvím důvěryhodné sítě:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Nepodařilo se spojit s {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Nepodařilo se přihlásit k {{#strong}}{{host}}{{/strong}}. Pro více "
+#~ "možností ověřování se a podporu řešení problémů přejděte na novější verzi "
+#~ "součásti cockpit-ws."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Nepodařilo se přihlásit k {{#strong}}{{host}}{{/strong}}. Aby bylo možné "
+#~ "se připojit k tomuto stroji, je třeba zapnout následující metody "
+#~ "ověřování v nastavení sshd na {{#strong}}{{host}}{{/strong}}:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "Připojujete se k {{host}} poprvé."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "klíč hostitele {{host}} se změnil"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Vyčistit nastavení přípojného bodu"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Vytvoření tohoto sdružení linek přeruší spojení se serverem a "
+#~ "znepřístupní tak rozhraní pro jeho správu."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Vytvoření tohoto mostu přeruší spojení se serverem a znepřístupní tak "
+#~ "rozhraní pro jeho správu."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Vytvoření tohoto týmu přeruší spojení se serverem a znepřístupní tak "
+#~ "rozhraní pro jeho správu."
+
+#~ msgid "IP settings"
+#~ msgstr "Nastavení pro IP protokol"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Vypnutí <b>$0</b> přeruší spojení se serverem a znepřístupní tak rozhraní "
+#~ "pro jeho správu."
+
+#~ msgid "Administrator password"
+#~ msgstr "Heslo správce"
+
+#~ msgid "Computer OU"
+#~ msgstr "Organizační jednotka počítače"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr ""
+#~ "Smazání RAID zařízení vymaže veškerá data, která se na něm nacházejí."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "Smazání skupiny svazků vymaže veškerá data v ní."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Nepřepisovat existující data"
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#~ msgid "Erase"
+#~ msgstr "Smazat"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Naformátovat disk $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr ""
+#~ "Formátování úložného zařízení vymaže všechna data, která na něm nyní jsou."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Název stroje není v doméně možné měnit"
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#~ msgid "More"
+#~ msgstr "Další"
+
+#~ msgid "Not joined"
+#~ msgstr "Nepřidáno do"
+
+#~ msgid "One time password"
+#~ msgstr "Jednorázové heslo"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 z $1 na $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Je třeba, aby hodina bylo číslo z rozmezí 0 až 23"
+
+#~ msgid "Last login:"
+#~ msgstr "Poslední přihlášení:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Je třeba, aby minuta bylo číslo z rozmezí 0-59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "Od minulého úspěšného přihlášení mezitím došlo k $0 neúspěšnému pokusu."
+#~ msgstr[1] ""
+#~ "Od minulého úspěšného přihlášení mezitím došlo k $0 neúspěšným pokusům."
+#~ msgstr[2] ""
+#~ "Od minulého úspěšného přihlášení mezitím došlo k $0 neúspěšným pokusům."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "např. „$0“"
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 aktivní zóny"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 výskyt"
+#~ msgstr[1] "$0 výskyty"
+#~ msgstr[2] "$0 výskytů"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Licencováno pod:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Odemknout při startu systému"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Odemknout pouze pro čtení"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Prohledat záznamy událostí pomocí kombinace pojmů:"
+
+#~ msgid "Show filters"
+#~ msgstr "Zobrazit filtry"
+
+#~ msgid "Text"
+#~ msgstr "Text"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "libovolně formulovaný řetězec jako regulární výraz"
+
+#~ msgid "e.g."
+#~ msgstr "např."
+
+#~ msgid "log fields"
+#~ msgstr "kolonky záznamu událostí"
+
+#~ msgid "qualifiers"
+#~ msgstr "kvalifikátory"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "Vyp/zap. nastavení relace"
+
+#~ msgid "(none)"
+#~ msgstr "(žádné)"
+
+#~ msgid "Create timers"
+#~ msgstr "Vytvořit časovače"
+
+#~ msgid "Recommended default"
+#~ msgstr "Doporučené výchozí"
+
+# auto translated by TM merge from project: virt-manager, version: master,
+# DocId: virt-manager
+#~ msgid "Run"
+#~ msgstr "Spustit"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select unit state"
+#~ msgstr "Typ konzole"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Zapnout uložené metriky"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Zapnout uložené metriky"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Vynutit odebrání heslové fráze v $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Pokud tang-show-keys není k dispozici, spusťte následující:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Co když příkaz tang-show-keys není k dispozici?"
+
+#~ msgid "key slot $0"
+#~ msgstr "slot pro klíč $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Něco se pokazilo"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "Toto nezafungovalo, zkuste to prosím znovu"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "Nemáte oprávnění pro získání přístupu na úrovni správce systému."
+
+#, fuzzy
+#~| msgid "Automatic updates"
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Automatické aktualizace"
+
+#, fuzzy
+#~| msgid "IP configuration"
+#~ msgid "$0 CPU configuration"
+#~ msgstr "Nastavení IP"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 síť"
+#~ msgstr[1] "$0 sítě"
+#~ msgstr[2] "$0 sítí"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 je k dispozici pro většinu operačních systémů. Pro instalaci "
+#~ "vyhledejte v GNOME Software nebo spusťte následující:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 úprava nastavení paměti"
+
+#~ msgid "$0 network"
+#~ msgstr "Síť $0"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 virt. procesor"
+#~ msgstr[1] "$0 virt. procesory"
+#~ msgstr[2] "$0 virt. procesorů"
+
+# auto translated by TM merge from project: libreport, version: master, DocId:
+# libreport
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 podrobnosti virt. procesoru"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "Nastavení virtuálního síťového rozhraní $0"
+
+#~ msgid "Add network interface"
+#~ msgstr "Přidat síťové rozhraní"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Přidat virtuální síťové rozhraní"
+
+#~ msgid "Additional"
+#~ msgstr "Další"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Adresa se nenachází v podsíti"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr ""
+#~ "Smazáním zachyceného stavu bude veškerý jím zachycovaný obsah ztracen."
+
+#~ msgid "Always attach"
+#~ msgstr "Vždy připojit"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Jeho připojení udělá tento disk sdílitelný pro každý virt. stroj, který "
+#~ "ho používá."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Spouštět libvirt automaticky při startu"
+
+#~ msgid "Autostart"
+#~ msgstr "Automatické spouštění"
+
+#~ msgid "Boot order"
+#~ msgstr "Pořadí zavádění"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Nastavení pořadí zavádění se nepodařilo uložit"
+
+#~ msgid "Bus"
+#~ msgstr "Sběrnice"
+
+#, fuzzy
+#~| msgid "VCPU settings could not be saved"
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "Nastavení virt. procesoru se nepodařilo uložit"
+
+#~ msgid "CPU type"
+#~ msgstr "Typ procesoru"
+
+#~ msgid "Change firmware"
+#~ msgstr "Změnit firmware"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Změny se projeví až po vypnutí virt. stroje"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Zvolte operační systém"
+
+#, fuzzy
+#~| msgid ""
+#~| "Clicking \"Launch Remote Viewer\" will download a .vv file and launch $0."
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "Kliknutím na „Spustit vzdálený prohlížeč“ se stáhne soubor ve formátu .vv "
+#~ "a spustí se $0."
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Obraz se nedaří smazat"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Potvrďte tuto akci"
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#~ msgid "Connect"
+#~ msgstr "Připojit"
+
+#, fuzzy
+#~| msgid "Connect with any $0 viewer application."
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "Připojte se libovolnou aplikací pro zobrazování $0."
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Připojuje se ke službě virtualizace"
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#~ msgid "Connection"
+#~ msgstr "Spojení"
+
+#~ msgid "Console"
+#~ msgstr "Konzole"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Jader na patici"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Nedaří se vrátit do podoby ze zachyceného stavu"
+
+# auto translated by TM merge from project: libvirt, version: master, DocId:
+# libvirt
+#~ msgid "Crashed"
+#~ msgstr "Zhavarovalo"
+
+#~ msgid "Create VM"
+#~ msgstr "Vytvořit virt. stroj"
+
+#, fuzzy
+#~| msgid "Create partition on $0"
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Vytvořit oddíl na $0"
+
+#~ msgid "Create new"
+#~ msgstr "Vytvořit nový"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Vytvořit nový virtuální stroj"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Vytvořit virtuální síť"
+
+#~ msgid "Creating VM"
+#~ msgstr "vytváří se virt. stroj"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "vytváří se instalace virt. stroje"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Vytvoření virt. stroje $0 se nezdařilo"
+
+#~ msgid "Creation time"
+#~ msgstr "Okamžik vytvoření"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Stávající přiřazení"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Uživatelsky určený firmware: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP rozsah"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Smazat související soubory úložiště:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Smazat fond úložiště $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Smazat svazek v tomto fondu"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Smazání neaktivního fondu úložiště pouze zruší jeho definici. Obsah fondu "
+#~ "samotný smazán nebude."
+
+# auto translated by TM merge from project: Fedora Websites, version:
+# arm.fedoraproject.org, DocId: po/arm.fedoraproject.org
+#, fuzzy
+#~| msgid "Desktop"
+#~ msgid "Desktop viewer"
+#~ msgstr "Desktop"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Před pokusem o smazání odpojte veškeré disky, které využívají tento fond, "
+#~ "z virt. strojů."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Odpojeno od sériové konzole. Klikněte na tlačítko Připojit."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Disk $0 se nepodařilo odpojit od virt. stroje $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Disk se nepodařilo připojit"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Disk se nepodařilo vytvořit"
+
+#, fuzzy
+#~| msgid "Device file"
+#~ msgid "Disk image file"
+#~ msgstr "Soubor představující zařízení"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Nastavení disku nebylo možné uložit"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "Zachycený stav pouze disku"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "(virt.) doména zhavarovala"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "(virt.) doména je blokována na (výpočetním) prostředku"
+
+#~ msgid "Download an OS"
+#~ msgstr "Stáhnout operační systém"
+
+#~ msgid "Dying"
+#~ msgstr "Vypíná se"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Emulovaný stroj"
+
+#~ msgid "End"
+#~ msgstr "Konec"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Konec je třeba vyplnit"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Existující obraz disku na souborovém systému hostitele"
+
+#~ msgid "Expand"
+#~ msgstr "Rozbalit"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Firmware se nepodařilo změnit"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "Nepodařilo se zjistit IP adresy rozhraní nacházející se v $0"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr ""
+#~ "Nepodařilo se odeslat kombinaci kláves Ctrl+Alt+$0 do virt. stroje $1"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr "Mělo by být zapnuto méně než maximální počet virtuálních procesorů."
+
+# auto translated by TM merge from project: system-config-firewall, version:
+# master, DocId: po/system-config-firewall
+#~ msgid "File"
+#~ msgstr "Soubor"
+
+#~ msgid "Filter by name"
+#~ msgstr "Filtrovat podle názvu"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgid "Firmware"
+#~ msgstr "Firmware"
+
+#~ msgid "Force shut down"
+#~ msgstr "Vynutit vypnutí"
+
+#~ msgid "Forward mode"
+#~ msgstr "Režim přeposílání"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Režim přeposílání"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Vytvořit automaticky"
+
+# auto translated by TM merge from project: virt-manager, version: master,
+# DocId: virt-manager
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Skrýt další možnosti"
+
+#~ msgid "Host device"
+#~ msgstr "Zařízení hostitele"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgid "Host name"
+#~ msgstr "Název počítače"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Hostitele je třeba vyplnit"
+
+# auto translated by TM merge from project: Fedora Websites, version:
+# getfedora.org, DocId: po/getfedora
+#~ msgid "Hypervisor details"
+#~ msgstr "Podrobnosti o hypervizoru"
+
+#~ msgid "IP configuration"
+#~ msgstr "Nastavení IP"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 a IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4 síť"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv4 síť je třeba vyplnit"
+
+#~ msgid "IPv4 only"
+#~ msgstr "Pouze IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6 adresa"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6 síť"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6 síť je třeba vyplnit"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Pouze IPv6"
+
+#~ msgid "Idle"
+#~ msgstr "Nečinné"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Okamžitě spustit virt. stroj"
+
+#~ msgid "Import"
+#~ msgstr "Importovat virt. stroj"
+
+#~ msgid "Import VM"
+#~ msgstr "Importovat virt. stroj"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "Importovat virtuální stroj"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "Ve většině uspořádání, macvtap nefunguje pro komunikaci hostitel-host."
+
+#~ msgid "Initiator"
+#~ msgstr "Iniciátor"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "IQN iniciátoru by mělo být vyplněné"
+
+# auto translated by TM merge from project: Fedora Installation Guide,
+# version: f22, DocId: pot/SourceSpoke
+#, fuzzy
+#~| msgid "Installation source"
+#~ msgid "Installation source"
+#~ msgstr "Zdroj instalace"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Zdroj pro instalaci je třeba vyplnit"
+
+#~ msgid "Installation type"
+#~ msgstr "Typ instalace"
+
+#~ msgid "Interface type"
+#~ msgstr "Typ rozhraní"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Neplatná IPv4 maska nebo délka předpony"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Neplatná IPv6 adresa"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Neplatná IPv6 předpona"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Neplatný název souboru"
+
+#, fuzzy
+#~| msgid "Launch Remote Viewer"
+#~ msgid "Launch remote viewer"
+#~ msgstr "Spustit vzdálený prohlížeč"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr "Pokud si nepřejete vytvořit účet root, nevyplňujte heslo"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr "Pokud si nepřejete vytvářet uživatelský účet, nevyplňujte heslo"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr "Pokud si nepřejete vytvořit účet root, nevyplňujte heslo"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr "Libvirt nenalezlo na hostiteli žádný obraz UEFI/OVMF firmware"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt nebo hypervizor nepodporuje UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "Načítání prostředků"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr ""
+#~ "Aby bylo možné změnit typ sběrnice je třeba, aby virt. stroj byl vypnutý"
+
+#, fuzzy
+#~| msgid "Machine must be shut off before changing bus type"
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr ""
+#~ "Aby bylo možné změnit typ sběrnice je třeba, aby virt. stroj byl vypnutý"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Správa virtuálních strojů"
+
+#~ msgid "Manual connection"
+#~ msgstr "Ruční připojení"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Maska nebo délka předpony"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Masku nebo délku předpony je třeba vyplnit"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Přiřadit nanejvýš"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "Paměťové maximum se nepodařilo uložit"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Maximální počet virtuálních procesorů, přiřazených operačnímu systému "
+#~ "hosta"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Maximální počet virtuálních procesorů, přiřazených operačnímu systému "
+#~ "hosta. Je třeba, aby bylo z rozmezí 1 až $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Přenosová jednotka nejvýše"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Paměť se nepodařilo uložit"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "Je třeba, aby paměť nebyla 0 (nula)"
+
+# auto translated by TM merge from project: libbytesize, version: master,
+# DocId: libbytesize
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Typ modelu"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT na $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "Nepodařilo se změnit stav síť. rozhraní $0 virt. stroje $1"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Název obsahuje neplatné znaky"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Název je třeba vyplnit"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Název je třeba vyplnit"
+
+#~ msgid "Name: "
+#~ msgstr "Název: "
+
+#~ msgid "Netmask"
+#~ msgstr "Maska sítě"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Síť $0 se nepodařilo aktivovat"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Síť $0 se nepodařilo deaktivovat"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Zavádění ze sítě (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Síťový souborový systém"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Nastavení síťového rozhraní se nepodařilo uložit"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "Síťový výběr nepodporuje PXE."
+
+#~ msgid "Networks"
+#~ msgstr "Sítě"
+
+#~ msgid "New volume name"
+#~ msgstr "Název pro nový svazek"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "Na tomto stroji nejsou spuštěné nebo definované žádné virt. stroje"
+
+#, fuzzy
+#~| msgid "No description available"
+#~ msgid "No connection available"
+#~ msgstr "Není k dispozici žádný popis"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Pro tento virt. stroj nejsou definované žádné disky"
+
+#~ msgid "No network devices"
+#~ msgstr "Žádná síťová zařízení"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Pro tento virt. stroj nebyla určena žádná síťová rozhraní"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "Na tomto hostiteli není definována žádná síť"
+
+#~ msgid "No networks available"
+#~ msgstr "Žádné sítě k dispozici"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#~ msgid "No parent"
+#~ msgstr "Nic nenadřazeno"
+
+#, fuzzy
+#~| msgid "No disks defined for this VM"
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Pro tento virt. stroj nejsou definované žádné disky"
+
+#~ msgid "No state"
+#~ msgstr "Žádný stav"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "Na tomto stroji není definován žádný fond úložiště"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Nejsou k dispozici žádné fondy úložiště"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "Pro tento fond úložiště nejsou definované žádné úložné svazky"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Žádné virtuální sítě"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr "Netrvalou síť není možné smazat. Přestane existovat po vypnutí."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "Netrvalý fond úložiště není možné smazat. Přestane existovat po vypnutí."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Žádné (izolovaná síť)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Jeden či více z vybraných svazků je používán doménami. Aby bylo možné "
+#~ "svazek smazat, je třeba nejprve odpojit disky."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Upravit je možné pouze pokud je host vypnutý"
+
+#~ msgid "Open"
+#~ msgstr "Otevřít"
+
+#~ msgid "Operating system"
+#~ msgstr "Operační systém"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "Operace probíhá"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "Nadřazený zachycený stav"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Umístění v souborovém systému hostitele"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Popis umístění ISO souboru na souborovém systému hostitele"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Popis umístění ISO souboru na souborovém systému hostitele"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Popis umístění ISO souboru na souborovém systému hostitele"
+
+#~ msgid "Paused"
+#~ msgstr "Pozastaveno"
+
+#~ msgid "Persistence"
+#~ msgstr "Trvalost"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Fyzické diskové zařízení"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Fyzické diskové zařízení na hostiteli"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Zvolte fond úložiště"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "Zvolte svazek"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Zadejte název pro nový svazek"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Pro přístup k jeho konzoli je třeba virtuální stroj napřed zapnout."
+
+#~ msgid "Plug"
+#~ msgstr "Připojit"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "Typ fondu nepodporuje vytvoření svazku"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "Svazky fondu jsou využívány virt. stroji "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Upřednostňovaný počet patic který odhalit hostovi."
+
+#~ msgid "Prefix"
+#~ msgstr "Předpona"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "Délku předpony je třeba vyplnit"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "Dříve pořízené zachycené stavy vám umožňují se vrátit k dřívějšímu stavu "
+#~ "pokud se něco pokazí"
+
+#~ msgid "Product"
+#~ msgstr "Produkt"
+
+#~ msgid "Profile"
+#~ msgstr "Profil"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgid "Protocol"
+#~ msgstr "Protokol"
+
+#~ msgid "Remote URL"
+#~ msgstr "Vzdálená URL adresa"
+
+# auto translated by TM merge from project: Fedora Websites, version:
+# getfedora.org, DocId: po/getfedora
+#, fuzzy
+#~| msgid "Hypervisor details"
+#~ msgid "Remote viewer details"
+#~ msgstr "Podrobnosti o hypervizoru"
+
+#~ msgid "Revert"
+#~ msgstr "Vrátit zpět"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Vrátit zpět do podoby v zachyceném stavu $0"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "Vrácení zpět do podoby v tomto zachyceném stavu vezme virt. stroj zpět do "
+#~ "okamžiku pořízení zachyceného stavu a stávající stav bude ztracen, včetně "
+#~ "všech dat, která se nenacházejí v zachyceném stavu"
+
+#~ msgid "Root password"
+#~ msgstr "Heslo pro účet root"
+
+#~ msgid "Route to $0"
+#~ msgstr "Trasa na $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Spustit bezobslužnou instalaci"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Spustit při startu stroje"
+
+#, fuzzy
+#~| msgid "SPICE TLS port:"
+#~ msgid "SPICE TLS port"
+#~ msgstr "TLS port SPICE:"
+
+#, fuzzy
+#~| msgid "SPICE address:"
+#~ msgid "SPICE address"
+#~ msgstr "Adresa SPICE:"
+
+#, fuzzy
+#~| msgid "SPICE port:"
+#~ msgid "SPICE port"
+#~ msgstr "Port SPICE:"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select console type"
+#~ msgstr "Typ konzole"
+
+#~ msgid "Send key"
+#~ msgstr "Poslat stisk klávesy"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Poslat nemaskovatelné přerušení"
+
+#~ msgid "Serial console"
+#~ msgstr "Sériová konzole"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Nastavit DHCP rozsah"
+
+#~ msgid "Set manually"
+#~ msgstr "Nastavit ručně"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "Nastavení hesel pro uživatele pro bezobslužnou instalaci vyžaduje "
+#~ "spuštění virt. stroje při jeho vytváření"
+
+#~ msgid "Show additional options"
+#~ msgstr "Zobrazit další možnosti"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#~ msgid "Shut off"
+#~ msgstr "Vypnout"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr ""
+#~ "Aby bylo možné upravovat nastavení firmware, je třeba virt. stroj nejprve "
+#~ "vypnout"
+
+#~ msgid "Shutting down"
+#~ msgstr "Vypínání"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Zachycený stav se nepodařilo vytvořit"
+
+#~ msgid "Source format"
+#~ msgstr "Zdrojový formát"
+
+#~ msgid "Source path"
+#~ msgstr "Popis umístění zdroje"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Popis umístění zdroje je třeba vyplnit"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "Zdroj by měl začínat na http, ftp nebo nfs protokol"
+
+#~ msgid "Source volume group"
+#~ msgstr "zdrojová skupina svazků"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Spustit libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Spustit fond při startu hostitele"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Začátek je třeba vyplnit"
+
+#~ msgid "Startup"
+#~ msgstr "Při spuštění"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "Fond úložiště $0 se nepodařilo aktivovat"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "Fond úložiště $0 se nepodařilo deaktivovat"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Fond úložiště se nepodařilo uložit"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Název fondu úložiště"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "Velikost úložiště nemůže být nula"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Svazky úložiště není možné smazat"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "uspáno (správou napájení)"
+
+#~ msgid "Target path"
+#~ msgstr "Popis umístění cíle"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Popis umístění cíle je třeba vyplnit"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr "Virt. stroj je spuštěný a před smazáním bude vynuceně vypnut."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "Pro odpojení tohoto zařízení je třeba, aby virt. stroj byl spuštěný nebo "
+#~ "vypnutý"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "Složka na serveru, kterou exportovat"
+
+#~ msgid "The pool is empty"
+#~ msgstr "Fond je prázdný"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr "Vybraný operační systém nepodporuje bezobslužnou instalaci"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr "Zvolený operační systém vyžaduje přinejmenším $0 $1 operační paměti"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "Vybraný operační systém vyžaduje velikost úložiště přinejmenším $0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "Fond úložiště se nepodařilo smazat"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr ""
+#~ "Tento vir. stroj je přechodný. Pokud ho chcete smazat, stačí ho vypnout."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Tento svazek je zrovna používán: "
+
+#~ msgid "Threads per core"
+#~ msgstr "Vláken na jádro"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr "U přechodných virt. strojů nelze upravovat nastavení firmware"
+
+#~ msgid "Type ID"
+#~ msgstr "Identifikátor typu"
+
+#~ msgid "Unique name"
+#~ msgstr "Neopakující se název"
+
+#~ msgid "Unique network name"
+#~ msgstr "Doposud nepoužívaný název sítě"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Neznámý firmware"
+
+#~ msgid "Unplug"
+#~ msgstr "Odpojit"
+
+#~ msgid "Url"
+#~ msgstr "URL adresa"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "Nastavení virt. procesoru se nepodařilo uložit"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "Virt. stroj $0 už existuje"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "Nepodařilo se vynutit restart virt. stroje $0"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "Nepodařilo se vynutit vypnutí virt. stroje $0"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "Virt. stroj $0 se nezdařilo smazat"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "Virt. stroj $0 se nezdařilo nainstalovat"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "Virt. stroj $0 se nepodařilo restartovat"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "Nezdařilo se navázat v chodu virt. stroje $0"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "Nezdařilo se zaslat virt. stroji $0 nemaskovatelné přerušení"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "Virt. stroj $0 se nepodařilo vypnout"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "Virt. stroj $0 se nepodařilo zapnout"
+
+# auto translated by TM merge from project: selinux (policycoreutils),
+# version: master, DocId: policycoreutils
+#~ msgid "VM state"
+#~ msgstr "Stav virt. stroje"
+
+#, fuzzy
+#~| msgid "VNC TLS port:"
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC TLS port:"
+
+#, fuzzy
+#~| msgid "VNC address:"
+#~ msgid "VNC address"
+#~ msgstr "VNC adresa:"
+
+#, fuzzy
+#~| msgid "console"
+#~ msgid "VNC console"
+#~ msgstr "konzole"
+
+#, fuzzy
+#~| msgid "VNC port:"
+#~ msgid "VNC port"
+#~ msgstr "VNC port:"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Virtuální stroje"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Virtuální stroje"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Správa virtuálních strojů"
+
+#~ msgid "Virtual network"
+#~ msgstr "Virtuální síť"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Vytvoření virtuální sítě se nezdařilo"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Služba virtualizace (libvirt) není aktivní"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Název skupiny svazků je třeba vyplnit"
+
+#~ msgid "WWPN"
+#~ msgstr "Neopakující se číslo portu"
+
+#~ msgid "Writeable"
+#~ msgstr "Zapisovatelné"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Zapisovatelné a sdílené"
+
+#~ msgid "Yesterday"
+#~ msgstr "Včera"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "Je třeba vybrat co nejvíce odpovídající operační systém"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#~ msgid "disabled"
+#~ msgstr "zakázáno"
+
+#~ msgid "down"
+#~ msgstr "vypnuté"
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#~ msgid "enabled"
+#~ msgstr "povoleno"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "zařízení hostitele"
+
+#, fuzzy
+#~| msgid "to host path"
+#~ msgid "host passthrough"
+#~ msgstr "do umístění stroje"
+
+#~ msgid "hostdev"
+#~ msgstr "zařízení hostitele"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "Přímý cíl iSCSI"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "IQN iSCSI iniciátoru"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "IQN název iSCSI cíle"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "vícesměrvysílání"
+
+#~ msgid "no"
+#~ msgstr "ne"
+
+#~ msgid "no state saved"
+#~ msgstr "neuložen žádný stav"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "přesměrované zařízení"
+
+#~ msgid "server"
+#~ msgstr "server"
+
+#~ msgid "up"
+#~ msgstr "zapnuto"
+
+#~ msgid "vCPU count"
+#~ msgstr "Počet virt. procesorů"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "Maximum virt. procesorů"
+
+#~ msgid "vCPUs"
+#~ msgstr "virt. procesorů"
+
+#~ msgid "vhostuser"
+#~ msgstr "uzivatelvirtstroje"
+
+#~ msgid "view more..."
+#~ msgstr "zobrazit více…"
+
+#, fuzzy
+#~| msgid ""
+#~| "virt-install package needs to be installed on the system in order to "
+#~| "create new VMs"
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Aby bylo možné vytvářet nové virt. stroje je třeba, aby byl nainstalovaný "
+#~ "balíček virt-install"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Aby bylo možné vytvářet nové virt. stroje je třeba, aby byl nainstalovaný "
+#~ "balíček virt-install"
+
+#, fuzzy
+#~| msgid ""
+#~| "virt-install package needs to be installed on the system in order to "
+#~| "create new VMs"
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Aby bylo možné vytvářet nové virt. stroje je třeba, aby byl nainstalovaný "
+#~ "balíček virt-install"
+
+#~ msgid "vm"
+#~ msgstr "virt. stroj"
+
+#~ msgid "Local install media"
+#~ msgstr "Místní instalační médium"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "Nastavte heslo pro účet správce (root)"
+
+#~ msgid "21st"
+#~ msgstr "21."
+
+#~ msgid "22nd"
+#~ msgstr "22."
+
+#~ msgid "23rd"
+#~ msgstr "23."
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Po"
+
+# auto translated by TM merge from project: docbook-locales, version: master,
+# DocId: locale
+#~ msgid "Friday"
+#~ msgstr "pátek"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Hodina:minuta"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "Je třeba, aby hodina bylo číslo z rozmezí 0 až 23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Neplatný formát data."
+
+#~ msgid "Invalid number."
+#~ msgstr "Neplatné číslo."
+
+# auto translated by TM merge from project: docbook-locales, version: master,
+# DocId: locale
+#~ msgid "Monday"
+#~ msgstr "pondělí"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Opakovat každou hodinu"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Opakovat každý rok"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#~ msgid "Saturday"
+#~ msgstr "sobota"
+
+# auto translated by TM merge from project: docbook-locales, version: master,
+# DocId: locale
+#~ msgid "Sunday"
+#~ msgstr "neděle"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Tento den neexistuje ve všech měsících.<br> Časovač bude vykonán pouze v "
+#~ "měsících, které mají 31. den."
+
+# auto translated by TM merge from project: docbook-locales, version: master,
+# DocId: locale
+#~ msgid "Thursday"
+#~ msgstr "čtvrtek"
+
+# auto translated by TM merge from project: docbook-locales, version: master,
+# DocId: locale
+#~ msgid "Tuesday"
+#~ msgstr "úterý"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 aktualizace"
+#~ msgstr[1] "$0 aktualizace"
+#~ msgstr[2] "$0 aktualizací"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 oprava zabezpečení"
+#~ msgstr[1] "$1 opravy zabezpečení"
+#~ msgstr[2] "$1 oprav zabezpečení"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Správa zařízení úložiště"
+
+#~ msgid "Restart now"
+#~ msgstr "Restartovat nyní"
+
+# auto translated by TM merge from project: system-config-printer, version:
+# master, DocId: system-config-printer
+#~ msgid "Configure"
+#~ msgstr "Nastavit"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr ""
+#~ "Zavedení ze sítě je k dispozici pouze při použití systémových přípojení"
+
+# auto translated by TM merge from project: Fedora Release Notes, version:
+# f24, DocId: pot/Networking
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Síť"
+
+#~ msgid "No snapshots"
+#~ msgstr "Nejsou zachycené stavy"
+
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "Ve složce „$0“ nenalezen žádný takový soubor"
+
+#~ msgid "No updates pending"
+#~ msgstr "Žádné čekající aktualizace"
+
+#~ msgid "This directory is empty"
+#~ msgstr "Tato složka je prázdná"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr "Tento typ fondu nepodporuje vytvoření svazku úložiště"
+
+#~ msgid "undefined"
+#~ msgstr "nedefinované"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Nesprávný klíč stroje"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "Nepodvrženost totožnosti stroje {{#strong}}{{host}}{{/strong}} se nedaří "
+#~ "ověřit. Opravdu chcete pokračovat v připojování?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "Klíč {{#strong}}{{host}}{{/strong}} neodpovídá dříve používanému klíči. "
+#~ "Pokud tento stroj nebyl nedávno nahrazen, je pravděpodobné, že se někdo "
+#~ "pokouší zaútočit na vaše spojení s tímto strojem."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Neznámý klíč stroje"
+
+# auto translated by TM merge from project: virt-manager, version: master,
+# DocId: virt-manager
+#~ msgid "Address:"
+#~ msgstr "Adresa:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Je vyžadováno alespoň $0 disků."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "Připojte se libovolnou aplikací pro zobrazování SPICE nebo VNC."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Stáhnout MSI z $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Grafická konzole (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Grafická konzole v desktopovém prohlížeči"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Pro tento virtuální stroj není definována žádná konzole."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+# auto translated by TM merge from project: anaconda, version: f25, DocId:
+# main
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid ""
+#~ "For the host, either specify the hostname, IP address, an alias name or a "
+#~ "unique resource identifier for the SSH destination."
+#~ msgstr ""
+#~ "V případě hostitele, zadejte pro SSH cíl buď jeho (alternativní) název, "
+#~ "IP adresu nebo neopakující se identifikátor prostředku."
+
+#~ msgid ""
+#~ "Specify the host and the login user account for the host that you want to "
+#~ "add."
+#~ msgstr ""
+#~ "Zadejte název hostitele a účet pro přihlášení se pro hostitele, kterého "
+#~ "chcete přidat."
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgid "Entry"
+#~ msgstr "Položka"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Váš prohlížeč se odpojí, ale to neovlivní proces aktualizace. Můžete se "
+#~ "kdykoli znovu připojit a podívat se na postup."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "a automaticky restartovat stroj."
+
+#~ msgid "Dashboard"
+#~ msgstr "Přehled"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgid "Description input text"
+#~ msgstr "Vstupní text popisu"
+
+# auto translated by TM merge from project: appstream-glib, version: master,
+# DocId: appstream-glib
+#~ msgid "FAILED"
+#~ msgstr "NEZDAŘILO SE"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Spojení ztraceno. Pokus o opětovné připojení"
+
+#~ msgid "Name input text"
+#~ msgstr "Vstupní text názvu"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Síť"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Zadejte velikost nového svazku"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgid "Servers"
+#~ msgstr "Servery"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "V tuto chvíli jste připojení přímo na tento server. Nemůžete ho proto "
+#~ "smazat."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bity"
+#~ msgstr[2] "$0 bitů"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 bajt"
+#~ msgstr[1] "$0 bajty"
+#~ msgstr[2] "$0 bajtů"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 bajt"
+#~ msgstr[1] "$0 bajty"
+#~ msgstr[2] "$0 bajtů"
+
+#~ msgid "Bond settings "
+#~ msgstr "Nastavení spřažení "
+
+#~ msgid "IP settings "
+#~ msgstr "Nastavení IP "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "–"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "V Cockpit nastala neočekávaná vnitřní chyba. <br/><br/>Můžete zkusit "
+#~ "Cockpit restartovat kliknutím na opětovné načtení stránky ve svém "
+#~ "prohlížeči. Podrobnější informace o této chybě jsou vypsány v javascript "
+#~ "konzoli (<b>Ctrl-Shift-J</b> ve většině prohlížečů)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "Virt. stroj zhavaroval."
+
+#~ msgid "The VM is down."
+#~ msgstr "Virt. stroj je vypnutý."
+
+#~ msgid "The VM is going down."
+#~ msgstr "Virt. stroj se vypíná."
+
+#~ msgid "The VM is idle."
+#~ msgstr "Virt. stroj je nečinný."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr "Virt. stroj se poroučí (vypnutí nebo nedokončený pád)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "Virt. stroj je pozastavený."
+
+#~ msgid "The VM is running."
+#~ msgstr "Virt. stroj je spuštěný."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "Virt. stroj je uspaný svou vlastní správou napájení."
+
+#~ msgid "inactive"
+#~ msgstr "neaktivní"
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Nevyplňujte pro připojení se k tomuto stroji jako právě přihlášený "
+#~ "uživatel. Pokud zadáte jiné uživatelské jméno, takový uživatel bude vždy "
+#~ "použit při připojování k tomuto stroji."
+
+#~ msgid "2 min"
+#~ msgstr "2 min"
+
+#~ msgid "3 min"
+#~ msgstr "3 min"
+
+#~ msgid "4 min"
+#~ msgstr "4 min"
+
+#~ msgid "CPU graph"
+#~ msgstr "Graf procesoru"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "Stav procesoru"
+
+#~ msgid "Cached"
+#~ msgstr "Uloženo v mezipaměti"
+
+#~ msgid "Graphs"
+#~ msgstr "Grafy"
+
+#~ msgid "I/O wait"
+#~ msgstr "Čekání na vst./výst."
+
+# auto translated by TM merge from project: Fedora Release Notes, version:
+# f23, DocId: pot/Kernel
+#~ msgid "Kernel"
+#~ msgstr "Jádro"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Paměť"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "Využití operační paměti a odkládacího prostoru (swap)"
+
+#~ msgid "Memory graph"
+#~ msgstr "Graf paměti"
+
+#~ msgid "Network traffic"
+#~ msgstr "Síťový provoz"
+
+#~ msgid "Nice"
+#~ msgstr "Nice"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Synchronizovat uživatele"
+
+#~ msgid "Usage graphs"
+#~ msgstr "Grafy využití"
+
+#~ msgid "View graphs"
+#~ msgstr "Zobrazit grafy"
+
+# auto translated by TM merge from project: libvirt, version: master, DocId:
+# libvirt
+#~ msgid "idle"
+#~ msgstr "nečinný"
+
+#~ msgid "paused"
+#~ msgstr "pozastaveno"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#~ msgid "running"
+#~ msgstr "spuštěné"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.4, DocId:
+# cockpit
+#~ msgid "shut off"
+#~ msgstr "vypnuto"
+
+#~ msgid "shutdown"
+#~ msgstr "vypnout"
+
+#~ msgid "Add a new host"
+#~ msgstr "Přidat nového hostitele"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgid "Available"
+#~ msgstr "Dostupný"
+
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Zadejte IP adresu nebo název stroje"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Síťová rozhraní"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Tato verze cockpit-ws nepodporuje připojování ke stroji pomocí "
+#~ "alternativního uživatele nebo portu"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Pro vyzkoušení jiného portu bude třeba přejít na novější verzi cockpit-ws."
+
+#~ msgid "$0 template"
+#~ msgstr "$0 Šablona"
+
+#~ msgid "Instance of template: "
+#~ msgstr "Instance šablony: "
+
+#~ msgid "Instantiate"
+#~ msgstr "Vytvořit instanci a spustit"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 sdílení"
+
+#~ msgid "All In One"
+#~ msgstr "Vše v jednom"
+
+# auto translated by TM merge from project: ibus, version: head, DocId: ibus10
+#~ msgid "Always"
+#~ msgstr "Vždy"
+
+# auto translated by TM merge from project: docbook-locales, version: master,
+# DocId: locale
+#~ msgid "Author"
+#~ msgstr "Autor"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Data předaná mechanizmu passwd1 jsou chybná"
+
+#~ msgid "CPU priority"
+#~ msgstr "Priorita procesoru"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Nedaří se spojit s Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Změnit limity prostředku"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Změnit limity prostředků"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit se nepodařilo přihlásit k {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit se nepodařilo přihlásit k {{#strong}}{{host}}{{/strong}}. Své "
+#~ "přihlašovací údaje můžete změnit níže. {{#can_sync}}Namísto toho můžete "
+#~ "chtít {{#sync_link}}synchronizovat účty a hesla{{/sync_link}}.{{/"
+#~ "can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Kombinované využití paměti"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Kombinované využití $0 jádra procesoru"
+#~ msgstr[1] "Kombinované využití $0 jader procesoru"
+#~ msgstr[2] "Kombinované využití $0 jader procesoru"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "Příkaz je třeba vyplnit"
+
+#~ msgid "Command:"
+#~ msgstr "Příkaz:"
+
+#~ msgid "Commit"
+#~ msgstr "Odeslat"
+
+#~ msgid "Commit image"
+#~ msgstr "Odeslat obraz"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Připojování k Docker"
+
+#~ msgid "Container name"
+#~ msgstr "Název kontejneru"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Kontejner je nyní označen jako nespuštěný, ale běžné zastavení se "
+#~ "nezdařilo."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Kontejner je v tuto chvíli spuštěný."
+
+#~ msgid "Container:"
+#~ msgstr "Kontejner:"
+
+# auto translated by TM merge from project: Fedora Release Notes, version:
+# f24, DocId: pot/Containers
+#~ msgid "Containers"
+#~ msgstr "Kontejnery"
+
+# auto translated by TM merge from project: Fedora Release Notes, version:
+# f24, DocId: pot/Containers
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Kontejnery"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Nedaří se změnit skupiny uživatele"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Nedaří se změnit heslo uživatele"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Nedaří se připojit ke stroji"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Nedaří se vytvořit nové uživatele"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Nedaří se vypsat místní uživatele"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Nedaří se vypsat uživatele"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Nedaří se načíst data uživatele"
+
+#~ msgid "Created:"
+#~ msgstr "Vytvořeno:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Docker kontejnery"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker na tomto systému není nainstalovaný nebo aktivovaný"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Duplikovat alternativní názvy"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Duplikovat port"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "Zadejte heslovou frázi pro přidání identity do ssh-agent"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Zadání jiného hesla zde znamená, že ho bude třeba zadávat pokaždé znovu "
+#~ "při připojování k tomuto stroji"
+
+#~ msgid "Environment"
+#~ msgstr "Prostředí"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Chyba při načítání uživatelů: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Chybové hlášení z Docker:"
+
+# auto translated by TM merge from project: Fedora Websites, version:
+# alt.fedoraproject.org, DocId: po/alt.fedoraproject.org
+#~ msgid "Everything"
+#~ msgstr "Vše"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Skončilo s $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Odhalit porty kontejneru"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Nepodařilo se spustit Docker: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Nepodařilo se zastavit Docker rozsah: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Získat nový obraz"
+
+# auto translated by TM merge from project: Spacewalk Backend, version:
+# master, DocId: client/rhel/rhn-client-tools/po/rhn-client-tools
+#~ msgid "IP address:"
+#~ msgstr "IP adresa:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "Délka předpony IP adresy:"
+
+# auto translated by TM merge from project: libvirt, version: master, DocId:
+# libvirt
+#~ msgid "Id"
+#~ msgstr "Identif."
+
+# auto translated by TM merge from project: libvirt, version: master, DocId:
+# libvirt
+#~ msgid "Id:"
+#~ msgstr "Identif.:"
+
+#~ msgid "Image"
+#~ msgstr "Obraz"
+
+#~ msgid "Image $0"
+#~ msgstr "Obraz $0"
+
+#~ msgid "Image search"
+#~ msgstr "Hledání obrazu"
+
+#~ msgid "Image:"
+#~ msgstr "Obraz:"
+
+#~ msgid "Images"
+#~ msgstr "Obrazy"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Obrazy"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Obrazy a spuštěné kontejnery"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Pro synchronizaci uživatelů je třeba se přihlásit k {{#strong}}{{host}}{{/"
+#~ "strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Neplatný port"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Kerberos tiket"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Nevyplňujte pro připojení k tomuto stroji jako právě přihlášený "
+#~ "uživatel{{#default_user}} ({{default_user}}){{/default_user}}. Pokud "
+#~ "zadáte jiné uživatelské jméno, tento uživatel bude vždy použit při "
+#~ "připojování k tomuto stroji."
+
+#~ msgid "Link to another container"
+#~ msgstr "Propojení s jiným kontejnerem"
+
+#~ msgid "Links:"
+#~ msgstr "Linky:"
+
+#~ msgid "Login Password"
+#~ msgstr "Přihlašovací heslo"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC adresa:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Limit paměti"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Připojit svazky kontejneru"
+
+#~ msgid "No container specified"
+#~ msgstr "Není určený žádný kontejner"
+
+#~ msgid "No containers"
+#~ msgstr "Žádné kontejnery"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Žádné kontejnery, které by odpovídaly stávajícímu filtru"
+
+#~ msgid "No images"
+#~ msgstr "Žádné obrazy"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Stávajícímu filtru neodpovídají žádné obrazy"
+
+#~ msgid "No results for $0"
+#~ msgstr "Žádné výsledky pro $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Stávajícímu filtru neodpovídají žádné obrazy"
+
+#~ msgid "No running containers"
+#~ msgstr "Žádné spuštěné kontejnery"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Žádné spuštěné kontejnery, které by odpovídaly stávajícímu filtru"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Neoprávněni k přístupu k Dockeru na tomto systému"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "V případě nezdaru, zkusit znovu $0 krát"
+#~ msgstr[1] "V případě nezdaru, zkusit znovu $0 krát"
+#~ msgstr[2] "V případě nezdaru, zkusit znovu $0 krát"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Potvrďte vynucené smazání $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Zkuste jiný pojem"
+
+#~ msgid "Ports:"
+#~ msgstr "Porty:"
+
+#~ msgid "Problems"
+#~ msgstr "Problémy"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Pouze pro čtení"
+
+#~ msgid "ReadWrite"
+#~ msgstr "Čtení i zápis"
+
+# auto translated by TM merge from project: dnf, version: master, DocId:
+# po/dnf
+#~ msgid "Repository"
+#~ msgstr "Repozitář"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Zásada restartu:"
+
+#~ msgid "Retries:"
+#~ msgstr "Opětovných pokusů:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Použít mé heslo i pro připojování se k ostatním strojům"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Vyberte uživatele které chcete synchronizovat s {{#strong}}{{host}}{{/"
+#~ "strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Nastavit proměnné prostředí pro kontejner"
+
+#~ msgid "Severity:"
+#~ msgstr "Závažnost:"
+
+#~ msgid "Show all containers"
+#~ msgstr "Zobrazit všechny kontejnery"
+
+#~ msgid "Start Docker"
+#~ msgstr "Spustit Docker"
+
+# auto translated by TM merge from project: libvirt, version: master, DocId:
+# libvirt
+#~ msgid "State:"
+#~ msgstr "Stav:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Synchronizovat"
+
+#~ msgid "Tag"
+#~ msgstr "Štítek"
+
+# auto translated by TM merge from project: ibus, version: head, DocId: ibus10
+#~ msgid "Tags"
+#~ msgstr "Šítky"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "Následující kontejnery závisí na tomto obrazu a přestanou být použitelné."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Tento obraz neexistuje."
+
+#~ msgid "Type a password"
+#~ msgstr "Zadejte heslo"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Filtrujte psaním…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "Pokud nezastaveno"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Nepodporovaný mechanizmus nastavení"
+
+#~ msgid "Up since $0"
+#~ msgstr "Zapnuto od $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Využito kontejnery"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Pomocí přihlašovacích údajů, které jsou k dispozici"
+
+#~ msgid "Volumes"
+#~ msgstr "Svazky"
+
+#~ msgid "Volumes:"
+#~ msgstr "Svazky:"
+
+#~ msgid "With terminal"
+#~ msgstr "S terminálem"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Jste sice připojení k {{#strong}}{{host}}{{/strong}}, nicméně pro "
+#~ "synchronizaci uživatelských účtů je zapotřebí uživatele s oprávněním pro "
+#~ "správu systému."
+
+# auto translated by TM merge from project: IBus-Chewing, version: master,
+# DocId: ibus-chewing
+#~ msgid "default"
+#~ msgstr "výchozí"
+
+#~ msgid "key"
+#~ msgstr "klíč"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "hledat podle názvu, jmenného prostoru nebo popisu"
+
+#~ msgid "shares"
+#~ msgstr "sdílení"
+
+#~ msgid "to host port"
+#~ msgstr "na port stroje"
+
+#~ msgid "value"
+#~ msgstr "hodnota"
+
+#~ msgid "Master"
+#~ msgstr "Hlavní"
+
+# auto translated by TM merge from project: FreeIPA, version: ipa-4-5, DocId:
+# po/ipa
+#~ msgid "Password for $0"
+#~ msgstr "Heslo pro $0"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "Uživatel <b>$0</b> není oprávněn spravovat servery"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Účty"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "Uživatel <b>$0</b> není oprávněn upravovat účty"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Účet správce (root) není možné smazat"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Účet správce (root) není možné přejmenovat"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "Nemáte oprávnění pro přidání nové zóny"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "Nemáte oprávnění pro přidání služeb do zóny $0"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "Nemáte oprávnění pro odebrání služby $0"
+
+#~ msgid "Account Settings"
+#~ msgstr "Nastavení účtu"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Přidat stroj na přehled"
+
+#~ msgid "Machines"
+#~ msgstr "Stroje"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "Přihlášení má oprávnění povýšené na úroveň pro správu"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Heslo není použitelné pro privilegované úlohy nebo pro připojování se k "
+#~ "ostatním strojům"
+
+#~ msgid "Privileged"
+#~ msgstr "Privilegované"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Použít mé heslo i pro privilegované úlohy a připojování k dalším strojům"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr "Uživatel $0 nemá oprávnění měnit název stroje"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr ""
+#~ "Uživatel $0 nemá oprávnění měnit vypínat nebo restartovat tento server"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "Uživatel <b>$0</b> není oprávněn měnit profily"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "Uživatel <b>$0</b> není oprávněn spravovat úložiště"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr "Uživatel <b>$0</b> není oprávněn upravovat nastavení sítě"
+
+#~ msgid "Required by $0"
+#~ msgstr "Vyžadováno $0"
+
+#~ msgid "Automatic Startup"
+#~ msgstr "Automatické spouštění"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Naposledy spuštěno"
+
+#~ msgid "Next Run"
+#~ msgstr "Příští spuštění"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "Uživatel <b>$0</b> není oprávněn pro vytváření časovačů"
+
+#~ msgid "Connecting"
+#~ msgstr "Připojování"
+
+#~ msgid "About Cockpit"
+#~ msgstr "O Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (sdíleno s operačním systémem)"
+
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 zranitelnost"
+#~ msgstr[1] "$0 zranitelnosti"
+#~ msgstr[2] "$0 zranitelností"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Přidat další úložiště"
+
+#~ msgid "Add Storage"
+#~ msgstr "Přidat úložiště"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Další úložiště"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Všechna data na vybraných discích budou vymazána a disky budou přidány do "
+#~ "fondu úložiště."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Nastavit úložiště…"
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Nedaří se přidat všechny disky"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Vymazat kontejnery a resetovat fond úložiště"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr "Informace o Docker fondu úložiště nejsou k dispozici."
+
+#~ msgid "Local Disks"
+#~ msgstr "Místní disky"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "Nenalezeno žádné další místní úložiště."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Znovu naformátovat a přidat disky"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "Resetování fondu úložiště vymaže všechny kontejnery a uvolní disky ve "
+#~ "fondu."
+
+# auto translated by TM merge from project: Fedora OpenSSH Guide, version:
+# master, DocId: Security
+#~ msgid "Security"
+#~ msgstr "Zabezpečení"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr "Docker fond úložiště není na tomto systému možné spravovat."
+
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "Při skenu z $time ($type) nalezena $count zranitelnost:"
+#~ msgstr[1] "Při skenu z $time ($type) nalezeny $count zranitelnosti:"
+#~ msgstr[2] "Při skenu z $time ($type) nalezeno $count zranitelností:"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "Sken z $time ($type) nenalezl žádné zranitelnosti."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "Sken z $time ($type) nebyl úspěšný."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Virtualizační služba je k dispozici"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "Nemáte oprávnění spravovat fond úložiště pro Docker."
+
+#~ msgid "$0 GiB / $1 GiB"
+#~ msgstr "$0 GiB / $1 GiB"
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} -> $cport"
+
+#~ msgid "Hide Performance Options"
+#~ msgstr "Předvolby pro výkonnost"
+
+#~ msgid "Show Performance Options"
+#~ msgstr "Zobrazit předvolby pro výkonnost"
diff --git a/po/de.po b/po/de.po
new file mode 100644
index 0000000..5038e7c
--- /dev/null
+++ b/po/de.po
@@ -0,0 +1,13184 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Peter <pvolpe@redhat.com>, 2015. #zanata
+# Michael Hofer <mail@michaelhofer.ch>, 2016. #zanata
+# cockpit <cockpituous@gmail.com>, 2016. #zanata
+# Paul Ritter <paul-ritter@gmx.net>, 2017. #zanata
+# cockpit <cockpituous@gmail.com>, 2017. #zanata
+# Fabian Affolter <fab@fedoraproject.org>, 2018. #zanata
+# Ludek Janda <ljanda@redhat.com>, 2018. #zanata
+# cockpit <cockpituous@gmail.com>, 2018. #zanata
+# Fabian Affolter <fab@fedoraproject.org>, 2019. #zanata
+# Martin Pitt <mpitt@redhat.com>, 2019. #zanata, 2020.
+# Michael Hofer <mail@michaelhofer.ch>, 2019. #zanata
+# Stefan Lehmann <stefan.lehmann@mailbox.org>, 2019. #zanata
+# Jens Maucher <jens.maucher@online.de>, 2020.
+# Mike Wille <mike@wille.io>, 2020.
+# Julian Metza <julian.metza@asmi.de>, 2020.
+# Anonymous <noreply@weblate.org>, 2020.
+# Jonas Becker <jonasbecker1.jb@gmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-10-10 10:23+0000\n"
+"Last-Translator: N B <nb476823@gmail.com>\n"
+"Language-Team: German <https://translate.fedoraproject.org/projects/cockpit/"
+"main/de/>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.0.2\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "# der Benutzer"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+msgstr[1] "$0 CPUs"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 aktive Zone"
+msgstr[1] "$0 aktive Zonen"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 verfügbar"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "$0 Dateisysteme können nicht vergrößer werden."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "$0 Dateisysteme können nicht verkleinert werden."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "$0 Dateisysteme können hier nicht skaliert werden."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "$0 Dateisysteme können hier nicht skaliert werden."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 Chunk Größe"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 kritische Treffer"
+msgstr[1] "$0 Treffer, darunter kritische"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 data + $1 Overhead verwendet von $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 Tag"
+msgstr[1] "$0 Tage"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 Festplatte fehlt"
+msgstr[1] "$0 Festplatten fehlen"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 Datenträger fehlt"
+msgstr[1] "$0 Datenträger fehlen"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 Datenträger"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 Dokumentation"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 Error"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 mit Code $1 beendet"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 fehlgeschlagen"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 fehlgeschlagene Anmeldung"
+msgstr[1] "$0 fehlgeschlagene Anmeldungen"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "$0 Dateisystem"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 frei"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 Stunde"
+msgstr[1] "$0 Stunden"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 wichtiger Treffer"
+msgstr[1] "$0 Treffer, darunter wichtige"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 ist eine existierende Datei"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 verwendet"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 ist in keinem Repository verfügbar."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 Schlüssel geändert"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 mit Signal $1 beendet"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 wenig wichtiger Treffer"
+msgstr[1] "$0 wenig unwichtige Treffer"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 Minute"
+msgstr[1] "$0 Minuten"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 Treffer"
+msgstr[1] "$0 Treffer, darunter moderate"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 Monat"
+msgstr[1] "$0 Monate"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 weitere..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 Paket"
+msgstr[1] "$0 Pakete"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 Paket benötigt einen Systemneustart"
+msgstr[1] "$0 Pakete benötigen einen Systemneustart"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 Seite"
+msgstr[1] "$0 Seiten"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "Partition"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 Sicherheitsupdate verfügbar"
+msgstr[1] "$0 Sicherheitsupdates verfügbar"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 Dienst ist fehlerhaft"
+msgstr[1] "$0 Dienste sind fehlerhaft"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 Dienst muss neu gestartet werden"
+msgstr[1] "$0 Dienste müssen neu gestartet werden"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 Slot verbleibend"
+msgstr[1] "$0 Slots verbleibend"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 Lastspitze"
+msgstr[1] "$0 Lastspitzen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+#, fuzzy
+#| msgid "Not synchronized"
+msgid "$0 synchronized"
+msgstr "Nicht synchronisiert"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 gesamt"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 Aktualisierung verfügbar"
+msgstr[1] "$0 Aktualisierungen verfügbar"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 verwendet von $1 ($2 gespeichert)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 Woche"
+msgstr[1] "$0 Wochen"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 wird installiert."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 Jahr"
+msgstr[1] "$0 Jahre"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 Zone"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: Absturz bei $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (von $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Erzeuge Partition $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Lokaler Einhängepunkt"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Nicht eingehängt"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(empfohlen)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", einschließlich $1 Sicherheitsupdate"
+msgstr[1] ", einschließlich $1 Sicherheitsupdates"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 Tag"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 Stunde"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 minute"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 Minute"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 Woche"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14."
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 Minuten"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 Minuten"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 Minuten"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4."
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 Minuten"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 Minuten"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5."
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 Stunden"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 Minuten"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9."
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Eine kompatible Version von Cockpit ist auf $0 nicht installiert."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "Ein Dateisystem mit diesem Namen existiert bereits in diesem Pool."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "Eine Gruppe mit diesem Namen existiert bereits"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Ein moderner Browser ist für Sicherheit, Zuverlässigkeit und Leistung "
+"erforderlich."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Ein gebündelter Netzwerkadapter kombiniert mehrere Netzweradapter zu einem "
+"logischen Netzwerkadapter mit mehr Durchsatz und Redundanz."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Ein neuer SSH-Schlüssel unter $0 wird für $1 auf $2 erstellt und zur Datei "
+"$3 von $4 auf $5 hinzugefügt."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Ein Paket benötigt einen Neustart des Systems, damit die Aktualisierungen "
+"wirksam werden:"
+msgstr[1] ""
+"Einige Pakete benötigen einen Neustart des Systems, damit die "
+"Aktualisierungen wirksam werden:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Ein Pool mit diesem Namen existiert bereits."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"Ein Dienst muss neu gestartet werden, damit die Aktualisierungen wirksam "
+"werden:"
+msgstr[1] ""
+"Einige Dienste müssen neu gestartet werden, damit die Aktualisierungen "
+"wirksam werden:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "Physische Datenträger können hier nicht geändert werden."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP-Überwachung"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP-Ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Über Web Console"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Abwesend"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Schlüssel akzeptieren und anmelden"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+#, fuzzy
+#| msgid "Set password"
+msgid "Acceptable password"
+msgstr "Passwort setzen"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Kontoablauf"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Konto nicht verfügbar oder Änderungen nicht möglich."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Konten"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Aktion"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Aktionen"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Aktivieren"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Aktiviere $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Aktiv"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Aktive Sicherung"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Aktive Seiten"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Aktiviert seit "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Aktiver Status"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Adaptiver Lastausgleich (alb)"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Adaptiver Sendeausgleich (tlb)"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Hinzufügen"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "$0 hinzufügen"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "DNS Server hinzufügen"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Netzwerkgebundene Festplattenverschlüsselung hinzufügen"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Tang Keyserver hinzufügen"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "VLAN hinzufügen"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "VPN hinzufügen"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "WireGuard VPN hinzufügen"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+#, fuzzy
+#| msgid "Add item"
+msgid "Add a bitmap"
+msgstr "Element hinzufügen"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Neue Zone hinzufügen"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+#, fuzzy
+#| msgid "MAC address"
+msgid "Add address"
+msgstr "Adresse hinzufügen"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Blockorientierte Geräte hinzufügen"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Bündelung hinzufügen"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Bridge hinzufügen"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Datenträger hinzufügen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Datenträger hinzufügen"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "iSCSI-Portal hinzufügen"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Schlüssel hinzufügen"
+
+#: pkg/storaged/stratis/pool.jsx:551
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add keyserver"
+msgstr "Schlüsselserver hinzufügen"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Mitglied hinzufügen"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Neuen Host hinzufügen"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Neue Zone hinzufügen"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "Passphrase hinzufügen"
+
+#: pkg/networkmanager/wireguard.jsx:290
+#, fuzzy
+#| msgid "Add member"
+msgid "Add peer"
+msgstr "Knoten hinzufügen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "Physical volume"
+msgid "Add physical volume"
+msgstr "Physisches Volumen"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Ports hinzufügen"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Ports zu Zone $0 hinzufügen"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Öffentlichen Schlüssel hinzufügen"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+#, fuzzy
+#| msgid "Add item"
+msgid "Add route"
+msgstr "Route hinzufügen"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+#, fuzzy
+#| msgid "DNS search domains"
+msgid "Add search domain"
+msgstr "Suchdomänen hinzufügen"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Dienste hinzufügen"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Services zu Zone $0 hinzufügen"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Services zu Zone $0 hinzufügen"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Team hinzufügen"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Zone hinzufügen"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "Hinzufügen von \"$0\" zu Verschlüsselungsoptionen"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Hinzufügen von \"$0\" zu Dateisystemoptionen"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Das Hinzufügen von $0 unterbricht die Verbindung zum Server und macht die "
+"Administrationsoberfläche nicht mehr verfügbar."
+
+#: pkg/storaged/stratis/pool.jsx:468
+#, fuzzy
+#| msgid ""
+#| "Saving a new passphrase requires unlocking the disk. Please provide a "
+#| "current disk passphrase."
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Das Hinzufügen eines neuen Schlüsselserver erfordert das Entsperren des "
+"Bereichs . Bitte geben Sie eine aktuelle Passphrase an."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Das Hinzufügen von eigenen Ports wird firewalld neu starten. Ein Neustart "
+"setzt alle Freischaltungen auf Laufzeitebene zurück!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Schlüssel wird hinzugefügt"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Füge physikalischen Datenträger zu $target hinzu"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Hinzufügen von rd.neednet=1 zur kernel command line"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Zusätzliches DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Zusätzliche DNS-Suchdomänen $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Weitere Aktionen"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Zusätzliche Adresse $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Zusatzpakete:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Mehr Ports"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Adresse"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Adresse $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Adresse darf nicht leer sein"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Adresse ist keine gültige URL"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Adressen"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "Adresse darf nicht leer sein oder ist nicht korrekt formatiert"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Mit der Cockpit Web Konsole administrieren"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Administrativer Zugang"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr ""
+"Zum Erstellen und Abrufen von Berichten ist ein administrativer Zugang "
+"erforderlich."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Administrativer Zugang erforderlich"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Fortgeschrittenes TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Nach"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Nach dem Verlassen der Domain können sich nur noch Nutzer mit lokalen "
+"Zugängen an dieser Maschine anmelden. Dies betrifft auch andere Dienste wie "
+"z.B. die DNS Auflösungs Einstellungen und die Liste der vertrauenswürdigen "
+"CAs könnte sich ändern."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Nach dem Systemstart"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Alarm"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Alarm und oben"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Alias"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Alle"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Alle Updates"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Alles-in-einem"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Ausführung zulassen (unmask)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "Erlaubte IP-Adressen"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Erlaubte Adressen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Ansible Rollendokumentation"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Jede Zeichenkette in den log Nachrichten kann gefiltert werden. Die "
+"Zeichenkette kann auch die Form eines herkömmlichen Ausdrucks haben. Filtern "
+"nach log Nachrichtenfeldern wird ebenfalls unterstützt. Dies sind durch "
+"Leerzeichen getrennte Werte in der Form FELD=WERT, wobei WERT eine durch "
+"Kommata getrennte Liste von möglichen Werten sein kann."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Erscheinungsbild"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr ""
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Anwendungen"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Anwendungsliste"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Anwenden und Neustarten"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Kernel-Live-Patches anwenden"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Diese Einstellungen anwenden"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Neue Policy wird angewendet... Dies kann ein paar Minuten dauern."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Einstellungen werden angewandt ..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Aktualisierungen werden angewandt"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Das Anwenden der Updates ist fehlgeschlagen"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "Angemessen für wichtige mounts, wie /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Apps"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Bewertung"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Asset-Tag"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "Beim Starten"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Mindestens $0 Datenträger ist nötig."
+msgstr[1] "Mindestens $0 Datenträger sind nötig."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "Mindestens ein blockorientiertes Gerät ist erforderlich."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Mindestens ein Datenträger ist nötig."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "Bei Minute"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "Bei Sekunde"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "Zu einer bestimmten Zeit"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Attribute"
+
+# translation auto-copied from project Drools Workbench, version 6.0.0,
+# document org.drools/drools-wb-guided-rule-editor-
+# client/org/drools/workbench/screens/guided/rule/client/resources/i18n/Constants,
+# author jdimanos
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Audit-Protokoll"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Authentifiziere"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Authentifiziere"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Authentifizierung"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Authentifizierung fehlgeschlagen"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Authentifizierung fehlgeschlagen: Server geschlossene Verbindung"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr "Privilegierte Aktionen der Cockpit Web-Konsole benötigen Berechtigung"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Authentifikation erforderlich"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "SSH-Schlüssel autorisieren"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Autorisierte öffentliche SSH-Schlüssel"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automatisch"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automatisch (nur DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Automatische Anmeldung"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Automatische Updates"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Startet automatisch"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Automatisch (NTP)"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Automatische Benutzung zusätzlicher NTP-Server"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Automatisch (spezifische NTP-Server)"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Automatisierungs-Skript"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Verfügbare Ziele am $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Verfügbare Updates"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "Grow partition"
+msgid "BIOS boot partition"
+msgstr "Partition erweitern"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "BIOS-Datum"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "BIOS-Version"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Zurück zu den Konten"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Falsch"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Falsche Einstellung"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Balancer"
+
+# translation auto-copied from project Drools Workbench, version 6.0.0,
+# document org.drools/drools-wb-guided-rule-editor-
+# client/org/drools/workbench/screens/guided/rule/client/resources/i18n/Constants,
+# author jdimanos
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Bevor"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Bindet an"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Schwarz"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blade"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Bladegehäuse"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "Blockorientiertes Gerät"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Gerät für Dateisysteme blockieren"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Blockorientierte Geräte"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Gesperrt"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Bond"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Booten"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"Bootvorgang schlägt fehl, falls das Dateisystem nicht mountet, verhindert "
+"Fernzugriff"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr ""
+"Bootvorgang ist erfolgreich, auch wenn das Dateisystem nicht eingehängt ist"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Gebunden"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Netzwerkbrücke"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Netzwerkbrücken-Port"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Netzwerkbrücke-Port-Einstellungen"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Broadcast"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Fehlerhafte Konfiguration"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Bug Fix Updates verfügbar"
+
+# ctx::sourcefile::/rhn/errata/manage/Create.do
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Fehlermeldungen"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Bus-Erweiterungsgehäuse"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Wenn Sie das Passwort des SSH-Schlüssels $0 in das Anmeldepasswort von $1 "
+"auf $2 ändern, wird der Schlüssel automatisch verfügbar gemacht und Sie "
+"können sich in Zukunft ohne Passwort auf $3 anmelden."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " Browserprüfung umgehen "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "Prozessor"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Prozessor-Sicherheit"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "Prozessor-Sicherheits-Schalter"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Prozessor-Auslastung"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "Prozessor Nutzung/Auslastung"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Cache"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr ""
+"Kann ein Hostname, eine IP-Adresse, ein Alias-Name oder ein ssh:// URI sein"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr ""
+"Es konnten keine Logeinträge für die aktuellen Filtereinstellungen gefunden "
+"werden"
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Abbrechen"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Herunterfahren abbrechen"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Neustart abbrechen"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Kann nicht aktiviert werden"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Verbindung zu einer unbekannten Maschine nicht möglich"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Anmeldeinformationen können nicht weitergeleitet werden"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+"Kann keiner Domäne beitreten, da realmd auf diesem System nicht installiert "
+"ist."
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Vorgang kann nicht für die Vergangenheit geplant werden"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Kapazität"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Träger"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Ändern"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "Kryptografische Richtlinie ändern"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Rechnernamen ändern"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Ändern Sie den Namen des iSCSI-Initiators"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Ändern Sie den Namen des iSCSI-Initiators"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change label"
+msgstr "Shell ändern"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Passwort ändern"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Passwort ändern"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Leistungsprofil ändern"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Profil ändern"
+
+#: pkg/users/shell-dialog.js:70
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change shell"
+msgstr "Shell ändern"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Systemzeit ändern"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Passwort von $0 ändern"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Bridge Port-Einstellungen"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Geänderte Schlüssel sind oft das Ergebnis einer Neuinstallation des "
+"Betriebssystems. Allerdings kann eine unerwartete Änderung auf einen Versuch "
+"eines Dritten hinweisen, Ihre Verbindung auszuspähen."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Das Ändern der Einstellungen wird die Verbindung zum Server unterbrechen und "
+"damit den Zugriff auf die Benutzeroberfläche unmöglich machen."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Auf Aktualisierungen prüfen"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"Überprüfen Sie, ob der SHA-256- oder SHA-1-Hash des Befehls mit diesem "
+"Dialogfeld übereinstimmt."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "Überprüfen Sie den Schlüsselhash mit dem Tang-Server."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "$target wird überprüft"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "IP wird überprüft"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "RAID-Gerät $target wird überprüft"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "RAID-Gerät $target wird überprüft und repariert"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Prüfung auf Paket $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Prüfung auf NBDE Unterstützung in der initrd"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Auf neue Anwendungen wird geprüft"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Auf Paketaktualisierungen wird geprüft..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Installierte Software wird überprüft"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Überprüfen abhängiger Prozesse"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Software-Status wird überprüft"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Wählen Sie die in der Applikation zu verwendende Sprache"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Datenblock Grösse"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Klasse"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "$target wird aufgeräumt"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "'Starten Fehlgeschlagen' zurücksetzen"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Alle Filter entfernen"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Suche zurücksetzen"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Klartext-Gerät"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Client software"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Schließen"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Ausgewählte Seiten schließen"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Die Cockpit-Authentifizierung ist falsch konfiguriert."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "Cockpit Konfiguration von NetworkManager und Firewalld"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit konnte den angegebenen Host nicht erreichen."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit hatte einen unerwarteten Fehler."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit ist ein Server Manager zur einfachen Verwaltung Ihrer Linux Server "
+"via Web Browser. Ein Wechsel zwischen dem Terminal und der Weboberfläche ist "
+"kein Problem. Ein Service, der via Cockpit gestartet wurde, kann im Terminal "
+"beendet werden. Genauso können Fehler, welche im Terminal vorkommen, im "
+"Cockpit Journal angezeigt werden."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr ""
+"Cockpit ist ein interaktives Administrationsinterface für Linux Server."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit ist mit der Software auf dem System nicht kompatibel."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit ist nicht installiert"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit ist auf dem System nicht installiert."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit ist perfekt für neue Systemadministratoren, da es ihnen auf einfache "
+"Weise ermöglicht, simple Aufgaben wie Speicherverwaltung, Journal / Logfile "
+"Analyse oder das Starten und Stoppen von Diensten durchzuführen. Sie können "
+"gleichzeitig mehrere Server überwachen und verwalten. Fügen Sie weitere "
+"Maschinen mit einem Klick hinzu und Ihre Maschinen schauen zu ihren Kumpels."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit wird in Ihrem Browser möglicherweise nicht korrekt dargestellt"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Sammeln und Packen von Diagnose und Support Daten"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Sammeln von Kernel-Absturz-Auszügen"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Kennzahlen sammeln"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Farbe"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "Komma-separierte Ports, Bereiche und Dienste sind erlaubt"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Befehl"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Befehl nicht gefunden"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Kommentar"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Kommunikation mit tuned schlug fehl"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Kompakte PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Kompatibel mit allen Systemen und Geräten (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Kompatibel mit modernen Systemen und Festplatten > 2TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Absturzberichte komprimieren um Speicherplatz zu sparen"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Komprimierung"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Bedingung $0=$1 wurde nicht erfüllt"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Bedingung fehlgeschlagen"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Konfiguration"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Konfiguriere"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Konfiguriere IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Konfiguriere kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Konfiguriere System-Einstellungen"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Bestätigen"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Löschen von $0 bestätigen"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Neues Passwort wiederholen"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Neues Schlüssel-Passwort wiederholen"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Neues Passwort wiederholen"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Passwort bestätigen"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Betätigen sie das entfernen von $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Bestätigen Sie die Entfernung mit einer alternativen Passphrase"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Stoppen von $0 bestätigen"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "Konflikt von"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Konflikte"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Verbindung automatisch herstellen"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Verbinden zu"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Verbinden mit:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Verbinde zum SETroubleshoot Daemon..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Verbindung zum dbus fehlgeschlagen: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Verbindung zur Maschine wird hergestellt"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Fehler bei der Verbindung"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Verbindung fehlgeschlagen"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Zeitüberschreitung bei der Verbindung."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "Verbindung geht verloren"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Besteht aus"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Kontaktierte Domain"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Enthält:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Weiter"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Sitzung Fortsetzen"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Steuerung"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Strg"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Controller"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Convertible"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Kopiert"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Kopieren"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "In Zwischenablage kopieren"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Kern $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "Konnte $0 nicht kontaktieren"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Ort für Absturzberichte"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Absturz melden"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Crash-System"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Erstellen"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "LVM2-Volumengruppe erstellen"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "RAID-Gerät erzeugen"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "RAID-Gerät erzeugen"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Stratis Pool erstellen"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Einen neuen SSH-Schlüssel erstellen und ihn autorisieren"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Einen Schnappschuss des Dateisystems $0 erstellen"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Account mit nicht eindeutiger UID erstellen"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Account mit schwachem Passwort erstellen"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Erstellen und Ändern des Besitzes des Home-Verzeichnisses"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Anlegen und Einhängen"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create and mount"
+msgid "Create and start"
+msgstr "Anlegen und Einhängen"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Dateisystem erstellen"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Anlegen"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Logischen Datenträger erstellen"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Neues Konto anlegen"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Neues Dateisystem erstellen"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Neue Gruppe erstellen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Logischen Datenträger erstellen"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Neue Task-Datei mit diesem Inhalt erstellen."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "Logischen Datenträger erstellen"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "Nur anlegen"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Partition erzeugen"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Partition auf $0 anlegen"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Partitionstabelle anlegen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Snapshot erzeugen"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Snapshot erzeugen und einhängen"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Nur Snapshot erzeugen"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create storage volume"
+msgid "Create storage device"
+msgstr "Datenträger erstellen"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Volume erzeugen"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Thin Volume erstellen"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Timer erstellen"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Datenträgerverbund erstellen"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Erstellt"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "LVM2-Volumengruppe $target erstellt"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "Erzeuge RAID-Gerät $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "VDO-Gerät wird erstellt"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Dateisystem auf $target wird erzeugt"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Erzeuge logischen Datenträger $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Erzeuge Partition $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Erzeuge Snapshot von $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Die Erstellung dieser $0 unterbricht die Verbindung zum Server und macht die "
+"Administrationsoberfläche nicht mehr erreichbar."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Kritisch und höher"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Kryptografische Richtlinien sind ein Systembestandteil, der die "
+"kryptografischen Kern-Subsysteme konfiguriert. Dabei deckt er das TLS, "
+"IPSec, SSH, DNSSec und Kerberos Protokoll ab."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "Kryptografische Richtlinie"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "Die kryptografische Richtlinie ist inkonsistent"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Strg+Einfügen"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Strg-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Aktueller Boot"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Aktuell höchste CPU Last"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Aktuell verwendet"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Currently in use"
+msgid "Currently not supported"
+msgstr "Aktuell verwendet"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "Brauch"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "Benutzerdefinierte kryptografische Richtlinie"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Benutzerdefinierte Einhängeoptionen"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Eigene Ports"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom ports"
+msgid "Custom type"
+msgstr "Eigene Ports"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Eigene Zonen"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "STANDARD mit SHA-1 Signatur erlaubt."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS Suchdomänen"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "DNS-Suchdomänen $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Täglich"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Gefahrenalarm:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Dunkel"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Daten"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Verwendete Daten"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Datumsangaben sollten im Format JJJJ-MM-TT hh:mm:ss sein. Alternativ werden "
+"auch die Begriffe 'gestern', 'heute', 'morgen' verstanden. 'Now' bezieht "
+"sich auf die aktuelle Zeit. Es können auch relative Zeiten angegeben werden "
+"mit den Vorzeichen '-' oder '+'"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Deaktivieren"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Logischen Datenträger umbenennen"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Wird deaktiviert"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Deaktiviere $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Debug und höher"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Um eins verringern"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+#, fuzzy
+#| msgid "RAID 4 (dedicated parity)"
+msgid "Dedicated parity (RAID 4)"
+msgstr "RAID 4 (verteilte Daten & feste Parität)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Deduplizierung"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Standard"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Verzögerung"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Verzögerung muss eine Zahl sein"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Löschen"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "$0 löschen"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Konto löschen"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Dateien löschen"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Gruppe löschen"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Löschen"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Bericht dauerhaft löschen?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Das Löschen von $0 unterbricht die Verbindung zum Server und macht die "
+"Administrationsoberfläche nicht mehr erreichbar."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Lösche $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "LVM2-Volumengruppe $target wird gelöscht"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr ""
+"Das Löschen eines Stratis Pools entfernt alle sich darin befindlichen Daten."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr ""
+"Beim Löschen eines Dateisystems werden alle darin enthaltenen Daten gelöscht."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr ""
+"Beim Löschen eines logischen Datenträgers werden alle darin enthaltenen "
+"Daten gelöscht."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr ""
+"Beim Löschen einer Partition werden alle darin enthaltenen Daten gelöscht."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting erases all data on a RAID device."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Beim Löschen werden alle Daten auf einem RAID-Gerät gelöscht."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "Beim Löschen werden alle Daten auf einem VDO-Gerät gelöscht."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "Beim Löschen werden alle Daten auf einer Volumengruppe gelöscht."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting erases all data on a volume group."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "Beim Löschen werden alle Daten auf einer Volumengruppe gelöscht."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "Beim Löschen werden die folgenden Dateien entfernt:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Beschreibung"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Desktop"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Abnehmbar"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Details"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Entwicklung"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Gerät"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Gerätedatei"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Gerät ist schreibgeschützt"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Dienstname"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Diagnoseberichte"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Ordner"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Verzeichnis $0 ist nicht beschreibbar oder existiert nicht."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Simultanes Multithreading deaktivieren"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Firewall deaktivieren"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Tuned deaktivieren"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Deaktiviert"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Interaktives Passwort nicht erlauben"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Authentifizierung durch Passwort nicht erlauben"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Verbiete die Ausführung (mask)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Verbindung trennen"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Getrennt"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Festplatten-E/A"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Datenträger ist OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Festplatte versagt"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Disk-Passphrase"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Datenträger"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Festplattenbelegung"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+#, fuzzy
+#| msgid "Dismiss $0 alert"
+#| msgid_plural "Dismiss $0 alerts"
+msgid "Dismiss"
+msgstr "Verwerfe $0 Warnung"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Verwerfe $0 Warnung"
+msgstr[1] "Verwerfe $0 Warnungen"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Ausgewählte Alarme verwerfen"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Anzeigesprache"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+#, fuzzy
+#| msgid "RAID 5 (distributed parity)"
+msgid "Distributed parity (RAID 5)"
+msgstr "RAID 5 (verteilte Daten & verteilte Parität)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "Nicht einhängen"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Nicht beim Systemstart automatisch einhängen"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Dockingstation"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Nicht automatisch starten"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "Wird beim Systemstart nicht automatisch eingehängt"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Domain"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Domäne"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Domänenadressen"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Name des Domain-Administrators"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Passwort des Domain-Administrators"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Die Domäne konnte nicht kontaktiert werden"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Domäne wird nicht unterstützt"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Nicht wiederholen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+#, fuzzy
+#| msgid "RAID 6 (double distributed parity)"
+msgid "Double distributed parity (RAID 6)"
+msgstr "RAID 6 (verteilte Daten & doppelt verteilte Parität)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Herunterladen"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Laden Sie kostenlos einen neuen Browser herunter"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Heruntergeladen"
+
+# translation auto-copied from project guvnor, version 6.0.0, document
+# org.guvnor/guvnor-m2repo-editor-
+# client/org/guvnor/m2repo/client/resources/i18n/M2RepoEditorConstants, author
+# nmirasch
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Herunterladen"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "wird heruntergeladen $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Speichergerät"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Doppelter Rang"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Erweiterte Partition"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Bearbeiten"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "/etc/motd bearbeiten"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Tang-Keyserver bearbeiten"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "Editieren der VLAN Einstellungen"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "WireGuard VPN bearbeiten"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "Bond-Einstellungen bearbeiten"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "Netzwerkbrücke-Einstellungen bearbeiten"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Bearbeiten des eigenen Service in Zone $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Host bearbeiten"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Hosts bearbeiten"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "motd bearbeiten"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Einhängepunkt"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Regeln und Zonen bearbeiten"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Dienst bearbeiten"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Dienst $0 bearbeiten"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "Team Einstellungen bearbeiten"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Benutzer bearbeiten"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Das Bearbeiten eines Schlüssels erfordert einen freien Steckplatz"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Auswerfen $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "Embedded PC"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Leer"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Leeren"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Leeres Passwort"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Leere $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Aktivieren"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Gerät aktivieren oder deaktivieren"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Service aktivieren"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Firewall aktivieren"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Aktiviert"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "$0 wird aktiviert"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Daten verschlüsseln"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Daten mit einem Tang-Schlüsselserver verschlüsseln"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "Verschlüsseln von Daten mit einer Passphrase"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Verschlüsselt"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Verschlüsselt $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted Stratis pool $0"
+msgid "Encrypted Stratis pool"
+msgstr "Verschlüsselter Stratis Pool $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Verschlüsselter logischer Datenträger von $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Verschlüsselte Partition von $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Verschlüsselung"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Verschlüsselungsoptionen"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Verschlüsselungs-Passphrase"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Verschlüsselungstyp"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Beendet"
+
+#: pkg/networkmanager/wireguard.jsx:309
+#, fuzzy
+#| msgid "Entrypoint"
+msgid "Endpoint"
+msgstr "Einsprungpunkt"
+
+#: pkg/networkmanager/wireguard.jsx:276
+#, fuzzy
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"Der Endpunkt, der als \"Server\" fungiert, muss als host:port angegeben "
+"werden, andernfalls kann er leer gelassen werden."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Wird erzwungen"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Verbesserungsaktualisierungen verfügbar"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Geben Sie eine gültige MAC-Adresse ein"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Gesamtes Subnetzwerk"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Eintrag bei $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "$target wird gelöscht"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Errata"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Fehler"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Fehler und höher"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Fehler ist aufgetreten"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "PackageKit ist nicht installiert"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Fehlermeldung"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr ""
+"Semanage konnte nicht ausgeführt werden um Veränderungen am System zu "
+"entdecken"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Fehler beim Speichern der autorisierten Schlüssel: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Fehler beim Einstellen des SELinux-Modus: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet-MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet-MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Beispiel: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Beispiel: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Perfektes Passwort"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Erweiterungsgehäuse"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Konto ablaufen lassen am"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Konto auf $0 ablaufen lassen"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Exportieren"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "In das Netzwerk exportieren"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Erweiterte Informationen"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Erweiterte Partition"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS wurde nicht richtig aktiviert"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS mit weiteren Einschränkungen der Common Criteria."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Fehlgeschlagen"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Maschine konnte nicht hinzugefügt werden: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Hinzufügen des Portes fehlgeschlagen"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Hinzufügen des Services fehlgeschlagen"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Hinzufügen der Zone fehlgeschlagen"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Passwort konnte nicht geändert werden"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "PCP konnte nicht konfiguriert werden"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Fehler beim Löschen der Warnung: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Fehler beim Deaktivieren"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Deaktivierung des tuned-Profils ist fehlgeschlagen"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Maschine konnte nicht editiert werden: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Fehler bei der Bearbeitung des Dienstes"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "$0 konnte nicht in firewalld aktiviert werden"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Tuned konnte nicht aktiviert werden"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Protokolle konnten nicht abgerufen werden"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Fehler beim Laden autorisierter Schlüssel."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Laden der Einheit fehlgeschlagen"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"Das Parsen der Unit-Datein für dnf-automatic.timer oderr dnf-automatic-"
+"install.timer ist fehlgeschlagen. Bitte eigene Überschreibungen in der "
+"Konfigurationsautomatisierung entfernen."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Dienst konnte nicht neu gestartet werden"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Änderungen in /etc/motd konnten nicht gespeichert werden"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Fehler beim Speichern der Einstellungen"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Starten fehlgeschlagen"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Profil konnte nicht gewechselt werden"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Dateistatus"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Dateisystem"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "Dateisystem ist gesperrt"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Dateisystemname"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Dateisystem auf $target wird erzeugt"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "Dateisysteme sind bereits unter diesem Einhängepunkt eingehängt."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Filtere nach Name oder Beschreibung"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Menüpunkte filtern"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Filterdienste"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Filter"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Fingerabdruck"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Firewall"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Firewall ist nicht verfügbar"
+
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Firmware version"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "NBDE-Unterstützung beheben"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Schriftgröße"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Ausführen verboten"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Änderung erzwingen"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Löschen erzwingen"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Passwortänderung erzwingen"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Formatieren"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "$0 formatieren"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Formatieren und einhängen"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format and mount"
+msgid "Format and start"
+msgstr "Formatieren und einhängen"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Nur Formatieren"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "Beim Formatieren werden alle Daten auf einem Speichergerät gelöscht."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Weiterleitungsverzögerung $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Framezahl"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Freiraum"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Freitext-Suche"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Freitags"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "Von"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Vollständiger Name"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Gateway"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Allgemein"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Generiert"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Gehe zu $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Gehe zur Anwendung"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Zu 'Jetzt' gehen"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Ansicht als Graph"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Grafische Ansicht Optionsmenu"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Gruppe"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Gruppenname"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Gruppen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Wachsen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Inhalte erweitern"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Vergrößern Sie die logische Größe von $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Logisches Volumen erhöhen"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "Partition erweitern"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "Den Pool so vergrößern, dass er den gesamten Platz einnimmt"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Wachsen Sie, um den gesamten Raum einzunehmen"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Hairpin Modus"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Hairpin-Modus"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Handheld"
+
+#: pkg/storaged/drive/drive.jsx:64
+#, fuzzy
+#| msgid "Hard Disk"
+msgid "Hard Disk Drive"
+msgstr "Festplatte"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Hardware-Informationen"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Meldungen"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Hello-Zeit $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Hilfe"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Hide confirmation password"
+msgstr "Passwort bestätigen"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "Use password"
+msgid "Hide password"
+msgstr "Passwort verwenden"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "Hierachie ID"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "Höhere Interoperabilität auf Kosten einer größeren Angriffsfläche."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "History Packet-Anzahl"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Heimatverzeichnis"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Host"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Host-Schlüssel ist falsch"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Hostname"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Hosts"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Stündlich"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Stunde"
+
+#: pkg/storaged/crypto/tang.jsx:115
+#, fuzzy
+msgid "How to check"
+msgstr "Wie zu überprüfen"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP-Adresse"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"IP-Adresse mit Routing-Präfix. Trennen Sie mehrere Werte mit einem Komma. "
+"Beispiel: 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "IPv4-Adressen"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4-Einstellungen"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6-Einstellungen"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Kennung"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Wenn leer, wird die Kennung auf Basis der zugehörigen Port-Dienste und Port-"
+"Nummern generiert"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Wenn der Fingerabdruck übereinstimmt, klicke \"Schlüssel akzeptieren und "
+"einloggen\". Andernfalls, Login abbrechen und den Administrator kontaktieren."
+
+#: pkg/shell/hosts_dialog.jsx:480
+#, fuzzy
+#| msgid ""
+#| "If the fingerprint matches, click 'Accept key and connect'. Otherwise, do "
+#| "not connect and contact your administrator."
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Wenn der Fingerabdruck übereinstimmt, klicke 'Schlüssel akzeptieren und "
+"verbinden'. Andernfalls, Verbindung ablehnen und den Administrator "
+"kontaktieren."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ignorieren"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "In"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "Führen Sie in einem Terminal folgenden Befehl aus: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Um in Zukunft die Anmeldung bei $0 als $1 ohne Passwort zu ermöglichen, "
+"verwenden Sie das Anmeldepasswort von $2 auf $3 als Schlüsselpasswort oder "
+"lassen Sie das Schlüsselpasswort leer."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "Synchron"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Inaktiv"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "Logischen Datenträger erstellen"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Enthaltene Dienste"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"Eingehende Anfragen werden standardmäßig blockiert. Ausgehende Anfragen "
+"werden nicht blockiert."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Inkonsistenter Dateisystem-Einhängepunkt"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Um eins erhöhen"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Indexspeicher"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Indirekt"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Info"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Info und höher"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Initialisieren"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Festplatte $0 initialisieren"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "Beim Initialisieren werden alle Daten auf einem Datenträger gelöscht."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Initialisierung ..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Einblicke: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Installation"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "NFS-Unterstützung installieren"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Stratis-Unterstützung installieren"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Alle Aktualisierungen installieren"
+
+#: pkg/apps/application-list.jsx:200
+#, fuzzy
+#| msgid "Package information"
+msgid "Install application information"
+msgstr "Paket-Informationen"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "cockpit-pcp installieren"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "kpatch-Aktualisierungen installieren"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+#, fuzzy
+#| msgid "Install Stratis support"
+msgid "Install realmd support"
+msgstr "Realmd-Unterstützung installieren"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Sicherheitsaktualisierungen installieren"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr ""
+"Installieren Sie setroubleshoot-server, um SELinux-Ereignisse zu behandeln."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Software installieren"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Bitte installieren Sie die $0 Paket"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr ""
+"Die Installation ohne installiertes Cockpit-Paket wird nicht unterstützt"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Installiert"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Wird installiert"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "$0 wird installiert"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "Die Installation von $0 würde $1 entfernen."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Pakete werden installiert"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Schnittstelle"
+msgstr[1] "Schnittstellen"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Schnittstellenmitglieder"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Schnittstellen"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Interner Fehler"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Interner Fehler: Ungültiger Challenge-Header"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Ungültig"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Ungültige Adresse $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Ungültiges Datumsformat"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Ungültiges Datumsformat und ungültiges Zeitformat"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Ungültiges Ablaufdatum"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Ungültige Dateiberechtigungen"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Ungültiger Schlüssel"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Ungültige Metrik $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Ungültige Anzahl von Tagen"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Ungültige Port-Nummer"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Ungültiger Prefix $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Port"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Ungültiger Bereich"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Ungültiges Zeitformat"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Ungültige Zeitzone"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Benutzername oder Passwort ungültig"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "IoT-Gateway"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "Läuft sshd auf einem anderen Port?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Jobs"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Beitreten"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Einer Domäne beitreten"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Einer Domain beitreten"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+#, fuzzy
+#| msgid "Join"
+msgid "Joining"
+msgstr "Beitreten"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Zum Beitritt einer Domäne wird realmd benötigt"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Der Beitritt zu dieser Domain wird nicht unterstützt"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Tritt dem Namespace Of bei"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Tagebuch"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Journal-Eintrag"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Journal-Eintrag nicht gefunden"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Gehe zu"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit ist nicht installiert"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "kdump-Einstellungen prüfen"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Verbindung halten"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Kernel-Absturz-Auszug"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Kernel dump"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "Kernel-Live-Patch $0 ist aktiv"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Kernel-Live-Patch $0 ist installiert"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Kernel-Live-Patch-Einstellungen"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Kernel-Live-Patching"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Schlüsselpasswort"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr ""
+"Schlüsselplätze mit unbekannten Typen können hier nicht bearbeitet werden"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Schlüsselquelle"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Schlüssel"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Schlüsselserver"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Keyserver-Adresse"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr ""
+"Das Entfernen des Keyservers verhindert möglicherweise das Entsperren $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP-Schlüssel"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "ALTLAST mit Active-Directory-Interoperabilität."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+msgid "LVM2 VDO pool"
+msgstr "VDO pool"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Logischer Datenträger"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Logische Volumen"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volume"
+msgstr "Physisches Volumen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volumes"
+msgstr "Physisches Volumen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM2-Volumengruppe"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "LVM2-Volumengruppe $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Laptop"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Letzte 24 Stunden"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Letzte 7 Tage"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Zuletzt aktiv"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "Der letzte Schlüsselschlitz kann nicht entfernt werden"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Zuletzt geprüft: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "Der letzte Schlüsselschlitz kann nicht entfernt werden"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Letzte Anmeldung"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Zuletzt geändert: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Letzte erfolgreiche Anmeldung:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Mehr erfahren"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "$0 verlassen"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Domäne verlassen"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Leer lassen, um die Verschlüsselung zu überspringen"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Lizenziert unter der GNU LGPL Version 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Leicht"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Zugang einschränken"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Eingeschränkter Zugang"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"Der eingeschränkte Zugriffsmodus schränkt die administrativen Rechte ein. "
+"Einige Teile der Web-Konsole sind in ihrer Funktionalität eingeschränkt."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Einschränkungen"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Verzögerung bei der Deaktivierung einer Verbindung"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Link local"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Verbindungsüberwachung"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Verzögerung bei der Aktivierung einer Verbindung"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Link beobachten"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Verbunden"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem $0"
+msgid "Linux filesystem data"
+msgstr "Dateisystem $0 aushängen"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Auslagerungsspeicher"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Lauschen"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "Auf Port Lauschen"
+
+#: pkg/networkmanager/wireguard.jsx:149
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Listen port must be a number"
+msgstr "Der Port muss eine Nummer sein"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Auflisten der Einheiten"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Auflisten der Einheiten fehlgeschlagen: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Last"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Lastverteilung"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Frühere Daten laden"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Frühere Einträge laden"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Laden verfügbarer Aktualisierungen fehlgeschlagen"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Verfügbare Aktualisierungen werden geladen, bitte warten ..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Frühere Einträge werden geladen"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Schlüssel werden geladen ..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Laden von SSH-Schlüsseln fehlgeschlagen"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Laden der Einheiten fehlgeschlagen"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Pakete werden geladen..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "System-Änderungen laden..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Laden der Einheit fehlgeschlagen"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Lade..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Lokale Konten"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Lokales Dateisystem"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Lokaler Einhängepunkt"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "No storage"
+msgid "Local storage"
+msgstr "Kein Speicher"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "lokal in $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Ort"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Schließen"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "$0 sperren"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Konto sperren"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgid "Locked"
+msgid "Locked data"
+msgstr "Gesperrt"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Sperren $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Anmelden"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Bei $0 anmelden"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Melden Sie sich mit dem Server-Benutzerkonto an."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Nachrichten protokollieren"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Abmelden"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Benutzer abmelden"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Angemeldet"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Logisch"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Logisches Volume (Momentaufnahme)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Logische Größe"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Logischer Datenträger"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Logisches Volume (Momentaufnahme)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Logisches Volumen von $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Anmeldung"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Nochmal anmelden"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Anmeldung fehlgeschlagen"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Login format"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Anmeldeverlauf"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Liste des Anmeldeverlaufs"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "$0 abmelden"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Abmeldung erfolgreich"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Protokolle"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Low-Profile-Desktop"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Brotdose"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+# translation auto-copied from project Blivet, version master, document blivet
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "RAID-Gerät"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "RAID-Gerät $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Gerät ist schreibgeschützt"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "Dienst läuft nicht"
+
+# translation auto-copied from project Blivet, version master, document blivet
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "RAID-Gerät"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Datenträger hinzufügen"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (empfohlen)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU muss eine positive Zahl sein"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Maschinen-ID"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "SSH-Fingerabdrücke auf diesem Rechner"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Hauptservergehäuse"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Wartung"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "Dateisystemgrößen bearbeiten"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Speicher verwalten"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Verwaltete Schnittstellen"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "LVMs verwalten"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "NFS-Freigaben verwalten"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "RAIDs verwalten"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "VDOs verwalten"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "VLANs verwalten"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Firewall verwalten"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Verwalten der Netzwerkverbindungen"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Netzwerkbrücken verwalten"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Verwalten der Netzwerkteams"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Partitionen verwalten"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Physische Laufwerke verwalten"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Dienste verwalten"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Software-Aktualisierungen verwalten"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Benutzerkonten verwalten"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Manuell"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Manuell"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "$target als fehlerhaft markieren"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Dienst maskieren"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Maskiert"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Der Maskierungsdienst verhindert die Ausführung aller abhängigen Einheiten. "
+"Dies kann größere Auswirkung haben als vorhergesehen. Bitte bestätigen Sie, "
+"dass Sie diese Einheit maskieren wollen."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Maximales Alter der Nachrichten $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Optisches Speichergerät"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Speicher"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Speicher-Technologie"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Speichernutzung"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Speichernutzung/-austausch"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Zusammengeführt"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Nachricht an angemeldete Benutzer"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "Im Journal finden sich möglicherweise Meldungen zu dem Fehler:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Verwendete Metadaten"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Methode"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Kennzahlen"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Metriken und Verlauf"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Kennzahlenverlauf konnte nicht geladen werden"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Kennzahleneinstellungen"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Mini-Tower"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Minute muss eine Zahl zwischen 0 und 59 sein"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "Minütlich"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minuten"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Milderungen"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Modus"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Modell"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Ändern $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Montags"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Überwachungsintervall"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Überwachungsziele"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Monatlich"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Weitere Informationen..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Einhängen"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "Einhängepunkt"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr ""
+"Einhängen nachdem das Netzwerk wieder zur Verfügung steht, Fehlermeldung "
+"ignorieren"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Automatisch bei Start einhängen"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "beim Start einhängen"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Automatisch bei Start unter $0 einhängen"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Einhängen bevor die Dienste starten"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Einhängekonfiguration"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Dateisystem einhängen"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Jetzt einbinden"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Jetzt unter $0 einhängen"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Einhängoptionen"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Einhängepunkt"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Einhängepunkt darf nicht leer sein"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Einhängepunkt darf nicht leer sein."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Einhängepunkt wird bereits von $0 verwendet"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Einhängepunkt muss mit \"/\" beginnen."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Dateisystem als nur lesbar einhängen"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Einhängen ohne abzuwarten, Fehlermeldung ignorieren"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "$target wird eingehängt"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Hängt sich ein, bevor die Dienste starten"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Hängt sich parallel zu den Diensten ein"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+"Hängt sich parallel zu den Diensten ein, aber erst wenn das Netzwerk zur "
+"Vefügung steht"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Multi-System-Chassis"
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Multipathed devices"
+msgid "Multipathed devices"
+msgstr "Mehrpfadige Geräte"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+"Mehrere Adressen können mit Kommas oder Leerzeichen als Trennzeichen "
+"angegeben werden."
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS-Mount"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA-Ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP-Server"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Name"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Name darf nicht leer sein."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Name darf nicht leer sein."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Name darf nicht länger als $0 Bytes sein"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Name darf nicht länger als $0 Zeichen sein"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Name darf nicht länger als 127 Zeichen sein."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Name darf nicht länger als 127 Zeichen sein."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Name darf nicht das Zeichen '$0' enthalten."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "Name darf nicht das Zeichen '$0' enthalten."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Name darf keine Leerzeichen enthalten."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Benötigen Sie mindestens einen NTP-Server"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Netzwerk"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Netzwerk-I/O"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Netzwerkbindung"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Netzwerkgeräte und -diagramme erfordern NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Netzwerkprotokolle"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Netzwerk-Nutzung"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager ist nicht installiert"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager wird nicht ausgeführt"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Netzwerk-Nutzung"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Netzwerk"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Nie"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Konto nie ablaufen lassen"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Passwort verfällt niemals"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Nie angemeldet"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Neuer NFS-Mount"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Neuer Host"
+
+#: pkg/shell/hosts_dialog.jsx:464
+#, fuzzy
+#| msgid "New host"
+msgid "New host: $0"
+msgstr "Neuer Host"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Neues Schlüsselpasswort"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Neuer Name"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Neue Passphrase"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Neues Passwort"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Das neue Passwort wurde nicht akzeptiert"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Weiter"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Nein"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "Keine Kennung angegeben"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Keine SELinux-Alarme"
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Keine Anwendungen installiert oder verfügbar."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Keine verfügbaren Slots"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Es sind keine blockorientierten Geräte verfügbar."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "Kein Startgerät gefunden"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Kein Träger"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Keine Konfiguration gefunden"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Keine Daten verfügbar"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Keine Daten verfügbar zwischen $0 und $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Keine Verzögerung"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Keine Beschreibung verfügbar"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Keine Beschreibung angegeben."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "Kein Startgerät gefunden"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Es sind keine Festplatten verfügbar."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Keine Protokolle gefunden"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "Keine Ergebnisse gefunden"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Keine Verschlüsselung"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Keine Ereignisse"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Kein Dateisystem"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Keine Dateisysteme"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Keine freien Schlüsselplätze"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Kein freier Platz"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "Kein freier Speicherplatz nach dieser Partition"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Kein Gruppenname angegeben"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Es wurden keine Host-Schlüssel gefunden."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Kein Installationspaket für diese Anwendung gefunden."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Keine Schlüssel hinzugefügt"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "Keine passenden Sprachen"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Keine Protokolleinträge"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Keine logischen Volumes"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Keine Protokolle gefunden"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Keine passenden Ergebnisse"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Keine Medien eingelegt"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Keine Partitionierung"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "Keine Partitionierung"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "Keine Peers hinzugefügt."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "Physikalische Volumen"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Es wurde kein echter Name angegeben"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Keine Ergebnisse gefunden"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Keine Ergebnisse entsprechen den Filterkriterien. Löschen Sie alle Filter, "
+"um die Ergebnisse anzuzeigen."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Keine Treffer in Regelwerk"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No storage"
+msgid "No storage found"
+msgstr "Kein Speicher"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Keine logischen Volumes"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Datei oder Verzeichnis nicht vorhanden"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Keine Systemänderungen"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Keine Systemberichte."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Keine Aktualisierungen"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Es wurde kein Benutzername angegeben."
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Kein"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Ungültiger privater Schlüssel"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Keine Berechtigung, die Firewall zu deaktivieren"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Keine Berechtigung, die Firewall zu aktivieren"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Nicht verfügbar"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Nicht verbunden"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Nicht mit Insights verbunden"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Nicht mit Host verbunden"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space"
+msgid "Not enough free space"
+msgstr "Nicht genügend Platz"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "Nicht genügend Platz"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "Nicht genug Platz für Vergrößerung"
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Nicht gefunden"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Nicht installiert"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "Not permitted to configure realms"
+msgid "Not permitted to configure network devices"
+msgstr "Keine Berechtigung Realms zu konfigurieren"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Keine Berechtigung Realms zu konfigurieren"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Diese Aktion darf nicht ausgeführt werden."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Nicht bereit"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Nicht registriert"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Läuft nicht"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Nicht eingerichtet"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Nicht synchronisiert"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Hinweis"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Notizbuch"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Hinweis und höher"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Netzwerkadressen, Hostnamen und Benutzernamen obfuskieren"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Obfuskiert"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Aufgetreten $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Zwischen aufgetreten $0 und $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Vorkommnisse"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "OK"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Alte Passphrase"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Altes Passwort"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Altes Passwort wurde nicht akzeptiert"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "Auf einem eingehangenem Gerät"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Bei einem Ausfall"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Wenn Cockpit installiert ist, aktivieren Sie es mit \"systemctl enable --now "
+"cockpit.socket\"."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Nur $0 von $1 werden verwendet."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Nur Alphabete, Zahlen,:, _,. , @ , - sind erlaubt"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Nur Notfall"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"Beim Booten im FIPS-Modus nur zugelassene und erlaubte Algorithmen verwenden."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Uuups!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr ""
+"Öffnen Sie den pmproxy-Dienst in der Firewall, um Metriken auszutauschen."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Operation '$operation' auf $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Einstellungen"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Oder verwenden Sie einen gebündelten Browser"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Weitere"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"Andere Authentifizierungsmethoden stehen auch dann zur Verfügung, wenn die "
+"interaktive Passwortauthentifizierung nicht erlaubt ist."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Andere Einstellungen"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Aus"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Überblick"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Überschreiben"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Vorhandene Daten mit Nullen überschreiben (langsamer)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Paket cockpit-pcp fehlt für den Kennzahlenverlauf"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Paket-Informationen"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit ist abgestürzt"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit ist nicht installiert"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit hat Fehlercode gemeldet $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Pakete"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Seitenname"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Parent"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Parent $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Teil von"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Teil von $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Partition"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Partition von $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "Die Größe der Partition beträgt $0. Die Größe des Inhalts beträgt $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Partitionierung"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Partitionen"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Passiv"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Passwort"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "Passphrase darf nicht leer sein"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Passphrase darf nicht leer sein"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Passwort-Satz von irgendeinem anderen Schlüsselplatz"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr ""
+"Das Entfernen der Passphrase verhindert möglicherweise das Entsperren $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Passphrasen stimmen nicht überein."
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Passwort"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Passwort erfolgreich geändert"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Passwort ablaufen"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "Passwort ist länger als 256 Zeichen"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Das Passwort kann nicht akzeptiert werden"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Das gewählte Passwort ist zu schwach"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Passwort muss geändert werden"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Passwort wurde nicht akzeptiert"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Passworthinweis"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Einfügen"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Fehler beim Einfügen"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "Vorhandenen Schlüssel einfügen"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Fügen Sie hier den Inhalt Ihrer öffentlichen SSH-Schlüsseldatei ein"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Pfad"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Pfadkosten"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Pfadkosten $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Pfad auf dem Server"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Pfad auf dem Server darf nicht leer sein."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Pfad auf dem Server muss mit \"/\" beginnen."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Pfad zum Verzeichnis"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Pfad zur Datei"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Pfade"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Pause"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr "Peer #$0 hat ungültigen Endpunkt-Port. Port muss eine Zahl sein."
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"Peer #$0 hat einen ungültigen Endpunkt. Er muss als Host:Port angegeben "
+"werden, z.B. 1.2.3.4:51820 oder example.com:51820"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "Peers"
+
+#: pkg/networkmanager/wireguard.jsx:273
+#, fuzzy
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"Peers sind andere Rechner, die eine Verbindung zu diesem Rechner herstellen. "
+"Öffentliche Schlüssel von anderen Rechnern werden untereinander ausgetauscht."
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot sammelt und analysiert die Leistungskennzahlen Ihres "
+"Systems."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Leistungsprofil"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Peripheriechassis"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Permanent"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "Gruppe $0 endgültig löschen?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "$0 dauerhaft löschen?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "$0 dauerhaft löschen?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete subvolume $0?"
+msgstr "$0 dauerhaft löschen?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Erlaubnis verweigert"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Zulässig"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Physisch"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "Physical Volumes"
+msgstr "Physikalische Volumen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Physikalische Volumen"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "Physische Datenträger können hier nicht geändert werden."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Datum auswählen"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Einheit festlegen"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Ping-Intervall"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Ping-Ziel"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Festgelegte Einheit"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Pizza-Box"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr ""
+"Bitte authentifizieren Sie sich, um administrativen Zugriff zu erhalten"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr ""
+"Bitte aktivieren Sie JavaScript, um die Web-Konsole verwenden zu können."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Bitte installieren Sie die $0 Paket"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Bitte beheben Sie das Problem und laden Sie diese Seite neu."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Bitte geben Sie ein Ablaufdatum an"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr ""
+"Bitte geben Sie den Host an, zu dem eine Verbindung hergestellt werden soll"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "Bitte hängen Sie diese zuerst aus."
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Pool für Thin Logical Volumes"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Pool für dünn bereitgestellte Datenträger"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Pool für dünn bereitgestellte Datenträger"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "Pool Passphrase"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Port"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "tragbar"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Ports"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Partition erzeugen"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Präfixlänge"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Präfix oder Netzmaske"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Vorbereitung läuft"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Derzeit"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Beibehalten"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Anzeige-Rechnername"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Vorheriger Bootvorgang"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Primär"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Priorität"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Priorität $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "Privater Schlüssel"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "Problem beim Administrator werden"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Problemdetails"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Problem Info"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Prozesse, die den Ort nutzen"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Fortschritt: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Projekt-Website"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Die Aufforderung über passwd ist abgelaufen"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Die Aufforderung über ssh-add ist abgelaufen"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Die Aufforderung über ssh-keygen ist abgelaufen"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Propagiert reload to"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Schützt vor kurzfristig zu erwartenden Angriffen auf Kosten der "
+"Interoperabilität."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "Passwort-Satz für den Pool dieser blockierten Geräte:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Öffentlicher Schlüssel"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+"Der öffentliche Schlüssel wird generiert, wenn ein gültiger privater "
+"Schlüssel eingegeben wird"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Verwendungszweck"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (verteilt)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (gespiegelt)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (verteilt & gespiegelt)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (verteilte Daten & feste Parität)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (verteilte Daten & verteilte Parität)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (verteilte Daten & doppelt verteilte Parität)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID-Chassis"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID Ebene"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Rack-Einbaugehäuse"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Zufällig"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Bereich"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "Bereich muss strikt geordnet sein"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Rang"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Raw zu einem Gerät"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Lesen"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Mehr lesen..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Nur-lesen"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Wird gelesen"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Wird gelesen ..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Bereit"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Bereiten"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Echter Rechnername"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Ein echter Hostname darf nur Kleinbuchstaben, Ziffern, Bindestriche und "
+"Punkte enthalten (mit ausgefüllten Unterdomänen)."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Der tatsächliche Hostname darf höchstens 64 Zeichen umfassen"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Erneut anwenden und neu starten"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Neustart"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Neustart nach Fertigstellung"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Neustart empfohlen"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "System neu starten ..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Empfangen"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Zuletzt genutzte Hosts"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Empfohlene, sichere Einstellungen für aktuelle Bedrohungsmodelle."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Erneut verbinden"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Am Erholen"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "RAID-Gerät $target wird wiederhergestellt"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Paketinformationen aktualisieren"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Verbindung ablehnen Host ist unbekannt"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Verbindung ablehnen Hostkey stimmt nicht überein"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Verbindung ablehnen Hostkey ist unbekannt"
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "Regenerieren"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "initrd wird neu generiert"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Registrieren…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "Zugehörige Prozesse und Dienste werden zwangsweise gestoppt."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "Zugehörige Prozesse werden zwangsweise gestoppt."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "Zugehörige Dienste werden zwangsweise gestoppt."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Neu Laden"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Propagiert von neu laden"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Wird neu geladen"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Der Zustand der verbleibenden Dienste wird neu geladen"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "Fernzugriff über CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "Fernzugriff über FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Remote über NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "Remote über NFS"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "Fernzugriff über SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Remote über SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "Remote über SSH"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Umzüge:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Entfernen"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Entferne $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Dienst $0 aus der Zone $1 entfernen"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Löschen $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Tang-Schlüsselserver entfernen?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Gerät entfernen"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Host entfernen"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Element entfernen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Remove missing physical volumes?"
+msgstr "Entferne physikalischen Datenträger von $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "Passwort-Satz von Schlüsselplatz $0 entfernen?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "Passphrase entfernen?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Dienst $0 entfernen"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Zone $0 entfernen"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Entfernen"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Entfernen $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Das Entfernen von $0 wird die Verbindung zum Server unterbrechen und damit "
+"den Zugriff auf die Benutzeroberfläche unmöglich machen."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "Entferne $target vom RAID-Gerät"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"Das Entfernen einer Passphrase ohne Bestätigung einer anderen Passphrase "
+"kann das Entsperren oder die Schlüsselverwaltung verhindern, wenn andere "
+"Passphrasen vergessen oder verloren werden."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Entferne physikalischen Datenträger von $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Das entfernen des Cockpit-Services kann die Web Konsole nicht mehr "
+"erreichbar machen. Stelen sie sicher, dass die Zone keine Auswirkungen auf "
+"ihre aktuelle Web Konsolen Verbindung hat."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "Das entfernen der Zone wird alle Service in ihr entfernen."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Umbenennen"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Stratis Pool umbenennen"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Dateisystem umbenennen"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Gruppe umbennen"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "Gruppe $0 umbenennen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Logischen Datenträger umbenennen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Datenträgerverbund umbennen"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "$target wird umbenannt"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "Umbennen einer Gruppe hat Auswirkungen auf sudo und ähnliche Regeln"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Repair logical volume $0"
+msgstr "Logischen Datenträger umbenennen"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Reparieren $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Wiederholen"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Monatlich wiederholen"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Passphrase wiederholen"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Wöchentlich wiederholen"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Melden"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Report Bezeichnung"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Zu ABRT Analytics senden"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Gemeldet; keine Links verfügbar"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Meldung fehlgeschlagen"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Meldung wurde abgebrochen"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Berichte"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Berichte:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Erfordert jedes Mal ein Passwort $0 Tage"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Kennwortänderung an erforderlich $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Passwortänderung bei der ersten Anmeldung verlangen"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Benötigt von"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Benötigt von "
+
+# translation auto-copied from project RHN Satellite UI, version 5.7, document
+# java/code/src/com/redhat/rhn/frontend/strings/jsp/StringResource, author
+# jdimanos
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Erfordert"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Benötigt Administrator-Zugriff zum bearbeiten"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Requisit"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Erfordernis von"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Reservierter Speicher"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Zurücksetzen"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Passwort zurücksetzen"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Größenänderung von $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Die Größenänderung eines verschlüsselten Dateisystems benötigt das "
+"entsperren der Festplatte. Bitte geben sie das Passwort ein."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Neustarten"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Dienste neu starten"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Dienste werden neu gestartet ..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Wird neu gestartet"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Verbindung wird wiederhergestellt"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"Die Ergebnisse des Absturzes werden über $0 nach $1 als $2 kopiert, wenn "
+"kdump richtig konfiguriert ist."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+"Die Ergebnisse des Absturzes werden in $0 als $1 gespeichert, wenn kdump "
+"richtig konfiguriert ist."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Fortfahren"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Vorhandene Verschlüsselung wiederverwenden"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Vorhandene Verschlüsselung wiederverwenden ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "Überprüfung der Verschlüsselungsrichtlinie"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Protokolle bewerten"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Round robin"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Routen"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Ausführen um"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Neuen Bericht ausführen"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Ausführen auf"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Bericht ausführen"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Runner"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Läuft"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Laufzeit"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux-Zugriffssteuerungsfehler"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux ist auf dem System deaktiviert"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux ist auf dem System deaktiviert"
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux-Richtlinie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "Der Status von SELinux ist unbekannt."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "SELinux-Problembehandlung"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "SMART-Selbsttest von $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"Die SOS-Berichterstattung sammelt Systeminformationen, die bei der Diagnose "
+"von Problemen helfen."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH-Schlüssel"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "Der SSH-Schlüssel ist kein Pfad"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH-Schlüssel"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "STP Forward Delay"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "STP Hello-Zeitintervall"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP Maximum Message Age"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP-Priorität"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr "Safari Benutzer müssen das selbst signierten Zertifikat importieren:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Samstags"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Speichern"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Sichern und Neustarten"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Änderungen speichern"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Speicherplatz durch komprimieren der einzelnen Blöcke mit LZ4 spare"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr ""
+"Sparen Sie Platz, indem identische Datenblöcke nur einmal gespeichert werden"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Das Speichern einer neuen Passphrase erfordert das Entsperren der "
+"Festplatte. Bitte geben Sie eine aktuelle Disk-Passphrase an."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Planmäßiges Ausschalten um $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Planmäßiger Neustart um $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "PC mit versiegeltem Gehäuse"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Suche"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Suchdomäne"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Nach Name oder Kennung suchen"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Nach Name, Gruppe oder Kennung suchen"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "Sekunde muss eine Zahl zwischen 0 und 59 sein"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Sekunden"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "SSH-Schlüssel"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "$target wird sicher gelöscht"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Sicherheitsverstärkte Linuxkonfiguration und Problemlösung"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Sicherheitsupdates verfügbar"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Nur Sicherheitsaktualisierungen"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Sicherheitsaktualisierungen werden für $0 auf $1 angewandt"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Auswählen"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Einen Bezeichner auswählen"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Methode auswählen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Senden"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Senden"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Seriennummer"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Server"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Serveradresse"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Serveradresse darf nicht leer sein."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Server darf nicht leer sein"
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Der Server hat die Verbindung beendet."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Server-Software"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Dienst"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Dienst hat einen Fehler"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Serviceprotokolle"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Dienste"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Dienste, die den Ort nutzen"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Sitzung"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Sitzung läuft bald ab"
+
+# translation auto-copied from project KIE Workbench - Common, version 6.0.0,
+# document org.kie.workbench.widgets/kie-wb-common-
+# ui/org/kie/workbench/common/widgets/client/resources/i18n/HumanReadableConstants,
+# author jdimanos
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Einstellen"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Hostname setzen"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Partition auf $0 anlegen"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Passwort setzen"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Zeit setzen"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Einstellen"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Konfiguration"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Schwaches Passwort festlegen"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Abweichungen vom konfigurierten Zustand werden beim nächsten Neustart "
+"zurückgesetzt."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Einrichten"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Richte Loop Device $target ein"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Einstellungen"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Schweregrad"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Geteilt"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Shell"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Shell script"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Einfügen"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "Alle Threads anzeigen"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Alle Threads anzeigen"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Show confirmation password"
+msgstr "Passwort bestätigen"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Fingerabdrücke anzeigen"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Meldungen mit der angegebenen Zeichenkette anzeigen."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Meldungen für die angegebene systemd-Einheit anzeigen."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Anzeige der Meldungen eines speziellen Bootvorgangs."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Weitere Beziehungen anzeigen"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "New password"
+msgid "Show password"
+msgstr "Neues Passwort"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Beziehungen anzeigen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Verkleinern"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Logisches Volumen verkleinern"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "Partition verkleinern"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Volumen verkleinern"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Herunterfahren"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Herunterfahren"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Seit"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Einzelner Rang"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Größe"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "Größe darf nicht negativ sein"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "Größe darf nicht Null sein"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Größe zu groß"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Größe muss eine Zahl sein"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Größe muss mindestens sein $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Hauptnavigation überspringen"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Zum Inhalt springen"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Slot"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Slot $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Schnappschuss"
+
+# translation auto-copied from project MRG Realtime Reference Guide, version
+# 2.2, document Sockets
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Sockets"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Aktualisierungen"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Softwarebasierte Umgehungen helfen, CPU-Sicherheitsprobleme zu vermeiden. "
+"Diese Abhilfemaßnahmen haben den Nebeneffekt, dass sie die Leistung "
+"verringern. Das Ändern dieser Einstellungen erfolgt auf eigene Gefahr."
+
+#: pkg/storaged/drive/drive.jsx:63
+#, fuzzy
+msgid "Solid State Drive"
+msgstr "Solid-State Datenträger"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Lösung erfolgreich angewendet"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Lösung fehlgeschlagen"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Lösungen"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"Einige Blockgeräte dieses Pools haben sich nach der Erstellung des Pools "
+"vergrößert. Der Pool kann sicher erweitert werden, um den neu verfügbaren "
+"Platz zu nutzen."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"Ein anderes Programm verwendet derzeit den Paketmanager, bitte warten Sie ..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Manche Software muss manuell neu gestartet werden"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Sortiert von am wenigsten bis am meisten vertrauenswürdig"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Platzsparender Computer"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Spanning tree protocol"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Spanning tree protocol (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Ersatz"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Bestimmte Zeit"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Geschwindigkeit"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Stabil"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Starten"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Starten und aktivieren"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Multipath starten"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Dienst starten"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr ""
+"Start der Anzeige von Einträgen, die am oder nach dem angegebenen Datum "
+"liegen."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr ""
+"Start der Anzeige von Einträgen, die am oder vor dem angegebenen Datum "
+"liegen."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Gestartet"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "RAID-Gerät $target wird gestartet"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Auslagerungsbereich $target wird gestartet"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Status"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Statisch"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Status"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Stick PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Sticky"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Stoppen"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Anhalten und deaktivieren"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Anhalten und entfernen"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Anhalten und aushängen"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Gerät anhalten"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Bearbeitung von Hosts beenden"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Bericht stoppen"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "RAID-Gerät $target wird angehalten"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Auslagerungsbereich $target wird angehalten"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Speicher"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "Der Speicher kann auf diesem System nicht verwaltet werden."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Speicherprotokolle"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Passphrase speichern"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Gespeichertes Passwort"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "$0 blockorientiertes Gerät"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "Add block devices"
+msgid "Stratis block devices"
+msgstr "Blockorientierte Geräte hinzufügen"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+#, fuzzy
+#| msgid "VDO backing devices can not be made smaller"
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Stratis-Blockdevs können nicht kleiner gemacht werden"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystem"
+msgstr "Dateisystem erstellen"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems"
+msgstr "Dateisystem erstellen"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Dateisystem erstellen"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Stratis Pool"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:97
+#, fuzzy
+#| msgid "Set password"
+msgid "Strong password"
+msgstr "Passwort setzen"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Kontrollabschnitt"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Stil"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Sub-Chassis"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Sub-Notebook"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "Das Abonnieren von systemd-Signalen ist fehlgeschlagen: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+msgid "Subvolume needs to be mounted"
+msgstr ""
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Erfolgreich in die Zwischenablage kopiert"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Erfolgreich in die Zwischenablage kopiert!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Sonntags"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Swap"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "$0 Dateisysteme können hier nicht skaliert werden."
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Auslagerung"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Schalter von $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "$0 ausschalten"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "$0 anschalten"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Auf administrativen Zugang umschalten"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Auf eingeschränkten Zugang umschalten"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Das Ausschalten von $0 wird die Verbindung zum Server unterbrechen und damit "
+"den Zugriff auf die Benutzeroberfläche unmöglich machen."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Das Einschalten von $0 wird die Verbindung zum Server unterbrechen und damit "
+"den Zugriff auf die Benutzeroberfläche unmöglich machen."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Synchronisiert"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Mit $0 synchronisiert"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Wird synchronisiert"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "Synchronisiere RAID-Gerät $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "System"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Systemdiagnose"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Systeminformationen"
+
+# ctx::sourcefile::/systems/SystemEntitlements
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "System ist aktualisiert"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "System Modifikationen"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Systemzeit"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemd Unit"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tablett"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang Keyserver"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Ziel"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Ziele"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Team"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Netzwerk-Team Anschluss"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Team port Einstellungen"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminal"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Sitzung beenden"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Konfiguration prüfen"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Der Test ist nur verfügbar, wenn der Dienst kdump läuft."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "kdump-Einstellungen prüfen"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Testen Sie die kdump-Einstellungen, indem Sie den Kernel abstürzen lassen. "
+"Dies kann eine Weile dauern und das System wird möglicherweise nicht "
+"automatisch neu gestartet. Stürzen Sie das System nicht absichtlich ab, "
+"während eine wichtige Aufgabe ausgeführt wird."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Prüfe Verbindung"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "Das Paket $0 ist in keinem Repositorium verfügbar."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr ""
+"Das $0 Paket muss installiert werden, um Stratis Pools erzeugen zu können."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "Das Paket $0 muss installiert sein."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "Das Paket $0 wird installiert, um VDO-Geräte zu erstellen."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "Die IP-Adresse oder der Hostname darf keine Leerzeichen enthalten."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "Das RAID-Array befindet sich in einem degradierten Zustand"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr "Das RAID-Gerät muss aktiv sein, um Festplatten entfernen zu können."
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+"Der SSH-Schlüssel $0 von $1 auf $2 wird der Datei $3 von $4 auf $5 "
+"hinzugefügt."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"Der SSH-Schlüssel $0 wird für den Rest der Sitzung zur Verfügung gestellt "
+"und steht auch für die Anmeldung bei anderen Hosts zur Verfügung."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"Der SSH-Schlüssel für die Anmeldung bei $0 ist durch ein Passwort geschützt, "
+"und der Host erlaubt keine Anmeldung mit einem Passwort. Bitte geben Sie das "
+"Passwort des Schlüssels auf $1 an."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"Der SSH-Schlüssel für die Anmeldung bei $0 ist geschützt. Sie können sich "
+"entweder mit Ihrem Anmeldepasswort oder mit dem Passwort des Schlüssels bei "
+"$1 anmelden."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"Das Konto '$0'wird gezwungen sein Passwort bei der nächsten Anmeldung zu "
+"ändern"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Der Cockpit-Dienst ist automatisch enthalten"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"Der konfigurierte Zustand ist unbekannt und kann sich beim nächsten Neustart "
+"ändern."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"Die Erstellung dieses VDO-Geräts wurde nicht abgeschlossen und das Gerät "
+"kann nicht verwendet werden."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"Der aktuell angemeldete Benutzer kann keine Informationen zu Schlüsseln "
+"anzeigen."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"Die Festplatte muss vor dem Formatieren entsperrt werden. Bitte geben Sie "
+"eine bestehende Passphrase an."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "Die Datei $0 wird gelöscht."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "Das Dateisystem hat keinen permanenten Einhängepunkt"
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "Das Dateisystem hat keinen permanenten Einhängepunkt"
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Das Dateisystem ist so konfiguriert, dass es beim Booten automatisch "
+"eingehängt wird, aber sein Verschlüsselungscontainer wird zu diesem "
+"Zeitpunkt nicht entsperrt."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Das Dateisystem ist momentan eingehangen, wird aber bei nächsten Systemstart "
+"nicht eingehangen."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Das Dateisystem ist momentan als $0 eingehangen und wird beim nächsten "
+"Systemstart als $1 eingehangen."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Das Dateisystem ist momentan als $0 eingehangen und wird beim nächsten "
+"Systemstart nicht eingehangen."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Das Dateisystem ist momentan als nicht eingehangen und wird beim nächsten "
+"Systemstart eingehangen."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Das Dateisystem ist nicht eingehängt."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"Das Dateisystem wird beim nächsten Booten entsperrt und eingehängt. Dies "
+"kann die Eingabe einer Passphrase erfordern."
+
+#: pkg/shell/hosts_dialog.jsx:494
+#, fuzzy
+#| msgid "Show fingerprints"
+msgid "The fingerprint should match:"
+msgstr "Fingerabdrücke anzeigen"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Der folgende Dienst wird neu gestartet:"
+msgstr[1] "Die folgenden Dienste werden neu gestartet:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "Der vollständige Name darf keine Doppelpunkte enthalten."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "Die Gruppenkennung muss eine positive ganze Zahl sein"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Der Gruppenname darf nur aus Buchstaben von a bis z, Zahlen, Punkten, Binde- "
+"und Unterstrichen bestehen"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"Das Heimatverzeichnis $0 existiert bereits. Sein Besitz wird auf den neuen "
+"Benutzer geändert."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "Die initrd muss neu generiert werden."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Das Schlüsselpasswort darf nicht leer sein"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Die Schlüsselpasswörter stimmen nicht überein"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "Der von Ihnen angegebene Schlüssel ist ungültig."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Der letzte Schlüsselschlitz kann nicht entfernt werden"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "Die aufgelisteten Prozesse und Dienste werden zwangsweise gestoppt."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "Die aufgelisteten Prozesse werden zwangsweise gestoppt."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "Die aufgelisteten Dienste werden zwangsweise gestoppt."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr ""
+"Der angemeldete Benutzer ist nicht berechtigt, Systemänderungen einzusehen"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "Die Maschine startet neu"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "Der Einhängepunkt $0 wird von diesen Prozessen verwendet:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "Der Einhängepunkt $0 wird von diesen Diensten verwendet:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Das neue Schlüsselpasswort darf nicht leer sein"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Das Passwort darf nicht leer sein"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Die Passwörter stimmen nicht überein"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Die Passwörter stimmen nicht überein."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"Der entstandene Fingerabdruck kann über öffentliche Methoden, einschließlich "
+"E-Mail, weitergegeben werden."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Der Server hat die Authentifizierung abgelehnt. '$0Mit der Passwort-"
+"Authentifizierung stehen keine anderen unterstützten "
+"Authentifizierungsmethoden zur Verfügung."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr ""
+"Der Server hat die Authentifizierung mit allen unterstützten Methoden "
+"abgelehnt."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"Das System unterstützt momentan nicht das Entsperren eines Dateisystems mit "
+"einem Tang Schlüsselserver während des Bootvorgangs."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"Das System unterstützt momentan nicht das Entsperren des root-Dateisystems "
+"mit einem Tang Schlüsselserver."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+"Der Benutzer $0 ist nicht berechtigt, die CPU-Sicherheitsmaßnahmen zu ändern"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr ""
+"Der Benutzer $0 ist nicht berechtigt, kryptografische Richtlinien zu ändern"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr ""
+"Dem Benutzer $0 ist es nicht gestattet, den Kernel testweise abstürzen zu "
+"lassen"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"Der Benutzer muss sich ab- und wieder anmelden, damit die neue Konfiguration "
+"wirksam wird."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Der Benutzername darf nur aus Buchstaben, Zahlen, Punkten, Binde- und "
+"Unterstrichen bestehen."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"Die Konfiguration des Webbrowsers verhindert, dass Cockpit ausgeführt wird "
+"(nicht erreichbar) $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Derzeit sind keine aktiven Seiten vorhanden"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"Es gibt Geräte mit mehreren Pfaden im System, der Multipath-Dienst wird "
+"jedoch nicht ausgeführt."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "In dieser Zone gibt es keine aktiven Dienste"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Für dieses Konto existieren keine autorisierten öffentlichen Schlüssel"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"Der Platz im Pool reicht nicht aus, um einen Snapshot von diesem Dateisystem "
+"zu erstellen. Mindestens $0 sind erforderlich, aber nur $1 sind verfügbar."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr ""
+"Bei der Verbindung zur Maschine ist ein unerwarteter Fehler aufgetreten."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Diese zusätzlichen Schritte sind notwendig:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Diese Änderungen werden vorgenommen:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Dünnes logisches Volumen"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Pool für dünn bereitgestellte Datenträger"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+#, fuzzy
+#| msgid ""
+#| "This RAID array has no write-intent bitmap. Such a bitmap can reduce "
+#| "sychronization times significantly."
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"Dieses RAID-Array hat keine Bitmap für die Schreibberechtigung. Eine solche "
+"Bitmap kann die Synchronisierungszeiten erheblich reduzieren."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"Dieser NFS-Mount wird verwendet und nur die Optionen können geändert werden."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Dieses VDO-Gerät verwendet nicht alle seine Hintergrundgeräte."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Dieses Gerät kann hier nicht verwaltet werden."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Dieses Gerät kann hier nicht verwaltet werden."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "Dieses Gerät ist derzeit in Gebrauch."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Dieses Feld darf nicht leer sein"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Diese Gruppe ist die primäre Gruppe für die folgenden Benutzer:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr ""
+"Dieser Host wird nach der Installation der Aktualisierungen neu gestartet."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "Diese Informationen werden nur auf dem System gespeichert."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Dieser Schlüsselserver ist die einzige Möglichkeit, den Pool zu entsperren "
+"und kann nicht entfernt werden."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr ""
+"Dieses logische Volumen ist nicht vollständig durch seinen Inhalt belegt."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Diese Maschine wurde bereits hinzugefügt"
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Dies kann eine Weile dauern"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "Diese Partition ist nicht vollständig durch seinen Inhalt belegt."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Diese Passphrase ist die einzige Möglichkeit, den Pool zu entsperren und "
+"kann nicht entfernt werden."
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr ""
+"Dieser Pool nutzt nicht den gesamten Speicherplatz auf seinen Blockgeräten."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "Dieser Pool befindet sich in einem schlechten Zustand."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Dieses System ist nicht registriert"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Dieses System benutzt ein benutzerdefiniertes Profil"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Dieses System benutzt das empfohlene Profil"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"Dieses Tool konfiguriert die SELinux Policy und hilft dabei Verletzungen der "
+"Policy zu verstehen und aufzulösen."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"Dieses Tool konfiguriert das System zum Schreiben von Kernel Absturz "
+"Auszügen auf Datenträger."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"Dieses Tool generiert ein Archiv der Konfiguration und Diagnoseinformation "
+"des laufenden Systems.Das Archiv kann lokal oder zentral abgespeichert "
+"werden zum Zweck der Archivierung oder Nachverfolgung oder kann an den "
+"Technischen Support, Entwickler oder Systemadministratoren gesendet werden, "
+"um bei der Fehlersuche oder Debugging zu helfen."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"Dieses Tool verwaltet den lokalen Speicher, wie etwa Dateisysteme, LVM2 "
+"Volume Gruppen und NFS Einhängepunkte."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"Dieses Tool verwaltet die Netzwerkumgebung wie etwa Bindungen, Bridges, "
+"Teams, VLANs und Firewalls durch den NetworkManager und Firewalld. Der "
+"NetworkManager ist inkompatibel mit dem Ubuntus Standard systemd-networkd "
+"und Debians ifupdown Scipts."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Dieses Gerät kann nicht explizit aktiviert werden."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Dieser Benutzername existiert bereits"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr ""
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Dieser Webbrowser ist zu alt, um die Web-Konsole auszuführen (fehlende $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Damit wird eine Übereinstimmung für '_BOOT_ID=' hinzugefügt. Wird nichts "
+"angegeben, werden die Protokolle für den aktuellen Bootvorgang angezeigt. "
+"Wird die Boot-ID nicht angegeben, werden bei einem positiven Offset die "
+"Boote ab dem Anfang des Journals angezeigt, bei einem Offset gleich oder "
+"kleiner als Null werden die Boote ab dem Ende des Journals angezeigt. So "
+"bedeutet 1 den ersten im Journal gefundenen Boot in chronologischer "
+"Reihenfolge, 2 den zweiten usw., während -0 der letzte Boot ist, -1 der "
+"vorletzte Boot usw."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Dies fügt Übereinstimmungen für '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' und "
+"'UNIT=' hinzu, um alle möglichen Meldungen für die angegebene Einheit zu "
+"finden. Kann mehrere durch Komma getrennte Einheiten enthalten. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Dies ermöglicht es Ihnen, sich in Zukunft ohne Passwort anzumelden."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Diese Zone enthält den Cockpit-Dienst. Stellen Sie sicher, dass diese Zone "
+"nicht auf Ihre aktuelle Web-Konsolenverbindung angewendet wird."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Donnerstags"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Ebene"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Zeit"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Zeitzone"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Timer-Erstellung fehlgeschlagen"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "Timer-Löschung fehlgeschlagen"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Timer"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Tipp: Wenn Ihr Schlüsselpasswort mit Ihrem Login-Passwort übereinstimmt, "
+"können Sie sich an anderen Systemen automatisiert anmelden."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Überprüfen Sie bitte den Fingerabdruck des Host-Schlüssels, um "
+"sicherzustellen, dass Ihre Verbindung nicht von einem böswilligen Dritten "
+"ausgespäht wird:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Um Aktualisierungen zu erhalten, muss dieses System bei Red Hat registriert "
+"sein; entweder im Red Hat Customer Portal oder einem lokalen Subscription-"
+"Dienst."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Um einen Fingerabdruck zu überprüfen, führen Sie die folgenden Schritte auf "
+"$0 aus, während Sie physisch an der Maschine sitzen oder über ein "
+"vertrauenswürdiges Netzwerk:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Heute"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Umschalten"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Datumsauswahl umschalten"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Filter umschalten"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Zu viele Daten"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Werkzeuge"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Top 5 CPU-Dienste"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "Die 5 wichtigsten Festplattennutzungsdienste"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "Top 5 Speicherdienste"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Gesamtgröße: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Turm"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Vorübergehend"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Wird übertragen"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Auslöser"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Ausgelöst durch"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Löst aus"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Fehlersuche"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Fehlerbehebung…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+#, fuzzy
+#| msgid "Untrusted host"
+msgid "Trust and add host"
+msgstr "Nicht vertrauenswürdiger Host"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Schlüssel vertrauen"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Vertrauensstufe"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Versuchen Sie es nochmal"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Versuche mit {{Server}} zu synchronisieren"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Dienstags"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned konnte nicht gestartet werden"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned ist ein Dienst, der Ihr System überwacht und die Leistung unter "
+"bestimmten Arbeitsbelastungen optimiert. Das Herzstück von Tuned sind "
+"Profile, die Ihr System für verschiedene Anwendungsfälle abstimmen."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned ist nicht verfügbar"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned läuft nicht"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned ist ausgeschaltet"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Administrator-Zugriff aktivieren"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Typ"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Name darf nicht das Zeichen '$0' enthalten."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Zum Filtern tippen"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Kann diese Lösung nicht automatisiert anwenden"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Es kann keine Verbindung zu dieser Adresse hergestellt werden"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "$0 kann nicht kontaktiert werden."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Der angegebene Host $0 kann nicht kontaktiert werden. Stellen Sie sicher, "
+"dass ssh auf Port $1 läuft, oder geben Sie einen anderen Port in der Adresse "
+"an."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Alarmdetails können nicht abgerufen werden."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Die Anmeldung bei $0 mit SSH-Schlüsselauthentifizierung ist nicht möglich. "
+"Bitte geben Sie das Passwort ein. Vielleicht möchten Sie Ihre SSH-Schlüssel "
+"für die automatische Anmeldung einrichten."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Die Anmeldung bei $0 ist nicht möglich. Der Host akzeptiert weder ein "
+"Passwort noch einen Ihrer SSH-Schlüssel."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Kann Server nicht erreichen"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Mount kann nicht entfernt werden"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+#, fuzzy
+#| msgid "Encrypted logical volume of $0"
+msgid "Unable to repair logical volume $0"
+msgstr "Verschlüsselter logischer Datenträger von $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "Fix kann nicht ausgeführt werden: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "Einstellungen können nicht gespeichert werden"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "Einstellungen können nicht gespeichert werden: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Kann setroubleshootd nicht starten"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Das Aushängen des Dateisystems ist nicht möglich"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Nicht verfügbar"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Nicht verfügbare Pakete"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Rückgängig"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "Unerwarteter PackageKit-Fehler bei der Installation von $0: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Unerwarteter Fehler"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Unerkannte Daten"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Einheit"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Unbekannt"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Unbekannte \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Unbekannt ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Unbekannte Anwendung"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Unbekannte Konfiguration"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Unbekannter Host-Name"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Unbekannten Servicename"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Unbekannter Typ"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Öffnen"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Beim Booten automatisch entsperren"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Verschlüsselten Stratis-Pool freischalten"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "Schlüssel $0 entsperren"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Entsperren $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Festplatte wird entsperrt"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Unverwaltete Schnittstellen"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Aushängen"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "Dateisystem $0 aushängen"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Jetzt aushängen"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "$target wird ausgehängt"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Unbennant"
+
+#: pkg/systemd/service-details.jsx:184
+#, fuzzy
+msgid "Unpin unit"
+msgstr "Einheit abstecken"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Unerkannte Daten"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "Unrecognized data can not be made smaller here."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Nicht erkannte Daten können hier nicht verkleinert werden."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Nicht erkannte Daten können hier nicht verkleinert werden."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Nicht unterstützter Datenträger"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "Bis"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Nicht vertrauenswürdiger Host"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Update"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Aktualisierungserfolgstabelle"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Update-Verlauf"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Paket Informationen updaten"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "Aktualisierung war erfolgreich"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Aktualisiert"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr ""
+"Aktualisierte Pakete erfordern möglicherweise einen Neustart, um wirksam zu "
+"werden."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Updates verfügbar"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Aktualisierungsverlauf"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Aktualisierungen werden $0 auf $1 angewandt"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Aktualisiere"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Update Status..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Betriebszeit"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Nutzung"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "Nutzung von $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Verwenden"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Komprimierung verwenden"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Deduplizierung verwenden"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Schlüssel verwenden"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Passwort verwenden"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr ""
+"Benutze die folgenen Schlüssel zur Authentifizierung an anderen Systemen"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Ausführliche Protokollierung verwenden"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Benutzt"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"Nützlich für Montierungen, die optional sind oder Interaktion erfordern (z. "
+"B. Passphrasen)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Benutzer"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "Benutzer ID"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "Die Benutzer-ID wird bereits von einem anderen Benutzer verwendet"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "Die Benutzerkennung muss eine positive ganze Zahl sein"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "Die Benutzer-ID darf nicht höher als $0 sein"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "Die Benutzer-ID darf nicht niedriger als $0 sein"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Benutzername"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Der Benutzername darf nicht leer sein"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Benutzername"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "LUKS-Verschlüsselung verwenden"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Tang-Server verwenden"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "VDO-Sicherungsgeräte können nicht kleiner gemacht werden"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO-Gerät $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "VDO-Dateisystemvolumen (Komprimierung/Deduplizierung)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN-Kennung"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Validiere Adresse"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Authentifizierungstoken überprüfen"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Anbieter"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Verifiziert"
+
+#: pkg/shell/hosts_dialog.jsx:489
+#, fuzzy
+#| msgid "Fingerprint"
+msgid "Verify fingerprint"
+msgstr "Fingerabdruck"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Schlüssel überprüfen"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Überprüfung läuft"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Version"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "$target wird sehr sicher gelöscht"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Alle CPUs ansehen"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Alle Festplatten ansehen"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Alle Protokolle ansehen"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Alle Dienste ansehen"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Automatisierungs-Script anzeigen"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Detaillierte Protokolle ansehen"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Hardware-Details anzeigen"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Anmeldeverlauf ansehen"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "Protokolle ansehen"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Metriken und Verlauf ansehen"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Durchsatz pro Festplatte ansehen"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Projekt-Webseite ansehen"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Bericht ansehen"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Aktualisierungsprotokoll ansehen"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr ""
+"Zum Ansehen der Speicherinformationen ist ein administrativer Zugriff "
+"erforderlich."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Firewall besuchen"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Datenträgerverbund"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Volume group is missing physical volumes"
+msgstr "Entferne physikalischen Datenträger von $target"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Die Größe des Volumens beträgt $0. Die Größe des Inhalts beträgt $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Wartet"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Warten auf Details ..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Warte auf Eingabe…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr "Warten, bis andere Programme mit dem Paketmanager fertig sind ..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Warten, bis andere Software-Verwaltungsvorgänge abgeschlossen sind"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Warte auf den Start. . ."
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Gesucht von"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Will"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Warnung und höher"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+#, fuzzy
+#| msgid "Set weak password"
+msgid "Weak password"
+msgstr "Schwaches Passwort festlegen"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Web Konsole"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Webkonsole für Linux-Server"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Web-Konsole wird neu gestartet"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Die Webkonsole läuft mit limitierten Berechtigungen."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Logo der Web-Konsole"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Mittwochs"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Wöchentlich"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Wochen"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Wenn"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "Wenn leer, mit dem aktuellen Benutzer verbinden"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Wenn die Web-Konsole neu gestartet wird, sehen Sie keine "
+"Fortschrittsinformationen mehr. Allerdings wird der Aktualisierungsprozess "
+"im Hintergrund fortgesetzt. Stellen Sie die Verbindung wieder her, um den "
+"Aktualisierungsprozess weiter zu verfolgen."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"Wenn diese Option aktiviert ist, erlaubt der neue Pool kein "
+"Overprovisioning. Sie müssen für jedes Dateisystem, das im Pool erstellt "
+"wird, eine maximale Größe angeben. Dateisysteme können nach der Erstellung "
+"nicht mehr vergrößert werden. Snapshots werden bei der Erstellung "
+"vollständig zugewiesen. Die Summe aller Maximalgrößen darf die Größe des "
+"Pools nicht überschreiten. Dies hat den Vorteil, dass der Speicherplatz für "
+"Dateisysteme in diesem Pool nicht auf überraschende Weise erschöpft werden "
+"kann. Der Nachteil ist, dass Sie die maximale Größe für jedes Dateisystem im "
+"Voraus kennen müssen und die Erstellung von Snapshots begrenzt ist."
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Weiß"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "Wird auf \"Automatisch\" gesetzt"
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "World wide name"
+msgid "World wide name"
+msgstr "Weltweiter Name"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Schreiben"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Hauptsächlich Schreiben"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Schreiben"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Benutzername oder Passwort falsch"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Jährlich"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Ja"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Sie stellen zum ersten Mal eine Verbindung zu $0 her."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Sie sind nicht berechtigt, die Firewall zu ändern."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Sie haben keine Berechtigung, die autorisierten öffentlichen Schlüssel von "
+"diesem Konto anzuzeigen."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Sie wurden wegen Inaktivität abgemeldet."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Wollen sie probieren alte Einträge zu laden."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+"Vielleicht möchten Sie das Passwort des Schlüssels für die automatische "
+"Anmeldung ändern."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Sie müssen länger warten, um Ihr Passwort zu ändern"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr ""
+"Sie müssen sich erneut anmelden, um den Kennzahlenverlauf sehen zu können"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Sie haben nun Administrator Zugriff."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Sie werden in $0 Sekunden abgemeldet."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Ihr Konto"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"Ihr Browser lässt das Einfügen über das Kontextmenü nicht zu. Sie können "
+"Umschalt+Einfügen verwenden."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr ""
+"Ihr Browser speichert Ihre Zugriffsstufe über mehrere Sitzungen hinweg."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Ihr Server wird die Verbindung bald beenden. Sie können die Verbindung nach "
+"dem Neustart wiederherstellen."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Ihre Sitzung wurde beendet."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Die Session ist abgelaufen. Bitte neu einloggen."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Zone"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[Binärdaten]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[keine Daten]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr ""
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "Zugriff"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "Aktiv"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "Add-on"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "AddOn"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+#, fuzzy
+#| msgid "Isolated network"
+msgid "after network"
+msgstr "Isoliertes Netzwerk"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "Programme"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "Asset-Tag"
+
+# translation auto-copied from project Customer Portal Translations, version
+# PortalCaseManagementStrings, document PortalCaseManagementStrings, author
+# hpeters
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "um"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "Durchschnitt: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "Bündel"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "Starten"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "Brücke"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "Andere Geräte"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "Andere Geräte"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolume"
+msgstr "Speichervolumen"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Speichervolumen"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs volume"
+msgstr "Speichervolumen"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "Bug-Fix"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "Bytes"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "Cache"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "Cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "ändern"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman ist nicht installiert"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "Kommando"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "Konsole"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "coredump"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "CPU"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "Absturz"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "$0 Daten"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "Datum"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Deaktivieren"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "Debug"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "löschen"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "deaktivieren"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "Datenträger"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "Datenträger"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "Domain"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "Speichergerät"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "bearbeiten"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "aktivieren"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted"
+msgid "encrypted"
+msgstr "Verschlüsselt"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "Verschlüsselung"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "Verbesserung"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "Fehler"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "jeden Freitag"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "jeden Montag"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "jeden Samstag"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "jeden Sonntag"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "jeden Donnerstag"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "jeden Dienstag"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "jeden Mittwoch"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "jeden Tag"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "Erweiterung"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "Kann SSH-Hostschlüssel nicht anzeigen: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "Dateisystem"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "Firewall"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "für aktuelle und zukünftige Kernel"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "nur für den aktuellen Kernel"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "Formatieren"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "von $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "von $0 auf $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "Fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "Kurven"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "wachsen"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "Hardware"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "Verlauf"
+
+# translation auto-copied from project Skynet topics, version 1, document
+# 15687-444004, author rgromans
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "host"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "iSCSI targets"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "iSCSI targets"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "iSCSI-Portal hinzufügen"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "Ausfall ignorieren"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "in den meisten Browsern"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "inkonsistent"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "initialisieren"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "Installation"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "Schnittstelle"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "ungültig: Mehrere Ziele definiert"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "Journal"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "journalctl manpage"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "kdump Status"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "Schlüssel"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "Login"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "Maschine"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "Maske"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "max: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "member of RAID device"
+msgid "member of MDRAID device"
+msgstr "Mitglied des RAID-Geräts"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "Speicher"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "Kennzahlen"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "Milderung"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "weitere Details"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "Einhängen"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "Netzwerk"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "beim Booten nie einhängen"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "NFS Export ist leer"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "NFS-Server ist leer"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "NFS-Server ist nicht gültig IPv6"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "kein"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "von $0 CPU"
+msgstr[1] "von $0 CPUs"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "auf $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "Betriebssystem"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "os"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "Paket"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "Partition"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "Passwort"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "Passwortqualität"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "Patches"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "Pfad"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "Leistung"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "physisches Volumen der LVM2-Volumengruppe"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "Plugin"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service ist fehlgeschlagen"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service kann keine Daten sammeln"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service läuft nicht"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr ""
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "Port"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "Leistung"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "RAID"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "Arbeitsspeicher"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "nur lesbar"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "empfohlen"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "aus LVM2 entfernen"
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "remove from RAID"
+msgid "remove from MDRAID"
+msgstr "aus RAID entfernen"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "aus LVM2 entfernen"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "Neustart"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "Rollen"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "$0 wird ausgeführt"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "Sicherheit"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "Seriell"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "Dienst"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "Shell"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "zeige weniger"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "Zeig mehr"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "verkleinern"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "Socket"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "Lösung"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "Lösungsdetails"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "Sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+#, fuzzy
+#| msgid "Reporting failed"
+msgid "sos report failed"
+msgstr "Meldung fehlgeschlagen"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "Der SSH-Schlüssel ist kein Pfad"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "SSH-Server ist leer"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "Stopp"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+#, fuzzy
+#| msgid "On failure"
+msgid "stop boot on failure"
+msgstr "Bei einem Ausfall"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+#, fuzzy
+#| msgid "Stopped"
+msgid "stopped"
+msgstr "Angehalten"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "Sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "Ziel"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "TCP"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "Team"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "Zeit"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "Timer"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "Einheit"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "unbekannt"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "Unbekanntes Ziel"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "Freigeben"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "Aushängen"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "nicht partitionierter Speicherplatz auf $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "Benutzer"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "Benutzername"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#, fuzzy
+msgid "using key description $0"
+msgstr "Beschreibung"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "Version"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "VLan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "Laufwerk"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "Warnung"
+
+#: pkg/networkmanager/wireguard.jsx:84
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "wireguard-tools package is not installed"
+msgstr "PackageKit ist nicht installiert"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "Ja"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "Zone"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Kdump-Dienst nicht installiert. Bitte stellen Sie sicher, dass das Paket "
+#~ "kexec-tools installiert ist."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Kein Speicher reserviert Hängen Sie an die Kernel-Befehlszeile eine "
+#~ "Option crashkernel an (z. B. in / etc / default / grub), um Speicherplatz "
+#~ "beim Booten zu reservieren. Beispiel: crashkernel = 512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Dienst läuft nicht"
+
+#~ msgid "Service is starting"
+#~ msgstr "Dienst startet"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Dienst ist beendet"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Dienst wird beendet"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr ""
+#~ "Dies wird die kdump-Konfiguration durch einen Kernel-Absturz testen."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "crashkernel nicht in der Kernel-Befehlszeile konfiguriert"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (verschlüsselt)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 Stratis Pool"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 Cache"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 unbekannter Stufe"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 frei"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr ""
+#~ "Eine Ersatzfestplatte muss zuerst hinzugefügt werden, bevor diese "
+#~ "entfernt werden kann."
+
+#~ msgid "Backing device"
+#~ msgstr "Sicherungsgerät"
+
+#~ msgid "Block"
+#~ msgstr "Block"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Kapazität"
+
+#~ msgid "Content"
+#~ msgstr "Inhalt"
+
+#~ msgid "Create devices"
+#~ msgstr "Erstelle Geräte"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Gerät"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Gerätedatei"
+
+#~ msgid "Devices"
+#~ msgstr "Geräte"
+
+#~ msgid "Drives"
+#~ msgstr "Laufwerke"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Verschlüsselte Volumes müssen entsperrt werden, bevor ihre Größe geändert "
+#~ "werden kann."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Dateisystem (verschlüsselt)"
+
+#~ msgid "Filesystems"
+#~ msgstr "Dateisysteme"
+
+#~ msgid "Free"
+#~ msgstr "Verfügbar"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Geben Sie Speicherplatz in dieser Gruppe frei: Verkleinern oder löschen "
+#~ "Sie andere logische Volumen oder fügen Sie ein weiteres physisches "
+#~ "Volumen hinzu."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Inaktives Volumen"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Schlüsselserver"
+
+#~ msgid "LVM2 member"
+#~ msgstr "LVM2-Mitglied"
+
+#~ msgid "Locked devices"
+#~ msgstr "Gesperrte Geräte"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Gesperrte verschlüsselte Daten"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Modell"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS-Mounts"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "NFS-Unterstützung nicht installiert"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Es sind keine NFS-Mounts eingerichtet"
+
+#~ msgid "No devices"
+#~ msgstr "Keine Geräte"
+
+#~ msgid "No drives attached"
+#~ msgstr "Keine Laufwerke angeschlossen"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Es sind keine iSCSI-Ziele eingerichtet"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Nicht genug Platz für neue Dateisysteme"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Andere Daten"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Partitioniertes blockorientiertes Gerät"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Passphrase"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "Physische Datenträger können hier nicht geändert werden."
+
+#~ msgid "Pool"
+#~ msgstr "Pool"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Pool für dünne Mengen"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID-Level"
+
+#~ msgid "RAID member"
+#~ msgstr "RAID-Mitglied"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Entfernbares Speichergerät"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "$0 Laufwerk anzeigen"
+#~ msgstr[1] "Alle $0 Laufwerke anzeigen"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "$0 Laufwerk anzeigen"
+#~ msgstr[1] "Alle $0 Laufwerke anzeigen"
+
+#~ msgid "Show all"
+#~ msgstr "Alle anzeigen"
+
+#~ msgid "Source"
+#~ msgstr "Quelle"
+
+#~ msgid "Start pool"
+#~ msgstr "Pool starten"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Pool starten, um Dateisysteme zu sehen."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Status"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Gestoppter Stratis Pool"
+
+#~ msgid "Stratis member"
+#~ msgstr "Stratis-Mitglied"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Stratis Pool $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "Support ist installiert."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "Das RAID-Gerät muss aktiv sein, um Ersatzfestplatten hinzufügen zu können."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "Die letzte Festplatte eines RAID-Geräts kann nicht entfernt werden."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr ""
+#~ "Der letzte physische Datenträger einer Datenträgergruppe kann nicht "
+#~ "entfernt werden."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "An anderer Stelle ist nicht genügend freier Speicherplatz vorhanden, um "
+#~ "diesen physischen Datenträger zu entfernen. Wenigstens $0 mehr Freiraum "
+#~ "wird benötigt."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr ""
+#~ "Diese Diskette kann nicht entfernt werden, während das Gerät "
+#~ "wiederhergestellt wird."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr ""
+#~ "Dieses Volume muss aktiviert werden, bevor die Größe geändert werden kann."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Unerkannte Daten"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Belegung"
+
+#~ msgid "Used for"
+#~ msgstr "Verwendet für"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "VDO-Unterstützung"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO-Gerät"
+
+#~ msgid "Volume"
+#~ msgstr "Datenträger"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automatisch (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Schlüssel akzeptieren und verbinden"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Verschlüsselte Volumes können hier nicht skaliert werden."
+
+#, fuzzy
+#~| msgid "Tang keyserver"
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Tang Keyserver"
+
+#, fuzzy
+#~| msgid "New passphrase"
+#~ msgid "Use a passphrase"
+#~ msgstr "Neue Passphrase"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitmap"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Dieser Pool kann hier nicht entsperrt werden, da seine "
+#~ "Schlüsselbeschreibung nicht im erwarteten Format ist."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Bitmap umschalten"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Stellen Sie sicher, dass der Schlüsselhash vom Tang-Server mit einem der "
+#~ "folgenden übereinstimmt:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Manuell mit SSH prüfen: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Portnummer und -typ passen nicht zusammen"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Verschlüsselter Stratis Pool gesperrt"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Fehler beim Löschen der Warnung: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Alarm kann nicht abgerufen werden: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 bytes Binäredaten]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Disk I/O Spitze"
+
+#~ msgid "Event"
+#~ msgstr "Ereignis"
+
+#~ msgid "Event logs"
+#~ msgstr "Ereignisprotokolle"
+
+#~ msgid "Load spike"
+#~ msgstr "Lastspitze"
+
+#~ msgid "Memory spike"
+#~ msgstr "Speicherspitze"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Netzwerk-I/O-Lastspitze"
+
+#~ msgid "ssh key"
+#~ msgstr "SSH-Schlüssel"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Wenn diese Option aktiviert ist, wird das Dateisystem beim nächsten "
+#~ "Booten nicht eingehängt, auch wenn es vorher eingehängt wurde. Dies ist "
+#~ "nützlich, wenn das Einhängen während des Bootens nicht möglich ist, z. B. "
+#~ "wenn eine Passphrase erforderlich ist, um das Dateisystem zu entsperren, "
+#~ "das Booten aber unbeaufsichtigt ist."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Niemals beim Start einhängen"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "Auflisten von Unit-Dateien fehlgeschlagen: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "Einheit nicht gefunden"
+
+#~ msgid "Validating key"
+#~ msgstr "Schlüssel wird überprüft"
+
+#, fuzzy
+#~| msgid "Delete $0 volume"
+#~| msgid_plural "Delete $0 volumes"
+#~ msgid "Delete $0 group"
+#~ msgstr "$0 Datenträger löschen"
+
+#~ msgid "Container administrator"
+#~ msgstr "Container administrator"
+
+#~ msgid "Image builder"
+#~ msgstr "Image builder"
+
+#~ msgid "Roles"
+#~ msgstr "Rollen"
+
+#~ msgid "Server administrator"
+#~ msgstr "Server administrator"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Unix-Gruppe: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Erfolgreich auf die Tastatur kopiert"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Diagnoseberichte"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Kernel-Auszug"
+
+#~ msgid "Software Updates"
+#~ msgstr "Software-Aktualisierungen"
+
+#~ msgid "Update log"
+#~ msgstr "Aktualisierungsprotokoll"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "Das NFS-Speicherauszugsziel ist nicht als Serverpfad formatiert"
+
+#~ msgid "$0 Zone"
+#~ msgstr "$0 Zone"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Diagnosebericht erstellen"
+
+#~ msgid "Done!"
+#~ msgstr "Fertig!"
+
+#~ msgid "Download report"
+#~ msgstr "Bericht herunterladen"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Es wurde kein Archiv erstellt."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Bitte bestätigen Sie das Löschen von $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "Das erzeugte Archiv enthält Daten, die als vertraulich gelten, und dessen "
+#~ "Inhalt sollte von der Organisation, von der es stammt, überprüft werden, "
+#~ "bevor es an Dritte weitergegeben wird."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Dieses Tool erfasst Systemkonfigurations- und Diagnoseinformationen von "
+#~ "diesem System, um Probleme mit dem System zu diagnostizieren."
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Das Paket ist mit dieser Cockpit-Version nicht kompatibel"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Dieses Paket erfordert Cockpit in der Version %s oder höher"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Leistungskennzahlen"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "nur lesbar"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB gesamt"
+
+#~ msgid "Apply"
+#~ msgstr "Anwenden"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Keine Berechtigung, die Zone $0 zu entfernen"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Klicken Sie $0 erneut, um das Passwort zu verwenden."
+
+#~ msgid "Access"
+#~ msgstr "Zugriff"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "Datenträger ist FEHLERHAFT"
+
+#~ msgid "Logical Size"
+#~ msgstr "Logische Größe"
+
+#~ msgid "Security updates "
+#~ msgstr "Sicherheitsaktualisierungen "
+
+#~ msgid "Updates "
+#~ msgstr "Aktualisierungen "
+
+#~ msgid "(Optional)"
+#~ msgstr "(optional)"
+
+#~ msgid "Active since"
+#~ msgstr "Aktiv seit"
+
+#~ msgid "Affected locations"
+#~ msgstr "Betroffene Zuweisung"
+
+#~ msgid "Process"
+#~ msgstr "Prozess"
+
+#~ msgid "Remove and delete"
+#~ msgstr "Entfernen und löschen"
+
+#~ msgid "Remove and format"
+#~ msgstr "Entfernen und formatieren"
+
+#~ msgid "Remove and grow"
+#~ msgstr "Entfernen und wachsen"
+
+#~ msgid "Remove and initialize"
+#~ msgstr "Entfernen und initialisieren"
+
+#~ msgid "Remove and shrink"
+#~ msgstr "Entfernen und verkleinern"
+
+#~ msgid "Remove and stop device"
+#~ msgstr "Entfernen und Gerät anhalten"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Das Dateisystem wird von Anmeldesitzungen und Systemdiensten verwendet. "
+#~ "Durch Fortfahren werden diese gestoppt."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Das Dateisystem wird von Anmeldesitzungen verwendet. Durch Fortfahren "
+#~ "werden diese gestoppt."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Das Dateisystem wird von Systemdiensten verwendet. Durch Fortfahren "
+#~ "werden diese gestoppt."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Dieses Gerät verfügt über Dateisysteme, die derzeit verwendet werden. "
+#~ "Wenn Sie fortfahren, werden alle darauf befindlichen Dateisysteme "
+#~ "deaktiviert."
+
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Dieses Gerät wird derzeit für LVM2-Volumengruppen verwendet."
+
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Dieses Gerät wird derzeit für LVM2-Volumengruppen verwendet. Wenn Sie "
+#~ "fortfahren, wird es aus seinen Volumengruppen entfernt."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Dieses Gerät wird derzeit für RAID-Geräte verwendet."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Dieses Gerät wird derzeit für RAID-Geräte verwendet. Wenn Sie fortfahren, "
+#~ "wird es von den RAID-Geräten entfernt."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Dieses Gerät wird derzeit für Volume-Gruppen verwendet."
+
+#~ msgid "Unmount and delete"
+#~ msgstr "Aushängen und löschen"
+
+#~ msgid "Unmount and format"
+#~ msgstr "Aushängen und formatieren"
+
+#~ msgid "Unmount and grow"
+#~ msgstr "Aushängen und vergrößern"
+
+#~ msgid "Unmount and initialize"
+#~ msgstr "Aushängen und initialisieren"
+
+#~ msgid "Unmount and shrink"
+#~ msgstr "Aushängen und verkleinern"
+
+#~ msgid "Unmount and stop device"
+#~ msgstr "Aushängen und Gerät anhalten"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 von $1"
+
+#~ msgid "Blockdev"
+#~ msgstr "Blockgerät"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Körperliches Volumen von $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Mitglied des RAID-Geräts $0"
+
+#~ msgid "Menu"
+#~ msgstr "Menü"
+
+#~ msgid "VDO backing"
+#~ msgstr "VDO-Unterstützung"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Erstellen Sie einen Speicherpool"
+
+#~ msgid "Mount Options"
+#~ msgstr "Einhängeoptionen"
+
+#, fuzzy
+#~| msgid "Reset Storage Pool"
+#~ msgid "Stratis Pool"
+#~ msgstr "Setzen Sie den Speicherpool zurück"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Eine Diskette wird benötigt."
+
+#~ msgid "Disk"
+#~ msgstr "Festplatte"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Nur für ältere Programme, reduziert die Performance."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Installiere VDO-Unterstützung"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Verwenden Sie eine 512-Byte-Emulation"
+
+#~ msgid "System services"
+#~ msgstr "System Dienste"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Diagnosebericht erstellen "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr ""
+#~ "Das gleichzeitige Verbinden zu mehr als {{ limit }} Maschinen wird nicht "
+#~ "unterstützt."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "Kerberos-basiertes SSO"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Anmelden auf {{host}}"
+
+#, fuzzy
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Die Passwörter stimmen nicht überein"
+
+#, fuzzy
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr ""
+#~ "Cockpit konnte den Host {{#strong}}{{host}}{{/strong}} nicht erreichen."
+
+#, fuzzy
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Cockpit konnte sich nicht anmelden {{#strong}}{{host}}{{/strong}}. "
+#~ "{{#can_sync}}Möglicherweise möchten Sie es versuchen {{#sync_link}}"
+#~ "Benutzer synchronisieren{{/sync_link}}.{{/can_sync}} Für weitere "
+#~ "Authentifizierungsoptionen und Unterstützung bei der Fehlerbehebung "
+#~ "aktualisieren Sie bitte Cockpit-ws auf eine neuere Version."
+
+#, fuzzy
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Cockpit konnte sich nicht anmelden {{#strong}}{{host}}{{/strong}}. Um "
+#~ "dieses Gerät mit Cockpit zu verwenden, müssen Sie in der sshd-"
+#~ "Konfiguration eine der folgenden Authentifizierungsmethoden aktivieren "
+#~ "{{#strong}}{{host}}{{/strong}}:"
+
+#, fuzzy
+#~| msgid "Force change"
+#~ msgid "{{host}} key changed"
+#~ msgstr "Änderung erzwingen"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Einhängepunkt-Konfiguration löschen"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Durch das Erstellen dieser Verbindung wird die Verbindung zum Server "
+#~ "unterbrochen und die Verwaltungsbenutzeroberfläche wird nicht verfügbar."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Das Erzeugen dieser Netzwerkbrücke wird die Verbindung zum Server "
+#~ "unterbrechen und damit den Zugriff auf die Benutzeroberfläche unmöglich "
+#~ "machen."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Durch das Erstellen dieses Teams wird die Verbindung zum Server "
+#~ "unterbrochen und die Verwaltungsoberfläche wird nicht verfügbar."
+
+#~ msgid "IP settings"
+#~ msgstr "IPv4-Einstellungen"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Das Ausschalten von <b>$0</b> wird die Verbindung zum Server unterbrechen "
+#~ "und damit den Zugriff auf die Benutzeroberfläche unmöglich machen."
+
+#~ msgid "Administrator password"
+#~ msgstr "Passwort des Administrators"
+
+#~ msgid "Computer OU"
+#~ msgstr "Computer OU"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr ""
+#~ "Das Löschen eines RAID-Gerätes löscht alle darauf vorhandenen Daten."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr ""
+#~ "Das Löschen eines Datenträgerverbunds löscht alle sich darin befindenden "
+#~ "Daten."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Vorhandene Daten nicht überschreiben"
+
+#~ msgid "Erase"
+#~ msgstr "Säubern"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Datenträger $0 formatieren"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr ""
+#~ "Das Formatieren eines Speichergeräts löscht alle darauf vorhandenen Daten."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Host-Name sollte nicht in eine Domain gewechselt werden"
+
+#~ msgid "More"
+#~ msgstr "Mehr"
+
+#, fuzzy
+#~ msgid "Not joined"
+#~ msgstr "Nicht montiert"
+
+#~ msgid "One time password"
+#~ msgstr "Einmalpasswort"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 von $1 aus $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Die Stunde muss eine Zahl zwischen 0 und 23 sein"
+
+#~ msgid "Last login:"
+#~ msgstr "Letzte Anmeldung:"
+
+#, fuzzy
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Minute muss eine Zahl zwischen 0 und 59 sein"
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "z.B. \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 aktive Zonen"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 Vorkommen"
+#~ msgstr[1] "$1 Vorkommen"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Lizenziert unter:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Beim Booten entsperren"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Schreibgeschützt entsperren"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Durchsuchen sie die Logdateien mit einer Kombination von Begriffen:"
+
+#~ msgid "Show filters"
+#~ msgstr "Filter anzeigen"
+
+#~ msgid "Text"
+#~ msgstr "Text"
+
+#~ msgid "e.g."
+#~ msgstr "z.B."
+
+#~ msgid "qualifiers"
+#~ msgstr "Kennzeichner"
+
+#, fuzzy
+#~ msgid "Toggle session settings"
+#~ msgstr "Bridge Port-Einstellungen"
+
+#~ msgid "(none)"
+#~ msgstr "(keine)"
+
+#~ msgid "Create timers"
+#~ msgstr "Timer erstellen"
+
+#~ msgid "Run"
+#~ msgstr "Ausführen"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select unit state"
+#~ msgstr "Konsolentyp"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Gespeicherte Metriken aktivieren"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Gespeicherte Metriken aktivieren"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Entfernen der Passphrase erzwingen $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Wenn Tang-Show-Keys nicht verfügbar sind, führen Sie Folgendes aus:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Was ist, wenn Tang-Show-Keys nicht verfügbar sind?"
+
+#~ msgid "key slot $0"
+#~ msgstr "Schlüsselschlitz $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Etwas hat nicht funktioniert"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "Konnte keinen administrativen Zugriff erlangen."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Automatische Updates sind nicht eingerichtet"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "$0 CPU-Konfiguration"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 Netzwerk"
+#~ msgstr[1] "$0 Netzwerke"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 ist für die meisten Betriebssysteme verfügbar. Um es zu installieren, "
+#~ "suchen Sie es in der GNOME-Software oder führen Sie Folgendes aus:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 Speicher-Anpassung"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 Netzwerk"
+
+# ctx::sourcefile::/rhn/systems/details/virtualization/VirtualGuestsList.do
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPU"
+#~ msgstr[1] "$0 vCPUs"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 vCPU-Details"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 Virtuelle Netzwerkschnittstelle hinzufügen"
+
+#~ msgid "Add network interface"
+#~ msgstr "Netzwerkschnittstelle hinzufügen"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Virtuelle Netzwerkschnittstelle hinzufügen"
+
+#~ msgid "Additional"
+#~ msgstr "Mehr"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Adresse befindet sich nicht im Subnetz"
+
+#~ msgid "Always attach"
+#~ msgstr "Immer verbinden"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Durch das Einhängen der Platte wird Sie für jede VM die sie verwendet, "
+#~ "freigegeben."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Starten Sie libvirt automatisch beim Booten"
+
+#~ msgid "Autostart"
+#~ msgstr "Autostart"
+
+#~ msgid "Boot order"
+#~ msgstr "Boot-Reihenfolge"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Boot-Reihenfolge konnte nicht gespeichert werden"
+
+#~ msgid "Bus"
+#~ msgstr "Bus"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "CPU-Einstellungen konnten nicht gespeichert werden"
+
+#~ msgid "CPU type"
+#~ msgstr "Prozessor-Typ"
+
+#~ msgid "Change firmware"
+#~ msgstr "Firmware ändern"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Änderungen werden nach dem Herunterfahren der VM wirksam"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Betriebssystem auswählen"
+
+#, fuzzy
+#~| msgid ""
+#~| "Clicking \"Launch Remote Viewer\" will download a .vv file and launch $0."
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "Klicken Sie auf \"Remote Viewer starten\", um eine .vv-Datei "
+#~ "herunterzuladen und zu starten $0."
+
+#~ msgid "Clone"
+#~ msgstr "Klonen"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Bild kann nicht geladen werden"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Aktion bestätigen"
+
+#~ msgid "Connect"
+#~ msgstr "Verbinden"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "Mit einer Anwendung für die folgenen Protokolle verbinden"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Verbindung zum Virtualisierungsdienst herstellen"
+
+#~ msgid "Connection"
+#~ msgstr "Verbindung"
+
+#~ msgid "Console"
+#~ msgstr "Konsole"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Kerne pro Socket"
+
+#, fuzzy
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Konnte den Storage Pool nicht zurücksetzen"
+
+#~ msgid "Crashed"
+#~ msgstr "Abgestürzt"
+
+#~ msgid "Create VM"
+#~ msgstr "VM erstellen"
+
+#, fuzzy
+#~| msgid "Create partition on $0"
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Partition auf $0 anlegen"
+
+# translation auto-copied from project Drools Workbench, version 6.0.0,
+# document org.drools/drools-wb-guided-rule-editor-
+# client/org/drools/workbench/screens/guided/rule/client/resources/i18n/Constants,
+# author jdimanos
+#~ msgid "Create new"
+#~ msgstr "Neu erstellen"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Neue Virtuelle Maschine erstellen"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Erstelle Virtuelles Netzwerk"
+
+#~ msgid "Creating VM"
+#~ msgstr "VM erstellen"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "VM Installation erstellen"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Das Erstellen von VM $0 ist fehlgeschlagen"
+
+#~ msgid "Creation time"
+#~ msgstr "Zeit der Erstellung"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Strg + Alt +$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "aktuelle Zuweisung"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Benutzerdefinierte Firmware: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP Bereich"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Verknüpfte Speicherdateien löschen:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Lösche Speicher-Pool $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Das Volume in diesem Pool löschen"
+
+#, fuzzy
+#~| msgid "Desktop"
+#~ msgid "Desktop viewer"
+#~ msgstr "Desktop"
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr ""
+#~ "Verbindung zur seriellen Konsole getrennt Klicken Sie auf die "
+#~ "Schaltfläche 'Neu verbinden'."
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Festplatte konnte nicht angeschlossen werden"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Datenträger konnte nicht erstellt werden"
+
+#, fuzzy
+#~| msgid "Device file"
+#~ msgid "Disk image file"
+#~ msgstr "Gerätedatei"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Festplatten-Einstellungen konnten nicht gespeichert werden"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "Nur-Datenträger-Schnappschuss"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "Domain ist abgestürzt"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "Domain ist durch Resource blockiert"
+
+#~ msgid "Download an OS"
+#~ msgstr "Ein Betriebssystem herunterladen"
+
+#, fuzzy
+#~| msgid "dying"
+#~ msgid "Dying"
+#~ msgstr "im Sterben"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Emulierte Maschine"
+
+#~ msgid "End"
+#~ msgstr "Ende"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Das Ende sollte nicht leer sein"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Existierenes Festplatten Abbild auf dem Host Dateisystem"
+
+#~ msgid "Expand"
+#~ msgstr "Erweitern"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Firmware konnte nicht geändert werden"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "IP-Adresse der Schnittstellen in $0 können nicht bezogen werden"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr ""
+#~ "Senden der Tastenkombination Strg+Alt+$0 an die VM $1 fehlgeschlagen"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr ""
+#~ "Es sollte weniger als die maximale Anzahl virtueller CPUs aktiviert sein."
+
+# translation auto-copied from project Customer Portal Translations, version
+# 006_DrupalPO, document interface, author hpeters
+#~ msgid "File"
+#~ msgstr "Datei"
+
+#, fuzzy
+#~ msgid "Filter by name"
+#~ msgstr "Filtere nach Name oder Beschreibung"
+
+#~ msgid "Firmware"
+#~ msgstr "Firmware"
+
+#~ msgid "Force shut down"
+#~ msgstr "Herunterfahren erzwingen"
+
+#~ msgid "Forward mode"
+#~ msgstr "Weiterleitungs-Modus"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Weiterleitungs-Modus"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Automatisch generieren"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Zusätzlichen Einstellungen verstecken"
+
+#~ msgid "Host device"
+#~ msgstr "Host-Gerät"
+
+#~ msgid "Host name"
+#~ msgstr "Rechnername"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Host sollte nicht leer sein"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Hypervisor-Details"
+
+#~ msgid "IP configuration"
+#~ msgstr "IP-Konfiguration"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 und IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4-Netzwerk"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv4-Netzwerk sollte nicht leer sein"
+
+#~ msgid "IPv4 only"
+#~ msgstr "Nur IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6-Adresse"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6-Netzwerk"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6-Netzwerk sollte nicht leer sein"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Nur IPv6"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Die VM sofort starten"
+
+#~ msgid "Import"
+#~ msgstr "Importieren"
+
+#~ msgid "Import VM"
+#~ msgstr "VM importieren"
+
+#, fuzzy
+#~ msgid "Import a virtual machine"
+#~ msgstr "Virtuelle Maschine importieren"
+
+#, fuzzy
+#~| msgid "Installation source"
+#~ msgid "Installation source"
+#~ msgstr "Installationsquelle"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Installationsquelle sollte nicht leer sein"
+
+#~ msgid "Installation type"
+#~ msgstr "Installationstyp"
+
+#~ msgid "Interface type"
+#~ msgstr "Art der Schnittstelle"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Ungültige IPv4-Maske oder Prefix-Länge"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Ungültige IPv6-Adresse"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Ungültiger IPv6-Prefix"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Ungültiger Dateiname"
+
+#, fuzzy
+#~| msgid "Launch Remote Viewer"
+#~ msgid "Launch remote viewer"
+#~ msgstr "Starten Sie den Remote Viewer"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr ""
+#~ "Lassen sie das Passwort leer, wenn sie keinen Root Account erstellen "
+#~ "möchte"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "Lassen sie das Passwort leer, wenn sie keinen Benutzer Account erstellen "
+#~ "möchte"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr ""
+#~ "Lassen sie das Passwort leer, wenn sie keinen Root Account erstellen "
+#~ "möchte"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt hat kein UEFI/OVMF Firmware-Abbild auf dem Host-System gefunden"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt oder der Hypervisor unterstützen UEFI nicht"
+
+#~ msgid "Loading resources"
+#~ msgstr "Lade Ressourcen"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Virtuelle Maschinen verwalten"
+
+#~ msgid "Manual connection"
+#~ msgstr "Manuelle Verbindung"
+
+#, fuzzy
+#~| msgid ""
+#~| "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~| "between 1 and $0"
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Maximale Anzahl der für das Gastbetriebssystem zugewiesenen virtuellen "
+#~ "CPUs, die zwischen 1 und 1 liegen muss $0"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Maximale Anzahl der für das Gastbetriebssystem zugewiesenen virtuellen "
+#~ "CPUs, die zwischen 1 und 1 liegen muss $0"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Speicher konnte nicht gespeichert werden"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "Speicher darf nicht 0 sein"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Modelltyp"
+
+#, fuzzy
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Der Name darf nicht das Zeichen '$0' enthalten."
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Name darf nicht leer sein"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Name sollte nicht leer sein"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Netzwerk $0 konnte nicht aktiviert werden"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Netzwerk $0 konnte nicht deaktiviert werden"
+
+#~ msgid "Network file system"
+#~ msgstr "Netzwerk-Dateisystem"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Netzwerkadaptereinstellungen konnten nicht gespeichert werden"
+
+#~ msgid "Networks"
+#~ msgstr "Netzwerke"
+
+#~ msgid "New volume name"
+#~ msgstr "Neuer Volume-Name"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr ""
+#~ "Auf diesem Host ist weder eine VM definiert, noch wird eine VM ausgeführt"
+
+#, fuzzy
+#~| msgid "No description available"
+#~ msgid "No connection available"
+#~ msgstr "Keine Beschreibung verfügbar"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Keine Festplatten für diese VM definiert"
+
+#~ msgid "No network devices"
+#~ msgstr "Keine Netzwerkgeräte"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Keine Netzwerkschnittstellen für diese VM definiert"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "Kein Netzwerk auf diesem Host definiert"
+
+#~ msgid "No networks available"
+#~ msgstr "Keine Netzwerke verfügbar"
+
+#, fuzzy
+#~ msgid "No parent"
+#~ msgstr "Parent"
+
+#, fuzzy
+#~| msgid "No disks defined for this VM"
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Keine Festplatten für diese VM definiert"
+
+#, fuzzy
+#~| msgid "No storage"
+#~ msgid "No state"
+#~ msgstr "Kein Speicher"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "Auf diesem Host ist kein Speicherpool definiert"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Keine Speicherpools vorhanden"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "Für dieses Speicherpool sind keine Speichervolumes definiert"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Keine virtuellen Netzwerke"
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Getrennt (Isoliertes Netzwerk)"
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Kann nur bearbeitet werden wenn Gast ausgeschaltet ist"
+
+#~ msgid "Open"
+#~ msgstr "Öffnen"
+
+#~ msgid "Operating system"
+#~ msgstr "Betriebssystem"
+
+#, fuzzy
+#~ msgid "Parent snapshot"
+#~ msgstr "Snapshot erzeugen"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Pfad zur ISO-Datei im Dateisystem des Hosts"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Pfad zur ISO-Datei im Dateisystem des Hosts"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Pfad zur ISO-Datei im Dateisystem des Hosts"
+
+#, fuzzy
+#~| msgid "Pause"
+#~ msgid "Paused"
+#~ msgstr "Pause"
+
+#, fuzzy
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Konnte den Storage Pool nicht zurücksetzen"
+
+#, fuzzy
+#~ msgid "Please choose a volume"
+#~ msgstr "Bitte geben Sie den neuen Datenträgernamen ein"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Bitte geben Sie den neuen Datenträgernamen ein"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Starten Sie die virtuelle Maschine, um auf die Konsole zuzugreifen."
+
+#~ msgid "Plug"
+#~ msgstr "Plug"
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr ""
+#~ "Bevorzugte Anzahl von Sockets , die dem Gast zugänglich gemacht werden "
+#~ "sollen."
+
+#~ msgid "Prefix"
+#~ msgstr "Präfix"
+
+#~ msgid "Product"
+#~ msgstr "Produkt"
+
+#, fuzzy
+#~ msgid "Profile"
+#~ msgstr "Profil ändern"
+
+#~ msgid "Protocol"
+#~ msgstr "Protokoll"
+
+#~ msgid "Remote URL"
+#~ msgstr "Remote-URL"
+
+#, fuzzy
+#~| msgid "Hypervisor details"
+#~ msgid "Remote viewer details"
+#~ msgstr "Hypervisor-Details"
+
+#, fuzzy
+#~ msgid "Revert"
+#~ msgstr "Nie"
+
+#, fuzzy
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Snapshot erzeugen"
+
+#~ msgid "Root password"
+#~ msgstr "Root-Passwort"
+
+#~ msgid "Route to $0"
+#~ msgstr "Route zu $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Installation im Hintergrund ausführen"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Starten wenn Host hochfährt"
+
+#, fuzzy
+#~| msgid "SPICE TLS port:"
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE TLS-Port:"
+
+#, fuzzy
+#~| msgid "SPICE address:"
+#~ msgid "SPICE address"
+#~ msgstr "SPICE-Adresse:"
+
+#, fuzzy
+#~| msgid "SPICE port:"
+#~ msgid "SPICE port"
+#~ msgstr "SPICE port:"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select console type"
+#~ msgstr "Konsolentyp"
+
+#~ msgid "Send key"
+#~ msgstr "Schlüssel senden"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Nicht maskierbare Unterbrechung senden"
+
+#~ msgid "Serial console"
+#~ msgstr "Serielle Konsole"
+
+#~ msgid "Set manually"
+#~ msgstr "Manuell einstellen"
+
+#~ msgid "Show additional options"
+#~ msgstr "Zusätzlicher Optionen anzeigen"
+
+#, fuzzy
+#~ msgid "Shut off"
+#~ msgstr "Ausgeschaltet"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr ""
+#~ "Fahren Sie die VM herunter, um die Firmware-Konfiguration zu bearbeiten"
+
+#, fuzzy
+#~ msgid "Shutting down"
+#~ msgstr "Herunterfahren"
+
+#, fuzzy
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Datenträger konnte nicht erstellt werden"
+
+#~ msgid "Source format"
+#~ msgstr "Quellenformat"
+
+#~ msgid "Source path"
+#~ msgstr "Quellpfad"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Quellpfad sollte nicht leer sein"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "Die Quelle sollte mit dem http,ftp oder nfs Protokoll starten"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Starten Sie libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Starten Sie den Pool, wenn der Host bootet"
+
+#~ msgid "Startup"
+#~ msgstr "Anlaufen"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Speicherpool konnte nicht erstellt werden"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Speicherpoolname"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "Angehalten (PM)"
+
+#~ msgid "Target path"
+#~ msgstr "Zielpfad"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Zielpfad sollte nicht leer sein"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr "Die VM ist in Betrieb und wird vor dem Löschen abgebrochen."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "Um das Gerät zu entfernen muss die VM laufen oder ausgeschaltet sein"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "Das Verzeichnis auf dem Server, der exportiert wird"
+
+#~ msgid "The pool is empty"
+#~ msgstr "Der Pool ist leer"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "Der Speicher-Pool konnte nicht entfernt werden"
+
+#~ msgid "Threads per core"
+#~ msgstr "Fäden pro Kern"
+
+#~ msgid "Type ID"
+#~ msgstr "Typ ID"
+
+#~ msgid "Unique name"
+#~ msgstr "Einzigartiger Name"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Unbekannte Firmware"
+
+#~ msgid "Unplug"
+#~ msgstr "Ziehen Sie den Stecker heraus"
+
+#~ msgid "Url"
+#~ msgstr "Url"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "VCPU-Einstellungen konnten nicht gespeichert werden"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "VM $0 existiert bereits"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "VM $0 konnte nicht zum Neustart gezwungen werden"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "VM $0 konnte nicht zum Herunterfahren gezwungen werden"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "VM $0 konnte nicht gelöscht werden"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "VM$0 konnte nicht installiert werden"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "VM $0 konnte nicht neugestartet werden"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "VM $0 konnte nicht fortgesetzt werden"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "NMI konnte nicht an VM $0 gesendet werden"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "VM $0 konnte nicht heruntergefahren werden"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "VM $0 konnte nicht gestartet werden"
+
+#, fuzzy
+#~ msgid "VM state"
+#~ msgstr "Status"
+
+#, fuzzy
+#~| msgid "VNC TLS port:"
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC-TLS-Port:"
+
+#, fuzzy
+#~| msgid "VNC address:"
+#~ msgid "VNC address"
+#~ msgstr "VNC-Adresse:"
+
+#, fuzzy
+#~| msgid "console"
+#~ msgid "VNC console"
+#~ msgstr "Konsole"
+
+#, fuzzy
+#~| msgid "VNC port:"
+#~ msgid "VNC port"
+#~ msgstr "VNC-Port:"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Virtuelle Maschinen"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Virtuelle Maschinen"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Verwaltung Virtueller Maschinen"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Virtuelles Netzwerk konnte nicht erstellt werden"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Der Virtualisierungsdienst (libvirt) ist nicht aktiv"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Beschreibbar"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Beschreibbar und geteilt"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "Bitte wählen sie das passende Betriebssystem aus"
+
+#~ msgid "cdrom"
+#~ msgstr "CD-ROM"
+
+#~ msgid "disabled"
+#~ msgstr "Aus"
+
+#~ msgid "down"
+#~ msgstr "runter"
+
+#~ msgid "enabled"
+#~ msgstr "An"
+
+#~ msgid "ethernet"
+#~ msgstr "Ethernet"
+
+#~ msgid "host device"
+#~ msgstr "Host-Gerät"
+
+#, fuzzy
+#~| msgid "to host path"
+#~ msgid "host passthrough"
+#~ msgstr "zum Host-Pfad"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "iSCSI initiator IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "Nein"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "weitergeleitete Geräte"
+
+#~ msgid "server"
+#~ msgstr "server"
+
+# translation auto-copied from project RHN Satellite UI, version 5.5, document
+# java/code/src/com/redhat/rhn/frontend/strings/jsp/StringResource
+#~ msgid "up"
+#~ msgstr "hoch"
+
+#~ msgid "vCPU count"
+#~ msgstr "Anzahl vCPUs"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU maximum"
+
+# ctx::sourcefile::/rhn/systems/details/virtualization/VirtualGuestsList.do
+#~ msgid "vCPUs"
+#~ msgstr "vCPUs"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostbenutzer"
+
+#, fuzzy
+#~| msgid "Read more..."
+#~ msgid "view more..."
+#~ msgstr "Mehr lesen..."
+
+#, fuzzy
+#~| msgid ""
+#~| "virt-install package needs to be installed on the system in order to "
+#~| "create new VMs"
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Das \"virt-install\" Paket muss auf dem System installiert werden, um "
+#~ "neue VMs zu erstellen"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Das \"virt-install\" Paket muss auf dem System installiert werden, um "
+#~ "neue VMs zu erstellen"
+
+#, fuzzy
+#~| msgid ""
+#~| "virt-install package needs to be installed on the system in order to "
+#~| "create new VMs"
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Das \"virt-install\" Paket muss auf dem System installiert werden, um "
+#~ "neue VMs zu erstellen"
+
+#~ msgid "vm"
+#~ msgstr "VM"
+
+#~ msgid "Local install media"
+#~ msgstr "Lokales Installationsmedium"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#, fuzzy
+#~ msgid "Please set a root password"
+#~ msgstr "Benutzername oder Passwort falsch"
+
+#~ msgid "21st"
+#~ msgstr "21."
+
+#~ msgid "22nd"
+#~ msgstr "22."
+
+#~ msgid "23rd"
+#~ msgstr "23."
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Nach"
+
+#~ msgid "Friday"
+#~ msgstr "Freitag"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Stunde : Minute"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "Die Stunde muss eine Zahl zwischen 0 und 23 sein"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Ungültiges Datumformat."
+
+#~ msgid "Invalid number."
+#~ msgstr "Ungültige Nummer."
+
+#~ msgid "Monday"
+#~ msgstr "Montag"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Stündlich wiederholen"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Jährlich wiederholen"
+
+#~ msgid "Saturday"
+#~ msgstr "Samstag"
+
+#~ msgid "Sunday"
+#~ msgstr "Sonntag"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Dieser Tag ist nicht in allen Monaten vorhanden.<br> Die Aktion wird nur "
+#~ "in Monaten mit 31 Tagen ausgeführt."
+
+#~ msgid "Thursday"
+#~ msgstr "Donnerstag"
+
+#~ msgid "Tuesday"
+#~ msgstr "Dienstag"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 Aktualisierung"
+#~ msgstr[1] "$0 Aktualisierungen"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 Sicherheitsupdate"
+#~ msgstr[1] "$1 Sicherheitsupdates"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Speichergeräte verwalten"
+
+#~ msgid "Restart now"
+#~ msgstr "Jetzt neustarten"
+
+#~ msgid "Configure"
+#~ msgstr "Konfigurieren"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Netzwerk"
+
+#, fuzzy
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "Datei oder Verzeichnis nicht vorhanden"
+
+#~ msgid "No updates pending"
+#~ msgstr "Keine Updates ausstehend"
+
+#, fuzzy
+#~| msgid "ssh server is empty"
+#~ msgid "This directory is empty"
+#~ msgstr "SSH-Server ist leer"
+
+#~ msgid "undefined"
+#~ msgstr "nicht definiert"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Falscher Host-Schlüssel"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "Die Authentizität von Host {{#strong}}{{host}}{{/strong}} kann nicht "
+#~ "bestätigt werden. Sind Sie sicher, dass Sie fortfahren wollen?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "Der Schlüssel von {{#strong}}{{host}}{{/strong}} stimmt nicht mit dem "
+#~ "zuletzt benutzten Schlüssel überein. Falls die Maschine nicht kürzlich "
+#~ "ausgetauscht wurde, ist ein Angriff auf die Verbindung zur Maschine sehr "
+#~ "wahrscheinlich."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Unbekannter Host-Schlüssel"
+
+#~ msgid "Address:"
+#~ msgstr "Adresse:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Mindestens $0 Datenträger sind nötig."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr ""
+#~ "Verbinden Sie sich mit einer beliebigen SPICE- oder VNC-Viewer-Anwendung."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Laden Sie das MSI von herunter $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Grafikkonsole (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Grafikkonsole im Desktop Viewer"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Keine Konsole für diese virtuelle Maschine definiert."
+
+#~ msgid "SPICE"
+#~ msgstr "WÜRZEN"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid "Entry"
+#~ msgstr "Eintrag"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Ihr Browser wird die Verbindung trennen, der Update-Vorgang wird dadurch "
+#~ "jedoch nicht beeinflusst. Sie können die Verbindung in wenigen "
+#~ "Augenblicken wiederherstellen, um den Fortschritt fortzusetzen."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "und starten Sie die Maschine automatisch neu."
+
+#~ msgid "Dashboard"
+#~ msgstr "Dashboard"
+
+#, fuzzy
+#~ msgid "Description input text"
+#~ msgstr "Beschreibung"
+
+#~ msgid "FAILED"
+#~ msgstr "KAPUTT"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Verbindung verloren."
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Netzwerk"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Bitte geben Sie die neue Volumengröße ein"
+
+#~ msgid "Servers"
+#~ msgstr "Server"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "Sie sind derzeit direkt mit diesem Server verbunden und können ihn "
+#~ "deshalb nicht löschen."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bits"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 bytes"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 bytes"
+
+#~ msgid "Bond settings "
+#~ msgstr "Bond Einstellungen "
+
+#~ msgid "IP settings "
+#~ msgstr "IP Einstellungen "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Es trat ein unerwarteter interner Fehler auf. <br/><br/>Sie können "
+#~ "Versuchen, Cockpit durch Aktualisieren der Seite neu zu starten. Die "
+#~ "Javascript Konsole enthält Details zum Fehler (<b>Strg-Umschalt-J</b> in "
+#~ "den meisten Browsern)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "Die VM ist abgestürzt."
+
+#~ msgid "The VM is down."
+#~ msgstr "Die VM ist ausgeschaltet."
+
+#~ msgid "The VM is going down."
+#~ msgstr "Die VM wird ausgeschaltet."
+
+#~ msgid "The VM is idle."
+#~ msgstr "Die VM ist untätig."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr ""
+#~ "Die VM befindet sich gerade im Sterben (Herunterfahren oder Absturz ist "
+#~ "nicht abgeschlossen)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "Die VM ist pausiert."
+
+#~ msgid "The VM is running."
+#~ msgstr "Die VM läuft."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "Die VM ist vom Gast Power Management suspendiert."
+
+#~ msgid "inactive"
+#~ msgstr "Inaktiv"
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Lassen Sie das Feld leer, um sich als aktuell angemeldeter Benutzer mit "
+#~ "diesem System zu verbinden. Wenn Sie einen anderen Benutzernamen "
+#~ "eingeben, wird dieser Benutzer immer verwendet, wenn Sie eine Verbindung "
+#~ "zu diesem System herstellen."
+
+#~ msgid "2 min"
+#~ msgstr "2 Minuten"
+
+#~ msgid "3 min"
+#~ msgstr "3 Minuten"
+
+#~ msgid "4 min"
+#~ msgstr "4 Minuten"
+
+#~ msgid "CPU graph"
+#~ msgstr "CPU-Diagramm"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "CPU status"
+
+#~ msgid "Cached"
+#~ msgstr "Zwischengespeichert"
+
+#~ msgid "Graphs"
+#~ msgstr "Kurve"
+
+#~ msgid "I/O wait"
+#~ msgstr "I / O Warten"
+
+#~ msgid "Kernel"
+#~ msgstr "Kernel"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Speicher"
+
+#~ msgid "Network traffic"
+#~ msgstr "Netzwerk-Verkehr"
+
+#~ msgid "Nice"
+#~ msgstr "Nice-Wert"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Benutzer synchronisieren"
+
+#~ msgid "View graphs"
+#~ msgstr "Diagramme anzeigen"
+
+#~ msgid "idle"
+#~ msgstr "im Leerlauf"
+
+#~ msgid "paused"
+#~ msgstr "pausiert"
+
+#~ msgid "running"
+#~ msgstr "Läuft"
+
+#~ msgid "shut off"
+#~ msgstr "Ausgeschaltet"
+
+#~ msgid "shutdown"
+#~ msgstr "Herunterfahren"
+
+#~ msgid "Add a new host"
+#~ msgstr "Neuen Host hinzufügen"
+
+#~ msgid "Available"
+#~ msgstr "Verfügbar"
+
+#, fuzzy
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Geben Sie die IP Adresse oder den Hostnamen an"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Netzwerkadapter"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Das Verbinden zu einem Host mit alternativen Benutzernamen oder auf "
+#~ "anderen Ports wird von dieser Version von cockpit-ws nicht unterstützt"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Um einen anderen Port zu benutzen müssen Sie cockpit-ws aktualisieren."
+
+#~ msgid "$0 template"
+#~ msgstr "$0 template"
+
+#~ msgid "Instantiate"
+#~ msgstr "Instanzieren"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 mal geteilt"
+
+#~ msgid "All In One"
+#~ msgstr "Alles in einem"
+
+#~ msgid "Always"
+#~ msgstr "Immer"
+
+#~ msgid "Author"
+#~ msgstr "Autor"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Fehlerhafte Daten an passwd1 Mechanismus übergeben"
+
+#~ msgid "CPU priority"
+#~ msgstr "CPU Priorität"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Verbindung zu Docker kann nicht hergestellt werden"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Ressourcenlimits anpassen"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Ressourcenlimits anpassen"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr ""
+#~ "Cockpit konnte sich bei {{#strong}}{{host}}{{/strong}} nicht einloggen."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit konnte sich nicht anmelden {{#strong}}{{host}}{{/strong}}. Sie "
+#~ "können Ihre Authentifizierungsinformationen unten ändern. {{#can_sync}}"
+#~ "Sie können es vorziehen {{#sync_link}}Konten und Passwörter "
+#~ "synchronisieren{{/sync_link}}.{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Kombinierte Speichernutzung"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Kombinierte Verwendung von $0 CPU-Kern"
+#~ msgstr[1] "Kombinierte Verwendung von $0 CPU-Kernen"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "Befehl darf nicht leer sein."
+
+#~ msgid "Command:"
+#~ msgstr "Befehl:"
+
+#~ msgid "Commit"
+#~ msgstr "Festschreiben"
+
+#~ msgid "Commit image"
+#~ msgstr "Commit image"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Verbinde zu Docker"
+
+#~ msgid "Container name"
+#~ msgstr "Container name"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Der Container ist zur Zeit als nicht als in Ausführung gekennzeichnet, "
+#~ "das reguläre Anhalten schlug jedoch fehlt."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Der Container läuft zur Zeit."
+
+#~ msgid "Container:"
+#~ msgstr "Container:"
+
+#~ msgid "Containers"
+#~ msgstr "Container"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Container"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Benutzergruppen konnten nicht geändert werden"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Passwort konnte nicht geändert werden"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Verbindung zur Maschine konnte nicht hergstellt werden"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Neue Benutzer konnten nicht angelegt werden"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Lokale Benutzer konnten nicht gelistet werden"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Benutzer konnten nicht gelistet werden"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Benutzerdaten konnten nicht geladen werden"
+
+#~ msgid "Created:"
+#~ msgstr "Erstellt:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Docker-Container"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr ""
+#~ "Docker ist auf Ihrem System entweder nicht installiert oder nicht "
+#~ "aktiviert"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Alias duplizieren"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Port duplizieren"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Wenn Sie hier ein anderes Passwort eingeben, müssen Sie es jedes Mal "
+#~ "erneut eingeben, wenn Sie sich erneut mit diesem System verbinden"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys, author hedda
+#~ msgid "Environment"
+#~ msgstr "Umgebung"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Fehler beim Laden von Benutzern: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Fehlermeldung von Docker:"
+
+#~ msgid "Everything"
+#~ msgstr "Alles"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Beendet $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Legen Sie Containerports frei"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Start von Docker fehlgeschlagen: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Docker-Bereich konnte nicht angehalten werden: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Holen Sie sich ein neues Bild"
+
+#~ msgid "IP address:"
+#~ msgstr "IP-Adresse:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "IP-Präfixlänge:"
+
+#~ msgid "Id"
+#~ msgstr "ID"
+
+#~ msgid "Id:"
+#~ msgstr "Id:"
+
+#~ msgid "Image"
+#~ msgstr "Bild"
+
+#~ msgid "Image $0"
+#~ msgstr "Bild $0"
+
+#~ msgid "Image search"
+#~ msgstr "Bildersuche"
+
+#~ msgid "Image:"
+#~ msgstr "Bild:"
+
+#~ msgid "Images"
+#~ msgstr "Abbilder"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Abbilder"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Bilder und laufende Container"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Um Benutzer zu synchronisieren, müssen Sie sich bei anmelden {{#strong}}"
+#~ "{{host}}{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Ungültiger Port"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Kerberos Ticket"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Lassen Sie das Feld leer, um sich als aktuell angemeldeter Benutzer mit "
+#~ "diesem System zu verbinden{{#default_user}} ({{default_user}}){{/"
+#~ "default_user}}. Wenn Sie einen anderen Benutzernamen eingeben, wird "
+#~ "dieser Benutzer immer für die Verbindung zu diesem System verwendet."
+
+#~ msgid "Link to another container"
+#~ msgstr "Verbindung zu einem anderen Container"
+
+#~ msgid "Links:"
+#~ msgstr "Links:"
+
+#~ msgid "Login Password"
+#~ msgstr "Passwort"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC-Adresse:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Speicherlimite"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Mounten Sie Container-Volumes"
+
+#~ msgid "No container specified"
+#~ msgstr "Kein Container angegeben"
+
+#~ msgid "No containers"
+#~ msgstr "Keine Container"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Keine Container, die dem aktuellen Filter entsprechen"
+
+#~ msgid "No images"
+#~ msgstr "Keine Bilder"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Keine Bilder, die dem aktuellen Filter entsprechen"
+
+#~ msgid "No results for $0"
+#~ msgstr "Keine Ergebnisse für $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Keine Bilder, die dem aktuellen Filter entsprechen"
+
+#~ msgid "No running containers"
+#~ msgstr "Keine laufenden Container"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Keine laufenden Container, die dem aktuellen Filter entsprechen"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Keine Berechtigung, auf diesem System Docker zu verwenden"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "Bei einem Fehlschlag versuchen Sie es erneut $0 Zeit"
+#~ msgstr[1] "Bei einem Fehlschlag versuchen Sie es erneut $0 mal"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Bitte bestätigen Sie das erzwungene Löschen von $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Bitte versuchen Sie es mit einem anderen Begriff"
+
+#~ msgid "Ports:"
+#~ msgstr "Ports:"
+
+# translation auto-copied from project KIE Workbench - Common, version 6.0.0,
+# document org.kie.workbench.screens/kie-wb-common-project-imports-editor-
+# client/org/kie/workbench/common/screens/projectimportsscreen/client/resources/i18n/ProjectConfigScreenConstants,
+# author jdimanos
+#~ msgid "Problems"
+#~ msgstr "Probleme"
+
+#~ msgid "ReadOnly"
+#~ msgstr "ReadOnly"
+
+#~ msgid "ReadWrite"
+#~ msgstr "ReadWrite"
+
+#~ msgid "Repository"
+#~ msgstr "Repository"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Neustartrichtlinie:"
+
+#~ msgid "Retries:"
+#~ msgstr "Wiederholungen:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Mein Passwort für Remote-Verbindungen verwenden"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Wählen Sie die Nutzer aus, die mit {{#strong}}{{host}}{{/strong}} "
+#~ "synchronisiert werden sollen"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Umgebungsvariablen für Container setzen"
+
+#~ msgid "Severity:"
+#~ msgstr "Schwere:"
+
+#~ msgid "Show all containers"
+#~ msgstr "Zeige alle Container an"
+
+#~ msgid "Start Docker"
+#~ msgstr "Docker starten"
+
+#~ msgid "State:"
+#~ msgstr "Status:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Synchronisieren"
+
+#~ msgid "Tag"
+#~ msgstr "Tag"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#~ msgid "Tags"
+#~ msgstr "Tags"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "Die folgenden Container hängen von diesem Image ab und werden unbrauchbar."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Dieses Image existiert nicht."
+
+#~ msgid "Type a password"
+#~ msgstr "Geben Sie ein Passwort ein"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Zum Filtern tippen. . ."
+
+#~ msgid "Unless stopped"
+#~ msgstr "Wenn nicht gestoppt"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Nicht unterstützter Setup-Mechanismus"
+
+#~ msgid "Up since $0"
+#~ msgstr "Up seit $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Genutzt von Containern"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Benutze verfügbare Zugangsdaten"
+
+#~ msgid "Volumes"
+#~ msgstr "Datenträger"
+
+#~ msgid "Volumes:"
+#~ msgstr "Volumen:"
+
+#~ msgid "With terminal"
+#~ msgstr "Mit Konsole"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Sie sind mit {{#strong}}{{host}}{{/strong}} verbunden, zum "
+#~ "Synchronisieren von Nutzern sind jedoch Administratorrechte erforderlich."
+
+#~ msgid "default"
+#~ msgstr "Vorgabe"
+
+#~ msgid "key"
+#~ msgstr "Schlüssel"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "Suche nach Name, Namensraum oder Beschreibung"
+
+#~ msgid "shares"
+#~ msgstr "Freigaben"
+
+#~ msgid "to host port"
+#~ msgstr "An Port"
+
+# translation auto-copied from project Cloudforms Cloud Engine User Guide,
+# version 1.0, document topics/References/Cloud/AppXML/CF_AppXML_values,
+# author hpeters
+#~ msgid "value"
+#~ msgstr "Wert"
+
+#~ msgid "Master"
+#~ msgstr "Master"
+
+#, fuzzy
+#~| msgid "Password"
+#~ msgid "Password for $0"
+#~ msgstr "Passwort"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr ""
+#~ "Der Benutzer <b>$0</b> hat keine Rechte, um Server zu administrieren"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Konten"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "Der Benutzer <b>$0</b> hat keine Rechte, Konten zu verändern"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Root-Konto konnte nicht gelöscht werden"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Root-Konto konnte nicht umbenannt werden"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "Keine Berechtigung, die Zone hinzuzufügen"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "Keine Berechtigung, den Dienst der Zone $0 hinzuzufügen"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "Keine Berechtigung, den Dienst $0 zu entfernen"
+
+#~ msgid "Account Settings"
+#~ msgstr "Kontoeinstellungen"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Maschine zum Dashboard hinzufügen"
+
+#~ msgid "Machines"
+#~ msgstr "Maschinen"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "Anmeldung hat Administrator-Rechte"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Das Kennwort kann nicht für privilegierte Aufgaben oder zum Herstellen "
+#~ "einer Verbindung mit anderen Computern verwendet werden"
+
+#~ msgid "Privileged"
+#~ msgstr "Privilegiert"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Mein Passwort für Administrator-Aufgaben und zum Verbinden zu anderen "
+#~ "Maschinen verwenden"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "Der Benutzer <b>$0</b> darf das Profil nicht ändern"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "Der Benutzer <b>$0</b> hat keine Rechte, Speicher zu verwalten."
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr ""
+#~ "Der User <b>$0</b> hat keine Rechte, die Netzwerkeinstellungen zu "
+#~ "verändern."
+
+#, fuzzy
+#~| msgid "Required by "
+#~ msgid "Required by $0"
+#~ msgstr "Benötigt von "
+
+#~ msgid "Automatic Startup"
+#~ msgstr "Autostart"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Letzter Auslöser"
+
+#~ msgid "Next Run"
+#~ msgstr "Nächster Run"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "Der Benutzer <b>$0</b> hat keine Rechte zum Anlegen von Timern"
+
+#~ msgid "Connecting"
+#~ msgstr "Verbindungsaufbau"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Über Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (gemeinsam mit dem Betriebssystem freigegeben)"
+
+#, fuzzy
+#~| msgid "1 Vulnerability"
+#~| msgid_plural "$0 Vulnerabilities"
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "1 Verwundbarkeit"
+#~ msgstr[1] "$0 Verwundbarkeiten"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Zusätzlichen Speicher hinzufügen"
+
+#~ msgid "Add Storage"
+#~ msgstr "Speicher hinzufügen"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Zusätzlicher Speicher"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Die ausgewählten Datenträger werden zum Storage Pool hinzugefügt. Dabei "
+#~ "werden sämtliche Daten auf den ausgewählten Datenträgern gelöscht."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Speicher konfigurieren ..."
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Es konnten nicht alle Festplatten hinzugefügt werden"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Container löschen und Speicherpool zurücksetzen"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr "Informationen zum Docker-Speicherpool sind nicht verfügbar."
+
+#, fuzzy
+#~ msgid "Local Disks"
+#~ msgstr "$0 Datenträger"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "Es wurde kein zusätzlicher lokaler Speicher gefunden."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Datenträger neu formatieren und hinzufügen"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "Das Zurücksetzen des Storage Pool löscht alle Container und Datenträger "
+#~ "im Pool."
+
+#~ msgid "Security"
+#~ msgstr "Sicherheit"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr ""
+#~ "Der Docker Storage Pool kann auf diesem System nicht verwaltet werden."
+
+#, fuzzy
+#~| msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "Der Scan von $time ($type) keine Schwachstellen gefunden."
+#~ msgstr[1] "Der Scan von $time ($type) keine Schwachstellen gefunden."
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "Der Scan von $time ($type) keine Schwachstellen gefunden."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "Der Scan von $time ($type) war nicht erfolgreich."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Der Virtualisierungsdienst ist verfügbar"
+
+#, fuzzy
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "Sie haben keine Berechtigung, den Docker Storage Pool zu verwalten."
+
+#~ msgid "$0 GiB / $1 GiB"
+#~ msgstr "$0 GiB / $1 GiB"
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} -> $cport"
diff --git a/po/es.po b/po/es.po
new file mode 100644
index 0000000..49406f8
--- /dev/null
+++ b/po/es.po
@@ -0,0 +1,13230 @@
+# Gerardo Rosales <grosale86@gmail.com>, 2015. #zanata
+# Kenneth Guerra <kg2795@gmail.com>, 2015. #zanata
+# William Moreno Reyes <williamjmorenor@gmail.com>, 2015. #zanata
+# magjogui <magjogui@fedoraproject.org>, 2015. #zanata
+# Abdel G. Martínez L. <abdel.g.martinez.l@gmail.com>, 2016. #zanata
+# Aura Lila <lila2390@gmail.com>, 2016. #zanata
+# Neville A. Cross <yn1v@fedoraproject.org>, 2016. #zanata
+# Omar Berroterán S. <omarberroteranlkf@gmail.com>, 2016. #zanata
+# William Moreno Reyes <williamjmorenor@gmail.com>, 2016. #zanata
+# Adolfo Jayme <fitoschido@gmail.com>, 2017. #zanata
+# Claudio Rodrigo Pereyra Diaz <elsupergomez@fedoraproject.org>, 2017. #zanata
+# Emilio Herrera <ehespinosa57@gmail.com>, 2017. #zanata, 2020.
+# Omar Berroterán S. <omarberroteranlkf@gmail.com>, 2017. #zanata
+# cockpit <cockpituous@gmail.com>, 2017. #zanata
+# Emilio Herrera <ehespinosa57@gmail.com>, 2018. #zanata, 2020.
+# Máximo Castañeda Riloba <mcrcctm@gmail.com>, 2018. #zanata
+# cockpit <cockpituous@gmail.com>, 2018. #zanata
+# Gerardo Rosales <grosale86@gmail.com>, 2019. #zanata
+# Matej Marusak <mmarusak@redhat.com>, 2020. #zanata
+# Adolfo Jayme Barrientos <fitoschido@gmail.com>, 2020.
+# Álvaro Castillo <sincorchetes@gmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2024-02-12 11:34+0000\n"
+"Last-Translator: Miguel Ángel Sánchez <maletils@gmail.com>\n"
+"Language-Team: Spanish <https://translate.fedoraproject.org/projects/cockpit/"
+"main/es/>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.3.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "Número de usuarios"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+msgstr[1] "$0 CPUs"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 zona activa"
+msgstr[1] "$0 zonas activas"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 disponibles"
+
+#: pkg/storaged/block/resize.jsx:266
+msgid "$0 can not be made larger"
+msgstr "$0 no se pueden extender"
+
+#: pkg/storaged/block/resize.jsx:263
+msgid "$0 can not be made smaller"
+msgstr "$0 no se pueden reducir"
+
+#: pkg/storaged/block/resize.jsx:259
+msgid "$0 can not be resized"
+msgstr "$0 no se pueden redimensionar"
+
+#: pkg/storaged/block/resize.jsx:255
+msgid "$0 can not be resized here"
+msgstr "$0 no se puede redimensionar aquí"
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "tamaño de fragmento de $0"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 impacto crítico"
+msgstr[1] "$0 impactos, incluyendo impactos críticos"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 de datos + $1 de sobrecoste utilizados de $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 día"
+msgstr[1] "$0 días"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "Falta $0 disco"
+msgstr[1] "Faltan $0 discos"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "falta $0 disco"
+msgstr[1] "Faltan $0 discos"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 discos"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "Documentación de $0"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "Error $0"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 finalizó con el código $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 falló"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 intento de inicio de sesión fallido"
+msgstr[1] "$0 intentos de inicio de sesión fallidos"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "$0 filesystem"
+msgstr "sistema de archivos $0"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 libre"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 hora"
+msgstr[1] "$0 horas"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 impacto importante"
+msgstr[1] "$0 impactos, incluyendo impactos importantes"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 es un fichero existente"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 está en uso"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 no está disponible en ningún repositorio."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 clave cambiada"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 terminado con señal $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 impacto poco grave"
+msgstr[1] "$0 impactos poco graves"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minuto"
+msgstr[1] "$0 minutos"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 impacto moderado"
+msgstr[1] "$0 impactos, incluyendo impactos moderados"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 mes"
+msgstr[1] "$0 meses"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 más..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 paquete"
+msgstr[1] "$0 paquetes"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 paquete necesita un reinicio del sistema"
+msgstr[1] "$0 paquetes necesitan un reinicio del sistema"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 página"
+msgstr[1] "$0 páginas"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+msgid "$0 partitions"
+msgstr "$0 particiones"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 Actualización de seguridad disponible"
+msgstr[1] "$0 Actualizaciones de seguridad disponibles"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 servicio ha fallado"
+msgstr[1] "$0 servicios han fallado"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 servicio necesita reiniciarse"
+msgstr[1] "$0 servicios necesitan reiniciarse"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 hueco disponible"
+msgstr[1] "$0 huecos disponibles"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 pico"
+msgstr[1] "$0 picos"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0 sincronizado"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 total"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 Actualización disponible"
+msgstr[1] "$0 Actualizaciones disponibles"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "usado $0 de $1 ($2 guardado)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 semana"
+msgstr[1] "$0 semanas"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "Se instalará: $0."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 año"
+msgstr[1] "$0 años"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "Zona $0"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: falló a los $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (del $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+msgid "(Not part of target)"
+msgstr "(No es parte del destino)"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+msgid "(no assigned mount point)"
+msgstr "(no tiene punto de montaje)"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+msgid "(not mounted)"
+msgstr "(no montado)"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(recomendado)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", incluyendo $1 corrección de seguridad"
+msgstr[1] ", incluyendo $1 correcciones de seguridad"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 día"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 hora"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 min"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 minuto"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 semana"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 min"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 minutos"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 minutos"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 min"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minutos"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 horas"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 minutos"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "No se ha instalado una versión compatible de Cockpit en $0."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr ""
+"Un sistema de archivos con ese nombre ya existe en este grupo de "
+"almacenamiento."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "Ya existe un grupo con este nombre"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Se requiere un navegador moderno por seguridad, fiabilidad y rendimiento."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Una agregación de enlaces de red combina múltiples interfaces de red en una "
+"sola interfaz lógica con un mayor rendimiento o redundancia."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Se creará una nueva clave SSH en $0 para $1 en $2 y se añadirá al fichero $3 "
+"de $4 en $5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Un paquete necesita un reinicio del sistema para que la actualización surta "
+"efecto:"
+msgstr[1] ""
+"Varios paquetes necesitan un reinicio del sistema para que las "
+"actualizaciones surtan efecto:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Ya existe un grupo de almacenamiento con este nombre."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"Un servicio necesita reiniciarse para que las actualizaciones surtan efecto:"
+msgstr[1] ""
+"Varios servicios necesitan reiniciarse para que las actualizaciones surtan "
+"efecto:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr ""
+"A un grupo de volúmenes sin volúmenes físicos no se le puede cambiar el "
+"nombre."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "Supervisión de ARP"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "Ping de ARP"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Acerca de la consola Web"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Ausente"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Aceptar la clave y loguearse"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "Contraseña aceptable"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Expiración de la cuenta"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "La cuenta no está disponible o no se puede modificar."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Cuentas"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Acción"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Acciones"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Activar"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr "Activar antes de cambiar el tamaño"
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Activando $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Activo"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Copia de seguridad activa"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Páginas activas"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Activo desde "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Estado de actividad"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Balanceo de carga adaptativo"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Transmisión adaptativo para el balanceo de carga"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Añadir"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Añadir $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "Añadir servidor DNS"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Añadir cifrado de disco enlazado a la red"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Añadir servidor de claves Tang"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Añadir VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "Añadir VPN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "Añadir VPN WireGuard"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "Añadir un mapa de bits"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Añadir una nueva zona"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "Añadir dirección"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Añadir dispositivos de bloques"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Añadir agregación"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Añadir puente"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Añadir disco"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Añadir discos"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Agregar portal de iSCSI"
+
+# #-#-#-#-# es.po (PACKAGE VERSION) #-#-#-#-#
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Añadir clave"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "Añadir servidor de claves"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Añadir miembro"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Añadir un nuevo anfitrión"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Añadir una nueva zona"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "Añadir contraseña"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "Añadir usuario"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+msgid "Add physical volume"
+msgstr "Añadir volumen físico"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Añadir puertos"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Añadir puertos a la zona $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Añadir clave pública"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "Añadir ruta"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "Añadir dominio de búsqueda"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Añadir servicios"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Añadir servicios a la zona $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Añadir servicios a la zona $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Añadir equipo"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Añadir zona"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "Añadiendo \"$0\" a las opciones de cifrado"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Añadiendo \"$0\" a las opciones de sistema de archivos"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Si se añade $0 se cerrará la conexión con el servidor y se perderá acceso a "
+"la interfaz de administración."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Para añadir un servidor de claves se necesita primero desbloquear el grupo "
+"de almacenamiento. Por favor, introduzca la contraseña del grupo de "
+"almacenamiento existente."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Añadir puertos específicos hará que se recargue firewalld. ¡Puede que se "
+"pierda la configuración actual!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Añadiendo clave"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Añadiendo volumen físico a $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Añadiendo rd.neednet=1 a la línea de comandos del kernel"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "DNS adicional $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Búsqueda adicional de dominios DNS $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Acciones adicionales"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Dirección adicional $val"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Paquetes adicionales:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Puertos adicionales"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Dirección"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Dirección $val"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "La dirección no puede estar vacía"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "La dirección no es una URL válida"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Dirección"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "Las direcciones no tienen un formato válido"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Administrando con la consola Web de Cockpit"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Acceso administrativo"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr "Se requiere acceso administrativo para crear y acceder a los informes."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Se requiere acceso administrativo"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "TCA avanzado"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Después"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Después de sacarlo del dominio, solo los usuarios con credenciales locales "
+"pueden ver los registros de esta máquina. Esto puede que afecte a otros "
+"servicios como la configuración de resolución de DNS y el listado de las CA "
+"de confianza."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Después de que el sistema arranque"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Alerta"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Alerta y superior"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Alias"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Todo"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+"Todos los volúmenes físicos seleccionados a $0 son necesarios para el diseño "
+"elegido."
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Todas las actualizaciones"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Todo en uno"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Permitir su ejecución (desenmascarar)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "Direcciones IP permitidas"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Direcciones permitidas"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "Se debe seleccionar $0 adicional"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Documentación de los roles de Ansible"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Se puede filtrar cualquier cadena de texto en los mensajes de los registros. "
+"La cadena también puede tener forma de expresión regular. También admite el "
+"filtrado por campos de registro de mensajes. Estos son valores separados por "
+"espacios, en forma CAMPO = VALOR, donde el valor puede ser una lista de "
+"valores posibles separados por comas."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Apariencia"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "Falta información sobre aplicaciones"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Aplicaciones"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Lista de aplicaciones"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Aplicar y reiniciar"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Aplicar los parches en vivo del kernel"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Aplicar esta solución"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Aplicando la nueva política... Esto puede tardar unos minutos."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Aplicando la solución..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Instalando las actualizaciones"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Fallo al instalar las actualizaciones"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "Apropiado para puntos de montaje críticos, como /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Aplicaciones"
+
+#: pkg/storaged/drive/drive.jsx:102
+msgid "Assessment"
+msgstr "Evaluación"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Etiqueta de propiedad"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "Al arranque"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Se necesita al menos $0 disco."
+msgstr[1] "Se necesita al menos $0 discos."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "Se necesita al menos un dispositivo de bloques."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Se necesita al menos un disco."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr "Al menos un padre debe estar montado con permisos de escritura"
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "Al minuto"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "Al segundo"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "A una hora específica"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Atributos"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Registro de auditoría"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Autenticar"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Autenticando"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Autenticación"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Falló la autenticación"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Falló la autenticación: se ha cerrado la conexión con el servidor"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Se requiere autenticación para elevar privilegios y realizar tareas de "
+"administración en la consola Web de Cockpit"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Se necesita autenticación"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Autorizar la clave SSH"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Claves SSH públicas autorizadas"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automático"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automático (solo DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Acceso automático"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Actualizaciones automáticas"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Se ejecuta automáticamente"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Utilizando NTP de forma automática"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Utilizando automáticamente servidores NTP adicionales"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Utilizando automáticamente servidores NTP específicos"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Script de automatización"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Objetivos disponibles en $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Actualizaciones disponibles"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+msgid "BIOS boot partition"
+msgstr "Partición de arranque BIOS"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "Fecha de la BIOS"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "Versión de la BIOS"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Volver a cuentas de usuario"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Malo"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Configuración incorrecta"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Equilibrador"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Antes"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Asociado a"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Negro"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blade"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Chasis tipo blade"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "Dispositivo de bloques"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Dispositivo de bloques para sistemas de archivos"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Dispositivos de bloques"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Bloqueado"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Agregación de enlaces"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Arranque"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"El arranque falla si el sistema de archivos no se puede montar, previniendo "
+"acceso remoto"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "El arranque no falla aunque el sistema de archivos no se monte"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Balanceado por"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Puente"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Puerto puente"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Configuración del puerto puente"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Difusión"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Configuración inválida"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr "El volumen btrfs está montado"
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Actualizaciones disponibles que corrigen errores"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Errores"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Chasis de expansión de bus"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Al cambiar la contraseña de la clave SSH $0 a la contraseña de acceso de $1 "
+"en $2, dicha clave quedará disponible automáticamente y podrá acceder a $3 "
+"sin contraseña en el futuro."
+
+#: pkg/static/login.html:61
+msgid "Bypass browser check"
+msgstr "Omitir la verificación del navegador"
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Seguridad de CPU"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "Configuración de seguridad de la CPU"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Uso de CPU"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "Uso/load de CPU"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Antememoria"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "Puede ser un hostname, dirección IP, alias, o URI ssh://"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr ""
+"No se pudo encontrar ningún registro mediante la combinación de filtros "
+"actuales."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Cancelar apagado"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Cancelar reinicio"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "No se puede activar"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "No se puede conectar con un anfitrión desconocido"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "No se pueden transferir los datos de acceso"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+"No se puede unir a un dominio porque el realmd no está disponible en este "
+"sistema"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "No se puede planificar un evento ocurrido en el pasado"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Capacidad"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Transporte"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Cambiar"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "Cambiar políticas de criptografía"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Cambiar el nombre del anfitrión"
+
+#: pkg/storaged/overview/overview.jsx:152
+msgid "Change iSCSI initiater name"
+msgstr "Cambiar el nombre iniciador iSCSI"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Cambiar el nombre iniciador del iSCSI"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/btrfs/volume.jsx:97
+msgid "Change label"
+msgstr "Cambiar etiqueta"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Cambiar la contraseña"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Cambiar la contraseña"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Cambiar el perfil de rendimiento"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Cambiar el perfil"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "Cambiar shell"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Cambiar la hora del sistema"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Cambiar la contraseña de $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Cambiar los ajustes"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Las llaves cambiadas pueden llevar una reinstalación del sistema operativo. "
+"Sin embargo, un cambio inesperado puede indicar un intento de interceptar su "
+"conexión mediante un tercero."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr "Cambiar el tipo de partición puede impedir que el sistema arranque."
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Cambiando los ajustes cerrará la conexión al servidor, y perderá el acceso a "
+"la IU de administración."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Comprobar si hay actualizaciones"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"Compruebe que el hash SHA-256 o SHA-1 del comando coincide con este diálogo."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "Compruebe el hash de clave con el servidor Tang."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Comprobando $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Comprobando la IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+msgid "Checking MDRAID device $target"
+msgstr "Comprobando el dispositivo MDRAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Comprobando y reparando el dispositivo MDRAID $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Comprobando si el paquete $0 está instalado"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Comprobando soporte para NBDE en el initrd"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Comprobando si hay nuevas aplicaciones"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Comprobando si hay actualizaciones de paquetes..."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Comprobando el software instalado"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Comprobando procesos relacionados"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Comprobando estado del software"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Seleccione el idioma que se va a utilizar en la aplicación"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Tamaño de la porción"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Clase"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Limpiando el $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Borrar 'Falló al iniciar'"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Limpiar todos los filtros"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Limpiar la búsqueda"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Dispositivo en texto plano"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Cliente de software"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Cerrar"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Cerrar las páginas seleccionadas"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "La configuración de autenticación de Cockpit está mal configurada."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "Configuración en Cockpit de NetworkManager y Firewalld"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit no se pudo conectar con el anfitrión especificado."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit tuvo un error interno no esperado."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit es un gestor de servidores que facilita la administración de "
+"servidores Linux a través de un navegador web. Es posible alternar entre el "
+"portal web y la consola sin problemas. Un servicio que haya empezado por "
+"Cockpit se puede parar desde la terminal. Asimismo, si se produce un error "
+"en el terminal, podrá verlo en la bitácora de Cockpit."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr ""
+"Cockpit es una interfaz interactiva que permite la gestión de servidores "
+"Linux."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit no es compatible con el software del sistema."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit no está instalado"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit no está instalado en el sistema."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit es ideal para administradores de sistemas nóveles, ya que permite "
+"realizar con sencillez tareas como gestionar almacenamiento, inspeccionar "
+"bitácoras e iniciar y detener servicios. Puede monitorizar y administrar "
+"varios servidores a la vez. Tan solo añádalos con solo pulsar un botón y sus "
+"máquinas se encargarán del resto."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Puede que Cockpit no se muestre correctamente en su navegador"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Recoger y empaquetar datos de diagnóstico y soporte"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Recoger volcados de colapso del kernel"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Almacenar métricas"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Color"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr ""
+"Los puertos, rangos y/o servicios deben estar separados por comas para que "
+"sean válidos"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Comando"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Comando no encontrado"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Comentario"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Falló la comunicación con el servicio tuned"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "PCI compacto"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Compatible con todos los sistemas y dispositivos (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Compatible con los sistemas modernos y discos duros > 2TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Comprimir volcado de colapsos para ahorrar espacio"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Compresión"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "La condición $0=$1 no se cumple"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Condición fallida"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Configuración"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Configurando"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Configurando la IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Configurando el servicio de kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Cambiando la configuración del sistema"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Confirmar"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Confirme la eliminación de $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Confirme la contraseña de la clave"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Confirme la nueva contraseña de la clave"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Confirme la nueva contraseña"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Confirmar contraseña"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Confirme el borrado de $0"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Confirme la eliminación con otra contraseña alternativa"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Confirme la parada de $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "En conflicto por"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Conflictos"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Conectar automáticamente"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Conectar a"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Conectar a:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Conectándose al servicio de SETroubleshoot..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Falló la conexión con dbus: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Conectándose a la máquina"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Error de conexión"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Conexión fallida"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "La conexión ha caducado."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "Se perderá la conexión"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.6, DocId:
+# cockpit
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Consiste en"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Contactado con el dominio"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Contenidos:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Continuar"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Continúe con la sesión"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Control"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Control"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Controlador"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Convertible"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Copiado"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Copiar"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Copiar al portapapeles"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Núcleo $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "No se pudo contactar con $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Ubicación del volcado de colapsos"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Reporte de colapso"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Colapsar el sistema"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Crear"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "Crear un grupo de volúmenes LVM2"
+
+#: pkg/storaged/overview/overview.jsx:145
+msgid "Create MDRAID device"
+msgstr "Crear un dispositivo MDRAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Crear un dispositivo RAID"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Crear un grupo de almacenamiento Stratis"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Crear una clave SSH y autorizarla"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Crear una instantánea del sistema de archivos $0"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Crear la cuenta con el UID no único"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Crear la cuenta con la contraseña débil"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Crear y cambiar propiedad del directorio personal"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Crear y montar"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Create and start"
+msgstr "Crear e iniciar"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Crear un sistema de archivos"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Crearlo"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Crear un volumen lógico"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Crear una cuenta"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Crear un nuevo sistema de archivos"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Crear nuevo grupo"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Crear un volumen lógico"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Crear un archivo de tarea con este contenido."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+msgid "Create new thinly provisioned logical volume"
+msgstr "Crear un nuevo volumen lógico con aprovisionamiento ligero"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "Sólo crear"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Crear una partición"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Crear una partición en $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Crear una tabla de particiones"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Crear una instantánea"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Crear una instantánea y montar"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Sólo crear una instantánea"
+
+#: pkg/storaged/overview/overview.jsx:174
+msgid "Create storage device"
+msgstr "Crear un dispositivo de almacenamiento"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+msgid "Create subvolume"
+msgstr "Crear un subvolumen"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Crear un volumen de aprovisionamiento fino"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Crear un temporizador"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Crear un grupo de volúmenes"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Creado"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Creando un grupo de volúmenes LVM2 $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+msgid "Creating MDRAID device $target"
+msgstr "Creando un dispositivo MDRAID en $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "Creando un dispositivo VDO"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Creando un sistema de archivos en $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Creando un volumen lógico en $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Creando una partición en $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Creando una instantánea de $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Crear esta $0 cerrará la conexión con el servidor y hará que la interfaz de "
+"administración no esté disponible."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Crítico y superior"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Las políticas de criptografía son un componente del sistema que configura "
+"los principales subsistemas criptográficos, cubriendo los protocolos TLS, "
+"IPSec, SSH, DNSSec y Kerberos."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "Políticas de criptografía"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "Las políticas de criptografía seleccionadas son inconsistentes"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Arranque actual"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Uso máximo de CPU actual"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Usado actualmente"
+
+#: pkg/kdump/kdump-view.jsx:535
+msgid "Currently not supported"
+msgstr "No soportado actualmente"
+
+#: pkg/storaged/partitions/partition.jsx:146
+msgid "Custom"
+msgstr "Personalizado"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "Política de criptografía personalizada"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Opciones de montaje personalizadas"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Puertos específicos"
+
+#: pkg/storaged/partitions/partition.jsx:180
+msgid "Custom type"
+msgstr "Tipo personalizado"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Zonas personalizadas"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "DEFAULT con posibilidad de verificación de firmas SHA-1."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "Búsqueda de dominios DNS"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "Buscar dominios DNS $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "A diario"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Alerta de peligro:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Oscuro"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Datos"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Datos utilizados"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"Los datos se almacenarán en dos copias y también de forma alterna en los "
+"volúmenes físicos seleccionados, para mejorar tanto la confiabilidad como el "
+"rendimiento. Es necesario seleccionar al menos cuatro volúmenes."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"Los datos se almacenarán en dos o más copias en los volúmenes físicos "
+"seleccionados, para mejorar la confiabilidad. Es necesario seleccionar al "
+"menos dos volúmenes."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+"Los datos se almacenarán en los volúmenes físicos seleccionados de forma "
+"alterna para mejorar el rendimiento. Es necesario seleccionar al menos dos "
+"volúmenes."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"Los datos se almacenarán en los volúmenes físicos seleccionados para que uno "
+"de ellos pueda perderse sin afectar los datos. Es necesario seleccionar al "
+"menos tres volúmenes."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+"Los datos se almacenarán en los volúmenes físicos seleccionados para que uno "
+"de ellos pueda perderse sin afectar los datos. Los datos también se "
+"almacenan de forma alterna para mejorar el rendimiento. Es necesario "
+"seleccionar al menos tres volúmenes."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+"Los datos se almacenarán en los volúmenes físicos seleccionados de modo que "
+"se puedan perder hasta dos de ellos al mismo tiempo sin afectar los datos. "
+"Los datos también se almacenan de forma alterna para mejorar el rendimiento. "
+"Es necesario seleccionar al menos cinco volúmenes."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+"Los datos se almacenarán en los volúmenes físicos seleccionados sin ninguna "
+"redundancia adicional ni mejoras de rendimiento."
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Las especificaciones de fecha deberían estar en formato YYYY-MM-DD hh:mm:ss. "
+"De igual manera, las cadenas 'yesterday', 'today', 'tomorrow' también se "
+"entienden. 'now' se refiere al tiempo actual. Finalmente, se pueden "
+"especificar tiempos relativos con prefijo '-','+'"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Desactivar"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+msgid "Deactivate logical volume $0/$1?"
+msgstr "¿Desactivar volumen lógico $0/$1?"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Desactivando"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Desactivando el $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Debug y superior"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Disminuir en uno"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "Paridad dedicada (RAID 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Deduplicación"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Predeterminado"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Retardo"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "El retardo debe ser númerico"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Eliminar"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Eliminar $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Eliminar cuenta"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Borrar ficheros"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Eliminar grupo"
+
+#: pkg/storaged/stratis/pool.jsx:292
+msgid "Delete pool"
+msgstr "Eliminar grupo"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "¿Eliminar informe permanentemente?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Eliminando $0 cerrará la conexión con el servidor y hará que la interfaz de "
+"administración no esté disponible."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Eliminando $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Eliminado el grupo de volúmenes LVM2 $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr ""
+"Eliminando un grupo de almacenamiento Stratis borrará toda la información "
+"que hay en él."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr ""
+"Eliminando un sistema de archivos borrará toda la información que haya en él."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr ""
+"Eliminando un volumen lógico borrará toda la información que hay en él."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "Eliminando una partición borrará toda la información que hay en ella."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "La eliminación borrará toda la información en el dispositivo MDRAID."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "La eliminación borrará toda la información en un dispositivo VDO."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "La eliminación borrará toda la información en un grupo de volúmenes."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr ""
+"La eliminación borrará toda la información en este subvolumen y todos sus "
+"hijos."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "El proceso eliminará los siguientes archivos:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Descripción"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Escritorio"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Desmontable"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Detalles"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Desarrollo"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Dispositivo"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Fichero de dispositivo"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "El dispositivo es de sólo lectura"
+
+#: pkg/storaged/block/other.jsx:60
+msgid "Device number"
+msgstr "Número del dispositivo"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Informes de diagnóstico"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Directorio"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "El directorio $0 no es editable o no existe."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Deshabilitar el multi-hilo simultáneo"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Desactivar el cortafuegos"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Deshabilitar tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Deshabilitado"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Denegar la autenticación interactiva con contraseña"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Denegar autenticación por contraseña"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "No permite ejecutar (máscara)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Desconectar"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Desconectado"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "E/S de disco"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "El disco está OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "El disco está fallando"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Contraseña del disco"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Discos"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Uso de discos"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "Descartar"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Descartar $0 alerta"
+msgstr[1] "Descartar $0 alertas"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Descartar las alertas seleccionadas"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Idioma de visualización"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "Paridad distribuida (RAID 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "No montar"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "No se monta automáticamente en el arranque"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Estación de acoplamiento"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "No se inicia automáticamente en el inicio"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "No se monta durante el arranque"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Dirección de dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Nombre del administrador del dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Contraseña del administrador del dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "No se pudo contactar con el dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "El dominio no está soportado"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "No repetir"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "Doble paridad distribuida (RAID 6)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Descargar"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Descargar un navegador libre"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Descargado"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Descargando"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Descargando $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Disco"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Rango dual"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+msgid "EFI system partition"
+msgstr "Partición de sistema EFI"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Editar"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Editar /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Editar el servidor de llaves Tang"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "Modificar la configuración VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "Modificar VPN WireGuard"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "Modificar la configuración de la agregación de enlaces"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "Modificar la configuración de puentes"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Editar servicio personalizado en la zona $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Editar anfitrión"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Editar anfitriones"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Editar motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Punto de montaje"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Editar reglas y zonas"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Editar servicio"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Editar el servicio $0"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "Modificar la configuración de equipos"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Editar usuario"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Editar una clave requiere un guardaclaves libre"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Expulsando a $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "PC integrado"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Vacío"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Vacío"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Contraseña vacía"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Eliminando el contenido de $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Habilitar"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Habilitar o deshabilitar el dispositivo"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Habilitar servicio"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Habilitar el cortafuegos"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Habilitado"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "Habilitando $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Encriptar datos"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Cifrar datos con un servidor de claves Tang"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "Cifrar datos con una contraseña"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Cifrado"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "$0 cifrado"
+
+#: pkg/storaged/stratis/pool.jsx:275
+msgid "Encrypted Stratis pool"
+msgstr "Grupo de almacenamiento cifrado Stratis"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Volumen lógico cifrado de $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Partición cifrada de $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Encriptación"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Opciones de cifrado"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Contraseña de cifrado"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Tipo de cifrado"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Finalizado"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "Punto final"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"Los puntos finales que actúen como \"servidor\" deben especificarse como "
+"anfitrión:puerto, en otro caso puede dejarse vacío."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Hacer cumplir"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Actualizaciones de mejora disponibles"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Introduzca una dirección MAC válida"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "En toda la subred"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Entrada en $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Eliminando $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Errata"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Error"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Error y superior"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Ha ocurrido un error"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "Error al instalar $0: PackageKit no está instalado"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Mensaje de error"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr ""
+"Error al ejecutar semanage para descubrir las modificaciones del sistema"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Error al guardar las llaves autorizadas: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Error al establecer el modo de SELinux: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "Se deben seleccionar exactamente $0 volúmenes físicos"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+"Es necesario seleccionar exactamente $0 volúmenes físicos, uno para cada "
+"franja del volumen lógico."
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Ejemplo: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Ejemplo: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Contraseña excelente"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Chasis de expansión"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Expirar la cuenta en"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Expirar la cuenta en $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Fichero export"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Exportar a la red"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Información extendida"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Partición extendida"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS no está activado adecuadamente"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS con restricciones Common Criteria."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Falló"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Fallo al añadir la máquina: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Fallo al añadir el puerto"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Fallo al añadir el servicio"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Fallo al añadir la zona"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Error al cambiar contraseña"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "Fallo al configurar PCP"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Fallo al eliminar la alerta: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Falló la desactivación del servicio tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Fallo al desactivar el perfil de tuned"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Fallo al editar máquina: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Fallo al editar el servicio"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "Fallo al habilitar $0 en firewalld"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Falló la habilitación de tuned"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Fallo al obtener los registros"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Fallo al cargar las claves autorizadas."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Fallo al cargar la unidad"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"No se pudieron analizar los archivos de la unidad para dnf-automatic.timer o "
+"dnf-automatic-install.timer. Elimine las anulaciones personalizadas para "
+"configurar las actualizaciones automáticas."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Fallo al reiniciar el servicio"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Fallo al guardar los cambios en /etc/motd"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Fallo al aplicar la configuración"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Falló al iniciar"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Falló el cambio de perfil"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Estado del archivo de unidad"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Sistema de archivos"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "El sistema de archivos está bloqueado"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Nombre del sistema de archivos"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Sistema de archivos fuera del destino"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr ""
+"Los sistemas de archivos ya están montados dentro de este punto de montaje."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Filtrar por nombre o descripción"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Filtrar elementos del menú"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Filtrar servicios"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Filtros"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Huella dactilar"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Cortafuegos"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "El cortafuegos no está disponible"
+
+#: pkg/storaged/drive/drive.jsx:122
+msgid "Firmware version"
+msgstr "Versión del firmware"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "Arreglar soporte para NBDE"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Tamaño de la fuente"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Prohibido ejecutarlo"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Forzar cambio"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Forzar el borrado"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Cambio forzado de la contraseña"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Formatear"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Formatear $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Formatear y montar"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Format and start"
+msgstr "Formatear e iniciar"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Sólo formatear"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr ""
+"Formateando eliminará todos los datos de un dispositivo de almacenamiento."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Retardo del reenvío $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Número del frame"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Espacio libre"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Búsqueda libre"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Los viernes"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "Desde"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Nombre completo"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Pasarela"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "General"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Generado"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Ir a $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Ir a la aplicación"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Ir a ahora"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Visibilidad de los gráficos"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Menú de opciones de visibilidad de los gráficos"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Grupo"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Nombre del grupo"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Grupos"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Crecer"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Incrementar el contenido"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Expandir el tamaño lógico de $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Expandir el volumen lógico"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "Extender partición"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "Extender el volumen para que ocupe todo el espacio"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Expandir para tomar todo el espacio"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Modo horquilla"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Modo horquilla"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Dispositivo de mano"
+
+#: pkg/storaged/drive/drive.jsx:64
+msgid "Hard Disk Drive"
+msgstr "Controlador de Disco Duro"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Información del hardware"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Salud"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Tiempo de vida $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Ayuda"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "Ocultar contraseña de confirmación"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "Ocultar contraseña"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "ID de jerarquía"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "Mayor interoperabilidad a costa de una mayor superficie de ataque."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Cuenta del histórico de paquetes"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Directorio personal"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Anfitrión"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "La tecla del anfitrión es incorrecta"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Nombre del anfitrión"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Anfitriones"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Cada hora"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Horas"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "Cómo comprobarlo"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+"ERROR INTERNO - Este volumen lógico está marcado como activo y debe tener un "
+"dispositivo de bloque asociado. Sin embargo, no se pudo encontrar ningún "
+"dispositivo de bloqueo de este tipo."
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "Dirección IP"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"Una dirección IP con un prefijo de enrutamiento. Separe los valores con una "
+"coma. Por ejemplo: 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "Direcciones IPv4"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "Configuración IPv4"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "Configuración IPv6"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Identificador"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Si se deja vacío, se generará un ID basado en los servicios y números de "
+"puerto asociados"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Si la huella coincide, pulse \"Aceptar la clave e iniciar sesión\". En caso "
+"contrario, no inicie sesión y contacte con su administrador."
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Si la huella coincide, pulse \"Confiar en el anfitrión y añadirlo\". En caso "
+"contrario, no se conecte y contacte con su administrador."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ignorar"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "Entrante"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "En un terminal, ejecute: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Para permitir iniciar sesión sin contraseña a $1 en $0 en adelante, use la "
+"contraseña de inicio de sesión de $2 en $3 como la contraseña de su clave, o "
+"deje la contraseña en blanco."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "En sincronía"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Inactivo"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+msgid "Inactive logical volume"
+msgstr "Volumen lógico inactivo"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Servicios incluidos"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"Las conexiones entrantes se bloquean por defecto. Las salientes no se "
+"bloquean."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Punto de montaje de sistema inconsistente"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Incrementar en uno"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Índice de memoria"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Indirecto"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Información"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Informativo y superior"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Inicializar"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Inicializar disco $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "Inicializando un disco eliminará todos los datos que contenga."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Inicializando..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Insights: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Instalar"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Instalar el soporte NFS"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Instalar soporte para Stratis"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Instalar todas las actualizaciones"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "Instalar información de aplicaciones"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Instalar cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Instalar todas las actualizaciones de kpatch"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "Instale soporte para realmd"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Instalar las actualizaciones de seguridad"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr ""
+"Instale «setroubleshoot-server» para solucionar problemas con los eventos de "
+"SELinux."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Instalar software"
+
+#: pkg/kdump/kdump-view.jsx:571
+msgid "Install the $0 package."
+msgstr "Instale el paquete $0."
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr ""
+"No es posible realizar su instalación sin tener el paquete de cockpit "
+"instalado"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Instalado"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Instalando"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Instalando $0"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "Instalar $0 eliminaría $1."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Instalando paquetes"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Interfaz"
+msgstr[1] "Interfaces"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Miembros de la interfaz"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Interfaces"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Error interno"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Error Interno: Encabezado del reto no valido"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "No válido"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "La dirección $0 no es válida"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Formato de fecha inválido"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Formato de fecha y hora inválidos"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Fecha de expiración invalida"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Permisos de archivo invalidos"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Llave inválida"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "La métrica $0 no es válida"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Numero de días incorrecto"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Número de puerto inválido"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "El prefijo $0 no es válido"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "El prefijo o la máscara de red $0 no es válido"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Rango inválido"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Formato de hora inválido"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Zona horaria no válida"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Nombre de usuario o contraseña inválidos"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "Pasarela IoT"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "¿El servicio SSHD está corriendo en un puerto diferente?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Trabajos"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Unirse"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Unirse a un dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Unirse a un dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Uniéndose"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Para unirse a un dominio debe instalar realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "No puede unirse a este dominio porque no está soportado"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Se une a un espacio de nombres de"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Bitácora"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Bitácora de entradas"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Entrada de bitácora no encontrada"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Ir a"
+
+#: pkg/kdump/kdump-view.jsx:569
+msgid "Kdump service is not installed."
+msgstr "El servicio kdump no está instalado."
+
+#: pkg/kdump/kdump-view.jsx:604
+msgid "Kdump settings"
+msgstr "Ajustes de kdump"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Mantener la conexión"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Volcado de colapso del kernel"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr "El kernel no arrancó con la opción $0"
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Volcado del kernel"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "El parche en vivo $0 del kernel está activo"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "El parche en vivo $0 del kernel está instalado"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Ajustes de los parches en vivo del kernel"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Aplicación de parches en vivo del kernel"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Contraseña de la clave"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Los guardaclaves de tipo desconocido no se pueden editar aquí"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Clave fuente"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Claves"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Servidor de claves"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Dirección del servidor de claves"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "La eliminación del servidor de claves puede prevenir el desbloqueo $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "Clave de LACP"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "LEGACY con interoperabilidad con Active Directory."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+msgid "LVM2 VDO pool"
+msgstr "Grupo VDO LVM2"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+msgid "LVM2 logical volume"
+msgstr "Volumen lógico LVM2"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+msgid "LVM2 logical volumes"
+msgstr "Volúmenes lógicos LVM2"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "Volumen físico LVM2"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+msgid "LVM2 physical volumes"
+msgstr "Volúmenes físicos LVM2"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "Grupo de volúmenes LVM2"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "Grupo de volúmenes LVM2 $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr "Etiqueta"
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Portátil"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Últimas 24 horas"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Últimos 7 días"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Última vez activo"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+msgid "Last cannot be removed"
+msgstr "El último no puede ser eliminado"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Última comprobación: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+msgid "Last disk can not be removed"
+msgstr "El último disco no se puede eliminar"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Último inicio de sesión"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Última modificación: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Último inicio de sesión correcto:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "Layout"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Aprenda más"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "Abandonar $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Abandonar el dominio"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Dejar en blanco para omitir cifrado"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Licenciada bajo la GNU LGPL versión 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Claro"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Limitar acceso"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Acceso limitado"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"El modo de acceso limitado restringe los privilegios administrativos. "
+"Algunas partes de la consola web tendrán funcionalidad reducida."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Límites"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "Lineal"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Retardo a desconexión de enlace"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Enlace local"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Monitorización del enlace"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Retardo a conexión de enlace"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Ver enlace"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Vinculado"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+msgid "Linux filesystem data"
+msgstr "Sistema de archivos de datos Linux"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+msgid "Linux swap space"
+msgstr "Área de intercambio Linux"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Escuchando"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "Puerto de escucha"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "El puerto de escucha debe ser un número"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Listando unidades"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Fallo al listar unidades: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Load"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Balanceo de carga"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Cargar datos anteriores"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Cargar entradas anteriores"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Fallo al cargar las actualizaciones disponibles"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Cargando actualizaciones disponibles, por favor espere..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Cargando entradas anteriores"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Cargando claves..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Fallo al cargar las claves SSH"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Fallo al cargar las unidades"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Cargando paquetes..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Cargando modificaciones del sistema..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Fallo al cargar la unidad"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Cargando..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Cuentas locales"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Sistema de archivos local"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Punto de montaje local"
+
+#: pkg/storaged/overview/overview.jsx:160
+msgid "Local storage"
+msgstr "Almacenamiento local"
+
+#: pkg/kdump/kdump-view.jsx:450
+msgid "Local, $0"
+msgstr "Local, $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Ubicación"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Bloquear"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "Bloquear a $0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Bloquear cuenta"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+msgid "Locked data"
+msgstr "Datos bloqueados"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Bloquendo $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Iniciar sesión"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Iniciar sesión en $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Acceda con su cuenta de usuario del servidor."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Mensajes de registro"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Salir"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Finalizar sesión del usuario"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Sesión iniciada"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Lógico"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+msgid "Logical Volume Manager partition"
+msgstr "Partición del gestor de volúmenes Logical Volume Manager"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Tamaño lógico"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Volumen lógico"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Volumen lógico(Instantánea)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Volumen lógico de $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Iniciar sesión"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Acceda de nuevo"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Inicio de sesión fallido"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Formato de acceso"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Historial de inicios de sesión"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Lista del historial de inicios de sesión"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "Finalizar sesión de $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Ha cerrado sesión de forma exitosa"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Registros"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Perfil bajo de escritorio"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Caja de almuerzo"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+msgid "MDRAID device"
+msgstr "Dispositivo MDRAID"
+
+#: pkg/storaged/utils.js:334
+msgid "MDRAID device $0"
+msgstr "Dispositivo MDRAID $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+msgid "MDRAID device is recovering"
+msgstr "El dispositivo MDRAID se está recuperando"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+msgid "MDRAID device must be running"
+msgstr "El dispositivo MDRAID debe estar ejecutándose"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+msgid "MDRAID disk"
+msgstr "disco MDRAID"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+msgid "MDRAID disks"
+msgstr "discos MDRAID"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (Recomendado)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "El MTU debe ser un número positivo"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Id. de máquina"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Huellas de clave SSH de la máquina"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Chasis del servidor principal"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Modo de mantenimiento"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+"Las páginas maliciosas en una máquina remota pueden afectar a otros hosts "
+"conectados"
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "Gestionar los tamaños de los sistemas de archivos"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Gestionar el almacenamiento"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Interfaces gestionadas"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "Gestión de LVMs"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "Gestión de montajes NFS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "Gestión de RAIDs"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "Gestión de VDOs"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "Gestión de VLANs"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Gestión del cortafuegos"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Gestión de agregaciones de enlaces de red"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Gestión de puentes de red"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Gestión de agregaciones de interfaces de red"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Gestión de particiones"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Gestión de discos físicos"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Gestión de servicios"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Gestión de actualizaciones de software"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Gestión de cuentas de usuario"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Manual"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Manualmente"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Marcando $target como fallido"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Enmascarar un servicio"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Enmascarado"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Enmascarando un servicio para prevenir que todas las unidades dependan de su "
+"ejecución. Esto puede tener un impacto mayor de los previsto. Por favor "
+"confirme que quiere enmascarar esta unidad."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Tiempo máximo del mensaje $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+msgid "Media drive"
+msgstr "Unidad de medios"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Memoria"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Tecnología de la memoria"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Uso de memoria"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Uso de memoria/swap"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Fusionado"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Mensaje para usuarios activos"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr ""
+"Los mensajes relacionados con los fallos se podrían encontrar en la bitácora:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Metadatos utilizados"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Método"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Métrica"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Métricas e histórico"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "No se pudo cargar el histórico de memoria"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Ajustes de métricas"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Mini Torre"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "El minuto debe ser un número comprendido entre 0 y 59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "Cada minuto"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minutos"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr "Espejo (RAID 1)"
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Mitigaciones"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Modo"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Modelo"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Modificando $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Los lunes"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Intervalo de monitorización"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Monitorizar objetivos"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Mensualmente"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Más información..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Montar"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "Punto de montaje"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "Montar después de que la red esté disponible, ignorar si falla"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Montar automáticamente en el arranque"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Montar en el arranque"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Montar automáticamente $0 en el arranque"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Montar antes de que se inicien los servicios"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Configuración de montaje"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Montar sistema de archivos"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Montar ahora"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Montar en $0 ahora"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Opciones de montaje"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Punto de montaje"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "El punto de montaje no puede estar vacío"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "El punto de montaje no puede estar vacío."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "El punto de montaje se está utilizando para $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "El punto de montaje debe empezar con \"/\"."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Montar en modo sólo lectura"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Montar sin esperas, ignorar si falla"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Montando $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Se monta antes de que se inicien los servicios"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Se monta en paralelo con los servicios"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+"Se monta en paralelo con los servicios, pero después de que la red esté "
+"disponible"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Chasis multisistema"
+
+#: pkg/storaged/drive/drive.jsx:135
+msgid "Multipathed devices"
+msgstr "Dispositivos de rutas múltiples"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+"Se pueden especificar múltiples direcciones utilizando comas o espacios como "
+"delimitadores."
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "Montar NFS"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "Ping de NSNA"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "Servidor NTP"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Nombre"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Nombre no puede estar vacío."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Nombre no puede estar vacío."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Nombre no puede superar los $0 bytes"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Nombre no puede superar los $0 caracteres"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Nombre no puede superar los 127 caracteres."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+msgid "Name cannot be longer than 255 characters."
+msgstr "Nombre no puede superar los 255 caracteres."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Nombre no puede contener el carácter «$0»."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+msgid "Name cannot contain the character '/'."
+msgstr "Nombre no puede contener el carácter «/»."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Nombre no puede contener espacios."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr "Necesita un disco de repuesto"
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Se requiere al menos un servidor NTP"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Red"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "E/S de red"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Agregación de enlaces de red"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Los dispositivos y los gráficos de red requieren de NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Registros de redes"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Uso de red"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager no está instalado"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager no se está ejecutando"
+
+#: pkg/storaged/overview/overview.jsx:168
+msgid "Networked storage"
+msgstr "Almacenamiento de red"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Redes"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Nunca"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "La cuenta nunca expira"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "La clave nunca expira"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Nunca ha iniciado sesión"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Nuevo montaje NFS"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Añadir un nuevo anfitrión"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "Nuevo anfitrión: $0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Nueva contraseña de la clave"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Nuevo nombre"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Nueva contraseña"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Nueva contraseña"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "No se aceptó la nueva contraseña"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Siguiente"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "No"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "No se ha especificado un ID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "No hay ninguna alerta de SELinux."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "No hay aplicaciones instaladas o disponibles."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "No hay ranuras disponibles"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "No hay dispositivos de bloques disponibles."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+msgid "No block devices found"
+msgstr "No se encontraron dispositivos de bloques"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "No hay un proveedor"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "No se encontró ninguna configuración"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "No hay datos disponibles"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "No hay datos disponibles entre $0 y $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Sin retardo"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "No hay una descripción disponible"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "No se ha suministrado una descripción."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+msgid "No devices found"
+msgstr "No se encontraron dispositivos"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "No hay discos disponibles."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+msgid "No disks found"
+msgstr "No se encontraron discos"
+
+#: pkg/storaged/iscsi/session.jsx:79
+msgid "No drives found"
+msgstr "No se encontraron unidades"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Sin cifrado"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Sin eventos"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Ningún sistema de archivos"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "No hay sistemas de archivos"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "No hay guardaclaves libres"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "No queda espacio disponible"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "No hay espacio libre después de esta partición"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "No se ha especificado un nombre de grupo"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "No se encontró ninguna clave de anfitrión."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "No se ha encontrado el paquete de instalación para esta aplicación."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Sin claves añadidas"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "No coincide ningún idioma"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "No hay entradas de registro"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "No hay volúmenes lógicos"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "No se encontraron registros"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "No se encontraron resultados"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "No hay un medio insertado"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "No se particiona"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+msgid "No partitions found"
+msgstr "No se encontraron particiones"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "Sin usuarios añadidos."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+msgid "No physical volumes found"
+msgstr "No se encontraron volúmenes físicos"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "No hay un nombre real especificado"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "No se encontraron resultados"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"No se obtuvieron resultados según el criterio de búsqueda. Limpie todos los "
+"filtros para mostrar los resultados."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Sin impactos"
+
+#: pkg/storaged/overview/overview.jsx:190
+msgid "No storage found"
+msgstr "No se encontró almacenamiento"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+msgid "No subvolumes"
+msgstr "No hay subvolúmenes"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "No existe el archivo o directorio"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "No hay modificaciones para el sistema"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "No hay informes del sistema."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "No actualizar"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Nombre de usuario no especificado"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Ninguno"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "No es una clave privada válida"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "No está autorizado para desactivar el cortafuegos"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "No está autorizado para activar el cortafuegos"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "No está disponible"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "No está conectado"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "No se ha conectado a Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "No se ha conectado al anfitrión"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+msgid "Not enough free space"
+msgstr "No hay suficiente espacio libre"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "No hay espacio suficiente"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+msgid "Not enough space to grow"
+msgstr "No hay espacio suficiente para expandir"
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "No se ha encontrado"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "No instalado"
+
+#: pkg/networkmanager/network-interface.jsx:680
+msgid "Not permitted to configure network devices"
+msgstr "Sin permisos para configurar dispositivos de red"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Sin permisos para configurar realms"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "No está permitido llevar a cabo esta acción."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "No está listo"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "No está registrado"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "No está ejecutándose"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Sin configurar"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "No está sincronizado"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Nota"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Portátil"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Notice y superior"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Ofuscar direcciones de red, nombres de anfitriones y de usuarios"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Ofuscado"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Ocurrió $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Ocurrió entre $0 y $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Ocurrencias"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "Aceptar"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Contraseña antigua"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Contraseña vieja"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "No se aceptó la contraseña vieja"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "En un dispositivo montado"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "En caso de fallo"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Una vez que se instale Cockpit, habilítelo con \"systemctl enable --now "
+"cockpit.socket\"."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Sólo $0 de $1 fue utilizado."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Solo se permiten caracteres alfanuméricos y _ , : , . , @ , -"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Solo en caso de emergencia"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"Sólo se usan determinados algoritmos aprobados y permitidos cuando se "
+"arranca en modo FIPS."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "¡Ooops!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "Abra el servicio pmproxy en el cortafuegos para compartir métricas."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Actividad '$operation' en $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Opciones"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "O use un navegador integrado"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Otro"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"Hay otros métodos de autenticación disponibles incluso cuando se deshabilita "
+"la autenticación interactiva con contraseña."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Otras opciones"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Saliente"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Visión global"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Sobrescribir"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Sobrescribir los datos existentes con ceros (más lento)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Falta el paquete cockpit-pcp para realizar un histórico de métricas"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Información de paquetes"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit colapsó"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit no está instalado"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit reportó un error con código $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Paquetes"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Nombre de la página"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Padre"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Padre $parent"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Parte de"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Parte de $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Partición"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Partición de $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "El tamaño de la partición es $0. El tamaño del contenido es $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Particionamiento"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Particiones"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Pasivo"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Contraseña"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "La contraseña no puede estar vacía"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "La contraseña no puede estar vacía"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Contraseña de cualquier otro guardaclaves"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Eliminar la contraseña puede prevenir el desbloqueo $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "La contraseña no coincide"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Contraseña"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Contraseña cambiada con éxito"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Expiración de la contraseña"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "La contraseña no puede superar los 256 caracteres"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "La contraseña no es válida"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "La contraseña es muy débil"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Se debe cambiar la contraseña"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Contraseña no válida"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Consejo sobre la contraseña"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Pegar"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Error al pegar"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.6, DocId:
+# cockpit
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "Pegar clave existente"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Pegue aquí el contenido de su clave SSH pública"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Ruta"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Coste de ruta"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Coste de la ruta $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Ruta en el servidor"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "La ruta en el servidor no puede estar vacía."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "La ruta en el servidor debe empezar con \"/\"."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Ruta del directorio"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Ruta al fichero"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Rutas"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Pausar"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+"El usuario #$0 tiene un puerto de punto final no válido. El puerto debe ser "
+"un número."
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"El usuario #$0 tiene un punto final no válido. Debe especificarse como "
+"anfitrión:puerto. Por ejemplo, 1.2.3.4:51820 o example.com:51820"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "Usuarios"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"Los usuarios son otras máquinas que se conectan con esta. Las claves "
+"públicas de otras máquinas se comparten entre sí."
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot recolecta y analiza métricas de rendimiento de su "
+"sistema."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Perfil de rendimiento"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Chasis periférico"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Permanente"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "¿Eliminar el grupo $0 permanentemente?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "¿Eliminar $0 permanentemente?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "¿Eliminar permanentemente el volumen lógico $0/$1?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+msgid "Permanently delete subvolume $0?"
+msgstr "¿Eliminar el subvolumen $0 permanentemente?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Permiso denegado"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Permisivo"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Físico"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "Volúmenes Físicos"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Volúmenes físicos"
+
+#: pkg/storaged/block/resize.jsx:279
+msgid "Physical volumes can not be resized here"
+msgstr "No se puede modificar el tamaño de los volúmenes físicos aquí"
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Selecciona una fecha"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Fijar unidad"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Intervalo de ping"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Hacer ping al objetivo"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Unidad fijada"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Caja de pizza"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Autentíquese para obtener acceso administrativo"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Por favor, habilite JavaScript para usar la consola Web."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Por favor, instale el paquete $0"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Por favor, resuelva el problema y recargue esta página."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Por favor especifique una fecha de expiración"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Por favor especifique el anfitrión a conectarse"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "Por favor, desmóntelos primero."
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Grupo de volúmenes de aprovisionamiento fino"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Grupo para volúmenes lógicos LVM2 con aprovisionamiento ligero"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Grupo para volúmenes de aprovisionamiento fino"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "Contraseña del grupo de almacenamiento"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Puerto"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Portable"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Puertos"
+
+#: pkg/storaged/partitions/partition.jsx:116
+msgid "PowerPC PReP boot partition"
+msgstr "Partición de arranque PowerPC PReP"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Longitud del prefijo"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Tamaño del prefijo o máscara de red"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Preparando"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Presente"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Mantener"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Nombre bonito del anfitrión"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Arranque previo"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Primario"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Prioridad"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Prioridad $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "Clave privada"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "Problema al cambiar a administrador"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Detalles del problema"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Información del problema"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Procesos usando la ubicación"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Progreso: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Sitio Web del proyecto"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Se ha agotado el tiempo de espera para passwd"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Ha expirado el tiempo de ssh-add"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Se ha agotado el tiempo de espera para ssh-keygen"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Propagar la recargar de"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Protege de ataques anticipados en el futuro a corto plazo a costa de una "
+"menor interoperabilidad."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr ""
+"Introduzca la contraseña del grupo de almacenamiento de estos dispositivos "
+"de bloques:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Clave pública"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+"Se generará la clave pública cuando se introduzca una clave privada válida"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Propósito"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (Franja)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (Espejo)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (Franja de espejos)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (Paridad dedicada)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (Paridad distribuida)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (Doble paridad distribuida)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "Chasis RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "Nivel de RAID"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr "RAID10 necesita un número par de volúmenes físicos"
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Chasis montado en rack"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Aleatorio"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Rango"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "El rango debe estar estrictamente ordenado"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Posición"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Raw a un dispositivo"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Lectura"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Leer más..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Solo lectura"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Leyendo"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Leyendo…"
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Listo"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Preparado"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Nombre real del anfitrión"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"El nombre real del anfitrión solo puede contener caracteres en minúscula, "
+"dígitos, guiones y puntos (con subdominios poblados)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "El nombre real del anfitrión debe tener 64 caracteres o menos"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Aplicar y reiniciar"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Reiniciar"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Reiniciar al terminar"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Se recomienda reiniciar"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Reiniciar el sistema..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Recibiendo"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Anfitriones recientes"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Recomendado, ajustes seguros para los modelos de ataque actuales."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Reconectarse"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Recuperando"
+
+#: pkg/storaged/jobs-panel.jsx:70
+msgid "Recovering MDRAID device $target"
+msgstr "Recuperando el dispositivo MDRAID $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Actualizando la información de paquetes"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Rechazando la conexión. Se desconoce el anfitrión"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Rechazando conexión. La clave del anfitrión no coincide"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Rechazando la conexión. La clave de anfitrión es desconocida"
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "Regenerar"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "Regenerando el initrd"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Registrar…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "Los procesos y servicios relacionados serán detenidos forzadamente."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "Los procesos relacionados serán detenidos forzadamente."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "Los servicios relacionados serán detenidos forzadamente."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Recargar"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Recargar la propagación desde"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Recargando"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Recargando el estado del resto de servicios"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "Remoto sobre CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "Remoto sobre FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Remoto sobre NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+msgid "Remote over NFS, $0"
+msgstr "Remoto sobre NFS, $0"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "Remoto sobre SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Remoto sobre SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+msgid "Remote over SSH, $0"
+msgstr "Remoto sobre SSH, $0"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Borrados:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Eliminar"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Eliminar $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Eliminar el servicio $0 la zona $1"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "¿Eliminar $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "¿Eliminar servidor de claves Tang?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Eliminar el dispositivo"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Eliminar anfitrión"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Eliminar elemento"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "¿Eliminar los volúmenes físicos faltantes?"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "¿Eliminar la contraseña del guardaclaves $0?"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "¿Eliminar la contraseña?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Eliminar el servicio $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Eliminar la zona $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Eliminando"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Eliminando $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Eliminando $0 cerrará la conexión con el servidor y hará que la consola de "
+"administración no esté disponible."
+
+#: pkg/storaged/jobs-panel.jsx:66
+msgid "Removing $target from MDRAID device"
+msgstr "Eliminando el $target del dispositivo MDRAID"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"Eliminar una contraseña sin confirmar con otra contraseña puede impedir el "
+"desbloqueo o la gestión de claves, si otras contraseñas se pierden o se "
+"olvidan."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Eliminando el volumen físico de $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Eliminando el servicio de cockpit cerrará la conexión con el servidor y hará "
+"que la consola de administración no esté disponible. Asegúrese de que esta "
+"zona se aplique en su conexión de la consola Web actual."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr ""
+"Eliminando la zona eliminará todos los servicios que estén asociados a esta."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Renombrar"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Renombrar grupo de almacenamiento Stratis"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Renombrar sistema de archivos"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Renombrar grupo"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "Renombrar grupo $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Cambiar el nombre de volumen lógico"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Renombrar un grupo de volumen"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Renombrando a $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "Renombrar un grupo puede afectar a sudo y reglas similares"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr "Reparar"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+msgid "Repair logical volume $0"
+msgstr "Reparar volumen lógico $0"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Reparando a $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Repetir"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Repetir mensualmente"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Repita la contraseña"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Repetir cada semana"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Informe"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Etiqueta de informe"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Informar a ABRT Analytics"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Informado; no hay enlaces disponibles"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Fallo al crear el informe"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "El informe fue cancelado"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Informes"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Informes:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Hay que cambiar la contraseña cada $0 días"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Hay que cambiar la contraseña en $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Requerir cambiar la contraseña en el primer inicio de sesión"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Requerido por"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Requerido por "
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Requiere"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Es necesario tener permisos de acceso administrativi para editar"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Requisito"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Requisito de"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+"Establezca una reserva de memoria durante el arranque configurando la opción "
+"'$0' en la cadena de parámetros del kernel. Por ejemplo, añada '$1' a $2 en "
+"$3 o use el editor de parámetros del kernel de su distribución."
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Memoria reservada"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Reiniciar"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Restablecer contraseña"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Redimensionando $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Para cambiar el tamaño de un sistema de archivos cifrado, se necesita "
+"primero desbloquear el disco. Por favor, introduzca una contraseña actual "
+"del disco."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Reiniciar"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Reiniciar servicios"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Reiniciar servicios..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Reiniciando"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Restableciendo la conexión"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"Los resultados del colapso se copiarán a través de $0 a $1 en forma de $2, "
+"si kdump está correctamente configurado."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+"Los resultados del colapso se copiarán a $0 en forma de $1, si kdump está "
+"correctamente configurado."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Reanudar"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Reutilizar cifrado existente"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Reutilizar cifrado existente ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "Revisar las políticas de criptografía"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Revisión de registros"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Round robin"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Rutas"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Ejecutar a las"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Crear nuevo informe"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Ejecutar cada"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Crear informe"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+"Ejecute este comando en la máquina remota a través de una red de confianza o "
+"de forma física:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Lanzador"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Ejecutando"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Tiempo de ejecución"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "Errores de control de acceso de SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "Se ha desactivado SELinux en el sistema"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "Se ha desactivado SELinux en el sistema."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "Normativa de SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "Se desconoce el estado de sistema de SELinux."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "Solución de problemas de SELinux"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "SMART auto-diagnóstico de $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"Los informes SOS recogen información del sistema para ayudarle a "
+"diagnosticar problemas."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "Clave SSH"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "La clave SSH no es una ruta"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "Claves SSH"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "Retardo del reenvío STP"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "Tiempo de saludo STP"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "Tiempo máximo del mensaje STP"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "Prioridad STP"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Los usuarios de Safari tienen que importar y confiar en la CA del "
+"certificado autofirmado:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Los sábados"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Guardar"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Guardar y reiniciar"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Guardar cambios"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Ahorre espacio en la compresión individual de bloques con LZ4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Ahorre espacio almacenando bloques de datos idénticos solo una vez"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Para guardar una nueva contraseña se necesita primero desbloquear el disco. "
+"Por favor, introduzca una contraseña actual del disco."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Apagado programado a las $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Reinicio programado el $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "PC de caja sellada"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Buscar"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Dominio de búsquedas"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Buscar por nombre o ID"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Buscar por nombre, grupo o ID"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "El segundo debe ser un número comprendido entre 0 y 59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Segundos"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Claves de Secure Shell"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Eliminando de forma segura $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Configuración de Security Enhanced Linux y resolución de problemas"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Actualizaciones de seguridad disponibles"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Sólo actualizaciones de seguridad"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Las actualizaciones de seguridad se aplicarán el $0 a las $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Seleccionar"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Seleccionar un identificador"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Seleccionar método"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+"Seleccione los volúmenes físicos que se deben utilizar para reparar el "
+"volumen lógico. Se necesitan al menos $0."
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Enviar"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Enviando"
+
+#: pkg/storaged/drive/drive.jsx:123
+msgid "Serial number"
+msgstr "Número de serie"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Servidor"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Dirección del servidor"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "La dirección del servidor no puede estar vacía."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Servidor no puede estar vacío."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "El servidor ha cerrado la conexión."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Software de servidor"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Servicio"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "El servicio tiene un error"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Bitácoras del servicio"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Servicios"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Servicios usando la ubicación"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Sesión"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "La sesión está apunto de expirar"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Establecer"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Establecer un nombre de anfitrión"
+
+#: pkg/storaged/partitions/partition.jsx:173
+msgid "Set partition type of $0"
+msgstr "Establecer el tipo de partición de $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Establecer contraseña"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Establecer la hora"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Ajustar a"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Preparar"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Establecer contraseña débil"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Los ajustes se desvían del estado configurado y serán revertidos en el "
+"próximo arranque."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Configurar"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Configurando el dispositivo de retorno $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Ajustes"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Severidad"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Compartido"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Shell"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Script de shell"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+msgid "Show all $0 rows"
+msgstr "Mostrar todas las $0 filas"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Mostrar todos los hilos"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Show confirmation password"
+msgstr "Mostrar contraseña de confirmación"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Mostrar las huellas dactilares"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Mostrar mensajes que contengan una cadena dada."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Mostrar mensajes para la unidad de systemd especificada."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Mostrar mensajes de un arranque específico."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Mostrar más relaciones"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "Mostrar contraseña"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Mostrar relaciones"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Encogimiento"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Encoger un volumen lógico"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "Reducir partición"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Encoger un volumen"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Apagar"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Apagar"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Desde"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Rango único"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Tamaño"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "El tamaño no puede ser negativo"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "El tamaño no puede ser cero"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "El tamaño es demasiado grande"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "El tamaño debe ser un número"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "El tamaño debe ser al menos $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Saltar navegación principal"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Saltar al contenido"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Compartimento"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Compartimento $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Crear una instantánea"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Sockets"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Actualizaciones de software"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Las soluciones alternativas basadas en software ayudan a evitar problemas de "
+"seguridad de la CPU. Estas mitigaciones tienen el efecto secundario de "
+"reducir el rendimiento. Cambie estas configuraciones bajo su propio riesgo."
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr "Unidad de Estado Sólido"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Solución aplicada con éxito"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Fallo en la solución"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Soluciones"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"Algunos dispositivos de bloques en este grupo de almacenamiento han "
+"aumentado su tamaño después de que el grupo se crease. El grupo de "
+"almacenamiento puede ser extendido sin peligro para usar el nuevo espacio "
+"disponible."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"Algún otro programa está usando actualmente el gestor de paquetes, por favor "
+"espere..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Parte del software necesita ser reiniciado manualmente"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr "Lo siento"
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Ordenado de menos a más confiable"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Ordenador compacto"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Spanning tree protocol"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Spanning tree protocol (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Repuesto"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Hora específica"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Velocidad"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Estable"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Iniciar"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Empezar y activar"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Inicio multitrayecto"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Iniciar servicio"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr ""
+"Comenzar a mostrar entradas desde la fecha especificada a la más reciente, "
+"incluyéndola."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr ""
+"Comenzar a mostrar entradas desde la fecha especificada a la más antigua, "
+"incluyéndola."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Iniciado"
+
+#: pkg/storaged/jobs-panel.jsx:64
+msgid "Starting MDRAID device $target"
+msgstr "Iniciando dispositivo MDRAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Iniciando espacio del área de intercambio $target"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Estado"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Estático"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Estado"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "PC USB"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Pegajoso"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Detener"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Parar y desactivar"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Parar y eliminar"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Parar y desmontar"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Parar dispositivo"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Dejar de editar anfitriones"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Detener informe"
+
+#: pkg/storaged/jobs-panel.jsx:63
+msgid "Stopping MDRAID device $target"
+msgstr "Deteniendo dispositivo MDRAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Deteniendo el espacio del área de intercambio $target"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Almacenamiento"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "El almacenamiento no se puede gestionar en este sistema."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Registros de almacenamiento"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Guardar contraseña"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Contraseña almacenada"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+msgid "Stratis block device"
+msgstr "Dispositivo de bloques Stratis"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+msgid "Stratis block devices"
+msgstr "Dispositivos de bloques Stratis"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Los dispositivos de bloques de Stratis no se pueden reducir más"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+msgid "Stratis filesystem"
+msgstr "Sistema de archivos Stratis"
+
+#: pkg/storaged/stratis/pool.jsx:300
+msgid "Stratis filesystems"
+msgstr "Sistemas de archivos Stratis"
+
+#: pkg/storaged/stratis/pool.jsx:361
+msgid "Stratis filesystems pool"
+msgstr "Grupo de sistemas de archivos Stratis"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Grupo de almacenamiento Stratis"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr "Rayado (RAID 0)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr "Divisiones y espejos (RAID 10)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr "Divisiones"
+
+#: pkg/lib/cockpit-components-password.jsx:97
+msgid "Strong password"
+msgstr "Contraseña fuerte"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Stub"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Estilo"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Sub Chasis"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Subportátil"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "Falló la suscripción a las señales de systemd: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+msgid "Subvolume needs to be mounted"
+msgstr "El subvolumen debe estar montado"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr "El subvolumen debe estar montado con permisos de escritura"
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Copiado al portapapeles con éxito"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "¡Copiado al portapapeles con éxito!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Los domingos"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Área de intercambio"
+
+#: pkg/storaged/block/resize.jsx:292
+msgid "Swap can not be resized here"
+msgstr "El área de intercambio no se puede redimensionar aquí"
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Área de intercambio usada"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Control de encendido de $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Apagar $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Encender $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Cambiar a acceso administrativo"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Pasar a acceso limitado"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Apagando $0 cerrará la conexión al servidor, y perderá el acceso a la IU de "
+"administración."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Encendiendo $0 cerrará la conexión al servidor, y perderá el acceso a la IU "
+"de administración."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Sincronizado"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Sincronizado con $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Sincronizando"
+
+#: pkg/storaged/jobs-panel.jsx:71
+msgid "Synchronizing MDRAID device $target"
+msgstr "Sincronizando el dispositivo MDRAID $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Sistema"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Diagnósticos del sistema"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Información del sistema"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "El sistema está actualizado"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Modificaciones del sistema"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Hora del sistema"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Unidades de systemd"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tableta"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Servidor de claves Tang"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Objetivo"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Objetivos"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Equipo"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Puerto del equipo"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Ajustes del puerto del equipo"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminal"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Cerrar sesión"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Comprobar la configuración"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr ""
+"Las pruebas solo están disponibles cuando el servicio kdump esté "
+"ejecutándose."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Ajustes de prueba de kdump"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Compruebe la configuración de kdump colapsando el kernel. Esto puede tomar "
+"un tiempo y puede que el sistema no se reinicie automáticamente. No colapse "
+"deliberadamente el sistema mientras se ejecute cualquier proceso importante."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Probando la conexión"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "El paquete $0 no está disponible desde ningún repositorio."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr ""
+"El paquete $0 debe instalarse para crear grupos de almacenamiento Stratis."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "Se debe instalar el paquete $0."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "El paquete $0 será instalado para crear dispositivos VDO."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr ""
+"La dirección IP o el nombre de dominio no pueden contener espacios en blanco."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+msgid "The MDRAID device is in a degraded state"
+msgstr "El dispositivo de MDRAID está degradado"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+msgid "The MDRAID device must be running"
+msgstr "El dispositivo MDRAID debe estar ejecutándose"
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr "La clave SSH $0 de $1 en $2 será añadida al archivo $3 de $4 en $5."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"La clave SSH $0 estará disponible durante el resto de la sesión y también lo "
+"estará para unirse a otros anfitriones."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"La clave SSH para iniciar sesión en $0 está protegida por contraseña, y el "
+"anfitrión no permite iniciar sesión con contraseña. Por favor, introduzca la "
+"contraseña de dicha clave en $1."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"La clave SSH para iniciar sesión en $0 está protegida. Puedes iniciar sesión "
+"tanto con la contraseña de usuario como introduciendo la contraseña de dicha "
+"clave en $1."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"Se forzará a la cuenta '$0' para que cambie su contraseña en el próximo "
+"acceso"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "El servicio de cockpit está incluido de forma automática"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"El estado configurado es desconocido, podría cambiar en el próximo arranque."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"No ha finalizado la creación de este dispositivo VDO y no se podrá utilizar."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr "El usuario actual no tiene permitido ver la información de las claves."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"Se necesita desbloquear el disco antes de formatearlo. Por favor, introduzca "
+"una contraseña existente."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "Se eliminará el archivo $0."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+msgid "The filesystem has no assigned mount point."
+msgstr "El sistema de archivos no tiene un punto de montaje asignado."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "El sistema de archivos no tiene un punto de montaje permanente."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"El sistema de archivos está configurado para ser automáticamente montado en "
+"$0 al arranque pero su contenedor de encriptación no estará desbloqueado "
+"para entonces."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"El sistema de archivos está montado pero no se montará después del arranque."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"El sistema de archivos está montado en $0 pero se montará en $1 en el "
+"próximo arranque."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"El sistema de archivos está montado en $0 pero no se montará en el próximo "
+"arranque."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"El sistema de archivos no está montado pero se montará en el próximo "
+"arranque."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "El sistema de archivos no está montado."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"El sistema de archivos se desbloqueará en el siguiente arranque. Puede que "
+"se requiera introducir una contraseña."
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "La huella debería coincidir con:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "El siguiente servicio será reiniciado:"
+msgstr[1] "Los siguientes servicios serán reiniciados:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "El nombre completo no debe contener dos puntos."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "El ID de grupo debe ser un número entero positivo"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"El nombre de grupo sólo puede contener letras (a-z), dígitos, puntos, "
+"guiones y guiones bajos"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"El directorio personal $0 ya existe. Su propiedad será cambiada al nuevo "
+"usuario."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "Se debe regenerar el initrd."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "La contraseña de la clave no puede estar vacía"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Las contraseñas de la clave no coinciden"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "La clave introducida no resultó válida."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "No se puede eliminar el último guardaclaves"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "Los procesos y servicios listados serán detenidos forzadamente."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "Los procesos listados serán detenidos forzadamente."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "Los servicios listados serán detenidos forzadamente."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr ""
+"El usuario autenticado no tiene permisos para ver las modificaciones del "
+"sistema"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "La máquina se está reiniciando"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "El punto de montaje $0 está siendo usado por los siguientes procesos:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "El punto de montaje $0 está siendo usado por los siguientes servicios:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "La nueva contraseña de la clave no puede estar vacía"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "La contraseña no puede estar vacía"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Las contraseñas no coinciden"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Las contraseñas no coinciden."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"La huella resultante es apta para compartirse en público, incluyendo correo "
+"electrónico."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"La huella resultante puede ser distribuida por medios públicos, incluyendo "
+"correo electrónico, sin riesgos. Si está pidiendo a alguien que haga la "
+"verificación por usted, pueden enviarle los resultados usando cualquier "
+"método."
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"El servidor rechazó autenticar '$0' utilizando la autenticación de "
+"contraseña y no hay disponble otro método de autenticación soportado."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr "El servido rehusó autenticar usando los métodos soportados."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"El sistema no permite actualmente desbloquear un sistema de archivos con un "
+"servidor Tang durante el arranque."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"El sistema no permite actualmente desbloquear el sistema de archivos raíz "
+"con un servidor Tang."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+"El usuario $0 tiene permisos para cambiar las mitigaciones de seguridad de "
+"la CPU"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr ""
+"El usuario $0 no tiene permisos para modificar las políticas de criptografía"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "El usuario $0 no tiene permisos para colapsar el kernerl"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"El usuario debe cerrar sesión y volver a iniciarla para que la nueva "
+"configuración tome efecto."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"El nombre de usuario sólo puede contener letras (a-z), dígitos, puntos, "
+"guiones y guiones bajos."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"La configuración del navegador evita que se ejecute Cockpit (inaccesible $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "No hay actualmente páginas activas"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"Hay dispositivos con múltiples rutas en el sistema, pero no se está "
+"ejecutando el servicio de rutas."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "No hay servicios activos en esta zona"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "No hay claves públicas autorizadas para esta cuenta."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+"No hay suficiente espacio disponible que pueda usarse para una reparación. "
+"Se necesitan al menos $0 en volúmenes físicos que estén disponibles para "
+"este volumen lógico."
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"No hay espacio suficiente en el grupo de almacenamiento para crear una "
+"instantánea de este sistema de archivos. Se requieren $0 pero sólo hay $1 "
+"disponibles."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Hubo un error no esperado mientra se conectaba a la máquina."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Son necesarios estos pasos adicionales:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Se realizarán los siguientes cambios:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Volumen lógico fino"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Volúmenes lógicos LVM2 con aprovisionamiento ligero"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"Este dispositivo MDRAID no tiene un mapa de bits. Los mapas de bits pueden "
+"reducir significativamente los tiempos de sincronización."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr "Este montaje NFS está en uso y solo se pueden cambiar sus opciones."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Este dispositivo VDO no usa todos sus dispositivos de respaldo."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+msgid "This device can not be used for the installation target."
+msgstr "Este dispositivo no se puede utilizar como destino de instalación."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Este dispositivo no se puede gestionar aquí."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "Este dispositivo está en uso."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Este campo no puede estar vacío"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Este grupo es el grupo primario para los siguientes usuarios:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr ""
+"El anfitrión se reiniciará cuando se hayan instalado las actualizaciones."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "La información sólo se almacenará en el sistema."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Este servidor de claves es el único método para desbloquear el grupo de "
+"almacenamiento y no puede ser eliminado."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+"Este volumen lógico ha perdido algunos de sus volúmenes físicos y ya no se "
+"puede utilizar. Debe eliminarlo y crear uno nuevo para ocupar su lugar."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+"Este volumen lógico ha perdido algunos de sus volúmenes físicos pero aún no "
+"ha perdido ningún dato. Debe repararlo para restaurar su redundancia "
+"original."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+"Este volumen lógico ha perdido algunos de sus volúmenes físicos, pero es "
+"posible que aún no haya perdido ningún dato. Es posible que puedas repararlo."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "El contenido de este volumen lógico no lo está usando por completo."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Esta máquina ha sido añadida."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Esto llevará un tiempo"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "El contenido de esta partición no la está usando por completo."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Esta contraseña es el único método para desbloquear el grupo de "
+"almacenamiento y no puede ser eliminada."
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr ""
+"Este grupo de almacenamiento no usa todo el espacio de sus dispositivos de "
+"bloques."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "Este grupo de almacenamiento está degradado."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Este sistema no está registrado"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "El sistema utiliza un perfil personalizado"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "El sistema utiliza el perfil recomendado"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"Esta herramienta configura la política de SELinux y puede ayudarle a "
+"comprender y resolver infracciones de la política."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"Esta herramienta configura el sistema para que escriba en disco los volcados "
+"de colapso del kernel."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"Esta herramienta genera un archivo de configuración e información de "
+"diagnóstico del sistema en ejecución. El archivo puede ser almacenado "
+"localmente o centralmente para propósitos de registro o seguimiento, o puede "
+"ser enviado a representantes de soporte técnico, desarrolladores o "
+"administradores de sistemas para asistir con la detección de fallos y su "
+"corrección."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"Esta herramienta gestiona el almacenamiento local, como sistemas de "
+"archivos, grupos de volúmenes LVM2 y montajes NFS."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"Esta herramienta gestiona la configuración de red como agrupaciones, "
+"puentes, grupos, las VLAN y cortafuegos usando NetworkManager y Firewalld. "
+"NetworkManager es incompatible con systemd-networkd habilitado por defecto "
+"en Ubuntu y con los scripts de ifupdown de Debian."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Esta unidad no está diseñada para que se habilite de forma explícita."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "El nombre del usuario ya existe"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr "A este grupo de volúmenes le faltan algunos volúmenes físicos."
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Este navegador web es demasiado viejo para ejecutar la consola Web (no "
+"soporta $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Esto añadirá una entrada para '_BOOT_ID=' en la búsqueda. Si no se "
+"especifica se mostrarán los registros del arranque actual. Si se omite el ID "
+"del arranque, un valor positivo se usará para buscar arranques desde el "
+"comienzo del registro, y un valor menor o igual que cero buscará los "
+"arranques desde el final del registro. Por ello, 1 significa el primer "
+"arranque encontrado en el registro por orden cronológico, 2 el segundo y "
+"así; mientras que -0 es el último arranque, -1 el anterior y así."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Esto añadirá entradas para '_SYSTEMD_UNIT', 'COREDUMP_UNIT=' y 'UNIT=' en la "
+"búsqueda para encontrar todos los mensajes posibles de dicha unidad. Se "
+"pueden añadir más unidades separadas por coma. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Esto le permitirá iniciar sesión sin contraseña en el futuro."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Esta zona contiene un servicio cockpit. Estese seguro de que esta zona no se "
+"aplica en su conexión con la consola web actual."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Los jueves"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Clase"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Hora"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Huso horario"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Fallo en la creación del temporizador"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "Fallo en la eliminación del temporizador"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Temporizadores"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Consejo: Haga coincidir la contraseña de su clave y su contraseña de acceso "
+"para autenticarse de forma automática en otros sistemas."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Para garantizar que su conexión no sea interceptada por un tercero "
+"malicioso, por favor verifique la huella de clave del anfitrión:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Para obtener actualizaciones de software, este sistema necesita registrarse "
+"en Red Hat, bien usando el portal cliente de Red Hat o un servidor de "
+"suscripción local."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Para verificar una huella, ejecute lo siguiente en $0 mientras se sitúa "
+"físicamente frente a la máquina o a través de una red de confianza:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Hoy"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Alternar"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Alternar el selector de fecha"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Alternar filtros"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Demasiados datos"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Herramientas"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Top 5 servicios de CPU"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "Top 5 servicios en uso de disco"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "Top 5 servicios de memoria"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Tamaño total: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Torre"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Transitorio"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Transmitiendo"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Disparador"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Desencadenado por"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Disparadores"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Soporte"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Resolución de errores…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "Confiar en el anfitrión y añadirlo"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Clave de confianza"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Nivel de confianza"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Intentar otra vez"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Intentando sincronizar con $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Los martes"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned ha fallado al iniciar"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned es un servicio que monitoriza su sistema y optimiza el rendimiendo "
+"ante ciertas cargas de trabajo. El núcleo de Tuned son los perfiles, que "
+"ecualizan su sistema para distintos tipos de necesidades."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned no está disponible"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned no se está ejecutando"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned está apagado"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Habilitar el acceso administrativo"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Tipo"
+
+#: pkg/storaged/partitions/partition.jsx:165
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Tipo sólo puede contener los caracteres de 0 a 9, de A a F y «-»."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr "Tipo debe tener el formato NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+"Tipo debe contener exactamente dos caracteres hexadecimales (0 a 9, A a F)."
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Escribe para filtrar"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "No se pudo aplicar esta solución automáticamente"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "No se pudo realizar una conexión con esa dirección"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "No se pudo contactar con $0."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"No se pudo contactar con el servidor $0. Asegúrese de que el servidor está "
+"ejecutando el servicio de SSH en el puerto $1, o especifique otro puerto en "
+"la dirección facilitada."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "No se pudo obtener detalles de la alerta."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"No se pudo iniciar sesión en $0 usando autenticación por clave SSH. Por "
+"favor, introduzca la contraseña. Quizás quiera configurar claves SSH para "
+"permitir inicios de sesión automáticos."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"No se pudo iniciar sesión en $0. El servidor no acepta inicio de sesión por "
+"contraseña ni ninguna de tus claves SSH."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Imposible conectar al servidor"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "No se pudo eliminar el montaje"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+msgid "Unable to repair logical volume $0"
+msgstr "No se puede reparar el volumen lógico $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "No se pudo ejecutar la correción: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "No se pudo aplicar los ajustes"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "No se pudo aplicar los ajustes: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "No se pudo iniciar setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "No se pudo desmontar el sistema de archivos"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "No disponible"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Paquetes no disponibles"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Deshacer"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "Error no esperado de PackageKit durante la instalación de $0: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Error inesperado"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+msgid "Unformatted data"
+msgstr "Datos sin formato"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Unidad"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Desconocido"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Desconocido \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Desconocido ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Aplicación desconocida"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Configuración desconocida"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Nombre del anfitrión desconocido"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Nombre del servicio desconocido"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Tipo desconocido"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Desbloquear"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Desbloquear automáticamente en el arranque"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr "Desbloquear antes de cambiar el tamaño"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Desbloquear grupo de almacenamiento Stratis cifrado"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "Desbloquear clave $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Desbloquendo $target"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Desbloqueando el disco"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Interfaces no gestionadas"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Desmontar"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "Desmontar el sistema de archivos $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Desmontar ahora"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Desmontando $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Sin nombre"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "Desfijar unidad"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Datos no reconocidos"
+
+#: pkg/storaged/block/resize.jsx:306
+msgid "Unrecognized data can not be made smaller here"
+msgstr ""
+"Los datos que no están reconocidos no se pueden hacer más pequeños aquí"
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr ""
+"Los datos que no están reconocidos no se pueden hacer más pequeños aquí."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+msgid "Unsupported logical volume"
+msgstr "Volumen lógico no soportado"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "Hasta"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Anfitrión no seguro"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Actualizar"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Tabla de actualización exitosa"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Histórico de actualización"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Actualizar la información de paquetes"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "La actualización fue correcta"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Actualizado"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr ""
+"Los paquetes actualizados pueden necesitar un reinicio para tomar efecto."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Actualizaciones disponibles"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Histórico de actualizaciones"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Las actualizaciones se aplicarán el $0 a las $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Actualizando"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Actualizando el estado..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Tiempo en línea"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Uso"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "Uso de $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Uso"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Usar compresión"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Usar deduplicación"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Usar clave"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Usar contraseña"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Se utilizan las siguientes claves para autenticarse con otros sistemas"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Usar registros detallados"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Usado"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"Útil para puntos de montaje que son opcionales o que necesitan interacción "
+"(como contraseñas)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Usuario"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "ID de Usuario (UID)"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "Otro usuario ya utiliza este UID"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "El UID debe ser un número entero positivo"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "El UID no debe ser superior a $0"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "El UID no debe ser inferior a $0"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Nombre de usuario"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "El nombre de usuario no puede estar vacío"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Nombre de usuario"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "Usar cifrado LUKS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Usar un servidor Tang"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "Los dispositivos de respaldo VDO no se pueden hacer más pequeños"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "Dispositivo VDO $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "Volumen de sistema de archivos VDO (compresión/desduplicación)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "ID de VLAN"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Validando dirección"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Validando código de autenticación"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Proveedor"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Verificado"
+
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "Verifique la huella"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Verificar clave"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Verificando"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Versión"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Borrado de forma muy segura $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Mostrar todas las CPUs"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Mostrar todos los discos"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Ver todos los registros"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Mostrar todos los servicios"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Ver el script de automatización"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Ver registros detallados"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Ver los detalles del hardware"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Ver el historial de inicios de sesión"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "Ver todos los registros"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Ver métricas e histórico"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Mostrar tráfico de cada disco"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Sitio Web del proyecto"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Ver el informe"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Ver registro de actualización"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "Necesita acceso administrativo para ver información sobre la memoria."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Ir al cortafuegos"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Grupo de volúmenes"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+msgid "Volume group is missing physical volumes"
+msgstr "Al grupo de volúmenes le faltan volúmenes físicos"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "El tamaño del volumen es $0. El tamaño del contenido es $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Esperando"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Esperando a los detalles..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Esperando por una entrada de datos…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"Esperando que otros programas terminen de usar el gestor de paquetes..."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Esperando a que finalicen otras operaciones de gestión de software"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Esperando para arrancar…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Buscado por"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Quiere"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Aviso y superior"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+msgid "Weak password"
+msgstr "Contraseña débil"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Consola Web"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Consola web para servidores Linux"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "La consola Web se reiniciará"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "La consola Web está en modo de acceso limitado."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Logotipo de la consola Web"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Los miércoles"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Semanalmente"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Semanas"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Intervalo"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "Si se deja vacío, se conectará usando el usuario actual"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Cuando se reinicie la consola Web, no podrás ver la información de progreso. "
+"Sin embargo, el proceso de actualización continuará de fondo. Reconéctate "
+"para continuar viendo el proceso de actualización."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"Si se activa esta opción, el nuevo grupo de almacenamiento no permitirá el "
+"sobreaprovisionamiento. Necesita especificar un tamaño máximo para cada "
+"sistema de archivos que se cree en el grupo de almacenamiento. Los sistemas "
+"de archivos no podrán ser extendidos después de su creación. Las "
+"instantáneas ocuparán su tamaño por completo desde su creación. La suma de "
+"todos los tamaños máximos no puede exceder el tamaño del grupo de "
+"almacenamiento. La ventaja de este sistema es que los sistemas de archivos "
+"en este grupo no podrán quedarse sin espacio de forma inesperada. Las "
+"desventajas son que necesita saber el tamaño máximo para cada sistema de "
+"archivos de antemano y que la creación de instantáneas está limitada."
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Blanco"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "Se establecerá a \"Automático\""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+msgid "World wide name"
+msgstr "Nombre World Wide"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Escritura"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Escribir casi todo"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Escribiendo"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Nombre de usuario o contraseña equivocada"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Anualmente"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Sí"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Se está conectando con $0 por primera vez."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Usted no está autorizado a modificar el cortafuegos."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"No tiene permiso para ver las claves públicas autorizadas para esta cuenta."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Se le ha cerrado la sesión por inactividad."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Intente cargar entradas antiguas."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+"Quizás quieras cambiar la contraseña de la clave para iniciar sesión "
+"automáticamente."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Debe esperar más tiempo para cambiar su contraseña"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr ""
+"Necesitas volver a iniciar sesión para poder ver el histórico de métricas"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Ahora tienes acceso administrativo."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Se cerrará la sesión en $0 segundos."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Su cuenta"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"Tu navegador no admite pegar desde el menú contextual. Puedes usar "
+"Shift+Insert."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "Su navegador recordará su nivel de acceso entre sesiones."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Su servidor cerrará la conexión pronto. Usted se puede volver a conectar "
+"después de que se haya reiniciado."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Su sesión se ha terminado."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Su sesión ha expirado. Por favor inicie sesión otra vez."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Zona"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[datos binarios]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[no hay datos]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "acceso"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "activo"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "complemento"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "complemento"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "después de la red"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "aplicaciones"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "etiqueta de propiedad"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "en"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "media: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "Dispositivo de respaldo para dispositivo VDO"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "agregación de enlaces"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "arrancar"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "puente"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+msgid "btrfs device"
+msgstr "Dispositivo btrfs"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+msgid "btrfs devices"
+msgstr "Dispositivos btrfs"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+msgid "btrfs subvolume"
+msgstr "Subvolumen btrfs"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr "Subvolumen btrfs $0 de $1"
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+msgid "btrfs subvolumes"
+msgstr "Subvolúmenes btrfs"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "Volumen btrfs"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "correción del fallo"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "bytes"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+msgid "cache"
+msgstr "Antememoria"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "cambiar"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman no está instalado"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "comando"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "consola"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "coredump"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "cpu"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "colapso"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+msgid "data"
+msgstr "datos"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "fecha"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+msgid "deactivate"
+msgstr "desactivar"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "depurar"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "Eliminar"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr "dispositivo de volumen btrfs"
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "desactivar"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "disco"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "discos"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "dominio"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "disco"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "editar"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "activar"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+msgid "encrypted"
+msgstr "cifrado"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "cifrado"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "mejora"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "error"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "cada viernes"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "cada lunes"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "cada sábado"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "cada domingo"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "cada jueves"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "cada martes"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "cada miércoles"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "cada día"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "extensión"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "no se pudo mostrar las claves ssh del anfitrión: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "sistema de archivos"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "cortafuegos"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "para el actual y futuros kernels"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "sólo en el kernel actual"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "formato"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "desde $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "desde $0 en $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "gráficos"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "incrementar"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "hardware"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "histórico"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "anfitrión"
+
+#: pkg/storaged/drive/drive.jsx:65
+msgid "iSCSI Drive"
+msgstr "Disco iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+msgid "iSCSI drives"
+msgstr "Discos iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:45
+msgid "iSCSI portal"
+msgstr "portal de iSCSI"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "se ignora fallo"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "en la mayoría de los navegadores"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "inconsistente"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "inicializar"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "instalar"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "interfaz"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "no válido: definidos múltiples objetivos"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "journal"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "Página de manual de journalctl"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "estado de kdump"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "teclas"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "login"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "máquina"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "máscara"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "máx: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+msgid "member of MDRAID device"
+msgstr "Miembro de dispositivo MDRAID"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "miembro del grupo de almacenamiento Stratis"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "memoria"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "métricas"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "migitación"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "más detalles"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "mount"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "red"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "nunca se monta en el arranque"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "la ruta al fichero export está vacía"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "la dirección del servidor nfs está vacía"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "el servidor nfs no es una dirección IPv6 válida"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "nice"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "ninguno"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "de $0 CPU"
+msgstr[1] "de $0 núcleos de CPU"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "desde $1"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "sistema operativo"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "so"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "paquete"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "partición"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "contraseña"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "calidad de la contraseña"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "parches"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "ruta"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "rendimiento"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "volumen físico de un grupo de volúmenes LVM2"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "plugin"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service ha fallado"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "El servicio pmlogger.service no puede recolectar datos"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service no se está ejecutando"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "pod"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "puerto"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "corriente"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "solo lectura"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "recomendado"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "quitar de LVM2"
+
+#: pkg/storaged/utils.js:934
+msgid "remove from MDRAID"
+msgstr "quitar del MDRAID"
+
+#: pkg/storaged/utils.js:904
+msgid "remove from btrfs volume"
+msgstr "quitar del volumen btrfs"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "reiniciar"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "roles"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "ejecutando $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "seguridad"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "serie"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "servicio"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "shell"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "mostrar menos"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "mostrar más"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "reducir"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "cerrar"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "socket"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "solución"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "detalles de solución"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "Fallo al crear el informe sos"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "La clave ssh no es una ruta"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "el servidor ssh está vacío"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "detener"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "se detiene el arranque si falla"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+msgid "stopped"
+msgstr "detenido"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sistema"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "target"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "equipo"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "hora"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "temporizador"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "unit"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "desconocido"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "destino u objetivo desconocido"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "desenmascarar"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "desmontar"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "espacio no particionado en $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "usuario"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "nombre de usuario"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "utilizando la descripción de clave $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "versión"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "volumen"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "aviso"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "El paquete wireguard-tools no está instalado"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "sí"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "zona"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "No está instalado el servicio Kdump. Asegúrese de que esté instalado el "
+#~ "paquete kexec-tools."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "No se ha reservado memoria. Anexe una opción de crashkernel a la línea de "
+#~ "comandos del kernel (p. ej. en /etc/default/grub) para reservar memoria "
+#~ "durante el arranque. Ejemplo: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Se está ejecutando el servicio"
+
+#~ msgid "Service is starting"
+#~ msgstr "Se está iniciando el servicio"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Se ha detenido el servicio"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Se está deteniendo el servicio"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Esto comprobará la configuración de kdump colapsando el kernel."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr ""
+#~ "crashkernel no está configurado en los parámetros de arranque del núcleo"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (cifrado)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "Grupo de almacenamiento Stratis de $0"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 caché"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 de tipo desconocido"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 disponibles"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "Debe agregarse un disco de recambio primero antes de quitar este."
+
+#~ msgid "Backing device"
+#~ msgstr "Dispositivo de respaldo"
+
+#~ msgid "Block"
+#~ msgstr "Bloque"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Capacidad"
+
+#~ msgid "Content"
+#~ msgstr "Contenido"
+
+#~ msgid "Create devices"
+#~ msgstr "Crear dispositivos"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Dispositivo"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Fichero de dispositivo"
+
+#~ msgid "Devices"
+#~ msgstr "Dispositivos"
+
+#~ msgid "Drives"
+#~ msgstr "Discos"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Es necesario que los volúmenes cifrados estén desbloqueados para poder "
+#~ "cambiar sus tamaños."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Sistema de archivos (cifrado)"
+
+#~ msgid "Filesystems"
+#~ msgstr "Sistema de archivos"
+
+#~ msgid "Free"
+#~ msgstr "Libre"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Libere espacio en este grupo: reduzca o elimine otros volúmenes lógicos o "
+#~ "añada otro volumen físico."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Volumen inactivo"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Servidor de claves"
+
+#~ msgid "LVM2 member"
+#~ msgstr "Miembro de LVM2"
+
+#~ msgid "Locked devices"
+#~ msgstr "Dispositivos bloqueados"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Datos cifrados y bloqueados"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Modelo"
+
+#~ msgid "NFS mounts"
+#~ msgstr "Montajes NFS"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "NFS support not installed"
+#~ msgstr "No está instalado el soporte para NFS"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "No hay ajustes de montaje NFS"
+
+#~ msgid "No devices"
+#~ msgstr "No hay dispositivos"
+
+#~ msgid "No drives attached"
+#~ msgstr "No hay dispositivos montados"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "No se han ajustados los objetivos iSCSI"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "No hay suficiente espacio para nuevos sistemas de archivos"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Otros datos"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Dispositivo de bloques particionado"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Contraseña"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "No se puede modificar el tamaño de los volúmenes físicos aquí."
+
+#~ msgid "Pool"
+#~ msgstr "Grupo"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Grupo de aprovisionamiento de volúmenes finos"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "Nivel de RAID"
+
+#~ msgid "RAID member"
+#~ msgstr "Miembro del RAID"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Disco extraíble"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Mostrar $0 dispositivo"
+#~ msgstr[1] "Mostrar todos los $0 dispositivos"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Mostrar $0 disco"
+#~ msgstr[1] "Mostrar todos los $0 discos"
+
+#~ msgid "Show all"
+#~ msgstr "Mostrar todo"
+
+#~ msgid "Source"
+#~ msgstr "Fuente"
+
+#~ msgid "Start pool"
+#~ msgstr "Iniciar grupo de almacenamiento"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr ""
+#~ "Inicie el grupo de almacenamiento para ver los sistemas de archivos."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Estado"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Grupo de almacenamiento Stratis detenido"
+
+#~ msgid "Stratis member"
+#~ msgstr "Miembro de Stratis"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Grupo de almacenamiento Stratis $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "El soporte está instalado."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "El dispositivo RAID debe estar ejecutándose para añadir discos de "
+#~ "repuesto."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "El último disco del dispositivo RAID no se puede eliminar."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr ""
+#~ "El último volumen físico de un grupo de volumen no se puede eliminar."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "No hay suficiente espacio físico en otro lugar para borrar este volumen "
+#~ "físico. Es necesario al menos $0 más de espacio libre."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr ""
+#~ "El disco no se puede eliminar mientras el dispositivo se esté recuperando."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr ""
+#~ "Este volumen se necesita activar antes de poder modificar el tamaño."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Dato desnocnocido"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Uso"
+
+#~ msgid "Used for"
+#~ msgstr "Usado para"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "Respaldo VDO"
+
+#~ msgid "VDO device"
+#~ msgstr "Dispositivo VDO"
+
+#~ msgid "Volume"
+#~ msgstr "Volumen"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automático (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Aceptar la clave y conectar"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "No se puede cambiar el tamaño de los volúmenes cifrados desde aquí."
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Utilizar un servidor de claves Tang"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Use a passphrase"
+#~ msgstr "Utilizar una contraseña"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Mapa de bits"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Este grupo de almacenamiento no puede ser desbloqueado aquí porque su "
+#~ "formato de descripción de clave no es el esperado."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Alternar bitmap"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Asegúrese de que el hash de la clave del servidor Tang coincida con uno "
+#~ "de los siguientes:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Comprobar manualmente con SSH: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "El número de puerto y el tipo no coinciden"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Grupo de almacenamiento Stratis bloqueado y cifrado"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Error mientras se eliminaba la alerta: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Incapaz de obtener la alerta: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 bites de datos binarios]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Pico de E/S de disco"
+
+#~ msgid "Event"
+#~ msgstr "Evento"
+
+#~ msgid "Event logs"
+#~ msgstr "Registro de eventos"
+
+#~ msgid "Load spike"
+#~ msgstr "Pico de Load"
+
+#~ msgid "Memory spike"
+#~ msgstr "Pico de memoria"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Pico de E/S de red"
+
+#~ msgid "ssh key"
+#~ msgstr "clave ssh"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Si se marca esta opción, el sistema de archivos no será montado durante "
+#~ "el siguiente arranque incluso si ya lo estaba antes. Esto es útil si no "
+#~ "es posible montarlo durante el arranque, por ejemplo cuando se requiere "
+#~ "una contraseña para desbloquear el sistema de archivos y se usa un inicio "
+#~ "desatendido."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Nunca montar en el arranque"
+
+#~ msgid "Listing unit files"
+#~ msgstr "Listando los archivos de unidad"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "Fallo al listar los archivos de unidad: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "Unidad no encontrada"
+
+#~ msgid "Validating key"
+#~ msgstr "Validando clave"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "Eliminar grupo $0"
+
+#~ msgid "Container administrator"
+#~ msgstr "Administrador de contenedores"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Image builder"
+#~ msgstr "Constructor de imágenes"
+
+#~ msgid "Roles"
+#~ msgstr "Roles"
+
+#~ msgid "Server administrator"
+#~ msgstr "Administrador del servidor"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Grupo Unix: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Copiado al portapapeles"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Informes de diagnóstico"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Volcado del kernel"
+
+#~ msgid "Software Updates"
+#~ msgstr "Actualizaciones de software"
+
+#~ msgid "Update log"
+#~ msgstr "Registro de actualización"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "nfs dump target no está declarado como servidor:ruta"
+
+#~ msgid "$0 Zone"
+#~ msgstr "Zona $0"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Informes de diagnóstico"
+
+#~ msgid "Done!"
+#~ msgstr "¡Hecho!"
+
+#~ msgid "Download report"
+#~ msgstr "Descargar informe"
+
+#~ msgid "No archive has been created."
+#~ msgstr "El archivo no se ha creado."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Confirme la eliminación de $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "El archivo generado contiene datos que se consideran confidenciales y su "
+#~ "contenido debería poderse revisar por la organización de origen antes de "
+#~ "que se transfiera a cualquier tercero."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Esta herramienta recogerá la configuración e información de diagnóstico "
+#~ "desde este sistema para averiguar problemas que tengan en el sistema."
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "Falta el paquete cockpit-system en el servidor"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Este paquete no es compatible con esta versión de Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Este paquete requiere Cockpit versión %s o posterior"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Métricas de rendimiento"
+
+#~ msgid "Reboot to apply new crypto policy"
+#~ msgstr "Reiniciar para aplicar las nuevas políticas de criptografía"
+
+#~ msgid "Save only"
+#~ msgstr "Sólo guardar"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB total"
+
+#~ msgid "Apply"
+#~ msgstr "Aplicar"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "No está autorizado para eliminar la zona $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Ha clic $0 de nuevo para utilizar la misma contraseña."
+
+#~ msgid "Access"
+#~ msgstr "Acceso"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "EL DISCO ESTÁ FALLANDO"
+
+#~ msgid "Logical Size"
+#~ msgstr "Tamaño lógico"
+
+#~ msgid "Security updates "
+#~ msgstr "Actualizaciones de seguridad "
+
+#~ msgid "Updates "
+#~ msgstr "Actualizaciones "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Opcional)"
+
+#~ msgid "Active since"
+#~ msgstr "Activo desde"
+
+#, fuzzy
+#~| msgid "Current allocation"
+#~ msgid "Affected locations"
+#~ msgstr "Asignación actual"
+
+#~ msgid "Process"
+#~ msgstr "Proceso"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Remove and delete"
+#~ msgstr "Parar y eliminar"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and format"
+#~ msgstr "Formato de acceso"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and grow"
+#~ msgstr "Formato de acceso"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and initialize"
+#~ msgstr "Formato de acceso"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and shrink"
+#~ msgstr "Formato de acceso"
+
+#, fuzzy
+#~| msgid "Remove device"
+#~ msgid "Remove and stop device"
+#~ msgstr "Eliminar el dispositivo"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "El sistema de archivos está en uso por sesiones de usuarios y servicios "
+#~ "del sistema. Procediendo a detenerlos."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "El sistema de archivos está en uso por sesiones de usuario. Procediendo a "
+#~ "detenerlos."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Hay servicios del sistema utilizando el sistema de archivos. Procediendo "
+#~ "a pararlos."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Este dispositivo tiene sistemas de archivos en uso. Procediendo a "
+#~ "desmontar todos los sistemas de archivos que hay en él."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Este dispositivo está utilizándose por grupos de volúmenes."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#, fuzzy
+#~| msgid ""
+#~| "This device is currently used for volume groups. Proceeding will remove "
+#~| "it from its volume groups."
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Este dispositivo se está utilizando en grupos de volúmenes. Procediendo a "
+#~ "eliminarlos desde sus grupos de volúmenes."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Este dispositivo se está utilizando por dispositivos RAID."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Hay dispositivos RAID que están utilizando este dispositivo. Procediendo "
+#~ "a eliminarlos desde los dispositivos RAID."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Este dispositivo está utilizándose por grupos de volúmenes."
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Unmount and delete"
+#~ msgstr "Parar y eliminar"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and format"
+#~ msgstr "Desmontar ahora"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and grow"
+#~ msgstr "Desmontar ahora"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and initialize"
+#~ msgstr "Desmontar ahora"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and shrink"
+#~ msgstr "Desmontar ahora"
+
+#, fuzzy
+#~| msgid "On a mounted device"
+#~ msgid "Unmount and stop device"
+#~ msgstr "En un dispositivo montado"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 de $1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "Bloqueado"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Volumen físico de $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Miembro de un dispositivo RAID $0"
+
+#~ msgid "VDO backing"
+#~ msgstr "Respaldo VDO"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Crear un grupo de almacenamiento"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "Opciones de montaje"
+
+#, fuzzy
+#~| msgid "Reset Storage Pool"
+#~ msgid "Stratis Pool"
+#~ msgstr "Restablecer grupo de almacenamiento"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Es necesario un disco."
+
+#~ msgid "Disk"
+#~ msgstr "Disco"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Solo para aplicaciones residuales. Reduce el rendimiento."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Instalar el soporte para VDO"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Usar emulación de 512 Byte"
+
+#~ msgid "System services"
+#~ msgstr "Servicios del sistema"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Crear un informe de diagnóstico "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr ""
+#~ "Conectarse simultáneamente a un número de máquinas superior a {{ limit }} "
+#~ "no está soportado."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "SSO Basado en Kerberos"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Iniciar sesión en {{host}}"
+
+#, fuzzy
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Las contraseñas no coinciden"
+
+#, fuzzy
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit no pudo contactar con {{#strong}}{{host}}{{/strong}}."
+
+#, fuzzy
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Cockpit no pudo acceder a {{#strong}}{{host}}{{/strong}}. {{#can_sync}}"
+#~ "Quizás pueda probar a {{#sync_link}}sincronizar los usuarios{{/"
+#~ "sync_link}}.{{/can_sync}} Para obtener más opciones de autenticación y "
+#~ "asistencia para solucionar problemas, actualice cockpit-ws a una versión "
+#~ "más reciente."
+
+#, fuzzy
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Cockpit no pudo acceder a {{#strong}}{{host}}{{/strong}}. Para usar está "
+#~ "máquina con Cockpit, necesitará activar uno de los métodos de "
+#~ "autenticación siguientes en la configuración del servicio SSH {{#strong}}"
+#~ "{{host}}{{/strong}}:"
+
+#, fuzzy
+#~| msgid "$0 key changed"
+#~ msgid "{{host}} key changed"
+#~ msgstr "$0 clave cambiada"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Limpiar la configuración de los puntos de montaje"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Crear este balanceo cerrará la conexión con el servidor y hará que la "
+#~ "consola de administración no esté disponible."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Crear este puente cerrará la conexión con el servidor y hará que la "
+#~ "consola de administración no esté disponible."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Crear este equipo cerrará la conexión con el servidor y hará que la "
+#~ "consola de administración no esté disponible."
+
+#, fuzzy
+#~| msgid "IPv4 settings"
+#~ msgid "IP settings"
+#~ msgstr "Configuración IPv4"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Apagando <b>$0</b> cerrará la conexión con el servidor y hará que la "
+#~ "consola de administración no esté disponible."
+
+#~ msgid "Administrator password"
+#~ msgstr "Contraseña de Administrador"
+
+#~ msgid "Computer OU"
+#~ msgstr "Ordenador OU"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr ""
+#~ "Eliminar un dispositivo RAID borrará toda la información que hay en él."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr ""
+#~ "Eliminar un grupo de volúmenes borrará toda la información que haya en él."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "No sobreescribir los datos existentes"
+
+#~ msgid "Erase"
+#~ msgstr "Eliminar"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Formatear el disco $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr ""
+#~ "Formatear un dispositivo de almacenamiento eliminará todos los datos en "
+#~ "el."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "El nombre del anfitrión no debería cambiarse en un dominio"
+
+#~ msgid "More"
+#~ msgstr "Más"
+
+#~ msgid "Not joined"
+#~ msgstr "No unido"
+
+#~ msgid "One time password"
+#~ msgstr "Contraseña de un solo uso"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 desde $1 en $2"
+
+#, fuzzy
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "La hora tiene que tener un número entre 0 y 23"
+
+#~ msgid "Last login:"
+#~ msgstr "Último inicio de sesión:"
+
+#, fuzzy
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "El minuto debe ser un número comprendido entre 0 y 59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "Hubo $ 0 intento fallido de inicio de sesión desde el último inicio de "
+#~ "sesión exitoso."
+#~ msgstr[1] ""
+#~ "Hubo $ 0 intentos fallidos de inicio de sesión desde el último inicio de "
+#~ "sesión exitoso."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "p. ej., \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 Zonas activas"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$1 ocurrencia"
+#~ msgstr[1] "$0 ocurrencias"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Licenciado bajo:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Desbloquear al arranque"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Desbloquear solo lectura"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Encontrar registros con una combinación de términos:"
+
+#~ msgid "Show filters"
+#~ msgstr "Mostrar filtros"
+
+#, fuzzy
+#~ msgid "e.g."
+#~ msgstr "p. ej., \"$0\""
+
+#, fuzzy
+#~ msgid "Toggle session settings"
+#~ msgstr "Cambiar los ajustes"
+
+#~ msgid "(none)"
+#~ msgstr "(ninguno)"
+
+#~ msgid "Create timers"
+#~ msgstr "Crear temporizadores"
+
+#~ msgid "Recommended default"
+#~ msgstr "Recomendado por defecto"
+
+#~ msgid "Run"
+#~ msgstr "Ejecutar"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select unit state"
+#~ msgstr "Tipo de consola"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Habilitar guardado de métricas"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Habilitar guardado de métricas"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Forzar la eliminación de la contraseña en $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Si tang-show-keys no está disponible, ejecute lo siguiente:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "¿Qué pasa si tang-show-keys no está disponible?"
+
+#~ msgid "key slot $0"
+#~ msgstr "compartimento para claves $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Algo fue equivocado"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "Esto no ha funcionado; inténtelo de nuevo"
+
+#, fuzzy
+#~| msgid "Automatic updates"
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Actualizaciones automáticas"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "Configuración de CPU"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 Red"
+#~ msgstr[1] "$0 Redes"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 está disponible en la mayoría de los sistemas operativos. Para "
+#~ "instalarlo, búsquelo en Software de GNOME o ejecute la orden siguiente:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 ajuste de memoria"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 Red"
+
+#, fuzzy
+#~| msgid "vCPUs"
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "vCPUs"
+#~ msgstr[1] "vCPUs"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 detalles de vCPU"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 configuraciones de interfaz de red virtual"
+
+#~ msgid "Add network interface"
+#~ msgstr "Añadir una interfaz de red"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Añadir una interfaz de red virtual"
+
+#~ msgid "Additional"
+#~ msgstr "Adicional"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "La dirección no está dentro de una subred"
+
+#~ msgid "Always attach"
+#~ msgstr "Siempre adjunto"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Al añadirlo, este disco será compartido por todas las VM que lo utilicen."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Iniciar automáticamente libvirt en el arranque"
+
+#~ msgid "Autostart"
+#~ msgstr "Inicio automático"
+
+#~ msgid "Boot order"
+#~ msgstr "Orden de arranque"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "La configuración del orden de arranque no se pudo guardar"
+
+#~ msgid "Bus"
+#~ msgstr "Bus"
+
+#, fuzzy
+#~| msgid "VCPU settings could not be saved"
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "No se pudo guardar la configuración de la vCPU"
+
+#~ msgid "CPU type"
+#~ msgstr "Tipo de CPU"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Change firmware"
+#~ msgstr "Cambiar el firmware"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Los cambios entrarán en vigor después de apagar la máquina virtual"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Escoge un sistema operativo"
+
+#, fuzzy
+#~| msgid ""
+#~| "Clicking \"Launch Remote Viewer\" will download a .vv file and launch $0."
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "Haciendo clic \"Lanzar visor remoto\" hará que se descargue un archivo "
+#~ "con la extensión .vv y también se ejecutará $0."
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "No se puede cargar la imagen"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Confirme esta acción"
+
+#~ msgid "Connect"
+#~ msgstr "Conectar"
+
+#, fuzzy
+#~| msgid "Connect with any $0 viewer application."
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "Conectar con algún visor de aplicación $0."
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Conectándose al servicio de virtualización"
+
+#~ msgid "Connection"
+#~ msgstr "Conexión"
+
+#, fuzzy
+#~| msgid "Consoles"
+#~ msgid "Console"
+#~ msgstr "Consolas"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.6, DocId:
+# cockpit
+#~ msgid "Cores per socket"
+#~ msgstr "Núcleos por socket"
+
+#, fuzzy
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "No podría reiniciar el pool de almacenamiento"
+
+#, fuzzy
+#~| msgid "crashed"
+#~ msgid "Crashed"
+#~ msgstr "cerrado"
+
+#~ msgid "Create VM"
+#~ msgstr "Crear una MV"
+
+#, fuzzy
+#~| msgid "Create partition on $0"
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Crear una partición en $0"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-7.6, DocId:
+# cockpit
+#~ msgid "Create new"
+#~ msgstr "Crear"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Crear una máquina virtual"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Crear una red virtual"
+
+#, fuzzy
+#~ msgid "Creating VM"
+#~ msgstr "Crear una MV"
+
+#, fuzzy
+#~ msgid "Creating VM installation"
+#~ msgstr "Ejecutar la instalación desatendida"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "La creación de la MV $0 falló"
+
+#~ msgid "Creation time"
+#~ msgstr "Hora de creación"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Asignación actual"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Firmware específico: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "Rango DHCP"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Eliminar archivos de almacenamiento asociados:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Eliminar el grupo de almacenamiento $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Eliminar los volúmenes que están dentro de este grupo"
+
+#, fuzzy
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Eliminar un grupo de almacenamiento solo borrará la información que no "
+#~ "esté definida. Su contenido no se eliminará."
+
+#, fuzzy
+#~| msgid "Desktop"
+#~ msgid "Desktop viewer"
+#~ msgstr "Escritorio"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Desmontando los discos utilizando este grupo desde cualquiera de las MVs "
+#~ "antes de intentar eliminarlo."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr ""
+#~ "Desconectado desde la consola serial. Haga clic en el botón de conectar."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "El disco $0 no se desconecta de la máquina virtual $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "El disco no se pudo montar"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "El disco no se pudo crear"
+
+#, fuzzy
+#~| msgid "Device file"
+#~ msgid "Disk image file"
+#~ msgstr "Fichero de dispositivo"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Las configuraciones del disco no se pudieron guardar"
+
+#~ msgid "Download an OS"
+#~ msgstr "Descargar un SO"
+
+#, fuzzy
+#~| msgid "dying"
+#~ msgid "Dying"
+#~ msgstr "agonizante"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Máquina emulada"
+
+#~ msgid "End"
+#~ msgstr "Terminar"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Terminar no debe de estar vacío"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Existe una imagen de disco en el sistema de archivos del anfitrión"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Falló al cambiar el firmware"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr ""
+#~ "Falló al recuperar la dirección IP de las interfaces presentes en $0"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "Falló al enviar las teclas Ctrl+Alt+$0 a la VM $1"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr "Deben habilitarse menos CPU virtuales de las permitidas."
+
+#~ msgid "File"
+#~ msgstr "Archivo"
+
+#, fuzzy
+#~ msgid "Filter by name"
+#~ msgstr "Filtrar por nombre o descripción"
+
+#~ msgid "Firmware"
+#~ msgstr "Firmware"
+
+#~ msgid "Force shut down"
+#~ msgstr "Forzar el apagado"
+
+#~ msgid "Forward mode"
+#~ msgstr "Modo de redireccionamiento"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Modo de reenvío"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Generado automáticamente"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Ocultar opciones adicionales"
+
+#~ msgid "Host device"
+#~ msgstr "Dispositivo anfitrión"
+
+#~ msgid "Host name"
+#~ msgstr "Nombre del anfitrión"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "El anfitrión no debe estar vacío"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Detalles del hipervisor"
+
+#~ msgid "IP configuration"
+#~ msgstr "Configuración de la IP"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 e IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "Red IPv4"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "La red IPv4 no debería estar vacía"
+
+#~ msgid "IPv4 only"
+#~ msgstr "Solo IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "Dirección IPv6"
+
+#~ msgid "IPv6 network"
+#~ msgstr "Red IPv6"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "La red IPv6 no debería estar vacía"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Solo IPv6"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Iniciar inmediatamente la MV"
+
+#~ msgid "Import"
+#~ msgstr "Importar"
+
+#~ msgid "Import VM"
+#~ msgstr "Importar una MV"
+
+#, fuzzy
+#~ msgid "Import a virtual machine"
+#~ msgstr "Importar una máquina virtual"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "En muchas configuraciones, macvtap no funciona para un comunicación de "
+#~ "red anfitrión-huésped."
+
+#~ msgid "Initiator"
+#~ msgstr "Iniciador"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "El iniciador IQN no debería estar vacío"
+
+#, fuzzy
+#~| msgid "Installation source"
+#~ msgid "Installation source"
+#~ msgstr "Fuente de la instalación"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "La instalación del código no debería estar vacío"
+
+#~ msgid "Installation type"
+#~ msgstr "Tipo de instalación"
+
+#~ msgid "Interface type"
+#~ msgstr "Tipo de interfaz"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Máscara o longitud del prefijo de IPv4 inválidos"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Dirección IPv6 inválida"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Prefijo IPv6 inválido"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Nombre de fichero no válido"
+
+#, fuzzy
+#~| msgid "Launch Remote Viewer"
+#~ msgid "Launch remote viewer"
+#~ msgstr "Lanzar visualizador remoto"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr ""
+#~ "Deje la contraseña en blanco si no quiere crear una cuenta de usuario root"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "Deje la contraseña en blanco si no quiere tener una cuenta de usuario "
+#~ "creada"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr ""
+#~ "Deje la contraseña en blanco si no quiere crear una cuenta de usuario root"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt no detecta ninguna imagen de firmware de UEFI/OVMF instalada en "
+#~ "el anfitrión"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt o el hipervisor no soportan UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "Cargando recursos"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "La máquina debe estar apagada antes de cambiar el tipo de bus"
+
+#, fuzzy
+#~| msgid "Machine must be shut off before changing bus type"
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "La máquina debe estar apagada antes de cambiar el tipo de bus"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Gestionando las máquinas virtuales"
+
+#~ msgid "Manual connection"
+#~ msgstr "Conexión manual"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Máscara o longitud del prefijo"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "La máscara o la longitud del prefijo no debería estar vacío"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Asignación máxima"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "La memoria máxima no se pudo guardar"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#, fuzzy
+#~| msgid ""
+#~| "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~| "between 1 and $0"
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Número máximo de CPU virtuales asignadas al SO invitado, entre 1 y $0"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Número máximo de CPU virtuales asignadas al SO invitado, entre 1 y $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Unidad máxima de transmisión"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "La memoria no se pudo guardar"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "La memoria no debe ser 0"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Tipo de modelo"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT para $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "NIC $0 de la VM $1 falló al cambiar de estado"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "El nombre contiene caracteres no válidos"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Nombre no debe estar vacío"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Nombre no debería estar vacío"
+
+#~ msgid "Name: "
+#~ msgstr "Nombre: "
+
+#~ msgid "Netmask"
+#~ msgstr "Máscara de red"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Falló al activarse la red $0"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Falló al activarse la red $0"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Arranque vía red (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Sistema de archivos de red"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "No se pudo guardar la configuración de la interfaz de red"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "La selección de la red no soporta PXE."
+
+#~ msgid "Networks"
+#~ msgstr "Redes"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "New volume name"
+#~ msgstr "Nuevo nombre para el volumen"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "No hay ninguna MV en ejecución o definida en este anfitrión"
+
+#, fuzzy
+#~| msgid "No description available"
+#~ msgid "No connection available"
+#~ msgstr "No hay una descripción disponible"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "No se han definido discos para esta MV"
+
+#~ msgid "No network devices"
+#~ msgstr "No hay dispositivos de red"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "No se han definido interfaces de red para esta MV"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "No hay una red definida en este anfitrión"
+
+#~ msgid "No networks available"
+#~ msgstr "No hay redes disponibles"
+
+#, fuzzy
+#~ msgid "No parent"
+#~ msgstr "Padre"
+
+#, fuzzy
+#~| msgid "No disks defined for this VM"
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "No se han definido discos para esta MV"
+
+#, fuzzy
+#~| msgid "No storage"
+#~ msgid "No state"
+#~ msgstr "No hay almacenamiento"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "No hay un grupo de almacenamiento definido en este anfitrión"
+
+#~ msgid "No storage pools available"
+#~ msgstr "No hay grupos de almacenamiento disponibles"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr ""
+#~ "No hay volúmenes de almacenamiento definidos para este grupo de "
+#~ "almacenamiento"
+
+#~ msgid "No virtual networks"
+#~ msgstr "No hay redes virtuales"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "La red no persistente no se puede eliminar. Dejará de existir cuando está "
+#~ "desactivado."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "El grupo de almacenamiento no persistente no se puede eliminar. Dejará de "
+#~ "existir cuando está desactivado."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Ninguno (Red aislada)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Hay dominios que están utilizando uno o más volúmenes de los que se han "
+#~ "seleccionado. Desmonte primero los discos para poder eliminar los "
+#~ "volúmenes."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Solo se puede editar cuando el huésped esté apagado"
+
+#~ msgid "Open"
+#~ msgstr "Abrir"
+
+#~ msgid "Operating system"
+#~ msgstr "Sistema operativo"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "La operación está en progreso"
+
+#, fuzzy
+#~ msgid "Parent snapshot"
+#~ msgstr "Crear una instantánea"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Ruta en el sistema de archivos del anfitrión"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Ruta hacia la imagen ISO en el sistema de archivos del anfitrión"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Ruta hacia la imagen ISO en el sistema de archivos del anfitrión"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Ruta hacia la imagen ISO en el sistema de archivos del anfitrión"
+
+#, fuzzy
+#~| msgid "Pause"
+#~ msgid "Paused"
+#~ msgstr "Pausar"
+
+#~ msgid "Persistence"
+#~ msgstr "Persistencia"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Dispositivo de disco físico"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Dispositivo de disco físico en el anfitrión"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Escoja un grupo de almacenamiento"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Please choose a volume"
+#~ msgstr "Seleccione un volumen"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Please enter new volume name"
+#~ msgstr "Introduzca el nuevo nombre del volumen"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Por favor arranque la maquina virtual para acceder a su consola."
+
+#~ msgid "Plug"
+#~ msgstr "Enchufe"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "El tipo de grupo no soporta la creación de volúmenes"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "Las MVs están utilizando un grupo de volúmenes: "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Número preferido de sockets para exponerle al huésped."
+
+#~ msgid "Prefix"
+#~ msgstr "Prefijo"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "La longitud del prefijo no debe estar vacía"
+
+#~ msgid "Product"
+#~ msgstr "Producto"
+
+#~ msgid "Profile"
+#~ msgstr "Perfil"
+
+#~ msgid "Protocol"
+#~ msgstr "Protocolo"
+
+#~ msgid "Remote URL"
+#~ msgstr "URL remota"
+
+#, fuzzy
+#~| msgid "Hypervisor details"
+#~ msgid "Remote viewer details"
+#~ msgstr "Detalles del hipervisor"
+
+#, fuzzy
+#~ msgid "Revert"
+#~ msgstr "Nunca"
+
+#, fuzzy
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Crear una instantánea"
+
+#~ msgid "Root password"
+#~ msgstr "Contraseña del usuario root"
+
+#~ msgid "Route to $0"
+#~ msgstr "Ruta para $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Ejecutar la instalación desatendida"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Ejecutar cuando el anfitrión arranque"
+
+#, fuzzy
+#~| msgid "SPICE TLS port:"
+#~ msgid "SPICE TLS port"
+#~ msgstr "Puerto SPICE TLS:"
+
+#, fuzzy
+#~| msgid "SPICE address:"
+#~ msgid "SPICE address"
+#~ msgstr "Dirección SPICE:"
+
+#, fuzzy
+#~| msgid "SPICE port:"
+#~ msgid "SPICE port"
+#~ msgstr "Puerto SPICE:"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select console type"
+#~ msgstr "Tipo de consola"
+
+#~ msgid "Send key"
+#~ msgstr "Tecla enviar"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Enviar interrupción no enmascarable"
+
+#~ msgid "Serial console"
+#~ msgstr "Consola serie"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Establecer un rango DHCP"
+
+#~ msgid "Set manually"
+#~ msgstr "Establecer manualmente"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "Establecer las contraseñas de usuario para la instalación desatendida "
+#~ "requiere iniciar la MV al crearla"
+
+#~ msgid "Show additional options"
+#~ msgstr "Mostrar opciones adicionales"
+
+#, fuzzy
+#~ msgid "Shut off"
+#~ msgstr "apagar"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "Apague la MV para editar la configuración del firmware"
+
+#, fuzzy
+#~ msgid "Shutting down"
+#~ msgstr "Apagar"
+
+#, fuzzy
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "El disco no se pudo crear"
+
+#~ msgid "Source format"
+#~ msgstr "Formato fuente"
+
+#~ msgid "Source path"
+#~ msgstr "Dirección de origen"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "La ruta de origen no debe estar vacía"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "La fuente debería empezar con http, ftp o protocolo nfs"
+
+#~ msgid "Source volume group"
+#~ msgstr "Origen del grupo de volumen"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Iniciar libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Iniciar el grupo cuando el anfitrión arranque"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Empezar no debería estar vacío"
+
+#~ msgid "Startup"
+#~ msgstr "Puesta en marcha"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "El grupo de almacenamiento $0 falló al activarse"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "El grupo de almacenamiento $0 falló al desactivarse"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "No se pudo crear el grupo de almacenamiento"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Nombre del grupo de almacenamiento"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "El tamaño del almacenamiento no debe ser 0"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Los volúmenes de almacenamiento no se pueden eliminar"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "suspendido (PM)"
+
+#~ msgid "Target path"
+#~ msgstr "Ruta de destino"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "La ruta de destino no debe estar vacía"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr ""
+#~ "La MV está ejecutándose y se forzará el apagado antes de la eliminación."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "La máquina virtual necesita ejecutarse o apagarse para desmontar este "
+#~ "dispositivo"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "El directorio en el servidor se está exportando"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "The pool is empty"
+#~ msgstr "El grupo está vacío"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr ""
+#~ "El sistema operativo seleccionado no soporta una instalación desatendida"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr ""
+#~ "El sistema operativo seleccionado tiene una memoria mínima necesaria de "
+#~ "$0 $1"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "El sistema operativo tiene un almacenamiento mínimo que requerido de $0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "No se pudo eliminar el grupo de almacenamiento"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr "Esta MV es temporal. Apáguela si desea eliminarla."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Este volumen ya lo utiliza: "
+
+#~ msgid "Threads per core"
+#~ msgstr "Hilos por núcleo"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr ""
+#~ "Las MVs que son temporales no tienen soporte para editar la configuración "
+#~ "del firmware"
+
+#~ msgid "Type ID"
+#~ msgstr "Tipo de ID"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Unique name"
+#~ msgstr "Nombre único"
+
+#~ msgid "Unique network name"
+#~ msgstr "Nombre único de red"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Firmware desconocido"
+
+#~ msgid "Unplug"
+#~ msgstr "Desenchufar"
+
+#~ msgid "Url"
+#~ msgstr "URL"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "No se pudo guardar la configuración de la vCPU"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "La MV $0 ya existe"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "La MV $0 falló al forzar el reinicio"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "La MV $0 falló al forzar un apagado"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "La MV $0 falló al eliminarse"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "La MV $0 falló al instalarse"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "La MV $0 falló al reiniciarse"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "La MV $0 falló al despausarse"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "La MV $0 falló al enviar el NMI"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "La MV $0 falló al apagarse"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "La MV $0 falló al arrancarse"
+
+#, fuzzy
+#~ msgid "VM state"
+#~ msgstr "Estado"
+
+#, fuzzy
+#~| msgid "VNC TLS port:"
+#~ msgid "VNC TLS port"
+#~ msgstr "Puerto VNC TLS:"
+
+#, fuzzy
+#~| msgid "VNC address:"
+#~ msgid "VNC address"
+#~ msgstr "Dirección VNC:"
+
+#, fuzzy
+#~| msgid "console"
+#~ msgid "VNC console"
+#~ msgstr "consola"
+
+#, fuzzy
+#~| msgid "VNC port:"
+#~ msgid "VNC port"
+#~ msgstr "Puerto VNC:"
+
+#, fuzzy
+#~ msgid "Virtual Machines"
+#~ msgstr "Máquinas virtuales"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Máquinas virtuales"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Gestión de máquinas virtuales"
+
+#~ msgid "Virtual network"
+#~ msgstr "Red virtual"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "La red virtual falló al crearse"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Servicio de virtualización (libvirt) no está activo"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "El grupo de volumen no debe estar vacío"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Puede escribirse"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Puede escribirse y compartirse"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "Debe seleccionar el sistema operativo que mejor se ajuste"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "desactivado"
+
+#~ msgid "down"
+#~ msgstr "apagado"
+
+#~ msgid "enabled"
+#~ msgstr "activado"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "dispositivo anfitrión"
+
+#, fuzzy
+#~| msgid "to host path"
+#~ msgid "host passthrough"
+#~ msgstr "ruta del anfitrión"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "Objetivo directo iSCI"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "Iniciador de iSCSI IQN"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "Objetivo de iSCSI en IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "no"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "dispositivos redirigido"
+
+#~ msgid "server"
+#~ msgstr "servidor"
+
+#~ msgid "up"
+#~ msgstr "encendido"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "vCPU count"
+#~ msgstr "Número de vCPU"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU máximo"
+
+#~ msgid "vCPUs"
+#~ msgstr "vCPUs"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#, fuzzy
+#~| msgid "Read more..."
+#~ msgid "view more..."
+#~ msgstr "Leer más..."
+
+#, fuzzy
+#~| msgid ""
+#~| "virt-install package needs to be installed on the system in order to "
+#~| "create new VMs"
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Hay que instalar el paquete virt-install para poder crear nuevas máquinas "
+#~ "virtuales"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Hay que instalar el paquete virt-install para poder crear nuevas máquinas "
+#~ "virtuales"
+
+#, fuzzy
+#~| msgid ""
+#~| "virt-install package needs to be installed on the system in order to "
+#~| "create new VMs"
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Hay que instalar el paquete virt-install para poder crear nuevas máquinas "
+#~ "virtuales"
+
+#~ msgid "vm"
+#~ msgstr "vm"
+
+#~ msgid "Local install media"
+#~ msgstr "Instalar desde un medio local"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#, fuzzy
+#~ msgid "Please set a root password"
+#~ msgstr "Por favor, asigne una contraseña al usuario root o a otro usuario"
+
+#~ msgid "21st"
+#~ msgstr "21.º"
+
+#~ msgid "22nd"
+#~ msgstr "22.º"
+
+#~ msgid "23rd"
+#~ msgstr "23.º"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Después"
+
+#~ msgid "Friday"
+#~ msgstr "Viernes"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Hora : Minuto"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "La hora tiene que tener un número entre 0 y 23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "El formato de la fecha no es válido."
+
+#~ msgid "Invalid number."
+#~ msgstr "El número no es válido."
+
+#~ msgid "Monday"
+#~ msgstr "Lunes"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Repetir cada hora"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Repetir cada año"
+
+#~ msgid "Saturday"
+#~ msgstr "Sábado"
+
+#~ msgid "Sunday"
+#~ msgstr "Domingo"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Este día no se encuentra en todos los meses.<br> El temporizador solo se "
+#~ "ejecutará en los meses que tengan 31 días."
+
+#~ msgid "Thursday"
+#~ msgstr "Jueves"
+
+#~ msgid "Tuesday"
+#~ msgstr "Martes"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 actualización"
+#~ msgstr[1] "$0 actualizaciones"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 corrección de seguridad"
+#~ msgstr[1] "$1 correcciones de seguridad"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Gestionando los dispositivos de almacenamiento"
+
+#~ msgid "Restart now"
+#~ msgstr "Reiniciar ahora"
+
+#~ msgid "Configure"
+#~ msgstr "Configurar"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr ""
+#~ "El arranque por red solo está disponible cuando se usa una conexión de "
+#~ "sistema"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Redes"
+
+#, fuzzy
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "No existe el archivo o directorio"
+
+#~ msgid "No updates pending"
+#~ msgstr "No hay actualizaciones pendientes"
+
+#, fuzzy
+#~| msgid "ssh server is empty"
+#~ msgid "This directory is empty"
+#~ msgstr "el servidor ssh está vacío"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr ""
+#~ "Este tipo de grupo no soporta la creación de un volumen de almacenamiento"
+
+#~ msgid "undefined"
+#~ msgstr "sin definir"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "La llave del anfitrión es incorrecta"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "La autenticidad del anfitrión {{#strong}}{{host}}{{/strong}} no se ha "
+#~ "podido establecer.¿Está seguro que desea continuar con la conexión?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "La clave de {{#strong}}{{host}}{{/strong}} no coincide con la clave que "
+#~ "se ha utilizado previamente. A menos que esta máquina haya sido "
+#~ "reemplazada recientemente, es posible que alguien esté intentando acceder "
+#~ "de forma forzada a esta máquina."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Clave del anfitrión desconocida"
+
+#~ msgid "Address:"
+#~ msgstr "Dirección:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Se necesitan al menos $0 discos."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "Conectar con algún visor de aplicación SPICE o VNC."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Descargar el MSI desde $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Consola gráfica (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Consola gráfica en visualizador de escritorio"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "No hay una consola definida para esta máquina virtual."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid "Entry"
+#~ msgstr "Entrada"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Su navegador se desconectará pero esto no afectará al proceso de "
+#~ "actualización. Usted se puede volver a conectar en breves momentos para "
+#~ "continuar vigilando el progreso."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "y reinicia la máquina automáticamente."
+
+#~ msgid "Dashboard"
+#~ msgstr "Tablero"
+
+#, fuzzy
+#~ msgid "Description input text"
+#~ msgstr "Descripción"
+
+#~ msgid "FAILED"
+#~ msgstr "FALLÓ"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Conexión perdida. Intentando conectarse nuevamente"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Red"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Please enter new volume size"
+#~ msgstr "Introduzca el nuevo tamaño del volumen"
+
+#~ msgid "Servers"
+#~ msgstr "Servidores"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "Actualmente está conectado directamente a este servidor. No puede "
+#~ "eliminarlo."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bits"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 bytes"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 bytes"
+
+#~ msgid "Bond settings "
+#~ msgstr "Configuración de balanceo "
+
+#~ msgid "IP settings "
+#~ msgstr "Ajustes IP "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Cockpit ha experimentado un error interno inesperado. <br/><br/>Puede "
+#~ "intentar reiniciarlo si refresca la página. La consola de JavaScript "
+#~ "contiene detalles del error (<b>Ctrl + Mayús. + J</b> en la mayoría de "
+#~ "los navegadores)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "La VM se ha cerrado."
+
+#~ msgid "The VM is down."
+#~ msgstr "La MV está apagada."
+
+#~ msgid "The VM is going down."
+#~ msgstr "La MV se está apagando."
+
+#~ msgid "The VM is idle."
+#~ msgstr "La MV está en espera."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr "La MV está moribunda (el apagado o el cierre no se ha completado)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "La MV está pausada."
+
+#~ msgid "The VM is running."
+#~ msgstr "La MV se está ejecutando."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr ""
+#~ "La MV está suspendida por la administración de energía del invitado."
+
+#~ msgid "inactive"
+#~ msgstr "inactivo"
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Déjelo en blanco para conectarse a esta máquina como el usuario "
+#~ "actualmente conectado. Si ingresa un nombre de usuario diferente, ese "
+#~ "usuario siempre se usará cuando se conecte a esta máquina."
+
+#~ msgid "2 min"
+#~ msgstr "2 min"
+
+#~ msgid "3 min"
+#~ msgstr "3 min"
+
+#~ msgid "4 min"
+#~ msgstr "4 min"
+
+#~ msgid "CPU graph"
+#~ msgstr "Gráfico de CPU"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "Estado de la CPU"
+
+#~ msgid "Cached"
+#~ msgstr "Prealmacenado"
+
+#~ msgid "Graphs"
+#~ msgstr "Gráficas"
+
+#~ msgid "I/O wait"
+#~ msgstr "Espera de E/S"
+
+#~ msgid "Kernel"
+#~ msgstr "Kernel"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Memoria"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "Memoria y uso del área de intercambio"
+
+#~ msgid "Memory graph"
+#~ msgstr "Gráfico de memoria"
+
+#~ msgid "Network traffic"
+#~ msgstr "Tráfico de red"
+
+#~ msgid "Nice"
+#~ msgstr "Bien"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Sincronizar usuarios"
+
+#~ msgid "Usage graphs"
+#~ msgstr "Gráficos de uso"
+
+#~ msgid "View graphs"
+#~ msgstr "Ver gráficos"
+
+#~ msgid "idle"
+#~ msgstr "en espera"
+
+#~ msgid "paused"
+#~ msgstr "en pausa"
+
+#~ msgid "running"
+#~ msgstr "en ejecución"
+
+#~ msgid "shut off"
+#~ msgstr "apagar"
+
+#~ msgid "shutdown"
+#~ msgstr "apagar"
+
+#~ msgid "Add a new host"
+#~ msgstr "Añadir un Nuevo Host"
+
+#~ msgid "Available"
+#~ msgstr "Disponible"
+
+#, fuzzy
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Ingrese la dirección IP o el nombre del anfitrión"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Interfaces de red"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Esta versión de cockpit-ws no admite conexiones con un servidor mediante "
+#~ "un usuario o puerto alterno"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Para tratar un puerto diferente va a necesitar actualizar cockpit-ws a "
+#~ "una nueva versión."
+
+#~ msgid "$0 template"
+#~ msgstr "$0 Plantilla"
+
+#~ msgid "Instance of template: "
+#~ msgstr "Instancia de plantilla: "
+
+#~ msgid "Instantiate"
+#~ msgstr "Instanciar"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 intercambios"
+
+#~ msgid "All In One"
+#~ msgstr "Todo en uno"
+
+#~ msgid "Always"
+#~ msgstr "Siempre"
+
+#~ msgid "Author"
+#~ msgstr "Autor"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Datos inválidos pasados por el mecanismo passwd1"
+
+#~ msgid "CPU priority"
+#~ msgstr "Prioridad del CPU"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Puede&rsquo;t conectar a Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Cambiar los límites del recurso"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Cambiar los límites de los recursos"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit no pudo acceder a {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit no pudo acceder a {{#strong}}{{host}}{{/strong}}. Puede cambiar "
+#~ "sus credenciales de autenticación más abajo. {{#can_sync}}Quizás prefiera "
+#~ "{{#sync_link}}sincronizar las cuentas y las contraseñas{{/sync_link}}.{{/"
+#~ "can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Uso de memoria combinado"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Uso combinado de núcleo de CPU $0"
+#~ msgstr[1] "Uo combinado de núcleos de CPU $0"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "El comando no puede estar vacío"
+
+#~ msgid "Command:"
+#~ msgstr "Comando:"
+
+#~ msgid "Commit"
+#~ msgstr "Confirmar"
+
+#~ msgid "Commit image"
+#~ msgstr "Confirmar una imagen"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Conectándose a Docker"
+
+#~ msgid "Container name"
+#~ msgstr "Nombre del contenedor"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr "El contenedor marcado no se está ejecutando, pero falló al pararlo."
+
+#~ msgid "Container is currently running."
+#~ msgstr "El contenedor se está ejecutando."
+
+#~ msgid "Container:"
+#~ msgstr "Contenedor:"
+
+#~ msgid "Containers"
+#~ msgstr "Contenedores"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Contenedores"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "No se pudieron cambiar los grupos del usuario"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "No se pudo cambiar la contraseña del usuario"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "No se pudo conectar a la máquina"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "No se pueden crear nuevos usuarios"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "No se pueden listar los usuarios locales"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "No se puede listar los usuarios"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "No se pudo cargar los datos del usuario"
+
+#~ msgid "Created:"
+#~ msgstr "Creado:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Contenedores de Dockers"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker no está instalado o no está activo en el sistema"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Alias duplicado"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Puerto duplicado"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "Introduzca la contraseña para añadir una identidad al ssh-agent"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Introduciendo una contraseña diferente significa que necesitará volver a "
+#~ "introducir la contraseña cada vez que quiera conectarse a esta máquina"
+
+#~ msgid "Environment"
+#~ msgstr "Entorno"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Error cargando a los usuarios: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Mensaje de error de Docker:"
+
+#~ msgid "Everything"
+#~ msgstr "Todo"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Código de salida $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Exponer puertos de contenedores"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Falló al iniciar Docker: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Falló al detener Docker scope: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Obtener una nueva imagen"
+
+#~ msgid "IP address:"
+#~ msgstr "Dirección IP:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "Longitud de Prefijo de la IP:"
+
+#~ msgid "Id"
+#~ msgstr "Id"
+
+#~ msgid "Id:"
+#~ msgstr "Id:"
+
+#~ msgid "Image"
+#~ msgstr "Imagen"
+
+#~ msgid "Image $0"
+#~ msgstr "Imagen $0"
+
+#~ msgid "Image search"
+#~ msgstr "Buscar imagen"
+
+#~ msgid "Image:"
+#~ msgstr "Imagen:"
+
+#~ msgid "Images"
+#~ msgstr "Imágenes"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Imágenes"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Imágenes y contenedores en ejecución"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Con el objetivo de sincronizar usuarios, necesita acceder a {{#strong}}"
+#~ "{{host}}{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Puerto inválido"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Comprobante de Kerberos"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Déjelo en blanco para conectarse a esta máquina como el usuario "
+#~ "actualmente conectado{{#default_user}}{{/default_user}} . Si ingresa un "
+#~ "nombre de usuario diferente, ese usuario siempre se usará cuando se "
+#~ "conecte a esta máquina."
+
+#~ msgid "Link to another container"
+#~ msgstr "Enlazar con otro contenedor"
+
+#~ msgid "Links:"
+#~ msgstr "Enlaces:"
+
+#~ msgid "Login Password"
+#~ msgstr "Contraseña de inicio de sesión"
+
+#~ msgid "MAC address:"
+#~ msgstr "Dirección MAC:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Límite de memoria"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Montar volúmenes de contenedores"
+
+#~ msgid "No container specified"
+#~ msgstr "No se ha especificado un contenedor"
+
+#~ msgid "No containers"
+#~ msgstr "No hay contenedores"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "No hay contenedores que correspondan con el filtro actual"
+
+#~ msgid "No images"
+#~ msgstr "No hay imágenes"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "No hay imágenes que correspondan con el filtro actual"
+
+#~ msgid "No results for $0"
+#~ msgstr "No hay resultados para $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "No hay imágenes que correspondan con el filtro actual"
+
+#~ msgid "No running containers"
+#~ msgstr "No hay contenedores ejecutándose"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "No hay contenedores ejecutándose que coincidan con el filtro actual"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "No está autorizado para acceder a Docker en este sistema"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "En caso de fallo, reintentar $0 vez"
+#~ msgstr[1] "En caso de fallo, reintentar $0 veces"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Por favor confirme, forzar el borrado de $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Pruebe con otro término"
+
+#~ msgid "Ports:"
+#~ msgstr "Puertos:"
+
+#~ msgid "Problems"
+#~ msgstr "Problemas"
+
+#~ msgid "ReadOnly"
+#~ msgstr "SoloLectura"
+
+#~ msgid "ReadWrite"
+#~ msgstr "LecturaEscritura"
+
+#~ msgid "Repository"
+#~ msgstr "Repositorio"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Normativa de reinicio:"
+
+#~ msgid "Retries:"
+#~ msgstr "Reintentos:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Reutilizar mi contraseña para conexiones remotas"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Seleccione los usuarios que le gustaría sincronizarse con {{#strong}}"
+#~ "{{host}}{{/strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Ajustar variables de entorno del contenedor"
+
+#~ msgid "Severity:"
+#~ msgstr "Severidad:"
+
+#~ msgid "Show all containers"
+#~ msgstr "Mostrar todos los contenedores"
+
+#~ msgid "Start Docker"
+#~ msgstr "Iniciar Docker"
+
+#~ msgid "State:"
+#~ msgstr "Estado:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Sincronizar"
+
+#~ msgid "Tag"
+#~ msgstr "Etiqueta"
+
+#~ msgid "Tags"
+#~ msgstr "Etiquetas"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "Los siguientes contenedores dependen de esta imagen y no podrán usarse."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Esta imagen no existe."
+
+#~ msgid "Type a password"
+#~ msgstr "Introduzca una contraseña"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Escribe para filtrar…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "A menos que se detenga"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Mecanismo de ajuste no soportado"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Up since $0"
+#~ msgstr "En marcha desde $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Utilizado por contenedores"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Usando credenciales disponibles"
+
+#~ msgid "Volumes"
+#~ msgstr "Volúmenes"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Volumes:"
+#~ msgstr "Volúmenes:"
+
+#~ msgid "With terminal"
+#~ msgstr "Con terminal"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Usted está conectado a {{#strong}}{{host}}{{/strong}}, sin embargo para "
+#~ "sincronizar usuarios, se necesita un usuario con privilegios de súper "
+#~ "usuario."
+
+#~ msgid "default"
+#~ msgstr "por defecto"
+
+#~ msgid "key"
+#~ msgstr "clave"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "buscar por nombre, espacio de nombres o descripción"
+
+#~ msgid "shares"
+#~ msgstr "comparticiones"
+
+#~ msgid "to host port"
+#~ msgstr "puerto del anfitrión"
+
+#~ msgid "value"
+#~ msgstr "valor"
+
+#~ msgid "Master"
+#~ msgstr "Maestro"
+
+#~ msgid "Password for $0"
+#~ msgstr "Contraseña para $0"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "El usuario <b>$0</b> no está autorizado para administrar servidores"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Cuentas"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "El usuario <b>$0</b> no está autorizado para modificar cuentas"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "No es posible eliminar la cuenta root"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "No es posible renombrar la cuenta del usuario root"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "No está autorizado para añadir una zona nueva"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "No está autorizado para añadir servicios a la zona $0"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "No está autorizado para eliminar el servicio $0"
+
+#~ msgid "Account Settings"
+#~ msgstr "Configuración de la cuenta"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Añadir máquina al tablero"
+
+#~ msgid "Machines"
+#~ msgstr "Máquinas"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "El inicio de sesión ha aumentado los privilegios de administrador"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Contraseña no utilizable para tareas privilegiadas o para conectar a "
+#~ "otras máquinas"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Privileged"
+#~ msgstr "Privilegiado"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Reutilizar mi contraseña para tareas con privilegios y para conectar con "
+#~ "otras máquinas"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr ""
+#~ "El usuario $0 no tiene permisos para modificar el nombre de los "
+#~ "anfitriones"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr ""
+#~ "El usuario $0 no tiene permisos para apagar o reiniciar este servidor"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "El usuario <b>$0</b> no está autorizado para cambiar perfiles"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr ""
+#~ "El usuario <b>$0</b> no está autorizado para gestionar el almacenamiento"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr ""
+#~ "El usuario <b>$0</b> no está autorizado para modificar la configuración "
+#~ "de la red"
+
+# auto translated by TM merge from project: Cockpit, version: rhel-8.0, DocId:
+# cockpit
+#~ msgid "Required by $0"
+#~ msgstr "Requerido por $0"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Última ejecución"
+
+#~ msgid "Next Run"
+#~ msgstr "En la próxima ejecución"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "El usuario <b>$0</b> no tiene permisos para crear temporizadores"
+
+#~ msgid "Connecting"
+#~ msgstr "Conectando"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Acerca de Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (compartido con el SO)"
+
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 vulnerabilidad"
+#~ msgstr[1] "$0 vulnerabilidades"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Añadir almacenamiento adicional"
+
+#~ msgid "Add Storage"
+#~ msgstr "Añadir almacenamiento"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Almacenamiento Adicional"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Todos los datos en los discos seleccionados serán borrados y los discos "
+#~ "serán añadidos al pool de almacenamiento."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Configurar almacenamiento..."
+
+#~ msgid "Could not add all disks"
+#~ msgstr "No podría añadir todos los discos"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Borrar contenedores y reajustar el pool de almacenamiento"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr ""
+#~ "No hay información disponible sobre el grupo de almacenamiento de Docker."
+
+#~ msgid "Local Disks"
+#~ msgstr "Discos locales"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "No se encontró ningún almacenamiento local adicional."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Reformatear y añadir discos"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "Si restablece el grupo de almacenamiento se borrarán todos los "
+#~ "contenedores y los discos de liberación del grupo."
+
+#~ msgid "Security"
+#~ msgstr "Seguridad"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr ""
+#~ "El grupo del almacenamiento Docker no se puede administrar en este "
+#~ "sistema."
+
+#, fuzzy
+#~| msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "El escaneo desde $time ($type) no encontró vulnerabilidades."
+#~ msgstr[1] "El escaneo desde $time ($type) no encontró vulnerabilidades."
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "El escaneo desde $time ($type) no encontró vulnerabilidades."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "El escaneo desde $time ($type) no tuvo éxito."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "El Servicio de Virtualización Está Disponible"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr ""
+#~ "Usted no tiene permisos para gestionar el grupo de almacenamiento Docker."
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} → $cport"
diff --git a/po/fi.po b/po/fi.po
new file mode 100644
index 0000000..d56f449
--- /dev/null
+++ b/po/fi.po
@@ -0,0 +1,12939 @@
+# Helinä Turumore <helina.turunen@gmail.com>, 2017. #zanata
+# Jiri Grönroos <jiri.gronroos@iki.fi>, 2017. #zanata
+# Toni Rantala <trantalafilo@gmail.com>, 2017. #zanata
+# Helinä Turumore <helina.turunen@gmail.com>, 2018. #zanata
+# Jiri Grönroos <jiri.gronroos@iki.fi>, 2018. #zanata
+# Jiri Grönroos <jiri.gronroos@iki.fi>, 2019. #zanata
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2024-01-28 21:36+0000\n"
+"Last-Translator: Ville-Pekka Vainio <vpvainio@iki.fi>\n"
+"Language-Team: Finnish <https://translate.fedoraproject.org/projects/cockpit/"
+"main/fi/>\n"
+"Language: fi\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.3.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "# käyttäjää"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+msgstr[1] "$0 CPU:ta"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 Git"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 aktiivinen vyöhyke"
+msgstr[1] "$0 aktiivista vyöhykettä"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 käytettävissä"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "tiedostojärjestelmiä $0 ei voida suurentaa."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "tiedostojärjestelmiä $0 ei voida pienentää."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "tiedostojärjestelmien $0 kokoa ei voida muuttaa tässä."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "tiedostojärjestelmien $0 kokoa ei voida muuttaa tässä."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 palan koko"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 kriittinen osuma"
+msgstr[1] "$0 osumaa, sisältäen kriittisiä"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 data + $1 ylikäyttö $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 päivä"
+msgstr[1] "$0 päivää"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 levyä ei löydy"
+msgstr[1] "$0 levyjä ei löydy"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 levyä ei löydy"
+msgstr[1] "$0 levyjä ei löydy"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 levyt"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 -dokumentaatio"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 virhe"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 poistui koodilla $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 epäonnistui"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 epäonnistunut sisäänkirjautumisyritys"
+msgstr[1] "$0 epäonnistutta sisäänkirjautumisyritystä"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "$0 tiedostojärjestelmä"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 vapaa"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 tunti"
+msgstr[1] "$0 tuntia"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 tärkeä osuma"
+msgstr[1] "$0 osumaa, mukaan lukien tärkeät"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 on olemassa oleva tiedosto"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 on käytössä"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 ei ole saatavilla mistään ohjelmistovarastosta."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 avain muuttunut"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 tapettu signaalilla $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 matala vakavuus -osuma"
+msgstr[1] "$0 matala vakavuus -osumaa"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minuutti"
+msgstr[1] "$0 minuuttia"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 kohtalainen osuma"
+msgstr[1] "$0 osumia, mukaan lukien kohtalaiset"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 kuukausi"
+msgstr[1] "$0 kuukautta"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 lisää..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 paketti"
+msgstr[1] "$0 pakettia"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 paketti vaatii järjestelmän uudelleenkäynnistyksen"
+msgstr[1] "$0 pakettia vaatii järjestelmän uudelleenkäynnistyksen"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 sivu"
+msgstr[1] "$0 sivua"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "osio"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 turvallisuuspäivitys saatavilla"
+msgstr[1] "$0 turvallisuuspäivitystä saatavilla"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 palvelu on epäonnistunut"
+msgstr[1] "$0 palvelua on epäonnistunut"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 palvelu on käynnistettävä uudelleen"
+msgstr[1] "$0 palvelua on käynnistettävä uudelleen"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 paikka on jäljellä"
+msgstr[1] "$0 paikkaa on jäljellä"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 piikki"
+msgstr[1] "$0 piikkiä"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+#, fuzzy
+#| msgid "Not synchronized"
+msgid "$0 synchronized"
+msgstr "Ei synkronisoitu"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 yhteensä"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 päivitys saatavilla"
+msgstr[1] "$0 päivitystä saatavilla"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 käytetty kohteesta $1 ($2 tallennettu)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 viikko"
+msgstr[1] "$0 viikkoa"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 asennetaan."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 vuosi"
+msgstr[1] "$0 vuotta"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 -vyöhyke"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: kaatui hetkellä $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (kohteesta $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Luodaan osiota $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Paikallinen liitoskohta"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Ei liitetty"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(suositeltu)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", mukaan lukien $1 turvallisuuspäivitys"
+msgstr[1] ", mukaan lukien $1 turvallisuuspäivitystä"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 Mit"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 päivä"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 tunti"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 min"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 minuutti"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 viikko"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 Kit"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14."
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 min"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 Kit"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 Mit"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 minuuttia"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 Kit"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 Kit"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 minuuttia"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4."
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 min"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minuuttia"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 Kit"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5."
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 tuntia"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 minuuttia"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 Kit"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 Kit"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9."
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Yhteensopivaa versiota Cockpitistä ei ole asennettu kohteessa $0."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "Tämänniminen tiedostojärjestelmä on jo tässä varannossa."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "Tämän niminen ryhmä on jo olemassa"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Nykyaikainen selain vaaditaan turvallisuuden, luotettavuuden ja "
+"suorituskyvyn takaamiseksi."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Verkkosidos yhdistää useita verkkoliitäntöjä yhdeksi loogiseksi liitännäksi, "
+"jolla on korkeampi suorituskyky tai redundanssi."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Uusi SSH-avain nimellä $0 luodaan käyttäjälle $1 koneella $2 ja se lisätään "
+"käyttäjän $3 tiedostoon $4 koneella $5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Paketti vaatii uudelleenkäynnistyksen, jotta päivitykset tulevat voimaan:"
+msgstr[1] ""
+"Jotkut paketit vaativat uudelleenkäynnistyksen, jotta päivitykset tulevat "
+"voimaan:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Tämän niminen varanto on jo olemassa."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"Palvelu on käynnistettävä uudelleen, jotta päivitykset tulevat voimaan:"
+msgstr[1] ""
+"Joitakin palveluja on käynnistettävä uudelleen, jotta päivitykset tulevat "
+"voimaan:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "Fyysisten taltioiden kokoja ei voida muuttaa tässä."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP-monitorointi"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP:n ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Tietoja Verkkokonsolista"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Poissa"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Hyväksy avain ja kirjaudu sisään"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+#, fuzzy
+#| msgid "Set password"
+msgid "Acceptable password"
+msgstr "Aseta salasana"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Tilin vanheneminen"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Tili ei ole käytettävissä tai sitä ei voi muokata."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Käyttäjätilit"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Toiminto"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Toimet"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Aktivoi"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Aktivoidaan $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Aktiivinen"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Aktiivinen varmennus"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Aktiiviset sivut"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Aktiivinen lähtien "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Aktiivinen tila"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Mukautuva kuormantasaus"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Mukautuva lähtevän kuorman tasaus"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Lisää"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Lisää $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add DNS server"
+msgstr "Tang-avainpalvelin"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Lisää verkkoon sidottu levyn salaus"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Lisää Tang-avainpalvelin"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Lisää VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+#, fuzzy
+#| msgid "Add VLAN"
+msgid "Add VPN"
+msgstr "Lisää VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+#, fuzzy
+#| msgid "Add item"
+msgid "Add a bitmap"
+msgstr "Lisää kohta"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Lisää uusi vyöhyke"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+#, fuzzy
+#| msgid "MAC address"
+msgid "Add address"
+msgstr "MAC-osoite"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Lisää lohkolaitteita"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Lisää sidos"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Lisää silta"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Lisää levy"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Lisää levyjä"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Lisää iSCSI-portaali"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Lisää avain"
+
+#: pkg/storaged/stratis/pool.jsx:551
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add keyserver"
+msgstr "Tang-avainpalvelin"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Lisää jäsen"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Lisää uusi kone"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Lisää uusi vyöhyke"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "Lisää tunnuslause"
+
+#: pkg/networkmanager/wireguard.jsx:290
+#, fuzzy
+#| msgid "Add member"
+msgid "Add peer"
+msgstr "Lisää jäsen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "LVM2 physical volume"
+msgid "Add physical volume"
+msgstr "LVM2 fyysinen taltio"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Lisää portit"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Lisää portteja vyöhykkeeseen $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Lisää julkinen avain"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+#, fuzzy
+#| msgid "Add item"
+msgid "Add route"
+msgstr "Lisää kohta"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+#, fuzzy
+#| msgid "DNS search domains"
+msgid "Add search domain"
+msgstr "DNS-hakutoimialueet"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Lisää palveluja"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Lisää palveluja vyöhykkeeseen $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Lisää palveluja vyöhykkeeseen $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Lisää joukkue"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Lisää vyöhyke"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "Lisätään \"$0\" salauksen valintoihin"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Lisätään \"$0\" tiedostojärjestelmän asetuksiin"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0:n lisääminen katkaisee yhteyden palvelimeen, jolloin "
+"hallintakäyttöliittymä ei ole saatavilla."
+
+#: pkg/storaged/stratis/pool.jsx:468
+#, fuzzy
+#| msgid ""
+#| "Saving a new passphrase requires unlocking the disk. Please provide a "
+#| "current disk passphrase."
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Uuden salasanan tallentaminen edellyttää levyn lukituksen avaamista. Anna "
+"nykyinen levyn tunnuslause."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Mukautettujen porttien lisääminen lataa firewalld:n uudelleen. "
+"Uudelleenlataus johtaa kaikkien vain-ajonaikaisen kokoonpanon menetykseen!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Avainta lisätään"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Lisätään fyysinen taltio $target:een"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Lisätään rd.neednet=1 kernelin komentoriville"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Ylimääräinen DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Lisä DNS-hakutoimialueita $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Ylimääräiset toimet"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Ylimääräinen osoite $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Ylimääräiset paketit:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Ylimääräiset portit"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Osoite"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Osoite $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Osoite ei voi olla tyhjä"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Osoite ei ole kelvollinen URL"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Osoitteet"
+
+#: pkg/networkmanager/wireguard.jsx:52
+#, fuzzy
+#| msgid "Address cannot be empty"
+msgid "Addresses are not formatted correctly"
+msgstr "Osoite ei voi olla tyhjä"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Hallinta Cockpit-verkkokonsolilla"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Ylläpitäjän käyttöoikeudet"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr ""
+"Raporttien luomiseen ja käyttämiseen tarvitaan ylläpitäjän käyttöoikeudet."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Tarvitaan ylläpitäjän käyttöoikeudet"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Edistynyt TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Jälkeen"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Kun olet poistunut toimialueesta, vain käyttäjät, joilla on paikalliset "
+"kirjautumistiedot, voivat kirjautua tähän koneeseen. Tämä voi vaikuttaa myös "
+"muihin palveluihin, kuten DNS-ratkaisimen asetukset, ja luotettujen "
+"varmentajien CA-luettelo voi muuttua."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Järjestelmän käynnistyksen jälkeen"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Hälytys"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Varoitus ja yli"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Alias"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Kaikki"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Kaikki päivitykset"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Kaikki yhdessä"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Salli, että suoritetaan (paljastaa)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+#, fuzzy
+#| msgid "Allowed addresses"
+msgid "Allowed IPs"
+msgstr "Sallitut osoitteet"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Sallitut osoitteet"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Ansible-roolien dokumentaatio"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Mikä tahansa tekstimerkkijono lokiviesteissä voidaan suodattaa. Merkkijono "
+"voi olla myös säännöllisen lausekkeen muodossa. Tukee myös suodatusta "
+"viestilokikenttien mukaan. Nämä ovat välilyönnillä erotettuja arvoja "
+"muodossa KENTTÄ=ARVO, jossa arvo voi olla pilkuilla erotettu luettelo "
+"mahdollisista arvoista."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Ulkomuoto"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr ""
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Sovellukset"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Sovellusten luettelo"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Käytä ja käynnistä uudelleen"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Asenna ytimen live-paikkaukset"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Toteuta tämä ratkaisu"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Uuden käytännön soveltaminen; tämä saattaa kestää muutaman minuutin."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Toteutetaan ratkaisu..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Asennetaan päivityksiä"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Päivitysten asentaminen epäonnistui"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "Soveltuu kriittisiin liitoksiin, kuten /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Sovellukset"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Arviointi"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Sisältötunniste"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "Käynnistyksen yhteydessä"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Vähintään $0 levy tarvitaan."
+msgstr[1] "Vähintään $0 levyä tarvitaan."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "Vähintään yksi lohkolaite tarvitaan."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Vähintään yksi levy tarvitaan."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "Hetkessä"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "Toiseksi"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "Tiettynä aikana"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Attribuutit"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Audit-loki"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Tunnistaudu"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Tunnistaudutaan"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Tunnistautuminen"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Tunnistautuminen epäonnistui"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Tunnistautuminen epäonnistui: Palvelin sulki yhteyden"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Todennus vaaditaan etuoikeutettujen tehtävien suorittamiseen Cockpit Web "
+"Console:ssa"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Tunnistautuminen vaaditaan"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Valtuuta SSH-avain"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Valtuutetut julkiset SSH-avaimet"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automaattinen"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automaattinen (vain DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Automaattinen sisäänkirjautuminen"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Automaattiset päivitykset"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Käynnistyy automaattisesti"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Käytetään automaattisesti NTP:tä"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Käytetään automaattisesti lisättyjä NTP-palvelimia"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Käytetään automaattisesti tiettyjä NTP-palvelimia"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Automaatio-komentosarja"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Käytettävät kohteet osoitteessa $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Saatavilla olevat päivitykset"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "partition"
+msgid "BIOS boot partition"
+msgstr "osio"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "BIOS-päiväys"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "BIOS-versio"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Takaisin käyttäjätileihin"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Huono"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Huono asetus"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Tasapainottaja"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Ennen"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Sitoo tähän"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Musta"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Terä"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Teräkotelo"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "lohkolaite"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Lohkolaite tiedostojärjestelmille"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Lohkolaitteet"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Estetty"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Side"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Käynnistys"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"Käynnistys epäonnistuu, jos tiedostojärjestelmä ei liity, mikä estää "
+"etäkäytön"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "Käynnistys onnistuu vaikka tiedostojärjestelmä ei liity"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Sillä sidottu"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Silta"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Sillan portti"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Sillan porttiasetukset"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Yleislähetys"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Rikkinäinen kokoonpano"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Virheenkorjauspäivityksiä saatavilla"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Viat"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Väylän laajennusalusta"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Vaihtamalla SSH-avaimen $0 salasanan käyttäjän $1 kirjautumissalasanaksi "
+"koneella $2, avain tulee automaattisesti saataville ja voit kirjautua sisään "
+"koneelle $3 jatkossa ilman salasanaa."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " Ohita selaimen tarkistus "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "Suoritin"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Suorittimen turvallisuus"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "Suorittimen turvallisuusvalinnat"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Suorittimen käyttö"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "Suorittimen käyttö/kuorma"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Välimuisti"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "Voi olla konenimi, IP-osoite, aliaksen nimi tai ssh:// URI"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr "Lokeja ei löydy käyttämällä nykyistä suodatinten yhdistelmää."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Peru"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Peru sammutus"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Peru uudelleenkäynnistys"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Ei voida ottaa käyttöön"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Ei voida yhdistää tuntemattomaan koneeseen"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Kirjautumistietoja ei voi välittää eteenpäin"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+"Toimialueeseen ei voi liittyä, koska realmd ei ole käytettävissä tässä "
+"järjestelmässä"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Tapahtumaa ei voi aikatauluttaa menneisyyteen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Koko"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Kuljetin"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Vaihda"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "Muuta kryptografista käytäntöä"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Vaihda konenimi"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Vaihda iSCSI-aloittajan nimi"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Vaihda iSCSI-aloittajan nimi"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change shell"
+msgid "Change label"
+msgstr "Vaihda komentotulkkia"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Vaihda tunnuslause"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Vaihda salasana"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Vaihda suorituskyvyn profiili"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Vaihda profiili"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "Vaihda komentotulkkia"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Vaihda järjestelmän aika"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Vaihda avaimen $0 salasana"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Vaihda asetuksia"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Muutetut avaimet ovat usein seurausta käyttöjärjestelmän "
+"uudelleenasennuksesta. Odottamaton muutos voi kuitenkin tarkoittaa kolmannen "
+"osapuolen yritystä siepata yhteys."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Asetuksien vaihtaminen katkaisee yhteyden palvelimeen, jolloin "
+"hallintakäyttöliittymä ei ole saatavilla."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Etsi päivityksiä"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"Tarkasta että komennon tuottama SHA-256- tai SHA-1-tarkistesumma vastaa tätä "
+"ikkunaa."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "Tarkasta avaimen tarkistussumma käyttäen Tang-palvelinta."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Tarkistetaan $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Tarkistetaan IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "Tarkistetaan RAID-laite $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Tarkistetaan ja korjataan RAID-laite $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Etsitään $0 pakettia"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Tarkistetaan NBDE-tukea initrd:ssä"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Etsitään uusia sovelluksia"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Tarkistetaan pakettipäivityksiä ..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Tarkistetaan asennettu ohjelmisto"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Tarkastetaan liittyviä prosesseja"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Tarkistetaan ohjelmiston tila"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Valitse sovelluksessa käytettävä kieli"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Palan koko"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Luokka"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Siivotaan kohteelle $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Tyhjennä \"Aloitus epäonnistui\""
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Tyhjennä kaikki suodattimet"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Tyhjennä haku"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Selkotekstilaite"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Asiakasohjelmisto"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Sulje"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Sulje valitut sivut"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Cockpitin tunnistautuminen on konfiguroitu virheellisesti."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "NetworkManagerin ja Firewalld:n Cockit-asetukset"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit ei saanut yhteyttä koneeseen."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpitissa oli odottamaton sisäinen virhe."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit on palvelinhallintatyökalu, joka tekee ylläpidon helpoksi selaimen "
+"kautta. Liikkuminen päätteen ja verkkokäyttöliittymän välillä ei ole "
+"ongelma. Cockpitissä aloitettu palvelu voidaan lopettaa päätteessä. Samaten "
+"päätteessä näkyvä virheilmoitus voidaan nähdä myös Cockpitin journal-"
+"näkymässä."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr ""
+"Cockpit on vuorovaikutteinen Linux-palvelimen ylläpitäjän käyttöliittymä."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit ei ole yhteensopiva järjestelmän ohjelmiston kanssa."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit ei ole asennettu"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit ei ole asennettu järjestelmässä."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit on täydellinen uusille ylläpitäjille. Sen avulla voi tehdä helposti "
+"toimenpiteitä kuten tallennustilan hallintaa, lokien tarkistamista sekä "
+"palveluiden käynnistämistä ja lopettamista. Voit monitoroida ja hallita "
+"useita palvelimia samanaikaisesti. Lisää ne yhdellä napsautuksella ja "
+"koneesi katsovat kavereidensa perään."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit ei ehkä näy oikein selaimessasi"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Kerää ja paketoi diagnostiikkaa ja tukitietoja"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Kerää ytimen kaatumisvedoksia"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Kerää mittarit"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Väri"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "Pilkuilla erotetut portit, alueet ja palvelut hyväksytään"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Komento"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Komentoa ei löytynyt"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Kommentti"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Yhteydenpito tuned:n kanssa epäonnistui"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Kompakti PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Yhteensopiva kaikkien järjestelmien ja laitteiden kanssa (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr ""
+"Yhteensopiva modernien järjestelmien ja kovalevyjen kanssa > 2 Tt (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Tiivistä kaatumisen tyhjennys säästääksesi tilaa"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Pakkaus"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Ehto $0=$1 ei toteutunut"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Ehto epäonnistui"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Kokoonpano"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Asetetaan"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Asetetaan IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "kdump:n määrittäminen"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Järjestelmäasetusten määrittäminen"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Vahvista"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Vahvista kohteen $0 poistaminen"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Vahvista avaimen salasana"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Vahvista uusi avaimen salasana"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Vahvista uusi salasana"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Vahvista salasana"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Vahvista $0:n poisto"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Vahvista poisto vaihtoehtoisella tunnuslauseella"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Vahvista kohteen $0 pysäyttäminen"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "Näiden kanssa ristiriitainen"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Ristiriidat"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Yhdistä automaattisesti"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Yhdistä"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Yhdistä kohteeseen:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Yhdistetään SETroubleshoot daemoniin..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Yhteyden muodostaminen dbus-verkkoon epäonnistui: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Yhdistetään koneeseen"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Yhteysvirhe"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Yhteys epäonnistui"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Yhteys aikakatkaistiin."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "Yhteys tulee katoamaan"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Näistä koostuu"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Toimialue, johon otettiin yhteyttä"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Sisältää:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Jatka"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Jatka istuntoa"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Hallinta"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Hallinta"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Ohjain"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Muunnettavissa"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Kopioitu"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Kopio"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Kopioi leikepöydälle"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Ydin $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "Ei saatu yhteyttä kohteeseen $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Kaatumisen tyhjennyksen sijainti"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Kaatumisraportointi"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Kaatumisjärjestelmä"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Luo"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "Luo LVM2-taltioryhmä"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "Luo RAID-laite"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Luo RAID-laite"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Luo stratisvaranto"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Luo uusi SSH-avain ja valtuuta se"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Luodaan tilannevedos tiedostojärjestelmästä $0"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Luo tili käyttäjänumerolla (UID), joka ei ole ainutkertainen"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Luo tili heikolla salasanalla"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Luo kotihakemisto ja muuta sen omistajuus"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Luo ja liitä"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create and mount"
+msgid "Create and start"
+msgstr "Luo ja liitä"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Luo tiedostojärjestelmä"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Luo se"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Luo looginen taltio"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Luo uusi tili"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Luo uusi tiedostojärjestelmä"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Luo uusi ryhmä"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Luo uusi looginen taltio"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Luo uusi tehtävätiedosto tällä sisällöllä."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "Luo uusi looginen taltio"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "Vain luominen"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Luo osio"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Luo osio $0:een"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Luo osiotaulukko"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Luo tilannevedos"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Luo tilannevedos ja liitä"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Luo vain tilannevedos"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create storage volume"
+msgid "Create storage device"
+msgstr "Luo varastointitaltio"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Luo taltio"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Luo ohut taltio"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Luo ajastin"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Luo taltioryhmä"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Luotu"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Luodaan LVM2 taltioryhmä $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "Luodaan RAID-laite $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "Luodaan VDO-laitetta"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Luodaan tiedostojärjestelmää $target:een"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Luodan loogista taltiota $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Luodaan osiota $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Luodaan tilannevedos $target:sta"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Kohteen $0 luominen katkaisee yhteyden palvelimeen, jolloin "
+"hallintakäyttöliittymä ei ole saatavilla."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Kriittinen ja yli"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Kryptografiset käytännöt on järjestelmäkomponentti, joka määrittää keskeiset "
+"salausalijärjestelmät, jotka kattavat TLS-, IPSec-, SSH-, DNSSec- ja "
+"Kerberos-protokollat."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "Kryptografinen käytäntö"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "Kryptografinen käytäntö on epäjohdonmukainen"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Vaihto-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Tämänhetkinen käynnistys"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Nykyinen korkein suoritinten käyttö"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Tällä hetkellä käytössä"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Currently in use"
+msgid "Currently not supported"
+msgstr "Tällä hetkellä käytössä"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "mukautettu"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "Mukautettu kryptografinen käytäntö"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Mukautetut liitosvalinnat"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Mukautetut portit"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom path"
+msgid "Custom type"
+msgstr "Mukautettu polku"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Mukautetut vyöhykkeet"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "OLETUS, SHA-1-allekirjoituksen vahvistus sallittu."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS-hakutoimialueet"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "DNS-hakutoimialueet $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Joka päivä"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Vaaran hälytys:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Tumma"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Tiedot"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Dataa käytetty"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Päivämäärätietojen on oltava muodossa VVVV-KK-PP hh:mm:ss. Vaihtoehtoisesti "
+"merkkijonot \"eilen\", \"tänään\", \"huomenna\" ymmärretään. \"nyt\" viittaa "
+"nykyiseen aikaan. Lopuksi suhteelliset ajat voidaan määrittää etuliitteellä "
+"'-' tai '+'"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Deaktivoi"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Nimeä uudelleen looginen taltio"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Deaktivoidaan"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Deaktivoidaan $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Virheenkorjaus ja yli"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Vähennä yhdellä"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+#, fuzzy
+#| msgid "RAID 4 (dedicated parity)"
+msgid "Dedicated parity (RAID 4)"
+msgstr "RAID 4 (omistettu pariteetti)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Päällekkäisyyden poisto"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Oletus"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Viive"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Viiveen tulee olla numero"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Poista"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Poista $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Poista tili"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Poista tiedostot"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Poista ryhmä"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Poista"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Poista raportti pysyvästi?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Kohteen $0 poistaminen katkaisee yhteyden palvelimeen, jolloin "
+"hallintakäyttöliittymä ei ole saatavilla."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Poistetaan $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Tuhotaan LVM2 taltioryhmä $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "Stratisvarannon poistaminen tuhoaa kaiken sillä olevan datan."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "Tiedostojärjestelmän poistaminen tuhoaa kaiken sillä olevan datan."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "Loogisen taltion poistaminen tuhoaa kaiken sillä olevan datan."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "Osion poistaminen tuhoaa kaiken sillä olevan datan."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting erases all data on a RAID device."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Poistaminen tuhoaa kaiken RAID-laitteella olevan datan."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "VDO-laitteen poistaminen tuhoaa kaiken sillä olevan datan."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "Poistaminen poistaa kaikki taltioryhmässä olevat tiedot."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting erases all data on a volume group."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "Poistaminen poistaa kaikki taltioryhmässä olevat tiedot."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "Poistaminen poistaa seuraavat tiedostot:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Kuvaus"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Työpöytä"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Irrotettava"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Yksityiskohdat"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Kehitys"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Laite"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Laitetiedosto"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Laite on vain-luku-muotoa"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Palvelun nimi"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Diagnostiikkaraportit"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Hakemisto"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Hakemistoon $0 ei voi kirjoittaa tai se ei ole olemassa."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Poista samanaikainen monisäikeisyys käytöstä"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Poista palomuuri käytöstä"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Poista tuned käytöstä"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Ei käytössä"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Estä interaktiivinen salasana"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Kiellä salasanalla tunnistautuminen"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Estä suorittaminen (piilota)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Katkaise yhteys"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Katkaistu"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Levyn I/O"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Levy on kunnossa"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Levy on rikkoutumassa"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Levyn tunnuslause"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Levyt"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Levyjen käyttö"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+#, fuzzy
+#| msgid "Dismiss $0 alert"
+#| msgid_plural "Dismiss $0 alerts"
+msgid "Dismiss"
+msgstr "Hylkää $0 hälytys"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Hylkää $0 hälytys"
+msgstr[1] "Hylkää $0 hälytykset"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Hylkää valitut hälytykset"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Käytettävä kieli"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+#, fuzzy
+#| msgid "RAID 5 (distributed parity)"
+msgid "Distributed parity (RAID 5)"
+msgstr "RAID 5 (hajautettu pariteetti)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "Älä liitä"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Älä liitä automaattisesti käynnistyksen yhteydessä"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Telakka"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Ei käynnisty automaattisesti"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "Ei liitetä käynnistyksen yhteydessä"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Toimialue"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Toimialue"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Toimialue-osoite"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Toimialueen ylläpitäjän nimi"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Toimialueen ylläpitäjän salasana"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Toimialueeseen ei saatu yhteyttä"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Toimialue ei ole tuettu"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Älä toista"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+#, fuzzy
+#| msgid "RAID 6 (double distributed parity)"
+msgid "Double distributed parity (RAID 6)"
+msgstr "RAID 6 (kaksinkertainen hajautettu pariteetti)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Lataa"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Lataa uusi selain ilmaiseksi"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Ladattu"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Ladataan"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Ladataan $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Asema"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Kaksinkertainen sijoitus"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Laajennettu osio"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Muokkaa"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Muokkaa /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Muokkaa Tang-avainpalvelinta"
+
+#: pkg/networkmanager/vlan.jsx:91
+#, fuzzy
+#| msgid "VLAN settings"
+msgid "Edit VLAN settings"
+msgstr "VLAN-asetukset"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:135
+#, fuzzy
+#| msgid "Bond settings"
+msgid "Edit bond settings"
+msgstr "Sidoksen asetukset"
+
+#: pkg/networkmanager/bridge.jsx:96
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Edit bridge settings"
+msgstr "Sillan asetukset"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Muokkaa mukautettua palvelua $0-vyöhykkeellä"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Muokkaa konetta"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Muokkaa koneita"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Muokkaa motd-tiedostoa"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Liitoskohta"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Muokkaa sääntöjä ja vyöhykkeitä"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Muokkaa palvelua"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Muokkaa palvelua $0"
+
+#: pkg/networkmanager/team.jsx:154
+#, fuzzy
+#| msgid "Team settings"
+msgid "Edit team settings"
+msgstr "Joukkueen asetukset"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Muokkaa käyttäjää"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Avaimen muokkaaminen vaatii vapaan paikan"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Työnnetään $target ulos"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "Sulautettu tietokone"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Tyhjä"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Tyhjä"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Tyhjä salasana"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Tyhjennetään $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Ota käyttöön"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Ota laite käyttöön tai poista se käytöstä"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Ota palvelu käyttöön"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Ota palomuuri käyttöön"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Käytössä"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "Otetaan käyttöön $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Salaa tiedot"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+#, fuzzy
+#| msgid "Edit Tang keyserver"
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Muokkaa Tang-avainpalvelinta"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+#, fuzzy
+#| msgid "Encryption passphrase"
+msgid "Encrypt data with a passphrase"
+msgstr "Salauksen tunnuslause"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Salattu"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Salattu $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted Stratis pool $0"
+msgid "Encrypted Stratis pool"
+msgstr "Salattu Stratisvaranto $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Salattu looginen taltio $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Salattu osio $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Salaus"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Salauksen valinnat"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Salauksen tunnuslause"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Salauksen tyyppi"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Päättynyt"
+
+#: pkg/networkmanager/wireguard.jsx:309
+#, fuzzy
+#| msgid "Mount point"
+msgid "Endpoint"
+msgstr "Liitoskohta"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Pakottava"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Parannuspäivitykset saatavilla"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Anna kelvollinen MAC-osoite"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Koko aliverkko"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Merkintä $0:lla"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Tyhjennetään $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Virheet"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Virhe"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Virhe ja yli"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Tapahtui virhe"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "Virhe asennettaessa pakettia $0: PackageKit ei ole asennettu"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Virheviesti"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "Virhe suoritettaessa semanagea järjestelmän muutosten löytämiseksi"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Virhe tallentaessa valtuutettuja avaimia: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Virhe asettaessa SELinuxin tilaa: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Esimerkki: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Esimerkki: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Erinomainen salasana"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Laajennusrunko"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Vanhenna tili"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Vanhenna tili $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Vie"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Vie verkkoon"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Laajennettu tieto"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Laajennettu osio"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS ei ole otettu oikein käyttöön"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS jossa muita yleisiä kriteerejä koskevia rajoituksia."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Epäonnistui"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Ei voitu lisätä konetta: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Portin lisääminen epäonnistui"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Palvelun lisääminen epäonnistui"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Vyöhykkeen lisääminen epäonnistui"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Salasanan vaihtaminen epäonnistui"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "PCP:n määritys epäonnistui"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Hälytyksen tuhoaminen epäonnistui: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "tuned:n käytöstä poistaminen epäonnistui"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "muokatun profiilin käytöstä poistaminen epäonnistui"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Koneen muokkaaminen epäonnistui: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Palvelun muokkaaminen epäonnistui"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "$0:n käyttöönotto firewalld:ssä epäonnistui"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "tuned:n käyttöönotto epäonnistui"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Logien nouto epäonnistui"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Valtuutettujen avainten lataaminen epäonnistui."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Yksikön lataaminen epäonnistui"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"Dnf-automatic.timer- tai dnf-automatic-install.timer-tiedostojen "
+"jäsentäminen epäonnistui. Poista mukautetut ohitukset määrittääksesi "
+"automaattiset päivitykset."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Ei voitu käynnistää uudelleen palvelua"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Muutosten tallentaminen /etc/motd:een epäonnistui"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Asetusten tallentaminen epäonnistui"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Käynnistäminen epäonnistui"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Profiilin vaihtaminen epäonnistui"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Tiedoston tila"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Tiedostojärjestelmä"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "Tiedostojärjestelmä on lukittu"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Tiedostojärjestelmän nimi"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Luodaan tiedostojärjestelmää $target:een"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+#, fuzzy
+#| msgid "The filesystem is already mounted at $0. Proceeding will unmount it."
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr ""
+"Tiedostojärjestelmä on jo liitetty hakemistoon $0. Jatkaminen irrottaa sen."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Suodata nimen tai kuvauksen mukaan"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Suodata valikkokohteita"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Suodata palveluja"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Suotimet"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Sormenjälki"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Palomuuri"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Palomuuri ei ole saatavilla"
+
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Laiteohjelmiston versio"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "Korjaa NBDE-tuki"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Kirjasinkoko"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Kielletty suorittamasta"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Pakota muutos"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Pakota poisto"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Pakota salasanan vaihdos"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Alusta"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Alusta $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Alusta ja liitä"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format and mount"
+msgid "Format and start"
+msgstr "Alusta ja liitä"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Vain alustus"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "Talletuslaitteen alustaminen tuhoaa kaiken sillä olevan datan."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Edelleenohjauksen viive $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Kehyksen numero"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Vapaa tila"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Vapaamuotoinen haku"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Perjantai"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "Lähde"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Koko nimi"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Yhdyskäytävä"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Yleinen"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Luotu"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Mene kohteeseen $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Siirry sovellukseen"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Mene nyt"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Kaavion näkyvyys"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Kaavion näkyvyysasetusvalikko"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Ryhmä"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Ryhmän nimi"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Ryhmät"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Kasvata"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Kasvata sisältöä"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Kasvata loogista kokoa $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Kasvata loogista taltiota"
+
+#: pkg/storaged/block/resize.jsx:409
+#, fuzzy
+#| msgid "partition"
+msgid "Grow partition"
+msgstr "osio"
+
+#: pkg/storaged/stratis/pool.jsx:337
+#, fuzzy
+#| msgid "Grow to take all space"
+msgid "Grow the pool to take all space"
+msgstr "Kasvata viemään koko tilan"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Kasvata viemään koko tilan"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Hair Pin -tila"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Hairpin-tila"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Kädessä pidettävä"
+
+#: pkg/storaged/drive/drive.jsx:64
+#, fuzzy
+#| msgid "Hard Disk"
+msgid "Hard Disk Drive"
+msgstr "Kiintolevy"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Laitteistotiedot"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Terveys"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Tervehdysaika $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Ohje"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Hide confirmation password"
+msgstr "Vahvista salasana"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "Use password"
+msgid "Hide password"
+msgstr "Käytä salasanaa"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "Hierarkiatunnus"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr ""
+"Parempi yhteentoimivuus lisääntyneen hyökkäysrajapinnan kustannuksella."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Historian pakettien määrä"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Kotihakemisto"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Kone"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Koneen avain on väärin"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Koneen nimi"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Koneet"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Joka tunti"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Tuntia"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "Kuinka tarkastaa"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "Tunniste"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "Tunnus $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP-osoite"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"IP-osoite reitityksen etuliitteellä. Erota useita arvoja pilkulla. "
+"Esimerkki: 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+#, fuzzy
+#| msgid "IPv4 address"
+msgid "IPv4 addresses"
+msgstr "IPv4-osoite"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4-asetukset"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6-asetukset"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Tunniste"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Jos tämä jätetään tyhjäksi, tunniste generoidaan porttien palveluiden ja "
+"porttinumeroiden perusteella"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Jos sormenjälki on sama, napsauta \"Hyväksy avain ja kirjaudu sisään\". "
+"Muussa tapauksessa älä kirjaudu sisään ja ota yhteyttä ylläpitäjään."
+
+#: pkg/shell/hosts_dialog.jsx:480
+#, fuzzy
+#| msgid ""
+#| "If the fingerprint matches, click 'Accept key and connect'. Otherwise, do "
+#| "not connect and contact your administrator."
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Jos sormenjälki on sama, napsauta 'Hyväksy avain ja muodosta yhteys'. Muussa "
+"tapauksessa älä muodosta yhteyttä ja ota yhteyttä järjestelmänvalvojaan."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ohita"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "Sisään"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "Suorita päätteessä: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Jos haluat sallia sisäänkirjautumisen koneelle $0 käyttäjänä $1 ilman "
+"salasanaa tulevaisuudessa, käytä käyttäjän $2 sisäänkirjautumissalasanaa "
+"koneen $3 avainsalasanana tai jätä avaimen salasana tyhjäksi."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "Synkronoitu"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Epäaktiivinen"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "Luo looginen taltio"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Mukana olevat palvelut"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr "Saapuvat pyynnöt estetään oletuksena. Lähteviä pyyntöjä ei estetä."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Epäjohdonmukainen tiedostojärjestelmän liitos"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Lisää yhdellä"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Indeksin muisti"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Epäsuora"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Tiedot"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Tiedot ja yli"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Alusta"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Alusta levy $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "Taltion alustaminen tuhoaa kaikki sillä olevat tiedot."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Alustetaan ..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Oivalluksia: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Asennus"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Asenna NFS-tuki"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Asenna Stratis-tuki"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Asenna kaikki päivitykset"
+
+#: pkg/apps/application-list.jsx:200
+#, fuzzy
+#| msgid "Package information"
+msgid "Install application information"
+msgstr "Pakettitiedot"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Asenna cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Asenna kpatch-päivitykset"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "Asenna realmd-tuki"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Asenna tietoturvapäivitykset"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr ""
+"Asenna setroubleshoot-server tehdäksesi vianetsinnän SELinux-tapahtumista."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Asennetaan ohjelmistoja"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Asenna $0-paketti"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr "Asennusta ei tueta ilman asennettua cockpit-pakettia"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Asennettu"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Asennetaan"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Asennetaan $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "Paketin $0 asentaminen poistaa paketin $1."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Asennetaan paketit"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Liitäntä"
+msgstr[1] "Liitännät"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Liitännän jäsenet"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Liitännät"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Sisäinen virhe"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Sisäinen virhe: Virheellinen haasteen otsikko"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Virheellinen"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Ei kelvollinen osoite $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Virheellinen päivämuoto"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Virheellinen päivämuoto ja aikamuoto"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Ei kelvollinen vanhenemispäivä"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Virheelliset tiedosto-oikeudet"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Virheellinen avain"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Virheellinen metriikka $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Virheellinen päivien lukumäärä"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Virheellinen porttinumero"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Virheellinen etuliite $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Virheellinen etuliite tai verkkopeite $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Ei kelvollinen alue"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Virheellinen aikamuoto"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Virheellinen aikavyöhyke"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Virheellinen käyttäjätunnus tai salasana"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "IoT -yhdyskäytävä"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "Onko sshd käynnissä eri portissa?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Työt"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Liity"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Liity toimialueeseen"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Liity toimialueeseen"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Liitytään"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Toimialueeseen liittyminen vaatii realmd:n asentamisen"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Liittyminen tähän toimialueeseen ei ole tuettu"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Liittyy tämän nimitilaan"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Päiväkirja"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Päiväkirjan tietue"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Päiväkirjan tietuetta ei löytynyt"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Hyppää päiväykseen"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit ei ole asennettu"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Testaa kdump-asetukset"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Pidä yhteys"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Ytimen kaatumisvedos"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Ytimen tyhjennys"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "Ytimen live-korjaus $0 on aktiivinen"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Ytimen live-korjaus $0 on asennettu"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Ytimen live-korjausten asetukset"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Ytimen live-korjauksia asennetaan"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Avaimen salasana"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Avainpaikkoja tuntemattomilla tyypeillä ei voi muokata täällä"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Avainlähde"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Avaimet"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Avainpalvelin"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Avainpalvelimen osoite"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "Avaimenpalvelimen poisto voi estää lukituksen avaamisen$0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP-avain"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "LEGACY Active Directoryn yhteentoimivuudella."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "VDO pool"
+msgid "LVM2 VDO pool"
+msgstr "VDO-varanto"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Looginen taltio"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Loogiset taltiot"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "LVM2 fyysinen taltio"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "LVM2 physical volume"
+msgid "LVM2 physical volumes"
+msgstr "LVM2 fyysinen taltio"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM-taltioryhmä"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "LVM2-taltioryhmä $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Kannettava"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Viimeiset 24 tuntia"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Viimeiset 7 päivää"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Viimeksi aktiivinen"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "Viimeistä avaimen paikkaa ei voida poistaa"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Viimeksi tarkistettu: $0 sitten"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "Viimeistä avaimen paikkaa ei voida poistaa"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Edellinen kirjautuminen"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Viimeksi muokattu: $0 sitten"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Edellinen onnistunut kirjautuminen:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Opi lisää"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "Poistu $0:sta"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Jätä toimialue"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Jätä tyhjäksi ohittaaksesi salauksen"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Käytetään GNU LGPL -versio 2.1 lisenssiä"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Vaalea"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Rajoita pääsyä"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Rajoitettu käyttö"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"Rajoitettu käyttö -tila rajoittaa ylläpito-oikeuksia. Joitakin "
+"verkkokonsolin toimintoja on karsittu."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Rajat"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Linkki alas -viive"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Yksityisosoite"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Linkin valvonta"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Linkki ylös -viive"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Linkin seuranta"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Linkitetty"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem $0"
+msgid "Linux filesystem data"
+msgstr "Irrota tiedostojärjestelmä $0"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Sivutuksen tila"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Kuuntele"
+
+#: pkg/networkmanager/wireguard.jsx:242
+#, fuzzy
+#| msgid "Listen"
+msgid "Listen port"
+msgstr "Kuuntele"
+
+#: pkg/networkmanager/wireguard.jsx:149
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Listen port must be a number"
+msgstr "Koon tulee olla numero"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Listauksen yksiköt"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Yksiköiden listaus epäonnistui: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Kuormitus"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Kuormantasaus"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Lataa aiemmat tiedot"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Lataa aiemmat merkinnät"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Saatavilla olevien päivitysten lataus epäonnistui"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Ladataan saatavilla olevat päivitykset, odota hetki..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Ladataan aiemmat merkinnät"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Ladataan avaimet..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "SSH-avainten lataaminen epäonnistui"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Yksiköiden lataus epäonnistui"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Ladataan paketteja..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Ladataan järjestelmän muutoksia ..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Yksiköiden lataus epäonnistui"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Ladataan..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Paikalliset tilit"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Paikallinen tiedostojärjestelmä"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Paikallinen liitoskohta"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "No storage"
+msgid "Local storage"
+msgstr "Ei tallennustilaa"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "paikallisesti polussa $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Sijainti"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Lukitse"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "Lukitse $0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Lukitse tili"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgid "Locked"
+msgid "Locked data"
+msgstr "Lukitse"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Lukitaan $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Kirjaudu sisään"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Kirjaudu kohteeseen $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Kirjaudu sisään palvelimesi käyttäjätilillä."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Kirjaa viestit"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Kirjaudu ulos"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Kirjaa käyttäjä ulos"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Kirjautunut sisään"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Looginen"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Looginen taltio (tilannevedos)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Looginen koko"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Looginen taltio"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Looginen taltio (tilannevedos)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "$0:n looginen taltio"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Sisäänkirjautuminen"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Kirjaudu sisään uudelleen"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Kirjautuminen epäonnistui"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Sisäänkirjautumisen muoto"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Kirjautumishistoria"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Sisäänkirjautumisten historialista"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "Kirjaa $0 ulos"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Uloskirjautuminen onnistui"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Lokit"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Matalan tason työpöytä"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Eväslaatikko"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "RAID-laite"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "RAID-laite $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Laite on vain-luku-muotoa"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "Palvelu on käynnissä"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "RAID-laite"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Lisää levyjä"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (suositeltu)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU:n tulee olla positiivinen numero"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Koneen ID"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Koneen SSH-avaimen sormenjäljet"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Pääpalvelimen runko"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Ylläpito"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+#, fuzzy
+#| msgid "Rename filesystem"
+msgid "Manage filesystem sizes"
+msgstr "Nimeä uudelleen tiedostojärjestelmä"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Tallennustilan hallinta"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Hallitut liitännät"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "LVM:ien hallinta"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "NFS-liitosten hallinta"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "RAIDien hallinta"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "VDO:iden hallinta"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "VLANien hallinta"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Palomuurin hallinta"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Verkkosidosten hallinta"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Verkkosiltojen hallinta"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Verkkojoukkojen hallinta"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Osioiden hallinta"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Fyysisten asemien hallinta"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Palvelujen hallinta"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Ohjelmistopäivitysten hallinta"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Käyttäjätilien hallinta"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Käsikirja"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Manuaalisesti"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Merkitään $target virheelliseksi"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Piilota palvelu"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Piilotettu"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Palvelun piilottaminen estää kaikkia riippuvaisia yksiköitä toimimasta. "
+"Tällä voi olla odotettua suurempi vaikutus. Vahvista, että haluat piilottaa "
+"tämän yksikön."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Viestin enimmäisikä $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Optinen asema"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Muisti"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Muistitekniikka"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Muistin käyttö"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Muistin käyttö/näennäismuisti"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Yhdistetty"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Viesti sisäänkirjautuneille käyttäjille"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "Virheeseen liittyvät viestit saattavat löytyä päiväkirjasta:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Metadataa käytetty"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Menetelmä"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Mittarit"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Mittarit ja historia"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Mittaushistoriaa ei voitu ladata"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Mittareiden asetukset"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Minitietokone"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Minitorni"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Minuutit tulee olla esitetty numerovälillä 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "Tarkasti"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minuutit"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Lievennykset"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Tila"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Malli"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Muokataan $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Maanantai"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Monitorointiväli"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Kohteiden seuranta"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Joka kuukausi"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Lisätietoja..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Liitos"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "Liitoskohta"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "Liitä, kun verkko tulee saataville, jätä vika huomiotta"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Liitä myös automaattisesti käynnistyksessä"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Liitä käynnistyksen yhteydessä"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Liitä automaattisesti $0:een käynnistyksessä"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Liitä ennen palveluiden aloittamista"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Liitoksen kokoonpano"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Liitoksen tiedostojärjestelmä"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Liitä nyt"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Liitä hakemistoon $0 nyt"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Liitosvalinnat"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Liitoskohta"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Liitoskohta ei voi olla tyhjä"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Liitoskohta ei voi olla tyhjä."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Liitoskohta on jo käytetty lohkolaitetta $0 varten"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Liitoskohdan täytyy alkaa \"/\":lla."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Liitä vain luku -tilassa"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Liitä odottamatta, jätä vika huomiotta"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Liitetään $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Liittyy ennen palveluiden aloittamista"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Liittyy yhtäaikaa palveluiden aloittamisten kanssa"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+"Liittyy yhtäaikaa palveluiden aloittamisten kanssa, mutta sen jälkeen, kun "
+"verkko on käytettävissä"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Monijärjestelmäinen alusta"
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Multipathed devices"
+msgid "Multipathed devices"
+msgstr "Monipolkuiset laitteet"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS-liitos"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA:n ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP-palvelin"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Nimi"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Nimi ei voi olla tyhjä."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Nimi ei voi olla tyhjä."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Nimi voi olla enintään $0 tavua pitkä"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Nimi voi sisältää enintään $0 merkkiä"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Nimi voi sisältää enintään 127 merkkiä."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Nimi voi sisältää enintään 127 merkkiä."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Nimi ei voi sisältää merkkiä '$0'."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "Nimi ei voi sisältää merkkiä '$0'."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Nimi ei voi sisältää välilyöntiä."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Tarvitaan vähintään yksi NTP-palvelin"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Verkko"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Verkon I/O"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Verkkosidos"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Verkkolaitteet ja kaaviot vaativat NetworkManagerin"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Verkkolokit"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Verkon käyttö"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager ei ole asennettu"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager ei ole käynnissä"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Verkon käyttö"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Verkko"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Ei koskaan"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Älä koskaan vanhenna tiliä"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Älä koskaan vanhenna salasanaa"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Ei koskaan sisäänkirjautuneena"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Uusi NFS-liitos"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Uusi kone"
+
+#: pkg/shell/hosts_dialog.jsx:464
+#, fuzzy
+#| msgid "New host"
+msgid "New host: $0"
+msgstr "Uusi kone"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Uuden avaimen salasana"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Uusi nimi"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Uusi tunnuslause"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Uusi salasana"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Uutta salasanaa ei hyväksytty"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Seuraava"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Ei"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "ID:tä ei ole määritetty"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Ei SELinux-hälytyksiä."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Ei sovelluksia asennettuna tai saatavilla."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Ei käytettävissä olevia paikkoja"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Lohkolaitteita ei ole saatavilla."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "Käynnistyslaitetta ei löytynyt"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Ei kantajaa"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Konfigurointitietoja ei löytynyt"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Ei tietoja saatavilla"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Tietoja ei ole saatavilla välillä $0 ja $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Ei viivettä"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Ei kuvausta saatavilla"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Kuvausta ei annettu."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "Käynnistyslaitetta ei löytynyt"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Levyjä ei ole saatavilla."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Ei löytynyt lokeja"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "Tuloksia ei löytynyt"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Ei salausta"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Ei tapahtumia"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Ei tiedostojärjestelmää"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Ei tiedostojärjestelmää"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Ei vapaita avainpaikkoja"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Ei vapaata tilaa"
+
+#: pkg/storaged/partitions/partition.jsx:79
+#, fuzzy
+#| msgid "Create partition"
+msgid "No free space after this partition"
+msgstr "Luo osio"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Ryhmän nimeä ei ole määritetty"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Koneen avaimia ei löytynyt."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Tälle sovellukselle ei löytynyt asennuspakettia."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Ei avaimia lisätty"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "Ei hakua vastaavia kieliä"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Ei lokimerkintöjä"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Ei loogisia taltioita"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Ei löytynyt lokeja"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Ei vastaavia tuloksia"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Ei välinettä asetettuna"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Ei osiointia"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "Ei osiointia"
+
+#: pkg/networkmanager/wireguard.jsx:341
+#, fuzzy
+#| msgid "No keys added"
+msgid "No peers added."
+msgstr "Ei avaimia lisätty"
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "Fyysiset taltiot"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Oikeaa nimeä ei ole määritetty"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Tuloksia ei löytynyt"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Ei tuloksia, jotka vastaavat suodatinkriteerejä. Tyhjennä kaikki suodattimet "
+"nähdäksesi tulokset."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Ei sääntöosumia"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No storage"
+msgid "No storage found"
+msgstr "Ei tallennustilaa"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Ei loogisia taltioita"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Tiedostoa tai hakemistoa ei löydy"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Ei järjestelmän muutoksia"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Ei järjestelmän ilmoituksia."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Ei päivityksiä"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Käyttäjänimeä ei ole määritetty"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Ei yhtään"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Ei kelvollinen yksityinen avain"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Ei oikeutta poistaa palomuuri käytöstä"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Ei oikeutta ottaa käyttöön palomuuria"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Ei saatavilla"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Ei kytketty"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Ei yhdistetty tilastoihin"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Ei yhdistetty koneeseen"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space"
+msgid "Not enough free space"
+msgstr "Ei tarpeeksi tilaa"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "Ei tarpeeksi tilaa"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "Ei tarpeeksi tilaa kasvaa."
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Ei löytynyt"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Ei asennettu"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "Not permitted to configure realms"
+msgid "Not permitted to configure network devices"
+msgstr "Alueen määrittäminen ei ole sallittua"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Alueen määrittäminen ei ole sallittua"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Ei oikeutta suorittaa tätä toimintoa."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Ei valmiina"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Ei rekisteröity"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Ei käynnissä"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Ei asetettu"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Ei synkronisoitu"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Huomautus"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Muistikirja"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Ilmoitus ja yli"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Hämärrä verkko-osoitteet, isäntänimet ja käyttäjänimet"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Hämärretty"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Tapahtui $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Tapahtui välillä $0 ja $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Tapahtumat"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "OK"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Vanha tunnuslause"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Vanha salasana"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Vanhaa salasanaa ei hyväksytty"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "Liitetyllä laitteella"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Epäonnistumisesta"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Kun Cockpit on asennettu, ota se käyttöön 'systemctl enable --now cockpit."
+"socket'."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Vain $0 ja $1 ovat käytössä."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Vain aakkoset, numerot, : , _ , . , @ , - ovat sallittuja"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Vain hätätilanne"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"Käytä vain hyväksyttyjä ja sallittuja algoritmeja käynnistettäessä FIPS-"
+"tilassa."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Hups!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "Avaa pmproxy-palvelu palomuurissa jakaaksesi mittaustietoja."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Toimi '$operation' $target:lla"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Valinnat"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Tai käytä valmiiksi asennettua selainta"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Muu"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"Vaikka interaktiivinen salasanalla tunnistautuminen on estetty, muut "
+"tunnistusmenetelmät ovat edelleen käytettävissä."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Muut valinnat"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Ulos"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Esittely"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Korvaa"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Korvaa olemassa olevat tiedot nollilla (hitaampi)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Paketti cockpit-pcp puuttuu, mittarihistoriaa ei voida näyttää"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Pakettitiedot"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit kaatui"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit ei ole asennettu"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit raportoi virhekoodin $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Paketit"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Sivun nimi"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Vanhemmat"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "$parent-vanhempi"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Osa tätä"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Osa kokonaisuudesta $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Osio"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "$0:n osio"
+
+#: pkg/storaged/partitions/partition.jsx:205
+#, fuzzy
+#| msgid "Volume size is $0. Content size is $1."
+msgid "Partition size is $0. Content size is $1."
+msgstr "Taltion koko on $0. Sisällön koko on $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Osiointi"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Osiot"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Passiivinen"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Tunnuslause"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "Tunnuslause ei voi olla tyhjä"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Tunnuslause ei voi olla tyhjä"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Tunnuslause mistä tahansa muusta avainpaikasta"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Salalauseen poisto voi estää laitteen $0 lukituksen avaamisen."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Tunnuslauseet eivät täsmää"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Salasana"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Salasanan vaihtaminen onnistui"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Salasanan vanheneminen"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "Salasana on yli 256 merkkiä pitkä"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Salasana ei ole hyväksyttävä"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Salasana on liian heikko"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Salasana tulee vaihtaa"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Salasanaa ei hyväksytty"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Salasanan vihje"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Siirrä"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Liittämisvirhe"
+
+#: pkg/networkmanager/wireguard.jsx:215
+#, fuzzy
+#| msgid "Use existing"
+msgid "Paste existing key"
+msgstr "Käytä olemassaolevaa"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Liitä julkisen SSH-avaintiedostosi sisältö tähän"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Polku"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Polun kustannus"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Polun kustannus $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Polku palvelimella"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Polku palvelimella ei voi olla tyhjä."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Polun palvelimella täytyy alkaa \"/\"."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Polku hakemistoon"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Polku tiedostoon"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Polut"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Keskeytä"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot kerää ja analysoi suorituskykymittareita "
+"järjestelmästäsi."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Suorituskykyprofiili"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Lisälaitteen kotelo"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Pysyvä"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "Poista ryhmä $0 pysyvästi?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "Poista $0 pysyvästi?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Poista $0 pysyvästi?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete subvolume $0?"
+msgstr "Poista $0 pysyvästi?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Käyttö estetty"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Salliva"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Fyysinen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "Physical Volumes"
+msgstr "Fyysiset taltiot"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Fyysiset taltiot"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "Fyysisten taltioiden kokoja ei voida muuttaa tässä."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Valitse päivämäärä"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Merkitse yksikkö"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Ping-väli"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Pingin kohteet"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Merkitty yksikkö"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Pizza-laatikko"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Kirjaudu ylläpito-oikeuksien saamiseksi"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Ota JavaScript käyttöön, jotta voit käyttää Verkkokonsolia."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Asenna $0-paketti"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Ratkaise ongelma ja lataa sivu uudelleen."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Määritä vanhenemispäivä"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Määritä kone, johon haluat muodostaa yhteyden"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr ""
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Varanto ohuita loogisia taltioita varten"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Varanto ohuesti varattuja taltioita varten"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Varanto ohuesti varattuja taltioita varten"
+
+#: pkg/storaged/stratis/pool.jsx:464
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Pool passphrase"
+msgstr "Vanha tunnuslause"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Portti"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Kannettava"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Portit"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Luo osio"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Etuliitteen pituus"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Etuliitteen pituus tai verkkopeite"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Valmistellaan"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Nykyinen"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Säilytä"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Tyylikäs konenimi"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Edellinen käynnistys"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Ensisijainen"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Prioriteetti"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Prioriteetti $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+#, fuzzy
+#| msgid "Private"
+msgid "Private key"
+msgstr "Yksityinen"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "Ongelma ylläpitäjäksi tulemisessa"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Ongelman yksityiskohdat"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Ongelmatiedot"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Prosessit, jotka käyttävät sijaintia"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Edistyminen: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Projektin verkkosivusto"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Kysely 'passwd':n kautta aikakatkaistiin"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Kysely 'ssh-add':in kautta aikakatkaistiin"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Kysely 'ssh-keygen':in kautta aikakatkaistiin"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Laajentaa uudelleenlatausta kohteeseen"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Suojaa ennakoiduilta lähiajan hyökkäyksiltä yhteentoimivuuden kustannuksella."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "Anna tunnuslause näiden lohkolaitteiden varannolle:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Julkinen avain"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Tarkoitus"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (raita)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (peili)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (peilien raita)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (omistettu pariteetti)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (hajautettu pariteetti)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (kaksinkertainen hajautettu pariteetti)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID-runko"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID-taso"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM-muisti"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Räkkiin liitettävä runko"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Satunnainen"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Alue"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "Alueen on oltava tiukasti järjestetty"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Sijoitus"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Raw laitteeseen"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Luku"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Lue lisää..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Vain luku"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Luetaan"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Luetaan..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Valmis"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Valmis"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Oikea konenimi"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Oikea konenimi voi sisältää vain pieniä kirjaimia, numeroita, viivoja ja "
+"pisteitä (täytetyillä alitoimialueilla)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Oikea konenimi saa sisältää enintään 64 merkkiä"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Ota käyttöön uudelleen ja käynnistä uudelleen"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Käynnistä uudelleen"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Käynnistä uudelleen päivitysten asentamisen jälkeen"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Uudelleenkäynnistystä suositellaan"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Käynnistä järjestelmä uudelleen..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Vastaanotetaan"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Viimeaikaiset isännät"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Suositeltu, turvalliset asetukset nykyisiä uhkamalleja vastaan."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Yhdistä uudelleen"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Palautetaan"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "Palautetaan RAID-laite $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Päivitetään pakettitietoja"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Kieltäytyminen yhteyden muodostamisesta. Kone on tuntematon"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Kieltäytyminen yhteyden muodostamisesta. Koneen avain ei täsmää"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Kieltäytyminen yhteyden muodostamisesta. Koneen avain on tuntematon"
+
+#: pkg/networkmanager/wireguard.jsx:224
+#, fuzzy
+#| msgid "Generated"
+msgid "Regenerate"
+msgstr "Luotu"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "Luodaan uudelleen initrd"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Rekisteröi…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "Liittyvät prosessit ja palvelut lopetetaan väkisin."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "Liittyvät prosessit lopetetaan väkisin."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "Liittyvät palvelut lopetetaan väkisin."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Lataa uudelleen"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Uudelleenlataus levitetty tältä"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Ladataan uudelleen"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Ladataan jäljellä olevien palvelujen tila"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "Etäkäyttö CIFS/SMB:n välityksellä"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "Etäkäyttö FTP:n välityksellä"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Etäkäyttö NFS:n välityksellä"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "Etäkäyttö NFS:n välityksellä"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "Etäkäyttö SFTP:n välityksellä"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Etäkäyttö SSH:n välityksellä"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "Etäkäyttö SSH:n välityksellä"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Poistot:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Poista"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Poista $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Poista palvelu $0 vyöhykkeeltä $1"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Poistetaanko $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Poista Tang-avainpalvelin?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Poista laite"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Poista kone"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Poista kohde"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Remove missing physical volumes?"
+msgstr "Fyysinen taltio poistetaan $Target:sta"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "Poista tunnuslause avainpaikasta $0?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "Poista tunnuslause?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Poista palvelu $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Poista vyöhyke $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Poistetaan"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Poistetaan $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Verkkoliitännän $0 poistaminen katkaisee yhteyden palvelimeen, jolloin "
+"hallintakäyttöliittymä ei ole saatavilla."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "$Target poistetaan RAID-laitteesta"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"Salasanan poistaminen ilman toisen salasanan vahvistamista saattaa estää "
+"lukituksen avaamisen tai avaimen hallinnan, jos muut salasanat unohdetaan "
+"tai menetetään."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Fyysinen taltio poistetaan $Target:sta"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"cockpit-palvelun poistaminen voi johtaa siihen, että verkkokonsoli ei ole "
+"tavoitettavissa. Varmista, että tämä vyöhyke ei koske nykyistä "
+"verkkokonsoliyhteyttäsi."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "Vyöhykkeen poistaminen poistaa kaikki sen sisäiset palvelut."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Nimeä uudelleen"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Nimeä uudelleen Stratisvaranto"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Nimeä uudelleen tiedostojärjestelmä"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Nimeä ryhmä uudelleen"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "Nimeä uudelleen ryhmä $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Nimeä uudelleen looginen taltio"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Nimeä uudelleen taltioryhmä"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Nimetään uudelleen $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr ""
+"Ryhmän nimen vaihtaminen saattaa vaikuttaa sudoon ja muihin vastaaviin "
+"sääntöihin"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Repair logical volume $0"
+msgstr "Nimeä uudelleen looginen taltio"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Korjataan $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Toista"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Toista joka kuukausi"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Toista tunnuslause"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Toista joka viikko"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Raportoi"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Raportin nimiö"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Raportoi ABRT Analyticsille"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Raportoitu; linkkejä ei ole saatavilla"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Raportointi epäonnistui"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Raportointi peruutettiin"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Ilmoitukset"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Raportit:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Vaadi salasanan vaihto $0 päivän välein"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Vaadi salasanan vaihto $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Vaadi salasanan vaihto ensimmäisen käynnistyksen yhteydessä"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Tämän edellyttämänä"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Tämän edellyttämänä "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Vaatii"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Muokkaaminen edellyttää ylläpitäjän käyttöoikeuksia"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Vaaditaan"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Tämä vaaditaan"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Varattu muisti"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Nollaa"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Nollaa salasana"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Muutetaan $target kokoa"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Salatun tiedostojärjestelmän koon muuttaminen edellyttää levyn lukituksen "
+"avaamista. Anna nykyinen levyn tunnuslause."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Käynnistä uudelleen"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Käynnistä palvelut uudelleen"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Käynnistä palvelut uudelleen..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Käynnistetään uudelleen"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Palautetaan yhteys"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"Kaatumisen tulokset kopioidaan $0:sta $1:een nimellä $2, jos kdump on "
+"määritetty oikein."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+"Kaatumisen tulokset tallennetaan kohtaan $0 nimellä $1, jos kdump on "
+"määritetty oikein."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Palauta"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Käytä olemassa olevaa salausta uudelleen"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Käytä olemassa olevaa salausta uudelleen ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "Tarkastele kryptografista käytäntöä"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Tarkistetaan lokeja"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "kiertovuorottelu"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Reitit"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Aja klo"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Tee uusi raportti"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Aja kohteessa"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Tee raportti"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Suorittaja"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Käynnissä"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Ajonaikainen"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux-pääsynvalvontavirheet"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux on poistettu käytöstä järjestelmässä"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux on poistettu käytöstä järjestelmässä."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux-käytäntö"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "SELinux-järjestelmän tila on tuntematon."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "SELinux-ongelmanratkaisu"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "Levyn $target SMART-itsetesti"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"SOS-raportointi kerää järjestelmätietoja auttaakseen ongelmien "
+"diagnosoinnissa."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH-avain"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "SSH-avain ei ole polku"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH-avaimet"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "STP-ohjauksen viive"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "STP-hello-aika"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP-viestin enimmäisikä"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP:n prioriteetti"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Safarin käyttäjien on tuotava itse allekirjoittavan varmentajan varmenne ja "
+"luotettava siihen:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Lauantai"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Tallenna"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Tallenna ja käynnistä uudelleen"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Tallenna muutokset"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Säästä tilaa pakkaamalla yksittäisiä lohkoja LZ4:llä"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Säästä tilaa tallentamalla identtiset tietolohkot vain kerran"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Uuden salasanan tallentaminen edellyttää levyn lukituksen avaamista. Anna "
+"nykyinen levyn tunnuslause."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Ajastettu sammutus $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Ajastettu sammutus kello $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "Suljettu tietokonekotelo"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Haku"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Etsi verkkoaluetta"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Etsi nimeä tai ID:tä"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Etsi nimeä, ryhmää tai ID:tä"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "Sekuntit tulee olla esitetty numerovälillä 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Sekunnit"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Secure shell -avaimet"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Poistaa turvallisesti $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Security Enhanced Linuxin asetukset ja ongelmanratkaisu"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Tietoturvapäivityksiä saatavilla"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Vain tietoturvapäivityksiä"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Turvallisuuspäivitykset asennetaan $0 klo $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Valitse"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Valitse tunniste"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Valitse menetelmä"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Lähetä"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Lähetetään"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Sarjanumero"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Palvelin"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Palvelimen osoite"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Palvelimen osoite ei voi olla tyhjä."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Palvelin ei voi olla tyhjä."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Palvelin on sulkenut yhteyden."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Palvelinohjelmisto"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Palvelu"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Palvelussa on virhe"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Palvelulokit"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Palvelut"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Palvelut, jotka käyttävät sijaintia"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Istunto"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Istunto on päättymässä"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Aseta"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Määritä konenimi"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Luo osio $0:een"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Aseta salasana"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Aseta aika"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Aseta tähän"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Asetukset tehty"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Aseta heikko salasana"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Asetus poikkeaa määritetystä tilasta ja palaa seuraavalla käynnistyskerralla."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Tehdään asetuksia"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Silmukkalaitteen $target asettaminen"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Asetukset"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Vakavuus"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Jaettu"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Komentotulkki"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Komentotulkin komentosarja"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Vaihto + Syöttö"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "Näytä kaikki ketjut"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Näytä kaikki ketjut"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Show confirmation password"
+msgstr "Vahvista salasana"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Näytä sormenjäljet"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Näytä viestit, jotka sisältävät annetun merkkijonon."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Näytä määritetyn järjestelmäyksikön viestit."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Näytä viestit tietystä käynnistyksestä."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Näytä lisää suhteita"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "New password"
+msgid "Show password"
+msgstr "Uusi salasana"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Näytä suhteet"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Kutista"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Kutista looginen taltio"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+#, fuzzy
+#| msgid "partition"
+msgid "Shrink partition"
+msgstr "osio"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Kutista taltio"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Sammuta"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Sammuta"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Siitä asti kun"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Yksi sijoitus"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Koko"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "Koko ei voi olla negatiivinen"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "Koko ei voi olla nolla"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Koko on liian suuri"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Koon tulee olla numero"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Koon tulee olla vähintään $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Ohita päävalikko"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Siirry sisältöön"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Paikka"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Paikka $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Tilannevedos"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Pistokkeet"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Ohjelmistopäivitykset"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Ohjelmistopohjaiset ratkaisut auttavat estämään suorittimen "
+"tietoturvaongelmia. Näillä lievennyksillä on sivuvaikutus suorituskyvyn "
+"heikentämisessä. Muuta näitä asetuksia omalla vastuullasi."
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Ratkaisun soveltaminen onnistui"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Ratkaisu epäonnistui"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Ratkaisut"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"Joidenkin tämän varannon lohkolaitteiden koko on kasvanut varannon luomisen "
+"jälkeen. Varantoa voidaan turvallisesti kasvattaa hyödyntämään nyt vapaana "
+"olevaa tilaa."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr "Joku muu ohjelma käyttää tällä hetkellä paketinhallintaa, odota ..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Jotkin ohjelmistot on käynnistettävä uudelleen manuaalisesti"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Lajiteltu vähiten luotettavista luotetuimpiin"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Tilaa säästävä tietokone"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Spanning tree -protokolla"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Spanning tree -protokolla (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Varaosa"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Tietty aika"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Nopeus"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Vakaa"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Käynnistä"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Käynnistä ja ota käyttöön"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Käynnistä monipolku"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Käynnistä palvelu"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr ""
+"Aloita merkintöjen näyttäminen määritetystä tai sitä myöhemmästä päivästä."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr ""
+"Aloita merkintöjen näyttäminen määritetystä tai sitä aiemmasta päivästä."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Aloitettu"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "Käynnistetään RAID-laite $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Aloitetaan sivutustila $target"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Tila"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Staattinen"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Tila"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Tikku-PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Tahmea"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Pysäytä"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Pysäytä ja poista käytöstä"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Pysäytä ja poista"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Pysäytä ja irrota"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Pysäytä laite"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Lopeta koneiden muokkaaminen"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Pysäytä raportti"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "Pysäytetään RAID-laite $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Lopetetaan sivutustilan $target"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Tallennustila"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "Tallennustilaa ei voida hallita tässä järjestelmässä."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Tallennustilan lokit"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Tallenna tunnuslause"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Tallennettu tunnuslause"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "$0 lohkolaite"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "Add block devices"
+msgid "Stratis block devices"
+msgstr "Lisää lohkolaitteita"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+#, fuzzy
+#| msgid "VDO backing devices can not be made smaller"
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "VDO-taustalaitteita ei voi tehdä pienemmiksi"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystem"
+msgstr "Luo tiedostojärjestelmä"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems"
+msgstr "Luo tiedostojärjestelmä"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Luo tiedostojärjestelmä"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Stratis-varanto"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:97
+#, fuzzy
+#| msgid "Set password"
+msgid "Strong password"
+msgstr "Aseta salasana"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Tynkä"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Tyyli"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Alirunko"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Pieni kannettava tietokone"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "Systemd-signaalien tilaaminen epäonnistui: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "Taltion luominen epäonnnistui"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Kopioitu onnistuneesti leikepöydälle"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Kopioitu onnistuneesti leikepöydälle!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Sunnuntai"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Sivutus"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "tiedostojärjestelmien $0 kokoa ei voida muuttaa tässä."
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Sivutettu"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Vaihda $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Kytke $0 pois päältä"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Kytke $0 päälle"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Vaihda ylläpitäjän käyttöoikeuksiin"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Vaihda rajoitettuun käyttöön"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 kytkeminen pois päältä katkaisee yhteyden palvelimeen ja tekee hallinnon "
+"käyttöliittymän ei saataville."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Laitteen $0 kytkeminen päälle katkaisee yhteyden palvelimeen, jolloin "
+"hallintakäyttöliittymä ei ole saatavilla."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Synkronoitu"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Synkronoi palvelimen $0 kanssa"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Synkronoidaan"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "Synkronoidaan RAID-laitetta $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Järjestelmä"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Järjestelmän diagnostiikka"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Järjestelmätiedot"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Järjestelmä on ajan tasalla"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Järjestelmän muutokset"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Järjestelmän aika"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemd-yksiköt"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tabletti"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang-avainpalvelin"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Kohde"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Kohteet"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Joukkue"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Joukkueen portti"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Joukkueen portin asetukset"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Pääte"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Lopeta istunto"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Testimääritys"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Testi on käytettävissä vain kdump-palvelun ollessa käynnissä."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Testaa kdump-asetukset"
+
+#: pkg/kdump/kdump-view.jsx:374
+#, fuzzy
+#| msgid ""
+#| "This will test kdump settings by crashing the kernel and thereby the "
+#| "system. Depending on the settings, the system may not automatically "
+#| "reboot and the process may take a while."
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Tämä testaa kdump-asetukset kaatamalla ytimen ja siten järjestelmän. "
+"Asetuksista riippuen järjestelmä ei välttämättä käynnisty automaattisesti "
+"uudelleen ja prosessi voi kestää jonkin aikaa."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Testataan yhteyttä"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "$0 paketti ei ole saatavilla mistään ohjelmistovarastosta."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "Paketti $0 on asennettava Stratis-varannon luomiseen."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "Paketti $0 on asennettava."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "Paketti $0 asennetaan VDO-laitteiden luomiseksi."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP-osoite tai konenimi ei voi sisältää välilyöntiä."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "RAID-pakka on heikentyneessä tilassa"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr "RAID-laitteen on oltava käynnissä levyjen poistamiseksi."
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+"Käyttäjän $0:n SSH-avain $1 koneella $2 lisätään käyttäjän $4 tiedostoon $3 "
+"koneella $5."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"SSH-avain $0 on käytettävissä koko istunnon ajan ja on käytettävissä myös "
+"muille koneille sisäänkirjautumista varten."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"SSH-avain koneelle $0 sisäänkirjautumista varten on suojattu salasanalla, "
+"eikä kone salli salasanalla kirjautumista. Anna avaimen $1 salasana."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"SSH-avain koneelle $0 sisäänkirjautumista varten on suojattu. Voit kirjautua "
+"sisään joko kirjautumissalasanallasi tai antamalla avaimen $1 salasanan."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"Tili '$0' on pakotettu vaihtamaan salasanan seuraavalla "
+"sisäänkirjautumiskerralla"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Palvelu cockpit sisältyy automaattisesti"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"Määritetty tila on tuntematon, se saattaa muuttua seuraavalla "
+"käynnistyskerralla."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr "Tämän VDO-laitteen luominen ei päättynyt eikä laitetta voida käyttää."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"Tällä hetkellä kirjautuneena oleva käyttäjä ei saa nähdä tietoja avaimista."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr "Levy on avattava ennen alustamista. Anna olemassa oleva tunnuslause."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "Tiedosto $0 poistetaan."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "Tiedostojärjestelmällä ei ole pysyvää liitoskohtaa."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "Tiedostojärjestelmällä ei ole pysyvää liitoskohtaa."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Tiedostojärjestelmä on määritetty asennettavaksi automaattisesti "
+"käynnistykseen, mutta sen salauskontin lukitusta ei avata silloin."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Tiedostojärjestelmä on tällä hetkellä liitettynä, mutta sitä ei liitetä "
+"seuraavan käynnistyksen jälkeen."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Tiedostojärjestelmä on tällä hetkellä liitettynä hakemistoon $0, mutta se "
+"liitetään seuraavan käynnistyksen jälkeen hakemistoon $1."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Tiedostojärjestelmä on tällä hetkellä liitettynä hakemistoon $0, mutta sitä "
+"ei liitetä seuraavan käynnistyksen jälkeen."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Tiedostojärjestelmä ei tällä hetkellä ole liitettynä, mutta se liitetään "
+"seuraavan käynnistyksen jälkeen."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Tiedostojärjestelmä ei ole liitettynä."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"Tiedostojärjestelmän lukitus avataan ja se liitetään järjestelmään "
+"seuraavassa käynnistyksessä. Tämä saattaa edellyttää salasanan syöttämistä."
+
+#: pkg/shell/hosts_dialog.jsx:494
+#, fuzzy
+#| msgid "Show fingerprints"
+msgid "The fingerprint should match:"
+msgstr "Näytä sormenjäljet"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Seuraava palvelu käynnistetään uudelleen:"
+msgstr[1] "Seuraavat palvelut käynnistetään uudelleen:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "Koko nimi ei saa sisältää kaksoispisteitä."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "Ryhmän ID:n tulee olla positiivinen kokonaisluku"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Ryhmän nimi voi koostua vain a–z-kirjaimista, numeroista, pisteistä, "
+"viivoista ja alaviivoista"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"Kotihakemisto $0 on jo olemassa. Sen omistajuus vaihdetaan uudelle "
+"käyttäjälle."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "Initrd on luotava uudelleen."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Avaimen salasana ei voi olla tyhjä"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Avainsalasanat eivät täsmää"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "Antamasi avain ei ollut kelvollinen."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Viimeistä avaimen paikkaa ei voida poistaa"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "Listatut prosessit ja palvelut lopetetaan väkisin."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "Listatut palvelut lopetetaan väkisin."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "Listatut palvelut lopetetaan väkisin."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "Sisäänkirjautunut käyttäjä ei saa tarkastella järjestelmän muutoksia"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "Kone käynnistyy uudelleen"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "Liitoskohta $0 on näiden prosessien käytössä:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "Liitoskohta $0 on näiden palveluiden käytössä:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Uuden avaimen salasana ei voi olla tyhjä"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Salasana ei voi olla tyhjä"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Salasanat eivät täsmää"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Salasanat eivät täsmää."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"Tuloksena oleva sormenjälki sopii jakaa julkisilla menetelmillä, mukaan "
+"lukien sähköposti."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Palvelin kieltäytyi todentamasta käyttäjää '$0' salasanatodennusta käyttäen, "
+"eikä muita tuettuja todennustapoja ole käytettävissä."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr ""
+"Palvelin kieltäytyi tunnistautumista käyttäen mitään tuetuista tavoista."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"Järjestelmä ei tällä hetkellä tue tiedostojärjestelmän lukituksen avaamista "
+"Tang-avainpalvelimella käynnistyksen aikana."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"Järjestelmä ei tällä hetkellä tue root-tiedostojärjestelmän lukituksen "
+"avaamista Tang-avainpalvelimella."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr "Käyttäjä $0 ei saa muuttaa suorittimen suojauksen lievennyksiä"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "Käyttäjällä $0 ei ole oikeutta muokata kryptografisia käytäntöjä"
+
+#: pkg/kdump/kdump-view.jsx:505
+#, fuzzy
+#| msgid "The user <b>$0</b> is not permitted to manage servers"
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "Käyttäjällä <b>$0</b> ei ole oikeutta hallita palvelimia"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"Käyttäjän on kirjauduttava ulos ja takaisin sisään, jotta uudet asetukset "
+"tulevat voimaan."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Käyttäjänimi voi koostua vain a–z-kirjaimista, numeroista, pisteistä, "
+"viivoista ja alaviivoista."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"Verkkoselaimen kokoonpano estää Cockpitin suorittamasta (ei käytettävissä $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Tällä hetkellä ei ole aktiivisia sivuja"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"Järjestelmässä on laitteita, joilla on useita polkuja, mutta monitiepalvelu "
+"ei ole käynnissä."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "Tällä vyöhykkeellä ei ole aktiivisia palveluja"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Tällä tilillä ei ole valtuutettuja julkisia avaimia."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"Varannossa ei ole tarpeeksi tilaa tilannevedoksen tekemiseen tästä "
+"tiedostojärjestelmästä. Vähintään $0 vaaditaan, mutta vain $1 on saatavilla."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Laitteeseen yhdistämisessä tapahtui odottamaton virhe."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Nämä lisävaiheet ovat välttämättömiä:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Nämä muutokset tullaan tekemään:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Ohut looginen taltio"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Varanto ohuesti varattuja taltioita varten"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+#, fuzzy
+#| msgid ""
+#| "This RAID array has no write-intent bitmap. Such a bitmap can reduce "
+#| "sychronization times significantly."
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"Tällä RAID-ryhmällä ei ole kirjoitusaikeista bittikarttaa. Tällainen "
+"bittikartta voi lyhentää synkronointiaikoja merkittävästi."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"Tämä NFS-liitos on käytössä ja vain sen valintoja on mahdollista muokata."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Tämä VDO-laite ei käytä kaikkia sen tukilaitetta."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Tätä laitetta ei voida hallita täällä."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Tätä laitetta ei voida hallita täällä."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "Tämä laite on tällä hetkellä käytössä."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Tämä kenttä ei voi olla tyhjä"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Tämä ryhmä on ensisijainen ryhmä seuraaville käyttäjille:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "Tämä kone käynnistyy uudelleen, kun päivitykset on asennettu."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "Nämä tiedot tallennetaan vain järjestelmään."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Tämä avainpalvelin on ainoa tapa avata varannon lukitus, eikä sitä voi "
+"poistaa."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "Sisältö ei käytä tätä loogista taltiota kokonaan."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Tämä kone on jo lisätty."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Tässä voi mennä hetki"
+
+#: pkg/storaged/partitions/partition.jsx:204
+#, fuzzy
+#| msgid "This logical volume is not completely used by its content."
+msgid "This partition is not completely used by its content."
+msgstr "Sisältö ei käytä tätä loogista taltiota kokonaan."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Tämä salalause on ainoa tapa avata varannon lukitus, eikä sitä voi poistaa."
+
+#: pkg/storaged/stratis/pool.jsx:333
+#, fuzzy
+#| msgid "This VDO device does not use all of its backing device."
+msgid "This pool does not use all the space on its block devices."
+msgstr "Tämä VDO-laite ei käytä kaikkia sen tukilaitetta."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "Varanto on heikentyneessä tilassa."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Tätä järjestelmää ei ole rekisteröity"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Tämä järjestelmä käyttää mukautettua profiilia"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Tämä järjestelmä käyttää suositeltua profiilia"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"Tämä työkalu asettaa SELinux-käytännön ja auttaa käytäntörikkeiden "
+"ymmärtämisessä ja ratkaisemisessa."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"Tämä työkalu asettaa järjestelmän kirjoittamaan ytimen kaatumisvedokset "
+"levylle."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"Tämä työkalu luo arkiston konfiguraatio- ja diagnostiikkatiedoista käynnissä "
+"olevasta järjestelmästä. Arkisto voidaan tallentaa paikallisesti tai "
+"keskitetysti tallennus- tai seurantatarkoituksiin tai se voidaan lähettää "
+"teknisen tuen edustajille, kehittäjille tai järjestelmänvalvojille auttamaan "
+"teknisten vikojen etsinnässä ja virheenkorjauksessa."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"Tämä työkalu hallinnoi paikallista tallennustilaa, kuten "
+"tiedostojärjestelmiä, LVM2-taltioryhmiä ja NFS-liitoksia."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"Tämä työkalu hallitsee verkkoyhteyksiä, kuten sidoksia, siltoja, ryhmiä, "
+"VLAN-verkkoja ja palomuureja NetworkManagerin ja Firewalld:n avulla. "
+"NetworkManager ei ole yhteensopiva Ubuntun oletusarvoisten systemd-networkd- "
+"ja Debianin ifupdown-komentosarjojen kanssa."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Tätä yksikköä ei ole suunniteltu nimenomaisesti käytettäväksi."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Käyttäjätunnus on jo olemassa"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr ""
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Tämä verkkoselain on liian vanha Web-konsolin käyttämiseen (puuttuu $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Tämä lisää osuman '_BOOT_ID ='. Jos sitä ei ole määritetty, nykyisen "
+"käynnistyksen lokit näytetään. Jos käynnistystunnus jätetään pois, "
+"positiivinen siirtymä etsii käynnistykset päiväkirjan alusta alkaen ja yhtä "
+"suuri tai pienempi kuin nolla-siirtymä etsii käynnistykset päiväkirjan "
+"lopusta alkaen. Siten 1 tarkoittaa ensimmäistä löydettyä käynnistystä "
+"päiväkirjassa aikajärjestyksessä, 2 toista ja niin edelleen; kun -0 on "
+"viimeinen käynnistys, -1 käynnistys ennen viimeistä jne."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Tämä lisää osuman '_SYSTEMD_UNIT =', 'COREDUMP_UNIT =' ja 'UNIT =' "
+"löytääkseen kaikki mahdolliset viestit annetulle yksikölle. Voi sisältää "
+"enemmän yksiköitä pilkuilla erotettuna. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Tämän avulla voit tulevaisuudessa kirjautua sisään ilman salasanaa."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Tämä vyöhyke sisältää cockpit-palvelun. Varmista, että tämä vyöhyke ei koske "
+"nykyistä verkkokonsoliyhteyttäsi."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Torstai"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Kerros"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Aika"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Aikavyöhyke"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Ajastimen luonti epäonnistui"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "Ajastimen poisto epäonnistui"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Ajastimet"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Vinkki: Aseta avaimesi salasana samaksi kuin käyttäjätunnuksesi salasana, "
+"jotta voit automaattisesti tunnistautua muita järjestelmiä kohden."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Tarkista koneen avaimen sormenjälki varmistaaksesi, että haitallinen kolmas "
+"osapuoli ei sieppaa yhteyttä:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Ohjelmistopäivitysten saamiseksi tämä järjestelmä on rekisteröitävä Red "
+"Hatille joko käyttämällä Red Hat -asiakasportaalia tai paikallista "
+"tilauspalvelinta."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Vahvistaaksesi sormenjäljen, suorita seuraava koneella $0 istuessasi "
+"fyysisesti koneen ääressä tai luotettavan verkon kautta:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Tänään"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Vaihda"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Vaihda päivämäärän valitsin"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Vaihda suodattimet"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Liian paljon dataa"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Työkalut"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Viisi suorittimen pääpalvelua"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "Viisi eniten levyä käyttävää palvelua"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "Viisi muistin pääpalvelua"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Koko yhteensä: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Torni"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Tilapäinen"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Lähetetään"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Laukaise"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Tämän laukaisema"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Laukaisimet"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Vianetsintä"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Vianetsintä…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+#, fuzzy
+#| msgid "Untrusted host"
+msgid "Trust and add host"
+msgstr "Epäluotettava kone"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Luottamusavain"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Luottamustaso"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Yritä uudelleen"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Yritetään synkronoida palvelimen $0 kanssa"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Tiistai"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned:n käynnistys epäonnistui"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned on palvelu, joka valvoo järjestelmääsi ja optimoi suorituskyvyn "
+"tietyillä kuormituksilla. Tuned:n ytimessä on profiileja, jotka virittävät "
+"järjestelmän eri käyttötapauksiin."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned ei ole saatavilla"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned ei ole käynnissä"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned on pois päältä"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Ota ylläpitäjän käyttöoikeudet käyttöön"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Tyyppi"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Nimi ei voi sisältää merkkiä '$0'."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Kirjoita suodattaaksesi"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Tämän ratkaisun automaattinen asettaminen epäonnistui"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Siihen osoitteeseen yhdistäminen epäonnistui"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "Ei saada yhteyttä kohteeseen $0."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Annettuun koneeseen $0 ei voida ottaa yhteyttä. Varmista, että ssh on "
+"käynnissä portissa $1, tai määritä toinen portti osoitteeseen."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Virheen lisätietojen hakeminen epäonnistui."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Ei voida kirjautua sisään koneelle $0 SSH-avaimella. Anna salasana. Haluat "
+"ehkä määrittää SSH-avaimesi automaattista kirjautumista varten."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Ei voida kirjautua sisään koneelle $0. Kone ei hyväksy salasanakirjautumista "
+"tai mitään SSH-avaimistasi."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Palvelimeen ei saada yhteyttä"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Liitosta ei voi poistaa"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+#, fuzzy
+#| msgid "Encrypted logical volume of $0"
+msgid "Unable to repair logical volume $0"
+msgstr "Salattu looginen taltio $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "Ei voitu ajaa korjausta: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "Asetusten tallentaminen epäonnistui"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "Asetusten asettaminen epäonnistui: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Ei voitu käynnistää setroubleshootd:tä"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Tiedostojärjestelmää ei voi irrottaa"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Ei käytettävissä"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Ei käytettävissä olevia paketteja"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Kumoa"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "Odottamaton PackageKit-virhe paketin $0 asennuksen aikana: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Odottamaton virhe"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Tunnistamaton data"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Yksikkö"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Tuntematon"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Tuntematon \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Tuntematon ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Tuntematon sovellus"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Tuntematon konfiguraatio"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Tuntematon konenimi"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Tuntematon palvelun nimi"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Tuntematon tyyppi"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Avaa"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Avaa lukitus automaattisesti käynnistyksessä"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Avaa salattu Stratis-varanto"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "Avaa avaimen $0 lukitus"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Avataan $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Avataan levyn lukitus"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Hallitsemattomat liitännät"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Irroita"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "Irrota tiedostojärjestelmä $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Irrota nyt"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Irrotetaan $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Nimeämätön"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "Poista yksikön merkkaus"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Tunnistamaton data"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "Unrecognized data can not be made smaller here."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Tunnistamatonta dataa ei voi pienentää täällä."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Tunnistamatonta dataa ei voi pienentää täällä."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Tukematon taltio"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "Kunnes"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Epäluotettava kone"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Päivitä"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Päivityksen onnistumisen taulukko"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Päivityshistoria"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Päivitä pakettitiedot"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "Päivitys onnistui"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Päivitetty"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr ""
+"Päivitetyt paketit saattavat vaatia uudelleenkäynnistyksen, jotta muutokset "
+"tulevat voimaan."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Päivityksiä saatavilla"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Päivitysten historia"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Päivitykset asennetaan $0 klo $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Päivitetään"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Päivitetään tilaa ..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Toiminta-aika"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Käyttö"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "$0 käyttö"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Käytä"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Käytä pakkausta"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Käytä päällekkäisyyden poistoa"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Käytä avainta"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Käytä salasanaa"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Käytä seuraavia avaimia todentamaan muita järjestelmiä kohden"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Käytä monisanaista lokia"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Käytetty"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"Hyödyllinen liitoksille, jotka ovat valinnaisia tai tarvitsevat "
+"vuorovaikutusta (kuten tunnuslauseita)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Käyttäjä"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "Käyttäjänumero"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "Tätä käyttäjänumeroa käyttää jo toinen käyttäjä"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "Käyttäjän ID-numeron tulee olla positiivinen kokonaisluku"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "Käyttäjän ID-numero ei saa olla suurempi kuin $0"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "Käyttäjän ID-numero ei saa olla pienempi kuin $0"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Käyttäjänimi"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Käyttäjätunnus ei voi olla tyhjä"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Käyttäjätunnus"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "Käytetään LUKS-salausta"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Käytetään Tang-palvelinta"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "VDO-taustalaitteita ei voi tehdä pienemmiksi"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO-laite $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "VDO-tiedostojärjestelmän taltio (pakkaus/kopioiden poisto)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN-tunnus"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Vahvistetaan osoite"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Vahvistetaan todennustunnus"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Toimittaja"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Vahvistettu"
+
+#: pkg/shell/hosts_dialog.jsx:489
+#, fuzzy
+#| msgid "Fingerprint"
+msgid "Verify fingerprint"
+msgstr "Sormenjälki"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Vahvista avain"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Varmistetaan"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Versio"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Poistetaan hyvin turvallisesti $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Näytä kaikki CPU:t"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Näytä kaikki levyt"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Katso kaikki lokit"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Katso kaikki palvelut"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Näytä automaatio-komentosarja"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Katso yksityiskohtaiset lokit"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Tarkastele laitteiston yksityiskohtia"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Tarkastele kirjautumishistoriaa"
+
+#: pkg/storaged/stratis/pool.jsx:351
+#, fuzzy
+#| msgid "View all logs"
+msgid "View logs"
+msgstr "Katso kaikki lokit"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Tarkastele tietoja ja historiaa"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Näytä levykohtainen suorituskyky"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Näytä projektin verkkosivusto"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Tarkastele raporttia"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Katso päivityslokit"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr ""
+"Muistiin liittyvien tietojen katselu vaatii ylläpitäjän käyttöoikeudet."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Käy palomuurissa"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Taltioryhmä"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Volume group is missing physical volumes"
+msgstr "Fyysinen taltio poistetaan $Target:sta"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Taltion koko on $0. Sisällön koko on $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Odotetaan"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Odotetaan lisätietoja..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Odotetaan syötettä…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr "Odotetaan muiden ohjelmien paketinhallinnan käyttämisen päättymistä..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Odotetaan muiden ohjelmistojen hallintatoimintojen päättymistä"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Odotetaan aloittamista …"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Tämän haluama"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Haluaa"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Varoitus ja yli"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+#, fuzzy
+#| msgid "Set weak password"
+msgid "Weak password"
+msgstr "Aseta heikko salasana"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Verkkokonsoli"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Verkkokonsoli Linux-palvelimille"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Verkkokonsoli käynnistyy uudelleen"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Verkkokonsoli toimii rajoitetun pääsyn tilassa."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Verkkokonsolilogo"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Keskiviikko"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Joka viikko"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Viikot"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Kun"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "Tyhjänä ollessa, muodosta yhteys nykyisenä käyttäjänä"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Kun verkkokonsoli käynnistetään uudelleen, et enää näe edistymistä koskevia "
+"tietoja. Päivitysprosessi jatkuu kuitenkin taustalla. Muodosta yhteys "
+"uudelleen jatkaaksesi päivitysprosessin seurantaa."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"Kun tämä vaihtoehto on valittuna, uusi varanto ei salli yliallokaatiota. "
+"Sinun on määritettävä enimmäiskoko jokaiselle varannossa luodulle "
+"tiedostojärjestelmälle. Tiedostojärjestelmiä ei voi suurentaa luomisen "
+"jälkeen. Tilannevedokset on allokoitu kokonaan luomisen yhteydessä. Kaikkien "
+"enimmäiskokojen summa ei voi ylittää varannon kokoa. Tämän etuna on, että "
+"tämän varannon tietojärjestelmistä tila ei voi loppua yllättävällä tavalla. "
+"Haittapuolena on, että sinun on tiedettävä kunkin tiedostojärjestelmän "
+"enimmäiskoko etukäteen ja tilannevedosten luominen on rajoitettua."
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Valkoinen"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:124
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "World wide name"
+msgid "World wide name"
+msgstr "Maailmanlaajuinen nimi"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Kirjoitus"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Kirjoitus-enimmäkseen"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Kirjoitetaan"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Väärä käyttäjätunnus tai salasana"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Vuosittain"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Kyllä"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Yhdistät koneeseen $0 ensimmäistä kertaa."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Sinulla ei ole valtuuksia muokata palomuuria."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Sinulla ei ole oikeutta tarkastella tämän käyttäjätilin julkisia avaimia."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Sinut on kirjautunut ulos toimimattomuuden vuoksi."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Voit yrittää ladata vanhempia merkintöjä."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+"Haluat ehkä vaihtaa avaimen salasanan automaattista sisäänkirjautumista "
+"varten."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Sinun täytyy odottaa kauemmin vaihtaaksesi salasanasi"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr ""
+"Sinun on kirjauduttava uudelleen sisään, jotta voit nähdä mittausten "
+"historiaa"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Sinulla on nyt ylläpitäjän käyttöoikeudet."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Sinut kirjataan ulos $0 sekunnin kuluttua."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Tilisi"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"Selaimesi ei salli liittämistä pikavalikosta. Voit käyttää Vaihto+Insert."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "Selaimesi muistaa käyttöoikeustasosi istuntojen ajan."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Palvelimesi sulkee yhteyden pian. Voit muodostaa yhteyden uudelleen, kun "
+"palvelin on käynnistynyt uudelleen."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Istuntosi on päätetty."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Istuntosi on vahventunut. Ole hyvä ja kirjaudu uudelleen sisään."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Alue"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[binääridata]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[ei dataa]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "pääsy"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "aktiivinen"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "lisäosa"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "lisäosa"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "verkon jälkeen"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "sovellukset"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "sisältötunniste"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "kello"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "keskiarvo: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "taustalaite VDO-laitteelle"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "sidos"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "käynnistys"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "silta"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "Muut laitteet"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "Muut laitteet"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs subvolume"
+msgstr "Varastointivarannon taltio"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Varastoinnin taltiot"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "Varastointivarannon taltio"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "viankorjaus"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "tavua"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "Välimuisti"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "c-ryhmät"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "vaihda"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman ei ole asennettu"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "komento"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "konsoli"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "ytimen tyhjennys"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "suoritin"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "kaatuminen"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "$0 tiedot"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "päivämäärä"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Deaktivoi"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "virheenjäljitys"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "Poista"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "poista käytöstä"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "levy"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "levyt"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "toimialue"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "asema"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "muokkaa"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "ota käyttöön"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted"
+msgid "encrypted"
+msgstr "Salattu"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "salaus"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "parannus"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "virhe"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "perjantaisin"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "maanantaisin"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "lauantaisin"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "sunnuntaisin"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "torstaisin"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "tiistaisin"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "keskiviikkoisin"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "päivittäin"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "laajennus"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "ssh-koneavainten luettelointi epäonnistui: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "tiedostojärjestelmä"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "palomuuri"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "nykyisille ja tuleville ytimille"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "vain nykyiselle kernelille"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "alusta"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "$0:sta"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "sijainnista $0 päätteellä $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "kaaviot"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "kasvata"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "laitteisto"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "historia"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "kone"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "iSCSI-kohde"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "iSCSI-kohteet"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "Lisää iSCSI-portaali"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "jätä virhe huomiotta"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "useimmissa selaimissa"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "epäkonsistentti"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "alusta"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "asenna"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "liitäntä"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "virheellinen: useita kohteita määritelty"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "päiväkirja"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "journalctl:n man-sivu"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "kdump-tila"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "avaimet"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "sisäänkirjautuminen"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "kone"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "peite"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "maksimi: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "member of RAID device"
+msgid "member of MDRAID device"
+msgstr "RAID-laitteen jäsenenä"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "Stratis-varannon jäsen"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "muisti"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "mittarit"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "lievennys"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "lisätietoja"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "liitos"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "verkko"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "Älä koskaan liitä käynnistyksen yhteydessä"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "nfs export on tyhjä"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "nfs-palvelin on tyhjä"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "nfs-palvelin ei ole kelvollinen IPv6"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "nice"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "Ei mitään"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "$0 CPU:sta"
+msgstr[1] "$0 CPU:ista"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "päätteellä $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "käyttöjärjestelmä"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "käyttöjärjestelmä"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "paketti"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "osio"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "salasana"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "salasanan laatu"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "paikkauksia"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "polku"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "suorituskyky"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "LVM2-taltioryhmän fyysinen taltio"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "kytkettävä"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service on epäonnistunut"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service ei onnistu tietojen keräämisessä"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service ei ole käynnissä"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "pod"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "portti"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "virta"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram-muisti"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "luku-vain"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "suositeltu"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "poista LVM2:sta"
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "remove from RAID"
+msgid "remove from MDRAID"
+msgstr "poista RAID:ista"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "poista LVM2:sta"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "käynnistä uudelleen"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "roolit"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "käyttöjärjestelmänä $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "turvallisuus"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "sarja"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "palvelu"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "komentotulkki"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "näytä vähemmän"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "näytä enemmän"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "kutista"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "sulje"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "pistoke"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "ratkaisu"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "ratkaisun yksityiskohdat"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "sos-raportointi epäonnistui"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ssh-avain ei ole polku"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "SSH-palvelin on tyhjä"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "pysäytä"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "pysäyttä käynnistyksen epäonnistuessa"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+#, fuzzy
+#| msgid "Stopped"
+msgid "stopped"
+msgstr "Pysäytetty"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "kohde"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "joukkue"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "aika"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "ajastin"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "yksikkö"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "tuntematon"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "tuntematon kohde"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "poista peite"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "irrota"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "osioimaton tila levyllä $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "käyttäjä"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "käyttäjänimi"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#, fuzzy
+#| msgid "No description"
+msgid "using key description $0"
+msgstr "Ei kuvausta"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "versio"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "taltio"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "varoitus"
+
+#: pkg/networkmanager/wireguard.jsx:84
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "wireguard-tools package is not installed"
+msgstr "PackageKit ei ole asennettu"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "kyllä"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "vyöhyke"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Kdump-palvelua ei ole asennettu. Varmista, että paketti kexec-tools on "
+#~ "asennettu."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Muistia ei ole varattu. Liitä valitsin ”crashkernel” ytimen "
+#~ "komentoriville (esim. /etc/default/grub) varaamaan muistia "
+#~ "käynnistyshetkellä. Esimerkki: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Palvelu on käynnissä"
+
+#~ msgid "Service is starting"
+#~ msgstr "Palvelu käynnistyy"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Palvelu on pysäytetty"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Palvelu pysähtyy"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Tämä testaa kdump-kokoonpanon kaatamalla ytimen."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "crashkernel ei ole määritetty ytimen komentorivillä"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (salattu)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 Stratisvaranto"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 välimuisti"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 tuntemattomasta tasosta"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 vapaana"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "Varalevy pitää lisätä ennen kuin tämä levy voidaan poistaa."
+
+#~ msgid "Backing device"
+#~ msgstr "Taustalaite"
+
+#~ msgid "Block"
+#~ msgstr "Lohko"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Koko"
+
+#~ msgid "Content"
+#~ msgstr "Sisältö"
+
+#~ msgid "Create devices"
+#~ msgstr "Luo laitteet"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Laite"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Laitetiedosto"
+
+#~ msgid "Devices"
+#~ msgstr "Laitteet"
+
+#~ msgid "Drives"
+#~ msgstr "Asemat"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Salattujen taltioiden lukitus tulee olla avattu, ennen kuin niiden kokoa "
+#~ "voidaan muuttaa."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Tiedostojärjestelmä (salattu)"
+
+#~ msgid "Filesystems"
+#~ msgstr "Tiedostojärjestelmät"
+
+#~ msgid "Free"
+#~ msgstr "Vapaa"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Vapauta tilaa tässä ryhmässä: Kutista tai poista muita loogisia taltioita "
+#~ "tai lisää uusi fyysinen taltio."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Epäaktiivinen taltio"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Avainpalvelin"
+
+#~ msgid "LVM2 member"
+#~ msgstr "LVM2 jäsen"
+
+#~ msgid "Locked devices"
+#~ msgstr "Lukitut laitteet"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Lukitut salatut tiedot"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Malli"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS-liitokset"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "NFS-tuki ei ole asennettu"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Yhtään NFS-liitosta ei ole asetettu"
+
+#~ msgid "No devices"
+#~ msgstr "Ei laiteita"
+
+#~ msgid "No drives attached"
+#~ msgstr "Ei liitettyjä asemia"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "ISCSI-kohteita ei ole asetettu"
+
+#, fuzzy
+#~| msgid "Block device for filesystems"
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Lohkolaite tiedostojärjestelmille"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Muut tiedot"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Osioitu lohkolaite"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Tunnuslause"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "Fyysisten taltioiden kokoja ei voida muuttaa tässä."
+
+#~ msgid "Pool"
+#~ msgstr "Varanto"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Varanto ohuita taltioita varten"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID-taso"
+
+#~ msgid "RAID member"
+#~ msgstr "RAID-jäsen"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Irrotettava asema"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Näytä $0 laite"
+#~ msgstr[1] "Näytä kaikki $0 laitetta"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Näytä $0 asema"
+#~ msgstr[1] "Näytä kaikki $0 asemaa"
+
+#~ msgid "Show all"
+#~ msgstr "Näytä kaikki"
+
+#~ msgid "Source"
+#~ msgstr "Lähde"
+
+#~ msgid "Start pool"
+#~ msgstr "Käynnistä varanto"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Käynnistä varanto nähdäksesi tiedostojärjestelmät."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Tila"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Pysäytetty Stratis-varanto"
+
+#~ msgid "Stratis member"
+#~ msgstr "Stratis-jäsen"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Stratis-varanto $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "Tuki on asennettu."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr "RAID-laitteen on oltava käynnissä voidakseen lisätä varalevyjä."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "RAID-laitteen viimeistä levyä ei voida poistaa."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "Taltioryhmän viimeistä fyysistä taltiota ei voida poistaa."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Muualla ei ole tarpeeksi vapaata tilaa tämän fyysisen taltion "
+#~ "poistamiseksi. Tarvitaan vähintään $0 lisää vapaata tilaa."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "Tätä levyä ei voida poistaa laitteen palautumisen aikana."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr "Tämä taltio tulee aktivoida ennen kuin sen kokoa voi muuttaa."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Tunnistamattomat datat"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Käyttö"
+
+#~ msgid "Used for"
+#~ msgstr "Käytetään"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "VDO-tausta"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO-laite"
+
+#~ msgid "Volume"
+#~ msgstr "Taltio"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automaattinen (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Hyväksy avain ja muodosta yhteys"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Salattujen taltioiden kokoa ei voi muuttaa täällä."
+
+#, fuzzy
+#~| msgid "Tang keyserver"
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Tang-avainpalvelin"
+
+#, fuzzy
+#~| msgid "New passphrase"
+#~ msgid "Use a passphrase"
+#~ msgstr "Uusi tunnuslause"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitmap"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Tätä varantoa ei voi avata täällä, koska sen avaimen kuvaus ei ole "
+#~ "odotetussa muodossa."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Vaihda bitmap"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Varmista, että Tang-palvelimen avaimen hajautus vastaa yhtä seuraavista:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Tarkista manuaalisesti SSH:lla: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Portin numero ja tyyppi eivät täsmää"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Lukittu ja salattu Stratisvaranto"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Virhe poistaessa hälytystä: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Ei voitu hakea virhettä: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 tavua binääridataa]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Levyn I/O -piikki"
+
+#~ msgid "Event"
+#~ msgstr "Tapahtuma"
+
+#~ msgid "Event logs"
+#~ msgstr "Tapahtumalokeja"
+
+#~ msgid "Load spike"
+#~ msgstr "Kuormapiikki"
+
+#~ msgid "Memory spike"
+#~ msgstr "Muistipiikki"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Verkon I/O -piikki"
+
+#~ msgid "ssh key"
+#~ msgstr "ssh-avain"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Jos tämä vaihtoehto on valittuna, tiedostojärjestelmää ei liitetä "
+#~ "seuraavan käynnistyksen aikana, vaikka se olisi ollut liitettynä aiemmin. "
+#~ "Tästä on hyötyä, jos liittäminen käynnistyksen aikana ei ole mahdollista, "
+#~ "esimerkiksi silloin, kun salasana tarvitaan tiedostojärjestelmän "
+#~ "lukituksen avaamiseen, mutta käynnistys on ilman valvontaa."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Älä koskaan liitä käynnistyksen yhteydessä"
+
+#~ msgid "Listing unit files"
+#~ msgstr "Luetteloidaan yksikkötiedostot"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "Yksikkötiedostojen luettelointi epäonnistui: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "Yksikköä ei löytynyt"
+
+#~ msgid "Validating key"
+#~ msgstr "Vahvistetaan avainta"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "Poista $0 ryhmä"
+
+#~ msgid "Container administrator"
+#~ msgstr "Kontin ylläpitäjä"
+
+#~ msgid "Image builder"
+#~ msgstr "Kuvanrakentaja"
+
+#~ msgid "Roles"
+#~ msgstr "Roolit"
+
+#~ msgid "Server administrator"
+#~ msgstr "Palvelimen ylläpitäjä"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Unix-ryhmä: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Kopioitu onnistuneesti leikepöydälle"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Diagnostiikkaraportit"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Ytimen vedos"
+
+#~ msgid "Software Updates"
+#~ msgstr "Ohjelmistopäivitykset"
+
+#~ msgid "Update log"
+#~ msgstr "Päivitysloki"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "nfs dump -kohdetta ei ole muotoiltu tapaan 'palvelin:polku'"
+
+#~ msgid "$0 Zone"
+#~ msgstr "$0 vyöhyke"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Luo diagnostiikkaraportti"
+
+#~ msgid "Done!"
+#~ msgstr "Valmis!"
+
+#~ msgid "Download report"
+#~ msgstr "Lataa raportti"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Arkistoa ei ole luotu."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Vahvista kohteen $0 poistaminen"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "Luotu arkisto sisältää arkaluonteisina pidettyjä tietoja, ja alkuperäisen "
+#~ "organisaation tulisi tarkistaa sen sisältö ennen sen välittämistä "
+#~ "kolmannelle osapuolelle."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Tämä työkalu kerää järjestelmän kokoonpano- ja vianmääritystietoja "
+#~ "järjestelmän ongelmien diagnosoimiseksi."
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "Palvelimelta puuttuu cockpit-system-paketti"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Tämä paketti ei ole yhteensopiva tämän Cockpitin version kanssa"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Tämä paketti vaatii Cockpitin version %s tai uudemman"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Suorituskykymittarit"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "luku-vain"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 Git yhteensä"
+
+#~ msgid "Apply"
+#~ msgstr "Toteuta"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Ei oikeutta poistaa vyöhykettä $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Napsauta $0 uudelleen käyttääksesi salasanaa joka tapauksessa."
+
+#~ msgid "Access"
+#~ msgstr "Pääsy"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "LEVY ON PETTÄMÄSSÄ"
+
+#~ msgid "Logical Size"
+#~ msgstr "Looginen koko"
+
+#~ msgid "Security updates "
+#~ msgstr "Tietoturvapäivityksiä "
+
+#~ msgid "Updates "
+#~ msgstr "Päivitykset "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Valinnainen)"
+
+#~ msgid "Active since"
+#~ msgstr "Aktiivinen lähtien"
+
+#~ msgid "Affected locations"
+#~ msgstr "Koskettaa paikkoja"
+
+#~ msgid "Process"
+#~ msgstr "Prosessi"
+
+#~ msgid "Remove and delete"
+#~ msgstr "Poista ja tyhjennä"
+
+#~ msgid "Remove and format"
+#~ msgstr "Poista ja alusta"
+
+#~ msgid "Remove and grow"
+#~ msgstr "Poista ja kasvata"
+
+#~ msgid "Remove and initialize"
+#~ msgstr "Poista ja alusta"
+
+#~ msgid "Remove and shrink"
+#~ msgstr "Poista ja kutista"
+
+#~ msgid "Remove and stop device"
+#~ msgstr "Poista ja pysäytä laite"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Tiedostojärjestelmää käyttävät sisäänkirjautumisistunnot ja "
+#~ "järjestelmäpalvelut. Jatkaminen lopettaa nämä."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Tiedostojärjestelmää käyttävät sisäänkirjautumisistunnot. Jatkaminen "
+#~ "lopettaa nämä."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Tiedostojärjestelmää käyttävät järjestelmäpalvelut. Jatkaminen lopettaa "
+#~ "nämä."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Tällä laitteella on tällä hetkellä käytössä tiedostojärjestelmiä. "
+#~ "Jatkaminen irrottaa kaikki sen tiedostojärjestelmät."
+
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Tätä laitetta käytetään parhaillaan LVM2 taltioryhmille."
+
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Tätä laitetta käytetään tällä hetkellä LVM2 taltioryhmiä varten. "
+#~ "Jatkaminen poistaa sen taltioryhmistään."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Tätä laitetta käytetään parhaillaan RAID-laitteille."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Tätä laitetta käytetään tällä hetkellä RAID-laitteisiin. Jatkaminen "
+#~ "poistaa sen RAID-laitteistaan."
+
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Tätä laitetta käytetään parhaillaan Stratisvarannoille."
+
+#~ msgid "Unmount and delete"
+#~ msgstr "Irrota ja poista"
+
+#~ msgid "Unmount and format"
+#~ msgstr "Irrota ja alusta"
+
+#~ msgid "Unmount and grow"
+#~ msgstr "Irrota ja kasvata"
+
+#~ msgid "Unmount and initialize"
+#~ msgstr "Irrota ja alusta"
+
+#~ msgid "Unmount and shrink"
+#~ msgstr "Irrota ja kutista"
+
+#~ msgid "Unmount and stop device"
+#~ msgstr "Irrota ja pysäytä laite"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0/$1"
+
+#~ msgid "Blockdev"
+#~ msgstr "Lohkolaite"
+
+#~ msgid "Blockdev of Stratis pool $0"
+#~ msgstr "Stratisvarannon $0 lohkolaite"
+
+#~ msgid "Blockdev of locked Stratis pool $0"
+#~ msgstr "Lukitun stratisvarannon $0 lohkolaite"
+
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "$0:n LVM2 fyysinen taltio"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "RAID-laitteen $0 jäsenenä"
+
+#~ msgid "Menu"
+#~ msgstr "Valikko"
+
+#~ msgid "VDO backing"
+#~ msgstr "VDO-varmuus"
+
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Luo stratisvaranto"
+
+#~ msgid "Mount Options"
+#~ msgstr "Liitosvalinnat"
+
+#~ msgid "Stratis Pool"
+#~ msgstr "Stratisvaranto"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Levy vaaditaan."
+
+#~ msgid "Disk"
+#~ msgstr "Levy"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Vain vanhoja sovelluksia varten. Vähentää suorituskykyä."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Asenna VDO-tuki"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Käytä 512-tavuemulointia"
+
+#~ msgid "System services"
+#~ msgstr "Järjestelmäpalvelut"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Luo diagnostiikkaraportti "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr ""
+#~ "Samanaikainen yhdistäminen useampaan kuin {{ limit }} koneeseen ei ole "
+#~ "tuettu."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "Kerberos-pohjainen SSO"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Kirjaudu kohteeseen {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Uudet avainsalasanat eivät täsmää"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "Vahvistaaksesi sormenjäljen, suorita seuraava {{host}}:lla istuessasi "
+#~ "fyysisesti koneen ääressä tai luotettavan verkon kautta:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "{{#strong}}{{host}}{{/strong}}:een ei saa yhteyttä."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}}:hen ei voi kirjautua. Lisää "
+#~ "todennusasetuksia ja vianmääritystukea päivittämällä cockpit-ws uudempaan "
+#~ "versioon."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}}:hen ei voi kirjautua. Yhteyden "
+#~ "muodostamiseksi tähän isäntään sinun on otettava käyttöön yksi "
+#~ "seuraavista todennusmenetelmistä {{#strong}}{{host}}{{/strong}}:n sshd-"
+#~ "määrityksessä:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "Yhdistät {{host}}:een ensimmäistä kertaa."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "{{host}} avain vaihdettu"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Tyhjennä liitoskohdan kokoonpano"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Tämän sidoksen luominen katkaisee yhteyden palvelimeen ja tekee hallinnon "
+#~ "käyttöliittymän ei saataville."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Tämän sillan luominen katkaisee yhteyden serverille, jolloin "
+#~ "hallintakäyttöliittymä ei ole saatavilla."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Tämän työryhmän luominen katkaisee yhteyden palvelimeen, jolloin "
+#~ "hallintakäyttöliittymä ei ole saatavilla."
+
+#~ msgid "IP settings"
+#~ msgstr "IP-asetukset"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Kytkeminen pois päältä <b>$0</b> katkaisee yhteyden palvelimeen, jolloin "
+#~ "hallintakäyttöliittymä ei ole saatavilla."
+
+#~ msgid "Administrator password"
+#~ msgstr "Ylläpidon salasana"
+
+#~ msgid "Computer OU"
+#~ msgstr "Tietokone-OU"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "RAID-laitteen poistaminen tuhoaa kaiken sillä olevan datan."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "Taltion poistaminen tuhoaa kaiken sillä olevan datan."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Älä ylikirjoita olemassa olevaa dataa"
+
+#~ msgid "Erase"
+#~ msgstr "Poisto"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Alusta levy $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr "Levylaitteen alustaminen tuhoaa kaiken sillä olevan datan."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Isäntänimeä ei tule muuttaa toimialueessa"
+
+#~ msgid "More"
+#~ msgstr "Lisää"
+
+#~ msgid "Not joined"
+#~ msgstr "Ei liittynyt"
+
+#~ msgid "One time password"
+#~ msgstr "Kertakäyttöinen salasana"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 $1:sta $2:lla"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Tunnin on oltava luku välillä 0 ja 23"
+
+#~ msgid "Last login:"
+#~ msgstr "Edellinen kirjautuminen:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Minuuttien on oltava luku välillä 0 ja 59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "Edellisen onnistuneen kirjautumisen jälkeen kirjautuminen on "
+#~ "epäonnistunut $0 kertaa."
+#~ msgstr[1] ""
+#~ "Edellisen onnistuneen kirjautumisen jälkeen kirjautuminen on "
+#~ "epäonnistunut $0 kertaa."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "esim. \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 aktiiviset vyöhykkeet"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 tapahtuma"
+#~ msgstr[1] "$0 tapahtumaa"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Lisensoitu:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Avaa käynnistyksen yhteydessä"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Avaa luku-vain:n lukitus"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Hae lokista termien yhdistelmällä:"
+
+#~ msgid "Show filters"
+#~ msgstr "Näytä suodattimet"
+
+#~ msgid "Text"
+#~ msgstr "Teksti"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "mikä tahansa vapaamuotoinen merkkijono säännöllisenä lausekkeena"
+
+#~ msgid "e.g."
+#~ msgstr "esim."
+
+#~ msgid "log fields"
+#~ msgstr "lokikentät"
+
+#~ msgid "qualifiers"
+#~ msgstr "karsinnat"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "Vaihda istunnon asetuksia"
+
+#~ msgid "(none)"
+#~ msgstr "(ei mitään)"
+
+#~ msgid "Create timers"
+#~ msgstr "Luo ajastimet"
+
+#~ msgid "Recommended default"
+#~ msgstr "Suositeltu oletus"
+
+#~ msgid "Run"
+#~ msgstr "Suorita"
+
+#~ msgid "Select unit state"
+#~ msgstr "Valitse uusi tila"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Ota tallennetut mittarit käyttöön"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Ota tallennetut mittarit käyttöön"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Pakota salalauseen poisto $0:ssa"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Jos tang-show-keys eivät ole käytettävissä, suorita seuraava:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Entä jos tang-show-keys ei ole käytettävissä?"
+
+#~ msgid "key slot $0"
+#~ msgstr "avainaukko $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Jotain meni pieleen"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "Tämä ei toiminut, yritä uudelleen"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "Et voi hankkia järjestelmänvalvojan käyttöoikeuksia."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Automaattiset päivitykset eivät ole määritetty"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "$0 CPU :n kokoonpano"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 verkko"
+#~ msgstr[1] "$0 verkkoa"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 on saatavilla useimmille käyttöjärjestelmille. Asentaaksesi sen, hae "
+#~ "sitä GNOME Softwaresta, tai aja seuraava komento:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 muistin säätö"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 verkko"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPU"
+#~ msgstr[1] "$0 vCPU:ta"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 vCPU:n yksityiskohdat"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 virtuaalisen verkkoliitännän asetukset"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "Aktivoi varastointivaranto taltioiden hallitsemiseksi"
+
+#~ msgid "Add network interface"
+#~ msgstr "Lisää verkkoliitäntä"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Lisää virtuaalinen verkkoliitäntä"
+
+#~ msgid "Additional"
+#~ msgstr "Ylimääräinen"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Osoite ei aliverkossa"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr ""
+#~ "Tilannevedoksen poistamisen jälkeen kaikki siepattu sisältö menetetään."
+
+#~ msgid "Always attach"
+#~ msgstr "Kiinnitä aina"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Sen liittäminen tekee levystä jaettavan jokaiselle sitä käyttävälle "
+#~ "virtuaalikoneelle."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Käynnistä libvirt automaattisesti käynnistyksen yhteydessä"
+
+#~ msgid "Autostart"
+#~ msgstr "Automaattikäynnistys"
+
+#~ msgid "Boot order"
+#~ msgstr "Käynnistysjärjestys"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Käynnistysjärjestysasetuksia ei voitu tallentaa"
+
+#~ msgid "Bus"
+#~ msgstr "Väylä"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "CD/DVD-levy"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "CPU:n kokoonpanoa ei voitu tallentaa"
+
+#~ msgid "CPU type"
+#~ msgstr "CPU-tyyppi"
+
+#~ msgid "Change firmware"
+#~ msgstr "Vaihda laiteohjelmisto"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Muutokset tulevat voimaan virtuaalikoneen sammuttamisen jälkeen"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Valitse käyttöjärjestelmä"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "Painamalla \"Käynnistä etäkatseluohjelma\" lataat .vv-tiedoston ja "
+#~ "käynnistät $0."
+
+#~ msgid "Clone"
+#~ msgstr "Kloonaa"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Vahvista tämä toimi"
+
+#~ msgid "Connect"
+#~ msgstr "Yhdistä"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "Yhdistä mihin tahansa katseluohjelmaan seuraaviin protokolliin"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Yhdistetään virtualisointipalveluun"
+
+#~ msgid "Connection"
+#~ msgstr "Yhteys"
+
+#~ msgid "Console"
+#~ msgstr "Konsoli"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Ytimiä per kanta"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Tilannevedoksen palauttaminen epäonnistui"
+
+#~ msgid "Crashed"
+#~ msgstr "Kaatui"
+
+#~ msgid "Create VM"
+#~ msgstr "Luo VM"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Luo $0:een perustuva klooninen virtuaalikone"
+
+#~ msgid "Create new"
+#~ msgstr "Luo uusi"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Luo uusi virtuaalikone"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Luo virtuaaliverkko"
+
+#~ msgid "Creating VM"
+#~ msgstr "Virtuaalikoneen luominen"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "Luodaan virtuaalikoneen asennus"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Virtuaalikoneen $0 luominen epäonnistui"
+
+#~ msgid "Creation time"
+#~ msgstr "Luomisaika"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Nykyinen kohdentaminen"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Mukautettu laiteohjelmisto: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP-alue"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Poista liittyvät tallennustiedostot:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Poista varastointivaranto $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Poista tämän varannon sisäiset taltiot"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Passiivisen varastointivarannon poistaminen poistaa vain määrittelyn "
+#~ "varannosta. Sen sisältöä ei poisteta."
+
+#~ msgid "Desktop viewer"
+#~ msgstr "Työpöydän katseluohjelma"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Irrota tätä varantoa käyttävät levyt kaikista virtuaalikoneista ennen "
+#~ "poistoyritystä."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Irrotettu sarjakonsolista. Napsauta yhdistä-painiketta."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Levyä $0 ei voida irrottaa virtuaalikoneesta $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Levyn liittäminen epäonnistui"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Levyn luominen epäonnistui"
+
+#~ msgid "Disk image file"
+#~ msgstr "Levytiedostotiedosto"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Levyn asetuksia ei voitu tallentaa"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "Vain levy -tilannevedos"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "Toimialue on kaatunut"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "Toimialue on estetty resurssilla"
+
+#~ msgid "Download an OS"
+#~ msgstr "Lataa käyttöjärjestelmä"
+
+#~ msgid "Dying"
+#~ msgstr "Kuolemassa"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Sulautettu kone"
+
+#~ msgid "End"
+#~ msgstr "Loppu"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Loppu ei saa olla tyhjä"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Poistutaan levykuvasta isännän tiedostojärjestelmässä"
+
+#~ msgid "Expand"
+#~ msgstr "Laajenna"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Laiteohjelmiston vaihtaminen epäonnistui"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "$0:ssa olevien liitäntöjen IP-osoitteiden noutaminen epäonnistui"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr ""
+#~ "Näppäimen Ctrl+Alt+$0 lähettäminen virtuaalikoneeseen $1 epäonnistui"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr ""
+#~ "Alle virtuaalisten suorittimien enimmäismäärän tulisi olla käytössä."
+
+#~ msgid "File"
+#~ msgstr "Tiedosto"
+
+#~ msgid "Filter by name"
+#~ msgstr "Suodata nimen mukaan"
+
+#~ msgid "Firmware"
+#~ msgstr "Laiteohjelmisto"
+
+#~ msgid "Force shut down"
+#~ msgstr "Pakota sammutus"
+
+#~ msgid "Forward mode"
+#~ msgstr "Eteenpäin-tila"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Edelleenlähetystila"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Luo automaattisesti"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "Siirry virtuaalikoneiden luetteloon"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Piilota lisävaihtoehdot"
+
+#~ msgid "Host device"
+#~ msgstr "Isäntälaite"
+
+#~ msgid "Host name"
+#~ msgstr "Isäntänimi"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Nimen ei saa olla tyhjä"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Hypervisorin yksityiskohdat"
+
+#~ msgid "IP configuration"
+#~ msgstr "IP-määritykset"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 ja IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4-verkko"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv4-verkko ei tulisi olla tyhjä"
+
+#~ msgid "IPv4 only"
+#~ msgstr "IPv4 vain"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6-osoite"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6-verkko"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6-verkko ei tulisi olla tyhjä"
+
+#~ msgid "IPv6 only"
+#~ msgstr "IPv6 vain"
+
+#~ msgid "Idle"
+#~ msgstr "Jouten"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Käynnistä virtuaalikone välittömästi"
+
+#~ msgid "Import"
+#~ msgstr "Tuo"
+
+#~ msgid "Import VM"
+#~ msgstr "Tuo virtuaalikone"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "Tuo virtuaalikone"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "Useimmissa kokoonpanoissa macvtap ei toimi isäntä-vieras-verkkoyhteydessä."
+
+#~ msgid "Initiator"
+#~ msgstr "Aloittaja"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "Aloittaja IQN ei saa olla tyhjä"
+
+#~ msgid "Installation source"
+#~ msgstr "Asennuslähde"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Asennuslähteen ei tulisi olla tyhjä"
+
+#~ msgid "Installation type"
+#~ msgstr "Asennustyyppi"
+
+#~ msgid "Interface type"
+#~ msgstr "Liitäntätyyppi"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Virheellinen IPv4-peitteen tai etuliitteen pituus"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Ei kelvollinen IPv6-osoite"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Virheellinen IPv6-etuliite"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Virheellinen tiedostonimi"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "Käynnistä etäkatselin"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr "Jätä salasana tyhjäksi, jos et halua root:n tilin luomista"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr "Jätä salasana tyhjäksi, jos et halua käyttäjätilin luomista"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr "Jätä salasana tyhjäksi, jos et halua root:n tilin luomista"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt ei havainnut isäntään asennettuja UEFI/OVMF-laiteohjelmistokuvia"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt tai hypervisori ei tue UEFI:a"
+
+#~ msgid "Loading resources"
+#~ msgstr "Ladataan resursseja"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "Kone on sammutettava ennen väylätyypin vaihtamista"
+
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "Kone on sammutettava ennen välimuistin tilan vaihtamista"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Virtuaalikoneiden hallinta"
+
+#~ msgid "Manual connection"
+#~ msgstr "Manuaalinen yhteys"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Peitteen tai etuliitteen pituus"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Peitteen tai etuliitteen pituus ei tulisi olla tyhjä"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Enimmäisvaraus"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "Enimmäismuistia ei voitu tallentaa"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Vieraskäyttöjärjestelmälle varattu virtuaalisten suorittimien "
+#~ "enimmäismäärä"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Vieraskäyttöjärjestelmälle varattu virtuaalisten suorittimien "
+#~ "enimmäismäärä, jonka on oltava välillä 1 ja $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Enimmäislähetysyksikkö"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Muistia ei voitu tallentaa"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "Muisti ei saa olla 0"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Mallityyppi"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT $0:een"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "Virtuaalikoneen $1 verkkokortti $0 ei muuttanut tilaa"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Nimi sisältää laittomia merkkejä"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Nimi ei saa olla tyhjä"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Nime ei tulisi olla tyhjä"
+
+#~ msgid "Name: "
+#~ msgstr "Nimi: "
+
+#~ msgid "Netmask"
+#~ msgstr "Verkkopeite"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Verkkoa $0 ei voitu aktivoida"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Verkkoa $0 ei voitu poistaa käytöstä"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Verkkokäynnistys (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Verkkotiedostojärjestelmä"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Verkkoliitännän asetuksia ei voitu tallentaa"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "Verkon valinta ei tue PXE:ta."
+
+#~ msgid "Networks"
+#~ msgstr "Verkot"
+
+#~ msgid "New volume name"
+#~ msgstr "Uusi taltion nimi"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr ""
+#~ "Mikään virtuaalikone ei ole käynnissä tai määritelty tässä isännässä"
+
+#~ msgid "No connection available"
+#~ msgstr "Yhteyksia ei ole saatavilla"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Tälle virtuaalikoneelle ei ole määritetty levyjä"
+
+#~ msgid "No network devices"
+#~ msgstr "Ei verkkolaitteita"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Tälle virtuaalikoneelle ei ole määritetty verkkoliitäntöjä"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "Tälle isännälle ei ole määritetty verkkoa"
+
+#~ msgid "No networks available"
+#~ msgstr "Verkkoja ei ole käytettävissä"
+
+#~ msgid "No parent"
+#~ msgstr "Ei vanhempi"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Tälle virtuaalikoneelle ei ole määritetty tilannevedoksia"
+
+#~ msgid "No state"
+#~ msgstr "Ei tilaa"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "Tälle isännälle ei ole määritetty varastointivarantoa"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Ei varastointivarantoja saatavilla"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "Tälle varastointivarannolle ei ole määritetty varastointitaltioita"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Ei virtuaaliverkkoja"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "Ei-pysyvää verkkoa ei voida poistaa. Se lakkaa olemasta, kun se "
+#~ "deaktivoidaan."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "Ei-pysyvää varastointivarantoa ei voida poistaa. Se lakkaa olemasta, kun "
+#~ "se deaktivoidaan."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Ei-eristetty verkko"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Toimialueet käyttävät yhtä tai useampaa valittua taltiota. Irrota levyt "
+#~ "ensin, jotta taltio voidaan poistaa."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Muokattavissa vain, kun vieras on sammutettuna"
+
+#~ msgid "Open"
+#~ msgstr "Avaa"
+
+#~ msgid "Operating system"
+#~ msgstr "Käyttöjärjestelmä"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "Toiminto käynnissä"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "Vanhempi-tilannevedos"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Polku isännän tiedostojärjestelmässä"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Polku ISO-tiedostoon isännän tiedostojärjestelmässä"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Polku tiedostoon isännän tiedostojärjestelmässä"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Polku tiedostoon isännän tiedostojärjestelmässä"
+
+#~ msgid "Paused"
+#~ msgstr "Keskeytetty"
+
+#~ msgid "Persistence"
+#~ msgstr "Pysyvyys"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Fyysinen levylaite"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Fyysinen levylaite isännässä"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Valitse tallennusvaranto"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "Valitse taltio"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Anna uusi taltion nimi"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Käynnistä virtuaalikone päästäksesi sen konsoliin."
+
+#~ msgid "Plug"
+#~ msgstr "Kytke"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "Varannon on oltava aktiivinen taltion luomiseksi"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "Varannon tyyppi ei tue taltion luomista"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "Varannon taltioita käyttävät virtuaalikoneet "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Vieraalle altistettavaksi haluttu pistokkeiden määrä ."
+
+#~ msgid "Prefix"
+#~ msgstr "Etuliite"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "Etuliitteen pituuss ei tulisi olla tyhjä"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "Aiemmin otettujen tilannevedoksien avulla voit palata aikaisempaan "
+#~ "tilaan, jos jokin menee pieleen"
+
+#~ msgid "Product"
+#~ msgstr "Tuote"
+
+#~ msgid "Profile"
+#~ msgstr "Profiili"
+
+#~ msgid "Protocol"
+#~ msgstr "Protokolla"
+
+#~ msgid "Remote URL"
+#~ msgstr "Etä-URL"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "Etäkatseluohjelman yksityiskohdat"
+
+#~ msgid "Revert"
+#~ msgstr "Palaa"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Palaa tilannevedokseen $0"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "Palautus tähän tilannevedokseen palauttaa virtuaalikoneen tilannevedoksen "
+#~ "aikaan ja nykyinen tila häviää, samoin kuin kaikki tiedot, joita ei ole "
+#~ "siepattu tilannevedoksessa"
+
+#~ msgid "Root password"
+#~ msgstr "Root:n salasana"
+
+#~ msgid "Route to $0"
+#~ msgstr "Reitti $0:een"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Suorita asennus ilman valvontaa"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Suorita, kun isäntä käynnistyy"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE-TLS-portti"
+
+#~ msgid "SPICE address"
+#~ msgstr "SPICE-osoite"
+
+#~ msgid "SPICE port"
+#~ msgstr "SPICE-portti"
+
+#~ msgid "Select console type"
+#~ msgstr "Valitse konsolin tyyppi"
+
+#~ msgid "Send key"
+#~ msgstr "Lähetä avain"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Lähetä ei-peitettävä keskeytys"
+
+#~ msgid "Serial console"
+#~ msgstr "Sarjakonsoli"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Määritä DHCP-alue"
+
+#~ msgid "Set manually"
+#~ msgstr "Aseta käsin"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "Käyttäjän salasanojen asettaminen valvomatonta asennusta varten "
+#~ "edellyttää virtuaalikoneen käynnistämistä sen luomisen yhteydessä"
+
+#~ msgid "Show additional options"
+#~ msgstr "Näytä lisävaihtoehdot"
+
+#~ msgid "Shut off"
+#~ msgstr "Sammuta"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr ""
+#~ "Sammuta virtuaalikone, jotta voit muokata laiteohjelmiston kokoonpanoa"
+
+#~ msgid "Shutting down"
+#~ msgstr "Sammutetaan"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Tilannevedoksen luominen epäonnistui"
+
+#~ msgid "Source format"
+#~ msgstr "Lähteen muoto"
+
+#~ msgid "Source path"
+#~ msgstr "Lähteen polku"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Lähdepolku ei saa olla tyhjä"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "Lähteen tulisi alkaa http-, ftp- tai nfs-protokollalla"
+
+#~ msgid "Source volume group"
+#~ msgstr "Lähteen taltioryhmä"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Käynnistä libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Käynnistä varanto, kun isäntä käynnistyy"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Käynnistys ei tulisi olla tyhjä"
+
+#~ msgid "Startup"
+#~ msgstr "Aloittaa"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "Tallennusvarannon $0 aktivointi epäonnistui"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "Tallennusvarannon $0 aktivoinnin poisto epäonnistui"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Varastointivarannon luominen epäonnistui"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Varastointivarannon nimi"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "Tallennuskoko ei saa olla 0"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr ""
+#~ "Tallennustaltion koko ei saa ylittää tallennusvarannon kapasiteettia ($0 "
+#~ "$1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Varastointitaltioita ei voitu poistaa"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "Pysäytetty (PM)"
+
+#~ msgid "Target path"
+#~ msgstr "Kohteen polku"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Kohdepolku ei saa olla tyhjä"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr ""
+#~ "Virtuaalikone on käynnissä ja se pakotetaan pois päältä ennen poistamista."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "Virtuaalikoneen on oltava käynnissä tai sammutettuna laitteen "
+#~ "irrottamiseksi"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "Viedyn palvelimen hakemisto"
+
+#~ msgid "The pool is empty"
+#~ msgstr "Varanto on tyhjä"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr "Valittu käyttöjärjestelmä ei tue valvomatonta asennusta"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr "Valitulla käyttöjärjestelmällä on vähimmäismäärä muistia $0 $1"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "Valitun käyttöjärjestelmän tallennustilan vähimmäisvaatimus on $0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "Tallennusvarantoa ei voitu poistaa"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr ""
+#~ "Tämä virtuaalikone on ohimenevä. Sammuta se, jos haluat poistaa sen."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Tätä taltiota käyttää jo: "
+
+#~ msgid "Threads per core"
+#~ msgstr "Säikeitä per ydin"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr ""
+#~ "Ohimenevät virtuaalikoneet eivät tue laiteohjelmiston kokoonpanon "
+#~ "muokkaamista"
+
+#~ msgid "Type ID"
+#~ msgstr "Tyypin tunnus"
+
+#~ msgid "Unique name"
+#~ msgstr "Ainutlaatuinen nimi"
+
+#~ msgid "Unique network name"
+#~ msgstr "Ainutlaatuinen verkon nimi"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Tuntematon laiteohjelmisto"
+
+#~ msgid "Unplug"
+#~ msgstr "Kytke pois"
+
+#~ msgid "Up to $0 $1 available on the default location"
+#~ msgstr "Aina $0 $1 saakka on käytettävissä oletussijainnissa"
+
+#~ msgid "Up to $0 $1 available on the host"
+#~ msgstr "Aina $0 $1 saakka on käytettävissä isännässä"
+
+#~ msgid "Url"
+#~ msgstr "Url"
+
+#~ msgid "User login must not be empty when user password is set"
+#~ msgstr ""
+#~ "Käyttäjätunnus ei saa olla tyhjä, kun käyttäjän salasana on asetettu"
+
+#~ msgid "User password must not be empty when user login is set"
+#~ msgstr ""
+#~ "Käyttäjän salasana ei saa olla tyhjä, kun käyttäjätunnus on asetettu"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "VCPU:n asetuksia ei voitu tallentaa"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "Virtuaalikone $0 on jo olemassa"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "Virtuaalikone $0 ei ole olemassa yhteydellä $1"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "Virtuaalikoneen $0 uudelleenkäynnistyksen pakottaminen epäonnistui"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "Virtuaalikone $0 epäonnistui pakottamaan sammutus"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "Virtuaalikone $0 epäonnistui saamaan itse poistetuksi"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "Virtuaalikone $0 epäonnistui saamaan itse asennetuksi"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "Virtuaalikone $0 epäonnistui käynnistymään uudelleen"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "Virtuaalikone $0 epäonnistui palautumaan"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "Virtuaalikone $0 epäonnistui lähettämään NMI"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "Virtuaalikone $0 epäonnistui sammuttamaan"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "Virtuaalikone $0 epäonnistui käynnistymään"
+
+#~ msgid "VM state"
+#~ msgstr "Virtuaalikoneen tila"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC-TLS-portti"
+
+#~ msgid "VNC address"
+#~ msgstr "VNC-osoite"
+
+#~ msgid "VNC console"
+#~ msgstr "VNC-konsoli"
+
+#~ msgid "VNC port"
+#~ msgstr "VNC-portti"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Virtuaalikoneet"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Virtuaalikoneet"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Virtuaalikoneiden hallinta"
+
+#~ msgid "Virtual network"
+#~ msgstr "Virtuaaliverkko"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Virtuaaliverkon luominen epäonnistui"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Virtualisointipalvelu (libvirt) ei ole aktiivinen"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Taltioryhmän nimi ei tulisi olla tyhjä"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Kirjoitettava"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Kirjoitettava ja jaettu"
+
+#~ msgid "Yesterday"
+#~ msgstr "Eilen"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "Sinun on valittava parhaiten vastaava käyttöjärjestelmä"
+
+#~ msgid "cdrom"
+#~ msgstr "cd-rom"
+
+#~ msgid "disabled"
+#~ msgstr "pois käytöstä"
+
+#~ msgid "down"
+#~ msgstr "alas"
+
+#~ msgid "enabled"
+#~ msgstr "käytössä"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "isäntälaite"
+
+#~ msgid "host passthrough"
+#~ msgstr "isännän läpivienti"
+
+#~ msgid "hostdev"
+#~ msgstr "isäntälaite"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "iSCSI -suora kohde"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "iSCSI-aloittaja IQN"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "iSCSI-kohteen IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "monilähetys"
+
+#~ msgid "no"
+#~ msgstr "ei"
+
+#~ msgid "no state saved"
+#~ msgstr "ei tilaa tallennettuna"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "uudelleenohjattu laite"
+
+#~ msgid "server"
+#~ msgstr "palvelin"
+
+#~ msgid "up"
+#~ msgstr "ylös"
+
+#~ msgid "vCPU count"
+#~ msgstr "vCPU-lasku"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU enintään"
+
+#~ msgid "vCPUs"
+#~ msgstr "vCPU:t"
+
+#~ msgid "vhostuser"
+#~ msgstr "virtuaali-isännän käyttäjä"
+
+#~ msgid "view more..."
+#~ msgstr "Katso lisää..."
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Paketti virt-install on asennettava järjestelmään virtuaalikoneiden "
+#~ "kloonaamiseksi"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Paketti virt-install on asennettava järjestelmään uusien "
+#~ "virtuaalikoneiden luomiseksi"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Paketti virt-install on asennettava järjestelmään tämän attribuutin "
+#~ "muokkaamiseksi"
+
+#~ msgid "vm"
+#~ msgstr "vm"
+
+#~ msgid "Local install media"
+#~ msgstr "Paikallinen asennusmedia"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "Aseta root:n salasana"
+
+#~ msgid "21st"
+#~ msgstr "21."
+
+#~ msgid "22nd"
+#~ msgstr "22."
+
+#~ msgid "23rd"
+#~ msgstr "23."
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Jälkeen"
+
+#~ msgid "Friday"
+#~ msgstr "Perjantai"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Tunti : Minuutti"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "Tunti tarvitsee numeron väliltä 0-23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Virheellinen päivämuoto."
+
+#~ msgid "Invalid number."
+#~ msgstr "Virheellinen numero."
+
+#~ msgid "Monday"
+#~ msgstr "Maanantai"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Toista joka tunti"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Toista joka vuosi"
+
+#~ msgid "Saturday"
+#~ msgstr "Lauantai"
+
+#~ msgid "Sunday"
+#~ msgstr "Sunnuntai"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Tätä päivää ei ole kaikissa kuukausissa. <br> Ajastin ajetaan vain "
+#~ "kuukausissa, joissa on 31. päivä."
+
+#~ msgid "Thursday"
+#~ msgstr "Torstai"
+
+#~ msgid "Tuesday"
+#~ msgstr "Tiistai"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 päivitys"
+#~ msgstr[1] "$0 päivitystä"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 turvallisuuspäivitys"
+#~ msgstr[1] "$1 turvallisuuspäivitystä"
+
+#~ msgid "Restart now"
+#~ msgstr "Käynnistä uudelleen nyt"
+
+#~ msgid "Configure"
+#~ msgstr "Muokkaa asetuksia"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Verkko"
+
+#~ msgid "No updates pending"
+#~ msgstr "Ei odottavia päivityksiä"
+
+#~ msgid "This directory is empty"
+#~ msgstr "Tämä hakemisto on tyhjä"
+
+#~ msgid "undefined"
+#~ msgstr "määrittämätön"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Väärä Host Avain"
+
+#~ msgid "Address:"
+#~ msgstr "Osoite:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Vähintään $0 levyä tarvitaan."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "Yhdistä mihin tahansa SPICE- tai VNC-katseluohjelmaan."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Lataa MSI kohteesta $0"
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid "Entry"
+#~ msgstr "Merkintä"
+
+#~ msgid "Dashboard"
+#~ msgstr "Kojelauta"
+
+#~ msgid "Description input text"
+#~ msgstr "Kuvauksen syöttöteksti"
+
+#~ msgid "FAILED"
+#~ msgstr "EPÄONNISTUI"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Yhteys menetettiin. Yritetään yhdistää uudelleen"
+
+#~ msgid "Servers"
+#~ msgstr "Palvelimet"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "Olet tällä hetkellä suoraan yhteydessä tähän palvelimeen. Et voi poistaa "
+#~ "sitä."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bitti"
+#~ msgstr[1] "$0 bittiä"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 tavu"
+#~ msgstr[1] "$0 tavua"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 tavu"
+#~ msgstr[1] "$0 tavua"
+
+#, fuzzy
+#~| msgid "Bond settings"
+#~ msgid "Bond settings "
+#~ msgstr "Sidosasetukset "
+
+#, fuzzy
+#~| msgid "IP settings"
+#~ msgid "IP settings "
+#~ msgstr "IP-asetukset "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Cockpitissä tapahtui odottamaton virhe. <br/><br/>Voit yrittää käynnistää "
+#~ "Cockpitin uudelleen päivittämällä selaimesi sivun. Javascript-konsolissa "
+#~ "on lisätietoja tästä virheestä (<b>Ctrl-Shift-J</b> useimmissa "
+#~ "selaimissa)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "Virtuaalikone kaatui."
+
+#~ msgid "The VM is down."
+#~ msgstr "Virtuaalikone on alhaalla."
+
+#~ msgid "The VM is going down."
+#~ msgstr "Virtuaalikone on menossa alas."
+
+#~ msgid "The VM is idle."
+#~ msgstr "Virtuaalikone on jouten."
+
+#~ msgid "The VM is paused."
+#~ msgstr "Virtuaalikone on keskeytetty."
+
+#~ msgid "The VM is running."
+#~ msgstr "Virtuaalikone on käynnissä."
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Jätä tyhjäksi yhdistääksesi tähän koneeseen nykyisenä käyttäjänä. Jos "
+#~ "annat eri käyttäjänimen, sitä tullaan aina käyttämään yhdistettäessä "
+#~ "tähän koneeseen."
+
+#~ msgid "2 min"
+#~ msgstr "2 min"
+
+#~ msgid "3 min"
+#~ msgstr "3 min"
+
+#~ msgid "4 min"
+#~ msgstr "4 min"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "Prosessorin tila"
+
+#~ msgid "Cached"
+#~ msgstr "Välimuistissa"
+
+#~ msgid "I/O wait"
+#~ msgstr "I/O Odottaa"
+
+#~ msgid "Kernel"
+#~ msgstr "Kernel"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Muisti"
+
+#~ msgid "Network traffic"
+#~ msgstr "Verkkoliikenne"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Synkronoi käyttäjät"
+
+#~ msgid "idle"
+#~ msgstr "jouten"
+
+#~ msgid "running"
+#~ msgstr "suoritetaan"
+
+#~ msgid "shut off"
+#~ msgstr "sammutettu"
+
+#~ msgid "shutdown"
+#~ msgstr "sammuta"
+
+#~ msgid "Available"
+#~ msgstr "Saatavilla"
+
+#, fuzzy
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Anna IP-osoite tai host-nimi"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Kokeillaksesi toista porttia sinun tulee päivittää cockpit-ws uudempaan "
+#~ "versioon."
+
+#~ msgid "$0 template"
+#~ msgstr "$0 mallipohja"
+
+#~ msgid "Instantiate"
+#~ msgstr "Instansoi"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 jaot"
+
+#~ msgid "Always"
+#~ msgstr "Aina"
+
+#~ msgid "Author"
+#~ msgstr "Tekijä"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Virheellistä dataa välitetty passwd1-mekanismille"
+
+#~ msgid "CPU priority"
+#~ msgstr "Prosessorin prioriteetti"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Ei voitu yhdistää Dockeriin"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Muuta resurssin rajoja"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Muuta resurssien rajoja"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr ""
+#~ "Cockpit ei pystynyt kirjautumaan kohteeseen {{#strong}}{{host}}{{/"
+#~ "strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit ei pystynyt kirjautumaan kohteeseen {{#strong}}{{host}}{{/"
+#~ "strong}}. Voit vaihtaa todentamiseen käyttämiäsi tunnuksi alla. "
+#~ "{{#can_sync}}Haluat ehkä {{#sync_link}}synkronoida käyttäjätilit ja "
+#~ "salasanat{{/sync_link}}.{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Yhdistetty muistin käyttö"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Yhdistetty $0 CPU-ytimen käyttö"
+#~ msgstr[1] "Yhdistetty $0 CPU-ydinten käyttö"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "Komento ei voi olla tyhjä"
+
+#~ msgid "Command:"
+#~ msgstr "Komento:"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Yhdistetään Dockeriin"
+
+#~ msgid "Container name"
+#~ msgstr "Kontin nimi"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Kontti on merkitty ei käynnissä olevaksi, mutta tavallinen pysäyttäminen "
+#~ "epäonnistui."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Kontti on parhaillaan käynnissä."
+
+#~ msgid "Container:"
+#~ msgstr "Kontti:"
+
+#~ msgid "Containers"
+#~ msgstr "Kontit"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Kontit"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Ei voitu muuttaa käyttäjäryhmiä"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Ei voitu muuttaa käyttäjän salasanaa"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Ei saatu yhteyttä koneeseen"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Uusia käyttäjiä ei voitu luoda"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Paikallaisia käyttäjiä ei voitu listata"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Käyttäjiä ei voitu listata"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Käyttäjän dataa ei voitu ladata"
+
+#~ msgid "Created:"
+#~ msgstr "Luotu:"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker ei ole asennettu tai aktiivisena järjestelmässä"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Aliaksen duplikaatti"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Portin duplikaatti"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Erilaisen salasanan antaminen tässä tarkoittaa, että sinun tulee antaa "
+#~ "salasana uudestaan joka kerta kun yhdistät uudelleen tähän koneeseen."
+
+#~ msgid "Environment"
+#~ msgstr "Ympäristö"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Virhe ladattaessa käyttäjiä: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Virheviesti Dockerilta:"
+
+#~ msgid "Everything"
+#~ msgstr "Kaikki"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Poistui $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Paljasta kontin portit"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Ei voitu pysäyttää Dockerin scopea: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Hae uusi levykuva"
+
+#~ msgid "IP address:"
+#~ msgstr "IP-osoitteet:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "IP Prefix Pituus:"
+
+#~ msgid "Id"
+#~ msgstr "Id"
+
+#~ msgid "Id:"
+#~ msgstr "Id:"
+
+#~ msgid "Image"
+#~ msgstr "Levykuva"
+
+#~ msgid "Image search"
+#~ msgstr "Levykuvan haku"
+
+#~ msgid "Image:"
+#~ msgstr "Levykuva:"
+
+#~ msgid "Images"
+#~ msgstr "Levykuvat"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Levykuvat"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Levykuvat ja käynnissä olevat kontit"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Synkronoidaksesi käyttäjät, sinun tulee olla kirjautuneena kohteeseen "
+#~ "{{#strong}}{{host}}{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Virheellinen portti"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Kerberos-tiketti"
+
+#~ msgid "Link to another container"
+#~ msgstr "Yhdistä toiseen konttiin"
+
+#~ msgid "Login Password"
+#~ msgstr "Kirjautumissalasana"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC-osoite:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Muistiraja"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Liitä konttitaltiot"
+
+#~ msgid "No containers"
+#~ msgstr "Ei kontteja"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Ei nykyistä suodatusta vastaavia kontteja"
+
+#~ msgid "No running containers"
+#~ msgstr "Ei käynnissä olevia kontteja"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Ei suodatusta vastaavia käynnissä olevia kontteja"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Vahvista kohteen $0 pakotettu poistaminen"
+
+#~ msgid "Ports:"
+#~ msgstr "Portit:"
+
+#~ msgid "Problems"
+#~ msgstr "Ongelmat"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Uudelleenkäynnistyksen käytäntö:"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Aseta kontin ympäristömuuttujat"
+
+#~ msgid "Start Docker"
+#~ msgstr "Käynnistä Docker"
+
+#~ msgid "State:"
+#~ msgstr "Tila:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Synkronoi"
+
+#~ msgid "This image does not exist."
+#~ msgstr "Tätä kuvaa ei ole olemassa."
+
+#~ msgid "Type a password"
+#~ msgstr "Kirjoita salasana"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Kirjoita suodattaaksesi..."
+
+#~ msgid "Unless stopped"
+#~ msgstr "Ellei pysäytetty"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Käytetään saatavilla olevia käyttäjävaltuuksia"
+
+#~ msgid "Volumes"
+#~ msgstr "Taltiot"
+
+#~ msgid "With terminal"
+#~ msgstr "Terminaalilla"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Olet yhdistetty kohteeseen {{#strong}}{{host}}{{/strong}}, mutta "
+#~ "synkronoidaksesi käyttäjiä vaaditaan yksi käyttäjä superuser-oikeuksilla."
+
+#~ msgid "default"
+#~ msgstr "Oletus"
+
+#~ msgid "key"
+#~ msgstr "avain"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "etsi nimellä, nimiavaruudella tai kuvauksella"
+
+#~ msgid "shares"
+#~ msgstr "jaot"
+
+#~ msgid "value"
+#~ msgstr "Arvo"
+
+#, fuzzy
+#~| msgid "No containers that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Ei nykyistä suodatusta vastaavia kontteja"
+
+#~ msgid "Master"
+#~ msgstr "Master"
+
+#, fuzzy
+#~| msgid "Password"
+#~ msgid "Password for $0"
+#~ msgstr "Salasana"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Käyttäjätilit"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "Käyttäjällä <b>$0</b> ei ole oikeutta muokata tilejä"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Root-käyttäjätilin poistaminen epäonnistui"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Root-käyttäjätilin uudelleennimeäminen epäonnistui"
+
+#~ msgid "Account Settings"
+#~ msgstr "Tilin asetukset"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Lisää kone kojelaudalle"
+
+#~ msgid "Machines"
+#~ msgstr "Koneet"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "Käyttäjällä <b>$0</b> ei ole oikeutta hallita tallennustilaa"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr "Käyttäjällä <b>$0</b> ei ole oikeutta muokata verkkoasetuksia"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Edellinen liipaisin"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "Käyttäjällä <b>$0</b> ei ole oikeutta luoda ajastimia"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Lisää Cockpitistä"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (jaettu OS:n kanssa)"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Lisää uusi tallennustila"
+
+#~ msgid "Add Storage"
+#~ msgstr "Lisää tallennustila"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Kaikki valituilla levyillä oleva data poistetaan ja levyt lisätään "
+#~ "tallennuspooliin."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Aseta tallennustila..."
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Ei voitu lisätä kaikkia levyjä"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Tyhjennä kontit ja resetoi tallennusvaranto"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr "Tieto Dockering tallentovarannosta ei ole saatavilla."
+
+#~ msgid "Local Disks"
+#~ msgstr "Paikalliset levyt"
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Virtualisointipalvelu ei ole saatavilla"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "Sinulla ei ole oikeutta hallita Dockerin tallennusvarantoa."
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} -> $cport"
diff --git a/po/fr.po b/po/fr.po
new file mode 100644
index 0000000..313ff8d
--- /dev/null
+++ b/po/fr.po
@@ -0,0 +1,13412 @@
+# Jean-Baptiste Holcroft <jean-baptiste@holcroft.fr>, 2015. #zanata, 2020.
+# Jérôme Fenal <jfenal@gmail.com>, 2015. #zanata
+# Jérôme Bivia <jerome.bivia@gmail.com>, 2016. #zanata
+# Jérôme Fenal <jfenal@gmail.com>, 2016. #zanata
+# Peter Perron <peter.perron@videotron.ca>, 2017. #zanata
+# Ludek Janda <ljanda@redhat.com>, 2018. #zanata, 2020.
+# cockpit <cockpituous@gmail.com>, 2018. #zanata
+# Jean-Baptiste Holcroft <jean-baptiste@holcroft.fr>, 2019. #zanata, 2020.
+# Jérôme Fenal <jfenal@gmail.com>, 2019. #zanata
+# Ludek Janda <ljanda@redhat.com>, 2019. #zanata, 2020.
+# Matej Marusak <mmarusak@redhat.com>, 2019. #zanata, 2020.
+# corina roe <croe@redhat.com>, 2019. #zanata
+# Julien Humbert <julroy67@gmail.com>, 2020.
+# Luclu7 <me@luclu7.fr>, 2020.
+# Anonymous <noreply@weblate.org>, 2020.
+# Sébastien POHER <sogal@member.fsf.org>, 2020.
+# Côme Borsoi <fedora@borsoi.fr>, 2020.
+# Sébastien Pascal-Poher <spoher@pm.me>, 2020.
+# Olivier FRANCHET <o.franchet@dominux.fr>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-06-23 09:20+0000\n"
+"Last-Translator: Alain Nussbaumer <alain.nussbaumer@alleluia.ch>\n"
+"Language-Team: French <https://translate.fedoraproject.org/projects/cockpit/"
+"main/fr/>\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n > 1\n"
+"X-Generator: Weblate 4.18.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "# Nombre d'utilisateurs"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 Processeur"
+msgstr[1] "$0 Processeurs"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 zone active"
+msgstr[1] "$0 zones actives"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 disponible"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "$0 les systèmes de fichiers ne peuvent pas être agrandis."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "$0 les systèmes de fichiers ne peuvent pas être réduits."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "$0 les systèmes de fichiers ne peuvent pas être redimensionnés ici."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "$0 les systèmes de fichiers ne peuvent pas être redimensionnés ici."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 Taille de bloc"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 règle critique atteinte"
+msgstr[1] "$0 règles atteintes, incluant celles critiques"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 données + $1 surcharge utilisée de $2 ( $3 )"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 jour"
+msgstr[1] "$0 jours"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disque est manquant"
+msgstr[1] "$0 disques sont manquants"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disque est manquant"
+msgstr[1] "$0 disques sont manquants"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 Disques"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 documentation"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 erreur"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 quitté avec le code $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 échoué"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 échec de la tentative de connexion"
+msgstr[1] "$0 échec des tentatives de connexion"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "$0 système de fichiers"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 disponible"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 heure"
+msgstr[1] "$0 heures"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 règle importante atteinte"
+msgstr[1] "$0 règles atteintes, incluant celles importantes"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 est un fichier existant"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 est en cours d’utilisation"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 n’est disponible dans aucun référentiel."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 clé modifiée"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 killed avec un signal $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 règle peu sévère atteinte"
+msgstr[1] "$0 règles peu sévères atteintes"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minute"
+msgstr[1] "$0 minutes"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 règle modérée atteinte"
+msgstr[1] "$0 règles atteintes, incluant celles modérées"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 mois"
+msgstr[1] "$0 mois"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 plus..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 paquet"
+msgstr[1] "$0 paquets"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 le paquet a besoin d’un redémarrage du système"
+msgstr[1] "$0 les paquets doivent être redémarrés"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 page"
+msgstr[1] "$0 pages"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "partition"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 correctif de sécurité disponible"
+msgstr[1] "$0 correctifs de sécurité disponibles"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 service a échoué"
+msgstr[1] "$0 services ont échoué"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 le service doit être relancé"
+msgstr[1] "$0 les services doivent être redémarrés"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 il reste un créneau"
+msgstr[1] "$0 logements restants"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 pic"
+msgstr[1] "$0 pics"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+#, fuzzy
+#| msgid "Not synchronized"
+msgid "$0 synchronized"
+msgstr "Non synchronisé"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 total"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 mise à jour disponible"
+msgstr[1] "$0 mises à jour disponibles"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 utilisé de $1 ($2 enregistré)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 semaine"
+msgstr[1] "$0 semaines"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 sera installé."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 an"
+msgstr[1] "$0 ans"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 zone"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0 : plantage à $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (de $host )"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Création de la partition $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Point de montage local"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Non monté"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(recommandé)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", y compris $1 correctif de sécurité"
+msgstr[1] ", y compris $1 correctifs de sécurité"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 Mio"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 jour"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 heure"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 min"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 minute"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 semaine"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10ème"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11ème"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 Kio"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12ème"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13ème"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14ème"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 mns"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15ème"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 Kio"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16ème"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17ème"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18ème"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19ème"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1er"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 Mio"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 minutes"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20ème"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24ème"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25ème"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26ème"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27ème"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28ème"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29ème"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2ème"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30ème"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31ème"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 Kio"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3ème"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 Kio"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 minutes"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4ème"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 min"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minutes"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 Kio"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5ème"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 heures"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 minutes"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 Kio"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6ème"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7ème"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 Kio"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8ème"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9ème"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Une version compatible de Cockpit n’est pas installée sur $0."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "Un système de fichiers portant ce nom existe déjà dans ce pool."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "Un groupe portant ce nom existe déjà"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Un navigateur moderne est nécessaire pour la sécurité, la fiabilité et la "
+"performance."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Une agrégation réseau combine plusieurs interfaces réseau en une seule "
+"interface logique avec un débit plus élevé ou une redondance."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Une nouvelle clé SSH à $0 sera créée pour $1 sur $2 et elle sera ajoutée au "
+"fichier $3 de $4 sur $5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Un paquet nécessite un redémarrage du système pour que les mises à jour "
+"prennent effet :"
+msgstr[1] ""
+"Certains paquets nécessitent un redémarrage du système pour que les mises à "
+"jour prennent effet :"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Un pool portant ce nom existe déjà."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"Un service doit être redémarré pour que les mises à jour prennent effet :"
+msgstr[1] ""
+"Certains services doivent être redémarrés pour que les mises à jour prennent "
+"effet :"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "Les volumes physiques ne peuvent pas être redimensionnés ici."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "Surveillance ARP"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "Ping ARP"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "À propos de la console web"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Absent"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Accepter la clé et se connecter"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+#, fuzzy
+#| msgid "Set password"
+msgid "Acceptable password"
+msgstr "Définir le mot de passe"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Expiration du compte"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Le compte n’est pas disponible ou ne peut pas être modifié."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Comptes"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Action"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Actions"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Activer"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Activation de $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Actif"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Sauvegarde active"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Pages actives"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Actif depuis "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "État actif"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Répartition adaptative de la charge"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Répartition adaptative de la charge d’émission"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Ajouter"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Ajouter $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add DNS server"
+msgstr "Serveur de clés Tang"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Ajouter le cryptage de disque lié au réseau"
+
+#: pkg/storaged/stratis/pool.jsx:458
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add Tang keyserver"
+msgstr "Serveur de clés Tang"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Ajouter un VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+#, fuzzy
+#| msgid "Add VLAN"
+msgid "Add VPN"
+msgstr "Ajouter un VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+#, fuzzy
+#| msgid "Add item"
+msgid "Add a bitmap"
+msgstr "Ajouter un élément"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Ajouter une nouvelle zone"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+#, fuzzy
+#| msgid "MAC address"
+msgid "Add address"
+msgstr "Adresse MAC"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Ajouter des blocs de périphériques"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Ajouter un lien"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Ajouter un pont"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Ajouter un disque"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Ajouter des disques"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Ajouter un portail iSCSI"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Ajouter une clé"
+
+#: pkg/storaged/stratis/pool.jsx:551
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add keyserver"
+msgstr "Serveur de clés Tang"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Ajouter un membre"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Ajouter un nouvel hôte"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Ajouter une nouvelle zone"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Add passphrase"
+msgstr "Ancienne phrase secrète"
+
+#: pkg/networkmanager/wireguard.jsx:290
+#, fuzzy
+#| msgid "Add member"
+msgid "Add peer"
+msgstr "Ajouter un membre"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "Physical volume"
+msgid "Add physical volume"
+msgstr "Volume physique"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Ajouter des ports"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Ajouter des ports à la zone $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Ajouter une clé publique"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+#, fuzzy
+#| msgid "Add item"
+msgid "Add route"
+msgstr "Ajouter un élément"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+#, fuzzy
+#| msgid "DNS search domains"
+msgid "Add search domain"
+msgstr "Domaines de recherche DNS"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Ajouter des services"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Ajouter des services à la zone $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Ajouter des services à la zone $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Ajouter une équipe"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Ajouter une zone"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "Ajout de \"$0\" aux options de cryptage"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Ajout de \"$0\" aux options du système de fichiers"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"L’ajout de $0 interrompra la connexion au serveur et rendra l’interface "
+"d’administration indisponible."
+
+#: pkg/storaged/stratis/pool.jsx:468
+#, fuzzy
+#| msgid ""
+#| "Saving a new passphrase requires unlocking the disk. Please provide a "
+#| "current disk passphrase."
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Pour enregistrer une nouvelle phrase secrète, il faut déverrouiller le "
+"disque. Veuillez fournir une phrase secrète actuelle."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Ajouter des ports personnalisés rechargera firewalld. Le rechargement de la "
+"configuration entraînera la perte de celle en cours d’exécution !"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Ajout d’une clé"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Ajout d’un volume physique à $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Ajout de rd.neednet=1 à la ligne de commande du noyau"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "DNS supplémentaire $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Domaines de recherche DNS supplémentaires $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Actions supplémenaires"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Adresse supplémentaire $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Paquets supplémentaires :"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Ports additionnels"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Adresse"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Adresse $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "L’adresse ne peut pas être vide"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "L’adresse ne correspond pas à une URL valide"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Adresses"
+
+#: pkg/networkmanager/wireguard.jsx:52
+#, fuzzy
+#| msgid "Address cannot be empty"
+msgid "Addresses are not formatted correctly"
+msgstr "L’adresse ne peut pas être vide"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Administration avec la console web de Cockpit"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Accès administrateur"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr ""
+"Un accès administratif est nécessaire pour créer et accéder aux rapports."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Accès administratif requis"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "TCA avancé"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Après"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Une fois le domaine quitté, seuls les utilisateurs disposant d’identifiants "
+"locaux pourront se connecter à cette machine. Cela peut également affecter "
+"d’autres services tels que les paramètres de résolution DNS. La liste des "
+"autorités de certification approuvées peut, en outre, changer."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Après le démarrage du système"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Alerte"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Alerte et au-dessus"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Alias"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Tout"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Toutes les mises à jour"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Tout en un"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Autoriser l’exécution (unmask)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+#, fuzzy
+#| msgid "Allowed addresses"
+msgid "Allowed IPs"
+msgstr "Adresses autorisées"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Adresses autorisées"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Documentation des rôles Ansible"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Toute chaîne de texte dans les messages des journaux peut être filtrée. La "
+"chaîne peut également être sous la forme d’une expression régulière. "
+"Supporte également le filtrage par les champs du journal des messages. Il "
+"s’agit de valeurs séparées par des espaces, sous la forme FIELD=VALUE, où "
+"valeur peut être une liste de valeurs possibles séparées par des virgules."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Apparence"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr ""
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Applications"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Liste des applications"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Appliquer et redémarrer"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Appliquer les correctifs à chaud du noyau"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Appliquer cette solution"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr ""
+"Application de la nouvelle stratégie… Cette opération peut prendre plusieurs "
+"minutes."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Application de la solution..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Application des mises à jour"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "L’application des mises à jour a échoué"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "Approprié pour les montages critiques, tels que /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Applis"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Évaluation"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Étiquette d’inventaire"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "Au démarrage"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Au moins $0 disque est nécessaire."
+msgstr[1] "Au moins $0 disques sont nécessaires."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "Au moins un périphérique en mode bloc est nécessaire."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Au moins un disque est nécessaire."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "À la minute"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "À la seconde"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "À un moment précis"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Attributs"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Journal d’audit"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "S’authentifier"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Authentification"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Authentification"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Échec d’authentification"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Échec d’authentification : connexion close par le serveur"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Une authentification est nécessaire pour effectuer les tâches privilégiées "
+"dans la console web Cockpit"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Authentification requise"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Autoriser la clé SSH"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Clés SSH publiques autorisées"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automatique"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automatique (DHCP uniquement)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Connexion automatique"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Mises à jour automatiques"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Démarre automatiquement"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Utiliser automatiquement NTP"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Utilisation automatique de serveurs NTP supplémentaires"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Utiliser automatiquement des serveurs NTP spécifiques"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Script d’automatisation"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Cibles disponibles sur $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Mises à jour disponibles"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "partition"
+msgid "BIOS boot partition"
+msgstr "partition"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "Date du BIOS"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "Version du BIOS"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Retour aux comptes"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Érroné"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Mauvais réglage"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Répartiteur de charge"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Avant"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Liaison vers"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Noir"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Lame"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Boîtier en lame"
+
+#: pkg/storaged/block/other.jsx:41
+#, fuzzy
+#| msgid "Block devices"
+msgid "Block device"
+msgstr "Périphérique en mode bloc"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Périphérique bloc pour système de fichiers"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Périphérique en mode bloc"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Bloqué"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Liaison"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Amorçage"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"Le démarrage échoue si le système de fichiers n'est pas monté, ce qui "
+"empêche l'accès à distance"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr ""
+"Le démarrage réussit toujours lorsque le système de fichiers n'est pas monté"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Liés par"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Pont"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Port du pont"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Paramètres du port du pont"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Diffuser"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Configuration endommagée"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Mises à jour des corrections de bogues disponibles"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Anomalies"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Châssis d’extension de bus"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"En remplaçant le mot de passe de la clé SSH $0 par le mot de passe de "
+"connexion de $1 sur $2, la clé sera automatiquement rendue disponible et "
+"vous pourrez vous connecter à $3 sans mot de passe à l’avenir."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " Ignorer la vérification du navigateur "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Sécurité du CPU"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "Paramètres de sécurité du CPU"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Utilisation CPU"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "Utilisation/charge CPU"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Cache"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr ""
+"Il peut s’agir d’un nom d’hôte, d'une adresse IP, d’un nom d'alias ou de "
+"ssh:// URI"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr ""
+"Impossible de trouver des journaux en utilisant la combinaison actuelle de "
+"filtres."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Annuler"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Annuler la mise hors tension"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Annuler le redémarrage"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Ne peut pas être activé"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Impossible de se connecter à une machine inconnue"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Impossible de transférer les identifiants de connexion"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+"Impossible de joindre un domaine, car realmd n’est pas disponible sur votre "
+"système"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Impossible de planifier un événement dans le passé"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Capacité"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Opérateur"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Modification"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+#, fuzzy
+#| msgid "Change crypto policy"
+msgid "Change cryptographic policy"
+msgstr "Changer la stratégie de chiffrement"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Modifier le nom d’hôte"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Modifier le nom de l’initiateur iSCSI"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Modifier le nom de l’initiateur iSCSI"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change label"
+msgstr "Modifier la phrase secrète"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Modifier la phrase secrète"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Modifier le mot de passe"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Modifier le profil de performance"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Modifier le profil"
+
+#: pkg/users/shell-dialog.js:70
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change shell"
+msgstr "Modifier la phrase secrète"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Modifier l’heure du système"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Changer le mot de passe de $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Modifier les paramètres"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Les changements de clés sont souvent le résultat d’une réinstallation du "
+"système d’exploitation. Cependant, un changement inattendu peut indiquer une "
+"tentative d’interception de votre connexion par un tiers."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"La modification des paramètres interrompt la connexion au serveur et rend "
+"l’interface utilisateur d’administration indisponible."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Vérifier les mises à jour"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Vérification $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Vérification de l’adresse IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "Vérification du périphérique RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Vérification et réparation du périphérique RAID $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Vérification du paquet $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Vérification du support NBDE dans initrd"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Vérification de nouvelles applications"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Vérification des mises à jour des paquets..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Vérification des logiciels installés"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Vérification des processus connexes"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Vérification de l’état des logiciels"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Choisissez la langue à utiliser dans l’application"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Taille de bloc"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Classe"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Nettoyage pour $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Effacer « Échec du démarrage »"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Supprimer tous les filtres"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Effacer la recherche"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Périphérique en clair"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Logiciel client"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Fermer"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Fermer les pages sélectionnées"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "L’authentification de Cockpit est mal configurée."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "Configuration du cockpit de NetworkManager et Firewalld"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit n’a pas pu contacter l’hôte indiqué."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Erreur interne inattendue dans cockpit."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit est un gestionnaire de serveur qui facilite l’administration de vos "
+"serveurs Linux via un navigateur Web. Sauter entre le terminal et l’outil "
+"web n’est pas un problème. Un service démarré via Cockpit peut être arrêté "
+"via le terminal. De même, si une erreur se produit dans le terminal, elle "
+"s’affiche dans l’interface du journal Cockpit."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr ""
+"Cockpit est une interface interactive d’administration de serveur Linux."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit n’est pas compatible avec le logiciel sur le système."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit n’est pas installé"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit n’est pas installé sur le système."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit est parfait pour les nouveaux administrateurs système, leur "
+"permettant d’effectuer facilement des tâches simples telles que "
+"l’administration du stockage, l’inspection des journaux, le démarrage et "
+"l’arrêt des services. Vous pouvez surveiller et administrer plusieurs "
+"serveurs en même temps. Il suffit de les ajouter en un seul clic et vos "
+"machines s’occuperont de leurs pairs."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit peut ne pas s'afficher correctement dans votre navigateur"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Collecte et regroupement des données de diagnostic et d'assistance"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Collecter les vidages de mémoire sur incidents du noyau"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Collecter des mesures"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Couleur"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr ""
+"Les ports, les plages et les services séparés par des virgules sont acceptés"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Commander"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Commande introuvable"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Commentaire"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "La communication avec tuned a échoué"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "PCI compact"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Compatible avec tous les systèmes et périphériques (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Compatible avec les systèmes modernes et les disques durs > 2 To (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr ""
+"Compresser les vidages de mémoire sur incidents pour économiser de l’espace"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Compression"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "La condition $0 = $1 n’a pas été remplie"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Condition non remplie"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Configuration"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Configuration en cours"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Configuration de l’adresse IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Configuration de « kdump »"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Modification des paramètres système"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Confirmer"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Confirmer la suppression de $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Confirmer le mot de passe de la clé"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Confirmer le nouveau mot de passe de la clé"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Confirmer le nouveau mot de passe"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Confirmer le mot de passe"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Confirmer la suppression de $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Confirmer la suppression avec une phrase secrète alternative"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Confirmer l'arrêt de $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "Conflit avec"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Conflits"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Connecter automatiquement"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Se connecter à"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Se connecter à :"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Connexion au démon SETroubleshoot..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "La connexion à dbus a échoué : $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Connexion à la machine"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Erreur de connexion"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Échec de la connexion"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "La connexion a expiré."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "La connexion sera perdue"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Comprend"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Domaine contacté"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Contient :"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Poursuivre"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Continuer la session"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Contrôle"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Contrôle"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Contrôleur"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Convertible"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Copié"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Copier"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Copier dans le presse-papier"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Core $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "Impossible à contacter $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Emplacement des vidages de mémoire sur incidents"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Rapport d’incident"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Système de plantage"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Créer"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "Créer un groupe de volume LVM2"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "Créer Périphérique RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Créer Périphérique RAID"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Créer un pool Stratis"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Créer une nouvelle clé SSH et l’autoriser"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Créer un instantané du système de fichiers $0"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Créer un compte avec un UID non unique"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Créer un compte avec un mot de passe faible"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Créer et modifier la propriété du répertoire personnel"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Créer et monter"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create and mount"
+msgid "Create and start"
+msgstr "Créer et monter"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Créer un système de fichiers"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Créez-le"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Créer un volume logique"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Créer un nouveau compte"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Créer un nouveau système de fichiers"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Créer un nouveau groupe"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Créer un nouveau volume logique"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Créez un nouveau fichier de tâches avec ce contenu."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "Créer un nouveau volume logique"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "Créer uniquement"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Créer Partition"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Créer une partition sur $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Créer une table de partition"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Créer Instantané"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Créer un instantané et le monter"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Créer un instantané uniquement"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create storage volume"
+msgid "Create storage device"
+msgstr "Création un volume de stockage"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Créer un volume"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Créer Volume dynamique"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Créer Timer"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Créer Groupe de volumes"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Créé"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Création d’un groupe de volumes LVM2 $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "Création d’un périphérique RAID $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "Création d’un périphérique VDO"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Création d’un système de fichiers sur $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Création du volume logique $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Création de la partition $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Création d’un instantané de $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"La création de $0 interrompra la connexion au serveur et rendra l’interface "
+"d’administration indisponible."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Critique et au-dessus"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+#, fuzzy
+#| msgid ""
+#| "Crypto Policies is a system component that configures the core "
+#| "cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and "
+#| "Kerberos protocols."
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Les « Stratégie de chiffrement » sont un composant système qui configure les "
+"sous-systèmes de cryptographie. Il couvre les protocols TLS, IPSec, SSH, "
+"DNSSec, et Kerberos."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+#, fuzzy
+#| msgid "Crypto policy"
+msgid "Cryptographic policy"
+msgstr "Stratégie de chiffrement"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+#, fuzzy
+#| msgid "Crypto policy is inconsistent"
+msgid "Cryptographic policy is inconsistent"
+msgstr "La stratégie de chiffrement est incohérente"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Inser"
+
+#: pkg/shell/shell-modals.jsx:198
+#, fuzzy
+#| msgid "Ctrl-Shift-J"
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-J"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Démarrage actuel"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Pic d’utilisation du processeur"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Actuellement en cours d’utilisation"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Currently in use"
+msgid "Currently not supported"
+msgstr "Actuellement en cours d’utilisation"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "personnalisé"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+#, fuzzy
+#| msgid "Custom crypto policy"
+msgid "Custom cryptographic policy"
+msgstr "Stratégie de chiffrement personnalisée"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Options de montage personnalisées"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Ports personnalisés"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom path"
+msgid "Custom type"
+msgstr "Chemin personnalisé"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Zones personnalisées"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "DEFAUT avec vérification de la signature SHA-1 autorisée."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "Domaines de recherche DNS"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "Domaines de recherche DNS $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Tous les jours"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Alerte de danger :"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Sombre"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Données"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Données utilisées"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Les spécifications de date doivent être au format AAAA-MM-JJ hh:mm:ss. "
+"Alternativement, les chaînes « hier », « aujourd’hui », « demain » sont bien "
+"comprises. « maintenant » fait référence à l’heure actuellement. Enfin, des "
+"temps relatifs peuvent être spécifiés, précédés de « - » ou « + »"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Désactiver"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Renommer le volume logique"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Désactivation en cours"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Désactivation de $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Débogage et au-dessus"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Diminution par un"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+#, fuzzy
+#| msgid "RAID 4 (dedicated parity)"
+msgid "Dedicated parity (RAID 4)"
+msgstr "RAID 4 (Parité dédiée)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Déduplication"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Par défaut"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Retard"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Le retard doit correspondre à un nombre"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Supprimer"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Supprimer $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Supprimer le compte"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Supprimer les fichiers"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Supprimer le groupe"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Supprimer"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Supprimer définitivement le rapport ?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"La suppression de $0 interrompra la connexion au serveur et rendra "
+"l’interface d’administration indisponible."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Suppression de $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Suppression d’un groupe de volumes LVM2 $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr ""
+"La suppression d’un pool Stratis efface toutes les données qu’il contient."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr ""
+"La suppression d’un système de fichiers entraîne la suppression de toutes "
+"les données qu’il contient."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr ""
+"La suppression d’un volume logique supprimera toutes les données qu’il "
+"contient."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr ""
+"La suppression d’une partition supprimera toutes les données qui s’y "
+"trouvent."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting erases all data on a RAID device."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "La suppression efface toutes les données d’un périphérique RAID."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "La suppression efface toutes les données d’un périphérique VDO."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "La suppression efface toutes les données d’un groupe de volumes."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting erases all data on a volume group."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "La suppression efface toutes les données d’un groupe de volumes."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "La suppression supprimera les fichiers suivants :"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Description"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Bureau"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Détachable"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Détails"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Développement"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Périphérique"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Fichier de périphérique"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Périphérique en lecture seule"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Nom du service"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Rapports de diagnostic"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Répertoire"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Le répertoire $0 n’est pas accessible en écriture ou n’existe pas."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Désactiver le multithreading simultané"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Désactiver le pare-feu"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Désactiver tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Désactivé"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Interdire le mot de passe interactif"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Interdire l’authentification par mot de passe"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Empêcher l’exécution (mask)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Déconnecter"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Déconnecté"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "E / S disque"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Le disque est OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Le disque est défaillant"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Phrase secrète du disque"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Disques"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Utilisation des disques"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+#, fuzzy
+#| msgid "Dismiss $0 alert"
+#| msgid_plural "Dismiss $0 alerts"
+msgid "Dismiss"
+msgstr "Rejeter l’alerte $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Rejeter l’alerte $0"
+msgstr[1] "Rejeter les alertes $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Rejeter les alertes sélectionnées"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Langue d’affichage"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+#, fuzzy
+#| msgid "RAID 5 (distributed parity)"
+msgid "Distributed parity (RAID 5)"
+msgstr "RAID 5 (Parité répartie)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "Ne pas monter"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Ne pas monter automatiquement au démarrage"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Station d’accueil"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Ne démarre pas automatiquement"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "Ne se monte pas au démarrage"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Domaine"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Domaine"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Adresse de domaine"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Nom de l’administrateur du domaine"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Mot de passe administrateur du domaine"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Le domaine n’a pas pu être contacté"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Le domaine n’est pas pris en charge"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Ne pas répéter"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+#, fuzzy
+#| msgid "RAID 6 (double distributed parity)"
+msgid "Double distributed parity (RAID 6)"
+msgstr "RAID 6 (Double parité répartie)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Télécharger"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Télécharger un nouveau navigateur gratuitement"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Téléchargé"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Téléchargement"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Téléchargement $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Lecteur"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Double rang"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Partition étendue"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Modifier"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Editer /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Modifier le serveur de clés Tang"
+
+#: pkg/networkmanager/vlan.jsx:91
+#, fuzzy
+#| msgid "VLAN settings"
+msgid "Edit VLAN settings"
+msgstr "Paramètres VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:135
+#, fuzzy
+#| msgid "Bond settings"
+msgid "Edit bond settings"
+msgstr "Paramètres de liaison"
+
+#: pkg/networkmanager/bridge.jsx:96
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Edit bridge settings"
+msgstr "Paramètres du pont"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Modifier le service personnalisé dans la zone $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Modifier hôte"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Modifier hôtes"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Modifier motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Point de montage"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Modifier les règles et les zones"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Modifier le service"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Service d'édition $0"
+
+#: pkg/networkmanager/team.jsx:154
+#, fuzzy
+#| msgid "Team settings"
+msgid "Edit team settings"
+msgstr "Paramètres de l’équipe"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Modifier l'utilisateur"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "La modification d’une clé nécessite un emplacement libre"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Éjecter $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "PC intégré"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Vide"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Vide"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Mot de passe vide"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "$target en cours de vidage"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Activer"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Activer ou désactiver le périphérique"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Activer le service"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Activer le pare-feu"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Activée"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "Permettre l'accès à $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Chiffrer les données"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+#, fuzzy
+#| msgid "Edit Tang keyserver"
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Modifier le serveur de clés Tang"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+#, fuzzy
+#| msgid "Encryption passphrase"
+msgid "Encrypt data with a passphrase"
+msgstr "Phrase de chiffrement"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Chiffré"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Chiffré $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted Stratis pool $0"
+msgid "Encrypted Stratis pool"
+msgstr "Pool Stratis chiffré $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Volume logique chiffré de $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Partition chiffrée de $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Chiffrement"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Options de chiffrement"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Phrase de chiffrement"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Type de chiffrement"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Terminé"
+
+#: pkg/networkmanager/wireguard.jsx:309
+#, fuzzy
+#| msgid "Entrypoint"
+msgid "Endpoint"
+msgstr "Point d’accès"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Imposer les règles"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Mises à jour des améliorations disponibles"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Entrez une adresse MAC valide"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Ensemble du sous-réseau"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Entrée à $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Effacement de $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Errata"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Erreur"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Erreur et au-dessus"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Une erreur s’est produite"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "Erreur d'installation de $0 : PackageKit n'est pas installé"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Message d’erreur"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr ""
+"Erreur d’exécution de semanage pour découvrir les modifications du système"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Erreur lors de l’enregistrement des clés autorisées : "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Erreur lors du réglage du mode SELinux : « $0 »"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "MAC Ethernet"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "MTU Ethernet"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Exemple : 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Exemple : 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Mot de passe excellent"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Châssis d’extension"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Expiration du compte le"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Expiration du compte le $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Exporter"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Exporter vers le réseau"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Informations complémentaires"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Partition étendue"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS n’est pas correctement activé"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS avec des restrictions supplémentaires liées aux critères communs."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Échoué"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Échec de l’ajout de la machine : $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Échec de l’ajout du port"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Échec de l’ajout du service"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Échec de l’ajout de la zone"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Échec de la modification du mot de passe"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "Échec de la configuration de PCP"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Échec de la suppression de l’alerte : $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Échec de la désactivation de « tuned »"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Échec de la désactivation du profil « tuned »"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Échec de la modification de la machine : $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Échec de la modification du service"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "Échec de l’activation $0 dans firewalld"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Échec de l’activation de « tuned »"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Echec de la récupération des journaux"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Échec du chargement des clés autorisées."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Échec du chargement de l'unité"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"Échec de l’analyse des fichiers unitaires pour dnf-automatic.timer ou dnf-"
+"automatic-install.timer. Veuillez supprimer les remplacements personnalisés "
+"pour configurer les mises à jour automatiques."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Échec du redémarrage du service"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Impossible d’enregistrer les modifications dans /etc/motd"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Échec de l’enregistrement des paramètres"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Échec du démarrage"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Échec du changement de profil"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "État du fichier"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Système de fichiers"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "Le système de fichiers est verrouillé"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Nom du système de fichiers"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Création d’un système de fichiers sur $target"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+#, fuzzy
+#| msgid "The filesystem is already mounted at $0. Proceeding will unmount it."
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "Le système de fichiers est déjà monté à $0. La procédure le démontera."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Filtrer par nom ou description"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Filtrer les éléments de menu"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Services de filtrage"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Filtres"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Empreinte de la clé"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Pare-feu"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Le pare-feu n’est pas disponible"
+
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Version du micrologiciel"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "Correction du support NBDE"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Taille de police"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Non autorisé à exécuter"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Forcer la modification"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Forcer la suppression"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Forcer la modification de mot de passe"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Formater"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Formater $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Format et montage"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format and mount"
+msgid "Format and start"
+msgstr "Format et montage"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Format uniquement"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "Le formatage efface toutes les données d’un périphérique de stockage."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Délai d’attente de redirection $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Numéro de cadre"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Espace libre"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Recherche en forme libre"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Vendredis"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "De"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Nom complet"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Passerelle"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Général"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Généré"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Aller à $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Aller à l’application"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Aller à maintenant"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Visibilité du graphique"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Menu des options de visibilité du graphique"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Groupe"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Nom du groupe"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Groupes"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Augmenter"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Augmenter le contenu"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Augmenter la taille logique de $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Augmenter le volume logique"
+
+#: pkg/storaged/block/resize.jsx:409
+#, fuzzy
+#| msgid "partition"
+msgid "Grow partition"
+msgstr "partition"
+
+#: pkg/storaged/stratis/pool.jsx:337
+#, fuzzy
+#| msgid "Grow to take all space"
+msgid "Grow the pool to take all space"
+msgstr "Augmenter en vue de prendre tout l’espace"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Augmenter en vue de prendre tout l’espace"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Mode Hair Pin"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Mode Hair Pin"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Portable"
+
+#: pkg/storaged/drive/drive.jsx:64
+#, fuzzy
+#| msgid "Hard Disk"
+msgid "Hard Disk Drive"
+msgstr "Disque dur"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Informations sur le matériel"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Santé"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Bonjour $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Aide"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Hide confirmation password"
+msgstr "Confirmer le mot de passe"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "Use password"
+msgid "Hide password"
+msgstr "Utiliser un mot de passe"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "ID de la hiérarchie"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "Une meilleure interopérabilité au prix d’une surface d’attaque accrue."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Comptage des paquets d’historique"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Répertoire utilisateur"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Hôte"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "La clé de l’hôte est incorrecte"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Nom d’hôte"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Hôtes"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Toutes les heures"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Heures"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr ""
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "Adresse IP"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"Adresse IP avec préfixe de routage. Séparez les valeurs multiples par une "
+"virgule. Exemple : 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+#, fuzzy
+#| msgid "IPv4 address"
+msgid "IPv4 addresses"
+msgstr "Adresse IPv4"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "Paramètres IPv4"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "Paramètres IPv6"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Identifiant"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"S’il est laissé vide, l’ID sera généré sur la base des services et des "
+"numéros de port associés"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Si l’empreinte digitale correspond, cliquez sur « Accepter la clé et se "
+"connecter ». Sinon, ne vous connectez pas et contactez votre administrateur."
+
+#: pkg/shell/hosts_dialog.jsx:480
+#, fuzzy
+#| msgid ""
+#| "If the fingerprint matches, click 'Accept key and connect'. Otherwise, do "
+#| "not connect and contact your administrator."
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Si l’empreinte digitale correspond, cliquez sur « Accepter la clé et se "
+"connecter ». Sinon, ne vous connectez pas et contactez votre administrateur."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ignorer"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "Dans"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Afin d’autoriser la connexion à $0 en tant que $1 sans mot de passe à "
+"l’avenir, utilisez le mot de passe de connexion de $2 sur $3 comme mot de "
+"passe clé, ou laissez le mot de passe clé vide."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "En sync"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Inactif"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "Créer un volume logique"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "services inclus"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"Les requêtes entrantes sont bloquées par défaut. Les requêtes sortantes ne "
+"sont pas bloquées."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Montage de système de fichiers incohérent"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Augmentation d’un"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Mémoire de l’index"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Indirect"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Info"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Info et au-dessus"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Initialiser"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Initialiser le disque $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "L’initialisation efface toutes les données d’un disque."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Initialisation..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Aperçus : "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Installer"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Installer la prise en charge NFS"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Installer le support Stratis"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Installer toutes les mises à jour"
+
+#: pkg/apps/application-list.jsx:200
+#, fuzzy
+#| msgid "Package information"
+msgid "Install application information"
+msgstr "Informations sur le paquet"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Installer le cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Installer les mises à jour de kpatch"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "Installer le support realmd"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Installer les mises à jour de sécurité"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr "Installez setroubleshoot-server pour dépanner les événements SELinux."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Installer le logiciel"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Veuillez installer le paquet $0"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "installée"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Installation"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Installation de $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "L’installation de $0 supprimerait $1."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Installation des paquets"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Interface"
+msgstr[1] "Interfaces"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Membres de l’interface"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Interfaces"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Erreur interne"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Erreur interne : en-tête de la question de Challenge non valide"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Invalide"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Adresse non valide $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Format de date non valide"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Format de date non valide et format d’heure non valide"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Date d’expiration non valide"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Les autorisations de fichier ne sont pas valides"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Clé non valide"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Métrique non valide $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Nombre de jours non valide"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Numéro de port invalide"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Préfixe non valide $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Préfixe ou masque de réseau non valide $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Plage invalide"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Format d’heure non valide"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Fuseau horaire incorrect"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Nom d’utilisateur ou mot de passe non valide"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "Passerelle IoT"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "« sshd » est-il en cours d’exécution sur un port différent ?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Tâches"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Joindre"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Rejoignez un domaine"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Joindre un domaine"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Rejoindre"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Rejoindre un domaine nécessite l’installation de realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Rejoindre ce domaine n’est pas pris en charge"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Rejoint l’espace de noms de"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Journal"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Entrée de journal"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Entrée de journal non trouvée"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Aller à"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit n’est pas installé"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Tester les paramètres de kdump"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Gardez la connexion"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Vidage de mémoire sur incident du noyau"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Kernel Dump"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "Le correctif à chaud du noyau $0 est actif"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Le correctif à chaud du noyau $0 est installé"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Paramètres des correctifs à chaud du noyau"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Corrections à chaud du noyau"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Mot de passe clé"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Les emplacements de clé de type inconnu ne peuvent pas être édités ici"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Source de la clé"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Clés"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Serveur de clés"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Adresse du serveur"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr ""
+"La suppression du serveur de clés peut empêcher le déverrouillage de $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "Clé LACP"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "LEGACY avec interopérabilité Active Directory."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "VDO pool"
+msgid "LVM2 VDO pool"
+msgstr "Pool VDO"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Volume logique"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Volume logique"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volume"
+msgstr "Volume physique"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volumes"
+msgstr "Volume physique"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "Groupe de volumes LVM2"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "Groupe de volumes LVM2 $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Portable"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Dernières 24 heures"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Les 7 derniers jours"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Dernier actif"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "Le dernier logement de clé ne peut pas être retiré"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Dernière vérification : $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "Le dernier logement de clé ne peut pas être retiré"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Dernière connexion"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Dernière modification : $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Dernière connexion réussie :"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "En savoir plus"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "Laissez $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Quitter le domaine"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Laissez vide pour ignorer le cryptage"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Sous licence GNU LGPL version 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Clair"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Limiter l’accès"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Accès limité"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"Le mode accès limité restreint les privilèges d’administration. Certaines "
+"parties de la console web auront des fonctionnalités réduites."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Limites"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Délai de chute de lien"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Lien local"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Surveillance du lien"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Délai d’activation de lien"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Observation du lien"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Lié"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem $0"
+msgid "Linux filesystem data"
+msgstr "Démonter le système de fichiers $0"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Espace swap"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Écouter"
+
+#: pkg/networkmanager/wireguard.jsx:242
+#, fuzzy
+#| msgid "Listen"
+msgid "Listen port"
+msgstr "Écouter"
+
+#: pkg/networkmanager/wireguard.jsx:149
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Listen port must be a number"
+msgstr "La taille doit correspondre à un nombre"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Lister les unités"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "L’énumération des unités a échoué : $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Charge"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "L’équilibrage de charge"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Charger des données antérieures"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Charger les entrées précédentes"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Le chargement des mises à jour disponibles a échoué"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr ""
+"Le chargement des mises à jour disponibles est en cours, veuillez "
+"patienter..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Chargement des entrées antérieures"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Chargement des clés..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Le chargement des clés SSH a échoué"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Le chargement des unités a échoué"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Chargement des paquets..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Chargement des modifications système..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Échec du chargement de l'unité"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Chargement..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Comptes locaux"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Système de fichiers local"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Point de montage local"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "No storage"
+msgid "Local storage"
+msgstr "Aucun stockage"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "localement dans $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Emplacement"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Verrouillage"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "Verrouillage $0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Verrouiller le compte"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgid "Locked"
+msgid "Locked data"
+msgstr "Verrouillé"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Verrouillage de $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Connexion"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Connectez-vous à $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Connectez-vous avec votre compte d’utilisateur du serveur."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Enregistrer les messages"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Déconnexion"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Déconnexion de l'utilisateur"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Connecté"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Logique"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Volume logique (instantané)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Taille logique"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Volume logique"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Volume logique (instantané)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Volume logique de $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Nom d’utilisateur"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Se connecter à nouveau"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Échec de la connexion"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Format de connexion"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Historique des connexions"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Liste de l’historique des connexions"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "Déconnexion $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Déconnexion réussie"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Journaux"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Bureau de profil bas"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Lunch Box"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "Périphérique RAID"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "Périphérique RAID $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Périphérique en lecture seule"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "Le service est en cours d’exécution"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "Périphérique RAID"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Ajouter des disques"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (recommandé)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU doit être un nombre positif"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "ID machine"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Empreintes des clés SSH de la machine"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Châssis principal du serveur"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Maintenance"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+#, fuzzy
+#| msgid "Rename filesystem"
+msgid "Manage filesystem sizes"
+msgstr "Renommer le système de fichiers"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Gérer le stockage"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Interfaces non gérées"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "Gestion des LVM"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "Gérer les montages NFS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "Gestion des RAID"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "Gestion des ODV"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "Gestion des réseaux locaux virtuels (VLAN)"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Gestion du pare-feu"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Gestion des liens de réseau"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Gérer les ponts de mise en réseau"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Gestion du réseau"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Gestion des partitions"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Gestion des lecteurs physiques"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Gestion des services"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Gestion des mises à jour logicielles"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Gestion des comptes utilisateurs"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Manuel"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Manuellement"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Marquage de $target comme défectueux"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Service Masque"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Masqué"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Le service Masque empêche toutes les unités dépendantes d’être exécutées. "
+"Cela peut avoir un plus gros impact que prévu. Veuillez confirmer que vous "
+"souhaitez masquer cette unité."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Âge maximal du message $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Lecteur optique"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Mémoire"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Technologie de mémoire"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Utilisation de la mémoire"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Utilisation de la mémoire/swap"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Fusionné"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Message aux utilisateurs connectés"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "Les messages relatifs à la défaillance se trouvent dans le journal :"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Méta-données utilisées"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Méthode"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Métrique"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Métriques et historique"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "L’historique des métriques n’a pas pu être chargé"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Paramètres des métriques"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Mini Tour"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Minute doit correspondre à un nombre entre 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "Minutieusement"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minutes"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Mitigations"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Mode"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Modèle"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Modifier $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "lundis"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Intervalle de surveillance"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Objectifs de surveillance"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Mensuel"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Plus d’infos..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Monter"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+#, fuzzy
+#| msgid "Mount point"
+msgid "Mount Point"
+msgstr "Point de montage"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "Monter après que le réseau soit devenu disponible, ignorer les échecs"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Monter automatiquement au démarrage également"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Monter au démarrage"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Montage automatique $0 au démarrage"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Montage avant le début des services"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Configuration de montage"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Monter le système de fichiers"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Monter maintenant"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Montez sur $0 maintenant"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Options de montage"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Point de montage"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Le point de montage ne peut pas être vide"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Le point de montage ne peut pas être vide."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Le point de montage est déjà utilisé pour $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Le point de montage doit commencer par « / »."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Monter en lecture seule"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Monter sans attendre, ignorer les échecs"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Montage de $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Montage avant le démarrage des services"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Montage en parallèle avec les services"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+"Montage en parallèle avec les services, mais après que le réseau soit "
+"disponible"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Châssis multi-système"
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Multipathed devices"
+msgid "Multipathed devices"
+msgstr "Gestion des appareils multi-trajets"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "Point de montage NFS"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "Ping NSNA"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "Serveur NTP"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Nom"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Le nom ne peut pas être vide."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Le nom ne peut pas être vide."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Le nom ne peut pas dépasser $0 octets"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Le nom ne peut pas dépasser $0 caractères"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Le nom ne peut pas dépasser 127 caractères."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Le nom ne peut pas dépasser 127 caractères."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Le nom ne peut pas contenir le caractère « $0 »."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "Le nom ne peut pas contenir le caractère « $0 »."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Le nom ne peut pas contenir d’espace."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Besoin d’au moins un serveur NTP"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Réseau"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Réseau E/S"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Agrégation de réseau"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Les périphériques réseau et les graphiques nécessitent NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Journaux du réseau"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Utilisation du réseau"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager n’est pas installé"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager n’est pas en cours d’exécution"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Utilisation du réseau"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Réseau"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Jamais"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Ne jamais expirer le compte"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Ne jamais faire expirer le mot de passe"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Jamais connecté"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Nouveau point de montage NFS"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Nouvel hôte"
+
+#: pkg/shell/hosts_dialog.jsx:464
+#, fuzzy
+#| msgid "New host"
+msgid "New host: $0"
+msgstr "Nouvel hôte"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Nouveau mot de passe clé"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Nouveau nom"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Nouvelle phrase secrète"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Nouveau mot de passe"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Le nouveau mot de passe n’a pas été accepté"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Prochain"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Non"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "Pas d'ID spécifié"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Pas d’alertes SELinux."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Aucune application installée ou disponible."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Pas d’emplacements disponibles"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Aucun périphérique en mode bloc n’est disponible."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "Aucun périphérique de démarrage trouvé"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Pas de transporteur"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Aucune configuration trouvée"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Aucune donnée disponible"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Aucune donnée disponible entre $0 et $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Aucun délai"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Aucune description disponible"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Aucune description fournie."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "Aucun périphérique de démarrage trouvé"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Aucun disque disponible."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Aucun journal trouvé"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "Aucun résultat trouvé"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Pas de cryptage"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Aucun événement"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Aucun système de fichiers"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Aucun système de fichiers"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Pas d’emplacements libres pour les clés"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Pas d’espace libre"
+
+#: pkg/storaged/partitions/partition.jsx:79
+#, fuzzy
+#| msgid "Create partition"
+msgid "No free space after this partition"
+msgstr "Créer Partition"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Aucun nom de groupe spécifié"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Aucune clé d’hôte trouvée."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Aucun paquet d’installation n’a été trouvé pour cette application."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Aucune clé n’a été ajoutée"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "Aucune langue ne correspond"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Aucune entrée de journal"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Pas de volumes logiques"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Aucun journal trouvé"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Aucun résultat correspondant n’a été trouvé"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Aucun média inséré"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Pas de partitionnement"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "Pas de partitionnement"
+
+#: pkg/networkmanager/wireguard.jsx:341
+#, fuzzy
+#| msgid "No keys added"
+msgid "No peers added."
+msgstr "Aucune clé n’a été ajoutée"
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "Volumes physiques"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Aucun nom réel n’est renseigné"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Aucun résultat trouvé"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Aucun résultat correspondant aux critères de filtrage n’a été trouvé. "
+"Effacez tous les filtres pour afficher les résultats."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Aucune règle atteinte"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No storage"
+msgid "No storage found"
+msgstr "Aucun stockage"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Pas de volumes logiques"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Aucun fichier ou répertoire de ce nom"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Aucune modification système"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Aucun rapport sur le système."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Pas de mise à jour"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Aucun nom d’utilisateur n’est renseigné"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Aucun"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Clé privée non valide"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Non autorisé à désactiver le pare-feu"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Non autorisé à activer le pare-feu"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Indisponible"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Pas connecté"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Non connecté à Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Non connecté à l’hôte"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough free space"
+msgstr "Espace insuffisant pour propager."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space"
+msgstr "Espace insuffisant pour propager."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "Espace insuffisant pour propager."
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Non trouvé"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Non installé"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "Not permitted to configure realms"
+msgid "Not permitted to configure network devices"
+msgstr "Non autorisé à configurer les domaines"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Non autorisé à configurer les domaines"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Non autorisé à effectuer cette action."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Pas prêt"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Non inscrit"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Pas en cours d’exécution"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Non défini"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Non synchronisé"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Note"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Ordinateur portable"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Notification et au-dessus"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr ""
+"Obscurcir les adresses réseau, les noms d'hôtes et les noms d'utilisateurs"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Rendu obscure"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "S’est produit à $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "S’est produit entre $0 et $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Occurrences"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "Ok"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Ancienne phrase secrète"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Ancien mot de passe"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Ancien mot de passe non accepté"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "Sur un périphérique monté"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "En cas d’échec"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Une fois que Cockpit est installé, l’activer avec « systemctl enable --now "
+"cockpit.socket »."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Seulement $0 de $1 sont utilisés."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr ""
+"Seuls les caractères alphabétiques, les nombres,:, _,. , @ , - sont autorisés"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Urgences uniquement"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"N’utiliser que des algorithmes approuvés et autorisés lors du démarrage en "
+"mode FIPS."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Zut !"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr ""
+"Ouvrez le service pmproxy dans le pare-feu pour partager des métriques."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Opération « $operation » sur $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Options"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Ou utilisez un navigateur groupé"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Autre"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"D’autres méthodes d’authentification sont toujours disponibles même lorsque "
+"l’authentification par mot de passe interactif n’est pas autorisée."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Autres options"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Sortie"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Aperçu"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Écraser"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Ecraser les données existantes avec des zéros (plus lent)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Le paquet cockpit-pcp est manquant pour l’historique des métriques"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Informations sur le paquet"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "Plantage de « PackageKit »"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit non installé"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit a signalé le code d’erreur $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Paquets"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Nom de la page"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Parent"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Parent $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Partie de"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Partie de $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Partition"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Partition de $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+#, fuzzy
+#| msgid "Volume size is $0. Content size is $1."
+msgid "Partition size is $0. Content size is $1."
+msgstr "La taille du volume est $0. La taille du contenu est $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Partitionnement"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Partitions"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Passif"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Phrase secrète"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "La phrase secrète ne peut pas être vide"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "La phrase secrète ne peut pas être vide"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Phrase secrète de n’importe quel autre emplacement de clé"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr ""
+"La suppression de la phrase secrète peut empêcher le déverrouillage de $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Les phrases secrète ne correspondent pas"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Mot de passe"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Le mot de passe a été modifié avec succès"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Expiration du mot de passe"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "Le mot de passe fait plus de 256 caractères"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Le mot de passe n’est pas acceptable"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Le mot de passe est trop faible"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Le mot de passe doit être changé"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Mot de passe non accepté"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Conseil sur les mots de passe"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Coller"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Erreur de collage"
+
+#: pkg/networkmanager/wireguard.jsx:215
+#, fuzzy
+#| msgid "Use existing"
+msgid "Paste existing key"
+msgstr "Utiliser l’existant"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Collez le contenu de votre clé publique SSH ici"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Chemin"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Coût du chemin"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Coût du chemin $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Chemin sur le serveur"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Le chemin sur le serveur ne peut pas être vide."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Le chemin sur le serveur doit commencer par « / »."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Chemin vers le répertoire"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Chemin d’accès au fichier"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Chemins"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Suspendre"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot recueille et analyse les mesures de performance de "
+"votre système."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Profil de performance"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Châssis périphérique"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Permanent"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "Supprimer définitivement le groupe $0 ?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "Supprimer définitivement $0 ?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Supprimer définitivement $0 ?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete subvolume $0?"
+msgstr "Supprimer définitivement $0 ?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Permission refusée"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Permissif"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Physique"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "Physical Volumes"
+msgstr "Volumes physiques"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Volumes physiques"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "Les volumes physiques ne peuvent pas être redimensionnés ici."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Choisissez une date"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Unité Pin"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Intervalle de ping"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Cible de ping"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Unité épinglée"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Pizza Box"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Veuillez vous authentifier pour obtenir un accès administrateur"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Veuillez activer JavaScript pour utiliser la console Web."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Veuillez installer le paquet $0"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Veuillez résoudre le problème et recharger cette page."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Veuillez spécifier une date d’expiration"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Veuillez spécifier l’hôte auquel vous connecter"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr ""
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Pool pour les volumes logiques dynamiques"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Pool pour les volumes à provisionnement dynamique"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Pool pour les volumes à provisionnement dynamique"
+
+#: pkg/storaged/stratis/pool.jsx:464
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Pool passphrase"
+msgstr "Ancienne phrase secrète"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Port"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Portable"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Ports"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Créer Partition"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Longueur du préfixe"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Longueur du préfixe ou masque de réseau"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Préparation en cours"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Présent"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Préserver"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Nom d’hôte pretty"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Démarrage précédent"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Primaire"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Priorité"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Priorité $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+#, fuzzy
+#| msgid "Private"
+msgid "Private key"
+msgstr "Privé"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "Problème pour devenir administrateur"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Détails du problème"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Informations sur le problème"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Processus utilisant l’emplacement"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Progrès : $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Site du projet"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "L’invite via passwd a expiré"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "L’invite via ssh-add a expiré"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "L’invite via ssh-keygen a expiré"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Recharger Propagation jusqu’à"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Protège contre les attaques anticipées à court terme au détriment de "
+"l’interopérabilité."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr ""
+"Fournissez la phrase secrète pour le pool sur ces périphériques en mode "
+"bloc :"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Clé publique"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Objectif"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ( $0 )"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (Bande)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (Miroir)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (Bande de miroirs)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (Parité dédiée)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (Parité répartie)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (Double parité répartie)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "Châssis RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "Niveau RAID"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Châssis de montage en rack"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Aléatoire"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Gamme"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "La plage doit être strictement ordonnée"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Rang"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Raw à un appareil"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Lire"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "En savoir plus..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Lecture seule"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Lecture en cours"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Lecture en cours..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Prêt"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Prêt"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Nom d’hôte réel"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Le nom d’hôte réel ne peut contenir que des caractères minuscules, des "
+"chiffres, des tirets et des points (avec des sous-domaines remplis)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Le nom d’hôte réel doit comporter 64 caractères ou moins"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Appliquer à nouveau et redémarrer"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Redémarrer"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Redémarrage une fois terminé"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Redémarrage recommandé"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Redémarrer le système..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Réception"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Hôtes récents"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Recommandé, paramètres de sécurité pour les modèles actuels de menace."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Reconnecter"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Recouvrement"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "Recouvrement du périphérique RAID $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Actualisation des informations sur le paquet"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Connexion refusée. L’hôte est inconnu"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Connexion refusée. Hostkey ne correspond pas"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Connexion refusée. Hostkey est inconnu"
+
+#: pkg/networkmanager/wireguard.jsx:224
+#, fuzzy
+#| msgid "Generated"
+msgid "Regenerate"
+msgstr "Généré"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "Régénération d’ initrd"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Enregistrement…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "Les processus et services connexes seront arrêtés de force."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "Les processus connexes seront arrêtés de force."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "Les services connexes seront arrêtés par la force."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Recharger"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Recharger Propagation à partir de"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Rechargement"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Recharger l’état des services restants"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "À distance sur CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "À distance sur FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "À distance sur NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "À distance sur NFS"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "À distance par SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "À distance par SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "À distance par SSH"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Suppressions :"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Retirer"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Supprimer $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Supprimer le service $0 de la zone $1"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Supprimer $0 ?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Supprimer le serveur de clés Tang ?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Supprimer le périphérique"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Retirer l’hôte"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Supprimer l’élément"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Remove missing physical volumes?"
+msgstr "Suppression du volume physique de $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "Supprimer la phrase de secrète dans l’emplacement de la clé $0 ?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+#, fuzzy
+#| msgid "Remove passphrase"
+msgid "Remove passphrase?"
+msgstr "Supprimer la phrase secrète"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Supprimer le service $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Supprimer la zone $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Suppression"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Suppression de $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"La suppression de $0 interrompra la connexion au serveur et rendra "
+"l’interface utilisateur d’administration indisponible."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "Supprimer $target du périphérique RAID"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"La suppression d’une phrase secrète sans confirmation d’une autre phrase "
+"secrète peut empêcher le déverrouillage ou la gestion des clés, si d’autres "
+"phrases secrète sont oubliées ou perdues."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Suppression du volume physique de $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"La suppression du service de cockpit pourrait rendre la console Web "
+"inaccessible. Assurez-vous que cette zone ne s’applique pas à votre "
+"connexion actuelle à la console Web."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "La suppression de la zone supprime tous les services qui s’y trouvent."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Renommer"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Renommer le pool Stratis"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Renommer le système de fichiers"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Renommer le groupe"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "Renommer le groupe $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Renommer le volume logique"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Renommer le groupe de volumes"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Renommer $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "Renommer un groupe peut affecter sudo et les règles similaires"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Repair logical volume $0"
+msgstr "Renommer le volume logique"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Réparer $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Répéter"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Répéter chaque mois"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Répéter la phrase secrète"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Répéter chaque semaine"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Signaler"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Étiquette de rapport"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Rapport à ABRT Analytics"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Signalé ; aucun lien disponible"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Échec de la déclaration"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "La déclaration a été annulée"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Rapports"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Rapports :"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Exiger un changement de mot de passe tous les $0 jours"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Exiger un changement de mot de passe sur $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Exiger un changement de mot de passe à la première connexion"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Requis par"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Requis par "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Nécessite"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Nécessite un accès administrateur pour pouvoir modifier"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Conditions requises"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Requis par"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Mémoire réservée"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Réinitialiser"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Définir le mot de passe"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Redimensionnement $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Il faut déverrouiller le disque pour pouvoir redimensionner un système de "
+"fichier chiffré. Veuillez fournir une phrase secrète actuelle."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Redémarrer"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Redémarrage des services"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Redémarrer les services..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Redémarrage"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Restauration de la connexion"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Reprendre"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Réutiliser le cryptage existant"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Réutilisation du cryptage existant ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+#, fuzzy
+#| msgid "Review crypto policy"
+msgid "Review cryptographic policy"
+msgstr "Revoir la stratégie de chiffrement"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Révision des journaux"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Round-robin"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Routes"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Exécuter à"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Exécuter un nouveau rapport"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Exécuter sur"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Exécuter le rapport"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Exécuteur"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "En cours"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Temps d’exécution"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "Erreurs de contrôle d’accès SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux est désactivé sur le système"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux est désactivé sur le système."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "Stratégie SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "L’état du système SELinux est inconnu."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "Dépannage SELinux"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "SMART auto-test de $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"Le rapport SOS recueille des informations sur le système afin de faciliter "
+"le diagnostic des problèmes."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "Clé SSH"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "La clé SSH n'est pas un chemin"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "Clés SSH"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "Délai de réacheminement STP"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "Durée Hello STP"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "Âge maximal de message STP"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "Priorité STP"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Les utilisateurs de Safari doivent importer et accepter le certificat de "
+"l’AC autosignataire :"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Samedis"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Enregistrer"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Enregistrer et redémarrer"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Enregistrer les modifications"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Économiser de l’espace en compressant les blocs individuels avec LZ4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr ""
+"Économiser de l’espace en stockant une seule fois les blocs de données "
+"identiques"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Pour enregistrer une nouvelle phrase secrète, il faut déverrouiller le "
+"disque. Veuillez fournir une phrase secrète actuelle."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Mise hors tension programmée à $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Redémarrage programmé à $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "PC scellé"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Recherche"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Domaine de recherche"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Recherche de nom ou d'identification"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Recherche par nom, groupe ou ID"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "Seconde doit correspondre à un nombre entre 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Secondes"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Clés Secure Shell"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Effacer en toute sécurité $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Configuration et dépannage de Linux avec renforcement de la sécurité"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Mises à jour de sécurité disponibles"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Mises à jour de sécurité uniquement"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Les mises à jour de sécurité seront appliquées $0 à $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Sélectionner"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Sélectionnez un identifiant"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Sélectionnez la méthode"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Envoyer"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Envoi"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Numéro de série"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Serveur"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Adresse du serveur"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "L’adresse du serveur ne peut pas être vide."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Le serveur ne peut pas être vide."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Le serveur a fermé la connexion."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Logiciels du serveur"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Service"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Le service a une erreur"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Journaux de service"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Services"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Services utilisant l’emplacement"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Session"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "La session est sur le point d’expirer"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Ensemble"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Définir le nom d’hôte"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Créer une partition sur $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Définir le mot de passe"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Régler l’heure"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Mis à"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Installation"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Définir un mot de passe faible"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"La configuration s’écarte de l’état configuré de départ et reviendra à cet "
+"état au prochain démarrage."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Mise en place"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Configuration du périphérique de boucle $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Paramètres"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Gravité"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Partagé"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Interpréteur de commande"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Script d’interpréteur de commande"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Maj+Inser"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "Afficher tous les fils de discussion"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Afficher tous les fils de discussion"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Show confirmation password"
+msgstr "Confirmer le mot de passe"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Afficher les empreintes"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Afficher les messages contenant une chaîne donnée."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Afficher les messages de l’unité systemd spécifiée."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Afficher les messages d’un démarrage spécifique."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Afficher plus de relations"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "New password"
+msgid "Show password"
+msgstr "Nouveau mot de passe"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Afficher les relations"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Réduire"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Réduire le volume logique"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+#, fuzzy
+#| msgid "partition"
+msgid "Shrink partition"
+msgstr "partition"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Réduire le volume"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Fermeture"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Éteindre"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Depuis"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Rang unique"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Taille"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "La taille ne peut pas être négative"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "La taille ne peut pas être nulle"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "La taille est trop grande"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "La taille doit correspondre à un nombre"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "La taille doit être au moins $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Sauter la navigation principale"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Passer au contenu"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Emplacement"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Emplacement $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Instantané"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Prises"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Mises à jour de logiciel"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Les contournements logiciels aident à éviter les problèmes de sécurité du "
+"processeur. Ces mitigations on pour effet de bord de réduire les "
+"performances. Modifiez ces paramètres à vos risques et périls."
+
+#: pkg/storaged/drive/drive.jsx:63
+#, fuzzy
+#| msgid "Solid-State Disk"
+msgid "Solid State Drive"
+msgstr "Disque statique à semi-conducteurs (SSD)"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Solution appliquée avec succès"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "La solution a échoué"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Solutions"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"Un autre programme utilise actuellement le gestionnaire de paquets, veuillez "
+"patienter..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Certains logiciels doivent être redémarrés manuellement"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Trié du moins fiable au plus fiable"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Ordinateur gain de place"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Protocole Spanning Tree"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Protocole Spanning Tree (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "De rechange"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Temps spécifique"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Vitesse"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Stable"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Démarrer"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Démarrer et activer"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Démarrer Multipath"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Démarrer le service"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr ""
+"Commencez à afficher les entrées à la date spécifiée ou plus récente que "
+"celle-ci."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "Commencez à afficher les entrées au plus tard à la date spécifiée."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Démarré"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "Démarrage du périphérique RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Démarrage de swapspace $target"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "État"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Statique"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "État"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Stick PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Persistant"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Arrêter"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Arrêter et désactiver"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Arrêter et retirer"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Arrêter et démonter"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Arrêter le périphérique"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Arrêter de modifier les hôtes"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Arrêter le rapport"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "Arrêt du périphérique RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Arrêt de l’espace d’échange $target"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Stockage"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "Le stockage ne peut pas être géré sur ce système."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Journaux de stockage"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Stocker la phrase secrète"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Phrase secrète stockée"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "$0 périphérique bloc"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "Add block devices"
+msgid "Stratis block devices"
+msgstr "Ajouter des blocs de périphériques"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+#, fuzzy
+#| msgid "VDO backing devices can not be made smaller"
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Les sauvegardes VDO ne peuvent pas être plus petits"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystem"
+msgstr "Créer un système de fichiers"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems"
+msgstr "Créer un système de fichiers"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Créer un système de fichiers"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Pool Stratis"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:97
+#, fuzzy
+#| msgid "Set password"
+msgid "Strong password"
+msgstr "Définir le mot de passe"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Stub"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Style"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Sous-châssis"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Sub-Notebook"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "L’abonnement aux signaux de systemd a échoué : $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "Le volume n’a pas pu être créé"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Copié avec succès dans le presse-papiers"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Copié avec succès dans le presse-papiers !"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Dimanches"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Swap"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "$0 les systèmes de fichiers ne peuvent pas être redimensionnés ici."
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Échanger"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Éteindre $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Éteindre $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Allumer $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Passer à l’accès administrateur"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Passer en accès limité"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"La désactivation de $0 interrompra la connexion au serveur et rendra "
+"l’interface utilisateur d’administration indisponible."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"L’activation de $0 interrompra la connexion au serveur et rendra l’interface "
+"utilisateur d’administration indisponible."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Synchronisé"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Synchronisé avec $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Synchronisation"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "Synchronisation du périphérique RAID $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Système"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Diagnostics du système"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Informations sur le système"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Le système est à jour"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Modifications système"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Heure système"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Unités Systemd"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tablette"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Serveur de clés Tang"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Cible"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Cibles"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Équipe"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Port de l’équipe"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Paramètres du port de l’équipe"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminal"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Terminer la session"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Configuration du test"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr ""
+"Le test n’est disponible que lorsque le service kdump est en cours "
+"d’exécution."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Tester les paramètres de kdump"
+
+#: pkg/kdump/kdump-view.jsx:374
+#, fuzzy
+#| msgid ""
+#| "This will test kdump settings by crashing the kernel and thereby the "
+#| "system. Depending on the settings, the system may not automatically "
+#| "reboot and the process may take a while."
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Ceci testera les paramètres de « kdump » en plantant le noyau et ainsi le "
+"système. Selon les paramètres, le système peut ne pas redémarrer "
+"automatiquement et le processus peut prendre un certain temps."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Test de connexion"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "Le paquet $0 n’est disponible dans aucun référentiel."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "Le paquet $0 doit être installé pour créer des pools Stratis."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "Le paquet $0 doit être installé."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "Le paquet $0 sera installé pour créer des périphériques VDO."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "L’adresse IP ou le nom d’hôte ne peut pas contenir d’espace."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "Le RAID Array est dans un état dégradé"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr ""
+"Le périphérique RAID doit être en cours d’exécution pour pouvoir supprimer "
+"les disques."
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr "La clé SSH $0 de $1 sur $2 sera ajoutée au fichier $3 de $4 sur $5."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"La clé SSH $0 sera disponible pour le reste de la session et sera également "
+"disponible pour la connexion à d’autres hôtes."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"La clé SSH permettant de se connecter à $0 est protégée par un mot de passe, "
+"et l’hôte ne permet pas de se connecter avec un mot de passe. Veuillez "
+"fournir le mot de passe de la clé à $1."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"La clé SSH permettant de se connecter à $0 est protégée. Vous pouvez vous "
+"connecter avec votre mot de passe de connexion ou en fournissant le mot de "
+"passe de la clé à $1."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"Le compte « $0 » sera forcé de changer de mot de passe lors de la prochaine "
+"connexion"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Le service de cockpit est automatiquement inclus"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr "L’état configuré est inconnu, il peut changer au démarrage suivant."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"La création de ce périphérique VDO n’est pas terminée et l’appareil ne peut "
+"pas être utilisé."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"L’utilisateur actuellement connecté n’est pas autorisé à voir les "
+"informations sur les clés."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"Le disque doit être déverrouillé avant d'être formaté. Veuillez fournir une "
+"phrase secrète existante."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "Le fichier $0 sera supprimé."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "Le système de fichiers n’a pas de point de montage permanent."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "Le système de fichiers n’a pas de point de montage permanent."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Le système de fichiers est configuré pour être automatiquement monté au "
+"démarrage mais son conteneur de cryptage ne sera pas déverrouillé à ce "
+"moment-là."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Le système de fichiers est actuellement monté mais ne le sera plus après le "
+"prochain démarrage."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Le système de fichiers est actuellement monté sur $0 mais sera monté sur $1 "
+"au prochain démarrage."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Le système de fichiers est actuellement monté $0 mais ne sera pas monté "
+"après le prochain démarrage."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Le système de fichiers n’est pas actuellement monté mais sera monté au "
+"prochain démarrage."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Le système de fichiers n’est pas monté."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"Le système de fichiers sera déverrouillé et monté au prochain démarrage. "
+"Cela peut nécessiter la saisie d’une phrase secrète."
+
+#: pkg/shell/hosts_dialog.jsx:494
+#, fuzzy
+#| msgid "Show fingerprints"
+msgid "The fingerprint should match:"
+msgstr "Afficher les empreintes"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Le service suivant sera redémarré :"
+msgstr[1] "Les services suivants seront redémarrés :"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "Le nom complet ne doit pas contenir de deux-points."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "L'ID du groupe doit être un nombre entier positif"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Le nom du groupe ne peut contenir que des lettres de a à z, des chiffres, "
+"des points, des tirets et des tirets bas"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"Le répertoire personnel $0 existe déjà. Son propriétaire sera le nouvel "
+"utilisateur."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "L'initrd doit être régénéré."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Le mot de passe de la clé ne peut pas être vide"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Les mots de passe sont différents"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "La clé que vous avez fournie n’était pas valide."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Le dernier logement de clé ne peut pas être retiré"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "Les processus et services listés seront stoppés en force."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "Les processus énumérés seront stoppés en force."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "Les services répertoriés seront stoppés en force."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr ""
+"L’utilisateur actuellement connecté n’est pas autorisé à voir les "
+"modifications système"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "La machine est en train de redémarrer"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "Le point de montage $0 est utilisé par ces processus :"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "Le point de montage $0 est utilisé par ces services :"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Le nouveau mot de passe de la clé ne peut pas être vide"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Le mot de passe ne peut pas être vide"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Le mot de passe ne correspond pas"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Le mot de passe ne correspond pas."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"L’empreinte digitale qui en résulte peut être partagée via des méthodes "
+"publiques, y compris par courrier électronique."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Le serveur a refusé d’authentifier « $0 » en utilisant l’authentification "
+"par mot de passe, et aucune autre méthode d’authentification prise en charge "
+"n’est disponible."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr ""
+"Le serveur a refusé d’authentifier en utilisant des méthodes prises en "
+"charge."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"Le système ne prend pas actuellement en charge le déverrouillage d'un "
+"système de fichiers avec un serveur de clés Tang pendant le démarrage."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"Le système ne prend pas actuellement en charge le déverrouillage du système "
+"de fichiers racine avec un serveur de clés Tang."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+"L’utilisateur $0 n’est pas autorisé à changer les mitigations de sécurité "
+"pour le CPU"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+#, fuzzy
+#| msgid "The user $0 is not permitted to change crypto policies"
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr ""
+"L’utilisateur $0 n’est pas autorisé à modifier les stratégies de chiffrement"
+
+#: pkg/kdump/kdump-view.jsx:505
+#, fuzzy
+#| msgid "The user $0 is not permitted to create timers"
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "L’utilisateur $0 n’a pas le droit de créer de minuteurs"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"L'utilisateur doit se déconnecter et se reconnecter pour que la nouvelle "
+"configuration prenne effet."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Le nom d’utilisateur ne peut contenir que des lettres de az, des chiffres, "
+"des points, des tirets et des traits de soulignement."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"La configuration du navigateur Web empêche l’exécution de Cockpit "
+"(inaccessible $0 )"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Il n’y a actuellement aucune page active"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"Il y a des périphériques avec plusieurs chemins sur le système, mais le "
+"service multipath n’est pas en cours d’exécution."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "Il n’y a pas de services actifs dans cette zone"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Il n’y a aucune clé publique autorisée pour ce compte."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Il y a eu une erreur inattendue lors de la connexion à la machine."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Ces étapes supplémentaires sont nécessaires :"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Ces changements seront effectués :"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Volume logique dynamique"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Pool pour les volumes à provisionnement dynamique"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"Ce montage NFS est en cours d’utilisation et seules ses options peuvent être "
+"modifiées."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Ce périphérique VDO n’utilise pas tout son périphérique de sauvegarde."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Ce périphérique ne peut pas être géré ici."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Ce périphérique ne peut pas être géré ici."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "Ce périphérique est actuellement en cours d’utilisation."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Ce champ ne peut pas être vide"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Ce groupe est le groupe primaire pour les utilisateurs suivants :"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "Cet hôte redémarrera après l’installation des mises à jour."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "Ces informations sont stockées uniquement sur le système."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "Ce volume logique n’est pas complètement utilisé par son contenu."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Cette machine a déjà été ajoutée."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Cela peut prendre un peu de temps"
+
+#: pkg/storaged/partitions/partition.jsx:204
+#, fuzzy
+#| msgid "This logical volume is not completely used by its content."
+msgid "This partition is not completely used by its content."
+msgstr "Ce volume logique n’est pas complètement utilisé par son contenu."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:333
+#, fuzzy
+#| msgid "This VDO device does not use all of its backing device."
+msgid "This pool does not use all the space on its block devices."
+msgstr "Ce périphérique VDO n’utilise pas tout son périphérique de sauvegarde."
+
+#: pkg/storaged/stratis/pool.jsx:348
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "This pool is in a degraded state."
+msgstr "Le RAID Array est dans un état dégradé"
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Ce système n’est pas enregistré"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Ce système utilise un profil personnalisé"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Ce système utilise le profil recommandé"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"Cet outil configure la politique SELinux et peut aider à comprendre et à "
+"résoudre les violations de la politique."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"Cet outil configure le système pour qu'il écrive les vidages de mémoire sur "
+"incidents du noyau sur le disque."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"Cet outil génère une archive des informations de configuration et de "
+"diagnostic du système en cours d'exécution. Ces archives peuvent être "
+"stockées localement ou centralement à des fins d'enregistrement ou de suivi, "
+"ou être envoyées aux représentants du support technique, aux développeurs ou "
+"aux administrateurs système pour les aider dans la recherche d'erreurs "
+"techniques et le débogage."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"Cet outil gère le stockage local, tel que les systèmes de fichiers, les "
+"groupes de volumes LVM2 et les montages NFS."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"Cet outil gère les réseaux tels que les liens, les ponts, les équipes, les "
+"VLAN et les pare-feu en utilisant NetworkManager et Firewalld. "
+"NetworkManager est incompatible avec les scripts par défaut systemd-networkd "
+"d'Ubuntu et ifupdown de Debian."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Cette unité n’est pas conçue pour être activée explicitement."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Cet identifiant existe déjà"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr ""
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Ce navigateur est trop vieux pour faire fonctionner la console Web (manquant "
+"$0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Cela ajoutera une correspondance pour « _BOOT_ID= ». S’ils ne sont pas "
+"spécifiés, les journaux du démarrage en cours seront affichés. Si l’ID de "
+"démarrage est omis, un décalage positif recherchera les amorçages à partir "
+"du début du journal, et un décalage égal ou inférieur à zéro recherchera les "
+"démarrages à partir de la fin du journal. Ainsi, « 1 » indique le premier "
+"amorçage trouvé dans le journal dans l’ordre chronologique, « 2 » le second "
+"et ainsi de suite ; tandis que « -0 » est le dernier démarrage, « -1 » le "
+"démarrage l’avant-dernier, et ainsi de suite."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Cela ajoutera une correspondance pour « _SYSTEMD_UNIT= », « COREDUMP_UNIT= » "
+"et « UNIT= » pour trouver tous les messages possibles pour l’unité donnée. "
+"Peut contenir plus d’unités séparées par une virgule. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Cela vous permettra de vous connecter sans mot de passe à l’avenir."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Cette zone contient le service du cockpit. Assurez-vous que cette zone ne "
+"s’applique pas à votre connexion actuelle à la console Web."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Jeudis"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Niveau"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Temps"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Fuseau horaire"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Échec création minuteur"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "La suppression de la minuterie a échoué"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Minuteurs"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Conseil : associez votre mot de passe clé à votre mot de passe de connexion "
+"pour vous authentifier automatiquement à d’autres systèmes."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Pour vous assurer que votre connexion n’est pas interceptée par un tiers "
+"malveillant, veuillez vérifier l’empreinte de la clé de l’hôte :"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Pour obtenir des mises à jour logicielles, ce système doit être enregistré "
+"auprès de Red Hat, en utilisant le portail client Red Hat ou un serveur "
+"d’abonnement local."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Pour vérifier une empreinte digitale, exécutez les opérations suivantes sur "
+"$0 en étant physiquement assis devant la machine ou en passant par un réseau "
+"de confiance :"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Aujourd’hui"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Basculer"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Basculer le sélecteur de date"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Basculer les filtres"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Trop de données"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Outils"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Top 5 des services CPU"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "Top 5 des services d'utilisation de disque"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "Top 5 des services de mémoire"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Taille totale : $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Tower"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Transitoire"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Transmettre"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Déclencheur"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Déclenché par"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Déclencheurs"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Dépannage"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Dépannage…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+#, fuzzy
+#| msgid "Untrusted host"
+msgid "Trust and add host"
+msgstr "Hôte non sécurisé"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Clé de confiance"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Niveau de confiance"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Réessayer"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Essaye de synchroniser avec $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Mardis"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned n’a pas pu démarrer"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned est un service qui surveille votre système et optimise les "
+"performances sous certaines charges de travail. Le cœur de Tuned est "
+"constitué de profils, qui permettent de régler votre système pour différents "
+"cas d’utilisation."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned n’est pas disponible"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned ne fonctionne pas"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned est désactivé"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Activez l’accès administrateur"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Type"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Le nom ne peut pas contenir le caractère « $0 »."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Tapez pour filtrer"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Cette solution n’a pas pu être appliquée automatiquement"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Incapable de se connecter à cette adresse"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "Impossible de contacter $0."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Le cockpit n’a pas pu contacter l’hôte donné $0. Assurez-vous que SSH "
+"fonctionne sur le port $1 ou spécifiez un autre port dans l’adresse."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Incapable d’obtenir les détails de l’alerte."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Impossible de se connecter à $0 en utilisant l’authentification par clé SSH. "
+"Veuillez fournir le mot de passe. Vous pouvez configurer vos clés SSH pour "
+"une connexion automatique."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Impossible de se connecter à $0. L’hôte n’accepte pas le mot de passe de "
+"connexion ou l’une de vos clés SSH."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Incapable d’atteindre le serveur"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Incapable de supprimer le montage"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+#, fuzzy
+#| msgid "Encrypted logical volume of $0"
+msgid "Unable to repair logical volume $0"
+msgstr "Volume logique chiffré de $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "Impossible d'exécuter le correctif : $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "Impossible d’enregistrer les paramètres"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "Impossible d’enregistrer les paramètres : $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Incapable de démarrer setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Incapable de démonter le système de fichiers"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Non disponible"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Paquets indisponibles"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Annuler"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "Erreur PackageKit inattendue lors de l'installation de $0 : $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Erreur inattendue"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Données non reconnues"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Unité"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Inconnu"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Inconnu « $0 »"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Inconnu ( $0 )"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Application inconnue"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Configuration inconnue"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Nom d’hôte inconnu"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Nom de service inconnu"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Type inconnu"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Déverrouiller"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Déverrouillage automatique au démarrage"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Déverrouiller le pool Stratis crypté"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "Clé de déverrouillage $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Déverrouillage $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Déverrouillage du disque"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Interfaces non gérées"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Démonter"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "Démonter le système de fichiers $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Supprimer le montage maintenant"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Démonter $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Anonyme"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "Débrocher l'unité"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Données non reconnues"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "Unrecognized data can not be made smaller here."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Les données non reconnues ne peuvent pas être plus petites ici."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Les données non reconnues ne peuvent pas être plus petites ici."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Volume non pris en charge"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "Jusqu’à"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Hôte non sécurisé"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Mise à jour"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Mise à jour du tableau des réussites"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Mise à jour Historique"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Mise à jour des informations sur le paquet"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "La mise à jour est réussie"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Actualisé"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr ""
+"Les paquets mis à jour peuvent nécessiter un redémarrage pour prendre effet."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Mises à jour disponibles"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Historique des mises à jour"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Les mises à jour seront appliquées $0 à $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Mise à jour en cours"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Mise à jour du l'état..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Durée de fonctionnement"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Utilisation"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "Utilisation de $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Utiliser"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Utiliser la compression"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Déduplication"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Utiliser la clé"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Utiliser un mot de passe"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Utilisez les clés suivantes pour vous authentifier à d’autres systèmes"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Utiliser la journalisation verbeuse"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Utilisé"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"Utile pour les montages facultatifs ou nécessitant une interaction (comme "
+"les phrases de passe)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Utilisateur"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "Identifiant utilisateur"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr ""
+"L'identifiant de l'utilisateur est déjà utilisé par un autre utilisateur"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "L'identifiant de l'utilisateur doit être un nombre entier positif"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "L'ID de l'utilisateur ne doit pas être supérieur à $0"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "L'ID de l'utilisateur ne doit pas être inférieur à $0"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Nom d’utilisateur"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Le nom d’utilisateur ne peut pas être vide"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Nom d’utilisateur"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "Utiliser le cryptage LUKS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Utilisation du serveur Tang"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "Les sauvegardes VDO ne peuvent pas être plus petits"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "Périphérique VDO $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "Volume du système de fichiers VDO (compression/dédoublonnage)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN ID"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Validation de l’adresse"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Validation du jeton d’authentification"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Fournisseur"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Vérifié"
+
+#: pkg/shell/hosts_dialog.jsx:489
+#, fuzzy
+#| msgid "Fingerprint"
+msgid "Verify fingerprint"
+msgstr "Empreinte de la clé"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Vérifier la clé"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Vérification"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Version"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Effacement très sécurisé de $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Voir tous les processeurs"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Afficher tous les disques"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Voir tous les journaux"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Voir tous les services"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Afficher le script d’automation"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Afficher les journaux détaillés"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Voir les détails du matériel"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Afficher l’historique des connexions"
+
+#: pkg/storaged/stratis/pool.jsx:351
+#, fuzzy
+#| msgid "View all logs"
+msgid "View logs"
+msgstr "Voir tous les journaux"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Voir les métriques et l’historique"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Afficher le débit par disque"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Voir le site du projet"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Voir le rapport"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Afficher le journal des mises à jour"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr ""
+"La visualisation des informations sur la mémoire nécessite un accès "
+"administratif."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Visitez le pare-feu"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Groupe de volumes"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Volume group is missing physical volumes"
+msgstr "Suppression du volume physique de $target"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "La taille du volume est $0. La taille du contenu est $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Attente"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "En attente de détails..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "En attente d’informations…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"En attente d’autres programmes pour terminer l’utilisation du gestionnaire "
+"de paquets..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Attente de la fin des autres opérations de gestion du logiciel"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "En attendant de commencer…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Recherché par"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Recherches"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Avertissement et au-dessus"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+#, fuzzy
+#| msgid "Set weak password"
+msgid "Weak password"
+msgstr "Définir un mot de passe faible"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Console Web"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Console web pour serveurs Linux"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "La console Web va redémarrer"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "La console Web est exécutée en mode accès limité."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Logo de la console web"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Mercredis"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Hebdomadaire"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Semaines"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Quand"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "Lorsqu’il est vide, connectez-vous avec l’utilisateur actuel"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Lorsque la console Web est redémarrée, vous ne verrez plus les informations "
+"relatives à la progression. Toutefois, le processus de mise à jour se "
+"poursuivra en arrière-plan. Reconnectez-vous pour continuer à suivre le "
+"processus de mise à jour."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Blanc"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:124
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "World wide name"
+msgid "World wide name"
+msgstr "WW Nom"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Écriture"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Écriture - le plus souvent"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Écriture"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Mauvais nom d’utilisateur ou mot de passe"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Annuel"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Oui"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Vous vous connectez à $0 pour la première fois."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Vous n’êtes pas autorisé à modifier le pare-feu."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Vous n’êtes pas autorisé à afficher les clés publiques autorisées pour ce "
+"compte."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Vous avez été déconnecté pour cause d’inactivité."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Vous pouvez essayer de charger des entrées plus anciennes."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+"Vous pouvez modifier le mot de passe de la clé de connexion automatique."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Vous devez encore attendre avant de changer votre mot de passe"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr ""
+"Vous devez vous connecter à nouveau pour consulter l’historique des métriques"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Vous avez l’accès administrateur."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Vous serez déconnecté dans $0 secondes."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Votre compte"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"Votre navigateur n’autorise pas le collage à partir du menu contextuel. Vous "
+"pouvez utiliser Maj+Insérer."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr ""
+"Votre navigateur se souviendra de votre niveau d’accès d’une session à "
+"l’autre."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Votre serveur fermera la connexion bientôt. Vous pouvez vous reconnecter "
+"après le redémarrage."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Votre session a été interrompue."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Votre session a expiré. Veuillez vous reconnecter."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Zone"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[données binaires]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[pas de données]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "accès"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "actif"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "greffon"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "greffon"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "après le réseau"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "applis"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "Étiquette d’actif"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "à"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "moyenne : $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "périphérique de sauvegarde pour périphérique VDO"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "bond"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "boot"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "pont"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "Autres périphériques"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "Autres périphériques"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs subvolume"
+msgstr "Volume de stockage"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Volumes de stockage"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "Volume de stockage"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "Correctif de bogue"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "octets"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "Cache"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+#, fuzzy
+#| msgid "Change"
+msgid "change"
+msgstr "Modification"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman n'est pas installé"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "commande"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "console"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "coredump"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "processeur"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "plantage"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "$0 données"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "date"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Désactiver"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "déboguer"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "supprimer"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "désactiver"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "disque"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "disques"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "domaine"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "disque"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "modifier"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "activer"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted"
+msgid "encrypted"
+msgstr "Chiffré"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "chiffrement"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "amélioration"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "erreur"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "tous le Vendredi"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "tous les Lundi"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "tous les Samedi"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "tous les Dimanches"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "tous les Jeudi"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "tous les Mardi"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "tous les Mercredi"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "tous les jours"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "extension"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "échec de listage des clés de l’hôte SSH : $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "système de fichiers"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "pare-feu"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "pour les noyaux actuels et futurs"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "pour le noyau actuel uniquement"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "formater"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "de $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "de $0 sur $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "graphiques"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "développer"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "matériel"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "historique"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "hôte"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "Cible iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "Cibles iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "Ajouter un portail iSCSI"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "ignorer l'échec"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "dans la plupart des navigateurs"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "Incohérent"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "initialiser"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "Installer"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "Interface"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "invalide : plusieurs cibles définies"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "journal"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "page man de journalctl"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "État kdump"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "clés"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "login"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "machine"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "masque"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "max : $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "member of RAID device"
+msgid "member of MDRAID device"
+msgstr "membre du périphérique RAID"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "membre du pool Stratis"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "mémoire"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "métriques"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "mitigation"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "Plus de détails"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "monter"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "réseau"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "ne jamais monter au démarrage"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "L'exportation nfs est vide"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "le serveur nfs est vide"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "Le serveur nfs n'est pas un IPv6 valide"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "nice"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "aucun"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "de $0 CPU"
+msgstr[1] "de $0 CPUs"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "sur $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "système d’exploitation"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "os"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "package"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "partition"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "mot de passe"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "qualité mot de passe"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "correctifs"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "chemin"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "performance"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "volume physique du groupe de volumes LVM2"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "plugin"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "Le service pmlogger.service a échoué"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "Le service pmlogger.service ne parvient pas à collecter des données"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "Le service pmlogger.service n’est pas en cours d’exécution"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "pod"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "port"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "puissance"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "lecture seule"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "conseillé"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "retirer de LVM2"
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "remove from RAID"
+msgid "remove from MDRAID"
+msgstr "retirer du RAID"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "retirer de LVM2"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "redémarrer"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "rôles"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "$0 en cours d’exécution"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "sécurité"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "série"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "service"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "shell"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "montrer moins"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "montrer plus"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "réduire"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "fermer"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "socket"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "solution"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "détails de la solution"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+#, fuzzy
+#| msgid "Reporting failed"
+msgid "sos report failed"
+msgstr "Échec de la déclaration"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "La clé SSH n’est pas un chemin d’accès"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "le serveur SSH est vide"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "arrêter"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "arrêter le démarrage en cas d'échec"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+#, fuzzy
+#| msgid "Stopped"
+msgid "stopped"
+msgstr "Arrêté"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "cible"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "équipe"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "heure"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "minuteur"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "UDP"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "unité"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "inconnu"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "cible inconnue"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "unmask"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "Démonter"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "espace non partitionné sur $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "utilisateur"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "nom d’utilisateur"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#, fuzzy
+#| msgid "No description"
+msgid "using key description $0"
+msgstr "Aucune description"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "version"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "volume"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "avertissement"
+
+#: pkg/networkmanager/wireguard.jsx:84
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "wireguard-tools package is not installed"
+msgstr "PackageKit non installé"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "oui"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "zone"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Le service Kdump n’est pas installé. Veuillez vous assurer que le paquet "
+#~ "kexec-tools est installé."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Pas de mémoire réservée. Ajoutez une option « crashkernel » à la ligne de "
+#~ "commande du noyau (par exemple dans /etc/default/grub) pour réserver de "
+#~ "la mémoire au démarrage. Exemple : crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Le service est en cours d’exécution"
+
+#~ msgid "Service is starting"
+#~ msgstr "Le service s’arrête"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Le service est arrêté"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Le service s’arrête"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Cela testera la configuration de « kdump » en plantant le noyau."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "crashkernel non configuré dans la ligne de commande du noyau"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (chiffré)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 Pool Stratis"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 cache"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 de niveau inconnu"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 disponible"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr ""
+#~ "Un disque de rechange doit être ajouté en premier avant que ce disque "
+#~ "puisse être retiré."
+
+#~ msgid "Backing device"
+#~ msgstr "Périphérique de sauvegarde"
+
+#~ msgid "Block"
+#~ msgstr "Bloc"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Capacité"
+
+#~ msgid "Content"
+#~ msgstr "Contenu"
+
+#~ msgid "Create devices"
+#~ msgstr "Créer des partitions"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Périphérique"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Fichier de périphérique"
+
+#~ msgid "Devices"
+#~ msgstr "Périphériques"
+
+#~ msgid "Drives"
+#~ msgstr "Lecteur"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Les volumes chiffrés doivent être déverrouillés avant de pouvoir être "
+#~ "redimensionnés."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Système de fichiers (crypté)"
+
+#~ msgid "Filesystems"
+#~ msgstr "Systèmes de fichiers"
+
+#~ msgid "Free"
+#~ msgstr "Libre"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Libérez de l’espace dans ce groupe : réduire ou supprimer les autres "
+#~ "volumes logiques ou ajouter un autre volume physique."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Volume inactif"
+
+#, fuzzy
+#~| msgid "Keyserver"
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Serveur de clés"
+
+#~ msgid "LVM2 member"
+#~ msgstr "Membre de LVM2"
+
+#~ msgid "Locked devices"
+#~ msgstr "Périphériques verrouillés"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Données chiffrées verrouillées"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Modèle"
+
+#~ msgid "NFS mounts"
+#~ msgstr "Points de montage NFS"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "Prise en charge NFS non installée"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Aucun point de montage NFS configuré"
+
+#~ msgid "No devices"
+#~ msgstr "Aucun périphérique"
+
+#~ msgid "No drives attached"
+#~ msgstr "Aucun lecteur relié"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Aucune cible iSCSI configurée"
+
+#, fuzzy
+#~| msgid "Block device for filesystems"
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Périphérique bloc pour système de fichiers"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Autre informations"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Périphérique en mode bloc partitionné"
+
+#, fuzzy
+#~| msgid "Passphrase"
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Phrase secrète"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "Les volumes physiques ne peuvent pas être redimensionnés ici."
+
+#~ msgid "Pool"
+#~ msgstr "Pool"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Pool pour les volumes dynamiques"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "Niveau RAID"
+
+#~ msgid "RAID member"
+#~ msgstr "Membre RAID"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Lecteur amovible"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Afficher le périphérique $0"
+#~ msgstr[1] "Afficher tous les périphériques $0"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Afficher $0 lecteur"
+#~ msgstr[1] "Afficher tous les lecteurs $0"
+
+#~ msgid "Show all"
+#~ msgstr "Tout afficher"
+
+#~ msgid "Source"
+#~ msgstr "La source"
+
+#~ msgid "Start pool"
+#~ msgstr "Démarrer le pool"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Démarrer le pool pour voir les systèmes de fichiers."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "État"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Arrêt du pool Stratis"
+
+#~ msgid "Stratis member"
+#~ msgstr "Membre de Stratis"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Pool Stratis $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "La prise en charge est installée."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "Le périphérique RAID doit être en cours d’exécution pour pouvoir ajouter "
+#~ "des disques supplémentaires."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "Le dernier disque d’un périphérique RAID ne peut pas être supprimé."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr ""
+#~ "Le dernier volume physique d’un groupe de volumes ne peut pas être "
+#~ "supprimé."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Il n’y a pas assez d’espace libre ailleurs pour supprimer ce volume "
+#~ "physique. Il faut au moins $0 plus d’espace libre."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr ""
+#~ "Ce disque ne peut pas être retiré pendant le recouvrement du périphérique."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr "Ce volume doit être activé avant de pouvoir être redimensionné."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Données non reconnues"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Utilisation"
+
+#~ msgid "Used for"
+#~ msgstr "Utilisé pour"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "Sauvegarde VDO"
+
+#~ msgid "VDO device"
+#~ msgstr "Périphérique VDO"
+
+#~ msgid "Volume"
+#~ msgstr "Volume"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automatique (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Accepter la clé et se connecter"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Les volumes chiffrés ne peuvent pas être redimensionnés ici."
+
+#, fuzzy
+#~| msgid "Tang keyserver"
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Serveur de clés Tang"
+
+#, fuzzy
+#~| msgid "New passphrase"
+#~ msgid "Use a passphrase"
+#~ msgstr "Nouvelle phrase secrète"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitmap"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Ce pool ne peut pas être déverrouillé ici car la description de sa clé "
+#~ "n’est pas dans le format attendu."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Basculer l’image bitmap"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Assurez-vous que le hachage de la clé du serveur Tang corresponde à l’un "
+#~ "des suivants :"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Vérifier manuellement avec SSH : "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Le numéro de port et son type ne correspondent pas"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Pool Stratis verrouillé et chiffré"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Erreur lors de la suppression de l’alerte : $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Incapable d’obtenir l’alerte : $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[ $0 octets de données binaires]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Pointe d’entrée/sortie de disque"
+
+#~ msgid "Event"
+#~ msgstr "Événement"
+
+#~ msgid "Event logs"
+#~ msgstr "Journaux événements"
+
+#~ msgid "Load spike"
+#~ msgstr "Pointe de charge"
+
+#~ msgid "Memory spike"
+#~ msgstr "Pic de mémoire"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Pointe d’E/S du réseau"
+
+#~ msgid "ssh key"
+#~ msgstr "clé SSH"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Si cette option est cochée, le système de fichiers ne sera pas monté lors "
+#~ "du prochain démarrage, même s’il a été monté avant. Ceci est utile si le "
+#~ "montage pendant le démarrage n’est pas possible, par exemple lorsqu’une "
+#~ "phrase secrète est nécessaire pour déverrouiller le système de fichiers "
+#~ "mais que le démarrage est sans surveillance."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Ne jamais monter au démarrage"
+
+#~ msgid "Listing unit files"
+#~ msgstr "Lister les fichiers d’unités"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "L’énumération des fichiers d’unités a échoué : $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "Unité introuvable"
+
+#~ msgid "Validating key"
+#~ msgstr "Clé de validation"
+
+#, fuzzy
+#~| msgid "Delete $0 volume"
+#~| msgid_plural "Delete $0 volumes"
+#~ msgid "Delete $0 group"
+#~ msgstr "Supprimer le volume $0"
+
+#~ msgid "Container administrator"
+#~ msgstr "Administrateur de conteneur"
+
+#~ msgid "Image builder"
+#~ msgstr "Image Builder"
+
+#~ msgid "Roles"
+#~ msgstr "Les rôles"
+
+#~ msgid "Server administrator"
+#~ msgstr "Administrateur de serveur"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Groupe Unix : $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Copié avec succès sur le clavier"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Rapports de diagnostic"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Dump du noyau"
+
+#~ msgid "Software Updates"
+#~ msgstr "Mises à jour logicielles"
+
+#~ msgid "Update log"
+#~ msgstr "Journaux de la mise à jour"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr ""
+#~ "La cible du dump NFS n’a pas été formatée sous la forme : serveur:chemin"
+
+#~ msgid "$0 Zone"
+#~ msgstr "$0 Zone"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Créer un rapport de diagnostic"
+
+#~ msgid "Done!"
+#~ msgstr "Terminé !"
+
+#~ msgid "Download report"
+#~ msgstr "Télécharger le rapport"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Aucune archive n’a été créée."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Veuillez confirmer la suppression de $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "L’archive générée contient des données considérées comme sensibles et son "
+#~ "contenu doit être examiné par l’organisation d’origine avant d’être "
+#~ "transmis à un tiers."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Cet outil collecte les informations de configuration et de diagnostic du "
+#~ "système à partir de ce système afin de les utiliser pour diagnostiquer "
+#~ "les problèmes du système."
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "Il manque au serveur le paquet cockpit-system"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Ce paquet n’est pas compatible avec cette version de Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Ce paquet nécessite la version Cockpit %s ou supérieure"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Mesure des performances"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "lecture seule"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB total"
+
+#~ msgid "Apply"
+#~ msgstr "Appliquer"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Non autorisé à supprimer la zone $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Cliquez à nouveau sur $0 pour l'utiliser malgré tout."
+
+#~ msgid "Access"
+#~ msgstr "Accès"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "DISQUE EN ÉCHEC"
+
+#~ msgid "Logical Size"
+#~ msgstr "Taille logique"
+
+#~ msgid "Security updates "
+#~ msgstr "Mises à jour de sécurité "
+
+#~ msgid "Updates "
+#~ msgstr "Mises à jour "
+
+#~ msgid "(Optional)"
+#~ msgstr "(facultatif)"
+
+#~ msgid "Active since"
+#~ msgstr "Actif depuis"
+
+#~ msgid "Affected locations"
+#~ msgstr "Endroits touchés"
+
+#~ msgid "Process"
+#~ msgstr "Processus"
+
+#~ msgid "Remove and delete"
+#~ msgstr "Enlever et supprimer"
+
+#~ msgid "Remove and format"
+#~ msgstr "Supprimer et formater"
+
+#~ msgid "Remove and grow"
+#~ msgstr "Enlever et augmenter"
+
+#~ msgid "Remove and initialize"
+#~ msgstr "Retirer et initialiser"
+
+#~ msgid "Remove and shrink"
+#~ msgstr "Enlever et rétrécir"
+
+#~ msgid "Remove and stop device"
+#~ msgstr "Retirer et arrêter le périphérique"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Le système de fichiers est utilisé par les sessions en cours et les "
+#~ "services système. Si vous continuez, vous les interromprez."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Le système de fichiers est utilisé par des sessions en cours. Si vous "
+#~ "continuez, vous les interromprez."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Le système de fichiers est utilisé par les sessions en cours et les "
+#~ "services système. Si vous continuez, vous les interromprez."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Cet appareil possède des systèmes de fichiers actuellement utilisés. Si "
+#~ "vous continuez, vous démontrez ainsi tous les systèmes de fichiers sur "
+#~ "celui-ci."
+
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr ""
+#~ "Ce périphérique est actuellement utilisé pour les groupes de volumes LVM2."
+
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Ce périphérique est actuellement utilisé pour les groupes de volumes "
+#~ "LVM2. En procédant, il sera retiré de ses groupes de volumes."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Cet appareil est actuellement utilisé pour les appareils RAID."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Cet appareil est actuellement utilisé pour les appareils RAID. Le "
+#~ "processus va le retirer de ses périphériques RAID."
+
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Ce dispositif est actuellement utilisé pour les piscines Stratis."
+
+#~ msgid "Unmount and delete"
+#~ msgstr "Démonter et supprimer"
+
+#~ msgid "Unmount and format"
+#~ msgstr "Démonter et formater"
+
+#~ msgid "Unmount and grow"
+#~ msgstr "Démontage et augmentation"
+
+#~ msgid "Unmount and initialize"
+#~ msgstr "Démontage et initialisation"
+
+#~ msgid "Unmount and shrink"
+#~ msgstr "Démontage et rétraction"
+
+#~ msgid "Unmount and stop device"
+#~ msgstr "Démonter et arrêter le périphérique"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 of $1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "Bloqué"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Volume physique de $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Membre de RAID Device $0"
+
+#~ msgid "Menu"
+#~ msgstr "Menu"
+
+#~ msgid "VDO backing"
+#~ msgstr "Sauvegarde VDO"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Création du pool de stockage"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "Options de montage"
+
+#, fuzzy
+#~| msgid "Reset Storage Pool"
+#~ msgid "Stratis Pool"
+#~ msgstr "Réinitialiser le pool de stockage"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Un disque est nécessaire."
+
+#~ msgid "Disk"
+#~ msgstr "Disque"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr ""
+#~ "Pour les anciennes applications uniquement. Réduit les performances."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Installer le support VDO"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Utiliser l’émulation 512 octets"
+
+#~ msgid "System services"
+#~ msgstr "Services système"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Créer un rapport de diagnostic "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr ""
+#~ "La connexion simultanée à plus de {{ limit }} machines n’est pas prise en "
+#~ "charge."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "SSO basé sur Kerberos"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Connectez-vous à {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Les nouveaux mots de passe clés ne correspondent pas"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "Pour vérifier une empreinte digitale, exécutez les opérations suivantes "
+#~ "sur {{host}} en étant physiquement assis devant la machine ou en passant "
+#~ "par un réseau de confiance :"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Impossible de contacter {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Impossible de se connecter à {{#strong}}{{host}}{{/strong}}. Pour plus "
+#~ "d'options d'authentification et de support de dépannage, veuillez mettre "
+#~ "à niveau le cockpit-ws vers une version plus récente."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Impossible de se connecter à {{#strong}}{{host}}{{/strong}}. Pour vous "
+#~ "connecter à cet hôte, vous devrez activer l'une des méthodes "
+#~ "d'authentification suivantes dans la configuration de sshd sur {{#strong}}"
+#~ "{{host}}{{/strong}} :"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "Vous vous connectez à {{host}} pour la première fois."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "{{host}} clé modifiée"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Supprimer la configuration du point de montage"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "La création de cette liaison interrompra la connexion au serveur et "
+#~ "rendra l’interface utilisateur d’administration indisponible."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "La création de ce pont interrompra la connexion au serveur et rendra "
+#~ "l’interface utilisateur d’administration indisponible."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "La création de cette équipe interrompra la connexion au serveur et rendra "
+#~ "l’interface utilisateur d’administration indisponible."
+
+#~ msgid "IP settings"
+#~ msgstr "Paramètres IP"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Éteindre <b>$0</b> interrompra la connexion au serveur et rendra "
+#~ "l’interface utilisateur d’administration indisponible."
+
+#~ msgid "Administrator password"
+#~ msgstr "Mot de passe administrateur"
+
+#~ msgid "Computer OU"
+#~ msgstr "OU ordinateur"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr ""
+#~ "La suppression d’un périphérique RAID effacera toutes les données qu’il "
+#~ "contient."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr ""
+#~ "La suppression d’un groupe de volumes effacera toutes les données qu’il "
+#~ "contient."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Ne pas écraser les données existantes"
+
+#~ msgid "Erase"
+#~ msgstr "Effacer"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Formater le disque $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr ""
+#~ "Le formatage d’un périphérique de stockage effacera toutes les données "
+#~ "qu’il contient."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Le nom de l’hôte ne devrait pas être changé dans un domaine"
+
+#~ msgid "More"
+#~ msgstr "Plus"
+
+#~ msgid "Not joined"
+#~ msgstr "Non joint"
+
+#~ msgid "One time password"
+#~ msgstr "Mot de passe à usage unique"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 de $1 sur $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Les heures doivent être un nombre compris entre 0 et 23"
+
+#~ msgid "Last login:"
+#~ msgstr "Dernière connexion :"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Les minutes doivent correspondre à un nombre compris entre 0 et 59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "Il y a eu $0 tentative de connexion échouée depuis la dernière connexion "
+#~ "réussie."
+#~ msgstr[1] ""
+#~ "Il y a eu $0 tentatives de connexion échouées depuis la dernière "
+#~ "connexion réussie."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "par ex. \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 zones actives"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 occurrence"
+#~ msgstr[1] "$0 occurrences"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Licencié sous :"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Déverrouiller au démarrage"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Déverrouiller en lecture seule"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Recherchez les journaux avec une combinaison de termes :"
+
+#~ msgid "Show filters"
+#~ msgstr "Afficher les filtres"
+
+#~ msgid "Text"
+#~ msgstr "Texte"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "toute chaîne comme expression rationnelle"
+
+#~ msgid "e.g."
+#~ msgstr "par ex."
+
+#~ msgid "log fields"
+#~ msgstr "champs du journal"
+
+#~ msgid "qualifiers"
+#~ msgstr "qualifiants"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "Basculer les paramètres de la session"
+
+#~ msgid "(none)"
+#~ msgstr "(aucun)"
+
+#~ msgid "Create timers"
+#~ msgstr "Créer des timers"
+
+#~ msgid "Recommended default"
+#~ msgstr "Valeur par défaut recommandée"
+
+#~ msgid "Run"
+#~ msgstr "Exécuter"
+
+#~ msgid "Select unit state"
+#~ msgstr "Sélectionnez l'état de l'unité"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Activer les métriques stockées"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Activer les métriques stockées"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Forcer la suppression de la phrase de chiffrement dans $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Si « tang-show-keys » n’est pas disponible, exécutez ce qui suit :"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Que faire si les touches tang-show ne sont pas disponibles ?"
+
+#~ msgid "key slot $0"
+#~ msgstr "logement de clé $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Quelque chose s’est mal passé"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "Cela n’a pas fonctionné, veuillez réessayer"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "Vous ne pouvez pas obtenir l’accès administrateur."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Les mises à jour automatiques ne sont pas mises en place"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "$0 Configuration du CPU"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 Réseau"
+#~ msgstr[1] "$0 Réseaux"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 est disponible pour la plupart des systèmes d’exploitation. Pour "
+#~ "l’installer, recherchez-le dans le logiciel GNOME ou exécutez les "
+#~ "opérations suivantes :"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 ajustement de la mémoire"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 réseau"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPU"
+#~ msgstr[1] "$0 vCPUs"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 détails vCPU"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 les paramètres de l'interface réseau virtuelle"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "Activer le pool de stockage pour gérer les volumes"
+
+#~ msgid "Add network interface"
+#~ msgstr "Ajouter une interface réseau"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Ajouter une interface de réseau virtuel"
+
+#~ msgid "Additional"
+#~ msgstr "Additionnel"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Adresse ne faisant pas partie du sous-réseau"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr ""
+#~ "Après la suppression de l'instantané, tout son contenu capturé sera perdu."
+
+#~ msgid "Always attach"
+#~ msgstr "Toujours attacher"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Le fait de l’attacher rendra ce disque partageable pour chaque VM qui "
+#~ "l'utilise."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Lancement automatique de libvirt au démarrage"
+
+#~ msgid "Autostart"
+#~ msgstr "Démarrage automatique"
+
+#~ msgid "Boot order"
+#~ msgstr "Ordre d’amorçage"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Les paramètres d’ordre d’amorçage n’ont pas pu être enregistrés"
+
+#~ msgid "Bus"
+#~ msgstr "Bus"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "Disque CD/DVD"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "La configuration du CPU n'a pas pu être sauvegardée"
+
+#~ msgid "CPU type"
+#~ msgstr "Type de CPU"
+
+#~ msgid "Change firmware"
+#~ msgstr "Changer de firmware"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Ces modifications prendront effet après l'extinction de la VM"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Choisissez un système d’exploitation"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "En cliquant sur \"Launch Remote Viewer\" (lancer l’afficheur à distance), "
+#~ "vous téléchargerez un fichier .vv et le lancerez $0 ."
+
+#~ msgid "Clone"
+#~ msgstr "Clone"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Impossible de charger l’image"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Confirmer cette action"
+
+#~ msgid "Connect"
+#~ msgstr "Connecter"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr ""
+#~ "Connectez-vous à n'importe quelle application de visualisation pour les "
+#~ "protocoles suivants"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Connexion au service de virtualisation"
+
+#~ msgid "Connection"
+#~ msgstr "Connexion"
+
+#~ msgid "Console"
+#~ msgstr "Console"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Cœurs par prise"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "N'a pas pu revenir à l'instantané"
+
+#~ msgid "Crashed"
+#~ msgstr "Planté"
+
+#~ msgid "Create VM"
+#~ msgstr "Créer une VM"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Créer un clone VM basé sur $0"
+
+#~ msgid "Create new"
+#~ msgstr "Créer Nouveau"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Créer une nouvelle machine virtuelle"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Créer un réseau virtuel"
+
+#~ msgid "Creating VM"
+#~ msgstr "Création de la VM"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "Création de l’installation de la VM"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "La création de la VM $0 a échoué"
+
+#~ msgid "Creation time"
+#~ msgstr "Temps de création"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Allocation actuelle"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Micrologiciel personnalisé : $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "Plage DHCP"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Supprimer les fichiers de stockage associés :"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Supprimer le pool de stockage $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Supprimer les volumes de ce pool"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Supprimer un pool de stockage inactif va seulement déprogrammer le pool. "
+#~ "Son contenu ne sera pas supprimé."
+
+#~ msgid "Desktop viewer"
+#~ msgstr "Affichage de bureau"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Détachez les disques utilisant ce pool de toutes les VM avant de tenter "
+#~ "la suppression."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Déconnecté de la console série. Cliquez sur le bouton Connecter."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Échec du détachement du disque $0 de la VM $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Le disque n’a pas pu être attaché"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Le disque n’a pas pu être créé"
+
+#~ msgid "Disk image file"
+#~ msgstr "Fichier image disque"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr ""
+#~ "Les paramètres de configuration du disque n’ont pas pu être sauvegardés"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "Instantané du disque uniquement"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "Le domaine s'est planté"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "Le domaine est bloqué sur la ressource"
+
+#~ msgid "Download an OS"
+#~ msgstr "Télécharger un OS"
+
+#~ msgid "Dying"
+#~ msgstr "Sur le point ce cesser toute activité"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Machine émulée"
+
+#~ msgid "End"
+#~ msgstr "Fin"
+
+#~ msgid "End should not be empty"
+#~ msgstr "L’extrémité ne doit pas être vide"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Image disque existante sur le système de fichiers de l’hôte"
+
+#~ msgid "Expand"
+#~ msgstr "Développer"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Échec de la modification du micrologiciel"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr ""
+#~ "Impossible de récupérer les adresses IP des interfaces présentes dans $0"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "Échec de l’envoi de la touche Ctrl+Alt+$0 à la VM $1"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr ""
+#~ "Un nombre inférieur au nombre maximal de processeurs virtuels doit être "
+#~ "activé."
+
+#~ msgid "File"
+#~ msgstr "Fichier"
+
+#~ msgid "Filter by name"
+#~ msgstr "Filtrer par nom"
+
+#~ msgid "Firmware"
+#~ msgstr "Micrologiciel"
+
+#~ msgid "Force shut down"
+#~ msgstr "Arrêt forcé"
+
+#~ msgid "Forward mode"
+#~ msgstr "Mode de redirection"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Mode de redirection"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Générer automatiquement"
+
+#~ msgid "GiB"
+#~ msgstr "Go"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "Aller à la liste des VM"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Cacher les options supplémentaires"
+
+#~ msgid "Host device"
+#~ msgstr "Périphérique hôte"
+
+#~ msgid "Host name"
+#~ msgstr "Nom de l'hôte"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Le nom d’hôte n’est peut-être pas vide"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Détails sur l’hyperviseur"
+
+#~ msgid "IP configuration"
+#~ msgstr "Configuration IP"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 et IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "Réseau IPv4"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "Le réseau IPv4 ne doit pas être vide"
+
+#~ msgid "IPv4 only"
+#~ msgstr "IPv4 seulement"
+
+#~ msgid "IPv6 address"
+#~ msgstr "Adresse IPv6"
+
+#~ msgid "IPv6 network"
+#~ msgstr "Réseau IPv6"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "Le réseau IPv6 ne doit pas être vide"
+
+#~ msgid "IPv6 only"
+#~ msgstr "IPv6 seulement"
+
+#~ msgid "Idle"
+#~ msgstr "inactif"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Démarrer immédiatement la VM"
+
+#~ msgid "Import"
+#~ msgstr "Importer"
+
+#~ msgid "Import VM"
+#~ msgstr "Importer une VM"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "Importer une machine virtuelle"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "Dans la plupart des configurations, « macvtap » ne fonctionne pas pour "
+#~ "les communications en réseau entre hôte et invité."
+
+#~ msgid "Initiator"
+#~ msgstr "Initiateur"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "L’initiateur IQN ne doit pas rester vide"
+
+#~ msgid "Installation source"
+#~ msgstr "Source d’installation"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "La source de l’installation ne doit pas être vide"
+
+#~ msgid "Installation type"
+#~ msgstr "Type d’installation"
+
+#~ msgid "Interface type"
+#~ msgstr "Type d’interface"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Longueur de masque ou de préfixe IPv4 non valide"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Adresse IPv6 non valide"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Adresse IPv6 non valide"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Nom de fichier non valide"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "Lancer l’afficheur à distance"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr ""
+#~ "Laissez le mot de passe vide si vous ne souhaitez pas qu’un compte root "
+#~ "soit créé"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "Laissez le mot de passe vide si vous ne souhaitez pas qu’un compte "
+#~ "d’utilisateur soit créé"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr ""
+#~ "Laissez le mot de passe vide si vous ne souhaitez pas qu’un compte root "
+#~ "soit créé"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt n’a détecté aucune image de micrologiciel UEFI/OVMF installée sur "
+#~ "l’hôte"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt ou l’hyperviseur ne prend pas en charge UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "Chargement des ressources"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "La machine doit être arrêtée avant de changer de type de bus"
+
+#, fuzzy
+#~| msgid "Machine must be shut off before changing bus type"
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "La machine doit être arrêtée avant de changer de type de bus"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Gestion des machines virtuelles"
+
+#~ msgid "Manual connection"
+#~ msgstr "Connexion manuelle"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Masque ou Longueur du préfixe"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Le masque ou la longueur du préfixe ne doit pas être vide"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Allocation maximale"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "La mémoire maximale n’a pas pu être sauvegardée"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Nombre maximum de CPU virtuels alloués pour le système d'exploitation "
+#~ "invité"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Nombre maximum de CPU virtuels alloués pour l’OS invité, doit être "
+#~ "compris entre 1 et $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Unité maximale de transmission (MTU)"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "La mémoire n’a pas pu être sauvegardée"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "La mémoire ne doit pas être 0"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Type de modèle"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT vers $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "Échec du changement d’état du NIC $0 de la VM $1"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Le nom contient des caractères invalides"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Le nom ne doit pas être vide"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Le nom ne doit pas être vide"
+
+#~ msgid "Name: "
+#~ msgstr "Nom : "
+
+#~ msgid "Netmask"
+#~ msgstr "Masque réseau"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Échec de l’activation du réseau $0"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Échec de la désactivation du réseau $0"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Démarrage réseau (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Système de fichiers par réseau (NFS)"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Les paramètres de l’interface réseau n’ont pas pu être enregistrés"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "Le réseau sélectionné ne prend pas en charge PXE."
+
+#~ msgid "Networks"
+#~ msgstr "Réseaux"
+
+#~ msgid "New volume name"
+#~ msgstr "Nouveau nom de volume"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr ""
+#~ "Aucune machine virtuelle n’est en cours d’exécution ou définie sur cet "
+#~ "hôte"
+
+#~ msgid "No connection available"
+#~ msgstr "Aucune connexion disponible"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Aucun disque défini pour cette machine virtuelle"
+
+#~ msgid "No network devices"
+#~ msgstr "Aucun périphérique réseau disponible"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Aucune interface réseau définie pour cette machine virtuelle"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "Aucun réseau n’est défini sur cet hôte"
+
+#~ msgid "No networks available"
+#~ msgstr "Aucun réseau disponible"
+
+#~ msgid "No parent"
+#~ msgstr "Pas de parent"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Aucun instantané défini pour cette VM"
+
+#~ msgid "No state"
+#~ msgstr "Pas d'État"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "Aucun pool de stockage n’a été défini sur cet hôte"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Aucun pool de stockage disponible"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "Aucun volume de stockage n’a été défini pour ce pool de stockage"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Aucun réseau virtuel"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "Un réseau non persistant ne peut pas être supprimé. Il cesse d’exister "
+#~ "quand il est désactivé."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "Un réseau non persistant ne peut pas être supprimé. Il cesse d’exister "
+#~ "quand il est désactivé."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Aucune (réseau isolé)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Un ou plusieurs modules sélectionnés sont utiliés par les domaines. "
+#~ "Détachez d’abords les disques pour permettre la suppression du volume."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Modifiable uniquement quand l’invité est fermé"
+
+#~ msgid "Open"
+#~ msgstr "Ouvrir"
+
+#~ msgid "Operating system"
+#~ msgstr "Système d’exploitation"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "Opération en cours"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "Aperçu des parents"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Chemin d’accès sur le système de fichiers de l’hôte"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Chemin d’accès au fichier ISO sur le système de fichiers de l’hôte"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Chemin d’accès au fichier ISO sur le système de fichiers de l’hôte"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Chemin d’accès au fichier ISO sur le système de fichiers de l’hôte"
+
+#~ msgid "Paused"
+#~ msgstr "Mis en pause"
+
+#~ msgid "Persistence"
+#~ msgstr "Persistance"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Périphérique de disque physique"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Périphérique de disque physique sur l’hôte"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Veuillez choisir un pool de stockage"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "Veuillez choisir un volume"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Veuillez saisir un nouveau nom de volume"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Veuillez démarrer la machine virtuelle pour accéder à sa console."
+
+#~ msgid "Plug"
+#~ msgstr "Connecteur"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "Le pool doit être actif pour créer du volume"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "Le type de pool ne supporte pas la création de volume"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "Les volumes de pool sont utilisés par les VM "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Nombre choisi de sockets à exposer à l’invité."
+
+#~ msgid "Prefix"
+#~ msgstr "Préfixe"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "La longueur du préfixe ne doit pas être vide"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "Les clichés pris précédemment vous permettent de revenir à un état "
+#~ "antérieur si quelque chose ne va pas"
+
+#~ msgid "Product"
+#~ msgstr "Produit"
+
+#~ msgid "Profile"
+#~ msgstr "Profil"
+
+#~ msgid "Protocol"
+#~ msgstr "Protocole"
+
+#~ msgid "Remote URL"
+#~ msgstr "URL distante"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "Détails de la visionneuse à distance"
+
+#~ msgid "Revert"
+#~ msgstr "Rétablir"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Revenir à l'instantané $0"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "Le retour à cet instantané ramènera la VM au moment de l'instantané et "
+#~ "l'état actuel sera perdu, ainsi que toutes les données non saisies dans "
+#~ "un instantané"
+
+#~ msgid "Root password"
+#~ msgstr "Mot de passe administrateur"
+
+#~ msgid "Route to $0"
+#~ msgstr "Routage vers $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Exécuter une installation sans surveillance"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Démarrer quand l’hôte est amorcé"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "Port SPICE TLS"
+
+#~ msgid "SPICE address"
+#~ msgstr "Adresse SPICE"
+
+#~ msgid "SPICE port"
+#~ msgstr "Port SPICE"
+
+#~ msgid "Select console type"
+#~ msgstr "Sélectionnez le type de console"
+
+#~ msgid "Send key"
+#~ msgstr "Envoi de la clé"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Envoyer une interruption non masquable"
+
+#~ msgid "Serial console"
+#~ msgstr "Console série"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Définir plage DHCP"
+
+#~ msgid "Set manually"
+#~ msgstr "Définir manuellement"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "La définition des mots de passe utilisateur pour les installations sans "
+#~ "surveillance nécessite le démarrage de la VM lors de sa création"
+
+#~ msgid "Show additional options"
+#~ msgstr "Actions supplémentaires"
+
+#~ msgid "Shut off"
+#~ msgstr "Éteindre"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "Éteindre la VM afin de modifier la configuration du micrologiciel"
+
+#~ msgid "Shutting down"
+#~ msgstr "Fermeture"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "L'instantané n'a pas été créé"
+
+#~ msgid "Source format"
+#~ msgstr "Format de la source"
+
+#~ msgid "Source path"
+#~ msgstr "Chemin d’accès à la source"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Le chemin de la source ne doit pas être vide"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "La source devrait commencer par le protocole http, ftp ou nfs"
+
+#~ msgid "Source volume group"
+#~ msgstr "Renommer le groupe de volumes"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Lancer libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Démarrer le pool quand l’hôte est amorcé"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Le démarrage ne doit pas être vide"
+
+#~ msgid "Startup"
+#~ msgstr "Démarrage"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "Échec de l’activation du pool de stockage $0"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "Échec de la désactivation du pool de stockage $0"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Le pool de stockage n’a pas pu être créé"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Nom du pool de stockage"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "La taille de stockage ne doit pas être de 0"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr ""
+#~ "La taille du volume de stockage ne doit pas dépasser la capacité du pool "
+#~ "de stockage ($0 $1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Les volumes de stockage n’ont pas pu être supprimés"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "Mode Attente (PM)"
+
+#~ msgid "Target path"
+#~ msgstr "Chemin cible"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Le chemin cible ne doit pas rester vide"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr ""
+#~ "La VM est en cours d’exécution, son arrêt sera forcé avant la suppression."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr "La VM doit être en marche ou arrêtée pour détacher ce périphérique"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "Le répertoire du serveur à exporter"
+
+#~ msgid "The pool is empty"
+#~ msgstr "La pool est vide"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr ""
+#~ "Le système d’exécution sélectionné ne prend pas en charge les "
+#~ "installations sans surveillance"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr ""
+#~ "Le système d’exploitation sélectionné a un besoin en mémoire minimum de "
+#~ "$0 $1"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "Le système d’exécution sélectionné a un besoin en mémoire minimum de $0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "Le pool de stockage n’a pas pu être supprimé"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr ""
+#~ "Cette VM est temporaire. Éteignez-la si vous souhaitez la supprimer."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Ce volume est déjà utilisé par : "
+
+#~ msgid "Threads per core"
+#~ msgstr "Threads par noyaux"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr ""
+#~ "Les machines virtuelles temporaires ne permettent pas de modifier la "
+#~ "configuration du micrologiciel"
+
+#~ msgid "Type ID"
+#~ msgstr "ID type"
+
+#~ msgid "Unique name"
+#~ msgstr "Nom unique"
+
+#~ msgid "Unique network name"
+#~ msgstr "Nom du réseau unique"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Micrologiciel inconnu"
+
+#~ msgid "Unplug"
+#~ msgstr "Débrancher"
+
+#~ msgid "Url"
+#~ msgstr "Url"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr ""
+#~ "Les paramètres de configuration de VCPU n’ont pas pu être sauvegardés"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "VM $0 existe déjà"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "VM $0 n'existe pas sur la connexion $1"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "Échec du redémarrage forcé de la VM $0"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "Échec de l’arrêt forcé de la VM $0"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "Échec de la suppression de la VM $0"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "Échec de l’installation de la VM $0"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "Échec du redémarrage de la VM $0"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "Échec de la reprise de la VM $0"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "Échec de l’envoi NMI de la VM $0"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "Échec de l’arrêt de la VM $0"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "Échec du démarrage de la VM $0"
+
+#~ msgid "VM state"
+#~ msgstr "État VM"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "Port VNC TLS"
+
+#~ msgid "VNC address"
+#~ msgstr "Adresse VNC"
+
+#~ msgid "VNC console"
+#~ msgstr "Console VNC"
+
+#~ msgid "VNC port"
+#~ msgstr "Port VNC"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Machines virtuelles"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Machines virtuelles"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Gestion des machines virtuelles"
+
+#~ msgid "Virtual network"
+#~ msgstr "Réseau virtuel"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Le réseau virtuel n’a pas pu être créé"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Le service de virtualisation (libvirt) n’est pas actif"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Le nom du groupe de volumes ne doit pas être vide"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Ecrivable"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Ecrivable et partagé"
+
+#~ msgid "Yesterday"
+#~ msgstr "Hier"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "Vous devez sélectionner le système d’exploitation le plus proche"
+
+#~ msgid "cdrom"
+#~ msgstr "CD ROM"
+
+#~ msgid "disabled"
+#~ msgstr "désactivé"
+
+#~ msgid "down"
+#~ msgstr "arrêté"
+
+#~ msgid "enabled"
+#~ msgstr "activée"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "périphérique hôte"
+
+#~ msgid "host passthrough"
+#~ msgstr "host passthrough"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "Cible directe iSCSI"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "iSCSI Initiator IQN"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "Cible iSCSI IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "non"
+
+#~ msgid "no state saved"
+#~ msgstr "aucun état n'est sauvé"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "périphérique redirigé"
+
+#~ msgid "server"
+#~ msgstr "serveur"
+
+#~ msgid "up"
+#~ msgstr "en cours"
+
+#~ msgid "vCPU count"
+#~ msgstr "Nombre de vCPU"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU Maximum"
+
+#~ msgid "vCPUs"
+#~ msgstr "vCPU"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#~ msgid "view more..."
+#~ msgstr "voir plus..."
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Le paquet virt-install doit être installé sur le système pour pouvoir "
+#~ "créer les nouvelles VM"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Le paquet virt-install doit être installé sur le système pour pouvoir "
+#~ "créer les nouvelles VM"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Le paquet virt-install doit être installé sur le système pour pouvoir "
+#~ "créer les nouvelles VM"
+
+#~ msgid "vm"
+#~ msgstr "vm"
+
+#~ msgid "Local install media"
+#~ msgstr "Média d’installation local"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "Veuillez définir un mot de passe root"
+
+#~ msgid "21st"
+#~ msgstr "21ème"
+
+#~ msgid "22nd"
+#~ msgstr "22ème"
+
+#~ msgid "23rd"
+#~ msgstr "23ème"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Après"
+
+#~ msgid "Friday"
+#~ msgstr "Vendredi"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Heure : Minute"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "L’heure doit être un nombre compris entre 0 et 23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Format de date incorrect."
+
+#~ msgid "Invalid number."
+#~ msgstr "Numéro non valide."
+
+#~ msgid "Monday"
+#~ msgstr "Lundi"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Répéter toutes les heures"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Répéter chaque année"
+
+#~ msgid "Saturday"
+#~ msgstr "Samedi"
+
+#~ msgid "Sunday"
+#~ msgstr "Dimanche"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Ce jour n’est pas présent tous les mois.<br> Le minuteur ne sera exécuté "
+#~ "que pour les mois qui ont un 31ème jour."
+
+#~ msgid "Thursday"
+#~ msgstr "Jeudi"
+
+#~ msgid "Tuesday"
+#~ msgstr "Mardi"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 mise à jour"
+#~ msgstr[1] "$0 mises à jour"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 correctif de sécurité"
+#~ msgstr[1] "$1 correctifs de sécurité"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Gestion des appareils de stockage"
+
+#~ msgid "Restart now"
+#~ msgstr "Redémarrer maintenant"
+
+#~ msgid "Configure"
+#~ msgstr "Configurer"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr ""
+#~ "L’amorçage réseau est disponible uniquement lorsque vous utilisez une "
+#~ "connexion système"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Réseau"
+
+#~ msgid "No snapshots"
+#~ msgstr "Pas d’instantané"
+
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "Aucun fichier de ce type n’a été trouvé dans le répertoire $0"
+
+#~ msgid "No updates pending"
+#~ msgstr "Aucune mise à jour en attente"
+
+#~ msgid "This directory is empty"
+#~ msgstr "Ce répertoire est vide"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr ""
+#~ "Ce type de pool ne prend pas en charge la création de Volumes de stockage"
+
+#~ msgid "undefined"
+#~ msgstr "indéfini"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Clé hôte incorrecte"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "L’authenticité de l’hôte {{#strong}}{{host}}{{/strong}} ne peut pas être "
+#~ "établie. Êtes-vous sûr de vouloir continuer à vous connecter ?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "La clé de {{#strong}}{{host}}{{/strong}} ne correspond pas à la clé "
+#~ "précédemment utilisée. À moins que cette machine ait été récemment "
+#~ "remplacée, il est probable que quelqu’un tente d’attaquer votre connexion "
+#~ "à cette machine."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Clé d’hôte inconnue"
+
+#~ msgid "Address:"
+#~ msgstr "Adresse :"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Au moins $0 disques sont nécessaires."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr ""
+#~ "Connectez-vous avec n’importe quelle application SPICE ou visionneuse VNC."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Télécharger le MSI depuis $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Console graphique (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Console graphique dans la visionneuse de bureau"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Aucune console définie pour cette machine virtuelle."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid ""
+#~ "For the host, either specify the hostname, IP address, an alias name or a "
+#~ "unique resource identifier for the SSH destination."
+#~ msgstr ""
+#~ "Pour l’hôte, spécifiez le nom d’hôte, l’adresse IP, un nom d’alias ou un "
+#~ "identificateur de ressource pour la destination SSH."
+
+#~ msgid ""
+#~ "Specify the host and the login user account for the host that you want to "
+#~ "add."
+#~ msgstr ""
+#~ "Spécifiez l’hôte et le nom du compte utilisateur pour l’hôte que vous "
+#~ "voulez ajouter."
+
+#~ msgid "Entry"
+#~ msgstr "Entrée"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Votre navigateur se déconnectera, mais cela n’affectera pas le processus "
+#~ "de mise à jour, vous pourrez vous reconnecter dans quelques instants pour "
+#~ "continuer à suivre la progression."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "et redémarrez la machine automatiquement."
+
+#~ msgid "Dashboard"
+#~ msgstr "Tableau de bord"
+
+#~ msgid "Description input text"
+#~ msgstr "Texte de la description"
+
+#~ msgid "FAILED"
+#~ msgstr "ÉCHOUÉ"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Connexion perdue. Essayer de se reconnecter"
+
+#~ msgid "Name input text"
+#~ msgstr "Champ texte du nom"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Réseau"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Veuillez saisir une nouvelle taille de volume"
+
+#~ msgid "Servers"
+#~ msgstr "Les serveurs"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "Vous êtes actuellement connecté directement à ce serveur. Vous ne pouvez "
+#~ "pas le supprimer."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bits"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 octet"
+#~ msgstr[1] "$0 octets"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 octet"
+#~ msgstr[1] "$0 octets"
+
+#~ msgid "Bond settings "
+#~ msgstr "Paramètres de liaison "
+
+#~ msgid "IP settings "
+#~ msgstr "Paramètres IP "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Cockpit a rencontré une erreur interne inattendue. <br/><br/> Vous pouvez "
+#~ "essayer de redémarrer Cockpit en actualisant la fenêtre de votre "
+#~ "navigateur. La console JavaScript contient des détails sur cette erreur "
+#~ "(<b>Ctrl-Maj-J</b> dans la plupart des navigateurs)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "La VM a planté."
+
+#~ msgid "The VM is down."
+#~ msgstr "La VM est éteinte."
+
+#~ msgid "The VM is going down."
+#~ msgstr "La VM est en baisse."
+
+#~ msgid "The VM is idle."
+#~ msgstr "La VM est inactive."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr ""
+#~ "La VM est en train de mourir (l’arrêt ou le plantage n’est pas terminé)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "La machine virtuelle est en pause."
+
+#~ msgid "The VM is running."
+#~ msgstr "La VM est en cours d’exécution."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr ""
+#~ "La machine virtuelle est suspendue par la gestion de l’alimentation de "
+#~ "l’invité."
+
+#~ msgid "inactive"
+#~ msgstr "inactif"
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Laissez vide pour vous connecter à cette machine en tant qu’utilisateur "
+#~ "actuellement connecté. Si vous saisissez un nom d’utilisateur différent, "
+#~ "cet utilisateur sera toujours utilisé lors de la connexion à cette "
+#~ "machine."
+
+#~ msgid "2 min"
+#~ msgstr "2 minutes"
+
+#~ msgid "3 min"
+#~ msgstr "3 minutes"
+
+#~ msgid "4 min"
+#~ msgstr "4 minutes"
+
+#~ msgid "CPU graph"
+#~ msgstr "Graphique du CPU"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "Statut du processeur"
+
+#~ msgid "Cached"
+#~ msgstr "En cache"
+
+#~ msgid "Graphs"
+#~ msgstr "Graphiques"
+
+#~ msgid "I/O wait"
+#~ msgstr "I / O Attente"
+
+#~ msgid "Kernel"
+#~ msgstr "Noyau"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Mémoire"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "Usage Mémoire & Swap"
+
+#~ msgid "Memory graph"
+#~ msgstr "Graphique de mémoire"
+
+#~ msgid "Network traffic"
+#~ msgstr "Trafic réseau"
+
+#~ msgid "Nice"
+#~ msgstr "Priorité"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Synchroniser les utilisateurs"
+
+#~ msgid "Usage graphs"
+#~ msgstr "Graphiques d’utilisation"
+
+#~ msgid "View graphs"
+#~ msgstr "Voir les graphiques"
+
+#~ msgid "idle"
+#~ msgstr "inactif"
+
+#~ msgid "paused"
+#~ msgstr "mis en pause"
+
+#~ msgid "running"
+#~ msgstr "en cours"
+
+#~ msgid "shut off"
+#~ msgstr "éteindre"
+
+#~ msgid "shutdown"
+#~ msgstr "fermeture"
+
+#~ msgid "Add a new host"
+#~ msgstr "Ajouter un nouvel hôte"
+
+#~ msgid "Available"
+#~ msgstr "Disponible"
+
+#, fuzzy
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Saisir l’adresse IP ou le nom d’hôte"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Interfaces réseau"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Cette version de cockpit-ws ne prend pas en charge la connexion à un hôte "
+#~ "avec un autre utilisateur ou port"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Pour essayer un port différent, vous devrez mettre à niveau cockpit-ws à "
+#~ "une version plus récente."
+
+#~ msgid "$0 template"
+#~ msgstr "$0 Modèle"
+
+#~ msgid "Instance of template: "
+#~ msgstr "Instance du modèle : "
+
+#~ msgid "Instantiate"
+#~ msgstr "Instancier"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 actions"
+
+#~ msgid "All In One"
+#~ msgstr "Tout en un"
+
+#~ msgid "Always"
+#~ msgstr "Toujours"
+
+#~ msgid "Author"
+#~ msgstr "Auteur"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Mauvaises données transmises pour le mécanisme passwd1"
+
+#~ msgid "CPU priority"
+#~ msgstr "Priorité en CPU"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Impossible de se connecter à Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Modifier les limites de ressources"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Modifier les limites de ressources"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit n’a pas pu se connecter à {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit n’a pas pu se connecter à {{#strong}}{{host}}{{/strong}}. Vous "
+#~ "pouvez modifier vos identifiants ci-dessous. {{#can_sync}} Vous pouvez "
+#~ "préférer {{#sync_link}} synchroniser les comptes et les mots de passe {{/"
+#~ "sync_link}}.{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Utilisation de la mémoire combinée"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Utilisation combinée de $0 noyau CPU"
+#~ msgstr[1] "Utilisation combinée de $0 noyaux CPU"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "La commande ne peut pas être vide"
+
+#~ msgid "Command:"
+#~ msgstr "Commande :"
+
+#~ msgid "Commit"
+#~ msgstr "Enregistrer"
+
+#~ msgid "Commit image"
+#~ msgstr "Enregistrer l’image"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Connexion à Docker"
+
+#~ msgid "Container name"
+#~ msgstr "Nom du conteneur"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Le conteneur est actuellement marqué comme n’étant pas en cours "
+#~ "d’exécution, mais l’arrêt normal a échoué."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Le conteneur est en cours d’exécution."
+
+#~ msgid "Container:"
+#~ msgstr "Conteneur :"
+
+#~ msgid "Containers"
+#~ msgstr "Conteneurs"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Conteneurs"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Impossible de modifier les groupes d’utilisateurs"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Impossible de modifier le mot de passe utilisateur"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Impossible de se connecter à la machine"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Impossible de créer de nouveaux utilisateurs"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Impossible de répertorier les utilisateurs locaux"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Impossible de lister les utilisateurs"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Impossible de charger les données utilisateur"
+
+#~ msgid "Created:"
+#~ msgstr "Créé :"
+
+#~ msgid "Docker containers"
+#~ msgstr "Conteneurs de docker"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker n’est pas installé ou activé sur le système"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Alias en double"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Port dupliqué"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "Entrez la phrase de passe pour ajouter l’identité à ssh-agent"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Saisir un mot de passe différent signifie que vous devrez le ressaisir à "
+#~ "chaque fois que vous vous reconnectez à cette machine"
+
+#~ msgid "Environment"
+#~ msgstr "Environnement"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Erreur lors du chargement des utilisateurs : {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Message d’erreur de Docker :"
+
+#~ msgid "Everything"
+#~ msgstr "Tout"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Sortie $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Exposer les ports conteneurs"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Échec du démarrage de Docker : $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Échec de l’arrêt de Docker scope : $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Obtenir une nouvelle image"
+
+#~ msgid "IP address:"
+#~ msgstr "Adresse IP :"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "Longueur du préfixe IP :"
+
+#~ msgid "Id"
+#~ msgstr "Id"
+
+#~ msgid "Id:"
+#~ msgstr "Id :"
+
+#~ msgid "Image"
+#~ msgstr "Image"
+
+#~ msgid "Image $0"
+#~ msgstr "Image $0"
+
+#~ msgid "Image search"
+#~ msgstr "Recherche d’image"
+
+#~ msgid "Image:"
+#~ msgstr "Image :"
+
+#~ msgid "Images"
+#~ msgstr "Images"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Images"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Images et conteneurs en cours"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Pour synchroniser les utilisateurs, vous devez vous connecter à "
+#~ "{{#strong}}{{host}}{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Port non valide"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Ticket Kerberos"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Laissez vide pour vous connecter à cette machine en tant qu’utilisateur "
+#~ "actuellement connecté user{{#default_user}} ({{default_user}}){{/"
+#~ "default_user}}.. Si vous saisissez un nom d’utilisateur différent, cet "
+#~ "utilisateur sera toujours utilisé pour se connecter à cette machine."
+
+#~ msgid "Link to another container"
+#~ msgstr "Lien vers un autre conteneur"
+
+#~ msgid "Links:"
+#~ msgstr "Liens :"
+
+#~ msgid "Login Password"
+#~ msgstr "Mot de passe"
+
+#~ msgid "MAC address:"
+#~ msgstr "Adresse Mac :"
+
+#~ msgid "Memory limit"
+#~ msgstr "Limite de mémoire"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Monter les volumes de conteneur"
+
+#~ msgid "No container specified"
+#~ msgstr "Aucun conteneur n’est renseigné"
+
+#~ msgid "No containers"
+#~ msgstr "Aucun conteneur"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Aucun conteneur correspondant au filtre actuel"
+
+#~ msgid "No images"
+#~ msgstr "Pas d’images"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Aucune image correspondant au filtre actuel"
+
+#~ msgid "No results for $0"
+#~ msgstr "Aucun résultat pour $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Aucune image correspondant au filtre actuel"
+
+#~ msgid "No running containers"
+#~ msgstr "Aucun conteneur en cours d’exécution"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Aucun conteneur en cours d’exécution correspondant au filtre actuel"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Non autorisé à accéder à Docker sur ce système"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "En cas d’échec, réessayez $0 fois"
+#~ msgstr[1] "En cas d’échec, réessayez $0 fois"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Veuillez confirmer la suppression forcée de $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Veuillez essayer un autre terme"
+
+#~ msgid "Ports:"
+#~ msgstr "Ports :"
+
+#~ msgid "Problems"
+#~ msgstr "Problèmes"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Lecture seulement"
+
+#~ msgid "ReadWrite"
+#~ msgstr "LectureÉcriture"
+
+#~ msgid "Repository"
+#~ msgstr "Référentiel"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Redémarrer la stratégie :"
+
+#~ msgid "Retries:"
+#~ msgstr "Tentatives :"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Réutiliser mon mot de passe pour les connexions à distance"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Sélectionnez les utilisateurs que vous souhaitez synchroniser avec "
+#~ "{{#strong}}{{host}}{{/strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Définir les variables d’environnement du conteneur"
+
+#~ msgid "Severity:"
+#~ msgstr "Gravité :"
+
+#~ msgid "Show all containers"
+#~ msgstr "Afficher tous les conteneurs"
+
+#~ msgid "Start Docker"
+#~ msgstr "Démarrer Docker"
+
+#~ msgid "State:"
+#~ msgstr "État :"
+
+#~ msgid "Synchronize"
+#~ msgstr "Synchroniser"
+
+#~ msgid "Tag"
+#~ msgstr "Balise"
+
+#~ msgid "Tags"
+#~ msgstr "Balises"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "Les conteneurs suivants dépendent de cette image et deviendront "
+#~ "inutilisables."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Cette image n’existe pas."
+
+#~ msgid "Type a password"
+#~ msgstr "Tapez un mot de passe"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Tapez pour filtrer…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "Sauf si arrêté"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Mécanisme d’installation non pris en charge"
+
+#~ msgid "Up since $0"
+#~ msgstr "En cours depuis $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Utilisé par les conteneurs"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Utilisation des identifiants disponibles"
+
+#~ msgid "Volumes"
+#~ msgstr "Volumes"
+
+#~ msgid "Volumes:"
+#~ msgstr "Volumes :"
+
+#~ msgid "With terminal"
+#~ msgstr "Avec terminal"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Vous êtes connecté à {{#strong}}{{host}}{{/strong}}, mais pour "
+#~ "synchroniser les utilisateurs, un utilisateur avec des privilèges de "
+#~ "super-utilisateur est requis."
+
+#~ msgid "default"
+#~ msgstr "par défaut"
+
+#~ msgid "key"
+#~ msgstr "clé"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "recherche par nom, espace de noms ou description"
+
+#~ msgid "shares"
+#~ msgstr "shares"
+
+#~ msgid "to host port"
+#~ msgstr "accueillir le port"
+
+#~ msgid "value"
+#~ msgstr "valeur"
+
+#~ msgid "Master"
+#~ msgstr "Master"
+
+#~ msgid "Password for $0"
+#~ msgstr "Mot de passe pour $0"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "L’utilisateur <b>$0</b> n’est pas autorisé à gérer les serveurs"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Comptes"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "L’utilisateur <b>$0</b> n’est pas autorisé à modifier les comptes"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Incapable de supprimer le compte root"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Incapable de renommer le compte root"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "Non autorisé à ajouter une nouvelle zone"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "Non autorisé à ajouter des services à la zone $0"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "Non autorisé à retirer le service $0"
+
+#~ msgid "Account Settings"
+#~ msgstr "Paramètres du compte"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Ajouter une machine au tableau de bord"
+
+#~ msgid "Machines"
+#~ msgstr "Machines"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "La connexion a escaladé les privilèges administrateur"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Mot de passe non utilisable pour des tâches privilégiées ou pour se "
+#~ "connecter à d’autres machines"
+
+#~ msgid "Privileged"
+#~ msgstr "Privilégié"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Réutiliser mon mot de passe pour les tâches privilégiées et me connecter "
+#~ "à d’autres machines"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr "L’utilisateur $0 n’est pas autorisé à modifier les noms d’hôte"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr ""
+#~ "L’utilisateur $0n’est pas autorisé à arrêter ou redémarrer ce serveur"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "L’utilisateur <b>$0</b> n’a pas le droit de modifier les profils"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "L’utilisateur <b>$0</b> n’est pas autorisé à gérer le stockage"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr ""
+#~ "L’utilisateur <b>$0</b> n’est pas autorisé à modifier les paramètres "
+#~ "réseau"
+
+#~ msgid "Required by $0"
+#~ msgstr "Requis par $0"
+
+#~ msgid "Automatic Startup"
+#~ msgstr "Démarrage automatique"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Dernier déclencheur"
+
+#~ msgid "Next Run"
+#~ msgstr "Prochaine exécution"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr ""
+#~ "L’utilisateur <b>$0</b> n’a pas les autorisations pour créer des timers"
+
+#~ msgid "Connecting"
+#~ msgstr "Connexion en cours"
+
+#~ msgid "About Cockpit"
+#~ msgstr "À propos de Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (partagé avec l’OS)"
+
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 vulnérabilité"
+#~ msgstr[1] "$0 vulnérabilités"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Ajouter un espace de stockage supplémentaire"
+
+#~ msgid "Add Storage"
+#~ msgstr "Ajouter du stockage"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Stockage supplémentaire"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Toutes les données sur les disques sélectionnés seront effacées et les "
+#~ "disques seront ajoutés au pool de stockage."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Configurer le stockage…"
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Impossible d’ajouter tous les disques"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Effacer les conteneurs et réinitialiser le pool de stockage"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr ""
+#~ "Les informations sur le pool de stockage Docker ne sont pas disponibles."
+
+#~ msgid "Local Disks"
+#~ msgstr "Disques locaux"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "Aucun espace de stockage local supplémentaire trouvé."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Reformatez et ajoutez des disques"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "La réinitialisation du pool de stockage efface tous les conteneurs et "
+#~ "libère des disques dans le pool."
+
+#~ msgid "Security"
+#~ msgstr "Sécurité"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr "Le pool de stockage Docker ne peut pas être géré sur ce système."
+
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "Le scan de $time ( $type ) a trouvé $count vulnérabilité :"
+#~ msgstr[1] "Le scan de $time ( $type ) a trouvé $count vulnérabilités :"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "Le scan de $time ( $type ) n’a trouvé aucune vulnérabilité."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "Le scan de $time ( $type ) n’a pas réussi."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Le service de virtualisation est disponible"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "Vous n’êtes pas autorisé à gérer le pool de stockage Docker."
+
+#~ msgid "$0 GiB / $1 GiB"
+#~ msgstr "$0 GiB / $1 GiB"
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip} : ${hport} -> $cport"
+
+#~ msgid "Hide Performance Options"
+#~ msgstr "Masquer les options de performance"
+
+#~ msgid "Show Performance Options"
+#~ msgstr "Afficher les options de performance"
diff --git a/po/he.po b/po/he.po
new file mode 100644
index 0000000..a6b5d3e
--- /dev/null
+++ b/po/he.po
@@ -0,0 +1,12992 @@
+# #-#-#-#-# cockpit.c.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Yaron Shahrabani <sh.yaron@gmail.com>, 2020.
+# Anonymous <noreply@weblate.org>, 2020.
+#
+# #-#-#-#-# cockpit.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# #-#-#-#-# cockpit.appstream.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# #-#-#-#-# cockpit.polkit.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-11-22 21:01+0000\n"
+"Last-Translator: Yaron Shahrabani <sh.yaron@gmail.com>\n"
+"Language-Team: Hebrew <https://translate.fedoraproject.org/projects/cockpit/"
+"main/he/>\n"
+"Language: he\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && "
+"n % 10 == 0) ? 2 : 3));\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "מס׳ משתמשים"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "מעבד אחד"
+msgstr[1] "$0 מעבדים"
+msgstr[2] "$0 מעבדים"
+msgstr[3] "$0 מעבדים"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "אזור פעיל"
+msgstr[1] "שני אזורים פעילים"
+msgstr[2] "$0 אזורים פעילים"
+msgstr[3] "$0 אזורים פעילים"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 זמינים"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "אי אפשר להגדיל $0 ממערכות הקבצים."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "אי אפשר להקטין $0 ממערכות הקבצים."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "אי אפשר לשנות כאן גודל של $0 ממערכות הקבצים."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "אי אפשר לשנות כאן גודל של $0 ממערכות הקבצים."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "גודל נתח $0"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "פגיעה חמורה אחת"
+msgstr[1] "$0 פגיעות, כולל חמורות"
+msgstr[2] "$0 פגיעות, כולל חמורות"
+msgstr[3] "$0 פגיעות, כולל חמורות"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 נתונים + $1 תוספת מנוצלים מתוך $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "יום"
+msgstr[1] "יומיים"
+msgstr[2] "$0 ימים"
+msgstr[3] "$0 ימים"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "כונן אחד חסר"
+msgstr[1] "$0 כוננים חסרים"
+msgstr[2] "$0 כוננים חסרים"
+msgstr[3] "$0 כוננים חסרים"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "כונן אחד חסר"
+msgstr[1] "$0 כוננים חסרים"
+msgstr[2] "$0 כוננים חסרים"
+msgstr[3] "$0 כוננים חסרים"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 כוננים"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "התיעוד של $0"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "שגיאת $0"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 הסתיים עם הקוד $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 נכשל"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "ניסיון כניסה שנכשל"
+msgstr[1] "שני ניסיונות כניסה שנכשלו"
+msgstr[2] "$0 ניסיונות כניסה שנכשלו"
+msgstr[3] "$0 ניסיונות כניסה שנכשלו"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "מערכת קבצים $0"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 פנויים"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "שעה"
+msgstr[1] "שעתיים"
+msgstr[2] "$0 שעות"
+msgstr[3] "$0 שעות"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "פגיעה חשובה אחת"
+msgstr[1] "$0 פגיעות חשובות"
+msgstr[2] "$0 פגיעות חשובות"
+msgstr[3] "$0 פגיעות חשובות"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 הוא קובץ קיים"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 בשימוש"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 אינו זמין מאף מאגר."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "המפתח $0 השתנה"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 נקטל עם האות $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "פגיעה אחת בדרגת חומרה נמוכה"
+msgstr[1] "$0 פגיעות בדרגת חומרה נמוכה"
+msgstr[2] "$0 פגיעות בדרגת חומרה נמוכה"
+msgstr[3] "$0 פגיעות בדרגת חומרה נמוכה"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "דקה"
+msgstr[1] "$0 דקות"
+msgstr[2] "$0 דקות"
+msgstr[3] "$0 דקות"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "פגיעה אחת בינונית"
+msgstr[1] "$0 פגיעות, כולל בינוניות"
+msgstr[2] "$0 פגיעות, כולל בינוניות"
+msgstr[3] "$0 פגיעות, כולל בינוניות"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "חודש"
+msgstr[1] "חודשיים"
+msgstr[2] "$0 חודשים"
+msgstr[3] "$0 חודשים"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 נוספים…"
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "חבילה אחת"
+msgstr[1] "$0 חבילות"
+msgstr[2] "$0 חבילות"
+msgstr[3] "$0 חבילות"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "חבילה אחת דורשת הפעלה מחדש של המערכת"
+msgstr[1] "$0 חבילות דורשות הפעלה מחדש של המערכת"
+msgstr[2] "$0 חבילות דורשות הפעלה מחדש של המערכת"
+msgstr[3] "$0 חבילות דורשות הפעלה מחדש של המערכת"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "עמוד אחד"
+msgstr[1] "$0 עמודים"
+msgstr[2] "$0 עמודים"
+msgstr[3] "$0 עמודים"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "מחיצה"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "יש עדכון אבטחה"
+msgstr[1] "יש $0 עדכוני אבטחה"
+msgstr[2] "יש $0 עדכוני אבטחה"
+msgstr[3] "יש $0 עדכוני אבטחה"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "שירות אחד נכשל"
+msgstr[1] "$0 שירותים נכשלו"
+msgstr[2] "$0 שירותים נכשלו"
+msgstr[3] "$0 שירותים נכשלו"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "יש להפעיל מחדש שירות אחד"
+msgstr[1] "יש להפעיל מחדש $0 שירותים"
+msgstr[2] "יש להפעיל מחדש $0 שירותים"
+msgstr[3] "יש להפעיל מחדש $0 שירותים"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "נותרה משבצת אחת"
+msgstr[1] "נותרו $0 משבצות"
+msgstr[2] "נותרו $0 משבצות"
+msgstr[3] "נותרו $0 משבצות"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "קפיצה פתאומית אחת"
+msgstr[1] "$0 קפיצות פתאומיות"
+msgstr[2] "$0 קפיצות פתאומיות"
+msgstr[3] "$0 קפיצות פתאומיות"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0 מסונכרן"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 סך הכול"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "עדכון אחד זמין"
+msgstr[1] "$0 עדכונים זמינים"
+msgstr[2] "$0 עדכונים זמינים"
+msgstr[3] "$0 עדכונים זמינים"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 בשימוש מתוך $1 ($2 נחסכו)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "שבוע"
+msgstr[1] "שבועיים"
+msgstr[2] "$0 שבועות"
+msgstr[3] "$0 שבועות"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 יותקן."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "שנה"
+msgstr[1] "שנתיים"
+msgstr[2] "$0 שנים"
+msgstr[3] "$0 שנים"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "אזור $0"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: קריסה ב־$1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (מ־$host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "נוצרת מחיצה $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "נקודת עגינה מקומית"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "לא מעוגן"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(מומלץ)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", כולל תיקון אבטחה אחד"
+msgstr[1] ", כולל $0 תיקוני אבטחה"
+msgstr[2] ", כולל $0 תיקוני אבטחה"
+msgstr[3] ", כולל $0 תיקוני אבטחה"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "יום"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "שעה"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "דקה"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "דקה"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "שבוע"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "עשירי"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "אחד עשר"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "שניים עשר"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "שלושה עשר"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "ארבעה עשר"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 דקות"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "חמישה עשר"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "שישה עשר"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "שבעה עשר"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "שמונה עשר"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "תשעה עשר"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "אחד"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 דקות"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "עשרים"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "עשרים ואחד"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "עשרים ושניים"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "עשרים ושלושה"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "עשרים וארבעה"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "עשרים וחמישה"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "עשרים ושישה"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "עשרים ושבעה"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "עשרים ושמונה"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "עשרים ותשעה"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "שניים"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "שלושים"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "שלושים ואחד"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "שלושה"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 דקות"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "ארבעה"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 דקות"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 דקות"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "חמישה"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 שעות"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 דקות"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "שישה"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "שבעה"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "שמונה"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "תשעה"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "לא מותקנת גרסה תואמת של Cockpit ב־$0."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "כבר קיימת מערכת קבצים בשם הזה במאגר הזה."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "כבר קיימת קבוצה בשם הזה"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr "נדרש דפדפן עדכני לטובת אבטחה, אמינות וביצועים."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"מאגד רשת משלב מספר מנשקי רשת למנשק לוגי אחד עם קצב העברה גבוה יותר או יתירות."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"מפתח SSH חדש תחת $0 ייווצר עבור $1 על גבי $2 והוא יתווסף לקובץ $3 של $4 על "
+"גבי $5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] "חבילה אחת דורשת הפעלה מחדש כדי שהשינויים ייכנסו לתוקף:"
+msgstr[1] "$0 חבילות דורשות הפעלה מחדש כדי שהשינויים ייכנסו לתוקף:"
+msgstr[2] "$0 חבילות דורשות הפעלה מחדש כדי שהשינויים ייכנסו לתוקף:"
+msgstr[3] "$0 חבילות דורשות הפעלה מחדש כדי שהשינויים ייכנסו לתוקף:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "כבר קיים מאגר בשם הזה."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] "יש שירות שעליך להפעיל מחדש כדי שהשינויים ייכנסו לתוקף:"
+msgstr[1] "יש שירותים שעליך להפעיל מחדש כדי שהשינויים ייכנסו לתוקף:"
+msgstr[2] "יש שירותים שעליך להפעיל מחדש כדי שהשינויים ייכנסו לתוקף:"
+msgstr[3] "יש שירותים שעליך להפעיל מחדש כדי שהשינויים ייכנסו לתוקף:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "לא ניתן לשנות שם לקבוצת כרכים שחסרים לה כרכים פיזיים."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "מעקב אחר ARP"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "פינג ARP"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "על המסוף המקוון"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "חסר"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "לקבל את המפתח ולהיכנס"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "סיסמה מקובלת"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "תפוגת תוקף חשבון"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "החשבון לא זמין או שאין אפשרות לערוך אותו."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "חשבונות"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "פעולה"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "פעולות"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "הפעלה"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "$target מופעל"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "פעיל"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "גיבוי פעיל"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "עמודים פעילים"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "פעיל מאז "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "מצב פעיל"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "איזון עומס מסתגל"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "איזון עומס העברה מסתגל"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "הוספה"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "הוספת $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "הוספת שרת DNS"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "הוספת הצפנת כונן תלוית רשת"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "הוספת שרת מפתחות Tang"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "הוספת VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "הוספת VPN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "הוספת VPN מסוג WireGuard"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "הוספת מפת סיביות"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "הוספת אזור חדש"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "הוספת כתובת"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "הוספת התקני בלוק"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "הוספת מאגד"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "הוספת גשר"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "הוספת כונן"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "הוספת כוננים"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "הוספת שער גישה ל־iSCSI"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "הוספת מפתח"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "הוספת שרת מפתחות"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "הוספת חבר"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "הוספת מארח חדש"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "הוספת אזור חדש"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "הוספת מילת צופן"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "הוספת עמית"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "Physical volume"
+msgid "Add physical volume"
+msgstr "כרך פיזי"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "הוספת פתחות"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "הוספת פתחות לאזור $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "הוספת מפתח ציבורי"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "הוספת נתיב"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "הוספת שם תחום לחיפוש"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "הוספת שירותים"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "הוספת שירותים לאזור $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "הוספת שירותים לאזור $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "הוספת צוות"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "הוספת אזור"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "„$0” נוספה לאפשרויות הצפנה"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "„$0” נוספת לאפשרויות מערכת הקבצים"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "הוספת $0 תקטע את החיבור לשרת ותגרום לכך שמנשק הניהול לא יהיה זמין."
+
+#: pkg/storaged/stratis/pool.jsx:468
+#, fuzzy
+#| msgid ""
+#| "Saving a new passphrase requires unlocking the disk. Please provide a "
+#| "current disk passphrase."
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"שמירת מילת צופן חדשה דורשת את שחרור הכונן. נא לספק את מילת הצופן הנוכחית של "
+"הכונן."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"הוספת פתחות מותאמות אישית תטען מחדש את firewalld. טעינה מחדש תגרום לאבדן של "
+"הגדרות שנקבעו לזמן הריצה בלבד!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "נוסף מפתח"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "הוספת כרך פיזי אל $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "rd.neednet=1 מתווספת לשורת הפקודה של הליבה"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "$val נוסף ב־DNS"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "חיפוש שמות תחום DNS נוספים $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "פעולות נוספות"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "כתובת נוספת $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "חבילות נוספות:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "פתחות נוספות"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "כתובת"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "כתובת $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "הכתובת לא יכולה להיות ריקה"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "הכתובת אינה תקנית"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "כתובות"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "הכתובות אינן בתבנית נכונה"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "ניהול עם המסוף המקוון Cockpit"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "גישה ניהולית"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr "נדרשת גישה ניהולית כדי ליצור ולגשת לדוחות."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "נדרשת גישה ניהולית"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "TCA מתקדם"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "אחרי"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"לאחר עזיבת שם התחום, רק משתמשים עם פרטי גישה מקומיים יוכלו להיכנס למכונה "
+"הזאת. זה עשוי להשפיע על שירותים אחרים כגון פתרון בעזרת DNS ורשימת רשויות "
+"האישורים המהימנות עשויה להשתנות."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "לאחר עליית המערכת"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "התראה"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "אזעקה ומעלה"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "כינוי"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "הכול"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr "כל %0 הכרכים הפיזיים הנבחרים נחוצים לפריסה הנבחרת."
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "כל העדכונים"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "אול אין ואן"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "לאפשר הרצה (ביטול מיסוך)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "כתובות IP מורשות"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "כתובות מורשות"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "חובה לבחור ב־$0 נוסף"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "תיעוד תפקידים של Ansible"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"אפשר לסנן כל מחרוזת טקסט בהודעות שביומנים. המחרוזת יכולה להיות גם בצורת "
+"ביטוי רגולרי. יש תמיכה גם בסינון לפי שדות ביומן ההודעות. כאן משתמשים בערכים "
+"מופרדים ברווחים בצורה שדה=ערך, כאשר הערך יכול להיות רשימה מופרדת בפסיקים של "
+"ערכים אפשריים."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "מראה"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "חסר מידע על היישום"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "יישומים"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "רשימת יישומים"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "החלה והפעלה מחדש"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "החלת טלאי ליבה באופן חי"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "החלת הפתרון הזה"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "המדיניות החדשה חלה… עשוי לארוך מספר דקות."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "הפתרון חל…"
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "העדכונים חלים"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "החלת העדכונים נכשלה"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "מתאים לעיגונים מהותיים כגון ‎/var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "יישומים"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "הערכה"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "תג נכס"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "עם העלייה"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "נדרש כונן אחד לפחות."
+msgstr[1] "נדרשים $0 כוננים לפחות."
+msgstr[2] "נדרשים $0 כוננים לפחות."
+msgstr[3] "נדרשים $0 כוננים לפחות."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "נדרש התקן בלוק אחד לפחות."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "נדרש כונן אחד לפחות."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "בדקה"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "בשנייה"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "במועד מסוים"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "מאפיינים"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "יומן פיקוח"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "אימות"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "מתבצע אימות"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "אימות"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "האימות נכשל"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "האימות נכשל: השרת סגר את החיבור"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr "נדרש אימות כדי לבצע משימות שדורשות השראה עם המסוף המקוון Cockpit"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "נדרש אימות"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "אישור מפתח SSH"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "מפתחות SSH ציבוריים מורשים"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "אוטומטית"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "אוטומטית (DHCP בלבד)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "כניסה אוטומטית"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "עדכונים אוטומטיים"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "מתחיל אוטומטית"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "אוטומטית באמצעות NTP"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "אוטומטית באמצעות שרתי NTP נוספים"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "אוטומטית באמצעות שרתי NTP מסוימים"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "סקריפט אוטומטי"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "יעדים זמינים על $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "עדכונים זמינים"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "Grow partition"
+msgid "BIOS boot partition"
+msgstr "הגדלת מחיצה"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "תאריך BIOS"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "גרסת BIOS"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "חזרה לחשבונות"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "פסול"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "הגדרה פסולה"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "מאזן"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "לפני"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "מתאגד אל"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "שחור"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "בלייד"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "אריזת בלייד"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "התקן בלוק"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "התקן בלוק למערכות הפעלה"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "התקני בלוק"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "חסום"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "מאגד"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "עלייה"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr "העלייה נכשלת אם מערכת הקבצים לא מעוגנת, ובכך נמנעת גישה מרחוק"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "העלייה עדיין תצליח כאשר מערכת הקבצים לא מתעגנת"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "מאוגד על ידי"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "גשר"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "פתחת גשר"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "הגדרות פתחת גשר"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "שידור"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "הגדרות פגומות"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "קיים עדכון מתקן תקלות"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "תקלות"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "שלדת הרחבת אפיקים"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"החלפת סיסמת מפתח ה־SSH‏ $0 לסיסמת הכניסה של $1 על גבי $2, תוביל לכך שהמפתח "
+"אוטומטית יהיה זמין ותהיה לך אפשרות להיכנס אל $3 ללא סיסמה בעתיד."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " עקיפת בדיקת דפדפן "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "מעבד"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "אבטחת מעבד"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "מתגי אבטחת מעבד"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "ניצולת מעבד"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "ניצולת/עומס מעבד"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "מכלא"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "יכול להיות שם מארח, כתובת IP, שם כינוי או כתובת ssh://‎ מלאה"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr "לא ניתן למצוא יומנים עם שילוב המסננים הנוכחי."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "ביטול"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "ביטול כיבוי"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "ביטול הפעלה מחדש"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "לא ניתן להפעיל"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "לא ניתן להתחבר למארח לא ידוע"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "לא ניתן להעביר פרטי גישה"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr "לא ניתן להצטרף לשם תחום כיוון ש־realmd לא זמין במערכת הזו"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "לא ניתן לתזמן אירוע לעבר"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "קיבולת"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "ספקית"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "החלפה"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "החלפת מדיניות קריפטוגרפית"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "החלפת שם מארח"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "החלפת שם מאתחל ה־iSCSI"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "החלפת שם מאתחל ה־iSCSI"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change shell"
+msgid "Change label"
+msgstr "החלפת מעטפת"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "החלפת מילת צופן"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "החלפת סיסמה"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "החלפת פרופיל ביצועים"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "החלפת פרופיל"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "החלפת מעטפת"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "החלפת שעון המערכת"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "החלפת הסיסמה של $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "שינוי ההגדרות"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"מפתחות שהוחלפו הם לעתים תוצאה של התקנת מערכת הפעלה מחדש. עם זאת, שינוי בלתי "
+"צפוי עשוי להעיד שגורם צד־שלישי מנסה ליירט את החיבור שלך."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr "שינוי ההגדרות יפגע בחיבור לשרת וימנע את הגישה למנשק הניהול."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "איתור עדכונים"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr "כדאי לבדוק שגיבוב ה־SHA-256 או SHA-1 מהפקודה תואם לחלונית הזאת."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "בדיקת גיבוב המפתח מול שרת ה־Tang."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "$target בבדיקה"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "ה־IP נבדק"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "התקן ה־RAID‏ $target נבדק"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "התקן ה־RAID‏ $target נבדק ומתוקן"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "מתבצע איתור של החבילה $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "מתבצעת בדיקה לאיתור תמיכה ב־NBDE ב־initrd"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "מתבצע איתור של יישומים חדשים"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "מתבצע איתור של עדכוני חבילות…"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "התכנה שמותקנת נבדקת"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "התהליכים הקשורים נבדקים"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "מצב התכנית בבדיקה"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "נא לבחור את שפת היישום לשימוש"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "גודל נתח"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "מחלקה"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "מתבצע ניקוי לכבוד $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "לנקות ‚התחלה נכשלה’"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "ניקוי כל המסננים"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "ניקוי החיפוש"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "התקן טקסט גלוי"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "תכנה מצד לקוח"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "סגירה"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "סגירת העמודים הנבחרים"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "האימות של Cockpit לא מוגדר נכון."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "ההגדרות של Cockpit ל־NetworkManager ול־Firewalld"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "ל־Cockpit אין אפשרות ליצור קשר עם המארח שסופק."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "ב־Cockpit אירעה שגיאה בלתי צפויה."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit הוא מנהל שרתים שמקל על ניהול שרתי הלינוקס שלך דרך הדפדפן. מעבר חטוף "
+"בין המסוף והכלי המקוון הוא פשוט וקל. שירות שהופעל דרך Cockpit ניתן לעצור דרך "
+"המסוף. באותו האופן, אם מתרחשת שגיאה במסוף ניתן לצפות בה במנשק היומן של "
+"Cockpit."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit הוא מנשק אינטראקטיבי לניהול שרתי לינוקס."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit אינו תואם לתכנה שרצה על המערכת."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit אינו מותקן"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit אינו מותקן על המערכת."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit הוא מושלם למנהלי מערכות מתחילים, מאפשר להם לבצע משימות פשוטות בקלות "
+"כגון ניהול אחסון, חקירת יומנים והפעלה ועצירה של שירותים. ניתן לנהל ולעקוב "
+"אחר מספר שרתים בו־זמנית. כל שעליך לעשות הוא להוסיף אותם בלחיצה בודדת "
+"והמכונות שלך תדאגנה לחברותיהן."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "יכול להיות ש־Cockpit לא יעובד כראוי בדפדפן שלך"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "איסוף ואריזה של נתוני ניתוח ותמיכה"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "איסוף היטלי קריסת ליבה"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "איסוף מדדים"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "צבע"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "פתחות, טווחים ושירותים מופרדים בפסיקים יתקבלו"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "פקודה"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "הפקודה לא נמצאה"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "הערה"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "התקשורת עם tuned נכשלה"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "PCI חסכוני"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "תואם לכל המערכות וההתקנים (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "תואם למערכות ולכוננים חדישים > 2 ט״ב (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "דחיסת היטלי קריסה כדי לחסוך במקום"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "דחיסה"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "התנאי $0=$1 לא התקיים"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "התנאי נכשל"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "הגדרות"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "מתבצעת הגדרה"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "מוגדרת כתובת IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "kdump מוגדר"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "הגדרות המערכת מוגדרות"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "אישור"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "אישור המחיקה של $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "אישור סיסמת מפתח"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "אישור סיסמה חדשה למפתח"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "אישור סיסמה חדשה"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "אישור סיסמה"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "אישור הסרת $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "אישור הסרה עם מילת צופן חלופית"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "אישור עצירת $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "בסתירה עם"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "סותר"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "התחברות אוטומטית"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "התחברות אל"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "התחברות אל:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "מתבצעת התחברות לסוכן SETroubleshoot…"
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "החיבור ל־dbus נכשל: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "מתבצעת התחברות למכונה"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "שגיאת התחברות"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "ההתחברות נכשלה"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "הזמן שהוקצב להתחברות תם."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "החיבור יאבד"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "מורכב מ־"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "שם תחום שנוצר אתו קשר"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "מכיל:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "להמשיך"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "להמשיך הפעלה"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "בקרה"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Control"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "בקר"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "מתהפך"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "הועתק"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "העתקה"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "העתקה ללוח הגזירים"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "ליבה $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "לא ניתן ליצור קשר עם $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "מיקום היטלי קריסה"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "דיווח על קריסות"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "הקרסת המערכת"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "יצירה"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "יצירת קבוצת כרכים מסוג LVM2"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "יצירת התקן RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "יצירת התקן RAID"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "יצירת מאגר Stratis"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "ליצור מפתח SSH חדש ולאשר אותו"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "יצירת תמונת מצב של מערכת הקבצים $0"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "יצירת חשבון עם מזהה משתמש לא ייחודי"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "יצירת חשבון עם סיסמה חלשה"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "יצירה ושינוי בעלות על תיקיית הבית"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "יצירה ועיגון"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create and mount"
+msgid "Create and start"
+msgstr "יצירה ועיגון"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "יצירת מערכת קבצים"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "ליצור אותו"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "יצירת כרך לוגי"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "יצירת חשבון חדש"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "יצירת מערכת קבצים חדשה"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "יצירת קבוצה חדשה"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "יצירת כרך לוגי חדש"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "יצירת קובץ משימה עם התוכן הזה."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "יצירת כרך לוגי חדש"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "יצירה בלבד"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "יצירת מחיצה"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "יצירת מחיצה על $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "יצירת טבלת מחיצות"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "יצירת תמונת מצב"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "יצירת תמונת מצב ועיגון"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "יצירת תמונת מצב בלבד"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create storage volume"
+msgid "Create storage device"
+msgstr "יצירת כרך אחסון"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "יצירת כרך"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "יצירת כרך רזה"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "יצירת מתזמן"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "יצירת קבוצת כרכים"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "נוצרה"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "קבוצת הכרכים $target מסוג LVM2 נוצרת"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "נוצר התקן RAID‏ $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "נוצר התקן VDO"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "נוצרת מערכת קבצים על $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "נוצר כרך לוגי על $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "נוצרת מחיצה $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "נוצרת תמונת מצב של $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "יצירת ה־$0 הזה תקטע את החיבור לשרת ותמנע את הגישה למנשק הניהול."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "חמור ומעלה"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"ערכות מדיניות קריפטוגרפיות הוא רכיב מערכת שמגדיר את המערכות הקריפטוגרפיות של "
+"הליבה וחולש על פני פרוטוקולי TLS,‏ IPSec,‏ SSH,‏ DNSSec ו־Kerberos."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "מדיניות קריפטוגרפית"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "המדיניות הקריפטוגרפית אינה אחידה"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "עלייה נוכחית"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "ניצולת מעבד מרבית נוכחית"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "בשימוש כרגע"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Currently in use"
+msgid "Currently not supported"
+msgstr "בשימוש כרגע"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "מותאם אישית"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "מדיניות קריפטוגרפית משלך"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "אפשרויות עיגון מותאמות אישית"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "פתחות מותאמות אישית"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom path"
+msgid "Custom type"
+msgstr "נתיב מותאם אישית"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "אזורים מותאמים אישית"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "מותר ברירת מחדל עם אימות חתימה SHA-1."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "חיפוש שמות תחום DNS"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "חיפוש שמות תחום DNS‏ $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "יומי"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "אזעקת סכנה:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "כהה"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "נתונים"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "נתונים בשימוש"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"הנתונים יאוחסנו כשני עותקים וגם באופן מתחלף בכרכים הפיזיים הנבחרים כדי לשבר "
+"את האמינות ואת הביצועים. יש לבחור בארבעה כרכים לפחות."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"הנתונים יאוחסנו כשני עותקים ויותר בכרכים הפיזיים הנבחרים, כדי לשפר את "
+"האמינות. יש לבחור בשני כרכים לפחות."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+"הנתונים יאוחסנו בכרכים הפיזיים הנבחרים באופן מתחלף כדי לשפר את הביצועים. יש "
+"לבחור לפחות שני כרכים."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"הנתונים יאוחסנו בכרכים הפיזיים הנבחרים כדי שאם אחד מהם יושבת לא תהיה השפעה "
+"על המידע. יש לבחור לפחות שלושה כרכים."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"הגדרות תאריכים אמורות להיות בתבנית YYYY-MM-DD hh:mm:ss. לחלופין אפשר להשתמש "
+"במחרוזות ‚yesterday’ (אתמול), ‚today’ (היום) ו־‚tomorrow’ (מחר). "
+"‚now’ (עכשיו) מתייחס לשעה הנוכחית. בנוסף, אפשר לציין זמן יחסי עם קידומת של "
+"‚-’ או ‚+’"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "השבתה"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "שינוי שם של כרך פיזי"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "מתבצעת השבתה"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "מתבצעת השבתה של $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "ניפוי שגיאות ומעלה"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "להקטין באחד"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "פיזור+זוגיות בכונן ייעודי (RAID 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "הסרת כפילות"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "בררת מחדל"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "השהיה"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "השהיה חייבת להיות מספר"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "מחיקה"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "מחיקת $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "מחיקת חשבון"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "מחיקת קבצים"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "מחיקת קבוצה"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "מחיקה"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "למחוק את הדוח לצמיתות?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "מחיקת $0 תקטע את החיבור לשרת ותמנע את הגישה למנשק הניהול."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "$target נמחק"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "קבוצת הכרכים $target מסוג LVM2 נמחקת"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "מחיקת מאגר Stratis תמחק את כל הנתונים שהוא מכיל."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "מחיקת מערכת קבצים תמחק את כל הנתונים שבה."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "מחיקת כרך לוגי תמחק את כל הנתונים שבו."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "מחיקת מחיצה תמחק את כל הנתונים שבה."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting erases all data on a RAID device."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "מחיקה תמחק את כל הנתונים שעל התקן ה־RAID."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "מחיקה תמחק את כל הנתונים של התקן ה־VDO."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "מחיקה מוחקת את כל הנתונים בקבוצת כרכים."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting erases all data on a volume group."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "מחיקה מוחקת את כל הנתונים בקבוצת כרכים."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "מחיקה תסיר את הקבצים הבאים:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "תיאור"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "שולחן עבודה"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "נתיק"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "פרטים"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "פיתוח"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "התקן"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "קובץ התקן"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "ההתקן הוא לקריאה בלבד"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "שם השירות"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "דוחות אבחון"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "תיקייה"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "התיקייה $0 איזה זמינה לכתיבה או שאינה קיימת."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "השבתת ריבוי תהליכים מקבילי"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "השבתת חומת האש"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "השבתת tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "מושבת"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "איסור סיסמה אינטראקטיבית"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "לא לאפשר אימות בסיסמה"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "לא לאפשר הרצה (מיסוך)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "ניתוק"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "מנותק"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "קלט/פלט של כונן"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "הכונן תקין"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "הכונן נכשל"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "מילת צופן לכונן"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "כוננים"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "ניצולת כוננים"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "התעלמות"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "התעלמות מאזעקה"
+msgstr[1] "התעלמות מ־$0 אזעקות"
+msgstr[2] "התעלמות מ־$0 אזעקות"
+msgstr[3] "התעלמות מ־$0 אזעקות"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "התעלמות מההתראות הנבחרות"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "שפת התצוגה"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "פיזור+זוגיות מפוזרת (RAID 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "לא לעגן"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "לא לעגן אוטומטית עם העלייה"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "תחנת עגינה"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "לא מתחיל אוטומטית"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "לא תעוגן עם העלייה"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "שם תחום"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "שם תחום"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "כתובת שם תחום"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "שם מנהל שם תחום"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "סיסמת מנהל שם תחום"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "לא ניתן ליצור קשר עם שם התחום"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "שם התחום אינו נתמך"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "לא לחזור"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "פיזור+קוד ריד-סולומון (RAID 6)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "הורדה"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "הורדת דפדפן חדש בחינם"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "הורד"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "בהורדה"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "$0 בהורדה"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "כונן"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "דו־צדדי"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "מחיצה מורחבת"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "עריכה"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "עריכת ‎/etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "עריכת שרת מפתחות Tang"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "עריכת הגדרות VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "עריכת VPN מסוג VPN"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "עריכת הגדרות מאגד"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "עריכת הגדרות גשר"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "עריכת שירות משלך באזור $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "עריכת מארח"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "עריכת מארחים"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "עריכת ההודעה היומית"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "נקודת עיגון"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "עריכת כללים ואזורים"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "עריכת שירות"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "עריכת השירות $0"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "עריכת הגדרות ציוות"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "עריכת משתמש"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "עריכת מפתח דורשת משבצת פנויה"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "$target נשלף כעת"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "מחשב משובץ"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "ריק"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "לרוקן"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "סיסמה ריקה"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "$target מתרוקן"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "הפעלה"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "להפעיל או להשבית את ההתקן"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "הפעלת שירות"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "הפעלת חומת האש"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "מופעל"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "$0 מופעל"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "הצפנת נתונים"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "הצפנת הנתונים עם שרת מפתחות Tang"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "הצפנת נתונים עם מילת צופן"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "מוצפן"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "$0 מוצפן"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted Stratis pool $0"
+msgid "Encrypted Stratis pool"
+msgstr "מאגר Stratis מוצפן $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "כרך לוגי מוצפן בגודל $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "מחיצה מוצפנת בגודל $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "הצפנה"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "אפשרויות הצפנה"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "מילת צופן ההצפנה"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "סוג הצפנה"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "הסתיים"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "נקודת קצה"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"נקודת קצה שמתנהגת כ„שרת” צריך לציין בתור מארח:פתחה (host:port), אחרת אפשר "
+"להשאיר ריק."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "נאכף"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "קיימים עדכוני שיפורים"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "נא למלא כתובת MAC תקנית"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "כל תת־הרשת"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "רשומה ב־$0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "$target נמחק"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "טעויות ידועות"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "שגיאה"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "שגיאה ומעלה"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "אירעה שגיאה"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "שגיאה בהתקנת $0:‏ PackageKit אינו מותקן"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "הודעת שגיאה"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "שגיאה בהרצת semanage לצורך גילוי השינויים במערכת"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "שגיאה בשמירת המפתחות המורשים: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "שגיאה בעת הגדרת מצב SELinux:‏ ‚$0’"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "כתובת חומרת אתרנט"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "יחידת העברה מרבית של אתרנט"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "יש לבחור בדיוק $0 כרכים פיזיים"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr "יש לבחור בדיוק ב־$0 כרכים פיזיים, אחד לכל יחידת פיזור של הכרך הלוגי."
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "לדוגמה: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "לדוגמה: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "סיסמה מצוינת"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "שלדת הרחבה"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "החשבון יפוג ב־"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "החשבון יפוג ב־$0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "ייצוא"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "ייצוא לרשת"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "פירוט מורחב"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "מחיצה מורחבת"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS לא מופעל כראוי"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS עם מגבלות קריטריונים משותפים נוספות."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "נכשל"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "הוספת המכונה נכשלה: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "הוספת הפתחה נכשלה"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "הוספת השירות נכשל"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "הוספת האזור נכשלה"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "החלפת הסיסמה נכשלה"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "הגדרת PCP נכשלה"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "מחיקת האזעקה נכשלה: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "ההשבתה של tuned נכשלה"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "השבתת פרופיל ה־tuned נכשלה"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "עריכת המכונה נכשלה: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "עריכת השירות נכשלה"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "הפעלת $0 ב־firewalld נכשלה"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "ההפעלה של tuned נכשלה"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "משיכת יומנים נכשלה"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "טעינת המפתחות המורשים נכשלה."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "טעינת היחידה נכשלה"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"פענוח קובצי היחידה של dnf-automatic.timer או dnf-automatic-install.timer. נא "
+"להסיר את המעקפים שהוקמו כדי להגדיר עדכונים אוטומטיים."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "הפעלת השירות מחדש נכשלה"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "שמירת השינויים ב־‎/etc/motd נכשלה"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "שמירת ההגדרות נכשלה"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "ההפעלה נכשלה"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "החלפת פרופיל נכשלה"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "מצב הקובץ"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "מערכת קבצים"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "מערכת הקבצים נעולה"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "שם מערכת קבצים"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "נוצרת מערכת קבצים על $target"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "מערכות הקבצים כבר מעוגנות תחת נקודת העיגון הזאת."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "סינון לפי שם או תיאור"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "סינון פריטי תפריט"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "סינון שירותים"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "מסננים"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "טביעת אצבע"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "חומת אש"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "חומת האש אינה זמינה"
+
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "גרסת קושחה"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "תיקון תמיכה ב־NBDE"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "גודל גופן"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "נאסר עליו לפעול"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "לאלץ החלפה"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "לאלץ מחיקה"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "לאלץ החלפת סיסמה"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "פרמוט"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "פרמוט $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "פרמוט ועגינה"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format and mount"
+msgid "Format and start"
+msgstr "פרמוט ועגינה"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "פרמוט בלבד"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "פרמוט ימחק את כל הנתונים בהתקן אחסון."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "השהיית העברה $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "מספר מסגרת"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "מקום פנוי"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "חיפוש חופשי"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "ימי שישי"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "התחלה"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "שם מלא"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "שער גישה"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "כללי"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "נוצר"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "לעבור אל $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "לעבור ליישום"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "לעבור כעת"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "חשיפת התרשים"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "תפריט אפשרויות חשיפת תרשים"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "קבוצה"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "שם הקבוצה"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "קבוצות"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "הגדלה"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "הגדלת התוכן"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "להגדיל לגודל לוגי של $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "הגדלת הכרך הלוגי"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "הגדלת מחיצה"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "להגדיל את המאגר על חשבון כל המקום הפנוי"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "להגדיל על חשבון כל המקום הפנוי"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "מצב סיכת שיער"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "מצב סיכת שיער"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "נישא"
+
+#: pkg/storaged/drive/drive.jsx:64
+msgid "Hard Disk Drive"
+msgstr ""
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "פרטי חומרה"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "בריאות"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "זמן Hello‏ $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "עזרה"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "הסתרת סיסמת האישור"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "הסתרת הסיסמה"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "מזהה היררכיה"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "מרחב תמרון גדול יותר על חשבון חזית פגיעה יותר לתקיפות."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "ספירת חבילות היסטורית"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "תיקיית בית"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "מארח"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "מפתח המארח שגוי"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "שם מארח"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "מארחים"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "שעתי"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "שעות"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "איך לבדוק"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "מזהה"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "מזהה $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "כתובת IP"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"כתובת IP עם קידומת ניתוב. יש להפריד בין ערכים בפסיק. למשל: 192.0.2.0/24, "
+"‎2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "כתובות IPv4"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "הגדרות IPv4"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "הגדרות IPv6"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "מזהה"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"אם השדה נשאר ריק, המזהה יווצר אוטומטית על בסיס השירותים על הפתחה המשויכת "
+"ומספרי הפתחות"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"אם טביעת האצבע תואמת, יש ללחוץ על „לקבל את המפתח להיכנס”. אחרת, לא להתחבר "
+"וליצור קשר עם הנהלת המערכת."
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"אם טביעת האצבע תואמת, יש ללחוץ על ‚מתן אמון והוספת מארח’. אחרת, לא להתחבר "
+"וליצור קשר עם הנהלת המערכת."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "התעלמות"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "נכנס"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "במסוף יש להריץ: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"כדי לאפשר כניסה אל $0 בתור $1 ללא סיסמה בעתיד, עליך להשתמש בסיסמת הכניסה של "
+"$2 על גבי $3 כסיסמת המפתח או להשאיר את סיסמת המפתח ריקה."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "בסנכרון"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "לא פעיל"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "יצירת כרך לוגי"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "שירותים כלולים"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr "בקשות נכנסות נחסמות כברירת מחדל. בקשות יוצאות לא נחסמות."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "עיגון בלתי אחיד של מערכת קבצים"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "להגדיל באחד"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "זיכרון אינדקס"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "עקיף"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "מידע"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "מידע ומעלה"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "אתחול"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "אתחול הכונן $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "אתחול מוחק את כל הנתונים בכונן."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "מופעל…"
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "תובנות: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "התקנה"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "התקנת תמיכה ב־NFS"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "התקנת תמיכה ב־Stratis"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "התקנת כל העדכונים"
+
+#: pkg/apps/application-list.jsx:200
+#, fuzzy
+#| msgid "Package information"
+msgid "Install application information"
+msgstr "פרטי חבילה"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "התקנת cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "התקנת עדכוני kpatch"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "התקנת תמיכה ב־realmd"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "התקנת עדכוני אבטחה"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr "יש להתקין את setroubleshoot-server כי לטפל באירועי SELinux."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "התקנת תכנה"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "נא להתקין את החבילה $0"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr "ההתקנה לא נתמכת ללא חבילת cockpit מותקנת"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "מותקן"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "בהתקנה"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "$0 בהתקנה"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "התקנת $0 תסיר את $1."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "חבילות מותקנות"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "מנשק"
+msgstr[1] "מנשקים"
+msgstr[2] "מנשקים"
+msgstr[3] "מנשקים"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "חברים במנשק"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "מנשקים"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "שגיאה פנימה"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "שגיאה פנימית: כותרת אתגר שגויה"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "שגוי"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "כתובת שגויה $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "מבנה התאריך שגוי"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "מבנה תאריך שגוי ומבנה שעה שגוי"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "מועד התפוגה שגוי"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "הרשאות הקובץ שגויות"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "מפתח שגוי"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "מדד שגוי $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "מספר הימים שגוי"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "מספר הפתחה שגוי"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "קידומת שגויה $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "קידומת או מסכת רשת שגויה $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "טווח שגוי"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "מבנה השעה שגוי"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "אזור זמן שגוי"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "שם משתמש או סיסמה שגויים"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "שער גישה IoT"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "האם sshd פועל על פתחה אחרת?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "משימות"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "הצטרפות"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "הצטרפות לשם תחום"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "הצטרפות לשם תחום"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "הצטרפות"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "הצטרפות לשם תחום דורש את התקנת realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "אין תמיכה בהצטרפות לשם התחום הזה"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "מצטרף למרחב השם של"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "ז׳ורנל"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "רשומת ז׳ורנל"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "רשומת הז׳ורנל לא נמצאה"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "מעבר אל"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit אינו מותקן"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "בדיקת הגדרות kdump"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "לשמור על החיבור"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "היטל קריסת ליבה"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "היטל ליבה"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "טלאי ליבה באופן חי $0 פעיל"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "טלאי ליבה חי $0 מותקן"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "הגדרות טלאי ליבה חי"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "הטלאת ליבה חיה"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "סיסמת מפתח"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "משבצות מפתח עם סוגים בלתי מוכרים אינן ניתנות לעריכה כאן"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "מקור מפתח"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "מפתחות"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "שרת מפתחות"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "כתובת שרת מפתחות"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "הסרת שרת מפתחות עלולה למנוע את שחרור $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "מפתח LACP"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "מיושן עם תאימות מול Active Directory."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "VDO pool"
+msgid "LVM2 VDO pool"
+msgstr "מאגר VDO"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "כרך לוגי"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "כרכים לוגיים"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volume"
+msgstr "כרך פיזי"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volumes"
+msgstr "כרך פיזי"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "קבוצת כרכים ב־LVM2"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "קבוצת כרכים LVM2‏ $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "מחשב נייד"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "24 השעות האחרונות"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "7 הימים האחרונים"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "פעילות אחרונה"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "לא ניתן להסיר את משבצת המפתח האחרונה"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "בדיקה אחרונה: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "לא ניתן להסיר את משבצת המפתח האחרונה"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "כניסה אחרונה"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "שינוי אחרון: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "כניסה אחרונה שהצליחה:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "פריסה"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "מידע נוסף"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "לעזוב את $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "עזיבת שם התחום"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "ניתן להשאיר ריק כדי לדלג על הצפנה"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "בכפוף לרישיון LGPL מבית GNU בגרסה 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "בהירה"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "הגבלת גישה"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "גישה מוגבלת"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"מצב גישה מוגבלת מגביל את ההרשאות הניהוליות. חלקים מסוימים במסוף המקוון יפעלו "
+"באופן חלקי."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "הגבלות"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "קווי"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "השהיית קטיעת קישור"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "קישור מקומי"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "מעקב אחר קישורים"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "השהיית חיבור קישור"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "עקיבה אחר קישורים"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "מקושר"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem $0"
+msgid "Linux filesystem data"
+msgstr "ניתוק מערכת הקבצים $0"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "שטח החלפה"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "האזנה"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "פתחת האזנה"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "פתחת ההאזנה חייבת להיות מספר"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "היחידות מוצגות"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "הצגת היחידות נכשלה: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "טעינה"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "איזון עומס"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "טעינת נתונים קודמים"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "טעינת רשומות קודמות"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "טעינת העדכונים הזמינים נכשלה"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "העדכונים הזמינים נטענים, נא להמתין…"
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "רשומות קודמות נטענות"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "המפתחות נטענים…"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "טעינת מפתחות ה־SSH נכשלה"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "טעינת היחידות נכשלה"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "החבילות נטענות…"
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "השינויים למערכת נטענים…"
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "טעינת היחידה נכשלה"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "בטעינה…"
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "חשבונות מקומיים"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "מערכת קבצים מקומית"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "נקודת עגינה מקומית"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "No storage"
+msgid "Local storage"
+msgstr "אין אחסון"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "מקומית ב־$0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "מיקום"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "נעילה"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "נעילת $0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "נעילת חשבון"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgid "Locked"
+msgid "Locked data"
+msgstr "נעול"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "$target ננעל"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "כניסה"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "כניסה אל $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "כניסה עם חשבון המשתמש שלך בשרת."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "הודעות יומן"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "יציאה"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "הוצאת משתמש"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "נכנסת"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "לוגי"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "כרך לוגי (תמונת מצב)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "גודל לוגי"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "כרך לוגי"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "כרך לוגי (תמונת מצב)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "כרך לוגי בגודל $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "כניסה"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "נא להיכנס שוב"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "הכניסה נכשלה"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "תצורת כניסה"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "היסטוריית כניסות"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "רשימת היסטוריית כניסות"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "הוצאת $0 מהמערכת"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "היציאה הצליחה"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "יומנים"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "מחשב שולחני עם פרופיל נמוך"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "קופסת אוכל"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "התקן RAID"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "התקן RAID‏ $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "ההתקן הוא לקריאה בלבד"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "השירות פעיל"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "התקן RAID"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "הוספת כוננים"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (מומלץ)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU חייב להיות מספר חיובי"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "מזהה מכונה"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "טביעות אצבע מפתח SSH של מכונה"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "שלדת שרת ראשית"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "תחזוקה"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+#, fuzzy
+#| msgid "Rename filesystem"
+msgid "Manage filesystem sizes"
+msgstr "שינוי שם מערכת קבצים"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "ניהול אחסון"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "מנשקים מנוהלים"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "ניהול LVMים"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "ניהול עיגוני NFS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "ניהול RAIDים"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "ניהול VDOים"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "ניהול VLANים"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "ניהול חומת אש"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "ניהול מאגדי רשת"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "ניהול גישורי רשת"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "ניהול ציוותי רשת"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "ניהול מחיצות"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "ניהול כוננים פיזיים"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "ניהול שירותים"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "ניהול עדכוני תכנה"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "ניהול חשבונות משתמשים"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "ידני"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "ידנית"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "$target מסומן כתקול"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "מיסוך שירות"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "ממוסך"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"מיסוך שירות מונע מכל היחידות התלויות לפעול. להגדרה זו עשויה להיות השפעה "
+"גדולה מהצפוי. נא לאשר שברצונך למסך את היחידה הזאת."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "גיל הודעה מרבי $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "כונן אופטי"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "זיכרון"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "טכנולוגיית זיכרון"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "שימוש בזיכרון"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "שימוש בזיכרון/החלפה"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "ממוזגת"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "הודעה למשתמשים שנמצאים במערכת"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "ניתן למצוא הודעות שקשורות בכשל בז׳ורנל:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "נתוני על בשימוש"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "שיטה"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "מדד (עדיפות)"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "מדדים והיסטוריה"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "לא ניתן לטעון את היסטוריית המדדים"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "הגדרות מדדים"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "מחשב מוקטן"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "מארז מוקטן"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "דקה חייבת להיות מספר בין 0 ל־59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+#, fuzzy
+#| msgid "Minutes"
+msgid "Minutely"
+msgstr "דקות"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "דקות"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "אפחותים"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "מצב"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "דגם"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "מתבצע שינוי ב־$target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "ימי שני"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "הפרש בין דגימות מעקב"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "יעדי מעקב"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "חודשי"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "מידע נוסף…"
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "עיגון"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+#, fuzzy
+#| msgid "Mount point"
+msgid "Mount Point"
+msgstr "נקודת עיגון"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "לעגן לאחר שהרשת זמינה, להתעלם מכשל"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "לעגן אוטומטית גם עם העלייה"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "לעגן בעלייה"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "לעגן אוטומטית אל $0 בזמן העלייה"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "לעגן לפני הפעלת השירותים"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "הגדרות עיגון"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "עיגון מערכת קבצים"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "לעגן כעת"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "לעגן אל $0 כעת"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "אפשרויות עיגון"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "נקודת עיגון"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "נקודת העיגון לא יכולה להיות ריקה"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "נקודת העיגון לא יכולה להיות ריקה."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "נקודת העיגון כבר משמשת לטובת $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "נקודת העיגון חייבת להתחיל ב־„/”."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "עיגון לקריאה בלבד"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "לעגן בלי להמתין, התעלמות מכשל"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "מתבצע עיגון של $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "מעוגן לפני הפעלת השירותים"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "מעוגן במקביל לשירותים"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr "מעוגן במקביל לשירותים, אבל אחרי שהרשת זמינה"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "שלדה למגוון מערכות"
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Multipathed devices"
+msgid "Multipathed devices"
+msgstr "התקנים מרובי־נתיבים"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "עיגון NFS"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "פינג מסוג NSNA"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "שרת NTP"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "שם"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "השם לא יכול להיות ריק."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "השם לא יכול להיות ריק."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "השם לא יכול להיות יותר מ־$0 בתים"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "השם לא יכול להיות ארוך מ־$0 תווים"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "השם לא יכול להיות ארוך מ־127 תווים."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "השם לא יכול להיות ארוך מ־127 תווים."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "השם לא יכול להכיל את התו ‚$0’."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "השם לא יכול להכיל את התו ‚$0’."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "השם לא יכול להכיל רווח."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "נדרש שרת NTP אחד לפחות"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "רשת"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "קלט/פלט רשת"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "מאגד רשת"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "התקני רשת ותרשימים דורשים NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "יומני תקשורת"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "ניצולת רשת"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager אינו מותקן"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager אינו פעיל"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "ניצולת רשת"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "תקשורת"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "אף פעם"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "חשבון שתוקפו לא פג"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "הסיסמה לא תפוג לעולם"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "מעולם לא נכנס"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "עיגון NFS חדש"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "מארח חדש"
+
+#: pkg/shell/hosts_dialog.jsx:464
+#, fuzzy
+#| msgid "New host"
+msgid "New host: $0"
+msgstr "מארח חדש"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "סיסמה חדשה למפתח"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "שם חדש"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "מילת צופן חדשה"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "סיסמה חדשה"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "הסיסמה החדשה לא התקבלה"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "הבא"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "לא"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "לא מוגדר מזהה"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "אין אזעקות SELinux."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "אף יישום לא מותקן או זמין."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "אין משבצות פנויות"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "אין התקני בלוק זמינים."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "לא נמצא התקן עלייה"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "אין ספקית"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "לא נמצאו הגדרות"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "אין נתונים זמינים"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "אין נתונים זמינים בין $0 ל־$1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "אין השהיה"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "אין תיאור זמין"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "לא סופק תיאור."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "לא נמצא התקן עלייה"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "אין כוננים זמינים."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "לא נמצאו יומנים"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "לא נמצאו תוצאות"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "אין הצפנה"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "אין אירועים"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "אין מערכת קבצים"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "אין מערכות קבצים"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "אין משבצות מפתח פנויות"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "אין מקום פנוי"
+
+#: pkg/storaged/partitions/partition.jsx:79
+#, fuzzy
+#| msgid "Create partition"
+msgid "No free space after this partition"
+msgstr "יצירת מחיצה"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "לא צוין שם לקבוצה"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "לא נמצאו מפתחות מארחים."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "לא נמצאה חבילת התקנה ליישום הזה."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "לא נוספו מפתחות"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "אין שפות תואמות"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "אין רשומות ביומן"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "אין כרכים לוגיים"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "לא נמצאו יומנים"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "אין כללים תואמים"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "לא הוכנס אמצעי מדיה"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "אין חלוקה למחיצות"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "אין חלוקה למחיצות"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "לא נוספו עמיתים."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "כרכים פיזיים"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "לא צוין שם אמתי"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "לא נמצאו תוצאות"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr "לא נמצאו תוצאות לתנאי המסנן. יש לנקות את כל המסננים כדי להציג תוצאות."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "אין פגיעות בכללים"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No storage"
+msgid "No storage found"
+msgstr "אין אחסון"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "אין כרכים לוגיים"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "אין קובץ או תיקייה בשם הזה"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "אין שינויים במערכת"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "אין דוחות מערכת."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "אין עדכונים"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "לא צוין שם משתמש"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "אין"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "לא מפתח פרטי תקני"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "לא מורשה להשבית את חומת האש"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "לא מורשה להפעיל את חומת האש"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "לא זמין"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "לא מחובר"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "לא מחובר לתובנות"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "לא מחובר למארח"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space"
+msgid "Not enough free space"
+msgstr "אין מספיק מקום"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "אין מספיק מקום"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "אין מספיק מקום כדי לגדול."
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "לא נמצא"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "לא מותקן"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "Not permitted to configure realms"
+msgid "Not permitted to configure network devices"
+msgstr "המשתמש $0 אינו מורשה לשנות מתחמים"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "המשתמש $0 אינו מורשה לשנות מתחמים"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "לא מורשה לבצע את הפעולה הזאת."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "לא מוכן"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "לא רשום"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "לא פועל"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "לא מוגדר"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "לא מסונכרן"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "הערה"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "מחברת"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "הודעה ומעלה"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "לערפל את כתובת הרשת, המארחים ואת שמות המשתמשים"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "מעורפל"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "התרחש ב־$0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "התרחש בין $0 ל־$1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "מופעים"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "אישור"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "מילת צופן ישנה"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "סיסמה ישנה"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "הסיסמה הישנה לא התקבלה"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "על התקן מעוגן"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "בעת כשל"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"לאחר התקנת Cockpit, יש להפעיל את השירות בעזרת „systemctl enable --now "
+"cockpit.socket”."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "רק $0 מתוך $1 מנוצלים."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "מותר רק אותיות באנגלית, מספרים, : , _ , . , @ , -"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "חירום בלבד"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr "להשתמש רק באלגוריתמים מאושרים ומורשים בעת טעינה במצב FIPS."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "אופס!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "יש לפתוח את שירות pmproxy בחומת האש כדי לשתף מדדים."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "המשימה ‚$operation’ על $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "אפשרויות"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "או להשתמש בדפדפן מובנה"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "אחר"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"שיטות אימות אחרות עדיין זמינות אפילו כאשר אימות סיסמאות אינטראקטיבי אסור "
+"לשימוש."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "אפשרויות אחרות"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "יוצא"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "סקירה"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "שכתוב"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "שכתוב על נתונים קיימים באפסים (אטי יותר)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "מזהה תהליך"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "החבילה cockpit-pcp חסרה להיסטוריית מדדים"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "פרטי חבילה"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit קרס"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit אינו מותקן"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit דיווח את קוד השגיאה $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "חבילות"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "שם העמוד"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "הורה"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "$parent הורה"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "חלק מתוך"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "חלק מתוך $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "מחיצה"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "מחיצה של $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+#, fuzzy
+#| msgid "Volume size is $0. Content size is $1."
+msgid "Partition size is $0. Content size is $1."
+msgstr "גודל הכרך הוא $0. גודל התוכן הוא $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "חלוקה למחיצות"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "מחיצות"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "סביל"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "מילת צופן"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "מילת הצופן לא יכולה להיות ריקה"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "מילת הצופן לא יכולה להיות ריקה"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "מילת צופן מכל משבצת מפתחות אחרת"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "הסרת מילת הצופן עשויה למנוע את השחרור של $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "מילות הצופן אינן תואמות"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "סיסמה"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "הסיסמה הוחלפה בהצלחה"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "תפוגת סיסמה"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "הסיסמה ארוכה מ־256 תווים"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "הסיסמה לא מקובלת"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "הסיסמה חלשה מדי"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "חובה לשנות את הסיסמה"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "הסיסמה לא התקבלה"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "רמז לסיסמה"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "הדבקה"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "שגיאת הדבקה"
+
+#: pkg/networkmanager/wireguard.jsx:215
+#, fuzzy
+#| msgid "Use existing"
+msgid "Paste existing key"
+msgstr "להשתמש בקיים"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "עליך להדביק את תוכן קובץ מפתח ה־SSH הציבורי שלך כאן"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "נתיב"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "עלות נתיב"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "עלות הנתיב $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "נתיב בשרת"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "הנתיב בשרת לא יכול להיות ריק."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "הנתיב בשרת חייב להתחיל ב־„/”."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "נתיב לתיקייה"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "הנתיב לקובץ"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "נתיבים"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "השהיה"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "עמיתים"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr "טייס משנה לביצועים אוסף ומנתח מדדי ביצועים מהמערכת שלך."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "פרופיל ביצועים"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "שלדת התקנים חיצוניים"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "קבוע"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "למחוק את הקבוצה $0 לצמיתות?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "למחוק את $0 לצמיתות?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "למחוק את $0 לצמיתות?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete subvolume $0?"
+msgstr "למחוק את $0 לצמיתות?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "ההרשאה נדחתה"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "מתירני"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "פיזי"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "כרכים פיזיים"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "כרכים פיזיים"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "כאן ניתן לשנות גודל של כרכים פיזיים."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "בחירת תאריך"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "נעיצת יחידה"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "הפרש בין פינגים"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "יעד פינג"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "יחידה נעוצה"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "קופסת פיצה"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "נא לעבור אימות כדי לקבל גישת ניהול"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "נא להפעיל JavaScript כדי להשתמש במסוף לדפדפן."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "נא להתקין את החבילה $0"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "נא לפתור את התקלה ולרענן את העמוד הזה."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "נא לציין מועד תפוגת תוקף"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "נא לציין את המארח לחיבור"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr ""
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "מאגר לכרכים לוגיים רזים"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "מאגר לכרכים באפסנה צרה"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "מאגר לכרכים באפסנה צרה"
+
+#: pkg/storaged/stratis/pool.jsx:464
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Pool passphrase"
+msgstr "מילת צופן ישנה"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "פתחה"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "נייד"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "פתחות"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "יצירת מחיצה"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "אורך קידומת"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "אורך קידומת או מסכת רשת"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "בהכנה"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "נוכחי"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "שימור"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "שם מארח יפה"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "עלייה קודמת"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "עיקרי"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "עדיפות"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "עדיפות $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "מפתח פרטי"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "תקלה בהעברה לניהול"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "פרטי הבעיה"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "מידע על הבעיה"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "תהליכים שמשתמשים במיקום"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "התקדמות: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "אתר המיזם"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "משך הצגת הבקשה דרך passwd תם"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "משך הצגת הבקשה דרך ssh-add תם"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "משך הצגת הבקשה דרך ssh-keygen תם"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "מפיץ רענון אל"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr "מגן מפני התקפות צפויות בעתיד הקרוב על חשבון מרחב תמרון."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "נא לספק מילת צופן למאגר על התקני בלוק אלו:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "מפתח ציבורי"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "תכלית"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (פיזור נתונים)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (שכפול)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (פיזור נתונים משוכפלים)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (פיזור עם זוגיות בכונן ייעודי)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (פיזור עם זוגיות מפוזרת)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (פיזור עם קוד ריד-סולומון)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "שלדת RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "רמת RAID"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "זיכרון"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "שלדת מעגן מתלה (Rack)"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "אקראי"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "טווח"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "הטווח חייב להיות מסודר בקפידה"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "דירוג"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "גולמי להתקן"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "קריאה"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "מידע נוסף…"
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "קריאה בלבד"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "מתבצעת קריאה"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "מתבצעת קריאה…"
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "מוכן"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "מוכן"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "שם המארח האמתי"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"שם המארח האמתי יכול להכיל רק אותיות קטנות באנגלית, ספרות, מינוסים ונקודות "
+"(עם תת־שמות תחום מאוכלסים)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "אורך שם התחום האמתי חייב להיות קצר מ־64 תווים"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "החלה חוזרת והפעלה מחדש"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "הפעלה מחדש"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "להפעיל מחדש לאחר השלמת התהליך"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "מומלץ להפעיל מחדש"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "הפעלת המערכת מחדש…"
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "קבלה"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "מארחים אחרונים"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "מומלץ, הגדרות מאובטחות בהתאם לדגמי האיום הנוכחיים."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "התחברות מחדש"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "מתבצע שיקום"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "התקן ה־RAID‏ $target עובר שיקום"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "פרטי החבילה מתרעננים"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "החיבור מסורב. המארח אינו מוכר"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "החיבור מסורב. מפתח המארח לא תואם"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "החיבור מסורב. מפתח המארח אינו מוכר"
+
+#: pkg/networkmanager/wireguard.jsx:224
+#, fuzzy
+#| msgid "Generated"
+msgid "Regenerate"
+msgstr "נוצר"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "initrd נוצר מחדש"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "הרשמה…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "התהליכים והשירותים הקשורים ייעצרו בכוח."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "התהליכים הקשורים ייעצרו בכוח."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "השירותים הקשורים ייעצרו בכוח."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "רענון"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "הטעינה הופצה מהמקור"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "מתבצע רענון"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "המצב של שאר השירותים מתרענן"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "מרוחק דרך CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "מרוחק דרך FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "מרוחק דרך NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "מרוחק דרך NFS"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "מרוחק דרך SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "מרוחק דרך SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "מרוחק דרך SSH"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "הסרות:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "הסרה"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "הסרת $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "הסרת השירות $0 מהאזור $1"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "להסיר את $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "להסיר את שרת המפתחות Tang?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "הסרת התקן"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "הסרת מארח"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "הסרת פריט"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "להסיר את הכרכים הפיזיים החסרים?"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "להסיר את מילת הצופן מעל משבצת המפתחות $0?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "להסיר מילת צופן?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "הסרת השירות $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "הסרת האזור $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "בהסרה"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "$0 בהסרה"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "הסרת $0 תקטע את החיבור לשרת ותמנע את הגישה למנשק הניהול."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "מתבצעת הסרה של $target מהתקן RAID"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"הסרת מילת צופן ללא אישור של מילת צופן אחרת עשוי למנוע שחרור או את ניהול "
+"המפתחות במקרה שמילות צופן נוספות נשכחו או שאבדו."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "מתבצעת הסרה של כרך פיזי מתוך $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"הסרת השירות cockpit עלול לגרום לקטיעת שירות המסוף המקוון. נא לוודא שאזור זה "
+"אינו חל על חיבור המסוף המקוון הנוכחי שלך."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "הסרת האזור תסיר את כל השירותים שבו."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "שינוי שם"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "שינוי שם למאגר Stratis"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "שינוי שם מערכת קבצים"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "שינוי שם של קבוצה"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "שינוי שם הקבוצה $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "שינוי שם של כרך פיזי"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "שינוי שם של קבוצת כרכים"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "השם של $target מוחלף"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "שינוי שם של קבוצה יכולה להשפיע על sudo ועל כללים דומים"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Repair logical volume $0"
+msgstr "שינוי שם של כרך פיזי"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "$target מתוקן"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "לחזור"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "לחזור באופן חודשי"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "חזרה על מילת הצופן"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "לחזור באופן שבועי"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "דיווח"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "תווית דיווח"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "דיווח לניתוח של ABRT"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "מדווח, אין קישורים זמינים"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "הדיווח נכשל"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "הדיווח בוטל"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "דוחות"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "דיווחים:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "לדרוש החלפת סיסמה כל $0 ימים"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "לדרוש החלפת סיסמה ב־$0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "לדרוש החלפת סיסמה בכניסה הראשונה"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "נדרש על ידי"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "נדרש על ידי "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "דורש"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "נדרשת גישה ניהולית כדי לערוך"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "הכרח"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "הכרח של"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "זיכרון שמור"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "איפוס"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "איפוס סיסמה"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "הגודל של $target משתנה"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"שינוי גודל של מערכת קבצים מוצפנת דורשת את שחרור הכונן. נא לספק את מילת הצופן "
+"הנוכחית של הכונן."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "הפעלה מחדש"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "להפעיל שירותים מחדש"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "שירותים מופעלים מחדש…"
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "מתבצעת הפעלה מחדש"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "החיבור משוחזר"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "המשך"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "שימוש מחדש בהצפנה קיימת"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "שימוש מחדש בהצפנה קיימת ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+#, fuzzy
+#| msgid "Review crypto policy"
+msgid "Review cryptographic policy"
+msgstr "סקירת המדיניות הקריפטוגרפית"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "היומנים נסקרים"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "ראונד-רובין"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "ניתובים"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "להריץ בתזמון"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "הרצת דוח חדש"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "להריץ על"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "הרצת דוח"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "שליח"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "פועל"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "זמן ריצה"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "שגיאות בקרת גישה מ־SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux מושבת במערכת"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux מושבת במערכת."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "מדיניות SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "המצב של SELinux אינו ידוע."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "פתרון תקלות ב־SELinux"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "בדיקת SMART עצמית של $0"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr "דיווח חירום (SOS) אוסף מידע על המערכת כדי לסייע בניתוח תקלות."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "מפתח SSH"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "מפתח ה־SSH אינו נתיב"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "מפתחות SSH"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "השהיית העברת STP"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "זמן Hello של STP"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "גיל ההודעה המרבי של STP"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "עדיפות STP"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"משתמשי Safari צריכים לייבא ולתת אמון באישור מרשות האישורים שנחתמה עצמית:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "שבתות"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "שמירה"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "שמירה והפעלה מחדש"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "שמירת השינויים"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "ניתן לחסוך במקום על ידי דחיסת בלוקים עם LZ4 באופן פרטני"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "ניתן לחסוך במקום על ידי אחסון בלוקים עם נתונים זהים פעם אחת בלבד"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"שמירת מילת צופן חדשה דורשת את שחרור הכונן. נא לספק את מילת הצופן הנוכחית של "
+"הכונן."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "תוזמן כיבוי ל־$0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "תוזמנה הפעלה מחדש ל־$0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "מחשב במארז אטום"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "חיפוש"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "שם תחום לחיפוש"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "חיפוש אחר שם או מזהה"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "חיפוש אחר שם, קבוצה או מזהה"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "שנייה חייבת להיות מספר בין 0 ל־59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "שניות"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "מפתחות מעטפת מאובטחת"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "$target נמחק בצורה מאובטחת"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "הגדרות ופתרון תקלות של Security Enhanced Linux"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "יש עדכוני אבטחה"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "עדכוני אבטחה בלבד"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "עדכוני אבטחת מידע יחולו ב$0 בשעה $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "בחירה"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "בחירת מזהה"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "בחירת שיטה"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "שליחה"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "שליחה"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "מספר סידורי"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "שרת"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "כתובת השרת"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "כתובת השרת לא יכולה להיות ריקה."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "השרת לא יכול להיות ריק."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "השרת סגר את החיבור."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "התכנה בשרת"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "שירות"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "יש שגיאה בשירות"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "יומני שירות"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "שירותים"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "שירותים שמשתמשים במיקום"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "הפעלה"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "תוקף ההפעלה עומד לפוג"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "הגדרה"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "הגדרת שם מארח"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "יצירת מחיצה על $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "הגדרת סיסמה"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "הגדרת שעה"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "הגדרה לכדי"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "הקמה"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "הגדרת סיסמה חלשה"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr "ההגדרה חורגת מהמצב המוגדר ותוחזר למצבה המקורי עם העלייה הבאה."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "מתבצעת הקמה"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "קם התקן לולאה $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "הגדרות"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "דרגת חומרה"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "משותף"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "מעטפת"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "סקריפט מעטפת"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "להציג את כל התהליכונים"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "להציג את כל התהליכונים"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Show confirmation password"
+msgstr "אישור סיסמה"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "הצגת טביעות אצבע"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "הצגת הודעות שמכילות את המחרוזת שסופקה."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "הצגת הודעות ליחידת ה־systemd שצוינה."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "הצגת הודעות לעלייה מסוימת."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "הצגת קשרים נוספים"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "הצגת הסיסמה"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "הצגת קשרים"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "כיווץ"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "כיווץ הכרך הלוגי"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+#, fuzzy
+#| msgid "partition"
+msgid "Shrink partition"
+msgstr "מחיצה"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "כיווץ כרך"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "כיבוי"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "כיבוי"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "מאז"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "שורה אחת"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "גודל"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "הגודל לא יכול להיות שלילי"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "הגודל לא יכול להיות אפס"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "הגודל גדול מדי"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "הגודל חייב להיות מספר"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "הגודל חייב להיות לפחות $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "דילוג על הניווט הראשי"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "דילוג לתוכן"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "משבצת"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "משבצת $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "תמונת מצב"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "שקעים"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "עדכוני תכנה"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"מעקפי תכנה מסייעים למנוע תקלות אבטחה במעבד. לאפחותים אלו יש השפעות לוואי "
+"בדמות הפחתת ביצועים. שינוי ההגדרות האלו הוא על אחריותך בלבד."
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "הפתרון חל בהצלחה"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "הפתרון נכשל"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "פתרונות"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr "תכנית אחרת משתמשת כרגע במנהל החבילות, נא להמתין…"
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "יש להפעיל חלק מהתכניות מחדש"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "מסודר מהכי מהימן להכי פחות"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "מחשב חסכוני במקום"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "פרוטוקול העץ הפורש"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "פרוטוקול העץ הפורש (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "עודף"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "זמן מסוים"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "מהירות"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "יציב"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "התחלה"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "התחלה והפעלה"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "הפעלת רב נתיבי"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "התחלת שירות"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr "להתחיל להציג רשומות בתאריך או לפני התאריך שצוין."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "להתחיל להציג רשומות בתאריך או אחרי התאריך שצוין."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "הופעל"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "התקן ה־RAID‏ $target מתחיל"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "שטח ההחלפה $target מתחיל"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "מצב"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "סטטי"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "מצב"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "מחשב מקלון"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "דביק"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "עצירה"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "עצירה והשבתה"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "עצירה והסרה"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "עצירה וניתוק"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "עצירת התקן"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "הפסקת עריכת מארחים"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "עצירת הדוח"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "התקן ה־RAID‏ $target נעצר"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "שטח ההחלפה $target נעצר"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "אחסון"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "לא ניתן לנהל את האחסון במערכת הזאת."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "יומני אחסון"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "אחסון מילת צופן"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "מילת צופן מאוחסנת"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "התקן בלוק בגודל $0"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "Add block devices"
+msgid "Stratis block devices"
+msgstr "הוספת התקני בלוק"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+#, fuzzy
+#| msgid "VDO backing devices can not be made smaller"
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "אין אפשרות להקטין התקני גיבוי VDO"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystem"
+msgstr "יצירת מערכת קבצים"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems"
+msgstr "יצירת מערכת קבצים"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems pool"
+msgstr "יצירת מערכת קבצים"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "מאגר Stratis‏"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:97
+#, fuzzy
+#| msgid "Set password"
+msgid "Strong password"
+msgstr "הגדרת סיסמה"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "בדל (Stub)"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "סגנון"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "תת שלדה"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "תת מחברת"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "הרישום ל־signals (אותות) של systemd נכשל: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "יצירת הכרך נכשלה"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "ההעתקה ללוח הגזירים צלחה"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "ההעתקה ללוח הגזירים צלחה!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "ימי ראשון"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "החלפה"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "אי אפשר לשנות כאן גודל של $0 ממערכות הקבצים."
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "יציאה מההחלפה"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "כיבוי $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "כיבוי $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "הדלקת $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "מעבר לגישה ניהולית"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "העברה לגישה מוגבלת"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "כיבוי $0 יקטע את החיבור לשרת וגרום לכך שמנשק הניהול לא יהיה זמין."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "הפעלת $0 תקטע את החיבור לשרת ותגרום לכך שמנשק הניהול לא יהיה זמין."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "מסונכרן"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "מסונכרן עם $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "מתבצע סנכרון"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "התקן ה־RAID‏ $target מסונכרן"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "מערכת"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "ניתוח המערכת"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "פרטי המערכת"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "המערכת עדכנית"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "שינויים במערכת"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "שעון המערכת"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "יחידות Systemd"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "מחשב לוח"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "שרת מפתחות Tang"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "יעד"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "יעדים"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "ציוות"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "פתחת ציוות"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "הגדרות פתחת ציוות"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "מסוף"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "חיסול הפעלה"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "בדיקת הגדרות"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "הבדיקה זמינה רק כאשר השירות kdump פעיל."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "בדיקת הגדרות kdump"
+
+#: pkg/kdump/kdump-view.jsx:374
+#, fuzzy
+#| msgid ""
+#| "This will test kdump settings by crashing the kernel and thereby the "
+#| "system. Depending on the settings, the system may not automatically "
+#| "reboot and the process may take a while."
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"פעולה זו תבדוק את ההגדרות של kdump על ידי הקרסת הליבה ולכן גם את המערכת. "
+"בהתאם להגדרות, המערכת עשויה לפעול מחדש אוטומטית והתהליך עשוי לארוך זמן מה."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "החיבור נבדק"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "החבילה $0 אינה זמינה באף מאגר."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "החבילה $0 חייבת להיות מותקנת כדי ליצור מאגרי Stratis."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "החבילה $0 חייבת להיות מותקנת."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "החבילה $0 תותקן כדי ליצור התקני VDO."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "כתובת ה־IP או שם המארח לא יכולים להכיל רווח."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "מערך ה־RAID נמצא במצב ירוד"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr "התקן ה־RAID חייב לפעול כדי להסיר כוננים."
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr "מפתח ה־SSH‏ $0 של $1 על גבי $2 יתווסף לקובץ $3 של $4 על גבי $5."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"מפתח ה־SSH‏ $0 יהפוך לזמין עד ליציאה מהמערכת ויהיה זמין לכניסה למארחים אחרים "
+"גם כן."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"מפתח ה־SSH לכניסה אל $0 מוגן בסיסמה, והמארח לא מרשה להיכנס למערכת עם סיסמה. "
+"נא לספק את הסיסמה למפתח שב־$1."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"מפתח ה־SSH לכניסה אל $0 מוגן. יש לך אפשרות להיכנס עם שם המשתמש והסיסמה שלך "
+"או על ידי אספקת הסיסמה למפתח שב־$1."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr "החשבון ‚$0’ יאולץ להחליף את הסיסמה שלו עם כניסתו הבאה למערכת"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "שירות ה־cockpit נכלל אוטומטית"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr "המצב שמוגדר אינו ידוע, זה עשוי להשתנות בעלייה הבאה של המערכת."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr "יצירת התקן ה־VDO הזה לא הסתיימה ולא ניתן להשתמש בהתקן."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr "המשתמש שמחובר כרגע אינו מורשה לצפות במידע על מפתחות."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr "יש לשחרר את הכונן בטרם פרמוטו. נא לספק מילת צופן נוכחית."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "לא ניתן למחוק את הקובץ $0."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "למערכת הקבצים אין נקודת עגינה קבועה."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "למערכת הקבצים אין נקודת עגינה קבועה."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"מערכת הקבצים מוגדרת להתעגן אוטומטית עם הפעלת המערכת אבל מכולת ההצפנה שלה לא "
+"תשוחרר באותו הזמן."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr "מערכת הקבצים מעוגנת כעת אך העיגון ינותק לאחר העלייה הבאה של המערכת."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"מערכת הקבצים מעוגנת למיקום $0 אך תעוגן למיקום $1 עם העלייה הבאה של המערכת."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr "מערכת הקבצים מעוגנת למיקום $0 אך לא תעוגן עם העלייה הבאה של המערכת."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr "מערכת הקבצים אינה מעוגנת כעת אך היא תעוגן עם ההפעלה הבאה של המערכת."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "מערכת הקבצים אינה מעוגנת."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"מערכת ההפעלה תשוחרר ותעוגן בעלייה הבאה. יכול להיות שצריך למלא מילת צופן."
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "טביעת האצבע אמורה להיות תואמת:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "השירות הבא יופעל מחדש:"
+msgstr[1] "השירותים הבאים יופעלו מחדש:"
+msgstr[2] "השירותים הבאים יופעלו מחדש:"
+msgstr[3] "השירותים הבאים יופעלו מחדש:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "השם המלא לא יכול להכיל נקודתיים."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "מזהה הקבוצה חייב להיות מספר שלם וחיובי"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"שם הקבוצה יכול להיות מורכב מהתווים a-z, ספרות, נקודות, מינוסים וקווים תחתונים"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr "תיקיית הבית $0 כבר קיימת. הבעלות עליה תועבר לידי המשתמש החדש."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "יש לייצר את ה־initrd מחדש."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "סיסמת המפתח לא יכולה להישאר ריקה"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "סיסמאות המפתח אינן תואמות"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "המפתח שסיפקת אינו תקף."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "לא ניתן להסיר את משבצת המפתח האחרונה"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "התהליכים והשירותים המוצגים ייעצרו בכוח."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "התהליכים המוצגים ייעצרו בכוח."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "אי אפשר לעצור בכוח את השירותים שמופיעים."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "המשתמש הנוכחי שנכנס למערכת אינו מורשה לצפות בשינויים שבוצעו במערכת"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "המכונה מופעלת מחדש"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "נקודת העיגון $0 משמשת את התהליכים האלה:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "נקודת העיגון $0 משמשת את השירותים האלה:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "סיסמת המפתח החדשה לא יכולה להישאר ריקה"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "סיסמת המפתח לא יכולה להישאר ריקה"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "הסיסמאות אינן תואמות"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "הסיסמאות אינן תואמות."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr "זה בסדר לשתף את טביעת האצבע באופן ציבורי, לרבות בדוא״ל."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr "השרת סירב לאמת את ‚$0’ עם אימות בסיסמה ואין שיטות אימות אחרות נתמכות."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr "השרת סירב לאמת בעזרת השיטות הנתמכות."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"המערכת לא תומכת כרגע בשחרור מערכת הקבצים עם שרת מפתחות Tang במהלך העלייה."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr "המערכת לא תומכת כרגע בשחרור מערכת הקבצים עם שרת מפתחות Tang."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr "המשתמש $0 אינו מורשה לשנות את אפחותי אבטחת המעבד"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "המשתמש $0 אינו מורשה להחליף בין ערכות מדיניות קריפטוגרפיות"
+
+#: pkg/kdump/kdump-view.jsx:505
+#, fuzzy
+#| msgid "The user $0 is not permitted to create timers"
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "המשתמש $0 אינו מורשה ליצור מתזמנים"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr "על המשתמש לצאת ולהיכנס בחזרה למערכת כדי שההגדרות החדשות תיכנסנה לתוקף."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"שם המשתמש יכול להיות מורכב מהתווים a-z, ספרות, נקודות, מינוסים וקווים "
+"תחתונים."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr "הגדרות הדפדפן מונעות מ־Cockpit לפעול ($0 בלתי נגיש)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "אין דפים פעילים כרגע"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr "יש התקנים עם מגוון נתיבים במערכת אך שירות ריבוי הנתיבים אינו פועל."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "אין שירותים פעילים באזור הזה"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "אין מפתחות ציבוריים מורשים לחשבון זה."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "הייתה שגיאה בלתי צפויה בעת ההתחברות למכונה."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "הצעדים הנוספים האלו הכרחיים:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "אלו השינויים שיתבצעו:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "כרך לוגי צר"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "מאגר לכרכים באפסנה צרה"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr "עיגון NFS זה נמצא בשימוש ואפשר לשנות רק את האפשרויות שלו."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "התקן VDO זה אינו משתמש בכל ההתקן המגבה שלו."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "לא ניתן לנהל את ההתקן הזה כאן."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "לא ניתן לנהל את ההתקן הזה כאן."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "ההתקן הזה בשימוש כרגע."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "שדה זה לא יכול להישאר ריק"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "הקבוצה הזאת היא הקבוצה העיקרית של המשתמשים הבאים:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "מארח זה יופעל מחדש לאחר התקנת העדכונים."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "המידע הזה מאוחסן רק במערכת."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "בכרך לוגי זה לא נעשה שימוש מלא על ידי התוכן שלו."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "מכונה זו כבר נוספה."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "פעולה זו עשויה לארוך זמן מה"
+
+#: pkg/storaged/partitions/partition.jsx:204
+#, fuzzy
+#| msgid "This logical volume is not completely used by its content."
+msgid "This partition is not completely used by its content."
+msgstr "בכרך לוגי זה לא נעשה שימוש מלא על ידי התוכן שלו."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:333
+#, fuzzy
+#| msgid "This VDO device does not use all of its backing device."
+msgid "This pool does not use all the space on its block devices."
+msgstr "התקן VDO זה אינו משתמש בכל ההתקן המגבה שלו."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "המאגר הזה נמצא במצב ירוד."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "מערכת זו אינה רשומה"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "מערכת זו משתמשת בפרופיל מותאם אישית"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "מערכת זו משתמשת בפרופיל המומלץ"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"הכלי הזה מגדיר את המדיניות של SELinux ויכול לסייע והבנת ופתרון הפרות של "
+"המדיניות."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr "הכלי הזה מגדיר למערכת לכתוב את היטלי הקריסה של הליבה לכונן."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"כלי זה מייצר ארכיון של הגדרות ופרטי ניתוח של המערכת. אפשר לאחסן את הארכיון "
+"מקומית או באופן מרכזי למטרות תיעוד או מעקב או לנציגי תמיכה, מתכנתים או מנהלי "
+"מערכות כדי שיוכלו לסייע באיתור תקלות טכניות וניפוי שגיאות."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"כלי זה מנהל אחסון מקומי כגון מערכות קבצים, קבוצות כרכים ב־LVM2 ועיגונים של "
+"NFS."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "יחידה זו לא תוכננה להפעלה באופן מפורש."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "שם משתמש זה כבר קיים"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr ""
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr "דפדפן זה מיושן מכדי להריץ את המסוף לדפדפן (חסר $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"הבחירה באפשרות הזאת תוסיף התאמה ל־‚_BOOT_ID=‎’. אם לא נבחרה האפשרות יופיעו רק "
+"שורות התיעוד של הטעינה הנוכחית. אם מזהה הטעינה הושמט, היסט חיובי יחפש את "
+"הטעינות מתחילת היומן והיסט שווה או קטן מאפס יחפש טעינות החל מסוף היומן. "
+"בהתאם לכך, 1 זאת הטעינה הראשונה שנמצאת ביומן בסדר כרונולוגי, 2 היא השנייה "
+"וכן הלאה, בעוד ‎-0 היא הטעינה האחרונה, ‎-1 היא הטעינה לפני האחרונה וכן הלאה."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"פעולה זו תוסיף התאמה לביטויים ‚‎_SYSTEMD_UNIT=‎’,‏ ‚COREDUMP_UNIT=‎’ ו־‚UNIT=‎’ "
+"כדי למצוא את כל ההודעות האפשריות ליחידה מסוימת. יכולה להכיל יותר מיחידה אחת "
+"אם מפרידים ביניהן בפסיקים. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "הגדרה זו תאפשר לך להיכנס ללא סיסמה בעתיד."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"אזור זה מכיל את שירות ה־Cockpit. נא לוודא שאזור זה אינו חל על חיבור המסוף "
+"המקוון הנוכחי שלך."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "ימי חמישי"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "רמה"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "שעה"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "אזור זמן"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "יצירת קוצב הזמן נכשלה"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "מחיקת קוצב הזמן נכשלה"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "מתזמנים"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"עצה: אפשר להגדיר את סיסמת המפתח שלך כמו סיסמת הכניסה שלך כדי להתאמת אוטומטית "
+"מול מערכות אחרות."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"כדי לוודא שהחיבור שלך לא מיורט על ידי גורמי צד־שלישי זדוניים, נא לאמת את "
+"טביעת האצבע של מפתח המארח:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"כדי לקבל עדכוני תכנה, יש לרשום את המערכת הזאת ב־Red Hat, או דרך פורטל "
+"הלקוחות של Red Hat או דרך שרת מינוי מקומי."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"כדי לאמת את טביעת האצבע, יש להריץ את הפקודה הבאה על $0 במהלך ישיבה פיזית מול "
+"המכונה או דרך רשת מהימנה:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "היום"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "בורר"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "החלפת מצב בורר תאריכים"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "החלפת מצב מסננים"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "יותר מדי נתונים"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "כלים"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "5 שירותי המעבד המובילים"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "5 שירותי ניצולת הכונן המובילים"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "5 שירותי הזיכרון המובילים"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "גודל כולל: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "מארז גבוה"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "זמני"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "בהעברה"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "הקפצה"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "מוקפץ על ידי"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "הקפצות"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "איתור תקלות"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "פתרון תקלות…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "מתן אמון והוספת מארח"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "לתת אמון במפתח"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "רמת אמון"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "לנסות שוב"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "מתבצע ניסיון להסתנכרן מול $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "ימי שלישי"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "ההפעלה של Tuned נכשלה"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned הוא שירות שעוקב אחר המערכת שלך וממטב את הביצועים תחת עומסים מסוימים. "
+"הליבה של Tuned הם פרופילים, שמכוונים את המערכת שלך למגוון מקרי בוחן."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned אינו זמין"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned אינו מופעל"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned כבוי"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "הפעלת גישה ניהולית"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "סוג"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "השם לא יכול להכיל את התו ‚$0’."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "יש להקליד כדי לסנן"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "מזהה ייחודי"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "לא ניתן להחיל את הפתרון הזה אוטומטית"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "לא ניתן להתחבר לכתובת הזו"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "לא ניתן לצור קשר עם $0."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"לא ניתן ליצור קשר עם המארח שסופק $0. נא לוודא שיש לו ssh פעיל בפתחה $1 או "
+"לציין פתחה אחרת בכתובת."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "לא ניתן לקבל פרטי אזעקה."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"לא ניתן להיכנס אל $0 באמצעות אימות עם מפתח SSH. נא לספק את הסיסמה. ייתכן "
+"שעידף לך להגדיר את מפתחות ה־SSH שלך לכניסה אוטומטית."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"לא ניתן להיכנס אל $0. המארח לא מקבל כניסה עם סיסמה או אף אחד ממפתחות ה־SSH "
+"האחרים שלך."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "לא ניתן להגיע לשרת"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "לא ניתן להסיר עיגון"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+#, fuzzy
+#| msgid "Encrypted logical volume of $0"
+msgid "Unable to repair logical volume $0"
+msgstr "כרך לוגי מוצפן בגודל $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "לא ניתן להריץ תיקון: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "לא ניתן לשמור הגדרות"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "לא ניתן לשמור את ההגדרות: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "לא ניתן להפעיל את setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "לא ניתן לנתק את מערכת הקבצים"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "לא זמין"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "חבילות לא זמינות"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "ביטול שינוי"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "שגיאת PackageKit בלתי צפויה במהלך התקנת $0:‏ $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "שגיאה בלתי צפויה"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "נתונים לא מזוהים"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "יחידה"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "לא ידוע"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "„$0” לא ידוע"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "לא ידוע ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "יישום לא ידוע"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "הגדרות לא ידועות"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "שם המארח לא ידוע"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "שם השירות לא ידוע"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "סוג לא ידוע"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "שחרור"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "לשחרר אוטומטית עם העלייה"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "שחרור מאגר Stratis מוצפן"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "שחרור המפתח $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "$target משוחרר"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "הכונן משוחרר"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "מנשקים לא מנוהלים"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "ניתוק"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "ניתוק מערכת הקבצים $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "לנתק כעת"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "$target מנותק"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "ללא שם"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "שחרור היחידה"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "נתונים לא מזוהים"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "Unrecognized data can not be made smaller here."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "לא ניתן להקטין כאן נתונים בלתי מזוהים."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "לא ניתן להקטין כאן נתונים בלתי מזוהים."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "כרך לא נתמך"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "עד"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "מארח בלתי מהימן"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "עדכון"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "טבלת עדכונים מוצלחים"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "היסטוריית עדכונים"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "פרטי חבילת עדכון"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "העדכון הצליח"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "מעודכנת"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr "ייתכן שהחבילות שעודכנו תדרושנה הפעלה מחדש כדי להיכנס לתוקף."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "עדכונים זמינים"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "היסטוריית עדכונים"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "עדכונים יחולו ב$0 בשעה $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "מתבצע עדכון"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "המצב מתעדכן…"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "זמן פעילות"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "שימוש"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "שימוש מתוך $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "שימוש"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "להשתמש בדחיסה"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "להשתמש בהסרת כפילות"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "להשתמש במפתח"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "להשתמש בסיסמה"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "להשתמש במפתחות הבאים כדי להתאמת מול שרתים אחרים"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "להשתמש בתיעוד מפורט"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "בשימוש"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr "חיוני לעיגוני רשות או שדורשים התערבות (כמו מילת צופן)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "משתמש"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "מזהה משתמש"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "מזהה משתמש זה כבר בשימוש על ידי משתמש אחר"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "מזהה המשתמש חייב להיות מספר שלם וחיובי"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "מזהה המשתמש לא יכול להיות גדול מ־$0"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "מזהה המשתמש לא יכול להיות קטן מ־$0"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "שם משתמש"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "שם המשתמש לא יכול להיות ריק"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "שם משתמש"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "באמצעות הצפנת LUKS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "באמצעות שרת Tang"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "אין אפשרות להקטין התקני גיבוי VDO"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "התקן VDO‏ $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "כרך מערכת קבצים VDO (דחיסה/הסרת כפולים)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "מזהה VLAN"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "הכתובת מתוקפת"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "אסימון האימות מתוקף"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "ספק"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "מאומת"
+
+#: pkg/shell/hosts_dialog.jsx:489
+#, fuzzy
+#| msgid "Fingerprint"
+msgid "Verify fingerprint"
+msgstr "טביעת אצבע"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "אימות מפתח"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "מתבצע אימות"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "גרסה"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "$target נמחק באופן מאוד מאובטח"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "הצגת כל המעבדים"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "הצגת כל הכוננים"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "הצגת כל היומנים"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "הצגת כל השירותים"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "הצגת סקריפט אוטומציה"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "הצגת יומנים מפורטים"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "הצגת פרטי חומרה"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "הצגת היסטוריית כניסות למערכת"
+
+#: pkg/storaged/stratis/pool.jsx:351
+#, fuzzy
+#| msgid "View all logs"
+msgid "View logs"
+msgstr "הצגת כל היומנים"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "הצגת מדדים והיסטוריה"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "הצגת תעבורה לפי כונן"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "הצגת אתר המיזם"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "הצגת דוח"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "הצגת יומן עדכון"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "צפייה בפרטי זיכרון דורשת גישה ניהולית."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "ביקור בחומת האש"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "קבוצת כרכים"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "Remove missing physical volumes?"
+msgid "Volume group is missing physical volumes"
+msgstr "להסיר את הכרכים הפיזיים החסרים?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "גודל הכרך הוא $0. גודל התוכן הוא $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "בהמתנה"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "בהמתנה לפרטים…"
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "בהמתנה לקלט…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr "בהמתנה לתכניות אחרות שתסיימנה להשתמש במנהל החבילות…"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "בהמתנה לסיום פעולות ניהול תכנה אחרות"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "בהמתנה להפעלה…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "נדרש על ידי"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "דורש"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "אזהרה ומעלה"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+#, fuzzy
+#| msgid "Set weak password"
+msgid "Weak password"
+msgstr "הגדרת סיסמה חלשה"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "מסוף מקוון"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "מסוף מקוון לשרתי לינוקס"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "המסוף המקוון יופעל מחדש"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "המסוף המקוון מופעל במצב גישה מוגבלת."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "לוגו המסוף המקוון"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "ימי רביעי"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "שבועי"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "שבועות"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "מתי"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "כאשר ריק, להתחבר עם המשתמש הנוכחי"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"כאשר המסוף המקוון מופעל מחדש, לא יופיע עוד מידע על התהליך. עם זאת, תהליך "
+"העדכון ימשיך ברקע. יש להתחבר מחדש כדי להמשיך לצפות בתהליך העדכון."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "לבן"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:124
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "World wide name"
+msgid "World wide name"
+msgstr "שם בינלאומי (WWN)"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "כתיבה"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "בעיקר כתיבה"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "מתבצעת כתיבה"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "שם משתמש או סיסמה שגויים"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR (קסור)"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "שנתי"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "כן"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "זאת ההתחברות הראשונה שלך אל $0."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "אין לך הרשאה לשנות את חומת האש."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr "אין לך הרשאה לצפות במפתחות ציבוריים מורשים לחשבון זה."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "יצאת מהמערכת עקב חוסר פעילות."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "כדאי לך לנסות לטעון רשומות ישנות יותר."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr "יתכן שעדיף לך להחליף את הסיסמה של המפתח לטובת כניסה אוטומטית."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "עליך להמתין זמן רב יותר כדי להחליף את הסיסמה שלך"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr "יש להיכנס מחדש כדי שיתאפשר לך לצפות במדדים ובהיסטוריה"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "מעתה יש לך גישה ניהולית."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "המערכת תוציא אותך בעוד $0 שניות."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "החשבון שלך"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr "הדפדפן שלך לא מרשה להדביק מתפריט ההקשר. אפשר להשתמש ב־Shift+Insert."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "הדפדפן שלך יזכור את רמת הגישה שלך בין הפעלות."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"השרת שלך יסגור את החיבור בקרוב. באפשרותך להתחבר אליו מחדש לאחר שיופעל מחדש."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "ההפעלה שלך הושמדה."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "תוקף ההפעלה שלך פג. נא להיכנס שוב."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "אזור"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[נתונים בינריים]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[אין נתונים]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "גישה"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "פעיל"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "תוספת"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "תוספת"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "לאחר הרשת"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "יישומים"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "תג נכס"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "at"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "ממוצע: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "התקן גיבוי להתקן VDO"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "מאגד"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "עלייה"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "גשר"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "התקנים אחרים"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "התקנים אחרים"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs subvolume"
+msgstr "כרך אחסון"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "כרכי אחסון"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "כרך אחסון"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "תיקון תקלה"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "בתים"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "מכלא"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "החלפה"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman אינו מותקן"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "פקודה"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "מסוף"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "היטל ליבה"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "מעבד"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "קריסה"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "נתוני $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "תאריך"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "השבתה"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "ניפוי שגיאות"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "מחיקה"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "השבתה"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "כונן"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "כוננים"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "שם תחום"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "כונן"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "עריכה"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "הפעלה"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted"
+msgid "encrypted"
+msgstr "מוצפן"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "הצפנה"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "שיפור"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "שגיאה"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "כל יום שישי"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "כל יום שני"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "כל שבת"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "כל יום ראשון"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "כל יום חמישי"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "כל יום שלישי"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "כל יום רביעי"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "כל יום"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "הרחבה"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "הצגת מפתחות ה־ssh של המארח נכשלה: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "מערכת קבצים"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "חומת אש"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "לליבות נוכחיות ועתידיות"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "לליבה הנוכחית בלבד"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "פרמוט"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "מ־$0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "מ־$0 על גבי $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "תרשימים"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "הגדלה"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "חומרה"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "היסטוריה"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "מארח"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "יעד iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "יעדי iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "הוספת שער גישה ל־iSCSI"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "התעלמות מכשל"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "ברוב הדפדפנים"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "חסר אחידות"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "אתחול"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "התקנה"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "מנשק"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "שגוי: הוגדרו מספר יעדים"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "ז׳ורנל"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "מדריך השימוש ב־journalctl"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "מצב kdump"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "מפתחות"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "כניסה"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "מכונה"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "מסכה"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "מרבי: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "member of RAID device"
+msgid "member of MDRAID device"
+msgstr "חבר בהתקן RAID"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "חבר במאגר Stratis"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "זיכרון"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "מדדים"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "אפחות"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "פרטים נוספים"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "עיגון"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "רשת"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "לעולם לא לעגן בעלייה"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "ה־export של ה־nfs ריק"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "שרת ה־nfs ריק"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "לשרת ה־nfs אין כתובת IPv6 תקנית"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "nice"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "אין"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "מתוך מעבד אחד"
+msgstr[1] "מתוך $0 מעבדים"
+msgstr[2] "מתוך $0 מעבדים"
+msgstr[3] "מתוך $0 מעבדים"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "על גבי $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "מערכת הפעלה"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "מערכת הפעלה"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "חבילה"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "מחיצה"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "סיסמה"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "איכות הסיסמה"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "טלאים"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "נתיב"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "ביצועים"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "כרך פיזי של קבוצת כרכים מסוג LVM2"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "תוסף"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service נכשל"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service נכשל באיסוף נתונים"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "השירות ‚pmlogger.service’ אינו פעיל"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "פּוֹד (pod)"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "פתחה"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "חשמל"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "זיכרון"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "קריאה בלבד"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "מומלץ"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "הסרה מ־LVM2"
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "remove from RAID"
+msgid "remove from MDRAID"
+msgstr "הסרה מה־RAID"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "הסרה מ־LVM2"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "הפעלה מחדש"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "תפקידים"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "פועל $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "אבטחה"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "טורי"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "שירות"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "מעטפת"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "להציג פחות"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "להציג יותר"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "כיווץ"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "לסגור"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "שקע"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "פתרון"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "פרטי הפתרון"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "חירום"
+
+#: pkg/sosreport/sosreport.jsx:183
+#, fuzzy
+#| msgid "Reporting failed"
+msgid "sos report failed"
+msgstr "הדיווח נכשל"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "מפתח ה־ssh אינו נתיב"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "שרת ה־ssh ריק"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "עצירה"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "עצירת העלייה בעת כשל"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+#, fuzzy
+#| msgid "Stopped"
+msgid "stopped"
+msgstr "נעצר"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "יעד"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "ציוות"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "זמן"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "קוצב זמן"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "יחידה"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "לא ידוע"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "יעד לא ידוע"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "הסרת מיסוך"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "ניתוק"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "מקום שאינו מחיצה ב־$0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "משתמש"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "שם משתמש"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#, fuzzy
+#| msgid "No description"
+msgid "using key description $0"
+msgstr "אין תיאור"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "גרסה"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "כרך"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "אזהרה"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "חבילת wireguard-tools לא מותקנת"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "כן"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "אזור"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr "השירות Kdump אינו מותקן. נא לוודא שהחבילה kexec-tools מותקנת."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "אין זיכרון שמור. יש להוסיף את האפשרות crashkernel לשורת הפקודה של הליבה "
+#~ "(כלומר תחת ‎/etc/default/grub) כדי לשמור זיכרון בזמן העלייה. למשל: "
+#~ "crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "השירות פעיל"
+
+#~ msgid "Service is starting"
+#~ msgstr "השירות מתחיל"
+
+#~ msgid "Service is stopped"
+#~ msgstr "השירות נעצר"
+
+#~ msgid "Service is stopping"
+#~ msgstr "השירות בהליכי עצירה"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "פעולה זו תבדוק את ההגדרות של kdump על ידי הקרסת הליבה."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "crashkernel אינו מוגדר בשורת הפקודה של הליבה"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (מוצפן)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "מאגר Stratis‏ $0"
+
+#~ msgid "$0 cache"
+#~ msgstr "מכלא $0"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 של רמה לא ידועה"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0,‏ $1 פנויים"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "יש להוסיף כונן חלופי לפני שניתן יהיה להסיר את הכונן הזה."
+
+#~ msgid "Backing device"
+#~ msgstr "התקן גיבוי"
+
+#~ msgid "Block"
+#~ msgstr "בלוק"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "קיבולת"
+
+#~ msgid "Content"
+#~ msgstr "תוכן"
+
+#~ msgid "Create devices"
+#~ msgstr "יצירת התקנים"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "התקן"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "קובץ התקן"
+
+#~ msgid "Devices"
+#~ msgstr "התקנים"
+
+#~ msgid "Drives"
+#~ msgstr "כוננים"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr "יש לשחרר כרכים מוצפנים לפני שניתן יהיה לשנות את גודלם."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "מערכת קבצים (מוצפנת)"
+
+#~ msgid "Filesystems"
+#~ msgstr "מערכות קבצים"
+
+#~ msgid "Free"
+#~ msgstr "פנוי"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "פינוי מקום בקבוצה הזו: יש לכווץ או למחוק כרכים לוגיים אחרים או להוסיף עוד "
+#~ "כרך פיזי."
+
+#~ msgid "Inactive volume"
+#~ msgstr "כרך לא פעיל"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "שרת מפתחות"
+
+#~ msgid "LVM2 member"
+#~ msgstr "חבר ב־LVM2"
+
+#~ msgid "Locked devices"
+#~ msgstr "התקנים נעולים"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "נתונים מוצפנים נעולים"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "דגם"
+
+#~ msgid "NFS mounts"
+#~ msgstr "עיגוני NFS"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "לא מותקנת תמיכה ב־NFS"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "לא הוקמו עיגוני NFS"
+
+#~ msgid "No devices"
+#~ msgstr "אין התקנים"
+
+#~ msgid "No drives attached"
+#~ msgstr "אף כונן לא מחובר"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "לא הוגדרו יעדי iSCSI"
+
+#, fuzzy
+#~| msgid "Block device for filesystems"
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "התקן בלוק למערכות הפעלה"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "נתונים אחרים"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "התקן בלוק שמחולק למחיצות"
+
+#, fuzzy
+#~| msgid "Passphrase"
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "מילת צופן"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "כאן ניתן לשנות גודל של כרכים פיזיים."
+
+#~ msgid "Pool"
+#~ msgstr "מאגר"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "מאגר לכרכים רזים"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "רמת RAID"
+
+#~ msgid "RAID member"
+#~ msgstr "חבר ב־RAID"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "כונן נתיק"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "להציג את המכשיר"
+#~ msgstr[1] "להציג את כל $0 המכשירים"
+#~ msgstr[2] "להציג את כל $0 המכשירים"
+#~ msgstr[3] "להציג את כל $0 המכשירים"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "להציג כונן אחד"
+#~ msgstr[1] "להציג את כל $0 הכוננים"
+#~ msgstr[2] "להציג את כל $0 הכוננים"
+#~ msgstr[3] "להציג את כל $0 הכוננים"
+
+#~ msgid "Show all"
+#~ msgstr "להציג הכול"
+
+#~ msgid "Source"
+#~ msgstr "מקור"
+
+#~ msgid "Start pool"
+#~ msgstr "הפעלת מאגר"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "יש להפעיל את המאגר כדי לראות את מערכות הקבצים."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "מצב"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "מאגר Stratis שנעצר"
+
+#~ msgid "Stratis member"
+#~ msgstr "חבר ב־Stratis"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "מאגר Stratis‏ $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "התמיכה מותקנת."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr "התקן ה־RAID חייב לפעול כדי להוסיף כוננים עודפים."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "לא ניתן להסיר את הכונן האחרון של התקן ה־RAID."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "לא ניתן להסיר את הכרך הפיזי האחרון של קבוצת כרכים."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "אין מספיק מקום פנוי היכנשהו כדי להסיר את הכרך הפיזי הזה. נדרשים לפחות עוד "
+#~ "$0 של מקום פנוי."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "לא ניתן להסיר את הכונן הזה בזמן שההתקן משתקם."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr "יש להפעיל את הכרך הזה לפני שיתאפשר לשנות את גודלו."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "מזהה ייחודי"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "נתונים לא מזוהים"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "שימוש"
+
+#~ msgid "Used for"
+#~ msgstr "משמש לטובת"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "גיבוי VDO"
+
+#~ msgid "VDO device"
+#~ msgstr "התקן VDO"
+
+#~ msgid "Volume"
+#~ msgstr "כרך"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "אוטומטית (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "לקבל את המפתח ולהתחבר"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "לא ניתן לשנות גודל של כרכים מוצפנים מכאן."
+
+#, fuzzy
+#~| msgid "Tang keyserver"
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "שרת מפתחות Tang"
+
+#, fuzzy
+#~| msgid "New passphrase"
+#~ msgid "Use a passphrase"
+#~ msgstr "מילת צופן חדשה"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "מפת סיביות"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "לא ניתן לשחרר את המאגר הזה כאן כיוון שתיאור המפתח שלו אינו בתבנית הנכונה."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "החלפת מצב מפת סיביות"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr "נא לוודא שגיבוב המפתח משרת ה־Tang תואם לאחד מהבאים:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "לבדוק ידנית עם SSH: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "מספר הפתחה והסוג אינם תואמים"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "מאגר Stratis מוצפן נעול"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "שגיאה בעת מחיקת אזעקה: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "לא ניתן לקבל אזעקה: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 בתים של נתונים בינריים]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "קפיצה פתאומית בפעולות קלט/פלט של כונן"
+
+#~ msgid "Event"
+#~ msgstr "אירוע"
+
+#~ msgid "Event logs"
+#~ msgstr "יומני אירועים"
+
+#~ msgid "Load spike"
+#~ msgstr "קפיצת עומס פתאומית"
+
+#~ msgid "Memory spike"
+#~ msgstr "קפיצה פתאומית בניצול הזיכרון"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "קפיצה פתאומית בפעולות קלט/פלט של רשת"
+
+#~ msgid "ssh key"
+#~ msgstr "מפתח ssh"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "אם האפשרות הזאת מסומנת, מערכת הקבצים לא תעוגן במהלך הטעינה הבעיה אפילו אם "
+#~ "עוגנה לפני כן. שימושי אם עיגון במהלך הטעינה אינו אפשרי, למשל כאשר נדרשת "
+#~ "מילת צופן כדי לשחרר את נעילת מערכת הקבצים אך תהליך הטעינה מונע את זה."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "לעולם לא לעגן בעלייה"
+
+#~ msgid "Listing unit files"
+#~ msgstr "קובצי ה־unit (יחידה) מוצגים"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "הצגת קובצי unit נכשלה: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "היחידה לא נמצאה"
+
+#~ msgid "Validating key"
+#~ msgstr "המפתח מתוקף"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "מחיקת הקבוצה $0"
+
+#~ msgid "Container administrator"
+#~ msgstr "מנהל המכולה"
+
+#~ msgid "Image builder"
+#~ msgstr "בונה תמונות"
+
+#~ msgid "Roles"
+#~ msgstr "תפקידים"
+
+#~ msgid "Server administrator"
+#~ msgstr "הנהלת השרת"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "קבוצת יוניקס: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "ההעתקה ללוח הגזירים צלחה"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "דוחות אבחון"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "היטל ליבה"
+
+#~ msgid "Software Updates"
+#~ msgstr "עדכוני תכנה"
+
+#~ msgid "Update log"
+#~ msgstr "יומן עדכונים"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "יעד היטל ה־nfs אינו כתוב בצורה server:path"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "דוחות אבחון"
+
+#~ msgid "Done!"
+#~ msgstr "בוצע!"
+
+#~ msgid "Download report"
+#~ msgstr "הורדת דוח"
+
+#~ msgid "No archive has been created."
+#~ msgstr "לא נוצר אף ארכיון."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "נא לאשר את המחיקה של $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "הארכיון שנוצר מכיל מידע שנחשב רגיש ועל מי שארגן את המקור לסקור אותו לפני "
+#~ "העברה לגורמי צד שלישי."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "כלי זה יאסוף את תצורת המערכת ואת פרטי האבחון מהמערכת הזאת לשימוש לטובת "
+#~ "אבחון בעיות במערכת."
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "חבילה זו אינה נתמכת בגרסה זו של Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "חבילה זו דורשת Cockpit בגרסה %s ומעלה"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "מדדי ביצועים"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "קריאה בלבד"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB סך הכול"
+
+#~ msgid "Apply"
+#~ msgstr "החלה"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "לא מורשה להסיר את האזור $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "יש ללחוץ על $0 שוב כדי להשתמש בסיסמה בכל זאת."
+
+#~ msgid "Access"
+#~ msgstr "גישה"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "הכונן כושל"
+
+#~ msgid "Logical Size"
+#~ msgstr "גודל לוגי"
+
+#~ msgid "Security updates "
+#~ msgstr "עדכוני אבטחה "
+
+#~ msgid "Updates "
+#~ msgstr "עדכונים "
+
+#~ msgid "(Optional)"
+#~ msgstr "(רשות)"
+
+#~ msgid "Active since"
+#~ msgstr "פעיל מאז"
+
+#~ msgid "Affected locations"
+#~ msgstr "מיקומים שהושפעו"
+
+#~ msgid "Process"
+#~ msgstr "תהליך"
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Remove and delete"
+#~ msgstr "עצירה ומחיקה"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and format"
+#~ msgstr "תצורת כניסה"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and grow"
+#~ msgstr "תצורת כניסה"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and initialize"
+#~ msgstr "תצורת כניסה"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and shrink"
+#~ msgstr "תצורת כניסה"
+
+#, fuzzy
+#~| msgid "Remove device"
+#~ msgid "Remove and stop device"
+#~ msgstr "הסרת התקן"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "מערכת הקבצים בשימוש על ידי הפעלות כניסה ושירותי מערכת. המשך הפעילות תעצור "
+#~ "אותן."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr "מערכת הקבצים בשימוש על ידי הפעלות כניסה. המשך הפעילות תעצור אותן."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr "מערכת הקבצים בשימוש על ידי שירותי מערכת. המשך הפעילות תעצור אותם."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "להתקן הזה יש מערכות קבצים שנמצאות בשימוש. המשך הפעילות ינתק את מערכת "
+#~ "הקבצים שבו."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "התקן זה נמצא בשימוש לטובת קבוצות כרכים."
+
+#, fuzzy
+#~| msgid ""
+#~| "This device is currently used for volume groups. Proceeding will remove "
+#~| "it from its volume groups."
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "התקן זה משמש כרגע לטובת קבוצות כרכים. המשך הפעילות תסיר אותו מקבוצות "
+#~ "הכרכים שלו."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "התקן זה משמש כרגע להתקני RAID."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "התקן זה משמש כרגע להתקני RAID. המשך הפעילות תסיר אותו מהתקני ה־RAID שלו."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "התקן זה נמצא בשימוש לטובת קבוצות כרכים."
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Unmount and delete"
+#~ msgstr "עצירה ומחיקה"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and format"
+#~ msgstr "לנתק כעת"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and grow"
+#~ msgstr "לנתק כעת"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and initialize"
+#~ msgstr "לנתק כעת"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and shrink"
+#~ msgstr "לנתק כעת"
+
+#, fuzzy
+#~| msgid "On a mounted device"
+#~ msgid "Unmount and stop device"
+#~ msgstr "על התקן מעוגן"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 מתוך $1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "חסום"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "כרך פיזי בגודל $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "חבר בהתקן ה־RAID‏ $0"
+
+#~ msgid "Menu"
+#~ msgstr "תפריט"
+
+#~ msgid "VDO backing"
+#~ msgstr "גיבוי VDO"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "יצירת מאגר אחסון"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "אפשרויות עיגון"
+
+#~ msgid "A disk is needed."
+#~ msgstr "צריך כונן."
+
+#~ msgid "Disk"
+#~ msgstr "כונן"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "ליישומים מיושנים בלבד. פוגע בביצועים."
+
+#~ msgid "Install VDO support"
+#~ msgstr "התקנת תמיכה ב־VDO"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "להשתמש בהדמיית 512 בתים"
+
+#~ msgid "System services"
+#~ msgstr "שירותי המערכת"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "יצירת דוח אבחון "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr "אין תמיכה בהתחברות במקביל ליותר מ־{{ limit }} מכונות."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "SSO מבוסס Kerberos"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "כניסה אל {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "ססמאות המפתח החדשות אינן תואמות"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "כדי לאמת את טביעת האצבע, יש להריץ את הפקודה הבאה על {{host}} במהלך ישיבה "
+#~ "פיזית מול המכונה או דרך רשת מהימנה:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "לא ניתן ליצור קשר עם {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "לא ניתן להיכנס אל {{#strong}}{{host}}{{/strong}}. לאפשרויות אימות נוספות "
+#~ "ולפתרון תקלות נא לשדרג את cockpit-ws לגרסה חדשה יותר."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "לא ניתן להיכנס אל {{#strong}}{{host}}{{/strong}}. כדי להתחבר למארח הזה "
+#~ "יהיה עליך להפעיל את אחת משיטות האימות הבאות בהגדרות ה־sshd של {{#strong}}"
+#~ "{{host}}{{/strong}}:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "זאת ההתחברות הראשונה שלך אל {{host}}."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "המפתח של {{host}} השתנה"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "ניקוי הגדרות נקודות עגינה"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr "יצירת מאגד זה תקטע את החיבור לשרת ותמנע את הגישה למנשק הניהול."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr "יצירת הגשר הזה תקטע את החיבור לשרת ותמנע את החיבור למנשק הניהול."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr "יצירת הציוות הזה תקטע את החיבור לשרת ותמנע את החיבור למנשק הניהול."
+
+#~ msgid "IP settings"
+#~ msgstr "הגדרות IP"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "כיבוי <b>$0</b> יקטע את החיבור לשרת ותגרום לכך שמנשק הניהול לא יהיה זמין."
+
+#~ msgid "Administrator password"
+#~ msgstr "ססמת ניהול"
+
+#~ msgid "Computer OU"
+#~ msgstr "יחידה ארגונית של מחשבים"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "מחיקת התקן RAID תמחק את כל הנתונים שעליו."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "מחיקת קבוצת כרכים תמחק את כל הנתונים שעליה."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "לא לשכתב על נתונים קיימים"
+
+#~ msgid "Erase"
+#~ msgstr "מחיקה"
+
+#~ msgid "Format disk $0"
+#~ msgstr "פרמוט הכונן $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr "פרמוט התקן אחסון ימחק את כל הנתונים שבו."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "שם מארח לא אמור להשתנות בשם תחום"
+
+#~ msgid "More"
+#~ msgstr "עוד"
+
+#~ msgid "Not joined"
+#~ msgstr "לא מצורף"
+
+#~ msgid "One time password"
+#~ msgstr "ססמה חד־פעמית"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 מתוך $1 על $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "שעה חייבת להיות ערך בין 0 ל־23"
+
+#~ msgid "Last login:"
+#~ msgstr "כניסה אחרונה:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "דקות חייבות להיות מספר בין 0 ל־59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] "היה ניסיון כניסה כושל למערכת מאז הכניסה המוצלחת האחרונה."
+#~ msgstr[1] "היו $0 ניסיונות כניסה כושלים למערכת מאז הכניסה המוצלחת האחרונה."
+#~ msgstr[2] "היו $0 ניסיונות כניסה כושלים למערכת מאז הכניסה המוצלחת האחרונה."
+#~ msgstr[3] "היו $0 ניסיונות כניסה כושלים למערכת מאז הכניסה המוצלחת האחרונה."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "למשל: „$0”"
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 אזורים פעילים"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "מופע אחד"
+#~ msgstr[1] "$0 מופעים"
+#~ msgstr[2] "$0 מופעים"
+#~ msgstr[3] "$0 מופעים"
+
+#~ msgid "Licensed under:"
+#~ msgstr "תחת הרישיון:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "שחרור עם העלייה"
+
+#~ msgid "Unlock read only"
+#~ msgstr "שחרור קריאה בלבד"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "חיפוש ביומנים עם שילוב של הביטויים:"
+
+#~ msgid "Show filters"
+#~ msgstr "הצגת מסננים"
+
+#~ msgid "Text"
+#~ msgstr "טקסט"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "כל מחרוזת חופשית כביטוי רגולרי"
+
+#~ msgid "e.g."
+#~ msgstr "למשל:"
+
+#~ msgid "log fields"
+#~ msgstr "שדות יומן"
+
+#~ msgid "qualifiers"
+#~ msgstr "קטגוריות"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "החלפה בין ערכות הגדרות"
+
+#~ msgid "(none)"
+#~ msgstr "(אין)"
+
+#~ msgid "Create timers"
+#~ msgstr "יצירת מתזמנים"
+
+#~ msgid "Recommended default"
+#~ msgstr "בררת מחדל מומלצת"
+
+#~ msgid "Run"
+#~ msgstr "הפעלה"
+
+#~ msgid "Select unit state"
+#~ msgstr "נא לבחור מצב יחידה"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "הפעלת מדדים מאוחסנים"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "הפעלת מדדים מאוחסנים"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "אילוץ הסרת מילת צופן מ־$0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "אם tang-show-keys אינו זמין, להפעיל את זה:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "מה אם tang-show-keys אינו זמין?"
+
+#~ msgid "key slot $0"
+#~ msgstr "משבצת מפתח $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "משהו השתבש"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "זה לא עבד, נא לנסות שוב"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "לא ניתן לקבל גישה ניהולית."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "לא מוגדרים עדכונים אוטומטיים"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "הגדרת המעבד $0"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "רשת $0"
+#~ msgstr[1] "$0 רשתות"
+#~ msgstr[2] "$0 רשתות"
+#~ msgstr[3] "$0 רשתות"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 זמין לרוב מערכות ההפעלה. כדי להתקין אותו, יש לחפש אותו ב„תכנה של "
+#~ "GNOME” או להריץ את הפקודה הבאה:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "התאמת זיכרון עבור $0"
+
+#~ msgid "$0 network"
+#~ msgstr "רשת $0"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "מעבד וירטואלי אחד"
+#~ msgstr[1] "$0 מעבדים וירטואליים"
+#~ msgstr[2] "$0 מעבדים וירטואליים"
+#~ msgstr[3] "$0 מעבדים וירטואליים"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "פרטי המעבד הווירטואלי $0"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "הגדרות מנשק הרשת הווירטואלי $0"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "יש להפעיל את מאגר האחסון כדי לנהל כרכים"
+
+#~ msgid "Add network interface"
+#~ msgstr "הוספת מנשק רשת"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "הוספת מנשק רשת וירטואלי"
+
+#~ msgid "Additional"
+#~ msgstr "נוסף"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "הכתובת אינה בתת־הרשת"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr "לאחר מחיקת תמונת המצב, כל התוכן שנלכד בה יאבד."
+
+#~ msgid "Always attach"
+#~ msgstr "להצמיד תמיד"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr "חיבורו יהפוך את הכונן למשותף לכל מכונה וירטואלית שמשתמשת בו."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "הפעלת libvirt אוטומטית עם ההפעלה"
+
+#~ msgid "Autostart"
+#~ msgstr "התחלה אוטומטית"
+
+#~ msgid "Boot order"
+#~ msgstr "סדר עלייה"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "לא ניתן לשמור הגדרות סדר עלייה"
+
+#~ msgid "Bus"
+#~ msgstr "אפיק"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "תקליטור/DVD"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "לא ניתן לשמור את הגדרות המעבד"
+
+#~ msgid "CPU type"
+#~ msgstr "סוג מעבד"
+
+#~ msgid "Change firmware"
+#~ msgstr "החלפת קושחה"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "השינויים יחולו לאחר הפעלת המכונה הווירטואלית מחדש"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "נא לבחור מערכת הפעלה"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr "לחיצה על „הפעלת מציג חיצוני” תוריד קובץ ‎.vv ותפעיל את $0."
+
+#~ msgid "Clone"
+#~ msgstr "שכפול"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "לא ניתן לטעון תמונה"
+
+#~ msgid "Confirm this action"
+#~ msgstr "אישור הפעולה הזאת"
+
+#~ msgid "Connect"
+#~ msgstr "התחברות"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "ניתן להתחבר עם כל יישום שמציג עבור הפרוטוקולים הבאים"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "מתבצעת התחברות לשירות וירטואליזציה"
+
+#~ msgid "Connection"
+#~ msgstr "התחברות"
+
+#~ msgid "Console"
+#~ msgstr "מסוף"
+
+#~ msgid "Cores per socket"
+#~ msgstr "ליבות לתושבת"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "לא ניתן להחזיר לתמונת המצב"
+
+#~ msgid "Crashed"
+#~ msgstr "קרס"
+
+#~ msgid "Create VM"
+#~ msgstr "יצירת מכונה וירטואלית"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "יצירת שכפול של מכונה וירטואלית על בסיס $0"
+
+#~ msgid "Create new"
+#~ msgstr "יצירת חדש"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "יצירת מכונה וירטואלית חדשה"
+
+#~ msgid "Create virtual network"
+#~ msgstr "יצירת רשת וירטואלית"
+
+#~ msgid "Creating VM"
+#~ msgstr "יצירת מכונה וירטואלית"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "יצירת התקנת מכונה וירטואלית"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "היצירה של המכונה הווירטואלית $0 נכשלה"
+
+#~ msgid "Creation time"
+#~ msgstr "זמן יצירה"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "ההקצאה הנוכחית"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "קושחה מותאמת אישית: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "טווח DHCP"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "מחיקת קובצי אחסון משויכים:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "מחיקת מאגר אחסון $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "מחיקת הכרכים שבתוך המאגר הזה"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr "מחיקת מאגר אחסון בלתי פעיל תסיר את הגדרת המאגר. התוכן שבו לא יימחק."
+
+#~ msgid "Desktop viewer"
+#~ msgstr "מציג שולחנות עבודה"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "יש לנתק את הכוננים שמשתמשים במאגר הזה מכל מכונה וירטואלית בטרם ביצוע "
+#~ "ניסיון מחיקה."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "מנותק מהמסוף הטורי. יש ללחוץ על כפתור החיבור."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "ניתוק הכונן $0 מהמכונה הווירטואלית $1 נכשל"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "חיבור הכונן נכשל"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "יצירת הכונן נכשלה"
+
+#~ msgid "Disk image file"
+#~ msgstr "קובץ דמות כונן"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "לא ניתן לשמור את הגדרות הכונן"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "תמונת מצב בכונן בלבד"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "שם התחום קרס"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "שם התחום חסום במשאב"
+
+#~ msgid "Download an OS"
+#~ msgstr "הורדת מערכת הפעלה"
+
+#~ msgid "Dying"
+#~ msgstr "גוסס"
+
+#~ msgid "Emulated machine"
+#~ msgstr "מכונה מדומה"
+
+#~ msgid "End"
+#~ msgstr "סוף"
+
+#~ msgid "End should not be empty"
+#~ msgstr "הסוף אמור להיות ריק"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "תמונת כונן קיימת במערכת הקבצים של המארח"
+
+#~ msgid "Expand"
+#~ msgstr "הרחבה"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "החלפת הקושחה נכשלה"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "קבלת כתובות ה־IP של המנשקים שקיימים תחת $0 נכשלה"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "שליחת צירוף המקשים Ctrl+Alt+$0 למכונה הווירטואלית $1 נכשלה"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr "יש להפעיל פחות מכמות המעבדים הווירטואלים המרבית."
+
+#~ msgid "File"
+#~ msgstr "קובץ"
+
+#~ msgid "Filter by name"
+#~ msgstr "סינון לפי שם"
+
+#~ msgid "Firmware"
+#~ msgstr "קושחה"
+
+#~ msgid "Force shut down"
+#~ msgstr "לאלץ כיבוי"
+
+#~ msgid "Forward mode"
+#~ msgstr "מצב העברה"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "מצב העברה"
+
+#~ msgid "Generate automatically"
+#~ msgstr "לייצר אוטומטית"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "מעבר לרשימת המכונות הווירטואליות"
+
+#~ msgid "Hide additional options"
+#~ msgstr "הסתרת אפשרויות נוספות"
+
+#~ msgid "Host device"
+#~ msgstr "התקן מארח"
+
+#~ msgid "Host name"
+#~ msgstr "שם מארח"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "המארח לא אמור להיות ריק"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "פרטי Hypervisor"
+
+#~ msgid "IP configuration"
+#~ msgstr "הגדרת IP"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 ו־IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "רשת IPv4"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "רשת IPv4 לא אמורה להישאר ריקה"
+
+#~ msgid "IPv4 only"
+#~ msgstr "IPv4 בלבד"
+
+#~ msgid "IPv6 address"
+#~ msgstr "כתובת IPv6"
+
+#~ msgid "IPv6 network"
+#~ msgstr "רשת IPv6"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "רשת IPv6 לא אמורה להישאר ריקה"
+
+#~ msgid "IPv6 only"
+#~ msgstr "IPv6 בלבד"
+
+#~ msgid "Idle"
+#~ msgstr "בהמתנה"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "הפעלת מכונה וירטואלית מיידית"
+
+#~ msgid "Import"
+#~ msgstr "ייבוא"
+
+#~ msgid "Import VM"
+#~ msgstr "ייבוא מכונה וירטואלית"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "ייבוא מכונה וירטואלית"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "ברוב אפשרויות ההגדרה, macvtap לא עובד לטובת תקשורת רשת בין מארח לאורח."
+
+#~ msgid "Initiator"
+#~ msgstr "מאתחל"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "ה־IQN של המפעיל לא אמור להיות ריק"
+
+#~ msgid "Installation source"
+#~ msgstr "מקור התקנה"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "חובה למלא את מקור ההתקנה"
+
+#~ msgid "Installation type"
+#~ msgstr "סוג התקנה"
+
+#~ msgid "Interface type"
+#~ msgstr "סוג מנשק"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "מסכה או אורך קידומת של IPv4 שגוי"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "כתובת IPv6 שגויה"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "קידומת IPv6 שגויה"
+
+#~ msgid "Invalid filename"
+#~ msgstr "שם הקובץ שגוי"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "הפעלת מציג מרחוק"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr "ניתן להשאיר את הססמה ריקה אם בחירתך היא שלא ליצור חשבון על (root)"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr "ניתן להשאיר את הססמה ריקה אם בחירתך היא שלא ליצור חשבון משתמש"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr "ניתן להשאיר את הססמה ריקה אם בחירתך היא שלא ליצור חשבון על (root)"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr "Libvirt לא זיהה תמונת קושחה מסוג UEFI/OVMF מותקנת על המארח"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt או ה־hypervisor לא תומכים ב־UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "משאבים נטענים"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "יש לכבות את המכונה לפני החלפת סוג אפיק"
+
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "יש לכבות את המכונה לפני החלפת מצב מטמון"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "ניהול מכונות וירטואליות"
+
+#~ msgid "Manual connection"
+#~ msgstr "חיבור ידני"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "מסכה או אורך קידומת"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "מסכה או אורך קידומת לא אמור להישאר ריק"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "הקצאה מרבית"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "לא ניתן לשמור את הזיכרון המרבי"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr "כמות המעבדים הווירטואליים המרבית שמוקצת למערכת ההפעלה האורחת"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "כמות המעבדים הווירטואליים המרבית שמוקצת למערכת ההפעלה האורחת, שחייבת "
+#~ "להיות בין 1 ל־$0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "יחידת העברה מרבית"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "לא ניתן לשמור את הזיכרון"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "אסור שהזיכרון יהיה 0"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "סוג דגם"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT אל $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "החלפת מצב מתאם הרשת $0 של המכונה הווירטואלית $0 נכשלה"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "השם מכיל תווים שגויים"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "השם לא יכול להישאר ריק"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "השם לא אמור להיות ריק"
+
+#~ msgid "Name: "
+#~ msgstr "שם: "
+
+#~ msgid "Netmask"
+#~ msgstr "מסכת רשת"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "הפעלת הרשת $0 נכשלה"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "השבתת הרשת $0 נכשלה"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "עלייה מהרשת (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "מערכת קבצים רשתית"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "לא ניתן לשמור את הגדרות מנשק הרשת"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "בחירת הרשת לא תומכת ב־PXE."
+
+#~ msgid "Networks"
+#~ msgstr "רשתות"
+
+#~ msgid "New volume name"
+#~ msgstr "שם חדש לכרך"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "אף מכונה וירטואלית לא מופעלת או מוגדרת על המארח הזה"
+
+#~ msgid "No connection available"
+#~ msgstr "אין חיבור זמין"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "לא מוגדרים כוננים למכונה הווירטואלית הזאת"
+
+#~ msgid "No network devices"
+#~ msgstr "אין התקני רשת"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "לא הוגדרו מנשקי רשת למכונה הווירטואלית הזאת"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "לא הוגדרה רשת למארח הזה"
+
+#~ msgid "No networks available"
+#~ msgstr "אין רשתות זמינות"
+
+#~ msgid "No parent"
+#~ msgstr "אין הורה"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "לא מוגדרות תמונות מצב למכונה הווירטואלית הזאת"
+
+#~ msgid "No state"
+#~ msgstr "אין מצב"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "לא הוגדר מאגר אחסון למארח הזה"
+
+#~ msgid "No storage pools available"
+#~ msgstr "אין מאגרי אחסון זמינים"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "לא הוגדרו כרכי אחסון למאגר האחסון הזה"
+
+#~ msgid "No virtual networks"
+#~ msgstr "אין רשתות וירטואליות"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "לא ניתן למחוק רשת שאינה עקבית. היא פשוט חדלה מלהתקיים לאחר סיום הפעלתה."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "לא ניתן למחוק מאגר אחסון שאינו עקבי. הוא פשוט חדל מלהתקיים לאחר סיום "
+#~ "הפעלתו."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "אין (רשת מבודדת)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "אחד או יותר מהכרכים שנבחרו משמשים את שם התחום. יש לנתק את הכוננים תחילה "
+#~ "כדי לאפשר מחיקת כרכים."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "ניתן לעריכה רק כאשר האורח כבוי"
+
+#~ msgid "Open"
+#~ msgstr "פתיחה"
+
+#~ msgid "Operating system"
+#~ msgstr "מערכת הפעלה"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "המשימה בתהליך"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "תמונת מצב הורה"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "הנתיב במערכת הקבצים של המארח"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "הנתיב לקובץ ISO על מערכת הקבצים של המארח"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "הנתיב לקובץ במערכת הקבצים של המארח"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "הנתיב לקובץ במערכת הקבצים של המארח"
+
+#~ msgid "Paused"
+#~ msgstr "מושהה"
+
+#~ msgid "Persistence"
+#~ msgstr "עקביות"
+
+#~ msgid "Physical disk device"
+#~ msgstr "התקן כונן פיזי"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "התקן כונן פיזי במארח"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "נא לבחור מאגר אחסון"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "נא לבחור כרך"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "נא להקליד את שם הכרך החדש"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "נא להפעיל את המכונה הווירטואלית כדי לגשת למסוף שלה."
+
+#~ msgid "Plug"
+#~ msgstr "חיבור"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "יש להפעיל את המאגר כדי ליצור כרך"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "סוג המאגר לא תומך ביצירת כרכים"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "כרכי המאגר משמשים מכונות וירטואליות "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "מספר השקעים המועדף לחשיפה לאורח."
+
+#~ msgid "Prefix"
+#~ msgstr "קידומת"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "אורך הקידומת לא אמור להיות ריק"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr "תמונות מצב שנלכדו בעבר מאפשרות לך לחזור למצב קודם אם משהו השתבש"
+
+#~ msgid "Product"
+#~ msgstr "מוצר"
+
+#~ msgid "Profile"
+#~ msgstr "פרופיל"
+
+#~ msgid "Protocol"
+#~ msgstr "פרוטוקול"
+
+#~ msgid "Remote URL"
+#~ msgstr "כתובת מרוחקת"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "פרטי המציג מרחוק"
+
+#~ msgid "Revert"
+#~ msgstr "החזרה אחורה"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "החזרה לתמונת מצב $0"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "החזרה לתמונת המצב הזו תחזיר את המכונה הווירטואלית אחורה לזמן תמונת המצב "
+#~ "והמצב הנוכחי יאבד לצד כל הנתונים שלא נלכדו קודם לכן בתמונת המצב"
+
+#~ msgid "Root password"
+#~ msgstr "ססמת משתמש על (root)"
+
+#~ msgid "Route to $0"
+#~ msgstr "ניתוב אל $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "הפעלת התקנה ללא מגע"
+
+#~ msgid "Run when host boots"
+#~ msgstr "הפעלה עם עליית המארח"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "פתחת TLS של SPICE"
+
+#~ msgid "SPICE address"
+#~ msgstr "כתובת SPICE"
+
+#~ msgid "SPICE port"
+#~ msgstr "פתחת SPICE"
+
+#~ msgid "Select console type"
+#~ msgstr "בחירת סוג מסוף"
+
+#~ msgid "Send key"
+#~ msgstr "שליחת מקש"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "שליחת פסיקה שלא ניתן למסך"
+
+#~ msgid "Serial console"
+#~ msgstr "מסוף טורי"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "הגדרת טווח DHCP"
+
+#~ msgid "Set manually"
+#~ msgstr "הגדרה ידנית"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "הגדרת ססמאות משתמש להתקנה ללא מגע דורש את הפעלת המכונה הווירטואלית בעת "
+#~ "יצירת"
+
+#~ msgid "Show additional options"
+#~ msgstr "הצגת אפשרויות נוספות"
+
+#~ msgid "Shut off"
+#~ msgstr "לכבות"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "יש לכבות את המכונה הווירטואלית כדי לערוך הגדרות קושחה"
+
+#~ msgid "Shutting down"
+#~ msgstr "מתבצע כיבוי"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "יצירת תמונת המצב נכשלה"
+
+#~ msgid "Source format"
+#~ msgstr "תצורת מקור"
+
+#~ msgid "Source path"
+#~ msgstr "נתיב מקור"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "נתיב המקור לא אמור להיות ריק"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "המקור אמור להיפתח בפרוטוקולי http,‏ ftp או nfs"
+
+#~ msgid "Source volume group"
+#~ msgstr "קבוצת כרכים מקורית"
+
+#~ msgid "Start libvirt"
+#~ msgstr "הפעלת libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "הפעלת המאגר עם עליית המארח"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "ההתחלה לא אמורה להיות ריקה"
+
+#~ msgid "Startup"
+#~ msgstr "בהתחלה"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "הפעלת מאגר האחסון $0 נכשלה"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "השבתת מאגר האחסון $0 נכשלה"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "יצירת מאגר האחסון נכשלה"
+
+#~ msgid "Storage pool name"
+#~ msgstr "שם מאגר אחסון"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "אסור שגודל האחסון יהיה 0"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr "גודל כרך האחסון לא יכול לחרוג מקיבולת מאגר האחסון ($0 $1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "לא ניתן למחוק את כרכי האחסון"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "מושהה (מכונה פיזית)"
+
+#~ msgid "Target path"
+#~ msgstr "נתיב יעד"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "נתיב היעד לא יכול להיות ריק"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr "המכונה הווירטואלית פועלת ויהיה עליך לכבות אותה לפני המחיקה."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr "על המכונה הווירטואלית להיות פעילה או כבויה כדי לנתק את ההתקן הזה"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "התיקייה על השרת שמיוצא"
+
+#~ msgid "The pool is empty"
+#~ msgstr "המאגר ריק"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr "מערכת ההפעלה הנבחרת לא תומכת בהתקנה ללא מגע"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr "למערכת ההפעלה הנוכחית יש דרישות סף מבחינת זיכרון של $0 $1"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr "למערכת ההפעלה הנוכחית יש דרישות סף מבחינת אחסון של $0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "לא ניתן למחוק את מאגר האחסון"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr "זו מכונה וירטואלית זמנית. ניתן לכבות אותה אם ברצונך למחוק אותה."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "כרך זה כבר בשימוש על ידי: "
+
+#~ msgid "Threads per core"
+#~ msgstr "תהליכונים למעבד"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr "מכונות וירטואליות זמניות לא תומכות בעריכת הגדרות קושחה"
+
+#~ msgid "Type ID"
+#~ msgstr "מזהה סוג"
+
+#~ msgid "Unique name"
+#~ msgstr "שם ייחודי"
+
+#~ msgid "Unique network name"
+#~ msgstr "שם רשת ייחודי"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "קושחה לא ידועה"
+
+#~ msgid "Unplug"
+#~ msgstr "הוצאה מהשקע"
+
+#~ msgid "Up to $0 $1 available on the default location"
+#~ msgstr "עד $0 $1 זמינים במיקום בררת המחדל"
+
+#~ msgid "Up to $0 $1 available on the host"
+#~ msgstr "עד $0 $1 זמינים במארח"
+
+#~ msgid "Url"
+#~ msgstr "כתובת"
+
+#~ msgid "User login must not be empty when user password is set"
+#~ msgstr "שם המשתמש לא יכול להיות ריק כשמוגדרת ססמה"
+
+#~ msgid "User password must not be empty when user login is set"
+#~ msgstr "ססמת המשתמש לא יכולה להיות ריקה כשמוגדר שם משתמש"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "לא ניתן לשמור את הגדרות המעבד הווירטואלי"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "המכונה הווירטואלית $0 כבר קיימת"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "המכונה הווירטואלית $0 לא קיימת בחיבור $1"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "הפעלת המכונה הווירטואלית $0 מחדש בכוח נכשלה"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "כיבוי המכונה הווירטואלית $0 בכוח נכשל"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "מחיקת המכונה הווירטואלית $0 נכשלה"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "התקנת המכונה הווירטואלית $0 נכשלה"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "הפעלת המכונה הווירטואלית $0 מחדש נכשלה"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "המשך המכונה הווירטואלית $0 נכשל"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "המכונה הווירטואלית $0 נכשלה בשליחת פסיקה ללא אפשרות מיסוך (NMI)"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "כיבוי המכונה הווירטואלית $0 נכשל"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "הפעלת המכונה הווירטואלית $0 נכשלה"
+
+#~ msgid "VM state"
+#~ msgstr "מצב מכונה וירטואלית"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "פתחת TLS של VNC"
+
+#~ msgid "VNC address"
+#~ msgstr "כתובת VNC"
+
+#~ msgid "VNC console"
+#~ msgstr "מסוף VNC"
+
+#~ msgid "VNC port"
+#~ msgstr "פתחת VNC"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "מכונות וירטואליות"
+
+#~ msgid "Virtual machines"
+#~ msgstr "מכונות וירטואליות"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "ניהול מכונות וירטואליות"
+
+#~ msgid "Virtual network"
+#~ msgstr "רשת וירטואלית"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "יצירת הרשת הווירטואלית נכשלה"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "שירות הווירטואליזציה (libvirt) אינו פעיל"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "שם קבוצת הכרכים לא אמור להיות ריק"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "ניתן לכתיבה"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "ניתן לכתיבה ומשותף"
+
+#~ msgid "Yesterday"
+#~ msgstr "אתמול"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "עליך לבחור את מערכת ההפעלה שהכי מתאימה"
+
+#~ msgid "cdrom"
+#~ msgstr "תקליטור"
+
+#~ msgid "disabled"
+#~ msgstr "מושבת"
+
+#~ msgid "down"
+#~ msgstr "מושבת"
+
+#~ msgid "enabled"
+#~ msgstr "זמין"
+
+#~ msgid "ethernet"
+#~ msgstr "אתרנט"
+
+#~ msgid "host device"
+#~ msgstr "התקן מארח"
+
+#~ msgid "host passthrough"
+#~ msgstr "מעקף מארח"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "יעד ישיר של iSCSI"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "IQN של מאתחל iSCSI"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "יעד IQN של iSCSI"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "לא"
+
+#~ msgid "no state saved"
+#~ msgstr "לא נשמר מצב"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "התקן מופנה"
+
+#~ msgid "server"
+#~ msgstr "שרת"
+
+#~ msgid "up"
+#~ msgstr "פעיל"
+
+#~ msgid "vCPU count"
+#~ msgstr "כמות מעבדים וירטואליים"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "מעבד וירטואלי מרבי"
+
+#~ msgid "vCPUs"
+#~ msgstr "מעבדים וירטואליים"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#~ msgid "view more..."
+#~ msgstr "להציג עוד…"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr "יש להתקין את החבילה virt-install במערכת כדי לשכפל מכונות וירטואליות"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "יש להתקין את החבילה virt-install במערכת כדי ליצור מכונות וירטואליות חדשות"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr "יש להתקין את החבילה virt-install במערכת כדי לערוך את המאפיין הזה"
+
+#~ msgid "vm"
+#~ msgstr "מכונה וירטואלית"
+
+#~ msgid "Local install media"
+#~ msgstr "אמצעי התקנה מקומי"
+
+#~ msgid "URL"
+#~ msgstr "כתובת"
+
+#~ msgid "Please set a root password"
+#~ msgstr "נא להגדיר ססמה למשתמש העל (root)"
+
+#~ msgid "21st"
+#~ msgstr "עשרים ואחד"
+
+#~ msgid "22nd"
+#~ msgstr "עשרים ושניים"
+
+#~ msgid "23rd"
+#~ msgstr "עשרים ושלושה"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "אחרי"
+
+#~ msgid "Friday"
+#~ msgstr "יום שישי"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "שעה : דקה"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "שעה צריכה להיות ערך בין 0 ל־23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "מבנה תאריך שגוי."
+
+#~ msgid "Invalid number."
+#~ msgstr "מספר שגוי."
+
+#~ msgid "Monday"
+#~ msgstr "יום שני"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "לחזור באופן שעתי"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "לחזור באופן שנתי"
+
+#~ msgid "Saturday"
+#~ msgstr "שבת"
+
+#~ msgid "Sunday"
+#~ msgstr "יום ראשון"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "יום זה לא קיים בכל החודשים.<br> המתזמן יופעל רק בחודשים שיש בהם 31 ימים."
+
+#~ msgid "Thursday"
+#~ msgstr "יום חמישי"
+
+#~ msgid "Tuesday"
+#~ msgstr "יום שלישי"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "עדכון אחד"
+#~ msgstr[1] "$0 עדכונים"
+#~ msgstr[2] "$0 עדכונים"
+#~ msgstr[3] "$0 עדכונים"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "תיקון אבטחה אחד"
+#~ msgstr[1] "$0 תיקוני אבטחה"
+#~ msgstr[2] "$0 תיקוני אבטחה"
+#~ msgstr[3] "$0 תיקוני אבטחה"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "ניהול התקני אחסון"
+
+#~ msgid "Restart now"
+#~ msgstr "להפעיל מחדש כעת"
+
+#~ msgid "Configure"
+#~ msgstr "הגדרה"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr "עלייה מהרשת זמינה רק בעת שימוש בחיבור מערכת"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "תקשורת"
+
+#~ msgid "No snapshots"
+#~ msgstr "אין תמונות מצב"
+
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "לא נמצא קובץ כזה בתיקייה ‚$0’"
+
+#~ msgid "No updates pending"
+#~ msgstr "אין עדכונים ממתינים"
+
+#~ msgid "This directory is empty"
+#~ msgstr "התיקייה הזאת ריקה"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr "סוג מאגר זה אינו תומך ביצירת כרכי אחסון"
+
+#~ msgid "undefined"
+#~ msgstr "לא מוגדר"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "מפתח המארח שגוי"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "לא ניתן לוודא את אמינות המארח {{#strong}}{{host}}{{/strong}}. להמשיך "
+#~ "ולהתחבר?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "המפתח של {{#strong}}{{host}}{{/strong}} אינו תואם למפתח בו נעשה שימוש "
+#~ "בעבר. אלמלא המכונה הזאת הוחלפה לאחרונה, כנראה שמישהו מנסה לתקוף את החיבור "
+#~ "שלך למכונה הזאת."
+
+#~ msgid "Unknown host key"
+#~ msgstr "מפתח מארח לא ידוע"
+
+#~ msgid "Address:"
+#~ msgstr "כתובת:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "נדרשים $0 כוננים לפחות."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "ניתן להתחבר עם כל יישום שמציג SPICE או VNC."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "הורדת ה־MSI מ־$0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "מסוף גרפי (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "מסוף גרפי במציג שולחנות העבודה"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "לא מוגדר מסוף למכונה הווירטואלית הזאת."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid ""
+#~ "For the host, either specify the hostname, IP address, an alias name or a "
+#~ "unique resource identifier for the SSH destination."
+#~ msgstr ""
+#~ "למארח, יש לציין שאם מארח, כתובת IP, כינוי או מזה משאב ייחודי ליעד ה־SSH."
+
+#~ msgid ""
+#~ "Specify the host and the login user account for the host that you want to "
+#~ "add."
+#~ msgstr "נא לציין את המארח ואת שם הכניסה של המארח המיועד להוספה."
+
+#~ msgid "Entry"
+#~ msgstr "רשומה"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "הדפדפן שלך ינותק, אך אין השפעה על תהליך העדכון. ניתן להתחבר מחדש בעוד "
+#~ "מספר רגעים כדי להמשיך לצפות בהתקדמות."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "ולהפעיל את המכונה מחדש אוטומטית."
+
+#~ msgid "Dashboard"
+#~ msgstr "לוח מחוונים"
+
+#~ msgid "Description input text"
+#~ msgstr "קלט טקסט לתיאור"
+
+#~ msgid "FAILED"
+#~ msgstr "נכשל"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "החיבור אבד. מתבצע ניסיון חיבור מחדש"
+
+#~ msgid "Name input text"
+#~ msgstr "טקסט קלט שם"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "רשת"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "נא להקליד את גודל הכרך החדש"
+
+#~ msgid "Servers"
+#~ msgstr "שרתים"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr "כרגע החיבור שלך לשרת הזה הוא ישיר. אין לך אפשרות למחוק אותו."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "סיבית אחת"
+#~ msgstr[1] "$0 סיביות"
+#~ msgstr[2] "$0 סיביות"
+#~ msgstr[3] "$0 סיביות"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "בית אחד"
+#~ msgstr[1] "$0 בתים"
+#~ msgstr[2] "$0 בתים"
+#~ msgstr[3] "$0 בתים"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "בית אחד"
+#~ msgstr[1] "$0 בתים"
+#~ msgstr[2] "$0 בתים"
+#~ msgstr[3] "$0 בתים"
+
+#~ msgid "Bond settings "
+#~ msgstr "הגדרות מאגד "
+
+#~ msgid "IP settings "
+#~ msgstr "הגדרות IP "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "התרחשה שגיאה פנימית בלתי צפויה ב־Cockpit. <br/><br/>ניתן לנסות להפעיל את "
+#~ "Cockpit מחדש על ידי לחיצה על רענון בדפדפן שלך. מסוף ה־javascript מכיל "
+#~ "פרטים על השגיאה הזאת (<b>Ctrl-Shift-J</b> ברוב הדפדפנים)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "המכונה הווירטואלית קרסה."
+
+#~ msgid "The VM is down."
+#~ msgstr "המכונה הווירטואלית כבויה."
+
+#~ msgid "The VM is going down."
+#~ msgstr "המכונה הווירטואליות בהליכי כיבוי."
+
+#~ msgid "The VM is idle."
+#~ msgstr "המכונה הווירטואלית ממתינה."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr "המכונה הווירטואלית בתהליכי גסיסה (כיבוי או קריסה שלא הושלמה)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "המכונה הווירטואלית הופסקה."
+
+#~ msgid "The VM is running."
+#~ msgstr "המכונה הווירטואלית פועלת."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "המכונה הווירטואלית מושהית על ידי ניהול צריכת החשמל של האורח."
+
+#~ msgid "inactive"
+#~ msgstr "בלתי פעיל"
+
+#~ msgid "Avatar"
+#~ msgstr "תמונת משתמש"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "יש להשאיר ריק כדי להתחבר למכונה הזאת בתור המשתמש שמחובר כרגע. אם יוקלד שם "
+#~ "משתמש אחר, המשתמש הזה תמיד ישמש להתחברות למכונה זו."
+
+#~ msgid "2 min"
+#~ msgstr "2 דקות"
+
+#~ msgid "3 min"
+#~ msgstr "3 דקות"
+
+#~ msgid "4 min"
+#~ msgstr "4 דקות"
+
+#~ msgid "CPU graph"
+#~ msgstr "תרשים מעבד"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "מצב מעבד"
+
+#~ msgid "Cached"
+#~ msgstr "במכלא"
+
+#~ msgid "Graphs"
+#~ msgstr "תרשימים"
+
+#~ msgid "I/O wait"
+#~ msgstr "המתנת קלט/פלט"
+
+#~ msgid "Kernel"
+#~ msgstr "ליבה"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "זיכרון"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "שימוש בזיכרון ובהחלפה"
+
+#~ msgid "Memory graph"
+#~ msgstr "תרשים זיכרון"
+
+#~ msgid "Network traffic"
+#~ msgstr "תעבורת רשת"
+
+#~ msgid "Nice"
+#~ msgstr "Nice (זמן מעבד)"
+
+#~ msgid "Synchronize users"
+#~ msgstr "סנכרון משתמשים"
+
+#~ msgid "Usage graphs"
+#~ msgstr "תרשימי שימוש"
+
+#~ msgid "View graphs"
+#~ msgstr "הצגת תרשימים"
+
+#~ msgid "idle"
+#~ msgstr "בהמתנה"
+
+#~ msgid "paused"
+#~ msgstr "מושהה"
+
+#~ msgid "running"
+#~ msgstr "פועל"
+
+#~ msgid "shut off"
+#~ msgstr "לכבות"
+
+#~ msgid "shutdown"
+#~ msgstr "לכבות"
+
+#~ msgid "Add a new host"
+#~ msgstr "הוספת מארח חדש"
+
+#~ msgid "Available"
+#~ msgstr "זמין"
+
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "נא למלא כתובת IP או שם מארח"
+
+#~ msgid "Network interfaces"
+#~ msgstr "מנשקי רשת"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "גרסה זו של cockpit-ws אינה תומכת בחיבור למארח עם משתמש או פתחה חלופיים"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr "כדי לנסות פתחה אחרת יהיה עליך לשדרג את cockpit-ws לגרסה חדשה יותר."
+
+#~ msgid "$0 template"
+#~ msgstr "תבנית $0"
+
+#~ msgid "Instance of template: "
+#~ msgstr "מופע של תבנית: "
+
+#~ msgid "Instantiate"
+#~ msgstr "יצירת עצם מוגדר"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 שיתופים"
+
+#~ msgid "All In One"
+#~ msgstr "הכול באחד"
+
+#~ msgid "Always"
+#~ msgstr "תמיד"
+
+#~ msgid "Author"
+#~ msgstr "יוצר"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "הועברו נתונים פגומים למנגנון passwd1"
+
+#~ msgid "CPU priority"
+#~ msgstr "עדיפות מעבד"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "אי אפשר להתחבר ל־Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "החלפת מגבלות משאב"
+
+#~ msgid "Change resources limits"
+#~ msgstr "החלפת מגבלות משאבים"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "ל־Cockpit לא הייתה אפשרות להיכנס אל {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "ל־Cockpit לא הייתה אפשרות להיכנס אל {{#strong}}{{host}}{{/strong}}. ניתן "
+#~ "לשנות את פרטי האימות שלך להלן. {{#can_sync}}אולי עדיף לך {{#sync_link}}"
+#~ "לסנכרן חשבונות וססמאות{{/sync_link}}.{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "ניצולת זיכרון משותפת"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "ניצולת משותפת של ליבת מעבד אחת"
+#~ msgstr[1] "ניצולת משותפת של $0 ליבות מעבד"
+#~ msgstr[2] "ניצולת משותפת של $0 ליבות מעבד"
+#~ msgstr[3] "ניצולת משותפת של $0 ליבות מעבד"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "הפקודה לא יכולה להיות ריקה"
+
+#~ msgid "Command:"
+#~ msgstr "פקודה:"
+
+#~ msgid "Commit"
+#~ msgstr "הגשה"
+
+#~ msgid "Commit image"
+#~ msgstr "הגשת תמונה"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "מתבצעת התחברות ל־Docker"
+
+#~ msgid "Container name"
+#~ msgstr "שם מכולה"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr "המכולה מסומנת שאינה פועלת אבל בקשת עצירה רגילה נכשלה."
+
+#~ msgid "Container is currently running."
+#~ msgstr "המכולה פועלת כרגע."
+
+#~ msgid "Container:"
+#~ msgstr "מכולה:"
+
+#~ msgid "Containers"
+#~ msgstr "מכולות"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "מכולות"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "לא ניתן להחליף את קבוצות המשתמש"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "לא ניתן להחליף את ססמת המשתמש"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "לא ניתן להתחבר למכונה"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "לא ניתן ליצור משתמשים חדשים"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "לא ניתן להציג משתמשים מקומיים"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "לא ניתן להציג משתמשים"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "לא ניתן לטעון נתוני משתמש"
+
+#~ msgid "Created:"
+#~ msgstr "נוצרה:"
+
+#~ msgid "Docker containers"
+#~ msgstr "מכליות Docker"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker לא מותקן או מופעל על המערכת"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "כינוי כפול"
+
+#~ msgid "Duplicate port"
+#~ msgstr "פתחה כפולה"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "נא להקליד מילת צופן כדי להוסיף זהות ל־ssh-agent"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "מילוי ססמה שונה כאן גורר שיהיה עליך להקליד אותה מחדש עם כל חיבור מחדש "
+#~ "למכונה הזאת"
+
+#~ msgid "Environment"
+#~ msgstr "סביבה"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "שגיאה בטעינת משתמשים: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "הודעת שגיאה ב־Docker:"
+
+#~ msgid "Everything"
+#~ msgstr "הכול"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "יציאה עם $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "חשיפת פתחות של מכולות"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "הפעלת Docker נכשלה: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "עצירת היקף ה־Docker נכשלה: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "קבלת תמונה חדשה"
+
+#~ msgid "IP address:"
+#~ msgstr "כתובת IP:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "אורך קידומת IP:"
+
+#~ msgid "Id"
+#~ msgstr "מזהה"
+
+#~ msgid "Id:"
+#~ msgstr "מזהה:"
+
+#~ msgid "Image"
+#~ msgstr "תמונה"
+
+#~ msgid "Image $0"
+#~ msgstr "תמונה $0"
+
+#~ msgid "Image search"
+#~ msgstr "חיפוש תמונות"
+
+#~ msgid "Image:"
+#~ msgstr "תמונה:"
+
+#~ msgid "Images"
+#~ msgstr "תמונות"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "תמונות"
+
+#~ msgid "Images and running containers"
+#~ msgstr "תמונות ומכולות שרצות"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr "כי לסנכרן משתמשים, עליך להיכנס אל {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "פתחה שגויה"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "כרטיס Kerberos"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "יש להשאיר ריק כדי להתחבר למכונה הזאת בתור המשתמש שמחובר כרגע "
+#~ "{{#default_user}} ({{default_user}}){{/default_user}}. אם יוקלד שם משתמש "
+#~ "אחר, המשתמש הזה תמיד ישמש להתחברות למכונה זו."
+
+#~ msgid "Link to another container"
+#~ msgstr "קישור למכולה אחרת"
+
+#~ msgid "Links:"
+#~ msgstr "קישורים:"
+
+#~ msgid "Login Password"
+#~ msgstr "ססמת כניסה"
+
+#~ msgid "MAC address:"
+#~ msgstr "כתובת MAC:"
+
+#~ msgid "Memory limit"
+#~ msgstr "מגבלת זיכרון"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "עיגון כרכים של מכולות"
+
+#~ msgid "No container specified"
+#~ msgstr "לא מוגדרת מכולה"
+
+#~ msgid "No containers"
+#~ msgstr "אין מכולות"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "אין מכולות שתואמות למסנן הנוכחי"
+
+#~ msgid "No images"
+#~ msgstr "אין תמונות"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "אין תמונות שתואמות למסנן הנוכחי"
+
+#~ msgid "No results for $0"
+#~ msgstr "אין תוצאות לחיפוש $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "אין תמונות שתואמות למסנן הנוכחי"
+
+#~ msgid "No running containers"
+#~ msgstr "אין מכולות פעילות"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "אין מכולות פעילות שתואמות למסנן הנוכחי"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "לא מורשה לגשת ל־Docker במערכת הזאת"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "בעת כשל, לנסות עוד פעם"
+#~ msgstr[1] "בעת כשל, לנסות עוד פעמיים"
+#~ msgstr[2] "בעת כשל, לנסות עוד $0 פעמים"
+#~ msgstr[3] "בעת כשל, לנסות עוד $0 פעמים"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "נא לאשר מחיקה בכוח של $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "נא לנסות ביטוי אחר"
+
+#~ msgid "Ports:"
+#~ msgstr "פתחות:"
+
+#~ msgid "Problems"
+#~ msgstr "בעיות"
+
+#~ msgid "ReadOnly"
+#~ msgstr "קריאה בלבד"
+
+#~ msgid "ReadWrite"
+#~ msgstr "קריאה כתיבה"
+
+#~ msgid "Repository"
+#~ msgstr "מאגר תמונות"
+
+#~ msgid "Restart policy:"
+#~ msgstr "מדיניות הפעלה מחדש:"
+
+#~ msgid "Retries:"
+#~ msgstr "ניסיונות חוזרים:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "להשתמש בססמה שלי שוב לחיבורים מרוחקים"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "נא לבחור את המשתמשים שברצונך לסנכרן מול {{#strong}}{{host}}{{/strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "הגדרת משתני סביבה למכולה"
+
+#~ msgid "Severity:"
+#~ msgstr "דרגת חומרה:"
+
+#~ msgid "Show all containers"
+#~ msgstr "להציג את כל המכולות"
+
+#~ msgid "Start Docker"
+#~ msgstr "הפעלת Docker"
+
+#~ msgid "State:"
+#~ msgstr "מצב:"
+
+#~ msgid "Synchronize"
+#~ msgstr "סנכרון"
+
+#~ msgid "Tag"
+#~ msgstr "תגית"
+
+#~ msgid "Tags"
+#~ msgstr "תגיות"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr "המכולות הבאות תלויות בתמונה הזאת ותהפוכנה בלתי שמישות."
+
+#~ msgid "This image does not exist."
+#~ msgstr "תמונה זו אינה קיימת."
+
+#~ msgid "Type a password"
+#~ msgstr "נא להקליד ססמה"
+
+#~ msgid "Type to filter…"
+#~ msgstr "יש להקליד כדי לסנן…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "אלמלא נעצר"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "מנגנון ההתקנה לא נתמך"
+
+#~ msgid "Up since $0"
+#~ msgstr "פעיל מאז $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "בשימוש על ידי מכולות"
+
+#~ msgid "Using available credentials"
+#~ msgstr "נעשה שימוש בפרטי גישה זמינים"
+
+#~ msgid "Volumes"
+#~ msgstr "כרכים"
+
+#~ msgid "Volumes:"
+#~ msgstr "כרכים:"
+
+#~ msgid "With terminal"
+#~ msgstr "עם המסוף"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "נכנסת אל {{#strong}}{{host}}{{/strong}}, עם זאת, כדי לסנכרן משתמשים, נדרש "
+#~ "משתמש עם הרשאות משתמש על."
+
+#~ msgid "default"
+#~ msgstr "בררת מחדל"
+
+#~ msgid "key"
+#~ msgstr "מפתח"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "חיפוש לפי שם, מרחב שם או תיאור"
+
+#~ msgid "shares"
+#~ msgstr "שיתופים"
+
+#~ msgid "to host port"
+#~ msgstr "לפתחת המארח"
+
+#~ msgid "value"
+#~ msgstr "ערך"
+
+#~ msgid "Master"
+#~ msgstr "עיקרי"
+
+#~ msgid "Password for $0"
+#~ msgstr "ססמה עבור $0"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "המשתמש <b>$0</b> אינו מורשה לנהל שרתים"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "חשבונות"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "המשתמש <b>$0</b> אינו מורשה לערוך חשבונות"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "לא ניתן למחוק את חשבון העל (root)"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "לא ניתן לשנות שם חשבון על (root)"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "לא מורשה להוסיף אזור חדש"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "לא מורשה להוסיף שירותים לאזור $0"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "לא מורשה להסיר את השירות $0"
+
+#~ msgid "Account Settings"
+#~ msgstr "הגדרות חשבון"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "הוספת מכונה ללוח מחוונים"
+
+#~ msgid "Machines"
+#~ msgstr "מכונות"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "הכניסה הועלתה להרשאות ניהול"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "הססמה לא שימושית למשימות שדורשות הרשאות ניהוליות או כדי להתחבר למכונות "
+#~ "אחרות"
+
+#~ msgid "Privileged"
+#~ msgstr "מורשה"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr "להשתמש בססמה שלי שוב למשימות שדורשות הרשאה ולהתחבר למכונות אחרות"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr "המשתמש $0 אינו מורשה לערוך שמות מארחים"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr "המשתמש $0 אינו מורשה לכבות או להפעיל את השרת מחדש"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "המשתמש <b>$0</b> אינו מורשה להחליף פרופילים"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "המשתמש <b>$0</b> אינו מורשה לנהל אחסון"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr "המשתמש <b>$0</b> אינו מורשה לשנות הגדרות תקשורת"
diff --git a/po/it.po b/po/it.po
new file mode 100644
index 0000000..38725d1
--- /dev/null
+++ b/po/it.po
@@ -0,0 +1,13733 @@
+# Edoardo Spadoni <edoardo.spadoni@nethesis.it>, 2018. #zanata
+# Elena Metelli <byruit@gmail.com>, 2018. #zanata
+# Giacomo Sanchietti <giacomo.sanchietti@gmail.com>, 2018. #zanata
+# Giovanni Grieco <giovanni.grc96@gmail.com>, 2018. #zanata
+# Ludek Janda <ljanda@redhat.com>, 2018. #zanata
+# cockpit <cockpituous@gmail.com>, 2018. #zanata
+# Ludek Janda <ljanda@redhat.com>, 2019. #zanata
+# Massimiliano Torromeo <massimiliano.torromeo@gmail.com>, 2020.
+# Roberto Bellingeri <bellingeri@netguru.it>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-07-17 09:21+0000\n"
+"Last-Translator: Nathan <nathan95@live.it>\n"
+"Language-Team: Italian <https://translate.fedoraproject.org/projects/cockpit/"
+"main/it/>\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 4.18.2\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "di utenti"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+msgstr[1] "$0 CPU"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 Zona Attiva"
+msgstr[1] "$0 Zone Attive"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 disponibile"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "$0 non possono essere ingranditi."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "$0 non possono essere ridotti."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "I $0 filesystem non possono essere ridimensionati qui."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "I $0 filesystem non possono essere ridimensionati qui."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 Dimensione del blocco"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 risultato critico"
+msgstr[1] "$0 risultati, inclusi critici"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 dati + $1 overhead utilizzato di $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 giorno"
+msgstr[1] "$0 giorni"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disco inesistente"
+msgstr[1] "$0 dischi inesistenti"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disco inesistente"
+msgstr[1] "$0 dischi inesistenti"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 dischi"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "Documentazione $0"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "Errore $0"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 terminato con codice $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 fallito"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 tentativo di accesso fallito"
+msgstr[1] "$0 tentativi di accesso falliti"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "$0 filesystem"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 liberi"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 ora"
+msgstr[1] "$0 ore"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 risultato importante"
+msgstr[1] "$0 risultati, inclusi importanti"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 è un file non esistente"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 è in uso"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 non è disponibile in nessun archivio web."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "Chiave $0 modificata"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 terminato con codice $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 risultato di bassa severità"
+msgstr[1] "$0 risultati, inclusi bassa gravità"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minuto"
+msgstr[1] "$0 minuti"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 risultato moderato"
+msgstr[1] "$0 risultati, inclusi moderati"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 mese"
+msgstr[1] "$0 mesi"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 di più..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 pacchetto"
+msgstr[1] "$0 pacchetti"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 pacchetto richiede un riavvio di sistema"
+msgstr[1] "$0 pacchetti richiedono un riavvio di sistema"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 pagina"
+msgstr[1] "$0 pagine"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "partizione"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "Aggiornamento di sicurezza disponibile"
+msgstr[1] "Aggiornamenti di sicurezza disponibili"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 servizio ha fallito"
+msgstr[1] "$0 servizi hanno fallito"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 servizio deve essere riavviato"
+msgstr[1] "$0 servizi devono essere riavviati"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 slot disponibile"
+msgstr[1] "$0 slot disponibili"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 Picco"
+msgstr[1] "$0 Picco"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+#, fuzzy
+#| msgid "Not synchronized"
+msgid "$0 synchronized"
+msgstr "Non sincronizzato"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 totale"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 aggiornamento disponibile"
+msgstr[1] "$0 aggiornamenti disponibili"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 usato di $1 ($2 salvato)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 settimana"
+msgstr[1] "$0 settimane"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 sarà installato."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 anno"
+msgstr[1] "$0 anni"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "Zona $0"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: crash a $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (da $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Creazione della partizione $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Punto di montaggio locale"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Non montato"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(raccomandato)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", compresa $1 correzione di sicurezza"
+msgstr[1] ", comprese $1 correzioni di sicurezza"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 giorno"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 ora"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 min"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 minuto"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 settimana"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10°"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11°"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12°"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13°"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14°"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 min"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15°"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16°"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17°"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18°"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19°"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1°"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 minuti"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20°"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21esimo"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22esimo"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23esimo"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24°"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25°"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26°"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27°"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28°"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29°"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2°"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30°"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31°"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3°"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 minuti"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4°"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 min"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minuti"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5°"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 ore"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 minuti"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6°"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7°"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8°"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9°"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Una versione compatibile di Cockpit non è installata su $0."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "Un filesystem con questo nome esiste già in questo pool."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "Esiste già un gruppo con questo nome."
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Un browser moderno è necessario per la sicurezza, l'affidabilità e le "
+"prestazioni."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Un bond di rete combina più interfacce di rete in un'unica interfaccia "
+"portata del canale o ridondanza più elevati."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Una nuova chiave SSH in $0 verrà creata per $1 su $2 e sarà aggiunta al file "
+"$3 di $4 su $5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Un pacchetto richiede un riavvio di sistema perché gli aggiornamenti "
+"prendano effetto:"
+msgstr[1] ""
+"Alcuni pacchietti richiedono un riavvio di sistema perché gli aggiornamenti "
+"prendano effetto:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Esiste già un pool con questo nome."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"Un servizio deve essere riavviato perché gli aggiornamenti prendano effetto:"
+msgstr[1] ""
+"Alcuni servizi devono essere riavviati perché gli aggiornamenti prendano "
+"effetto:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "I volumi fisici non possono essere ridimensionati qui."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "Monitoraggio ARP"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Informazioni su Web Console"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Assente"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Accetta la chiave ed effettua l'accesso"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+#, fuzzy
+#| msgid "Set password"
+msgid "Acceptable password"
+msgstr "Imposta password"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Scadenza account"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Account non disponibile o non modificabile."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Account"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Azione"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Azioni"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Attiva"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Attivazione $target"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Attivo"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Backup attivo"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Pagine attive"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Attivo dal "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Stato attivo"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Bilanciamento del carico adattivo"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Bilanciamento del carico di trasmissione adattivo"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Aggiungi"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Aggiungi $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add DNS server"
+msgstr "Keyserver Tang"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Aggiungi la crittografia dei dischi legati alla rete"
+
+#: pkg/storaged/stratis/pool.jsx:458
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add Tang keyserver"
+msgstr "Keyserver Tang"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Aggiungi VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+#, fuzzy
+#| msgid "Add VLAN"
+msgid "Add VPN"
+msgstr "Aggiungi VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+#, fuzzy
+#| msgid "Add item"
+msgid "Add a bitmap"
+msgstr "Aggiungi elemento"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Aggiungi una nuova zona"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+#, fuzzy
+#| msgid "MAC address"
+msgid "Add address"
+msgstr "Indirizzo MAC"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Aggiungi dispositivi a blocchi"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Aggiungi bond"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Aggiungi bridge"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Aggiungi disco"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Aggiungi dischi"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Aggiungi portale iSCSI"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Aggiungi chiave"
+
+#: pkg/storaged/stratis/pool.jsx:551
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add keyserver"
+msgstr "Keyserver Tang"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Aggiungi membro"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Aggiungi un nuovo host"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Aggiungi nuova zona"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Add passphrase"
+msgstr "Vecchia frase di accesso"
+
+#: pkg/networkmanager/wireguard.jsx:290
+#, fuzzy
+#| msgid "Add member"
+msgid "Add peer"
+msgstr "Aggiungi membro"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "Physical volume"
+msgid "Add physical volume"
+msgstr "Volume fisico"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Aggiungi Porte"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Aggiungi porte alla zona $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Aggiungi chiave pubblica"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+#, fuzzy
+#| msgid "Add item"
+msgid "Add route"
+msgstr "Aggiungi elemento"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+#, fuzzy
+#| msgid "DNS search domains"
+msgid "Add search domain"
+msgstr "Domini di ricerca DNS"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Aggiungi Servizi"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Aggiungi servizi alla zona $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Aggiungi servizi alla zona $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Aggiungi team"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Aggiungi zona"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "Aggiungi \"$0\" alle Opzioni di cifratura"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Aggiunta di \"$0\" alle opzioni del filesystem"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"L'aggiunta di $0 interromperà la connessione al server e renderà non "
+"disponibile l'interfaccia utente di amministrazione."
+
+#: pkg/storaged/stratis/pool.jsx:468
+#, fuzzy
+#| msgid ""
+#| "Saving a new passphrase requires unlocking the disk. Please provide a "
+#| "current disk passphrase."
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Per salvare una nuova frase di accesso è necessario sbloccare il disco. "
+"Fornire una frase di accesso attuale del disco."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Aggiungere porte personalizzate ricaricherà firewalld. Un ricaricamento "
+"provocherà la perdita di qualsiasi configurazione non persistente!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Aggiungo la chiave"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Aggiungo il volume fisico a $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Aggiunta di rd.neednet=1 alla riga di comando del kernel"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "DNS aggiuntivo $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Domini di ricerca DNS aggiuntivi $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Azioni aggiuntive"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Indirizzo aggiuntivo $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Pacchetti aggiuntivi:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Porte aggiuntive"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Indirizzo"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Indirizzo $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "L'indirizzo non può essere vuoto"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "L'indirizzo non è un URL valido"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Indirizzi"
+
+#: pkg/networkmanager/wireguard.jsx:52
+#, fuzzy
+#| msgid "Address cannot be empty"
+msgid "Addresses are not formatted correctly"
+msgstr "L'indirizzo non può essere vuoto"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Amministrazione con Cockpit Web Console"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Accesso amministrativo"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr "È necessario l'accesso amministrativo per creare e accedere ai report."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Richiesto accesso amministrativo"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "TCA avanzato"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Dopo"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Dopo aver lasciato il dominio, solo gli utenti con credenziali locali "
+"potranno accedere a questa macchina. Ciò può influire anche su altri servizi "
+"poiché le impostazioni della risoluzione DNS e l'elenco delle CA affidabili "
+"potrebbero cambiare."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Dopo l'avvio del sistema"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Avviso"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Allarme e oltre"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Alias"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Tutti"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Tutti gli aggiornamenti"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Tutto in una volta"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Consenti esecuzione (unmask)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+#, fuzzy
+#| msgid "Allowed addresses"
+msgid "Allowed IPs"
+msgstr "Indirizzi consentiti"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Indirizzi consentiti"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Documentazione sui ruoli Ansible"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Qualsiasi stringa di testo nei messaggi di registro può essere filtrata. La "
+"stringa può anche essere sotto forma di un'espressione regolare. Supporta "
+"anche il filtraggio in base ai campi del registro dei messaggi. Questi sono "
+"valori separati da spazi, nella forma CAMPO=VALORE, dove il valore può "
+"essere un elenco di valori possibili separati da virgole."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Aspetto"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr ""
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Applicazioni"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Lista applicazioni"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Applica e riavvia"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Applicare le patch live del kernel"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Applicare questa soluzione"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr ""
+"Applicazione di una nuova politica... L'operazione potrebbe richiedere "
+"alcuni minuti."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Applicazione della soluzione..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Sto installando gli aggiornamenti"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Impossibile installare gli aggiornamenti"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "È appropriato per i montaggi critici, come ad esempio /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Applicazioni"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Valutazione"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Tag asset"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "all'avvio"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "È necessario almeno $0 disco."
+msgstr[1] "Sono necessari almeno $0 dischi."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "È necessario almeno un dispositivo a blocchi."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "È necessario almeno un disco."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "Al minuto"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "Al secondo"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "In un momento specifico"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Attributi"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Log audit"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Autenticare"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Autenticazione"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Autenticazione"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Autenticazione fallita"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Autenticazione fallita: il server ha terminato la connessione"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"E' necessario autenticarsi per eseguire azioni privilegiate con Cockpit Web "
+"Console"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Autenticazione necessaria"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Autorizza chiave SSH"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Chiavi SSH pubbliche autorizzate"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automatico"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automatico (solo DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Accesso automatico"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Aggiornamenti automatici"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Si avvia automaticamente"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Automaticamente utilizzando NTP"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Automaticamente utilizzando server NTP addizionali"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Automaticamente utilizzando server NTP specifici"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Script di automazione"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Target disponibili su $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Aggiornamenti disponibili"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "partition"
+msgid "BIOS boot partition"
+msgstr "partizione"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "Data del BIOS"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "Versione del BIOS"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Torna agli account"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Male"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Impostazione errata"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Equilibratore"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Prima"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Si lega a"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Nero"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blade"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Chassis del blade"
+
+#: pkg/storaged/block/other.jsx:41
+#, fuzzy
+#| msgid "Block devices"
+msgid "Block device"
+msgstr "Dispositivi a blocchi"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Dispositivo di blocco per i filesystem"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Dispositivi a blocchi"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Bloccato"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Bond"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Boot"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"L'avvio fallisce se il filesystem non viene montato, impedendo l'accesso "
+"remoto"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "L'avvio riesce comunque quando il filesystem non viene montato"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Legato da"
+
+# translation auto-copied from project Anaconda, version master, document
+# anaconda, author Gregorio
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Ponte"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Porta del bridge"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Impostazioni della porta del bridge"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Broadcast"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Configurazione errata"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Aggiornamenti per bug fix disponibili"
+
+# ctx::sourcefile::/rhn/errata/manage/Create.do
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Bug"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Bus di espansione chassis"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Cambiando la password della chiave SSH $0 con la password di accesso di $1 "
+"su $2, la chiave sarà automaticamente resa disponibile e potrai in futuro "
+"accedere a $3 senza password."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " Bypassa il controllo del browser "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Sicurezza CPU"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "Impostazioni di Sicurezza della CPU"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Utilizzo CPU"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "Utilizzo CPU/load"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Cache"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "Può essere un hostname, un indirizzo IP, un alias, o un URI ssh://"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr ""
+"Non è possibile trovare alcun log utilizzando l'attuale combinazione di "
+"filtri."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Annulla"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Annulla spegnimento"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Annulla il riavvio"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Non può essere abilitato"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Impossibile connettersi a un host sconosciuto"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Impossibile inoltrare le credenziali di accesso"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+"Impossibile accedere a un dominio perché realmd non è disponibile su questo "
+"sistema"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Non è possibile programmare eventi del passato"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Capacità"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Carrier"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Cambia"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "Cambia la politica di crittografia"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Cambia nome host"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Cambia il nome dell'iniziatore iSCSI"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Cambia il nome dell'iniziatore iSCSI"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change label"
+msgstr "Cambiare la frase di accesso"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Cambiare la frase di accesso"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Cambia password"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Modifica del profilo performance"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Cambia profilo"
+
+#: pkg/users/shell-dialog.js:70
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change shell"
+msgstr "Cambiare la frase di accesso"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Modifica dell'ora di sistema"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Cambia la password di $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Modifica le impostazioni"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"La modifica delle chiavi sono di solito il risultato di una reinstallazione "
+"del sistema. Tuttavia, una modifica inaspettata può indicare un tentativo di "
+"intercettazione da parte di un soggetto terzo."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"La modifica delle impostazioni interromperà la connessione al server e "
+"renderà l'interfaccia utente di amministrazione non disponibile."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Controlla gli aggiornamenti"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"Controlla che l'hash SHA-256 o SHA-1 del comando corrisponda a questa "
+"finestra di dialogo."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "Controlla l'hash della chiave con il server Tang."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Controllo $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Controllo dell'IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "Controllo del dispositivo RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Controllo e riparazione del dispositivo RAID $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Verifica per $0 pacchetto"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Verifica del supporto NBDE in initrd"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Verifica di nuove applicazioni"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Verifica aggiornamento pacchetti..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Verifica del software installato"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Verifica dei processi correlati"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Controllo stato software"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Scegliere la lingua da utilizzare nell'applicazione"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Dimensione chunk"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Classe"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Pulizia per $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Cancella 'Impossibile avviare'"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Cancella tutti i filtri"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Cancella Ricerca"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Dispositivo con testo in chiaro"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Software Client"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Chiudi"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Chiudere le pagine selezionate"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "L'autenticazione di Cockpit non è configurata correttamente."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "Configurazione Cockpit del NetworkManager e del Firewalld"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit non ha potuto contattare l'host inserito."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit ha avuto un errore interno inaspettato"
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit è un gestore di server che rende facile amministrare i server Linux "
+"tramite un browser web. Cambiare tra il terminale e lo strumento web non è "
+"un problema. Un servizio avviato tramite Cockpit può essere interrotto "
+"tramite il terminale. Allo stesso modo, se si verifica un errore nel "
+"terminale, può essere visto nella sezione del registro di Cockpit."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit è un'interfaccia amministrativa interattiva per server Linux."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit non è compatibile con il software del sistema."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit non è installato"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit non è installato sul sistema."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit è perfetto per i nuovi amministratori di sistema, consentendo loro "
+"di eseguire facilmente semplici operazioni come la gestione "
+"dell'archiviazione, l'ispezione del registro e l'avvio e l'arresto dei "
+"servizi. È possibile monitorare e amministrare più server allo stesso tempo. "
+"Basta aggiungerli con un solo clic e se ne prenderà subito cura."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit potrebbe non essere visualizzato correttamente nel tuo browser"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Raccogliere e creare un pacchetto di dati diagnostici e di supporto"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Acquisisci i dump dei crash del kernel"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Raccogli le metriche"
+
+# titolo di scheda in dialogo di stampa
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Colore"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "Sono accettate porte, intervalli e servizi separati da virgole"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Comando"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Comando non trovato"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Commento"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Comunicazione con tuned non riuscita"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "PCI compatto"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Compatibile con tutti i sistemi e dispositivi (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Compatibile con sistemi moderni e dischi rigidi > 2TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Comprimi i dump di crash per risparmiare spazio"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Compressione"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Condizione $0=$1 non soddisfatta"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Condizione non riuscita"
+
+# translation auto-copied from project libreport, version master, document
+# libreport
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Configurazione"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Configurazione"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Configurazione dell'IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Configurazione di kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Configurazione delle impostazioni di sistema"
+
+# translation auto-copied from project Katello, version 0.1v, document app,
+# author fvalen
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Conferma"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Conferma la cancellazione di $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Conferma la password chiave"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Conferma la nuova password chiave"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Conferma nuova password"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Conferma la password"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Conferma la rimozione di $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Conferma la rimozione con una passphrase alternativa"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Confermare l'arresto di $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "Conflitto da"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Conflitti"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Connessione automatica"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Collegati a"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Collegati a:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Collegamento al demone SETroubleshoot..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Connessione a dbus non riuscita: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Collegamento alla macchina"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Errore di connessione"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Connessione fallita"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Il collegamento è scaduto."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "La connessione andrà persa"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Consiste di"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Dominio contattato"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Contiene:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Continua"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Continua la sessione"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Controllo"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Controllo"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Controllo"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Convertibile"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Copiato"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Copia"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Copia negli appunti"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Core $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "Impossibile contattare $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Posizione del dump di crash"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Segnalazione di arresti anomali"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Sistema in crash"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Crea"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "Crea gruppo di volumi LVM2"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "Creare un dispositivo RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Creare un dispositivo RAID"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Crea un pool Stratis"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Crea una nuova chiave SSH e autorizzala"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Crea uno snapshot del filesystem $0"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Crea un account con un UID non univoco"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Crea l'account con una password debole"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Creare e cambiare la proprietà della home directory"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Crea e monta"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create and mount"
+msgid "Create and start"
+msgstr "Crea e monta"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Crea filesystem"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Crea"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Crea un volume logico"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Crea un nuovo account"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Crea nuovo filesystem"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Crea nuovo gruppo"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Crea un nuovo volume logico"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Crea un nuovo file di attività con questo contenuto."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "Crea un nuovo volume logico"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "crea soltanto"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Crea partizione"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Crea partizione su $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Crea tabella delle partizioni"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Crea snapshot"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Crea snapshot e monta"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Crea soltanto lo snapshot"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create storage volume"
+msgid "Create storage device"
+msgstr "Crea volume di archiviazione"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Crea volume"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Crea volume thin"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Creare timer"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Crea gruppo di volumi"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys, author fvalen
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Creato"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Creazione di un gruppo di volumi LVM2 $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "Creazione di un dispositivo RAID $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "Creazione del dispositivo VDO"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Creazione del filesystem su $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Creazione di un volume logico $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Creazione della partizione $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Creazione di uno snapshot di $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"La creazione di questo $0 interromperà la connessione al server e renderà "
+"non disponibile l'interfaccia utente di amministrazione."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Critico e superiore"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Cryptographic Policies è un componente di sistema che configura i "
+"sottosistemi crittografici di base, coprendo i protocolli TLS, IPSec, SSH, "
+"DNSSec e Kerberos."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "policy di crittografia"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "La policy di crittografia è incoerente"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Boot corrente"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Utilizzo massimo attuale della CPU"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Attualmente in uso"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Currently in use"
+msgid "Currently not supported"
+msgstr "Attualmente in uso"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "personalizzato"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "Policy di crittografia personalizzata"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Opzioni di montaggio personalizzate"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Porte personalizzate"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom path"
+msgid "Custom type"
+msgstr "Path personalizzato"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Zone personalizzate"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "DEFAULT con verifica della firma SHA-1 consentita."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "Domini di ricerca DNS"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "Domini di ricerca DNS $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Giornaliero"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Avviso di pericolo:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Scuro"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Dati"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Dati utilizzati"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Le specifiche della data devono essere nel formato AAAA-MM-GG hh:mm:ss. In "
+"alternativa si intendono le stringhe 'yesterday', 'today', 'tomorrow'. 'now' "
+"si riferisce all'ora corrente. Infine, possono essere specificati tempi "
+"relativi, preceduti da '-' o '+'"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Disattiva"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Rinomina il volume logico"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Disattivazione"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Disattivazione $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Debug e superiore"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Diminuisci di uno"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+#, fuzzy
+#| msgid "RAID 4 (dedicated parity)"
+msgid "Dedicated parity (RAID 4)"
+msgstr "RAID 4 (parità dedicata)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Deduplicazione"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Predefinito"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Ritardo"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Il ritardo deve essere un numero"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Cancella"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Cancella $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Elimina un account"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Cancella file"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Elimina gruppo"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Cancella"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Eliminare il rapporto in modo permanente?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"L'eliminazione di $0 interromperà la connessione al server e renderà "
+"l'interfaccia utente di amministrazione non disponibile."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Eliminazione $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Eliminazione del gruppo di volumi LVM2 $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr ""
+"L'eliminazione di un pool Stratis cancellerà tutti i dati in esso contenuti."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr ""
+"L'eliminazione di un filesystem cancellerà tutti i dati in esso contenuti."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr ""
+"L'eliminazione di un volume logico cancellerà tutti i dati in esso contenuti."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr ""
+"L'eliminazione di una partizione cancellerà tutti i dati in essa contenuti."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting erases all data on a RAID device."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "L'eliminazione cancella tutti i dati su un dispositivo RAID."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "L'eliminazione cancella tutti i dati su un dispositivo VDO."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "L'eliminazione cancella tutti i dati su un gruppo di volumi."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting erases all data on a volume group."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "L'eliminazione cancella tutti i dati su un gruppo di volumi."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "L'eliminazione rimuoverà i seguenti file:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Descrizione"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Desktop"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Rimovibile"
+
+# translation auto-copied from project libreport, version master, document
+# libreport
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Dettagli"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Sviluppo"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Dispositivo"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "File dispositivo"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Il dispositivo è in sola lettura"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Nome del servizio"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Rapporti diagnostici"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Directory"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "La cartella $0 non è scrivibile o non esiste."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Disabilita il multithreading simultaneo"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Disabilita il firewall"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Disabilita tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Disabilitato"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Non consentire password interattiva"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Non consentire l'autenticazione con password"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Non consentire l'esecuzione (mask)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Disconnetti"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Disconnesso"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "I/O disco"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Il disco è OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Disco non funzionante"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Frase di accesso del disco"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Dischi"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Uso dei dischi"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+#, fuzzy
+#| msgid "Dismiss $0 alert"
+#| msgid_plural "Dismiss $0 alerts"
+msgid "Dismiss"
+msgstr "Ignora $0 avviso"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Ignora $0 avviso"
+msgstr[1] "Ignora $0 avvisi"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Ignora gli avvisi selezionati"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Lingua di visualizzazione"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+#, fuzzy
+#| msgid "RAID 5 (distributed parity)"
+msgid "Distributed parity (RAID 5)"
+msgstr "RAID 5 (parità distribuita)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "Non montare"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Non montare automaticamente all'avvio"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Stazione di docking"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Non si avvia automaticamente"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "Non montare durante il boot"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Indirizzo del dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Nome dell'amministratore di dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Password dell'amministratore di dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Il dominio non può essere contattato"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Il dominio non è supportato"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Non ripetere"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+#, fuzzy
+#| msgid "RAID 6 (double distributed parity)"
+msgid "Double distributed parity (RAID 6)"
+msgstr "RAID 6 (doppia parità distribuita)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Scarica"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Scarica gratuitamente un nuovo browser"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Scaricato"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Download in corso"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Download di $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Unità"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Dual rank"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Partizione estesa"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Modifica"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Modifica /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Modifica Tang keyserver"
+
+#: pkg/networkmanager/vlan.jsx:91
+#, fuzzy
+#| msgid "VLAN settings"
+msgid "Edit VLAN settings"
+msgstr "Impostazioni VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:135
+#, fuzzy
+#| msgid "Bond settings"
+msgid "Edit bond settings"
+msgstr "Impostazioni del bond"
+
+#: pkg/networkmanager/bridge.jsx:96
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Edit bridge settings"
+msgstr "Impostazioni del bridge"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Modifica servizi personalizzati nella zona $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Modifica host"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Modifica host"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Modifica motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Punto di montaggio"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Modifica regole e zone"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Modifica servizio"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Modifica servizio $0"
+
+#: pkg/networkmanager/team.jsx:154
+#, fuzzy
+#| msgid "Team settings"
+msgid "Edit team settings"
+msgstr "Impostazioni del team"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Modifica utente"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "La modifica di una chiave richiede uno slot libero"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Espulsione $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "PC integrato"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Vuoto"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Vuoto"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Password vuota"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Svuotamento $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Abilita"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Abilita o disabilita il dispositivo"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Attiva il servizio"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Abilita il firewall"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Attivato"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "Abilitazione di $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Crittografa dati"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+#, fuzzy
+#| msgid "Edit Tang keyserver"
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Modifica Tang keyserver"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+#, fuzzy
+#| msgid "Encryption passphrase"
+msgid "Encrypt data with a passphrase"
+msgstr "Passphrase di crittografia"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Crittografato"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "$0 crittografato"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted Stratis pool $0"
+msgid "Encrypted Stratis pool"
+msgstr "Pool Stratis crittografato $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Volume logico criptato di $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Partizione criptata di $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Cifratura"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Opzioni di cifratura"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Passphrase di crittografia"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Tipo di crittografia"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Concluso"
+
+#: pkg/networkmanager/wireguard.jsx:309
+#, fuzzy
+#| msgid "Entrypoint"
+msgid "Endpoint"
+msgstr "Punto d'ingresso"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Applicazione"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Aggiornamenti disponibili"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Immettere un indirizzo MAC valido"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Intera sottorete"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Voce a $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Cancellazione $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Errata"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Errore"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Errore e superiore"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Si è verificato un errore"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "Errore nell'installazione di $0: PackageKit non è installato"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Messaggio di errore"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr ""
+"Errore durante l'esecuzione di semanage per rilevare le modifiche di sistema"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Errore nel salvare le chiavi autorizzate: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Errore durante l'impostazione del modo SELinux: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Esempio: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Esempio: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Password eccellente"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Chassis di espansione"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Scadenza dell'account il"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Scadenza dell'account il $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Esporta"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Esporta in rete"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Informazioni estese"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Partizione estesa"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS non è abilitato correttamente"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS con ulteriori restrizioni dei Criteri Comuni."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Fallito"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Impossibile aggiungere la macchina: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Impossibile aggiungere la porta"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Impossibile aggiungere il servizio"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Impossibile aggiungere la zona"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Impossibile cambiare la password"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "Impossibile configurare PCP"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Impossibile cancellare l'avviso: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Impossibile disabilitare tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Impossibile disabilitare il profilo tuned"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Impossibile modificare la macchina: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Impossibile modificare il servizio"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "Impossibile abilitare firewalld"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Impossibile abilitare tuned"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Impossibile recuperare i logs"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Impossibile caricare le chiavi autorizzate."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Impossibile carica l'unità"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"Impossibile analizzare i file dell'unità per dnf-automatic.timer o dnf-"
+"automatic-install.timer. Rimuovi le impostazioni personalizzate per "
+"configurare gli aggiornamenti automatici."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Impossibile riavviare il servizio"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Impossibile salvare le modifiche in /etc/motd"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Impossibile salvare le impostazioni"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Impossibile avviare"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Impossibile cambiare profilo"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Stato file"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "File system"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "Il filesystem è bloccato"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Nome del file system"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Creazione del filesystem su $target"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+#, fuzzy
+#| msgid "The filesystem is already mounted at $0. Proceeding will unmount it."
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "Il file system è già montato in $0. Procedendo verrà smontato."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Filtra per nome o descrizione"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Filtra oggetti menu"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Filtra Servizi"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Filtri"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Impronta digitale"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Firewall"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Il firewall non è disponibile"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Versione firmware"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "Fix supporto NBDE"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Dimensione font"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Esecuzione non autorizzata"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Forza modifica"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Forza Cancella"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Forzare la modifica della password"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Formatta"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Formatta $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Formatta e monta"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format and mount"
+msgid "Format and start"
+msgstr "Formatta e monta"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Formatta soltanto"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr ""
+"La formattazione cancellerà tutti i dati contenuti in un dispositivo di "
+"memoria."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Ritardo in avanti $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Numero del frame"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Spazio libero"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Ricerca libera"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Venerdì"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "Da"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Nome completo"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Gateway"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Generale"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Generato"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Vai a $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Vai all'applicazione"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Vai ora"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Visibilità del grafico"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Menu delle opzioni di visibilità del grafico"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Gruppo"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Nome del gruppo"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Gruppi"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Espandi"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Espandi Contenuto"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Espandi le dimensioni logiche di $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Espandi il volume logico"
+
+#: pkg/storaged/block/resize.jsx:409
+#, fuzzy
+#| msgid "partition"
+msgid "Grow partition"
+msgstr "partizione"
+
+#: pkg/storaged/stratis/pool.jsx:337
+#, fuzzy
+#| msgid "Grow to take all space"
+msgid "Grow the pool to take all space"
+msgstr "Espandi per occupare tutto lo spazio"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Espandi per occupare tutto lo spazio"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Modalità hairpin"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Modalità hairpin"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Palmare"
+
+#: pkg/storaged/drive/drive.jsx:64
+#, fuzzy
+#| msgid "Hard Disk"
+msgid "Hard Disk Drive"
+msgstr "Disco rigido"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Informazioni hardware"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Salute"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Ciao tempo $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Aiuto"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Hide confirmation password"
+msgstr "Conferma la password"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "User password"
+msgid "Hide password"
+msgstr "Password utente"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "ID gerarchico"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr ""
+"Maggiore interoperabilità a scapito di una maggiore superficie di attacco."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Conteggio pacchetti cronologia"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Home Directory"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Host"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "La chiave host non è corretta"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Nome host"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Host"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Orario"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Ore"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "Come controllare"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "Indirizzo IP"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"Indirizzo IP con prefisso di routing. Separare più valori con una virgola. "
+"Esempio: 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+#, fuzzy
+#| msgid "IPv4 address"
+msgid "IPv4 addresses"
+msgstr "Indirizzo IPv4"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "Impostazioni IPv4"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "Impostazioni IPv6"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Identificativo"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Se lasciato vuoto, l'ID verrà generato in base alla porta dei servizi "
+"associati e ai numeri di porta"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Se l'impronta digitale coincide, clicca \"Accetta la chiave e autentica\". "
+"Altrimenti, non autenticare e contatta l'amministratore."
+
+#: pkg/shell/hosts_dialog.jsx:480
+#, fuzzy
+#| msgid ""
+#| "If the fingerprint matches, click 'Accept key and connect'. Otherwise, do "
+#| "not connect and contact your administrator."
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Se l'impronta digitale coincide, clicca 'Accetta la chiave e connetti'. "
+"Altrimenti, non connettere e contatta l'amministratore."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ignora"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "Ingresso"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "Nel terminale, esegui: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Per consentire l'accesso a $0 come $1 senza password in futuro, utilizza la "
+"password di accesso di $2 su $3 come password della chiave, o lascia vuota "
+"la password della chiave."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "In sincronizzazione"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Inattiva"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "Crea un volume logico"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Servizi inclusi"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"Le richieste in ingresso sono bloccate per impostazione predefinita. Le "
+"richieste in uscita non sono bloccate."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Montaggio del filesystem incoerente"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Aumenta di uno"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Memoria indice"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Indiretto"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Info"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Info e superiore"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Inizializzare"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Inizializza il disco $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "L'inizializzazione cancella tutti i dati su un disco."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Inizializzazione..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Approfondimenti: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Installa"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Installa il supporto NFS"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Installa il supporto Stratis"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Installa tutti gli aggiornamenti"
+
+#: pkg/apps/application-list.jsx:200
+#, fuzzy
+#| msgid "Package information"
+msgid "Install application information"
+msgstr "Informazioni sul pacchetto"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Installa cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Installa gli aggiornamenti kpatch"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "Installa il supporto realmd"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Installa gli aggiornamenti di sicurezza"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr ""
+"Installare setroubleshoot-server per la risoluzione dei problemi degli "
+"eventi SELinux."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Installa il software"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Installare il pacchetto $0"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr ""
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Installato"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Installazione in corso"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Installazione di $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "L'installazione di $0 rimuoverebbe $1."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Installazione pacchetti"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Interfaccia"
+msgstr[1] "Interfacce"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Membri interfaccia"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Interfacce"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Errore interno"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Errore interno: challenge header non valido"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Non valido"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Indirizzo $0 non valido"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Formato data non valido"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Formato data non valido e formato ora non valido"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Data di scadenza non valida"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Autorizzazioni file non valide"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Chiave non valida"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Metrica $0 non valida"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Numero di giorni non valido"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Numero porta non valido"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Prefisso $0 non valido"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Prefisso o maschera di rete non valido $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Intervallo non valido"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Formato ora non valido"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Fuso orario non valido"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Password o nome utente non valido"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "Gateway IoT"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "sshd è in esecuzione su una porta diversa?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Lavori"
+
+# ctx::sourcefile::Navigation Menu
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Associa"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Entra in un dominio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Associa al dominio"
+
+# ctx::sourcefile::Navigation Menu
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Associazione"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "L'associazione a un dominio richiede l'installazione di realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "L'associane a questo dominio non è supportata"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Associa al namespace di"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Registro"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Voce del registro"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Voce del registro non trovata"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Salta a"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit non è installato"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Test impostazioni kdump"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Mantenere la connessione"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Dump del crash del kernel"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Kernel dump"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "La patch live Kernel $0 è attiva"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "La patch live Kernel $0 è installata"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Impostazioni della patch live del kernel"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Patch live del kernel"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Password della chiave"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr ""
+"Gli slot per chiavi con tipi sconosciuti non possono essere modificati qui"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Fonte chiave"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Chiavi"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Key server"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Indirizzo Key server"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "La rimozione del key server può impedire lo sblocco di $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "Chiave LACP"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "LEGACY con interoperabilità ad Active Directory."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "Pool"
+msgid "LVM2 VDO pool"
+msgstr "Pool"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Volume logico"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Volumi logici"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volume"
+msgstr "Volume fisico"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volumes"
+msgstr "Volume fisico"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "Gruppo di volumi LVM2"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "Gruppo di volumi LVM2 $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Portatile"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Ultime 24 ore"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Ultimi 7 giorni"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "ultima attività"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "L'ultimo slot per chiavi non può essere rimosso"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Ultimo controllo: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "L'ultimo slot per chiavi non può essere rimosso"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Ultimo accesso"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Ultima modifica: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Ultimo accesso riuscito:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Per saperne di più"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "Lasciare $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Abbandona dominio"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Lascia vuoto per saltare la crittografia"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Concesso in licenza sotto GNU LGPL versione 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Chiaro"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Limitare l'accesso"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Accesso limitato"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"La modalità di accesso limitato limita i privilegi di amministratore. Alcune "
+"parti della Web Console avranno funzionalità ridotte."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Limiti"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Ritardo caduta collegamento"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Collegamento locale"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Monitoraggio dei collegamenti"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Ritardo ripristino collegamento"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Link watch"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Collegato"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem"
+msgid "Linux filesystem data"
+msgstr "Smonta il file system"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Spazio di swap"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Ascolta"
+
+#: pkg/networkmanager/wireguard.jsx:242
+#, fuzzy
+#| msgid "Listen"
+msgid "Listen port"
+msgstr "Ascolta"
+
+#: pkg/networkmanager/wireguard.jsx:149
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Listen port must be a number"
+msgstr "La dimensione deve essere un numero"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Elencando unità"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Unità elencate fallite: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Carico"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Bilanciamento del carico"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Carica dati precedenti"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Carica voci precedenti"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Caricamento degli aggiornamenti disponibili non riuscito"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Caricamento degli aggiornamenti disponibili, attendere....."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Carica voci precedenti"
+
+# translation auto-copied from project evolution, version 3.8.5, document
+# evolution-3.8
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Caricamento delle chiavi..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Caricamento delle chiavi SSH non riuscito"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Caricamento delle unità fallito"
+
+# translation auto-copied from project evolution, version 3.8.5, document
+# evolution-3.8
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Caricamento pacchetti..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Caricamento modifiche del sistema..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Caricamento unità fallito"
+
+# translation auto-copied from project evolution, version 3.8.5, document
+# evolution-3.8
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Caricamento in corso..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Account locali"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "File system locale"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Punto di montaggio locale"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "No storage"
+msgid "Local storage"
+msgstr "Nessuna archiviazione"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "localmente in $0"
+
+# xmp (proprietà immagine)
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Posizione"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Blocca"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "Blocca $0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Blocca account"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgid "Locked"
+msgid "Locked data"
+msgstr "Bloccato"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Blocco $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Accedi"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Entra in $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Accedi con il tuo account utente del server."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Messaggi di log"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Esci"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "disconnetti l'utente"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Autenticato"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Logico"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Volume logico (snapshot)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Dimensione logica"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Volume logico"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Volume logico (snapshot)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Volume logico di $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Accesso"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Effettua di nuovo il login"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Login fallito"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Formato di accesso"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Cronologia accessi"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Elenco cronologia accessi"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "Disconnetti $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Logout riuscito"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Log"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Desktop a basso profilo"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Lunch box"
+
+# translation auto-copied from project Satellite6 Foreman, version 6.1,
+# document foreman
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+# translation auto-copied from project Blivet, version master, document blivet
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "Dispositivo RAID"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "Dispositivo RAID $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Il dispositivo è in sola lettura"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "Il servizio è in funzione"
+
+# translation auto-copied from project Blivet, version master, document blivet
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "Dispositivo RAID"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Aggiungi dischi"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (consigliato)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU deve essere un numero positivo"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "ID macchina"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Impronte digitali delle chiavi SSH della macchina"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Chassis del server principale"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Manutenzione"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+#, fuzzy
+#| msgid "Rename filesystem"
+msgid "Manage filesystem sizes"
+msgstr "Rinominare file system"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Gestisci archiviazione"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Interfacce gestite"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "Gestione dei LVM"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "Gestione dei montaggi NFS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "Gestione RAID"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "Gestione VDO"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "Gestione VLAN"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Gestione del firewall"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Gestendo bond di rete"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Gestendo ponti della rete"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Gestendo team della rete"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Gestione delle partizioni"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Gestione dei drive fisici"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Gestione dei servizi"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Gestione degli aggiornamenti software"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Gestione degli account utente"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Manuale"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Manualmente"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Marco $target come difettoso"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Maschera servizio"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Mascherato"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Il mascheramento del servizio impedisce l'avvio di tutte le unità da esso "
+"dipendenti. Questo può avere un impatto maggiore del previsto. Conferma di "
+"voler mascherare questa unità."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Età massima del messaggio $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Unità ottica"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Memoria"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Tecnologia di memoria"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Utilizzo memoria"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Utilizzo memoria/swap"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Fuso"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Messaggio agli utenti autenticati"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "I messaggi relativi all'errore potrebbero essere trovati nel registro:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Metadati utilizzati"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Metodo"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Metriche"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Metriche e storia"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Non è stato possibile caricare la cronologia delle metriche"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Impostazioni delle metriche"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Mini tower"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Il minuto deve essere un numero compreso tra 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "Minuti"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minuti"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Mitigazioni"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Modalità"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Modello"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Modifica $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Lunedì"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Intervallo di monitoraggio"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Obiettivi di monitoraggio"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Mensilmente"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Più informazioni..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Monta"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+#, fuzzy
+#| msgid "Mount point"
+msgid "Mount Point"
+msgstr "Punto di montaggio"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "Monta dopo che la rete diventa disponibile, ignora il fallimento"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Monta anche automaticamente all'avvio"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Montaggio all'avvio"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Monta automaticamente su $0 all'avvio"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Monta prima dell'avvio dei servizi"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Configurazione di montaggio"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Monta Filesystem"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Monta ora"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Monta ora su $0"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Opzioni di montaggio"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Punto di montaggio"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Il punto di montaggio non può essere vuoto"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Il punto di montaggio non può essere vuoto."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Punto di montaggio già utilizzato per $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Il punto di montaggio deve iniziare con \"/\"."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Monta in sola lettura"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Monta senza aspettare, ignora il fallimento"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Monto $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Monta prima dell'avvio dei servizi"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Monta durante l'avvio dei servizi"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr "Monta in parallelo con i servizi, ma dopo che la rete è disponibile"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Chassis multisistema"
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Multipathed devices"
+msgid "Multipathed devices"
+msgstr "Dispositivi multipath"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "Montaggio NFS"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "Server NTP"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Nome"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Il nome non può essere vuoto."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Il nome non può essere vuoto."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Il nome non può essere più lungo di $0 byte"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Il nome non può essere più lungo di $0 caratteri"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Il nome non può essere più lungo di 127 caratteri."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Il nome non può essere più lungo di 127 caratteri."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Il nome non può contenere il carattere '$0''."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "Il nome non può contenere il carattere '$0''."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Il nome non può contenere spazi bianchi."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "E' necessario almeno un server NTP"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Rete"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "I/O rete"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Bond di Rete"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "I dispositivi di rete e i grafici richiedono NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Log di rete"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Utilizzo rete"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager non è installato"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager non è in esecuzione"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Utilizzo rete"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Rete"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Mai"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Nessun account scaduto"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Non far scadere mai la password"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Nessuno uutenticato"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Nuovo supporto NFS"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Nuovo host"
+
+#: pkg/shell/hosts_dialog.jsx:464
+#, fuzzy
+#| msgid "New host"
+msgid "New host: $0"
+msgstr "Nuovo host"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Nuova password della chiave"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Nuovo nome"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Nuova frase di accesso"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Nuova password"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "La nuova password non è stata accettata"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Avanti"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "No"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "Nessun ID specificato"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Nessun avviso SELinux."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Nessuna applicazione installata o disponibile."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Non ci sono slot disponibili"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+#, fuzzy
+#| msgid "No disks are available."
+msgid "No block devices are available."
+msgstr "Nessun disco disponibile."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "Nessun dispositivo di avvio trovato"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Nessun carrier"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Nessuna configurazione trovata"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Nessun dato disponibile"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Nessun dato disponibile tra $0 e $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Nessun ritardo"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Nessuna descrizione disponibile"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Nessuna descrizione fornita."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "Nessun dispositivo di avvio trovato"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Nessun disco disponibile."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Nessun log trovato"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "nessun risultato trovato"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Nessuna crittografia"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Nessun evento"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Nessun file system"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Nessun file system"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Nessuno slot libero per le chiavi"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Nessuno spazio libero disponibile"
+
+#: pkg/storaged/partitions/partition.jsx:79
+#, fuzzy
+#| msgid "Create partition"
+msgid "No free space after this partition"
+msgstr "Crea partizione"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Non è stato specificato il nome del gruppo"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Non sono state trovate chiavi host."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Nessun pacchetto di installazione trovato per questa applicazione."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Nessuna chiave aggiunta"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "Nessuna lingua trovata"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Nessuna voce nel log"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Nessun volume logico"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Nessun log trovato"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Nessun risultato corrispondente"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Nessun supporto inserito"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Nessun partizionamento"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "Nessun partizionamento"
+
+#: pkg/networkmanager/wireguard.jsx:341
+#, fuzzy
+#| msgid "No keys added"
+msgid "No peers added."
+msgstr "Nessuna chiave aggiunta"
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "Volumi fisici"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Nessun nome reale specificato"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "nessun risultato trovato"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Nessun risultato corrisponde ai criteri del filtro. Cancella tutti i filtri "
+"per mostrare i risultati."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Nessuna regola trovata"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No storage"
+msgid "No storage found"
+msgstr "Nessuna archiviazione"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Nessun volume logico"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Nessun file o directory"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Nessuna modifica di sistema"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Nessun report di sistema"
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Nessun aggiornamento"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Nessun nome utente specificato"
+
+# FIXME: consultare bug 572158
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Nessuno"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Chiave privata invalida"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Non autorizzato a disabilitare il firewall"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Non autorizzato ad abilitare il firewall"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Non disponibile"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Non connesso"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Non connesso a Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Non connesso all'host"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough free space"
+msgstr "Spazio non sufficiente per espandere."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space"
+msgstr "Spazio non sufficiente per espandere."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "Spazio non sufficiente per espandere."
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Non trovato"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Non installato"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "The user <b>$0</b> is not permitted to modify realms"
+msgid "Not permitted to configure network devices"
+msgstr "L'utente non <b>$0</b>è autorizzato a modificare i regni"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+#, fuzzy
+#| msgid "The user <b>$0</b> is not permitted to modify realms"
+msgid "Not permitted to configure realms"
+msgstr "L'utente non <b>$0</b>è autorizzato a modificare i regni"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Non è consentito eseguire questa azione."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Non pronto"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Non Registrato"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Non in esecuzione"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Nessun set up"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Non sincronizzato"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Note"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Portatile"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Notice e oltre"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Offuscare indirizzi di rete, nomi host e nomi utente"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Offuscato"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Si è verificato $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Si è verificato tra $0 e $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Occorrenze"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "Ok"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Vecchia frase di accesso"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Vecchia password"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Vecchia password non accettata"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "Su un dispositivo montato"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "In caso di guasto"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Una volta installato Cockpit, abilitarlo con \"systemctl enable --now "
+"cockpit.socket\"."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Solo $0 di $1 sono utilizzati."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Sono ammessi solo lettere, numeri, : , _ , . , @ ,"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Solo emergenza"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"Utilizzare solo algoritmi approvati e consentiti durante l'avvio in modalità "
+"FIPS."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Ooops!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "Apri il servizio pmproxy nel firewall per condividere le metriche."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Operazione '$operation' su $target"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Opzioni"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Oppure utilizzare un browser in bundle"
+
+# translation auto-copied from project control-center, version 3.8.6, document
+# gnome-control-center-2.0
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Altro"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"Altri metodi di autenticazione sono ancora disponibili anche quando "
+"l'autenticazione tramite password interattiva non è consentita."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Altre opzioni"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Uscita"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Panoramica"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Sovrascrivere"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Sovrascrivere i dati esistenti con zeri (lento)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Pacchetto cockpit-pcp mancante per la cronologia delle metriche"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Informazioni sul pacchetto"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit si è interrotto"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit non è installato"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "Codice di errore segnalato da PackageKit $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Pacchetti"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Nome pagina"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Genitore"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Genitore $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Parte di"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Parte di $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Partizione"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Partizione di $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+#, fuzzy
+#| msgid "Volume size is $0. Content size is $1."
+msgid "Partition size is $0. Content size is $1."
+msgstr "La dimensione del volume è $0. La dimensione del contenuto è $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Partizionamento"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Partizioni"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Passivo"
+
+# translation auto-copied from project Anaconda, version master, document
+# anaconda
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Frase di accesso"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "La frase di accesso non può essere vuota"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "La frase di accesso non può essere vuota"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Passphrase da qualsiasi altro slot per chiavi"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "La rimozione della frase di accesso può impedire lo sblocco $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Le frasi di accesso non corrispondono"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Password"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Password modificata con successo"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Scadenza della password"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "La password è più lunga di 256 caratteri"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "La password non è accettabile"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "La password è troppo debole"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "La password deve essere cambiata"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Password non accettata"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Suggerimento per la password"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Incolla"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Incolla errore"
+
+#: pkg/networkmanager/wireguard.jsx:215
+#, fuzzy
+#| msgid "Use existing"
+msgid "Paste existing key"
+msgstr "Usa esistente"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Incollare qui il contenuto del file della chiave SSH pubblica"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Percorso"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Costo percorso"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Costo della traiettoria $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Percorso su server"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Il percorso sul server non può essere vuoto."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Il percorso sul server deve iniziare con \"/\"."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Percorso della directory"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Percorso del file"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Percorsi"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Pausa"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot raccoglie e analizza le metriche delle prestazioni dal "
+"tuo sistema."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Profilo delle prestazioni"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Chassis periferico"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Permanente"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "Eliminare definitivamente $0 il gruppo?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "Eliminare definitivamente $0?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Eliminare definitivamente $0?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete subvolume $0?"
+msgstr "Eliminare definitivamente $0?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Permesso negato"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Permissivo"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Fisico"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "Physical Volumes"
+msgstr "Volumi fisici"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Volumi fisici"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "I volumi fisici non possono essere ridimensionati qui."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Scegli una data"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Unità pinnate"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Intervallo ping"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Target ping"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Unità pinnate"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Pizza box"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Autenticarsi per ottenere l'accesso amministrativo"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Abilita Javascript per usare Web Console"
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Installare il pacchetto $0"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Per favore risolvi il problema e ricarica questa pagina."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Specifica una data di scadenza"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Specifica l'host a cui connettersi"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr ""
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Pool per volumi logici thin"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Pool per volumi con thin provisioning"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Pool per volumi con thin provisioning"
+
+#: pkg/storaged/stratis/pool.jsx:464
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Pool passphrase"
+msgstr "Vecchia frase di accesso"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Porta"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Portatile"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Porte"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Crea partizione"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Lunghezza prefisso"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Lunghezza prefisso o maschera di rete"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Preparazione"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Presente"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Preserva"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Nome dell'host grazioso"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Avvio precedente"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Primario"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Priorità"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Priorità $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+#, fuzzy
+#| msgid "Private"
+msgid "Private key"
+msgstr "Privato"
+
+#: pkg/shell/superuser.jsx:194
+#, fuzzy
+#| msgid "Domain administrator name"
+msgid "Problem becoming administrator"
+msgstr "Nome dell'amministratore di dominio"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Dettagli del problema"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Informazioni sul problema"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Elabora utilizzando la posizione"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Progresso: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Sito web del progetto"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Richiesta tramite passwd scaduta"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Richiesta tramite ssh-add scaduta"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Richiesta tramite ssh-keygen scaduta"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Propagati ricarica su"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Protegge dagli attacchi futuri previsti a breve termine a scapito "
+"dell'interoperabilità."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "Fornisci la passphrase per il pool su questi dispositivi a blocchi:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Chiave pubblica"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Scopo"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (stripe)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (mirror)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (stripe di mirror)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (parità dedicata)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (parità distribuita)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (doppia parità distribuita)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "Chassis RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "Livello RAID"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Chassis a rack"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Casuale"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Intervallo"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "L'intervallo deve essere rigorosamente ordinato"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Posizione"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Raw a un dispositivo"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Leggere"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Leggi di più..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Sola lettura"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Lettura"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Lettura..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Pronto"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Pronto"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Nome dell'host reale"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Il nome host reale può contenere solo caratteri minuscoli, cifre, trattini e "
+"punti (con sottodomini popolati)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Il nome host reale deve essere di 64 caratteri o meno"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "riapplica e riavvia"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Riavvia"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Riavvio dopo il completamento"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Riavvio consigliato"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Riavvio del sistema..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Ricevo"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Host recenti"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Impostazioni consigliate e sicure per gli attuali modelli di minaccia."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Ricollego"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Recupero"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "Recupero del dispositivo RAID $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Aggiorno le informazioni sul pacchetto"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Rifiuto di connessione. L'host è sconosciuto"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Rifiuto di connessione. Il tasto Hostkey non corrisponde"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Rifiuto di connessione. La chiave host è sconosciuta"
+
+#: pkg/networkmanager/wireguard.jsx:224
+#, fuzzy
+#| msgid "Generated"
+msgid "Regenerate"
+msgstr "Generato"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "rigenerazione del initrd"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Registrati…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "I processi e i servizi correlati verranno interrotti forzatamente."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "I processi correlati verranno interrotti forzatamente."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "I servizi correlati verranno interrotti forzatamente."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Ricarica"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Ricarica propagato da"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Ricarica"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Ricaricare lo stato dei servizi rimanenti"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "Remoto su CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "Remoto su FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Remoto su NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "Remoto su NFS"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "Remoto su SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Remoto su SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "Remoto su SSH"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Rimozioni:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Elimina"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Rimuovere $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Rimuovere il servizio $0 dalla zona $1"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Rimuovere $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Rimuovere keyserver Tang?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Rimuovere il dispositivo"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Rimuovere l'host"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Rimuovere l'oggetto"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Remove missing physical volumes?"
+msgstr "Rimozione del volume fisico da $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "Rimuovere la frase di accesso in slot $0?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+#, fuzzy
+#| msgid "Remove passphrase"
+msgid "Remove passphrase?"
+msgstr "Rimuovere la frase di accesso"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Rimuovere il servizio $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Rimuovere la zona $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Rimozione"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Rimozione $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"La rimozione di $0 interromperà la connessione al server e renderà "
+"l'interfaccia utente di amministrazione non disponibile."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "Rimozione $target dal dispositivo RAID"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"La rimozione di una passphrase senza la conferma di un'altra passphrase può "
+"impedire lo sblocco o la gestione delle chiavi, se altre passphrase vengono "
+"dimenticate o perse."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Rimozione del volume fisico da $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"La rimozione del servizio cockpit potrebbe rendere irraggiungibile la Web "
+"Console. Assicurarsi che questa zona non si applichi alla corrente "
+"connessione alla Web Console."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "La rimozione della zona rimuoverà tutti i servizi al suo interno."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Rinomina"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Rinomina il pool Stratis"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Rinominare file system"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Rinomina il gruppo"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "Rinominare il gruppo $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Rinomina il volume logico"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Rinomina gruppo di volumi"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Rinomino $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "Rinominare un gruppo può influenzare le regole di sudo e simili"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Repair logical volume $0"
+msgstr "Rinomina il volume logico"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Riparazione $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Ripeti"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Ripeti mensilmente"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Ripeti la frase di accesso"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Ripeti ogni settimana"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Notifica"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Etichetta del report"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Segnala ad ABRT Analytics"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Segnalato; nessun collegamento disponibile"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Segnalazione fallita"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "La segnalazione è stata annullata"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Rapporti"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Rapporti:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Richiedi la modifica della password ogni $0 giorni"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Richiedi la modifica della password su $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Richiedi la modifica della password al primo accesso"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Richiesto da"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Richiesto da "
+
+# translation auto-copied from project CFSE, version sam-1.2, document app,
+# author fvalen
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Ha bisogno di"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Richiede accesso amministrativo per la modifica"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Requisito"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Requisito di"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Memoria riservata"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Azzera"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Resetta password"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Ridimensionamento $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Il ridimensionamento di un file system crittografato richiede lo sblocco del "
+"disco. Fornire la frase di accesso corrente del disco."
+
+# ctx::sourcefile::/rhn/admin/config/Restart.do
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Riavvia"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Riavvia i servizi"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Riavvia i servizi..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Riavvio"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Ripristino della connessione"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Riprendi"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Riutilizza la crittografia esistente"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Riutilizza la crittografia esistente ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "Rivedi le policy di crittografia"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Revisione dei log"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Round robin"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Rotte"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Esegui a"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Esegui nuovo report"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Eseguire in"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Esegui report"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Esecutore"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "In esecuzione"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Runtime"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "Errori del controllo accessi SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux è disabilitato sul sistema"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux è disabilitato sul sistema."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "Policy SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "Lo stato del sistema SELinux è sconosciuto."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "Risoluzione dei problemi di SELinux"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "Autotest SMART di $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"I rapporti SOS raccolgono informazioni di sistema per aiutare nella diagnosi "
+"dei problemi."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "Chiave SSH"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "la chiave SSH non è un percorso"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "Chiavi SSH"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "Ritardo in avanti STP"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "Ciao tempo STP"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "Età massima del messaggio STP"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "Priorità STP"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Gli utenti di Safari devono importare e fidarsi del certificato della CA "
+"autofirmata:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Sabati"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Salva"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Salva e riavvia"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Salva modifiche"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Risparmia spazio comprimendo i singoli blocchi con LZ4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Risparmia spazio memorizzando blocchi di dati identici una sola volta"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Per salvare una nuova frase di accesso è necessario sbloccare il disco. "
+"Fornire una frase di accesso attuale del disco."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Spegnimento programmato alle $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Riavvio programmato alle $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "PC sigillato"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Ricerca"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "dominio di ricerca"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Cerca per nome o ID"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Cerca per nome, gruppo o ID"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "Il secondo deve essere un numero compreso tra 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Secondi"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Chiavi secure shell"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Cancellazione sicura di $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Configurazione e risoluzione dei problemi di Security Enhanced Linux"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Aggiornamenti di sicurezza disponibili"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Solo aggiornamenti di sicurezza"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Gli aggiornamenti di sicurezza saranno applicati $0 a $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Seleziona"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Seleziona un identificativo"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Seleziona metodo"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Invia"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Invio"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Numero di serie"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys, author fvalen
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Server"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Indirizzo del server"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "L'indirizzo del server non può essere vuoto."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Il server non può essere vuoto."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Il server ha chiuso la connessione."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Software Server"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Servizio"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Il servizio ha un errore"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Log servizi"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Servizi"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Servizi che utilizzano la posizione"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Sessione"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "La sessione sta per scadere"
+
+# ctx::sourcefile::/rhn/systems/details/virtualization/VirtualGuestsList.do
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Imposta"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Imposta il nome host"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Crea partizione su $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Imposta password"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Imposta tempo"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Imposta a"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Configura"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Imposta una password debole"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"L'impostazione devia dallo stato configurato e sarà ripristinata al prossimo "
+"avvio."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Impostazione"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Impostazione del dispositivo di loop $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Impostazioni"
+
+# translation auto-copied from project Customer Portal Translations, version
+# PCM_template, document template, author fvalen
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Severità"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Condivisa"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Shell"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Script di shell"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "Mostra tutti i threads"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Mostra tutti i threads"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Show confirmation password"
+msgstr "Conferma la password"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Mostra le impronte digitali"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Mostra i messaggi contenenti una determinata stringa."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Mostra i messaggi per l'unità systemd specificata."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Mostra i messaggi da un avvio specifico."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Mostra più relazioni"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "New password"
+msgid "Show password"
+msgstr "Nuova password"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Mostra relazioni"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Riduci"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Restringi il volume logico"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+#, fuzzy
+#| msgid "partition"
+msgid "Shrink partition"
+msgstr "partizione"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Riduci il Volume"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Arresto"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Spegni"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Da"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Single rank"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Dimensione"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "La dimensione non può essere negativa"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "La dimensione non può essere zero"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "La dimensione è troppo grande"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "La dimensione deve essere un numero"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "La dimensione deve essere almeno $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Salta la navigazione principale"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Salta al contenuto"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Slot"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Slot $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Snapshot"
+
+# translation auto-copied from project Satellite6 Katello, version Sam-1.3.0,
+# document katello, author fvalen
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Socket"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Aggiornamenti software"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Soluzioni alternative basate su software aiutano a prevenire problemi di "
+"sicurezza della CPU. Queste mitigazioni hanno l'effetto collaterale di "
+"ridurre le prestazioni. Modificare queste impostazioni a proprio rischio."
+
+#: pkg/storaged/drive/drive.jsx:63
+#, fuzzy
+#| msgid "Solid-State Disk"
+msgid "Solid State Drive"
+msgstr "Disco a stato solido"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Soluzione applicata con successo"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Soluzione fallita"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Soluzioni"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"Qualche altro programma sta attualmente utilizzando il gestore di pacchetti, "
+"si prega di attendere..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Alcuni software devono essere riavviati manualmente"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Ordinati dal meno affidabile al più affidabile"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Computer space-saving"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Protocollo spanning tree"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Protocollo dello spanning tree (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Ricambio"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Tempo specifico"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Velocità"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Stabile"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Avvia"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Avvia e Abilita"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Avvia Multipath"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Avvia il servizio"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr ""
+"Inizia a mostrare le voci a partire dalla data specificata o da una data "
+"successiva."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "Inizia a mostrare le voci entro e non oltre la data specificata."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Avviato"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "Avvio del dispositivo RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Avvio dello spazio di swap $target"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Stato"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Statico"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Stato"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Stick PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Sticky"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Ferma"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Ferma e Disabilita"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Ferma e rimuovi"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Ferma e smonta"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Ferma dispositivo"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Interrompi la modifica degli host"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Ferma report"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "Arresto del dispositivo RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Arresto dello spazio di swap $target"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Archiviazione"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "L'archiviazione non può essere gestita su questo sistema."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Log archiviazione"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Conserva la frase di accesso"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Frase di accesso memorizzata"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "$0 Dispositivo a blocchi"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "Add block devices"
+msgid "Stratis block devices"
+msgstr "Aggiungi dispositivi a blocchi"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+#, fuzzy
+#| msgid "VDO backing devices can not be made smaller"
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "I dispositivi di supporto VDO non possono essere resi più piccoli"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystem"
+msgstr "Crea filesystem"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems"
+msgstr "Crea filesystem"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Crea filesystem"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Pool Stratis"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:97
+#, fuzzy
+#| msgid "Set password"
+msgid "Strong password"
+msgstr "Imposta password"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "urtare"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Stile"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Sub-Chassis"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Sub-Notebook"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "La sottoscrizione alle segnalazioni di sistemd è fallita"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "Impossibile creare il volume"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Copiato con successo negli appunti"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Copiato con successo negli appunti!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Domeniche"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Swap"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "I $0 filesystem non possono essere ridimensionati qui."
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Swap out"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+#, fuzzy
+#| msgid "Switch off $0"
+msgid "Switch of $0"
+msgstr "Spegni $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Spegni $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Accendi $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+#, fuzzy
+#| msgid "Turn on administrative access"
+msgid "Switch to administrative access"
+msgstr "Attiva l'accesso amministrativo"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Passa ad accesso limitato"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+#, fuzzy
+#| msgid ""
+#| "Switching off <b>$0</b> will break the connection to the server, and will "
+#| "make the administration UI unavailable."
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"La disattivazione di <b>$0</b> interromperà la connessione al server e "
+"renderà l'interfaccia utente di amministrazione non disponibile."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+#, fuzzy
+#| msgid ""
+#| "Switching on <b>$0</b> will break the connection to the server, and will "
+#| "make the administration UI unavailable."
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"L'attivazione di <b>$0</b> interromperà la connessione al server e renderà "
+"l'interfaccia utente di amministrazione non disponibile."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Sincronizzato"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Sincronizzato con $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Sincronizzazione"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "Sincronizzazione del dispositivo RAID $target"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Sistema"
+
+#: pkg/sosreport/sosreport.jsx:520
+#, fuzzy
+#| msgid "System modifications"
+msgid "System diagnostics"
+msgstr "Modifiche di Sistema"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Informazioni di sistema"
+
+# ctx::sourcefile::/systems/SystemEntitlements
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Il sistema è aggiornato"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Modifiche di Sistema"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Ora di sistema"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Unità systemd"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tablet"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Keyserver Tang"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Destinazione"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Destinazioni"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Team"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Porta team"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Impostazioni della porta del team"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminale"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Termina la sessione"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Test di configurazione"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Il test è disponibile solo mentre il servizio kdump è in funzione."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Test impostazioni kdump"
+
+#: pkg/kdump/kdump-view.jsx:374
+#, fuzzy
+#| msgid ""
+#| "This will test kdump settings by crashing the kernel and thereby the "
+#| "system. Depending on the settings, the system may not automatically "
+#| "reboot and the process may take a while."
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Questo testerà le impostazioni di kdump mandando in crash il kernel e quindi "
+"il sistema. A seconda delle impostazioni, il sistema potrebbe non riavviarsi "
+"automaticamente e il processo potrebbe richiedere del tempo."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Test connessione"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+#, fuzzy
+#| msgid "$0 is not available from any repository."
+msgid "The $0 package is not available from any repository."
+msgstr "$0 non è disponibile in nessun archivio web."
+
+#: pkg/storaged/overview/overview.jsx:122
+#, fuzzy
+#| msgid "The $0 package must be installed to create VDO devices."
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "Il pacchetto $0 deve essere installato per creare dispositivi VDO."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+#, fuzzy
+#| msgid "The $0 package must be installed to create VDO devices."
+msgid "The $0 package must be installed."
+msgstr "Il pacchetto $0 deve essere installato per creare dispositivi VDO."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+#, fuzzy
+#| msgid "The $0 package must be installed to create VDO devices."
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "Il pacchetto $0 deve essere installato per creare dispositivi VDO."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "L'indirizzo IP o il nome host non possono contenere spazi."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "L'array RAID è in uno stato degradato"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr "Il dispositivo RAID deve essere in funzione per rimuovere i dischi."
+
+#: pkg/shell/hosts_dialog.jsx:828
+#, fuzzy
+#| msgid ""
+#| "The SSH key ${key} of ${luser} on ${lhost} will be added to the ${afile} "
+#| "file of ${ruser} on ${rhost}."
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+"La chiave SSH ${key} di ${luser} su ${lhost} verrà aggiunta al file ${afile} "
+"di ${ruser} su ${rhost}."
+
+#: pkg/shell/hosts_dialog.jsx:865
+#, fuzzy
+#| msgid ""
+#| "The SSH key {{#strong}}{{key}}{{/strong}} will be made available for the "
+#| "remainder of the session and will be available for login to other hosts "
+#| "as well."
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"La chiave SSH {{#strong}} {{key}} {{/strong}} sarà resa disponibile per il "
+"resto della sessione e sarà disponibile anche per l'accesso ad altri host."
+
+#: pkg/shell/hosts_dialog.jsx:773
+#, fuzzy
+#| msgid ""
+#| "The SSH key for logging in to {{#strong}}{{full_address}}{{/strong}} is "
+#| "protected. You can log in with either your login password or by providing "
+#| "the password of the key at {{#strong}}{{key}}{{/strong}}."
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"La chiave SSH per accedere a {{#strong}} {{full_address}} {{/strong}} è "
+"protetta. Puoi accedere con la tua password di accesso o fornendo la "
+"password della chiave a {{#strong}} {{key}} {{/strong}}."
+
+#: pkg/shell/hosts_dialog.jsx:778
+#, fuzzy
+#| msgid ""
+#| "The SSH key for logging in to {{#strong}}{{full_address}}{{/strong}} is "
+#| "protected. You can log in with either your login password or by providing "
+#| "the password of the key at {{#strong}}{{key}}{{/strong}}."
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"La chiave SSH per accedere a {{#strong}} {{full_address}} {{/strong}} è "
+"protetta. Puoi accedere con la tua password di accesso o fornendo la "
+"password della chiave a {{#strong}} {{key}} {{/strong}}."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"L'account '$0' sarà costretto a cambiare la propria password al prossimo "
+"login"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Il servizio cockpit è incluso automaticamente"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"Lo stato configurato è sconosciuto, potrebbe cambiare al prossimo avvio."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"La creazione di questo dispositivo VDO non è terminata e il dispositivo non "
+"può essere utilizzato."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"L'utente attualmente connesso non è autorizzato a vedere le informazioni "
+"sulle chiavi."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"Il disco deve essere sbloccato prima della formattazione. Fornisci una "
+"passphrase esistente."
+
+#: pkg/sosreport/sosreport.jsx:368
+#, fuzzy
+#| msgid "The $0 could not be deleted"
+msgid "The file $0 will be deleted."
+msgstr "$0 non può essere eliminato"
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "Il file system non ha un punto di montaggio permanente."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "Il file system non ha un punto di montaggio permanente."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Il file system è configurato per essere montato automaticamente all'avvio, "
+"ma il suo container di crittografia non sarà sbloccato in quel momento."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Il file system è attualmente montato ma non verrà montato al prossimo avvio."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Il file system è attualmente montato su $0 ma verrà montato su $1 al "
+"prossimo avvio."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Il file system è attualmente montato su $0 ma non verrà montato dopo il "
+"prossimo avvio."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Il file system non è attualmente montato ma verrà montato al prossimo avvio."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Il file system non è montato."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"Il filesystem verrà sbloccato e montato all'avvio successivo. Ciò potrebbe "
+"richiedere l'immissione di una passphrase."
+
+#: pkg/shell/hosts_dialog.jsx:494
+#, fuzzy
+#| msgid "Show fingerprints"
+msgid "The fingerprint should match:"
+msgstr "Mostra le impronte digitali"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Verrà riavviato il seguente servizio:"
+msgstr[1] "Verranno riavviati i seguenti servizi:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "Il nome completo non deve contenere i due punti."
+
+#: pkg/users/group-create-dialog.js:83
+#, fuzzy
+#| msgid "MTU must be a positive number"
+msgid "The group ID must be positive integer"
+msgstr "MTU deve essere un numero positivo"
+
+#: pkg/users/group-create-dialog.js:66
+#, fuzzy
+#| msgid ""
+#| "The user name can only consist of letters from a-z, digits, dots, dashes "
+#| "and underscores."
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Il nome utente può essere composto solo da lettere da a-z, cifre, punti, "
+"trattini e sottolineature."
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"La directory home $0 esiste già. La sua proprietà verrà cambiata con quella "
+"del nuovo utente."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "L'initrd deve essere rigenerato."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "La password della chiave non può essere vuota"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Le password della chiave non corrispondono"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "La chiave fornita non era valida."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "L'ultimo slot per chiavi non può essere rimosso"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "I processi e i servizi elencati verranno interrotti forzatamente."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "I processi elencati verranno interrotti forzatamente."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "I servizi elencati verranno interrotti forzatamente."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr ""
+"L'utente che ha effettuato l'accesso non è autorizzato a visualizzare le "
+"modifiche di sistema"
+
+#: pkg/shell/indexes.jsx:459
+#, fuzzy
+#| msgid "The machine is restarting"
+msgid "The machine is rebooting"
+msgstr "La macchina si sta riavviando"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "Il punto di montaggio $0 è utilizzato da questi processi:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "Il punto di montaggio $0 è utilizzato da questi servizi:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "La nuova password della chiave non può essere vuota"
+
+#: pkg/shell/hosts_dialog.jsx:679
+#, fuzzy
+#| msgid "The key password can not be empty"
+msgid "The password can not be empty"
+msgstr "La password della chiave non può essere vuota"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Le password non corrispondono"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Le password non corrispondono."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"L'impronta digitale risultante è idonea per la condivisione pubblica, email "
+"inclusa."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Il server ha rifiutato di autenticare '$0' utilizzando l'autenticazione con "
+"password, e non sono disponibili altri metodi di autenticazione supportati."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr ""
+"Il server ha rifiutato di autenticarsi utilizzando qualsiasi metodo "
+"supportato."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"Attualmente il sistema non supporta lo sblocco di un filesystem con un "
+"keyserver Tang durante l'avvio."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"Attualmente il sistema non supporta lo sblocco del filesystem root con un "
+"keyserver Tang."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+"L'utente $0 non è autorizzato a modificare le mitigazioni di sicurezza della "
+"CPU"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+#, fuzzy
+#| msgid "The user <b>$0</b> is not permitted to change profiles"
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "L'utente non <b>$0</b>è autorizzato a modificare i profili"
+
+#: pkg/kdump/kdump-view.jsx:505
+#, fuzzy
+#| msgid "The user <b>$0</b> is not permitted to modify realms"
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "L'utente non <b>$0</b>è autorizzato a modificare i regni"
+
+#: pkg/users/account-details.js:469
+#, fuzzy
+#| msgid "The user must log out and log back in to fully change roles."
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"L'utente deve disconnettersi e riconnettersi per cambiare completamente "
+"ruoli."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Il nome utente può essere composto solo da lettere da a-z, cifre, punti, "
+"trattini e sottolineature."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"La configurazione del browser web impedisce l'esecuzione di Cockpit ($0 "
+"inaccessibile)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Attualmente non ci sono pagine attive"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"Ci sono dispositivi con percorsi multipli sul sistema, ma il servizio "
+"multipath non è in esecuzione."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "Non ci sono servizi attivi in questa zona"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Non ci sono chiavi pubbliche autorizzate per questo account."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr ""
+"Si è verificato un errore imprevisto durante la connessione alla macchina."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Questi passaggi aggiuntivi sono necessari:"
+
+#: pkg/storaged/dialog.jsx:1235
+#, fuzzy
+#| msgid "This web console will be updated."
+msgid "These changes will be made:"
+msgstr "Questa console web sarà aggiornata."
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Volume logico thin"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Pool per volumi con thin provisioning"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"Questo supporto NFS è in uso e solo le sue opzioni possono essere modificate."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr ""
+"Questo dispositivo VDO non utilizza tutti i suoi dispositivi di supporto."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Questo dispositivo non può essere gestito qui."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Questo dispositivo non può essere gestito qui."
+
+#: pkg/storaged/dialog.jsx:1130
+#, fuzzy
+#| msgid "This device is currently used for VDO devices."
+msgid "This device is currently in use."
+msgstr "Questo dispositivo è attualmente utilizzato per dispositivi VDO."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+#, fuzzy
+#| msgid "This field cannot be empty."
+msgid "This field cannot be empty"
+msgstr "Questo campo non può essere vuoto."
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Questo gruppo è il gruppo principale per i seguenti utenti:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr ""
+"Questo host verrà riavviato quando gli aggiornamenti saranno installati."
+
+#: pkg/sosreport/sosreport.jsx:303
+#, fuzzy
+#| msgid "The collected information will be stored locally on the system."
+msgid "This information is stored only on the system."
+msgstr "Le informazioni raccolte saranno memorizzate localmente sul sistema."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "Questo volume logico non è completamente utilizzato dal suo contenuto."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Questa macchina è già stata aggiunta."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "L'operazione potrebbe richiedere del tempo"
+
+#: pkg/storaged/partitions/partition.jsx:204
+#, fuzzy
+#| msgid "This logical volume is not completely used by its content."
+msgid "This partition is not completely used by its content."
+msgstr "Questo volume logico non è completamente utilizzato dal suo contenuto."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:333
+#, fuzzy
+#| msgid "This VDO device does not use all of its backing device."
+msgid "This pool does not use all the space on its block devices."
+msgstr ""
+"Questo dispositivo VDO non utilizza tutti i suoi dispositivi di supporto."
+
+#: pkg/storaged/stratis/pool.jsx:348
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "This pool is in a degraded state."
+msgstr "L'array RAID è in uno stato degradato"
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Questo sistema non è registrato"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Questo sistema utilizza un profilo personalizzato"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Questo sistema utilizza il profilo raccomandato"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"Questo strumento configura i criteri SELinux e può aiutare a comprendere e "
+"risolvere le violazioni dei criteri."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"Questo strumento configura il sistema per scrivere i crash dump del kernel "
+"su disco."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"Questo strumento genera un archivio di informazioni sulla configurazione e "
+"sulla diagnostica del sistema in esecuzione. L'archivio può essere "
+"conservato localmente o centralmente per scopi di registrazione o "
+"tracciamento oppure può essere inviato ai rappresentanti dell'assistenza "
+"tecnica, agli sviluppatori o agli amministratori di sistema per aiutarli "
+"nella ricerca di errori e nel debug."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"Questo strumento gestisce lo storage locale, come i filesystem, i gruppi di "
+"volumi LVM2 e i mount NFS."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"Questo strumento gestisce le reti, come i bond, i bridge, i team, le VLAN e "
+"i firewall utilizzando NetworkManager e Firewalld. NetworkManager è "
+"incompatibile con gli script systemd-networkd di Ubuntu e ifupdown di Debian."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr ""
+"Questa unità non è stata progettata per essere abilitata esplicitamente."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Questo nome utente esiste già"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr ""
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Questo browser web è troppo vecchio per eseguire Web Console ($0 mancante)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Questo aggiungerà una corrispondenza per '_BOOT_ID='. Se non specificato "
+"verranno mostrati i log per l'avvio corrente. Se l'ID di avvio viene omesso, "
+"un offset positivo cercherà gli avvii a partire dall'inizio del journal e un "
+"offset uguale o inferiore a zero cercherà gli avvioi a partire dalla fine "
+"del journal. Pertanto, 1 indica il primo avvio trovato nel journal in ordine "
+"cronologico, 2 il secondo e così via; mentre -0 è l'ultimo avvio, -1 l'avvio "
+"prima dell'ultimo e così via."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Questo aggiungerà una corrispondenza per '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' "
+"e 'UNIT=' per trovare tutti i messaggi possibili per l'unità data. Può "
+"contenere più unità separate da virgola. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Ciò ti consentirà di accedere senza password in futuro."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Questa zona contiene il servizio cockpit. Assicurarsi che questa zona non si "
+"applichi all'attuale connessione della Web Console."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Giovedì"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Livello"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Ora"
+
+# ctx::sourcefile::/rhn/account/LocalePreferences
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Fuso Orario"
+
+#: pkg/systemd/timer-dialog.jsx:155
+#, fuzzy
+#| msgid "Connection failed"
+msgid "Timer creation failed"
+msgstr "Connessione fallita"
+
+#: pkg/systemd/service-details.jsx:725
+#, fuzzy
+#| msgid "Connection failed"
+msgid "Timer deletion failed"
+msgstr "Connessione fallita"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Timer"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Suggerimento: fai in modo che la password della tua chiave corrisponda alla "
+"password di accesso per l'autenticazione automatica in altri sistemi."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Per assicurare che la connessione non sia intercettata da un soggetto terzo "
+"malelvolo, verifica l'impronta digitale dell'host:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Per ottenere gli aggiornamenti software, questo sistema deve essere "
+"registrato con Red Hat, utilizzando il portale clienti Red Hat o un server "
+"locale in abbonamento."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Per verificare un'impronta digitale, esegui su $0 mentre sei fisicamente di "
+"fronte alla macchina o attraverso una rete fidata:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Oggi"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Attiva/disattiva"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Attiva/disattiva la selezione della data"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Attiva/disattiva i filtri"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Troppi dati"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Strumenti"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Primi 5 servizi CPU"
+
+#: pkg/metrics/metrics.jsx:963
+#, fuzzy
+#| msgid "Top 5 CPU services"
+msgid "Top 5 disk usage services"
+msgstr "Primi 5 servizi CPU"
+
+#: pkg/metrics/metrics.jsx:903
+#, fuzzy
+#| msgid "No network devices"
+msgid "Top 5 memory services"
+msgstr "Primi 5 servizi di memoria"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Dimensione totale: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Tower"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Transitorio"
+
+#: pkg/networkmanager/plots.js:39
+#, fuzzy
+#| msgid "Team settings"
+msgid "Transmitting"
+msgstr "Impostazioni del team"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+#, fuzzy
+#| msgid "Triggers"
+msgid "Trigger"
+msgstr "Trigger"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Attivato da"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Trigger"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Risoluzione dei problemi"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+#, fuzzy
+#| msgid "Troubleshoot"
+msgid "Troubleshoot…"
+msgstr "Risoluzione dei problemi"
+
+#: pkg/shell/hosts_dialog.jsx:466
+#, fuzzy
+#| msgid "Untrusted host"
+msgid "Trust and add host"
+msgstr "Host non fidato"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Chiave fidata"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Livello di fedeltà"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Riprova"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Tentativo di sincronizzazione con $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Martedì"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Impossibile avviare tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"tuned è un servizio che monitora il tuo sistema e ottimizza le prestazioni "
+"sotto alcuni carichi di lavoro. il cuore di tuned sono i profili, che "
+"ottimizzano il tuo sistema in casi differenti."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned non è disponibile"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned non è in esecuzione"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned è disattivato"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Attiva l'accesso amministrativo"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Tipo"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Il nome non può contenere il carattere '$0''."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Tipo da filtrare"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys, author fvalen
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Impossibile applicare automaticamente questa soluzione"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Impossibile connettersi a quell'indirizzo"
+
+#: pkg/shell/hosts_dialog.jsx:361
+#, fuzzy
+#| msgid "Unable to get alert: $0"
+msgid "Unable to contact $0."
+msgstr "Impossibile ottenere l'avviso: $0"
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Impossibile contattare l'host specificato $0. Assicurati che ssh sia in "
+"esecuzione sulla porta $1 o specifica un'altra porta nell'indirizzo."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Impossibile ottenere i dettagli dell'avviso."
+
+#: pkg/shell/hosts_dialog.jsx:770
+#, fuzzy
+#| msgid ""
+#| "Unable to log in to {{#strong}}{{full_address}}{{/strong}} using SSH key "
+#| "authentication. Please provide the password. You may want to set up your "
+#| "SSH keys for automatic login."
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Impossibile accedere a {{#strong}}{{full_address}}{{/strong}} utilizzando "
+"l'autenticazione con chiave SSH. Si prega di fornire la password. Potresti "
+"voler configurare le tue chiavi SSH per l'accesso automatico."
+
+#: pkg/shell/hosts_dialog.jsx:768
+#, fuzzy
+#| msgid ""
+#| "Unable to log in to {{#strong}}{{full_address}}{{/strong}}. The host does "
+#| "not accept password login or any of your SSH keys."
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Impossibile accedere a {{#strong}}{{full_address}}{{/strong}}. L'host non "
+"accetta password di accesso o nessuna delle tue chiavi SSH."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Impossibile raggiungere il server"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Impossibile rimuovere il supporto"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+#, fuzzy
+#| msgid "Encrypted logical volume of $0"
+msgid "Unable to repair logical volume $0"
+msgstr "Volume logico criptato di $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+#, fuzzy
+#| msgid "Unable to run fix: %0"
+msgid "Unable to run fix: $0"
+msgstr "Impossibile eseguire la correzione: %0"
+
+#: pkg/kdump/kdump-view.jsx:209
+#, fuzzy
+#| msgid "Unable to apply settings: $0"
+msgid "Unable to save settings"
+msgstr "Impossibile applicare le impostazioni: $0"
+
+#: pkg/kdump/kdump-view.jsx:214
+#, fuzzy
+#| msgid "Unable to apply settings: $0"
+msgid "Unable to save settings: $0"
+msgstr "Impossibile applicare le impostazioni: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Impossibile avviare setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Impossibile smontare il file system"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Non disponibile"
+
+#: pkg/packagekit/kpatch.jsx:238
+#, fuzzy
+#| msgid "Unavailable"
+msgid "Unavailable packages"
+msgstr "Non disponibile"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Annulla"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "Errore inatteso di PackageKit durante l'installazione di $0: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Errore imprevisto"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Dati non riconosciuti"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Unità"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Sconosciuto"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Sconosciuto \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Sconosciuto ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Applicazione sconosciuta"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Configurazione sconosciuta"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Nome host sconosciuto"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Nome del servizio sconosciuto"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Tipo sconosciuto"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Sblocca"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+#, fuzzy
+#| msgid "Mount also automatically on boot"
+msgid "Unlock automatically on boot"
+msgstr "Monta anche automaticamente all'avvio"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Encrypted data"
+msgid "Unlock encrypted Stratis pool"
+msgstr "Dati crittografati"
+
+#: pkg/shell/credentials.jsx:309
+#, fuzzy
+#| msgid "Unlock key"
+msgid "Unlock key $0"
+msgstr "Sblocca la chiave"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Sbloccaggio $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+#, fuzzy
+#| msgid "Unlocking disk..."
+msgid "Unlocking disk"
+msgstr "Sblocco disco..."
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Interfacce non gestite"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Smonta"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+#, fuzzy
+#| msgid "Unmount filesystem"
+msgid "Unmount filesystem $0"
+msgstr "Smonta il file system"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Smonta adesso"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Smontaggio $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Senza nome"
+
+#: pkg/systemd/service-details.jsx:184
+#, fuzzy
+#| msgid "Systemd units"
+msgid "Unpin unit"
+msgstr "Unità systemd"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Dati non riconosciuti"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "Unrecognized data can not be made smaller here."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "I dati non riconosciuti non possono essere ridotti qui."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "I dati non riconosciuti non possono essere ridotti qui."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Volume non supportato"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "Fino a"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Host non fidato"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Aggiorna"
+
+#: pkg/packagekit/updates.jsx:754
+#, fuzzy
+#| msgid "Updates available"
+msgid "Update Success Table"
+msgstr "Aggiornamenti disponibili"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Cronologia degli aggiornamenti"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Aggiorna informazioni sul pacchetto"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+#, fuzzy
+#| msgid "Logout successful"
+msgid "Update was successful"
+msgstr "Logout riuscito"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Aggiornato"
+
+#: pkg/packagekit/updates.jsx:682
+#, fuzzy
+#| msgid "Updated packages may require a restart to take effect."
+msgid "Updated packages may require a reboot to take effect."
+msgstr ""
+"I pacchetti aggiornati possono richiedere un riavvio per avere effetto."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Aggiornamenti disponibili"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Cronologia degli aggiornamenti"
+
+#: pkg/packagekit/autoupdates.jsx:366
+#, fuzzy
+#| msgid "will be applied $0 at $1"
+msgid "Updates will be applied $0 at $1"
+msgstr "sarà applicato $0 a $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Aggiornamento"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Aggiornamento dello stato..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+#, fuzzy
+msgid "Uptime"
+msgstr "Uptime"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Utilizzo"
+
+#: pkg/storaged/storage-controls.jsx:206
+#, fuzzy
+#| msgid "Usage of $0 CPU core"
+#| msgid_plural "Usage of $0 CPU cores"
+msgid "Usage of $0"
+msgstr "Utilizzo del core della $0CPU"
+
+# spazio su disco, quindi maschile
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+#, fuzzy
+#| msgid "Used"
+msgid "Use"
+msgstr "Usato"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+#, fuzzy
+#| msgid "Compression"
+msgid "Use compression"
+msgstr "Compressione"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+#, fuzzy
+#| msgid "Deduplication"
+msgid "Use deduplication"
+msgstr "Deduplicazione"
+
+#: pkg/shell/credentials.jsx:133
+#, fuzzy
+#| msgid "ssh key"
+msgid "Use key"
+msgstr "chiave ssh"
+
+#: pkg/users/account-create-dialog.js:114
+#, fuzzy
+#| msgid "User password"
+msgid "Use password"
+msgstr "Password utente"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Utilizza le seguenti chiavi per autenticarti in altri sistemi"
+
+#: pkg/sosreport/sosreport.jsx:322
+#, fuzzy
+#| msgid "Last login"
+msgid "Use verbose logging"
+msgstr "Ultimo accesso"
+
+# spazio su disco, quindi maschile
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Usato"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"Utile per i mount che sono opzionali o che necessitano di interazione (come "
+"le passphrase)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Utente"
+
+#: pkg/users/account-create-dialog.js:104
+#, fuzzy
+#| msgid "User"
+msgid "User ID"
+msgstr "Utente"
+
+#: pkg/users/account-create-dialog.js:191
+#, fuzzy
+#| msgid "This volume is already used by another VM."
+msgid "User ID is already used by another user"
+msgstr "Questo volume è già utilizzato da un'altra VM."
+
+#: pkg/users/account-create-dialog.js:181
+#, fuzzy
+#| msgid "MTU must be a positive number"
+msgid "User ID must be a positive integer"
+msgstr "MTU deve essere un numero positivo"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "L'ID utente non deve essere superiore a $0"
+
+#: pkg/users/account-create-dialog.js:184
+#, fuzzy
+#| msgid "Name cannot be longer than $0 bytes"
+msgid "User ID must not be lower than $0"
+msgstr "Il nome non può essere più lungo di $0 byte"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Nome utente"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Il nome utente non può essere vuoto"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Nome utente"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "encryption"
+msgid "Using LUKS encryption"
+msgstr "crittografia"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "Edit Tang keyserver"
+msgid "Using Tang server"
+msgstr "Modifica Tang keyserver"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "I dispositivi di supporto VDO non possono essere resi più piccoli"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "Dispositivo VDO $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "Volume del filesystem VDO (compressione/deduplicazione)"
+
+# translation auto-copied from project Anaconda, version master, document
+# anaconda, author Gregorio
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN ID"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Convalida dell'indirizzo"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Convalida del token di autenticazione"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Rivenditore"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Verificato"
+
+#: pkg/shell/hosts_dialog.jsx:489
+#, fuzzy
+#| msgid "Fingerprint"
+msgid "Verify fingerprint"
+msgstr "Impronta digitale"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Verifica la chiave"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Verifica"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Versione"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Cancellazione molto sicura di $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+#, fuzzy
+#| msgid "All logs"
+msgid "View all CPUs"
+msgstr "Tutti i log"
+
+#: pkg/metrics/metrics.jsx:803
+#, fuzzy
+#| msgid "All logs"
+msgid "View all disks"
+msgstr "Tutti i log"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+#, fuzzy
+#| msgid "All logs"
+msgid "View all logs"
+msgstr "Tutti i log"
+
+#: pkg/systemd/service-details.jsx:549
+#, fuzzy
+#| msgid "Filter services"
+msgid "View all services"
+msgstr "Filtra Servizi"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Visualizza script di automazione"
+
+#: pkg/metrics/metrics.jsx:1147
+#, fuzzy
+#| msgid "All logs"
+msgid "View detailed logs"
+msgstr "Tutti i log"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Visualizza i dettagli hardware"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+#, fuzzy
+#| msgid "View details and history"
+msgid "View login history"
+msgstr "Visualizza dettagli e cronologia"
+
+#: pkg/storaged/stratis/pool.jsx:351
+#, fuzzy
+#| msgid "All logs"
+msgid "View logs"
+msgstr "Tutti i log"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+#, fuzzy
+#| msgid "View details and history"
+msgid "View metrics and history"
+msgstr "Visualizza dettagli e cronologia"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Visualizza il throughput per disco"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Visualizza il sito web del progetto"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Visualizza rapporto"
+
+#: pkg/packagekit/updates.jsx:619
+#, fuzzy
+#| msgid "All logs"
+msgid "View update log"
+msgstr "Tutti i log"
+
+#: pkg/systemd/hwinfo.jsx:299
+#, fuzzy
+#| msgid "You now have administrative access."
+msgid "Viewing memory information requires administrative access."
+msgstr "Ora hai accesso amministrativo."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+#, fuzzy
+#| msgid "Firewall"
+msgid "Visit firewall"
+msgstr "Firewall"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Gruppo di volumi"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Volume group is missing physical volumes"
+msgstr "Rimozione del volume fisico da $target"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "La dimensione del volume è $0. La dimensione del contenuto è $1."
+
+# translation auto-copied from project evolution-data-server, version el6,
+# document evolution-data-server-2.32
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "In attesa"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Aspettando i dettagli..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "In attesa di input…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"In attesa che altri programmi finiscano di usare il gestore di pacchetti..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "In attesa che finiscano le altre operazioni di gestione del software"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "In attesa di avvio…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Ricercato da"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Vuole"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Avvertenza e superiore"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+#, fuzzy
+#| msgid "Set weak password"
+msgid "Weak password"
+msgstr "Imposta una password debole"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Web Console"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Web Console per server Linux"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+#, fuzzy
+#| msgid "Web Console for Linux servers"
+msgid "Web Console will restart"
+msgstr "Web Console per server Linux"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "La Web Console è in esecuzione in modalità di accesso limitato."
+
+#: pkg/shell/shell-modals.jsx:66
+#, fuzzy
+#| msgid "Web Console"
+msgid "Web console logo"
+msgstr "Web Console"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Mercoledì"
+
+#: pkg/systemd/timer-dialog.jsx:246
+#, fuzzy
+#| msgid "Weeks"
+msgid "Weekly"
+msgstr "Settimane"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Settimane"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Quando"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "Se vuoti, connetti con l'utente corrente"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Quando la console Web viene riavviata, non vedrai più le informazioni sullo "
+"stato di avanzamento. Tuttavia, il processo di aggiornamento continuerà in "
+"background. Riconnettiti per continuare a monitorare il processo di "
+"aggiornamento."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Bianco"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:124
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "World wide name"
+msgid "World wide name"
+msgstr "Nome globale"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Scrivi"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Write-mostly"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "In scrittura"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Nome utente o password errata"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Annuale"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Sì"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Collegamento a $0 per la prima volta"
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Non sei autorizzato a modificare il firewall."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Non hai il permesso di visualizzare le chiavi pubbliche autorizzate per "
+"questo account."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Sei stato disconnesso per inattività."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Puoi provare a caricare voci precedenti."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+"Potrebbe essere necessario modificare la password della chiave per l'accesso "
+"automatico."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Devi aspettare più a lungo per cambiare la tua password"
+
+#: pkg/metrics/metrics.jsx:1805
+#, fuzzy
+#| msgid "You need to relogin to be able to see metrics"
+msgid "You need to relogin to be able to see metrics history"
+msgstr "Occorre un nuovo accesso per visualizzare le metriche"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Ora hai accesso amministrativo."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Verrai disconnesso tra $0 secondi."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Il tuo account"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"Il tuo browser non consente l'incolla dal menu contestuale. Puoi usare "
+"Maiusc+Ins."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "Il tuo browser ricorderà il tuo livello di accesso tra le sessioni."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Il server chiuderà presto la connessione. È possibile riconnettersi dopo che "
+"è stato riavviato."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "La tua sessione e' terminata."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "La sessione è scaduta. Effettua di nuovo il login."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+#, fuzzy
+#| msgid "$0 Zone"
+msgid "Zone"
+msgstr "Zona $0"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[dati binari]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[nessun dato]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "accesso"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "attivo"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "add-on"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "componente aggiuntivo"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+#, fuzzy
+#| msgid "Isolated network"
+msgid "after network"
+msgstr "Rete isolata"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "applicazioni"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "asset tag"
+
+# translation auto-copied from project Customer Portal Translations, version
+# PortalCaseManagementStrings, document PortalCaseManagementStrings, author
+# fvalen
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "a"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "media: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+#, fuzzy
+#| msgid "Create VDO device"
+msgid "backing device for VDO device"
+msgstr "Creare un dispositivo VDO"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "bond"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "avvio"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "bridge"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "Altri dispositivi"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "Altri dispositivi"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs subvolume"
+msgstr "Volume di Archiviazione"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Volumi di Archiviazione"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "Volume di Archiviazione"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "bug fix"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "byte"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "Cache"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+#, fuzzy
+#| msgid "Change"
+msgid "change"
+msgstr "Cambia"
+
+#: pkg/metrics/metrics.jsx:654
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "cockpit-podman is not installed"
+msgstr "Cockpit non è installato"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "comando"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "console"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "coredump"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "cpu"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "crash"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "$0 dati"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "data"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Disattiva"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "debug"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+#, fuzzy
+#| msgid "Delete"
+msgid "delete"
+msgstr "Cancella"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "disabilita"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "disco"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "dischi"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "dominio"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "disco"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "modifica"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "abilita"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted"
+msgid "encrypted"
+msgstr "Crittografato"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "crittografia"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "miglioramento"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "errore"
+
+#: pkg/packagekit/autoupdates.jsx:354
+#, fuzzy
+#| msgid "every day"
+msgid "every Friday"
+msgstr "quotidianamente"
+
+#: pkg/packagekit/autoupdates.jsx:350
+#, fuzzy
+#| msgid "every day"
+msgid "every Monday"
+msgstr "quotidianamente"
+
+#: pkg/packagekit/autoupdates.jsx:355
+#, fuzzy
+#| msgid "every day"
+msgid "every Saturday"
+msgstr "quotidianamente"
+
+#: pkg/packagekit/autoupdates.jsx:356
+#, fuzzy
+#| msgid "every day"
+msgid "every Sunday"
+msgstr "quotidianamente"
+
+#: pkg/packagekit/autoupdates.jsx:353
+#, fuzzy
+#| msgid "every day"
+msgid "every Thursday"
+msgstr "quotidianamente"
+
+#: pkg/packagekit/autoupdates.jsx:351
+#, fuzzy
+#| msgid "every day"
+msgid "every Tuesday"
+msgstr "quotidianamente"
+
+#: pkg/packagekit/autoupdates.jsx:352
+#, fuzzy
+#| msgid "Wednesday"
+msgid "every Wednesday"
+msgstr "Mercoledì"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "quotidianamente"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "estensione"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "impossibile elencare le chiavi host ssh: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "file system"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "firewall"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "per kernel attuali e futuri"
+
+#: pkg/packagekit/kpatch.jsx:270
+#, fuzzy
+#| msgid "Mount read only"
+msgid "for current kernel only"
+msgstr "Monta in sola lettura"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "format"
+msgstr "format"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+#, fuzzy
+#| msgctxt "<date> from <host>"
+#| msgid "$0 from $1"
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "$0 da $1"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+#, fuzzy
+#| msgctxt "<date> on <terminal>"
+#| msgid "$0 on $1"
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "$0 su $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "grafici"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "Crescere"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "hardware"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "cronologia"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "host"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "Target iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "target iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "Aggiungi portale iSCSI"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+#, fuzzy
+#| msgid "On failure"
+msgid "ignore failure"
+msgstr "In caso di guasto"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "nella maggior parte dei browser"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys, author fvalen
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+#, fuzzy
+#| msgid "Content"
+msgid "inconsistent"
+msgstr "Contenuto"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#, fuzzy
+#| msgid "Initializing..."
+msgid "initialize"
+msgstr "Inizializzazione..."
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "installa"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "interfaccia"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "non valido: sono stati definiti più obiettivi multipli"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "registro"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "pagina man di journalctl"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "stato kdump"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "chiavi"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "accesso"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "macchina"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "maschera"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "max: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "Member of RAID device"
+msgid "member of MDRAID device"
+msgstr "Membro del dispositivo RAID"
+
+#: pkg/storaged/dialog.jsx:1113
+#, fuzzy
+#| msgid "Reset Storage Pool"
+msgid "member of Stratis pool"
+msgstr "Reimpostare la piscina di stoccaggio"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "memoria"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "metriche"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "mitigazione"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+#, fuzzy
+#| msgid "More details"
+msgid "more details"
+msgstr "Maggiori dettagli"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "mount"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "rete"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+#, fuzzy
+#| msgid "Mount at boot"
+msgid "never mount at boot"
+msgstr "Montaggio all'avvio"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+#, fuzzy
+#| msgid "ssh server is empty"
+msgid "nfs export is empty"
+msgstr "il server ssh è vuoto"
+
+#: pkg/kdump/kdump-client.js:125
+#, fuzzy
+#| msgid "ssh server is empty"
+msgid "nfs server is empty"
+msgstr "il server ssh è vuoto"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "Il server nfs non è un IPv6 valido"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "nice"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "nessuno"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "di $0 CPU"
+msgstr[1] "di $0 CPU"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+#, fuzzy
+#| msgctxt "<date> on <terminal>"
+#| msgid "$0 on $1"
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "$0 su $1"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "sistema operativo"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "os"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "pacchetto"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "partizione"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "password"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+#, fuzzy
+#| msgid "password"
+msgid "password quality"
+msgstr "password"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "patch"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "percorso"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+#| msgid "Performance Metrics"
+msgid "performance"
+msgstr "Metriche di performance"
+
+#: pkg/storaged/dialog.jsx:1110
+#, fuzzy
+#| msgid "Create volume group"
+msgid "physical volume of LVM2 volume group"
+msgstr "Crea gruppo di volumi"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "plugin"
+
+#: pkg/metrics/metrics.jsx:1833
+#, fuzzy
+#| msgid "$0 service has failed"
+#| msgid_plural "$0 services have failed"
+msgid "pmlogger.service has failed"
+msgstr "$0 servizio ha fallito"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service non riesce a raccogliere i dati"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service non è in esecuzione"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "pod"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "porta"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "alimentazione"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "sola lettura"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "raccomandato"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "rimuovere da LVM2"
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "remove from RAID"
+msgid "remove from MDRAID"
+msgstr "rimuovere dal RAID"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "rimuovere da LVM2"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "riavvia"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "ruoli"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "$0 in esecuzione"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "sicurezza"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "seriale"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "servizio"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "shell"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "mostra meno"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "mostra di più"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "riduci"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "shut"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "socket"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "soluzione"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "dettagli della soluzione"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+#, fuzzy
+#| msgid "Reporting failed"
+msgid "sos report failed"
+msgstr "Segnalazione fallita"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "la chiave ssh non è un percorso"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "il server ssh è vuoto"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "ferma"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+#, fuzzy
+#| msgid "On failure"
+msgid "stop boot on failure"
+msgstr "In caso di guasto"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+#, fuzzy
+#| msgid "Stopped"
+msgid "stopped"
+msgstr "Fermato"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "destinazione"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "team"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "ora"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "timer"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "unità"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "sconosciuto"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "destinazione sconosciuta"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "unmask"
+msgstr "smaschera"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "smonta"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "spazio non partizionato su $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "utente"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "nome utente"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#, fuzzy
+#| msgid "No description"
+msgid "using key description $0"
+msgstr "Nessuna descrizione"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "versione"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "volume"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "avviso"
+
+#: pkg/networkmanager/wireguard.jsx:84
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "wireguard-tools package is not installed"
+msgstr "PackageKit non è installato"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "si"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "zona"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Servizio Kdump non installato. Assicurarsi che il pacchetto kexec-tools "
+#~ "sia installato."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Nessuna memoria riservata. Aggiungere un'opzione crashkernel alla riga di "
+#~ "comando del kernel (p.e. in /etc/default/grub) per riservare memoria "
+#~ "all'avvio. Esempio: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Il servizio è in funzione"
+
+#~ msgid "Service is starting"
+#~ msgstr "Il servizio è in fase di avvio"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Il servizio è interrotto"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Il servizio si sta fermando"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr ""
+#~ "Questo testerà la configurazione di kdump mandando in crash il kernel."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "crashkernel non è configurato nella riga di comando del kernel"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (crittografato)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 Pool Stratis"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 Cache"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 di livello sconosciuto"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 libero"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr ""
+#~ "È necessario aggiungere un disco di riserva prima di poterlo rimuovere."
+
+#~ msgid "Backing device"
+#~ msgstr "Dispositivo di supporto"
+
+#~ msgid "Block"
+#~ msgstr "Blocco"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Capacità"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys, author fvalen
+#~ msgid "Content"
+#~ msgstr "Contenuto"
+
+#~ msgid "Create devices"
+#~ msgstr "Crea dispositivi"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Dispositivo"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "File dispositivo"
+
+#~ msgid "Devices"
+#~ msgstr "Dispositivi"
+
+#~ msgid "Drives"
+#~ msgstr "Drive"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "I volumi criptati devono essere sbloccati prima di poter essere "
+#~ "ridimensionati."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Filesystem (criptato)"
+
+#~ msgid "Filesystems"
+#~ msgstr "File system"
+
+# spazio su disco, quindi maschile
+#~ msgid "Free"
+#~ msgstr "Libero"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Recuperare spazio in questo gruppo: ridurre o eliminare volumi logici "
+#~ "oppure aggiungere un altro volume fisico."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Volume inattivo"
+
+#, fuzzy
+#~| msgid "Keyserver"
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Key server"
+
+#~ msgid "LVM2 member"
+#~ msgstr "Membro LVM2"
+
+#~ msgid "Locked devices"
+#~ msgstr "Dispositivi bloccati"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Dati crittografati bloccati"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Modello"
+
+#~ msgid "NFS mounts"
+#~ msgstr "Supporti NFS"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "Supporto NFS non installato"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Nessun supporto NFS configurato"
+
+#~ msgid "No devices"
+#~ msgstr "Nessun dispositivo"
+
+#~ msgid "No drives attached"
+#~ msgstr "Nessuna unità collegata"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Non sono stati impostati target iSCSI"
+
+#, fuzzy
+#~| msgid "Block device for filesystems"
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Dispositivo di blocco per i filesystem"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Altri dati"
+
+#, fuzzy
+#~| msgid "$0 block device"
+#~ msgid "Partitioned block device"
+#~ msgstr "Partiziona Dispositivo a blocchi"
+
+# translation auto-copied from project Anaconda, version master, document
+# anaconda
+#, fuzzy
+#~| msgid "Passphrase"
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Frase di accesso"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "I volumi fisici non possono essere ridimensionati qui."
+
+#~ msgid "Pool"
+#~ msgstr "Pool"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Pool per volumi thin"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "Livello RAID"
+
+#~ msgid "RAID member"
+#~ msgstr "Membro RAID"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Unità rimovibile"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Mostra $0 dispositivo"
+#~ msgstr[1] "Mostra tutti i $0 dispositivi"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Mostra $0 disco"
+#~ msgstr[1] "Mostra tutti i $0 dischi"
+
+#~ msgid "Show all"
+#~ msgstr "Mostra tutto"
+
+#~ msgid "Source"
+#~ msgstr "Sorgente"
+
+#~ msgid "Start pool"
+#~ msgstr "Avvia pool"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Avvia il pool per vedere i filesystem."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Stato"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Stratis pool arrestato"
+
+#~ msgid "Stratis member"
+#~ msgstr "Membri Stratis"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Pool Stratis $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "Il supporto è installato."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "Il dispositivo RAID deve essere in funzione per poter aggiungere dischi "
+#~ "di riserva."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "L'ultimo disco di un dispositivo RAID non può essere rimosso."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr ""
+#~ "L'ultimo volume fisico di un gruppo di volumi non può essere rimosso."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Non c'è abbastanza spazio libero altrove per rimuovere questo volume "
+#~ "fisico. È almeno necessario $0 spazio libero."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr ""
+#~ "Questo disco non può essere rimosso mentre il dispositivo è in fase di "
+#~ "ripristino."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr ""
+#~ "Questo volume deve essere attivato prima di poter essere ridimensionato."
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys, author fvalen
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Dati non riconosciuti"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#, fuzzy
+#~| msgid "Usage"
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Utilizzo"
+
+#, fuzzy
+#~| msgid "Used by"
+#~ msgid "Used for"
+#~ msgstr "Usato da"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "Supporto VDO"
+
+#~ msgid "VDO device"
+#~ msgstr "Dispositivo VDO"
+
+# nome per a11y
+#~ msgid "Volume"
+#~ msgstr "Volume"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automatico (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Accetta la chiave e connetti"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "I volumi criptati non possono essere ridimensionati qui."
+
+#, fuzzy
+#~| msgid "Tang keyserver"
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Keyserver Tang"
+
+#, fuzzy
+#~| msgid "New passphrase"
+#~ msgid "Use a passphrase"
+#~ msgstr "Nuova frase di accesso"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitmap"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Questo pool non può essere sbloccato qui perché la descrizione della "
+#~ "chiave non è nel formato previsto."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Attiva/disattiva bitmap"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Assicurarsi che l'hash della chiave dal server Tang corrisponda a uno dei "
+#~ "seguenti:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Controllare manualmente con SSH: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Il numero e il tipo di porta non corrispondono"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Pool Stratis crittografato bloccato"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Errore durante l'eliminazione dell'avviso: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Impossibile ottenere l'avviso: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0byte di dati binari]"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#~ msgid "Disk I/O spike"
+#~ msgstr "Picco I/O disco"
+
+#~ msgid "Event"
+#~ msgstr "Evento"
+
+#~ msgid "Event logs"
+#~ msgstr "Log eventi"
+
+#~ msgid "Load spike"
+#~ msgstr "Picco di carico"
+
+#~ msgid "Memory spike"
+#~ msgstr "Picco di memoria"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Picco di I/O rete"
+
+#~ msgid "ssh key"
+#~ msgstr "chiave ssh"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Se questa opzione è selezionata, il filesystem non verrà montato durante "
+#~ "il prossimo avvio anche se è stato montato precedentemente. Ciò è utile "
+#~ "se il montaggio durante l'avvio non è possibile, ad esempio quando è "
+#~ "necessaria una passphrase per sbloccare il filesystem ma l'avvio non è "
+#~ "presidiato."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Non montare mai all'avvio"
+
+#~ msgid "Listing unit files"
+#~ msgstr "Elenco dei file di unità"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "Elenco dei file dell'unità non riuscito: $0"
+
+#, fuzzy
+#~| msgid "Not found"
+#~ msgid "Unit not found"
+#~ msgstr "Non trovato"
+
+#~ msgid "Validating key"
+#~ msgstr "Chiave di convalida"
+
+#, fuzzy
+#~| msgid "Delete $0 volume"
+#~| msgid_plural "Delete $0 volumes"
+#~ msgid "Delete $0 group"
+#~ msgstr "Elimina il volume $0"
+
+#~ msgid "Container administrator"
+#~ msgstr "Amministratore del container"
+
+#~ msgid "Image builder"
+#~ msgstr "Costruttore di immagini"
+
+#~ msgid "Roles"
+#~ msgstr "Ruoli"
+
+#~ msgid "Server administrator"
+#~ msgstr "Amministratore del server"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Gruppo Unix: $0"
+
+#, fuzzy
+#~| msgid "Copy to clipboard"
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Copia negli appunti"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Rapporti Diagnostici"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Kernel Dump"
+
+#~ msgid "Software Updates"
+#~ msgstr "Aggiornamenti Software"
+
+#~ msgid "Update log"
+#~ msgstr "Log aggiornamenti"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "la destinazione di nfs dump non è formattata come server:path"
+
+#~ msgid "$0 Zone"
+#~ msgstr "Zona $0"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Rapporti diagnostici"
+
+#~ msgid "Done!"
+#~ msgstr "Fatto!"
+
+#~ msgid "Download report"
+#~ msgstr "Download report"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Non è stato creato alcun archivio."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Conferma la cancellazione di $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "L'archivio generato contiene dati considerati sensibili e il suo "
+#~ "contenuto deve essere rivisto dall'organizzazione di origine prima di "
+#~ "essere passato a terzi."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Questo strumento raccoglierà informazioni di configurazione del sistema e "
+#~ "informazioni diagnostiche da questo sistema per l'uso con la diagnosi dei "
+#~ "problemi con il sistema."
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Questo pacchetto non è compatibile con questa versione di Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Questo pacchetto richiede la versione Cockpit %s o successiva"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Metriche di performance"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "sola lettura"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB totali"
+
+#~ msgid "Apply"
+#~ msgstr "Applica"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Non autorizzato a rimuovere la zona $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Fare di nuovo clic su $0 per utilizzare comunque la password."
+
+#~ msgid "Access"
+#~ msgstr "Accesso"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "IL DISCO SI STA GUASTANDO"
+
+#, fuzzy
+#~| msgid "Logical size"
+#~ msgid "Logical Size"
+#~ msgstr "Dimensione logica"
+
+#~ msgid "Security updates "
+#~ msgstr "Aggiornamenti di sicurezza "
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#~ msgid "Updates "
+#~ msgstr "Aggiornamenti "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Opzionale)"
+
+#~ msgid "Active since"
+#~ msgstr "Attivo dal"
+
+#, fuzzy
+#~| msgid "Current allocation"
+#~ msgid "Affected locations"
+#~ msgstr "Posizioni affette"
+
+#~ msgid "Process"
+#~ msgstr "Processi"
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Remove and delete"
+#~ msgstr "Ferma ed elimina"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and format"
+#~ msgstr "Formato di accesso"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and grow"
+#~ msgstr "Formato di accesso"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and initialize"
+#~ msgstr "Formato di accesso"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and shrink"
+#~ msgstr "Formato di accesso"
+
+#, fuzzy
+#~| msgid "Remove device"
+#~ msgid "Remove and stop device"
+#~ msgstr "Rimuovere il dispositivo"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Il file system è in uso dalle sessioni di login e dai servizi di sistema. "
+#~ "Procedendo si fermeranno."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Il file system è in uso per le sessioni di login. Procedendo si "
+#~ "fermeranno."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Il file system è in uso dai servizi di sistema. Procedendo si fermeranno."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Questo dispositivo ha file system che sono attualmente in uso. Procedendo "
+#~ "si smontano tutti i file system su di esso."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Questo dispositivo è attualmente utilizzato per gruppi di volumi."
+
+#, fuzzy
+#~| msgid ""
+#~| "This device is currently used for volume groups. Proceeding will remove "
+#~| "it from its volume groups."
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Questo dispositivo è attualmente utilizzato per i gruppi di volumi. "
+#~ "Procedendo, lo si rimuoverà dai suoi gruppi di volumi."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Questo dispositivo è attualmente utilizzato per dispositivi RAID."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Questo dispositivo è attualmente utilizzato per i dispositivi RAID. "
+#~ "Procedendo, lo si rimuoverà dai suoi dispositivi RAID."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Questo dispositivo è attualmente utilizzato per gruppi di volumi."
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Unmount and delete"
+#~ msgstr "Ferma ed elimina"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and format"
+#~ msgstr "Smonta adesso"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and grow"
+#~ msgstr "Smonta adesso"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and initialize"
+#~ msgstr "Smonta adesso"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and shrink"
+#~ msgstr "Smonta adesso"
+
+#, fuzzy
+#~| msgid "On a mounted device"
+#~ msgid "Unmount and stop device"
+#~ msgstr "Su un dispositivo montato"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 di $1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "Bloccato"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Volume fisico di $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Membro del dispositivo RAID $0"
+
+#~ msgid "VDO backing"
+#~ msgstr "Supporto VDO"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Crea pool di archiviazione"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "Opzioni di montaggio"
+
+#, fuzzy
+#~| msgid "Reset Storage Pool"
+#~ msgid "Stratis Pool"
+#~ msgstr "Reimpostare la piscina di stoccaggio"
+
+#~ msgid "A disk is needed."
+#~ msgstr "È necessario un disco."
+
+#~ msgid "Disk"
+#~ msgstr "Disco"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Solo per applicazioni legacy. Riduce le prestazioni."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Installa il supporto VDO"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Usa l'emulazione a 512 byte"
+
+#~ msgid "System services"
+#~ msgstr "Servizi di sistema"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Crea un rapporto diagnostico "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr ""
+#~ "Il collegamento simultaneo di più di {{ limit }} macchine non è "
+#~ "supportato."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "SSO basato su Kerberos"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Accedi a {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Le nuove password della chiave non corrispondono"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "Per verificare un'impronta digitale, esegui su {{host}} mentre sei "
+#~ "fisicamente di fronte alla macchina o attraverso una rete fidata:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Impossibile contattare {{#strong}} {{host}} {{/strong}}."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Impossibile accedere a {{#strong}}{{host}}{{/strong}}. Per ulteriori "
+#~ "opzioni di autenticazione e supporto per la risoluzione dei problemi, "
+#~ "aggiorna cockpit-ws a una versione più recente."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Impossibile accedere a {{#strong}}{{host}}{{/strong}}. Per connetterti a "
+#~ "questo host dovrai abilitare uno dei seguenti metodi di autenticazione "
+#~ "nella configurazione sshd su {{#strong}}{{host}}{{/strong}}:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "Collegamento a {{host}} per la prima volta"
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "chiave {{host}} modificata"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Cancella la configurazione del punto di mount"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "La creazione di questo bond interromperà la connessione al server e "
+#~ "renderà l'interfaccia utente di amministrazione non disponibile."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "La creazione di questo bridge interromperà la connessione al server e "
+#~ "renderà l'interfaccia utente di amministrazione non disponibile."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "La creazione di questo team interromperà la connessione al server e "
+#~ "renderà l'interfaccia utente di amministrazione non disponibile."
+
+#~ msgid "IP settings"
+#~ msgstr "Impostazioni IP"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "La disattivazione di <b>$0</b> interromperà la connessione al server e "
+#~ "renderà l'interfaccia utente di amministrazione non disponibile."
+
+#~ msgid "Administrator password"
+#~ msgstr "Password amministratore"
+
+#~ msgid "Computer OU"
+#~ msgstr "OU del computer"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr ""
+#~ "L'eliminazione di un dispositivo RAID cancellerà tutti i dati in esso "
+#~ "contenuti."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr ""
+#~ "L'eliminazione di un gruppo di volumi cancellerà tutti i dati in esso "
+#~ "contenuti."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Non sovrascrivere i dati esistenti"
+
+#~ msgid "Erase"
+#~ msgstr "Elimina"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Formatta disco $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr ""
+#~ "La formattazione di un dispositivo di archiviazione cancella tutti i dati "
+#~ "in esso contenuti."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Il nome dell'host non dovrebbe essere cambiato in un dominio"
+
+#~ msgid "More"
+#~ msgstr "Di più"
+
+#~ msgid "Not joined"
+#~ msgstr "Non unito"
+
+#~ msgid "One time password"
+#~ msgstr "Password univoca"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 da $1 su $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Le ore devono essere un numero compreso tra 0-23"
+
+#~ msgid "Last login:"
+#~ msgstr "Ultimo accesso:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "I minuti devono essere un numero compreso tra 0 e 59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "C'è $0 tentativo di accesso fallito dall'ultimo accesso effettuato."
+#~ msgstr[1] ""
+#~ "Ci sono $0 tentativi di accesso falliti dall'ultimo accesso effettuato."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "es: \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 Zone Attive"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 occorrenza"
+#~ msgstr[1] "$0 occorrenze"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Concesso in licenza:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Sblocca all'avvio"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Sblocca solo lettura"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Cerca nei log con una combinazione di termini:"
+
+#~ msgid "Show filters"
+#~ msgstr "Mostra filtri"
+
+#~ msgid "Text"
+#~ msgstr "Testo"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "qualsiasi stringa in formato libero come espressione regolare"
+
+#~ msgid "e.g."
+#~ msgstr "es"
+
+#~ msgid "log fields"
+#~ msgstr "campi di log"
+
+#~ msgid "qualifiers"
+#~ msgstr "qualificatori"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "Attiva/disattiva le impostazioni di sessione"
+
+#~ msgid "(none)"
+#~ msgstr "(nessuno)"
+
+#~ msgid "Create timers"
+#~ msgstr "Crea timer"
+
+#~ msgid "Recommended default"
+#~ msgstr "Impostazione predefinita consigliata"
+
+#~ msgid "Run"
+#~ msgstr "Esegui"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select unit state"
+#~ msgstr "Tipo di console"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Abilita il salvataggio delle metriche"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Abilita il salvataggio delle metriche"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Forza rimozione della frase di accesso in $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Se il tasto tang-show-key non è disponibile, eseguire quanto segue:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "E se tang-show-keys non è disponibile?"
+
+#~ msgid "key slot $0"
+#~ msgstr "slot $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Qualcosa non ha funzionato"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "Non ha funzionato, per favore riprova"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "Non è possibile ottenere l'accesso amministrativo."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Gli aggiornamenti automatici non sono impostati"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "Configurazione CPU $0"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "Rete $0"
+#~ msgstr[1] "Rete $0"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 è disponibile per la maggior parte dei sistemi operativi. Per "
+#~ "installarlo, cercalo in GNOME Software o esegui quanto segue:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 regolazione della memoria"
+
+#~ msgid "$0 network"
+#~ msgstr "Rete $0"
+
+# ctx::sourcefile::/rhn/systems/details/virtualization/VirtualGuestsList.do
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPU"
+#~ msgstr[1] "$0 vCPU"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 dettagli vCPU"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 impostazioni dell'interfaccia di rete virtuale"
+
+#, fuzzy
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "Attiva la storage pool per amministrare i volumi"
+
+#~ msgid "Add network interface"
+#~ msgstr "Aggiungi interfaccia di rete"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Aggiungi interfaccia di rete virtuale"
+
+#~ msgid "Additional"
+#~ msgstr "Aggiuntivo"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "L'indirizzo non è nella sottorete"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr ""
+#~ "Dopo aver eliminato lo snapshot, tutto il contenuto acquisito andrà perso."
+
+#~ msgid "Always attach"
+#~ msgstr "Allega sempre"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Il collegamento renderà questo disco condivisibile per ogni VM che lo "
+#~ "utilizza."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Avvia automaticamente libvirt al boot"
+
+#~ msgid "Autostart"
+#~ msgstr "Avvio automatico"
+
+#~ msgid "Boot order"
+#~ msgstr "Ordine di Avvio"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Impossibile salvare le impostazioni dell'ordine di avvio"
+
+#~ msgid "Bus"
+#~ msgstr "Bus"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "Disco CD/DVD"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "Impossibile salvare la configurazione della CPU"
+
+#~ msgid "CPU type"
+#~ msgstr "Tipo CPU"
+
+#~ msgid "Change firmware"
+#~ msgstr "Cambia firmware"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Le modifiche avranno effetto dopo lo spegnimento della VM"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Scegli un sistema operativo"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "Cliccare \"Avvia visualizzatore remoto\" scaricherà un file .vv e avvierà "
+#~ "$0."
+
+#~ msgid "Clone"
+#~ msgstr "Clona"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Impossibile caricare l'immagine"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Conferma questa azione"
+
+#~ msgid "Connect"
+#~ msgstr "Connetti"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr ""
+#~ "Connetti con qualsiasi applicazione di visualizzazione per i protocolli "
+#~ "seguenti"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Connessione al servizio di virtualizzazione"
+
+# ctx::sourcefile::Navigation Menu
+#~ msgid "Connection"
+#~ msgstr "Connessione"
+
+#~ msgid "Console"
+#~ msgstr "Console"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Core per socket"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Impossibile ripristinare lo snapshot"
+
+#, fuzzy
+#~| msgid "crashed"
+#~ msgid "Crashed"
+#~ msgstr "Interrotto"
+
+#~ msgid "Create VM"
+#~ msgstr "Crea VM"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Crea un clone della VM basata su $0"
+
+#~ msgid "Create new"
+#~ msgstr "Crea nuovo"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Crea nuova macchina virtuale"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Crea rete virtuale"
+
+#~ msgid "Creating VM"
+#~ msgstr "Creazione VM"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "Creazione dell'installazione della VM"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Creazione della VM $0 non riuscita"
+
+#~ msgid "Creation time"
+#~ msgstr "Data di creazione"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Allocazione corrente"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Firmware personalizzato: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "Intervallo DHCP"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Elimina i file di archiviazione associati:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Elimina il pool di archiviazione $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Elimina i volumi all'interno di questo pool"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "L'eliminazione di un pool di archiviazione inattivo annullerà soltanto la "
+#~ "definizione del pool. Il suo contenuto non verrà eliminato."
+
+#~ msgid "Desktop viewer"
+#~ msgstr "Visualizzatore desktop"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Scollegare i dischi utilizzando questo pool da qualsiasi VM prima di "
+#~ "tentare l'eliminazione."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Disconnesso dalla console seriale. Fai clic sul pulsante Connetti."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Impossibile scollegare il disco $0 dalla VM $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Il disco non è stato collegato"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Impossibile creare il disco"
+
+#~ msgid "Disk image file"
+#~ msgstr "File immagine disco"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Impossibile salvare le impostazioni del disco"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "Solo snapshot del disco"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "Il dominio è andato in crash"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "Il dominio è bloccato sulla risorsa"
+
+#~ msgid "Download an OS"
+#~ msgstr "Scarica un OS"
+
+#, fuzzy
+#~| msgid "dying"
+#~ msgid "Dying"
+#~ msgstr "In chiusura"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Macchina emulata"
+
+#~ msgid "End"
+#~ msgstr "Fine"
+
+#~ msgid "End should not be empty"
+#~ msgstr "La fine non può essere vuota"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Immagine del disco esistente sul file system dell'host"
+
+#~ msgid "Expand"
+#~ msgstr "Espandi"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Impossibile cambiare il firmware"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr ""
+#~ "Impossibile recuperare gli indirizzi IP delle interfacce presenti in $0"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "Impossibile inviare la combinazione Ctrl+Alt+$0 alla VM $1"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr ""
+#~ "Un numero massimo inferiore di CPU virtuali dovrebbe essere abilitato."
+
+#~ msgid "File"
+#~ msgstr "File"
+
+#~ msgid "Filter by name"
+#~ msgstr "Filtra per nome"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#~ msgid "Firmware"
+#~ msgstr "Firmware"
+
+#~ msgid "Force shut down"
+#~ msgstr "Forza spegnimento"
+
+#~ msgid "Forward mode"
+#~ msgstr "Modalità di reindirizzamento"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Modalità di inoltro"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Genera automaticamente"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "Vai alla lista delle VM"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Nascondi opzioni aggiuntive"
+
+#~ msgid "Host device"
+#~ msgstr "Dispositivo host"
+
+#~ msgid "Host name"
+#~ msgstr "Nome dell'host"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "L'host non deve essere vuoto"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Dettagli Hypervisor"
+
+#~ msgid "IP configuration"
+#~ msgstr "Configurazione IP"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 e IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "Rete IPv4"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "La rete IPv4 non può essere vuota"
+
+#~ msgid "IPv4 only"
+#~ msgstr "Solo IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "Indirizzo IPv6"
+
+#~ msgid "IPv6 network"
+#~ msgstr "Rete IPv6"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "La rete IPv6 non può essere vuota"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Solo IPv6"
+
+#~ msgid "Idle"
+#~ msgstr "Inattivo"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Avvia immediatamente la VM"
+
+#~ msgid "Import"
+#~ msgstr "Importa"
+
+#~ msgid "Import VM"
+#~ msgstr "Importa VM"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "Importa una macchina virtuale"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "Nella maggior parte delle configurazioni, macvtap non funziona per le "
+#~ "comunicazioni di rete da host a guest."
+
+#~ msgid "Initiator"
+#~ msgstr "Iniziatore"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "L'iniziatore IQN non può essere vuoto"
+
+#~ msgid "Installation source"
+#~ msgstr "Fonte di installazione"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "L'origine dell'installazione non può essere vuota"
+
+#~ msgid "Installation type"
+#~ msgstr "Tipo di installazione"
+
+#~ msgid "Interface type"
+#~ msgstr "Tipo di interfaccia"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Lunghezza maschera o prefisso IPv4 non valido"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Indirizzo IPv6 non valido"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Prefisso IPv6 non valido"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Nome file non valido"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "Avviare il visualizzatore remoto"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr "Lascia vuota la password se non si desidera creare un account root"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "Lasciare vuota la password se non si desidera creare un account utente"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr "Lascia vuota la password se non si desidera creare un account root"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt non ha rilevato alcuna immagine del firmware UEFI/OVMF installata "
+#~ "sull'host"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt o hypervisor non supporta UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "Caricamento delle risorse"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "La macchina deve essere spenta prima di cambiare il tipo di bus"
+
+#, fuzzy
+#~| msgid "Machine must be shut off before changing bus type"
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "La macchina deve essere spenta prima di cambiare il tipo di bus"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Gestione delle macchine virtuali"
+
+#~ msgid "Manual connection"
+#~ msgstr "Collegamento manuale"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Maschera o lunghezza prefisso"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "La maschera o la lunghezza del prefisso non può essere vuota"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Allocazione massima"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "Non è stato possibile salvare la memoria massima"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Numero massimo di CPU virtuali allocate per il sistema operativo guest"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Numero massimo di CPU virtuali allocate per il sistema operativo guest, "
+#~ "che deve essere compreso tra 1 e $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Unità di trasmissione massima (MTU)"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Non è stato possibile salvare la memoria"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "La memoria non può essere 0"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Tipo di modello"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT su $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "NIC $0 di VM $1 non è riuscito a cambiare stato"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Il nome contiene caratteri non validi"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Il nome non può essere vuoto"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Il nome non deve essere vuoto"
+
+#~ msgid "Name: "
+#~ msgstr "Nome: "
+
+#~ msgid "Netmask"
+#~ msgstr "Maschera di rete"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Impossibile attivare la rete $0"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Impossibile disattivare la rete $0"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Avvio da rete (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "File system di rete"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Impossibile salvare le impostazioni dell'interfaccia di rete"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "La selezione della rete non supporta PXE."
+
+#~ msgid "Networks"
+#~ msgstr "Reti"
+
+#~ msgid "New volume name"
+#~ msgstr "Nuovo nome del volume"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "Nessuna VM è in esecuzione o definita su questo host"
+
+#~ msgid "No connection available"
+#~ msgstr "Nessuna connessione disponibile"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Nessun disco definito per questa VM"
+
+#~ msgid "No network devices"
+#~ msgstr "Nessun dispositivo di rete"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Nessuna interfaccia di rete definita per questa macchina virtuale"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "Nessuna rete definita su questo host"
+
+#~ msgid "No networks available"
+#~ msgstr "Nessuna rete disponibile"
+
+#, fuzzy
+#~ msgid "No parent"
+#~ msgstr "Nessun padre"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Nessuno snapshot definito per questa VM"
+
+#~ msgid "No state"
+#~ msgstr "Nessuno stato"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "Nessun pool di archiviazione è definito su questo host"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Nessun pool di archiviazione disponibile"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr ""
+#~ "Nessun Volume di archiviazione definito per questo pool di archiviazione"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Nessuna rete virtuale"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "La rete non persistente non può essere eliminata. Cessa di esistere "
+#~ "quando viene disattivata."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "Il pool di archiviazione non persistente non può essere eliminato. Cessa "
+#~ "di esistere quando è disattivato."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Nessuno (Rete Isolata)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Uno o più volumi selezionati sono utilizzati dai domini. Scollegare prima "
+#~ "i dischi per consentire l'eliminazione del volume."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Modificabile solo quando il guest è spento"
+
+#~ msgid "Open"
+#~ msgstr "Apri"
+
+#~ msgid "Operating system"
+#~ msgstr "Sistema Operativo"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "Operazione in corso"
+
+#, fuzzy
+#~ msgid "Parent snapshot"
+#~ msgstr "Snapshot genitore"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Percorso sul file system dell'host"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Percorso del file ISO sul file system dell'host"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Percorso al file sul file system dell'host"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Percorso al file sul file system dell'host"
+
+#~ msgid "Paused"
+#~ msgstr "In pausa"
+
+#~ msgid "Persistence"
+#~ msgstr "Persistenza"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Dispositivo disco fisico"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Dispositivo disco fisico sull'host"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Scegli un pool di archiviazione"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "Scegli un volume"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Inserisci il nuovo nome del volume"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Avvia la macchina virtuale per accedere alla sua console."
+
+#~ msgid "Plug"
+#~ msgstr "Spina"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "Il pool deve essere attivo per creare un volume"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "Il tipo di pool non supporta la creazione di volumi"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "I volumi del pool vengono utilizzati dalle VM "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Numero preferito di socket da esporre al guest."
+
+#~ msgid "Prefix"
+#~ msgstr "Prefisso"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "La lunghezza del prefisso non può essere vuota"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "Gli snapshot eseguiti in precedenza consentono di tornare a uno stato "
+#~ "precedente se qualcosa va storto"
+
+#~ msgid "Product"
+#~ msgstr "Prodotto"
+
+#~ msgid "Profile"
+#~ msgstr "Profilo"
+
+#~ msgid "Protocol"
+#~ msgstr "Protocollo"
+
+#~ msgid "Remote URL"
+#~ msgstr "URL remoto"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "Dettagli visualizzatore remoto"
+
+#~ msgid "Revert"
+#~ msgstr "Ripristina"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Ripristina allo snapshot $0"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "Il ripristino di questa istantanea riporterà la VM al momento "
+#~ "dell'istantanea e lo stato corrente andrà perso, insieme a tutti i dati "
+#~ "non acquisiti in un'istantanea"
+
+#~ msgid "Root password"
+#~ msgstr "Password di root"
+
+#~ msgid "Route to $0"
+#~ msgstr "Rotta verso $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Esegui l'installazione automatica"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Esegui all'avvio dell'host"
+
+#, fuzzy
+#~| msgid "SPICE TLS port:"
+#~ msgid "SPICE TLS port"
+#~ msgstr "Porta SPICE TLS:"
+
+#, fuzzy
+#~| msgid "SPICE address:"
+#~ msgid "SPICE address"
+#~ msgstr "Indirizzo SPICE:"
+
+#, fuzzy
+#~| msgid "SPICE port:"
+#~ msgid "SPICE port"
+#~ msgstr "Porta SPICE:"
+
+#~ msgid "Select console type"
+#~ msgstr "Seleziona il tipo di console"
+
+#~ msgid "Send key"
+#~ msgstr "Invia tasto"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Invia interruzione non mascherabile"
+
+#~ msgid "Serial console"
+#~ msgstr "Console seriale"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Imposta intervallo DHCP"
+
+#~ msgid "Set manually"
+#~ msgstr "Imposta manualmente"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "L'impostazione delle password utente per l'installazione automatica "
+#~ "richiede l'avvio della VM durante la sua creazione"
+
+#~ msgid "Show additional options"
+#~ msgstr "Mostra opzioni aggiuntive"
+
+#~ msgid "Shut off"
+#~ msgstr "Arresto"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "Spegni la VM per modificare la configurazione del firmware"
+
+#~ msgid "Shutting down"
+#~ msgstr "Spegnimento"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Impossibile creare lo snapshot"
+
+#~ msgid "Source format"
+#~ msgstr "Formato sorgente"
+
+#~ msgid "Source path"
+#~ msgstr "Percorso della sorgente"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Il percorso della sorgente non deve essere vuoto"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "La sorgente deve iniziare con il protocollo http, ftp o nfs"
+
+#~ msgid "Source volume group"
+#~ msgstr "Sorgente del gruppo di volumi"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Avvia libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Avvia il pool all'avvio dell'host"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "L'inizio non può essere vuoto"
+
+#~ msgid "Startup"
+#~ msgstr "Avvio"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "Impossibile attivare il pool di archiviazione $0"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "Impossibile disattivare il pool di archiviazione $0"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Impossibile creare il pool di archiviazione"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Nome del pool di archiviazione"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "Le dimensioni di archiviazione non possono essere 0"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr ""
+#~ "Il volume di archiviazione non deve eccedere il volume del pool di "
+#~ "archiviazione ($0 $1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "I Volumi di Archiviazione non possono essere eliminati"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "Sospesi (PM)"
+
+#~ msgid "Target path"
+#~ msgstr "Percorso di destinazione"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Il percorso di destinazione non deve essere vuoto"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr ""
+#~ "La VM è in esecuzione e verrà forzatamente arrestata prima "
+#~ "dell'eliminazione."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "La VM deve essere in esecuzione o spenta per rimuovere questo dispositivo"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "La directory sul server da esportare"
+
+#~ msgid "The pool is empty"
+#~ msgstr "Il pool è vuoto"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr ""
+#~ "Il sistema operativo selezionato non supporta l'installazione automatica"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr ""
+#~ "Il sistema operativo selezionato ha un requisito di memoria minimo di $0 "
+#~ "$1"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "Il sistema operativo selezionato ha un requisito di dimensione minima di "
+#~ "archiviazione di $0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "Impossibile eliminare il pool di archiviazione"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr "Questa VM è temporanea. Chiudila se desideri cancellarla."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Questo volume è già utilizzato da: "
+
+#~ msgid "Threads per core"
+#~ msgstr "Threads per core"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr ""
+#~ "Le VM non persistenti non supportano la modifica della configurazione "
+#~ "firmware"
+
+#~ msgid "Type ID"
+#~ msgstr "ID tipo"
+
+#~ msgid "Unique name"
+#~ msgstr "Nome univoco"
+
+#~ msgid "Unique network name"
+#~ msgstr "Nome di rete univoco"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Firmware sconosciuto"
+
+#~ msgid "Unplug"
+#~ msgstr "Scollegare"
+
+#~ msgid "Url"
+#~ msgstr "URL"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "Non è stato possibile salvare le impostazioni VCPU"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "La VM $0 esiste già"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "Impossibile riavviare forzatamente la VM $0"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "Impossibile spegnere forzatamente la VM $0"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "Impossibile eliminare la VM $0"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "Impossibile installare la VM $0"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "Impossibile Riavviare la VM $0"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "Impossibile riprendere la VM $0"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "La VM $0 non è riuscita a inviare l'NMI"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "Impossibile spegnere la VM $0"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "Impossibile avviare la VM $0"
+
+#~ msgid "VM state"
+#~ msgstr "Stato VM"
+
+#, fuzzy
+#~| msgid "VNC TLS port:"
+#~ msgid "VNC TLS port"
+#~ msgstr "Porta VNC TLS:"
+
+#, fuzzy
+#~| msgid "VNC address:"
+#~ msgid "VNC address"
+#~ msgstr "Indirizzo VNC:"
+
+#~ msgid "VNC console"
+#~ msgstr "Console VNC"
+
+#, fuzzy
+#~| msgid "VNC port:"
+#~ msgid "VNC port"
+#~ msgstr "Porta VNC:"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Macchine Virtuali"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Macchine virtuali"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Gestione macchine virtuali"
+
+#~ msgid "Virtual network"
+#~ msgstr "Rete virtuale"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Impossibile creare la rete virtuale"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Il servizio di virtualizzazione (libvirt) non è attivo"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Il nome del gruppo di volumi non può essere vuoto"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Scrivibile"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Scrivibile e condiviso"
+
+#~ msgid "Yesterday"
+#~ msgstr "Ieri"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "È necessario selezionare il sistema operativo più prossimo"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "disabilitato"
+
+#~ msgid "down"
+#~ msgstr "giù"
+
+#~ msgid "enabled"
+#~ msgstr "abilitato"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "dispositivo host"
+
+#~ msgid "host passthrough"
+#~ msgstr "host passthrough"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "Target iSCSI diretto"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "Iniziatore iSCSI IQN"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "Target iSCSI IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "no"
+
+#~ msgid "no state saved"
+#~ msgstr "nessuno stato salvato"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "dispositivo reindirizzato"
+
+#~ msgid "server"
+#~ msgstr "server"
+
+#~ msgid "up"
+#~ msgstr "su"
+
+#~ msgid "vCPU count"
+#~ msgstr "conteggio vCPU"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU Massimo"
+
+# ctx::sourcefile::/rhn/systems/details/virtualization/VirtualGuestsList.do
+#~ msgid "vCPUs"
+#~ msgstr "vCPU"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#~ msgid "view more..."
+#~ msgstr "vedi di più..."
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Il pacchetto virt-install deve essere installato sul sistema per clonare "
+#~ "le macchine virtuali"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Il pacchetto virt-install deve essere installato sul sistema per creare "
+#~ "nuove macchine virtuali"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Il pacchetto virt-install deve essere installato sul sistema per "
+#~ "modificare questo attributo"
+
+#~ msgid "vm"
+#~ msgstr "vm"
+
+#~ msgid "Local install media"
+#~ msgstr "Supporto di Installazione Locale"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "Imposta una password di root"
+
+#~ msgid "21st"
+#~ msgstr "21°"
+
+#~ msgid "22nd"
+#~ msgstr "22°"
+
+#~ msgid "23rd"
+#~ msgstr "23°"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Dopo"
+
+#~ msgid "Friday"
+#~ msgstr "Venerdì"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Ora : Minuto"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "L'ora deve essere un numero compreso tra 0-23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Formato data non valido."
+
+#~ msgid "Invalid number."
+#~ msgstr "Numero non valido."
+
+#~ msgid "Monday"
+#~ msgstr "Lunedì"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Ripeti ogni ora"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Ripeti ogni anno"
+
+#~ msgid "Saturday"
+#~ msgstr "Sabato"
+
+#~ msgid "Sunday"
+#~ msgstr "Domenica"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Questo giorno non esiste in tutti i mesi. Il<br> timer verrà eseguito "
+#~ "solo nei mesi che hanno 31 giorni."
+
+#~ msgid "Thursday"
+#~ msgstr "Giovedì"
+
+#~ msgid "Tuesday"
+#~ msgstr "Martedì"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 aggiornamento"
+#~ msgstr[1] "$0 aggiornamenti"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 correzione di sicurezza"
+#~ msgstr[1] "$1 correzioni di sicurezza"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Gestione dei dispositivi di archiviazione"
+
+#~ msgid "Restart now"
+#~ msgstr "Riavvia ora"
+
+#~ msgid "Configure"
+#~ msgstr "Configura"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr ""
+#~ "L'avvio di rete è disponibile solo quando si utilizza la connessione di "
+#~ "sistema"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Rete"
+
+#~ msgid "No snapshots"
+#~ msgstr "Nessun snapshot"
+
+#, fuzzy
+#~| msgid "No such file or directory '$0'"
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "Nessun file o directory '$0'"
+
+#~ msgid "No updates pending"
+#~ msgstr "Nessun aggiornamento in attesa"
+
+#, fuzzy
+#~| msgid "ssh server is empty"
+#~ msgid "This directory is empty"
+#~ msgstr "il server ssh è vuoto"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr ""
+#~ "Questo tipo di pool non supporta la creazione di Volumi di Archiviazione"
+
+#~ msgid "undefined"
+#~ msgstr "non definito"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Chiave host errata"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "L'autenticità dell'host non {{#strong}}{{host}}{{/strong}} può essere "
+#~ "stabilita. Sei sicuro di voler continuare a connetterti?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "La chiave di {{#strong}}{{host}}{{/strong}} non corrisponde alla chiave "
+#~ "precedentemente in uso. Se questa macchina non è stata sostituita di "
+#~ "recente, è probabile che qualcuno stia cercando di attaccare la vostra "
+#~ "connessione a questa macchina."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Chiave host sconosciuto"
+
+#~ msgid "Address:"
+#~ msgstr "Indirizzo:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Sono necessari almeno dei $0 dischi."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr ""
+#~ "Connessione con qualsiasi applicazione di visualizzazione SPICE o VNC."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Scarica l'MSI da $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Console grafica (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Console grafica in Desktop Viewer"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Nessuna console definita per questa macchina virtuale."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid ""
+#~ "For the host, either specify the hostname, IP address, an alias name or a "
+#~ "unique resource identifier for the SSH destination."
+#~ msgstr ""
+#~ "Per l'host, specificare il nome host, l'indirizzo IP, un nome alias o un "
+#~ "identificatore di risorsa univoco per la destinazione SSH."
+
+#~ msgid "Entry"
+#~ msgstr "Voce"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Il tuo browser si scollegherà, ma questo non influisce sul processo di "
+#~ "aggiornamento. È possibile riconnettersi in pochi istanti per continuare "
+#~ "a guardare i progressi."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "e riavviare automaticamente la macchina."
+
+#~ msgid "Dashboard"
+#~ msgstr "Dashboard"
+
+#, fuzzy
+#~ msgid "Description input text"
+#~ msgstr "Descrizione"
+
+#~ msgid "FAILED"
+#~ msgstr "FALLITO"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Ho perso la connessione. Cerco di ricollegarmi a un'altra volta"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Rete"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Si prega di inserire una nuova dimensione del volume"
+
+#~ msgid "Servers"
+#~ msgstr "Server"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "Al momento sei connesso direttamente a questo server. Non puoi "
+#~ "cancellarlo."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bit"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 byte"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 byte"
+
+#~ msgid "Bond settings "
+#~ msgstr "Impostazioni bond "
+
+#~ msgid "IP settings "
+#~ msgstr "Impostazioni IP "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Cockpit ha avuto un errore interno inaspettato. <br/><br/>Puoi provare a "
+#~ "riavviare Cockpit premendo aggiorna nel tuo browser. La console "
+#~ "javascript contiene dettagli su questo errore (<b>Ctrl-Shift-J</b> nella "
+#~ "maggior parte dei browser)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "La VM si è interrotta."
+
+#~ msgid "The VM is down."
+#~ msgstr "La VM è spenta."
+
+#~ msgid "The VM is going down."
+#~ msgstr "La VM si sta spegnendo."
+
+#~ msgid "The VM is idle."
+#~ msgstr "La VM è inattiva."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr "La macchina virtuale sta morendo (arresto o crash non completato)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "La VM è in pausa."
+
+#~ msgid "The VM is running."
+#~ msgstr "La macchina virtuale è in funzione."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "La VM è sospesa dalla gestione dell'alimentazione degli ospiti."
+
+#~ msgid "inactive"
+#~ msgstr "Inattivo"
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Lasciare vuoto per connettersi a questa macchina come utente attualmente "
+#~ "connesso. Se si immette un nome utente diverso, tale utente sarà sempre "
+#~ "utilizzato quando ci si connette a questa macchina."
+
+#~ msgid "2 min"
+#~ msgstr "2 min"
+
+#~ msgid "3 min"
+#~ msgstr "3 min"
+
+#~ msgid "4 min"
+#~ msgstr "4 min"
+
+#~ msgid "CPU graph"
+#~ msgstr "Grafico CPU"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "Stato CPU"
+
+#~ msgid "Cached"
+#~ msgstr "Imbarcato"
+
+#~ msgid "Graphs"
+#~ msgstr "Grafici"
+
+#~ msgid "I/O wait"
+#~ msgstr "Attesa I/O"
+
+#~ msgid "Kernel"
+#~ msgstr "Kernel"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Memoria"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "Utilizzo Memoria & Swap"
+
+#~ msgid "Memory graph"
+#~ msgstr "Grafico Memoria"
+
+#~ msgid "Network traffic"
+#~ msgstr "Traffico di rete"
+
+#~ msgid "Nice"
+#~ msgstr "Nice"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Sincronizzare gli utenti"
+
+#~ msgid "Usage graphs"
+#~ msgstr "Grafici di utilizzo"
+
+#~ msgid "View graphs"
+#~ msgstr "Visualizza grafici"
+
+#~ msgid "idle"
+#~ msgstr "in attesa"
+
+#~ msgid "paused"
+#~ msgstr "in pausa"
+
+#~ msgid "running"
+#~ msgstr "in esecuzione"
+
+#~ msgid "shut off"
+#~ msgstr "spento"
+
+#~ msgid "shutdown"
+#~ msgstr "spegnimento"
+
+#~ msgid "Add a new host"
+#~ msgstr "Aggiungi un Nuovo Host"
+
+# spazio su disco, quindi maschile
+#~ msgid "Available"
+#~ msgstr "Disponibile"
+
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Inserire l'indirizzo IP o il nome host"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Interfacce di rete"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Questa versione di cockpit-ws non supporta la connessione a un host con "
+#~ "un utente alternativo o una porta"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Per provare un porto diverso è necessario aggiornare le ganasce della "
+#~ "cabina di pilotaggio ad una versione più recente."
+
+#~ msgid "$0 template"
+#~ msgstr "$0 template"
+
+#~ msgid "Instance of template: "
+#~ msgstr "Istanza del modello: "
+
+#~ msgid "Instantiate"
+#~ msgstr "Istantanea"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 condivisioni"
+
+#~ msgid "All In One"
+#~ msgstr "Tutto In Uno"
+
+#~ msgid "Always"
+#~ msgstr "Sempre"
+
+#~ msgid "Author"
+#~ msgstr "Autore"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Dati errati passati per il meccanismo passwd1"
+
+#~ msgid "CPU priority"
+#~ msgstr "Priorità della CPU"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Non è possibile connettersi a Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Modificare i limiti delle risorse"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Modificare i limiti delle risorse"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr ""
+#~ "Cockpit non è stata in grado di effettuare il login{{#strong}}{{host}}{{/"
+#~ "strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit non è stato in grado di accedere{{#strong}}{{host}}{{/strong}}. È "
+#~ "possibile modificare le credenziali di autenticazione qui sotto. "
+#~ "{{#can_sync}}Si può preferire di {{#sync_link}}sincronizzare gli account "
+#~ "e le password{{/sync_link}}.{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Utilizzo della memoria combinata"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Utilizzo combinato di $0 core della CPU"
+#~ msgstr[1] "Utilizzo combinato di $0 core della CPU"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "Il comando non può essere vuoto"
+
+#~ msgid "Command:"
+#~ msgstr "Comando:"
+
+#~ msgid "Commit"
+#~ msgstr "Commit"
+
+#~ msgid "Commit image"
+#~ msgstr "Commit immagine"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Collegamento a Docker"
+
+#~ msgid "Container name"
+#~ msgstr "Nome container"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Il Container è attualmente contrassegnato come non funzionante, ma "
+#~ "l'arresto regolare non è riuscito."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Il contenitore è attualmente in funzione."
+
+#~ msgid "Container:"
+#~ msgstr "Container:"
+
+#~ msgid "Containers"
+#~ msgstr "Container"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Container"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Non è stato possibile cambiare i gruppi di utenti"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Impossibile cambiare la password utente"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Non riusciva a connettersi alla macchina"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Non è stato possibile creare nuovi utenti"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Impossibile elencare gli utenti locali"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Non è stato possibile elencare gli utenti"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Impossibile caricare i dati utente"
+
+# ctx::sourcefile::/rhn/account/UserDetails
+#~ msgid "Created:"
+#~ msgstr "Creato:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Contenitori Docker"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker non è installato o attivato sul sistema"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Duplica alias"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Duplica porta"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "Inserire una passphrase per aggiungere un identità a ssh-agent"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "L'inserimento di una password diversa significa che sarà necessario "
+#~ "riscriverla ogni volta che ci si ricollega a questa macchina"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys, author fvalen
+#~ msgid "Environment"
+#~ msgstr "Ambiente"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Errore nel caricamento degli utenti: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Messaggio di errore da parte di Docker:"
+
+#~ msgid "Everything"
+#~ msgstr "Tutto"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Codice di uscita $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Esporre le porte dei contenitori"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Impossibile avviare Docker: $0"
+
+#, fuzzy
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Impossibile fermare Docker scope: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Ricevi una nuova immagine"
+
+#~ msgid "IP address:"
+#~ msgstr "Indirizzo IP:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "Lunghezza prefisso IP:"
+
+# translation auto-copied from project Satellite6 Hammer CLI Foreman, version
+# 6.1, document hammer-cli-foreman
+#~ msgid "Id"
+#~ msgstr "Id"
+
+#~ msgid "Id:"
+#~ msgstr "Id:"
+
+#~ msgid "Image"
+#~ msgstr "Immagine"
+
+#~ msgid "Image $0"
+#~ msgstr "Immagine $0"
+
+#~ msgid "Image search"
+#~ msgstr "Ricerca immagini"
+
+#~ msgid "Image:"
+#~ msgstr "Immagine:"
+
+#~ msgid "Images"
+#~ msgstr "Immagini"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Immagini"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Immagini e container in esecuzione"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Per sincronizzare gli utenti, è necessario accedere a {{#strong}}{{host}}"
+#~ "{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Porta non valida"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Biglietto Kerberos"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Lasciare vuoto per connettersi a questa macchina come utente attualmente "
+#~ "connesso{{#default_user}} ({{default_user}}){{/default_user}}. Se si "
+#~ "inserisce un nome utente diverso, tale utente sarà sempre utilizzato per "
+#~ "connettersi a questa macchina."
+
+#~ msgid "Link to another container"
+#~ msgstr "Collegamento ad un altro contenitore"
+
+#~ msgid "Links:"
+#~ msgstr "Link:"
+
+#~ msgid "Login Password"
+#~ msgstr "Password di login"
+
+#~ msgid "MAC address:"
+#~ msgstr "Indirizzo MAC:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Limite memoria"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Montare i volumi dei container"
+
+#~ msgid "No container specified"
+#~ msgstr "Nessun contenitore specificato"
+
+#~ msgid "No containers"
+#~ msgstr "Nessun contenitore"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Nessun contenitore che corrisponde al filtro corrente"
+
+#~ msgid "No images"
+#~ msgstr "Nessuna immagine"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Nessuna immagine che corrisponde al filtro corrente"
+
+#~ msgid "No results for $0"
+#~ msgstr "Nessun risultato per $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Nessuna immagine che corrisponde al filtro corrente"
+
+#~ msgid "No running containers"
+#~ msgstr "Nessun contenitore in funzione"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Nessun contenitore in esecuzione che corrisponde al filtro corrente"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Non autorizzato ad accedere a Docker su questo sistema"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "In caso di guasto, $0tempo di ripetizione del tentativo"
+#~ msgstr[1] "In caso di guasto, $0tempi di ripetizione"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Si prega di confermare la cancellazione forzata di $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Per favore, prova con un altro termine"
+
+#~ msgid "Ports:"
+#~ msgstr "Porte:"
+
+#~ msgid "Problems"
+#~ msgstr "Problemi"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Solo lettura"
+
+#~ msgid "ReadWrite"
+#~ msgstr "Lettura scrittura"
+
+# translation auto-copied from project Red Hat Satellite User Guide, version
+# 6.0, document appe-Glossary_of_Terms
+#~ msgid "Repository"
+#~ msgstr "Repository"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Politica di riavvio:"
+
+#~ msgid "Retries:"
+#~ msgstr "Riprova:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Riutilizza la mia password per le connessioni remote"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Selezionare gli utenti con cui si desidera essere sincronizzati "
+#~ "{{#strong}}{{host}}{{/strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Impostare le variabili d'ambiente del container"
+
+#~ msgid "Severity:"
+#~ msgstr "Severità:"
+
+#~ msgid "Show all containers"
+#~ msgstr "Mostra tutti i container"
+
+#~ msgid "Start Docker"
+#~ msgstr "Avvia Docker"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#~ msgid "State:"
+#~ msgstr "Stato:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Sincronizza"
+
+# translation auto-copied from project shotwell-core, version 0.14.1, document
+# shotwell-core
+#~ msgid "Tag"
+#~ msgstr "Etichetta"
+
+# translation auto-copied from project subscription-manager, version 1.9.X,
+# document keys
+#~ msgid "Tags"
+#~ msgstr "Tag"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "I seguenti contenitori dipendono da questa immagine e diventeranno "
+#~ "inutilizzabili."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Questa immagine non esiste."
+
+#~ msgid "Type a password"
+#~ msgstr "Digitare una password"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Tipo da filtrare…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "A meno che non si sia fermato"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Meccanismo di impostazione non supportato"
+
+#~ msgid "Up since $0"
+#~ msgstr "Avviato dal $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Usato da Contenitori"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Utilizzo delle credenziali disponibili"
+
+#~ msgid "Volumes"
+#~ msgstr "Volumi"
+
+#~ msgid "Volumes:"
+#~ msgstr "Volumi:"
+
+#~ msgid "With terminal"
+#~ msgstr "Con terminale"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Sei connesso a {{#strong}}{{host}}{{/strong}}, ma per sincronizzare gli "
+#~ "utenti è necessario un utente con privilegi di superutente."
+
+#~ msgid "default"
+#~ msgstr "predefinito"
+
+#~ msgid "key"
+#~ msgstr "chiave"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "ricerca per nome, spazio dei nomi o descrizione"
+
+#~ msgid "shares"
+#~ msgstr "condivisioni"
+
+#~ msgid "to host port"
+#~ msgstr "per ospitare la porta"
+
+# ctx::sourcefile::NOT USED
+#~ msgid "value"
+#~ msgstr "valore"
+
+#~ msgid "Master"
+#~ msgstr "Maestro"
+
+#, fuzzy
+#~| msgid "Password"
+#~ msgid "Password for $0"
+#~ msgstr "Password"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "L'utente non <b>$0</b> è autorizzato a gestire i server"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Account"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "L'utente non <b>$0</b>è autorizzato a modificare gli account"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Impossibile cancellare l'account di root"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Impossibile rinominare l'account di root"
+
+#~ msgid "Account Settings"
+#~ msgstr "Impostazioni account"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Aggiungi Macchina alla Dashboard"
+
+#~ msgid "Machines"
+#~ msgstr "Macchine"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "Il login ha aumentato i privilegi amministrativi"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Password non utilizzabile per compiti privilegiati o per connettersi ad "
+#~ "altre macchine"
+
+#~ msgid "Privileged"
+#~ msgstr "Privilegiato"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Riutilizza la mia password per attività privilegiate e per connettersi ad "
+#~ "altre macchine"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "L'utente non <b>$0</b>è autorizzato a gestire l'archiviazione"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr ""
+#~ "L'utente non <b>$0</b>è autorizzato a modificare le impostazioni di rete"
+
+#, fuzzy
+#~| msgid "Required by"
+#~ msgid "Required by $0"
+#~ msgstr "Richiesto da"
+
+#~ msgid "Automatic Startup"
+#~ msgstr "Avvio Automatico"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Ultimo trigger"
+
+#~ msgid "Next Run"
+#~ msgstr "Prossima esecuzione"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "L<b>$0</b>'utente non ha i permessi per la creazione di timer"
+
+#~ msgid "Connecting"
+#~ msgstr "Connessione"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Informazioni su Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (condiviso con il SO)"
+
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 Vulnerabilità"
+#~ msgstr[1] "$0 Vulnerabilità"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Aggiungi memoria aggiuntiva"
+
+#~ msgid "Add Storage"
+#~ msgstr "Aggiungi Storage"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Spazio di archiviazione aggiuntivo"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Tutti i dati sui dischi selezionati verranno cancellati e i dischi "
+#~ "verranno aggiunti al pool di archiviazione."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Configura storage....."
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Impossibile aggiungere tutti i dischi"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Cancellare i contenitori e resettare il pool di stoccaggio"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr ""
+#~ "Le informazioni sul pool di archiviazione Docker non sono disponibili."
+
+#~ msgid "Local Disks"
+#~ msgstr "Dischi locali"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "Non è stata trovata alcuna ulteriore conservazione locale."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Riformattare e aggiungere dischi"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "Il ripristino della piscina di stoccaggio cancellerà tutti i contenitori "
+#~ "e i dischi di rilascio nella piscina."
+
+#~ msgid "Security"
+#~ msgstr "Sicurezza"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr ""
+#~ "Il pool di archiviazione Docker non può essere gestito su questo sistema."
+
+#, fuzzy
+#~| msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "La scansione da $time ($type) non ha trovato vulnerabilità."
+#~ msgstr[1] "La scansione da $time ($type) non ha trovato vulnerabilità."
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "La scansione da $time ($type) non ha trovato vulnerabilità."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "La scansione da $time ($type) non ha avuto successo."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Il servizio di virtualizzazione è disponibile"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "Non hai il permesso di gestire il pool di archiviazione di Docker."
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} -> $cport"
diff --git a/po/its/polkit.its b/po/its/polkit.its
new file mode 100644
index 0000000..1c37e6b
--- /dev/null
+++ b/po/its/polkit.its
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<its:rules xmlns:its="http://www.w3.org/2005/11/its"
+ version="2.0">
+ <its:translateRule selector="//*" translate="no"/>
+ <its:translateRule selector="//action/description |
+ //action/message"
+ translate="yes"/>
+</its:rules>
diff --git a/po/its/polkit.loc b/po/its/polkit.loc
new file mode 100644
index 0000000..c7427ec
--- /dev/null
+++ b/po/its/polkit.loc
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<locatingRules>
+ <locatingRule name="polkit policy" pattern="*.policy">
+ <documentRule localName="policyconfig" target="polkit.its"/>
+ </locatingRule>
+</locatingRules>
diff --git a/po/ja.po b/po/ja.po
new file mode 100644
index 0000000..7e7f3f7
--- /dev/null
+++ b/po/ja.po
@@ -0,0 +1,12844 @@
+# cockpit <cockpituous@gmail.com>, 2017. #zanata
+# Casey Jones <nahareport@live.com>, 2018. #zanata
+# Ludek Janda <ljanda@redhat.com>, 2018. #zanata, 2020.
+# cockpit <cockpituous@gmail.com>, 2018. #zanata
+# Hiroshi Yamanaka <hyamanak@redhat.com>, 2019. #zanata
+# Keiko Moriguchi <kemorigu@redhat.com>, 2019. #zanata
+# cockpit <cockpituous@gmail.com>, 2019. #zanata
+# Martin Pitt <mpitt@redhat.com>, 2020.
+# Casey Jones <nahareport@yahoo.com>, 2020.
+# Hajime Taira <htaira@pantora.net>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2024-02-12 16:45+0000\n"
+"Last-Translator: 김인수 <simmon@nplob.com>\n"
+"Language-Team: Japanese <https://translate.fedoraproject.org/projects/"
+"cockpit/main/ja/>\n"
+"Language: ja\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"X-Generator: Weblate 5.3.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "ユーザー数"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 アクティブゾーン"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 利用可能"
+
+#: pkg/storaged/block/resize.jsx:266
+msgid "$0 can not be made larger"
+msgstr "$0 を大きくすることはできません"
+
+#: pkg/storaged/block/resize.jsx:263
+msgid "$0 can not be made smaller"
+msgstr "$0 を小さくすることはできません"
+
+#: pkg/storaged/block/resize.jsx:259
+msgid "$0 can not be resized"
+msgstr "$0 のサイズは変更できません"
+
+#: pkg/storaged/block/resize.jsx:255
+msgid "$0 can not be resized here"
+msgstr "ここでは $0 のサイズを変更できません"
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 チャンクサイズ"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "「重大な影響」を含む $0 件が該当"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 データ + $1 オーバーヘッドが $2 ($3) を使用しています"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 日"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 本のディスクがありません"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 本のディスクがありません"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 ディスク"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 ドキュメント"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 エラー"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 がコード $1 で終了しました"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 が失敗しました"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 がログインの試行に失敗しました"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "$0 filesystem"
+msgstr "$0 ファイルシステム"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 空き"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 時間"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "「重要な影響」を含む $0 件が該当"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 は既存のファイルです"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 は使用されています"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 は、あらゆるリポジトリーから利用できません。"
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 キーが変更されました"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 がシグナル $1 で終了しました"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "「低度の影響」を含む $0 件が該当"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 分"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "「中程度の影響」を含む $0 件が該当"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 カ月"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 詳細表示..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 パッケージ"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 パッケージにはシステムの再起動が必要です"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 ページ"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+msgid "$0 partitions"
+msgstr "$0 パーティション"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 セキュリティー修正が利用可能"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "失敗したサービスの数 ($0)"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 サービスを再起動する必要があります"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 スロットが残ります"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 スパイク"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0 同期済み"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 合計"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 更新を利用できます"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 が $1 ($2 が保存) を使用しています"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 週"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 がインストールされます。"
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 年"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 ゾーン"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: $1 でクラッシュ"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name ($host)"
+
+#: pkg/storaged/dialog.jsx:1218
+msgid "(Not part of target)"
+msgstr "(ターゲットに含まれていません)"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+msgid "(no assigned mount point)"
+msgstr "(マウントポイントが割り当てられていません)"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+msgid "(not mounted)"
+msgstr "(マウントされていません)"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(推奨)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] "セキュリティー修正を $1 個含む"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 日"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 時間"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 分"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 分"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 週間"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10 日"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12 日"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13 日"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14 日"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 分"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16 日"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17 日"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18 日"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19 日"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 分"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20 日"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21 日"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22 日"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23 日"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24 日"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29 日"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 分"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4 日"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 分"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 分"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5 日"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 時間"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 分"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6 日"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8 日"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9 日"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Cockpit の互換バージョンが $0 にインストールされていません。"
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "この名前のファイルシステムは、すでにこのプールに存在します。"
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "この名前のグループはすでに存在します"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"セキュリティー、信頼性、およびパフォーマンスに対して最新のブラウザーが必要で"
+"す。"
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"ネットワークボンディングは、複数のネットワークインターフェイスを単一の論理イ"
+"ンターフェイスにまとめることで、スループットや冗長性を向上させます。"
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"$0 で新しい SSH キーが $2 の $1 用に作成され、$5 上の $3 の $3 ファイルに追加"
+"されました。"
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"パッケージの更新を有効にするには、システムを再起動する必要があります:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "この名前を持つプールはすでに存在します。"
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] "更新を有効にするには、サービスを再起動する必要があります:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "物理ボリュームが欠落しているボリュームグループの名前は変更できません。"
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP 監視"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Webコンソールについて"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "不在"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "キーを受け入れてログイン"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "受け入れられるパスワード"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "アカウントの有効期限"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "アカウントが利用可能でないか、アカウントを編集できません。"
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "アカウント"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "アクション"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "動作"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "有効化"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr "サイズ変更前のアクティブ化"
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "$target のアクティベート"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "動作中"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "アクティブなバックアップ"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "アクティブページ"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "アクティブになった日時 "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "アクティブな状態"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "適応ロードバランス"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "適応送信のロードバランス"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "追加"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "$0 の追加"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "DNS サーバーの追加"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "ネットワークバウンドディスクの暗号化追加"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Tang キーサーバーの追加"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "VLAN の追加"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "VPN の追加"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "WireGuard VPN の追加"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "bitmap の追加"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "新規ゾーンの追加"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "アドレスの追加"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "ブロックデバイスの追加"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "ボンディングの追加"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "ブリッジの追加"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "ディスクの追加"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "ディスクの追加"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "iSCSI ポータルの追加"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "キーの追加"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "キーサーバーの追加"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "メンバーの追加"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "新規ホストの追加"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "新規ゾーンの追加"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "パスフレーズの追加"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "ピアの追加"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+msgid "Add physical volume"
+msgstr "物理ボリュームの追加"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "ポートの追加"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "$0 ゾーンにポートを追加"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "公開鍵の追加"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "ルートの追加"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "検索ドメインの追加"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "サービスの追加"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "$0 ゾーンにサービスを追加"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "ゾーン $0 にサービスを追加"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "チームの追加"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "ゾーンの追加"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "暗号化オプションへの \"$0\" の追加"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "ファイルシステムオプションへの \"$0\" の追加"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 を追加すると、サーバーへの接続が切断され、管理 UI が利用できなくなります。"
+
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"キーサーバーを追加するには、プールのロック解除が必要です。既存のプールパスフ"
+"レーズを提供してください。"
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"カスタムポートを追加すると、firewalld がリロードされます。リロードにより、ラ"
+"ンタイムのみの設定が失われます!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "キーの追加"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "$target への物理ボリュームの追加"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "カーネルコマンドラインへの rd.neednet=1 の追加"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "追加の DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "追加の DNS 検索ドメイン $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "その他の動作"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "追加のアドレス $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "追加のパッケージ:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "追加ポート"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "アドレス"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "アドレス $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "アドレスは空欄にできません"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "アドレスの URL は有効ではありません"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "アドレス"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "アドレスの形式が正しくありません"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Cockpit Web コンソールでの管理"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "管理アクセス"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr "レポートの作成およびアクセスには、管理アクセスが必要です。"
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "管理アクセスが必要"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "高度な TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "後"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"ドメインの終了後は、ローカル認証情報を持つユーザーだけが、このマシンにログイ"
+"ンできます。DNS 解決設定および信頼される CA の一覧が変更する可能性があるた"
+"め、他のサービスにも影響を及ぼす場合があります。"
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "システムブート後"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "アラート"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "アラート以上のレベル"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "エイリアス"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "すべて"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr "選択したレイアウトには、選択した $0 の物理ボリュームがすべて必要です。"
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "すべてのアップデート"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "オールインワン"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "実行を許可 (マスク解除)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "利用可能な IP"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "許可されたアドレス"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "追加の $0 を選択する必要があります"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Ansible ロールのドキュメント"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"ログメッセージのテキスト文字列はフィルターでフィルタリングできます。文字列"
+"は、正規表現の形式でも指定できます。また、メッセージログフィールドによるフィ"
+"ルタリングもサポートできます。これらは、FIELD=VALUE の形式でスペースで区切っ"
+"た値は、使用できる値のカンマ区切りのリストになります。"
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "外観"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "アプリケーション情報が見つかりません"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "アプリケーション"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "アプリケーションリスト"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "適用して再起動する"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "カーネルライブパッチの適用"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "このソリューションの適用"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "新しいポリシーを適用しています... これには数分かかる場合があります。"
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "ソリューションの適用中 ..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "アップデートの適用中"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "アップデートの適用に失敗しました"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "/var などの重要なマウントに適しています"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "アプリ"
+
+#: pkg/storaged/drive/drive.jsx:102
+msgid "Assessment"
+msgstr "評価"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "アセットタグ"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "起動時"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "少なくとも $0 個のディスクが必要です。"
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "1 つ以上のブロックデバイスが必要です。"
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "1 つ以上のディスクが必要です。"
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr "最低でも 1 つの親を、書き込み可能な状態でマウントする必要があります"
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "分"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "秒"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "特定の時間"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "属性"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "監査ログ"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "認証する"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "認証中"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "認証"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "認証に失敗しました"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "認証に失敗しました: サーバーの接続が切断されました"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr "Cockpit Web コンソールでの特権タスクの実行には、認証が必要です"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "認証が必要です"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "SSH キーの認証"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "承認された公開 SSH 鍵"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "自動"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "自動 (DHCP のみ)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "自動ログイン"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "自動アップデート"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "自動起動"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "NTP を自動的に使用"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "追加の NTP サーバーを自動的に使用"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "特定の NTP サーバーを自動的に使用"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "オートメーションスクリプト"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "$0 で利用可能なターゲット"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "利用可能なアップデート"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+msgid "BIOS boot partition"
+msgstr "BIOS ブートパーティション"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "BIOS の日付"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "BIOS のバージョン"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "アカウントに戻る"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "正しくない"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "正しくない設定"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "バランサー"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "前"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "バインドする"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "黒"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "ブレード"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "ブレードエンクロージャー"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "ブロックデバイス"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "ファイルシステム用ブロックデバイス"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "ブロックデバイス"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "ブロック済み"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "ボンディング"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "ブート"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"ファイルシステムがマウントされず、リモートアクセスができないと、起動に失敗し"
+"ます"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "ファイルシステムがマウントされなくても、起動は成功します"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "バインドされた"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "ブリッジ"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "ブリッジポート"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "ブリッジポート設定"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "ブロードキャスト"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "破損した設定"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr "Btrfs ボリュームがマウントされています"
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "バグ修正の更新が利用可能"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "バグ"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "バス拡張シャーシ"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"SSH キー $0 のパスワードを $2 の $1 のログインパスワードに変更することで、自"
+"動的にキーが使えるようになります。これで、パスワードなしで $3 にログインでき"
+"るようになります。"
+
+#: pkg/static/login.html:61
+msgid "Bypass browser check"
+msgstr "ブラウザーチェックの回避"
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "CPU セキュリティー"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "CPU セキュリティートグル"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "CPU 使用率"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "CPU 使用率/負荷"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "キャッシュ"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "ホスト名、IP アドレス、エイリアス名、または ssh:// URI を指定できます"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr ""
+"現在のフィルターの組み合わせを使用して、ログを見つけることができません。"
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "取り消し"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "電源オフの取り消し"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "再起動の取り消し"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "有効にできません"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "不明なホストには接続できません"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "ログインのクレデンシャルをフォワードできません"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr "realmd がこのシステムで利用できないため、ドメインに参加できません"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "過去のイベントはスケジュールできません"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "容量"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "キャリア"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "変更"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "暗号化ポリシーの変更"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "ホスト名の変更"
+
+#: pkg/storaged/overview/overview.jsx:152
+msgid "Change iSCSI initiater name"
+msgstr "iSCSI イニシエーター名の変更"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "iSCSI イニシエーター名の変更"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+msgid "Change label"
+msgstr "ラベルの変更"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "パスフレーズの変更"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "パスワードの変更"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "パフォーマンスプロファイルの変更"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "プロファイルの変更"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "シェルの変更"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "システム時間の変更"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "$0 のパスワードを変更します"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "設定の変更"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"キーを変更すると、オペレーティングシステムを再インストールすることになること"
+"が多くあります。ただし、予期しない変更により接続の傍受が試行される場合もあり"
+"ます。"
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+"パーティションタイプを変更すると、システムが起動しなくなる可能性があります。"
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"設定を変更すると、サーバーへの接続が切断され、管理 UI が利用できなくなりま"
+"す。"
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "更新の確認"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"そのコマンドの SHA-256 または SHA-1 が、このダイアログに一致するかどうかを確"
+"認します。"
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "キーハッシュを Tang サーバーで確認します。"
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "$target の確認中"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "IP の確認中"
+
+#: pkg/storaged/jobs-panel.jsx:68
+msgid "Checking MDRAID device $target"
+msgstr "MDRAID デバイス $target の確認"
+
+#: pkg/storaged/jobs-panel.jsx:69
+msgid "Checking and repairing MDRAID device $target"
+msgstr "MDRAID デバイス $target の確認および修復"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "$0 パッケージの確認中"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "initrd での NBDE サポートの確認中"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "新しいアプリケーションの確認中"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "パッケージの更新を確認しています..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "インストールされたソフトウェアの確認中"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "関連プロセスの確認中"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "ソフトウェアステータスの確認"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "アプリケーションで使用する言語の選択"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "チャンクサイズ"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "クラス"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "$target のクリーンアップ中"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "'起動に失敗' を消去"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "すべてのフィルターの消去"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "検索の消去"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "cleartext デバイス"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "クライアントソフトウェア"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "閉じる"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "選択されたページを閉じる"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Cockpit の認証が間違って設定されています。"
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "Cockpit の NetworkManager と Firewalld の設定"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit は該当するホストに接続できませんでした。"
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "cockpit で予期しない内部エラーが発生しました。"
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit は、Web ブラウザーで Linux サーバーを簡単に管理できるサーバーマネー"
+"ジャーです。端末と Web ツールを区別せずに使用できます。Cockpit で起動された"
+"サービスは端末で停止できます。同様に、端末でエラーが発生した場合は、そのエ"
+"ラーを Cockpit ジャーナルインターフェースで確認できます。"
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit は対話型 Linux サーバー管理インターフェースです。"
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit にはシステム上のそのソフトウェアとの互換性がありません。"
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit はインストールされていません"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit はシステムにインストールされていません。"
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit は経験が少ないシステム管理者に最適です。これらのシステム管理者はスト"
+"レージの管理、ジャーナルの検査、サービスの起動および停止などの単純なタスクを"
+"簡単に実行できるようになります。また、複数のサーバーを同時に監視および管理で"
+"きます。これらのサーバーはクリックするだけで追加できます。追加後に、ご使用の"
+"マシンは他のマシンを管理するようになります。"
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr ""
+"Cockpit がお使いのブラウザーで正しくレンダリングされない可能性があります"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "診断およびサポートデータの収集とパッケージ化"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "カーネルクラッシュダンプの収集"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "メトリクスの収集"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "色"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "ポート、範囲、およびサービスをコンマ区切りで指定できます"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "コマンド"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "コマンドが見つかりません"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "コメント"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "tuned との通信に失敗しました"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "PCI の圧縮"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "すべてのシステムおよびデバイスとの互換性あり (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr ""
+"最新のシステムとの互換性があり、ハードディスクが 2TB よりも大きい (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "クラッシュダンプを圧縮し容量を節約する"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "圧縮"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "条件 $0=$1 を満たしていませんでした"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "条件が満たされませんでした"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "設定"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "設定中"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "IP の設定中"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "kdumpを設定中"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "システム設定の変更"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "確認"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "$0 の削除を確定する"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "キーパスワードの確認"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "新しいキーパスワードの確認"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "新規パスワードの確認"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "パスワードの確認"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "$0 の削除を確認"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "代替のパスフレーズで削除を確認"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "$0 の停止を確定する"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "競合する"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "競合"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "自動的に接続"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "接続先"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "接続先:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "SETroubleshoot デーモンへ接続中 ..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "dbus への接続に失敗しました: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "マシンへ接続中"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "接続エラー"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "接続に失敗しました"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "接続がタイムアウトしました。"
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "接続が失われます"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "構成するもの"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "接続されたドメイン"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "次を含む:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "続行"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "セッションを続ける"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "コントロール"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "コントロール"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "コントローラー"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "変換可能"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "コピー済み"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "コピー"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "クリップボードにコピー"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "コア $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "$0 に問い合わせられませんでした"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "クラッシュダンプの場所"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "クラッシュの報告"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "システムのクラッシュ"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "作成"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "LVM2 ボリュームグループの作成"
+
+#: pkg/storaged/overview/overview.jsx:145
+msgid "Create MDRAID device"
+msgstr "MDRAID デバイスの作成"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "RAID デバイスの作成"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Stratis プールの作成"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "新しい SSH キーを作成して承認します"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "ファイルシステム $0 のスナップショットの作成"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "一意ではない UID を使用したアカウントの作成"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "脆弱なパスワードを使用したアカウントの作成"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "ホームディレクトリーの所有権の作成および変更"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "作成およびマウント"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Create and start"
+msgstr "作成および起動"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "ファイルシステムの作成"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "作成"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "論理ボリュームの作成"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "アカウントの新規作成"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "ファイルシステムの新規作成"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "グループの新規作成"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "論理ボリュームの新規作成"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "このコンテンツで新しいタスクファイルを作成します。"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+msgid "Create new thinly provisioned logical volume"
+msgstr "シンプロビジョニングされた論理ボリュームの新規作成"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "作成のみ"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "パーティションの作成"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "$0 上でのパーティションの作成"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "パーティションテーブルの作成"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "スナップショットの作成"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "スナップショットの作成およびマウント"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "スナップショットの作成のみ"
+
+#: pkg/storaged/overview/overview.jsx:174
+msgid "Create storage device"
+msgstr "ストレージデバイスの作成"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+msgid "Create subvolume"
+msgstr "サブボリュームの作成"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "シンボリュームの作成"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "タイマーの作成"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "ボリュームグループの作成"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "作成済み"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "LVM2 ボリュームグループ $target の作成"
+
+#: pkg/storaged/jobs-panel.jsx:67
+msgid "Creating MDRAID device $target"
+msgstr "MDRAID デバイス $target の作成"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "VDO デバイスの作成"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "$target 上でのファイルシステムの作成"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "論理ボリューム $target の作成"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "パーティション $target の作成"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "$target のスナップショットの作成"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"この $0 を作成すると、サーバーへの接続が切断され、管理 UI が利用できなくなり"
+"ます。"
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "重大以上のレベル"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"暗号化ポリシーは、TLS、IPSec、SSH、DNSSec、および Kerberos プロトコルに対応す"
+"るコア暗号化サブシステムを設定するシステムコンポーネントです。"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "暗号化ポリシー"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "暗号化ポリシーに一貫性がありません"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "現在の起動"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "現在の CPU 使用率のトップ"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "現在使用中"
+
+#: pkg/kdump/kdump-view.jsx:535
+msgid "Currently not supported"
+msgstr "現在サポートされていません"
+
+#: pkg/storaged/partitions/partition.jsx:146
+msgid "Custom"
+msgstr "Custom"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "カスタム暗号化ポリシー"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "カスタムのマウントオプション"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "カスタムポート"
+
+#: pkg/storaged/partitions/partition.jsx:180
+msgid "Custom type"
+msgstr "カスタムタイプ"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "カスタムゾーン"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "SHA-1 署名検証があるデフォルトは使用できます。"
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS 検索ドメイン"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "DNS 検索ドメイン $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "毎日"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Danger アラート:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "ダーク"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "データ"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "使用済みデータ"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"信頼性とパフォーマンスを向上させるために、データは 2 つのコピーとして、複数の"
+"選択した物理ボリュームに交互に保存されます。4 つ以上のボリュームを選択する必"
+"要があります。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"信頼性を向上させるために、データは複数の選択した物理ボリュームに 2 つ以上のコ"
+"ピーとして保存されます。2 つ以上のボリュームを選択する必要があります。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+"パフォーマンスを向上させるために、データは複数の選択した物理ボリュームに交互"
+"に保存されます。2 つ以上のボリュームを選択する必要があります。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"物理ボリュームの 1 つが失われてもデータに影響しないように、データは複数の選択"
+"した物理ボリュームに保存されます。3 つ以上のボリュームを選択する必要がありま"
+"す。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+"物理ボリュームの 1 つが失われてもデータに影響しないように、データは複数の選択"
+"した物理ボリュームに保存されます。また、パフォーマンスを向上させるために、"
+"データは交互に保存されます。3 つ以上のボリュームを選択する必要があります。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+"物理ボリュームが同時に 2 つまで失われてもデータに影響しないように、データは複"
+"数の選択した物理ボリュームに保存されます。また、パフォーマンスを向上させるた"
+"めに、データは交互に保存されます。5 つ以上のボリュームを選択する必要がありま"
+"す。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+"データは選択した物理ボリュームに保存されます。冗長性やパフォーマンスの向上は"
+"ありません。"
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"日付の指定は、YYYY-MM-DD hh:mm:ss の形式にする必要がありま"
+"す。'yesterday'、'today'、'tomorrow' という文字列も認識されます。'now' は現在"
+"の時刻を指します。相対時間も指定できます。その場合は '-' または '+' を先頭に"
+"付けます"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "解除"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+msgid "Deactivate logical volume $0/$1?"
+msgstr "論理ボリューム $0/$1 を非アクティブ化しますか?"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "非アクティブ化"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "$target の非アクティブ化"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "デバッグ以上のレベル"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "1 つ減らす"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "専用パリティー (RAID 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "重複"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "デフォルト"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "遅延"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "遅延は数字である必要があります"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "削除"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "$0 の削除"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "アカウントの削除"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "ファイルの削除"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "グループの削除"
+
+#: pkg/storaged/stratis/pool.jsx:292
+msgid "Delete pool"
+msgstr "プールの削除"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "レポートを完全に削除しますか?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 を削除すると、サーバーへの接続が切断され、管理 UI が利用できなくなります。"
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "$target の削除中"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "LVM2 ボリュームグループ $target の削除"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr ""
+"Stratis プールを削除すると、Stratis プールに含まれるデータがすべて消去されま"
+"す。"
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr ""
+"ファイルシステムを削除すると、そのファイルシステム内のすべてのデータが削除さ"
+"れます。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr ""
+"論理ボリュームを削除すると、論理ボリューム内のすべてのデータが削除されます。"
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr ""
+"パーティションを削除すると、パーティション内のすべてのデータが削除されます。"
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "削除すると、MDRAID デバイスのデータがすべて消去されます。"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "削除すると、VDO デバイスのデータがすべて消去されます。"
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "削除すると、ボリュームグループのデータがすべて消去されます。"
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr ""
+"削除すると、このサブボリュームおよびその子のデータがすべて消去されます。"
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "削除すると、以下のファイルが削除されます:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "説明"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "デスクトップ"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "割り当て解除可能"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "詳細"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "開発"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "デバイス"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "デバイスファイル"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "デバイスは読み取り専用です"
+
+#: pkg/storaged/block/other.jsx:60
+msgid "Device number"
+msgstr "デバイス番号"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "診断レポート"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "フォルダー"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "ディレクトリー $0 は書込み可能でないか、存在しません。"
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "同時マルチスレッディングの無効化"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "ファイアウォールの無効化"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "tuned の無効化"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "無効"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "対話式パスワードを禁止する"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "パスワード認証を禁止する"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "実行を禁止 (マスク)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "切断"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "切断されています"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "ディスク I/O"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "ディスクは OK です"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "ディスクで障害が発生中"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "ディスクのパスフレーズ"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "ディスク"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "ディスクの使用状況"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "解除"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "$0 アラートの解除"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "選択したアラートの解除"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "表示言語"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "分散パリティー (RAID 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "マウントしない"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "起動時に自動的にマウントしない"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "ドッキングステーション"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "自動起動しません"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "起動時にマウントしません"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "ドメイン"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "ドメイン"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "ドメインアドレス"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "ドメイン管理者名"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "ドメイン管理者パスワード"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "ドメイン に接続できませんでした"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "ドメインはサポートされません"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "繰り返さない"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "ダブル分散パリティー (RAID 6)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "ダウンロード"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "新しいブラウザーを無料でダウンロードします"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "ダウンロードされました"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "ダウンロード中"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "$0 をダウンロード中"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "ドライブ"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "デュアルランク"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+msgid "EFI system partition"
+msgstr "EFI システムパーティション"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "編集"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "/etc/motd を編集する"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Tang キーサーバーを編集する"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "VLAN 設定の編集"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "WireGuard VPN を編集する"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "ボンディング設定の編集"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "ブリッジ設定の編集"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "$0 ゾーンでカスタムサービスを編集する"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "ホストを編集する"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "ホストを編集する"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "motd を編集する"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+msgid "Edit mount point"
+msgstr "マウントポイント 編集"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "ルールとゾーンを編集する"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "サービスの編集"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "サービス $0 を編集する"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "チーム設定の編集"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "ユーザーの編集"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "キーの編集にはフリースロットが必要です"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "$target の取り出し中"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "組み込み PC"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "空"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "空"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "パスワードが入力されていません"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "$target を空にしています"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "有効化"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "デバイスを有効化または無効化します"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "サービスを有効にします"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "ファイアウォールを有効にします"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "有効"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "$0 の有効化"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "暗号化データ"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Tang キーサーバーでデータを暗号化する"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "パスフレーズでデータを暗号化する"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "暗号化"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "暗号化された $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+msgid "Encrypted Stratis pool"
+msgstr "暗号化された Stratis プール"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "暗号化された $0 の論理ボリューム"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "暗号化された $0 のパーティション"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "暗号化"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "暗号化オプション"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "暗号化パスフレーズ"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "暗号化タイプ"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "終了"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "エンドポイント"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"\"サーバー\" として機能するエンドポイントは host:port の形式で指定する必要が"
+"あります。それ以外の場合は空白のままにできます。"
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "強制"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "機能拡張の更新を利用できます"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "有効な MAC アドレスを入力します"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "サブネット全体"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "$0 のエントリー"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "$target の削除中"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "エラータ"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "エラー"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "エラー以上のレベル"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "エラーが発生しました"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "$0 のインストールエラー: PackageKit はインストールされていません"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "エラーメッセージ"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "システム変更を検出するための semanage の実行にエラーが発生しました"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "承認された鍵の保存中にエラーが発生しました: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "SELinux モードの設定中にエラーが発生しました: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "$0 個の物理ボリュームを選択する必要があります"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+"論理ボリュームのストライプごとに 1 つずつ、合計で $0 個の物理ボリュームを選択"
+"する必要があります。"
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "例: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "例: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "優れたパスワード"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "拡張シャーシ"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "アカウントの有効期限:"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "アカウントの有効期限: $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "エクスポート"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "ネットワークへエクスポート"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "拡張情報"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "拡張パーティション"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS が適切に有効化されていません"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "さらにコモンクライテリアの制限がある FIPS。"
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "失敗"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "マシンの追加に失敗しました: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "ポートの追加に失敗しました"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "サービスの追加に失敗しました"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "ゾーンの追加に失敗しました"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "パスワードの変更に失敗しました"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "PCP の設定に失敗しました"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "アラートの削除に失敗しました: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "tuned の無効化に失敗しました"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "tuned プロファイルの無効化に失敗しました"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "マシンの編集に失敗しました: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "サービスの編集に失敗しました"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "firewalld での $0 の有効化に失敗しました"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "tuned の有効化に失敗しました"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "ログの取得に失敗しました"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "承認された鍵のロードに失敗しました。"
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "ユニットのロードに失敗しました"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"dnf-automatic.timer または dnf-automatic-install.timer のユニットファイルの分"
+"析に失敗しました。カスタムオーバーライドを削除して、自動更新を設定してくださ"
+"い。"
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "サービスの再起動に失敗しました"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "変更を /etc/motd に保存できませんでした"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "設定の保存に失敗しました"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "起動に失敗"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "プロファイルの切り替えに失敗しました"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "ファイル状態"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "ファイルシステム"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "ファイルシステムがロックされています"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "ファイルシステム名"
+
+#: pkg/storaged/dialog.jsx:1114
+msgid "Filesystem outside the target"
+msgstr "ターゲット外のファイルシステム"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "このマウントポイントの下にはファイルシステムがマウント済みです。"
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "名前または説明による絞り込み"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "フィルターメニューアイテム"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "サービスの絞り込み"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "フィルター"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "フィンガープリント"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "ファイアウォール"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "ファイアウォールは利用できません"
+
+#: pkg/storaged/drive/drive.jsx:122
+msgid "Firmware version"
+msgstr "ファームウェアバージョン"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "NBDE サポートの修正"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "フォントのサイズ"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "実行が禁止されています"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "変更の強制"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "削除の強制"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "パスワード変更の強制"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "フォーマット"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "$0 のフォーマット"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "フォーマットおよびマウント"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Format and start"
+msgstr "フォーマットおよび起動"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "フォーマットのみ"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "フォーマットすると、ストレージデバイスのすべてのデータが削除されます。"
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "フォワード遅延 $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "フレーム番号"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "空き領域"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "フリーフォーム検索"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "毎週金曜日"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "ログイン元"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "フルネーム"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "ゲートウェイ"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "全般"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "生成しました"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "$0 に移動"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "アプリケーションへ移動"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "今すぐ移動"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "グラフ表示の可否"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "グラフ表示オプションメニュー"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "グループ"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "グループ名"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "グループ"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "増加"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "コンテンツを増やす"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "$0 の論理サイズを増加"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "論理ボリュームの増加"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "パーティションの拡張"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "すべての領域を使用してプールを拡大する"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "すべての領域を使用して増加"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "ヘアピンモード"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "ヘアピンモード"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "ハンドヘルド"
+
+#: pkg/storaged/drive/drive.jsx:64
+msgid "Hard Disk Drive"
+msgstr "ハードディスクドライブ"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "ハードウェア情報"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "ヘルス"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Hello タイム $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "ヘルプ"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "確認パスワードの非表示"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "パスワードの非表示"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "階層 ID"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "攻撃対象が増えるという代償を払ってでも、相互運用性を高めます。"
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "履歴パッケージ数"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "ホームディレクトリー"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "ホスト"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "ホスト鍵が正しくありません"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "ホスト名"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "ホスト"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "1 時間ごと"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "時"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "チェック方法"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+"内部エラー - この論理ボリュームはアクティブとマークされており、関連付けられた"
+"ブロックデバイスが必要です。しかし、そのようなブロックデバイスは見つかりませ"
+"ん。"
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP アドレス"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"ルーティングプレフィックス付きの IP アドレス。複数の値はコンマで区切ります。"
+"例: 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "IPv4 アドレス"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4 の設定"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6 の設定"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "識別子"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"空白のままの場合、関連付けられたポートサービスおよびポート番号に基づいて ID "
+"が生成されます"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"フィンガープリントが一致する場合は、Accept key and login をクリックします。一"
+"致しない場合は、ログインせずに、管理者にお問い合わせください。"
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"フィンガープリントが一致している場合は、'ホストを信頼して追加' をクリックしま"
+"す。一致していない場合は、接続せずに管理者にお問い合わせください。"
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "無視"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "内"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "ターミナルで以下を実行します: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"今後、$0 にパスワードなしで $1 としてログインできるようにするには、$3 上の "
+"$2 のログインパスワードをキーパスワードとして使用するか、キーパスワードを空欄"
+"のままにしてください。"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "同期"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "停止"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+msgid "Inactive logical volume"
+msgstr "非アクティブな論理ボリューム"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "含まれているサービス"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"受信リクエストはデフォルトでブロックされます。発信リクエストはブロックされま"
+"せん。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "一貫性のないシステムマウント"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "1 つ増やす"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "インデックスメモリー"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "間接"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "情報"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "情報以上のレベル"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "初期化"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "ディスク $0 を初期化します"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "初期化すると、ディスク上の全データが削除されます。"
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "初期化中..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Insights: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "インストール"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "NFS サポートをインストールする"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Stratis サポートをインストールする"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "すべてのアップデートをインストールする"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "アプリケーション情報をインストールする"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "cockpit-pcp をインストールする"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "kpatch の更新をインストールする"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "realmd サポートをインストールする"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "セキュリティーアップデートをインストールする"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr ""
+"setroubleshoot-server をインストールして SELinux イベントをトラブルシュートし"
+"ます。"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "ソフトウェアをインストール"
+
+#: pkg/kdump/kdump-view.jsx:571
+msgid "Install the $0 package."
+msgstr "$0 パッケージをインストールします。"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr ""
+"cockpit パッケージがインストールされていない状態でのインストールはサポート対"
+"象外です"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "インストール済み"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "インストール中"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "$0 をインストール中"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "$0 をインストールすると、$1 が削除されます。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "パッケージのインストール"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "インターフェース"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "インターフェースメンバー"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "インターフェース"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "内部エラー"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "内部エラー: 無効なチャレンジヘッダー"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "無効"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "無効なアドレス $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "無効な日付形式"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "無効な日付形式と無効な時間形式"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "無効な有効期限"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "無効なファイルパーミッション"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "無効な鍵"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "無効なメトリック $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "無効な日数"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "無効なポート番号"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "無効なプレフィックス $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "無効なプレフィックスまたはネットマスク $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "無効な範囲"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "無効な時間形式"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "無効なタイムゾーン"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "無効なユーザー名またはパスワード"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "IoT ゲートウェイ"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "sshd が別のポートで実行されていますか?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "ジョブ"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "参加"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "ドメインへの参加"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "ドメイン参加"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "参加する"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "ドメインに参加するには、realmd のインストールが必要です"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "このドメインの参加はサポートされていません"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "名前空間に参加"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "ジャーナル"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "ジャーナルエントリー"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "ジャーナルエントリーが見つかりません"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "次にジャンプ"
+
+#: pkg/kdump/kdump-view.jsx:569
+msgid "Kdump service is not installed."
+msgstr "Kdump サービスがインストールされていません。"
+
+#: pkg/kdump/kdump-view.jsx:604
+msgid "Kdump settings"
+msgstr "Kdump 設定"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "接続の保持"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "カーネルクラッシュダンプ"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr "カーネルが $0 設定を使用してブートしませんでした"
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "カーネルダンプ"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "カーネルライブパッチ $0 はアクティブです"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "カーネルライブパッチ $0 がインストールされています"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "カーネルライブパッチの設定"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "カーネルライブパッチ"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "キーパスワード"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "タイプ不明のキースロットは、ここでは編集できません"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "キーソース"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "キー"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "キーサーバー"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "キーサーバーのアドレス"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "キーサーバーの削除により、$0 のロック解除ができない可能性があります。"
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP キー"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "Active Directory との相互運用性があるレガシー。"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+msgid "LVM2 VDO pool"
+msgstr "LVM2 VDO プール"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+msgid "LVM2 logical volume"
+msgstr "LVM2 論理ボリューム"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+msgid "LVM2 logical volumes"
+msgstr "LVM2 論理ボリューム"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "LVM2 物理ボリューム"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+msgid "LVM2 physical volumes"
+msgstr "LVM2 物理ボリューム"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM2 ボリュームグループ"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "LVM2 ボリュームグループ $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr "ラベル"
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "ラップトップ"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "過去 24 時間"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "過去 7 日間"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "最後に操作を行った時間"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+msgid "Last cannot be removed"
+msgstr "最後のものは削除できません"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "最終確認: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+msgid "Last disk can not be removed"
+msgstr "最後のディスクは削除できません"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "最終ログイン"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "最終更新日: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "最後の正常ログイン:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "レイアウト"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "もっと詳しく"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "$0 の脱退"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "ドメインの脱退"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "暗号化をスキップするには空白のままにします"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "GNU LGPL バージョン 2.1 でのライセンス"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "ライト"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "アクセスの制限"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "制限付きアクセス"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"限定アクセスモードでは、管理者権限が制限されます。Web コンソールの一部の機能"
+"が低下します。"
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "制限"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "リニア"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "リンクダウンの遅延"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "ローカルリンク"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "リンクのモニタリング"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "リンクアップの遅延"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "リンクの監視"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "リンク済み"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+msgid "Linux filesystem data"
+msgstr "Linux ファイルシステムデータ"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+msgid "Linux swap space"
+msgstr "Linux スワップ領域"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "リッスン"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "リッスンポート"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "リッスンポートは数値である必要があります"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "ユニットの一覧表示"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "ユニットの一覧表示に失敗しました: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "ロード"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "ロードバランシング"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "以前のデータのロード"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "以前のエントリーのロード"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "利用可能なアップデートのロードに失敗しました"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "利用可能なアップデートをロード中です。しばらくお待ちください..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "以前のエントリーの読み込み"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "キーをロード中..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "SSH キーの読み込みに失敗しました"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "ユニットの読み込みに失敗しました"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "パッケージの読み込み中..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "システム変更をロード中..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "ユニットのロードに失敗しました"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "ロード中..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "ローカルアカウント"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "ローカルファイルシステム"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "ローカルマウントポイント"
+
+#: pkg/storaged/overview/overview.jsx:160
+msgid "Local storage"
+msgstr "ローカルストレージ"
+
+#: pkg/kdump/kdump-view.jsx:450
+msgid "Local, $0"
+msgstr "ローカル、$0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "場所"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "ロック"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "$0 のロック"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "アカウントのロック"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+msgid "Locked data"
+msgstr "ロックされたデータ"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "$target のロック中"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "ログイン"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "$0 へのログイン"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "サーバーのユーザーアカウントでログインします。"
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "ログメッセージ"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "ログアウト"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "ユーザーのログアウト"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "すでにログインしています"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "論理"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+msgid "Logical Volume Manager partition"
+msgstr "論理ボリュームマネージャーのパーティション"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "論理サイズ"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "論理ボリューム"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "論理ボリューム (スナップショット)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "$0 の論理ボリューム"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "ログイン"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "再ログイン"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "ログインが失敗しました"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "ログインフォーマット"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "ログイン履歴"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "ログイン履歴の一覧"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "$0 のログアウト"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "ログアウトが正常に行われました"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "ログ"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "低プロファイルデスクトップ"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "ランチボックス"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+msgid "MDRAID device"
+msgstr "MDRAID デバイス"
+
+#: pkg/storaged/utils.js:334
+msgid "MDRAID device $0"
+msgstr "MDRAID デバイス $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+msgid "MDRAID device is recovering"
+msgstr "MDRAID デバイスが復旧中です"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+msgid "MDRAID device must be running"
+msgstr "MDRAID デバイスが実行中である必要があります"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+msgid "MDRAID disk"
+msgstr "MDRAID ディスク"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+msgid "MDRAID disks"
+msgstr "MDRAID ディスク"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (推奨)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU は正の数値である必要があります"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "マシン ID"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "マシンSSH 鍵フィンガープリント"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "メインサーバーシャーシ"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "メンテナンス"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+"リモートマシン上の悪意のあるページが、接続されている他のホストに影響を与える"
+"可能性があります"
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "ファイルシステムの管理"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "ストレージの管理"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "管理対象インターフェース"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "LVM の管理"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "NFS マウントの管理"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "RAID の管理"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "VDO の管理"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "VLAN の管理"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "ファイアウォール管理"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "ネットワークボンディングの管理"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "ネットワークブリッジの管理"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "ネットワークチームの管理"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "パーティション管理"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "物理ドライブの管理"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "サービス管理"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "ソフトウェアアップデート管理"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "ユーザーアカウント管理"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "手作業"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "手動"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "$target を問題があるものとしてマークする"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "サービスのマスク"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "マスク"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"サービスをマスクすると、すべての依存するユニットが実行されなくなります。大き"
+"な影響が及ぶ可能性があります。このユニットをマスクすることを確認してくださ"
+"い。"
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "メッセージ最大期間 $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+msgid "Media drive"
+msgstr "メディアドライブ"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "メモリ"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "メモリーテクノロジー"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "メモリー使用状況"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "メモリー使用量/スワップ"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "マージ済み"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "ログインしているユーザーへのメッセージ"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "障害に関連するメッセージは、ジャーナルで見つかる場合があります:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "使用済みメタデータ"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "方法"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "メトリック"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "メトリックスおよび履歴"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "メトリックス履歴をロードできませんでした"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "メトリックス設定"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "ミニ PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "ミニタワー"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "分は 0〜59 の数字である必要があります"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "1 分間隔"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "分"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr "ミラー (RAID 1)"
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "緩和"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "モード"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "モデル"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "$target の変更"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "毎週月曜日"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "監視間隔"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "ターゲットの監視"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "毎月"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "詳細..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "マウント"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "マウントポイント"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "ネットワークが利用可能になった後にマウントし、失敗を無視します"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "起動時に自動的にもマウントします"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "起動時にマウント"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "起動時に自動的に $0 にマウントします"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "サービス起動前にマウントします"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "マウント設定"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "ファイルシステムをマウントします"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "今すぐマウントします"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "$0 に今すぐマウントします"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "マウントオプション"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "マウントポイント"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "マウントポイントは空欄にできません"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "マウントポイントは空欄にできません。"
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "マウントポイントはすでに $0 で使用しています"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "マウントポイントは \"/\" で開始してください。"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "読み取り専用でマウント"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "待機せずにマウントし、失敗を無視します"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "$target のマウント"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "サービス起動前にマウントします"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "サービスと並行してマウントします"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+"サービスと並行してマウントしますが、ネットワークが利用可能になった後にマウン"
+"トします"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "マルチシステムシャーシ"
+
+#: pkg/storaged/drive/drive.jsx:135
+msgid "Multipathed devices"
+msgstr "マルチパスデバイス"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+"コンマまたはスペースを区切り文字として使用すると、複数のアドレスを指定できま"
+"す。"
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS マウント"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP サーバー"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "名前"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "名前は空欄にできません。"
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "名前は空欄にすることができません。"
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "名前は、$0 バイトを超えることができません"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "名前は、$0 文字を超えることができません"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "名前は 127 文字を超えることができません。"
+
+#: pkg/storaged/btrfs/utils.jsx:87
+msgid "Name cannot be longer than 255 characters."
+msgstr "名前は 255 文字を超えることができません。"
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "名前には文字 '$0' を含めることができません。"
+
+#: pkg/storaged/btrfs/utils.jsx:89
+msgid "Name cannot contain the character '/'."
+msgstr "名前には文字 '/' を含めることができません。"
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "名前にはスペースを含めることができません。"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr "スペアディスクが必要です"
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "少なくとも 1 つの NTP サーバーが必要です"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "ネットワーク"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "ネットワーク I/O"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "ネットワークボンディング"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "ネットワークデバイスおよびグラフには、NetworkManager が必要です"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "ネットワークログ"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "ネットワーク使用量"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager はインストールされていません"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager は実行していません"
+
+#: pkg/storaged/overview/overview.jsx:168
+msgid "Networked storage"
+msgstr "ネットワークストレージ"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "ネットワーキング"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "しない"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "アカウントを失効しない"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "パスワードを失効しない"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "ログインされていません"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "NFS の新規マウント"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "新規ホスト"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "新しいホスト: $0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "新しいキーパスワード"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "新しい名前"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "新しいパスフレーズ"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "新規パスワード"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "新規パスワードは受け入れられませんでした"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "次へ"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "いいえ"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "ID が指定されていません"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "SELinux アラートがありません。"
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "アプリケーションがインストールされていないか、利用できません。"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "利用可能なスロットはありません"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "ブロックデバイスは利用できません。"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+msgid "No block devices found"
+msgstr "ブロックデバイスが見つかりません"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "キャリアなし"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "設定が見つかりません"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "利用可能なデータなし"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "$0 と $1 間で利用できるデータなし"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "遅延なし"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "利用可能な説明はありません"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "説明が入力されていません。"
+
+#: pkg/storaged/btrfs/volume.jsx:135
+msgid "No devices found"
+msgstr "デバイスが見つかりません"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "ディスクが利用できません。"
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+msgid "No disks found"
+msgstr "ディスクが見つかりません"
+
+#: pkg/storaged/iscsi/session.jsx:79
+msgid "No drives found"
+msgstr "ドライブが見つかりません"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "暗号化なし"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "イベントがありません"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "ファイルシステムなし"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "ファイルシステムがありません"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "フリーのキースロットはありません"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "空き領域なし"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "このパーティションの後に空き領域がありません"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "グループ名が指定されていません"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "ホストキーが見つかりません。"
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "このアプリケーションのインストールパッケージが見つかりませんでした。"
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "追加されたキーはありません"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "一致する言語がありません"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "ログエントリーなし"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "論理ボリュームなし"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "ログが見つかりません"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "一致する結果はありません"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "メディアが挿入されていません"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "パーティションなし"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+msgid "No partitions found"
+msgstr "パーティションが見つかりません"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "ピアが追加されていません。"
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+msgid "No physical volumes found"
+msgstr "物理ボリュームが見つかりません"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "実際の名前が指定されていません"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "結果が見つかりません"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"絞り込みの基準と一致する結果がありません。結果を見るためには全ての絞り込み基"
+"準を解除してください。"
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "ルールヒットなし"
+
+#: pkg/storaged/overview/overview.jsx:190
+msgid "No storage found"
+msgstr "ストレージが見つかりません"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+msgid "No subvolumes"
+msgstr "サブボリュームなし"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "このようなファイルまたはディレクトリーがありません"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "システム変更がありません"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "システムレポートがありません。"
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "更新なし"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "ユーザー名が指定されていません"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "なし"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "有効な秘密鍵ではありません"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "ファイアウォールを無効にする権限がありません"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "ファイアウォールを有効にする権限がありません"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "利用できません"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "接続していません"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Insights に接続されていません"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "ホストに接続されていません"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+msgid "Not enough free space"
+msgstr "十分な空き領域がありません"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "スペースが不足しています"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+msgid "Not enough space to grow"
+msgstr "拡張するのに十分な領域がありません"
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "見つかりません"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "インストールされていません"
+
+#: pkg/networkmanager/network-interface.jsx:680
+msgid "Not permitted to configure network devices"
+msgstr "ネットワークデバイスの設定が許可されていません"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "レルムの設定が許可されていません"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "この動作を実行する権限がありません。"
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "準備ができていません"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "登録されていません"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "実行中ではありません"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "設定されていません"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "同期されていません"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "注記"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "ノートブック"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "注意以上のレベル"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "ネットワークアドレス、ホスト名、およびユーザー名の難読化"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "難読化"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "発生件数 $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "$0〜$1 の発生件数"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "発生"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "OK"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "古いパスフレーズ"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "古いパスワード"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "古いパスワードは受け入れられません"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "マウントされたデバイス上"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "障害発生時"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Cockpit がインストールされたら、\"systemctl enable --now cockpit.socket\" コ"
+"マンドで有効にします。"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "$1 のうち $0 だけが使用されています。"
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "アルファベット、数字、:、 _ 、.、@、- のみを使用できます"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "緊急のみ"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"FIPS モードで起動する場合は、承認および許可されたアルゴリズムのみを使用してく"
+"ださい。"
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "問題が発生しました!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "ファイアウォールで pmproxy サービスを開き、メトリックを共有します。"
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "$target 上の操作 '$operation'"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "オプション"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "あるいは、バンドルされたブラウザーを使用します"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "その他"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"対話式パスワード認証が許可されない場合でも、他の認証方法を利用できます。"
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "他のオプション"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "外"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "概要"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "上書き"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "既存のデータをゼロで上書き (低速)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "メトリックス履歴に、パッケージ cockpit-pcp がありません"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "パッケージ情報"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit がクラッシュしました"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit はインストールされていません"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit が、エラーコード $0 を報告しました"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "パッケージ"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "ページ名"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "親"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "親 $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "一部"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "$0 の一部"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "パーティション"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "$0 のパーティション"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "パーティションサイズは $0 です。コンテンツサイズは $1 です。"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "パーティション構成"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "パーティション"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "パッシブ"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "パスフレーズ"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "パスフレーズは空欄にすることはできません"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "パスフレーズは空欄にすることができません"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "その他のキースロットのパスフレーズ"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "パスフレーズの削除で、$0 のロック解除ができない可能性があります。"
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "パスフレーズが一致しません"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "パスワード"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "パスワードが正しく変更されました"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "パスワードの有効期限"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "パスワードは 256 文字以内で設定する必要があります"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "パスワードは受け入れられません"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "パスワードが弱すぎます"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "パスワードを変更する必要があります"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "パスワードは受け入れられません"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "パスワードヒント"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "貼り付け"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "貼り付けエラー"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "既存の鍵を貼り付けます"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "公開 SSH 鍵ファイルの内容をここに貼り付けます"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "パス"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "パスコスト"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "パスコスト $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "サーバーのパス"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "サーバーのパスは空欄にはできません。"
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "サーバーのパスは \"/\" で開始してください。"
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "ディレクトリーへのパス"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "ファイルのパス"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "パス"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "一時停止"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+"ピア #$0 に無効なエンドポイントポートがあります。ポートは数値である必要があり"
+"ます。"
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"ピア #$0 に無効なエンドポイントがあります。host:port の形式で指定する必要があ"
+"ります。例: 1.2.3.4:51820、example.com:51820"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "ピア"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"ピアとは、このマシンに接続する他のマシンです。他のマシンの公開鍵が相互に共有"
+"されます。"
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"パフォーマンス Co-Pilot は、お使いのシステムからパフォーマンスメトリックスを"
+"収集して分析します。"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "パフォーマンスプロファイル"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "周辺機器シャーシ"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "永続"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "$0 グループを完全に削除しますか ?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "$0 を完全に削除しますか?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "論理ボリューム $0/$1 を完全に削除しますか?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+msgid "Permanently delete subvolume $0?"
+msgstr "サブボリューム $0 を完全に削除しますか?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "パーミッションが拒否されました"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "許容"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "物理"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "物理ボリューム"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "物理ボリューム"
+
+#: pkg/storaged/block/resize.jsx:279
+msgid "Physical volumes can not be resized here"
+msgstr "ここでは物理ボリュームのサイズを変更できません"
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "日付けの選択"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "ユニットを固定する"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Ping 間隔"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Ping ターゲット"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "固定されたユニット"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Pizza box"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "管理者アクセスを得るために認証を行ってください"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Web コンソールを使用するには JavaScript を有効にしてください。"
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "$0 パッケージをインストールしてください"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "問題を解決し、このページを再ロードしてください。"
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "有効期限を指定してください"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "接続するホストを指定してください"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "先にアンマウントしてください。"
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "シン論理ボリューム用プール"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "シンプロビジョニングされた LVM2 論理ボリューム用プール"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "シンプロビジョニングされたボリューム用プール"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "プールパスフレーズ"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "ポート"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "ポータブル"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "ポート"
+
+#: pkg/storaged/partitions/partition.jsx:116
+msgid "PowerPC PReP boot partition"
+msgstr "PowerPC PReP ブートパーティション"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "プレフィックス長"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "プレフィックス長 / ネットマスク"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "準備中"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "存在"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "保存"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "プリティホスト名"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "以前のブート"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "プライマリ"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "優先度"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "優先度 $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "秘密鍵"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "管理者への切り替えに問題が発生しました"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "問題の詳細"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "問題の情報"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "ロケーションを使用するプロセス"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "進行状況: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "プロジェクト Web サイト"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "passwd によるプロンプトがタイムアウトしました"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "ssh-add によるプロンプトがタイムアウトしました"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "ssh-keygen によるプロンプトがタイムアウトしました"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "再ロード先を伝搬"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr "相互運用性を犠牲にして、近い将来に予想される攻撃から保護します。"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "このブロックデバイス上のプールのパスフレーズを指定します:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "公開鍵"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr "有効な秘密鍵を入力すると公開鍵が生成されます"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "目的"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (ストライプ)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (ミラー)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (ミラーのストライプ)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (専用パリティー)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (分散パリティー)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (ダブル分散パリティー)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID シャーシ"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID レベル"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr "RAID10 には偶数個の物理ボリュームが必要です"
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "ラックマウントシャーシ"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "ランダム"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "範囲"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "範囲の順序は厳密でなければなりません"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "ランク"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "デバイスに対する Raw"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "読み取り"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "さらに読む..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "読み取り専用"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "読み取り中"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "読み取り中 ..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "準備ができています"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "準備ができています"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "実際のホスト名"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"実際のホスト名には小文字、数字、ダッシュ、およびピリオドのみを使用できます "
+"(入力されたサブドメインを含む)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "実際のホスト名は 64 文字以下である必要があります"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "再適用して再起動する"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "再起動"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "完了後に再起動する"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "再起動を推奨します"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "システムの再起動..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "受信中"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "直近のホスト"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "現在の脅威モデルに推奨される安全な設定。"
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "再接続"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "復旧"
+
+#: pkg/storaged/jobs-panel.jsx:70
+msgid "Recovering MDRAID device $target"
+msgstr "MDRAID デバイス $target の復旧"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "パッケージ情報の更新中"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "接続を拒否しています。ホストが不明です"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "接続を拒否しています。ホストキーが一致しません"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "接続を拒否しています。ホストキーが不明です"
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "再生成"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "initrd の再生成"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "登録中…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "関連するプロセスとサービスは強制的に停止されます。"
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "関連するプロセスは強制的に停止されます。"
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "関連するサービスは強制的に停止されます。"
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "再ロード"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "伝搬元を再ロード"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "リロード中"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "残りのサービス状態の再読み込み"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "CIFS/SMB 経由のリモート"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "FTP 経由のリモート"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "NFS 経由のリモート"
+
+#: pkg/kdump/kdump-view.jsx:456
+msgid "Remote over NFS, $0"
+msgstr "NFS 経由のリモート、$0"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "SFTP 経由のリモート"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "SSH 経由のリモート"
+
+#: pkg/kdump/kdump-view.jsx:453
+msgid "Remote over SSH, $0"
+msgstr "SSH 経由のリモート、$0"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "削除:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "削除"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "$0 を削除する"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "$1 ゾーンから $0 サービスを削除する"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "$0 を削除しますか?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Tang キーサーバーを削除しますか?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "デバイスの削除"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "ホストの削除"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "アイテムの削除"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "見つからない物理ボリュームを削除しますか?"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "キースロットのパスフレーズ $0 を削除しますか?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "パスフレーズを削除しますか?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "サービス $0 を削除します"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "ゾーン $0 を削除します"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "削除中"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "$0 を削除中"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 を削除すると、サーバーへの接続が切断され、管理 UI が利用できなくなります。"
+
+#: pkg/storaged/jobs-panel.jsx:66
+msgid "Removing $target from MDRAID device"
+msgstr "$target を MDRAID デバイスから削除"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"別のパスフレーズの確認なしでパスフレーズを削除すると、その他のパスフレーズを"
+"忘れたり、紛失した場合に、アンロックやキー管理ができなくなることがあります。"
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "$target から物理ボリュームを削除"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Cockpit サービスを削除すると、web コンソールにアクセスできなくなる可能性があ"
+"ります。このゾーンが現在お使いの web コンソール接続に適用されないことを確認し"
+"てください。"
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "ゾーンを削除すると、そのゾーン内のすべてのサービスが削除されます。"
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "名前変更"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Stratis プールの名前変更"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "ファイルシステムの名前変更"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "グループの名前変更"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "グループ $0 の名前変更"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "論理ボリュームの名前変更"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "ボリュームグループの名前変更"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "$target の名前変更"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr ""
+"グループの名前を変更すると、sudo や同様のルールに影響が及ぶ可能性があります"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr "修復"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+msgid "Repair logical volume $0"
+msgstr "論理ボリューム $0 の修復"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "$target の修復"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "繰り返し"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "毎月繰り返す"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "パスフレーズの繰り返し"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "毎週繰り返す"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "レポート"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "レポートラベル"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "ABRT アナリティクスへ報告します"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "報告済み、利用可能なリンクはありません"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "報告に失敗しました"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "報告はキャンセルされました"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "レポート"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "レポート:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "$0 日ごとのパスワードの変更が必要"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "$0 でパスワードの変更が必要"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "初回ログイン時にパスワードの変更が必要"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "必要とされる"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "以下により必要 "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "必須"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "編集には管理者アクセスが必要"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "必須"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "必須の"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+"カーネルコマンドラインに '$0' オプションを設定して、ブート時にメモリーを予約"
+"してください。たとえば、'$1' を $3 の $2 に追加するか、ディストリビューション"
+"のカーネル引数エディターを使用します。"
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "予約済みメモリー"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "リセット"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "パスワードのリセット"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "$target のサイズ変更"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"暗号化されたファイルシステムのサイズ変更には、ディスクのロック解除が必要で"
+"す。現在のディスクのパスフレーズを提供してください。"
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "再起動"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "サービスの再起動"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "サービスの再起動..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "再起動中"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "接続の復元"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"kdump が正しく設定されている場合は、クラッシュの結果が $0 から $1 に $2 とし"
+"てコピーされます。"
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+"kdump が正しく設定されている場合は、クラッシュの結果が $0 に $1 として保存さ"
+"れます。"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "再開"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "既存の暗号化を再利用"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "既存の暗号化を再利用 ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "暗号化ポリシーを確認する"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "ログのレビュー"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "ラウンドロビン"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "ルート"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "実行時刻"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "新規レポートを実行する"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "以下で実行"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "レポートを実行する"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+"信頼できるネットワーク経由で、またはリモートマシンで直接、次のコマンドを実行"
+"します:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "ランナー"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "実行中"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "ランタイム"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux アクセス制御エラー"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux はシステムで無効です"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux はシステムで無効です。"
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux ポリシー"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "SELinux システムステータスが不明です。"
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "SELinux トラブルシュート"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "$target の SMART 自己テスト"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr "SOS レポートは、問題の診断に役立つシステム情報を収集します。"
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH キー"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "SSH キーはパスではありません"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH キー"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "STP フォワード遅延"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "STP Hello タイム"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP メッセージ最大期間"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP 優先度"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Safari ユーザーは、自己署名 CA 証明書をインポートし、信頼する必要があります:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "毎週土曜日"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "保存"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "保存および再起動"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "変更の保存"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "LZ4 で個別のブロックを圧縮して空き領域を節約します"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "同一のデータブロックを 1 回だけ保存することで、空き領域を節約します"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"新しいパスフレーズの保存には、ディスクのロック解除が必要です。現在のディスク"
+"のパスフレーズを提供してください。"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "電源オフが $0 に予定されています"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "再起動が $0 に予定されています"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "シールドケース PC"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "検索"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "検索ドメイン"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "名前または ID の検索"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "名前、グループ、または ID の検索"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "秒は 0〜59 の数字でなければなりません"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "秒"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "安全なシェルキー"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "$target を安全に削除"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Security Enhanced Linux の設定とトラブルシューティング"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "セキュリティーの更新を利用できます"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "セキュリティー更新のみ"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "$0 $1 にセキュリティーアップデートが適用されます"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "選択"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "識別子の選択"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "メソッドの選択"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+"論理ボリュームの修復に使用する物理ボリュームを選択します。$0 以上が必要です。"
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "送信"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "送信中"
+
+#: pkg/storaged/drive/drive.jsx:123
+msgid "Serial number"
+msgstr "シリアルナンバー"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "サーバー"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "サーバーアドレス"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "サーバーアドレスは空欄にできません。"
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "サーバーは空欄にできません。"
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "サーバーの接続が終了しました。"
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "サーバーソフトウェア"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "サービス"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "サービスでエラーが発生しました"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "サービスログ"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "サービス"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "ロケーションを使用するサービス"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "セッション"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "セッションはまもなく終了します"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "セット"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "ホスト名を設定します"
+
+#: pkg/storaged/partitions/partition.jsx:173
+msgid "Set partition type of $0"
+msgstr "$0 のパーティションタイプの設定"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "パスワードの設定"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "時間の設定"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "設定値"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "設定済み"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "脆弱なパスワードの設定"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr "設定が設定された状態と異なるため、次回起動時に元の状態に戻ります。"
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "設定中"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "ループデバイス $target の設定"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "設定"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "重大度"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "共有"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "シェル"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "シェルスクリプト"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+msgid "Show all $0 rows"
+msgstr "$0 行をすべて表示"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "すべてのスレッドの表示"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Show confirmation password"
+msgstr "確認パスワードの表示"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "フィンガープリントの表示"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "与えられた文字列が含まれるメッセージを表示します。"
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "指定した systemd ユニットのメッセージを表示します。"
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "特定のブートのメッセージを表示します。"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "その他の関係の表示"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "パスワードを表示する"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "関係の表示"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "縮小"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "論理ボリュームの縮小"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "パーティションの縮小"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "ボリュームの縮小"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "シャットダウン"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "シャットダウン"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "フィルター開始時刻"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "シングルランク"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "サイズ"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "サイズはマイナスにすることができません"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "サイズはゼロにすることができません"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "サイズが大きすぎます"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "サイズは数値である必要があります"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "サイズは $0 以上にする必要があります"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "メインナビゲーションをスキップします"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "コンテンツへスキップ"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "スロット"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "スロット $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "スナップショット"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "ソケット"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "ソフトウェア更新"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"ソフトウェアベースの回避策により、CPU セキュリティー問題を回避します。これら"
+"の緩和策にはパフォーマンスの低下という副次的な影響があります。リスクをご理解"
+"の上、これらの設定を変更してください。"
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr "ソリッドステートドライブ"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "ソリューションが正常に適用されました"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "ソリューションが失敗しました"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "ソリューション"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"プールの作成後に、このプールのブロックデバイスでサイズが増大したものがありま"
+"す。このプールを安全に拡張するには新たに利用可能な領域を使用してください。"
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"別のプログラムがパッケージマネージャーを使用中です。しばらくお待ちください..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "一部のソフトウェアを手動で再起動する必要があります"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr "申し訳ございません"
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "信頼性が最も低い順から最も高い順"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "省スペースコンピューター"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "スパニング ツリープロトコル"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "スパニング ツリープロトコル (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "スペア"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "特定の時間"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "速度"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "安定"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "起動"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "起動と有効化"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "マルチパスの開始"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "サービスの起動"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr "指定の日付以降のエントリーまたは新しいエントリーを表示します。"
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "指定の日付以前のエントリーまたはそれ以前のエントリーを表示します。"
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "開始"
+
+#: pkg/storaged/jobs-panel.jsx:64
+msgid "Starting MDRAID device $target"
+msgstr "MDRAID $target デバイスの起動"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "スワップ領域 $target の起動"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "状態"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "静的"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "ステータス"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "スティッキー PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "スティッキー"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "停止"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "停止と無効化"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "停止して削除"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "停止してアンマウント"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "デバイスの停止"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "ホストの編集を停止"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "レポートを停止する"
+
+#: pkg/storaged/jobs-panel.jsx:63
+msgid "Stopping MDRAID device $target"
+msgstr "MDRAID デバイス $target の停止"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "スワップ領域 $target の停止"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "ストレージ"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "ストレージは、このシステムで管理できません。"
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "ストレージログ"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "パスフレーズの保存"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "保存されたパスフレーズ"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+msgid "Stratis block device"
+msgstr "Stratis ブロックデバイス"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+msgid "Stratis block devices"
+msgstr "Stratis ブロックデバイス"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Stratis blockdevs は縮小できません"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+msgid "Stratis filesystem"
+msgstr "Stratis ファイルシステム"
+
+#: pkg/storaged/stratis/pool.jsx:300
+msgid "Stratis filesystems"
+msgstr "Stratis ファイルシステム"
+
+#: pkg/storaged/stratis/pool.jsx:361
+msgid "Stratis filesystems pool"
+msgstr "Stratis ファイルシステムプール"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Stratis プール"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr "ストライプ (RAID 0)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr "ストライプおよびミラー (RAID 10)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr "ストライプ"
+
+#: pkg/lib/cockpit-components-password.jsx:97
+msgid "Strong password"
+msgstr "強固なパスワード"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "スタブ"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "スタイル"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "サブシャーシ"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "サブノート"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "systemd シグナルへのサブスクライブに失敗しました: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+msgid "Subvolume needs to be mounted"
+msgstr "サブボリュームをマウントする必要があります"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr "サブボリュームは書き込み可能でマウントする必要があります"
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "クリップボードへのコピーに成功しました"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "クリップボードへのコピーに成功しました!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "毎週日曜日"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "スワップ"
+
+#: pkg/storaged/block/resize.jsx:292
+msgid "Swap can not be resized here"
+msgstr "ここではスワップのサイズを変更できません"
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "スワップアウト"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "$0 の切り替え"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "$0 をオフにする"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "$0 をオンにする"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "管理者アクセスへの切り替え"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "アクセス制限への切り替え"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 をオフにすると、サーバーへの接続が切断され、管理 UI が利用できなくなりま"
+"す。"
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 をオンにすると、サーバーへの接続が切断され、管理 UI が利用できなくなりま"
+"す。"
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "同期済み"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "$0 と同期済み"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "同期中"
+
+#: pkg/storaged/jobs-panel.jsx:71
+msgid "Synchronizing MDRAID device $target"
+msgstr "MDRAID デバイス $target の同期"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "システム"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "システムの診断"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "システム情報"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "システムは最新の状態です"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "システム変更"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "システム時間"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "システム単位"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "タブレット"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang キーサーバー"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "ターゲット"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "ターゲット"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "チーム"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Team ポート"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "チームポート設定"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "端末"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "セッションの終了"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "設定のテスト"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "テストは kdump サービスが実行中の間だけ利用可能です。"
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "kdump 設定のテスト"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"カーネルをクラッシュして kdump 設定をテストします。処理にしばらく時間がかか"
+"り、システムが自動的に再起動されない場合があります。重要なタスクを実行中に、"
+"システムを意図的にクラッシュさせないようにしてください。"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "接続のテスト"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "$0 パッケージは、あらゆるリポジトリーから利用できません。"
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr ""
+"Stratis デバイスを作成するために $0 パッケージをインストールする必要がありま"
+"す。"
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "$0 パッケージがインストールされている必要があります。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "$0 パッケージがインストールされ、VDO デバイスを作成します。"
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP アドレスまたはホスト名には空白を含めることができません。"
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+msgid "The MDRAID device is in a degraded state"
+msgstr "MDRAID デバイスがデグレード状態です"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+msgid "The MDRAID device must be running"
+msgstr "MDRAID デバイスが実行中である必要があります"
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr "$2 の $1 の SSH キー $0 は、$5 上の $4 の $3 ファイルに追加されます。"
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"SSH キー $0 はセッションの残りの時間に利用できるようになり、他のホストにもロ"
+"グインできるようになります。"
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"$0 にログインするための SSH キーはパスワードで保護され、パスワードによるログ"
+"インを許可しません。$1 にキーのパスワードを指定してください。"
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"$0 にログインするための SSH キーは保護されています。ログインパスワードでログ"
+"インするか、$1 で鍵のパスワードを提供することでログインできます。"
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr "アカウント '$0' が次回ログインする際に、パスワードの変更が求められます"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Cockpit サービスは自動的に含まれます"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr "設定された状態が不明です。状態は次回の起動時に変わることがあります。"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr "この VDO デバイスの作成は終了していないため、使用できません。"
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"現在ログイン中のユーザーは、キーに関する情報を見ることを許可されていません。"
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"ディスクをフォーマットする前にロックを解除する必要があります。既存のパスフ"
+"レーズを指定してください。"
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "ファイル $0 が削除されます。"
+
+#: pkg/storaged/filesystem/utils.jsx:191
+msgid "The filesystem has no assigned mount point."
+msgstr "ファイルシステムにマウントポイントが割り当てられていません。"
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "ファイルシステムには永続的なマウントポイントがありません。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"ファイルシステムは起動時に自動的にマウントされるように設定されていますが、そ"
+"の暗号化コンテナーはその時点ではアンロックされません。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"ファイルシステムは現在マウントされていますが、次回のブート後はマウントされま"
+"せん。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"ファイルシステムは現在 $0 にマウントされていますが、次回のブート時に $1 にマ"
+"ウントされます。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"ファイルシステムは現在 $0 にマウントされていますが、次回のブート後はマウント"
+"されません。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"ファイルシステムは現在マウントされていませんが、次回のブート時にマウントされ"
+"ます。"
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "ファイルシステムはマウントされていません。"
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"ファイルシステムはロック解除され、次回の起動時にマウントされます。パスフレー"
+"ズの入力が必要になる場合があります。"
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "フィンガープリントは次のものと一致する必要があります:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "以下のサービスが再起動します:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "フルネームにはコロンを含めることはできません。"
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "グループ ID は正の整数である必要があります"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"グループ名には、a-z、数字、ドット、ダッシュ、およびアンダースコアだけが指定で"
+"きます"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"ホームディレクトリー $0 はすでに存在します。その所有権が新しいユーザーに変更"
+"されます。"
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "initrd を再生成する必要があります。"
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "キーのパスワードは空にすることはできません"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "キーのパスワードが一致しません"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "提供した鍵が有効ではありません。"
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "最後のキースロットは削除できません"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "一覧表示されたプロセスとサービスは強制的に停止されます。"
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "一覧表示されたプロセスは強制的に停止されます。"
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "一覧表示されたサービスは強制的に停止されます。"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "ログインしているユーザーには、システム変更を表示する権限がありません"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "マシンが再起動中です"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "マウントポイント $0 は、以下のプロセスにより使用されています:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "マウントポイント $0 は、以下のサービスにより使用されています:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "新しいキーのパスワードは空にすることはできません"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "パスワードは空にできません"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "パスワードが一致しません"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "パスワードが一致しません。"
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"作成されたフィンガープリントは、電子メールを含むパブリックメソッドを介して共"
+"有すると問題ありません。"
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"生成されたフィンガープリントは、メールなどのパブリックな方法で共有しても問題"
+"ありません。他の人に検証を依頼した場合、その人は任意の方法で結果を送信できま"
+"す。"
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"サーバーはパスワード認証を使用した '$0' の認証を拒否しました。サポートされた"
+"他の認証方法は利用できません。"
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr "サーバーはサポートされた方法を使用した認証を拒否しました。"
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"現在、このシステムでは、起動時に Tang 鍵サーバーを使用したファイルシステムの"
+"ロック解除をサポートしていません。"
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"現在、このシステムは Tang 鍵サーバーを使用したルートファイルシステムのロック"
+"解除をサポートしていません。"
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr "ユーザー $0 は、cpu セキュリティー緩和の変更を許可されていません"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "ユーザー $0 は、暗号化ポリシーの変更を許可されていません"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "ユーザー $0 は、カーネルクラッシュのテストを許可されていません"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"新しい設定を有効にするには、ユーザーはログアウトしてログインし直す必要があり"
+"ます。"
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"ユーザー名は a〜z の文字、数字、ドット、ダッシュ、およびアンダースコアだけで"
+"構成されます。"
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"Web ブラウザーの設定により、Cockpit の実行は防がれます (アクセスできない $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "現在アクティブなページはありません"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"システムに複数のパスを持つデバイスがありますが、マルチパスサービスが実行され"
+"ていません。"
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "このゾーンにはアクティブなサービスがありません"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "このアカウントに承認された公開鍵がありません。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+"修復に使用できる十分な領域がありません。この論理ボリューム用に使用されていな"
+"い領域が、物理ボリュームに $0 以上必要です。"
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"このプールには、このファイルシステムのスナップショットを作成するための十分な"
+"領域がありません。少なくとも $0 が必要ですが、利用可能な領域は $1 のみです。"
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "マシンへの接続中に予期しないエラーが発生しました。"
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "以下の追加手順が必要です:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "以下の変更が行われます:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "シン論理ボリューム"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "シンプロビジョニングされた LVM2 論理ボリューム"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"この MDRAID デバイスには write-intent ビットマップがありません。このビット"
+"マップを使用すると、同期時間を大幅に短縮できます。"
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr "この NFS マウントは使用中で、そのオプションだけを変更できます。"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "この VDO デバイスは、そのバッキングデバイスをすべて使用していません。"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+msgid "This device can not be used for the installation target."
+msgstr "このデバイスはインストール対象として使用できません。"
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "このデバイスはここで管理できません。"
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "このデバイスは現在使用されています。"
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "このフィールドは必須の項目です"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "このグループは、以下のユーザーのプライマリーグループです:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "このホストは、更新後に再起動します。"
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "この情報は、システムにのみ保存されます。"
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"このキーサーバーは、プールのロック解除する唯一の手段であるため、削除できませ"
+"ん。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+"この論理ボリュームは一部の物理ボリュームを失ったため、使用できなくなりまし"
+"た。この論理ボリュームを削除して、代わりに新しいものを作成する必要がありま"
+"す。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+"この論理ボリュームは一部の物理ボリュームを失いましたが、データはまだ失われて"
+"いません。元の冗長性を復元するには、この論理ボリュームを修復する必要がありま"
+"す。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+"この論理ボリュームは一部の物理ボリュームを失いましたが、データはまだ失われて"
+"いない可能性があります。この論理ボリュームを修復できる可能性があります。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "この論理ボリュームは、コンテンツによって完全には使用されていません。"
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "このマシンはすでに追加されています。"
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "これにはしばらく時間がかかることがあります"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "このパーティションは、コンテンツによって完全に使い切られていません。"
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+"このパスフレーズは、プールのロックを解除するための唯一の手段であるため、削除"
+"できません。"
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr ""
+"このプールは、プールのブロックデバイスにある領域をすべて使用していません。"
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "このプールは、degraded の状態にあります。"
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "このシステムは登録されていません"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "このシステムはカスタムプロファイルを使用しています"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "このシステムは推奨プロファイルを使用しています"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"このツールは、SELinux ポリシーを設定します。また、ポリシー違反の把握と解決に"
+"役立ちます。"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"このツールは、カーネルクラッシュダンプをディスクに書き込むようにシステムを設"
+"定します。"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"このツールは、実行中のシステムから設定および診断情報のアーカイブを生成しま"
+"す。アーカイブは、記録や追跡の目的でローカルまたは一元的に保存することも、技"
+"術的な障害の発見やデバッグを支援するためにテクニカルサポート担当者、開発者、"
+"システム管理者に送信することもできます。"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"このツールは、ファイルシステム、LVM2 ボリュームグループ、NFS マウントなどの"
+"ローカルストレージを管理します。"
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"このツールは、NetworkManager と Firewalld を使用して、ボンディング、ブリッ"
+"ジ、チーム、VLAN、ファイアウォールなどのネットワーク設定を管理します。"
+"NetworkManager は、Ubuntu のデフォルトの systemd-networkd および Debian の "
+"ifupdown スクリプトと互換性がありません。"
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "このユニットは明示的に有効にするよう設計されていません。"
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "このユーザー名はすでに存在します"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr "このボリュームグループの一部の物理ボリュームがありません。"
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"この Web ブラウザーは古いため、Web コンソールを実行できません ($0 が不明)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"これにより、_BOOT_ID=' 一致を追加します。指定のない場合は、現在のブートのログ"
+"が表示されます。ブート ID を省略すると、正のオフセットはジャーナルの開始から"
+"ブートを探します。また、ゼロまたはゼロ未満のオフセットは、ジャーナルの最後か"
+"らブートを探します。したがって、1 は時系列順でジャーナルで見つかった最初の"
+"ブートを意味し、2 は 2 番目のブートを意味します (以下、同様)。また、-0 は最後"
+"のブート、-1 は最後の前のブートとなります。"
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"これにより、'_SYSTEMD_UNIT='、'COREDUMP_UNIT='、'UNIT=' と一致させ、指定のユ"
+"ニットに対する考えられるメッセージをすべて特定します。複数のユニットをコンマ"
+"で区切ることができます。 "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "これにより、今後はパスワードなしでログインできるようになります。"
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"このゾーンには Cockpit サービスが含まれます。このゾーンが現在の web コンソー"
+"ル接続に適用されないことを確認してください。"
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "毎週木曜日"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "階層"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "時間"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "タイムゾーン"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "タイマーの作成に失敗しました"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "タイマーの削除に失敗しました"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "タイマー"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"ヒント: 他のシステムに対して自動的に認証する場合は、鍵のパスワードをログイン"
+"パスワードに一致させます。"
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"悪意のあるサードパーティーによって接続がインターセプトされないようにするに"
+"は、ホストキーフィンガープリントを確認してください:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"ソフトウェアアップデートを取得するには、このシステムを Red Hat に登録する必要"
+"があります。登録には、Red Hat カスタマーポータルまたはローカルのサブスクリプ"
+"ションサーバーを使用します。"
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"フィンガープリントを確認するには、マシン上に物理的に置かれるか、信頼できる"
+"ネットワークを介して $0 で次のコマンドを実行します:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "今日"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "切り替え"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "日付選択の切り替え"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "フィルターの切り替え"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "データが多すぎます"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "ツール"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "CPU 使用率が高い上位 5 サービス"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "ディスク使用量が多い上位 5 サービス"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "メモリー使用量が多い上位 5 サービス"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "合計サイズ: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "タワー"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "一時的"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "送信中"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "トリガー"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "トリガー元"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "トリガー"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "トラブルシュート"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "トラブルシュート…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "ホストを信頼して追加"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "キーを信頼します"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "信頼レベル"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "再試行します"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "$0 との同期を試行中です"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "毎週火曜日"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned の起動に失敗しました"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned は、システムを監視し、特定のワークロードでパフォーマンスを最適化する"
+"サービスです。Tuned のコアは、さまざまなユースケースに合わせてシステムを調整"
+"するプロファイルです。"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned が利用できません"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned が実行中ではありません"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned がオフです"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "管理者アクセスをオンにする"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "タイプ"
+
+#: pkg/storaged/partitions/partition.jsx:165
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr ""
+"タイプには、0 から 9、A から F、および \"-\" のみを含めることができます。"
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+"タイプは、NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN の形式である必要があります。"
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+"タイプには 16 進文字 (0 から 9、A から F) が 2 つ含まれている必要があります。"
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "フィルターのために入力"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "このソリューションを自動的に適用できません"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "そのアドレスに接続できません"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "$0 と通信できません。"
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"該当するホスト $0 に接続できませんでした。そのホストのポート $1 で ssh が実行"
+"されていることを確認するか、アドレスで別のポートを指定します。"
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "アラート詳細を取得できません。"
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"SSH キー認証で $0 にログインできません。パスワードを入力してください。SSH "
+"キーを自動ログイン用に設定しておいてください。"
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"$0 にログインできません。ホストが、パスワードログインまたは SSH キーを受け付"
+"けていません。"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "サーバーに到達できません"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "マウントを削除できません"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+msgid "Unable to repair logical volume $0"
+msgstr "論理ボリューム $0 を修復できません"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "修正を実行できません: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "設定を保存できません"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "設定を保存できません: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "setroubleshootd を起動できません"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "ファイルシステムをアンマウントできません"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "利用できません"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "利用できないパッケージ"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "元に戻す"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "$0 のインストール時に予期しない PackageKit エラーが発生しました: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "予期しないエラー"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+msgid "Unformatted data"
+msgstr "未フォーマットのデータ"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "ユニット"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "不明"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "不明な \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "不明な ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "不明なアプリケーション"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "不明な設定"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "不明なホスト名"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "不明なサービス名"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "不明なタイプ"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "ロック解除"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "システムの起動時に自動ロック解除"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr "サイズ変更前のロック解除"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "暗号化 Stratis プールのロック解除"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "ロック解除キー $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "$target をロック解除中"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "ディスクのロック解除"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "未管理のインターフェース"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "アンマウント"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "ファイルシステム $0 のアンマウント"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "今すぐアンマウントします"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "$target のアンマウント中"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "名前なし"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "ユニットの固定を解除する"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "認識されないデータ"
+
+#: pkg/storaged/block/resize.jsx:306
+msgid "Unrecognized data can not be made smaller here"
+msgstr "ここでは、認識されないデータを小さくすることはできません"
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "ここでは、認識されないデータを小さくすることはできません。"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+msgid "Unsupported logical volume"
+msgstr "サポートされていない論理ボリューム"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "フィルター終了時刻"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "信用できないホスト"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "更新"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "更新成功テーブル"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "履歴の更新"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "パッケージ情報アップデート"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "更新が正常に実行されました"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "更新済み"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr "パッケージの更新を反映するには、再起動が必要になる場合があります。"
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "更新を利用できます"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "更新履歴"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "$0 $1 に更新が適用されます"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "更新中"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "ステータスを更新中..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "稼働時間"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "使用率"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "$0 の使用率"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "使用"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "圧縮の使用"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "重複排除の使用"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "キーの使用"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "ユーザーパスワード"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "他のシステムに対して認証する場合は次の鍵を使用します"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "詳細なログを使用する"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "使用済み"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"オプションのマウントや対話が必要なマウント (パスフレーズなど) に役立ちます"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "ユーザー"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "ユーザー ID"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "ユーザー ID が別のユーザーによってすでに使用されています"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "ユーザー ID は正の整数である必要があります"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "ユーザー ID は $0 よりも大きくすることはできません"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "ユーザー ID は $0 よりも小さくすることはできません"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "ユーザー名"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "ユーザー名は空にできません"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "ユーザー名"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "LUKS 暗号化の使用"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Tang サーバーの使用"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "VDO バッキングデバイスを小さくすることはできません"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO デバイス $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "VDO ファイルシステムボリューム (圧縮/重複排除)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN ID"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "アドレスの検証"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "認証トークンの検証"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "ベンダー"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "検証済み"
+
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "フィンガープリントの検証"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "キーを検証します"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "検証中"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "バージョン"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "$target を非常に安全に削除"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "すべての CPU を表示する"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "すべてのディスクの表示"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "すべてのログの表示"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "すべてのサービスの表示"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "オートメーションスクリプトの表示"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "詳細なログを表示する"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "ハードウェアの詳細の表示"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "ログイン履歴の表示"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "ログの表示"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "メトリックスおよび履歴の表示"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "ディスクごとのスループットの表示"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "プロジェクト Web サイトの表示"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "レポートの表示"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "更新ログの表示"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "メモリー情報を表示するには、管理アクセスが必要です。"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "ファイアウォールへのアクセス"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "ボリュームグループ"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+msgid "Volume group is missing physical volumes"
+msgstr "ボリュームグループに物理ボリュームがありません"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "ボリュームサイズは $0 です。コンテンツサイズは $1 です。"
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "待機中"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "詳細を待機中..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "入力を待機中…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"パッケージマネージャーを使用して、その他のプログラムを終了するのを待機中..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "他のソフトウェア管理オペレーションが終了するまで待機中"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "開始を待機中…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "以下で必要"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "必要"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "警告以上のレベル"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+msgid "Weak password"
+msgstr "脆弱なパスワード"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Webコンソール"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Linux サーバー用 Web コンソール"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Web コンソールが再起動します"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Web コンソールが制限付きアクセスモードで実行されています。"
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Web コンソールロゴ"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "毎週水曜日"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "毎週"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "週"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "条件"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "空の場合は、現在のユーザーに接続します"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Web コンソールを再起動すると、進捗情報が表示されなくなります。ただし、更新プ"
+"ロセスはバックグラウンドで続行されます。更新プロセスの監視を継続するには、再"
+"接続します。"
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"このオプションが選択されている場合は、新しいプールでオーバープロビジョニング"
+"できません。対象のプールで作成する最大サイズを各ファイルシステムごとに指定す"
+"る必要があります。ファイルシステムは、作成後に拡張できません。全スナップ"
+"ショットの割り当ては作成時に行われます。それぞれの最大サイズの合計は、プール"
+"のサイズを超過できません。こうすることで、このプール内のファイルシステムで予"
+"想外に領域が不足しないようにするという利点があります。欠点としては、各システ"
+"ムの最大サイズと、スナップショットの作成には上限があることを事前に把握してお"
+"く必要があります。"
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "白"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "\"自動\" に設定されます"
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+msgid "World wide name"
+msgstr "World wide name"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "書き込み"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "多くは書き込み"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "書き込み中"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "ユーザー名またはパスワードが間違っています"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "毎年"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "はい"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "初めて $0 に接続しています。"
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "ファイアウォールを修正する権限がありません。"
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr "このアカウントに承認された公開鍵を表示するパーミッションがありません。"
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "アクティビティーがないためログアウトされました。"
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "古いエントリーのロードを試行することができます。"
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr "自動ログイン用の鍵のパスワードの変更が必要な場合があります。"
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "パスワードを変更するにはより長い時間の経過が必要です"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr "メトリックス履歴を表示するには、ログインし直す必要があります"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "これで管理者アクセスが可能になりました。"
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "$0 秒後にログアウトされます。"
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "お使いのアカウント"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"お使いのブラウザーでは、コンテキストメニューからの貼り付けが許可されていませ"
+"ん。Shift+Insert を使用できます。"
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "ブラウザーはセッション間のアクセスレベルを記憶します。"
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr "サーバーがまもなく接続を閉じます。再起動したら再接続できます。"
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "セッションが終了しました。"
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "セッションの有効期限が切れました。再度ログインしてください。"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "ゾーン"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[バイナリーデータ]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[データなし]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "アクセス"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "アクティブ"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "アドオン"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "アドオン"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "ネットワークの後"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "アプリ"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "アセットタグ"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "時間"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "平均: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "VDO デバイスのバッキングデバイス"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "BIOS"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "ボンディング"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "ブート"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "ブリッジ"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+msgid "btrfs device"
+msgstr "btrfs デバイス"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+msgid "btrfs devices"
+msgstr "btrfs デバイス"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+msgid "btrfs subvolume"
+msgstr "btrfs サブボリューム"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr "$1 の btrfs サブボリューム $0"
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+msgid "btrfs subvolumes"
+msgstr "btrfs サブボリューム"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+msgid "btrfs volume"
+msgstr "btrfs ボリューム"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "バグ修正"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "バイト"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+msgid "cache"
+msgstr "キャッシュ"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "変更"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman がインストールされていません"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "コマンド"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "コンソール"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "コアダンプ"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "CPU"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "クラッシュ"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+msgid "data"
+msgstr "データ"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "日付"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+msgid "deactivate"
+msgstr "非アクティブ化"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "デバッグ"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "削除"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr "btrfs ボリュームのデバイス"
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "無効にする"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "ディスク"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "ディスク"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "ドメイン"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "ドライブ"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "編集"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "有効にする"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+msgid "encrypted"
+msgstr "暗号化"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "暗号化"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "機能強化"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "エラー"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "毎週金曜日"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "毎週月曜日"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "毎週土曜日"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "毎週日曜日"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "毎週木曜日"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "毎週火曜日"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "毎週水曜日"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "毎日"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "拡張"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "ssh ホスト鍵の一覧表示に失敗しました: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "ファイルシステム"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "ファイアウォール"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "現在および将来のカーネルを対象とする"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "現在のカーネルのみを対象とする"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "フォーマット"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "$0 から"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "$1 の $0 から"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "グラフ"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "増加"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "ハードウェア"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "履歴"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "ホスト"
+
+#: pkg/storaged/drive/drive.jsx:65
+msgid "iSCSI Drive"
+msgstr "iSCSI ドライブ"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+msgid "iSCSI drives"
+msgstr "iSCSI ドライブ"
+
+#: pkg/storaged/iscsi/session.jsx:45
+msgid "iSCSI portal"
+msgstr "iSCSI ポータル"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "失敗を無視"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "多くのブラウザー"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "一貫性がない"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "初期化"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "インストール"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "インターフェース"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "無効: 複数のターゲットが定義されています"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "IPv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "ジャーナル"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "journalctl man ページ"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "kdump ステータス"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "鍵"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "ログイン"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "LUKS"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "MAC"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "マシン"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "マスク"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "最大: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+msgid "member of MDRAID device"
+msgstr "MDRAID デバイスのメンバー"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "Stratis プールのメンバー"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "メモリー"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "メトリックス"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "軽減"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "詳細"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "マウント"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "ネットワーク"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "起動時にマウントしない"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "NFS エクスポートが空です"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "NFS サーバーが空です"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "NFS サーバーは有効な IPv6 ではありません"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "Nice 値"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "なし"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "CPU 数 ($0)"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "$0 上"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "オペレーティングシステム"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "OS"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "パッケージ"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "パーティション"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "パスワード"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "パスワードの強度"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "パッチ"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "パス"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "PCI"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "パフォーマンス"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "LVM2 ボリュームグループの物理ボリューム"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "プラグイン"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service が失敗しました"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service がデータの収集に失敗しました"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service サービスが実行されていません"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "Pod"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "ポート"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "電源"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "RAID"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "RAM"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "読み取り専用"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "推奨"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "LVM2 からの削除"
+
+#: pkg/storaged/utils.js:934
+msgid "remove from MDRAID"
+msgstr "MDRAID からの削除"
+
+#: pkg/storaged/utils.js:904
+msgid "remove from btrfs volume"
+msgstr "btrfs ボリュームの削除"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "再起動"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "ロール"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "実行中 $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "セキュリティー"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "シリアル"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "サービス"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "シェル"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "簡易表示"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "詳細表示"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "縮小"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "シャット"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "ソケット"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "ソリューション"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "ソリューションの詳細"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "sos repot 失敗"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ssh 鍵 はパスではありません"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "ssh サーバーが空です"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "停止"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "失敗時にブートを停止"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+msgid "stopped"
+msgstr "停止中"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "ターゲット"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "TCP"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "チーム"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "時間"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "タイマー"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "単位"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "不明"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "不明なターゲット"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "マスク解除"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "アンマウント"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "$0 の未パーティション領域"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "ユーザー"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "ユーザー名"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "キー説明 $0 の使用"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "バージョン"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "VLAN"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "ボリューム"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "警告"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "wireguard-tools パッケージがインストールされていません"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "はい"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "ゾーン"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Kdump サービスがインストールされていません。パッケージ kexec-tools がイン"
+#~ "ストールされていることを確認してください。"
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "メモリーが予約されていません。crashkernel オプションをカーネルコマンドライ"
+#~ "ンに追加して (例: /etc/default/grub)、起動時にメモリーを予約します。例: "
+#~ "crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "サービスが実行中です"
+
+#~ msgid "Service is starting"
+#~ msgstr "サービスが起動中です"
+
+#~ msgid "Service is stopped"
+#~ msgstr "サービスが停止されています"
+
+#~ msgid "Service is stopping"
+#~ msgstr "サービスが停止中です"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "kdump 設定はカーネルをクラッシュすることによりテストされます。"
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "カーネルコマンドラインで crashkernel が設定されていません"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (暗号化)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 Stratis プール"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 キャッシュ"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "不明な層の $0"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 空き"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr ""
+#~ "このディスクを削除する前に、スペアディスクを追加する必要があります。"
+
+#~ msgid "Backing device"
+#~ msgstr "バッキングデバイス"
+
+#~ msgid "Block"
+#~ msgstr "ブロック"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "容量"
+
+#~ msgid "Content"
+#~ msgstr "コンテンツ"
+
+#~ msgid "Create devices"
+#~ msgstr "デバイスの作成"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "デバイス"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "デバイスファイル"
+
+#~ msgid "Devices"
+#~ msgstr "デバイス"
+
+#~ msgid "Drives"
+#~ msgstr "ドライブ"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "暗号化したボリュームは、サイズを変更する前にロックを解除する必要がありま"
+#~ "す。"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "ファイルシステム (暗号化)"
+
+#~ msgid "Filesystems"
+#~ msgstr "ファイルシステム"
+
+#~ msgid "Free"
+#~ msgstr "空き"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "このグループ内で領域を解放します: 他の論理ボリュームを縮小または削除する"
+#~ "か、新たな物理ボリュームを追加してください。"
+
+#~ msgid "Inactive volume"
+#~ msgstr "非アクティブなボリューム"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "キーサーバー"
+
+#~ msgid "LVM2 member"
+#~ msgstr "LVM2 メンバー"
+
+#~ msgid "Locked devices"
+#~ msgstr "ロックされたデバイス"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "ロックされた暗号化データ"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "モデル"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS マウント"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "NFS サポートはインストールされていません"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "NFS マウントが設定されていません"
+
+#~ msgid "No devices"
+#~ msgstr "デバイスがありません"
+
+#~ msgid "No drives attached"
+#~ msgstr "ドライブが割り当てられていません"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "iSCSI ターゲットが設定されていません"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "新しいファイルシステムの容量が不足しています"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "他のデータ"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "パーティション設定されたブロックデバイス"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "パスフレーズ"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "ここでは、物理ボリュームのサイズを変更することができません。"
+
+#~ msgid "Pool"
+#~ msgstr "プール"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "シンボリューム用プール"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID レベル"
+
+#~ msgid "RAID member"
+#~ msgstr "RAID メンバー"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "リムーバブルドライブ"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "$0 デバイスを表示します"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "$0 ドライブを表示します"
+
+#~ msgid "Show all"
+#~ msgstr "すべて表示"
+
+#~ msgid "Source"
+#~ msgstr "ソース"
+
+#~ msgid "Start pool"
+#~ msgstr "プールの起動"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "プールを起動してファイルシステムを確認します。"
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "状態"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "停止した Stratis プール"
+
+#~ msgid "Stratis member"
+#~ msgstr "Stratis メンバー"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Stratis プール $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "サポートはインストールされました。"
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "スペアディスクを追加する場合は、RAID デバイスが実行中である必要がありま"
+#~ "す。"
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "RAID デバイスの最後のディスクは取り外すことができません。"
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "ボリュームグループの最後の物理ボリュームは削除できません。"
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "この物理ボリュームを削除するのに十分な空き領域がありません。少なくとも $0 "
+#~ "の空き領域が必要です。"
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "このディスクは、デバイスが復旧中に取り外すことができません。"
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr ""
+#~ "このボリュームは、サイズを変更する前にアクティベートする必要があります。"
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "認識されないデータ"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "使用率"
+
+#~ msgid "Used for"
+#~ msgstr "用途"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "VDO バッキング"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO デバイス"
+
+#~ msgid "Volume"
+#~ msgstr "ボリューム"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "自動 (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "キーを受け入れて接続"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "ここでは、暗号化したボリュームのサイズを変更することができません。"
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Tang キーサーバーの使用"
+
+#~ msgid "Use a passphrase"
+#~ msgstr "パスフレーズの使用"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "ビットマップ"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "キーの説明が想定される形式ではないため、このプールはここでアンロックできま"
+#~ "せん。"
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "ビットマップを切り替える"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Tang サーバーのキーハッシュが、以下のいずれかと一致することを確認します:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "手動で SSH を確認します: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "ポート番号とタイプが一致しません"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "ロックされている暗号化 Stratis プール"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "アラートの削除中にエラーが発生しました: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "アラートを取得できません: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[バイナリーデータの $0 バイト]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "ディスク I/O スパイク"
+
+#~ msgid "Event"
+#~ msgstr "イベント"
+
+#~ msgid "Event logs"
+#~ msgstr "イベントログ"
+
+#~ msgid "Load spike"
+#~ msgstr "スパイクのロード"
+
+#~ msgid "Memory spike"
+#~ msgstr "メモリーのスパイク"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "ネットワーク I/O スパイク"
+
+#~ msgid "ssh key"
+#~ msgstr "ssh 鍵"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "このオプションにチェックが付けられると、次回の起動時にファイルシステムがマ"
+#~ "ウントされていても、次回の起動時にマウントされません。これは、ファイルシス"
+#~ "テムのロックを解除する必要があるが、起動が無人の場合などに、起動時にマウン"
+#~ "トできない場合に役立ちます。"
+
+#~ msgid "Never mount at boot"
+#~ msgstr "起動時のマウントなし"
+
+#~ msgid "Listing unit files"
+#~ msgstr "ユニットファイルの一覧表示"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "ユニットファイルの一覧表示に失敗しました: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "単位が見つかりません"
+
+#~ msgid "Validating key"
+#~ msgstr "鍵の検証"
+
+#, fuzzy
+#~| msgid "Delete $0 volume"
+#~| msgid_plural "Delete $0 volumes"
+#~ msgid "Delete $0 group"
+#~ msgstr "$0 ボリュームの削除"
+
+#~ msgid "Container administrator"
+#~ msgstr "コンテナー管理者"
+
+#~ msgid "Image builder"
+#~ msgstr "イメージビルダー"
+
+#~ msgid "Roles"
+#~ msgstr "ロール"
+
+#~ msgid "Server administrator"
+#~ msgstr "サーバー管理者"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Unixグループ: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "キーボードにコピーされました"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "診断レポート"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "カーネルダンプ"
+
+#~ msgid "Software Updates"
+#~ msgstr "ソフトウェア更新"
+
+#~ msgid "Update log"
+#~ msgstr "ログの更新"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "nfs ダンプターゲットは server:path の形式になっていません"
+
+#, fuzzy
+#~| msgid "Zone"
+#~ msgid "$0 Zone"
+#~ msgstr "ゾーン"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "診断レポートの作成"
+
+#~ msgid "Done!"
+#~ msgstr "完了!"
+
+#~ msgid "Download report"
+#~ msgstr "レポートをダウンロードします"
+
+#~ msgid "No archive has been created."
+#~ msgstr "アーカイブが作成されていません。"
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "$0 の削除を確定してください"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "生成されたアーカイブには機密とみなされるデータが含まれており、その内容は第"
+#~ "三者に渡される前に発信元の組織によって確認される必要があります。"
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "このツールは、システムの問題の診断で使用するためにシステムからシステム設定"
+#~ "と診断情報を収集します。"
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "サーバーに cockpit-system パッケージがありません"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "このパッケージには Cockpit のこのバージョンとの互換性がありません"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "このパッケージには Cockpit バージョン %s 以降が必要です"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "パフォーマンスメトリックス"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "読み取り専用"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB の合計"
+
+#~ msgid "Apply"
+#~ msgstr "適用"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "ゾーン $0 を削除する権限がありません"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "$0 を再びクリックしてこのパスワードを使用します。"
+
+#~ msgid "Access"
+#~ msgstr "アクセス"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "ディスクで障害が発生中"
+
+#~ msgid "Logical Size"
+#~ msgstr "論理サイズ"
+
+#~ msgid "Security updates "
+#~ msgstr "セキュリティー更新 "
+
+#~ msgid "Updates "
+#~ msgstr "更新 "
+
+#~ msgid "(Optional)"
+#~ msgstr "(オプション)"
+
+#~ msgid "Active since"
+#~ msgstr "以降有効"
+
+#~ msgid "Affected locations"
+#~ msgstr "影響を受ける場所"
+
+#~ msgid "Process"
+#~ msgstr "プロセス"
+
+#~ msgid "Remove and delete"
+#~ msgstr "削除および削除"
+
+#~ msgid "Remove and format"
+#~ msgstr "削除およびフォーマット"
+
+#~ msgid "Remove and grow"
+#~ msgstr "削除および拡張"
+
+#~ msgid "Remove and initialize"
+#~ msgstr "削除および初期化"
+
+#~ msgid "Remove and shrink"
+#~ msgstr "削除および縮小"
+
+#~ msgid "Remove and stop device"
+#~ msgstr "デバイスの削除と停止"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "このファイルシステムは、ログインセッションおよびシステムサービスで使用中で"
+#~ "す。続行すると、これらを停止します。"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "このファイルシステムは、ログインセッションで使用中です。続行すると、これら"
+#~ "を停止します。"
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "このファイルシステムは、システムサービスで使用中です。続行すると、これらを"
+#~ "停止します。"
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "このデバイスには、現在使用中のファイルシステムがあります。続行すると、この"
+#~ "デバイスのファイルシステムをすべてアンマウントします。"
+
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "このデバイスは、LVM2 ボリュームグループに使用されています。"
+
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "このデバイスは現在、LVM2 ボリュームグループに使用されています。続行する"
+#~ "と、そのボリュームグループからこのデバイスが削除されます。"
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "このデバイスは、現在 RAID デバイスに使用されています。"
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "このデバイスは、現在 RAID デバイスに使用されています。続行すると、RAID デ"
+#~ "バイスからこのデバイスが削除されます。"
+
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "このデバイスは現在、Stratis プールに使用されます。"
+
+#~ msgid "Unmount and delete"
+#~ msgstr "アンマウントおよび削除"
+
+#~ msgid "Unmount and format"
+#~ msgstr "アンマウントおよびフォーマット"
+
+#~ msgid "Unmount and grow"
+#~ msgstr "アンマウントおよび拡張"
+
+#~ msgid "Unmount and initialize"
+#~ msgstr "アンマウントおよび初期化"
+
+#~ msgid "Unmount and shrink"
+#~ msgstr "アンマウントおよび縮小"
+
+#~ msgid "Unmount and stop device"
+#~ msgstr "デバイスのアンマウントおよび停止"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0/$1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "ブロック済み"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "$0 の物理ボリューム"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "RAID デバイス $0 のメンバー"
+
+#~ msgid "Menu"
+#~ msgstr "メニュー"
+
+#~ msgid "VDO backing"
+#~ msgstr "VDO バッキング"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "ストレージプールの作成"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "マウントオプション"
+
+#, fuzzy
+#~| msgid "Reset Storage Pool"
+#~ msgid "Stratis Pool"
+#~ msgstr "ストレージプールのリセット"
+
+#~ msgid "A disk is needed."
+#~ msgstr "1 つのディスクが必要です。"
+
+#~ msgid "Disk"
+#~ msgstr "ディスク"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "レガシーアプリケーション専用です。パフォーマンスが低下します。"
+
+#~ msgid "Install VDO support"
+#~ msgstr "VDO サポートをインストールする"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "512 バイトのエミュレーションを使用します"
+
+#~ msgid "System services"
+#~ msgstr "システムサービス"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "診断レポートの作成 "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr "{{ limit }} 台を超えるマシンへの同時接続はサポートされていません。"
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "Kerberos ベース SSO"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "{{host}} へのログイン"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "新しいキーのパスワードが一致しません"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "フィンガープリントを確認するには、マシン上に物理的に置配置されている、また"
+#~ "は、信頼できるネットワークを介して {{host}} で次のコマンドを実行します:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "{{#strong}}{{host}}{{/strong}} と通信できません。"
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}} にログインできませんでした。他の認証オプ"
+#~ "ションとトラブルシューティングのサポートが必要な場合は、cockpit-ws を新し"
+#~ "いバージョンにアップグレードしてください。"
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}} にログインできませんでした。このホストに接"
+#~ "続するには、{{#strong}}{{host}}{{/strong}} 上の sshd 設定で次の認証方法の"
+#~ "いずれかを有効にする必要があります:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "初めて {{host}} に接続しています。"
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "{{host}} キーが変更されました"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "マウントポイントの設定を消去"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "このボンドを作成すると、サーバーへの接続が切断され、管理 UI が利用できなく"
+#~ "なります。"
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "このブリッジを作成すると、サーバーへの接続が切断され、管理 UI が利用できな"
+#~ "くなります。"
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "このチームを作成すると、サーバーへの接続が切断され、管理 UI が利用できなく"
+#~ "なります。"
+
+#~ msgid "IP settings"
+#~ msgstr "IP 設定"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "<b>$0</b> をオフにすると、サーバーへの接続が切断され、管理 UI が利用できな"
+#~ "くなります。"
+
+#~ msgid "Administrator password"
+#~ msgstr "管理者パスワード"
+
+#~ msgid "Computer OU"
+#~ msgstr "コンピューター OU"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr ""
+#~ "RAID デバイスを削除すると、そのデバイス上のすべてのデータが削除されます。"
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr ""
+#~ "ボリュームグループを削除すると、ボリュームグループ上のすべてのデータが削除"
+#~ "されます。"
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "既存のデータを上書きしない"
+
+#~ msgid "Erase"
+#~ msgstr "削除"
+
+#~ msgid "Format disk $0"
+#~ msgstr "ディスク $0 のフォーマット"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr ""
+#~ "ストレージデバイスをフォーマットすると、そのデバイス上のすべてのデータが削"
+#~ "除されます。"
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "ドメインでホスト名は変更できません"
+
+#~ msgid "More"
+#~ msgstr "詳細表示"
+
+#~ msgid "Not joined"
+#~ msgstr "参加していません"
+
+#~ msgid "One time password"
+#~ msgstr "ワンタイムパスワード"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$2 の $1 から $0"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "時間は 0 から 23 の間の数字で指定する必要があります"
+
+#~ msgid "Last login:"
+#~ msgstr "最終ログイン:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "分は 0~59 の間の数字でなければなりません"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] "最後の正しいログインの後に $0 回の失敗ログインの試行があります。"
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "例: \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 アクティブゾーン"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 件"
+
+#~ msgid "Licensed under:"
+#~ msgstr "ライセンス情報:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "起動時にロック解除"
+
+#~ msgid "Unlock read only"
+#~ msgstr "読み取り専用のロックを解除"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "用語の組み合わせでログを検索します:"
+
+#~ msgid "Show filters"
+#~ msgstr "フィルターを表示"
+
+#~ msgid "Text"
+#~ msgstr "テキスト"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "正規表現としての任意の自由形式文字列"
+
+#~ msgid "e.g."
+#~ msgstr "例:"
+
+#~ msgid "log fields"
+#~ msgstr "ログフィールド"
+
+#~ msgid "qualifiers"
+#~ msgstr "修飾子"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "セッション設定を切り替える"
+
+#~ msgid "(none)"
+#~ msgstr "(なし)"
+
+#~ msgid "Create timers"
+#~ msgstr "タイマーの作成"
+
+#~ msgid "Recommended default"
+#~ msgstr "推奨されるデフォルト"
+
+#~ msgid "Run"
+#~ msgstr "実行"
+
+#~ msgid "Select unit state"
+#~ msgstr "ユニットの状態の選択"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "保存されたメトリックを有効にします"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "保存されたメトリックを有効にします"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "$0 のパスフレーズの削除を強制します"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "tang-show-keys を利用できない場合は、以下を実行します:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "tang-show-keys を利用できない場合はどうしますか?"
+
+#~ msgid "key slot $0"
+#~ msgstr "キースロット $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "問題が発生しました"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "失敗しました。もう一度お試しください"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "管理者アクセスを取得することはできません。"
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "自動更新が設定されていません"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "$0 CPU 設定"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 ネットワーク"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 は、多数のオペレーティングシステムで利用できます。インストールするに"
+#~ "は、GNOME ソフトウェアで検索するか、以下を実行します:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 メモリー調整"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 ネットワーク"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPU"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 vCPU 詳細"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 仮想ネットワークインターフェーイス設定"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "ストレージプールをアクティベートしてボリュームを管理"
+
+#~ msgid "Add network interface"
+#~ msgstr "ネットワークインターフェイスの追加"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "仮想ネットワークインターフェイスの追加"
+
+#~ msgid "Additional"
+#~ msgstr "追加"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "アドレスはサブネット内にありません"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr ""
+#~ "スナップショットを削除すると、そのキャプチャされたすべてのコンテンツが失わ"
+#~ "れます。"
+
+#~ msgid "Always attach"
+#~ msgstr "常に割り当てる"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "割り当てることで、このディスクを使用するすべての VM でこのディスクが共有可"
+#~ "能になります。"
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "起動時に libvirt を自動開始"
+
+#~ msgid "Autostart"
+#~ msgstr "自動起動"
+
+#~ msgid "Boot order"
+#~ msgstr "ブート順序"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "ブート順序の設定は保存できませんでした"
+
+#~ msgid "Bus"
+#~ msgstr "バス"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "CD/DVD ディスク"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "CPU 設定を保存できませんでした"
+
+#~ msgid "CPU type"
+#~ msgstr "CPU タイプ"
+
+#~ msgid "Change firmware"
+#~ msgstr "ファームウェアの変更"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "変更は、仮想マシンをシャットダウンすると反映されます"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "オペレーティングシステムの選択"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "\"リモートビューアーの起動\" をクリックすると、.vv ファイルをダウンロード"
+#~ "し、$0 を起動します。"
+
+#~ msgid "Clone"
+#~ msgstr "クローン"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "イメージをロードできません"
+
+#~ msgid "Confirm this action"
+#~ msgstr "この動作を確認"
+
+#~ msgid "Connect"
+#~ msgstr "接続"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "以下のプロトコル用のビューアーアプリケーションへ接続"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "仮想化サービスへ接続中"
+
+#~ msgid "Connection"
+#~ msgstr "接続"
+
+#~ msgid "Console"
+#~ msgstr "コンソール"
+
+#~ msgid "Cores per socket"
+#~ msgstr "ソケットごとのコア"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "スナップショットに戻すことができませんでした"
+
+#~ msgid "Crashed"
+#~ msgstr "クラッシュした"
+
+#~ msgid "Create VM"
+#~ msgstr "仮想マシンの作成"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "$0 に基づいてクローン VM を作成します"
+
+#~ msgid "Create new"
+#~ msgstr "新規作成"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "仮想マシンの新規作成"
+
+#~ msgid "Create virtual network"
+#~ msgstr "仮想ネットワークの作成"
+
+#~ msgid "Creating VM"
+#~ msgstr "VM を作成中"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "VM インストールを作成中"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "VM $0 の作成に失敗"
+
+#~ msgid "Creation time"
+#~ msgstr "時間の作成"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "現在の割り当て"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "カスタムファームウェア: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP の範囲"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "関連するストレージファイルの削除:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "ストレージプール $0 の削除"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "このプール内のボリュームを削除します"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "停止状態のストレージプールの削除は、プールの定義を解除するだけです。コンテ"
+#~ "ンツは削除されません。"
+
+#~ msgid "Desktop viewer"
+#~ msgstr "デスクトップビューアー"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "削除を試みる前に、このプールを使用して VM からディスクを切り離します。"
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "シリアルコンソールから切断されました。接続ボタンをクリックします。"
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "ディスク $0 は、VM $1 からの切断に失敗しました"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "ディスクの割り当てに失敗しました"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "ディスクの作成に失敗しました"
+
+#~ msgid "Disk image file"
+#~ msgstr "ディスクイメージファイル"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "ディスク設定を保存できませんでした"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "ディスクのみのスナップショット"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "ドメインがクラッシュしました"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "ドメインがリソース上でブロックされています"
+
+#~ msgid "Download an OS"
+#~ msgstr "OS をダウンロードします"
+
+#~ msgid "Dying"
+#~ msgstr "終了中"
+
+#~ msgid "Emulated machine"
+#~ msgstr "エミュレートされたマシン"
+
+#~ msgid "End"
+#~ msgstr "末尾"
+
+#~ msgid "End should not be empty"
+#~ msgstr "末尾は空欄にできません"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "ホストファイルシステム上の既存のディスクイメージ"
+
+#~ msgid "Expand"
+#~ msgstr "展開"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "ファームウェアの変更に失敗しました"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "$0 のインターフェイスの IP アドレスの取得に失敗しました"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "VM $1 へのキー送信 Ctrl+Alt+$0 が失敗しました"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr "仮想 CPU の最大数よりも少ない数を有効にするべきです。"
+
+#~ msgid "File"
+#~ msgstr "File"
+
+#~ msgid "Filter by name"
+#~ msgstr "名前での絞り込み"
+
+#~ msgid "Firmware"
+#~ msgstr "ファームウェア"
+
+#~ msgid "Force shut down"
+#~ msgstr "シャットダウンの強制"
+
+#~ msgid "Forward mode"
+#~ msgstr "フォワードモード"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "フォワードモード"
+
+#~ msgid "Generate automatically"
+#~ msgstr "自動生成"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "仮想マシンの一覧に移動"
+
+#~ msgid "Hide additional options"
+#~ msgstr "その他のオプションを非表示にします"
+
+#~ msgid "Host device"
+#~ msgstr "ホストデバイス"
+
+#~ msgid "Host name"
+#~ msgstr "ホスト名"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "ホストは空にできません"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "ハイパーバイザー詳細"
+
+#~ msgid "IP configuration"
+#~ msgstr "IP 設定"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 および IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4 ネットワーク"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv4 ネットワークは空欄にできません"
+
+#~ msgid "IPv4 only"
+#~ msgstr "IPv4 のみ"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6 アドレス"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6 ネットワーク"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6 ネットワークは空欄にできません"
+
+#~ msgid "IPv6 only"
+#~ msgstr "IPv6 のみ"
+
+#~ msgid "Idle"
+#~ msgstr "アイドル"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "仮想マシンをすぐに起動"
+
+#~ msgid "Import"
+#~ msgstr "インポート"
+
+#~ msgid "Import VM"
+#~ msgstr "VM のインポート"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "仮想マシンのインポート"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "ほとんどの構成で、macvtap は、ホストからゲストへのネットワーク通信には正し"
+#~ "く動作しません。"
+
+#~ msgid "Initiator"
+#~ msgstr "イニシエーター"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "イニシエーター IQN は空にできません"
+
+#~ msgid "Installation source"
+#~ msgstr "インストールソース"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "インストールソースは空欄にできません"
+
+#~ msgid "Installation type"
+#~ msgstr "インストールタイプ"
+
+#~ msgid "Interface type"
+#~ msgstr "インターフェース形式"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "無効な IPv4 マスクまたはプレフィックス長"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "無効な IPv6 アドレス"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "無効な IPv6 プレフィックス"
+
+#~ msgid "Invalid filename"
+#~ msgstr "無効なファイル名"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "リモートビューアーの起動"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr ""
+#~ "rootアカウントを作成したくない場合はパスワードを空白のままにしてください"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "ユーザーアカウントを作成したくない場合はパスワードを空白のままにしてくださ"
+#~ "い"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr ""
+#~ "rootアカウントを作成したくない場合はパスワードを空白のままにしてください"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "libvirtがホストにインストールされているUEFI/OVMFファームウェアイメージを検"
+#~ "出できませんでした"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "libvirtまたはハイパーバイザーがUEFIをサポートしていません"
+
+#~ msgid "Loading resources"
+#~ msgstr "リソースのロード中"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "バスタイプの変更前にマシンをシャットオフする必要があります"
+
+#, fuzzy
+#~| msgid "Machine must be shut off before changing bus type"
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "バスタイプの変更前にマシンをシャットオフする必要があります"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "仮想マシン管理"
+
+#~ msgid "Manual connection"
+#~ msgstr "手動接続"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "マスクまたはプレフィックス長"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "マスクまたはプレフィックス長は空欄にできません"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "最大割り当て"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "最大メモリーは保存できませんでした"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr "ゲスト OS に割り当てられる仮想 CPU の最大数"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "ゲスト OS に割り当てられる仮想 CPU の最大数で、これは 1 から $0 の間でなけ"
+#~ "ればなりません"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Maximum transmission unit"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "メモリーは保存できませんでした"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "メモリは 0 以外である必要があります"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "モデルタイプ"
+
+#~ msgid "NAT to $0"
+#~ msgstr "$0 への NAT"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "VM $1 の NIC $0 は、状態の変更に失敗しました"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "名前に無効な文字が含まれています"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "名前は空欄にできません"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "名前は空欄にできません"
+
+#~ msgid "Name: "
+#~ msgstr "名前: "
+
+#~ msgid "Netmask"
+#~ msgstr "ネットマスク"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "ネットワーク $0 のアクティベートに失敗しました"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "ネットワーク $0 の停止に失敗しました"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "ネットワークブート (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "ネットワークファイルシステム"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "ネットワークインターフェース設定を保存できませんでした"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "ネットワーク選択では PXE がサポートされていません。"
+
+#~ msgid "Networks"
+#~ msgstr "ネットワーク"
+
+#~ msgid "New volume name"
+#~ msgstr "新しいボリューム名"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "仮想マシンがこのホストで実行されていないか、定義されていません"
+
+#~ msgid "No connection available"
+#~ msgstr "利用可能な接続なし"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "この仮想マシンに対してディスクが定義されていません"
+
+#~ msgid "No network devices"
+#~ msgstr "ネットワークデバイスはありません"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "この仮想マシンにはネットワークインターフェースが定義されていません"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "このホストで定義されるネットワークはありません"
+
+#~ msgid "No networks available"
+#~ msgstr "利用可能なネットワークはありません"
+
+#~ msgid "No parent"
+#~ msgstr "親なし"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "この仮想マシンに対してスナップショットが定義されていません"
+
+#~ msgid "No state"
+#~ msgstr "状態なし"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "このホストではストレージプールが定義されていません"
+
+#~ msgid "No storage pools available"
+#~ msgstr "利用可能なストレージプールはありません"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "このストレージプールにストレージボリュームが定義されていません"
+
+#~ msgid "No virtual networks"
+#~ msgstr "仮想ネットワークはありません"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "非永続的なネットワークは削除できません。非アクティブ化されると、存在しなく"
+#~ "なります。"
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "非永続ストレージプールは削除できません。非アクティブ化されると、存在しなく"
+#~ "なります。"
+
+#~ msgid "None (isolated network)"
+#~ msgstr "なし (隔離されたネットワーク)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "ひとつまたは複数の選択されたボリュームがドメインで使用されています。最初に"
+#~ "ディスクを切断し、ボリュームが削除できるようにします。"
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "ゲストがシャットオフされた場合のみ、編集可能です"
+
+#~ msgid "Open"
+#~ msgstr "開く"
+
+#~ msgid "Operating system"
+#~ msgstr "オペレーティングシステム"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "操作を実行中"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "親のスナップショット"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "ホストファイルシステム上のパス"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "ホストファイルシステムの ISO ファイルのパス"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "ホストファイルシステムのファイルへのパス"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "ホストファイルシステムのファイルへのパス"
+
+#~ msgid "Paused"
+#~ msgstr "一時停止"
+
+#~ msgid "Persistence"
+#~ msgstr "永続"
+
+#~ msgid "Physical disk device"
+#~ msgstr "物理ディスクデバイス"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "ホスト上の物理ディスクデバイス"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "ストレージプールを選択してください"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "ボリュームを選択してください"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "新しいボリューム名を入力してください"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "仮想マシンを起動して、コンソールにアクセスしてください。"
+
+#~ msgid "Plug"
+#~ msgstr "プラグ"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "ボリュームを作成するには、プールがアクティブである必要があります"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "プールタイプは、ボリューム作成をサポートしません"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "プールのボリュームは VM が使用します "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "ゲストへの公開用の推奨されるソケットの数。"
+
+#~ msgid "Prefix"
+#~ msgstr "プレフィックス"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "プレフィックス長は空欄にできません"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "以前に撮影したスナップショットでは、何か問題が発生した場合に以前の状態に戻"
+#~ "すことができます"
+
+#~ msgid "Product"
+#~ msgstr "製品"
+
+#~ msgid "Profile"
+#~ msgstr "プロファイル"
+
+#~ msgid "Protocol"
+#~ msgstr "プロトコル"
+
+#~ msgid "Remote URL"
+#~ msgstr "リモート URL"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "リモートビューアーの詳細"
+
+#~ msgid "Revert"
+#~ msgstr "元に戻す"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "スナップショット $0 に戻す"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "このスナップショットに戻すと、VM はスナップショットの時の状態に戻り、現在"
+#~ "の状態はスナップショットにキャプチャされていないデータとともに失われます"
+
+#~ msgid "Root password"
+#~ msgstr "rootパスワード"
+
+#~ msgid "Route to $0"
+#~ msgstr "$0 へのルーティング"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "無人インストール実行"
+
+#~ msgid "Run when host boots"
+#~ msgstr "ホスト起動時に実行します"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE TLS ポート"
+
+#~ msgid "SPICE address"
+#~ msgstr "SPICE アドレス"
+
+#~ msgid "SPICE port"
+#~ msgstr "SPICE ポート"
+
+#~ msgid "Select console type"
+#~ msgstr "コンソールタイプの選択"
+
+#~ msgid "Send key"
+#~ msgstr "キーの送信"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "マスク不可割り込みを送信します"
+
+#~ msgid "Serial console"
+#~ msgstr "シリアルコンソール"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "DHCP の範囲を設定します"
+
+#~ msgid "Set manually"
+#~ msgstr "手動で設定します"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "無人インストールのためのユーザーパスワード設定には、作成時にVMを開始する必"
+#~ "要があります"
+
+#~ msgid "Show additional options"
+#~ msgstr "その他のオプションを表示します"
+
+#~ msgid "Shut off"
+#~ msgstr "シャットオフ"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "ファームウェア設定を編集するためにVMをシャットオフ"
+
+#~ msgid "Shutting down"
+#~ msgstr "シャットダウン中"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "スナップショットの作成に失敗しました"
+
+#~ msgid "Source format"
+#~ msgstr "ソースフォーマット"
+
+#~ msgid "Source path"
+#~ msgstr "ソースパス"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "ソースパスは空欄にできません"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "ソースは、http、ftp、または nfs プロトコルで開始する必要があります"
+
+#~ msgid "Source volume group"
+#~ msgstr "ソースボリュームグループ"
+
+#~ msgid "Start libvirt"
+#~ msgstr "libvirt の開始"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "ホスト起動時にプールを開始します"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "開始は空欄にできません"
+
+#~ msgid "Startup"
+#~ msgstr "起動"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "ストレージプール $0 は、アクティベートに失敗しました"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "ストレージプール $0 は、停止に失敗しました"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "ストレージプールの作成に失敗しました"
+
+#~ msgid "Storage pool name"
+#~ msgstr "ストレージプール名"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "ストレージサイズは 0 であってはいけません"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr ""
+#~ "ストレージボリュームサイズは、ストレージプールの容量を超えることはできませ"
+#~ "ん ($0 $1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "ストレージボリュームは削除できませんでした"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "一時停止中 (PM)"
+
+#~ msgid "Target path"
+#~ msgstr "ターゲットパス"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "ターゲットパスは空欄にできません"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr ""
+#~ "仮想マシンが稼動しているため、削除前に強制的に電源がオフになります。"
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "このデバイスを切断するには、VM を実行するか、シャットオフする必要がありま"
+#~ "す"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "サーバー上のディレクトリーをエクスポート中"
+
+#~ msgid "The pool is empty"
+#~ msgstr "プールが空です"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr ""
+#~ "選択したオペレーティングシステムは無人インストールをサポートしていません"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr ""
+#~ "選択したオペレーティングシステムには、必要なメモリー $0 $1 があります"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "選択したオペレーティングシステムには、最小ストレージザイズ $0 $1 が必要で"
+#~ "す"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "ストレージプールを削除できませんでした"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr ""
+#~ "これはtransient VMです。削除したい場合はシャットダウンしてください。"
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "このボリュームは、すでに以下が使用しています: "
+
+#~ msgid "Threads per core"
+#~ msgstr "コアあたりのスレッド"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr "transient VMはファームウェア設定の編集をサポートしていません"
+
+#~ msgid "Type ID"
+#~ msgstr "Type ID"
+
+#~ msgid "Unique name"
+#~ msgstr "固有名"
+
+#~ msgid "Unique network name"
+#~ msgstr "一意のネットワーク名"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "不明なファームウェア"
+
+#~ msgid "Unplug"
+#~ msgstr "アンプラグ"
+
+#~ msgid "Url"
+#~ msgstr "URL"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "VCPU 設定を保存できませんでした"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "VM $0 はすでに存在します"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "VM $0 は $1 接続に存在しません"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "VM $0 は、強制的に再起動させることに失敗しました"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "VM $0 は、強制的にシャットダウンさせることに失敗しました"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "VM $0 の削除に失敗しました"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "VM $0 のインストールに失敗しました"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "VM $0 は、再起動に失敗しました"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "VM $0 の再開に失敗しました"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "VM $0 は NMI の送信に失敗しました"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "VM $0 はシャットダウンに失敗しました"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "VM $0 は起動に失敗しました"
+
+#~ msgid "VM state"
+#~ msgstr "VM の状態"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC TLS ポート"
+
+#~ msgid "VNC address"
+#~ msgstr "VNC アドレス"
+
+#~ msgid "VNC console"
+#~ msgstr "VNC コンソール"
+
+#~ msgid "VNC port"
+#~ msgstr "VNC ポート"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "仮想マシン"
+
+#~ msgid "Virtual machines"
+#~ msgstr "仮想マシン"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "仮想マシン管理"
+
+#~ msgid "Virtual network"
+#~ msgstr "仮想ネットワーク"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "仮想ネットワークの作成に失敗しました"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "仮想化サービス (libvirt) は有効ではありません"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "ボリュームグループ名は空欄にできません"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "書き込み可能"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "書き込み可能で共有済み"
+
+#~ msgid "Yesterday"
+#~ msgstr "昨日"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "最も一致するオペレーティングシステムを選択する必要があります"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "無効"
+
+#~ msgid "down"
+#~ msgstr "下へ"
+
+#~ msgid "enabled"
+#~ msgstr "有効"
+
+#~ msgid "ethernet"
+#~ msgstr "Ethernet"
+
+#~ msgid "host device"
+#~ msgstr "ホストデバイス"
+
+#~ msgid "host passthrough"
+#~ msgstr "ホストパススルー"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "iSCSI ダイレクトターゲット"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "iSCSI イニシエーター IQN"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "iSCSI ターゲット IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "いいえ"
+
+#~ msgid "no state saved"
+#~ msgstr "保存されている状態がありません"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "リダイレクトデバイス"
+
+#~ msgid "up"
+#~ msgstr "上へ"
+
+#~ msgid "vCPU count"
+#~ msgstr "vCPU 数"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU 最大値"
+
+#~ msgid "vCPUs"
+#~ msgstr "vCPU"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#~ msgid "view more..."
+#~ msgstr "詳細を表示..."
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "VM を複製するには、virt-install パッケージをシステムにインストールする必要"
+#~ "があります"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "新しい VM を作成するには、virt-install パッケージをシステムにインストール"
+#~ "する必要があります"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "この属性を編集するには、virt-install パッケージをシステムにインストールす"
+#~ "る必要があります"
+
+#~ msgid "vm"
+#~ msgstr "vm"
+
+#~ msgid "Local install media"
+#~ msgstr "ローカルインストールメディア"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "root パスワードを設定してください"
+
+#~ msgid "21st"
+#~ msgstr "21 日"
+
+#~ msgid "22nd"
+#~ msgstr "22 日"
+
+#~ msgid "23rd"
+#~ msgstr "23 日"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "後"
+
+#~ msgid "Friday"
+#~ msgstr "金曜日"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "時間: 分"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "時間は 0〜23 の数字である必要があります"
+
+#~ msgid "Invalid date format."
+#~ msgstr "無効な日付形式。"
+
+#~ msgid "Invalid number."
+#~ msgstr "無効な数字。"
+
+#~ msgid "Monday"
+#~ msgstr "月曜日"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "毎時繰り返す"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "毎年繰り返す"
+
+#~ msgid "Saturday"
+#~ msgstr "土曜日"
+
+#~ msgid "Sunday"
+#~ msgstr "日曜日"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "この日はすべての月で存在しません。<br> タイマーは 31 日がある月でのみ実行"
+#~ "されます。"
+
+#~ msgid "Thursday"
+#~ msgstr "木曜日"
+
+#~ msgid "Tuesday"
+#~ msgstr "火曜日"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 更新"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 セキュリティー修正"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "ストレージデバイス管理"
+
+#~ msgid "Restart now"
+#~ msgstr "すぐに再起動"
+
+#~ msgid "Configure"
+#~ msgstr "設定"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr ""
+#~ "ネットワークブートは、システム接続を使用している場合にのみ使用できます"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "ネットワーキング"
+
+#~ msgid "No snapshots"
+#~ msgstr "スナップショットなし"
+
+#, fuzzy
+#~| msgid "No such file or directory '$0'"
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "「$0」というファイルやディテクトリーはありません"
+
+#~ msgid "No updates pending"
+#~ msgstr "保留中の更新がありません"
+
+#, fuzzy
+#~| msgid "ssh server is empty"
+#~ msgid "This directory is empty"
+#~ msgstr "ssh サーバーが空です"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr "このプールタイプは、ストレージボリュームの作成に対応していません"
+
+#~ msgid "undefined"
+#~ msgstr "未定義"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "正しくないホストキー"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "ホスト {{#strong}}{{host}}{{/strong}} の認証を確立できません。本当に接続を"
+#~ "維持しますか?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}} の鍵が、以前に使用された鍵と一致しません。"
+#~ "このマシンが最近置き換えられたものでない限り、誰かがこのマシンへの接続を攻"
+#~ "撃しようとしている可能性があります。"
+
+#~ msgid "Unknown host key"
+#~ msgstr "不明なホストキー"
+
+#~ msgid "Address:"
+#~ msgstr "アドレス:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "少なくとも $0 ディスクが必要です。"
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "SPICE または VNC のビューアーアプリケーションに接続します。"
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "MSI を $0 からダウンロードします"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "グラフィックコンソール (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "デスクトップビューアーのグラフィックコンソール"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "この仮想マシンにはコンソールが定義されていません。"
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid "Entry"
+#~ msgstr "エントリー"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "ブラウザーが切断されますが、更新プロセスへの影響はありません。すぐに再接続"
+#~ "して進捗確認を継続できます。"
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "マシンを自動的に再起動します。"
+
+#~ msgid "Dashboard"
+#~ msgstr "ダッシュボード"
+
+#~ msgid "Description input text"
+#~ msgstr "説明入力テキスト"
+
+#~ msgid "FAILED"
+#~ msgstr "失敗"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "接続が失われました。再接続の試行中です"
+
+#~ msgid "Name input text"
+#~ msgstr "名前入力テキスト"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "ネットワーク"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "新しいボリュームサイズを入力してください"
+
+#~ msgid "Servers"
+#~ msgstr "サーバー"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr "現在このサーバーに直接接続されています。削除できません。"
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 ビット"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 バイト"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 バイト"
+
+#, fuzzy
+#~| msgid "Bond settings"
+#~ msgid "Bond settings "
+#~ msgstr "ボンド設定"
+
+#, fuzzy
+#~| msgid "IP settings"
+#~ msgid "IP settings "
+#~ msgstr "IP 設定"
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Cockpit で予期しない内部エラーが発生しました。<br/><br/>ブラウザーで更新を"
+#~ "押して Cockpit の再起動を試行できます。javascript コンソールにはこのエラー"
+#~ "に関する詳細が含まれます (ほとんどのブラウザーでは <b>Ctrl-Shift-J</b>)。"
+
+#~ msgid "The VM crashed."
+#~ msgstr "仮想マシンがクラッシュしました。"
+
+#~ msgid "The VM is down."
+#~ msgstr "仮想マシンがダウンしています。"
+
+#~ msgid "The VM is going down."
+#~ msgstr "仮想マシンがダウンします。"
+
+#~ msgid "The VM is idle."
+#~ msgstr "仮想マシンがアイドル状態です。"
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr ""
+#~ "仮想マシンが終了中の状態です (シャットダウンまたはクラッシュが完了していま"
+#~ "せん)。"
+
+#~ msgid "The VM is paused."
+#~ msgstr "仮想マシンが一時停止しています。"
+
+#~ msgid "The VM is running."
+#~ msgstr "仮想マシンが実行中です。"
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "仮想マシンはゲストの電源管理によって一時停止されています。"
+
+#~ msgid "inactive"
+#~ msgstr "非アクティブ"
+
+#~ msgid "Avatar"
+#~ msgstr "アバター"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "現在ログインしているユーザーとしてこのマシンに接続する場合は空白のままにし"
+#~ "ます。異なるユーザー名を入力した場合は、このマシンへの接続時にそのユーザー"
+#~ "が常に使用されます。"
+
+#~ msgid "2 min"
+#~ msgstr "2 分"
+
+#~ msgid "3 min"
+#~ msgstr "3 分"
+
+#~ msgid "4 min"
+#~ msgstr "4 分"
+
+#~ msgid "CPU graph"
+#~ msgstr "CPU グラフ"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "CPU ステータス"
+
+#~ msgid "Cached"
+#~ msgstr "キャッシュ済み"
+
+#~ msgid "Graphs"
+#~ msgstr "グラフ"
+
+#~ msgid "I/O wait"
+#~ msgstr "I/O 待機"
+
+#~ msgid "Kernel"
+#~ msgstr "カーネル"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "メモリ"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "メモリー & スワップの使用方法"
+
+#~ msgid "Memory graph"
+#~ msgstr "メモリーグラフ"
+
+#~ msgid "Network traffic"
+#~ msgstr "ネットワークトラフィック"
+
+#~ msgid "Nice"
+#~ msgstr "Nice値"
+
+#~ msgid "Synchronize users"
+#~ msgstr "ユーザーの同期"
+
+#~ msgid "Usage graphs"
+#~ msgstr "使用法のグラフ"
+
+#~ msgid "View graphs"
+#~ msgstr "グラフの表示"
+
+#~ msgid "idle"
+#~ msgstr "アイドル"
+
+#~ msgid "paused"
+#~ msgstr "一時停止"
+
+#~ msgid "running"
+#~ msgstr "実行中"
+
+#~ msgid "shut off"
+#~ msgstr "シャットオフ"
+
+#~ msgid "shutdown"
+#~ msgstr "シャットダウン"
+
+#~ msgid "Add a new host"
+#~ msgstr "新しいホストの追加"
+
+#~ msgid "Available"
+#~ msgstr "利用可能"
+
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "IP アドレスまたはホスト名の入力"
+
+#~ msgid "Network interfaces"
+#~ msgstr "ネットワークインターフェース"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "cockpit-ws のこのバージョンでは、別のユーザーまたはポートによるホストへの"
+#~ "接続がサポートされません"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "別のポートを試すには、cockpit-ws を新しいバージョンにアップグレードする必"
+#~ "要があります。"
+
+#~ msgid "$0 template"
+#~ msgstr "$0 テンプレート"
+
+#~ msgid "Instance of template: "
+#~ msgstr "テンプレートのインスタンス: "
+
+#~ msgid "Instantiate"
+#~ msgstr "インスタンス化"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 シェア"
+
+#~ msgid "All In One"
+#~ msgstr "一体型"
+
+#~ msgid "Always"
+#~ msgstr "常時"
+
+#~ msgid "Author"
+#~ msgstr "作成者"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "passwd1 メカニズムに間違ったデータが渡されました"
+
+#~ msgid "CPU priority"
+#~ msgstr "CPU 優先度"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Docker に接続できません"
+
+#~ msgid "Change resource limits"
+#~ msgstr "リソース制限の変更"
+
+#~ msgid "Change resources limits"
+#~ msgstr "リソース制限の変更"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr ""
+#~ "Cockpit は {{#strong}}{{host}}{{/strong}} にログインできませんでした。"
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit は {{#strong}}{{host}}{{/strong}} にログインできませんでした。認証"
+#~ "情報は以下で変更できます。{{#can_sync}}{{#sync_link}}アカウントとパスワー"
+#~ "ドの同期{{/sync_link}}を実行できます。{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "合計メモリー使用量"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "$0 CPU コアの合計使用率"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "コマンドは空にすることができません"
+
+#~ msgid "Command:"
+#~ msgstr "コマンド:"
+
+#~ msgid "Commit"
+#~ msgstr "コミット"
+
+#~ msgid "Commit image"
+#~ msgstr "イメージのコミット"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Docker へ接続中"
+
+#~ msgid "Container name"
+#~ msgstr "コンテナー名"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "コンテナーは現在実行中でないと示されていますが、正常な停止に失敗しました。"
+
+#~ msgid "Container is currently running."
+#~ msgstr "コンテナーは現在実行中です。"
+
+#~ msgid "Container:"
+#~ msgstr "コンテナー:"
+
+#~ msgid "Containers"
+#~ msgstr "コンテナー"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "コンテナー"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "ユーザーグループを変更できませんでした"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "ユーザーパスワードを変更できませんでした"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "マシンに接続できませんでした"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "新規ユーザーを作成できませんでした"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "ローカルユーザーを一覧表示できませんでした"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "ユーザーを一覧表示できませんでした"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "ユーザーデータをロードできませんでした"
+
+#~ msgid "Created:"
+#~ msgstr "作成済み:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Docker コンテナー"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr ""
+#~ "Docker はインストールされていないか、システムでアクティベートされていませ"
+#~ "ん"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "重複するエイリアス"
+
+#~ msgid "Duplicate port"
+#~ msgstr "重複するポート"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "パスフレーズを入力し、ssh-agent に ID を追加します"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "ここで別のパスワードを入力すると、このマシンに接続するときに毎回そのパス"
+#~ "ワードを再入力する必要があります"
+
+#~ msgid "Environment"
+#~ msgstr "環境"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "ユーザーのロード中にエラーが発生しました: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Docker からのエラーメッセージ:"
+
+#~ msgid "Everything"
+#~ msgstr "すべて"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "終了した $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "コンテナーポートの公開"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Docker の起動に失敗しました: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Docker スコープの停止に失敗しました: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "新規イメージの取得"
+
+#~ msgid "IP address:"
+#~ msgstr "IP アドレス:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "IP プレフィックスの長さ:"
+
+#~ msgid "Id"
+#~ msgstr "ID"
+
+#~ msgid "Id:"
+#~ msgstr "Id:"
+
+#~ msgid "Image"
+#~ msgstr "イメージ"
+
+#~ msgid "Image $0"
+#~ msgstr "イメージ $0"
+
+#~ msgid "Image search"
+#~ msgstr "イメージ検索"
+
+#~ msgid "Image:"
+#~ msgstr "イメージ:"
+
+#~ msgid "Images"
+#~ msgstr "イメージ"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "イメージ"
+
+#~ msgid "Images and running containers"
+#~ msgstr "イメージおよび実行中のコンテナー"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "ユーザーを同期するには、{{#strong}}{{host}}{{/strong}} にログインする必要"
+#~ "があります。"
+
+#~ msgid "Invalid port"
+#~ msgstr "無効なポート"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Kerberos チケット"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "現在ログインしているユーザー {{#default_user}} ({{default_user}}){{/"
+#~ "default_user}} としてこのマシンに接続する場合は空白のままにします。別の"
+#~ "ユーザー名を入力すると、このマシンへの接続時にそのユーザーが常に使用されま"
+#~ "す。"
+
+#~ msgid "Link to another container"
+#~ msgstr "別のコンテナーへのリンク"
+
+#~ msgid "Links:"
+#~ msgstr "リンク:"
+
+#~ msgid "Login Password"
+#~ msgstr "ログインパスワード"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC アドレス:"
+
+#~ msgid "Memory limit"
+#~ msgstr "メモリー制限"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "マウントコンテナーボリューム"
+
+#~ msgid "No container specified"
+#~ msgstr "コンテナーが指定されていません"
+
+#~ msgid "No containers"
+#~ msgstr "コンテナーなし"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "現在のフィルターに一致するコンテナーがありません"
+
+#~ msgid "No images"
+#~ msgstr "イメージなし"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "現在のフィルターに一致するイメージがありません"
+
+#~ msgid "No results for $0"
+#~ msgstr "$0 の結果なし"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "現在のフィルターに一致するイメージがありません"
+
+#~ msgid "No running containers"
+#~ msgstr "実行中のコンテナーはありません"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "現在のフィルターに一致する実行中のコンテナーがありません"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "このシステム上の Docker にアクセスする権限がありません"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "障害発生時に、$0 回再試行します"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "$0 の強制削除を確定してください"
+
+#~ msgid "Please try another term"
+#~ msgstr "別の用語を試してください"
+
+#~ msgid "Ports:"
+#~ msgstr "ポート:"
+
+#~ msgid "Problems"
+#~ msgstr "問題"
+
+#~ msgid "ReadOnly"
+#~ msgstr "読み取り専用"
+
+#~ msgid "ReadWrite"
+#~ msgstr "読み書き"
+
+#~ msgid "Repository"
+#~ msgstr "リポジトリー"
+
+#~ msgid "Restart policy:"
+#~ msgstr "再起動ポリシー:"
+
+#~ msgid "Retries:"
+#~ msgstr "再試行回数:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "リモート接続でパスワードを再利用"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr "{{#strong}}{{host}}{{/strong}} と同期するユーザーを選択します"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "コンテナー環境変数の設定"
+
+#~ msgid "Severity:"
+#~ msgstr "重要度:"
+
+#~ msgid "Show all containers"
+#~ msgstr "すべてのコンテナーの表示"
+
+#~ msgid "Start Docker"
+#~ msgstr "Docker の起動"
+
+#~ msgid "State:"
+#~ msgstr "状態:"
+
+#~ msgid "Synchronize"
+#~ msgstr "同期"
+
+#~ msgid "Tag"
+#~ msgstr "タグ"
+
+#~ msgid "Tags"
+#~ msgstr "タグ"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "以下のコンテナーはこのイメージに依存しており、使用できなくなります。"
+
+#~ msgid "This image does not exist."
+#~ msgstr "このイメージは存在しません。"
+
+#~ msgid "Type a password"
+#~ msgstr "パスワードを入力します"
+
+#~ msgid "Type to filter…"
+#~ msgstr "フィルターのために入力します…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "停止されていない場合"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "サポートされないセットアップメカニズム"
+
+#~ msgid "Up since $0"
+#~ msgstr "$0 から稼働中"
+
+#~ msgid "Used by containers"
+#~ msgstr "コンテナーにより使用済み"
+
+#~ msgid "Using available credentials"
+#~ msgstr "利用可能な認証情報の使用"
+
+#~ msgid "Volumes"
+#~ msgstr "ボリューム"
+
+#~ msgid "Volumes:"
+#~ msgstr "ボリューム:"
+
+#~ msgid "With terminal"
+#~ msgstr "端末の使用"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}} に接続されていますが、ユーザーを同期するに"
+#~ "は、スーパーユーザー権限を持つユーザーが必要です。"
+
+#~ msgid "default"
+#~ msgstr "default"
+
+#~ msgid "key"
+#~ msgstr "鍵"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "名前、名前空間、または説明別の検索"
+
+#~ msgid "shares"
+#~ msgstr "共有"
+
+#~ msgid "to host port"
+#~ msgstr "ホストポートに対して"
+
+#~ msgid "value"
+#~ msgstr "value"
+
+#~ msgid "Master"
+#~ msgstr "マスター"
+
+#~ msgid "Password for $0"
+#~ msgstr "$0 のパスワード"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "ユーザー <b>$0</b> はサーバーを管理することを許可されていません"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "アカウント"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "ユーザー <b>$0</b> はアカウントを変更することを許可されていません"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "root アカウントを削除できません"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "root アカウントの名前を変更できません"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "新しいゾーンを追加する権限がありません"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "ゾーン $0 にサービスを追加する権限がありません"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "サービス $0 を削除する権限がありません"
+
+#~ msgid "Account Settings"
+#~ msgstr "アカウント設定"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "ダッシュボードへのマシンの追加"
+
+#~ msgid "Machines"
+#~ msgstr "マシン"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "ログインで、管理者の権限をエスカレートさせました"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "特権タスクの実行または他のマシンへの接続のためにパスワードを使用できません"
+
+#~ msgid "Privileged"
+#~ msgstr "権限"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "特権タスクの実行または他のマシンへの接続のためにパスワードを再使用します"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr "ユーザー $0 は、ホスト名を変更することを許可されていません"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr ""
+#~ "ユーザー $0 は、このサーバーをシャットダウンまたは再起動することを許可され"
+#~ "ていません"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "ユーザー <b>$0</b> は、プロファイルの変更を許可されていません"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "ユーザー <b>$0</b> はストレージを管理することを許可されていません"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr ""
+#~ "ユーザー <b>$0</b> はネットワーク設定を変更することを許可されていません"
+
+#~ msgid "Required by $0"
+#~ msgstr "$0 により必要"
+
+#~ msgid "Automatic Startup"
+#~ msgstr "自動起動"
+
+#~ msgid "Last Trigger"
+#~ msgstr "最後のトリガー"
+
+#~ msgid "Next Run"
+#~ msgstr "次回の実行日時"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr ""
+#~ "ユーザー <b>$0</b> はタイマーを作成するパーミッションを持っていません"
+
+#~ msgid "Connecting"
+#~ msgstr "接続中"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Cockpit について"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (OS と共有)"
+
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 脆弱性"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "ストレージの追加"
+
+#~ msgid "Add Storage"
+#~ msgstr "ストレージの追加"
+
+#~ msgid "Additional Storage"
+#~ msgstr "追加のストレージ"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "選択されたディスク上のすべてのデータが削除され、ストレージプールにディスク"
+#~ "が追加されます。"
+
+#~ msgid "Configure storage..."
+#~ msgstr "ストレージの設定 ..."
+
+#~ msgid "Could not add all disks"
+#~ msgstr "すべてのディスクを追加することはできませんでした"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "コンテナーの削除とストレージプールのリセット"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr "Docker ストレージプールに関する情報は利用できません。"
+
+#~ msgid "Local Disks"
+#~ msgstr "ローカルディスク"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "追加ローカルストレージがありません。"
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "ディスクの再フォーマットおよび追加"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "ストレージプールをリセットすると、すべてのコンテナーが削除され、プール内の"
+#~ "ディスクが解放されます。"
+
+#~ msgid "Security"
+#~ msgstr "Security"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr "Docker ストレージプールはこのシステムで管理できません。"
+
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "$time ($type) のスキャンで、脆弱性が $count 見つかりました:"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "$time ($type) のスキャンでは、脆弱性は見つかりませんでした。"
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "$time ($type) のスキャンは成功しませんでした。"
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "仮想化サービスは利用可能です"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "Docker ストレージプールを管理するパーミッションがありません。"
+
+#~ msgid "$0 GiB / $1 GiB"
+#~ msgstr "$0 GiB / $1 GiB"
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} -> $cport"
+
+#~ msgid "Hide Performance Options"
+#~ msgstr "パフォーマンスオプションを非表示にします"
+
+#~ msgid "Show Performance Options"
+#~ msgstr "パフォーマンスオプションを表示します"
diff --git a/po/ka.po b/po/ka.po
new file mode 100644
index 0000000..a9ef8ea
--- /dev/null
+++ b/po/ka.po
@@ -0,0 +1,10451 @@
+# #-#-#-#-# cockpit.c.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# #-#-#-#-# cockpit.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# #-#-#-#-# cockpit.appstream.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# #-#-#-#-# cockpit.polkit.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2024-02-05 13:36+0000\n"
+"Last-Translator: Temuri Doghonadze <temuri.doghonadze@gmail.com>\n"
+"Language-Team: Georgian <https://translate.fedoraproject.org/projects/"
+"cockpit/main/ka/>\n"
+"Language: ka\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.3.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "მომხმარებლების #"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+msgstr[1] "$0 ცალი CPU"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 გიბ"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 აქტიური ზონა"
+msgstr[1] "$0 ცალი აქტიური ზონა"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "ხელმისაწვდომია $0"
+
+#: pkg/storaged/block/resize.jsx:266
+msgid "$0 can not be made larger"
+msgstr "$0 მეტად ვეღარ გაიზრდება"
+
+#: pkg/storaged/block/resize.jsx:263
+msgid "$0 can not be made smaller"
+msgstr "$0 მეტად ვეღარ დაპატარავდება"
+
+#: pkg/storaged/block/resize.jsx:259
+msgid "$0 can not be resized"
+msgstr "$0 ზომას ვერ შეცვლით"
+
+#: pkg/storaged/block/resize.jsx:255
+msgid "$0 can not be resized here"
+msgstr "$0-ის ზომას აქ ვერ შეცვლით"
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "ნაგლეჯის ზომა $0"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 კრიტიკული დარტყმა"
+msgstr[1] "$0 დარტყმა, კრიტიკულის ჩათვლით"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 მონაცემები + $1 დამატებით გამოყენებული $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 დღე"
+msgstr[1] "დღეები: $0"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 დისკი აკლია"
+msgstr[1] "$0 ცალი დისკი აკლია"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 დისკი აკლია"
+msgstr[1] "$0 ცალი დისკი აკლია"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 დისკი"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 დოკუმენტაცია"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 შეცდომა"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0-ის გამოსვლის კოდია $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 წარუმატებელია"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "შესვლის $0 წარუმატებელი მცდელობა"
+msgstr[1] "შესვლის $0 ცალი წარუმატებელი მცდელობა"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "$0 filesystem"
+msgstr "ფაილური სისტემა $0"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 თავისუფალი"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 საათი"
+msgstr[1] "საათი: $0"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 მნიშვნელოვანი მოხვედრა"
+msgstr[1] "$0 მოხვედრა, მნიშვნელოვნის ჩათვლით"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 არსებული ფაილია"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 გამოიყენება"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 ხელმიუწვდომელია ყველა რეპოზიტორიიდან."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 გასაღები შეიცვალა"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 მოკვდა სიგნალით $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 დაბალი სერიოზულობის მოხვედრა"
+msgstr[1] "$0 ცალი დაბალი სერიოზულობის მოხვედრა"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 წუთი"
+msgstr[1] "წუთი: $0"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 საშუალო მოხვედრა"
+msgstr[1] "$0 მოხვედრა, საშუალოს ჩათვლით"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 თვე"
+msgstr[1] "თვე: $0"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "კიდევ $0..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 პაკეტი"
+msgstr[1] "$0 ცალი პაკეტი"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 პაკეტს სჭირდება სისტემის გადატვირთვა"
+msgstr[1] "$0 ცალ პაკეტს სჭირდება სისტემის გადატვირთვა"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 გვერდი"
+msgstr[1] "$0 ცალი გვერდი"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+msgid "$0 partitions"
+msgstr "$0 დანაყოფი"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "ხელმისაწვდომია $0 უსაფრთხოების განახლება"
+msgstr[1] "ხელმისაწვდომია $0 ცალი უსაფრთხოების განახლება"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 სერვისის შეცდომა"
+msgstr[1] "$0 ცალი სერვისის შეცდომა"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 სერვისს სჭირდება სისტემის გადატვირთვა"
+msgstr[1] "$0 ცალ სერვისს სჭირდება სისტემის გადატვირთვა"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "დარჩენილია $0 სლოტი"
+msgstr[1] "დარჩენილი სლოტების რაოდენობა $0"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 პიკი"
+msgstr[1] "$0 პიკი"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0 სინქრონიზებულია"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "ჯამში $0"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "ხელმისაწვდომია $0 განახლება"
+msgstr[1] "ხელმისაწვდომია $0 ცალი განახლება"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "გამოყენებულია $0 $1-დან (შენახულია $2)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 კვირა"
+msgstr[1] "კვირა: $0"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "დაყენდება $0."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 წელი"
+msgstr[1] "წელი: $0"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 ზონა"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: ავარია $1-თან"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name ($host-დან)"
+
+#: pkg/storaged/dialog.jsx:1218
+msgid "(Not part of target)"
+msgstr "(სამიზნის ნაწილი არაა)"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+msgid "(no assigned mount point)"
+msgstr "(მინიჭებული მიმაგრების წერტილის გარეშე)"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+msgid "(not mounted)"
+msgstr "(მიმაგრებული არაა)"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(რეკომენდებულია)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", $1 უსაფრთხოების პაჩი"
+msgstr[1] ", $1 ცალი უსაფრთხოების პაჩი"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 მიბ"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 დღე"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 საათი"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 წთ"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 წთ"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 კვირა"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "მეათე"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "მეთერთმეტე"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 კიბ"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "მეთორმეტე"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13-ე"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14-ე"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 წთ"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15-ე"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 კიბ"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16-ე"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17-ე"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18-ე"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19-ე"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "პირველი"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 მიბ"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 წთ"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20-ე"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21-ე"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22-ე"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23-ე"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24-ე"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25-ე"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26-ე"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27-ე"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28-ე"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29-ე"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "მეორე"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30-ე"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31-ე"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 კიბ"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "მესამე"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 კიბ"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 წთ"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "მეოთხე"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 წთ"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 წთ"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 კიბ"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "მეხუთე"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "5 სთ"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 წთ"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 კიბ"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "მეექვსე"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "მეშვიდე"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 კიბ"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "მერვე"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "მეცხრე"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "$0-ზე არ აყენია Cockpit-ის თავსებადი ვერსია."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "ფაილური სისტემა ამ სახელით მითითებულ პულში უკვე არსებობს."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "ჯგუფი ამ სახელით უკვე არსებობს"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"უსაფრთხოებისთვის, წარმადობისთვის და საიმედოობისთვის საჭიროა ახალი ბრაუზერი."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"ქსელური bond აერთიანებს ქსელის მრავალ ინტერფეისს ერთ ლოგიკურ ინტერფეისში "
+"უფრო მაღალი გამტარობის ან წვდომადომის მისაღებად."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"ახალი SSH გასაღები $0-თვის შეიქნება $1-თვის $2-ზე და დაემატება $3 ფაილს $4-"
+"ის ფაილს $5-ზე."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] "ცვლილებების სანახავად პაკეტს სისტემის გადატვირთვა სჭირდება:"
+msgstr[1] "ზოგიერთ პაკეტს ცვლილებების სანახავად სისტემის გადატვირთვა სჭირდება:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "პული ამ სახელით უკვე არსებობს."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] "განსაახლებლად სერვისს სჭირდება სისტემის გადატვირთვა:"
+msgstr[1] "განსაახლებლად სერვისებს სჭირდებათ სისტემის გადატვირთვა:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "ტომების ჯგუფს, რომელსაც ფიზიკური ტომები აკლია, სახელს ვერ გადაარქმევთ."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP-ის მონიტორინგი"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "ვებ კონსოლის შესახებ"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "აკლია"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "მიიღეთ გასაღები და შედით"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "მისაღები პაროლი"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "ანგარიშის ვადა"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "ანგარიში ხელმიუწვდომელია ან ჩასწორებას არ ექვემდებარება."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "ანგარიშები"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "ქმედება"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "ქმედებები"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "აქტივაცია"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr "აქტივაცია ზომის შეცვლამდე"
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "$target-ის აქტივაცია"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "აქტიური"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "აქტიური მარქაფი"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "აქტიური გვერდები"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "აქტიურია "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "აქტიური მდგომარეობა"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "დატვირთვის ადაპტიური გადანაწილება"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "გადაცემის დატვირთვის ადაპტაციური გადანაწილება"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "დამატება"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "$0-ის დამატება"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "DNS სერვერის დამატება"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "ქსელზე მიბმული დისკის დაშიფვრის დამატება"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Tang გასაღებების სერვერის დამატება"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "VLAN-ის დამატება"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "VPN-ის დამატება"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "WireGuard VPN-ის დამატება"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "ბიტური რუკის დამატება"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "ახალი ზონის დამატება"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "მისამართის დამატება"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "ბლოკური მოწყობილობების დამატება"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Bond-ის დამატება"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "ხიდის დამატება"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "დისკის დამატება"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "დისკების დამატება"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "iSCSI პორტალის დამატება"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "გასაღების დამატება"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "გასაღებების სერვერის დამატება"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "წევრის დამატება"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "ახალი ჰოსტის დამატება"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "ახალი ზონის დამატება"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "საკვანძო ფრაზის დამატება"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "პარტნიორის დამატება"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+msgid "Add physical volume"
+msgstr "ფიზიკური ტომის დამატება"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "პორტების დამატება"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "პორტების დამატება ზონაში $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "საჯარო გასაღების დამატება"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "რაუტის დამატება"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "საძებნი დომენის დამატება"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "სერვისების დამატება"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "სერვისების დამატება ზონაში $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "სერვისების დამატება ზონაში $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "გუნდის დამატება"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "ზონის დამატება"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "\"$0\"-ის დამატება დაშიფვრის პარამეტრებში"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "\"$0\"-ის დამატება ფაილური სისტემის პარამეტრებში"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0-ის დამატება გაწყვეტს კავშირს სერვერთან და ადმინისტრირების ინტერფეისს "
+"ხელმიუწვდომელს გახდის."
+
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"ახალი გასარებების სერვერის დასამატებლად საჭიროა პულის განბლოკვა. შეიყვანეთ "
+"არსებული პულის საკვანძო ფრაზა."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"მითითებული პორტების დაატება იწვევს firewalld-ის გადატვირთვას. ეს კი იწვევს "
+"გაშვებული კონფიგურაციის(ინახება მეხსიერებაში) დაკარგვას!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "გასაღების დამატება"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "$target-ზე ფიზიკური მოცულობების დამატება"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "ბირთვის ბრძანების სტრიქონში rd.neednet=1 -ის დამატება"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "დამატებითი DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "DNS-ის დამატებითი საძებნი დომენები $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "დამატებითი ქმედებები"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "დამატებითი მისამართი $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "დამატებითი პაკეტები:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "დამატებითი პორტები"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "მისამართი"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "მისამართი $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "მისამართი არ შეიძლება ცარიელი იყოს"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "მისამართი არ წარმოადგენს სწორ URL-ს"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "მისამართები"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "მისამართის ფორმატი არასწორია"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Cockput ვებ კონსოლით ადმინისტრირება"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "ადმინისტრატიული წვდომა"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr ""
+"რეპორტების შესაქმნელად საჭიროა გადაერთოთ \"ადმინისტრატორის წვდომა\"-ზე."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "საჭიროა ადმინისტრაციული წვდომა"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "დამატებითი TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "შემდეგ"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"დომენიდან გამოსვლის შემდეგ ამ მანქანაზე შესვლა შეეძლებათ მხოლოდ ლოკალური "
+"ანგარიშების მქონე მომხმარებლებს. ასევე შეიძლება შეფერხებით იმუშაოს სხვა "
+"ისეთმა სერვისებმა, როგორიცაა მაგალითად DNS და შეიცვლება სანდო CA-ები."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "სისტემის ჩატვირთვის შემდეგ"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "გაფრთხილება"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "გაფრთხილება და უარესები"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "მეტსახელი"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "ყველა"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr "ყველა $0 მონიშნული ფიზიკური ტომია საჭირო არჩეული განლაგებისთვის."
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "ყველა განახლება"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "ყველა-ერთში"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "გაშვების ნების დართვა (ნიღბის მოხსნა)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "დაშვებული IP-ები"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "ნებადართული მისამართები"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "საჭიროა დამატებით $0-ის მონიშვნა"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Ansible-ის როლების დოკუმენტაცია"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"ჟურნალის შეტყობინებებში შესაძლებელია ნებისმიერი სტრიქონის გაფიტვრა. სტრიქონი "
+"ასევე შეიძლება მოიძებნოს რეგულარული გამოსახულებებითაც. ასევე არსებობს "
+"ჟურნალის ველების მიხედვით გაფილტვრის მხარდაჭერა. წარმოადგენენ ჰარეთი გაყოფილ "
+"მნისვნელობებს FIELD=VALUE ფორმაში, სადაც მნიშვნელობები შეიძლება "
+"წარმოადგენდეს მძიმით გამოყოფილ შესაძლო მნიშვნელობების სიას."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "გარეგნობა"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "აპლიკაციის ინფორმაცია ვერ ვიპოვე"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "აპლიკაციები"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "აპების სია"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "გადატარება და გადატვირთვა"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "გაშვებული ბირთვის პაჩების გადატარება"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "ამ გადაწყვეტის გადატარება"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "ახალი წესების გადატარება... შეიძლება რამდენიმე წუთი დასჭირდეს."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "გადაწყვეტის გადატარება..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "განახლებების გადატარება"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "განახლებების გადატარების შეცდომა"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "სათანადოა კრიტიკული მიმაგრების წერტილებისთვის, როგორიცაა /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "აპები"
+
+#: pkg/storaged/drive/drive.jsx:102
+msgid "Assessment"
+msgstr "შეფასება"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "აქტივის ჭდე"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "ჩატვირთვისას"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "საჭიროა $0 დისკი მაინც."
+msgstr[1] "საჭიროა $0 ცალი დისკი მაინც."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "საჭიროა ერთი ბლოკური ტიპის მოწყობილობა მაინც."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "საჭიროა ერთი დისკი მაინც."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr "აუცილებელია, სულ ცოტა, ერთი მშობელი ჩაწერადად იყოს მიმაგრებული"
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "1 წთ"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "წმ"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "მითითებულ დროს"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "ატრიბუტები"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "აუდიტის ჟურნალი"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "ავთენტიკაცია"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "ავთენტიკაცია"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "ავთენტიკაცია"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "ავთენტიკაციის შეცდომა"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "ავთენტიკაციის შეცდომა: სერვერმა კავშირი დახურა"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Cockpit ვებ კონსოლში პრივილეგირებული ამოცანების შესასრულებლად საჭიროა "
+"ავთენტიკაცია"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "საჭიროა ავთენტიკაცია"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "SSH გასაღების ავტორიზაცია"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "ავტორიზებული საჯარო SSH გასაღებები"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "ავტომატური"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "ავტომატური (მხოლოდ DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "ავტომატური შესვლა"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "ავტომატური განახლებები"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "ავტომატურად გაეშვება"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "ავტომატურად, NTP-ით"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "დამატებითი NTP სერვერების ავტომატური გამოყენება"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "მითითებული NTP სერვერების ავტომატური გამოყენება"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "ავტომატიზაციის სკრიპტი"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "ხელმისაწვდომი სამიზნე $0-ზე"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "ხელმისაწვდომი განახლებები"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+msgid "BIOS boot partition"
+msgstr "BIOS ჩატვირთვის დანაყოფი"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "BIOS-ის თარიღი"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "BIOS-ის ვერსია"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "ანგარიშებზე დაბრუნება"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "ცუდი"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "ცუდი პარამეტრი"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "ბალანსერი"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "წინ"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "ებმება"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "შავი"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blade"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "კალათი"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "ბლოკური მოწყობილობა"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "ბლოკური მოწყობილობები ფაილური სისტემებისთვის"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "ბლოკური მოწყობილობები"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "დაბლოკილია"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "ინტერფეისების გადაბმა"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "ჩატვირთვა"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"ჩატვირთვა ავარიულია, თუ ფაილური სისტემა მიმაგრებული არ იქნება, რის შედეგადაც "
+"დაშორებული წვდომა არ გექნებათ"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "ჩატვირთვა გაგრძელდება, თუ ფაილური სისტემა მიმაგრებული არაა"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "მიბმულია"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "ხიდი"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "ხიდის პორტი"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "ხიდის პორტის მორგება"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "გადაცემა"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "გაფუჭებული კონფიგურაცია"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr "Btrfs ტომი მიმაგრებულია"
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "ხელმისაწვდომია შეცდომების გასწორებები"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "შეცდომები"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "მატარებლის გაფართოების შასი"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"$1-ის $2-ზე შესასვლელი SSH გასაღების, $0-ზე პაროლის შეცვლით თქვენი გასაღები "
+"ავტომატურად გახდება ხელმისაწვდომი. ოპერაციის შემდეგ $3-ზე შესვლას პაროლის "
+"გარეშე შეძლებთ."
+
+#: pkg/static/login.html:61
+msgid "Bypass browser check"
+msgstr "ბრაუზერის შემოწმების გამოტოვება"
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "CPU უსაფრთხოება"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "CPU-ის უსაფრთხოების გადამრთველები"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "CPU-ის გამოყენება"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "CPU-ის გამოყენება/დატვირთვა"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "ქეში"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "შეიძლება იყოს ჰოსტის სახელი, IP მისამართი, DNS სახელი ან ssh://URI"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr ""
+"მიმდინარე ფილტრების კომბინაციის ჟურნალში ჩანაწერების მოძებნა შეუძლებელია."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "გაუქმება"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "გამორთვის გაუქმება"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "გადატვირთვის გაუქმება"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "ვერ ჩაირთვება"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "უცნობ ჰოსტთან მიერთების პრობლემა"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "მომხმარებლისადაპაროლის გადაგზავნის შეცდომა"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr "ამ სისტემაზე realmd-ს არარსებობის გამო დომენში ჩართვა შეუძლებელია"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "მოვლენის წარსულ დროში დანიშვნა შეუძლებელია"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "მოცულობა"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "გადამზიდი"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "შეცვლა"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "კრიპტოგრაფიული პოლიტიკის შეცვლა"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "ჰოსტის სახელის შეცვლა"
+
+#: pkg/storaged/overview/overview.jsx:152
+msgid "Change iSCSI initiater name"
+msgstr "iSCSI ინიციატორის სახელის შეცვლა"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "iSCSI ინიციატორის სახელის შეცვლა"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+msgid "Change label"
+msgstr "ჭდის შეცვლა"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "საკვანძო ფრაზის შეცვლა"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "პაროლის შეცვლა"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "წარმადობის პროფილის შეცვლა"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "პროფილის შეცვლა"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "გარსის შეცვლა"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "სისტემური დროის შეცვლა"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "$0-ის პაროლის შეცვლა"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "პარამეტრების შეცვლა"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"შეცვლილი გასაღებები ხშირად ოპერაციული სისტემის გადაყენებაზე მიუთითებს. ამავე "
+"დროს მოულოდნელი ცვლილება კავშირის გადასაჭერად მესამე პირის ჩარევასაც "
+"შეიძლება ნიშნავდეს."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+"დანაყოფის ტიპების შეცვლამ, შეიძლება, სისტემის ჩატვირთვას ხელი შეუშალოს."
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"ამ პარამეტრის ცვლილებები გამოიწვევს კავშირის წვეტას და გათიშავს "
+"ადმინისტრირების ინტერფეისს."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "განახლებების შემოწმება"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"შემწმება, ბრძანების სტრიქონიდან მოსული SHA-256 ან SHA-1 ჰეში ფანჯრისას თუ "
+"ემთხვევა."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "გასაღების ჰეშის Tang სერვერთან შემოწმება."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "$target-ის შემოწმება"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "IP-ის შემოწმება"
+
+#: pkg/storaged/jobs-panel.jsx:68
+msgid "Checking MDRAID device $target"
+msgstr "MDRAID მოწყობილობის, $target-ის შემოწმება"
+
+#: pkg/storaged/jobs-panel.jsx:69
+msgid "Checking and repairing MDRAID device $target"
+msgstr "MDRAID მოწყობილობა $target-ის შემოწმება და შეკეთება"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "$0 პაკეტის შემოწმება"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Initrd-ში NBDE-ის მხარდაჭერის შემოწმება"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "ახალ აპლიკაციებზე შემოწმება"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "პაკეტების განახლებების შემოწმება..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "დაყენებული პროგრამული უზრუნველყოფის შემოწმება"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "შესაბამისი პროცესების შემოწმება"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "პროგრამის სტატუსის შემოწმება"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "აირჩიეთ აპლიკაციის ენა"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "ბლოკის ზომა"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "კლასი"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "$target-ის გასუფთავება"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "'გაშვების შეცდომა'-ის გასუფთავება"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "ყველა ფილტრის გასუფთავება"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "ძებნის გასუფთავება"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "მოწყობილობის შექმნა"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "კლიენტი პროგრამა"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "დახურვა"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "მონიშნული გვერდების დახურვა"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Cockpit-ის ავთენტიკაციის არასწორი კონფიგურაცია."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "NetworkManager-ის და Firewalld-ის მორგება Cockpit-ით"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit-ს მითითებულ ჰოსტთან დაკავშირება არ შეუძლია ."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit-ის შიდა შეცდომა."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit წარმოადგენს სერვერის მმართველს, რომლითაც Linux სერვერების "
+"ადმინისტრირება ბრაუზერითაც შეგიძლიათ. ტერმინალსა და ვებ ხელსაწყოს შორის "
+"გადართვა პრობლემა არაა. Cockpit-ით გაშვებული სერვისი შეგიძლიათ გააჩეროთ "
+"ტერმინალთაც. ასევე, თუ შეცდომა დაფიქსირდება ტერმინალში, მისი ნახვა Cockpit-"
+"ის საშუალებითაც შეგიძლიათ."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr ""
+"Cockpit-ი წარმოადგენს ლინუქსის სერვერების ადმინისტრირების ინტერაქტიულ "
+"ინტერფეისს."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr ""
+"Cockpit-ი შეუთავსებელია თქვენს სერვერზე დაყენებულ პროგრამულ უზრუნველყოფასთან."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit-ი დაყენებული არაა"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit-ი ამ სისტემაზე დაყენებული არაა."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit შესანიშნავია ახალი სისტემური ადმინისტრატორებისთვის. ის მათ "
+"საშუალებას აძლევს ადვილად შეასრულონ ისეთი მარტივი ამოცანები, როგორიცაა "
+"შენახვის ადმინისტრირება, ჟურნალების შემოწმება და სერვისების დაწყება და "
+"გაჩერება. შეგიძლიათ ერთდროულად რამდენიმე სერვერის მონიტორინგი და "
+"ადმინისტრირება. უბრალოდ დაამატეთ ისინი ერთი დაწკაპუნებით და თქვენი მანქანები "
+"იზრუნებს მის მეგობრებზე."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit-ი თქვენს ბრაუზერში შეიძლება არასწორად გამოჩნდეს"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "მოაგროვეთ დიაგნოსტიკური და მხარდაჭერის მონაცემები"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "ოპერაციული სისტემის ბირთვის ავარიის დამპები"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "მეტრიკების შეგროვება"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "ფერი"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr ""
+"შესაძლო მნიშვნელობებია მძიმით გამოყოფილი პორტები, დიაპაზონები და სერვისები"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "ბრძანება"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "ბრძანება ნაპოვნი არაა"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "კომენტარი"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "tuned-თან კავშირის შეცდომა"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "კომპაქტური PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "თავსებადია ყველა სისტემასა და მოწყობილობასთან (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "თავსებადია თანამედროვე სისტემებთან და >2TB დისკებთან (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "გამორთვის დამპების შეკუმშვა დისკის დაზოგვის მიზნით"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "შეკუმშვა"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "პირობა $0=$1 არ შესრულდა"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "პირობის შეცდომა"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "მორგება"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "მორგება"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "IP-ის მორგება"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "kdump-ის მორგება"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "სისტემის პარამეტრების მორგება"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "დასტური"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "დაადასტურეთ $0-ის წაშლა"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "მეორედ შეიყვანეთ გასაღების პაროლი"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "დაადასტურეთ გასაღების ახალი პაროლი"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "მეორედ შეიყვანეთ ახალი პაროლი"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "დაადასტურეთ პაროლი"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "დაადასტურეთ $0-ის წაშლა"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "არასავალდებულო საკვნძო ფრაზის წაშლის დადასტურება"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "დაადასტურეთ $0-ის გაჩერება"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "კონფლიქტშია"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "კონფლიქტები"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "ავტომატურად დაკავშირება"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "დაკავშირება"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "დაკავშირება:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "SETroubleshoot-ის დემონთან დაკავშირება..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "dbus-თან დაკავშირების შეცდომა: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "მანქანასთან დაკავშირება"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "დაკავშირების შეცდომა"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "დაკავშირების შეცდომა"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "კავშირის დრო გავიდა."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "კავშირი დაიკარგება"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "შედგება"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "დომენთან კავშირი წარმატებულია"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "შეიცავს:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "გაგრძელება"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "სესიის გაგრძელება"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "კონტროლი"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "კონტროლი"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "კონტროლერი"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "გარდაქმნადი"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "დაკოპირებულია"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "კოპირება"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "ბაფერში კოპირება"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "ბირთვი $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "$0-ს ვერ დავუკავშირდი"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "ავარიული დამპის მდებარეობა"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "ავარიის ანგარიში"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "ავარიული სისტემა"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "შექმნა"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "LVM2 საცავის ჯგუფის შექმნა"
+
+#: pkg/storaged/overview/overview.jsx:145
+msgid "Create MDRAID device"
+msgstr "MDRAID მოწყობილობის შექმნა"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "RAID მოწყობილობის შექმნა"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Stratis-ის პულის შექმნა"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "შექმენით SSH-ის ახალი გასაღები და გაატარეთ ავტორიზაცია"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "ფაილური სისტემა $0-ის მყისიერი ასლის გადაღება"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "შექმენით ანგარიში არა-უნიკალური UID-ით"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "შექმენით ანგარიში სუსტი პაროლით"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "საწყისი საქაღალდის შექმნა და მისი მფლობელის შეცვლა"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "შექმნა და მიმაგრება"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Create and start"
+msgstr "შექმნა და გაშვება"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "ფაილური სისტემის შექმნა"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "შექმნა"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "ლოგიკური ტომის შექმნა"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "ახალი ანგარიშის შექმნა"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "ახალი ფაილური სისტემის შექმნა"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "ახალი ჯგუფის შექმნა"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "ახალი ლოგიკური ტომის შექმნა"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "ახალი ამოცანის ამ შემცველობით შექმნა."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+msgid "Create new thinly provisioned logical volume"
+msgstr "ახალი თხლად გამოყოფილი ლოგიკური ტომის შექმნა"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "მხოლოდ შექმნა"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "განაყოფის შექმნა"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "$0-ზე სექციის შექმნა"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "დაყოფის ცხრილის შექმნა"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "სწრაფი ასლის შექმნა"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "სწრაფი ასლის შექმნა და მიმაგრება"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "მხოლოდ სწრაფი ასლის შექმნა"
+
+#: pkg/storaged/overview/overview.jsx:174
+msgid "Create storage device"
+msgstr "საცავის მოწყობილობების შექმნა"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+msgid "Create subvolume"
+msgstr "ქვეტომის შექმნა"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "თხელი ტომის შექმნა"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "ტაიმერის შექმნა"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "ტომების ჯგუფის შექმნა"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "შეიქმნა"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "LVM2 საცავების ჯგუფი $target-ის შექმნა"
+
+#: pkg/storaged/jobs-panel.jsx:67
+msgid "Creating MDRAID device $target"
+msgstr "MDRAID მოწყობილობა $target-ის შექმნა"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "VDO მოწყობილობის შექმნა"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "$target-ზე ფაილური სისტემის შექმნა"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "ლოგიკური საცავს $target-ის შექმნა"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "დანაყოფის შექმნა $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "$taget-ის სწრაფი ასლის შექმნა"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0-ის შექმნა გაწყვეტს სერვერთან კავშირს. ადმინისტრირების ინტერფეისი "
+"მიუწვდომელი გახდება."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "კრიტიკული და ზემოთ"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"კრიპტოგრაფიის წესები წარმოადგენს სისტემურ კომპონენტს, რომელიც ძირითადი "
+"კრიპტოგრაფიული ქვესისტემების, TLS/IPSec/SSH/DNNSEC და kerberos-ის "
+"კონფიგურაციას უზრუნველყოფს."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "კრიპტოგრაფიის პოლიტიკა"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "კრიპტოგრაფიის პოლიტიკა არამდგრადია"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "მიმდინარე ჩატვირთვა"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "CPU-ის მიმდინარე დატვირთვა"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "ამჟამად გამოიყენება"
+
+#: pkg/kdump/kdump-view.jsx:535
+msgid "Currently not supported"
+msgstr "ამჟამად მხარდაჭერილი არაა"
+
+#: pkg/storaged/partitions/partition.jsx:146
+msgid "Custom"
+msgstr "ხელით"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "კრიპტოგრაფიის პოლიტიკის მორგება"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "მიმაგრების მორგება"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "ხელით მითითებული პორტები"
+
+#: pkg/storaged/partitions/partition.jsx:180
+msgid "Custom type"
+msgstr "მომხმარებლის ტიპი"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "ხელით მითითებული ზონები"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "DEFAULT-ი SHA-1 ხელმოწერის გადამოწმებით დაშვებულია."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS-ში საძებნი დომენები"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "DNS-ში საძებნი დომენები $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "დღიურად"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "საშიშროების შეტყობინება:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "ბლენი"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "მონაცემები"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "გამოყენებული მონაცემები"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"მონაცემები 2 ასლად იქნება შენახული და კიდევ, ცვალებადი მიდგომით, მონიშნულ "
+"ფიზიკურ ტომებზე, რათა გაუმჯობესდეს ორივე, სანდოობა და წარმადობა. საჭიროა, "
+"მინიმუმ, 4 ტომის მონიშვნა."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"მონაცემები შენახული იქნება ორი ან მეტი ასლის სახით მონიშნულ ფიზიკურ ტომებზე, "
+"რათა საიმედოობა გაზარდოს. საჭიროა, მინიმუმ, 2 ტომის მონიშვნა."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+"მონაცემები შენახული იქნება მონიშნულ ფიზიკურ ტომებზე ცვალებადი სახით, რათა "
+"წარმადობა გაიზარდოს. საჭიროა, მინიმუმ, 2 ტომი."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"მონაცემები შეინახება მონიშნულ ფიზიკურ ტომებზე ისე, რომ შეგიძლიათ, ერთ-ერთი "
+"მათგანი მონაცემების დაკარგვის გარეშე დაკარგოთ. საჭიროა მინიმუმ 3 ტომი."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+"მონაცემები შენახული იქნება მონიშნულ ფიზიკურ ტომებზე ისე, რომ ერთ-ერთი "
+"მათგანის დაკარგვა მონაცემების დაკარგვის გარეშე შეგიძლიათ. მონაცემები "
+"შენახული იქნება მონაცვლეობითი მიმდევრობით, წარმადობის გასაზრდელად. საჭიროა "
+"სულ ცოტა სამი ტომის მონიშვნა."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+"მონაცემები შენახული იქნება მონიშნულ ფიზიკურ ტომებზე ისე, რომ, ერთდროულად, "
+"ორი მათგანი შეგეძლებათ დაკარგოთ, მონაცემების კარგვის გარეშე. მონაცემები "
+"შენახულია მონაცვლეობითი მიმდევრობით, რათა წარმადობა გაზარდოს. საჭიროა "
+"მინიმუმ ხუთი ტომის არჩევა."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+"მონაცემები მონიშნულ ტომებზე დამატებითი საიმედოობის თუ წარმადობის გაზრდის "
+"გარეშე იქნება დამახსოვრებული."
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"თარიღის სპეციფიკაციები უნდა იყოს YYYY-MM-DD ფორმატის სთ:მთ:წ.წ. "
+"ალტერნატიულად გაგებულია სტრიქონები \"გუშინ\", \"დღეს\", \"ხვალ\". \"ახლა\" "
+"ეხება მიმდინარე დროს. და ბოლოს, შედარებითი დრო შეიძლება იყოს მითითებული, "
+"პრეფიქსით '-' ან '+'"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "დეაქტივაცია"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+msgid "Deactivate logical volume $0/$1?"
+msgstr "მოხდეს ლოგიკური ტომის, $0/$1 დეაქტივაცია?"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "დეაქტივაცია"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "$target-ის დეაქტივაცია"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "გამართვა და ზემოთ"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "ერთით შემცირება"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "გამოყოფილი ლუწობა (RAID 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "დედუპლიკაცია"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "ნაგულისხმები"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "დაყოვნება"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "დაყოვნება რიცხვი უნდა იყოს"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "წაშლა"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "$0-ის წაშლა"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "ანგარიშის წაშლა"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "ფაილების წაშლა"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "ჯგუფის წაშლა"
+
+#: pkg/storaged/stratis/pool.jsx:292
+msgid "Delete pool"
+msgstr "პულის წაშლა"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "წავშალო ანგარიში სამუდამოდ?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0-ის წაშლა შეწყვეტს კავშირს სერვერთან და ადმინისტრაციის ინტერფეისს "
+"მიუწვდომელს გახდის."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "$target-ის წაშლა"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "LVM2 საცავების ჯგუფი $taget-ის წაშლა"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "Stratis-ის პულის წაშლა მასზე ჩაწერილი მონაცემების წაშლასაც გამოიწვევს."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "ფაილური სისტემის წაშლა მასზე არსებული მონაცემების წაშლასაც გამოიწვევს."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "ლოგიკური საცავის წაშლა მასზე მდებარე მონაცემების წაშლასაც გამოიწვევს."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "დანაყოფის წაშლა მასზე მდებარე მონაცემების წაშლასაც გამოიწვევს."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+msgid "Deleting erases all data on a MDRAID device."
+msgstr ""
+"MDRAID მოწყობილობის წაშლის შემთხვევაში ასევე წაიშლება ზედ მდებარე "
+"მონაცემებიც."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr ""
+"VDO მოწყობილობის წაშლის შემთხვევაში ასევე წაიშლება ზედ მდებარე ყველა "
+"მონაცემი."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "საცავების ჯგუფის წაშლისას ასევე წაიშლება ზედ მდებარე მონაცემებიც."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "წაშლა გაანადგურებს მონაცემებს ამ ქვეტომზე და ყველა მის შვილზე."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "ასევე წაიშლება შემდეგი ფაილები:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "აღწერა"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "სამუშაო მაგიდა"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "მოძრობადი"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "დეტალები"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "განვითარება"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "მოწყობილობა"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "მოწყობილობის ფაილი"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "მოწყობილობა მხოლოდ კითხვადია"
+
+#: pkg/storaged/block/other.jsx:60
+msgid "Device number"
+msgstr "მოწყობილობის ნომერი"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "დიაგნოსტიკის ანგარიშები"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "საქაღალდე"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "საქაღალდე $0 არ არსებობს ან არ გაქვთ ჩაწერის წვდომა."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "მრავალნაკადიანობის გამორთვა"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "ბრანდმაუერის გამორთვა"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "tuned-ის გამორთვა"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "გათიშულია"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "პაროლის ხელით შეყვანის აკრძალვა"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "პაროლით შესვლის აკრძალვა"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "გაშვების აკრძალვა (შენიღბვა)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "გათიშვა"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "გამომძვრალია"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "დისკის I/O"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "დისკი კარგად გამოიყურება"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "დისკი კვდება"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "დისკის საკვანძო სიტყვა"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "დისკები"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "დისკების გამოყენება"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "მოცილება"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "$0 გაფრთხილების მოცილება"
+msgstr[1] "$0 ცალი გაფრთხილების მოცილება"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "მონიშნული გაფრთხილებების მოცილება"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "ენა"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "განაწილებული ლუწობა (RAID 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "არ მიამაგრო"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "ჩატვირთვისას არ მიმაგრება"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "სამაგრი დაფა"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "არ გაეშვება ავტომატურად"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "ჩატვირთვისას არ მიმაგრება"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "დომენი"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "დომენი"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "დომენის მისამართი"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "დომენის ადმინისტრატორის სახელი"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "დომენის ადმინისტრატორის პაროლი"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "დომენთან კავშირის შეცდომა"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "დომენის მხარდაჭერა არ არსებობს"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "არ გაიმეორო"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "ორმაგად განაწილებული ლუწობა (RAID 6)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "გადმოწერა"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "გადმოწერეთ ახალი ბრაუზერი უფასოდ"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "გადმოწერილია"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "გადმოწერა"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "$0-ის გადმოწერა"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "დისკი"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "ორმაგი რანგი"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+msgid "EFI system partition"
+msgstr "EFI-ის სისტემური დანაყოფი"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "ჩასწორება"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "/etc/motd-ის ჩასწორება"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Tang გასაღებების სერვერის ჩასწორება"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "VLAN-ის პარამეტრების ჩასწორება"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "WireGuard VPN-ის ჩასწორება"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "Bond-ის პარამეტრების ჩასწორება"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "Bridge-ის პარამეტრების ჩასწორება"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "ხელით მითითებული სერვისის ჩასწორება ზონაში $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "ჰოსტის ჩასწორება"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "hosts ფაილის ჩასწორება"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "motd ფაილის ჩასწორება"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "მიმაგრების წერტილი"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "წესებისა და ზონების ჩასწორება"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "სერვისის ჩასწორება"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "სერვისის ჩასწორება: $0"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "Team-ის პარამეტრების ჩასწორება"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "მომხმარებლის ჩასწორება"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "გასაღების ჩასასწორებლად საჭიროა თავისუფალი სლოტი"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "$target-ის გამოგდება"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "ჩაშენებული PC"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "ცარიელი"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "ცარიელი"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "ცარიელი პაროლი"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "$target-ის დაცარიელება"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "ჩართვა"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "მოწყობილობის ჩართვა ან გამორთვა"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "სერვისის ჩართვა"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "ბრანდმაუერის ჩართვა"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "ჩართულია"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "$0-ის ჩართვა"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "მონაცემების დაშიფვრა"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "მონაცემების დაშიფვრა Tang-ის გასაღებების სერვერით"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "დაშიფვრა საკვანძო ფრაზით"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "დაშიფრულია"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "$0 დაშიფრულია"
+
+#: pkg/storaged/stratis/pool.jsx:275
+msgid "Encrypted Stratis pool"
+msgstr "სტრატისის დაშიფრული პული"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "ლოგიკური დაშიფრული საცავი $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "$0-ის დაშიფრული სექცია"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "დაშიფვრა"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "დაშიფვრის მორგება"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "დაშიფვრის საკვანძო ფრაზა"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "დაშიფვრის ტიპი"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "დასრულდა"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "ბოლოწერტილი"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"ბოლოწერტილები, რომლებიც იქცევიან, როგორც \"სერვერი\", ჰოსტი:პორტი სახით უნდა "
+"იყოს მითითებული, ან ცარიელი."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "ძალით"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "ხელმისაწვდომია განახლებები"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "შეიყვანეთ სწორი MAC მისამართი"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "მთელი ქვექსელი"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "$0-ში ჩაწერა"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "$target-ის წაშლა"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "დამატებითი პაჩები"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "შეცდომა"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "შეცდომა და ზემოთ"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "შეცდომა"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "$0-ის დაყენების შეცდომა: PackageKit დაყენებული არაა"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "შეცდომის შეტყობინება"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "სისტემის მოდიფიკაციის საპოვნელად semanage-ის გაშვების შეცდომა"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "ავტორიზაციის გასაღებების ჩაწერის შეცდომა: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "SELinux-ის რეჟიმის, '$0'-ის დაყენების შეცდომა"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet-ის MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet-ის MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "საჭიროა ზუსტად $0 ფიზიკური ტომის მონიშვნა"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+"საჭიროა ზუსტად $0 ფიზიკური ტომის მონიშვნა, თითო ზოლი თითო ლოგიკური ტომისთვის."
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "მაგალითად: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "მაგალითად: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "გადასარევი პაროლი"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "გაფართოების კორპუსი"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "ანგარიშის ვადა"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "ანგარიშის ვადა $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "გატანა"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "ქსელში გატანა"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "გაფართოებული ინფორმაცია"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "გაფართოებული განაყოფი"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS-ი სწორად არაა ჩართული"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS დამატებითი საერთო პირობის შეზღუდვებით."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "შეცდომით"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "მანქანის დამატების შეცდომა: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "პორტის დამატების შეცდომა"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "სერვისის დამატების შეცდომა"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "ზონის დამატების შეცდომა"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "პაროლის შეცვლის შეცდომა"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "PCP-ის მორგების შეცდომა"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "გაფრთხილების წაშლის შეცდომა: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "tuned-ის გამორთვის შეცდომა"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "tuned-ის პროფილის გამორთვის შეცდომა"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "მანქანის ჩასწორების შეცდომა: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "სერვისის ჩასწორების შეცდომა"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "firewalld-ში $0-ის ჩართვის შეცდომა"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "tuned-ის ჩართვის შეცდომა"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "ჟურნალის გამოთხოვნის შეცდომა"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "ავტორიზებული გასაღებების ჩატვირთვის შეცდომა."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "ერთეულის ჩატვირთვის შეცდომა"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"dnf-automatic.timer ან dnf-automatic-install.timer-ისთვის სერვისის ფაილების "
+"დამუშავება ვერ მოხერხდა. გთხოვთ, წაშალოთ მორგებული უგულებელყოფა ავტომატური "
+"განახლებების კონფიგურაციისთვის."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "სერვისის რესტარტის შეცდომა"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "/etc/motd-ში ცვლილებების შეტანის შეცდომა"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "პარამეტრების შენახვის შეცდომა"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "გაშვების შეცდომა"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "პროფილის გადართვის შეცდომა"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "ფაილის მდგომარეობა"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "ფაილური სისტემა"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "ფაილური სისტემა ჩაკეტილია"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "ფაილური სისტემის სახელი"
+
+#: pkg/storaged/dialog.jsx:1114
+msgid "Filesystem outside the target"
+msgstr "ფაილური სისტემა სამიზნის გარეთ"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "ამ მიმაგრების წერტილის ქვეშ ფაილური სისტემები უკვე მიმაგრებულია."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "სახელით ან აღწერით გაფილტვრა"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "მენიუს ელემენტების გაფილტვრა"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "სერვისების ფილტრი"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "ფილტრები"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "ანაბეჭდი"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "ბრანდმაუერი"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "ბრანდმაუერი ხელმიუწვდომელია"
+
+#: pkg/storaged/drive/drive.jsx:122
+msgid "Firmware version"
+msgstr "მიკროკოდის ვერსია"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "NBDE-ის მხარდაჭერის გასწორება"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "ფონტის ზომა"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "გაშვება აკრძალულია"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "ძალით შეცვლა"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "ძალით წაშლა"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "პაროლის ძალით შეცვლა"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "ფორმატი"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "$0-ის დაფორმატება"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "დაფორმატება და მიმაგრება"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Format and start"
+msgstr "დაფორმატება და გაშვება"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "მხოლოდ დაფორმატება"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "ფორმატირება ბლოკურ მოწყობილობაზე მდებარე ყველა მონაცემს წაშლის."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "გადაგზავნის დაყოვნება $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "კადრის ნომერი"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "თავისუფალი სივრცე"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "ძებნის თავისუფალი ფორმა"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "კვირაობით"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "დასაწყისი"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "სრული სახელი"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "რაუტერი"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "საერთო"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "გენერირებული"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "$0-ზე გადასვლა"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "აპლიკაციაზე გადასვლა"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "ახლავე გადასვლა"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "გრაფიკის ხილვადობა"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "გრაფიკის ხილვადობის პარამეტრების მენიუ"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "ჯგუფი"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "ჯგუფის სახელი"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "ჯგუფები"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "გაზრდა"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "შემცველობის გაზრდა"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "$0-ის ლოგიკური ზომის გადიდება"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "ლოგიკური ტომის გაზრდა"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "დანაყოფის გაზრდა"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "პულის გადიდება მაქს. შესაძლო ზომამდე"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "გადიდება მაქს. შესაძლო ზომამდე"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "NAT Hairpin-ის რეჟიმი"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "NAT hairpin რეჟიმი"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "ჯიბის"
+
+#: pkg/storaged/drive/drive.jsx:64
+msgid "Hard Disk Drive"
+msgstr "მყარი დისკი"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "ინფორმაცია აპარატურის შესახებ"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "ჯანმრთელობა"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "მისალმების დრო $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "დახმარება"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "დადასტურების პაროლის დამალვა"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "პაროლის დამალვა"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "იერარქიის ID"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "მაღალი ურთიერთკავშირი შეტევის გაზრდილი ზედაპირის ხარჯზე."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "ისტორიის პაკეტის რიცხვი"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "საწყისი საქაღალდე"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "ჰოსტი"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "ჰოსტის გასაღები არასწორია"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "ჰოსტის სახელი"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "ჰოსტები"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "საათობრივ"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "საათი"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "როგორ შევამოწმო"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+"შიდა შეცდომა - ეს ლოგიკური ტომი მონიშნულია, როგორც აქტიური და უნდა ჰქონდეს "
+"ასოცირებული ბლოკური მოწყობილობა. მაგრამ ასეთი ბლოკური მოწყობილობა აღმოჩენილი "
+"არაა."
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP მისამართი"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"IP მისამართი რაუტინგის პრეფიქსით. ერთმანეთისგან გამოიყოფა მძიმით. მაგ: "
+"192.0.2.0, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "IPv4 მისამართი"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4-ის მორგება"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6-ის მორგება"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "იდენტიფიკატორი"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"თუ ცარიელია, ID დაგენერირდება შესაბამისი პორტის სერვისებისა და პორტის "
+"ნომრებისგან"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"თუ ანაბეჭდი ემთხვევა, დააწექით \"გასაღების მიღება და შესვლა\"-ს. ან არ "
+"შეხვიდეთ და დაუკავშირდით ადმინისტრატორს."
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"თუ ანაბეჭდი ემთხვევა, დააწექით \"ჰოსტის ნდობა და დამატებას\", ან არ შეხვიდეთ "
+"და დაუკავშირდით ადმინისტრატორს."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "იგნორი"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "შიგნით"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "ტერმინალში გაუშვით: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"მომავალში $1-ზე $0-ით უპაროლოდ შესვლისთვის გამოიყენეთ $2-ის პაროლი $3-ზე "
+"როგორც გასაღების პაროლი ან დატოვეთ გასაღების პაროლი ცარიელი."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "სინქრონიზებულია"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "არააქტიური"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+msgid "Inactive logical volume"
+msgstr "არააქტიური ლოგიკური ტომი"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "მოყოლილი სერვისები"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"შემომავალი მოთხოვნები ნაგულისხმებად იბლოკება. გამავალი მოთხოვნები არ "
+"იბლოკება."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "ფაილური სისტემის არასწორი მიმაგრება"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "ერთით გაზრდა"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "ინდექსების მეხსიერება"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "არაპირდაპირი"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "ინფორმაცია"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "ინფორმაცია და ზემოთ"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "ინიციალიზაცია"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "დისკის ინიციალიზაცია $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "ინიციალიზაცია დისკზე მდებარე მონაცემებს მთლიანად წაშლის."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "ინიციალიზაცია..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Insights: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "დაყენება"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "NFS-ის მხარდაჭერის დაყენება"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Stratis-ის მხარდაჭერის დაყენება"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "ყველა განახლების დაყენება"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "აპლიკაციის ინფორმაციის დაყენება"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "cockpt-pcp-ის დაყენება"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Kpatch-ის განახლების დაყენება"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "Realmd-ის მხარდაჭერის დაყენება"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "უსაფრთხოების განახლებების დაყენება"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr "SELinux-ის შეცდომებში გასარკვევად დააყენეთ setroubleshoot-server."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "პროგრამების დაყენება"
+
+#: pkg/kdump/kdump-view.jsx:571
+msgid "Install the $0 package."
+msgstr "დააყენეთ პაკეტი $0."
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr "დაყენება უკვე დაყენებული პაკეტის cockpit გარეშე მხარდაჭერილი არაა"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "დაყენებული"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "მიმდინარეობს დაყენება"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "$0-ის დაყენება"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "$0-ის დაყენება წაშლის $1-ს."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "პაკეტების დაყენება"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "ცალი ინტერფეისი"
+msgstr[1] "ინტერფეისი"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "ინტერფეისის წევრები"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "ინტერფეისები"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "შიდა შეცდომა"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "შიდა შეცდომა: გამოწვევის არასწორი თავსართი"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "არასწორი"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "არასწორი მისამართი: $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "თარიღის არასწორი ფორმატი"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "თარიღისა და დროის არასწორი ფორმატი"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "არასწორი ვადა"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "ფაილის არასწორი წვდომები"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "არასწორი გასაღები"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "არასწორი მეტრიკა $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "დღეებს არასწორი რაოდენობა"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "პორტის არასწორი ნომერი"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "არასწორი პრეფიქსი $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "არასწორი პრეფიქსი ან ქსელის მასკა $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "არასწორი დიაპაზონი"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "დროის არასწორი ფორმატი"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "დროის არასწორი სარტყელი"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "არასწორი მომხმარებელი ან პაროლი"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "IoT gateway"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "sshd-ი სხვა პორტზე ხომ არაა გაშვებული?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "ამოცანები"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "შეერთება"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "დომენში შესვლა"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "დომენზე დაერთება"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "შეერთება"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "დომენში შესასვლელად საჭიროა realmd-ის დაყენება"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "დომენში შესვლის მხარდაჭერა არ არსებობს"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "ზონაში შეერთება"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "ჟურნალი"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "ჟურნალის ელემენტი"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "ჟურნალის ერთეული ნაპოვნი არაა"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "გადასვლა"
+
+#: pkg/kdump/kdump-view.jsx:569
+msgid "Kdump service is not installed."
+msgstr "Kdump-ის სერვისი დაყენებული არაა."
+
+#: pkg/kdump/kdump-view.jsx:604
+msgid "Kdump settings"
+msgstr "kdump-ის პარამეტრები"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "კავშირის შენარჩუნება"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "ოპერაციული სისტემის ბირთვის ავარიის დამპი"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr "ბირთვი პარამეტრით $0 არ ჩაიტვირთა"
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "ბირთვის დამპი"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "ცოცხალი ბირთვის პაჩი $0 ახლა აქტიურია"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "ცოცხალი ბირთვის პაჩი $0 დაყენებულია"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "ჩართული ბირთვის პაჩის მორგება"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "ოპერაციული სისტემის გაშვებული ბირთვის ჩასწორება"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "გასაღების პაროლი"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "უცნობი ტიპის გასაღებების ჩასწორება შეუძლებელია"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "გასაღების წყარო"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "გასაღებები"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "გასაღებების სერვერი"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "გასაღების სერვერის მისამართი"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr ""
+"გასაღებების სერვერის წაშლით თქვენ შეიძლება დაკარგოთ $0-ის განბლოკვის "
+"საშუალება."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP გასაღები"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "LEGACY-ი Active Directory-სთან სამუშაოდ."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+msgid "LVM2 VDO pool"
+msgstr "LVM2 VDO პული"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+msgid "LVM2 logical volume"
+msgstr "LVM2 ლოგიკური ტომი"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+msgid "LVM2 logical volumes"
+msgstr "LVM2 ლოგიკური ტომები"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "LVM2 ფიზიკური ტომი"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+msgid "LVM2 physical volumes"
+msgstr "LVM2 ფიზიკური ტომები"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM2 ტომების ჯგუფი"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "LVM2 ტომის ჯგუფი $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr "ჭდე"
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "ლეპტოპი"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "ბოლო 24 საათი"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "ბოლო 7 დღე"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "ბოლოს აქტიური"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+msgid "Last cannot be removed"
+msgstr "ბოლოს ვერ წაშლით"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "ბოლოს შემოწმდა: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+msgid "Last disk can not be removed"
+msgstr "ბოლო დისკს ვერ წაშლით"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "ბოლო შესვლა"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "ბოლოს შეიცვალა: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "ბოლო წარმატებული შესვლა:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "განლაგება"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "გაიგეთ მეტი"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "$0-დან გასვლა"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "დომენიდან გასვლა"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "შიფრაციის გამოსატოვებლად ცარიელი დატოვეთ"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "ლიცენზირებულია GNU LGPL -ის ვერსია 2.1-ით"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "ღია"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "წვდომის შეზღუდვა"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "შეზღუდული წვდომა"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"შეზღუდული წვდომა გისაზღვრავთ ადმინისტრატიულ პრივილეგიებს. ვებ კონსოლის "
+"ზოგიერთი ნაწილი შეზღუდული იქნება."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "ლიმიტები"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "წრფივი"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "კავშირის გათიშვის დაყოვნება"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "ლოკალური ბმის მისამართი"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "კავშირის მონიტორინგი"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "კავშირის აღდგენის დაყოვნება"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "კავშირის თვალყურმდევნი"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "მიბმული"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+msgid "Linux filesystem data"
+msgstr "Linux-ის ფაილური სისტემის მონაცემები"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+msgid "Linux swap space"
+msgstr "Linux-ის სვოპის სივრცე"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "მოსმენა"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "პორტის მოსმენა"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "მოსასმენი პორტი რიცხვი უნდა იყოს"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "სერვისების სია"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "სერვისების სიის გამოტანის შეცდომა: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "ჩატვირთვა"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "დატვირთვის გადანაწილება"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "ადრინდელი მონაცემების ჩატვირთვა"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "ადრინდელი ელემენტების ჩატვირთვა"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "ხელმისაწვდომი განახლებების ჩატვირთვი შეცდომა"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "მიმდინარეობს ხელისაწვდომი განახლებების ჩატვირთვა. გთხოვთ მოითმინოთ..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "ადრინდელი ელემენტების ჩატვირთვა"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "გასაღებების ჩატვირთვა..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "SSH გასაღებების ჩატვირთვის შეცდომა"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "სერვისების ჩატვირთვის შეცდომა"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "პაკეტების ჩატვირთვა..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "სისტემის ცვლილებების ჩატვირთვა..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "სერვისების ჩატვირთვის შეცდომა"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "ჩატვირთვა..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "ლოკალური ანგარიშები"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "ლოკალური ფაილური სისტემა"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "მიმაგრების ლოკალური წერტილი"
+
+#: pkg/storaged/overview/overview.jsx:160
+msgid "Local storage"
+msgstr "ლოკალური საცავი"
+
+#: pkg/kdump/kdump-view.jsx:450
+msgid "Local, $0"
+msgstr "ლოკალური, $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "მდებარეობა"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "დაკეტვა"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "$0-ის დაბლოკვა"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "ანგარიშის დაბლოკვა"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+msgid "Locked data"
+msgstr "დაბლოკილი მონაცემები"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "$target-ის ჩაკეტვა"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "შესვლა"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "$0-ში შესვლა"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "შედით სერვერის თქვენი ანგარიშით."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "ჟურნალის შეტყობინებები"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "გასვლა"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "მომხმარებლის გაგდება"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "შესვლა წარმატებულია"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "ლოგიკური"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+msgid "Logical Volume Manager partition"
+msgstr "ლოგიკური ტომების მმართველის დანაყოფი"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "ლოგიკური ზომა"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "ლოგიკური ტომი"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "ლოგიკური ტომი (სწრაფი ასლი)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "$0-ის ლოგიკური სივრცე"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "შესვლა"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "თავიდან შესვლა"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "შესვლა წარუმატებელია"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "შესვლის ფორმატი"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "შესვლების ისტორია"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "შესვლის ისტორია"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "$0-ის გაგდება"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "გასვლა წარმატებულია"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "ჟურნალი"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "დაბალი პროფილის სამუშაო მაგიდა"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Lunch box"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+msgid "MDRAID device"
+msgstr "MDRAID მოწყობილობა"
+
+#: pkg/storaged/utils.js:334
+msgid "MDRAID device $0"
+msgstr "MDRAID მოწყობილობა $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+msgid "MDRAID device is recovering"
+msgstr "მიმდინარეობს MDRAID მოწყობილობის აღდგენა"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+msgid "MDRAID device must be running"
+msgstr "MDRAID მოწყობილობა გაშვებული უნდა იყოს"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+msgid "MDRAID disk"
+msgstr "MDRAID დისკი"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+msgid "MDRAID disks"
+msgstr "MDRAID დისკები"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (რეკომენდებულია)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU დადებითი რიცხვი უნდა იყოს"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "მანქანის ID"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "მანქანის SSH გასაღების ანაბეჭდები"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "სერვერის მთავარი შასი"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "სარემონტო"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+"დაშორებულ მანქანებზე დაჰოსტილ მავნე გვერდებს სხვა მიერთებულ ჰოსტებზეც "
+"შეუძლიათ გავლენს მოხდენა"
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "ფაილური სისტემის ზომების მართვა"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "საცავის მართვა"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "მართული ინტერფეისები"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "LVM-ის მართვა"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "NFS მიმაგრებების მართვა"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "RAID-ის მართვა"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "VDO-ის მართვა"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "VLAN-ის მართვა"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "ბრანდმაუერის მართვა"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "ქსელის ბარათების გაერთიანების მართვა"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "ქსელური ხიდების მართვა"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "ქსელური ბარათების გუნდების მართვა"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "დამაყოფების მართვა"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "ფიზიკური დისკების მართვა"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "სერვისების მართვა"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "პროგრამული უზრუნველყოფის განახლების მართვა"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "მომხმარებლების ანგარიშების მართვა"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "ხელით მითითება"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "ხელით მითითებული"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "$target-ის გაფუჭებულად მონიშვნა"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "სერვისის დამალვა"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "შენიღბული"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"სერვისის შენიღბვა კრძალავს მასზე დამოკიდებული ყველა სერვისის გაშვებას. ამას "
+"იმაზე მეტი ეფექტი შეიძლება ჰქონეს, ვიდრე თქვენ გგონიათ. გთხოვთ დაადასტუროთ, "
+"რომ გნებავთ ამის გაკეთება."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "შეტყობინების მაქს. ვადა $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+msgid "Media drive"
+msgstr "მედია დისკი"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "მეხსიერება"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "მეხსიერების ტექნოლოგია"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "მეხსიერების გამოყენება"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "მეხსიერების გამოყენება/სვოპი"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "შერწყმული"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "შესული მომხმარებლებისთვის შეტყობინების გაგზავნა"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "შეტყობინებები ამ შეცდომის შესახებ შეგიძლიათ იპოვოთ ჟურნალში:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "გამოყენებული მეტამონაცემები"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "მეთოდი"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "მეტრული"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "გრაფიკების და ისტორიის ნახვა"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "მეტრიკების ისტორიის ჩატვირთვის შეცდომა"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "მეტრიკების მორგება"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "მინი PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "კომპიუტერი პატარა ყუთით"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "წუთი უნდა იყოს რიცხვი შუალედში 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "წთ"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "წუთი"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr "სარკისებური (RAID 1)"
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "დაცვის გეგმები"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "რეჟიმი"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "მოდელი"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "$target-ის ჩასწორება"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "ორშაბათობით"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "მონიტორინგის ინტერვალი"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "სამიზნეების მონიტორინგი"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "თვეში ერთხელ"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "მეტი ინფორმაცია..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "მიმაგრება"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "მიმაგრების წერტილი"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "მიმაგრება ქსელის ჩართვის შემდეგ. გამოტოვება შეცდომის შემთხვევაში"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "ავტომატური მიმაგრება ჩატვირთვის დროს"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "ჩატვირთვისას მიმაგრება"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "$0-ზე ავტომატურად მიმაგრება ჩატვირთვის დროს"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "მიმაგრება სერვისების გაშვებამდე"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "მიმაგრების კონფიგურაცია"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "ფაილური სისტემის მიმაგრება"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "ახლა მიმაგრება"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "$0-ზე მიმაგრება"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "მიმაგრების მორგება"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "მიმაგრების წერტილი"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "მიმაგრების წერტილი არ შეიძლება ცარიელი იყოს"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "მიმაგრების წერტილი არ შეიძლება ცარიელი იყოს."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "მიმაგრების წერტილი უკვე გამოიყენება $0-თვის"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "მიმაგრების წერტილი უნდა იწყებოდეს \"/\"-ით."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "მიმაგრებულია, მხოლოდ წასაკითხად"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "მიმაგრება დალოდების გარეშე. გამოტოვება შეცდომის შემთხვევაში"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "$target-ის მიმაგრება"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "მიმაგრება სერვისების გაშვებამდე"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "სერვისების გაშვებასთან ერთად მიმაგრება"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr "მიმაგრება სერვისების გაშვებასთან ერთად მას შემდეგ, რაც ქსელი ჩაირთვება"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "მრავალსისტემიანი ყუთი"
+
+#: pkg/storaged/drive/drive.jsx:135
+msgid "Multipathed devices"
+msgstr "მრავალბილიკა მოწყობილობები"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr "ერთზე მეტი მისამართის მითითება მძიმეებით ან გამოტოვებებით შეგიძლიათ."
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS მიმაგრება"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP სერვერი"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "სახელი"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "სახელი არ შეიძლება ცარიელი იყოს."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "სახელი არ შეიძლება იყოს ცარიელი."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "სახელი არ შეიძლება $0 ბაიტზე გრძელი იყოს"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "სახელი არ შეიძლება $0 სიმბოლოზე მეტი იყოს"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "სახელი არ შეიძლება 127 სიმბოლოზე გრძელი იყოს."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+msgid "Name cannot be longer than 255 characters."
+msgstr "სახელი არ შეიძლება 255 სიმბოლოზე გრძელი იყოს."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "სახელი არ შეიძლება შეიცავდეს სიმბოლოს '$0'."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+msgid "Name cannot contain the character '/'."
+msgstr "სახელი არ შეიძლება შეიცავდეს სიმბოლოს '/'."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "სახელი არ შეიძლება შეიცავდეს ცარიელ სიმბოლოებს."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr "საჭიროა სათადარიგო დისკი"
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "საჭიროა ერთი NTP სერვერი მაინც"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "ქსელი"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "ქსელური I/O"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "ქსელის ბარათების გაერთიანება"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "ქსელურ მოწყობილობებს და გრაფიკებს NetworkManager-ი ესაჭიროებათ"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "ქსელის ჟურნალი"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "ქსელის გამოყენება"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager დაყენებული არაა"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager გაშვებული არაა"
+
+#: pkg/storaged/overview/overview.jsx:168
+msgid "Networked storage"
+msgstr "ქსელური საცავი"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "ქსელი"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "არასდროს"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "უვადო ანგარიში"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "უვადო პაროლი"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "არასდროს შემოსულა"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "ახალი NFS მიმაგრება"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "ახალი ჰოსტი"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "ახალი ჰოსტი: $0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "გასაღების ახალი პაროლი"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "ახალი სახელი"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "ახალი საკვანძო ფრაზა"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "ახალი პაროლი"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "ახალი პაროლი მიუღებელია"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "შემდეგი"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "არა"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "ID მითითებული არაა"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "SELinux-ის გაფრთხილებების გარეშე."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "დაყენებული ან ხელმისაწვდომი აპლიკაციების გარეშე."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "თავისუფალი სლოტი ხელმიუწვდომელია"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "ბლოკური მოწყობილობები ხელმიუწვდომელია."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+msgid "No block devices found"
+msgstr "ბლოკური მოწყობილობები აღმოჩენილი არაა"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "შეამოწმეთ კავშირი"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "კონფიგურაცია ნაპოვნი არაა"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "მონაცემები ხელმიუწვდომელია"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "$0-დან $1-მდე მონაცემები ნაპოვნი არაა"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "დაყოვნების გარეშე"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "აღწერა ხელმიუწვდომელია"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "აღწერის გარეშე."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+msgid "No devices found"
+msgstr "მოწყობილობები აღმოჩენილი არაა"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "დისკები ხელმიუწვდომელია."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+msgid "No disks found"
+msgstr "დისკები ვერ ვიპოვე"
+
+#: pkg/storaged/iscsi/session.jsx:79
+msgid "No drives found"
+msgstr "ამძრავები ვერ ვიპოვე"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "დაშიფვრის გარეშე"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "მოვლენების გარეშე"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "ფაილური სისტემების გარეშე"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "ფაილური სისტემების გარეშე"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "გასაღების თავისუფალი სლოტების გარეშე"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "თავისუფალი ადგილის გარეშე"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "ამ დანაყოფის შემდეგ თავისუფალი ადგილი არაა"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "ჯგუფის სახელი მითითებული არაა"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "ჰოსტის გასაღებები ნაპოვნი არაა."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "აპლიკაციის შესაბამისი პაკეტი ვერ მოიძებნა."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "გასაღებები არ დამატებულა"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "შესაბამისი ენის გარეშე"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "ჟურნალის ცარიელია"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "ლოგიკური ტომების გარეშე"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "ჟურნალი ცარიელია"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "პასუხები არაა"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "დისკი არ დევს"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "დანაყოფების გარეშე"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+msgid "No partitions found"
+msgstr "დანაყოფები ვერ ვიპოვე"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "პარტნიორები დამატებული არაა."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+msgid "No physical volumes found"
+msgstr "ფიზიკური ტომები ვერ ვიპოვე"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "რეალური სახელი მითითებული არაა"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "შედეგები ნაპოვნი არაა"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"თქვენს ფილტრს შედეგები არ აქვს. შედეგების საჩვენებლად გაასუფთავეთ ფილტრები."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "არ მოხვდა წესებში"
+
+#: pkg/storaged/overview/overview.jsx:190
+msgid "No storage found"
+msgstr "საცავი ვერ ვიპოვე"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+msgid "No subvolumes"
+msgstr "ქვეტომების გარეშე"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "ფაილი ან საქაღალდე არ არსებობს"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "სისტემა შეცვლილი არაა"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "სისტემის ანგარიშების გარეშე."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "განახლებების გარეშე"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "მომხმარებელი მითითებული არაა"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "არცერთი"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "არ წარმოადგენს სწორ პირად გასაღებს"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "ბრანდმაუერის გამოსართავად საჭიროა ავტორიზაცია"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "ბრანდმაუერის ჩასართავად საჭიროა ავტორიზაცია"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "ხელმიუწვდომელია"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "დაკავშირებული არაა"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "არაა მიერთებული Insights-თან"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "არაა მიერთებული ჰოსტთან"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+msgid "Not enough free space"
+msgstr "თავისუფალი ადგილი საკმარისი არაა"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "ადგილი საკმარისი არაა"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+msgid "Not enough space to grow"
+msgstr "გასადიდებლად არასაკმარისი სივრცეა"
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "ნაპოვნი არაა"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "დაყენებული არაა"
+
+#: pkg/networkmanager/network-interface.jsx:680
+msgid "Not permitted to configure network devices"
+msgstr "ქსელური მოწყობილობების მორგების უფლება არ გაქვთ"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "realm-ების მორგების წვდომა აკრძალულია"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "არ გაქვთ მითითებული მოქმედების შესასრულებლად საკმარისი წვდომა."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "მზად არაა"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "რეგისტრირებული არაა"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "გაშვებული არაა"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "მორგებული არაა"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "სინქრონიზებული არაა"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "არცერთი"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "ნოუთბუქი"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "გაფრთხილება და ზემოთ"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "ქსელის მისამართების, ჰოსტისა და მომხმარებლის სახელების ობფუსკაცია"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "ობფუსკირებულია"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "მოხდა $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "მოხდა $0-სა და $1-ს შორის"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "გამოვლენები"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "დიახ"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "ძველი საკვანძო ფრაზა"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "ძველი პაროლი"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "ძველი პაროლი მიუღებელია"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "მიმაგრებულ მოწყობილობაზე"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "შეცდომის შემთხვევაში"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"როცა Cockpit-ს დააყენებთ, შეგიძლიათ მისი ჩართვაც, ბრძანებით \"systemctl "
+"enable --now cockpit.socket\"."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "$1-დან გამოყენებულია მხოლოდ $0."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "დაშვებულია მხოლოდ ლათინური ანბანი, ციფრები, :, _ და @"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "მხოლოდ გადაუდებელ შემთხვევაში"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"FIPS რეჟიმში ჩატვირთვისას მხოლოდ დადასტურებული ალგორითმების გამოყენება."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "უკაცრავად!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "მეტრიკების გასაზიარებლად ბრანდმაუერში გახსენით pmproxy-ის სერვისი."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "ოპერაცია '$operation' $target-ზე"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "პარამეტრები"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "ან გამოიყენეთ მოყოლილი ბრაუზერი"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "სხვა"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"ავთენტიკაციის სხვა მეთოდები ჯერ კიდევ ხელმისაწვდომია იმ დროსაც კი, როცა "
+"პაროლით ინტერაქტიული ავთენტიკაცია გამორთულია."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "სხვა პარამეტრები"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "გარე"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "გადახედვა"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "გადაწერა"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "არსებულ მონაცემებზე ნულების გადაწერა (ნელი)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "მეტრიკების ისტორიაში პაკეტი cockpit-pcp არ არსებობს"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "ინფორმაცია პაკეტის შესახებ"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit-ის ავარია"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit დაყენებული არაა"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit-ის შეცდომის კოდი $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "პაკეტები"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "გვერდის სახელი"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "მშობელი"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "მშობელი $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "წარმოადგენს ნაწილს"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "$0-ის ნაწილი"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "განაყოფი"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "$0-ის დანაყოფი"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "დანაყოფის ზომაა $0. შემცველობის ზომა კი - $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "დაყოფა"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "დანაყოფები"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "პასიური"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "საკვანძო სიტყვა"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "საკვანძო ფრაზა ცარიელი არ შეიძლება იყოს"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "საკვანძო ფრაზა არ შეიძლება ცარიელი იყოს"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "საკვანძო ფრაზა ნებისმიერი სხვა გასაღების სლოტიდან"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "საკვანძო ფრაზის მოცილებამ შეიძლება შეუძლებელი გახადოს $0-ის განბლოკვა."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "საკვანძო ფრაზები ერთმანეთს არ ემთხვევა"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "პაროლი"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "პაროლი წარმატებით შეიცვალა"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "პაროლის ვადა"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "პაროლი 256 სიმბოლოზე გრძელია"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "პაროლი მიუღებელია"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "პაროლი ძალიან სუსტია"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "საჭიროა პაროლის შეცვლა"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "პაროლი მიუღებელია"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "პაროლის მინიშნება"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "ჩასმა"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "ჩასმის შეცდომა"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "არსებული გასაღების ჩასმა"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "ჩააკოპირეთ SSH-ის თქვენი საჯარო გასაღების შემცველობა"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "ბილიკი"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "ბილიკის ღირებულება"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "ბილიკის ფასი $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "ბილიკი სერვერზე"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "სერვერზე ბილიკის ველი არ შეიძლება ცარიელი იყოს."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "ბილიკი სერვერზე უნდა იწყებოდეს სიმბოლოთი \"/\"."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "ბილიკი საქაღალდემდე"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "ბილიკი ფაილამდე"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "ბილიკი"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "პაუზა"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+"პარტნიორის #$0 ბოლოწერტილის პორტი არასწორია. პორტის ნომერი რიცხვი უნდა იყოს."
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"პარტნიორის #$0 ბოლოწერტილი არასწორია. ის ჰოსტი:პორტი სახით უნდა იყოს "
+"მითითებული. მაგ: 1.3.4.5:41232 ან example.com:41223"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "პარტნიორები"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"პარტნიორები სხვა მანქანებია, რომლებიც ამას უერთდებიან. საჯარო გასაღებები "
+"სხვა მანქანებიდან ასევე გაზიარებული იქნება."
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"წარმადობის Co-Pilot-ი აგროვებს და აანალიზებს თქვენი სისტემის წარმადობის "
+"მეტრიკებს."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "წარმადობის პროფილი"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "გარე კორპუსი"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "მუდმივი"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "სამუდამოდ წავშალო $0 ჯგუფი?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "წავშალო სამუდამოდ $0?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "წავშალო სამუდამოდ ლოგიკური ტომი $0/$1?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+msgid "Permanently delete subvolume $0?"
+msgstr "წავშალო სამუდამოდ ქვეტომი $0?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "წვდომა აკრძალულია"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "დაშვებადი"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "ფიზიკური"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "ფიზიკური ტომები"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "ფიზიკური ტომები"
+
+#: pkg/storaged/block/resize.jsx:279
+msgid "Physical volumes can not be resized here"
+msgstr "ფიზიკურ ტომებს აქ ადგილს ვერ შეუცვლით"
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "აირჩიეთ თარიღი"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "მიჭიკარტება"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Ping-ის დაყოვნება"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "მიზნის დაპინგვა"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "მიჭიკარტებული"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "პიცისყუთი"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "ადმინისტრატორი წვდომის მისაღებად გთხოვთ გაიაროთ ავთენტიკაცია"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "ვებ კონსოლის გამოსაყენებლად საჭიროა JavaScript-ის ჩართვა."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "დააყენეთ პაკეტი $0"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "გადაწყვიტეთ პრობლემა და განაახლეთ ეს გვერდი."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "დააყენეთ ვადა"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "შეიყვანეთ მისაერთებელი ჰოსტის სახელი"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "ჯერ ისინი მოხსენით."
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "თხელი ლოგიკური საცავების პული"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "პული თხლად გამოყოფილი LVM2 ლოგიკური ტომებისთვის"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "თხლად გაწერილი საცავების პული"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "პულის საკვანძო ფრაზა"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "პორტი"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "გადატანადი"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "პორტები"
+
+#: pkg/storaged/partitions/partition.jsx:116
+msgid "PowerPC PReP boot partition"
+msgstr "PowerPC PReP ჩატვირთვის დანაყოფი"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "პრეფიქსის სიგრძე"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "პრეფიქსის სიგრძე ან ნეტმასკა"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "მომზადება"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "წარმოდგენილია"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "არ შეცვალო"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "ჰოსტის ლამაზი სახელი"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "წინა ჩატვირთვა"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "ძირითადი"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "პრიორიტეტი"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "$priority პრიორიტეტი"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "პირადი გასაღები"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "ადმინისტრატორად გახდომის შეცდომა"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "პრობლემის დეტალები"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "ინფორმაცია პრობლემის შესახებ"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "მდებარეობის გამოყენებელი პროცესები"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "მიმდინარეობა: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "პროექტის ვებგვერდი"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "მოთხოვნას passwd-ის გავლით ვადა გაუვიდა"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "მოთხოვნას ssh-add-ის გავლით დრო გაუვიდა"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "მოთხოვნას ssh-keygen-ის გავლით დრო გაუვიდა"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "გადატვირთვა გავრცელდება"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"იცავს მოსალოდნელი უახლოესი მომავალი თავდასხმებისგან თავსებადობის ხარჯზე."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "შეიყვანეთ საკვანძო ფრაზა ამ ბლოკური მოწყობილობების პულისტვის:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "საჯარო გასაღები"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr "საჯარო გასაღები დაგენერირდება, როცა სწორ პირად გასაღებს შეიყვანთ"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "დანიშნულება"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (stripe)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (სარკე)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (სარკეების ზოლები)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (გამოყოფილი ლუწობა)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (განაწილებული ლუწობა)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (ორმაგად განაწილებული ლუწობა)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID კალათი"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID-ის დონე"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr "RAID10-ს ფიზიკური ტომების ლუწი რიცხვი სჭირდება"
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "რეკში ჩასადგმელი შასი"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "შემთხვევითი"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "დიაპაზონი"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "დიაპაზონი მკაცრად უნდა იყოს განსაზღვრული"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "რანგი"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "პირდაპირი წვდომა მოწყობილობასთან"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "წაკითხვა"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "მეტის წაკითხვა..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "მხოლოდ წასაკითხად"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "ვკითხულობ"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "კითხვა..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "მზადაა"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "მზადაა"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "ჰოსტის რეალური სახელი"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"ჰოსტის რეალური სახელი შეიძლება მხოლოდ შეიცავდეს პატარა სიმბოლოებს, ციფრებს, "
+"ტირეებს და ჰარეებს"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "ჯოსტის რეალური სახელი 64 სიმბოლოზე დიდი არ უნდა იყოს"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "თავიდან გადატარება და გადატვირთვა"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "გადატვირთვა"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "გადატვირთვა დასრულების შემდეგ"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "რეკომენდებულია გადატვირთვა"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "სისტემის გადატვირთვა..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "მიღება"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "ბოლო ჰოსტები"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "რეკომენდებული, დაცული პარამეტრები მიმდინარე დაცვის მოდელისთვის."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "თავიდან დაკავშირება"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "აღდგენა"
+
+#: pkg/storaged/jobs-panel.jsx:70
+msgid "Recovering MDRAID device $target"
+msgstr "MDRAID მოწყობილობა $target-ის აღდგენა"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "პაკეტების ინფორმაციის განახლება"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "კავშირი უარყოფილია. ჰოსტი უცნობია"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "კავშირი უარყოფითია. ჰოსტის გასაღებები არ ემთხვევა"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "კავშირი უარყოფილია. ჰოსტის უცნობი გასაღები"
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "თავიდან გენერაცია"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "Initrd-ის თავიდან გენერაცია"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "რეგისტრაცია…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "დაკავშირებული პროცესები და სერვისები ძალით იქნებიან გაჩერებული."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "დაკავშირებული პროცესები ძალით იქნება გაჩერებული."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "დაკავშირებული სერვისები ძალით იქნება გაჩერებული."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "თავიდან ჩატვირთვა"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "გადატვირთვის წყარო"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "თავიდან ჩატვირთვა"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "დარჩენილი სერვისების მდგომარეობის გადატვირთვა"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "გარედან CIFS/SMB-ზე"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "გარედან FTP-ზე"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "გარედან NFS-ზე"
+
+#: pkg/kdump/kdump-view.jsx:456
+msgid "Remote over NFS, $0"
+msgstr "დაშორებული NFS-ის ზემოდან, $0"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "გარედან SFTP-ზე"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "გარედან SSH-ზე"
+
+#: pkg/kdump/kdump-view.jsx:453
+msgid "Remote over SSH, $0"
+msgstr "დაშორებული SSH-ის ზემოდან, $0"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "წაიშლება:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "წაშლა"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "$0-ის წაშლა"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "$1 ზონიდან $0 სერვისის წაშლა"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "წავშალო $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "წავშალო Tang გასაღებების სერვერი?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "მოწყობილობის წაშლა"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "ჰოსტის წაშლა"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "ელემენტის წაშლა"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "წავშალო ნაკლული ფიზიკური ტომები?"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "წავშალო საკვანძო ფრაზა გასაღების სლოტი $0-დან?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "წავშალო საკვანძო სიტყვა?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "სერვისის წაშლა: $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "ზონის წაშლა: $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "წაშლა"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "$0-ის წაშლა"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0-ის წაშლა გაწყვეტს კავშირს სერვერთან და შეუძლებელს გახდის ადმინისტრირების "
+"ინტერფეისთან წვდომას."
+
+#: pkg/storaged/jobs-panel.jsx:66
+msgid "Removing $target from MDRAID device"
+msgstr "$target-ის MDRAID მოწყობილობიდან მოცილება"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"კვანძური ფრაზის წაშლას სხვა კვანძური ფრაზის ჩამატების გარეშე შეუძლია "
+"გამოიწვიოს გასაღებების მართვის დაბლოკვა, თუ სხვა საკვანძო ფრაზები "
+"დაგავწყდებათ."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "$target-დან ფიზიკური საცავის მოცილება"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"cockpit-ის სერვისის წაშლამ შეიძლება ვებ კონსოლი მიუწვდომელი გახადოს. "
+"დარწმუნდით, რომ ზონის ცვლილება ვებ კონსოლთან კავშირს არ გაწყვეტს."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "ზონის წაშლა მასში მყოფი სერვისების წაშლასაც გამოიწევევს."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "სახელის გადარქმევა"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Stratis-ის პულისთვის სახელის გადარქმევა"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "ფაილური სისტემის სახელის გადარქმევა"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "ჯგუფის სახელის გადარქმევა"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "ჯგუფის ($0) სახელის გადარქმევა"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "ლოგიკური ტომისთვის სახელის გადარქმევა"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "ტომების ჯგუფისათვის სახელის გადარქმევა"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "$target-ის სახელის გადარქმევა"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr ""
+"ჯგუფის სახელის გადარქმევამ შეიძლება sudo-ის ან რამე მსგავსის წესები დაარღვიოს"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr "შეკეთება"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+msgid "Repair logical volume $0"
+msgstr "ლოგიკური ტომის $0 შეკეთება"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "$target-ის შეკეთება"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "გამეორება"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "თვეში ერთხელ გამეორება"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "გაიმეორეთ საკვანძო სიტყვა"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "კვირაში ერთხელ გამეორება"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "ანგარიში"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "ანგარიშის ჭდე"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "ABRT ანალიტიკისთვის გადაცემა"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "ანგარიში გაგზავნილია; ბმულები მიუწვდომელია"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "ანგარიშის გადაგზავნის შეცდომა"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "ანგარიში გაუქმდა"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "ანგარიშები"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "ანგარიშები:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "პაროლის შეცვლა ყოველ $0 დღეში"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "$0-ზე პაროლის შეცვლის მოთხოვნა"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "პირველ შესვლაზე პაროლის შეცვლის მოთხოვნა"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "სჭირდება"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "სჭირდება "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "სჭირდება"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "ჩასასწორებლად საჭიროა ადმინისტრატორის წვდომა"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "მოთხოვნა"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "ვის საჭიროებასა წარმოადგენს"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+"გამოყავით მეხსიერება ჩატვირთვისას ბირთვი ბრძანების სტრიქონის '$0' დაყენებით. "
+"მაგალითად, მიაწერეთ '$1' $2-ს $3-ში ან გამოიყენეთ თქვენი დისტრიბუტივის "
+"ბირთვის არგუმენტების რედაქტორი."
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "რეზერვირებული მეხსიერება"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "საწყის მნიშვნელობებზე დაბრუნება"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "პაროლის თავიდან დაყენება"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "$target-ის ზომის შესვლა"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"დაშიფრული ფაილური სისტემის ზომის შეცვლას ჯერ დისკის განბლოკვა სჭირდება. "
+"შეიყვანეთ დისკის საკვანძო ფრაზა."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "გადატვირთვა"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "სერვისების რესტარტი"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "სერვისების რესტარტი..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "გადატვირთვა"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "კავშირის აღდგენა"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"ავარიის შედეგები $0-დან $1-მდე $2-ის სახით დაკოპრდება, თუ kdump სწორადაა "
+"მორგებული."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+"ავარიის შედეგები შენახული იქნება $0-ში, როგორც $1, თუ kdump სწორადაა "
+"მორგებული."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "გაგრძელება"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "არსებული დაშიფვრის გამოყენება"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "არსებული დაშიფვრის ($0) გამოყენება"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "კრიპტოგრაფიის პოლიტიკის გადახედვა"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "ჟურნალის გადახედვა"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Round robin"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "მარშრუტები"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "გაშვების დრო"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "ახალი ანგარიშის გაშვება"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "გასაშვები პლატფორმა"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "ანგარიშის გაშვება"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr "გაუშვით ეს ბრძანება სანდო ქსელით ან ფიზიკურად, დაშორებულ მანქანაზე:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "გამშვები"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "გაშვებული"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "გაშვების დრო"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux-ის წვდომის კონტროლის შეცდომები"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "ამ სისტემაზე SELinux-ი გათიშულია"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "ამ სისტემაზე SELinux-ი გათიშულია."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux-ის წესები"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "SELinux-ის სტატუსი უცნობია."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "SELinux-ის პრობლემების გადაჭრა"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "$target-ის SMART თვითშემოწმების გაშვება"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"SOS reporting აგროვებს სისტემურ ინფორმაციას პრობლემების გამოვლენაში "
+"დასახმარებლად."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH გასაღები"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "SSH გასაღები ბილიკს არ წარმოადგენს"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH გასაღებები"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "STP გადამისამართების დაყოვნება"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "STP hello-ის დრო"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP შეტყობინების მაქსიმალური ასაკი"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP პრიორიტეტი"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Safari-ის მომხმარებლებს სჭირდებათ სერტიფიკატის შემოტანა და სანდო "
+"სერტიფიკატების სიაში ჩამატება:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "შაბათობით"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "შენახვა"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "შენახვა და გადატვირთვა"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "ცვლილებების შენახვა"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "ადგილის შენახვა ინდივიდუალური ბლოკების LZ4-ის საშუალებით შეკუმშვით"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr ""
+"ადგილის შენახვა მონაცემების ერთნაირი ბლოკების მხოლოდ ერთხელ შენახვის "
+"საშუალებით"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"ახალი საკვანძო სიტყვის შესანახად საჭიროა დისკის განბლოკვა. შეიყვანეთ დისკის "
+"საკვანძო ფრაზა."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "გათიშვის დროა $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "გადატვირთვის დროა $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "დალუქული PC"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "ძებნა"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "ძებნის დომენი"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "სახელით ან ID-ით ძებნა"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "სახელით, ჯგუფით ან ID-ით ძებნა"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "წამი უნდა იყოს რიცხვი შუალედში 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "წამი"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "SSH-ის გასაღებები"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "$target-ის გარანტირებული წაშლა"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Linux-ის გაფართოებული უსაფრთხოების (SELinux) მორგება და გამართვა"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "ხელმისაწვდომია უსაფრთოხების განახლებები"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "მხოლოდ უსაფრთხოების განახლებები"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "უსაფრთხოების პაჩები გადატარდება $0-ზე $1-ზე"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "არჩევა"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "აირჩიეთ იდენტიფიკატორი"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "აირჩიეთ მეთოდი"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+"აირჩიეთ ფიზიკური ტომები, რომელიც ლოგიკური ტომის შესაკეთებლად იქნება "
+"გამოყენებული. საჭიროა მინიმუმ $0."
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "გაგზავნა"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "გაგზავნა"
+
+#: pkg/storaged/drive/drive.jsx:123
+msgid "Serial number"
+msgstr "სერიული ნომერი"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "სერვერი"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "სერვერის მისამართი"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "სერვერის მისამართი ცარიელი არ შეიძლება იყოს."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "სერვერი არ შეიძლება ცარიელი იყოს."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "სერვერმა დახურა კავშირი."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "სერვერული პროგრამები"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "სერვისი"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "სერვისის შეცდომა"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "სერვერის ჟურნალი"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "სერვისები"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "მდებარეობის გამომყენებელი სერვისები"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "სესია"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "სესიას ვადა გასდის"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "დაყენება"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "ჰოსტის სახელის დაყენება"
+
+#: pkg/storaged/partitions/partition.jsx:173
+msgid "Set partition type of $0"
+msgstr "$0-ის დანაყოფის ტიპის დაყენება"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "პაროლის დაყენება"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "დროის დაყენება"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "გაგზავნის მიმღები"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "მორგება"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "დააყენეთ სუსტი პაროლი"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"პარამეტრები განსხვავდება შენახული მნიშვნელობებისგან. აღდგება გადატვირთვის "
+"დროს."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "მორგება"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "$target-ზე მარყუჟული მოწყობილობის შექმნა"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "მორგება"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "სიმძიმე"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "ზიარი"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "გარსი"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "გარსის სკრიპტი"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+msgid "Show all $0 rows"
+msgstr "ყელა $0 მწკრივის ჩვენება"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "ყველა ნაკადის ჩვენება"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Show confirmation password"
+msgstr "დადასტურების პაროლის ჩვენება"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "ანაბეჭდების ჩვენება"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "იმ შეტყობინებების ჩვენება, რომლებიც მითითებულ სტრიქონს შეიცავენ."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "system-ის მითითებული სერვისის შეტყობინებების ჩვენება."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "შეტყობინებების მითითებული ჩატვირთვიდან ჩვენება."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "მეტი ნათესაობის ჩვენება"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "პაროლის ჩვენება"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "ურთიერთობების ჩვენება"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "შეწნეხვა"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "ლოგიკური ტომის შემცირება"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "დანაყოფის შემცირება"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "ტომის შემცირება"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "გამორთვა"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "გამორთვა"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "საწყისი დრო"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "ერთრანგიანი"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "ზომა"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "ზომა არ შეიძლება უარყოფითი იყოს"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "ზომა არ შეიძლება ნულის ტოლი იყოს"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "ზომა ძალიან დიდია"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "ზომა რიცხვი უნდა იყოს"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "ზომის მინიმალური მნიშვნელობაა $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "მთავარი ნავიგაციის გამოტოვება"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "შემცველობაზე გადახტომა"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "სლოტი"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "სლოტი $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "სწრაფი ასლი"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "სოკეტები"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "პროგრამების განახლებები"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"პროცესორის შეცდომების პროგრამულ გასწორებას შეუძლია თავიდან აგარიდოთ მასთან "
+"დაკავშირებული პრობლემები. მაგრამ ამ პაჩებს აქვთ გვერდითი ეფექტი - ისინი "
+"აგდებენ წარმადობას. ამ პარამეტრის ცვლილების შედეგებზე პასუხს თქვენ აგებთ."
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr "მყარსხეულიანი დისკი"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "გადაწყვეტა წარმატებით გადატარდა"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "გადაწყვეტილების შეცდომა"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "გადაწყვეტები"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"ამ პულში ზოგიერთი ბლოკური მოწყობილობა ზომაში პულის შექმნის შემდეგ გაიზარდა. "
+"პულის გაზრდა დამატებული ადგილის გამოსაყენებლად უსაფრთხოა."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"პაკეტების მმართველს ამჟამად სხვა პროგრამა იყენებს. გთხოვთ, მოითმინოთ..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "ზოგიერთ პროგრამულ უზრუნველყოფას ხელით გადატვირთვა სჭირდება"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr "უკაცრავად"
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "დალაგებულია ყველაზე ნაკლებიდან ყველაზე მეტად სანდომდე"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "პატარა ზომის კომპიუტერი"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Spanning tree protocol"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Spanning tree protocol (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "მარქაფი"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "მითითებული დრო"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "სიჩქარე"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "სტაბილური"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "დაწყება"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "ჩართვა და გაშვება"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "multipath-ის გაშვება"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "სერვისის გაშვება"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr "მხოლოდ მითითებული თარიღის შემდეგი შეტყობინებების ჩვენება."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "მხოლოდ მითითებულ თარიღზე ძველი შეტყობინებების ჩვენება."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "დაწყებულია"
+
+#: pkg/storaged/jobs-panel.jsx:64
+msgid "Starting MDRAID device $target"
+msgstr "MDRAID მოწყობილობა $target-ის გაშვება"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "სვაპის ($target) გაშვება"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "მდგომარეობა"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "სტატიკური"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "მდგომარეობა"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Stick PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "წებოვანი"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "გაჩერება"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "გაჩერება და გამორთვა"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "გაჩერება და წაშლა"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "გაჩერება და მოძრობა"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "მოწყობილობის გაჩერება"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "hosts ფაილის ჩასწორების დამთავრება"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "ანგარიშის გაჩერება"
+
+#: pkg/storaged/jobs-panel.jsx:63
+msgid "Stopping MDRAID device $target"
+msgstr "MDRAID მოწყობილობა $target-ის გაჩერება"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "სვაპის ($target) გაჩერება"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "საცავი"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "ამ სისტემაზე საცავის მართვა შეუძლებელია."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "საცავის ჟურნალი"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "საკვანძო სიტყვის დამახსოვრება"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "დამახსოვრებული საკვანძო სიტყვა"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+msgid "Stratis block device"
+msgstr "Stratis-ის ბლოკური მოწყობილობა"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+msgid "Stratis block devices"
+msgstr "Stratis-ის ბლოკური მოწყობილობები"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Stratis ბლოკური მოწყობილობები მეტად ვეღარ დაპატარავდება"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+msgid "Stratis filesystem"
+msgstr "Stratis-ის ფაილური სისტემა"
+
+#: pkg/storaged/stratis/pool.jsx:300
+msgid "Stratis filesystems"
+msgstr "Stratis-ს ფაილური სისტემები"
+
+#: pkg/storaged/stratis/pool.jsx:361
+msgid "Stratis filesystems pool"
+msgstr "Stratis-ის ფაილური სისტემების პული"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Startis-ის პული"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr "ზოლებით (RAID 0)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr "ზოლებით და სარკისებურად (RAID 10)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr "ზოლები"
+
+#: pkg/lib/cockpit-components-password.jsx:97
+msgid "Strong password"
+msgstr "ძლიერი პაროლი"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Stub"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "სტილი"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "ქვე-კორპუსი"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "ქვე-ნოუთბუქი"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "systemd-ის სიგნალების გამოწერის შეცდომა: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+msgid "Subvolume needs to be mounted"
+msgstr "საჭიროა, ქვეტომი მიმაგრებული იყოს"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr "აუცილებელია, ქვეტომი ჩაწერადად იყოს მიმაგრებული"
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "წარმატებით დაკოპირდა ბუფერში"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "წარმატებით დაკოპირდა ბუფერში!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "კვირაობით"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "სვაპი"
+
+#: pkg/storaged/block/resize.jsx:292
+msgid "Swap can not be resized here"
+msgstr "სვოპის ზომას აქ ვერ შეცვლით"
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "გამოყენებული სვაპი"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "$0-ის გამორთვა"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "$0-ის გამორთვა"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "$0-ის ჩართვა"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "ადმინისტრატორის წვდომაზე გადართვა"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "შეზღუდულ წვდომებზე გადართვა"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0-ის გათიშვა დაარღვევს სერვერთან კავშირს და შეუძლებელს გახდის "
+"ადმინისტრირების ინტერფეისთან წვდომას."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0-ის ჩართვა დაარღვევს სერვერთან კავშირს და შეუძლებელს გახდის "
+"ადმინისტრირების ინტერფეისთან წვდომას."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "სინქრონიზებულია"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "სინქრონიზებულია $0-თან"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "სინქრონიზაცია"
+
+#: pkg/storaged/jobs-panel.jsx:71
+msgid "Synchronizing MDRAID device $target"
+msgstr "MDRAID მოწყობილობა $target-ის სინქრონიზაცია"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "სისტემა"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "სისტემური დიაგნოსტიკა"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "ინფორმაცია სისტემის შესახებ"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "სისტემა განახლებულია"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "სისტემური ცვლილებები"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "სისტემური დრო"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemd unit-ები"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "ტაბლეტი"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "გასაღებების სერვერი Tang"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "სამიზნე"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "სამიზნეები"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "გუნდი"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Team-ის პორტები"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "გუნდური პორტის მორგება"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "ტერმინალი"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "სესიის დასრულება"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "კონფიგურაციის ტესტი"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "ტესტი ხელმისაწვდომია მხოლოდ როცა სერვისი kdump გაშვებულია."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "kdump-ის პარამეტრების ტესტი"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"გატესტავს kdump-ის პარამეტრს ბირთვის წინასწარგანზრახული ავარიით. ამას საკმაო "
+"დრო შეიძლება, დასჭირდეს და შეიძლება, სისტემა თვითონ არ გადაიტვირთოს. ნუ "
+"გამოიძახებთ სისტემის წინასწარგანზრახულ ავარიას, თუ მასზე რამე მნიშვნელოვანი "
+"ამოცანაა გაშვებული."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "კავშირის შემოწმება"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "$0 პაკეტი ხელმიუწვდომელია ყველა რეპოზიტორიიდან."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "Stratis-ის პულის შესაქმნელად საჭიროა დაყენებული იყოს პაკეტი $0."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "საჭიროა დაყენებული იყოს პაკეტი $0."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "VDO მოწყობილობების შესაქმნელად დაყენდება პაკეტი $0."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP მისამართი და ჰოსტის სახელი არ შეიძლება ცარიელ ადგილებს შეიცავდეს."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+msgid "The MDRAID device is in a degraded state"
+msgstr "MDRAID მოწყობილობა ავარიულ მდგომარეობაშია"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+msgid "The MDRAID device must be running"
+msgstr "MDRAID მოწყობილობა გაშვებული უნდა იყოს"
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr "SSH გასაღები $1-ის $0 $2-ზე დაემატება ფაილ $3-ს $5-ზე $4-ს."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"SSH გასაღები $0 ხელმისაწვდომია სესიის ბოლომდე და ხელმისაწვდომი იქნება სხვა "
+"ჰოსტებზე შესასვლელადაც."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"$0-ზე შესასვლელი SSH გასაღები პაროლითაა დაცული და ჰოსტს პაროლით შესვლა "
+"გამორთული აქვს. შეიყვანეთ $1-ზე არსებული გასაღების პაროლი."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"$0-ზე შესასვლელი SSH გასაღები დაცულია. შეგიძლიათ შეხვიდეთ ან თქვენი პაროლით "
+"ან გასაღები $1-ის პაროლით."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr "ანგარიში '$0' შემდეგ შესვლაზე იძულებული იქნება პაროლი შეცვალოს"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Cocpit-ის სერვისი ავტომატურად მოჰყვება"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"მორგების მდგომარეობა უცნობია. შემდეგი გადატვირთვის შემდეგ შეიძლება შეიცვალოს."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"VDO მოწყობილობის შექმნა გაუქმებულია. მოწყობილობის გამოყენება შეუძლებელია."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"ამჟამად შესულ მომხმარებელს არ აქვს უფლება გასაღებების შესახებ ინფორმაცია "
+"მოითხოვოს."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr "დაფორმატებამდე საჭიროა დისკის განბლოკვა. შეიყვანეთ საკვანძო ფრაზა."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "$0 წაიშლება."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+msgid "The filesystem has no assigned mount point."
+msgstr "ფაილურ სისტემას მინიჭებული მამაგრების წერტილი არ გააჩნია."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "ფაილურ სისტემას მუდმივი მისამაგრებელი წერტილი არ გააჩნია."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"ფაილური სისტემა მორგებულია ჩატვირთვისას ავტომატურად მისამაგრებლად. მაგრამ "
+"დაშიფვრის მისი კონტეინერი ამ დროს განბლოკილი არ იქნება."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"ამჟამად ფაილური სისტემა მიმაგრებულია, მაგრამ შემდეგი ჩატვირთვისას აღარ "
+"იქნება."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"ფაილური სისტემა ამჟამად მიმაგრებულია $0-ზე, მაგრამ შემდეგი ჩატვირთვისას "
+"მიმაგრებული იქნება $1-ზე."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"ფაილური სისტემა უკვე მიმაგრებულია $0ზე, მაგრამ შემდეგი ჩატვირთვისას არ "
+"იქნება მიმაგრებული."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"ფაილური სისტემა ახლა მიმაგრებული არაა, მაგრამ იქნება შემდეგი ჩატვირთვისას."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "ფაილური სისტემა მიმაგრებული არაა."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"ფაილური სისტემა განიბლოკება და მიმაგრდება შემდეგი ჩატვირთვისას. ამას "
+"საკვანძო სიტყვის შეყვანა შეიძლება დასჭირდეს."
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "ანაბეჭდების უნდა ემთხვეოდეს:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "დარესტარტდება შემდეგი სერვისი:"
+msgstr[1] "დარესტარტდება შემდეგი სერვისები:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "სრული სახელი ორწერტილებს არ უნდა შეიცავდეს."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "ჯგუფის ID დადებითი რიცხვი უნდა იყოს"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"ჯგუფის სახელი შეიძლება მხოლოდ შეიცავდეს a-z სიმბოლოებს, ციფრებს, წერტილებს, "
+"ტირეებს და ქვედა ტირეებს"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"საწყისი საქაღალდე $0 უკვე არსებობს. მისი მფლობელი ახალ მომხმარებელზე "
+"შეიცვლება."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "Initrd-ის თავიდან გენერაცია აუცილებელია."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "გასაღების პაროლი ცარიელი ვერ იქნება"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "გასაღების პაროლები არ ემთხვევა"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "დამატებული გასაღები არასწორია."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "ბოლო სლოტს ვერ წაშლით"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "ჩამოთვლილი პროცესები და სერვისები ძალით იქნება გაჩერებული."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "ჩამოთვლილი პროცესები ძალით გაჩერდება."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "ჩამოთვლილი სერვისები ძალით გაჩერდება."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "შესულ მომხმარებელს არ აქვს სისტემური ცვლილებების ნახვს უფლება"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "მიმდინარეობის მანქანის გადატვირთვა"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "მიმაგრების წერტილი $0 დაკავებულია ჩამოთვლილი პროცესების მიერ:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "მიმაგრების წერტილი $0 გამოიყენება ჩამოთვლილი სერვისების მიერ:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "გასაღების ახალი საკვანძო სიტყვა ცარიელი ვერ იქნება"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "პაროლი არ შეიძლება ცარიელი იყოს"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "პაროლები არ ემთხვევა"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "პაროლები არ ემთხვევა."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr "მიღებული ანაბეჭდების გაზიარება პრობლემა არაა."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"მიღებული ანაბეჭდის გაზიარება, ელფოსტის ჩათვლით, უსაფრთხოა. თუ თქვენ სხვას "
+"სთხოვთ, თქვენი სახელით გადაამოწმოს, მათ, შედეგების გამოგზავნა ნებისმიერი "
+"მეთოდით შეუძლიათ."
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"სერვერმა უარყო $0-ის ავთენტიკაცია პაროლის საშუალებით და ავთენტიკაციის სხვა "
+"საშუალებები ხელმიუწვდომელია."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr "სერვერმა ყველა მხარდაჭერილი მეთოდით ავთენტიკაცია უარჰყო."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"სისტემას ამჟამად ჩატვირთვისას ფაილური სისტემის Tang-ის გასაღებების სერვერით "
+"განბლოკვის მხარდაჭერა არ გააჩნია."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"სისტემას ამჟამად ჩატვირთვისას Tang-ის გასაღებების სერვერით root ფაილური "
+"სისტემის განბლოკვის მხარდაჭერა არ გააჩნია."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+"მომხმარებელს '$0' არ აქვს უფლება შეცვალოს პროცესორის უსაფრთხოების პროგრამული "
+"გადაწყვეტილების პარამეტრები"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "მომხმარებელს '$0' კრიპტოგრაფიის პოლიტიკის შეცვლის უფლება არ აქვს"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "მომხმარებელს '$0' ბირთვის სატესტო ავარის ტესტი უფლება არ აქვს"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"ახალი კონფიგურაციის სრულად გამოსაყენებლად მომხმარებელი სისტემაში თავიდან "
+"უნდა შევიდეს."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"მომხმარებლის სახელი შეიძლება მხოლოდ შეიცავდეს a-z სიმბოლოებს, ციფრებს, "
+"წერტილებს, ტირეებს და ქვედა ტირეებს."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"Cockpit-ის გაშვება შეუძლებელია ბრაუზერის კონფიგურაციის გამო ($0 მიუწვდომელია)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "აქტიური გვერდების გარეშე"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"სისმტეაში არის მოწყობილობები მრავალი ბილიკით, მაგრამ multipath სერვისი "
+"გაშვებული არაა."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "ამ ზონაში აქტიური სერვისები არ არსებობს"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "ამ ანგარიშისთვის ავტორიზებული საჯარო გასაღებები ხელმიუწვდომელია."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+"შეკეთებისთვის საკმარისი ადგილი არაა. საჭიროა მინიმუმ $0 ადგილი ფიზიკურ "
+"ტომებზე, რომლებიც ლოგიკურ ტომებს არ უკავიათ."
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"პულში ამ ფაილური სისტემის სწრაფი ასლის ასაღებად საკმარისი ადგილი არაა. "
+"აუცილებელია სულ ცოტა $0, მაგრამ ხელმისაწვდომია, მხოლოდ, $1."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "მანქანასთან კავშირის გაუთვალისწინებელი შეცდომა."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "საჭიროა ეს დამატებითი ნაბიჯები:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "მოხდება ეს ცვლილებები:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "თხელი ლოგიკური ტომი"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "თხლად გაწერილი LVM2 ლოგიკური ტომები"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"ამ MDRAID მოწყობილობას wirte-intent ბიტური რუკა არ გააჩნია. ასეთ ბიტურ რუკას "
+"სინქრონიზაციის დრო საგრძნობლად შეუძლია, შეამციროს."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"NFS მიმაგრება უკვე გამოიყენება და მხოლოდ მისი პარამეტრების შეცვლა შეგიძლიათ."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "VDO მოწყობილობა თავის ყველა ზურგის მოწყობილობას არ იყენებს."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+msgid "This device can not be used for the installation target."
+msgstr "ამ მოწყობილობას დაყენების სამიზნედ ვერ გამოიყენებთ."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "მოწყობილობა აქედან ვერ იმართება."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "მოწყობილობა უკვე გამოიყენება."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "ველი არ შეიძლება ცარიელი იყოს"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "ეს ჯგუფი ძირითადი ჯგუფია შემდეგი მომხმარებლებისთვის:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "განახლებების დაყენების შემდეგ სისტემა გადაიტვირთება."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "შეგროვებული ინფორმაცია მხოლოდ სისტემაში ინახება."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"ეს გასაღებების სერვერი ამ პულის განბლოკვის ერთად-ერთი საშუალებაა და მისი "
+"წაშლა შეუძლებელია."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+"ლოგიკურმა ტომმა რამდენიმე ფიზიკური ტომი დაკარგა და ვეღარ გამოიყენებთ. "
+"საჭიროა, წაშალოთ ის და თავიდან შექმნათ."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+"ამ ლოგიკურმა ტომმა რამდენიმე ფიზიკური ტომი დაკარგა, მაგრამ მონაცემები ჯერ "
+"არა. შეაკეთეთ ის საწყისი საიმედოობის დასაბრუნებლად."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+"ამ ლოგიკურმა ტომმა რამდენიმე ფიზიკური ტომი დაკარგა, მაგრამ მონაცემები ჯერ "
+"არა. წესით, მისი შეკეთება უნდა შეძლოთ."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "ლოგიკური საცავი მთლიანად არ გამოიყენება."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "ეს მანქანა უკვე დამატებულია."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "საკმაო დრო დასჭირდება"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "ეს დანაყოფი მისი შემცველობის მიერ სრულად არ გამოიყენება."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+"ეს საკვანძო ფრაზა ამ პულის განბლოკვისთვის ერთად-ერთი საშუალებაა და მისი "
+"წაშლა შეუძლებელია."
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr "ეს პული მის ბლოკურ მოწყობილობებზე არსებულ ადგილს ბოლომდე არ იყენებს."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "ეს პულ ავარიულ მდგომარეობაშია."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "სისტემა რეგისტრირებული არაა"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "ეს სისტემა მორგებულ პროფილს იყენებს"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "ეს სისტემა რეკომენდებულ პროფილს იყენებს"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"ეს პროგრამა როგორც SELinux-ის პოლიტიკის მორგებაში, ასევე მის ბოლომდე "
+"გაგებაში და პოლიტიკის დარღვევის გადაწყვეტაში დაგეხმარებათ."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"ეს პროგრამა სისტემას ბირთვის ავარიის შემთხვევაში დისკზე დამპის ჩაწერის "
+"მორგებაში დაგეხმარებათ."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"ეს პროგრამა გაშვებული სისტემიდან კონფიგურაცისა და დიაგნოსტიკის მოგროვებაში "
+"დაგეხმარებათ. არქივი შეგიძლიათ ლოკალურად შეინახოთ, ან ცენტრალურად, ჩაწერისა "
+"და ტრეკინგის მიზნებისთვის, ან შეგიძლიათ გადააგზავნოთ მხარდაჭერის, "
+"პროგრამისტებისა და სისტემური ადმინისტრატორების ჯგუფებთან, რათა აპარატურული "
+"პრობლემები აღმოაჩინოთ და გადაჭრათ."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"ეს პროგრამა ლოკალურ საცავს, როგორიცაა ფაილურ სისტემები, LVM2 ტომის ჯგუფები "
+"და NFS მიმაგრებები, მართავს."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"ეს პროგრამა მართავს ქსელს. bond ინტერფეისების, ხიდების, ჯგუფური "
+"ინტერფეისების, VLAN-ების და ბრანდმაუერების მართვა NetworkManager-ისა და "
+"FIrewalld-ის საშუალებით. NetworkManager-ი Ubuntu-ის ნაგულისხმებ systemd-"
+"networkd და Debian-ის ifupdown სკრიპტებთან შეუთავსებელია."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "ერთეული პირდაპირი ჩართვისთვის განკუთვნილი არაა."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "მომხმარებლის სახელი უკვე არსებობს"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr "ამ ტომების ჯგუფის ფიზიკური ტომები აკლია."
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr "ეს ბრაუზერ ძალიან ძველია ვებ კონსოლის გასაშვებად (არ გააჩნია $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"დაამატებს თანხვედრას '_BOOT_IID='-თვის. თუ მითითებული არაა, გამოყენებყლი "
+"იქნება მიმდინარე ჩატვრთვა. თუ ჩატვირთვის ID გამოტოვებულია, გამოყენებული "
+"იქნება დადებითი წანაცვლება ჟურნალის დასაწყისიდან და გამოტანილი იქნება "
+"პირველი რამდენიმე ჩანაწერი. ამიტომ, 1 ნიშნავს პირველ ჩატვირთვას, 2 მეორეს და "
+"ა.შ. მაგრამ -0 ბოლო ჩატვირთვაა, -1 ბოლოს წინა და ა.შ."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"დაამატებს თანხვედრას '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' და 'UNIT='-თან "
+"მითითებული ერთეულის ჟურნალის შეტყობინებებში საძებნად. შეიძლება შეიცავდეს მეტ "
+"ერთეულსაც, გამოყოფილს მძიმეებით. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "უფლებას მოგცემთ მომავალში პაროლის გარეშე შეხვიდეთ."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"ზონა შეიცავს cockpit-ის სერვისს. დარწმუნდით, რომ ზონის ცვლილება ვებ კონსოლის "
+"თქვენს მიმდინარე სესიას არ ბლოკავს."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "ხუთშაბათობით"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "კლასი"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "დრო"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "დროის სარტყელი"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "ტაიმერის შექმნის შეცდომა"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "ტაიმერის წაშლის შეცდომა"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "ტაიმერები"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"მინიშნება: სხვა სისტემებში უპაროლოდ შესასვლელად დაამთხვიეთ თქვენი შესვლის "
+"პაროლი გასაღების პაროლს."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"იმაში დასარწმუნებლად, რომ არ ხდება კავშირის გადაჭერა, შეამოწმეთ ჰოსტის "
+"ანაბეჭდი:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"პროგრამების განახლებების მისაღებად საჭიროა სისტემის Red Hat-ში "
+"დარეგისტრირება RedHat-ის პორტალით ან გამოწერების ლოკალური სერვერით."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"ანაბეჭდის შესამოწმებლად გაუშვით ეს ბრძანება $0-ზე როცა ფიზიკურად ან სანდო "
+"ქსელით იქნებით შესული მანქანაზე:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "დღეს"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "გადართვა"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "თარიღის ამრჩევის გადართვა"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "ფილტრების ჩართ/გამორთ"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "მეტისმეტად ბევრი მონაცემი"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "ხელსაწყოები"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "ყველაზე მეტი CPU-ის მხარჯავი 5 სერვისი"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "ყველაზე მეტი დისკის მხარჯავი 5 სერვისი"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "ყველაზე მეტი მეხსიერების მხარჯავი 5 სერვისი"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "ჯამური ზომა: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "კომპიუტერის კორპუსი"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "შუალედური"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "გადაცემა"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "ტრიგერი"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "დამტრიგერებელი"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "ტრიგერები"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "პრობლემების გადაწყვეტა"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "პრობლემების გადაწყვეტა…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "ჰოსტის ნდობა და დამატება"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "ნდობის გასაღები"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "ნდობის დონე"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "თავიდან სცადეთ"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "$0-თან სინქრონიზაციის მცდელობა"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "სამშაბათობით"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned-ის გაშვების შეცდომა"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned წარმოადგენს სერვისს, რომელიც უთვალთვალებს თქვენს სისტემას და "
+"აოპტიმიზირებს მას ზოგიერთიდატვირთვისთვის. Tuned-ის ბირთვის წარმოადგენს მისი "
+"პროფილები, რომლებიც სისტემას სხვადასხვა დატვირთვას ავტომატურად მოარგებს."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned-ი ხელმიუწვდომელია"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned-ი გაშვებული არაა"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned გამორთულია"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "ადმინისტრატორის წვდომების ჩართვა"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "ტიპი"
+
+#: pkg/storaged/partitions/partition.jsx:165
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "ტიპი შეიძლება, მხოლოდ, შეიცავდეს სიმბოლოებს 0-9, A-F და \"-\"."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr "ტიპი უნდა იყოს ფორმით NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+"ტიპი, მხოლოდ, ზუსტად ორ თექვსმეტობით სიმბოლოს (0-9, A-F) უნდა შეიცავდეს."
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "გასაფილტრად აკრიფეთ"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "გადაწყვეტის ავტომატური გადატარების შეცდომა"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "მისამართზე დაკავშირების შეცდომა"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "$0-თან კავშირის შეცდომა."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"მითითებულ, $0 ჰოსტთან კავშირის პრობლემა. დარწმუნდით, რომ ssh გაშვებულია $1-ე "
+"პორტზე ან მიუთითეთ სხვა პორტი."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "გაფრთხილების დეტალების მიღების შეცდომა."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"$0-ზე SSH გასაღებით ავთენტიკაციის შესვლის პრობლემა. შეიყვანეთ პაროლი. "
+"ავტომატური შესვლისთვის შეიძლება საჭირო გახდეს თქვენი SSH გასაღებების მორგება."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"$0-ზე შესვლის პრობლემა. ჰოსტი არ იღებს პაროლით შესვლას ან არცერთ თქვენს SSH "
+"გასაღებს."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "სერვერამდე მიღწევის შეცდომა"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "მიმაგრების მოხსნის შეცდომა"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+msgid "Unable to repair logical volume $0"
+msgstr "ლოგიკური ტომის ($0) შეკეთება შეუძლებელია"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "პაჩის გაშვების შეცდომა: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "პარამეტრების შენახვის შეცდომა"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "პარამეტრების შენახვის შეცდომა: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "setroubleshootd-ის გაშვების შეცდომა"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "ფაილური სისტემის მოძრობის შეცდომა"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "ხელმიუწვდომელია"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "ხელმიუწვდომელი პაკეტები"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "დაბრუნება"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "მოულოდნელი PackageKit-ის შეცდომა $0-ის დაყენებისას: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "მოულოდნელი შეცდომა"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+msgid "Unformatted data"
+msgstr "დაუფორმატებელი მონაცემები"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "მოწყობილობა"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "უცნობი"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "უცნობი \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "უცნობი ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "უცნობი აპლიკაცია"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "უცნობი კონფიგურაცია"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "ჰოსტის უცნობი სახელი"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "სერვისის უცნობი სახელი"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "უცნობი ტიპი"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "განბლოკვა"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "ავტომატური განბლოკვა ჩატვირთვისას"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr "განბლოკვა ზომის შეცვლამდე"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Stratis-ის დაშიფრული პულის განბლოკვა"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "გასაღების განბლოკვა: $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "$target-ის განბლოკვა"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "დისკის განბლოკვა"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "უმართავი ინტერფეისები"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "მოძრობა"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "ფაილური სისტემის მოძრობა: $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "მიმაგრების მოხსნა"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "$target-ის მიმაგრების მოხსნა"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "უსახელო"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "განჭიკარტება"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "შეუცნობელი მონაცემები"
+
+#: pkg/storaged/block/resize.jsx:306
+msgid "Unrecognized data can not be made smaller here"
+msgstr "აქ უცნობი მონაცემების დაპატარავება შეუძლებელია"
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "აქ უცნობი მონაცემების დაპატარავება შეუძლებელია."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+msgid "Unsupported logical volume"
+msgstr "მხარდაუჭერელი ლოგიკური ტომი"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "დასასრულის დრო"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "არასანდო ჰოსტი"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "განახლება"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "წარმატების ცხრილის განახლება"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "ისტორიის განახლება"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "პაკეტების ინფორმაციის განახლება"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "განახლება წარმატებულად დასრულდა"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "განახლებულია"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr ""
+"განახლებული პაკეტების ძალაში შესასვლელად შეიძლება გადატვირთვა გახდეს საჭირო."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "ხელმისაწვდომია განახლება"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "განახლებების ისტორია"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "განახლებები გადატარდება $0 $1-ზე"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "მიმდინარეობს განახლება"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "სტატუსის განახლება..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "ჩართულია"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "გამოყენება"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "$0-ის გამოყენების წესები"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "გამოყენება"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "შეკუმშვის გამოყენება"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "დედუპლიკაციის გამოყენება"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "გასაღების გამოყენება"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "პაროლის გამოყენება"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "ამ გასაღებების სხვა სისტემებში შესასვლელად გამოყენება"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "გაფართოებული ჟურნალიზაციის ჩართვა"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "გამოყენებულია"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"გამოსადეგია მიმაგრების წერტილებისთვის, რომლებიც სავალდებული არაა და ჩარევას "
+"საჭიროებს (მაგ: საკვანძო სიტყვის შეყვანას)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "მომხმარებელი"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "მომხმარებლის ID"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "მომხმარებლის ID უკვე გამოიყენება"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "მომხმარებლის ID დადებითი რიცხვი უნდა იყოს"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "მომხმარებლის ID $0-ზე მეტი ვერ იქნება"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "მომხმარებლის ID $0-ზე ნაკლები ვერ იქნება"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "მომხმარებლის სახელი"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "მომხმარებლის სახელი ცარიელი არ შეიძლება იყოს"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "მომხმარებლის სახელი"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "LUKS დაშიფვრის გამოყენებით"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Tang სერვერის გამოყენებით"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "VDO-ის მხარდამჭერი მოწყობილობები მეტად ვეღარ დაპატარავდება"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO მოწყობილობა: $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "VDO ფაილური სისტემის საცავი (შეკუმშვა/დედუპლიკაცია)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN ID"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "მისამართის დადასტურება"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "ავთენტიკაციის კოდის გადამოწმება"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "მომწოდებელი"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "შემოწმებული"
+
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "ანაბეჭდის გადამოწმება"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "გასაღების შემოწმება"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "შემოწმება"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "ვერსია"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "$target-ის ძალიან უსაფრთხოდ წაშლა"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "ყველა პროცესორის ნახვა"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "ყველა დისკის ნახვა"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "ყველა ჟურნალის ნახვა"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "ყველა სერვისის ნახვა"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "ავტომატიზაციის სკრიპტის ნახვა"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "დეტალური ჟურნალის ნახვა"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "აპარატურის დეტალების ნახვა"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "შესვლების ისტორიის ნახვა"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "ჟურნალის ნახვა"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "გრაფიკებისა და ისტორიის ნახვა"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "დისკების მიმოცვლის სათითაოდ ნახვა"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "პროექტის ვებგვერდის ნახვა"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "ანგარიშის ნახვა"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "განახლების ჟურნალის ნახვა"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "მეხსიერების ინფორმაციის ნახვას ადმინისტრირების წვდომა სჭირდება."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "ბრანდმაუერზე გადასვლა"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "ტომების ჯგუფი"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+msgid "Volume group is missing physical volumes"
+msgstr "ტომების ჯგუფს ფიზიკური ტომები აკლია"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "საცავის ზომაა $0. შემცველობის კი $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "მოლოდინი"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "დეტალების მოლოდინი..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "შეყვანის მოლოდინი…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"სხვა პროგრამების მიერ პაკეტების მმართველის გამოყენების დასრულების მოლოდინი..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "პროგრამების მართვის სხვა ოპერაციების დასრულების მოლოდინი"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "გაშვების მოლოდინი…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "სჭირდება"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "უნდა"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "წინასწარი გაფრთხილებები და ზემოთ"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+msgid "Weak password"
+msgstr "სუსტი პაროლი"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "ვებ კონსოლი"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "ვებ კონსოლი Linux სერვერებისთვის"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "ვებ კონსოლი დარესტარტდება"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "ვებ კონსოლი გაშვებულია შეზღუდული წვდომის რეჟიმში."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "ვებ კონსოლის ლოგო"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "ოთხშაბათობით"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "კვირაში ერთხელ"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "კვირა"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "როდის"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "როცა ცარიელია, მიმდინარე მომხმარებლით შესვლა"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"როცა ვებ კონსოლი გადაიტვირთება, თქვენ ვეღარ დაინახავთ ინფორმაციას "
+"მიმდინარეობის შესახებ. მაგრამ განახლების პროცესი ფონურად გაგრძელდება. "
+"მიმდინარეობის დასანახავად დაუკავშირდით თავიდან."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"როცა ეს პარამეტრი ჩართულია, ახალი პული დაშვებულზე მეტის სამუშაოდ მომზადებს "
+"უფლებას არ მოგცემთ. უნდა მიუთითოთ მაქსიმალური ზომა თითოეული ფაილური "
+"სისტემისთვის, რომელსაც პულში შექმნით. ფაილური სისტემების გაზრდა მათი შექმნის "
+"შემდეგ შეუძლებელი იქნება. სწრაფი ასლების გამოყოფა სრულად ხდება მათი "
+"შექმნისას. მათი ჯამური ზომა არ შეძლება, პულის ზომაზე მეტი იყოს. ამ მიდგომის "
+"დადებითი მხარე ისაა, რომ ამ პულში არსებულ ფაილურ სისტემებზე ადგილის გათავება "
+"თქვენთვის მოულოდნელობა არ იქნება. უარყოფითი მხარე კი ის, რომ ყველა ფაილური "
+"სისტემის ზომა წინასწარ უნდა იცოდეთ და სწრაფი ასლების შექმნა შეზღუდულია."
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "თეთრი"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "დაყენებული იქნება \"ავტომატურზე\""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+msgid "World wide name"
+msgstr "WWN სახელი"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "ჩაწერა"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "ძირითადად-ჩაწერა"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "ჩაწერა"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "არასწორი მოხმარებელი ან პაროლი"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "წლიურად"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "დიახ"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "$0-ს პირველად უკავშირდებით."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "ბრანდმაუერის ჩასწორების წვდომა აკრძალულია."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr "არ გაქვთ ამ ანგარიშის საჯარო გასაღებების ნახვის წვდომა."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "გამოხვედით უქმედ ყოფნის გამო."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "სცადეთ ჩატვირთოთ ძველი ელემენტები."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr "ავტომატურად შესაცვლელად შეიძლება გასაღების პაროლის შეცვლა მოგიწიოთ."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "პაროლის შესაცვლელად უფრო მეტხანს უნდა მოითმინოთ"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr "მეტრიკის ისტორიის სანახავად საჭიროა ავტორიზაცია"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "ახლა გაქვთ ადმინისტრატორის წვდომა."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "გახვალთ $0 წამში."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "თქვენი ანგარიში"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"თქვენს ბრაუზერს არ გააჩნია კონტექსტური მენიუდან ჩასმის მხარდაჭერა. სცადეთ "
+"დააჭიროთ Shift+Insert-ს."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "თქვენი ბრაუზერი თქვენი წვდომის დონეებს სესიებს შორისაც დაიმახსოვრებს."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"თქვენი სერვერი მალე კავშირს დახურავს. გადატვირთვის შემდეგ შეგეძლებათ თავიდან "
+"დაუკავშირდეთ."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "თქვენი სესია გაწყვეტილია."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "სესიის ვადა გასულია. თავიდან შედით."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "ზონა"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[ბინარული მონაცემები]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[მონაცემების გარეშე]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "წვდომა"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "აქტიური"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "დამატება"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "დამატება"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "ქსელის ჩართვის შემდეგ"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "აპები"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "აქტივის ჭდე"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "დრო"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "საშუალო: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "VDO მოწყობილობის მხარდამჭერი მოწყობილობა"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "გადაბმა"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "ჩატვირთვა"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "ხიდი"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+msgid "btrfs device"
+msgstr "btrfs მოწყობილობება"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+msgid "btrfs devices"
+msgstr "btrfs მოწყობილობები"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+msgid "btrfs subvolume"
+msgstr "Btrfs ქვეტომი"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr "btrfs ქვეტომი $0 $1-დან"
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+msgid "btrfs subvolumes"
+msgstr "btrfs ქვეტომები"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+msgid "btrfs volume"
+msgstr "btrfs ტომი"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "შეცდ. გასწ"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "ბაიტი"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+msgid "cache"
+msgstr "კეში"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "შეცვლა"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "Cockpit-podman-ი დაყენებული არაა"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "ბრძანება"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "კონსოლი"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "coredump"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "პროცესორი"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "ავარია"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+msgid "data"
+msgstr "მონაცემები"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "თარიღი"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+msgid "deactivate"
+msgstr "დეაქტივაცია"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "შეცდომების მოძებნა"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "წაშლა"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr "btrfs ტომის მოწყობილობა"
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "გამორთვა"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "დისკი"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "დისკები"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "დომენი"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "დისკი"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "ჩასწორება"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "ჩართვა"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+msgid "encrypted"
+msgstr "დაშიფრულია"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "დაშიფვრა"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "გაუმჯობესება"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "შეცდომა"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "ყოველ პარასკევს"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "ყოველ ორშაბათს"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "ყოველ შაბათს"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "ყოველ კვირას"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "ყოველ ხუთშაბათს"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "ყოველ სამშაბათს"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "ყოველ ოთხშაბათს"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "ყოველდღე"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "გაფართოება"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "ssh-ის ჰოსტის გასაღებების სიის გამოტანის შეცდომა: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "ფაილური სისტემა"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "ბრანდმაუერი"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "მიმდინარე და მომავალი ბირთვებისთვის"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "მხოლოდ მიმდინარე ბირთვისთვის"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "ფორმატი"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "$0-დან"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "$0დან $1-ზე"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "გრაფიკები"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "გაზრდა"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "აპარატურა"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "ისტორია"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "ჰოსტი"
+
+#: pkg/storaged/drive/drive.jsx:65
+msgid "iSCSI Drive"
+msgstr "iSCSI დისკი"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+msgid "iSCSI drives"
+msgstr "iSCSI დისკები"
+
+#: pkg/storaged/iscsi/session.jsx:45
+msgid "iSCSI portal"
+msgstr "iSCSI პორტალი"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "გამოტოვება შეცდომის შემთხვევაში"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "ბრაუზერების უმეტესობაში"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "არამდგრადი"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "ინიციალიზაცია"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "დაყენება"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "ინტერფეისი"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "არასწორია: განსაზღვრულია ერთზე მეტი სამიზნე"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "ჟურნალი"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "journalctl-ის მინი-დოკუმენტაცია"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "kdump-ის მდგომარეობა"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "გასაღებები"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "შესვლა"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "მანქანა"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "ნიღაბი"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "მაქს: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+msgid "member of MDRAID device"
+msgstr "MDRAID მოწყობილობის წევრი"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "Stratis-ის პულის წევრი"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "მეხსიერება"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "მეტრიკები"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "შემოვლა"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "მეტი დეტალი"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "მიმაგრება"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "ქსელი"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "არასოდეს მიმაგრდება ჩატვირთვის დროს"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "nfs გატანა ცარიელია"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "nfs სერვერი ცარიელია"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "nfs სერვერი სწორი IPv6 არაა"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "პრიორიტეტი"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "არცერთი"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "$0 CPU-დან"
+msgstr[1] "$0 ცალი CPU-დან"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "$0-ზე"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "ოპერაციული სისტემა"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "ოპერაციული სისტემა"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "პაკეტი"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "განყოფილება"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "პაროლი"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "პაროლის ხარისხი"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "პაჩები"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "ბილიკი"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "წარმადობა"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "LVM2 საცავების ჯგუფის წევრი ფიზიკური საცავი"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "ჩადგმა"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service-ის შეცდომა"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service-ის შეცდომა მონაცემების შეგროვებისას"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service გაშვებული არაა"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "pod"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "პორტი"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "კვება"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "RAID"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "მხოლოდ კითხვისთვის"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "რეკომენდებულია"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "LVM2-დან წაშლა"
+
+#: pkg/storaged/utils.js:934
+msgid "remove from MDRAID"
+msgstr "MDRAID-დან წაშლა"
+
+#: pkg/storaged/utils.js:904
+msgid "remove from btrfs volume"
+msgstr "წაშლა btrfs ტომიდან"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "რესტარტი"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "როლები"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "$0-ის გაშვება"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "უსაფრთხოება"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "მიმდევრობითი"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "სერვისი"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "გარსი"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "ნაკლების ჩვენება"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "მეტის ჩვენება"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "შემცირება"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "გათიშვა"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "სოკეტი"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "გადაწყვეტა"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "გადაწყვეტის დეტალები"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "sos-ის ანგარიშის შეცდომა"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ssh გასაღები არ წარმოადგენს ბილიკს"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "ssh სერვერი ცარიელია"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "გაჩერება"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "შეცდომის შემთხვევაში ჩატვირთვის გაჩერება"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+msgid "stopped"
+msgstr "გაჩერებულია"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "მიზანი"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "გუნდი"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "დრო"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "ტაიმერი"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "მოდული"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "უცნობი"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "უცნობი სამიზნე ობიექტი"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "unmask"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "მოძრობა"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "$0-ზე დარჩენილი დაუხლეჩავი სივრცე"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "მომხმარებელი"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "მომხმარებლის სახელი"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "ვიყენებ გასაღების აღწერას $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "ვერსია"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "საცავი"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "გაფრთხილება"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "პაკეტი wireguard-tools დაყენებული არაა"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "დიახ"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "ზონა"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "სერვისი Kdump დაყენებული არაა. დარწმუნდით, რომ პაკეტი kexec-tools "
+#~ "დაყენებულია."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "მეხსიერება არაა რეზერვირებული. რეზერვირებისთვის მიუწერეთ თქვენი ბირთვის "
+#~ "ბრძანებათა ველს crashkernel პარამეტრი (მაგ /etc/default/grub ფაილში). "
+#~ "მაგ: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "სერვისი გაშვებულია"
+
+#~ msgid "Service is starting"
+#~ msgstr "სერვისი ეშვება"
+
+#~ msgid "Service is stopped"
+#~ msgstr "სერვისი გაჩერებულია"
+
+#~ msgid "Service is stopping"
+#~ msgstr "სერვისი ჩერდება"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "kdump-ის ტესტი ბირთვის წინასწარგანზრახული ავარიით."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "ბირთვის ბრძანებების სტრიქონში crashkernel აღწერილი არაა"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (დაშიფრული)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "Stratis-ის $0 პული"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 ქეში"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 უცნობი კლასიდან"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 თავისუფალი"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "დისკის მოცილებამდე საჭიროა სამარქაფო დისკის დამატება."
+
+#~ msgid "Backing device"
+#~ msgstr "სამარქაფო მოწყობილობა"
+
+#~ msgid "Block"
+#~ msgstr "ბლოკი"
+
+#~ msgid "Content"
+#~ msgstr "შემცველობა"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "მოწყობილობა"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "მოწყობილობის ფაილი"
+
+#~ msgid "Devices"
+#~ msgstr "მოწყობილობები"
+
+#~ msgid "Drives"
+#~ msgstr "დისკები"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr "ზომის შეცვლამდე საჭიროა დაშიფრული საცავების განბლოკვა."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "ფაილური სისტემა (დაშიფრული)"
+
+#~ msgid "Filesystems"
+#~ msgstr "ფაილური სისტემები"
+
+#~ msgid "Free"
+#~ msgstr "თავისუფალი"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "გაათავისუფლეთ ადგილი ამ ჯგუფში. შეამცირეთ ან წაშალეთ სხვა ლოგიკური "
+#~ "საცავები ან დაამატეთ ახალი ფიზიკური საცავი."
+
+#~ msgid "Inactive volume"
+#~ msgstr "არააქტიური საცავი"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "გასაღებების სერვერი"
+
+#~ msgid "LVM2 member"
+#~ msgstr "LVM2-ის წევრი"
+
+#~ msgid "Locked devices"
+#~ msgstr "ჩაკეტილი მოწყობილობები"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "მოდელი"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS მიმაგრებები"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "NFS-ის მხარდაჭერა დაყენებული არაა"
+
+#~ msgid ""
+#~ "New logical volumes can not be created while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "ახალი ლოგიკური ტომების შექმნა მაშინ, როცა ტომების ჯგუფს ფიზიკური ტომები "
+#~ "აკლია, შეუძლებელია."
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "NFS მიმაგრებები ნაპოვნი არაა"
+
+#~ msgid "No devices"
+#~ msgstr "მოწყობილობების გარეშე"
+
+#~ msgid "No drives attached"
+#~ msgstr "დისკები მიმაგრებული არაა"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "iSCSI სამიზნეები მორგებული არაა"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "ახალი ფაილური სისტემებისთვის ადგილი საკმარისი არაა"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "სხვა მონაცემები"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "დაყოფილი ბლოკური მოწყობილობა"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "საკვანძო სიტყვა"
+
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "ფიზიკურ ტომებს ვერ წაშლით, სანამ ტომების ჯგუფს ფიზიკური ტომები აკლია."
+
+#~ msgid "Pool"
+#~ msgstr "პული"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "თხელი საცავების პული"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID-ის დონე"
+
+#~ msgid "RAID member"
+#~ msgstr "RAID-ის წევრი"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "მოხსნადი დისკი"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "$0 მოწყობილობის ჩვენება"
+#~ msgstr[1] "$0 მოწყობილობის(სულ) ჩვენება"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "$0 დისკის ჩვენება"
+#~ msgstr[1] "$0 დისკის (სულ) ჩვენება"
+
+#~ msgid "Show all"
+#~ msgstr "ყველას ჩვენება"
+
+#~ msgid "Source"
+#~ msgstr "წყარო"
+
+#~ msgid "Start pool"
+#~ msgstr "პულის გაშვება"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "გაუშვით პული ფაილური სისტემის დასანახავად."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "მდგომარეობა"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Stratis-ის პული გაჩერებულია"
+
+#~ msgid "Stratis member"
+#~ msgstr "Stratis-ის წევრი"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Stratis-ის პული $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "მხარდაჭერა დაყენებულია."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "დამატებითი დისკების დასამატებლად RAID მოწყობილობა გაშვებული უნდა იყოს."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "RAID მოწყობილობის ბოლო დისკს ვერ წაშლით."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "საცავის ჯგუფში ბოლო ფიზიკურ საცავს ვერ წაშლით."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "ამ ფიზიკური სივრცის წასაშლელად თავისუფალი სივრცე არსად არაა. საჭიროა სულ "
+#~ "ცოტა $0 ."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "დისკის წაშლა, სანამ მოწყობილობა აღდგენის რეჟიმშია, შეუძლებელია."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr "ზომის შეცვლამდე საჭიროა საცავის აქტივაცია."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "შეუცნობელი მონაცემები"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "გამოყენება"
+
+#~ msgid "Used for"
+#~ msgstr "გამოიყენება"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "VDO მარქაფი"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO მოწყობილობა"
+
+#~ msgid "Volume"
+#~ msgstr "საცავი"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "ავტომატური (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "დაეთანხმეთ გასაღებს და დაუკავშირდით"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "დაშიფრული საცავების ზომის შეცვლას აქედან ვერ შეძლებთ."
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Tang გასაღებების სერვერის გამოყენება"
+
+#~ msgid "Use a passphrase"
+#~ msgstr "საკვანძო ფრაზის გამოყენება"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "ბიტური რუკა"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "პულის განბლოკვა შეუძლებელია მისი საკვანძო განმარტების არასწორი ფორმატის "
+#~ "გამო."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "ბიტური რუკის ჩართ/გამორთ"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "სარწმუნდით, რომ Tang სერვერის გასაღების ჰეში ემთხვევა ერთერთს ამ სიიდან:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "SSH-სთან ხელით შემოწმება: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "პორტის ნომერი და ტიპი არ ემთხვევა"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "ჩაკეტილი დაშიფრული სტრატისის პული"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "გაფრთხილების წაშლის შეცდომა: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "გაფრთხილების მიღების შეცდომა: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[ბინარული მონაცემების $0 ბაიტი]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "დისკის I/O-ის პიკი"
+
+#~ msgid "Event"
+#~ msgstr "მოვლენა"
+
+#~ msgid "Event logs"
+#~ msgstr "მოვლენების ჟურნალი"
+
+#~ msgid "Load spike"
+#~ msgstr "დატვირთვის პიკი"
+
+#~ msgid "Memory spike"
+#~ msgstr "მეხსიერების პიკი"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "ქსელის I/O პიკი"
+
+#~ msgid "ssh key"
+#~ msgstr "ssh გასაღები"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "თუ ჩართულია, ფაილური სისტემა არ მიმაგრდება შემდეგი ჩატვირთვისას იმის "
+#~ "მიუხედავად, იყო თუ არა ის მიმაგრებული მანამდე. სასარგებლოა, თუ მიმაგრება "
+#~ "ჩატვირთვისას შეუძლებელია. მაგალითად, თუ გადატვირთვისას საჭიროა პაროლი."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "არ მიამაგრო სისტემის გაშვებისას"
+
+#~ msgid "Listing unit files"
+#~ msgstr "სერვისის ფაილების სია"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "მწყობრიდან გამოსული სერვისების სია: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "ვერ ვიპოვე"
+
+#~ msgid "Validating key"
+#~ msgstr "გასაღების დადასტურება"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "$0 ჯგუფის წაშლა"
+
+#~ msgid "Container administrator"
+#~ msgstr "კონტეინერის ადმინისტრატორი"
+
+#~ msgid "Image builder"
+#~ msgstr "გამოსახულების ამგები"
+
+#~ msgid "Roles"
+#~ msgstr "როლები"
+
+#~ msgid "Server administrator"
+#~ msgstr "სერვერი ადმინისტრატორი"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Unix ჯგუფი: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "წარმატებით დაკოპირდა ბუფერში"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "დიაგნოსტიკის ანგარიშები"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "ბირთვის დამპი"
+
+#~ msgid "Software Updates"
+#~ msgstr "პროგრამების განახლებები"
+
+#~ msgid "Update log"
+#~ msgstr "ჟურნალის განახლება"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "nfs dump-ის დანიშნულება არ გამოიყურება, როგორც სერვერი:ბილიკი"
+
+#~ msgid "$0 Zone"
+#~ msgstr "$0 ზონა"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "დიაგნოსტიკური ანგარიშის შექმნა"
+
+#~ msgid "Done!"
+#~ msgstr "მზდაა!"
+
+#~ msgid "Download report"
+#~ msgstr "ანგარიშის გადმოწერა"
+
+#~ msgid "No archive has been created."
+#~ msgstr "არქივი არ შექმნილა."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "დაადასტურეთ $0-ის წაშლა"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "გენერირებული არქივი შეიცავს მონაცემებს, რომლებიც შეიძლება განვიხილოთ, "
+#~ "როგორც პირადული და მისი შემცველობა კარგად უნდა იქნას გადახედილი მესამე "
+#~ "პირებისთვს გადაცემამდე."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "ეს ხელსაწყო მოაგროვებს სისტემის კონფიგურაციისა და დიაგნოსტიკის "
+#~ "ინფორმაციას და გამოიყენებს პრობლემების აღმოსაფხვრელად."
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "სერვერზე არ აყენია პაკეტი cockpit-system"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "პაკეტი Cocpit-ის ამ ვერსიასთან თავსებადი არაა"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "ამ პაკეტს Cockpit-ის %s ან უფრო ახალი ვერსია სჭირდება"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "წარმადობის მეტრიკები"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "მხოლოდ კითხვისთვის"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "სულ $0 გიბ"
+
+#~ msgid "Apply"
+#~ msgstr "გადატარება"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "$0 ზონის წასაშლელად საჭიროა ავტორიზაცია"
diff --git a/po/ko.po b/po/ko.po
new file mode 100644
index 0000000..88f2513
--- /dev/null
+++ b/po/ko.po
@@ -0,0 +1,12810 @@
+# MinWoo Joh <igtzhsou@naver.com>, 2015. #zanata
+# MinWoo Joh <igtzhsou@naver.com>, 2016. #zanata
+# Eun-Ju Kim <eukim@redhat.com>, 2018. #zanata
+# Ludek Janda <ljanda@redhat.com>, 2018. #zanata, 2020.
+# cockpit <cockpituous@gmail.com>, 2018. #zanata
+# Eun-Ju Kim <eukim@redhat.com>, 2019. #zanata
+# MinWoo Joh <igtzhsou@naver.com>, 2019. #zanata
+# Matej Marusak <mmarusak@redhat.com>, 2020.
+# Seungyeon Choi <p@ij.rs>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2024-02-12 16:45+0000\n"
+"Last-Translator: 김인수 <simmon@nplob.com>\n"
+"Language-Team: Korean <https://translate.fedoraproject.org/projects/cockpit/"
+"main/ko/>\n"
+"Language: ko\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"X-Generator: Weblate 5.3.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "# 사용자"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 활성 영역"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 사용 가능"
+
+#: pkg/storaged/block/resize.jsx:266
+msgid "$0 can not be made larger"
+msgstr "$0는 더 크게 만들 수 없습니다"
+
+#: pkg/storaged/block/resize.jsx:263
+msgid "$0 can not be made smaller"
+msgstr "$0는 더 작게 만들 수 없습니다"
+
+#: pkg/storaged/block/resize.jsx:259
+msgid "$0 can not be resized"
+msgstr "$0는 크기를 조정 할 수 없습니다"
+
+#: pkg/storaged/block/resize.jsx:255
+msgid "$0 can not be resized here"
+msgstr "$0는 여기에서 크기를 조정 할 수 없습니다"
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 청크 크기"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "심각한 영향을 포함 $0 건 해당"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 데이터 + $1 오버헤드가 $2 ($3) 사용"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 일"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 디스크가 없습니다"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 디스크가 없습니다"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 디스크"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 문서"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 오류"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0가 코드 $1로 종료됨"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0가 실패"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 로그인 시도 실패"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "$0 filesystem"
+msgstr "$0 파일 시스템"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 공간"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 시"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "중요한 영향을 포함 $0 건 해당"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0는 존재하는 파일입니다"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 사용 중"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0는 저장소에서 사용 할 수 없습니다."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 키 변경됩니다"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$1 시그널에 의해 $0가 종료되었습니다"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 낮은 심각도"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 분"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 보통 영향"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 달"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 더 알아보기..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 꾸러미"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 꾸러미의 경우 시스템을 재시작해야 합니다"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 페이지"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+msgid "$0 partitions"
+msgstr "$0 파티션"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "사용 가능한 $0 보안 수정"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 서비스 실패"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 서비스를 재시작해야 합니다"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 슬롯이 남아 있습니다"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 순간 사용량"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0가 동기화 되었습니다"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "총 $0"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 사용 가능한 최신화"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0/$1 ($2 저장)을 사용하고 있습니다"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 주"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0가 설치됩니다."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 년"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 영역"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: $1에서 충돌"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name ($host에서)"
+
+#: pkg/storaged/dialog.jsx:1218
+msgid "(Not part of target)"
+msgstr "(대상에 포함되지 않음)"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+msgid "(no assigned mount point)"
+msgstr "(할당된 적재 지점이 없음)"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+msgid "(not mounted)"
+msgstr "(적재 되지 않음)"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(권장 사항)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] "$1 보안 수정 포함"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 일"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1시간"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 분"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 분"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 주"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10일"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11일"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12일"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13일"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14일"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 분"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15일"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16일"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17일"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18일"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19일"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1일"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20분"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20일"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21일"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22일"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23일"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24일"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25일"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26일"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27일"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28일"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29일"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2일"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30일"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31일"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3일"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 분"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4일"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5분"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5분"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5일"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 시간"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 분"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6일"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7일"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8일"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9일"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "호환 가능한 Cockpit 버전이 {{#strong}}에 설치되어 있지 않습니다."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "이 이름을 가진 파일 시스템이 이미 이 풀에 있습니다."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "이 이름으로된 그룹이 이미 존재합니다"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr "보안, 안정성, 성능을 위해 최신 브라우저가 필요합니다."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"네트워크 본딩은 여러 네트워크 연결장치를 하나의 논리적 연결장치로 결합하여 처"
+"리량 또는 중복성을 증가시킵니다."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"$0에서 신규 SSH 키는 $1에서($2에 대해) 생성되고 $3에서 $4에서($5에 대해) 파일"
+"에 추가됩니다."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] "최신화를 적용하려면 꾸러미의 시스템을 재시작해야 합니다:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "이와 같은 이름을 갖는 풀은 이미 존재합니다."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] "최신화를 적용하려면 서비스를 다시 시작해야 합니다:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "누락된 물리 볼륨인 볼륨 그룹은 이름을 재지정 할 수 없습니다."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP 모니터링"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP 핑"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "웹 콘솔 정보"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "부재"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "키 수락 및 로그인"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "허용되는 비밀번호"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "계정 만료"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "계정을 사용할 수 없거나 편집 할 수 없습니다."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "계정"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "동작"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "동작"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "활성화"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr "크기 조정전 활성화"
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "$target 활성화 중"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "활성"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "활성 백업"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "활성 페이지"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "이후 활성화 "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "활성화 상태"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "적응형 로드 밸런싱"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "적응형 전송 로드 밸런싱"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "추가"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "$0 추가"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "DNS 서버 추가"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "네트워크 연결 디스크 암호화 추가"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Tang 키 서버 추가"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "VLAN 추가"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "VPN 추가"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "WireGuard VPN 추가"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "비트맵 추가"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "신규 영역 추가"
+
+# auto translated by TM merge from project: RHOSP Director Installation and
+# Usage , version: 11-Korean, DocId: master
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "주소 추가"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "블록 장치 추가"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "본드 추가"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "브릿지 추가"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "디스크 추가"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "디스크 추가"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "iSCSI 포털 추가"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "키 추가"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "키 서버 추가"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "멤버 추가"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "신규 호스트 추가"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "신규 영역 추가"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "암호문 추가"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "장치(peer) 추가"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+msgid "Add physical volume"
+msgstr "물리 볼륨 추가"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "포트 추가"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "$0 영역에 포트 추가"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "공개 키 추가"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "라우트 추가"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "DNS 검색 추가"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "서비스 추가"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "$0 영역에 서비스 추가"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "$0 영역에 서비스 추가"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "팀 추가"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "영역 추가"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "암호화 옵션에 \"$0\"를 추가하기"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "파일 시스템 옵션에 \"$0\" 추가하기"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 을 추가하면 서버와의 연결이 끊어지고 관리 UI를 사용 할 수 없게 됩니다."
+
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"키 서버 추가하기는 풀 잠금 해제가 필요합니다. 기존 풀 암호문을 제공해 주세요."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"사용자 지정 포트를 추가하면 방화벽이 다시 적재됩니다. 방화벽을 다시 적재하면 "
+"런타임 전용 설정이 손실됩니다!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "키 추가 중"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "$target에 물리 볼륨 추가 중"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "커널 명령행에 rd.neednet=1 추가하기"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "추가 DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "추가 DNS 검색 도메인 $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "추가 동작"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "추가 주소 $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "추가 꾸러미 :"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "추가 포트"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "주소"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "주소 $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "주소는 비워 둘 수 없습니다"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "주소가 유효하지 않습니다"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "주소"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "주소가 올바른 형식이 아닙니다"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Cockpit 웹 콘솔로 관리"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "관리 접근"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr "관리 접근은 보고서 생성하거고 접근해야 합니다."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "필요한 관리 접근"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "고급 TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "이후"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"도메인 종료 후 로컬 인증 정보가 있는 사용자만 시스템에 로그인 할 수 있습니"
+"다. 이 경우 DNS 확인 설정 및 신뢰할 수 있는 CA 목록이 변경 될 수 있으므로 다"
+"른 서비스에도 영향을 미칠 수 있습니다."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "시스템 부팅 후"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "주의"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "경고 이상의 수준"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "별명"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "모두"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr "모든 $0 선택된 물리 볼륨은 선택된 배열을 위해 필요합니다."
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "모든 최신화"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "일체형"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "실행 허용 (마스크 해제)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "허용된 IP"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "허용된 주소"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "추가적인 $0는 선택해야 합니다"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "앤서블"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Ansible 역할 문서"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"로그 메시지에서 모든 텍스트 문자열을 필터링 될 수 있습니다. 문자열은 또한 정"
+"규식 표현의 형식일 수도 있습니다. 또한 메시지 로그 부분에 의해 필터링을 지원"
+"합니다. 이들은 형식 FIELD=VALUE 에서 공백으로 분리된 값이며, 해당 값은 가능"
+"한 값의 쉼표로 구분된 목록일 수 있습니다."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "표시형식"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "응용프로그램 정보가 누락되었습니다"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "응용프로그램"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "응용프로그램 목록"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "적용하고 재시작"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "커널 실시간 패치 적용"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "솔루션 적용"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "새로운 정책을 적용하고...이는 몇 분 정도 걸릴 수 있습니다."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "솔루션 적용 중 ..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "최신화 적용 중"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "최신화 적용 실패"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "/var와 같은 중요한 적재에 적절한"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "앱"
+
+#: pkg/storaged/drive/drive.jsx:102
+msgid "Assessment"
+msgstr "평가"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "자산 태그"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "부팅"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "최소 $0개의 디스크가 필요합니다."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "최소 1개의 블록 장치가 필요합니다."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "최소 1개의 디스크가 필요합니다."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr "적어도 하나 이상의 상위 부분이 쓸 수 있도록 적재되어야 합니다"
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "분에"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "초"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "특정 시간"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "속성"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "감사 로그"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "인증"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "인증 중입니다"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "인증"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "인증 실패"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "인증 실패: 서버 연결 끊김"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr "Cockpit 웹 콘솔의 권한 작업을 수행하려면 인증이 필요합니다"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "인증이 필요합니다"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "SSH 키 승인"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "승인된 공개 SSH 키"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "자동"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "자동(DHCP만)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "자동 로그인"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "자동 최신화"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "자동 시작"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "자동으로 NTP 사용"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "추가 NTP 서버를 자동으로 사용"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "특정 NTP 서버를 자동으로 사용"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "자동 스크립트"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "$0에서 사용 가능한 대상"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "사용 가능한 최신화"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "바이오스"
+
+#: pkg/storaged/partitions/partition.jsx:114
+msgid "BIOS boot partition"
+msgstr "바이오스 부트 파티션"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "바이오스 날짜"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "바이오스 버전"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "계정으로 돌아가기"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "나쁨"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "잘못된 설정"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "벨런서"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "이전"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "바인딩 위치"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "블랙"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "블레이드"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "블레이드 인클로저"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "블럭 장치"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "파일 시스템의 블록 장치"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "블록 장치"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "차단됨"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "본드"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "부팅"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr "만약 파일 시스템이 적재되지 않으면 부팅이 실패하여 원격 접근이 차단됨"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "파일 시스템이 적재되어 않았을 때에도 부팅이 계속 진행됩니다"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "바인딩 기준"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "브릿지"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "브릿지 포트"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "브릿지 포트 설정"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "브로트캐스트"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "손상된 설정"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr "Btrfs 볼륨이 적재되었습니다"
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "사용 가능한 결점 수정 최신화"
+
+# ctx::sourcefile::/rhn/errata/manage/Create.do
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "결점"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "버스 확장 섀시"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"SSH 키 $0의 비밀번호를 $1의 ($2에서) 로그인 비밀번호로 변경하면, 키가 자동으"
+"로 제공되며 추후 비밀번호 없이 $3에 로그인 할 수 있습니다."
+
+#: pkg/static/login.html:61
+msgid "Bypass browser check"
+msgstr "웹 탐색기 점검을 우회합니다"
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "중앙처리장치"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "중앙처리장치 보안"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "중앙처리장치 보안 전환"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "중앙처리장치 사용량"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "중앙처리장치 사용량/부하"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "캐쉬"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "호스트 이름, IP 주소, 별칭 이름 또는 ssh:// URI일 수 있습니다"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr "현재 필터 조합을 사용하여 기록을 찾을 수 없습니다."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "취소"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "전원 끄기 취소"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "재부팅 취소"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "활성화할 수 없습니다"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "알 수 없는 호스트에 연결 할 수 없습니다"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "로그인 정보를 전송할 수 없습니다"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr "이 시스템에서 realmd를 사용할 수 없으므로 도메인에 참여할 수 없습니다"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "이전 이벤트를 예약할 수 없습니다"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "용량"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "캐리어"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "변경"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "암호화 정책을 변경합니다"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "호스트 이름 변경"
+
+#: pkg/storaged/overview/overview.jsx:152
+msgid "Change iSCSI initiater name"
+msgstr "iSCSI 개시자 이름 변경"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "iSCSI 개시자 이름 변경"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+msgid "Change label"
+msgstr "분류 변경"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "암호문 변경"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "비밀번호 변경"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "성능 프로파일 변경"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "프로파일 변경"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "쉘 변경"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "시스템 시간 변경"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "$0의 비밀번호 변경"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "설정 변경"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"때때로 운영 체제 재설치로 인해 키가 변경될 수 있습니다. 그러나 예기치 않은 변"
+"경은 연결을 가로채는 타사의 시도를 나타낼 수도 있습니다."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr "파티션 유형 변경은 부팅을 막을 수 있습니다."
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"설정을 변경하면 서버와의 연결이 끊어지고, 관리 UI를 사용 할 수 없게 됩니다."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "최신화를 확인"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"명령에서 SHA-256 또는 SHA-1 해쉬가 이와 같은 대화상자와 일치하는지 확인합니"
+"다."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "탕(Tang) 서버와 함께 키 해쉬를 확인합니다."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "$target 확인 중"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "IP확인 중입니다"
+
+#: pkg/storaged/jobs-panel.jsx:68
+msgid "Checking MDRAID device $target"
+msgstr "MD레이드 장치 $target 확인 중"
+
+#: pkg/storaged/jobs-panel.jsx:69
+msgid "Checking and repairing MDRAID device $target"
+msgstr "MD레이드 장치 $target 확인과 복구 중"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "$0 꾸러미를 점검 중"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "initrd에서 NBDE 지원을 위한 점검하기"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "신규 응용프로그램 확인 중"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "꾸러미 최신화 점검 중 ..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "설치된 소프트웨어 확인 중"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "연관된 프로세서 확인 중"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "소프트웨어 상태 확인 중"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "응용프로그램에 사용 할 언어를 선택하세요"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "청크 크기"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "등급"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "$target 지우는 중"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "‘시작 실패’ 지우기"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "모든 필터 지우기"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "검색 지우기"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "일반 문자 장치"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "클라이언트 소프트웨어"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "닫기"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "선택한 페이지 닫기"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Cockpit 인증이 잘못 설정되어 있습니다."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "NetworkManager 및 Firewalld의 Cockpit 구성"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit을 지정된 호스트에 연결 할 수 없습니다."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit에서 예기치 않은 내부 오류가 발생했습니다."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit은 웹 브라우저에서 리눅스 서버를 쉽게 관리 할 수 있는 서버 관리자입니"
+"다. 터미널과 웹 도구을 구분하지 않고 사용할 수 있습니다. Cockpit에서 시작된 "
+"서비스는 터미널을 통해 중지할 수 있습니다. 마찬가지로 터미널에서 오류가 발생"
+"한 경우 해당 오류를 Cockpit 저널 연결장치에서 확인 할 수 있습니다."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit은 대화형 리눅스 서버 관리 연결장치입니다."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit은 시스템의 소프트웨어와 호환성이 없습니다."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit이 설치되어 있지 않습니다"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "시스템에 Cockpit이 설치되어 있지 않습니다."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit은 경험이 적은 시스템 관리자에게 적합합니다. 시스템 관리자는 저장장치 "
+"관리, 저널 검사, 서비스 시작 및 중지 등의 간단한 작업을 쉽게 수행할 수 있으"
+"며 여러 서버를 동시에 모니터링 및 관리 할 수 있습니다. 간단하게 장비를 추가하"
+"여 서버를 추가할 수 있으며 추가 후 다른 기기를 관리할 수 있습니다."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit은 자신의 웹 탐색기에서 정확하게 렌더링 되지 않을 수 있습니다"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "진단 및 지원 자료 수집 및 꾸러미"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "커널 충돌 덤프 수집"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "메트릭 수집"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "색"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "쉼표로-구분된 포트, 범위, 서비스가 허용됩니다"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "명령"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "명령을 찾을 수 없음"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "설명"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "tuned와의 통신에 실패했습니다"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "PCI 압축"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "모든 시스템과 장치와 호환됩니다 (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "최신 시스템과 2TB 이상의 하드 디스크와 호환됩니다 (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "충돌 덤프를 압축하여 공간 절약하기"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "압축"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "조건 $0=$1이 충족되지 않았습니다"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "조건이 충족되지 않았습니다"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "설정"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "설정 중입니다"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "IP 인증 중입니다"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "kdump 설정"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "시스템 설정 구성"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "확인"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "$0 삭제를 확인합니다"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "키 비밀빈호 확인"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "신규 키 비밀번호 확인"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "신규 비밀번호 확인"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "비밀번호 확인"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "$0 제거 확인"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "대체 암호문 삭제 확인"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "$0의 정지를 확인합니다"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "충돌 대상"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "충돌"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "자동으로 연결"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "연결 대상"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "연결 대상:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "SETroubleshoot 데몬에 연결 중..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "dbus에 연결하는 데 실패했습니다: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "장치에 연결 중입니다"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "연결 오류"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "연결 실패"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "연결 시간 초과."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "연결이 끊어졌습니다"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "구성"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "연락 된 도메인"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "포함:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "진행"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "세션 계속 진행"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "제어"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "제어"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "제어기"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "변환 가능"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "복사됨"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "복사"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "클립보드로 복사"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "코어 $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "$0에 연결 할 수 없습니다"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "충돌 덤프 위치"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "크래시 보고"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "충돌 시스템"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "생성"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "LVM2 볼륨 그룹 만들기"
+
+#: pkg/storaged/overview/overview.jsx:145
+msgid "Create MDRAID device"
+msgstr "MD레이드 장치 만들기"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "레이드 장치 만들기"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "스트라티스 풀 생성"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "신규 SSH 키를 생성하고 승인합니다"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "파일 시스템 $0의 순간찍기 생성"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "비-고유 UID로 계정 만들기"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "약한 비밀번호로 계정 만들기"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "홈 디렉토리의 소유권 생성 및 변경"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "생성과 적재"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Create and start"
+msgstr "생성과 시작"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "파일 시스템 생성"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "만들기"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "논리 볼륨 만들기"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "신규 계정 만들기"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "신규 파일 시스템 생성"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "신규 그룹 만들기"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "신규 논리 볼륨 만들기"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "이 컨텐츠로 신규 작업 파일을 만듭니다."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+msgid "Create new thinly provisioned logical volume"
+msgstr "신규 씬 프로비젼닝 논리 볼륨 만들기"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "읽기 전용"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "파티션 만들기"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "$0에 파티션 만들기"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "파티션 테이블 만들기"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "순간찍기 만들기"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "순간찍기 생성과 적재"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "순간찍기 전용 생성"
+
+#: pkg/storaged/overview/overview.jsx:174
+msgid "Create storage device"
+msgstr "저장소 장치 만들기"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+msgid "Create subvolume"
+msgstr "하위볼륨 만들기"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "씬 볼륨 만들기"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "타이머 만들기"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "볼륨 그룹 만들기"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "생성일"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "LVM2 볼륨 그룹 $target 생성 중"
+
+#: pkg/storaged/jobs-panel.jsx:67
+msgid "Creating MDRAID device $target"
+msgstr "MD레이드 장치 $target 생성 중"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "VDO 장치 생성 중"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "$target에 파일 시스템 만드는 중"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "논리 볼륨 $target 생성 중"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "파티션 $target 생성 중"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "$target 순간찍기 생성 중"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"이와 같은 $0 를 생성하기는 서버와의 연결이 끊어지고 관리 UI를 사용 할 수 없"
+"게 합니다."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "중대한 이상 수준"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"암호화 정책은 TLS, IPSec, SSH, DNSSec와 커버러스 통신규약을 포함하는 핵심 암"
+"호화 하위 시스템을 구성하는 시스템 구성 요소입니다."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "암호화 정책"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "암호화 정책이 일관적이지 않습니다"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "현재 부팅"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "현재 최대 CPU 사용량"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "현재 사용 중"
+
+#: pkg/kdump/kdump-view.jsx:535
+msgid "Currently not supported"
+msgstr "현재 지원되지 않음"
+
+#: pkg/storaged/partitions/partition.jsx:146
+msgid "Custom"
+msgstr "사용자 지정"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "사용자 정의 암호화 정책"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "사용자 정의 적재 옵션"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "사용자 지정 포트"
+
+#: pkg/storaged/partitions/partition.jsx:180
+msgid "Custom type"
+msgstr "사용자 유형"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "사용자 지정 영역"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "SHA-1 서명 확인이 허용되는 DEFAULT."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS 검색 도메인"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "DNS 검색 도메인 $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "매일"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "위험 경고:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "진하게"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "데이터"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "사용된 데이터"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"자료는 안정성과 성능을 모두 향상시키기 위해 선택한 물리적 볼륨에 두 개의 복사"
+"본으로 교대로 저장됩니다. 선택하려면 적어도 4개의 볼륨이 필요합니다."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"자료는 안정성을 향상시키기 위해 선택한 물리적 볼륨에 두 개 이상의 복사본으로 "
+"저장됩니다. 선택하려면 적어도 2개의 볼륨이 필요합니다."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+"자료는 성능을 모두 향상시키기 위해 교대로 선택한 물리적 볼륨에 저장됩니다. 선"
+"택하려면 적어도 2개의 볼륨이 필요합니다."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"자료는 선택한 물리적 볼륨에 저장되므로 그 중 하나가 손실되어도 영향을 주지 않"
+"습니다. 선택하려면 최소한 3개의 볼륨이 필요합니다."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+"자료는 선택한 물리적 볼륨에 저장되므로 그 중 하나가 손실되어도 영향을 주지 않"
+"습니다. 자료는 또한 성능을 향상하기 위해 교차 저장됩니다. 선택하려면 최소한 3"
+"개의 볼륨이 필요합니다."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+"자료는 선택한 물리적 볼륨에 저장되므로 그 중 두 개가 손실되어도 영향을 주지 "
+"않습니다. 자료는 또한 성능을 향상하기 위해 교차 저장됩니다. 선택하려면 최소"
+"한 5개의 볼륨이 필요합니다."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+"자료는 어떤 추가적인 중복 또는 성능 개선없이 선택된 물리 볼륨에 저장됩니다."
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"날짜 상세는 연-월-일 시:분:초 (YYYY-MM-DD hh:mm:ss)형식이어야 합니다. 대안으"
+"로 문자열 '어제', '오늘', '내일'로 이해 할 수 있습니다. '지금'은 현재 시간을 "
+"나타냅니다. 마지막으로, '-' 또는 '+'를 접두사로 사용하여, 상대 시간으로 지정 "
+"할 수 있습니다"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "비활성화"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+msgid "Deactivate logical volume $0/$1?"
+msgstr "논리 볼륨 $0/$1을 비활성할까요?"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "비활성화 중 입니다"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "$target 비활성화 중"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "디버그 이상의 수준"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "1 씩 감소"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "전용 페리티 (레이드 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "중복"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "기본"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "지연"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "지연은 숫자여야 합니다"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "삭제"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "$0 삭제"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "계정 삭제"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "파일 삭제"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "그룹 삭제"
+
+#: pkg/storaged/stratis/pool.jsx:292
+msgid "Delete pool"
+msgstr "풀 삭제"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "영구적으로 보고를 삭제?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 을 삭제하기는 서버와의 연결이 끊어지고 관리 UI를 사용 할 수 없게 합니다."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "$target 삭제 중"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "LVM2 볼륨 그룹 $target 삭제 중"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "스트라티스 풀 삭제는 이를 포함한 모든 자료를 삭제합니다."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "파일 시스템을 삭제는 해당 파일에 있는 모든 자료를 삭제합니다."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "논리 볼륨을 삭제하면 논리 볼륨 내의 모든 데이터도 함께 삭제됩니다."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "파티션을 삭제하면 파티션 내의 모든 자료도 함께 삭제됩니다."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "삭제하면 MD레이드 장치에서 모든 자료를 삭제합니다."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "삭제하면 VDO 장치에서 모든 자료를 삭제합니다."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "삭제하면 볼륨 그룹에서 모든 자료가 제거됩니다."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr ""
+"삭제하면 이와 같은 하위볼륨에서의 모든 자료와 모든 하위 볼륨이 제거됩니다."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "삭제시 다음 파일을 제거합니다:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "설명"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "데스크탑"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "분리 가능"
+
+# translation auto-copied from project libreport, version master, document
+# libreport
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "상세정보"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "개발"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "장치"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "장치 파일"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "장치는 읽기 전용입니다"
+
+#: pkg/storaged/block/other.jsx:60
+msgid "Device number"
+msgstr "장치 번호"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "진단 보고서"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "디렉토리"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "디렉토리 $0에 쓰기 불가능하거나 존재하지 않습니다."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "동시 멀티스레딩 비활성화"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "방화벽 비활성화"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "tuned 비활성화"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "사용 안함"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "대화형 비밀번호 허용 안 함"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "비밀번호 인증을 허용하지 않습니다"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "실행 할 수 없음 (마스크)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "연결 끊김"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "연결 해제됩니다"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "디스크 I/O"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "디스크는 정상입니다"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "디스크에 오류가 났습니다"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "디스크 비밀번호"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "디스크"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "디스크 사용량"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "제거"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "$0 경고 해제"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "선택한 경고 해제"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "표시 언어"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "분산 패리티 (레이드 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "적재하지 않습니다"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "부팅시 자동으로 적재하지 마세요"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "도킹 스테이션"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "자동으로 시작되지 않습니다"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "부팅시 적재하지 않습니다"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "도메인"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "도메인"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "도메인 주소"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "도메인 관리자 이름"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "도메인 관리자 비밀번호"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "도메인에 연결할 수 없음"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "도메인이 지원되지 않습니다"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "반복 실행하지 않습니다"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "이중 분산 패리티 (레이드 6)"
+
+# translation auto-copied from project CFSE, version sam-1.2, document app,
+# author eukim
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "내려받기"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "신규 브라우저 내려받기 (무료)"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "내려받기 됨"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "내려받기 중"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "$0 내려받기 중"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "드라이브"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "듀얼 랭크"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+msgid "EFI system partition"
+msgstr "EFI 시스템 파티션"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "편집"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "/etc/motd 편집"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Tang 키 서버 편집"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "VLAN 설정 편집"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "WireGuard VPN를 편집합니다"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "본드 설정 편집"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "브릿지 설정 편집"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "$0 영역에서 사용자 정의 서비스 편집"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "호스트 편집"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "호스트 편집"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "motd 편집"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+msgid "Edit mount point"
+msgstr "적재 지점 편집"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "규칙 및 영역 편집"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "서비스 편집"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "서비스 $0 편집"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "팀 설정 편집"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "사용자 편집"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "키 편집에 빈 슬롯이 필요합니다"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "$target 꺼내는 중"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "임베디드 PC"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "비었음"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "비었음"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "빈 비밀번호"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "$target 비우는 중"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "활성화"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "장치 활성화 또는 비활성화"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "서비스 활성화"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "방화벽 활성화"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "사용"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "$0 활성화"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "자료 암호화"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Tang 키 서버로 데이터 암호화"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "암호로 데이터 암호화"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "암호화됨"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "$0 암호화됩니다"
+
+#: pkg/storaged/stratis/pool.jsx:275
+msgid "Encrypted Stratis pool"
+msgstr "암호화된 스트라티스 풀"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "암호화된 $0의 논리 볼륨"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "암호화된 $0의 파티션"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "암호화"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "암호화 옵션"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "암호화된 암호"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "암호화 유형"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "종료됨"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "종료점"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"\"서버\"로 동작하는 종료점은 호스트:포트로 지정되어야 하며, 다시 말하면 공백"
+"으로 놔둘 수 있습니다."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "강제"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "향상된 최신화 사용 가능"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "유효한 MAC 주소를 입력하세요"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "전체 서브넷"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "$0에서 항목"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "$target 제거 중"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "에라타"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "오류"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "오류 이상"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "오류가 발생했습니다"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "$0 설치 중에 오류 발생: PackageKit이 설치되어 있지 않습니다"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "오류 메시지"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "시스템 수정을 검색하기 위해 semanage 실행 중 오류 발생"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "승인된 키를 저장하는 동안 오류가 발생했습니다: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "SELinux 모드 설정 도중 오류 발생: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "이더넷 MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "이더넷 MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "정확히 $0 물리 볼륨이 선택되어야 합니다"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+"정확히 $0 물리 볼륨은 논리 볼륨의 개별 스트라이프 마다 선택되어야 합니다."
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "예: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "예: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "우수한 비밀번호"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "확장 섀시"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "계정 만료"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "$0의 계정 만료"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "내보내기"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "네트워크로 내보내기"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "확장 정보"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "확장 파티션"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS는 적절하게 활성화되지 않았습니다"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "추가 공통 기준 제한 사항이 있는 FIPS."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "실패함"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "컴퓨터를 추가하지 못했습니다: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "포트를 추가하지 못했습니다"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "서비스 추가 실패"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "영역 추가 실패"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "비밀번호 변경 실패"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "PCP 구성에 실패하였습니다"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "알림을 삭제하지 못했습니다: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "tuned 비활성화에 실패했습니다"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "조정된 프로파일 비활성화에 실패함"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "컴퓨터를 편집하지 못했습니다: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "서비스 편집하는데 실패"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "방화벽에서 $0 활성화에 실패"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "tuned 활성화에 실패하였습니다"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "패치 기록에실패했습니다"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "승인된 키를 불러오지 못했습니다."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "장치을 적재하는데 실패함"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"dnf-automatic.timer 또는 dnf-automatic-install.timer를 위한 단위 파일을 구문"
+"분석하는데 실패했습니다. 자동 최신화를 구성하려면 사용자 지정 재정의를 제거하"
+"세요."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "서비스를 재시작하지 못했습니다"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "/etc/motd에 변경 사항을 저장하지 못했습니다"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "설정을 저장하는데 실패함"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "시작하지 못했습니다"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "프로파일 전환에 실패했습니다"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "파일 상태"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "파일 시스템"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "파일 시스템이 잠겨 있습니다"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "파일시스템 이름"
+
+#: pkg/storaged/dialog.jsx:1114
+msgid "Filesystem outside the target"
+msgstr "대상 외부의 파일시스템"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "파일 시스템은 이미 이 적재지점 아래에 적재되어 있습니다."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "이름 또는 설명에 따라 필터링"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "필터 메뉴 항목"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "필터 서비스"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "필터"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "지문"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "방화벽"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "방화벽을 사용 할 수 없습니다"
+
+#: pkg/storaged/drive/drive.jsx:122
+msgid "Firmware version"
+msgstr "펌웨어 버전"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "NBDE 지원 수정"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "폰트 크기"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "실행 금지"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "강제 변경"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "강제 삭제"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "강제 비밀번호 변경"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "포멧"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "$0 포맷"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "초기화 및 적재"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Format and start"
+msgstr "초기화 및 시작"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "초기화 전용"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "초기화하면 저장장치에서 모든 자료가 제거됩니다."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "포워드 딜레이 $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "프레임 번호"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "여유공간"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "자유-형식 검색"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "금요일"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "에서"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "성명"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "게이트웨이"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "일반"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "생성됨"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "$0로 이동"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "응용프로그램으로 이동"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "지금 바로 가기"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "그래프 가시성"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "그래프 가시성 옵션 메뉴"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "그룹"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "그룹 이름"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "그룹"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "확장"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "컨텐츠 확장"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "$0의 논리 크기 확장"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "논리 불륨 확장"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "파티션 늘리기"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "모든 공간을 사용하여 풀 확장하기"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "모든 공간을 사용하여 확장하기"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Hair pin 모드"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Hairpin 모드"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "휴대용"
+
+#: pkg/storaged/drive/drive.jsx:64
+msgid "Hard Disk Drive"
+msgstr "하드 디스크 드라이브"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "하드웨어 정보"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "상태"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Hello 타임 $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "도움말"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "확인 비밀번호 숨기기"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "비밀번호 숨기기"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "계층 ID"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "증가된 공격 표면을 희생하는 더 높은 상호 운용성."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "기록 꾸러미 수"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "홈 디렉토리"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "호스트"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "호스트 키가 잘못되었습니다"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "호스트 이름"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "호스트"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "한 시간 마다"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "시"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "점검하는 방법"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+"내부 오류 - 이와 같은 논리 볼륨은 활성화로 표시되며 연관된 블록 장치가 있어"
+"야 합니다. 아무튼, 이런 블럭 장치를 찾을 수 없습니다."
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP 주소"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"라우팅 프리픽스와 IP 주소. 쉼표로 여러 값을 구분합니다. 예시: 192.0.2.0/24, "
+"2001 : db8 :: / 32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "IPv4 주소"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4 설정"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6 설정"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "식별자"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"만약 비워져 있으면, ID는 연관된 포트 서비스와 포트 번호를 기반으로 발생합니다"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"지문이 일치하면 \"키 수락 및 로그인\"을 눌러주세요. 일치 하지 않을 경우 로그"
+"인하지 않고 관리자에게 문의하십시오."
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"만약 지문이 일치하면, '키 ' 호스트 신뢰 및 추가'를 눌러주세요. 그렇지 않다"
+"면, 연결하지 않고 관리자에게 문의하세요."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "무시"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "입력"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "터미널에서 실행합니다: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"추후 암호 없이 $0에 $1으로 로그인하려면, $2의 $3 로그인 비밀번호를 키 암호로 "
+"사용하거나 키 비밀번호를 비워 두세요."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "동기화"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "비활성"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+msgid "Inactive logical volume"
+msgstr "논리 볼륨 비활성화"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "포함된 서비스"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"기본적으로 들어오는 요청은 차단됩니다. 외부로 나가는 요청은 차단되지 않습니"
+"다."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "일관되지 않은 파일 시스템 적재"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "1 씩 증가"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "인덱스 메모리"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "간접"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "정보"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "정보 이상의 수준"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "초기화합니다"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "디스크 $0를 초기화합니다"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "초기화는 디스크에서 모든 자료를 제거합니다."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "초기화 중..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Insights: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "설치"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "NFS 지원 설치"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "스트라티스 지원 설치"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "모든 최신화 설치"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "응용프로그램 정보를 설치합니다"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "cockpit-pcp 설치"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "k패치 최신화를 설치합니다"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "realmd 지원 설치"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "보안 최신화 설치"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr "setroubleshoot-server를 설치하여 SELinux 이벤트를 해결합니다."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "소프트웨어 설치"
+
+#: pkg/kdump/kdump-view.jsx:571
+msgid "Install the $0 package."
+msgstr "$0 꾸러미를 설치합니다."
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr "설치는 설치된 cockpit 꾸러미 없이 지원되지 않습니다"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "설치되었습니다"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "설치 중"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "$0 설치 중"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "$0을 설치하면 $1이 제거됩니다."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "꾸러미 설치"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "연결장치"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "연결장치 구성원"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "연결장치"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "내부 오류"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "내부 오류: 잘못된 챌린지 헤더"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "유효하지 않음"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "잘못된 주소 $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "잘못된 날짜 형식"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "잘못된 날짜 형식 및 잘못된 시간 형식"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "잘못된 만료 기간"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "잘못된 파일 권한"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "잘못된 키"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "잘못된 메트릭 $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "일 수가 잘못되었습니다"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "잘못된 포트 번호"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "잘못된 접두어 $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "잘못된 접두어 또는 넷마스크 $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "잘못된 범위"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "잘못된 시간 형식"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "잘못된 시간대"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "잘못된 사용자 이름 또는 비밀번호"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "IoT 게이트웨이"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "다른 포트에서 sshd가 실행되고 있습니까?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "작업"
+
+# ctx::sourcefile::Navigation Menu
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "참가"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "도메인에 가입"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "도메인 가입"
+
+# ctx::sourcefile::Navigation Menu
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "참가하기"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "도메인에 가입하려면 realmd를 설치해야 합니다"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "도메인 가입이 지원되지 않습니다"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "네임스페이스에 참여"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "저널"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "저널 항목"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "저널 항목를 찾을 수 없습니다"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "다음으로 이동"
+
+#: pkg/kdump/kdump-view.jsx:569
+msgid "Kdump service is not installed."
+msgstr "Kdump 서비스가 설치되어 있지 않습니다."
+
+#: pkg/kdump/kdump-view.jsx:604
+msgid "Kdump settings"
+msgstr "kdump 설정"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "계속 연결"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "커널 충돌 덤프"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr "커널은 $0 설정으로 부트되지 않았습니다"
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "커널 덤프"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "커널 실시간 패치 $0가 활성화 중입니다"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "커널 실시간 패치 $0가 설치되었습니다"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "커널 실시간 패치 설정"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "커널 실시간 패치 중"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "키 비밀번호"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "알 수 없는 유형의 키 슬롯은 여기에서 수정할 수 없습니다"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "키 소스"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "키"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "키 서버"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "키 서버 주소"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "키 서버를 제거하면 $0 잠금 해제가 되지 않을 수 있습니다."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP 키"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "동적 디렉토리 상호 운영성을 갖는 레거시."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+msgid "LVM2 VDO pool"
+msgstr "LVM2 VDO 풀"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+msgid "LVM2 logical volume"
+msgstr "LVM2 논리 볼륨"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+msgid "LVM2 logical volumes"
+msgstr "LVM2 논리 볼륨"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "LVM2 물리 볼륨"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+msgid "LVM2 physical volumes"
+msgstr "LVM2 물리 볼륨"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM2 볼륨 그룹"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "LVM2 볼륨 그룹 $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr "이름표"
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "랩탑"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "지난 24 시간"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "지난 7일"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "최근 활성"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+msgid "Last cannot be removed"
+msgstr "마지막은 제거 될 수 없습니다"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "마지막 접속 날짜: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+msgid "Last disk can not be removed"
+msgstr "마지막 디스크는 제거 될 수 없습니다"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "마지막 로그인"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "마지막 수정 날짜: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "마지막으로 성공한 로그인:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "배열"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "더 알아보기"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "$0 나가기"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "도메인 나가기"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "암호화를 건너뛰려면 공백으로 둡니다"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "GNU LGPL 버전 2.1 하에서 인가됨"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "경량"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "접근 제한"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "제한된 접근"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"제한된 접근 방식은 관리 권한을 제한합니다. 웹 콘솔의 일부 부분은 기능이 축소"
+"됩니다."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "제한"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "선형성"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "연결 종료 지연"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "로컬 연결"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "연결 모니터링"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "연결 동작 지연"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "연결 보기"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "연결됨"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+msgid "Linux filesystem data"
+msgstr "리눅스 파일시스템 자료"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+msgid "Linux swap space"
+msgstr "리눅스 스왑 공간"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "경청하다"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "수신 포트"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "수신 포트는 숫자여야 합니다"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "단위 나열"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "단위 나열에 실패함: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "적재"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "부하 분산"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "이전 자료 적재"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "이전 항목 적재"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "사용 가능한 최신화 적재에 실패했습니다"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "사용 가능한 최신화를 적재하고 있습니다. 잠시만 기다리십시오..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "이전 항목 적재하기"
+
+# translation auto-copied from project oVirt, version ovirt-3.5, document
+# frontend/webadmin/modules/webadmin/src/main/resources/org/ovirt/engine/ui/frontend/org.ovirt.engine.ui.webadmin.ApplicationConstants
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "키 적재 중..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "SSH 키 로드에 실패했습니다"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "단위 로드에 실패했습니다"
+
+# translation auto-copied from project oVirt, version ovirt-3.5, document
+# frontend/webadmin/modules/webadmin/src/main/resources/org/ovirt/engine/ui/frontend/org.ovirt.engine.ui.webadmin.ApplicationConstants
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "꾸러미 적재 중..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "시스템 수정 적재 중..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "장치 적재에 실패함"
+
+# translation auto-copied from project oVirt, version ovirt-3.5, document
+# frontend/webadmin/modules/webadmin/src/main/resources/org/ovirt/engine/ui/frontend/org.ovirt.engine.ui.webadmin.ApplicationConstants
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "적재 중..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "로컬 계정"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "로컬 파일 시스템"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "로컬 적재 지점"
+
+#: pkg/storaged/overview/overview.jsx:160
+msgid "Local storage"
+msgstr "로컬 저장소"
+
+#: pkg/kdump/kdump-view.jsx:450
+msgid "Local, $0"
+msgstr "로컬, $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "위치"
+
+# 키보드에 써 있는 단어이므로 번역하지 않는다
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "잠그기"
+
+# 키보드에 써 있는 단어이므로 번역하지 않는다
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "$0 잠금"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "계정 잠금"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+msgid "Locked data"
+msgstr "잠김 자료"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "$target 잠금 중"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "로그인"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "$0에 로그인"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "서버 사용자 계정으로 로그인합니다."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "로그 메세지"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "로그아웃"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "사용자 로그아웃"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "로그인 상태"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "논리"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+msgid "Logical Volume Manager partition"
+msgstr "논리 볼륨 관리 파티션"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "논리 크기"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "논리 볼륨"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "논리 볼륨(순간찍기)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "$0 논리 볼륨"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "로그인"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "다시 로그인"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "로그인 실패"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "로그인 형식"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "로그인 기록"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "로그인 기록 목록"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "$0 로그아웃"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "성공적으로 로그아웃되었습니다"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "기록"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "낮은 프로파일 데스크탑"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Lunch Box"
+
+# translation auto-copied from project Satellite6 Hammer CLI Foreman, version
+# 6.1, document hammer-cli-foreman
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+# translation auto-copied from project Blivet, version master, document blivet
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+msgid "MDRAID device"
+msgstr "MD레이드 장치"
+
+#: pkg/storaged/utils.js:334
+msgid "MDRAID device $0"
+msgstr "MD레이드 장치 $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+msgid "MDRAID device is recovering"
+msgstr "MD레이드 장치는 복구 중입니다"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+msgid "MDRAID device must be running"
+msgstr "MD레이드 장치는 실행 중이어야 합니다"
+
+# translation auto-copied from project Blivet, version master, document blivet
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+msgid "MDRAID disk"
+msgstr "MD레이드 디스크"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+msgid "MDRAID disks"
+msgstr "MD레이드 디스크"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII(권장 사항)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU는 양수여야 합니다"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "장치 ID"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "장비 SSH 키 지문"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "메인 서버 섀시"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "유지 관리"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr "원격 장비에서 악성 부분은 다른 연결된 호스트에 영향을 미칠 수 있습니다"
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "파일 시스템 크기 관리"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "관리 저장소"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "관리 연결장치"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "LVM 관리"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "NFS 적재 관리하기"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "레이드 관리"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "VDO 관리"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "VLAN 관리"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "방화벽 관리"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "네트워킹 본드 관리"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "네트워킹 브릿지 관리"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "네트워킹 팀 관리"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "파티션 관리"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "물리적 드라이브 관리"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "서비스 관리"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "소프트웨어 최신화 관리"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "사용자 계정 관리"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "수동"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "수동"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "$target을 오류가 있는 것으로 표시"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "마스크 서비스"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "마스크 설정되었습니다"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"마스크 서비스는 모든 종속 장치가 실행되지 않도록합니다. 이는 예상보다 큰 영향"
+"을 미칠 수 있습니다. 이 장치에 마스크를 설정할 지 확인하십시오."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "최대 메세지 수명 $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+msgid "Media drive"
+msgstr "미디어 드라이브"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "메모리"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "메모리 기술"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "메모리 사용량"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "메모리 사용량/스왑"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "병합됨"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "로그인한 사용자에게 보내는 메세지"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "실행 실패와 관련된 메시지는 저널에서 찾을 수 있습니다:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "사용된 메타데이터"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "방식"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "메트릭"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "측정 항목과 내역 보기"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "메트릭 내역을 적재 할 수 없습니다"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "메트릭 설정"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "미니 PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "미니 타워"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "분은 0-59 사이의 값이어야 합니다"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "분"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "분"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr "미러 (레이드 1)"
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "완화 방법"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "방식"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "모델"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "$target 수정 중"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "월요일"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "관리 주기"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "관리 대상"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "월간"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "자세한 정보..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "적재"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "적재 지점"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "네트워크를 사용 할 수 있게 된 후 적재, 실패를 무시합니다"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "부팅시 자동으로 적재"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "재시작 시 적재"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "부팅시 $0에서 자동으로 적재"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "서비스를 시작하기 전에 적재"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "적재 구성"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "파일 시스템 적재"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "지금 적재"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "지금 $0에서 적재"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "적재 옵션"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "적재 지점"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "적재 지점을 비워둘 수 없습니다"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "적재 지점을 비워둘 수 없습니다."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "적재 지점은 $0로 이미 사용되고 있습니다"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "적재 지점은 \"/\"로 시작해야 합니다."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "읽기 전용으로 적재"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "기다림 없이 적재, 실패를 무시합니다"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "$target 적재 중"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "서비스를 시작하기 전에 적재"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "서비스와 병행하여 적재"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr "서비스와 병행하여 적재, 하지만 네트워크가 가용해야 합니다"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "멀티 시스템 섀시"
+
+#: pkg/storaged/drive/drive.jsx:135
+msgid "Multipathed devices"
+msgstr "다중 경로 장치"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr "다중 주소는 구분 기호로 쉼표(,) 또는 공백을 사용해 지정해야 합니다."
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS 적재"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA 핑"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP 서버"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "이름"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "이름을 입력하셔야 합니다."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "이름을 입력하셔야 합니다."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "이름은 $0 바이트보다 길 수 없습니다"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "이름은 $0자를 초과할 수 없습니다"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "이름은 127자보다 길 수 없습니다."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+msgid "Name cannot be longer than 255 characters."
+msgstr "이름은 255자 보다 길 수 없습니다."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "이름에는 문자 '$0'를 포함할 수 없습니다."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+msgid "Name cannot contain the character '/'."
+msgstr "이름에는 문자 '/'를 포함 할 수 없습니다."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "이름에는 공백이 없어야 합니다."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr "예비 디스크가 필요합니다"
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "최소 하나의 NTP 서버가 필요합니다"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "네트워크"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "네트워크 I/O"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "네트워크 본드"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "네트워크 장치 및 그래프에는 NetworkManager가 필요합니다"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "네트워크 기록"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "네트워크 사용량"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager가 설치되어 있지 않습니다"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager가 동작하지 않음"
+
+#: pkg/storaged/overview/overview.jsx:168
+msgid "Networked storage"
+msgstr "네트워크 저장소"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "네트워킹"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "하지 않기"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "계정 잠금을 하지 않습니다"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "비밀번호가 만료되어서는 안됩니다"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "로그인 하지 않음"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "신규 NFS 적재"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "신규 호스트"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "신규 호스트: $0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "신규 키 비밀번호"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "신규 이름"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "신규 암호문"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "신규 비밀번호"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "신규 비밀번호가 허용되지 않습니다"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "다음"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "아니오"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "지정된 ID가 없습니다"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "SELinux 알림이 없습니다."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "설치되거나 사용 가능한 응용프로그램이 없습니다."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "사용 가능한 홈이 없습니다"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "사용 가능한 블록 장치가 없습니다."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+msgid "No block devices found"
+msgstr "블럭 장치를 찾을 수 없습니다"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "캐리어 없음"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "설정을 찾을 수 없습니다"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "사용 가능한 데이터 없음"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "$0 ~ $1 사이에 사용 가능한 데이터 없음"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "지연 없음"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "사용 가능한 설명 없음"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "설명이 없습니다."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+msgid "No devices found"
+msgstr "장치를 찾을 수 없습니다"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "사용 가능한 디스크가 없습니다."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+msgid "No disks found"
+msgstr "디스크를 찾을 수 없음"
+
+#: pkg/storaged/iscsi/session.jsx:79
+msgid "No drives found"
+msgstr "드라이브를 찾을 수 없음"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "암호화 없음"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "사건 없음"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "파일시스템 없음"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "파일 시스템 없음"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "여유 키 슬롯이 없습니다"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "여유공간이 없습니다"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "이 파티션 후에는 여유 공간이 없습니다"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "지정된 그룹 이름이 없습니다"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "호스트 키를 찾을 수 없습니다."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "이 꾸러미에 대한 설치 꾸러미를 찾을 수 없습니다."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "추가된 키가 없습니다"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "일치하는 언어가 없습니다"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "로그 항목이 없습니다"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "논리 볼륨 없음"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "기록을 찾을 수 없음"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "일치하는 결과를 찾을 수 없습니다"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "미디어가 삽입되어 있지 않습니다"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "파티션 설정하지 않음"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+msgid "No partitions found"
+msgstr "파티션을 찾을 수 없음"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "추가된 장치(peer)가 없습니다."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+msgid "No physical volumes found"
+msgstr "물리 볼륨을 찾을 수 없음"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "실제 이름이 지정되지 않았습니다"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "결과를 찾을 수 없습니다"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"필터 기준과 일치하는 결과가 없습니다. 결과를 표시하려면 모든 필터를 지우십시"
+"오."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "규칙 히트 없음"
+
+#: pkg/storaged/overview/overview.jsx:190
+msgid "No storage found"
+msgstr "저장소를 찾을 수 없음"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+msgid "No subvolumes"
+msgstr "하위볼륨 없음"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "이러한 파일 또는 디렉토리가 없습니다"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "시스템 수정 없음"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "시스템 보고 없음."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "최신화 없음"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "지정된 사용자 이름이 없습니다"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "없음"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "유효한 개인 키가 없습니다"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "방화벽을 비활성화할 권한이 없습니다"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "방화벽을 활성화할 권한이 없습니다"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "사용 불가"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "연결되지 않음"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Insights 에 연결되어 있지 않습니다"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "호스트에 연결되어 있지 않음"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+msgid "Not enough free space"
+msgstr "여유 공간이 부족합니다"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "공간이 부족합니다"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+msgid "Not enough space to grow"
+msgstr "확장 공간이 부족합니다"
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "찾을 수 없습니다"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "설치되지 않음"
+
+#: pkg/networkmanager/network-interface.jsx:680
+msgid "Not permitted to configure network devices"
+msgstr "네트워크 장치 구성을 허용하지 않습니다"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "영역 구성을 허용하지 않습니다"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "이 작업을 실행할 수 있는 권한이 없습니다."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "준비되지 않음"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "등록되지 않음"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "미동작"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "설정 없음"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "동기화 되어 있지 않습니다"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "알림"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "노트북"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "알림 이상의 수준"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "네트워크 주소, 호스트이름과 사용자이름 난독화"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "난독화됨"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "발생 건수 $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "$0에서 $1 사이의 발생 건수"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "발생"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "확인"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "이전 비밀번호"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "이전 비밀번호"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "이전 비밀번호가 허용되지 않습니다"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "적재된 장치에서"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "실패한 경우"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Cockpit이 설치되면 \"systemctl enable --now cockpit.socket\"을 사용하여 이를 "
+"활성화합니다."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "$1 중 $0만 사용됩니다."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "알파벳, 숫자, : , _ , . , @ , - 만 사용 할 수 있습니다"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "긴급 상황에서만"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr "FIPS 방식으로 부팅 할 때에 승인되고 허용된 알고리즘만 사용하세요."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "어머나!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "메트릭을 공유하는 방화벽에서 pmproxy 서비스를 엽니다."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "$target에서 '$operation' 작업"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "옵션"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "번들된 브라우저 사용"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "기타"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"다른 인증 방식은 대화형 비밀번호 인증이 허용되지 않을 때에 여전히 사용 할 수 "
+"있습니다."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "기타 옵션"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "출력"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "개요"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "덮어쓰기"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "기존의 자료를 제로로 덮어쓰기 (더 느리게)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "메트릭 내역에 꾸러미 cockpit-pcp가 누락되어 있습니다"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "꾸러미 정보"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit가 충돌했습니다"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit이 설치되어 있지 않습니다"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit가 오류 코드 $0를 보고했습니다"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "꾸러미"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "분야 이름"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "부모"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "부모 $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "일부분"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "$0의 일부분"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "파티션"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "$0의 파티션"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "파티션 크기는 $0입니다. 해당 크기는 $1입니다."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "파티션"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "파티션"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "수동"
+
+# translation auto-copied from project Anaconda, version master, document
+# anaconda
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "암호문"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "암호문은 비워 둘 수 없습니다"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "암호문는 비워 둘 수 없습니다"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "다른 키 슬롯에서 암호문"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "암호문 제거에서 $0 잠금 해제가 되지 않을 수 있습니다."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "암호문이 일치하지 않습니다"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "비밀번호"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "비밀번호가 성공적으로 변경되었습니다"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "비밀번호 만료"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "비밀번호는 256자보다 길 수 있습니다"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "비밀번호가 허용되지 않습니다"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "비밀번호가 너무 취약합니다"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "비밀번호를 변경해야 합니다"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "비밀번호가 허용되지 않습니다"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "비밀번호 팁"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "붙여넣기"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "붙임 오류"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "기존 키를 붙입니다"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "SSH 공개키 파일 내용을 여기에 붙여넣기합니다"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "경로"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "경로 비용"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "경로 비용 $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "서버상의 경로"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "서버상의 경로는 비워둘 수 없습니다."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "서버상의 경로는 \"/\"로 시작해야 합니다."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "디렉토리 경로"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "파일의 경로"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "경로"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "일시정지"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+"장치(peer) #$0는 잘못된 종료점을 가지고 있습니다. 포트는 숫자이어야 합니다."
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"장치(peer) #$0는 잘못된 종료점을 가지고 있습니다. 호스트:포트, 예제) "
+"1.2.3.4:51820 또는 example.com:51820처럼 지정되어야 합니다"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "장치(peer)"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"장치(peer)는 이와 같은 장치에 연결되어 있는 다른 장비입니다. 다른 장비에서 공"
+"용 키는 서로 공유됩니다."
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot는 당신의 시스템에서 성능 메트릭을 수집하고 분석합니다."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "성능 프로파일"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "주변 장치 섀시"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "영구적"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "영구적으로 $0 그룹을 삭제 할까요?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "영구적으로 $0를 삭제 할까요?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "영구적으로 논리 볼륨 $0/$1를 삭제할까요?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+msgid "Permanently delete subvolume $0?"
+msgstr "영구적으로 하위볼륨 $0를 삭제 할까요?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "권한이 거부되었습니다"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "허용"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "물리"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "물리 볼륨"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "물리 볼륨"
+
+#: pkg/storaged/block/resize.jsx:279
+msgid "Physical volumes can not be resized here"
+msgstr "물리적 볼륨은 여기에서 크기를 변경 할 수 없습니다"
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "날짜 선택"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "고정 단위"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "핑 간격"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "핑 대상"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "고정된 단위"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "피자 박스"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "관리 접근 권한을 얻으려면 인증하세요"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "웹 콘솔을 사용하려면 JavaScript를 활성화하십시오."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "$0 꾸러미를 설치해 주십시오"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "문제를 해결하고 이 부분을 다시 적재하세요."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "만료 날짜를 지정해 주십시오"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "연결할 호스트를 지정해 주십시오"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "우선 적재된 부분을 해제해 주세요."
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "씬 논리 볼륨을 위한 풀"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "씬 프로비저닝된 LVM2 논리 볼륨을 위한 풀"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "씬 프로비저닝된 볼륨을 위한 풀"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "풀 암호문"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "포트"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "이동식"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "포트"
+
+#: pkg/storaged/partitions/partition.jsx:116
+msgid "PowerPC PReP boot partition"
+msgstr "PowerPC PReP 부트 파티션"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "접두 길이"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "접두 길이 또는 넷마스크"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "준비 중입니다"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "존재"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "유지"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "지정 호스트 이름"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "이전 재시작"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "주"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "우선순위"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "우선순위 $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "개인 키"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "관리자에게서 발생한 문제"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "문제 상세 정보"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "문제 정보"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "위치를 사용하는 프로세스"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "진행: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "프로젝트 웹사이트"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "비밀번호를 통한 메세지 제공 시간이 초과되었습니다"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "ssh-add를 통한 메세지 제공 시간이 초과되었습니다"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "ssh-keygen을 통한 메세지 제공시간이 초과되었습니다"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "다음 위치에 다시 로드 전파"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"상호 운용성을 희생하며 예상되는 단기-적인 미래 공격으로부터 보호합니다."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "이들 블록 장치에서 풀을 위한 암호문를 제공합니다:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "공개 키"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr "공개 키는 유효한 개인 키가 입력 될 때에 생성됩니다"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "목적"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "레이드 ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "레이드 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "레이드 0(스트라이프)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "레이드 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "레이드 1(미러)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "레이드 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "레이드 10(미러 스트라이프)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "레이드 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "레이드 4(전용 패리티)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "레이드 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "레이드 5(분산 패리티)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "레이드 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "레이드 6(이중 분산 패리티)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "레이드 섀시"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "레이드 수준"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr "레이드10은 짝수의 물리 볼륨이 필요합니다"
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "램"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "랙 마운트 섀시"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "임의"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "범위"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "범위의 순서를 준수해야 합니다"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "순위"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "로우(raw) 장치"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "읽기"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "더 알아보기..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "읽기 전용"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "읽기"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "읽는 중..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "준비"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "준비"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "실제 호스트 이름"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"실제 호스트 이름에는 소문자, 숫자, 대시, 마침표만 사용할 수 있습니다. (입력"
+"된 하위 도메인 포함)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "실제 호스트 이름은 64자 보다 적은 문자로 구성되어야 합니다"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "다시 적용하고 재시작"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "재시작"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "완료 후 재시작"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "재시작 권장"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "시스템 재시작..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "수신중"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "최근 호스트"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "현재 위협 모델을 위해 권장된 보안 설정."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "재연결"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "복구 중"
+
+#: pkg/storaged/jobs-panel.jsx:70
+msgid "Recovering MDRAID device $target"
+msgstr "MD레이드 장치 $target 복구 중"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "꾸러미 정보 새로고침 중"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "연결을 거부하고 있습니다. 알 수 없는 호스트입니다"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "연결을 거부하고 있습니다. 호스트 키가 일치하지 않습니다"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "연결을 거부하고 있습니다. 알 수 없는 호스트 키입니다"
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "다시-생성합니다"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "initrd 재생하기"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "등록 중…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "연관된 프로세서와 서비스는 강제 종료됩니다."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "연관된 프로세서는 강제 종료됩니다."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "연관된 서비스는 강제 종료됩니다."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "다시읽기"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "전달 소스를 다시 로드"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "다시 적재 중"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "나머지 서비스의 상태 재적재 중"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "CIFS/SMB를 통한 원격 연결"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "FTP를 통한 원격 연결"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "NFS를 통한 원격 연결"
+
+#: pkg/kdump/kdump-view.jsx:456
+msgid "Remote over NFS, $0"
+msgstr "NFS, $0를 통한 원격 연결"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "SFTP를 통한 원격 연결"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "SSH를 통한 원격 연결"
+
+#: pkg/kdump/kdump-view.jsx:453
+msgid "Remote over SSH, $0"
+msgstr "SSH, $0를 통한 원격 연결"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "삭제:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "제거"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "$0 삭제"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "$1영역에서 $0 서비스 제거"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "$0를 삭제하시겠습니까?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Tang 키 서버 삭제?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "장치 삭제"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "호스트 제거"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "항목 제거"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "누락된 물리 볼륨 제거?"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "키 슬롯 $0에서 암호문를 제거합니까?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "암호문 제거?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "$0 서비스 제거"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "$0 영역 제거"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "삭제 중"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "$0 삭제 중"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 을 제거하면 서버와의 연결이 끊어지고 관리 UI를 사용 할 수 없게 됩니다."
+
+#: pkg/storaged/jobs-panel.jsx:66
+msgid "Removing $target from MDRAID device"
+msgstr "MD레이드 장치에서 $target 제거 중"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"다른 암호문의 확인 없이 암호문을 제거하는 것은 만약 다른 암호문은 잊었거나 또"
+"는 잃어 버렸을 경우에 잠금해제 또는 키 관리를 방지 할 수 있습니다."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "$target에서 물리 볼륨 삭제 중"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Cockpit 서비스 제거는 web 콘솔에 접근하지 못 할 수 있습니다. 이 영역이 현재 "
+"사용 중인 web 콘솔 연결에 적용되지 않는지 확인하십시오."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "영역을 제거하면 영역 내의 모든 서비스가 제거됩니다."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "이름변경"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "스트라티스 풀 이름 재지정"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "파일 시스템 이름 변경"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "그룹 이름변경"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "그룹 $0 이름변경"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "논리 볼륨 이름 변경"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "볼륨 그룹 이름변경"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "$target 이름변경 중"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "그룹 이름변경은 sudo 및 유사 규칙에 영향을 줍니다"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr "복구"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+msgid "Repair logical volume $0"
+msgstr "논리 볼륨 $0를 복구합니다"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "$target 복구 중"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "반복"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "매달 반복"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "암호문 반복"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "매주 반복"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "보고"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "보고 이름표"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "ABRT 분석에 보고합니다"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "보고되었습니다; 사용 가능한 연결이 없습니다"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "보고에 실패했습니다"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "보고가 취소되었습니다"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "보고서"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "보고서 :"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "$0 일 마다 비밀번호를 변경해야 합니다"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "$0에서 비밀번호를 변경해야 합니다"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "처음 로그인에서 비밀번호를 변경해야 합니다"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "필요 사항"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "필요 사항 "
+
+# translation auto-copied from project Satellite6 Katello CLI, version 6.0,
+# document keys, author eukim
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "요구 사항"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "편집하려면 관리 권한이 필요합니다"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "필수 사항"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "필수 사항"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+"커널 명령 줄의 '$0' 옵션 설정에서 부트시에 메모리를 예약합니다. 예로, '$1'을 "
+"$2로 확장 ($3에서) 또는 자신의 배포판의 커널 인수 편집기를 사용합니다."
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "예약된 메모리"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "초기화"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "비밀번호 재설정"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "$target 크기 변경 중"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"암호화된 파일 시스템의 크기를 변경하려면 디스크 잠금을 해제해야 합니다. 현재 "
+"디스크의 암호문을 입력하십시오."
+
+# ctx::sourcefile::/rhn/admin/config/Restart.do
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "재시작"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "서비스 재시작"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "서비스 재시작..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "재시작 중"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "연결 복원 중"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"kdump가 올바르게 구성된 경우 충돌 결과는 $0에서 $1까지 $2로 복사됩니다."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr "kdump가 올바르게 구성된 경우 충돌 결과는 $0에 $1로 저장됩니다."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "다시 시작"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "기존 암호화 재사용"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "기존 암호화($0) 재사용"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "암호화 정책을 검토합니다"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "로그 검토"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "라운드 로빈"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "경로"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "다음에서 실행"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "신규 보고서 실행"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "다음 실행"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "보고서 실행"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+"신뢰하는 네트워크 또는 원격 장비에서 물리적으로 이와 같은 명령을 실행합니다:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "실행자"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "작동중"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "동작시간"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux 접근 제어 오류"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "시스템에서 SELinux가 비활성화 되어 있습니다"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "시스템에서 SELinux가 비활성화되어 있습니다."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux 정책"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "SELinux 시스템 상태를 알 수 없습니다."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "SELinux 문제 해결"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "$target의 SMART 셀프 테스트"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr "SOS 보고는 문제 진단과 함께 도움이 되는 시스템 정보를 수집합니다."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH 키"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "ssh 키는 경로가 없습니다"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH 키"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "STP 전송 지연"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "STP Hello 타임"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP 최대 메시지 보관 기간"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP 우선순위"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Safari 사용자는 자체 서명 CA 인증서를 가져와서 인증서를 신뢰해야 합니다:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "토요일"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "저장"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "저장 및 재시작"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "변경 사항 저장"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "LZ4로 개별 블록을 압축하여 공간을 절약합니다"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "동일한 데이터 블록을 한 번만 저장하여 공간을 절약합니다"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"신규 암호문를 저장하려면 디스크 잠금 해제가 필요합니다. 현재 디스크의 암호문"
+"을 제공하십시오."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "$0에 예약된 전원 끄기"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "$0에서 예정된 재시작"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "쉴드 케이스 PC"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "검색"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "검색 도메인"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "이름 또는 ID 검색"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "이름, 그룹 또는 ID 검색"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "초는 0-59 사이의 값이어야 합니다"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "초"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "보안 쉘 키"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "$target을 안전하게 삭제"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "보안이 향상된 리눅스 구성과 문제해결"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "사용 가능한 보안 최신화"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "보안 최신화만"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "보안 최신화는 $1에서 $0에 적용됩니다"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "선택"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "식별자 선택"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "선택 방법"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+"논리 볼륨을 복구하는데 사용해야 하는 물리 볼륨을 선택합니다. 적어도 $0가 필요"
+"합니다."
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "전송"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "전송중"
+
+#: pkg/storaged/drive/drive.jsx:123
+msgid "Serial number"
+msgstr "일련 번호"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "서버"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "서버 주소"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "서버 주소는 비워둘 수 없습니다."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "서버는 비워둘 수 없습니다."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "서버 연결이 종료되었습니다."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "서버 소프트웨어"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "서비스"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "서비스에 오류가 발생했습니다"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "서비스 로그"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "서비스"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "위치를 사용 중인 서비스"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "세션"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "세션이 곧 만료됩니다"
+
+# ctx::sourcefile::/rhn/systems/details/virtualization/VirtualGuestsList.do
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "설정"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "호스트 이름 설정"
+
+#: pkg/storaged/partitions/partition.jsx:173
+msgid "Set partition type of $0"
+msgstr "$0의 파티션 유형을 설정"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "비밀번호 설정"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "시간 설정"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "설정"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "설정"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "약한 비밀번호 설정"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr "설정은 구성된 상태가 다르기 때문에 다음 부팅시 원래 상태로 돌아갑니다."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "설정 중"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "루프 장치 $target 설정 중"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "설정"
+
+# translation auto-copied from project Customer Portal Translations, version
+# PCM_template, document template, author eukim
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "심각도"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "공유되었습니다"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "쉘"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "쉘 스크립트"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+msgid "Show all $0 rows"
+msgstr "모든 $0 행 표시"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "모든 스레드 표시"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Show confirmation password"
+msgstr "비밀번호 확인을 표시합니다"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "지문 표시"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "제공된 문자열을 포함하여 메시지를 표시합니다."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "상세한 systemd 단위를 위해 메세지를 표시하기."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "특정 부팅에서 메세지 표시하기."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "더 많은 연관관계를 보여줍니다"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "비밀번호 표시"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "연관관계를 보여줍니다"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "축소"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "논리 볼륨 축소"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "파티션 축소"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "볼륨 축소"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "종료"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "종료"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "이후"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "단일 등급"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "크기"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "크기는 음수일 수 없습니다"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "크기가 0 이 될 수 없습니다"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "크기가 너무 큽니다"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "크기는 숫자여야 합니다"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "크기는 최소 $0이어야 합니다"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "기본 탐색 건너 뛰기"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "내용으로 건너뛰기"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "슬롯"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "슬롯 $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "순간찍기"
+
+# translation auto-copied from project Satellite6 Katello, version Sam-1.3.0,
+# document katello, author eukim
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "소켓"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "소프트웨어 최신화"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"소프트웨어 기반 완화 방법은 CPU 보안 문제를 예방하는데 도움이 될 수 있습니"
+"다. 이러한 완화 옵션은 성능을 저하시키는 부작용이 있습니다. 이러한 부작용을 "
+"인지하고 필요에 따라 설정을 변경하십시오."
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr "반도체형 드라이브"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "해결책이 성공적으로 적용되었습니다"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "해결책이 실패했습니다"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "해결책"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"이 풀의 일부 블록 장치는 풀이 생성된 후 크기가 늘어났습니다. 풀은 새로 사용 "
+"가능한 공간을 활용하여 안전하게 확장할 수 있습니다."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr "다른 프로그램이 꾸러미 관리자를 사용 중입니다. 잠시 기다려주세요..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "일부 소프트웨어는 수동으로 다시 시작해야 합니다"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr "죄송합니다"
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "신뢰도가 가장 낮은 순에서 가장 높은 순으로 정렬"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "공간-절약형 컴퓨터"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "스패닝 트리 통신규약"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "스패닝 트리 통신규약(STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "예비"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "특정 시간"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "속도"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "안정적"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "시작"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "시작 및 활성화"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "멀티패스 시작"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "서비스 시작"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr "항목 표시를 시작하거나 지정된 날짜보다 최신입니다."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "항목 표시를 시작하거나 지정된 날짜보다 오래되었습니다."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "시작됨"
+
+#: pkg/storaged/jobs-panel.jsx:64
+msgid "Starting MDRAID device $target"
+msgstr "MD레이드 장치 $target 시작 중"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "스왑공간 $target 시작 중"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "상태"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "정적"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "상태"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "스틱 PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "끈적"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "중지"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "중지 및 비활성화"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "중지 및 제거"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "중지 및 적재 해제"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "장치 중지"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "호스트 편집 중지"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "보고서 멈춤"
+
+#: pkg/storaged/jobs-panel.jsx:63
+msgid "Stopping MDRAID device $target"
+msgstr "MD레이드 장치 $target 중지 중"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "스왑공간 $target 중지 중"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "저장소"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "저장소는 이 시스템에서 관리 할 수 없습니다."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "저장소 기록"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "암호문 저장"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "저장된 암호문"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+msgid "Stratis block device"
+msgstr "스트라티스 블럭 장치"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+msgid "Stratis block devices"
+msgstr "스트라티스 블럭 장치"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Stratis blockdevs를 더 작게 할 수 없습니다"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+msgid "Stratis filesystem"
+msgstr "스트라티스 파일 시스템"
+
+#: pkg/storaged/stratis/pool.jsx:300
+msgid "Stratis filesystems"
+msgstr "스트라티스 파일 시스템"
+
+#: pkg/storaged/stratis/pool.jsx:361
+msgid "Stratis filesystems pool"
+msgstr "스트라티스 파일 시스템 풀"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "스트라티스 풀"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr "스트라이프 (레이드 0)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr "미러된 스트라이프 (레이드 10)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr "스트라이프"
+
+#: pkg/lib/cockpit-components-password.jsx:97
+msgid "Strong password"
+msgstr "강력한 비밀번호"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "스텁"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "유형"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "서브 섀시"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "서브 노트북"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "systemd 신호 구독에 실패했습니다. $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+msgid "Subvolume needs to be mounted"
+msgstr "하위볼륨은 적재가 필요합니다"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr "하위볼륨은 쓸 수 있도록 적재되어야 합니다"
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "성공적으로 클립보드로 복사됨"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "성공적으로 클립보드로 복사됨!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "일요일"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "스왑"
+
+#: pkg/storaged/block/resize.jsx:292
+msgid "Swap can not be resized here"
+msgstr "스왑은 여기에서 크기를 조정 할 수 없습니다"
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "스왑 아웃"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "$0 스위치"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "$0 끄기"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "$0 켜기"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "관리자 접근으로 전환"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "제한된 접근으로 전환"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "$0를 끄면 서버와의 연결이 끊어지며 관리 UI를 사용 할 수 없게 됩니다."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "$0 을 켜면 서버와의 연결이 끊어지며 관리 UI를 사용 할 수 없게 됩니다."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "동기화됩니다"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "$0와 동기화됩니다"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "동기화 중"
+
+#: pkg/storaged/jobs-panel.jsx:71
+msgid "Synchronizing MDRAID device $target"
+msgstr "MD레이드 장치 $target 동기화 중"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "시스템"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "시스템 진단"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "시스템 정보"
+
+# ctx::sourcefile::/systems/SystemEntitlements
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "시스템이 최신 상태입니다"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "시스템 수정"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "시스템 시간"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemd 단위"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "테블릿"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang 키 서버"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "대상"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "대상"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "팀"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "팀 포트"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "팀 포트 설정"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "터미널"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "세션 종료"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "구성 시험"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "시험에서는 kdump 서비스가 실행 중인 동안에만 사용 할 수 있습니다."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "kdump 설정 시험"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"커널 충돌에 의해 kdump 설정을 시험합니다. 이와 같은 작업은 시간이 걸릴 수 있"
+"고 시스템은 자동으로 재시작되지 않을 수도 있습니다. 중요한 작업이 실행되는 동"
+"안 의도적으로 시스템을 충돌시키지 마세요."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "연결 시험 중"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "$0 꾸러미는 저장소에서 사용 할 수 없습니다."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "$0 꾸러미는 스트라티스 풀을 생성하려면 설치해야 합니다."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "$0 꾸러미는 설치되어야 합니다."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "$0 꾸러미는 VDO 장치를 생성하려면 설치됩니다."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP 주소 또는 호스트 이름에는 공백을 포함할 수 없습니다."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+msgid "The MDRAID device is in a degraded state"
+msgstr "MD레이드 배열은 성능이 저하된 상태입니다"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+msgid "The MDRAID device must be running"
+msgstr "MD레이드 장치는 동작 중이어야 합니다"
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr "$1의 SSH 키 $0 ($2에서)는 $4의 $3 ($5에서)파일로 추가됩니다."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"SSH 키 $0는 나머지 세션에서 사용 할 수 있고 다른 호스트에 로그인 할 때에도 사"
+"용 할 수 있습니다."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"$0에 로그인하기 위한 SSH 키가 비밀번호에 의해 보호되고 있으며, 그리고 호스트"
+"는 비밀번호로 로그인을 허용 하지 않습니다. $1에서 키의 비밀번호를 제공하세요."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"$0에 로그인하기 위해 SSH 키가 보호되고 있습니다. 당신은 $1에서 자신의 로그인 "
+"비밀번호에 의하거나 키의 비밀번호 제공을 통해 로그인 할 수 있습니다.."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr "'$0' 계정은 다음 로그인 시 비밀번호를 변경해야 합니다"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "cockpit 서비스가 자동으로 포함됩니다"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr "설정된 상태를 알 수 없습니다. 이는 다음 부팅시에 변경될 수 있습니다."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr "VDO 장치 생성이 완료되지 않았기 때문에 장치를 사용 할 수 없습니다."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr "현재 로그인한 사용자는 키에 대한 정보를 볼 수 없습니다."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"디스크는 초기화 하기 전에 잠금이 해제된 상태이어야 합니다. 기존 암호문를 입"
+"력하세요."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "파일 $0가 삭제됩니다."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+msgid "The filesystem has no assigned mount point."
+msgstr "파일시스템은 할당된 적재 지점이 없습니다."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "파일 시스템에 영구 적재 지점이 없습니다."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"파일 시스템이 부팅 시 자동으로 적재되도록 구성되어 있지만 이 암호화된 컨테이"
+"너는 그 때에 잠금이 해제되지 않습니다."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr "파일 시스템이 현재 적재되어 있지만 다음 부팅 후에는 적재되지 않습니다."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"파일 시스템이 현재 $0에 적재되어 있지만 다음 부팅 후에는 $1에 적재됩니다."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"파일 시스템이 현재 $0에 적재되어 있지만 다음 부팅 후에는 적재되지 않습니다."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr "파일 시스템이 현재 적재되어 있지 않지만 다음 부팅시에 적재됩니다."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "파일 시스템이 적재되어 있지 않습니다."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"파일 시스템은 잠금 해제되어 있어야 하고 다음 부팅시에는 적재됩니다. 이는 암호"
+"문를 입력해야 할 수 있습니다."
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "지문 표시가 일치해야 합니다:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "다음 서비스가 다시 시작됩니다:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "성명은 쌍점이 포함되지 않아야 합니다."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "그룹 ID는 양의 정수이어야 합니다"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr "그룹 이름은 a~z 문자, 숫자, 점, 줄표와 밑줄만으로 구성 할 수 있습니다"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"홈 디렉토리 $0가 이미 존재합니다. 이 소유권은 신규 사용자로 변경됩니다."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "initrd는 재생되어야 합니다."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "키 비밀번호를 비워둘 수 없습니다"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "키 비밀번호가 일치하지 않습니다"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "입력하신 키가 유효하지 않습니다."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "마지막 키 홈은 제거 할 수 없습니다"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "나열된 프로세서와 서비스는 강제 종료됩니다."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "나열된 프로세서는 강제 종료됩니다."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "나열된 서비스가 강제로 중지됩니다."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "로그인한 사용자는 시스템 수정 사항을 볼 수 없습니다"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "장치가 재시작되고 있습니다"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "적재 지점 $0는 이들 프로세서에 의해서 사용 중입니다:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "적재지점 $0는 이들 서비스에 의해 사용 중입니다:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "신규 키 비밀번호는 비워둘 수 없습니다"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "비밀번호를 비워둘 수 없습니다"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "비밀번호가 일치하지 않습니다"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "비밀번호가 일치하지 않습니다."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"최종 지문을 전자우편을 포함한 공개적인 방법을 통해 공유 할 수 있습니다."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"결과 지문은 전자우편을 포함하는 공개 방식을 통해 공유되어도 해도 좋습니다. 만"
+"약 누군가 당신을 위해 인증하도록 요청하는 경우, 어떤 방식을 사용하든 결과를 "
+"전송 할 수 있습니다."
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"서버가 비밀번호 인증을 사용하여 '$0' 인증을 거부했습니다. 지원되는 다른 인증 "
+"방법을 사용 할 수 없습니다."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr "서버가 지원되는 방법을 사용하여 인증을 거부했습니다."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"시스템은 현재 부팅시에 탕 키서버와 함께 파일 시스템 잠김 해제를 지원하지 않습"
+"니다."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"시스템은 현재 탕 키서버와 함께 root 파일 시스템 잠김 해제를 지원하지 않습니"
+"다."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr "사용자 $0는 cpu 보안 완화 변경을 허용하지 않습니다"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "사용자 $0는 암호화 정책을 변경 할 수 없습니다"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "사용자 $0는 커널 충돌을 시험하도록 허용되지 않습니다"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"사용자는 새로운 구성을 적용하려면 로그 아웃 했다가 다시 로그인 해야 합니다."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr "사용자 이름에는 a~z 문자, 숫자, 점, 줄표와 밑줄만 사용 할 수 있습니다."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr "웹 검색기의 설정에 따라 Cockpit이 실행되지 않습니다 (접근 불가능 $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "현재 활성화된 페이지가 없습니다"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"시스템에 여러 경로를 갖는 장치가 있지만 다중 경로 서비스가 동작되고 있지 않습"
+"니다."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "이 영역에는 활성 서비스가 없습니다"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "이 계정에 승인된 공개키가 없습니다."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+"복구를 위해 사용 가능한 여유 공간이 충분하지 않습니다. 적어도 $0가 해당 논리 "
+"볼륨을 위해 사용되지 않는 물리 볼륨에서 필요합니다."
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"이 파일 시스템의 순간찍기를 만들기에는 풀 공간이 부족합니다. 최소 $0가 필요하"
+"지만 $1만 사용 가능합니다."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "기기에 연결하는 동안 예기치 않은 오류가 발생했습니다."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "다음과 같은 추가 단계가 필요합니다:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "이들 변경 사항은 다음과 같이 변경됩니다:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "씬 논리 볼륨"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "씬 프로비저닝된 LVM2 논리 볼륨"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"MD레이드 장치는 쓰기-의도 비트맵이 없습니다. 이러한 비트맵은 동기화 시간을 상"
+"당히 감소 시킬 수 있습니다."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr "NFS 적재는 사용되고 있으며 선택만 변경 할 수 있습니다."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "이 VDO 장치는 백업 장치를 사용하지 않습니다."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+msgid "This device can not be used for the installation target."
+msgstr "이 장치는 설치 대상을 위해 사용 될 수 없습니다."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "이 장치는 여기서 관리 할 수 없습니다."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "이 장치는 현재 사용 중입니다."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "이 분야를 비워 둘 수 없습니다"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "이 그룹은 다음 사용자를 위한 기본 그룹입니다:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "최신화가 설치되면 이 호스트가 재시작됩니다."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "이와 같은 정보는 시스템에서만 저장됩니다."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"이와 같은 키 서버는 풀을 잠금 해제하는 유일한 방법이고 제거 될 수 없습니다."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+"이와 같은 논리 볼륨은 물리 볼륨의 일부를 손실하여 더 이상 사용될 수 없습니"
+"다. 이를 삭제하고 신규로 생성하고 이를 대체하세요."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+"이와 같은 논리 볼륨은 아직 자료를 손실하지 않았지만 물리 볼륨의 일부를 손실하"
+"였습니다. 원래의 중복된 부분을 복원하려면 이를 복구하세요."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+"이와 같은 논리 볼륨은 아직 자료를 손실하지 않았지만 물리 볼륨의 일부를 손실하"
+"였습니다. 당신은 이를 복구 할 수 있습니다."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "이 논리 볼륨의 컨텐츠에 의해 완전히 사용되고 있지 않습니다."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "이 컴퓨터는 이미 추가되어 있습니다."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "시간이 걸릴 수 있습니다"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "이 파티션은 컨텐츠에 의해 완전히 사용되고 있지 않습니다."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr "암호문은 풀을 잠금 해제하는 유일한 방법이고 제거 될 수 없습니다."
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr "이 풀은 블록 장치의 모든 공간을 사용하지 않습니다."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "이 풀은 성능이 저하된 상태입니다."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "이 시스템은 등록되어 있지 않습니다"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "이 시스템은 사용자 정의 프로파일을 사용하고 있습니다"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "이 시스템은 권장 프로파일을 사용하고 있습니다"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"이와 같은 도구는 SELinux 정책을 구성하고 정책 위반을 이해하고 해결하는데 도움"
+"을 줄 수 있습니다."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"이와 같은 도구는 커널 충돌 덤프를 디스크에 작성하도록 시스템을 구성합니다."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"이와 같은 도구는 동작 중인 시스템에서 구성 및 진단 정보의 아카이브를 생성합니"
+"다. 아카이브는 기록 또는 추적 목적으로 로컬 또는 집중적으로 저장되거나 기술"
+"적 오류-찾기와 디버깅을 지원하기 위해 기술 지원 담당자, 개발자 또는 시스템 관"
+"리자에게 보낼 수 있습니다."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"이와 같은 도구는 파일시스템, LVM2 볼륨 그룹, 그리고 NFS 적재와 같은 로컬 저장"
+"소를 관리합니다."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"이와 같은 도구는 NetworkManager 및 Firewalld를 사용하여 bonds, bridges, "
+"teams, VLAN과 방화벽과 같은 네트워킹을 관리합니다. NetworkManager는 우분투 기"
+"본 systemd-netowrkd 및 데비안의 ifupdown 스크립트와는 호환되지 않습니다."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "이 장치는 명시적으로 사용하도록 설계되어 있지 않습니다."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "이 사용자 이름이 이미 존재합니다"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr "이와 같은 볼륨 그룹은 일부 물리 볼륨이 없습니다."
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr "이 웹 브라우저는 오래되어 웹 콘솔을 실행할 수 없습니다($0 누락)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"이는 '_BOOT_ID='를 위해 일치 항목을 추가합니다. 만약 지정하지 않으면 현재 부"
+"팅을 위한 기록(로그)가 표시 될 것입니다. 만약 부트 ID가 생략되었으면, 양수 오"
+"프셋은 저널의 시작 부분에서 시작하여 부팅을 조회 할 것이고, 0 보다 같거나 작"
+"은 오프셋은 저널의 끝에서 부팅을 조회 할 것입니다. 그러므로, 1은 시간 순서에"
+"서 저널에서 발견된 처음 부팅을 의미하고, 2는 두 번째 부팅 등을 의미합니다; 반"
+"면 -0은 마지막 부팅이고, -1은 마지막 부팅 전의 부팅입니다."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"이는 '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' 와 'UNIT=' 을 위한 일치를 추가하여 제"
+"공된 단위를 위해 모든 가능한 메시지를 찾습니다. 쉼표에 의해 구분된 더 많은 단"
+"위를 포함할 수 있습니다. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "이를 통해 나중에 비밀번호 없이 로그인 할 수 있습니다."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"이 영역에는 Cockpit 서비스가 포함되어 있습니다. 이 영역이 현재 웹 콘솔 연결"
+"에 적용되지 않는지 확인하십시오."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "목요일"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "계층"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "시간"
+
+# ctx::sourcefile::/rhn/account/LocalePreferences
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "시간대"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "타이머 생성에 실패했습니다"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "타이머 삭제가 실패했습니다"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "타이머"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"도움말: 다른 시스템에 자동으로 인증하려면 키 비밀번호가 로그인 비밀번호와 일"
+"치해야 합니다."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"악성의 제3자가 귀하의 연결을 가로채지 않도록 하려면 호스트 키 지문을 확인하십"
+"시오:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"소프트웨어 최신화 하려면, 이 시스템은 Red Hat 고객 포털이나 로컬 서브스크립"
+"션 서버를 사용하여 Red Hat에 등록되어 있어야 합니다."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"지문을 확인하려면 물리적인 장치 또는 신뢰할 수 있는 네트워크를 통해 $0에서 다"
+"음을 실행합니다:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "오늘"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "전환"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "날짜 선택기 전환"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "필터 전환"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "데이터가 너무 많습니다"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "도구"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "상위 5개의 CPU 서비스"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "상위 5개 디스크 사용 서비스"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "상위 5개의 메모리 서비스"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "전체 크기: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "타워"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "과도현상"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "전송 중"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "트리거"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "트리거 대상"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "트리거"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "문제 해결"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "문제 해결…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "호스트 신뢰 및 추가"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "신뢰 키"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "신뢰 단계"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "다시 시도"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "$0와 동기화를 시도 중입니다"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "화요일"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned 시작에 실패했습니다"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned는 시스템을 모니터링하고 특정 워크로드에 대해 성능을 최적화하는 서비스입"
+"니다. Tuned의 핵심은 다른 사용 사례에 맞게 시스템을 조정하는 프로파일입니다."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned를 사용 할 수 없습니다"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned가 동작되지 않음"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned이 종료되어 있습니다"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "관리자 접근 설정"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "유형"
+
+#: pkg/storaged/partitions/partition.jsx:165
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "유형은 문자 0에서 9까지, A에서 F, 그리고 \"-\"만을 포함합니다."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr "유형은 형식 NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN이어야 합니다."
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+"유형은 정확하게 2가지 16진수 문자(0에서 9, A에서 F)가 포함되어야 합니다."
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "필터 입력"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "이와 같은 해결책을 자동으로 적용 할 수 없습니다"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "해당 주소에 연결 할 수 없음"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "$0를 연락 할 수 없음."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"지정된 호스트 $0에 연결할 수 없습니다. 포트 $1에서 ssh가 실행되고 있는지 확인"
+"하거나 주소에서 다른 포트를 지정합니다."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "알림 세부 정보를 가져올 수 없습니다."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"SSH 키 인증을 사용하여 $0에 로그인 할 수 없습니다. 비밀번호를 입력하세요. 자"
+"동 로그인을 위해 자신의 SSH 키를 설정 할 수 있습니다."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"$0에 로그인 할 수 없습니다. 호스트는 비밀번호 로그인 또는 자신의 SSH 키를 허"
+"용하지 않습니다."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "서버에 연결 할 수 없습니다"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "적재를 제거 할 수 없습니다"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+msgid "Unable to repair logical volume $0"
+msgstr "논리 볼륨 $0를 복구 할 수 없음"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "수정 프로그램을 실행할 수 없습니다: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "설정을 저장 할 수 없음"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "설정을 저장 할 수 없음: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "setroubleshootd를 시작 할 수 없습니다"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "파일 시스템 적재를 해제 할 수 없습니다"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "사용 불가능"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "사용 할 수 없는 꾸러미"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "실행 취소"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "$0 설치 중에 예상치 못한 PackageKit 오류: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "예상치 못한 오류"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+msgid "Unformatted data"
+msgstr "초기화 되지 않은 자료"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "단위"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "알 수 없음"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "알 수 없는 \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "알 수 없음 ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "알 수 없는 응용프로그램"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "알 수 없는 설정"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "알 수 없는 호스트 이름"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "알 수 없는 서비스 이름"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "알 수 없는 유형"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "잠금 해제"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "부팅 시 자동으로 잠금 해제"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr "크기 조정전 잠금해제"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "암호화된 스트라티스 풀 잠금 해제"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "키 $0 잠금 해제"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "$target 잠금 해제 중"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "디스크 잠금 해제 중"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "관리하지 않는 연결장치"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "적재 해제"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "$0 파일 시스템 적재 해제"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "지금 적재 해제"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "$target 적재 해제 중"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "이름 없음"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "고정해제 단위"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "인식되지 않는 데이터"
+
+#: pkg/storaged/block/resize.jsx:306
+msgid "Unrecognized data can not be made smaller here"
+msgstr "여기에서는 인식되지 않은 자료를 작게 할 수 없습니다"
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "여기에서는 인식되지 않은 데이터를 작게할 수 없습니다."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+msgid "Unsupported logical volume"
+msgstr "미지원 논리 볼륨"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "까지"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "지원되지 않는 호스트"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "최신화"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "최신화 성공 테이블"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "최신화 내역"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "꾸러미 정보 최신화"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "최신화에 성공했습니다"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "최신화됨"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr "최신화된 꾸러미를 적용하려면 시스템을 재시작해야 합니다."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "사용 가능한 최신화"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "최신화 내역"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "최신화는 $1에서 $0에 적용됩니다"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "최신화 중"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "상태 최신화 중 ..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "가동 시간"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "사용량"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "$0 사용량"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "사용"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "압축 사용"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "중복 제거 사용"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "키 사용"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "비밀번호 사용"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "다른 시스템에 대해 인증하려면 다음 키를 사용합니다"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "상세한 로그인 사용"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "사용됨"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr "선택적이거나 상호 작용이 필요한 적재에 유용함 (암호문과 같은 경우)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "사용자"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "사용자 ID"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "사용자 ID는 이미 다른 사용자에 의해 사용되고 있습니다"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "사용자 ID는 양의 정수이어야 합니다"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "사용자 ID는 $0보다 크지 않아야만 합니다"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "사용자 ID는 $0보다 작지 안아야만 합니다"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "사용자 이름"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "사용자 이름을 입력하셔야 합니다"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "사용자 이름"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "LUKS 암호화 사용 중"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Tang 서버 사용 중"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "VDO 백업 장치를 작게 할 수 없습니다"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO 장치 $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "VDO 파일 시스템 볼륨 (압축/중복제거)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN ID"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "주소 확인"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "인증 토큰 확인"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "제조사"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "확인"
+
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "지문 검증"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "키 확인"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "확인 중"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "버전"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "$target을 매우 안전하게 삭제"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "모든 CPU 보기"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "모든 디스크 보기"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "모든 기록 보기"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "모든 서비스 보기"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "자동 스크립트 보기"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "상세한 기록 보기"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "하드웨어 세부 사항보기"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "로그인 내역 보기"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "기록 보기"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "측정 항목과 내역 보기"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "디스크-당 처리량 보기"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "프로젝트 웹사이트 보기"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "보고서 보기"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "최신화 기록 보기"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "메모리 정보 보기는 관리자 접근 권한이 필요합니다."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "방화벽 방문"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "볼륨 그룹"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+msgid "Volume group is missing physical volumes"
+msgstr "볼륨 그룹은 물리 볼륨에서 누락되었습니다"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "볼륨 크기는 $0입니다. 내용 크기는 $1입니다."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "대기 중입니다"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "상세 정보 대기 중..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "입력 대기 중…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"꾸러미 관리자를 사용 중이어서 다른 프로그램이 종료 할 때까지 대기 중..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "다른 소프트웨어 관리 작업이 완료될 때 까지 대기 중"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "시작을 기다리는 중…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "필요한 대상"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "필요"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "경고 이상의 수준"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+msgid "Weak password"
+msgstr "취약한 비밀번호"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "웹 콘솔"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "리눅스 서버를 위한 웹콘솔"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "웹 콘솔이 재시작됩니다"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "웹 콘솔이 제한된 접근 방식으로 실행 중입니다."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "웹 콘솔 로고"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "수요일"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "매주"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "주"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "날짜"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "비어 있는 경우 현재 사용자와 연결합니다"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"웹 콘솔이 다시 시작되면 더 이상 진행률 정보가 표시되지 않습니다. 아무튼, 최신"
+"화 처리는 백그라운드에서 계속됩니다. 최신화 처리를 계속 확인하려면 다시 연결"
+"하세요."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"이 옵션을 선택하면 새 풀에서 오버프로비저닝을 허용하지 않습니다. 풀에 생성되"
+"는 각 파일 시스템의 최대 크기를 지정해야 합니다. 파일 시스템을 생성한 후에는 "
+"더 크게 만들 수 없습니다. 순간찍기는 생성 시 완전히 할당됩니다. 모든 최대 크"
+"기의 합은 풀 크기를 초과 할 수 없습니다. 이러한 기능의 장점은 풀의 파일 시스"
+"템에서 공간이 부족해질 수 없다는 것입니다. 단점은 각 파일 시스템의 최대 크기"
+"를 미리 알아야 하고 순간찍기 생성이 제한된다는 점입니다."
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "흰색"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "\"자동\"으로 설정됩니다"
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "와이어가드"
+
+#: pkg/storaged/drive/drive.jsx:124
+msgid "World wide name"
+msgstr "고유 식별자"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "쓰기"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "대부분 쓰기"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "쓰기"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "잘못된 사용자 이름 또는 비밀번호"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "매년"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "네"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "처음으로 $0에 연결됩니다."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "방화벽을 수정할 수있는 권한이 없습니다."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr "이 계정에 승인된 공개 키를 표시할 수 있는 권한이 없습니다."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "비활성 상태로 인해 로그 아웃되었습니다."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "오래된 항목이 로드될 수 있습니다."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr "자동 로그인을 위해 키 비밀번호를 변경 할 수 있습니다."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "비밀번호 변경을 위해 조금 더 기다려 주십시오"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr "메트릭을 기록을 보려면 다시 로그인해야 합니다"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "이제 관리자 접근 권한이 있습니다."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "$0 초 후에 로그 아웃됩니다."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "사용자 계정"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"당신의 검색기는 내용 메뉴에서 붙여넣기를 허용하지 않습니다. Shift+Insert를 사"
+"용 할 수 있습니다."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "당신의 검색기는 세션을 통해 자신의 접근 수준을 기억합니다."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr "서버 연결이 종료됩니다. 다시 시작하여 연결을 재개할 수 있습니다."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "세션이 종료되었습니다."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "세션이 만료되었습니다. 다시 로그인하십시오."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "영역"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[바이너리 데이터]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[데이터 없음]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "접근"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "활성"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "add-on"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "부가 기능"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "네트워크 이후에"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "응용프로그램"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "자산 태그"
+
+# translation auto-copied from project Customer Portal Translations, version
+# PortalCaseManagementStrings, document PortalCaseManagementStrings, author
+# eukim
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "시간"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "평균: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "VDO 장치용 백업 장치"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "본드"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "부팅"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "브릿지"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+msgid "btrfs device"
+msgstr "btrfs 장치"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+msgid "btrfs devices"
+msgstr "btrfs 장치"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+msgid "btrfs subvolume"
+msgstr "btrfs 하위볼륨"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr "btrfs 하위 볼륨 $0/$1"
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+msgid "btrfs subvolumes"
+msgstr "btrfs 하위볼륨"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+msgid "btrfs volume"
+msgstr "btrfs 볼륨"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "버그 수정"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "바이트"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+msgid "cache"
+msgstr "캐쉬"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "변경"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "Cockpit-podman이 설치되어 있지 않습니다"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "명령"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "콘솔"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "코어 덤프"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "중앙처리장치"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "충돌"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+msgid "data"
+msgstr "자료"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "날짜"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+msgid "deactivate"
+msgstr "비활성화"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "디버그"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "삭제"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr "btrfs 볼륨 장치"
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "비활성화"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "디스크"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "디스크"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "도메인"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "드라이브"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "편집"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "활성화"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+msgid "encrypted"
+msgstr "암호화됨"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "암호화"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "기능 개선"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "오류"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "매주 금요일"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "매주 월요일"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "매주 토요일"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "매주 일요일"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "매주 목요일"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "매주 화요일"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "매주 수요일"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "매일"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "확장"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "ssh 호스트 키를 나열하지 못했습니다: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "파일 시스템"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "방화벽"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "현재와 미래의 커널을 위하여"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "현재 커널만"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "포맷"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "$0에서"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "$1의 $0에서"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "그래프"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "확장"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "하드웨어"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "내역"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "호스트"
+
+#: pkg/storaged/drive/drive.jsx:65
+msgid "iSCSI Drive"
+msgstr "iSCSI 드라이브"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+msgid "iSCSI drives"
+msgstr "iSCSI 드라이브"
+
+#: pkg/storaged/iscsi/session.jsx:45
+msgid "iSCSI portal"
+msgstr "iSCSI 포털"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "실패를 무시"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "대부분의 브라우저에서"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "일관성 없음"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "초기화"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "설치"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "연결장치"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "유효하지 않음: 다중 대상이 정의됨"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "저널"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "journalctl man 부분"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "K덤프"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "K덤프 상태"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "키"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "로그인"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "LUKS"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "장치"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "마스크"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "최대: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+msgid "member of MDRAID device"
+msgstr "MD레이드 장치의 구성원"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "스트라티스 풀의 구성원"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "메모리"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "메트릭"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "완화"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "더 자세히"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "적재"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "네트워크"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "부팅시에 적재하지 않습니다"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "nfs export가 비어 있습니다"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "nfs 서버가 비어 있습니다"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "nfs 서버는 유효한 IPv6가 아닙니다"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "우선순위"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "없음"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "$0 CPU"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "$0에서"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "운영 체제"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "os"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "꾸러미"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "파티션"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "비밀번호"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "비밀번호 수준"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "패치"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "경로"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "성능"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "LVM2 볼륨 그룹의 물리적인 볼륨"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "플러그인"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service가 실패했습니다"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service는 자료 수집에 실패하였습니다"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service가 동작 하지 않습니다"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "포드"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "포트"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "전원"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "레이드"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "램"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "읽기 전용"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "권장 사항"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "LVM2에서 제거"
+
+#: pkg/storaged/utils.js:934
+msgid "remove from MDRAID"
+msgstr "MD레이드에서 제거"
+
+#: pkg/storaged/utils.js:904
+msgid "remove from btrfs volume"
+msgstr "btrfs 볼륨에서 제거"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "재시작"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "역할"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "$0 실행 중"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "보안"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "시리얼"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "서비스"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "쉘"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "덜 보기"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "더 보기"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "축소"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "종료"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "소켓"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "해결책"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "해결 상세 정보"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "sos 보고에 실패했습니다"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ssh 키는 경로가 없습니다"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "ssh 서버가 비어 있습니다"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "중지"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "실패한 경우에 부트 중지"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+msgid "stopped"
+msgstr "정지됨"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "대상"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "팀"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "시간"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "타이머"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "단위"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "알 수 없음"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "알 수 없는 대상"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "마스크 해제"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "적재 해제"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "$0에서 비파티션 공간"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "사용자"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "사용자 이름"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "키 설명 $0를 사용하기"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "버전"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "볼륨"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "경고"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "wireguard-tools 꾸러미는 이 설치되지 않았습니다"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "예"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "영역"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Kdump 서비스가 설치되어 있지 않습니다. kexec-tools 꾸러미가 설치를 확인하"
+#~ "십시오."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "메모리가 예약되어 있지 않습니다. crashkernel 옵션을 커맨드라인에 추가하고 "
+#~ "(예: /etc/default/grub) 시작시 메모리를 예약합니다. 예: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "서비스 실행 중입니다"
+
+#~ msgid "Service is starting"
+#~ msgstr "서비스가 재시동되고 있습니다"
+
+#~ msgid "Service is stopped"
+#~ msgstr "서비스가 중지되었습니다"
+
+#~ msgid "Service is stopping"
+#~ msgstr "서비스가 중지되고 있습니다"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "커널 충돌에 인해 kdump 구성을 시험할 것입니다."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "깨진 커널이 커널 명령 행에서 구성되지 않음"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (암호화됨)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 스트라티스 풀"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 캐쉬"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 알 수 없는 계층"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 여유공간"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "디스크를 제거하기 전 예비용 디스크를 추가해야 합니다."
+
+#~ msgid "Backing device"
+#~ msgstr "백업 장치"
+
+#~ msgid "Block"
+#~ msgstr "블록"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "용량"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgid "Content"
+#~ msgstr "컨텐츠"
+
+#~ msgid "Create devices"
+#~ msgstr "장치 만들기"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "장치"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "장치 파일"
+
+#~ msgid "Devices"
+#~ msgstr "장치"
+
+#~ msgid "Drives"
+#~ msgstr "드라이브"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr "암호화된 볼륨 크기를 변경하기 전에 잠금을 해제해야 합니다."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "파일 시스템 (암호화됨)"
+
+#~ msgid "Filesystems"
+#~ msgstr "파일 시스템"
+
+#~ msgid "Free"
+#~ msgstr "남은 양"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "이 그룹에서 여유 공간을 확보합니다: 다른 논리 볼륨을 축소 또는 제거하거나 "
+#~ "신규 물리 볼륨을 추가하십시오."
+
+#~ msgid "Inactive volume"
+#~ msgstr "비활성 볼륨"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "키 서버"
+
+#~ msgid "LVM2 member"
+#~ msgstr "LVM2 구성원"
+
+#~ msgid "Locked devices"
+#~ msgstr "장치 잠금"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "암호화된 자료 잠김"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "모델"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS 적재"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "NFS 지원이 설치되어 있지 않습니다"
+
+#~ msgid ""
+#~ "New logical volumes can not be created while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "신규 논리 볼륨은 볼륨 그룹이 물리적 볼륨이 누락되었을 때에 제거 될 수 없습"
+#~ "니다."
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "NFS 적재 설정 없음"
+
+#~ msgid "No devices"
+#~ msgstr "장치 없음"
+
+#~ msgid "No drives attached"
+#~ msgstr "드라이브가 연결되어 있지 않습니다"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "iSCSI 대상이 설정되어 있지 않습니다"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "새 파일 시스템을 위한 공간이 부족합니다"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "기타 데이터"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "파티션된 블록 장치"
+
+# translation auto-copied from project Anaconda, version master, document
+# anaconda
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "암호문"
+
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "물리 볼륨은 볼륨 그룹이 물리적 볼륨이 누락되었을 때에 제거 될 수 없습니다."
+
+#~ msgid "Pool"
+#~ msgstr "풀"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "씬 볼륨을 위한 풀"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "레이드 수준"
+
+#~ msgid "RAID member"
+#~ msgstr "레이드 구성요소"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "삭제 가능한 드라이브"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "$0 장치 표시"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "$0 드라이브 표시"
+
+#~ msgid "Show all"
+#~ msgstr "모두 표시"
+
+#~ msgid "Source"
+#~ msgstr "원천"
+
+#~ msgid "Start pool"
+#~ msgstr "풀 시작"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "파일 시스템을 보려면 풀을 시작합니다."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "상태"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "중단된 스트라티스 풀"
+
+#~ msgid "Stratis member"
+#~ msgstr "스트라티스 구성원"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "스트라티스 풀 $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "지원이 설치되어 있습니다."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr "예비 디스크를 추가하려면 레이드 장치가 실행되고 있어야 합니다."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "레이드 장치의 마지막 디스크는 제거 할 수 없습니다."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "볼륨 그룹의 마지막 물리 볼륨은 제거 할 수 없습니다."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "이 물리 볼륨을 제거하는데 필요한 공간이 충분하지 않습니다. 최소 $0의 여유 "
+#~ "공간이 필요합니다."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "디스크는 장치를 복구하는 동안 제거 할 수 없습니다."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr "볼륨 크기를 변경하기 전에 활성화해야 합니다."
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "인식되지 않는 데이터"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "사용량"
+
+#~ msgid "Used for"
+#~ msgstr "사용 대상"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "VDO 백업"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO 장치"
+
+#~ msgid "Volume"
+#~ msgstr "볼륨"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "자동(DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "키 수락 및 연결"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "여기에서는 암호화된 볼륨 크기를 변경할 수 없습니다."
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Tang 키 서버 사용"
+
+#~ msgid "Use a passphrase"
+#~ msgstr "암호문 사용"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "비트맵"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "이 풀은 키 설명이 예상 형식이 아니기 때문에 여기에서 잠금 해제 할 수 없습"
+#~ "니다."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "비트맵 전환"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr "Tang 서버가 다음 중 하나에서 키 해시가 일치하는지 확인합니다:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "수동으로 SSH로 확인합니다. "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "포트 번호 및 유형이 일치하지 않습니다"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "암호화된 스트라티스 풀 잠김"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "알림을 삭제하는 동안 오류가 발생했습니다: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "알림을 받을 수 없음: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[바이너리 데이터의 $0 바이트]"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#~ msgid "Disk I/O spike"
+#~ msgstr "디스크 I/O 순간 사용량"
+
+#~ msgid "Event"
+#~ msgstr "사건"
+
+#~ msgid "Event logs"
+#~ msgstr "사건 기록"
+
+#~ msgid "Load spike"
+#~ msgstr "적재 순간 사용량"
+
+#~ msgid "Memory spike"
+#~ msgstr "메모리 순간 사용량"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "네트워크 I/O 순간 사용량"
+
+#~ msgid "ssh key"
+#~ msgstr "ssh 키"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "만약 선택이 확인 되었으면, 파일 시스템은 심지어 이전에 적재되어져 있을지라"
+#~ "도 다음 부팅 중에 적재되지 않을 것입니다. 이는 부팅 중에 적재가 불가능 "
+#~ "할 때에 유용 하고, 예를 들어 암호가 파일 시스템을 잠금 해제 할 때에 필요하"
+#~ "지만 부팅은 사용되지 않습니다."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "부팅시 적재되지 않음"
+
+#~ msgid "Listing unit files"
+#~ msgstr "단위 파일 나열하기"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "장치 파일 나열에 실패했습니다. $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "찾을 수 없음"
+
+#~ msgid "Validating key"
+#~ msgstr "키 확인"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "$0 그룹 삭제"
+
+#~ msgid "Container administrator"
+#~ msgstr "컨테이너 관리자"
+
+#~ msgid "Image builder"
+#~ msgstr "이미지 빌더"
+
+# auto translated by TM merge from project: RHEV Administration Guide,
+# version: 4.0, DocId: chap-
+# Administering_and_Maintaining_the_Red_Hat_Enterprise_Virtualization_Environment
+#~ msgid "Roles"
+#~ msgstr "역할"
+
+#~ msgid "Server administrator"
+#~ msgstr "서버 관리자"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "유닉스 그룹: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "키보드에 성공적으로 복사되었습니다"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "진단 보고서"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "커널 덤프"
+
+#~ msgid "Software Updates"
+#~ msgstr "소프트웨어 최신화"
+
+#~ msgid "Update log"
+#~ msgstr "최신화 기록"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "nfs 덤프 대상이 server:path 형식으로 되어 있지 않습니다"
+
+#~ msgid "$0 Zone"
+#~ msgstr "$0 영역"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "진단 보고서 작성"
+
+#~ msgid "Done!"
+#~ msgstr "완료!"
+
+#~ msgid "Download report"
+#~ msgstr "보고 내려받기"
+
+#~ msgid "No archive has been created."
+#~ msgstr "생성된 아카이브가 없습니다."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "$0 삭제를 확인해주세요"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "생성된 저장소에는 기밀로 간주되는 자료가 포함되어 있으며 해당 내용은 제 3"
+#~ "자에게 전달되기 전에 원래 조직에서 검토해야 합니다."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "이 도구은 시스템 문제 진단에 사용하기 위해 시스템에서 시스템 설정 및 진단 "
+#~ "정보를 수집합니다."
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "서버는 cockpit-system 꾸러미가 누락되었습니다"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "이 꾸러미는 Cockpit 버전과 호환되지 않습니다"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "이 꾸러미에는 Cockpit 버전 %s 이상이 필요합니다"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "성능 메트릭"
+
+#~ msgid "Reboot to apply new crypto policy"
+#~ msgstr "신규 암호와 정책을 적용하기 위해 재시작합니다"
+
+#~ msgid "Save only"
+#~ msgstr "저장만"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "총계 $0 GiB"
+
+#~ msgid "Apply"
+#~ msgstr "적용"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "$0 영역을 제거할 권한이 없습니다"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "비밀번호를 어떻게든 사용하려면 $0을 다시 눌러주세요."
+
+#~ msgid "Access"
+#~ msgstr "액세스"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "디스크에 오류 발생"
+
+#~ msgid "Logical Size"
+#~ msgstr "논리 크기"
+
+#~ msgid "Security updates "
+#~ msgstr "보안 최신화 "
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgid "Updates "
+#~ msgstr "업데이트 "
+
+#~ msgid "(Optional)"
+#~ msgstr "(옵션)"
+
+#~ msgid "Active since"
+#~ msgstr "이후 활성화"
+
+#~ msgid "Affected locations"
+#~ msgstr "관련 위치"
+
+#~ msgid "Process"
+#~ msgstr "프로세스"
+
+#~ msgid "Remove and delete"
+#~ msgstr "제거와 삭제"
+
+#~ msgid "Remove and format"
+#~ msgstr "제거와 형식"
+
+#~ msgid "Remove and grow"
+#~ msgstr "제거와 성장"
+
+#~ msgid "Remove and initialize"
+#~ msgstr "제거와 초기화"
+
+#~ msgid "Remove and shrink"
+#~ msgstr "제거와 축소"
+
+#~ msgid "Remove and stop device"
+#~ msgstr "장치 제거와 사용하지 않기"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "파일 시스템은 로그인 세션 및 시스템 서비스에 사용되고 있습니다. 계속 진행"
+#~ "하면 이러한 작업이 중지됩니다."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "파일 시스템은 로그인 세션에서 사용되고 있습니다. 계속 진행하면 이러한 작업"
+#~ "이 중지됩니다."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "파일 시스템은 시스템 서비스에서 사용되고 있습니다. 계속 진행하면 이러한 작"
+#~ "업이 중지됩니다."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "이 장치에는 현재 사용 중인 파일 시스템이 있습니다. 계속 진행하면 이 장치"
+#~ "의 파일 시스템을 모두 마운트 해제하게 됩니다."
+
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "이 장치는 현재 LVM2 볼륨 그룹을 위해 사용되고 있습니다."
+
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "이 장치는 현재 LVM2 볼륨 그룹을 위해 사용되고 있습니다. 계속 진행하면 볼"
+#~ "륨 그룹에서 이 장치가 제거됩니다."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "현재 이 장치는 RAID 장치로 사용되고 있습니다."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "현재 이 장치는 RAID 장치로 사용되고 있습니다. 계속 진행하면 RAID 장치에서 "
+#~ "이 장치가 제거됩니다."
+
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "이 장치는 현재 스트라티스 풀을 위해 사용되고 있습니다."
+
+#~ msgid "Unmount and delete"
+#~ msgstr "내려놓기와 삭제"
+
+#~ msgid "Unmount and format"
+#~ msgstr "내려 놓기와 초기화"
+
+#~ msgid "Unmount and grow"
+#~ msgstr "내려놓기와 성장"
+
+#~ msgid "Unmount and initialize"
+#~ msgstr "내려 놓기와 초기화"
+
+#~ msgid "Unmount and shrink"
+#~ msgstr "내려 놓기와 축소"
+
+#~ msgid "Unmount and stop device"
+#~ msgstr "장치 내려놓기와 사용하지 않기"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 / $1"
+
+#~ msgid "Blockdev"
+#~ msgstr "블록장치"
+
+#~ msgid "Blockdev of Stratis pool $0"
+#~ msgstr "스트라티스 풀 $0의 블록장치"
+
+#~ msgid "Blockdev of locked Stratis pool $0"
+#~ msgstr "잠김 스트라티스 풀 S0의 블록장치"
+
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "$0의 LVM2 물리 볼륨"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "RAID 장치 $0 멤버"
+
+#~ msgid "Menu"
+#~ msgstr "메뉴"
+
+#~ msgid "VDO backing"
+#~ msgstr "VDO 백업"
+
+#~ msgid "Create Stratis Pool"
+#~ msgstr "스트라티스 풀 생성"
+
+#~ msgid "Mount Options"
+#~ msgstr "적재 옵션"
+
+#~ msgid "Stratis Pool"
+#~ msgstr "스트라티스 풀"
+
+#~ msgid "A disk is needed."
+#~ msgstr "디스크가 필요합니다."
+
+#~ msgid "Disk"
+#~ msgstr "디스크"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "기존 응용프로그램 전용입니다. 성능이 저하됩니다."
+
+#~ msgid "Install VDO support"
+#~ msgstr "VDO 지원 설치"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "512 바이트 에뮬레이션 사용"
+
+#~ msgid "System services"
+#~ msgstr "시스템 서비스"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "진단 보고서 만들기 "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr "{{ limit }} 대 이상의 컴퓨터에 동시 연결을 지원하지 않습니다."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "Kerberos 기반 SSO"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "{{host}}에 로그인"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "신규 키 비밀번호가 일치하지 않습니다"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "지문을 확인하려면 물리적인 장치 또는 신뢰할 수 있는 네트워크를 통해 "
+#~ "{{host}}에서 다음을 실행합니다:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "{{#strong}}{{host}}{{/strong}}에 연결할 수 없습니다."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}}에 로그인 할 수 없습니다. 다른 인증 선택 및 "
+#~ "문제 해결에 대한 지원이 필요한 경우 cockpit-ws를 신규 버전으로 업그레이드"
+#~ "하세요."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}}에 로그인할 수 없습니다. 이 호스트에 연결하"
+#~ "려면 {{#strong}}{{host}}{{/strong}}의 sshd 구성에서 다음 인증 방법 중 하나"
+#~ "를 사용해야 합니다:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "처음으로 {{host}}에 연결됩니다."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "{{host}} 키 변경됩니다"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "적재 지점 설정 지우기"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "이 본드를 생성하면 서버와의 연결이 끊어지고, 관리 UI를 사용 할 수 없게 됩"
+#~ "니다."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "이 브릿지를 생성하면 서버와의 연결이 끊어지고, 관리 UI를 사용 할 수 없게 "
+#~ "됩니다."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "이 팀을 생성하면 서버와의 연결이 끊어지고, 관리 UI를 사용 할 수 없게 됩니"
+#~ "다."
+
+#~ msgid "IP settings"
+#~ msgstr "IP 설정"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "<b>$0</b>를 끄면 서버와의 연결이 끊어지고 관리 UI를 사용 할 수 없게 됩니"
+#~ "다."
+
+#~ msgid "Administrator password"
+#~ msgstr "관리자 비밀번호"
+
+#~ msgid "Computer OU"
+#~ msgstr "컴퓨터 OU"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "RAID 장치를 제거하면 내부의 모든 데이터를 지우게 됩니다."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "볼륨 그룹을 삭제하면 내부의 모든 자료도 함께 삭제됩니다."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "기존 자료를 덮어쓰지 않습니다"
+
+#~ msgid "Erase"
+#~ msgstr "제거"
+
+#~ msgid "Format disk $0"
+#~ msgstr "디스크 $0 포멧"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr "장치를 포멧하면 내부의 모든 데이터를 지우게 됩니다."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "도메인에서 호스트 이름을 변경 할 수 없습니다"
+
+#~ msgid "More"
+#~ msgstr "이상"
+
+#~ msgid "Not joined"
+#~ msgstr "미가입"
+
+#~ msgid "One time password"
+#~ msgstr "일회용 비밀번호"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$2에 $1에서 $0"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "시간은 0에서 23 사이의 숫자 여야합니다"
+
+#~ msgid "Last login:"
+#~ msgstr "마지막 로그인:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "분은 0에서 59 사이의 숫자이어야합니다"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] "마지막으로 로그인한 후 $0의 로그인 시도가 실패했습니다."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "예: \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 활성 영역"
+
+#~ msgid "Dismiss $0 alerts"
+#~ msgstr "$0 알림 해제"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 발생"
+
+#~ msgid "Licensed under:"
+#~ msgstr "저작권 보유자:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "부팅 시 잠금 해제"
+
+#~ msgid "Unlock read only"
+#~ msgstr "읽기 전용 잠금 해제"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "결합된 조건을 사용하여 로그를 검색하십시오:"
+
+#~ msgid "Show filters"
+#~ msgstr "필터 표시"
+
+#~ msgid "Text"
+#~ msgstr "텍스트"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "정규식으로 사용되는 자유 형식 문자열"
+
+#~ msgid "e.g."
+#~ msgstr "예."
+
+#~ msgid "log fields"
+#~ msgstr "로그 필드"
+
+#~ msgid "qualifiers"
+#~ msgstr "qualifiers"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "세션 설정 전환"
+
+#~ msgid "(none)"
+#~ msgstr "(없음)"
+
+#~ msgid "Create timers"
+#~ msgstr "타이머 만들기"
+
+#~ msgid "Recommended default"
+#~ msgstr "기본 권장 사항"
+
+#~ msgid "Run"
+#~ msgstr "실행"
+
+#~ msgid "Select unit state"
+#~ msgstr "장치 상태 선택"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "저장된 항목 사용"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "저장된 항목 사용"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "$0에서 암호문을 강제 삭제합니다"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "tang-show-keys를 사용할 수 없는 경우 다음을 실행합니다:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "tang-show-keys를 사용할 수 없는 경우는 어떻게 합니까?"
+
+#~ msgid "key slot $0"
+#~ msgstr "키 슬롯 $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "문제가 발생했습니다"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "작동하지 않습니다. 다시 시도하십시오"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "관리 액세스 권한을 얻을 수 없습니다."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "자동 최신화가 설정되어 있지 않습니다"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "$0 CPU 구성"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 네트워크"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 대부분의 운영 체제에서 사용 할 수 있습니다. 설치하려면 그놈 소프트웨어"
+#~ "에서 검색하거나 다음을 실행하십시오:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 메모리 조정"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 네트워크"
+
+# auto translated by TM merge from project: RHEV Administration Guide,
+# version: 3.6-async1, DocId: chap-
+# Administering_and_Maintaining_the_Red_Hat_Enterprise_Virtualization_Environment
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPUs"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 vCPU 세부 정보"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 가상 네트워크 인터페이스 설정"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "저장소 풀을 활성화하여 볼륨 관리"
+
+#~ msgid "Add network interface"
+#~ msgstr "네트워크 인터페이스 추가"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "가상 네트워크 인터페이스 추가"
+
+#~ msgid "Additional"
+#~ msgstr "추가"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "서브넷 내에 없는 주소"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr "스냅샷을 삭제하면 캡처된 모든 컨텐츠가 손실됩니다."
+
+#~ msgid "Always attach"
+#~ msgstr "항상 연결"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr "연결하면 이 디스크를 사용하는 모든 VM에서 공유할 수 있습니다."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "부트시 자동으로 libvirt 시작"
+
+#~ msgid "Autostart"
+#~ msgstr "자동 시작"
+
+#~ msgid "Boot order"
+#~ msgstr "부팅 순서"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "부팅 순서 설정을 저장하지 못했습니다"
+
+#~ msgid "Bus"
+#~ msgstr "버스"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "CD/DVD 디스크"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "CPU 구성을 저장할 수 없습니다"
+
+#~ msgid "CPU type"
+#~ msgstr "CPU 유형"
+
+#~ msgid "Change firmware"
+#~ msgstr "펌웨어 변경"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "VM 종료 후 변경사항이 적용됩니다"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "운영 체제를 선택하십시오"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr "\"원격 표시기 시작\"을 눌러 .vv 파일을 내려받기해 $0를 시작합니다."
+
+#~ msgid "Clone"
+#~ msgstr "복제"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "이미지를 로드할 수 없습니다"
+
+#~ msgid "Confirm this action"
+#~ msgstr "작업 확인"
+
+#~ msgid "Connect"
+#~ msgstr "연결"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "다음 통신규약에 대해 표시기 응용프로그램에 연결합니다"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "가상화 서비스에 연결 중입니다"
+
+# ctx::sourcefile::Navigation Menu
+#~ msgid "Connection"
+#~ msgstr "연결"
+
+#~ msgid "Console"
+#~ msgstr "콘솔"
+
+#~ msgid "Cores per socket"
+#~ msgstr "소켓당 코어 수"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "스냅샷으로 전환할 수 없습니다"
+
+#~ msgid "Crashed"
+#~ msgstr "충돌"
+
+#~ msgid "Create VM"
+#~ msgstr "가상 머신 만들기"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "$0 기준 복제 VM 만들기"
+
+#~ msgid "Create new"
+#~ msgstr "새로 만들기"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "새로운 가상 머신 만들기"
+
+#~ msgid "Create virtual network"
+#~ msgstr "가상 네트워크 만들기"
+
+#~ msgid "Creating VM"
+#~ msgstr "VM 생성 중"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "VM 설치 생성 중"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "VM $0 생성에 실패했습니다"
+
+#~ msgid "Creation time"
+#~ msgstr "생성 시간"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "현재 할당"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "사용자 지정 펌웨어 : $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP 범위"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "연결된 저장소 파일 삭제:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "저장소 풀 $0 삭제"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "풀 내에 있는 볼륨을 삭제합니다"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "비활성화된 저장소 풀을 삭제하려면 풀 정의를 해제하면 됩니다. 내용은 삭제되"
+#~ "지 않습니다."
+
+#~ msgid "Desktop viewer"
+#~ msgstr "데스크탑 뷰어"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr "삭제를 시도하기 전에 VM에서 이 풀을 사용하여 디스크를 분리하십시오."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "직렬 콘솔에서 연결이 끊어졌습니다. 연결 버튼을 눌러주세요."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "디스크 $0 가 VM $1에서 분리되지 않음"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "디스크를 연결하지 못했습니다"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "디스크를 만들지 못했습니다"
+
+#~ msgid "Disk image file"
+#~ msgstr "디스크 이미지 파일"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "디스크 설정을 저장할 수 없습니다"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "디스크 전용 스냅샷"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "도메인이 충돌했습니다"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "리소스에서 도메인이 차단되었습니다"
+
+#~ msgid "Download an OS"
+#~ msgstr "OS 내려받기"
+
+#~ msgid "Dying"
+#~ msgstr "종료 중"
+
+#~ msgid "Emulated machine"
+#~ msgstr "에뮬레이트된 기기"
+
+#~ msgid "End"
+#~ msgstr "종료"
+
+#~ msgid "End should not be empty"
+#~ msgstr "끝은 비워둘 수 없습니다"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "호스트의 파일 시스템에서 기존 디스크 이미지"
+
+#~ msgid "Expand"
+#~ msgstr "확장"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "펌웨어를 변경하지 못했습니다"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "$0에 표시된 인터페이스의 IP 주소를 가져오지 못했습니다"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "Ctrl+Alt+$0키를 VM $1(으)로 보내지 못했습니다"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr "최대 가상 CPU 수 보다 적은 수를 활성화해야 합니다."
+
+#~ msgid "File"
+#~ msgstr "파일"
+
+#~ msgid "Filter by name"
+#~ msgstr "이름으로 필터링"
+
+#~ msgid "Firmware"
+#~ msgstr "펌웨어"
+
+#~ msgid "Force shut down"
+#~ msgstr "강제 종료"
+
+#~ msgid "Forward mode"
+#~ msgstr "포워드 모드"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "전송 방법"
+
+#~ msgid "Generate automatically"
+#~ msgstr "자동 생성"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "VM 목록으로 이동"
+
+#~ msgid "Hide additional options"
+#~ msgstr "추가 옵션 숨기기"
+
+#~ msgid "Host device"
+#~ msgstr "호스트 장치"
+
+#~ msgid "Host name"
+#~ msgstr "호스트 이름"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "호스트는 비워둘 수 없습니다"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "하이퍼 바이저 세부 사항"
+
+#~ msgid "IP configuration"
+#~ msgstr "IP 구성"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 및 IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4 네트워크"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv4 네트워크는 비워둘 수 없습니다"
+
+#~ msgid "IPv4 only"
+#~ msgstr "IPv4만"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6 주소"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6 네트워크"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6 네트워크는 비워둘 수 없습니다"
+
+#~ msgid "IPv6 only"
+#~ msgstr "IPv6 만"
+
+#~ msgid "Idle"
+#~ msgstr "유휴"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "VM을 바로 시작"
+
+#~ msgid "Import"
+#~ msgstr "가져오기"
+
+#~ msgid "Import VM"
+#~ msgstr "VM 가져 오기"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "가상 머신 가져 오기"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "대부분의 설정에서 macvtap는 호스트와 게스트 간 네트워크 통신에서 작동하지 "
+#~ "않습니다."
+
+#~ msgid "Initiator"
+#~ msgstr "개시자"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "개시자 IQN은 비워둘 수 없습니다"
+
+#~ msgid "Installation source"
+#~ msgstr "설치 소스"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "설치 소스는 비워둘 수 없습니다"
+
+#~ msgid "Installation type"
+#~ msgstr "설치 유형"
+
+#~ msgid "Interface type"
+#~ msgstr "인터페이스 유형"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "잘못된 IPv4 매스크 또는 프리픽스 길이"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "잘못된 IPv6 주소"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "잘못된 IPv6 프리픽스"
+
+#~ msgid "Invalid filename"
+#~ msgstr "잘못된 파일 이름"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "원격 뷰어 시작"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr "root 계정을 만들지 않으려면 암호를 비워 두십시오"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr "사용자 계정을 만들지 않으려면 암호를 비워 두십시오"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr "root 계정을 만들지 않으려면 암호를 비워 두십시오"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt가 호스트에 설치된 UEFI/OVMF 펌웨어 이미지를 감지하지 못했습니다"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt 또는 하이퍼바이저는 UEFI를 지원하지 않습니다"
+
+#~ msgid "Loading resources"
+#~ msgstr "리소스 로딩 중"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "버스 유형을 변경하기 전에 장치를 종료해야 합니다"
+
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "캐쉬 방법을 변경하기 전에 장치를 종료해야 합니다"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "가상 머신 관리"
+
+#~ msgid "Manual connection"
+#~ msgstr "수동 연결"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "마스크 또는 프리픽스 길이"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "마스크 또는 프리픽스 길이는 비워둘 수 없습니다"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "최대 할당"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "최대 메모리는 저장 할 수 없습니다"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr "게스트 OS에 할당된 가상 CPU의 최대 개수"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr "게스트 OS에 할당된 가상 CPU의 최대 개수는 1에서 $0 사이여야 합니다"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "MTU(Maximum Transmission Unit)"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "메모리를 저장 할 수 없습니다"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "메모리는 0이 아니어야 합니다"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "모델 유형"
+
+#~ msgid "NAT to $0"
+#~ msgstr "$0에 NAT"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "VM $1의 NIC $0은 상태 변경에 실패했습니다"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "이름에 잘못된 문자가 있습니다"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "이름을 입력하셔야 합니다"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "이름을 입력해야 합니다"
+
+#~ msgid "Name: "
+#~ msgstr "이름: "
+
+#~ msgid "Netmask"
+#~ msgstr "넷마스크"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "네트워크 $0의 활성화에 실패했습니다"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "네트워크 $0의 비활성화에 실패했습니다"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "네트워크 부팅(PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "네트워크 파일 시스템"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "네트워크 인터페이스 설정을 저장하지 못했습니다"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "네트워크 선택은 PXE를 지원하지 않습니다."
+
+#~ msgid "Networks"
+#~ msgstr "네트워크"
+
+#~ msgid "New volume name"
+#~ msgstr "새 볼륨 이름"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "이 호스트에서 실행 중이거나 정의된 가상 머신이 없습니다"
+
+#~ msgid "No connection available"
+#~ msgstr "사용 가능한 연결 없음"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "이 가상 머신에 지정된 디스크가 없습니다"
+
+#~ msgid "No network devices"
+#~ msgstr "네트워크 장치 없음"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "이 가상 머신에 네트워크 연결장치가 정의되지 않았습니다"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "이 호스트에 정의된 네트워크가 없습니다"
+
+#~ msgid "No networks available"
+#~ msgstr "사용 가능한 네트워크가 없습니다"
+
+#~ msgid "No parent"
+#~ msgstr "부모 개체 없음"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "이 VM에 대해 정의된 스냅샷 없음"
+
+#~ msgid "No state"
+#~ msgstr "상태 없음"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "이 호스트에 지정된 저장소 풀이 없습니다"
+
+#~ msgid "No storage pools available"
+#~ msgstr "사용 가능한 저장소 풀 없음"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "이 저장소 풀에 대해 정의된 저장소 볼륨 없음"
+
+#~ msgid "No virtual networks"
+#~ msgstr "가상 네트워크 없음"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "비영구적 네트워크는 삭제할 수 없습니다. 비활성화되면 존재하지 않습니다."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "비영구적 저장소 풀은 삭제 할 수 없습니다. 비활성화 되면 존재하지 않습니다."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "없음(격리된 네트워크)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "하나 이상의 선택된 볼륨이 도메인에서 사용되고 있습니다. 먼저 디스크를 분리"
+#~ "하여 볼륨을 삭제하십시오."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "게스트가 종료된 경우에만 편집 할 수 있습니다"
+
+#~ msgid "Open"
+#~ msgstr "열기"
+
+#~ msgid "Operating system"
+#~ msgstr "운영 체제"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "실행 중인 작업"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "부모 스냅샷"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "호스트 파일시스템의 경로"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "호스트의 파일 시스템에서 ISO 파일로의 경로"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "호스트 파일 시스템에서 파일의 경로"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "호스트 파일 시스템에서 파일의 경로"
+
+#~ msgid "Paused"
+#~ msgstr "일시정지"
+
+#~ msgid "Persistence"
+#~ msgstr "고집"
+
+#~ msgid "Physical disk device"
+#~ msgstr "물리적 디스크 장치"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "호스트에서 물리적 디스크 장치"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "저장소 풀을 선택하십시오"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "볼륨을 선택하십시오"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "새 볼륨 이름을 입력해 주십시오"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "가상 머신을 시작하여 콘솔에 액세스하십시오."
+
+#~ msgid "Plug"
+#~ msgstr "플러그"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "볼륨 생성을 위해 풀을 활성화해야 합니다"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "풀 유형이 볼륨 생성을 지원하지 않습니다"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "풀 볼륨은 VM에서 사용됩니다 "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "게스트에게 공개하기 위한 권장 소캣 수입니다."
+
+#~ msgid "Prefix"
+#~ msgstr "접두어"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "프리픽스 길이는 비워둘 수 없습니다"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "이전에 촬영한 스냅샷을 사용하면 문제가 발생했을 때 이전 상태로 돌아갈 수 "
+#~ "있습니다"
+
+#~ msgid "Product"
+#~ msgstr "제품"
+
+#~ msgid "Profile"
+#~ msgstr "프로파일"
+
+#~ msgid "Protocol"
+#~ msgstr "프로토콜"
+
+#~ msgid "Remote URL"
+#~ msgstr "원격 URL"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "원격 뷰어 세부 정보"
+
+#~ msgid "Revert"
+#~ msgstr "되돌리기"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "스냅샷 $0(으)로 되돌리기"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "이 스냅샷으로 되돌리면 VM이 스냅샷 촬영 시점으로 돌아가며 스냅샷에 캡처되"
+#~ "지 않은 데이터와 함께 현재 상태가 모두 손실됩니다"
+
+#~ msgid "Root password"
+#~ msgstr "root 비밀번호"
+
+#~ msgid "Route to $0"
+#~ msgstr "$0로 라우팅"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "무인 설치 실행"
+
+#~ msgid "Run when host boots"
+#~ msgstr "호스트 부팅 시 실행"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE TLS 포트"
+
+#~ msgid "SPICE address"
+#~ msgstr "SPICE 주소"
+
+#~ msgid "SPICE port"
+#~ msgstr "SPICE 포트"
+
+#~ msgid "Select console type"
+#~ msgstr "콘솔 유형 선택"
+
+#~ msgid "Send key"
+#~ msgstr "키 전송"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "마스크 불가능 인터럽트 보내기"
+
+#~ msgid "Serial console"
+#~ msgstr "직렬 콘솔"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "DHCP 범위 설정"
+
+#~ msgid "Set manually"
+#~ msgstr "수동 설정"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "자동 설치를 위한 사용자 암호를 설정하려면 VM을 생성할 때 VM을 시작해야합니"
+#~ "다"
+
+#~ msgid "Show additional options"
+#~ msgstr "추가 옵션 표시"
+
+#~ msgid "Shut off"
+#~ msgstr "종료"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "펌웨어 구성을 편집하려면 VM을 종료하십시오"
+
+#~ msgid "Shutting down"
+#~ msgstr "종료 중"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "스냅샷을 만들지 못했습니다"
+
+#~ msgid "Source format"
+#~ msgstr "소스 형식"
+
+#~ msgid "Source path"
+#~ msgstr "소스 경로"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "소스 경로를 비워둘 수 없습니다"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "소스은 http, ftp, nfs 통신규약으로 시작해야 합니다"
+
+#~ msgid "Source volume group"
+#~ msgstr "소스 볼륨 그룹"
+
+#~ msgid "Start libvirt"
+#~ msgstr "libvirt 시작"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "호스트 부팅 시 풀 시작"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "시작은 비워둘 수 없습니다"
+
+#~ msgid "Startup"
+#~ msgstr "시작"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "저장소 풀 $0의 활성화에 실패했습니다"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "저장소 풀 $0의 비활성화에 실패했습니다"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "저장소 풀 생성에 실패했습니다"
+
+#~ msgid "Storage pool name"
+#~ msgstr "저장소 풀 이름"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "저장소 크기가 0이 되어서는 안됩니다"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr "저장소 볼륨 크기는 저장소 풀의 용량을 초과해서는 안 됩니다($0 $1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "저장소 볼륨은 삭제 할 수 없습니다"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "일시 정지됩니다(PM)"
+
+#~ msgid "Target path"
+#~ msgstr "대상 경로"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "대상 경로를 비워둘 수 없습니다"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr "가상 머신이 실행되고 있으므로 삭제 전 강제로 종료됩니다."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr "이 장치를 분리하려면 VM이 실행 중이거나 종료되어 있어야합니다"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "서버에 디렉토리를 내보내는 중"
+
+#~ msgid "The pool is empty"
+#~ msgstr "풀이 비어 있습니다"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr "선택한 운영 체제가 자동 설치를 지원하지 않습니다"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr "선택한 운영 체제의 최소 메모리 요구 사항은 $0 $1입니다"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr "선택한 운영 체제의 최소 저장소 크기 요구 사항은 $0 $1입니다"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "저장소 풀을 삭제 할 수 없습니다"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr "이 VM은 일시적입니다. 삭제하려면 VM을 종료하십시오."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "이 볼륨은 다음에서 이미 사용하고 있습니다. "
+
+#~ msgid "Threads per core"
+#~ msgstr "코어 당 쓰레드"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr "Transient VM은 펌웨어 구성 편집을 지원하지 않습니다"
+
+#~ msgid "Type ID"
+#~ msgstr "ID 입력"
+
+#~ msgid "Unique name"
+#~ msgstr "고유한 이름"
+
+#~ msgid "Unique network name"
+#~ msgstr "고유한 네트워크 이름"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "알 수 없는 펌웨어"
+
+#~ msgid "Unplug"
+#~ msgstr "언플러그"
+
+#~ msgid "Up to $0 $1 available on the default location"
+#~ msgstr "지정된 장소에서 최대 $0 $1 사용 가능"
+
+#~ msgid "Up to $0 $1 available on the host"
+#~ msgstr "호스트에서 최대 $0 $1 사용 가능"
+
+#~ msgid "Url"
+#~ msgstr "URL"
+
+#~ msgid "User login must not be empty when user password is set"
+#~ msgstr "사용자 로그인은 사용자 비밀번호 설정 할 때에 비워두지 않아야 합니다"
+
+#~ msgid "User password must not be empty when user login is set"
+#~ msgstr "사용자 비밀번호는 사용자 로그인 설정 할 때에 비워두지 않아야 합니다"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "VCPU 설정을 저장 할 수 없습니다"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "VM $0이 이미 존재합니다"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "VM $0이(가) $1 연결에 존재하지 않습니다"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "VM $0의 강제 재시작에 실패했습니다"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "VM $0 강제 종료에 실패했습니다"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "VM $0 삭제에 실패했습니다"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "VM $0 설치에 실패했습니다"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "VM $0의 재시작에 실패했습니다"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "VM $0 재개에 실패했습니다"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "VM $0의 NMI 전송에 실패했습니다"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "VM $0 종료에 실패했습니다"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "VM $0 시작에 실패했습니다"
+
+#~ msgid "VM state"
+#~ msgstr "VM 상태"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC TLS 포트"
+
+#~ msgid "VNC address"
+#~ msgstr "VNC 주소"
+
+#~ msgid "VNC console"
+#~ msgstr "VNC 콘솔"
+
+#~ msgid "VNC port"
+#~ msgstr "VNC 포트"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "가상머신"
+
+#~ msgid "Virtual machines"
+#~ msgstr "가상 머신"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "가상 머신 관리"
+
+#~ msgid "Virtual network"
+#~ msgstr "가상 네트워크"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "가상 네트워크를 만들지 못했습니다"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "가상화 서비스(libvirt)가 활성화되어 있지 않습니다"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "볼륨 그룹 이름은 비워둘 수 없습니다"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "쓰기 가능"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "쓰기 가능 및 공유"
+
+#~ msgid "Yesterday"
+#~ msgstr "어제"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "가장 일치하는 운영 체제를 선택해야합니다"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "비활성화됩니다"
+
+#~ msgid "down"
+#~ msgstr "아래로"
+
+#~ msgid "enabled"
+#~ msgstr "활성화됩니다"
+
+#~ msgid "ethernet"
+#~ msgstr "이더넷"
+
+#~ msgid "host device"
+#~ msgstr "호스트 장치"
+
+#~ msgid "host passthrough"
+#~ msgstr "호스트 통과"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "iSCSI 직접 대상"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "iSCSI 개시자 IQN"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "iSCSI 대상 IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "아니요"
+
+#~ msgid "no state saved"
+#~ msgstr "저장된 상태 없음"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "재전송된 장치"
+
+#~ msgid "server"
+#~ msgstr "서버"
+
+#~ msgid "up"
+#~ msgstr "위로"
+
+#~ msgid "vCPU count"
+#~ msgstr "vCPU 개수"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU 최대"
+
+# auto translated by TM merge from project: RHEV Administration Guide,
+# version: 3.6-async1, DocId: chap-
+# Administering_and_Maintaining_the_Red_Hat_Enterprise_Virtualization_Environment
+#~ msgid "vCPUs"
+#~ msgstr "vCPU"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#~ msgid "view more..."
+#~ msgstr "더 알아보기..."
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr "VM을 복제하려면 시스템에 virt-install 꾸러미를 설치해야 합니다"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "새로운 가상 머신을 생성하려면 시스템에 virt-install 꾸러미를 설치해야 합니"
+#~ "다"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr "이 속성을 편집하려면 시스템에 virt-install 꾸러미를 설치해야 합니다"
+
+#~ msgid "vm"
+#~ msgstr "vm"
+
+#~ msgid "Local install media"
+#~ msgstr "로컬 설치 미디어"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "루트 암호를 설정하십시오"
+
+#~ msgid "21st"
+#~ msgstr "21일"
+
+#~ msgid "22nd"
+#~ msgstr "22일"
+
+#~ msgid "23rd"
+#~ msgstr "23일"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "이후"
+
+#~ msgid "Friday"
+#~ msgstr "금요일"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "시간 : 분"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "시간은 0-23 사이의 값이어야 합니다"
+
+#~ msgid "Invalid date format."
+#~ msgstr "날짜 형식이 잘못되어 있습니다."
+
+#~ msgid "Invalid number."
+#~ msgstr "잘못된 번호."
+
+#~ msgid "Monday"
+#~ msgstr "월요일"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "매시간 반복"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "매년 반복"
+
+#~ msgid "Saturday"
+#~ msgstr "토요일"
+
+#~ msgid "Sunday"
+#~ msgstr "일요일"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "이 날은 모든 달에 존재하지 않습니다.<br> 타이머는 31일이 있는 달에만 실행"
+#~ "됩니다."
+
+#~ msgid "Thursday"
+#~ msgstr "목요일"
+
+#~ msgid "Tuesday"
+#~ msgstr "화요일"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 업데이트"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 보안 수정"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "저장 장치 관리"
+
+#~ msgid "Restart now"
+#~ msgstr "지금 다시 시작"
+
+#~ msgid "Configure"
+#~ msgstr "설정"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr "네트워크 부팅은 시스템 연결을 사용할 때만 사용할 수 있습니다"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "네트워킹"
+
+#, fuzzy
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "이러한 파일 또는 디렉토리가 없습니다"
+
+#~ msgid "No updates pending"
+#~ msgstr "보류 중인 업데이트가 없습니다"
+
+#, fuzzy
+#~| msgid "ssh server is empty"
+#~ msgid "This directory is empty"
+#~ msgstr "ssh 서버가 비어 있습니다"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr "이 풀 유형은 스토리지 볼륨 생성을 지원하지 않습니다"
+
+#~ msgid "undefined"
+#~ msgstr "정의되지 않음"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "잘못된 호스트 키"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "호스트 {{#strong}}{{host}}{{/strong}} 인증을 설정할 수 없습니다. 연결을 유"
+#~ "지하시겠습니까?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}}의 키가 이전에 사용된 키와 일치하지 않습니"
+#~ "다. 이 컴퓨터가 최근에 교체된 것이 아닌 경우 누군가가 이 컴퓨터에 대한 연"
+#~ "결을 공격할 가능성이 있습니다."
+
+#~ msgid "Unknown host key"
+#~ msgstr "알 수 없는 호스트 키"
+
+#~ msgid "Address:"
+#~ msgstr "주소:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "최소 $0 개의 디스크가 필요합니다."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "SPICE 또는 VNC 뷰어 애플리케이션에 연결합니다."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "$0에서 MSI를 다운로드합니다"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "그래픽 콘솔 (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "데스크탑 뷰어 그래픽 콘솔"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "이 가상 머신에 지정된 콘솔이 없습니다."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid "Entry"
+#~ msgstr "항목"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "브라우저 연결이 해제되지만 업데이트 프로세스에는 영향을 미치지 않습니다. "
+#~ "다시 연결 하여 진행 상황을 계속 확인할 수 있습니다."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "자동으로 컴퓨터를 다시 시작합니다."
+
+# auto translated by TM merge from project: RHEV Administration Guide,
+# version: 4.0, DocId: chap-
+# Administering_and_Maintaining_the_Red_Hat_Enterprise_Virtualization_Environment
+#~ msgid "Dashboard"
+#~ msgstr "대시보드"
+
+#, fuzzy
+#~ msgid "Description input text"
+#~ msgstr "설명"
+
+#~ msgid "FAILED"
+#~ msgstr "실패"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "연결이 끓어졌습니다. 다시 시도 하는 중입니다"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "네트워크"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "새 볼륨 크기를 입력해 주십시오"
+
+#~ msgid "Servers"
+#~ msgstr "서버"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr "현재 직접적으로 이 서버에 연결되어 있습니다. 삭제하실 수 없습니다."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 비트"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 바이트"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 바이트"
+
+#, fuzzy
+#~| msgid "Bond settings"
+#~ msgid "Bond settings "
+#~ msgstr "본드 설정"
+
+#, fuzzy
+#~| msgid "IP settings"
+#~ msgid "IP settings "
+#~ msgstr "IP 설정"
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Cockpit에서 예상치 못한 내부 오류가 발생했습니다. <br/><br/>브라우저에서 "
+#~ "업데이트를 눌러 Cockpit 재부팅을 시도할 수 있습니다. javascript 콘솔에는 "
+#~ "이러한 오류에 대한 상세 정보가 포함되어 있습니다 (대부분의 브라우저에서 "
+#~ "<b>Ctrl-Shift-J</b>)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "가상 머신이 크래시되었습니다."
+
+#~ msgid "The VM is down."
+#~ msgstr "가상 머신이 다운되었습니다."
+
+#~ msgid "The VM is going down."
+#~ msgstr "가상 머신이 다운됩니다."
+
+#~ msgid "The VM is idle."
+#~ msgstr "가상 머신이 유휴 상태입니다."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr ""
+#~ "가상 머신이 종료되고 있습니다 (종료 또는 크래시가 완료되지 않았습니다)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "가상 머신이 일시 중지합니다."
+
+#~ msgid "The VM is running."
+#~ msgstr "가상 머신이 실행되고 있습니다."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "가상 머신은 게스트 전원 관리에 의해 일시 중지됩니다."
+
+#~ msgid "inactive"
+#~ msgstr "비활성"
+
+#~ msgid "Avatar"
+#~ msgstr "아바타"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "이 컴퓨터에 현재 로그인한 사용자로 연결하려면 비워 두십시오. 다른 사용자 "
+#~ "이름을 입력한 경우 이 컴퓨터에 연결할 때 이 사용자가 항상 사용됩니다."
+
+#~ msgid "2 min"
+#~ msgstr "2분"
+
+#~ msgid "3 min"
+#~ msgstr "3분"
+
+#~ msgid "4 min"
+#~ msgstr "4분"
+
+#~ msgid "CPU graph"
+#~ msgstr "CPU 그래프"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "CPU 상태"
+
+#~ msgid "Cached"
+#~ msgstr "캐시됨"
+
+#~ msgid "Graphs"
+#~ msgstr "그래프"
+
+#~ msgid "I/O wait"
+#~ msgstr "I/O 대기"
+
+#~ msgid "Kernel"
+#~ msgstr "커널"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "메모리"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "메모리 & 스왑 사용"
+
+#~ msgid "Memory graph"
+#~ msgstr "메모리 그래프"
+
+#~ msgid "Network traffic"
+#~ msgstr "네트워크 통신량"
+
+#~ msgid "Nice"
+#~ msgstr "Nice"
+
+#~ msgid "Synchronize users"
+#~ msgstr "사용자 동기화"
+
+#~ msgid "Usage graphs"
+#~ msgstr "사용 그래프"
+
+#~ msgid "View graphs"
+#~ msgstr "그래프 보기"
+
+#~ msgid "idle"
+#~ msgstr "유휴상태"
+
+#~ msgid "paused"
+#~ msgstr "일시 중지"
+
+#~ msgid "running"
+#~ msgstr "실행 중"
+
+#~ msgid "shut off"
+#~ msgstr "종료"
+
+#~ msgid "shutdown"
+#~ msgstr "종료"
+
+#~ msgid "Add a new host"
+#~ msgstr "새 호스트 추가"
+
+#~ msgid "Available"
+#~ msgstr "사용 가능"
+
+#, fuzzy
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "IP 주소 또는 호스트 명을 입력하세요"
+
+#~ msgid "Network interfaces"
+#~ msgstr "네트워크 인터페이스"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "cockpit-ws 버전에서는 다른 사용자 또는 포트가 있는 호스트 연결이 지원되지 "
+#~ "않습니다"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "다른 포트를 사용하려면 cockpit-ws를 최신 버전으로 업그레이드해야 합니다."
+
+#~ msgid "$0 template"
+#~ msgstr "$0 템플릿"
+
+#~ msgid "Instance of template: "
+#~ msgstr "템플릿 인스턴스: "
+
+#~ msgid "Instantiate"
+#~ msgstr "인스턴스화"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 공유"
+
+#~ msgid "All In One"
+#~ msgstr "일체형"
+
+#~ msgid "Always"
+#~ msgstr "항상"
+
+#~ msgid "Author"
+#~ msgstr "작성자"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "passwd1 메커니즘에 잘못된 데이터가 전달되었습니다"
+
+#~ msgid "CPU priority"
+#~ msgstr "CPU 우선순위"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Docker에 연결할 수 없습니다"
+
+#~ msgid "Change resource limits"
+#~ msgstr "리소스 제한 변경"
+
+#~ msgid "Change resources limits"
+#~ msgstr "리소스 제한 변경"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit은 {{#strong}}{{host}}{{/strong}}에 로그인할 수 없습니다."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit은 {{#strong}}{{host}}{{/strong}}에 로그인할 수 없습니다. 다음에서 "
+#~ "인증 정보를 변경할 수 있습니다. {{#can_sync}}{{#sync_link}}계정과 암호 동"
+#~ "기화{{/sync_link}}를 실행할 수 있습니다.{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "총 메모리 사용량"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "$0 CPU 코어의 총 사용량"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "명령을 비워 둘 수 없습니다"
+
+#~ msgid "Command:"
+#~ msgstr "명령:"
+
+#~ msgid "Commit"
+#~ msgstr "커밋"
+
+#~ msgid "Commit image"
+#~ msgstr "이미지 커밋"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Docker에 연결중입니다"
+
+#~ msgid "Container name"
+#~ msgstr "컨테이너 이름"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "컨테이너는 현재 실행중이지 않은 것으로 보이지만, 정상적인 종료가 되지 않았"
+#~ "습니다."
+
+#~ msgid "Container is currently running."
+#~ msgstr "컨테이너가 현재 실행중입니다."
+
+#~ msgid "Container:"
+#~ msgstr "컨테이너:"
+
+#~ msgid "Containers"
+#~ msgstr "컨테이너들"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "컨테이너들"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "사용자 그룹을 변경할 수 없습니다"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "사용자 암호를 변경할 수 없습니다"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "장치에 연결할 수 없습니다"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "새 사용자를 생성할 수 없습니다"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "로컬 사용자를 나열할 수 없습니다"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "사용자를 나열할 수 없습니다"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "사용자 데이터를 가져올 수 없습니다"
+
+#~ msgid "Created:"
+#~ msgstr "작성일:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Docker 컨테이너"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "본 시스템에 Docker가 설치되어 있지 않거나 활성화되어 있지 않습니다"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "alias 복제"
+
+#~ msgid "Duplicate port"
+#~ msgstr "포트 복제"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "ssh-agent에 아이디를 추가하려면 암호를 입력하십시오"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "여기에 다른 암호를 입력하면 이 컴퓨터에 연결할 때 마다 암호를 다시 입력해"
+#~ "야 합니다"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgid "Environment"
+#~ msgstr "환경"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "사용자를 로드하는 동안 오류가 발생했습니다: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Docker로 부터의 오류 메세지:"
+
+#~ msgid "Everything"
+#~ msgstr "전체"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "$ExitCode 로 종료"
+
+#~ msgid "Expose container ports"
+#~ msgstr "컨테이너 포트 공개"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Docker 실행을 실패하였습니다: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Docker Scope를 정지하지 못했습니다: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "새 이미지 가져오기"
+
+#~ msgid "IP address:"
+#~ msgstr "IP 주소:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "IP 접두어 길이:"
+
+# translation auto-copied from project Satellite6 Hammer CLI Foreman, version
+# 6.1, document hammer-cli-foreman
+#~ msgid "Id"
+#~ msgstr "ID"
+
+#~ msgid "Id:"
+#~ msgstr "Id:"
+
+#~ msgid "Image"
+#~ msgstr "이미지"
+
+#~ msgid "Image $0"
+#~ msgstr "이미지 $0"
+
+#~ msgid "Image search"
+#~ msgstr "이미지 검색"
+
+#~ msgid "Image:"
+#~ msgstr "이미지:"
+
+#~ msgid "Images"
+#~ msgstr "이미지"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "이미지"
+
+#~ msgid "Images and running containers"
+#~ msgstr "이미지 및 실행 중인 컨테이너"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "사용자를 동기화하려면 {{#strong}}{{host}}{{/strong}}에 로그인해야 합니다."
+
+#~ msgid "Invalid port"
+#~ msgstr "잘못된 포트"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Kerberos 티켓"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "컴퓨터에 현재 로그인한 사용자{{#default_user}} ({{default_user}}){{/"
+#~ "default_user}}로 연결하려면 비워두십시오. 다른 사용자 이름을 입력하면 이 "
+#~ "컴퓨터에 연결할 때 입력한 사용자가 항상 사용됩니다."
+
+#~ msgid "Link to another container"
+#~ msgstr "다른 컨테이너로의 링크"
+
+#~ msgid "Links:"
+#~ msgstr "링크:"
+
+#~ msgid "Login Password"
+#~ msgstr "로그인 암호"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC 주소:"
+
+#~ msgid "Memory limit"
+#~ msgstr "메모리 제한"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "컨테이너 볼륨 마운트"
+
+#~ msgid "No container specified"
+#~ msgstr "컨테이너가 지정되지 않았습니다"
+
+#~ msgid "No containers"
+#~ msgstr "컨테이너 없음"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "현재 필터에 일치하는 컨테이너가 없습니다"
+
+#~ msgid "No images"
+#~ msgstr "이미지 없음"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "현재 필터에 일치하는 이미지가 없습니다"
+
+#~ msgid "No results for $0"
+#~ msgstr "$0의 결과를 찾을 수 없습니다"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "현재 필터에 일치하는 이미지가 없습니다"
+
+#~ msgid "No running containers"
+#~ msgstr "실행 중인 컨테이너가 없습니다"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "현재 필터에 일치하는 실행 중인 컨테이너가 없습니다"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "본 시스템 상의 Docker에 대한 접근 권한이 없습니다"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "오류 발생 시, $0 번 다시 시도"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "$0 강제 삭제를 확인해주세요"
+
+#~ msgid "Please try another term"
+#~ msgstr "다른 용어를 사용해 보십시오"
+
+#~ msgid "Ports:"
+#~ msgstr "포트:"
+
+#~ msgid "Problems"
+#~ msgstr "문제"
+
+#~ msgid "ReadOnly"
+#~ msgstr "읽기 전용"
+
+#~ msgid "ReadWrite"
+#~ msgstr "읽기쓰기"
+
+#~ msgid "Repository"
+#~ msgstr "리포지터리"
+
+#~ msgid "Restart policy:"
+#~ msgstr "재시작 정책:"
+
+#~ msgid "Retries:"
+#~ msgstr "재시도 횟수:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "원격 연결에 비밀번호를 재사용"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr "{{#strong}}{{host}}{{/strong}}와 동기화할 사용자를 선택합니다"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "컨테이너 환경 변수 설정"
+
+#~ msgid "Severity:"
+#~ msgstr "심각도:"
+
+#~ msgid "Show all containers"
+#~ msgstr "모든 컨테이너 표시"
+
+#~ msgid "Start Docker"
+#~ msgstr "Docker 시작"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#~ msgid "State:"
+#~ msgstr "상태:"
+
+#~ msgid "Synchronize"
+#~ msgstr "동기화"
+
+#~ msgid "Tag"
+#~ msgstr "태그"
+
+#~ msgid "Tags"
+#~ msgstr "태그"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr "다음 컨테이너는 이 이미지에 종속되어 있으며 사용할 수 없게 됩니다."
+
+#~ msgid "This image does not exist."
+#~ msgstr "이미지가 존재하지 않습니다."
+
+#~ msgid "Type a password"
+#~ msgstr "암호 입력"
+
+#~ msgid "Type to filter…"
+#~ msgstr "필터 입력…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "중지될 때 까지"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "지원되지 않는 설정 메커니즘"
+
+#~ msgid "Up since $0"
+#~ msgstr "$0에서 실행 중"
+
+#~ msgid "Used by containers"
+#~ msgstr "컨테이너에 의해 사용"
+
+#~ msgid "Using available credentials"
+#~ msgstr "사용 가능한 인증 정보 사용"
+
+#~ msgid "Volumes"
+#~ msgstr "볼륨"
+
+#~ msgid "Volumes:"
+#~ msgstr "볼륨:"
+
+#~ msgid "With terminal"
+#~ msgstr "터미널 실행"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}}에 연결되어 있지만 사용자를 동기화하려면 수"
+#~ "퍼 유저 권한을 가진 사용자가 필요합니다."
+
+#~ msgid "default"
+#~ msgstr "기본"
+
+#~ msgid "key"
+#~ msgstr "키"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "이름, 네임 스페이스, 설명으로 검색"
+
+#~ msgid "shares"
+#~ msgstr "공유"
+
+#~ msgid "to host port"
+#~ msgstr "호스트 포트로"
+
+# ctx::sourcefile::NOT USED
+#~ msgid "value"
+#~ msgstr "value"
+
+#~ msgid "Master"
+#~ msgstr "마스터"
+
+#~ msgid "Password for $0"
+#~ msgstr "$0의 암호"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "사용자 <b>$0</b> 는 서버 관리 권한이 없습니다"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "계정"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "사용자 <b>$0</b>는 계정을 수정할 수 없습니다"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "root 계정을 삭제할 수 없음"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "root 계정 이름을 다시 설정할 수 없습니다"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "새 영역을 추가할 권한이 없습니다"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "$0 영역에 서비스를 추가할 권한이 없습니다"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "$0 서비스를 제거 할 권한이 없습니다"
+
+#~ msgid "Account Settings"
+#~ msgstr "계정 설정"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "대시보드에 시스템 추가"
+
+#~ msgid "Machines"
+#~ msgstr "장치들"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "로그인 관리자의 권한이 상승되었습니다"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "권한 작업 실행 또는 다른 컴퓨터에 연결하기 위해 암호를 사용할 수 없습니다"
+
+#~ msgid "Privileged"
+#~ msgstr "권한"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr "권한 작업 실행 또는 다른 컴퓨터에 연결을 위해 암호를 재사용합니다"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr "사용자 $0은/는 호스트 이름을 변경할 수 없습니다"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr "사용자 $0은/는 이 서버를 종료 또는 재시작할 수 없습니다"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "사용자 <b>$0</b>는 프로파일을 변경할 수 없습니다"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "사용자 <b>$0</b>는 스토리지를 관리할 수 없습니다"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr "사용자 <b>$0</b> 는 네트워크 설정 권한이 없습니다"
+
+#, fuzzy
+#~| msgid "Required by "
+#~ msgid "Required by $0"
+#~ msgstr "필요 사항 "
+
+#~ msgid "Automatic Startup"
+#~ msgstr "자동 시작"
+
+#~ msgid "Last Trigger"
+#~ msgstr "마지막 트리거"
+
+#~ msgid "Next Run"
+#~ msgstr "다음 실행"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "사용자 <b>$0</b>은/는 타이머를 만들 권한이 없습니다"
+
+#~ msgid "Connecting"
+#~ msgstr "연결 중"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Cockpit 정보"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (OS와 공유)"
+
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 취약점"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "스토리지 추가"
+
+#~ msgid "Add Storage"
+#~ msgstr "스토리지 추가"
+
+#~ msgid "Additional Storage"
+#~ msgstr "추가 스토리지"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "선택된 디스크의 모든 데이터가 삭제되며 디스크는 스토리지 풀에 추가됩니다."
+
+#~ msgid "Configure storage..."
+#~ msgstr "스토리지 설정..."
+
+#~ msgid "Could not add all disks"
+#~ msgstr "모든 디스크를 추가할 수 없습니다"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "컨테이너 제거 및 스토리지 풀 재설정"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr "Docker 스토리지 풀에 관한 정보를 사용할 수 없습니다."
+
+#~ msgid "Local Disks"
+#~ msgstr "로컬 디스크"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "추가 로컬 스토리지를 찾을 수 없습니다."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "디스크 재포맷 및 디스크 추가"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "스토리지 풀을 재설정하면 풀의 모든 컨테이너와 릴리스 디스크가 삭제됩니다."
+
+#~ msgid "Security"
+#~ msgstr "보안"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr "Docker 스토리지 풀은 이 시스템에서 관리할 수 없습니다."
+
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "$time ($type) 검사에서 $count 취약점이 발견되었습니다:"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "$time ($type) 검사에서 취약점이 발견되지 않았습니다."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "$time ($type) 검사에 성공하지 못했습니다."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "가상화 서비스를 사용할 수 있습니다"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "Docker 스토리지 풀을 관리할 수 있는 권한이 없습니다."
+
+#~ msgid "$0 GiB / $1 GiB"
+#~ msgstr "$0 GiB / $1 GiB"
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} -> $cport"
+
+#~ msgid "Hide Performance Options"
+#~ msgstr "성능 옵션 숨기기 "
diff --git a/po/nb_NO.po b/po/nb_NO.po
new file mode 100644
index 0000000..bdae154
--- /dev/null
+++ b/po/nb_NO.po
@@ -0,0 +1,12807 @@
+# #-#-#-#-# cockpit.c.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# #-#-#-#-# cockpit.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# #-#-#-#-# cockpit.appstream.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+# #-#-#-#-# cockpit.polkit.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-07-31 12:26+0000\n"
+"Last-Translator: yangyangdaji <1504305527@qq.com>\n"
+"Language-Team: Norwegian Bokmål <https://translate.fedoraproject.org/"
+"projects/cockpit/main/nb_NO/>\n"
+"Language: nb_NO\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 4.18.2\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+msgstr[1] "$0 CPUer"
+
+#: pkg/lib/machine-info.js:229
+#, fuzzy
+#| msgid "$0 GiB total"
+msgid "$0 GiB"
+msgstr "$0 GiB totalt"
+
+#: pkg/networkmanager/network-main.jsx:174
+#, fuzzy
+#| msgid "$0 active zone"
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 aktiv sone"
+msgstr[1] "$0 aktiv sone"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+#, fuzzy
+#| msgid "$0 GiB available"
+msgid "$0 available"
+msgstr "$0 GiB tilgjengelig"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "$0 filsystemer kan ikke gjøres større."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "$0 filsystemer kan ikke gjøres mindre."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "$0 filsystemer kan ikke endres størrelse på her."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "$0 filsystemer kan ikke endres størrelse på her."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr ""
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 kritisk treff"
+msgstr[1] "$0 treff, inkludert kritiske"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 data + $1 overhead brukt av $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 dag"
+msgstr[1] "$0 dager"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disk mangler"
+msgstr[1] "$0 disker mangler"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disk mangler"
+msgstr[1] "$0 disker mangler"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 disker"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 dokumentasjon"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 feil"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 avsluttet med koden $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 feilet"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+#, fuzzy
+#| msgid "VM $0 failed to pause"
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "VM $0 kunne ikke pause"
+msgstr[1] "VM $0 kunne ikke pause"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 file system"
+msgid "$0 filesystem"
+msgstr "$0 filsystem"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 ledig"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 time"
+msgstr[1] "$0 timer"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 viktig treff"
+msgstr[1] "$0 viktige treff"
+
+#: pkg/users/account-create-dialog.js:378
+#, fuzzy
+#| msgid "$0 disk is missing"
+#| msgid_plural "$0 disks are missing"
+msgid "$0 is an existing file"
+msgstr "$0 disk mangler"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+#, fuzzy
+#| msgid "$0 is in active use"
+msgid "$0 is in use"
+msgstr "$0 er i aktiv bruk"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 er ikke tilgjengelig fra noe depot."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 nøkkel endret"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 drept med signal $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 treff med lav alvorlighetsgrad"
+msgstr[1] "$0 treff med lave alvorlighetsgrader"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minutt"
+msgstr[1] "$0 minutter"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 moderat treff"
+msgstr[1] "$0 moderate treff"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 måned"
+msgstr[1] "$0 måneder"
+
+#: pkg/users/accounts-list.js:339
+#, fuzzy
+#| msgid "Read more..."
+msgid "$0 more..."
+msgstr "Les mer…"
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 pakke"
+msgstr[1] "$0 pakker"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 pakke trenger en omstart av systemet"
+msgstr[1] "$0 pakker trenger en omstart av systemet"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 side"
+msgstr[1] "$0 sider"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "partisjon"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 sikkerhetsrettelse tilgjengelig"
+msgstr[1] "$0 sikkerhetsrettelser tilgjengelig"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 tjeneste har feilet"
+msgstr[1] "$0 tjenester har feilet"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$o tjeneste trenger omstart"
+msgstr[1] "$o tjenester trenger omstart"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 slot gjenstår"
+msgstr[1] "$0 slot-er gjenstår"
+
+#: pkg/metrics/metrics.jsx:1333
+#, fuzzy
+#| msgid "CPU spike"
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "CPU-topp"
+msgstr[1] "CPU-topp"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+#, fuzzy
+#| msgid "Not synchronized"
+msgid "$0 synchronized"
+msgstr "Ikke synkronisert"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 totalt"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 oppdatering tilgjengelig"
+msgstr[1] "$0 oppdateringer tilgjengelig"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 brukt av $1 ($2 lagret)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 uke"
+msgstr[1] "$0 uker"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 vil bli installert."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 år"
+msgstr[1] "$0 år"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 sone"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: krasj på $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (fra $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Oppretter partisjon $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Lokalt monteringspunkt"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Ikke montert"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+#, fuzzy
+#| msgid "recommended"
+msgid "(recommended)"
+msgstr "anbefalt"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", inkludert $1 sikkerhetsrettelse"
+msgstr[1] ", inkludert $1 sikkerhetsrettelser"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 dag"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 time"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 min"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 minutt"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 uke"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14."
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 min"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 minutter"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 minutter"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4."
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 min"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minutter"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5."
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 timer"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 minutter"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9."
+
+#: pkg/shell/hosts_dialog.jsx:98
+#, fuzzy
+#| msgid ""
+#| "A compatible version of Cockpit is not installed on {{#strong}}{{host}}{{/"
+#| "strong}}."
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr ""
+"En kompatibel versjon av Cockpit er ikke installert på {{#strong}}{{host}}{{/"
+"strong}}."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr ""
+
+#: pkg/users/group-create-dialog.js:71
+#, fuzzy
+#| msgid "This user name already exists"
+msgid "A group with this name already exists"
+msgstr "Dette brukernavnet finnes allerede"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr "Det kreves en moderne nettleser for sikkerhet, pålitelighet og ytelse."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"En nettverksbinding kombinerer flere nettverksgrensesnitt til ett logisk "
+"grensesnitt med høyere gjennomstrømning eller redundans."
+
+#: pkg/shell/hosts_dialog.jsx:795
+#, fuzzy
+#| msgid ""
+#| "A new SSH key at ${key} will be created for ${luser} on ${lhost} and it "
+#| "will be added to the ${afile} file of ${ruser} on ${rhost}."
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"En ny SSH-nøkkel på ${key} blir opprettet for ${luser} på ${lhost}, og den "
+"vil bli lagt til ${afile}-filen på ${ruser} på ${rhost}."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"En pakke trenger en omstart av systemet for at oppdateringene skal tre i "
+"kraft:"
+msgstr[1] ""
+"Noen pakker trenger en omstart av systemet for at oppdateringene skal tre i "
+"kraft:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"En tjeneste må startes på nytt for at oppdateringene skal tre i kraft:"
+msgstr[1] ""
+"Noen tjenester må startes på nytt for at oppdateringene skal tre i kraft:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "Fysiske volumer kan ikke endres her."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP overvåking"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Om Web konsoll"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Fraværende"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Godta nøkkel og logg inn"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+#, fuzzy
+#| msgid "Set password"
+msgid "Acceptable password"
+msgstr "Angi passord"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Konto utløper"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Kontoen er ikke tilgjengelig eller kan ikke redigeres."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Kontoer"
+
+#: pkg/storaged/dialog.jsx:1240
+#, fuzzy
+#| msgid "Actions"
+msgid "Action"
+msgstr "Handlinger"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Handlinger"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Aktiver"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Aktiverer $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Aktiv"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr ""
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Aktive sider"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Aktiv siden "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+#, fuzzy
+#| msgid "Activate"
+msgid "Active state"
+msgstr "Aktiver"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Adaptiv lastbalansering"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr ""
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Legg til"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Legg til $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add DNS server"
+msgstr "Tang nøkkelserver"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:458
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add Tang keyserver"
+msgstr "Tang nøkkelserver"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Legg til VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+#, fuzzy
+#| msgid "Add VLAN"
+msgid "Add VPN"
+msgstr "Legg til VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+#, fuzzy
+#| msgid "Add team"
+msgid "Add a bitmap"
+msgstr "Legg til team"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Legg til en ny sone"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+#, fuzzy
+#| msgid "MAC address"
+msgid "Add address"
+msgstr "MAC-adresse"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Add block devices"
+msgstr "$0 blokk enhet"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Legg til binding"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Legg til bridge"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Legg til disk"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Legg til disker"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Legg til iSCSI-portal"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Legg til nøkkel"
+
+#: pkg/storaged/stratis/pool.jsx:551
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add keyserver"
+msgstr "Tang nøkkelserver"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Legg til medlem"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Legg til ny vert"
+
+#: pkg/networkmanager/firewall.jsx:1043
+#, fuzzy
+#| msgid "Add a new zone"
+msgid "Add new zone"
+msgstr "Legg til en ny sone"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Add passphrase"
+msgstr "Gammel passordfrase"
+
+#: pkg/networkmanager/wireguard.jsx:290
+#, fuzzy
+#| msgid "Add member"
+msgid "Add peer"
+msgstr "Legg til medlem"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "Physical volume"
+msgid "Add physical volume"
+msgstr "Fysisk volum"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Legg til porter"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Legg til porter i $0 sonen"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Legg til offentlig nøkkel"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+#, fuzzy
+#| msgid "Add team"
+msgid "Add route"
+msgstr "Legg til team"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+#, fuzzy
+#| msgid "DNS search domains"
+msgid "Add search domain"
+msgstr "DNS-søkedomener"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Legg til tjenester"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Legg til tjenester i $0-sonen"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Legg til tjenester i sone $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Legg til team"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Legg til sone"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+#, fuzzy
+#| msgid "Encryption options"
+msgid "Adding \"$0\" to encryption options"
+msgstr "Krypteringsalternativer"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr ""
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+#, fuzzy
+#| msgid ""
+#| "Adding <b>$0</b> will break the connection to the server, and will make "
+#| "the administration UI unavailable."
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Hvis du legger til <b>$0</b>, brytes forbindelsen til serveren, og "
+"administrasjonsgrensesnittet gjøres utilgjengelig."
+
+#: pkg/storaged/stratis/pool.jsx:468
+#, fuzzy
+#| msgid ""
+#| "Saving a new passphrase requires unlocking the disk. Please provide a "
+#| "current disk passphrase."
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"For å lagre en ny passfrase, må du låse opp disken. Oppgi en aktuell "
+"passfrase."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Hvis du legger til egendefinerte porter, lastes firewalld på nytt. En "
+"omlasting vil resultere i tap av en hvilken som helst kjøretidskonfigurasjon!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Legger til nøkkel"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Legger til fysisk volum til $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Ekstra DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Ekstra DNS-søkedomener $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Ekstra handlinger"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Ekstra adresse $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Ekstra pakker:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Ekstra porter"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Adresse"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Adresse $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Adressen kan ikke være tom"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Adressen er ikke en gyldig URL"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Adresser"
+
+#: pkg/networkmanager/wireguard.jsx:52
+#, fuzzy
+#| msgid "Address cannot be empty"
+msgid "Addresses are not formatted correctly"
+msgstr "Adressen kan ikke være tom"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Administrasjon med Cockpit Web konsoll"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Administrativ tilgang"
+
+#: pkg/sosreport/sosreport.jsx:426
+#, fuzzy
+#| msgid ""
+#| "You need to switch to \"Administrative access\" in order to create "
+#| "reports."
+msgid "Administrative access is required to create and access reports."
+msgstr "Du må bytte til \"Administrativ tilgang\" for å opprette rapporter."
+
+#: pkg/sosreport/sosreport.jsx:425
+#, fuzzy
+#| msgid "Administrative access"
+msgid "Administrative access required"
+msgstr "Administrativ tilgang"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Avansert TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Etter"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Etter å ha forlatt domenet, er det bare brukere med lokal legitimasjon som "
+"kan logge på denne maskinen. Dette kan også påvirke andre tjenester ettersom "
+"innstillinger for DNS-oppløsning og listen over klarerte CAer kan endres."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Etter systemstart"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Varsel"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Varsel og over"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Alle"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Alle oppdateringer"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Alt i ett"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Tillat å kjøre (umaskert)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+#, fuzzy
+#| msgid "Allowed addresses"
+msgid "Allowed IPs"
+msgstr "Tillatte adresser"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Tillatte adresser"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Ansible roller dokumentasjon"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Utseende"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr ""
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Applikasjoner"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Applikasjonsliste"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+#, fuzzy
+#| msgid "Save and reboot"
+msgid "Apply and reboot"
+msgstr "Lagre og start på nytt"
+
+#: pkg/packagekit/kpatch.jsx:261
+#, fuzzy
+#| msgid "Applying updates"
+msgid "Apply kernel live patches"
+msgstr "Tar i bruk oppdateringer"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Bruk denne løsningen"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Bruker løsning ..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Tar i bruk oppdateringer"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Å ta i bruk oppdateringer feilet"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr ""
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Apper"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Vurdering"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Eiendomsmerke"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+#, fuzzy
+#| msgid "boot"
+msgid "At boot"
+msgstr "oppstart"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Minst $0 disk er nødvendig."
+msgstr[1] "Minst $0 disker er nødvendige."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+#, fuzzy
+#| msgid "At least one disk is needed."
+msgid "At least one block device is needed."
+msgstr "Minst én disk er nødvendig."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Minst én disk er nødvendig."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+#, fuzzy
+#| msgid "1 minute"
+msgid "At minute"
+msgstr "1 minutt"
+
+#: pkg/systemd/timer-dialog.jsx:260
+#, fuzzy
+#| msgid "Seconds"
+msgid "At second"
+msgstr "Sekunder"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "På et bestemt tidspunkt"
+
+#: pkg/sosreport/sosreport.jsx:503
+#, fuzzy
+#| msgid "Edit $0 attributes"
+msgid "Attributes"
+msgstr "Rediger $0 attributter"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr ""
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Autentiser"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Autentiserer"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Autentisering"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Autentisering feilet"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Autentisering feilet: Serveren lukket tilkoblingen"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Autentisering er nødvendig for å utføre privilegerte oppgaver med Cockpit "
+"Web konsoll"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Autentisering kreves"
+
+#: pkg/shell/hosts_dialog.jsx:826
+#, fuzzy
+#| msgid "Authorize SSH key."
+msgid "Authorize SSH key"
+msgstr "Autoriser SSH-nøkkel."
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Autoriserte offentlige SSH-nøkler"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automatisk"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automatisk (kun DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Automatisk pålogging"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Automatiske oppdateringer"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Starter automatisk"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Automatisk med bruk av NTP"
+
+#: pkg/lib/serverTime.js:570
+#, fuzzy
+#| msgid "Automatically using specific NTP servers"
+msgid "Automatically using additional NTP servers"
+msgstr "Automatisk med bruk av spesifikke NTP servere"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Automatisk med bruk av spesifikke NTP servere"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Automatiseringsskript"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Tilgjengelige mål på $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Tilgjengelige oppdateringer"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "partition"
+msgid "BIOS boot partition"
+msgstr "partisjon"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "BIOS dato"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "BIOS versjon"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Tilbake til kontoer"
+
+#: pkg/systemd/services.jsx:257
+#, fuzzy
+#| msgid "Blade"
+msgid "Bad"
+msgstr "Blad"
+
+#: pkg/systemd/services.jsx:223
+#, fuzzy
+#| msgid "Bond settings"
+msgid "Bad setting"
+msgstr "Binding-innstillinger"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Før"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Binder seg til"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Svart"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blad"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Blad-kabinett"
+
+#: pkg/storaged/block/other.jsx:41
+#, fuzzy
+#| msgid "Locked devices"
+msgid "Block device"
+msgstr "Låste enheter"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Blokk enhet for filsystemer"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+#, fuzzy
+#| msgid "Locked devices"
+msgid "Block devices"
+msgstr "Låste enheter"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Blokkert"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Binding"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+#, fuzzy
+#| msgid "The filesystem is not mounted."
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "Filsystemet er ikke montert."
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Bundet av"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Bro"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Bro port"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Bro port innstillinger"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Broadcast"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Ødelagt konfigurasjon"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Feilrettingsoppdateringer tilgjengelig"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Feil"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:811
+#, fuzzy
+#| msgid ""
+#| "By changing the password of the SSH key ${key} to the login password of "
+#| "${luser} on ${lhost}, the key will be automatically made available and "
+#| "you can log in to ${rhost} without password in the future."
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Ved å endre passordet til SSH-nøkkelen ${key} til påloggingspassordet på "
+"${luser} på ${lhost}, blir nøkkelen automatisk tilgjengelig, og du kan logge "
+"på ${rhost} uten passord i fremtiden."
+
+#: pkg/static/login.html:61
+msgid "Bypass browser check"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "CPU-sikkerhet"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "CPU-bruk"
+
+#: pkg/metrics/metrics.jsx:1939
+#, fuzzy
+#| msgid "CPU usage"
+msgid "CPU usage/load"
+msgstr "CPU-bruk"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Hurtigbuffer"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "Kan være et vertsnavn, IP-adresse, aliasnavn eller ssh:// URI"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr "Finner ingen logger med den nåværende kombinasjonen av filtre."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Avbryt"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr ""
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+#, fuzzy
+#| msgid "Force reboot"
+msgid "Cancel reboot"
+msgstr "Tving omstart"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Kan ikke aktiveres"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Kan ikke koble til en ukjent vert"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Kan ikke videresende påloggingsinformasjonen"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+"Kan ikke bli med i et domene fordi realmd ikke er tilgjengelig på dette "
+"systemet"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Kan ikke tidsplanlegge hendelse i fortiden"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Kapasitet"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Transportør"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Endre"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+#, fuzzy
+#| msgid "Custom encryption options"
+msgid "Change cryptographic policy"
+msgstr "Tilpassede krypteringsalternativer"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Endre vertsnavn"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Endre navn på iSCSI Initiator"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Endre navn på iSCSI Initiator"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change label"
+msgstr "Endre passfrase"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Endre passfrase"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Endre passord"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Endre ytelsesprofil"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Endre profil"
+
+#: pkg/users/shell-dialog.js:70
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change shell"
+msgstr "Endre passfrase"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Endre systemtid"
+
+#: pkg/shell/hosts_dialog.jsx:809
+#, fuzzy
+#| msgid "Change the password of ${key}."
+msgid "Change the password of $0"
+msgstr "Endre passordet på ${key}."
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Endre innstillingene"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Endrede nøkler er ofte resultatet av reinstallering av operativsystemet. "
+"Imidlertid kan en uventet endring indikere et tredjepartsforsøk på å fange "
+"opp forbindelsen din."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Endring av innstillingene vil bryte forbindelsen til serveren, og gjøre "
+"administrasjonsgrensesnittet utilgjengelig."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Se etter oppdateringer"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Sjekker $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Sjekker IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "Kontrollerer RAID-enheten $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Kontrollerer og reparerer RAID-enhet $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+#, fuzzy
+#| msgid "Checking for package updates..."
+msgid "Checking for $0 package"
+msgstr "Ser etter pakkeoppdateringer ..."
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr ""
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Ser etter nye applikasjoner"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Ser etter pakkeoppdateringer ..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Kontrollerer installert programvare"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Kontrollerer programvarestatus"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr ""
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Klasse"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Rydder opp for $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Fjern 'Kunne ikke starte'"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Fjern alle filtre"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Tøm søket"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+#, fuzzy
+#| msgid "Create devices"
+msgid "Cleartext device"
+msgstr "Opprett enheter"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Klient programvare"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Lukk"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Lukk valgte sider"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Cockpit autentisering er konfigurert feil."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr ""
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit kunne ikke kontakte den angitte verten."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit hadde en uventet intern feil."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit er en serveradministrator som gjør det enkelt å administrere Linux-"
+"serverne dine via en nettleser. Å bytte mellom terminalen og nettverktøyet "
+"er ikke noe problem. En tjeneste startet via Cockpit kan stoppes via "
+"terminalen. På samme måte, hvis det oppstår en feil i terminalen, kan den "
+"sees i Cockpit journal-grensesnittet."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit er et interaktivt administrasjonsgrensesnitt for Linux-server."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit er ikke kompatibel med programvaren på systemet."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit er ikke installert"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit er ikke installert på systemet."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit er perfekt for nye sysadminer, slik at de enkelt kan utføre enkle "
+"oppgaver som lagringsadministrasjon, inspisere journaler og starte og stoppe "
+"tjenester. Du kan overvåke og administrere flere servere samtidig. Bare legg "
+"dem til med et enkelt klikk, så vil maskinene dine se etter kompisene."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+#, fuzzy
+#| msgid "Kernel dump"
+msgid "Collect kernel crash dumps"
+msgstr "Kjerne dump"
+
+#: pkg/metrics/metrics.jsx:1494
+#, fuzzy
+#| msgid "Enable stored metrics"
+msgid "Collect metrics"
+msgstr "Aktiver lagret metrikk"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Farge"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+#, fuzzy
+#| msgid "Comma-separated ports, ranges, and aliases are accepted"
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "Kommaseparerte porter, områder og alias aksepteres"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Kommando"
+
+#: pkg/systemd/timer-dialog.jsx:181
+#, fuzzy
+#| msgid "Unit not found"
+msgid "Command not found"
+msgstr "Enheten ble ikke funnet"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Kommentar"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Kommunikasjonen med tuned feilet"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr ""
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Kompatibel med alle systemer og enheter (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Kompatibel med moderne system og harddisker> 2TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Komprimer krasjdumper for å spare plass"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Komprimering"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Betingelse $0=$1 ble ikke oppfylt"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Betingelse feilet"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Konfigurasjon"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Konfigurerer"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Konfigurerer IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Konfigurerer kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Konfigurere systeminnstillinger"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Bekreft"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+#, fuzzy
+#| msgid "Please confirm deletion of $0"
+msgid "Confirm deletion of $0"
+msgstr "Bekreft sletting av $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Bekreft nøkkel-passord"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Bekreft nytt nøkkel-passord"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Bekreft nytt passord"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+#, fuzzy
+#| msgid "Confirm key password"
+msgid "Confirm password"
+msgstr "Bekreft nøkkel-passord"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Bekreft fjerning av $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+#, fuzzy
+#| msgid "Confirm removal with passphrase"
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Bekreft fjerning med passfrase"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+#, fuzzy
+#| msgid "Please confirm stopping of $0"
+msgid "Confirm stopping of $0"
+msgstr "Bekreft stopping av $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Konflikter"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Koble til automatisk"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Koble til"
+
+#: pkg/static/login.js:660
+#, fuzzy
+#| msgid "Connect to"
+msgid "Connect to:"
+msgstr "Koble til"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Kobler til SETroubleshoot daemon ..."
+
+#: pkg/systemd/services.jsx:291
+#, fuzzy
+#| msgid "Connection failed"
+msgid "Connecting to dbus failed: $0"
+msgstr "Tilkoblingen feilet"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Kobler til maskinen"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Tilkoblingsfeil"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Tilkoblingen feilet"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Tidsavbrudd for tilkoblingen."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "Tilkoblingen går tapt"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Består av"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Kontaktet domene"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Inneholder:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Fortsett"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Fortsett økt"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Kontroll"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Kontroll"
+
+#: pkg/systemd/abrtLog.jsx:128
+#, fuzzy
+#| msgid "Control"
+msgid "Controller"
+msgstr "Kontroll"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Konverterbar"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr ""
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Kopier"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Kopier til utklippstavle"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Kjerne $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+#, fuzzy
+#| msgid "Could not contact {{host}}"
+msgid "Could not contact $0"
+msgstr "Kunne ikke kontakte {{host}}"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Crash dump plassering"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Krasjrapportering"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Krasj system"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Opprett"
+
+#: pkg/storaged/overview/overview.jsx:146
+#, fuzzy
+#| msgid "Create volume group"
+msgid "Create LVM2 volume group"
+msgstr "Opprett volumgruppe"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "Opprett RAID-enhet"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Opprett RAID-enhet"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+#, fuzzy
+#| msgid "Create storage pool"
+msgid "Create Stratis pool"
+msgstr "Opprett lagrings-pool"
+
+#: pkg/shell/hosts_dialog.jsx:793
+#, fuzzy
+#| msgid "Create a new SSH key and authorize it."
+msgid "Create a new SSH key and authorize it"
+msgstr "Opprett en ny SSH-nøkkel og godkjenn den."
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+#, fuzzy
+#| msgid "Creating snapshot of $target"
+msgid "Create a snapshot of filesystem $0"
+msgstr "Oppretter øyeblikksbilde av $target"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+#, fuzzy
+#| msgid "Create new account"
+msgid "Create and mount"
+msgstr "Opprett ny konto"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create new account"
+msgid "Create and start"
+msgstr "Opprett ny konto"
+
+#: pkg/storaged/stratis/pool.jsx:91
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Create filesystem"
+msgstr "Filsystem"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Opprett den"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Opprett logisk volum"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Opprett ny konto"
+
+#: pkg/storaged/stratis/pool.jsx:307
+#, fuzzy
+#| msgid "Create new volume"
+msgid "Create new filesystem"
+msgstr "Opprett nytt volum"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+#, fuzzy
+#| msgid "Create volume group"
+msgid "Create new group"
+msgstr "Opprett volumgruppe"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Opprett nytt logisk volum"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Opprett ny oppgavefil med dette innholdet."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "Opprett nytt logisk volum"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+#, fuzzy
+#| msgid "read only"
+msgid "Create only"
+msgstr "skrivebeskyttet"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Opprett partisjon"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Opprett partisjon på $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Opprett partisjonstabell"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Opprett øyeblikksbilde"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+#, fuzzy
+#| msgid "Create snapshot"
+msgid "Create snapshot and mount"
+msgstr "Opprett øyeblikksbilde"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+#, fuzzy
+#| msgid "Create snapshot"
+msgid "Create snapshot only"
+msgstr "Opprett øyeblikksbilde"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create storage volume"
+msgid "Create storage device"
+msgstr "Opprett lagringsvolum"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Opprett volum"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Opprett tynt volum"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Lag timer"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Opprett volumgruppe"
+
+#: pkg/sosreport/sosreport.jsx:502
+#, fuzzy
+#| msgid "Create"
+msgid "Created"
+msgstr "Opprett"
+
+#: pkg/storaged/jobs-panel.jsx:76
+#, fuzzy
+#| msgid "Creating volume group $target"
+msgid "Creating LVM2 volume group $target"
+msgstr "Oppretter volumgruppe $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "Oppretter RAID-enhet $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+#, fuzzy
+#| msgid "Create VDO device"
+msgid "Creating VDO device"
+msgstr "Opprett VDO-enhet"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Oppretter filsystem på $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Oppretter logisk volum $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Oppretter partisjon $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Oppretter øyeblikksbilde av $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+#, fuzzy
+#| msgid ""
+#| "Creating this VLAN will break the connection to the server, and will make "
+#| "the administration UI unavailable."
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Hvis du oppretter dette VLANet, brytes forbindelsen til serveren, og "
+"administrasjonsgrensesnittet blir utilgjengelig."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Kritisk og over"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+#, fuzzy
+#| msgid "Custom encryption options"
+msgid "Cryptographic policy"
+msgstr "Tilpassede krypteringsalternativer"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr ""
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+#, fuzzy
+#| msgid "Ctrl-Shift-J"
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-J"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Nåværende oppstart"
+
+#: pkg/metrics/metrics.jsx:757
+#, fuzzy
+#| msgid "Current"
+msgid "Current top CPU usage"
+msgstr "Gjeldende"
+
+#: pkg/storaged/dialog.jsx:1178
+#, fuzzy
+#| msgid "Current"
+msgid "Currently in use"
+msgstr "Gjeldende"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Current"
+msgid "Currently not supported"
+msgstr "Gjeldende"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "egendefinert"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+#, fuzzy
+#| msgid "Custom encryption options"
+msgid "Custom cryptographic policy"
+msgstr "Tilpassede krypteringsalternativer"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Tilpassede monteringsalternativer"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Egendefinerte porter"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom path"
+msgid "Custom type"
+msgstr "Egendefinert sti"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Egendefinerte soner"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS-søkedomener"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "DNS-søkedomener $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Daglig"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Farevarsel:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Mørk"
+
+#: pkg/storaged/stratis/pool.jsx:197
+#, fuzzy
+#| msgid "Data used"
+msgid "Data"
+msgstr "Data brukt"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Data brukt"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Deaktiver"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Omdøp logisk volum"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Deaktiverer"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Deaktiverer $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Debug og over"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Reduser med en"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+#, fuzzy
+#| msgid "RAID 4 (dedicated parity)"
+msgid "Dedicated parity (RAID 4)"
+msgstr "RAID 4 (dedikert paritet)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Deduplisering"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr ""
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Forsinkelse"
+
+#: pkg/systemd/timer-dialog.jsx:216
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Delay must be a number"
+msgstr "Størrelsen må være et tall"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Slett"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Slett $0"
+
+#: pkg/users/accounts-list.js:80
+#, fuzzy
+#| msgid "Create new account"
+msgid "Delete account"
+msgstr "Opprett ny konto"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Slett filer"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+#, fuzzy
+#| msgid "Create new account"
+msgid "Delete group"
+msgstr "Opprett ny konto"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Slett"
+
+#: pkg/sosreport/sosreport.jsx:350
+#, fuzzy
+#| msgid "Delete content"
+msgid "Delete report permanently?"
+msgstr "Slett innhold"
+
+#: pkg/networkmanager/network-interface.jsx:116
+#, fuzzy
+#| msgid ""
+#| "Deleting <b>$0</b> will break the connection to the server, and will make "
+#| "the administration UI unavailable."
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Hvis du sletter <b>$0</b> , brytes forbindelsen til serveren, og "
+"administrasjonsgrensesnittet blir utilgjengelig."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Sletter $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+#, fuzzy
+#| msgid "Deleting volume group $target"
+msgid "Deleting LVM2 volume group $target"
+msgstr "Sletter volumgruppe $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+#, fuzzy
+#| msgid "Deleting a volume group will erase all data on it."
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "Sletting av en volumgruppe vil slette alle dataene på den."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+#, fuzzy
+#| msgid "Deleting a partition will delete all data in it."
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "Sletting av en partisjon vil slette alle dataene i den."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "Hvis du sletter et logisk volum, slettes alle dataene i det."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "Sletting av en partisjon vil slette alle dataene i den."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Sletting av en VDO-enhet vil slette alle dataene på den."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on a VDO device."
+msgstr "Sletting av en VDO-enhet vil slette alle dataene på den."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on a volume group."
+msgstr "Sletting av en VDO-enhet vil slette alle dataene på den."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "Sletting av en VDO-enhet vil slette alle dataene på den."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Beskrivelse"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr ""
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr ""
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Detaljer"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Utvikling"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Enhet"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Enhetsfil"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Enheten er skrivebeskyttet"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Tjeneste navn"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Diagnose rapporter"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Katalog"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Katalogen $0 er ikke skrivbar eller eksisterer ikke."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Deaktiver samtidig multitråding"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Deaktiver brannmuren"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Deaktiver tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Deaktivert"
+
+#: pkg/users/account-details.js:276
+#, fuzzy
+#| msgid "Domain administrator password"
+msgid "Disallow interactive password"
+msgstr "Domeneadministrator passord"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Ikke tillat å kjøre (masker)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Koble fra"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Frakoblet"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Disk I/U"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Disken er OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+#, fuzzy
+#| msgid "$0 disk is missing"
+#| msgid_plural "$0 disks are missing"
+msgid "Disk is failing"
+msgstr "$0 disk mangler"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Disk passfrase"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Disker"
+
+#: pkg/metrics/metrics.jsx:792
+#, fuzzy
+#| msgid "Disks"
+msgid "Disks usage"
+msgstr "Disker"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+#, fuzzy
+#| msgid "Dismiss"
+msgid "Dismiss"
+msgstr "Forkast"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+#, fuzzy
+#| msgid "Dismiss"
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Forkast"
+msgstr[1] "Forkast"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+#, fuzzy
+#| msgid "Close selected pages"
+msgid "Dismiss selected alerts"
+msgstr "Lukk valgte sider"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Visnings språk"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+#, fuzzy
+#| msgid "RAID 5 (distributed parity)"
+msgid "Distributed parity (RAID 5)"
+msgstr "RAID 5 (distribuert paritet)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+#, fuzzy
+#| msgid "Not mounted"
+msgid "Do not mount"
+msgstr "Ikke montert"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Ikke monter automatisk ved oppstart"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Dokkingstasjon"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Starter ikke automatisk"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+#, fuzzy
+#| msgid "Do not mount automatically on boot"
+msgid "Does not mount during boot"
+msgstr "Ikke monter automatisk ved oppstart"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Domene"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+#, fuzzy
+#| msgid "Domain"
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Domene"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Domene adresse"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Domene administratornavn"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Domeneadministrator passord"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+#, fuzzy
+#| msgid "Domain $0 could not be contacted"
+msgid "Domain could not be contacted"
+msgstr "Domene $0 kunne ikke kontaktes"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+#, fuzzy
+#| msgid "Domain $0 is not supported"
+msgid "Domain is not supported"
+msgstr "Domene $0 støttes ikke"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Ikke gjenta"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+#, fuzzy
+#| msgid "RAID 6 (double distributed parity)"
+msgid "Double distributed parity (RAID 6)"
+msgstr "RAID 6 (dobbel fordelt paritet)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+#, fuzzy
+#| msgid "Downloaded"
+msgid "Download"
+msgstr "Nedlastet"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Last ned en ny nettleser gratis"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Nedlastet"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Laster ned"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Laster ned $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "DIsk"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Dobbel rangering"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Utvidet partisjon"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Rediger"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Rediger /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Rediger Tang nøkkelserver"
+
+#: pkg/networkmanager/vlan.jsx:91
+#, fuzzy
+#| msgid "VLAN settings"
+msgid "Edit VLAN settings"
+msgstr "VLAN-innstillinger"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:135
+#, fuzzy
+#| msgid "Bond settings"
+msgid "Edit bond settings"
+msgstr "Binding-innstillinger"
+
+#: pkg/networkmanager/bridge.jsx:96
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Edit bridge settings"
+msgstr "Bro-innstillinger"
+
+#: pkg/networkmanager/firewall.jsx:596
+#, fuzzy
+#| msgid "Add services to $0 zone"
+msgid "Edit custom service in $0 zone"
+msgstr "Legg til tjenester i $0-sonen"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Rediger vert"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Rediger verter"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Rediger motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Monteringspunkt"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Rediger regler og soner"
+
+#: pkg/networkmanager/firewall.jsx:595
+#, fuzzy
+#| msgid "Add services"
+msgid "Edit service"
+msgstr "Legg til tjenester"
+
+#: pkg/networkmanager/firewall.jsx:119
+#, fuzzy
+#| msgid "Remove service $0"
+msgid "Edit service $0"
+msgstr "Fjern tjenesten $0"
+
+#: pkg/networkmanager/team.jsx:154
+#, fuzzy
+#| msgid "Team settings"
+msgid "Edit team settings"
+msgstr "Team innstillinger"
+
+#: pkg/users/accounts-list.js:59
+#, fuzzy
+#| msgid "Add services"
+msgid "Edit user"
+msgstr "Legg til tjenester"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Redigering av en nøkkel krever en ledig slot"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Løser ut $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "Innebygd PC"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Tom"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Tom"
+
+#: pkg/users/account-create-dialog.js:201
+#, fuzzy
+#| msgid "Key password"
+msgid "Empty password"
+msgstr "Nøkkel-passord"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Tømmer $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+#, fuzzy
+#| msgid "Enabled"
+msgid "Enable"
+msgstr "Aktivert"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Aktiver eller deaktiver enheten"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Aktiver tjeneste"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Aktiver brannmuren"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Aktivert"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+#, fuzzy
+#| msgid "Installing $0"
+msgid "Enabling $0"
+msgstr "Installerer $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Krypter data"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+#, fuzzy
+#| msgid "Edit Tang keyserver"
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Rediger Tang nøkkelserver"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+#, fuzzy
+#| msgid "Encryption"
+msgid "Encrypt data with a passphrase"
+msgstr "Kryptering"
+
+#: pkg/sosreport/sosreport.jsx:450
+#, fuzzy
+#| msgid "Encrypted $0"
+msgid "Encrypted"
+msgstr "Kryptert $0"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Kryptert $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted partition of $0"
+msgid "Encrypted Stratis pool"
+msgstr "Kryptert partisjon på $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Kryptert logisk volum på $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Kryptert partisjon på $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Kryptering"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Krypteringsalternativer"
+
+#: pkg/sosreport/sosreport.jsx:309
+#, fuzzy
+#| msgid "Encryption"
+msgid "Encryption passphrase"
+msgstr "Kryptering"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+#, fuzzy
+#| msgid "Encryption"
+msgid "Encryption type"
+msgstr "Kryptering"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:309
+#, fuzzy
+#| msgid "Mount point"
+msgid "Endpoint"
+msgstr "Monteringspunkt"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Forbedringsoppdateringer tilgjengelig"
+
+#: pkg/networkmanager/mac.jsx:47
+#, fuzzy
+#| msgid "Invalid IPv4 address"
+msgid "Enter a valid MAC address"
+msgstr "Ugyldig IPv4-adresse"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Hele subnettet"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Oppføring på $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr ""
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Feil"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Feil og over"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Det har oppstått en feil"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "PackageKit er ikke installert"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Feilmelding"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "Feil ved kjøring av semanage for å oppdage systemendringer"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Feil ved lagring av autoriserte nøkler: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Feil under innstilling av SELinux-modus: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Eksempel: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Eksempel: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Utmerket passord"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr ""
+
+#: pkg/users/expiration-dialogs.js:71
+#, fuzzy
+#| msgid "Lock account on $0"
+msgid "Expire account on"
+msgstr "Lås konto på $0"
+
+#: pkg/users/account-details.js:78
+#, fuzzy
+#| msgid "Lock account on $0"
+msgid "Expire account on $0"
+msgstr "Lås konto på $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+#, fuzzy
+#| msgid "port"
+msgid "Export"
+msgstr "port"
+
+#: pkg/metrics/metrics.jsx:1511
+#, fuzzy
+#| msgid "Routed network"
+msgid "Export to network"
+msgstr "Rutet nettverk"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Utvidet informasjon"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Utvidet partisjon"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Feilet"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Kunne ikke legge til maskin: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Kunne ikke legge til port"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Kunne ikke legge til tjeneste"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Kunne ikke legge til sone"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Kunne ikke endre passord"
+
+#: pkg/metrics/metrics.jsx:1487
+#, fuzzy
+#| msgid "Failed to clone VM $0"
+msgid "Failed to configure PCP"
+msgstr "Kunne ikke klone VM $0"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Kunne ikke slette varselet: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Kunne ikke deaktivere tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+#, fuzzy
+#| msgid "Failed to disable tuned profile"
+msgid "Failed to disable tuned profile"
+msgstr "Kunne ikke deaktivere tuned profil"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Kunne ikke redigere maskin: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+#, fuzzy
+#| msgid "Failed to add service"
+msgid "Failed to edit service"
+msgstr "Kunne ikke legge til tjeneste"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+#, fuzzy
+#| msgid "Failed to enable tuned"
+msgid "Failed to enable $0 in firewalld"
+msgstr "Kunne ikke aktivere tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Kunne ikke aktivere tuned"
+
+#: pkg/systemd/logsJournal.jsx:259
+#, fuzzy
+#| msgid "Failed to switch profile"
+msgid "Failed to fetch logs"
+msgstr "Kunne ikke bytte profil"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Kunne ikke laste autoriserte nøkler."
+
+#: pkg/systemd/service-details.jsx:539
+#, fuzzy
+#| msgid "Failed to add port"
+msgid "Failed to load unit"
+msgstr "Kunne ikke legge til port"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Kan ikke omstarte tjenesten"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Kunne ikke lagre endringene i /etc/motd"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+#, fuzzy
+#| msgid "Unable to apply settings: $0"
+msgid "Failed to save settings"
+msgstr "Kan ikke bruke innstillingene: $0"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Kunne ikke starte"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Kunne ikke bytte profil"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+#, fuzzy
+#| msgid "Failed to start"
+msgid "File state"
+msgstr "Kunne ikke starte"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Filsystem"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+#, fuzzy
+#| msgid "Filesystems"
+msgid "Filesystem is locked"
+msgstr "Filsystemer"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Filsystem navn"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Oppretter filsystem på $target"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+#, fuzzy
+#| msgid "The filesystem is already mounted at $0. Proceeding will unmount it."
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr ""
+"Filsystemet er allerede montert på $0. Hvis du fortsetter, demonteres det."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Filtrer etter navn eller beskrivelse"
+
+#: pkg/shell/shell-modals.jsx:134
+#, fuzzy
+#| msgid "Filter services"
+msgid "Filter menu items"
+msgstr "Filtrer tjenester"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Filtrer tjenester"
+
+#: pkg/systemd/logs.jsx:237
+#, fuzzy
+#| msgid "Filter"
+msgid "Filters"
+msgstr "Filter"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Fingeravtrykk"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Brannmur"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Brannmur er ikke tilgjengelig"
+
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Fastvareversjon"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Skriftstørrelse"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Forbudt å kjøre"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Tving endring"
+
+#: pkg/users/delete-group-dialog.js:51
+#, fuzzy
+#| msgid "Delete"
+msgid "Force delete"
+msgstr "Slett"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Tving passordendring"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Format"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Formater $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+#, fuzzy
+#| msgid "Stop and unmount"
+msgid "Format and mount"
+msgstr "Stopp og demonter"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Stop and unmount"
+msgid "Format and start"
+msgstr "Stopp og demonter"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+#, fuzzy
+#| msgid "Format"
+msgid "Format only"
+msgstr "Format"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+#, fuzzy
+#| msgid "Formatting a disk will erase all data on it."
+msgid "Formatting erases all data on a storage device."
+msgstr "Formatering av en disk vil slette alle dataene på den."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr ""
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr ""
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Ledig plass"
+
+#: pkg/systemd/logs.jsx:378
+#, fuzzy
+#| msgid "Clear search"
+msgid "Free-form search"
+msgstr "Tøm søket"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Fredager"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Fullt navn"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+#, fuzzy
+#| msgid "IoT gateway"
+msgid "Gateway"
+msgstr "IoT-gateway"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Generelt"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+#, fuzzy
+#| msgid "General"
+msgid "Generated"
+msgstr "Generelt"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Gå til $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Gå til applikasjon"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Gå til nå"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr ""
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Gruppe"
+
+#: pkg/users/accounts-list.js:238
+#, fuzzy
+#| msgid "Volume group name"
+msgid "Group name"
+msgstr "Volumgruppenavn"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+#, fuzzy
+#| msgid "Group"
+msgid "Groups"
+msgstr "Gruppe"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr ""
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Øk logisk størrelse på $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Øk logisk volum"
+
+#: pkg/storaged/block/resize.jsx:409
+#, fuzzy
+#| msgid "partition"
+msgid "Grow partition"
+msgstr "partisjon"
+
+#: pkg/storaged/stratis/pool.jsx:337
+#, fuzzy
+#| msgid "Grow to take all space"
+msgid "Grow the pool to take all space"
+msgstr "Voks for å ta all plass"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Voks for å ta all plass"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr ""
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Håndholdt"
+
+#: pkg/storaged/drive/drive.jsx:64
+msgid "Hard Disk Drive"
+msgstr ""
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Maskinvareinformasjon"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Helse"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Hjelp"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm key password"
+msgid "Hide confirmation password"
+msgstr "Bekreft nøkkel-passord"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "User password"
+msgid "Hide password"
+msgstr "Bruker passord"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr ""
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+#, fuzzy
+#| msgid "Directory"
+msgid "Home directory"
+msgstr "Katalog"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Vert"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Vertsnøkkelen er feil"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Vertsnavn"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Verter"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Hver time"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Timer"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr ""
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP adresse"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"IP-adresse med ruteprefiks. Separer flere verdier med komma. Eksempel: "
+"192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+#, fuzzy
+#| msgid "IPv4 address"
+msgid "IPv4 addresses"
+msgstr "IPv4- adresse"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4-innstillinger"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6-innstillinger"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Identifikator"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Hvis fingeravtrykket stemmer overens, klikker du på \"Godta nøkkel og logg "
+"inn\". Ellers ikke logg inn og kontakt administratoren din."
+
+#: pkg/shell/hosts_dialog.jsx:480
+#, fuzzy
+#| msgid ""
+#| "If the fingerprint matches, click \"Accept key and connect\". Otherwise, "
+#| "do not connect and contact your administrator."
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Hvis fingeravtrykket stemmer overens, klikker du på \"Godta nøkkel og koble "
+"til\". Ellers må du ikke koble til og kontakte administratoren din."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ignorer"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "Inn"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+#, fuzzy
+#| msgid ""
+#| "In order to allow log in to ${rhost} as ${ruser} without password in the "
+#| "future, use the login password of ${luser} on ${lhost} as the key "
+#| "password, or leave the key password blank."
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"For å tillate pålogging til ${rhost} som ${ruser} uten passord i fremtiden, "
+"bruk påloggingspassordet på ${luser} på ${lhost} som nøkkelpassord, eller la "
+"nøkkelpassordet være tomt."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "I synk"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Inaktiv"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "Opprett logisk volum"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Inkluderte tjenester"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Inkonsistent filsystemmontering"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Øk med en"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Indeks minne"
+
+#: pkg/systemd/services.jsx:254
+#, fuzzy
+#| msgid "direct"
+msgid "Indirect"
+msgstr "direkte"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Info"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Info og over"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+#, fuzzy
+#| msgid "Initializing..."
+msgid "Initialize"
+msgstr "Initialiserer…"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+#, fuzzy
+#| msgid "Initializing..."
+msgid "Initialize disk $0"
+msgstr "Initialiserer…"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+#, fuzzy
+#| msgid "Formatting a disk will erase all data on it."
+msgid "Initializing erases all data on a disk."
+msgstr "Formatering av en disk vil slette alle dataene på den."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Initialiserer…"
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Innsikt: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Installer"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Installer NFS-støtte"
+
+#: pkg/storaged/overview/overview.jsx:121
+#, fuzzy
+#| msgid "Install NFS support"
+msgid "Install Stratis support"
+msgstr "Installer NFS-støtte"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Installer alle oppdateringer"
+
+#: pkg/apps/application-list.jsx:200
+#, fuzzy
+#| msgid "Package information"
+msgid "Install application information"
+msgstr "Pakkeinformasjon"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Installer cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+#, fuzzy
+#| msgid "Install all updates"
+msgid "Install kpatch updates"
+msgstr "Installer alle oppdateringer"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+#, fuzzy
+#| msgid "Install NFS support"
+msgid "Install realmd support"
+msgstr "Installer NFS-støtte"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Installer sikkerhetsoppdateringer"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr "Installer setroubleshoot-server for å feilsøke SELinux-hendelser."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Installer programvare"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Vennligst installer $0-pakken"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Installert"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Installerer"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Installerer $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+#, fuzzy
+#| msgid "Installing $0"
+msgid "Installing $0 would remove $1."
+msgstr "Installerer $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+#, fuzzy
+#| msgid "Installing"
+msgid "Installing packages"
+msgstr "Installerer"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+#, fuzzy
+#| msgid "Interface"
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Grensesnitt"
+msgstr[1] "Grensesnitt"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Grensesnittmedlemmer"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Grensesnitt"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Intern feil"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr ""
+
+#: pkg/systemd/services.jsx:253
+#, fuzzy
+#| msgid "Invalid key"
+msgid "Invalid"
+msgstr "Ugyldig nøkkel"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Ugyldig adresse $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Ugyldig datoformat"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Ugyldig datoformat og ugyldig tidsformat"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Ugyldig utløpsdato"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Ugyldige filtillatelser"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Ugyldig nøkkel"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Ugyldig metrikk $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Ugyldig antall dager"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Ugyldig portnummer"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Ugyldig prefiks $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Ugyldig prefiks eller nettmaske $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Ugyldig område"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Ugyldig tidsformat"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Ugyldig tidssone"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Ugyldig brukernavn eller passord"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "IoT-gateway"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "Kjører sshd på en annen port?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Jobber"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Bli med"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+#, fuzzy
+#| msgid "Join a domain"
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Bli med i et domene"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Bli med i domene"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+#, fuzzy
+#| msgid "Join"
+msgid "Joining"
+msgstr "Bli med"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Å bli med i et domene krever installasjon av realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Å bli med i dette domenet støttes ikke"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr ""
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Journal"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Journaloppføring"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Journaloppføring ikke funnet"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Gå til"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit er ikke installert"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Test kdump-innstillinger"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Hold forbindelsen"
+
+#: pkg/kdump/kdump-view.jsx:581
+#, fuzzy
+#| msgid "Kernel dump"
+msgid "Kernel crash dump"
+msgstr "Kjerne dump"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Kjerne dump"
+
+#: pkg/packagekit/kpatch.jsx:366
+#, fuzzy
+#| msgid "Support is installed."
+msgid "Kernel live patch $0 is active"
+msgstr "Support er installert."
+
+#: pkg/packagekit/kpatch.jsx:373
+#, fuzzy
+#| msgid "Support is installed."
+msgid "Kernel live patch $0 is installed"
+msgstr "Support er installert."
+
+#: pkg/packagekit/kpatch.jsx:299
+#, fuzzy
+#| msgid "Change the settings"
+msgid "Kernel live patch settings"
+msgstr "Endre innstillingene"
+
+#: pkg/packagekit/kpatch.jsx:282
+#, fuzzy
+#| msgid "Kernel Dump"
+msgid "Kernel live patching"
+msgstr "Kjerne dump"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Nøkkel-passord"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Nøkkel-slots med ukjente typer kan ikke redigeres her"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Nøkkelkilde"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Nøkler"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Nøkkelserver"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Nøkkelserver adresse"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "Fjerning av nøkkelserver kan forhindre opplåsing av $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP-nøkkel"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr ""
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "Pool"
+msgid "LVM2 VDO pool"
+msgstr "Pool"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Logisk volum"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Logiske volumer"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volume"
+msgstr "Fysisk volum"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volumes"
+msgstr "Fysisk volum"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+#, fuzzy
+#| msgid "LVM volume group"
+msgid "LVM2 volume group"
+msgstr "LVM volumgruppe"
+
+#: pkg/storaged/utils.js:340
+#, fuzzy
+#| msgid "LVM volume group"
+msgid "LVM2 volume group $0"
+msgstr "LVM volumgruppe"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Siste 24 timer"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Siste 7 dager"
+
+#: pkg/users/accounts-list.js:391
+#, fuzzy
+#| msgid "active"
+msgid "Last active"
+msgstr "aktiv"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "Den siste nøkkel-sloten kan ikke fjernes"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Sist sjekket: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "Den siste nøkkel-sloten kan ikke fjernes"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Siste innlogging"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+#, fuzzy
+#| msgid "Last checked: $0"
+msgid "Last modified: $0"
+msgstr "Sist sjekket: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+#, fuzzy
+#| msgid "Last failed login:"
+msgid "Last successful login:"
+msgstr "Siste feilede pålogging:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Lær mer"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#, fuzzy
+#| msgid "Remove $0"
+msgid "Leave $0"
+msgstr "Fjern $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Forlat domene"
+
+#: pkg/sosreport/sosreport.jsx:317
+#, fuzzy
+#| msgid "Using LUKS encryption"
+msgid "Leave empty to skip encryption"
+msgstr "Bruker LUKS-kryptering"
+
+#: pkg/shell/shell-modals.jsx:63
+#, fuzzy
+#| msgid "GNU LGPL version 2.1"
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "GNU LGPL versjon 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Lys"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Begrens tilgang"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Begrenset tilgang"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"Begrenset tilgangsmodus begrenser administrative rettigheter. Noen deler av "
+"webkonsollet har redusert funksjonalitet."
+
+#: pkg/systemd/abrtLog.jsx:143
+#, fuzzy
+#| msgid "Limit access"
+msgid "Limits"
+msgstr "Begrens tilgang"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Linkovervåking"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr ""
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem"
+msgid "Linux filesystem data"
+msgstr "Demonter filsystemet"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+msgid "Linux swap space"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:670
+#, fuzzy
+#| msgid "Persistent"
+msgid "Listen"
+msgstr "Vedvarende"
+
+#: pkg/networkmanager/wireguard.jsx:242
+#, fuzzy
+#| msgid "Persistent"
+msgid "Listen port"
+msgstr "Vedvarende"
+
+#: pkg/networkmanager/wireguard.jsx:149
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Listen port must be a number"
+msgstr "Størrelsen må være et tall"
+
+#: pkg/systemd/services.jsx:683
+#, fuzzy
+#| msgid "Systemd units"
+msgid "Listing units"
+msgstr "Systemd enheter"
+
+#: pkg/systemd/services.jsx:503
+#, fuzzy
+#| msgid "Last checked: $0"
+msgid "Listing units failed: $0"
+msgstr "Sist sjekket: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Last"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Lastbalansering"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Last tidligere data"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Last tidligere oppføringer"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Lasting av tilgjengelige oppdateringer feilet"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Laster tilgjengelige oppdateringer, vent ..."
+
+#: pkg/systemd/logsJournal.jsx:290
+#, fuzzy
+#| msgid "Load earlier entries"
+msgid "Loading earlier entries"
+msgstr "Last tidligere oppføringer"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Laster nøkler..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Lasting av SSH-nøkler feilet"
+
+#: pkg/systemd/services.jsx:664
+#, fuzzy
+#| msgid "Loading of SSH keys failed"
+msgid "Loading of units failed"
+msgstr "Lasting av SSH-nøkler feilet"
+
+#: pkg/shell/shell-modals.jsx:78
+#, fuzzy
+#| msgid "Loading keys..."
+msgid "Loading packages..."
+msgstr "Laster nøkler..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Laster inn systemendringer ..."
+
+#: pkg/systemd/service.jsx:129
+#, fuzzy
+#| msgid "Login failed"
+msgid "Loading unit failed"
+msgstr "Innlogging feilet"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Laster..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Lokale kontoer"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Lokalt filsystem"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Lokalt monteringspunkt"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "No storage"
+msgid "Local storage"
+msgstr "Ingen lagring"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "lokalt i $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Plassering"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Lås"
+
+#: pkg/users/lock-account-dialog.js:29
+#, fuzzy
+#| msgid "Lock"
+msgid "Lock $0"
+msgstr "Lås"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Lås konto"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Encrypted data"
+msgid "Locked data"
+msgstr "Krypterte data"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Låser $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Logg inn"
+
+#: pkg/shell/hosts_dialog.jsx:763
+#, fuzzy
+#| msgid "Go to $0"
+msgid "Log in to $0"
+msgstr "Gå til $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Logg inn med server brukerkontoen din."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Logg meldinger"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Logg ut"
+
+#: pkg/users/accounts-list.js:68
+#, fuzzy
+#| msgid "Log out"
+msgid "Log user out"
+msgstr "Logg ut"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Logget inn"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Logisk"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Logisk volum (øyeblikksbilde)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Logisk størrelse"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Logisk volum"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Logisk volum (øyeblikksbilde)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Logisk volum på $0"
+
+#: pkg/static/login.js:361
+#, fuzzy
+#| msgid "Last login"
+msgid "Login"
+msgstr "Siste innlogging"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Logg inn igjen"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Innlogging feilet"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr ""
+
+#: pkg/users/account-logs-panel.jsx:73
+#, fuzzy
+#| msgid "history"
+msgid "Login history"
+msgstr "historikk"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr ""
+
+#: pkg/users/logout-account-dialog.js:29
+#, fuzzy
+#| msgid "Slot $0"
+msgid "Logout $0"
+msgstr "Slot $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr ""
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Logger"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr ""
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Lunsjboks"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "RAID-enhet"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "RAID-enhet $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Enheten er skrivebeskyttet"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "Tjenesten kjører"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "RAID-enhet"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Legg til disker"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (anbefalt)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU må være et positivt tall"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Maskin ID"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Fingeravtrykk for maskinens SSH-nøkler"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr ""
+
+#: pkg/systemd/services.jsx:238
+#, fuzzy
+#| msgid "interface"
+msgid "Maintenance"
+msgstr "grensesnitt"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Manage filesystem sizes"
+msgstr "Filsystem"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+#, fuzzy
+#| msgid "Managed interfaces"
+msgid "Manage storage"
+msgstr "Administrerte grensesnitt"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Administrerte grensesnitt"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "Administrere LVMer"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "Administrere NFS-monteringer"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "Administrere RAIDer"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "Administrere VDOer"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "Administrere VLAN"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Administrere brannmur"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Administrere nettverksbindinger"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Administrere nettverksbroer"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Administrere nettverksteam"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Administrere partisjoner"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Administrere fysiske disker"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Administrere tjenester"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Administrere programvareoppdateringer"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Administrere brukerkontoer"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Manuell"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Manuelt"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Markerer $target som feilende"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Masker tjeneste"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Maskert"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Maskering av en tjeneste forhindrer alle avhengige enheter fra å kjøre. "
+"Dette kan ha større innvirkning enn forventet. Vennligst bekreft at du vil "
+"maskere denne enheten."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Maksimal meldingsalder $ max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Optisk disk"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Minne"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Minneteknologi"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Minnebruk"
+
+#: pkg/metrics/metrics.jsx:1939
+#, fuzzy
+#| msgid "Memory usage"
+msgid "Memory usage/swap"
+msgstr "Minnebruk"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr ""
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Melding til innloggede brukere"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "Meldinger relatert til feilen kan finnes i journalen:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Metadata brukt"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Metode"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr ""
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+#, fuzzy
+#| msgid "View details and history"
+msgid "Metrics and history"
+msgstr "Vis detaljer og historikk"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Metrics settings"
+msgstr "Bro-innstillinger"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini-PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Minutt må være et tall mellom 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+#, fuzzy
+#| msgid "Minutes"
+msgid "Minutely"
+msgstr "Minutter"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minutter"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Modus"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Modell"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Endrer $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Mandager"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Overvåkingsintervall"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Overvåker mål"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Månedlig"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Mer info..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Montér"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+#, fuzzy
+#| msgid "Mount point"
+msgid "Mount Point"
+msgstr "Monteringspunkt"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Monter også automatisk ved oppstart"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Monter ved oppstart"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Monter automatisk på $0 ved oppstart"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Monterings-konfigurasjon"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Monter filsystem"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Monter nå"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Monter på $0 nå"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Innstillinger for montering"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Monteringspunkt"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Monteringspunkt kan ikke være tomt"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Monteringspunkt kan ikke være tomt."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Monteringspunktet er allerede brukt for $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Monteringspunktet må starte med \"/\"."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Monter skrivebeskyttet"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Monterer $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgid "Other devices"
+msgid "Multipathed devices"
+msgstr "Andre enheter"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS-montering"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP Server"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Navn"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Navnet kan ikke være tomt."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Navnet kan ikke være tomt."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Navnet kan ikke være lengre enn $0 byte"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Navnet kan ikke være lengre enn $0 tegn"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Navnet kan ikke være lengre enn 127 tegn."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Navnet kan ikke være lengre enn 127 tegn."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Navnet kan ikke inneholde tegnet '$0'."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "Navnet kan ikke inneholde tegnet '$0'."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Navnet kan ikke inneholde mellomrom."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Trenger minst en NTP-server"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Nettverk"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Nettverk I/U"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr ""
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Nettverksenheter og grafer krever NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Nettverkslogger"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Nettverksbruk"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager er ikke installert"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager kjører ikke"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Nettverksbruk"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Nettverk"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Aldri"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+#, fuzzy
+#| msgid "Never lock account"
+msgid "Never expire account"
+msgstr "Lås aldri kontoen"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Passordet utløper aldri"
+
+#: pkg/users/accounts-list.js:173
+#, fuzzy
+#| msgid "Logged in"
+msgid "Never logged in"
+msgstr "Logget inn"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Ny NFS-montering"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Ny vert"
+
+#: pkg/shell/hosts_dialog.jsx:464
+#, fuzzy
+#| msgid "New host"
+msgid "New host: $0"
+msgstr "Ny vert"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Nytt nøkkelpassord"
+
+#: pkg/users/rename-group-dialog.jsx:35
+#, fuzzy
+#| msgid "Rename"
+msgid "New name"
+msgstr "Omdøp"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Ny passfrase"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Nytt passord"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Nytt passord ble ikke godtatt"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Neste"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Nei"
+
+#: pkg/users/group-create-dialog.js:79
+#, fuzzy
+#| msgid "No real name specified"
+msgid "No ID specified"
+msgstr "Ingen virkelige navn angitt"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Ingen SELinux-varsler."
+
+#: pkg/apps/application-list.jsx:198
+#, fuzzy
+#| msgid "No applications installed or available"
+msgid "No applications installed or available."
+msgstr "Ingen applikasjoner installert eller tilgjengelig"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Ingen tilgjengelige slots"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+#, fuzzy
+#| msgid "No disks are available."
+msgid "No block devices are available."
+msgstr "Ingen disker er tilgjengelige."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "Ingen oppstartsenhet funnet"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Ingen transportør"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Ingen konfigurasjon funnet"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Ingen data tilgjengelig"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Ingen data tilgjengelig mellom $0 og $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Ingen forsinkelse"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Ingen beskrivelse tilgjengelig"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Ingen beskrivelse gitt."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "Ingen oppstartsenhet funnet"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Ingen disker er tilgjengelige."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Ingen logger funnet"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "Ingen resultater funnet"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+#, fuzzy
+#| msgid "encryption"
+msgid "No encryption"
+msgstr "kryptering"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Ingen filsystem"
+
+#: pkg/storaged/stratis/pool.jsx:360
+#, fuzzy
+#| msgid "No filesystem"
+msgid "No filesystems"
+msgstr "Ingen filsystem"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Ingen ledige nøkkelslots"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Ingen ledig plass"
+
+#: pkg/storaged/partitions/partition.jsx:79
+#, fuzzy
+#| msgid "Create partition"
+msgid "No free space after this partition"
+msgstr "Opprett partisjon"
+
+#: pkg/users/group-create-dialog.js:62
+#, fuzzy
+#| msgid "No real name specified"
+msgid "No group name specified"
+msgstr "Ingen virkelige navn angitt"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Ingen vertsnøkler funnet."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Ingen installasjonspakke funnet for dette programmet."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Ingen nøkler lagt til"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr ""
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Ingen loggoppføringer"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Ingen logiske volumer"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Ingen logger funnet"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Ingen samsvarende resultater"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Ingen medier satt inn"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Ingen partisjonering"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "Ingen partisjonering"
+
+#: pkg/networkmanager/wireguard.jsx:341
+#, fuzzy
+#| msgid "No keys added"
+msgid "No peers added."
+msgstr "Ingen nøkler lagt til"
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "Fysiske volumer"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Ingen virkelige navn angitt"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Ingen resultater funnet"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Ingen resultater samsvarer med filterkriteriene. Fjern alle filtre for å "
+"vise resultater."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Ingen regel treff"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No storage"
+msgid "No storage found"
+msgstr "Ingen lagring"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Ingen logiske volumer"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Ingen slik fil eller katalog"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Ingen systemendringer"
+
+#: pkg/sosreport/sosreport.jsx:499
+#, fuzzy
+#| msgid "No system modifications"
+msgid "No system reports."
+msgstr "Ingen systemendringer"
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Ingen oppdateringer"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Ingen brukernavn spesifisert"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Ingen"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Ikke en gyldig privat nøkkel"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Ikke autorisert til å deaktivere brannmuren"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Ikke autorisert til å aktivere brannmuren"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Ikke tilgjengelig"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Ikke tilkoblet"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Ikke koblet til Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Ikke koblet til verten"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough free space"
+msgstr "Ikke nok plass til å vokse."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space"
+msgstr "Ikke nok plass til å vokse."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "Ikke nok plass til å vokse."
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Ikke funnet"
+
+#: pkg/packagekit/kpatch.jsx:246
+#, fuzzy
+#| msgid "Installed"
+msgid "Not installed"
+msgstr "Installert"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "Failed to clone VM $0"
+msgid "Not permitted to configure network devices"
+msgstr "Kunne ikke klone VM $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+#, fuzzy
+#| msgid "Failed to clone VM $0"
+msgid "Not permitted to configure realms"
+msgstr "Kunne ikke klone VM $0"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Ikke tillatt å utføre denne handlingen."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Ikke klar"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Ikke registrert"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Kjører ikke"
+
+#: pkg/packagekit/autoupdates.jsx:346
+#, fuzzy
+#| msgid "No NFS mounts set up"
+msgid "Not set up"
+msgstr "Ingen NFS-monteringer er satt opp"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Ikke synkronisert"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Notat"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Notis og over"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Forekom $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Forekom mellom $0 og $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#, fuzzy
+#| msgid "Occurances"
+msgid "Occurrences"
+msgstr "Forekomster"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "Ok"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Gammel passordfrase"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Gammelt passord"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Gammelt passord aksepteres ikke"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "På en montert enhet"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Ved feil"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Når Cockpit er installert, kan den aktiveres med \"systemctl enable --now "
+"cockpit.socket\"."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Bare $0 av $1 brukes."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Bare biokstaver, tall,:, _,. , @ , - er tillatt"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Bare nødsituasjon"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Operasjon '$operation' på $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Alternativer"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Eller bruk en nettleser som følger med"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Annen"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Andre alternativer"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Ut"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Oversikt"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+#, fuzzy
+#| msgid "Overview"
+msgid "Overwrite"
+msgstr "Oversikt"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+#, fuzzy
+#| msgid "Overwrite existing data with zeros"
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Overskriv eksisterende data med nuller"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+#, fuzzy
+#| msgid "ID"
+msgid "PID"
+msgstr "ID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Pakke cockpit-pcp mangler i metrikk-historikken"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Pakkeinformasjon"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit krasjet"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit er ikke installert"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit rapporterte feilkode $ 0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Pakker"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Sidenavn"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Del av"
+
+#: pkg/networkmanager/interfaces.js:1385
+#, fuzzy
+#| msgid "Part of "
+msgid "Part of $0"
+msgstr "Del av "
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Partisjon"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Partisjon på $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+#, fuzzy
+#| msgid "Volume size is $0. Content size is $1."
+msgid "Partition size is $0. Content size is $1."
+msgstr "Volumstørrelsen er $0. Innholdsstørrelsen er $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Partisjonering"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+#, fuzzy
+#| msgid "Partition"
+msgid "Partitions"
+msgstr "Partisjon"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Passiv"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Passfrase"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+#, fuzzy
+#| msgid "Passphrase cannot be empty"
+msgid "Passphrase can not be empty"
+msgstr "Passfrasen kan ikke være tom"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Passfrasen kan ikke være tom"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+#, fuzzy
+#| msgid "Passphrase cannot be empty"
+msgid "Passphrase from any other key slot"
+msgstr "Passfrasen kan ikke være tom"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Fjerning av passfrase kan forhindre opplåsing av $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Passfraser stemmer ikke overens"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Passord"
+
+#: pkg/shell/credentials.jsx:259
+#, fuzzy
+#| msgid "Solution applied successfully"
+msgid "Password changed successfully"
+msgstr "Løsningen ble brukt"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Passord utløper"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Password is longer than 256 characters"
+msgstr "Navnet kan ikke være lengre enn 127 tegn."
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Passord er ikke akseptabelt"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Passordet er for svakt"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Passord må endres"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Passord ikke akseptert"
+
+#: pkg/shell/credentials.jsx:274
+#, fuzzy
+#| msgid "Password"
+msgid "Password tip"
+msgstr "Passord"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Lim inn"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+#, fuzzy
+#| msgid "Internal error"
+msgid "Paste error"
+msgstr "Intern feil"
+
+#: pkg/networkmanager/wireguard.jsx:215
+#, fuzzy
+#| msgid "Use existing"
+msgid "Paste existing key"
+msgstr "Bruk eksisterende"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Lim inn innholdet i den offentlige SSH-nøkkelfilen din her"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Sti"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Sti kostnad"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Sti kostnad $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Sti på server"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Sti på server kan ikke være tom."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Stien på serveren må starte med \"/\"."
+
+#: pkg/users/account-create-dialog.js:88
+#, fuzzy
+#| msgid "Directory"
+msgid "Path to directory"
+msgstr "Katalog"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Sti til fil"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Stier"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Pause"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Ytelsesprofil"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Perifert chassis"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Permanent"
+
+#: pkg/users/delete-group-dialog.js:33
+#, fuzzy
+#| msgid "Delete $0 volume"
+#| msgid_plural "Delete $0 volumes"
+msgid "Permanently delete $0 group?"
+msgstr "Slett $0 volum"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Delete $0 volume"
+#| msgid_plural "Delete $0 volumes"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Slett $0 volum"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Delete $0 volume"
+#| msgid_plural "Delete $0 volumes"
+msgid "Permanently delete subvolume $0?"
+msgstr "Slett $0 volum"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Ingen tilgang"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr ""
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Fysisk"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "Physical Volumes"
+msgstr "Fysiske volumer"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Fysiske volumer"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "Fysiske volumer kan ikke endres her."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Velg dato"
+
+#: pkg/systemd/service-details.jsx:184
+#, fuzzy
+#| msgid "Systemd units"
+msgid "Pin unit"
+msgstr "Systemd enheter"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Ping-intervall"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Ping mål"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr ""
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Pizzaboks"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Autentiser for å få administrativ tilgang"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Aktiver JavaScript for å bruke Web konsollet."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Vennligst installer $0-pakken"
+
+#: pkg/packagekit/updates.jsx:1492
+#, fuzzy
+#| msgid "Please reload the page after resolving the issue."
+msgid "Please resolve the issue and reload this page."
+msgstr "Last siden på nytt etter å ha løst problemet."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Angi en utløpsdato"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Vennligst angi verten du vil koble til"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr ""
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Pool for tynne logiske volumer"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Pool for tynt provisjonerte volumer"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Pool for tynt provisjonerte volumer"
+
+#: pkg/storaged/stratis/pool.jsx:464
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Pool passphrase"
+msgstr "Gammel passordfrase"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Port"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Bærbar"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Porter"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Opprett partisjon"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Prefikslengde"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Prefikslengde eller nettmaske"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Forbereder"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Til stede"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+#, fuzzy
+#| msgid "Preserve"
+msgid "Preserve"
+msgstr "Bevar"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "\"Pent\" vertsnavn"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Forrige oppstart"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Primær"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Prioritet"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Prioritet $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+#, fuzzy
+#| msgid "Private"
+msgid "Private key"
+msgstr "Privat"
+
+#: pkg/shell/superuser.jsx:194
+#, fuzzy
+#| msgid "Domain administrator name"
+msgid "Problem becoming administrator"
+msgstr "Domene administratornavn"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Problemdetaljer"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Problem info"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr ""
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Prosjektets nettsted"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Spørring via passwd ble tidsavbrutt"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Spørring via ssh-add ble tidsavbrutt"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Spørring via ssh-keygen ble tidsavbrutt"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Videreformidler omlasting til"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Offentlig nøkkel"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Formål"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (stripe)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (speil)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (stripe av speil)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (dedikert paritet)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (distribuert paritet)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (dobbel fordelt paritet)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID chassis"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID-nivå"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr ""
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Tilfeldig"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Les"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Les mer…"
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Skrivebeskyttet"
+
+#: pkg/storaged/plot.jsx:96
+#, fuzzy
+#| msgid "Reload"
+msgid "Reading"
+msgstr "Last på nytt"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Lesing..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Klar"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Klar"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Faktisk vertsnavn"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Faktisk vertsnavn kan bare inneholde små bokstaver, sifre, bindestreker og "
+"punktum (med befolket underdomener)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Faktisk vertsnavn må være på 64 tegn eller mindre"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+#, fuzzy
+#| msgid "Save and reboot"
+msgid "Reapply and reboot"
+msgstr "Lagre og start på nytt"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Omstart"
+
+#: pkg/packagekit/updates.jsx:614
+#, fuzzy
+#| msgid "Reboot recommended"
+msgid "Reboot after completion"
+msgstr "Omstart anbefales"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Omstart anbefales"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Omstart systemet..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Mottar"
+
+#: pkg/static/login.html:165
+#, fuzzy
+#| msgid "Edit hosts"
+msgid "Recent hosts"
+msgstr "Rediger verter"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr ""
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Koble til på nytt"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Gjenoppretter"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "Gjenoppretter RAID enhet $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Oppdaterer pakkeinformasjon"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Nekter å koble til. Verten er ukjent"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Nekter å koble til. Vertsnøkkel samsvarer ikke"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Nekter å koble til. Vertsnøkkel er ukjent"
+
+#: pkg/networkmanager/wireguard.jsx:224
+#, fuzzy
+#| msgid "General"
+msgid "Regenerate"
+msgstr "Generelt"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+#, fuzzy
+#| msgid "Generating report"
+msgid "Regenerating initrd"
+msgstr "Genererer rapport"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Registrer…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Last på nytt"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Last videreformidlet fra"
+
+#: pkg/systemd/services.jsx:233
+#, fuzzy
+#| msgid "Reload"
+msgid "Reloading"
+msgstr "Last på nytt"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Last inn tilstanden på gjenværende tjenestene"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:465
+#, fuzzy
+#| msgid "Remove zone $0"
+msgid "Remote over FTP"
+msgstr "Fjern sone $0"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remove zone $0"
+msgid "Remote over NFS, $0"
+msgstr "Fjern sone $0"
+
+#: pkg/kdump/kdump-view.jsx:467
+#, fuzzy
+#| msgid "Remove zone $0"
+msgid "Remote over SFTP"
+msgstr "Fjern sone $0"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remove zone $0"
+msgid "Remote over SSH, $0"
+msgstr "Fjern sone $0"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr ""
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Fjern"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Fjern $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Fjern $0-tjenesten fra $1-sonen"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Fjern $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+#, fuzzy
+#| msgid "Remove Tang keyserver"
+msgid "Remove Tang keyserver?"
+msgstr "Fjern Tang nøkkelserver"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Fjern enheten"
+
+#: pkg/static/login.js:634
+#, fuzzy
+#| msgid "Remove device"
+msgid "Remove host"
+msgstr "Fjern enheten"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+#, fuzzy
+#| msgid "Remove device"
+msgid "Remove item"
+msgstr "Fjern enheten"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Remove missing physical volumes?"
+msgstr "Fjerner fysisk volum fra $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+#, fuzzy
+#| msgid "Remove passphrase in $0?"
+msgid "Remove passphrase in key slot $0?"
+msgstr "Fjern passfrasen i $0?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+#, fuzzy
+#| msgid "Remove passphrase"
+msgid "Remove passphrase?"
+msgstr "Fjern passfrasen"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Fjern tjenesten $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Fjern sone $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Fjerner"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Fjerner $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+#, fuzzy
+#| msgid ""
+#| "Removing <b>$0</b> will break the connection to the server, and will make "
+#| "the administration UI unavailable."
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Hvis du fjerner <b>$0</b> , brytes forbindelsen til serveren og "
+"administrasjonsgrensesnittet blir utilgjengelig."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "Fjerner $target fra RAID-enhet"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Fjerner fysisk volum fra $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Fjerning av cockpit-tjenesten kan føre til at web konsollet blir "
+"utilgjengelig. Forsikre deg om at denne sonen ikke gjelder for din nåværende "
+"web konsoll forbindelse."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "Hvis du fjerner sonen, fjernes alle tjenester i den."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Omdøp"
+
+#: pkg/storaged/stratis/pool.jsx:168
+#, fuzzy
+#| msgid "Create storage pool"
+msgid "Rename Stratis pool"
+msgstr "Opprett lagrings-pool"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Rename filesystem"
+msgstr "Filsystem"
+
+#: pkg/users/group-actions.jsx:39
+#, fuzzy
+#| msgid "Rename volume group"
+msgid "Rename group"
+msgstr "Omdøp volumgruppe"
+
+#: pkg/users/rename-group-dialog.jsx:60
+#, fuzzy
+#| msgid "Volume group $0"
+msgid "Rename group $0"
+msgstr "Volumgruppe $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Omdøp logisk volum"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Omdøp volumgruppe"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Omdøper $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Repair logical volume $0"
+msgstr "Omdøp logisk volum"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Reparerer $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Gjenta"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Gjenta månedlig"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Gjenta passfrasen"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Gjenta ukentlig"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Rapport"
+
+#: pkg/sosreport/sosreport.jsx:306
+#, fuzzy
+#| msgid "Report"
+msgid "Report label"
+msgstr "Rapport"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Rapporter til ABRT Analytics"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Rapportert; ingen lenker tilgjengelig"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Rapportering feilet"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Rapportering ble kansellert"
+
+#: pkg/sosreport/sosreport.jsx:496
+#, fuzzy
+#| msgid "Reports:"
+msgid "Reports"
+msgstr "Rapporter:"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Rapporter:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Krev passordendring hver $0 dager"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Krev passordendring på $0"
+
+#: pkg/users/account-create-dialog.js:119
+#, fuzzy
+#| msgid "Require password change on $0"
+msgid "Require password change on first login"
+msgstr "Krev passordendring på $0"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Kreves av"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Kreves av "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Krever"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Krever administrasjonstilgang for å redigere"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Nødvendig"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Reservert minne"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Tilbakestill"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Tilbakestill passord"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Å endre størrelse på et kryptert filsystem krever at du låser opp disken. "
+"Oppgi en aktuell passfrase."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Omstart"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Omstart tjenester"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Omstart tjenester..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Omstarter"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Gjenoppretter tilkoblingen"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Gjenoppta"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+#, fuzzy
+#| msgid "Using LUKS encryption"
+msgid "Reuse existing encryption"
+msgstr "Bruker LUKS-kryptering"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+#, fuzzy
+#| msgid "Custom encryption options"
+msgid "Review cryptographic policy"
+msgstr "Tilpassede krypteringsalternativer"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Ruter"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:293
+#, fuzzy
+#| msgid "View report"
+msgid "Run new report"
+msgstr "Vis rapport"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+#, fuzzy
+#| msgid "Report"
+msgid "Run report"
+msgstr "Rapport"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr ""
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Kjører"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+#, fuzzy
+#| msgid "time"
+msgid "Runtime"
+msgstr "tid"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux tilgangskontroll feil"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux er deaktivert på systemet"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux er deaktivert på systemet."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux-policy"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "SELinux-systemstatus er ukjent."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "Feilsøking av SELinux"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "SMART selvtest av $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH-nøkkel"
+
+#: pkg/kdump/kdump-view.jsx:164
+#, fuzzy
+#| msgid "ssh key isn't a path"
+msgid "SSH key isn't a path"
+msgstr "ssh-nøkkel er ikke en bane"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH-nøkler"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr ""
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr ""
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP maksimal meldingsalder"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP-prioritet"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Safari-brukere må importere og stole på sertifikatet for den selvsignerende "
+"CA:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Lørdager"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Lagre"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Lagre og start på nytt"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Lagre endringer"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Spar plass ved å komprimere individuelle blokker med LZ4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Spar plass ved å lagre identiske datablokker bare en gang"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"For å lagre en ny passfrase, må du låse opp disken. Oppgi en aktuell "
+"passfrase."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr ""
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr ""
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Søk"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+#, fuzzy
+#| msgid "DNS search domains"
+msgid "Search domain"
+msgstr "DNS-søkedomener"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr ""
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:280
+#, fuzzy
+#| msgid "Minute needs to be a number between 0-59"
+msgid "Second needs to be a number between 0-59"
+msgstr "Minutt må være et tall mellom 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Sekunder"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Secure Shell nøkler"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Sikkerhetsoppdateringer tilgjengelig"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Bare sikkerhetsoppdateringer"
+
+#: pkg/packagekit/autoupdates.jsx:365
+#, fuzzy
+#| msgid "will be applied $0 at $1"
+msgid "Security updates will be applied $0 at $1"
+msgstr "vil bli brukt $0 til $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Velg"
+
+#: pkg/systemd/logs.jsx:321
+#, fuzzy
+#| msgid "Identifier"
+msgid "Select a identifier"
+msgstr "Identifikator"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+#, fuzzy
+#| msgid "Select"
+msgid "Select method"
+msgstr "Velg"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Send"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Sender"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Serienummer"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Server"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Serveradresse"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Serveradressen kan ikke være tom."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Serveren kan ikke være tom."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Serveren har lukket forbindelsen."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Server programvare"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Tjeneste"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Tjenesten har en feil"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Tjenestelogger"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Tjenester"
+
+#: pkg/storaged/dialog.jsx:1153
+#, fuzzy
+#| msgid "Service is starting"
+msgid "Services using the location"
+msgstr "Tjenesten starter"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Økt"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Økten er i ferd med å utløpe"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Sett"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Sett vertsnavn"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Opprett partisjon på $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Angi passord"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Sett tid"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Satt til"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Sett opp"
+
+#: pkg/users/password-dialogs.js:245
+#, fuzzy
+#| msgid "Set password"
+msgid "Set weak password"
+msgstr "Angi passord"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Innstillingen avviker fra den konfigurerte tilstanden og vil tilbakestilles "
+"ved neste oppstart."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Setter opp"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:919
+#, fuzzy
+#| msgid "Setting up"
+msgid "Settings"
+msgstr "Setter opp"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Alvorlighetsgrad"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Delt"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+#, fuzzy
+#| msgid "shell"
+msgid "Shell"
+msgstr "skall"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Shell-skript"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "Vis alle tråder"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Vis alle tråder"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm key password"
+msgid "Show confirmation password"
+msgstr "Bekreft nøkkel-passord"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Vis fingeravtrykk"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "New password"
+msgid "Show password"
+msgstr "Nytt passord"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Krymp"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Krymp logisk volum"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+#, fuzzy
+#| msgid "partition"
+msgid "Shrink partition"
+msgstr "partisjon"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Krymp volum"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Slå av"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Slå av"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr ""
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Størrelse"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "Størrelse kan ikke være negativ"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "Størrelsen kan ikke være null"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Størrelsen er for stor"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Størrelsen må være et tall"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Størrelsen må være minst $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr ""
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Gå til innhold"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Slot"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Slot $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+#, fuzzy
+#| msgid "Snapshots"
+msgid "Snapshot"
+msgstr "Øyeblikksbilder"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr ""
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Programvare oppdateringer"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Programvarebasert løsning hjelper til med å forhindre CPU-"
+"sikkerhetsproblemer. Disse begrensningene har bivirkningen at ytelsen "
+"reduseres. Endre disse innstillingene på egen risiko."
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Løsningen ble brukt"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Løsningen feilet"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Løsninger"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr "Noen andre programmer bruker for øyeblikket pakkebehandling, vent ..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Noe programvare må startes på nytt manuelt"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Sortert fra minst til mest betrodd"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Plassbesparende datamaskin"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Spanning tree protokoll"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Spanning tree protokoll (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Reserve"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Spesifikk tid"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Hastighet"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Stabil"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Start"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Start og aktiver"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr ""
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Start tjenesten"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr ""
+
+#: pkg/users/account-logs-panel.jsx:77
+#, fuzzy
+#| msgid "Start"
+msgid "Started"
+msgstr "Start"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "Starter RAID-enhet $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr ""
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Tilstand"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Statisk"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Status"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr ""
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Stopp"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Stopp og deaktiver"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Stopp og fjern"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Stopp og demonter"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Stopp enhet"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Slutt å redigere verter"
+
+#: pkg/sosreport/sosreport.jsx:275
+#, fuzzy
+#| msgid "Create report"
+msgid "Stop report"
+msgstr "Opprett rapport"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "Stopper RAID-enhet $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr ""
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Lagring"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "Lagring kan ikke administreres på dette systemet."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Lagringslogger"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr ""
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr ""
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "$0 blokk enhet"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block devices"
+msgstr "$0 blokk enhet"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "$0 filsystemer kan ikke gjøres mindre."
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Stratis filesystem"
+msgstr "Filsystem"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Stratis filesystems"
+msgstr "Filsystem"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Filsystem"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Storage pools"
+msgid "Stratis pool"
+msgstr "Lagrings-pooler"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:97
+#, fuzzy
+#| msgid "Set password"
+msgid "Strong password"
+msgstr "Angi passord"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr ""
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr ""
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr ""
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr ""
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr ""
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "Volumet kunne ikke opprettes"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+#, fuzzy
+#| msgid "Copy to clipboard"
+msgid "Successfully copied to clipboard"
+msgstr "Kopier til utklippstavle"
+
+#: pkg/storaged/crypto/tang.jsx:118
+#, fuzzy
+#| msgid "Copy to clipboard"
+msgid "Successfully copied to clipboard!"
+msgstr "Kopier til utklippstavle"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Søndager"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Swap"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "$0 filsystemer kan ikke endres størrelse på her."
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Swap ut"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Slå av $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Slå av $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Slå på $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+#, fuzzy
+#| msgid "Turn on administrative access"
+msgid "Switch to administrative access"
+msgstr "Slå på administrativ tilgang"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Bytt til begrenset tilgang"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+#, fuzzy
+#| msgid ""
+#| "Switching off <b>$0</b> will break the connection to the server, and will "
+#| "make the administration UI unavailable."
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Hvis du slår av <b>$0</b>, brytes forbindelsen til serveren, og gjør "
+"administrasjonsgrensesnittet utilgjengelig."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+#, fuzzy
+#| msgid ""
+#| "Switching on <b>$0</b> will break the connection to the server, and will "
+#| "make the administration UI unavailable."
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Hvis du slår på <b>$0</b>, brytes forbindelsen til serveren, og gjør "
+"administrasjonsgrensesnittet utilgjengelig."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Synkronisert"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Synkronisert med $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Synkroniserer"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "Synkroniserer RAID-enhet $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "System"
+
+#: pkg/sosreport/sosreport.jsx:520
+#, fuzzy
+#| msgid "System modifications"
+msgid "System diagnostics"
+msgstr "Systemendringer"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Systeminformasjon"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Systemet er oppdatert"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Systemendringer"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Systemtid"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemd enheter"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Nettbrett"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang nøkkelserver"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Mål"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Mål"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Team"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Team port"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Team Port innstillinger"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminal"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Avslutt økt"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Test konfigurasjon"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Test er bare tilgjengelig mens kdump-tjenesten kjører."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Test kdump-innstillinger"
+
+#: pkg/kdump/kdump-view.jsx:374
+#, fuzzy
+#| msgid ""
+#| "This will test kdump settings by crashing the kernel and thereby the "
+#| "system. Depending on the settings, the system may not automatically "
+#| "reboot and the process may take a while."
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Dette vil teste kdump-innstillinger ved å krasje kjernen og derved systemet. "
+"Avhengig av innstillingene, kan det hende at systemet ikke starter på nytt "
+"automatisk, og prosessen kan ta en stund."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Tester tilkobling"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+#, fuzzy
+#| msgid "$0 is not available from any repository."
+msgid "The $0 package is not available from any repository."
+msgstr "$0 er ikke tilgjengelig fra noe depot."
+
+#: pkg/storaged/overview/overview.jsx:122
+#, fuzzy
+#| msgid "The $0 package must be installed to create VDO devices."
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "$0-pakken må være installert for å opprette VDO-enheter."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+#, fuzzy
+#| msgid "The $0 package must be installed to create VDO devices."
+msgid "The $0 package must be installed."
+msgstr "$0-pakken må være installert for å opprette VDO-enheter."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+#, fuzzy
+#| msgid "The $0 package must be installed to create VDO devices."
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "$0-pakken må være installert for å opprette VDO-enheter."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP-adressen eller vertsnavnet kan ikke inneholde mellomrom."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "RAID-matrisen er i degradert tilstand"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr "RAID-enheten må kjøre for å kunne fjerne disker."
+
+#: pkg/shell/hosts_dialog.jsx:828
+#, fuzzy
+#| msgid ""
+#| "The SSH key ${key} of ${luser} on ${lhost} will be added to the ${afile} "
+#| "file of ${ruser} on ${rhost}."
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+"SSH-nøkkelen ${key} på ${luser} på ${lhost} vil bli lagt til ${afile}-filen "
+"på ${ruser} på ${rhost}."
+
+#: pkg/shell/hosts_dialog.jsx:865
+#, fuzzy
+#| msgid ""
+#| "The SSH key {{#strong}}{{key}}{{/strong}} will be made available for the "
+#| "remainder of the session and will be available for login to other hosts "
+#| "as well."
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"SSH-nøkkelen {{#strong}}{{key}}{{/strong}} blir gjort tilgjengelig for "
+"resten av økten og vil også være tilgjengelig for pålogging til andre verter."
+
+#: pkg/shell/hosts_dialog.jsx:773
+#, fuzzy
+#| msgid ""
+#| "The SSH key for logging in to {{#strong}}{{full_address}}{{/strong}} is "
+#| "protected. You can log in with either your login password or by providing "
+#| "the password of the key at {{#strong}}{{key}}{{/strong}}."
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"SSH-nøkkelen for pålogging til {{#strong}}{{full_address}}{{/strong}} er "
+"beskyttet. Du kan logge på med enten påloggingspassordet ditt eller ved å "
+"oppgi passordet til nøkkelen på {{#strong}}{{key}}{{/strong}}."
+
+#: pkg/shell/hosts_dialog.jsx:778
+#, fuzzy
+#| msgid ""
+#| "The SSH key for logging in to {{#strong}}{{full_address}}{{/strong}} is "
+#| "protected. You can log in with either your login password or by providing "
+#| "the password of the key at {{#strong}}{{key}}{{/strong}}."
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"SSH-nøkkelen for pålogging til {{#strong}}{{full_address}}{{/strong}} er "
+"beskyttet. Du kan logge på med enten påloggingspassordet ditt eller ved å "
+"oppgi passordet til nøkkelen på {{#strong}}{{key}}{{/strong}}."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr "Kontoen '$0' vil bli tvunget til å endre passordet ved neste pålogging"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Cockpit tjenesten er automatisk inkludert"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"Den konfigurerte tilstanden er ukjent, den kan endres ved neste oppstart."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"Opprettelsen av denne VDO-enheten ble ikke fullført, og enheten kan ikke "
+"brukes."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr "Den påloggede brukeren har ikke lov til å se informasjon om nøkler."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:368
+#, fuzzy
+#| msgid "The $0 could not be deleted"
+msgid "The file $0 will be deleted."
+msgstr "$0 kunne ikke slettes"
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "Filsystemet har ikke noe permanent monteringspunkt."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "Filsystemet har ikke noe permanent monteringspunkt."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Filsystemet er konfigurert til å monteres automatisk ved oppstart, men "
+"krypteringsbeholderen blir ikke låst opp på det tidspunktet."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Filsystemet er for øyeblikket montert, men blir ikke montert etter neste "
+"oppstart."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Filsystemet er for øyeblikket montert på $0, men vil bli montert på $1 ved "
+"neste oppstart."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Filsystemet er for øyeblikket montert på $0, men blir ikke montert etter "
+"neste oppstart."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Filsystemet er foreløpig ikke montert, men vil bli montert på neste oppstart."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Filsystemet er ikke montert."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:494
+#, fuzzy
+#| msgid "Show fingerprints"
+msgid "The fingerprint should match:"
+msgstr "Vis fingeravtrykk"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Følgende tjeneste startes på nytt:"
+msgstr[1] "Følgende tjenester startes på nytt:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr ""
+
+#: pkg/users/group-create-dialog.js:83
+#, fuzzy
+#| msgid "MTU must be a positive number"
+msgid "The group ID must be positive integer"
+msgstr "MTU må være et positivt tall"
+
+#: pkg/users/group-create-dialog.js:66
+#, fuzzy
+#| msgid ""
+#| "The user name can only consist of letters from a-z, digits, dots, dashes "
+#| "and underscores."
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Brukernavnet kan bare bestå av bokstaver fra a-z, sifre, prikker, "
+"bindestreker og understrekninger."
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Nøkkelpassordet kan ikke være tomt"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Nøkkelpassordene stemmer ikke overens"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "Nøkkelen du oppga var ikke gyldig."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Den siste nøkkel-sloten kan ikke fjernes"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1367
+#, fuzzy
+#| msgid "The following service will be restarted:"
+#| msgid_plural "The following services will be restarted:"
+msgid "The listed services will be forcefully stopped."
+msgstr "Følgende tjeneste startes på nytt:"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "Den påloggede brukeren har ikke lov til å se systemendringer"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "Maskinen starter på nytt"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Det nye nøkkelpassordet kan ikke være tomt"
+
+#: pkg/shell/hosts_dialog.jsx:679
+#, fuzzy
+#| msgid "The key password can not be empty"
+msgid "The password can not be empty"
+msgstr "Nøkkelpassordet kan ikke være tomt"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Passordene samsvarer ikke"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Passordene samsvarer ikke."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"Det resulterende fingeravtrykket er greit å dele via offentlige metoder, "
+"inkludert e-post."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Serveren nektet å autentisere '$0' ved hjelp av passordgodkjenning, og ingen "
+"andre støttede godkjenningsmetoder er tilgjengelige."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr "Serveren nektet å godkjenne ved hjelp av de støttede metodene."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr "Brukeren $0 har ikke lov til å endre CPU-sikkerhetsreduksjoner"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+#, fuzzy
+#| msgid "The user $0 is not permitted to change cpu security mitigations"
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "Brukeren $0 har ikke lov til å endre CPU-sikkerhetsreduksjoner"
+
+#: pkg/kdump/kdump-view.jsx:505
+#, fuzzy
+#| msgid "The user $0 is not permitted to change cpu security mitigations"
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "Brukeren $0 har ikke lov til å endre CPU-sikkerhetsreduksjoner"
+
+#: pkg/users/account-details.js:469
+#, fuzzy
+#| msgid "The user must log out and log back in to fully change roles."
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr "Brukeren må logge ut og logge inn for å endre rolle fullstendig."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Brukernavnet kan bare bestå av bokstaver fra a-z, sifre, prikker, "
+"bindestreker og understrekninger."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"Nettleserkonfigurasjonen forhindrer Cockpit i å kjøre (utilgjengelig $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Det er for øyeblikket ingen aktive sider"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "Det er ingen aktive tjenester i denne sonen"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Det er ingen autoriserte offentlige nøkler for denne kontoen."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Det oppsto en uventet feil under tilkoblingen til maskinen."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr ""
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Tynt logisk volum"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Pool for tynt provisjonerte volumer"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr "Denne NFS-monteringen er i bruk, og bare alternativene kan endres."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Denne enheten kan ikke administreres her."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Denne enheten kan ikke administreres her."
+
+#: pkg/storaged/dialog.jsx:1130
+#, fuzzy
+#| msgid "This device is currently used for VDO devices."
+msgid "This device is currently in use."
+msgstr "Denne enheten brukes for øyeblikket til VDO-enheter."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Dette feltet kan ikke være tomt"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "Denne verten vil starte på nytt etter at oppdateringer er installert."
+
+#: pkg/sosreport/sosreport.jsx:303
+#, fuzzy
+#| msgid "The collected information will be stored locally on the system."
+msgid "This information is stored only on the system."
+msgstr "Den innsamlede informasjonen lagres lokalt på systemet."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "Dette logiske volumet brukes ikke fullstendig av innholdet."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Denne maskinen er allerede lagt til."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Dette kan ta en stund"
+
+#: pkg/storaged/partitions/partition.jsx:204
+#, fuzzy
+#| msgid "This logical volume is not completely used by its content."
+msgid "This partition is not completely used by its content."
+msgstr "Dette logiske volumet brukes ikke fullstendig av innholdet."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:348
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "This pool is in a degraded state."
+msgstr "RAID-matrisen er i degradert tilstand"
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Dette systemet er ikke registrert"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Dette systemet bruker en tilpasset profil"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Dette systemet bruker den anbefalte profilen"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Dette brukernavnet finnes allerede"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr ""
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr "Denne nettleseren er for gammel til å kjøre web konsollet (mangler $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Dette vil tillate deg å logge på uten passord i fremtiden."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Denne sonen inneholder cockpit-tjenesten. Forsikre deg om at denne sonen "
+"ikke gjelder for din nåværende web konsoll forbindelse."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Torsdager"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Tid"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Tidssone"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Oppretting av timer feilet"
+
+#: pkg/systemd/service-details.jsx:725
+#, fuzzy
+#| msgid "Timer creation failed"
+msgid "Timer deletion failed"
+msgstr "Oppretting av timer feilet"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Timere"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Tips: Gjør at nøkkelpassordet ditt samsvarer med påloggingspassordet ditt "
+"for automatisk autentisering mot andre systemer."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"For å sikre at forbindelsen din ikke blir fanget opp av en ondsinnet "
+"tredjepart, må du verifisere vertsnøkkelens fingeravtrykk:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"For å få programvareoppdateringer, må dette systemet registreres hos Red "
+"Hat, enten ved hjelp av Red Hat Customer Portal eller en lokal "
+"abonnementsserver."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"For å bekrefte et fingeravtrykk, kjør følgende på $ 0 mens du sitter fysisk "
+"ved maskinen eller gjennom et pålitelig nettverk:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "I dag"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr ""
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+#, fuzzy
+#| msgid "Hide filters"
+msgid "Toggle filters"
+msgstr "Skjul filtre"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "For mye data"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Verktøy"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Topp 5 CPU-tjenester"
+
+#: pkg/metrics/metrics.jsx:963
+#, fuzzy
+#| msgid "Top 5 CPU services"
+msgid "Top 5 disk usage services"
+msgstr "Topp 5 CPU-tjenester"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "Topp 5 minnetjenester"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Total størrelse: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr ""
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr ""
+
+#: pkg/networkmanager/plots.js:39
+#, fuzzy
+#| msgid "Team settings"
+msgid "Transmitting"
+msgstr "Team innstillinger"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+#, fuzzy
+#| msgid "Triggers"
+msgid "Trigger"
+msgstr "Utløsere"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Utløst av"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Utløsere"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Feilsøk"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Feilsøk…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr ""
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr ""
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Prøv på nytt"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Prøver å synkronisere med $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Tirsdager"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned kunne ikke starte"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned er en tjeneste som overvåker systemet ditt og optimaliserer ytelsen "
+"under visse arbeidsbelastninger. Kjernen i Tuned er profiler som justerer "
+"systemet ditt for forskjellige brukstilfeller."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned er ikke tilgjengelig"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned kjører ikke"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned er av"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Slå på administrativ tilgang"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Type"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Navnet kan ikke inneholde tegnet '$0'."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Skriv for å filtrere"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Kan ikke bruke denne løsningen automatisk"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Kan ikke koble til den adressen"
+
+#: pkg/shell/hosts_dialog.jsx:361
+#, fuzzy
+#| msgid "Unable to get alert: $0"
+msgid "Unable to contact $0."
+msgstr "Kan ikke bli varslet: $0"
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Kan ikke kontakte den angitte verten $0. Forsikre deg om at den har ssh på "
+"port $1, eller angi en annen port i adressen."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Kan ikke få varslingsdetaljer."
+
+#: pkg/shell/hosts_dialog.jsx:770
+#, fuzzy
+#| msgid ""
+#| "Unable to log in to {{#strong}}{{full_address}}{{/strong}} using SSH key "
+#| "authentication. Please provide the password. You may want to set up your "
+#| "SSH keys for automatic login."
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Kan ikke logge på {{#strong}}{{full_address}}{{/strong}} ved hjelp av SSH-"
+"nøkkel autentisering. Oppgi passordet. Det kan være lurt å konfigurere SSH-"
+"nøklene for automatisk pålogging."
+
+#: pkg/shell/hosts_dialog.jsx:768
+#, fuzzy
+#| msgid ""
+#| "Unable to log in to {{#strong}}{{full_address}}{{/strong}}. The host does "
+#| "not accept password login or any of your SSH keys."
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Kan ikke logge på {{#strong}}{{full_address}}{{/strong}}. Verten godtar ikke "
+"passordinnlogging eller noen av SSH-nøklene dine."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Kan ikke nå serveren"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Kan ikke fjerne monteringen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+#, fuzzy
+#| msgid "Encrypted logical volume of $0"
+msgid "Unable to repair logical volume $0"
+msgstr "Kryptert logisk volum på $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+#, fuzzy
+#| msgid "Unable to run fix: %0"
+msgid "Unable to run fix: $0"
+msgstr "Kan ikke kjøre fix:%0"
+
+#: pkg/kdump/kdump-view.jsx:209
+#, fuzzy
+#| msgid "Unable to apply settings: $0"
+msgid "Unable to save settings"
+msgstr "Kan ikke bruke innstillingene: $0"
+
+#: pkg/kdump/kdump-view.jsx:214
+#, fuzzy
+#| msgid "Unable to apply settings: $0"
+msgid "Unable to save settings: $0"
+msgstr "Kan ikke bruke innstillingene: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Kan ikke starte setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Kan ikke avmontere filsystemet"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Utilgjengelig"
+
+#: pkg/packagekit/kpatch.jsx:238
+#, fuzzy
+#| msgid "Unavailable"
+msgid "Unavailable packages"
+msgstr "Utilgjengelig"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr ""
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Uventet feil"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Ukjente data"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Enhet"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Ukjent"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Ukjent \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Ukjent ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Ukjent program"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Ukjent konfigurasjon"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Ukjent vertsnavn"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Ukjent tjenestenavn"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Ukjent type"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Lås opp"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+#, fuzzy
+#| msgid "Mount also automatically on boot"
+msgid "Unlock automatically on boot"
+msgstr "Monter også automatisk ved oppstart"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Encrypted data"
+msgid "Unlock encrypted Stratis pool"
+msgstr "Krypterte data"
+
+#: pkg/shell/credentials.jsx:309
+#, fuzzy
+#| msgid "Unlock key"
+msgid "Unlock key $0"
+msgstr "Lås opp nøkkel"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Låser opp $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+#, fuzzy
+#| msgid "Unlocking disk..."
+msgid "Unlocking disk"
+msgstr "Låser opp disk ..."
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Ikke-administrerte grensesnitt"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Demonter"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+#, fuzzy
+#| msgid "Unmount filesystem"
+msgid "Unmount filesystem $0"
+msgstr "Demonter filsystemet"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Demonter nå"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Demonterer $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Uten navn"
+
+#: pkg/systemd/service-details.jsx:184
+#, fuzzy
+#| msgid "Systemd units"
+msgid "Unpin unit"
+msgstr "Systemd enheter"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Ukjente data"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "$0 filsystemer kan ikke gjøres mindre."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr ""
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Volum som ikke støttes"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr ""
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Oppdater"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Oppdateringshistorikk"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Oppdater pakkeinformasjon"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "Oppdateringen var vellykket"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Oppdatert"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr "Oppdaterte pakker kan kreve omstart for å tre i kraft."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Oppdateringer tilgjengelig"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Oppdateringshistorikk"
+
+#: pkg/packagekit/autoupdates.jsx:366
+#, fuzzy
+#| msgid "will be applied $0 at $1"
+msgid "Updates will be applied $0 at $1"
+msgstr "vil bli brukt $0 til $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Oppdaterer"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Oppdaterer status ..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Oppetid"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Bruk"
+
+#: pkg/storaged/storage-controls.jsx:206
+#, fuzzy
+#| msgid "Usage"
+msgid "Usage of $0"
+msgstr "Bruk"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+#, fuzzy
+#| msgid "Used"
+msgid "Use"
+msgstr "Brukt"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Bruk komprimering"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Bruk deduplisering"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Bruk nøkkel"
+
+#: pkg/users/account-create-dialog.js:114
+#, fuzzy
+#| msgid "User password"
+msgid "Use password"
+msgstr "Bruker passord"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Bruk følgende nøkler for å autentisere mot andre systemer"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr ""
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Brukt"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Bruker"
+
+#: pkg/users/account-create-dialog.js:104
+#, fuzzy
+#| msgid "User"
+msgid "User ID"
+msgstr "Bruker"
+
+#: pkg/users/account-create-dialog.js:191
+#, fuzzy
+#| msgid "This volume is already used by another VM."
+msgid "User ID is already used by another user"
+msgstr "Dette volumet brukes allerede av en annen VM."
+
+#: pkg/users/account-create-dialog.js:181
+#, fuzzy
+#| msgid "MTU must be a positive number"
+msgid "User ID must be a positive integer"
+msgstr "MTU må være et positivt tall"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:184
+#, fuzzy
+#| msgid "Name cannot be longer than $0 bytes"
+msgid "User ID must not be lower than $0"
+msgstr "Navnet kan ikke være lengre enn $0 byte"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Brukernavn"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Brukernavnet kan ikke være tomt"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Brukernavn"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "Bruker LUKS-kryptering"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Bruker Tang-server"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr ""
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO-enhet $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN ID"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Validerer adresse"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Validerer godkjenningstoken"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Leverandør"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Verifisert"
+
+#: pkg/shell/hosts_dialog.jsx:489
+#, fuzzy
+#| msgid "Fingerprint"
+msgid "Verify fingerprint"
+msgstr "Fingeravtrykk"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Verifiser nøkkel"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Verifiserer"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Versjon"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+#, fuzzy
+#| msgid "All logs"
+msgid "View all CPUs"
+msgstr "Alle logger"
+
+#: pkg/metrics/metrics.jsx:803
+#, fuzzy
+#| msgid "All logs"
+msgid "View all disks"
+msgstr "Alle logger"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+#, fuzzy
+#| msgid "All logs"
+msgid "View all logs"
+msgstr "Alle logger"
+
+#: pkg/systemd/service-details.jsx:549
+#, fuzzy
+#| msgid "Filter services"
+msgid "View all services"
+msgstr "Filtrer tjenester"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Vis automatiseringsskript"
+
+#: pkg/metrics/metrics.jsx:1147
+#, fuzzy
+#| msgid "All logs"
+msgid "View detailed logs"
+msgstr "Alle logger"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Se maskinvaredetaljer"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+#, fuzzy
+#| msgid "View details and history"
+msgid "View login history"
+msgstr "Vis detaljer og historikk"
+
+#: pkg/storaged/stratis/pool.jsx:351
+#, fuzzy
+#| msgid "All logs"
+msgid "View logs"
+msgstr "Alle logger"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+#, fuzzy
+#| msgid "View details and history"
+msgid "View metrics and history"
+msgstr "Vis detaljer og historikk"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr ""
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Vis prosjektets nettsted"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Vis rapport"
+
+#: pkg/packagekit/updates.jsx:619
+#, fuzzy
+#| msgid "All logs"
+msgid "View update log"
+msgstr "Alle logger"
+
+#: pkg/systemd/hwinfo.jsx:299
+#, fuzzy
+#| msgid "You now have administrative access."
+msgid "Viewing memory information requires administrative access."
+msgstr "Du har nå administrativ tilgang."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+#, fuzzy
+#| msgid "Firewall"
+msgid "Visit firewall"
+msgstr "Brannmur"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Volumgruppe"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Volume group is missing physical volumes"
+msgstr "Fjerner fysisk volum fra $target"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Volumstørrelsen er $0. Innholdsstørrelsen er $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Venter"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Venter på detaljer..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Venter på input…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"Venter på at andre programmer skal bli ferdige å bruke pakkebehandling..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr ""
+"Venter på at andre programvareadministrasjons-operasjoner skal fullføres"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Venter på å starte…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Ønsket av"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Ønsker"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Advarsel og over"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+#, fuzzy
+#| msgid "Set password"
+msgid "Weak password"
+msgstr "Angi passord"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Web konsoll"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Web konsoll for Linux servere"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Web konsoll starter på nytt"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Web konsoll kjører i begrenset tilgangsmodus."
+
+#: pkg/shell/shell-modals.jsx:66
+#, fuzzy
+#| msgid "Web Console"
+msgid "Web console logo"
+msgstr "Web konsoll"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Onsdager"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Ukentlig"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Uker"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Når"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Når Web konsoll startes på nytt, vil du ikke lenger se fremdrifts-"
+"informasjon. Oppdateringsprosessen vil imidlertid fortsette i bakgrunnen. "
+"Koble til igjen for å fortsette å se oppdateringsprosessen."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Hvit"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:124
+msgid "World wide name"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Skriv"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr ""
+
+#: pkg/storaged/plot.jsx:101
+#, fuzzy
+#| msgid "Waiting"
+msgid "Writing"
+msgstr "Venter"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Feil brukernavn eller passord"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Årlig"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Ja"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Du kobler til $0 for første gang."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Du er ikke autorisert til å endre brannmuren."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Du har ikke tillatelse til å se de autoriserte offentlige nøklene for denne "
+"kontoen."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Du har blitt logget ut på grunn av inaktivitet."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Du kan prøve å laste inn eldre oppføringer."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+"Det kan være lurt å endre passordet til nøkkelen for automatisk pålogging."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Du må vente lenger på å endre passordet ditt"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr ""
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Du har nå administrativ tilgang."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Du blir logget ut om $0 sekunder."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Din konto"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "Nettleseren din husker tilgangsnivået ditt på tvers av øktene."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Serveren din lukker tilkoblingen snart. Du kan koble til igjen etter at den "
+"har startet på nytt."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Økten din er avsluttet."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Økten din har utløpt. Vennligst logg inn igjen."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr ""
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[binære data]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[ingen data]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "tilgang"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "aktiv"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "tillegg"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "tillegg"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+#, fuzzy
+#| msgid "Isolated network"
+msgid "after network"
+msgstr "Isolert nettverk"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "apper"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "eiendomsmerke"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr ""
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1112
+#, fuzzy
+#| msgid "Create VDO device"
+msgid "backing device for VDO device"
+msgstr "Opprett VDO-enhet"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "binding"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "oppstart"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "bro"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Stop device"
+msgid "btrfs device"
+msgstr "Stopp enhet"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "No devices"
+msgid "btrfs devices"
+msgstr "Ingen enheter"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs subvolume"
+msgstr "Lagringsvolum"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Lagringsvolumer"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "Lagringsvolum"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "feilretting"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "bytes"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "Hurtigbuffer"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "Endre"
+
+#: pkg/metrics/metrics.jsx:654
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "cockpit-podman is not installed"
+msgstr "Cockpit er ikke installert"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "kommando"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "konsoll"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "coredump"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "cpu"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "krasj"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 day"
+#| msgid_plural "$0 days"
+msgid "data"
+msgstr "$0 dag"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "dato"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Deaktiver"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "debug"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+#, fuzzy
+#| msgid "Delete"
+msgid "delete"
+msgstr "Slett"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "deaktiver"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "disk"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "disker"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "domene"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "dIsk"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "rediger"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "aktiver"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted $0"
+msgid "encrypted"
+msgstr "Kryptert $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "kryptering"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "feil"
+
+#: pkg/packagekit/autoupdates.jsx:354
+#, fuzzy
+#| msgid "every day"
+msgid "every Friday"
+msgstr "hver dag"
+
+#: pkg/packagekit/autoupdates.jsx:350
+#, fuzzy
+#| msgid "every day"
+msgid "every Monday"
+msgstr "hver dag"
+
+#: pkg/packagekit/autoupdates.jsx:355
+#, fuzzy
+#| msgid "every day"
+msgid "every Saturday"
+msgstr "hver dag"
+
+#: pkg/packagekit/autoupdates.jsx:356
+#, fuzzy
+#| msgid "every day"
+msgid "every Sunday"
+msgstr "hver dag"
+
+#: pkg/packagekit/autoupdates.jsx:353
+#, fuzzy
+#| msgid "every day"
+msgid "every Thursday"
+msgstr "hver dag"
+
+#: pkg/packagekit/autoupdates.jsx:351
+#, fuzzy
+#| msgid "every day"
+msgid "every Tuesday"
+msgstr "hver dag"
+
+#: pkg/packagekit/autoupdates.jsx:352
+#, fuzzy
+#| msgid "every day"
+msgid "every Wednesday"
+msgstr "hver dag"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "hver dag"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "utvidelse"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "kunne ikke liste ssh vertsnøkler: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "filsystem"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "brannmur"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr ""
+
+#: pkg/packagekit/kpatch.jsx:270
+#, fuzzy
+#| msgid "Mount read only"
+msgid "for current kernel only"
+msgstr "Monter skrivebeskyttet"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "format"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+#, fuzzy
+#| msgctxt "<date> from <host>"
+#| msgid "$0 from $1"
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "$0 fra $1"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+#, fuzzy
+#| msgctxt "<date> on <terminal>"
+#| msgid "$0 on $1"
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "$0 på $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "grafer"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "maskinvare"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "historikk"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "vert"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "iSCSI mål"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "iSCSI mål"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "Legg til iSCSI-portal"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+#, fuzzy
+#| msgid "On failure"
+msgid "ignore failure"
+msgstr "Ved feil"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "i de fleste nettlesere"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+#, fuzzy
+#| msgid "Content"
+msgid "inconsistent"
+msgstr "Innhold"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#, fuzzy
+#| msgid "Initializing..."
+msgid "initialize"
+msgstr "Initialiserer…"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "installer"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "grensesnitt"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "ugyldig: flere mål er definert"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "journal"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "journalctl manpage"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "kdump status"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "nøkler"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "maskin"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "maske"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "Member of RAID device"
+msgid "member of MDRAID device"
+msgstr "Medlem av RAID-enhet"
+
+#: pkg/storaged/dialog.jsx:1113
+#, fuzzy
+#| msgid "Create storage pool"
+msgid "member of Stratis pool"
+msgstr "Opprett lagrings-pool"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "minne"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+#, fuzzy
+#| msgid "More details"
+msgid "more details"
+msgstr "Flere detaljer"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "monter"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "nettverk"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+#, fuzzy
+#| msgid "Mount at boot"
+msgid "never mount at boot"
+msgstr "Monter ved oppstart"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+#, fuzzy
+#| msgid "ssh server is empty"
+msgid "nfs export is empty"
+msgstr "ssh server er tom"
+
+#: pkg/kdump/kdump-client.js:125
+#, fuzzy
+#| msgid "ssh server is empty"
+msgid "nfs server is empty"
+msgstr "ssh server er tom"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "ingen"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "av $0 CPU"
+msgstr[1] "av $0 CPUer"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+#, fuzzy
+#| msgctxt "<date> on <terminal>"
+#| msgid "$0 on $1"
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "$0 på $1"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "operativsystem"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "os"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "pakke"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "partisjon"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "passord"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "passordkvalitet"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "sti"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+#| msgid "Performance Metrics"
+msgid "performance"
+msgstr "Ytelsesmålinger"
+
+#: pkg/storaged/dialog.jsx:1110
+#, fuzzy
+#| msgid "Create volume group"
+msgid "physical volume of LVM2 volume group"
+msgstr "Opprett volumgruppe"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "utvidelse"
+
+#: pkg/metrics/metrics.jsx:1833
+#, fuzzy
+#| msgid "$0 service has failed"
+#| msgid_plural "$0 services have failed"
+msgid "pmlogger.service has failed"
+msgstr "$0 tjeneste har feilet"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1826
+#, fuzzy
+#| msgid "Is 'pmlogger' service running?"
+msgid "pmlogger.service is not running"
+msgstr "Kjører 'pmlogger' -tjenesten?"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr ""
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "port"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "skrivebeskyttet"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "anbefalt"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr ""
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "remove from MDRAID"
+msgstr "Fjerner $target fra RAID-enhet"
+
+#: pkg/storaged/utils.js:904
+msgid "remove from btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "omstart"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "roller"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "kjører $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "sikkerhet"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "tjeneste"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "skall"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "vis mindre"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "vis mer"
+
+#: pkg/storaged/block/resize.jsx:566
+#, fuzzy
+#| msgid "Shrink"
+msgid "shrink"
+msgstr "Krymp"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "løsning"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "løsningsdetaljer"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+#, fuzzy
+#| msgid "Reporting failed"
+msgid "sos report failed"
+msgstr "Rapportering feilet"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ssh-nøkkel er ikke en bane"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "ssh server er tom"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:181
+#, fuzzy
+#| msgid "On failure"
+msgid "stop boot on failure"
+msgstr "Ved feil"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+msgid "stopped"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "mål"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "team"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "tid"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "timer"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "enhet"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "ukjent"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "ukjent mål"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "unmask"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "unmount"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "upartisjonert plass på $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "bruker"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "brukernavn"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#, fuzzy
+#| msgid "No description"
+msgid "using key description $0"
+msgstr "Ingen beskrivelse"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "versjon"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "volum"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "advarsel"
+
+#: pkg/networkmanager/wireguard.jsx:84
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "wireguard-tools package is not installed"
+msgstr "PackageKit er ikke installert"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "ja"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "sone"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Kdump-tjenesten er ikke installert. Forsikre deg om at pakke kexec-tools "
+#~ "er installert."
+
+#~ msgid "Service is running"
+#~ msgstr "Tjenesten kjører"
+
+#~ msgid "Service is starting"
+#~ msgstr "Tjenesten starter"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Tjenesten er stoppet"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Tjenesten stopper"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Dette vil teste kdump-konfigurasjonen ved å krasje kjernen."
+
+#, fuzzy
+#~| msgid "Encrypted $0"
+#~ msgid "$0 (encrypted)"
+#~ msgstr "Kryptert $0"
+
+#, fuzzy
+#~| msgid "$0 Storage pool"
+#~| msgid_plural "$0 Storage pools"
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 lagrings-pool"
+
+#, fuzzy
+#~| msgid "Cache"
+#~ msgid "$0 cache"
+#~ msgstr "Hurtigbuffer"
+
+#, fuzzy
+#~| msgid "unknown target"
+#~ msgid "$0 of unknown tier"
+#~ msgstr "ukjent mål"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 ledig"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "En ekstra disk må legges til først før denne disken kan fjernes."
+
+#~ msgid "Block"
+#~ msgstr "Blokk"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Kapasitet"
+
+#~ msgid "Content"
+#~ msgstr "Innhold"
+
+#~ msgid "Create devices"
+#~ msgstr "Opprett enheter"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Enhet"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Enhetsfil"
+
+#~ msgid "Devices"
+#~ msgstr "Enheter"
+
+#~ msgid "Drives"
+#~ msgstr "Disker"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr "Krypterte volumer må låses opp før de kan endres."
+
+#, fuzzy
+#~| msgid "Filesystem directory"
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Filsystem katalog"
+
+#~ msgid "Filesystems"
+#~ msgstr "Filsystemer"
+
+#~ msgid "Free"
+#~ msgstr "Ledig"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Frigjør plass i denne gruppen: Krymp eller slett andre logiske volumer "
+#~ "eller legg til et annet fysisk volum."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Inaktivt volum"
+
+#, fuzzy
+#~| msgid "Keyserver"
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Nøkkelserver"
+
+#, fuzzy
+#~| msgid "Add member"
+#~ msgid "LVM2 member"
+#~ msgstr "Legg til medlem"
+
+#~ msgid "Locked devices"
+#~ msgstr "Låste enheter"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Modell"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS monteringer"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "NFS-støtte ikke installert"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Ingen NFS-monteringer er satt opp"
+
+#~ msgid "No drives attached"
+#~ msgstr "Ingen stasjoner tilkoblet"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Ingen iSCSI-mål er satt opp"
+
+#, fuzzy
+#~| msgid "Block device for filesystems"
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Blokk enhet for filsystemer"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Andre data"
+
+#, fuzzy
+#~| msgid "$0 block device"
+#~ msgid "Partitioned block device"
+#~ msgstr "$0 blokk enhet"
+
+#, fuzzy
+#~| msgid "Passphrase"
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Passfrase"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "Fysiske volumer kan ikke endres her."
+
+#~ msgid "Pool"
+#~ msgstr "Pool"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Pool for tynne volumer"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID-nivå"
+
+#~ msgid "RAID member"
+#~ msgstr "RAID-medlem"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Avtakbar stasjon"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Vis $0 enhet"
+#~ msgstr[1] "Vis alle $0 enheter"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Vis $0 stasjon"
+#~ msgstr[1] "Vis alle $0 stasjoner"
+
+#~ msgid "Show all"
+#~ msgstr "Vis alle"
+
+#~ msgid "Source"
+#~ msgstr "Kilde"
+
+#, fuzzy
+#~| msgid "Storage pools"
+#~ msgid "Start pool"
+#~ msgstr "Lagrings-pooler"
+
+#, fuzzy
+#~| msgid "Block device for filesystems"
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Blokk enhet for filsystemer"
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Tilstand"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Opprett lagrings-pool"
+
+#, fuzzy
+#~| msgid "Interface members"
+#~ msgid "Stratis member"
+#~ msgstr "Grensesnittmedlemmer"
+
+#, fuzzy
+#~| msgid "Storage pools"
+#~ msgid "Stratis pool $0"
+#~ msgstr "Lagrings-pooler"
+
+#~ msgid "Support is installed."
+#~ msgstr "Support er installert."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr "RAID-enheten må kjøre for å kunne legge til ekstra disker."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "Den siste disken til en RAID-enhet kan ikke fjernes."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "Du kan ikke fjerne det siste fysiske volumet i en volumgruppe."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Det er ikke nok ledig plass andre steder for å fjerne dette fysiske "
+#~ "volumet. Minst $0 mer ledig plass er nødvendig."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "Denne disken kan ikke fjernes mens enheten gjenoppretter."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr "Dette volumet må aktiveres før det kan endres."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Ukjente data"
+
+#, fuzzy
+#~| msgid "Usage"
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Bruk"
+
+#, fuzzy
+#~| msgid "Used by"
+#~ msgid "Used for"
+#~ msgstr "Brukt av"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO-enhet"
+
+#~ msgid "Volume"
+#~ msgstr "Volum"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automatisk (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Godta nøkkel og koble til"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Krypterte volumer kan ikke endres her."
+
+#, fuzzy
+#~| msgid "Tang keyserver"
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Tang nøkkelserver"
+
+#, fuzzy
+#~| msgid "New passphrase"
+#~ msgid "Use a passphrase"
+#~ msgstr "Ny passfrase"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitmap"
+
+#, fuzzy
+#~| msgid "Make sure the key hash from the Tang server matches:"
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr "Forsikre deg om at nøkkelhash fra Tang-serveren samsvarer med:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Sjekk manuelt med SSH: "
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Portnummer og type samsvarer ikke"
+
+#, fuzzy
+#~| msgctxt "storage-id-desc"
+#~| msgid "Encrypted data"
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Krypterte data"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Feil under sletting av varsel: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Kan ikke bli varslet: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 bytes med binære data]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Disk I/O-topp"
+
+#~ msgid "Event"
+#~ msgstr "Hendelse"
+
+#~ msgid "Event logs"
+#~ msgstr "Hendelseslogger"
+
+#~ msgid "Load spike"
+#~ msgstr "Last-topp"
+
+#~ msgid "Memory spike"
+#~ msgstr "Minne topp"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Nettverk I/O topp"
+
+#~ msgid "ssh key"
+#~ msgstr "ssh nøkkel"
+
+#, fuzzy
+#~| msgid "Mount at boot"
+#~ msgid "Never mount at boot"
+#~ msgstr "Monter ved oppstart"
+
+#~ msgid "Unit not found"
+#~ msgstr "Enheten ble ikke funnet"
+
+#~ msgid "Validating key"
+#~ msgstr "Validerer nøkkel"
+
+#~ msgid "Container administrator"
+#~ msgstr "Container administrator"
+
+#~ msgid "Roles"
+#~ msgstr "Roller"
+
+#~ msgid "Server administrator"
+#~ msgstr "Server administrator"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Unix-gruppe: $0"
+
+#, fuzzy
+#~| msgid "Copy to clipboard"
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Kopier til utklippstavle"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Diagnose rapporter"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Kjerne dump"
+
+#~ msgid "Software Updates"
+#~ msgstr "Programvare oppdateringer"
+
+#~ msgid "Update log"
+#~ msgstr "Oppdateringslogg"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "nfs dump mål er ikke formatert som server:sti"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Lag diagnoserapport"
+
+#~ msgid "Done!"
+#~ msgstr "Ferdig!"
+
+#~ msgid "Download report"
+#~ msgstr "Last ned rapport"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Ingen arkiv er opprettet."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Bekreft sletting av $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "Det genererte arkivet inneholder data som anses som sensitive, og "
+#~ "innholdet bør gjennomgås av den opprinnelige organisasjonen før det "
+#~ "sendes til tredjepart."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Dette verktøyet vil samle systemkonfigurasjon og diagnose-informasjon fra "
+#~ "dette systemet for bruk ved diagnostisering av problemer med systemet."
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Denne pakken er ikke kompatibel med denne versjonen av Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Denne pakken krever Cockpit-versjon %s eller nyere"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Ytelsesmålinger"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "skrivebeskyttet"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB totalt"
+
+#~ msgid "Apply"
+#~ msgstr "Bruk"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Ikke autorisert til å fjerne sone $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Klikk $0 på nytt for å bruke passordet allikevel."
+
+#~ msgid "Access"
+#~ msgstr "Tilgang"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "DISKEN FEILER"
+
+#, fuzzy
+#~| msgid "Logical size"
+#~ msgid "Logical Size"
+#~ msgstr "Logisk størrelse"
+
+#~ msgid "Security updates "
+#~ msgstr "Sikkerhetsoppdateringer "
+
+#~ msgid "Updates "
+#~ msgstr "Oppdateringer "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Valgfri)"
+
+#~ msgid "Active since"
+#~ msgstr "Aktiv siden"
+
+#~ msgid "Affected locations"
+#~ msgstr "Berørte plasseringer"
+
+#~ msgid "Process"
+#~ msgstr "Prosess"
+
+#, fuzzy
+#~| msgid "Source format"
+#~ msgid "Remove and delete"
+#~ msgstr "Kildeformat"
+
+#, fuzzy
+#~| msgid "Source format"
+#~ msgid "Remove and format"
+#~ msgstr "Kildeformat"
+
+#, fuzzy
+#~| msgid "Source format"
+#~ msgid "Remove and grow"
+#~ msgstr "Kildeformat"
+
+#, fuzzy
+#~| msgid "Source format"
+#~ msgid "Remove and initialize"
+#~ msgstr "Kildeformat"
+
+#, fuzzy
+#~| msgid "Source format"
+#~ msgid "Remove and shrink"
+#~ msgstr "Kildeformat"
+
+#, fuzzy
+#~| msgid "Remove device"
+#~ msgid "Remove and stop device"
+#~ msgstr "Fjern enheten"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Filsystemet er i bruk av påloggingsøkter og systemtjenester. Dersom du "
+#~ "fortsetter vil disse stoppes."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Filsystemet er i bruk av påloggingsøkter. Dersom du fortsetter vil disse "
+#~ "stoppes."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Filsystemet er i bruk av systemtjenester. Dersom du fortsetter vil disse "
+#~ "stoppes."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Denne enheten har filsystemer som er i bruk for øyeblikket. Hvis du "
+#~ "fortsetter, demonteres alle filsystemene på den."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Denne enheten brukes for øyeblikket for volumgrupper."
+
+#, fuzzy
+#~| msgid ""
+#~| "This device is currently used for volume groups. Proceeding will remove "
+#~| "it from its volume groups."
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Denne enheten brukes for øyeblikket for volumgrupper. Hvis du fortsetter, "
+#~ "fjernes den fra volumgruppene."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Denne enheten brukes for øyeblikket til RAID-enheter."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Denne enheten brukes for øyeblikket til RAID-enheter. Hvis du fortsetter, "
+#~ "fjernes den fra RAID-enhetene."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Denne enheten brukes for øyeblikket for volumgrupper."
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and delete"
+#~ msgstr "Demonter nå"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and format"
+#~ msgstr "Demonter nå"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and grow"
+#~ msgstr "Demonter nå"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and initialize"
+#~ msgstr "Demonter nå"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and shrink"
+#~ msgstr "Demonter nå"
+
+#, fuzzy
+#~| msgid "On a mounted device"
+#~ msgid "Unmount and stop device"
+#~ msgstr "På en montert enhet"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 av $1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "Blokkert"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Fysisk volum på $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Medlem av RAID-enhet $0"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Opprett lagrings-pool"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "Innstillinger for montering"
+
+#~ msgid "A disk is needed."
+#~ msgstr "En disk er nødvendig."
+
+#~ msgid "Disk"
+#~ msgstr "Disk"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Bare for eldre applikasjoner. Reduserer ytelsen."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Installer VDO-støtte"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Bruk 512 byte-emulering"
+
+#~ msgid "System services"
+#~ msgstr "Systemtjenester"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Lag diagnoserapport "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr "Tilkobling samtidig til mer enn {{ limit }} maskiner støttes ikke."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "Kerberos-basert SSO"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Logg på {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "De nye passordene til nøkkelen stemmer ikke overens"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "For å bekrefte et fingeravtrykk, kjør følgende på {{host}} mens du sitter "
+#~ "fysisk ved maskinen eller gjennom et pålitelig nettverk:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Kan ikke kontakte {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Kan ikke logge på {{#strong}}{{host}}{{/strong}}. For flere "
+#~ "autentiseringsalternativer og feilsøkingsstøtte kan du oppgradere cockpit-"
+#~ "ws til en nyere versjon."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Kan ikke logge på {{#strong}}{{host}}{{/strong}}. For å koble deg til "
+#~ "denne verten må du aktivere en av følgende autentiseringsmetoder i sshd-"
+#~ "konfigurasjonen på {{#strong}}{{host}}{{/strong}}:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "Du kobler til {{host}} for første gang."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "{{host}} nøkkel endret"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Fjern monteringspunkt-konfigurasjonen"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Hvis du oppretter denne bindingen, brytes forbindelsen til serveren og "
+#~ "administrasjonsgrensesnittet blir utilgjengelig."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Hvis du oppretter denne bridgen, brytes forbindelsen til serveren og "
+#~ "administrasjonsgrensesnittet blir utilgjengelig."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Hvis du oppretter dette teamet, brytes forbindelsen til serveren og "
+#~ "administrasjonsgrensesnittet blir utilgjengelig."
+
+#~ msgid "IP settings"
+#~ msgstr "IP-innstillinger"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Hvis du slår av <b>$0</b>, brytes forbindelsen til serveren, og gjør "
+#~ "administrasjonsgrensesnittet utilgjengelig."
+
+#~ msgid "Administrator password"
+#~ msgstr "Administratorpassord"
+
+#~ msgid "Computer OU"
+#~ msgstr "Datamaskin OU"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "Hvis du sletter en RAID-enhet, slettes alle dataene på den."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Ikke overskriv eksisterende data"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Formater disk $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr "Formatering av en lagringsenhet vil slette alle dataene på den."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Vertsnavnet skal ikke endres i et domene"
+
+#~ msgid "More"
+#~ msgstr "Mer"
+
+#~ msgid "One time password"
+#~ msgstr "Engangspassord"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 fra $1 på $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Timer må være et tall mellom 0 og 23"
+
+#~ msgid "Last login:"
+#~ msgstr "Siste innlogging:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Minutter må være et tall mellom 0 og 59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "Det var $0 mislykket påloggingsforsøk siden forrige vellykkede pålogging."
+#~ msgstr[1] ""
+#~ "Det var $0 mislykkede påloggingsforsøk siden forrige vellykkede pålogging."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "f.eks. \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 aktive soner"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 forekomst"
+#~ msgstr[1] "$0 forekomster"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Lisensiert under:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Lås opp ved oppstart"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Lås opp skrivebeskyttet"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Søk i loggene med en kombinasjon av termer:"
+
+#~ msgid "Show filters"
+#~ msgstr "Vis filtre"
+
+#~ msgid "Text"
+#~ msgstr "Tekst"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "hvilken som helst friformstreng som regulært uttrykk"
+
+#~ msgid "e.g."
+#~ msgstr "f.eks."
+
+#~ msgid "log fields"
+#~ msgstr "logg-felt"
+
+#~ msgid "qualifiers"
+#~ msgstr "kvalifikatorer"
+
+#~ msgid "(none)"
+#~ msgstr "(ingen)"
+
+#~ msgid "Create timers"
+#~ msgstr "Lag timere"
+
+#~ msgid "Recommended default"
+#~ msgstr "Anbefalt standard"
+
+#~ msgid "Run"
+#~ msgstr "Kjør"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Aktiver lagret metrikk"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Tving fjerning av passfrase i $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Hvis tang-show-keys ikke er tilgjengelig, kjører du følgende:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Hva om tang-show-keys ikke er tilgjengelig?"
+
+#~ msgid "key slot $0"
+#~ msgstr "nøkkel-slot $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Noe gikk galt"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "Dette fungerte ikke. Prøv på nytt"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "Du kan ikke få administrativ tilgang."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Automatiske oppdateringer er ikke satt opp"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "$0 CPU konfigurasjon"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 Nettverk"
+#~ msgstr[1] "$0 Nettverk"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 er tilgjengelig for de fleste operativsystemer. For å installere det, "
+#~ "søk etter det i GNOME-programvaren eller kjør følgende:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 minnejustering"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 nettverk"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPU"
+#~ msgstr[1] "$0 vCPUer"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 vCPU detaljer"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 innstillinger for virtuelt nettverksgrensesnitt"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "Aktiver lagrings-poolen for å administrere volumer"
+
+#~ msgid "Add network interface"
+#~ msgstr "Legg til nettverksgrensesnitt"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Legg til virtuelt nettverksgrensesnitt"
+
+#~ msgid "Additional"
+#~ msgstr "Ekstra"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Adressen ligger ikke i subnettet"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr ""
+#~ "Etter å ha slettet øyeblikksbildet, vil alt innfanget innhold gå tapt."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Start libvirt automatisk ved oppstart"
+
+#~ msgid "Autostart"
+#~ msgstr "Autostart"
+
+#~ msgid "Boot order"
+#~ msgstr "Oppstartsrekkefølge"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Innstillinger for oppstartsrekkefølge kunne ikke lagres"
+
+#~ msgid "Bus"
+#~ msgstr "Buss"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "CD/DVD disk"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "CPU-konfigurasjon kunne ikke lagres"
+
+#~ msgid "CPU type"
+#~ msgstr "CPU-type"
+
+#~ msgid "Change firmware"
+#~ msgstr "Bytt fastvare"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr ""
+#~ "Endringer vil tre i kraft etter at den virtuelle maskinen er slått av"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Velg et operativsystem"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "Ved å klikke på \"Start ekstern visning\" lastes en .vv-fil ned og $0 "
+#~ "startes."
+
+#~ msgid "Clone"
+#~ msgstr "Klon"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Bekreft denne handlingen"
+
+#~ msgid "Connect"
+#~ msgstr "Koble til"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr ""
+#~ "Koble til et hvilket som helst visningsprogram for følgende protokoller"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Kobler til virtualiseringstjeneste"
+
+#~ msgid "Connection"
+#~ msgstr "Tilkobling"
+
+#~ msgid "Console"
+#~ msgstr "Konsoll"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Kjerner per sokkel"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Kunne ikke gå tilbake til øyeblikksbildet"
+
+#~ msgid "Crashed"
+#~ msgstr "Krasjet"
+
+#~ msgid "Create VM"
+#~ msgstr "Opprett VM"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Opprett en klone-VM basert på $0"
+
+#~ msgid "Create new"
+#~ msgstr "Opprett ny"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Opprett ny virtuell maskin"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Opprett virtuelt nettverk"
+
+#~ msgid "Creating VM"
+#~ msgstr "Oppretter VM"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "Oppretter VM-installasjon"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Opprettelsen av VM $0 feilet"
+
+#~ msgid "Creation time"
+#~ msgstr "Opprettelsestidspunkt"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Gjeldende tildeling"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Tilpasset fastvare: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP-område"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Slett tilknyttede lagringsfiler:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Slett lagrings-pool $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Slett volumene i denne poolen"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Slette en inaktiv lagrings-pool vil bare avdefinere poolen. Innholdet vil "
+#~ "ikke bli slettet."
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Koble diskene som bruker denne poolen fra alle virtuelle maskiner før du "
+#~ "prøver å slette."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Koblet fra seriekonsollet. Klikk på tilkoblingsknappen."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Disk $0 kan ikke løsnes fra VM $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Disken kunne ikke kobles til"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Disken ble ikke opprettet"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Diskinnstillinger kunne ikke lagres"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "Domenet har krasjet"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "Domenet er blokkert på ressursen"
+
+#~ msgid "Download an OS"
+#~ msgstr "Last ned et OS"
+
+#~ msgid "Dying"
+#~ msgstr "Døende"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Emulert maskin"
+
+#~ msgid "End"
+#~ msgstr "Slutt"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Slutten skal ikke være tom"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Eksisterende diskbilde på vertens filsystem"
+
+#~ msgid "Expand"
+#~ msgstr "Utvid"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Kunne ikke endre fastvare"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "Kunne ikke hente IP-adressene til grensesnittene i $0"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "Kunne ikke sende tast Ctrl+Alt+$0 til VM $1"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr "Det bør aktiveres færre enn maksimalt antall virtuelle CPU-er."
+
+#~ msgid "File"
+#~ msgstr "Fil"
+
+#~ msgid "Filter by name"
+#~ msgstr "Filtrer etter navn"
+
+#~ msgid "Firmware"
+#~ msgstr "Fastvare"
+
+#~ msgid "Force shut down"
+#~ msgstr "Tving nedstenging"
+
+#~ msgid "Forward mode"
+#~ msgstr "Videresendingsmodus"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Videresendingsmodus"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Generer automatisk"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "Gå til VM-listen"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Skjul flere alternativer"
+
+#~ msgid "Host device"
+#~ msgstr "Vertsenhet"
+
+#~ msgid "Host name"
+#~ msgstr "Vertsnavn"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Vert kan ikke være tom"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Hypervisor detaljer"
+
+#~ msgid "IP configuration"
+#~ msgstr "IP-konfigurasjon"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 og IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4-nettverk"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv6-nettverk kan ikke være tomt"
+
+#~ msgid "IPv4 only"
+#~ msgstr "Bare IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6-adresse"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6-nettverk"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6-nettverk kan ikke være tomt"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Bare IPv6"
+
+#~ msgid "Idle"
+#~ msgstr "Ledig"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Start VM umiddelbart"
+
+#~ msgid "Import"
+#~ msgstr "Importer"
+
+#~ msgid "Import VM"
+#~ msgstr "Importer VM"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "Importer en virtuell maskin"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "I de fleste konfigurasjoner fungerer macvtap ikke for kommunikasjon "
+#~ "mellom vert og gjest."
+
+#~ msgid "Initiator"
+#~ msgstr "Initiator"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "Initiator IQN kan ikke være tom"
+
+#~ msgid "Installation source"
+#~ msgstr "Installasjonskilde"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Installasjonskilde kan ikke være tom"
+
+#~ msgid "Installation type"
+#~ msgstr "Installasjonstype"
+
+#~ msgid "Interface type"
+#~ msgstr "Grensesnitt type"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Ugyldig IPv4-maske eller prefikslengde"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Ugyldig IPv6-adresse"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Ugyldig IPv6-prefiks"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Ugyldig filnavn"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "Start fjernvisning"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr "La passordet være tomt hvis du ikke ønsker å opprette en rotkonto"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "La passordet være tomt hvis du ikke ønsker å opprette en brukerkonto"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr "La passordet være tomt hvis du ikke ønsker å opprette en rotkonto"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt oppdaget ikke noe UEFI/OVMF firmwarebilde installert på verten"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt eller hypervisor støtter ikke UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "Laster ressurser"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "Maskinen må være stengt før du bytter busstype"
+
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "Maskinen må være slått av før du bytter hurtigbuffermodus"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Administrere virtuelle maskiner"
+
+#~ msgid "Manual connection"
+#~ msgstr "Manuell tilkobling"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Maske eller prefikslengde"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Masken eller prefikslengden kan ikke være tom"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Maksimal tildeling"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "Maksimalt minne kunne ikke lagres"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr "Maksimalt antall virtuelle CPUer som er tildelt for gjeste-OS"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Maksimalt antall virtuelle CPUer som er tildelt for gjeste-OS, som må "
+#~ "være mellom 1 og $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Maksimal overføringsenhet"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Minne kunne ikke lagres"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "Minne kan ikke være 0"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Modelltype"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT til $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "NIC $0 for VM $1 kunne ikke endre tilstand"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Navnet inneholder ugyldige tegn"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Navnet kan ikke være tomt"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Navnet bør ikke være tomt"
+
+#~ msgid "Name: "
+#~ msgstr "Navn: "
+
+#~ msgid "Netmask"
+#~ msgstr "Nettmaske"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Nettverk $0 kunne ikke aktiveres"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Nettverk $0 kunne ikke deaktiveres"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Nettverksstart (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Nettverksfilsystem"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Innstillinger for nettverksgrensesnitt kunne ikke lagres"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "Nettverksvalg støtter ikke PXE."
+
+#~ msgid "Networks"
+#~ msgstr "Nettverk"
+
+#~ msgid "New volume name"
+#~ msgstr "Nytt volumnavn"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "Ingen VM kjører eller er definert på denne verten"
+
+#~ msgid "No connection available"
+#~ msgstr "Ingen tilkobling tilgjengelig"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Ingen disker definert for denne VMen"
+
+#~ msgid "No network devices"
+#~ msgstr "Ingen nettverksenheter"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Ingen nettverksgrensesnitt definert for denne VMen"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "Ingen nettverk er definert på denne verten"
+
+#~ msgid "No networks available"
+#~ msgstr "Ingen nettverk tilgjengelig"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Ingen øyeblikksbilder er definert for denne VM"
+
+#~ msgid "No state"
+#~ msgstr "Ingen tilstand"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "Ingen lagrings-pool er definert på denne verten"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Ingen lagrings-pooler tilgjengelig"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "Ingen lagringsvolumer definert for denne lagrings-poolen"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Ingen virtuelle nettverk"
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Ingen (isolert nettverk)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Ett eller flere utvalgte volumer brukes av domener. Koble fra diskene "
+#~ "først for å tillate sletting av volum."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Bare redigerbar når gjesten er slått av"
+
+#~ msgid "Open"
+#~ msgstr "Åpen"
+
+#~ msgid "Operating system"
+#~ msgstr "Operativsystem"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "Operasjon pågår"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Sti på vertens filsystem"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Sti til ISO-fil på vertens filsystem"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Sti til fil på vertens filsystem"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Sti til fil på vertens filsystem"
+
+#~ msgid "Paused"
+#~ msgstr "Pauset"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Fysisk disk-enhet"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Fysisk disk-enhet på vert"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Velg en lagrings-pool"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "Velg et volum"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Vennligst skriv inn nytt volumnavn"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Start den virtuelle maskinen for å få tilgang til konsollen."
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "Pool må være aktiv for å opprette volum"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "Bassengtype støtter ikke volumoppretting"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "Pool-ets volumer brukes av virtuelle maskiner "
+
+#~ msgid "Prefix"
+#~ msgstr "Prefiks"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "Prefikslengden skal ikke være tom"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "Tidligere tatte øyeblikksbilder lar deg gå tilbake til en tidligere "
+#~ "tilstand hvis noe går galt"
+
+#~ msgid "Product"
+#~ msgstr "Produkt"
+
+#~ msgid "Profile"
+#~ msgstr "Profil"
+
+#~ msgid "Protocol"
+#~ msgstr "Protokoll"
+
+#~ msgid "Revert"
+#~ msgstr "Gå tilbake"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Gå tilbake til øyeblikksbilde $0"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "Å gå tilbake til dette øyeblikksbildet vil føre VM tilbake til "
+#~ "tidspunktet for øyeblikksbildet, og den nåværende tilstanden vil gå tapt, "
+#~ "sammen med data som ikke blir tatt i et øyeblikksbilde"
+
+#~ msgid "Root password"
+#~ msgstr "Root passord"
+
+#~ msgid "Route to $0"
+#~ msgstr "Rute til $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Kjør installasjon uten tilsyn"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Kjør når verten starter"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE TLS-port"
+
+#~ msgid "SPICE address"
+#~ msgstr "SPICE-adresse"
+
+#~ msgid "SPICE port"
+#~ msgstr "SPICE-port"
+
+#~ msgid "Select console type"
+#~ msgstr "Velg konsolltype"
+
+#~ msgid "Send key"
+#~ msgstr "Send tast"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Send avbrudd som ikke kan maskeres"
+
+#~ msgid "Serial console"
+#~ msgstr "Seriell konsoll"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Sett DHCP-område"
+
+#~ msgid "Set manually"
+#~ msgstr "Sett manuelt"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "Hvis du skal angi brukerpassord for ubetjent installasjon, må du starte "
+#~ "VM når du oppretter den"
+
+#~ msgid "Show additional options"
+#~ msgstr "Vis flere alternativer"
+
+#~ msgid "Shut off"
+#~ msgstr "Slå av"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "Slå av VM for å redigere fastvarekonfigurasjon"
+
+#~ msgid "Shutting down"
+#~ msgstr "Slår av"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Øyeblikksbilde kunne ikke opprettes"
+
+#~ msgid "Source path"
+#~ msgstr "Kildesti"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Kildestien skal ikke være tom"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "Kilden skal starte med http-, ftp- eller nfs-protokoll"
+
+#~ msgid "Source volume group"
+#~ msgstr "Kilde volumgruppe"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Start libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Start pool når verten starter"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Start skal ikke være tom"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "Lagrinrgspool $0 kunne ikke aktiveres"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "Lagringspool $0 kunne ikke deaktiveres"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Lagrings-pool kunne ikke opprettes"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Lagrings-pool navn"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "Lagringsstørrelsen kan ikke være 0"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr ""
+#~ "Lagringsvolumstørrelsen kan ikke overstige kapasiteten på lagrings-poolen "
+#~ "($0 $1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Lagringsvolumer kunne ikke slettes"
+
+#~ msgid "Target path"
+#~ msgstr "Målsti"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Målstien skal ikke være tom"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr "VM kjører og vil bli tvunget avslått før sletting."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr "VM må kjøre eller slås av for å koble fra denne enheten"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "Katalogen på serveren som eksporteres"
+
+#~ msgid "The pool is empty"
+#~ msgstr "Poolen er tom"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr "Det valgte operativsystemet støtter ikke installasjon uten tilsyn"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr "Det valgte operativsystemet har et minimumskrav til minne på $0 $1"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "Det valgte operativsystemet har et minimumskrav på lagringsstørrelse på "
+#~ "$0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "Lagrings-poolen kunne ikke slettes"
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Dette volumet er allerede brukt av: "
+
+#~ msgid "Threads per core"
+#~ msgstr "Tråder per kjerne"
+
+#~ msgid "Type ID"
+#~ msgstr "Type ID"
+
+#~ msgid "Unique name"
+#~ msgstr "Unikt navn"
+
+#~ msgid "Unique network name"
+#~ msgstr "Unikt nettverksnavn"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Ukjent fastvare"
+
+#~ msgid "Up to $0 $1 available on the default location"
+#~ msgstr "Opptil $0 $1 tilgjengelig på standard plassering"
+
+#~ msgid "Up to $0 $1 available on the host"
+#~ msgstr "Opptil $0 $1 tilgjengelig på verten"
+
+#~ msgid "Url"
+#~ msgstr "Url"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "VCPU-innstillingene kunne ikke lagres"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "VM $0 fnnes allerede"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "VM $0 finnes ikke på $1-tilkobling"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "VM $0 klarte ikke å tvinge omstart"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "VM $0 kunne ikke tvinge avstenging"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "VM $0 kunne ikke slettes"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "VM $0 kunne ikke installeres"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "VM $0 kunne ikke omstartes"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "VM $0 kunne ikke gjenopptas"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "VM $0 kunne ikke sende NMI"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "VM $0 kunne ikke slås av"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "VM $0 kunne ikke startes"
+
+#~ msgid "VM state"
+#~ msgstr "VM-tilstand"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC TLS-port"
+
+#~ msgid "VNC address"
+#~ msgstr "VNC-adresse"
+
+#~ msgid "VNC console"
+#~ msgstr "VNC konsoll"
+
+#~ msgid "VNC port"
+#~ msgstr "VNC-port"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Virtuelle maskiner"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Virtuelle maskiner"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Administrasjon av virtuelle maskiner"
+
+#~ msgid "Virtual network"
+#~ msgstr "Virtuelt nettverk"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Kunne ikke opprette et virtuelt nettverk"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Virtualiseringstjeneste (libvirt) er ikke aktiv"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Volumgruppenavnet skal ikke være tomt"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Skrivbar"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Skrivbar og delt"
+
+#~ msgid "Yesterday"
+#~ msgstr "I går"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "Du må velge det operativsystemet som passer best"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "deaktivert"
+
+#~ msgid "down"
+#~ msgstr "nede"
+
+#~ msgid "enabled"
+#~ msgstr "aktivert"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "vertsenhet"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "iSCSI direkte mål"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "iSCSI mål IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "nei"
+
+#~ msgid "no state saved"
+#~ msgstr "ingen tilstand lagret"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "viderekoblet enhet"
+
+#~ msgid "server"
+#~ msgstr "server"
+
+#~ msgid "up"
+#~ msgstr "oppe"
+
+#~ msgid "vCPU count"
+#~ msgstr "vCPU antall"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU maksimum"
+
+#~ msgid "vCPUs"
+#~ msgstr "vCPUer"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#~ msgid "view more..."
+#~ msgstr "se mer..."
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "virt-install-pakken må installeres på systemet for å klone virtuelle "
+#~ "maskiner"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "virt-install-pakken må installeres på systemet for å opprette nye "
+#~ "virtuelle maskiner"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "virt-install-pakken må installeres på systemet for å redigere dette "
+#~ "attributtet"
+
+#~ msgid "vm"
+#~ msgstr "vm"
+
+#~ msgid "Local install media"
+#~ msgstr "Lokale installasjonsmedier"
+
+#~ msgid "URL"
+#~ msgstr "URL"
diff --git a/po/nl.po b/po/nl.po
new file mode 100644
index 0000000..ee65ff8
--- /dev/null
+++ b/po/nl.po
@@ -0,0 +1,13233 @@
+# Toon van Gerwen <translation@vgerwen.nl>, 2017. #zanata
+# sanne raymaekers <sanne.raymaekers@gmail.com>, 2019. #zanata
+# Geert Warrink <geert.warrink@onsnet.nu>, 2020.
+# Richard E. van der Luit <fedoraproject@veneax.nl>, 2020.
+# Dennis ten Hoove <dennistenhoove@protonmail.com>, 2020.
+# Anonymous <noreply@weblate.org>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-11-13 13:37+0000\n"
+"Last-Translator: Geert Warrink <geert.warrink@onsnet.nu>\n"
+"Language-Team: Dutch <https://translate.fedoraproject.org/projects/cockpit/"
+"main/nl/>\n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.1.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "# gebruikers"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+msgstr[1] "$0 CPU's"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 actieve zone"
+msgstr[1] "$0 actieve zones"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 beschikbaar"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "Bestandssystemen kunnen niet uitgebreid worden."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "Bestandssystemen kunnen niet verkleind worden."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "De grootte van bestandssystemen kan hier niet gewijzigd worden."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "De grootte van bestandssystemen kan hier niet gewijzigd worden."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 brokgrootte"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 kritieke treffer"
+msgstr[1] "$0 treffers, inclusief de kritieke"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 data + $1 overhead gebruikt van $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 dag"
+msgstr[1] "$0 dagen"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 schijf ontbreekt"
+msgstr[1] "$0 schijven ontbreken"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 schijf ontbreekt"
+msgstr[1] "$0 schijven ontbreken"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 schijven"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 documentatie"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 fout"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 verlaten met code $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 mislukte"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 mislukte inlogpoging"
+msgstr[1] "$0 mislukte inlogpogingen"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "$0 bestandssysteem"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 vrij"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 uur"
+msgstr[1] "$0 uren"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 belangrijke treffer"
+msgstr[1] "$0 treffers, inclusief de belangrijke"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 is een bestaand bestand"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 is in gebruik"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 is van geen enkele repository beschikbaar."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 sleutel gewijzigd"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 is afgeschoten met signaal $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 treffer met lage ernst"
+msgstr[1] "$0 treffers met lage ernst"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minuut"
+msgstr[1] "$0 minuten"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 matige treffer"
+msgstr[1] "$0 treffers, inclusief de matige"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 maand"
+msgstr[1] "$0 maanden"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 meer..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 pakket"
+msgstr[1] "$0 pakketten"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 pakket moet opnieuw worden opgestart"
+msgstr[1] "$0 pakketten moeten opnieuw worden opgestart"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 pagina"
+msgstr[1] "$0 pagina's"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "partitie"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 beveiligingsreparatie beschikbaar"
+msgstr[1] "$0 beveiligingsreparaties beschikbaar"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 service is mislukt"
+msgstr[1] "$0 services zijn mislukt"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 service moet opnieuw worden gestart"
+msgstr[1] "$0 services moeten opnieuw worden gestart"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 slot blijft over"
+msgstr[1] "$0 slots blijven over"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 CPU-piek"
+msgstr[1] "$0 CPU-pieken"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0 gesynchroniseerd"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 totaal"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 update beschikbaar"
+msgstr[1] "$0 updates beschikbaar"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 gebruikt van $1 ($2 opgeslagen)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 week"
+msgstr[1] "$0 weken"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 zal geïnstalleerd worden."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 jaar"
+msgstr[1] "$0 jaren"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 zone"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: crash op $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (van $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Partitie $target aanmaken"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Lokale aankoppelpunten"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Niet aangekoppeld"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(aanbevolen)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", inclusief $1 beveiligingsreparatie"
+msgstr[1] ", inclusief $1 beveiligingsreparaties"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 dag"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 uur"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 min"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 minuut"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 week"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10e"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11e"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12e"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13e"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14e"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 min"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "14e"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16e"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17e"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18e"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19e"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1e"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 minuten"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29e"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31e"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3e"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 minuten"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4e"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 min"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minuten"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5e"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 uren"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 minuten"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6e"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7e"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8e"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9de"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Er is geen compatibele versie van Cockpit geïnstalleerd op $0."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "Er bestaat al een bestandssysteem met deze naam in deze pool."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "Er bestaat al een groep met deze naam"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Een moderne browser is vereist voor goede beveiliging, betrouwbaarheid en "
+"prestaties."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Een netwerkverbinding combineert meerdere netwerkinterfaces tot één logische "
+"interface met een hogere doorvoer of redundantie."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Een nieuwe SSH-sleutel op $0 wordt gemaakt voor $1 op $2 en deze wordt "
+"toegevoegd aan het $3-bestand van $4 op $5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Een pakket moet opnieuw worden opgestart om de updates van kracht te laten "
+"worden:"
+msgstr[1] ""
+"Sommige pakketten moeten opnieuw worden opgestart om de updates van kracht "
+"te laten worden:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Er bestaat al een pool met deze naam."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"Een service moet opnieuw worden gestart om de updates van kracht te laten "
+"worden:"
+msgstr[1] ""
+"Sommige services moeten opnieuw worden gestart om de updates van kracht te "
+"laten worden:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr ""
+"Een volumegroep met ontbrekende fysieke volumes kan niet worden hernoemd."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP-bewaking"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP-ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Over Web Console"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Afwezig"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Accepteer sleutel en log in"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "Acceptabel wachtwoord"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Accountverloop"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Account niet beschikbaar of kan niet worden bewerkt."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Accounts"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Actie"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Acties"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Activeren"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Activeren $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Actief"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Actieve back-up"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Actieve pagina's"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Actief sinds "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Actieve toestand"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Adaptieve taakverdeling"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Adaptieve overdrachtstaakverdeling"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Toevoegen"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Toevoegen $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "Voeg DNS-server toe"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Voeg netwerkgebonden schijfversleuteling toe"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Voeg Tang sleutelserver toe"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "VLAN toevoegen"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "VPN toevoegen"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "WireGuard VPN toevoegen"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "Voeg een bitmap toe"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Een nieuwe zone toevoegen"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "Voeg adres toe"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Voeg blokapparaten toe"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Binding toevoegen"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Brug toevoegen"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Schijf toevoegen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Schijven toevoegen"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "iSCSI-portaal toevoegen"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Sleutel toevoegen"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "Voeg sleutelserver toe"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Voeg lid toe"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Nieuwe host toevoegen"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Nieuwe zone toevoegen"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "Voeg wachtzin toe"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "Voeg gelijke toe"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "LVM2 physical volume"
+msgid "Add physical volume"
+msgstr "LVM2 fysieke volume"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Poorten toevoegen"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Poorten toevoegen aan $0 zone"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Publieke sleutel toevoegen"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "Voeg route toe"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "Voeg zoekdomein toe"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Services toevoegen"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Services toevoegen aan $0 zone"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Services toevoegen aan zone $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Team toevoegen"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Zone toevoegen"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "\"$0\" toevoegen aan versleutelingsopties"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "\"$0\" toevoegen aan bestandssysteemopties"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Toevoegen van $0 verbreekt de verbinding met de server en maakt de "
+"beheerdersinterface onbereikbaar."
+
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Om een sleutelserver toe te voegen, moet de pool worden ontgrendeld. Geef de "
+"bestaande wachtwoordzin voor de pool op."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Door aangepaste poorten toe te voegen, wordt 'firewalld' opnieuw geladen. "
+"Herladen zal leiden tot het verlies van elke runtime-only-configuratie!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Toevoegen sleutel"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Toevoegen van fysieke volume aan $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "rd.neednet=1 toevoegen aan kernelcommandoregel"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Extra DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Extra DNS-zoekdomeinen $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Extra acties"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Extra adres $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Extra pakketten:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Extra poorten"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Adres"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Adres $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Adres mag niet leeg zijn"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Adres is geen geldige URL"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Adressen"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "Adressen zijn niet correct geformatteerd"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Beheer met Cockpit Web Console"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Administratieve toegang"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr "Beheerderstoegang is vereist om rapporten te maken en te openen."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Beheerderstoegang vereist"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Geavanceerde TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Na"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Na het verlaten van het domein kunnen alleen gebruikers met lokale "
+"referenties inloggen op deze machine. Dit kan ook andere services "
+"beïnvloeden, aangezien de DNS-resolutie-instellingen en de lijst met "
+"vertrouwde CA's kunnen veranderen."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Na het opstarten van het systeem"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Alarm"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Alert en hoger"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Alias"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Alle"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+"Alle $0 geselecteerde fysieke volumes zijn nodig voor de gekozen lay-out."
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Alle vernieuwingen"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Alles in een"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Sta uitvoeren toe (ontmaskeren)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "Toegestane IP's"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Toegestane adressen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "Er moet een extra $0 worden geselecteerd"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Ansible rollen documentatie"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Elke tekenreeks in de logboekberichten kan worden gefilterd. De string kan "
+"ook de vorm hebben van een reguliere expressie. Ondersteunt ook filteren op "
+"berichtlogboekvelden. Dit zijn door spaties gescheiden waarden, in de vorm "
+"FIELD=WAARDE, waarbij de waarde een door komma's gescheiden lijst van "
+"mogelijke waarden kan zijn."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Uiterlijk"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "Toepassingsinformatie ontbreekt"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Toepassingen"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Toepassingenlijst"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Toepassen en opnieuw opstarten"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Pas kernel live-patches toe"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Deze oplossing toepassen"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Nieuw beleid toepassen... Dit kan enkele minuten duren."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Oplossing toepassen..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Toepassen van vernieuwingen"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Toepassen van vernieuwingen mislukte"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "Geschikt voor kritieke koppelingen, zoals /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Apps"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Beoordeling"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Asset-tag"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "Bij opstarten"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Minimaal $0 schijf is noodzakelijk."
+msgstr[1] "Minimaal $0 schijven zijn noodzakelijk."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "Minimaal één blokapparaat is noodzakelijk."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Minimaal een schijf is noodzakelijk."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "Op minuut"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "Ten tweede"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "Op specifiek tijdstip"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Attributen"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Audit-logboek"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Authenticatie uitvoeren"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Authenticatie uitvoeren"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Authenticatie"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Authenticatie mislukte"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Authenticatie mislukte: Server verbrak verbinding"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Authenticatie is vereist voor het uitvoeren van bevoorrechte taken met de "
+"Cockpit Web Console"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Authenticatie is vereist"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Autoriseer SSH-sleutel"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Geautoriseerde openbare SSH-sleutels"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automatisch"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automatisch (alleen DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Automatisch inloggen"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Automatische vernieuwingen"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Automatisch starten"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Automatisch gebruik van NTP"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Automatisch gebruik van extra NTP servers"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Automatisch gebruik van specifieke NTP servers"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Automatiserings-script"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Beschikbare doelen op $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Beschikbare vernieuwingen"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "Grow partition"
+msgid "BIOS boot partition"
+msgstr "Laat partitie groeien"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "BIOS datum"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "BIOS versie"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Terug naar accounts"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Slecht"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Slechte instelling"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Balancer"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Voor"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Bindt aan"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Zwart"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Antenne"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Antennebehuizing"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "Blokapparaat"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Blok apparaat voor bestandsystemen"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Blokapparaten"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Geblokkeerd"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Binding"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Boot"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"Opstarten mislukt als het bestandssysteem niet aankoppelt, waardoor toegang "
+"op afstand wordt voorkomen"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "Het opstarten lukt nog steeds als het bestandssysteem niet aankoppelt"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Gebonden door"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Brug"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Brugpoort"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Brugpoort instellingen"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Uitzending"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Defecte configuratie"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Updates voor bugreparaties beschikbaar"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Bugs"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Busuitbreidingschassis"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Door het wachtwoord van de SSH-sleutel $0 te wijzigen in het inlogwachtwoord "
+"van $1 op $2, wordt de sleutel automatisch beschikbaar gemaakt en kun je in "
+"de toekomst zonder wachtwoord inloggen op $3."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " Browsercontrole omzeilen "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "CPU-beveiliging"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "CPU-beveiliging schakelaars"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "CPU-gebruik"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "CPU-gebruik/belasting"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Cache"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "Kan een hostnaam, IP-adres, aliasnaam, of ssh:// URI zijn"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr "Kan geen logboeken vinden met de huidige combinatie van filters."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Annuleren"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Uitschakelen annuleren"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Opnieuw starten annuleren"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Kan niet worden ingeschakeld"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Kan niet verbinden met een onbekende host"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Kan inloggegevens niet doorsturen"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+"Kan geen lid worden van een domein omdat realmd niet beschikbaar is op dit "
+"systeem"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Kan evenement niet in het verleden plannen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Capaciteit"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Carrier"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Verandering"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "Wijzig cryptografisch beleid"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Verander hostnaam"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Verander naam van de iSCSI-initiator"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Verander naam van de iSCSI-initiator"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change shell"
+msgid "Change label"
+msgstr "Verander shell"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Verander wachtzin"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Verander wachtwoord"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Verander prestatieprofiel"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Verander profiel"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "Verander shell"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Verander systeemtijd"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Verander het wachtwoord van $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Verander de instellingen"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Gewijzigde sleutels zijn vaak het resultaat van een herinstallatie van het "
+"besturingssysteem. Een onverwachte wijziging kan echter wijzen op een poging "
+"van derden om je verbinding te onderscheppen."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Als je de instellingen wijzigt, wordt de verbinding met de server verbroken "
+"en is de beheerdersinterface niet beschikbaar."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Controleer op updates"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"Controleer of de SHA-256- of SHA-1-hash van de opdracht overeenkomt met dit "
+"dialoog."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "Controleer de sleutel-hash met de Tang-server."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Controleren van $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Controleren van IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "Controleren van RAID-apparaat $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Controleren en repareren van RAID-apparaat $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Controleren op $0 pakket"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Controleren op NBDE-ondersteuning in de initrd"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Controleren op nieuwe toepassingen"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Controleren op pakketvernieuwingen..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Controleren op geïnstalleerde software"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Controleren op gerelateerde processen"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Controleren van softwarestatus"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Kies de taal die in de toepassing moet worden gebruikt"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Brok grootte"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Klasse"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Opschonen voor $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Wis 'Kan niet starten'"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Wis alle filters"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Wis zoekopdrachten"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Cleartext-apparaat"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Cliënt software"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Sluiten"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Sluit geselecteerde pagina's"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Cockpit authenticatie is verkeerd geconfigureerd."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "Cockpit configuratie van NetworkManager en Firewalld"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit kon geen contact maken met de opgegeven host."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit had een onverwachte interne fout."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit is een serverbeheerder waarmee je Linux-servers eenvoudig kunt "
+"beheren via een webbrowser. Omschakelen tussen de terminal en het "
+"webgereedschap is geen probleem. Een service gestart via Cockpit kan via de "
+"terminal worden gestopt. Evenzo, als er een fout optreedt in de terminal, "
+"kan deze worden gezien in de Cockpit-logboekinterface."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit is een interactieve Linux-serverbeheerinterface."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit is niet compatibel met de software op het systeem."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit is niet geïnstalleerd"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit is niet op het systeem geïnstalleerd."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit is perfect voor nieuwe systeembeheerders, waardoor ze eenvoudig "
+"eenvoudige taken kunnen uitvoeren, zoals opslagbeheer, het inspecteren van "
+"logboeken en het starten en stoppen van services. Je kunt meerdere servers "
+"tegelijkertijd bewaken en beheren. Voeg ze gewoon toe met een enkele klik en "
+"je machines zullen voor zijn maatjes zorgen."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit wordt mogelijk niet correct weergegeven in je browser"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Verzamel en verpak diagnostische en ondersteuningsdata"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Verzamel kernelcrashdumps"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Verzamel metriek"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Kleur"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr ""
+"Door komma's gescheiden poorten, bereiken en services worden geaccepteerd"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Commando"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Commando niet gevonden"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Commentaar"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Communicatie met tuned is mislukt"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Compact PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Compatibel met alle systemen en apparaten (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Verenigbaar met moderne systemen en harde schijven > 2TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Comprimeer crashdumps om ruimte te besparen"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Compressie"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Aan conditie $0=$1 werd niet voldaan"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Conditie mislukte"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Configuratie"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Configureren"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Configureren van IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "kdump configureren"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Systeeminstellingen configureren"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Bevestigen"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Bevestig het verwijderen van $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Bevestig sleutelwachtwoord"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Bevestig nieuw sleutelwachtwoord"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Bevestig nieuw wachtwoord"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Bevestigen wachtwoord"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Bevestig het verwijderen van $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Bevestig verwijderen met een alternatieve wachtzin"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Bevestig het stoppen van $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "Conflict met"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Conflicten"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Verbind automatisch"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Verbinden met"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Verbinden met:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Verbinding maken met SETroubleshoot daemon..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Verbinding met dbus mislukte: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Verbinding maken met de machine"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Verbindingsfout"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Verbinding mislukte"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Er is een time-out opgetreden voor de verbinding."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "Verbinding wordt verbroken"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Bestaat uit"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Gecontacteerd domein"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Bevat:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Doorgaan"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Vervolg sessie"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Controle"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Controle"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Controleur"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Converteerbaar"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Gekopieerd"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Kopiëren"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Kopiëren naar clipboard"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Kern $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "Kon geen contact opnemen met $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Crashdump locatie"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Crashrapportage"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Crash systeem"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Aanmaken"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "Aanmaken LVM2-volumegroep"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "Maak RAID-apparaat aan"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Maak RAID-apparaat aan"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Maak Stratus pool aan"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Maak een nieuwe SSH-sleutel en autoriseer deze"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Snapshot van bestandssysteem $0 aanmaken"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Account aanmaken met niet-unieke UID"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Account aanmaken met zwak wachtwoord"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Maak een thuismap aan en wijzig het eigendom"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Aanmaken en aankoppelen"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create and mount"
+msgid "Create and start"
+msgstr "Aanmaken en aankoppelen"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Bestandssysteem aanmaken"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Maak het aan"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Maak logische volume aan"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Nieuw account aanmaken"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Nieuw bestandssysteem aanmaken"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Maak nieuwe groep aan"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Maak nieuwe logische volume aan"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Maak nieuw taakbestand aan met deze inhoud."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "Maak nieuwe logische volume aan"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "Alleen aanmaken"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Partitie aanmaken"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Partitie aanmaken op $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Partitietabel aanmaken"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Maak snapshot aan"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Maak snapshot en koppel aan"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Maak alleen een snapshot"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create storage volume"
+msgid "Create storage device"
+msgstr "Maak opslagvolume aan"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Maak volume aan"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Maak dun volume aan"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Maak timer aan"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Maak volumegroep aan"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Aangemaakt"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "LVM2-Volumegroep $target aanmaken"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "RAID-apparaat $target aanmaken"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "VDO-apparaat aanmaken"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Bestandssysteem op $target aanmaken"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Logisch volume $target aanmaken"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Partitie $target aanmaken"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Snapshot van $target aanmaken"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Het aanmaken van deze $0 zal de verbinding met de server verbreken waardoor "
+"deze beheerdersinterface niet meer beschikbaar zal zijn."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Kritiek en hoger"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Crypto-beleid is een systeemcomponent die de belangrijkste cryptografische "
+"subsystemen configureert, waaronder de TLS-, IPSec-, SSH-, DNSSec- en "
+"Kerberos-protocollen."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "Cryptografisch beleid"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "Crypto-beleid is inconsistent"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Huidige opstartprocedure"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Huidig top CPU-gebruik"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Momenteel in gebruik"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Currently in use"
+msgid "Currently not supported"
+msgstr "Momenteel in gebruik"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "aangepast"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "Aangepast cryptografisch beleid"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Aangepaste aankoppelopties"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Aangepaste poorten"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom path"
+msgid "Custom type"
+msgstr "Aangepast pad"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Aangepaste zones"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "STANDAARD met SHA-1 handtekeningverificatie toegestaan."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS zoekdomeinen"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "DNS zoekdomeinen $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Dagelijks"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Gevaar alert:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Donker"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Data"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Gebruikte data"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"Data wordt als twee kopieën en ook op afwisselende wijze op de geselecteerde "
+"fysieke volumes opgeslagen, om zowel de betrouwbaarheid als de prestaties te "
+"verbeteren. Er moeten minimaal vier volumes worden geselecteerd."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"Data wordt opgeslagen als twee of meer kopieën op de geselecteerde fysieke "
+"volumes, om de betrouwbaarheid te verbeteren. Er moeten minimaal twee "
+"volumes worden geselecteerd."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+"Data wordt afwisselend op de geselecteerde fysieke volumes opgeslagen om de "
+"prestaties te verbeteren. Er moeten minimaal twee volumes worden "
+"geselecteerd."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"Data wordt opgeslagen op de geselecteerde fysieke volumes, zodat een ervan "
+"verloren kan gaan zonder de data te beïnvloeden. Er moeten minimaal drie "
+"volumes worden geselecteerd."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+"Data wordt opgeslagen op de geselecteerde fysieke volumes, zodat een ervan "
+"verloren kan gaan zonder de data te beïnvloeden. Data wordt ook afwisselend "
+"opgeslagen om de prestaties te verbeteren. Er moeten minimaal drie volumes "
+"worden geselecteerd."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+"Data wordt op de geselecteerde fysieke volumes opgeslagen, zodat er maximaal "
+"twee tegelijk verloren kunnen gaan zonder dat dit gevolgen heeft voor de "
+"data. Data wordt ook afwisselend opgeslagen om de prestaties te verbeteren. "
+"Er moeten minimaal vijf volumes worden geselecteerd."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+"Data wordt opgeslagen op de geselecteerde fysieke volumes zonder enige extra "
+"redundantie of prestatieverbeteringen."
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Datumspecificaties moeten het formaat JJJJ-MM-DD uu:mm:ss hebben. Als "
+"alternatief worden de strings 'gisteren', 'vandaag', 'morgen' begrepen. 'nu' "
+"verwijst naar de huidige tijd. Ten slotte kunnen relatieve tijden worden "
+"opgegeven, voorafgegaan door '-' of '+'"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Deactiveren"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Repair logical volume $0"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Herstel logische volume $0"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Wordt gedeactiveerd"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "$target deactiveren"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Debug en hoger"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Verlaag met één"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "Toegewijde pariteit (RAID 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Deduplicatie"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Standaard"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Vertraging"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Vertraging moet een getal zijn"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Verwijderen"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Verwijder $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Account verwijderen"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Verwijder bestanden"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Groep verwijderen"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Verwijderen"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Rapport definitief verwijderen?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Verwijderen van $0 verbreekt de verbinding met de server waardoor de "
+"beheerdersinterface niet meer beschikbaar zal zijn."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Verwijder $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "LVM2-volumegroep $target verwijderen"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "Verwijderen van een Stratis pool zal alle data die het bevat wissen."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "Verwijderen van een bestandssysteem zal alle data erop wissen."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "Verwijderen van een logische volume zal alle data erop wissen."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "Verwijderen van een partitie zal alle data erop wissen."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting erases all data on a RAID device."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Verwijderen vernietigt alle data op een RAID-apparaat."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "Verwijderen vernietigt alle data op een VDO-apparaat."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "Verwijderen vernietigt alle data op een volumegroep."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting erases all data on a volume group."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "Verwijderen vernietigt alle data op een volumegroep."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "Bij verwijderen worden de volgende bestanden verwijderd:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Beschrijving"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Bureaublad"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Demonteerbaar"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Details"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Ontwikkeling"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Apparaat"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Apparaatbestand"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Apparaat is alleen-lezen"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Servicenaam"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Diagnostische rapporten"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Map"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Map $0 is niet schrijfbaar of bestaat niet."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Gelijktijdige multithreading uitschakelen"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Schakel de firewall uit"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Schakel tuned uit"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Uitgeschakeld"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Interactief wachtwoord niet toestaan"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Wachtwoordverificatie niet toestaan"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Uitvoeren niet toestaan (masker)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Verbinding verbreken"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Verbinding verbroken"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Schijf I/O"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Schijf is OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Schijf werkt niet"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Schijf wachtzin"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Schijven"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Schijfgebruik"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "Afwijzen"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "$0 melding afwijzen"
+msgstr[1] "$0 meldingen afwijzen"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Geselecteerde meldingen afwijzen"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Schermtaal"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "Gedistribueerde pariteit (RAID 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "Koppel niet aan"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Niet automatisch aankoppelen tijdens het opstarten"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Docking station"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Start niet automatisch op"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "Wordt niet aangekoppeld tijdens het opstarten"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Domein"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Domein"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Domeinadres"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Domeinbeheerdersnaam"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Domeinbeheerderswachtwoord"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Er kan geen contact gemaakt worden met domein"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Domein wordt niet ondersteund"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Niet herhalen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "Dubbel gedistribueerde pariteit (RAID 6)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Download"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Download gratis een nieuwe browser"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Gedownload"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Downloaden"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "$0 downloaden"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Station"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Dubbele rangorde"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Uitgebreide partitie"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Bewerken"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Bewerk /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Bewerk Tang sleutelserver"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "Bewerk VLAN-instellingen"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "Bewerk WireGuard VPN"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "Bewerk bindingsinstellingen"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "Bewerk bruginstellingen"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Bewerk aangepaste service in $0 zone"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Bewerk host"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Bewerk hosts"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Bewerk motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Aankoppelpunt"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Bewerk regels en zones"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Bewerk service"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Bewerk service $0"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "Bewerk teaminstellingen"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Bewerk gebruiker"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Een sleutel bewerken vereist een vrij slot"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "$target uitwerpen"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "Ingebouwde pc"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Leeg"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Leegmaken"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Leeg wachtwoord"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "$target leegmaken"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Zet aan"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Schakel het apparaat in of uit"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Service inschakelen"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Schakel de firewall in"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Ingeschakeld"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "$0 inschakelen"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Data versleutelen"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Versleutel data met een Tang sleutelserver"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "Versleutel data met een wachtzin"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Versleuteld"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "$0 versleuteld"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted Stratis pool $0"
+msgid "Encrypted Stratis pool"
+msgstr "Versleutelde Stratis pool $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Versleutelde logische volume van $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Versleutelde partitie van $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Versleuteling"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Versleutelingsopties"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Versleutelingswachtzin"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Versleutelingstype"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Beëindigd"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "Eindpunt"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"Het eindpunt dat als \"server\" fungeert, moet worden opgegeven als host:"
+"poort, anders kan het leeg worden gelaten."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Afdwingen"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Verbeteringsupdates beschikbaar"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Voer een geldig MAC-adres in"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Volledig subnet"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Ingang op $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "$target wissen"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Errata"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Fout"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Fout en hoger"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Er is een fout opgetreden"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "Fout bij het installeren van $0: PackageKit is niet geïnstalleerd"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Foutbericht"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr ""
+"Fout tijdens het uitvoeren van semanage om systeemaanpassingen te ontdekken"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Fout tijdens het opslaan van gemachtigde sleutels: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Fout tijdens het instellen van SELinux modus: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "Er moeten exact $0 fysieke volumes worden geselecteerd"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+"Er moeten exact $0 fysieke volumes worden geselecteerd, één voor elke streep "
+"van het logische volume."
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Voorbeeld: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Voorbeeld: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Uitstekend wachtwoord"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Uitbreidingschassis"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Account laten verlopen op"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Account laten verlopen op $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Exporteren"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Exporteer naar netwerk"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Uitgebreide informatie"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Uitgebreide partitie"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS is niet correct ingeschakeld"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS met verdere Common Criteria-beperkingen."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Mislukt"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Kan machine niet toevoegen: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Kan poort niet toevoegen"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Kan service niet toevoegen"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Kan zone niet toevoegen"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Kan wachtwoord niet veranderen"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "Kan PCP niet configureren"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Kan waarschuwing niet verwijderen: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Kan tuned niet uitzetten"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Kan tuned profiel niet uitzetten"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Kan machine niet bewerken: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Service bewerken mislukte"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "Kan $0 niet inschakelen in firewalld"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Kan tuned niet inschakelen"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Kan logboeken niet ophalen"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Kan gemachtigde sleutels niet laden."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Kan unit niet laden"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"Kan de eenheidsbestanden voor dnf-automatic.timer of dnf-automatic-install."
+"timer niet ontleden. Verwijder aangepaste overschrijvingen om automatische "
+"updates te configureren."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Kan service niet opnieuw opstarten"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Kan veranderingen in /etc/motd niet opslaan"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Kan instellingen niet opslaan"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Starten mislukt"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Kan profiel niet wijzigen"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Bestandstatus"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Bestandssysteem"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "Bestandssysteem is vergrendeld"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Bestandssysteemnaam"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Bestandssysteem op $target aanmaken"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "Bestandssystemen zijn al aangekoppeld op dit aankoppelpunt."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Filteren op naam of beschrijving"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Filtermenu items"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Filter services"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Filters"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Vingerafdruk"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Firewall"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Firewall is niet beschikbaar"
+
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Firmwareversie"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "NBDE-ondersteuning repareren"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Lettertypegrootte"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Uitvoeren is verboden"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Forceer verandering"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Forceer verwijderen"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Forceer wachtwoordverandering"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Formatteren"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "$0 formatteren"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Formatteren en aankoppelen"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format and mount"
+msgid "Format and start"
+msgstr "Formatteren en aankoppelen"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Alleen formatteren"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "Formatteren verwijdert alle data op een opslagapparaat."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Voorwaartse vertraging $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Framenummer"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Vrije ruimte"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Zoeken in vrije vorm"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "vrijdagen"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "Van"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Volledige naam"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Gateway"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Algemeen"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Gegenereerd"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Ga naar $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Ga naar toepassing"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Ga nu naar"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Grafiek zichtbaarheid"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Optiemenu grafiek zichtbaarheid"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Groep"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Naam van groep"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Groepen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Vergroten"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Vergroot inhoud"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Vergroot logische grootte van $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Vergroot logische volume"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "Laat partitie groeien"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "Vergroot de pool om alle ruimte in te nemen"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Vergroot om alle ruimte in te nemen"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Haarspeld modus"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Haarspeld modus"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Handheld"
+
+#: pkg/storaged/drive/drive.jsx:64
+#, fuzzy
+#| msgid "Hard Disk"
+msgid "Hard Disk Drive"
+msgstr "Harde schijf"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Hardware informatie"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Gezondheid"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Hallo tijd $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Hulp"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "Bevestigingswachtwoord verbergen"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "Wachtwoord verbergen"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "Hiërarchie-ID"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "Hogere interoperabiliteit ten koste van een groter aanvalsoppervlak."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Geschiedenis van pakketaantallen"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Thuismap"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Host"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Hostsleutel is onjuist"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Hostnaam"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Hosts"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Elk uur"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Uren"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "Hoe te controleren"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP-adres"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"IP-adres met routeprefix. Scheid meerdere waarden met een komma. Voorbeeld: "
+"192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "IPv4-adressen"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4-instellingen"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6-instellingen"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Identificatie"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Indien leeg gelaten, wordt ID gegenereerd op basis van bijbehorende "
+"poortservices en poortnummers"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Als de vingerafdruk overeenkomt, klik dan op \"Accepteer sleutel en log "
+"in\". Log anders niet in en neem contact op met je beheerder."
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Als de vingerafdruk overeenkomt, klik dan op 'Vertrouw en host toevoegen'. "
+"Verbind anders niet en neem contact op met je beheerder."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Negeren"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "In"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "Voer in een terminal uit: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Om in de toekomst inloggen op $0 als $1 zonder wachtwoord toe te staan, "
+"gebruik je het inlogwachtwoord van $2 op $3 als het sleutelwachtwoord, of "
+"laat je het sleutelwachtwoord leeg."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "Synchroon"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Inactief"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "Maak logische volume aan"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Inbegrepen services"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"Inkomende verzoeken worden standaard geblokkeerd. Uitgaande verzoeken worden "
+"niet geblokkeerd."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Inconsistente bestandssysteemkoppeling"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Verhogen met één"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Indexgeheugen"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Indirect"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Info"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Info en hoger"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Initialiseren"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Initialiseren van schijf $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "Initialiseren verwijdert alle data op een schijf."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Initialiseren ..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Inzichten: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Installeren"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Installeer NFS-ondersteuning"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Installeer Stratis-ondersteuning"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Installeer alle updates"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "Installeer toepassingsinformatie"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Installeer cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Installeer kpatch updates"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "Installeer realmd-ondersteuning"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Installeer beveiligingsupdates"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr ""
+"Installeer setroubleshoot-server om SELinux-gebeurtenissen op te lossen."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Installeer software"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Installeer het $0 pakket"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr "Installatie niet ondersteund zonder geïnstalleerd cockpitpakket"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Geïnstalleerd"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Installeren"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "$0 installeren"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "Als je $0 installeert, wordt $1 verwijderd."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Pakketten installeren"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Interface"
+msgstr[1] "Interfaces"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Interface leden"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Interfaces"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Interne fout"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Interne fout: ongeldige exceptie koptekst"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Ongeldig"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Ongeldig adres $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Ongeldige datum"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Ongeldige datumnotatie en ongeldige tijdnotatie"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Ongeldige vervaldatum"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Ongeldige bestandsrechten"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Ongeldige sleutel"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Ongeldige metriek $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Ongeldig aantal dagen"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Ongeldig poortnummer"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Ongeldig voorvoegsel $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Ongeldig voorvoegsel of netmasker $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Ongeldig bereik"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Ongeldige tijdnotatie"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Ongeldige tijdzone"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Ongeldige gebruikersnaam of wachtwoord"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "IoT-gateway"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "Werkt sshd op een andere poort?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Taken"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Toetreden"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Word lid van een domein"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Word lid van domein"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Toetreden"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Lid worden van een domein vereist de installatie van realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Deelnemen aan dit domein wordt niet ondersteund"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Word lid van de naamruimte van"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Logboek"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Logboekingang"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Logboekingang werd niet gevonden"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Spring naar"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit is niet geïnstalleerd"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Test kdump instellingen"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Houd verbinding"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Kernelcrashdump"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Kerneldump"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "Kernel live-patch $0 is actief"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Kernel live-patch $0 is geïnstalleerd"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Kernel live-patch instellingen"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Kernel live patchen"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Sleutelwachtwoord"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Sleutelslots met onbekende typen kunnen hier niet worden bewerkt"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Sleutelbron"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Sleutels"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Sleutelserver"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Sleutelserveradres"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "Het verwijderen van keyserver kan voorkomen dat $0 wordt ontgrendeld."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP-sleutel"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "LEGACY met Active Directory-interoperabiliteit."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "VDO pool"
+msgid "LVM2 VDO pool"
+msgstr "VDO-pool"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Logische volume"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Logische volumes"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "LVM2 fysieke volume"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "LVM2 physical volume"
+msgid "LVM2 physical volumes"
+msgstr "LVM2 fysieke volume"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM2 volumegroep"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "LVM2 volumegroep $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Laptop"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Laatste 24 uur"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Laatste 7 dagen"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Laatst actief"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "De laatste sleutelslot kan niet verwijderd worden"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Laatst gecontroleerd: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "De laatste sleutelslot kan niet verwijderd worden"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Laatste aanmelding"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Laatst gewijzigd: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Laatste succesvolle aanmelding:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "Indeling"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Kom meer te weten"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "Verlaat $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Verlaat domein"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Laat leeg om versleuteling over te slaan"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Licentie onder GNU LGPL versie 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Licht"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Beperk toegang"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Beperkte toegang"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"De beperkte toegangsmodus beperkt de beheerdersrechten. Sommige delen van de "
+"webconsole hebben een verminderde functionaliteit."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Grenzen"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "Lineair"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Link down vertraging"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Link lokaal"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Link monitoring"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Link up vertraging"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Link toezicht"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Gekoppeld"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem $0"
+msgid "Linux filesystem data"
+msgstr "Bestandssysteem $0 afkoppelen"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Swapruimte"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Luisteren"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "Luisterpoort"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "Luisterpoort moet een getal zijn"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Eenheden weergeven"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Eenheden weergeven mislukte: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Belasting"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Taakverdeling"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Laad eerdere data"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Laad eerdere ingangen"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Het laden van beschikbare updates is mislukt"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Beschikbare updates worden geladen, even geduld..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Laad eerdere ingangen"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Sleutels laden..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Het laden van SSH-sleutels is mislukt"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Het laden van eenheden mislukte"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Pakketten laden..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Systeemwijzigingen laden..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Het laden van unit mislukte"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Laden..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Lokale accounts"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Lokaal bestandssysteem"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Lokale aankoppelpunten"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "No storage"
+msgid "Local storage"
+msgstr "Geen opslag"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "lokaal in $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Locatie"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Slot"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "Vergrendel $0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Account vergrendelen"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgid "Locked"
+msgid "Locked data"
+msgstr "Afgesloten"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "$target vergrendelen"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Inloggen"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Log in op $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Log in met je servergebruikersaccount."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Log berichten"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Uitloggen"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Log de gebruiker uit"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Ingelogd"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Logisch"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Logische volume (Snapshot)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Logische grootte"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Logische volume"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Logische volume (Snapshot)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Logische volume van $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Log in"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Log opnieuw in"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Inloggen mislukte"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Inlogformaat"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Inloggeschiedenis"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Inloggeschiedenislijst"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "$0 uitloggen"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Uitloggen succesvol"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Logboeken"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Laag profiel bureaublad"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Lunchbox"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "RAID-apparaat"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "RAID-apparaat $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Apparaat is alleen-lezen"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "Service is actief"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "RAID-apparaat"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Schijven toevoegen"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (Aanbevolen)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU moet een positief getal zijn"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Machine-ID"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Machine SSH-sleutel vingerafdrukken"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Hoofdserverchassis"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Onderhoud"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+"Kwaadwillende pagina's op een externe machine kunnen andere aangesloten "
+"hosts beïnvloeden"
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "Beheer bestandssysteemgroottes"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Beheerde opslag"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Beheerde interfaces"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "LVM's beheren"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "NFS-aankoppelingen beheren"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "RAID's beheren"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "VDO's beheren"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "VLAN's beheren"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Firewall beheren"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Netwerkbindingen beheren"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Netwerkbruggen beheren"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Netwerkteams beheren"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Partities beheren"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Fysieke schijven beheren"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Services beheren"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Software-updates beheren"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Gebruikersaccounts beheren"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Handmatig"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Handmatig"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "$target als defect markeren"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Masker service"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Gemaskeerd"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Service maskeren voorkomt dat alle afhankelijke eenheden worden uitgevoerd. "
+"Dit kan een grotere impact hebben dan verwacht. Bevestig dat je deze eenheid "
+"wilt maskeren."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Maximale berichtleeftijd $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Optisch station"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Geheugen"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Geheugen technologie"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Geheugengebruik"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Geheugengebruik/-swap"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Samengevoegd"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Bericht aan ingelogde gebruikers"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr ""
+"Berichten met betrekking tot de fout zijn mogelijk te vinden in het journaal:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Gebruikte metadata"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Methode"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Metriek"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Statistieken en geschiedenis"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Statistieken kon niet worden geladen"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Metriekinstellingen"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Mini tower"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Minuut moet een getal tussen 0-59 zijn"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "Per minuut"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minuten"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr "Gespiegeld (RAID 1)"
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Beperkende factoren"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Modus"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Model"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "$target aanpassen"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "maandagen"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Bewakingsinterval"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Doelen controleren"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Maandelijks"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Meer info..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Aankoppelen"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "Aankoppelpunt"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "Aankoppelen nadat het netwerk beschikbaar is, negeer de fout"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Koppel ook automatisch aan tijdens het opstarten"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Koppel aan tijdens het opstarten"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Koppel automatisch aan op $0 tijdens het opstarten"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Aankoppelen voordat de services starten"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Aankoppelconfiguratie"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Bestandssysteem aankoppelen"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Koppel nu aan"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Koppel nu aan op $0"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Aankoppelopties"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Aankoppelpunt"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Aankoppelpunt mag niet leeg zijn"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Aankoppelpunt mag niet leeg zijn."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Aankoppelpunt wordt al gebruikt voor $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Aankoppelpunt moet beginnen met \"/\"."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Koppel alleen-lezen aan"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Aankoppelen zonder te wachten, negeer mislukken"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "$target aankoppelen"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Aankoppelen voordat services starten"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Aankoppelen in parallel met services"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+"Wordt parallel met services gekoppeld, maar nadat het netwerk beschikbaar is"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Chassis met meerdere systemen"
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Multipathed devices"
+msgid "Multipathed devices"
+msgstr "Multipad apparaten"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+"Er kunnen meerdere adressen worden opgegeven met komma's of spaties als "
+"scheidingstekens."
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS-aankoppeling"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP-server"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Naam"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Naam mag niet leeg zijn."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Naam mag niet leeg zijn."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Naam mag niet langer zijn dan $0 bytes"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Naam mag niet langer dan $0 lettertekens"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Naam mag niet langer zijn dan 127 lettertekens."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Naam mag niet langer zijn dan 127 lettertekens."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Naam mag het letterteken '$0' niet bevatten."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "Naam mag het letterteken '$0' niet bevatten."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Naam mag geen spaties bevatten."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Minimaal één NTP-server nodig"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Netwerk"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Netwerk I/O"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Netwerkbinding"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Netwerkapparaten en grafieken vereisen NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Netwerklogboeken"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Netwerkgebruik"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager is niet geïnstalleerd"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager is niet actief"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Netwerkgebruik"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Netwerken"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Nooit"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Account nooit laten verlopen"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Verloop het wachtwoord nooit"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Nooit ingelogd"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Nieuwe NFS-aankoppeling"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Nieuwe host"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "Nieuwe host: $0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Nieuw sleutelwachtwoord"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Nieuwe naam"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Nieuwe wachtzin"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Nieuw wachtwoord"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Nieuw wachtwoord is niet geaccepteerd"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Volgende"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Nee"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "Geen ID opgegeven"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Geen SELinux-waarschuwingen."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Geen toepassingen geïnstalleerd of beschikbaar."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Geen slots beschikbaar"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Geen blokapparaten beschikbaar."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "Geen opstartapparaat gevonden"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Geen signaal"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Geen configuratie gevonden"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Geen data beschikbaar"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Geen data beschikbaar tussen $0 en $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Geen vertraging"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Geen beschrijving beschikbaar"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Geen beschrijving aangeboden."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "Geen opstartapparaat gevonden"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Geen schijven beschikbaar."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Geen logs gevonden"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "Geen resultaten gevonden"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Geen versleuteling"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Geen evenementen"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Geen bestandssysteem"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Geen bestandssystemen"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Geen vrije sleutel slots"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Geen vrije ruimte"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "Geen vrije ruimte na deze partitie"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Geen groepnaam opgegeven"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Geen hostsleutels gevonden ."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Geen installatiepakket gevonden voor deze toepassing."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Geen sleutels toegevoegd"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "Er komen geen talen overeen"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Geen logboekvermeldingen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Geen logische volumes"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Geen logs gevonden"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Geen overeenkomende resultaten"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Geen media geplaatst"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Geen partitionering"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "Geen partitionering"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "Geen gelijken toegevoegd."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "Fysieke volumes"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Geen echte naam opgegeven"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Geen resultaten gevonden"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Geen resultaten komen overeen met de filtercriteria. Wis alle filters om "
+"resultaten weer te geven."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Geen regeltreffers"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No storage"
+msgid "No storage found"
+msgstr "Geen opslag"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Geen logische volumes"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Bestand of map bestaat niet"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Geen systeemwijzigingen"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Geen systeemrapporten."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Geen updates"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Geen gebruikersnaam opgegeven"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Geen"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Geen geldige privésleutel"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Niet gemachtigd om de firewall uit te schakelen"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Niet gemachtigd om de firewall in te schakelen"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Niet beschikbaar"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Niet verbonden"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Niet verbonden met Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Niet verbonden met host"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space"
+msgid "Not enough free space"
+msgstr "Niet genoeg ruimte"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "Niet genoeg ruimte"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "Niet genoeg ruimte om te groeien."
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Niet gevonden"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Niet geïnstalleerd"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "Not permitted to configure realms"
+msgid "Not permitted to configure network devices"
+msgstr "Het is niet toegestaan om realms te configureren"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Het is niet toegestaan om realms te configureren"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Niet toegestaan om deze actie uit te voeren."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Niet klaar"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Niet geregistreerd"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Niet actief"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Niet ingesteld"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Niet gesynchroniseerd"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Notitie"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Notebook"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Kennisgeving en hoger"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Verduister netwerkadressen, hostnamen en gebruikersnamen"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Verduisterd"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Heeft plaatsgevonden $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Heeft plaatsgevonden tussen $0 en $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Voorvallen"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "OK"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Oude wachtzin"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Oud wachtwoord"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Oude wachtwoord niet geaccepteerd"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "Op een aangekoppeld apparaat"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Bij mislukking"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Nadat Cockpit geïnstalleerd is, schakel je het in met \"systemctl enable --"
+"now cockpit.socket\"."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Alleen $0 van $1 worden gebruikt."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Alleen letters, cijfers, : , _ , . , @ , - zijn toegestaan"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Alleen noodgevallen"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"Gebruik alleen goedgekeurde en toegestane algoritmen bij het opstarten in "
+"FIPS-modus."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Ooops!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "Open de pmproxy-service in de firewall om metrische gegevens te delen."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Bewerking '$operation' op $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Opties"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Of gebruik een gebundelde browser"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Andere"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"Andere authenticatiemethoden zijn nog steeds beschikbaar, zelfs als "
+"interactieve wachtwoordauthenticatie niet is toegestaan."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Andere opties"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Uit"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Overzicht"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Overschrijven"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Bestaande data overschrijven met nullen (langzamer)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Pakket cockpit-pcp ontbreekt voor metrische geschiedenis"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Pakketinformatie"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit is gecrasht"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit is niet geïnstalleerd"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit rapporteerde foutcode $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Pakketten"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Paginanaam"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Ouder"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Ouder $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Onderdeel van"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Onderdeel van $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Partitie"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Partitie van $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "Partitiegrootte is $0. Inhoudsgrootte is $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Partitionering"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Partities"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Passief"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Wachtzin"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "Wachtzin mag niet leeg zijn"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Wachtzin mag niet leeg zijn"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Wachtzin van een ander sleutelslot"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Verwijdering van wachtzin kan voorkomen dat $0 wordt ontgrendeld."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Wachtzinnen komen niet overeen"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Wachtwoord"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Wachtwoord succesvol gewijzigd"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Wachtwoord verloop"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "Wachtwoord is langer dan 256 lettertekens"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Wachtwoord is niet acceptabel"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Wachtwoord is te zwak"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Wachtwoord moet veranderd worden"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Wachtwoord wordt niet geaccepteerd"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Wachtwoord tip"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Plakken"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Plakfout"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "Plak een bestaande sleutel"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Plak hier de inhoud van je openbare SSH-sleutelbestand"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Pad"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Padkosten"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Padkosten $0"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Pad op server"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Pad op server mag niet leeg zijn."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Pad op server moet beginnen met \"/\"."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Pad naar de map"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Pad naar bestand"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Paden"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Pauze"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr "Peer #$0 heeft een ongeldige eindpuntpoort. Poort moet een getal zijn."
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"Peer #$0 heeft een ongeldig eindpunt. Het moet worden opgegeven als host:"
+"poort, b.v. 1.2.3.4:51820 of voorbeeld.com:51820"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "Peers"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"Peers zijn andere machines die verbinding maken met deze. Openbare sleutels "
+"van andere machines worden met elkaar gedeeld."
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot verzamelt en analyseert prestatiestatistieken van jouw "
+"systeem."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Prestatieprofiel"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Randchassis"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Blijvend"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "$0 groep permanent verwijderen?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "$0 permanent verwijderen?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "$0 permanent verwijderen?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete subvolume $0?"
+msgstr "$0 permanent verwijderen?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Toestemming geweigerd"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Toegeeflijk"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Fysiek"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "Fysieke volumes"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Fysieke volumes"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "Fysieke volumes kunnen hier niet aangepast worden."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Kies datum"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Pin-eenheid"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Ping interval"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Ping doel"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Vastgezette eenheid"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Pizzadoos"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Authenticeer om administratieve toegang te krijgen"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Zet JavaScript aan om de Webconsole te gebruiken."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Installeer het $0 pakket"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Los het probleem op en laad deze pagina opnieuw."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Geef een vervaldatum op"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Geef de host op waarmee je verbinding wilt maken"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "Ontkoppel ze eerst."
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Pool voor dunne logische volumes"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Pool voor dun ingerichte volumes"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Pool voor dun ingerichte volumes"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "Pool wachtzin"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Poort"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Draagbaar"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Poorten"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Partitie aanmaken"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Voorvoegsel-lengte"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Voorvoegsel-lengte of netmasker"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Voorbereiden"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Aanwezig"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Behouden"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Mooie hostnaam"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Vorige opstart"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Primair"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Prioriteit"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Prioriteit $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "Privé sleutel"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "Probleem om beheerder te worden"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Probleemdetails"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Probleem info"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Processen die de locatie gebruiken"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Voortgang: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Project website"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Vragen via wachtwoord is verlopen"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Vragen via ssh-add is verlopen"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Vragen ssh-keygen is verlopen"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Propageren herladen naar"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Beschermt tegen verwachte toekomstige aanvallen op korte termijn ten koste "
+"van interoperabiliteit."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "Geef de wachtzin voor de pool op deze blokapparaten:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Publieke sleutel"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+"Er wordt een publieke sleutel gegenereerd wanneer een geldige privésleutel "
+"wordt ingevoerd"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Doel"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (stripe)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (spiegel)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (stripe van spiegels)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (toegewijde pariteit)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (gedistribueerde pariteit)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (dubbel gedistribueerde pariteit)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID-chassis"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID-niveau"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr "RAID10 heeft een even aantal fysieke volumes nodig"
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Rackmontagechassis"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Willekeurig"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Reeks"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "Bereik moet strikt geordend zijn"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Rang"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Raw naar een apparaat"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Lezen"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Lees meer..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Alleen-lezen"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Lezen"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Lezend..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Klaar"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Klaar"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Echte hostnaam"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Echte hostnaam mag alleen kleine letters, cijfers, streepjes en punten "
+"bevatten (met bevolkte subdomeinen)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Echte hostnaam moet 64 tekens of minder bevatten"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Opnieuw toepassen en opnieuw opstarten"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Opnieuw opstarten"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Opnieuw opstarten na voltooiing"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Opnieuw opstarten aanbevolen"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Systeem opnieuw opstarten..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Ontvangen"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Recente hosts"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Aanbevolen, veilige instellingen voor huidige dreigingsmodellen."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Opnieuw aansluiten"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Herstellen"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "RAID-apparaat $target herstellen"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Pakketinformatie verversen"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Verbinding maken geweigerd. Host is onbekend"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Verbinding maken geweigerd. Hostsleutel komt niet overeen"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Verbinding maken geweigerd. Hostsleutel is onbekend"
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "Regenereer"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "initrd opnieuw genereren"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Registreren…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "Gerelateerde processen en services worden met kracht gestopt."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "Gerelateerde processen worden met kracht gestopt."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "Gerelateerde services worden met kracht gestopt."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Opnieuw laden"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Opnieuw laden gepropageerd van"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Opnieuw laden"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "De status van de resterende services opnieuw laden"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "Op afstand via CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "Op afstand via FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Op afstand via NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "Op afstand via NFS"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "Op afstand via SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Op afstand via SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "Op afstand via SSH"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Verwijderingen:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Verwijderen"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Verwijder $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Verwijder $0 service uit $1 zone"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "$0 verwijderen?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Tang-sleutelserver verwijderen?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Verwijder apparaat"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Verwijder host"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Verwijder item"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "Ontbrekende fysieke volumes verwijderen?"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "Wachtzin in sleutelslot $0 verwijderen?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "Wachtzin verwijderen?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Verwijder service $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Verwijder zone $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Verwijderen"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "$0 verwijderen"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Als je $0 verwijdert, wordt de verbinding met de server verbroken en wordt "
+"de beheerdersinterface onbereikbaar."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "$target verwijderen uit RAID-apparaat"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"Het verwijderen van een wachtzin zonder bevestiging van een andere wachtzin "
+"kan ontgrendeling of sleutelbeheer voorkomen als andere wachtzinnen worden "
+"vergeten of verloren gaan."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Fysiek volume verwijderen uit $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Als je de cockpit-service verwijdert, kan de webconsole onbereikbaar worden. "
+"Zorg ervoor dat deze zone niet van toepassing is op je huidige "
+"webconsoleverbinding."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "Als je de zone verwijdert, worden alle services erin verwijderd."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Hernoemen"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Naam van Stratis pool wijzigen"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Bestandssysteem hernoemen"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Hernoem groep"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "Hernoem groep $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Hernoemen logische volume"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Hernoem volumegroep"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Hernoem $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr ""
+"Het hernoemen van een groep kan van invloed zijn op sudo en vergelijkbare "
+"regels"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr "Herstellen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+msgid "Repair logical volume $0"
+msgstr "Herstel logische volume $0"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "$target repareren"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Herhaal"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Maandelijks herhalen"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Herhaal wachtzin"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Wekelijks herhalen"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Rapport"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Rapporteer label"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Rapporteer aan ABRT Analytics"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Gerapporteerd; geen links beschikbaar"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Rapportage mislukte"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Rapportage is geannuleerd"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Rapporten"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Rapporten:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Vereis wachtwoordwijziging elke $0 dagen"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Verwacht wachtwoordwijziging op $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Verwacht wachtwoordwijziging bij eerste login"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Vereist door"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Vereist door "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Vereisten"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Vereist beheerdersrechten om te bewerken"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Vereiste"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Vereiste van"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Gereserveerd geheugen"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Reset"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Wachtwoord herstellen"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Grootte van $target wijzigen"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Het wijzigen van de grootte van een versleuteld bestandssysteem vereist dat "
+"de schijf wordt ontgrendeld. Geef een huidige wachtzin voor de schijf op."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Opnieuw starten"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Start services opnieuw"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Services opnieuw opstarten..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Herstarten"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Verbinding herstellen"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"De resultaten van de crash zullen via $0 naar $1 worden gekopieerd als $2, "
+"als kdump correct is geconfigureerd."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+"De resultaten van de crash zullen in $0 worden opgeslagen als $1 als kdump "
+"correct is geconfigureerd."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Samenvatten"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Bestaande versleuteling hergebruiken"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Hergebruiken van bestaande versleuteling ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "Bekijk cryptografisch beleid"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Logboeken bekijken"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Round-robin"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Routes"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Uitvoeren op"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Vier nieuw rapport uit"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Uitvoeren op"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Voer rapport uit"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+"Voer deze opdracht uit via een vertrouwd netwerk of fysiek op de externe "
+"machine:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Uitvoerder"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Uitvoeren"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Runtime"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux toegangscontrolefouten"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux is uitgeschakeld op het systeem"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux is uitgeschakeld op het systeem."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux-beleid"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "SELinux systeemstatus is onbekend."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "SELinux problemen oplossen"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "SMART zelftest van $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"SOS-rapportage verzamelt systeeminformatie om te helpen bij het "
+"diagnosticeren van problemen."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH-sleutel"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "SSH-sleutel is geen pad"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH-sleutels"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "STP voorwaartse vertraging"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "STP hallo tijd"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP maximale berichtleeftijd"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP prioriteit"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Safari-gebruikers moeten het certificaat van de zelfondertekende CA "
+"importeren en vertrouwen:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "zaterdagen"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Opslaan"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Opslaan en opnieuw opstarten"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Sla veranderingen op"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Bespaar ruimte door afzonderlijke blokken te comprimeren met LZ4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Bespaar ruimte door identieke datablokken slechts één keer op te slaan"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Voor het opslaan van een nieuwe wachtzin moet de schijf worden ontgrendeld. "
+"Geef een huidige wachtzin voor de schijf op."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Geplande uitschakeling om $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Geplande herstart om $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "Gesloten PC"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Zoeken"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Zoek domein"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Zoek op naam of ID"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Zoek op naam, groep of ID"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "Seconde moet een getal tussen 0-59 zijn"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Seconden"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Veilige shell-sleutels"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "$target veilig wissen"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Security Enhanced Linux configuratie en probleemoplossing"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Beveiligingsupdates beschikbaar"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Alleen beveiligingsupdates"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Beveiligingsupdates worden $0 toegepast bij $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Selecteren"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Selecteer een ID"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Selecteer methode"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+"Selecteer de fysieke volumes die moeten worden gebruikt om het logische "
+"volume te repareren. Er zijn minimaal $0 nodig."
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Zenden"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Verzenden"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Serienummer"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Server"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Serveradres"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Serveradres mag niet leeg zijn."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Server mag niet leeg zijn."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Server heeft de verbinding verbroken."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Server software"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Service"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Service heeft een fout"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Servicelogboeken"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Services"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Services die gebruik maken van de locatie"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Sessie"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Sessie verloopt binnenkort"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Instellen"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Stel hostnaam in"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Partitie aanmaken op $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Wachtwoord instellen"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Stel tijd in"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Stel in op"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Opstellen"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Zwak wachtwoord instellen"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Instelling wijkt af van de geconfigureerde status en keert terug bij de "
+"volgende keer opstarten."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Opzetten"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Loop apparaat $target instellen"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Instellingen"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Strengheid"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Gedeeld"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Shell"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Shell-script"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "Toon alle threads"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Toon alle threads"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Show confirmation password"
+msgstr "Bevestigingswachtwoord tonen"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Toon vingerafdrukken"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Toon berichten die een gegeven string bevatten."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Toon berichten voor de gespecificeerde systemd unit."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Toon berichten van een specifieke boot."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Toon meer relaties"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "Wachtwoord tonen"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Toon relaties"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Krimpen"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Krimp logische volume"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "Verklein partitie"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Krimp volume"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Afsluiten"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Afsluiten"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Sinds"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Enkele rang"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Grootte"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "Grootte mag niet negatief zijn"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "Grootte mag niet nul zijn"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Grootte is te groot"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Grootte moet een getal zijn"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Grootte moet tenminste #0 zijn"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Sla de hoofdnavigatie over"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Doorgaan naar inhoud"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Slot"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Slot $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Snapshot"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Sockets"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Software updates"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Op software gebaseerde oplossingen helpen CPU-beveiligingsproblemen te "
+"voorkomen. Deze mitigaties hebben als neveneffect dat ze de prestaties "
+"verminderen. Wijzig deze instellingen op eigen risico."
+
+#: pkg/storaged/drive/drive.jsx:63
+#, fuzzy
+#| msgid "Solid-State Disk"
+msgid "Solid State Drive"
+msgstr "Solid-state schijf"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Oplossing succesvol toegepast"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Oplossing mislukt"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Oplossingen"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"Sommige blokapparaten van deze pool zijn groter geworden nadat de pool is "
+"gemaakt. De pool kan veilig worden vergroot om de nieuw beschikbare ruimte "
+"te gebruiken."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"Een ander programma gebruikt momenteel de pakketbeheerder, een ogenblik "
+"geduld..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Sommige software moet handmatig opnieuw worden opgestart"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Gesorteerd van minst tot meest vertrouwd"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Ruimtebesparende computer"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Spanning tree protocol"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Spanning tree protocol (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Reserve"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Specifieke tijd"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Snelheid"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Stabiel"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Start"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Start en schakel in"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Start multipath"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Start service"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr ""
+"Begin met het weergeven van ingangen op of nieuwer dan de opgegeven datum."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr ""
+"Begin met het weergeven van ingangen op of ouder dan de opgegeven datum."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Gestart"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "RAID-apparaat $target starten"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Swapspace $target starten"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Toestand"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Statisch"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Toestand"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Stick PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Kleverig"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Stop"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Stop en schakel uit"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Stop en verwijder"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Stoppen en ontkoppelen"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Stop apparaat"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Stop met het bewerken van hosts"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Stop rapport"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "RAID-apparaat $target stoppen"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Swapspace $target stoppen"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Opslag"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "Opslag kan op dit systeem niet beheerd worden."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Opslaglogboeken"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Sla wachtzin op"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Opgeslagen wachtzin"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "$0 blokapparaat"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "Add block devices"
+msgid "Stratis block devices"
+msgstr "Voeg blokapparaten toe"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Stratis blockdevs kunnen niet kleiner gemaakt worden"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystem"
+msgstr "Bestandssysteem aanmaken"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems"
+msgstr "Bestandssysteem aanmaken"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Bestandssysteem aanmaken"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Stratis-pool"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr "Gestreept (RAID 0)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr "Gestreept en gespiegeld (RAID 10)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr "Strepen"
+
+#: pkg/lib/cockpit-components-password.jsx:97
+msgid "Strong password"
+msgstr "Sterk wachtwoord"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Stuk"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Stijl"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Sub-chassis"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Sub-notebook"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "Abonneren op systemd-signalen is mislukt: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "Volume kan niet aangemaakt worden"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Succesvol naar het klembord gekopieerd"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Succesvol naar het klembord gekopieerd!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "zondagen"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Swap"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "De grootte van bestandssystemen kan hier niet gewijzigd worden."
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Uitwisselen"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Schakelaar van $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Schakel $0 uit"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Schakel $0 in"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Schakel om naar beheerderstoegang"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Schakel over naar beperkte toegang"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Als je $0 uitschakelt, wordt de verbinding met de server verbroken en is de "
+"beheerdersinterface niet beschikbaar."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Als je $0 inschakelt, wordt de verbinding met de server verbroken en is de "
+"beheerdersinterface niet beschikbaar."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Gesynchroniseerd"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Gesynchroniseerd met $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Synchroniseren"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "RAID-apparaat $target synchroniseren"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Systeem"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Systeemdiagnose"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Systeeminformatie"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Systeem is bijgewerkt"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Systeemaanpassingen"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Systeemtijd"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemd units"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tablet"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang sleutelserver"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Doel"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Doelen"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Team"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Teampoort"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Teampoortinstellingen"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminal"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Sessie beëindigen"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Test configuratie"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Test is alleen beschikbaar terwijl de kdump-service actief is."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Test kdump instellingen"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Test kdump-instellingen door de kernel te laten crashen. Dit kan enige tijd "
+"duren en het systeem wordt mogelijk niet automatisch opnieuw opgestart. Laat "
+"het systeem niet doelbewust crashen terwijl er een belangrijke taak wordt "
+"uitgevoerd."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Verbinding testen"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "Het $0 pakket is van geen enkele repository beschikbaar."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "Het $0 pakket moet worden geïnstalleerd om Stratis pools aan te maken."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "Het $0 pakket moet worden geïnstalleerd."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "Het $0 pakket zal worden geïnstalleerd om VDO-apparaten aan te maken."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "Het IP-adres of hostnaam mag geen spaties bevatten."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "De RAID-array bevindt zich in een slechte staat"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr ""
+"Het RAID apparaat moet actief zijn om een extra schijven te kunnen "
+"verwijderen."
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+"De SSH-sleutel $0 van $1 op $2 zal worden toegevoegd aan het $3 bestand van "
+"$4 op $5."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"De SSH-sleutel $0 zal beschikbaar worden gesteld voor de rest van de sessie "
+"en zal ook beschikbaar zijn om in te loggen bij andere hosts."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"De SSH-sleutel voor inloggen op $0 is beschermd met een wachtwoord en de "
+"host staat inloggen met een wachtwoord niet toe. Geef het wachtwoord van de "
+"sleutel op $1."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"De SSH-sleutel voor inloggen op $0 is beschermd. Je kunt inloggen met je "
+"inlogwachtwoord of door het wachtwoord van de sleutel op te geven op $1."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"Het account '$0' zal zijn wachtwoord moeten wijzigen bij de volgende inlog"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "De cockpit-service is automatisch inbegrepen"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"De geconfigureerde status is onbekend, deze kan bij de volgende keer "
+"opstarten veranderen."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"Het aanmaken van dit VDO-apparaat is niet voltooid en het apparaat kan niet "
+"gebruikt worden."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"De momenteel ingelogde gebruiker mag geen informatie over sleutels zien."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"De schijf moet worden ontgrendeld voor het formatteren. Geef een bestaande "
+"wachtzin op."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "De bestand $0 kon niet worden verwijderd."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "Het bestandssysteem heeft geen permanent koppelpunt."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "Het bestandssysteem heeft geen permanent koppelpunt."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Het bestandssysteem is geconfigureerd om automatisch te worden aangekoppeld "
+"bij het opstarten, maar de encryptiecontainer wordt op dat moment niet "
+"ontgrendeld."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Het bestandssysteem is momenteel aangekoppeld, maar wordt niet aangekoppeld "
+"na de volgende keer opstarten."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Het bestandssysteem is momenteel aangekoppeld op $0 maar zal worden "
+"aangekoppeld op $1 bij de volgende opstart."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Het bestandssysteem is momenteel aangekoppeld op $0 maar zal niet "
+"aangekoppeld worden na de volgende opstart."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Het bestandssysteem is momenteel niet aangekoppeld, maar wordt bij de "
+"volgende opstart aangekoppeld."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Het bestandssysteem is niet aangekoppeld."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"Het bestandssysteem wordt ontgrendeld en aangekoppeld bij de volgende keer "
+"opstarten. Hiervoor moet mogelijk een wachtzin worden ingevoerd."
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "De vingerafdruk moet overeenkomen met:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "De volgende service wordt opnieuw gestart:"
+msgstr[1] "De volgende services worden opnieuw gestart:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "De volledige naam mag geen dubbele punten bevatten."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "De groeps-ID moet een positief geheel getal zijn"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"De groepsnaam kan alleen bestaan uit letters van a-z, cijfers, punten, "
+"streepjes en onderstrepingstekens"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"De thuismap $0 bestaat al. Het eigendom wordt gewijzigd naar de nieuwe "
+"gebruiker."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "De initrd moet opnieuw worden gegenereerd."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Het sleutelwachtwoord mag niet leeg zijn"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "De sleutelwachtwoorden komen niet overeen"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "De sleutel die je hebt opgegeven, is niet geldig."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "De laatste sleutelslot kan niet verwijderd worden"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "De vermelde processen en services worden met kracht gestopt."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "De vermelde processen worden met kracht gestopt."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "De vermelde services worden met kracht gestopt."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "De ingelogde gebruiker mag geen systeemwijzigingen bekijken"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "De machine wordt opnieuw opgestart"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "Het aankoppelpunt $0 is in gebruik bij deze processen:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "Het aankoppelpunt $0 is in gebruik bij deze services:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Het nieuwe sleutelwachtwoord mag niet leeg zijn"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Het wachtwoord mag niet leeg zijn"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "De wachtwoorden komen niet overeen"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "De wachtwoorden komen niet overeen."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"De resulterende vingerafdruk is prima te delen via openbare methoden, "
+"inclusief e-mail."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"De resulterende vingerafdruk kan prima worden gedeeld via openbare methoden, "
+"waaronder e-mail. Als je iemand anders vraagt om de verificatie voor je uit "
+"te voeren, kan hij of zij de resultaten op elke gewenste manier verzenden."
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"De server weigerde '$0' te verifiëren met wachtwoordverificatie en er zijn "
+"geen andere ondersteunde verificatiemethoden beschikbaar."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr "De server weigerde te verifiëren met behulp van ondersteunde methoden."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"Het systeem ondersteunt momenteel niet het ontgrendelen van een "
+"bestandssysteem met een Tang-sleutelserver tijdens het opstarten."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"Het systeem biedt momenteel geen ondersteuning voor het ontgrendelen van het "
+"rootbestandssysteem met een Tang-sleutelserver."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+"Het is gebruiker $0 niet toegestaan cpu-beveiligingsmaatregelen te wijzigen"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "Het is gebruiker $0 niet toegestaan om crypto-beleid te wijzigen"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr ""
+"Het is gebruiker $0 niet toegestaan om de kernel als test te laten crashen"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"De gebruiker moet zich afmelden en weer aanmelden om de nieuwe configuratie "
+"van kracht te laten worden."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"De gebruikersnaam kan alleen bestaan uit letters van a-z, cijfers, punten, "
+"streepjes en onderstrepingstekens."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"De configuratie van de webbrowser voorkomt dat Cockpit wordt uitgevoerd "
+"(ontoegankelijk $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Er zijn momenteel geen actieve pagina's"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"Er zijn apparaten met meerdere paden op het systeem, maar de multipath-"
+"service is niet actief."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "Er zijn geen actieve services in deze zone"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Er zijn geen geautoriseerde openbare sleutels voor dit account."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+"Er is niet voldoende ruimte beschikbaar die kan worden gebruikt voor een "
+"reparatie. Er is minimaal $0 nodig voor fysieke volumes die nog niet voor "
+"dit logische volume worden gebruikt."
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"Er is niet genoeg ruimte in de pool om een momentopname van dit "
+"bestandssysteem te maken. Er is minimaal $0 vereist, maar er is slechts $1 "
+"beschikbaar."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr ""
+"Er is een onverwachte fout opgetreden tijdens het verbinden met de machine."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Deze extra stappen zijn nodig:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Deze veranderingen zullen worden gemaakt:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Dunne logische volume"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Pool voor dun ingerichte volumes"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+#, fuzzy
+#| msgid ""
+#| "This RAID array has no write-intent bitmap. Such a bitmap can reduce "
+#| "sychronization times significantly."
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"Deze RAID-array heeft geen bitmap met schrijfintentie. Een dergelijke bitmap "
+"kan de synchronisatietijden aanzienlijk verkorten."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"Deze NFS-mount is in gebruik en alleen de opties kunnen gewijzigd worden."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Dit VDO-apparaat gebruikt niet al zijn achtergrondapparatuur."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Dit apparaat kan hier niet beheerd worden."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Dit apparaat kan hier niet beheerd worden."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "Dit apparaat is momenteel in gebruik."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Dit veld mag niet leeg zijn"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Deze groep is de primaire groep voor de volgende gebruikers:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "Deze host zal opnieuw opstarten nadat updates zijn geïnstalleerd."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "Deze informatie wordt alleen op het systeem opgeslagen."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Deze keyserver is de enige manier om de pool te ontgrendelen en kan niet "
+"worden verwijderd."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+"Dit logische volume heeft een deel van zijn fysieke volumes verloren en kan "
+"niet langer worden gebruikt. Je moet het verwijderen en een nieuwe maken om "
+"zijn plaats in te nemen."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+"Dit logische volume heeft een deel van zijn fysieke volumes verloren, maar "
+"nog geen data. Je moet het repareren om de oorspronkelijke redundantie te "
+"herstellen."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+"Dit logische volume heeft een deel van zijn fysieke volumes verloren, maar "
+"mogelijk nog geen data. Misschien kun je het repareren."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "Dit logische volume wordt niet volledig gebruikt door de inhoud."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Deze machine is al toegevoegd."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Dit kan een tijdje duren"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "Deze partitie wordt niet volledig gebruikt door de inhoud ervan."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Deze wachtzin is de enige manier om de pool te ontgrendelen en kan niet "
+"worden verwijderd."
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr "Deze pool gebruikt niet alle ruimte op zijn blokapparaten."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "Deze pool bevindt zich in een slechte staat."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Dit systeem is niet geregistreerd"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Dit systeem gebruikt een aangepast profiel"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Dit systeem gebruikt het aanbevolen profiel"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"Dit gereedschap configureert het SELinux-beleid en kan helpen bij het "
+"begrijpen en oplossen van beleidsschendingen."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"Dit gereedschap configureert het systeem om kernelcrashdumps naar schijf te "
+"schrijven."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"Dit gereedschap genereert een archief met configuratie- en diagnostische "
+"informatie van het draaiende systeem. Het archief kan lokaal of centraal "
+"worden opgeslagen voor opname- of trackingsoeleinden of kan worden verzonden "
+"naar vertegenwoordigers van de technische ondersteuning, ontwikkelaars of "
+"systeembeheerders om te helpen bij het opsporen van technische fouten en het "
+"debuggen."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"Dit gereedschap beheert lokale opslag, zoals bestandssystemen, LVM2-"
+"volumegroepen en NFS-koppelingen."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"Dit gereedschap beheert netwerken zoals bindingen, bruggen, teams, VLAN's en "
+"firewalls met behulp van NetworkManager en Firewalld. NetworkManager is niet "
+"compatibel met Ubuntu's standaard systemd-networkd en Debian's ifupdown-"
+"scripts."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Dit apparaat is niet ontworpen om expliciet te worden ingeschakeld."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Deze gebruikersnaam bestaat al"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr "Bij deze volumegroep ontbreken enkele fysieke volumes."
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Deze webbrowser is te oud om de Webconsole uit te voeren ($0 ontbreekt)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Dit voegt een overeenkomst toe voor '_BOOT_ID='. Als dit niet is opgegeven, "
+"worden de logboeken voor de huidige boot weergegeven. Als de boot-ID wordt "
+"weggelaten, zoekt een positieve offset de boots op vanaf het begin van het "
+"journaal, en een offset die gelijk is aan of kleiner is dan nul, zoekt de "
+"boots op vanaf het einde van het journaal. Dus 1 betekent de eerste boot die "
+"in chronologische volgorde in het journaal wordt gevonden, 2 de tweede "
+"enzovoort; terwijl -0 de laatste keer opstarten is, -1 de opstart voor het "
+"laatst, enzovoort."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Dit voegt een overeenkomst toe voor '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' en "
+"'UNIT=' om alle mogelijke berichten voor de gegeven eenheid te vinden. Kan "
+"meer eenheden bevatten, gescheiden door komma's. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Hiermee kun je in de toekomst zonder wachtwoord inloggen."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Deze zone bevat de cockpitservice. Zorg ervoor dat deze zone niet van "
+"toepassing is op je huidige webconsoleverbinding."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "donderdagen"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Niveau"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Tijd"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Tijdzone"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Het aanmaken van de timer is mislukt"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "Het verwijderen de timer is mislukt"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Timers"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Tip: zorg ervoor dat je sleutelwachtwoord overeenkomt met je inlogwachtwoord "
+"om automatisch te verifiëren bij andere systemen."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Controleer de vingerafdruk van de hostsleutel om ervoor te zorgen dat je "
+"verbinding niet wordt onderschept door een kwaadwillende derde partij:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Om software-updates te krijgen, moet dit systeem worden geregistreerd bij "
+"Red Hat, hetzij met behulp van de Red Hat Customer Portal of een lokale "
+"abonnementsserver."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Om een vingerafdruk te verifiëren, voer je het volgende uit op $0 terwijl je "
+"fysiek achter de machine zit of via een vertrouwd netwerk:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Vandaag"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Wissel"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Datumkiezer omschakelen"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Schakel filters om"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Teveel data"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Gereedschappen"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Top 5 CPU-services"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "Top 5 schijfgebruik-services"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "Top 5 geheugenservices"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Totale grootte: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Toren"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Kortstondig"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Zenden"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Trigger"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Veroorzaakt door"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Triggers"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Problemen"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Problemen oplossen…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "Vertrouw en voeg host toe"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Vertrouwenssleutel"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Vertrouwensniveau"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Probeer opnieuw"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Bezig met synchroniseren met $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "dinsdagen"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned kan niet gestart worden"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned is een service die je systeem bewaakt en de prestaties onder bepaalde "
+"werkbelastingen optimaliseert. De kern van Tuned zijn profielen, die je "
+"systeem afstemmen op verschillende gebruikssituaties."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned is niet beschikbaar"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned is niet actief"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned is uitgeschakeld"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Schakel beheerderstoegang in"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Type"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Naam mag het letterteken '$0' niet bevatten."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Typ om te filteren"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Kan deze oplossing niet automatisch toepassen"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Kan niet verbinden met dat adres"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "Niet mogelijk $0 te contacteren."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Kan geen contact opnemen met opgegeven host $0. Zorg ervoor dat ssh wordt "
+"uitgevoerd op poort $1 of specificeer een andere poort in het adres."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Kan geen alarmdetails krijgen."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Kan niet inloggen op $0 met SSH-sleutelverificatie. Geef het wachtwoord op. "
+"Mogelijk wil je jouw SSH-sleutels instellen voor automatisch inloggen."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Kan niet inloggen op $0. De host accepteert geen aanmelden met wachtwoord of "
+"een van je SSH-sleutels."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Kan server niet bereiken"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Kan aankoppeling niet verwijderen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+msgid "Unable to repair logical volume $0"
+msgstr "Kan logische volume $0 niet repareren"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "Kan reparatie niet uitvoeren: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "Kan instellingen niet opslaan"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "Kan instellingen niet opslaan: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Kan setroubleshootd niet starten"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Kan bestandssysteem niet afkoppelen"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Niet beschikbaar"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Niet beschikbare pakketten"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Ongedaan maken"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "Onverwachte PackageKit-fout tijdens installatie van $0: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Onverwachte fout"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Onbekende data"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Unit"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Onbekend"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Onbekend \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Onbekend ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Onbekende toepassing"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Onbekende configuratie"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Onbekende hostnaam"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Onbekende servicenaam"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Onbekend type"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Ontgrendelen"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Automatisch ontgrendelen bij opstarten"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Ontgrendel versleutelde Stratis pool"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "Ontgrendel sleutel $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Ontgrendelen $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Schijf ontgrendelen"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Onbeheerde interfaces"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Afkoppelen"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "Bestandssysteem $0 afkoppelen"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Nu afkoppelen"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "$target afkoppelen"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Naamloos"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "Eenheid vrijgeven"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Onbekende data"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "Unrecognized data can not be made smaller here."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Onbekende data kan hier niet kleiner gemaakt worden."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Onbekende data kan hier niet kleiner gemaakt worden."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Niet-ondersteunde volume"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "Tot"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Niet vertrouwde host"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Bijwerken"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Update succestabel"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Update geschiedenis"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Vernieuw pakketinformatie"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "Update is gelukt"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Bijgewerkt"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr ""
+"Bijgewerkte pakketten moeten mogelijk opnieuw worden opgestart om van kracht "
+"te worden."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Updates beschikbaar"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Updategeschiedenis"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Updates worden $0 toegepast bij $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Bijwerken"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Status bijwerken..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Uptime"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Gebruik"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "Gebruik van $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Gebruik"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Gebruik compressie"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Gebruik deduplicatie"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Gebruik sleutel"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Gebruik wachtwoord"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Gebruik de volgende sleutels om te verifiëren bij andere systemen"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Uitgebreide logboekregistratie gebruiken"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Gebruikt"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"Handig voor aankoppelingen die optioneel zijn of interactie nodig hebben "
+"(zoals wachtwoordzinnen)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Gebruiker"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "Gebruikers-ID"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "Gebruikers-ID wordt al gebruikt door een andere gebruiker"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "Gebruikers-ID moet een positief geheel getal zijn"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "Gebruikers-ID mag niet hoger zijn dan $0"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "Gebruikers-ID mag niet langer zijn dan $0"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Gebruikersnaam"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Gebruikersnaam mag niet leeg zijn"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Gebruikersnaam"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "Gebruikt LUKS-versleuteling"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Gebruikt Tang-server"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "VDO-ondersteuningsapparaten kunnen niet kleiner gemaakt worden"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO-apparaat $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "VDO-bestandssysteemvolume (compressie/deduplicatie)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN-ID"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Adres valideren"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Verificatietoken valideren"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Leverancier"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Geverifieerd"
+
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "Vingerafdruk verifiëren"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Verifieer sleutel"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Controleren"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Versie"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Zeer veilig wissen van $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Bekijk alle CPU's"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Bekijk alle schijven"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Bekijk alle logboeken"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Bekijk alle services"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Bekijk automatiseringsscript"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Bekijk gedetailleerde logboeken"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Bekijk hardwaredetails"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Bekijk inloggeschiedenis"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "Bekijk logboeken"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Bekijk statistieken en geschiedenis"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Bekijk doorvoer per schijf"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Bekijk projectwebsite"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Bekijk rapport"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Updatelogboek bekijken"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "Voor het bekijken van geheugeninformatie is beheerderstoegang vereist."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Bezoek firewall"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Volumegroep"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "This volume group is missing some physical volumes."
+msgid "Volume group is missing physical volumes"
+msgstr "Bij deze volumegroep ontbreken enkele fysieke volumes."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Volumegrootte is $0. Inhoudsgrootte is $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Aan het wachten"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Wachten op details..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Wachten op input…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"Wachten op andere programma's om het gebruik van de pakketbeheerder te "
+"beëindigen..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Wachten tot andere softwarebeheerhandelingen voltooid zijn"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Wachten om te beginnen…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Gezocht door"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Behoeften"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Waarschuwing en hoger"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+msgid "Weak password"
+msgstr "Zwak wachtwoord"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Webconsole"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Webconsole voor Linux-servers"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Webconsole zal opnieuw opstarten"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "De webconsole wordt uitgevoerd in de beperkte toegang modus."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Logo van webconsole"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "woensdagen"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Wekelijks"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Weken"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Wanneer"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "Maak verbinding met de huidige gebruiker als deze leeg is"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Wanneer de webconsole opnieuw wordt opgestart, zie je geen "
+"voortgangsinformatie meer. Het updateproces wordt echter op de achtergrond "
+"voortgezet. Maak opnieuw verbinding om het updateproces te blijven volgen."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"Als deze optie is aangevinkt, staat de nieuwe pool geen overprovisioning "
+"toe. Je moet een maximale grootte opgeven voor elk bestandssysteem dat in de "
+"pool wordt gemaakt. Bestandssystemen kunnen na creatie niet groter worden "
+"gemaakt. Snapshots worden bij het maken volledig toegewezen. De som van alle "
+"maximale afmetingen mag de grootte van de pool niet overschrijden. Het "
+"voordeel hiervan is dat bestandssystemen in deze pool op verrassende wijze "
+"niet zonder ruimte kunnen komen te zitten. Het nadeel is dat je van tevoren "
+"de maximale grootte voor elk bestandssysteem moet weten en dat het maken van "
+"snapshots beperkt is."
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Wit"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "Wordt ingesteld op \"Automatisch\""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "World wide name"
+msgid "World wide name"
+msgstr "Wereldwijde naam"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Schrijven"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Meestal-schrijven"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Schrijven"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Verkeerde gebruikersnaam of wachtwoord"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Jaarlijks"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Ja"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Je maakt voor de eerste keer verbinding met $0."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Jr bent niet bevoegd om de firewall te wijzigen."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Je hebt geen toestemming om de geautoriseerde openbare sleutels voor dit "
+"account te bekijken."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Je bent uitgelogd vanwege inactiviteit."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Je kunt proberen oudere items te laden."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+"Mogelijk wil je het wachtwoord van de sleutel wijzigen om automatisch in te "
+"loggen."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Je moet langer wachten om je wachtwoord te wijzigen"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr "Je moet opnieuw inloggen om statistieken te kunnen zien"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Je hebt nu administratieve toegang."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Je wordt over $0 seconden uitgelogd."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Jouw account"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"Je browser staat plakken vanuit het contextmenu niet toe. Je kunt "
+"Shift+Insert gebruiken."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "Je browser onthoudt je toegangsniveau tijdens sessies."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Je server verbreekt de verbinding binnenkort. Je kunt opnieuw verbinding "
+"maken nadat deze opnieuw is opgestart."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Je sessie is beëindigd."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Je sessie is verlopen. Log nogmaals in."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Zone"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[binaire data]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[geen data]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "toegang"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "actief"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "toevoeging"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "toevoeging"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "na netwerk"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "apps"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "asset-tag"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "op"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "gemiddelde: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "steunapparaat voor VDO-apparaat"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "binding"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "opstart"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "brug"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "Andere apparaten"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "Andere apparaten"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs subvolume"
+msgstr "Opslagvolume"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Opslagvolumes"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "Opslagvolume"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "bugfix"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "bytes"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "Cache"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "verandering"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman is niet geïnstalleerd"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "commando"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "console"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "core dump"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "cpu"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "crash"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "$0 data"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "datum"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Deactiveren"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "debug"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "verwijderen"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dim"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "uitschakelen"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "schijf"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "schijven"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "domein"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "station"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "bewerken"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "inschakelen"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted"
+msgid "encrypted"
+msgstr "Versleuteld"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "versleuteling"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "verbetering"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "fout"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "elke vrijdag"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "elke maandag"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "elke zaterdag"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "elke zondag"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "elke donderdag"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "elke dinsdag"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "elke woensdag"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "elke dag"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "extensie"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "kan ssh-host-sleutels niet vermelden: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "bestandssysteem"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "firewall"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "voor huidige en toekomstige kernels"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "alleen voor huidige kernel"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "formatteren"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "van $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "van $0 op $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "grafieken"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "toenemen"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "hardware"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "geschiedenis"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "host"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "iSCSI-doel"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "iSCSI-doelen"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "iSCSI-portaal toevoegen"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "negeer mislukken"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "in de meeste browsers"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "inconsequent"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "initialiseren"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "installeren"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "interface"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "ongeldig: meerdere doelen gedefinieerd"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "logboek"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "journalctl manualpagina"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "kdump status"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "sleutels"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "inloggen"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "machine"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "masker"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "max: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "member of RAID device"
+msgid "member of MDRAID device"
+msgstr "onderdeel van RAID-apparaat"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "onderdeel van Stratis-pool"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "geheugen"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "metriek"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "matiging"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "meer details"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "aankoppelen"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "netwerk"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "nooit aankoppelen tijdens opstarten"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "nfs-server is leeg"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "NFS-server is leeg"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "NFS-server is geen geldige IPv6"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "nice"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "geen"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "van $0 CPU"
+msgstr[1] "van $0 CPU's"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "op $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "besturingssysteem"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "os"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "pakket"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "partitie"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "wachtwoord"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "wachtwoordkwaliteit"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "patches"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "pad"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "performance"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "fysieke volume van LVM2-volumegroep"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "plug-in"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service is mislukt"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service kan geen data verzamelen"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service is niet actief"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "pod"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "poort"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "vermogen"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "alleen-lezen"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "aanbevolen"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "verwijderen uit LVM2"
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "remove from RAID"
+msgid "remove from MDRAID"
+msgstr "verwijderen uit RAID"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "verwijderen uit LVM2"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "herstarten"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "rollen"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "$0 wordt uitgevoerd"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "veiligheid"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "serieel"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "service"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "shell"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "toon minder"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "toon meer"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "krimpen"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "afsluiten"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "socket"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "oplossing"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "Oplossing details"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "sos rapport mislukte"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ssh-sleutel is geen pad"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "ssh-server is leeg"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "stop"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "stop opstarten bij mislukken"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+#, fuzzy
+#| msgid "Stopped"
+msgid "stopped"
+msgstr "Gestopt"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "doel"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "team"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "tijd"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "timer"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "unit"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "onbekend"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "onbekend doel"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "ontmaskeren"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "afkoppelen"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "niet-gepartitioneerde ruimte op $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "gebruiker"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "gebruikersnaam"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "gebruik sleutelbeschrijving $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "versie"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "volume"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "waarschuwing"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "Pakket wireguard-tools is niet geïnstalleerd"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "ja"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "zone"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Kdump-service niet geïnstalleerd. Zorg ervoor dat kexec-tools is "
+#~ "geïnstalleerd."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Geen geheugen gereserveerd. Voeg een crashkernel-optie toe aan de kernel-"
+#~ "commandoregel (bijvoorbeeld in /etc/default/grub) om tijdens het "
+#~ "opstarten geheugen te reserveren. Voorbeeld: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Service is actief"
+
+#~ msgid "Service is starting"
+#~ msgstr "Service begint"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Service is gestopt"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Service stopt"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Dit test de kdump-configuratie door de kernel te laten crashen."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "crashkernel is niet geconfigureerd in de kernelcommandoregel"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (versleuteld)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 Stratis-pool"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 cache"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 van onbekend niveau"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 vrij"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr ""
+#~ "Een extra schijf dient toegevoegd te worden voordat deze schijf kan "
+#~ "worden verwijderd."
+
+#~ msgid "Backing device"
+#~ msgstr "Ondersteunend apparaat"
+
+#~ msgid "Block"
+#~ msgstr "Blok"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Capaciteit"
+
+#~ msgid "Content"
+#~ msgstr "Inhoud"
+
+#~ msgid "Create devices"
+#~ msgstr "Maak apparaten aan"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Apparaat"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Apparaatbestand"
+
+#~ msgid "Devices"
+#~ msgstr "Apparaten"
+
+#~ msgid "Drives"
+#~ msgstr "Stations"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Versleutelde volumes moeten worden ontgrendeld voordat ze in grootte "
+#~ "kunnen worden aangepast."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Bestandssysteem (versleuteld)"
+
+#~ msgid "Filesystems"
+#~ msgstr "Bestandssystemen"
+
+#~ msgid "Free"
+#~ msgstr "Vrij"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Maak ruimte vrij in deze groep: verklein of verwijder andere logische "
+#~ "volumes of voeg een ander fysiek volume toe."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Inactieve volume"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Sleutelserver"
+
+#~ msgid "LVM2 member"
+#~ msgstr "LVM2 lid"
+
+#~ msgid "Locked devices"
+#~ msgstr "Vergrendelde apparaten"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Vergrendelde versleutelde data"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Model"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS-aankoppelingen"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "NFS-ondersteuning niet geïnstalleerd"
+
+#~ msgid ""
+#~ "New logical volumes can not be created while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "Er kunnen geen nieuwe logische volumes worden gemaakt als er in een "
+#~ "volumegroep fysieke volumes ontbreken."
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Geen NFS-aankoppelingen ingesteld"
+
+#~ msgid "No devices"
+#~ msgstr "Geen apparaten"
+
+#~ msgid "No drives attached"
+#~ msgstr "Geen schijven aangesloten"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Geen iSCSI-doelen ingesteld"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Niet genoeg ruimte voor nieuwe bestandsystemen"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Andere data"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Gepartitioneerd blokapparaat"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Wachtzin"
+
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "Fysieke volumes kunnen niet worden verwijderd als er in een volumegroep "
+#~ "fysieke volumes ontbreken."
+
+#~ msgid "Pool"
+#~ msgstr "Pool"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Pool voor dunne volumes"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID-niveau"
+
+#~ msgid "RAID member"
+#~ msgstr "RAID-lid"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Verwijderbaar station"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Toon $0 apparaat"
+#~ msgstr[1] "Toon alle $0 apparaten"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Toon $0 station"
+#~ msgstr[1] "Toon alle $0 stations"
+
+#~ msgid "Show all"
+#~ msgstr "Toon alles"
+
+#~ msgid "Source"
+#~ msgstr "Bron"
+
+#~ msgid "Start pool"
+#~ msgstr "Start pool"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Start pool om bestandssystemen te zien."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Toestand"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Stratis-pool gestopt"
+
+#~ msgid "Stratis member"
+#~ msgstr "Stratis lid"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Stratis-pool $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "Ondersteuning is geïnstalleerd."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "Het RAID-apparaat moet actief zijn om een extra schijven toe te kunnen "
+#~ "voegen."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "De laatste schijf van een RAID-apparaat kan niet worden verwijderd."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr ""
+#~ "Het laatste fysieke volume van een volumegroep kan niet verwijderd worden."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Er is elders onvoldoende vrije ruimte om dit fysieke volume te "
+#~ "verwijderen. Er is minimaal $0 meer vrije ruimte nodig."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr ""
+#~ "Deze schijf kan niet worden verwijderd gedurende de herstel periode."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr ""
+#~ "Dit volume moet worden geactiveerd voordat het in grootte kan worden "
+#~ "aangepast."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Onbekende data"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Gebruik"
+
+#~ msgid "Used for"
+#~ msgstr "Gebruikt voor"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "VDO-ondersteuning"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO-apparaat"
+
+#~ msgid "Volume"
+#~ msgstr "Volume"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automatisch (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Accepteer sleutel en maak verbinding"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Versleutelde volumes kunnen hier niet in grootte worden aangepast."
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Gebruik een Tang sleutelserver"
+
+#~ msgid "Use a passphrase"
+#~ msgstr "Gebruik een wachtzin"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitmap"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Deze pool kan hier niet worden ontgrendeld omdat de sleutelbeschrijving "
+#~ "niet in het verwachte formaat is."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Wissel bitmap"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Zorg ervoor dat de sleutelhash van de Tang-server overeenkomt met een van "
+#~ "de volgende:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Handmatig controleren met SSH: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Poortnummer en type komen niet overeen"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Vergrendelde versleutelde Stratis pool"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Fout tijdens het verwijderen van een waarschuwing: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Kan geen melding krijgen: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 bytes binaire data]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Schijf I/O piek"
+
+#~ msgid "Event"
+#~ msgstr "Gebeurtenis"
+
+#~ msgid "Event logs"
+#~ msgstr "Gebeurtenislogboeken"
+
+#~ msgid "Load spike"
+#~ msgstr "Laad piek"
+
+#~ msgid "Memory spike"
+#~ msgstr "Geheugen piek"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Netwerk I/O piek"
+
+#~ msgid "ssh key"
+#~ msgstr "ssh-sleutel"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Als deze optie is aangevinkt, zal het bestandssysteem niet worden "
+#~ "aangekoppeld tijdens de volgende keer opstarten, zelfs als het eerder was "
+#~ "aangekoppeld. Dit is handig als aankoppelen tijdens het opstarten niet "
+#~ "mogelijk is, zoals wanneer een wachtzin vereist is om het bestandssysteem "
+#~ "te ontgrendelen, maar het opstarten zonder toezicht is."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Nooit aankoppelen tijdens het opstarten"
+
+#~ msgid "Listing unit files"
+#~ msgstr "Eenheidsbestanden weergeven"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "Het weergeven van eenheidsbestanden is mislukt: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "Unit niet gevonden"
+
+#~ msgid "Validating key"
+#~ msgstr "Sleutel valideren"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "Verwijder $0 groep"
+
+#~ msgid "Container administrator"
+#~ msgstr "Containerbeheerder"
+
+#~ msgid "Image builder"
+#~ msgstr "Image bouwer"
+
+#~ msgid "Roles"
+#~ msgstr "Rollen"
+
+#~ msgid "Server administrator"
+#~ msgstr "Serverbeheerder"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Unix groep: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Succesvol gekopieerd naar toetsenbord"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Diagnostische rapporten"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Kerneldump"
+
+#~ msgid "Software Updates"
+#~ msgstr "Software updates"
+
+#~ msgid "Update log"
+#~ msgstr "Update logboek"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "nfs dump target is niet geformatteerd als server:pad"
+
+#~ msgid "$0 Zone"
+#~ msgstr "$0 Zone"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Maak diagnostisch rapport aan"
+
+#~ msgid "Done!"
+#~ msgstr "Klaar!"
+
+#~ msgid "Download report"
+#~ msgstr "Download rapport"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Er is geen archief aangemaakt."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Bevestig verwijdering van $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "Het gegenereerde archief bevat data die als gevoelig wordt beschouwd en "
+#~ "de inhoud ervan moet door de oorspronkelijke organisatie worden "
+#~ "beoordeeld voordat deze aan derden wordt doorgegeven."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Deze tool verzamelt systeemconfiguratie en diagnostische informatie van "
+#~ "dit systeem voor gebruik bij het diagnosticeren van problemen met het "
+#~ "systeem."
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "Server mist het cockpit systeempakket"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Dit pakket is niet compatibel met deze versie van Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Dit pakket vereist Cockpit-versie %s of hoger"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Prestatiestatistieken"
+
+#~ msgid "Reboot to apply new crypto policy"
+#~ msgstr "Herstart om nieuw crypto-beleid toe te passen"
+
+#~ msgid "Save only"
+#~ msgstr "Alleen opslaan"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB totaal"
+
+#~ msgid "Apply"
+#~ msgstr "Toepassen"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Niet gemachtigd om zone $0 te verwijderen"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Klik nogmaals op $0 om het wachtwoord toch te gebruiken."
+
+#~ msgid "Access"
+#~ msgstr "Toegang"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "SCHIJF SCHIET TE KORT"
+
+#~ msgid "Logical Size"
+#~ msgstr "Logische grootte"
+
+#~ msgid "Security updates "
+#~ msgstr "Beveiligingsupdates "
+
+#~ msgid "Updates "
+#~ msgstr "Updates "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Optioneel)"
+
+#~ msgid "Active since"
+#~ msgstr "Actief sinds"
+
+#~ msgid "Affected locations"
+#~ msgstr "Betrokken locaties"
+
+#~ msgid "Process"
+#~ msgstr "Proces"
+
+#~ msgid "Remove and delete"
+#~ msgstr "Verwijder en maak leeg"
+
+#~ msgid "Remove and format"
+#~ msgstr "Verwijderen en formatteren"
+
+#~ msgid "Remove and grow"
+#~ msgstr "Verwijderen en groeien"
+
+#~ msgid "Remove and initialize"
+#~ msgstr "Verwijderen en initialiseren"
+
+#~ msgid "Remove and shrink"
+#~ msgstr "Verwijderen en verkleinen"
+
+#~ msgid "Remove and stop device"
+#~ msgstr "Verwijder en stop apparaat"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Het bestandssysteem wordt gebruikt door inlogsessies en systeemservices. "
+#~ "Als je doorgaat, worden deze gestopt."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Het bestandssysteem wordt gebruikt door inlogsessies. Als je doorgaat, "
+#~ "worden deze gestopt."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Het bestandssysteem wordt gebruikt door systeemservices. Als je doorgaat, "
+#~ "worden deze gestopt."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Dit apparaat heeft bestandssystemen die momenteel in gebruik zijn. Als je "
+#~ "doorgaat, worden alle bestandssystemen ontkoppeld."
+
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Dit apparaat wordt momenteel gebruikt voor LVM2-volumegroepen."
+
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Dit apparaat wordt momenteel gebruikt voor LVM2-volumegroepen. Als je "
+#~ "doorgaat, wordt het uit de volumegroepen verwijderd."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Dit apparaat wordt momenteel gebruikt voor RAID-apparaten."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Dit apparaat wordt momenteel gebruikt voor RAID-apparaten. Als je "
+#~ "doorgaat, wordt het van zijn RAID-apparaten verwijderd."
+
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Dit apparaat wordt momenteel gebruikt voor Stratis pools."
+
+#~ msgid "Unmount and delete"
+#~ msgstr "Koppel af en verwijder"
+
+#~ msgid "Unmount and format"
+#~ msgstr "Afkoppelen en formatteren"
+
+#~ msgid "Unmount and grow"
+#~ msgstr "Afkoppelen en groeien"
+
+#~ msgid "Unmount and initialize"
+#~ msgstr "Afkoppelen en initialiseren"
+
+#~ msgid "Unmount and shrink"
+#~ msgstr "Afkoppelen en verkleinen"
+
+#~ msgid "Unmount and stop device"
+#~ msgstr "Afkoppelen en apparaat stoppen"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 van $1"
+
+#~ msgid "Blockdev"
+#~ msgstr "Blokapparaat"
+
+#~ msgid "Blockdev of Stratis pool $0"
+#~ msgstr "Blokapparaat van Stratis pool $0"
+
+#~ msgid "Blockdev of locked Stratis pool $0"
+#~ msgstr "Blokapparaat van vergrendelde Stratis pool $0"
+
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "LVM2 fysieke volume van $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Onderdeel van RAID-apparaat $0"
+
+#~ msgid "Menu"
+#~ msgstr "Menu"
+
+#~ msgid "VDO backing"
+#~ msgstr "VDO-ondersteuning"
+
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Maak Stratis pool aan"
+
+#~ msgid "Mount Options"
+#~ msgstr "Aankoppelopties"
+
+#~ msgid "Stratis Pool"
+#~ msgstr "Stratis pool"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Er is een schijf nodig."
+
+#~ msgid "Disk"
+#~ msgstr "Schijf"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Alleen voor oudere toepassingen. Vermindert prestaties."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Installeer VDO-ondersteuning"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Gebruik 512 byte emulatie"
+
+#~ msgid "System services"
+#~ msgstr "Systeemservices"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Maak diagnostisch rapport aan "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr ""
+#~ "Het gelijktijdig verbinden met meer dan {{ limiet }} machines wordt niet "
+#~ "ondersteund."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "Op Kerberos gebaseerde SSO"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Log in op {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "De nieuwe sleutelwachtwoorden komen niet overeen"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "Om een vingerafdruk te verifiëren, voer je het volgende uit op {{host}} "
+#~ "terwijl je fysiek achter de machine zit of via een vertrouwd netwerk:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Kan geen contact krijgen met {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Kan niet inloggen op {{#strong}}{{host}}{{/ strong}}. Upgrade cockpit-ws "
+#~ "naar een nieuwere versie voor meer authenticatie-opties en ondersteuning "
+#~ "voor probleemoplossing."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Kan niet inloggen op {{#strong}}{{host}}{{/strong}}. Om met deze host te "
+#~ "verbinden zul je een van de volgende authenticatie methoden moeten "
+#~ "aanzetten in de sshd-configuratie op{{#strong}}{{host}}{{/strong}}:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "Je maakt voor de eerste keer verbinding met {{host}}."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "{{host}} sleutel gewijzigd"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Wis aankoppelpuntconfiguratie"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Het aanmaken van deze binding zal de verbinding met de server verbreken "
+#~ "waardoor deze beheerdersinterface niet meer beschikbaar zal zijn."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Het aanmaken van deze brug zal de verbinding met de server verbreken "
+#~ "waardoor deze beheerdersinterface niet meer beschikbaar zal zijn."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Het aanmaken van dit team zal de verbinding met de server verbreken "
+#~ "waardoor deze beheerdersinterface niet meer beschikbaar zal zijn."
+
+#~ msgid "IP settings"
+#~ msgstr "IP-instellingen"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Als je <b>$0</b> uitschakelt, wordt de verbinding met de server verbroken "
+#~ "en wordt de beheerdersinterface onbereikbaar."
+
+#~ msgid "Administrator password"
+#~ msgstr "Beheerderswachtwoord"
+
+#~ msgid "Computer OU"
+#~ msgstr "Computer OU"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "Verwijderen van een RAID-apparaat zal alle data erop wissen."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "Verwijderen van een volumegroep zal alle data erop wissen."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Overschrijf geen bestaande data"
+
+#~ msgid "Erase"
+#~ msgstr "Wissen"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Schijf $0 formatteren"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr "Het formatteren van een opslagapparaat zal alle data verwijderen."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Hostnaam mag niet worden gewijzigd in een domein"
+
+#~ msgid "More"
+#~ msgstr "Meer"
+
+#~ msgid "Not joined"
+#~ msgstr "Niet aangesloten"
+
+#~ msgid "One time password"
+#~ msgstr "Eenmalig wachtwoord"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 uit $1 op $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Uren moet een getal zijn tussen 0 en 23"
+
+#~ msgid "Last login:"
+#~ msgstr "Laatste aanmelding:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Minuten moet een getal zijn tussen 0 en 59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "Er was $0 mislukte aanmeldingspoging sinds de laatste succesvolle "
+#~ "aanmelding."
+#~ msgstr[1] ""
+#~ "Er waren $0 mislukte aanmeldingspogingen sinds de laatste succesvolle "
+#~ "aanmelding."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "bijv. \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 actieve zones"
+
+#~ msgid "Dismiss $0 alerts"
+#~ msgstr "$0 meldingen afwijzen"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 voorval"
+#~ msgstr[1] "$0 voorvallen"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Licentie onder:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Ontgrendel bij opstarten"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Alleen-lezen ontgrendelen"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Zoek in de logboeken met een combinatie van termen:"
+
+#~ msgid "Show filters"
+#~ msgstr "Toon filters"
+
+#~ msgid "Text"
+#~ msgstr "Tekst"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "elke vrije tekenreeks als reguliere expressie"
+
+#~ msgid "e.g."
+#~ msgstr "b.v."
+
+#~ msgid "log fields"
+#~ msgstr "logboekvelden"
+
+#~ msgid "qualifiers"
+#~ msgstr "kwalificaties"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "Wissel sessie-instellingen"
+
+#~ msgid "(none)"
+#~ msgstr "(geen)"
+
+#~ msgid "Create timers"
+#~ msgstr "Maak timers aan"
+
+#~ msgid "Recommended default"
+#~ msgstr "Aanbevolen standaardwaarde"
+
+#~ msgid "Run"
+#~ msgstr "Uitvoeren"
+
+#~ msgid "Select unit state"
+#~ msgstr "Selecteer unit status"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Schakel opgeslagen metriek in"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Schakel opgeslagen metriek in"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Forceer verwijderen van wachtzin in $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Als tang-show-keys niet beschikbaar is, voer je het volgende uit:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Wat als tang-show-keys niet beschikbaar is?"
+
+#~ msgid "key slot $0"
+#~ msgstr "sleutel slot $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Er is iets fout gegaan"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "Dit werkte niet, probeer het opnieuw"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "Je kunt geen administratieve toegang krijgen."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Automatische vernieuwingen zijn niet ingesteld"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "$0 CPU-configuratie"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 Netwerk"
+#~ msgstr[1] "$0 Netwerken"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 is beschikbaar voor de meeste besturingssystemen. Om het te "
+#~ "installeren zoek je het op in GNOME Software of je voert het volgende uit:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 geheugenaanpassing"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 netwerk"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPU"
+#~ msgstr[1] "$0 vCPU's"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 vCPU-details"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 virtueel netwerkinterface instellingen"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "Activeer de opslagpool om volumes te beheren"
+
+#~ msgid "Add network interface"
+#~ msgstr "Netwerkinterface toevoegen"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Virtueel netwerkinterface toevoegen"
+
+#~ msgid "Additional"
+#~ msgstr "Extra"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Adres bevindt zich niet in subnet"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr ""
+#~ "Na het verwijderen van de snapshot gaat alle vastgelegde inhoud verloren."
+
+#~ msgid "Always attach"
+#~ msgstr "Altijd hechten"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Door deze aan te sluiten, wordt deze schijf deelbaar voor elke VM die "
+#~ "deze gebruikt."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Start libvirt automatisch tijdens het opstarten"
+
+#~ msgid "Autostart"
+#~ msgstr "Automatische start"
+
+#~ msgid "Boot order"
+#~ msgstr "Opstartvolgorde"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Opstartvolgorde instellingen konden niet opgeslagen worden"
+
+#~ msgid "Bus"
+#~ msgstr "Bus"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "CD/DVD-schijf"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "CPU-configuratie kon niet worden opgeslagen"
+
+#~ msgid "CPU type"
+#~ msgstr "CPU-type"
+
+#~ msgid "Change firmware"
+#~ msgstr "Verander firmware"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Wijzigingen worden van kracht na het afsluiten van de VM"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Kies een besturingssysteem"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "Klikken op \"Lanceer viewer op afstand\" zal een .vv bestand downloaden "
+#~ "en $0 opstarten."
+
+#~ msgid "Clone"
+#~ msgstr "Kloon"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Kan image niet laden"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Bevestig deze actie"
+
+#~ msgid "Connect"
+#~ msgstr "Verbinden"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr ""
+#~ "Maak verbinding met elke viewertoepassing voor de volgende protocollen"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Verbinding maken met Virtualisatie-service"
+
+#~ msgid "Connection"
+#~ msgstr "Verbinding"
+
+#~ msgid "Console"
+#~ msgstr "Console"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Kernen per socket"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Kan niet terugkeren naar snapshot"
+
+#~ msgid "Crashed"
+#~ msgstr "Gecrasht"
+
+#~ msgid "Create VM"
+#~ msgstr "Maak VM aan"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Maak een kloon VM aan gebaseerd op $0"
+
+#~ msgid "Create new"
+#~ msgstr "Nieuwe aanmaken"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Nieuwe virtuele machine aanmaken"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Maak virtueel netwerk aan"
+
+#~ msgid "Creating VM"
+#~ msgstr "VM aanmaken"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "VM installatie aanmaken"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Aanmaken van VM $0 mislukte"
+
+#~ msgid "Creation time"
+#~ msgstr "Aanmaaktijd"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Huidige toewijzing"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Aangepaste firmware: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP-bereik"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Verwijder bijbehorende opslagbestanden:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Verwijder opslagpool $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Verwijder de volumes in deze pool"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Het verwijderen van een inactieve opslagpool zal alleen de definitie van "
+#~ "de pool verwijderen. De inhoud ervan wordt niet verwijderd."
+
+#~ msgid "Desktop viewer"
+#~ msgstr "Bureaubladviewer"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Koppel de schijven die deze pool gebruiken los van alle VM's voordat je "
+#~ "ze probeert te verwijderen."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Verbinden met seriële console verbroken. Klik op de verbinden knop."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Schijf $0 kan niet worden losgekoppeld van VM $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Schijf kon niet worden aangesloten"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Schijf kon niet aangemaakt worden"
+
+#~ msgid "Disk image file"
+#~ msgstr "Schijfimagebestand"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Schijfinstellingen konden niet opgeslagen worden"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "Momentopname op schijf"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "Domein is vastgelopen"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "Domein is geblokkeerd op hulpbron"
+
+#~ msgid "Download an OS"
+#~ msgstr "Download een OS"
+
+#~ msgid "Dying"
+#~ msgstr "Achteruitgaan"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Geëmuleerde machine"
+
+#~ msgid "End"
+#~ msgstr "Einde"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Einde mag niet leeg zijn"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Bestaande schijfimage op het bestandssysteem van de host"
+
+#~ msgid "Expand"
+#~ msgstr "Uitbreiden"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Kan firmware niet veranderen"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "Het ophalen van de IP-adressen van de interfaces in $0 is mislukt"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "Kan toets Ctrl+Alt+$0 niet verzenden naar VM $1"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr ""
+#~ "Minder dan het maximale aantal virtuele CPU's moet zijn ingeschakeld."
+
+#~ msgid "File"
+#~ msgstr "Bestand"
+
+#~ msgid "Filter by name"
+#~ msgstr "Filteren op naam"
+
+#~ msgid "Firmware"
+#~ msgstr "Firmware"
+
+#~ msgid "Force shut down"
+#~ msgstr "Forceer afsluiten"
+
+#~ msgid "Forward mode"
+#~ msgstr "Voorwaartse modus"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Modus doorsturen"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Automatisch genereren"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "Ga naar VM's lijst"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Verberg extra opties"
+
+#~ msgid "Host device"
+#~ msgstr "Hostapparaat"
+
+#~ msgid "Host name"
+#~ msgstr "Hostnaam"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Host mag niet leeg zijn"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Hypervisor details"
+
+#~ msgid "IP configuration"
+#~ msgstr "IP-configuratie"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 en IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4-netwerk"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv4-netwerk mag niet leeg zijn"
+
+#~ msgid "IPv4 only"
+#~ msgstr "alleen IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6-adres"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6-netwerk"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6-netwerk mag niet leeg zijn"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Alleen IPv6"
+
+#~ msgid "Idle"
+#~ msgstr "Inactief"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Start VM onmiddellijk"
+
+#~ msgid "Import"
+#~ msgstr "Importeren"
+
+#~ msgid "Import VM"
+#~ msgstr "Importeer VM"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "Importeer een virtuele machine"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "In de meeste configuraties werkt macvtap niet voor netwerkcommunicatie "
+#~ "van host naar gast."
+
+#~ msgid "Initiator"
+#~ msgstr "Initiator"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "Initiator IQN mag niet leeg zijn"
+
+#~ msgid "Installation source"
+#~ msgstr "Installatiebron"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Installatiebron mag niet leeg zijn"
+
+#~ msgid "Installation type"
+#~ msgstr "Installatietype"
+
+#~ msgid "Interface type"
+#~ msgstr "Interfacetype"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Ongeldig IPv4-masker of voorvoegsel lengte"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Ongeldig IPv6-adres"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Ongeldig IPv6-voorvoegsel"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Ongeldige bestandsnaam"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "Start viewer op afstand"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr "Laat het wachtwoord leeg als je geen root-account wilt aanmaken"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "Laat het wachtwoord leeg als je geen gebruikersaccount wilt aanmaken"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr "Laat het wachtwoord leeg als je geen root-account wilt aanmaken"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr "Libvirt heeft geen UEFI/OVMF firmware-image op de host gedetecteerd"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt of hypervisor ondersteunt UEFI niet"
+
+#~ msgid "Loading resources"
+#~ msgstr "Bronnen laden"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr ""
+#~ "De machine moet worden uitgeschakeld voordat het bustype wordt gewijzigd"
+
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr ""
+#~ "De machine moet worden uitgeschakeld voordat de cachemodus wordt gewijzigd"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Virtuele machines beheren"
+
+#~ msgid "Manual connection"
+#~ msgstr "Handmatige verbinding"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Masker of voorvoegsel lengte"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Masker of voorvoegsel lengte mag niet leeg zijn"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Maximale toewijzing"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "Maximaal geheugen kon niet worden opgeslagen"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Maximaal aantal virtuele CPU's toegewezen aan het gast-besturingssysteem"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Maximaal aantal virtuele CPU's toegewezen voor het gastbesturingssysteem, "
+#~ "dat moet liggen tussen 1 en $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Maximale transmissie-eenheid"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Geheugen kon niet worden opgeslagen"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "Geheugen mag niet 0 zijn"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Modeltype"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT naar $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "NIC $0 van VM $1 kon de status niet wijzigen"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Naam bevat ongeldige tekens"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Naam mag niet leeg zijn"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Naam mag niet leeg zijn"
+
+#~ msgid "Name: "
+#~ msgstr "Naam: "
+
+#~ msgid "Netmask"
+#~ msgstr "Netmasker"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Netwerk $0 kan niet geactiveerd worden"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Netwerk $0 kan niet gedeactiveerd worden"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Netwerk opstarten (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Netwerk bestandssyteem"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Netwerkinterface-instellingen konden niet opgeslagen worden"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "Netwerk selectie ondersteunt PXE niet."
+
+#~ msgid "Networks"
+#~ msgstr "Netwerken"
+
+#~ msgid "New volume name"
+#~ msgstr "Nieuwe volumenaam"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "Er is geen VM actief of gedefinieerd op deze host"
+
+#~ msgid "No connection available"
+#~ msgstr "Geen verbinding beschikbaar"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Geen schijven gedefinieerd voor deze VM"
+
+#~ msgid "No network devices"
+#~ msgstr "Geen netwerkapparaten"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Geen netwerkinterfaces gedefinieerd voor deze VM"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "Geen netwerk gedefinieerd op deze host"
+
+#~ msgid "No networks available"
+#~ msgstr "Geen netwerken beschikbaar"
+
+#~ msgid "No parent"
+#~ msgstr "Geen ouder"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Geen momentopnames gedefinieerd voor deze VM"
+
+#~ msgid "No state"
+#~ msgstr "Geen status"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "Er is geen opslagpool gedefinieerd op deze host"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Geen opslagpools beschikbaar"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "Geen opslagvolumes gedefinieerd voor deze opslagpool"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Geen virtuele netwerken"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "Niet-perminent netwerk kan niet worden verwijderd. Het houdt op te "
+#~ "bestaan wanneer het wordt gedeactiveerd."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "Niet-permanente opslagpool kan niet worden verwijderd. Het houdt op te "
+#~ "bestaan wanneer het wordt gedeactiveerd."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Geen (geïsoleerd netwerk)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Een of meer geselecteerde volumes worden gebruikt door domeinen. Maak "
+#~ "eerst de schijven los om het volume te verwijderen."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Alleen bewerkbaar wanneer de gast uitgeschakeld is"
+
+#~ msgid "Open"
+#~ msgstr "Openen"
+
+#~ msgid "Operating system"
+#~ msgstr "Besturingssysteem"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "Bewerking is bezig"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "Ouder momentopname"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Pan op bestandssysteem van host"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Pad naar ISO-bestand op bestandssysteem van host"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Pad naar bestand op bestandssysteem van host"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Pad naar bestand op bestandssysteem van host"
+
+#~ msgid "Paused"
+#~ msgstr "Gepauzeerd"
+
+#~ msgid "Persistence"
+#~ msgstr "Volhardend"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Fysiek schijfapparaat"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Fysiek schijfapparaat op host"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Kies een opslagpool"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "Kies een volume"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Voer een nieuwe volumenaam in"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Start de virtuele machine om toegang te krijgen tot de console."
+
+#~ msgid "Plug"
+#~ msgstr "Plug"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "Pool moet actief zijn om volume te creëren"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "Pooltype ondersteunt geen volume-creatie"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "De volumes van pool worden gebruikt door VM's "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Gewenst aantal sockets voor de gast."
+
+#~ msgid "Prefix"
+#~ msgstr "Voorvoegsel"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "Prefixlengte mag niet leeg zijn"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "Met eerder gemaakte momentopnames kun je terugkeren naar een eerdere "
+#~ "toestand als er iets misgaat"
+
+#~ msgid "Product"
+#~ msgstr "Product"
+
+#~ msgid "Profile"
+#~ msgstr "Profiel"
+
+#~ msgid "Protocol"
+#~ msgstr "Protocol"
+
+#~ msgid "Remote URL"
+#~ msgstr "URL op afstand"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "Viewerdetails op afstand"
+
+#~ msgid "Revert"
+#~ msgstr "Terugdraaien"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Keer terug naar snapshot $0"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "Als je terugkeert naar deze snapshot, keert de VM terug naar het moment "
+#~ "van de snapshot en gaat de huidige status verloren, samen met alle "
+#~ "gegevens die niet in een snapshot zijn vastgelegd"
+
+#~ msgid "Root password"
+#~ msgstr "Rootwachtwoord"
+
+#~ msgid "Route to $0"
+#~ msgstr "Route naar $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Voer een installatie zonder toezicht uit"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Uitvoeren wanneer de host opstart"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE TLS poort"
+
+#~ msgid "SPICE address"
+#~ msgstr "SPICE adres"
+
+#~ msgid "SPICE port"
+#~ msgstr "SPICE poort"
+
+#~ msgid "Select console type"
+#~ msgstr "Selecteer consoletype"
+
+#~ msgid "Send key"
+#~ msgstr "Verzend sleutel"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Verzend niet-maskeerbare onderbreking"
+
+#~ msgid "Serial console"
+#~ msgstr "Seriële console"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Stel DHCP-bereik in"
+
+#~ msgid "Set manually"
+#~ msgstr "Stel handmatig in"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "Het instellen van de gebruikerswachtwoorden voor installatie zonder "
+#~ "toezicht vereist het starten van de VM bij het maken ervan"
+
+#~ msgid "Show additional options"
+#~ msgstr "Extra opties tonen"
+
+#~ msgid "Shut off"
+#~ msgstr "Uit"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "Schakel de VM uit om de firmwareconfiguratie te bewerken"
+
+#~ msgid "Shutting down"
+#~ msgstr "Afsluiten"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Momentopname kon niet aangemaakt worden"
+
+#~ msgid "Source format"
+#~ msgstr "Bronformaat"
+
+#~ msgid "Source path"
+#~ msgstr "Bronpad"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Bronpad mag niet leeg zijn"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "Bron moet beginnen met http, ftp of nfs protocol"
+
+#~ msgid "Source volume group"
+#~ msgstr "Bron volumegroep"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Start libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Start pool wanneer host opstart"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Start mag niet leeg zijn"
+
+#~ msgid "Startup"
+#~ msgstr "Opstarten"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "Opslagpool $0 kan niet geactiveerd worden"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "Opslagpool $0 kan niet gedeactiveerd worden"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Opslagpool kan niet worden gemaakt"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Opslagpoolnaam"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "Opslaggrootte mag niet nul zijn"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr ""
+#~ "De grootte van het opslagvolume mag de capaciteit van de opslagpool niet "
+#~ "overschrijden ($0 $1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Opslagvolumes konden niet verwijderd worden"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "Geschorst (PM)"
+
+#~ msgid "Target path"
+#~ msgstr "Doelpad"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Doelpad mag niet leeg zijn"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr "De VM is actief en wordt vóór verwijdering uitgeschakeld."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "De VM moet actief of uitgeschakeld zijn om dit apparaat te ontkoppelen"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "De map op de server die wordt geëxporteerd"
+
+#~ msgid "The pool is empty"
+#~ msgstr "De pool is leeg"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr ""
+#~ "Het geselecteerde besturingssysteem ondersteunt geen installatie zonder "
+#~ "toezicht"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr ""
+#~ "Het geselecteerde besturingssysteem heeft een minimale geheugenvereiste "
+#~ "van $0 $1"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "Het geselecteerde besturingssysteem heeft een minimale "
+#~ "geheugengroottevereiste van $ 0 $ 1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "De opslagpool kon niet verwijderd worden"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr ""
+#~ "Deze virtuele machine is van voorbijgaande aard. Sluit het af als je het "
+#~ "wilt verwijderen."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Dit volume wordt al gebruikt door: "
+
+#~ msgid "Threads per core"
+#~ msgstr "Threads per kern"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr ""
+#~ "Tijdelijke VM's bieden geen ondersteuning voor het bewerken van "
+#~ "firmwareconfiguratie"
+
+#~ msgid "Type ID"
+#~ msgstr "Type-ID"
+
+#~ msgid "Unique name"
+#~ msgstr "Unieke naam"
+
+#~ msgid "Unique network name"
+#~ msgstr "Unieke netwerknaam"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Onbekende firmware"
+
+#~ msgid "Unplug"
+#~ msgstr "Loskoppelen"
+
+#~ msgid "Up to $0 $1 available on the default location"
+#~ msgstr "Tot $0 $1 beschikbaar op de standaardlocatie"
+
+#~ msgid "Up to $0 $1 available on the host"
+#~ msgstr "Tot $0 $1 beschikbaar op de host"
+
+#~ msgid "Url"
+#~ msgstr "Url"
+
+#~ msgid "User login must not be empty when user password is set"
+#~ msgstr ""
+#~ "Gebruikersaanmelding mag niet leeg zijn als het gebruikerswachtwoord is "
+#~ "ingesteld"
+
+#~ msgid "User password must not be empty when user login is set"
+#~ msgstr ""
+#~ "Het gebruikerswachtwoord mag niet leeg zijn als gebruikersaanmelding is "
+#~ "ingesteld"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "VCPU-instellingen konden niet opgeslagen worden"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "VM $0 bestaat al"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "VM $0 bestaat niet op $1 verbinding"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "VM $0 kan opnieuw opstarten niet forceren"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "VM $0 kon afsluiten niet forceren"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "VM $0 kan niet verwijderd worden"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "VM $0 kan niet geïnstalleerd worden"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "VM $0 kon niet opnieuw opstarten"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "VM $0 kan niet hervat worden"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "VM $0 kan NMI niet verzenden"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "VM $0 kan niet afgesloten worden"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "VM $0 kan niet gestart worden"
+
+#~ msgid "VM state"
+#~ msgstr "VM toestand"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC TLS-poort"
+
+#~ msgid "VNC address"
+#~ msgstr "VNC-adres"
+
+#~ msgid "VNC console"
+#~ msgstr "VNC-console"
+
+#~ msgid "VNC port"
+#~ msgstr "VNC-poort"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Virtuele machines"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Virtuele machines"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Beheer van virtuele machines"
+
+#~ msgid "Virtual network"
+#~ msgstr "Virtueel netwerk"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Virtueel netwerk kan niet aangemaakt worden"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Virtualisatieservice (libvirt) is niet actief"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Naam van volumegroep mag niet leeg zijn"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Beschrijfbaar"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Beschrijfbaar en gedeeld"
+
+#~ msgid "Yesterday"
+#~ msgstr "Gisteren"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "Je moet het meest overeenkomende besturingssysteem selecteren"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "uitgeschakeld"
+
+#~ msgid "down"
+#~ msgstr "niet actief"
+
+#~ msgid "enabled"
+#~ msgstr "ingeschakeld"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "hostapparaat"
+
+#~ msgid "host passthrough"
+#~ msgstr "host doorgeven"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "iSCSI direct doel"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "iSCSI-initiator IQN"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "iSCSI-doel IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "nee"
+
+#~ msgid "no state saved"
+#~ msgstr "geen toestand opgeslagen"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "omgeleid apparaat"
+
+#~ msgid "server"
+#~ msgstr "server"
+
+#~ msgid "up"
+#~ msgstr "actief"
+
+#~ msgid "vCPU count"
+#~ msgstr "vCPU telling"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU maximum"
+
+#~ msgid "vCPUs"
+#~ msgstr "vCPU's"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhost-gebruiker"
+
+#~ msgid "view more..."
+#~ msgstr "bekijk meer..."
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "virt-install pakket moet op het systeem geïnstalleerd worden om VM's te "
+#~ "klonen"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "virt-install pakket moet op het systeem geïnstalleerd worden om nieuwe "
+#~ "VM's te creëren"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "virt-install pakket moet op het systeem geïnstalleerd worden om dit "
+#~ "kenmerk te bewerken"
+
+#~ msgid "vm"
+#~ msgstr "vm"
+
+#~ msgid "Local install media"
+#~ msgstr "Lokale installatiemedia"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "Stel een rootwachtwoord in"
+
+#~ msgid "21st"
+#~ msgstr "21e"
+
+#~ msgid "22nd"
+#~ msgstr "22e"
+
+#~ msgid "23rd"
+#~ msgstr "23e"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Na"
+
+#~ msgid "Friday"
+#~ msgstr "vrijdag"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Uur : minuut"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "Uur moet een getal zijn tussen 0-23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Ongeldige datumnotatie."
+
+#~ msgid "Invalid number."
+#~ msgstr "Ongeldig getal."
+
+#~ msgid "Monday"
+#~ msgstr "maandag"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Herhaal elk uur"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Jaarlijks herhalen"
+
+#~ msgid "Saturday"
+#~ msgstr "zaterdag"
+
+#~ msgid "Sunday"
+#~ msgstr "zondag"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Deze dag bestaat niet in alle maanden.<br> De timer wordt alleen "
+#~ "uitgevoerd in maanden met 31e."
+
+#~ msgid "Thursday"
+#~ msgstr "donderdag"
+
+#~ msgid "Tuesday"
+#~ msgstr "dinsdag"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 vernieuwing"
+#~ msgstr[1] "$0 vernieuwingen"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 beveiligingsreparatie"
+#~ msgstr[1] "$1 beveiligingsreparaties"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Opslagapparaten beheren"
+
+#~ msgid "Restart now"
+#~ msgstr "Nu herstarten"
+
+#~ msgid "Configure"
+#~ msgstr "Configureer"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr ""
+#~ "Netwerk opstarten is alleen beschikbaar bij gebruik van de "
+#~ "systeemverbinding"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Netwerken"
+
+#~ msgid "No snapshots"
+#~ msgstr "Geen momentopnames"
+
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "Een dergelijk bestand is niet gevonden in map '$0'"
+
+#~ msgid "No updates pending"
+#~ msgstr "Geen updates in behandeling"
+
+#~ msgid "This directory is empty"
+#~ msgstr "Deze map is leeg"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr ""
+#~ "Dit pooltype biedt geen ondersteuning voor het maken van opslagvolume"
+
+#~ msgid "undefined"
+#~ msgstr "onbepaald"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Onjuiste hostsleutel"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "De authenticiteit van host {{#strong}}{{host}}{{/strong}} kan niet worden "
+#~ "vastgesteld. Weet je zeker dat je door wilt gaan met verbinden?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "De sleutel van {{#strong}}{{host}}{{/strong}} komt niet overeen met de "
+#~ "eerder gebruikte sleutel. Tenzij deze machine onlangs is vervangen, is "
+#~ "het waarschijnlijk dat iemand je verbinding met deze machine probeert aan "
+#~ "te vallen."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Onbekende hostsleutel"
+
+#~ msgid "Address:"
+#~ msgstr "Adres:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Minimaal $0 schijven zijn noodzakelijk."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "Verbind met elke SPICE of VNC viewertoepassing."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Download de MSI van $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Grafische console (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Grafische console in Desktop Viewer"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Voor deze virtuele machine is geen console gedefinieerd."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid ""
+#~ "For the host, either specify the hostname, IP address, an alias name or a "
+#~ "unique resource identifier for the SSH destination."
+#~ msgstr ""
+#~ "Geef voor de host de hostnaam, het IP-adres, een aliasnaam of een unieke "
+#~ "bron-ID op voor de SSH-bestemming."
+
+#~ msgid ""
+#~ "Specify the host and the login user account for the host that you want to "
+#~ "add."
+#~ msgstr ""
+#~ "Geef de host en het inloggebruikersaccount op voor de host die je wilt "
+#~ "toevoegen."
+
+#~ msgid "Entry"
+#~ msgstr "Ingang"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Je browser zal de verbinding verbreken, maar dit heeft geen invloed op "
+#~ "het updateproces. Je kunt binnen enkele ogenblikken opnieuw verbinding "
+#~ "maken om de voortgang te blijven bekijken."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "en start de machine automatisch opnieuw."
+
+#~ msgid "Dashboard"
+#~ msgstr "Dashboard"
+
+#~ msgid "Description input text"
+#~ msgstr "Beschrijving inputtekst"
+
+#~ msgid "FAILED"
+#~ msgstr "MISLUKT"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Verbinding verbroken. Probeer opnieuw verbinding te maken"
+
+#~ msgid "Name input text"
+#~ msgstr "Naam inputtekst"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Netwerk"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Voer een nieuwe volumegrootte in"
+
+#~ msgid "Servers"
+#~ msgstr "Servers"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "Je bent momenteel rechtstreeks verbonden met deze server. Je kunt het "
+#~ "niet verwijderen."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bits"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 bytes"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 bytes"
+
+#~ msgid "Bond settings "
+#~ msgstr "Bindinginstellingen "
+
+#~ msgid "IP settings "
+#~ msgstr "IP-instellingen "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Cockpit had een onverwachte interne fout. <br/> <br/>Je kunt proberen "
+#~ "Cockpit opnieuw op te starten door in je browser op Vernieuwen te "
+#~ "drukken. De javascript-console bevat details over deze fout (<b>Ctrl-"
+#~ "Shift-J</b> in de meeste browsers)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "De VM is gecrasht."
+
+#~ msgid "The VM is down."
+#~ msgstr "De VM is uitgeschakeld."
+
+#~ msgid "The VM is going down."
+#~ msgstr "De VM wordt uitgeschakeld."
+
+#~ msgid "The VM is idle."
+#~ msgstr "De VM is niet actief."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr "De VM is bezig met sterven (afsluiten of crash is niet voltooid)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "De VM is gepauzeerd."
+
+#~ msgid "The VM is running."
+#~ msgstr "De VM is actief."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "De VM wordt opgeschort door gast-energiebeheer."
+
+#~ msgid "inactive"
+#~ msgstr "inactief"
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Laat leeg om verbinding te maken met deze machine als de momenteel "
+#~ "aangemelde gebruiker. Als je een andere gebruikersnaam invoert, wordt die "
+#~ "gebruiker altijd gebruikt bij het verbinden met deze machine."
+
+#~ msgid "2 min"
+#~ msgstr "2 min"
+
+#~ msgid "3 min"
+#~ msgstr "3 min"
+
+#~ msgid "4 min"
+#~ msgstr "4 min"
+
+#~ msgid "CPU graph"
+#~ msgstr "CPU-grafiek"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "CPU-status"
+
+#~ msgid "Cached"
+#~ msgstr "Gecached"
+
+#~ msgid "Graphs"
+#~ msgstr "Grafieken"
+
+#~ msgid "I/O wait"
+#~ msgstr "I/O wachttijd"
+
+#~ msgid "Kernel"
+#~ msgstr "Kernel"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Geheugen"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "Geheugen & swap gebruik"
+
+#~ msgid "Memory graph"
+#~ msgstr "Geheugen grafiek"
+
+#~ msgid "Network traffic"
+#~ msgstr "Netwerkverkeer"
+
+#~ msgid "Nice"
+#~ msgstr "Nice"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Gebruikers synchroniseren"
+
+#~ msgid "Usage graphs"
+#~ msgstr "Gebruik grafieken"
+
+#~ msgid "View graphs"
+#~ msgstr "Bekijk grafieken"
+
+#~ msgid "idle"
+#~ msgstr "nietsdoend"
+
+#~ msgid "paused"
+#~ msgstr "gepauzeerd"
+
+#~ msgid "running"
+#~ msgstr "in uitvoering"
+
+#~ msgid "shut off"
+#~ msgstr "uitzetten"
+
+#~ msgid "shutdown"
+#~ msgstr "afsluiten"
+
+#~ msgid "Add a new host"
+#~ msgstr "Een nieuwe host toevoegen"
+
+#~ msgid "Available"
+#~ msgstr "Beschikbaar"
+
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Vul IP-adres of hostnaam in"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Netwerkinterfaces"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Deze versie van cockpit-ws biedt geen ondersteuning voor het verbinden "
+#~ "met een host met een alternatieve gebruiker of poort"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Om een andere poort te proberen, moet je cockpit-ws upgraden naar een "
+#~ "nieuwere versie."
+
+#~ msgid "$0 template"
+#~ msgstr "$0 Templaat"
+
+#~ msgid "Instance of template: "
+#~ msgstr "Voorbeeld van sjabloon: "
+
+#~ msgid "Instantiate"
+#~ msgstr "Instantiëren"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 delen"
+
+#~ msgid "All In One"
+#~ msgstr "Alles in een"
+
+#~ msgid "Always"
+#~ msgstr "Altijd"
+
+#~ msgid "Author"
+#~ msgstr "Auteur"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Onjuiste data doorgegeven voor passwd1-mechanisme"
+
+#~ msgid "CPU priority"
+#~ msgstr "CPU-prioriteit"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Kan niet verbinden met Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Verander resource limieten"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Limieten voor bronnen veranderen"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit kon niet inloggen op {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit kon niet inloggen op {{#strong}}{{host}}{{/strong}}. Je kunt je "
+#~ "authenticatiegegevens hieronder veranderen. {{#can_sync}}Je wilt "
+#~ "misschien {{#sync_link}}accounts en wachtwoorden synchroniseren{{/"
+#~ "sync_link}}.{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Gecombineerd geheugengebruik"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Gecombineerd gebruik van $0 CPU-kern"
+#~ msgstr[1] "Gecombineerd gebruik van $0 CPU-kernen"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "Commando mag niet leeg zijn"
+
+#~ msgid "Command:"
+#~ msgstr "Commando:"
+
+#~ msgid "Commit"
+#~ msgstr "Committeren"
+
+#~ msgid "Commit image"
+#~ msgstr "Committeer image"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Verbinding maken met Docker"
+
+#~ msgid "Container name"
+#~ msgstr "Containernaam"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Container is momenteel gemarkeerd als niet actief, maar ordelijk stoppen "
+#~ "is mislukt."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Container is momenteel actief."
+
+#~ msgid "Container:"
+#~ msgstr "Container:"
+
+#~ msgid "Containers"
+#~ msgstr "Containers"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Containers"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Kon gebruikersgroepen niet veranderen"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Kon gebruikerswachtwoord niet veranderen"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Kon niet verbinden met de machine"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Kon geen nieuwe gebruikers aanmaken"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Kon lokale gebruikers niet tonen"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Kon gebruikers niet tonen"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Kon gebruikersdata niet laden"
+
+#~ msgid "Created:"
+#~ msgstr "Aangemaakt:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Docker containers"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker is niet geïnstalleerd of geactiveerd op het systeem"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Dubbele alias"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Dubbele poort"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "Voer een wachtzin in om identiteit aan ssh-agent toe te voegen"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Als je hier een ander wachtwoord invoert, moet je dit opnieuw typen "
+#~ "wanneer je opnieuw verbinding maakt met deze machine"
+
+#~ msgid "Environment"
+#~ msgstr "Omgeving"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Fout tijdens het laden van gebruikers: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Foutboodschap van Docker:"
+
+#~ msgid "Everything"
+#~ msgstr "Alles"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Verlaten met $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Containerpoorten blootleggen"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Starten van Docker mislukt: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Kan Docker scope niet stoppen: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Verkrijg nieuwe image"
+
+#~ msgid "IP address:"
+#~ msgstr "IP-adres:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "Lengte IP-voorvoegsel:"
+
+#~ msgid "Id"
+#~ msgstr "Id"
+
+#~ msgid "Id:"
+#~ msgstr "Id:"
+
+#~ msgid "Image"
+#~ msgstr "Image"
+
+#~ msgid "Image $0"
+#~ msgstr "Image $0"
+
+#~ msgid "Image search"
+#~ msgstr "Image opzoeken"
+
+#~ msgid "Image:"
+#~ msgstr "Image:"
+
+#~ msgid "Images"
+#~ msgstr "Images"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Images"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Images en containers uitvoeren"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Om gebruikers te synchroniseren, moet je inloggen op {{#strong}}{{host}}"
+#~ "{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Ongeldige poort"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Kerberos-ticket"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Laat leeg om verbinding te maken met deze machine als de momenteel "
+#~ "ingelogde gebruiker{{#default_user}} ({{default_user}}){{/default_user}}. "
+#~ "Als je een andere gebruikersnaam invoert, wordt die gebruiker altijd "
+#~ "gebruikt om verbinding te maken met deze machine."
+
+#~ msgid "Link to another container"
+#~ msgstr "Link naar een andere container"
+
+#~ msgid "Links:"
+#~ msgstr "Links:"
+
+#~ msgid "Login Password"
+#~ msgstr "Inlogwachtwoord"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC adres:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Geheugenlimiet"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Koppel containervolumes aan"
+
+#~ msgid "No container specified"
+#~ msgstr "Geen container opgegeven"
+
+#~ msgid "No containers"
+#~ msgstr "Geen containers"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Geen containers die overeenkomen met het huidige filter"
+
+#~ msgid "No images"
+#~ msgstr "Geen images"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Geen images die overeenkomen met het huidige filter"
+
+#~ msgid "No results for $0"
+#~ msgstr "Geen resultaten voor $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Geen images die overeenkomen met het huidige filter"
+
+#~ msgid "No running containers"
+#~ msgstr "Geen actieve containers"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Geen actieve containers die overeenkomen met het huidige filter"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Geen toestemming voor toegang tot Docker op dit systeem"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "Bij mislukking, probeer $0 keer opnieuw"
+#~ msgstr[1] "Bij mislukking, probeer $0 keer opnieuw"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Bevestig geforceerde verwijdering van $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Probeer een andere term"
+
+#~ msgid "Ports:"
+#~ msgstr "Poorten:"
+
+#~ msgid "Problems"
+#~ msgstr "Problemen"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Alleen-lezen"
+
+#~ msgid "ReadWrite"
+#~ msgstr "Lezen schrijven"
+
+#~ msgid "Repository"
+#~ msgstr "Repository"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Herstart beleid:"
+
+#~ msgid "Retries:"
+#~ msgstr "Pogingen:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Hergebruik mijn wachtwoord voor verbindingen op afstand"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Selecteer de gebruikers die gesynchroniseerd moeten worden met {{#strong}}"
+#~ "{{host}}{{/strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Stel containeromgevingsvariabelen in"
+
+#~ msgid "Severity:"
+#~ msgstr "Strengheid:"
+
+#~ msgid "Show all containers"
+#~ msgstr "Toon alle containers"
+
+#~ msgid "Start Docker"
+#~ msgstr "Start Docker"
+
+#~ msgid "State:"
+#~ msgstr "Toestand:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Synchroniseren"
+
+#~ msgid "Tag"
+#~ msgstr "Label"
+
+#~ msgid "Tags"
+#~ msgstr "Tags"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "De volgende containers zijn afhankelijk van deze image en worden "
+#~ "onbruikbaar."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Deze image bestaat niet."
+
+#~ msgid "Type a password"
+#~ msgstr "Type een wachtwoord"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Typ om te filteren…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "Tenzij gestopt"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Niet-ondersteund instellingsmechanisme"
+
+#~ msgid "Up since $0"
+#~ msgstr "Actief sinds $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Gebruikt door containers"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Beschikbare inloggegevens gebruiken"
+
+#~ msgid "Volumes"
+#~ msgstr "Volumes"
+
+#~ msgid "Volumes:"
+#~ msgstr "Volumes:"
+
+#~ msgid "With terminal"
+#~ msgstr "Met terminal"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Je bent verbonden met {{#strong}}{{host}}{{/strong}}, maar om gebruikers "
+#~ "te synchroniseren is een gebruiker met superuser-rechten vereist."
+
+#~ msgid "default"
+#~ msgstr "standaard"
+
+#~ msgid "key"
+#~ msgstr "sleutel"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "zoeken op naam, naamruimte of beschrijving"
+
+#~ msgid "shares"
+#~ msgstr "delen"
+
+#~ msgid "to host port"
+#~ msgstr "naar host-poort"
+
+#~ msgid "value"
+#~ msgstr "waarde"
+
+#~ msgid "Master"
+#~ msgstr "Meester"
+
+#~ msgid "Password for $0"
+#~ msgstr "Wachtwoord voor $0"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr ""
+#~ "De gebruiker <b>$0</b> heeft geen rechten voor het beheren van servers"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Accounts"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr ""
+#~ "De gebruiker <b>$0</b> heeft geen rechten voor het wijzigen van accounts"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Kan root-account niet verwijderen"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Kan root-account niet hernoemen"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "Niet gemachtigd om een nieuwe zone toe te voegen"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "Niet gemachtigd om services toe te voegen aan zone $0"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "Niet gemachtigd om service $0 te verwijderen"
+
+#~ msgid "Account Settings"
+#~ msgstr "Account-instellingen"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Machine aan Dashboard toevoegen"
+
+#~ msgid "Machines"
+#~ msgstr "Machines"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "Inloggen heeft beheerdersrechten geëscaleerd"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Wachtwoord niet bruikbaar voor geprivilegieerde taken of om verbinding te "
+#~ "maken met andere machines"
+
+#~ msgid "Privileged"
+#~ msgstr "Bevoorrecht"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Hergebruik mijn wachtwoord voor bevoorrechte taken en te verbinden met "
+#~ "andere machines"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr "Het is gebruiker $0 niet toegestaan om hostnamen te wijzigen"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr "De gebruiker $0 mag deze server niet afsluiten of opnieuw opstarten"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr ""
+#~ "De gebruiker <b>$0</b> heeft geen rechten voor het wijzigen van profielen"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr ""
+#~ "De gebruiker <b>$0</b> heeft geen rechten voor het beheren van opslag"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr ""
+#~ "De gebruiker <b>$0</b> heeft geen rechten voor het wijzigen van "
+#~ "netwerkinstellingen"
+
+#~ msgid "Required by $0"
+#~ msgstr "Vereist door $0"
+
+#~ msgid "Automatic Startup"
+#~ msgstr "Automatisch opstarten"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Laatste trigger"
+
+#~ msgid "Next Run"
+#~ msgstr "Volgende run"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "De gebruiker <b>$0</b> heeft geen rechten voor het maken van timers"
+
+#~ msgid "Connecting"
+#~ msgstr "Verbinden"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Over Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (gedeeld met het OS)"
+
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 Kwetsbaarheid"
+#~ msgstr[1] "$0 Kwetsbaarheden"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Extra opslag toevoegen"
+
+#~ msgid "Add Storage"
+#~ msgstr "Opslag toevoegen"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Extra opslag"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Alle gegevens op de geselecteerde schijven worden verwijderd en de "
+#~ "schijven worden toegevoegd aan de opslagruimte."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Configureer opslag..."
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Niet alle schijven konden worden toegevoegd"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Wis containers en reset opslagpool"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr "Informatie over de Docker-opslagpool is niet beschikbaar."
+
+#~ msgid "Local Disks"
+#~ msgstr "Lokale schijven"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "Geen extra lokale opslag gevonden."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Formatteer en voeg de schijven toe"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "Door de opslagpool te resetten, worden alle containers gewist en worden "
+#~ "schijven in de pool vrijgegeven."
+
+#~ msgid "Security"
+#~ msgstr "Veiligheid"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr "De Docker-opslagpool kan niet op dit systeem beheerd worden."
+
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "De scan van $time ($type) vond $count kwetsbaarheid:"
+#~ msgstr[1] "De scan van $time ($type) vond $count kwetsbaarheden:"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "De scan van $time ($type) vond geen kwetsbaarheden."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "The scan van $time ($type) was niet succesvol."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Virtualisatieservice is beschikbaar"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "Je hebt geen toestemming om de Docker-opslagpool te beheren."
diff --git a/po/pl.po b/po/pl.po
new file mode 100644
index 0000000..08f35c0
--- /dev/null
+++ b/po/pl.po
@@ -0,0 +1,13285 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Peter <pvolpe@redhat.com>, 2015. #zanata
+# Piotr Drąg <piotrdrag@gmail.com>, 2015. #zanata, 2020.
+# Piotr Drąg <piotrdrag@gmail.com>, 2016. #zanata, 2020.
+# Piotr Drąg <piotrdrag@gmail.com>, 2017. #zanata, 2020.
+# Piotr Drąg <piotrdrag@gmail.com>, 2018. #zanata, 2020.
+# Piotr Drąg <piotrdrag@gmail.com>, 2019. #zanata, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2024-02-05 13:36+0000\n"
+"Last-Translator: Daviteusz <daviteusz0@gmail.com>\n"
+"Language-Team: Polish <https://translate.fedoraproject.org/projects/cockpit/"
+"main/pl/>\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.3.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "Liczba użytkowników"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 procesor"
+msgstr[1] "$0 procesory"
+msgstr[2] "$0 procesorów"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 aktywna strefa"
+msgstr[1] "$0 aktywne strefy"
+msgstr[2] "$0 aktywnych stref"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 dostępne"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "Systemy plików $0 nie mogą być powiększane."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "Systemy plików $0 nie mogą być zmniejszane."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "Nie można tutaj zmieniać rozmiaru systemów plików $0."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "Nie można tutaj zmieniać rozmiaru systemów plików $0."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "Rozmiar bloku: $0"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 trafienie krytyczne"
+msgstr[1] "$0 trafienia, w tym krytyczne"
+msgstr[2] "$0 trafień, w tym krytyczne"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 danych + $1 nadwyżki użyto z $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 dzień"
+msgstr[1] "$0 dni"
+msgstr[2] "$0 dni"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "Brak $0 dysku"
+msgstr[1] "Brak $0 dysków"
+msgstr[2] "Brak $0 dysków"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "Brak $0 dysku"
+msgstr[1] "Brak $0 dysków"
+msgstr[2] "Brak $0 dysków"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "Dyski: $0"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "dokumentacja $0"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "błąd $0"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 zakończyło działanie z kodem $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 się nie powiodło"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 próba zalogowania zakończona niepowodzeniem"
+msgstr[1] "$0 próby zalogowania zakończone niepowodzeniem"
+msgstr[2] "$0 prób zalogowania zakończonych niepowodzeniem"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "System plików $0"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 wolne"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 godzina"
+msgstr[1] "$0 godziny"
+msgstr[2] "$0 godzin"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 ważne trafienie"
+msgstr[1] "$0 trafienia, w tym ważne"
+msgstr[2] "$0 trafień, w tym ważne"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "Plik $0 już istnieje"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 jest używane"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 nie jest dostępne w żadnym repozytorium."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "Zmieniono klucz $0"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 zostało zakończone z sygnałem $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 trafienie o niskiej ważności"
+msgstr[1] "$0 trafienia o niskiej ważności"
+msgstr[2] "$0 trafień o niskiej ważności"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minuta"
+msgstr[1] "$0 minuty"
+msgstr[2] "$0 minut"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 trafienie o umiarkowanej ważności"
+msgstr[1] "$0 trafienia, w tym o umiarkowanej ważności"
+msgstr[2] "$0 trafień, w tym o umiarkowanej ważności"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 miesiąc"
+msgstr[1] "$0 miesiące"
+msgstr[2] "$0 miesięcy"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 więcej…"
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 pakiet"
+msgstr[1] "$0 pakiety"
+msgstr[2] "$0 pakietów"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 pakiet wymaga ponownego uruchomienia komputera"
+msgstr[1] "$0 pakiety wymagają ponownego uruchomienia komputera"
+msgstr[2] "$0 pakietów wymaga ponownego uruchomienia komputera"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 strona"
+msgstr[1] "$0 strony"
+msgstr[2] "$0 stron"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "partycja"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 poprawka zabezpieczeń jest dostępna"
+msgstr[1] "$0 poprawki zabezpieczeń są dostępne"
+msgstr[2] "$0 poprawek zabezpieczeń jest dostępnych"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 usługa się nie powiodła"
+msgstr[1] "$0 usługi się nie powiodły"
+msgstr[2] "$0 usług się nie powiodło"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 usługa musi zostać ponownie uruchomiona"
+msgstr[1] "$0 usługi muszą zostać ponownie uruchomione"
+msgstr[2] "$0 usług musi zostać ponownie uruchomionych"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 pozostałe gniazdo"
+msgstr[1] "$0 pozostałe gniazda"
+msgstr[2] "$0 pozostałych gniazd"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 szczyt"
+msgstr[1] "$0 szczyty"
+msgstr[2] "$0 szczytów"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+#, fuzzy
+#| msgid "Not synchronized"
+msgid "$0 synchronized"
+msgstr "Niesynchronizowane"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 razem"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 aktualizacja jest dostępna"
+msgstr[1] "$0 aktualizacje są dostępne"
+msgstr[2] "$0 aktualizacji jest dostępnych"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 używanych z $1 ($2 zapisano)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 tydzień"
+msgstr[1] "$0 tygodnie"
+msgstr[2] "$0 tygodni"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 zostanie zainstalowane."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 rok"
+msgstr[1] "$0 lata"
+msgstr[2] "$0 lat"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "Strefa $0"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: awaria o $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (z $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Tworzenie partycji $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Lokalny punkt montowania"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Niezamontowane"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(zalecane)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", w tym $1 poprawka zabezpieczeń"
+msgstr[1] ", w tym $1 poprawki zabezpieczeń"
+msgstr[2] ", w tym $1 poprawek zabezpieczeń"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 dzień"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 godzina"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 minuta"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 minuta"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 tydzień"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14."
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 minut"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 minut"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 minut"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4."
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 minut"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minut"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5."
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 godzin"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "Godzina"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "LACP 802.3ad"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9."
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Na $0 nie zainstalowano zgodnej wersji Cockpit."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "System plików o tej nazwie już istnieje w tej puli."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "Grupa o tej nazwie już istnieje"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Z powodów bezpieczeństwa, niezawodności i wydajności wymagana jest "
+"nowoczesna przeglądarka."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Wiązanie sieciowe łączy wiele interfejsów sieciowych w jeden interfejs "
+"logiczny o wyższej przepustowości lub redundancji."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Nowy klucz SSH w $0 zostanie utworzony dla użytkownika $1 na komputerze $2 "
+"i zostanie dodany do pliku $3 użytkownika $4 na komputerze $5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Pakiet wymaga ponownego uruchomienia komputera, aby aktualizacje zostały "
+"uwzględnione:"
+msgstr[1] ""
+"Część pakietów wymaga ponownego uruchomienia komputera, aby aktualizacje "
+"zostały uwzględnione:"
+msgstr[2] ""
+"Część pakietów wymaga ponownego uruchomienia komputera, aby aktualizacje "
+"zostały uwzględnione:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Pula o tej nazwie już istnieje."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"Usługa wymaga ponownego uruchomienia, aby aktualizacje zostały uwzględnione:"
+msgstr[1] ""
+"Część usług wymaga ponownego uruchomienia, aby aktualizacje zostały "
+"uwzględnione:"
+msgstr[2] ""
+"Część usług wymaga ponownego uruchomienia, aby aktualizacje zostały "
+"uwzględnione:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "Nie można tutaj zmieniać rozmiaru woluminów fizycznych."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "Monitorowanie ARP"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "Ping ARP"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "O programie"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Nieobecne"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Przyjmij klucz i zaloguj się"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+#, fuzzy
+#| msgid "Set password"
+msgid "Acceptable password"
+msgstr "Ustaw hasło"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Wygasanie konta"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Konto jest niedostępne lub nie może być modyfikowane."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Konta"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Działanie"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Działania"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Aktywuj"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Aktywowanie $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Aktywne"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Aktywna kopia zapasowa"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Aktywne strony"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Aktywna od "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Aktywny stan"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Adaptacyjne równoważenie obciążenia"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Adaptacyjne równoważenie obciążenia przesyłu"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Dodaj"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Dodaj $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "Dodaj serwer DNS"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Dodaj szyfrowanie dysku dowiązanego sieciowo"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Dodaj serwer kluczy Tang"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Dodaj VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "Dodaj VPN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "Dodaj VPN WireGuard"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "Dodaj mapę bitową"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Dodaj nową strefę"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "Dodaj adres"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Dodaj urządzenia blokowe"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Dodaj wiązanie"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Dodaj mostek"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Dodaj dysk"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Dodaj dyski"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Dodaj portal iSCSI"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Dodaj klucz"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "Dodaj serwer kluczy"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Dodaj element"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Dodaj nowy komputer"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Dodaj nową strefę"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "Dodaj hasło"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "Dodaj partnera"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "LVM2 physical volume"
+msgid "Add physical volume"
+msgstr "Wolumin fizyczny LVM2"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Dodaj porty"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Dodaj porty do strefy $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Dodaj klucz publiczny"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "Dodaj trasę"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "Dodaj domenę wyszukiwania"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Dodaj usługi"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Dodaj usługi do strefy $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Dodaj usługi do strefy $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Dodaj zespół"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Dodaj strefę"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "Dodawanie „$0” do opcji szyfrowania"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Dodawanie „$0” do opcji systemów plików"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Dodanie $0 zerwie połączenie z serwerem i uniemożliwi korzystanie "
+"z interfejsu administracji."
+
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Dodanie serwera kluczy wymaga odblokowania puli. Proszę podać hasło "
+"istniejącej puli."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Dodanie niestandardowych portów spowoduje ponowne wczytanie zapory "
+"sieciowej. Ponowne wczytanie spowoduje utratę całej konfiguracji tylko dla "
+"obecnej sesji."
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Dodawanie klucza"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Dodawanie woluminu fizycznego do $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Dodawanie rd.neednet=1 do wiersza poleceń jądra"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Dodatkowy DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Dodatkowe domeny wyszukiwania DNS $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Dodatkowe działania"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Dodatkowy adres $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Dodatkowe pakiety:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Dodatkowe porty"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Adres"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Adres $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Adres nie może być pusty"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Adres nie jest prawidłowym adresem URL"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Adresy"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "Adresy są niewłaściwie sformatowane"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Administracja za pomocą konsoli internetowej Cockpit"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Dostęp administracyjny"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr ""
+"Do tworzenia i wyświetlania zgłoszeń wymagany jest dostęp administracyjny."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Wymagany jest dostęp administracyjny"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Zaawansowane TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Po"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Po opuszczeniu domeny, tylko użytkownicy z lokalnymi danymi uwierzytelniania "
+"będą mogli zalogować się do tego komputera. Może to mieć wpływ także na inne "
+"usługi, ponieważ ustawienia rozwiązywania DNS i listy zaufanych CA mogą ulec "
+"zmianie."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Po uruchomieniu systemu"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Alarm"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Alarmy i powyżej"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Alias"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Wszystkie"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Wszystkie aktualizacje"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Wszystko w jednym"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Zezwolenie na uruchamianie (odmaskowanie)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "Dozwolone adresy IP"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Dozwolone adresy"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Dokumentacja ról Ansible"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Można filtrować dowolny ciąg tekstowy w komunikatach dziennika. Ciąg może "
+"być także w formie wyrażenia regularnego. Obsługiwane jest także filtrowanie "
+"według pól komunikatu dziennika. Są to wartości rozdzielone spacjami "
+"w formie POLE=WARTOŚĆ, gdzie wartość może być listą możliwych wartości "
+"rozdzielonych przecinkami."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Wygląd"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "Brak informacji o aplikacjach"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Aplikacje"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Lista aplikacji"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Zastosuj i uruchom ponownie"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Zastosuj łaty jądra bez wyłączania"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Zastosuj to rozwiązanie"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Zastosowywanie nowych zasad… Może to zająć kilka minut."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Zastosowywanie rozwiązania…"
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Zastosowywanie aktualizacji"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Zastosowanie aktualizacji się nie powiodło"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "Odpowiednie dla krytycznych punktów montowania, takich jak /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Aplikacje"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Ocena"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Etykieta zasobu"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "Podczas uruchamiania"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Wymagany jest co najmniej $0 dysk."
+msgstr[1] "Wymagane są co najmniej $0 dyski."
+msgstr[2] "Wymaganych jest co najmniej $0 dysków."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "Wymagane jest co najmniej jedno urządzenie blokowe."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Wymagany jest co najmniej jeden dysk."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "O minucie"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "O sekundzie"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "O podanym czasie"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Atrybuty"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Dziennik audytu"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Uwierzytelnij"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Uwierzytelnianie"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Uwierzytelnienie"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Uwierzytelnienie się nie powiodło"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Uwierzytelnienie się nie powiodło: serwer zamknął połączenie"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Wymagane jest uwierzytelnienie, aby wykonać zadania wymagające uprawnień za "
+"pomocą konsoli internetowej Cockpit"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Wymagane jest uwierzytelnienie"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Upoważnij klucz SSH"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Upoważnione publiczne klucze SSH"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automatyczne"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automatyczne (tylko DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Automatyczne logowanie"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Automatyczne aktualizacje"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Automatycznie się uruchamia"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Automatycznie za pomocą NTP"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Automatycznie za pomocą dodatkowych serwerów NTP"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Automatycznie za pomocą podanych serwerów NTP"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Skrypt automatyzacji"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Dostępne cele w $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Dostępne aktualizacje"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "Grow partition"
+msgid "BIOS boot partition"
+msgstr "Powiększ partycję"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "Data BIOS-u"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "Wersja BIOS-u"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Wróć do kont"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Błędne"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Błędne ustawienie"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Równoważenie"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Przed"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Dowiązuje do"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Czarny"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Kasetowy"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Obudowa kasetowa"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "Urządzenie blokowe"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Urządzenie blokowe dla systemów plików"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Urządzenia blokowe"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Zablokowane"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Wiązanie"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Uruchomienie"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"Uruchomienie się nie powiedzie, jeśli system plików nie jest montowany, "
+"uniemożliwiając zdalny dostęp"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr ""
+"Uruchomienie nadal się powiedzie, kiedy system plików nie jest montowany"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Dowiązane przez"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Mostek"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Port mostku"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Ustawienia portu mostku"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Broadcast"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Uszkodzona konfiguracja"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Dostępne są aktualizacje naprawiające błędy"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Błędy"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Obudowa rozszerzenia magistrali"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Zmieniając hasło klucza SSH $0 na hasło logowania użytkownika $1 na "
+"komputerze $2, klucz automatycznie stanie się dostępny i w przyszłości "
+"będzie można logować się na komputerze $3 bez hasła."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " Obejdź sprawdzanie przeglądarki "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "Procesor"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Zabezpieczenia procesora"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "Przełączniki zabezpieczeń procesora"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Użycie procesora"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "Użycie/obciążenie procesora"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Pamięć podręczna"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr ""
+"Może być nazwą komputera, adresem IP, nazwą aliasu lub adresem URI typu "
+"ssh://"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr ""
+"Nie można odnaleźć żadnych dzienników za pomocą obecnego połączenia filtrów."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Anuluj"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Anuluj wyłączenie komputera"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Anuluj ponowne uruchomienie"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Nie można włączyć"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Nie można połączyć się z nieznanym komputerem"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Nie można przekazać danych uwierzytelniania logowania"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+"Nie można dołączyć do domeny, ponieważ usługa realmd nie jest dostępna w tym "
+"systemie"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Nie można planować zdarzeń w przeszłości"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Pojemność"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Operator"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Zmień"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "Zmień zasady kryptograficzne"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Zmień nazwę komputera"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Zmień nazwę inicjatora iSCSI"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Zmień nazwę inicjatora iSCSI"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change shell"
+msgid "Change label"
+msgstr "Zmień powłokę"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Zmień hasło"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Zmień hasło"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Zmień profil wydajności"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Zmień profil"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "Zmień powłokę"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Zmień czas systemu"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Zmień hasło klucza $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Zmień ustawienia"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Zmienione klucze są często wynikiem przeinstalowania systemu operacyjnego. "
+"Nieoczekiwana zmiana może jednak wskazywać na próbę przechwycenia połączenia "
+"przez stronę trzecią."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Zmiana ustawień zerwie połączenie z serwerem i uniemożliwi korzystanie "
+"z interfejsu administracji."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Wyszukaj aktualizacje"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"Proszę sprawdzić, czy suma SHA-256 lub SHA-1 z wiersza poleceń zgadza się z "
+"tą w tym oknie."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "Proszę sprawdzić sumę klucza z serwerem Tang."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Sprawdzanie $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Sprawdzanie IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "Sprawdzanie urządzenia RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Sprawdzanie i naprawianie urządzenia RAID $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Wyszukiwanie pakietu $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Wyszukiwanie obsługi NBDE w obrazie initrd"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Wyszukiwanie nowych aplikacji"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Wyszukiwanie aktualizacji pakietów…"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Sprawdzanie zainstalowanego oprogramowania"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Sprawdzanie powiązanych procesów"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Sprawdzanie stanu oprogramowania"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Proszę wybrać język używany w aplikacji"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Rozmiar bloku"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Klasa"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Czyszczenie dla $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Wyczyść „Uruchomienie się nie powiodło”"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Wyczyść wszystkie filtry"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Wyczyść wyszukiwanie"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Urządzenie tekstowe"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Oprogramowanie klienta"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Zamknij"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Zamknij zaznaczone strony"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Uwierzytelnianie Cockpit jest niepoprawnie skonfigurowane."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "Konfiguracja usług NetworkManager i firewalld w programie Cockpit"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit nie może skontaktować się z podanym komputerem."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Wystąpił nieoczekiwany wewnętrzny błąd w programie Cockpit."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit to menedżer serwerów ułatwiający administrowanie serwerów Linux "
+"przez przeglądarkę WWW. Można z łatwością przechodzić między terminalem "
+"a narzędziami WWW. Usługa uruchomiona za pomocą Cockpit może zostać "
+"zatrzymana za pomocą terminala. Jeśli błąd wystąpi w terminalu, to można go "
+"zobaczyć w interfejsie dziennika Cockpit."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit to interaktywny interfejs do administrowania serwerami Linux."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit nie jest zgodny z oprogramowaniem w systemie."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit nie jest zainstalowany"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit nie jest zainstalowany w systemie."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit jest idealny dla nowych administratorów, umożliwiając łatwe "
+"wykonywanie prostych zadań, takich jak administracja urządzeniami do "
+"przechowywania danych, badanie dzienników oraz uruchamianie i zatrzymywanie "
+"usług. Można monitorować i administrować wiele serwerów w tym samym czasie. "
+"Wystarczy dodać je pojedynczym kliknięciem."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit może nie być poprawnie wyświetlany w używanej przeglądarce"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Zbieranie i pakowanie danych diagnostycznych i wsparcia"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Zbieranie zrzutów awarii jądra"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Zbieraj statystyki"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Kolor"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "Przyjmowane są porty, zakresy i usługi oddzielone przecinkami"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Polecenie"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Nie odnaleziono polecenia"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Komentarz"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Komunikacja z tuned się nie powiodła"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Kompaktowe PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Zgodne ze wszystkimi systemami i urządzeniami (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Zgodne z nowoczesnymi systemami i dyskami twardymi > 2 TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Kompresowanie zrzutów awarii, aby oszczędzać miejsce"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Kompresja"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Warunek $0=$1 nie został spełniony"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Warunek się nie powiódł"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Konfiguracja"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Konfigurowanie"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Konfigurowanie IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Konfigurowanie kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Konfigurowanie ustawień systemu"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Potwierdź"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Potwierdź usunięcie $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Potwierdź hasło klucza"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Potwierdź nowe hasło klucza"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Potwierdź nowe hasło"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Potwierdź hasło"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Potwierdź usunięcie $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Potwierdź usunięcie alternatywnym hasłem"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Potwierdź zatrzymanie $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "W konflikcie z"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Powoduje konflikt z"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Łączenie automatyczne"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Połącz z"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Połącz z:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Łączenie z usługą SETroubleshoot…"
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Połączenie z usługą D-Bus się nie powiodło: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Łączenie z komputerem"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Błąd połączenia"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Połączenie się nie powiodło"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Połączenie przekroczyło czas oczekiwania."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "Połączenie zostanie utracone"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Składa się z"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Skontaktowana domena"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Zawiera:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Kontynuuj"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Kontynuuj sesję"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Sterowanie"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Sterowanie"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Kontroler"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "2 w jednym"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Skopiowano"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Skopiuj"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Skopiuj do schowka"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "$0. rdzeń"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "Nie można skontaktować się z $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Położenie zrzutu awarii"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Zgłaszanie awarii"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "System awarii"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Utwórz"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "Utwórz grupę woluminów LVM2"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "Utwórz urządzenie RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Utwórz urządzenie RAID"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Utwórz pulę Stratis"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Utwórz nowy klucz SSH i go upoważnij"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Utwórz migawkę systemu plików $0"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Utwórz konto z nieunikalnym UID"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Utwórz konto ze słabym hasłem"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Utwórz i zmień właściciela katalogu domowego"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Utwórz i zamontuj"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create and mount"
+msgid "Create and start"
+msgstr "Utwórz i zamontuj"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Utwórz system plików"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Utwórz"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Utwórz wolumin logiczny"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Utwórz nowe konto"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Utwórz nowy system plików"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Utwórz nową grupę"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Utwórz nowy wolumin logiczny"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Utwórz nowy plik zadania o tej treści."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "Utwórz nowy wolumin logiczny"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "Tylko utwórz"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Utwórz partycję"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Utwórz partycję na $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Utwórz tablicę partycji"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Utwórz migawkę"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Utwórz migawkę i zamontuj"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Tylko utwórz migawkę"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create storage volume"
+msgid "Create storage device"
+msgstr "Utwórz wolumin urządzeń do przechowywania danych"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Utwórz wolumin"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Utwórz cienki wolumin"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Utwórz licznik"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Utwórz grupę woluminów"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Utworzono"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Tworzenie grupy woluminów LVM2 $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "Tworzenie urządzenia RAID $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "Tworzenie urządzenia VDO"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Tworzenie systemu plików w $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Tworzenie woluminu logicznego $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Tworzenie partycji $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Tworzenie migawki $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Utworzenie tej sieci $0 zerwie połączenie z serwerem i uniemożliwi "
+"korzystanie z interfejsu administracji."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Krytyczne i powyżej"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Zasady kryptograficzne to składnik systemu konfigurujący główne podsystemy "
+"kryptograficzne, w tym protokoły TLS, IPSec, SSH, DNSSec i Kerberos."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "Zasady kryptograficzne"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "Zasady kryptograficzne są niespójne"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Obecne uruchomienie"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Obecnie najwyższe użycie procesora"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Obecnie używane"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Currently in use"
+msgid "Currently not supported"
+msgstr "Obecnie używane"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "niestandardowe"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "Niestandardowe zasady kryptograficzne"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Niestandardowe opcje montowania"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Niestandardowe porty"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom path"
+msgid "Custom type"
+msgstr "Niestandardowa ścieżka"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Niestandardowe strefy"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "„Domyślne” z dozwoloną weryfikacją podpisów SHA-1."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "Domeny wyszukiwania DNS"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "Domeny wyszukiwania DNS $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Codziennie"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Alarm niebezpieczeństwa:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Ciemny"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Dane"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Użyte dane"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Określenia dat powinny być w formacie RRRR-MM-DD gg:mm:ss. Dodatkowo "
+"obsługiwane są ciągi „yesterday” (wczoraj), „today” (dzisiaj) "
+"i „tomorrow” (jutro). „now” odnosi się do obecnego czasu. Można także podać "
+"czas względny za pomocą przedrostka „-” lub „+”."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Dezaktywuj"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Zmiana nazwy woluminu logicznego"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Dezaktywowanie"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Dezaktywowanie $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Debugowania i powyżej"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Zmniejsz o jedną jednostkę"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+#, fuzzy
+#| msgid "RAID 4 (dedicated parity)"
+msgid "Dedicated parity (RAID 4)"
+msgstr "RAID 4 (Dedykowana parzystość)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Deduplikacja"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Domyślne"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Opóźnienie"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Opóźnienie musi być liczbą"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Usuń"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Usuń $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Usuń konto"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Usuń pliki"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Usuń grupę"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Usuń"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Trwale usunąć zgłoszenie?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Usunięcie $0 zerwie połączenie z serwerem i uniemożliwi korzystanie "
+"z interfejsu administracji."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Usuwanie $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Usuwanie grupy woluminów LVM2 $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "Usunięcie puli Stratis usunie wszystkie zawarte w niej dane."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "Usunięcie systemu plików usunie wszystkie znajdujące się w nim dane."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr ""
+"Usunięcie woluminu logicznego usunie wszystkie znajdujące się na nim dane."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "Usunięcie partycji usunie wszystkie znajdujące się na niej dane."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting erases all data on a RAID device."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Usunięcie usuwa wszystkie dane na urządzeniu RAID."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "Usunięcie usuwa wszystkie dane na urządzeniu VDO."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "Usunięcie usuwa wszystkie dane w grupie woluminów."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting erases all data on a volume group."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "Usunięcie usuwa wszystkie dane w grupie woluminów."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "Usunięcie usunie te pliki:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Opis"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Komputer stacjonarny"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Odłączalny"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Szczegóły"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Rozwój"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Urządzenie"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Plik urządzenia"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Urządzenie jest tylko do odczytu"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Nazwa usługi"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Raporty diagnostyczne"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Katalog"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Katalog $0 nie jest zapisywalny lub nie istnieje."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Wyłącz wielowątkowość współbieżną"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Wyłącz zaporę sieciową"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Wyłącz tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Wyłączone"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Bez zezwolenia na interaktywne hasło"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Bez zezwolenia na uwierzytelnianie hasłem"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Bez zezwolenia na uruchamianie (zamaskowanie)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Rozłącz"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Rozłączono"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Wejście/wyjście dysku"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Dysk jest OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Na dysku występują błędy"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Hasło dysku"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Dyski"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Użycie dysków"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+#, fuzzy
+#| msgid "Dismiss $0 alert"
+#| msgid_plural "Dismiss $0 alerts"
+msgid "Dismiss"
+msgstr "Zamknij $0 alarm"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Zamknij $0 alarm"
+msgstr[1] "Zamknij $0 alarmy"
+msgstr[2] "Zamknij $0 alarmów"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Zamknij zaznaczone alarmy"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Język"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+#, fuzzy
+#| msgid "RAID 5 (distributed parity)"
+msgid "Distributed parity (RAID 5)"
+msgstr "RAID 5 (Rozproszona parzystość)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "Bez montowania"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Bez automatycznego montowania podczas uruchamiania"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Stacja dokująca"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Nie uruchamia się automatycznie"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "Bez montowania podczas uruchamiania"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Domena"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Domena"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Adres domeny"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Nazwa administratora domeny"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Hasło administratora domeny"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Nie można skontaktować się z domeną"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Domena jest nieobsługiwana"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Bez powtarzania"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+#, fuzzy
+#| msgid "RAID 6 (double distributed parity)"
+msgid "Double distributed parity (RAID 6)"
+msgstr "RAID 6 (Podwójna rozproszona parzystość)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Pobierz"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Pobierz nową przeglądarkę za darmo"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Pobrano"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Pobieranie"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Pobieranie $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Napęd"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Podwójny stopień"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Rozszerzona partycja"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Modyfikuj"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Modyfikuj /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Modyfikuj serwer kluczy Tang"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "Modyfikuj ustawienia VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "Modyfikuj VPN WireGuard"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "Modyfikuj ustawienia wiązania"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "Modyfikuj ustawienia mostka"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Modyfikuj niestandardową usługę w strefie $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Modyfikuj komputer"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Modyfikuj komputery"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Modyfikuj motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Punkt montowania"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Modyfikuj reguły i strefy"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Modyfikuj usługę"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Modyfikuj usługę $0"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "Modyfikuj ustawienia zespołu"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Modyfikuj użytkownika"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Modyfikacja klucza wymaga wolnego gniazda"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Wysuwanie $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "Komputer osadzony"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Puste"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Puste"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Puste hasło"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Opróżnianie $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Włącz"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Włącz lub wyłącz urządzenie"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Włącz usługę"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Włącz zaporę sieciową"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Włączone"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "Włączanie $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Zaszyfruj dane"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Zaszyfruj dane za pomocą serwera kluczy Tang"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "Zaszyfruj dane za pomocą hasła"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Zaszyfrowane"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Zaszyfrowane $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted Stratis pool $0"
+msgid "Encrypted Stratis pool"
+msgstr "Zaszyfrowana pula Stratis $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Zaszyfrowany wolumin logiczny $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Zaszyfrowana partycja $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Szyfrowanie"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Opcje szyfrowania"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Hasło szyfrowania"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Typ szyfrowania"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Zakończono"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "Punkt końcowy"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"Punkt końcowy działający jako „serwer” musi zostać podany jako komputer:"
+"port, w przeciwnym razie można zostawić puste."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Wymuszanie"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Dostępne są aktualizacje z ulepszeniami"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Proszę podać prawidłowy adres MAC"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Cała podsieć"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Wpis o $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Czyszczenie $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Poprawka"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Błąd"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Błędy i powyżej"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Wystąpił błąd"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "Błąd podczas instalowania $0: usługa PackageKit nie jest zainstalowana"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Komunikat o błędzie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr ""
+"Błąd podczas wykonywania polecenia semanage, aby wykryć modyfikacje systemu"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Błąd podczas zapisywania upoważnionych kluczy: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Błąd podczas ustawiania trybu SELinuksa: „$0”"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "MAC ethernetu"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "MTU ethernetu"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Przykład: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Przykład: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Doskonałe hasło"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Obudowa rozszerzenia"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Wygaś konto w dniu"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Wygaś konto w dniu $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Eksport"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Eksport do sieci"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Rozszerzone informacje"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Rozszerzona partycja"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS nie jest poprawnie włączone"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS z dalszymi ograniczeniami Common Criteria."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Niepowodzenie"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Dodanie komputera się nie powiodło: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Dodanie portu się nie powiodło"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Dodanie usługi się nie powiodło"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Dodanie strefy się nie powiodło"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Zmiana hasła się nie powiodła"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "Skonfigurowanie PCP się nie powiodło"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Usunięcie alarmu się nie powiodło: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Wyłączenie tuned się nie powiodło"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Wyłączenie profilu tuned się nie powiodło"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Modyfikacja komputera się nie powiodła: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Modyfikacja usługi się nie powiodła"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "Włączenie $0 w usłudze firewalld się nie powiodło"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Włączenie tuned się nie powiodło"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Pobranie dzienników się nie powiodło"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Wczytanie upoważnionych kluczy się nie powiodło."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Wczytanie jednostki się nie powiodło"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"Przetworzenie plików jednostek dla dnf-automatic.timer lub dnf-automatic-"
+"install.timer się nie powiodło. Proszę usunąć własne zastąpienia, aby "
+"skonfigurować automatyczne aktualizacje."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Ponowne uruchomienie usługi się nie powiodło"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Zapisanie zmian w /etc/motd się nie powiodło"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Zapisanie ustawień się nie powiodło"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Uruchomienie się nie powiodło"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Przełączenie profilu się nie powiodło"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Stan pliku"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "System plików"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "System plików jest zablokowany"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Nazwa systemu plików"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Tworzenie systemu plików w $target"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "Pod tym punktem montowania są już zamontowane systemy plików."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Filtrowanie według nazwy lub opisu"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Filtruj elementy menu"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Filtrowanie usług"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Filtry"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Odcisk"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Zapora sieciowa"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Zapora sieciowa jest niedostępna"
+
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Wersja oprogramowania sprzętowego"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "Napraw obsługę NBDE"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Rozmiar czcionki"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Zabronione uruchamianie"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Wymuś zmianę"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Wymuś usunięcie"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Wymuszenie zmiany hasła"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Sformatuj"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Sformatuj $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Sformatuj i zamontuj"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format and mount"
+msgid "Format and start"
+msgstr "Sformatuj i zamontuj"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Tylko sformatuj"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr ""
+"Sformatowanie usuwa wszystkie dane na urządzeniu do przechowywania danych."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Opóźnienie w przód $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Numer klatki"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Wolne miejsce"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Wyszukiwanie dowolnego tekstu"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "piątki"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "Od"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Imię i nazwisko"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Brama"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Ogólne"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Utworzone"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Przejdź do $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Przejdź do aplikacji"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Przejdź teraz"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Widoczność wykresu"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Menu opcji widoczności wykresu"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Grupa"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Nazwa grupy"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Grupy"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Powiększ"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Powiększ zawartość"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Powiększ rozmiar logiczny $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Powiększ wolumin logiczny"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "Powiększ partycję"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "Powiększ pulę do użycia całego miejsca"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Powiększ do użycia całego miejsca"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Tryb Hairpin"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Tryb Hairpin"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Przenośny"
+
+#: pkg/storaged/drive/drive.jsx:64
+#, fuzzy
+#| msgid "Hard Disk"
+msgid "Hard Disk Drive"
+msgstr "Dysk twardy"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Informacje o sprzęcie"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Stan"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Czas powitania $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Pomoc"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Hide confirmation password"
+msgstr "Potwierdź hasło"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "Use password"
+msgid "Hide password"
+msgstr "Użycie hasła"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "Identyfikator hierarchii"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "Wyższa interoperacyjność kosztem zwiększonej płaszczyźnie ataku."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Liczba pakietów w historii"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Katalog domowy"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Gospodarz"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Klucz komputera jest niepoprawny"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Nazwa komputera"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Komputery"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Co godzinę"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Godziny"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "Jak sprawdzić"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "Identyfikator"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "Identyfikator $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "Adres IP"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"Adres IP z przedrostkiem trasowania. Wiele wartości należy oddzielić "
+"przecinkiem. Przykład: 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "Adresy IPv4"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "Ustawienia IPv4"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "Ustawienia IPv6"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Identyfikator"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Jeśli zostanie pozostawione puste, to identyfikator zostanie utworzony na "
+"podstawie powiązanych usług portu i numerów portów"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Jeśli odcisk się zgadza, należy kliknąć „Przyjmij klucz i zaloguj się”. "
+"W przeciwnym przypadku nie należy się logować i należy skontaktować się "
+"z administratorem."
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Jeśli odcisk się zgadza, należy kliknąć „Zaufaj i dodaj komputer”. "
+"W przeciwnym przypadku nie należy się łączyć i należy skontaktować się "
+"z administratorem."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ignorowanie"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "Wejście"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "W terminalu należy wykonać: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Aby umożliwić w przyszłości logowanie na komputerze $0 jako użytkownik $1 "
+"bez hasła, należy użyć hasła logowania użytkownika $2 na komputerze $3 jako "
+"hasło klucza lub pozostawić hasło klucza puste."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "Zsynchronizowane"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Nieaktywne"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "Utwórz wolumin logiczny"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Dołączone usługi"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"Żądania przychodzące są domyślnie blokowane. Żądania wychodzące nie są "
+"blokowane."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Niespójny punkt montowania systemu plików"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Zwiększ o jedną jednostkę"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Pamięć indeksu"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Niebezpośrednie"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Informacje"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Informacje i powyżej"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Zainicjuj"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Zainicjuj dysk $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "Zainicjowanie usuwa wszystkie dane na dysku."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Inicjowanie…"
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Insights: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Zainstaluj"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Zainstaluj obsługę NFS"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Zainstaluj obsługę Stratis"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Zainstaluj wszystkie aktualizacje"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "Zainstaluj informacje o aplikacjach"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Zainstaluj cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Zainstaluj aktualizacje kpatch"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "Zainstaluj obsługę realmd"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Zainstaluj aktualizacje zabezpieczeń"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr ""
+"Należy zainstalować setroubleshoot-server, aby rozwiązywać problemy ze "
+"zdarzeniami SELinuksa."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Zainstaluj oprogramowanie"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Proszę zainstalować pakiet $0"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr "Instalacja bez zainstalowanego pakietu cockpit jest nieobsługiwana"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Zainstalowano"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Instalowanie"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Instalowanie $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "Zainstalowanie $0 usunęłoby $1."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Instalowanie pakietów"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Interfejs"
+msgstr[1] "Interfejsy"
+msgstr[2] "Interfejsy"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Elementy interfejsu"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Interfejsy"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Wewnętrzny błąd"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Wewnętrzny błąd: nieprawidłowy nagłówek wyzwania"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Nieprawidłowe"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Nieprawidłowy adres $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Nieprawidłowy format daty"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Nieprawidłowy format daty i nieprawidłowy format czasu"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Nieprawidłowa data wygaśnięcia"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Nieprawidłowe uprawnienia pliku"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Nieprawidłowy klucz"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Nieprawidłowe parametry $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Nieprawidłowa liczba dni"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Nieprawidłowy numer portu"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Nieprawidłowy przedrostek $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Nieprawidłowy przedrostek lub maska sieci $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Nieprawidłowy zakres"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Nieprawidłowy format czasu"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Nieprawidłowa strefa czasowa"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Nieprawidłowa nazwa użytkownika lub hasło"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "Brama IoT"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "Czy sshd jest uruchomione na innym porcie?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Zadania"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Dołącz"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Dołącz do domeny"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Dołącz do domeny"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Dołączanie"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Dołączenie do domeny wymaga instalacji usługi realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Dołączenie do tej domeny jest nieobsługiwane"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Dołącza do przestrzeni nazw"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Dziennik"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Wpis dziennika"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Nie odnaleziono wpisu dziennika"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Przejdź do"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit nie jest zainstalowany"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Przetestuj ustawienia kdump"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Utrzymywanie połączenia"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Zrzut awarii jądra"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Zrzut jądra"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "Łata jądra bez wyłączania $0 jest aktywna"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Łata jądra bez wyłączania $0 jest zainstalowana"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Ustawienia łat jądra bez wyłączania"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Łatanie jądra bez wyłączania"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Hasło klucza"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Nie można modyfikować gniazd na klucze o nieznanym typie w tym miejscu"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Źródło kluczy"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Klucze"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Serwer kluczy"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Adres serwera kluczy"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "Usunięcie serwera kluczy może uniemożliwić odblokowanie $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "Klucz LACP"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "„Przestarzałe” z interoperacyjnością z Active Directory."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "VDO pool"
+msgid "LVM2 VDO pool"
+msgstr "Pula VDO"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Wolumin logiczny"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Woluminy logiczne"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "Wolumin fizyczny LVM2"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "LVM2 physical volume"
+msgid "LVM2 physical volumes"
+msgstr "Wolumin fizyczny LVM2"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "Grupa woluminów LVM2"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "Grupa woluminów LVM2 $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Laptop"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Ostatni dzień"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Ostatni tydzień"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Ostatnia aktywność"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "Nie można usunąć ostatniego gniazda na klucz"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Ostatnio wyszukano: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "Nie można usunąć ostatniego gniazda na klucz"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Ostatnie logowanie"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Ostatnia modyfikacja: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Ostatnie udane logowanie:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Więcej informacji"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "Opuść $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Opuść domenę"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Pozostawienie pustego pola spowoduje pominięcie szyfrowania"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Na warunkach licencji GNU LGPL w wersji 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Jasny"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Ogranicz dostęp"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Ograniczony dostęp"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"Tryb ograniczonego dostępu ogranicza uprawnienia administracyjne. Część "
+"konsoli internetowej będzie miała zmniejszoną funkcjonalność."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Ograniczenia"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Opóźnienie łącza w dół"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Link-local"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Monitorowanie łącza"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Opóźnienie łącza w górę"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Obserwacja łącza"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Powiązane"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem $0"
+msgid "Linux filesystem data"
+msgstr "Odmontuj system plików $0"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Przestrzeń wymiany"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Nasłuchiwanie"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "Port nasłuchiwania"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "Port nasłuchiwania musi być liczbą"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Wyświetlanie jednostek"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Wyświetlenie jednostek się nie powiodło: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Obciążenie"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Równoważenie obciążenia"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Wczytaj wcześniejsze dane"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Wczytaj wcześniejsze wpisy"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Wczytanie dostępnych aktualizacji się nie powiodło"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Wczytywanie dostępnych aktualizacji, proszę czekać…"
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Wczytywanie wcześniejszych wpisów"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Wczytywanie kluczy…"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Wczytanie kluczy SSH się nie powiodło"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Wczytanie jednostek się nie powiodło"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Wczytywanie pakietów…"
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Wczytywanie modyfikacji systemu…"
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Wczytanie jednostki się nie powiodło"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Wczytywanie…"
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Lokalne konta"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Lokalny system plików"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Lokalny punkt montowania"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "No storage"
+msgid "Local storage"
+msgstr "Brak urządzeń do przechowywania danych"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "lokalnie w $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Położenie"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Zablokuj"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "Zablokuj „$0”"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Zablokuj konto"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgid "Locked"
+msgid "Locked data"
+msgstr "Zablokowano"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Blokowanie $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Zaloguj"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Zaloguj się na $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Logowanie za pomocą konta użytkownika serwera."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Komunikaty dziennika"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Wyloguj"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Wyloguj użytkownika"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Zalogowano"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Logiczny"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Wolumin logiczny (migawka)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Rozmiar logiczny"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Wolumin logiczny"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Wolumin logiczny (migawka)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Wolumin logiczny $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Logowanie"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Zaloguj ponownie"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Logowanie się nie powiodło"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Format logowania"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Historia logowania"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Lista historii logowania"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "Wyloguj użytkownika „$0”"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Pomyślnie wylogowano"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Dzienniki"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Komputer stacjonarny o mniejszym rozmiarze"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Lunch box"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "Urządzenie RAID"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "Urządzenie RAID $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Urządzenie jest tylko do odczytu"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "Usługa jest uruchomiona"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "Urządzenie RAID"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Dodaj dyski"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (zalecane)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU musi być liczbą dodatnią"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Identyfikator komputera"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Odciski kluczy SSH komputera"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Główna obudowa serwera"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Konserwacja"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+"Szkodliwe strony na zdalnym komputerze mogą mieć wpływ na pozostałe "
+"połączone komputery"
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "Zarządzaj rozmiarami systemów plików"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Zarządzanie urządzeniami do przechowywania danych"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Zarządzane interfejsy"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "Zarządzanie LVM"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "Zarządzanie punktami montowania NFS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "Zarządzanie RAID"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "Zarządzanie VDO"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "Zarządzanie VLAN"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Zarządzanie zaporą sieciową"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Zarządzanie wiązaniami sieciowymi"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Zarządzanie mostkami sieciowymi"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Zarządzanie zespołami sieciowymi"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Zarządzanie partycjami"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Zarządzanie napędami fizycznymi"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Zarządzanie usługami"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Zarządzanie aktualizacjami oprogramowania"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Zarządzanie kontami użytkowników"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Ręczne"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Ręcznie"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Oznaczanie $target jako wadliwe"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Zamaskuj usługę"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Zamaskowana"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Zamaskowanie usługi uniemożliwia uruchomienie wszystkich zależnych usług. "
+"Może to mieć większy efekt, niż się wydaje. Proszę potwierdzić, że ta "
+"jednostka ma zostać zamaskowana."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Maksymalny wiek komunikatu $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Napęd optyczny"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Pamięć"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Technologia pamięci"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Użycie pamięci"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Użycie pamięci/wymiany"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Połączone"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Wiadomość do zalogowanych użytkowników"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr ""
+"Być może w dzienniku znajdują się komunikaty związane z niepowodzeniem:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Użyte metadane"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Metoda"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Statystyki"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Statystyki i historia"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Nie można wczytać historii statystyk"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Ustawienia statystyk"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Mini tower"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Minuta musi być liczbą między 0 a 59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "Co minutę"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minuty"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Poprawki zmniejszające ryzyko"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Tryb"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Model"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Modyfikowanie $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "poniedziałki"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Odstęp monitorowania"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Cele monitorowania"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Co miesiąc"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Więcej informacji…"
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Zamontuj"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "Punkt montowania"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "Montowanie po włączeniu sieci, ignorowanie niepowodzenia"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Montowanie także automatycznie podczas uruchamiania"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Montowanie podczas uruchamiania"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Montowanie automatycznie na $0 podczas uruchamiania"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Montowanie przed uruchomieniem usług"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Konfiguracja montowania"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Zamontuj system plików"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Zamontuj teraz"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Zamontuj na $0 teraz"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Opcje montowania"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Punkt montowania"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Punkt montowania nie może być pusty"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Punkt montowania nie może być pusty."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Punkt montowania jest już używany dla $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Punkt montowania musi zaczynać się od „/”."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Montowanie tylko do odczytu"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Montowanie bez czekania, ignorowanie niepowodzenia"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Montowanie $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Montuje przed uruchomieniem usług"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Montuje równolegle z usługami"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr "Montuje równolegle z usługami, ale po włączeniu sieci"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Obudowa dla wielu komputerów"
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Multipathed devices"
+msgid "Multipathed devices"
+msgstr "Urządzenia wielościeżkowe"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr "Można podać wiele adresów rozdzielonych przecinkami lub spacjami."
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "Punkt montowania NFS"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "Ping NSNA"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "Serwer NTP"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Nazwa"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Nazwa nie może być pusta."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Nazwa nie może być pusta."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Nazwa nie może być dłuższa niż $0 B"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Nazwa nie może być dłuższa niż $0 znaków"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Nazwa nie może być dłuższa niż 127 znaków."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Nazwa nie może być dłuższa niż 127 znaków."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Nazwa nie może zawierać znaku „$0”."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "Nazwa nie może zawierać znaku „$0”."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Nazwa nie może zawierać spacji."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Wymagany jest co najmniej jeden serwer NTP"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Sieć"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Wejście/wyjście sieci"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Wiązanie sieciowe"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Urządzenia i wykresy sieciowe wymagają usługi NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Dzienniki sieci"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Użycie sieci"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "Usługa NetworkManager nie jest zainstalowana"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "Usługa NetworkManager nie jest uruchomiona"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Użycie sieci"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Sieć"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Nigdy"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Bez wygasania konta"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Bez wygasania hasła"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Nigdy nie zalogowano"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Nowy punkt montowania NFS"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Nowy komputer"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "Nowy komputer: $0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Nowe hasło klucza"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Nowa nazwa"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Nowe hasło"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Nowe hasło"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Nie przyjęto nowego hasła"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Dalej"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Nie"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "Nie podano identyfikatora"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Brak alarmów SELinuksa."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Brak zainstalowanych lub dostępnych aplikacji."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Brak dostępnych gniazd"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Brak dostępnych urządzeń blokowych."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "Nie odnaleziono żadnego urządzenia startowego"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Brak operatora"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Nie odnaleziono konfiguracji"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Brak dostępnych danych"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Brak dostępnych danych między $0 a $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Brak opóźnienia"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Brak dostępnego opisu"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Nie podano opisu."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "Nie odnaleziono żadnego urządzenia startowego"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Brak dostępnych dysków."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Nie odnaleziono dzienników"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "Brak wyników"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Bez szyfrowania"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Brak zdarzeń"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Brak systemu plików"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Brak systemów plików"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Brak wolnych gniazd na klucze"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Brak wolnego miejsca"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "Brak wolnego miejsca po tej partycji"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Nie podano nazwy grupy"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Nie odnaleziono kluczy komputera."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Nie odnaleziono pakietu instalacji dla tego programu."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Nie dodano kluczy"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "Brak pasujących języków"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Brak wpisów dziennika"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Brak woluminów logicznych"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Nie odnaleziono dzienników"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Brak wyników"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Nie włożono żadnego nośnika"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Brak partycjonowania"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "Brak partycjonowania"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "Nie dodano żadnych partnerów."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "Woluminy fizyczne"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Nie podano nazwy obszaru"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Brak wyników"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Brak wyników pasujących do filtru. Należy wyczyścić wszystkie filtry, aby "
+"wyświetlić wyniki."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Brak trafień"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No storage"
+msgid "No storage found"
+msgstr "Brak urządzeń do przechowywania danych"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Brak woluminów logicznych"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Nie ma takiego pliku lub katalogu"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Brak modyfikacji systemu"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Brak zgłoszeń systemowych."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Brak aktualizacji"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Nie podano nazwy użytkownika"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Brak"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Nieprawidłowy klucz prywatny"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Brak upoważnienia do wyłączenia zapory sieciowej"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Brak upoważnienia do włączenia zapory sieciowej"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Niedostępne"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Nie połączono"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Nie połączono z Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Nie połączono z komputerem"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space"
+msgid "Not enough free space"
+msgstr "Za mało miejsca"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "Za mało miejsca"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "Za mało miejsca do powiększenia."
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Nie odnaleziono"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Nie zainstalowano"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "Not permitted to configure realms"
+msgid "Not permitted to configure network devices"
+msgstr "Brak zezwolenia na konfigurowanie obszarów"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Brak zezwolenia na konfigurowanie obszarów"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Brak uprawnień do wykonania tego działania."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Niegotowe"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Nie zarejestrowano"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Niedziałające"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Nie ustawiono"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Niesynchronizowane"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Uwaga"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Notebook"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Uwagi i powyżej"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Zaciemnienie adresów sieciowych, nazw komputerów i nazw użytkowników"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Zaciemnione"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Wystąpiło $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Wystąpiło między $0 a $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Wystąpienia"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "OK"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Poprzednie hasło"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Poprzednie hasło"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Nie przyjęto poprzedniego hasła"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "Na zamontowanym urządzeniu"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Podczas niepowodzenia"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Po zainstalowaniu programu Cockpit należy go włączyć za pomocą polecenia "
+"„systemctl enable --now cockpit.socket”."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Tylko $0 z $1 jest używane."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Dozwolone są tylko litery, liczby i znaki „:”, „_”, „.”, „@”, „-”"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Tylko awaryjne"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"Użycie tylko zatwierdzonych i dozwolonych algorytmów podczas uruchamiania "
+"w trybie FIPS."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Ups!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr ""
+"Należy otworzyć usługę pmproxy w zaporze sieciowej, aby udostępniać "
+"statystyki."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Działanie „$operation” na $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Opcje"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Lub użyj dołączonej przeglądarki"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Inne"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"Inne metody uwierzytelniania są nadal dostępne, nawet kiedy uwierzytelnianie "
+"interaktywnym hasłem nie jest dozwolone."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Inne opcje"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Wyjście"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Przegląd"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Zastąp"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Zastąp istniejące dane zerami (wolniejsze)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Brak pakietu „cockpit-pcp” dla historii statystyk"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Informacje o pakiecie"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "Usługa PackageKit uległa awarii"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "Usługa PackageKit nie jest zainstalowana"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "Usługa PackageKit zgłosiła kod błędu $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Pakiety"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Nazwa strony"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Nadrzędne"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Nadrzędne $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Część"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Część $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Partycja"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Partycja $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "Rozmiar partycji to $0. Rozmiar zawartości to $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Partycjonowanie"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Partycje"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Pasywnie"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Hasło"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "Hasło nie może być puste"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Hasło nie może być puste"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Hasło z jakiegoś innego gniazda na klucze"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Usunięcie hasła może uniemożliwić odblokowanie $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Hasła się nie zgadzają"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Hasło"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Pomyślnie zmieniono hasło"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Wygasanie hasła"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "Hasło jest dłuższe niż 256 znaków"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Hasło jest do przyjęcia"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Hasło jest za słabe"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Należy zmienić hasło"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Nie przyjęto hasła"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Wskazówka na temat hasła"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Wklej"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Błąd wklejania"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "Wklej istniejący klucz"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Proszę tutaj wkleić zawartość pliku publicznego klucza SSH"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Ścieżka"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Koszt ścieżki"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Koszt ścieżki $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Ścieżka na serwerze"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Ścieżka na serwerze nie może być pusta."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Ścieżka na serwerze musi zaczynać się od „/”."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Ścieżka do katalogu"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Ścieżka do pliku"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Ścieżki"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Wstrzymaj"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+"$0. partner ma nieprawidłowy port punktu końcowego. Port musi być liczbą."
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"$0. partner ma nieprawidłowy punkt końcowy. Musi zostać podany jako komputer:"
+"port, np. 1.2.3.4:51820 lub example.com:51820"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "Partnerzy"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"Partnerzy to inne komputery łączące się z tym. Klucze publiczne z innych "
+"komputerów będą udostępniane między nimi."
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot zbiera i analizuje statystyki wydajności komputera."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Profil wydajności"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Obudowa peryferyjna"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Trwałe"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "Trwale usunąć grupę $0?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "Trwale usunąć $0?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Trwale usunąć $0?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete subvolume $0?"
+msgstr "Trwale usunąć $0?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Odmowa dostępu"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Zezwalanie"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Fizyczny"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "Physical Volumes"
+msgstr "Woluminy fizyczne"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Woluminy fizyczne"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "Nie można tutaj zmieniać rozmiaru woluminów fizycznych."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Wybierz datę"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Przypnij jednostkę"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Odstęp między pingami"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Cel pingu"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Przypięta jednostka"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Pizza box"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Proszę się uwierzytelnić, aby uzyskać dostęp administracyjny"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr ""
+"Proszę włączyć obsługę języka JavaScript, aby używać konsoli internetowej."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Proszę zainstalować pakiet $0"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Proszę rozwiązać problem i odświeżyć tę stronę."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Proszę podać datę wygaśnięcia"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Proszę podać komputer, z którym się połączyć"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "Proszę je najpierw odmontować."
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Pula dla cienkich woluminów logicznych"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Pula dla cienko nadzorowanych woluminów"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Pula dla cienko nadzorowanych woluminów"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "Hasło puli"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Port"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Przenośne"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Porty"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Utwórz partycję"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Długość przedrostka"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Długość przedrostka lub maska sieci"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Przygotowywanie"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Obecne"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Zachowanie"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Czytelna nazwa komputera"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Poprzednie uruchomienie"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Główne"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Priorytet"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Priorytet $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "Klucz prywatny"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "Problem podczas zostawania administratorem"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Informacje o problemie"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Informacje o problemie"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Procesy używające położenia"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Postęp: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Strona projektu"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Pytanie przez passwd przekroczyło czas oczekiwania"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Pytanie przez ssh-add przekroczyło czas oczekiwania"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Pytanie przez ssh-keygen przekroczyło czas oczekiwania"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Wysyła ponowne wczytanie do"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Chroni przed atakami spodziewanymi w niedalekiej przyszłości kosztem "
+"interoperacyjności."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "Proszę podać hasło puli na tych urządzeniach blokowych:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Klucz publiczny"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+"Klucz publiczny zostanie utworzony po podaniu prawidłowego klucza prywatnego"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Zastosowanie"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (Striping)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (Lustrzany)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RID 10 (Striping lustrzany)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (Dedykowana parzystość)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (Rozproszona parzystość)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (Podwójna rozproszona parzystość)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "Obudowa RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "Poziom macierzy RAID"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Obudowa do montowania w szafie"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Losowo"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Zakres"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "Zakres musi być w ścisłej kolejności"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Stopień"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Surowe do urządzenia"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Odczyt"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Więcej informacji…"
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Tylko do odczytu"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Odczytywanie"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Odczytywanie…"
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Gotowe"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Gotowe"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Prawdziwa nazwa komputera"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Prawdziwa nazwa komputera może zawierać tylko małe litery, cyfry, myślniki "
+"i kropki (z zaludnionymi poddomenami)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Prawdziwa nazwa komputera może mieć co najwyżej 64 znaki"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Zastosuj jeszcze raz i uruchom ponownie"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Uruchom ponownie"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Ponowne uruchomienie po ukończeniu"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Zalecane jest ponowne uruchomienie"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Ponownie uruchom komputer…"
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Odbieranie"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Ostatnie komputery"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Zalecane, bezpieczne ustawienia wobec obecnych modeli zagrożeń."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Połącz ponownie"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Odzyskiwanie"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "Przywracanie urządzenia RAID $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Odświeżanie informacji o pakietach"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Odmowa połączenia. Komputer jest nieznany"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Odmowa połączenia. Klucze komputera się nie zgadzają"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Odmowa połączenia. Klucz komputera jest nieznany"
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "Utwórz ponownie"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "Ponowne tworzenie obrazu initrd"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Zarejestruj…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "Powiązane procesy i usługi zostaną siłowo zatrzymane."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "Powiązane procesy zostaną siłowo zatrzymane."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "Powiązane usługi zostaną siłowo zatrzymane."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Wczytaj ponownie"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Odebrano ponowne wczytanie z"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Wczytywanie ponownie"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Ponowne wczytywanie stanu pozostałych usług"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "Zdalnie przez CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "Zdalnie przez FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Zdalnie przez NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "Zdalnie przez NFS"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "Zdalnie przez SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Zdalnie przez SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "Zdalnie przez SSH"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Usuwane:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Usuń"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Usuń $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Usuń usługę $0 ze strefy $1"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Usunąć $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Usunąć serwer kluczy Tang?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Usuń urządzenie"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Usuń komputer"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Usuń element"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Remove missing physical volumes?"
+msgstr "Usuwanie woluminu fizycznego z $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "Usunąć hasło w gnieździe na klucze $0?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "Usunąć hasło?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Usuń usługę $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Usuń strefę $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Usuwanie"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Usuwanie $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Usunięcie $0 zerwie połączenie z serwerem i uniemożliwi korzystanie "
+"z interfejsu administracji."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "Usuwanie $target z urządzenia RAID"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"Usunięcie hasła bez potwierdzenia innym hasłem może uniemożliwić "
+"odblokowywanie lub zarządzanie kluczami, jeśli inne hasła zostaną zapomniane "
+"lub utracone."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Usuwanie woluminu fizycznego z $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Usunięcie usługi Cockpit może spowodować niedostępność konsoli internetowej. "
+"Proszę się upewnić, że ta strefa nie ma zastosowania do obecnego połączenia "
+"konsoli internetowej."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "Usunięcie strefy usunie wszystkie zawarte w niej usługi."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Zmień nazwę"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Zmień nazwę puli Stratis"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Zmień nazwę systemu plików"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Zmień nazwę grupy"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "Zmień nazwę grupy $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Zmiana nazwy woluminu logicznego"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Zmień nazwę grupy woluminów"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Zmienianie nazwy $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "Zmiana nazwy grupy może wpłynąć na sudo i podobne reguły"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Repair logical volume $0"
+msgstr "Zmiana nazwy woluminu logicznego"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Naprawianie $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Powtarzanie"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Powtarzanie co miesiąc"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Powtórzenie hasła"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Powtarzanie co tydzień"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Zgłoś"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Etykieta zgłoszenia"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Zgłoś do ABRT Analytics"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Zgłoszono, brak dostępnych odnośników"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Zgłoszenie się nie powiodło"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Zgłaszanie zostało anulowane"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Zgłoszenia"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Zgłoszenia:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Wymaganie zmiany hasła co $0 dni"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Wymaganie zmiany hasła w dniu $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Wymaganie zmiany hasła po pierwszym zalogowaniu"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Wymagane przez"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Wymagane przez "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Wymaga"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Modyfikacja wymaga dostępu administratora"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Potrzebne"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Potrzebne dla"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Zastrzeżona pamięć"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Przywróć"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Przywróć hasło"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Zmienianie rozmiaru $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Zmiana rozmiaru zaszyfrowanego systemu plików wymaga odblokowania dysku. "
+"Proszę podać obecne hasło dysku."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Uruchom ponownie"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Ponownie uruchom usługi"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Ponownie uruchom usługi…"
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Ponowne uruchamianie"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Przywracanie połączenia"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"Wyniki awarii zostaną skopiowane przez $0 do $1 jako $2, jeśli kdump jest "
+"właściwie skonfigurowane."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+"Wyniki awarii będą przechowywane w $0 jako $1, jeśli kdump jest właściwie "
+"skonfigurowane."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Wznów"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Ponowne użycie istniejącego szyfrowania"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Ponowne użycie istniejącego szyfrowania ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "Przejrzyj zasady kryptograficzne"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Przeglądanie dzienników"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Round robin"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Trasy"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Uruchom o"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Wykonaj nowe zgłoszenie"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Uruchom w dniu"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Wykonaj zgłoszenie"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+"Należy wykonać to polecenie przez zaufaną sieć lub fizycznie na zdalnym "
+"komputerze:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Uruchamianie"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Działające"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Czas działania"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "Błędy kontroli dostępu SELinuksa"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux jest wyłączony w systemie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux jest wyłączony w systemie."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "Polityka SELinuksa"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "Stan SELinuksa systemu jest nieznany."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "Rozwiązywanie problemów z SELinuksem"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "Test SMART $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"Zgłaszanie SOS zbiera informacje o systemie, aby pomóc diagnozować problemy."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "Klucz SSH"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "Klucz SSH nie jest ścieżką"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "Klucze SSH"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "Opóźnienie w przód STP"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "Czas powitania STP"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "Maksymalny wiek komunikatu STP"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "Priorytet STP"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Użytkownicy przeglądarki Safari muszą zaimportować certyfikat "
+"samopodpisującego CA i oznaczyć go jako zaufany:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "soboty"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Zapisz"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Zapisz i uruchom ponownie"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Zapisz zmiany"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Oszczędź miejsce kompresując poszczególne bloki za pomocą L4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr ""
+"Oszczędź miejsce przez przechowywanie identycznych bloków danych tylko raz"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Zapisanie nowego hasła wymaga odblokowania dysku. Proszę podać obecne hasło "
+"dysku."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Zaplanowane wyłączenie komputera: $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Zaplanowane ponowne uruchomienie: $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "Sealed-case PC"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Szukaj"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Domena wyszukiwania"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Wyszukiwanie nazwy lub identyfikatora"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Wyszukiwanie nazwy, grupy lub identyfikatora"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "Sekunda musi być liczbą między 0 a 59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Sekundy"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Klucze SSH"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Bezpieczne usuwanie zawartości $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Konfiguracja i rozwiązywanie błędów z SELinuksem"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Dostępne są aktualizacje zabezpieczeń"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Tylko aktualizacje zabezpieczeń"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Aktualizacje zabezpieczeń zostaną zastosowane $0 o $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Wybierz"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Wybierz identyfikator"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Wybierz metodę"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Wyślij"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Wysyłanie"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Numer seryjny"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Serwer"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Adres serwera"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Adres serwera nie może być pusty."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Serwer nie może być pusty."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Serwer zamknął połączenie."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Oprogramowanie serwera"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Usługa"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Usługa ma błąd"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Dzienniki serwera"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Usługi"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Usługi używające położenia"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Sesja"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Sesja zaraz wygaśnie"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Ustaw"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Ustaw nazwę komputera"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Utwórz partycję na $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Ustaw hasło"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Ustaw czas"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Ustaw na"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Ustaw"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Ustaw słabe hasło"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Ustawienie odbiega od skonfigurowanego stanu i zostanie przywrócone po "
+"następnym uruchomieniu."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Ustawianie"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Ustawianie urządzenia zwrotnego $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Ustawienia"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Ważność"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Współdzielone"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Powłoka"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Skrypt powłoki"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "Wyświetl wszystkie wątki"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Wyświetl wszystkie wątki"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Show confirmation password"
+msgstr "Potwierdź hasło"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Wyświetl odciski"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Wyświetla komunikaty zawierające podany ciąg."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Wyświetla komunikaty dla podanej jednostki systemd."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Wyświetla komunikaty z podanego uruchomienia."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Więcej związków"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "New password"
+msgid "Show password"
+msgstr "Nowe hasło"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Związki"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Zmniejsz"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Zmniejsz wolumin logiczny"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "Zmniejsz partycję"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Zmniejsz wolumin"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Wyłącz"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Wyłącz"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Od"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Pojedynczy stopień"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Rozmiar"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "Rozmiar nie może być ujemny"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "Rozmiar nie może wynosić zero"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Rozmiar jest za duży"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Rozmiar musi być liczbą"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Rozmiar musi wynosić co najmniej $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Pomiń główną nawigację"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Przejdź do treści"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Gniazdo"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Gniazdo $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Migawka"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Gniazda"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Aktualizacje oprogramowania"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Programowe obejścia błędów pomagają uniknąć problemów bezpieczeństwa "
+"procesora. Ich efektem ubocznym jest zmniejszenie wydajności. Należy "
+"zmieniać te ustawienia na własne ryzyko."
+
+#: pkg/storaged/drive/drive.jsx:63
+#, fuzzy
+#| msgid "Solid-State Disk"
+msgid "Solid State Drive"
+msgstr "Dysk SSD"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Pomyślnie zastosowano rozwiązanie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Rozwiązanie się nie powiodło"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Rozwiązania"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"Część urządzeń blokowych tej puli powiększyło się po utworzeniu puli. Można "
+"bezpiecznie powiększyć pulę, aby wykorzystać nowo dostępne miejsce."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr "Inny program obecnie używa menedżera pakietów, proszę czekać…"
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Część oprogramowania musi zostać ręcznie uruchomiona ponownie"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Uporządkowane od najmniej do najbardziej zaufanych"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Komputer oszczędzający miejsce"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Protokół STP"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Protokół STP"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Zapasowe"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Podany czas"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Prędkość"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Stabilna"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Rozpocznij"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Uruchom i włącz"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Uruchom urządzenie wielościeżkowe"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Uruchom usługę"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr "Wyświetlanie wpisów z podanego dnia lub nowszych."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "Wyświetlanie wpisów z podanego dnia lub starszych."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Rozpoczęto"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "Uruchamianie urządzenia RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Uruchamianie przestrzeni wymiany $target"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Stan"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Statyczne"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Stan"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Stick PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Lepkie"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Zatrzymaj"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Zatrzymaj i wyłącz"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Zatrzymaj i usuń"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Zatrzymaj i odmontuj"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Zatrzymaj urządzenie"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Zatrzymaj modyfikowanie komputerów"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Zatrzymaj zgłoszenie"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "Zatrzymywanie urządzenia RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Zatrzymywanie przestrzeni wymiany $target"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Przechowywanie danych"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr ""
+"Urządzenia do przechowywania danych nie mogą być zarządzane na tym systemie."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Dzienniki urządzeń do przechowywania danych"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Przechowaj hasło"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Zachowane hasło"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "Urządzenie blokowe $0"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "Add block devices"
+msgid "Stratis block devices"
+msgstr "Dodaj urządzenia blokowe"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Urządzenia blokowe Stratis nie mogą być zmniejszane"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystem"
+msgstr "Utwórz system plików"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems"
+msgstr "Utwórz system plików"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Utwórz system plików"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Pula Stratis"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:97
+#, fuzzy
+#| msgid "Set password"
+msgid "Strong password"
+msgstr "Ustaw hasło"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Pozostałość"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Styl"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Obudowa podrzędna"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Sub-Notebook"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "Subskrypcja sygnałów systemd się nie powiodła: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "Utworzenie woluminu się nie powiodło"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Pomyślnie skopiowano do schowka"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Pomyślnie skopiowano do schowka."
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "niedziele"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Partycja wymiany"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "Nie można tutaj zmieniać rozmiaru systemów plików $0."
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Wyjście przestrzeni wymiany"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Wyłącz $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Wyłącz $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Włącz $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Przełącz na dostęp administracyjny"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Przełącz na ograniczony dostęp"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Wyłączenie $0 zerwie połączenie z serwerem i uniemożliwi korzystanie "
+"z interfejsu administracji."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Włączenie $0 zerwie połączenie z serwerem i uniemożliwi korzystanie "
+"z interfejsu administracji."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Synchronizowane"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Synchronizowane z $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Synchronizowanie"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "Synchronizowanie urządzenia RAID $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "System"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Diagnostyka systemu"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Informacje o systemie"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "System jest aktualny"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Modyfikacje systemu"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Czas systemowy"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Jednostki systemd"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tablet"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Serwer kluczy Tang"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Cel"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Cele"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Zespół"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Port zespołu"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Ustawienia portu zespołu"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminal"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Zakończ sesję"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Przetestuj konfigurację"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Test jest dostępny tylko, jeśli usługa kdump jest uruchomiona."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Przetestuj ustawienia kdump"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Testuje ustawienia kdump przez zawieszenie jądra. Może to chwilę zająć, "
+"a komputer może nie zostać automatycznie włączony ponownie. Nie należy "
+"celowo zawieszać komputera w czasie wykonywania ważnych zadań."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Testowanie połączenia"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "Pakiet $0 nie jest dostępny w żadnym repozytorium."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "Pakiet $0 musi być zainstalowany, aby tworzyć pule Stratis."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "Pakiet $0 musi być zainstalowany."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "Pakiet $0 zostanie zainstalowany, aby utworzyć urządzenia VDO."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "Adres IP lub nazwa komputera nie może zawierać spacji."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "Macierz RAID jest w stanie zdegradowanym"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr "Urządzenie RAID musi być uruchomione, aby usunąć dyski."
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+"Klucz SSH $0 użytkownika $1 na komputerze $2 zostanie dodany do pliku $3 "
+"użytkownika $4 na komputerze $5."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"Klucz SSH $0 stanie się dostępny przez pozostały czas sesji i będzie "
+"dostępny do logowania także na innych komputerach."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"Klucz SSH do logowania w $0 jest chroniony hasłem, ale komputer nie zezwala "
+"na logowanie za pomocą hasła. Proszę podać hasło klucza w $1."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"Klucz SSH do logowania w $0 jest chroniony. Można zalogować się za pomocą "
+"hasła logowania lub podając hasło klucza w $1."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"Konto „$0” będzie zmuszone do zmiany hasła podczas następnego logowania"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Usługa Cockpit jest automatycznie dołączana"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"Nieznany stan konfiguracji, może zostać zmieniony po następnych uruchomieniu."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"Utworzenie tego urządzenia VDO nie zostało ukończone, więc nie można go "
+"używać."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"Obecnie zalogowany użytkownik nie ma zezwolenia na wyświetlanie informacji "
+"o kluczach."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"Dysk musi zostać odblokowany przed formatowaniem. Proszę podać istniejące "
+"hasło."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "Plik $0 zostanie usunięty."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "System plików nie ma trwałego punktu montowania."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "System plików nie ma trwałego punktu montowania."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"System plików jest skonfigurowany do automatycznego montowania podczas "
+"uruchamiania, ale jego kontener szyfrowania nie będzie wtedy odblokowywany."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"System plików jest obecnie zamontowany, ale nie będzie zamontowany po "
+"następnym uruchomieniu."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"System plików jest obecnie zamontowany na $0, ale będzie zamontowany na $1 "
+"podczas następnego uruchomienia."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"System plików jest obecnie zamontowany na $0, ale nie będzie zamontowany po "
+"następnym uruchomieniu."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"System plików nie jest obecnie zamontowany, ale będzie zamontowany podczas "
+"następnego uruchomienia."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "System plików nie jest zamontowany."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"System plików zostanie odblokowany i zamontowany podczas następnego "
+"uruchomienia. Może to wymagać wpisania hasła."
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "Odcisk powinien się zgadzać:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Ta usługa zostanie ponownie uruchomiona:"
+msgstr[1] "Te usługi zostaną ponownie uruchomione:"
+msgstr[2] "Te usługi zostaną ponownie uruchomione:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "Imię i nazwisko nie może zawierać dwukropków."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "Identyfikator grupy musi być dodatnią liczbą całkowitą"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Nazwa grupy może składać się tylko z liter od A do Z, cyfr, kropek, "
+"myślników i podkreślników"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"Katalog domowy $0 już istnieje. Jego właściciel zostanie zmieniony na nowego "
+"użytkownika."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "Obraz initrd musi zostać ponownie utworzony."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Hasło klucza nie może być puste"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Hasła klucza się nie zgadzają"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "Podany klucz jest nieprawidłowy."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Nie można usunąć ostatniego gniazda na klucz"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "Wymienione procesy i usługi zostaną siłowo zatrzymane."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "Wymienione procesy zostaną siłowo zatrzymane."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "Wymienione usługi zostaną siłowo zatrzymane."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr ""
+"Zalogowany użytkownik nie ma zezwolenia na wyświetlanie modyfikacji systemu"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "Komputer jest ponownie uruchamiany"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "Punkt montowania $0 jest używany przez te procesy:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "Punkt montowania $0 jest używany przez te usługi:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Nowe hasło klucza nie może być puste"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Hasło nie może być puste"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Hasła się nie zgadzają"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Hasła się nie zgadzają."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"Powstały odcisk można udostępniać publicznie, na przykład przez e-mail."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"Powstały odcisk można udostępniać publicznie, na przykład przez e-mail. "
+"Jeśli weryfikacja jest wykonywana za użytkownika przez kogoś innego, to ta "
+"osoba może wysłać wyniki na dowolny sposób."
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Serwer odmówił uwierzytelnienia „$0” za pomocą hasła, a żadne inne "
+"obsługiwane metody uwierzytelniania nie są dostępne."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr ""
+"Serwer odmówił uwierzytelnienia za pomocą wszystkich obsługiwanych metod."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"Komputer obecnie nie obsługuje odblokowywania systemu plików za pomocą "
+"serwera kluczy Tang podczas uruchamiania."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"Komputer obecnie nie obsługuje odblokowywania głównego systemu plików za "
+"pomocą serwera kluczy Tang."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+"Użytkownik $0 nie ma zezwolenia na zmianę poprawek zabezpieczeń procesora "
+"zmniejszających ryzyko"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "Użytkownik $0 nie ma zezwolenia na zmianę zasad kryptograficznych"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "Użytkownik $0 nie ma zezwolenia na testowe zawieszanie jądra"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"Użytkownik musi zostać wylogowany i zalogowany z powrotem, aby nowa "
+"konfiguracja została uwzględniona."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Nazwa użytkownika może składać się tylko z liter od A do Z, cyfr, kropek, "
+"myślników i podkreślników."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"Konfiguracja przeglądarki uniemożliwia działanie programu Cockpit ($0 jest "
+"niedostępne)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Obecnie nie ma aktywnych stron"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"W systemie obecne są urządzenia z wieloma ścieżkami, ale usługa urządzeń "
+"wielościeżkowych nie jest uruchomiona."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "Brak aktywnych usług w tej strefie"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Brak upoważnionych publicznych kluczy dla tego konta."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"Za mało miejsca w puli, aby wykonać migawkę tego systemu plików. Wymagane "
+"jest co najmniej $0, ale dostępne jest tylko $1."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Wystąpił nieoczekiwany błąd podczas łączenia z maszyną."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Potrzebne jest wykonanie tych dodatkowych kroków:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Te zmiany zostaną wprowadzone:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Cienki wolumin logiczny"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Pula dla cienko nadzorowanych woluminów"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+#, fuzzy
+#| msgid ""
+#| "This RAID array has no write-intent bitmap. Such a bitmap can reduce "
+#| "sychronization times significantly."
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"Ta macierz RAID nie ma mapy bitowej zamiaru zapisu. Taka mapa bitowa może "
+"znacznie zmniejszyć czasy synchronizacji."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"Ten punkt montowania NFS jest używany. Można zmieniać tylko jego opcje."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "To urządzenie VDO nie używa całości swojego urządzenia podstawowego."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Tutaj nie można zarządzać tym urządzeniem."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Tutaj nie można zarządzać tym urządzeniem."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "To urządzenie jest obecnie używane."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "To pole nie może być puste"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Ta grupa jest główną grupą dla tych użytkowników:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr ""
+"Ten komputer zostanie ponownie uruchomiony po zainstalowaniu aktualizacji."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "Ta informacja jest przechowywana tylko na komputerze."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Ten serwer kluczy jest jedynym sposobem na odblokowanie puli i nie może "
+"zostać usunięty."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr ""
+"Ten wolumin logiczny nie jest całkowicie używany przez swoją zawartość."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Ten komputer został już dodany."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Może to chwilę zająć"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "Ta partycja nie jest całkowicie używana przez swoją zawartość."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+"To hasło jest jedynym sposobem na odblokowanie puli i nie może zostać "
+"usunięte."
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr "Ta pula nie używa całego miejsca na swoich urządzeniach blokowych."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "Ta pula jest w stanie zdegradowanym."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Ten system nie jest zarejestrowany"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Ten system używa niestandardowego profilu"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Ten system używa zalecanego profilu"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"To narzędzie konfiguruje zasady SELinuksa i może pomóc w zrozumieniu "
+"i rozwiązywaniu naruszeń zasad."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"To narzędzie konfiguruje komputer do zapisywania zrzutów awarii jądra na "
+"dysku."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"To narzędzie tworzy archiwum konfiguracji i informacji diagnostycznych "
+"z działającego komputera. Archiwum może być przechowywane lokalnie lub "
+"centralnie do celów nagrywania i śledzenia, albo może być wysyłane do "
+"przedstawicieli pomocy technicznej, programistów lub administratorów "
+"komputera w celu wspomagania znajdowania źródła problemu i debugowania."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"To narzędzie zarządza lokalnymi urządzeniami do przechowywania danych, "
+"takimi jak systemy plików, grupy woluminów LVM2 czy punkty montowania NFS."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"To narzędzie zarządza sieciami, takimi jak wiązania, mostki, zespoły, VLAN "
+"i zapory sieciowe za pomocą usług NetworkManager i firewalld. Usługa "
+"NetworkManager jest niezgodna z domyślnymi skryptami systemd-networkd "
+"systemu Ubuntu i ifupdown systemu Debian."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Ta jednostka nie została zaprojektowana do bezpośredniego włączania."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Ta nazwa użytkownika już istnieje"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr ""
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Ta przeglądarka jest za stara, aby uruchomić konsolę internetową (brak $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Spowoduje to dodanie dopasowania do „_BOOT_ID=”. Jeśli nie zostanie podane, "
+"to zostaną wyświetlone dzienniki dla obecnego uruchomienia. Jeśli "
+"identyfikator uruchomienia zostanie pominięty, to dodatnie wyrównanie "
+"wyszuka uruchomienia zaczynając od początku zapisów, a równe lub mniejsze "
+"niż zero wyrównanie wyszuka uruchomienia zaczynając od końca zapisów. 1 "
+"oznacza więc pierwsze uruchomienie odnalezione w zapisach w kolejności "
+"chronologicznej, 2 drugie i tak dalej, zaś -0 to ostatnie uruchomienie, -1 "
+"przedostatnie uruchomienie i tak dalej."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Spowoduje to dodanie dopasowania do „_SYSTEMD_UNIT=”, „COREDUMP_UNIT=” "
+"i „UNIT=”, aby znaleźć wszystkie możliwe komunikaty dla podanej jednostki. "
+"Może zawierać więcej jednostek rozdzielonych przecinkami. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Umożliwi to logowanie bez hasła w przyszłości."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Ta strefa zawiera usługę Cockpit. Proszę się upewnić, że ta strefa nie ma "
+"zastosowania do obecnego połączenia konsoli internetowej."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "czwartki"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Warstwa"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Czas"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Strefa czasowa"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Utworzenie licznika się nie powiodło"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "Usunięcie licznika się nie powiodło"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Liczniki"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Wskazówka: ustawienie hasła klucza na takie samo, jak hasło logowania "
+"automatycznie uwierzytelni w innych systemach."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Aby upewnić się, że połączenie nie jest przechwytywane przez szkodliwą "
+"stronę trzecią, proszę zweryfikować odcisk klucza komputera:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Aby otrzymywać aktualizacje oprogramowania, ten system musi zostać "
+"zarejestrowany w firmie Red Hat za pomocą serwisu Red Hat Customer Portal "
+"lub lokalnego serwera subskrypcji."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Aby zweryfikować odcisk klucza, należy wykonać poniższe polecenie na $0 "
+"fizycznie siedząc przy komputerze lub przez zaufaną sieć:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Dzisiaj"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Przełącz"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Przełącz wybór daty"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Przełącz filtry"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Za dużo danych"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Narzędzia"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Top 5 usług obciążających procesor"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "Top 5 usług używających dysku"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "Top 5 usług obciążających pamięć"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Całkowity rozmiar: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Tower"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Przejściowe"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Przesyłanie"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Wyzwalacz"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Wyzwalane przez"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Wyzwalacze"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Rozwiązywanie problemów"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Rozwiąż problem…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "Zaufaj i dodaj komputer"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Zaufaj kluczowi"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Poziom zaufania"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Spróbuj ponownie"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Próbowanie synchronizacji z $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "wtorki"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Uruchomienie tuned się nie powiodło"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned to usługa monitorująca komputer i optymalizująca wydajność w pewnych "
+"zadaniach. Rdzeniem Tuned są profile, które dostosowują komputer do różnych "
+"zastosowań."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "tuned jest niedostępne"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "tuned nie jest uruchomione"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "tuned jest wyłączone"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Włącz dostęp administracyjny"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Typ"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Nazwa nie może zawierać znaku „$0”."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Filtrowanie"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Nie można automatycznie zastosować tego rozwiązania"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Nie można połączyć z tym adresem"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "Nie można skontaktować się z $0."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Nie można skontaktować się z podanym komputerem $0. Proszę się upewnić, że "
+"ma on uruchomioną usługę ssh na porcie $1 lub podać inny port w adresie."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Nie można pobrać informacji o alarmie."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Nie można zalogować się w $0 za pomocą uwierzytelniania kluczem SSH. Proszę "
+"podać hasło. Można później skonfigurować klucze SSH do automatycznego "
+"logowania."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Nie można zalogować się w $0. Komputer nie przyjmuje logowania hasłem ani "
+"żadnego z kluczy SSH użytkownika."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Nie można połączyć się z serwerem"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Nie można usunąć punktu montowania"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+#, fuzzy
+#| msgid "Encrypted logical volume of $0"
+msgid "Unable to repair logical volume $0"
+msgstr "Zaszyfrowany wolumin logiczny $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "Nie można wykonać poprawki: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "Nie można zapisać ustawień"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "Nie można zapisać ustawień: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Nie można uruchomić usługi setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Nie można odmontować systemu plików"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Niedostępne"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Niedostępne pakiety"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Cofnij"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "Nieoczekiwany błąd usługi PackageKit podczas instalacji $0: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Nieoczekiwany błąd"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Nierozpoznane dane"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Jednostka"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Nieznane"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Nieznane „$0”"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Nieznane ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Nieznana aplikacja"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Nieznana konfiguracja"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Nieznana nazwa komputera"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Nieznana nazwa usługi"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Nieznany typ"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Odblokuj"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Automatyczne odblokowywanie podczas uruchamiania"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Odblokuj zaszyfrowaną pulę Stratis"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "Odblokuj klucz $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Odblokowywanie $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Odblokowywanie dysku"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Niezarządzane interfejsy"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Odmontuj"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "Odmontuj system plików $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Odmontuj teraz"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Odmontowywanie $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Bez nazwy"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "Odepnij jednostkę"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Nierozpoznane dane"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "Unrecognized data can not be made smaller here."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Nie można tutaj zmniejszać nieznanych danych."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Nie można tutaj zmniejszać nieznanych danych."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Nieobsługiwany wolumin"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "Do"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Niezaufany komputer"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Zaktualizuj"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Zaktualizuj tabelę powodzenia"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Historia aktualizacji"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Zaktualizuj informacje o pakiecie"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "Aktualizacja była pomyślna"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Zaktualizowano"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr "Zaktualizowane pakiety mogą wymagać ponownego uruchomienia."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Dostępne są aktualizacje"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Historia aktualizacji"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Aktualizacje zostaną zastosowane $0 o $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Aktualizowanie"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Aktualizowanie stanu…"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Czas działania"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Użycie"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "Użycie $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Użyj"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Kompresja"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Deduplikacja"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Użyj klucza"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Użycie hasła"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Użycie poniższych kluczy do uwierzytelniania w innych systemach"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Więcej informacji w dzienniku"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Używane"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"Przydatne dla punktów montowania, które są opcjonalne lub potrzebują "
+"działania użytkownika (np. hasła)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Użytkownik"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "Identyfikator użytkownika"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "Identyfikator jest już używany przez innego użytkownika"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "Identyfikator użytkownika musi być dodatnią liczbą całkowitą"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "Identyfikator użytkownika nie może być większy niż $0"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "Identyfikator użytkownika nie może być mniejszy niż $0"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Nazwa użytkownika"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Nazwa użytkownika nie może być pusta"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Nazwa użytkownika"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "Używanie szyfrowania LUKS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Używanie serwera Tang"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "Urządzenia podstawowe VDO nie mogą być zmniejszane"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "Urządzenie VDO $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "Wolumin systemu plików VDO (kompresja/deduplikacja)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "Identyfikator VLAN"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Sprawdzanie poprawności adresu"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Sprawdzanie poprawności tokenu uwierzytelniania"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Producent"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Zweryfikowano"
+
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "Zweryfikuj odcisk"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Zweryfikuj klucz"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Weryfikowanie"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Wersja"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Bardzo bezpieczne usuwanie zawartości $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Wszystkie procesory"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Wszystkie dyski"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Wszystkie dzienniki"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Wszystkie usługi"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Wyświetl skrypt automatyzacji"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Wyświetl szczegółowe dzienniki"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Wyświetl informacje o sprzęcie"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Wyświetl historię logowania"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "Wyświetl dzienniki"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Wyświetl statystyki i historię"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Przepustowość każdego dysku"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Strona projektu"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Wyświetl zgłoszenie"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Dziennik aktualizacji"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "Wyświetlanie informacji o pamięci wymaga dostępu administracyjnego."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Otwórz zaporę sieciową"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Grupa woluminów"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Volume group is missing physical volumes"
+msgstr "Usuwanie woluminu fizycznego z $target"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Rozmiar woluminu to $0. Rozmiar zawartości to $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Oczekiwanie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Oczekiwanie na informacje…"
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Oczekiwanie na dane…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr "Oczekiwanie, aż inne programy skończą korzystać z menedżera pakietów…"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr ""
+"Oczekiwanie na ukończenie pozostałych działań zarządzania oprogramowaniem"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Oczekiwanie na rozpoczęcie…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Chciane przez"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Chce"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Ostrzeżenia i powyżej"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+#, fuzzy
+#| msgid "Set weak password"
+msgid "Weak password"
+msgstr "Ustaw słabe hasło"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Konsola internetowa"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Konsola internetowa dla serwerów systemu Linux"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Konsola internetowa zostanie ponownie uruchomiona"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Konsola internetowa działa w trybie ograniczonego dostępu."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Logo konsoli internetowej"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "środy"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Co tydzień"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Tygodnie"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Kiedy"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "Pozostawienie pustej spowoduje połączenie z obecnym użytkownikiem"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Kiedy konsola internetowa jest ponownie uruchamiana, informacje o postępie "
+"nie będą widoczne. Proces aktualizacji będzie kontynuowany w tle. Ponowne "
+"połączenie umożliwi obserwowanie procesu aktualizacji."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Biały"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "Zostanie ustawione na „Automatyczne”"
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "World wide name"
+msgid "World wide name"
+msgstr "WWN"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Zapis"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Głównie zapisywane"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Zapisywanie"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Błędna nazwa użytkownika lub hasło"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Co roku"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Tak"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Łączenie z $0 po raz pierwszy."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Brak upoważnienia do modyfikacji zapory sieciowej."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Brak uprawnienia do wyświetlania upoważnionych publicznych kluczy dla tego "
+"konta."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Wylogowano z powodu nieaktywności."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Można spróbować wczytać starsze wpisy."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr "Można zmienić hasło klucza do automatycznego logowania."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Należy poczekać dłużej na zmianę hasła"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr "Aby wyświetlić historię statystyk, należy się zalogować ponownie"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Uzyskano dostęp administracyjny."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Wylogowanie nastąpi za $0 s."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Twoje konto"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"Używana przeglądarka nie zezwala na wklejanie z menu kontekstowego. Można "
+"użyć klawiszy Shift+Insert."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr ""
+"Używana przeglądarka będzie pamiętała poziom dostępu użytkownika między "
+"sesjami."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Serwer niedługo zamknie połączenie. Można połączyć jeszcze raz po jego "
+"ponownym uruchomieniu."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Sesja została zakończona."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Sesja wygasła. Proszę zalogować się ponownie."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Strefa"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[dane binarne]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[brak danych]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "ABRT"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "dostęp"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "aktywne"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "dodatek"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "dodatek"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "po sieci"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "aplikacje"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "etykieta zasobu"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "w"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "AVC"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "średnia: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "urządzenie podstawowe urządzenia VDO"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "BIOS"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "wiązanie"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "uruchom"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "mostek"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "Inne urządzenia"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "Inne urządzenia"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs subvolume"
+msgstr "Wolumin urządzeń do przechowywania danych"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Woluminy urządzeń do przechowywania danych"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "Wolumin urządzeń do przechowywania danych"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "poprawka błędu"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "B"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "Pamięć podręczna"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "CGroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "zmień"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman nie jest zainstalowane"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "polecenie"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "konsola"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "zrzut core"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "procesor"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "awaria"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "$0 danych"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "data"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Dezaktywuj"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "debuguj"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "usuń"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "DIMM"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "wyłącz"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "dysk"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "dyski"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "DNF"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "domena"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "napęd"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "modyfikuj"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "włącz"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted"
+msgid "encrypted"
+msgstr "Zaszyfrowane"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "szyfrowanie"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "ulepszenie"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "błąd"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "co piątek"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "co poniedziałek"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "co sobotę"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "co niedzielę"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "co czwartek"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "co wtorek"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "co środę"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "codziennie"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "rozszerzenie"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "wyświetlenie listy kluczy SSH komputera się nie powiodło: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "system plików"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "zapora sieciowa"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "dla obecnych i przyszłych jąder"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "tylko dla obecnego jądra"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "format"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "z $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "z $0 na $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "wykresy"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "powiększ"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "sprzęt"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "historia"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "gospodarz"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "Cel iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "Cele iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "Dodaj portal iSCSI"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "ignorowanie niepowodzenia"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "w większości przeglądarek"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "niespójne"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "zainicjuj"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "zainstaluj"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "interfejs"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "nieprawidłowe: podano wiele celów"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "IPv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iSCSI"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "dziennik"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "strona podręcznika journalctl"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "stan kdump"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "klucze"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "logowanie"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "LUKS"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "LVM2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "MAC"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "komputer"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "maska"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "maksymalnie: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "member of RAID device"
+msgid "member of MDRAID device"
+msgstr "element urządzenia RAID"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "element puli Stratis"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "pamięć"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "statystyki"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "poprawka zmniejszająca ryzyko"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "więcej informacji"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "montowanie"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "NBDE"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "sieć"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "bez montowania podczas uruchamiania"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "NFS"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "eksport NFS jest pusty"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "serwer NFS jest pusty"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "serwer NFS nie jest prawidłowym adresem IPv6"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "nice"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "brak"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "$0 procesora"
+msgstr[1] "$0 procesorów"
+msgstr[2] "$0 procesorów"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "na $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "system operacyjny"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "system operacyjny"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "pakiet"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "PackageKit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "partycja"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "hasło"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "jakość hasła"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "łaty"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "ścieżka"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "PCI"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "wydajność"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "wolumin fizyczny grupy woluminów LVM2"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "wtyczka"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service się nie powiodło"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service nie może zbierać danych"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service nie jest uruchomione"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "pojemnik"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "port"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "zasilanie"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "RAID"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "RAM"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "tylko do odczytu"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "zalecane"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "usuń z LVM2"
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "remove from RAID"
+msgid "remove from MDRAID"
+msgstr "usuń z RAID"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "usuń z LVM2"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "uruchom ponownie"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "role"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "pod kontrolą $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "bezpieczeństwo"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "szeregowe"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "usługa"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "SETroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "powłoka"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "wyświetl mniej"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "wyświetl więcej"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "zmniejsz"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "wyłącz"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "gniazdo"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "rozwiązanie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "informacje o rozwiązaniu"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "Zgłoszenie sos się nie powiodło"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "SSH"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "klucz SSH nie jest ścieżką"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "serwer SSH jest pusty"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "zatrzymaj"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "zatrzymanie uruchamiania po niepowodzeniu"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+#, fuzzy
+#| msgid "Stopped"
+msgid "stopped"
+msgstr "Zatrzymano"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "Tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "cel"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "TCP"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "zespołowe"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "czas"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "licznik"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "UDisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "UDP"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "jednostka"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "nieznane"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "nieznany cel"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "unmask"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "odmontowanie"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "nieprzydzielone miejsce na $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "użytkownik"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "nazwa użytkownika"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "używanie opisu klucza $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "VDO"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "wersja"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "VLAN"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "wolumin"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "ostrzeżenie"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "Pakiet wireguard-tools nie jest zainstalowany"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "tak"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "strefa"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Usługa Kdump nie jest zainstalowana. Proszę się upewnić, że pakiet kexec-"
+#~ "tools jest zainstalowany."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Brak zastrzeżonej pamięci. Proszę dodać opcję crashkernel do wiersza "
+#~ "poleceń jądra (tzn. w /etc/default/grub), aby zastrzec pamięć w czasie "
+#~ "uruchamiania. Przykład: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Usługa jest uruchomiona"
+
+#~ msgid "Service is starting"
+#~ msgstr "Usługa jest uruchamiana"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Usługa jest zatrzymana"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Usługa jest zatrzymywana"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Przetestuje to konfigurację kdump przez zawieszenie jądra."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "Nie skonfigurowano opcji crashkernel w wierszu poleceń jądra"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (zaszyfrowane)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "Pula Stratis o rozmiarze $0"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 pamięci podręcznej"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 nieznanej warstwy"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, wolne: $1"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "Przed usunięciem tego dysku należy najpierw dodać dysk zapasowy."
+
+#~ msgid "Backing device"
+#~ msgstr "Urządzenie podstawowe"
+
+#~ msgid "Block"
+#~ msgstr "Zablokuj"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Pojemność"
+
+#~ msgid "Content"
+#~ msgstr "Zawartość"
+
+#~ msgid "Create devices"
+#~ msgstr "Utwórz urządzenia"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Urządzenie"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Plik urządzenia"
+
+#~ msgid "Devices"
+#~ msgstr "Urządzenia"
+
+#~ msgid "Drives"
+#~ msgstr "Napędy"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Zaszyfrowane woluminy muszą zostać odblokowane przed zmianą rozmiaru."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Systemu plików (zaszyfrowany)"
+
+#~ msgid "Filesystems"
+#~ msgstr "Systemy plików"
+
+#~ msgid "Free"
+#~ msgstr "Wolne"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Należy zwolnić miejsce w tej grupie: zmniejszyć lub usunąć inne woluminy "
+#~ "logiczne lub dodać kolejny wolumin fizyczny."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Nieaktywny wolumin"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Serwer kluczy"
+
+#~ msgid "LVM2 member"
+#~ msgstr "Element LVM2"
+
+#~ msgid "Locked devices"
+#~ msgstr "Zablokowane urządzenia"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Zablokowane zaszyfrowane dane"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Model"
+
+#~ msgid "NFS mounts"
+#~ msgstr "Punkty montowania NFS"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "Obsługa NFS nie jest zainstalowana"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Nie ustawiono żadnych punktów montowania NFS"
+
+#~ msgid "No devices"
+#~ msgstr "Brak urządzeń"
+
+#~ msgid "No drives attached"
+#~ msgstr "Nie podłączono napędów"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Nie ustawiono żadnych celów iSCSI"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Za mało miejsca dla nowych systemów plików"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Inne dane"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Partycjonowane urządzenie blokowe"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Hasło"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "Nie można tutaj zmieniać rozmiaru woluminów fizycznych."
+
+#~ msgid "Pool"
+#~ msgstr "Pula"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Pula dla cienkich woluminów"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "Poziom macierzy RAID"
+
+#~ msgid "RAID member"
+#~ msgstr "Element macierzy RAID"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Napęd wymienny"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Wyświetl $0 urządzenie"
+#~ msgstr[1] "Wyświetl wszystkie $0 urządzenia"
+#~ msgstr[2] "Wyświetl wszystkie $0 urządzeń"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Wyświetl $0 napęd"
+#~ msgstr[1] "Wyświetl wszystkie $0 napędy"
+#~ msgstr[2] "Wyświetl wszystkie $0 napędów"
+
+#~ msgid "Show all"
+#~ msgstr "Wyświetl wszystkie"
+
+#~ msgid "Source"
+#~ msgstr "Źródło"
+
+#~ msgid "Start pool"
+#~ msgstr "Rozpocznij pulę"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Rozpoczęcie puli wyświetli systemy plików."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Stan"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Zatrzymano pulę Stratis"
+
+#~ msgid "Stratis member"
+#~ msgstr "Element Stratis"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Pula Stratis $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "Obsługa jest zainstalowana."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr "Urządzenie RAID musi być uruchomione, aby dodać zapasowe dyski."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "Nie można usuwać ostatniego dysku urządzenia RAID."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "Nie można usunąć ostatniego woluminu fizycznego grupy woluminów."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Za mało wolnego miejsca, aby usunąć ten wolumin fizyczny. Wymagane jest "
+#~ "co najmniej $0 wolnego miejsca więcej."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "Ten dysk nie może zostać usunięty podczas przywracania urządzenia."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr "Ten wolumin musi zostać aktywowany przed zmianą rozmiaru."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Nierozpoznane dane"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Użycie"
+
+#~ msgid "Used for"
+#~ msgstr "Używane dla"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "Podstawa VDO"
+
+#~ msgid "VDO device"
+#~ msgstr "Urządzenie VDO"
+
+#~ msgid "Volume"
+#~ msgstr "Wolumin"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automatyczne (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Przyjmij klucz i połącz się"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Nie można tutaj zmieniać rozmiaru zaszyfrowanych woluminów."
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Użyj serwera kluczy Tang"
+
+#~ msgid "Use a passphrase"
+#~ msgstr "Użyj hasła"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitmapa"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Nie można tutaj odblokować tej puli, ponieważ jej opis klucza nie jest "
+#~ "w oczekiwanym formacie."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Przełącz bitmapę"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Proszę się upewnić, że suma kontrolna klucza z serwera Tang zgadza się "
+#~ "z jednym z:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Ręczne sprawdzenie za pomocą SSH: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Numer i typ portu się nie zgadzają"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Zablokowana zaszyfrowana pula Stratis"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Błąd podczas usuwania alarmu: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Nie można pobrać alarmu: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 B danych binarnych]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Szczyt wejścia/wyjścia dysku"
+
+#~ msgid "Event"
+#~ msgstr "Zdarzenie"
+
+#~ msgid "Event logs"
+#~ msgstr "Dzienniki zdarzeń"
+
+#~ msgid "Load spike"
+#~ msgstr "Szczyt obciążenia"
+
+#~ msgid "Memory spike"
+#~ msgstr "Szczyt pamięci"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Szczyt wejścia/wyjścia sieci"
+
+#~ msgid "ssh key"
+#~ msgstr "klucz SSH"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Jeśli ta opcja zostanie zaznaczona, to system plików nie zostanie "
+#~ "zamontowany podczas następnego uruchomienia nawet, jeśli był wcześniej "
+#~ "zamontowany. Jest to przydatne, jeśli montowanie podczas uruchamiania nie "
+#~ "jest możliwe, na przykład kiedy hasło jest wymagane do odblokowania "
+#~ "systemu plików, ale uruchamianie jest nienadzorowane."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Bez montowania podczas uruchamiania"
+
+#~ msgid "Listing unit files"
+#~ msgstr "Wyświetlanie plików jednostek"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "Wyświetlenie plików jednostek się nie powiodło: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "Nie odnaleziono jednostki"
+
+#~ msgid "Validating key"
+#~ msgstr "Sprawdzanie poprawności klucza"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "Usuń grupę $0"
+
+#~ msgid "Container administrator"
+#~ msgstr "Administrator kontenera"
+
+#~ msgid "Image builder"
+#~ msgstr "Budowanie obrazu"
+
+#~ msgid "Roles"
+#~ msgstr "Role"
+
+#~ msgid "Server administrator"
+#~ msgstr "Administrator serwera"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Grupa uniksowa: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Pomyślnie skopiowano do klawiatury"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Raporty diagnostyczne"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Zrzut jądra"
+
+#~ msgid "Software Updates"
+#~ msgstr "Aktualizacje oprogramowania"
+
+#~ msgid "Update log"
+#~ msgstr "Dziennik aktualizacji"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "cel zrzutu NFS nie jest sformatowany jako serwer:ścieżka"
+
+#~ msgid "$0 Zone"
+#~ msgstr "Strefa $0"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Utwórz raport diagnostyczny"
+
+#~ msgid "Done!"
+#~ msgstr "Gotowe."
+
+#~ msgid "Download report"
+#~ msgstr "Pobierz raport"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Nie utworzono archiwum."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Proszę potwierdzić usunięcie $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "Utworzone archiwum zawiera dane uważane za prywatne, więc jego zawartość "
+#~ "powinna zostać przejrzana przez pierwotną organizację, zanim zostanie ono "
+#~ "przekazane jakiejkolwiek stronie trzeciej."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "To narzędzie zbierze konfigurację systemu i informacje diagnostyczne "
+#~ "z tego systemu do użycia w diagnozowaniu problemów z systemem."
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "Brak pakietu cockpit-system na serwerze"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Ten pakiet nie jest zgodny z tą wersją Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Ten pakiet wymaga Cockpit w wersji %s lub nowszej"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Statystyki wydajności"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "tylko do odczytu"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB razem"
+
+#~ msgid "Apply"
+#~ msgstr "Zastosuj"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Brak upoważnienia do usunięcia strefy $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Kliknij $0 ponownie, aby użyć hasła mimo to."
+
+#~ msgid "Access"
+#~ msgstr "Dostęp"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "NA DYSKU WYSTĘPUJĄ BŁĘDY"
+
+#~ msgid "Logical Size"
+#~ msgstr "Rozmiar logiczny"
+
+#~ msgid "Security updates "
+#~ msgstr "Aktualizacje zabezpieczeń "
+
+#~ msgid "Updates "
+#~ msgstr "Aktualizacje "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Opcjonalne)"
+
+#~ msgid "Active since"
+#~ msgstr "Aktywne od"
+
+#~ msgid "Affected locations"
+#~ msgstr "Położenia, na które wpływa"
+
+#~ msgid "Process"
+#~ msgstr "Proces"
+
+#~ msgid "Remove and delete"
+#~ msgstr "Usuń"
+
+#~ msgid "Remove and format"
+#~ msgstr "Usuń i sformatuj"
+
+#~ msgid "Remove and grow"
+#~ msgstr "Usuń i powiększ"
+
+#~ msgid "Remove and initialize"
+#~ msgstr "Usuń i zainicjuj"
+
+#~ msgid "Remove and shrink"
+#~ msgstr "Usuń i zmniejsz"
+
+#~ msgid "Remove and stop device"
+#~ msgstr "Usuń i zatrzymaj urządzenie"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "System plików jest używany przez sesje logowania i usługi systemowe. "
+#~ "Kontynuacja je zatrzyma."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "System plików jest używany przez sesje logowania. Kontynuacja je zatrzyma."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "System plików jest używany przez usługi systemowe. Kontynuacja je "
+#~ "zatrzyma."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "To urządzenie ma obecnie używane systemy plików. Kontynuacja odmontuje "
+#~ "wszystkie jego systemy plików."
+
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "To urządzenie jest obecnie używane dla grup woluminów LVM2."
+
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "To urządzenie jest obecnie używane dla grup woluminów LVM2. Kontynuacja "
+#~ "usunie je z jego grup woluminów."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "To urządzenie jest obecnie używane dla urządzeń RAID."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "To urządzenie jest obecnie używane dla urządzeń RAID. Kontynuacja usunie "
+#~ "je z jego urządzeń RAID."
+
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "To urządzenie jest obecnie używane dla pul Stratis."
+
+#~ msgid "Unmount and delete"
+#~ msgstr "Odmontuj i usuń"
+
+#~ msgid "Unmount and format"
+#~ msgstr "Odmontuj i sformatuj"
+
+#~ msgid "Unmount and grow"
+#~ msgstr "Odmontuj i powiększ"
+
+#~ msgid "Unmount and initialize"
+#~ msgstr "Odmontuj i zainicjuj"
+
+#~ msgid "Unmount and shrink"
+#~ msgstr "Odmontuj i zmniejsz"
+
+#~ msgid "Unmount and stop device"
+#~ msgstr "Odmontuj i zatrzymaj urządzenie"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 z $1"
+
+#~ msgid "Blockdev"
+#~ msgstr "Blockdev"
+
+#~ msgid "Blockdev of Stratis pool $0"
+#~ msgstr "Blockdev puli Stratis $0"
+
+#~ msgid "Blockdev of locked Stratis pool $0"
+#~ msgstr "Blockdev zablokowanej puli Stratis $0"
+
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Wolumin fizyczny LVM2 $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Element urządzenia RAID $0"
+
+#~ msgid "Menu"
+#~ msgstr "Menu"
+
+#~ msgid "VDO backing"
+#~ msgstr "Podstawa VDO"
+
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Utwórz pulę Stratis"
+
+#~ msgid "Mount Options"
+#~ msgstr "Opcje montowania"
+
+#~ msgid "Stratis Pool"
+#~ msgstr "Pula Stratis"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Wymagany jest dysk."
+
+#~ msgid "Disk"
+#~ msgstr "Dysk"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Tylko dla przestarzałych programów. Zmniejsza wydajność."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Zainstaluj obsługę VDO"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Emulacja 512 bajtów"
+
+#~ msgid "System services"
+#~ msgstr "Usługi systemowe"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Utwórz raport diagnostyczny "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr ""
+#~ "Jednoczesne łączenie z więcej niż {{ limit }} komputerami jest "
+#~ "nieobsługiwane."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "SSO oparte na Kerberos"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Zaloguj do {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Nowe hasła klucza się nie zgadzają"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "Aby sprawdzić poprawność odcisku klucza, należy wykonać poniższe "
+#~ "polecenie na {{host}} fizycznie siedząc przy komputerze lub przez zaufaną "
+#~ "sieć:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Nie można skontaktować się z {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Nie można zalogować się w {{#strong}}{{host}}{{/strong}}. Zaktualizowanie "
+#~ "cockpit-ws do nowszej wersji udostępni więcej opcji uwierzytelniania "
+#~ "i pomocy w rozwiązywaniu problemów."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Nie można zalogować się w {{#strong}}{{host}}{{/strong}}. Aby połączyć "
+#~ "się z tym komputerem, należy włączyć jedną z poniższych metod "
+#~ "uwierzytelniania w konfiguracji usługi sshd w {{#strong}}{{host}}{{/"
+#~ "strong}}:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "Łączenie z {{host}} po raz pierwszy."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "Zmieniono klucz {{host}}"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Wyczyść konfigurację punktu montowania"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Utworzenie tego wiązania zerwie połączenie z serwerem i uniemożliwi "
+#~ "korzystanie z interfejsu administracji."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Utworzenie tego mostka zerwie połączenie z serwerem i uniemożliwi "
+#~ "korzystanie z interfejsu administracji."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Utworzenie tego zespołu zerwie połączenie z serwerem i uniemożliwi "
+#~ "korzystanie z interfejsu administracji."
+
+#~ msgid "IP settings"
+#~ msgstr "Ustawienia IP"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Wyłączenie <b>$0</b> zerwie połączenie z serwerem i uniemożliwi "
+#~ "korzystanie z interfejsu administracji."
+
+#~ msgid "Administrator password"
+#~ msgstr "Hasło administratora"
+
+#~ msgid "Computer OU"
+#~ msgstr "OU komputera"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr ""
+#~ "Usunięcie urządzenia RAID usunie wszystkie znajdujące się na nim dane."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr ""
+#~ "Usunięcie grupy woluminów usunie wszystkie znajdujące się w niej dane."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Nie zastępuj istniejących danych"
+
+#~ msgid "Erase"
+#~ msgstr "Wymaż"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Sformatuj dysk $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr ""
+#~ "Sformatowanie urządzenia do przechowywania danych usunie wszystkie "
+#~ "znajdujące się na nim dane."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Nie należy zmieniać nazwy komputera w domenie"
+
+#~ msgid "More"
+#~ msgstr "Więcej"
+
+#~ msgid "Not joined"
+#~ msgstr "Niedołączone"
+
+#~ msgid "One time password"
+#~ msgstr "Hasło jednorazowe"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 z $1 na $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Godzina musi być liczbą między 0 a 23"
+
+#~ msgid "Last login:"
+#~ msgstr "Ostatnie logowanie:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Minuta musi być liczbą między 0 a 59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "Od ostatniego pomyślnego logowania wystąpiła $0 nieudana próba logowania."
+#~ msgstr[1] ""
+#~ "Od ostatniego pomyślnego logowania wystąpiły $0 nieudane próby logowania."
+#~ msgstr[2] ""
+#~ "Od ostatniego pomyślnego logowania wystąpiło $0 nieudanych prób logowania."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "np. „$0”"
+
+#~ msgid "$0 active zones"
+#~ msgstr "Aktywne strefy: $0"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 wystąpienie"
+#~ msgstr[1] "$0 wystąpienia"
+#~ msgstr[2] "$0 wystąpień"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Na warunkach licencji:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Odblokowywanie podczas uruchamiania"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Odblokowywanie tylko do odczytu"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Przeszukaj dzienniki za pomocą połączenia terminów:"
+
+#~ msgid "Show filters"
+#~ msgstr "Wyświetl filtry"
+
+#~ msgid "Text"
+#~ msgstr "Tekst"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "dowolny ciąg jako wyrażenie regularne"
+
+#~ msgid "e.g."
+#~ msgstr "np."
+
+#~ msgid "log fields"
+#~ msgstr "pola dziennika"
+
+#~ msgid "qualifiers"
+#~ msgstr "kwalifikatory"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "Przełącz ustawienia sesji"
+
+#~ msgid "(none)"
+#~ msgstr "(brak)"
+
+#~ msgid "Create timers"
+#~ msgstr "Utwórz liczniki"
+
+#~ msgid "Recommended default"
+#~ msgstr "Zalecana wartość domyślna"
+
+#~ msgid "Run"
+#~ msgstr "Uruchom"
+
+#~ msgid "Select unit state"
+#~ msgstr "Wybierz stan jednostki"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Włącz przechowywane statystyki"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Włącz przechowywane statystyki"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Wymuś usunięcie hasła w $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Jeśli tang-show-keys jest niedostępne, należy wykonać:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Co, jeśli tang-show-keys jest niedostępne?"
+
+#~ msgid "key slot $0"
+#~ msgstr "gniazdo na klucz $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Coś się nie powiodło"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "To nie zadziałało, proszę spróbować ponownie"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "Nie można uzyskać dostępu administracyjnego."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Automatyczne aktualizacje nie są skonfigurowane"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "Konfiguracja procesora $0"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 sieć"
+#~ msgstr[1] "$0 sieci"
+#~ msgstr[2] "$0 sieci"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 jest dostępne dla większości systemów operacyjnych. Aby zainstalować, "
+#~ "należy wyszukać w Menedżerze oprogramowania GNOME lub wykonać polecenie:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "Dostosowanie pamięci $0"
+
+#~ msgid "$0 network"
+#~ msgstr "Sieć $0"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 wirtualny procesor"
+#~ msgstr[1] "$0 wirtualne procesory"
+#~ msgstr[2] "$0 wirtualnych procesorów"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "Informacje o wirtualnym procesorze $0"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "Ustawienia wirtualnego interfejsu sieciowego $0"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr ""
+#~ "Aktywuj pulę urządzeń do przechowywania danych, aby administrować "
+#~ "woluminami"
+
+#~ msgid "Add network interface"
+#~ msgstr "Dodaj interfejs sieciowy"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Dodaj wirtualny interfejs sieciowy"
+
+#~ msgid "Additional"
+#~ msgstr "Dodatkowe"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Adres nie jest w podsieci"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr "Po usunięciu migawki cała jej treść zostanie utracona."
+
+#~ msgid "Always attach"
+#~ msgstr "Podłączanie za każdym razem"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Podłączenie spowoduje, że ten dysk będzie udostępniany każdej używającej "
+#~ "go maszynie wirtualnej."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Automatyczne włączanie usługi libvirt podczas uruchamiania"
+
+#~ msgid "Autostart"
+#~ msgstr "Automatyczne uruchamianie"
+
+#~ msgid "Boot order"
+#~ msgstr "Kolejność uruchamiania"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Nie można zapisać ustawień kolejności uruchamiania"
+
+#~ msgid "Bus"
+#~ msgstr "Magistrala"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "Płyta CD/DVD"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "Nie można zapisać konfiguracji procesora"
+
+#~ msgid "CPU type"
+#~ msgstr "Typ procesora"
+
+#~ msgid "Change firmware"
+#~ msgstr "Zmień oprogramowanie sprzętowe"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Zmiany zostaną uwzględnione po wyłączeniu maszyny wirtualnej"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Wybierz system operacyjny"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "Kliknięcie „Uruchom zdalną przeglądarkę” pobierze plik .vv i uruchomi $0."
+
+#~ msgid "Clone"
+#~ msgstr "Sklonuj"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Nie można wczytać obrazu"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Potwierdź to działanie"
+
+#~ msgid "Connect"
+#~ msgstr "Połącz"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "Połącz z dowolną przeglądarką wymienionych protokołów"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Łączenie z usługą wirtualizacji"
+
+#~ msgid "Connection"
+#~ msgstr "Połączenie"
+
+#~ msgid "Console"
+#~ msgstr "Konsola"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Rdzenie na gniazdo"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Nie można przywrócić do migawki"
+
+#~ msgid "Crashed"
+#~ msgstr "Uległo awarii"
+
+#~ msgid "Create VM"
+#~ msgstr "Utwórz maszynę wirtualną"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Utwórz klon maszyny wirtualnej na podstawie $0"
+
+#~ msgid "Create new"
+#~ msgstr "Utwórz nowy"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Utwórz nową maszynę wirtualną"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Utwórz sieć wirtualną"
+
+#~ msgid "Creating VM"
+#~ msgstr "tworzenie maszyny wirtualnej"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "tworzenie instalacji maszyny wirtualnej"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Utworzenie maszyny wirtualnej $0 się nie powiodło"
+
+#~ msgid "Creation time"
+#~ msgstr "Czas utworzenia"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Obecny przydział"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Niestandardowe oprogramowanie sprzętowe: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "Zakres DHCP"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Usunięcie powiązanych plików urządzeń do przechowywania danych:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Usuń pulę urządzeń do przechowywania danych $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Usuń woluminy wewnątrz tej puli"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Usunięcie nieaktywnej puli urządzeń do przechowywania danych spowoduje "
+#~ "tylko usunięcie określenia puli. Jej zawartość nie zostanie usunięta."
+
+#~ msgid "Desktop viewer"
+#~ msgstr "Przeglądarka pulpitu"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Przed próbą usunięcia należy odłączyć dyski używające tej puli ze "
+#~ "wszystkich maszyn wirtualnych."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Rozłączono z konsoli szeregowej. Proszę kliknąć przycisk „Połącz”."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Odłączenie dysku $0 z maszyny wirtualnej $1 się nie powiodło"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Podłączenie dysku się nie powiodło"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Utworzenie dysku się nie powiodło"
+
+#~ msgid "Disk image file"
+#~ msgstr "Plik obrazu dysku"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Nie można zapisać ustawień dysku"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "Migawka tylko dysku"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "Domena uległa awarii"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "Domena jest zablokowana na zasobie"
+
+#~ msgid "Download an OS"
+#~ msgstr "Pobierz system operacyjny"
+
+#~ msgid "Dying"
+#~ msgstr "W trakcie umierania"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Emulowany komputer"
+
+#~ msgid "End"
+#~ msgstr "Koniec"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Koniec nie może być pusty"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Istniejący obraz dysku w systemie plików gospodarza"
+
+#~ msgid "Expand"
+#~ msgstr "Rozwiń"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Zmiana oprogramowania sprzętowego się nie powiodła"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "Pobranie adresów IP interfejsów obecnych w $0 się nie powiodło"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr ""
+#~ "Wysłanie klawiszy Ctrl+Alt+$0 do maszyny wirtualnej $1 się nie powiodło"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr ""
+#~ "Mniej niż maksymalna liczba wirtualnych procesorów powinna być włączona."
+
+#~ msgid "File"
+#~ msgstr "Plik"
+
+#~ msgid "Filter by name"
+#~ msgstr "Filtrowanie według nazwy"
+
+#~ msgid "Firmware"
+#~ msgstr "Oprogramowanie sprzętowe"
+
+#~ msgid "Force shut down"
+#~ msgstr "Wymuś wyłączenie"
+
+#~ msgid "Forward mode"
+#~ msgstr "Tryb przekierowania"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Tryb przekierowania"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Utwórz automatycznie"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "Przejdź do listy maszyn wirtualnych"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Ukryj dodatkowe opcje"
+
+#~ msgid "Host device"
+#~ msgstr "Urządzenie gospodarza"
+
+#~ msgid "Host name"
+#~ msgstr "Nazwa komputera"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Gospodarz nie może być pusty"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Informacje o nadzorcy"
+
+#~ msgid "IP configuration"
+#~ msgstr "Konfiguracja IP"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 i IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "Sieć IPv4"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "Sieć IPv4 nie może być pusta"
+
+#~ msgid "IPv4 only"
+#~ msgstr "Tylko IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "Adres IPv6"
+
+#~ msgid "IPv6 network"
+#~ msgstr "Sieć IPv6"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "Sieć IPv6 nie może być pusta"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Tylko IPv6"
+
+#~ msgid "Idle"
+#~ msgstr "Bezczynne"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Od razu uruchom maszynę wirtualną"
+
+#~ msgid "Import"
+#~ msgstr "Zaimportuj"
+
+#~ msgid "Import VM"
+#~ msgstr "Zaimportuj maszynę wirtualną"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "Zaimportuj maszynę wirtualną"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "W przypadku większości konfiguracji, macvtap nie działa dla gospodarza "
+#~ "w celu komunikacji sieciowej gościa."
+
+#~ msgid "Initiator"
+#~ msgstr "Inicjator"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "Inicjator IQN nie może być pusty"
+
+#~ msgid "Installation source"
+#~ msgstr "Źródło instalacji"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Źródło instalacji nie może być puste"
+
+#~ msgid "Installation type"
+#~ msgstr "Typ instalacji"
+
+#~ msgid "Interface type"
+#~ msgstr "Typ interfejsu"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Nieprawidłowa długość maski lub przedrostka IPv4"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Nieprawidłowy adres IPv6"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Nieprawidłowy przedrostek IPv6"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Nieprawidłowa nazwa pliku"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "Uruchom zdalną przeglądarkę"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr ""
+#~ "Pozostawienie pustego hasła spowoduje, że konto roota nie zostanie "
+#~ "utworzone"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "Pozostawienie pustego hasła spowoduje, że konto użytkownika nie zostanie "
+#~ "utworzone"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr ""
+#~ "Pozostawienie pustego hasła spowoduje, że konto roota nie zostanie "
+#~ "utworzone"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Biblioteka libvirt nie wykryła żadnego obrazu oprogramowania sprzętowego "
+#~ "UEFI/OVMF zainstalowanego na gospodarzu"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Biblioteka libvirt lub nadzorca nie obsługuje UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "Wczytywanie zasobów"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "Maszyna musi być wyłączona przed zmianą typu magistrali"
+
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "Maszyna musi być wyłączona przed zmianą trybu pamięci podręcznej"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Zarządzanie maszynami wirtualnymi"
+
+#~ msgid "Manual connection"
+#~ msgstr "Ręczne połączenie"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Długość maski lub przedrostka"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Długość maski lub przedrostka nie może być pusta"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Maksymalny przydział"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "Nie można zapisać maksymalnej ilości pamięci"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Maksymalna liczba wirtualnych procesorów przydzielonych do systemu "
+#~ "operacyjnego gościa"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Maksymalna liczba wirtualnych procesorów przydzielonych do systemu "
+#~ "operacyjnego gościa, musi wynosić między 1 a $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "MTU"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Nie można zapisać pamięci"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "Pamięć nie może wynosić 0"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Typ modelu"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT do $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "Zmiana stanu NIC $0 maszyny wirtualnej $1 się nie powiodła"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Nazwa zawiera nieprawidłowe znaki"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Nazwa nie może być pusta"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Nazwa nie może być pusta"
+
+#~ msgid "Name: "
+#~ msgstr "Nazwa: "
+
+#~ msgid "Netmask"
+#~ msgstr "Maska sieci"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Aktywacja sieci $0 się nie powiodła"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Dezaktywacja sieci $0 się nie powiodła"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Uruchamianie sieciowe (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "NFS"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Nie można zapisać ustawień interfejsu sieciowego"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "Wybór sieci nie obsługuje PXE."
+
+#~ msgid "Networks"
+#~ msgstr "Sieci"
+
+#~ msgid "New volume name"
+#~ msgstr "Nazwa nowego woluminu"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr ""
+#~ "Brak uruchomionych lub określonych maszyn wirtualnych na tym gospodarzu"
+
+#~ msgid "No connection available"
+#~ msgstr "Brak dostępnego połączenia"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Nie określono dysków dla tej maszyny wirtualnej"
+
+#~ msgid "No network devices"
+#~ msgstr "Brak urządzeń sieciowych"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Nie określono interfejsów sieciowych dla tej maszyny wirtualnej"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "Na tym gospodarzu nie określono żadnej sieci"
+
+#~ msgid "No networks available"
+#~ msgstr "Brak dostępnych sieci"
+
+#~ msgid "No parent"
+#~ msgstr "Brak nadrzędnego"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Nie określono migawek dla tej maszyny wirtualnej"
+
+#~ msgid "No state"
+#~ msgstr "Brak stanu"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr ""
+#~ "Na tym gospodarzu nie określono żadnej puli urządzeń do przechowywania "
+#~ "danych"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Brak dostępnych pul urządzeń do przechowywania danych"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr ""
+#~ "Dla tej puli urządzeń do przechowywania danych nie określono żadnych "
+#~ "woluminów urządzeń do przechowywania danych"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Brak sieci wirtualnych"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "Nie można usunąć nietrwałej sieci. Przestaje ona istnieć, kiedy zostaje "
+#~ "dezaktywowana."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "Nie można usunąć nietrwałej puli urządzeń do przechowywania danych. "
+#~ "Przestaje ona istnieć, kiedy zostaje dezaktywowana."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Brak (odizolowana sieć)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Jeden lub więcej zaznaczonych woluminów jest używanych przez domeny. "
+#~ "Należy najpierw odłączyć dyski, aby umożliwić usuwanie woluminów."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Można zmieniać tylko przy wyłączonym gościu"
+
+#~ msgid "Open"
+#~ msgstr "Otwórz"
+
+#~ msgid "Operating system"
+#~ msgstr "System operacyjny"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "Działanie jest wykonywane"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "Migawka nadrzędna"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Ścieżka w systemie plików gospodarza"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Ścieżka do pliku ISO w systemie plików gospodarza"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Ścieżka do pliku w systemie plików gospodarza"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Ścieżka do pliku w systemie plików gospodarza"
+
+#~ msgid "Paused"
+#~ msgstr "Wstrzymane"
+
+#~ msgid "Persistence"
+#~ msgstr "Trwałość"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Fizyczne urządzenie dyskowe"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Fizyczne urządzenie dyskowe na gospodarzu"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Proszę wybrać pulę urządzeń do przechowywania danych"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "Proszę wybrać wolumin"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Proszę podać nazwę nowego woluminu"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr ""
+#~ "Proszę uruchomić maszynę wirtualną, aby uzyskać dostęp do jej konsoli."
+
+#~ msgid "Plug"
+#~ msgstr "Podłącz"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "Pula musi być aktywna, aby utworzyć wolumin"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "Typ puli nie obsługuje tworzenia woluminów"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "Woluminy puli są używane przez maszyny wirtualne "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Preferowana liczba gniazd eksponowanych gościowi."
+
+#~ msgid "Prefix"
+#~ msgstr "Przedrostek"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "Długość przedrostka nie może być pusta"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "Poprzednio wykonane migawki umożliwiają przywrócenie do wcześniejszego "
+#~ "stanu, jeśli coś się nie powiedzie"
+
+#~ msgid "Product"
+#~ msgstr "Produkt"
+
+#~ msgid "Profile"
+#~ msgstr "Profil"
+
+#~ msgid "Protocol"
+#~ msgstr "Protokół"
+
+#~ msgid "Remote URL"
+#~ msgstr "Zdalny adres URL"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "Informacje o zdalnej przeglądarce"
+
+#~ msgid "Revert"
+#~ msgstr "Przywróć"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Przywróć do migawki $0"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "Przywrócenie do tej migawki przeniesie maszynę wirtualną do czasu migawki "
+#~ "i jej obecny stan zostanie utracony, razem ze wszelkimi danymi nieujętymi "
+#~ "w migawce"
+
+#~ msgid "Root password"
+#~ msgstr "Hasło roota"
+
+#~ msgid "Route to $0"
+#~ msgstr "Trasa do $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Uruchom nienadzorowaną instalację"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Uruchamianie po włączeniu gospodarza"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "Port TLS dla SPICE"
+
+#~ msgid "SPICE address"
+#~ msgstr "Adres SPICE"
+
+#~ msgid "SPICE port"
+#~ msgstr "Port SPICE"
+
+#~ msgid "Select console type"
+#~ msgstr "Wybierz typ konsoli"
+
+#~ msgid "Send key"
+#~ msgstr "Wyślij klawisz"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Wyślij niemaskowalne przerwanie"
+
+#~ msgid "Serial console"
+#~ msgstr "Konsola szeregowa"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Ustaw zakres DHCP"
+
+#~ msgid "Set manually"
+#~ msgstr "Ustaw ręcznie"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "Ustawienie haseł użytkowników dla nienadzorowanych instalacji wymaga "
+#~ "uruchomienia maszyny wirtualnej podczas jej tworzenia"
+
+#~ msgid "Show additional options"
+#~ msgstr "Wyświetl dodatkowe opcje"
+
+#~ msgid "Shut off"
+#~ msgstr "Wyłącz"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr ""
+#~ "Należy wyłączyć maszynę wirtualną, aby zmodyfikować konfigurację "
+#~ "oprogramowania sprzętowego"
+
+#~ msgid "Shutting down"
+#~ msgstr "Wyłączanie"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Utworzenie migawki się nie powiodło"
+
+#~ msgid "Source format"
+#~ msgstr "Format źródłowy"
+
+#~ msgid "Source path"
+#~ msgstr "Ścieżka źródłowa"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Ścieżka źródłowa nie może być pusta"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "Źródło musi zaczynać się od protokołu http, ftp lub nfs"
+
+#~ msgid "Source volume group"
+#~ msgstr "Źródłowa grupa woluminów"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Uruchom usługę libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Włączanie puli po uruchomieniu gospodarza"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Początek nie może być pusty"
+
+#~ msgid "Startup"
+#~ msgstr "Uruchamianie"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr ""
+#~ "Aktywacja puli urządzeń do przechowywania danych $0 się nie powiodła"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr ""
+#~ "Dezaktywacja puli urządzeń do przechowywania danych $0 się nie powiodła"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Utworzenie puli urządzeń do przechowywania danych się nie powiodło"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Nazwa puli urządzeń do przechowywania danych"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "Rozmiar urządzenia do przechowywania danych nie może wynosić 0"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr ""
+#~ "Rozmiar woluminu urządzeń do przechowywania danych nie może przekraczać "
+#~ "pojemności puli urządzeń do przechowywania danych ($0 $1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Nie można usunąć woluminów urządzeń do przechowywania danych"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "uśpione (zarządzanie zasilaniem)"
+
+#~ msgid "Target path"
+#~ msgstr "Ścieżka docelowa"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Ścieżka docelowa nie może być pusta"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr ""
+#~ "Maszyna wirtualna jest uruchomiona i przed jej usunięciem zostanie "
+#~ "wymuszone jej wyłączenie."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "Maszyna wirtualna musi być uruchomiona lub wyłączona, aby odłączyć to "
+#~ "urządzenie"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "Eksportowany katalog na serwerze"
+
+#~ msgid "The pool is empty"
+#~ msgstr "Pula jest pusta"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr "Wybrany system operacyjny nie obsługuje nienadzorowanej instalacji"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr "Wybrany system operacyjny wymaga co najmniej $0 $1 pamięci"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "Wybrany system operacyjny wymaga urządzenia do przechowywania danych "
+#~ "o rozmiarze co najmniej $0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "Nie można usunąć puli urządzeń do przechowywania danych"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr ""
+#~ "Ta maszyna wirtualna jest tymczasowa. Wyłączenie spowoduje jej usunięcie."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Ten wolumin jest już używany przez: "
+
+#~ msgid "Threads per core"
+#~ msgstr "Wątki na rdzeń"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr ""
+#~ "Tymczasowe maszyny wirtualne nie obsługują modyfikowania konfiguracji "
+#~ "oprogramowania sprzętowego"
+
+#~ msgid "Type ID"
+#~ msgstr "Identyfikator typu"
+
+#~ msgid "Unique name"
+#~ msgstr "Unikalna nazwa"
+
+#~ msgid "Unique network name"
+#~ msgstr "Unikalna nazwa sieci"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Nieznane oprogramowanie sprzętowe"
+
+#~ msgid "Unplug"
+#~ msgstr "Odłącz"
+
+#~ msgid "Up to $0 $1 available on the default location"
+#~ msgstr "Do $0 $1 dostępnej w domyślnym położeniu"
+
+#~ msgid "Up to $0 $1 available on the host"
+#~ msgstr "Do $0 $1 dostępnej na gospodarzu"
+
+#~ msgid "Url"
+#~ msgstr "URL"
+
+#~ msgid "User login must not be empty when user password is set"
+#~ msgstr ""
+#~ "Login użytkownika nie może być pusty, kiedy hasło użytkownika jest "
+#~ "ustawione"
+
+#~ msgid "User password must not be empty when user login is set"
+#~ msgstr ""
+#~ "Hasło użytkownika nie może być puste, kiedy login użytkownika jest "
+#~ "ustawiony"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "Nie można zapisać ustawień wirtualnego procesora"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "Maszyna wirtualna $0 już istnieje"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "Maszyna wirtualna $0 nie istnieje na połączeniu $1"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr ""
+#~ "Wymuszenie ponownego uruchomienia maszyny wirtualnej $0 się nie powiodło"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "Wymuszenie wyłączenia maszyny wirtualnej $0 się nie powiodło"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "Usunięcie maszyny wirtualnej $0 się nie powiodło"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "Zainstalowanie maszyny wirtualnej $0 się nie powiodło"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "Ponowne uruchomienie maszyny wirtualnej $0 się nie powiodło"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "Wznowienie maszyny wirtualnej $0 się nie powiodło"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "Wysłanie NMI w maszynie wirtualnej $0 się nie powiodło"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "Wyłączenie maszyny wirtualnej $0 się nie powiodło"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "Uruchomienie maszyny wirtualnej $0 się nie powiodło"
+
+#~ msgid "VM state"
+#~ msgstr "Stan maszyny wirtualnej"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "Port TLS dla VNC"
+
+#~ msgid "VNC address"
+#~ msgstr "Adres VNC"
+
+#~ msgid "VNC console"
+#~ msgstr "Konsola VNC"
+
+#~ msgid "VNC port"
+#~ msgstr "Port VNC"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Maszyny wirtualne"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Maszyny wirtualne"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Zarządzanie maszynami wirtualnymi"
+
+#~ msgid "Virtual network"
+#~ msgstr "Sieć wirtualna"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Utworzenie sieci wirtualnej się nie powiodło"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Usługa wirtualizacji (libvirt) nie jest aktywna"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Nazwa grupy woluminów nie może być pusta"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Zapisywalny"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Zapisywalny i udostępniany"
+
+#~ msgid "Yesterday"
+#~ msgstr "Wczoraj"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "Należy wybrać najlepiej pasujący system operacyjny"
+
+#~ msgid "cdrom"
+#~ msgstr "CD-ROM"
+
+#~ msgid "disabled"
+#~ msgstr "wyłączone"
+
+#~ msgid "down"
+#~ msgstr "w dół"
+
+#~ msgid "enabled"
+#~ msgstr "włączone"
+
+#~ msgid "ethernet"
+#~ msgstr "Ethernet"
+
+#~ msgid "host device"
+#~ msgstr "urządzenie gospodarza"
+
+#~ msgid "host passthrough"
+#~ msgstr "przejście gospodarza"
+
+#~ msgid "hostdev"
+#~ msgstr "urządzenie gospodarza"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "Bezpośredni cel iSCSI"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "Inicjator IQN iSCSI"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "IQN celu iSCSI"
+
+#~ msgid "iso"
+#~ msgstr "ISO"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "multicast"
+
+#~ msgid "no"
+#~ msgstr "nie"
+
+#~ msgid "no state saved"
+#~ msgstr "nie zapisano żadnego stanu"
+
+#~ msgid "pxe"
+#~ msgstr "PXE"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "QEMU"
+
+#~ msgid "redirected device"
+#~ msgstr "przekierowane urządzenie"
+
+#~ msgid "server"
+#~ msgstr "serwer"
+
+#~ msgid "up"
+#~ msgstr "w górę"
+
+#~ msgid "vCPU count"
+#~ msgstr "Liczba wirtualnych procesorów"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "Maksymalna liczba wirtualnych procesorów"
+
+#~ msgid "vCPUs"
+#~ msgstr "Wirtualne procesory"
+
+#~ msgid "vhostuser"
+#~ msgstr "użytkownik gospodarza wirtualizacji"
+
+#~ msgid "view more..."
+#~ msgstr "wyświetl więcej…"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Aby klonować maszyny wirtualne, w systemie musi być zainstalowany pakiet "
+#~ "virt-install"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Aby tworzyć nowe maszyny wirtualne, w systemie musi być zainstalowany "
+#~ "pakiet virt-install"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Aby modyfikować ten atrybut, w systemie musi być zainstalowany pakiet "
+#~ "virt-install"
+
+#~ msgid "vm"
+#~ msgstr "maszyna wirtualna"
+
+#~ msgid "Local install media"
+#~ msgstr "Lokalny nośnik instalacji"
+
+#~ msgid "URL"
+#~ msgstr "Adres URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "Proszę ustawić hasło roota"
+
+#~ msgid "21st"
+#~ msgstr "21."
+
+#~ msgid "22nd"
+#~ msgstr "22."
+
+#~ msgid "23rd"
+#~ msgstr "23."
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Po"
+
+#~ msgid "Friday"
+#~ msgstr "piątek"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Godzina : Minuta"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "Godzina musi być liczbą między 0 a 23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Nieprawidłowy format daty."
+
+#~ msgid "Invalid number."
+#~ msgstr "Nieprawidłowy numer."
+
+#~ msgid "Monday"
+#~ msgstr "poniedziałek"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Powtarzanie co godzinę"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Powtarzanie co roku"
+
+#~ msgid "Saturday"
+#~ msgstr "sobota"
+
+#~ msgid "Sunday"
+#~ msgstr "niedziela"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Ten dzień nie istnieje we wszystkich miesiącach.<br> Licznik będzie "
+#~ "wykonywany tylko w miesiącach z 31. dniem."
+
+#~ msgid "Thursday"
+#~ msgstr "czwartek"
+
+#~ msgid "Tuesday"
+#~ msgstr "wtorek"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 aktualizacja"
+#~ msgstr[1] "$0 aktualizacje"
+#~ msgstr[2] "$0 aktualizacji"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 poprawka zabezpieczeń"
+#~ msgstr[1] "$1 poprawki zabezpieczeń"
+#~ msgstr[2] "$1 poprawek zabezpieczeń"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Zarządzanie urządzeniami do przechowywania danych"
+
+#~ msgid "Restart now"
+#~ msgstr "Uruchom ponownie teraz"
+
+#~ msgid "Configure"
+#~ msgstr "Skonfiguruj"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr ""
+#~ "Uruchamianie sieciowe jest dostępne tylko podczas używania połączenia "
+#~ "systemowego"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Sieć"
+
+#~ msgid "No snapshots"
+#~ msgstr "Brak migawek"
+
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "Nie odnaleziono takiego pliku w katalogu „$0”"
+
+#~ msgid "No updates pending"
+#~ msgstr "Brak oczekujących aktualizacji"
+
+#~ msgid "This directory is empty"
+#~ msgstr "Ten katalog jest pusty"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr ""
+#~ "Ten typ puli nie obsługuje tworzenia woluminów do przechowywania danych"
+
+#~ msgid "undefined"
+#~ msgstr "nieokreślone"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Niepoprawny klucz komputera"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "Nie można ustalić autentyczności komputera {{#strong}}{{host}}{{/"
+#~ "strong}}. Na pewno kontynuować łączenie?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "Klucz {{#strong}}{{host}}{{/strong}} nie pasuje do poprzednio używanego "
+#~ "klucza. Jeśli ten komputer nie został niedawno wymieniany, może to "
+#~ "oznaczać próbę ataku na połączenie z tym komputerem."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Nieznany klucz komputera"
+
+#~ msgid "Address:"
+#~ msgstr "Adres:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Wymagane są co najmniej $0 dyski."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "Połącz z dowolną przeglądarką SPICE lub VNC."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Pobierz plik MSI z $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Konsola graficzna (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Konsola graficzna w przeglądarce pulpitu"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Dla tej maszyny wirtualnej nie określono żadnej konsoli."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid ""
+#~ "For the host, either specify the hostname, IP address, an alias name or a "
+#~ "unique resource identifier for the SSH destination."
+#~ msgstr ""
+#~ "Dla gospodarza należy podać nazwę komputera, adres IP, nazwę-alias lub "
+#~ "unikalny identyfikator zasobu dla docelowego SSH."
+
+#~ msgid ""
+#~ "Specify the host and the login user account for the host that you want to "
+#~ "add."
+#~ msgstr ""
+#~ "Należy podać konta użytkowników gospodarza i logowania dla dodawanego "
+#~ "gospodarza."
+
+#~ msgid "Entry"
+#~ msgstr "Wpis"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Przeglądarka się rozłączy, ale nie wpłynie to na proces aktualizacji. "
+#~ "Można połączyć się ponownie za kilka chwil, aby kontynuować obserwację "
+#~ "procesu."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "i automatycznie uruchom komputer ponownie."
+
+#~ msgid "Dashboard"
+#~ msgstr "Panel kontrolny"
+
+#~ msgid "Description input text"
+#~ msgstr "Wprowadzanie tekstu opisu"
+
+#~ msgid "FAILED"
+#~ msgstr "NIEPOWODZENIE"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Utracono połączenie. Próbowanie ponownego połączenia"
+
+#~ msgid "Name input text"
+#~ msgstr "Wprowadzanie tekstu nazwy"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Sieć"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Proszę podać rozmiar nowego woluminu"
+
+#~ msgid "Servers"
+#~ msgstr "Serwery"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr "Obecnie połączono bezpośrednio z tym serwerem. Nie można go usunąć."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bity"
+#~ msgstr[2] "$0 bitów"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 bajt"
+#~ msgstr[1] "$0 bajty"
+#~ msgstr[2] "$0 bajtów"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 bajt"
+#~ msgstr[1] "$0 bajty"
+#~ msgstr[2] "$0 bajtów"
+
+#~ msgid "Bond settings "
+#~ msgstr "Ustawienia wiązania "
+
+#~ msgid "IP settings "
+#~ msgstr "Ustawienia IP "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "—"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Wystąpił nieoczekiwany wewnętrzny błąd w programie Cockpit. <br/><br/"
+#~ ">Można spróbować ponownie uruchomić program Cockpit przez odświeżenie "
+#~ "strony w przeglądarce. Konsola języka JavaScript zawiera informacje o tym "
+#~ "błędzie (<b>Ctrl-Shift-J</b> w większości przeglądarek)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "Maszyna wirtualna uległa awarii."
+
+#~ msgid "The VM is down."
+#~ msgstr "Maszyna wirtualna jest wyłączona."
+
+#~ msgid "The VM is going down."
+#~ msgstr "Maszyna wirtualna jest wyłączana."
+
+#~ msgid "The VM is idle."
+#~ msgstr "Maszyna wirtualna jest bezczynna."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr ""
+#~ "Maszyna wirtualna jest w trakcie umierania (wyłączanie lub nie ukończono "
+#~ "awarii)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "Maszyna wirtualna jest wstrzymana."
+
+#~ msgid "The VM is running."
+#~ msgstr "Maszyna wirtualna jest uruchomiona."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "Maszyna wirtualna jest uśpiona przez zarządzanie zasilaniem."
+
+#~ msgid "inactive"
+#~ msgstr "nieaktywne"
+
+#~ msgid "Avatar"
+#~ msgstr "Awatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Pozostawienie pustego pola spowoduje łączenie z tą maszyną jako obecnie "
+#~ "zalogowany użytkownik. Jeśli zostanie podana inna nazwa użytkownika, to "
+#~ "będzie ona zawsze używana podczas łączenia z tą maszyną."
+
+#~ msgid "2 min"
+#~ msgstr "2 minuty"
+
+#~ msgid "3 min"
+#~ msgstr "3 minuty"
+
+#~ msgid "4 min"
+#~ msgstr "4 minuty"
+
+#~ msgid "CPU graph"
+#~ msgstr "Wykres procesora"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "Stan procesora"
+
+#~ msgid "Cached"
+#~ msgstr "W pamięci podręcznej"
+
+#~ msgid "Graphs"
+#~ msgstr "Wykresy"
+
+#~ msgid "I/O wait"
+#~ msgstr "Oczekiwanie wejścia/wyjścia"
+
+#~ msgid "Kernel"
+#~ msgstr "Jądro"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Pamięć"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "Użycie pamięci i przestrzeni wymiany"
+
+#~ msgid "Memory graph"
+#~ msgstr "Wykres pamięci"
+
+#~ msgid "Network traffic"
+#~ msgstr "Ruch sieciowy"
+
+#~ msgid "Nice"
+#~ msgstr "Nice"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Zsynchronizuj użytkowników"
+
+#~ msgid "Usage graphs"
+#~ msgstr "Wykresy użycia"
+
+#~ msgid "View graphs"
+#~ msgstr "Wyświetl wykresy"
+
+#~ msgid "idle"
+#~ msgstr "bezczynne"
+
+#~ msgid "paused"
+#~ msgstr "wstrzymane"
+
+#~ msgid "running"
+#~ msgstr "działanie"
+
+#~ msgid "shut off"
+#~ msgstr "wyłącz"
+
+#~ msgid "shutdown"
+#~ msgstr "wyłącz"
+
+#~ msgid "Add a new host"
+#~ msgstr "Dodaj nowego gospodarza"
+
+#~ msgid "Available"
+#~ msgstr "Dostępne"
+
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Adres IP lub nazwa komputera"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Interfejsy sieciowe"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Ta wersja cockpit-ws nie obsługuje łączenia z komputerem z alternatywnym "
+#~ "użytkownikiem lub portem"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Należy zaktualizować cockpit-ws do nowszej wersji, aby spróbować innego "
+#~ "portu."
+
+#~ msgid "$0 template"
+#~ msgstr "Szablon $0"
+
+#~ msgid "Instance of template: "
+#~ msgstr "Wystąpienie szablonu: "
+
+#~ msgid "Instantiate"
+#~ msgstr "Wystąpienie"
+
+#~ msgid "$0 shares"
+#~ msgstr "Udziały: $0"
+
+#~ msgid "All In One"
+#~ msgstr "Zintegrowane"
+
+#~ msgid "Always"
+#~ msgstr "Zawsze"
+
+#~ msgid "Author"
+#~ msgstr "Autor"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Przekazano błędne dane dla mechanizmu passwd1"
+
+#~ msgid "CPU priority"
+#~ msgstr "Priorytet procesora"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Nie można połączyć z Dockerem"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Zmień ograniczenia zasobów"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Zmień ograniczenia zasobów"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit nie może zalogować się do {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit nie może zalogować się do {{#strong}}{{host}}{{/strong}}. Poniżej "
+#~ "można zmienić dane uwierzytelniające. {{#can_sync}}Można też "
+#~ "{{#sync_link}}synchronizować konta i hasła{{/sync_link}}.{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Łączne użycie pamięci"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Łączne użycie $0 rdzenia procesora"
+#~ msgstr[1] "Łączne użycie $0 rdzeni procesora"
+#~ msgstr[2] "Łączne użycie $0 rdzeni procesora"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "Polecenie nie może być puste"
+
+#~ msgid "Command:"
+#~ msgstr "Polecenie:"
+
+#~ msgid "Commit"
+#~ msgstr "Zatwierdź"
+
+#~ msgid "Commit image"
+#~ msgstr "Zatwierdź obraz"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Łączenie z Dockerem"
+
+#~ msgid "Container name"
+#~ msgstr "Nazwa kontenera"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Kontener jest obecnie oznaczony jako działający, ale zwykłe zatrzymanie "
+#~ "się nie powiodło."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Kontener jest obecnie uruchomiony."
+
+#~ msgid "Container:"
+#~ msgstr "Kontener:"
+
+#~ msgid "Containers"
+#~ msgstr "Kontenery"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Kontenery"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Nie można zmienić grup użytkownika"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Nie można zmienić hasła użytkownika"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Nie można połączyć z komputerem"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Nie można utworzyć nowych użytkowników"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Nie można wyświetlić listy lokalnych użytkowników"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Nie można wyświetlić listy użytkowników"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Nie można wczytać danych użytkownika"
+
+#~ msgid "Created:"
+#~ msgstr "Utworzono:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Kontenery Docker"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker nie jest zainstalowany lub aktywowany w tym systemie"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Podwójny alias"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Podwójny port"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "Proszę podać hasło, aby dodać tożsamość do usługi ssh-agent"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Wpisanie tutaj innego hasła oznacza, że będzie wymagane jego wpisanie za "
+#~ "każdym połączeniem do tej maszyny"
+
+#~ msgid "Environment"
+#~ msgstr "Środowisko"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Błąd podczas wczytywania użytkowników: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Komunikat o błędzie Dockera:"
+
+#~ msgid "Everything"
+#~ msgstr "Wszystko"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Zakończono $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Eksponuj porty kontenera"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Uruchomienie Dockera się nie powiodło: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Zatrzymanie zakresu Dockera się nie powiodło: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Pobierz nowy obraz"
+
+#~ msgid "IP address:"
+#~ msgstr "Adres IP:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "Długość przedrostka IP:"
+
+#~ msgid "Id"
+#~ msgstr "Identyfikator"
+
+#~ msgid "Id:"
+#~ msgstr "Identyfikator:"
+
+#~ msgid "Image"
+#~ msgstr "Obraz"
+
+#~ msgid "Image $0"
+#~ msgstr "Obraz $0"
+
+#~ msgid "Image search"
+#~ msgstr "Wyszukiwanie obrazów"
+
+#~ msgid "Image:"
+#~ msgstr "Obraz:"
+
+#~ msgid "Images"
+#~ msgstr "Obrazy"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Obrazy"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Obrazy i uruchomione kontenery"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Aby synchronizować użytkowników, należy się zalogować w {{#strong}}"
+#~ "{{host}}{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Nieprawidłowy port"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Zgłoszenie Kerberos"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Pozostawienie pustego pola spowoduje łączenie z tą maszyną jako obecnie "
+#~ "zalogowany użytkownik {{#default_user}}({{default_user}}){{/"
+#~ "default_user}}. Jeśli zostanie podana inna nazwa użytkownika, to będzie "
+#~ "ona zawsze używana podczas łączenia z tą maszyną."
+
+#~ msgid "Link to another container"
+#~ msgstr "Odnośnik do innego kontenera"
+
+#~ msgid "Links:"
+#~ msgstr "Odnośniki:"
+
+#~ msgid "Login Password"
+#~ msgstr "Hasło logowania"
+
+#~ msgid "MAC address:"
+#~ msgstr "Adres MAC:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Ograniczenie pamięci"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Zamontuj woluminy kontenera"
+
+#~ msgid "No container specified"
+#~ msgstr "Nie podano kontenera"
+
+#~ msgid "No containers"
+#~ msgstr "Brak kontenerów"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Brak kontenerów pasujących do obecnego filtru"
+
+#~ msgid "No images"
+#~ msgstr "Brak obrazów"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Brak obrazów pasujących do obecnego filtru"
+
+#~ msgid "No results for $0"
+#~ msgstr "Brak wyników dla $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Brak obrazów pasujących do obecnego filtru"
+
+#~ msgid "No running containers"
+#~ msgstr "Brak uruchomionych kontenerów"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Brak uruchomionych kontenerów pasujących do obecnego filtru"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Brak upoważnienia do łączenia z Dockerem w tym systemie"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "Po niepowodzeniu próbowanie ponownie $0 raz"
+#~ msgstr[1] "Po niepowodzeniu próbowanie ponownie $0 razy"
+#~ msgstr[2] "Po niepowodzeniu próbowanie ponownie $0 razy"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Proszę potwierdzić wymuszenie usunięcia $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Proszę spróbować innego terminu"
+
+#~ msgid "Ports:"
+#~ msgstr "Porty:"
+
+#~ msgid "Problems"
+#~ msgstr "Problemy"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Tylko do odczytu"
+
+#~ msgid "ReadWrite"
+#~ msgstr "Odczyt i zapis"
+
+#~ msgid "Repository"
+#~ msgstr "Repozytorium"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Zasady ponownego uruchamiania:"
+
+#~ msgid "Retries:"
+#~ msgstr "Próby:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Użycie istniejącego hasła do zdalnych połączeń"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Proszę wybrać użytkowników do synchronizacji z {{#strong}}{{host}}{{/"
+#~ "strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Ustaw zmienne środowiskowe kontenera"
+
+#~ msgid "Severity:"
+#~ msgstr "Ważność:"
+
+#~ msgid "Show all containers"
+#~ msgstr "Wyświetl wszystkie kontenery"
+
+#~ msgid "Start Docker"
+#~ msgstr "Uruchom Dockera"
+
+#~ msgid "State:"
+#~ msgstr "Stan:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Zsynchronizuj"
+
+#~ msgid "Tag"
+#~ msgstr "Etykieta"
+
+#~ msgid "Tags"
+#~ msgstr "Etykiety"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "Poniższe kontenery zależą od tego obrazu i nie będzie już można ich "
+#~ "używać."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Ten obraz nie istnieje."
+
+#~ msgid "Type a password"
+#~ msgstr "Proszę wpisać hasło"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Filtrowanie…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "Do zatrzymania"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Nieobsługiwany mechanizm ustawiania"
+
+#~ msgid "Up since $0"
+#~ msgstr "Działa od $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Używane przez kontenery"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Używanie dostępnych danych uwierzytelniania"
+
+#~ msgid "Volumes"
+#~ msgstr "Woluminy"
+
+#~ msgid "Volumes:"
+#~ msgstr "Woluminy:"
+
+#~ msgid "With terminal"
+#~ msgstr "Z terminalem"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Połączono z {{#strong}}{{host}}{{/strong}}, jednakże do synchronizacji "
+#~ "użytkowników wymagane są uprawnienia superużytkownika."
+
+#~ msgid "default"
+#~ msgstr "domyślnie"
+
+#~ msgid "key"
+#~ msgstr "klucz"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "wyszukiwanie według nazwy, przestrzeni nazw lub opisu"
+
+#~ msgid "shares"
+#~ msgstr "udziały"
+
+#~ msgid "to host port"
+#~ msgstr "do portu gospodarza"
+
+#~ msgid "value"
+#~ msgstr "wartość"
+
+#~ msgid "Master"
+#~ msgstr "Główne"
+
+#~ msgid "Password for $0"
+#~ msgstr "Hasło dla $0"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "Użytkownik <b>$0</b> nie ma zezwolenia na zarządzanie serwerami"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Konta"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "Użytkownik <b>$0</b> nie ma zezwolenia na modyfikowanie kont"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Nie można usunąć konta roota"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Nie można zmienić nazwy konta roota"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "Brak upoważnienia do dodania nowej strefy"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "Brak upoważnienia do dodania usług do strefy $0"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "Brak upoważnienia do usunięcia usługi $0"
+
+#~ msgid "Account Settings"
+#~ msgstr "Ustawienia konta"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Dodaj komputer do panelu kontrolnego"
+
+#~ msgid "Machines"
+#~ msgstr "Komputery"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "Login uzyskał uprawnienia administratora"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Hasło nieodpowiednie dla zadań wymagających uprawnień lub do łączenia "
+#~ "z innymi komputerami"
+
+#~ msgid "Privileged"
+#~ msgstr "Uprawnione"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Użycie istniejącego hasła do zadań wymagających uprawnień i łączenia "
+#~ "z innymi komputerami"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr "Użytkownik $0 nie ma zezwolenia na modyfikowanie nazw komputerów"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr ""
+#~ "Użytkownik $0 nie ma zezwolenia na wyłączanie lub ponowne uruchamianie "
+#~ "tego serwera"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "Użytkownik <b>$0</b> nie ma zezwolenia na zmianę profili"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr ""
+#~ "Użytkownik <b>$0</b> nie ma zezwolenia na zarządzanie urządzeniami do "
+#~ "przechowywania danych"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr ""
+#~ "Użytkownik <b>$0</b> nie ma zezwolenia na modyfikowanie ustawień sieci"
+
+#~ msgid "Required by $0"
+#~ msgstr "Wymagane przez $0"
+
+#~ msgid "Automatic Startup"
+#~ msgstr "Automatyczne uruchamianie"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Ostatnio wyzwolono"
+
+#~ msgid "Next Run"
+#~ msgstr "Następne uruchomienie"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "Użytkownik <b>$0</b> nie ma uprawnień do tworzenia liczników"
+
+#~ msgid "Connecting"
+#~ msgstr "Łączenie"
+
+#~ msgid "About Cockpit"
+#~ msgstr "O programie Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (współdzielone z systemem operacyjnym)"
+
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 zagrożenie"
+#~ msgstr[1] "$0 zagrożenia"
+#~ msgstr[2] "$0 zagrożeń"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Dodaj dodatkowe urządzenia do przechowywania danych"
+
+#~ msgid "Add Storage"
+#~ msgstr "Dodaj urządzenia do przechowywania danych"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Dodatkowe urządzenia do przechowywania danych"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Wszystkie dane na wybranych dyskach zostaną usunięte, a dyski zostaną "
+#~ "dodane do puli urządzeń do przechowywania danych."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Skonfiguruj urządzenia do przechowywania danych…"
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Nie można dodać wszystkich dysków"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Usuń kontenery i przywróć pulę urządzeń do przechowywania danych"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr ""
+#~ "Informacje o puli urządzeń do przechowywania danych Docker są niedostępne."
+
+#~ msgid "Local Disks"
+#~ msgstr "Lokalne dyski"
+
+#~ msgid "No additional local storage found."
+#~ msgstr ""
+#~ "Nie odnaleziono dodatkowych lokalnych urządzeń do przechowywania danych."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Sformatuj ponownie i dodaj dyski"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "Przywrócenie puli urządzeń do przechowywania danych usunie wszystkie "
+#~ "kontenery i zwolni dyski w puli."
+
+#~ msgid "Security"
+#~ msgstr "Zabezpieczenia"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr ""
+#~ "Pula urządzeń do przechowywania danych Docker nie może być zarządzana na "
+#~ "tym systemie."
+
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "Wyszukiwanie z $time ($type) znalazło $count zagrożenie:"
+#~ msgstr[1] "Wyszukiwanie z $time ($type) znalazło $count zagrożenia:"
+#~ msgstr[2] "Wyszukiwanie z $time ($type) znalazło $count zagrożeń:"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "Wyszukiwanie z $time ($type) nie zwróciło żadnych zagrożeń."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "Wyszukiwanie z $time ($type) się nie powiodło."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Usługa wirtualizacji jest dostępna"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr ""
+#~ "Brak uprawnień do zrządzania pulą urządzeń do przechowywania danych "
+#~ "Docker."
+
+#~ msgid "$0 GiB / $1 GiB"
+#~ msgstr "$0 GiB/$1 GiB"
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} → $cport"
+
+#~ msgid "Hide Performance Options"
+#~ msgstr "Ukryj opcje wydajności"
+
+#~ msgid "Show Performance Options"
+#~ msgstr "Wyświetl opcje wydajności"
diff --git a/po/pt_BR.po b/po/pt_BR.po
new file mode 100644
index 0000000..4f94e7c
--- /dev/null
+++ b/po/pt_BR.po
@@ -0,0 +1,13318 @@
+# Bruno R. Zanuzzo <xmrbrz@fedoraproject.org>, 2015. #zanata
+# Luis Carlos Peters Motta <luiscpmotta@gmail.com>, 2015. #zanata
+# Daniel Lara <danniel@fedoraproject.org>, 2016. #zanata
+# Lucas Landim Pinheiro <detona.headmetal@gmail.com>, 2016. #zanata, 2020.
+# Daniel Lara <danniel@fedoraproject.org>, 2017. #zanata
+# Mateus de Melo Santos <mateusms@tutanota.com>, 2017. #zanata
+# Daniel Lara <danniel@fedoraproject.org>, 2018. #zanata
+# Ludek Janda <ljanda@redhat.com>, 2018. #zanata
+# Luis Carlos Peters Motta <luiscpmotta@gmail.com>, 2018. #zanata
+# Luiz Fernando Pereira <luizfernandopereira@outlook.com.br>, 2018. #zanata
+# cockpit <cockpituous@gmail.com>, 2018. #zanata
+# Leonardo Bressan Motyczka <leomoty@gmail.com>, 2019. #zanata
+# Mateus de Melo Santos <mateusms@tutanota.com>, 2019. #zanata
+# cockpit <cockpituous@gmail.com>, 2019. #zanata
+# Daniel Lara Souza <daniellarasouza@yahoo.com.br>, 2020.
+# Paulo Eduardo Faversani <pauloeduardo2709@gmail.com>, 2020.
+# Anonymous <noreply@weblate.org>, 2020.
+# Fábio Rodrigues Ribeiro <farribeiro@gmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-09-15 20:35+0000\n"
+"Last-Translator: Fábio Rodrigues Ribeiro <farribeiro@gmail.com>\n"
+"Language-Team: Portuguese (Brazil) <https://translate.fedoraproject.org/"
+"projects/cockpit/main/pt_BR/>\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"X-Generator: Weblate 5.0.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "# de usuários"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+msgstr[1] "$0 CPUs"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 zona Ativa"
+msgstr[1] "$0 zonas ativas"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 disponível"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "$0 sistemas de arquivos não podem ser maiores."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "$0 sistemas de arquivos não podem ser menores."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "$0 sistemas de arquivos não podem ser redimensionados aqui."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "$0 sistemas de arquivos não podem ser redimensionados aqui."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 Tamanho do Bloco"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+#, fuzzy
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 Problema Crítico"
+msgstr[1] "$0 Problemas, incluindo críticos"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 dados + $1 sobrecarga usada de $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 dia"
+msgstr[1] "$0 dias"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disco não encontrado"
+msgstr[1] "$0 discos não encontrados"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disco não encontrado"
+msgstr[1] "$0 discos não encontrados"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 Discos"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 documentação"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 erro"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 saiu com o código $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 falhou"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "Tentativa de login falhou para o usuário $0"
+msgstr[1] "Tentativas de login falharam para o usuário $0"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "$0 sistema de arquivos"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 livre"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 hora"
+msgstr[1] "$0 horas"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+#, fuzzy
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 hit importantes"
+msgstr[1] "$0 hits, incluindo importantes"
+
+#: pkg/users/account-create-dialog.js:378
+#, fuzzy
+#| msgid "$0 disk is missing"
+#| msgid_plural "$0 disks are missing"
+msgid "$0 is an existing file"
+msgstr "$0 disco não encontrado"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 está em uso"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 não está disponível em nenhum repositório."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 chave alterada"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 mortos com sinal $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 hit de baixa gravidade"
+msgstr[1] "$0 hits de baixas gravidades"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minuto"
+msgstr[1] "$0 minutos"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+#, fuzzy
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 mês"
+msgstr[1] "$0 meses"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 mês"
+msgstr[1] "$0 meses"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 mais..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 pacote"
+msgstr[1] "$0 pacotes"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 pacote precisa de uma reinicialização do sistema"
+msgstr[1] "$0 pacotes precisam de uma reinicialização do sistema"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 página"
+msgstr[1] "$0 páginas"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "partição"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 correção de segurança disponível"
+msgstr[1] "$0 correções de segurança disponíveis"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 serviço falhou"
+msgstr[1] "$0 serviços falharam"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 serviço precisa ser reiniciado"
+msgstr[1] "$0 serviços precisam ser reiniciados"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 slot restante"
+msgstr[1] "$0 slots restantes"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 spike"
+msgstr[1] "$0 spikes"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+#, fuzzy
+#| msgid "Not synchronized"
+msgid "$0 synchronized"
+msgstr "Não sincronizado"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 total"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 atualização disponível"
+msgstr[1] "$0 atualizações disponíveis"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 usados of $1 ($2 salvos)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 semana"
+msgstr[1] "$0 semanas"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 será instalado."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 ano"
+msgstr[1] "$0 anos"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 zona"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr ""
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$nome(vindo de $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Criando partição $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Ponto de montagem local"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Não montado"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(recomendado)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", incluindo $1 correção de segurança"
+msgstr[1] ", incluindo $1 correções de segurança"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 dia"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 hora"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 min"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 Minuto"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 semana"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10º"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11º"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12º"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13º"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14º"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 min"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15º"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16º"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17º"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18º"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19º"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1º"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 Minutos"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20º"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21º"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22º"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23º"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24º"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25º"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26º"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27º"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28º"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29º"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2º"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30º"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31º"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3º"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 Minutos"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4º"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 min"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minutos"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5º"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 horas"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 Minutos"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6º"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7º"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8º"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9º"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Uma versão compatível do Cockpit não está instalada em $0."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "Um sistema de arquivos com esse nome já existe nessa pool."
+
+#: pkg/users/group-create-dialog.js:71
+#, fuzzy
+#| msgid "This user name already exists"
+msgid "A group with this name already exists"
+msgstr "Este usuário já existe"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Um navegador moderno é necessário para segurança, confiabilidade e "
+"desempenho."
+
+#: pkg/networkmanager/bond.jsx:142
+#, fuzzy
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Um vínculo de rede combina várias interfaces de rede em uma interface lógica "
+"com maior taxa de transferência ou redundância."
+
+#: pkg/shell/hosts_dialog.jsx:795
+#, fuzzy
+#| msgid ""
+#| "A new SSH key at ${key} will be created for ${luser} on ${lhost} and it "
+#| "will be added to the ${afile} file of ${ruser} on ${rhost}."
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Uma nova chave SSH em ${key} será criada para ${luser} em ${lhost} e será "
+"adicionada ao arquivo ${afile} de ${ruser} em ${rhost}."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Um pacote precisa de uma reinicialização do sistema para que as atualizações "
+"tenham efeito:"
+msgstr[1] ""
+"Alguns pacotes precisam de uma reinicialização do sistema para que as "
+"atualizações tenham efeito:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Já existe uma pool com esse nome."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"Um serviço precisa ser reiniciado para que as atualizações tenham efeito:"
+msgstr[1] ""
+"Alguns serviços precisam ser reiniciados para que as atualizações tenham "
+"efeito:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "Volumes físicos não podem ser redimensionados aqui."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP Monitoramento"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Sobre o console web"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Ausente"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Aceitar a chave e entrar"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+#, fuzzy
+#| msgid "Set password"
+msgid "Acceptable password"
+msgstr "Definir uma Senha"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Conta expirou"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Conta não disponível ou não pode ser editada."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Contas"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Ação"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Ações"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Ativar"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Ativando $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Ativo"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Backup Ativo"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Páginas Ativas"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Ativo desde "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+#, fuzzy
+#| msgid "Activate"
+msgid "Active state"
+msgstr "Ativar"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Balanceamento de carga dinâmico"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Transmissão dinâmica de balanceamento de carga"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Adicionar"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Adicionar $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+#, fuzzy
+msgid "Add DNS server"
+msgstr "Servidor de chaves Tang"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Adicionar Criptografia de Disco Limitada à Rede"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Adicionar Servidor de chaves Tang"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Adicionar VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+#, fuzzy
+#| msgid "Add VLAN"
+msgid "Add VPN"
+msgstr "Adicionar VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+#, fuzzy
+#| msgid "Add team"
+msgid "Add a bitmap"
+msgstr "Adicionar Equipe"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Adicionar uma nova zona"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+#, fuzzy
+#| msgid "MAC address"
+msgid "Add address"
+msgstr "MAC address"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Add block devices"
+msgstr "$0 Dispositivos de Bloco"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Adicionar Vínculo"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Adicionar Ponte"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Adicionar disco"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Adicionar Discos"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Adicionar Portal iSCSI"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Adicionar chave"
+
+#: pkg/storaged/stratis/pool.jsx:551
+#, fuzzy
+msgid "Add keyserver"
+msgstr "Servidor de chaves Tang"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Adicionar membro"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Adicionar um novo host"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Adicionar nova zona"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "Adicionar Senha"
+
+#: pkg/networkmanager/wireguard.jsx:290
+#, fuzzy
+#| msgid "Add member"
+msgid "Add peer"
+msgstr "Adicionar membro"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "Physical volume"
+msgid "Add physical volume"
+msgstr "Volume Físico"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Adicionar Portas"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Adicionar portas para zona $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Adicionar chave pública"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+#, fuzzy
+#| msgid "Add item"
+msgid "Add route"
+msgstr "Adicionar item"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+#, fuzzy
+#| msgid "DNS search domains"
+msgid "Add search domain"
+msgstr "DNS Busca de Domínios"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Adicionar serviços"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Adicionar serviços para zona $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Adicionar serviços na zona $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Adicionar Equipe"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Adicionar zona"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+#, fuzzy
+#| msgid "Encryption options"
+msgid "Adding \"$0\" to encryption options"
+msgstr "Opções de Criptografia"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Adicionando \"$0\" às opções do sistema de arquivos"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Adicionar $0 irá interromper a conexão com o servidor e tornará a interface "
+"de administração indisponível."
+
+#: pkg/storaged/stratis/pool.jsx:468
+#, fuzzy
+#| msgid ""
+#| "Saving a new passphrase requires unlocking the disk. Please provide a "
+#| "current disk passphrase."
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Salvar uma nova senha requer o desbloqueio do disco. Por favor, forneça uma "
+"senha de disco atual."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Adicionar portas personalizadas recarregará o firewalld. Uma recarga "
+"resultará na perda de qualquer configuração somente em tempo de execução!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Adicionando chave"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Adicionando volume físico a $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Adicionando rd.neednet=1 à linha de comando do kernel"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "DNS Adicional $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Domínios de Pesquisa de DNS Adicionais $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Ações adicionais"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Endereço adicional $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Pacotes adicionais:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Portas adicionais"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Endereço"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Endereço $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "O endereço não pode estar vazio"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "O endereço não é um URL válido"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Endereços"
+
+#: pkg/networkmanager/wireguard.jsx:52
+#, fuzzy
+#| msgid "Address cannot be empty"
+msgid "Addresses are not formatted correctly"
+msgstr "O endereço não pode estar vazio"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Administração com o Console Web do Cockpit"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Acesso administrativo"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr "É necessário acesso administrativo para criar e acessar relatórios."
+
+#: pkg/sosreport/sosreport.jsx:425
+#, fuzzy
+#| msgid "Administrative access"
+msgid "Administrative access required"
+msgstr "Acesso administrativo"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "TCA Avançado"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Após"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Após sair do domínio, apenas os usuários com credenciais locais poderão "
+"fazer login nesta máquina. Isso também pode afetar outros serviços, como as "
+"configurações de resolução DNS e a lista de autoridades de certificação "
+"confiáveis."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Após a inicialização do sistema"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Alerta"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Alerta e acima"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Apelido"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Todos"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Todas as atualizações"
+
+#: pkg/lib/machine-info.js:72
+#, fuzzy
+msgid "All-in-one"
+msgstr "All-in-one"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Permitir a execução (desmascarar)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+#, fuzzy
+#| msgid "Allowed addresses"
+msgid "Allowed IPs"
+msgstr "Endereços permitidos"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Endereços permitidos"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Documentação de papéis do Ansible"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Qualquer sequência de texto nas mensagens de log pode ser filtrada. A "
+"sequência também pode estar na forma de uma expressão regular. Também "
+"suporta filtragem por campos de registro de mensagem. Esses valores são "
+"separados por espaços, no formato CAMPO=VALOR, onde o valor pode ser uma "
+"lista separada por vírgulas de valores possíveis."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Aparência"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr ""
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Aplicações"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Lista de aplicações"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Aplicar e reiniciar"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Aplicar live patches de kernel"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Aplicar essa solução"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Aplicando a nova política... Isso pode levar alguns minutos."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Aplicando solução..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Aplicando atualizações"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "A aplicação de atualizações falhou"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "Apropriado para montagens críticas, como /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Apps"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Avaliação"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Etiqueta de ativo"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+#, fuzzy
+#| msgid "boot"
+msgid "At boot"
+msgstr "inicialização"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Pelo menos $0 disco é necessário."
+msgstr[1] "Pelo menos $0 discos são necessários."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+#, fuzzy
+#| msgid "At least one disk is needed."
+msgid "At least one block device is needed."
+msgstr "Pelo menos um disco é necessário."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Pelo menos um disco é necessário."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "A cada minuto"
+
+#: pkg/systemd/timer-dialog.jsx:260
+#, fuzzy
+#| msgid "Seconds"
+msgid "At second"
+msgstr "Segundos"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "Em tempo específico"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Atributos"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Log de auditoria"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Autenticar"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Autenticando"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Autenticação"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Falha na Autenticação"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Falha na Autenticação: Conexão encerrada com servidor"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Autenticação é necessária para executar ações privilegiadas no Console Web "
+"do Cockpit"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Autenticação requerida"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Autorizar chave SSH"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Chaves Públicas Autorizadas de SSH"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automático"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automático (apenas DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+#, fuzzy
+msgid "Automatic login"
+msgstr "Automático"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Atualizações automáticas"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Inicia automaticamente"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Usando automaticamente o NTP"
+
+#: pkg/lib/serverTime.js:570
+#, fuzzy
+#| msgid "Automatically using specific NTP servers"
+msgid "Automatically using additional NTP servers"
+msgstr "Usando automaticamente servidores NTP específicos"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Usando automaticamente servidores NTP específicos"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Script de automação"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Alvos disponíveis em $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Atualizações Disponíveis"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "partition"
+msgid "BIOS boot partition"
+msgstr "partição"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "BIOS data"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "BIOS versão"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Voltar para Contas"
+
+#: pkg/systemd/services.jsx:257
+#, fuzzy
+#| msgid "Blade"
+msgid "Bad"
+msgstr "Blade"
+
+#: pkg/systemd/services.jsx:223
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Bad setting"
+msgstr "Configurações de Bond"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Balanceador"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Antes"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Vincula a"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Preto"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blade"
+
+#: pkg/lib/machine-info.js:88
+#, fuzzy
+msgid "Blade enclosure"
+msgstr "Blade enclosure"
+
+#: pkg/storaged/block/other.jsx:41
+#, fuzzy
+#| msgid "Block devices"
+msgid "Block device"
+msgstr "Dispositivos de bloco"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Dispositivo de bloqueio para sistemas de arquivos"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Dispositivos de bloco"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Bloqueado"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Bond"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Inicialização"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"O inicialização falha se o sistema de arquivos não é montado, impedindo o "
+"acesso remoto"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr ""
+"A inicialização ainda é bem-sucedida quando o sistema de arquivos não é "
+"montado"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Vinculado pela"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Ponte"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Bridge porta"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Configurações da Porta de Ponte"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Broadcast"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Configuração quebrada"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Atualizações de correção de bug disponíveis"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Bugs"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Chassi de Expansão de Barramento"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Ao alterar a senha da chave SSH $0 para a senha de login de $1 em $2, a "
+"chave será automaticamente disponibilizada e você poderá fazer login em $3 "
+"sem senha no futuro."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " Burlar verificação do navegador "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Segurança da CPU"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "Interruptores de segurança da CPU"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Uso da CPU"
+
+#: pkg/metrics/metrics.jsx:1939
+#, fuzzy
+#| msgid "CPU usage"
+msgid "CPU usage/load"
+msgstr "Uso da CPU"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+#, fuzzy
+msgid "Cache"
+msgstr "Cache"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "Pode ser um nome de host, endereço IP, nome de alias ou URI ssh://"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr ""
+"Não é possível encontrar nenhum registro usando a combinação atual de "
+"filtros."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr ""
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+#, fuzzy
+#| msgid "Force restart"
+msgid "Cancel reboot"
+msgstr "Forçar reinicialização"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Não pode ser habilitado"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Não é possível conectar a um host desconhecido"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Não é possível prosseguir com as credenciais de login"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Não é possível agendar eventos no passado"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Capacidade"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Sinal"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Alterar"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+#, fuzzy
+#| msgid "Restart policy"
+msgid "Change cryptographic policy"
+msgstr "Reinicie a Política"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Alterar o Nome do Host"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Alterar Nome do Iniciador iSCSI"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Alterar Nome do Iniciador iSCSI"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change label"
+msgstr "Alterar senha"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Alterar senha"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Alterar Senha"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Perfil alterar o desempenho"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Alterar o perfil"
+
+#: pkg/users/shell-dialog.js:70
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change shell"
+msgstr "Alterar senha"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Alterar Horário do Sistema"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Altere a senha de $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Alterar as configurações"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"As chaves alteradas frequentemente são resultado de uma reinstalação do "
+"sistema operacional. No entanto, uma alteração inesperada pode indicar uma "
+"tentativa de terceiros de interceptar sua conexão."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Alterar as configurações irá encerrar a conexão com o servidor, e tornará a "
+"administração interface do usuário indisponível."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Verificar Atualizações"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Vericando $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Checando IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "Checando Dispositivo RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Checando e Reparando o Dispositivo RAID $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Verificando a presença do pacote $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr ""
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Verificando novos aplicativos"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Verificando atualizações de pacote..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Verificando o software instalado"
+
+#: pkg/storaged/dialog.jsx:1267
+#, fuzzy
+#| msgid "Checking for public keys"
+msgid "Checking related processes"
+msgstr "Checando chaves públicas"
+
+#: pkg/packagekit/updates.jsx:1399
+#, fuzzy
+#| msgid "Checking $target"
+msgid "Checking software status"
+msgstr "Vericando $target"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Escolha o idioma a ser usado no aplicativo"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Tamanho do Bloco"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Classe"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Limpando $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr ""
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Limpar todos os filtros"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Limpar pesquisa"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+#, fuzzy
+#| msgid "Create devices"
+msgid "Cleartext device"
+msgstr "Criar dispositivos"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Software do cliente"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Fechar"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Fechar Páginas Selecionadas"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "A autenticação do Cockpit está configurada incorretamente."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr ""
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "O Cockpit não poderia entrar em contato com o host fornecido."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit teve um erro interno inesperado."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit é um gerenciador de Servidores que facilita a tarefa de administrar "
+"seu servidor Linux via navegador web. Alternar entre o terminal e a "
+"ferramenta web, não é dif[icil. Um serviço pode ser iniciado pelo Cockpit e "
+"finalizado pelo Terminal. Da mesma forma, se um erro ocorrer no terminal, "
+"este pode ser detectado via interface do Cockpit."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit é uma interface interativa de administração de servidor Linux."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "O Cockpit não é compatível com o software no sistema."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit não está instalado"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit não está instalado no sistema."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit é perfeito para novos administradores de sistemas, permitindo-lhes "
+"facilmente realizar tarefas simples, como a administração de armazenamento, "
+"inspecionando logs e iniciar/parar serviços. É possível monitorar e "
+"administrar vários servidores ao mesmo tempo. Basta adicioná-los com um "
+"único clique e suas máquinas vão cuidar de seus companheiros."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+#, fuzzy
+msgid "Collect kernel crash dumps"
+msgstr "Dump do Kernel"
+
+#: pkg/metrics/metrics.jsx:1494
+#, fuzzy
+#| msgid "Store metrics"
+msgid "Collect metrics"
+msgstr "Armazenar métricas"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Cor"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Comando"
+
+#: pkg/systemd/timer-dialog.jsx:181
+#, fuzzy
+#| msgid "Not found"
+msgid "Command not found"
+msgstr "Não encontrado"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Comentário"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "A comunicação com sintonizado falhou"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Compacto PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Compatível com todos os sistemas e dispositivos (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Compatível com sistema moderno e discos rígidos > 2TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Comprimir despejos de travamento para economizar espaço"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Compressão"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Condição $0=$1 não foi cumprida"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Condição falhou"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Configuração"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Configurando"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Configurando IP"
+
+#: pkg/kdump/manifest.json:0
+#, fuzzy
+msgid "Configuring kdump"
+msgstr "Configurando"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "Configuring system settings"
+msgstr "Alterar as configurações"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Confirmar"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+#, fuzzy
+#| msgid "Please confirm deletion of $0"
+msgid "Confirm deletion of $0"
+msgstr "Por favor, confirme a remoção de $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Confirme a senha da chave"
+
+#: pkg/shell/hosts_dialog.jsx:817
+#, fuzzy
+msgid "Confirm new key password"
+msgstr "Confirmar Nova Senha"
+
+#: pkg/users/password-dialogs.js:148
+#, fuzzy
+msgid "Confirm new password"
+msgstr "Confirme nova senha"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Confirme a senha"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Confirmar remoção de $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+#, fuzzy
+#| msgid "Confirm removal with passphrase"
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Confirme a remoção com a frase secreta"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+#, fuzzy
+#| msgid "Please confirm stopping of $0"
+msgid "Confirm stopping of $0"
+msgstr "Por favor confirme a parada de $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "Conflito por"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Conflitos"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Conecte automaticamente"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Conectar à"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Conectar à:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Conectando-se ao daemon SETroubleshoot..."
+
+#: pkg/systemd/services.jsx:291
+#, fuzzy
+#| msgid "Connection failed"
+msgid "Connecting to dbus failed: $0"
+msgstr "Conexão falhou"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Conectando a máquina"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Erro de Conexão"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Conexão falhou"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "A conexão expirou."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "A conexão será perdida"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Consiste em"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Domínio contatado"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Contém:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Continuar"
+
+#: pkg/shell/shell-modals.jsx:179
+#, fuzzy
+msgid "Continue session"
+msgstr "Continue sessão"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Controle"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Controle"
+
+#: pkg/systemd/abrtLog.jsx:128
+#, fuzzy
+#| msgid "Control"
+msgid "Controller"
+msgstr "Controller"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Conversível"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr ""
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Copiar"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Copiar para área de transferência"
+
+#: pkg/metrics/metrics.jsx:744
+#, fuzzy
+#| msgid "Remove $0"
+msgid "Core $0"
+msgstr "Remover $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+#, fuzzy
+#| msgid "Could not contact {{host}}"
+msgid "Could not contact $0"
+msgstr "Não foi possível contatar {{host}}"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Local de despejo de travamento"
+
+#: pkg/systemd/reporting.jsx:431
+#, fuzzy
+msgid "Crash reporting"
+msgstr "Relatório de Travamento"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Sistema de travamento"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Criar"
+
+#: pkg/storaged/overview/overview.jsx:146
+#, fuzzy
+#| msgid "Create volume group"
+msgid "Create LVM2 volume group"
+msgstr "Criar Grupo de Volumes"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "Criar dispositivo RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Criar dispositivo RAID"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+#, fuzzy
+#| msgid "Create storage pool"
+msgid "Create Stratis pool"
+msgstr "Criar pool de armazenamento"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+#, fuzzy
+#| msgid "Creating snapshot of $target"
+msgid "Create a snapshot of filesystem $0"
+msgstr "Criando snapshot de $target"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+#, fuzzy
+#| msgid "Create new account"
+msgid "Create and mount"
+msgstr "Criar Nova Conta"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create new account"
+msgid "Create and start"
+msgstr "Criar Nova Conta"
+
+#: pkg/storaged/stratis/pool.jsx:91
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Create filesystem"
+msgstr "Sistema de arquivos"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Criá-lo"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Criar Volume Lógico"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Criar Nova Conta"
+
+#: pkg/storaged/stratis/pool.jsx:307
+#, fuzzy
+#| msgid "Create new volume"
+msgid "Create new filesystem"
+msgstr "Criar novo volume"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+#, fuzzy
+#| msgid "Create volume group"
+msgid "Create new group"
+msgstr "Criar Grupo de Volumes"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Criar novo Volume Lógico"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr ""
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "Criar novo Volume Lógico"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+#, fuzzy
+#| msgid "Create new"
+msgid "Create only"
+msgstr "Criar novo"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Criar Partição"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Criar Partição em $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Criar tabela de partição"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Criar Snapshot"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+#, fuzzy
+#| msgid "Create snapshot"
+msgid "Create snapshot and mount"
+msgstr "Criar Snapshot"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+#, fuzzy
+#| msgid "Create snapshot"
+msgid "Create snapshot only"
+msgstr "Criar Snapshot"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+msgid "Create storage device"
+msgstr "Criar volume de armazenamento"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Criar volume"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Criar Thin Volume"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Criar Temporizador"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Criar Grupo de Volumes"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Criado"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Criando o grupo de volumes LVM2 $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "Criando Dispositivo RAID $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "Criando dispositivo VDO"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Criando sistema de arquivos em $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Criando volume lógico $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Criando partição $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Criando snapshot de $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Criar esse $0 quebrará a conexão com o servidor e tornará a interface de "
+"administração indisponível."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Crítico e acima"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+#, fuzzy
+#| msgid "Restart policy"
+msgid "Cryptographic policy"
+msgstr "Reinicie a Política"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+#, fuzzy
+#| msgid "Restart policy"
+msgid "Cryptographic policy is inconsistent"
+msgstr "Reinicie a Política"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+#, fuzzy
+#| msgid "Ctrl-Shift-J"
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Inicialização atual"
+
+#: pkg/metrics/metrics.jsx:757
+#, fuzzy
+msgid "Current top CPU usage"
+msgstr "Atual"
+
+#: pkg/storaged/dialog.jsx:1178
+#, fuzzy
+msgid "Currently in use"
+msgstr "Atual"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+msgid "Currently not supported"
+msgstr "Atual"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "personalizado"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+#, fuzzy
+#| msgid "Custom encryption options"
+msgid "Custom cryptographic policy"
+msgstr "Opções de criptografia personalizadas"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Opções de montagem personalizadas"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Portas customizadas"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+msgid "Custom type"
+msgstr "Caminho customizado"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Zonas customizadas"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS Busca de Domínios"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "Domínios de Pesquisa DNS $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Diariamente"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Escuro"
+
+#: pkg/storaged/stratis/pool.jsx:197
+#, fuzzy
+#| msgid "Data used"
+msgid "Data"
+msgstr "Dados Usados"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Dados Usados"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Desativar"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Renomear Volume Lógico"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Desativando"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Desativando $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Depurar e acima"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+#, fuzzy
+#| msgid "RAID 4 (dedicated parity)"
+msgid "Dedicated parity (RAID 4)"
+msgstr "RAID 4 (Paridade Dedicada)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Desduplicação"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Padrão"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Atraso"
+
+#: pkg/systemd/timer-dialog.jsx:216
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Delay must be a number"
+msgstr "O tamanho deve ser um número"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Excluir"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Deletar $0"
+
+#: pkg/users/accounts-list.js:80
+#, fuzzy
+#| msgid "Create new account"
+msgid "Delete account"
+msgstr "Criar Nova Conta"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Excluir Arquivos"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+#, fuzzy
+#| msgid "Create new account"
+msgid "Delete group"
+msgstr "Criar Nova Conta"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Excluir"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Excluir relatório permanentemente?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Excluindo $0 a conexão com o servidor será encerrada, e tornará a "
+"administração da interface do usuário indisponível."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Deletando $0"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Excluindo grupo de volume LVM2 $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+#, fuzzy
+#| msgid "Deleting a container will erase all data in it."
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "A exclusão de um container irá apagar todos os dados nele."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+#, fuzzy
+#| msgid "Deleting a partition will delete all data in it."
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "A exclusão de uma partição apaga todos os dados da mesma."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "Excluindo um volume lógico irá excluir todos os dados nele."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "A exclusão de uma partição apaga todos os dados da mesma."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "A exclusão de um dispositivo VDO apagará todos os dados nele."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on a VDO device."
+msgstr "A exclusão de um dispositivo VDO apagará todos os dados nele."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on a volume group."
+msgstr "A exclusão de um dispositivo VDO apagará todos os dados nele."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "A exclusão de um dispositivo VDO apagará todos os dados nele."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Descrição"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Desktop"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Destacável"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Detalhes"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Desenvolvimento"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Dispositivo"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Arquivo do dispositivo"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Dispositivo é somente leitura"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Nome do serviço"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Relatório de diagnostico"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Diretório"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "O diretório $0 não é gravável ou não existe."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr ""
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Desabilitar o firewall"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Desabilitar tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Desabilitado"
+
+#: pkg/users/account-details.js:276
+#, fuzzy
+#| msgid "Domain administrator password"
+msgid "Disallow interactive password"
+msgstr "Senha do Administrador de Domínio"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr ""
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Desconectar"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Desconectado"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Entrada e Saida de disco"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "O Disco está OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+#, fuzzy
+#| msgid "$0 disk is missing"
+#| msgid_plural "$0 disks are missing"
+msgid "Disk is failing"
+msgstr "$0 disco não encontrado"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Senha de disco"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Discos"
+
+#: pkg/metrics/metrics.jsx:792
+#, fuzzy
+#| msgid "Disks"
+msgid "Disks usage"
+msgstr "Discos"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+#, fuzzy
+#| msgid "Dismiss $0 alert"
+#| msgid_plural "Dismiss $0 alerts"
+msgid "Dismiss"
+msgstr "Descartar $0 alerta"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Descartar $0 alerta"
+msgstr[1] "Descartar $0 alertas"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+#, fuzzy
+#| msgid "Close selected pages"
+msgid "Dismiss selected alerts"
+msgstr "Fechar Páginas Selecionadas"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Linguagem Exibida"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+#, fuzzy
+#| msgid "RAID 5 (distributed parity)"
+msgid "Distributed parity (RAID 5)"
+msgstr "RAID 5 (Paridade Distribuída)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+#, fuzzy
+#| msgid "Not mounted"
+msgid "Do not mount"
+msgstr "Não montado"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr ""
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Estação de ancoragem"
+
+#: pkg/systemd/services-list.jsx:84
+#, fuzzy
+msgid "Does not automatically start"
+msgstr "Conecte automaticamente"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr ""
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Domínio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+#, fuzzy
+#| msgid "Domain"
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Domínio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Endereço do Domínio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Nome do Administrador de Domínio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Senha do Administrador de Domínio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "O Domínio não pôde ser contatado"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "O Domínio não é suportado"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Não Repita"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+#, fuzzy
+#| msgid "RAID 6 (double distributed parity)"
+msgid "Double distributed parity (RAID 6)"
+msgstr "RAID 6 (Paridade Duplamente Distribuída)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Baixar"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Baixe um novo navegador gratuitamente"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Baixado"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Baixando"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Baixando $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Unidade"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Partição Extendida"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Editar"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Editar /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Editar o servidor de chaves Tang"
+
+#: pkg/networkmanager/vlan.jsx:91
+#, fuzzy
+#| msgid "VLAN settings"
+msgid "Edit VLAN settings"
+msgstr "Configurações VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:135
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Edit bond settings"
+msgstr "Configurações de Bond"
+
+#: pkg/networkmanager/bridge.jsx:96
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Edit bridge settings"
+msgstr "Configurações de Ponte"
+
+#: pkg/networkmanager/firewall.jsx:596
+#, fuzzy
+#| msgid "Add services to $0 zone"
+msgid "Edit custom service in $0 zone"
+msgstr "Adicionar serviços para zona $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Editar host"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Editar hosts"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Editar motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Ponto de Montagem"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Editar regras e zonas"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Editar Serviço"
+
+#: pkg/networkmanager/firewall.jsx:119
+#, fuzzy
+#| msgid "Edit server"
+msgid "Edit service $0"
+msgstr "Editar Servidor"
+
+#: pkg/networkmanager/team.jsx:154
+#, fuzzy
+#| msgid "Team settings"
+msgid "Edit team settings"
+msgstr "Configurações da Equipe"
+
+#: pkg/users/accounts-list.js:59
+#, fuzzy
+#| msgid "Edit server"
+msgid "Edit user"
+msgstr "Editar Servidor"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Editar uma chave requer um espaço livre"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Ejetando $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr ""
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Vazio"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Vazio"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Senha vazia"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Esvaziando $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Habilitar"
+
+#: pkg/networkmanager/network-interface.jsx:685
+#, fuzzy
+#| msgid "Failed to disable tuned"
+msgid "Enable or disable the device"
+msgstr "Falha ao desabilitar tuned"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Ativar serviço"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Habilitar o firewall"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Habilitado"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+#, fuzzy
+#| msgid "Installing $0"
+msgid "Enabling $0"
+msgstr "Instalando $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Criptografar dados"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+#, fuzzy
+#| msgid "Edit Tang keyserver"
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Editar o servidor de chaves Tang"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+#, fuzzy
+#| msgid "Encryption type"
+msgid "Encrypt data with a passphrase"
+msgstr "Tipo de encriptação"
+
+#: pkg/sosreport/sosreport.jsx:450
+#, fuzzy
+#| msgid "Encrypted $0"
+msgid "Encrypted"
+msgstr "Encriptado"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Encriptado"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted partition of $0"
+msgid "Encrypted Stratis pool"
+msgstr "Partição Criptografada de $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Volume Lógico Criptografado de $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Partição Criptografada de $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Encriptação"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Opções de Criptografia"
+
+#: pkg/sosreport/sosreport.jsx:309
+#, fuzzy
+#| msgid "Encryption type"
+msgid "Encryption passphrase"
+msgstr "Tipo de encriptação"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Tipo de encriptação"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:309
+#, fuzzy
+#| msgid "Entrypoint"
+msgid "Endpoint"
+msgstr "Ponto de entrada"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Atualizações de aprimoramento disponíveis"
+
+#: pkg/networkmanager/mac.jsx:47
+#, fuzzy
+#| msgid "Invalid address $0"
+msgid "Enter a valid MAC address"
+msgstr "Endereço inválido $0"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr ""
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Apagando $target"
+
+#: pkg/packagekit/updates.jsx:381
+#, fuzzy
+msgid "Errata"
+msgstr "Errata"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Erro"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Erro e acima"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Ocorreu um erro"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "PackageKit não está instalado"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Mensagem de erro"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr ""
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Erro ao salvar chaves autorizadas: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Erro ao definir o modo de SELinux: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Exemplo: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Exemplo: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Senha excelente"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Chassi de Expansão"
+
+#: pkg/users/expiration-dialogs.js:71
+#, fuzzy
+#| msgid "Lock account on $0"
+msgid "Expire account on"
+msgstr "Bloquear conta em $0"
+
+#: pkg/users/account-details.js:78
+#, fuzzy
+#| msgid "Lock account on $0"
+msgid "Expire account on $0"
+msgstr "Bloquear conta em $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Exportar"
+
+#: pkg/metrics/metrics.jsx:1511
+#, fuzzy
+msgid "Export to network"
+msgstr "Rede roteada"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Informação estendida"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Partição Extendida"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Falhou"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Falha ao adicionar a máquina: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Falha ao adicionar porta"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Falha ao adicionar serviço"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Falha ao adicionar zona"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Falha ao mudar senha"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "Falha ao configurar PCP"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Falha ao deletar alerta: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Falha ao desabilitar tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Falha ao desativar o perfil do tuned"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Falha ao editar a máquina: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Falha ao editar serviço"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+#, fuzzy
+#| msgid "Failed to enable tuned"
+msgid "Failed to enable $0 in firewalld"
+msgstr "Falha ao habilitar tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Falha ao habilitar tuned"
+
+#: pkg/systemd/logsJournal.jsx:259
+#, fuzzy
+#| msgid "Failed to switch profile"
+msgid "Failed to fetch logs"
+msgstr "Falha ao mudar de perfil"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Falha ao carregar as chaves autorizadas."
+
+#: pkg/systemd/service-details.jsx:539
+#, fuzzy
+#| msgid "Failed to add port"
+msgid "Failed to load unit"
+msgstr "Falha ao adicionar porta"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:498
+#, fuzzy
+#| msgid "Failed to start"
+msgid "Failed to restart service"
+msgstr "Falha ao iniciar"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+#, fuzzy
+msgid "Failed to save changes in /etc/motd"
+msgstr "Falha ao mudar senha"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+#, fuzzy
+#| msgid "Unable to apply settings: $0"
+msgid "Failed to save settings"
+msgstr "ão é possível aplicar as configurações: $0"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Falha ao iniciar"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Falha ao mudar de perfil"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+#, fuzzy
+#| msgid "Failed to start"
+msgid "File state"
+msgstr "Falha ao iniciar"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Sistema de arquivos"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+#, fuzzy
+#| msgid "Filesystems"
+msgid "Filesystem is locked"
+msgstr "Sistema de Arquivos"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Nome do Sistema de Arquivos"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Criando sistema de arquivos em $target"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+#, fuzzy
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr ""
+"O sistema de arquivos está em uso por sessões de login. O processo "
+"interromperá isso."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Filtrar por nome ou descrição"
+
+#: pkg/shell/shell-modals.jsx:134
+#, fuzzy
+#| msgid "Filter services"
+msgid "Filter menu items"
+msgstr "Serviços de filtro"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Serviços de filtro"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Filtros"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Digital"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Firewall"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Firewall não está disponível"
+
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Versão do firmware"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Tamanho da fonte"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr ""
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Force Mudança"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Remoção forçada"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Force troca de senha"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Formate"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Formate $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Formatar e montar"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format and mount"
+msgid "Format and start"
+msgstr "Formatar e montar"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+#, fuzzy
+#| msgid "Format"
+msgid "Format only"
+msgstr "Formate"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+#, fuzzy
+#| msgid "Formatting a disk will erase all data on it."
+msgid "Formatting erases all data on a storage device."
+msgstr "A formatação de um disco apaga todos os dados do mesmo."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Encaminhado Atraso $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr ""
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Espaço Livre"
+
+#: pkg/systemd/logs.jsx:378
+#, fuzzy
+#| msgid "Clear search"
+msgid "Free-form search"
+msgstr "Limpar pesquisa"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Sextas"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "De"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Nome Completo"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Gateway"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Geral"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Gerado"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Ir para $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Vá para o aplicativo"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Ir para agora"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr ""
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Grupo"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Nome do Grupo"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Grupos"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Crescer"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+#, fuzzy
+msgid "Grow content"
+msgstr "Aumente o conteúdo"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Crescer tamanho lógico de $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Aumentar o Volume Lógico"
+
+#: pkg/storaged/block/resize.jsx:409
+#, fuzzy
+#| msgid "partition"
+msgid "Grow partition"
+msgstr "partição"
+
+#: pkg/storaged/stratis/pool.jsx:337
+#, fuzzy
+#| msgid "Grow to take all space"
+msgid "Grow the pool to take all space"
+msgstr "Crescer para tomar todo o espaço"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Crescer para tomar todo o espaço"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Hair Pin modo"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Hairpin modo"
+
+#: pkg/lib/machine-info.js:70
+#, fuzzy
+msgid "Handheld"
+msgstr "Portátil"
+
+#: pkg/storaged/drive/drive.jsx:64
+#, fuzzy
+#| msgid "Hard Disk"
+msgid "Hard Disk Drive"
+msgstr "Disco Rígido"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Informação de Hardware"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Tempo de hello $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Ajuda"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Hide confirmation password"
+msgstr "Confirme a senha"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "User password"
+msgid "Hide password"
+msgstr "Senha de Usuário"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr ""
+
+#: pkg/packagekit/history.jsx:112
+#, fuzzy
+msgid "History package count"
+msgstr "Bloquear Conta"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Diretório pessoal"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Máquina"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Chave de Host incorreta"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Nome do host"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Hosts"
+
+#: pkg/systemd/timer-dialog.jsx:244
+#, fuzzy
+#| msgid "Hours"
+msgid "Hourly"
+msgstr "Horas"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Horas"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr ""
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "Endereço IP"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"Endereço IP com prefixo de roteamento. Separe múltiplos valores com uma "
+"vírgula. Exemplo: 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+#, fuzzy
+#| msgid "IPv4 address"
+msgid "IPv4 addresses"
+msgstr "Endereço IPv4"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4 Ajustes"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6 Ajustes"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Identificador"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ignorar"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "Em Sincronização"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Inativo"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "Criar Volume Lógico"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr ""
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Memória de índice"
+
+#: pkg/systemd/services.jsx:254
+#, fuzzy
+#| msgid "direct"
+msgid "Indirect"
+msgstr "direto"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Info"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Info e acima"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Inicializar"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Inicializando disco $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+#, fuzzy
+#| msgid "Formatting a disk will erase all data on it."
+msgid "Initializing erases all data on a disk."
+msgstr "A formatação de um disco apaga todos os dados do mesmo."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Inicializando ..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Ideias: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Instale"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Instalar o suporte ao NFS"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Instalar suporte ao Stratis"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Instalar todas as atualizações"
+
+#: pkg/apps/application-list.jsx:200
+#, fuzzy
+#| msgid "Package information"
+msgid "Install application information"
+msgstr "Informações do pacote"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1429
+#, fuzzy
+#| msgid "Install all updates"
+msgid "Install kpatch updates"
+msgstr "Instalar todas as atualizações"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+#, fuzzy
+#| msgid "Install NFS support"
+msgid "Install realmd support"
+msgstr "Instale o suporte ao NFS"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Instalar atualizações de segurança"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr ""
+"Instale o setroubleshoot-server para solucionar problemas de eventos SELinux."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Instale Software"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Por favor, instale o pacote de $0"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Instalado"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Instalando"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Instalando $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "A instalação de $0 removeria $1."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+#, fuzzy
+#| msgid "Installing"
+msgid "Installing packages"
+msgstr "Instalando"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Interface"
+msgstr[1] "Interfaces"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Membros da interface"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Interfaces"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Erro interno"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Erro interno: Cabeçalho de desafio inválido"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Inválido"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Endereço inválido $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Formato de data inválido"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Formato de data inválido e formato de tempo inválido"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Data de validade inválida"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Permissão de arquivos inválida"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Chave inválida"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Métrica inválida $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Número inválido de dias"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Número de porta inválido"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Prefixo inválido $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Prefixo ou máscara de rede inválidos $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Intervalo inválido"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Formato de tempo inválido"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Fuso horário inválido"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Nome de usuário ou senha inválidos"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "Gateway IoT"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "O sshd está sendo executado em uma porta diferente?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Trabalhos"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Afiliar-se"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+#, fuzzy
+#| msgid "Join a domain"
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Ingressar em um domínio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Associar-se ao domínio"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+#, fuzzy
+#| msgid "Join"
+msgid "Joining"
+msgstr "Afiliar-se"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+#, fuzzy
+msgid "Joining a domain requires installation of realmd"
+msgstr "A adesão à este domínio não é suportada"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "A adesão à este domínio não é suportada"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Junte os espaços nos nomes de"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Diário"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Entrada do diário"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Entrada do diário não encontrada"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit não está instalado"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Testar configurações kdump"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Manter conexão"
+
+#: pkg/kdump/kdump-view.jsx:581
+#, fuzzy
+msgid "Kernel crash dump"
+msgstr "Dump do Kernel"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Dump do Kernel"
+
+#: pkg/packagekit/kpatch.jsx:366
+#, fuzzy
+#| msgid "Kernel patch $0 is installed"
+msgid "Kernel live patch $0 is active"
+msgstr "Patch do kernel $0 está instalado"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Live Patch do kernel $0 está instalado"
+
+#: pkg/packagekit/kpatch.jsx:299
+#, fuzzy
+#| msgid "Change the settings"
+msgid "Kernel live patch settings"
+msgstr "Alterar as configurações"
+
+#: pkg/packagekit/kpatch.jsx:282
+#, fuzzy
+msgid "Kernel live patching"
+msgstr "Dump do Kernel"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Nova senha"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Slots chave com tipos desconhecidos não podem ser editados aqui"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Fonte chave"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Chaves"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Servidor de chaves"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Endereço do servidor de chaves"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "A remoção do Keyserver pode impedir o desbloqueio de $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "Chave LACP"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr ""
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "Pool"
+msgid "LVM2 VDO pool"
+msgstr "Pool"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Volume Lógico"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Volumes lógico"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volume"
+msgstr "Volume Físico"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volumes"
+msgstr "Volume Físico"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "Grupo de Volumes LVM2"
+
+#: pkg/storaged/utils.js:340
+#, fuzzy
+msgid "LVM2 volume group $0"
+msgstr "Grupo de volumes LVM"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Laptop"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Últimas 24 horas"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Últimos 7 dias"
+
+#: pkg/users/accounts-list.js:391
+#, fuzzy
+#| msgid "active"
+msgid "Last active"
+msgstr "ativo"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "O último slot chave não pode ser removido"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Última verificação: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "O último slot chave não pode ser removido"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Último Login"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Última modificação: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+#, fuzzy
+#| msgid "Last failed login:"
+msgid "Last successful login:"
+msgstr "Último login com falha:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Saiba mais"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#, fuzzy
+#| msgid "Remove $0"
+msgid "Leave $0"
+msgstr "Remover $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Abandonar Domínio"
+
+#: pkg/sosreport/sosreport.jsx:317
+#, fuzzy
+#| msgid "Restoring connection"
+msgid "Leave empty to skip encryption"
+msgstr "Restaurando conexão"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr ""
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Limitar acesso"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Acesso limitado"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+
+#: pkg/systemd/abrtLog.jsx:143
+#, fuzzy
+#| msgid "Limit access"
+msgid "Limits"
+msgstr "Limitar acesso"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Link down atraso"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Link Local"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Monitoramento de Link"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Link up atraso"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Link para assistir"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+#, fuzzy
+#| msgid "Links"
+msgid "Linked"
+msgstr "Links"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem"
+msgid "Linux filesystem data"
+msgstr "Desmontar o sistema de arquivos"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Swap Espaço"
+
+#: pkg/systemd/service-details.jsx:670
+#, fuzzy
+#| msgid "Persistent"
+msgid "Listen"
+msgstr "Persistente"
+
+#: pkg/networkmanager/wireguard.jsx:242
+#, fuzzy
+#| msgid "Persistent"
+msgid "Listen port"
+msgstr "Persistente"
+
+#: pkg/networkmanager/wireguard.jsx:149
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Listen port must be a number"
+msgstr "O tamanho deve ser um número"
+
+#: pkg/systemd/services.jsx:683
+#, fuzzy
+msgid "Listing units"
+msgstr "Serviços do Sistema"
+
+#: pkg/systemd/services.jsx:503
+#, fuzzy
+#| msgid "Last modified: $0"
+msgid "Listing units failed: $0"
+msgstr "Última modificação: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Balanceamento de Carga"
+
+#: pkg/metrics/metrics.jsx:1979
+#, fuzzy
+#| msgid "Load earlier entries"
+msgid "Load earlier data"
+msgstr "Carregar logs anteriores"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Carregar logs anteriores"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Falha ao carregar atualizações disponíveis"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Carregando as atualizações disponíveis, por favor aguarde ..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Carregando logs anteriores"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Carregando chaves..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "O carregamento das chaves SSH falhou"
+
+#: pkg/systemd/services.jsx:664
+#, fuzzy
+#| msgid "Loading of SSH keys failed"
+msgid "Loading of units failed"
+msgstr "O carregamento das chaves SSH falhou"
+
+#: pkg/shell/shell-modals.jsx:78
+#, fuzzy
+#| msgid "Loading..."
+msgid "Loading packages..."
+msgstr "Carregando..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Carregando modificações do sistema..."
+
+#: pkg/systemd/service.jsx:129
+#, fuzzy
+#| msgid "Login failed"
+msgid "Loading unit failed"
+msgstr "Falha ao logar"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Carregando..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Contas Locais"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Sistema de Arquivos Local"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Ponto de montagem local"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+msgid "Local storage"
+msgstr "Nenhum Armazenamento"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "localmente em $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Localização"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Travar"
+
+#: pkg/users/lock-account-dialog.js:29
+#, fuzzy
+#| msgid "Lock"
+msgid "Lock $0"
+msgstr "Travar"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Bloquear Conta"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgid "Locked"
+msgid "Locked data"
+msgstr "Bloqueado"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Bloqueando $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Entrar"
+
+#: pkg/shell/hosts_dialog.jsx:763
+#, fuzzy
+#| msgid "Go to $0"
+msgid "Log in to $0"
+msgstr "Ir para $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Faça o login com sua conta de usuário do servidor."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Mensagens de Log"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Log out"
+
+#: pkg/users/accounts-list.js:68
+#, fuzzy
+#| msgid "Log out"
+msgid "Log user out"
+msgstr "Log out"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Logado"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Lógico"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Volume Lógico (Snapshot)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Tamanho Lógico"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Volume Lógico"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Volume Lógico (Snapshot)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Volume Lógico de $0"
+
+#: pkg/static/login.js:361
+#, fuzzy
+#| msgid "login"
+msgid "Login"
+msgstr "Login"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Logar Novamente"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Falha ao logar"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Formato de Login"
+
+#: pkg/users/account-logs-panel.jsx:73
+#, fuzzy
+#| msgid "history"
+msgid "Login history"
+msgstr "histórico"
+
+#: pkg/users/account-logs-panel.jsx:75
+#, fuzzy
+#| msgid "Login format"
+msgid "Login history list"
+msgstr "Formato de Login"
+
+#: pkg/users/logout-account-dialog.js:29
+#, fuzzy
+#| msgid "Slot $0"
+msgid "Logout $0"
+msgstr "Slot $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Login Bem Sucedido"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Logs"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Desktop de baixo perfil"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Lunch box"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "Dispositivo RAID"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "Dispositivo RAID $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Dispositivo é somente leitura"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "O serviço está em execução"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "Dispositivo RAID"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Adicionar Discos"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (Recomendado)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "O MTU deve ser um número positivo"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "ID de Máquina"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Máquina de Chaves SSH e Impressões digitais"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Chassi do Servidor Principal"
+
+#: pkg/systemd/services.jsx:238
+#, fuzzy
+#| msgid "Maintanance"
+msgid "Maintenance"
+msgstr "Manutenção"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Manage filesystem sizes"
+msgstr "Sistema de arquivos"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+#, fuzzy
+#| msgid "Unmanaged interfaces"
+msgid "Manage storage"
+msgstr "Interfaces Não Gerenciadas"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Interfaces Gerenciadas"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "Managing LVMs"
+msgstr "Dispositivo de apoio"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "NFS mounts"
+msgid "Managing NFS mounts"
+msgstr "Gerenciando montagens NFS"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "Managing RAIDs"
+msgstr "Dispositivo de apoio"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "Managing VDOs"
+msgstr "Dispositivo de apoio"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+msgid "Managing VLANs"
+msgstr "Dispositivo de apoio"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr ""
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+msgid "Managing networking bonds"
+msgstr "Rede"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+msgid "Managing networking bridges"
+msgstr "Rede"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+msgid "Managing networking teams"
+msgstr "Rede"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "Managing partitions"
+msgstr "Criando partição $target"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "Managing physical drives"
+msgstr "Dispositivo de apoio"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "Managing services"
+msgstr "Dispositivo de apoio"
+
+#: pkg/packagekit/manifest.json:0
+#, fuzzy
+msgid "Managing software updates"
+msgstr "Atualizações de Software"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Manual"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Manualmente"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Marcando $target como defeituoso"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Serviço de máscara"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Máxima permanência da mensagem $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Drive óptico"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Memória"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Uso de memória"
+
+#: pkg/metrics/metrics.jsx:1939
+#, fuzzy
+#| msgid "Memory usage"
+msgid "Memory usage/swap"
+msgstr "Uso de memória"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr ""
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Mensagem para usuários logados"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr ""
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Metadados Usados"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Método"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Métricas"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+#, fuzzy
+#| msgid "history"
+msgid "Metrics and history"
+msgstr "histórico"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Não foi possível carregar o histórico de métricas"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Metrics settings"
+msgstr "Configurações de Ponte"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Mini Torre"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Minutos precisam ser números entre 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+#, fuzzy
+#| msgid "Minutes"
+msgid "Minutely"
+msgstr "Minutos"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minutos"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Modo"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Modelo"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Modificando $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Segundas"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Monitorando Intervalo"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Alvod e Monitoramento"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Mais info..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Montar"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+#, fuzzy
+#| msgid "Mount point"
+msgid "Mount Point"
+msgstr "Ponto de Montagem"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Monte na Inicialização"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Configuração de montagem"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Montar sistema de arquivos"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Opções de Montagem"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Ponto de Montagem"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "O ponto de montagem não pode estar vazio"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "O ponto de montagem não pode estar vazio."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "O ponto de montagem deve começar com \"/\"."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Monte só de leitura"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Montando $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Chassi Multi-sistema"
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Multipathed devices"
+msgid "Multipathed devices"
+msgstr "Dispositivos do Multipath"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "Montagem NFS"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "Servidor NTP"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Nome"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "O nome não pode estar vazio."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "O nome não pode estar vazio."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "O nome não pode ser maior que $0 bytes"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "O nome não pode ser maior do que $0 caracteres"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "O nome não pode ser maior do que 127 caracteres."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "O nome não pode ser maior do que 127 caracteres."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "O nome não pode conter o caractere '$0'."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "O nome não pode conter o caractere '$0'."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Nome não pode conter espaço em branco."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Precisa de pelo menos um servidor NTP"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Rede"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "E/S de rede"
+
+#: pkg/networkmanager/bond.jsx:138
+#, fuzzy
+msgid "Network bond"
+msgstr "Rede"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Dispositivos de rede e gráficos requerem o NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Logs de rede"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Uso de rede"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager não está instalado"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager não está rodando"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Uso de rede"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Rede"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Nunca"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+#, fuzzy
+#| msgid "Never lock account"
+msgid "Never expire account"
+msgstr "Nunca bloquear conta"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Senha nunca expira"
+
+#: pkg/users/accounts-list.js:173
+#, fuzzy
+#| msgid "Logged in"
+msgid "Never logged in"
+msgstr "Logado"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Nova montagem de volume NFS"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Novo host"
+
+#: pkg/shell/hosts_dialog.jsx:464
+#, fuzzy
+#| msgid "New host"
+msgid "New host: $0"
+msgstr "Novo host"
+
+#: pkg/shell/hosts_dialog.jsx:812
+#, fuzzy
+msgid "New key password"
+msgstr "Nova Senha"
+
+#: pkg/users/rename-group-dialog.jsx:35
+#, fuzzy
+#| msgid "Rename"
+msgid "New name"
+msgstr "Renomear"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Nova senha"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Nova Senha"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Nova senha não foi aceita"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Próximo"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Não"
+
+#: pkg/users/group-create-dialog.js:79
+#, fuzzy
+#| msgid "No alias specified"
+msgid "No ID specified"
+msgstr "Nenhum apelido especificado"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Nenhum alerta SELinux."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Nenhum aplicativo instalado ou disponível."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Sem slots disponíveis"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+#, fuzzy
+#| msgid "No disks are available."
+msgid "No block devices are available."
+msgstr "Sem discos disponíveis."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "Nenhum dispositivo de inicialização encontrado"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Sem sinal"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Nenhuma configuração encontrada"
+
+#: pkg/metrics/metrics.jsx:1869
+#, fuzzy
+#| msgid "Not available"
+msgid "No data available"
+msgstr "Indisponível"
+
+#: pkg/metrics/metrics.jsx:1865
+#, fuzzy
+#| msgid "Occurred between $0 and $1"
+msgid "No data available between $0 and $1"
+msgstr "Ocorreu entre $0 e $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Sem Atraso"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Sem descrição disponível"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Nenhuma descrição fornecida."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "Nenhum dispositivo de inicialização encontrado"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Sem discos disponíveis."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Nenhum log encontrado"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "Nenhum resultado encontrado"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+#, fuzzy
+msgid "No encryption"
+msgstr "Descrição"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Sem eventos"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Nenhum Sistema de Arquivos"
+
+#: pkg/storaged/stratis/pool.jsx:360
+#, fuzzy
+#| msgid "No filesystem"
+msgid "No filesystems"
+msgstr "Nenhum Sistema de Arquivos"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Nenhum slot de chave livre"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Não há espaço livre"
+
+#: pkg/storaged/partitions/partition.jsx:79
+#, fuzzy
+#| msgid "Create partition"
+msgid "No free space after this partition"
+msgstr "Criar Partição"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Nenhum nome de grupo especificado"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Nenhuma chave de host encontrada."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Nenhum pacote de instalação encontrado para este aplicativo."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Nenhuma chave adicionada"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr ""
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+#, fuzzy
+msgid "No log entries"
+msgstr "Carregar logs anteriores"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Nenhum Volume Lógico"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Nenhum log encontrado"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Nenhuma mídia inserida"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Sem particionamento"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "Sem particionamento"
+
+#: pkg/networkmanager/wireguard.jsx:341
+#, fuzzy
+#| msgid "No keys added"
+msgid "No peers added."
+msgstr "Nenhuma chave adicionada"
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "Volumes Físicos"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Nenhum nome real especificado"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Nenhum resultado encontrado"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr ""
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+msgid "No storage found"
+msgstr "Nenhum Armazenamento"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Nenhum Volume Lógico"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Diretório ou arquivo não encontrado"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Nenhuma modificações no sistema"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Sem relatórios do sistema."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Sem atualizações"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Nenhum nome de usuário foi especificado"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Nenhum"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Chave privada não válida"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr ""
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Indisponível"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Não conectado"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr ""
+
+#: pkg/shell/indexes.jsx:465
+#, fuzzy
+msgid "Not connected to host"
+msgstr "Não conectado"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "No free space"
+msgid "Not enough free space"
+msgstr "Não há espaço livre"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+#, fuzzy
+#| msgid "No free space"
+msgid "Not enough space"
+msgstr "Não há espaço livre"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "No free space"
+msgid "Not enough space to grow"
+msgstr "Não há espaço livre"
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Não encontrado"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Não instalado"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "The user <b>$0</b> is not permitted to modify realms"
+msgid "Not permitted to configure network devices"
+msgstr "O usuário <b>$0</b> não tem permissão para modificar reinos"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+#, fuzzy
+#| msgid "The user <b>$0</b> is not permitted to modify realms"
+msgid "Not permitted to configure realms"
+msgstr "O usuário <b>$0</b> não tem permissão para modificar reinos"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Não é permitido executar esta ação."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Não está pronto"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Não registrado"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Não está rodando"
+
+#: pkg/packagekit/autoupdates.jsx:346
+#, fuzzy
+#| msgid "No NFS mounts set up"
+msgid "Not set up"
+msgstr "Nenhum volume NFS montado"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Não sincronizado"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Nota"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Notebook"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Observe e acima"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Ocorreu $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Ocorreu entre $0 e $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#, fuzzy
+#| msgid "Occurances"
+msgid "Occurrences"
+msgstr "Ocorrências"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "Ok"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Senha antiga"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Senha Atual"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Senha antiga não aceita"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "Em um dispositivo montado"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Em Falha"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Uma vez instalado o Cockpit, habilite-o com \"systemctl enable --now cockpit."
+"socket\"."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Somente $0 de $1 está em uso."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Apenas alfabetos, números, : , _ , . , @ , - são permitidos"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Apenas emergência"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Ooops!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Operação '$operation' em $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Opções"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Ou use um navegador incluído"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "De outros"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Outras Opções"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Saída"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Visão geral"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+#, fuzzy
+#| msgid "Overview"
+msgid "Overwrite"
+msgstr "Visão geral"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+#, fuzzy
+#| msgid "Overwrite existing data with zeros"
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Sobrescrever dados existentes com zeros"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+#, fuzzy
+#| msgid "ID"
+msgid "PID"
+msgstr "ID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Informações do pacote"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit caiu"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit não está instalado"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit reportou código de erro $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Pacotes"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Nome da página"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Parente"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Parte de"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Parte of $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Partição"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Partição de $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr ""
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Particionamento"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Partições"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Passivo"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Frase-senha"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "A senha não pode estar vazia"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "A senha não pode estar vazia"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+#, fuzzy
+#| msgid "Passphrase cannot be empty"
+msgid "Passphrase from any other key slot"
+msgstr "A senha não pode estar vazia"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "A remoção da senha pode impedir o desbloqueio de $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "As senhas não correspondem"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Senha"
+
+#: pkg/shell/credentials.jsx:259
+#, fuzzy
+#| msgid "Solution applied successfully"
+msgid "Password changed successfully"
+msgstr "Solução aplicada com êxito"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Expiração de Senha"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "A senha tem mais de 256 caracteres"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Senha não é aceitavél"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Senha é muito fraca"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "A senha deve ser alterada"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Senha não aceita"
+
+#: pkg/shell/credentials.jsx:274
+#, fuzzy
+#| msgid "Password"
+msgid "Password tip"
+msgstr "Senha"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Colar"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+#, fuzzy
+#| msgid "Internal error"
+msgid "Paste error"
+msgstr "Erro interno"
+
+#: pkg/networkmanager/wireguard.jsx:215
+#, fuzzy
+#| msgid "Use existing"
+msgid "Paste existing key"
+msgstr "Usar existente"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Cole o conteúdo do seu arquivo de chave pública SSH aqui"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Caminho"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Custo do Caminho"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Caminho no servidor"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Caminho no servidor não pode estar vazio."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Caminho no servidor deve começar com \"/\"."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Caminho para o Diretório"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Caminho para o arquivo"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Caminhos"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Pausar"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Perfil de desempenho"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Chassi Periférico"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Permanente"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "Permanentemente deletar $0 grupo?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Permanently delete $0 group?"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Permanentemente deletar $0 grupo?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0 group?"
+msgid "Permanently delete subvolume $0?"
+msgstr "Permanentemente deletar $0 grupo?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Permissão negada"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Permissivo"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Fisica"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "Physical Volumes"
+msgstr "Volumes Físicos"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Volumes Físicos"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "Volumes físicos não podem ser redimensionados aqui."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:184
+#, fuzzy
+msgid "Pin unit"
+msgstr "Serviços do Sistema"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Intervalo de Ping"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Alvo do Ping"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr ""
+
+#: pkg/lib/machine-info.js:64
+#, fuzzy
+msgid "Pizza box"
+msgstr "Caixa de pizza"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr ""
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Por favor, instale o pacote de $0"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr ""
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Por favor especifique uma data de expiração"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Por favor, especifique o host para se conectar"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr ""
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Buscando por Thin Logical Volumes"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Pool para volumes finamente provisionados"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Pool para volumes finamente provisionados"
+
+#: pkg/storaged/stratis/pool.jsx:464
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Pool passphrase"
+msgstr "Senha antiga"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Porta"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Portatil"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Portas"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Criar Partição"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Comprimento do prefixo"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Comprimento do prefixo ou máscara de rede"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Preparando"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Presente"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+#, fuzzy
+#| msgid "Preserve"
+msgid "Preserve"
+msgstr "Preservar"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Nome de Host Bonito"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Primário"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Prioridade"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Prioridade $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+#, fuzzy
+#| msgid "Trust key"
+msgid "Private key"
+msgstr "Chave de confiança"
+
+#: pkg/shell/superuser.jsx:194
+#, fuzzy
+#| msgid "Domain administrator name"
+msgid "Problem becoming administrator"
+msgstr "Nome do Administrador de Domínio"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Detalhes do problema"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Informação do Problema"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr ""
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Site do projeto"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Tempo excedido na tentativa de solicitação via senha"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "A solicitação via ssh-add expirou"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Solicitação via ssh-keygen expirou"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Propaga Recarregar para"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Chave Pública"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Propósito"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (Distribuição)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (Espelhamento)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (Distribuição de Espelhos)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (Paridade Dedicada)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (Paridade Distribuída)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (Paridade Duplamente Distribuída)"
+
+#: pkg/lib/machine-info.js:81
+#, fuzzy
+msgid "RAID chassis"
+msgstr "Chassis do RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "Nível de RAID"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+#, fuzzy
+msgid "Rack mount chassis"
+msgstr "Chassi de montagem em rack"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Aleatório(a)"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Intervalo"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "O intervalo deve ser estritamente ordenado"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Raw para um dispositivo"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Leitura"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Somente leitura"
+
+#: pkg/storaged/plot.jsx:96
+#, fuzzy
+#| msgid "Reading"
+msgid "Reading"
+msgstr "Lendo"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Lendo..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Pronto"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Pronto"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Nome Real de Host"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Nome de host real só pode conter caracteres minúsculos, dígitos, traços e "
+"períodos (com subdomínios preenchidos)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Nome de host real deve conter 64 caracteres ou menos"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr ""
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Reiniciar"
+
+#: pkg/packagekit/updates.jsx:614
+#, fuzzy
+#| msgid "Reboot recommended"
+msgid "Reboot after completion"
+msgstr "Recomenda-se reiniciar"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Recomenda-se reiniciar"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr ""
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Recebendo"
+
+#: pkg/static/login.html:165
+#, fuzzy
+#| msgid "Recent"
+msgid "Recent hosts"
+msgstr "Recente"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr ""
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Reconectar"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Recuperação"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "Recuperando Dispositivo RAID $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Atualizando informações do pacote"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Recusando-se a se conectar. O host é desconhecido"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Recusando-se a se conectar. A chave não coincide"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Recusando-se a se conectar. A chave é desconhecida"
+
+#: pkg/networkmanager/wireguard.jsx:224
+#, fuzzy
+#| msgid "Generated"
+msgid "Regenerate"
+msgstr "Gerado"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+#, fuzzy
+#| msgid "Generating report"
+msgid "Regenerating initrd"
+msgstr "Gerando relatório"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Registro…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Recarregar"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Recarregar propagado de"
+
+#: pkg/systemd/services.jsx:233
+#, fuzzy
+#| msgid "Reading"
+msgid "Reloading"
+msgstr "Lendo"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:469
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over CIFS/SMB"
+msgstr "Remoto sobre NFS"
+
+#: pkg/kdump/kdump-view.jsx:465
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over FTP"
+msgstr "Remoto sobre NFS"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Remoto sobre NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "Remoto sobre NFS"
+
+#: pkg/kdump/kdump-view.jsx:467
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SFTP"
+msgstr "Remoto sobre SSH"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Remoto sobre SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "Remoto sobre SSH"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Remoções:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Remover"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Remover $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Remover $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Remover o servidor de chaves Tang?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Remover dispositivo"
+
+#: pkg/static/login.js:634
+#, fuzzy
+#| msgid "Remove device"
+msgid "Remove host"
+msgstr "Remover dispositivo"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+#, fuzzy
+#| msgid "Remove device"
+msgid "Remove item"
+msgstr "Remover dispositivo"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Remove missing physical volumes?"
+msgstr "Removendo volume físico de $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+#, fuzzy
+#| msgid "Remove passphrase in $0?"
+msgid "Remove passphrase in key slot $0?"
+msgstr "Remover frase secreta em $0?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "Remover senha?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Remover serviço $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Remover zona $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Removendo"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Removendo $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+#, fuzzy
+#| msgid ""
+#| "Removing <b>$0</b> will break the connection to the server, and will make "
+#| "the administration UI unavailable."
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Removendo <b>$0</b> irá encerrar a conexão com o servidor e tornará a "
+"administração da interface do usuário indisponível."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "Removendo $target de Dispositivo RAID"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Removendo volume físico de $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr ""
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Renomear"
+
+#: pkg/storaged/stratis/pool.jsx:168
+#, fuzzy
+#| msgid "Reset Storage Pool"
+msgid "Rename Stratis pool"
+msgstr "Resetar Pool de Armazenamento"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Rename filesystem"
+msgstr "Sistema de arquivos"
+
+#: pkg/users/group-actions.jsx:39
+#, fuzzy
+#| msgid "Rename volume group"
+msgid "Rename group"
+msgstr "Renomear Grupo de Volume"
+
+#: pkg/users/rename-group-dialog.jsx:60
+#, fuzzy
+#| msgid "Volume group $0"
+msgid "Rename group $0"
+msgstr "Grupo do Volume $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Renomear Volume Lógico"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Renomear Grupo de Volume"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Renomeando $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Repair logical volume $0"
+msgstr "Renomear Volume Lógico"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Reparando $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+#, fuzzy
+#| msgid "Repeat daily"
+msgid "Repeat"
+msgstr "Repita Diariamente"
+
+#: pkg/systemd/timer-dialog.jsx:335
+#, fuzzy
+#| msgid "Repeat monthly"
+msgid "Repeat monthly"
+msgstr "Repita Mensalmente"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Repita a frase secreta"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Repita Semanalmente"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Relatório"
+
+#: pkg/sosreport/sosreport.jsx:306
+#, fuzzy
+#| msgid "Report"
+msgid "Report label"
+msgstr "Relatório"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:496
+#, fuzzy
+#| msgid "Report"
+msgid "Reports"
+msgstr "Relatório"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr ""
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Requer mudança de senha a cada $0 dias"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Exigir alteração de senha em $0"
+
+#: pkg/users/account-create-dialog.js:119
+#, fuzzy
+#| msgid "Require password change on $0"
+msgid "Require password change on first login"
+msgstr "Exigir alteração de senha em $0"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Solicitado por"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Requere"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Requisita"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Requisitode"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Memória reservada"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Redefinir"
+
+#: pkg/users/password-dialogs.js:278
+#, fuzzy
+#| msgid "Set password"
+msgid "Reset password"
+msgstr "Definir uma Senha"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Redimensionando $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Reiniciar"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+#, fuzzy
+#| msgid "Start service"
+msgid "Restart services"
+msgstr "Começar serviço"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+#, fuzzy
+#| msgid "Start service"
+msgid "Restart services..."
+msgstr "Reiniciar serviços..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Reiniciando"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Restaurando conexão"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:255
+#, fuzzy
+#| msgid "Restoring connection"
+msgid "Reuse existing encryption"
+msgstr "Restaurando conexão"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+#, fuzzy
+#| msgid "Restart policy"
+msgid "Review cryptographic policy"
+msgstr "Reinicie a Política"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "Reviewing logs"
+msgstr "Logs de rede"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Round robin"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Rotas"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+#, fuzzy
+#| msgid "Run image"
+msgid "Run at"
+msgstr "Executar Imagem"
+
+#: pkg/sosreport/sosreport.jsx:293
+#, fuzzy
+#| msgid "Report"
+msgid "Run new report"
+msgstr "Relatório"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+#, fuzzy
+#| msgid "Report"
+msgid "Run report"
+msgstr "Relatório"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Executor"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Executando"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr ""
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux Erros de Controle de Acesso"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux está desativado no sistema"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux está desativado no sistema."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "Política de SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "O status do sistema SELinux é desconhecido."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "Solução de problemas de SELinux"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "SMART auto-teste de $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "Chave SSH"
+
+#: pkg/kdump/kdump-view.jsx:164
+#, fuzzy
+#| msgid "ssh key isn't a path"
+msgid "SSH key isn't a path"
+msgstr "chave ssh não é um caminho"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "Chave SSH"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "STP Atraso de Redirecionamento"
+
+#: pkg/networkmanager/bridge.jsx:113
+#, fuzzy
+msgid "STP hello time"
+msgstr "tempo hello STP"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP Máxima permanência de mensagem"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP Prioridade"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Sábados"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Salvar"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Salvar Mudanças"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Salvar uma nova senha requer o desbloqueio do disco. Por favor, forneça uma "
+"senha de disco atual."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr ""
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr ""
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "PC com caixa vedada"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:310
+#, fuzzy
+#| msgid "DNS search domains"
+msgid "Search domain"
+msgstr "DNS Busca de Domínios"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr ""
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:280
+#, fuzzy
+#| msgid "Minute needs to be a number between 0-59"
+msgid "Second needs to be a number between 0-59"
+msgstr "Minutos precisam ser números entre 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Segundos"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Apagando com segurança $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Atualizações de segurança disponíveis"
+
+#: pkg/packagekit/autoupdates.jsx:289
+#, fuzzy
+#| msgid "Security updates available"
+msgid "Security updates only"
+msgstr "Atualizações de segurança disponíveis"
+
+#: pkg/packagekit/autoupdates.jsx:365
+#, fuzzy
+#| msgid "Security updates available"
+msgid "Security updates will be applied $0 at $1"
+msgstr "Atualizações de segurança disponíveis"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Selecione"
+
+#: pkg/systemd/logs.jsx:321
+#, fuzzy
+#| msgid "select container"
+msgid "Select a identifier"
+msgstr "selecionar contêiner"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+#, fuzzy
+#| msgid "Select"
+msgid "Select method"
+msgstr "Selecione"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Enviar"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Enviando"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Número de série"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Servidor"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Endereço do Servidor"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "O endereço do servidor não pode estar vazio."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "O servidor não pode estar vazio."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "O servidor encerrou a conexão."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Software de servidor"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Serviço"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "O serviço tem um erro"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Logs de Serviço"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Serviços"
+
+#: pkg/storaged/dialog.jsx:1153
+#, fuzzy
+#| msgid "Service is starting"
+msgid "Services using the location"
+msgstr "O serviço está iniciando"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Sessão"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "A sessão está prestes a expirar"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Definir"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Definir nome do host"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Criar Partição em $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Definir uma Senha"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Definir Tempo"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Configurado para"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Configuração"
+
+#: pkg/users/password-dialogs.js:245
+#, fuzzy
+#| msgid "Set password"
+msgid "Set weak password"
+msgstr "Definir uma Senha"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"A configuração se desvia do estado configurado e reverterá na próxima "
+"inicialização."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Configurando"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Configurando o dispositivo de loop $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Configurações"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Gravidade"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Compartilhado"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Shell script"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all images"
+msgid "Show all $0 rows"
+msgstr "Exibir todas as imagens"
+
+#: pkg/systemd/abrtLog.jsx:264
+#, fuzzy
+#| msgid "Show all images"
+msgid "Show all threads"
+msgstr "Exibir todas as imagens"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Show confirmation password"
+msgstr "Confirme a senha"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Exibir digitais"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "New password"
+msgid "Show password"
+msgstr "Nova Senha"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Compactar"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Compactar Logical Volume"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+#, fuzzy
+#| msgid "partition"
+msgid "Shrink partition"
+msgstr "partição"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+#, fuzzy
+msgid "Shrink volume"
+msgstr "Diminuir volume"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Encerrar"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Desligar"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Desde"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Tamanho"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "O tamanho não pode ser negativo"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "O tamanho não pode ser zero"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "O tamanho é muito extenso"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "O tamanho deve ser um número"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "O tamanho deve ser pelo menos $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr ""
+
+#: pkg/shell/index.html:18
+#, fuzzy
+msgid "Skip to content"
+msgstr "Conteúdo"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Slot"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Slot $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+#, fuzzy
+msgid "Snapshot"
+msgstr "Criar Snapshot"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Sockets"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Atualizações de Software"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:63
+#, fuzzy
+#| msgid "Solid-State Disk"
+msgid "Solid State Drive"
+msgstr "Disco de Estado Sólido"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Solução aplicada com êxito"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Falha na solução"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Soluções"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"Algum outro programa está atualmente usando o gerenciador de pacotes, por "
+"favor aguarde ..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr ""
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr ""
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Computador com economia de espaço"
+
+#: pkg/networkmanager/network-interface.jsx:498
+#, fuzzy
+msgid "Spanning tree protocol"
+msgstr "Protocolo Spanning tree"
+
+#: pkg/networkmanager/bridge.jsx:105
+#, fuzzy
+msgid "Spanning tree protocol (STP)"
+msgstr "Protocolo Spanning tree (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Reposição"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Tempo Específico"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Velocidade"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Estável"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Iniciar"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Iniciar e habilitar"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Iniciar Multipath"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Começar serviço"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr ""
+
+#: pkg/users/account-logs-panel.jsx:77
+#, fuzzy
+#| msgid "Start"
+msgid "Started"
+msgstr "Iniciar"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "Iniciando o Dispositivo RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Iniciando swapspace $target"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Estado"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Estático"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Estado"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Stick PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+#, fuzzy
+msgid "Sticky"
+msgstr "Pegajoso"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Pare"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Parar e desabilitar"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Pare e remova"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Parar e desmontar"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Parar dispositivo"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:275
+#, fuzzy
+#| msgid "Create report"
+msgid "Stop report"
+msgstr "Criar relatório"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "Parando o Dispositivo RAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Parando swapspace $target"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Armazenamento"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "O armazenamento não pode ser gerenciado neste sistema."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Logs de Armazenamento"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Armazene a senha"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Senha armazenada"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "$0 Dispositivos de Bloco"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block devices"
+msgstr "$0 Dispositivos de Bloco"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+#, fuzzy
+#| msgid "VDO backing devices can not be made smaller"
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Dispositivos de suporte VDO não podem ser menores"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Stratis filesystem"
+msgstr "Sistema de arquivos"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Stratis filesystems"
+msgstr "Sistema de arquivos"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Sistema de arquivos"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+msgid "Stratis pool"
+msgstr "Pools de Armazenamento"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:97
+#, fuzzy
+#| msgid "Set password"
+msgid "Strong password"
+msgstr "Definir uma Senha"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr ""
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr ""
+
+#: pkg/lib/machine-info.js:78
+#, fuzzy
+msgid "Sub-Chassis"
+msgstr "Sub Chassis"
+
+#: pkg/lib/machine-info.js:73
+#, fuzzy
+msgid "Sub-Notebook"
+msgstr "Sub Notebook"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr ""
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+msgid "Subvolume needs to be mounted"
+msgstr ""
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Copiado com sucesso para a área de transferência"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Copiado com sucesso para a área de transferência!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Domingos"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Swap"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "$0 sistemas de arquivos não podem ser redimensionados aqui."
+
+#: pkg/metrics/metrics.jsx:129
+#, fuzzy
+#| msgid "Swap used"
+msgid "Swap out"
+msgstr "Trocar"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+#, fuzzy
+#| msgid "Switch off $0"
+msgid "Switch of $0"
+msgstr "Desligar $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Desligar $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Ligar $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+#, fuzzy
+msgid "Switch to administrative access"
+msgstr "Senha de Administrador"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr ""
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+#, fuzzy
+#| msgid ""
+#| "Switching off <b>$0</b> will break the connection to the server, and will "
+#| "make the administration UI unavailable."
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Desligando <b>$0</b> irá encerrar a conexão com o servidor, e tornará a "
+"administração da interface do usuário indisponível."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+#, fuzzy
+#| msgid ""
+#| "Switching on <b>$0</b> will break the connection to the server, and will "
+#| "make the administration UI unavailable."
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Ligando <b>$0</b> irá encerrar a conexão com o servidor e tornará a "
+"administração da interface do usuário indisponível."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Sincronizado"
+
+#: pkg/lib/serverTime.js:450
+#, fuzzy
+msgid "Synchronized with $0"
+msgstr "Sincronizado com {{Server}}"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Sincronizando"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "Sincronizando Dispositivo RAID $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Sistema"
+
+#: pkg/sosreport/sosreport.jsx:520
+#, fuzzy
+#| msgid "System modifications"
+msgid "System diagnostics"
+msgstr "Modificações no sistema"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Informação do sistema"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "O sistema está atualizado"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Modificações no sistema"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Hora do sistema"
+
+#: pkg/systemd/services-list.jsx:50
+#, fuzzy
+msgid "Systemd units"
+msgstr "Serviços do Sistema"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tablet"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+#, fuzzy
+msgid "Tang keyserver"
+msgstr "Servidor de chaves Tang"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Alvo"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Alvos"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Equipe"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Porta da Equipe"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Configurações da Porta da Equipe"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminal"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Encerrar Sessão"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Configuração Teste"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "O teste só está disponível enquanto o serviço kdump está em execução."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Testar configurações kdump"
+
+#: pkg/kdump/kdump-view.jsx:374
+#, fuzzy
+#| msgid ""
+#| "This will test kdump settings by crashing the kernel and thereby the "
+#| "system. Depending on the settings, the system may not automatically "
+#| "reboot and the process may take a while."
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Isso irá testar as configurações de kdump, quebrando o kernel e, desse modo, "
+"o sistema. Dependendo das configurações, o sistema não pode reiniciar "
+"automaticamente e o processo pode demorar um pouco."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Testando conexão"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+#, fuzzy
+#| msgid "$0 is not available from any repository."
+msgid "The $0 package is not available from any repository."
+msgstr "$0 não está disponível em nenhum repositório."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "O pacote $0 deve estar instalado."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "O endereço IP ou nome do host não podem conter espaços."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "A matriz RAID está em um estado degradado"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr "O dispositivo RAID deve estar em execução para remover discos."
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr "A conta '$0' será forçado a mudar sua senha no próximo login"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"O estado configurado é desconhecido, pode mudar na próxima inicialização."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"A criação deste dispositivo VDO não foi concluída e o dispositivo não pode "
+"ser usado."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"O usuário atualmente conectado não tem permissão para ver informações sobre "
+"chaves."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:368
+#, fuzzy
+#| msgid "$0 will be installed."
+msgid "The file $0 will be deleted."
+msgstr "$0 será instalado."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+msgid "The filesystem has no assigned mount point."
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:494
+#, fuzzy
+#| msgid "Show fingerprints"
+msgid "The fingerprint should match:"
+msgstr "Exibir digitais"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "O seguinte serviço será reiniciado:"
+msgstr[1] "Os seguintes serviços serão reiniciados:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr ""
+
+#: pkg/users/group-create-dialog.js:83
+#, fuzzy
+#| msgid "MTU must be a positive number"
+msgid "The group ID must be positive integer"
+msgstr "O MTU deve ser um número positivo"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"O nome do grupo só pode conter letras de a-z, dígitos, pontos, traços e "
+"sublinhados"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "A senha da chave não pode estar vazia"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "As senhas da chave não coincidem"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "A chave que você forneceu não era válida."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "O último slot chave não pode ser removido"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "Os serviços listados serão interrompidos à força."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr ""
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "A máquina esta reiniciando"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "A nova senha da chave não pode estar vazia"
+
+#: pkg/shell/hosts_dialog.jsx:679
+#, fuzzy
+#| msgid "The key password can not be empty"
+msgid "The password can not be empty"
+msgstr "A senha da chave não pode estar vazia"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "As senhas não batem"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "As senhas não batem."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"O servidor se recusou a autenticar '$0' usando a autenticação de senha e "
+"nenhum outro método de autenticação suportado está disponível."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr ""
+"O servidor se recusou a autenticar usando quaisquer métodos suportados."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "O usuário $0 não tem permissão para alterar políticas criptográficas"
+
+#: pkg/kdump/kdump-view.jsx:505
+#, fuzzy
+#| msgid "The user <b>$0</b> is not permitted to modify realms"
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "O usuário <b>$0</b> não tem permissão para modificar reinos"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"O nome de usuário coniste em letas de a-z, digitos, pontos, barras e "
+"underline."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"A configuração do navegador da Web impede que o Cockpit seja executado "
+"(inacessível $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Atualmente não há páginas ativas"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"Há dispositivos com vários caminhos no sistema, mas o serviço de "
+"multicaminho não está sendo executado."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "Não existem serviços ativos nesta zona"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Não existem chaves públicas autorizadas para esta conta."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Ocorreu um erro inesperado enquanto conectava-se à máquina."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Essas mudanças serão realizadas:"
+
+#: pkg/storaged/utils.js:286
+#, fuzzy
+msgid "Thin logical volume"
+msgstr "Volume lógico Thin"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Pool para volumes finamente provisionados"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"Esta montagem NFS está em uso e somente suas opções podem ser alteradas."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Este dispositivo VDO não usa todo o seu dispositivo de apoio."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Este dispositivo não pode ser gerenciado aqui."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Este dispositivo não pode ser gerenciado aqui."
+
+#: pkg/storaged/dialog.jsx:1130
+#, fuzzy
+#| msgid "This device is currently used for VDO devices."
+msgid "This device is currently in use."
+msgstr "Este dispositivo é usado atualmente para dispositivos VDO."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Este campo não pode estar vazio"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:303
+#, fuzzy
+#| msgid "The collected information will be stored locally on the system."
+msgid "This information is stored only on the system."
+msgstr "As informações coletadas serão armazenadas localmente no sistema."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Esta máquina já foi adicionada."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Isso pode demorar um pouco"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:333
+#, fuzzy
+#| msgid "This VDO device does not use all of its backing device."
+msgid "This pool does not use all the space on its block devices."
+msgstr "Este dispositivo VDO não usa todo o seu dispositivo de apoio."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "Esta piscina está em estado de degradação."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Este sistema não está registrado"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Este sistema está usando um perfil personalizado"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Este sistema está usando o perfil recomendado"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Esta unidade não foi projetada para ser habilitada explicitamente."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Este usuário já existe"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr ""
+
+#: pkg/static/login.js:212
+#, fuzzy
+#| msgid "This web browser is too old to run Cockpit (missing $0)"
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr "Este navegador é muito antigo para executar o Cockpit (faltando $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Quintas"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Tempo"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Fuso Horário"
+
+#: pkg/systemd/timer-dialog.jsx:155
+#, fuzzy
+#| msgid "Connection failed"
+msgid "Timer creation failed"
+msgstr "Conexão falhou"
+
+#: pkg/systemd/service-details.jsx:725
+#, fuzzy
+#| msgid "Connection failed"
+msgid "Timer deletion failed"
+msgstr "Conexão falhou"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Temporizadores"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Dica: faça a sua senha de chave corresponder à sua senha de login para "
+"autenticar automaticamente contra outros sistemas."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Para obter atualizações de software, este sistema precisa ser registrado na "
+"Red Hat, usando o Portal do Cliente Red Hat ou um servidor de assinatura "
+"local."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Hoje"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr ""
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+#, fuzzy
+msgid "Toggle filters"
+msgstr "Limpar todos filtros"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Muitos dados"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Ferramentas"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Top 5 serviços de CPU"
+
+#: pkg/metrics/metrics.jsx:963
+#, fuzzy
+#| msgid "Top 5 CPU services"
+msgid "Top 5 disk usage services"
+msgstr "Top 5 serviços de CPU"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "Top 5 serviços de memória"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Tamanho total: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Torre"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr ""
+
+#: pkg/networkmanager/plots.js:39
+#, fuzzy
+#| msgid "Team settings"
+msgid "Transmitting"
+msgstr "Configurações da Equipe"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+#, fuzzy
+#| msgid "Triggers"
+msgid "Trigger"
+msgstr "Triggers"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Disparado por"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Triggers"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Solução de problemas"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+#, fuzzy
+#| msgid "Troubleshoot"
+msgid "Troubleshoot…"
+msgstr "Solução de problemas…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+#, fuzzy
+#| msgid "Untrusted host"
+msgid "Trust and add host"
+msgstr "Host não confiável"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Chave de confiança"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr ""
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Tentar novamente"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Tentando sincronizar com $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Terças"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Falhou ao iniciar Tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned não está disponível"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned não está em execução"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned está fora"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Tipo"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "O nome não pode conter o caractere '$0'."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Tipo para filtrar"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Não é possível aplicar esta solução automaticamente"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Não é possível conectar a esse endereço"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "Incapaz de contatar $0."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Não foi possível entrar em contato com o host $0. Certifique-se de que o ssh "
+"esteja rodando na porta $1 ou especifique outra porta no endereço."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Não é possível obter detalhes de alerta."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Não é possível acessar o servidor"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Não é possível remover a unidade montade"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+#, fuzzy
+#| msgid "Encrypted logical volume of $0"
+msgid "Unable to repair logical volume $0"
+msgstr "Volume Lógico Criptografado de $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+#, fuzzy
+#| msgid "Unable to run fix: %0"
+msgid "Unable to run fix: $0"
+msgstr "Não é possível executar correção: %0"
+
+#: pkg/kdump/kdump-view.jsx:209
+#, fuzzy
+#| msgid "Unable to apply settings: $0"
+msgid "Unable to save settings"
+msgstr "ão é possível aplicar as configurações: $0"
+
+#: pkg/kdump/kdump-view.jsx:214
+#, fuzzy
+#| msgid "Unable to apply settings: $0"
+msgid "Unable to save settings: $0"
+msgstr "ão é possível aplicar as configurações: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Incapaz de iniciar setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Não é possível desmontar o sistema de arquivos"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Indisponível"
+
+#: pkg/packagekit/kpatch.jsx:238
+#, fuzzy
+#| msgid "Unavailable"
+msgid "Unavailable packages"
+msgstr "Indisponível"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr ""
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Erro inesperado"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Dados não reconhecidos"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Unidade"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Desconhecido"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Desconhecido \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Desconhecido ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Aplicação Desconhecida"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Configuração desconhecida"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Nome de host desconhecido"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Nome de serviço desconhecido"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Tipo desconhecido"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Destravar"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+#, fuzzy
+#| msgid "Unlock at boot"
+msgid "Unlock automatically on boot"
+msgstr "Desbloquear na inicialização"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Encrypted data"
+msgid "Unlock encrypted Stratis pool"
+msgstr "Dados encriptados"
+
+#: pkg/shell/credentials.jsx:309
+#, fuzzy
+#| msgid "Unlock key"
+msgid "Unlock key $0"
+msgstr "Desbloquear Chave"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Desbloqueando $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Desbloqueando o disco"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Interfaces Não Gerenciadas"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Desmontar"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+#, fuzzy
+#| msgid "Unmount filesystem"
+msgid "Unmount filesystem $0"
+msgstr "Desmontar o sistema de arquivos"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Desmontando $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Não nomeado"
+
+#: pkg/systemd/service-details.jsx:184
+#, fuzzy
+msgid "Unpin unit"
+msgstr "Serviços do Sistema"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Dados não reconhecidos"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "Unrecognized data can not be made smaller here."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Dados não reconhecidos não podem ser reduzidos aqui."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Dados não reconhecidos não podem ser reduzidos aqui."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Volume não suportado"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr ""
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Host não confiável"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Atualizar"
+
+#: pkg/packagekit/updates.jsx:754
+#, fuzzy
+#| msgid "Updates available"
+msgid "Update Success Table"
+msgstr "Atualizações disponíveis"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Atualizar histórico"
+
+#: pkg/apps/application-list.jsx:163
+#, fuzzy
+msgid "Update package information"
+msgstr "Informações do pacote"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "A atualização foi bem sucedida"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Atualizado"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr ""
+"Os pacotes atualizados podem exigir uma reinicialização para ter efeito."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Atualizações disponíveis"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Histórico de atualizações"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Atualizando"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr ""
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr ""
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Uso"
+
+#: pkg/storaged/storage-controls.jsx:206
+#, fuzzy
+#| msgid "Usage of $0 CPU core"
+#| msgid_plural "Usage of $0 CPU cores"
+msgid "Usage of $0"
+msgstr "Uso de $0 núcleo da CPU"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+#, fuzzy
+#| msgid "Used"
+msgid "Use"
+msgstr "Usado"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+#, fuzzy
+#| msgid "Compression"
+msgid "Use compression"
+msgstr "Compressão"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+#, fuzzy
+#| msgid "Deduplication"
+msgid "Use deduplication"
+msgstr "Desduplicação"
+
+#: pkg/shell/credentials.jsx:133
+#, fuzzy
+#| msgid "ssh key"
+msgid "Use key"
+msgstr "chave ssh"
+
+#: pkg/users/account-create-dialog.js:114
+#, fuzzy
+#| msgid "User password"
+msgid "Use password"
+msgstr "Senha de Usuário"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Use as seguintes chaves para autenticar contra outros sistemas"
+
+#: pkg/sosreport/sosreport.jsx:322
+#, fuzzy
+#| msgid "Last login"
+msgid "Use verbose logging"
+msgstr "Último Login"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Usado"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Usuário"
+
+#: pkg/users/account-create-dialog.js:104
+#, fuzzy
+#| msgid "User"
+msgid "User ID"
+msgstr "Usuário"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:181
+#, fuzzy
+#| msgid "MTU must be a positive number"
+msgid "User ID must be a positive integer"
+msgstr "O MTU deve ser um número positivo"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:184
+#, fuzzy
+#| msgid "Name cannot be longer than $0 bytes"
+msgid "User ID must not be lower than $0"
+msgstr "O nome não pode ser maior que $0 bytes"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Nome do usuário"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "O nome de usuário não pode estar vazio"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Nome de Usuário"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "Edit Tang keyserver"
+msgid "Using Tang server"
+msgstr "Editar o servidor de chaves Tang"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "Dispositivos de suporte VDO não podem ser menores"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "Dispositivo VDO $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+#, fuzzy
+msgid "VLAN ID"
+msgstr "VLAN Id"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Validando endereço"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Validando token de autenticação"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Fabricante"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Verificado"
+
+#: pkg/shell/hosts_dialog.jsx:489
+#, fuzzy
+#| msgid "Fingerprint"
+msgid "Verify fingerprint"
+msgstr "Digital"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Verificar chave"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Verificando"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Versão"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Apagando com muita segurança $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+#, fuzzy
+#| msgid "View all logs"
+msgid "View all CPUs"
+msgstr "Ver todos os logs"
+
+#: pkg/metrics/metrics.jsx:803
+#, fuzzy
+#| msgid "View all logs"
+msgid "View all disks"
+msgstr "Ver todos os logs"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Ver todos os logs"
+
+#: pkg/systemd/service-details.jsx:549
+#, fuzzy
+#| msgid "Filter services"
+msgid "View all services"
+msgstr "Serviços de filtro"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1147
+#, fuzzy
+#| msgid "View all logs"
+msgid "View detailed logs"
+msgstr "Ver todos os logs"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr ""
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:351
+#, fuzzy
+#| msgid "View all logs"
+msgid "View logs"
+msgstr "Ver todos os logs"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr ""
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Ver site do projeto"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:619
+#, fuzzy
+#| msgid "View all logs"
+msgid "View update log"
+msgstr "Ver todos os logs"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "Visualizar informações de memória requer acesso administrativo."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+#, fuzzy
+#| msgid "Firewall"
+msgid "Visit firewall"
+msgstr "Firewall"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Grupo de volumes"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Volume group is missing physical volumes"
+msgstr "Removendo volume físico de $target"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Aguardando"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Esperando por detalhes..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr ""
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"Aguardando outros programas terminarem de usar o gerenciador de pacotes ..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Aguardando que outras operações de gerenciamento de software terminem"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Requerido por"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Requer"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Aviso e acima"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+#, fuzzy
+#| msgid "Set password"
+msgid "Weak password"
+msgstr "Definir uma Senha"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Console web"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Console da Web para servidores Linux"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+#, fuzzy
+#| msgid "Web Console for Linux servers"
+msgid "Web Console will restart"
+msgstr "Console da Web para servidores Linux"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr ""
+
+#: pkg/shell/shell-modals.jsx:66
+#, fuzzy
+msgid "Web console logo"
+msgstr "Consoles"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Quartas"
+
+#: pkg/systemd/timer-dialog.jsx:246
+#, fuzzy
+#| msgid "Weeks"
+msgid "Weekly"
+msgstr "Semanas"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Semanas"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Quando"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Branco"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:124
+msgid "World wide name"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Escrita"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Maioria-Escrita"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Escrevendo"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Nome de usuário ou senha incorretos"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Sim"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr ""
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Você não está autorizado a modificar o firewall."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Você não tem permissão para exibir as chaves públicas autorizadas para esta "
+"conta."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr ""
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Você deve esperar mais tempo para alterar sua senha"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr ""
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr ""
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr ""
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Sua conta"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Seu servidor fechará a conexão em breve. Você pode reconectar depois de ter "
+"reiniciado."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Sua sessão foi encerrada."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Sua sessão expirou. Por favor, faça o login novamente."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Zona"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[dados binários]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[sem dados]"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "acesso"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "ativo"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "complemento"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "complemento"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+#, fuzzy
+msgid "after network"
+msgstr "Rede isolada"
+
+#: pkg/apps/manifest.json:0
+#, fuzzy
+msgid "apps"
+msgstr "apps"
+
+#: pkg/packagekit/manifest.json:0
+#, fuzzy
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "no"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1112
+#, fuzzy
+#| msgid "Create VDO device"
+msgid "backing device for VDO device"
+msgstr "Criar dispositivo VDO"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "inicialização"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "ponte"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "Outros dispositivos"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "Outros dispositivos"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+msgid "btrfs subvolume"
+msgstr "Volume de armazenamento"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Volumes de Armazenamento"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+msgid "btrfs volume"
+msgstr "Volume de armazenamento"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "correção de bug"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "bytes"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+msgid "cache"
+msgstr "Cache"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+#, fuzzy
+#| msgid "Change"
+msgid "change"
+msgstr "Alterar"
+
+#: pkg/metrics/metrics.jsx:654
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "cockpit-podman is not installed"
+msgstr "Cockpit não está instalado"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "comando"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "console"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "cpu"
+msgstr "cpu"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr ""
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "$0 dados"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "data"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Desativar"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "depuração"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+#, fuzzy
+#| msgid "Delete"
+msgid "delete"
+msgstr "Excluir"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "desabilitado"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "disco"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "discos"
+
+#: pkg/packagekit/manifest.json:0
+#, fuzzy
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr ""
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr ""
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted $0"
+msgid "encrypted"
+msgstr "Encriptado"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "criptografia"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "Aprimoramento"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "erro"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "toda Sexta-feira"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "toda Segunda-feira"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "todo Sabádo"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "todo Domingo"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "toda Quinta-feira"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "toda Terça-feira"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "toda Quarta-feira"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "todo dia"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "extensão"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "falha ao listar chaves locais de ssh: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "sistema de arquivos"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+msgid "firewall"
+msgstr "firewall"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "apenas para o kernel atual e futuros"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "apenas para o kernel atual"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr ""
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+#, fuzzy
+#| msgctxt "<date> from <host>"
+#| msgid "$0 from $1"
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "$0 de $1"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+#, fuzzy
+#| msgctxt "<date> on <terminal>"
+#| msgid "$0 on $1"
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "$0 em $1"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "hardware"
+msgstr "hardware"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "histórico"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "host"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI Drive"
+msgstr "Alvos iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "Alvos iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "Adicionar Portal iSCSI"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "ignorar falhas"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr ""
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "inconsistente"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "inicializar"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr ""
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+msgid "interface"
+msgstr "interface"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "inválido: vários destinos definidos"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "manpage do journalctl"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "status kdump"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "chaves"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "login"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "member of RAID device"
+msgid "member of MDRAID device"
+msgstr "membro do Dispositivo RAID"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "membro do grupo Stratis"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "memória"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "métricas"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "mitigação"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "mais detalhes"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr ""
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "rede"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "nunca montar na inicialização"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "exportação NFS está vazia"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "servidor nfs está vazio"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "nenhum"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+#, fuzzy
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "de $0 núcleo da CPU"
+msgstr[1] "de $0 núcleos da CPU"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "em $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "sistema operacional"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr ""
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "pacote"
+
+#: pkg/packagekit/manifest.json:0
+#, fuzzy
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "partição"
+
+#: pkg/users/manifest.json:0
+#, fuzzy
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "senha"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+#| msgid "Store metrics"
+msgid "performance"
+msgstr "Armazenar métricas"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "volume físico do grupo de volumes LVM2"
+
+#: pkg/apps/manifest.json:0
+#, fuzzy
+msgid "plugin"
+msgstr "plugin"
+
+#: pkg/metrics/metrics.jsx:1833
+#, fuzzy
+#| msgid "$0 service has failed"
+#| msgid_plural "$0 services have failed"
+msgid "pmlogger.service has failed"
+msgstr "$0 serviço falhou"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1826
+#, fuzzy
+#| msgid "Service is running"
+msgid "pmlogger.service is not running"
+msgstr "O serviço está em execução"
+
+#: pkg/metrics/metrics.jsx:648
+#, fuzzy
+msgid "pod"
+msgstr "pod"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "somente leitura"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "recomendado"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "remover do LVM2"
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "remove from RAID"
+msgid "remove from MDRAID"
+msgstr "remover do RAID"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "remover do LVM2"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr ""
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr ""
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "segurança"
+
+#: pkg/selinux/manifest.json:0
+#, fuzzy
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr ""
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr ""
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "mostrar menos"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "mostrar mais"
+
+#: pkg/storaged/block/resize.jsx:566
+#, fuzzy
+#| msgid "Shrink"
+msgid "shrink"
+msgstr "Compactar"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "socket"
+msgstr "socket"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "solução"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "detalhes da solução"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr ""
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "chave ssh não é um caminho"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "servidor ssh está vazio"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "parar a inicialização em caso de falha"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+#, fuzzy
+#| msgid "Stopped"
+msgid "stopped"
+msgstr "Parado"
+
+#: pkg/metrics/metrics.jsx:112
+#, fuzzy
+#| msgid "yes"
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr ""
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "time"
+msgstr "time"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr ""
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "desconhecido"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "alvo desconhecido"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "unmask"
+msgstr "unmask"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "unmount"
+msgstr "unmount"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "espaço não particionado em $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "usuário"
+
+#: pkg/users/manifest.json:0
+#, fuzzy
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+#, fuzzy
+msgid "version"
+msgstr "versão"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+msgid "volume"
+msgstr "volume"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "aviso"
+
+#: pkg/networkmanager/wireguard.jsx:84
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "wireguard-tools package is not installed"
+msgstr "PackageKit não está instalado"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "sim"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "zona"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Serviço Kdump não instalado. Assegure-se de que o pacote kexec-tools está "
+#~ "instalado."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Nenhuma memória reservada. Acrescente uma opção crashkernel à linha de "
+#~ "comando do kernel (por exemplo, em /etc/default/grub) para reservar "
+#~ "memória no momento de inicialização. Exemplo: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "O serviço está em execução"
+
+#~ msgid "Service is starting"
+#~ msgstr "O serviço está iniciando"
+
+#~ msgid "Service is stopped"
+#~ msgstr "O serviço parou"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Serviço está a parar"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Isso testará a configuração do kdump, quebrando o kernel."
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (encriptado)"
+
+#, fuzzy
+#~| msgid "Storage pool"
+#~| msgid_plural "Storage pools"
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 Pool de Armazenamento"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 cache"
+
+#, fuzzy
+#~| msgid "unknown target"
+#~ msgid "$0 of unknown tier"
+#~ msgstr "alvo desconhecido"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 livre"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr ""
+#~ "Um disco de reposição precisa ser adicionado primeiro antes que este "
+#~ "disco possa ser removido."
+
+#~ msgid "Backing device"
+#~ msgstr "Dispositivo de apoio"
+
+#~ msgid "Block"
+#~ msgstr "Bloco"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Capacidade"
+
+#~ msgid "Content"
+#~ msgstr "Conteúdo"
+
+#~ msgid "Create devices"
+#~ msgstr "Criar dispositivos"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Dispositivo"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Arquivo de dispositivo"
+
+#~ msgid "Devices"
+#~ msgstr "Dispositivos"
+
+#~ msgid "Drives"
+#~ msgstr "Unidades"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Volumes criptografados precisam ser desbloqueados antes de serem "
+#~ "redimensionados."
+
+#, fuzzy
+#~| msgid "Filesystem directory"
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Diretório do sistema de arquivos"
+
+#~ msgid "Filesystems"
+#~ msgstr "Sistema de Arquivos"
+
+#~ msgid "Free"
+#~ msgstr "Livre"
+
+#~ msgid "Inactive volume"
+#~ msgstr "Volume inativo"
+
+#, fuzzy
+#~| msgid "Keyserver"
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Servidor de chaves"
+
+#, fuzzy
+#~| msgid "Add member"
+#~ msgid "LVM2 member"
+#~ msgstr "Adicionar membro"
+
+#~ msgid "Locked devices"
+#~ msgstr "Dispositivos bloqueados"
+
+#, fuzzy
+#~| msgctxt "storage-id-desc"
+#~| msgid "Encrypted data"
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Dados encriptados"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Modelo"
+
+#~ msgid "NFS mounts"
+#~ msgstr "Montagens NFS"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "Suporte ao NFS não instalado"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Nenhum volume NFS montado"
+
+#~ msgid "No devices"
+#~ msgstr "Nenhum dispositivo"
+
+#~ msgid "No drives attached"
+#~ msgstr "Não há unidades anexadas"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Nenhum destino iSCSI configurado"
+
+#, fuzzy
+#~| msgid "Block device for filesystems"
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Dispositivo de bloqueio para sistemas de arquivos"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Outros Dados"
+
+#, fuzzy
+#~| msgid "$0 block device"
+#~ msgid "Partitioned block device"
+#~ msgstr "$0 Dispositivos de Bloco"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Frase de senha"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "Volumes físicos não podem ser redimensionados aqui."
+
+#~ msgid "Pool"
+#~ msgstr "Pool"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Pool para Volumes Finos"
+
+#, fuzzy
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "Nível de RAID"
+
+#~ msgid "RAID member"
+#~ msgstr "Membro RAID"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Drive removível"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Mostrar $0 dispositivo"
+#~ msgstr[1] "Mostrar todos os $0 dispositivos"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Mostrar $0 drive"
+#~ msgstr[1] "Mostrar todos os $0 drives"
+
+#~ msgid "Show all"
+#~ msgstr "Exibir todos"
+
+#~ msgid "Source"
+#~ msgstr "Fonte"
+
+#, fuzzy
+#~ msgid "Start pool"
+#~ msgstr "Pools de Armazenamento"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Inicie o pool para ver os sistemas de arquivos."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Estado"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Criar pool de armazenamento"
+
+#, fuzzy
+#~| msgid "Interface members"
+#~ msgid "Stratis member"
+#~ msgstr "Membros da interface"
+
+#, fuzzy
+#~ msgid "Stratis pool $0"
+#~ msgstr "Pools de Armazenamento"
+
+#~ msgid "Support is installed."
+#~ msgstr "O suporte está instalado."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "O dispositivo RAID deve estar em execução para adicionar discos "
+#~ "sobressalentes."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "O último disco de um dispositivo RAID não pode ser removido."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr ""
+#~ "O último volume físico de um grupo de volumes não pode ser removido."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Não há espaço livre suficiente em outro lugar para remover este volume "
+#~ "físico. Pelo menos mais $0 de espaço livre é necessário."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr ""
+#~ "Este disco não pode ser removido enquanto o dispositivo está se "
+#~ "recuperando."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr "Este volume precisa ser ativado antes de poder ser redimensionado."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Dados não reconhecidos"
+
+#, fuzzy
+#~| msgid "Usage"
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Uso"
+
+#, fuzzy
+#~| msgid "Used"
+#~ msgid "Used for"
+#~ msgstr "Usado"
+
+#, fuzzy
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "Apoio VDO"
+
+#~ msgid "VDO device"
+#~ msgstr "Dispositivo VDO"
+
+#~ msgid "Volume"
+#~ msgstr "Volume"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automático (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Aceitar a chave e conectar"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Volumes criptografados não podem ser redimensionados aqui."
+
+#, fuzzy
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Servidor de chaves Tang"
+
+#, fuzzy
+#~| msgid "New passphrase"
+#~ msgid "Use a passphrase"
+#~ msgstr "Nova senha"
+
+#, fuzzy
+#~| msgid "Make sure the key hash from the Tang server matches:"
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr "Certifique-se de que o hash da chave do servidor Tang corresponda:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Verificar manualmente com o SSH: "
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "O número e o tipo da porta não correspondem"
+
+#, fuzzy
+#~| msgctxt "storage-id-desc"
+#~| msgid "Encrypted data"
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Dados encriptados"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Erro durante a exclusão alerta: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Não é possível obter alerta: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 bytes de data bynária]"
+
+#, fuzzy
+#~| msgid "Disk I/O"
+#~ msgid "Disk I/O spike"
+#~ msgstr "Entrada e Saida de disco"
+
+#~ msgid "Event"
+#~ msgstr "Evento"
+
+#, fuzzy
+#~ msgid "Event logs"
+#~ msgstr "Logs de rede"
+
+#~ msgid "Memory spike"
+#~ msgstr "Pico de memória"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Pico de E/S de rede"
+
+#~ msgid "ssh key"
+#~ msgstr "chave ssh"
+
+#, fuzzy
+#~| msgid "Mount at boot"
+#~ msgid "Never mount at boot"
+#~ msgstr "Monte na Inicialização"
+
+#, fuzzy
+#~| msgid "Not found"
+#~ msgid "Unit not found"
+#~ msgstr "Não encontrado"
+
+#~ msgid "Validating key"
+#~ msgstr "Validando chave"
+
+#~ msgid "Container administrator"
+#~ msgstr "Administrador de contêineres"
+
+#~ msgid "Image builder"
+#~ msgstr "Criador de Imagem de Disco"
+
+#~ msgid "Roles"
+#~ msgstr "Papéis"
+
+#~ msgid "Server administrator"
+#~ msgstr "Administrador do servidor"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Grupo unix: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Copiado com sucesso para o teclado"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Relatório de diagnóstico"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Dump do Kernel"
+
+#~ msgid "Software Updates"
+#~ msgstr "Atualizações de software"
+
+#~ msgid "Update log"
+#~ msgstr "Atualizar Log"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "destino de despejo nfs não é formatado como servidor: caminho"
+
+#, fuzzy
+#~| msgid "Zone"
+#~ msgid "$0 Zone"
+#~ msgstr "Zona"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Criar um relatório de diagnostico"
+
+#~ msgid "Done!"
+#~ msgstr "Feito!"
+
+#~ msgid "Download report"
+#~ msgstr "Relatório de download"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Nenhum arquivo foi criado."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Por favor, confirme a remoção de $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "O arquivo gerado contém dados considerados confidenciais e seu conteúdo "
+#~ "deve ser revisado pela organização de origem antes de ser passado para "
+#~ "quaisquer terceiros."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Esta ferramenta irá coletar a configuração do sistema e informações "
+#~ "diagnósticas deste sistema para diagnosticar problemas com o mesmo."
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Este pacote não é compatível com esta versão do Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Este pacote requer a versão do Cockpit %s ou posterior"
+
+#, fuzzy
+#~| msgid "Store metrics"
+#~ msgid "Performance Metrics"
+#~ msgstr "Armazenar métricas"
+
+#, fuzzy
+#~| msgid "IPv4 only"
+#~ msgid "Save only"
+#~ msgstr "Somente IPv4"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB total"
+
+#~ msgid "Apply"
+#~ msgstr "Aplicar"
+
+#~ msgid "Access"
+#~ msgstr "Acesso"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "O DISCO ESTÁ FALHANDO"
+
+#, fuzzy
+#~| msgid "Logical size"
+#~ msgid "Logical Size"
+#~ msgstr "Tamanho Lógico"
+
+#, fuzzy
+#~| msgid "Security updates available"
+#~ msgid "Security updates "
+#~ msgstr "Atualizações de segurança "
+
+#~ msgid "Updates "
+#~ msgstr "Atualizações "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Opcional)"
+
+#~ msgid "Active since"
+#~ msgstr "Ativo desde"
+
+#~ msgid "Affected locations"
+#~ msgstr "Locais afetados"
+
+#~ msgid "Process"
+#~ msgstr "Processo"
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Remove and delete"
+#~ msgstr "Pare e apague"
+
+#, fuzzy
+#~ msgid "Remove and format"
+#~ msgstr "Formato de Login"
+
+#, fuzzy
+#~ msgid "Remove and grow"
+#~ msgstr "Formato de Login"
+
+#, fuzzy
+#~ msgid "Remove and initialize"
+#~ msgstr "Formato de Login"
+
+#, fuzzy
+#~ msgid "Remove and shrink"
+#~ msgstr "Formato de Login"
+
+#, fuzzy
+#~| msgid "Remove device"
+#~ msgid "Remove and stop device"
+#~ msgstr "Remover dispositivo"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "O sistema de arquivos está em uso por sessões de login e serviços do "
+#~ "sistema. O processo interromperá isso."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "O sistema de arquivos está em uso por sessões de login. O processo "
+#~ "interromperá isso."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "O sistema de arquivos está em uso pelos serviços do sistema. O processo "
+#~ "interromperá isso."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Este dispositivo possui sistemas de arquivos atualmente em uso. A "
+#~ "continuação desmontará todos os sistemas de arquivos nele."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Este dispositivo é usado atualmente para grupos de volumes."
+
+#, fuzzy
+#~| msgid ""
+#~| "This device is currently used for volume groups. Proceeding will remove "
+#~| "it from its volume groups."
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Este dispositivo é usado atualmente para grupos de volumes. O processo "
+#~ "irá removê-lo de seus grupos de volumes."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Este dispositivo é atualmente usado por dispositivos RAID."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Este dispositivo é usado atualmente para dispositivos RAID. O processo "
+#~ "irá removê-lo de seus dispositivos RAID."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Este dispositivo é usado atualmente para grupos de volumes."
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Unmount and delete"
+#~ msgstr "Pare e apague"
+
+#, fuzzy
+#~| msgid "Unmounting $target"
+#~ msgid "Unmount and format"
+#~ msgstr "Desmontando $target"
+
+#, fuzzy
+#~| msgid "Unmounting $target"
+#~ msgid "Unmount and grow"
+#~ msgstr "Desmontando $target"
+
+#, fuzzy
+#~| msgid "Unmounting $target"
+#~ msgid "Unmount and initialize"
+#~ msgstr "Desmontando $target"
+
+#, fuzzy
+#~| msgid "Unmounting $target"
+#~ msgid "Unmount and shrink"
+#~ msgstr "Desmontando $target"
+
+#, fuzzy
+#~| msgid "On a mounted device"
+#~ msgid "Unmount and stop device"
+#~ msgstr "Em um dispositivo montado"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 of $1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "Bloqueado"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Volume físico de $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Membro do Dispositivo RAID $0"
+
+#~ msgid "Menu"
+#~ msgstr "Menu"
+
+#, fuzzy
+#~ msgid "VDO backing"
+#~ msgstr "Apoio VDO"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Criar pool de armazenamento"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "Opções de Montagem"
+
+#, fuzzy
+#~| msgid "Reset Storage Pool"
+#~ msgid "Stratis Pool"
+#~ msgstr "Resetar Pool de Armazenamento"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Um disco é necessário."
+
+#~ msgid "Disk"
+#~ msgstr "Disco"
+
+#~ msgid "Install VDO support"
+#~ msgstr "Instale o suporte do VDO"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Use a emulação de 512 bytes"
+
+#~ msgid "System services"
+#~ msgstr "Serviços do Sistema"
+
+#, fuzzy
+#~| msgid "Create diagnostic report"
+#~ msgid "Create diagnostic report "
+#~ msgstr "Criar um relatório de diagnostico "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr ""
+#~ "Conectando simultaneamente a mais de {{ limit }} as máquinas não são "
+#~ "suportadas."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "Baseado no Kerberos SSO"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Logar em {{host}}"
+
+#, fuzzy
+#~ msgid "The new key passwords do not match"
+#~ msgstr "As senhas não batem"
+
+#, fuzzy
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr ""
+#~ "O Cockpit não foi capaz de entrar em contato {{#strong}}{{host}}{{/"
+#~ "strong}}."
+
+#, fuzzy
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "O Cockpit não pôde fazer login no {{#strong}}{{host}}{{/strong}}. "
+#~ "{{#can_sync}}Talvez você queira tentar {{#sync_link}}sincronizar "
+#~ "usuários{{/sync_link}}.{{/can_sync}} Para obter mais opções de "
+#~ "autenticação e suporte a solução de problemas, atualize o cockpit-ws para "
+#~ "uma versão mais recente."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Não foi possível fazer login em {{#strong}}{{host}}{{/strong}}. Para se "
+#~ "conectar a este host, você precisará habilitar um dos seguintes métodos "
+#~ "de autenticação na configuração do sshd em {{#strong}}{{host}}{{/strong}}:"
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "{{host}} chave alterada"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Criando essa ligação a conexão com o servidor será encerrada, e tornará a "
+#~ "administração da interface do usuário indisponível."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Criando essa ponte a conexão com o servidor será encerrada, e tornará a "
+#~ "administração da interface do usuário indisponível."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Criando essa equipe a conexão com o servidor será encerrada e tornará a "
+#~ "administração da interface do usuário indisponível."
+
+#~ msgid "IP settings"
+#~ msgstr "Configurações de IP"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Desligando <b>$0</b> irá encerrar a conexão com o servidor, e tornará a "
+#~ "administração da interface do usuário indisdisponível."
+
+#~ msgid "Administrator password"
+#~ msgstr "Senha de Administrador"
+
+#~ msgid "Computer OU"
+#~ msgstr "Computador OU"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "A exclusão de um dispositivo RAID apagará todos os dados nele."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "A exclusão de um grupo de volumes apaga todos os dados do mesmo."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Não sobrescrever dados existentes"
+
+#~ msgid "Erase"
+#~ msgstr "Apagar"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Formate Disco $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr "Formatar esse dispositivo apagará todos os dados contidos nele."
+
+#~ msgid "More"
+#~ msgstr "Mais"
+
+#, fuzzy
+#~ msgid "Not joined"
+#~ msgstr "Não montado"
+
+#~ msgid "One time password"
+#~ msgstr "Uma senha do tempo"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 de $1 em $2"
+
+#, fuzzy
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "A hora precisa ser um número entre 0-23"
+
+#~ msgid "Last login:"
+#~ msgstr "Último login:"
+
+#, fuzzy
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Minutos precisam ser números entre 0-59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "Houve $0 falha na tentativa de login desde o último login bem-sucedido."
+#~ msgstr[1] ""
+#~ "Houveram $0 falhas na tentativa de login desde o último login bem-"
+#~ "sucedido."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "e.g. \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 Zonas Ativas"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 ocorrência"
+#~ msgstr[1] "$0 ocorrências"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Licenciado em:"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Desbloquear somente leitura"
+
+#, fuzzy
+#~ msgid "Show filters"
+#~ msgstr "Exibir digitais"
+
+#, fuzzy
+#~ msgid "e.g."
+#~ msgstr "e.g. \"$0\""
+
+#, fuzzy
+#~ msgid "Toggle session settings"
+#~ msgstr "Alterar as configurações"
+
+#~ msgid "(none)"
+#~ msgstr "(nenhum)"
+
+#~ msgid "Create timers"
+#~ msgstr "Criar Temporizadores"
+
+#~ msgid "Run"
+#~ msgstr "Executar"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select unit state"
+#~ msgstr "Selecione o estado da unidade"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Forçar a remoção da senha em $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Se tang-show-keys não estiver disponível, execute o seguinte:"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "E se tang-show-keys não estiver disponível?"
+
+#, fuzzy
+#~ msgid "key slot $0"
+#~ msgstr "slot chave $0"
+
+#, fuzzy
+#~| msgid "Automatic updates"
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Atualizações automáticas"
+
+#, fuzzy
+#~| msgid "IP configuration"
+#~ msgid "$0 CPU configuration"
+#~ msgstr "$0 Configuração de CPU"
+
+#, fuzzy
+#~| msgid "$0 network"
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 Rede"
+#~ msgstr[1] "$0 Redes"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 está disponível para a maioria dos sistemas operacionais. Para instalá-"
+#~ "lo, procure-o no GNOME Software ou execute o seguinte:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 ajuste de memória"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 Rede"
+
+#, fuzzy
+#~| msgid "vCPUs"
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPU"
+#~ msgstr[1] "$0 vCPUs"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 detalhes de vCPU"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 configurações de interface de rede virtual"
+
+#~ msgid "Add network interface"
+#~ msgstr "Adicionar Interface de Rede"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Adicionar interface de rede virtual"
+
+#~ msgid "Additional"
+#~ msgstr "Adicional"
+
+#, fuzzy
+#~ msgid "Address not within subnet"
+#~ msgstr "Endereço não está dentro da sub-rede"
+
+#, fuzzy
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr "Após excluir o snapshot, todo o conteúdo capturado será perdido."
+
+#~ msgid "Always attach"
+#~ msgstr "Sempre anexar"
+
+#, fuzzy
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Ao anexa-lo, esse disco será compartilhado por todas as VM que o "
+#~ "utilizarem."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Iniciar automaticamente a libvirt na inicialização"
+
+#, fuzzy
+#~ msgid "Autostart"
+#~ msgstr "Auto-iniciar"
+
+#~ msgid "Boot order"
+#~ msgstr "Ordem de Inicialização"
+
+#~ msgid "Bus"
+#~ msgstr "Bus"
+
+#, fuzzy
+#~| msgid "VCPU settings could not be saved"
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "Configurações de VCPU não puderam ser salvas"
+
+#~ msgid "CPU type"
+#~ msgstr "Tipo da CPU"
+
+#, fuzzy
+#~ msgid "Change firmware"
+#~ msgstr "Alterar senha"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "As alterações entrarão em vigor após o desligamento da VM"
+
+#, fuzzy
+#~| msgid ""
+#~| "Clicking \"Launch Remote Viewer\" will download a .vv file and launch $0."
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "Clicar em \"Iniciar Visualizador Remoto\" fará o download de um arquivo ."
+#~ "vv e será iniciado $0."
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Não é possível carregar a imagem"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Confirme esta ação"
+
+#~ msgid "Connect"
+#~ msgstr "Conectar"
+
+#, fuzzy
+#~| msgid "Connect with any $0 viewer application."
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr ""
+#~ "Conecte-se com qualquer aplicativo de visualização para seguintes os "
+#~ "protocolos"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Conectando-se ao serviço de virtualização"
+
+#~ msgid "Connection"
+#~ msgstr "Conexão"
+
+#, fuzzy
+#~| msgid "Consoles"
+#~ msgid "Console"
+#~ msgstr "Console"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Núcleos por soquete"
+
+#, fuzzy
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Não foi possível redefinir o pool de armazenamento"
+
+#, fuzzy
+#~| msgid "crashed"
+#~ msgid "Crashed"
+#~ msgstr "Travou"
+
+#~ msgid "Create VM"
+#~ msgstr "Criar VM"
+
+#, fuzzy
+#~| msgid "Create partition on $0"
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Criar Partição em $0"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Criar nova máquina virtual"
+
+#, fuzzy
+#~ msgid "Create virtual network"
+#~ msgstr "Criar rede virtual"
+
+#, fuzzy
+#~ msgid "Creating VM"
+#~ msgstr "Criar VM"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Criação da vm $0 falhou"
+
+#, fuzzy
+#~ msgid "Creation time"
+#~ msgstr "Tempo de criação"
+
+#, fuzzy
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+Del"
+
+#~ msgid "Current allocation"
+#~ msgstr "Alocação atual"
+
+#~ msgid "DHCP range"
+#~ msgstr "Intervalo DHCP"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Excluir arquivos de armazenamento associados:"
+
+#, fuzzy
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Apagar pool de armazenamento $0"
+
+#, fuzzy
+#~| msgid "Desktop"
+#~ msgid "Desktop viewer"
+#~ msgstr "Desktop"
+
+#, fuzzy
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Desconectado do console serial. Clique no botão conectar."
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Disco falhou ao ser anexado"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Disco falhou ao ser criado"
+
+#, fuzzy
+#~| msgid "Device file"
+#~ msgid "Disk image file"
+#~ msgstr "Arquivo de imagem de disco"
+
+#, fuzzy
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Não foi possível salvar as configurações do disco"
+
+#, fuzzy
+#~ msgid "Domain has crashed"
+#~ msgstr "O domínio travou"
+
+#~ msgid "Download an OS"
+#~ msgstr "Baixar um SO"
+
+#, fuzzy
+#~| msgid "dying"
+#~ msgid "Dying"
+#~ msgstr "morrendo"
+
+#, fuzzy
+#~ msgid "Failed to change firmware"
+#~ msgstr "Falha ao mudar senha"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr "Menos do que o número máximo de CPUs virtuais deve ser ativado."
+
+#~ msgid "File"
+#~ msgstr "Arquivo"
+
+#, fuzzy
+#~ msgid "Filter by name"
+#~ msgstr "Filtrar por nome ou descrição"
+
+#, fuzzy
+#~ msgid "Firmware"
+#~ msgstr "Versão"
+
+#~ msgid "Force shut down"
+#~ msgstr "Forçar Desligamento"
+
+#, fuzzy
+#~ msgid "Forward mode"
+#~ msgstr "Modo Forward"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#, fuzzy
+#~ msgid "Hide additional options"
+#~ msgstr "Ações adicionais"
+
+#~ msgid "Host device"
+#~ msgstr "Dispositivo Host"
+
+#~ msgid "Host name"
+#~ msgstr "Nome do host"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Host não deve estar vazio"
+
+#, fuzzy
+#~ msgid "Hypervisor details"
+#~ msgstr "Mais detalhes"
+
+#~ msgid "IP configuration"
+#~ msgstr "Configuração IP"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 e IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "Rede IPv4"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "Rede IPv4 não deve estar vazia"
+
+#~ msgid "IPv6 address"
+#~ msgstr "Endereço IPv6"
+
+#~ msgid "IPv6 network"
+#~ msgstr "Rede IPv6"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "Rede IPv6 não deve estar vazia"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Imediatamente Iniciar VM"
+
+#, fuzzy
+#~ msgid "Import"
+#~ msgstr "Importar VM"
+
+#~ msgid "Import VM"
+#~ msgstr "Importar VM"
+
+#, fuzzy
+#~ msgid "Import a virtual machine"
+#~ msgstr "Criar nova máquina virtual"
+
+#, fuzzy
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "Na maioria das configurações, macvtap não funciona para a comunicação da "
+#~ "rede entre host para convidado."
+
+#, fuzzy
+#~| msgid "Installation source"
+#~ msgid "Installation source"
+#~ msgstr "Fonte de Instalação"
+
+#, fuzzy
+#~ msgid "Installation source must not be empty"
+#~ msgstr "A fonte de instalação não deve estar vazia"
+
+#, fuzzy
+#~ msgid "Installation type"
+#~ msgstr "Tipo de Instalação"
+
+#, fuzzy
+#~ msgid "Interface type"
+#~ msgstr "Tipo de Interface"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Nome de arquivo inválido"
+
+#, fuzzy
+#~| msgid "Launch Remote Viewer"
+#~ msgid "Launch remote viewer"
+#~ msgstr "Iniciar o Visualizador Remoto"
+
+#, fuzzy
+#~ msgid "Loading resources"
+#~ msgstr "Carregando recursos"
+
+#, fuzzy
+#~ msgid "Managing virtual machines"
+#~ msgstr "Máquinas Virtuais"
+
+#~ msgid "Manual connection"
+#~ msgstr "Conexão manual"
+
+#, fuzzy
+#~ msgid "Mask or prefix length"
+#~ msgstr "Comprimento da máscara ou prefixo"
+
+#, fuzzy
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Comprimento da mascara ou prefixo não pode estar vazio"
+
+#, fuzzy
+#~ msgid "Maximum allocation"
+#~ msgstr "Alocação Máxima"
+
+#, fuzzy
+#~| msgid ""
+#~| "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~| "between 1 and $0"
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Número máximo de CPUs virtuais alocadas para o sistema operacional "
+#~ "virtualizado, que deve estar entre 1 e $0"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Número máximo de CPUs virtuais alocadas para o sistema operacional "
+#~ "virtualizado, que deve estar entre 1 e $0"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Tipo de modelo"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "O nome contém caracteres inválidos"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "O nome não deve estar vazio"
+
+#, fuzzy
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Boot por Rede (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Sistema de arquivos de rede"
+
+#, fuzzy
+#~ msgid "Networks"
+#~ msgstr "Redes"
+
+#~ msgid "New volume name"
+#~ msgstr "Novo nome do volume"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "Nenhuma VM está sendo executada ou definida neste host"
+
+#, fuzzy
+#~| msgid "Not available"
+#~ msgid "No connection available"
+#~ msgstr "Indisponível"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Nenhum disco definido para esta VM"
+
+#, fuzzy
+#~ msgid "No network devices"
+#~ msgstr "Nenhum dispositivos de rede"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Nenhuma interface de rede definida para esta VM"
+
+#, fuzzy
+#~ msgid "No parent"
+#~ msgstr "Parente"
+
+#, fuzzy
+#~| msgid "No disks defined for this VM"
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Nenhum disco definido para esta VM"
+
+#, fuzzy
+#~ msgid "No state"
+#~ msgstr "Estado"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "Nenhum conjunto de armazenamento é definido neste host"
+
+#, fuzzy
+#~ msgid "No storage pools available"
+#~ msgstr "Nenhum pools de armazenamento disponível"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr ""
+#~ "Nenhum volume de armazenamento definido para este pool de armazenamento"
+
+#~ msgid "Operating system"
+#~ msgstr "Sistema Operacional"
+
+#, fuzzy
+#~ msgid "Parent snapshot"
+#~ msgstr "Criar Snapshot"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Caminho no sistema de arquivos do host"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Caminho para o arquivo ISO no sistema de arquivos do host"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Caminho para o arquivo ISO no sistema de arquivos do host"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Caminho para o arquivo ISO no sistema de arquivos do host"
+
+#, fuzzy
+#~| msgid "paused"
+#~ msgid "Paused"
+#~ msgstr "pausado"
+
+#~ msgid "Persistence"
+#~ msgstr "Persistência"
+
+#, fuzzy
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Não foi possível redefinir o pool de armazenamento"
+
+#, fuzzy
+#~ msgid "Please choose a volume"
+#~ msgstr "Por favor insira o novo nome do volume"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Por favor insira o novo nome do volume"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Por favor, inicie a máquina virtual para acessar seu console."
+
+#, fuzzy
+#~ msgid "Plug"
+#~ msgstr "Plugue"
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Número preferido de soquetes para expor ao convidado."
+
+#, fuzzy
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "O comprimento do prefixo não deve estar vazio"
+
+#, fuzzy
+#~ msgid "Profile"
+#~ msgstr "Alterar o perfil"
+
+#~ msgid "Protocol"
+#~ msgstr "Protocolo"
+
+#~ msgid "Remote URL"
+#~ msgstr "URL remoto"
+
+#, fuzzy
+#~ msgid "Remote viewer details"
+#~ msgstr "Detalhes do visualizador remoto"
+
+#, fuzzy
+#~ msgid "Revert"
+#~ msgstr "Nunca"
+
+#, fuzzy
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Reverter para o snapshot $0"
+
+#, fuzzy
+#~ msgid "Root password"
+#~ msgstr "Definir uma Senha"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE TLS Porta"
+
+#~ msgid "SPICE address"
+#~ msgstr "SPICE Endereço"
+
+#~ msgid "SPICE port"
+#~ msgstr "SPICE Porta"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select console type"
+#~ msgstr "Tipo de console"
+
+#~ msgid "Send key"
+#~ msgstr "Chave de envio"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Enviar interrupção não mascarada"
+
+#~ msgid "Serial console"
+#~ msgstr "Console Serial"
+
+#, fuzzy
+#~ msgid "Set DHCP range"
+#~ msgstr "Defina intervalo DHCP"
+
+#, fuzzy
+#~ msgid "Show additional options"
+#~ msgstr "Ações adicionais"
+
+#, fuzzy
+#~ msgid "Shut off"
+#~ msgstr "desligar"
+
+#, fuzzy
+#~ msgid "Shutting down"
+#~ msgstr "Encerrar"
+
+#, fuzzy
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Disco falhou ao ser criado"
+
+#, fuzzy
+#~ msgid "Source format"
+#~ msgstr "Formato de origem"
+
+#~ msgid "Source path"
+#~ msgstr "Caminho da origem"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "O caminho da origem não deve estar vazio"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "A fonte deve começar com o protocolo http, ftp ou nfs"
+
+#, fuzzy
+#~ msgid "Source volume group"
+#~ msgstr "Grupo de volume de origem"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Iniciar libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Iniciar pool quando o host inicializa"
+
+#~ msgid "Startup"
+#~ msgstr "Comece"
+
+#, fuzzy
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "O pool de armazenamento $0 falhou ao ser ativado"
+
+#, fuzzy
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "O pool de armazenamento $0 falhou ao ser desativado"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "O pool de armazenamento não pôde ser criado"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Nome do pool de armazenamento"
+
+#, fuzzy
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Volumes de armazenamento não pode ser apagados"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "suspenso (PM)"
+
+#~ msgid "Target path"
+#~ msgstr "Caminho de Destino"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "O caminho de destino não deve estar vazio"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr "A VM está em execução e será desativada antes da exclusão."
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "O diretório no servidor que está sendo exportado"
+
+#~ msgid "The pool is empty"
+#~ msgstr "A piscina está vazia"
+
+#, fuzzy
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "O pool de armazenamento não pode ser apagado"
+
+#~ msgid "Threads per core"
+#~ msgstr "Tópicos por núcleo"
+
+#~ msgid "Unique name"
+#~ msgstr "Nome único"
+
+#, fuzzy
+#~ msgid "Unique network name"
+#~ msgstr "Nome único de rede"
+
+#, fuzzy
+#~ msgid "Unknown firmware"
+#~ msgstr "alvo desconhecido"
+
+#~ msgid "Unplug"
+#~ msgstr "Desplugar"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "Configurações de VCPU não puderam ser salvas"
+
+#, fuzzy
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "VM $0 falhou em reiniciar forçado"
+
+#, fuzzy
+#~ msgid "VM state"
+#~ msgstr "Estado"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC TLS Porta"
+
+#~ msgid "VNC address"
+#~ msgstr "VNC Endereço"
+
+#, fuzzy
+#~| msgid "Consoles"
+#~ msgid "VNC console"
+#~ msgstr "Consoles"
+
+#~ msgid "VNC port"
+#~ msgstr "VNC Porta"
+
+#, fuzzy
+#~ msgid "Virtual Machines"
+#~ msgstr "Máquinas Virtuais"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Máquinas Virtuais"
+
+#, fuzzy
+#~ msgid "Virtual machines management"
+#~ msgstr "Máquinas Virtuais"
+
+#, fuzzy
+#~ msgid "Virtual network"
+#~ msgstr "Rede Virtual"
+
+#, fuzzy
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Rede virtual falou em ser criada"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Serviço de virtualização (libvirt) não está ativo"
+
+#, fuzzy
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Nome do Grupo de Volume não pode ser vazio"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "desabilitado"
+
+#~ msgid "down"
+#~ msgstr "down"
+
+#~ msgid "enabled"
+#~ msgstr "habilitado"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#, fuzzy
+#~| msgid "to host path"
+#~ msgid "host passthrough"
+#~ msgstr "para hospedar o caminho"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "não"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "server"
+#~ msgstr "servidor"
+
+#~ msgid "up"
+#~ msgstr "up"
+
+#~ msgid "vCPU count"
+#~ msgstr "quantidade de vCPU"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "Máximo de vCPU"
+
+#~ msgid "vCPUs"
+#~ msgstr "vCPUs"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#, fuzzy
+#~| msgid ""
+#~| "virt-install package needs to be installed on the system in order to "
+#~| "create new VMs"
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "o pacote virt-install precisa estar instalado no sistema para poder criar "
+#~ "novas VMs"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "o pacote virt-install precisa estar instalado no sistema para poder criar "
+#~ "novas VMs"
+
+#, fuzzy
+#~| msgid ""
+#~| "virt-install package needs to be installed on the system in order to "
+#~| "create new VMs"
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "o pacote virt-install precisa estar instalado no sistema para poder criar "
+#~ "novas VMs"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#, fuzzy
+#~ msgid "Please set a root password"
+#~ msgstr "Nome de usuário ou senha incorretos"
+
+#~ msgid "21st"
+#~ msgstr "21º"
+
+#~ msgid "22nd"
+#~ msgstr "22º"
+
+#~ msgid "23rd"
+#~ msgstr "23º"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Após"
+
+#~ msgid "Friday"
+#~ msgstr "Sexta-feira"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Hora : Minuto"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "A hora precisa ser um número entre 0-23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Formato de data inválido."
+
+#~ msgid "Invalid number."
+#~ msgstr "Número inválido."
+
+#~ msgid "Monday"
+#~ msgstr "Segunda-feira"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Repita a cada hora"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Repita Anualmente"
+
+#~ msgid "Saturday"
+#~ msgstr "Sábado"
+
+#~ msgid "Sunday"
+#~ msgstr "Domingo"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Este dia não existe em todos os meses.<br> O temporizador só será "
+#~ "executado em meses que tenham 31 dias."
+
+#~ msgid "Thursday"
+#~ msgstr "Quinta-feira"
+
+#~ msgid "Tuesday"
+#~ msgstr "Terça-feira"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 atualizar"
+#~ msgstr[1] "$0 atualizações"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 correção de segurança"
+#~ msgstr[1] "$1 correções de segurança"
+
+#, fuzzy
+#~ msgid "Managing storage devices"
+#~ msgstr "Raw para um dispositivo"
+
+#~ msgid "Restart now"
+#~ msgstr "Reinicie agora"
+
+#~ msgid "Configure"
+#~ msgstr "Configurar"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Networking"
+
+#, fuzzy
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "Diretório ou arquivo não encontrado"
+
+#~ msgid "No updates pending"
+#~ msgstr "Nenhuma atualização pendente"
+
+#, fuzzy
+#~| msgid "ssh server is empty"
+#~ msgid "This directory is empty"
+#~ msgstr "servidor ssh está vazio"
+
+#~ msgid "undefined"
+#~ msgstr "indefinido"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Chave de Host Incorreta"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "A autenticidade do host {{#strong}}{{host}}{{/strong}} não pode ser "
+#~ "estabelecida. Tem certeza de que deseja continuar conectando?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "A chave de {{#strong}}{{host}}{{/strong}} não corresponde à chave "
+#~ "anteriormente em uso. A menos que esta máquina foi recentemente "
+#~ "substituída, é provável que alguém está tentando atacar a sua conexão com "
+#~ "esta máquina."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Chave de Host Desconhecida"
+
+#~ msgid "Address:"
+#~ msgstr "Endereço:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Pelo menos $0 discos são necessários."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "Conecte-se com qualquer aplicativo visualizador SPICE ou VNC."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Baixe o MSI de $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Console de gráficos (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Console gráfico no visualizador da área de trabalho"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Nenhum console definido para esta máquina virtual."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid "Entry"
+#~ msgstr "Entrada"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Seu navegador será desconectado, mas isso não afeta o processo de "
+#~ "atualização. Você pode se reconectar em alguns instantes para continuar "
+#~ "acompanhando o progresso."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "e reinicie a máquina automaticamente."
+
+#~ msgid "Dashboard"
+#~ msgstr "Painel"
+
+#, fuzzy
+#~ msgid "Description input text"
+#~ msgstr "Descrição"
+
+#~ msgid "FAILED"
+#~ msgstr "FALHOU"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Conexão Perdida. Tentando reconectar"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Por favor insira o novo tamanho do volume"
+
+#~ msgid "Servers"
+#~ msgstr "Servidores"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "Você está conectado diretamente a este servidor. Você não pode excluí-lo."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bits"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 bytes"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 bytes"
+
+#, fuzzy
+#~| msgid "Bond settings"
+#~ msgid "Bond settings "
+#~ msgstr "Configurações de Vínculo"
+
+#, fuzzy
+#~| msgid "IP settings"
+#~ msgid "IP settings "
+#~ msgstr "Configurações de IP"
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "O Cockpit teve um erro interno inesperado. <br/><br/>Você pode tentar "
+#~ "reiniciar o Cockpit pressionando Recarregar em seu navegador. O console "
+#~ "JavaScript contém detalhes sobre esse erro (<b>Ctrl-Shift-J</b> na "
+#~ "maioria dos navegadores)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "A VM caiu."
+
+#~ msgid "The VM is down."
+#~ msgstr "A VM está desligada."
+
+#~ msgid "The VM is going down."
+#~ msgstr "A VM está desligando."
+
+#~ msgid "The VM is idle."
+#~ msgstr "A VM está ociosa."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr ""
+#~ "A VM está em processo de falha total (Desligar ou Crash não está "
+#~ "concluída)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "A VM está pausada."
+
+#~ msgid "The VM is running."
+#~ msgstr "A VM está em execução."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "A VM é suspensa pela gerência de poder do convidado."
+
+#~ msgid "inactive"
+#~ msgstr "inativo"
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Deixe em branco para se conectar a esta máquina como o usuário conectado "
+#~ "no momento. Se você digitar um nome de usuário diferente, esse usuário "
+#~ "sempre será usado ao se conectar a esta máquina."
+
+#~ msgid "2 min"
+#~ msgstr "2 min"
+
+#~ msgid "3 min"
+#~ msgstr "3 min"
+
+#~ msgid "4 min"
+#~ msgstr "4 min"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "Status da CPU"
+
+#~ msgid "Cached"
+#~ msgstr "Cache"
+
+#~ msgid "I/O wait"
+#~ msgstr "E/S Espera"
+
+#~ msgid "Kernel"
+#~ msgstr "Kernel"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Memória"
+
+#~ msgid "Network traffic"
+#~ msgstr "Tráfego de Rede"
+
+#~ msgid "Nice"
+#~ msgstr "Bom"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Sincronizar usuários"
+
+#~ msgid "idle"
+#~ msgstr "ocioso"
+
+#~ msgid "running"
+#~ msgstr "executando"
+
+#~ msgid "shut off"
+#~ msgstr "desligar"
+
+#~ msgid "shutdown"
+#~ msgstr "desligar"
+
+#, fuzzy
+#~ msgid "Add a new host"
+#~ msgstr "Adicionar um novo host"
+
+#~ msgid "Available"
+#~ msgstr "Disponível"
+
+#, fuzzy
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Digite o endereço IP ou nome de host"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Esta versão do cockpit-ws não suporta conectar a um host com um usuário "
+#~ "ou porta alternativa"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Para tentar uma porta diferente, você precisará atualizar o cockpit-ws "
+#~ "para uma versão mais recente."
+
+#~ msgid "$0 template"
+#~ msgstr "Modelo $0"
+
+#~ msgid "Instantiate"
+#~ msgstr "Instanciar"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 compartilhamentos"
+
+#~ msgid "All In One"
+#~ msgstr "Tudo em um"
+
+#~ msgid "Always"
+#~ msgstr "Sempre"
+
+#~ msgid "Author"
+#~ msgstr "Autor"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Dados corrompidos passaram por passwd1 mecanismo"
+
+#~ msgid "CPU priority"
+#~ msgstr "Prioridade de CPU"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Pode&rsquo;t conectar ao Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Alterar limites de recursos"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Alterar limites de recursos"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr ""
+#~ "O Cockpit não foi capaz de efetuar log in em {{#strong}}{{host}}{{/"
+#~ "strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "O Cockpit não pôde efetuar login em {{#strong}}{{host}}{{/strong}}. Você "
+#~ "pode alterar as credenciais de autenticação abaixo. {{#can_sync}}Você "
+#~ "pode preferir {{#sync_link}} sincronizar contas e senhas{{/sync_link}}.{{/"
+#~ "can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Uso de memória combinada"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Uso combinado de núcleo de CPU de $0"
+#~ msgstr[1] "Uso combinado de núcleos de CPU de $0 CPU"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "O comando não pode estar vazio"
+
+#~ msgid "Command:"
+#~ msgstr "Comando:"
+
+#~ msgid "Commit"
+#~ msgstr "Commit"
+
+#~ msgid "Commit image"
+#~ msgstr "Imagem Commit"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Conectando ao Docker"
+
+#~ msgid "Container name"
+#~ msgstr "Nome do Container"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Container está atualmente marcado como parado, mas parada normal falhou."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Container está em execução."
+
+#~ msgid "Container:"
+#~ msgstr "Contêiner:"
+
+#~ msgid "Containers"
+#~ msgstr "Containers"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Containers"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Não foi possível alterar os grupos de usuários"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Não foi possível alterar a senha do usuário"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Não pode conectar a máquina"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Não foi possível criar novos usuários"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Não foi possível listar os usuários locais"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Não foi possível listar os usuários"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Não foi possível carregar os dados do usuário"
+
+#~ msgid "Created:"
+#~ msgstr "Criado:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Contêineres Docker"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker não está instalado ou ativado no sistema"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Apelido duplicado"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Porta duplicada"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Introduzir uma senha diferente aqui significa que você precisará digitá-"
+#~ "la cada vez que você se reconecta a esta máquina"
+
+#~ msgid "Environment"
+#~ msgstr "Ambiente"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Erro ao carregar usuários: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Mensagem de erro do Docker:"
+
+#~ msgid "Everything"
+#~ msgstr "Tudo"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Encerrado $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Expor portas de contêiner"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Falha ao iniciar Docker: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Falha ao parar Docker escopo: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Obter nova imagem"
+
+#~ msgid "IP address:"
+#~ msgstr "Endereço IP:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "Comprimento do prefixo IP:"
+
+#~ msgid "Id"
+#~ msgstr "Id"
+
+#~ msgid "Id:"
+#~ msgstr "Id:"
+
+#~ msgid "Image"
+#~ msgstr "Imagem"
+
+#~ msgid "Image $0"
+#~ msgstr "Imagem $0"
+
+#~ msgid "Image search"
+#~ msgstr "Pesquisa de Imagem"
+
+#~ msgid "Image:"
+#~ msgstr "Imagem:"
+
+#~ msgid "Images"
+#~ msgstr "Imagens"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Imagens"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Imagens e contêineres em execução"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Para sincronizar os usuários, você precisa fazer login em {{#strong}}"
+#~ "{{host}}{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Porta inválida"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Tíquete Kerberos"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Deixe em branco para se conectar a esta máquina como a conexão atualmente "
+#~ "efetuada como user{{#default_user}} ({{default_user}}){{/default_user}}. "
+#~ "Se você inserir um nome de usuário diferente, esse usuário sempre será "
+#~ "usado para conectar-se a esta máquina."
+
+#~ msgid "Link to another container"
+#~ msgstr "Link para outro contêiner"
+
+#~ msgid "Links:"
+#~ msgstr "Links:"
+
+#~ msgid "Login Password"
+#~ msgstr "Senha de Login"
+
+#~ msgid "MAC address:"
+#~ msgstr "Endereço MAC:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Limite de Memória"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Montar contêiners de volumes"
+
+#~ msgid "No container specified"
+#~ msgstr "Nenhum container especificado"
+
+#~ msgid "No containers"
+#~ msgstr "Nenhum contêiner"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Nenhum contêiner que corresponda ao filtro atual"
+
+#~ msgid "No images"
+#~ msgstr "Nenhuma imagem"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Sem imagens que correspondam ao filtro atual"
+
+#~ msgid "No results for $0"
+#~ msgstr "Nenhum resuldado para $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Sem imagens que correspondam ao filtro atual"
+
+#~ msgid "No running containers"
+#~ msgstr "Sem contêiners em execução"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Sem contêineres em execução que correspondem ao filtro atual"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Não autorizado para acessar Docker neste sistema"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "Em falha, repetir $0 vez"
+#~ msgstr[1] "Em falha, repetir $0 vezes"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Por favor, confirme a exclusão forçada de $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Por favor, tente outro termo"
+
+#~ msgid "Ports:"
+#~ msgstr "Portas:"
+
+#~ msgid "Problems"
+#~ msgstr "Problemas"
+
+#~ msgid "ReadOnly"
+#~ msgstr "SomenteLeitura"
+
+#~ msgid "Repository"
+#~ msgstr "Repositório"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Reinicie a Política:"
+
+#~ msgid "Retries:"
+#~ msgstr "Tentativas:"
+
+#, fuzzy
+#~| msgid "Reuse my password for privileged tasks"
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Reutilize minha senha para tarefas privilegiadas"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Selecione os usuários que você gostaria de ser sincronizado com "
+#~ "{{#strong}}{{host}}{{/strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Definir variáveis de ambiente de contêiner"
+
+#~ msgid "Severity:"
+#~ msgstr "Gravidade:"
+
+#~ msgid "Show all containers"
+#~ msgstr "Exibir todos os contêineres"
+
+#~ msgid "Start Docker"
+#~ msgstr "Iniciar Docker"
+
+#~ msgid "State:"
+#~ msgstr "Estado:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Sincronizar"
+
+#~ msgid "Tag"
+#~ msgstr "Marca"
+
+#~ msgid "Tags"
+#~ msgstr "Tags"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "Os contêineres a seguir dependem dessa imagem e ficarão inutilizáveis."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Esta imagem não existe."
+
+#~ msgid "Type a password"
+#~ msgstr "Digite uma senha"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Tipo para filtrar…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "Parou-se"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Mecanismo de instalação sem suporte"
+
+#~ msgid "Up since $0"
+#~ msgstr "Ativo desde $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Usado pelos Contêineres"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Usando credenciais disponíveis"
+
+#~ msgid "Volumes"
+#~ msgstr "Volumes"
+
+#~ msgid "Volumes:"
+#~ msgstr "Volumes:"
+
+#~ msgid "With terminal"
+#~ msgstr "Com o terminal"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Você está conectado a {{#strong}}{{host}}{{/strong}}, no entanto, para "
+#~ "sincronizar os usuários, é necessário um usuário com privilégios de "
+#~ "superusuário."
+
+#~ msgid "default"
+#~ msgstr "padrão"
+
+#~ msgid "key"
+#~ msgstr "chave"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "pesquisar por nome, tamanho do nome ou descrição"
+
+#~ msgid "shares"
+#~ msgstr "compartilhamentos"
+
+#~ msgid "to host port"
+#~ msgstr "para porta do host"
+
+#~ msgid "value"
+#~ msgstr "valor"
+
+#~ msgid "Master"
+#~ msgstr "Mestre"
+
+#, fuzzy
+#~| msgid "Password"
+#~ msgid "Password for $0"
+#~ msgstr "Senha"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "O usuário <b>$0</b> não tem permissão para gerenciar servidores"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Contas"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "O usuário <b>$0</b> não tem permissão para modificar contas"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Incapaz de deletar a conta root"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Incapaz de renomear a conta root"
+
+#~ msgid "Account Settings"
+#~ msgstr "Configurações de Conta"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Adicionar Máquina ao Painel"
+
+#~ msgid "Machines"
+#~ msgstr "Máquinas"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "O login proveu privilégios de administrador"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Senha não utilizável para tarefas privilegiadas ou para se conectar a "
+#~ "outras máquinas"
+
+#~ msgid "Privileged"
+#~ msgstr "Privilegiado"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Reutilizar minha senha para tarefas privilegiadas e conectar-se a outras "
+#~ "máquinas"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr ""
+#~ "O usuário <b>$0</b> não tem permissão para gerenciar o armazenamento"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr ""
+#~ "O usuário <b>$0</b> não tem permissão para modificar as configurações de "
+#~ "rede"
+
+#, fuzzy
+#~| msgid "Required by"
+#~ msgid "Required by $0"
+#~ msgstr "Solicitado por"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Último Trigger"
+
+#~ msgid "Next Run"
+#~ msgstr "Próxima Execução"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "O usuário <b>$0</b> não tem permissões para criar temporizadores"
+
+#~ msgid "Connecting"
+#~ msgstr "Conectando"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Sobre o Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr "(compartilhado com o SO)"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Inserir Armazenamento Adicional"
+
+#~ msgid "Add Storage"
+#~ msgstr "Adicionar Armazenamento"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Armazenamento Adicional"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Todos os dados em discos selecionados serão apagados e os discos serão "
+#~ "adicionados ao pool de armazenamento."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Configurar armazenamento..."
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Não foi possível adicionar todos os discos"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Apagar contêineres e redefinir pool de armazenamento"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr ""
+#~ "As informações sobre o pool de armazenamento do Docker não estão "
+#~ "disponíveis."
+
+#~ msgid "Local Disks"
+#~ msgstr "Discos Locais"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "Nenhum armazenamento local adicional encontrado."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Reformatar e adicionar discos"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "A redefinição do pool de armazenamento apagará todos os recipientes e "
+#~ "discos de liberação no pool."
+
+#~ msgid "Security"
+#~ msgstr "Segurança"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr ""
+#~ "O pool de armazenamento do Docker não pode ser gerenciado neste sistema."
+
+#, fuzzy
+#~| msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "O scanner de $time ($type) não detectou vulnerabilidades."
+#~ msgstr[1] "O scanner de $time ($type) não detectou vulnerabilidades."
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "O scanner de $time ($type) não detectou vulnerabilidades."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "O scanner de $time ($type) não funcionou."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Serviço de virtualização disponível"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr ""
+#~ "Você não tem permissão para gerenciar o Docker e pool de armazenamento."
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} -> $cport"
diff --git a/po/ru.po b/po/ru.po
new file mode 100644
index 0000000..df7e755
--- /dev/null
+++ b/po/ru.po
@@ -0,0 +1,13636 @@
+# Ludek Janda <ljanda@redhat.com>, 2018. #zanata
+# cockpit <cockpituous@gmail.com>, 2018. #zanata
+# Dmitry Astankov <mornie@basealt.ru>, 2019. #zanata
+# cockpit <cockpituous@gmail.com>, 2019. #zanata
+# Igor Gorbounov <igor.gorbounov@gmail.com>, 2020.
+# Anonymous <noreply@weblate.org>, 2020.
+# Martin Pitt <mpitt@redhat.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-07-31 12:26+0000\n"
+"Last-Translator: yangyangdaji <1504305527@qq.com>\n"
+"Language-Team: Russian <https://translate.fedoraproject.org/projects/cockpit/"
+"main/ru/>\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 4.18.2\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "# пользователей"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 процессор"
+msgstr[1] "$0 процессора"
+msgstr[2] "$0 процессоров"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 ГиБ"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 активная зона"
+msgstr[1] "$0 активных зоны"
+msgstr[2] "$0 активных зон"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "Доступно $0"
+
+#: pkg/storaged/block/resize.jsx:266
+#, fuzzy
+#| msgid "$0 filesystems can not be made larger."
+msgid "$0 can not be made larger"
+msgstr "Файловые системы $0 не могут быть увеличены."
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "$0 filesystems can not be made smaller."
+msgid "$0 can not be made smaller"
+msgstr "Файловые системы $0 не могут быть уменьшены."
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized"
+msgstr "Размер файловых систем $0 невозможно изменить здесь."
+
+#: pkg/storaged/block/resize.jsx:255
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "$0 can not be resized here"
+msgstr "Размер файловых систем $0 невозможно изменить здесь."
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "размер блока: $0"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 критическое попадание"
+msgstr[1] "$0 попадания, включая критическое"
+msgstr[2] "$0 попаданий, включая критическое"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 данных + $1 служебной информации использовано из $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 день"
+msgstr[1] "$0 дня"
+msgstr[2] "$0 дней"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 диск отсутствует"
+msgstr[1] "$0 диска отсутствуют"
+msgstr[2] "$0 дисков отсутствуют"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 диск отсутствует"
+msgstr[1] "$0 диска отсутствуют"
+msgstr[2] "$0 дисков отсутствуют"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "дисков: $0"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "Документация $0"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "Ошибка $0"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "Процесс $0 завершил работу с кодом $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "Сбой процесса $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 неудачная попытка входа в систему"
+msgstr[1] "$0 неудачные попытки входа в систему"
+msgstr[2] "$0 неудачных попыток входа в систему"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "Файловая система $0"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "Свободно $0"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 час"
+msgstr[1] "$0 часа"
+msgstr[2] "$0 часов"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 важное попадание"
+msgstr[1] "$0 попадания, включая важное"
+msgstr[2] "$0 попаданий, включая важное"
+
+#: pkg/users/account-create-dialog.js:378
+#, fuzzy
+#| msgid "$0 disk is missing"
+#| msgid_plural "$0 disks are missing"
+msgid "$0 is an existing file"
+msgstr "$0 диск отсутствует"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 используется"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "Компонент $0 недоступен в репозиториях."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "Изменен $0 ключ"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "Процесс $0 прерван с сигналом $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 попадание с низким уровнем серьезности"
+msgstr[1] "$0 попадания с низким уровнем серьезности"
+msgstr[2] "$0 попаданий с низким уровнем серьезности"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 минута"
+msgstr[1] "$0 минуты"
+msgstr[2] "$0 минут"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 умеренное попадание"
+msgstr[1] "$0 попадания, включая умеренные"
+msgstr[2] "$0 попаданий, включая умеренные"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 месяц"
+msgstr[1] "$0 месяца"
+msgstr[2] "$0 месяцев"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "На $0 больше…"
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 пакет"
+msgstr[1] "$0 пакета"
+msgstr[2] "$0 пакетов"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 пакет требует перезагрузки системы"
+msgstr[1] "$0 пакета требуют перезагрузки системы"
+msgstr[2] "$0 пакетов требуют перезагрузки системы"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 пакет"
+msgstr[1] "$0 пакета"
+msgstr[2] "$0 пакетов"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "раздел"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "Доступно $0 обновление безопасности"
+msgstr[1] "Доступно $0 обновления безопасности"
+msgstr[2] "Доступно $0 обновлений безопасности"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "Сбой $0 службы"
+msgstr[1] "Сбой $0 служб"
+msgstr[2] "Сбой $0 служб"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 служба требует перезапуска"
+msgstr[1] "$0 службы требуют перезапуска"
+msgstr[2] "$0 служб требуют перезапуска"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "Остался $0 слот"
+msgstr[1] "Осталось $0 слота"
+msgstr[2] "Осталось $0 слотов"
+
+#: pkg/metrics/metrics.jsx:1333
+#, fuzzy
+#| msgid "CPU spike"
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "Всплеск процессора"
+msgstr[1] "Всплеск процессора"
+msgstr[2] "Всплеск процессора"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+#, fuzzy
+#| msgid "Not synchronized"
+msgid "$0 synchronized"
+msgstr "Не синхронизировано"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "Всего $0"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "Доступно $0 обновление"
+msgstr[1] "Доступно $0 обновления"
+msgstr[2] "Доступно $0 обновлений"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 использовано из $1 ($2 сэкономлено)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 неделя"
+msgstr[1] "$0 недели"
+msgstr[2] "$0 недель"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "Будет выполнена установка $0."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 год"
+msgstr[1] "$0 года"
+msgstr[2] "$0 лет"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "Зона $0"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: сбой в $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (от $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Создание раздела $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Локальная точка подключения"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Не подключено"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(рекомендовано)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", в том числе $1 исправление безопасности"
+msgstr[1] ", в том числе $1 исправления безопасности"
+msgstr[2] ", в том числе $1 исправлений безопасности"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 МиБ"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 день"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 час"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 мин"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 минута"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 неделя"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11-го числа"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 КиБ"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14-го числа"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 мин"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15-го числа"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 КиБ"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1-го числа"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 МиБ"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 минут"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31-го числа"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 КиБ"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3-го числа"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 КиБ"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 минут"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4-го числа"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 мин"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 минут"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 КиБ"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5-го числа"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 часов"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 минут"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 КиБ"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7-го числа"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 КиБ"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8-го числа"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9-го числа"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Совместимая версия Cockpit не установлена на $0."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "Файловая система с таким именем уже существует в этом пуле."
+
+#: pkg/users/group-create-dialog.js:71
+#, fuzzy
+#| msgid "A pool with this name exists already."
+msgid "A group with this name already exists"
+msgstr "Пул с таким названием уже существует."
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Для обеспечения безопасности, надёжности и производительности необходим "
+"современный браузер."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Сетевое объединение сливает несколько сетевых интерфейсов в один логический "
+"интерфейс с увеличенной пропускной способностью или с избыточностью."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Будет создан новый ключ SSH в $0 для $1 на $2 и добавлен в файл $3 $4 на $5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Чтобы обновления вступили в силу, пакету требуется перезагрузка системы:"
+msgstr[1] ""
+"Чтобы обновления вступили в силу, некоторым пакетам требуется перезагрузка "
+"системы:"
+msgstr[2] ""
+"Чтобы обновления вступили в силу, некоторым пакетам требуется перезагрузка "
+"системы:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Пул с таким названием уже существует."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] "Чтобы изменения вступили в силу, необходимо перезапустить службу:"
+msgstr[1] ""
+"Чтобы изменения вступили в силу, необходимо перезапустить некоторые службы:"
+msgstr[2] ""
+"Чтобы изменения вступили в силу, необходимо перезапустить некоторые службы:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "Размер физических томов невозможно изменить здесь."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "Мониторинг ARP"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "О веб-консоли"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Отсутствует"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Принять ключ и войти в систему"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+#, fuzzy
+#| msgid "Set password"
+msgid "Acceptable password"
+msgstr "Задать пароль"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Срок действия учётной записи"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Учётная запись недоступна или не может быть изменена."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Учётные записи"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Действие"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Действия"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Включить"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Включение $target"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Активно"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Активное резервирование"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Активные страницы"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Активно с "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Активное состояние"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Адаптивная балансировка нагрузки"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Адаптивная балансировка нагрузки передачи"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Добавить"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Добавить $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add DNS server"
+msgstr "Сервер криптографических ключей Tang"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:458
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add Tang keyserver"
+msgstr "Сервер криптографических ключей Tang"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Добавить VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+#, fuzzy
+#| msgid "Add VLAN"
+msgid "Add VPN"
+msgstr "Добавить VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+#, fuzzy
+#| msgid "Add item"
+msgid "Add a bitmap"
+msgstr "Добавить элемент"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Добавить новую зону"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+#, fuzzy
+#| msgid "MAC address"
+msgid "Add address"
+msgstr "MAC-адрес"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Добавить блочные устройства"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Добавить Bond"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Добавить Bridge"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Добавить диск"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Добавление дисков"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Добавить портал iSCSI"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Добавить ключ"
+
+#: pkg/storaged/stratis/pool.jsx:551
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add keyserver"
+msgstr "Сервер криптографических ключей Tang"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Добавить участника"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Добавить новый узел"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Добавить новую зону"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Add passphrase"
+msgstr "Старая парольная фраза"
+
+#: pkg/networkmanager/wireguard.jsx:290
+#, fuzzy
+#| msgid "Add member"
+msgid "Add peer"
+msgstr "Добавить участника"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "Physical volume"
+msgid "Add physical volume"
+msgstr "Физический том"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Добавить порты"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Добавить порты в зону $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Добавить открытый ключ"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+#, fuzzy
+#| msgid "Add item"
+msgid "Add route"
+msgstr "Добавить элемент"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+#, fuzzy
+#| msgid "DNS search domains"
+msgid "Add search domain"
+msgstr "Домены поиска DNS"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Добавить службы"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Добавить сервисы в зону $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Добавить службы в зону $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Добавить Team"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Добавить зону"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+#, fuzzy
+#| msgid "Encryption options"
+msgid "Adding \"$0\" to encryption options"
+msgstr "Параметры шифрования"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr ""
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Добавление $0 приведёт к разрыву соединения с сервером и недоступности "
+"интерфейса администратора."
+
+#: pkg/storaged/stratis/pool.jsx:468
+#, fuzzy
+#| msgid ""
+#| "Saving a new passphrase requires unlocking the disk. Please provide a "
+#| "current disk passphrase."
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Для сохранения новой парольной фразы требуется снятие блокировки диска. "
+"Укажите действующую парольную фразу диска."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Добавление пользовательских портов приведёт к перезагрузке firewalld. При "
+"этом будет утеряна любая конфигурация среды выполнения!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Добавление ключа"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Добавление физического тома в $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Дополнительный DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Дополнительные домены поиска DNS $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Дополнительные действия"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Дополнительный адрес $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Дополнительные пакеты:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Дополнительные порты"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Адрес"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Адрес $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Адрес не может быть пустым"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Недопустимое значение URL-адреса"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Адреса"
+
+#: pkg/networkmanager/wireguard.jsx:52
+#, fuzzy
+#| msgid "Address cannot be empty"
+msgid "Addresses are not formatted correctly"
+msgstr "Адрес не может быть пустым"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Администрирование с помощью веб-панели управления Cockpit"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Административный доступ"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr ""
+"Требуется административный доступ для создания отчетов и доступа к ним."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Требуется административный доступ"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Расширенный TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "After="
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"После выхода из домена только пользователи с локальными учётными данными "
+"смогут выполнить вход в эту систему. Это также может повлиять и на другие "
+"службы, поскольку параметры разрешения DNS и список доверенных центров "
+"сертификации могут измениться."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "После загрузки системы"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Предупреждение"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Оповещения и выше"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Псевдоним"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Все"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Все обновления"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Все в одном"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Разрешить выполнение (снять маску)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+#, fuzzy
+#| msgid "Allowed addresses"
+msgid "Allowed IPs"
+msgstr "Разрешённые адреса"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Разрешённые адреса"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Документация к ролям Ansible"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Любая текстовая строка в сообщениях журнала может быть отфильтрована. Строка "
+"также может быть представлена в виде регулярного выражения. Также "
+"поддерживает фильтрацию по полям журнала сообщений. Это значения, "
+"разделенные пробелами, в форме ПОЛЕ=ЗНАЧЕНИЕ, где значение может быть "
+"списком возможных значений, разделенных запятыми."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Стиль отображения"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr ""
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Приложения"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Список приложений"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Применить и перезагрузить"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Применить интерактивные исправления ядра"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Применить это решение"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Применение новой политики… Это может занять несколько минут."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Применение решения…"
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Применение обновлений"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Не удалось применить обновления"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr ""
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Приложения"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Оценка"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Тег актива"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+#, fuzzy
+#| msgid "boot"
+msgid "At boot"
+msgstr "загрузка"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Необходим хотя бы $0 диск."
+msgstr[1] "Необходимо хотя бы $0 диска."
+msgstr[2] "Необходимо хотя бы $0 дисков."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "Необходимо хотя бы одно блочное устройство."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Необходим хотя бы один диск."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "В минуту"
+
+#: pkg/systemd/timer-dialog.jsx:260
+#, fuzzy
+#| msgid "Seconds"
+msgid "At second"
+msgstr "сек"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "В определённое время"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Атрибуты"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Журнал аудита"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Проверка подлинности"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Проверка подлинности"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Проверка подлинности"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Ошибка проверки подлинности"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Не удалось выполнить проверку подлинности: сервер закрыл соединение"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Для выполнения привилегированных задач с помощью веб-консоли Cockpit "
+"необходима проверка подлинности"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Необходима проверка подлинности"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Авторизовать SSH-ключ"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Авторизованные открытые SSH-ключи"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Автоматически"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Автоматически (только DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Автоматический вход"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Автоматическое обновление"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Запускается автоматически"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Автоматически с использованием серверов NTP"
+
+#: pkg/lib/serverTime.js:570
+#, fuzzy
+#| msgid "Automatically using specific NTP servers"
+msgid "Automatically using additional NTP servers"
+msgstr "Автоматически с использованием определённых серверов NTP"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Автоматически с использованием определённых серверов NTP"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Сценарий автоматизации"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Доступные цели на $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Доступные обновления"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "partition"
+msgid "BIOS boot partition"
+msgstr "раздел"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "Дата BIOS"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "Версия BIOS"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Вернуться к учётным записям"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Плохо"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Плохой параметр"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Подсистема балансировки"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Before="
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "BindsTo="
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Чёрный"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Ультракомпактный сервер"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Корзина"
+
+#: pkg/storaged/block/other.jsx:41
+#, fuzzy
+#| msgid "Block devices"
+msgid "Block device"
+msgstr "Блочные устройства"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Устройство блочного ввода-вывода для файловых систем"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Блочные устройства"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Заблокировано"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Объединение"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Загрузка"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+#, fuzzy
+#| msgid "The filesystem is not mounted."
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "Файловая система не смонтирована."
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "BoundBy="
+
+# translation auto-copied from project Anaconda, version master, document
+# anaconda, author Igor Gorbounov
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Мост"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Порт моста"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Параметры порта моста"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Трансляция"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Неверная конфигурация"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Доступны исправления ошибок"
+
+# ctx::sourcefile::/rhn/errata/manage/Create.do
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Ошибки"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Корпус расширения шины"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Если изменить пароль ключа SSH $0 на пароль для входа $1 на $2, ключ будет "
+"автоматически доступен, и вы в будущем сможете входить в $3 без пароля."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " Обойти проверку браузера "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "ЦП"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Безопасность процессора"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "Переключатели безопасности ЦП"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Использование ЦП"
+
+#: pkg/metrics/metrics.jsx:1939
+#, fuzzy
+#| msgid "CPU usage"
+msgid "CPU usage/load"
+msgstr "Использование ЦП"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Кэш"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "Может быть именем узла, IP-адресом, псевдонимом или ssh:// URI"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr "Не удается найти журналы на основе текущего сочетания фильтров."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Отмена"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Отменить выключение"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Отменить перезагрузку"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Не может быть активирован"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Не удаётся подключиться к неизвестному компьютеру"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Не удаётся передать учётные данные для входа"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+"Не удается присоединиться к домену, потому что на этой системе нет realmd"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Невозможно запланировать событие в прошлом"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Ёмкость"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Несущая частота"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Изменить"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+#, fuzzy
+#| msgid "Change crypto policy"
+msgid "Change cryptographic policy"
+msgstr "Изменить правила шифрования"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Изменить имя узла"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Изменить имя инициатора iSCSI"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Изменить имя инициатора iSCSI"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change label"
+msgstr "Изменить парольную фразу"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Изменить парольную фразу"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Изменить пароль"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Изменить профиль производительности"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Изменить профиль"
+
+#: pkg/users/shell-dialog.js:70
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change shell"
+msgstr "Изменить парольную фразу"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Изменить системное время"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Изменить пароль для $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Изменить параметры"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Изменение ключей часто происходит в результате переустановки операционной "
+"системы. Однако неожиданное изменение может указывать на попытку третьей "
+"стороны перехватить ваше соединение."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Изменение параметров приведёт к разрыву соединения с сервером и "
+"недоступности интерфейса администратора."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Проверить наличие обновлений"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Проверка $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Проверка IP-адреса"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "Проверка RAID-устройства $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Проверка и восстановление RAID-устройства $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+#, fuzzy
+#| msgid "Checking for package updates..."
+msgid "Checking for $0 package"
+msgstr "Проверка наличия обновлений пакетов…"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr ""
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Проверка наличия новых приложений"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Проверка наличия обновлений пакетов…"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Проверка установленного программного обеспечения"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Проверка связанных процессов"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Проверка состояния программного обеспечения"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Выберите язык, который будет использоваться в приложении"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Размер блока"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Класс"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Очистка данных $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Очистить «Сбой запуска»"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Сбросить все фильтры"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Сбросить поиск"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Устройство с открытым текстом"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Клиентское программное обеспечение"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Закрыть"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Закрыть выделенные страницы"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Проверка подлинности Cockpit настроена неправильно."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr ""
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Не удалось установить связь между Cockpit и заданным узлом."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "У Cockpit произошла неожиданная внутренняя ошибка."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit представляет собой диспетчер серверов, упрощающий администрирование "
+"серверов Linux через веб-браузер. Переключение между терминалом и веб-"
+"инструментом не представляет сложности. Служба, запущенная с помощью "
+"Cockpit, может быть остановлена через терминал. Аналогично, если в терминале "
+"возникает ошибка, её можно увидеть в интерфейсе журнала Cockpit."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit — это интерактивный интерфейс администратора серверов Linux."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit не совместим с программным обеспечением в системе."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit не установлен"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit не установлен в системе."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit идеально подходит для новых системных администраторов, позволяя им "
+"легко выполнять простые задачи, такие как администрирование хранилищ, "
+"проверка журналов и запуск и остановка служб. С помощью Cockpit вы можете "
+"контролировать и администрировать несколько серверов одновременно. Просто "
+"добавьте их одним щелчком мыши, и ваш компьютер позаботится о своих "
+"приятелях."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit может отображаться некорректно в вашем браузере"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+#, fuzzy
+#| msgid "Kernel crash dump"
+msgid "Collect kernel crash dumps"
+msgstr "Аварийный дамп ядра"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Собрать метрики"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Цвет"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "Допустимы порты, диапазоны и службы с запятыми в качестве разделителей"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Команда"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Команда не найдена"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Комментарий"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Сбой связи с демоном tuned"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Компактный PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Совместимость со всеми системами и устройствами (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr ""
+"Совместимость с современными системами и жёсткими дисками объёмом более 2 ТБ "
+"(GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Сжимать аварийные дампы для экономии места"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Сжатие"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Не выполнено условие $0=$1"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Условие не выполнено"
+
+# translation auto-copied from project libreport, version master, document
+# libreport
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Настройка"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Настройка"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Настройка IP-адреса"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Настройка kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Настройка параметров системы"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Подтверждение"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Подтвердите удаление $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Подтвердите ключевой пароль"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Подтвердите новый ключевой пароль"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Подтвердите новый пароль"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Подтвердите пароль"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Подтвердите удаление $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Подтвердите удаление с помощью альтернативной кодовой фразы"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Подтвердите остановку $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "ConflictedBy="
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Conflicts="
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Подключаться автоматически"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Подключиться к"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Подключиться к:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Подключение к демону SETroubleshoot…"
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Не удалось подключиться к dbus: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Подключение к компьютеру"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Ошибка подключения"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Ошибка подключения"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Превышено время ожидания подключения."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "Подключение будет прервано"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "ConsistsOf="
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Связь с доменом установлена"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Содержит:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Дальше"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Продолжить сессию"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Управление"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Управление"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Контроллер"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Компьютер-трансформер"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Скопировано"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Копировать"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Копировать в буфер обмена"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Ядро $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "Не удалось связаться с $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Расположение аварийного дампа"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Сообщение о сбое"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Сбой системы"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Создать"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "Создать группу томов LVM2"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "Создание RAID-устройства"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Создание RAID-устройства"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Создать пул Stratis"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Создать новый SSH-ключ и авторизовать его"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Создать снимок файловой системы $0"
+
+#: pkg/users/account-create-dialog.js:557
+#, fuzzy
+#| msgid "Create account with weak password"
+msgid "Create account with non-unique UID"
+msgstr "Создать учётную запись со слабым паролем"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Создать учётную запись со слабым паролем"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+#, fuzzy
+#| msgid "Create new account"
+msgid "Create and mount"
+msgstr "Создать учётную запись"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create new account"
+msgid "Create and start"
+msgstr "Создать учётную запись"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Создать файловую систему"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Создать"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Создание логического тома"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Создать учётную запись"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Создать новую файловую систему"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+#, fuzzy
+#| msgid "Create volume group"
+msgid "Create new group"
+msgstr "Создать группу томов"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Создать новый логический том"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Создайте новый файл задачи с этим содержимым."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+#, fuzzy
+#| msgid "Create new logical volume"
+msgid "Create new thinly provisioned logical volume"
+msgstr "Создать новый логический том"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+#, fuzzy
+#| msgid "read only"
+msgid "Create only"
+msgstr "только для чтения"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Создать раздел"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Создание раздела на $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Создать таблицу разделов"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Создание моментального снимка"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+#, fuzzy
+#| msgid "Create snapshot"
+msgid "Create snapshot and mount"
+msgstr "Создание моментального снимка"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+#, fuzzy
+#| msgid "Create snapshot"
+msgid "Create snapshot only"
+msgstr "Создание моментального снимка"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create storage volume"
+msgid "Create storage device"
+msgstr "Создание тома хранилища"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Создать том"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Создать тонкий том"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Создать таймер"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Создать группу томов"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Дата создания"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Создание группы томов LVM2 $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "Создание RAID-устройства $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "Создание устройства VDO"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Создание файловой системы на $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Создание логического тома $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Создание раздела $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Создание моментального снимка $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Создание этой $0 приведёт к разрыву соединения с сервером и недоступности "
+"интерфейса администратора."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Критические оповещения и выше"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+#, fuzzy
+#| msgid ""
+#| "Crypto Policies is a system component that configures the core "
+#| "cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and "
+#| "Kerberos protocols."
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Crypto Policies — это системный компонент для настройки основных "
+"криптографических подсистем, включающих протоколы TLS, IPSec, SSH, DNSSec и "
+"Kerberos."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+#, fuzzy
+#| msgid "Crypto policy"
+msgid "Cryptographic policy"
+msgstr "Правила шифрования"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+#, fuzzy
+#| msgid "Crypto policy is inconsistent"
+msgid "Cryptographic policy is inconsistent"
+msgstr "Правило шифрования несовместимо"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+#, fuzzy
+#| msgid "Ctrl-Shift-J"
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-J"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Текущая сессия"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Максимальное текущее использование ЦП"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Используется в данный момент"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Currently in use"
+msgid "Currently not supported"
+msgstr "Используется в данный момент"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "другой"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+#, fuzzy
+#| msgid "Custom crypto policy"
+msgid "Custom cryptographic policy"
+msgstr "Дополнительное правило шифрования"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Пользовательские параметры подключения"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Настраиваемые порты"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom ports"
+msgid "Custom type"
+msgstr "Настраиваемые порты"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Настраиваемые зоны"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "Домены поиска DNS"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "Домены поиска DNS $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Ежедневно"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Оповещение об опасности:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Тёмный"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Данные"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Использованные данные"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Дата должна быть представлена в формате YYYY-MM-DD hh:mm:ss. Также можно "
+"использовать строки «yesterday» (вчера), «today» (сегодня), "
+"«tomorrow» (завтра). Строка «now» (сейчас) означает текущее время. Наконец, "
+"можно указывать относительное время с префиксом «-» или «+»"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Отключить"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Переименование логического тома"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Отключение"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Отключение $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Отладочные сообщения и выше"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Уменьшить на один"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+#, fuzzy
+#| msgid "RAID 4 (dedicated parity)"
+msgid "Dedicated parity (RAID 4)"
+msgstr "RAID 4 (выделенная чётность)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Дедупликация"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "По умолчанию"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Задержка"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Величина задержки должна быть задана в виде числа"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Удалить"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Удалить $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Удалить учётную запись"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Удалить файлы"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Удалить группу"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Удалить"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Окончательно удалить отчёт?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Удаление $0 приведёт к разрыву соединения с сервером и недоступности "
+"интерфейса администратора."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Удаление $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Удаление группы томов LVM2 $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr ""
+"Удаление пула Stratis приведёт к уничтожению всех содержащихся в нём данных."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr ""
+"Удаление файловой системы приведёт к уничтожению всех содержащихся в ней "
+"данных."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "Удаление логического тома приведёт к удалению всех данных в нём."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "Удаление раздела приведёт к удалению всех данных в нём."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting erases all data on a RAID device."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Удаление приведёт к уничтожению всех данных на RAID-устройстве."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "Удаление приведёт к уничтожению всех данных на устройстве VDO."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr ""
+"Удаление приведёт к уничтожению всех данных, содержащихся в группе томов."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting erases all data on a volume group."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr ""
+"Удаление приведёт к уничтожению всех данных, содержащихся в группе томов."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "При удалении будут уничтожены следующие файлы:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Описание"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Настольный компьютер"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Съёмный компьютер"
+
+# translation auto-copied from project libreport, version master, document
+# libreport
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Подробности"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Разработка"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Устройство"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Файл устройства"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Устройство доступно только для чтения"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Название службы"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Диагностические отчёты"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Каталог"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Каталог $0 недоступен для записи или не существует."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Отключить одновременную многопотоковость"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Отключить межсетевой экран"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Отключить демон tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Отключено"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Запретить интерактивный пароль"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Запретить проверку подлинности с помощью пароля"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Запретить выполнение (добавить маску)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Отключиться"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Отключено"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager, author ypoyarko
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Дисковый ввод-вывод"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Диск в порядке"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Сбой диска"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Парольная фраза диска"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Диски"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Использование диска"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+#, fuzzy
+#| msgid "Dismiss $0 alert"
+#| msgid_plural "Dismiss $0 alerts"
+msgid "Dismiss"
+msgstr "Закрыть $0 оповещение"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Закрыть $0 оповещение"
+msgstr[1] "Закрыть $0 оповещения"
+msgstr[2] "Закрыть $0 оповещений"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Закрыть выделенные оповещения"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Язык интерфейса"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+#, fuzzy
+#| msgid "RAID 5 (distributed parity)"
+msgid "Distributed parity (RAID 5)"
+msgstr "RAID 5 (распределённая чётность)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+#, fuzzy
+#| msgid "Not mounted"
+msgid "Do not mount"
+msgstr "Не подключено"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Не монтировать автоматически при загрузке"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Стыковочный узел"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Не запускается автоматически"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+#, fuzzy
+#| msgid "Do not mount automatically on boot"
+msgid "Does not mount during boot"
+msgstr "Не монтировать автоматически при загрузке"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Домен"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Домен"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Адрес домена"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Имя администратора домена"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Пароль администратора домена"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Не удалось установить связь с доменом"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Домен не поддерживается"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Без повтора"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+#, fuzzy
+#| msgid "RAID 6 (double distributed parity)"
+msgid "Double distributed parity (RAID 6)"
+msgstr "RAID 6 (двойная распределённая чётность)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Загрузить"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Загрузите новый браузер бесплатно"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Загружено"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Загрузка"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Загрузка $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Устройство"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Двухранговая"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Дополнительный раздел"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Изменить"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Изменение /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Изменение сервера криптографических ключей Tang"
+
+#: pkg/networkmanager/vlan.jsx:91
+#, fuzzy
+#| msgid "VLAN settings"
+msgid "Edit VLAN settings"
+msgstr "Настройка VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:135
+#, fuzzy
+#| msgid "Bond settings"
+msgid "Edit bond settings"
+msgstr "Параметры соединения"
+
+#: pkg/networkmanager/bridge.jsx:96
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Edit bridge settings"
+msgstr "Параметры моста"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Изменение пользовательской службы в зоне $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Изменить узел"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Изменить узлы"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Изменение motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Точка подключения"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Изменить правила и зоны"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Изменить службу"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Изменить службу $0"
+
+#: pkg/networkmanager/team.jsx:154
+#, fuzzy
+#| msgid "Team settings"
+msgid "Edit team settings"
+msgstr "Параметры сопряжения"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Изменить пользователя"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Для редактирования ключа необходим свободный слот"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Извлечение $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "Встраиваемый компьютер"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Пусто"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Очистить"
+
+#: pkg/users/account-create-dialog.js:201
+#, fuzzy
+#| msgid "Key password"
+msgid "Empty password"
+msgstr "Пароль ключа"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Очистка $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Включить"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Включение или отключение устройства"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Включить службу"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Включить межсетевой экран"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Включено"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+#, fuzzy
+#| msgid "Installing $0"
+msgid "Enabling $0"
+msgstr "Установка $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Шифровать данные"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+#, fuzzy
+#| msgid "Edit Tang keyserver"
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Изменение сервера криптографических ключей Tang"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+#, fuzzy
+#| msgid "Encryption passphrase"
+msgid "Encrypt data with a passphrase"
+msgstr "Парольная фраза шифрования"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Зашифрованный"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Зашифрованный $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted Stratis pool $0"
+msgid "Encrypted Stratis pool"
+msgstr "Зашифрованный пул Stratis $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Зашифрованный логический том $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Зашифрованный раздел $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Шифрование"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Параметры шифрования"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Парольная фраза шифрования"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Тип шифрования"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Завершение"
+
+#: pkg/networkmanager/wireguard.jsx:309
+#, fuzzy
+#| msgid "Entrypoint"
+msgid "Endpoint"
+msgstr "Точка входа"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Принудительная"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Доступны улучшения"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Введите допустимый MAC-адрес"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Адрес всей подсети"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Запись в $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Стирание $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Ошибки"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Ошибка"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Сообщения об ошибках и выше"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Произошла ошибка"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "PackageKit не установлен"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Сообщение об ошибке"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "Ошибка выполнения команды semanage для обнаружения изменений системы"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Ошибка при сохранении авторизованных ключей: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Ошибка при установке режима SELinux: «$0»"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Пример: 22, ssh, 8080, 5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Пример: 88, 2019, nfs, rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Отличный пароль"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Корпус расширения"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Дата завершения срока действия учётной записи"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Дата завершения срока действия учётной записи: $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Экспорт"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Экспортировать в сеть"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Расширенная информация"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Дополнительный раздел"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS активирован неправильно"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Сбой"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Не удалось добавить компьютер: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Не удалось добавить порт"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Не удалось добавить службу"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Не удалось добавить зону"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Не удалось изменить пароль"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "Не удалось настроить конфигурацию PCP"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Не удалось удалить оповещение: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Не удалось отключить демон tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Не удалось отключить профиль демона tuned"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Не удалось изменить компьютер: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Не удалось изменить службу"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "Не удалось включить $0 в firewalld"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Не удалось включить демон tuned"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Не удалось получить журналы"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Не удалось загрузить авторизованные ключи."
+
+#: pkg/systemd/service-details.jsx:539
+#, fuzzy
+#| msgid "Failed to add port"
+msgid "Failed to load unit"
+msgstr "Не удалось добавить порт"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"Не удалось обработать юнит-файлы для dnf-automatic.timer или dnf-automatic-"
+"install.timer. Удалите пользовательские переопределения для настройки "
+"автоматического обновления."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Не удалось перезапустить службу"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Не удалось сохранить изменения в /etc/motd"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Не удалось сохранить параметры"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Сбой запуска"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Не удалось переключить профиль"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Состояние файла"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Файловая система"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "Файловая система заблокирована"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Имя файловой системы"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Создание файловой системы на $target"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+#, fuzzy
+#| msgid "The filesystem is already mounted at $0. Proceeding will unmount it."
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr ""
+"Файловая система уже смонтирована в $0. Продолжение операции приведет к её "
+"размонтированию."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Фильтровать по имени или описанию"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Фильтровать элементы меню"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Фильтровать службы"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Фильтры"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Отпечаток"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Межсетевой экран"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Межсетевой экран недоступен"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Версия микропрограммы"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Размер шрифта"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Выполнение запрещено"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Принудительно изменить"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Принудительно удалить"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Принудительно изменить пароль"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Форматировать"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Форматирование $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+#, fuzzy
+#| msgid "Stop and unmount"
+msgid "Format and mount"
+msgstr "Остановить и отключить"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Stop and unmount"
+msgid "Format and start"
+msgstr "Остановить и отключить"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+#, fuzzy
+#| msgid "Format"
+msgid "Format only"
+msgstr "Форматировать"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "При форматировании устройства хранения все данные на нём удаляются."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Задержка смены состояний $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Номер кадра"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "свободного места"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Произвольный поиск"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "По пятницам"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "От"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Полное имя"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Шлюз"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Общее"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Сгенерирован"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Перейти к $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Перейти к приложению"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Текущий момент"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr ""
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Группа"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Имя группы"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Группы"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Расширить"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Расширить содержимое"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Расширение логического размера $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Расширение логического тома"
+
+#: pkg/storaged/block/resize.jsx:409
+#, fuzzy
+#| msgid "partition"
+msgid "Grow partition"
+msgstr "раздел"
+
+#: pkg/storaged/stratis/pool.jsx:337
+#, fuzzy
+#| msgid "Grow to take all space"
+msgid "Grow the pool to take all space"
+msgstr "Расширить на всё пространство"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Расширить на всё пространство"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Режим NAT loopback"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Режим NAT loopback"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Наладонный компьютер"
+
+#: pkg/storaged/drive/drive.jsx:64
+#, fuzzy
+#| msgid "Hard Disk"
+msgid "Hard Disk Drive"
+msgstr "Жёсткий диск"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Сведения об оборудовании"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Здоровье"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Время приветствия $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Помощь"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Hide confirmation password"
+msgstr "Подтвердите пароль"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "Use password"
+msgid "Hide password"
+msgstr "Введите пароль"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "ИД иерархии"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr ""
+"Увеличение возможностей взаимодействия за счёт увеличения количества "
+"направлений атак."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Число пакетов в истории"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+#, fuzzy
+#| msgid "Directory"
+msgid "Home directory"
+msgstr "Каталог"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Узел"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Неверный ключ узла"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Имя узла"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Узлы"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Ежечасно"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "ч"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr ""
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "Идентификатор"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ИД $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP-адрес"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"IP-адрес с префиксом маршрутизации. Несколько значений нужно разделять "
+"запятой. Например: 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+#, fuzzy
+#| msgid "IPv4 address"
+msgid "IPv4 addresses"
+msgstr "IPv4-адрес"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "Параметры IPv4"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "Параметры IPv6"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Идентификатор"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Если оставить поле пустым, ИД будет сгенерирован на основе связанных служб и "
+"номеров порта"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"При совпадении отпечатка нажмите кнопку «Принять ключ и войти в систему». В "
+"противном случае, не входите в систему и свяжитесь с администратором."
+
+#: pkg/shell/hosts_dialog.jsx:480
+#, fuzzy
+#| msgid ""
+#| "If the fingerprint matches, click 'Accept key and connect'. Otherwise, do "
+#| "not connect and contact your administrator."
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"При совпадении отпечатка нажмите кнопку «Принять ключ и подключиться». В "
+"противном случае, не подключайтесь и свяжитесь с администратором."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Игнорировать"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "Вход"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"В целях обеспечения дальнейшего входа без пароля на $0 как $1, можно в "
+"качестве пароля ключа использовать пароль для входа $2 на $3или оставить "
+"поле для пароля ключа пустым."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "Синхронизировано"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Неактивно"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Create logical volume"
+msgid "Inactive logical volume"
+msgstr "Создание логического тома"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Включённые службы"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"Входящие запросы заблокированы по умолчанию. Исходящие запросы не "
+"заблокированы."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Неправильное монтирование файловой системы"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Увеличить на один"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Память индексов"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Indirect"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Информация"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Информационные сообщения и выше"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Инициализировать"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Инициализировать диск $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "При инициализации все данные на диске уничтожаются."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Инициализация…"
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Анализ: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Установить"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Установка поддержки NFS"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Установка поддержки Stratis"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Установить все обновления"
+
+#: pkg/apps/application-list.jsx:200
+#, fuzzy
+#| msgid "Package information"
+msgid "Install application information"
+msgstr "Сведения о пакете"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Установить cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Установить обновления kpatch"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+#, fuzzy
+#| msgid "Install Stratis support"
+msgid "Install realmd support"
+msgstr "Установка поддержки Stratis"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Установить обновления безопасности"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr "Установите setroubleshoot-server для устранения неполадок в SELinux."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Установка программного обеспечения"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Установите пакет $0"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr ""
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Установлено"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Установка"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Установка $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "Установка $0 приведёт к удалению $1."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Установка пакетов"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Интерфейс"
+msgstr[1] "Интерфейсы"
+msgstr[2] "Интерфейсы"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Члены интерфейса"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Интерфейсы"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Внутренняя ошибка"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Внутренняя ошибка: недопустимый заголовок запроса"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Недопустимый"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Недопустимый адрес $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Недопустимый формат даты"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Недопустимый формат даты и недопустимый формат времени"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Неверная дата окончания срока действия"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Недопустимые разрешения для файлов"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Недопустимый ключ"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Недопустимая метрика $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Недопустимое количество дней"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Недопустимый номер порта"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Недопустимый префикс $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Недопустимый префикс или сетевая маска $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Недопустимый диапазон"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Недопустимый формат времени"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Недопустимый часовой пояс"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Недопустимое имя пользователя или пароль"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "Шлюз Интернета вещей"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "Работает ли sshd на другом порту?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Задания"
+
+# ctx::sourcefile::Navigation Menu
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Присоединиться"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Присоединение к домену"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Присоединиться к домену"
+
+# ctx::sourcefile::Navigation Menu
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Присоединение"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Присоединение к этому домену требует установки realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Присоединение к этому домену не поддерживается"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "JoinsNamespaceOf="
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Журнал"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Запись в журнале"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Запись в журнале не найдена"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Перейти к"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit не установлен"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Проверка параметров kdump"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Сохранить подключение"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Аварийный дамп ядра"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Дамп ядра"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "Интерактивное исправление ядра $0 активировано"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Интерактивное исправление ядра $0 установлено"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Параметры интерактивного исправления ядра"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Интерактивное исправление ядра"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Пароль ключа"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Слоты для ключей с неизвестными типами нельзя изменить здесь"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Источник ключа"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Ключи"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Сервер криптографических ключей"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Адрес сервера криптографических ключей"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr ""
+"Удаление сервера криптографических ключей может помешать разблокировке $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "Ключ LACP"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr ""
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "VDO pool"
+msgid "LVM2 VDO pool"
+msgstr "Пул VDO"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Логический том"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Логические тома"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volume"
+msgstr "Физический том"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "Physical volume"
+msgid "LVM2 physical volumes"
+msgstr "Физический том"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "Группа томов LVM2"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "Группа томов LVM2 $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Полноразмерный ноутбук"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Последние 24 часа"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Последние 7 дней"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Последняя активность"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "Последний слот для ключа не может быть удалён"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Последняя проверка: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "Последний слот для ключа не может быть удалён"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Последний вход"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Время последнего изменения: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Последний удачный вход:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Подробнее"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "Выйти из $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Выйти из домена"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Оставьте пустым для пропуска шифрования"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Распространяется на условиях лицензии GNU LGPL, версия 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Светлый"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Ограничить доступ"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Ограниченный доступ"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"В ограниченном режиме доступа ограничиваются полномочия администратора. В "
+"некоторых разделах веб-консоли будет урезаны функциональные возможности."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Ограничения"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Задержка разрыва соединения"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Локальный адрес канала"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Мониторинг ссылок"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Задержка установки соединения"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Просмотр ссылок"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Linked"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem $0"
+msgid "Linux filesystem data"
+msgstr "Размонтирование файловой системы $0"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Область подкачки"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Прослушивать"
+
+#: pkg/networkmanager/wireguard.jsx:242
+#, fuzzy
+#| msgid "Listen"
+msgid "Listen port"
+msgstr "Прослушивать"
+
+#: pkg/networkmanager/wireguard.jsx:149
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Listen port must be a number"
+msgstr "Размер должен быть числом"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Вывод списка юнитов"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Не удалось вывести список юнитов: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Нагрузка"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Балансировка нагрузки"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Загрузить более ранние данные"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Загрузить более ранние записи"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Загрузка доступных обновлений не удалась"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Загрузка доступных обновлений…"
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Загрузка более ранних записей"
+
+# translation auto-copied from project webkitgtk3, version 2.0.3, document
+# webkitgtk3, author ypoyarko
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Загрузка ключей…"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Не удалось загрузить SSH-ключи"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Не удалось загрузить юниты"
+
+# translation auto-copied from project webkitgtk3, version 2.0.3, document
+# webkitgtk3, author ypoyarko
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Загрузка пакетов…"
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Загрузка изменений системы…"
+
+#: pkg/systemd/service.jsx:129
+#, fuzzy
+#| msgid "Loading unit failed: $0"
+msgid "Loading unit failed"
+msgstr "Не удалось загрузить юнит: $0"
+
+# translation auto-copied from project webkitgtk3, version 2.0.3, document
+# webkitgtk3, author ypoyarko
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Загрузка…"
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Локальные учётные записи"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Локальная файловая система"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Локальная точка подключения"
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "No storage"
+msgid "Local storage"
+msgstr "Нет хранилища"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "локально в $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Расположение"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Заблокировать"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "Блокирование $0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Заблокировать учётную запись"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgid "Locked"
+msgid "Locked data"
+msgstr "Заблокировано"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Блокировка $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Войти"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Вход на $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Войдите в систему с помощью своей учётной записи пользователя сервера."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Сообщения журнала"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Выход"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Выполнить выход пользователя"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Вход выполнен"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Логический размер"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Логический том (моментальный снимок)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Логический размер"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Логический том"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Логический том (моментальный снимок)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Логический том $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Вход"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Войти снова"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Ошибка входа"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Формат учётной записи"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "История входов"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Список истории входов"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "Выход $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Выход из системы успешно выполнен"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Журналы"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Низкопрофильный настольный компьютер"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Портативный компьютер в ударопрочном корпусе"
+
+# translation auto-copied from project Satellite6 Hammer CLI Foreman, version
+# 6.1, document hammer-cli-foreman
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+# translation auto-copied from project Blivet, version master, document blivet
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "RAID-устройство"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "RAID-устройство $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Устройство доступно только для чтения"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "Служба запущена"
+
+# translation auto-copied from project Blivet, version master, document blivet
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "RAID-устройство"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Добавление дисков"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (рекомендуется)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "Значение MTU должно быть положительным числом"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Идентификатор системы"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Отпечатки SSH-ключей компьютера"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Главный серверный корпус"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Обслуживание"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+#, fuzzy
+#| msgid "Rename filesystem"
+msgid "Manage filesystem sizes"
+msgstr "Переименование файловой системы"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+#, fuzzy
+#| msgid "Managed interfaces"
+msgid "Manage storage"
+msgstr "Управляемые интерфейсы"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Управляемые интерфейсы"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "Управление томами LVM"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "Управление подключениями по NFS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "Управление RAID-устройствами"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "Управление устройствами VDO"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "Управление VLAN"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Управление межсетевым экраном"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Управление сетевыми объединениями"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Управление сетевыми мостами"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Управление сетевыми сопряжениями"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Управление разделами"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Управление физическими дисками"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Управление службами"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Управление обновлениями программного обеспечения"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Управление учетными записями пользователей"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Вручную"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Вручную"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Маркировка $target как неисправного"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Скрыть службу"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Скрыто"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Маскирование службы делает невозможным выполнение всех связанных с ней "
+"служб. Эффект может оказаться серьёзнее, чем кажется. Подтвердите "
+"маскирование данной службы."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Максимальное время жизни сообщения $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Оптический привод"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Память"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Технология памяти"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Использование памяти"
+
+#: pkg/metrics/metrics.jsx:1939
+#, fuzzy
+#| msgid "Memory usage"
+msgid "Memory usage/swap"
+msgstr "Использование памяти"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Объединённый"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Сообщение для вошедших пользователей"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "Сообщения, связанные с ошибкой, могут быть найдены в журнале:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Используемые метаданные"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Метод"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Метрика"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Метрики и история"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Не удалось загрузить историю метрик"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Параметры метрик"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Мини-ПК"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Компьютер в корпусе «мини-башня»"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Количество минут должно лежать в интервале от 0 до 59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+#, fuzzy
+#| msgid "Minutes"
+msgid "Minutely"
+msgstr "мин"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "мин"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Меры по защите"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Режим"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Модель"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Изменение $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "По понедельникам"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Интервал мониторинга"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Цели мониторинга"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Ежемесячно"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Дополнительные сведения…"
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Подключение"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+#, fuzzy
+#| msgid "Mount point"
+msgid "Mount Point"
+msgstr "Точка подключения"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Монтировать также автоматически при загрузке"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Подключение при загрузке"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Монтировать автоматически на $0 при загрузке"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Настройки монтирования"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Монтировать файловую систему"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Монтировать сейчас"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Монтировать на $0 сейчас"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Параметры подключения"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Точка подключения"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Точка монтирования не может быть пустой"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Точка подключения не может быть пуста."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Точка монтирования уже используется для $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Точка подключения должна начинаться с символа «/»."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Подключение только для чтения"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Подключение $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Корпус для нескольких систем"
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Multipathed devices"
+msgid "Multipathed devices"
+msgstr "Многоканальные устройства"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "Подключение по NFS"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "Сервер NTP"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Имя"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Имя не должно быть пустым."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Имя не должно быть пустым."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Длина имени в байтах не должна превышать $0"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Длина имени в символах не должна превышать $0"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Длина имени должна составлять не более 127 символов."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Длина имени должна составлять не более 127 символов."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Имя не должно содержать знак «$0»."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "Имя не должно содержать знак «$0»."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Имя не должно содержать пробелы."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Требуется по крайней мере один сервер NTP"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Сеть"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Сетевой ввод/вывод"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Сетевое объединение"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Для сетевых устройств и графиков необходим NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Сетевые журналы"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Использование сети"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager не установлен"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager не работает"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Использование сети"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Сеть"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Никогда"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Учётная запись без срока действия"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Пароль с неограниченным сроком действия"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Вход никогда не выполнялся"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Новое подключение по NFS"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Новый узел"
+
+#: pkg/shell/hosts_dialog.jsx:464
+#, fuzzy
+#| msgid "New host"
+msgid "New host: $0"
+msgstr "Новый узел"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Новый пароль ключа"
+
+#: pkg/users/rename-group-dialog.jsx:35
+#, fuzzy
+#| msgid "Rename"
+msgid "New name"
+msgstr "Переименовать"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Новая парольная фраза"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Новый пароль"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Новый пароль не был принят"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Далее"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Нет"
+
+#: pkg/users/group-create-dialog.js:79
+#, fuzzy
+#| msgid "No alias specified"
+msgid "No ID specified"
+msgstr "Не указан псевдоним"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Оповещения SELinux отсутствуют."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Установленных или доступных приложений нет."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Нет доступных слотов"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Нет доступных блочных устройств."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No block devices found"
+msgstr "Загрузочное устройство не найдено"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Нет несущей частоты"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Конфигурация не найдена"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Данные недоступны"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Данные с $0 по $1 недоступны"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Без задержки"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Описание отсутствует"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Описание отсутствует."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No boot device found"
+msgid "No devices found"
+msgstr "Загрузочное устройство не найдено"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Нет доступных дисков."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Журналов не найдено"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "Нет результатов"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Без шифрования"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Нет файловой системы"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Нет файловых систем"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Нет свободных слотов для ключей"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Нет свободного места"
+
+#: pkg/storaged/partitions/partition.jsx:79
+#, fuzzy
+#| msgid "Create partition"
+msgid "No free space after this partition"
+msgstr "Создать раздел"
+
+#: pkg/users/group-create-dialog.js:62
+#, fuzzy
+#| msgid "No real name specified"
+msgid "No group name specified"
+msgstr "Не указано настоящее имя"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Ключи узла не найдены."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Пакет установки для данного приложения не найден."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Нет добавленных ключей"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr ""
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Нет записей в журнале"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Нет логических томов"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Журналов не найдено"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Соответствующие результаты не найдены"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Носитель не вставлен"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Без разбиения"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No partitioning"
+msgid "No partitions found"
+msgstr "Без разбиения"
+
+#: pkg/networkmanager/wireguard.jsx:341
+#, fuzzy
+#| msgid "No keys added"
+msgid "No peers added."
+msgstr "Нет добавленных ключей"
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "No physical volumes found"
+msgstr "Физические тома"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Не указано настоящее имя"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Нет результатов"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Совпадения с критериями фильтров отсутствуют. Сбросьте все фильтры для "
+"вывода результатов."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Нет совпадений с правилами"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No storage"
+msgid "No storage found"
+msgstr "Нет хранилища"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Нет логических томов"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Нет такого файла или каталога"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Изменения системы отсутствуют"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Системные отчёты отсутствуют."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Обновлений нет"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Не указано имя пользователя"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Нет"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Недопустимый закрытый ключ"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Нет прав для отключения межсетевого экрана"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Нет прав для включения межсетевого экрана"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Недоступно"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Подключение отсутствует"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Нет подключения к Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Отсутствует подключение к узлу"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough free space"
+msgstr "Недостаточно места для увеличения."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space"
+msgstr "Недостаточно места для увеличения."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "Недостаточно места для увеличения."
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Не найдено"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Не установлено"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "Not permitted to configure realms"
+msgid "Not permitted to configure network devices"
+msgstr "Запрещено настраивать области"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Запрещено настраивать области"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Нет прав на выполнение этого действия."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Не готово"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Не зарегистрировано"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Не работает"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Не настроено"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Не синхронизировано"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Примечание"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Ноутбук"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Уведомления и выше"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Скрывать сетевые адреса, имена узлов и пользователей"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Скрытый"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Произошло $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Произошло в период между $0 и $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "События"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "OK"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Старая парольная фраза"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Старый пароль"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Старый пароль не был принят"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "На подключенном устройстве"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "OnFailure="
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"После установки Cockpit включите его при помощи команды «systemctl enable --"
+"now cockpit.socket»."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Используется только $0 из $1."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Допускаются только буквы, цифры и символы «:», «_», «.», «@» и «-»"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Только экстренные оповещения"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"При загрузке в режиме FIPS можно использовать только утверждённые и "
+"разрешённые алгоритмы."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Упс!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr ""
+"Включите службу pmproxy в межсетевом экране для совместного использования "
+"метрик."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Операция «$operation» над $target"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Параметры"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Или используйте встроенный браузер"
+
+# translation auto-copied from project Satellite6 Katello, version Sam-1.3.0,
+# document katello, author ypoyarko
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Прочее"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"Другие методы проверки подлинности остаются доступны даже при запрете "
+"интерактивной проверки подлинности пароля."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Другие параметры"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Выход"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Обзор"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Перезаписать"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Заменить существующие данные нулями (медленно)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Пакет cockpit-pcp для истории метрик отсутствует"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Сведения о пакете"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "Сбой PackageKit"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit не установлен"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit передал код ошибки $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Пакеты"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Название страницы"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Родительский элемент"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Родительский элемент $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "PartOf="
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Часть $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Раздел"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Раздел $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+#, fuzzy
+#| msgid "Volume size is $0. Content size is $1."
+msgid "Partition size is $0. Content size is $1."
+msgstr "Размер тома составляет $0. Содержимое занимает $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Разбиение"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Разделы"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Пассивно"
+
+# translation auto-copied from project Anaconda, version master, document
+# anaconda, author Igor Gorbounov
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Парольная фраза"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "Парольная фраза не может быть пустой"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Парольная фраза не может быть пустой"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Парольная фраза из любого другого слота для ключа"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Удаление парольной фразы может помешать разблокировке $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Парольные фразы не совпадают"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Пароль"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Пароль успешно изменён"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Срок действия пароля"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "Длина пароля составляет более 256 символов"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Недопустимый пароль"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Пароль недостаточно надёжен"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Необходимо изменить пароль"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Пароль не принят"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Подсказка для пароля"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Вставить"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Ошибка вставки"
+
+#: pkg/networkmanager/wireguard.jsx:215
+#, fuzzy
+#| msgid "Use existing"
+msgid "Paste existing key"
+msgstr "Использовать существующий"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Вставьте сюда содержимое вашего файла открытого SSH-ключа"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Путь"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Стоимость пути"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Стоимость пути $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Путь на сервере"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Путь на сервере не может быть пустым."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Путь на сервере должен начинаться с символа «/»."
+
+#: pkg/users/account-create-dialog.js:88
+#, fuzzy
+#| msgid "Directory"
+msgid "Path to directory"
+msgstr "Каталог"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Путь к файлу"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Пути"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Приостановить"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot собирает и анализирует метрики производительностипо "
+"данной системе."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Профиль производительности"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Корпус для периферийных устройств"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Постоянный"
+
+#: pkg/users/delete-group-dialog.js:33
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete $0 group?"
+msgstr "Окончательно удалить $0?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "Окончательно удалить $0?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Окончательно удалить $0?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete subvolume $0?"
+msgstr "Окончательно удалить $0?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "В разрешении отказано"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Нестрогая"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Физический размер"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+#, fuzzy
+#| msgid "Physical volumes"
+msgid "Physical Volumes"
+msgstr "Физические тома"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Физические тома"
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Physical volumes can not be resized here."
+msgid "Physical volumes can not be resized here"
+msgstr "Размер физических томов невозможно изменить здесь."
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Выбор даты"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Закрепить юнит"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Интервал команды ping"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Цель команды ping"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Закреплённый юнит"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Ультратонкий корпус"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Пройдите проверку подлинности для получения прав администратора"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Включите JavaScript для использования веб-консоли."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Установите пакет $0"
+
+#: pkg/packagekit/updates.jsx:1492
+#, fuzzy
+#| msgid "Please reload the page after resolving the issue."
+msgid "Please resolve the issue and reload this page."
+msgstr "Перезагрузите страницу после устранения проблемы."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Укажите дату окончания срока действия"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Укажите узел для подключения"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr ""
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Пул для тонких логических томов"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Пул для «тонко» резервируемых томов"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Пул для «тонко» резервируемых томов"
+
+#: pkg/storaged/stratis/pool.jsx:464
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Pool passphrase"
+msgstr "Старая парольная фраза"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Порт"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Портативный компьютер"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Порты"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Создать раздел"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Длина префикса"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Длина префикса или маска сети"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Подготовка"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Присутствует"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Сохранять"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Автоматическое имя узла"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Предыдущая загрузка"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Первичный интерфейс"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Приоритет"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Приоритет $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+#, fuzzy
+#| msgid "Private"
+msgid "Private key"
+msgstr "Частная"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "При получении прав администратора возникли проблемы"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Подробности проблемы"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Информация о проблеме"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Процессы, использующие это расположение"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Ход выполнения: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Веб-сайт проекта"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Превышено время ожидания запроса по passwd"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Превышено время ожидания запроса по ssh-add"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Превышено время ожидания запроса по ssh-keygen"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "PropagatesReloadTo="
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Обеспечивает защиту от атак, предполагаемых в ближайшей перспективе, за счёт "
+"уменьшения возможностей взаимодействия."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "Введите парольную фразу для пула этих блоковых устройств:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Открытый ключ"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Назначение"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (чередование)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (зеркалирование)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (чередование зеркалирования)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (выделенная чётность)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (распределённая чётность)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (двойная распределённая чётность)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "Корпус для RAID-массива"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "Уровень RAID"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "ОЗУ"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Монтируемый в стойку корпус"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Случайный"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Диапазон"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "Диапазон должен быть строго упорядочен"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Ранг"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Непосредственный доступ к устройству"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Чтение"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Подробнее…"
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Только для чтения"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Чтение"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Чтение…"
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Готово"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Готово"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Реальное имя узла"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Реальное имя узла может содержать только строчные буквы, цифры, тире и точки "
+"(с заполненными поддоменами)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Реальное имя узла должно содержать не более 64 символов"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Повторно применить и перезагрузить"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Перезагрузка"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Перезагрузить после завершения"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Рекомендуется перезагрузка"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Перезагрузить систему…"
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Приём"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Недавние узлы"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr ""
+"Рекомендуется, безопасные настройки для существующих сейчас моделей угроз."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Повторить подключение"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Восстановливается"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "Восстановление RAID-устройства $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Обновление сведений о пакете"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Отказано в подключении. Неизвестный узел"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Отказано в подключении. Неверный ключ узла"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Отказано в подключении. Неизвестный ключ узла"
+
+#: pkg/networkmanager/wireguard.jsx:224
+#, fuzzy
+#| msgid "Generated"
+msgid "Regenerate"
+msgstr "Сгенерирован"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+#, fuzzy
+#| msgid "Generating report"
+msgid "Regenerating initrd"
+msgstr "Создание отчёта"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Зарегистрировать…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "Связанные процессы и службы будут принудительно остановлены."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "Связанные процессы будут принудительно остановлены."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "Связанные службы будут принудительно остановлены."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Перезагрузить"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "ReloadPropagatedFrom="
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Перезагрузка"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Перезагрузка состояния оставшихся служб"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "Удалённый доступ по CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "Удалённый доступ по FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Удалённый доступ по NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "Удалённый доступ по NFS"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "Удалённый доступ по SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Удалённый доступ по SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "Удалённый доступ по SSH"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Для удаления:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Удалить"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Удалить $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Удалить службу $0 из зоны $1"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Удалить $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Удалить сервер криптографических ключей Tang?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Удалить устройство"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Удалить узел"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Удалить элемент"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Remove missing physical volumes?"
+msgstr "Удаление физического тома из $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "Удалить парольную фразу в слоте ключа $0?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+#, fuzzy
+#| msgid "Remove passphrase"
+msgid "Remove passphrase?"
+msgstr "Удаление парольной фразы"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Удалить службу $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Удалить зону $0"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Удаление"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Удаление $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Удаление $0 приведёт к разрыву соединения с сервером и недоступности "
+"интерфейса администратора."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "Удаление $target с RAID-устройства"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"Удаление парольной фразы без подтверждения другой парольной фразой может "
+"привести к невозможности разблокировки или запрету управления ключами, если "
+"другие парольные фразы забыты или утеряны."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Удаление физического тома из $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Удаление службы cockpit может привести к недоступности веб-консоли. "
+"Убедитесь, что эта зона не относится к вашему текущему подключению к веб-"
+"консоли."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "Удаление зоны приведет к удалению всех служб в ней."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Переименовать"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Переименование пула Stratis"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Переименование файловой системы"
+
+#: pkg/users/group-actions.jsx:39
+#, fuzzy
+#| msgid "Rename volume group"
+msgid "Rename group"
+msgstr "Переименование группы томов"
+
+#: pkg/users/rename-group-dialog.jsx:60
+#, fuzzy
+#| msgid "Volume group $0"
+msgid "Rename group $0"
+msgstr "Группа томов $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Переименование логического тома"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Переименование группы томов"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Переименование $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+#, fuzzy
+#| msgid "Rename logical volume"
+msgid "Repair logical volume $0"
+msgstr "Переименование логического тома"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Восстановление $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Повторять"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Повторять ежемесячно"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Подтверждение парольной фразы"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Повторять еженедельно"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Сообщить"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Метка отчёта"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Сообщить в ABRT Analytics"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Сообщение сделано; нет ссылок"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Сообщение не удалось"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Отчеты отменены"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Отчёты"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Отчеты:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Требовать изменения пароля каждые $0 дней"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Потребовать смену пароля $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Потребовать смену пароля при первом входе в систему"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "RequiredBy="
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Требуется для "
+
+# translation auto-copied from project RHN Satellite UI, version 5.7, document
+# java/code/src/com/redhat/rhn/frontend/strings/jsp/StringResource, author
+# ypoyarko
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Requires="
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Для изменения необходимы права администратора"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Requisite="
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "RequisiteOf="
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Зарезервированная память"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Сброс"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Сбросить пароль"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Изменение размера $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Для изменения размера зашифрованной файловой системы необходимо "
+"разблокировать диск. Введите парольную фразу текущего диска."
+
+# ctx::sourcefile::/rhn/admin/config/Restart.do
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Перезапустить"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Перезапуск служб"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Перезапустить службы…"
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Перезапуск"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Восстановление подключения"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Возобновить"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Повторное использование существующего шифрования"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Повторное использование существующего шифрования ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+#, fuzzy
+#| msgid "Review crypto policy"
+msgid "Review cryptographic policy"
+msgstr "Пересмотреть правила шифрования"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Просмотр журналов"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Циклический перебор"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Маршруты"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Запускать в"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Создать новый отчёт"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Запускать по"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Создать отчёт"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Средство запуска"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Работает"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Среда выполнения"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "Ошибки управления доступом SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux отключён в системе"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux отключён в системе."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "Политика SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "Состояние системы SELinux неизвестно."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "Устранение неполадок SELinux"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "SMART самопроверка $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"В процессе создания SOS-отчёта производится сбор системной информации для "
+"оказания помощи при выявлении проблем."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH-ключ"
+
+#: pkg/kdump/kdump-view.jsx:164
+#, fuzzy
+#| msgid "ssh key isn't a path"
+msgid "SSH key isn't a path"
+msgstr "ключ SSH не является путём"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH-ключи"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "Задержка смены состояний"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "Время приветствия"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "Максимальное время жизни сообщения"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "Приоритет"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Пользователям Safari необходимо импортировать и установить доверие к "
+"самозаверенному сертификату центра сертификации:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "По субботам"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Сохранить"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Сохранить и перезагрузить"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Сохранить изменения"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Экономьте место, сжимая отдельные блоки по алгоритму LZ4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Экономьте место, сохраняя блоки идентичных данных лишь один раз"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Для сохранения новой парольной фразы требуется снятие блокировки диска. "
+"Укажите действующую парольную фразу диска."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Запланировано отключение в $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Запланирована перезагрузка в $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "Компьютер с невскрываемым корпусом"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Поиск"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Домен поиска"
+
+#: pkg/users/accounts-list.js:296
+#, fuzzy
+#| msgid "Search for name, group or ID"
+msgid "Search for name or ID"
+msgstr "Поиск имени, группы или идентификатора"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Поиск имени, группы или идентификатора"
+
+#: pkg/systemd/timer-dialog.jsx:280
+#, fuzzy
+#| msgid "Minute needs to be a number between 0-59"
+msgid "Second needs to be a number between 0-59"
+msgstr "Количество минут должно лежать в интервале от 0 до 59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "сек"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Ключи Secure Shell"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Надёжное стирание $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Доступны обновления безопасности"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Только обновления безопасности"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Обновления безопасности будут применены $0 в $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Выбрать"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Выбор идентификатора"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Выбор метода"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Отправить"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Передача"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Серийный номер"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Сервер"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Адрес сервера"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Адрес сервера не может быть пустым."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Сервер не может быть пустым."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Сервер закрыл соединение."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Серверное программное обеспечение"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Служба"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Ошибка в службе"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Журналы службы"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Службы"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Службы, использующие это расположение"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Сеанс"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Сессия истекает"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Настроить"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Установить название узла"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Создание раздела на $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Задать пароль"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Настроить время"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Установлено на"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Настроено"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Задать слабый пароль"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Настройка отклонилась от настроенного состояния и будет восстановлена при "
+"следующей загрузке."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Настройка"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Настройка петлевого устройства $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Параметры"
+
+# translation auto-copied from project Customer Portal Translations, version
+# PCM_template, document template, author ypoyarko
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Серьёзность"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "С общим доступом"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+#, fuzzy
+#| msgid "shell"
+msgid "Shell"
+msgstr "оболочка"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Сценарий оболочки"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "Показать все потоки"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Показать все потоки"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Show confirmation password"
+msgstr "Подтвердите пароль"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Показать отпечатки"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Показать сообщения, содержащие данную строку."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Показать сообщения для указанного юнита systemd."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Показать сообщения для указанной загрузки."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Показать больше связей"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "New password"
+msgid "Show password"
+msgstr "Новый пароль"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Показать связи"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Сжать"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Сжатие логического тома"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+#, fuzzy
+#| msgid "partition"
+msgid "Shrink partition"
+msgstr "раздел"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Сжать том"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Завершение работы"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Выключить"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "C"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Одноранговая"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Размер"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "Размер не может быть отрицательным"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "Размер не может быть равен нулю"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Слишком большой размер"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Размер должен быть числом"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Размер должен быть не менее $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Пропустить основную навигацию"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Перейти к содержимому"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Слот"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Слот $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Моментальный снимок"
+
+# translation auto-copied from project Satellite6 Katello, version Sam-1.3.0,
+# document katello, author ypoyarko
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Сокеты"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Обновления программного обеспечения"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Программные обходные решения помогают предотвратить проблемы безопасности "
+"ЦП. Побочным эффектом этих мер является снижение производительности. Вы "
+"изменяете эти параметры на свой страх и риск."
+
+#: pkg/storaged/drive/drive.jsx:63
+#, fuzzy
+#| msgid "Solid-State Disk"
+msgid "Solid State Drive"
+msgstr "Твердотельный накопитель"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Решение успешно применено"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Не удалось применить решение"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Решения"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"Диспетчер пакетов в настоящее время используется другой программой. "
+"Подождите…"
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Некоторые программы необходимо перезагрузить вручную"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Отсортировано от наименее доверенных к наиболее доверенным"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Компактный компьютер"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Протокол связующего дерева"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Протокол связующего дерева (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "В запасе"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Определённое время"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Скорость"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Стабильный"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Запустить"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Включить и запустить"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Запустить multipath"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Запустить службу"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr "Показывать записи, начиная с указанной даты и более поздних."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "Показывать записи, начиная с указанной даты и более ранних."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Начало"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "Запуск RAID-устройства $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Запуск области подкачки $target"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Состояние"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Статически"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Состояние"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "ПК-брелок"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Закреплено"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Остановить"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Остановить и отключить"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Остановить и удалить"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Остановить и отключить"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Остановить устройство"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Закончить изменение узлов"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Остановить создание отчёта"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "Остановка RAID-устройства $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Остановка области подкачки $target"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Хранилище"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "Управление хранилищем недоступно в данной системе."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Журналы хранения"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Хранить парольную фразу"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Сохранённая парольная фраза"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "$0 Блочное устройство"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "Add block devices"
+msgid "Stratis block devices"
+msgstr "Добавить блочные устройства"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+#, fuzzy
+#| msgid "VDO backing devices can not be made smaller"
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Устройства резервирования VDO не могут быть уменьшены"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystem"
+msgstr "Создать файловую систему"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems"
+msgstr "Создать файловую систему"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Create filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Создать файловую систему"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Пул Stratis"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:97
+#, fuzzy
+#| msgid "Set password"
+msgid "Strong password"
+msgstr "Задать пароль"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Заглушка"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Внешний вид"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Дополнительный корпус"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Субноутбук"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "Сбой подписки на сигналы systemd: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "Не удалось создать том"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Успешно скопировано в буфер обмена"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Успешно скопировано в буфер обмена!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "По воскресеньям"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Подкачка"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 filesystems can not be resized here."
+msgid "Swap can not be resized here"
+msgstr "Размер файловых систем $0 невозможно изменить здесь."
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Подкачка"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Переключение $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Отключить $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Включить $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Переключиться на доступ с правами администратора"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Переключится на ограниченный доступ"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Отключение $0 приведёт к разрыву соединения с сервером и недоступности "
+"интерфейса администратора."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Включение $0 приведёт к разрыву соединения с сервером и недоступности "
+"интерфейса администратора."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Синхронизировано"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Синхронизировано с $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Синхронизация"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "Синхронизация RAID-устройства $target"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager, author ypoyarko
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Система"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Диагностика системы"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Сведения о системе"
+
+# ctx::sourcefile::/systems/SystemEntitlements
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Система не нуждается в обновлении"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Изменения системы"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Системное время"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Юниты systemd"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Планшетный ПК"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Сервер криптографических ключей Tang"
+
+# Этап передачи событий
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Цель"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Цели"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Сопряжение"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Порт сопряжения"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Параметры порта сопряжения"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Терминал"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Завершить сеанс"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Проверить конфигурацию"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Проверка доступна только при запущенной службе kdump."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Проверка параметров kdump"
+
+#: pkg/kdump/kdump-view.jsx:374
+#, fuzzy
+#| msgid ""
+#| "This will test kdump settings by crashing the kernel and thereby the "
+#| "system. Depending on the settings, the system may not automatically "
+#| "reboot and the process may take a while."
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Проверка параметров kdump будет произведена путём осуществления сбоя в ядре "
+"и, тем самым, во всей системе. В зависимости от параметров система может не "
+"перезагружаться автоматически, а процесс может занять некоторое время."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Проверка подключения"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+#, fuzzy
+#| msgid "$0 is not available from any repository."
+msgid "The $0 package is not available from any repository."
+msgstr "Компонент $0 недоступен в репозиториях."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "Для создания пулов Stratis должен быть установлен пакет $0."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+#, fuzzy
+#| msgid "The $0 package must be installed to create Stratis pools."
+msgid "The $0 package must be installed."
+msgstr "Для создания пулов Stratis должен быть установлен пакет $0."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "Для создания устройств VDO будет установлен пакет $0."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP-адрес или имя узла не могут содержать пробелы."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "RAID-массив находится в состоянии сбоя"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr "RAID-устройство должно быть запущено для удаления дисков."
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr "SSH-ключ $0 для $1 на $2 будет добавлен к файлу $3 для $4 на $5."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"SSH-ключ $0 будет предоставлен на оставшееся время сеанса, а также будет "
+"доступен для входа на другие узлы."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"SSH-ключ для входа на $0 защищён паролем, а вход с паролем на узле запрещён. "
+"Введите пароль для ключа на $1."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"SSH-ключ для входа на $0 защищён. Вход возможен либо при указании пароля для "
+"входа, либо пароля ключа на $1."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"Для учётной записи «$0» будет запрошено принудительное изменение пароля при "
+"следующем входе в систему"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Служба cockpit автоматически входит в состав"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"Настроенное состояние неизвестно и может измениться при следующей загрузке."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"Создание данного устройства VDO не завершилось, поэтому устройство не может "
+"быть использовано."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr "Текущему пользователю не разрешено просматривать сведения о ключах."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"Перед форматированием диск необходимо разблокировать. Введите "
+"соответствующую парольную фразу."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "Файл $0 будет удалён."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "У этой файловой системы нет постоянной точки монтирования."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "У этой файловой системы нет постоянной точки монтирования."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Настройка файловой системы обеспечивает автоматическое монтирование при "
+"загрузке, но к моменту монтирования контейнер шифрования не будет "
+"разблокирован."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Файловая система сейчас смонтирована, после следующей загрузки не будет "
+"смонтирована."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Файловая система сейчас смонтирована на $0, но при следующей загрузке будет "
+"монтироваться на $1."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Файловая система сейчас смонтирована на $0, но не будет монтироваться при "
+"следующей загрузке."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Файловая система сейчас не смонтирована, но будет монтироваться при "
+"следующей загрузке."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Файловая система не смонтирована."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"Файловая система будет разблокирована и смонтирована при следующей загрузке. "
+"При этом может потребоваться ввод парольной фразы."
+
+#: pkg/shell/hosts_dialog.jsx:494
+#, fuzzy
+#| msgid "Show fingerprints"
+msgid "The fingerprint should match:"
+msgstr "Показать отпечатки"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Следующая служба будет перезагружена:"
+msgstr[1] "Следующие службы будут перезагружены:"
+msgstr[2] "Следующие службы будут перезагружены:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "Полное имя не должно содержать двоеточий."
+
+#: pkg/users/group-create-dialog.js:83
+#, fuzzy
+#| msgid "MTU must be a positive number"
+msgid "The group ID must be positive integer"
+msgstr "Значение MTU должно быть положительным числом"
+
+#: pkg/users/group-create-dialog.js:66
+#, fuzzy
+#| msgid ""
+#| "The user name can only consist of letters from a-z, digits, dots, dashes "
+#| "and underscores."
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Имя пользователя может содержать только латинские буквы a-z, цифры, точки, "
+"тире и символы подчёркивания."
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Пароль ключа не может быть пустым"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Пароли ключа не совпадают"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "Предоставленный ключ недействителен."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Последний слот для ключа не может быть удалён"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "Перечисленные процессы и службы будут принудительно остановлены."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "Перечисленные процессы будут принудительно остановлены."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "Перечисленные службы будут принудительно остановлены."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "Текущему пользователю запрещено просматривать изменения системы"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "Компьютер перезагружается"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "Точка монтирования $0 используется следующими процессами:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "Точка монтирования $0 используется следующими службами:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Новый пароль ключа не может быть пустым"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Пароль не может быть пустым"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Пароли не совпадают"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Пароли не совпадают."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"Полученный отпечаток можно распространять по общедоступным каналам связи, "
+"включая электронную почту."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Сервер отклонил проверку подлинности «$0» с помощью пароля, а другие "
+"доступные методы проверки подлинности отсутствуют."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr ""
+"Сервер отклонил проверку подлинности с использованием любых поддерживаемых "
+"методов."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+"Пользователю $0 не разрешено изменять параметры, влияющие на безопасность ЦП"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+#, fuzzy
+#| msgid "The user $0 is not permitted to change crypto policies"
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "Пользователю $0 запрещено изменять правила шифрования"
+
+#: pkg/kdump/kdump-view.jsx:505
+#, fuzzy
+#| msgid "The user $0 is not permitted to change the system time"
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "Пользователю $0 запрещено изменять системное время"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"Пользователь должен выйти и снова войти в систему, чтобы применить изменения "
+"параметров."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Имя пользователя может содержать только латинские буквы a-z, цифры, точки, "
+"тире и символы подчёркивания."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr "Конфигурация веб-браузера препятствует запуску Cockpit (недоступен $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "В настоящее время нет активных страниц"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"В системе есть устройства, использующие несколько путей, но служба multipath "
+"не работает."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "В этой зоне нет активных служб"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Для этой учётной записи нет авторизованных открытых ключей."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Произошла непредвиденная ошибка при подключении к компьютеру."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Будут внесены следующие изменения:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Тонкий логический том"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+#, fuzzy
+#| msgid "Pool for thinly provisioned volumes"
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Пул для «тонко» резервируемых томов"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"Данное подключение по NFS используется. Возможно только изменение его "
+"параметров."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Данное устройство VDO не полностью использует резервное устройство."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Этим устройством невозможно управлять здесь."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Этим устройством невозможно управлять здесь."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "Это устройство в данный момент используется."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Это поле не может быть пустым"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Данная группа является основной группой для следующих пользователей:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "Этот узел будет перезагружен после установки обновлений."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "Эта информация хранится только в системе."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "Логический том используется содержимым не до конца."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Этот компьютер уже добавлен."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Это может занять некоторое время"
+
+#: pkg/storaged/partitions/partition.jsx:204
+#, fuzzy
+#| msgid "This logical volume is not completely used by its content."
+msgid "This partition is not completely used by its content."
+msgstr "Логический том используется содержимым не до конца."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:333
+#, fuzzy
+#| msgid "This VDO device does not use all of its backing device."
+msgid "This pool does not use all the space on its block devices."
+msgstr "Данное устройство VDO не полностью использует резервное устройство."
+
+#: pkg/storaged/stratis/pool.jsx:348
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "This pool is in a degraded state."
+msgstr "RAID-массив находится в состоянии сбоя"
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Эта система не зарегистрирована"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Эта система использует настраиваемый профиль"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Эта система использует рекомендуемый профиль"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Эта служба не предназначена для явного включения."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Такое имя пользователя уже существует"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr ""
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Этот веб-браузер слишком устарел для запуска Веб-консоли (отсутствует $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Здесь можно добавить строку для совпадения с параметром «_BOOT_ID=». При "
+"отсутствии строки выводятся записи журнала для текущей загрузки. Если вместо "
+"идентификатора загрузки указано положительное число, будут выведены загрузки "
+"с началаведения журнала, если же число меньше или равно нулю, вывод записей "
+"начинается с конца журнала. Например, 1 выводит первую загрузку в журнале в "
+"хронологическом порядке, 2 — вторую и так далее; при этом -0 означает "
+"последнюю загрузку, -1 предпоследнюю загрузку и так далее."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Здесь можно добавить строку для совпадения с параметрами «_SYSTEMD_UNIT=», "
+"«COREDUMP_UNIT=» и «UNIT=» для поиска всех возможных сообщений по указанному "
+"юниту. Значение параметра может быть представлено в виде нескольких юнитов, "
+"разделённых запятыми. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "В дальнейшем это позволит выполнять вход без ввода пароля."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Эта зона содержит службу cockpit. Убедитесь, что эта зона не относится к "
+"текущему соединению веб-консоли."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "По четвергам"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Уровень"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Время"
+
+# ctx::sourcefile::/rhn/account/LocalePreferences
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Часовой пояс"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Не удалось создать таймер"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "Не удалось удалить таймер"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Таймеры"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Совет: сделайте пароль ключа и пароль для входа в систему одинаковыми для "
+"автоматической проверки подлинности в других системах."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Чтобы убедиться в том, что данные вашего соединения не были перехвачены "
+"злоумышленниками, проверьте отпечаток ключа данного узла:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Для получения обновлений программного обеспечения система должна быть "
+"зарегистрирована Red Hat с использованием либо клиентского портала Red Hat, "
+"либо локального сервера подписки."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Для проверки отпечатка запустите следующую команду на $0, физически работая "
+"на этом компьютере или по доверенной сети:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Сегодня"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Переключить"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Переключить средство выбора даты"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Переключение фильтров"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Слишком много данных"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Инструменты"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "5 служб с самым большим потреблением процессорного времени"
+
+#: pkg/metrics/metrics.jsx:963
+#, fuzzy
+#| msgid "Top 5 CPU services"
+msgid "Top 5 disk usage services"
+msgstr "5 служб с самым большим потреблением процессорного времени"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "5 служб с самым большим потреблением памяти"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Общий размер: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Компьютер в корпусе «башня»"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Transient"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Передача"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Триггер"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "TriggeredBy="
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Triggers="
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Устранить неполадки"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Устранить неполадки…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+#, fuzzy
+#| msgid "Untrusted host"
+msgid "Trust and add host"
+msgstr "Недоверенный узел"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Ключ доверия"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Уровень доверия"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Повторить попытку"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Попытка синхронизации с $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "По вторникам"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Сбой при запуске демона tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned — это служба, предназначенная для контроля системы и оптимизации "
+"производительности при определённых нагрузках. В основе Tuned лежат профили, "
+"позволяющие настроить систему для различных случаев использования."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Демон tuned недоступен"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Демон tuned не запущен"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Демон tuned отключен"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Включить доступ с правами администратора"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Тип"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Имя не должно содержать знак «$0»."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Введите условия фильтра"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Не удалось применить это решение автоматически"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Не удалось подключиться к адресу"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "Не удалось установить связь с $0."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Не удалось установить связь c заданным узлом $0. Убедитесь, что на порте $1 "
+"работает SSH или укажите в адресе другой порт."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Не удалось получить сведения об оповещении."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Не удалось войти на $0 с проверкой подлинности по SSH-ключу. Введите пароль. "
+"Для автоматического входа следует настроить SSH-ключи."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Не удалось войти на $0. Узел не принимает вход ни по паролю, ни по одному из "
+"имеющихся SSH-ключей."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Не удаётся связаться с сервером"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Не удаётся удалить подключение"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+#, fuzzy
+#| msgid "Encrypted logical volume of $0"
+msgid "Unable to repair logical volume $0"
+msgstr "Зашифрованный логический том $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+#, fuzzy
+#| msgid "Unable to run fix: %0"
+msgid "Unable to run fix: $0"
+msgstr "Не удаётся запустить исправление: %0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "Не удалось сохранить параметры"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "Не удалось сохранить параметры: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Не удаётся запустить демон setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Не удаётся отключить файловую систему"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Недоступно"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Недоступные пакеты"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Отменить изменения"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr ""
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Непредвиденная ошибка"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Нераспознанные данные"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Устройство"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Неизвестно"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Неизвестно «$0»"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Неизвестно ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Неизвестное приложение"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Неизвестная конфигурация"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Неизвестное имя узла"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Неизвестное название службы"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Неизвестный тип"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Разблокировать"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Автоматически снимать блокировку при загрузке"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Снятие блокировки зашифрованного пула Stratis"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "Разблокирование ключа $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Снятие блокировки $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Снятие блокировки диска"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Неуправляемые интерфейсы"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Отключить"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "Размонтирование файловой системы $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Размонтировать сейчас"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Отключение $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Без названия"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "Открепить юнит"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Нераспознанные данные"
+
+#: pkg/storaged/block/resize.jsx:306
+#, fuzzy
+#| msgid "Unrecognized data can not be made smaller here."
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Нераспознанные данные не могут быть уменьшены здесь."
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Нераспознанные данные не могут быть уменьшены здесь."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Неподдерживаемый том"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "До"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Недоверенный узел"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Обновить"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Таблица успешного обновления"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "История обновлений"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Обновить сведения о пакете"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "Обновление выполнено успешно"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Обновлено"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr "После обновления пакетов может потребоваться перезагрузка."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Доступны обновления"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "История обновлений"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Применение обновлений будет происходить $0 в $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Обновление"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Обновление состояния…"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Время работы"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Использование"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "Использование $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Использование"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Использовать сжатие"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Использовать дедупликацию"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Использовать ключ"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Введите пароль"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Используйте следующие ключи для проверки подлинности в других системах"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Использовать ввод подробных сведений в журнал"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Использовано"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Пользователь"
+
+#: pkg/users/account-create-dialog.js:104
+#, fuzzy
+#| msgid "User"
+msgid "User ID"
+msgstr "Пользователь"
+
+#: pkg/users/account-create-dialog.js:191
+#, fuzzy
+#| msgid "This volume is already used by another VM."
+msgid "User ID is already used by another user"
+msgstr "Этот том уже используется другой виртуальной машиной."
+
+#: pkg/users/account-create-dialog.js:181
+#, fuzzy
+#| msgid "MTU must be a positive number"
+msgid "User ID must be a positive integer"
+msgstr "Значение MTU должно быть положительным числом"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:184
+#, fuzzy
+#| msgid "Name cannot be longer than $0 bytes"
+msgid "User ID must not be lower than $0"
+msgstr "Длина имени в байтах не должна превышать $0"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Имя пользователя"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Имя пользователя не может быть пустым"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Имя пользователя"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "С использованием шифрования LUKS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "С использованием сервера Tang"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "Устройства резервирования VDO не могут быть уменьшены"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "Устройство VDO $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "Том файловой системы VDO (сжатие/дедупликация)"
+
+# translation auto-copied from project Anaconda, version master, document
+# anaconda
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "Идентификатор VLAN"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Проверка адреса"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Проверка маркера аутентификации"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Производитель"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Проверено"
+
+#: pkg/shell/hosts_dialog.jsx:489
+#, fuzzy
+#| msgid "Fingerprint"
+msgid "Verify fingerprint"
+msgstr "Отпечаток"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Проверка ключа"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Проверка"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Версия"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Крайне надёжное стирание $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Смотреть все ЦП"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Смотреть все диски"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Смотреть все журналы"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Смотреть все службы"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Просмотреть сценарий автоматизации"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Смотреть подробные журналы"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Просмотреть сведения об оборудовании"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Смотреть истории входов"
+
+#: pkg/storaged/stratis/pool.jsx:351
+#, fuzzy
+#| msgid "View all logs"
+msgid "View logs"
+msgstr "Смотреть все журналы"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Смотреть метрики и историю"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Смотреть пропускную способность по дискам"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Смотреть веб-сайт проекта"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Просмотреть отчет"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Смотреть журнал обновлений"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "Для информации о памяти необходимы права администратора."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Перейти к межсетевому экрану"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Группа томов"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "Removing physical volume from $target"
+msgid "Volume group is missing physical volumes"
+msgstr "Удаление физического тома из $target"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Размер тома составляет $0. Содержимое занимает $1."
+
+# translation auto-copied from project evolution-data-server, version el6,
+# document evolution-data-server-2.32
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Ожидание"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Ожидание подробностей…"
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Ожидание входных данных…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"Ожидание завершения использования другими программами диспетчера пакетов…"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr ""
+"Ожидание завершения других операций управления программным обеспечением"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Ожидание пуска…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "WantedBy="
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Wants="
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Предупреждающие сообщения и выше"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+#, fuzzy
+#| msgid "Set weak password"
+msgid "Weak password"
+msgstr "Задать слабый пароль"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Веб-консоль"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Веб-консоль для серверов Linux"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Веб-консоль будет перезапущена"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Веб-консоль работает в режиме ограниченного доступа."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Логотип веб-консоли"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "По средам"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Еженедельно"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "нед"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Когда"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr ""
+"При отсутствии значения соединение будет осуществляться от текущего "
+"пользователя"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"После перезагрузки веб-консоли перестанет отображатьсяиндикатор выполнения. "
+"Однако процесс обновления будет продолжаться в фоне. Возобновить просмотр "
+"процесса обновления можно будет после повторного подключения."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Белый"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:124
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "World wide name"
+msgid "World wide name"
+msgstr "WWN-имя"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Запись"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "В основном запись"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Запись"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Неверное имя пользователя или пароль"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Ежегодно"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Да"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Подключение к $0 выполняется в первый раз."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "У вас нет прав на изменение брандмауэра."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Отсутствует разрешение на просмотр авторизованных открытых ключей для этой "
+"учётной записи."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Вы вышли из системы из-за неактивности."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Вы можете попробовать загрузить более старые записи."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr "Может потребоваться смена пароля ключа для автоматического входа."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Для изменения пароля необходимо подождать"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr "Для просмотра истории метрик необходимо повторно войти в систему"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Получены права администратора."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Вы выйдете из системы через $0 секунд."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Текущая учётная запись"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"В данном браузере отсутствует возможность вставки с помощью контекстного "
+"меню. Можно использовать комбинацию Shift+Insert."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "Браузер запоминает уровень доступа от сеанса к сеансу."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Ваш сервер скоро закроет соединение. После его перезапуска вы сможете "
+"подключиться повторно."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Сеанс завершён."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Срок действия сеанса истёк. Войдите в систему снова."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Зона"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[двоичные данные]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[нет данных]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "доступ"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "активно"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "дополнение"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "дополнение"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+#, fuzzy
+#| msgid "Isolated network"
+msgid "after network"
+msgstr "Изолированная сеть"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "программы"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "тег актива"
+
+# translation auto-copied from project Customer Portal Translations, version
+# PortalCaseManagementStrings, document PortalCaseManagementStrings, author
+# ypoyarko
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "в"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "среднее: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "устройство резервного копирования для устройства VDO"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "привязка"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "загрузка"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "мост"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "Другие устройства"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "Другие устройства"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs subvolume"
+msgstr "Том хранилища"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Тома хранилища"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "Том хранилища"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "исправление ошибки"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "байтов"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "Кэш"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "Изменить"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman не установлен"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "команда"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "консоль"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "дамп ядра"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "процессор"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "сбой"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "$0 данные"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "дата"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Отключить"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "отладка"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "удалить"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "отключить"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "диск"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "диски"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "домен"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "привод"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "редактировать"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "включить"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted"
+msgid "encrypted"
+msgstr "Зашифрованный"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "шифрование"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "улучшение"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "ошибка"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "каждую пятницу"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "каждый понедельник"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "каждую субботу"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "каждое воскресенье"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "каждый четверг"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "каждый вторник"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "каждую среду"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "ежедневно"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "расширение"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "не удалось отобразить список SSH-ключей узла: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "файловая система"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "Межсетевой экран"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "для текущего и будущего ядра"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "только для текущего ядра"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "форматировать"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "c $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "с $0 на $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "графики"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "расширить"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "оборудование"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "история"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "узел"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "Цель iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "Цели iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "Добавить портал iSCSI"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+#, fuzzy
+#| msgid "On failure"
+msgid "ignore failure"
+msgstr "OnFailure="
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "в большинстве браузеров"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "несогласованный"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "инициализировать"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "установить"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "интерфейс"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "недопустимо: определены несколько целей"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "журнал"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "Man-страница journalctl"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "Состояние kdump"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "ключи"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "вход"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "компьютер"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "маска"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "максимальное: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "member of RAID device"
+msgid "member of MDRAID device"
+msgstr "участник RAID-устройства"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "участник пула Stratis"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "память"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "метрики"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "защита"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "дополнительные сведения"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "монтировать"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "сеть"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+#, fuzzy
+#| msgid "never mounted at boot"
+msgid "never mount at boot"
+msgstr "никогда не монтируется при загрузке"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "пустое значение экспорта nfs"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "пустое значение сервера nfs"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "сервер nfs не является допустимым сервером IPv6"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "приоритетность"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "нет"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "из $0 процессора"
+msgstr[1] "из $0 процессоров"
+msgstr[2] "из $0 процессоров"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "на $1"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "операционная система"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "ОС"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "пакет"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "раздел"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "пароль"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "качество пароля"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "исправления"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "путь"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "производительность"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "физический том группы томов LVM2"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "подключаемый модуль"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "сбой службы pmlogger.service"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service не может собрать данные"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service не работает"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "контейнер"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "порт"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "питание"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "RAID"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ОЗУ"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "только для чтения"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "рекомендуется"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "удалить из LVM2"
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "remove from RAID"
+msgid "remove from MDRAID"
+msgstr "удалить из RAID"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "удалить из LVM2"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "перезапустить"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "роли"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "выполняется $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "безопасность"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "последовательный"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "служба"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "оболочка"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "показать меньше"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "показать больше"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "сжать"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "закрывать"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "socket"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "решение"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "детали решения"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+#, fuzzy
+#| msgid "Reporting failed"
+msgid "sos report failed"
+msgstr "Сообщение не удалось"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ключ SSH не является путём"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "SSH-сервер пуст"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "остановить"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+#, fuzzy
+#| msgid "On failure"
+msgid "stop boot on failure"
+msgstr "OnFailure="
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+#, fuzzy
+#| msgid "Stopped"
+msgid "stopped"
+msgstr "Остановлен"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "система"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "цель"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "команда"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "время"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "таймер"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "блок"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "неизвестно"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "неизвестная цель"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "снять маску"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "размонтировать"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "неразмеченная область диска на $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "пользователь"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "имя пользователя"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#, fuzzy
+msgid "using key description $0"
+msgstr "Описание"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "версия"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "том"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "предупреждение"
+
+#: pkg/networkmanager/wireguard.jsx:84
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "wireguard-tools package is not installed"
+msgstr "PackageKit не установлен"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "да"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "зона"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Служба Kdump не установлена. Убедитесь, что установлен пакет kexec-tools."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Зарезервированная память отсутствует. Добавьте параметр crashkernel к "
+#~ "командной строке ядра (например, по адресу /etc/default/grub), чтобы "
+#~ "зарезервировать память во время загрузки. Пример: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Служба запущена"
+
+#~ msgid "Service is starting"
+#~ msgstr "Служба запускается"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Служба остановлена"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Служба останавливается"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr ""
+#~ "Конфигурация kdump будет проверена путём осуществления умышленного сбоя в "
+#~ "ядре."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "crashkernel не настроен в командной строке ядра"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (зашифровано)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 буфер Stratis"
+
+#~ msgid "$0 cache"
+#~ msgstr "Кэш $0"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 неизвестного уровня"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, свободно $1"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr ""
+#~ "Перед тем, как этот диск можно будет удалить, необходимо добавить "
+#~ "запасной диск."
+
+#~ msgid "Backing device"
+#~ msgstr "Резервное устройство"
+
+#~ msgid "Block"
+#~ msgstr "Блок"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Ёмкость"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgid "Content"
+#~ msgstr "Содержимое"
+
+#~ msgid "Create devices"
+#~ msgstr "Создать устройства"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Устройство"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Файл устройства"
+
+#~ msgid "Devices"
+#~ msgstr "Устройства"
+
+#~ msgid "Drives"
+#~ msgstr "Диски"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Зашифрованные тома необходимо разблокировать, прежде чем станет возможным "
+#~ "изменение их размера."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Файловая система (с шифрованием)"
+
+#~ msgid "Filesystems"
+#~ msgstr "Файловые системы"
+
+#~ msgid "Free"
+#~ msgstr "Свободно"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Освободите место в этой группе: сожмите или удалите другие логические "
+#~ "тома или добавьте новый физический том."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Неактивный том"
+
+#, fuzzy
+#~| msgid "Keyserver"
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Сервер криптографических ключей"
+
+#~ msgid "LVM2 member"
+#~ msgstr "Участник LVM2"
+
+#~ msgid "Locked devices"
+#~ msgstr "Заблокированные устройства"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Заблокированные зашифрованные данные"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Модель"
+
+#~ msgid "NFS mounts"
+#~ msgstr "Подключения по NFS"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "Поддержка NFS не установлена"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Подключения по NFS отсутствуют"
+
+#~ msgid "No devices"
+#~ msgstr "Нет устройств"
+
+#~ msgid "No drives attached"
+#~ msgstr "Нет присоединённых дисков"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Нет установленных целей iSCSI"
+
+#, fuzzy
+#~| msgid "Block device for filesystems"
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Устройство блочного ввода-вывода для файловых систем"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Другие данные"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Размеченное блочное устройство"
+
+# translation auto-copied from project Anaconda, version master, document
+# anaconda, author Igor Gorbounov
+#, fuzzy
+#~| msgid "Passphrase"
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Парольная фраза"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "Размер физических томов невозможно изменить здесь."
+
+#~ msgid "Pool"
+#~ msgstr "Пул"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Пул для тонких томов"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "Уровень RAID"
+
+#~ msgid "RAID member"
+#~ msgstr "Участник RAID-массива"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Съёмный носитель"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Показать $0 устройство"
+#~ msgstr[1] "Показать $0 устройства"
+#~ msgstr[2] "Показать $0 устройств"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Показать $0 диск"
+#~ msgstr[1] "Показать $0 диска"
+#~ msgstr[2] "Показать $0 дисков"
+
+#~ msgid "Show all"
+#~ msgstr "Показать все"
+
+#~ msgid "Source"
+#~ msgstr "Источник"
+
+#, fuzzy
+#~| msgid "Stratis pool"
+#~ msgid "Start pool"
+#~ msgstr "Пул Stratis"
+
+#, fuzzy
+#~| msgid "Unlock pool to see filesystems."
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Разблокировать пул для просмотра файловых систем."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Состояние"
+
+#, fuzzy
+#~| msgid "Create Stratis pool"
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Создать пул Stratis"
+
+#~ msgid "Stratis member"
+#~ msgstr "Участник Stratis"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Пул Stratis $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "Поддержка установлена."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "RAID-устройство должно быть запущено для добавления запасных дисков."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "Последний диск RAID-устройства не может быть удалён."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "Последний физический том группы томов не может быть удалён."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Для удаления этого физического тома в других местах недостаточно "
+#~ "свободного места. Требуется по крайней мере на $0 свободного места больше."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "Этот диск не может быть удалён во время восстановления устройства."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr ""
+#~ "Этот том необходимо активировать, прежде чем можно будет изменить его "
+#~ "размер."
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Нераспознанные данные"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Использование"
+
+#~ msgid "Used for"
+#~ msgstr "Назначение"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "Резервирование VDO"
+
+#~ msgid "VDO device"
+#~ msgstr "Устройство VDO"
+
+#~ msgid "Volume"
+#~ msgstr "Том"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Автоматически (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Принять ключ и подключиться"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Размер зашифрованных томов невозможно изменить здесь."
+
+#, fuzzy
+#~| msgid "Tang keyserver"
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Сервер криптографических ключей Tang"
+
+#, fuzzy
+#~| msgid "New passphrase"
+#~ msgid "Use a passphrase"
+#~ msgstr "Новая парольная фраза"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Битовая карта"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Здесь этот пул не может быть разблокирован, так как описание ключа не "
+#~ "соответствуют ожидаемому формату."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Переключить битовую карту"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Убедитесь в том, что хэш ключа с сервера Tang соответствует одному из "
+#~ "следующих значений:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Проверить вручную через SSH: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Номер и тип порта не совпадают"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Заблокированный зашифрованный пул Stratis"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Ошибка при удалении оповещения: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Не удалось получить оповещение: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 байтов двоичных данных]"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager, author ypoyarko
+#~ msgid "Disk I/O spike"
+#~ msgstr "Пик ввода-вывода диска"
+
+#~ msgid "Event"
+#~ msgstr "Событие"
+
+#~ msgid "Event logs"
+#~ msgstr "Журналы событий"
+
+#~ msgid "Load spike"
+#~ msgstr "Пик нагрузки"
+
+#~ msgid "Memory spike"
+#~ msgstr "Пик использования памяти"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Пик сетевого ввода/вывода"
+
+#~ msgid "ssh key"
+#~ msgstr "ключ SSH"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "При включении этого параметра монтирование файловой системы в процессе "
+#~ "следующей загрузки производиться не будет, даже если ранее оно уже "
+#~ "осуществлялось. Это удобно, если выполнить монтирование при загрузке "
+#~ "системы не представляется возможным. Например, когда необходимости ввести "
+#~ "парольную фразу для разблокировки файловой системы, а загрузка "
+#~ "осуществляется в автоматическом режиме."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Никогда не монтировать при загрузке"
+
+#~ msgid "Listing unit files"
+#~ msgstr "Вывод списка юнит-файлов"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "Не удалось вывести список юнит-файлов: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "Юнит не найден"
+
+#~ msgid "Validating key"
+#~ msgstr "Проверка ключа"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "Удаление группы $0"
+
+#~ msgid "Container administrator"
+#~ msgstr "Администратор контейнера"
+
+#~ msgid "Image builder"
+#~ msgstr "Image builder"
+
+#~ msgid "Roles"
+#~ msgstr "Роли"
+
+#~ msgid "Server administrator"
+#~ msgstr "Администратор сервера"
+
+#, fuzzy
+#~| msgid "Copy to clipboard"
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Копировать в буфер обмена"
+
+#, fuzzy
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Диагностические отчёты"
+
+#, fuzzy
+#~ msgid "Kernel Dump"
+#~ msgstr "Дамп ядра"
+
+#, fuzzy
+#~ msgid "Software Updates"
+#~ msgstr "Обновления программного обеспечения"
+
+#~ msgid "Update log"
+#~ msgstr "Журнал обновлений"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "Формат цели дампа NFS отличается от вида сервер:путь"
+
+#~ msgid "$0 Zone"
+#~ msgstr "Зона $0"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Создание диагностического отчёта"
+
+#~ msgid "Done!"
+#~ msgstr "Завершено."
+
+#~ msgid "Download report"
+#~ msgstr "Загрузить отчёт"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Архив не создан."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Подтвердите удаление $0"
+
+#, fuzzy
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "Созданный архив содержит данные, считающиеся конфиденциальными, и его "
+#~ "содержимое должно быть проверено создавшей его организацией, прежде чем "
+#~ "передавать его третьей стороне. "
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Этот инструмент соберёт информацию о конфигурации системы и "
+#~ "диагностическую информацию для использования при диагностике проблем с "
+#~ "системой."
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Этот пакет несовместим с текущей версией Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Для этого пакета необходим Cockpit версии %s или более новой"
+
+#, fuzzy
+#~| msgid "Performance profile"
+#~ msgid "Performance Metrics"
+#~ msgstr "Профиль производительности"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "только для чтения"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "Всего $0 ГиБ"
+
+#~ msgid "Apply"
+#~ msgstr "Применить"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Нет прав для удаления зоны $0"
+
+#~ msgid "Access"
+#~ msgstr "Доступ"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "СБОЙ В РАБОТЕ ДИСКА"
+
+#, fuzzy
+#~| msgid "Logical size"
+#~ msgid "Logical Size"
+#~ msgstr "Логический размер"
+
+#, fuzzy
+#~| msgid "Security updates available"
+#~ msgid "Security updates "
+#~ msgstr "Доступны обновления безопасности"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#, fuzzy
+#~| msgid "Update"
+#~ msgid "Updates "
+#~ msgstr "Обновить"
+
+#~ msgid "(Optional)"
+#~ msgstr "(необязательно)"
+
+#~ msgid "Active since"
+#~ msgstr "Активно с"
+
+#, fuzzy
+#~| msgid "Current allocation"
+#~ msgid "Affected locations"
+#~ msgstr "Текущее распределение"
+
+#~ msgid "Process"
+#~ msgstr "Процесс"
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Remove and delete"
+#~ msgstr "Остановить и удалить"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and format"
+#~ msgstr "Формат учётной записи"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and grow"
+#~ msgstr "Формат учётной записи"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and initialize"
+#~ msgstr "Формат учётной записи"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and shrink"
+#~ msgstr "Формат учётной записи"
+
+#, fuzzy
+#~| msgid "Remove device"
+#~ msgid "Remove and stop device"
+#~ msgstr "Удалить устройство"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Файловая система используется сеансами входа в систему и системными "
+#~ "службами. При продолжении они будут остановлены."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Файловая система используется сеансами входа в систему. При продолжении "
+#~ "они будут остановлены."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Файловая система используется системными службами. При продолжении они "
+#~ "будут остановлены."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "На этом устройстве имеются файловые системы, используемые в данный "
+#~ "момент. При продолжении все файловые системы на нём будут отключены."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "В данный момент это устройство используется для групп томов."
+
+#, fuzzy
+#~| msgid ""
+#~| "This device is currently used for volume groups. Proceeding will remove "
+#~| "it from its volume groups."
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "В данный момент это устройство используется для групп томов. При "
+#~ "продолжении оно будет удалено из групп томов."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "В данный момент это устройство используется для RAID-устройств."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "В данный момент это устройство используется для RAID-устройств. При "
+#~ "продолжении оно будет удалено с RAID-устройств."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "В данный момент это устройство используется для групп томов."
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Unmount and delete"
+#~ msgstr "Остановить и удалить"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and format"
+#~ msgstr "Размонтировать сейчас"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and grow"
+#~ msgstr "Размонтировать сейчас"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and initialize"
+#~ msgstr "Размонтировать сейчас"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and shrink"
+#~ msgstr "Размонтировать сейчас"
+
+#, fuzzy
+#~| msgid "On a mounted device"
+#~ msgid "Unmount and stop device"
+#~ msgstr "На подключенном устройстве"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 из $1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "Заблокировано"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Физический том группы $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Член RAID-устройства $0"
+
+#~ msgid "VDO backing"
+#~ msgstr "Резервирование VDO"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Создать пул носителей"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "Параметры подключения"
+
+#, fuzzy
+#~| msgid "Reset Storage Pool"
+#~ msgid "Stratis Pool"
+#~ msgstr "Сброс пула носителей"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Требуется диск."
+
+#~ msgid "Disk"
+#~ msgstr "Диск"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Только для устаревших приложений. Снижает производительность."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Установить поддержку VDO"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Использовать эмуляцию 512-байтных секторов"
+
+#~ msgid "System services"
+#~ msgstr "Системные службы"
+
+#, fuzzy
+#~| msgid "Create diagnostic report"
+#~ msgid "Create diagnostic report "
+#~ msgstr "Создание диагностического отчёта"
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr ""
+#~ "Одновременное подключение к количеству компьютеров, превышающему "
+#~ "{{ limit }}, не поддерживается."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "SSO на основе Kerberos"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Выполнить вход в {{host}}"
+
+#, fuzzy
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Пароли не совпадают"
+
+#, fuzzy
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr ""
+#~ "Не удалось установить связь между Cockpit и {{#strong}}{{host}}{{/"
+#~ "strong}}."
+
+#, fuzzy
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Не удалось выполнить вход в {{#strong}}{{host}}{{/strong}} с помощью "
+#~ "Cockpit. {{#can_sync}}Вы можете попытаться {{#sync_link}}синхронизировать "
+#~ "пользователей{{/sync_link}}.{{/can_sync}} Для получения дополнительных "
+#~ "параметров проверки подлинности и устранения неполадок обновите cockpit-"
+#~ "ws до новой версии."
+
+#, fuzzy
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Не удалось выполнить вход в {{#strong}}{{host}}{{/strong}} с помощью "
+#~ "Cockpit. Для использования Cockpit на этом компьютере вам необходимо "
+#~ "включить один из следующих методов проверки подлинности в конфигурации "
+#~ "SSHD на {{#strong}}{{host}}{{/strong}}:"
+
+#, fuzzy
+#~| msgid "Force change"
+#~ msgid "{{host}} key changed"
+#~ msgstr "Принудительно изменить"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Очистить конфигурацию точки монтирования"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Создание этого объединения приведёт к разрыву соединения с сервером и "
+#~ "недоступности интерфейса администратора."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Создание этого моста приведёт к разрыву соединения с сервером и "
+#~ "недоступности интерфейса администратора."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Создание этого сопряжения приведёт к разрыву соединения с сервером и "
+#~ "недоступности интерфейса администратора."
+
+#, fuzzy
+#~| msgid "IPv4 settings"
+#~ msgid "IP settings"
+#~ msgstr "Параметры IPv4"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Отключение <b>$0</b> приведёт к разрыву соединения с сервером и "
+#~ "недоступности интерфейса администратора."
+
+#~ msgid "Administrator password"
+#~ msgstr "Пароль администратора"
+
+#~ msgid "Computer OU"
+#~ msgstr "Подразделение"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "Удаление RAID-устройства приведёт к удалению всех данных на нём."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "Удаление группы томов приведёт к удалению всех данных в них."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Не заменять существующие данные"
+
+#~ msgid "Erase"
+#~ msgstr "Удаление"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Форматирование диска $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr ""
+#~ "При форматировании запоминающего устройства все данные на нём будут "
+#~ "удалены."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Имя узла не должно изменяться в домене"
+
+#~ msgid "More"
+#~ msgstr "Подробнее"
+
+#, fuzzy
+#~ msgid "Not joined"
+#~ msgstr "Не подключено"
+
+#~ msgid "One time password"
+#~ msgstr "Одноразовый пароль"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 из $1 на $2"
+
+#, fuzzy
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Количество часов должно лежать в интервале от 0 до 23"
+
+#~ msgid "Last login:"
+#~ msgstr "Последний вход:"
+
+#, fuzzy
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Количество минут должно лежать в интервале от 0 до 59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] "Была $0 неудачная попытка с последнего успешного входа."
+#~ msgstr[1] "Было $0 неудачных попытки с последнего успешного входа."
+#~ msgstr[2] "Было $0 неудачных попыток с последнего успешного входа."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "например, «$0»"
+
+#~ msgid "$0 active zones"
+#~ msgstr "Активные зоны: $0"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 событие"
+#~ msgstr[1] "$0 события"
+#~ msgstr[2] "$0 событий"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Лицензия:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Разблокировать при загрузке"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Разблокировать только для чтения"
+
+#, fuzzy
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Не удается найти журналы на основе текущего сочетания фильтров."
+
+#, fuzzy
+#~ msgid "Show filters"
+#~ msgstr "Показать отпечатки"
+
+#, fuzzy
+#~ msgid "e.g."
+#~ msgstr "например, «$0»"
+
+#, fuzzy
+#~ msgid "Toggle session settings"
+#~ msgstr "Изменить параметры"
+
+#~ msgid "(none)"
+#~ msgstr "(нет)"
+
+#~ msgid "Create timers"
+#~ msgstr "Создание таймера"
+
+#~ msgid "Recommended default"
+#~ msgstr "Рекомендуется по умолчанию"
+
+#~ msgid "Run"
+#~ msgstr "Запуск"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select unit state"
+#~ msgstr "Тип консоли"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Включить сохраненные метрики"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Включить сохраненные метрики"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Принудительно удалить парольную фразу в $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr ""
+#~ "Если команда tang-show-keys недоступна, выполните следующую команду:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Что делать, если команда tang-show-keys недоступна?"
+
+#~ msgid "key slot $0"
+#~ msgstr "слот для ключа $0"
+
+#, fuzzy
+#~| msgid "Automatic updates"
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Автоматическое обновление"
+
+#, fuzzy
+#~| msgid "IP configuration"
+#~ msgid "$0 CPU configuration"
+#~ msgstr "Настройка IP"
+
+#, fuzzy
+#~| msgid "$0 network"
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 сеть"
+#~ msgstr[1] "$0 сеть"
+#~ msgstr[2] "$0 сеть"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "Средство $0 доступно для большинства операционных систем. Чтобы "
+#~ "установить его, выполните поиск среди программного обеспечения GNOME или "
+#~ "запустите следующую команду:"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 сеть"
+
+# ctx::sourcefile::/rhn/systems/details/virtualization/VirtualGuestsList.do
+#, fuzzy
+#~| msgid "vCPUs"
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "Виртуальные ЦП"
+#~ msgstr[1] "Виртуальные ЦП"
+#~ msgstr[2] "Виртуальные ЦП"
+
+#, fuzzy
+#~ msgid "$0 vCPU details"
+#~ msgstr "Подробности проблемы"
+
+#, fuzzy
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "Добавление виртуального сетевого интерфейса"
+
+#~ msgid "Add network interface"
+#~ msgstr "Добавить сетевой интерфейс"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Добавление виртуального сетевого интерфейса"
+
+#~ msgid "Additional"
+#~ msgstr "Дополнительно"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Адрес должен находиться в диапазоне адресов подсети"
+
+#~ msgid "Always attach"
+#~ msgstr "Всегда присоединять"
+
+#, fuzzy
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Присоединение этого диска сделает его общим для всех использующих его "
+#~ "виртуальных машин."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Автоматически запускать libvirt при загрузке"
+
+#~ msgid "Autostart"
+#~ msgstr "Автозапуск"
+
+#~ msgid "Boot order"
+#~ msgstr "Порядок загрузки"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Не удалось сохранить параметры порядка загрузки"
+
+#~ msgid "Bus"
+#~ msgstr "Шина"
+
+#, fuzzy
+#~| msgid "VCPU settings could not be saved"
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "Не удалось сохранить параметры виртуальных ЦП"
+
+#~ msgid "CPU type"
+#~ msgstr "Тип процессора"
+
+#~ msgid "Change firmware"
+#~ msgstr "Изменить микропрограмму"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Изменения вступят в силу после выключения виртуальной машины"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Выберите операционную систему"
+
+#, fuzzy
+#~| msgid ""
+#~| "Clicking \"Launch Remote Viewer\" will download a .vv file and launch $0."
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "После нажатия кнопки «Запустить средство удалённого просмотра» будет "
+#~ "загружен файл .vv и выполнен запуск $0."
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Не удаётся загрузить образ"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Подтвердите это действие"
+
+#~ msgid "Connect"
+#~ msgstr "Подключиться"
+
+#, fuzzy
+#~| msgid "Connect with any $0 viewer application."
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "Подключиться с помощью любого $0-клиента."
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Подключение к службе виртуализации"
+
+# ctx::sourcefile::Navigation Menu
+#~ msgid "Connection"
+#~ msgstr "Подключение"
+
+#, fuzzy
+#~| msgid "Consoles"
+#~ msgid "Console"
+#~ msgstr "Консоли"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Количество ядер на сокет"
+
+#, fuzzy
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Не удалось сбросить пул носителей"
+
+#, fuzzy
+#~| msgid "crashed"
+#~ msgid "Crashed"
+#~ msgstr "аварийно завершила работу"
+
+#~ msgid "Create VM"
+#~ msgstr "Создать ВМ"
+
+#, fuzzy
+#~| msgid "Create partition on $0"
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Создание раздела на $0"
+
+#~ msgid "Create new"
+#~ msgstr "Создать новый"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Создание новой виртуальной машины"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Создать виртуальную сеть"
+
+#, fuzzy
+#~ msgid "Creating VM"
+#~ msgstr "Создать ВМ"
+
+#, fuzzy
+#~ msgid "Creating VM installation"
+#~ msgstr "Запустить автоматическую установку"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Ошибка создания виртуальной машины $0"
+
+#, fuzzy
+#~ msgid "Creation time"
+#~ msgstr "Создать таймер"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Текущее распределение"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Специализированная микропрограмма: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "Диапазон адресов DHCP"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Удаление связанных файлов хранилища:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Удаление пула носителей $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Удалить тома в этом пуле"
+
+#, fuzzy
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Удаление неактивного пула носителей только отменит определение пула. Его "
+#~ "содержимое не будет удалено."
+
+#, fuzzy
+#~| msgid "Desktop"
+#~ msgid "Desktop viewer"
+#~ msgstr "Настольный компьютер"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Перед попыткой удаления отсоедините диски, использующие этот пул, от всех "
+#~ "виртуальных машин."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Отключено от последовательной консоли. Нажмите кнопку «Подключить»."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Не удалось отсоединить диск $0 от виртуальной машины $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Не удалось присоединить диск"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Не удалось создать диск"
+
+#, fuzzy
+#~| msgid "Device file"
+#~ msgid "Disk image file"
+#~ msgstr "Файл устройства"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Не удалось сохранить параметры диска"
+
+#~ msgid "Download an OS"
+#~ msgstr "Загрузить ОС"
+
+#, fuzzy
+#~| msgid "dying"
+#~ msgid "Dying"
+#~ msgstr "умирающий"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Эмулированный компьютер"
+
+#~ msgid "End"
+#~ msgstr "Конец"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Конец не должен быть пустым"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Существующий образ диска в файловой системе узла"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Не удалось изменить микропрограмму"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "Не удалось получить IP-адреса интерфейсов, присутствующих в $0"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "Не удалось послать сочетание клавиш Ctrl+Alt+$0 в ВМ $1"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr ""
+#~ "Количество включённых виртуальных ЦП должно быть меньше их максимального "
+#~ "числа."
+
+#~ msgid "File"
+#~ msgstr "Файл"
+
+#, fuzzy
+#~ msgid "Filter by name"
+#~ msgstr "Фильтровать по имени или описанию..."
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgid "Firmware"
+#~ msgstr "Микропрограмма"
+
+#~ msgid "Force shut down"
+#~ msgstr "Принудительное завершение работы"
+
+#~ msgid "Forward mode"
+#~ msgstr "Режим пересылки"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Режим переадресации"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Создать автоматически"
+
+#~ msgid "GiB"
+#~ msgstr "ГиБ"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Скрыть дополнительные параметры"
+
+#~ msgid "Host device"
+#~ msgstr "Главное устройство"
+
+#~ msgid "Host name"
+#~ msgstr "Имя узла"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Имя узла не должно быть пустым"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Сведения о гипервизоре"
+
+#~ msgid "IP configuration"
+#~ msgstr "Настройка IP"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 и IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "Сеть IPv4"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "Сеть IPv4 не должна быть пустой"
+
+#~ msgid "IPv4 only"
+#~ msgstr "Только IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6-адрес"
+
+#~ msgid "IPv6 network"
+#~ msgstr "Сеть IPv6"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "Сеть IPv6 не должна быть пустой"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Только IPv6"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Запустить ВМ сразу после создания"
+
+#~ msgid "Import"
+#~ msgstr "Импортировать"
+
+#~ msgid "Import VM"
+#~ msgstr "Импортировать ВМ"
+
+#, fuzzy
+#~ msgid "Import a virtual machine"
+#~ msgstr "Импортировать виртуальную машину"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "В большинстве конфигураций macvtap не работает при взаимодействии узла и "
+#~ "гостевой сети"
+
+#~ msgid "Initiator"
+#~ msgstr "Инициатор"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "IQN инициатора не должно быть пустым"
+
+#, fuzzy
+#~| msgid "Installation source"
+#~ msgid "Installation source"
+#~ msgstr "Источник установки"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Источник установки не должен быть пустым"
+
+#~ msgid "Installation type"
+#~ msgstr "Тип установки"
+
+#~ msgid "Interface type"
+#~ msgstr "Тип интерфейса"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Недопустимая маска или длина префикса адреса IPv4"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Недопустимый IPv6-адрес"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Недопустимый префикс адреса IPv6"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Недопустимое имя файла"
+
+#, fuzzy
+#~| msgid "Launch Remote Viewer"
+#~ msgid "Launch remote viewer"
+#~ msgstr "Запустить средство удалённого просмотра"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr ""
+#~ "Оставьте поле пароля пустым, если не хотите создавать учетную запись root"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "Оставьте поле пароля пустым, если не хотите создавать учетную запись "
+#~ "пользователя"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr ""
+#~ "Оставьте поле пароля пустым, если не хотите создавать учетную запись root"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt не удалось обнаружить какого-либо образа микропрограммы UEFI/"
+#~ "OVMF, установленного в основной системе"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt или гипервизор не поддерживают UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "Загрузка ресурсов"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "Машина должна быть выключена перед сменой типа шины"
+
+#, fuzzy
+#~| msgid "Machine must be shut off before changing bus type"
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "Машина должна быть выключена перед сменой типа шины"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Управление виртуальными машинами"
+
+#~ msgid "Manual connection"
+#~ msgstr "Подключение вручную"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Маска или длина префикса"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Маска или длина префикса не должна быть пустой"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Максимальный размер участка памяти"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "Не удалось сохранить максимальное значение для памяти"
+
+#, fuzzy
+#~| msgid ""
+#~| "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~| "between 1 and $0"
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Максимальное количество виртуальных процессоров, выделенных для гостевой "
+#~ "ОС, которое должно быть в интервале между 1 и $0"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Максимальное количество виртуальных процессоров, выделенных для гостевой "
+#~ "ОС, которое должно быть в интервале между 1 и $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Максимальный передаваемый блок данных"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Не удалось сохранить значение для памяти"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "Значение для памяти должно отличаться от нуля"
+
+#~ msgid "MiB"
+#~ msgstr "МиБ"
+
+#~ msgid "Model type"
+#~ msgstr "Тип модели"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT к $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr ""
+#~ "Не удалось изменить состояние сетевого адаптера $0 виртуальной машины $1"
+
+#, fuzzy
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Имя не должно содержать знак «$0»."
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Имя не должно быть пустым"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Имя не должно быть пустым"
+
+#~ msgid "Name: "
+#~ msgstr "Имя: "
+
+#~ msgid "Netmask"
+#~ msgstr "Маска сети"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Не удалось включить сеть $0"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Не удалось отключить сеть $0"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Сетевая загрузка (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Сетевая файловая система"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Не удалось сохранить параметры сетевого интерфейса"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "Выбор сети не поддерживает PXE."
+
+#~ msgid "Networks"
+#~ msgstr "Сети"
+
+#~ msgid "New volume name"
+#~ msgstr "Новое имя тома"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "На данном узле нет запущенных или определённых виртуальных машин"
+
+#, fuzzy
+#~| msgid "No description available"
+#~ msgid "No connection available"
+#~ msgstr "Описание отсутствует"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Отсутствуют диски, определённые для этой виртуальной машины"
+
+#~ msgid "No network devices"
+#~ msgstr "Сетевые устройства отсутствуют"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr ""
+#~ "Отсутствуют сетевые интерфейсы, определённые для этой виртуальной машины"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "На этом узле не определено ни одной сети"
+
+#~ msgid "No networks available"
+#~ msgstr "Нет доступных сетей"
+
+#, fuzzy
+#~ msgid "No parent"
+#~ msgstr "Родительский элемент"
+
+#, fuzzy
+#~| msgid "No disks defined for this VM"
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Отсутствуют диски, определённые для этой виртуальной машины"
+
+#, fuzzy
+#~| msgid "No storage"
+#~ msgid "No state"
+#~ msgstr "Нет хранилища"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "На этом узле не определён пул носителей"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Нет доступных пулов носителей"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "Для этого пула носителей не определены тома хранения"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Нет виртуальных сетей"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "Временная сеть не может быть удалена. Она перестает существовать после "
+#~ "отключения."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "Временный буфер хранения не может быть удален. Он перестает существовать "
+#~ "после отключения."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Нет (изолированная сеть)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Один или несколько выбранных томов используются доменами. Сначала "
+#~ "отсоедините диски, чтобы разрешить удаление тома."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Может редактироваться, только когда гостевая система выключена"
+
+#~ msgid "Open"
+#~ msgstr "Открытая"
+
+#~ msgid "Operating system"
+#~ msgstr "Операционная система"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "Операция выполняется"
+
+#, fuzzy
+#~ msgid "Parent snapshot"
+#~ msgstr "Создание моментального снимка"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Путь в файловой системе узла"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Путь к файлу ISO в файловой системе узла"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Путь к файлу ISO в файловой системе узла"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Путь к файлу ISO в файловой системе узла"
+
+#, fuzzy
+#~| msgid "Pause"
+#~ msgid "Paused"
+#~ msgstr "Приостановить"
+
+#~ msgid "Persistence"
+#~ msgstr "Сохраняемость"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Физические дисковое устройство"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Физическое дисковое устройство на узле"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Выберите буфер хранения"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "Выберите том"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Введите новое имя тома"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Запустите виртуальную машину, чтобы получить доступ к её консоли."
+
+#~ msgid "Plug"
+#~ msgstr "Подключить"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "Тип пула не поддерживает создание томов"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "Тома пула используются виртуальными машинами "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr ""
+#~ "Предпочтительное количество сокетов для выставления гостевой системе."
+
+#~ msgid "Prefix"
+#~ msgstr "Префикс"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "Длина префикса не должна быть пустой"
+
+#~ msgid "Product"
+#~ msgstr "Продукт"
+
+#~ msgid "Profile"
+#~ msgstr "Профиль"
+
+#~ msgid "Protocol"
+#~ msgstr "Протокол"
+
+#~ msgid "Remote URL"
+#~ msgstr "Удалённый URL-адрес"
+
+#, fuzzy
+#~| msgid "Hypervisor details"
+#~ msgid "Remote viewer details"
+#~ msgstr "Сведения о гипервизоре"
+
+#, fuzzy
+#~ msgid "Revert"
+#~ msgstr "Никогда"
+
+#, fuzzy
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Создание моментального снимка"
+
+#~ msgid "Root password"
+#~ msgstr "Пароль root"
+
+#~ msgid "Route to $0"
+#~ msgstr "Маршрутизация к $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Запустить автоматическую установку"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Выполнять при загрузке узла"
+
+#, fuzzy
+#~| msgid "SPICE TLS port:"
+#~ msgid "SPICE TLS port"
+#~ msgstr "Порт SPICE TLS:"
+
+#, fuzzy
+#~| msgid "SPICE address:"
+#~ msgid "SPICE address"
+#~ msgstr "Адрес SPICE:"
+
+#, fuzzy
+#~| msgid "SPICE port:"
+#~ msgid "SPICE port"
+#~ msgstr "Порт SPICE:"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select console type"
+#~ msgstr "Тип консоли"
+
+#~ msgid "Send key"
+#~ msgstr "Отправить клавишу"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Отправить немаскируемое прерывание"
+
+#~ msgid "Serial console"
+#~ msgstr "Последовательная консоль"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Задать диапазон DHCP"
+
+#~ msgid "Set manually"
+#~ msgstr "Задать вручную"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "Установка паролей пользователей для автоматической установки требует "
+#~ "запуска ВМ при ее создании"
+
+#~ msgid "Show additional options"
+#~ msgstr "Показать дополнительные параметры"
+
+#, fuzzy
+#~ msgid "Shut off"
+#~ msgstr "отключена"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "Выключите ВМ для редактирования конфигурации микропрограммы"
+
+#, fuzzy
+#~ msgid "Shutting down"
+#~ msgstr "Выключить"
+
+#, fuzzy
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Не удалось создать диск"
+
+#~ msgid "Source format"
+#~ msgstr "Формат источника"
+
+#~ msgid "Source path"
+#~ msgstr "Путь к источнику"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Путь к источнику не должен быть пустым"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "Путь должен начинаться с указания протокола HTTP, FTP или NFS"
+
+#~ msgid "Source volume group"
+#~ msgstr "Группа томов источника"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Запустить libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Запускать пул при загрузке узла"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Начало не должно быть пустым"
+
+#~ msgid "Startup"
+#~ msgstr "Запуск"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "Не удалось включить пул носителей $0"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "Не удалось отключить пул носителей $0"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Не удалось создать пул носителей"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Имя пула носителей"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "Размер хранилища не может быть равен нулю"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Не удалось удалить тома хранилища"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "приостановлено (PM)"
+
+#~ msgid "Target path"
+#~ msgstr "Конечный путь"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Конечный путь не должен быть пустым"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr ""
+#~ "Виртуальная машина работает и будет принудительно отключена перед "
+#~ "удалением."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "ВМ должна работать или быть отключена, чтобы отсоединить это устройство"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "Экспортируемый каталог на сервере"
+
+#~ msgid "The pool is empty"
+#~ msgstr "Пул пуст"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr ""
+#~ "Выбранная операционная система не поддерживает автоматической установки"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr "Минимальный объём памяти для выбранной операционной системы — $0 $1"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "Минимальный размер хранилища для выбранной операционной системы — $0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "Не удалось удалить пул носителей"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr "Эта ВМ временная. Выключите ее, если хотите ее удалить."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Этот том уже используется: "
+
+#~ msgid "Threads per core"
+#~ msgstr "Потоков на ядро"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr ""
+#~ "Временные ВМ не поддерживают редактирование конфигурации микропрограмм"
+
+#~ msgid "Type ID"
+#~ msgstr "Идентификатор типа"
+
+#~ msgid "Unique name"
+#~ msgstr "Уникальное имя"
+
+#~ msgid "Unique network name"
+#~ msgstr "Уникальное сетевое имя"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Неизвестная микропрограмма"
+
+#~ msgid "Unplug"
+#~ msgstr "Отключить"
+
+#~ msgid "Url"
+#~ msgstr "URL-адрес"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "Не удалось сохранить параметры виртуальных ЦП"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "ВМ $0 уже существует"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "Не удалось принудительно перезапустить виртуальную машину $0"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "Не удалось принудительно завершить работу виртуальной машины $0"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "Не удалось удалить виртуальную машину $0"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "Не удалось установить виртуальную машину $0"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "Не удалось перезапустить виртуальную машину $0"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "Не удалось возобновить виртуальную машину $0"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "Ошибка отправки немаскируемого прерывания виртуальной машиной $0"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "Не удалось завершить работу виртуальной машины $0"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "Не удалось запустить виртуальную машину $0"
+
+#, fuzzy
+#~ msgid "VM state"
+#~ msgstr "Состояние"
+
+#, fuzzy
+#~| msgid "VNC TLS port:"
+#~ msgid "VNC TLS port"
+#~ msgstr "Порт VNC TLS:"
+
+#, fuzzy
+#~| msgid "VNC address:"
+#~ msgid "VNC address"
+#~ msgstr "Адрес VNC:"
+
+#, fuzzy
+#~| msgid "console"
+#~ msgid "VNC console"
+#~ msgstr "консоль"
+
+#, fuzzy
+#~| msgid "VNC port:"
+#~ msgid "VNC port"
+#~ msgstr "Порт VNC:"
+
+#, fuzzy
+#~ msgid "Virtual Machines"
+#~ msgstr "Виртуальные машины"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Виртуальные машины"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Управление виртуальными машинами"
+
+#~ msgid "Virtual network"
+#~ msgstr "Виртуальная сеть"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Не удалось создать виртуальную сеть"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Служба виртуализации (libvirt) не активна"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Имя группы томов не должно быть пустым"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Для записи"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Для записи и совместного использования"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "Необходимо выбрать наиболее соответствующую операционную систему"
+
+#~ msgid "cdrom"
+#~ msgstr "дисковод для компакт-дисков"
+
+#~ msgid "disabled"
+#~ msgstr "отключён"
+
+#~ msgid "down"
+#~ msgstr "не работает"
+
+#~ msgid "enabled"
+#~ msgstr "включено"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "главное устройство"
+
+#, fuzzy
+#~| msgid "to host path"
+#~ msgid "host passthrough"
+#~ msgstr "к адресу узла"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "Цель iSCSI direct"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "IQN инициатора iSCSI"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "IQN цели iSCSI"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "нет"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "перенаправленное устройство"
+
+#~ msgid "server"
+#~ msgstr "сервер"
+
+#~ msgid "up"
+#~ msgstr "работает"
+
+#~ msgid "vCPU count"
+#~ msgstr "Количество виртуальных ЦП"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "Наибольшее количество виртуальных ЦП"
+
+# ctx::sourcefile::/rhn/systems/details/virtualization/VirtualGuestsList.do
+#~ msgid "vCPUs"
+#~ msgstr "Виртуальные ЦП"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#, fuzzy
+#~| msgid "Read more..."
+#~ msgid "view more..."
+#~ msgstr "Подробнее..."
+
+#, fuzzy
+#~| msgid ""
+#~| "virt-install package needs to be installed on the system in order to "
+#~| "create new VMs"
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Для создания новых виртуальных машин в системе должен быть установлен "
+#~ "пакет virt-install"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Для создания новых виртуальных машин в системе должен быть установлен "
+#~ "пакет virt-install"
+
+#, fuzzy
+#~| msgid ""
+#~| "virt-install package needs to be installed on the system in order to "
+#~| "create new VMs"
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Для создания новых виртуальных машин в системе должен быть установлен "
+#~ "пакет virt-install"
+
+#~ msgid "vm"
+#~ msgstr "ВМ"
+
+#~ msgid "Local install media"
+#~ msgstr "Локальный установочный носитель"
+
+#~ msgid "URL"
+#~ msgstr "URL-адрес"
+
+#, fuzzy
+#~ msgid "Please set a root password"
+#~ msgstr "Установите пароль root или пользователя"
+
+#~ msgid "21st"
+#~ msgstr "21-го числа"
+
+#~ msgid "22nd"
+#~ msgstr "22-го числа"
+
+#~ msgid "23rd"
+#~ msgstr "23-го числа"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "После"
+
+#~ msgid "Friday"
+#~ msgstr "Пятница"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Часы : Минуты"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "Количество часов должно лежать в интервале от 0 до 23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Недопустимый формат даты."
+
+#~ msgid "Invalid number."
+#~ msgstr "Недопустимый номер."
+
+#~ msgid "Monday"
+#~ msgstr "Понедельник"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Повторять ежечасно"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Повторять ежегодно"
+
+#~ msgid "Saturday"
+#~ msgstr "Суббота"
+
+#~ msgid "Sunday"
+#~ msgstr "Воскресенье"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Этот день существует не во всех месяцах.<br> Таймер будет выполняться "
+#~ "только в месяцы с 31 днём."
+
+#~ msgid "Thursday"
+#~ msgstr "Четверг"
+
+#~ msgid "Tuesday"
+#~ msgstr "Вторник"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 обновление"
+#~ msgstr[1] "$0 обновления"
+#~ msgstr[2] "$0 обновлений"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 исправление безопасности"
+#~ msgstr[1] "$1 исправления безопасности"
+#~ msgstr[2] "$1 исправлений безопасности"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Управление устройствами хранения"
+
+#~ msgid "Restart now"
+#~ msgstr "Перезагрузить сейчас"
+
+#~ msgid "Configure"
+#~ msgstr "Настроить"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr ""
+#~ "Сетевая загрузка доступна только при использовании подключения «System»"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Сеть"
+
+#, fuzzy
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "Нет такого файла или каталога"
+
+#~ msgid "No updates pending"
+#~ msgstr "Ожидающие обновления отсутствуют"
+
+#, fuzzy
+#~| msgid "ssh server is empty"
+#~ msgid "This directory is empty"
+#~ msgstr "SSH-сервер пуст"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr "Тип данного пула не поддерживает создание томов хранения"
+
+#~ msgid "undefined"
+#~ msgstr "не определено"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Неверный ключ узла"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "Подлинность узла {{#strong}}{{host}}{{/strong}} не может быть "
+#~ "установлена. Продолжить подключение?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "Ключ {{#strong}}{{host}}{{/strong}} не соответствует использовавшемуся "
+#~ "ранее ключу. Если этот компьютер не был недавно заменён, то вполне "
+#~ "вероятно, что кто-то пытается атаковать ваше соединение с этим "
+#~ "компьютером."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Неизвестный ключ узла"
+
+#~ msgid "Address:"
+#~ msgstr "Адрес:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Минимально необходимое количество дисков: $0."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "Подключиться с помощью любого клиента SPICE или VNC."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Загрузить MSI-файл с сайта $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Графическая консоль (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Графическая консоль в средстве просмотра для рабочего стола"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Консоль для этой виртуальной машины не определена."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid "Entry"
+#~ msgstr "Запись"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Ваш браузер отключится, но это не повлияет на процесс обновления. Вы "
+#~ "можете повторно подключиться через несколько секунд, чтобы продолжить "
+#~ "просмотр прогресса."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "и перезагружать компьютер автоматически."
+
+#~ msgid "Dashboard"
+#~ msgstr "Панель мониторинга"
+
+#, fuzzy
+#~ msgid "Description input text"
+#~ msgstr "Описание"
+
+#~ msgid "FAILED"
+#~ msgstr "СБОЙ"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Подключение прервано. Выполняется попытка повторного подключения"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Сеть"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Введите новый размер тома"
+
+#~ msgid "Servers"
+#~ msgstr "Серверы"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "В настоящий момент вы подключены непосредственно к этому серверу. "
+#~ "Невозможно его удалить."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 бит"
+#~ msgstr[1] "$0 бита"
+#~ msgstr[2] "$0 битов"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 байт"
+#~ msgstr[1] "$0 байта"
+#~ msgstr[2] "$0 байтов"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 байт"
+#~ msgstr[1] "$0 байта"
+#~ msgstr[2] "$0 байтов"
+
+#, fuzzy
+#~| msgid "Bond settings"
+#~ msgid "Bond settings "
+#~ msgstr "Параметры объединения"
+
+#, fuzzy
+#~| msgid "IP settings"
+#~ msgid "IP settings "
+#~ msgstr "Параметры IP"
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Неожиданная внутренняя ошибка Cockpit. <br/><br/>Вы можете попробовать "
+#~ "перезапустить Cockpit, нажав кнопку «Обновить» в вашем браузере. Консоль "
+#~ "javascript содержит сведения об этой ошибке (<b>Ctrl-Shift-J</b> в "
+#~ "большинстве браузеров)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "Виртуальная машина аварийно завершила работу."
+
+#~ msgid "The VM is down."
+#~ msgstr "Виртуальная машина не работает."
+
+#~ msgid "The VM is going down."
+#~ msgstr "Виртуальная машина завершает работу."
+
+#~ msgid "The VM is idle."
+#~ msgstr "Виртуальная машина бездействует."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr ""
+#~ "Виртуальная машина находится в процессе завершения работы или аварийного "
+#~ "выключения."
+
+#~ msgid "The VM is paused."
+#~ msgstr "Виртуальная машина приостановлена."
+
+#~ msgid "The VM is running."
+#~ msgstr "Виртуальная машина работает."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr ""
+#~ "Виртуальная машина приостановлена управлением питанием гостевой системы."
+
+#~ msgid "inactive"
+#~ msgstr "неактивно"
+
+#~ msgid "Avatar"
+#~ msgstr "Аватар"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Оставьте поле пустым, чтобы подключиться к этому компьютеру от имени "
+#~ "текущего пользователя. При указании другого имени пользователя, оно "
+#~ "всегда будет использоваться при подключении к данному компьютеру."
+
+#~ msgid "2 min"
+#~ msgstr "2 мин"
+
+#~ msgid "3 min"
+#~ msgstr "3 мин"
+
+#~ msgid "4 min"
+#~ msgstr "4 мин"
+
+#~ msgid "CPU graph"
+#~ msgstr "График процессора"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "Состояние процессора"
+
+#~ msgid "Cached"
+#~ msgstr "Кэшировано"
+
+#~ msgid "Graphs"
+#~ msgstr "Графики"
+
+#~ msgid "I/O wait"
+#~ msgstr "Ожидание ввода-вывода"
+
+#~ msgid "Kernel"
+#~ msgstr "Ядро"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Память"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "Использование памяти и подкачки"
+
+#~ msgid "Memory graph"
+#~ msgstr "График памяти"
+
+#~ msgid "Network traffic"
+#~ msgstr "Сетевой трафик"
+
+#~ msgid "Nice"
+#~ msgstr "Приоритет"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Синхронизация пользователей"
+
+#~ msgid "Usage graphs"
+#~ msgstr "Графики использования"
+
+#~ msgid "View graphs"
+#~ msgstr "Просмотреть графики"
+
+#~ msgid "idle"
+#~ msgstr "бездействует"
+
+#~ msgid "paused"
+#~ msgstr "приостановлена"
+
+#~ msgid "running"
+#~ msgstr "работает"
+
+#~ msgid "shut off"
+#~ msgstr "отключена"
+
+#~ msgid "shutdown"
+#~ msgstr "не работает"
+
+#, fuzzy
+#~ msgid "Add a new host"
+#~ msgstr "Добавить новую зону"
+
+#~ msgid "Available"
+#~ msgstr "Доступно"
+
+#, fuzzy
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Введите IP-адрес или имя узла"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Сетевые интерфейсы"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Данная версия cockpit-ws не поддерживает подключение к узлу с другим "
+#~ "пользователем или портом"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Чтобы использовать другой порт, необходимо обновить cockpit-ws до более "
+#~ "новой версии."
+
+#~ msgid "$0 template"
+#~ msgstr "Шаблон $0"
+
+#~ msgid "Instance of template: "
+#~ msgstr "Экземпляр шаблона: "
+
+#~ msgid "Instantiate"
+#~ msgstr "Создать экземпляр"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 shares"
+
+#~ msgid "All In One"
+#~ msgstr "Моноблок"
+
+#~ msgid "Always"
+#~ msgstr "Всегда"
+
+#~ msgid "Author"
+#~ msgstr "Автор"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Механизму passwd1 переданы неверные данные"
+
+#~ msgid "CPU priority"
+#~ msgstr "Приоритет ЦП"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Не удаётся подключиться к Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Изменить ограничения ресурса"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Изменение ограничений ресурсов"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr ""
+#~ "Не удалось выполнить вход в {{#strong}}{{host}}{{/strong}} с помощью "
+#~ "Cockpit."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Не удалось выполнить вход в {{#strong}}{{host}}{{/strong}} с помощью "
+#~ "Cockpit. Вы можете изменить учётные данные для проверки подлинности ниже. "
+#~ "{{#can_sync}}Возможно, вы предпочтёте {{#sync_link}}синхронизировать "
+#~ "учётные записи и пароли{{/sync_link}}.{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Объединённое использование памяти"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Объединённое использование $0 ядра процессора"
+#~ msgstr[1] "Объединённое использование $0 ядер процессора"
+#~ msgstr[2] "Объединённое использование $0 ядер процессора"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "Команда не может быть пустой"
+
+#~ msgid "Command:"
+#~ msgstr "Команда:"
+
+#~ msgid "Commit"
+#~ msgstr "Подтвердить"
+
+#~ msgid "Commit image"
+#~ msgstr "Сохранить образ"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Подключение к Docker"
+
+#~ msgid "Container name"
+#~ msgstr "Имя контейнера"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Контейнер помечен как незапущенный, но выполнить его обычную остановку не "
+#~ "удалось."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Контейнер сейчас запущен."
+
+#~ msgid "Container:"
+#~ msgstr "Контейнер:"
+
+#~ msgid "Containers"
+#~ msgstr "Контейнеры"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Контейнеры"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Не удалось изменить группы пользователей"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Не удалось изменить пароль пользователя"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Не удалось подключиться к компьютеру"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Не удалось создать новых пользователей"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Не удалось получить список локальных пользователей"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Не удалось получить список пользователей"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Не удалось загрузить данные пользователя"
+
+# ctx::sourcefile::/rhn/account/UserDetails
+#~ msgid "Created:"
+#~ msgstr "Дата создания:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Контейнеры Docker"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker не установлен или не активирован в системе"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Повторяющийся псевдоним"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Повторяющийся порт"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "Введите парольную фразу для добавления ключа в ssh-agent"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Ввод другого пароля здесь приведёт к тому, что вам придётся вводить его "
+#~ "заново при каждом подключении к этому компьютеру"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgid "Environment"
+#~ msgstr "Среда"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Произошла ошибка при загрузке пользователей: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Сообщение об ошибке от Docker:"
+
+#~ msgid "Everything"
+#~ msgstr "Все"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Выход с кодом $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Показывать порты контейнера"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Не удалось запустить Docker: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Не удалось остановить область Docker: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Получить новый образ"
+
+#~ msgid "IP address:"
+#~ msgstr "IP-адрес:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "Длина префикса:"
+
+# translation auto-copied from project Satellite6 Hammer CLI Foreman, version
+# 6.1, document hammer-cli-foreman
+#~ msgid "Id"
+#~ msgstr "Идентификатор"
+
+#~ msgid "Id:"
+#~ msgstr "Идентификатор:"
+
+#~ msgid "Image"
+#~ msgstr "Образ"
+
+#~ msgid "Image $0"
+#~ msgstr "Образ $0"
+
+#~ msgid "Image search"
+#~ msgstr "Поиск образов"
+
+#~ msgid "Image:"
+#~ msgstr "Образ:"
+
+#~ msgid "Images"
+#~ msgstr "Образы"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Образы"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Образы и работающие контейнеры"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Чтобы синхронизировать пользователей, вам необходимо войти в систему "
+#~ "{{#strong}}{{host}}{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Недопустимый порт"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Билет Kerberos"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Оставьте поле пустым, чтобы подключиться к этому компьютеру от имени "
+#~ "текущего пользователя{{#default_user}} ({{default_user}}){{/"
+#~ "default_user}}. При указании другого имени пользователя, оно всегда будет "
+#~ "использоваться при подключении к данному компьютеру."
+
+#~ msgid "Link to another container"
+#~ msgstr "Связать с другим контейнером"
+
+#~ msgid "Links:"
+#~ msgstr "Ссылки:"
+
+#~ msgid "Login Password"
+#~ msgstr "Пароль для входа"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC-адрес:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Предельный объём памяти"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Подключить тома контейнера"
+
+#~ msgid "No container specified"
+#~ msgstr "Контейнер не указан"
+
+#~ msgid "No containers"
+#~ msgstr "Контейнеры отсутствуют"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Отсутствуют контейнеры, соответствующие текущему фильтру"
+
+#~ msgid "No images"
+#~ msgstr "Образы отсутствуют"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Отсутствуют образы, соответствующие текущему фильтру"
+
+#~ msgid "No results for $0"
+#~ msgstr "Нет результатов для $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Отсутствуют образы, соответствующие текущему фильтру"
+
+#~ msgid "No running containers"
+#~ msgstr "Нет работающих контейнеров"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Нет работающих контейнеров, соответствующих текущему фильтру"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Право доступа к Docker в этой системе отсутствует"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "При сбое повторять попытку $0 раз"
+#~ msgstr[1] "При сбое повторять попытку $0 раза"
+#~ msgstr[2] "При сбое повторять попытку $0 раз"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Подтвердите принудительное удаление $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Попробуйте другое условие поиска"
+
+#~ msgid "Ports:"
+#~ msgstr "Порты:"
+
+#~ msgid "Problems"
+#~ msgstr "Проблемы"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Только чтение"
+
+#~ msgid "ReadWrite"
+#~ msgstr "Чтение/Запись"
+
+# translation auto-copied from project Red Hat Satellite User Guide, version
+# 6.0, document appe-Glossary_of_Terms
+#~ msgid "Repository"
+#~ msgstr "Репозиторий"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Политика перезагрузки:"
+
+#~ msgid "Retries:"
+#~ msgstr "Повторов:"
+
+#, fuzzy
+#~| msgid "Reuse my password for privileged tasks"
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Повторно использовать пароль для привилегированных задач"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Выберите пользователей, которых необходимо синхронизировать с {{#strong}}"
+#~ "{{host}}{{/strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Задать переменные среды контейнера"
+
+#~ msgid "Severity:"
+#~ msgstr "Серьёзность:"
+
+#~ msgid "Show all containers"
+#~ msgstr "Показать все контейнеры"
+
+#~ msgid "Start Docker"
+#~ msgstr "Запустить Docker"
+
+# translation auto-copied from project virt-manager, version 0.10.0, document
+# virt-manager
+#~ msgid "State:"
+#~ msgstr "Состояние:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Синхронизировать"
+
+# translation auto-copied from project shotwell-core, version 0.14.1, document
+# shotwell-core
+#~ msgid "Tag"
+#~ msgstr "Метка"
+
+# translation auto-copied from project subscription-manager, version 1.11.X,
+# document keys
+#~ msgid "Tags"
+#~ msgstr "Метки"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "Следующие контейнеры зависят от этого образа и станут непригодными для "
+#~ "использования."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Образ не существует."
+
+#~ msgid "Type a password"
+#~ msgstr "Введите пароль"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Введите условия фильтра..."
+
+#~ msgid "Unless stopped"
+#~ msgstr "Пока не будет остановлено"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Неподдерживаемый механизм установки"
+
+#~ msgid "Up since $0"
+#~ msgstr "Время начала работы: $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Используется контейнерами"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Используя доступные учётные данные"
+
+#~ msgid "Volumes"
+#~ msgstr "Тома"
+
+#~ msgid "Volumes:"
+#~ msgstr "Тома:"
+
+#~ msgid "With terminal"
+#~ msgstr "С терминалом"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Вы подключены к {{#strong}}{{host}}{{/strong}}, однако для синхронизации "
+#~ "пользователей необходимы права суперпользователя."
+
+#~ msgid "default"
+#~ msgstr "по умолчанию"
+
+#~ msgid "key"
+#~ msgstr "ключ"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "Поиск по имени, пространству имён или описанию"
+
+#~ msgid "shares"
+#~ msgstr "shares"
+
+#~ msgid "to host port"
+#~ msgstr "к порту узла"
+
+# ctx::sourcefile::NOT USED
+#~ msgid "value"
+#~ msgstr "значение"
+
+#~ msgid "Master"
+#~ msgstr "Главное"
+
+#~ msgid "Password for $0"
+#~ msgstr "Пароль для $0"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "Пользователю <b>$0</b> запрещено управлять серверами"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Учётные записи"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "Пользователю <b>$0</b> запрещено изменять учётные записи"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Невозможно удалить учётную запись суперпользователя"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Невозможно переименовать учётную запись суперпользователя"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "Нет прав на добавление новой зоны"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "Нет прав для добавления служб в зону $0"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "Нет прав для удаления службы $0"
+
+#~ msgid "Account Settings"
+#~ msgstr "Параметры учётной записи"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Добавление компьютера на информационную панель"
+
+#~ msgid "Machines"
+#~ msgstr "Компьютеры"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "Учётная запись имеет повышенные полномочия администратора"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Пароль не может быть использован для привилегированных задач или для "
+#~ "подключения к другим компьютерам"
+
+#~ msgid "Privileged"
+#~ msgstr "Привилегированный"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Повторно использовать пароль для привилегированных задач и для "
+#~ "подключения к другим компьютерам"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr "Пользователю $0 запрещено изменять имена узлов"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr "Пользователю $0 запрещено выключать или перезапускать этот сервер"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "Пользователю <b>$0</b> запрещено изменять профили"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "Пользователю <b>$0</b> запрещено управлять хранилищем"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr "Пользователю <b>$0</b> запрещено параметры сети"
+
+#, fuzzy
+#~| msgid "Required by "
+#~ msgid "Required by $0"
+#~ msgstr "Требуется для "
+
+#~ msgid "Automatic Startup"
+#~ msgstr "Автоматический запуск"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Последнее срабатывание"
+
+#~ msgid "Next Run"
+#~ msgstr "Следующий запуск"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "У пользователя <b>$0</b> нет разрешений на создание таймеров"
+
+#~ msgid "Connecting"
+#~ msgstr "Подключение"
+
+#~ msgid "About Cockpit"
+#~ msgstr "О Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (в общем доступе с ОС)"
+
+#, fuzzy
+#~| msgid "1 Vulnerability"
+#~| msgid_plural "$0 Vulnerabilities"
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 уязвимость"
+#~ msgstr[1] "$0 уязвимости"
+#~ msgstr[2] "$0 уязвимостей"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Добавление дополнительного хранилища"
+
+#~ msgid "Add Storage"
+#~ msgstr "Добавить хранилище"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Дополнительное хранилище"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Все данные на выбранных дисках будут удалены, а диски будут добавлены в "
+#~ "пул носителей."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Настроить хранилище..."
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Не удалось добавить все диски"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Удалить контейнеры и сбросить пул носителей"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr "Информация о пуле носителей Docker недоступна."
+
+#~ msgid "Local Disks"
+#~ msgstr "Локальные диски"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "Дополнительное локальное хранилище не найдено."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Переформатировать и добавить диски"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "Сброс пула носителей приведёт к удалению всех контейнеров и освобождению "
+#~ "дисков в пуле."
+
+#~ msgid "Security"
+#~ msgstr "Безопасность"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr "Пулом носителей Docker невозможно управлять в этой системе."
+
+#, fuzzy
+#~| msgid "The scan from $time ($type) found one vulnerability:"
+#~| msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "При сканировании $time ($type) была обнаружена одна уязвимость:"
+#~ msgstr[1] ""
+#~ "При сканировании $time ($type) было обнаружено $count уязвимости:"
+#~ msgstr[2] ""
+#~ "При сканировании $time ($type) было обнаружено $count уязвимостей:"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "При сканировании $time ($type) уязвимостей обнаружено не было."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "Сканирование $time ($type) не удалось."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Служба виртуализации доступна"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "У вас нет прав на управление пулом носителей Docker."
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} -> $cport"
+
+#~ msgid "Hide Performance Options"
+#~ msgstr "Скрыть параметры быстродействия"
+
+#~ msgid "Show Performance Options"
+#~ msgstr "Показать параметры быстродействия"
diff --git a/po/sk.po b/po/sk.po
new file mode 100644
index 0000000..cc87f72
--- /dev/null
+++ b/po/sk.po
@@ -0,0 +1,12369 @@
+# Matej Marusak <mmarusak@redhat.com>, 2019. #zanata, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-11-13 13:37+0000\n"
+"Last-Translator: Jose Riha <jose1711@gmail.com>\n"
+"Language-Team: Slovak <https://translate.fedoraproject.org/projects/cockpit/"
+"main/sk/>\n"
+"Language: sk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n"
+"X-Generator: Weblate 5.1.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "Počet používateľov"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 procesor"
+msgstr[1] "$0 procesory"
+msgstr[2] "$0 procesorov"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 aktívna zóna"
+msgstr[1] "$0 aktívne zóny"
+msgstr[2] "$0 aktívnych zón"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 k dispozícií"
+
+#: pkg/storaged/block/resize.jsx:266
+msgid "$0 can not be made larger"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:263
+#, fuzzy
+#| msgid "Cannot be enabled"
+msgid "$0 can not be made smaller"
+msgstr "Nie je možné povoliť"
+
+#: pkg/storaged/block/resize.jsx:259
+#, fuzzy
+#| msgid "Cannot be enabled"
+msgid "$0 can not be resized"
+msgstr "Nie je možné povoliť"
+
+#: pkg/storaged/block/resize.jsx:255
+msgid "$0 can not be resized here"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 Veľkosť bloku"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 kritický zásah"
+msgstr[1] "$0 kritické zásahy"
+msgstr[2] "$0 kritických zásahov"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 dáta + $1 réžie využitéj z $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 deň"
+msgstr[1] "$0 dni"
+msgstr[2] "$0 dní"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disk chýba"
+msgstr[1] "$0 disky chýbajú"
+msgstr[2] "$0 diskov chýba"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disk chýba"
+msgstr[1] "$0 disky chýbajú"
+msgstr[2] "$0 diskov chýba"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 Disky"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "Dokumentácia k $0"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 chyba"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 skončilo s kódom $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 sa nepodarilo"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "$0 filesystem"
+msgid "$0 filesystem"
+msgstr "$0 Súborový systém"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 dostupné"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 hodina"
+msgstr[1] "$0 hodiny"
+msgstr[2] "$0 hodín"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 dôležitý zásah"
+msgstr[1] "$0 zásahy, vrátane dôležitých"
+msgstr[2] "$0 zásahov, vrátane dôležitých"
+
+#: pkg/users/account-create-dialog.js:378
+#, fuzzy
+#| msgid "$0 disk is missing"
+#| msgid_plural "$0 disks are missing"
+msgid "$0 is an existing file"
+msgstr "$0 disk chýba"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+#, fuzzy
+#| msgid "$0 is in active use"
+msgid "$0 is in use"
+msgstr "$0 je práve používaný"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 nie je k dispozícií v žiadom repozitári."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 kľúč sa zmenil"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 nútene ukončené signálom $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 zásah s nízkou prioritou"
+msgstr[1] "$0 zásahy s nízkou prioritou"
+msgstr[2] "$0 zásahov s nízkou prioritou"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minúta"
+msgstr[1] "$0 minúty"
+msgstr[2] "$0 minút"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 zásah strednej závažnosti"
+msgstr[1] "$0 zásahy, vrátane strednej závažnosti"
+msgstr[2] "$0 zásahov, vrátane strednej závažnosti"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 mesiac"
+msgstr[1] "$0 mesiace"
+msgstr[2] "$0 mesiacov"
+
+#: pkg/users/accounts-list.js:339
+#, fuzzy
+#| msgid "view more..."
+msgid "$0 more..."
+msgstr "zobraziť viac..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 balíček"
+msgstr[1] "$0 balíčky"
+msgstr[2] "$0 balíčkov"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 stránka"
+msgstr[1] "$0 stránky"
+msgstr[2] "$0 stránok"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+#, fuzzy
+#| msgid "partition"
+msgid "$0 partitions"
+msgstr "oddiel"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "Je k dispozícií $0 oprava bezpečnosti"
+msgstr[1] "Sú k dispozícií $0 opravy bezpečnosti"
+msgstr[2] "Je k dispozícií $0 opráv bezpečnosti"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 služba zhavarovala"
+msgstr[1] "$0 služby zhavarovali"
+msgstr[2] "$0 služieb zhavarovalo"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+#, fuzzy
+#| msgid "$0 service has failed"
+#| msgid_plural "$0 services have failed"
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 služba zhavarovala"
+msgstr[1] "$0 služby zhavarovali"
+msgstr[2] "$0 služieb zhavarovalo"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 slot ostáva"
+msgstr[1] "$0 sloty ostávajú"
+msgstr[2] "$0 slotov ostáva"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+#, fuzzy
+#| msgid "Not synchronized"
+msgid "$0 synchronized"
+msgstr "Nezosynchronizované"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 celkom"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "Je dostupné $0 aktualizácia"
+msgstr[1] "Sú dostupné $0 aktualizácie"
+msgstr[2] "Je dostupných $0 aktualizácií"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 použité z $1 ($2 ušetrené)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 týždeň"
+msgstr[1] "$0 týždne"
+msgstr[2] "$0 týždňov"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 bude nainštalovaný."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 rok"
+msgstr[1] "$0 roky"
+msgstr[2] "$0 rokov"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$ zóna"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: pád v $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (z $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+msgid "(Not part of target)"
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:239
+msgid "(no assigned mount point)"
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+#, fuzzy
+#| msgid "Not mounted"
+msgid "(not mounted)"
+msgstr "Nepripojené"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(odporúčaný)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", vrátane $1 opravy zabezpečenia"
+msgstr[1] ", vrátane $1 opráv zabezpečenia"
+msgstr[2] ", vrátane $1 opráv zabezpečenia"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 deň"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 hodina"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 min"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 minúta"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 týždeň"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14."
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 min"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 minút"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 minút"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4."
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 min"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minút"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5."
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 hodín"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 minút"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9."
+
+#: pkg/shell/hosts_dialog.jsx:98
+#, fuzzy
+#| msgid ""
+#| "A compatible version of Cockpit is not installed on {{#strong}}{{host}}{{/"
+#| "strong}}."
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr ""
+"Na {{#strong}}{{host}}{{/strong}} nie je nainštalovaná kompatibilná verzia "
+"Cockpitu."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr ""
+
+#: pkg/users/group-create-dialog.js:71
+#, fuzzy
+#| msgid "This user name already exists"
+msgid "A group with this name already exists"
+msgstr "Toto užívateľské meno už existuje"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Z dôvodu zabezpečenia, spoľahlivosti a rýchlosti je potrebný moderný "
+"prehliadač."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:795
+#, fuzzy
+#| msgid ""
+#| "A new SSH key at ${key} will be created for ${luser} on ${lhost} and it "
+#| "will be added to the ${afile} file of ${ruser} on ${rhost}."
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Bude vytvorený nový SSH kľúč ${key} pre užívateľa ${luser} na ${lhost} a "
+"bude pridaný do súboru ${afile} užívateľa ${ruser} na ${rhost}."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP monitorovanie"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "O webovej konzoli"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Chýba"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Prijať kľúč a prihlásiť sa"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+#, fuzzy
+#| msgid "Set password"
+msgid "Acceptable password"
+msgstr "Nastaviť heslo"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Skončenie platnosti účtu"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Účet nie je k dispozícií alebo ho nie je možné zmeniť."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Účty"
+
+#: pkg/storaged/dialog.jsx:1240
+#, fuzzy
+#| msgid "Actions"
+msgid "Action"
+msgstr "Akcie"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Akcie"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Aktivovať"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Aktivuje sa $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Aktívny"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Aktívna záloha"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Aktívne stránky"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Aktívny od "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+#, fuzzy
+#| msgid "Activate"
+msgid "Active state"
+msgstr "Aktivovať"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Prispôsobujúce sa rozkladanie zátaže"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Prispôsobujúce sa rozkladanie prenosovej zátaže odesielania"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Pridať"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Pridať $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add DNS server"
+msgstr "Tang server s kľúčami"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:458
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add Tang keyserver"
+msgstr "Tang server s kľúčami"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Pridať VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+#, fuzzy
+#| msgid "Add VLAN"
+msgid "Add VPN"
+msgstr "Pridať VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+#, fuzzy
+#| msgid "Add item"
+msgid "Add a bitmap"
+msgstr "Pridať položku"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Pridať novú zónu"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+#, fuzzy
+#| msgid "Address"
+msgid "Add address"
+msgstr "Adresa"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Add block devices"
+msgstr "$0 Blokové zariadenie"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Pridať zlúčenie liniek"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Pridať sieťový most"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Pridať disk"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Pridať disky"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Pridať iSCSI portál"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Pridať kĺúč"
+
+#: pkg/storaged/stratis/pool.jsx:551
+#, fuzzy
+#| msgid "Tang keyserver"
+msgid "Add keyserver"
+msgstr "Tang server s kľúčami"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+#, fuzzy
+#| msgid "RAID member"
+msgid "Add member"
+msgstr "RAID člen"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Pridať nového hostiteľa"
+
+#: pkg/networkmanager/firewall.jsx:1043
+#, fuzzy
+#| msgid "Add a new zone"
+msgid "Add new zone"
+msgstr "Pridať novú zónu"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Add passphrase"
+msgstr "Pôvodná heslová fráza"
+
+#: pkg/networkmanager/wireguard.jsx:290
+#, fuzzy
+#| msgid "RAID member"
+msgid "Add peer"
+msgstr "RAID člen"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+#, fuzzy
+#| msgid "Logical volume"
+msgid "Add physical volume"
+msgstr "Logický zväzok"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Pridať porty"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Pridať porty do zóny $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Pridať verejný kĺúč"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+#, fuzzy
+#| msgid "Add item"
+msgid "Add route"
+msgstr "Pridať položku"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+#, fuzzy
+#| msgid "Search"
+msgid "Add search domain"
+msgstr "Hľadať"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Pridať služby"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Pridať službu do zóny $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Pridať služby do zóny $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Pridať tým"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Pridať zónu"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+#, fuzzy
+#| msgid "Encryption options"
+msgid "Adding \"$0\" to encryption options"
+msgstr "Možnosti šifrovania"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr ""
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Pridaním $0 sa preruší spojenie so serverom a zneprístupní sa tak rozhranie "
+"pre jeho správu."
+
+#: pkg/storaged/stratis/pool.jsx:468
+#, fuzzy
+#| msgid ""
+#| "Saving a new passphrase requires unlocking the disk. Please provide a "
+#| "current disk passphrase."
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Uloženie novej heslovej frázy vyžaduje odomknutie disku. Zadajte súčasnú "
+"heslovú frázu k disku."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Pridaním užívateľsky určených portov sa znovunačíta firewall, čím sa stratia "
+"nastavenia, ktoré boli vytvorené počas behu a neboli uložené!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Pridáva sa kľúč"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Pridáva sa fyzický zväzok do $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Ďalšie DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Ďalšie DNS domény k prehľadávaniu $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Ďalšie akcie"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Ďalšia adresa $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Ďalšie balíky:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Ďalšie porty"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Adresa"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Adresa $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Adresa nemôže byť prázdna"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Adresa nie je platné URL"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Adresy"
+
+#: pkg/networkmanager/wireguard.jsx:52
+#, fuzzy
+#| msgid "Address cannot be empty"
+msgid "Addresses are not formatted correctly"
+msgstr "Adresa nemôže byť prázdna"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Správa pomocou webovej konzole Cockpit"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Prístup na úrovni správcu"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:425
+#, fuzzy
+#| msgid "Administrative access"
+msgid "Administrative access required"
+msgstr "Prístup na úrovni správcu"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Pokročilé TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Po"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Po opustení domény sa do tohto stroja budú môcť prihlásiť iba tí užívatelia, "
+"ktorý majú účet priamo na ňom. Môže to postihnúť aj ostatné služby, ako je "
+"nastavenie DNS prekladu a môže sa zmeniť zoznam dôveryhodných cert. autorít."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Po štarte systému"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Výstraha"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Výstraha a závažnejšie"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Všetko"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Všetky aktualizácie"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "All-in-one"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Povoliť spustenie (odmaskovať)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+#, fuzzy
+#| msgid "Allowed addresses"
+msgid "Allowed IPs"
+msgstr "Povolené adresy"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Povolené adresy"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Dokumentácia k Ansible roliam"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Vzhľad"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr ""
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Aplikácie"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Zoznam aplikácií"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+#, fuzzy
+#| msgid "Save and reboot"
+msgid "Apply and reboot"
+msgstr "Uložiť a reštartovať"
+
+#: pkg/packagekit/kpatch.jsx:261
+#, fuzzy
+#| msgid "Applying updates"
+msgid "Apply kernel live patches"
+msgstr "Aplikujú sa aktulizácie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Aplikovať toto riešenie"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Aplikuje sa riešenie..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Aplikujú sa aktulizácie"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Aplikácia aktualizácií sa nepodarila"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr ""
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Aplikácie"
+
+#: pkg/storaged/drive/drive.jsx:102
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Assessment"
+msgid "Assessment"
+msgstr "Posudok"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Inventárny štítok"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+#, fuzzy
+#| msgid "boot"
+msgid "At boot"
+msgstr "boot"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Vyžaduje sa aspoň $0 disk."
+msgstr[1] "Vyžadujú sa aspoň $0 disky."
+msgstr[2] "Vyžaduje sa aspoň $0 diskov."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+#, fuzzy
+#| msgid "At least one disk is needed."
+msgid "At least one block device is needed."
+msgstr "Vyžaduje sa aspoň jeden disk."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Vyžaduje sa aspoň jeden disk."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+#, fuzzy
+#| msgid "1 minute"
+msgid "At minute"
+msgstr "1 minúta"
+
+#: pkg/systemd/timer-dialog.jsx:260
+#, fuzzy
+#| msgid "Seconds"
+msgid "At second"
+msgstr "Sekundy"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "V konkrétny čas"
+
+#: pkg/sosreport/sosreport.jsx:503
+#, fuzzy
+#| msgid "Edit $0 attributes"
+msgid "Attributes"
+msgstr "Upraviť $0 atribúty"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Záznam auditu"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Overiť sa"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Overuje sa"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Overovanie"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Overenie sa nepodarilo"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr ""
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Vyžaduje sa overenie"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Poveriť SSH kľúč"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Poverené verejné SSH kľúče"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automaticky"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Automatické prihlásenie"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Automatické aktualizácie"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Spúšťa sa automaticky"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Automaticky využitím NTP"
+
+#: pkg/lib/serverTime.js:570
+#, fuzzy
+#| msgid "Automatically using specific NTP servers"
+msgid "Automatically using additional NTP servers"
+msgstr "Automaticky využitím konkrétnych NTP serverov"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Automaticky využitím konkrétnych NTP serverov"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Automatizačný skript"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Dostupné ciele na $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Dostupné aktualizácie"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "partition"
+msgid "BIOS boot partition"
+msgstr "oddiel"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "Dátum vydania BIOSu"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "Verzia BIOSu"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Späť k účtom"
+
+#: pkg/systemd/services.jsx:257
+#, fuzzy
+#| msgid "Blade"
+msgid "Bad"
+msgstr "Blade"
+
+#: pkg/systemd/services.jsx:223
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Bad setting"
+msgstr "Nastavenia mostu"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Rozkladanie záťaže"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Pred"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Viaže sa na"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Čierny"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blade"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Skriňa pre blade servery"
+
+#: pkg/storaged/block/other.jsx:41
+#, fuzzy
+#| msgid "Locked devices"
+msgid "Block device"
+msgstr "Zamknuté zariadenia"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Blokové zariadenie pre súborové systémy"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+#, fuzzy
+#| msgid "Locked devices"
+msgid "Block devices"
+msgstr "Zamknuté zariadenia"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Blokované"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Väzba"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Zavádzanie"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Viazaný"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Most"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Port mostu"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Nastavenia portov mostu"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Vysielanie"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Chybné nastavenia"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Sú dostupné opravy chýb"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Chyby"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Zmenou hesla k SSH kľuču $0na prihlasovacie heslo užívateľa $1na $2 bude "
+"kľúč automaticky sprístupnený a na $3 sa nabudúce prihlásite už bez hesla."
+
+#: pkg/static/login.html:61
+msgid "Bypass browser check"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Zabezpečenie procesoru"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Využitie procesoru"
+
+#: pkg/metrics/metrics.jsx:1939
+#, fuzzy
+#| msgid "CPU usage"
+msgid "CPU usage/load"
+msgstr "Využitie procesoru"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Vyrovnávacia pamäť"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "Môže byť názov hostiteľa, IP adresa, alias alebo ssh:// URI"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr "Nepodarilo sa nájsť žiadne záznamy pre danú kombináciu filtrov."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Zrušiť"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr ""
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+#, fuzzy
+#| msgid "Force restart"
+msgid "Cancel reboot"
+msgstr "Vynútiť reštart"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Nie je možné povoliť"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Nie je možné sa prihlásiť k neznámemu hostiteľovi"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Nie je možné preposlať prístupové údaje"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr "Nie je možné pridanie do domény, pretože na tomto systéme chýba realmd"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Nie je možné plánovať udalosti do minulosti"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Kapacita"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Nosný signál"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Zmeniť"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+#, fuzzy
+#| msgid "Change performance profile"
+msgid "Change cryptographic policy"
+msgstr "Zmeniť profil výkonu"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Zmeniť názov stroja"
+
+#: pkg/storaged/overview/overview.jsx:152
+#, fuzzy
+#| msgid "Change iSCSI initiator name"
+msgid "Change iSCSI initiater name"
+msgstr "Zmeniť názov iSCSI iniciátoru"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Zmeniť názov iSCSI iniciátoru"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change label"
+msgstr "Zmeniť heslovú frázu"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Zmeniť heslovú frázu"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Zmeniť heslo"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Zmeniť profil výkonu"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Zmeniť profil"
+
+#: pkg/users/shell-dialog.js:70
+#, fuzzy
+#| msgid "Change passphrase"
+msgid "Change shell"
+msgstr "Zmeniť heslovú frázu"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Zmeniť systémový čas"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Zmeniť heslo kľúča $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Zmeniť nastavenia"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Zmenené kľúče sú často výsledkom preinštalovania operačného systému. Avšak "
+"neočakávaná zmena môže znamenať, že sa tretia strana snaží odpočúvať vaše "
+"spojenie."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Zmenou nastavení sa uší spojenie so serverom a zneprístupní sa tak rozhranie "
+"pre jeho správu."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Skontrolovať aktualizácie"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Overuje sa $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Overuje sa IP adresa"
+
+#: pkg/storaged/jobs-panel.jsx:68
+#, fuzzy
+#| msgid "Checking RAID device $target"
+msgid "Checking MDRAID device $target"
+msgstr "Overuje sa RAID zariadenie $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+#, fuzzy
+#| msgid "Checking and repairing RAID device $target"
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Kontroluje a opravuje sa RAID zariadenie $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+#, fuzzy
+#| msgid "Checking for package updates..."
+msgid "Checking for $0 package"
+msgstr "Kontrolujú sa aktualizácie..."
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr ""
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Hľadajú sa nové aplikácie"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Kontrolujú sa aktualizácie..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Zisťuje sa nainštalovaný software"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1399
+#, fuzzy
+#| msgid "Checking $target"
+msgid "Checking software status"
+msgstr "Overuje sa $target"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr ""
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Veľkosť bloku"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Trieda"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Čistenie pre $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Vyčistiť „Nepodarilo sa spustiť“"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Vyčistiť všetky filtre"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Vyčistiť vyhľadávanie"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+#, fuzzy
+#| msgid "Create devices"
+msgid "Cleartext device"
+msgstr "Vytvoriť zariadenia"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Klientský software"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Zavrieť"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr ""
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr ""
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpitu sa nepodarilo kontaktovať daného hostiteľa."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "V Cockpite nastala neočakávaná vnútorná chyba."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit je správca serveru, který uľahčuje správu Linuxových serverov cez "
+"webový prehliadač. Nie je žiadným problémom prechádzať medzi terminálom a "
+"webovým nástrojom. Služba spustená cez Cockpit môže byť zastavená v "
+"termináli. Podobne, pokiaľ dôjde k chybe v termináli, je toto vidieť v "
+"rozhraní žurnálu v Cockpite."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit je interaktívne rozhranie pre správu linuxových serverov."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit nie je kompatibilný so sofvérom na systéme."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit nie je nainštalovaný"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit nie je nainštalovaný na tomto systéme."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit je skvelý nástroj pre nových správcov serverov, ktorým jednoducho "
+"umožňuje vykonávať úlohy ako správa úložiska, kontrola žurnálu a spúšťanie "
+"či zastavovanie služieb. Môžte monitorovať a spravovať viacero serverov "
+"naraz. Stačí ich pridať jedným kliknutím a vaše stroje sa budú starať o "
+"svojích kamarátov."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1494
+#, fuzzy
+#| msgid "Store metrics"
+msgid "Collect metrics"
+msgstr "Ukladať metriky"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Farba"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+#, fuzzy
+#| msgid "Comma-separated ports, ranges, and aliases are accepted"
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "Sú prijímané čiarkou oddelené porty, rozsahy a alternatívne názvy"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Príkaz"
+
+#: pkg/systemd/timer-dialog.jsx:181
+#, fuzzy
+#| msgid "Not found"
+msgid "Command not found"
+msgstr "Nenájdené"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Komentár"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Komunikácia s tuned sa nepodarila"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Kompaktné PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Kompatibilné so všetkými systémami a zariadeniami (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Kompatibilné s modernými systémami a pevnými diskami > 2TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Komprimovať výpisi pamäti jadra pre úsporu miesta"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Kompresia"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Podmienka $0=$1 nebola splnená"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Podmienka nesplnená"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Konfigurácia"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Nastavuje sa"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Nastavuje sa IP adresa"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Konfigurácia kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Potvrdiť"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+#, fuzzy
+#| msgid "Please confirm deletion of $0"
+msgid "Confirm deletion of $0"
+msgstr "Potvrďte zmazanie $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Potvrdiť heslo ku kľúči"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Potvrdiť nové heslo ku kľúču"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Potvrdiť nové heslo"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Potvrdiť heslo"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr ""
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+#, fuzzy
+#| msgid "Please confirm deletion of $0"
+msgid "Confirm stopping of $0"
+msgstr "Potvrďte zmazanie $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "V konflikte"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "V konflikte"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Pripojiť automaticky"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Pripojiť k"
+
+#: pkg/static/login.js:660
+#, fuzzy
+#| msgid "Connect to"
+msgid "Connect to:"
+msgstr "Pripojiť k"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr ""
+
+#: pkg/systemd/services.jsx:291
+#, fuzzy
+#| msgid "Connection failed"
+msgid "Connecting to dbus failed: $0"
+msgstr "Pripojenie sa nepodarilo"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr ""
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Chyba pripojenia"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Pripojenie sa nepodarilo"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Časový limit spojenia vypršal."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Skladá sa z"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Pripojená doména"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Obsahuje:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Pokračovať"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Pokračovať v relácii"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Ovládanie"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Ovládanie"
+
+#: pkg/systemd/abrtLog.jsx:128
+#, fuzzy
+#| msgid "Control"
+msgid "Controller"
+msgstr "Ovládanie"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Počítač 2v1"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Skopírované"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Kopírovať"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:356
+#, fuzzy
+#| msgid "Could not contact {{host}}"
+msgid "Could not contact $0"
+msgstr "Nepodarilo sa kontaktovať {{host}}"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Nahlasovanie pádu"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Havarovať systém"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Vytvoriť"
+
+#: pkg/storaged/overview/overview.jsx:146
+#, fuzzy
+#| msgid "LVM volume group"
+msgid "Create LVM2 volume group"
+msgstr "LVM skupina zväzkov"
+
+#: pkg/storaged/overview/overview.jsx:145
+#, fuzzy
+#| msgid "Create RAID device"
+msgid "Create MDRAID device"
+msgstr "Vytvoriť RAID zariadenie"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Vytvoriť RAID zariadenie"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+#, fuzzy
+#| msgid "Create partition"
+msgid "Create Stratis pool"
+msgstr "Vytvoriť oddiel"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Vytvoriť nový SSH kľúč a poveriť ho"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+#, fuzzy
+#| msgid "Create snapshot"
+msgid "Create a snapshot of filesystem $0"
+msgstr "Zachytiť stav"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+#, fuzzy
+#| msgid "Create new"
+msgid "Create and mount"
+msgstr "Vytvoriť nový"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create new"
+msgid "Create and start"
+msgstr "Vytvoriť nový"
+
+#: pkg/storaged/stratis/pool.jsx:91
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Create filesystem"
+msgstr "Súborový systém"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Vytvoriť to"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:307
+#, fuzzy
+#| msgid "Network file system"
+msgid "Create new filesystem"
+msgstr "Sieťový súborový systém"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+#, fuzzy
+#| msgid "Create new"
+msgid "Create new group"
+msgstr "Vytvoriť nový"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr ""
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+msgid "Create new thinly provisioned logical volume"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+#, fuzzy
+#| msgid "read only"
+msgid "Create only"
+msgstr "iba na čítanie"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Vytvoriť oddiel"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr ""
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Vytvoriť tabuľku oddielov"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Zachytiť stav"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+#, fuzzy
+#| msgid "Create snapshot"
+msgid "Create snapshot and mount"
+msgstr "Zachytiť stav"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+#, fuzzy
+#| msgid "Create snapshot"
+msgid "Create snapshot only"
+msgstr "Zachytiť stav"
+
+#: pkg/storaged/overview/overview.jsx:174
+#, fuzzy
+#| msgid "Create devices"
+msgid "Create storage device"
+msgstr "Vytvoriť zariadenia"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Vytvoriť zväzok"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Vytvoriť časovač"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:502
+#, fuzzy
+#| msgid "Create"
+msgid "Created"
+msgstr "Vytvoriť"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Vytvára sa LVM2 skupina zväzkov $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+#, fuzzy
+#| msgid "Creating RAID device $target"
+msgid "Creating MDRAID device $target"
+msgstr "Vytvára sa RAID zariadenie $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+#, fuzzy
+#| msgid "Create VDO device"
+msgid "Creating VDO device"
+msgstr "Vytvoriť VDO zariadenie"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr ""
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Vytvorením $0 sa preruší spojenie so serverom a zneprístupní sa tak "
+"rozhranie pre jeho správu."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Kritické a závažnejšie"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr ""
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+#, fuzzy
+#| msgid "Ctrl-Shift-J"
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-J"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Od tohto spustenia"
+
+#: pkg/metrics/metrics.jsx:757
+#, fuzzy
+#| msgid "Current"
+msgid "Current top CPU usage"
+msgstr "Súčasný"
+
+#: pkg/storaged/dialog.jsx:1178
+#, fuzzy
+#| msgid "Current"
+msgid "Currently in use"
+msgstr "Súčasný"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Current"
+msgid "Currently not supported"
+msgstr "Súčasný"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "lastné"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Užívateľsky určené porty"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom ports"
+msgid "Custom type"
+msgstr "Užívateľsky určené porty"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Užívateľsky určené zóny"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Tmavý"
+
+#: pkg/storaged/stratis/pool.jsx:197
+#, fuzzy
+#| msgid "Data used"
+msgid "Data"
+msgstr "Využitých dát"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Využitých dát"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Deaktivovať"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+#, fuzzy
+#| msgid "Logical volume of $0"
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Logický zväzok na $0"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Deaktivuje sa"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Deaktivuje sa $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Ladiace a závažnejšie"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+#, fuzzy
+#| msgid "RAID 4 (dedicated parity)"
+msgid "Dedicated parity (RAID 4)"
+msgstr "RAID 4 (vyhradená parita)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Deduplikácia"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr ""
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Omeškanie"
+
+#: pkg/systemd/timer-dialog.jsx:216
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Delay must be a number"
+msgstr "Veľkosť musí byť číslo"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Zmazať"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Zmazať $0"
+
+#: pkg/users/accounts-list.js:80
+#, fuzzy
+#| msgid "Never lock account"
+msgid "Delete account"
+msgstr "Nikdy nezamknúť účet"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Zmazať súbory"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+#, fuzzy
+#| msgid "Never lock account"
+msgid "Delete group"
+msgstr "Nikdy nezamknúť účet"
+
+#: pkg/storaged/stratis/pool.jsx:292
+#, fuzzy
+#| msgid "Delete"
+msgid "Delete pool"
+msgstr "Zmazať"
+
+#: pkg/sosreport/sosreport.jsx:350
+#, fuzzy
+#| msgid "Delete content"
+msgid "Delete report permanently?"
+msgstr "Zmazať obsah"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Odstránením $0 sa preruší spojenie so serverom a zneprístupní sa tak "
+"rozhranie pre jeho správu."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Maže sa $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Maže sa LVM2 skupina zväzkov $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+#, fuzzy
+#| msgid "Deleting a container will erase all data in it."
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "Zmazaním kontajneru sa zmažú všetky dáta v ňom."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+#, fuzzy
+#| msgid "Deleting a partition will delete all data in it."
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "Zmazaním oddielu sa zmažú všetky dáta na ňom."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "Zmazaním oddielu sa zmažú všetky dáta na ňom."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Odstránením RAID zariadenia sa zmažú dáta na ňom."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on a VDO device."
+msgstr "Odstránením RAID zariadenia sa zmažú dáta na ňom."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on a volume group."
+msgstr "Odstránením RAID zariadenia sa zmažú dáta na ňom."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting a VDO device will erase all data on it."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "Odstránením RAID zariadenia sa zmažú dáta na ňom."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Popis"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Desktop"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Odpojiteľné"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Detaily"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Vývoj"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Zariadenie"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Súbor zariadenia"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Zariadenie je len na čítanie"
+
+#: pkg/storaged/block/other.jsx:60
+#, fuzzy
+#| msgid "Service name"
+msgid "Device number"
+msgstr "Názov služby"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Diagnostické hlásenia"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Zložka"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Zložka $0 nie je zapisovateľná alebo neexistuje."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Vypnúť simultánny multithreading"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Vypnúť bránu firewall"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Vypnúť tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Vypnutá"
+
+#: pkg/users/account-details.js:276
+#, fuzzy
+#| msgid "Domain administrator password"
+msgid "Disallow interactive password"
+msgstr "Heslo správcu domény"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Nepovoliť spustenie (maskovať)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Odpojiť"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Odpojené"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Diskový V/V"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Disk je OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+#, fuzzy
+#| msgid "$0 disk is missing"
+#| msgid_plural "$0 disks are missing"
+msgid "Disk is failing"
+msgstr "$0 disk chýba"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Heslová fráza k disku"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Disky"
+
+#: pkg/metrics/metrics.jsx:792
+#, fuzzy
+#| msgid "Disks"
+msgid "Disks usage"
+msgstr "Disky"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+#, fuzzy
+#| msgid "Dismiss $0 alert"
+#| msgid_plural "Dismiss $0 alerts"
+msgid "Dismiss"
+msgstr "Zahodiť $0 upozornenie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Zahodiť $0 upozornenie"
+msgstr[1] "Zahodiť $0 upozornenia"
+msgstr[2] "Zahodiť $0 upozornení"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr ""
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Jazyk zobrazenia"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+#, fuzzy
+#| msgid "RAID 5 (distributed parity)"
+msgid "Distributed parity (RAID 5)"
+msgstr "RAID 5 (distribuovaná parita)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+#, fuzzy
+#| msgid "Not mounted"
+msgid "Do not mount"
+msgstr "Nepripojené"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Nepripájať automaticky pri štarte systému"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Dokovacia stanica"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Nespúšťa sa automaticky"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+#, fuzzy
+#| msgid "Do not mount automatically on boot"
+msgid "Does not mount during boot"
+msgstr "Nepripájať automaticky pri štarte systému"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Doména"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Doména"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Adresa domény"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Meno správcu domény"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Heslo správcu domény"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Doménu sa nepodarilo kontaktovať"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Doména nie je podporovaná"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Neopakovať"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+#, fuzzy
+#| msgid "RAID 6 (double distributed parity)"
+msgid "Double distributed parity (RAID 6)"
+msgstr "RAID 6 (dvojitá distribuovaná parita)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Stiahnúť"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Stiahnite si zdarma nový prehliadač"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Stiahnuté"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Sťahuje sa"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Sťahuje sa $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Jednotka"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Dual rank"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Rozšírený oddiel"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Upraviť"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Upraviť /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Upraviť Tang server s kľúčami"
+
+#: pkg/networkmanager/vlan.jsx:91
+#, fuzzy
+#| msgid "VLAN settings"
+msgid "Edit VLAN settings"
+msgstr "VLAN nastavenia"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:135
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Edit bond settings"
+msgstr "Nastavenia mostu"
+
+#: pkg/networkmanager/bridge.jsx:96
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Edit bridge settings"
+msgstr "Nastavenia mostu"
+
+#: pkg/networkmanager/firewall.jsx:596
+#, fuzzy
+#| msgid "Add services to $0 zone"
+msgid "Edit custom service in $0 zone"
+msgstr "Pridať službu do zóny $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Upraviť hostiteľa"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Upraviť hostiteľov"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Upraviť motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Bod pripojenia"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Upraviť pravidlá a zóny"
+
+#: pkg/networkmanager/firewall.jsx:595
+#, fuzzy
+#| msgid "Add services"
+msgid "Edit service"
+msgstr "Pridať služby"
+
+#: pkg/networkmanager/firewall.jsx:119
+#, fuzzy
+#| msgid "Remove service $0"
+msgid "Edit service $0"
+msgstr "Odstrániť službu $0"
+
+#: pkg/networkmanager/team.jsx:154
+#, fuzzy
+#| msgid "Team settings"
+msgid "Edit team settings"
+msgstr "Nastavenia týmu"
+
+#: pkg/users/accounts-list.js:59
+#, fuzzy
+#| msgid "Add services"
+msgid "Edit user"
+msgstr "Pridať služby"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Úprava kľuču vyžaduje voľný slot"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Vysúva sa $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr ""
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Prázdny"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Vyprázdniť"
+
+#: pkg/users/account-create-dialog.js:201
+#, fuzzy
+#| msgid "Key password"
+msgid "Empty password"
+msgstr "Heslo ku kľúču"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Vyprázdňuje sa $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Povoliť"
+
+#: pkg/networkmanager/network-interface.jsx:685
+#, fuzzy
+#| msgid "Failed to disable tuned"
+msgid "Enable or disable the device"
+msgstr "Nepodarilo sa deaktivovať tuned"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Zapnúť službu"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Zapnúť bránu firewall"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Povolená"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+#, fuzzy
+#| msgid "Installing $0"
+msgid "Enabling $0"
+msgstr "Inštaluje sa $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Šifrovať dáta"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+#, fuzzy
+#| msgid "Edit Tang keyserver"
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Upraviť Tang server s kľúčami"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+#, fuzzy
+#| msgid "Encryption type"
+msgid "Encrypt data with a passphrase"
+msgstr "Typ šifrovania"
+
+#: pkg/sosreport/sosreport.jsx:450
+#, fuzzy
+#| msgid "Encrypted $0"
+msgid "Encrypted"
+msgstr "Šifrované $0"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Šifrované $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "Encrypted partition of $0"
+msgid "Encrypted Stratis pool"
+msgstr "Šifrovaný oddiel na $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Šifrovaný logický zväzok na $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Šifrovaný oddiel na $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Šifrovanie"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Možnosti šifrovania"
+
+#: pkg/sosreport/sosreport.jsx:309
+#, fuzzy
+#| msgid "Encryption type"
+msgid "Encryption passphrase"
+msgstr "Typ šifrovania"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Typ šifrovania"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:309
+#, fuzzy
+#| msgid "Entrypoint"
+msgid "Endpoint"
+msgstr "Vstupný bod"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Vynucuje sa"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr ""
+
+#: pkg/networkmanager/mac.jsx:47
+#, fuzzy
+#| msgid "MAC address"
+msgid "Enter a valid MAC address"
+msgstr "MAC adresa"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Celá podsieť"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Položka z"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Maže sa $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Errata"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Chyba"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Chyba a závažnejšie"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Vyskytla sa chyba"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "PackageKit nie je nainštalovaný"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Chybové hlásenie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr ""
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Chyba pri ukladaní poverených kľúčov: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Chyba pri nastavovaní SELinux režimu: „$0“"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Príklad: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Príklad: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Skvelé heslo"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr ""
+
+#: pkg/users/expiration-dialogs.js:71
+#, fuzzy
+#| msgid "Lock account on $0"
+msgid "Expire account on"
+msgstr "Zamknúť účet na $0"
+
+#: pkg/users/account-details.js:78
+#, fuzzy
+#| msgid "Lock account on $0"
+msgid "Expire account on $0"
+msgstr "Zamknúť účet na $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+#, fuzzy
+#| msgid "port"
+msgid "Export"
+msgstr "port"
+
+#: pkg/metrics/metrics.jsx:1511
+#, fuzzy
+#| msgid "Routed network"
+msgid "Export to network"
+msgstr "Smerovaná sieť"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Rozšírené informácie"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Rozšírený oddiel"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Neúspešné"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Nepodarilo sa pridať stroj: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Nepodarilo sa pridať port"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Nepodarilo sa pridať službu"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Nepodarilo sa pridať zónu"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Nepodarilo sa zmeniť heslo"
+
+#: pkg/metrics/metrics.jsx:1487
+#, fuzzy
+#| msgid "Failed to clone VM $0"
+msgid "Failed to configure PCP"
+msgstr "Nepodarilo sa klonovanie virt. stroja $0"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Nepodarilo sa deaktivovať tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+#, fuzzy
+#| msgid "Failed to disable tuned"
+msgid "Failed to disable tuned profile"
+msgstr "Nepodarilo sa deaktivovať tuned"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Nepodarilo sa upraviť stroj: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+#, fuzzy
+#| msgid "Failed to add service"
+msgid "Failed to edit service"
+msgstr "Nepodarilo sa pridať službu"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+#, fuzzy
+#| msgid "Failed to enable tuned"
+msgid "Failed to enable $0 in firewalld"
+msgstr "Nepodarilo sa aktivovať tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Nepodarilo sa aktivovať tuned"
+
+#: pkg/systemd/logsJournal.jsx:259
+#, fuzzy
+#| msgid "Failed to edit machine: $0"
+msgid "Failed to fetch logs"
+msgstr "Nepodarilo sa upraviť stroj: $0"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Nepodarilo sa načítať poverené kľúče."
+
+#: pkg/systemd/service-details.jsx:539
+#, fuzzy
+#| msgid "Failed to add port"
+msgid "Failed to load unit"
+msgstr "Nepodarilo sa pridať port"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:498
+#, fuzzy
+#| msgid "Failed to add service"
+msgid "Failed to restart service"
+msgstr "Nepodarilo sa pridať službu"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Nepodarilo sa uložiť zmeny v /etc/motd"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+#, fuzzy
+#| msgid "Failed to add service"
+msgid "Failed to save settings"
+msgstr "Nepodarilo sa pridať službu"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Nepodarilo sa spustiť"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr ""
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+#, fuzzy
+#| msgid "Failed to start"
+msgid "File state"
+msgstr "Nepodarilo sa spustiť"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Súborový systém"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+#, fuzzy
+#| msgid "Filesystems"
+msgid "Filesystem is locked"
+msgstr "Súborové systémy"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1114
+msgid "Filesystem outside the target"
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr ""
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr ""
+
+#: pkg/shell/shell-modals.jsx:134
+#, fuzzy
+#| msgid "Filter services"
+msgid "Filter menu items"
+msgstr "Filtrovať služby"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Filtrovať služby"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Filtre"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Odtlačok"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Brána firewall"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:122
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Firmware version"
+msgid "Firmware version"
+msgstr "Verzia firmware"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+#, fuzzy
+#| msgid "Chunk size"
+msgid "Font size"
+msgstr "Veľkosť bloku"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr ""
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Vynútiť zmenu"
+
+#: pkg/users/delete-group-dialog.js:51
+#, fuzzy
+#| msgid "Delete"
+msgid "Force delete"
+msgstr "Zmazať"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Vynútiť zmenu hesla"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Formátovať"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Formátovať $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+#, fuzzy
+#| msgid "Format disk $0"
+msgid "Format and mount"
+msgstr "Formátovať disk $0"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format disk $0"
+msgid "Format and start"
+msgstr "Formátovať disk $0"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+#, fuzzy
+#| msgid "Format"
+msgid "Format only"
+msgstr "Formátovať"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr ""
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr ""
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Voľný priestor"
+
+#: pkg/systemd/logs.jsx:378
+#, fuzzy
+#| msgid "Clear search"
+msgid "Free-form search"
+msgstr "Vyčistiť vyhľadávanie"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Piatky"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Celé meno"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Brána"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Všeobecné"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+#, fuzzy
+#| msgid "General"
+msgid "Generated"
+msgstr "Všeobecné"
+
+#: pkg/systemd/logDetails.jsx:52
+#, fuzzy
+#| msgid "Route to $0"
+msgid "Go to $0"
+msgstr "Trasa na $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Prejsť na aplikáciu"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Prejsť na súčasnosť"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr ""
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Skupina"
+
+#: pkg/users/accounts-list.js:238
+#, fuzzy
+#| msgid "Volume group name"
+msgid "Group name"
+msgstr "Názov skupiny zväzkov"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+#, fuzzy
+#| msgid "Group"
+msgid "Groups"
+msgstr "Skupina"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Zväčšiť"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Zväčšiť obsah"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:409
+#, fuzzy
+#| msgid "partition"
+msgid "Grow partition"
+msgstr "oddiel"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr ""
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr ""
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr ""
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Pre držanie do ruky"
+
+#: pkg/storaged/drive/drive.jsx:64
+#, fuzzy
+#| msgid "Hard Disk"
+msgid "Hard Disk Drive"
+msgstr "Pevný disk"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Informácie o hardware"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Zdravie"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Nápoveda"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Hide confirmation password"
+msgstr "Potvrdiť heslo"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "User password"
+msgid "Hide password"
+msgstr "Užívateľské heslo"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr ""
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Počet balíkov"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+#, fuzzy
+#| msgid "Directory"
+msgid "Home directory"
+msgstr "Zložka"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Hostiteľ"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Kľúč stroja nie je správny"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Názov stroja"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Hostitelia"
+
+#: pkg/systemd/timer-dialog.jsx:244
+#, fuzzy
+#| msgid "Hours"
+msgid "Hourly"
+msgstr "Hodiny"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Hodiny"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr ""
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP adresa"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+#, fuzzy
+#| msgid "IPv4 address"
+msgid "IPv4 addresses"
+msgstr "IPv4 adresa"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "Nastavenia IPv4"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "Nastavenia IPv6"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Identifikátor"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ignorovať"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "Vstup"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Aby v budúcnosti bolo možné prihlasovanie k $0 ako $1 bez hesla, použite "
+"prihlasovacie heslo užívateľa $2 na $3 ako heslo ku kľúču alebo heslo ku "
+"kľúču nevyplňujte."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "Synchronizované"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Neaktívne"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+#, fuzzy
+#| msgid "Inactive volume"
+msgid "Inactive logical volume"
+msgstr "Neaktívny zväzok"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Zahrnuté služby"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr ""
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr ""
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Nepriame"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Informácia a závažnejšie"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Inicializovať"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Inicializovať disk $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Inicializácia..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Insights: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Inštalovať"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Nainštalovať podporu pre NFS"
+
+#: pkg/storaged/overview/overview.jsx:121
+#, fuzzy
+#| msgid "Install NFS support"
+msgid "Install Stratis support"
+msgstr "Nainštalovať podporu pre NFS"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Nainštalovať všetky aktualizácie"
+
+#: pkg/apps/application-list.jsx:200
+#, fuzzy
+#| msgid "Package information"
+msgid "Install application information"
+msgstr "Informácie o balíku"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Nainštalovať cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+#, fuzzy
+#| msgid "Install all updates"
+msgid "Install kpatch updates"
+msgstr "Nainštalovať všetky aktualizácie"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+#, fuzzy
+#| msgid "Install NFS support"
+msgid "Install realmd support"
+msgstr "Nainštalovať podporu pre NFS"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Nainštalovať bezpečnostné aktualizácie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr "Nainštalovať setroubleshoot-server pre riešenie problémov so SELinux."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Inštalovať software"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Nainštalujte $0 balík"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Nainštalovaný"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Inštaluje sa"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Inštaluje sa $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+#, fuzzy
+#| msgid "Installing $0"
+msgid "Installing $0 would remove $1."
+msgstr "Inštaluje sa $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+#, fuzzy
+#| msgid "Installing"
+msgid "Installing packages"
+msgstr "Inštaluje sa"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+#, fuzzy
+#| msgid "Interface"
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Rozhranie"
+msgstr[1] "Rozhranie"
+msgstr[2] "Rozhranie"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+#, fuzzy
+#| msgid "Interfaces"
+msgid "Interface members"
+msgstr "Rozhrania"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Rozhrania"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Interná chyba"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr ""
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Neplatné"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr ""
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Neplatný formát dátumu"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Neplatný formát dátumu a času"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr ""
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr ""
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Neplatný kľúč"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr ""
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr ""
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr ""
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Neplatný rozsah"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Neplatný formát čase"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Neplatná časová zóna"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Neplatné užívateľské meno alebo heslo"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Úlohy"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Spojiť"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+#, fuzzy
+#| msgid "Join a domain"
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Pripojiť sa k doméne"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Pripojiť sa k doméne"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+#, fuzzy
+#| msgid "Join"
+msgid "Joining"
+msgstr "Spojiť"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Pripájanie sa do domény vyžaduje inštaláciu realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr ""
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Žurnál"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Položka žurnálu"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Preskočiť na"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit nie je nainštalovaný"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Vyskúšať nastavenia kdump"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Zachovať spojenie"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr ""
+
+#: pkg/packagekit/kpatch.jsx:366
+#, fuzzy
+#| msgid "Kernel patch $0 is installed"
+msgid "Kernel live patch $0 is active"
+msgstr "Záplata jadra systému $0 je nainštalovaná"
+
+#: pkg/packagekit/kpatch.jsx:373
+#, fuzzy
+#| msgid "Kernel patch $0 is installed"
+msgid "Kernel live patch $0 is installed"
+msgstr "Záplata jadra systému $0 je nainštalovaná"
+
+#: pkg/packagekit/kpatch.jsx:299
+#, fuzzy
+#| msgid "Change the settings"
+msgid "Kernel live patch settings"
+msgstr "Zmeniť nastavenia"
+
+#: pkg/packagekit/kpatch.jsx:282
+#, fuzzy
+#| msgid "Change the settings"
+msgid "Kernel live patching"
+msgstr "Zmeniť nastavenia"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Heslo ku kľúču"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Sloty kľúčov s neznámym typom tu nie je možné upraviť"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Kľúče"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Server s kľúčami"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr ""
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr ""
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+#, fuzzy
+#| msgid "Pool"
+msgid "LVM2 VDO pool"
+msgstr "Úložisko"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+#, fuzzy
+#| msgid "Logical volume"
+msgid "LVM2 logical volume"
+msgstr "Logický zväzok"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+#, fuzzy
+#| msgid "Logical volumes"
+msgid "LVM2 logical volumes"
+msgstr "Logické zväzky"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+#, fuzzy
+#| msgid "Physical volume of $0"
+msgid "LVM2 physical volume"
+msgstr "Fyzický zväzok na $0"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+#, fuzzy
+#| msgid "Physical volume of $0"
+msgid "LVM2 physical volumes"
+msgstr "Fyzický zväzok na $0"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM2 skupina zväzkov"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "LVM2 skupina zväzkov $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Notebook"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Posledných 24 hodín"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Posledných 7 dní"
+
+#: pkg/users/accounts-list.js:391
+#, fuzzy
+#| msgid "active"
+msgid "Last active"
+msgstr "Aktívny"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last cannot be removed"
+msgstr "Posledný slot kľúča nemôže byť odobraný"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Naposledy skontrolované: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+#, fuzzy
+#| msgid "The last key slot can not be removed"
+msgid "Last disk can not be removed"
+msgstr "Posledný slot kľúča nemôže byť odobraný"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Posledné prihlásenie"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+#, fuzzy
+#| msgid "Last checked: $0"
+msgid "Last modified: $0"
+msgstr "Naposledy skontrolované: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+#, fuzzy
+#| msgid "Last failed login:"
+msgid "Last successful login:"
+msgstr "Posledné nepodarené prihlásenie:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Zistiť viac"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr ""
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:317
+#, fuzzy
+#| msgid "encryption"
+msgid "Leave empty to skip encryption"
+msgstr "Šifrovanie"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Svetlý"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr ""
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr ""
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr ""
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr ""
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem"
+msgid "Linux filesystem data"
+msgstr "Odpojiť súborový systém"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Odkladací priestor"
+
+#: pkg/systemd/service-details.jsx:670
+#, fuzzy
+#| msgid "Persistent"
+msgid "Listen"
+msgstr "Trvalý"
+
+#: pkg/networkmanager/wireguard.jsx:242
+#, fuzzy
+#| msgid "Persistent"
+msgid "Listen port"
+msgstr "Trvalý"
+
+#: pkg/networkmanager/wireguard.jsx:149
+#, fuzzy
+#| msgid "Size must be a number"
+msgid "Listen port must be a number"
+msgstr "Veľkosť musí byť číslo"
+
+#: pkg/systemd/services.jsx:683
+#, fuzzy
+#| msgid "Systemd units"
+msgid "Listing units"
+msgstr "Systemd jednotky"
+
+#: pkg/systemd/services.jsx:503
+#, fuzzy
+#| msgid "Last checked: $0"
+msgid "Listing units failed: $0"
+msgstr "Naposledy skontrolované: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Záťaž"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr ""
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Načítať skoršie položky"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr ""
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Načítajú sa skoršie položky"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Načítavajú sa kľúče..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr ""
+
+#: pkg/systemd/services.jsx:664
+#, fuzzy
+#| msgid "Login failed"
+msgid "Loading of units failed"
+msgstr "Prihlásenie sa nepodarilo"
+
+#: pkg/shell/shell-modals.jsx:78
+#, fuzzy
+#| msgid "Loading keys..."
+msgid "Loading packages..."
+msgstr "Načítavajú sa kľúče..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Načítavajú sa systémové zmeny..."
+
+#: pkg/systemd/service.jsx:129
+#, fuzzy
+#| msgid "Login failed"
+msgid "Loading unit failed"
+msgstr "Prihlásenie sa nepodarilo"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Načítavanie..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr ""
+
+#: pkg/storaged/overview/overview.jsx:160
+#, fuzzy
+#| msgid "Managed interfaces"
+msgid "Local storage"
+msgstr "Spravované zariadenia"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "lokálne v $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Umiestnenie"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Zamknúť"
+
+#: pkg/users/lock-account-dialog.js:29
+#, fuzzy
+#| msgid "Lock"
+msgid "Lock $0"
+msgstr "Zamknúť"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Zamknúť účet"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Encrypted data"
+msgid "Locked data"
+msgstr "Šifrované dáta"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Zamyká sa $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Prihlásiť"
+
+#: pkg/shell/hosts_dialog.jsx:763
+#, fuzzy
+#| msgid "Route to $0"
+msgid "Log in to $0"
+msgstr "Trasa na $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr ""
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Záznamy udalostí"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Odhlásiť"
+
+#: pkg/users/accounts-list.js:68
+#, fuzzy
+#| msgid "Log out"
+msgid "Log user out"
+msgstr "Odhlásiť"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Prihlásený"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Logický"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Logický zväzok (zachytený stav)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Logická veľkosť"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Logický zväzok"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Logický zväzok (zachytený stav)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Logický zväzok na $0"
+
+#: pkg/static/login.js:361
+#, fuzzy
+#| msgid "login"
+msgid "Login"
+msgstr "prihlásenie"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Znovu prihlásiť"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Prihlásenie sa nepodarilo"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Formát prihlasovania"
+
+#: pkg/users/account-logs-panel.jsx:73
+#, fuzzy
+#| msgid "history"
+msgid "Login history"
+msgstr "história"
+
+#: pkg/users/account-logs-panel.jsx:75
+#, fuzzy
+#| msgid "Login format"
+msgid "Login history list"
+msgstr "Formát prihlasovania"
+
+#: pkg/users/logout-account-dialog.js:29
+#, fuzzy
+#| msgid "Slot $0"
+msgid "Logout $0"
+msgstr "Slot $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Odhlásenie bolo úspešné"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Záznamy udalostí"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Nízky desktop"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Kufríkový počítač"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID device"
+msgstr "RAID zariadenie"
+
+#: pkg/storaged/utils.js:334
+#, fuzzy
+#| msgid "RAID device $0"
+msgid "MDRAID device $0"
+msgstr "RAID zariadenie $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+#, fuzzy
+#| msgid "Device is read-only"
+msgid "MDRAID device is recovering"
+msgstr "Zariadenie je len na čítanie"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+#, fuzzy
+#| msgid "Service is running"
+msgid "MDRAID device must be running"
+msgstr "Služba je spustená"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+#, fuzzy
+#| msgid "RAID device"
+msgid "MDRAID disk"
+msgstr "RAID zariadenie"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+#, fuzzy
+#| msgid "Add disks"
+msgid "MDRAID disks"
+msgstr "Pridať disky"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (odporúčané)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU musí byť kladné číslo"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Identifikátor stroja"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr ""
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Hlavná skriňa serveru"
+
+#: pkg/systemd/services.jsx:238
+#, fuzzy
+#| msgid "interface"
+msgid "Maintenance"
+msgstr "rozhranie"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Manage filesystem sizes"
+msgstr "Súborový systém"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+#, fuzzy
+#| msgid "Managed interfaces"
+msgid "Manage storage"
+msgstr "Spravované zariadenia"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Spravované zariadenia"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "Managing services"
+msgid "Managing LVMs"
+msgstr "Správa služieb"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "Managing user accounts"
+msgid "Managing NFS mounts"
+msgstr "Správa užívateľských účtov"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "Managing services"
+msgid "Managing RAIDs"
+msgstr "Správa služieb"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "Managing services"
+msgid "Managing VDOs"
+msgstr "Správa služieb"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+#| msgid "Managing services"
+msgid "Managing VLANs"
+msgstr "Správa služieb"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr ""
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+#| msgid "Managing storage devices"
+msgid "Managing networking bonds"
+msgstr "Správa úložískových zariadení"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+#| msgid "Managing storage devices"
+msgid "Managing networking bridges"
+msgstr "Správa úložískových zariadení"
+
+#: pkg/networkmanager/manifest.json:0
+#, fuzzy
+#| msgid "Managing services"
+msgid "Managing networking teams"
+msgstr "Správa služieb"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "Managing services"
+msgid "Managing physical drives"
+msgstr "Správa služieb"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Správa služieb"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Správa softvérových aktualizácií"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Správa užívateľských účtov"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Ručne"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Ručne"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Maskovať službu"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Maskovaný"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Maskovanie služby zabráni spúšťaniu všetkých závislých jednotiek. Toto môže "
+"mať väčší dopad ako je zamýšľané. Prosím potvrďte, že chcete maskovať túto "
+"jednotku."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:62
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Optical drive"
+msgid "Media drive"
+msgstr "Mechanika optického disku"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Pamäť"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Využitie pamäte"
+
+#: pkg/metrics/metrics.jsx:1939
+#, fuzzy
+#| msgid "Memory usage"
+msgid "Memory usage/swap"
+msgstr "Využitie pamäte"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr ""
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr ""
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr ""
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr ""
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Metóda"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+#, fuzzy
+#| msgid "metrics"
+msgid "Metric"
+msgstr "metriky"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+#, fuzzy
+#| msgid "View details and history"
+msgid "Metrics and history"
+msgstr "Ukázať podrobnosti a históriu"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+#, fuzzy
+#| msgid "Bridge settings"
+msgid "Metrics settings"
+msgstr "Nastavenia mostu"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr ""
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Minúta musí byť číslo medzi 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+#, fuzzy
+#| msgid "Minutes"
+msgid "Minutely"
+msgstr "Minúty"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minúty"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Zmiernenia dopadu"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Režim"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Model"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Pondelky"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Viac _informácií..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Pripojiť"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+#, fuzzy
+#| msgid "Mount point"
+msgid "Mount Point"
+msgstr "Bod pripojenia"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Pripojiť pri štarte"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "pojiť súborový systém"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr ""
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Bod pripojenia"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Pripojiť iba na čítanie"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "pája sa $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:135
+#, fuzzy
+#| msgid "Other devices"
+msgid "Multipathed devices"
+msgstr "Iné zariadenia"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr ""
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP server"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Názov"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr ""
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr ""
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Názov nemôže byť dlhší ako $0 bytov"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Názov nemôže byť dlhší ako $0 znakov"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Názov nemôže byť dlhší ako 127 znakov."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Názov nemôže byť dlhší ako 127 znakov."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr ""
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot contain the character '/'."
+msgstr "Názov nemôže byť dlhší ako 127 znakov."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr ""
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Sieť"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Sieťový vstup/výstup"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr ""
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr ""
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Záznamy udalostí siete"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Využitie siete"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager nie je nainštalovaný"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager nebeží"
+
+#: pkg/storaged/overview/overview.jsx:168
+#, fuzzy
+#| msgid "Network usage"
+msgid "Networked storage"
+msgstr "Využitie siete"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Sieť"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Nikdy"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+#, fuzzy
+#| msgid "Never lock account"
+msgid "Never expire account"
+msgstr "Nikdy nezamknúť účet"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Nikdy neexpirovať heslo"
+
+#: pkg/users/accounts-list.js:173
+#, fuzzy
+#| msgid "Logged in"
+msgid "Never logged in"
+msgstr "Prihlásený"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Nové NFS pripojenie"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Nový hostiteľ"
+
+#: pkg/shell/hosts_dialog.jsx:464
+#, fuzzy
+#| msgid "New host"
+msgid "New host: $0"
+msgstr "Nový hostiteľ"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Nové heslo ku kľúču"
+
+#: pkg/users/rename-group-dialog.jsx:35
+#, fuzzy
+#| msgid "Rename"
+msgid "New name"
+msgstr "Premenovať"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Nová heslová fráza"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Nové heslo"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Nové heslo nebolo prijaté"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Ďalej"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Nie"
+
+#: pkg/users/group-create-dialog.js:79
+#, fuzzy
+#| msgid "No real name specified"
+msgid "No ID specified"
+msgstr "Nie je zadaný skutočný názov"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Žiadne výstrahy SELinux."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Nie sú nainštalované ani dostupné žiadne aplikácie."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Nie sú dostupné žiadne sloty"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Nie sú dostupné žiadne blokové zariadenia."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+#, fuzzy
+#| msgid "$0 block device"
+msgid "No block devices found"
+msgstr "$0 Blokové zariadenie"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Nebola nájdená žiadna konfigurácia"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Nie sú dostupné žiadne dáta"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr ""
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Nie je k dispozícií žiaden popis"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Nie je poskytnutý žiaden popis."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "$0 block device"
+msgid "No devices found"
+msgstr "$0 Blokové zariadenie"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+#, fuzzy
+#| msgid "No logs found"
+msgid "No disks found"
+msgstr "Nenájdené žiadne záznamy udalostí"
+
+#: pkg/storaged/iscsi/session.jsx:79
+#, fuzzy
+#| msgid "No results found"
+msgid "No drives found"
+msgstr "Neboli nájdené žiadne výsledky"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+#, fuzzy
+#| msgid "encryption"
+msgid "No encryption"
+msgstr "Šifrovanie"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr ""
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:360
+#, fuzzy
+#| msgid "filesystem"
+msgid "No filesystems"
+msgstr "Súborový systém"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr ""
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Žiadne voľné miesto"
+
+#: pkg/storaged/partitions/partition.jsx:79
+#, fuzzy
+#| msgid "Create partition"
+msgid "No free space after this partition"
+msgstr "Vytvoriť oddiel"
+
+#: pkg/users/group-create-dialog.js:62
+#, fuzzy
+#| msgid "No real name specified"
+msgid "No group name specified"
+msgstr "Nie je zadaný skutočný názov"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Neboli nájdené žiadne kľúče stroja."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Pre túto aplikáciu nebol nájdený žiaden inštalačný balíček."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Žiadne pridané kľúče"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr ""
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Žiadne záznamy udalostí"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Žiadne logické zväzky"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Nenájdené žiadne záznamy udalostí"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Žiadne vyhovujúce výsledky"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Nie je vložené žiadne médium"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr ""
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+#, fuzzy
+#| msgid "No logs found"
+msgid "No partitions found"
+msgstr "Nenájdené žiadne záznamy udalostí"
+
+#: pkg/networkmanager/wireguard.jsx:341
+#, fuzzy
+#| msgid "No keys added"
+msgid "No peers added."
+msgstr "Žiadne pridané kľúče"
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+#, fuzzy
+#| msgid "Physical volume of $0"
+msgid "No physical volumes found"
+msgstr "Fyzický zväzok na $0"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Nie je zadaný skutočný názov"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Neboli nájdené žiadne výsledky"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Žiadne zásahy pravidla"
+
+#: pkg/storaged/overview/overview.jsx:190
+#, fuzzy
+#| msgid "No logs found"
+msgid "No storage found"
+msgstr "Nenájdené žiadne záznamy udalostí"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Žiadne logické zväzky"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Žiadne systémové zmeny"
+
+#: pkg/sosreport/sosreport.jsx:499
+#, fuzzy
+#| msgid "No system modifications"
+msgid "No system reports."
+msgstr "Žiadne systémové zmeny"
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Žiadne aktualizácie"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Nebolo zadané užívateľské meno"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Žiadny"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr ""
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr ""
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "edostupné"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Nepripojené"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Nepripojené k Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Nepripojené k hostiteľovi"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough free space"
+msgstr "Pre zväčšenie nie je dostatok priestoru."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space"
+msgstr "Pre zväčšenie nie je dostatok priestoru."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+#, fuzzy
+#| msgid "Not enough space to grow."
+msgid "Not enough space to grow"
+msgstr "Pre zväčšenie nie je dostatok priestoru."
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Nenájdené"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Nenainštalovaný"
+
+#: pkg/networkmanager/network-interface.jsx:680
+#, fuzzy
+#| msgid "Failed to clone VM $0"
+msgid "Not permitted to configure network devices"
+msgstr "Nepodarilo sa klonovanie virt. stroja $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+#, fuzzy
+#| msgid "Failed to clone VM $0"
+msgid "Not permitted to configure realms"
+msgstr "Nepodarilo sa klonovanie virt. stroja $0"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Neoprávený k vykonaniu tejto akcie."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Nepripravené"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Nezaregistrované"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Nespustená"
+
+#: pkg/packagekit/autoupdates.jsx:346
+#, fuzzy
+#| msgid "No NFS mounts set up"
+msgid "Not set up"
+msgstr "Nie sú nastavené žiadne NFS pripojenia"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Nezosynchronizované"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Poznámka"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Notebook"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Oznámenie a závažnejšie"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Vyskytlo sa $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Vyskytlo sa medzi $0 a $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#, fuzzy
+#| msgid "Occurances"
+msgid "Occurrences"
+msgstr "Výskyty"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "Ok"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Pôvodná heslová fráza"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Pôvodné heslo"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Pôvodné heslo nebolo prijaté"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "Na pripojenom zariadení"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Pri chybe"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Keď bude Cockpit nainštalovaný, povoľte ho pomocou \"systemctl enable --now "
+"cockpit.socket\"."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Využíva sa len $0 z $1."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Sú povolené iba písmena, čísla a znaky :_.@-"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Iba núdzové"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Ooops!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Operácia '$operation' na $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Možnosti"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Alebo použite pribalený prehliadač"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Iný"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Iné voľby"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Výstup"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Prehľad"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+#, fuzzy
+#| msgid "Overview"
+msgid "Overwrite"
+msgstr "Prehľad"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+#, fuzzy
+#| msgid "Overwrite existing data with zeros"
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Prepísať existujúce dáta nulami"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+#, fuzzy
+#| msgid "ID"
+msgid "PID"
+msgstr "ID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Chýba balík cockpit-pcp a preto nie sú k dispozícií historické metriky"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Informácie o balíku"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit zhavaroval"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit nie je nainštalovaný"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit ohlásil chybový kód $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Balíčky"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Názov stránky"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Rodič"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Súčasť"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Súčasť $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Oddiel"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Oddiel na $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+#, fuzzy
+#| msgid "Volume size is $0. Content size is $1."
+msgid "Partition size is $0. Content size is $1."
+msgstr "Veľkosť zväzku je $0. Veľkosť obsahu je $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Vytváranie oddielov"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Oddiely"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Pasívny"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Heslová fráza"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "Heslová fráza nemôže byť prázdna"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Heslová fráza nemôže byť prázdna"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+#, fuzzy
+#| msgid "Passphrase cannot be empty"
+msgid "Passphrase from any other key slot"
+msgstr "Heslová fráza nemôže byť prázdna"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Zmazanie heslovej fráze môže brániť odomknutiu $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Heslové frázy sa líšia"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Heslo"
+
+#: pkg/shell/credentials.jsx:259
+#, fuzzy
+#| msgid "Solution applied successfully"
+msgid "Password changed successfully"
+msgstr "Riešenie bolo úspešne aplikované"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Skončenie platnosti hesla"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+#, fuzzy
+#| msgid "Name cannot be longer than $0 characters"
+msgid "Password is longer than 256 characters"
+msgstr "Názov nemôže byť dlhší ako $0 znakov"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Heslo nie je prijatelné"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Heslo je príliš slabé"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Heslo musí byť zmenené"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Heslo nebolo prijaté"
+
+#: pkg/shell/credentials.jsx:274
+#, fuzzy
+#| msgid "Password"
+msgid "Password tip"
+msgstr "Heslo"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Vložiť"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+#, fuzzy
+#| msgid "Internal error"
+msgid "Paste error"
+msgstr "Interná chyba"
+
+#: pkg/networkmanager/wireguard.jsx:215
+#, fuzzy
+#| msgid "Use existing"
+msgid "Paste existing key"
+msgstr "Použiť existujúci"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Cesta"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:88
+#, fuzzy
+#| msgid "Directory"
+msgid "Path to directory"
+msgstr "Zložka"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Cesta k súboru"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Cesty"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Pozastaviť"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr ""
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr ""
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Trvalá"
+
+#: pkg/users/delete-group-dialog.js:33
+#, fuzzy
+#| msgid "Delete $0"
+msgid "Permanently delete $0 group?"
+msgstr "Zmazať $0"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+#, fuzzy
+#| msgid "Delete $0"
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Zmazať $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Delete $0"
+msgid "Permanently delete subvolume $0?"
+msgstr "Zmazať $0"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Prístup odmietnutý"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Permisívny"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Fyzické"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+#, fuzzy
+#| msgid "Logical volume"
+msgid "Physical Volumes"
+msgstr "Logický zväzok"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:279
+#, fuzzy
+#| msgid "Key slots with unknown types can not be edited here"
+msgid "Physical volumes can not be resized here"
+msgstr "Sloty kľúčov s neznámym typom tu nie je možné upraviť"
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Vybrať dátum"
+
+#: pkg/systemd/service-details.jsx:184
+#, fuzzy
+#| msgid "Systemd units"
+msgid "Pin unit"
+msgstr "Systemd jednotky"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr ""
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr ""
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr ""
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr ""
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Nainštalujte $0 balík"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr ""
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr ""
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Zadajte stroj, ku ktorému sa chcete pripojiť"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr ""
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr ""
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:464
+#, fuzzy
+#| msgid "Old passphrase"
+msgid "Pool passphrase"
+msgstr "Pôvodná heslová fráza"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Port"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Prenosný"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Porty"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Vytvoriť oddiel"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Dĺžka eor"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Pripravuje sa"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Prítomné"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+#, fuzzy
+#| msgid "Preserve"
+msgid "Preserve"
+msgstr "Zachovať"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Pekný názov stroja"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr ""
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Primárny"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Priorita"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:213
+#, fuzzy
+#| msgid "Private"
+msgid "Private key"
+msgstr "Súkromný"
+
+#: pkg/shell/superuser.jsx:194
+#, fuzzy
+#| msgid "Domain administrator name"
+msgid "Problem becoming administrator"
+msgstr "Meno správcu domény"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Detaily o probléme"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr ""
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr ""
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr ""
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr ""
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr ""
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Účel"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (prekladanie)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (zrkadlenie)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (prekladanie zrkadiel)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (vyhradená parita)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (distribuovaná parita)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (dvojitá distribuovaná parita)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID skriňa"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID level"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr ""
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Náhodná"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Rozsah"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Rank"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Čítanie"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Iba na čítanie"
+
+#: pkg/storaged/plot.jsx:96
+#, fuzzy
+#| msgid "Reading"
+msgid "Reading"
+msgstr "Čítanie"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Číta sa..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Pripravené"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Pripraviť"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Skutočný názov stroja"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Skutočný názov stroja môže obsahovať iba malé písmená, číslice, pomlčky a "
+"bodky (pri použití subdomén)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Skutočný názov stroja musí byť 64 znakov alebo menej"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+#, fuzzy
+#| msgid "Save and reboot"
+msgid "Reapply and reboot"
+msgstr "Uložiť a reštartovať"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Reštartovať"
+
+#: pkg/packagekit/updates.jsx:614
+#, fuzzy
+#| msgid "Restart recommended"
+msgid "Reboot after completion"
+msgstr "Doporučuje sa reštart"
+
+#: pkg/packagekit/updates.jsx:1525
+#, fuzzy
+#| msgid "Restart recommended"
+msgid "Reboot recommended"
+msgstr "Doporučuje sa reštart"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr ""
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Prijímanie"
+
+#: pkg/static/login.html:165
+#, fuzzy
+#| msgid "Edit hosts"
+msgid "Recent hosts"
+msgstr "Upraviť hostiteľov"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr ""
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Znovu pripojiť"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Zotavuje sa"
+
+#: pkg/storaged/jobs-panel.jsx:70
+#, fuzzy
+#| msgid "Recovering RAID device $target"
+msgid "Recovering MDRAID device $target"
+msgstr "Zotavuje sa RAID zariadenia $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr ""
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Odmieta sa pripojenie. Stroj nie je známy"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Odmieta sa pripojenie. Kľúč stroja sa líši"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Odmieta sa pripojenie. Kľúč stroja nie je známy"
+
+#: pkg/networkmanager/wireguard.jsx:224
+#, fuzzy
+#| msgid "General"
+msgid "Regenerate"
+msgstr "Všeobecné"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+#, fuzzy
+#| msgid "Generating report"
+msgid "Regenerating initrd"
+msgstr "Generuje sa hlásenie"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Registrovať…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Znova načítať"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr ""
+
+#: pkg/systemd/services.jsx:233
+#, fuzzy
+#| msgid "Reading"
+msgid "Reloading"
+msgstr "Čítanie"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remove service $0"
+msgid "Remote over NFS, $0"
+msgstr "Odstrániť službu $0"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remove service $0"
+msgid "Remote over SSH, $0"
+msgstr "Odstrániť službu $0"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Odstránenia:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Odobrať"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Odstrániť službu $0 zo zóny $1"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Odstrániť Tang server s kľúčami?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Odstrániť zariadenie"
+
+#: pkg/static/login.js:634
+#, fuzzy
+#| msgid "Remove device"
+msgid "Remove host"
+msgstr "Odstrániť zariadenie"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+#, fuzzy
+#| msgid "Remove device"
+msgid "Remove item"
+msgstr "Odstrániť zariadenie"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+#, fuzzy
+#| msgid "Logical volume"
+msgid "Remove missing physical volumes?"
+msgstr "Logický zväzok"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:441
+#, fuzzy
+#| msgid "Repeat passphrase"
+msgid "Remove passphrase?"
+msgstr "Zopakujte heslovú frázu"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Odstrániť službu $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr ""
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Oboberá sa"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Odstraňuje sa $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Odstránenímním $0 sa preruší spojenie so serverom a zneprístupní sa tak "
+"rozhranie pre jeho správu."
+
+#: pkg/storaged/jobs-panel.jsx:66
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "Removing $target from MDRAID device"
+msgstr "Odstraňuje sa $target z RAID zariadenia"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr ""
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Premenovať"
+
+#: pkg/storaged/stratis/pool.jsx:168
+#, fuzzy
+#| msgid "Create partition"
+msgid "Rename Stratis pool"
+msgstr "Vytvoriť oddiel"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Rename filesystem"
+msgstr "Súborový systém"
+
+#: pkg/users/group-actions.jsx:39
+#, fuzzy
+#| msgid "Volume group"
+msgid "Rename group"
+msgstr "Skupina zväzkov"
+
+#: pkg/users/rename-group-dialog.jsx:60
+#, fuzzy
+#| msgid "Volume group $0"
+msgid "Rename group $0"
+msgstr "Skupina zväzkov $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr ""
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Premenováva sa $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+#, fuzzy
+#| msgid "Logical volume of $0"
+msgid "Repair logical volume $0"
+msgstr "Logický zväzok na $0"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Opravuje sa $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Opakovať"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Opakovať každý mesiac"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Zopakujte heslovú frázu"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Opakovať každý týždeň"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Nahlásiť"
+
+#: pkg/sosreport/sosreport.jsx:306
+#, fuzzy
+#| msgid "Report"
+msgid "Report label"
+msgstr "Nahlásiť"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Nahlásiť do ABRT Analytics"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Nahlásené; nie su dostupné žiadne odkazy"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Nahlasovanie sa nepodarilo"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Nahlasovanie bolo zrušené"
+
+#: pkg/sosreport/sosreport.jsx:496
+#, fuzzy
+#| msgid "Reports:"
+msgid "Reports"
+msgstr "Hlásenia:"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Hlásenia:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr ""
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Vyžadovať zmenu hesla v $0"
+
+#: pkg/users/account-create-dialog.js:119
+#, fuzzy
+#| msgid "Require password change on $0"
+msgid "Require password change on first login"
+msgstr "Vyžadovať zmenu hesla v $0"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Vyžadované"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Vyžadované "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Vyžaduje"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Pre úpravu sa vyžadujú administrátorské práva"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Rekvizita"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Vyhradená pamäť"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Reset"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Resetovať heslo"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Mení sa veľkosť $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Reštart"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Reštartovať služby"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Reštartovať služby..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Reštartuje sa"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Obnovuje sa spojenie"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Obnoviť chod"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+#, fuzzy
+#| msgid "encryption"
+msgid "Reuse existing encryption"
+msgstr "Šifrovanie"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Vyhodnocovanie záznamov udalostí"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Round robin"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Trasy"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+#, fuzzy
+#| msgid "Run image"
+msgid "Run at"
+msgstr "Spustiť obraz"
+
+#: pkg/sosreport/sosreport.jsx:293
+#, fuzzy
+#| msgid "View report"
+msgid "Run new report"
+msgstr "Zobraziť hlásenie"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+#, fuzzy
+#| msgid "Report"
+msgid "Run report"
+msgstr "Nahlásiť"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr ""
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Beží"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+#, fuzzy
+#| msgid "time"
+msgid "Runtime"
+msgstr "čas"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux je vypnutý na tomto systéme"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux je vypnutý na tomto systéme."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr ""
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "Riešenie problémov SELinux"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr ""
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH kľúč"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr ""
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH kľúče"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr ""
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr ""
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr ""
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP priorita"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Soboty"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Uložiť"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Uložiť a reštartovať"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Uložiť zmeny"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Šetriť priestor komprimovaním jednotlivých blokov pomocou LZ4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Šetriť priestor ukladaním rovnakých dátových blokov iba raz"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Uloženie novej heslovej frázy vyžaduje odomknutie disku. Zadajte súčasnú "
+"heslovú frázu k disku."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr ""
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr ""
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "Počítač so zapäčatenou skriňou"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Hľadať"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+#, fuzzy
+#| msgid "Search"
+msgid "Search domain"
+msgstr "Hľadať"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr ""
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:280
+#, fuzzy
+#| msgid "Minute needs to be a number between 0-59"
+msgid "Second needs to be a number between 0-59"
+msgstr "Minúta musí byť číslo medzi 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Sekundy"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Kľúče zabezpečeného shellu"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Bezpečne sa vymazáva $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Sú k dispozícií bezpečnostné aktualizácie"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Iba bezpečnostné aktualizácie"
+
+#: pkg/packagekit/autoupdates.jsx:365
+#, fuzzy
+#| msgid "Security updates available"
+msgid "Security updates will be applied $0 at $1"
+msgstr "Sú k dispozícií bezpečnostné aktualizácie"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Vybrať"
+
+#: pkg/systemd/logs.jsx:321
+#, fuzzy
+#| msgid "Identifier"
+msgid "Select a identifier"
+msgstr "Identifikátor"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+#, fuzzy
+#| msgid "Select"
+msgid "Select method"
+msgstr "Vybrať"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Poslať"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Vysielanie"
+
+#: pkg/storaged/drive/drive.jsx:123
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "Serial number"
+msgid "Serial number"
+msgstr "Sériové číslo"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Server"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Adresa server"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Adresa server nemôže byť prázdna."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Server nemôže byť prázdny."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Server zavrel spojenie."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Služba"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Služba má chybu"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Záznamy udalostí služby"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Služby"
+
+#: pkg/storaged/dialog.jsx:1153
+#, fuzzy
+#| msgid "Service is starting"
+msgid "Services using the location"
+msgstr "Služba sa spúšťa"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Sedenie"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Relácia čoskoro skončí"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Nastaviť"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Nastaviť názov stroja"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Partition of $0"
+msgid "Set partition type of $0"
+msgstr "Oddiel na $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Nastaviť heslo"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Nastaviť čas"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Nastaviť na"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Nastaviť"
+
+#: pkg/users/password-dialogs.js:245
+#, fuzzy
+#| msgid "Set password"
+msgid "Set weak password"
+msgstr "Nastaviť heslo"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Nastavenie sa líši od nastaveného stavu a bude vrátené pri ďalšom "
+"reštartovaní."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Nastavuje sa"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Nastavenia"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Závažnosť"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Zdielané"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+#, fuzzy
+#| msgid "shell"
+msgid "Shell"
+msgstr "shell"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Shellový skript"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+#, fuzzy
+#| msgid "Show all threads"
+msgid "Show all $0 rows"
+msgstr "Ukázať všetky vlákna"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Ukázať všetky vlákna"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+#, fuzzy
+#| msgid "Confirm password"
+msgid "Show confirmation password"
+msgstr "Potvrdiť heslo"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Zobraziť otlačky"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:139
+#, fuzzy
+#| msgid "New password"
+msgid "Show password"
+msgstr "Nové heslo"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Zmenšiť"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+#, fuzzy
+#| msgid "partition"
+msgid "Shrink partition"
+msgstr "oddiel"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Zmenšiť oddiel"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Vypnúť"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Vypnúť"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr ""
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Single rank"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Veľkosť"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Veľkosť je príliš veľká"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Veľkosť musí byť číslo"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Veľkosť musí byť aspoň $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Preskočiť hlavnú navigáciu"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Preskočiť na bo"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Slot"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Slot $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Zachytený stav"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Sokety"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Aktualizácie software"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Riešenie bolo úspešne aplikované"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Riešenie sa nepodarilo"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Riešenia"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr ""
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Zoradené od najmenej po najviac dôverihodné"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Miesto-šetriaci počítač"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr ""
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Náhradný"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Konkrétny čas"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Rýchlosť"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Stabilná"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Spustiť"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Spustiť a zapnúť"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Spustiť multipath"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Spustiť službu"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr ""
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Spustené"
+
+#: pkg/storaged/jobs-panel.jsx:64
+#, fuzzy
+#| msgid "Starting RAID device $target"
+msgid "Starting MDRAID device $target"
+msgstr "Spúšťa sa RAID zariadenie $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr ""
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Stav"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Statická"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Stav"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr ""
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Lepkavé"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Zastaviť"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Zastaviť zariadenie"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Ukončiť upravovanie hostiteľov"
+
+#: pkg/sosreport/sosreport.jsx:275
+#, fuzzy
+#| msgid "Create report"
+msgid "Stop report"
+msgstr "Vytvoriť hlásenie"
+
+#: pkg/storaged/jobs-panel.jsx:63
+#, fuzzy
+#| msgid "Stopping RAID device $target"
+msgid "Stopping MDRAID device $target"
+msgstr "Zastavuje sa RAID zariadenie $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr ""
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Úložisko"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr ""
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Záznamy udalostí úložiska"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr ""
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr ""
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block device"
+msgstr "$0 Blokové zariadenie"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+#, fuzzy
+#| msgid "$0 block device"
+msgid "Stratis block devices"
+msgstr "$0 Blokové zariadenie"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Stratis filesystem"
+msgstr "Súborový systém"
+
+#: pkg/storaged/stratis/pool.jsx:300
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Stratis filesystems"
+msgstr "Súborový systém"
+
+#: pkg/storaged/stratis/pool.jsx:361
+#, fuzzy
+#| msgid "Filesystem"
+msgid "Stratis filesystems pool"
+msgstr "Súborový systém"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+#, fuzzy
+#| msgid "$0 Storage pool"
+#| msgid_plural "$0 Storage pools"
+msgid "Stratis pool"
+msgstr "$0 fond úložiska"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr ""
+
+#: pkg/lib/cockpit-components-password.jsx:97
+#, fuzzy
+#| msgid "Set password"
+msgid "Strong password"
+msgstr "Nastaviť heslo"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr ""
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr ""
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Podskriňa"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Sub-Notebook"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr ""
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "Zväzok sa nepodarilo vytvoriť"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr ""
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Nedele"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Odkladanie"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "Cannot be enabled"
+msgid "Swap can not be resized here"
+msgstr "Nie je možné povoliť"
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr ""
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Vypnúť $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Vypnúť $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Zapnúť $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+#, fuzzy
+#| msgid "Administrative access"
+msgid "Switch to administrative access"
+msgstr "Prístup na úrovni správcu"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Prepnúť na obmedzený prístup"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Vypnutím $0 sa preruší spojenie so serverom a zneprístupní sa tak rozhranie "
+"pre jeho správu."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Zapnutím $0 sa preruší spojenie so serverom a zneprístupní sa tak rozhranie "
+"pre jeho správu."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Synchronizované"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Synchronizované s $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Synchronizuje sa"
+
+#: pkg/storaged/jobs-panel.jsx:71
+#, fuzzy
+#| msgid "Synchronizing RAID device $target"
+msgid "Synchronizing MDRAID device $target"
+msgstr "Synchronizuje sa RAID zariadenie $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Systém"
+
+#: pkg/sosreport/sosreport.jsx:520
+#, fuzzy
+#| msgid "System modifications"
+msgid "System diagnostics"
+msgstr "Modifikácie systému"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Informácie o systéme"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Systém je aktuálny"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Modifikácie systému"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Systémový čas"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemd jednotky"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tablet"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang server s kľúčami"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Cieľ"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Ciele"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Tým"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Port týmu"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Nastavenia portu týmu"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminál"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Ukončiť sedenie"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Vyskúšať konfiguráciu"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Test je k dispozícií len keď je spustená služba kdump."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Vyskúšať nastavenia kdump"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Skúša sa spojenie"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+#, fuzzy
+#| msgid "$0 is not available from any repository."
+msgid "The $0 package is not available from any repository."
+msgstr "$0 nie je k dispozícií v žiadom repozitári."
+
+#: pkg/storaged/overview/overview.jsx:122
+#, fuzzy
+#| msgid "The $0 package must be installed to create VDO devices."
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr ""
+"Aby bolo možné vytvárať VDO zariadenia je potrebné mať nainštalovaný balík "
+"$0."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+#, fuzzy
+#| msgid "The $0 package must be installed to create VDO devices."
+msgid "The $0 package must be installed."
+msgstr ""
+"Aby bolo možné vytvárať VDO zariadenia je potrebné mať nainštalovaný balík "
+"$0."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+#, fuzzy
+#| msgid "The $0 package must be installed to create VDO devices."
+msgid "The $0 package will be installed to create VDO devices."
+msgstr ""
+"Aby bolo možné vytvárať VDO zariadenia je potrebné mať nainštalovaný balík "
+"$0."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP adresa alebo názov stroja nemôže obsahovať prázdny znak."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "The MDRAID device is in a degraded state"
+msgstr "RAID pole je v degradovanom stave"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+#, fuzzy
+#| msgid "The RAID device must be running in order to remove disks."
+msgid "The MDRAID device must be running"
+msgstr "RAID zariadenie musí byť spustené aby bolo možné odobrať disky."
+
+#: pkg/shell/hosts_dialog.jsx:828
+#, fuzzy
+#| msgid ""
+#| "The SSH key ${key} of ${luser} on ${lhost} will be added to the ${afile} "
+#| "file of ${ruser} on ${rhost}."
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+"SSH kľúč ${key} užívateľa ${luser} na ${lhost} bude pridaný do súboru "
+"${afile} užívateľa ${ruser} na ${rhost}."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"SSH kľúč $0 bude sprístupnený po celú reláciu a bude k dispozícií tiež pre "
+"prihlasovanie se k ostatním strojom."
+
+#: pkg/shell/hosts_dialog.jsx:773
+#, fuzzy
+#| msgid ""
+#| "The SSH key for logging in to {{#strong}}{{full_address}}{{/strong}} is "
+#| "protected. You can log in with either your login password or by providing "
+#| "the password of the key at {{#strong}}{{key}}{{/strong}}."
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"SSH kľúč pre prihlasovanie sa k {{#strong}}{{full_address}}{{/strong}} je "
+"chránený. Môžete sa buď prihlásiť svojím prihlasovacím heslom alebo zadáním "
+"hesla ku kľúču {{#strong}}{{key}}{{/strong}}."
+
+#: pkg/shell/hosts_dialog.jsx:778
+#, fuzzy
+#| msgid ""
+#| "The SSH key for logging in to {{#strong}}{{full_address}}{{/strong}} is "
+#| "protected. You can log in with either your login password or by providing "
+#| "the password of the key at {{#strong}}{{key}}{{/strong}}."
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"SSH kľúč pre prihlasovanie sa k {{#strong}}{{full_address}}{{/strong}} je "
+"chránený. Môžete sa buď prihlásiť svojím prihlasovacím heslom alebo zadáním "
+"hesla ku kľúču {{#strong}}{{key}}{{/strong}}."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"Účet „$0“ bude pri najbližšom prihlásení vyzvaný k vynútenej zmene hesla"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Služba cockpit je zahrnutá automaticky"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr "Nakonfigurovaný stav nie je známy a môže sa zmeniť pri ďalšom štarte."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"Vytváranie tohoto VDO zariadenia nedokončilo a toto zariadenie nemôže byť "
+"použité."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"Prihlásený užívateľ nie je oprávnený k zobrazovaniu informácií o kľúčoch."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:368
+#, fuzzy
+#| msgid "The $0 could not be deleted"
+msgid "The file $0 will be deleted."
+msgstr "$0 sa nepodarilo zmazať"
+
+#: pkg/storaged/filesystem/utils.jsx:191
+msgid "The filesystem has no assigned mount point."
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:494
+#, fuzzy
+#| msgid "Show fingerprints"
+msgid "The fingerprint should match:"
+msgstr "Zobraziť otlačky"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr ""
+
+#: pkg/users/group-create-dialog.js:83
+#, fuzzy
+#| msgid "MTU must be a positive number"
+msgid "The group ID must be positive integer"
+msgstr "MTU musí byť kladné číslo"
+
+#: pkg/users/group-create-dialog.js:66
+#, fuzzy
+#| msgid ""
+#| "The user name can only consist of letters from a-z, digits, dots, dashes "
+#| "and underscores."
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Užívateľské meno môže obsahovať iba písmená a-z, čísla, bodky, pomlčky a "
+"podtržníky."
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Heslo ku kľúču nemôže byť prázdne"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Heslá ku kľúču sa líšia"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "Zadaný kľúč nie je platný."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Posledný slot kľúča nemôže byť odobraný"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr ""
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr ""
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Nové heslo ku kľúču nemôže byť prázdne"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Heslo nemôže byť prázdne"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Heslá sa líšia"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Heslá sa líšia."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Server odmietol overiť „$0“ pomocou overenia heslom a nie sú k dispozícií "
+"žiadne ďalšie metódy overenia."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr ""
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Užívateľské meno môže obsahovať iba písmená a-z, čísla, bodky, pomlčky a "
+"podtržníky."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr ""
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr ""
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Pri pripojovaní k stroji sa vyskytla neočakávaná chyba."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr ""
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr ""
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr ""
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr ""
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+msgid "This device can not be used for the installation target."
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1130
+#, fuzzy
+#| msgid "This device is currently used for VDO devices."
+msgid "This device is currently in use."
+msgstr "Toto zariadenie sa momentálne využíva pre RAID zariadenia."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+#, fuzzy
+#| msgid "User name cannot be empty"
+msgid "This field cannot be empty"
+msgstr "Užívateľské meno nemôže byť prázdne"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr ""
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "Tento stroj bude reštartovný keď budú aktualizácie nainštalované."
+
+#: pkg/sosreport/sosreport.jsx:303
+#, fuzzy
+#| msgid "The collected information will be stored locally on the system."
+msgid "This information is stored only on the system."
+msgstr "Zozbierané informácie budú ukladané lokálne na systéme."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr ""
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr ""
+
+#: pkg/storaged/stratis/pool.jsx:348
+#, fuzzy
+#| msgid "The RAID array is in a degraded state"
+msgid "This pool is in a degraded state."
+msgstr "RAID pole je v degradovanom stave"
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Toto užívateľské meno už existuje"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr ""
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Toto vám umožní sa nabudúce prihlásiť bez hesla."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "štvrtky"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Čas"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Časová zóna"
+
+#: pkg/systemd/timer-dialog.jsx:155
+#, fuzzy
+#| msgid "Connection failed"
+msgid "Timer creation failed"
+msgstr "Pripojenie sa nepodarilo"
+
+#: pkg/systemd/service-details.jsx:725
+#, fuzzy
+#| msgid "Connection failed"
+msgid "Timer deletion failed"
+msgstr "Pripojenie sa nepodarilo"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Časovače"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Tip: Nastavte si heslo ku kľúču rovnaké ako pre prihlasovanie a budete se "
+"voči ostatným systémom overovať automaticky."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Dnes"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr ""
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+#, fuzzy
+#| msgid "Hide filters"
+msgid "Toggle filters"
+msgstr "Skryť filtre"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr ""
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Nástroje"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "5 služieb najviac vyťažujúcich procesor"
+
+#: pkg/metrics/metrics.jsx:963
+#, fuzzy
+#| msgid "Top 5 CPU services"
+msgid "Top 5 disk usage services"
+msgstr "5 služieb najviac vyťažujúcich procesor"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr ""
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Celková veľkosť: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Veža"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr ""
+
+#: pkg/networkmanager/plots.js:39
+#, fuzzy
+#| msgid "Team settings"
+msgid "Transmitting"
+msgstr "Nastavenia týmu"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Spúšťač"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr ""
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Spúšťače"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Riešiť problém"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Riešiť problém…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+#, fuzzy
+#| msgid "Untrusted host"
+msgid "Trust and add host"
+msgstr "Nedôveryhodnotný stroj"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Stupeň dôveryhodnosti"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Skúsiť znova"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "utorky"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr ""
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr ""
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr ""
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Typ"
+
+#: pkg/storaged/partitions/partition.jsx:165
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr ""
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr ""
+
+#: pkg/shell/hosts_dialog.jsx:770
+#, fuzzy
+#| msgid ""
+#| "Unable to log in to {{#strong}}{{full_address}}{{/strong}} using SSH key "
+#| "authentication. Please provide the password. You may want to set up your "
+#| "SSH keys for automatic login."
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Nepodarilo sa prihlásiť k {{#strong}}{{full_address}}{{/strong}} pomocou "
+"overenia sa SSH kľúčom. Prosím zadejte heslo. Ak sa chcete prihlasovať "
+"automaticky, nastavte SSH klúče."
+
+#: pkg/shell/hosts_dialog.jsx:768
+#, fuzzy
+#| msgid ""
+#| "Unable to log in to {{#strong}}{{full_address}}{{/strong}}. The host does "
+#| "not accept password login or any of your SSH keys."
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Nepodarilo sa prihlásiť k {{#strong}}{{full_address}}{{/strong}}. Hostiteľ "
+"nepríjma prihlasovanie heslom alebo žiaden z vašich SSH kľúčov."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr ""
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+#, fuzzy
+#| msgid "Encrypted logical volume of $0"
+msgid "Unable to repair logical volume $0"
+msgstr "Šifrovaný logický zväzok na $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+#, fuzzy
+#| msgid "Failed to add service"
+msgid "Unable to run fix: $0"
+msgstr "Nepodarilo sa pridať službu"
+
+#: pkg/kdump/kdump-view.jsx:209
+#, fuzzy
+#| msgid "Failed to add service"
+msgid "Unable to save settings"
+msgstr "Nepodarilo sa pridať službu"
+
+#: pkg/kdump/kdump-view.jsx:214
+#, fuzzy
+#| msgid "Failed to add service"
+msgid "Unable to save settings: $0"
+msgstr "Nepodarilo sa pridať službu"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr ""
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr ""
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Nedostupné"
+
+#: pkg/packagekit/kpatch.jsx:238
+#, fuzzy
+#| msgid "Unavailable"
+msgid "Unavailable packages"
+msgstr "Nedostupné"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr ""
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr ""
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Neočakávaná chyba"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+#, fuzzy
+#| msgid "Unrecognized data"
+msgid "Unformatted data"
+msgstr "Nerozpoznané dáta"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Jednotka"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Neznáme"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Neznáme \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Neznáme ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Neznáma aplikácia"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Neznáma konfigurácia"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr ""
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Neznámy názov služby"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Neznámy typ"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Odomknúť"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+#, fuzzy
+#| msgid "Do not mount automatically on boot"
+msgid "Unlock automatically on boot"
+msgstr "Nepripájať automaticky pri štarte systému"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr ""
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Encrypted data"
+msgid "Unlock encrypted Stratis pool"
+msgstr "Šifrované dáta"
+
+#: pkg/shell/credentials.jsx:309
+#, fuzzy
+#| msgid "Unlock key"
+msgid "Unlock key $0"
+msgstr "Odomknúť kľuč"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Odomyká sa $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+#, fuzzy
+#| msgid "Unlocking disk..."
+msgid "Unlocking disk"
+msgstr "Odomyká sa disk..."
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Nespravované zariadenia"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Odpojiť"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+#, fuzzy
+#| msgid "Unmount filesystem"
+msgid "Unmount filesystem $0"
+msgstr "Odpojiť súborový systém"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Odpojiť teraz"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Odpája sa $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Bez názvu"
+
+#: pkg/systemd/service-details.jsx:184
+#, fuzzy
+#| msgid "Systemd units"
+msgid "Unpin unit"
+msgstr "Systemd jednotky"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Nerozpoznané dáta"
+
+#: pkg/storaged/block/resize.jsx:306
+msgid "Unrecognized data can not be made smaller here"
+msgstr ""
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr ""
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+#, fuzzy
+#| msgid "Unsupported volume"
+msgid "Unsupported logical volume"
+msgstr "Nepodporovaný zväzok"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr ""
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Nedôveryhodnotný stroj"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Aktualizovať"
+
+#: pkg/packagekit/updates.jsx:754
+#, fuzzy
+#| msgid "Updates available"
+msgid "Update Success Table"
+msgstr "Sú dostupné aktualizácie"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "História aktualizácií"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+#, fuzzy
+#| msgid "Logout successful"
+msgid "Update was successful"
+msgstr "Odhlásenie bolo úspešné"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Aktualizované"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Sú dostupné aktualizácie"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "História aktualizácií"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Aktualizuje sa"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Aktualizuje sa stav..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Doba chodu"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Využitie"
+
+#: pkg/storaged/storage-controls.jsx:206
+#, fuzzy
+#| msgid "Image $0"
+msgid "Usage of $0"
+msgstr "Obraz $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Použiť"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+#, fuzzy
+#| msgid "Compression"
+msgid "Use compression"
+msgstr "Kompresia"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+#, fuzzy
+#| msgid "Deduplication"
+msgid "Use deduplication"
+msgstr "Deduplikácia"
+
+#: pkg/shell/credentials.jsx:133
+#, fuzzy
+#| msgid "ssh key"
+msgid "Use key"
+msgstr "ssh kľúč"
+
+#: pkg/users/account-create-dialog.js:114
+#, fuzzy
+#| msgid "User password"
+msgid "Use password"
+msgstr "Užívateľské heslo"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr ""
+
+#: pkg/sosreport/sosreport.jsx:322
+#, fuzzy
+#| msgid "Last login"
+msgid "Use verbose logging"
+msgstr "Posledné prihlásenie"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Využité"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Používateľ"
+
+#: pkg/users/account-create-dialog.js:104
+#, fuzzy
+#| msgid "User"
+msgid "User ID"
+msgstr "Používateľ"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:181
+#, fuzzy
+#| msgid "MTU must be a positive number"
+msgid "User ID must be a positive integer"
+msgstr "MTU musí byť kladné číslo"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr ""
+
+#: pkg/users/account-create-dialog.js:184
+#, fuzzy
+#| msgid "Name cannot be longer than $0 bytes"
+msgid "User ID must not be lower than $0"
+msgstr "Názov nemôže byť dlhší ako $0 bytov"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Užívateľské meno"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Užívateľské meno nemôže byť prázdne"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Užívateľské meno"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "encryption"
+msgid "Using LUKS encryption"
+msgstr "Šifrovanie"
+
+#: pkg/storaged/manifest.json:0
+#, fuzzy
+#| msgid "Edit Tang keyserver"
+msgid "Using Tang server"
+msgstr "Upraviť Tang server s kľúčami"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr ""
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO zariadenie $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN ID"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Overuje sa adresa"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Kontroluje sa overovací žetón"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Výrobca"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Overené"
+
+#: pkg/shell/hosts_dialog.jsx:489
+#, fuzzy
+#| msgid "Fingerprint"
+msgid "Verify fingerprint"
+msgstr "Odtlačok"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Overiť kľúč"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Overuje sa"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Verzia"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Veľmi bezpečne sa maže $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+#, fuzzy
+#| msgid "All logs"
+msgid "View all CPUs"
+msgstr "Všetky záznamy"
+
+#: pkg/metrics/metrics.jsx:803
+#, fuzzy
+#| msgid "All logs"
+msgid "View all disks"
+msgstr "Všetky záznamy"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+#, fuzzy
+#| msgid "All logs"
+msgid "View all logs"
+msgstr "Všetky záznamy"
+
+#: pkg/systemd/service-details.jsx:549
+#, fuzzy
+#| msgid "Filter services"
+msgid "View all services"
+msgstr "Filtrovať služby"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Ukázať automatizačný skript"
+
+#: pkg/metrics/metrics.jsx:1147
+#, fuzzy
+#| msgid "All logs"
+msgid "View detailed logs"
+msgstr "Všetky záznamy"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Ukázať podrobnosti o hardware"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+#, fuzzy
+#| msgid "View details and history"
+msgid "View login history"
+msgstr "Ukázať podrobnosti a históriu"
+
+#: pkg/storaged/stratis/pool.jsx:351
+#, fuzzy
+#| msgid "All logs"
+msgid "View logs"
+msgstr "Všetky záznamy"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+#, fuzzy
+#| msgid "View details and history"
+msgid "View metrics and history"
+msgstr "Ukázať podrobnosti a históriu"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr ""
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Webové stránky projektu"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Zobraziť hlásenie"
+
+#: pkg/packagekit/updates.jsx:619
+#, fuzzy
+#| msgid "All logs"
+msgid "View update log"
+msgstr "Všetky záznamy"
+
+#: pkg/systemd/hwinfo.jsx:299
+#, fuzzy
+#| msgid "You can not gain administrative access."
+msgid "Viewing memory information requires administrative access."
+msgstr "Nemáte oprávnenia pre získanie prístupu na úrovni správcu systému."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+#, fuzzy
+#| msgid "Firewall"
+msgid "Visit firewall"
+msgstr "Brána firewall"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Skupina zväzkov"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+#, fuzzy
+#| msgid "Logical volume"
+msgid "Volume group is missing physical volumes"
+msgstr "Logický zväzok"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Veľkosť zväzku je $0. Veľkosť obsahu je $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Čaká sa"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Čaká sa na podrobnosti..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Čaká sa na vstup…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr "Čaká sa až ostatné programy dokončia používanie správcu balíčkov..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Čaká sa na dokončenie ostatných operácií správy balíčkov"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Čaká sa na spustenie…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Vyžadované"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Vyžaduje"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Varovanie a závažnejšie"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+#, fuzzy
+#| msgid "Set password"
+msgid "Weak password"
+msgstr "Nastaviť heslo"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Webová konzola"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Webová konzola pre linuxové servery"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+#, fuzzy
+#| msgid "Web Console for Linux servers"
+msgid "Web Console will restart"
+msgstr "Webová konzola pre linuxové servery"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Webová konzola beží v režime obmedzeného prístupu."
+
+#: pkg/shell/shell-modals.jsx:66
+#, fuzzy
+#| msgid "Web Console"
+msgid "Web console logo"
+msgstr "Webová konzola"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Stredy"
+
+#: pkg/systemd/timer-dialog.jsx:246
+#, fuzzy
+#| msgid "Weeks"
+msgid "Weekly"
+msgstr "Týždne"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Týždne"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Kedy"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Biely"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr ""
+
+#: pkg/storaged/drive/drive.jsx:124
+#, fuzzy
+#| msgctxt "storage"
+#| msgid "World wide name"
+msgid "World wide name"
+msgstr "World wide name"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Zápis"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Prevažne zápis"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Zapisuje sa"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Chybné užívateľské meno alebo heslo"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr ""
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Áno"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr ""
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Nemáte oprávnenia na správu brány firewall."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr "Nemáte oprávnenia zobrazovať overené kľúče pre tento účet."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Boli ste odhlásený z dôvodu neaktivity."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Môžte skúsiť načítať skoršie záznamy."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+"Pre automatické prihlasovanie sa budete možno chcieť zmeniť heslo ku kľúču."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Musíte počkať dlhšie aby bolo možné zmeniť vaše heslo"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr ""
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr ""
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr ""
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Váš účet"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr ""
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr ""
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Zóna"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[binárne dáta]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[žiadne dáta]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "prístup"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "Aktívny"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "doplnok"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "doplnok"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+#, fuzzy
+#| msgid "Isolated network"
+msgid "after network"
+msgstr "Izolovaná sieť"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "aplikácie"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "Inventárny štítok"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "o"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1112
+#, fuzzy
+#| msgid "Create VDO device"
+msgid "backing device for VDO device"
+msgstr "Vytvoriť VDO zariadenie"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "zväzok"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "boot"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "most"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Stop device"
+msgid "btrfs device"
+msgstr "Zastaviť zariadenie"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Stop device"
+msgid "btrfs devices"
+msgstr "Zastaviť zariadenie"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+msgid "btrfs subvolume"
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+msgid "btrfs subvolumes"
+msgstr ""
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "volume"
+msgid "btrfs volume"
+msgstr "zväzok"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "oprava chyby"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "bajty"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+#, fuzzy
+#| msgid "Cache"
+msgid "cache"
+msgstr "Vyrovnávacia pamäť"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+#, fuzzy
+#| msgid "Change"
+msgid "change"
+msgstr "Zmeniť"
+
+#: pkg/metrics/metrics.jsx:654
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "cockpit-podman is not installed"
+msgstr "Cockpit nie je nainštalovaný"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "Príkaz"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "konzola"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "výpis jadra"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "procesor"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "pád"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+#, fuzzy
+#| msgid "$0 data"
+msgid "data"
+msgstr "$0 dáta"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "dátum"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+#, fuzzy
+#| msgid "Deactivate"
+msgid "deactivate"
+msgstr "Deaktivovať"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "ladenie"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+#, fuzzy
+#| msgid "Delete"
+msgid "delete"
+msgstr "Zmazať"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "vypnúť"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "disk"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "disky"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "Doména"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "jednotka"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "upraviť"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "povoliť"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+#, fuzzy
+#| msgid "Encrypted $0"
+msgid "encrypted"
+msgstr "Šifrované $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "Šifrovanie"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "vylepšenie"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "Chyba"
+
+#: pkg/packagekit/autoupdates.jsx:354
+#, fuzzy
+#| msgid "every day"
+msgid "every Friday"
+msgstr "každý deň"
+
+#: pkg/packagekit/autoupdates.jsx:350
+#, fuzzy
+#| msgid "every day"
+msgid "every Monday"
+msgstr "každý deň"
+
+#: pkg/packagekit/autoupdates.jsx:355
+#, fuzzy
+#| msgid "every day"
+msgid "every Saturday"
+msgstr "každý deň"
+
+#: pkg/packagekit/autoupdates.jsx:356
+#, fuzzy
+#| msgid "every day"
+msgid "every Sunday"
+msgstr "každý deň"
+
+#: pkg/packagekit/autoupdates.jsx:353
+#, fuzzy
+#| msgid "every day"
+msgid "every Thursday"
+msgstr "každý deň"
+
+#: pkg/packagekit/autoupdates.jsx:351
+#, fuzzy
+#| msgid "every day"
+msgid "every Tuesday"
+msgstr "každý deň"
+
+#: pkg/packagekit/autoupdates.jsx:352
+#, fuzzy
+#| msgid "Wednesday"
+msgid "every Wednesday"
+msgstr "Streda"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "každý deň"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "rozšírenie"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr ""
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "Súborový systém"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "firewall"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr ""
+
+#: pkg/packagekit/kpatch.jsx:270
+#, fuzzy
+#| msgid "Mount read only"
+msgid "for current kernel only"
+msgstr "Pripojiť iba na čítanie"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "Formátovať"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+#, fuzzy
+#| msgctxt "<date> from <host>"
+#| msgid "$0 from $1"
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "$0 z $1"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+#, fuzzy
+#| msgctxt "<date> on <terminal>"
+#| msgid "$0 on $1"
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "$0 na $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "grafy"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "hardware"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "história"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "stroj"
+
+#: pkg/storaged/drive/drive.jsx:65
+#, fuzzy
+#| msgid "iSCSI target"
+msgid "iSCSI Drive"
+msgstr "iSCSI cieľ"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+#, fuzzy
+#| msgid "iSCSI targets"
+msgid "iSCSI drives"
+msgstr "iSCSI ciele"
+
+#: pkg/storaged/iscsi/session.jsx:45
+#, fuzzy
+#| msgid "Add iSCSI portal"
+msgid "iSCSI portal"
+msgstr "Pridať iSCSI portál"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+#, fuzzy
+#| msgid "On failure"
+msgid "ignore failure"
+msgstr "Pri chybe"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "vo väčšine prehliadačov"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+#, fuzzy
+#| msgid "Content"
+msgid "inconsistent"
+msgstr "Obsah"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#, fuzzy
+#| msgid "Initialize"
+msgid "initialize"
+msgstr "Inicializovať"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "inštalovať"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "rozhranie"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "neplatné: viacero definovaných cieľov"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "žurnál"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "manuálové stránky k journalctl"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "stav kdump"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "kľúče"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "prihlásenie"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "stroj"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "maska"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr ""
+
+#: pkg/storaged/dialog.jsx:1111
+#, fuzzy
+#| msgid "Member of RAID device"
+msgid "member of MDRAID device"
+msgstr "Člen RAID zariadenia"
+
+#: pkg/storaged/dialog.jsx:1113
+#, fuzzy
+#| msgid "Create partition"
+msgid "member of Stratis pool"
+msgstr "Vytvoriť oddiel"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "pamäť"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "metriky"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "zmiernenie"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+#, fuzzy
+#| msgid "Problem details"
+msgid "more details"
+msgstr "Detaily o probléme"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "mount"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "sieť"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+#, fuzzy
+#| msgid "Mount at boot"
+msgid "never mount at boot"
+msgstr "Pripojiť pri štarte"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+#, fuzzy
+#| msgid "ssh server is empty"
+msgid "nfs export is empty"
+msgstr "ssh server je prázdny"
+
+#: pkg/kdump/kdump-client.js:125
+#, fuzzy
+#| msgid "ssh server is empty"
+msgid "nfs server is empty"
+msgstr "ssh server je prázdny"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "prednosť (nice)"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "žiaden"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "z $0 procesoru"
+msgstr[1] "z $0 procesorov"
+msgstr[2] "z $0 procesorov"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+#, fuzzy
+#| msgctxt "<date> on <terminal>"
+#| msgid "$0 on $1"
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "$0 na $1"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "Operačný systém"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "os"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "balík"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "oddiel"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "heslo"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+#, fuzzy
+#| msgid "password"
+msgid "password quality"
+msgstr "heslo"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "umiestnenie"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "výkon"
+
+#: pkg/storaged/dialog.jsx:1110
+#, fuzzy
+#| msgid "LVM volume group"
+msgid "physical volume of LVM2 volume group"
+msgstr "LVM skupina zväzkov"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "zásuvný model"
+
+#: pkg/metrics/metrics.jsx:1833
+#, fuzzy
+#| msgid "$0 service has failed"
+#| msgid_plural "$0 services have failed"
+msgid "pmlogger.service has failed"
+msgstr "$0 služba zhavarovala"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service nebeží"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr ""
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "port"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "napájanie"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "iba na čítanie"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "odporúčaný"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr ""
+
+#: pkg/storaged/utils.js:934
+#, fuzzy
+#| msgid "Removing $target from RAID device"
+msgid "remove from MDRAID"
+msgstr "Odstraňuje sa $target z RAID zariadenia"
+
+#: pkg/storaged/utils.js:904
+msgid "remove from btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "reštartovať"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "role"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "beží na $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "bezpečnosť"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "sériové"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "služba"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "shell"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "ukázať menej"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "ukázať viac"
+
+#: pkg/storaged/block/resize.jsx:566
+#, fuzzy
+#| msgid "Shrink"
+msgid "shrink"
+msgstr "Zmenšiť"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "vypnúť"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "soket"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "riešenie"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "detaily riešenia"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+#, fuzzy
+#| msgid "Reporting failed"
+msgid "sos report failed"
+msgstr "Nahlasovanie sa nepodarilo"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr ""
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "ssh server je prázdny"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr ""
+
+#: pkg/storaged/filesystem/utils.jsx:181
+#, fuzzy
+#| msgid "On failure"
+msgid "stop boot on failure"
+msgstr "Pri chybe"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+msgid "stopped"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "cieľ"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "tým"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "čas"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "časovač"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "jednotka"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "neznáme"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "neznámy cieľ"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "odmaskovať"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "odpojiť"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr ""
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "užívateľ"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "username"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#, fuzzy
+#| msgid "No description"
+msgid "using key description $0"
+msgstr "Bez popisu"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "Verzia"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "zväzok"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "varovanie"
+
+#: pkg/networkmanager/wireguard.jsx:84
+#, fuzzy
+#| msgid "PackageKit is not installed"
+msgid "wireguard-tools package is not installed"
+msgstr "PackageKit nie je nainštalovaný"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "áno"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "zóna"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Služba kdump nie je nainštalovaná. Uistite sa, že balík kexec-tools je "
+#~ "nainštalovaný."
+
+#~ msgid "Service is running"
+#~ msgstr "Služba je spustená"
+
+#~ msgid "Service is starting"
+#~ msgstr "Služba sa spúšťa"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Služba je zastavená"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Služba sa zastavuje"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (šifrované)"
+
+#, fuzzy
+#~| msgid "$0 Storage pool"
+#~| msgid_plural "$0 Storage pools"
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 fond úložiska"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 vyrovnávacia pamäť"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 k dispozícií"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "Pred odobraním tohoto disku je potrebné pridať náhradný."
+
+#~ msgid "Backing device"
+#~ msgstr "Podkladové zariadenie"
+
+#~ msgid "Block"
+#~ msgstr "Blok"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Kapacita"
+
+#~ msgid "Content"
+#~ msgstr "Obsah"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Zariadenie"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Súbor zariadenia"
+
+#~ msgid "Devices"
+#~ msgstr "Zariadenia"
+
+#~ msgid "Drives"
+#~ msgstr "Jednotky"
+
+#, fuzzy
+#~| msgid "Filesystem"
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Súborový systém"
+
+#~ msgid "Filesystems"
+#~ msgstr "Súborové systémy"
+
+#~ msgid "Free"
+#~ msgstr "Voľno"
+
+#, fuzzy
+#~| msgid "Keyserver"
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Server s kľúčami"
+
+#, fuzzy
+#~| msgid "RAID member"
+#~ msgid "LVM2 member"
+#~ msgstr "RAID člen"
+
+#~ msgid "Locked devices"
+#~ msgstr "Zamknuté zariadenia"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Model"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "Podpora pre NFS nie je nainštalovaná"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Nie sú nastavené žiadne NFS pripojenia"
+
+#~ msgid "No drives attached"
+#~ msgstr "Nie sú pripojené žiadne jednotky"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Nie sú nastavené žiadne iSCSI ciele"
+
+#, fuzzy
+#~| msgid "Block device for filesystems"
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Blokové zariadenie pre súborové systémy"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Iné dáta"
+
+#, fuzzy
+#~| msgid "$0 block device"
+#~ msgid "Partitioned block device"
+#~ msgstr "$0 Blokové zariadenie"
+
+#, fuzzy
+#~| msgid "Passphrase"
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Heslová fráza"
+
+#~ msgid "Pool"
+#~ msgstr "Úložisko"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID level"
+
+#~ msgid "RAID member"
+#~ msgstr "RAID člen"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Ukázať $0 zariadenie"
+#~ msgstr[1] "Ukázať $0 zariadenia"
+#~ msgstr[2] "Ukázať $0 zariadení"
+
+#~ msgid "Show all"
+#~ msgstr "Ukázať všetko"
+
+#~ msgid "Source"
+#~ msgstr "Zdroj"
+
+#, fuzzy
+#~| msgid "$0 Storage pool"
+#~| msgid_plural "$0 Storage pools"
+#~ msgid "Start pool"
+#~ msgstr "$0 fond úložiska"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Cesta k ISO súboru na súborovom systéme hostiteľa"
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Stav"
+
+#, fuzzy
+#~| msgid "Create partition"
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Vytvoriť oddiel"
+
+#, fuzzy
+#~| msgid "Interfaces"
+#~ msgid "Stratis member"
+#~ msgstr "Rozhrania"
+
+#, fuzzy
+#~| msgid "$0 Storage pool"
+#~| msgid_plural "$0 Storage pools"
+#~ msgid "Stratis pool $0"
+#~ msgstr "$0 fond úložiska"
+
+#~ msgid "Support is installed."
+#~ msgstr "Podpora je nainštalovaná."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr ""
+#~ "RAID zariadenie musí byť spustené aby bolo možné pridať náhradné disky."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "Posledný disk RAID zariadenia nemôže byť odobraný."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Nerozpoznané dáta"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Využitie"
+
+#, fuzzy
+#~| msgid "Used by"
+#~ msgid "Used for"
+#~ msgstr "Používané"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "Podklad pre VDO"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO zariadenie"
+
+#~ msgid "Volume"
+#~ msgstr "Zväzok"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automaticky (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Prijať kľúč a pripojiť"
+
+#, fuzzy
+#~| msgid "Tang keyserver"
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Tang server s kľúčami"
+
+#, fuzzy
+#~| msgid "New passphrase"
+#~ msgid "Use a passphrase"
+#~ msgstr "Nová heslová fráza"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitová mapa"
+
+#, fuzzy
+#~| msgctxt "storage-id-desc"
+#~| msgid "Encrypted data"
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Šifrované dáta"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Vrchol diskového V/V"
+
+#~ msgid "Event"
+#~ msgstr "Udalosť"
+
+#, fuzzy
+#~| msgid "Reviewing logs"
+#~ msgid "Event logs"
+#~ msgstr "Vyhodnocovanie záznamov udalostí"
+
+#~ msgid "ssh key"
+#~ msgstr "ssh kľúč"
+
+#, fuzzy
+#~| msgid "Mount at boot"
+#~ msgid "Never mount at boot"
+#~ msgstr "Pripojiť pri štarte"
+
+#, fuzzy
+#~| msgid "Not found"
+#~ msgid "Unit not found"
+#~ msgstr "Nenájdené"
+
+#~ msgid "Validating key"
+#~ msgstr "Overuje sa kľúč"
+
+#~ msgid "Container administrator"
+#~ msgstr "Správca kontajnerov"
+
+#~ msgid "Roles"
+#~ msgstr "Role"
+
+#~ msgid "Server administrator"
+#~ msgstr "Správca serveru"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Diagnostické hlásenia"
+
+#~ msgid "Software Updates"
+#~ msgstr "Aktualizácie software"
+
+#~ msgid "Update log"
+#~ msgstr "Záznam udalosti aktualizácie"
+
+#, fuzzy
+#~| msgid "Zone"
+#~ msgid "$0 Zone"
+#~ msgstr "Zóna"
+
+#, fuzzy
+#~| msgid "Diagnostic reports"
+#~ msgid "Create diagnostic report"
+#~ msgstr "Diagnostické hlásenia"
+
+#~ msgid "Done!"
+#~ msgstr "Hotovo!"
+
+#~ msgid "Download report"
+#~ msgstr "Stiahnuť výkaz"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Nebol vytvorený žiaden archív."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Potvrďte zmazanie $0"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "iba na čítanie"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB celkom"
+
+#~ msgid "Apply"
+#~ msgstr "Použiť"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Ak chcete heslo aj tak použiť, kliknite na $0 ešte raz."
+
+#~ msgid "Access"
+#~ msgstr "Prístup"
+
+#, fuzzy
+#~| msgid "Logical size"
+#~ msgid "Logical Size"
+#~ msgstr "Logická veľkosť"
+
+#~ msgid "Security updates "
+#~ msgstr "Bezpečnostné aktualizácie "
+
+#~ msgid "Updates "
+#~ msgstr "Aktualizácie "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Voliteľné)"
+
+#~ msgid "Active since"
+#~ msgstr "Aktívny od"
+
+#~ msgid "Affected locations"
+#~ msgstr "Ovplyvnené umiestnenia"
+
+#~ msgid "Process"
+#~ msgstr "Proces"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and delete"
+#~ msgstr "Formát prihlasovania"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and format"
+#~ msgstr "Formát prihlasovania"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and grow"
+#~ msgstr "Formát prihlasovania"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and initialize"
+#~ msgstr "Formát prihlasovania"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and shrink"
+#~ msgstr "Formát prihlasovania"
+
+#, fuzzy
+#~| msgid "Remove device"
+#~ msgid "Remove and stop device"
+#~ msgstr "Odstrániť zariadenie"
+
+#, fuzzy
+#~| msgid "This device is currently used for VDO devices."
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Toto zariadenie sa momentálne využíva pre RAID zariadenia."
+
+#, fuzzy
+#~| msgid ""
+#~| "This device is currently used for RAID devices. Proceeding will remove "
+#~| "it from its RAID devices."
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Toto zariadenie sa momentálne využíva pre RAID zariadenia. Pokračovaním "
+#~ "ho z nich odoberiete."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Toto zariadenie sa momentálne využíva pre RAID zariadenia."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Toto zariadenie sa momentálne využíva pre RAID zariadenia. Pokračovaním "
+#~ "ho z nich odoberiete."
+
+#, fuzzy
+#~| msgid "This device is currently used for VDO devices."
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Toto zariadenie sa momentálne využíva pre RAID zariadenia."
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and delete"
+#~ msgstr "Odpojiť teraz"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and format"
+#~ msgstr "Odpojiť teraz"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and grow"
+#~ msgstr "Odpojiť teraz"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and initialize"
+#~ msgstr "Odpojiť teraz"
+
+#, fuzzy
+#~| msgid "Unmount now"
+#~ msgid "Unmount and shrink"
+#~ msgstr "Odpojiť teraz"
+
+#, fuzzy
+#~| msgid "On a mounted device"
+#~ msgid "Unmount and stop device"
+#~ msgstr "Na pripojenom zariadení"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 z $1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "Blokované"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Člen RAID zariadenia $0"
+
+#~ msgid "VDO backing"
+#~ msgstr "Podklad pre VDO"
+
+#, fuzzy
+#~| msgid "Create partition"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Vytvoriť oddiel"
+
+#, fuzzy
+#~| msgid "Mount point"
+#~ msgid "Mount Options"
+#~ msgstr "Bod pripojenia"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Je potrebný disk."
+
+#~ msgid "Disk"
+#~ msgstr "Disk"
+
+#~ msgid "Install VDO support"
+#~ msgstr "Nainštalovať podporu pre VDO"
+
+#~ msgid "System services"
+#~ msgstr "Systémové služby"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Prihlásiť na {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Nové heslá ku kľúču sa líšia"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Nepodarilo sa kontaktovať {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "{{host}} kľúč sa zmenil"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Vyčistiť nastavenia prípojného bodu"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Vytvorením tohto zväzku sa preruší spojenie so serverom a zneprístupní sa "
+#~ "tak rozhranie pre jeho správu."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Vytvorením tohto mostu sa preruší spojenie so serverom a zneprístupní sa "
+#~ "tak rozhranie pre jeho správu."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Vytvorením tohto týmu sa preruší spojenie so serverom a zneprístupní sa "
+#~ "tak rozhranie pre jeho správu."
+
+#~ msgid "IP settings"
+#~ msgstr "Nastavenia IP"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Vypnutím <b>$0</b> sa preruší spojenie so serverom a zneprístupní sa tak "
+#~ "rozhranie pre jeho správu."
+
+#~ msgid "Administrator password"
+#~ msgstr "Heslo správcu"
+
+#~ msgid "Computer OU"
+#~ msgstr "Organizačná jednotka počítača"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "Odstránením RAID zariadenia sa zmažú dáta na ňom."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "Zmazaním skupiny zväzkov sa zmažú všetky dáta na nej."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Neprepisovať existujúce dáta"
+
+#~ msgid "Erase"
+#~ msgstr "Zmazať"
+
+#~ msgid "More"
+#~ msgstr "Viac"
+
+#~ msgid "Not joined"
+#~ msgstr "Nepripojené"
+
+#~ msgid "One time password"
+#~ msgstr "Jednorázové heslo"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 z $1 na $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Hodiny musia byť čísla medzi 0 a 23"
+
+#~ msgid "Last login:"
+#~ msgstr "Posledné prihlásenie:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Minúty musia byť čísla medzi 0 a 59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "Od posledného úspešného prihlásenia došlo k $0 neúspešnému pokusu o "
+#~ "prihlásenie."
+#~ msgstr[1] ""
+#~ "Od posledného úspešného prihlásenia došlo k $0 neúspešným pokusom o "
+#~ "prihlásenie."
+#~ msgstr[2] ""
+#~ "Od posledného úspešného prihlásenia došlo k $0 neúspešným pokusom o "
+#~ "prihlásenie."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "napr. \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 aktívne zóny"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 výskyt"
+#~ msgstr[1] "$0 výskyty"
+#~ msgstr[2] "$0 výskytov"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Hľadať záznamy pomocou kombinácie pojmov:"
+
+#~ msgid "Show filters"
+#~ msgstr "Zobraziť filtre"
+
+#~ msgid "Text"
+#~ msgstr "Text"
+
+#~ msgid "e.g."
+#~ msgstr "napr."
+
+#~ msgid "log fields"
+#~ msgstr "položky záznamu udalosti"
+
+#~ msgid "qualifiers"
+#~ msgstr "kvalifikátory"
+
+#~ msgid "(none)"
+#~ msgstr "(žiadne)"
+
+#~ msgid "Create timers"
+#~ msgstr "Vytvoriť časovače"
+
+#~ msgid "Run"
+#~ msgstr "Spustiť"
+
+#, fuzzy
+#~| msgid "Select console type"
+#~ msgid "Select unit state"
+#~ msgstr "Vybrať typ konzole"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Ak tang-show-keys nie je k dispozícii, spustite nasledujúce:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Čo ak príkaz tang-show-keys nie je dostupný?"
+
+#~ msgid "key slot $0"
+#~ msgstr "slot pre kľúč $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Niečo sa nepodarilo"
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Automatické aktualizácie nie sú nastavené"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "Konfigurácia procesoru $0"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 Sieť"
+#~ msgstr[1] "$0 Siete"
+#~ msgstr[2] "$0 Sietí"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 je k dispozícií pre väčšinu operačných systémov. Pre inštaláciu "
+#~ "vyhľadejte v GNOME Software alebo spusťte nasledujúce:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 úprava nastavenia pamäti"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 Sieť"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 virt. procesor"
+#~ msgstr[1] "$0 virt. procesory"
+#~ msgstr[2] "$0 virt. procesorov"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 podrobnosti virt. procesoru"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "Nastavenie virtuálneho sieťového rozhrania $0"
+
+#~ msgid "Add network interface"
+#~ msgstr "Pridať sieťové rozhranie"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Pridať virtuálne sieťové rozhranie"
+
+#~ msgid "Additional"
+#~ msgstr "Ďalšie"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Adresa sa nenachádza v podsieti"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr "Zmazaním zachyteného stavu bude všetok ním zachytený obsah zmazaný."
+
+#~ msgid "Always attach"
+#~ msgstr "Vždy pripojiť"
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Spúšťať libvirt pri zavádzaní systému"
+
+#~ msgid "Autostart"
+#~ msgstr "Automatické spustenie"
+
+#~ msgid "Boot order"
+#~ msgstr "Poradie zavádzania"
+
+#~ msgid "Bus"
+#~ msgstr "Zbernica"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "Nepodarilo sa uložiť konfiguráciu procesoru"
+
+#~ msgid "CPU type"
+#~ msgstr "Typ procesoru"
+
+#~ msgid "Change firmware"
+#~ msgstr "Zmeniť firmware"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Zmeny sa prejavia až po vypnutí virt. stroja"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Vyberte operačný systém"
+
+#~ msgid "Clone"
+#~ msgstr "Klonovať"
+
+#~ msgid "Connect"
+#~ msgstr "Pripojiť"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Pripája sa k virtualizačnej službe"
+
+#~ msgid "Connection"
+#~ msgstr "Spojenie"
+
+#~ msgid "Console"
+#~ msgstr "Konzola"
+
+#~ msgid "Crashed"
+#~ msgstr "Havarovalo"
+
+#~ msgid "Create VM"
+#~ msgstr "Vytvoriť virt. stroj"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Vytvoriť nový virtuálny počítač"
+
+#~ msgid "Creating VM"
+#~ msgstr "Vytvára sa virt. stroj"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "Vytvára sa inštalácia virt. stroja"
+
+#~ msgid "Creation time"
+#~ msgstr "Čas vytvorenia"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Súčasná alokácia"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP rozsah"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Odstrániť súvisiace súbory úložiska:"
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Odpojené od sériovej konzoly. Kliknite na tlačítko Pripojiť."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Nepodarilo sa odpojiť disk $0 od virt. stroja $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Disk sa nepodarilo pripojiť"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Disk sa nepodarilo vytvoriť"
+
+#, fuzzy
+#~| msgid "Device file"
+#~ msgid "Disk image file"
+#~ msgstr "Súbor zariadenia"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Nastavenia disku nebolo možné uložiť"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "Zachytený stav iba disku"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "Doména zhavarovala"
+
+#~ msgid "Download an OS"
+#~ msgstr "Stiahnúť operačný systém"
+
+#~ msgid "Dying"
+#~ msgstr "Umiera"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Emulovaný stroj"
+
+#~ msgid "End"
+#~ msgstr "Koniec"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Je nutné vyplniť koniec"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Existujúcí obraz disku na súborovom systéme hostiteľa"
+
+#~ msgid "Expand"
+#~ msgstr "Rozbaliť"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Nepodarilo sa zmeniť firmware"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "Nepodarilo sa poslať klávesy Ctrl+Alt+$0 do virt. stroja $1"
+
+#~ msgid "File"
+#~ msgstr "Súbor"
+
+#~ msgid "Filter by name"
+#~ msgstr "Filtrovať podľa mena"
+
+#~ msgid "Firmware"
+#~ msgstr "Firmware"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Generovať automaticky"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Skryť ďalšie možnosti"
+
+#~ msgid "Host device"
+#~ msgstr "ariadenie hostiteľa"
+
+#~ msgid "Host name"
+#~ msgstr "Názov hostiteľa"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Je nutné vyplniť hostiteľa"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Podrobnosti o hypervizoru"
+
+#~ msgid "IP configuration"
+#~ msgstr "Konfigurácia IP"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 a IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4 sieť"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv4 sieť nemôže byť prázdna"
+
+#~ msgid "IPv4 only"
+#~ msgstr "Iba IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6 adresy"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6 sieť"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6 sieť nemôže byť prázdna"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Iba IPv6"
+
+#~ msgid "Idle"
+#~ msgstr "Neaktívne"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Okamžite spustiť virt. stroj"
+
+#~ msgid "Import"
+#~ msgstr "Importovať"
+
+#~ msgid "Import VM"
+#~ msgstr "Importovať virt. stroj"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "Importovať virt. stroj"
+
+#~ msgid "Initiator"
+#~ msgstr "Iniciátor"
+
+#~ msgid "Installation source"
+#~ msgstr "Zdroj inštalácie"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Zdroj inštalácie nemôže byť prázdny"
+
+#~ msgid "Installation type"
+#~ msgstr "Typ inštalácie"
+
+#~ msgid "Interface type"
+#~ msgstr "Typ rozhrania"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr "Ak nechcete vytvoriť účet root, heslo nevypĺňajte"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr "Ak nechcete vytvoriť účet root, heslo nevypĺňajte"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr "Ak nechcete vytvoriť účet root, heslo nevypĺňajte"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt nedetegoval na hostiteľovi žiadny nainštalovaný obraz UEFI/OVMF "
+#~ "firmvéru"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt alebo hypervízor nepodporuje UEFI"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Správa virtuálnych strojov"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Maximálna prenosová jednotka"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT na $0"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Je nutné vyplniť názov"
+
+#~ msgid "Name: "
+#~ msgstr "Názov: "
+
+#~ msgid "Netmask"
+#~ msgstr "Sieťová maska"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Zavádzanie zo siete (PXE)"
+
+#~ msgid "Networks"
+#~ msgstr "Siete"
+
+#~ msgid "New volume name"
+#~ msgstr "Názov nového zväzku"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "Na tomto stroji nie sú spustené ani definované žiadne virt. stroje"
+
+#, fuzzy
+#~| msgid "No connection available."
+#~ msgid "No connection available"
+#~ msgstr "Nie je k dispozícií žiadne pripojenie."
+
+#~ msgid "No network devices"
+#~ msgstr "Žiadne sieťové zariadenia"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "Na tomto stroji nie je definovaná žiadna sieť"
+
+#~ msgid "No networks available"
+#~ msgstr "Nie sú k dispozícií žiadne siete"
+
+#, fuzzy
+#~| msgid "No network is defined on this host"
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Na tomto stroji nie je definovaná žiadna sieť"
+
+#~ msgid "No state"
+#~ msgstr "Žiadny stav"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Žiadne virtuálne siete"
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Žiadne (izolovaná sieť)"
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Možné upraviť len pokiaľ je hosť vypnutý"
+
+#~ msgid "Open"
+#~ msgstr "Otvoriť"
+
+#~ msgid "Operating system"
+#~ msgstr "Operačný systém"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "Operácia sa vykonáva"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "Nadradený zachytený stav"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Cesta k ISO súboru na súborovom systéme hostiteľa"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Cesta k ISO súboru na súborovom systéme hostiteľa"
+
+#~ msgid "Paused"
+#~ msgstr "Zastavené"
+
+#~ msgid "Persistence"
+#~ msgstr "Trvalosť"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Fyzické diskové zariadenie"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Fyzické diskové zariadenie na hostiteľovi"
+
+#~ msgid "Plug"
+#~ msgstr "Pripojiť"
+
+#~ msgid "Prefix"
+#~ msgstr "Predpona"
+
+#~ msgid "Product"
+#~ msgstr "Produkt"
+
+#~ msgid "Profile"
+#~ msgstr "Profil"
+
+#~ msgid "Protocol"
+#~ msgstr "Protokol"
+
+#~ msgid "Revert"
+#~ msgstr "Vrátiť späť"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Vrátiť späť do podoby v zachytenom stave $0"
+
+#~ msgid "Root password"
+#~ msgstr "Heslo pre účet root"
+
+#~ msgid "Route to $0"
+#~ msgstr "Trasa na $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Spustiť bezobslužnú inštaláciu"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Spustiť pri štarte stroja"
+
+#, fuzzy
+#~| msgid "SPICE TLS Port"
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE TLS port"
+
+#, fuzzy
+#~| msgid "SPICE Address"
+#~ msgid "SPICE address"
+#~ msgstr "SPICE adresa"
+
+#, fuzzy
+#~| msgid "SPICE Port"
+#~ msgid "SPICE port"
+#~ msgstr "SPICE port"
+
+#~ msgid "Select console type"
+#~ msgstr "Vybrať typ konzole"
+
+#~ msgid "Send key"
+#~ msgstr "Poslať stlačenie klávesy"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Poslať nemaskovateľné prerušenie"
+
+#~ msgid "Serial console"
+#~ msgstr "Sériová konzola"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Nastaviť DHCP rozsah"
+
+#~ msgid "Set manually"
+#~ msgstr "Nastaviť ručne"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "Nastavanie hesiel pre užívateľa pre bezobslužnú inštálaciu vyžaduje "
+#~ "spustenie virtuálneho stroja pri jeho vytváraní"
+
+#~ msgid "Show additional options"
+#~ msgstr "Zobraziť ďalšie možnosti"
+
+#~ msgid "Shut off"
+#~ msgstr "Vypnúť"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "Vypnite virt. stroj aby bolo možné upraviť konfiguráciu firmvéru"
+
+#~ msgid "Shutting down"
+#~ msgstr "Vypína sa"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Nepodarilo sa zachytiť stav"
+
+#~ msgid "Source format"
+#~ msgstr "Zdrojový formát"
+
+#~ msgid "Source path"
+#~ msgstr "Zdrojová cesta"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Spustiť libvirt"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Je nutné vyplniť začiatok"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr "Vybraný operačný systém nepodporuje bezobslužnú inštaláciu"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr "Dočasné virt. stroje neumožnujú úpravy konfigurácie firmvéru"
+
+#~ msgid "Type ID"
+#~ msgstr "Iden. typu"
+
+#~ msgid "Unique name"
+#~ msgstr "Unikátne meno"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Neznámy firmware"
+
+#~ msgid "Unplug"
+#~ msgstr "Odpojiť"
+
+#~ msgid "Url"
+#~ msgstr "Url"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "Nastavenia virt. procesoru nebolo možné uložiť"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "Virt. stroj $0 už existuje"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "Virt. stroj $0 sa nepodarilo nútene reštartovať"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "Virt. stroj $0 sa nepodarilo nútene vypnúť"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "Virt. stroj $0 sa nepodarilo zmazať"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "Virt. stroj $0 sa nepodarilo nainštalovať"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "Virt. stroj $0 sa nepodarilo reštartovať"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "Virt. stroj $0 sa nepodarilo znovu začať"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "Nepodarilo sa poslať virt. stroju $0 nemaskovateľné prerušenie"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "Virt. stroj $0 sa nepodarilo vypnúť"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "Virt. stroj $0 sa nepodarilo naštartovať"
+
+#~ msgid "VM state"
+#~ msgstr "Stav virt. stroja"
+
+#, fuzzy
+#~| msgid "VNC TLS Port"
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC TLS port"
+
+#, fuzzy
+#~| msgid "VNC Address"
+#~ msgid "VNC address"
+#~ msgstr "VNC adresa"
+
+#~ msgid "VNC console"
+#~ msgstr "VNC konzola"
+
+#, fuzzy
+#~| msgid "VNC Port"
+#~ msgid "VNC port"
+#~ msgstr "VNC port"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Virtuálne stroje"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Virtuálne stroje"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Správa virtuálnych strojov"
+
+#~ msgid "Virtual network"
+#~ msgstr "Virtuálna sieť"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Vytvorenie virtuálnej siete sa nepodarilo"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Virtualizačná služba (libvirt) nie je aktívna"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Je potrebné vyplniť názov skupiny zväzkov"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Zapisovateľný"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Zapisovateľné a zdielané"
+
+#~ msgid "Yesterday"
+#~ msgstr "Včera"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "vypnuté"
+
+#~ msgid "down"
+#~ msgstr "vypnuté"
+
+#~ msgid "enabled"
+#~ msgstr "povolené"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "zariadenie hostiteľa"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "IQN iSCSI iniciátoru"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "IQN pre iSCSI cieľ"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "nie"
+
+#~ msgid "no state saved"
+#~ msgstr "žiaden uložený stav"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "presmerované zariadenie"
+
+#~ msgid "server"
+#~ msgstr "server"
+
+#~ msgid "up"
+#~ msgstr "spustené"
+
+#~ msgid "vCPU count"
+#~ msgstr "Počet virt. procesorov"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "Maximum virt. procesorov"
+
+#~ msgid "vCPUs"
+#~ msgstr "virt. procesory"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Balík virt-install musí byť nainštalovaný aby bolo možné klonovať "
+#~ "virtuálne stroje"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Balík virt-install musí byť nainštalovaný aby bolo možné vytvárať nové "
+#~ "virtuálne stroje"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Balík virt-install musí byť nainštalovaný aby bolo možné upravovať tento "
+#~ "atribút"
+
+#~ msgid "vm"
+#~ msgstr "virt. stroj"
+
+#~ msgid "Local install media"
+#~ msgstr "Lokálne inštalačné médium"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "Nastavte heslo pre účet správcu (root)"
+
+#~ msgid "21st"
+#~ msgstr "21."
+
+#~ msgid "22nd"
+#~ msgstr "22."
+
+#~ msgid "23rd"
+#~ msgstr "23."
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Po"
+
+#~ msgid "Friday"
+#~ msgstr "Piatok"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Hodina : Minúta"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "Hodina musí byť číslo medzi 0-23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Neplatný formát dátumu."
+
+#~ msgid "Invalid number."
+#~ msgstr "Neplatné číslo."
+
+#~ msgid "Monday"
+#~ msgstr "Pondelok"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Opakovať každú hodinu"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Opakovať každý rok"
+
+#~ msgid "Saturday"
+#~ msgstr "Sobota"
+
+#~ msgid "Sunday"
+#~ msgstr "Nedela"
+
+#~ msgid "Thursday"
+#~ msgstr "štvrtok"
+
+#~ msgid "Tuesday"
+#~ msgstr "utorok"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 aktualizácia"
+#~ msgstr[1] "$0 aktualizácie"
+#~ msgstr[2] "$0 aktualizácií"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 oprava zabezpečenia"
+#~ msgstr[1] "$1 opravy zabezpečenia"
+#~ msgstr[2] "$1 opráv zabezpečenia"
+
+#~ msgid "Restart now"
+#~ msgstr "Reštartovať teraz"
+
+#~ msgid "Configure"
+#~ msgstr "Nastaviť"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Sieť"
+
+#~ msgid "No updates pending"
+#~ msgstr "Žiadne čakajúce aktualizácie"
+
+#~ msgid "undefined"
+#~ msgstr "nedefinované"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Nesprávny kľúč stroja"
+
+#~ msgid "Address:"
+#~ msgstr "Adresa:"
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Stiahnuť MSI z $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Grafická konzola (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Grafická konzola v desktopovom prehliadači"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Pre tento virtuálny stroj nie je definovaná žiadna konzola."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid "Entry"
+#~ msgstr "Položka"
+
+#~ msgid "Dashboard"
+#~ msgstr "Prehľad"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Sieť"
+
+#~ msgid "Servers"
+#~ msgstr "Servery"
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bity"
+#~ msgstr[2] "$0 bitov"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 bajt"
+#~ msgstr[1] "$0 bajty"
+#~ msgstr[2] "$0 bajtov"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 bajt"
+#~ msgstr[1] "$0 bajty"
+#~ msgstr[2] "$0 bajtov"
+
+#, fuzzy
+#~| msgid "IP settings"
+#~ msgid "IP settings "
+#~ msgstr "Nastavenia IP"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "V Cockpitu sa vyskytla neočakávaná vnútorná chyba. <br/><br/>Môžete "
+#~ "skúsiť Cockpit reštartovať kliknutím na opätovné načítenie stránky vo "
+#~ "svojom prehliadači. Podrobnejšie informácie o tejto chybe sú vypísané v "
+#~ "javascript konzole (<b>Ctrl-Shift-J</b> vo väčšine prehliadačov)."
+
+#~ msgid "inactive"
+#~ msgstr "Neaktívne"
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid "2 min"
+#~ msgstr "2 min"
+
+#~ msgid "3 min"
+#~ msgstr "3 min"
+
+#~ msgid "4 min"
+#~ msgstr "4 min"
+
+#~ msgid "running"
+#~ msgstr "Beží"
+
+#, fuzzy
+#~ msgid "Add a new host"
+#~ msgstr "Pridať novú zónu"
+
+#, fuzzy
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Zadejte IP adresu alebo názov stroja"
+
+#~ msgid "$0 template"
+#~ msgstr "$0 Šablóna"
+
+#~ msgid "Author"
+#~ msgstr "Autor"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpitu sa nepodarilo prihlásiť k {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid "Command:"
+#~ msgstr "Príkaz:"
+
+#~ msgid "Commit image"
+#~ msgstr "Vytvoriť obraz"
+
+#~ msgid "Docker containers"
+#~ msgstr "Docker kontajnery"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker nie je nainštalovaný alebo aktivovaný na tomto systéme"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "Zadejte heslovú frázu pre pridanie identity do ssh-agenta"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Zadanie sem iného hesla znamená, že ho bude potreba zadávať vždy znova "
+#~ "pri pripojovaní k tomuto stroju"
+
+#~ msgid "Environment"
+#~ msgstr "Prostredie"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Chyba pri načítaní užívateľov: {{perm_failed}}"
+
+#~ msgid "Everything"
+#~ msgstr "Všetko"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Nepodarilo sa spustiť Docker: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Získať nové obrazy"
+
+#~ msgid "IP address:"
+#~ msgstr "IP adresa:"
+
+#~ msgid "Id"
+#~ msgstr "Id"
+
+#~ msgid "Id:"
+#~ msgstr "Id:"
+
+#~ msgid "Image"
+#~ msgstr "Obraz"
+
+#~ msgid "Image:"
+#~ msgstr "Obraz:"
+
+#~ msgid "Images"
+#~ msgstr "Obrazy"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Obrazy"
+
+#~ msgid "No running containers"
+#~ msgstr "Žiadne spustené kontajnery"
+
+#~ msgid "Start Docker"
+#~ msgstr "Spustiť Docker"
+
+#~ msgid "Up since $0"
+#~ msgstr "Beží od $0"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Účty"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Pridať ďalšie úložisko"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Ďalšie úložisko"
+
+#~ msgid "$0 GiB / $1 GiB"
+#~ msgstr "$0 GiB / $1 GiB"
diff --git a/po/sv.po b/po/sv.po
new file mode 100644
index 0000000..dec9356
--- /dev/null
+++ b/po/sv.po
@@ -0,0 +1,12578 @@
+# Göran Uddeborg <goeran@uddeborg.se>, 2018. #zanata
+# Göran Uddeborg <goeran@uddeborg.se>, 2019. #zanata
+# Peter von Weisz <daishan.swe@gmail.com>, 2019. #zanata
+# Jakob Sundberg <jakob@mress.se>, 2020.
+# Anonymous <noreply@weblate.org>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2024-02-05 13:36+0000\n"
+"Last-Translator: Luna Jernberg <bittin@reimu.nl>\n"
+"Language-Team: Swedish <https://translate.fedoraproject.org/projects/cockpit/"
+"main/sv/>\n"
+"Language: sv\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.3.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "# av användare"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU-kärna"
+msgstr[1] "$0 CPU-kärnor"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "Aktiv zon $0"
+msgstr[1] ""
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 tillgängligt"
+
+#: pkg/storaged/block/resize.jsx:266
+msgid "$0 can not be made larger"
+msgstr "$0 kan inte göras större"
+
+#: pkg/storaged/block/resize.jsx:263
+msgid "$0 can not be made smaller"
+msgstr "$0 kan inte göras mindre"
+
+#: pkg/storaged/block/resize.jsx:259
+msgid "$0 can not be resized"
+msgstr "$0 kan inte storleksändras"
+
+#: pkg/storaged/block/resize.jsx:255
+msgid "$0 can not be resized here"
+msgstr "$0 kan inte storleksändras"
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 styckesstorlek"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 kritisk träff"
+msgstr[1] "$0 träffar, inklusive kritiska"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 data + $1 extra använt av $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 dag"
+msgstr[1] "$0 dagar"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disk saknas"
+msgstr[1] "$0 diskar saknas"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disk saknas"
+msgstr[1] "$0 diskar saknas"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 diskar"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 dokumentation"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 fel"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 avslutade med kod $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 misslyckades"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 misslyckat inloggningsförsök"
+msgstr[1] "$0 misslyckade inloggningsförsök"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "$0 filesystem"
+msgstr "$0 filsystem"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 fritt"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 timme"
+msgstr[1] "$0 timmar"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 viktig träff"
+msgstr[1] "$0 träffar, inklusive viktiga"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 är en existerande fil"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 används"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 är inte tillgängligt från något förråd."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 nyckel ändrad"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 dödad med signal $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 träff med låg allvarlighet"
+msgstr[1] "$0 träffar med låg allvarlighet"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 minut"
+msgstr[1] "$0 minuter"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 måttlig träff"
+msgstr[1] "$0 träffar, inklusive måttliga"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 månad"
+msgstr[1] "$0 månader"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 mer..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 paket"
+msgstr[1] "$0 paket"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 paket behöver en omstart av systemet"
+msgstr[1] "$0 paket behöver en omstart av systemet"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 sida"
+msgstr[1] "$0 sidor"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+msgid "$0 partitions"
+msgstr "$0 partitioner"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 säkerhetsuppdatering tillgänglig"
+msgstr[1] "$0 säkerhetsuppdateringar tillgängliga"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 tjänsten har misslyckats"
+msgstr[1] "$0 tjänsterna har misslyckats"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 tjänst måste startas om"
+msgstr[1] "$0 tjänster måste startas om"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 fack återstår"
+msgstr[1] "$0 fack återstår"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 spik"
+msgstr[1] "$0 spikar"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0 synkroniserad"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 totalt"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 uppdatering tillgänglig"
+msgstr[1] "$0 uppdateringar tillgängliga"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 använt av $1 ($2 sparat)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 vecka"
+msgstr[1] "$0 veckor"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 kommer att installeras."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 år"
+msgstr[1] "$0 år"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "zon $0"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: krasch vid $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (från $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+msgid "(Not part of target)"
+msgstr "(Inte del av mål)"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+msgid "(no assigned mount point)"
+msgstr "(ingen tilldelad monteringspunkt)"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+msgid "(not mounted)"
+msgstr "(inte monterad)"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(rekommenderad)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", inklusive $1 säkerhetsfix"
+msgstr[1] ", inklusive $1 säkerhetsfixar"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 dag"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 timme"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 min"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 minut"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 vecka"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10:e"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11:e"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12:e"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13:e"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14:e"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "1 min"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15:e"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16:e"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17:e"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18:e"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19:e"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1:a"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 minuter"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20:e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21:a"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22:a"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23:e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24:e"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25:e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26:e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27:e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28:e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29:e"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2:a"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30:e"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31:a"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3:e"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 minuter"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4:e"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 min"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 minuter"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5:e"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 timmar"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 minuter"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6:e"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7:e"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8:e"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9:e"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "Ingen kompatibel version av Cockpit är installerad på $0."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "Ett filsystem med detta namn finns redan i denna pool."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "En grupp med detta namn finns redan"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr "En modern webbläsare krävs för säkerhet, pålitlighet och prestanda."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"En bindning kombinerar flera nätverksportar till en logisk port med högre "
+"hastighet eller tillförlitlighet."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"En ny SSH-nyckel på $0 kommer att skapas för $1 på $2 och den kommer att "
+"läggas till $3-filen på $4 på $5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Ett paket behöver en systemomstart för att uppdateringen skall få effekt:"
+msgstr[1] ""
+"Några paket behöver en systemomstart för att uppdateringen skall få effekt:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "En pool med detta namn finns redan."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"En tjänst måste startas om för att uppdateringarna ska träda i kraft:"
+msgstr[1] ""
+"Vissa tjänster måste startas om för att uppdateringarna ska träda i kraft:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "En volymgrupp med saknade fysiska volymer kan inte döpas om."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP-övervakning"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP-ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Om webkonsolen"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Frånvarande"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Acceptera nyckel och logga in"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "Acceptabelt lösenord"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Utgång av konto"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Kontot är inte tillgängligt eller kan inte redigeras."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Konton"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Åtgärd"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Åtgärder"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Aktivera"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr "Aktivera innan du ändrar storlek"
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Aktivera $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Aktivt"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Aktiv reserv"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Aktiva sidor"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Aktiv sedan "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Aktivt tillstånd"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Adaptiv lastbalansering"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Adaptiv lastbalansering av sändning"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Lägg till"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Lägg till $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "Lägg till DNS-server"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Lägg till nätverksbunden diskkryptering"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Lägg till Tang-nyckelserver"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Lägg till VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "Lägg till VPN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "Lägg till WireGuard VPN"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "Lägg till en bitmap"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Lägg till zon"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "Lägg till adress"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Lägg till blockenheter"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Lägg till bindning"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Lägg till brygga"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Lägg till disk"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Lägg till diskar"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Lägg till iSCSI-portal"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Lägg till nyckel"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "Lägg till nyckelserver"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Lägg till medlem"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Lägg till ny värd"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Lägg till zon"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "Lägg till lösenfras"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "Lägg till jämnlike"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+msgid "Add physical volume"
+msgstr "Lägg till fysisk volym"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Lägg till portar"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Lägg till portar till zonen $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Lägg till publik nyckel"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "Lägg till rutt"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "Lägg till sökdomän"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Lägg till tjänster"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Lägg till tjänster till zonen $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Lägg till tjänster till zon $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Lägg till team"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Lägg till zon"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "Lägger till \"$0\" till krypteringsalternativ"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Lägger till \"$0\" till filsystemsalternativ"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Att lägga till $0 kommer bryta anslutningen till servern, och kommer göra "
+"administrationsgränssnittet otillgängligt."
+
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Att lägga till en nyckelserver kräver upplåsning av poolen. Ange den "
+"existerande poollösenfrasen."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Att lägga till egna portar startar om firewalld . En omstart gör att "
+"osparade ändringar försvinner!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Lägger till nyckel"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Lägger till fysisk volym till $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Lägger till rd.neednet=1 till kärnans kommandorad"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Ytterligare DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Ytterligare DNS-sökdomäner $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Ytterligare åtgärder"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Ytterligare adress $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Ytterligare paket:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Ytterligare portar"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Adress"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Adress $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Adressen kan inte vara tom"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Adressen är inte en giltig URL"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Adresser"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "Adresser är inte korrekt formaterade"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Administration med Cockpits webbkonsol"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Administrativ tillgång"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr "Administrativ åtkomst krävs för att skapa och komma åt rapporter."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Administrativ åtkomst krävs"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Avancerad TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Efter"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Om du lämnar domänen kommer bara lokala användare kunna logga in på den här "
+"datorn. Det kan också påverka andra tjänster eftersom DNS och listan av "
+"betrodda CA kan ändras."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Efter systemstart"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Larm"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Larm och högre"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "alias"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Alla"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr "Alla $0 valda fysiska volymer behövs för den valda layouten."
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Alla uppdateringar"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Allt i ett"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Tillåt start (göm inte)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "Tillåtna IP-adresser"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Tillåtna adresser"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "Ytterligare $0 måste väljas"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Dokumentation för Ansibleroller"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Alla textsträngar i loggmeddelandena kan filtreras. Strängen kan också vara "
+"i form av ett reguljärt uttryck. Stöder även filtrering efter "
+"meddelandeloggfält. Dessa är mellanslagsseparerade värden, i formen "
+"FÄLT=VÄRDE, där värde kan vara kommaseparerad lista över möjliga värden."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Utseende"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "Applikationinformation saknas"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Program"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Programlista"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Verkställ och starta om"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Lägg på driftsuppdateringar av kärnan"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Lägg på denna lösning"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Tar ny policy i drift … Detta kan ta några minuter."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Lägger på lösning …"
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Lägger på uppdateringar"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Att lägga på uppdateringar misslyckades"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "Lämplig för kritiska monteringar, såsom /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Program"
+
+#: pkg/storaged/drive/drive.jsx:102
+msgid "Assessment"
+msgstr "Bedömning"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "MaskinID"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "Vid start"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Åtminstone $0 disk behövs."
+msgstr[1] "Åtminstone $0 diskar behövs."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "Åtminstone en blockenhet behövs."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Åtminstone en disk behövs."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr "Minst en förälder måste vara monterad skrivbar"
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "På minut"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "På sekund"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "Vid en specifik tidpunkt"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Attribut"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Granskningslogg"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Autentisera"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Autentiserar"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Autentisering"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Autentisering misslyckades"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Autentiseringen misslyckades: servern stängde ner förbindelsen"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Autentisering krävs för att utföra privilegierade uppgifter med Cockpits "
+"webbkonsol"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Autentisering krävs"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Auktorisera SSH-nyckel"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Auktoriserade publika SSH-nycklar"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Automatisk"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Automatiskt (endast DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Automatisk inloggning"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Automatiska uppdateringar"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Startar automatiskt"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Använder automatiskt NTP"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Använder automatiskt ytterligare NTP-servrar"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Använder automatiskt specifika NTP-servrar"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Automatiseringsskript"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Tillgängliga mål på $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Tillgängliga uppdateringar"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+msgid "BIOS boot partition"
+msgstr "BIOS uppstartspartition"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "BIOS-datum"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "BIOS-version"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Tilllbaka till konton"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Dålig"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Dålig inställning"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Balanserare"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Före"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Binder till"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Svart"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blad"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Bladhölje"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "Blockenhet"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Blockenhet för filsystem"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Blockenheter"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Blockerat"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Binda"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Start"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"Uppstart misslyckas om filsystemet inte monteras, vilket förhindrar "
+"fjärråtkomst"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "Uppstarten lyckas fortfarande när filsystemet inte monteras"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Bundet av"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Brygga"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Bryggport"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Bryggportinställningar"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Utsändning"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Trasig konfiguration"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr "Btrfs volym är monterad"
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Felrättningsuppdateringar tillgängliga"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Fel"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Bussexpansionschassi"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Genom att ändra lösenordet för SSH-nyckeln $0 till inloggningslösenordet $1 "
+"på $2, kommer nyckeln automatiskt att göras tillgänglig och du kan logga in "
+"på $3 utan lösenord i framtiden."
+
+#: pkg/static/login.html:61
+msgid "Bypass browser check"
+msgstr "Förbigå webbläsarkontroll"
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "CPU-säkerhet"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "CPU-säkerhetsinställningar"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "CPU-användning"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "CPU-användning/belastning"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Cache"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "Kan vara ett värdnamn, IP-adress, aliasnamn eller ssh:// URI"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr "Hittade inga loggar med dessa filter aktiva."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Avbryt"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Avbryt avstängning"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Avbryt omstart"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Kan inte slås på"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Kan inte ansluta till en okänd maskin"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Kan inte vidarebefordra inloggningsuppgifterna"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr ""
+"Kan inte ansluta till en domän eftersom realmd inte finns på detta system"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Kan inte schemalägga händelser som redan hänt"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Kapacitet"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Transport"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Ändra"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "Ändra kryptografisk policy"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Ändra värdnamn"
+
+#: pkg/storaged/overview/overview.jsx:152
+msgid "Change iSCSI initiater name"
+msgstr "Ändra namnet på iSCSI-initieraren"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Ändra iSCSI-initierarnamn"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+msgid "Change label"
+msgstr "Ändra etikett"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Ändra lösenfras"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Ändra lösenord"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Ändra prestandaprofil"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Ändra profil"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "Ändra skal"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Ändra systemtid"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Ändra lösenord av $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Ändra inställningarna"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Ändrade nycklar är ofta resultatet av en ominstallation av operativsystemet. "
+"En oväntad förändring kan dock indikera ett försök från tredje part att "
+"avlyssna din anslutning."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr "Att ändra partitionstyper kan förhindra att systemet startar upp."
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Att ändra inställningarna kommer bryta förbindelsen till servern, och kommer "
+"göra administrationsgränssnittet otillgängligt."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Kontrollera om det finns uppdateringar"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"Kontrollera att SHA-256- eller SHA-1-hash från kommandot matchar den här "
+"dialogrutan."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "Kontrollera nyckelhash med Tang-server."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Kontrollerar $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Kontrollerar IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+msgid "Checking MDRAID device $target"
+msgstr "Kontrollerar MDRAID-enhet $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Kontrollera och reparera MDRAID-enhet $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Söker efter $0 paket"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Söker efter NBDE-stöd i initrd"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Kontrollerar om det finns nya program"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Söker efter programuppdateringar..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Kontrollerar installerad programvara"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Kontrollerar relaterade processer"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Kontrollerar programvarustatus"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Välj språk som skall användas i programmet"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Styckesstorlek"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Klass"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Rensar upp för $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Nollställ \"Misslyckad start\""
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Nollställ alla filter"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Nollställ sökning"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Klar text enhet"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Klientprogramvara"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Stäng"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Stäng valda sidor"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Cockpit-autentisering är felaktigt konfigurerad."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "Cockpit konfiguration av NetworkManager och Firewalld"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit kunde inte kontakta den angivna värden."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit hade ett oväntat internt fel."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit är en serverhanterare som gör det lätt att administrera dina "
+"Linuxservrar via en webbläsare. Att hoppa mellan terminalen och "
+"webbverktyget är inget problem. En tjänst som startas via Cockpit kan "
+"stoppas via terminalen. Likaledes, om ett fel uppstår i terminalen kan det "
+"ses i Cockpits journalgränssnitt."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit är ett interaktivt administratörsgränssnitt för Linuxservrar."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit är inte kompatibelt med programvaran på systemet."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit är inte installerat"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit är inte installerat på systemet."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit är perfekt för nya systemadministratörer, låter dem lätt utföra "
+"enkla uppgifter såsom lagringsadministration, inspektion av journaler och "
+"att starta och stoppa tjänster. Du kan övervaka och administrera flera "
+"servrar på samma gång. Lägg bara till dem med ett enda klick och dina "
+"maskiner kommer se efter sina kompisar."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit kanske inte återges korrekt i din webbläsare"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Samla och paketera diagnostik och support data"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Samla kärnkraschdumpar"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Samla in mätvärden"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Färg"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "Kommaseparerade portar, intervall och tjänster accepteras"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Kommando"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Kommando hittades inte"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Kommentar"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Kommunikationen med tuned har misslyckats"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Kompakt PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Kompatibel med alla system och enheter (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Kompatibel med moderna system och hårddiskar > 2 TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Komprimera kraschdumpar för att spara utrymme"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Komprimering"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Villkoret $0=$1 uppfylldes inte"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Villkoret misslyckades"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Konfiguration"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Konfigurerar"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Konfigurerar IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Konfigurerar kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Konfigurerar systeminställningar"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Bekräfta"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Bekräfta raderingen av $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Bekräfta lösenord för nyckel"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Bekräfta nytt lösenord för nyckel"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Bekräfta nytt lösenord"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Bekräfta lösenord"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Bekräfta borttagning av $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Bekräfta borttagandet med en alternativ lösenfras"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Bekräfta stoppandet av $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "Står i konflikt med"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Konflikter"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Anslut automatiskt"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Anslut till"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Anslut till:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Anslut till SETroubleshoot-demonen …"
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Anslutningen till dbus misslyckades: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Ansluter till maskinen"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Anslutningsfel"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Anslutningen misslyckades"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Anslutningens tidsgräns överskreds."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "Anslutningen kommer gå förlorad"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Består av"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Kontaktade domänen"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Innehåller:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Fortsätt"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Fortsätt session"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Styrning"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Styrning"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Styrenhet"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Konvertibel"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Kopierade"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Kopiera"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Kopiera till urklipp"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Kärna $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "Kunde inte kontakta $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Kraschdumpplats"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Rapportera krascher"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Kraschsystem"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Skapa"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "Skapa LVM2 volymgrupp"
+
+#: pkg/storaged/overview/overview.jsx:145
+msgid "Create MDRAID device"
+msgstr "Skapa en MDRAID-enhet"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Skapa en RAID-enhet"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Skapa Stratis lagringspool"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Skapa en ny SSH-nyckel och auktorisera den"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Skapa en ögonblicksbild av filsystem $0"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Skapa konto med icke-unik UID"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Skapa konto med svagt lösenord"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Skapa och ändra ägarskap av hemkatalog"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Skapa och montera"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Create and start"
+msgstr "Skapa och starta"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Skapa filsystem"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Skapa den"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Skapa en logisk volym"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Skapa ett nytt konto"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Skapa nytt filsystem"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Skapa en ny grupp"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Skapa en ny logisk volym"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Skapa en ny uppgiftsfil med detta innehåll."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+msgid "Create new thinly provisioned logical volume"
+msgstr "Skapa en ny logisk volym med tunn provisionering"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "Skapa endast"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Skapa en partition"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Skapa partition på $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Skapa partitionstabell"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Skapa en ögonblicksbild"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Skapa en ögonblicksbild och montera"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Skapa en ögonblicksbild endast"
+
+#: pkg/storaged/overview/overview.jsx:174
+msgid "Create storage device"
+msgstr "Skapa lagringsenhet"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+msgid "Create subvolume"
+msgstr "Skapa undervolym"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Skapa en tunn volym"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Skapa en timer"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Skapa en volymgrupp"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Skapad"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Skapar LVM2 volymgrupp $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+msgid "Creating MDRAID device $target"
+msgstr "Skapar MDRAID-enhet $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "Skapar VDO-enhet"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Skapar ett filsystem på $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Skapar en logisk volym på $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Skapar en partition på $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Skapar en ögonblicksbild av $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Att skapa detta $0 kommer bryta anslutningen till servern, och kommer göra "
+"administrationsgränssnittet otillgängligt."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Kritisk och högre"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Kryptografisk policy är en systemkomponent som konfigurerar de grundläggande "
+"kryptografiska undersystemen, innefattande protokollen TLS, IPSec, SSH, "
+"DNSSec och Kerberos."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "Kryptografisk policy"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "Kryptografipolicyn är inkonsekvent"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Skift-J"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Nuvarande uppstart"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Aktuell högsta CPU-användning"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Används just nu"
+
+#: pkg/kdump/kdump-view.jsx:535
+msgid "Currently not supported"
+msgstr "Stöds inte för närvarande"
+
+#: pkg/storaged/partitions/partition.jsx:146
+msgid "Custom"
+msgstr "Anpassad"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "Anpassad kryptografisk policy"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Anpassade monteringsalternativ"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Egna portar"
+
+#: pkg/storaged/partitions/partition.jsx:180
+msgid "Custom type"
+msgstr "Anpassad typ"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Egna zoner"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "DEFAULT med verifiering av SHA-1-signatur tillåten."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS-sökdomäner"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "DNS-sökdomäner $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Dagligen"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Fara varning:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Mörk"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Data"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Data använt"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"Data kommer att lagras som två kopior och även på ett alternerande sätt på "
+"de valda fysiska volymerna, för att förbättra både tillförlitlighet och "
+"prestanda. Minst fyra volymer måste väljas."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"Data kommer att lagras som två eller flera kopior på de valda fysiska "
+"volymerna, för att förbättra tillförlitligheten. Minst två volymer måste "
+"väljas."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+"Data kommer att lagras på de valda fysiska volymerna på ett alternerande "
+"sätt för att förbättra prestandan. Minst två volymer måste väljas."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"Data kommer att lagras på de valda fysiska volymerna så att en av dem kan gå "
+"förlorad utan att data påverkas. Minst tre volymer måste väljas."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+"Data kommer att lagras på de valda fysiska volymerna så att en av dem kan gå "
+"förlorad utan att data påverkas. Data lagras också på ett alternerande sätt "
+"för att förbättra prestandan. Minst tre volymer måste väljas."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+"Data kommer att lagras på de valda fysiska volymerna så att upp till två av "
+"dem kan gå förlorade samtidigt utan att data påverkas. Data lagras också på "
+"ett alternerande sätt för att förbättra prestandan. Minst fem volymer måste "
+"väljas."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+"Data kommer att lagras på de valda fysiska volymerna utan ytterligare "
+"redundans eller prestandaförbättringar."
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Datumspecifikationen skall ha formatet ÅÅÅÅ-MM-DD hh:mm:ss. Alternativt "
+"förstås strängarna ”yesterday”, ”today”, ”tomorrow”. ”now” refererar till "
+"den aktuella tiden. Slutligen kan relativa tider anges, inledda med ”-” "
+"eller ”+”"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Avaktivera"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Inaktivera logisk volym $0/$1?"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Avaktiverar"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Avaktiverar $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Felsökning och högre"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Minska med en"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "Dedikerad paritet (RAID 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Avduplicering"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Standard"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Fördröjning"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Fördröjning måste vara en siffra"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Ta bort"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Ta bort $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Ta bort konto"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Ta bort filer"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Ta bort grupp"
+
+#: pkg/storaged/stratis/pool.jsx:292
+msgid "Delete pool"
+msgstr "Ta bort pool"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Ta bort rapporten permanent?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Att ta bort $0 kommer bryta anslutningen till servern, och kommer göra "
+"administrationsgränssnittet otillgängligt."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Tar bort $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Tar bort LVM2 volymgruppen $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "Om du tar bort en Stratis-pool raderas all data som den innehåller."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "Att ta bort ett filsystem kommer att ta bort all data i det."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "Att ta bort en logisk volym kommer att ta bort all data i den."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "Att ta bort en partition kommer att ta bort all data i den."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Om du raderar raderas all data på en MDRAID-enhet."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "Radering raderar all data på en VDO-enhet."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "Om du raderar raderas all data i en volymgrupp."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "Om du raderar raderas all data på denna undervolym och alla dess barn."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "Radering kommer ta bort följande filer:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Beskrivning"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Skrivbord"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Frånkopplingsbar"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Detaljer"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Utveckling"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Enhet"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Enhetsfil"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Enheten är skrivskyddad"
+
+#: pkg/storaged/block/other.jsx:60
+msgid "Device number"
+msgstr "Enhetsnummer"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Diagnostikrapporter"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Katalog"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Katalogen $0 är inte skrivbar eller finns inte."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Stäng av hyperthreading"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Stäng av brandväggen"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Avaktivera tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Avaktiverad"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Tillåt inte interaktivt lösenord"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Tillåt inte lösenordsautentisering"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Hindra körning (göm)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Koppla ifrån"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Frånkopplad"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Disk-I/O"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Disken är OK"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Disken är på väg att gå sönder"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Disklösenfras"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Diskar"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Diskanvändning"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "Avfärda"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Avfärda $0 larm"
+msgstr[1] "Avfärda $0 larm"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Avvisa valda aviseringar"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Visningsspråk"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "Distribuerad paritet (RAID 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "Montera inte"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Montera inte automatiskt vid start"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Dockningsstation"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Startar inte automatiskt"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "Monterar inte vid uppstart"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Domän"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Domän"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Domänadress"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Domänadministratörens namn"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Domänadministratörens lösenord"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Domän kunde inte kontaktas"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Domän stödjs inte"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Upprepa inte"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "Dubbel distribuerad paritet (RAID 6)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Hämta"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Hämta en ny webbläsare gratis"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Hämtat"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Hämtar"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Hämtar $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Enhet"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Dubbelrad"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+msgid "EFI system partition"
+msgstr "EFI-systempartition"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Redigera"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Redigera /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Redigera Tang-nyckelserver"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "Redigera VLAN-inställningar"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "Redigera WireGuard VPN"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "Redigera bondinställningar"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "Redigera brygginställningar"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Redigera anpassad tjänst i $0 zon"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Redigera värd"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Redigera värdar"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Redigera motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Monteringspunkt"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Redigera regler och zoner"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Redigera tjänst"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Redigera tjänst $0"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "Redigera Team-inställningar"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Redigera användare"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Att redigera en nyckel kräver ett fritt fack"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Matar ut $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "Inbäddad PC"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Tom"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Töm"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Tomt lösenord"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Tömmer $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Aktivera"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Aktivera eller avaktivera denna enhet"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Aktivera tjänst"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Slå på brandväggen"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Aktiverad"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "Aktiverar $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Kryptera data"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Kryptera data med en Tang-nyckelserver"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "Kryptera data med en lösenfras"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Krypterad"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Krypterad $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+msgid "Encrypted Stratis pool"
+msgstr "Krypterad Stratis-pool"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Krypterad logisk volym av $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Krypterad partition av $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Kryptering"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Krypteringsalternativ"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Krypteringslösenfras"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Krypteringstyp"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Slutade"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "Ändpunkt"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"Ändpunkt som fungerar som en \"server\" måste anges som värd:port, annars "
+"kan den lämnas tom."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Tvingande"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Förbättringsuppdateringar är tillgängliga"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Ange en giltig MAC-adress"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Hela subnätet"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Ingång vid $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Raderar $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Errata"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Fel"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Fel och högre"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Fel har uppstått"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "Fel vid installation $0: PackageKit är inte installerat"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Felmeddelande"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "Ett fel inträffade när semanage sökte efter ändringar i systemet"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Fel när auktoriserade nycklar sparades: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Fel när SELinux-läge ställdes in: ”$0”"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet-MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet-MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "Exakt $0 fysiska volymer måste väljas"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+"Exakt $0 fysiska volymer måste väljas, en för varje remsa av den logiska "
+"volymen."
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Exempel: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Exempel: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Utmärkt lösenord"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Expansionschassin"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Upphör konto på"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Upphör konto på $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Exportera"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Exportera till nätverk"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Utökad information"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Utökad partition"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS är inte korrekt aktiverat"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS med ytterligare begränsningar av allmänna kriterier."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Misslyckades"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Misslyckades att lägga till en maskin: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Kunde inte lägga till port"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Kunde inte lägga till tjänst"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Kunde inte lägga till zon"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Misslyckades att ändra lösenord"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "Misslyckades att konfigurera PCP"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Misslyckades att radera larmet: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Misslyckades att avaktivera tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Misslyckades med att avaktivera en tuned-profil"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Misslyckades att redigera en maskin: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Kunde inte redigera tjänst"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "Misslyckades med att aktivera $0 i firewalld"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Misslyckades att aktivera tuned"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Misslyckades att hämta loggar"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Misslyckades att läsa in auktoriserade nycklar."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Misslyckades att ladda enhet"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"Det gick inte att analysera enhetsfiler för dnf-automatic.timer eller dnf-"
+"automatic-install.timer. Ta bort anpassade åsidosättningar för att "
+"konfigurera automatiska uppdateringar."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Misslyckades med att starta om tjänst"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Misslyckades med att spara ändringar i /etc/motd"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Misslyckades att spara inställningarna"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Misslyckades att starta"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Misslyckades att byta profil"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Filstatus"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Filsystem"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "Filsystem är låst"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Filsystemsnamn"
+
+#: pkg/storaged/dialog.jsx:1114
+msgid "Filesystem outside the target"
+msgstr "Filsystem utanför mål"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "Filsystem är redan monterade under denna monteringspunkt."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Filtrera efter namn eller beskrivning"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Filtrera menyalternativ"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Filtrera tjänster"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Filter"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Fingeravtryck"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Brandvägg"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Brandväggen är inte tillgänglig"
+
+#: pkg/storaged/drive/drive.jsx:122
+msgid "Firmware version"
+msgstr "Version på fastprogramvara"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "Fixa NBDE-stöd"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Textstorlek"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Kan ej köras"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Framtvinga ändring"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Framtvinga borttagande"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Framtvinga lösenordsändring"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Formatera"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Formatera $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Formatera och montera"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Format and start"
+msgstr "Formatera och starta"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Formatera endast"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "Formatering raderar all data på en lagringsenhet."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Fördröjning framåt $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Bildrutenummer"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Ledigt utrymme"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Friformssökning"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Fredagar"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "Från"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Fullständigt namn"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Gateway"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Allmänt"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Genererad"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Gå till $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Gå till programmet"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Gå till nu"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Grafens synlighet"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Meny med alternativ för grafsynlighet"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Grupp"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Gruppnamn"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Grupper"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Utöka"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Utöka innehållet"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Utöka den logiska storleken av $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Utöka en logisk volym"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "Väx partition"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "Växa poolen så att den tar all plats"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Utöka till att ta allt utrymme"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Hårnålsläge"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Hårnålsläge"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Handhållen"
+
+#: pkg/storaged/drive/drive.jsx:64
+msgid "Hard Disk Drive"
+msgstr "Hårddiskenhet"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Hårdvaruinformation"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Hälsa"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Hälsningstid $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Hjälp"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "Dölj bekräftelselösenord"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "Dölj lösenord"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "Hierarki ID"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "Högre interoperabilitet till priset av en ökad attackyta."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Historiepaketantal"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Hemkatalog"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Värd"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Värdnyckeln är felaktig"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Värdnamn"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Värdar"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Varje timme"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Timmar"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "Hur man kontrollerar"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+"INTERNT FEL - Denna logiska volym är markerad som aktiv och bör ha en "
+"tillhörande blockenhet. Ingen sådan blockenhet kunde dock hittas."
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP-adress"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"IP-adress med prefix. Separera flera adresser med komma. Exempel: "
+"192.0.2.0/24,2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "IPv4-adresser"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4-inställningar"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6-inställningar"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Identifierare"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Om det lämnas tomt kommer ett ID att genereras baserat på associerade "
+"porttjänster och portnummer"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Om fingeravtrycket stämmer, klicka på \"Acceptera nyckel och logga in\". "
+"Annars, logga inte in och kontakta din administratör."
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Om fingeravtrycket stämmer, klicka på \"Lita på och lägg till värd\". "
+"Annars, anslut inte och kontakta din administratör."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ignorera"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "In"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "I en terminal, kör: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"För att tillåta inloggning till $0 som $1 utan lösenord i framtiden, använd "
+"inloggningslösenordet $2 på $3 som nyckellösenord, eller lämna "
+"nyckellösenordet tomt."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "I synk"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Inaktiv"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+msgid "Inactive logical volume"
+msgstr "Inaktiv logisk volym"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Inkluderade tjänster"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"Inkommande förfrågningar blockeras som standard. Utgående förfrågningar "
+"blockeras inte."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Felaktig monteringsinformation"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Öka med en"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Indexminne"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Indirekt"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Information"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Info och högre"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Initierar"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Initierar disk $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "Initiering raderar all data på en disk."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Initierar …"
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Detaljer: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Installera"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Installera stöd för NFS"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Installera Stratis stöd"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Installera alla uppdateringar"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "Installera applikationsinformation"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Installera cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Installera kpatch uppdateringar"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "Installera realmd stöd"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Installera säkerhetsuppdateringar"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr "Installera setroubleshoot-server för att felsöka SELinux-händelser."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Installera programvara"
+
+#: pkg/kdump/kdump-view.jsx:571
+msgid "Install the $0 package."
+msgstr "Installera $0-paketet."
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr "Installation stöds inte utan installerat cockpitpaket"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Installerad"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Installerar"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Installerar $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "Att installera $0 skulle ta bort $1."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Installerar paket"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Gränssnitt"
+msgstr[1] "Gränssnitt"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Gränssnittsmedlemmar"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Gränssnitt"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Internt fel"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Internt fel: felaktig utmaningshuvud"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Felaktig"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Felaktig adress $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Felaktigt datumformat"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Felaktigt datumformat och felaktigt tidsformat"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Felaktigt utgångsdatum"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Felaktiga filrättigheter"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Felaktig nyckel"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Felaktigt mått $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Felaktigt antal dagar"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Felaktigt portnummer"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Felaktigt prefix $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Felaktigt prefix eller nätmask $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Felaktigt område"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Felaktigt tidsformat"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Felaktig tidszon"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Felaktigt användarnamn eller lösenord"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "IoT-gateway"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "Kör sshd på en annan port?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Jobb"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Gå med"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Gå med i en domän"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Gå med i domän"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Går med"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Att gå med i en domän kräver installation av realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Att gå med i denna domän stödjs inte"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Går med i namnrymden för"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Journal"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Journalpost"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Journalposten hittades inte"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Hoppa till"
+
+#: pkg/kdump/kdump-view.jsx:569
+msgid "Kdump service is not installed."
+msgstr "Kdump-tjänst är inte installerad."
+
+#: pkg/kdump/kdump-view.jsx:604
+msgid "Kdump settings"
+msgstr "Kdump inställningar"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Behåll förbindelsen"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Kärnkraschdump"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr "Kernel startade inte med $0-inställningen"
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Kärndump"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "Kärn-live-patch $0 är aktiv"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Kärn-live-patch $0 är installerad"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Kärn-live-patch-inställningar"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Kärn-live-patchning"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Nyckellösenord"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Nyckelfack med okända typer kan inte redigeras här"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Nyckelkälla"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Nycklar"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Nyckelserver"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Nyckelserverns adress"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "Att ta bort nyckelservern kan förhindra upplåsning av $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP-nyckel"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "LEGACY med Active Directory-interoperabilitet."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+msgid "LVM2 VDO pool"
+msgstr "LVM2 VDO Pool"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+msgid "LVM2 logical volume"
+msgstr "LVM2 logisk volym"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+msgid "LVM2 logical volumes"
+msgstr "LVM2 logiska volymer"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "LVM2 fysisk volym"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+msgid "LVM2 physical volumes"
+msgstr "LVM2 fysiska volymer"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM2 Volymgrupp"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "LVM2 Volymgrupp $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr "Etikett"
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Bärbar dator"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Senaste 24 timmarna"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Senaste 7 dagarna"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Senast aktiv"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+msgid "Last cannot be removed"
+msgstr "Den sista kan inte tas bort"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Senast kontrollerad: $0 sedan"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+msgid "Last disk can not be removed"
+msgstr "Den senaste disken kan inte tas bort"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Senaste inloggning"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Senast ändrad: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Senaste lyckade inloggning:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "Layout"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Mer information"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "Lämna $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Lämna domänen"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Lämna tomt för att hoppa över kryptering"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Licensierad under GNU LGPL version 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Ljust"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Begränsa tillgång"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Begränsad tillgång"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"Begränsat läge begränsar administratörsprivilegier och tar bort "
+"funktionalitet i delar av webbkonsolen."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Begränsningar"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "Linjär"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Fördröjning när länk nere"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Länklokal"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Länkövervakning"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Fördröjning när länk uppe"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Länkbetraktelse"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Länkad"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+msgid "Linux filesystem data"
+msgstr "Linux filsystem data"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+msgid "Linux swap space"
+msgstr "Linux växlingsutrymme"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Lyssna"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "Lyssna port"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "Lyssna port måste vara ett nummer"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Listar enheter"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Misslyckades att lista enheter: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Belastning"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Lastbalansering"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Läs in tidigare data"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Läs in tidigare poster"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Inläsningen av tillgängliga uppdateringar misslyckades"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Läser in tillgängliga uppdateringar, var god dröj …"
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Läser in tidigare poster"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Läser in nycklar..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Inläsningen av SSH-nycklar misslyckades"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Inläsningen av enheter misslyckades"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Läser in paket..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Läser in anpassningar till systemet..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Läsa in enhet misslyckades"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Läser in …"
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Lokala konton"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Lokala filsystem"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Lokal monteringspunkt"
+
+#: pkg/storaged/overview/overview.jsx:160
+msgid "Local storage"
+msgstr "Lokal lagring"
+
+#: pkg/kdump/kdump-view.jsx:450
+msgid "Local, $0"
+msgstr "Lokal, $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Plats"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Lås"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "Lås $0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Lås kontot"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+msgid "Locked data"
+msgstr "Låst data"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Låser $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Logga in"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Logga in till $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Logga in med ditt serveranvändarkonto."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Loggmeddelanden"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Logga ut"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Logga ut användare"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Inloggad"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Logisk"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+msgid "Logical Volume Manager partition"
+msgstr "Logisk volymhanterare-partition"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Logisk storlek"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Logisk volym"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Logisk volym (ögonblicksbild)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Logisk volym av $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Inloggning"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Logga in igen"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Inloggningen misslyckades"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Inloggningsformat"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Inloggningshistorik"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Inloggningshistorielista"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "Logga ut $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Utloggningen lyckades"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Loggar"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Lågprofilskrivbord"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Lunchlåda"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+msgid "MDRAID device"
+msgstr "MDRAID-enhet"
+
+#: pkg/storaged/utils.js:334
+msgid "MDRAID device $0"
+msgstr "MDRAID-enhet $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+msgid "MDRAID device is recovering"
+msgstr "MDRAID-enhet återställs"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+msgid "MDRAID device must be running"
+msgstr "MDRAID-enhet måste vara igång"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+msgid "MDRAID disk"
+msgstr "MDRAID disk"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+msgid "MDRAID disks"
+msgstr "MDRAID diskar"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (Rekommenderas)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU måste vara ett positivt tal"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Maskin-ID"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Maskinens SSH-nyckels fingeravtryck"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Huvudserverchassi"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Underhåll"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr "Skadliga sidor på en fjärrdator kan påverka andra anslutna värdar"
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "Hantera filsystemstorlekar"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Hantera lagring"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Hanterade gränssnitt"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "Hantera LVM:er"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "Hantera NFS-monteringar"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "Hantera RAID:ar"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "Hantera VDO:er"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "Hantera VLAN"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Hantera brandvägg"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Hantera nätverksbindningar"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Hantera nätverksbryggor"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Hantera nätverksteam"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Hantera partitioner"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Hantera fysiska diskar"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Hantera tjänster"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Hantera programuppdateringar"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Hantera användarkonton"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Manuell"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Manuellt"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Markerar $target som felaktig"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Göm tjänst"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Gömd"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Att gömma en tjänst hindrar alla tjänster som beror på den att starta. Det "
+"kan ha en större effekt än tänkt. Se till att du verkligen vill gömma denna "
+"tjänst."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Maximal meddelandeålder $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+msgid "Media drive"
+msgstr "Media enhet"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Minne"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Minnesteknologi"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Minnesanvändning"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Minnesanvändning/swap"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Sammanslagna"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Meddelande till inloggade användare"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "Meddelanden angående detta fel kan finnas i journalen:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Metadata använt"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Metod"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Mått"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Mått och historik"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Metrikhistoriken kunde inte läsas in"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Måttinställningar"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini-PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Minitorn"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Minuterna måste vara ett tal mellan 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "Minutvis"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Minuter"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr "Speglad (RAID 1)"
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Rättningar"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Läge"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Modell"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Modifiera $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Måndagar"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Övervakningsintervall"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Övervakningsmål"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Månadsvis"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Mer information..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Montering"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "Monteringspunkt"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "Montera efter att nätverket blir tillgängligt, ignorera fel"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Montera också automatiskt vid start"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Montera vid uppstart"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Montera automatiskt på $0 vid start"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Montera innan tjänster startar"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Inställningar för monteringspunkter"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Montera filsystem"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Montera nu"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Montera på $0 nu"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Monteringsflaggor"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Monteringspunkt"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Monteringspunkten kan inte vara tom"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Monteringspunkten får inte vara tom."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Monteringspunkten är redan i användning för $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Monteringspunkten måste börja med ”/”."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Montera skrivskyddat"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Montera utan att vänta, ignorera fel"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Monterar $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Monteras innan tjänster startar"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Monteras parallellt med tjänster"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr "Monteras parallellt med tjänster, men efter nätverk är tillgängligt"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Multisystemschassi"
+
+#: pkg/storaged/drive/drive.jsx:135
+msgid "Multipathed devices"
+msgstr "Flervägsenheter"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+"Flera adresser kan anges med kommatecken eller mellanslag som avgränsare."
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS-montering"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA-ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP-server"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Namn"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Namnet får inte vara tomt."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Namnet får inte vara tomt."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Namnet får inte vara längre än $0 byte"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Namnet får inte vara längre än $0 tecken"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Namnet får inte vara längre än 127 tecken."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+msgid "Name cannot be longer than 255 characters."
+msgstr "Namnet får inte vara längre än 255 tecken."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Namnet får inte innehålla tecknet ”$0”."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+msgid "Name cannot contain the character '/'."
+msgstr "Namnet får inte innehålla tecknet '/'."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Namnet får inte innehålla blanktecken."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr "Behöver en reservdisk"
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Behöver åtminstone en NTP-server"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Nätverk"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Nätverks-I/O"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Nätverksbindning"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Nätverksenheter och -grafer behöver NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Nätverksloggar"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Nätverksanvändning"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager är inte installerad"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager kör inte"
+
+#: pkg/storaged/overview/overview.jsx:168
+msgid "Networked storage"
+msgstr "Nätverkslagring"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Nätverk"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Aldrig"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Upphör aldrig konto"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Låt aldrig lösenord gå ut"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Aldrig inloggad"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Ny NFS-montering"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Ny värd"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "Ny värd: $0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Nytt nyckellösenord"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Nytt namn"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Ny lösenfras"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Nytt lösenord"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Det nya lösenordet godtogs inte"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Nästa"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Nej"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "Inget ID angivet"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Inga SELinux-larm."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Inga program installerade eller tillgängliga."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Inga tillgängliga fack"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Inga block enheter är tillgängliga."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+msgid "No block devices found"
+msgstr "Ingen blockenhet hittades"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Ingen bärvåg"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Ingen konfiguration hittad"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Ingen data tillgänglig"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Ingen data tillgänglig mellan $0 och $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Ingen fördröjning"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Ingen beskrivning finns"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Ingen beskrivning tillhandahållen."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+msgid "No devices found"
+msgstr "Inga enheter hittades"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Inga diskar är tillgängliga."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+msgid "No disks found"
+msgstr "Inga diskar hittades"
+
+#: pkg/storaged/iscsi/session.jsx:79
+msgid "No drives found"
+msgstr "Inga diskar funna"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Ingen kryptering"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Inga händelser"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Inget filsystem"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Inga filsystem"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Inga fria nyckelfack"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Inget ledigt utrymme"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "Inget ledigt utrymme efter denna partition"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Inget gruppnamn angivet"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Inga värdnycklar hittade."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Inget installationspaket hittat för detta program."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Inga nycklar tillagda"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "Inga språk matchar"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Inga loggposter"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Inga logiska volymer"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Inga loggar hittades"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Inga matchande resultat"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Inget medium isatt"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Ingen partitionering"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+msgid "No partitions found"
+msgstr "Inga partitioner hittades"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "Inga jämlikar tillagda."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+msgid "No physical volumes found"
+msgstr "Inga fysiska volymer hittades"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Inget verkligt namn angivet"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Inga resultat funna"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Inga resultat matchar filterkriterierna. Rensa alla filter för att visa "
+"resultat."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Inga regelträffar"
+
+#: pkg/storaged/overview/overview.jsx:190
+msgid "No storage found"
+msgstr "Ingen lagring hittades"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+msgid "No subvolumes"
+msgstr "Inga undervolymer"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Filen eller katalogen finns inte"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Inga systemändringar"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Inga systemrapporter."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Inga uppdateringar"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Inget användarnamn angivet"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Inga"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Inte en giltig privat nyckel"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Inte behörig att inaktivera brandväggen"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Inte behörig att aktivera brandväggen"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Inte tillgängligt"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Inte ansluten"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Inte ansluten till Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Inte ansluten till värd"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+msgid "Not enough free space"
+msgstr "Inte tillräckligt med fritt utrymme"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "Inte tillräckligt med utrymme"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+msgid "Not enough space to grow"
+msgstr "Inte tillräckligt med utrymme för att växa"
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Finns inte"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Inte installerad"
+
+#: pkg/networkmanager/network-interface.jsx:680
+msgid "Not permitted to configure network devices"
+msgstr "Inte tillåtet att konfigurera nätverksenheter"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Inte tillåtet att konfigurera att ändra riken"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Inte tillåtet att utföra denna åtgärd."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Inte klar"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Inte registrerad"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Kör inte"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Inte uppsatt"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Inte synkroniserad"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Observera"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Bärbar (notebook)"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Notering och högre"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Mörka nätverksadresser, värdnamn och användarnamn"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Mörkad"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Hände $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Hände mellan $0 och $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Förekomster"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "Ok"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Gammal lösenfras"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Gammalt lösenord"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Det gamla lösenordet accepterades inte"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "På en monterad enhet"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Vid misslyckande"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"När Cockpit är installerat, aktivera det med ”systemctl enable --now cockpit."
+"socket”."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Endast $0 av $1 används."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Endast alfabetet, siffror, :, _, ., @, - är tillåtna"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Endast nödläge"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"Använd endast godkända och tillåtna algoritmer vid uppstart i FIPS-läge."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Hoppsan!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "Öppna pmproxy-tjänsten i brandväggen för att dela mätvärden."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Åtgärden ”$operation” på $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Alternativ"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Eller använd en medpackad bläddrare"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Annan"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"Andra autentiseringsmetoder är fortfarande tillgängliga även när interaktiv "
+"lösenordsautentisering inte är tillåten."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Andra alternativ"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Ut"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Översikt"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Skriv över"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Skriv över befintlig data med nollor (långsammare)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Paketet cockpit-pcp saknas för statistikhistorik"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Paketinformation"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit kraschade"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit är inte installerat"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit rapporterade felkod $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Paket"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Sidnamn"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Förälder"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Förälder $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Del av"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Del av $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Partition"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Partition av $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "Partitionsstorlek är $0. Innehållsstorlek är $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Partitionering"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Partitioner"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Passiv"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Lösenfras"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "Lösenfrasen kan inte vara tom"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Lösenfrasen får inte vara tom"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Lösenfrasen från vilket annat nyckelfack som helst"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Att ta bort lösenfrasen kan förhindra upplåsning av $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Lösenfraserna stämmer inte överens"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Lösenord"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Lösenordet har ändrats"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Utgång av lösenord"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "Lösenord får inte vara längre än 256 tecken"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Lösenordet är inte godtagbart"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Lösenordet är för svagt"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Lösenordet måste ändras"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Lösenordet accepterades inte"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Lösenordstips"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Klistra in"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Klistra in fel"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "Klistra in befintlig nyckel"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Klistra in innehållet i din publika SSH-nyckelfil här"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Sökväg"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Sökvägskostnad"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Sökvägskostnad $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Sökväg på servern"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Sökvägen på servern får inte vara tom."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Sökvägen på servern måste börja med ”/”."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Sökväg till katalog"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Sökväg till filen"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Sökvägar"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Pausa"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr "Jämlik #$0 har en ogiltig ändpunktsport. Port måste vara ett nummer."
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"Jämlik #$0 har ogiltig ändpunkt. Det måste anges som värd:port, t.ex. "
+"1.2.3.4:51820 eller exempel.com:51820"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "Jämlikar"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"Jämlikar är andra maskiner som ansluter till denna. Offentliga nycklar från "
+"andra maskiner kommer att delas med varandra."
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performance Co-Pilot samlar in och analyserar prestandamått från ditt system."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Prestandaprofil"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Periferichassi"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Permanent"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "Permanent ta bort $0 grupp?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "Permanent ta bort $0?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Vill du permanent ta bort logisk volym $0/$1?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+msgid "Permanently delete subvolume $0?"
+msgstr "Vill du permanent ta bort undervolym $0?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Åtkomst nekas"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Tillåtande"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Fysiskt"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "Fysiska volymer"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Fysiska volymer"
+
+#: pkg/storaged/block/resize.jsx:279
+msgid "Physical volumes can not be resized here"
+msgstr "Storleken på fysiska volymer kan inte ändras här"
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Välj datum"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Fäst enhet"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Ping-intervall"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Ping-mål"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Fästa enheter"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Pizzalåda"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Autentisera för att få administrativ åtkomst"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Aktivera JavaScript för att använda webbkonsolen."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Installera paketet $0"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Snälla lös problemet och ladda om den här sidan."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Ange ett utgångsdatum"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Ange värden att ansluta till"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "Vänligen avmontera dem först."
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Pool för tunna logiska volymer"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Pool för tunt underhållna LVM2 logiska volymer"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Pool för tunt underhållna volymer"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "Pool lösenfras"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Port"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Bärbar"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Portar"
+
+#: pkg/storaged/partitions/partition.jsx:116
+msgid "PowerPC PReP boot partition"
+msgstr "PowerPC PReP uppstartspartition"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Prefixlängd"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Prefixlängd eller nätmask"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Förbereder"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Närvarande"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Bevara"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Snyggt värdnamn"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Tidigare uppstart"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Primär"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Prioritet"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Prioritet $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "Privatnyckel"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "Problem med att bli administratör"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Problemdetaljer"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Probleminformation"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Processer som använder platsen"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Förlopp: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Projektwebbsajt"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Tidsgränsen överskreds vid fråga via passwd"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Tidsgränsen överskreds vid fråga via ssh-add"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Tidsgränsen överskreds vid fråga via ssh-keygen"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Vidarebefordrar omladdning till"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Skyddar mot förutsedda attacker på kort sikt på bekostnad av "
+"interoperabilitet."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "Ange lösenordsfrasen för poolen på dessa blockenheter:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Publik nyckel"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr "Publik nyckel kommer att genereras när en giltig privat nyckel anges"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Syfte"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (Strimlor)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (Spegel)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (Strimlor av speglar)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (dedikerad paritet)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (distribuerad paritet)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (dubbel distribuerad paritet)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID-chassi"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID-nivå"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr "RAID10 behöver ett jämnt antal fysiska volymer"
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Rackmonteringschassi"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Slumpvis"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Räckvidd"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "Räckvidden måste vara strikt ordnad"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Ordning"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Rå till en enhet"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Läs"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Läs mer..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Skrivskyddad"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Läser"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Läser …"
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Klar"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Klar"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Verkligt värdnamn"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Det verkliga värdnamnet kan endast innehålla gemena bokstäver, siffror, "
+"bindestreck och punkter (med populerade underdomäner)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Det verkliga värdnamnet får bara vara 64 tecken eller mindre"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Återverkställ och starta om"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Starta om"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Starta om efteråt"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Omstart rekommenderas"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Starta om system..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Tar emot"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Senaste värdar"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Rekommenderade, säkra inställningar för aktuella hotmodeller."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Återanslut"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Återställer"
+
+#: pkg/storaged/jobs-panel.jsx:70
+msgid "Recovering MDRAID device $target"
+msgstr "Återställer MDRAID-enhet $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Uppdaterar paketinformation"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Vägrar att ansluta. Värden är okänd"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Vägrar att ansluta. Värdnyckeln stämmer inte"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Vägrar att ansluta. Värdnyckeln är okänd"
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "Regenerera"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "Regenererar initrd"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Registrera …"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "Relaterade processer och tjänster kommer tvingas stoppa."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "Relaterade processer kommer tvingas stoppa."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "Relaterade tjänster kommer tvingas stoppa."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Läs om"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Omläsning vidarebefordrad från"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Laddar om"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Laddar om tillståndet för återstående tjänster"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "Fjärr över CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "Fjärr över FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Fjärr över NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+msgid "Remote over NFS, $0"
+msgstr "Fjärr över NFS, $0"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "Fjärr över SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Fjärr över SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+msgid "Remote over SSH, $0"
+msgstr "Fjärr över SSH, $0"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Borttagningar:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Ta bort"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Ta bort $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Ta bort tjänsten $0 från zon $1"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Ta bort $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Ta bort Tang-nyckelserver?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Ta bort enhet"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Ta bort värd"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Ta bort post"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "Ta bort saknade fysiska volymer?"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "Ta bort lösenfrasen i nyckelfack $0?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "Ta bort lösenfras?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Ta bort tjänst $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Ta bort zon $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Tar bort"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Tar bort $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Att ta bort $0 kommer bryta anslutningen till servern, och kommer göra "
+"administrationsgränssnittet otillgängligt."
+
+#: pkg/storaged/jobs-panel.jsx:66
+msgid "Removing $target from MDRAID device"
+msgstr "Tar bort $target från MDRAID-enheten"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"Att ta bort en lösenfras utan bekräftelse av en annan lösenfras kan "
+"förhindra upplåsning eller nyckelhantering, om andra lösenfraser glöms bort "
+"eller går förlorade."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Tar bort den fysiska volymen från $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Att ta bort cockpittjänsten kan leda till att webbkonsolen blir oåtkomlig. "
+"Se till att den här zonen inte gäller din nuvarande webbkonsolanslutning."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "Om du tar bort zonen tas alla tjänster inom den bort."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Byt namn"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Byt namn på Stratis pool"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Byt namn på filsystem"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Byt namn på grupp"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "Bytt namn på grupp $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Byt namn på en logisk volym"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Byt namn på en volymgrupp"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Byter namn på $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "Att byta namn på en grupp kan påverka sudo och liknande regler"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr "Reparera"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+msgid "Repair logical volume $0"
+msgstr "Reparera logisk volym $0"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Reparerar $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Upprepa"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Upprepa månadsvis"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Upprepa lösenfrasen"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Upprepa varje vecka"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Rapport"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Rapportetikett"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Rapportera till ABRT Analys"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Rapporterad; inga länkar tillgängliga"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Rapporteringen misslyckades"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Rapporteringen avbröts"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Rapporter"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Rapporter:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Begär en lösenordsändring var $0:e dag"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Begär lösenordsändring den $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Begär lösenordsändring vid första inloggningen"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Begärd av"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Krävs av "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Begär"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Kräver administrationsbehörighet för att redigera"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Behov"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Behov av"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+"Reservera minne vid uppstart genom att ställa in ett '$0'-alternativ på "
+"kärnans kommandorad. Till exempel, lägg till '$1' till $2 i $3 eller använd "
+"din distributions kärnargumentredigerare."
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Reserverat minne"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Återställ"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Återställ lösenord"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Ändrar storlek på $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Ändra storlek på ett krypterat filsystem kräver upplåsning av disken. Ange "
+"en aktuell disklösenfras."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Starta om"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Starta om tjänster"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Starta om tjänster..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Startar om"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Återställer förbindelsen"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"Resultaten av kraschen kommer att kopieras genom $0 till $1 som $2, om kdump "
+"är korrekt konfigurerad."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+"Resultaten av kraschen kommer att lagras i $0 som $1, om kdump är korrekt "
+"konfigurerat."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Återuppta"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Återanvänd befintlig kryptering"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Återanvänd befintlig kryptering ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "Granska kryptografisk policy"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Granska loggar"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Turas om"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Rutter"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Kör vid"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Kör en ny rapport"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Kör på"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Kör en rapport"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+"Kör det här kommandot över ett pålitligt nätverk eller fysiskt på "
+"fjärrdatorn:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Körare"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Kör"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Körtillfälle"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux-fel vid åtkomstkontroller"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux är avaktiverat på systemet"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux är avaktiverat på systemet."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux-policy"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "SELinux systemstatus är okänd."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "SELinux-felsökning"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "SMART-självtest av $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"SOS-rapportering samlar systeminformation för att hjälpa till med "
+"diagnostisering av problem."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH-nyckel"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "SSH-nyckel är inte en sökväg"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH-nycklar"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "Fördröjning av STP-vidarebefordran"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "STP Hello-tid"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP maximal meddelandeålder"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP-prioritet"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Safari-användare måste importera och lita på certifikatet för den "
+"självsignerande CA:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Lördagar"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Spara"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Spara och starta om"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Spara ändringar"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Spara utrymme genom att komprimera enskilda block med LZ4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Spara utrymme genom att lagra identiska datablock bara en gång"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Att spara en ny lösenfras kräver att disken låses upp. Ge en aktuell "
+"disklösenfras."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Schemalagd avstängning vid $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Schemalagd omstart till $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "PC med slutet hölje"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Sök"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Sökdomäner"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Sök efter namn eller ID"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Sök efter namn, grupp eller ID"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "Sekunder måste vara ett tal mellan 0-59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Sekunder"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Säkra skalnycklar"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Raderar säkert $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Säkerhetsförbättrad Linux-konfiguration och felsökning"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Säkerhetsuppdateringar tillgängliga"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Säkerhetsuppdateringar endast"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Säkerhetsuppdateringar kommer att tillämpas $0 vid $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Välj"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Välj en identifierare"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Välj metod"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+"Välj de fysiska volymer som ska användas för att reparera den logiska "
+"volymen. Minst $0 behövs."
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Skicka"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Skickar"
+
+#: pkg/storaged/drive/drive.jsx:123
+msgid "Serial number"
+msgstr "Serienummer"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Server"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Serveradress"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Serveradressen får inte vara tom."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Servern får inte vara tom."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Servern har stängt förbindelsen."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Serverprogramvara"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Tjänst"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Tjänsten har ett fel"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Tjänsteloggar"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Tjänster"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Tjänster som använder platsen"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Session"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Sessionen är på väg att löpa ut"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Sätt"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Ange värdnamn"
+
+#: pkg/storaged/partitions/partition.jsx:173
+msgid "Set partition type of $0"
+msgstr "Ställ in partitionstyp till $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Sätt lösenord"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Ställ in tiden"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Sätt till"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Sätt upp"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Ställ in ett svagt lösenord"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Inställningarna avviker från det konfigurerade tillståndet och kommer återgå "
+"vid nästa uppstart."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Sätter upp"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Sätter upp vändslingeenheten $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Inställningar"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Allvarsgrad"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Delad"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Skal"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Skalskript"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Skift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+msgid "Show all $0 rows"
+msgstr "Visa alla $0 rader"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Visa alla trådar"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Show confirmation password"
+msgstr "Visa bekräftelselösenord"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Visa fingeravtryck"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Visa meddelanden som innehåller en given sträng."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Visa meddelande för den angivna systemd-enheten."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Visa meddelanden från en specifik start."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Visa mer relationer"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "Visa lösenord"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Visa relationer"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Krymp"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Krymp en logisk volym"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "Krymp partition"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Krymp volym"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Stäng av"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Stäng av"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Sedan"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Ensam ordning"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Storlek"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "Storleken kan inte vara negativ"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "Storleken kan inte vara noll"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Storleken är för stor"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Storleken måste vara ett tal"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Storleken måste vara åtminstone $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Hoppa över huvudnavigationen"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Hoppa till innehållet"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Plats"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Plats $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Ögonblicksbild"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Uttag"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Programvaruuppdateringar"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Programvarubaserade sätt att gå runt problem hjälper till att förhindra CPU-"
+"säkerhetsrisker. Dessa begränsningar har sidoeffekten att reducera "
+"prestanda. Ändra dessa inställningar på egen risk."
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr "SSD-enhet"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Lösningen verkställd"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Lösningen misslyckades"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Lösningar"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"Vissa blockenheter i denna pool har vuxit i storlek efter att poolen "
+"skapades. Poolen kan säkert växa för att använda det nyligen tillgängliga "
+"utrymmet."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr "Något annat program använder just nu pakethanteraren, var god dröj …"
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Vissa program måste startas om manuellt"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr "Förlåt"
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Sorterad från minst till mest betrodd"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Utrymmessparande dator"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Uppspännande-träd-protokollet"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Uppspännande-träd-protokollet (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Reserv"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Specifik tid"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Hastighet"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Stabilt"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Starta"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Starta och aktivera"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Starta multipath"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Starta tjänst"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr "Börja visa poster på eller nyare än det angivna datumet."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "Börja visa poster på eller äldre än det angivna datumet."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Startad"
+
+#: pkg/storaged/jobs-panel.jsx:64
+msgid "Starting MDRAID device $target"
+msgstr "Startar MDRAID-enhet $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Starta växlingsutrymmet $target"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Tillstånd"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Statisk"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Status"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Pinndator"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Fast"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Stoppa"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Stoppa och avaktivera"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Stoppa och ta bort"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Stoppa och avmontera"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Stoppa enhet"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Sluta redigera värdar"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Stoppa rapporten"
+
+#: pkg/storaged/jobs-panel.jsx:63
+msgid "Stopping MDRAID device $target"
+msgstr "Stoppar MDRAID-enhet $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Stoppar växlingsutrymmet $target"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Lagring"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "Lagring kan inte hanteras på detta system."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Lagringsloggar"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Lagra lösenfrasen"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Lagrad lösenfras"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+msgid "Stratis block device"
+msgstr "Stratis blockenhet"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+msgid "Stratis block devices"
+msgstr "Stratis blockenheter"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Stratis blockenheter kan inte göras mindre"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+msgid "Stratis filesystem"
+msgstr "Stratis filsystem"
+
+#: pkg/storaged/stratis/pool.jsx:300
+msgid "Stratis filesystems"
+msgstr "Stratis filsystem"
+
+#: pkg/storaged/stratis/pool.jsx:361
+msgid "Stratis filesystems pool"
+msgstr "Stratis filsystem pool"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Stratis-pool"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr "Remsa (Raid 0)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr "Remsad och speglad (Raid 10)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr "Remsor"
+
+#: pkg/lib/cockpit-components-password.jsx:97
+msgid "Strong password"
+msgstr "Starkt lösenord"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Stubbe"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Stil"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Under-chassi"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "ULPC"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "Att prenumerera på systemd-signaler misslyckades: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+msgid "Subvolume needs to be mounted"
+msgstr "Undervolym måste monteras"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr "Undervolym måste monteras skrivbar"
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Kopierades till urklipp"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Kopierade till urklipp!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Söndagar"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Växlingsutrymme"
+
+#: pkg/storaged/block/resize.jsx:292
+msgid "Swap can not be resized here"
+msgstr "Det går inte att ändra storlek på swap här"
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Växla ut"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Ändra till $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Stäng av $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Sätt på $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Byt till administrativ åtkomst"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Byt till begränsad åtkomst"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Att slå av $0 kommer bryta anslutningen till servern, och kommer göra "
+"administrationsgränssnittet otillgängligt."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Att sätta på $0 kommer bryta anslutningen till servern, och kommer göra "
+"administrationsgränssnittet otillgängligt."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Synkroniserad"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Synkroniserad med $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Synkroniserar"
+
+#: pkg/storaged/jobs-panel.jsx:71
+msgid "Synchronizing MDRAID device $target"
+msgstr "Synkroniserar MDRAID-enheten $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "System"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Systemdiagnostik"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Systeminformation"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Systemet är uppdaterat"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Systemändringar"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Systemtid"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemenheter"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Platta"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang-nyckelserver"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Mål"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Mål"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Team"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Team-port"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Team-portsinställningar"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminal"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Avsluta sessionen"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Testa konfigurationen"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Testet är endast tillgängligt medan tjänsten kdump kör."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Testa kdump-inställningar"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Testa kdump-inställningarna genom att krascha kärnan. Detta kan ta ett tag "
+"och systemet kanske inte startar om automatiskt. Krascha inte systemet "
+"medvetet medan någon viktig uppgift körs."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Testar anslutningen"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "$0 paket är inte tillgängligt från något förråd."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "Paketet $0 måste installeras för att skapa Stratis-pooler."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "Paketet $0 måste vara installerat."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "Paketet $0 kommer att installeras för att skapa VDO-enheter."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP-adressen eller värdnamnet får inte innehålla blanktecken."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+msgid "The MDRAID device is in a degraded state"
+msgstr "MDRAID-enheten är i ett degraderat tillstånd"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+msgid "The MDRAID device must be running"
+msgstr "MDRAID-enheten måste vara igång"
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+"SSH-nyckeln $0 av $1 på $2 kommer att läggas till $3-filen på $4 på $5."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"SSH-nyckeln $0 kommer att göras tillgänglig för resten av sessionen och "
+"kommer att vara tillgänglig för inloggning även för andra värdar."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"SSH-nyckeln för att logga in på $0 är skyddad av ett lösenord, och värden "
+"tillåter inte inloggning med ett lösenord. Vänligen ange lösenordet för "
+"nyckeln på $1."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"SSH-nyckeln för att logga in på $0 är skyddad. Du kan logga in med antingen "
+"ditt inloggningslösenord eller genom att ange lösenordet för nyckeln för $1."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"Kontot ”$0” kommer att tvingas ändra sitt lösenord vid nästa inloggning"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Cockpit tjänsten ingår automatiskt"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"Det konfigurerade tillståndet är okänt, det kan ändra sig vid nästa uppstart."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr ""
+"Skapandet av denna VDO-enhet avslutade inte och enheten kan inte användas."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"Användaren som för närvarande är inloggad har inte tillstånd att se "
+"information om nycklar."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr "Disken måste låsas upp innan formatering. Ange en befintlig lösenfras."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "Filen $0 kommer att raderas."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+msgid "The filesystem has no assigned mount point."
+msgstr "Filsystemet har ingen tilldelad monteringspunkt."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "Filsystemet har ingen permanent monteringspunkt."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Filsystemet är konfigurerat att monteras automatiskt vid uppstart men dess "
+"krypteringsbehållare kommer inte att låsas upp vid den tidpunkten."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Filsystemet är för närvarande monterat men kommer inte att monteras efter "
+"nästa uppstart."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Filsystemet är för närvarande monterat på $0 men kommer att monteras på $1 "
+"vid nästa uppstart."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Filsystemet är för närvarande monterat på $0 men kommer inte att monteras "
+"efter nästa uppstart."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Filsystemet är för närvarande inte monterat men kommer att monteras vid "
+"nästa uppstart."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Filsystemet är inte monterat."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"Filsystemet kommer att låsas upp och monteras vid nästa uppstart. Detta kan "
+"kräva att du matar in en lösenfras."
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "Fingeravtrycket ska matcha:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Följande tjänst kommer att startas om:"
+msgstr[1] "Följande tjänster kommer att startas om:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "Det fullständiga namnet får inte innehålla kolon."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "Grupp-ID:t måste vara ett positivt heltal"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Gruppnamnet får endast bestå av bokstäver från a-z, siffror, punkter, "
+"bindestreck och understreck"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"Hemkatalogen $0 existerar redan. Dess ägarskap kommer att ändras till den "
+"nya användaren."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "Initrd måste återskapas."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Nyckellösenordet får inte vara tomt"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Nyckellösenorden stämmer inte överens"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "Nyckeln du angav var inte giltig."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Det sista nyckelfacket kan inte tas bort"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "De listade processerna och tjänsterna kommer tvingas stoppa."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "De listade processerna kommer att tvingas stoppa."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "De listade tjänsterna kommer att tvingas stoppa."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "Den inloggade användaren har inte tillåtelse att se systemändringar"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "Maskinen startar om"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "Monteringspunkten $0 används av dessa processer:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "Monteringspunkten $0 används av dessa tjänster:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Det nya nyckellösenordet får inte vara tomt"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Lösenordet får inte vara tomt"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Lösenorden stämmer inte överens"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Lösenorden stämmer inte överens."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"Det resulterande fingeravtrycket går bra att dela via offentliga metoder, "
+"inklusive e-post."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"Det resulterande fingeravtrycket är okej att dela via offentliga metoder, "
+"inklusive e-post. Om du ber någon annan att göra verifieringen åt dig kan de "
+"skicka resultaten med vilken metod som helst."
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Servern vägrade att autentisera ”$0” med lösenordsautentisering, och inga "
+"andra stödda autentiseringsmetoder är tillgängliga."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr "Servern vägrade att autentisera med några stödda metoder."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"Systemet stöder för närvarande inte upplåsning av ett filsystem med en Tang-"
+"nyckelserver under uppstart."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"Systemet stöder för närvarande inte upplåsning av rotfilsystemet med en Tang-"
+"nyckelserver."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr "Användaren $0 tillåts inte ändra CPU-säkerhetsbegränsningar"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "Användaren $0 har inte rättighet att ändra kryptografisk policy"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "Användaren $0 har inte tillåtelse att testa att krascha kärnan"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"Användaren måste logga ut och logga in igen för att den nya konfigurationen "
+"ska träda i kraft."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Användarnamnet får endast bestå av bokstäver från a-z, siffror, punkter, "
+"bindestreck och understreck."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"Webbläsarens konfiguration förhindrar Cockpit från att köra (oåtkomlig $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Det finns för närvarande inga aktiva sidor"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"Det finns enheter med flera sökvägar på systemet, men multipath-tjänsten kör "
+"inte."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "Det finns inga aktiva tjänster i denna zon"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Det finns inga auktoriserade publika nycklar för detta konto."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+"Det finns inte tillräckligt med utrymme som kan användas för reparation. "
+"Minst $0 behövs för fysiska volymer som inte redan används för denna logiska "
+"volym."
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"Det finns inte tillräckligt med utrymme i poolen för att ta en "
+"ögonblicksbild av detta filsystem. Minst $0 krävs men endast $1 är "
+"tillgängligt."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Det uppstod ett oväntat fel vid anslutning till maskinen."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Dessa ytterligare steg är nödvändiga:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Dessa ändringar kommer att göras:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Tunn logisk volym"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Tunt provisionerade LVM2 logiska volymer"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"Denna MDRAID-enhet har ingen bitmap för skrivavsikt. En sådan bitmap kan "
+"minska synkroniseringstiderna avsevärt."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr "Denna NFS-montering används och endast dess alternativ kan ändras."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Denna VDO-enhet använder inte alla sina underliggande enheter."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+msgid "This device can not be used for the installation target."
+msgstr "Den här enheten kan inte användas för installationsmålet."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Denna enhet kan inte hanteras här."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "Den här enheten används för närvarande."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Detta fält får inte vara tomt"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Den här gruppen är den primära gruppen för följande användare:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr ""
+"Denna värd kommer att starta om efter att uppdateringar har installerats."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "Denna information lagras endast på systemet."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Denna nyckelserver är det enda sättet att låsa upp pool och kan inte tas "
+"bort."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+"Denna logiska volym har förlorat några av sina fysiska volymer och kan inte "
+"längre användas. Du måste ta bort den och skapa en ny för att ta dess plats."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+"Denna logiska volym har förlorat några av sina fysiska volymer men har inte "
+"förlorat några data ännu. Du bör reparera den för att återställa den "
+"ursprungliga redundansen."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+"Denna logiska volym har förlorat några av sina fysiska volymer men kanske "
+"inte har förlorat någon data ännu. Du kanske kan reparera den."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "Denna logiska volym används inte helt av innehållet."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Denna maskin har redan lagts till."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Detta kan ta ett tag"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "Denna partition används inte helt av innehållet."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Denna lösenfras är det enda sättet att låsa upp pool och kan inte tas bort."
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr "Den här poolen använder inte allt utrymme på sina blockenheter."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "Denna pool är i ett degraderat tillstånd."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Detta system är inte registrerat"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Detta system använder en anpassad profil"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Detta system använder den rekommenderade profilen"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"Det här verktyget konfigurerar SELinux-policyn och kan hjälpa till med att "
+"förstå och lösa policy överträdelser."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"Det här verktyget konfigurerar systemet till att kunna skriva "
+"kärnkraschdumpar till disken."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"Detta verktyg genererar ett arkiv med konfiguration och diagnostisk "
+"information från det körande systemet. Arkivet kan förvaras lokalt eller "
+"centralt för inspelning eller spårningsändamål eller kan skickas till "
+"tekniska supportrepresentanter, utvecklare eller systemadministratörer för "
+"att hjälpa till med teknisk felspårning och felsökning."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"Detta verktyg hanterar lokal lagring, såsom filsystem, LVM2-volymgrupper och "
+"NFS-monteringar."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"Detta verktyg hanterar nätverk som bindningar, broar, team, VLAN och "
+"brandväggar med NetworkManager och Firewalld. NetworkManager är "
+"inkompatibelt med Ubuntus standard systemd-networkd och Debians ifupdown "
+"skript."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Denna enhet är inte gjord för att aktiveras explicit."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Detta användarnamn finns redan"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr "Den här volymgruppen saknar några fysiska volymer."
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Den här webbläsaren är för gammal för att köra webbkonsolen ($0 saknas)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Detta kommer lägga till en matchning av ”_BOOT_ID=”. Om det inte anges "
+"kommer den aktuella uppstarten visas. Om uppstarts-ID:t utelämnas kommer ett "
+"positivt avstånd leta efter uppstarter från början av journalen och ett lika-"
+"med-eller-mindre-än noll-avstånd leta efter uppstarter från slutet av "
+"journalen. Alltså betyder 1 den första uppstarten som finns i journalen i "
+"kronologisk ordning, 2 den andra och så vidare; medan -0 är sista "
+"uppstarten, -1 uppstarten före det, och så vidare."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Detta kommer lägga till en matchning av ”_SYSTEMD_UNIT=”, ”COREDUMP_UNIT=” "
+"och ”UNIT=” för att hitta alla möjliga meddelanden för den angivna enheten. "
+"Kan innehålla flera enheter separerade av komma. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Detta gör att du kan logga in utan lösenord i framtiden."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Denna zon innehåller cockpit-tjänsten. Se till att denna zon inte gäller din "
+"aktuella webbkonsolanslutning."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Torsdagar"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Nivå"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Tid"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Tidszon"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Att skapa en timer misslyckades"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "Att radera en timer misslyckades"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Timrar"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Tips: gör så att ditt nyckellösenord stämmer med ditt inloggningslösenord "
+"för att automatiskt autentisera mot andra system."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"För att säkerställa att din anslutning inte fångas upp av en skadlig tredje "
+"part, vänligen verifiera värdnyckelns fingeravtryck:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"För att få programvaruuppdateringar behöver detta system registreras hos Red "
+"Hat, antingen genom att använda Red Hats kundportal eller en lokal "
+"prenumerationsserver."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"För att verifiera ett fingeravtryck, kör följande på $0 medan du fysiskt "
+"sitter vid maskinen eller genom ett pålitligt nätverk:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Idag"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Växla"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Växla datumväljare"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Växla filter"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "För mycket data"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Verktyg"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Topp 5 CPU-tjänster"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "Topp 5 diskanvändningstjänster"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "Topp 5 minnestjänster"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Total storlek: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Torn"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Transient"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Överföring"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Utlösare"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Utlöst av"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Utlösare"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Felsök"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Felsök…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "Lita på och lägg till värd"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Förtroendenyckel"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Tillitsnivå"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Försök igen"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Försök att synkronisera med $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Tisdagar"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned har misslyckats med att starta"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned är en tjänst som övervakar systemet och optimerar prestandan under "
+"vissa belastningar. Kärnan av Tuned är profiler, vilka trimmar systemet för "
+"olika användningsfall."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned är inte tillgänglig"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned kör inte"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned är avslagen"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Aktivera administrativ åtkomst"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Typ"
+
+#: pkg/storaged/partitions/partition.jsx:165
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Typ kan bara innehålla tecknen 0 till 9, A till F och \"-\"."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr "Typ måste ha formatet NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+"Typ måste innehålla exakt två hexadecimala tecken (0 till 9, A till F)."
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Skriv för att filtrera"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Kan inte verkställa denna lösning automatiskt"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Kan inte ansluta till den adressen"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "Det går inte att kontakta $0."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Det går inte att kontakta den givna värden $0. Se till att ssh körs på port "
+"$1, eller ange en annan port i adressen."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Kan inte ta bort larmdetaljer."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Det går inte att logga in på $0 med SSH-nyckelautentisering. Ange "
+"lösenordet. Du kanske vill ställa in dina SSH-nycklar för automatisk "
+"inloggning."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Det går inte att logga in på $0. Värden accepterar inte lösenordsinloggning "
+"eller någon av dina SSH-nycklar."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Kan inte nå servern"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Kan inte ta bort monteringen"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+msgid "Unable to repair logical volume $0"
+msgstr "Kunde inte reparera logisk volym $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "Kan inte köra fixen: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "Kunde inte spara inställningarna"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "Kan inte spara inställningarna: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Kan inte starta setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Kan inte avmontera filsystem"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Otillgänglig"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Otillgängliga paket"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Ångra"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "Oväntat PackageKit-fel under installation av $0: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Oväntat fel"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+msgid "Unformatted data"
+msgstr "Oformaterad data"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Enhet"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Okänd"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Okänt ”$0”"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Okänt ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Okänt program"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Okänd konfiguration"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Okänt värdnamn"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Okänt tjänstnamn"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Okänd typ"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Lås upp"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Lås upp automatiskt vid start"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr "Lås upp innan du ändrar storlek"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Lås upp krypterad Stratis pool"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "Lås upp nyckel $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Lås upp $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Låser upp disk"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Ej hanterade gränssnitt"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Avmontera"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "Avmontera filsystem $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Avmontera nu"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Avmonterar $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Namnlös"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "Avfäst enhet"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Okända data"
+
+#: pkg/storaged/block/resize.jsx:306
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Okända data får inte göras mindre här"
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Okända data får inte göras mindre här."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+msgid "Unsupported logical volume"
+msgstr "Den logiska volymen stöds inte"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "Tills"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Ej betrodd värd"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Uppdatera"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Uppdatera framgångstabell"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Uppdateringshistorik"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Uppdatera paketinformation"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "Uppdateringen lyckades"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Uppdaterat"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr "Uppdaterade paket kan behöva en omstart för att få effekt."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Uppdateringar tillgängliga"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Uppdatera historik"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Uppdateringar kommer att tillämpas $0 vid $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Uppdaterar"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Uppdaterar status..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Upptid"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Användning"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "Användning av $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Använd"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Använd komprimering"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Använd deduplicering"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Använd nyckel"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Använd lösenord"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Använd följande nycklar för att autentisera mot andra system"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Använd utförlig loggning"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Använt"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"Användbart för monteringar som är valfria eller behöver interaktion (som "
+"lösenfraser)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Användare"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "Användar-ID"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "Användar-ID används redan av en annan användare"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "Användar-ID måste vara ett positivt heltal"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "Användar-ID måste inte vara högre än $0"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "Användar-ID får inte vara lägre än $0"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Användarnamn"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Användarnamnet får inte vara tomt"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Användarnamn"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "Använder LUKS-kryptering"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Använder Tang-server"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "VDO-underlagsenheter kan inte göras mindre"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO-enhet $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "VDO-filsystemvolym (komprimering/deduplicering)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN-ID"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Validerar adress"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Validerar autentiseringselementet"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Leverantör"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Verifierad"
+
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "Verifiera fingeravtryck"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Verifiera nyckel"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Verifierar"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Version"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Raderar mycket säkert $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Visa alla processorer"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Visa alla diskar"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Visa alla loggar"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Visa alla tjänster"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Visa automatiseringsskript"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Visa detaljerade loggar"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Visa hårdvarudetaljer"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Visa inloggningshistorik"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "Visa loggar"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Visa mätvärden och historik"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Visa per disk genomströmning"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Visa projektwebbsajt"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Visa rapport"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Visa uppdateringsloggen"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "Att visa minnesinformation kräver administrativ åtkomst."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Besök brandvägg"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Volymgrupp"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+msgid "Volume group is missing physical volumes"
+msgstr "Volymgruppen saknar fysiska volymer"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Volymstorlek är $0. Innehållsstorlek är $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Väntar"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Väntar på detaljer …"
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Väntar på inmatning…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr "Väntar på att andra program skall sluta använda pakethanteraren …"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Väntar på att andra programvaruhanteringsåtgärder skall bli klara"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Väntar på att starta …"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Önskat av"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Önskar"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Varning och högre"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+msgid "Weak password"
+msgstr "Svagt lösenord"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Webbkonsol"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Webbkonsol för Linuxservrar"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Webbkonsolen kommer att starta om"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Webbkonsolen körs i begränsad åtkomstläge."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Webbkonsolens logotyp"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Onsdagar"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Veckovis"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Veckor"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "När"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "När tomt, anslut med den aktuella användaren"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"När webbkonsolen startas om kommer man inte längre att se "
+"förloppsinformation. Dock kommer uppdateringsprocessen att fortsätta i "
+"bakgrunden. Återanslut för att fortsätta se uppdateringsprocessen."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"När det här alternativet är markerat kommer den nya poolen inte att tillåta "
+"överprovisionering. Du måste ange en maximal storlek för varje filsystem som "
+"skapas i poolen. Filsystem kan inte göras större efter att de skapats. "
+"Ögonblicksbilder tilldelas helt när de skapas. Summan av alla maximala "
+"storlekar får inte överstiga poolens storlek. Fördelen med detta är att "
+"filsystemen i denna pool inte kan få ont om utrymme på ett överraskande "
+"sätt. Nackdelen är att du behöver veta den maximala storleken för varje "
+"filsystem i förväg och skapande av ögonblicksbilder är begränsad."
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Vit"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "Kommer att ställas in på \"Automatisk\""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+msgid "World wide name"
+msgstr "Världsomfattande namn"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Skriv"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Skriv huvudsakligen"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Skriver"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Fel användarnamn eller lösenord"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Årligen"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Ja"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Du ansluter till $0 för första gången."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Du har inte rättigheter att ändra brandväggen."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Du har inte rättigheter att se de auktoriserade publika nycklarna för detta "
+"konto."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Du har blivit utloggad på grund av inaktivitet."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Du kan försöka ladda äldre poster."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr "Du kanske vill ändra lösenordet för nyckeln för automatisk inloggning."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Du måste vänta längre på att ändra ditt lösenord"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr "Man behöver logga in igen för att kunna se mätvärdeshistoriken"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Du har nu administrativ åtkomst."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Du kommer att loggas ut om $0 sekunder."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Ditt konto"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"Din webbläsare tillåter inte att klistra in från sammanhangsmenyn. Du kan "
+"använda Skift+Insert."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "Din webbläsare kommer ihåg din åtkomstnivå över sessioner."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Din server kommer stänga förbindelsen snart. Du kan återansluta efter att "
+"den har startat om."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Din session har avslutats."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Din session har gått ut. Logga in igen."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Zon"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[binärdata]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[inga data]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "åtkomst"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "aktiv"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "tillägg"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "tillägg"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "efter nätverk"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "program"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "tillgångstagg"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "klockan"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "genomsnitt: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "bakomliggande enhet för VDO-enheten"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "bindning"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "start"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "brygga"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+msgid "btrfs device"
+msgstr "btrfs enhet"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+msgid "btrfs devices"
+msgstr "btrfs enheter"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+msgid "btrfs subvolume"
+msgstr "btrfs undervolym"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr "btrfs undervolym $0 av $1"
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+msgid "btrfs subvolumes"
+msgstr "btrfs undervolymer"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+msgid "btrfs volume"
+msgstr "btrfs volym"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "felrättning"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "byte"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+msgid "cache"
+msgstr "cache"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "ändra"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman är inte installerat"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "kommando"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "konsol"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "coredump"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "cpu"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "krasch"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+msgid "data"
+msgstr "data"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "datum"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+msgid "deactivate"
+msgstr "avaktivera"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "felsök"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "ta bort"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr "enhet av btrfs volym"
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "avaktivera"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "disk"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "diskar"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "domän"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "disk"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "redigera"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "aktivera"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+msgid "encrypted"
+msgstr "krypterad"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "kryptering"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "förbättring"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "fel"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "varje fredag"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "varje måndag"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "varje lördag"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "varje söndag"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "varje torsdag"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "varje tisdag"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "varje onsdag"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "varje dag"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "tillägg"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "misslyckades att lista ssh-värdnycklar: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "filsystem"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "brandvägg"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "för nuvarande och framtida kärnor"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "för nuvarande kärna endast"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "formatera"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "från $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "från $0 på $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "grafer"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "väx"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "hårdvara"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "historik"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "värd"
+
+#: pkg/storaged/drive/drive.jsx:65
+msgid "iSCSI Drive"
+msgstr "iSCSI-disk"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+msgid "iSCSI drives"
+msgstr "iSCSI-diskar"
+
+#: pkg/storaged/iscsi/session.jsx:45
+msgid "iSCSI portal"
+msgstr "iSCSI-portal"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "ignorera fel"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "i de flesta webbläsare"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "inkonsekvent"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "initierar"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "installera"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "gränssnitt"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "felaktigt: flera mål definierade"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "journal"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "journalctl mansida"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "kdump-status"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "nycklar"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "login"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "maskin"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "mask"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "max: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+msgid "member of MDRAID device"
+msgstr "medlem i MDRAID-enhet"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "medlem i Stratis pool"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "minne"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "mått"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "rättningar"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "mer detaljer"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "montering"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "nätverk"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "Montera aldrig vid uppstart"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "nfs-exporten är tom"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "nfs-servern är tom"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "nfs-servern är inte giltig IPv6"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "nice"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "ingen"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "av $0 CPU-kärna"
+msgstr[1] "av $0 CPU-kärnor"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "på $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "operativsystem"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "os"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "paket"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "partition"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "lösenord"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "lösenordskvalitet"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "rättningar"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "sökväg"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "prestanda"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "fysisk volym på LVM2 volymgrupp"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "insticksmodul"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service har misslyckats"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service misslyckas att samla in data"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service körs inte"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "kapsel"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "port"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "ström"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "läs endast"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "rekommenderad"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "ta bort från LVM2"
+
+#: pkg/storaged/utils.js:934
+msgid "remove from MDRAID"
+msgstr "tar bort från MDRAID"
+
+#: pkg/storaged/utils.js:904
+msgid "remove from btrfs volume"
+msgstr "ta bort från btrfs volym"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "starta om"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "roller"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "kör $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "säkerhet"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "seriell"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "tjänst"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "skal"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "visa mindre"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "visa mer"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "krymp"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "stäng"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "uttag"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "lösning"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "lösningsdetaljer"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "sos rapportering misslyckades"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ssh-nyckeln är inte en sökväg"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "ssh-servern är tom"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "stoppa"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "Stoppa uppstart vid misslyckande"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+msgid "stopped"
+msgstr "stoppad"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "mål"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "team"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "tid"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "timer"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "enhet"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "okänd"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "okänt mål"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "avmaskera"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "avmontera"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "opartitionerat utrymmer på $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "användare"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "användarnamn"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "använder nyckelbeskrivning $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "version"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "volym"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "varning"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "wireguard-tools paketet är inte installerat"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "ja"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "zon"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Tjänsten kdump är inte installerad. Se till att paketet kexec-tools är "
+#~ "installerat."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Inget minne är reserverat. Lägg till ett crashkernel-argument till "
+#~ "kärnans kommandorad (t.ex. i /etc/default/grub) för att reservera minne "
+#~ "vid uppstartstillfället. Exempel: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Tjänsten kör"
+
+#~ msgid "Service is starting"
+#~ msgstr "Tjänsten startar"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Tjänsten är stoppad"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Tjänsten stoppas"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Detta kommer testa kdump-inställningarna genom att krascha kärnan."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "crashkernel är inte konfigurerad i kärnans kommandorad"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (krypterad)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 Stratis-pool"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 cache"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 av okänd nivå"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 fritt"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr ""
+#~ "En reservdisk behöver läggas till först före denna disk kan tas bort."
+
+#~ msgid "Backing device"
+#~ msgstr "Bakomliggande enhet"
+
+#~ msgid "Block"
+#~ msgstr "Block"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Kapacitet"
+
+#~ msgid "Content"
+#~ msgstr "Innehåll"
+
+#~ msgid "Create devices"
+#~ msgstr "Skapa enheter"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Enhet"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Enhetsfil"
+
+#~ msgid "Devices"
+#~ msgstr "Enheter"
+
+#~ msgid "Drives"
+#~ msgstr "Enheter"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Krypterade volymer behöver låsas upp innan deras storlek kan ändras."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Filsystem (krypterat)"
+
+#~ msgid "Filesystems"
+#~ msgstr "Filsystem"
+
+#~ msgid "Free"
+#~ msgstr "Ledigt"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Frigör utrymme i denna grupp: krymp eller radera andra logiska volymer "
+#~ "eller lägg till en ytterligare fysisk volym."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Inaktiv volym"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Nyckelserver"
+
+#~ msgid "LVM2 member"
+#~ msgstr "LVM2-medlem"
+
+#~ msgid "Locked devices"
+#~ msgstr "Låsta enheter"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Låst krypterad data"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Modell"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS-monteringar"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "Stöd för NFS är inte installerat"
+
+#~ msgid ""
+#~ "New logical volumes can not be created while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "Nya logiska volymer kan inte skapas medan en volymgrupp saknar fysiska "
+#~ "volymer."
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Inga NFS-monteringar uppsatta"
+
+#~ msgid "No devices"
+#~ msgstr "Inga enheter"
+
+#~ msgid "No drives attached"
+#~ msgstr "Inga diskar är anslutna"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Inga iSCSI-mål är uppsatta"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Inte tillräckligt med utrymme för nya filsystem"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Andra data"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Partitionerad blockenhet"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Lösenfras"
+
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "Fysiska volymer kan inte tas bort medan en volymgrupp saknar fysiska "
+#~ "volymer."
+
+#~ msgid "Pool"
+#~ msgstr "Pool"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Pool för tunna volymer"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID-nivå"
+
+#~ msgid "RAID member"
+#~ msgstr "RAID-medlem"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Löstagbar disk"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Visa $0 enhet"
+#~ msgstr[1] "Visa alla $0 enheter"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Visa $0 disk"
+#~ msgstr[1] "Visa alla $0 diskar"
+
+#~ msgid "Show all"
+#~ msgstr "Visa alla"
+
+#~ msgid "Source"
+#~ msgstr "Källa"
+
+#~ msgid "Start pool"
+#~ msgstr "Start pool"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Starta pool för att se filsystem."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Tillstånd"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Stoppa Stratis pool"
+
+#~ msgid "Stratis member"
+#~ msgstr "Stratis medlem"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Stratis-pool $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "Stöd är installerat."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr "RAID-enheten måste köra för att kunna lägga till reservdiskar."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "Den sista disken i en RAID-enhet kan inte tas bort."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "Den sista fysiska volymen i en volymgrupp kan inte tas bort."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Det finns inte tillräckligt med fritt utrymme någon annanstans för att ta "
+#~ "bort denna fysiska volym. Åtminstone $0 mer ledigt utrymme behövs."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "Denna disk kan inte tas bort medan enheten återhämtar sig."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr "Denna volym behöver aktiveras före dess storlek kan ändras."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Okända data"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Användning"
+
+#~ msgid "Used for"
+#~ msgstr "Används för"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "VDO-underlag"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO-enhet"
+
+#~ msgid "Volume"
+#~ msgstr "Volym"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Automatiskt (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Acceptera nyckel och anslut"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Storleken på krypterade volymer kan inte ändras här."
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Använd en Tang-nyckelserver"
+
+#~ msgid "Use a passphrase"
+#~ msgstr "Använd en lösenfras"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "bitmap"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Denna pool kan inte låsas upp här eftersom dess nyckelbeskrivning inte är "
+#~ "i det förväntade formatet."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Växla bitmap"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Se till att kontrollsumman för nyckeln från Tang-servern matchar något av "
+#~ "följande:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Kontrollera manuellt med SSH: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Portnummer och typ matchar inte"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Låst krypterad Stratis pool"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Fel när larmet togs bort: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Kan inte hämta larmet: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 byte med binärdata]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Disk-I/O-topp"
+
+#~ msgid "Event"
+#~ msgstr "Händelser"
+
+#~ msgid "Event logs"
+#~ msgstr "Händelseloggar"
+
+#~ msgid "Load spike"
+#~ msgstr "Belastningstopp"
+
+#~ msgid "Memory spike"
+#~ msgstr "Minnestopp"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Nätverks-I/O-topp"
+
+#~ msgid "ssh key"
+#~ msgstr "ssh-nyckel"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Om det här alternativet är markerat kommer filsystemet inte att monteras "
+#~ "under nästa uppstart även om det monterades innan det. Detta är "
+#~ "användbart om montering under uppstart inte är möjlig, till exempel när "
+#~ "en lösenordsfras krävs för att låsa upp filsystemet men uppstart är "
+#~ "obevakad."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Montera aldrig vid uppstart"
+
+#~ msgid "Listing unit files"
+#~ msgstr "Lista enhetsfiler"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "Misslyckades att lista enhetsfiler: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "Enheten finns inte"
+
+#~ msgid "Validating key"
+#~ msgstr "Validerar nyckeln"
+
+#, fuzzy
+#~| msgid "Delete $0 volume"
+#~| msgid_plural "Delete $0 volumes"
+#~ msgid "Delete $0 group"
+#~ msgstr "Ta bort volymen $0"
+
+#~ msgid "Container administrator"
+#~ msgstr "Behållaradministratör"
+
+#~ msgid "Image builder"
+#~ msgstr "Avbildsbyggare"
+
+#~ msgid "Roles"
+#~ msgstr "Roller"
+
+#~ msgid "Server administrator"
+#~ msgstr "Serveradministratör"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Unix grupp: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Kopierade till tangentbord"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Diagnostikrapporter"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Kärndump"
+
+#~ msgid "Software Updates"
+#~ msgstr "Programvaruuppdateringar"
+
+#~ msgid "Update log"
+#~ msgstr "Uppdatera loggen"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "nfs-dumpmålet är inte formaterat som server:sökväg"
+
+#~ msgid "$0 Zone"
+#~ msgstr "$0 Zon"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Skapa en diagnostisk rapport"
+
+#~ msgid "Done!"
+#~ msgstr "Klar!"
+
+#~ msgid "Download report"
+#~ msgstr "Hämta en rapport"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Inget arkiv har skapats."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Bekräfta raderingen av $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "Det genererade arkivet innehåller data som betraktas som känsligt och "
+#~ "dess innehåll bör granskas av ursprungsorganisationen innan den skickas "
+#~ "till någon tredje part."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Detta verktyg kommer samla in systemkonfiguration och diagnostisk "
+#~ "information från systemet för att användas vid diagnostisering av problem "
+#~ "med systemet."
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "Servern saknar cockpit-systempaketet"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Detta paket är inte kompatibelt med denna version av Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Detta paket behöver Cockpit version %s eller senare"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Prestandamått"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "läs endast"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB totalt"
+
+#~ msgid "Apply"
+#~ msgstr "Lägg på"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Inte behörig att ta bort zon $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Klicka på $0 igen för att använda detta lösenord ändå."
+
+#~ msgid "Access"
+#~ msgstr "Åtkomst"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "DISKEN GÅR SÖNDER"
+
+#~ msgid "Logical Size"
+#~ msgstr "Logisk Storlek"
+
+#~ msgid "Security updates "
+#~ msgstr "Säkerhetsuppdateringar "
+
+#~ msgid "Updates "
+#~ msgstr "Uppdateringar "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Valfritt)"
+
+#~ msgid "Active since"
+#~ msgstr "Aktivt sedan"
+
+#, fuzzy
+#~| msgid "Current allocation"
+#~ msgid "Affected locations"
+#~ msgstr "Nuvarande indelning"
+
+#~ msgid "Process"
+#~ msgstr "Process"
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Remove and delete"
+#~ msgstr "Stoppa och radera"
+
+#~ msgid "Remove and format"
+#~ msgstr "Ta bort och formatera"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and grow"
+#~ msgstr "Inloggningsformat"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and initialize"
+#~ msgstr "Inloggningsformat"
+
+#, fuzzy
+#~| msgid "Login format"
+#~ msgid "Remove and shrink"
+#~ msgstr "Inloggningsformat"
+
+#, fuzzy
+#~| msgid "Remove device"
+#~ msgid "Remove and stop device"
+#~ msgstr "Ta bort enhet"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Filsystemet används av inloggningssesioner och systemtjänster. Att gå "
+#~ "vidare kommer stoppa dessa."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Filsystemet används av inloggningssessioner. Att gå vidare kommer stoppa "
+#~ "dessa."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Filsystemet används av systemtjänster. Att gå vidare kommer stoppa dessa."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Denna enhet har filsystem som används just nu. Att gå vidare kommer "
+#~ "avmontera alla filsystem på den."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Denna enhet används just nu till volymgrupper."
+
+#, fuzzy
+#~| msgid ""
+#~| "This device is currently used for volume groups. Proceeding will remove "
+#~| "it from its volume groups."
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Denna enhet används just nu till volymgrupper. Att gå vidare kommer ta "
+#~ "bort den från sin volymgrupper."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Denna enhet används just nu till RAID-enheter."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Denna enhet används just nu till RAID-enheter. Att gå vidare kommer ta "
+#~ "bort den från sin RAID-enhet."
+
+#, fuzzy
+#~| msgid "This device is currently used for volume groups."
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Denna enhet används just nu till volymgrupper."
+
+#, fuzzy
+#~| msgid "Stop and delete"
+#~ msgid "Unmount and delete"
+#~ msgstr "Stoppa och radera"
+
+#~ msgid "Unmount and format"
+#~ msgstr "Avmontera och formatera"
+
+#, fuzzy
+#~| msgid "Unmounting $target"
+#~ msgid "Unmount and grow"
+#~ msgstr "Avmonterar $target"
+
+#, fuzzy
+#~| msgid "Unmounting $target"
+#~ msgid "Unmount and initialize"
+#~ msgstr "Avmonterar $target"
+
+#, fuzzy
+#~| msgid "Unmounting $target"
+#~ msgid "Unmount and shrink"
+#~ msgstr "Avmonterar $target"
+
+#, fuzzy
+#~| msgid "On a mounted device"
+#~ msgid "Unmount and stop device"
+#~ msgstr "På en monterad enhet"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 av $1"
+
+#, fuzzy
+#~| msgid "Blocked"
+#~ msgid "Blockdev"
+#~ msgstr "Blockerat"
+
+#, fuzzy
+#~| msgid "Physical volume of $0"
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Fysisk volym för $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Medlem i RAID-enhet $0"
+
+#~ msgid "VDO backing"
+#~ msgstr "VDO-underlag"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Skapa en lagringspool"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "Monteringsflaggor"
+
+#, fuzzy
+#~| msgid "Reset Storage Pool"
+#~ msgid "Stratis Pool"
+#~ msgstr "Återställ lagringspoolen"
+
+#~ msgid "A disk is needed."
+#~ msgstr "En disk behövs."
+
+#~ msgid "Disk"
+#~ msgstr "Disk"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Bara för gamla program. Går långsamt."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Installera stöd för VDO"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Använd 512-byteemulering"
+
+#~ msgid "System services"
+#~ msgstr "Systemtjänster"
+
+#, fuzzy
+#~| msgid "Create diagnostic report"
+#~ msgid "Create diagnostic report "
+#~ msgstr "Skapa en diagnostisk rapport"
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr "Att ansluta till mer än {{ limit }} maskiner samtidigt stödjs inte."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "Kerberos-baserad SSO"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Logga in på {{host}}"
+
+#, fuzzy
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Lösenorden stämmer inte överens"
+
+#, fuzzy
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit kunde inte kontakta {{#strong}}{{host}}{{/strong}}."
+
+#, fuzzy
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Cockpit kunde inte logga in på {{#strong}}{{host}}{{/strong}}. "
+#~ "{{#can_sync}}Du kan vilja försöka att {{#sync_link}}synkronisera "
+#~ "användare{{/sync_link}}.{{/can_sync}} För fler autentiseringsalternativ "
+#~ "och hjälp med felsökning, uppgradera cockpit-ws till en nyare version."
+
+#, fuzzy
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Cockpit kunde inte logga in på {{#strong}}{{host}}{{/strong}}. För att "
+#~ "använda denna maskin med cockpit behöver du aktivera en av följande "
+#~ "autentiseringsmetoder i sshd-konfigurationen på {{#strong}}{{host}}{{/"
+#~ "strong}}:"
+
+#, fuzzy
+#~| msgid "Force change"
+#~ msgid "{{host}} key changed"
+#~ msgstr "Framtvinga ändring"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Nollställ inställningarna för monteringspunkter"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Att skapa denna bindning kommer bryta anslutningen till servern, och "
+#~ "kommer göra administrationsgränssnittet otillgängligt."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Att skapa denna brygga kommer bryta anslutningen till servern, och kommer "
+#~ "göra administrationsgränssnittet otillgängligt."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Att skapa detta team kommer bryta anslutningen till servern, och kommer "
+#~ "göra administrationsgränssnittet otillgängligt."
+
+#, fuzzy
+#~| msgid "IPv4 settings"
+#~ msgid "IP settings"
+#~ msgstr "IPv4-inställningar"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Att slå av <b>$0</b> kommer bryta anslutningen till servern, och kommer "
+#~ "göra administrationsgränssnittet otillgängligt."
+
+#~ msgid "Administrator password"
+#~ msgstr "Administratörslösenord"
+
+#~ msgid "Computer OU"
+#~ msgstr "Dator-OU"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "Att ta bort en RAID-enhet kommer att radera all data på den."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "Att ta bort en volymgrupp kommer att radera all data i den."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Skriv inte över befintliga data"
+
+#~ msgid "Erase"
+#~ msgstr "Radera"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Formatera disken $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr "Att formatera en lagringsenhet kommer att radera all data på den."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Datornamn skall inte ändras i en domän"
+
+#~ msgid "More"
+#~ msgstr "Mera"
+
+#, fuzzy
+#~ msgid "Not joined"
+#~ msgstr "Inte monterat"
+
+#~ msgid "One time password"
+#~ msgstr "Engångslösenord"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 från $1 på $2"
+
+#, fuzzy
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Timmen måste vara ett tal mellan 0-23"
+
+#~ msgid "Last login:"
+#~ msgstr "Senaste inloggningen:"
+
+#, fuzzy
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Minuterna måste vara ett tal mellan 0-59"
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "t.ex. ”$0”"
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 Aktiva zoner"
+
+#, fuzzy
+#~| msgid "$0 occurrence"
+#~| msgid_plural "$1 occurrences"
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 förekomst"
+#~ msgstr[1] "$0 förekomster"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Licensierad under:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Lås upp vid uppstart"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Lås upp endast för läsning"
+
+#, fuzzy
+#~ msgid "Show filters"
+#~ msgstr "Visa fingeravtryck"
+
+#, fuzzy
+#~ msgid "e.g."
+#~ msgstr "t.ex. ”$0”"
+
+#, fuzzy
+#~ msgid "Toggle session settings"
+#~ msgstr "Ändra inställningarna"
+
+#~ msgid "(none)"
+#~ msgstr "(ingen)"
+
+#~ msgid "Create timers"
+#~ msgstr "Skapa timrar"
+
+#~ msgid "Run"
+#~ msgstr "Kör"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select unit state"
+#~ msgstr "Konsoltyp"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Aktivera mätdatalagring"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Aktivera mätdatalagring"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Framtvinga borttagande av lösenfras i $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Om tang-show-keys inte är tillgängligt, kör det följande:"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Vad gör man om tang-show-keys inte är tillgängligt?"
+
+#~ msgid "key slot $0"
+#~ msgstr "nyckelfack $0"
+
+#, fuzzy
+#~| msgid "Automatic updates"
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Automatiska uppdateringar"
+
+#, fuzzy
+#~| msgid "IP configuration"
+#~ msgid "$0 CPU configuration"
+#~ msgstr "IP-konfiguration"
+
+#, fuzzy
+#~| msgid "$0 network"
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 Nätverk"
+#~ msgstr[1] "$0 Nätverk"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 är tillgängligt till de flesta operativsystem. Sök efter det i GNOME-"
+#~ "programvara för att installera det eller kör följande:"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 Nätverk"
+
+#, fuzzy
+#~| msgid "vCPUs"
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "vCPU:er"
+#~ msgstr[1] "vCPU:er"
+
+#, fuzzy
+#~ msgid "$0 vCPU details"
+#~ msgstr "Problemdetaljer"
+
+#, fuzzy
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "Lägg till virtuell nätverksport"
+
+#~ msgid "Add network interface"
+#~ msgstr "Lägg till nätverksport"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Lägg till virtuell nätverksport"
+
+#~ msgid "Additional"
+#~ msgstr "Ytterligare"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Adress inte inom subnät"
+
+#~ msgid "Always attach"
+#~ msgstr "Anslut alltid"
+
+#, fuzzy
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr " Inkoppling av denna disk gör att alla VM:er kan använda den."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Starta automatiskt libvirt vid uppstart"
+
+#~ msgid "Autostart"
+#~ msgstr "Starta automatiskt"
+
+#~ msgid "Boot order"
+#~ msgstr "Uppstartsordning"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Uppstartsordningen kunde inte sparas"
+
+#~ msgid "Bus"
+#~ msgstr "Buss"
+
+#, fuzzy
+#~| msgid "VCPU settings could not be saved"
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "VCPU-inställningarna kunde inte sparas"
+
+#~ msgid "CPU type"
+#~ msgstr "CPU-typ"
+
+#~ msgid "Change firmware"
+#~ msgstr "Ändra hårdvarufirmware"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Ändringar kommer verkställas efter att VM:en stängs av"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Välj ett operativsystem"
+
+#, fuzzy
+#~| msgid ""
+#~| "Clicking \"Launch Remote Viewer\" will download a .vv file and launch $0."
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "Att klicka på ”Starta fjärrvisare” kommer hämta en .vv-fil och starta $0."
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Kan inte ladda avbilden"
+
+#~ msgid "Connect"
+#~ msgstr "Anslut"
+
+#, fuzzy
+#~| msgid "Connect with any $0 viewer application."
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "Anslut med godtycklig $0-visarprogram."
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Ansluter till virtualiseringstjänsten"
+
+#~ msgid "Connection"
+#~ msgstr "Anslutning"
+
+#, fuzzy
+#~| msgid "Consoles"
+#~ msgid "Console"
+#~ msgstr "Konsoler"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Kärnor per sockel"
+
+#, fuzzy
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Kunde inte återställa lagringspoolen"
+
+#, fuzzy
+#~| msgid "crashed"
+#~ msgid "Crashed"
+#~ msgstr "kraschad"
+
+#~ msgid "Create VM"
+#~ msgstr "Skapa en VM"
+
+#, fuzzy
+#~| msgid "Create partition on $0"
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Skapa partition på $0"
+
+#~ msgid "Create new"
+#~ msgstr "Skapa ny"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Skapa en ny virtuell maskin"
+
+#, fuzzy
+#~ msgid "Creating VM"
+#~ msgstr "Skapa en VM"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Misslyckades att skapa VM $0"
+
+#, fuzzy
+#~ msgid "Creation time"
+#~ msgstr "Skapa en timer"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Nuvarande indelning"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Eget hårdvaruprogram: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP område"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Ta bort assoicierade lagringsfiler:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Ta bort lagringspool $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Ta bort volymerna i denna pool"
+
+#, fuzzy
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Att ta bort en inaktiv lagringspool, tar bara bort poolnamnet. Innehållet "
+#~ "raderas inte."
+
+#, fuzzy
+#~| msgid "Desktop"
+#~ msgid "Desktop viewer"
+#~ msgstr "Skrivbord"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Koppla bort diskarna från VM:er som använder denna pool innan du försöker "
+#~ "ta bort poolen."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Frånkopplad från seriekonsolen. Klicka på Anslut."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Disken $0 kunde inte kopplas bort från VM $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Disken kunde inte anslutas"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Disken kunde inte skapas"
+
+#, fuzzy
+#~| msgid "Device file"
+#~ msgid "Disk image file"
+#~ msgstr "Enhetsfil"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Inställningarna för disken kunde inte sparas"
+
+#~ msgid "Download an OS"
+#~ msgstr "Ladda ner ett OS"
+
+#, fuzzy
+#~| msgid "dying"
+#~ msgid "Dying"
+#~ msgstr "döende"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Emulerad maskin"
+
+#~ msgid "End"
+#~ msgstr "Slut"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Slutet kan inte vara tomt"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "En annan disk på den här datorns filsystem"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Misslyckades att ändra hårdvaruprogram"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "Kunde inte hämta IP-adresser från portar i $0"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "Kunde inte skicka Ctrl+Alt+$0 till VM $1"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr ""
+#~ "Färre än det maximala antalet virtuella CPU:er skall vara aktiverade."
+
+#~ msgid "File"
+#~ msgstr "Arkiv"
+
+#, fuzzy
+#~ msgid "Filter by name"
+#~ msgstr "Filtrera efter namn eller beskrivning"
+
+#~ msgid "Firmware"
+#~ msgstr "Hårdvaruprogram"
+
+#~ msgid "Force shut down"
+#~ msgstr "Framtvinga avstängning"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Skapa automatiskt"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Göm ytterligare inställningar"
+
+#~ msgid "Host device"
+#~ msgstr "Värdenhet"
+
+#~ msgid "Host name"
+#~ msgstr "Värdnamn"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Värden får inte vara tom"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Detaljer om hypervisorn"
+
+#~ msgid "IP configuration"
+#~ msgstr "IP-konfiguration"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 och IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4 nätverk"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv4 nätverk kan inte vara tom"
+
+#~ msgid "IPv4 only"
+#~ msgstr "IPv4 enbart"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6-adress"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6 nätverk"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6 nätverk kan inte vara tom"
+
+#~ msgid "IPv6 only"
+#~ msgstr "IPv6 enbart"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Starta VM:en omedelbart"
+
+#~ msgid "Import"
+#~ msgstr "Importera"
+
+#~ msgid "Import VM"
+#~ msgstr "Importera VM"
+
+#, fuzzy
+#~ msgid "Import a virtual machine"
+#~ msgstr "Importera en virtuell maskin"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "I de flesta fall kan man inte använda macvtap för att kommunicera mellan "
+#~ "host och VM."
+
+#~ msgid "Initiator"
+#~ msgstr "Initierare"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "Initierarens IQN kan inte vara tom"
+
+#, fuzzy
+#~| msgid "Installation source"
+#~ msgid "Installation source"
+#~ msgstr "Installationskälla"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Installationskällan kan inte vara tom"
+
+#~ msgid "Installation type"
+#~ msgstr "Installationstyp"
+
+#~ msgid "Interface type"
+#~ msgstr "Gränssnittstyp"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Felaktig IPv4 nätverksmask eller prefix"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Felaktig IPv6-adress"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Felaktigt IPv6-prefix"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Felaktigt filnamn"
+
+#, fuzzy
+#~| msgid "Launch Remote Viewer"
+#~ msgid "Launch remote viewer"
+#~ msgstr "Starta fjärrvisare"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr "Lämna lösenordet tomt om du inte vill skapa en superanvändare"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr "Lämna lösenordet tomt om du inte vill skapa en användare"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr "Lämna lösenordet tomt om du inte vill skapa en superanvändare"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt hittade inte något UEFI/OVMF hårdvaruprogram installerat på värden"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt eller hypervisor stödjer inte UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "Laddar resurser"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "Maskinen måste vara avstängd innan det går att ändra busstyp"
+
+#, fuzzy
+#~| msgid "Machine must be shut off before changing bus type"
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "Maskinen måste vara avstängd innan det går att ändra busstyp"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Hantera virtuella maskiner"
+
+#~ msgid "Manual connection"
+#~ msgstr "Manuell anslutning"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Mask eller prefixlängd"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Mask eller prefixlängd kan inte vara tom"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Maximal tilldelning"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "Maximalt minne kunde inte sparas"
+
+#, fuzzy
+#~| msgid ""
+#~| "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~| "between 1 and $0"
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Maximalt antal virtuella CPU:er allokerade till gäst-OS:et, vilket måste "
+#~ "vara mellan 1 och $0"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Maximalt antal virtuella CPU:er allokerade till gäst-OS:et, vilket måste "
+#~ "vara mellan 1 och $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Maximal överföringsstorlek"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Minnet kunde inte sparas"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "Minne kan inte vara 0"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Modelltyp"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT till $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "NIC $0 på VM $1 kunde inte ändra läge"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Namnet innehåller ogiltiga tecken"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Namnet kan inte vara tomt"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Namnet får inte vara tomt"
+
+#~ msgid "Name: "
+#~ msgstr "Namn:"
+
+#~ msgid "Netmask"
+#~ msgstr "Nätmask"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Kunde inte starta nätverk $0"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Kunde inte stoppa nätverk $0"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Nätverksboot (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Nätverksfilsystem"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "Nätverksvalet tillåter inte PXE."
+
+#~ msgid "Networks"
+#~ msgstr "Nätverk"
+
+#~ msgid "New volume name"
+#~ msgstr "Nytt volymnamn"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "Ingen VM kör eller är definierad på denna värd."
+
+#, fuzzy
+#~| msgid "Not available"
+#~ msgid "No connection available"
+#~ msgstr "Inte tillgängligt"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Inga diskar är definerade för denna VM"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Inga nätverksgränssnitt är definierade för denna VM"
+
+#, fuzzy
+#~ msgid "No parent"
+#~ msgstr "Förälder"
+
+#, fuzzy
+#~| msgid "No disks defined for this VM"
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Inga diskar är definerade för denna VM"
+
+#, fuzzy
+#~ msgid "No state"
+#~ msgstr "Tillstånd"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "Ingen lagringspool är definierad på denna värd"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "Inga lagringsvolymer är definierade för denna lagringspool"
+
+#~ msgid "Operating system"
+#~ msgstr "Operativsystem"
+
+#, fuzzy
+#~ msgid "Parent snapshot"
+#~ msgstr "Skapa en ögonblicksbild"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Sökväg på värdens filsystem"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Sökväg till ISO-fil på värdens filsystem"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Sökväg till ISO-fil på värdens filsystem"
+
+#, fuzzy
+#~| msgid "Path to ISO file on host's file system"
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Sökväg till ISO-fil på värdens filsystem"
+
+#, fuzzy
+#~| msgid "paused"
+#~ msgid "Paused"
+#~ msgstr "stannad"
+
+#~ msgid "Persistence"
+#~ msgstr "Varaktighet"
+
+#, fuzzy
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Kunde inte återställa lagringspoolen"
+
+#, fuzzy
+#~ msgid "Please choose a volume"
+#~ msgstr "Ange ett nytt volymnamn"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Ange ett nytt volymnamn"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Starta den virtuella maskinen för att komma åt dess konsol."
+
+#~ msgid "Plug"
+#~ msgstr "Plugg"
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Föredraget antal uttag att exponera för gästen."
+
+#, fuzzy
+#~ msgid "Profile"
+#~ msgstr "Ändra profil"
+
+#~ msgid "Protocol"
+#~ msgstr "Protokoll"
+
+#~ msgid "Remote URL"
+#~ msgstr "Fjärr-URL"
+
+#, fuzzy
+#~| msgid "Hypervisor details"
+#~ msgid "Remote viewer details"
+#~ msgstr "Detaljer om hypervisorn"
+
+#, fuzzy
+#~ msgid "Revert"
+#~ msgstr "Aldrig"
+
+#, fuzzy
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Skapa en ögonblicksbild"
+
+#, fuzzy
+#~ msgid "Root password"
+#~ msgstr "Sätt lösenord"
+
+#, fuzzy
+#~| msgid "SPICE TLS port:"
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE TLS-port:"
+
+#, fuzzy
+#~| msgid "SPICE address:"
+#~ msgid "SPICE address"
+#~ msgstr "SPICE-adress:"
+
+#, fuzzy
+#~| msgid "SPICE port:"
+#~ msgid "SPICE port"
+#~ msgstr "SPICE-port:"
+
+#, fuzzy
+#~| msgid "Console type"
+#~ msgid "Select console type"
+#~ msgstr "Konsoltyp"
+
+#~ msgid "Send key"
+#~ msgstr "Skicka tangenttryck"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Skicka ej maskerbart avbrott"
+
+#~ msgid "Serial console"
+#~ msgstr "Seriekonsol"
+
+#, fuzzy
+#~ msgid "Show additional options"
+#~ msgstr "Ytterligare åtgärder"
+
+#, fuzzy
+#~ msgid "Shut off"
+#~ msgstr "stäng av"
+
+#, fuzzy
+#~ msgid "Shutting down"
+#~ msgstr "Stäng av"
+
+#, fuzzy
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Disken kunde inte skapas"
+
+#~ msgid "Source path"
+#~ msgstr "Källsökväg"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Källsökvägen får inte vara tom"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "Källan skall börja med ett av protokollen http, ftp eller nfs"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Starta libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Starta poolen när värden startar upp"
+
+#~ msgid "Startup"
+#~ msgstr "Uppstart"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Lagringspoolen kunde inte skapas"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Lagringspoolsnamn"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "vilande (SH)"
+
+#~ msgid "Target path"
+#~ msgstr "Målsökväg"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Målsökvägen får inte vara tom"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr "VM:en kör och kommer tvingande stängas av före den tas bort."
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "Katalogen på servern exporteras"
+
+#~ msgid "The pool is empty"
+#~ msgstr "Poolen är tom"
+
+#~ msgid "Threads per core"
+#~ msgstr "Trådar per kärna"
+
+#~ msgid "Unique name"
+#~ msgstr "Unikt namn"
+
+#, fuzzy
+#~ msgid "Unknown firmware"
+#~ msgstr "okänt mål"
+
+#~ msgid "Unplug"
+#~ msgstr "Koppla ur"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "VCPU-inställningarna kunde inte sparas"
+
+#, fuzzy
+#~ msgid "VM state"
+#~ msgstr "Tillstånd"
+
+#, fuzzy
+#~| msgid "VNC TLS port:"
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC TLS-port:"
+
+#, fuzzy
+#~| msgid "VNC address:"
+#~ msgid "VNC address"
+#~ msgstr "VNC-adress:"
+
+#, fuzzy
+#~| msgid "Consoles"
+#~ msgid "VNC console"
+#~ msgstr "Konsoler"
+
+#, fuzzy
+#~| msgid "VNC port:"
+#~ msgid "VNC port"
+#~ msgstr "VNC-port:"
+
+#, fuzzy
+#~ msgid "Virtual Machines"
+#~ msgstr "Virtuella maskiner"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Virtuella maskiner"
+
+#, fuzzy
+#~ msgid "Virtual machines management"
+#~ msgstr "Virtuella maskiner"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Virtualiseringstjänsten (libvirt) är inte aktiv"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "avaktiverat"
+
+#~ msgid "down"
+#~ msgstr "nere"
+
+#~ msgid "enabled"
+#~ msgstr "aktiverat"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#, fuzzy
+#~| msgid "to host path"
+#~ msgid "host passthrough"
+#~ msgstr "till värdsökväg"
+
+#~ msgid "hostdev"
+#~ msgstr "värdenhet"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "nej"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "server"
+#~ msgstr "server"
+
+#~ msgid "up"
+#~ msgstr "uppe"
+
+#~ msgid "vCPU count"
+#~ msgstr "vCPU-antal"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU-maximum"
+
+#~ msgid "vCPUs"
+#~ msgstr "vCPU:er"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhost-användare"
+
+#~ msgid "Local install media"
+#~ msgstr "Lokal installationsmedia"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#, fuzzy
+#~ msgid "Please set a root password"
+#~ msgstr "Fel användarnamn eller lösenord"
+
+#~ msgid "21st"
+#~ msgstr "21:a"
+
+#~ msgid "22nd"
+#~ msgstr "22:a"
+
+#~ msgid "23rd"
+#~ msgstr "23:e"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Efter"
+
+#~ msgid "Friday"
+#~ msgstr "Fredag"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Timme : Minut"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "Timmen måste vara ett tal mellan 0-23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Felaktigt datumformat."
+
+#~ msgid "Invalid number."
+#~ msgstr "Felaktigt tal."
+
+#~ msgid "Monday"
+#~ msgstr "Måndag"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Upprepa varje timma"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Upprepa årligen"
+
+#~ msgid "Saturday"
+#~ msgstr "Lördag"
+
+#~ msgid "Sunday"
+#~ msgstr "Söndag"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Denna dag finns inte i alla månader.<br>Timern kommer bara köras i "
+#~ "månader som har den 31:e."
+
+#~ msgid "Thursday"
+#~ msgstr "Torsdag"
+
+#~ msgid "Tuesday"
+#~ msgstr "Tisdag"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 uppdatering"
+#~ msgstr[1] "$0 uppdateringar"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 säkerhetsfix"
+#~ msgstr[1] "$1 säkerhetsfixar"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Hantera lagringsenheter"
+
+#~ msgid "Restart now"
+#~ msgstr "Starta om nu"
+
+#~ msgid "Configure"
+#~ msgstr "Konfigurera"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Nätverk"
+
+#, fuzzy
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "Filen eller katalogen finns inte"
+
+#~ msgid "No updates pending"
+#~ msgstr "Inga väntande uppdateringar"
+
+#, fuzzy
+#~| msgid "ssh server is empty"
+#~ msgid "This directory is empty"
+#~ msgstr "ssh-servern är tom"
+
+#~ msgid "undefined"
+#~ msgstr "odefinierad"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Felaktig värdnyckel"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "Autenticiteten hos värden {{#strong}}{{host}}{{/strong}} kan inte "
+#~ "fastställas. Är du säker på att du vill fortsätta ansluta?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "Nyckeln för {{#strong}}{{host}}{{/strong}} stämmer inte med den tidigare "
+#~ "använda nyckeln. Om inte denna maskin ersattes nyligen är det troligt "
+#~ "att någon försöker attackera din förbindelse till denna maskin."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Okänd värdnyckel"
+
+#~ msgid "Address:"
+#~ msgstr "Adress:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Åtminstone $0 diskar behövs."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "Anslut med godtycklig SPICE- eller VNC-visarprogram."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Hämta MSI:n från $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Grafisk konsol (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Grafisk konsol i skrivbordsvisare"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Ingen konsol definierad för denna virtuella maskin."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid "Entry"
+#~ msgstr "Post"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Din webbläsare kommer kopplas ifrån, men detta påverkar inte "
+#~ "uppdateringsprocessen. Du kan återansluta om några ögonblick för att "
+#~ "fortsätta följa förloppet."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "och starta automatiskt om maskinen."
+
+#~ msgid "Dashboard"
+#~ msgstr "Panel"
+
+#, fuzzy
+#~ msgid "Description input text"
+#~ msgstr "Beskrivning"
+
+#~ msgid "FAILED"
+#~ msgstr "MISSLYCKADES"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Förlorade förbindelsen. Försöker återansluta"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Nätverk"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Ange en ny volymstorlek"
+
+#~ msgid "Servers"
+#~ msgstr "Servrar"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "Du är för närvarande ansluten direkt till denna server. Du kan inte ta "
+#~ "bort den."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bitar"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 byte"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 byte"
+#~ msgstr[1] "$0 byte"
+
+#, fuzzy
+#~| msgid "Bond settings"
+#~ msgid "Bond settings "
+#~ msgstr "Bindinställningar"
+
+#, fuzzy
+#~| msgid "IP settings"
+#~ msgid "IP settings "
+#~ msgstr "IP-inställningar"
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "—"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Cockpit fick ett oväntat internt fel. <br/><br/>Du kan försöka starta om "
+#~ "Cockpit genom att trycka ladda om i din webbläsare. Javaskriptkosolen "
+#~ "innehåller detaljer om detta fel (<b>Ctrl-Shift-J</b> i de flesta "
+#~ "webbläsare)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "VM:en kraschade."
+
+#~ msgid "The VM is down."
+#~ msgstr "VM:en är nere."
+
+#~ msgid "The VM is going down."
+#~ msgstr "VM:en går ner."
+
+#~ msgid "The VM is idle."
+#~ msgstr "VM:en är inaktiv."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr ""
+#~ "VM:en är på gång att dö (en avstängning eller krasch är inte fullbordad)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "VM:en är pausad."
+
+#~ msgid "The VM is running."
+#~ msgstr "VM:en kör."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "VM:en är i vila av gästens strömhantering."
+
+#~ msgid "inactive"
+#~ msgstr "inaktiv"
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Lämna tomt för att ansluta till denna maskin som den nu inloggade "
+#~ "användaren. Om du anger ett annat användarnamn kommer den användaren "
+#~ "alltid användas när man ansluter till denna maskin."
+
+#~ msgid "2 min"
+#~ msgstr "2 min"
+
+#~ msgid "3 min"
+#~ msgstr "3 min"
+
+#~ msgid "4 min"
+#~ msgstr "4 min"
+
+#~ msgid "CPU graph"
+#~ msgstr "CPU-graf"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "CPU-status"
+
+#~ msgid "Cached"
+#~ msgstr "Cachad"
+
+#~ msgid "Graphs"
+#~ msgstr "Grafer"
+
+#~ msgid "I/O wait"
+#~ msgstr "I/O-väntan"
+
+#~ msgid "Kernel"
+#~ msgstr "Kärnan"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Minne"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "Minnes och swap-användning"
+
+#~ msgid "Memory graph"
+#~ msgstr "Graf över minnesanvändning"
+
+#~ msgid "Network traffic"
+#~ msgstr "Nätverkstrafik"
+
+#~ msgid "Nice"
+#~ msgstr "Nice"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Synkronisera användare"
+
+#~ msgid "idle"
+#~ msgstr "overksam"
+
+#~ msgid "running"
+#~ msgstr "kör"
+
+#~ msgid "shut off"
+#~ msgstr "stäng av"
+
+#~ msgid "shutdown"
+#~ msgstr "avstängning"
+
+#~ msgid "Available"
+#~ msgstr "Tillgängliga"
+
+#, fuzzy
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Ange en IP-adress eller ett värdnamn"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Nätverksportar"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Denna version av cockpit-ws stödjer inte att ansluta till en värd med en "
+#~ "alternativ användare eller port"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "För att använda en annan port behöver du uppgradera cockpit-ws till en "
+#~ "nyare version."
+
+#~ msgid "$0 template"
+#~ msgstr "$0 mall"
+
+#~ msgid "Instance of template: "
+#~ msgstr "Användning av mall: "
+
+#~ msgid "Instantiate"
+#~ msgstr "Instatiera"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 utdelningar"
+
+#~ msgid "All In One"
+#~ msgstr "Allt-i-ett"
+
+#~ msgid "Always"
+#~ msgstr "Alltid"
+
+#~ msgid "Author"
+#~ msgstr "Upphovsman"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Felaktiga data skickade för passwd1-mekanismen"
+
+#~ msgid "CPU priority"
+#~ msgstr "CPU-prioritet"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Kan inte ansluta till Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Ändra resursgränser"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Ändra resursers gränser"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit kunde inte logga in på {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit kunde inte logga in på {{#strong}}{{host}}{{/strong}}. Du kan "
+#~ "ändra dina autentiseringskreditiv nedan. {{#can_sync}}Du kanske "
+#~ "föredraratt {{#sync_link}}synkronisera konton och lösenord{{/sync_link}}."
+#~ "{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Kombinerad minnesanvändning"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Kombinerad användning av $0 CPU-kärna"
+#~ msgstr[1] "Kombinerad användning av $0 CPU-kärnor"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "Kommandot får inte vara tomt"
+
+#~ msgid "Command:"
+#~ msgstr "Kommando:"
+
+#~ msgid "Commit"
+#~ msgstr "Fastställ"
+
+#~ msgid "Commit image"
+#~ msgstr "Fastställ avbild"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Ansluter till Docker"
+
+#~ msgid "Container name"
+#~ msgstr "Behållarnamn"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Behållaren är för närvarande markerad som inte körande, men normalt "
+#~ "stoppande har misslyckats."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Behållaren kör för närvarande."
+
+#~ msgid "Container:"
+#~ msgstr "Behållare:"
+
+#~ msgid "Containers"
+#~ msgstr "Behållare"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Behållare"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Kunde inte ändra användarens grupper"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Kunde inte ändra användarens lösenord"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Kunde inte ansluta till maskinen"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Kunde inte skapa nya användare"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Kunde inte lista lokala användare"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Kunde inte lista användare"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Kunde inte läsa in användardata"
+
+#~ msgid "Created:"
+#~ msgstr "Skapad:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Docker containrar"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker är inte installerat eller aktiverat på systemet"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Dubblerat alias"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Dubblerad port"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "Ange ditt lösenord för att lägga till identiteten till ssh-agent"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Att ange ett annat lösenord här innebär att du kommer behöva skriva in "
+#~ "det igen varje gång du återansluter till denna maskin"
+
+#~ msgid "Environment"
+#~ msgstr "Miljö"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Fel när användare lästes in: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Felmeddelande från Docker:"
+
+#~ msgid "Everything"
+#~ msgstr "Allting"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Avslutade $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Exponerade behållarportar"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Misslyckades att starta Docker: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Misslyckades att stoppa Docker-scope: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Hämta ny avbild"
+
+#~ msgid "IP address:"
+#~ msgstr "IP-adress:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "IP-prefixlängd:"
+
+#~ msgid "Id"
+#~ msgstr "Id"
+
+#~ msgid "Id:"
+#~ msgstr "Id:"
+
+#~ msgid "Image"
+#~ msgstr "Avbild"
+
+#~ msgid "Image $0"
+#~ msgstr "Avbild $0"
+
+#~ msgid "Image search"
+#~ msgstr "Avbildssökning"
+
+#~ msgid "Image:"
+#~ msgstr "Avbild:"
+
+#~ msgid "Images"
+#~ msgstr "Avbilder"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Avbilder"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Avbilder och körande behållare"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "För att synkronisera användare behöver du logga in på {{#strong}}{{host}}"
+#~ "{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Felaktig port"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Kerberosbiljett"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Lämna tomt för att ansluta till denna maskin som den nu inloggade "
+#~ "användaren{{#default_user}} ({{default_user}}){{/default_user}}. Om du "
+#~ "anger ett annat användarnamn kommer den användaren alltid användas när "
+#~ "man ansluter till denna maskin."
+
+#~ msgid "Link to another container"
+#~ msgstr "Länka till en annan behållare"
+
+#~ msgid "Links:"
+#~ msgstr "Länkar:"
+
+#~ msgid "Login Password"
+#~ msgstr "Inloggningslösenord"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC-adress:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Minnesgräns"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Montera behållarvolymer"
+
+#~ msgid "No container specified"
+#~ msgstr "Ingen behållare angiven"
+
+#~ msgid "No containers"
+#~ msgstr "Inga behållare"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Inga behållare som stämmer med det aktuella filtret"
+
+#~ msgid "No images"
+#~ msgstr "Inga avbilder"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Inga avbilder som stämmer med det aktuella filtret"
+
+#~ msgid "No results for $0"
+#~ msgstr "Inga resultat för $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Inga avbilder som stämmer med det aktuella filtret"
+
+#~ msgid "No running containers"
+#~ msgstr "Inga körande behållare"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Inga körande behållare som stämmer med det aktuella filtret"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Har inte rättigheter att komma åt Docker på detta system"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "Vid misslyckande, försök igen $0 gång"
+#~ msgstr[1] "Vid misslyckande, försök igen $0 gånger"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Bekräfta den tvingande raderingen av $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Försök med en annan term"
+
+#~ msgid "Ports:"
+#~ msgstr "Portar:"
+
+#~ msgid "Problems"
+#~ msgstr "Problem"
+
+#~ msgid "ReadOnly"
+#~ msgstr "LäsEndast"
+
+#~ msgid "Repository"
+#~ msgstr "Förråd"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Omstartspolicy:"
+
+#~ msgid "Retries:"
+#~ msgstr "Omförsök:"
+
+#, fuzzy
+#~| msgid "Reuse my password for privileged tasks"
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Återanvänd mitt lösenord för priviligierade uppgifter"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Välj användaren som du vill skall synkorniseras med {{#strong}}{{host}}{{/"
+#~ "strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Sätt behållarens miljövariabler"
+
+#~ msgid "Severity:"
+#~ msgstr "Allvarsgrad:"
+
+#~ msgid "Show all containers"
+#~ msgstr "Visa alla behållare"
+
+#~ msgid "Start Docker"
+#~ msgstr "Starta Docker"
+
+#~ msgid "State:"
+#~ msgstr "Tillstånd:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Synkronisera"
+
+#~ msgid "Tag"
+#~ msgstr "Tagg"
+
+#~ msgid "Tags"
+#~ msgstr "Taggar"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "Följande behållare beror på denna avbild och kommer bli oanvändbara."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Denna avbild finns inte."
+
+#~ msgid "Type a password"
+#~ msgstr "Skriv ett lösenord"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Skriv för att filtrera …"
+
+#~ msgid "Unless stopped"
+#~ msgstr "Om inte stoppad"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Uppsättningsmekanism som ej stöds"
+
+#~ msgid "Up since $0"
+#~ msgstr "Uppe sedan $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Används av behållare"
+
+#~ msgid "Using available credentials"
+#~ msgstr "Använder tillgängliga kreditiv"
+
+#~ msgid "Volumes"
+#~ msgstr "Volymer"
+
+#~ msgid "Volumes:"
+#~ msgstr "Volymer:"
+
+#~ msgid "With terminal"
+#~ msgstr "Med terminal"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Du är ansluten till {{#strong}}{{host}}{{/strong}}, dock, för att "
+#~ "synkronisera användare krävs en användare med superanvändarens "
+#~ "rättigheter."
+
+#~ msgid "default"
+#~ msgstr "standard"
+
+#~ msgid "key"
+#~ msgstr "nyckel"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "leta efter namn, namnrymd eller beskrivning"
+
+#~ msgid "shares"
+#~ msgstr "utdelningar"
+
+#~ msgid "to host port"
+#~ msgstr "till värdport"
+
+#~ msgid "value"
+#~ msgstr "värde"
+
+#~ msgid "Master"
+#~ msgstr "Huvudgränssnitt"
+
+#, fuzzy
+#~| msgid "Password"
+#~ msgid "Password for $0"
+#~ msgstr "Lösenord"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "Användaren <b>$0</b> har inte rättighet att hantera servrar"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Konton"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "Användaren <b>$0</b> har inte rättighet att ändra konton"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Det går inte att ta bort root-kontot"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Kan inte byta namn på root-kontot"
+
+#~ msgid "Account Settings"
+#~ msgstr "Kontoinställningar"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Lägg till en maskin till panelen"
+
+#~ msgid "Machines"
+#~ msgstr "Maskiner"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "Inloggningen har upphöjda administratörsprivilegier"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Lösenordet är inte användbart för priviligerade uppgifter eller för att "
+#~ "ansluta till andra maskiner"
+
+#~ msgid "Privileged"
+#~ msgstr "Priviligierad"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Återanvänd mitt lösenord för priviligierade uppgifter och för att ansluta "
+#~ "till andra maskiner"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "Användaren <b>$0</b> har inte rättighet att hantera lagring"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr ""
+#~ "Användaren <b>$0</b> har inte rättighet att ändra nätverksinställningar"
+
+#, fuzzy
+#~| msgid "Required by"
+#~ msgid "Required by $0"
+#~ msgstr "Begärd av"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Senaste trigger"
+
+#~ msgid "Next Run"
+#~ msgstr "Nästa körning"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "Användaren <b>$0</b> har inte rättighet att skapa timrar"
+
+#~ msgid "Connecting"
+#~ msgstr "Ansluter"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Om Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr "(delat med OS:et)"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Lägg till ytterligare lagring"
+
+#~ msgid "Add Storage"
+#~ msgstr "Lägg till lagring"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Ytterligare lagring"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Alla data på valda diskar kommer raderas och diskarna kommer läggas till "
+#~ "i lagringspoolen."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Konfigurera lagring …"
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Kunde inte lägga till alla diskar"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Radera behållare och återställ lagringspoolen"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr "Information om Dockers lagringspool är inte tillgängligt."
+
+#~ msgid "Local Disks"
+#~ msgstr "Lokala diskar"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "Ingen ytterligare lokal lagring hittad."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Omformatera och lägg till diskar"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "Att återställa lagringspoolen kommer radera alla behållare och släppa "
+#~ "diskar i poolen."
+
+#~ msgid "Security"
+#~ msgstr "Säkerhet"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr "Dockers lagringspool kan inte hanteras på det här system."
+
+#, fuzzy
+#~| msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "Skanningen från $time ($type) hittade inga sårbarheter."
+#~ msgstr[1] "Skanningen från $time ($type) hittade inga sårbarheter."
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "Skanningen från $time ($type) hittade inga sårbarheter."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "Skanningen från $time ($type) lyckades inte."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Virtualiseringstjänsten är tillgänglig"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "Du har inte rättigheter att hantera lagringspoolen för Docker."
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} → $cport"
diff --git a/po/tr.po b/po/tr.po
new file mode 100644
index 0000000..0902818
--- /dev/null
+++ b/po/tr.po
@@ -0,0 +1,12594 @@
+# Akın Ömeroğlu <akinomeroglu@gmail.com>, 2015. #zanata
+# Emin Tufan Çetin <etcetin@gmail.com>, 2016. #zanata
+# cockpit <cockpituous@gmail.com>, 2016. #zanata
+# cockpit <cockpituous@gmail.com>, 2017. #zanata
+# Akın Ömeroğlu <akinomeroglu@gmail.com>, 2019. #zanata
+# Oğuz Ersen <oguzersen@protonmail.com>, 2020.
+# Akin Omeroglu <akinomeroglu@gmail.com>, 2020.
+# Anonymous <noreply@weblate.org>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2024-02-12 16:45+0000\n"
+"Last-Translator: Burak Yavuz <hitowerdigit@hotmail.com>\n"
+"Language-Team: Turkish <https://translate.fedoraproject.org/projects/cockpit/"
+"main/tr/>\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n>1)\n"
+"X-Generator: Weblate 5.3.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "Kullanıcı sayısı"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+msgstr[1] "$0 CPU"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 etkin bölge"
+msgstr[1] "$0 etkin bölge"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 kullanılabilir"
+
+#: pkg/storaged/block/resize.jsx:266
+msgid "$0 can not be made larger"
+msgstr "$0 daha büyütülemez"
+
+#: pkg/storaged/block/resize.jsx:263
+msgid "$0 can not be made smaller"
+msgstr "$0 daha küçültülemez"
+
+#: pkg/storaged/block/resize.jsx:259
+msgid "$0 can not be resized"
+msgstr "$0 yeniden boyutlandırılamaz"
+
+#: pkg/storaged/block/resize.jsx:255
+msgid "$0 can not be resized here"
+msgstr "$0 burada yeniden boyutlandırılamaz"
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 parça boyutu"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 kritik sonuç"
+msgstr[1] "$0 sonuç, kritik olanlar dahil"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 veri + $1 ek yük kullanıldı, toplam $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 gün"
+msgstr[1] "$0 gün"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disk eksik"
+msgstr[1] "$0 disk eksik"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "$0 disk eksik"
+msgstr[1] "$0 disk eksik"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 disk"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 belgeleleri"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 hatası"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0, $1 koduyla çıkış yaptı"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "$0 başarısız oldu"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 başarısız oturum açma girişimi"
+msgstr[1] "$0 başarısız oturum açma girişimi"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "$0 filesystem"
+msgstr "$0 dosya sistemi"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 boş"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 saat"
+msgstr[1] "$0 saat"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 önemli sonuç"
+msgstr[1] "$0 sonuç, önemli olanlar dahil"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 varolan bir dosyadır"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 kullanımda"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0, hiçbir depoda yok."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "$0 anahtarı değişti"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0, $1 sinyali ile sonlandırıldı"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 düşük önem derecesinde sonuç"
+msgstr[1] "$0 düşük önem derecesinde sonuç"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 dakika"
+msgstr[1] "$0 dakika"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 orta dereceli sonuç"
+msgstr[1] "$0 sonuç, orta dereceli olanlar dahil"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 ay"
+msgstr[1] "$0 ay"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "$0 daha..."
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 paket"
+msgstr[1] "$0 paket"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 paketin, sistemin yeniden başlatılmasına ihtiyacı var"
+msgstr[1] "$0 paketin, sistemin yeniden başlatılmasına ihtiyacı var"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 sayfa"
+msgstr[1] "$0 sayfa"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+msgid "$0 partitions"
+msgstr "$0 bölümleri"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 güvenlik düzeltmesi mevcut"
+msgstr[1] "$0 güvenlik düzeltmesi mevcut"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 hizmet başarısız oldu"
+msgstr[1] "$0 hizmet başarısız oldu"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 hizmetin yeniden başlatılması gerekiyor"
+msgstr[1] "$0 hizmetin yeniden başlatılması gerekiyor"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 yuva kaldı"
+msgstr[1] "$0 yuva kaldı"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 sıçrama"
+msgstr[1] "$0 sıçrama"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0 eşitlendi"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 toplam"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 güncelleme mevcut"
+msgstr[1] "$0 güncelleme mevcut"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 kullanıldı, toplam $1 ($2 tasarruf edildi)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 hafta"
+msgstr[1] "$0 hafta"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "$0 yüklenecektir."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 yıl"
+msgstr[1] "$0 yıl"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 bölgesi"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: $1 tarihinde çökme"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name ($host cihazından)"
+
+#: pkg/storaged/dialog.jsx:1218
+msgid "(Not part of target)"
+msgstr "(Hedefin bir parçası değil)"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+msgid "(no assigned mount point)"
+msgstr "(atanmış bağlama noktası yok)"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+msgid "(not mounted)"
+msgstr "(bağlanmadı)"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(önerilir)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", $1 güvenlik düzeltmesi dahil"
+msgstr[1] ", $1 güvenlik düzeltmesi dahil"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 gün"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 saat"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 dak"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 dakika"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 hafta"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14."
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 dak"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18."
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 dakika"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24."
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29."
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30."
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 dakika"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4."
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 dak"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 dakika"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5."
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 saat"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 dakika"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7."
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8."
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9."
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "$0 üzerinde uyumlu bir Cockpit sürümü yüklü değil."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "Bu havuzda bu adda bir dosya sistemi zaten var."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "Bu ada sahip bir grup zaten var"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Güvenlik, güvenilirlik ve performans için modern bir tarayıcı gereklidir."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Bir ağ birleştirmesi (bond), birden çok ağ arayüzünü daha yüksek aktarım "
+"hızı veya yedeklilik ile tek bir mantıksal arayüzde birleştirir."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"$2 üzerinde $1 için $0 konumunda yeni bir SSH anahtarı oluşturulacak ve $5 "
+"üzerindeki $4 kullanıcısının $3 dosyasına eklenecektir."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Güncellemelerin etkili olması için bir paket, sistemin yeniden "
+"başlatılmasını gerektiriyor:"
+msgstr[1] ""
+"Güncellemelerin etkili olması için bazı paketler, sistemin yeniden "
+"başlatılmasını gerektiriyor:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Bu adda bir havuz zaten var."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] ""
+"Güncellemelerin etkili olması için bir hizmetin yeniden başlatılması "
+"gerekiyor:"
+msgstr[1] ""
+"Güncellemelerin etkili olması için bazı hizmetlerin yeniden başlatılması "
+"gerekiyor:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "Eksik fiziksel birimlere sahip bir birim grubu yeniden adlandırılamaz."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP izleme"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Web Konsolu Hakkında"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Yok"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Anahtarı kabul et ve oturum aç"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "Kabul edilebilir parola"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Hesap süresinin dolması"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Hesap kullanılabilir değil ya da düzenlenemez."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Hesaplar"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Eylem"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Eylemler"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Etkinleştir"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr "Yeniden boyutlandırmadan önce etkinleştir"
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "$target etkinleştiriliyor"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Etkin"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Etkin yedekleme"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Etkin sayfalar"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Etkin olma başlangıcı "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Etkin durum"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Uyarlanabilir yük dengeleme"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Uyarlanabilir iletim yükü dengeleme"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Ekle"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "$0 ekle"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "DNS sunucusu ekle"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Ağa Bağlı Disk Şifrelemesi ekle"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Tang anahtar sunucusu ekle"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "VLAN ekle"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "VPN ekle"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "WireGuard VPN ekle"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "Bit eşlem ekle"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Yeni bir bölge ekle"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "Adres ekle"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Blok aygıtlarını ekle"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Birleştirme (Bond) ekle"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Köprü ekle"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Disk ekle"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Diskleri ekle"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "iSCSI portalı ekle"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Anahtar ekle"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "Anahtar sunucusu ekle"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Üye ekle"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Yeni anamakine ekle"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Yeni bölge ekle"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "Parola ekle"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "Kişi ekle"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+msgid "Add physical volume"
+msgstr "Fiziksel birim ekle"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Bağlantı noktaları ekle"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "$0 bölgesine bağlantı noktaları ekle"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Ortak anahtar ekle"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "Yönlendirme ekle"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "Arama etki alanı ekle"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Hizmetleri ekle"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "$0 bölgesine hizmetleri ekle"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "$0 bölgesine hizmetleri ekle"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Takım ekle"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Bölge ekle"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "Şifreleme seçeneklerine \"$0\" ekleniyor"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Dosya sistemi seçeneklerine \"$0\" ekleniyor"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 eklemek sunucuyla bağlantıyı kesecek ve yönetim kullanıcı arayüzünü "
+"kullanılamaz hale getirecektir."
+
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Bir anahtar sunucusu eklemek, havuzun kilidinin açılmasını gerektirir. "
+"Lütfen varolan havuz parolasını girin."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Özel bağlantı noktalarının eklenmesi firewalld hizmetini yeniden yükleyecek. "
+"Yeniden yükleme, herhangi bir sadece çalışma zamanı yapılandırmasının "
+"kaybolmasına neden olacak!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Anahtar ekleniyor"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "$target aygıtına fiziksel birim ekleniyor"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Çekirdek komut satırına rd.neednet=1 ekleniyor"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Ek DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Ek DNS arama etki alanları $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Ek eylemler"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Ek adres $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Ek paketler:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Ek bağlantı noktaları"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Adres"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Adres $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Adres boş olamaz"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Adres geçerli bir URL değil"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Adresler"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "Adresler doğru olarak biçimlendirilmemiş"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Cockpit Web Konsolu ile Yönetim"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Yönetimsel erişim"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr ""
+"Raporları oluşturmak ve raporlara erişmek için yönetimsel erişim gerekir."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Yönetimsel erişim gerekli"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Gelişmiş TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Sonra"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Etki alanından ayrıldıktan sonra, sadece yerel kimlik bilgilerine sahip "
+"kullanıcılar bu makinede oturum açabilecektir. DNS çözümleme ayarları ve "
+"güvenilen CA'ların listesi değişebileceğinden, bu durum diğer hizmetleri de "
+"etkileyebilir."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Sistem önyüklemesinden sonra"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Uyarı"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Uyarı ve üstü"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Kod adı"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Tümü"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr "Seçilen düzen için seçilen tüm $0 fiziksel birim gereklidir."
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Tüm güncellemeler"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Hepsi-bir-arada"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Çalıştırmaya izin ver (maskelemeyi kaldır)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "İzin verilen IP'ler"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "İzin verilen adresler"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "Bir ek $0 seçilmek zorundadır"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Ansible rolleri belgeleri"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Günlük iletilerindeki herhangi bir metin dizgisi süzülebilir. Dizgi ayrıca "
+"düzenli ifade biçiminde de olabilir. Ayrıca ileti günlük alanlarına göre "
+"süzmeyi de destekler. Bunlar, değerin olası değerlerin virgülle ayrılmış "
+"listesi olabilen, FIELD=VALUE biçimindeki boşlukla ayrılmış değerlerdir."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Görünüm"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "Uygulama bilgileri eksik"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Uygulamalar"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Uygulamalar listesi"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Uygula ve yeniden başlat"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Çekirdek canlı yamalarını uygula"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Bu çözümü uygula"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Yeni ilke uygulanıyor... Bu işlem birkaç dakika sürebilir."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Çözüm uygulanıyor..."
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Güncellemeler uygulanıyor"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Güncellemeleri uygulama başarısız oldu"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "/var gibi önemli bağlamalar için uygun"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Uygulamalar"
+
+#: pkg/storaged/drive/drive.jsx:102
+msgid "Assessment"
+msgstr "Değerlendirme"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Demirbaş etiketi"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "Önyüklemede"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "En az $0 disk gerekli."
+msgstr[1] "En az $0 disk gerekli."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "En az bir blok aygıtı gerekli."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "En az bir disk gerekli."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr "En az bir üst öğenin yazılabilir bağlanması gerekiyor"
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "Dakikada"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "Saniyede"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "Belirli bir zamanda"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Öznitelikler"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Denetim günlüğü"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Kimlik doğrula"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Kimlik doğrulanıyor"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Kimlik doğrulama"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Kimlik doğrulama başarısız oldu"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Kimlik doğrulama başarısız oldu: Sunucu bağlantıyı kapattı"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Cockpit Web Konsolu ile yetkili görevleri gerçekleştirmek için kimlik "
+"doğrulaması gerekir"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Kimlik doğrulaması gerekli"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "SSH anahtarını yetkilendir"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Yetkilendirilmiş ortak SSH anahtarları"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Otomatik"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Otomatik (Sadece DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Otomatik oturum açma"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Otomatik güncellemeler"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Otomatik olarak başlar"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Otomatik olarak NTP kullanarak"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Otomatik olarak ek NTP sunucularını kullanarak"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Otomatik olarak belirli NTP sunucularını kullanarak"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Otomatikleştirme betiği"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "$0 üzerindeki kullanılabilir hedefler"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Mevcut güncellemeler"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+msgid "BIOS boot partition"
+msgstr "BIOS önyükleme bölümü"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "BIOS tarihi"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "BIOS sürümü"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Hesaplara geri dön"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Hatalı"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Hatalı ayar"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Dengeleyici"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "Önce"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Bağlanır"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Siyah"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blade"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Blade kasası"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "Blok aygıt"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Dosya sistemleri için blok aygıtı"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Blok aygıtlar"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Engellendi"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Birleştirme (Bond)"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Önyükleme"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"Dosya sistemi bağlanmazsa önyükleme başarısız olur ve uzaktan erişim önlenir"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "Dosya sistemi bağlanmadığında önyükleme hala başarılı olur"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Bağlayan"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Köprü"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Köprü bağlantı noktası"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Köprü bağlantı noktası ayarları"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Yayınlama"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Bozuk yapılandırma"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr "Btrfs birimi bağlandı"
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Hata düzeltme güncellemeleri mevcut"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Hatalar"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Veri yolu genişletme kasası"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"$0 SSH anahtarının parolasını $2 üzerindeki $1 kullanıcısının oturum açma "
+"parolasıyla değiştirdiğinizde, anahtar otomatik olarak kullanılabilir hale "
+"getirilecek ve gelecekte parola olmadan $3 üzerinde oturum açabileceksiniz."
+
+#: pkg/static/login.html:61
+msgid "Bypass browser check"
+msgstr "Tarayıcı denetimini atla"
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "CPU güvenliği"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "CPU güvenlik geçişleri"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "CPU kullanımı"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "CPU kullanımı/yükü"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Önbellek"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "Bir anamakine adı, IP adresi, kod adı veya ssh:// URI olabilir"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr ""
+"Şu anki süzgeçlerin birleşimi kullanılarak herhangi bir günlük bulunamıyor."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "İptal"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Kapatmayı iptal et"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Yeniden başlatmayı iptal et"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Etkinleştirilemez"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Bilinmeyen bir anamakineye bağlanılamıyor"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Oturum açma kimlik bilgileri yönlendirilemiyor"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr "Bu sistemde realmd mevcut olmadığından bir etki alanına katılamıyor"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Geçmişteki olay zamanlanamıyor"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Kapasite"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Taşıyıcı"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Değiştir"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "Şifreleme ilkesini değiştir"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Anamakine adını değiştir"
+
+#: pkg/storaged/overview/overview.jsx:152
+msgid "Change iSCSI initiater name"
+msgstr "iSCSI başlatıcı adını değiştir"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "iSCSI başlatıcı adını değiştir"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+msgid "Change label"
+msgstr "Etiketi değiştir"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Parolayı değiştir"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Parolayı değiştir"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Performans profilini değiştir"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Profili değiştir"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "Kabuğu değiştir"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Sistem saatini değiştir"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "$0 parolasını değiştir"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Ayarları değiştir"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Değiştirilen anahtarlar genellikle bir işletim sisteminin yeniden "
+"yüklenmesinin sonucudur. Ancak, beklenmeyen bir değişiklik, üçüncü tarafın "
+"bağlantınıza müdahale etme girişimini gösterebilir."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr "Bölüm türlerinin değiştirilmesi sistemin önyüklemesini engelleyebilir."
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Ayarların değiştirilmesi sunucuyla bağlantıyı kesecek ve yönetim kullanıcı "
+"arayüzünü kullanılamaz hale getirecektir."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Güncellemeleri denetle"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"Komuttan SHA-256 veya SHA-1 adreslemesinin bu ileti öğesiyle eşleştiğini "
+"denetleyin."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "Anahtar adreslemesini Tang sunucusuyla denetleyin."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "$target denetleniyor"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "IP denetleniyor"
+
+#: pkg/storaged/jobs-panel.jsx:68
+msgid "Checking MDRAID device $target"
+msgstr "MDRAID aygıtı $target denetleniyor"
+
+#: pkg/storaged/jobs-panel.jsx:69
+msgid "Checking and repairing MDRAID device $target"
+msgstr "MDRAID aygıtı $target denetleniyor ve onarılıyor"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "$0 paketi denetleniyor"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "initrd'de NBDE desteği denetleniyor"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Yeni uygulamalar denetleniyor"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Paket güncellemeleri denetleniyor..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Yüklü yazılımlar denetleniyor"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "İlgili işlemler denetleniyor"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Yazılım durumu denetleniyor"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Uygulamada kullanılacak dili seçin"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Öbek boyutu"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Sınıf"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "$target temizleniyor"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "'Başlatılamadı' hatasını temizle"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Tüm süzgeçleri temizle"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Aramayı temizle"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Metin temizleme aygıtı"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "İstemci yazılımı"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Kapat"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Seçilen sayfaları kapat"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Cockpit kimlik doğrulaması yanlış yapılandırılmış."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "NetworkManager ve Firewalld'un Cockpit yapılandırması"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit, verilen anamakineyle iletişim kuramadı."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit'te beklenmeyen bir iç hata oldu."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit, Linux sunucularınızı bir web tarayıcısı aracılığıyla yönetmenizi "
+"kolaylaştıran bir sunucu yöneticisidir. Terminal ve web aracı arasında geçiş "
+"yapmak sorun değildir. Cockpit aracılığıyla başlatılan bir hizmet terminal "
+"aracılığıyla durdurulabilir. Aynı şekilde, terminalde bir hata meydana "
+"gelirse, Cockpit günlüğü arayüzünde görülebilir."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit, etkileşimli bir Linux sunucu yönetim arayüzüdür."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit, sistemdeki yazılımla uyumlu değil."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit yüklü değil"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit sistemde yüklü değil."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit yeni sistem yöneticileri için mükemmeldir; depolama yönetimi, "
+"günlükleri inceleme, hizmetleri başlatma ve durdurma gibi basit görevleri "
+"kolayca gerçekleştirmelerine olanak tanır. Aynı anda birkaç sunucuyu "
+"izleyebilir ve yönetebilirsiniz. Bunları tek bir tıklama ile ekleyin ve "
+"makineleriniz arkadaşlarıyla ilgilensin."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit tarayıcınızda düzgün görüntülenmeyebilir"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Tanılama ve destek verilerini topla ve paketle"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Çekirdek çökme dökümlerini topla"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Ölçümleri topla"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Renk"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr ""
+"Virgülle ayrılmış bağlantı noktaları, aralıklar ve hizmetler kabul edilir"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Komut"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Komut bulunamadı"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Yorum"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "tuned ile iletişim başarısız oldu"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Compact PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Tüm sistem ve aygıtlarla uyumlu (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Modern sistem ve 2TB'tan büyük sabit disklerle uyumlu (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Yer kazanmak için çökme dökümlerini sıkıştır"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Sıkıştırma"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "$0=$1 koşulu karşılanmadı"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Koşul başarısız oldu"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Yapılandırma"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Yapılandırılıyor"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "IP yapılandırma"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "kdump yapılandırma"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Sistem ayarlarını yapılandırma"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Onayla"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "$0 aygıtının silinmesini onayla"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Anahtar parolasını onayla"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Yeni anahtar parolasını onayla"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Yeni parolayı onayla"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Parolayı onayla"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "$0 kaldırılmasını onayla"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Kaldırmayı alternatif bir parola ile onayla"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "$0 aygıtının durdurulmasını onaylayın"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "Çakışan"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Çakışanlar"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "Otomatik olarak bağlan"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "Şuna bağlan"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "Şuna bağlan:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "SETroubleshoot arka plan programına bağlanılıyor..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Dbus'a bağlanma başarısız oldu: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Makineye bağlanılıyor"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Bağlantı hatası"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Bağlantı başarısız oldu"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Bağlantı zaman aşımına uğradı."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "Bağlantı kaybolacaktır"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "İçerir"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "İletişim kurulan etki alanı"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "İçerdikleri:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Devam"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Oturuma devam et"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Denetim"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Ctrl"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Denetleyici"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Dönüştürülebilir"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Kopyalandı"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Kopyala"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Panoya kopyala"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Çekirdek $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "$0 ile iletişim kurulamadı"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Çökme dökümü konumu"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Çökme bildirimi"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Sistemi çöktür"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Oluştur"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "LVM2 birim grubu oluştur"
+
+#: pkg/storaged/overview/overview.jsx:145
+msgid "Create MDRAID device"
+msgstr "MDRAID aygıtı oluştur"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "RAID aygıtı oluştur"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Stratis havuzu oluştur"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Yeni bir SSH anahtarı oluştur ve yetkilendir"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "$0 dosya sistemi için anlık görüntü oluştur"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Benzersiz olmayan UID ile hesap oluştur"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Zayıf parola ile hesap oluştur"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Ana dizin sahipliğini oluştur ve değiştir"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Oluştur ve bağla"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Create and start"
+msgstr "Oluştur ve başlat"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Dosya sistemi oluştur"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Oluştur"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Mantıksal birim oluştur"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Yeni hesap oluştur"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Yeni dosya sistemi oluştur"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Yeni grup oluştur"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Yeni mantıksal birim oluştur"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Bu içerikle yeni görev dosyası oluştur."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+msgid "Create new thinly provisioned logical volume"
+msgstr "Yeni ölçülü kaynak sağlanan mantıksal birim oluştur"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "Sadece oluştur"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Bölüm oluştur"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "$0 üzerinde bölüm oluştur"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Bölüm tablosu oluştur"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Anlık görüntü oluştur"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Anlık görüntü oluştur ve bağla"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Sadece anlık görüntü oluştur"
+
+#: pkg/storaged/overview/overview.jsx:174
+msgid "Create storage device"
+msgstr "Depolama aygıtı oluştur"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+msgid "Create subvolume"
+msgstr "Alt birim oluştur"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "İnce birim oluştur"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Zamanlayıcı oluştur"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Birim grubu oluştur"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Oluşturuldu"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "$target LVM2 birim grubu oluşturuluyor"
+
+#: pkg/storaged/jobs-panel.jsx:67
+msgid "Creating MDRAID device $target"
+msgstr "MDRAID aygıtı $target oluşturuluyor"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "VDO aygıtı oluşturuluyor"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "$target üzerinde dosya sistemi oluşturuluyor"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "$target mantıksal birimi oluşturuluyor"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "$target bölümü oluşturuluyor"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "$target için anlık görüntü oluşturuluyor"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Bu $0 oluşturmak sunucuyla bağlantıyı kesecek ve yönetim kullanıcı arayüzünü "
+"kullanılamaz hale getirecektir."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Kritik ve üstü"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"Şifreleme İlkeleri TLS, IPSec, SSH, DNSSec ve Kerberos protokollerini "
+"kapsayan temel şifreleme alt sistemlerini yapılandıran bir sistem "
+"bileşenidir."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "Şifreleme ilkesi"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "Şifreleme ilkesi tutarsız"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Şu anki önyükleme"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Şu anki en yüksek CPU kullanımı"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Şu an kullanımda"
+
+#: pkg/kdump/kdump-view.jsx:535
+msgid "Currently not supported"
+msgstr "Şu anda desteklenmiyor"
+
+#: pkg/storaged/partitions/partition.jsx:146
+msgid "Custom"
+msgstr "Özel"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "Özel şifreleme ilkesi"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Özel bağlama seçenekleri"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Özel bağlantı noktaları"
+
+#: pkg/storaged/partitions/partition.jsx:180
+msgid "Custom type"
+msgstr "Özel tür"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Özel bölgeler"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "SHA-1 imza doğrulamasına sahip VARSAYILAN'a izin verildi."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS arama etki alanları"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "DNS arama etki alanları $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Günlük"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Tehlike uyarısı:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Koyu"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Veri"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Kullanılan veri"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"Veriler, hem güvenilirliği hem de performansı artırmak için seçilen fiziksel "
+"birimlerde iki kopya halinde ve ayrıca dönüşümlü olarak depolanacaktır. En "
+"az dört birimin seçilmesi gerekir."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"Veriler, güvenilirliği artırmak için seçilen fiziksel birimlerde iki veya "
+"daha fazla kopya halinde depolanacaktır. En az iki birimin seçilmesi gerekir."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+"Veriler, performansı artırmak için seçilen fiziksel birimlerde dönüşümlü "
+"olarak depolanacaktır. En az iki birim seçilmesi gerekir."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"Veriler, seçilen fiziksel birimlerde depolanacak ve böylece veriler "
+"etkilenmeden bunlardan biri kaybedilebilecektir. En az üç birim seçilmesi "
+"gerekir."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+"Veriler, seçilen fiziksel birimlerde depolanacak ve böylece veriler "
+"etkilenmeden bunlardan biri kaybedilebilecektir. Performansı artırmak için "
+"veriler ayrıca dönüşümlü olarak depolanır. En az üç birim seçilmesi gerekir."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+"Veriler, seçilen fiziksel birimlerde depolanacak, böylece veriler "
+"etkilenmeden aynı anda en fazla iki tanesi kaybedilebilecektir. Performansı "
+"artırmak için veriler ayrıca dönüşümlü olarak depolanır. En az beş birim "
+"seçilmesi gerekir."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+"Veriler, herhangi bir ek yedeklilik veya performans iyileştirmesi olmaksızın "
+"seçilen fiziksel birimlerde depolanacaktır."
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Tarih özellikleri, YYYY-MM-DD hh:mm:ss biçiminde olmalıdır. Alternatif "
+"olarak 'yesterday', 'today', 'tomorrow' dizgileri de anlaşılır. 'now' şu "
+"anki zamanı ifade eder. Son olarak, göreceli zamanlar '-' veya '+' önekleri "
+"ile belirtilebilir"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Devre dışı bırak"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Mantıksal birim $0/$1 devre dışı bırakılsın mı?"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Devre dışı bırakılıyor"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "$target devre dışı bırakılıyor"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Hata ayıklama ve üstü"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Bir azalt"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "Adanmış eşlik (RAID 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Tekilleştirme"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Öntanımlı"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Gecikme"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Gecikme bir sayı olmak zorundadır"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Sil"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "$0 sil"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Hesabı sil"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Dosyaları sil"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Grubu sil"
+
+#: pkg/storaged/stratis/pool.jsx:292
+msgid "Delete pool"
+msgstr "Havuzu sil"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Rapor kalıcı olarak silinsin mi?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 silmek sunucuyla bağlantıyı kesecek ve yönetim kullanıcı arayüzünü "
+"kullanılamaz hale getirecektir."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "$target siliniyor"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "$target LVM2 birim grubu siliniyor"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "Bir Stratis havuzunu silmek içerdiği tüm verileri silecek."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "Bir dosya sistemini silmek, içindeki tüm verileri silecek."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "Mantıksal bir birimi silmek, içindeki tüm verileri silecektir."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "Bir bölümü silmek, içindeki tüm verileri silecektir."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Silme, bir MDRAID aygıtı üzerindeki tüm verileri siler."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "Silme, bir VDO aygıtı üzerindeki tüm verileri siler."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "Silme, bir birim grubu üzerindeki tüm verileri siler."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "Silme, bu alt birimdeki tüm verileri ve alt birimlerinin tümünü siler."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "Silme işlemi aşağıdaki dosyaları kaldıracak:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Açıklama"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Masaüstü"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Ayrılabilir"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Ayrıntılar"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Geliştirme"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Aygıt"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Aygıt dosyası"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Aygıt salt-okunur"
+
+#: pkg/storaged/block/other.jsx:60
+msgid "Device number"
+msgstr "Aygıt numarası"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Tanılama raporları"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Dizin"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "$0 dizini yazılabilir değil ya da mevcut değil."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Eşzamanlı çoklu kullanımı etkisizleştir"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Güvenlik duvarını etkisizleştir"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "tuned'i etkisizleştir"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Etkisizleştirildi"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Etkileşimli parolaya izin verme"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Parola ile kimlik doğrulamaya izin verme"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Çalıştırmaya izin verme (maskele)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Bağlantıyı kes"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Bağlantı kesildi"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Disk okuma/yazma"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "Disk TAMAM"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Disk bozuluyor"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Disk parolası"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Diskler"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Disk kullanımı"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "Yoksay"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "$0 uyarıyı yoksay"
+msgstr[1] "$0 uyarıyı yoksay"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Seçilen uyarıları yoksay"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Görüntüleme dili"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "Adanmış eşlik (RAID 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "Bağlama"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Önyüklemede otomatik olarak bağlama"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Kenetleme istasyonu"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Otomatik olarak başlamaz"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "Önyükleme sırasında bağlamaz"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Etki alanı"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Etki alanı"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Etki alanı adresi"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Etki alanı yöneticisi adı"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Etki alanı yöneticisi parolası"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Etki alanıyla iletişim kurulamadı"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Etki alanı desteklenmiyor"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Tekrarlama"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "Çift dağıtılmış eşlik (RAID 6)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "İndir"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Ücretsiz olarak yeni bir tarayıcı indir"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "İndirildi"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "İndiriliyor"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "$0 indiriliyor"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Sürücü"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Çift sıra"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+msgid "EFI system partition"
+msgstr "EFI sistem bölümü"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Düzenle"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "/etc/motd'yi düzenle"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Tang anahtar sunucusunu düzenle"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "VLAN ayarlarını düzenle"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "WireGuard VPN'i düzenle"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "Birleştirme ayarlarını düzenle"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "Köprü ayarlarını düzenle"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "$0 bölgesinde özel hizmeti düzenle"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Anamakineyi düzenle"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Anamakineleri düzenle"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "motd'yi düzenle"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+msgid "Edit mount point"
+msgstr "Bağlama noktasını düzenle"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Kuralları ve bölgeleri düzenle"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Hizmeti düzenle"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "$0 hizmetini düzenle"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "Takım ayarlarını düzenle"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Kullanıcı düzenle"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Bir anahtarı düzenlemek, boş bir yuva gerektirir"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "$target çıkarılıyor"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "Gömülü PC"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Boş"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Boşalt"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Boş parola"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "$target boşaltılıyor"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Etkinleştir"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Aygıtı etkinleştirin veya etkisizleştirin"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Hizmeti etkinleştir"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Güvenlik duvarını etkinleştir"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Etkinleştirildi"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "$0 etkinleştiriliyor"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Verileri şifrele"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Verileri Tang anahtar sunucusuyla şifrele"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "Verileri bir parolayla şifrele"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Şifrelenmiş"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Şifrelenmiş $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+msgid "Encrypted Stratis pool"
+msgstr "Şifrelenmiş Stratis havuzu"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "$0 aygıtının şifrelenmiş mantıksal birimi"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "$0 aygıtının şifrelenmiş bölümü"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Şifreleme"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Şifreleme seçenekleri"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Şifreleme parolası"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Şifreleme türü"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Bitti"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "Uç nokta"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"\"Sunucu\" gibi davranan uç noktanın anamakine:b.noktası olarak belirtilmesi "
+"gerekir, aksi takdirde boş bırakılabilir."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Zorunlu (enforcing)"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "İyileştirme güncellemeleri mevcut"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Geçerli bir MAC adresi girin"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Tüm alt ağ"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "$0 tarihindeki giriş"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "$target siliniyor"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Düzeltme"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Hata"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Hata ve üstü"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Hata meydana geldi"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "$0 yüklenirken hata oldu: PackageKit yüklü değil"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Hata iletisi"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr ""
+"Sistem değişikliklerini keşfetmek için semanage çalıştırılırken hata oldu"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Yetkili anahtarlar kaydedilirken hata oldu: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "SELinux kipi ayarlanırken hata oldu: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "Ethernet MAC adresi"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "Ethernet MTU değeri"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "Tam olarak $0 fiziksel birim seçilmek zorundadır"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+"Mantıksal birimin her şeritlemesi için bir tane olmak üzere tam olarak $0 "
+"fiziksel birimin seçilmesi gerekir."
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Örnek: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Örnek: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Mükemmel parola"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Genişletme kasası"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Hesap süresi dolma zamanı"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "$0 tarihinde hesap süresi doluyor"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Dışa Aktar"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Ağa aktar"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Genişletilmiş bilgiler"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Genişletilmiş bölüm"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS düzgün şekilde etkinleştirilmedi"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "Daha fazla Ortak Ölçüt kısıtlamasına sahip FIPS."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Başarısız oldu"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Makine ekleme başarısız oldu: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Bağlantı noktası ekleme başarısız oldu"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Hizmet ekleme başarısız oldu"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Bölge ekleme başarısız oldu"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Parolayı değiştirme başarısız oldu"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "PCP yapılandırma başarısız oldu"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Şu uyarıyı silme başarısız oldu: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "tuned etkisizleştirme başarısız oldu"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "tuned profilini etkisizleştirmesi başarısız oldu"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Şu makineyi düzenleme başarısız oldu: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Hizmeti düzenleme başarısız oldu"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "firewalld içinde $0 etkinleştirme başarısız oldu"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "tuned etkinleştirme başarısız oldu"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Günlükleri getirme başarısız oldu"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Yetkilendirilmiş anahtarları yükleme başarısız oldu."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Birim ekleme başarısız oldu"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"dnf-automatic.timer veya dnf-automatic-install.timer için birim dosyalarını "
+"ayrıştırma başarısız oldu. Lütfen otomatik güncellemeleri yapılandırmak için "
+"özel geçersiz kılmaları kaldırın."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Hizmeti yeniden başlatma başarısız oldu"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "/etc/motd içinde değişiklikleri kaydetme başarısız oldu"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Ayarları kaydetme başarısız oldu"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Başlatılamadı"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Profil değiştirme başarısız oldu"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Dosya durumu"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Dosya sistemi"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "Dosya sistemi kilitli"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Dosya sistemi adı"
+
+#: pkg/storaged/dialog.jsx:1114
+msgid "Filesystem outside the target"
+msgstr "Hedefin dışındaki dosya sistemi"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "Dosya sistemi zaten bu bağlama noktasının altına bağlanmış."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Ada veya açıklamaya göre süz"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Menü öğelerini süz"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Hizmetleri süz"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Süzgeçler"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Parmak izi"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Güvenlik duvarı"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Güvenlik duvarı kullanılabilir değil"
+
+#: pkg/storaged/drive/drive.jsx:122
+msgid "Firmware version"
+msgstr "Donanım yazılımı sürümü"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "NBDE desteğini düzelt"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Yazı tipi boyutu"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Çalıştırılması yasak"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Değiştirmeye zorla"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Silmeye Zorla"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Parola değiştirmeye zorla"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Biçimlendir"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "$0 biçimlendir"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Biçimlendir ve bağla"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Format and start"
+msgstr "Biçimlendir ve başlat"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Sadece biçimlendir"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "Biçimlendirme, depolama aygıtı üzerindeki tüm verileri siler."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Yönlendirme gecikmesi $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Çerçeve numarası"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Boş alan"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Serbest biçimli arama"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "Cuma günleri"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "Nereden"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Ad soyad"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Ağ geçidi"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Genel"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Oluşturuldu"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "$0 hizmetine git"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Uygulamaya git"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Şimdiye git"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Grafik görünürlüğü"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Grafik görünürlüğü seçenekleri menüsü"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Grup"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Grup adı"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Gruplar"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Büyüt"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "İçeriği büyüt"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "$0'ın mantıksal boyutunu büyüt"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Mantıksal birimi büyüt"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "Bölümü büyüt"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "Tüm alanı kaplayacak şekilde havuzu büyüt"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Tüm alanı kaplayacak şekilde büyüt"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Hair pin kipi"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Hairpin kipi"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Elde taşınan"
+
+#: pkg/storaged/drive/drive.jsx:64
+msgid "Hard Disk Drive"
+msgstr "Sabit Disk Sürücüsü"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Donanım bilgileri"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Sağlık"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Merhaba süresi $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Yardım"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "Onay parolasını gizle"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "Parolayı gizle"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "Hiyerarşi Kimliği"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "Artan saldırı yüzeyi pahasına daha yüksek birlikte çalışabilirlik."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Geçmiş paket sayısı"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Ana dizin"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Anamakine"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Anamakine anahtarı yanlış"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Anamakine adı"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Anamakineler"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Saatlik"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Saat"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "Nasıl denetlenir"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "Kimlik"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "Kimlik $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+"DAHİLİ HATA - Bu mantıksal birim etkin olarak işaretlendi ve "
+"ilişkilendirilmiş bir blok aygıta sahip olmalıdır. Ancak böyle bir blok "
+"aygıt bulunamadı."
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP adresi"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"Yönlendirme ön ekiyle birlikte IP adresi. Birden çok değeri virgülle ayırın. "
+"Örnek: 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "IPv4 adresleri"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4 ayarları"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6 ayarları"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Tanımlayıcı"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Eğer boş bırakılırsa, ilişkilendirilmiş bağlantı noktası hizmetlerine ve "
+"bağlantı noktası numaralarına dayanarak kimlik oluşturulacaktır"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Eğer parmak izi eşleşirse, \"Anahtarı kabul et ve oturum aç\"a tıklayın. "
+"Aksi takdirde, oturum açmayın ve yöneticinize başvurun."
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Eğer parmak izi eşleşirse, 'Anamakineye güven ve ekle'ye tıklayın. Aksi "
+"takdirde, bağlanmayın ve yöneticinize başvurun."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Yoksay"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "Gelen"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "Bir terminalde şunu çalıştırın: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Gelecekte $0 üzerinde parola olmadan $1 olarak oturum açmaya izin vermek "
+"için anahtar parolası olarak $3 üzerinde $2 kullanıcısının oturum açma "
+"parolasını kullanın veya anahtar parolasını boş bırakın."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "Eşitleme durumunda"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Etkin değil"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+msgid "Inactive logical volume"
+msgstr "Etkin olmayan mantıksal birim"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Dahil olan hizmetler"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr ""
+"Gelen istekler varsayılan olarak engellenir. Giden istekler engellenmez."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Tutarsız dosya sistemi bağlama"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Bir artır"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "İndeks belleği"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Dolaylı"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Bilgi"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Bilgi ve üstü"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Başlat"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "$0 diskini başlat"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "Başlatma, disk üzerindeki tüm verileri siler."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Başlatılıyor..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Insights: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Yükle"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "NFS desteğini yükle"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Stratis desteğini yükle"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Tüm güncellemeleri yükle"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "Uygulama bilgilerini yükle"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "cockpit-pcp'yi yükle"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "kpatch güncellemelerini yükle"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "realmd desteğini yükle"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Güvenlik güncellemelerini yükle"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr ""
+"SELinux olayları hakkındaki sorunları gidermek için setroubleshoot-server "
+"yükleyin."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Yazılım yükle"
+
+#: pkg/kdump/kdump-view.jsx:571
+msgid "Install the $0 package."
+msgstr "$0 paketini yükleyin."
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr "Yüklü cockpit paketi olmadan kurulum desteklenmez"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Yüklendi"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Yükleniyor"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "$0 yükleniyor"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "$0 paketini yüklemek $1 paketini kaldırır."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Paketler yükleniyor"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Arayüz"
+msgstr[1] "Arayüzler"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Arayüz üyeleri"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Arayüzler"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Dahili hata"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "İç hata: Geçersiz sınama üstbilgisi"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Geçersiz"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Geçersiz adres $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Geçersiz tarih biçimi"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Geçersiz tarih ve saat biçimi"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Geçersiz süre dolma tarihi"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Geçersiz dosya izinleri"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Geçersiz anahtar"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Geçersiz ölçüm $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Geçersiz gün sayısı"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Geçersiz bağlantı noktası numarası"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Geçersiz ön ek $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Geçersiz ön ek veya ağ maskesi $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Geçersiz aralık"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Geçersiz saat biçimi"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Geçersiz saat dilimi"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Geçersiz kullanıcı adı veya parola"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "IoT ağ geçidi"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "sshd farklı bir bağlantı noktasında mı çalışıyor?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "İşler"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "Katıl"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Bir etki alanına katıl"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Etki alanına katıl"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Katılıyor"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Bir etki alanına katılmak, realmd kurulumu gerektirir"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Bu etki alanına katılmak desteklenmiyor"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Katıldığı ait olduğu ad alanı"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Günlük"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Günlük girişi"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Günlük girişi bulunamadı"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Şuna atla"
+
+#: pkg/kdump/kdump-view.jsx:569
+msgid "Kdump service is not installed."
+msgstr "Kdump hizmeti yüklü değil."
+
+#: pkg/kdump/kdump-view.jsx:604
+msgid "Kdump settings"
+msgstr "Kdump ayarları"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Bağlantıyı koru"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Çekirdek çökme dökümü"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr "Çekirdek $0 ayarıyla önyükleme yapmadı"
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Çekirdek dökümü"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "Çekirdek canlı yaması $0 etkin"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Çekirdek canlı yaması $0 yüklendi"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Çekirdek canlı yaması ayarları"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Çekirdek canlı yamalama"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Anahtar parolası"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Bilinmeyen türlere sahip anahtar yuvaları burada düzenlenemez"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Anahtar kaynağı"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Anahtarlar"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Anahtar sunucusu"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Anahtar sunucusu adresi"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "Anahtar sunucusu kaldırma, $0 kilidini açmayı engelleyebilir."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP anahtarı"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "Active Directory birlikte çalışabilirliğine sahip ESKİ."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+msgid "LVM2 VDO pool"
+msgstr "LVM2 VDO havuzu"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+msgid "LVM2 logical volume"
+msgstr "LVM2 mantıksal birimi"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+msgid "LVM2 logical volumes"
+msgstr "LVM2 mantıksal birimleri"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "LVM2 fiziksel birimi"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+msgid "LVM2 physical volumes"
+msgstr "LVM2 fiziksel birimleri"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM2 birim grubu"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "$0 LVM2 birim grubu"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr "Etiket"
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Dizüstü"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Son 24 saat"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Son 7 gün"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Son etkin olma"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+msgid "Last cannot be removed"
+msgstr "Sonuncusu kaldırılamaz"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Son denetleme: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+msgid "Last disk can not be removed"
+msgstr "Son disk kaldırılamaz"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Son oturum açma"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Son değiştirilme: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Son başarılı oturum açma:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "Düzen"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Daha fazla bilgi edinin"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "$0'dan ayrıl"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Etki alanından ayrıl"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Şifrelemeyi atlamak için boş bırakın"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "GNU LGPL sürüm 2.1 altında lisanslıdır"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Açık"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Erişimi sınırla"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Sınırlı erişim"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"Sınırlı erişim kipi, yönetimsel yetkileri kısıtlar. Web konsolunun bazı "
+"kısımları sınırlı işlevselliğe sahip olacaktır."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Sınırlar"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "Doğrusal"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Bağlantı kapanma gecikmesi"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Yerel bağlantı"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Bağlantı izleme"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Bağlantı açılma gecikmesi"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Bağlantı izleme"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Bağlantılı"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+msgid "Linux filesystem data"
+msgstr "Linux dosya sistemi verileri"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+msgid "Linux swap space"
+msgstr "Linux takas alanı"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Dinle"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "Dinlenen bağlantı noktası"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "Dinlenen bağlantı noktası bir sayı olmak zorundadır"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Birimleri listeleme"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Birimleri listeleme başarısız oldu: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Yük"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Yük dengeleme"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Daha önceki verileri yükle"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Daha önceki girişleri yükle"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Mevcut güncellemeleri yükleme başarısız oldu"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Mevcut güncellemeler yükleniyor, lütfen bekleyin..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Daha önceki girişler yükleniyor"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Anahtarlar yükleniyor..."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "SSH anahtarlarını yükleme başarısız oldu"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Birimlerin yüklenmesi başarısız oldu"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Paketler yükleniyor..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Sistem değişiklikleri yükleniyor..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Birimin yüklenmesi başarısız oldu"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Yükleniyor..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Yerel hesaplar"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Yerel dosya sistemi"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Yerel bağlama noktası"
+
+#: pkg/storaged/overview/overview.jsx:160
+msgid "Local storage"
+msgstr "Yerel depolama"
+
+#: pkg/kdump/kdump-view.jsx:450
+msgid "Local, $0"
+msgstr "Yerel, $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Konum"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Kilitle"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "$0 kilitle"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Hesabı kilitle"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+msgid "Locked data"
+msgstr "Kilitli veriler"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "$target kilitleniyor"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Oturum aç"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "$0 üzerinde oturum aç"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Sunucu kullanıcı hesabınızla oturum açın."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Günlük iletileri"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Oturumu kapat"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Kullanıcı oturumunu kapat"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Oturum açıldı"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Mantıksal"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+msgid "Logical Volume Manager partition"
+msgstr "Mantıksal Birim Yöneticisi bölümü"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Mantıksal boyut"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Mantıksal birim"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Mantıksal birim (anlık görüntü)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "$0 bölümünün mantıksal birimi"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Oturum aç"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Tekrar oturum aç"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Oturum açma başarısız oldu"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Oturum açma biçimi"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Oturum açma geçmişi"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Oturum açma geçmişi listesi"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "$0 oturumunu kapat"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Oturumu kapatma başarılı oldu"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Günlükler"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Düşük profilli masaüstü"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Lunch box"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+msgid "MDRAID device"
+msgstr "MDRAID aygıtı"
+
+#: pkg/storaged/utils.js:334
+msgid "MDRAID device $0"
+msgstr "MDRAID aygıtı $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+msgid "MDRAID device is recovering"
+msgstr "MDRAID aygıtı kurtarılıyor"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+msgid "MDRAID device must be running"
+msgstr "MDRAID aygıtı çalışıyor olmak zorundadır"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+msgid "MDRAID disk"
+msgstr "MDRAID diski"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+msgid "MDRAID disks"
+msgstr "MDRAID diskleri"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (önerilir)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU pozitif bir sayı olmak zorundadır"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Makine kimliği"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Makine SSH anahtarı parmak izleri"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Ana sunucu kasası"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Bakım"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+"Uzak bir makinedeki kötü amaçlı sayfalar bağlı diğer anamakineleri "
+"etkileyebilir"
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "Dosya sistemi boyutlarını yönet"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Depolamayı yönet"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Yönetilen arayüzler"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "LVM'leri yönetme"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "NFS bağlamalarını yönetme"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "RAID'leri yönetme"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "VDO'ları yönetme"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "VLAN'ları yönetme"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Güvenlik duvarını yönetme"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Ağ birleştirmelerini yönetme"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Ağ köprülerini yönetme"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Ağ takımlarını yönetme"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Bölümleri yönetme"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Fiziksel sürücüleri yönetme"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Hizmetleri yönetme"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Yazılım güncellemelerini yönetme"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Kullanıcı hesaplarını yönetme"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Elle"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "El ile"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "$target hatalı olarak işaretleniyor"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Hizmeti maskele"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Maskelendi"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Hizmeti maskelemek, tüm bağımlı birimlerin çalışmasını engeller. Bunun "
+"beklenenden daha büyük etkisi olabilir. Lütfen bu birimi maskelemek "
+"istediğinizi onaylayın."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "En fazla ileti yaşı $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+msgid "Media drive"
+msgstr "Ortam sürücüsü"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Bellek"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Bellek teknolojisi"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Bellek kullanımı"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Bellek kullanımı/takası"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Birleştirildi"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Oturum açmış kullanıcılar için ileti"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "Hatayla ilgili iletiler günlükte bulunabilir:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Kullanılan üst veri"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Yöntem"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Ölçüm"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Ölçümler ve geçmiş"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Ölçümler geçmişi yüklenemedi"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Ölçümler ayarları"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Mini PC"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Mini tower"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Dakikanın 0 ile 59 arasında bir sayı olması gerekir"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "Dakikada"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Dakika"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr "Yansıtılmış (RAID 1)"
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Risk azaltmaları"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Kip"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Model"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "$target değiştiriliyor"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Pazartesi günleri"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "İzleme aralığı"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "İzleme hedefleri"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Aylık"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Daha fazla bilgi..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Bağla"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "Bağlama Noktası"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "Ağ kullanılabilir hale geldikten sonra bağla, hatayı yoksay"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Önyüklemede de otomatik olarak bağla"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Önyüklemede bağla"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Önyüklemede $0 noktasına otomatik olarak bağla"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Hizmetler başlamadan önce bağla"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Bağlama yapılandırması"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Dosya sistemini bağla"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Şimdi bağla"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Şimdi $0 noktasına bağla"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Bağlama seçenekleri"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Bağlama noktası"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Bağlama noktası boş olamaz"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Bağlama noktası boş olamaz."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Bağlama noktası zaten $0 için kullanılıyor"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Bağlama noktası \"/\" ile başlamak zorundadır."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Salt okunur bağla"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Beklemeden bağla, hatayı yoksay"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "$target bağlanıyor"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Hizmetler başlamadan önce bağlar"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Hizmetlerle paralel olarak bağlar"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+"Hizmetlerle paralel olarak bağlar, ancak ağ kullanılabilir olduktan sonra"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Çok sistemli kasa"
+
+#: pkg/storaged/drive/drive.jsx:135
+msgid "Multipathed devices"
+msgstr "Çok yollu aygıtlar"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr ""
+"Sınırlayıcılar olarak virgüller veya boşluklar kullanılarak birden çok adres "
+"belirtilebilir."
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS bağlama noktası"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP sunucusu"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Ad"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Ad boş olamaz."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Ad boş olamaz."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Ad $0 bayt'tan uzun olamaz"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Ad $0 karakterden uzun olamaz"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Ad 127 karakterden uzun olamaz."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+msgid "Name cannot be longer than 255 characters."
+msgstr "Ad 255 karakterden uzun olamaz."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Ad '$0' karakterini içeremez."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+msgid "Name cannot contain the character '/'."
+msgstr "Ad '/' karakterini içeremez."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "Ad boşluk karakterleri içeremez."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr "Yedek diske ihtiyaç var"
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "En az bir NTP sunucusu gerekli"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Ağ"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Ağ G/Ç"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Ağ birleştirme (bond)"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "Ağ aygıtları ve grafikleri için NetworkManager gerekir"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Ağ günlükleri"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Ağ kullanımı"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager yüklü değil"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager çalışmıyor"
+
+#: pkg/storaged/overview/overview.jsx:168
+msgid "Networked storage"
+msgstr "Ağa bağlı depolama"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Ağ"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Yok"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Hesabın süresi asla dolmasın"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Parolanın süresi asla dolmasın"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Hiç oturum açılmadı"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Yeni NFS bağlama noktası"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Yeni anamakine"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "Yeni anamakine: $0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Yeni anahtar parolası"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Yeni ad"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Yeni parola"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Yeni parola"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Yeni parola kabul edilmedi"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Sonraki"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Hayır"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "Belirtilen kimlik yok"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "SELinux uyarıları yok."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Yüklü veya kullanılabilir uygulamalar yok."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Kullanılabilir yuvalar yok"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Kullanılabilir blok aygıtlar yok."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+msgid "No block devices found"
+msgstr "Bulunan blok aygıtlar yok"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Taşıyıcı yok"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Bulunan yapılandırma yok"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Mevcut veri yok"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "$0 ile $1 arasında mevcut veri yok"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Gecikme yok"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Mevcut açıklama yok"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Sağlanan açıklama yok."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+msgid "No devices found"
+msgstr "Bulunan aygıtlar yok"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Kullanılabilir diskler yok."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+msgid "No disks found"
+msgstr "Bulunan diskler yok"
+
+#: pkg/storaged/iscsi/session.jsx:79
+msgid "No drives found"
+msgstr "Bulunan sürücüler yok"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Şifreleme yok"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Olaylar yok"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Dosya sistemi yok"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Dosya sistemleri yok"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Boş anahtar yuvaları yok"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Boş alan yok"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "Bu bölümden sonra boş alan yok"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Belirtilen grup adı yok"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Bulunan anamakine anahtarları yok."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Bu uygulama için kurulum paketi bulunamadı."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Eklenen anahtarlar yok"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "Eşleşen diller yok"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Günlük girişleri yok"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Mantıksal birimler yok"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Bulunan günlükler yok"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Eşleşen sonuçlar yok"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Takılı ortam yok"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Bölümlendirme yok"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+msgid "No partitions found"
+msgstr "Bulunan bölümler yok"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "Eklenen kişiler yok."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+msgid "No physical volumes found"
+msgstr "Bulunan fiziksel birimler yok"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Belirtilen gerçek ad yok"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Bulunan sonuçlar yok"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Süzme ölçütüyle eşleşen sonuçlar yok. Sonuçları göstermek için tüm "
+"süzgeçleri temizleyin."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Kural sonuçları yok"
+
+#: pkg/storaged/overview/overview.jsx:190
+msgid "No storage found"
+msgstr "Bulunan depolama yok"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+msgid "No subvolumes"
+msgstr "Alt birimler yok"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Böyle bir dosya ya da dizin yok"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Sistem değişiklikleri yok"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Sistem raporları yok."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Güncellemeler yok"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Belirtilen kullanıcı adı yok"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Yok"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Geçerli bir özel anahtar değil"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Güvenlik duvarını etkisizleştirmeye yetkili değil"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Güvenlik duvarını etkinleştirmeye yetkili değil"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Mevcut değil"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Bağlı değil"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Insights'a bağlı değil"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Anamakineye bağlı değil"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+msgid "Not enough free space"
+msgstr "Yeterli boş alan yok"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "Yeterli alan yok"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+msgid "Not enough space to grow"
+msgstr "Büyütmek için yeterli alan yok"
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Bulunamadı"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Yüklenmedi"
+
+#: pkg/networkmanager/network-interface.jsx:680
+msgid "Not permitted to configure network devices"
+msgstr "Ağ aygıtlarını yapılandırmaya izin verilmiyor"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Bölgeleri yapılandırmaya izin verilmiyor"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Bu eylemi gerçekleştirmeye izinli değil."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Hazır değil"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Kayıtlı değil"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Çalışmıyor"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Ayarlanmadı"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Eşitlenmedi"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Not"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Notebook"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Bildirim ve üstü"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Ağ adreslerini, anamakine adlarını ve kullanıcı adlarını gizleyin"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Gizlenmiş"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "$0 meydana geldi"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "$0 ile $1 arasında meydana geldi"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Oluşumlar"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "Tamam"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Eski parola"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Eski parola"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Eski parola kabul edilmedi"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "Bağlı bir aygıt üzerinde"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Başarısızlık durumunda"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Cockpit yüklendikten sonra, \"systemctl enable --now cockpit.socket\" "
+"komutuyla etkinleştirin."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Sadece $0 / $1 kullanılıyor."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Sadece harfler, sayılar, : , _ , . , @ , - karakterlerine izin verilir"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Sadece acil durumlar"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"FIPS modunda önyükleme yaparken yalnızca onaylanmış ve izin verilen "
+"algoritmaları kullan."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Eyvah!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "Ölçümleri paylaşmak için güvenlik duvarındaki pmproxy hizmetini açın."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "$target üzerinde '$operation' işlemi"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Seçenekler"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Veya paketlenmiş bir tarayıcı kullanın"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Diğer"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"Etkileşimli parola kimlik doğrulamasına izin verilmese bile diğer kimlik "
+"doğrulama yöntemleri hala kullanılabilir."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Diğer seçenekler"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Giden"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Genel Bakış"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Üzerine yaz"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Varolan verilerin üzerine sıfırlarla yaz (daha yavaş)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Ölçüm geçmişi için cockpit-pcp paketi eksik"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Paket bilgileri"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit çöktü"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit yüklü değil"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit $0 hata kodunu bildirdi"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Paketler"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Sayfa adı"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Üst öğe"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Üst öğe $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Parçası olduğu"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "$0 parçası"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Bölüm"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "$0'ın bölümü"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "Bölüm boyutu $0. İçerik boyutu $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Bölümlendirme"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Bölümler"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Pasif"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Parola"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "Parola boş olamaz"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Parola boş olamaz"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Başka bir anahtar yuvasından gelen parola"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Parolayı kaldırma, $0 kilidini açmayı engelleyebilir."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Parolalar eşleşmiyor"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Parola"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Parola başarılı olarak değiştirildi"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Parolanın süresinin dolması"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "Parola 256 karakterden uzun"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Parola kabul edilebilir değil"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Parola çok zayıf"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Parola değiştirilmek zorundadır"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Parola kabul edilmedi"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Parola ipucu"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Yapıştır"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Yapıştırma hatası"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "Varolan anahtarı yapıştır"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Ortak SSH anahtar dosyanızın içeriğini buraya yapıştırın"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Yol"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Yol maliyeti"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Yol maliyeti $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Sunucu üzerindeki yol"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Sunucu üzerindeki yol boş olamaz."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Sunucu üzerindeki yol \"/\" ile başlamak zorundadır."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Dizine giden yol"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Dosyanın yolu"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Yollar"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Duraklat"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr ""
+"Kişi #$0 geçersiz uç nokta bağlantı noktasına sahip. Bağlantı noktası bir "
+"sayı olmak zorundadır."
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"Kişi #$0 geçersiz uç noktaya sahip. Anamakine:b.noktası olarak belirtilmek "
+"zorundadır, örn. 1.2.3.4:51820 veya ornek.com:51820"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "Kişiler"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"Bununla bağlantı kuran kişiler diğer makinelerdir. Diğer makinelerin ortak "
+"anahtarları birbirleriyle paylaşılacaktır."
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Performans Yardımcı Pilotu, sisteminizden performans ölçümlerini toplar ve "
+"çözümler."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Performans profili"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Çevresel donanım kasası"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Kalıcı"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "$0 grubu kalıcı olarak silinsin mi?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "$0 aygıtı kalıcı olarak silinsin mi?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Mantıksal birim $0/$1 kalıcı olarak silinsin mi?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+msgid "Permanently delete subvolume $0?"
+msgstr "$0 alt birimi kalıcı olarak silinsin mi?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "İzin reddedildi"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "İzin veren"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Fiziksel"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "Fiziksel Birimler"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Fiziksel birimler"
+
+#: pkg/storaged/block/resize.jsx:279
+msgid "Physical volumes can not be resized here"
+msgstr "Fiziksel birimler burada yeniden boyutlandırılamaz"
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Tarih seçin"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Birimi sabitle"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Ping aralığı"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Ping hedefi"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Sabitlenmiş birim"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "Pizza box"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "Lütfen yönetimsel erişim elde etmek için kimlik doğrulayın"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "Web Konsolunu kullanmak için lütfen JavaScript'i etkinleştirin."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Lütfen $0 paketini yükleyin"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Lütfen sorunu çözün ve bu sayfayı yeniden yükleyin."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Lütfen bir süre dolma tarihi belirtin"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Lütfen bağlanılacak anamakineyi belirtin"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "Lütfen önce bunların bağlantısını kaldırın."
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "İnce mantıksal birimler için havuz"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Ölçülü kaynak sağlanan LVM2 mantıksal birimleri için havuz"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Ölçülü kaynak sağlanan birimler için havuz"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "Havuz parolası"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Bağlantı noktası"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Taşınabilir"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Bağlantı noktaları"
+
+#: pkg/storaged/partitions/partition.jsx:116
+msgid "PowerPC PReP boot partition"
+msgstr "PowerPC PReP önyükleme bölümü"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Ön ek uzunluğu"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Ön ek uzunluğu veya ağ maskesi"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Hazırlanıyor"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Mevcut"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Koru"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Okunaklı anamakine adı"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Önceki önyükleme"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Birincil"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Öncelik"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Öncelik $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "Özel anahtar"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "Yönetici olma sorunu"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Sorun ayrıntıları"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Sorun bilgileri"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Konumu kullanan işlemler"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "İlerleme: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Proje web sitesi"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "passwd aracılığıyla sorma zaman aşımına uğradı"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "ssh-add aracılığıyla sorma zaman aşımına uğradı"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "ssh-keygen aracılığıyla sorma zaman aşımına uğradı"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Yeniden yüklemeyi şuna yayar"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Birlikte çalışabilirlik pahasına beklenen yakın vadeli gelecekteki "
+"saldırılara karşı korur."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "Şu blok aygıtlarda havuz için parolayı sağlayın:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Ortak anahtar"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr "Geçerli bir özel anahtar girildiğinde ortak anahtar oluşturulacaktır"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Amaç"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (şeritleme)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (yansıtma)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (yansıtmaları şeritleme)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (adanmış eşlik)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (dağıtılmış eşlik)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (çift dağıtılmış eşlik)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID kasası"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID seviyesi"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr "RAID10 çift sayıda fiziksel birime ihtiyaç duyar"
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Raf montajlı kasa"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Rastgele"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Aralık"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "Aralık kesin bir şekilde sıralı olmalıdır"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Sıra"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Bir aygıta ham olarak"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Okuma"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Daha fazlasını okuyun..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Salt-okunur"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Okunuyor"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Okunuyor..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Hazır"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Hazırla"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Gerçek anamakine adı"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Gerçek anamakine adı sadece küçük harf, rakam, kısa çizgi ve nokta "
+"karakterlerini (doldurulmuş alt etki alanlarıyla) içerebilir"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Gerçek anamakine adı 64 karakter veya daha kısa olmak zorundadır"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Yeniden uygula ve yeniden başlat"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Yeniden başlat"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Tamamlandıktan sonra yeniden başlat"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Yeniden başlatma önerilir"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Sistemi yeniden başlat..."
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Alınan"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "En son anamakineler"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Geçerli tehdit modelleri için tavsiye edilen, güvenli ayarlar."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Yeniden bağlan"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Kurtarılıyor"
+
+#: pkg/storaged/jobs-panel.jsx:70
+msgid "Recovering MDRAID device $target"
+msgstr "MDRAID aygıtı $target kurtarılıyor"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Paket bilgileri yenileniyor"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Bağlanmayı reddediyor. Anamakine bilinmiyor"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Bağlanmayı reddediyor. Anamakine anahtarı eşleşmiyor"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Bağlanmayı reddediyor. Anamakine anahtarı bilinmiyor"
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "Yeniden oluştur"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "initrd yeniden oluşturuluyor"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Kaydol…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "İlgili işlemler ve hizmetler zorla durdurulacaktır."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "İlgili işlemler zorla durdurulacaktır."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "İlgili hizmetler zorla durdurulacaktır."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Yeniden yükle"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Yeniden yükleme şuradan yayıldı"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Yeniden yükleniyor"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Kalan hizmetlerin durumu yeniden yükleniyor"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "CIFS/SMB üzerinden uzak"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "FTP üzerinden uzak"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "NFS üzerinden uzak"
+
+#: pkg/kdump/kdump-view.jsx:456
+msgid "Remote over NFS, $0"
+msgstr "NFS üzerinden uzak, $0"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "SFTP üzerinden uzak"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "SSH üzerinden uzak"
+
+#: pkg/kdump/kdump-view.jsx:453
+msgid "Remote over SSH, $0"
+msgstr "SSH üzerinden uzak, $0"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Kaldırılanlar:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Kaldır"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "$0'ı kaldır"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "$0 hizmetini $1 bölgesinden kaldır"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "$0 kaldırılsın mı?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Tang anahtar sunucusu kaldırılsın mı?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Aygıtı kaldır"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Anamakineyi kaldır"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Öğeyi kaldır"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "Eksik fiziksel birimler kaldırılsın mı?"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "$0 anahtar yuvası içindeki parola kaldırılsın mı?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "Parola kaldırılsın mı?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "$0 hizmetini kaldır"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "$0 bölgesini kaldır"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Kaldırılıyor"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "$0 kaldırılıyor"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 kaldırmak sunucuyla bağlantıyı kesecek ve yönetim kullanıcı arayüzünü "
+"kullanılamaz hale getirecektir."
+
+#: pkg/storaged/jobs-panel.jsx:66
+msgid "Removing $target from MDRAID device"
+msgstr "MDRAID aygıtından $target kaldırılıyor"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"Başka bir parolayı onaylamadan bir parolayı kaldırmak, diğer parolalar "
+"unutulur veya kaybolursa, kilit açma veya anahtar yönetimini önleyebilir."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "$target aygıtından fiziksel birim kaldırılıyor"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Cockpit hizmetinin kaldırılması web konsolunu erişilemez hale getirebilir. "
+"Bu bölgenin şu anki web konsolu bağlantınıza uygulanmadığından emin olun."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "Bölgenin kaldırılması, içindeki tüm hizmetleri kaldıracak."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Yeniden adlandır"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Stratis havuzunu yeniden adlandır"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Dosya sistemini yeniden adlandır"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Grubu yeniden adlandır"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "$0 grubunu yeniden adlandır"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Mantıksal birimi yeniden adlandır"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Birim grubunu yeniden adlandır"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "$target yeniden adlandırılıyor"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "Bir grubu yeniden adlandırmak sudo ve benzeri kuralları etkileyebilir"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr "Onar"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+msgid "Repair logical volume $0"
+msgstr "$0 mantıksal birimini onar"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "$target onarılıyor"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Tekrarla"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Her ay tekrarla"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Parolayı tekrarla"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Her hafta tekrarla"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Bildir"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Bildirme etiketi"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "ABRT Analytics'e bildir"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Bildirildi; mevcut bağlantılar yok"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Bildirme başarısız oldu"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Bildirme iptal edildi"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Bildirmeler"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Bildirmeler:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Her $0 günde bir parola değişikliği gerektir"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "$0 tarihinde parola değişikliği gerektir"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "İlk oturum açıldığında parola değişikliği gerektir"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Gerektiren"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Gerektiren: "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Gerekliler"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Düzenlemek için yönetim erişimi gerektirir"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Gereklilik"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Gerekliliği"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+"Çekirdek komut satırında '$0' seçeneğini ayarlayarak önyükleme sırasında "
+"belleği ayırın. Örneğin, $3 içindeki $2 için '$1' ekleyin veya dağıtımınızın "
+"çekirdek bağımsız değişken düzenleyicisini kullanın."
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Ayrılan bellek"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Sıfırla"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Parolayı sıfırla"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "$target yeniden boyutlandırılıyor"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Şifrelenmiş bir dosya sistemini yeniden boyutlandırmak, diskin kilidinin "
+"açılmasını gerektirir. Lütfen şu anki bir disk parolasını girin."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Yeniden başlat"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Hizmetleri yeniden başlat"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Hizmetleri yeniden başlat..."
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Yeniden başlatılıyor"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Bağlantı geri yükleniyor"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"Eğer kdump doğru olarak yapılandırılmışsa, çökme sonuçları $0 ile $1 arası "
+"$2 olarak kopyalanacaktır."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+"Eğer kdump doğru olarak yapılandırılmışsa, $0 içindeki çökme sonuçları $1 "
+"olarak saklanacaktır."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Devam"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Varolan şifrelemeyi yeniden kullan"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Varolan şifrelemeyi yeniden kullan ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "Şifreleme ilkesini gözden geçir"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Günlükleri gözden geçirme"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Döngüsel"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Yönlendirmeler"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Çalıştırma saati"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Yeni rapor çalıştır"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Çalıştırma tarihi"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Raporu çalıştır"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+"Bu komutu güvenilir bir ağ üzerinden veya fiziksel olarak uzaktaki makinede "
+"çalıştırın:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Çalıştırıcı"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Çalışıyor"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Çalışma zamanı"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux erişim denetimi hataları"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "SELinux sistemde etkisizleştirildi"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux sistemde etkisizleştirildi."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux ilkesi"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "SELinux sistem durumu bilinmiyor."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "SELinux sorun giderme"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "$target aygıtının SMART kendi kendini denemesi"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"SOS raporlaması, sorunların teşhis edilmesine yardımcı olmak için sistem "
+"bilgilerini toplar."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH anahtarı"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "SSH anahtarı bir yol değildir"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH anahtarları"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "STP yönlendirme gecikmesi"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "STP merhaba süresi"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP en fazla ileti yaşı"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP önceliği"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Safari kullanıcılarının kendi kendine imzalanan CA sertifikasını içe "
+"aktarması ve ona güvenmesi gerekir:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Cumartesi günleri"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Kaydet"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Kaydet ve yeniden başlat"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Değişiklikleri kaydet"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "LZ4 ile ayrı blokları sıkıştırarak alandan tasarruf edin"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Aynı veri bloklarını sadece bir kez depolayarak alandan tasarruf edin"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Yeni bir parola kaydetmek, diskin kilidinin açılmasını gerektirir. Lütfen şu "
+"anki bir disk parolasını girin."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "$0 için kapatma zamanlandı"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "$0 için yeniden başlatma zamanlandı"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "Mühürlü Kasa PC"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Ara"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Etki alanını ara"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Ad veya kimlik ara"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Ad, grup veya kimlik ara"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "Saniyenin 0 ile 59 arasında bir sayı olması gerekir"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Saniye"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Güvenli kabuk anahtarları"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "$target güvenli bir şekilde siliniyor"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Güvenlik Gelişmiş Linux yapılandırması ve sorun giderme"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Güvenlik güncellemeleri mevcut"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Sadece güvenlik güncellemeleri"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Güvenlik güncellemeleri, $0 saat $1'da uygulanacaktır"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Seç"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Bir tanımlayıcı seçin"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Yöntem seç"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+"Mantıksal birimi onarmak için kullanılması gereken fiziksel birimleri seçin. "
+"En az 0$ ihtiyaç vardır."
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Gönder"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Gönderilen"
+
+#: pkg/storaged/drive/drive.jsx:123
+msgid "Serial number"
+msgstr "Seri numarası"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Sunucu"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Sunucu adresi"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Sunucu adresi boş olamaz."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Sunucu boş olamaz."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "Sunucu bağlantıyı kapattı."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Sunucu yazılımı"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Hizmet"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Hizmette bir hata var"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Hizmet günlükleri"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Hizmetler"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Konumu kullanan hizmetler"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Oturum"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Oturumun süresi dolmak üzere"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Ayarla"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Anamakine adını ayarla"
+
+#: pkg/storaged/partitions/partition.jsx:173
+msgid "Set partition type of $0"
+msgstr "Bölüm türünü $0 olarak ayarla"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Parola ayarla"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Saati ayarla"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Şuna ayarla"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Ayarlandı"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Zayıf parola ayarla"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Ayar, yapılandırılmış durumdan farklı ve bir sonraki önyüklemede geri "
+"döndürülecek."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Ayarlanıyor"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Döngü aygıtı $target ayarlanıyor"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Ayarlar"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Önem derecesi"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Paylaşılan"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Kabuk"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Kabuk betiği"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+msgid "Show all $0 rows"
+msgstr "Tüm $0 satırı göster"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Tüm iş parçacıklarını göster"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Show confirmation password"
+msgstr "Onay parolasını göster"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Parmak izlerini göster"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Verilen dizgiyi içeren iletileri gösterin."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Belirtilen systemd birimi için iletileri gösterin."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Belirli bir önyüklemeden gelen iletileri gösterin."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Daha fazla ilişki göster"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "Parolayı göster"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "İlişkileri göster"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Küçült"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Mantıksal birimi küçült"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "Bölümü küçült"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Birimi küçült"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Kapat"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Kapat"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "Başlangıç"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Tek sıra"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Boyut"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "Boyut sıfırdan küçük olamaz"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "Boyut sıfır olamaz"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Boyut çok büyük"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Boyut bir sayı olmak zorundadır"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Boyut en az $0 olmak zorundadır"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Ana gezinmeyi atla"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "İçeriğe atla"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Yuva"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Yuva $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Anlık görüntü"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Soketler"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Yazılım güncellemeleri"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Yazılım tabanlı geçici çözümler, CPU güvenlik sorunlarını önlemeye yardımcı "
+"olur. Bu risk azaltmaları, performansı düşürme yan etkisine sahiptir. Bu "
+"ayarları değiştirmekte sorumluluk size aittir."
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr "Katı Hal Sürücüsü"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Çözüm başarılı olarak uygulandı"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Çözüm başarısız oldu"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Çözümler"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"Bu havuzun bazı blok aygıtları, havuz oluşturulduktan sonra boyut olarak "
+"büyüdü. Havuz, yeni mevcut alanı kullanmak üzere güvenli bir şekilde "
+"büyütülebilir."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"Şu anda başka bir program paket yöneticisini kullanıyor, lütfen bekleyin..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Bazı yazılımların el ile yeniden başlatılması gerekiyor"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr "Üzgünüz"
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "En azdan en çok güvenilene doğru sıralandı"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Yerden kazandıran bilgisayar"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Spanning tree protokolü"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Spanning tree protokolü (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Yedek"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "Belirli bir zaman"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Hız"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Kararlı"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Başlat"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Başlat ve etkinleştir"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Multipath hizmetini başlat"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Hizmeti başlat"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr ""
+"Belirtilen tarihten daha yeni veya o tarihteki girişleri göstermeyi başlatın."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr ""
+"Belirtilen tarihten daha eski veya o tarihteki girişleri göstermeyi başlatın."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Başlatıldı"
+
+#: pkg/storaged/jobs-panel.jsx:64
+msgid "Starting MDRAID device $target"
+msgstr "MDRAID aygıtı $target başlatılıyor"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Takas alanı $target başlatılıyor"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Durum"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Sabit"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Durum"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Çubuk PC"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Yapışkan"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Durdur"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Durdur ve etkisizleştir"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Durdur ve kaldır"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Durdur ve bağlantısını kaldır"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Aygıtı durdur"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Anamakineleri düzenlemeyi durdur"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Raporu durdur"
+
+#: pkg/storaged/jobs-panel.jsx:63
+msgid "Stopping MDRAID device $target"
+msgstr "MDRAID aygıtı $target durduruluyor"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Takas alanı $target durduruluyor"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Depolama"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "Bu sistemde depolama yönetilemez."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Depolama günlükleri"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Parolayı sakla"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Saklanmış parola"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+msgid "Stratis block device"
+msgstr "Stratis blok aygıtı"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+msgid "Stratis block devices"
+msgstr "Stratis blok aygıtları"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Stratis blockdev'leri küçültülemez"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+msgid "Stratis filesystem"
+msgstr "Stratis dosya sistemi"
+
+#: pkg/storaged/stratis/pool.jsx:300
+msgid "Stratis filesystems"
+msgstr "Stratis dosya sistemleri"
+
+#: pkg/storaged/stratis/pool.jsx:361
+msgid "Stratis filesystems pool"
+msgstr "Stratis dosya sistemleri havuzu"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Stratis havuzu"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr "Şeritlenmiş (RAID 0)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr "Şeritlenmiş ve yansıtılmış (RAID 10)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr "Şeritlemeler"
+
+#: pkg/lib/cockpit-components-password.jsx:97
+msgid "Strong password"
+msgstr "Güçlü parola"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Kalıntı"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Stil"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Alt Kasa"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Alt Dizüstü"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "systemd sinyallerine abone olma başarısız oldu: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+msgid "Subvolume needs to be mounted"
+msgstr "Alt birimin bağlanması gerekiyor"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr "Alt birimin yazılabilir bağlanması gerekiyor"
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Başarılı olarak panoya kopyalandı"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Başarılı olarak panoya kopyalandı!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Pazar günleri"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Takas"
+
+#: pkg/storaged/block/resize.jsx:292
+msgid "Swap can not be resized here"
+msgstr "Takas burada yeniden boyutlandırılamaz"
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Giden takas"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "$0 anahtarı"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "$0 arayüzünü kapat"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "$0 arayüzünü aç"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Yönetimsel erişime geç"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Sınırlı erişime geç"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 kapatmak sunucuyla bağlantıyı kesecek ve yönetim kullanıcı arayüzünü "
+"kullanılamaz hale getirecektir."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"$0 açmak sunucuyla bağlantıyı kesecek ve yönetim kullanıcı arayüzünü "
+"kullanılamaz hale getirecektir."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Eşitlendi"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "$0 ile eşitlendi"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Eşitleniyor"
+
+#: pkg/storaged/jobs-panel.jsx:71
+msgid "Synchronizing MDRAID device $target"
+msgstr "MDRAID aygıtı $target eşitleniyor"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Sistem"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Sistem tanılamaları"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Sistem bilgileri"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Sistem güncel"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Sistem değişiklikleri"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Sistem saati"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemd birimleri"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Tablet"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang anahtar sunucusu"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Hedef"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Hedefler"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Takım"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Takım bağlantı noktası"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Takım bağlantı noktası ayarları"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Terminal"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Oturumu sonlandır"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Yapılandırmayı dene"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Deneme sadece kdump hizmeti çalışırken kullanılabilir."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "kdump ayarlarını dene"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Çekirdeği çökerterek kdump ayarlarını deneyin. Bu biraz zaman alabilir ve "
+"sistem otomatik olarak yeniden başlatılamayabilir. Herhangi bir önemli görev "
+"çalışırken sistemi kasıtlı olarak çökertmeyin."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Bağlantı deneniyor"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "$0 paketi hiçbir depoda yok."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "Stratis havuzları oluşturmak için $0 paketi yüklenmek zorundadır."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "$0 paketi yüklenmek zorundadır."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "VDO aygıtları oluşturmak için $0 paketi yüklenecektir."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP adresi veya anamakine adı boşluk karakteri içeremez."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+msgid "The MDRAID device is in a degraded state"
+msgstr "MDRAID aygıtı bozulmuş bir durumda"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+msgid "The MDRAID device must be running"
+msgstr "MDRAID aygıtı çalışıyor olmak zorundadır"
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr ""
+"$2 üzerindeki $1 kullanıcısının $0 SSH anahtarı, $5 üzerindeki $4 "
+"kullanıcısının $3 dosyasına eklenecektir."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"$0 SSH anahtarı, oturumun geri kalanı için kullanılabilir olacak ve diğer "
+"anamakinelerde oturum açmak için de kullanılabilecektir."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"$0 üzerinde oturum açmak için kullanılan SSH anahtarı korumalı ve anamakine "
+"bir parola ile oturum açmaya izin vermiyor. Lütfen $1 konumundaki anahtarın "
+"parolasını sağlayın."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"$0 üzerinde oturum açmak için kullanılan SSH anahtarı korumalı. Oturum açma "
+"parolanızla veya $1 konumundaki anahtarın parolasını sağlayarak oturum "
+"açabilirsiniz."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"'$0' hesabı, bir sonraki oturum açışında parolasını değiştirmeye "
+"zorlanacaktır"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Cockpit hizmeti otomatik olarak dahil edildi"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr "Yapılandırılmış durum bilinmiyor, bir sonraki önyüklemede değişebilir."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr "Bu VDO aygıtının oluşturulması tamamlanmadı ve aygıt kullanılamaz."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"Şu anda oturum açmış olan kullanıcının anahtarlar hakkındaki bilgileri "
+"görmesine izin verilmiyor."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"Biçimlendirmeden önce diskin kilidinin açılması gerekir. Lütfen varolan bir "
+"parola verin."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "$0 dosyası silinecektir."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+msgid "The filesystem has no assigned mount point."
+msgstr "Dosya sisteminin atanmış bir bağlama noktası yok."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "Dosya sisteminin kalıcı bir bağlama noktası yok."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Dosya sistemi önyüklemede otomatik olarak bağlanacak şekilde yapılandırıldı "
+"ancak şifreleme kapsayıcısının kilidi o anda açılmayacaktır."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Dosya sistemi şu anda bağlanmış durumda ancak bir sonraki önyüklemeden sonra "
+"bağlanmayacak."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Dosya sistemi şu anda $0 noktasında bağlanmış durumda ancak bir sonraki "
+"önyüklemede $1 noktasında bağlanacak."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Dosya sistemi şu anda $0 noktasında bağlanmış durumda ancak bir sonraki "
+"önyüklemeden sonra bağlanmayacak."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Dosya sistemi şu anda bağlanmış değil ancak bir sonraki önyüklemede "
+"bağlanacak."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Dosya sistemi bağlanmadı."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"Dosya sisteminin kilidi açılacak ve bir sonraki önyüklemeye bağlanacaktır. "
+"Bu bir parola girmeyi gerektirebilir."
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "Parmak izi eşleşmeli:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Aşağıdaki hizmet yeniden başlatılacaktır:"
+msgstr[1] "Aşağıdaki hizmetler yeniden başlatılacaktır:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "Ad soyad iki nokta üst üste içermemek zorundadır."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "Grup kimliği pozitif tamsayı olmak zorundadır"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Grup adı sadece a-z arasındaki harf, rakam, nokta, tire ve alt çizgi "
+"karakterlerinden oluşabilir"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"$0 ana dizini zaten var. Sahipliği yeni kullanıcı olarak değiştirilecektir."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "initrd yeniden oluşturulmak zorundadır."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Anahtar parolası boş olamaz"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Anahtar parolaları eşleşmiyor"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "Sağladığınız anahtar geçerli değil."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Son anahtar yuvası kaldırılamaz"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "Listelenen işlemler ve hizmetler zorla durdurulacaktır."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "Listelenen işlemler zorla durdurulacaktır."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "Listelenen hizmetler zorla durdurulacaktır."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr ""
+"Oturum açmış kullanıcının sistem değişikliklerini görüntülemesine izin "
+"verilmiyor"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "Makine yeniden başlatılıyor"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "$0 bağlama noktası şu işlemler tarafından kullanılmakta:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "$0 bağlama noktası şu hizmetler tarafından kullanılmakta:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Yeni anahtar parolası boş olamaz"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Parola boş olamaz"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Parolalar eşleşmiyor"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Parolalar eşleşmiyor."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"Ortaya çıkan parmak izinin, e-posta dahil olmak üzere herkese açık "
+"yöntemlerle paylaşılması uygundur."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"Ortaya çıkan parmak izi, e-posta dahil ortak yöntemler aracılığıyla "
+"paylaşılabilir. Eğer başka birinden doğrulamayı sizin için yapmasını "
+"istiyorsanız, sonuçları herhangi bir yöntemi kullanarak gönderebilirler."
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Sunucu, parola kimlik doğrulamasını kullanarak '$0' kullanıcısının kimliğini "
+"doğrulamayı reddetti ve kullanılabilir başka kimlik doğrulama yöntemi yok."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr ""
+"Sunucu, desteklenen herhangi bir yöntemi kullanarak kimlik doğrulamayı "
+"reddetti."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"Sistem şu anda önyükleme sırasında bir dosya sisteminin bir Tang anahtar "
+"sunucusuyla kilidinin açılmasını desteklemiyor."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"Sistem şu anda bir Tang anahtar sunucusuyla kök dosya sisteminin kilidini "
+"açmayı desteklemiyor."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr ""
+"$0 kullanıcısının CPU güvenlik riski azaltmalarını değiştirmesine izin "
+"verilmiyor"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "$0 kullanıcısının şifreleme ilkelerini değiştirmesine izin verilmiyor"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "$0 kullanıcısının çekirdeğin çökmesini denemesine izin verilmiyor"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"Yeni yapılandırmanın etkili olması için kullanıcı oturumu kapatmak ve tekrar "
+"oturum açmak zorundadır."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Kullanıcı adı sadece a-z arasındaki harf, rakam, nokta, tire ve alt çizgi "
+"karakterlerinden oluşabilir."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"Web tarayıcısı yapılandırması Cockpit'in çalışmasını engelliyor "
+"(erişilemeyen $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Şu anda etkin sayfalar yok"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"Sistemde birden çok yola sahip aygıtlar var, ancak multipath hizmeti "
+"çalışmıyor."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "Bu bölgede etkin hizmetler yok"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Bu hesap için yetkilendirilmiş ortak anahtarlar yok."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+"Onarım için kullanılabilecek yeterli alan yok. Bu mantıksal birim için zaten "
+"kullanılmayan fiziksel birimlerde en az $0 gereklidir."
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"Havuzda bu dosya sisteminin anlık görüntüsünü almak için yeterli alan yok. "
+"En az $0 gerekli ancak yalnızca $1 mevcut."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "Makineye bağlanırken beklenmeyen bir hata oldu."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Şu ek adımlar gereklidir:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Şu değişiklikler yapılacaktır:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "İnce mantıksal birim"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Ölçülü kaynak sağlanan LVM2 mantıksal birimleri"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"Bu MDRAID aygıtının yazma amaçlı bit eşlemi yoktur. Böyle bir bit eşlem, "
+"eşitleme sürelerini önemli ölçüde azaltabilir."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"Bu NFS bağlama noktası kullanımda ve sadece seçenekleri değiştirilebilir."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Bu VDO aygıtı, yedek aygıtlarının tümünü kullanmıyor."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+msgid "This device can not be used for the installation target."
+msgstr "Bu aygıt kurulum hedefi için kullanılamaz."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Bu aygıt burada yönetilemez."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "Bu aygıt şu anda kullanımda."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Bu alan boş olamaz"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Bu grup, aşağıdaki kullanıcılar için birincil gruptur:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "Güncellemeler yüklendikten sonra bu anamakine yeniden başlayacak."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "Bu bilgiler sadece sistemde saklanır."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Bu anahtar sunucusu, havuzun kilidini açmanın tek yoludur ve kaldırılamaz."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+"Bu mantıksal birim, fiziksel birimlerinin bir kısmını kaybetmiş ve artık "
+"kullanılamaz. Bunu silip yerine yenisini oluşturmanız gerekiyor."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+"Bu mantıksal birim, fiziksel birimlerinin bir kısmını kaybetmiş ancak henüz "
+"herhangi bir veri kaybetmemiş. Orijinal yedekliliğini geri yüklemek için "
+"onarmalısınız."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+"Bu mantıksal birim, fiziksel birimlerinin bir kısmını kaybetmiş ancak henüz "
+"herhangi bir veri kaybetmemiş olabilir. Belki onarabilirsiniz."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "Bu mantıksal birim, içeriği tarafından tamamen kullanılmıyor."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Bu makine zaten eklenmiş."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Bu biraz zaman alabilir"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "Bu bölüm, içeriği tarafından tamamen kullanılmıyor."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr "Bu parola, havuzun kilidini açmanın tek yoludur ve kaldırılamaz."
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr "Bu havuz, blok aygıtlardaki alanın tümünü kullanmaz."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "Bu havuz bozulmuş bir durumda."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Bu sistem kayıtlı değil"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Bu sistem özel bir profil kullanıyor"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Bu sistem önerilen profili kullanıyor"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"Bu araç, SELinux ilkesini yapılandırır ve ilke ihlallerinin anlaşılmasına ve "
+"çözülmesine yardımcı olabilir."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr ""
+"Bu araç, sistemi çekirdek çökme dökümlerini diske yazacak şekilde "
+"yapılandırır."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"Bu araç, çalışan sistemden bir yapılandırma ve tanılama bilgileri arşivi "
+"oluşturur. Arşiv, kayıt veya izleme amacıyla yerel veya merkezi olarak "
+"depolanabilir veya teknik hata bulma ve hata ayıklamaya yardımcı olması için "
+"teknik destek temsilcilerine, geliştiricilere veya sistem yöneticilerine "
+"gönderilebilir."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"Bu araç, dosya sistemleri, LVM2 birim grupları ve NFS bağlamaları gibi yerel "
+"depolamayı yönetir."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"Bu araç, NetworkManager ve Firewalld kullanarak birleştirmeler, köprüler, "
+"takımlar, VLAN'lar ve güvenlik duvarları gibi ağları yönetir. "
+"NetworkManager, Ubuntu'nun varsayılan systemd-networkd ve Debian'ın ifupdown "
+"betikleriyle uyumsuzdur."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Bu birim açıkça etkinleştirilecek şekilde tasarlanmadı."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Bu kullanıcı adı zaten var"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr "Bu birim grubunda bazı fiziksel birimler eksik."
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr "Bu web tarayıcısı, Web Konsolunu çalıştırmak için çok eski ($0 eksik)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Bu, '_BOOT_ID=' için bir eşleşme ekleyecektir. Eğer belirtilmemişse, şu anki "
+"önyükleme için günlükler gösterilecektir. Eğer önyükleme kimliği atlanırsa, "
+"pozitif bir denkleştirme, günlüğün başından itibaren başlayarak "
+"önyüklemeleri arayacak ve eşit ya da sıfırdan az denkleştirme, günlüğün "
+"sonundan başlayarak önyüklemeleri arayacaktır. Böylece, 1, günlükte bulunan "
+"ilk önyükleme, 2, ikincisi ve bunun gibi; -0 son önyükleme ise, -1 sondan "
+"önceki önyükleme ve bunun gibi."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Bu, belirli birim için olası tüm iletileri bulmak amacıyla '_SYSTEMD_UNIT=', "
+"'COREDUMP_UNIT=' ve 'UNIT=' için eşleşme ekleyecek. Virgülle ayrılmış daha "
+"fazla birim içerebilir. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Bu, gelecekte parola olmadan oturum açmanıza izin verecek."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"Bu bölge cockpit hizmetini içeriyor. Bu bölgenin geçerli web konsolu "
+"bağlantınıza uygulanmadığından emin olun."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Perşembe günleri"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Katman"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Zaman"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Saat dilimi"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Zamanlayıcı oluşturma başarısız oldu"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "Zamanlayıcı silme başarısız oldu"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Zamanlayıcılar"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"İpucu: Diğer sistemlerde otomatik olarak kimlik doğrulaması yapmak için "
+"anahtar parolanızın oturum açma parolanızla eşleşmesini sağlayın."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Bağlantınızın kötü niyetli bir üçüncü tarafça engellenmediğinden emin olmak "
+"için lütfen anamakine anahtar parmak izini doğrulayın:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Yazılım güncellemelerini almak için bu sistemin Red Hat Müşteri Portalı veya "
+"yerel bir abonelik sunucusu kullanılarak Red Hat'e kaydedilmesi gerekir."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Bir parmak izini doğrulamak için makinede fiziksel olarak bulunurken veya "
+"güvenilir bir ağ aracılığıyla 0$ üzerinde aşağıdakileri çalıştırın:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Bugün"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Değiştir"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Tarihi seçiciyi aç/kapat"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Süzgeçleri aç/kapat"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Çok fazla veri"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Araçlar"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "En iyi 5 CPU hizmeti"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "En iyi 5 disk kullanım hizmeti"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "En iyi 5 bellek hizmeti"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Toplam boyut: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Tower"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Geçici"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Aktarma"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Tetikleyici"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Tetikleyen"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Tetikleyiciler"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Sorun gider"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Sorun gider…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "Anamakineye güven ve ekle"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Güvenilen anahtar"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Güven seviyesi"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Tekrar dene"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "$0 ile eşitlemeye çalışılıyor"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Salı günleri"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned başlatılamadı"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned , sisteminizi izleyen ve belirli iş yükleri altında performansı en iyi "
+"hale getiren bir hizmettir. Tuned'ın çekirdeği, sisteminizi farklı kullanım "
+"durumları için ayarlayan profillerdir."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned kullanılabilir değil"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned çalışmıyor"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned kapalı"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Yönetimsel erişimi aç"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Tür"
+
+#: pkg/storaged/partitions/partition.jsx:165
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Tür yalnızca 0 - 9, A - F ve \"-\" karakterlerini içerebilir."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr "Tür NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN biçiminde olmak zorundadır."
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+"Tür tam olarak iki onaltılık karakter içermek zorundadır (0 - 9, A - F)."
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Süzmek için yazın"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Bu çözüm otomatik olarak uygulanamıyor"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Bu adrese bağlanılamıyor"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "$0 ile bağlantı kurulamıyor."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Verilen $0 anamakinesiyle iletişim kurulamıyor. SSH hizmetinin $1 nolu "
+"bağlantı noktasında çalıştığından emin olun veya adreste başka bir bağlantı "
+"noktası belirtin."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Uyarı ayrıntıları alınamıyor."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"SSH anahtar kimlik doğrulaması kullanılarak $0 üzerinde oturum açılamıyor. "
+"Lütfen parolayı girin. Otomatik oturum açma için SSH anahtarlarınızı "
+"ayarlamak isteyebilirsiniz."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"$0 üzerinde oturum açılamıyor. Anamakine, parola ile oturum açmayı veya SSH "
+"anahtarlarınızdan hiçbirini kabul etmiyor."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Sunucuya ulaşılamıyor"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Bağlama noktası kaldırılamıyor"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+msgid "Unable to repair logical volume $0"
+msgstr "$0 mantıksal birimi onarılamıyor"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "Düzeltme çalıştırılamıyor: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "Ayarlar kaydedilemiyor"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "Ayarlar kaydedilemiyor: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "setroubleshootd başlatılamıyor"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Dosya sisteminin bağlantısı kaldırılamıyor"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Kullanılamıyor"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Kullanılamayan paketler"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Geri al"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "$0 kurulumu sırasında beklenmeyen PackageKit hatası: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Beklenmeyen hata"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+msgid "Unformatted data"
+msgstr "Biçimlendirilmemiş veriler"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Birim"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Bilinmiyor"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Bilinmeyen \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Bilinmeyen ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Bilinmeyen uygulama"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Bilinmeyen yapılandırma"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Bilinmeyen anamakine adı"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Bilinmeyen hizmet adı"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Bilinmeyen tür"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Kilidi aç"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Önyüklemede kilidi otomatik olarak aç"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr "Yeniden boyutlandırmadan önce kilidini aç"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Şifrelenmiş Stratis havuzu kilidini aç"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "$0 anahtarı kilidini aç"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "$target kilidi açılıyor"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Disk kilidi açılıyor"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Yönetilmeyen arayüzler"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Bağlantıyı kaldır"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "$0 dosya sistemi bağlantısını kaldır"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Şimdi bağlantıyı kaldır"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "$target bağlantısı kaldırılıyor"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Adsız"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "Birimi sabitlemeyi kaldır"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Tanınmayan veri"
+
+#: pkg/storaged/block/resize.jsx:306
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Tanınmayan veriler burada küçültülemez"
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Tanınmayan veriler burada küçültülemez."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+msgid "Unsupported logical volume"
+msgstr "Desteklenmeyen mantıksal birim"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "Bitiş"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Güvenilmeyen anamakine"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Güncelle"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Başarı Tablosunu Güncelle"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Güncelleme geçmişi"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Paket bilgilerini güncelle"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "Güncelleme başarılı oldu"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Güncellendi"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr ""
+"Güncellenen paketlerin etkili olması için yeniden başlatma gerekebilir."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Güncellemeler mevcut"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Güncellemelerin geçmişi"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Güncellemeler, $0 saat $1'da uygulanacaktır"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Güncellenen"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Durum güncelleniyor..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Çalışma süresi"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Kullanım"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "$0 kullanımı"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Kullan"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Sıkıştırma kullan"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Tekilleştirme kullan"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Anahtar kullan"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Parola kullan"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Diğer sistemlerde kimlik doğrulamak için aşağıdaki anahtarları kullan"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Ayrıntılı günlükleme kullan"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Kullanılan"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"İsteğe bağlı olan veya etkileşim gerektiren bağlamalar için kullanışlıdır "
+"(parolalar gibi)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Kullanıcı"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "Kullanıcı kimliği"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "Kullanıcı kimliği zaten başka bir kullanıcı tarafından kullanılıyor"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "Kullanıcı kimliği bir pozitif tamsayı olmak zorundadır"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "Kullanıcı kimliği $0 değerinden yüksek olmamak zorundadır"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "Kullanıcı kimliği $0 değerinden düşük olmamak zorundadır"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Kullanıcı adı"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Kullanıcı adı boş olamaz"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Kullanıcı adı"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "LUKS şifrelemesi kullanma"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "Tang sunucusu kullanma"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "VDO yedek aygıtları küçültülemez"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO aygıtı $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "VDO dosya sistemi birimi (sıkıştırma/kopyaları kaldırma)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN kimliği"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Adres doğrulanıyor"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Kimlik doğrulama belirteci doğrulanıyor"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Satıcı"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Doğrulandı"
+
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "Parmak izini doğrula"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Anahtarı doğrula"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Doğrulanıyor"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Sürüm"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "$target çok güvenli bir şekilde siliniyor"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Tüm CPU'ları görüntüle"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Tüm diskleri görüntüle"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Tüm günlükleri görüntüle"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Tüm hizmetleri görüntüle"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Otomatikleştirme betiğini görüntüle"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Ayrıntılı günlükleri görüntüle"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Donanım ayrıntılarını görüntüle"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Oturum açma geçmişini görüntüle"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "Günlükleri görüntüle"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Ölçümleri ve geçmişi görüntüle"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Disk başına aktarım hızını görüntüle"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Proje web sitesini görüntüle"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Raporu görüntüle"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Güncelleme günlüğünü görüntüle"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "Bellek bilgilerini görüntülemek için yönetimsel erişim gerekir."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Güvenlik duvarını ziyaret et"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Birim grubu"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+msgid "Volume group is missing physical volumes"
+msgstr "Birim grubunda fiziksel birimler eksik"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Birim boyutu $0. İçerik boyutu $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Bekleniyor"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Ayrıntılar bekleniyor..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Girdi bekleniyor…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"Diğer programların paket yöneticisini kullanmayı bitirmesi bekleniyor..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Diğer yazılım yönetimi işlemlerinin bitmesi bekleniyor"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Başlatma bekleniyor…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "İsteyen"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "İstenen"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Uyarı ve üstü"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+msgid "Weak password"
+msgstr "Zayıf parola"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Web Konsolu"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Linux sunucuları için Web Konsolu"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Web Konsolu yeniden başlayacak"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Web konsolu sınırlı erişim kipinde çalışıyor."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Web konsol logosu"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Çarşamba günleri"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Haftalık"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Haftalar"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Zamanı"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "Boş olduğunda, şu anki kullanıcıyla bağlan"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Web Konsolu yeniden başlatıldığında, ilerleme bilgilerini artık "
+"görmeyeceksiniz. Ancak güncelleme işlemi arka planda devam edecek. "
+"Güncelleme işlemini izlemeye devam etmek için yeniden bağlanın."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"Bu seçenek işaretlendiğinde, yeni havuz fazla sağlamaya izin vermeyecektir. "
+"Havuzda oluşturulan her dosya sistemi için en fazla boyutu belirtmeniz "
+"gerekir. Dosya sistemleri oluşturulduktan sonra büyütülemez. Anlık "
+"görüntüler oluşturma sırasında tamamı ayrılır. En fazla boyut toplamı "
+"havuzun boyutunu aşamaz. Bunun avantajı ise şaşırtıcı bir şekilde bu "
+"havuzdaki dosya sistemlerinde yer sıkıntısı yaşanmamasıdır. Dezavantajı ise "
+"her dosya sistemi için en fazla boyutu önceden bilmeniz gerekmesi ve anlık "
+"görüntülerin oluşturulmasının sınırlı olmasıdır."
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Beyaz"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "\"Otomatik\" olarak ayarlanacak"
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+msgid "World wide name"
+msgstr "Dünya çapında ad"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Yazma"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Çoğunlukla yazma"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Yazma"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Kullanıcı adı veya parola yanlış"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Yıllık"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Evet"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "İlk kez $0 için bağlanıyorsunuz."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Güvenlik duvarını değiştirmek için yetkili değilsiniz."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"Bu hesap için yetkilendirilmiş ortak anahtarları görüntüleme izniniz yok."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Etkinlik olmamasından dolayı oturumunuz kapatıldı."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Daha eski girişleri yüklemeyi deneyebilirsiniz."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr ""
+"Otomatik oturum açmak için anahtarın parolasını değiştirmek isteyebilirsiniz."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Parolanızı değiştirmek için daha uzun süre beklemek zorundasınız"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr "Ölçümler geçmişini görebilmek için yeniden oturum açmanız gerekli"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Artık yönetimsel erişiminiz var."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "$0 saniye içinde oturumunuz kapatılacak."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Hesabınız"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"Tarayıcınız, bağlam menüsünden yapıştırmaya izin vermiyor. Shift+Insert "
+"kullanabilirsiniz."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "Tarayıcınız, oturumlar arasında erişim seviyenizi hatırlayacaktır."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Sunucunuz bağlantıyı yakında kapatacak. Yeniden başlatıldıktan sonra yeniden "
+"bağlanabilirsiniz."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Oturumunuz sonlandırıldı."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "Oturumunuzun süresi doldu. Lütfen tekrar oturum açın."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Bölge"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[ikili veri]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[veri yok]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "erişim"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "etkin"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "eklenti"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "eklenti"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "ağdan sonra"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "uygulamalar"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "demirbaş etiketi"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "saat"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "ortalama: %$0"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "VDO aygıtı için aygıt yedekleniyor"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "birleştirme"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "önyükleme"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "köprü"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+msgid "btrfs device"
+msgstr "btrfs aygıtı"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+msgid "btrfs devices"
+msgstr "btrfs aygıtı"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+msgid "btrfs subvolume"
+msgstr "btrfs alt birimi"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr "$0 / $1 btrfs alt birimi"
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+msgid "btrfs subvolumes"
+msgstr "btrfs alt birimi"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+msgid "btrfs volume"
+msgstr "btrfs birimi"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "hata düzeltme"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "bayt"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+msgid "cache"
+msgstr "önbellek"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgruplar"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "değiştir"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman yüklü değil"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "komut"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "konsol"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "çekirdek dökümü"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "cpu"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "çökme"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+msgid "data"
+msgstr "veri"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "tarih"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+msgid "deactivate"
+msgstr "devre dışı bırak"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "hata ayıklama"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "sil"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr "btrfs birimi aygıtı"
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "etkisizleştir"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "disk"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "diskler"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "etki alanı"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "sürücü"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "düzenle"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "etkinleştir"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+msgid "encrypted"
+msgstr "şifrelenmiş"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "şifreleme"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "iyileştirme"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "hata"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "her Cuma"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "her Pazartesi"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "her Cumartesi"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "her Pazar"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "her Perşembe"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "her Salı"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "her Çarşamba"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "her gün"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "uzantı"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "ssh anamakine anahtarlarını listeleme başarısız oldu: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "dosya sistemi"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "güvenlik duvarı"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "şu anki ve gelecek çekirdekler için"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "sadece şu anki çekirdek için"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "biçim"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "$0 anamakinesinden"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "$1 üzerinde $0 anamakinesinden"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "grafikler"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "büyüt"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "donanım"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "geçmiş"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "anamakine"
+
+#: pkg/storaged/drive/drive.jsx:65
+msgid "iSCSI Drive"
+msgstr "iSCSI Sürücüsü"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+msgid "iSCSI drives"
+msgstr "iSCSI sürücüleri"
+
+#: pkg/storaged/iscsi/session.jsx:45
+msgid "iSCSI portal"
+msgstr "iSCSI portalı"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "hatayı yoksay"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "çoğu tarayıcıda"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "tutarsız"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "başlat"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "yükle"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "arayüz"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "geçersiz: birden fazla hedef tanımlandı"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "günlük"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "journalctl kılavuz sayfası"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "kdump durumu"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "anahtarlar"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "oturum aç"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "makine"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "maskele"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "en fazla: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+msgid "member of MDRAID device"
+msgstr "MDRAID aygıtı üyesi"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "Stratis havuzu üyesi"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "bellek"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "ölçümler"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "risk azaltması"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "daha fazla ayrıntı"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "bağla"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "ağ"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "önyüklemede asla bağlama"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "nfs dışa aktarma boş"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "nfs sunucusu boş"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "nfs sunucusu geçerli IPv6 değil"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "harika"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "yok"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "$0 CPU'nun"
+msgstr[1] "$0 CPU'nun"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "$0 üzerinde"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "işletim sistemi"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "is"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "paket"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "bölüm"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "parola"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "parola kalitesi"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "yamalar"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "yol"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "performans"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "LVM2 birim grubunun fiziksel birimi"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "eklenti"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service başarısız oldu"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service veri toplamada başarısız oluyor"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service çalışmıyor"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "bölme"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "bağlantı noktası"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "güç"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "salt okunur"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "önerilir"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "LVM2'den kaldır"
+
+#: pkg/storaged/utils.js:934
+msgid "remove from MDRAID"
+msgstr "MDRAID'den kaldır"
+
+#: pkg/storaged/utils.js:904
+msgid "remove from btrfs volume"
+msgstr "btrfs biriminden kaldır"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "yeniden başlat"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "roller"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "$0 çalışıyor"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "güvenlik"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "seri"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "hizmet"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "kabuk"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "daha az göster"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "daha fazla göster"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "küçült"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "kapat"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "soket"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "çözüm"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "çözüm ayrıntıları"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "sos raporu başarısız oldu"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ssh anahtarı bir yol değil"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "ssh sunucusu boş"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "durdur"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "hatada önyüklemeyi durdur"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+msgid "stopped"
+msgstr "durduruldu"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "hedef"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "takım"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "saat"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "zamanlayıcı"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "birim"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "bilinmeyen"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "bilinmeyen hedef"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "maskelemeyi kaldır"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "bağlantıyı kaldır"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "$0 üzerinde bölümlendirilmemiş alan"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "kullanıcı"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "kullanıcı adı"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "kullanılan anahtar açıklaması $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "sürüm"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "birim"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "uyarı"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "wireguard-tools paketi yüklü değil"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "evet"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "bölge"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Kdump hizmeti yüklü değil. Lütfen kexec-tools paketinin yüklü olduğundan "
+#~ "emin olun."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Bellek ayrılmadı. Önyükleme sırasında bellek ayırmak için çekirdek komut "
+#~ "satırına (örn. /etc/default/grub içinde) bir crashkernel seçeneği "
+#~ "ekleyin. Örnek: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Hizmet çalışıyor"
+
+#~ msgid "Service is starting"
+#~ msgstr "Hizmet başlatılıyor"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Hizmet durduruldu"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Hizmet durduruluyor"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Bu, çekirdeği çöktürerek kdump yapılandırmasını deneyecek."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "crashkernel, çekirdek komut satırında yapılandırılmamış"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (şifrelenmiş)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 Stratis havuzu"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 önbelleği"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 bilinmeyen katmanı"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, $1 boş"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr ""
+#~ "Bu diskin kaldırılabilmesi için önce yedek bir diskin eklenmesi gerekir."
+
+#~ msgid "Backing device"
+#~ msgstr "Yedeklenen aygıt"
+
+#~ msgid "Block"
+#~ msgstr "Blok"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Kapasite"
+
+#~ msgid "Content"
+#~ msgstr "İçerik"
+
+#~ msgid "Create devices"
+#~ msgstr "Aygıtları oluştur"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Aygıt"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Aygıt dosyası"
+
+#~ msgid "Devices"
+#~ msgstr "Aygıtlar"
+
+#~ msgid "Drives"
+#~ msgstr "Sürücüler"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Şifrelenmiş birimlerin yeniden boyutlandırılabilmeleri için kilidinin "
+#~ "açılması gerekir."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Dosya sistemi (şifrelenmiş)"
+
+#~ msgid "Filesystems"
+#~ msgstr "Dosya sistemleri"
+
+#~ msgid "Free"
+#~ msgstr "Boş"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Bu grupta yer açın: Diğer mantıksal birimleri küçültün veya silin ya da "
+#~ "başka bir fiziksel birim ekleyin."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Etkin olmayan birim"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Anahtar sunucusu"
+
+#~ msgid "LVM2 member"
+#~ msgstr "LVM2 üyesi"
+
+#~ msgid "Locked devices"
+#~ msgstr "Kilitli aygıtlar"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Model"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS bağlama noktaları"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "NFS desteği yüklü değil"
+
+#~ msgid ""
+#~ "New logical volumes can not be created while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "Bir birim grubunda fiziksel birimler eksikken yeni mantıksal birimler "
+#~ "oluşturulamaz."
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "NFS bağlama noktası ayarlanmadı"
+
+#~ msgid "No devices"
+#~ msgstr "Aygıtlar yok"
+
+#~ msgid "No drives attached"
+#~ msgstr "Bağlı sürücüler yok"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "iSCSI hedefleri ayarlanmadı"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Yeni dosya sistemleri için yeterli alan yok"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Diğer veriler"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Bölünmüş blok aygıtı"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Parola"
+
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr ""
+#~ "Bir birim grubunda fiziksel birimler eksikken fiziksel birimler "
+#~ "kaldırılamaz."
+
+#~ msgid "Pool"
+#~ msgstr "Havuz"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "İnce birimler için havuz"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID seviyesi"
+
+#~ msgid "RAID member"
+#~ msgstr "RAID üyesi"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Çıkarılabilir sürücü"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "$0 aygıtı göster"
+#~ msgstr[1] "$0 aygıtın tümünü göster"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "$0 sürücüyü göster"
+#~ msgstr[1] "$0 sürücünün tümünü göster"
+
+#~ msgid "Show all"
+#~ msgstr "Tümünü göster"
+
+#~ msgid "Source"
+#~ msgstr "Kaynak"
+
+#~ msgid "Start pool"
+#~ msgstr "Havuzu başlat"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Dosya sistemlerini görmek için havuzu başlatın."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Durum"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Stratis havuzu durduruldu"
+
+#~ msgid "Stratis member"
+#~ msgstr "Stratis üyesi"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "$0 Stratis havuzu"
+
+#~ msgid "Support is installed."
+#~ msgstr "Destek yüklendi."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr "Yedek diskler eklemek için RAID aygıtı çalışıyor olmak zorundadır."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "Bir RAID aygıtının son diski kaldırılamaz."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "Bir birim grubunun son fiziksel birimi kaldırılamaz."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Bu fiziksel birimi kaldırmak için başka bir yerde yeterli boş alan yok. "
+#~ "En az $0 daha boş alana ihtiyaç var."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "Aygıt kurtarılırken bu disk kaldırılamaz."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr ""
+#~ "Bu birimin yeniden boyutlandırılabilmesi için etkinleştirilmesi gerekir."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Tanınmayan veri"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Kullanım"
+
+#~ msgid "Used for"
+#~ msgstr "Kullanılma"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "VDO yedek"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO aygıtı"
+
+#~ msgid "Volume"
+#~ msgstr "Birim"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Otomatik (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Anahtarı kabul et ve bağlan"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Şifrelenmiş birimler burada yeniden boyutlandırılamaz."
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Bir Tang anahtar sunucusu kullan"
+
+#~ msgid "Use a passphrase"
+#~ msgstr "Bir parola kullan"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitmap"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Anahtar açıklaması beklenen biçimde olmadığından, bu havuzun kilidi "
+#~ "burada açılamaz."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Bit eşlemi aç/kapat"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Tang sunucusundaki anahtar adreslemesinin aşağıdakilerden biriyle "
+#~ "eşleştiğinden emin olun:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "SSH ile elle denetleyin: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Bağlantı noktası numarası ve türü eşleşmiyor"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Kilitli şifrelenmiş Stratis havuzu"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Şu uyarı silinirken hata oldu: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Uyarı alınamıyor: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 bayt ikili veri]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Disk G/Ç sıçraması"
+
+#~ msgid "Event"
+#~ msgstr "Olay"
+
+#~ msgid "Event logs"
+#~ msgstr "Olay günlükleri"
+
+#~ msgid "Load spike"
+#~ msgstr "Yük sıçraması"
+
+#~ msgid "Memory spike"
+#~ msgstr "Bellek sıçraması"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Ağ G/Ç sıçraması"
+
+#~ msgid "ssh key"
+#~ msgstr "ssh anahtarı"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Eğer bu seçenek işaretlenirse, dosya sistemi bir sonraki önyükleme "
+#~ "sırasında, önüne bağlanmış olsa bile bağlanmayacaktır. Bu, önyükleme "
+#~ "sırasında bağlama mümkün değilse , örneğin dosya sisteminin kilidini "
+#~ "açmak için bir parola gerektiğinde, ancak önyüklemenin katılımsız "
+#~ "olmasında yararlıdır."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Önyüklemede asla bağlama"
+
+#~ msgid "Listing unit files"
+#~ msgstr "Birim dosyalarını listeleme"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "Birim dosyalarını listeleme başarısız oldu: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "Birim bulunamadı"
+
+#~ msgid "Validating key"
+#~ msgstr "Anahtar doğrulanıyor"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "$0 grubunu sil"
+
+#~ msgid "Container administrator"
+#~ msgstr "Kapsayıcı yöneticisi"
+
+#~ msgid "Image builder"
+#~ msgstr "Kalıp oluşturucu"
+
+#~ msgid "Roles"
+#~ msgstr "Roller"
+
+#~ msgid "Server administrator"
+#~ msgstr "Sunucu yöneticisi"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Unix grubu: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Başarılı olarak klavyeye kopyalandı"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Tanılama Raporları"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Çekirdek Dökümü"
+
+#~ msgid "Software Updates"
+#~ msgstr "Yazılım Güncellemeleri"
+
+#~ msgid "Update log"
+#~ msgstr "Güncelleme günlüğü"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "nfs döküm hedefi sunucu:yol olarak biçimlendirilmedi"
+
+#~ msgid "$0 Zone"
+#~ msgstr "$0 Bölgesi"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Tanılama raporu oluştur"
+
+#~ msgid "Done!"
+#~ msgstr "Bitti!"
+
+#~ msgid "Download report"
+#~ msgstr "Raporu indir"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Oluşturulmuş arşiv yok."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Lütfen $0 aygıtının silinmesini onaylayın"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "Oluşturulan arşiv hassas olarak değerlendirilen veriler içermektedir ve "
+#~ "herhangi bir üçüncü tarafa iletilmeden önce oluşturan organizasyon "
+#~ "tarafından içeriği gözden geçirilmelidir."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Bu araç, sistemle ilgili sorunları teşhis etmek amacıyla kullanmak için "
+#~ "bu sistemden, sistem yapılandırması ve tanılama bilgilerini toplayacak."
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "Sunucuda cockpit-system paketi eksik"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Bu paket, Cockpit'in bu sürümüyle uyumlu değil"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Bu paket, Cockpit'in %s veya daha sonraki bir sürümünü gerektirir"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Performans Ölçümleri"
+
+#~ msgid "Reboot to apply new crypto policy"
+#~ msgstr "Yeni şifreleme ilkesini uygulamak için yeniden başlat"
+
+#~ msgid "Save only"
+#~ msgstr "Yalnızca kaydet"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB toplam"
+
+#~ msgid "Apply"
+#~ msgstr "Uygula"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "$0 bölgesini kaldırmaya yetkili değil"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Parolayı yine de kullanmak için $0'a tekrar tıklayın."
+
+#~ msgid "Access"
+#~ msgstr "Erişim"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "DİSK BOZULUYOR"
+
+#~ msgid "Logical Size"
+#~ msgstr "Mantıksal Boyut"
+
+#~ msgid "Security updates "
+#~ msgstr "Güvenlik güncellemeleri "
+
+#~ msgid "Updates "
+#~ msgstr "Güncellemeler "
+
+#~ msgid "(Optional)"
+#~ msgstr "(İsteğe bağlı)"
+
+#~ msgid "Active since"
+#~ msgstr "Etkin olma başlangıcı"
+
+#~ msgid "Affected locations"
+#~ msgstr "Etkilenen yerler"
+
+#~ msgid "Process"
+#~ msgstr "İşlem"
+
+#~ msgid "Remove and delete"
+#~ msgstr "Kaldır ve sil"
+
+#~ msgid "Remove and format"
+#~ msgstr "Kaldır ve biçimlendir"
+
+#~ msgid "Remove and grow"
+#~ msgstr "Kaldır ve büyüt"
+
+#~ msgid "Remove and initialize"
+#~ msgstr "Kaldır ve başlat"
+
+#~ msgid "Remove and shrink"
+#~ msgstr "Kaldır ve küçült"
+
+#~ msgid "Remove and stop device"
+#~ msgstr "Kaldır ve aygıtı durdur"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Dosya sistemi açılan oturumlar ve sistem hizmetleri tarafından "
+#~ "kullanılıyor. Devam etmek bunları durduracak."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Dosya sistemi açılan oturumlar tarafından kullanılıyor. Devam etmek "
+#~ "bunları durduracak."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Dosya sistemi sistem hizmetleri tarafından kullanılıyor. Devam etmek "
+#~ "bunları durduracak."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "Bu aygıtta şu anda kullanımda olan dosya sistemleri var. Devam etmek, "
+#~ "üzerindeki tüm dosya sistemlerinin bağlantısını kaldıracak."
+
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Bu aygıt şu anda LVM2 birim grupları için kullanılıyor."
+
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Bu aygıt şu anda LVM2 birim grupları için kullanılıyor. Devam etmek onu "
+#~ "birim gruplarından kaldıracak."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Bu aygıt şu anda RAID aygıtları için kullanılıyor."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Bu aygıt şu anda RAID aygıtları için kullanılıyor. Devam etmek onu RAID "
+#~ "aygıtlarından kaldıracak."
+
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Bu aygıt şu anda Stratis havuzları için kullanılıyor."
+
+#~ msgid "Unmount and delete"
+#~ msgstr "Bağlantıyı kaldır ve sil"
+
+#~ msgid "Unmount and format"
+#~ msgstr "Bağlantıyı kaldır ve biçimlendir"
+
+#~ msgid "Unmount and grow"
+#~ msgstr "Bağlantıyı kaldır ve büyüt"
+
+#~ msgid "Unmount and initialize"
+#~ msgstr "Bağlantıyı kaldır ve başlat"
+
+#~ msgid "Unmount and shrink"
+#~ msgstr "Bağlantıyı kaldır ve küçült"
+
+#~ msgid "Unmount and stop device"
+#~ msgstr "Bağlantıyı kaldır ve aygıtı durdur"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0/$1"
+
+#~ msgid "Blockdev"
+#~ msgstr "Blockdev"
+
+#~ msgid "Blockdev of Stratis pool $0"
+#~ msgstr "$0 Stratis havuzu için blockdev"
+
+#~ msgid "Blockdev of locked Stratis pool $0"
+#~ msgstr "Kilitli $0 Stratis havuzu için blockdev"
+
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "$0 aygıtının LVM2 fiziksel birimi"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "$0 RAID aygıtının üyesi"
+
+#~ msgid "Menu"
+#~ msgstr "Menü"
+
+#~ msgid "VDO backing"
+#~ msgstr "VDO yedek"
+
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Stratis Havuzu Oluştur"
+
+#~ msgid "Mount Options"
+#~ msgstr "Bağlama Seçenekleri"
+
+#~ msgid "Stratis Pool"
+#~ msgstr "Stratis Havuzu"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Bir disk gerekli."
+
+#~ msgid "Disk"
+#~ msgstr "Disk"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Sadece eski uygulamalar için. Performansı düşürür."
+
+#~ msgid "Install VDO support"
+#~ msgstr "VDO desteğini yükle"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "512 bayt benzetimi kullan"
+
+#~ msgid "System services"
+#~ msgstr "Sistem hizmetleri"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Tanılama raporu oluştur "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr "Aynı anda {{ limit }} makineden fazlasına bağlanma desteklenmiyor."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "Kerberos tabanlı SSO"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "{{host}} cihazına oturum aç"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Yeni anahtar parolaları eşleşmiyor"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "Bir parmak izini doğrulamak için makinede fiziksel olarak bulunurken veya "
+#~ "güvenilir bir ağ aracılığıyla {{host}} üzerinde aşağıdakileri çalıştırın:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "{{#strong}}{{host}}{{/strong}} ile iletişim kurulamıyor."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}} cihazına oturum açılamıyor. Daha fazla "
+#~ "kimlik doğrulama seçeneği ve sorun giderme desteği için lütfen cockpit-"
+#~ "ws'yi daha yeni bir sürüme yükseltin."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}} cihazına oturum açılamıyor. Bu anamakineye "
+#~ "bağlanmak için {{#strong}}{{host}}{{/strong}} cihazındaki sshd "
+#~ "yapılandırmasında şu kimlik doğrulama yöntemlerinden birini "
+#~ "etkinleştirmeniz gerekecek:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "İlk kez {{host}} anamakinesine bağlanıyorsunuz."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "{{host}} anahtarı değişti"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Bağlama noktası yapılandırmasını temizle"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Bu birleştirmeyi (bond) oluşturmak sunucuyla bağlantıyı kesecek ve "
+#~ "yönetim kullanıcı arayüzünü kullanılamaz hale getirecektir."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Bu köprüyü oluşturmak sunucuyla bağlantıyı kesecek ve yönetim kullanıcı "
+#~ "arayüzünü kullanılamaz hale getirecektir."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Bu takımı oluşturmak sunucuyla bağlantıyı kesecek ve yönetim kullanıcı "
+#~ "arayüzünü kullanılamaz hale getirecektir."
+
+#~ msgid "IP settings"
+#~ msgstr "IP ayarları"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "<b>$0</b> kapatılması sunucuyla bağlantıyı kesecek ve yönetim kullanıcı "
+#~ "arayüzünü kullanılamaz hale getirecektir."
+
+#~ msgid "Administrator password"
+#~ msgstr "Yönetici parolası"
+
+#~ msgid "Computer OU"
+#~ msgstr "Bilgisayar kuruluş birimi"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "Bir RAID aygıtını silmek üzerindeki tüm verileri silecektir."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "Bir birim grubunu silmek üzerindeki tüm verileri silecektir."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Varolan verilerin üzerine yazma"
+
+#~ msgid "Erase"
+#~ msgstr "Sil"
+
+#~ msgid "Format disk $0"
+#~ msgstr "$0 diskini biçimlendir"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr ""
+#~ "Bir depolama aygıtını biçimlendirmek içindeki tüm verileri silecektir."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "Bir etki alanında anamakine adı değiştirilmemelidir"
+
+#~ msgid "More"
+#~ msgstr "Daha fazla"
+
+#~ msgid "Not joined"
+#~ msgstr "Katılmadı"
+
+#~ msgid "One time password"
+#~ msgstr "Tek seferlik parola"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$2 üzerinde $1 cihazından $0"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Saat, 0 ile 23 arasında bir sayı olmak zorundadır"
+
+#~ msgid "Last login:"
+#~ msgstr "Son oturum açma:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Dakika, 0 ile 59 arasında bir sayı olmak zorundadır"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "Son başarılı oturum açmadan bu yana $0 başarısız oturum açma denemesi "
+#~ "oldu."
+#~ msgstr[1] ""
+#~ "Son başarılı oturum açmadan bu yana $0 başarısız oturum açma denemesi "
+#~ "oldu."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "örn. \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0 etkin bölge"
+
+#~ msgid "Dismiss $0 alerts"
+#~ msgstr "$0 uyarıyı yoksay"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 defa meydana geldi"
+#~ msgstr[1] "$0 defa meydana geldi"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Lisansı:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Önyüklemede kilidi aç"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Salt okunur kilidini aç"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Günlükleri terimlerin bir birleşimi ile ara:"
+
+#~ msgid "Show filters"
+#~ msgstr "Süzgeçleri göster"
+
+#~ msgid "Text"
+#~ msgstr "Metin"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "düzenli ifade olarak herhangi bir serbest biçimli dizgi"
+
+#~ msgid "e.g."
+#~ msgstr "örn."
+
+#~ msgid "log fields"
+#~ msgstr "günlük alanları"
+
+#~ msgid "qualifiers"
+#~ msgstr "niteleyiciler"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "Oturum ayarlarını değiştir"
+
+#~ msgid "(none)"
+#~ msgstr "(yok)"
+
+#~ msgid "Create timers"
+#~ msgstr "Zamanlayıcılar oluştur"
+
+#~ msgid "Recommended default"
+#~ msgstr "Varsayılan önerilen"
+
+#~ msgid "Run"
+#~ msgstr "Çalıştır"
+
+#~ msgid "Select unit state"
+#~ msgstr "Birim durumunu seçin"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Ölçümleri saklamayı etkinleştir"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Ölçümleri saklamayı etkinleştir"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "$0 içindeki parolayı zorla kaldır"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Eğer tang-show-keys kullanılabilir değilse, şunu çalıştır:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "tang-show-keys kullanılamıyorsa ne yapılsın?"
+
+#~ msgid "key slot $0"
+#~ msgstr "anahtar yuvası $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Bir şeyler ters gitti"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "Bu işe yaramadı, lütfen tekrar deneyin"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "Yönetimsel erişim elde edemezsiniz."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Otomatik güncellemeler ayarlanmadı"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "$0 CPU yapılandırması"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 Ağ"
+#~ msgstr[1] "$0 Ağ"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0, çoğu işletim sisteminde kullanılabilir. Yüklemek için GNOME "
+#~ "Yazılımı'nda arayın veya aşağıdaki komutu çalıştırın:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 bellek ayarlaması"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 ağ"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPU"
+#~ msgstr[1] "$0 vCPU"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 vCPU ayrıntıları"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 sanal ağ arayüzü ayarları"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "Birimleri yönetmek için depolama havuzunu etkinleştirin"
+
+#~ msgid "Add network interface"
+#~ msgstr "Ağ arayüzü ekle"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Sanal ağ arayüzü ekle"
+
+#~ msgid "Additional"
+#~ msgstr "Ek"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Adres alt ağ içinde değil"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr ""
+#~ "Anlık görüntüyü sildikten sonra, kaydedilen tüm içeriği kaybolacaktır."
+
+#~ msgid "Always attach"
+#~ msgstr "Her zaman bağla"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Bağlanması, bu diski kullanan her sanal makine için onu paylaşılabilir "
+#~ "hale getirecektir."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Önyüklemede libvirt'i otomatik olarak başlat"
+
+#~ msgid "Autostart"
+#~ msgstr "Otomatik başlat"
+
+#~ msgid "Boot order"
+#~ msgstr "Önyükleme sırası"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Önyükleme sırası ayarları kaydedilemedi"
+
+#~ msgid "Bus"
+#~ msgstr "Veri yolu"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "CD/DVD disk"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "CPU yapılandırması kaydedilemedi"
+
+#~ msgid "CPU type"
+#~ msgstr "CPU türü"
+
+#~ msgid "Change firmware"
+#~ msgstr "Aygıt yazılımını değiştir"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Değişiklikler, sanal makine kapatıldıktan sonra etkili olacaktır"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Bir işletim sistemi seçin"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "\"Uzak görüntüleyiciyi başlat\"a tıklamak bir .vv dosyası indirecek ve $0 "
+#~ "başlatılacak."
+
+#~ msgid "Clone"
+#~ msgstr "Kopya"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Görüntü yüklenemiyor"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Bu eylemi onayla"
+
+#~ msgid "Connect"
+#~ msgstr "Bağlan"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr ""
+#~ "Aşağıdaki protokoller için herhangi bir görüntüleyici uygulamasıyla bağlan"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Sanallaştırma hizmetine bağlanılıyor"
+
+#~ msgid "Connection"
+#~ msgstr "Bağlantı"
+
+#~ msgid "Console"
+#~ msgstr "Konsol"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Soket başına çekirdek sayısı"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Anlık görüntüye geri döndürülemedi"
+
+#~ msgid "Crashed"
+#~ msgstr "Çöktü"
+
+#~ msgid "Create VM"
+#~ msgstr "Sanal makine oluştur"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "$ 0'a dayalı bir kopya sanal makine oluştur"
+
+#~ msgid "Create new"
+#~ msgstr "Yeni oluştur"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Yeni sanal makine oluştur"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Sanal ağ oluştur"
+
+#~ msgid "Creating VM"
+#~ msgstr "Sanal makine oluşturuluyor"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "Sanal makine kurulumu oluşturuluyor"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "$0 sanal makinesini oluşturma başarısız oldu"
+
+#~ msgid "Creation time"
+#~ msgstr "Oluşturma zamanı"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Şu anki ayırma"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Özel aygıt yazılımı: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP aralığı"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "İlişkili depolama dosyalarını sil:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "$0 depolama havuzunu sil"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Bu havuzun içindeki birimleri sil"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Etkin olmayan bir depolama havuzunu silmek, sadece havuzun tanımını "
+#~ "kaldıracaktır. İçeriği silinmeyecektir."
+
+#~ msgid "Desktop viewer"
+#~ msgstr "Masaüstü görüntüleyicisi"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Silmeyi denemeden önce bu havuzu kullanan diskleri herhangi bir sanal "
+#~ "makineden ayırın."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Seri konsoldan bağlantı kesildi. Bağlan düğmesine tıklayın."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "$0 diski, $1 sanal makinesinden ayırma başarısız"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Diskin bağlanması başarısız oldu"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Diskin oluşturulması başarısız oldu"
+
+#~ msgid "Disk image file"
+#~ msgstr "Disk kalıp dosyası"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Disk ayarları kaydedilemedi"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "Sadece disk anlık görüntüsü"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "Etki alanı çöktü"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "Etki alanı, kaynakta engellendi"
+
+#~ msgid "Download an OS"
+#~ msgstr "Bir işletim sistemi indir"
+
+#~ msgid "Dying"
+#~ msgstr "Sonlanıyor"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Benzetilmiş makine"
+
+#~ msgid "End"
+#~ msgstr "Bitiş"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Bitiş boş olmamalıdır"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Anamakinenin dosya sisteminde varolan disk kalıbı"
+
+#~ msgid "Expand"
+#~ msgstr "Genişlet"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Aygıt yazılımını değiştirme başarısız oldu"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "$0 içinde bulunan arayüzlerin IP adreslerini alma başarısız oldu"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr ""
+#~ "Ctrl+Alt+$0 tuş birleşimini $1 sanal makinesine gönderme başarısız oldu"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr "En fazla sanal CPU sayısından daha azı etkinleştirilmelidir."
+
+#~ msgid "File"
+#~ msgstr "Dosya"
+
+#~ msgid "Filter by name"
+#~ msgstr "Ada göre süz"
+
+#~ msgid "Firmware"
+#~ msgstr "Aygıt yazılımı"
+
+#~ msgid "Force shut down"
+#~ msgstr "Kapatmaya zorla"
+
+#~ msgid "Forward mode"
+#~ msgstr "Yönlendirme kipi"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Yönlendirme kipi"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Otomatik olarak oluştur"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "Sanal makineler listesine git"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Ek seçenekleri gizle"
+
+#~ msgid "Host device"
+#~ msgstr "Anamakine aygıtı"
+
+#~ msgid "Host name"
+#~ msgstr "Anamakine adı"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Anamakine boş olmamalıdır"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Hipervizör ayrıntıları"
+
+#~ msgid "IP configuration"
+#~ msgstr "IP yapılandırması"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 ve IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4 ağı"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv4 ağı boş olmamalıdır"
+
+#~ msgid "IPv4 only"
+#~ msgstr "Sadece IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6 adresi"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6 ağı"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6 ağı boş olmamalıdır"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Sadece IPv6"
+
+#~ msgid "Idle"
+#~ msgstr "Boşta"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Sanal makineyi hemen başlat"
+
+#~ msgid "Import"
+#~ msgstr "İçe aktar"
+
+#~ msgid "Import VM"
+#~ msgstr "Sanal makineyi içe aktar"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "Bir sanal makineyi içe aktar"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "Çoğu yapılandırmada, anamakineden konuğa ağ iletişimi için macvtap "
+#~ "çalışmaz."
+
+#~ msgid "Initiator"
+#~ msgstr "Başlatıcı"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "Başlatıcı IQN boş olmamalıdır"
+
+#~ msgid "Installation source"
+#~ msgstr "Kurulum kaynağı"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Kurulum kaynağı boş olmamalıdır"
+
+#~ msgid "Installation type"
+#~ msgstr "Kurulum türü"
+
+#~ msgid "Interface type"
+#~ msgstr "Arayüz türü"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Geçersiz IPv4 maskesi veya ön ek uzunluğu"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Geçersiz IPv6 adresi"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Geçersiz IPv6 ön eki"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Geçersiz dosya adı"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "Uzak görüntüleyiciyi başlat"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr ""
+#~ "Bir root hesabının oluşturulmasını istemiyorsanız parolayı boş bırakın"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "Bir kullanıcı hesabının oluşturulmasını istemiyorsanız parolayı boş "
+#~ "bırakın"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr ""
+#~ "Bir root hesabının oluşturulmasını istemiyorsanız parolayı boş bırakın"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt, anamakinede yüklü herhangi bir UEFI/OVMF aygıt yazılımı kalıbı "
+#~ "algılamadı"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt veya hipervizör UEFI'yi desteklemiyor"
+
+#~ msgid "Loading resources"
+#~ msgstr "Kaynaklar yükleniyor"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "Veri yolu türünü değiştirmeden önce makine kapatılmak zorundadır"
+
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "Önbellek kipini değiştirmeden önce makine kapatılmak zorundadır"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Sanal makineleri yönetme"
+
+#~ msgid "Manual connection"
+#~ msgstr "Elle bağlantı"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Maske veya ön ek uzunluğu"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Maske veya ön ek uzunluğu boş olmamalıdır"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "En fazla ayırma"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "En fazla bellek kaydedilemedi"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr "Konuk işletim sistemi için ayrılmış en fazla sanal CPU sayısı"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "1 ile $0 arasında olmak zorunda olan konuk işletim sistemi için ayrılmış "
+#~ "en fazla sanal CPU sayısı"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "En fazla iletim birimi (MTU)"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Bellek kaydedilemedi"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "Bellek 0 olmamak zorundadır"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "Model türü"
+
+#~ msgid "NAT to $0"
+#~ msgstr "$0 ağına NAT yap"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "$1 sanal makinesinin $0 NIC'inin durumunu değiştirme başarısız oldu"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "Ad geçersiz karakterler içeriyor"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Ad boş olmamak zorundadır"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Ad boş olmamalıdır"
+
+#~ msgid "Name: "
+#~ msgstr "Ad: "
+
+#~ msgid "Netmask"
+#~ msgstr "Ağ maskesi"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "$0 ağının etkinleştirilmesi başarısız oldu"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "$0 ağının devre dışı bırakılması başarısız oldu"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Ağ üzerinden önyükleme (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Ağ dosya sistemi (NFS)"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Ağ arayüzü ayarları kaydedilemedi"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "Ağ seçimi PXE'yi desteklemiyor."
+
+#~ msgid "Networks"
+#~ msgstr "Ağlar"
+
+#~ msgid "New volume name"
+#~ msgstr "Yeni birim adı"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "Bu anamakinede çalışan veya tanımlı sanal makine yok"
+
+#~ msgid "No connection available"
+#~ msgstr "Mevcut bağlantı yok"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Bu sanal makine için tanımlanan diskler yok"
+
+#~ msgid "No network devices"
+#~ msgstr "Ağ aygıtları yok"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Bu sanal makine için tanımlanan ağ arayüzleri yok"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "Bu anamakinede tanımlı ağ yok"
+
+#~ msgid "No networks available"
+#~ msgstr "Kullanılabilir ağlar yok"
+
+#~ msgid "No parent"
+#~ msgstr "Üst öğe yok"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Bu sanal makine için tanımlanan anlık görüntüler yok"
+
+#~ msgid "No state"
+#~ msgstr "Durum yok"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "Bu anamakinede tanımlı depolama havuzu yok"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Kullanılabilir depolama havuzları yok"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "Bu depolama havuzu için tanımlı depolama birimleri yok"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Sanal ağlar yok"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "Kalıcı olmayan ağ silinemez. Devre dışı bırakıldığında varlığı sona erer."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "Kalıcı olmayan depolama havuzu silinemez. Devre dışı bırakıldığında "
+#~ "varlığı sona erer."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Yok (yalıtılmış ağ)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Seçilen bir veya daha fazla birim etki alanları tarafından kullanılıyor. "
+#~ "Birimi silmeye izin vermek için önce diskleri ayırın."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Sadece konuk kapatıldığında düzenlenebilir"
+
+#~ msgid "Open"
+#~ msgstr "Açık"
+
+#~ msgid "Operating system"
+#~ msgstr "İşletim sistemi"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "İşlem devam ediyor"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "Üst öğe anlık görüntüsü"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Anamakinenin dosya sistemi üzerindeki yol"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Anamakinenin dosya sistemi üzerindeki ISO dosyasının yolu"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Anamakinenin dosya sistemindeki dosyanın yolu"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Anamakinenin dosya sistemindeki dosyanın yolu"
+
+#~ msgid "Paused"
+#~ msgstr "Duraklatıldı"
+
+#~ msgid "Persistence"
+#~ msgstr "Kalıcılık"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Fiziksel disk aygıtı"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Anamakinedeki fiziksel disk aygıtı"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Lütfen bir depolama havuzu seçin"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "Lütfen bir birim seçin"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Lütfen yeni birim adını girin"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "Konsoluna erişmek için lütfen sanal makineyi başlatın."
+
+#~ msgid "Plug"
+#~ msgstr "Tak"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "Birim oluşturmak için havuzun etkin olması gerekir"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "Havuz türü birim oluşturmayı desteklemiyor"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "Havuzun birimleri sanal makineler tarafından kullanılıyor "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Konuğa açılacak tercih edilen soket sayısı."
+
+#~ msgid "Prefix"
+#~ msgstr "Ön ek"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "Ön ek uzunluğu boş olmamalıdır"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "Önceden kaydedilmiş anlık görüntüler, bir şeyler ters giderse daha önceki "
+#~ "bir duruma geri dönmenizi sağlar"
+
+#~ msgid "Product"
+#~ msgstr "Ürün"
+
+#~ msgid "Profile"
+#~ msgstr "Profil"
+
+#~ msgid "Protocol"
+#~ msgstr "Protokol"
+
+#~ msgid "Remote URL"
+#~ msgstr "Uzak URL"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "Uzak görüntüleyici ayrıntıları"
+
+#~ msgid "Revert"
+#~ msgstr "Geri döndür"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "$0 anlık görüntüsüne geri döndür"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "Bu anlık görüntüye geri dönmek, sanal makineyi anlık görüntünün "
+#~ "oluşturulduğu zamana geri götürecek ve anlık görüntüde kaydedilmeyen "
+#~ "verilerle birlikte şu anki durum kaybedilecektir"
+
+#~ msgid "Root password"
+#~ msgstr "Root parolası"
+
+#~ msgid "Route to $0"
+#~ msgstr "$0 ağına yönlendir"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Katılımsız kurulumu çalıştır"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Anamakine önyüklendiğinde çalıştır"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE TLS bağlantı noktası"
+
+#~ msgid "SPICE address"
+#~ msgstr "SPICE adresi"
+
+#~ msgid "SPICE port"
+#~ msgstr "SPICE bağlantı noktası"
+
+#~ msgid "Select console type"
+#~ msgstr "Konsol türünü seçin"
+
+#~ msgid "Send key"
+#~ msgstr "Tuş gönder"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Maskelenemez kesme (NMI) gönder"
+
+#~ msgid "Serial console"
+#~ msgstr "Seri konsol"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "DHCP aralığını ayarla"
+
+#~ msgid "Set manually"
+#~ msgstr "El ile ayarla"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "Katılımsız kurulum için kullanıcı parolalarının ayarlanması, sanal "
+#~ "makinenin oluşturulurken başlatılmasını gerektirir"
+
+#~ msgid "Show additional options"
+#~ msgstr "Ek seçenekleri göster"
+
+#~ msgid "Shut off"
+#~ msgstr "Kapat"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr ""
+#~ "Aygıt yazılımı yapılandırmasını düzenlemek için sanal makineyi kapatın"
+
+#~ msgid "Shutting down"
+#~ msgstr "Kapatılıyor"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Anlık görüntünün oluşturulması başarısız oldu"
+
+#~ msgid "Source format"
+#~ msgstr "Kaynak biçimi"
+
+#~ msgid "Source path"
+#~ msgstr "Kaynak yolu"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Kaynak yolu boş olmamalıdır"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "Kaynak http, ftp veya nfs protokolüyle başlamalıdır"
+
+#~ msgid "Source volume group"
+#~ msgstr "Kaynak birim grubu"
+
+#~ msgid "Start libvirt"
+#~ msgstr "libvirt hizmetini başlat"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Anamakine önyüklendiğinde havuzu başlat"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Başlangıç boş olmamalıdır"
+
+#~ msgid "Startup"
+#~ msgstr "Başlatma"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "$0 depolama havuzunun etkinleştirilmesi başarısız oldu"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "$0 depolama havuzunun devre dışı bırakılması başarısız oldu"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Depolama havuzunun oluşturulması başarısız oldu"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Depolama havuzu adı"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "Depolama boyutu 0 olmamak zorundadır"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr ""
+#~ "Depolama birimi boyutu, depolama havuzunun kapasitesini ($0 $1) aşmamak "
+#~ "zorundadır"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Depolama birimleri silinemedi"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "Askıya alındı (Güç Yönetimi)"
+
+#~ msgid "Target path"
+#~ msgstr "Hedef yolu"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Hedef yolu boş olmamalıdır"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr "Sanal makine çalışıyor ve silinmeden önce kapanmaya zorlanacaktır."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "Bu aygıtı ayırmak için sanal makinenin çalışıyor olması veya kapatılması "
+#~ "gerekir"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "Dışa aktarılan sunucu üzerindeki dizin"
+
+#~ msgid "The pool is empty"
+#~ msgstr "Havuz boş"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr "Seçilen işletim sistemi katılımsız kurulumu desteklemiyor"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr "Seçilen işletim sisteminin en az $0 $1 bellek gereksinimi var"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "Seçilen işletim sisteminin en az $0 $1 depolama alanı gereksinimi var"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "Depolama havuzu silinemedi"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr "Bu sanal makine geçicidir. Silmek istiyorsanız kapatın."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Bu birim zaten şunlar tarafından kullanılıyor: "
+
+#~ msgid "Threads per core"
+#~ msgstr "Çekirdek başına iş parçacığı sayısı"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr ""
+#~ "Geçici sanal makineler, aygıt yazılımı yapılandırmasını düzenlemeyi "
+#~ "desteklemiyor"
+
+#~ msgid "Type ID"
+#~ msgstr "Tür kimliği"
+
+#~ msgid "Unique name"
+#~ msgstr "Benzersiz ad"
+
+#~ msgid "Unique network name"
+#~ msgstr "Benzersiz ağ adı"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Bilinmeyen aygıt yazılımı"
+
+#~ msgid "Unplug"
+#~ msgstr "Çıkart"
+
+#~ msgid "Up to $0 $1 available on the default location"
+#~ msgstr "Varsayılan konumda $0 $1'a kadar kullanılabilir"
+
+#~ msgid "Up to $0 $1 available on the host"
+#~ msgstr "Anamakinede $0 $1'a kadar kullanılabilir"
+
+#~ msgid "Url"
+#~ msgstr "URL"
+
+#~ msgid "User login must not be empty when user password is set"
+#~ msgstr ""
+#~ "Kullanıcı parolası ayarlandığında kullanıcı oturum açma bilgileri boş "
+#~ "olmamak zorundadır"
+
+#~ msgid "User password must not be empty when user login is set"
+#~ msgstr ""
+#~ "Kullanıcı oturum açma bilgileri ayarlandığında kullanıcı parolası boş "
+#~ "olmamak zorundadır"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "VCPU ayarları kaydedilemedi"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "$0 sanal makinesi zaten var"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "$0 sanal makinesi $1 bağlantısında mevcut değil"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "$0 sanal makinesini zorla yeniden başlatma başarısız oldu"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "$0 sanal makinesini zorla kapatma başarısız oldu"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "$0 sanal makinesinin silinmesi başarısız oldu"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "$0 sanal makinesinin yüklenmesi başarısız oldu"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "$0 sanal makinesini yeniden başlatma başarısız oldu"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "$0 sanal makinesini devam ettirme başarısız oldu"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "$0 sanal makinesinin NMI göndermesi başarısız oldu"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "$0 sanal makinesini kapatma başarısız oldu"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "$0 sanal makinesi başlatılamadı"
+
+#~ msgid "VM state"
+#~ msgstr "Sanal Makine durumu"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC TLS bağlantı noktası"
+
+#~ msgid "VNC address"
+#~ msgstr "VNC adresi"
+
+#~ msgid "VNC console"
+#~ msgstr "VNC konsolu"
+
+#~ msgid "VNC port"
+#~ msgstr "VNC bağlantı noktası"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Sanal Makineler"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Sanal makineler"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Sanal makine yönetimi"
+
+#~ msgid "Virtual network"
+#~ msgstr "Sanal ağ"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Sanal ağ oluşturulması başarısız oldu"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Sanallaştırma hizmeti (libvirt) etkin değil"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Birim grubu adı boş olmamalıdır"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Yazılabilir"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Yazılabilir ve paylaşılan"
+
+#~ msgid "Yesterday"
+#~ msgstr "Dün"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "En yakından eşleşen işletim sistemini seçmeniz gerekli"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "etkisizleştirildi"
+
+#~ msgid "down"
+#~ msgstr "kapalı"
+
+#~ msgid "enabled"
+#~ msgstr "etkinleştirildi"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "anamakine aygıtı"
+
+#~ msgid "host passthrough"
+#~ msgstr "anamakine geçişi"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "iSCSI doğrudan hedefi"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "iSCSI başlatıcı IQN"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "iSCSI hedefi IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "hayır"
+
+#~ msgid "no state saved"
+#~ msgstr "kaydedilen durum yok"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "yönlendirilen aygıt"
+
+#~ msgid "server"
+#~ msgstr "sunucu"
+
+#~ msgid "up"
+#~ msgstr "açık"
+
+#~ msgid "vCPU count"
+#~ msgstr "vCPU sayısı"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "en fazla vCPU"
+
+#~ msgid "vCPUs"
+#~ msgstr "vCPU'lar"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#~ msgid "view more..."
+#~ msgstr "daha fazla göster..."
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Sanal makineleri çoğaltmak için sistemde virt-install paketinin "
+#~ "yüklenmesi gerekir"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Yeni sanal makineler oluşturmak için sistemde virt-install paketinin "
+#~ "yüklenmesi gerekir"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Bu özniteliği düzenlemek için sistemde virt-install paketinin yüklenmesi "
+#~ "gerekir"
+
+#~ msgid "vm"
+#~ msgstr "sanal makine"
+
+#~ msgid "Local install media"
+#~ msgstr "Yerel yükleme ortamı"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "Lütfen bir root parolası ayarlayın"
+
+#~ msgid "21st"
+#~ msgstr "21."
+
+#~ msgid "22nd"
+#~ msgstr "22."
+
+#~ msgid "23rd"
+#~ msgstr "23."
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "Sonra"
+
+#~ msgid "Friday"
+#~ msgstr "Cuma"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Saat : Dakika"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "Saatin 0 ile 23 arasında bir sayı olması gerekir"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Geçersiz tarih biçimi."
+
+#~ msgid "Invalid number."
+#~ msgstr "Geçersiz sayı."
+
+#~ msgid "Monday"
+#~ msgstr "Pazartesi"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Her saat tekrarla"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Her yıl tekrarla"
+
+#~ msgid "Saturday"
+#~ msgstr "Cumartesi"
+
+#~ msgid "Sunday"
+#~ msgstr "Pazar"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Bu gün, bütün aylarda mevcut değil.<br> Zamanlayıcı sadece 31. güne sahip "
+#~ "olan aylarda çalıştırılacaktır."
+
+#~ msgid "Thursday"
+#~ msgstr "Perşembe"
+
+#~ msgid "Tuesday"
+#~ msgstr "Salı"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 güncelleme"
+#~ msgstr[1] "$0 güncelleme"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 güvenlik düzeltmesi"
+#~ msgstr[1] "$1 güvenlik düzeltmesi"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Depolama aygıtlarını yönetme"
+
+#~ msgid "Restart now"
+#~ msgstr "Şimdi yeniden başlat"
+
+#~ msgid "Configure"
+#~ msgstr "Yapılandır"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr ""
+#~ "Ağ üzerinden önyükleme sadece sistem bağlantısı kullanılırken "
+#~ "kullanılabilir"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Ağ"
+
+#~ msgid "No snapshots"
+#~ msgstr "Anlık görüntüler yok"
+
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "'$0' dizininde bulunan böyle bir dosya yok"
+
+#~ msgid "No updates pending"
+#~ msgstr "Bekleyen güncellemeler yok"
+
+#~ msgid "This directory is empty"
+#~ msgstr "Bu dizin boş"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr "Bu havuz türü, depolama birimi oluşturmayı desteklemiyor"
+
+#~ msgid "undefined"
+#~ msgstr "tanımlanmadı"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Yanlış anamakine anahtarı"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}} anamakinesinin özgünlüğü belirlenemez. "
+#~ "Bağlanmaya devam etmek istediğinize emin misiniz?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}} anahtarı, daha önce kullanılan anahtarla "
+#~ "eşleşmiyor. Bu makine yakın zamanda değiştirilmediyse, muhtemelen birisi "
+#~ "bu makineyle olan bağlantınıza saldırmaya çalışıyor."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Bilinmeyen anamakine anahtarı"
+
+#~ msgid "Address:"
+#~ msgstr "Adres:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "En az $0 disk gerekli."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "Herhangi bir SPICE veya VNC görüntüleyici uygulamasıyla bağlan."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "MSI dosyasını $0 adresinden indir"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Grafik konsolu (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Masaüstü görüntüleyicide grafik konsolu"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Bu sanal makine için tanımlanan konsol yok."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid ""
+#~ "For the host, either specify the hostname, IP address, an alias name or a "
+#~ "unique resource identifier for the SSH destination."
+#~ msgstr ""
+#~ "Anamakine için ya anamakine adını, IP adresini, kod adını ya da SSH "
+#~ "hedefi için benzersiz bir kaynak tanımlayıcısı belirtin."
+
+#~ msgid ""
+#~ "Specify the host and the login user account for the host that you want to "
+#~ "add."
+#~ msgstr ""
+#~ "Eklemek istediğiniz anamakine için anamakineyi ve oturum açma kullanıcı "
+#~ "hesabını belirtin."
+
+#~ msgid "Entry"
+#~ msgstr "Giriş"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Tarayıcınızın bağlantısı kesilecek, ancak bu güncelleme işlemini "
+#~ "etkilemez. İlerlemeyi izlemeye devam etmek için birkaç saniye içinde "
+#~ "yeniden bağlanabilirsiniz."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "ve makineyi otomatik olarak yeniden başlat."
+
+#~ msgid "Dashboard"
+#~ msgstr "Panel"
+
+#~ msgid "Description input text"
+#~ msgstr "Açıklama girdisi metni"
+
+#~ msgid "FAILED"
+#~ msgstr "BAŞARISIZ OLDU"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "Bağlantı kayboldu. Yeniden bağlanmaya çalışılıyor"
+
+#~ msgid "Name input text"
+#~ msgstr "Ad girdi metni"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Ağ"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Lütfen yeni birim boyutunu girin"
+
+#~ msgid "Servers"
+#~ msgstr "Sunucular"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr "Şu anda doğrudan bu sunucuya bağlısınız. Bunu silemezsiniz."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 bit"
+#~ msgstr[1] "$0 bit"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 bayt"
+#~ msgstr[1] "$0 bayt"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 bayt"
+#~ msgstr[1] "$0 bayt"
+
+#~ msgid "Bond settings "
+#~ msgstr "Birleştirme ayarları "
+
+#~ msgid "IP settings "
+#~ msgstr "IP ayarları "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Cockpit beklenmeyen bir iç hatayla karşılaştı. <br/><br/>Tarayıcınızdaki "
+#~ "yenile düğmesine basarak Cockpit'i yeniden başlatmayı deneyebilirsiniz. "
+#~ "Bu hatayla ilgili ayrıntılar javascript konsolunda bulunur (çoğu "
+#~ "tarayıcıda <b>Ctrl-Shift-J</b>)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "Sanal makine çöktü."
+
+#~ msgid "The VM is down."
+#~ msgstr "Sanal makine kapalı."
+
+#~ msgid "The VM is going down."
+#~ msgstr "Sanal makine kapanıyor."
+
+#~ msgid "The VM is idle."
+#~ msgstr "Sanal makine boşta."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr "Sanal makine sonlanma aşamasında (kapatma veya çökme tamamlanmadı)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "Sanal makine duraklatıldı."
+
+#~ msgid "The VM is running."
+#~ msgstr "Sanal makine çalışıyor."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "Sanal makine, konuk güç yönetimi tarafından askıya alındı."
+
+#~ msgid "inactive"
+#~ msgstr "etkin değil"
+
+#~ msgid "Avatar"
+#~ msgstr "Avatar"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Bu makineye şu anda oturum açmış kullanıcı olarak bağlanmak için boş "
+#~ "bırakın. Farklı bir kullanıcı adı girerseniz, bu makineye bağlanırken her "
+#~ "zaman o kullanıcı kullanılacaktır."
+
+#~ msgid "2 min"
+#~ msgstr "2 dak"
+
+#~ msgid "3 min"
+#~ msgstr "3 dak"
+
+#~ msgid "4 min"
+#~ msgstr "4 dak"
+
+#~ msgid "CPU graph"
+#~ msgstr "CPU grafiği"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "CPU durumu"
+
+#~ msgid "Cached"
+#~ msgstr "Önbelleklendi"
+
+#~ msgid "Graphs"
+#~ msgstr "Grafikler"
+
+#~ msgid "I/O wait"
+#~ msgstr "G/Ç bekleme"
+
+#~ msgid "Kernel"
+#~ msgstr "Çekirdek"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Bellek"
+
+#~ msgid "Memory graph"
+#~ msgstr "Bellek grafiği"
+
+#~ msgid "Network traffic"
+#~ msgstr "Ağ trafiği"
+
+#~ msgid "Nice"
+#~ msgstr "Öncelik"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Kullanıcıları eşitle"
+
+#~ msgid "Usage graphs"
+#~ msgstr "Kullanım grafikleri"
+
+#~ msgid "View graphs"
+#~ msgstr "Grafikleri görüntüle"
+
+#~ msgid "idle"
+#~ msgstr "boşta"
+
+#~ msgid "paused"
+#~ msgstr "duraklatıldı"
+
+#~ msgid "running"
+#~ msgstr "çalışıyor"
+
+#~ msgid "shut off"
+#~ msgstr "kapatıldı"
+
+#~ msgid "shutdown"
+#~ msgstr "kapat"
+
+#~ msgid "Add a new host"
+#~ msgstr "Yeni bir anamakine ekle"
+
+#~ msgid "Available"
+#~ msgstr "Kullanılabilir"
+
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "IP adresi veya anamakine adını girin"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Ağ arayüzleri"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "Bu cockpit-ws sürümü, bir anamakineye alternatif bir kullanıcı veya "
+#~ "bağlantı noktası ile bağlanmayı desteklemiyor"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr ""
+#~ "Farklı bir bağlantı noktası denemek için cockpit-ws'yi daha yeni bir "
+#~ "sürüme yükseltmeniz gerekecek."
+
+#~ msgid "$0 template"
+#~ msgstr "$0 Taslak"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 paylaşım"
+
+#~ msgid "All In One"
+#~ msgstr "Hepsi Bir Arada"
+
+#~ msgid "Always"
+#~ msgstr "Her zaman"
+
+#~ msgid "Author"
+#~ msgstr "Yazar"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "passwd1 mekanizması için hatalı veri iletildi"
+
+#~ msgid "CPU priority"
+#~ msgstr "CPU önceliği"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Docker&rsquo;a bağlanılamıyor"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Kaynak sınırlarını değiştir"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Kaynak sınırlarını değiştir"
+
+#~ msgid "Command:"
+#~ msgstr "Komut:"
+
+#~ msgid "Commit"
+#~ msgstr "Kaydet (Commit)"
+
+#~ msgid "Commit image"
+#~ msgstr "Kalıbı Kaydet"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Docker'a bağlanılıyor"
+
+#~ msgid "Container name"
+#~ msgstr "Konteyner Adı"
+
+#~ msgid "Container is currently running."
+#~ msgstr "Konteyner şu an çalışıyor."
+
+#~ msgid "Container:"
+#~ msgstr "Konteyner:"
+
+#~ msgid "Containers"
+#~ msgstr "Konteynerler"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Konteynerler"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Kullanıcı grupları değiştirilemedi"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Kullanıcı parolası değiştirilemedi"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Makineye bağlanılamadı"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Yeni kullanıcılar oluşturulamadı"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Yerel kullanıcılar listelenemedi"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Kullanıcılar listelenemedi"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Kullanıcı verileri yüklenemedi"
+
+#~ msgid "Created:"
+#~ msgstr "Oluşturuldu:"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker sistemde kurulu ya da etkinleştirilmiş değil"
+
+#~ msgid "Environment"
+#~ msgstr "Ortam"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Docker'dan hata mesajı:"
+
+#~ msgid "Everything"
+#~ msgstr "Her şey"
+
+#~ msgid "Get new image"
+#~ msgstr "Yeni bir kalıp al"
+
+#~ msgid "IP address:"
+#~ msgstr "IP Adresi:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "IP Ön Ek Uzunluğu:"
+
+#~ msgid "Image"
+#~ msgstr "Kalıp"
+
+#~ msgid "Image $0"
+#~ msgstr "Kalıp $0"
+
+#~ msgid "Image:"
+#~ msgstr "Kalıp:"
+
+#~ msgid "Images"
+#~ msgstr "Kalıplar"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Kalıplar"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Kalıplar ve çalışan konteynerler"
+
+#~ msgid "Invalid port"
+#~ msgstr "Geçersiz bağlantı noktası"
+
+#~ msgid "Links:"
+#~ msgstr "Bağlantılar:"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC Adresi:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Bellek sınırı"
+
+#~ msgid "No containers"
+#~ msgstr "Konteyner yok"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Geçerli filtreyle eşleşen konteyner yok"
+
+#~ msgid "No images"
+#~ msgstr "Kalıp yok"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Geçerli filtreyle eşleşen kalıp yok"
+
+#~ msgid "No running containers"
+#~ msgstr "Çalışan konteyner yok"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "Geçerli filtreyle eşleşen çalışan konteyner yok"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Bu sistemde Docker'a erişim yetkisi yok"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Lütfen $0 için zorla silme işlemini onaylayın"
+
+#~ msgid "Ports:"
+#~ msgstr "Bağlantı noktaları:"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Salt-Okunur"
+
+#~ msgid "ReadWrite"
+#~ msgstr "Oku-Yaz"
+
+#~ msgid "Repository"
+#~ msgstr "Depo"
+
+#~ msgid "Start Docker"
+#~ msgstr "Docker'ı Başlat"
+
+#~ msgid "State:"
+#~ msgstr "Durum:"
+
+#~ msgid "Tag"
+#~ msgstr "Etiket"
+
+#~ msgid "Tags"
+#~ msgstr "Etiketler"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Filtrelemek için yazın…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "Durdurulmadığı Sürece"
+
+#~ msgid "Up since $0"
+#~ msgstr "$0'dan beri çalışıyor"
+
+#~ msgid "Used by containers"
+#~ msgstr "Konteynerler Tarafından Kullanılan"
+
+#~ msgid "Volumes"
+#~ msgstr "Birimler"
+
+#~ msgid "Volumes:"
+#~ msgstr "Birimler:"
+
+#~ msgid "With terminal"
+#~ msgstr "Terminal ile"
+
+#~ msgid "default"
+#~ msgstr "öntanımlı"
+
+#~ msgid "shares"
+#~ msgstr "paylaşımlar"
+
+#~ msgid "value"
+#~ msgstr "değer"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Geçerli filtreyle eşleşen kalıp yok"
+
+#~ msgid "Master"
+#~ msgstr "Ana"
+
+#~ msgid "Password for $0"
+#~ msgstr "$0 için parola"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Hesaplar"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "<b>$0</b> kullanıcısının hesapları değiştirmesine izin verilmiyor"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Root hesabı silinemedi"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Root hesabı yeniden adlandırılamadı"
+
+#~ msgid "Account Settings"
+#~ msgstr "Hesap Ayarları"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Gösterge Paneline Makine Ekle"
+
+#~ msgid "Next Run"
+#~ msgstr "Sonraki Yürütme"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Kokpit Hakkında"
diff --git a/po/uk.po b/po/uk.po
new file mode 100644
index 0000000..54392ec
--- /dev/null
+++ b/po/uk.po
@@ -0,0 +1,13122 @@
+# Yuri Chornoivan <yurchor@ukr.net>, 2015, 2020.
+# Yuri Chornoivan <yurchor@ukr.net>, 2015. #zanata, 2020.
+# Yuri Chornoivan <yurchor@ukr.net>, 2016. #zanata, 2020.
+# Yuri Chornoivan <yurchor@ukr.net>, 2017. #zanata, 2020.
+# Yuri Chornoivan <yurchor@ukr.net>, 2018. #zanata, 2020.
+# Yuri Chornoivan <yurchor@ukr.net>, 2019. #zanata, 2020.
+# Yuri Chornoivan <yurchor@ukr.net>, 2020. #zanata
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2023-12-25 13:35+0000\n"
+"Last-Translator: Weblate Translation Memory <noreply-mt-weblate-translation-"
+"memory@weblate.org>\n"
+"Language-Team: Ukrainian <https://translate.fedoraproject.org/projects/"
+"cockpit/main/uk/>\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.3\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "К-ть користувачів"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 процесор"
+msgstr[1] "$0 процесори"
+msgstr[2] "$0 процесорів"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 ГіБ"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 активна зона"
+msgstr[1] "$0 активні зони"
+msgstr[2] "$0 активних зон"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "Доступно $0"
+
+#: pkg/storaged/block/resize.jsx:266
+msgid "$0 can not be made larger"
+msgstr "$0 не можна збільшувати"
+
+#: pkg/storaged/block/resize.jsx:263
+msgid "$0 can not be made smaller"
+msgstr "$0 не можна зменшувати"
+
+#: pkg/storaged/block/resize.jsx:259
+msgid "$0 can not be resized"
+msgstr "Розмір $0 не можна змінювати"
+
+#: pkg/storaged/block/resize.jsx:255
+msgid "$0 can not be resized here"
+msgstr "Тут не можна змінювати розмір $0"
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "Розмір фрагмента $0"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 збіг із критичними"
+msgstr[1] "$0 збіги із критичними"
+msgstr[2] "$0 збігів із критичними"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "$0 даних + $1 додатково використано з $2 ($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 день"
+msgstr[1] "$0 дні"
+msgstr[2] "$0 днів"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "Не вистачає $0 диска"
+msgstr[1] "Не вистачає $0 дисків"
+msgstr[2] "Не вистачає $0 дисків"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "Не вистачає $0 диска"
+msgstr[1] "Не вистачає $0 дисків"
+msgstr[2] "Не вистачає $0 дисків"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "Диски $0"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "Документація з $0"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "Помилка $0"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "$0 завершено роботу з кодом $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "Помилка $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 помилкова спроба увійти"
+msgstr[1] "$0 помилкових спроби увійти"
+msgstr[2] "$0 помилкових спроб увійти"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "$0 filesystem"
+msgstr "Файлова система $0"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "Вільно $0"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 година"
+msgstr[1] "$0 години"
+msgstr[2] "$0 годин"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 збіг із важливими"
+msgstr[1] "$0 збіги із важливими"
+msgstr[2] "$0 збігів із важливими"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 є наявним файлом"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 використовується"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "$0 немає у жодному зі сховищ."
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "Змінено ключ $0"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "$0 завершено з сигналом $1"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 збіг із низькою важливістю"
+msgstr[1] "$0 збіги із низькою важливістю"
+msgstr[2] "$0 збігів із низькою важливістю"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 хвилина"
+msgstr[1] "$0 хвилини"
+msgstr[2] "$0 хвилин"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 збіг зі збігами середньої важливості"
+msgstr[1] "$0 збіги зі збігами середньої важливості"
+msgstr[2] "$0 збігів зі збігами середньої важливості"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 місяць"
+msgstr[1] "$0 місяці"
+msgstr[2] "$0 місяців"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "І ще $0…"
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 пакунок"
+msgstr[1] "$0 пакунки"
+msgstr[2] "$0 пакунків"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 пакунок потребує перезавантаження системи"
+msgstr[1] "$0 пакунки потребують перезавантаження системи"
+msgstr[2] "$0 пакунків потребують перезавантаження системи"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 сторінка"
+msgstr[1] "$0 сторінки"
+msgstr[2] "$0 сторінок"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+msgid "$0 partitions"
+msgstr "$0 розділів"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "Доступне $0 оновлення захисту"
+msgstr[1] "Доступні $0 оновлення захисту"
+msgstr[2] "Доступні $0 оновлень захисту"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "Критична помилка $0 служби"
+msgstr[1] "Критична помилка $0 служб"
+msgstr[2] "Критична помилка $0 служб"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "Слід перезапустити $0 службу"
+msgstr[1] "Слід перезапустити $0 служби"
+msgstr[2] "Слід перезапустити $0 служб"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "Лишився $0 слот"
+msgstr[1] "Лишилося $0 слоти"
+msgstr[2] "Лишилося $0 слотів"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 пік"
+msgstr[1] "$0 піки"
+msgstr[2] "$0 піків"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0 синхронізовано"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "Загалом $0"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "Доступне $0 оновлення"
+msgstr[1] "Доступні $0 оновлення"
+msgstr[2] "Доступні $0 оновлень"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "$0 використано з $1 ($2 заощаджено)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 тиждень"
+msgstr[1] "$0 тижні"
+msgstr[2] "$0 тижнів"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "Буде встановлено $0."
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 рік"
+msgstr[1] "$0 роки"
+msgstr[2] "$0 років"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "зона $0"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0: аварія у $1"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name (з $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+#, fuzzy
+#| msgid "Creating partition $target"
+msgid "(Not part of target)"
+msgstr "Створюємо розділ $target"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+#, fuzzy
+#| msgid "Local mount point"
+msgid "(no assigned mount point)"
+msgstr "Локальна точка монтування"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+msgid "(not mounted)"
+msgstr "(не змонтовано)"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(рекомендовано)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ", включно із $1 виправленням захисту"
+msgstr[1] ", включно із $1 виправленнями захисту"
+msgstr[2] ", включно із $1 виправленнями захисту"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 МіБ"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 день"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 година"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 хв."
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 хвилина"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 тиждень"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10-е"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11-е"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 КіБ"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12-е"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13-е"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14-е"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 хв."
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15-е"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 КіБ"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16-е"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17-е"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18-е"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19-е"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1-е"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 МіБ"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 хвилин"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20-е"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21-е"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22-е"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23-е"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24-е"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25-е"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26-е"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27-е"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28-е"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29-е"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2-е"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30-е"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31-е"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 КіБ"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3-є"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 КіБ"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 хвилин"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4-е"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 хв."
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 хвилин"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 КіБ"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5-е"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 годин"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 хвилин"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 КіБ"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 КіБ"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9-е"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "На $0 не встановлено сумісної версії Cockpit."
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "У цьому буфері вже існує файлова система із цією назвою."
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "Група з такою назвою вже існує"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr ""
+"Для забезпечення безпеки, надійної роботи та швидкодії слід встановити "
+"сучасну програму для перегляду інтернету."
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"Мережевий зв'язок поєднує декілька інтерфейсів мережі у один логічний "
+"інтерфейс із вищою пропускною здатністю або вищим рівнем резервування."
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"Буде створено новий ключ SSH у $0 для $1 на $2 і його буде додано до файла "
+"$3 $4 на $5."
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] ""
+"Для набуття оновленнями пакунків чинності слід перезапустити систему:"
+msgstr[1] ""
+"Для набуття оновленнями пакунків чинності слід перезапустити систему:"
+msgstr[2] ""
+"Для набуття оновленнями пакунків чинності слід перезапустити систему:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "Буфер із цією назвою вже існує."
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] "Для набуття оновленнями чинності слід перезапустити деякі служби:"
+msgstr[1] "Для набуття оновленнями чинності слід перезапустити деякі служби:"
+msgstr[2] "Для набуття оновленнями чинності слід перезапустити деякі служби:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "Групу томів із пропущеними фізичними томами не можна перейменовувати."
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "Стеження за ARP"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "Луна-імпульс ARP"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "Про «Вебконсоль»"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "Відсутній"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "Прийняти ключ і увійти"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "Прийнятний пароль"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "Строк дії облікового запису"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "Обліковий запис недоступний або його не можна редагувати."
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "Облікові записи"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "Дія"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "Дії"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "Задіяти"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr "Активуйте до зміни розмірів"
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "Активуємо $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "Активний"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "Активне резервування"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "Активні сторінки"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "Активний з "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "Активний стан"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "Адаптивне урівноваження навантаження"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "Адаптивне урівноваження навантаження за передаванням"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "Додати"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "Додати $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "Додати сервер DNS"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "Додати шифрування диска з мережевою прив'язкоюю"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "Додати сервер ключів Tang"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "Додати VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "Додати VPN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "Додати WireGuard VPN"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "Додати бітову карту"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "Додати нову зону"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "Додати адресу"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "Додати блокові пристрої"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "Додати зв’язок"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "Додати місток"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "Додати диск"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "Додати диски"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "Додати портал iSCSI"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "Додати ключ"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "Додати сервер ключів"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "Додати учасника"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "Додати новий вузол"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "Додати нову зону"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "Додати пароль"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "Додати вузол"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+msgid "Add physical volume"
+msgstr "Додати фізичний том"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "Додати порти"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "Додати порти до зони $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "Додати відкритий ключ"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "Додати маршрут"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "Додати домен пошуку"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "Додавання служб"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "Додати служби до зони $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "Додати служби до зони $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "Додати команду"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "Додати зону"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "Додавання «$0» параметрів шифрування"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "Додавання «$0» до параметрів файлової системи"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Додавання $0 призведе до розірвання з’єднання із сервером і зробить "
+"адміністративний інтерфейс користувача недоступним."
+
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr ""
+"Додавання сервера ключів потребує розблокування буфера. Будь ласка, надайте "
+"пароль до наявного буфера."
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr ""
+"Додавання нетипових портів призведе до перезавантаження firewalld. Це "
+"перезавантаження спричинить втрату усіх налаштувань окремого сеансу роботи!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "Додавання ключа"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "Додаємо фізичний том до $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "Додавання rd.neednet=1 до командного рядка ядра"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "Додатковий DNS $val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "Додаткові домени пошуку DNS $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "Додаткові дії"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "Додаткова адреса $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "Додаткові пакунки:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "Додаткові порти"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "Адреса"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "Адреса $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "Адреса не може бути порожньою"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "Адреса є некоректною"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "Адреси"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "Адреси не форматовано належним чином"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "Адміністрування за допомогою вебконсолі Cockpit"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "Адміністративний доступ"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr ""
+"Для створення звітів і доступу до звітів потрібен адміністративний доступ."
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "Потрібен адміністративний доступ"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "Розширене TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "Після"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"Після полишення цього домену лише користувачі із локальними реєстраційними "
+"даними зможуть входити до системи на цій машині. Це також може вплинути на "
+"роботу інших служб, оскільки можуть змінитися параметри визначення адрес DNS "
+"та список довірених служб сертифікації."
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "Після завантаження системи"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "Попередження"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "Тривога і вище"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "Псевдонім"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "Всі"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr "Для вибраного компонування потрібні усі $0 вибрані фізичні томи."
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "Усі оновлення"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "Усе в одному"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "Дозволити запуск (зняти маску)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "Дозволені IP-адреси"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "Дозволені адреси"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "пройдіть процес відвідування синагоги $0"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Документація з ролей Ansible"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"Будь-який текстовий рядок у журналі можна фільтрувати. Рядок може бути також "
+"записано у формі формального виразу. Крім того, передбачено підтримку "
+"фільтрування за полями повідомлення журналу. Поля слід вказувати у формі "
+"відокремлених комами записів ПОЛЕ=ЗНАЧЕННЯ, де значенням є список "
+"відокремлених комами можливих значень."
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "Вигляд"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "Немає даних щодо програми"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "Програми"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "Список програм"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "Застосувати і перезавантажити"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "Застосувати інтерактивні латки до ядра"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "Застосувати це рішення"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "Застосовуємо нові правила… Застосування може тривати декілька хвилин."
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "Застосовуємо рішення…"
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "Застосовуємо оновлення"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "Не вдалося застосувати оновлення"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "Придатне для критичних монтувань, зокрема /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "Програми"
+
+#: pkg/storaged/drive/drive.jsx:102
+msgid "Assessment"
+msgstr "Оцінювання"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "Мітка активу"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "При завантаженні"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "Потрібен принаймні $0 диск."
+msgstr[1] "Потрібно принаймні $0 диски."
+msgstr[2] "Потрібно принаймні $0 дисків."
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "Потрібен принаймні один блоковий пристрій."
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "Потрібен принаймні один диск."
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "За хвилину"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "У секунду"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "У вказаний момент часу"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "Атрибути"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "Журнал інспектування"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "Розпізнавання"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "Автентифікація"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "Розпізнавання"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "Не вдалось пройти розпізнавання"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "Не вдалося пройти розпізнавання: з’єднання розірвано сервером"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr ""
+"Щоб отримати доступ до виконання привілейованих завдань за допомогою "
+"вебконсолі Cockpit, слід пройти розпізнавання"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "Слід пройти розпізнавання"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "Уповноважити ключ SSH"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "Уповноважені відкриті ключі SSH"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "Автоматично"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "Автоматично (лише DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "Автоматичний вхід"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "Автоматичні оновлення"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "Запускати автоматично"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "Автоматично на основі NTP"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "Автоматично за допомогою додаткових серверів NTP"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "Автоматично за допомогою певних серверів NTP"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "Скрипт автоматизації"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "Доступні призначення на $0"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "Доступні оновлення"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+#, fuzzy
+#| msgid "Grow partition"
+msgid "BIOS boot partition"
+msgstr "Збільшити розділ"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "Дата BIOS"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "Версія BIOS"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "Назад до облікових записів"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "Помилковий"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "Помилковий параметр"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "Балансир"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "До"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "Пов'язано з"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "Чорний"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "Blade"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "Обгортка Blade"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "Блоковий пристрій"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "Блоковий пристрій для файлових систем"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "Блокові пристрої"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "Заблоковано"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "Прив’язка"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "Завантаження"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr ""
+"Завантаження зазнає невдачі, якщо не монтується ця файлова система, "
+"заважаючи віддаленому доступу"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "Завантаження буде успішним, якщо файлову систему не змонтовано"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "Пов'язано"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "Міст"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "Порт містка"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "Параметри порту містка"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "Трансляція"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "Помилки у налаштуваннях"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr ""
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "Доступні оновлення із виправленнями вад"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "Вади"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "Апаратний блок розширення каналу"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"Після зміни пароля до ключа SSH $0 на пароль для входу до системи $1 на $2 "
+"доступ до ключа буде автоматично відкрито і ви зможете надалі входити до $3 "
+"без пароля."
+
+#: pkg/static/login.html:61
+#, fuzzy
+#| msgid " Bypass browser check "
+msgid "Bypass browser check"
+msgstr " Обійти перевірку браузера "
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "Процесор"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "Захист процесора"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "Перемикачі захисту процесора"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "Використання процесора"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "Використання/Завантаження процесора"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "Кеш"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr ""
+"Це може бути назва вузла, IP-адреса, альтернативна назва або адреса ssh://"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr "Не вдалося знайти журналів на основі поточної комбінації фільтрів."
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "Скасувати"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "Скасувати вимикання"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "Скасувати перезавантаження"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "Не може бути увімкнено"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "Не вдалося з'єднатися зі невідомим вузлом"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "Не вдалося переспрямувати реєстраційні дані для входу"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr "Неможливо долучитися до домену, оскільки у цій системі немає realmd"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "Не можна планувати подію на минуле"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "Місткість"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "Носій сигналу"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "Змінити"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "Змінити правила шифрування"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "Змінити назву вузла"
+
+#: pkg/storaged/overview/overview.jsx:152
+msgid "Change iSCSI initiater name"
+msgstr "Змінити назву ініціатора iSCSI"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "Змінити назву ініціатора iSCSI"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+#, fuzzy
+#| msgid "Change shell"
+msgid "Change label"
+msgstr "Змінити оболонку"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "Змінити пароль"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "Змінити пароль"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "Зміна профілю швидкодії"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "Змінити профіль"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "Змінити оболонку"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "Змінити системний час"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "Змінити пароль до $0"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "Змінити параметри"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"Зміна ключів часто є результатом перевстановлення операційної системи. Втім, "
+"неочікувана зміна може вказувати на сторонню спробу перехопити дані вашого "
+"з'єднання."
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr ""
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr ""
+"Зміна параметрів призведе до розірвання з’єднання із сервером і зробить "
+"адміністративний інтерфейс користувача недоступним."
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "Перевірити наявність оновлень"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr ""
+"Перевірте, чи збігається хеш SHA-256 або SHA-1, який отримано за допомогою "
+"команди, з наведеним у цьому вікні."
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "Перевірити хеш ключа за допомогою сервера Tang."
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "Перевіряємо $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "Перевіряємо IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+msgid "Checking MDRAID device $target"
+msgstr "Перевіряємо пристрій MDRAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+msgid "Checking and repairing MDRAID device $target"
+msgstr "Перевіряємо і відновлюємо пристрій MDRAID $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "Перевірка на пакунок $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "Перевіряємо на підтримку NBDE у initrd"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "Шукаємо нові програми"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "Шукаємо оновлення пакунків…"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "Перевіряємо встановлене програмне забезпечення"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "Перевіряємо пов'язані процеси"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "Перевіряємо стан програмного забезпечення"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "Виберіть мову перекладу, яку буде використано для інтерфейсу програми"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "Розмір фрагмента"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "Клас"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "Спорожнюємо для $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "Вилучити «Не вдалося запустити»"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "Зняти усі фільтри"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "Очистити критерій пошуку"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "Пристрій Cleartext"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "Клієнтське програмне забезпечення"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "Закрити"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "Закрити позначені сторінки"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Розпізнавання у Cockpit налаштовано з помилками."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "Налаштування Cockpit для NetworkManager і Firewalld"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit не вдалося встановити зв’язок із вказаним вузлом."
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "У Cockpit сталася неочікувана внутрішня помилка."
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit — програма для керування сервером, яка полегшує адміністрування "
+"ваших серверів під керуванням Linux за допомогою програми для перегляду "
+"сторінок інтернету. Ви зможете одночасно використовувати термінал і "
+"вебінструмент. Службу, яку було запущено за допомогою Cockpit, можна "
+"зупинити за допомогою термінала. І навпаки, якщо трапиться помилка у "
+"терміналі, ви побачите її у інтерфейсі журналу Cockpit."
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit — інтерактивний інтерфейс адміністратора сервера Linux."
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit є несумісним із програмним забезпеченням цієї системи."
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit не встановлено"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit у цій системі не встановлено."
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit — чудовий інструмент для системних адміністраторів-початківців. За "
+"його допомогою вони без проблем впораються із простими завданнями, зокрема "
+"адмініструванням сховищ даних, інспектуванням журналів та запуском і "
+"зупиненням служб. Ви зможете одночасно стежити за роботою декількох серверів "
+"і адмініструвати ці сервери. Просто додайте їх одним клацанням кнопкою миші "
+"і ваш комп’ютер сам нагляне за своїми приятелями."
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Можливо, Cockpit не буде показано належним чином у вашому браузері"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "Збирати і пакувати діагностичні дані і дані щодо підтримки"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "Збирати дампи аварій ядра"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "Зібрати метрику"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "Колір"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "Можна використовувати відокремлені комами порти, діапазони і служби"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "Команда"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "Команду не знайдено"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "Коментар"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "Не вдалося обмінятися даними з tuned"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "Компактний PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "Сумісний із усіма системами та пристроями (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "Сумісний зі сучасними системами та жорсткими дисками > 2 ТБ (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "Стиснути дампи аварійних завершень для заощадження місця"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "Стискання"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "Умову $0=$1 не виконано"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "Не виконано умову"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "Налаштування"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "Налаштовування"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "Налаштовуємо IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "Налаштовування kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "Налаштовування параметрів системи"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "Підтвердити"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "Підтвердьте вилучення $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "Підтвердження пароля до ключа"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "Підтвердження нового пароля до ключа"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "Підтвердження нового пароля"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "Підтвердження пароля"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "Підтвердження вилучення $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "Підтвердьте вилучення введенням альтернативного пароля"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "Підтвердьте зупинку $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "Конфліктує за"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "Конфліктує"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "З’єднуватися автоматично"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "З’єднатися з"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "З’єднатися з:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "Встановлюємо з’єднання із фоновою службою SETroubleshoot…"
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "Не вдалося встановити з'єднання із dbus: $0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "Встановлюємо з’єднання із комп’ютером"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "Помилка з'єднання"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "Не вдалося встановити з'єднання"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "Вичерпано час очікування на з’єднання."
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "З’єднання буде розірвано"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "Складається з"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "Пов'язаний домен"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "Містить:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "Продовжити"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "Продовжити сеанс"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "Керування"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "Ctrl"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "Контролер"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "Змінюваний"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "Скопійовано"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "Копіювати"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "Копіювати до буфера"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "Ядро $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "Не вдалося встановити зв’язок із $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "Розташування дампу аварійного завершення"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "Звітування щодо аварій"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "Аварійно завершити роботу системи"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "Створити"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "Створити групу томів LVM2"
+
+#: pkg/storaged/overview/overview.jsx:145
+msgid "Create MDRAID device"
+msgstr "Створити пристрій MDRAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "Створити пристрій RAID"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "Створити буфер Stratis"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "Створити ключ SSH і уповноважити його"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "Створити знімок файлової системи $0"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "Створити обліковий запис із повторюваним UID"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "Створити обліковий запис із простим паролем"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "Створення і зміна прав власності на домашній каталог"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "Створити і змонтувати"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Create and mount"
+msgid "Create and start"
+msgstr "Створити і змонтувати"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "Створити файлову систему"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "Створити"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "Створити логічний том"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "Створити новий рахунок"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "Створити файлову систему"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "Створити групу"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "Створити логічний том"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "Створити файл завдання із цим вмістом."
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+msgid "Create new thinly provisioned logical volume"
+msgstr "Створити логічний том з тонким прогнозуванням"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "Лише створити"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "Створити розділ"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "Створити розділ на $0"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "Створити таблицю розділів"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "Створення знімка"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "Створити знімок і змонтувати"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "Лише створити знімок"
+
+#: pkg/storaged/overview/overview.jsx:174
+msgid "Create storage device"
+msgstr "Створити пристрій зберігання даних"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+#, fuzzy
+#| msgid "Create volume"
+msgid "Create subvolume"
+msgstr "Створити том"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "Створити тонкий том"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "Створити таймер"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "Створити групу томів"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "Створено"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "Створюємо групу томів LVM2 $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+msgid "Creating MDRAID device $target"
+msgstr "Створення пристрою MDRAID $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "Створення пристрою VDO"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "Створюємо файлову систему на $target"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "Створюємо логічний том $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "Створюємо розділ $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "Створюємо знімок $target"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Створення цієї $0 призведе до розірвання з’єднання із сервером і зробить "
+"адміністративний інтерфейс користувача недоступним."
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Критичні і вище"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"«Правила шифрування» (Crypto Policies) — компонент системи, який налаштовує "
+"криптографічні підсистеми, використовуючи протоколи TLS, IPSec, SSH, DNSSec "
+"і Kerberos."
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "Правило шифрування"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "Правила шифрування є несумісними"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl-Shift-I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "Поточне завантаження"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "Поточний рейтинг використання процесора"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "Зараз використовується"
+
+#: pkg/kdump/kdump-view.jsx:535
+#, fuzzy
+#| msgid "Currently in use"
+msgid "Currently not supported"
+msgstr "Зараз використовується"
+
+#: pkg/storaged/partitions/partition.jsx:146
+#, fuzzy
+#| msgid "custom"
+msgid "Custom"
+msgstr "нетиповий"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "Нетипове правило шифрування"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "Нетипові параметри монтування"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "Нетипові порти"
+
+#: pkg/storaged/partitions/partition.jsx:180
+#, fuzzy
+#| msgid "Custom path"
+msgid "Custom type"
+msgstr "Нетиповий шлях"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "Нетипові зони"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "DEFAULT із дозволеною перевіркою підпису SHA-1."
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "Домени пошуку DNS"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "Домени пошуку DNS $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "Щодня"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "Попередження про небезпеку:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "Темний"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "Дані"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "Використано даних"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"Дані зберігатимуться як дві копії і також у інший спосіб на вибраних "
+"фізичних томах для підвищення надійності та швидкодії. Має бути вибрано "
+"принаймні чотири томи."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"Дані зберігатимуться як дві або декілька копій на вибраних фізичних томах "
+"для підвищення надійності. Має бути вибрано принаймні два томи."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr ""
+"Дані будуть зберігатися на вибраних фізичних томах у інший спосіб для "
+"підвищення швидкодії. Має бути вибрано принаймні два томи."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"Дані буде збережено на вибраних фізичних томах, один з яких може бути "
+"втрачено без втрати даних. Має бути вибрано принаймні три томи."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+"Дані буде збережено на вибраних фізичних томах, один з яких може бути "
+"втрачено без втрати даних. Дані також буде збережено в інший спосіб для "
+"підвищення швидкодії. Має бути вибрано принаймні три томи."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+"Дані буде збережено на вибраних фізичних томах, до двох з яких може бути "
+"одночасно втрачено без втрати даних. Дані також має бути збережено в інший "
+"спосіб для підвищення швидкодії. Має бути вибрано принаймні п'ять томів."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr ""
+"Дані буде збережено на вибраних фізичних томах без будь-якої надлишковості "
+"або підвищення швидкодії."
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"Дату слід вказувати у форматі РРРР-ММ-ДД гг:хх:сс. Крім того, можна "
+"використовувати рядки «yesterday» («вчора»), «today» («сьогодні»), "
+"«tomorrow» («завтра»). Рядок «now» відповідає поточному моменту часу. "
+"Нарешті, можна вказувати відносний час за допомогою префіксів «-» і «+»"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "Вимкнути"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+msgid "Deactivate logical volume $0/$1?"
+msgstr "Деактивувати логічний том $0/$1?"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "Деактивація"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "Деактивуємо $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Діагностика і вище"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "Зменшити на одиницю"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "Спеціальна парність (RAID 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "Скасування дублювання"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "Типовий"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "Затримка"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "Значенням затримки має бути число"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "Вилучити"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "Вилучити $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "Вилучити обліковий запис"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "Вилучити файли"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "Вилучити групу"
+
+#: pkg/storaged/stratis/pool.jsx:292
+msgid "Delete pool"
+msgstr "Вилучити резерв"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "Вилучити звіт остаточно?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Вилучення $0 призведе до розірвання з’єднання із сервером і зробить "
+"адміністративний інтерфейс користувача недоступним."
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "Вилучаємо $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "Вилучаємо групу томів LVM2 $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr ""
+"Вилучення буфера Stratis призведе до витирання усіх даних, що на ньому "
+"зберігаються."
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr ""
+"Вилучення файлової системи призведе до вилучення усіх даних, що на ньому "
+"зберігаються."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr ""
+"Вилучення логічного тому призведе до витирання усіх даних, що на ньому "
+"зберігаються."
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr ""
+"Вилучення розділу призведе до вилучення усіх даних, що на ньому зберігаються."
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "Вилучення призведе до витирання усіх даних на пристрої RAID."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "Вилучення призведе до витирання усіх даних на пристрої VDO."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "Вилучення призведе до витирання усіх даних на групі томів."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+#, fuzzy
+#| msgid "Deleting erases all data on a volume group."
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "Вилучення призведе до витирання усіх даних на групі томів."
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "У результаті вилучення буде усунено такі файли:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "Опис"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "Робоча станція"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "Змінний"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "Подробиці"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "Розробка"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "Пристрій"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "Файл пристрою"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "Пристрій придатний лише для читання"
+
+#: pkg/storaged/block/other.jsx:60
+msgid "Device number"
+msgstr "Номер пристрою"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "Діагностичні звіти"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "Каталог"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "Каталог $0 є непридатним до запису або не існує."
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "Вимкнути багатопотоковість"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "Вимкнути брандмауер"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "Вимкнути tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "Вимкнено"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "Заборонити інтерактивний пароль"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "Заборонити розпізнавання за паролем"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "Заборонити запуск (замаскувати)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "Від’єднатися"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "Роз'єднано"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "Дисковий ввід/вивід"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "З диском усе гаразд"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "Диск виходить з ладу"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "Пароль до диска"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "Диски"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "Використання дисків"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "Відкинути"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "Відкинути $0 попередження"
+msgstr[1] "Відкинути $0 попередження"
+msgstr[2] "Відкинути $0 попереджень"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "Відкинути позначені попередження"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "Мова перекладу"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "Розподілена парність (RAID 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "Не монтувати"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "Не монтувати автоматично під час завантаження"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "Станція заряджання"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "Не запускається автоматично"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "Не монтується під час завантаження"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "Домен"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "Домен"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "Адреса домену"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "Ім’я адміністратора домену"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "Пароль адміністратора домену"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "Не вдалося встановити зв’язок із доменом"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "Підтримки домену не передбачено"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "Не повторювати"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "Подвійна розподілена парність (RAID 6)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "Отримати"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "Отримайте новий браузер безкоштовно"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "Отримано"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "Отримуємо"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "Отримуємо $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "Диск"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "Подвійний ранг"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+#, fuzzy
+#| msgid "Extended partition"
+msgid "EFI system partition"
+msgstr "Розширений розділ"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "Змінити"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "Редагувати /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "Редагувати сервер ключів Tang"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "Редагувати параметри VLAN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "Редагувати WireGuard VPN"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "Редагувати параметри прив'язки"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "Редагувати параметри містка"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "Редагувати нетипову службу у зоні $0"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "Редагувати вузол"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "Редагувати вузли"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "Редагувати motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "Точка монтування"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "Редагування правил і зон"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "Змінити службу"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "Змінити службу $0"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "Редагувати параметри команди"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "Змінити запис користувача"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "Редагування ключа потребує вільного слоту"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "Видобуваємо $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "Вбудований ПК"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "Порожній"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "Спорожнити"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "Порожній пароль"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "Спорожняємо $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "Увімкнути"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "Увімкнути або вимкнути пристрій"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "Увімкнути службу"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "Увімкнути брандмауер"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "Увімкнено"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "Вмикаємо $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "Зашифрувати дані"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "Зашифрувати дані за допомогою сервера ключів Tang"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "Зашифрувати дані за допомогою пароля шифрування"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "Зашифрований"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "Зашифрований $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+msgid "Encrypted Stratis pool"
+msgstr "Шифрований буфер Stratis $0"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "Зашифрований логічний том $0"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "Шифрований розділ $0"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "Шифрування"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "Параметри шифрування"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "Пароль шифрування"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "Тип шифрування"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "Завершено"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "Кінцева точка"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr ""
+"Слід вказати кінцеву точку, яка працюватиме як «сервер», у форматі вузол:"
+"порт, інакше, можна не заповнювати."
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "Примусово"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "Доступні оновлення із поліпшеннями"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "Введіть коректну MAC-адресу"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "Уся підмережа"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "Запис у $0"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "Витираємо $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "Відомі помилки"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "Помилка"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Помилка і вище"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "Сталася помилка"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "Помилка під час спроби встановити $0: PackageKit не встановлено"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "Повідомлення про помилку"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "Помилка під час виконання semanage для виявлення змін у системі"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "Помилка під час спроби зберегти уповноважені ключі: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "Помилка під час спроби встановити режим SELinux: «$0»"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "MAC Ethernet"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "MTU Ethernet"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "Має бути вибрано точно $0 фізичних томів"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr ""
+"Має бути вибрано точно $0 фізичних ктомів по одному на кожну смугу логічного "
+"тому."
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "Приклад: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "Приклад: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "Чудовий пароль"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "Апаратний блок розширення"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "Завершити строк дії облікового запису"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "Завершити строк дії облікового запису $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "Експорт"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "Експортувати до мережі"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "Розширена інформація"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "Розширений розділ"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS не увімкнено належним чином"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "FIPS із подальшими обмеженнями загальних критеріїв."
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "Помилка"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "Не вдалося додати комп’ютер: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "Не вдалося додати порт"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "Не вдалося додати службу"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "Не вдалося додати зону"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "Не вдалося змінити пароль"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "Не вдалося налаштувати PCP"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "Не вдалося вилучити нагадування: $0"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "Не вдалося вимкнути tuned"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "Не вдалося вимкнути профіль tuned"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "Не вдалося змінити запис комп’ютера: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "Не вдалося змінити службу"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "Не вдалося увімкнути $0 у firewalld"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "Не вдалося увімкнути tuned"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "Не вдалося отримати журнал"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "Не вдалося завантажити уповноважені ключі."
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "Не вдалося завантажити модуль"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"Не вдалося обробити файли модулів для dnf-automatic.timer або dnf-automatic-"
+"install.timer. Будь ласка, вилучіть нетипові перевизначення, щоб налаштувати "
+"автоматичні оновлення."
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "Не вдалося перезапустити службу"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "Не вдалося зберегти зміни до /etc/motd"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "Не вдалося зберегти параметри"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "Не вдалося запустити"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "Не вдалося перемкнути профіль"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "Стан файла"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "Файлова система"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "Файлову систему заблоковано"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "Назва файлової системи"
+
+#: pkg/storaged/dialog.jsx:1114
+#, fuzzy
+#| msgid "Creating filesystem on $target"
+msgid "Filesystem outside the target"
+msgstr "Створюємо файлову систему на $target"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "Під цією точкою монтування вже змонтовано файлові системи."
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "Фільтрувати за назвою або описом"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "Фільтрувати пункти меню"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "Фільтрувати служби"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "Фільтри"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "Відбиток"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "Брандмауер"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "Брандмауер недоступний"
+
+#: pkg/storaged/drive/drive.jsx:122
+msgid "Firmware version"
+msgstr "Версія мікропрограми"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "Виправити підтримку NBDE"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "Розмір шрифту"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "Заборонено запускати"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "Примусова зміна"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "Примусове вилучення"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "Примусова зміна пароля"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "Формат"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "Форматувати $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "Форматувати і змонтувати"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+#, fuzzy
+#| msgid "Format and mount"
+msgid "Format and start"
+msgstr "Форматувати і змонтувати"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "Лише форматувати"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr ""
+"Форматування призведе до знищення усіх даних на пристрої для зберігання "
+"даних."
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "Затримка переспрямування $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "Кількість кадрів"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "Вільне місце"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "Пошук із довільним критерієм"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "П'ятниці"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "Від"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "Повне ім'я"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "Шлюз"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "Загальний"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "Створений"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "Перейти до $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "Перейти до програми"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "Перейти зараз"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "Видимість графіка"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "Меню параметрів видимості графіка"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "Група"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "Назва групи"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "Групи"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "Збільшити"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "Збільшити вміст"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "Збільшити логічний розмір $0"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "Збільшити логічний том"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "Збільшити розділ"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "Збільшити буфер так, щоб використати усе місце"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "Збільшити так, щоб використати усе місце"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "Режим початкової зони"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "Режим початкової зони (hairpin)"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "Кишеньковий пристрій"
+
+#: pkg/storaged/drive/drive.jsx:64
+msgid "Hard Disk Drive"
+msgstr "Жорсткий диск"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "Дані щодо обладнання"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "Здоров'я"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Час вітання $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "Довідка"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "Приховати підтвердження пароля"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "Приховати пароль"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "Ідентифікатор ієрархії"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr ""
+"Ширші можливості взаємодії за рахунок збільшення простору для можливих атак."
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "Кількість пакунків у журналі"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "Домашній каталог"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "Вузол"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "Ключ вузла є неправильним"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "Назва вузла"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "Вузли"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "Щогодини"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "Години"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "Як перевірити"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "Ід."
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "Ід. $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+"ВНУТРІШНЯ ПОМИЛКА — цей логічний том позначено як активний, він повинен мати "
+"пов'язаний блоковий пристрій. Втім, знайти такий блоковий пристрій не "
+"вдалося."
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP-адреса"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"IP-адреса із префіксом маршрутизації. Записи значень слід відокремлювати "
+"комами. Приклад: 192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "Адреси IPv4"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "Параметри IPv4"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "Параметри IPv6"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "Ідентифікатор"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr ""
+"Якщо не заповнювати, ідентифікатор буде створено на основі пов'язаних служб "
+"портів та номерів портів"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"Якщо відбиток є відповідним, натисніть «Прийняти ключ і увійти». Якщо ж це "
+"не так, не входьте і повідомте про подію адміністратору."
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"Якщо відбиток є відповідним, натисніть «Довіряти і додати вузол». Якщо ж це "
+"не так, не встановлюйте з'єднання і повідомте про подію адміністратору."
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "Ігнорувати"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "Вхід"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "У терміналі віддайте таку команду: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"Щоб уможливити вхід до $0 від імені $1 без пароля надалі, скористайтеся "
+"паролем для входу до системи $2 на $3 як паролем до ключа або не заповнюйте "
+"поле пароля ключа."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "Синхронізовано"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "Неактивний"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+msgid "Inactive logical volume"
+msgstr "Неактивний логічний том"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "Включені служби"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr "Типово, вхідні запити заблоковано. Вихідні запити не заблоковано."
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "Несумісне монтування файлових систем"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "Збільшити на одиницю"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "Пам'ять покажчика"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "Опосередкований"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "Інформація"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Інформація і вище"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "Ініціалізувати"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "Ініціалізувати диск $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr ""
+"Ініціалізація призведе до знищення усіх даних, які зберігаються на диску."
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "Ініціалізація…"
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Натяки: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "Встановити"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "Встановити підтримку NFS"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "Встановити підтримку Stratis"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "Встановити усі оновлення"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "Встановити дані щодо програми"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "Встановити cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "Встановити оновлення kpatch"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "Встановити підтримку realmd"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "Встановити оновлення захисту"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr ""
+"Встановіть setroubleshoot-server, щоб мати змогу діагностувати причини подій "
+"SELinux."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "Встановити програмне забезпечення"
+
+#: pkg/kdump/kdump-view.jsx:571
+#, fuzzy
+#| msgid "Please install the $0 package"
+msgid "Install the $0 package."
+msgstr "Будь ласка, встановіть пакунок $0"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr ""
+"Підтримки встановлення без встановленого пакунка cockpit не передбачено"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "Встановлено"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "Встановлення"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "Встановлюємо $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "Встановлення $0 призведе до вилучення $1."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "Встановлення пакунків"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "Інтерфейс"
+msgstr[1] "Інтерфейси"
+msgstr[2] "Інтерфейси"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "Учасники інтерфейсу"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "Інтерфейси"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "Внутрішня помилка"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "Внутрішня помилка: некоректний заголовок виклику"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "Некоректний"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "Некоректна адреса $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "Некоректний формат дати"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "Некоректний формат дати і часу"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "Некоректна дата строку завершення дії"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "Некоректні права доступу до файла"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "Некоректний ключ"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "Некоректна метрика $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "Некоректна кількість днів"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "Некоректний номер порту"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "Некоректний префікс $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "Некоректний префікс або маска мережі $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "Некоректний діапазон"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "Некоректний формат визначення часу"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "Некоректний часовий пояс"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "Некоректне ім’я користувача чи пароль"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "Шлюз IoT"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "sshd працює на іншому порті?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "Завдання"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "З'єднати"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "Долучитися до домену"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "Долучитися до домену"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "Долучення"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "Для долучення до домену слід встановити realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "Підтримки долучення до цього домену не передбачено"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "Долучається до простору назв"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "Журнал"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "Запис журналу"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "Не знайдено запису журналу"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "Перейти"
+
+#: pkg/kdump/kdump-view.jsx:569
+#, fuzzy
+#| msgid "Cockpit is not installed"
+msgid "Kdump service is not installed."
+msgstr "Cockpit не встановлено"
+
+#: pkg/kdump/kdump-view.jsx:604
+#, fuzzy
+#| msgid "Test kdump settings"
+msgid "Kdump settings"
+msgstr "Перевірити параметри kdump"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "Підтримувати з’єднання"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "Дамп аварії ядра"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr ""
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "Дамп ядра"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "Активною є інтерактивна латка до ядра $0"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "Встановлено інтерактивну латку до ядра $0"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "Параметри інтерактивних латок до ядра"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "Інтерактивне латання ядра"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "Пароль до ключа"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "Тут не можна редагувати слоти ключів із невідомими типами"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "Джерело ключа"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "Ключі"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "Сервер ключів"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Адреса сервера ключів"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "Вилучення сервера ключів може завадити розблокуванню $0."
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "Ключ LACP"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "LEGACY із взаємодією з Active Directory."
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+msgid "LVM2 VDO pool"
+msgstr "Буфер VDO LVM2"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+msgid "LVM2 logical volume"
+msgstr "Логічний том LVM2"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+msgid "LVM2 logical volumes"
+msgstr "Логічні томи LVM2"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "Фізичний том LVM2"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+msgid "LVM2 physical volumes"
+msgstr "Фізичні томи LVM2"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "Група томів LVM2"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "Група томів LVM2 $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr ""
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "Переносний ПК"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "Попередні 24 години"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "Попередні 7 днів"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "Останній активний"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+msgid "Last cannot be removed"
+msgstr "Останній не можна вилучати"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "Востаннє перевірено: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+msgid "Last disk can not be removed"
+msgstr "Не можна вилучати останній диск"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "Останній вхід"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "Востаннє змінено: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "Останній вдалий вхід:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "Розкладка"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "Докладніше"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "Полишити $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "Полишити домен"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "Не заповнюйте, щоб пропустити шифрування"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "Ліцензовано за умов дотримання GNU LGPL версії 2.1"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "Світлий"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "Обмежити доступ"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "Обмежений доступ"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr ""
+"Модель із обмеженим доступом обмежує адміністративні привілеї. Функціональні "
+"можливості деяких частин вебконсолі буде суттєво зменшено."
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "Обмеження"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "Лінійний"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "Затримка розірвання зв’язку"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "Пов’язати локальний"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "Спостереження за зв’язком"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "Затримка встановлення зв’язку"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "Спостереження за посиланнями"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "Пов'язаний"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+#, fuzzy
+#| msgid "Unmount filesystem $0"
+msgid "Linux filesystem data"
+msgstr "Демонтувати файлову систему $0"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+#, fuzzy
+#| msgctxt "storage-id-desc"
+#| msgid "Swap space"
+msgid "Linux swap space"
+msgstr "Резервна пам’ять"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "Очікувати"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "Порт очікування"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "Порт очікування має бути числом"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "Виведення списку модулів"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "Не вдалося вивести список модулів: $0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "Завантажити"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "Збалансовування навантаження"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "Завантажити попередні дані"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "Завантажити попередні записи"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "Не вдалося завантажити список доступних оновлень"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "Завантажуємо доступні оновлення, зачекайте…"
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "Завантажуємо попередні записи"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "Завантаження ключів…"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "Не вдалося завантажити ключі SSH"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "Не вдалося завантажити модулі"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "Завантаження пакунків…"
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "Завантажуємо модифікації системи…"
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "Не вдалося завантажити модуль"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "Завантаження…"
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "Локальні облікові записи"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "Локальна файлова система"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "Локальна точка монтування"
+
+#: pkg/storaged/overview/overview.jsx:160
+msgid "Local storage"
+msgstr "Локальне сховище даних"
+
+#: pkg/kdump/kdump-view.jsx:450
+#, fuzzy
+#| msgid "locally in $0"
+msgid "Local, $0"
+msgstr "локально у $0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "Розташування"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "Заблокувати"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "Заблокувати $0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "Заблокувати обліковий запис"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+msgid "Locked data"
+msgstr "Заблоковані дані"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "Блокуємо $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "Увійти"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "Увійти до $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "Увійти на основі даних вашого облікового запису користувача сервера."
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "Повідомлення журналу"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "Вийти"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "Вийти з облікового запису користувача"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "Вхід"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "Логічний"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+#, fuzzy
+#| msgid "Logical volume (snapshot)"
+msgid "Logical Volume Manager partition"
+msgstr "Логічний том (знімок)"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "Логічний розмір"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "Логічний том"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "Логічний том (знімок)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "Логічний том $0"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "Вхід"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "Користувач ще раз"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "Невдала спроба увійти"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "Формат входу"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "Журнал входів"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "Список журналу входів"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "Вийти з $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "Успішний вихід"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "Журнали"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "Низькопрофільна робоча станція"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "Пусковий комп'ютер"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+msgid "MDRAID device"
+msgstr "пристрій MDRAID"
+
+#: pkg/storaged/utils.js:334
+msgid "MDRAID device $0"
+msgstr "Пристій MDRAID $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+msgid "MDRAID device is recovering"
+msgstr "Пристрій MDRAID відновлюється"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+msgid "MDRAID device must be running"
+msgstr "Пристрій MDRAID має бути запущено"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+msgid "MDRAID disk"
+msgstr "Диск MDRAID"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+msgid "MDRAID disks"
+msgstr "Диски MDRAID"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (рекомендоване)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU має бути додатнім числом"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "Ід. комп’ютера"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "Відбитки ключів SSH комп’ютерів"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "Апаратний блок основного сервера"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "Супровід"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr ""
+"Сторінки зловмисників на віддаленому комп'ютері можуть впливати на роботу на "
+"інших з'єднаних вузлах"
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "Керування розмірами файлової системи"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "Керування сховищем"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "Керовані інтерфейси"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "Керування LVM"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "Керування монтуванням NFS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "Керування RAID"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "Керування VDO"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "Керування VLAN"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "Керування брандмауером"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "Керування зв'язками у мережі"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "Керування містками у мережі"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "Керування командами у мережі"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "Керування розділами диска"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "Керування фізичними дисками"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "Керування службами"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "Керування оновленням програм"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "Керування обліковими записами користувачів"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "Вручну"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "Вручну"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "Позначаємо $target як помилковий"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "Замаскувати службу"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "Замасковано"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"Маскування служби забороняє запуск усіх залежних модулів. Це може спричинити "
+"значніші наслідки, ніж ви очікуєте. Будь ласка, підтвердьте, що ви хочете "
+"замаскувати цей модуль."
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "Максимальний вік повідомлення $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+msgid "Media drive"
+msgstr "Мультимедійний носій"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "Пам'ять"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "Технологія пам'яті"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "Використання пам'яті"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "Використання/Резервування пам'яті"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "Об'єднаний"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "Повідомлення користувачам, які увійшли"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "Повідомлення, пов'язані із аварією, яких може не бути у журналі:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "Використано метаданих"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "Метод"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "Метрика"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "Метрика і журнал"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "Не вдалося завантажити журнал метрики"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "Параметри метрики"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "Міні-ПК"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "Міні-башточка"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "Хвилини слід вказувати у форматі числа від 0 до 59"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "За хвилинами"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "Хвилини"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr "Віддзеркалений (RAID 1)"
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "Запобіжні заходи"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "Режим"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "Модель"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "Змінюємо $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "Понеділки"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "Інтервал оновлення"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "Спостерігаємо за цілями"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "Щомісяця"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "Докладніше…"
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "Змонтувати"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "Точка монтування"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "Змонтувати після того, як стане доступною мережа, ігнорувати помилки"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "Також монтувати автоматично під час завантаження"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "Змонтувати при завантаженні"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "Монтувати автоматично до $0 під час завантаження"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "Змонтувати до запуску служб"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "Налаштування монтування"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "Змонтувати файлову систему"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "Змонтувати зараз"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "Змонтувати до $0 зараз"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "Параметри монтування"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "Точка монтування"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "Точка монтування не може бути порожньою"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "Точка монтування не може бути порожньою."
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "Точку монтування вже використано для $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "Запис точки монтування має починатися з «/»."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "Змонтувати лише для читання"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "Змонтувати без очікування, ігнорувати помилки"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "Монтуємо $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "Монтується до запуску служб"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "Монтується паралельно зі службами"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr ""
+"Монтується паралельно зі службами, але після того, як стане доступною мережа"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "Багатосистемний апаратний блок"
+
+#: pkg/storaged/drive/drive.jsx:135
+msgid "Multipathed devices"
+msgstr "Пристрої із багатьма шляхами"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr "Можна вказати декілька адрес, відокремивши їх комами або пробілами."
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "Змонтована NFS"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "Луна-імпульс NSNA"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "Сервер NTP"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "Назва"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "Назва не повинна бути порожньою."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "Назва не може бути порожньою."
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "Назва не повинна бути довшою за $0 байтів"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "Назва не повинна бути довшою за $0 символів"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "Назва не повинна бути довшою за 127 символів."
+
+#: pkg/storaged/btrfs/utils.jsx:87
+#, fuzzy
+#| msgid "Name cannot be longer than 127 characters."
+msgid "Name cannot be longer than 255 characters."
+msgstr "Назва не повинна бути довшою за 127 символів."
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "Назва не повинна містити символу «$0»."
+
+#: pkg/storaged/btrfs/utils.jsx:89
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Name cannot contain the character '/'."
+msgstr "Назва не повинна містити символу «$0»."
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "У назві не повинно бути пробілів."
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr "Потрібен резервний диск"
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "Потрібен принаймні один сервер NTP"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "Мережа"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "Вхід-вихід мережі"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "Мережевий зв'язок"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr ""
+"Для отримання списку пристроїв мережі та графіків слід встановити "
+"NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "Журнали роботи у мережі"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "Використання мережі"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "NetworkManager не встановлено"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager не запущено"
+
+#: pkg/storaged/overview/overview.jsx:168
+msgid "Networked storage"
+msgstr "Мережеве сховище"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "Робота у мережі"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "Ніколи"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "Строк дії облікового запису є нескінченним"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "Необмежений строк дії пароля"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "Ніколи не входив"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "Нове монтування NFS"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "Новий вузол"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "Новий вузол: $0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "Новий пароль до ключа"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "Нова назва"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "Новий пароль"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "Новий пароль"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "Новий пароль не прийнято"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "Далі"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "Ні"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "Не вказано ідентифікатора"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "Немає сповіщень SELinux."
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "Немає встановлених або доступних програм."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "Немає доступних слотів"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "Немає доступних блокових пристроїв."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+msgid "No block devices found"
+msgstr "Не знайдено блокових пристроїв"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "Немає сигналу"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "Не знайдено налаштувань"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "Немає доступним даних"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "Немає доступних даних між $0 і $1"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "Без затримки"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "Немає опису"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "Опису не надано."
+
+#: pkg/storaged/btrfs/volume.jsx:135
+#, fuzzy
+#| msgid "No block devices found"
+msgid "No devices found"
+msgstr "Не знайдено блокових пристроїв"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "Немає доступних дисків."
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+msgid "No disks found"
+msgstr "Дисків не знайдено"
+
+#: pkg/storaged/iscsi/session.jsx:79
+msgid "No drives found"
+msgstr "Дисків не знайдено"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "Без шифрування"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "Немає подій"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "Немає файлової системи"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "Немає файлових систем"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "Немає вільних слотів ключів"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "Недостатньо вільного простору"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "Після цього розділу є вільне місце"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "Не вказано назви групи"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "Не знайдено ключів вузлів."
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "Для цієї програми не знайдено пакунка для встановлення."
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "Не додано жодного ключа"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "Не є відповідною жодна мова"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "Немає записів у журналі"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "Немає логічних томів"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "Журналів не знайдено"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "Нічого не знайдено"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "Не виявлено носія даних"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "Немає розподілу на розділи"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+msgid "No partitions found"
+msgstr "Розділів не знайдено"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "Не додано жодного вузла."
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+msgid "No physical volumes found"
+msgstr "Не знайдено фізичних томів"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "Не вказано справжнього імені"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "Нічого не знайдено"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr ""
+"Не знайдено нічого, щоб відповідало критерію фільтрування. Зніміть усі "
+"фільтри, щоб переглянути результати."
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "Немає збігів з правилами"
+
+#: pkg/storaged/overview/overview.jsx:190
+msgid "No storage found"
+msgstr "Не знайдено сховища даних"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+#, fuzzy
+#| msgid "No logical volumes"
+msgid "No subvolumes"
+msgstr "Немає логічних томів"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "Немає такого файла або каталогу"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "Немає модифікацій системи"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "Немає системних звітів."
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "Немає оновлень"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "Не вказано імені користувача"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "Немає"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "Некоректний закритий ключ"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "Не уповноважено вимикати брандмауер"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "Не уповноважено вмикати брандмауер"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "Недоступний"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "Не з’єднано"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "Не з'єднано із Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "Не з'єднано із вузлом"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+msgid "Not enough free space"
+msgstr "Недостатньо вільного місця"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "Недостатньо місця"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+msgid "Not enough space to grow"
+msgstr "Недостатньо місця для збільшення"
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "Не знайдено"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "Не встановлено"
+
+#: pkg/networkmanager/network-interface.jsx:680
+msgid "Not permitted to configure network devices"
+msgstr "Не дозволено налаштовувати пристрої мережі"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "Не дозволено налаштовувати області"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "Немає дозволу на виконання цієї дії."
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "Не готовий"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "Не зареєстровано"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "Зупинено"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "Не налаштовано"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "Не синхронізовано"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "Примітка"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "Ноутбук"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Зауваження і вище"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "Заплутувати адреси мережі, назви вузлів та імена користувачів"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "Заплутано"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "Сталося $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "Сталося між $0 і $1"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "Випадки"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "Гаразд"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "Старий пароль"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "Старий пароль"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "Старий пароль не прийнято"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "На змонтованому пристрої"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "Якщо помилка"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"Після встановлення Cockpit його можна увімкнути за допомогою команди "
+"«systemctl enable --now cockpit.socket»."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "Використано лише $0 з $1."
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "Можна використовувати лише літери, цифри, : , _ , . , @ , -"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "Лише критичне"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr ""
+"Використовувати лише затверджені і дозволені алгоритми при завантаженні у "
+"режимі FIPS."
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "Вибачте!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr ""
+"Відкрити службу pmproxy у брандмауері для надання метрики у спільне "
+"користування."
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "Дія «$operation» над $target"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "Параметри"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "Або скористайтеся комплектним браузером"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "Інше"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr ""
+"Навіть якщо розпізнавання за інтерактивним паролем заборонено, можна "
+"скористатися іншими способами розпізнавання."
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "Інші параметри"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "Вихід"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "Огляд"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "Перезаписати"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "Перезаписати наявні дані нулями (повільно)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "Не вистачає пакунка cockpit-pcp для журналу метрики"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "Дані щодо пакунка"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "Аварійне завершення роботи PackageKit"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit не встановлено"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit повідомлено про помилку із кодом $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "Пакунки"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "Назва сторінки"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "Батьківський"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "Батьківський $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "Частина"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "Є частиною $0"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "Розділ"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "Розділ $0"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "Розмір розділу — $0. Розмір даних — $1."
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "Розподіл"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "Розділи"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "Неактивний"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "Пароль"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "Пароль не може бути порожнім"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "Пароль не може бути порожнім"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "Пароль з будь-якого іншого слоту ключів"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "Вилучення пароля може завадити розблокуванню $0."
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "Паролі не збігаються"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "Пароль"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "Пароль успішно змінено"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "Строк дії пароля"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "Довжина пароля перевищує 256 символів"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "Пароль є неприйнятним"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "Пароль є надто простим"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "Пароль має бути змінено"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "Пароль не прийнято"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "Підказка щодо пароля"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "Вставити"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "Помилка вставлення"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "Вставити наявний ключ"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "Сюди слід вставити вміст файла вашого відкритого ключа SSH"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "Шлях"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "Вартість маршруту"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "Вартість шляху $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "Шлях на сервері"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "Шлях на сервері не може бути порожнім."
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "Шлях на сервері має починатися з «/»."
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "Шлях до каталогу"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "Шлях до файла"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "Шляхи"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "Призупинити"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr "У вузла #$0 некоректний порт кінцевої точки. Порт має бути числом."
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"У вузла #$0 некоректна кінцева точка. Її слід вказати у форматі вузол:порт, "
+"приклад: 1.2.3.4:51820 або example.com:51820"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "Вузли"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"Вузли є іншими машинами, які з'єднано із цією. Відкриті ключі з інших машин "
+"буде надано у спільне користування цій машині."
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr ""
+"Co-Pilot швидкодіії збирає та аналізує метрику швидкодії вашої системи."
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "Профіль швидкодії"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "Периферійний апаратний блок"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "Постійний"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "Остаточно вилучити групу $0?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "Остаточно вилучити $0?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "Остаточно вилучити логічний том $0/$1?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+#, fuzzy
+#| msgid "Permanently delete $0?"
+msgid "Permanently delete subvolume $0?"
+msgstr "Остаточно вилучити $0?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "Доступ заборонено"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "Дозвільна"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "Фізичний"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "Фізичні томи"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "Фізичні томи"
+
+#: pkg/storaged/block/resize.jsx:279
+msgid "Physical volumes can not be resized here"
+msgstr "Тут не можна змінювати розміри фізичних томів"
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "Вибрати дату"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "Пришпилити модуль"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Проміжок між імпульсами"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Ціль тестування луною"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "Пришпилений модуль"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "З коробку для піци"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr ""
+"Будь ласка, пройдіть розпізнавання, щоб отримати адміністративний доступ"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr ""
+"Щоб мати змогу користуватися вебконсоллю, будь ласка, увімкніть JavaScript."
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "Будь ласка, встановіть пакунок $0"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "Будь ласка, усуньте проблему і перезавантажте цю сторінку."
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "Будь ласка, вкажіть кінцеву дату строку дії"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "Будь ласка, вкажіть вузол, з яким слід встановити з'єднання"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "Будь ласка, спочатку демонтуйте їх."
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "Буфер для тонких логічних томів"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "Буфер для тонких резервованих логічних томів LVM2"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "Буфер для тонких резервованих томів"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "Пароль до буфера"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "Порт"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "Портативний"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "Порти"
+
+#: pkg/storaged/partitions/partition.jsx:116
+#, fuzzy
+#| msgid "Create partition"
+msgid "PowerPC PReP boot partition"
+msgstr "Створити розділ"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "Довжина префікса"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "Довжина префікса або маска мережі"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "Приготування"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "Поточна"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "Зберегти"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "Зручна назва вузла"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "Попереднє завантаження"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "Основний"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "Пріоритетність"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "Пріоритетність $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "Закритий ключ"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "Проблеми із набуттям прав адміністратора"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "Подробиці щодо проблеми"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "Дані щодо проблеми"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "Процеси, що використовують це місце"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "Поступ: $0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "Сайт проєкту"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "Час очікування відповіді на запит за допомогою passwd вичерпано"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "Час очікування відповіді на запит за допомогою ssh-add вичерпано"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "Час очікування відповіді на запит за допомогою ssh-keygen вичерпано"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "Поширює перезавантаження на"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr ""
+"Захищає від очікуваних у короткостроковій перспективі атак за рахунок "
+"звуження можливостей взаємодії."
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "Вкажіть пароль до буфера даних на цих блокових пристроях:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "Відкритий ключ"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr ""
+"Відкритий ключ буде створено, коли буде введено коректний закритий ключ"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "Призначення"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (Стрічка)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (Дзеркало)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (Стрічка дзеркал)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (Пов’язана парність)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (Розподілена парність)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (Подвійна розподілена парність)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "Апаратний блок RAID"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "Рівень RAID"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr "Для RAID10 потрібна парна кількість фізичних томів"
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "Пам'ять"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "Апаратний блок монтування стійок"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "Випадковий"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "Діапазон"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "Діапазон має бути строго упорядковано"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Ранг"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "Без обробки на пристрій"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "Читання"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "Докладніше…"
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "Лише читання"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "Читання"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "Читання…"
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "Готовий"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "Готово"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "Справжня назва вузла"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr ""
+"Назва справжнього вузла має складатися лише з літер у нижньому регістрі, "
+"цифр, дефісів та точок (із заповненими піддоменами)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "Назва справжнього вузла має складатися не більше ніж з 64 символів"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "Повторно застосувати і перезавантажити"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "Перезавантажити"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "Перезавантажити після завершення"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "Рекомендовано перезавантажити"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "Перезавантажити систему…"
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "Отримання"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "Нещодавні вузли"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "Рекомендовано, безпечні параметри для поточних моделей загроз."
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "Повторно з’єднатися"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "Відновлюємо"
+
+#: pkg/storaged/jobs-panel.jsx:70
+msgid "Recovering MDRAID device $target"
+msgstr "Відновлюємо пристрій MDRAID $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "Оновлюємо дані щодо пакунків"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "Відмовляємо у з’єднанні. Невідомий вузол."
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "Відмовляємо у з’єднанні. Ключі вузла не збігаються."
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "Відмовляємо у з’єднанні. Невідомий ключ вузла."
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "Повторно створити"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "Повторно створюємо initrd"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "Зареєструвати…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "Пов'язані процеси і служби буде примусово зупинено."
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "Пов'язані процеси буде примусово зупинено."
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "Пов'язані служби буде примусово зупинено."
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "Перезавантажити"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "Поширене перезавантаження з"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "Перезавантаження"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "Перезавантажуємо стан решти служб"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "Віддалене на основі CIFS/SMB"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "Віддалене на основі FTP"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "Віддалене на основі NFS"
+
+#: pkg/kdump/kdump-view.jsx:456
+#, fuzzy
+#| msgid "Remote over NFS"
+msgid "Remote over NFS, $0"
+msgstr "Віддалене на основі NFS"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "Віддалене на основі SFTP"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "Віддалене на основі SSH"
+
+#: pkg/kdump/kdump-view.jsx:453
+#, fuzzy
+#| msgid "Remote over SSH"
+msgid "Remote over SSH, $0"
+msgstr "Віддалене на основі SSH"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "Вилучення:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "Вилучити"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "Вилучити $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "Вилучити службу $0 із зони $1"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "Вилучити $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "Вилучити сервер ключів Tang?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "Вилучити пристрій"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "Вилучити вузол"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "Вилучити запис"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "Вилучити пропущені фізичні томи?"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "Вилучити пароль у слоті ключів $0?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "Вилучити пароль?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "Вилучити службу $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "Вилучити зону $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "Вилучаємо"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "Вилучаємо $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Вилучення $0 призведе до розірвання з’єднання із сервером і зробить "
+"адміністративний інтерфейс користувача недоступним."
+
+#: pkg/storaged/jobs-panel.jsx:66
+msgid "Removing $target from MDRAID device"
+msgstr "Вилучаємо $target з пристрою MDRAID"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"Вилучення пароля без підтвердження іншим паролем може закрити доступ до "
+"розблоковування або керування ключами, якщо інші паролі буде забуто або "
+"втрачено."
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "Вилучаємо фізичний том з $target"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"Вилучення служби cockpit може призвести до недоступності вебконсолі. "
+"Переконайтеся, що застосування цієї зони не поширюється на ваше поточне "
+"з'єднання за допомогою вебконсолі."
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "Вилучення запису зони призведе до вилучення усіх служб у ній."
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "Перейменувати"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "Перейменувати буфер Stratis"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "Перейменувати файлову систему"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "Перейменувати групу"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "Перейменувати групу $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "Перейменувати логічний том"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "Перейменувати групу томів"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "Перейменовуємо $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "Перейменування групи може вплинути на роботу sudo та подібних правил"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr "Полагодити"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+msgid "Repair logical volume $0"
+msgstr "Полагодити логічний том $0"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "Відновлюємо $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "Повторення"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "Повторювати щомісяця"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "Повторіть пароль"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "Повторювати щотижня"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "Звіт"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "Мітка звіту"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "Звітувати до аналітики ABRT"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "Звіт створено; немає доступу до посилань"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "Невдале звітування"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "Звітування було скасовано"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "Звіти"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "Звіти:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "Вимагати зміну пароля кожні $0 днів"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "Вимагати зміну пароля $0"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "Вимагати зміну пароля при першому вході"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "Потрібен для"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "Потрібен для "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "Потребує"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "Потребує адміністративного доступу для редагування"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "Потрібний"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "Потрібний для"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "Зарезервована пам’ять"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "Скинути"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "Скинути пароль"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "Зміна розміру $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr ""
+"Зміна розмірів зашифрованої системи потребує розблокування диска. Будь "
+"ласка, вкажіть поточний пароль до диска."
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "Перезапустити"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "Перезапустити служби"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "Перезапустити служби…"
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "Перезапускаємо"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "Відновлюємо з’єднання"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr ""
+"Результати обробки даних аварії буде скопійовано за допомогою $0 до $1 як "
+"$2, якщо належним чином налаштовано kdump."
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr ""
+"Результати обробки даних аварії буде збережено до $0 як $1, якщо належним "
+"чином налаштовано kdump."
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "Відновити"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "Повторно використати наявне шифрування"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "Повторно використати наявне шифрування ($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "Переглянути правила шифрування"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "Перегляд журналів"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "Циклічне"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "Маршрути"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "Запустити"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "Запустити новий звіт"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "Запустити на"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "Запустити звітування"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr ""
+"Запустити цю команду довіреною мережею або фізично на віддаленому комп'ютері:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "Засіб для запуску"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "Працює"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "Простір виконання"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "Помилка керування доступом SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "У системі вимкнено SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "SELinux вимкнено у системі."
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "Правила SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "Стан системи SELinux є невідомим."
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "Усування вад SELinux"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "Самоперевірка SMART $target"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr ""
+"SOS-звітування збирає дані щодо системи для того, щоб допомогти у "
+"діагностуванні проблем."
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "Ключ SSH"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "Ключ SSH не вказано у форматі шляху"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "Ключі SSH"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "Затримка переспрямування STP"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "Час вітання STP"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "Максимальний вік повідомлення STP"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "Пріоритет STP"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr ""
+"Користувачам Safari слід імпортувати сертифікат самопідписаного CA і "
+"встановити довіру до нього:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "Суботи"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "Зберегти"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "Зберегти і перезавантажити"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "Зберегти зміни"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "Заощаджувати місце стисканням окремих блоків за допомогою LZ4"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "Заощаджувати місце, зберігаючи ідентичні блоки даних лише раз"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr ""
+"Збереження нового пароля потребує розблокування диска. Будь ласка, вкажіть "
+"поточний пароль до диска."
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "Заплановане вимикання о $0"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "Заплановане перезавантаження о $0"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "ПК з опломбованим корпусом"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "Пошук"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "Домен пошуку"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "Шукати за назвою або ідентифікатором"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "Шукати за назвою, групою або ідентифікатором"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "Секунди слід вказувати у форматі числа від 0 до 59"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "Секунди"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "Ключі SSH"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "Безпечно витираємо $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Налаштування Security Enhanced Linux та усування вад"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "Доступні оновлення захисту"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "Лише оновлення захисту"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "Оновлення безпеки буде застосовано $0 о $1"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "Вибрати"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "Виберіть ідентифікатор"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "Виберіть метод"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr ""
+"Виберіть фізичні томи, якими слід скористатися для того, щоб полагодити "
+"логічний том. Потрібно принаймні $0."
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "Надіслати"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "Надсилання"
+
+#: pkg/storaged/drive/drive.jsx:123
+msgid "Serial number"
+msgstr "Серійний номер"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "Сервер"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "Адреса сервера"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "Адреса сервера не може бути порожньою."
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "Запис сервера не може бути порожнім."
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "З’єднання розірвано сервером."
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "Програмне забезпечення сервера"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "Служба"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "Помилка у службі"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "Журнали служб"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "Служби"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "Служби, що використовують це місце"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "Сеанс"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "Строк дії сеансу вичерпується"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "Встановити"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "Встановити назву вузла"
+
+#: pkg/storaged/partitions/partition.jsx:173
+#, fuzzy
+#| msgid "Create partition on $0"
+msgid "Set partition type of $0"
+msgstr "Створити розділ на $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "Встановити пароль"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "Встановити час"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "Встановити значення"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "Налаштувати"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "Встановити простий пароль"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr ""
+"Параметр не відповідає налаштованому стану, його буде скинуто під час "
+"наступного завантаження."
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "Налаштовуємо"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "Налаштовуємо петльовий пристрій $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "Параметри"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "Важливість"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "Спільний"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Оболонка"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Скрипт оболонки"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+msgid "Show all $0 rows"
+msgstr "Показати усі $0 рядків"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "Показати усі потоки"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Show confirmation password"
+msgstr "Показати підтвердження пароля"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "Показати відбитки"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "Показати повідомлення, які містять вказаний рядок."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "Показати повідомлення для вказаного модуля systemd."
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "Показати повідомлення з вказаного завантаження."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "Показати більше зв'язків"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "Показати пароль"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "Показати зв'язки"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "Стиснути"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "Стиснути логічний том"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "Стиснути розділ"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "Стиснути том"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "Вимкнути"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "Вимкнути"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "З"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "Єдиний ранг"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "Розмір"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "Розмір не може бути від’ємним"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "Розмір не може бути нульовим"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "Розмір є надто великим"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "Розмір має бути числом"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "Розмір має бути не меншим за $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "Пропустити основну навігацію"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "Перейти до вмісту"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "Слот"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "Слот $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "Знімок"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "Сокети"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "Оновлення програм"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"Програмні заходи допомагають усунути проблеми із захистом процесора. Ці "
+"заходи мають побіжний ефект — зниження швидкодії. Змінюйте ці параметри, "
+"лише якщо повністю усвідомлюєте наслідки."
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr "Твердотільний диск"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "Рішення успішно застосовано"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "Помилка рішення"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "Рішення"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"Після створення буфера зріс розмір деяких блокових пристроїв у цьому буфері. "
+"Розмір буфера можна безпечно збільшити з метою використання доданого місця."
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr ""
+"Програмою для керування пакунків користується якась інша програма, будь "
+"ласка, зачекайте…"
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "Частину програмного забезпечення доведеться перезавантажити вручну"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr "Вибачте"
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "Упорядковано від найменшої до найбільшої довіри"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "Компактний комп'ютер"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "Протокол пересування ієрархією"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "Протокол пересування ієрархією (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "Запас"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "У визначений час"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "Швидкість"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "Стабільний"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "Почати"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "Запустити і увімкнути"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "Запустити Multipath"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "Запустити службу"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr "Почати показ записів, дата яких є рівною вказаній або новішою за неї."
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr ""
+"Показати показ записів, дата яких є рівною вказаній або старішою за неї."
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "Почато"
+
+#: pkg/storaged/jobs-panel.jsx:64
+msgid "Starting MDRAID device $target"
+msgstr "Запускаємо пристрій MDRAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "Запускаємо резервну область пам’яті $target"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "Стан"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "Статичний"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "Стан"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "Паличковий ПК"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "Липкий"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "Зупинити"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "Зупинити і вимкнути"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "Зупинити і вилучити"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "Зупинити і демонтувати"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "Зупинити пристрій"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "Припинити редагування вузлів"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "Зупинити звітування"
+
+#: pkg/storaged/jobs-panel.jsx:63
+msgid "Stopping MDRAID device $target"
+msgstr "Зупиняємо пристрій MDRAID $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "Зупиняємо резервну область пам’яті $target"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "Сховище даних"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "У цій системі не можна керувати сховищем даних."
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "Журнали зберігання"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "Зберігати пароль"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "Збережений пароль"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+msgid "Stratis block device"
+msgstr "Блоковий пристрій Stratis"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+msgid "Stratis block devices"
+msgstr "Блокові пристрої Stratis"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Блокові пристрої Stratis не можна звужувати"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+msgid "Stratis filesystem"
+msgstr "Файлова система Stratis"
+
+#: pkg/storaged/stratis/pool.jsx:300
+msgid "Stratis filesystems"
+msgstr "Файлові системи Stratis"
+
+#: pkg/storaged/stratis/pool.jsx:361
+msgid "Stratis filesystems pool"
+msgstr "Буфер файлових систем Stratis"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Буфер Stratis"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr "Смуговий (RAID 0)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr "Смуговий і віддзеркалений (RAID 10)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr "Смуги"
+
+#: pkg/lib/cockpit-components-password.jsx:97
+msgid "Strong password"
+msgstr "Складний пароль"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Затичка"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "Стиль"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "Підблок"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "Підноутбук"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "Не вдалося підписатися на сигнали systemd: $0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+#, fuzzy
+#| msgid "Volume failed to be created"
+msgid "Subvolume needs to be mounted"
+msgstr "Не вдалося створити том"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr ""
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "Успішно скопійовано до буфера обміну"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "Успішно скопійовано до буфера обміну!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "Неділі"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "Свопінґ"
+
+#: pkg/storaged/block/resize.jsx:292
+#, fuzzy
+#| msgid "$0 can not be resized here"
+msgid "Swap can not be resized here"
+msgstr "Тут не можна змінювати розмір $0"
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "Резервування"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "Перемикання $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "Вимкнути $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "Увімкнути $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "Перемкнутися на адміністративний доступ"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "Перемкнутися на обмежений доступ"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Вимикання $0 призведе до розірвання з’єднання із сервером і зробить "
+"адміністративний інтерфейс користувача недоступним."
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr ""
+"Вмикання $0 призведе до розірвання з’єднання із сервером і зробить "
+"адміністративний інтерфейс користувача недоступним."
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "Синхронізовано"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "Синхронізовано із $0"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "Синхронізація"
+
+#: pkg/storaged/jobs-panel.jsx:71
+msgid "Synchronizing MDRAID device $target"
+msgstr "Синхронізуємо пристрій MDRAID $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "Система"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "Діагностика системи"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "Інформація про систему"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "Система не потребує оновлення"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "Модифікації системи"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "Системний час"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Модулі systemd"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "Планшет"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Сервер ключів Tang"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "Призначення"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "Призначення"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "Команда"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "Порт команди"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "Параметри порту команди"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "Термінал"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "Перервати сеанс"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "Налаштування перевірки"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "Перевірка є доступною, лише якщо запущено службу kdump."
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "Перевірити параметри kdump"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"Виконати тестування параметрів kdump, аварійно завершивши роботу ядра. "
+"Процедура може бути досить тривалою, а система може не перезавантажитися "
+"автоматично. Не виконуйте навмисного аварійного завершення роботи системи, "
+"якщо система виконує хоч якесь важливе завдання."
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "Перевіряємо з’єднання"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "Пакунка $0 немає у жодному зі сховищ."
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "Для створення буферів Stratis має бути встановлено пакунок $0."
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "Має бути встановлено пакунок $0."
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "Для створення пристроїв VDO буде встановлено пакунок $0."
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "У IP-адресі або назві вузла не повинно бути пробілів."
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+msgid "The MDRAID device is in a degraded state"
+msgstr "Пристрій MDRAID перебуває у стані із погіршеними властивостями"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+msgid "The MDRAID device must be running"
+msgstr "Пристрій MDRAID має працювати"
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr "Ключ SSH $0 $1 на $2 буде додано до файла $3 $4 на $5."
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr ""
+"Ключ SSH $0 буде доступним протягом решти сеансу і також буде доступним для "
+"входу на інші вузли."
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"Ключ SSH для входу до $0 захищено паролем, а на вузлі заборонено вхід без "
+"пароля. Буль ласка, вкажіть пароль до ключа у $1."
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"Ключ SSH для входу до $0 захищено. Ви можете увійти або за допомогою пароль "
+"до вашого облікового запису або вказавши пароль до ключа у $1."
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr ""
+"Обліковий запис «$0» має примусово змінити пароль під час наступного входу"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "Службу cockpit включено автоматично"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr ""
+"Налаштований стан є невідомим, його може бути змінено під час наступного "
+"завантаження."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr "Створення цього пристрою VDO не завершено, ним не можна користуватися."
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr ""
+"Поточний користувач, від імені якого було здійснено вхід до системи, не має "
+"права перегляду даних щодо ключів."
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr ""
+"Перед форматуванням диск має бути розблоковано. Будь ласка, надайте наявний "
+"пароль."
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "Файл $0 буде вилучено."
+
+#: pkg/storaged/filesystem/utils.jsx:191
+#, fuzzy
+#| msgid "The filesystem has no permanent mount point."
+msgid "The filesystem has no assigned mount point."
+msgstr "У файлової системи немає сталої точки монтування."
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "У файлової системи немає сталої точки монтування."
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr ""
+"Файлову систему налаштовано на автоматичне монтування при вході до системи, "
+"але її контейнер шифрування не буде розблоковано вчасно."
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr ""
+"Файлову систему зараз змонтовано, але її не буде змонтовано після наступного "
+"завантаження."
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr ""
+"Файлову систему зараз змонтовано до $0, але її буде змонтовано до $1 під час "
+"наступного завантаження."
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr ""
+"Файлову систему зараз змонтовано до $0, але її не буде змонтовано після "
+"наступного завантаження."
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr ""
+"Файлову систему зараз не змонтовано, але її буде змонтовано під час "
+"наступного завантаження."
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "Файлову систему не змонтовано."
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr ""
+"Файлову систему буде розблоковано і змонтовано під час наступного "
+"завантаження. Це може потребувати введення пароля."
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "Відбиток має збігатися:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "Буде перезавантажено такі служби:"
+msgstr[1] "Буде перезавантажено такі служби:"
+msgstr[2] "Буде перезавантажено такі служби:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "У повному імені не повинно міститися двокрапок."
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "Ідентифікатор групи має бути додатнім цілим числом"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr ""
+"Назва групи може складатися лише із літер a-z, цифр, крапок, дефісів та "
+"символів підкреслювання"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr ""
+"Домашній каталог $0 вже існує. Права власності на нього буде змінено для "
+"допуску нового користувача."
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "initrd має бути створено повторно."
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "Пароль до ключа не може бути порожнім"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "Паролі до ключа не збігаються"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "Наданий вами ключ є некоректним."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "Не можна вилучати останній слот ключів"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "Процеси і служби зі списку буде примусово зупинено."
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "Процеси зі списку буде примусово зупинено."
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "Служби зі списку буде примусово зупинено."
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr ""
+"Користувач, який увійшов до системи, не має права переглядати модифікації "
+"системи"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "Комп’ютер перезавантажується"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "Точка монтування $0 перебуває у користуванні такими процесами:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "Точка монтування $0 перебуває у користуванні такими службами:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "Новий пароль до ключа не може бути порожнім"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "Пароль не може бути порожнім"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "Паролі не збігаються"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "Паролі не збігаються."
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr ""
+"Відбиток-результат можна поширювати у спосіб із загальним доступом, зокрема "
+"електронною поштою."
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"Відбиток-результат можна поширювати відкритими способами, включно із "
+"електронною поштою. Якщо ви просите когось виконати перевірку для вас, вони "
+"можуть надсилати результати за допомогою будь-якого способу."
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"Сервером відмовлено у розпізнаванні «$0» за допомогою пароля. Інших "
+"підтримуваних способів розпізнавання не передбачено."
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr ""
+"Сервер відмовився розпізнавати користувача за допомогою будь-якого з "
+"підтримуваних методів."
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr ""
+"У системі зараз не передбачено підтримки розблокування файлової системи з "
+"сервером ключів Tang під час завантаження."
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr ""
+"У системі зараз не передбачено підтримки розблокування кореневої файлової "
+"системи з сервером ключів Tang."
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr "Користувачу $0 заборонено змінювати заходи захисту процесора"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "Користувачеві $0 заборонено змінювати правила шифрування"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr ""
+"Користувачеві $0 заборонено виконувати тестове аварійне завершення роботи "
+"ядра"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr ""
+"Для набуття чинності новими налаштуваннями користувач має вийти із системи і "
+"увійти до неї знову."
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr ""
+"Ім’я користувача може складатися лише із літер a-z, цифр, крапок, дефісів та "
+"символів підкреслювання."
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr ""
+"Налаштування програми для перегляду інтернету забороняють запуск Cockpit "
+"(недоступна можливість $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "Зараз активних сторінок немає"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr ""
+"У системі є пристрої із декількома шляхами доступу, але службу multipath не "
+"запущено."
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "У цій зоні немає активних служб"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "Для цього облікового запису немає уповноважених відкритих ключів."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+"Недостатньо місця, яким можна скористатися для того, щоб полагодити систему. "
+"Потрібно принаймні $0 на фізичний томах, які ще не використано для цього "
+"логічного тому."
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"У буфері даних недостатньо вільного місця для створення знімка цієї файлової "
+"системи. Потрібно принаймні $0, а доступними є лише $1."
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr ""
+"Під час спроби встановити з'єднання із комп'ютером сталася неочікувана "
+"помилка."
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "Потрібні такі додаткові кроки:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "Буде внесено такі зміни:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "Тонкий логічний том"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "Тонкі резервовані логічні томи LVM2"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr ""
+"Цей пристрій MDRAID не має бітової карти призначення до запису. Така бітова "
+"карта може значно зменшити час синхронізації."
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr ""
+"Це монтування NFS використовується; можна лише змінювати його параметри."
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "Цей пристрій VDO не використовує увесь об'єм резервного пристрою."
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+#, fuzzy
+#| msgid "This device cannot be managed here."
+msgid "This device can not be used for the installation target."
+msgstr "Тут не можна керувати цим пристроєм."
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "Тут не можна керувати цим пристроєм."
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "Цей пристрій зараз перебуває у користуванні."
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "Вміст цього поля не може бути порожнім"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "Ця група є основою групою для таких користувачів:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "Цей вузол буде перезавантажено після встановлення оновлень."
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "Ці дані зберігатимуться лише у системі."
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Цей сервер ключів є єдиним способом розблокування буфера, його не можна "
+"вилучати."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+"Цей логічний том втратив частину його фізичних томів, ним не можна надалі "
+"користуватися. Вам слід вилучити його і створити новий, який займе його "
+"місце."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+"Цей логічний том втратив частину фізичних томів, але не втратив жодних "
+"даних. Вам слід полагодити його для відновлення початкової надлишковості."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+"Цей логічний том втратив частину своїх фізичних томів, але, ймовірно, жодних "
+"даних на ньому не втрачено. Ви, можливо, зможете його полагодити."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr ""
+"Місце на цьому логічному томі не повністю використано даними, які на ньому "
+"зберігаються."
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "Цей комп’ютер вже було додано."
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "Зачекайте"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr ""
+"Місце на цьому розділі не повністю використано даними, які на ньому "
+"зберігаються."
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr ""
+"Цей пароль є єдиним способом розблокування буфера, його не можна вилучати."
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr ""
+"Цей буфер даних не використовує усе доступне місце на його блокових "
+"пристроях."
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "Цей буфер перебуває у стані із погіршеними властивостями."
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "Цю систему не зареєстровано"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "Ця система використовує нетиповий профіль"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "Ця система використовує рекомендований профіль"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr ""
+"Цей інструмент налаштовує правила SELinux і може допомогти зрозуміти та "
+"усунути порушення правил."
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr "Цей інструмент налаштовує систему на запис дампів аварій ядра на диск."
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"Цей інструмент створюдє архів даних щодо налаштувань та діагностики для "
+"запущеної системи. Архів може бути збережено локально або централізовано з "
+"метою журналювання або стеження або надіслано до представників технічної "
+"підтримки, розробників або адміністраторів системи, щоб допомогти з пошуком "
+"технічних проблем та діагностикою."
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr ""
+"Цей інструмент керує локальним сховищем даних, зокрема файловими системами, "
+"групами томів LVM2 та монтуваннями NFS."
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"Цей інструмент керує можливостями роботи у мережі, зокрема зв'язками, "
+"містками, командами, віртуальними LAN та брандмауерами, за допомогою "
+"NetworkManager і Firewalld. NetworkManager є несумісним із типовим для "
+"Ubuntu systemd-networkd та скриптами ifupdown Debian."
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "Цей модуль не створено для вмикання явним чином."
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "Запис користувача із таким іменем уже існує"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr "У цій групі томів не вистачає деяких фізичних томів."
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr ""
+"Ця програма для перегляду інтернету є надто старою для роботи з вебконсоллю "
+"(не вистачає можливості $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"Це додасть відповідність «_BOOT_ID=». Якщо не вказано, буде показано журнал "
+"поточного завантаження. Якщо не вказано ідентифікатор завантаження, додатний "
+"відступ призводитиме до пошуку завантажень від початку журналу, а нульовий "
+"або від'ємний відступ призводитиме до пошуку завантажень починаючи від кінця "
+"журналу. Отже, 1 означає перше завантаження, яке буде знайдено у журналі у "
+"хронологічному порядку, 2 — друге завантаження тощо; а -0 — останнє "
+"завантаження, -1 — завантаження перед останнім тощо."
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"Це додасть відповідність «_SYSTEMD_UNIT=», «COREDUMP_UNIT=» та «UNIT=» для "
+"пошуку усіх можливих повідомлень для вказаного модуля. Запис може містити "
+"декілька записів модулів, які слід відокремлювати комами. "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "Це надасть вам змогу надалі входити без пароля."
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"У цій зоні міститься служба cockpit. Переконайтеся, що застосування цієї "
+"зони не поширюється на ваше поточне з'єднання за допомогою вебконсолі."
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "Четверги"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "Клас"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "Час"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "Часовий пояс"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "Не вдалося створити таймер"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "Не вдалося вилучити таймер"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "Таймери"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr ""
+"Підказка: для автоматичного розпізнавання на інших системах встановіть "
+"однаковий пароль для ключа і для входу до системи."
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr ""
+"Щоб переконатися, що дані вашого з'єднання не буде перехоплено "
+"зловмисниками, будь ласка, підтвердьте відбиток ключа вузла:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"Щоб отримувати оновлення програмного забезпечення, цю систему слід "
+"зареєструвати у Red Hat або за допомогою порталу клієнтів Red Hat, або за "
+"допомогою локального сервера передплати."
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"Щоб перевірити відбиток, віддайте вказану нижче команду для $0 під час "
+"безпосередньої роботи на комп'ютері або з використанням надійної мережі:"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "Сьогодні"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "Перемкнути"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "Перемкнути засіб вибору дати"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "Перемкнути фільтри"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "Забагато даних"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "Інструменти"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "Основні 5 служб процесора"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "Основні 5 служб, що використовують диск"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "Основні 5 служб пам'яті"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "Загальний розмір: $0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Башточка"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "Проміжний"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "Передавання"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "Перемикач"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "Причина вмикання"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "Умовні зміни"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "Діагностика проблем"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "Діагностика проблем…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "Довіряти і додати вузол"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "Довіряти ключу"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "Рівень довіри"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "Спробувати ще раз"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "Намагаємося синхронізуватися з $0"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "Вівторки"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned не вдалося запустити"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned — служба, яка стежить за вашою системою і оптимізує швидкодію за "
+"певних умов навантаження. Ядром Tuned є профілі, які налаштовують вашу "
+"систему для різних умов користування."
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned недоступна"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned не запущено"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned вимкнено"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "Увімкнути адміністративний доступ"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "Тип"
+
+#: pkg/storaged/partitions/partition.jsx:165
+#, fuzzy
+#| msgid "Name cannot contain the character '$0'."
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "Назва не повинна містити символу «$0»."
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr ""
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr ""
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "Введіть щось для фільтрування"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "Не вдалося застосувати це рішення у автоматичному режимі"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "Не вдалося встановити з’єднання із цією адресою"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "Не вдалося встановити зв'язок із $0."
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"Не вдалося встановити зв’язок із вказаним вузлом $0. Переконайтеся, що ssh "
+"запущено на порту $1 або вкажіть інший порт у адресі."
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "Не вдалося отримати параметри нагадувань."
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"Не вдалося увійти до $0 за допомогою розпізнавання за ключем SSH. Будь "
+"ласка, вкажіть пароль. Ви можете налаштувати ваші ключі SSH для "
+"автоматичного входу до системи."
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr ""
+"Не вдалося увійти до $0. Вузол не приймає входу за паролем або будь-яким з "
+"ваших ключів SSH."
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "Не вдалося досягти сервера"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "Не вдалося вилучити монтування"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+msgid "Unable to repair logical volume $0"
+msgstr "Не вдалося полагодити логічний том $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "Не вдалося запустити виправлення: $0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "Не вдалося зберегти параметри"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "Не вдалося зберегти параметри: $0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "Не вдалося запустити setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "Не вдалося демонтувати файлову систему"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "Недоступний"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "Недоступні пакунки"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "Скасувати"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "Неочікувана помилка PackageKit під час встановлення $0: $1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "Неочікувана помилка"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+msgid "Unformatted data"
+msgstr "Неформатовані дані"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "Модуль"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "Невідомий"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "Невідомий «$0»"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "Невідомий ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "Невідома програма"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "Невідомі налаштування"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "Невідома назва вузла"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "Невідома назва служби"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "Невідомий тип"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "Розблокувати"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "Автоматично розблокувати при завантаженні"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr "Розблокуйте до зміни розмірів"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "Розблокувати зашифрований буфер Stratis"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "Розблокувати ключ $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "Розблокуємо $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "Розблоковуємо диск"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "Некеровані інтерфейси"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "Демонтувати"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "Демонтувати файлову систему $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "Демонтувати зараз"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "Демонтуємо $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "Без назви"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "Відшпилити модуль"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "Нерозпізнані дані"
+
+#: pkg/storaged/block/resize.jsx:306
+msgid "Unrecognized data can not be made smaller here"
+msgstr "Тут не можна зменшити об'єм нерозпізнаних даних"
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "Тут не можна зменшити об'єм нерозпізнаних даних."
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+msgid "Unsupported logical volume"
+msgstr "Непідтримуваний логічний том"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "До"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "Ненадійний вузол"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "Оновити"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "Таблиця успіху оновлення"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "Історія оновлень"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "Оновити дані щодо пакунків"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "Успішне оновлення"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "Оновлено"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr "Використання оновлених пакунків може потребувати перезапуску."
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "Доступні оновлення"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "Журнал оновлень"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "Оновлення буде застосовано $0 о $1"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "Оновлення"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "Оновлюємо стан…"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "Тривалість роботи"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "Використання"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "Вживання $0"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "Використати"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "Скористатися стисканням"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "Скористатися скасуванням дублювання"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "Використати ключ"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "Використати пароль"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "Використати вказані нижче ключі для розпізнавання у інших системах"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "Скористатися докладним журналюванням"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "Використано"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr ""
+"Корисно для монтувань, які є необов'язковими або потребують взаємодії "
+"(зокрема паролів)"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "Користувач"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "Ід. користувача"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "Ідентифікатор користувача вже використано для іншого користувача"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "Ідентифікатор користувача має бути додатним цілим числом"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "Ідентифікатор користувача має бути числом, що перевищує $0"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "Ідентифікатор користувача не повинен бути меншим за $0"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "Ім'я користувача"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "Ім’я користувача не може бути порожнім"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "Користувач"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "З використанням шифрування LUKS"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "З використанням сервера Tang"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "Пристрої резервного копіювання VDO не можна звужувати"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "Пристрій VDO $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "Том файлової системи VDO (стискання/дедублікація)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "Ід. VLAN"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "Перевіряємо чинність адреси"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "Перевіряємо ключ розпізнавання"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "Постачальник"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "Перевірено"
+
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "Перевірити відбиток"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "Перевірити ключ"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "Перевіряємо"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "Версія"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "Дуже безпечно витираємо $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "Переглянути усі процесори"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "Переглянути усі диски"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "Переглянути усі журнали"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "Переглянути усі служби"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "Переглянути скрипт автоматизації"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "Переглянути докладні журнали"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "Переглянути параметри обладнання"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "Переглянути журнал входів"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "Переглянути журнали"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "Переглянути метрику і журнал"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "Переглянути пропускну здатність за дисками"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "Переглянути сайт проєкту"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "Переглянути звіт"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "Переглянути журнал оновлення"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "Перегляд даних щодо пам'яті потребує адміністративного доступу."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "Відвідати брандмауер"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "Група томів"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+msgid "Volume group is missing physical volumes"
+msgstr "У групі томів пропущено фізичні томи"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "Розмір тому — $0. Розмір даних — $1."
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "Очікування"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "Очікуємо на подробиці…"
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "Очікуємо на вхідні дані…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr ""
+"Очікуємо на завершення роботи з програмою для керування пакунками інших "
+"програм…"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "Очікуємо на завершення інших дій із програмним забезпеченням"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "Очікуємо на запуск…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "Бажаний для"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "Бажає"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Попередження і вище"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+msgid "Weak password"
+msgstr "Простий пароль"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Вебконсоль"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Вебконсоль для серверів під керуванням Linux"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Вебконсоль буде перезапущено"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Вебконсоль працює у режимі обмеженого доступу."
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Логотип вебконсолі"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "Середи"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "Щотижня"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "Тижні"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "Якщо"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "Якщо не вказано, з'єднатися із поточним користувачем"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"Після перезапуску вебконсолі ви не зможете більше бачити даних щодо поступу. "
+"Втім, процес оновлення триватиме у фоновому режимі. Встановіть з'єднання "
+"повторно, щоб продовжити спостереження за процесом оновлення."
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"Якщо позначено цей пункт, новий буфер не дозволятиме надмірного "
+"резервування. Вам потрібно буде вказати максимальний розмір для кожної "
+"файлової системи, яку буде створено у буфері. Після створення не можна буде "
+"збільшувати розмір файлових систем. Сума усіх максимальних розмірів не може "
+"перевищувати розмір буфера. Перевагою цього варіанта є те, що файлові "
+"системи у буфері не зможуть збільшувати свої розміри у непередбачений "
+"спосіб. Недоліком є те, що вам наперед потрібно знати максимальний розмір "
+"кожної файлової системи, а можливості створення знімків буде обмежено."
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "Білий"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "Буде встановлено значення «Автоматично»"
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+msgid "World wide name"
+msgstr "World wide name"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "Запис"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "Здебільшого запис"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "Запис"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "Помилкове ім’я користувача чи пароль"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "Щорічно"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "Так"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "Ви вперше встановлюєте з'єднання із $0."
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "Вас не уповноважено на внесення змін до брандмауера."
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr ""
+"У вас немає прав доступу для перегляду уповноважених відкритих ключів для "
+"цього облікового запису."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "Вашого користувача було викинуто із системи через неактивність."
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "Ви можете спробувати завантажити старіші записи."
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr "Ви можете змінити пароль до ключа для автоматичного входу до системи."
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "Для зміни вашого пароля вам доведеться ще почекати"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr ""
+"Вам слід повторно увійти до системи, щоб мати змогу переглядати журнал "
+"метрики"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "Тепер ви маєте адміністративний доступ."
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "Вашого користувача буде викинуто з системи за $0 секунд."
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "Ваш обліковий запис"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr ""
+"У вашій програмі для перегляду не передбачено можливості вставлення з "
+"контекстного меню. Ви можете скористатися для вставлення комбінацією "
+"Shift+Insert."
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "Ваш браузер запам'ятовуватиме ваш рівень доступу між сеансами."
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr ""
+"Невдовзі ваш сервер розірве з’єднання. Ви можете відновити це з’єднання, "
+"щойно сервер буде перезапущено."
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "Ваш сеанс перервано."
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr ""
+"Строк роботи у вашому сеансі вичерпано. Будь ласка, увійдіть до системи ще "
+"раз."
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "Зона"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[двійкові дані]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[немає даних]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "доступ"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "активний"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "додаток"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "addon"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "після мережі"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "програми"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "мітка активу"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "на"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "середнє: $0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "резервний пристрій для пристрою VDO"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "прив’язка"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "завантаження"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "місток"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs device"
+msgstr "Інші пристрої"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+#, fuzzy
+#| msgid "Other devices"
+msgid "btrfs devices"
+msgstr "Інші пристрої"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs subvolume"
+msgstr "Том сховища даних"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr ""
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+#, fuzzy
+#| msgid "Storage volumes"
+msgid "btrfs subvolumes"
+msgstr "Томи даних"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+#, fuzzy
+#| msgid "Storage volume"
+msgid "btrfs volume"
+msgstr "Том сховища даних"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "виправлення вад"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "байтів"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+msgid "cache"
+msgstr "кеш"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "cgroups"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "змінити"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "cockpit-podman не встановлено"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "команда"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "консоль"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "дамп ядра"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "процесор"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "аварія"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+msgid "data"
+msgstr "дані"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "дата"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+msgid "deactivate"
+msgstr "вимкнути"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "діагностика"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "вилучити"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr ""
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "вимкнути"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "диск"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "диски"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "домен"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "диск"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "редагувати"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "увімкнути"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+msgid "encrypted"
+msgstr "зашифрований"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "шифрування"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "поліпшення"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "помилка"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "кожної п'ятниці"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "кожного понеділка"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "кожної суботи"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "кожної неділі"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "кожного четверга"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "кожного вівторка"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "кожної середи"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "кожного дня"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "розширення"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "не вдалося побудувати список ключів SSH вузла: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "файлова система"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "брандмауер"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "для поточного і майбутніх ядер"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "лише для поточного ядра"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "формат"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "з $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "з $0 на $1"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "графіки"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "збільшити"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "обладнання"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "журнал"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "вузол"
+
+#: pkg/storaged/drive/drive.jsx:65
+msgid "iSCSI Drive"
+msgstr "Диск iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+msgid "iSCSI drives"
+msgstr "Диски iSCSI"
+
+#: pkg/storaged/iscsi/session.jsx:45
+msgid "iSCSI portal"
+msgstr "Портал iSCSI"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "ігнорувати помилки"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "у більшості браузерів"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "несумісний"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "ініціалізувати"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "встановити"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "інтерфейс"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "помилка: визначено декілька цілей"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "журнал"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "сторінка підручника з journalctl"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "стан kdump"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "ключі"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "вхід"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "машина"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "маска"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "макс.: $0%"
+
+#: pkg/storaged/dialog.jsx:1111
+msgid "member of MDRAID device"
+msgstr "елемент пристрою MDRAID"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "частина буфера Stratis"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "пам'ять"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "метрика"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "обхід"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "докладніше"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "монтування"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "мережа"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "ніколи не монтувати при завантаженні"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "Експортовані дані nfs є порожніми"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "Сервер nfs є порожнім"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "Сервер nfs не є коректним сервером IPv6"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "пріоритетність"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "немає"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "з $0 процесора"
+msgstr[1] "з $0 процесорів"
+msgstr[2] "з $0 процесорів"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "на $0"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "операційна система"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "ос"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "пакунок"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "розділ"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "пароль"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "якість пароля"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "латки"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "шлях"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "швидкодія"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "фізичний том групи томів LVM2"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "додаток"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "помилка pmlogger.service"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service не вдалося зібрати дані"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service не запущено"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "кокон"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "порт"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "живлення"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "лише читання"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "найліпший"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "вилучити з LVM2"
+
+#: pkg/storaged/utils.js:934
+msgid "remove from MDRAID"
+msgstr "вилучити з MDRAID"
+
+#: pkg/storaged/utils.js:904
+#, fuzzy
+#| msgid "remove from LVM2"
+msgid "remove from btrfs volume"
+msgstr "вилучити з LVM2"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "перезапуск"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "ролі"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "запущено $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "безпека"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "послідовний"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "служба"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "оболонка"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "показати менше"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "показати більше"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "стиснути"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "вимкнути"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "сокет"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "рішення"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "параметри рішення"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "не вдалося виконати звітування для порятунку"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ключ ssh не вказано у форматі шляху"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "запис сервера ssh є порожнім"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "зупинити"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "припинити завантаження при помилці"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+msgid "stopped"
+msgstr "зупинено"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "ціль"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "команда"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "time"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "секундомір"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "модуль"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "невідомо"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "невідоме призначення"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "unmask"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "демонтувати"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "нерозподілене місце на $0"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "користувач"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "користувач"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "з використанням опису ключа $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "версія"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "том"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "попередження"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "Не встановлено пакунок wireguard-tools"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "так"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "зона"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr ""
+#~ "Не встановлено службу Kdump. Будь ласка, переконайтеся, що у системі "
+#~ "встановлено пакунок kexec-tools."
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "Немає зарезервованої пам’яті. Додайте параметр crashkernel до командного "
+#~ "рядка ядра (наприклад у /etc/default/grub), щоб зарезервувати пам’ять під "
+#~ "час завантаження. Приклад: crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "Службу запущено"
+
+#~ msgid "Service is starting"
+#~ msgstr "Служба запускається"
+
+#~ msgid "Service is stopped"
+#~ msgstr "Службу зупинено"
+
+#~ msgid "Service is stopping"
+#~ msgstr "Служба зупиняється"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "Перевіряє налаштування kdump, аварійно завершуючи роботу ядра."
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "у рядку команди ядра не налаштовано crashkernel"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0 (зашифровано)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 буфер Stratis"
+
+#~ msgid "$0 cache"
+#~ msgstr "Кеш $0"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 невідомого класу"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "$0, вільно $1"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "Перш ніж вилучати цей диск, слід додати резервний диск."
+
+#~ msgid "Backing device"
+#~ msgstr "Резервний пристрій"
+
+#~ msgid "Block"
+#~ msgstr "Блок"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "Місткість"
+
+#~ msgid "Content"
+#~ msgstr "Вміст"
+
+#~ msgid "Create devices"
+#~ msgstr "Створити пристрої"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "Пристрій"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "Файл пристрою"
+
+#~ msgid "Devices"
+#~ msgstr "Пристрої"
+
+#~ msgid "Drives"
+#~ msgstr "Диски"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr ""
+#~ "Перш ніж можна буде змінювати розмір зашифрованих томів, такі томи слід "
+#~ "розблокувати."
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "Файлова система (зашифровано)"
+
+#~ msgid "Filesystems"
+#~ msgstr "Файлові системи"
+
+#~ msgid "Free"
+#~ msgstr "Вільно"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "Звільніть місце у цій групі: зменшіть розмір інших логічних томів або "
+#~ "вилучіть їх чи додайте ще один фізичний том."
+
+#~ msgid "Inactive volume"
+#~ msgstr "Неактивний том"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "Сервер ключів"
+
+#~ msgid "LVM2 member"
+#~ msgstr "Учасник LVM2"
+
+#~ msgid "Locked devices"
+#~ msgstr "Заблоковані пристрої"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "Заблоковано зашифровані дані"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "Модель"
+
+#~ msgid "NFS mounts"
+#~ msgstr "Монтування NFS"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "Не встановлено підтримки NFS"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "Монтувань NFS не налаштовано"
+
+#~ msgid "No devices"
+#~ msgstr "Немає пристроїв"
+
+#~ msgid "No drives attached"
+#~ msgstr "Не долучено жодного диска"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "Призначень iSCSI не налаштовано"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "Недостатньо місця для нових файлових систем"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "Інші дані"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "Поділений на розділи блоковий пристрій"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "Пароль"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "Тут не можна змінювати розміри фізичних томів."
+
+#~ msgid "Pool"
+#~ msgstr "Буфер"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "Буфер для тонких томів"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "Рівень RAID"
+
+#~ msgid "RAID member"
+#~ msgstr "Елемент RAID"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "Портативний диск"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "Показати усі $0 пристрій"
+#~ msgstr[1] "Показати усі $0 пристрої"
+#~ msgstr[2] "Показати усі $0 пристроїв"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "Показати увесь $0 диск"
+#~ msgstr[1] "Показати усі $0 диски"
+#~ msgstr[2] "Показати усі $0 дисків"
+
+#~ msgid "Show all"
+#~ msgstr "Показати усі"
+
+#~ msgid "Source"
+#~ msgstr "Джерело"
+
+#~ msgid "Start pool"
+#~ msgstr "Запустити буферизацію"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "Запустіть буфер, щоб побачити файлові системи."
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "Стан"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "Зупинений буфер Stratis"
+
+#~ msgid "Stratis member"
+#~ msgstr "Учасник Stratis"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Буфер Stratis $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "Підтримку встановлено."
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr "Для додавання резервних дисків має працювати пристрій RAID."
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "Останній диск пристрою RAID вилучати не можна."
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "Не можна вилучати останній фізичний том із групи томів."
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "Для вилучення цього фізичного тому недостатньо вільного місця. Потрібно "
+#~ "принаймні $0 вільного місця."
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr ""
+#~ "Цей диск не можна вилучати, доки пристрій перебуває у стані відновлення."
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr ""
+#~ "Перш ніж розмір цього тому можна буде змінювати, його слід активувати."
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "Нерозпізнані дані"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "Використання"
+
+#~ msgid "Used for"
+#~ msgstr "Використано для"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "Резерв VDO"
+
+#~ msgid "VDO device"
+#~ msgstr "Пристрій VDO"
+
+#~ msgid "Volume"
+#~ msgstr "Том"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "Автоматично (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "Прийняти ключ і з'єднатися"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "Тут не можна змінювати розмір шифрованих томів."
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "Використати сервер ключів Tang"
+
+#~ msgid "Use a passphrase"
+#~ msgstr "Використати пароль"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Бітова карта"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr ""
+#~ "Цей буфер не можна розблокувати тут, оскільки його опис ключа записано не "
+#~ "в очікуваному форматі."
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "Перемкнути бітову карту"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr ""
+#~ "Переконайтеся, що хеш ключа з сервера Tang відповідає одному з таких "
+#~ "критеріїв:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "Перевірка вручну за допомогою SSH: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "Номер порту і тип не є сумісними"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "Заблоковано зашифрований буфер Stratis"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "Помилка під час вилучення нагадування: $0"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "Не вдалося отримати нагадування: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 байтів двійкових даних]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "Пік дискового введення-виведення"
+
+#~ msgid "Event"
+#~ msgstr "Подія"
+
+#~ msgid "Event logs"
+#~ msgstr "Журнал подій"
+
+#~ msgid "Load spike"
+#~ msgstr "Завантажити пік"
+
+#~ msgid "Memory spike"
+#~ msgstr "Пік пам'яті"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "Пік входу-виходу мережі"
+
+#~ msgid "ssh key"
+#~ msgstr "ключ ssh"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "Якщо позначено цей пункт, файлову систему не буде змонтовано під час "
+#~ "наступного завантаження, навіть якщо раніше система її монтувала. Це "
+#~ "корисно, якщо монтування під час завантаження неможливе, зокрема, коли "
+#~ "для розблоковування файлової системи потрібен пароль, а завантаження "
+#~ "виконується в автоматичному режимі."
+
+#~ msgid "Never mount at boot"
+#~ msgstr "Ніколи не монтувати при завантаженні"
+
+#~ msgid "Listing unit files"
+#~ msgstr "Виведення списку файлів модулів"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "Не вдалося вивести список файлів модулів: $0"
+
+#~ msgid "Unit not found"
+#~ msgstr "Модуль не знайдено"
+
+#~ msgid "Validating key"
+#~ msgstr "Перевірка ключа"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "Вилучити групу $0"
+
+#~ msgid "Container administrator"
+#~ msgstr "Адміністратор контейнера"
+
+#~ msgid "Image builder"
+#~ msgstr "Побудова образів"
+
+#~ msgid "Roles"
+#~ msgstr "Ролі"
+
+#~ msgid "Server administrator"
+#~ msgstr "Адміністратор сервера"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Група у Unix: $0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "Успішно скопійовано до клавіатури"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "Діагностичні звіти"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "Дамп ядра"
+
+#~ msgid "Software Updates"
+#~ msgstr "Оновлення програм"
+
+#~ msgid "Update log"
+#~ msgstr "Журнал оновлень"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr ""
+#~ "форматування місця для дампів у nfs не збігається із потрібним: сервер:"
+#~ "шлях"
+
+#~ msgid "$0 Zone"
+#~ msgstr "Зона $0"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "Створити діагностичний звіт"
+
+#~ msgid "Done!"
+#~ msgstr "Готово!"
+
+#~ msgid "Download report"
+#~ msgstr "Звіт щодо отримання"
+
+#~ msgid "No archive has been created."
+#~ msgstr "Архів не було створено."
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "Будь ласка, підтвердьте вилучення $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "У створеному архіві містяться дані, які є конфіденційними. Його вміст має "
+#~ "бути переглянути організацією походження, перш ніж архів буде передано "
+#~ "стороннім особам."
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr ""
+#~ "Ця програма збере дані щодо налаштувань системи і діагностичні дані з "
+#~ "цієї системи для виявлення причин проблем у системі."
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "На сервері не встановлено пакунок cockpit-system"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "Цей пакунок несумісний із цією версією Cockpit"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "Для цього пакунка потрібна версія Cockpit %s або новіша"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "Метрика швидкодії"
+
+#~ msgid "Reboot to apply new crypto policy"
+#~ msgstr "Перезавантажте систему, щоб застосувати нові правила шифрування"
+
+#~ msgid "Save only"
+#~ msgstr "Лише збереження"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "Загалом $0 ГіБ"
+
+#~ msgid "Apply"
+#~ msgstr "Застосувати"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "Не уповноважено вилучати зону $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "Натисніть $0 ще раз, якщо слід використовувати саме цей пароль."
+
+#~ msgid "Access"
+#~ msgstr "Доступ"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "ДИСК НЕПРАЦЕЗДАТНИЙ"
+
+#~ msgid "Logical Size"
+#~ msgstr "Логічний розмір"
+
+#~ msgid "Security updates "
+#~ msgstr "Оновлення захисту "
+
+#~ msgid "Updates "
+#~ msgstr "Оновлення "
+
+#~ msgid "(Optional)"
+#~ msgstr "(Необов’язково)"
+
+#~ msgid "Active since"
+#~ msgstr "Активний з"
+
+#~ msgid "Affected locations"
+#~ msgstr "Задіяні місця"
+
+#~ msgid "Process"
+#~ msgstr "Процес"
+
+#~ msgid "Remove and delete"
+#~ msgstr "Вилучити і витерти"
+
+#~ msgid "Remove and format"
+#~ msgstr "Вилучити і форматувати"
+
+#~ msgid "Remove and grow"
+#~ msgstr "Вилучити і збільшити"
+
+#~ msgid "Remove and initialize"
+#~ msgstr "Вилучити і ініціалізувати"
+
+#~ msgid "Remove and shrink"
+#~ msgstr "Вилучити і зменшити"
+
+#~ msgid "Remove and stop device"
+#~ msgstr "Вилучити і зупинити роботу пристрою"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr ""
+#~ "Файлова система використовується службами системи або сеансами "
+#~ "користувачів. Виконання цієї дії призведе до припинення роботи цих служб "
+#~ "та сеансів."
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr ""
+#~ "Файлова система використовується сеансами користувачів. Виконання цієї "
+#~ "дії призведе до припинення роботи цих сеансів."
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr ""
+#~ "Файлова система використовується службами системи. Виконання цієї дії "
+#~ "призведе до припинення роботи цих служб."
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr ""
+#~ "На цьому пристрої міститься файлова система, яка зараз використовується. "
+#~ "Виконання дії призведе до демонтування усіх файлових систем на пристрої."
+
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "Цей пристрій зараза використовується для груп томів LVM2."
+
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr ""
+#~ "Цей пристрій зараз використовується для груп томів LVM2. Якщо дію буде "
+#~ "виконано, пристрій буде вилучено із його груп томів."
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "Цей пристрій зараз використовується для пристроїв RAID."
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr ""
+#~ "Цей пристрій зараз використовується для формування пристроїв RAID. Якщо "
+#~ "дію буде виконано, пристрій буде вилучено із його пристроїв RAID."
+
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "Цей пристрій зараза використовується для буферів Stratis."
+
+#~ msgid "Unmount and delete"
+#~ msgstr "Демонтувати і витерти"
+
+#~ msgid "Unmount and format"
+#~ msgstr "Демонтувати і форматувати"
+
+#~ msgid "Unmount and grow"
+#~ msgstr "Демонтувати і збільшити"
+
+#~ msgid "Unmount and initialize"
+#~ msgstr "Демонтувати і ініціалізувати"
+
+#~ msgid "Unmount and shrink"
+#~ msgstr "Демонтувати і зменшити"
+
+#~ msgid "Unmount and stop device"
+#~ msgstr "Демонтувати і зупинити роботу пристрою"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$0 з $1"
+
+#~ msgid "Blockdev"
+#~ msgstr "Блоковий пристрій"
+
+#~ msgid "Blockdev of Stratis pool $0"
+#~ msgstr "Блоковий пристрій буфера Stratis $0"
+
+#~ msgid "Blockdev of locked Stratis pool $0"
+#~ msgstr "Блоковий пристрій заблокованого буфера Stratis $0"
+
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "Фізичний том LVM2 $0"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "Елемент пристрою RAID $0"
+
+#~ msgid "Menu"
+#~ msgstr "Меню"
+
+#~ msgid "VDO backing"
+#~ msgstr "Резерв VDO"
+
+#~ msgid "Create Stratis Pool"
+#~ msgstr "Створити буфер Stratis"
+
+#~ msgid "Mount Options"
+#~ msgstr "Параметри монтування"
+
+#~ msgid "Stratis Pool"
+#~ msgstr "Буфер Stratis"
+
+#~ msgid "A disk is needed."
+#~ msgstr "Потрібен диск."
+
+#~ msgid "Disk"
+#~ msgstr "Диск"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "Лише для застарілих програм. Знижує швидкодію."
+
+#~ msgid "Install VDO support"
+#~ msgstr "Встановити підтримку VDO"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "Емуляція 512 байтів"
+
+#~ msgid "System services"
+#~ msgstr "Служби системи"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "Створити діагностичний звіт "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr ""
+#~ "Підтримки одночасного з’єднання із понад {{ limit }} комп’ютерами не "
+#~ "передбачено."
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "SSO на основі Kerberos"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "Вхід до {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "Нові паролі до ключа не збігаються"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "Щоб перевірити відбиток, віддайте вказану нижче команду для {{host}} під "
+#~ "час безпосередньої роботи на комп'ютері або з використанням надійної "
+#~ "мережі:"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Не вдалося зв'язатися із {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "Не вдалося увійти до {{#strong}}{{host}}{{/strong}}. Щоб отримати доступ "
+#~ "до ширшого спектра способів розпізнавання та до підтримки діагностики "
+#~ "помилок, будь ласка, оновіть cockpit-ws."
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "Не вдалося увійти до {{#strong}}{{host}}{{/strong}}. Щоб з'єднатися із "
+#~ "цим вузлом, вам слід увімкнути вказані нижче способи розпізнавання у "
+#~ "налаштуваннях sshd на {{#strong}}{{host}}{{/strong}}:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "Ви вперше встановлюєте з'єднання із {{host}}."
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "Змінено ключ {{host}}"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "Вилучити налаштування точки монтування"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Створення цього зв’язку призведе до розірвання з’єднання із сервером і "
+#~ "зробить адміністративний інтерфейс користувача недоступним."
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr ""
+#~ "Створення цього містка призведе до розірвання з’єднання із сервером і "
+#~ "зробить адміністративний інтерфейс користувача недоступним."
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr ""
+#~ "Створення цієї команди призведе до розірвання з’єднання із сервером і "
+#~ "зробить адміністративний інтерфейс користувача недоступним."
+
+#~ msgid "IP settings"
+#~ msgstr "Параметри IP"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr ""
+#~ "Вимикання <b>$0</b> призведе до розірвання з’єднання із сервером і "
+#~ "зробить адміністративний інтерфейс користувача недоступним."
+
+#~ msgid "Administrator password"
+#~ msgstr "Пароль адміністратора"
+
+#~ msgid "Computer OU"
+#~ msgstr "OU комп’ютера"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "Вилучення пристрою RAID призведе до витирання з нього усіх даних."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr ""
+#~ "Вилучення групи томів призведе до витирання усіх даних, що у ній "
+#~ "зберігаються."
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "Не перезаписувати наявні дані"
+
+#~ msgid "Erase"
+#~ msgstr "Витерти"
+
+#~ msgid "Format disk $0"
+#~ msgstr "Форматувати диск $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr ""
+#~ "Форматування пристрою для зберігання даних призведе до знищення даних, "
+#~ "які на ньому зберігаються."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "У домені не слід змінювати назву вузла"
+
+#~ msgid "More"
+#~ msgstr "Більше"
+
+#~ msgid "Not joined"
+#~ msgstr "Не об'єднано"
+
+#~ msgid "One time password"
+#~ msgstr "Одноразовий пароль"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 з $1 на $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "Години слід вказувати у форматі числа від 0 до 23"
+
+#~ msgid "Last login:"
+#~ msgstr "Останній вхід:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "Хвилини слід вказувати у форматі числа від 0 до 59"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] ""
+#~ "З часу останнього успішного входу до системи сталася $0 невдала спроба "
+#~ "входу."
+#~ msgstr[1] ""
+#~ "З часу останнього успішного входу до системи сталася $0 невдалих спроби "
+#~ "входу."
+#~ msgstr[2] ""
+#~ "З часу останнього успішного входу до системи сталася $0 невдалих спроб "
+#~ "входу."
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "наприклад «$0»"
+
+#~ msgid "$0 active zones"
+#~ msgstr "Активні зони $0"
+
+#~ msgid "Dismiss $0 alerts"
+#~ msgstr "Відкинути $0 попереджень"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 відповідник"
+#~ msgstr[1] "$0 відповідники"
+#~ msgstr[2] "$0 відповідників"
+
+#~ msgid "Licensed under:"
+#~ msgstr "Умови ліцензування:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "Розблокувати при завантаженні"
+
+#~ msgid "Unlock read only"
+#~ msgstr "Розблокувати лише для читання"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "Шукати у журналі за такою комбінацією ключів:"
+
+#~ msgid "Show filters"
+#~ msgstr "Показати фільтри"
+
+#~ msgid "Text"
+#~ msgstr "Текст"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "будь-який рядок довільного формату як формальний вираз"
+
+#~ msgid "e.g."
+#~ msgstr "наприклад"
+
+#~ msgid "log fields"
+#~ msgstr "поля журналу"
+
+#~ msgid "qualifiers"
+#~ msgstr "уточнювачі"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "Перемкнути параметри сеансу"
+
+#~ msgid "(none)"
+#~ msgstr "(немає)"
+
+#~ msgid "Create timers"
+#~ msgstr "Створити таймери"
+
+#~ msgid "Recommended default"
+#~ msgstr "Рекомендоване типове значення"
+
+#~ msgid "Run"
+#~ msgstr "Запустити"
+
+#~ msgid "Select unit state"
+#~ msgstr "Виберіть стан модуля"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "Увімкнути збережену метрику"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "Увімкнути збережену метрику"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "Примусово вилучити пароль у $0"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "Якщо програма tang-show-keys є недоступною, віддайте таку команду:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "Що робити, якщо tang-show-keys є недоступною?"
+
+#~ msgid "key slot $0"
+#~ msgstr "слот ключів $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "Щось пішло не так"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "Це спрацювало, будь ласка, повторіть спробу"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "Ви не можете набути прав адміністративного доступу."
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "Автоматичні оновлення не налаштовано"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "Налаштування процесора $0"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 мережа"
+#~ msgstr[1] "$0 мережі"
+#~ msgstr[2] "$0 мереж"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 можна користуватися у більшості операційних систем. Щоб встановити "
+#~ "пакунок із програмою, знайдіть відповідний запис у Програмних засобах "
+#~ "GNOME або віддайте таку команду:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "Коригування пам'яті $0"
+
+#~ msgid "$0 network"
+#~ msgstr "Мережа $0"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 вірт. проц."
+#~ msgstr[1] "$0 вірт. проц."
+#~ msgstr[2] "$0 вірт. проц."
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "Подробиці щодо віртуального процесора $0"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "Параметри інтерфейсу віртуальної мережі $0"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "Активувати буфер зберігання даних для адміністрування томів"
+
+#~ msgid "Add network interface"
+#~ msgstr "Додати інтерфейс мережі"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "Додати інтерфейс віртуальної мережі"
+
+#~ msgid "Additional"
+#~ msgstr "Додаткові"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "Адреса поза межами підмережі"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr "Після вилучення знімка, усі захоплені у ньому дані буде втрачено."
+
+#~ msgid "Always attach"
+#~ msgstr "Завжди долучати"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr ""
+#~ "Долучення диска надасть доступ до нього для усіх віртуальних машин, які "
+#~ "ним користуватимуться."
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "Автоматично запускати libvirt при завантаженні"
+
+#~ msgid "Autostart"
+#~ msgstr "Автозапуск"
+
+#~ msgid "Boot order"
+#~ msgstr "Порядок завантаження"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "Не вдалося зберегти параметри порядку завантаження"
+
+#~ msgid "Bus"
+#~ msgstr "Канал"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "Диск CD/DVD"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "Не вдалося зберегти параметри процесорів"
+
+#~ msgid "CPU type"
+#~ msgstr "Тип процесора"
+
+#~ msgid "Change firmware"
+#~ msgstr "Змінити мікропрограму"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "Зміни буде застосовано після завершення роботи ВМ"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "Виберіть операційну систему"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr ""
+#~ "У результаті натискання «Запустити віддалений переглядач» буде отримано "
+#~ "файл .vv і запущено $0."
+
+#~ msgid "Clone"
+#~ msgstr "Клонувати"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "Не вдалося завантажити образ"
+
+#~ msgid "Confirm this action"
+#~ msgstr "Підтвердьте цю дію"
+
+#~ msgid "Connect"
+#~ msgstr "З'єднатися"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr ""
+#~ "З’єднатися із будь-якою програмою перегляду для вказаних нижче протоколів"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "Встановлюємо зв'язок із службою віртуалізації"
+
+#~ msgid "Connection"
+#~ msgstr "З’єднання"
+
+#~ msgid "Console"
+#~ msgstr "Консоль"
+
+#~ msgid "Cores per socket"
+#~ msgstr "Кількість ядер на сокет"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "Не вдалося скинути систему до знімка"
+
+#~ msgid "Crashed"
+#~ msgstr "Аварійне завершення"
+
+#~ msgid "Create VM"
+#~ msgstr "Створення ВМ"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "Створити клон ВМ на основі $0"
+
+#~ msgid "Create new"
+#~ msgstr "Створити"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "Створити віртуальну машину"
+
+#~ msgid "Create virtual network"
+#~ msgstr "Створити віртуальну мережу"
+
+#~ msgid "Creating VM"
+#~ msgstr "створення ВМ"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "створення встановленої ВМ"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "Не вдалося створити ВМ $0"
+
+#~ msgid "Creation time"
+#~ msgstr "Час створення"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "Поточне отримання"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "Нетипова мікропрограма: $0"
+
+#~ msgid "DHCP range"
+#~ msgstr "Діапазон DHCP"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "Вилучити пов’язані файли у сховищі даних:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "Вилучити буфер зберігання даних $0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "Вилучити томи у цьому буфері"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr ""
+#~ "Вилучення неактивного буфера зберігання даних призведе лише до скасування "
+#~ "визначення буфера. Вміст буфера вилучено не буде."
+
+#~ msgid "Desktop viewer"
+#~ msgstr "Стільничний переглядач"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr ""
+#~ "Від'єднайте диски, які використовують цей буфер від усіх віртуальних "
+#~ "машин, перш ніж намагатися їх вилучити."
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "Від'єднано від послідовної консолі. Натисніть кнопку «З'єднатися»."
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "Не вдалося від'єднати диск $0 від віртуальної машини $1"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "Не вдалося долучити диск"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "Не вдалося створити диск"
+
+#~ msgid "Disk image file"
+#~ msgstr "Файл образу диска"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "Не вдалося зберегти параметри диска"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "Знімок лише на диску"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "Домен завершив роботу аварійно"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "Домен заблоковано на ресурсі"
+
+#~ msgid "Download an OS"
+#~ msgstr "Отримати операційну систему"
+
+#~ msgid "Dying"
+#~ msgstr "Вмирає"
+
+#~ msgid "Emulated machine"
+#~ msgstr "Емульована машина"
+
+#~ msgid "End"
+#~ msgstr "Кінець"
+
+#~ msgid "End should not be empty"
+#~ msgstr "Кінець не може бути порожнім"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "Наявний образ диска у файловій системі основної системи"
+
+#~ msgid "Expand"
+#~ msgstr "Розгорнути"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "Не вдалося змінити мікропрограму"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "Не вдалося отримати IP-адреси інтерфейсів, які є у $0"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "Не вдалося надіслати комбінацію клавіш Ctrl+Alt+$0 до ВМ $1"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr ""
+#~ "Має бути увімкнено менше за максимальну кількість віртуальних процесорів."
+
+#~ msgid "File"
+#~ msgstr "Файл"
+
+#~ msgid "Filter by name"
+#~ msgstr "Фільтрувати за назвою"
+
+#~ msgid "Firmware"
+#~ msgstr "Мікропрограма"
+
+#~ msgid "Force shut down"
+#~ msgstr "Примусово вимкнути"
+
+#~ msgid "Forward mode"
+#~ msgstr "Режим переспрямовування"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "Режим переспрямовування"
+
+#~ msgid "Generate automatically"
+#~ msgstr "Створити автоматично"
+
+#~ msgid "GiB"
+#~ msgstr "ГіБ"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "Перейти до списку ВМ"
+
+#~ msgid "Hide additional options"
+#~ msgstr "Приховати додаткові параметри"
+
+#~ msgid "Host device"
+#~ msgstr "Пристрій основної системи"
+
+#~ msgid "Host name"
+#~ msgstr "Назва вузла"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "Вузол не повинен бути порожнім"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Докладніше про гіпервізор"
+
+#~ msgid "IP configuration"
+#~ msgstr "Налаштування IP"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 і IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "Мережа IPv4"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "Мережа IPv4 не повинна бути порожньою"
+
+#~ msgid "IPv4 only"
+#~ msgstr "Лише IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "Адреса IPv6"
+
+#~ msgid "IPv6 network"
+#~ msgstr "Мережа IPv6"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "Мережа IPv6 не повинна бути порожньою"
+
+#~ msgid "IPv6 only"
+#~ msgstr "Лише IPv6"
+
+#~ msgid "Idle"
+#~ msgstr "Бездіяльність"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "Негайно запустити ВМ"
+
+#~ msgid "Import"
+#~ msgstr "Імпортувати"
+
+#~ msgid "Import VM"
+#~ msgstr "Імпортувати ВМ"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "Імпортувати віртуальну машину"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr ""
+#~ "У більшості конфігурацій macvtap не працює для обміну мережею між "
+#~ "основною і гостьовою системами."
+
+#~ msgid "Initiator"
+#~ msgstr "Ініціатор"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "IQN ініціатора має бути непорожнімy"
+
+#~ msgid "Installation source"
+#~ msgstr "Джерело для встановлення"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "Запис джерела встановлення не може бути порожнім"
+
+#~ msgid "Installation type"
+#~ msgstr "Тип встановлення"
+
+#~ msgid "Interface type"
+#~ msgstr "Тип інтерфейсу"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "Некоректна довжина маски IPv4 або префікса"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "Некоректна адреса IPv6"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "Некоректний префікс IPv6"
+
+#~ msgid "Invalid filename"
+#~ msgstr "Некоректна назва файла"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "Запустити віддалений переглядач"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr ""
+#~ "Не заповнюйте поле пароля, якщо ви не хочете, щоб було створено обліковий "
+#~ "запис root"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr ""
+#~ "Не заповнюйте поле пароля, якщо ви не хочете, щоб було створено обліковий "
+#~ "запис користувача"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr ""
+#~ "Не заповнюйте поле пароля, якщо ви не хочете, щоб було створено обліковий "
+#~ "запис root"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr ""
+#~ "Libvirt не вдалося виявити жодного образу мікропрограми UEFI/OVMF, "
+#~ "встановлено у основній системі"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "У libvirt або гіпервізорі не передбачено підтримки UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "Завантаження ресурсів"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "До внесення змін до типу каналу машину слід вимкнути"
+
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "До внесення змін до режиму кешування машину слід вимкнути"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "Керування віртуальними машинами"
+
+#~ msgid "Manual connection"
+#~ msgstr "З’єднання вручну"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "Довжина маски або префікса"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "Довжина маски або префікса повинна бути непорожньою"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "Максимальне отримання"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "Не вдалося зберегти дані щодо максимального обсягу пам'яті"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr ""
+#~ "Максимальна кількість віртуальних процесорів, наданих для гостьової "
+#~ "операційної системи"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr ""
+#~ "Максимальна кількість віртуальних процесорів, наданих для гостьової "
+#~ "операційної системи, має бути у межах від 1 до $0"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "Максимальна одиниця передавання"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "Не вдалося зберегти дані щодо обсягу пам'яті"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "Об'єм пам'яті не може бути нульовим"
+
+#~ msgid "MiB"
+#~ msgstr "МіБ"
+
+#~ msgid "Model type"
+#~ msgstr "Тип моделі"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT до $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "NIC $0 ВМ $1, не вдалося змінити стан"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "У назві містяться некоректні символи"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "Запис назви не може бути порожнім"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "Назва має бути непорожньою"
+
+#~ msgid "Name: "
+#~ msgstr "Назва: "
+
+#~ msgid "Netmask"
+#~ msgstr "Маска мережі"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "Не вдалося активувати мережу $0"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "Не вдалося деактивувати мережу $0"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "Мережеве завантаження (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "Мережева файлова система"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "Не вдалося зберегти параметри інтерфейсу мережі"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "Для вибору мережі не передбачено підтримки PXE."
+
+#~ msgid "Networks"
+#~ msgstr "Мережі"
+
+#~ msgid "New volume name"
+#~ msgstr "Назва нового тому"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr ""
+#~ "У цій основній системі не запущено або не визначено віртуальних машин"
+
+#~ msgid "No connection available"
+#~ msgstr "Немає доступних з'єднань"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "Для цієї ВМ не визначено дисків"
+
+#~ msgid "No network devices"
+#~ msgstr "Немає пристроїв мережі"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "Немає інтерфейсів мережі, які визначено для цієї ВМ"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "У цій основній системі мережу не визначено"
+
+#~ msgid "No networks available"
+#~ msgstr "Немає доступних мереж"
+
+#~ msgid "No parent"
+#~ msgstr "Немає батьківського запису"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "Для цієї ВМ не визначено знімків"
+
+#~ msgid "No state"
+#~ msgstr "Немає стану"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "На цьому вузлі не визначено буфера зберігання даних"
+
+#~ msgid "No storage pools available"
+#~ msgstr "Немає доступних буферів зберігання даних"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "Для цього буфера зберігання сховища не визначено томів сховища"
+
+#~ msgid "No virtual networks"
+#~ msgstr "Немає віртуальних мереж"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr ""
+#~ "Несталу мережу не можна вилучати. Вона просто зникне, якщо її вимкнути."
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr ""
+#~ "Несталий буфер зберігання даних не можна вилучати. Він просто зникне, "
+#~ "якщо його вимкнути."
+
+#~ msgid "None (isolated network)"
+#~ msgstr "Немає (ізольована мережа)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr ""
+#~ "Один або декілька позначених томів використовуються доменами. Щоб "
+#~ "уможливити вилучення цих томів, спочатку від'єднайте диски."
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "Можна редагувати, лише якщо гостьову систему вимкнено"
+
+#~ msgid "Open"
+#~ msgstr "Відкрита"
+
+#~ msgid "Operating system"
+#~ msgstr "Операційна система"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "Виконується дія"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "Батьківський знімок"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "Шлях у файловій системі вузла"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "Шлях до ISO у файловій системі основної системи"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "Шлях до файла у файловій системі основної системи"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "Шлях до файла у файловій системі основної системи"
+
+#~ msgid "Paused"
+#~ msgstr "Призупинено"
+
+#~ msgid "Persistence"
+#~ msgstr "Сталість"
+
+#~ msgid "Physical disk device"
+#~ msgstr "Фізичний пристрій диска"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "Фізичний пристрій у основній системі"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "Будь ласка, виберіть буфер зберігання даних"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "Будь ласка, виберіть том"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "Будь ласка, введіть назву нового тому"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr ""
+#~ "Будь ласка, запустіть віртуальну машину, щоб отримати доступ до її "
+#~ "консолі."
+
+#~ msgid "Plug"
+#~ msgstr "З'єднати"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "Для створення тому буфер має бути активним"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "Для типу буфера не пердебачено підтримки створення томів"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "Томи буфера використовуються ВМ "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "Бажана кількість сокетів, які слід надавати гостьовій системі."
+
+#~ msgid "Prefix"
+#~ msgstr "Префікс"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "Довжина префікса повинна бути непорожньою"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr ""
+#~ "За допомогою раніше зроблених знімків ви зможете повернутися до "
+#~ "попереднього стану, якщо щось піде не так"
+
+#~ msgid "Product"
+#~ msgstr "Продукт"
+
+#~ msgid "Profile"
+#~ msgstr "Профіль"
+
+#~ msgid "Protocol"
+#~ msgstr "Протокол"
+
+#~ msgid "Remote URL"
+#~ msgstr "Віддалена адреса"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "Параметри віддаленого переглядача"
+
+#~ msgid "Revert"
+#~ msgstr "Повернутися"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "Повернутися до знімка $0"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "Повернення до цього знімка переведе віртуальну машину до стану на момент "
+#~ "створення знімка — поточний стан буде втрачено разом із усіма даними, які "
+#~ "не захоплено до знімка"
+
+#~ msgid "Root password"
+#~ msgstr "Пароль root"
+
+#~ msgid "Route to $0"
+#~ msgstr "Маршрут до $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "Запустити автоматичне встановлення"
+
+#~ msgid "Run when host boots"
+#~ msgstr "Запустити під час завантаження основної системи"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "Порт TLS SPICE"
+
+#~ msgid "SPICE address"
+#~ msgstr "Адреса SPICE"
+
+#~ msgid "SPICE port"
+#~ msgstr "Порт SPICE"
+
+#~ msgid "Select console type"
+#~ msgstr "Вибір типу консолі"
+
+#~ msgid "Send key"
+#~ msgstr "Надіслати ключ"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "Надіслати немасковане переривання"
+
+#~ msgid "Serial console"
+#~ msgstr "Послідовна консоль"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "Встановити діапазон DHCP"
+
+#~ msgid "Set manually"
+#~ msgstr "Встановити вручну"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr ""
+#~ "Встановлення паролів користувачів для автоматичного встановлення потребує "
+#~ "запуску віртуальної машини при створенні системи"
+
+#~ msgid "Show additional options"
+#~ msgstr "Показати додаткові параметри"
+
+#~ msgid "Shut off"
+#~ msgstr "Вимкнути"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr ""
+#~ "Вимкніть віртуальну машину, щоб розпочати редагування налаштувань "
+#~ "мікропрограми"
+
+#~ msgid "Shutting down"
+#~ msgstr "Вимикання"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "Не вдалося створити знімок"
+
+#~ msgid "Source format"
+#~ msgstr "Початковий формат"
+
+#~ msgid "Source path"
+#~ msgstr "Шлях до джерела"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "Шлях до джерела не може бути порожнім"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr ""
+#~ "Адреса джерела має починатися із назви протоколу — http, ftp або nfs"
+
+#~ msgid "Source volume group"
+#~ msgstr "Початкова група томів"
+
+#~ msgid "Start libvirt"
+#~ msgstr "Запустити libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "Запускати резервне сховище після завантаження вузла"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "Початок повинен бути непорожнім"
+
+#~ msgid "Startup"
+#~ msgstr "Запуск"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "Не вдалося активувати буфер сховища $0"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "Не вдалося деактивувати буфер сховища $0"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "Не вдалося створити резервне сховище"
+
+#~ msgid "Storage pool name"
+#~ msgstr "Назва резервного сховища"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "Розмір сховища має бути ненульовим"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr ""
+#~ "Розмір тому сховища даних не повинен перевищувати місткість буфера "
+#~ "сховища даних ($0 $1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "Не вдалося вилучити томи сховища даних"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "призупинено (PM)"
+
+#~ msgid "Target path"
+#~ msgstr "Шлях призначення"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "Шлях призначення не може бути порожнім"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr ""
+#~ "Запущено віртуальну машину. Її буде примусово зупинено перед вилученням."
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr ""
+#~ "Віртуальну машину має бути запущено або вимкнено, щоб можна було "
+#~ "від'єднати цей пристрій"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "Каталог на сервері, який експортується"
+
+#~ msgid "The pool is empty"
+#~ msgstr "Буфер порожній"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr ""
+#~ "Для вибраної операційної системи не передбачено підтримки автоматичного "
+#~ "встановлення"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr ""
+#~ "Для вибраної операційної системи мінімальним обсягом пам'яті є $0 $1"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr ""
+#~ "Для вибраної операційної системи мінімальним об'ємом пристрою накопичення "
+#~ "даних є $0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "Не вдалося вилучити буфер сховища даних"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr ""
+#~ "Ця віртуальна машина є тимчасовою. Вимкніть її, якщо хочете її вилучити."
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "Цей том вже використано: "
+
+#~ msgid "Threads per core"
+#~ msgstr "Потоків на ядро"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr ""
+#~ "Для тимчасових віртуальних машин не передбачено підтримки редагування "
+#~ "налаштувань мікропрограми"
+
+#~ msgid "Type ID"
+#~ msgstr "Ідентифікатор типу"
+
+#~ msgid "Unique name"
+#~ msgstr "Унікальна назва"
+
+#~ msgid "Unique network name"
+#~ msgstr "Унікальна назва мережі"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "Невідома мікропрограма"
+
+#~ msgid "Unplug"
+#~ msgstr "Від'єднати"
+
+#~ msgid "Up to $0 $1 available on the default location"
+#~ msgstr "У типовому місці доступно до $0 $1"
+
+#~ msgid "Up to $0 $1 available on the host"
+#~ msgstr "У основній системі доступними є до $0 $1"
+
+#~ msgid "Url"
+#~ msgstr "Адреса"
+
+#~ msgid "User login must not be empty when user password is set"
+#~ msgstr ""
+#~ "Запис користувача не може бути порожнім, якщо встановлено пароль "
+#~ "користувача"
+
+#~ msgid "User password must not be empty when user login is set"
+#~ msgstr ""
+#~ "Пароль користувача не може бути порожнім, якщо встановлено запис "
+#~ "користувача"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "Не вдалося зберегти параметри віртуальних процесорів"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "ВМ із назвою $0 вже існує"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "На з'єднання $1 не існує ВМ $0"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "Не вдалося примусово перезавантажити віртуальну машину $0"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "Не вдалося примусово вимкнути віртуальну машину $0"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "Не вдалося вилучити віртуальну машину $0"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "Не вдалося встановити віртуальну машину $0"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "Не вдалося перезавантажити віртуальну машину $0"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "Не вдалося відновити роботу віртуальної машини $0"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "Не вдалося надіслати NMI віртуальній машині $0"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "Не вдалося вимкнути віртуальну машину $0"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "Не вдалося запустити віртуальну машину $0"
+
+#~ msgid "VM state"
+#~ msgstr "Стан ВМ"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "Порт TLS VNC"
+
+#~ msgid "VNC address"
+#~ msgstr "Адреса VNC"
+
+#~ msgid "VNC console"
+#~ msgstr "Консоль VNC"
+
+#~ msgid "VNC port"
+#~ msgstr "Порт VNC"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "Віртуальні машини"
+
+#~ msgid "Virtual machines"
+#~ msgstr "Віртуальні машини"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "Керування віртуальними машинами"
+
+#~ msgid "Virtual network"
+#~ msgstr "Віртуальна мережа"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "Не вдалося створити віртуальну мережу"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "Служба віртуалізації (libvirt) не є активною"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "Назва групи томів має бути непорожньою"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "Придатний до запису"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "Придатний до запису і спільний"
+
+#~ msgid "Yesterday"
+#~ msgstr "Вчора"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "Вам слід вибрати найвідповіднішу операційну систему"
+
+#~ msgid "cdrom"
+#~ msgstr "cdrom"
+
+#~ msgid "disabled"
+#~ msgstr "вимкнено"
+
+#~ msgid "down"
+#~ msgstr "нижче"
+
+#~ msgid "enabled"
+#~ msgstr "увімкнено"
+
+#~ msgid "ethernet"
+#~ msgstr "ethernet"
+
+#~ msgid "host device"
+#~ msgstr "пристрій основної системи"
+
+#~ msgid "host passthrough"
+#~ msgstr "передавання вузла"
+
+#~ msgid "hostdev"
+#~ msgstr "пристрій осн. системи"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "Безпосереднє призначення iSCSI"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "IQN ініціатора iSCSI"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "IQN цілі iSCSI"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "mcast"
+
+#~ msgid "no"
+#~ msgstr "ні"
+
+#~ msgid "no state saved"
+#~ msgstr "немає збережених станів"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "переспрямований пристрій"
+
+#~ msgid "server"
+#~ msgstr "сервер"
+
+#~ msgid "up"
+#~ msgstr "вище"
+
+#~ msgid "vCPU count"
+#~ msgstr "К-ть вірт. процесорів"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "Макс. вірт. процесорів"
+
+#~ msgid "vCPUs"
+#~ msgstr "Вірт. проц."
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#~ msgid "view more..."
+#~ msgstr "докладніше…"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr ""
+#~ "Для клонування віртуальних машин у системі має бути встановлено пакунок "
+#~ "virt-install"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr ""
+#~ "Для створення нових віртуальних машин у системі має бути встановлено "
+#~ "пакунок virt-install"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr ""
+#~ "Для редагування цього атрибута у системі має бути встановлено пакунок "
+#~ "virt-install"
+
+#~ msgid "vm"
+#~ msgstr "vm"
+
+#~ msgid "Local install media"
+#~ msgstr "Локальні носії для встановлення"
+
+#~ msgid "URL"
+#~ msgstr "Адреса"
+
+#~ msgid "Please set a root password"
+#~ msgstr "Будь ласка, встановіть пароль root"
+
+#~ msgid "21st"
+#~ msgstr "21-е"
+
+#~ msgid "22nd"
+#~ msgstr "22-е"
+
+#~ msgid "23rd"
+#~ msgstr "23-є"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "ПІсля"
+
+#~ msgid "Friday"
+#~ msgstr "п'ятниця"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "Година : Хвилина"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "Години слід вказувати у форматі числа від 0 до 23"
+
+#~ msgid "Invalid date format."
+#~ msgstr "Некоректний формат дати."
+
+#~ msgid "Invalid number."
+#~ msgstr "Некоректне число."
+
+#~ msgid "Monday"
+#~ msgstr "понеділок"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "Повторювати щогодини"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "Повторювати щороку"
+
+#~ msgid "Saturday"
+#~ msgstr "субота"
+
+#~ msgid "Sunday"
+#~ msgstr "неділя"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr ""
+#~ "Цей день є не в усіх місяцях.<br> Таймер виконуватиметься лише у місяці, "
+#~ "у яких є 31 день."
+
+#~ msgid "Thursday"
+#~ msgstr "четвер"
+
+#~ msgid "Tuesday"
+#~ msgstr "вівторок"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 оновлення"
+#~ msgstr[1] "$0 оновлення"
+#~ msgstr[2] "$0 оновлень"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 виправлення захисту"
+#~ msgstr[1] "$1 виправлення захисту"
+#~ msgstr[2] "$1 виправлень захисту"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "Керування пристроями зберігання даних"
+
+#~ msgid "Restart now"
+#~ msgstr "Перезапустити зараз"
+
+#~ msgid "Configure"
+#~ msgstr "Налаштувати"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr ""
+#~ "Завантаження з мережі доступне, лише якщо використано з'єднання «Система»"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "Робота у мережі"
+
+#~ msgid "No snapshots"
+#~ msgstr "Немає знімків"
+
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "У каталозі «$0» немає такого файла"
+
+#~ msgid "No updates pending"
+#~ msgstr "У черзі немає оновлень"
+
+#~ msgid "This directory is empty"
+#~ msgstr "Цей каталог є порожнім"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr "Для буферів цього типу не передбачено створення томів сховища даних"
+
+#~ msgid "undefined"
+#~ msgstr "не визначено"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "Неправильний ключ вузла"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "Не вдалося встановити ідентичність вузла {{#strong}}{{host}}{{/strong}}. "
+#~ "Ви справді хочете продовжити спробу встановити з’єднання?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "Ключ {{#strong}}{{host}}{{/strong}} не відповідає раніше використаному "
+#~ "ключу. Якщо нещодавно ніхто не міняв цього комп’ютера, ймовірною причиною "
+#~ "є спроба атаки на ваше з’єднання із цим комп’ютером."
+
+#~ msgid "Unknown host key"
+#~ msgstr "Невідомий ключ вузла"
+
+#~ msgid "Address:"
+#~ msgstr "Адреса:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "Потрібно принаймні $0 дисків."
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "З’єднатися із будь-якою програмою перегляду SPICE або VNC."
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "Отримати MSI з $0"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "Графічна консоль (VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Графічна консоль у перегляді стільниці"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "Для цієї віртуальної машини консолей не визначено."
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid ""
+#~ "For the host, either specify the hostname, IP address, an alias name or a "
+#~ "unique resource identifier for the SSH destination."
+#~ msgstr ""
+#~ "Для вузла вкажіть назву вузла, IP-адресу, альтернативну назву або "
+#~ "унікальний ідентифікатор ресурсу для SSH-призначення."
+
+#~ msgid ""
+#~ "Specify the host and the login user account for the host that you want to "
+#~ "add."
+#~ msgstr ""
+#~ "Вкажіть вузол та обліковий запис для входу до вузла, який ви хочете "
+#~ "додати."
+
+#~ msgid "Entry"
+#~ msgstr "Запис"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "Ваша програма для перегляду інтернету від'єднається, але це не вплине на "
+#~ "процес оновлення. Ви зможете з'єднатися знову за декілька секунд, щоб "
+#~ "продовжити спостереження за поступом."
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "і перезапуск системи автоматично."
+
+#~ msgid "Dashboard"
+#~ msgstr "Панель приладів"
+
+#~ msgid "Description input text"
+#~ msgstr "Вхідний текст опису"
+
+#~ msgid "FAILED"
+#~ msgstr "ПОМИЛКА"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "З’єднання розірвано. Намагаємося його повторно встановити"
+
+#~ msgid "Name input text"
+#~ msgstr "Вхідний текст назви"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "Мережа"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "Будь ласка, введіть розмір нового тому"
+
+#~ msgid "Servers"
+#~ msgstr "Сервери"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr ""
+#~ "Зараз ваш комп’ютер безпосередньо з’єднано із цим сервером. Ви не можете "
+#~ "його вилучити."
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 біт"
+#~ msgstr[1] "$0 біти"
+#~ msgstr[2] "$0 бітів"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 байт"
+#~ msgstr[1] "$0 байти"
+#~ msgstr[2] "$0 байтів"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 байт"
+#~ msgstr[1] "$0 байти"
+#~ msgstr[2] "$0 байтів"
+
+#~ msgid "Bond settings "
+#~ msgstr "Параметри зв’язку "
+
+#~ msgid "IP settings "
+#~ msgstr "Параметри IP "
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "У Cockpit сталася неочікувана внутрішня помилка. <br/><br/>Ви можете "
+#~ "спробувати перезапустити Cockpit натисканням кнопки оновлення у вашій "
+#~ "програмі для перегляду інтернету. у консолі javascript можна знайти "
+#~ "подробиці щодо цієї помилки (<b>Ctrl-Shift-J</b> у більшості програм для "
+#~ "перегляду інтернету)."
+
+#~ msgid "The VM crashed."
+#~ msgstr "Віртуальна машина аварійно завершила роботу."
+
+#~ msgid "The VM is down."
+#~ msgstr "Віртуальну машину вимкнено."
+
+#~ msgid "The VM is going down."
+#~ msgstr "Віртуальна машина вимикається."
+
+#~ msgid "The VM is idle."
+#~ msgstr "Віртуальна машина бездіяльна."
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr ""
+#~ "Віртуальна машина завершує роботу (вимикання або аварійне завершення у "
+#~ "процесі)."
+
+#~ msgid "The VM is paused."
+#~ msgstr "Роботу віртуальної машини призупинено."
+
+#~ msgid "The VM is running."
+#~ msgstr "Віртуальна машина працює."
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr ""
+#~ "Роботу віртуальної машини призупинено засобами керування живленням "
+#~ "гостьової системи."
+
+#~ msgid "inactive"
+#~ msgstr "неактивний"
+
+#~ msgid "Avatar"
+#~ msgstr "Аватар"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "Не заповнюйте, щоб встановити з’єднання із цією машиною від імені "
+#~ "поточного користувача системи. Якщо ви введете ім’я іншого користувача, "
+#~ "для встановлення з’єднання із цією машиною завжди використовуватиметься "
+#~ "це ім’я."
+
+#~ msgid "2 min"
+#~ msgstr "2 хв."
+
+#~ msgid "3 min"
+#~ msgstr "3 хв."
+
+#~ msgid "4 min"
+#~ msgstr "4 хв."
+
+#~ msgid "CPU graph"
+#~ msgstr "Графік процесора"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "Стан процесора"
+
+#~ msgid "Cached"
+#~ msgstr "Кеш"
+
+#~ msgid "Graphs"
+#~ msgstr "Графіки"
+
+#~ msgid "I/O wait"
+#~ msgstr "Очікування В-В"
+
+#~ msgid "Kernel"
+#~ msgstr "Ядро"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "Пам'ять"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "Використання звичайної і резервної пам'яті"
+
+#~ msgid "Memory graph"
+#~ msgstr "Графік пам'яті"
+
+#~ msgid "Network traffic"
+#~ msgstr "Навантаження на мережу"
+
+#~ msgid "Nice"
+#~ msgstr "Пріоритетність"
+
+#~ msgid "Synchronize users"
+#~ msgstr "Синхронізувати користувачів"
+
+#~ msgid "Usage graphs"
+#~ msgstr "Графіки використання"
+
+#~ msgid "View graphs"
+#~ msgstr "Переглянути графіки"
+
+#~ msgid "idle"
+#~ msgstr "бездіяльний"
+
+#~ msgid "paused"
+#~ msgstr "призупинено"
+
+#~ msgid "running"
+#~ msgstr "працює"
+
+#~ msgid "shut off"
+#~ msgstr "вимкнути"
+
+#~ msgid "shutdown"
+#~ msgstr "завершити роботу"
+
+#~ msgid "Add a new host"
+#~ msgstr "Додати новий вузол"
+
+#~ msgid "Available"
+#~ msgstr "Доступні"
+
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "Введіть IP-адресу або назву вузла"
+
+#~ msgid "Network interfaces"
+#~ msgstr "Інтерфейси мережі"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr ""
+#~ "У цій версії cockpit-ws не передбачено підтримки встановлення з’єднання "
+#~ "із вузлом від альтернативного користувача або на альтернативному порті"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr "Щоб спробувати інший порт, вам слід оновити cockpit-ws."
+
+#~ msgid "$0 template"
+#~ msgstr "Шаблон $0"
+
+#~ msgid "Instance of template: "
+#~ msgstr "Екземпляр шаблона: "
+
+#~ msgid "Instantiate"
+#~ msgstr "Створити екземпляр"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 спільних ресурсів"
+
+#~ msgid "All In One"
+#~ msgstr "Усе-в-одному"
+
+#~ msgid "Always"
+#~ msgstr "Завжди"
+
+#~ msgid "Author"
+#~ msgstr "Автор"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "Механізму passwd1 передано помилкові дані"
+
+#~ msgid "CPU priority"
+#~ msgstr "Пріоритетність процесора"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "Не вдалося зв’язатися із Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "Змінити обмеження ресурсів"
+
+#~ msgid "Change resources limits"
+#~ msgstr "Змінити обмеження ресурсів"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit не вдалося увійти до {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit не вдалося увійти до {{#strong}}{{host}}{{/strong}}. Ви можете "
+#~ "змінити ваші реєстраційні дані для розпізнавання нижче. {{#can_sync}}"
+#~ "Можливо, варто надати перевагу {{#sync_link}}синхронізації облікових "
+#~ "записів і паролів{{/sync_link}}.{{/can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "Поєднане використання пам’яті"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "Комбіноване використання $0 ядра процесора"
+#~ msgstr[1] "Комбіноване використання $0 ядер процесора"
+#~ msgstr[2] "Комбіноване використання $0 ядер процесора"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "Команда не може бути порожньою"
+
+#~ msgid "Command:"
+#~ msgstr "Команда:"
+
+#~ msgid "Commit"
+#~ msgstr "Надіслати"
+
+#~ msgid "Commit image"
+#~ msgstr "Надіслати образ"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "Встановлюємо з’єднання із Docker"
+
+#~ msgid "Container name"
+#~ msgstr "Назва контейнера"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr ""
+#~ "Зараз контейнер позначено як такий, що працює, але звичайна спроба "
+#~ "зупинити роботу зазнала невдачі."
+
+#~ msgid "Container is currently running."
+#~ msgstr "Контейнер працює."
+
+#~ msgid "Container:"
+#~ msgstr "Контейнер:"
+
+#~ msgid "Containers"
+#~ msgstr "Контейнери"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "Контейнери"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "Не вдалося змінити групи користувачів"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "Не вдалося змінити пароль користувача"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "Не вдалося встановити з’єднання із комп’ютером"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "Не вдалося створити записи користувачів"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "Не вдалося побудувати список локальних користувачів"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "Не вдалося побудувати список користувачів"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "Не вдалося завантажити дані користувача"
+
+#~ msgid "Created:"
+#~ msgstr "Створено:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Контейнери Docker"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "Docker не встановлено або не активовано у цій системі"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "Дублювання псевдоніма"
+
+#~ msgid "Duplicate port"
+#~ msgstr "Дублювання порту"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "Вкажіть пароль для додавання профілю до ssh-agent"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr ""
+#~ "Введення іншого пароля у це поле означатиме, що вам доведеться повторно "
+#~ "вводити його кожного разу, коли ви повторно встановлюватимете з’єднання "
+#~ "із цією машиною"
+
+#~ msgid "Environment"
+#~ msgstr "Середовище"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "Помилка під час завантаження списку користувачів: {{perm_failed}}"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "Повідомлення щодо помилки від Docker:"
+
+#~ msgid "Everything"
+#~ msgstr "Все"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "Завершено $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "Відкрити порти контейнера"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "Не вдалося запустити Docker: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "Не вдалося зупинити область Docker: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "Отримати новий образ"
+
+#~ msgid "IP address:"
+#~ msgstr "IP-адреса:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "Довжина префікса IP:"
+
+#~ msgid "Id"
+#~ msgstr "Ід."
+
+#~ msgid "Id:"
+#~ msgstr "Ід.:"
+
+#~ msgid "Image"
+#~ msgstr "Образ"
+
+#~ msgid "Image $0"
+#~ msgstr "Зображення $0"
+
+#~ msgid "Image search"
+#~ msgstr "Пошук образів"
+
+#~ msgid "Image:"
+#~ msgstr "Зображення:"
+
+#~ msgid "Images"
+#~ msgstr "Зображення"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "Образи"
+
+#~ msgid "Images and running containers"
+#~ msgstr "Образи і запущені контейнери"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr ""
+#~ "Для синхронізації параметрів користувачів, слід увійти до {{#strong}}"
+#~ "{{host}}{{/strong}}."
+
+#~ msgid "Invalid port"
+#~ msgstr "Некоректний порт"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Квиток Kerberos"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "Не заповнюйте, щоб встановити з’єднання із цією машиною від імені "
+#~ "поточного користувача системи{{#default_user}} ({{default_user}}){{/"
+#~ "default_user}}. Якщо ви введете ім’я іншого користувача, для встановлення "
+#~ "з’єднання із цією машиною завжди використовуватиметься це ім’я."
+
+#~ msgid "Link to another container"
+#~ msgstr "Посилання на інший контейнер"
+
+#~ msgid "Links:"
+#~ msgstr "Посилання:"
+
+#~ msgid "Login Password"
+#~ msgstr "Пароль користувача"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC-адреса:"
+
+#~ msgid "Memory limit"
+#~ msgstr "Обмеження пам’яті"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "Змонтувати томи контейнера"
+
+#~ msgid "No container specified"
+#~ msgstr "Контейнер не вказано"
+
+#~ msgid "No containers"
+#~ msgstr "Немає контейнерів"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "Немає контейнерів, які проходять поточні умови фільтрування"
+
+#~ msgid "No images"
+#~ msgstr "Немає образів"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "Немає образів, які проходять поточні умови фільтрування"
+
+#~ msgid "No results for $0"
+#~ msgstr "Не знайдено нічого, що б відповідало $0"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "Немає образів, які проходять поточні умови фільтрування"
+
+#~ msgid "No running containers"
+#~ msgstr "Немає запущених контейнерів"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr ""
+#~ "Немає запущених контейнерів, які проходять поточні умови фільтрування"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "Не уповноважено на доступ до Docker у цій системі"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "Якщо помилка, повторити $0 раз"
+#~ msgstr[1] "Якщо помилка, повторити $0 рази"
+#~ msgstr[2] "Якщо помилка, повторити $0 разів"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "Будь ласка, підтвердьте примусове вилучення $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "Будь ласка, спробуйте інший ключ"
+
+#~ msgid "Ports:"
+#~ msgstr "Порти:"
+
+#~ msgid "Problems"
+#~ msgstr "Проблеми"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Лише читання"
+
+#~ msgid "ReadWrite"
+#~ msgstr "Запис-читання"
+
+#~ msgid "Repository"
+#~ msgstr "Сховище"
+
+#~ msgid "Restart policy:"
+#~ msgstr "Правила перезапуску:"
+
+#~ msgid "Retries:"
+#~ msgstr "Кількість спроб:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "Повторно використати ваш пароль для віддалених з'єднань"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr ""
+#~ "Виберіть користувачів, записи яких ви б хотіли синхронізувати з "
+#~ "{{#strong}}{{host}}{{/strong}}"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "Встановити змінні середовища контейнера"
+
+#~ msgid "Severity:"
+#~ msgstr "Критичність:"
+
+#~ msgid "Show all containers"
+#~ msgstr "Показати усі контейнери"
+
+#~ msgid "Start Docker"
+#~ msgstr "Запустити Docker"
+
+#~ msgid "State:"
+#~ msgstr "Стан:"
+
+#~ msgid "Synchronize"
+#~ msgstr "Синхронізувати"
+
+#~ msgid "Tag"
+#~ msgstr "Мітка"
+
+#~ msgid "Tags"
+#~ msgstr "Мітки"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr ""
+#~ "Вказані нижче контейнери залежать від цього образу, отже стануть "
+#~ "непридатними до використання."
+
+#~ msgid "This image does not exist."
+#~ msgstr "Цього образу не існує."
+
+#~ msgid "Type a password"
+#~ msgstr "Введіть пароль"
+
+#~ msgid "Type to filter…"
+#~ msgstr "Введіть щось для фільтрування…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "Якщо не зупинено"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "Непідтримуваний механізм налаштовування"
+
+#~ msgid "Up since $0"
+#~ msgstr "Працює з $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "Використано контейнерами"
+
+#~ msgid "Using available credentials"
+#~ msgstr "З використанням доступних реєстраційних даних"
+
+#~ msgid "Volumes"
+#~ msgstr "Томи"
+
+#~ msgid "Volumes:"
+#~ msgstr "Томи:"
+
+#~ msgid "With terminal"
+#~ msgstr "За допомогою термінала"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "Ви встановлюєте з’єднання із {{#strong}}{{host}}{{/strong}}. Втім, для "
+#~ "синхронізації даних користувачів потрібні права доступу адміністративного "
+#~ "користувача."
+
+#~ msgid "default"
+#~ msgstr "типовий"
+
+#~ msgid "key"
+#~ msgstr "ключ"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "шукати за назвою, простором назв або описом"
+
+#~ msgid "shares"
+#~ msgstr "спільний"
+
+#~ msgid "to host port"
+#~ msgstr "до порту основної системи"
+
+#~ msgid "value"
+#~ msgstr "значення"
+
+#~ msgid "Master"
+#~ msgstr "Основний"
+
+#~ msgid "Password for $0"
+#~ msgstr "Пароль до $0"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "Користувачу <b>$0</b> заборонено керувати серверами"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "Облікові записи"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "Користувачу <b>$0</b> заборонено змінювати облікові записи"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "Неможливо вилучити обліковий запис root"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "Неможливо перейменувати обліковий запис root"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "Не уповноважено додавати нову зону"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "Не уповноважено додавати служби до зони $0"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "Не уповноважено вилучати службу $0"
+
+#~ msgid "Account Settings"
+#~ msgstr "Параметри облікового запису"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "Додати запис комп’ютера на панель"
+
+#~ msgid "Machines"
+#~ msgstr "Машини"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "Обліковий запис розширив права доступу до адміністративних"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr ""
+#~ "Пароль непридатний для виконання для привілейованих завдань та з’єднання "
+#~ "з іншими комп’ютерами"
+
+#~ msgid "Privileged"
+#~ msgstr "Привілейований"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr ""
+#~ "Повторно використати ваш пароль для привілейованих завдань та з’єднання з "
+#~ "іншими комп’ютерами"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr "Користувачеві $0 заборонено змінювати назви вузлів"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr "Користувачеві $0 заборонено вимикати або перезапускати цей сервер"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "Користувачу <b>$0</b> заборонено змінювати профілі"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "Користувачу <b>$0</b> не дозволено керувати сховищем даних"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr "Користувачу <b>$0</b> заборонено змінювати параметри роботи мережі"
+
+#~ msgid "Required by $0"
+#~ msgstr "Потрібен для $0"
+
+#~ msgid "Automatic Startup"
+#~ msgstr "Автоматичний запуск"
+
+#~ msgid "Last Trigger"
+#~ msgstr "Останній перемикач"
+
+#~ msgid "Next Run"
+#~ msgstr "Наступний запуск"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "Користувачу <b>$0</b> заборонено створювати таймери"
+
+#~ msgid "Connecting"
+#~ msgstr "Встановлюємо з’єднання"
+
+#~ msgid "About Cockpit"
+#~ msgstr "Про Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (спільний із операційною системою)"
+
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 вразливість"
+#~ msgstr[1] "$0 вразливості"
+#~ msgstr[2] "$0 вразливостей"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "Додати додаткове сховище"
+
+#~ msgid "Add Storage"
+#~ msgstr "Додати сховище даних"
+
+#~ msgid "Additional Storage"
+#~ msgstr "Додаткове сховище"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr ""
+#~ "Всі дані на позначених дисках буде витерто, а диски буде додано до "
+#~ "резервного сховища даних."
+
+#~ msgid "Configure storage..."
+#~ msgstr "Налаштувати сховище даних…"
+
+#~ msgid "Could not add all disks"
+#~ msgstr "Не вдалося додати усі диски"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "Витерти контейнери і відновити початковий стан резервного сховища"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr "Немає доступу до даних щодо резервного сховища даних Docker."
+
+#~ msgid "Local Disks"
+#~ msgstr "Локальні диски"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "Додаткових локальний сховищ даних не знайдено."
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "Повторно форматувати і додати диски"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr ""
+#~ "Скидання резервного сховища даних призведе до витирання усіх контейнерів "
+#~ "та звільнення дисків зі сховища."
+
+#~ msgid "Security"
+#~ msgstr "Безпека"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr "У цій системі не можна керувати резервним сховищем даних Docker."
+
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "Під час сканування $time ($type) виявлено $count вразливість:"
+#~ msgstr[1] "Під час сканування $time ($type) виявлено $count вразливості:"
+#~ msgstr[2] "Під час сканування $time ($type) виявлено $count вразливостей:"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "Скануванням від $time ($type) вразливостей не знайдено."
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "Сканування $time ($type) виконати не вдалося."
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "Доступна служба віртуалізації"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr ""
+#~ "У вас немає прав доступу до керування резервним сховищем даних Docker."
+
+#~ msgid "$0 GiB / $1 GiB"
+#~ msgstr "$0 ГіБ з $1 ГіБ"
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} -> $cport"
+
+#~ msgid "Hide Performance Options"
+#~ msgstr "Приховати параметри швидкодії"
+
+#~ msgid "Show Performance Options"
+#~ msgstr "Показати параметри швидкодії"
diff --git a/po/zh_CN.po b/po/zh_CN.po
new file mode 100644
index 0000000..d86a071
--- /dev/null
+++ b/po/zh_CN.po
@@ -0,0 +1,12557 @@
+# Translators:
+# 1dot75cm <sensor.wen@gmail.com>, 2016
+# mosquito <sensor.wen@gmail.com>, 2016. #zanata
+# vanlos wang <vanloswang@126.com>, 2016. #zanata
+# vanlos wang <vanloswang@126.com>, 2017. #zanata
+# Ludek Janda <ljanda@redhat.com>, 2018. #zanata, 2020.
+# Qiyu Yan <yanqiyu01@gmail.com>, 2018. #zanata
+# cockpit <cockpituous@gmail.com>, 2018. #zanata
+# Ludek Janda <ljanda@redhat.com>, 2019. #zanata, 2020.
+# Qiyu Yan <yanqiyu01@gmail.com>, 2019. #zanata
+# Tony Fu <tfu@redhat.com>, 2019. #zanata
+# Xiaofeng Wang <xiaofwan@redhat.com>, 2019. #zanata
+# cockpit <cockpituous@gmail.com>, 2019. #zanata
+# mosquito <sensor.wen@gmail.com>, 2019. #zanata
+# Pany <geekpany@gmail.com>, 2020.
+# Matej Marusak <mmarusak@redhat.com>, 2020.
+# Charles Lee <lchopn@gmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-12 02:47+0000\n"
+"PO-Revision-Date: 2024-02-08 07:49+0000\n"
+"Last-Translator: Tony Fu <tfu@redhat.com>\n"
+"Language-Team: Chinese (Simplified) <https://translate.fedoraproject.org/"
+"projects/cockpit/main/zh_CN/>\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"X-Generator: Weblate 5.3.1\n"
+
+#: pkg/users/accounts-list.js:240
+msgid "# of users"
+msgstr "用户数"
+
+#: pkg/metrics/metrics.jsx:708
+msgid "$0 CPU"
+msgid_plural "$0 CPUs"
+msgstr[0] "$0 CPU"
+
+#: pkg/lib/machine-info.js:229
+msgid "$0 GiB"
+msgstr "$0 GiB"
+
+#: pkg/networkmanager/network-main.jsx:174
+msgid "$0 active zone"
+msgid_plural "$0 active zones"
+msgstr[0] "$0 个已启用的安全区域"
+
+#: pkg/metrics/metrics.jsx:730 pkg/metrics/metrics.jsx:893
+msgid "$0 available"
+msgstr "$0 可用"
+
+#: pkg/storaged/block/resize.jsx:266
+msgid "$0 can not be made larger"
+msgstr "无法扩容 $0"
+
+#: pkg/storaged/block/resize.jsx:263
+msgid "$0 can not be made smaller"
+msgstr "无法缩小 $0"
+
+#: pkg/storaged/block/resize.jsx:259
+msgid "$0 can not be resized"
+msgstr "无法改变 $0 的大小"
+
+#: pkg/storaged/block/resize.jsx:255
+msgid "$0 can not be resized here"
+msgstr "无法在此改变 $0 的大小"
+
+#: pkg/storaged/mdraid/mdraid.jsx:255
+msgid "$0 chunk size"
+msgstr "$0 块大小"
+
+#: pkg/systemd/overview-cards/insights.jsx:196
+msgid "$0 critical hit"
+msgid_plural "$0 hits, including critical"
+msgstr[0] "$0 次缓存命中(含关键命中)"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:295
+msgid "$0 data + $1 overhead used of $2 ($3)"
+msgstr "已使用 $0 数据 + $1 额外开销,共 $2($3)"
+
+#: pkg/lib/cockpit-components-plot.jsx:226
+msgid "$0 day"
+msgid_plural "$0 days"
+msgstr[0] "$0 天"
+
+#: pkg/playground/translate.js:26 pkg/playground/translate.js:29
+#: pkg/storaged/mdraid/mdraid.jsx:260
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "找不到 $0 个磁盘"
+
+#: pkg/playground/translate.js:32 pkg/playground/translate.js:35
+msgctxt "disk-non-rotational"
+msgid "$0 disk is missing"
+msgid_plural "$0 disks are missing"
+msgstr[0] "找不到 $0 个磁盘"
+
+#: pkg/storaged/mdraid/mdraid.jsx:253
+msgid "$0 disks"
+msgstr "$0 个磁盘"
+
+#: pkg/shell/topnav.jsx:152
+msgid "$0 documentation"
+msgstr "$0 份文档"
+
+#: pkg/static/login.js:432 pkg/static/login.js:937
+msgid "$0 error"
+msgstr "$0 个错误"
+
+#: pkg/lib/cockpit.js:2713
+msgid "$0 exited with code $1"
+msgstr "进程 $0 已退出,返回码为 $1"
+
+#: pkg/lib/cockpit.js:2715
+msgid "$0 failed"
+msgstr "进程 $0 运行时出错"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:103
+msgid "$0 failed login attempt"
+msgid_plural "$0 failed login attempts"
+msgstr[0] "$0 次登录失败"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "$0 filesystem"
+msgstr "$0 文件系统"
+
+#: pkg/metrics/metrics.jsx:942
+msgid "$0 free"
+msgstr "$0 可用"
+
+#: pkg/lib/cockpit-components-plot.jsx:229
+msgid "$0 hour"
+msgid_plural "$0 hours"
+msgstr[0] "$0 小时"
+
+#: pkg/systemd/overview-cards/insights.jsx:201
+msgid "$0 important hit"
+msgid_plural "$0 hits, including important"
+msgstr[0] "$0 次命中(含关键命中)"
+
+#: pkg/users/account-create-dialog.js:378
+msgid "$0 is an existing file"
+msgstr "$0 是一个已存在的文件"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:68
+#: pkg/storaged/lvm2/block-logical-volume.jsx:151
+#: pkg/storaged/lvm2/volume-group.jsx:85 pkg/storaged/lvm2/volume-group.jsx:336
+#: pkg/storaged/block/format-dialog.jsx:264 pkg/storaged/block/resize.jsx:425
+#: pkg/storaged/block/resize.jsx:571 pkg/storaged/legacy-vdo/legacy-vdo.jsx:50
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:84 pkg/storaged/mdraid/mdraid.jsx:63
+#: pkg/storaged/mdraid/mdraid.jsx:116 pkg/storaged/stratis/filesystem.jsx:129
+#: pkg/storaged/stratis/pool.jsx:141
+#: pkg/storaged/partitions/format-disk-dialog.jsx:39
+#: pkg/storaged/partitions/partition.jsx:47
+msgid "$0 is in use"
+msgstr "$0 正在使用"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:160
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:35
+msgid "$0 is not available from any repository."
+msgstr "没有提供 $0 组件的仓库。"
+
+#: pkg/static/login.js:759 pkg/shell/hosts_dialog.jsx:464
+msgid "$0 key changed"
+msgstr "已更改 $0 个密钥"
+
+#: pkg/lib/cockpit.js:2711
+msgid "$0 killed with signal $1"
+msgstr "进程 $0 被信号 $1 终止"
+
+#: pkg/systemd/overview-cards/insights.jsx:211
+msgid "$0 low severity hit"
+msgid_plural "$0 low severity hits"
+msgstr[0] "$0 次低关键性命中"
+
+#: pkg/lib/cockpit-components-plot.jsx:232
+msgid "$0 minute"
+msgid_plural "$0 minutes"
+msgstr[0] "$0 分钟"
+
+#: pkg/systemd/overview-cards/insights.jsx:206
+msgid "$0 moderate hit"
+msgid_plural "$0 hits, including moderate"
+msgstr[0] "$0 次命中(含中等关键性命中)"
+
+#: pkg/lib/cockpit-components-plot.jsx:220
+msgid "$0 month"
+msgid_plural "$0 months"
+msgstr[0] "$0 月"
+
+#: pkg/users/accounts-list.js:339
+msgid "$0 more..."
+msgstr "其余 $0 用户…"
+
+#: pkg/packagekit/history.jsx:91
+msgid "$0 package"
+msgid_plural "$0 packages"
+msgstr[0] "$0 个软件包"
+
+#: pkg/packagekit/updates.jsx:703 pkg/packagekit/updates.jsx:818
+msgid "$0 package needs a system reboot"
+msgid_plural "$0 packages need a system reboot"
+msgstr[0] "$0 个软件包需要重启系统才能完成更新"
+
+#: pkg/metrics/metrics.jsx:134
+msgid "$0 page"
+msgid_plural "$0 pages"
+msgstr[0] "$0 个分页"
+
+#: pkg/storaged/partitions/partition-table.jsx:92
+msgid "$0 partitions"
+msgstr "$0 分区"
+
+#: pkg/packagekit/updates.jsx:790
+msgid "$0 security fix available"
+msgid_plural "$0 security fixes available"
+msgstr[0] "$0 个待安装的安全修复"
+
+#: pkg/systemd/services.jsx:600
+msgid "$0 service has failed"
+msgid_plural "$0 services have failed"
+msgstr[0] "$0 个系统服务故障"
+
+#: pkg/packagekit/updates.jsx:720 pkg/packagekit/updates.jsx:830
+msgid "$0 service needs to be restarted"
+msgid_plural "$0 services need to be restarted"
+msgstr[0] "$0 个系统服务需要重启"
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "$0 slot remains"
+msgid_plural "$0 slots remain"
+msgstr[0] "$0 个剩余插槽"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "$0 spike"
+msgid_plural "$0 spikes"
+msgstr[0] "$0 高负载"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:403
+msgid "$0 synchronized"
+msgstr "$0 同步"
+
+#: pkg/metrics/metrics.jsx:722 pkg/metrics/metrics.jsx:884
+#: pkg/metrics/metrics.jsx:950
+msgid "$0 total"
+msgstr "$0 总计"
+
+#: pkg/packagekit/updates.jsx:798
+msgid "$0 update available"
+msgid_plural "$0 updates available"
+msgstr[0] "$0 个可用更新"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:309
+msgid "$0 used of $1 ($2 saved)"
+msgstr "已使用 $0,共 $1(已保存 $2)"
+
+#: pkg/lib/cockpit-components-plot.jsx:223
+msgid "$0 week"
+msgid_plural "$0 weeks"
+msgstr[0] "$0 周"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:115
+msgid "$0 will be installed."
+msgstr "即将安装 $0。"
+
+#: pkg/lib/cockpit-components-plot.jsx:217
+msgid "$0 year"
+msgid_plural "$0 years"
+msgstr[0] "$0 年"
+
+#: pkg/networkmanager/firewall.jsx:195
+msgid "$0 zone"
+msgstr "$0 安全区域"
+
+#: pkg/systemd/logDetails.jsx:179
+msgid "$0: crash at $1"
+msgstr "$0:于 $1 崩溃"
+
+#: pkg/storaged/utils.js:274
+msgid "$name (from $host)"
+msgstr "$name(位于 $host)"
+
+#: pkg/storaged/dialog.jsx:1218
+msgid "(Not part of target)"
+msgstr "(不是目标的一部分)"
+
+#: pkg/storaged/filesystem/utils.jsx:239
+msgid "(no assigned mount point)"
+msgstr "(没有分配的挂载点)"
+
+#: pkg/storaged/filesystem/utils.jsx:236 pkg/storaged/filesystem/utils.jsx:241
+#: pkg/storaged/nfs/nfs.jsx:294
+msgid "(not mounted)"
+msgstr "(未挂载)"
+
+#: pkg/storaged/block/format-dialog.jsx:242
+msgid "(recommended)"
+msgstr "(推荐)"
+
+#: pkg/packagekit/updates.jsx:800
+msgid ", including $1 security fix"
+msgid_plural ", including $1 security fixes"
+msgstr[0] ",包含 $1 个安全更新"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:95
+msgid "1 MiB"
+msgstr "1 MiB"
+
+#: pkg/lib/cockpit-components-plot.jsx:270
+msgid "1 day"
+msgstr "1 天"
+
+#: pkg/lib/cockpit-components-plot.jsx:268
+msgid "1 hour"
+msgstr "1 小时"
+
+#: pkg/metrics/metrics.jsx:852
+msgid "1 min"
+msgstr "1 分钟"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:177
+msgid "1 minute"
+msgstr "1 分钟"
+
+#: pkg/lib/cockpit-components-plot.jsx:271
+msgid "1 week"
+msgstr "1 周"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "10th"
+msgstr "10 日"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "11th"
+msgstr "11 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:93
+msgid "128 KiB"
+msgstr "128 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "12th"
+msgstr "12 日"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "13th"
+msgstr "13 日"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "14th"
+msgstr "14 日"
+
+#: pkg/metrics/metrics.jsx:854
+msgid "15 min"
+msgstr "15 分钟"
+
+#: pkg/systemd/timer-dialog.jsx:338
+msgid "15th"
+msgstr "15 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:90
+msgid "16 KiB"
+msgstr "16 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "16th"
+msgstr "16 日"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "17th"
+msgstr "17 日"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "18th"
+msgstr "18 日"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "19th"
+msgstr "19 日"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "1st"
+msgstr "1 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:96
+msgid "2 MiB"
+msgstr "2 MiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:179
+msgid "20 minutes"
+msgstr "20 分钟"
+
+#: pkg/systemd/timer-dialog.jsx:339
+msgid "20th"
+msgstr "20 日"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "21th"
+msgstr "21 日"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "22th"
+msgstr "22 日"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "23th"
+msgstr "23 日"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "24th"
+msgstr "24 日"
+
+#: pkg/systemd/timer-dialog.jsx:340
+msgid "25th"
+msgstr "25 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "26th"
+msgstr "26 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "27th"
+msgstr "27 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "28th"
+msgstr "28 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "29th"
+msgstr "29 日"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "2nd"
+msgstr "2 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "30th"
+msgstr "30 日"
+
+#: pkg/systemd/timer-dialog.jsx:341
+msgid "31st"
+msgstr "31 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:91
+msgid "32 KiB"
+msgstr "32 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "3rd"
+msgstr "3 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:88
+msgid "4 KiB"
+msgstr "4 KiB"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:180
+msgid "40 minutes"
+msgstr "40 分钟"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "4th"
+msgstr "4 日"
+
+#: pkg/metrics/metrics.jsx:853
+msgid "5 min"
+msgstr "5 分钟"
+
+#: pkg/lib/cockpit-components-plot.jsx:267
+#: pkg/lib/cockpit-components-shutdown.jsx:178
+msgid "5 minutes"
+msgstr "5 分钟"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:94
+msgid "512 KiB"
+msgstr "512 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:336
+msgid "5th"
+msgstr "5 日"
+
+#: pkg/lib/cockpit-components-plot.jsx:269
+msgid "6 hours"
+msgstr "6 小时"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:181
+msgid "60 minutes"
+msgstr "60 分钟"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:92
+msgid "64 KiB"
+msgstr "64 KiB"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "6th"
+msgstr "6 日"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "7th"
+msgstr "7 日"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:89
+msgid "8 KiB"
+msgstr "8 KiB"
+
+#: pkg/networkmanager/bond.jsx:47
+msgid "802.3ad"
+msgstr "802.3ad"
+
+#: pkg/networkmanager/team.jsx:45
+msgid "802.3ad LACP"
+msgstr "802.3ad LACP"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "8th"
+msgstr "8 日"
+
+#: pkg/systemd/timer-dialog.jsx:337
+msgid "9th"
+msgstr "9 日"
+
+#: pkg/shell/hosts_dialog.jsx:98
+msgid "A compatible version of Cockpit is not installed on $0."
+msgstr "主机 $0 上找不到版本兼容的 Cockpit。"
+
+#: pkg/storaged/stratis/utils.jsx:123
+msgid "A filesystem with this name exists already in this pool."
+msgstr "存储池中存在重名文件系统。"
+
+#: pkg/users/group-create-dialog.js:71
+msgid "A group with this name already exists"
+msgstr "具有此名称的组已存在"
+
+#: pkg/static/login.html:39
+msgid ""
+"A modern browser is required for security, reliability, and performance."
+msgstr "请更新浏览器以确保安全性、可靠性和性能。"
+
+#: pkg/networkmanager/bond.jsx:142
+msgid ""
+"A network bond combines multiple network interfaces into one logical "
+"interface with higher throughput or redundancy."
+msgstr ""
+"网络绑定 (Network Bond) :将多个网络接口组合成一个逻辑接口以提高吞吐量和冗余"
+"度。"
+
+#: pkg/shell/hosts_dialog.jsx:795
+msgid ""
+"A new SSH key at $0 will be created for $1 on $2 and it will be added to the "
+"$3 file of $4 on $5."
+msgstr ""
+"将在 $2 上为 $1 在 $0 处创建一个新的 SSH 密钥,该密钥将被添加到 $5 上 $4 的"
+"$3 文件中。"
+
+#: pkg/packagekit/updates.jsx:1528
+msgid "A package needs a system reboot for the updates to take effect:"
+msgid_plural ""
+"Some packages need a system reboot for the updates to take effect:"
+msgstr[0] "如下软件包更新需要重启系统才能生效:"
+
+#: pkg/storaged/stratis/utils.jsx:34
+msgid "A pool with this name exists already."
+msgstr "已有重名存储池。"
+
+#: pkg/packagekit/updates.jsx:1532
+msgid "A service needs to be restarted for the updates to take effect:"
+msgid_plural ""
+"Some services need to be restarted for the updates to take effect:"
+msgstr[0] "需要重启如下系统服务才能使更新生效:"
+
+#: pkg/storaged/lvm2/volume-group.jsx:383
+msgid "A volume group with missing physical volumes can not be renamed."
+msgstr "无法重命名物理卷丢失的卷组。"
+
+#: pkg/networkmanager/bond.jsx:55
+msgid "ARP"
+msgstr "ARP"
+
+#: pkg/networkmanager/network-interface.jsx:422
+msgid "ARP monitoring"
+msgstr "ARP 监控"
+
+#: pkg/networkmanager/team.jsx:57
+msgid "ARP ping"
+msgstr "ARP ping"
+
+#: pkg/shell/topnav.jsx:174
+msgid "About Web Console"
+msgstr "关于网页控制台"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Absent"
+msgstr "空缺"
+
+#: pkg/static/login.js:668
+msgid "Accept key and log in"
+msgstr "接受密钥并登录"
+
+#: pkg/lib/cockpit-components-password.jsx:101
+msgid "Acceptable password"
+msgstr "可行的密码"
+
+#: pkg/users/expiration-dialogs.js:108
+msgid "Account expiration"
+msgstr "账号使用期限设置"
+
+#: pkg/users/account-details.js:187
+msgid "Account not available or cannot be edited."
+msgstr "此帐户不可用或不可编辑。"
+
+#: pkg/users/accounts-list.js:241 pkg/users/accounts-list.js:455
+#: pkg/users/account-details.js:236 pkg/users/manifest.json:0
+msgid "Accounts"
+msgstr "用户账户"
+
+#: pkg/storaged/dialog.jsx:1240
+msgid "Action"
+msgstr "操作"
+
+#: pkg/apps/application-list.jsx:90
+msgid "Actions"
+msgstr "操作"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:40
+msgid "Activate"
+msgstr "启用逻辑卷"
+
+#: pkg/storaged/block/resize.jsx:314
+msgid "Activate before resizing"
+msgstr "调整大小前激活"
+
+#: pkg/storaged/jobs-panel.jsx:73
+msgid "Activating $target"
+msgstr "正在启用逻辑卷 $target"
+
+#: pkg/networkmanager/interfaces.js:807 pkg/networkmanager/team.jsx:51
+msgid "Active"
+msgstr "已启用"
+
+#: pkg/networkmanager/bond.jsx:44 pkg/networkmanager/team.jsx:42
+msgid "Active backup"
+msgstr "已启用的备用接口"
+
+#: pkg/shell/topnav.jsx:212 pkg/shell/active-pages-modal.jsx:97
+#: pkg/shell/active-pages-modal.jsx:105
+msgid "Active pages"
+msgstr "当前激活的页面"
+
+#: pkg/systemd/service-details.jsx:465
+msgid "Active since "
+msgstr "完成启动时间: "
+
+#: pkg/systemd/services.jsx:828 pkg/systemd/services.jsx:829
+#: pkg/systemd/services.jsx:836
+msgid "Active state"
+msgstr "启动状态"
+
+#: pkg/networkmanager/bond.jsx:49
+msgid "Adaptive load balancing"
+msgstr "自适应性负载均衡"
+
+#: pkg/networkmanager/bond.jsx:48
+msgid "Adaptive transmit load balancing"
+msgstr "自适应性传输负载均衡"
+
+#: pkg/users/authorized-keys-panel.js:68
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:367
+#: pkg/storaged/iscsi/create-dialog.jsx:120
+#: pkg/storaged/iscsi/create-dialog.jsx:144
+#: pkg/storaged/lvm2/volume-group.jsx:209 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/mdraid/mdraid.jsx:167 pkg/storaged/stratis/pool.jsx:221
+#: pkg/storaged/crypto/keyslots.jsx:456 pkg/storaged/crypto/keyslots.jsx:779
+#: pkg/shell/credentials.jsx:184 pkg/shell/hosts_dialog.jsx:252
+msgid "Add"
+msgstr "添加"
+
+#: pkg/networkmanager/network-interface-members.jsx:166
+#: pkg/lib/cockpit-components-firewalld-request.jsx:146
+msgid "Add $0"
+msgstr "添加 $0"
+
+#: pkg/networkmanager/ip-settings.jsx:245
+#: pkg/networkmanager/ip-settings.jsx:250
+msgid "Add DNS server"
+msgstr "添加DNS服务器"
+
+#: pkg/storaged/crypto/keyslots.jsx:383
+msgid "Add Network Bound Disk Encryption"
+msgstr "添加网络绑定磁盘加密"
+
+#: pkg/storaged/stratis/pool.jsx:458
+msgid "Add Tang keyserver"
+msgstr "添加 Tang 密钥服务器"
+
+#: pkg/networkmanager/network-main.jsx:145 pkg/networkmanager/vlan.jsx:91
+msgid "Add VLAN"
+msgstr "添加 VLAN"
+
+#: pkg/networkmanager/network-main.jsx:141
+msgid "Add VPN"
+msgstr "添加 VPN"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Add WireGuard VPN"
+msgstr "添加 WireGuard VPN"
+
+#: pkg/storaged/mdraid/mdraid.jsx:279
+msgid "Add a bitmap"
+msgstr "添加位图"
+
+#: pkg/networkmanager/firewall.jsx:1042
+msgid "Add a new zone"
+msgstr "添加安全区域"
+
+#: pkg/networkmanager/ip-settings.jsx:179
+#: pkg/networkmanager/ip-settings.jsx:184
+msgid "Add address"
+msgstr "添加地址"
+
+#: pkg/storaged/stratis/pool.jsx:192 pkg/storaged/stratis/pool.jsx:288
+msgid "Add block devices"
+msgstr "添加块设备"
+
+#: pkg/networkmanager/bond.jsx:135 pkg/networkmanager/network-main.jsx:142
+msgid "Add bond"
+msgstr "添加绑定"
+
+#: pkg/networkmanager/bridge.jsx:96 pkg/networkmanager/network-main.jsx:144
+msgid "Add bridge"
+msgstr "添加网桥"
+
+#: pkg/storaged/mdraid/mdraid.jsx:219
+msgid "Add disk"
+msgstr "添加磁盘"
+
+#: pkg/storaged/lvm2/volume-group.jsx:196 pkg/storaged/mdraid/mdraid.jsx:154
+msgid "Add disks"
+msgstr "添加磁盘"
+
+#: pkg/storaged/overview/overview.jsx:153
+#: pkg/storaged/iscsi/create-dialog.jsx:29
+msgid "Add iSCSI portal"
+msgstr "添加 iSCSI 门户"
+
+#: pkg/users/authorized-keys-panel.js:113 pkg/storaged/crypto/keyslots.jsx:420
+#: pkg/shell/credentials.jsx:90
+msgid "Add key"
+msgstr "添加密钥"
+
+#: pkg/storaged/stratis/pool.jsx:551
+msgid "Add keyserver"
+msgstr "添加密钥服务器"
+
+#: pkg/networkmanager/network-interface-members.jsx:186
+msgid "Add member"
+msgstr "添加成员"
+
+#: pkg/shell/hosts.jsx:216 pkg/shell/hosts_dialog.jsx:251
+msgid "Add new host"
+msgstr "添加新主机"
+
+#: pkg/networkmanager/firewall.jsx:1043
+msgid "Add new zone"
+msgstr "添加安全区域"
+
+#: pkg/storaged/stratis/pool.jsx:380 pkg/storaged/stratis/pool.jsx:533
+msgid "Add passphrase"
+msgstr "添加密码"
+
+#: pkg/networkmanager/wireguard.jsx:290
+msgid "Add peer"
+msgstr "添加同行"
+
+#: pkg/storaged/lvm2/volume-group.jsx:243
+msgid "Add physical volume"
+msgstr "添加物理卷"
+
+#: pkg/networkmanager/firewall.jsx:598
+msgid "Add ports"
+msgstr "添加端口"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add ports to $0 zone"
+msgstr "添加端口到安全区域 $0"
+
+#: pkg/users/authorized-keys-panel.js:61
+msgid "Add public key"
+msgstr "添加公钥"
+
+#: pkg/networkmanager/ip-settings.jsx:341
+#: pkg/networkmanager/ip-settings.jsx:346
+msgid "Add route"
+msgstr "添加路线"
+
+#: pkg/networkmanager/ip-settings.jsx:293
+#: pkg/networkmanager/ip-settings.jsx:298
+msgid "Add search domain"
+msgstr "添加搜索域"
+
+#: pkg/networkmanager/firewall.jsx:185 pkg/networkmanager/firewall.jsx:598
+msgid "Add services"
+msgstr "添加系统服务"
+
+#: pkg/networkmanager/firewall.jsx:599
+msgid "Add services to $0 zone"
+msgstr "添加系统服务到安全区域 $0"
+
+#: pkg/networkmanager/firewall.jsx:184
+msgid "Add services to zone $0"
+msgstr "添加系统服务到安全区域 $0"
+
+#: pkg/networkmanager/network-main.jsx:143 pkg/networkmanager/team.jsx:154
+msgid "Add team"
+msgstr "添加绑定"
+
+#: pkg/networkmanager/firewall.jsx:810 pkg/networkmanager/firewall.jsx:815
+msgid "Add zone"
+msgstr "添加安全区域"
+
+#: pkg/storaged/crypto/keyslots.jsx:325
+msgid "Adding \"$0\" to encryption options"
+msgstr "将 \"$0\" 添加到加密选项"
+
+#: pkg/storaged/crypto/keyslots.jsx:314
+msgid "Adding \"$0\" to filesystem options"
+msgstr "将 \"$0\" 添加到文件系统选项"
+
+#: pkg/networkmanager/network-interface-members.jsx:165
+msgid ""
+"Adding $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "添加 $0 将中断您与服务器的连接,届时管理界面将无法使用。"
+
+#: pkg/storaged/stratis/pool.jsx:468
+msgid ""
+"Adding a keyserver requires unlocking the pool. Please provide the existing "
+"pool passphrase."
+msgstr "添加密钥服务器需要解锁池。 请提供现有的池密码。"
+
+#: pkg/networkmanager/firewall.jsx:611
+msgid ""
+"Adding custom ports will reload firewalld. A reload will result in the loss "
+"of any runtime-only configuration!"
+msgstr "添加自定义端口需要重启 firewalld,当前的运行时配置将会丢失!"
+
+#: pkg/storaged/crypto/keyslots.jsx:406
+msgid "Adding key"
+msgstr "添加密钥"
+
+#: pkg/storaged/jobs-panel.jsx:78
+msgid "Adding physical volume to $target"
+msgstr "添加物理卷至 $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:286
+msgid "Adding rd.neednet=1 to kernel command line"
+msgstr "在内核命令行中添加 rd.neednet=1"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "Additional DNS $val"
+msgstr "备用 DNS 服务器:$val"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "Additional DNS search domains $val"
+msgstr "备用 DNS 搜索域 $val"
+
+#: pkg/systemd/service-details.jsx:189
+msgid "Additional actions"
+msgstr "额外操作"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Additional address $val"
+msgstr "其他 IP 地址 $val"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:82
+msgid "Additional packages:"
+msgstr "额外软件包:"
+
+#: pkg/networkmanager/firewall.jsx:155
+msgid "Additional ports"
+msgstr "额外端口"
+
+#: pkg/networkmanager/ip-settings.jsx:198
+#: pkg/networkmanager/ip-settings.jsx:358
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/storaged/iscsi/session.jsx:92
+msgid "Address"
+msgstr "IP 地址"
+
+#: pkg/networkmanager/network-interface.jsx:298
+msgid "Address $val"
+msgstr "IP 地址: $val"
+
+#: pkg/storaged/crypto/tang.jsx:34
+msgid "Address cannot be empty"
+msgstr "地址不能为空"
+
+#: pkg/storaged/crypto/tang.jsx:36
+msgid "Address is not a valid URL"
+msgstr "该地址为无效 URL"
+
+#: pkg/networkmanager/ip-settings.jsx:169
+msgid "Addresses"
+msgstr "IP 地址"
+
+#: pkg/networkmanager/wireguard.jsx:52
+msgid "Addresses are not formatted correctly"
+msgstr "地址格式不正确"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:10
+msgid "Administration with Cockpit Web Console"
+msgstr "使用 Cockpit 网页控制台管理系统"
+
+#: pkg/shell/superuser.jsx:186 pkg/shell/superuser.jsx:329
+msgid "Administrative access"
+msgstr "管理员模式"
+
+#: pkg/sosreport/sosreport.jsx:426
+msgid "Administrative access is required to create and access reports."
+msgstr "创建和查阅报告需要管理员权限。"
+
+#: pkg/sosreport/sosreport.jsx:425
+msgid "Administrative access required"
+msgstr "需要管理员权限"
+
+#: pkg/lib/machine-info.js:86
+msgid "Advanced TCA"
+msgstr "高级 TCA"
+
+#: pkg/systemd/service-details.jsx:575
+msgid "After"
+msgstr "后于"
+
+#: pkg/systemd/overview-cards/realmd.jsx:318
+msgid ""
+"After leaving the domain, only users with local credentials will be able to "
+"log into this machine. This may also affect other services as DNS resolution "
+"settings and the list of trusted CAs may change."
+msgstr ""
+"离开该域后,仅有使用本地凭据的用户可以登录到该主机。这一操作还可能更改 DNS 解"
+"析设置及 CA 信任列表,进而影响其他服务。"
+
+#: pkg/systemd/timer-dialog.jsx:196
+msgid "After system boot"
+msgstr "系统引导后"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Alert"
+msgstr "警告"
+
+#: pkg/systemd/logs.jsx:66
+msgid "Alert and above"
+msgstr "“警报 (Alert)”及更高级别"
+
+#: pkg/systemd/services.jsx:249
+msgid "Alias"
+msgstr "别名"
+
+#: pkg/systemd/logs.jsx:111 pkg/systemd/logs.jsx:127 pkg/systemd/logs.jsx:156
+#: pkg/systemd/logs.jsx:292 pkg/systemd/logs.jsx:318
+msgid "All"
+msgstr "所有"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:158
+msgid "All $0 selected physical volumes are needed for the choosen layout."
+msgstr "所选布局需要所有选定的 $0 物理卷。"
+
+#: pkg/packagekit/autoupdates.jsx:295
+msgid "All updates"
+msgstr "所有更新"
+
+#: pkg/lib/machine-info.js:72
+msgid "All-in-one"
+msgstr "一体机"
+
+#: pkg/systemd/service-details.jsx:126
+msgid "Allow running (unmask)"
+msgstr "启用服务 (unmask)"
+
+#: pkg/networkmanager/wireguard.jsx:318
+msgid "Allowed IPs"
+msgstr "白名单IP地址"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:880
+msgid "Allowed addresses"
+msgstr "白名单地址"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:122
+msgid "An additional $0 must be selected"
+msgstr "必须额外选择 $0"
+
+#: pkg/lib/cockpit-components-modifications.jsx:88
+msgid "Ansible"
+msgstr "Ansible"
+
+#: pkg/lib/cockpit-components-modifications.jsx:96
+msgid "Ansible roles documentation"
+msgstr "Ansible 角色文档"
+
+#: pkg/systemd/logs.jsx:381
+msgid ""
+"Any text string in the logs messages can be filtered. The string can also be "
+"in the form of a regular expression. Also supports filtering by message log "
+"fields. These are space separated values, in form FIELD=VALUE, where value "
+"can be comma separated list of possible values."
+msgstr ""
+"您可以对日志中的任意文本字符串进行过滤(可使用正则表达式)。此外,本过滤器支"
+"持按消息日志字段进行过滤:各字段由空格分隔(格式为 FIELD=VALUE),其中赋值 "
+"(VALUE) 亦可是由逗号分隔列出的、符合格式的各种赋值。"
+
+#: pkg/systemd/terminal.jsx:167
+msgid "Appearance"
+msgstr "外观"
+
+#: pkg/apps/application-list.jsx:177
+msgid "Application information is missing"
+msgstr "缺少应用程序信息"
+
+#: pkg/apps/index.html:23 pkg/apps/application-list.jsx:184
+#: pkg/apps/application.jsx:146 pkg/apps/manifest.json:0
+msgid "Applications"
+msgstr "应用程序"
+
+#: pkg/apps/application-list.jsx:211
+msgid "Applications list"
+msgstr "应用程序列表"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Apply and reboot"
+msgstr "应用并重启"
+
+#: pkg/packagekit/kpatch.jsx:261
+msgid "Apply kernel live patches"
+msgstr "应用内核热补丁 (Live Patches)"
+
+#: pkg/selinux/setroubleshoot-view.jsx:106
+msgid "Apply this solution"
+msgstr "采用该解决方案"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:186
+msgid "Applying new policy... This may take a few minutes."
+msgstr "正在应用新策略…… 这可能需要几分钟时间。"
+
+#: pkg/selinux/setroubleshoot-view.jsx:83
+msgid "Applying solution..."
+msgstr "正在应用解决方案……"
+
+#: pkg/packagekit/updates.jsx:100
+msgid "Applying updates"
+msgstr "正在安装更新"
+
+#: pkg/packagekit/updates.jsx:101
+msgid "Applying updates failed"
+msgstr "更新安装失败"
+
+#: pkg/storaged/block/format-dialog.jsx:97
+msgid "Appropriate for critical mounts, such as /var"
+msgstr "适用于关键挂载,如 /var"
+
+#: pkg/shell/indexes.jsx:340
+msgid "Apps"
+msgstr "应用程序"
+
+#: pkg/storaged/drive/drive.jsx:102
+msgid "Assessment"
+msgstr "评估报告"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:117
+msgid "Asset tag"
+msgstr "资产标签"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:62
+msgid "At boot"
+msgstr "在引导时"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:105
+msgid "At least $0 disk is needed."
+msgid_plural "At least $0 disks are needed."
+msgstr[0] "至少需要 $0 块磁盘。"
+
+#: pkg/storaged/stratis/create-dialog.jsx:60
+msgid "At least one block device is needed."
+msgstr "至少需要 1 个块设备。"
+
+#: pkg/storaged/lvm2/volume-group.jsx:203
+#: pkg/storaged/lvm2/create-dialog.jsx:57 pkg/storaged/mdraid/mdraid.jsx:161
+#: pkg/storaged/stratis/pool.jsx:215
+msgid "At least one disk is needed."
+msgstr "至少需要 1 块磁盘。"
+
+#: pkg/storaged/btrfs/subvolume.jsx:298
+msgid "At least one parent needs to be mounted writable"
+msgstr "最少有一个父设备需要以可写模式挂载"
+
+#: pkg/systemd/timer-dialog.jsx:262
+msgid "At minute"
+msgstr "在分钟"
+
+#: pkg/systemd/timer-dialog.jsx:260
+msgid "At second"
+msgstr "在秒"
+
+#: pkg/systemd/timer-dialog.jsx:190
+msgid "At specific time"
+msgstr "在指定时间"
+
+#: pkg/sosreport/sosreport.jsx:503
+msgid "Attributes"
+msgstr "属性"
+
+#: pkg/selinux/setroubleshoot-view.jsx:357
+msgid "Audit log"
+msgstr "审计日志"
+
+#: pkg/shell/superuser.jsx:179 pkg/shell/superuser.jsx:216
+msgid "Authenticate"
+msgstr "认证"
+
+#: pkg/networkmanager/interfaces.js:799
+msgid "Authenticating"
+msgstr "正在认证"
+
+#: pkg/users/account-create-dialog.js:112 pkg/shell/hosts_dialog.jsx:840
+msgid "Authentication"
+msgstr "认证"
+
+#: pkg/static/login.js:927
+msgid "Authentication failed"
+msgstr "认证失败"
+
+#: pkg/static/login.js:917
+msgid "Authentication failed: Server closed connection"
+msgstr "认证失败:服务端已关闭连接"
+
+#: src/bridge/org.cockpit-project.cockpit-bridge.policy.in:11
+msgid ""
+"Authentication is required to perform privileged tasks with the Cockpit Web "
+"Console"
+msgstr "通过Cockpit Web Console进行验证后才能执行特权操作"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:136
+msgid "Authentication required"
+msgstr "需要验证"
+
+#: pkg/shell/hosts_dialog.jsx:826
+msgid "Authorize SSH key"
+msgstr "授权 SSH 密钥"
+
+#: pkg/users/authorized-keys-panel.js:120
+#: pkg/users/authorized-keys-panel.js:123
+msgid "Authorized public SSH keys"
+msgstr "授权公共 SSH 密钥"
+
+#: pkg/networkmanager/mtu.jsx:79 pkg/networkmanager/ip-settings.jsx:40
+#: pkg/networkmanager/ip-settings.jsx:244
+#: pkg/networkmanager/ip-settings.jsx:292
+#: pkg/networkmanager/ip-settings.jsx:340
+#: pkg/networkmanager/network-interface.jsx:378
+msgid "Automatic"
+msgstr "自动"
+
+#: pkg/networkmanager/ip-settings.jsx:41
+msgid "Automatic (DHCP only)"
+msgstr "自动 (仅 DHCP)"
+
+#: pkg/shell/hosts_dialog.jsx:870
+msgid "Automatic login"
+msgstr "自动登陆"
+
+#: pkg/packagekit/autoupdates.jsx:261 pkg/packagekit/autoupdates.jsx:384
+msgid "Automatic updates"
+msgstr "自动更新"
+
+#: pkg/systemd/services-list.jsx:82 pkg/systemd/service-details.jsx:509
+msgid "Automatically starts"
+msgstr "自动启动"
+
+#: pkg/lib/serverTime.js:563
+msgid "Automatically using NTP"
+msgstr "自动使用 NTP"
+
+#: pkg/lib/serverTime.js:570
+msgid "Automatically using additional NTP servers"
+msgstr "自动使用额外的 NTP 服务器"
+
+#: pkg/lib/serverTime.js:571
+msgid "Automatically using specific NTP servers"
+msgstr "自动使用指定的 NTP 服务器"
+
+#: pkg/lib/cockpit-components-modifications.jsx:86
+msgid "Automation script"
+msgstr "自动化脚本"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:108
+msgid "Available targets on $0"
+msgstr "$0 上可用的目标"
+
+#: pkg/packagekit/updates.jsx:445 pkg/packagekit/updates.jsx:927
+msgid "Available updates"
+msgstr "可用的更新"
+
+#: pkg/systemd/hwinfo.jsx:100
+msgid "BIOS"
+msgstr "BIOS"
+
+#: pkg/storaged/partitions/partition.jsx:114
+msgid "BIOS boot partition"
+msgstr "BIOS 引导分区"
+
+#: pkg/systemd/hwinfo.jsx:108
+msgid "BIOS date"
+msgstr "BIOS 日期"
+
+#: pkg/systemd/hwinfo.jsx:104
+msgid "BIOS version"
+msgstr "BIOS 版本"
+
+#: pkg/users/account-details.js:191
+msgid "Back to accounts"
+msgstr "返回账号"
+
+#: pkg/systemd/services.jsx:257
+msgid "Bad"
+msgstr "错误"
+
+#: pkg/systemd/services.jsx:223
+msgid "Bad setting"
+msgstr "错误设置"
+
+#: pkg/networkmanager/team.jsx:168
+msgid "Balancer"
+msgstr "平衡器"
+
+#: pkg/systemd/service-details.jsx:574
+msgid "Before"
+msgstr "前于"
+
+#: pkg/systemd/service-details.jsx:565
+msgid "Binds to"
+msgstr "绑定到"
+
+#: pkg/systemd/terminal.jsx:173
+msgid "Black"
+msgstr "黑色"
+
+#: pkg/lib/machine-info.js:87
+msgid "Blade"
+msgstr "刀片"
+
+#: pkg/lib/machine-info.js:88
+msgid "Blade enclosure"
+msgstr "刀片机箱"
+
+#: pkg/storaged/block/other.jsx:41
+msgid "Block device"
+msgstr "块设备"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:56
+msgid "Block device for filesystems"
+msgstr "文件系统的块设备"
+
+#: pkg/storaged/stratis/create-dialog.jsx:55
+#: pkg/storaged/stratis/stopped-pool.jsx:138 pkg/storaged/stratis/pool.jsx:210
+#: pkg/storaged/stratis/pool.jsx:567
+msgid "Block devices"
+msgstr "块设备"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:72
+msgid "Blocked"
+msgstr "受阻"
+
+#: pkg/networkmanager/network-interface.jsx:173
+#: pkg/networkmanager/network-interface.jsx:187
+#: pkg/networkmanager/network-interface.jsx:428
+msgid "Bond"
+msgstr "绑定"
+
+#: pkg/systemd/logs.jsx:356
+msgid "Boot"
+msgstr "引导"
+
+#: pkg/storaged/block/format-dialog.jsx:100
+msgid "Boot fails if filesystem does not mount, preventing remote access"
+msgstr "如果文件系统没有挂载,引导会失败,防止远程访问"
+
+#: pkg/storaged/block/format-dialog.jsx:111
+#: pkg/storaged/block/format-dialog.jsx:122
+msgid "Boot still succeeds when filesystem does not mount"
+msgstr "当文件系统没有挂载时,引导仍然成功"
+
+#: pkg/systemd/service-details.jsx:570
+msgid "Bound by"
+msgstr "边界为"
+
+#: pkg/networkmanager/network-interface.jsx:179
+#: pkg/networkmanager/network-interface.jsx:193
+#: pkg/networkmanager/network-interface.jsx:510
+msgid "Bridge"
+msgstr "网桥"
+
+#: pkg/networkmanager/network-interface.jsx:532
+msgid "Bridge port"
+msgstr "网桥端口"
+
+#: pkg/networkmanager/bridgeport.jsx:78
+msgid "Bridge port settings"
+msgstr "网桥端口设置"
+
+#: pkg/networkmanager/bond.jsx:46 pkg/networkmanager/team.jsx:44
+msgid "Broadcast"
+msgstr "广播"
+
+#: pkg/networkmanager/network-interface.jsx:441
+#: pkg/networkmanager/network-interface.jsx:477
+msgid "Broken configuration"
+msgstr "错误的配置"
+
+#: pkg/storaged/btrfs/volume.jsx:122
+msgid "Btrfs volume is mounted"
+msgstr "Btrfs 卷已被挂载"
+
+#: pkg/packagekit/updates.jsx:1437
+msgid "Bug fix updates available"
+msgstr "错误修复的更新可以使用"
+
+#: pkg/packagekit/updates.jsx:387
+msgid "Bugs"
+msgstr "程序错误"
+
+#: pkg/lib/machine-info.js:79
+msgid "Bus expansion chassis"
+msgstr "总线扩展机箱"
+
+#: pkg/shell/hosts_dialog.jsx:811
+msgid ""
+"By changing the password of the SSH key $0 to the login password of $1 on "
+"$2, the key will be automatically made available and you can log in to $3 "
+"without password in the future."
+msgstr ""
+"通过将 SSH 密钥 $0 的密码改为 $2 上 $1 的登录密码,密钥就会自动可用,您可以以"
+"后不需要密码就可以登陆到 $3 。"
+
+#: pkg/static/login.html:61
+msgid "Bypass browser check"
+msgstr "禁用浏览器检查"
+
+#: pkg/systemd/hwinfo.jsx:114 pkg/systemd/overview-cards/usageCard.jsx:132
+#: pkg/metrics/metrics.jsx:109 pkg/metrics/metrics.jsx:820
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1949
+msgid "CPU"
+msgstr "CPU"
+
+#: pkg/systemd/hwinfo.jsx:118
+msgid "CPU security"
+msgstr "CPU安全性"
+
+#: pkg/systemd/hwinfo.jsx:241
+msgid "CPU security toggles"
+msgstr "CPU安全性开关"
+
+#: pkg/metrics/metrics.jsx:108
+msgid "CPU usage"
+msgstr "CPU 使用率"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "CPU usage/load"
+msgstr "CPU 使用率/负载"
+
+#: pkg/packagekit/updates.jsx:369
+msgid "CVE"
+msgstr "CVE"
+
+#: pkg/storaged/stratis/pool.jsx:200
+msgid "Cache"
+msgstr "缓存"
+
+#: pkg/shell/hosts_dialog.jsx:262
+msgid "Can be a hostname, IP address, alias name, or ssh:// URI"
+msgstr "可以是主机名、IP 地址、别名或者 ssh:// URI"
+
+#: pkg/systemd/logsJournal.jsx:280
+msgid "Can not find any logs using the current combination of filters."
+msgstr "使用当前的过滤器组合找不到任何日志。"
+
+#: pkg/playground/translate.html:39 pkg/static/login.html:141
+#: pkg/networkmanager/dialogs-common.jsx:163
+#: pkg/networkmanager/firewall.jsx:617 pkg/networkmanager/firewall.jsx:818
+#: pkg/networkmanager/firewall.jsx:917
+#: pkg/lib/cockpit-components-shutdown.jsx:193
+#: pkg/lib/cockpit-components-dialog.jsx:131 pkg/apps/utils.jsx:69
+#: pkg/sosreport/sosreport.jsx:279 pkg/sosreport/sosreport.jsx:364
+#: pkg/kdump/kdump-view.jsx:235 pkg/systemd/reporting.jsx:383
+#: pkg/systemd/timer-dialog.jsx:151 pkg/systemd/service-details.jsx:84
+#: pkg/systemd/service-details.jsx:721 pkg/systemd/hwinfo.jsx:231
+#: pkg/systemd/overview-cards/realmd.jsx:434
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:314
+#: pkg/systemd/overview-cards/configurationCard.jsx:284
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:194
+#: pkg/systemd/overview-cards/motdCard.jsx:65
+#: pkg/packagekit/autoupdates.jsx:274 pkg/packagekit/kpatch.jsx:312
+#: pkg/packagekit/updates.jsx:542 pkg/packagekit/updates.jsx:580
+#: pkg/storaged/jobs-panel.jsx:140 pkg/metrics/metrics.jsx:1481
+#: pkg/shell/shell-modals.jsx:118 pkg/shell/credentials.jsx:313
+#: pkg/shell/superuser.jsx:182 pkg/shell/superuser.jsx:219
+#: pkg/shell/superuser.jsx:264 pkg/shell/hosts_dialog.jsx:285
+#: pkg/shell/hosts_dialog.jsx:382 pkg/shell/hosts_dialog.jsx:514
+#: pkg/shell/hosts_dialog.jsx:891 pkg/shell/active-pages-modal.jsx:100
+msgid "Cancel"
+msgstr "取消"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:90
+msgid "Cancel poweroff"
+msgstr "取消关机"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:94
+msgid "Cancel reboot"
+msgstr "取消重启"
+
+#: pkg/systemd/services-list.jsx:88
+msgid "Cannot be enabled"
+msgstr "无法被启用"
+
+#: pkg/shell/indexes.jsx:467
+msgid "Cannot connect to an unknown host"
+msgstr "不能连接至未知主机"
+
+#: pkg/lib/cockpit.js:3875
+msgid "Cannot forward login credentials"
+msgstr "无法转发登录凭证"
+
+#: pkg/systemd/overview-cards/realmd.jsx:74
+msgid "Cannot join a domain because realmd is not available on this system"
+msgstr "无法加入域,因为 realmd 在此系统上不可用"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:133
+#: pkg/lib/cockpit-components-shutdown.jsx:167
+msgid "Cannot schedule event in the past"
+msgstr "无法调度以前的事件"
+
+#: pkg/storaged/lvm2/volume-group.jsx:387 pkg/storaged/drive/drive.jsx:125
+#: pkg/storaged/mdraid/mdraid.jsx:296 pkg/storaged/btrfs/volume.jsx:127
+msgid "Capacity"
+msgstr "容量"
+
+#: pkg/networkmanager/network-interface.jsx:239
+msgid "Carrier"
+msgstr "载体"
+
+#: pkg/users/expiration-dialogs.js:115 pkg/users/expiration-dialogs.js:217
+#: pkg/users/shell-dialog.js:78 pkg/lib/serverTime.js:729
+#: pkg/systemd/overview-cards/configurationCard.jsx:283
+#: pkg/storaged/iscsi/create-dialog.jsx:171 pkg/storaged/stratis/pool.jsx:535
+msgid "Change"
+msgstr "变更"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:181
+msgid "Change cryptographic policy"
+msgstr "更改加密策略"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:281
+msgid "Change host name"
+msgstr "修改主机名"
+
+#: pkg/storaged/overview/overview.jsx:152
+msgid "Change iSCSI initiater name"
+msgstr "变更 iSCSI Initiater 名称"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:166
+msgid "Change iSCSI initiator name"
+msgstr "变更 iSCSI Initiator 名称"
+
+#: pkg/storaged/btrfs/volume.jsx:97
+msgid "Change label"
+msgstr "更改标签"
+
+#: pkg/storaged/stratis/pool.jsx:404 pkg/storaged/crypto/keyslots.jsx:478
+msgid "Change passphrase"
+msgstr "修改密码"
+
+#: pkg/shell/credentials.jsx:252
+msgid "Change password"
+msgstr "变更密码"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:307
+msgid "Change performance profile"
+msgstr "变更性能配置"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:311
+msgid "Change profile"
+msgstr "变更配置"
+
+#: pkg/users/shell-dialog.js:70
+msgid "Change shell"
+msgstr "修改 shell"
+
+#: pkg/lib/serverTime.js:722
+msgid "Change system time"
+msgstr "修改系统时间"
+
+#: pkg/shell/hosts_dialog.jsx:809
+msgid "Change the password of $0"
+msgstr "更改 $0 的密码"
+
+#: pkg/networkmanager/interfaces.js:1656
+msgid "Change the settings"
+msgstr "修改设置"
+
+#: pkg/static/login.html:81 pkg/shell/hosts_dialog.jsx:473
+msgid ""
+"Changed keys are often the result of an operating system reinstallation. "
+"However, an unexpected change may indicate a third-party attempt to "
+"intercept your connection."
+msgstr ""
+"密钥的变化通常是操作系统重新安装的结果。但是,意料外的变化可能代表有第三方尝"
+"试截获您的连接。"
+
+#: pkg/storaged/partitions/partition.jsx:188
+msgid "Changing partition types might prevent the system from booting."
+msgstr "更改分区类型可能会阻止系统引导。"
+
+#: pkg/networkmanager/interfaces.js:1655
+msgid ""
+"Changing the settings will break the connection to the server, and will make "
+"the administration UI unavailable."
+msgstr "修改设置将会中断与服务器的连接,并将导致管理界面不可用。"
+
+#: pkg/packagekit/updates.jsx:909
+msgid "Check for updates"
+msgstr "检查更新"
+
+#: pkg/storaged/crypto/tang.jsx:124
+msgid ""
+"Check that the SHA-256 or SHA-1 hash from the command matches this dialog."
+msgstr "检查命令中的 SHA-256 或 SHA-1 哈希值是否与此对话框匹配。"
+
+#: pkg/storaged/crypto/tang.jsx:113
+msgid "Check the key hash with the Tang server."
+msgstr "使用 Tang 服务器检查密钥哈希。"
+
+#: pkg/storaged/jobs-panel.jsx:52
+msgid "Checking $target"
+msgstr "检查 $target"
+
+#: pkg/networkmanager/interfaces.js:803
+msgid "Checking IP"
+msgstr "正在检查 IP"
+
+#: pkg/storaged/jobs-panel.jsx:68
+msgid "Checking MDRAID device $target"
+msgstr "检查 MDRAID 设备 $target"
+
+#: pkg/storaged/jobs-panel.jsx:69
+msgid "Checking and repairing MDRAID device $target"
+msgstr "检查并修复 MDRAID 设备 $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:229
+msgid "Checking for $0 package"
+msgstr "检查 $0 软件包"
+
+#: pkg/storaged/crypto/keyslots.jsx:265
+msgid "Checking for NBDE support in the initrd"
+msgstr "在 initrd 中检查 NBDE 支持"
+
+#: pkg/apps/application-list.jsx:158
+msgid "Checking for new applications"
+msgstr "检查新的应用"
+
+#: pkg/packagekit/updates.jsx:1389
+msgid "Checking for package updates..."
+msgstr "正在检查软件包更新..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:152
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:31
+msgid "Checking installed software"
+msgstr "检查安装的软件"
+
+#: pkg/storaged/dialog.jsx:1267
+msgid "Checking related processes"
+msgstr "检查相关的进程"
+
+#: pkg/packagekit/updates.jsx:1399
+msgid "Checking software status"
+msgstr "检查软件状态"
+
+#: pkg/shell/shell-modals.jsx:122
+msgid "Choose the language to be used in the application"
+msgstr "选择应用程序使用的语言"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:81
+msgid "Chunk size"
+msgstr "区块大小"
+
+#: pkg/systemd/hwinfo.jsx:276
+msgid "Class"
+msgstr "等级"
+
+#: pkg/storaged/jobs-panel.jsx:60
+msgid "Cleaning up for $target"
+msgstr "清理 $target"
+
+#: pkg/systemd/service-details.jsx:162
+msgid "Clear 'Failed to start'"
+msgstr "清除 'Failed to start'"
+
+#: pkg/systemd/services-list.jsx:58 pkg/systemd/logsJournal.jsx:277
+msgid "Clear all filters"
+msgstr "清除所有过滤规则"
+
+#: pkg/shell/nav.jsx:158
+msgid "Clear search"
+msgstr "清除搜寻"
+
+#: pkg/storaged/crypto/encryption.jsx:228
+msgid "Cleartext device"
+msgstr "明文设备"
+
+#: pkg/systemd/overview-cards/realmd.jsx:304
+msgid "Client software"
+msgstr "客户端软件"
+
+#: pkg/users/dialog-utils.js:58 pkg/networkmanager/interfaces.js:43
+#: pkg/lib/cockpit-components-modifications.jsx:76
+#: pkg/lib/cockpit-components-terminal.jsx:230 pkg/apps/utils.jsx:87
+#: pkg/systemd/overview-cards/realmd.jsx:278
+#: pkg/systemd/overview-cards/configurationCard.jsx:198
+#: pkg/storaged/dialog.jsx:456 pkg/shell/shell-modals.jsx:192
+#: pkg/shell/credentials.jsx:81 pkg/shell/superuser.jsx:190
+#: pkg/shell/superuser.jsx:198 pkg/shell/hosts_dialog.jsx:92
+msgid "Close"
+msgstr "关闭"
+
+#: pkg/shell/active-pages-modal.jsx:99
+msgid "Close selected pages"
+msgstr "关闭已选的页面"
+
+#: src/ws/cockpit.appdata.xml.in:7
+msgid "Cockpit"
+msgstr "Cockpit"
+
+#: pkg/static/login.js:452
+msgid "Cockpit authentication is configured incorrectly."
+msgstr "Cockpit 验证的配置不正确。"
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:6
+msgid "Cockpit configuration of NetworkManager and Firewalld"
+msgstr "NetworkManager 的 Cockpit 配置和防火墙"
+
+#: pkg/lib/cockpit.js:3881
+msgid "Cockpit could not contact the given host."
+msgstr "Cockpit 无法联系指定的主机。"
+
+#: pkg/shell/shell-modals.jsx:194
+msgid "Cockpit had an unexpected internal error."
+msgstr "Cockpit 出现意外的内部错误。"
+
+#: src/ws/cockpit.appdata.xml.in:10
+msgid ""
+"Cockpit is a server manager that makes it easy to administer your Linux "
+"servers via a web browser. Jumping between the terminal and the web tool is "
+"no problem. A service started via Cockpit can be stopped via the terminal. "
+"Likewise, if an error occurs in the terminal, it can be seen in the Cockpit "
+"journal interface."
+msgstr ""
+"Cockpit 是一个服务器管理工具,可以方便地通过浏览器来管理您的 Linux 服务器。在"
+"终端和 web 工具间自由切换将不是问题。通过 Cockpit 启动的服务可以通过终端停"
+"止。同样,如果在终端中发生错误, 也可以在 Cockpit 的日志接口中看到。"
+
+#: pkg/shell/shell-modals.jsx:70
+msgid "Cockpit is an interactive Linux server admin interface."
+msgstr "Cockpit 是一个交互式 Linux 服务器管理接口。"
+
+#: pkg/lib/cockpit.js:3879
+msgid "Cockpit is not compatible with the software on the system."
+msgstr "Cockpit 与系统上的软件不兼容。"
+
+#: pkg/shell/hosts_dialog.jsx:89
+msgid "Cockpit is not installed"
+msgstr "Cockpit 未安装"
+
+#: pkg/lib/cockpit.js:3873
+msgid "Cockpit is not installed on the system."
+msgstr "Cockpit 未安装在系统上。"
+
+#: src/ws/cockpit.appdata.xml.in:18
+msgid ""
+"Cockpit is perfect for new sysadmins, allowing them to easily perform simple "
+"tasks such as storage administration, inspecting journals and starting and "
+"stopping services. You can monitor and administer several servers at the "
+"same time. Just add them with a single click and your machines will look "
+"after its buddies."
+msgstr ""
+"Cockpit 是完美的系统管理员工具,它可以轻松完成简单的任务, 如存储管理, 检查日"
+"志信息,以及启动/停止服务。 您可以同时监控和管理多个服务器。点一键就可以添加"
+"服务器,并开始进行管理。"
+
+#: pkg/static/login.js:201
+msgid "Cockpit might not render correctly in your browser"
+msgstr "Cockpit 可能无法在您的浏览器中正确呈现"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:7
+msgid "Collect and package diagnostic and support data"
+msgstr "收集并打包诊断和支持数据"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:6
+msgid "Collect kernel crash dumps"
+msgstr "收集内核崩溃转储"
+
+#: pkg/metrics/metrics.jsx:1494
+msgid "Collect metrics"
+msgstr "收集指标"
+
+#: pkg/shell/hosts_dialog.jsx:269
+msgid "Color"
+msgstr "颜色"
+
+#: pkg/networkmanager/firewall.jsx:688 pkg/networkmanager/firewall.jsx:697
+msgid "Comma-separated ports, ranges, and services are accepted"
+msgstr "可以接受以逗号分割的端口、范围和服务"
+
+#: pkg/systemd/timer-dialog.jsx:174 pkg/storaged/dialog.jsx:1326
+#: pkg/storaged/dialog.jsx:1346
+msgid "Command"
+msgstr "命令"
+
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "Command not found"
+msgstr "未找到命令"
+
+#: pkg/shell/credentials.jsx:195
+msgid "Comment"
+msgstr "注释"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:97
+msgid "Communication with tuned has failed"
+msgstr "与 tuned 通信失败"
+
+#: pkg/lib/machine-info.js:85
+msgid "Compact PCI"
+msgstr "紧凑型 PCI"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:53
+msgid "Compatible with all systems and devices (MBR)"
+msgstr "兼容所有系统和设备 (MBR)"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:56
+msgid "Compatible with modern system and hard disks > 2TB (GPT)"
+msgstr "兼容现代系统,且硬盘空间大于 2TB (GPT)"
+
+#: pkg/kdump/kdump-view.jsx:319
+msgid "Compress crash dumps to save space"
+msgstr "对崩溃转储数据进行压缩以节省空间"
+
+#: pkg/kdump/kdump-view.jsx:314 pkg/storaged/lvm2/vdo-pool.jsx:84
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:229
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:325
+msgid "Compression"
+msgstr "压缩"
+
+#: pkg/systemd/service-details.jsx:605
+msgid "Condition $0=$1 was not met"
+msgstr "条件 $0=$1 不满足"
+
+#: pkg/systemd/service-details.jsx:677
+msgid "Condition failed"
+msgstr "条件失败"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:62
+msgid "Configuration"
+msgstr "配置"
+
+#: pkg/networkmanager/interfaces.js:797
+msgid "Configuring"
+msgstr "配置中"
+
+#: pkg/networkmanager/interfaces.js:801
+msgid "Configuring IP"
+msgstr "正在配置 IP"
+
+#: pkg/kdump/manifest.json:0
+msgid "Configuring kdump"
+msgstr "配置 kdump"
+
+#: pkg/systemd/manifest.json:0
+msgid "Configuring system settings"
+msgstr "配置系统设置"
+
+#: pkg/storaged/block/format-dialog.jsx:390
+#: pkg/storaged/stratis/create-dialog.jsx:84 pkg/storaged/stratis/pool.jsx:384
+#: pkg/storaged/stratis/pool.jsx:413
+msgid "Confirm"
+msgstr "确认"
+
+#: pkg/systemd/service-details.jsx:713 pkg/storaged/stratis/filesystem.jsx:137
+msgid "Confirm deletion of $0"
+msgstr "确认删除 $0"
+
+#: pkg/shell/hosts_dialog.jsx:801
+msgid "Confirm key password"
+msgstr "确认密钥密码"
+
+#: pkg/shell/hosts_dialog.jsx:817
+msgid "Confirm new key password"
+msgstr "确认新的密钥密码"
+
+#: pkg/users/password-dialogs.js:148
+msgid "Confirm new password"
+msgstr "确认新密码"
+
+#: pkg/users/account-create-dialog.js:140 pkg/shell/credentials.jsx:268
+msgid "Confirm password"
+msgstr "确认密码"
+
+#: pkg/networkmanager/firewall.jsx:913
+msgid "Confirm removal of $0"
+msgstr "确认移除 $0"
+
+#: pkg/storaged/crypto/keyslots.jsx:587
+msgid "Confirm removal with an alternate passphrase"
+msgstr "使用一个备用密码短语确认删除"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:58 pkg/storaged/mdraid/mdraid.jsx:71
+msgid "Confirm stopping of $0"
+msgstr "确认停止 $0"
+
+#: pkg/systemd/service-details.jsx:573
+msgid "Conflicted by"
+msgstr "冲突"
+
+#: pkg/systemd/service-details.jsx:572
+msgid "Conflicts"
+msgstr "冲突"
+
+#: pkg/networkmanager/network-interface.jsx:324
+msgid "Connect automatically"
+msgstr "自动连接"
+
+#: pkg/static/login.html:127
+msgid "Connect to"
+msgstr "连接到"
+
+#: pkg/static/login.js:660
+msgid "Connect to:"
+msgstr "连接到:"
+
+#: pkg/selinux/setroubleshoot-view.jsx:330
+msgid "Connecting to SETroubleshoot daemon..."
+msgstr "连接到 SETroubleshoot 守护进程..."
+
+#: pkg/systemd/services.jsx:291
+msgid "Connecting to dbus failed: $0"
+msgstr "连接到 dbus 失败:$0"
+
+#: pkg/shell/indexes.jsx:462
+msgid "Connecting to the machine"
+msgstr "正在连接至主机"
+
+#: pkg/shell/hosts.jsx:163
+msgid "Connection error"
+msgstr "连接错误"
+
+#: pkg/shell/failures.jsx:38
+msgid "Connection failed"
+msgstr "连接失败"
+
+#: pkg/lib/cockpit.js:3871
+msgid "Connection has timed out."
+msgstr "连接超时。"
+
+#: pkg/networkmanager/interfaces.js:57
+msgid "Connection will be lost"
+msgstr "连接将丢失"
+
+#: pkg/systemd/service-details.jsx:571
+msgid "Consists of"
+msgstr "组成"
+
+#: pkg/systemd/overview-cards/realmd.jsx:395
+msgid "Contacted domain"
+msgstr "联系的域"
+
+#: pkg/shell/nav.jsx:231
+msgid "Contains:"
+msgstr "包含:"
+
+#: pkg/packagekit/updates.jsx:764
+msgid "Continue"
+msgstr "继续"
+
+#: pkg/shell/shell-modals.jsx:179
+msgid "Continue session"
+msgstr "继续会话"
+
+#: pkg/playground/translate.js:20
+msgid "Control"
+msgstr "控制"
+
+#: pkg/playground/translate.js:23
+msgctxt "key"
+msgid "Control"
+msgstr "控制"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Controller"
+msgstr "控制器"
+
+#: pkg/lib/machine-info.js:90
+msgid "Convertible"
+msgstr "可转换"
+
+#: pkg/shell/credentials.jsx:212 pkg/shell/failures.jsx:44
+#: pkg/shell/hosts_dialog.jsx:475 pkg/shell/hosts_dialog.jsx:478
+#: pkg/shell/hosts_dialog.jsx:493 pkg/shell/hosts_dialog.jsx:495
+msgid "Copied"
+msgstr "复制的"
+
+#: pkg/lib/cockpit-components-terminal.jsx:211 pkg/shell/credentials.jsx:212
+#: pkg/shell/failures.jsx:44 pkg/shell/hosts_dialog.jsx:475
+#: pkg/shell/hosts_dialog.jsx:478 pkg/shell/hosts_dialog.jsx:493
+#: pkg/shell/hosts_dialog.jsx:495
+msgid "Copy"
+msgstr "复制"
+
+#: pkg/lib/cockpit-components-modifications.jsx:73 pkg/systemd/logs.jsx:416
+#: pkg/storaged/crypto/tang.jsx:117
+msgid "Copy to clipboard"
+msgstr "复制到剪贴板"
+
+#: pkg/metrics/metrics.jsx:744
+msgid "Core $0"
+msgstr "内核 $0"
+
+#: pkg/shell/hosts_dialog.jsx:356
+msgid "Could not contact $0"
+msgstr "无法联系 $0"
+
+#: pkg/kdump/kdump-view.jsx:221 pkg/kdump/kdump-view.jsx:617
+msgid "Crash dump location"
+msgstr "崩溃信息存放在"
+
+#: pkg/systemd/reporting.jsx:431
+msgid "Crash reporting"
+msgstr "崩溃报告"
+
+#: pkg/kdump/kdump-view.jsx:388
+msgid "Crash system"
+msgstr "导致系统崩溃"
+
+#: pkg/users/account-create-dialog.js:481 pkg/users/group-create-dialog.js:141
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:193
+#: pkg/storaged/lvm2/volume-group.jsx:120
+#: pkg/storaged/lvm2/create-dialog.jsx:63
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:269
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:58
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/mdraid/create-dialog.jsx:112
+#: pkg/storaged/stratis/create-dialog.jsx:122 pkg/storaged/stratis/pool.jsx:86
+#: pkg/storaged/btrfs/subvolume.jsx:138
+msgid "Create"
+msgstr "创建"
+
+#: pkg/storaged/overview/overview.jsx:146
+msgid "Create LVM2 volume group"
+msgstr "创建 LVM2 卷组"
+
+#: pkg/storaged/overview/overview.jsx:145
+msgid "Create MDRAID device"
+msgstr "创建 MDRAID 设备"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:45
+msgid "Create RAID device"
+msgstr "创建 RAID 设备"
+
+#: pkg/storaged/overview/overview.jsx:147
+#: pkg/storaged/stratis/create-dialog.jsx:48
+msgid "Create Stratis pool"
+msgstr "创建 Stratis 池"
+
+#: pkg/shell/hosts_dialog.jsx:793
+msgid "Create a new SSH key and authorize it"
+msgstr "创建一个新的 SSH 密钥,并对其进行授权"
+
+#: pkg/storaged/stratis/filesystem.jsx:84
+msgid "Create a snapshot of filesystem $0"
+msgstr "创建文件系统 $0 的快照"
+
+#: pkg/users/account-create-dialog.js:557
+msgid "Create account with non-unique UID"
+msgstr "使用非唯一 UID 创建帐户"
+
+#: pkg/users/account-create-dialog.js:532
+msgid "Create account with weak password"
+msgstr "创建具有弱密码的帐户"
+
+#: pkg/users/account-create-dialog.js:507
+msgid "Create and change ownership of home directory"
+msgstr "创建并更改主目录的所有权"
+
+#: pkg/storaged/block/format-dialog.jsx:317 pkg/storaged/stratis/pool.jsx:81
+#: pkg/storaged/btrfs/subvolume.jsx:132
+msgid "Create and mount"
+msgstr "创建并挂载"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Create and start"
+msgstr "创建并启动"
+
+#: pkg/storaged/stratis/pool.jsx:91
+msgid "Create filesystem"
+msgstr "创建文件系统"
+
+#: pkg/networkmanager/dialogs-common.jsx:323
+msgid "Create it"
+msgstr "创建它"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:164
+msgid "Create logical volume"
+msgstr "创建逻辑卷"
+
+#: pkg/users/account-create-dialog.js:466 pkg/users/accounts-list.js:443
+msgid "Create new account"
+msgstr "创建新账户"
+
+#: pkg/storaged/stratis/pool.jsx:307
+msgid "Create new filesystem"
+msgstr "创建新文件系统"
+
+#: pkg/users/accounts-list.js:306 pkg/users/group-create-dialog.js:134
+msgid "Create new group"
+msgstr "创建新组"
+
+#: pkg/storaged/lvm2/volume-group.jsx:264
+msgid "Create new logical volume"
+msgstr "创建新逻辑卷"
+
+#: pkg/lib/cockpit-components-modifications.jsx:92
+msgid "Create new task file with this content."
+msgstr "使用此内容创建新的任务文件。"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:78
+msgid "Create new thinly provisioned logical volume"
+msgstr "创建新的精简配置逻辑卷"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327 pkg/storaged/stratis/pool.jsx:82
+#: pkg/storaged/btrfs/subvolume.jsx:133
+msgid "Create only"
+msgstr "仅创建"
+
+#: pkg/storaged/partitions/partition-table.jsx:47
+msgid "Create partition"
+msgstr "创建分区"
+
+#: pkg/storaged/block/format-dialog.jsx:183
+msgid "Create partition on $0"
+msgstr "在$0上创建分区"
+
+#: pkg/storaged/partitions/actions.jsx:32
+msgid "Create partition table"
+msgstr "创建分区表"
+
+#: pkg/storaged/lvm2/volume-group.jsx:114
+#: pkg/storaged/lvm2/volume-group.jsx:132
+msgid "Create snapshot"
+msgstr "创建快照"
+
+#: pkg/storaged/stratis/filesystem.jsx:108
+msgid "Create snapshot and mount"
+msgstr "创建快照和挂载"
+
+#: pkg/storaged/stratis/filesystem.jsx:109
+msgid "Create snapshot only"
+msgstr "仅创建快照"
+
+#: pkg/storaged/overview/overview.jsx:174
+msgid "Create storage device"
+msgstr "创建存储设备"
+
+#: pkg/storaged/btrfs/subvolume.jsx:143 pkg/storaged/btrfs/subvolume.jsx:291
+msgid "Create subvolume"
+msgstr "创建子卷"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:42
+msgid "Create thin volume"
+msgstr "创建Thin卷"
+
+#: pkg/systemd/timer-dialog.jsx:57 pkg/systemd/timer-dialog.jsx:140
+msgid "Create timer"
+msgstr "创建定时器"
+
+#: pkg/storaged/lvm2/create-dialog.jsx:45
+msgid "Create volume group"
+msgstr "创建卷组"
+
+#: pkg/sosreport/sosreport.jsx:502
+msgid "Created"
+msgstr "创建于"
+
+#: pkg/storaged/jobs-panel.jsx:76
+msgid "Creating LVM2 volume group $target"
+msgstr "创建 LVM2 卷组 $target"
+
+#: pkg/storaged/jobs-panel.jsx:67
+msgid "Creating MDRAID device $target"
+msgstr "创建 MDRAID 设备 $target"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:284
+msgid "Creating VDO device"
+msgstr "创建 VDO 设备"
+
+#: pkg/storaged/jobs-panel.jsx:55
+msgid "Creating filesystem on $target"
+msgstr "在 $target 中创建文件系统"
+
+#: pkg/storaged/jobs-panel.jsx:81
+msgid "Creating logical volume $target"
+msgstr "创建逻辑卷 $target"
+
+#: pkg/storaged/jobs-panel.jsx:59
+msgid "Creating partition $target"
+msgstr "创建分区 $target"
+
+#: pkg/storaged/jobs-panel.jsx:75
+msgid "Creating snapshot of $target"
+msgstr "创建 $target 的快照"
+
+#: pkg/networkmanager/dialogs-common.jsx:322
+msgid ""
+"Creating this $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "创建该 $0 将会中断与服务器的连接,并且将会导致管理界面不可用。"
+
+#: pkg/systemd/logs.jsx:67
+msgid "Critical and above"
+msgstr "Critical 及更高级别"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:154
+msgid ""
+"Cryptographic Policies is a system component that configures the core "
+"cryptographic subsystems, covering the TLS, IPSec, SSH, DNSSec, and Kerberos "
+"protocols."
+msgstr ""
+"加密策略是一个系统组件,它配置内核加密子系统,覆盖 TLS、IPSec、SSH、DNSSec "
+"和 Kerberos 协议。"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:62
+msgid "Cryptographic policy"
+msgstr "加密策略"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "Cryptographic policy is inconsistent"
+msgstr "加密策略不一致"
+
+#: pkg/lib/cockpit-components-terminal.jsx:212
+msgid "Ctrl+Insert"
+msgstr "Ctrl+Insert"
+
+#: pkg/shell/shell-modals.jsx:198
+msgid "Ctrl-Shift-I"
+msgstr "Ctrl+Shift+I"
+
+#: pkg/systemd/logs.jsx:58
+msgid "Current boot"
+msgstr "当前启动"
+
+#: pkg/metrics/metrics.jsx:757
+msgid "Current top CPU usage"
+msgstr "当前 top CPU 用量"
+
+#: pkg/storaged/dialog.jsx:1178
+msgid "Currently in use"
+msgstr "当前在用"
+
+#: pkg/kdump/kdump-view.jsx:535
+msgid "Currently not supported"
+msgstr "目前不支持"
+
+#: pkg/storaged/partitions/partition.jsx:146
+msgid "Custom"
+msgstr "自定义"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:142
+msgid "Custom cryptographic policy"
+msgstr "自定义加密策略"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:56 pkg/storaged/nfs/nfs.jsx:173
+msgid "Custom mount options"
+msgstr "自定义挂载选项"
+
+#: pkg/networkmanager/firewall.jsx:639
+msgid "Custom ports"
+msgstr "自定义端口"
+
+#: pkg/storaged/partitions/partition.jsx:180
+msgid "Custom type"
+msgstr "自定义类型"
+
+#: pkg/networkmanager/firewall.jsx:839
+msgid "Custom zones"
+msgstr "自定义区域"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:108
+msgid "DEFAULT with SHA-1 signature verification allowed."
+msgstr "允许带有 SHA-1 签名验证的 DEFAULT。"
+
+#: pkg/networkmanager/ip-settings.jsx:237
+msgid "DNS"
+msgstr "DNS"
+
+#: pkg/networkmanager/network-interface.jsx:303
+msgid "DNS $val"
+msgstr "DNS $val"
+
+#: pkg/networkmanager/ip-settings.jsx:285
+msgid "DNS search domains"
+msgstr "DNS 搜索域"
+
+#: pkg/networkmanager/network-interface.jsx:306
+msgid "DNS search domains $val"
+msgstr "DNS 搜索域 $val"
+
+#: pkg/systemd/timer-dialog.jsx:245
+msgid "Daily"
+msgstr "每日"
+
+#: pkg/packagekit/updates.jsx:934
+msgid "Danger alert:"
+msgstr "危险警报:"
+
+#: pkg/systemd/terminal.jsx:174 pkg/shell/topnav.jsx:193
+msgid "Dark"
+msgstr "暗色"
+
+#: pkg/storaged/stratis/pool.jsx:197
+msgid "Data"
+msgstr "数据"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:80
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:144
+msgid "Data used"
+msgstr "使用的数据"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:143
+msgid ""
+"Data will be stored as two copies and also in an alternating fashion on the "
+"selected physical volumes, to improve both reliability and performance. At "
+"least four volumes need to be selected."
+msgstr ""
+"数据将存储为两份副本,并交替存储在选定的物理卷上,以提高可靠性和性能。至少需"
+"要选择四个卷。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:142
+msgid ""
+"Data will be stored as two or more copies on the selected physical volumes, "
+"to improve reliability. At least two volumes need to be selected."
+msgstr ""
+"数据将以两个或多个副本的形式存储在所选物理卷上,以提高可靠性。至少需要选择两"
+"个卷。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:141
+msgid ""
+"Data will be stored on the selected physical volumes in an alternating "
+"fashion to improve performance. At least two volumes need to be selected."
+msgstr "数据将交替存储在选定的物理卷上以提高性能。至少需要选择两个卷。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:144
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. At least three volumes need to be "
+"selected."
+msgstr ""
+"数据将存储在选定的物理卷上,这样其中一个卷丢失也不会影响数据。至少需要选择三"
+"个卷。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:145
+msgid ""
+"Data will be stored on the selected physical volumes so that one of them can "
+"be lost without affecting the data. Data is also stored in an alternating "
+"fashion to improve performance. At least three volumes need to be selected."
+msgstr ""
+"数据将存储在所选物理卷上,以便在其中一个出现故障时不会影响数据。数据也会以交"
+"替的方式使用以提高性能。至少需要选择三个卷。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:146
+msgid ""
+"Data will be stored on the selected physical volumes so that up to two of "
+"them can be lost at the same time without affecting the data. Data is also "
+"stored in an alternating fashion to improve performance. At least five "
+"volumes need to be selected."
+msgstr ""
+"数据将存储在所选物理卷上,以便在其中最多两个同时出现故障时不会影响数据。数据"
+"也会以交替的方式使用以提高性能。至少需要选择五个卷。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:140
+msgid ""
+"Data will be stored on the selected physical volumes without any additional "
+"redundancy or performance improvements."
+msgstr "数据将存储在所选物理卷上,它们没有额外的冗余功能或性能改进。"
+
+#: pkg/systemd/logs.jsx:330
+msgid ""
+"Date specifications should be of the format YYYY-MM-DD hh:mm:ss. "
+"Alternatively the strings 'yesterday', 'today', 'tomorrow' are understood. "
+"'now' refers to the current time. Finally, relative times may be specified, "
+"prefixed with '-' or '+'"
+msgstr ""
+"日期规格应为 YYYY-MM-DD hh:mm:ss 格式。另外,也可以使用字符串 'yesterday', "
+"'today', 'tomorrow'。'now' 代表当前时间。另外,还可以指定相对时间,前缀为 "
+"'-' 或 '+'"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:161
+#: pkg/storaged/lvm2/block-logical-volume.jsx:216
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:43
+msgid "Deactivate"
+msgstr "取消激活"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:158
+msgid "Deactivate logical volume $0/$1?"
+msgstr "取消激活逻辑卷 $0/$1?"
+
+#: pkg/networkmanager/interfaces.js:809
+msgid "Deactivating"
+msgstr "取消激活中"
+
+#: pkg/storaged/jobs-panel.jsx:74
+msgid "Deactivating $target"
+msgstr "取消激活 $target"
+
+#: pkg/systemd/logs.jsx:72
+msgid "Debug and above"
+msgstr "Debug 及更高级别"
+
+#: pkg/systemd/terminal.jsx:159
+msgid "Decrease by one"
+msgstr "减一"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:263
+msgid "Dedicated parity (RAID 4)"
+msgstr "专用奇偶校验 (RAID 4)"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:88
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:234
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:334
+msgid "Deduplication"
+msgstr "复制"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:37 pkg/shell/topnav.jsx:187
+msgid "Default"
+msgstr "默认"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:201 pkg/systemd/timer-dialog.jsx:200
+#: pkg/systemd/timer-dialog.jsx:209
+msgid "Delay"
+msgstr "延时"
+
+#: pkg/systemd/timer-dialog.jsx:216
+msgid "Delay must be a number"
+msgstr "延迟必须是一个数字"
+
+#: pkg/users/delete-account-dialog.js:60 pkg/users/delete-group-dialog.js:51
+#: pkg/users/account-details.js:227 pkg/networkmanager/firewall.jsx:121
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:914
+#: pkg/networkmanager/network-interface.jsx:725 pkg/sosreport/sosreport.jsx:358
+#: pkg/sosreport/sosreport.jsx:470 pkg/systemd/service-details.jsx:150
+#: pkg/systemd/service-details.jsx:719 pkg/systemd/abrtLog.jsx:290
+#: pkg/storaged/lvm2/block-logical-volume.jsx:79
+#: pkg/storaged/lvm2/block-logical-volume.jsx:222
+#: pkg/storaged/lvm2/volume-group.jsx:97
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:109
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:44
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:42
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:122
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:152
+#: pkg/storaged/mdraid/mdraid.jsx:126 pkg/storaged/mdraid/mdraid.jsx:226
+#: pkg/storaged/stratis/filesystem.jsx:141
+#: pkg/storaged/stratis/filesystem.jsx:179 pkg/storaged/stratis/pool.jsx:153
+#: pkg/storaged/btrfs/subvolume.jsx:227 pkg/storaged/btrfs/subvolume.jsx:305
+#: pkg/storaged/partitions/partition-table.jsx:61
+#: pkg/storaged/partitions/partition.jsx:58
+#: pkg/storaged/partitions/partition.jsx:104
+msgid "Delete"
+msgstr "删除"
+
+#: pkg/users/delete-account-dialog.js:53
+#: pkg/networkmanager/network-interface.jsx:117
+msgid "Delete $0"
+msgstr "删除 $0"
+
+#: pkg/users/accounts-list.js:80
+msgid "Delete account"
+msgstr "删除账户"
+
+#: pkg/users/delete-account-dialog.js:33
+msgid "Delete files"
+msgstr "删除文件"
+
+#: pkg/users/group-actions.jsx:45 pkg/storaged/lvm2/volume-group.jsx:248
+msgid "Delete group"
+msgstr "删除用户组"
+
+#: pkg/storaged/stratis/pool.jsx:292
+msgid "Delete pool"
+msgstr "删除池"
+
+#: pkg/sosreport/sosreport.jsx:350
+msgid "Delete report permanently?"
+msgstr "永久删除报告?"
+
+#: pkg/networkmanager/network-interface.jsx:116
+msgid ""
+"Deleting $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "删除 $0 将会中断与服务器的连接,并且将会导致管理界面不可用。"
+
+#: pkg/storaged/jobs-panel.jsx:58 pkg/storaged/jobs-panel.jsx:72
+msgid "Deleting $target"
+msgstr "删除 $target"
+
+#: pkg/storaged/jobs-panel.jsx:77
+msgid "Deleting LVM2 volume group $target"
+msgstr "删除 LVM2 卷组 $target"
+
+#: pkg/storaged/stratis/pool.jsx:152
+msgid "Deleting a Stratis pool will erase all data it contains."
+msgstr "删除 Stratis 池将会清除其包含的所有数据。"
+
+#: pkg/storaged/stratis/filesystem.jsx:140
+msgid "Deleting a filesystem will delete all data in it."
+msgstr "删除文件系统将会删除其中的所有数据。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:78
+msgid "Deleting a logical volume will delete all data in it."
+msgstr "删除逻辑卷将擦除其中的所有数据。"
+
+#: pkg/storaged/partitions/partition.jsx:57
+msgid "Deleting a partition will delete all data in it."
+msgstr "删除分区将删除其中的所有数据。"
+
+#: pkg/storaged/mdraid/mdraid.jsx:127
+msgid "Deleting erases all data on a MDRAID device."
+msgstr "删除将清除 MDRAID 设备中的所有数据。"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:123
+msgid "Deleting erases all data on a VDO device."
+msgstr "删除会擦除 VDO 设备上的所有数据."
+
+#: pkg/storaged/lvm2/volume-group.jsx:96
+msgid "Deleting erases all data on a volume group."
+msgstr "删除会擦除卷组上的所有数据."
+
+#: pkg/storaged/btrfs/subvolume.jsx:228
+msgid "Deleting erases all data on this subvolume and all it's children."
+msgstr "删除会擦除这个子卷组上的所有数据,以及它的子卷中的所有数据。"
+
+#: pkg/systemd/service-details.jsx:616
+msgid "Deletion will remove the following files:"
+msgstr "删除操作会删除以下文件:"
+
+#: pkg/networkmanager/firewall.jsx:706 pkg/networkmanager/firewall.jsx:850
+#: pkg/systemd/timer-dialog.jsx:166 pkg/storaged/dialog.jsx:1347
+msgid "Description"
+msgstr "描述"
+
+#: pkg/lib/machine-info.js:62
+msgid "Desktop"
+msgstr "桌面"
+
+#: pkg/lib/machine-info.js:91
+msgid "Detachable"
+msgstr "可拆开"
+
+#: pkg/systemd/overview-cards/realmd.jsx:416 pkg/packagekit/updates.jsx:451
+#: pkg/shell/credentials.jsx:109
+msgid "Details"
+msgstr "详情"
+
+#: pkg/playground/manifest.json:0
+msgid "Development"
+msgstr "开发"
+
+#: pkg/storaged/mdraid/mdraid.jsx:295 pkg/storaged/dialog.jsx:1133
+#: pkg/storaged/dialog.jsx:1238 pkg/metrics/metrics.jsx:785
+msgid "Device"
+msgstr "设备"
+
+#: pkg/storaged/drive/drive.jsx:132 pkg/storaged/block/other.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:287
+msgid "Device file"
+msgstr "设备文件"
+
+#: pkg/storaged/partitions/actions.jsx:27
+msgid "Device is read-only"
+msgstr "设备只读"
+
+#: pkg/storaged/block/other.jsx:60
+msgid "Device number"
+msgstr "设备号"
+
+#: pkg/sosreport/index.html:22 pkg/sosreport/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:5
+msgid "Diagnostic reports"
+msgstr "诊断报告"
+
+#: pkg/kdump/kdump-view.jsx:256 pkg/kdump/kdump-view.jsx:277
+#: pkg/kdump/kdump-view.jsx:303
+msgid "Directory"
+msgstr "目录"
+
+#: pkg/kdump/kdump-client.js:121
+msgid "Directory $0 isn't writable or doesn't exist."
+msgstr "目录 $0 不可写或不存在。"
+
+#: pkg/systemd/hwinfo.jsx:203
+msgid "Disable simultaneous multithreading"
+msgstr "禁用同步多线程"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Disable the firewall"
+msgstr "禁用防火墙"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:233
+msgid "Disable tuned"
+msgstr "禁用 tuned"
+
+#: pkg/networkmanager/firewall-switch.jsx:80
+#: pkg/networkmanager/ip-settings.jsx:46 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:246 pkg/systemd/services.jsx:687
+#: pkg/systemd/service-details.jsx:442 pkg/packagekit/autoupdates.jsx:344
+#: pkg/packagekit/kpatch.jsx:250
+msgid "Disabled"
+msgstr "禁用"
+
+#: pkg/users/account-details.js:276
+msgid "Disallow interactive password"
+msgstr "禁用交互式密码"
+
+#: pkg/users/account-create-dialog.js:127
+msgid "Disallow password authentication"
+msgstr "不允许密码验证"
+
+#: pkg/systemd/service-details.jsx:179
+msgid "Disallow running (mask)"
+msgstr "不允许运行 (屏蔽)"
+
+#: pkg/storaged/iscsi/session.jsx:55
+msgid "Disconnect"
+msgstr "断开"
+
+#: pkg/shell/indexes.jsx:160
+msgid "Disconnected"
+msgstr "已断开连接"
+
+#: pkg/metrics/metrics.jsx:137 pkg/metrics/metrics.jsx:138
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1939
+#: pkg/metrics/metrics.jsx:1951
+msgid "Disk I/O"
+msgstr "磁盘 I/O"
+
+#: pkg/storaged/drive/drive.jsx:106
+msgid "Disk is OK"
+msgstr "磁盘良好"
+
+#: pkg/storaged/drive/drive.jsx:105
+msgid "Disk is failing"
+msgstr "磁盘失败"
+
+#: pkg/storaged/crypto/keyslots.jsx:140
+msgid "Disk passphrase"
+msgstr "磁盘密码"
+
+#: pkg/storaged/lvm2/volume-group.jsx:198
+#: pkg/storaged/lvm2/create-dialog.jsx:52
+#: pkg/storaged/mdraid/create-dialog.jsx:99 pkg/storaged/mdraid/mdraid.jsx:156
+#: pkg/storaged/mdraid/mdraid.jsx:299 pkg/metrics/metrics.jsx:918
+msgid "Disks"
+msgstr "磁盘"
+
+#: pkg/metrics/metrics.jsx:792
+msgid "Disks usage"
+msgstr "磁盘使用"
+
+#: pkg/storaged/lvm2/volume-group.jsx:371
+msgid "Dismiss"
+msgstr "清除"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss $0 alert"
+msgid_plural "Dismiss $0 alerts"
+msgstr[0] "解除 $0 警报"
+
+#: pkg/selinux/setroubleshoot-view.jsx:398
+msgid "Dismiss selected alerts"
+msgstr "解除所选的警报"
+
+#: pkg/shell/shell-modals.jsx:115 pkg/shell/topnav.jsx:205
+msgid "Display language"
+msgstr "显示语言"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:264
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:87
+msgid "Distributed parity (RAID 5)"
+msgstr "分布式奇偶校验 (RAID 5)"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:82
+msgid "Do not mount"
+msgstr "不要挂载"
+
+#: pkg/storaged/filesystem/mismounting.jsx:206
+#: pkg/storaged/filesystem/mismounting.jsx:218
+msgid "Do not mount automatically on boot"
+msgstr "不要在启动时自动安装"
+
+#: pkg/lib/machine-info.js:71
+msgid "Docking station"
+msgstr "扩展坞"
+
+#: pkg/systemd/services-list.jsx:84
+msgid "Does not automatically start"
+msgstr "不自动启动"
+
+#: pkg/storaged/block/format-dialog.jsx:130
+msgid "Does not mount during boot"
+msgstr "在引导过程中不挂载"
+
+#: pkg/systemd/overview-cards/realmd.jsx:284
+#: pkg/systemd/overview-cards/configurationCard.jsx:80
+msgid "Domain"
+msgstr "域"
+
+#: pkg/systemd/overview-cards/realmd.jsx:281
+msgctxt "dialog-title"
+msgid "Domain"
+msgstr "域"
+
+#: pkg/systemd/overview-cards/realmd.jsx:440
+msgid "Domain address"
+msgstr "域地址"
+
+#: pkg/systemd/overview-cards/realmd.jsx:446
+msgid "Domain administrator name"
+msgstr "域管理员用户名"
+
+#: pkg/systemd/overview-cards/realmd.jsx:449
+msgid "Domain administrator password"
+msgstr "域管理员密码"
+
+#: pkg/systemd/overview-cards/realmd.jsx:396
+msgid "Domain could not be contacted"
+msgstr "无法联系到域"
+
+#: pkg/systemd/overview-cards/realmd.jsx:397
+msgid "Domain is not supported"
+msgstr "不支持域"
+
+#: pkg/systemd/timer-dialog.jsx:242
+msgid "Don't repeat"
+msgstr "不要重复"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:265
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:92
+msgid "Double distributed parity (RAID 6)"
+msgstr "双重分布式奇偶校验 (RAID 6)"
+
+#: pkg/sosreport/sosreport.jsx:460 pkg/sosreport/sosreport.jsx:466
+msgid "Download"
+msgstr "下载"
+
+#: pkg/static/login.html:42
+msgid "Download a new browser for free"
+msgstr "免费下载新的浏览器"
+
+#: pkg/packagekit/updates.jsx:110
+msgid "Downloaded"
+msgstr "已下载"
+
+#: pkg/packagekit/updates.jsx:104
+msgid "Downloading"
+msgstr "正在下载"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:189
+#: pkg/storaged/crypto/keyslots.jsx:218
+msgid "Downloading $0"
+msgstr "正在下载 $0"
+
+#: pkg/storaged/drive/drive.jsx:75
+msgid "Drive"
+msgstr "驱动"
+
+#: pkg/lib/machine-info.js:240 pkg/systemd/hw-detect.js:92
+msgid "Dual rank"
+msgstr "双通道"
+
+#: pkg/storaged/partitions/partition.jsx:115
+#: pkg/storaged/partitions/partition.jsx:123
+msgid "EFI system partition"
+msgstr "EFI 系统分区"
+
+#: pkg/networkmanager/firewall.jsx:119 pkg/kdump/kdump-view.jsx:476
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:231
+#: pkg/storaged/nfs/nfs.jsx:312 pkg/storaged/crypto/keyslots.jsx:725
+#: pkg/shell/hosts.jsx:167 pkg/shell/indexes.jsx:352
+msgid "Edit"
+msgstr "编辑"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:50
+msgid "Edit /etc/motd"
+msgstr "编辑 /etc/motd"
+
+#: pkg/storaged/crypto/keyslots.jsx:505
+msgid "Edit Tang keyserver"
+msgstr "编辑Tang keyserver"
+
+#: pkg/networkmanager/vlan.jsx:91
+msgid "Edit VLAN settings"
+msgstr "编辑 VLAN 设置"
+
+#: pkg/networkmanager/wireguard.jsx:205
+msgid "Edit WireGuard VPN"
+msgstr "编辑 WireGuard VPN"
+
+#: pkg/networkmanager/bond.jsx:135
+msgid "Edit bond settings"
+msgstr "编辑绑定设置"
+
+#: pkg/networkmanager/bridge.jsx:96
+msgid "Edit bridge settings"
+msgstr "编辑网桥设置"
+
+#: pkg/networkmanager/firewall.jsx:596
+msgid "Edit custom service in $0 zone"
+msgstr "编辑 $0 区中的自定义服务"
+
+#: pkg/shell/hosts_dialog.jsx:251
+msgid "Edit host"
+msgstr "编辑主机"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Edit hosts"
+msgstr "编辑多个主机"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:113
+msgid "Edit motd"
+msgstr "编辑 motd"
+
+#: pkg/storaged/filesystem/filesystem.jsx:100
+#: pkg/storaged/stratis/filesystem.jsx:174 pkg/storaged/btrfs/subvolume.jsx:263
+#, fuzzy
+#| msgid "Mount point"
+msgid "Edit mount point"
+msgstr "挂载点"
+
+#: pkg/networkmanager/network-main.jsx:161
+msgid "Edit rules and zones"
+msgstr "编辑规则和区域"
+
+#: pkg/networkmanager/firewall.jsx:595
+msgid "Edit service"
+msgstr "编辑服务"
+
+#: pkg/networkmanager/firewall.jsx:119
+msgid "Edit service $0"
+msgstr "编辑服务 $0"
+
+#: pkg/networkmanager/team.jsx:154
+msgid "Edit team settings"
+msgstr "编辑团队设置"
+
+#: pkg/users/accounts-list.js:59
+msgid "Edit user"
+msgstr "编辑用户"
+
+#: pkg/storaged/crypto/keyslots.jsx:727
+msgid "Editing a key requires a free slot"
+msgstr "编辑密钥需要一个空闲插槽"
+
+#: pkg/storaged/jobs-panel.jsx:41
+msgid "Ejecting $target"
+msgstr "弹出 $target"
+
+#: pkg/lib/machine-info.js:93
+msgid "Embedded PC"
+msgstr "嵌入式 PC"
+
+#: pkg/playground/translate.html:57 pkg/playground/translate.js:11
+msgid "Empty"
+msgstr "空"
+
+#: pkg/playground/translate.html:62 pkg/playground/translate.js:14
+#: pkg/playground/translate.js:17
+msgctxt "verb"
+msgid "Empty"
+msgstr "空"
+
+#: pkg/users/account-create-dialog.js:201
+msgid "Empty password"
+msgstr "空密码"
+
+#: pkg/storaged/jobs-panel.jsx:80
+msgid "Emptying $target"
+msgstr "清空 $target"
+
+#: pkg/packagekit/autoupdates.jsx:407 pkg/packagekit/kpatch.jsx:251
+msgid "Enable"
+msgstr "启用"
+
+#: pkg/networkmanager/network-interface.jsx:685
+msgid "Enable or disable the device"
+msgstr "启用或禁用设备"
+
+#: pkg/networkmanager/networkmanager.jsx:102
+msgid "Enable service"
+msgstr "启用服务"
+
+#: pkg/networkmanager/firewall-switch.jsx:74
+msgid "Enable the firewall"
+msgstr "启用防火墙"
+
+#: pkg/networkmanager/firewall-switch.jsx:80 pkg/kdump/kdump-view.jsx:541
+#: pkg/systemd/services.jsx:244 pkg/systemd/services.jsx:245
+#: pkg/systemd/services.jsx:686 pkg/packagekit/kpatch.jsx:253
+msgid "Enabled"
+msgstr "启用"
+
+#: pkg/storaged/crypto/keyslots.jsx:333
+msgid "Enabling $0"
+msgstr "启用 $0"
+
+#: pkg/storaged/stratis/create-dialog.jsx:71
+msgid "Encrypt data"
+msgstr "加密数据"
+
+#: pkg/storaged/stratis/create-dialog.jsx:99
+msgid "Encrypt data with a Tang keyserver"
+msgstr "使用 Tang 密钥服务器加密数据"
+
+#: pkg/storaged/stratis/create-dialog.jsx:70
+msgid "Encrypt data with a passphrase"
+msgstr "使用密码加密数据"
+
+#: pkg/sosreport/sosreport.jsx:450
+msgid "Encrypted"
+msgstr "已加密"
+
+#: pkg/storaged/utils.js:366
+msgid "Encrypted $0"
+msgstr "已加密 $0"
+
+#: pkg/storaged/stratis/pool.jsx:275
+msgid "Encrypted Stratis pool"
+msgstr "加密的 Stratis 池"
+
+#: pkg/storaged/utils.js:358
+msgid "Encrypted logical volume of $0"
+msgstr "$0 的已加密逻辑卷"
+
+#: pkg/storaged/utils.js:360
+msgid "Encrypted partition of $0"
+msgstr "$0 的已加密分区"
+
+#: pkg/storaged/block/format-dialog.jsx:375
+#: pkg/storaged/crypto/encryption.jsx:42
+msgid "Encryption"
+msgstr "加密"
+
+#: pkg/storaged/block/format-dialog.jsx:418
+#: pkg/storaged/crypto/encryption.jsx:189
+msgid "Encryption options"
+msgstr "加密选项"
+
+#: pkg/sosreport/sosreport.jsx:309
+msgid "Encryption passphrase"
+msgstr "加密密码短语"
+
+#: pkg/storaged/crypto/encryption.jsx:225
+msgid "Encryption type"
+msgstr "加密类型"
+
+#: pkg/users/account-logs-panel.jsx:78
+msgid "Ended"
+msgstr "结束"
+
+#: pkg/networkmanager/wireguard.jsx:309
+msgid "Endpoint"
+msgstr "端点"
+
+#: pkg/networkmanager/wireguard.jsx:276
+msgid ""
+"Endpoint acting as a \"server\" need to be specified as host:port, otherwise "
+"it can be left empty."
+msgstr "作为 \"服务器\" 的端点需要指定为 host:port,或者也可以留空。"
+
+#: pkg/selinux/setroubleshoot-view.jsx:262
+msgid "Enforcing"
+msgstr "强制"
+
+#: pkg/packagekit/updates.jsx:1439
+msgid "Enhancement updates available"
+msgstr "可用的功能增强更新"
+
+#: pkg/networkmanager/mac.jsx:47
+msgid "Enter a valid MAC address"
+msgstr "输入有效的 MAC 地址"
+
+#: pkg/networkmanager/firewall.jsx:204 pkg/networkmanager/firewall.jsx:886
+msgid "Entire subnet"
+msgstr "整个子网"
+
+#: pkg/systemd/logDetails.jsx:185
+msgid "Entry at $0"
+msgstr "于 $0 的条目"
+
+#: pkg/storaged/jobs-panel.jsx:54
+msgid "Erasing $target"
+msgstr "擦除 $target"
+
+#: pkg/packagekit/updates.jsx:381
+msgid "Errata"
+msgstr "勘误"
+
+#: pkg/apps/utils.jsx:81 pkg/sosreport/sosreport.jsx:381
+#: pkg/systemd/services.jsx:224 pkg/systemd/service-details.jsx:280
+#: pkg/storaged/overview/overview.jsx:94 pkg/storaged/multipath.jsx:60
+#: pkg/storaged/storage-controls.jsx:105 pkg/storaged/storage-controls.jsx:160
+msgid "Error"
+msgstr "错误"
+
+#: pkg/systemd/logs.jsx:68
+msgid "Error and above"
+msgstr "Error 及更高级别"
+
+#: pkg/metrics/metrics.jsx:1850
+msgid "Error has occurred"
+msgstr "发生错误"
+
+#: pkg/storaged/crypto/keyslots.jsx:254
+msgid "Error installing $0: PackageKit is not installed"
+msgstr "安装错误 $0: PackageKit 没有安装"
+
+#: pkg/selinux/setroubleshoot-view.jsx:414
+#: pkg/systemd/overview-cards/configurationCard.jsx:176
+msgid "Error message"
+msgstr "错误信息"
+
+#: pkg/selinux/setroubleshoot-view.jsx:430
+msgid "Error running semanage to discover system modifications"
+msgstr "运行 semanage 来发现系统的改变时出错"
+
+#: pkg/users/authorized-keys.js:110
+msgid "Error saving authorized keys: "
+msgstr "保存授权密钥时出错: "
+
+#: pkg/selinux/selinux.js:75
+msgid "Error while setting SELinux mode: '$0'"
+msgstr "设置 SELinux 模式时出错: '$0'"
+
+#: pkg/networkmanager/mac.jsx:71
+msgid "Ethernet MAC"
+msgstr "以太网 MAC"
+
+#: pkg/networkmanager/mtu.jsx:74
+msgid "Ethernet MTU"
+msgstr "以太网 MTU"
+
+#: pkg/networkmanager/team.jsx:56
+msgid "Ethtool"
+msgstr "Ethtool"
+
+#: pkg/storaged/block/resize.jsx:443
+msgid "Exactly $0 physical volumes must be selected"
+msgstr "必须选择 $0 个物理卷"
+
+#: pkg/storaged/block/resize.jsx:510
+msgid ""
+"Exactly $0 physical volumes need to be selected, one for each stripe of the "
+"logical volume."
+msgstr "必须选择 $0 个物理卷,每个逻辑卷条带使用一个。"
+
+#: pkg/networkmanager/firewall.jsx:687
+msgid "Example: 22,ssh,8080,5900-5910"
+msgstr "例如: 22,ssh,8080,5900-5910"
+
+#: pkg/networkmanager/firewall.jsx:696
+msgid "Example: 88,2019,nfs,rsync"
+msgstr "例如: 88,2019,nfs,rsync"
+
+#: pkg/lib/cockpit-components-password.jsx:47
+msgid "Excellent password"
+msgstr "密码强度良好"
+
+#: pkg/lib/machine-info.js:77
+msgid "Expansion chassis"
+msgstr "扩展机箱"
+
+#: pkg/users/expiration-dialogs.js:71
+msgid "Expire account on"
+msgstr "过期账号于"
+
+#: pkg/users/account-details.js:78
+msgid "Expire account on $0"
+msgstr "过期帐号于 $0"
+
+#: pkg/kdump/kdump-view.jsx:272
+msgid "Export"
+msgstr "导出"
+
+#: pkg/metrics/metrics.jsx:1511
+msgid "Export to network"
+msgstr "导出到网络"
+
+#: pkg/systemd/abrtLog.jsx:292
+msgid "Extended information"
+msgstr "扩展的信息"
+
+#: pkg/storaged/block/format-dialog.jsx:213
+#: pkg/storaged/partitions/partition-table.jsx:58
+msgid "Extended partition"
+msgstr "扩展分区"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:230
+msgid "FIPS is not properly enabled"
+msgstr "FIPS 没有被正确启用"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:122
+msgid "FIPS with further Common Criteria restrictions."
+msgstr "具有进一步通用标准限制的 FIPS。"
+
+#: pkg/networkmanager/interfaces.js:811 pkg/storaged/mdraid/mdraid-disk.jsx:68
+msgid "Failed"
+msgstr "已失败"
+
+#: pkg/shell/hosts_dialog.jsx:219
+msgid "Failed to add machine: $0"
+msgstr "添加主机失败: $0"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add port"
+msgstr "添加端口失败"
+
+#: pkg/networkmanager/firewall.jsx:392
+msgid "Failed to add service"
+msgstr "添加服务失败"
+
+#: pkg/networkmanager/firewall.jsx:781
+msgid "Failed to add zone"
+msgstr "添加区域失败"
+
+#: pkg/users/password-dialogs.js:103 pkg/users/password-dialogs.js:125
+#: pkg/lib/credentials.js:232
+msgid "Failed to change password"
+msgstr "修改密码失败"
+
+#: pkg/metrics/metrics.jsx:1487
+msgid "Failed to configure PCP"
+msgstr "配置 PCP 失败"
+
+#: pkg/selinux/setroubleshoot-client.js:154
+#: pkg/selinux/setroubleshoot-client.js:158
+msgid "Failed to delete alert: $0"
+msgstr "删除警告: $0 失败"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:162
+msgid "Failed to disable tuned"
+msgstr "禁用 tuned 失败"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:183
+msgid "Failed to disable tuned profile"
+msgstr "禁用调优的配置文件失败"
+
+#: pkg/shell/hosts_dialog.jsx:337
+msgid "Failed to edit machine: $0"
+msgstr "编辑主机失败: $0"
+
+#: pkg/networkmanager/firewall.jsx:370
+msgid "Failed to edit service"
+msgstr "编辑服务失败"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:113
+msgid "Failed to enable $0 in firewalld"
+msgstr "在 firewalld 中启用 $0 失败"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:160
+msgid "Failed to enable tuned"
+msgstr "启用 tuned 失败"
+
+#: pkg/systemd/logsJournal.jsx:259
+msgid "Failed to fetch logs"
+msgstr "获取日志失败"
+
+#: pkg/users/authorized-keys-panel.js:105
+msgid "Failed to load authorized keys."
+msgstr "载入 authorized key 失败。"
+
+#: pkg/systemd/service-details.jsx:539
+msgid "Failed to load unit"
+msgstr "加载单元失败"
+
+#: pkg/packagekit/autoupdates.jsx:375
+msgid ""
+"Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install."
+"timer. Please remove custom overrides to configure automatic updates."
+msgstr ""
+"为 dnf-automatic.timer 或 dnf-automatic-install.timer 解析单元文件失败。请删"
+"除自定义覆盖来配置自动更新。"
+
+#: pkg/packagekit/updates.jsx:498
+msgid "Failed to restart service"
+msgstr "重启服务失败"
+
+#: pkg/systemd/overview-cards/motdCard.jsx:58
+msgid "Failed to save changes in /etc/motd"
+msgstr "保存 /etc/motd 中的更改失败"
+
+#: pkg/networkmanager/dialogs-common.jsx:169
+msgid "Failed to save settings"
+msgstr "保存设置失败"
+
+#: pkg/systemd/services.jsx:235 pkg/systemd/service-details.jsx:451
+msgid "Failed to start"
+msgstr "启动失败"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:194
+msgid "Failed to switch profile"
+msgstr "切换配置集失败"
+
+#: pkg/systemd/services.jsx:844 pkg/systemd/services.jsx:845
+#: pkg/systemd/services.jsx:852
+msgid "File state"
+msgstr "文件状态"
+
+#: pkg/storaged/filesystem/filesystem.jsx:91
+msgid "Filesystem"
+msgstr "文件系统"
+
+#: pkg/storaged/filesystem/filesystem.jsx:144
+msgid "Filesystem is locked"
+msgstr "文件系统被锁住"
+
+#: pkg/storaged/filesystem/filesystem.jsx:117
+msgid "Filesystem name"
+msgstr "文件系统名称"
+
+#: pkg/storaged/dialog.jsx:1114
+msgid "Filesystem outside the target"
+msgstr "目标之外的文件系统"
+
+#: pkg/storaged/filesystem/utils.jsx:127
+msgid "Filesystems are already mounted below this mountpoint."
+msgstr "文件系统已在此挂载点下挂载。"
+
+#: pkg/systemd/services.jsx:820
+msgid "Filter by name or description"
+msgstr "根据名称或描述进行过滤"
+
+#: pkg/shell/shell-modals.jsx:134
+msgid "Filter menu items"
+msgstr "过滤菜单项"
+
+#: pkg/networkmanager/firewall.jsx:268
+msgid "Filter services"
+msgstr "过滤服务"
+
+#: pkg/systemd/logs.jsx:237
+msgid "Filters"
+msgstr "过滤"
+
+#: pkg/users/authorized-keys-panel.js:129 pkg/shell/credentials.jsx:203
+msgid "Fingerprint"
+msgstr "指纹"
+
+#: pkg/networkmanager/firewall.html:22 pkg/networkmanager/firewall.jsx:1058
+#: pkg/networkmanager/firewall.jsx:1065 pkg/networkmanager/network-main.jsx:165
+msgid "Firewall"
+msgstr "防火墙"
+
+#: pkg/networkmanager/firewall.jsx:1032
+msgid "Firewall is not available"
+msgstr "防火墙不可用"
+
+#: pkg/storaged/drive/drive.jsx:122
+msgid "Firmware version"
+msgstr "固件版本"
+
+#: pkg/storaged/crypto/keyslots.jsx:401
+msgid "Fix NBDE support"
+msgstr "修复 NBDE 支持"
+
+#: pkg/systemd/terminal.jsx:148 pkg/systemd/terminal.jsx:158
+msgid "Font size"
+msgstr "字体大小"
+
+#: pkg/systemd/services-list.jsx:86 pkg/systemd/service-details.jsx:433
+msgid "Forbidden from running"
+msgstr "禁止运行"
+
+#: pkg/users/account-details.js:308
+msgid "Force change"
+msgstr "强制变更"
+
+#: pkg/users/delete-group-dialog.js:51
+msgid "Force delete"
+msgstr "强制删除"
+
+#: pkg/users/password-dialogs.js:271
+msgid "Force password change"
+msgstr "强制密码变更"
+
+#: pkg/storaged/swap/swap.jsx:100 pkg/storaged/lvm2/physical-volume.jsx:50
+#: pkg/storaged/filesystem/filesystem.jsx:104
+#: pkg/storaged/block/unrecognized-data.jsx:41
+#: pkg/storaged/block/format-dialog.jsx:322
+#: pkg/storaged/block/format-dialog.jsx:332
+#: pkg/storaged/block/unformatted-data.jsx:36
+#: pkg/storaged/mdraid/mdraid-disk.jsx:47 pkg/storaged/stratis/blockdev.jsx:48
+#: pkg/storaged/btrfs/device.jsx:49
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:38
+msgid "Format"
+msgstr "格式化"
+
+#: pkg/storaged/block/format-dialog.jsx:185
+msgid "Format $0"
+msgstr "格式化 $0"
+
+#: pkg/storaged/block/format-dialog.jsx:317
+msgid "Format and mount"
+msgstr "格式和挂载"
+
+#: pkg/storaged/block/format-dialog.jsx:326
+msgid "Format and start"
+msgstr "格式和启动"
+
+#: pkg/storaged/block/format-dialog.jsx:318
+#: pkg/storaged/block/format-dialog.jsx:327
+msgid "Format only"
+msgstr "仅格式"
+
+#: pkg/storaged/block/format-dialog.jsx:445
+msgid "Formatting erases all data on a storage device."
+msgstr "格式化会擦除存储设备上的所有数据。"
+
+#: pkg/networkmanager/network-interface.jsx:502
+msgid "Forward delay $forward_delay"
+msgstr "转发延迟 $forward_delay"
+
+#: pkg/systemd/abrtLog.jsx:83
+msgid "Frame number"
+msgstr "帧号"
+
+#: pkg/storaged/partitions/partition-table.jsx:42
+msgid "Free space"
+msgstr "空闲空间"
+
+#: pkg/systemd/logs.jsx:378
+msgid "Free-form search"
+msgstr "自由形式搜索"
+
+#: pkg/systemd/timer-dialog.jsx:321 pkg/packagekit/autoupdates.jsx:313
+msgid "Fridays"
+msgstr "周五"
+
+#: pkg/users/account-logs-panel.jsx:79
+msgid "From"
+msgstr "自"
+
+#: pkg/users/account-create-dialog.js:68 pkg/users/accounts-list.js:389
+#: pkg/users/account-details.js:248
+msgid "Full name"
+msgstr "全名"
+
+#: pkg/networkmanager/ip-settings.jsx:214
+#: pkg/networkmanager/ip-settings.jsx:374
+msgid "Gateway"
+msgstr "网关"
+
+#: pkg/networkmanager/network-interface.jsx:316 pkg/systemd/abrtLog.jsx:296
+msgid "General"
+msgstr "通用"
+
+#: pkg/networkmanager/wireguard.jsx:214 pkg/systemd/services.jsx:255
+msgid "Generated"
+msgstr "生成的"
+
+#: pkg/systemd/logDetails.jsx:52
+msgid "Go to $0"
+msgstr "前往 $0"
+
+#: pkg/apps/application.jsx:109
+msgid "Go to application"
+msgstr "转到应用"
+
+#: pkg/lib/cockpit-components-plot.jsx:264
+msgid "Go to now"
+msgstr "转到现在"
+
+#: pkg/metrics/metrics.jsx:1933
+msgid "Graph visibility"
+msgstr "图形可见性"
+
+#: pkg/metrics/metrics.jsx:1921
+msgid "Graph visibility options menu"
+msgstr "图形可见性选项菜单"
+
+#: pkg/users/accounts-list.js:392 pkg/networkmanager/network-interface.jsx:401
+msgid "Group"
+msgstr "组"
+
+#: pkg/users/accounts-list.js:238
+msgid "Group name"
+msgstr "用户组名称"
+
+#: pkg/users/accounts-list.js:322 pkg/users/accounts-list.js:326
+#: pkg/users/account-details.js:443
+msgid "Groups"
+msgstr "用户组"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:211
+#: pkg/storaged/lvm2/vdo-pool.jsx:48
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:104
+#: pkg/storaged/block/resize.jsx:521 pkg/storaged/legacy-vdo/legacy-vdo.jsx:259
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:315
+#: pkg/storaged/partitions/partition.jsx:99
+msgid "Grow"
+msgstr "增长"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:281
+#: pkg/storaged/partitions/partition.jsx:213
+msgid "Grow content"
+msgstr "增长内容"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:247
+msgid "Grow logical size of $0"
+msgstr "增长 $0 的逻辑大小"
+
+#: pkg/storaged/block/resize.jsx:400
+msgid "Grow logical volume"
+msgstr "稀疏逻辑卷增长"
+
+#: pkg/storaged/block/resize.jsx:409
+msgid "Grow partition"
+msgstr "增加分区"
+
+#: pkg/storaged/stratis/pool.jsx:337
+msgid "Grow the pool to take all space"
+msgstr "扩展池以占据所有空间"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:238
+msgid "Grow to take all space"
+msgstr "增长到占据所有空间"
+
+#: pkg/networkmanager/bridgeport.jsx:87
+msgid "Hair pin mode"
+msgstr "发夹模式"
+
+#: pkg/networkmanager/network-interface.jsx:529
+msgid "Hairpin mode"
+msgstr "发夹模式"
+
+#: pkg/lib/machine-info.js:70
+msgid "Handheld"
+msgstr "手持式"
+
+#: pkg/storaged/drive/drive.jsx:64
+msgid "Hard Disk Drive"
+msgstr "硬盘设备"
+
+#: pkg/systemd/hwinfo.html:4 pkg/systemd/hwinfo.jsx:308
+msgid "Hardware information"
+msgstr "硬件信息"
+
+#: pkg/systemd/overview-cards/healthCard.jsx:36
+msgid "Health"
+msgstr "健康"
+
+#: pkg/networkmanager/network-interface.jsx:504
+msgid "Hello time $hello_time"
+msgstr "Hello 时间 $hello_time"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:295
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:168 pkg/shell/topnav.jsx:255
+msgid "Help"
+msgstr "帮助"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Hide confirmation password"
+msgstr "隐藏确认密码"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Hide password"
+msgstr "隐藏密码"
+
+#: pkg/systemd/abrtLog.jsx:128
+msgid "Hierarchy ID"
+msgstr "层次结构 ID"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:109
+msgid "Higher interoperability at the cost of an increased attack surface."
+msgstr "更高的互操作性会增加被安全攻击的面积。"
+
+#: pkg/packagekit/history.jsx:112
+msgid "History package count"
+msgstr "历史软件包数量"
+
+#: pkg/users/account-create-dialog.js:84 pkg/users/account-details.js:326
+msgid "Home directory"
+msgstr "家目录"
+
+#: pkg/shell/hosts.jsx:192 pkg/shell/hosts_dialog.jsx:255
+msgid "Host"
+msgstr "主机"
+
+#: pkg/lib/cockpit.js:3867
+msgid "Host key is incorrect"
+msgstr "主机密钥不正确"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:67
+msgid "Hostname"
+msgstr "主机名"
+
+#: pkg/shell/hosts.jsx:152
+msgid "Hosts"
+msgstr "主机"
+
+#: pkg/systemd/timer-dialog.jsx:244
+msgid "Hourly"
+msgstr "每小时"
+
+#: pkg/systemd/timer-dialog.jsx:212
+msgid "Hours"
+msgstr "小时"
+
+#: pkg/storaged/crypto/tang.jsx:115
+msgid "How to check"
+msgstr "如何检查"
+
+#: pkg/users/accounts-list.js:239 pkg/users/accounts-list.js:390
+#: pkg/users/group-create-dialog.js:47 pkg/networkmanager/firewall.jsx:700
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/pages.jsx:709
+#: pkg/storaged/btrfs/subvolume.jsx:334
+msgid "ID"
+msgstr "ID"
+
+#: pkg/networkmanager/network-interface.jsx:547
+msgid "ID $id"
+msgstr "ID $id"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:65
+msgid ""
+"INTERNAL ERROR - This logical volume is marked as active and should have an "
+"associated block device. However, no such block device could be found."
+msgstr ""
+"内部错误 - 此逻辑卷被标记为 active,应该有一个关联的块设备。但是,无法找到这"
+"样的块设备。"
+
+#: pkg/networkmanager/network-main.jsx:186
+#: pkg/networkmanager/network-main.jsx:201
+msgid "IP address"
+msgstr "IP 地址"
+
+#: pkg/networkmanager/firewall.jsx:896
+msgid ""
+"IP address with routing prefix. Separate multiple values with a comma. "
+"Example: 192.0.2.0/24, 2001:db8::/32"
+msgstr ""
+"带有路由前缀的IP地址。用逗号分隔多个值。例如:192.0.2.0/24, 2001:db8::/32"
+
+#: pkg/networkmanager/network-interface.jsx:569
+msgid "IPv4"
+msgstr "IPv4"
+
+#: pkg/networkmanager/wireguard.jsx:252
+msgid "IPv4 addresses"
+msgstr "IPv4 地址"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv4 settings"
+msgstr "IPv4 设置"
+
+#: pkg/networkmanager/network-interface.jsx:570
+msgid "IPv6"
+msgstr "IPv6"
+
+#: pkg/networkmanager/ip-settings.jsx:162
+msgid "IPv6 settings"
+msgstr "IPv6 设置"
+
+#: pkg/systemd/logs.jsx:224
+msgid "Identifier"
+msgstr "标识符"
+
+#: pkg/networkmanager/firewall.jsx:703
+msgid ""
+"If left empty, ID will be generated based on associated port services and "
+"port numbers"
+msgstr "如果为空,则将根据关联的端口服务和端口号生成 ID"
+
+#: pkg/static/login.html:90
+msgid ""
+"If the fingerprint matches, click \"Accept key and log in\". Otherwise, do "
+"not log in and contact your administrator."
+msgstr ""
+"如果指纹匹配,点\"Accept key and log in\"。否则,请不要登录并联系您的管理员。"
+
+#: pkg/shell/hosts_dialog.jsx:480
+msgid ""
+"If the fingerprint matches, click 'Trust and add host'. Otherwise, do not "
+"connect and contact your administrator."
+msgstr ""
+"如果指纹匹配,点 'Trust and add host'。否则,请不要连接并联系您的管理员。"
+
+#: pkg/networkmanager/ip-settings.jsx:44 pkg/packagekit/updates.jsx:686
+#: pkg/packagekit/updates.jsx:763
+msgid "Ignore"
+msgstr "忽略"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "In"
+msgstr "入"
+
+#: pkg/storaged/crypto/tang.jsx:116
+msgid "In a terminal, run: "
+msgstr "在终端中,运行: "
+
+#: pkg/shell/hosts_dialog.jsx:806 pkg/shell/hosts_dialog.jsx:823
+msgid ""
+"In order to allow log in to $0 as $1 without password in the future, use the "
+"login password of $2 on $3 as the key password, or leave the key password "
+"blank."
+msgstr ""
+"为了以后不需要密码就可以以 $1 身份登录到 $0 ,请使用 $3 上 $2 的登录密码作为"
+"密钥密码,或者将密钥密码留空。"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:69
+msgid "In sync"
+msgstr "同步中"
+
+#: pkg/networkmanager/interfaces.js:793 pkg/networkmanager/interfaces.js:1354
+#: pkg/networkmanager/interfaces.js:1361
+#: pkg/networkmanager/network-interface.jsx:256
+msgid "Inactive"
+msgstr "未激活"
+
+#: pkg/storaged/lvm2/inactive-logical-volume.jsx:33
+msgid "Inactive logical volume"
+msgstr "不活跃的逻辑卷"
+
+#: pkg/networkmanager/firewall.jsx:856
+msgid "Included services"
+msgstr "包括的服务"
+
+#: pkg/networkmanager/firewall.jsx:1068
+msgid ""
+"Incoming requests are blocked by default. Outgoing requests are not blocked."
+msgstr "传入请求被默认阻断。传出请求不会被阻断。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:224
+msgid "Inconsistent filesystem mount"
+msgstr "不一致的文件系统挂载"
+
+#: pkg/systemd/terminal.jsx:160
+msgid "Increase by one"
+msgstr "加一"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:320
+msgid "Index memory"
+msgstr "索引内存"
+
+#: pkg/systemd/services.jsx:254
+msgid "Indirect"
+msgstr "间接的"
+
+#: pkg/packagekit/updates.jsx:755
+msgid "Info"
+msgstr "信息"
+
+#: pkg/systemd/logs.jsx:71
+msgid "Info and above"
+msgstr "Info 及更高级别"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:69
+msgid "Initialize"
+msgstr "初始化"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:46
+msgid "Initialize disk $0"
+msgstr "初始化磁盘 $0"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:70
+msgid "Initializing erases all data on a disk."
+msgstr "初始化会擦除磁盘上的所有数据。"
+
+#: pkg/packagekit/updates.jsx:584
+msgid "Initializing..."
+msgstr "初始化中..."
+
+#: pkg/systemd/overview-cards/insights.jsx:230
+msgid "Insights: "
+msgstr "Insights: "
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:126
+#: pkg/apps/application-list.jsx:206 pkg/apps/application.jsx:52
+#: pkg/packagekit/kpatch.jsx:247
+msgid "Install"
+msgstr "安装"
+
+#: pkg/storaged/overview/overview.jsx:136
+msgid "Install NFS support"
+msgstr "安装 NFS 支持"
+
+#: pkg/storaged/overview/overview.jsx:121
+msgid "Install Stratis support"
+msgstr "安装 Stratis 支持"
+
+#: pkg/packagekit/updates.jsx:1416
+msgid "Install all updates"
+msgstr "安装所有更新"
+
+#: pkg/apps/application-list.jsx:200
+msgid "Install application information"
+msgstr "安装应用程序信息"
+
+#: pkg/metrics/metrics.jsx:1818
+msgid "Install cockpit-pcp"
+msgstr "安装 cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:1429
+msgid "Install kpatch updates"
+msgstr "安装 kpatch 更新"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Install realmd support"
+msgstr "安装 realmd 支持"
+
+#: pkg/packagekit/updates.jsx:1415 pkg/packagekit/updates.jsx:1422
+msgid "Install security updates"
+msgstr "安装安全更新"
+
+#: pkg/selinux/setroubleshoot-view.jsx:334
+msgid "Install setroubleshoot-server to troubleshoot SELinux events."
+msgstr "安装 setroubleshoot-server 来排查 SELinux 事件。"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:112
+msgid "Install software"
+msgstr "安装软件"
+
+#: pkg/kdump/kdump-view.jsx:571
+msgid "Install the $0 package."
+msgstr "安装 $0 软件包。"
+
+#: pkg/metrics/metrics.jsx:1817
+msgid "Installation not supported without installed cockpit package"
+msgstr "未安装 cockpit 软件包时不支持安装"
+
+#: pkg/packagekit/updates.jsx:111
+msgid "Installed"
+msgstr "已安装"
+
+#: pkg/apps/application.jsx:40 pkg/packagekit/updates.jsx:105
+msgid "Installing"
+msgstr "正在安装"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:193
+#: pkg/storaged/crypto/keyslots.jsx:222
+msgid "Installing $0"
+msgstr "正在安装 $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:39
+#: pkg/storaged/crypto/keyslots.jsx:238
+msgid "Installing $0 would remove $1."
+msgstr "安装 $0 将删除 $1 。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:41
+msgid "Installing packages"
+msgstr "安装软件包"
+
+#: pkg/networkmanager/firewall.jsx:200 pkg/metrics/metrics.jsx:814
+msgid "Interface"
+msgid_plural "Interfaces"
+msgstr[0] "接口"
+
+#: pkg/networkmanager/network-interface-members.jsx:197
+#: pkg/networkmanager/network-interface-members.jsx:199
+msgid "Interface members"
+msgstr "接口成员"
+
+#: pkg/networkmanager/bond.jsx:165 pkg/networkmanager/firewall.jsx:863
+#: pkg/networkmanager/network-main.jsx:180
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Interfaces"
+msgstr "接口"
+
+#: pkg/lib/cockpit.js:3869
+msgid "Internal error"
+msgstr "内部错误"
+
+#: pkg/static/login.js:907
+msgid "Internal error: Invalid challenge header"
+msgstr "内部错误:无效的挑战字头部"
+
+#: pkg/systemd/services.jsx:253
+msgid "Invalid"
+msgstr "无效"
+
+#: pkg/networkmanager/utils.js:89 pkg/networkmanager/utils.js:173
+msgid "Invalid address $0"
+msgstr "无效的地址 $0"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:118 pkg/lib/serverTime.js:687
+msgid "Invalid date format"
+msgstr "无效的日期格式"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:112
+msgid "Invalid date format and invalid time format"
+msgstr "无效的日期格式和时间格式"
+
+#: pkg/users/expiration-dialogs.js:98
+msgid "Invalid expiration date"
+msgstr "无效的过期时间"
+
+#: pkg/lib/credentials.js:294
+msgid "Invalid file permissions"
+msgstr "无效的文件权限"
+
+#: pkg/users/authorized-keys-panel.js:136
+msgid "Invalid key"
+msgstr "无效的 key"
+
+#: pkg/networkmanager/utils.js:55
+msgid "Invalid metric $0"
+msgstr "无效的度量 $0"
+
+#: pkg/users/expiration-dialogs.js:200
+msgid "Invalid number of days"
+msgstr "无效的天数"
+
+#: pkg/networkmanager/firewall.jsx:483
+msgid "Invalid port number"
+msgstr "无效的端口号"
+
+#: pkg/networkmanager/utils.js:41
+msgid "Invalid prefix $0"
+msgstr "无效的前缀 $0"
+
+#: pkg/networkmanager/utils.js:134
+msgid "Invalid prefix or netmask $0"
+msgstr "无效的前缀或掩码 $0"
+
+#: pkg/networkmanager/firewall.jsx:509
+msgid "Invalid range"
+msgstr "无效范围"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:115 pkg/lib/serverTime.js:690
+#: pkg/packagekit/autoupdates.jsx:323
+msgid "Invalid time format"
+msgstr "无效的时间格式"
+
+#: pkg/lib/serverTime.js:682
+msgid "Invalid timezone"
+msgstr "无效的时区"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:65
+#: pkg/storaged/iscsi/create-dialog.jsx:152
+msgid "Invalid username or password"
+msgstr "无效的用户名或密码"
+
+#: pkg/lib/machine-info.js:92
+msgid "IoT gateway"
+msgstr "IoT 网关"
+
+#: pkg/shell/hosts_dialog.jsx:362
+msgid "Is sshd running on a different port?"
+msgstr "sshd 是否在一个不同的端口上运行?"
+
+#: pkg/storaged/jobs-panel.jsx:205
+msgid "Jobs"
+msgstr "任务"
+
+#: pkg/systemd/overview-cards/realmd.jsx:432
+msgid "Join"
+msgstr "加入"
+
+#: pkg/systemd/overview-cards/realmd.jsx:438
+msgctxt "dialog-title"
+msgid "Join a domain"
+msgstr "加入域"
+
+#: pkg/systemd/overview-cards/realmd.jsx:463
+msgid "Join domain"
+msgstr "加入域"
+
+#: pkg/systemd/overview-cards/realmd.jsx:431
+msgid "Joining"
+msgstr "加入"
+
+#: pkg/systemd/overview-cards/realmd.jsx:71
+msgid "Joining a domain requires installation of realmd"
+msgstr "加入域需要安装 realmd"
+
+#: pkg/systemd/overview-cards/realmd.jsx:161
+msgid "Joining this domain is not supported"
+msgstr "不支持加入该域"
+
+#: pkg/systemd/service-details.jsx:579
+msgid "Joins namespace of"
+msgstr "加入命名空间"
+
+#: pkg/systemd/logs.html:23
+msgid "Journal"
+msgstr "日志"
+
+#: pkg/systemd/logDetails.jsx:167
+msgid "Journal entry"
+msgstr "日志条目"
+
+#: pkg/systemd/logDetails.jsx:146
+msgid "Journal entry not found"
+msgstr "未找到日志条目"
+
+#: pkg/metrics/metrics.jsx:1911
+msgid "Jump to"
+msgstr "跳到"
+
+#: pkg/kdump/kdump-view.jsx:569
+msgid "Kdump service is not installed."
+msgstr "没有安装 kdump 服务。"
+
+#: pkg/kdump/kdump-view.jsx:604
+msgid "Kdump settings"
+msgstr "kdump 设置"
+
+#: pkg/networkmanager/interfaces.js:69
+msgid "Keep connection"
+msgstr "保持连接"
+
+#: pkg/kdump/kdump-view.jsx:581
+msgid "Kernel crash dump"
+msgstr "内核崩溃转储"
+
+#: pkg/kdump/kdump-view.jsx:550
+msgid "Kernel did not boot with the $0 setting"
+msgstr "内核没有使用 $0 设置引导"
+
+#: pkg/kdump/index.html:23 pkg/kdump/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:5
+msgid "Kernel dump"
+msgstr "内核转储"
+
+#: pkg/packagekit/kpatch.jsx:366
+msgid "Kernel live patch $0 is active"
+msgstr "内核实时补丁 $0 处于活跃状态"
+
+#: pkg/packagekit/kpatch.jsx:373
+msgid "Kernel live patch $0 is installed"
+msgstr "内核实时补丁 $0 已安装"
+
+#: pkg/packagekit/kpatch.jsx:299
+msgid "Kernel live patch settings"
+msgstr "内核实时补丁设置"
+
+#: pkg/packagekit/kpatch.jsx:282
+msgid "Kernel live patching"
+msgstr "内核实时补丁"
+
+#: pkg/shell/hosts_dialog.jsx:796 pkg/shell/hosts_dialog.jsx:861
+msgid "Key password"
+msgstr "钥匙密码"
+
+#: pkg/storaged/crypto/keyslots.jsx:759
+msgid "Key slots with unknown types can not be edited here"
+msgstr "这里无法编辑未知类型的密钥 slot"
+
+#: pkg/storaged/crypto/keyslots.jsx:422
+msgid "Key source"
+msgstr "密钥源"
+
+#: pkg/storaged/crypto/keyslots.jsx:764 pkg/storaged/crypto/keyslots.jsx:787
+msgid "Keys"
+msgstr "密钥"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:134 pkg/storaged/stratis/pool.jsx:548
+#: pkg/storaged/crypto/keyslots.jsx:753
+msgid "Keyserver"
+msgstr "密钥服务器"
+
+#: pkg/storaged/stratis/create-dialog.jsx:102 pkg/storaged/stratis/pool.jsx:460
+#: pkg/storaged/crypto/keyslots.jsx:449 pkg/storaged/crypto/keyslots.jsx:507
+msgid "Keyserver address"
+msgstr "Keyserver 地址"
+
+#: pkg/storaged/stratis/pool.jsx:500 pkg/storaged/crypto/keyslots.jsx:633
+msgid "Keyserver removal may prevent unlocking $0."
+msgstr "删除 keyserver 可能会阻止解锁 $0。"
+
+#: pkg/networkmanager/teamport.jsx:93
+msgid "LACP key"
+msgstr "LACP 密钥"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:110
+msgid "LEGACY with Active Directory interoperability."
+msgstr "具有活动目录互操作性的 LEGACY。"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:42
+msgid "LVM2 VDO pool"
+msgstr "LVM2 VDO 池"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:191
+msgid "LVM2 logical volume"
+msgstr "LVM2 逻辑卷"
+
+#: pkg/storaged/lvm2/volume-group.jsx:257
+#: pkg/storaged/lvm2/volume-group.jsx:298
+msgid "LVM2 logical volumes"
+msgstr "LVM2 逻辑卷"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:40
+msgid "LVM2 physical volume"
+msgstr "LVM2物理卷"
+
+#: pkg/storaged/lvm2/volume-group.jsx:393
+msgid "LVM2 physical volumes"
+msgstr "LVM2 物理卷"
+
+#: pkg/storaged/lvm2/volume-group.jsx:231
+msgid "LVM2 volume group"
+msgstr "LVM2 卷组"
+
+#: pkg/storaged/utils.js:340
+msgid "LVM2 volume group $0"
+msgstr "LVM2 卷组 $0"
+
+#: pkg/storaged/btrfs/volume.jsx:118
+msgid "Label"
+msgstr "标签"
+
+#: pkg/lib/machine-info.js:68
+msgid "Laptop"
+msgstr "笔记本电脑"
+
+#: pkg/systemd/logs.jsx:60
+msgid "Last 24 hours"
+msgstr "最近 24 小时"
+
+#: pkg/systemd/logs.jsx:61
+msgid "Last 7 days"
+msgstr "最近 7 天"
+
+#: pkg/users/accounts-list.js:391
+msgid "Last active"
+msgstr "最近活动"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:73
+msgid "Last cannot be removed"
+msgstr "最后一个无法删除"
+
+#: pkg/packagekit/updates.jsx:785
+msgid "Last checked: $0"
+msgstr "最后检查于: $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:93
+msgid "Last disk can not be removed"
+msgstr "不能删除最后一个磁盘"
+
+#: pkg/users/account-details.js:266
+msgid "Last login"
+msgstr "最近登陆"
+
+#: pkg/storaged/crypto/encryption.jsx:98
+msgid "Last modified: $0"
+msgstr "最后修改于: $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:90
+msgid "Last successful login:"
+msgstr "最后成功的登录:"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:294
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:192
+msgid "Layout"
+msgstr "布局"
+
+#: pkg/networkmanager/bond.jsx:152 pkg/lib/cockpit-components-dialog.jsx:225
+#: pkg/lib/cockpit-components-dialog.jsx:232
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:291
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:119
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:164
+msgid "Learn more"
+msgstr "了解更多"
+
+#: pkg/systemd/overview-cards/realmd.jsx:314
+msgid "Leave $0"
+msgstr "离开 $0"
+
+#: pkg/systemd/overview-cards/realmd.jsx:311
+#: pkg/systemd/overview-cards/realmd.jsx:314
+#: pkg/systemd/overview-cards/realmd.jsx:316
+msgid "Leave domain"
+msgstr "离开域"
+
+#: pkg/sosreport/sosreport.jsx:317
+msgid "Leave empty to skip encryption"
+msgstr "留空以跳过加密"
+
+#: pkg/shell/shell-modals.jsx:63
+msgid "Licensed under GNU LGPL version 2.1"
+msgstr "根据 GNU LGPL 版本 2.1 获得版权"
+
+#: pkg/systemd/terminal.jsx:175 pkg/shell/topnav.jsx:190
+msgid "Light"
+msgstr "亮色"
+
+#: pkg/shell/superuser.jsx:261
+msgid "Limit access"
+msgstr "限制访问"
+
+#: pkg/shell/superuser.jsx:329
+msgid "Limited access"
+msgstr "被限制的访问"
+
+#: pkg/shell/superuser.jsx:278
+msgid ""
+"Limited access mode restricts administrative privileges. Some parts of the "
+"web console will have reduced functionality."
+msgstr "限制访问模式会限制管理员权限。Web 控制台部分功能将会减少。"
+
+#: pkg/systemd/abrtLog.jsx:143
+msgid "Limits"
+msgstr "限制"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:67
+msgid "Linear"
+msgstr "线性"
+
+#: pkg/networkmanager/bond.jsx:201 pkg/networkmanager/team.jsx:195
+msgid "Link down delay"
+msgstr "链路断开延时"
+
+#: pkg/networkmanager/ip-settings.jsx:42
+msgid "Link local"
+msgstr "本地链路"
+
+#: pkg/networkmanager/bond.jsx:188
+msgid "Link monitoring"
+msgstr "链路监控"
+
+#: pkg/networkmanager/bond.jsx:198 pkg/networkmanager/team.jsx:192
+msgid "Link up delay"
+msgstr "链路启用延时"
+
+#: pkg/networkmanager/team.jsx:185
+msgid "Link watch"
+msgstr "侦测连接状态"
+
+#: pkg/systemd/services.jsx:247 pkg/systemd/services.jsx:248
+msgid "Linked"
+msgstr "连接"
+
+#: pkg/storaged/partitions/partition.jsx:118
+#: pkg/storaged/partitions/partition.jsx:125
+msgid "Linux filesystem data"
+msgstr "Linux 文件系统数据"
+
+#: pkg/storaged/partitions/partition.jsx:117
+#: pkg/storaged/partitions/partition.jsx:124
+msgid "Linux swap space"
+msgstr "Linux 交换空间"
+
+#: pkg/systemd/service-details.jsx:670
+msgid "Listen"
+msgstr "监听"
+
+#: pkg/networkmanager/wireguard.jsx:242
+msgid "Listen port"
+msgstr "侦听端口"
+
+#: pkg/networkmanager/wireguard.jsx:149
+msgid "Listen port must be a number"
+msgstr "侦听端口必须是一个数字"
+
+#: pkg/systemd/services.jsx:683
+msgid "Listing units"
+msgstr "列出单元"
+
+#: pkg/systemd/services.jsx:503
+msgid "Listing units failed: $0"
+msgstr "列出单元失败:$0"
+
+#: pkg/metrics/metrics.jsx:115 pkg/metrics/metrics.jsx:116
+#: pkg/metrics/metrics.jsx:849 pkg/metrics/metrics.jsx:1949
+msgid "Load"
+msgstr "负载"
+
+#: pkg/networkmanager/team.jsx:43
+msgid "Load balancing"
+msgstr "负载均衡"
+
+#: pkg/metrics/metrics.jsx:1979
+msgid "Load earlier data"
+msgstr "载入较早的数据"
+
+#: pkg/systemd/logsJournal.jsx:289
+msgid "Load earlier entries"
+msgstr "载入更早条目"
+
+#: pkg/packagekit/updates.jsx:102
+msgid "Loading available updates failed"
+msgstr "加载可用的更新失败"
+
+#: pkg/packagekit/updates.jsx:96
+msgid "Loading available updates, please wait..."
+msgstr "正在加载可用的更新,请等待..."
+
+#: pkg/systemd/logsJournal.jsx:290
+msgid "Loading earlier entries"
+msgstr "载入更早的条目"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:179
+msgid "Loading keys..."
+msgstr "正在加载密钥 ......"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:175
+msgid "Loading of SSH keys failed"
+msgstr "加载 SSH 密钥失败"
+
+#: pkg/systemd/services.jsx:664
+msgid "Loading of units failed"
+msgstr "单元加载失败"
+
+#: pkg/shell/shell-modals.jsx:78
+msgid "Loading packages..."
+msgstr "加载软件包 ..."
+
+#: pkg/lib/cockpit-components-modifications.jsx:134
+msgid "Loading system modifications..."
+msgstr "加载系统改变..."
+
+#: pkg/systemd/service.jsx:129
+msgid "Loading unit failed"
+msgstr "加载单元失败"
+
+#: pkg/users/accounts-list.js:327 pkg/users/accounts-list.js:348
+#: pkg/users/accounts-list.js:461 pkg/users/account-details.js:176
+#: pkg/kdump/kdump-view.jsx:438 pkg/systemd/services.jsx:683
+#: pkg/systemd/logsJournal.jsx:268 pkg/systemd/logDetails.jsx:173
+#: pkg/systemd/service.jsx:132 pkg/storaged/storaged.jsx:66
+#: pkg/metrics/metrics.jsx:1978
+msgid "Loading..."
+msgstr "载入中..."
+
+#: pkg/users/index.html:23
+msgid "Local accounts"
+msgstr "本地账户"
+
+#: pkg/kdump/kdump-view.jsx:247
+msgid "Local filesystem"
+msgstr "本地文件系统"
+
+#: pkg/storaged/nfs/nfs.jsx:157
+msgid "Local mount point"
+msgstr "挂载点"
+
+#: pkg/storaged/overview/overview.jsx:160
+msgid "Local storage"
+msgstr "本地存储"
+
+#: pkg/kdump/kdump-view.jsx:450
+msgid "Local, $0"
+msgstr "本地,$0"
+
+#: pkg/kdump/kdump-view.jsx:243 pkg/storaged/pages.jsx:711
+#: pkg/storaged/dialog.jsx:1134 pkg/storaged/dialog.jsx:1239
+msgid "Location"
+msgstr "位置"
+
+#: pkg/users/lock-account-dialog.js:36 pkg/storaged/crypto/actions.jsx:73
+msgid "Lock"
+msgstr "锁定"
+
+#: pkg/users/lock-account-dialog.js:29
+msgid "Lock $0"
+msgstr "锁定$0"
+
+#: pkg/users/accounts-list.js:74
+msgid "Lock account"
+msgstr "锁定账户"
+
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:31
+msgid "Locked data"
+msgstr "锁定的数据"
+
+#: pkg/storaged/jobs-panel.jsx:43
+msgid "Locking $target"
+msgstr "锁定 $target"
+
+#: pkg/static/login.html:139 pkg/static/login.js:668 pkg/shell/failures.jsx:75
+#: pkg/shell/hosts_dialog.jsx:764
+msgid "Log in"
+msgstr "登录"
+
+#: pkg/shell/hosts_dialog.jsx:763
+msgid "Log in to $0"
+msgstr "登录到 $0"
+
+#: pkg/static/login.js:704
+msgid "Log in with your server user account."
+msgstr "使用您的服务器用户帐户登录。"
+
+#: pkg/lib/serverTime.js:464
+msgid "Log messages"
+msgstr "日志消息"
+
+#: pkg/users/logout-account-dialog.js:36 pkg/metrics/metrics.jsx:1806
+#: pkg/shell/topnav.jsx:222
+msgid "Log out"
+msgstr "注销"
+
+#: pkg/users/accounts-list.js:68
+msgid "Log user out"
+msgstr "注销用户"
+
+#: pkg/users/accounts-list.js:170 pkg/users/account-details.js:212
+msgid "Logged in"
+msgstr "登录"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:306
+msgid "Logical"
+msgstr "逻辑"
+
+#: pkg/storaged/partitions/partition.jsx:119
+#: pkg/storaged/partitions/partition.jsx:126
+msgid "Logical Volume Manager partition"
+msgstr "逻辑卷管理器分区"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:213
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:249
+msgid "Logical size"
+msgstr "逻辑大小"
+
+#: pkg/storaged/utils.js:290
+msgid "Logical volume"
+msgstr "逻辑卷"
+
+#: pkg/storaged/utils.js:288
+msgid "Logical volume (snapshot)"
+msgstr "逻辑卷 (快照)"
+
+#: pkg/storaged/utils.js:362
+msgid "Logical volume of $0"
+msgstr "$0 的逻辑卷"
+
+#: pkg/static/login.js:361
+msgid "Login"
+msgstr "登录"
+
+#: pkg/static/login.js:404
+msgid "Login again"
+msgstr "再次登录"
+
+#: pkg/lib/cockpit.js:3859
+msgid "Login failed"
+msgstr "登录失败"
+
+#: pkg/systemd/overview-cards/realmd.jsx:290
+msgid "Login format"
+msgstr "登陆格式"
+
+#: pkg/users/account-logs-panel.jsx:73
+msgid "Login history"
+msgstr "登录历史"
+
+#: pkg/users/account-logs-panel.jsx:75
+msgid "Login history list"
+msgstr "登陆历史列表"
+
+#: pkg/users/logout-account-dialog.js:29
+msgid "Logout $0"
+msgstr "注销 $0"
+
+#: pkg/static/login.js:405
+msgid "Logout successful"
+msgstr "注销成功"
+
+#: pkg/systemd/logDetails.jsx:194 pkg/systemd/manifest.json:0
+msgid "Logs"
+msgstr "日志"
+
+#: pkg/lib/machine-info.js:63
+msgid "Low profile desktop"
+msgstr "低调桌面"
+
+#: pkg/lib/machine-info.js:75
+msgid "Lunch box"
+msgstr "主机类型"
+
+#: pkg/networkmanager/mac.jsx:73 pkg/networkmanager/bond.jsx:168
+msgid "MAC"
+msgstr "MAC"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:122 pkg/storaged/mdraid/mdraid.jsx:196
+#: pkg/storaged/mdraid/mdraid.jsx:204
+msgid "MDRAID device"
+msgstr "MDRAID 设备"
+
+#: pkg/storaged/utils.js:334
+msgid "MDRAID device $0"
+msgstr "MDRAID 设备 $0"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:89
+msgid "MDRAID device is recovering"
+msgstr "MDRAID 设备正在恢复"
+
+#: pkg/storaged/mdraid/mdraid.jsx:193
+msgid "MDRAID device must be running"
+msgstr "MDRAID 设备必须正在运行"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:40
+msgid "MDRAID disk"
+msgstr "MDRAID 磁盘"
+
+#: pkg/storaged/mdraid/mdraid.jsx:302
+msgid "MDRAID disks"
+msgstr "MDRAID 磁盘"
+
+#: pkg/networkmanager/bond.jsx:54
+msgid "MII (recommended)"
+msgstr "MII (推荐)"
+
+#: pkg/networkmanager/network-interface.jsx:381
+msgid "MTU"
+msgstr "MTU"
+
+#: pkg/networkmanager/mtu.jsx:43
+msgid "MTU must be a positive number"
+msgstr "MTU 必须是正整数"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:123
+msgid "Machine ID"
+msgstr "机器编号"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:196
+msgid "Machine SSH key fingerprints"
+msgstr "主机 SSH 密钥指纹"
+
+#: pkg/lib/machine-info.js:76
+msgid "Main server chassis"
+msgstr "主服务器机箱"
+
+#: pkg/systemd/services.jsx:238
+msgid "Maintenance"
+msgstr "维护"
+
+#: pkg/shell/hosts_dialog.jsx:497
+msgid "Malicious pages on a remote machine may affect other connected hosts"
+msgstr "远程计算机上的恶意页面可能会影响其他连接的主机"
+
+#: pkg/storaged/stratis/create-dialog.jsx:115
+msgid "Manage filesystem sizes"
+msgstr "管理文件系统大小"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:6
+msgid "Manage storage"
+msgstr "管理存储"
+
+#: pkg/networkmanager/network-main.jsx:182
+msgid "Managed interfaces"
+msgstr "管理的接口"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing LVMs"
+msgstr "管理 LVM"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing NFS mounts"
+msgstr "管理 NFS 挂载"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing RAIDs"
+msgstr "管理 RAID"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing VDOs"
+msgstr "管理 VDO"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing VLANs"
+msgstr "管理 VLAN"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing firewall"
+msgstr "管理防火墙"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bonds"
+msgstr "管理网络绑定"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking bridges"
+msgstr "管理网络桥接"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "Managing networking teams"
+msgstr "管理网络团队"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing partitions"
+msgstr "管理分区"
+
+#: pkg/storaged/manifest.json:0
+msgid "Managing physical drives"
+msgstr "管理物理驱动"
+
+#: pkg/systemd/manifest.json:0
+msgid "Managing services"
+msgstr "管理服务"
+
+#: pkg/packagekit/manifest.json:0
+msgid "Managing software updates"
+msgstr "管理软件更新"
+
+#: pkg/users/manifest.json:0
+msgid "Managing user accounts"
+msgstr "管理用户帐户"
+
+#: pkg/networkmanager/ip-settings.jsx:43
+msgid "Manual"
+msgstr "手动"
+
+#: pkg/lib/serverTime.js:562
+msgid "Manually"
+msgstr "手动的"
+
+#: pkg/storaged/jobs-panel.jsx:65
+msgid "Marking $target as faulty"
+msgstr "标记 $target 为故障"
+
+#: pkg/systemd/service-details.jsx:167 pkg/systemd/service-details.jsx:169
+msgid "Mask service"
+msgstr "屏蔽服务"
+
+#: pkg/systemd/services.jsx:250 pkg/systemd/services.jsx:251
+#: pkg/systemd/service-details.jsx:432
+msgid "Masked"
+msgstr "已屏蔽"
+
+#: pkg/systemd/service-details.jsx:168
+msgid ""
+"Masking service prevents all dependent units from running. This can have "
+"bigger impact than anticipated. Please confirm that you want to mask this "
+"unit."
+msgstr ""
+"屏蔽服务会阻止所有依赖的单元被运行。这所造成的影响可能会比预期的大。请确认您"
+"需要屏蔽这个单元。"
+
+#: pkg/networkmanager/network-interface.jsx:506
+msgid "Maximum message age $max_age"
+msgstr "最大消息有效期 $max_age"
+
+#: pkg/storaged/drive/drive.jsx:62
+msgid "Media drive"
+msgstr "介质驱动器"
+
+#: pkg/systemd/service-details.jsx:665 pkg/systemd/hwinfo.jsx:290
+#: pkg/systemd/hwinfo.jsx:334 pkg/systemd/overview-cards/usageCard.jsx:144
+#: pkg/metrics/metrics.jsx:123 pkg/metrics/metrics.jsx:880
+#: pkg/metrics/metrics.jsx:1572 pkg/metrics/metrics.jsx:1950
+msgid "Memory"
+msgstr "内存"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Memory technology"
+msgstr "内存拓扑"
+
+#: pkg/metrics/metrics.jsx:122
+msgid "Memory usage"
+msgstr "内存使用率"
+
+#: pkg/metrics/metrics.jsx:1939
+msgid "Memory usage/swap"
+msgstr "内存使用率/swap"
+
+#: pkg/systemd/services.jsx:225
+msgid "Merged"
+msgstr "合并"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:198
+msgid "Message to logged in users"
+msgstr "发送给已登录用户的信息"
+
+#: pkg/shell/failures.jsx:43
+msgid "Messages related to the failure might be found in the journal:"
+msgstr "与失败相关的消息可能会在日志中找到:"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:83
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:145
+msgid "Metadata used"
+msgstr "已使用的元数据"
+
+#: pkg/shell/superuser.jsx:205
+msgid "Method"
+msgstr "模式"
+
+#: pkg/networkmanager/ip-settings.jsx:382
+msgid "Metric"
+msgstr "指标"
+
+#: pkg/metrics/index.html:20 pkg/metrics/metrics.jsx:2000
+msgid "Metrics and history"
+msgstr "指标和历史数据"
+
+#: pkg/metrics/metrics.jsx:1841
+msgid "Metrics history could not be loaded"
+msgstr "无法加载指标历史记录"
+
+#: pkg/metrics/metrics.jsx:1463 pkg/metrics/metrics.jsx:1557
+msgid "Metrics settings"
+msgstr "指标设置"
+
+#: pkg/lib/machine-info.js:94
+msgid "Mini PC"
+msgstr "迷你电脑"
+
+#: pkg/lib/machine-info.js:65
+msgid "Mini tower"
+msgstr "迷你塔式主机"
+
+#: pkg/systemd/timer-dialog.jsx:273
+msgid "Minute needs to be a number between 0-59"
+msgstr "分钟需要是 0-59 间的数字"
+
+#: pkg/systemd/timer-dialog.jsx:243
+msgid "Minutely"
+msgstr "每分钟"
+
+#: pkg/systemd/timer-dialog.jsx:211
+msgid "Minutes"
+msgstr "分钟"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:261
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:77
+msgid "Mirrored (RAID 1)"
+msgstr "镜像 (RAID 1)"
+
+#: pkg/systemd/hwinfo.jsx:69
+msgid "Mitigations"
+msgstr "缓解"
+
+#: pkg/networkmanager/bond.jsx:171
+msgid "Mode"
+msgstr "模式"
+
+#: pkg/systemd/hwinfo.jsx:277
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:111
+#: pkg/storaged/drive/drive.jsx:121
+msgid "Model"
+msgstr "型号"
+
+#: pkg/storaged/jobs-panel.jsx:44 pkg/storaged/jobs-panel.jsx:50
+#: pkg/storaged/jobs-panel.jsx:57
+msgid "Modifying $target"
+msgstr "修改 $target"
+
+#: pkg/systemd/timer-dialog.jsx:317 pkg/packagekit/autoupdates.jsx:309
+msgid "Mondays"
+msgstr "周一"
+
+#: pkg/networkmanager/bond.jsx:194
+msgid "Monitoring interval"
+msgstr "监控间隔"
+
+#: pkg/networkmanager/bond.jsx:205
+msgid "Monitoring targets"
+msgstr "监控目标"
+
+#: pkg/systemd/timer-dialog.jsx:247
+msgid "Monthly"
+msgstr "每月"
+
+#: pkg/packagekit/updates.jsx:941
+msgid "More info..."
+msgstr "更多信息..."
+
+#: pkg/storaged/filesystem/filesystem.jsx:103
+#: pkg/storaged/filesystem/mounting-dialog.jsx:277 pkg/storaged/nfs/nfs.jsx:310
+#: pkg/storaged/stratis/filesystem.jsx:177 pkg/storaged/btrfs/subvolume.jsx:275
+msgid "Mount"
+msgstr "挂载"
+
+#: pkg/storaged/btrfs/subvolume.jsx:149
+msgid "Mount Point"
+msgstr "挂载点"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:78
+msgid "Mount after network becomes available, ignore failure"
+msgstr "网络可用后挂载,忽略失败"
+
+#: pkg/storaged/filesystem/mismounting.jsx:210
+msgid "Mount also automatically on boot"
+msgstr "在引导时也自动挂载"
+
+#: pkg/storaged/nfs/nfs.jsx:171
+msgid "Mount at boot"
+msgstr "引导时挂载"
+
+#: pkg/storaged/filesystem/mismounting.jsx:202
+#: pkg/storaged/filesystem/mismounting.jsx:214
+msgid "Mount automatically on $0 on boot"
+msgstr "在引导时在 $0 自动挂载"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:70
+msgid "Mount before services start"
+msgstr "服务启动前挂载"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:273
+msgid "Mount configuration"
+msgstr "挂载配置"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:271
+msgid "Mount filesystem"
+msgstr "挂载文件系统"
+
+#: pkg/storaged/filesystem/mismounting.jsx:207
+msgid "Mount now"
+msgstr "现在挂载"
+
+#: pkg/storaged/filesystem/mismounting.jsx:203
+msgid "Mount on $0 now"
+msgstr "现在在 $0 挂载"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:47 pkg/storaged/nfs/nfs.jsx:168
+msgid "Mount options"
+msgstr "挂载选项"
+
+#: pkg/storaged/filesystem/filesystem.jsx:147
+#: pkg/storaged/filesystem/mounting-dialog.jsx:246
+#: pkg/storaged/block/format-dialog.jsx:345 pkg/storaged/nfs/nfs.jsx:329
+#: pkg/storaged/stratis/filesystem.jsx:91
+#: pkg/storaged/stratis/filesystem.jsx:226 pkg/storaged/stratis/pool.jsx:104
+#: pkg/storaged/btrfs/subvolume.jsx:335
+msgid "Mount point"
+msgstr "挂载点"
+
+#: pkg/storaged/filesystem/utils.jsx:115
+msgid "Mount point cannot be empty"
+msgstr "挂载点不能为空"
+
+#: pkg/storaged/nfs/nfs.jsx:162
+msgid "Mount point cannot be empty."
+msgstr "挂载点不能为空。"
+
+#: pkg/storaged/filesystem/utils.jsx:121
+msgid "Mount point is already used for $0"
+msgstr "挂载点已用于 $0"
+
+#: pkg/storaged/nfs/nfs.jsx:164
+msgid "Mount point must start with \"/\"."
+msgstr "挂载点必须以“/”开头。"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:55 pkg/storaged/nfs/nfs.jsx:172
+msgid "Mount read only"
+msgstr "只读挂载"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:74
+msgid "Mount without waiting, ignore failure"
+msgstr "无需等待即可挂载,忽略失败"
+
+#: pkg/storaged/jobs-panel.jsx:48
+msgid "Mounting $target"
+msgstr "挂载 $target"
+
+#: pkg/storaged/block/format-dialog.jsx:94
+msgid "Mounts before services start"
+msgstr "服务启动前挂载"
+
+#: pkg/storaged/block/format-dialog.jsx:108
+msgid "Mounts in parallel with services"
+msgstr "与服务并行挂载"
+
+#: pkg/storaged/block/format-dialog.jsx:119
+msgid "Mounts in parallel with services, but after network is available"
+msgstr "与服务并行挂载,但是在网络可用后"
+
+#: pkg/lib/machine-info.js:84
+msgid "Multi-system chassis"
+msgstr "多系统机箱"
+
+#: pkg/storaged/drive/drive.jsx:135
+msgid "Multipathed devices"
+msgstr "多路径设备"
+
+#: pkg/networkmanager/wireguard.jsx:256
+msgid ""
+"Multiple addresses can be specified using commas or spaces as delimiters."
+msgstr "可以使用逗号或空格作为分隔符来指定多个地址。"
+
+#: pkg/storaged/nfs/nfs.jsx:133 pkg/storaged/nfs/nfs.jsx:297
+msgid "NFS mount"
+msgstr "NFS 挂载"
+
+#: pkg/networkmanager/team.jsx:58
+msgid "NSNA ping"
+msgstr "NSNA ping"
+
+#: pkg/lib/serverTime.js:550
+msgid "NTP server"
+msgstr "NTP 服务器"
+
+#: pkg/users/authorized-keys-panel.js:128 pkg/users/group-create-dialog.js:39
+#: pkg/networkmanager/dialogs-common.jsx:142
+#: pkg/networkmanager/network-main.jsx:185
+#: pkg/networkmanager/network-main.jsx:200 pkg/systemd/timer-dialog.jsx:157
+#: pkg/systemd/hwinfo.jsx:86 pkg/packagekit/updates.jsx:448
+#: pkg/storaged/iscsi/create-dialog.jsx:111
+#: pkg/storaged/iscsi/create-dialog.jsx:168
+#: pkg/storaged/lvm2/block-logical-volume.jsx:49
+#: pkg/storaged/lvm2/block-logical-volume.jsx:238
+#: pkg/storaged/lvm2/block-logical-volume.jsx:286
+#: pkg/storaged/lvm2/volume-group.jsx:64 pkg/storaged/lvm2/volume-group.jsx:116
+#: pkg/storaged/lvm2/volume-group.jsx:380
+#: pkg/storaged/lvm2/create-dialog.jsx:47 pkg/storaged/lvm2/vdo-pool.jsx:78
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:166
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:44
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:138
+#: pkg/storaged/filesystem/filesystem.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:141
+#: pkg/storaged/block/format-dialog.jsx:340
+#: pkg/storaged/mdraid/create-dialog.jsx:47 pkg/storaged/mdraid/mdraid.jsx:288
+#: pkg/storaged/stratis/create-dialog.jsx:50
+#: pkg/storaged/stratis/filesystem.jsx:86
+#: pkg/storaged/stratis/filesystem.jsx:197
+#: pkg/storaged/stratis/filesystem.jsx:221 pkg/storaged/stratis/pool.jsx:93
+#: pkg/storaged/stratis/pool.jsx:170 pkg/storaged/stratis/pool.jsx:518
+#: pkg/storaged/btrfs/subvolume.jsx:145 pkg/storaged/btrfs/subvolume.jsx:333
+#: pkg/storaged/btrfs/volume.jsx:99 pkg/storaged/partitions/partition.jsx:219
+#: pkg/shell/credentials.jsx:101
+msgid "Name"
+msgstr "名称"
+
+#: pkg/storaged/stratis/utils.jsx:32 pkg/storaged/stratis/utils.jsx:119
+msgid "Name can not be empty."
+msgstr "名称不能为空."
+
+#: pkg/storaged/btrfs/utils.jsx:85 pkg/storaged/utils.js:212
+msgid "Name cannot be empty."
+msgstr "名称不能为空。"
+
+#: pkg/storaged/utils.js:241
+msgid "Name cannot be longer than $0 bytes"
+msgstr "名称不能长于 $0 字节"
+
+#: pkg/storaged/utils.js:239
+msgid "Name cannot be longer than $0 characters"
+msgstr "名称不能长于 $0 个字符"
+
+#: pkg/storaged/utils.js:214
+msgid "Name cannot be longer than 127 characters."
+msgstr "名称不能长于127个字符。"
+
+#: pkg/storaged/btrfs/utils.jsx:87
+msgid "Name cannot be longer than 255 characters."
+msgstr "名称不能超过 255 个字符。"
+
+#: pkg/storaged/utils.js:218
+msgid "Name cannot contain the character '$0'."
+msgstr "名称不能包含字符 '$0'。"
+
+#: pkg/storaged/btrfs/utils.jsx:89
+msgid "Name cannot contain the character '/'."
+msgstr "名称不能包含字符 '/'。"
+
+#: pkg/storaged/utils.js:220
+msgid "Name cannot contain whitespace."
+msgstr "名称不能包含空格。"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:91
+msgid "Need a spare disk"
+msgstr "需要一个备用磁盘"
+
+#: pkg/lib/serverTime.js:695
+msgid "Need at least one NTP server"
+msgstr "至少需要一个 NTP 服务器"
+
+#: pkg/metrics/metrics.jsx:979 pkg/metrics/metrics.jsx:1572
+#: pkg/metrics/metrics.jsx:1939 pkg/metrics/metrics.jsx:1952
+msgid "Network"
+msgstr "网络"
+
+#: pkg/metrics/metrics.jsx:144 pkg/metrics/metrics.jsx:145
+msgid "Network I/O"
+msgstr "网络 I/O"
+
+#: pkg/networkmanager/bond.jsx:138
+msgid "Network bond"
+msgstr "网络绑定"
+
+#: pkg/networkmanager/networkmanager.jsx:101
+msgid "Network devices and graphs require NetworkManager"
+msgstr "网络设备和图形需要 NetworkManager"
+
+#: pkg/networkmanager/network-main.jsx:207
+msgid "Network logs"
+msgstr "网络日志"
+
+#: pkg/metrics/metrics.jsx:988
+msgid "Network usage"
+msgstr "网络使用率"
+
+#: pkg/networkmanager/networkmanager.jsx:93
+msgid "NetworkManager is not installed"
+msgstr "未安装 NetworkManager"
+
+#: pkg/networkmanager/networkmanager.jsx:77
+msgid "NetworkManager is not running"
+msgstr "NetworkManager 未运行"
+
+#: pkg/storaged/overview/overview.jsx:168
+msgid "Networked storage"
+msgstr "网络存储"
+
+#: pkg/networkmanager/index.html:23 pkg/networkmanager/firewall.jsx:1057
+#: pkg/networkmanager/network-interface.jsx:705
+#: pkg/networkmanager/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:5
+msgid "Networking"
+msgstr "网络"
+
+#: pkg/users/account-details.js:214
+msgid "Never"
+msgstr "从不"
+
+#: pkg/users/expiration-dialogs.js:42 pkg/users/account-details.js:75
+msgid "Never expire account"
+msgstr "账号从不过期"
+
+#: pkg/users/expiration-dialogs.js:154 pkg/users/account-details.js:67
+msgid "Never expire password"
+msgstr "密码从不过期"
+
+#: pkg/users/accounts-list.js:173
+msgid "Never logged in"
+msgstr "从未登录"
+
+#: pkg/storaged/overview/overview.jsx:151 pkg/storaged/nfs/nfs.jsx:133
+msgid "New NFS mount"
+msgstr "新的 NFS 挂载"
+
+#: pkg/static/login.js:763
+msgid "New host"
+msgstr "新主机"
+
+#: pkg/shell/hosts_dialog.jsx:464
+msgid "New host: $0"
+msgstr "新主机:$0"
+
+#: pkg/shell/hosts_dialog.jsx:812
+msgid "New key password"
+msgstr "新密钥密码"
+
+#: pkg/users/rename-group-dialog.jsx:35
+msgid "New name"
+msgstr "新名称"
+
+#: pkg/storaged/stratis/pool.jsx:411 pkg/storaged/crypto/keyslots.jsx:433
+#: pkg/storaged/crypto/keyslots.jsx:483
+msgid "New passphrase"
+msgstr "新密码"
+
+#: pkg/users/password-dialogs.js:147 pkg/shell/credentials.jsx:265
+msgid "New password"
+msgstr "新密码"
+
+#: pkg/users/password-dialogs.js:86 pkg/lib/credentials.js:241
+msgid "New password was not accepted"
+msgstr "新密码不被接受"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:37
+msgid "Next"
+msgstr "下一步"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:290
+msgid "No"
+msgstr "否"
+
+#: pkg/users/group-create-dialog.js:79
+msgid "No ID specified"
+msgstr "没有指定 ID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:325
+msgid "No SELinux alerts."
+msgstr "没有 SELinux 警告。"
+
+#: pkg/apps/application-list.jsx:198
+msgid "No applications installed or available."
+msgstr "应用未安装或不可用."
+
+#: pkg/storaged/crypto/keyslots.jsx:776
+msgid "No available slots"
+msgstr "没有可用的插槽"
+
+#: pkg/storaged/stratis/create-dialog.jsx:57
+msgid "No block devices are available."
+msgstr "没有可用的块设备。"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:140 pkg/storaged/stratis/pool.jsx:569
+msgid "No block devices found"
+msgstr "没有找到块设备"
+
+#: pkg/networkmanager/interfaces.js:1356
+msgid "No carrier"
+msgstr "无载体"
+
+#: pkg/kdump/kdump-view.jsx:471
+msgid "No configuration found"
+msgstr "未找到配置"
+
+#: pkg/metrics/metrics.jsx:1869
+msgid "No data available"
+msgstr "没有可用数据"
+
+#: pkg/metrics/metrics.jsx:1865
+msgid "No data available between $0 and $1"
+msgstr "在 $0 和 $1 间没有可用数据"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:175
+msgid "No delay"
+msgstr "无延时"
+
+#: pkg/networkmanager/firewall.jsx:852
+msgid "No description available"
+msgstr "没有可用描述"
+
+#: pkg/apps/application.jsx:85
+msgid "No description provided."
+msgstr "没有提供说明。"
+
+#: pkg/storaged/btrfs/volume.jsx:135
+msgid "No devices found"
+msgstr "没有找到设备"
+
+#: pkg/storaged/lvm2/volume-group.jsx:200
+#: pkg/storaged/lvm2/create-dialog.jsx:54
+#: pkg/storaged/mdraid/create-dialog.jsx:101 pkg/storaged/mdraid/mdraid.jsx:158
+#: pkg/storaged/stratis/pool.jsx:212
+msgid "No disks are available."
+msgstr "没有可用的磁盘。"
+
+#: pkg/storaged/mdraid/mdraid.jsx:301
+msgid "No disks found"
+msgstr "没有找到磁盘"
+
+#: pkg/storaged/iscsi/session.jsx:79
+msgid "No drives found"
+msgstr "没有找到驱动器"
+
+#: pkg/storaged/block/format-dialog.jsx:247
+msgid "No encryption"
+msgstr "无加密"
+
+#: pkg/metrics/metrics.jsx:1333
+msgid "No events"
+msgstr "没有事件"
+
+#: pkg/storaged/block/format-dialog.jsx:211
+msgid "No filesystem"
+msgstr "无文件系统"
+
+#: pkg/storaged/stratis/pool.jsx:360
+msgid "No filesystems"
+msgstr "无文件系统"
+
+#: pkg/storaged/crypto/keyslots.jsx:781
+msgid "No free key slots"
+msgstr "没有空闲的密钥 slot"
+
+#: pkg/storaged/lvm2/volume-group.jsx:225
+msgid "No free space"
+msgstr "没有剩余空间"
+
+#: pkg/storaged/partitions/partition.jsx:79
+msgid "No free space after this partition"
+msgstr "这个分区后没有可用空间"
+
+#: pkg/users/group-create-dialog.js:62
+msgid "No group name specified"
+msgstr "没有指定组名称"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:181
+msgid "No host keys found."
+msgstr "没有找到主机密钥。"
+
+#: pkg/apps/utils.jsx:77
+msgid "No installation package found for this application."
+msgstr "没有找到该应用的安装包。"
+
+#: pkg/storaged/crypto/keyslots.jsx:698
+msgid "No keys added"
+msgstr "没有添加密钥"
+
+#: pkg/shell/shell-modals.jsx:152
+msgid "No languages match"
+msgstr "没有语言匹配"
+
+#: pkg/systemd/service.jsx:166 pkg/metrics/metrics.jsx:1149
+msgid "No log entries"
+msgstr "无日志条目"
+
+#: pkg/storaged/lvm2/volume-group.jsx:297
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:126
+msgid "No logical volumes"
+msgstr "没有逻辑卷"
+
+#: pkg/systemd/logsJournal.jsx:281 pkg/systemd/logsJournal.jsx:324
+msgid "No logs found"
+msgstr "没有找到日志"
+
+#: pkg/users/accounts-list.js:350 pkg/users/accounts-list.js:463
+#: pkg/systemd/services-list.jsx:59
+msgid "No matching results"
+msgstr "没有找到匹配的结果"
+
+#: pkg/storaged/drive/drive.jsx:128
+msgid "No media inserted"
+msgstr "没有插入介质"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:58
+msgid "No partitioning"
+msgstr "无分区"
+
+#: pkg/storaged/partitions/partition-table.jsx:107
+msgid "No partitions found"
+msgstr "没有找到分区"
+
+#: pkg/networkmanager/wireguard.jsx:341
+msgid "No peers added."
+msgstr "没有添加对等。"
+
+#: pkg/storaged/lvm2/volume-group.jsx:392
+msgid "No physical volumes found"
+msgstr "没有找到物理卷"
+
+#: pkg/users/account-create-dialog.js:168
+msgid "No real name specified"
+msgstr "未指定真实姓名"
+
+#: pkg/systemd/logs.jsx:315 pkg/shell/nav.jsx:157
+msgid "No results found"
+msgstr "没有找到结果"
+
+#: pkg/systemd/services-list.jsx:57
+msgid ""
+"No results match the filter criteria. Clear all filters to show results."
+msgstr "没有结果符合过滤条件。清除所有过滤器以显示结果。"
+
+#: pkg/systemd/overview-cards/insights.jsx:184
+msgid "No rule hits"
+msgstr "无规则命中"
+
+#: pkg/storaged/overview/overview.jsx:190
+msgid "No storage found"
+msgstr "没有找到存储"
+
+#: pkg/storaged/btrfs/volume.jsx:147
+msgid "No subvolumes"
+msgstr "没有子卷"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:182
+#: pkg/lib/credentials.js:191
+msgid "No such file or directory"
+msgstr "没有该文件或目录"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "No system modifications"
+msgstr "没有系统改变"
+
+#: pkg/sosreport/sosreport.jsx:499
+msgid "No system reports."
+msgstr "没有系统报告。"
+
+#: pkg/packagekit/autoupdates.jsx:283
+msgid "No updates"
+msgstr "没有更新"
+
+#: pkg/users/account-create-dialog.js:151
+msgid "No user name specified"
+msgstr "未指定用户名"
+
+#: pkg/networkmanager/firewall.jsx:858 pkg/kdump/kdump-view.jsx:488
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:232
+msgid "None"
+msgstr "无"
+
+#: pkg/lib/credentials.js:277
+msgid "Not a valid private key"
+msgstr "无效的私钥"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to disable the firewall"
+msgstr "无权禁用防火墙"
+
+#: pkg/networkmanager/firewall-switch.jsx:65
+msgid "Not authorized to enable the firewall"
+msgstr "无权启用防火墙"
+
+#: pkg/networkmanager/interfaces.js:791 pkg/packagekit/kpatch.jsx:240
+msgid "Not available"
+msgstr "不可用"
+
+#: pkg/selinux/selinux.js:252
+msgid "Not connected"
+msgstr "未连接"
+
+#: pkg/systemd/overview-cards/insights.jsx:164
+msgid "Not connected to Insights"
+msgstr "没有连接到 Insights"
+
+#: pkg/shell/indexes.jsx:465
+msgid "Not connected to host"
+msgstr "没有连接到主机"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:78
+msgid "Not enough free space"
+msgstr "没有足够的可用空间"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:95
+#: pkg/storaged/stratis/filesystem.jsx:76 pkg/storaged/stratis/pool.jsx:310
+msgid "Not enough space"
+msgstr "没有足够空间"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:183
+msgid "Not enough space to grow"
+msgstr "没有足够的空间来增长"
+
+#: pkg/systemd/services.jsx:222 pkg/storaged/pages.jsx:275
+#: pkg/storaged/pages.jsx:278
+msgid "Not found"
+msgstr "未找到"
+
+#: pkg/packagekit/kpatch.jsx:246
+msgid "Not installed"
+msgstr "未安装"
+
+#: pkg/networkmanager/network-interface.jsx:680
+msgid "Not permitted to configure network devices"
+msgstr "不允许配置网络设备"
+
+#: pkg/systemd/overview-cards/realmd.jsx:462
+msgid "Not permitted to configure realms"
+msgstr "不允许配置域"
+
+#: pkg/lib/cockpit.js:3857
+msgid "Not permitted to perform this action."
+msgstr "不允许执行该操作。"
+
+#: pkg/playground/translate.html:29
+msgid "Not ready"
+msgstr "未就绪"
+
+#: pkg/packagekit/updates.jsx:1366
+msgid "Not registered"
+msgstr "没有注册"
+
+#: pkg/systemd/services.jsx:234 pkg/systemd/services.jsx:237
+#: pkg/systemd/services.jsx:697 pkg/systemd/service-details.jsx:472
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Not running"
+msgstr "未运行"
+
+#: pkg/packagekit/autoupdates.jsx:346
+msgid "Not set up"
+msgstr "未设置"
+
+#: pkg/lib/serverTime.js:458
+msgid "Not synchronized"
+msgstr "未同步"
+
+#: pkg/systemd/service-details.jsx:275
+msgid "Note"
+msgstr "注意"
+
+#: pkg/lib/machine-info.js:69
+msgid "Notebook"
+msgstr "笔记本"
+
+#: pkg/systemd/logs.jsx:70
+msgid "Notice and above"
+msgstr "Notice 及更高级别"
+
+#: pkg/sosreport/sosreport.jsx:320
+msgid "Obfuscate network addresses, hostnames, and usernames"
+msgstr "模糊处理网络地址、主机名和用户名"
+
+#: pkg/sosreport/sosreport.jsx:454
+msgid "Obfuscated"
+msgstr "模糊处理"
+
+#: pkg/selinux/setroubleshoot-view.jsx:347
+msgid "Occurred $0"
+msgstr "发生于 $0"
+
+#: pkg/selinux/setroubleshoot-view.jsx:342
+msgid "Occurred between $0 and $1"
+msgstr "在 $0 和 $1 间发生"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:98
+#: pkg/selinux/setroubleshoot-view.jsx:414
+msgid "Occurrences"
+msgstr "发生"
+
+#: pkg/lib/cockpit-components-dialog.jsx:159
+msgid "Ok"
+msgstr "确认"
+
+#: pkg/storaged/stratis/pool.jsx:406 pkg/storaged/crypto/keyslots.jsx:480
+msgid "Old passphrase"
+msgstr "旧密码"
+
+#: pkg/users/password-dialogs.js:140
+msgid "Old password"
+msgstr "旧密码"
+
+#: pkg/users/password-dialogs.js:55 pkg/lib/credentials.js:221
+msgid "Old password not accepted"
+msgstr "旧密码不被接受"
+
+#: pkg/kdump/kdump-view.jsx:463
+msgid "On a mounted device"
+msgstr "在一个已挂载的设备上"
+
+#: pkg/systemd/service-details.jsx:576
+msgid "On failure"
+msgstr "失败时"
+
+#: src/ws/cockpit.appdata.xml.in:26
+msgid ""
+"Once Cockpit is installed, enable it with \"systemctl enable --now cockpit."
+"socket\"."
+msgstr ""
+"在安装 Cockpit 后,使用 \"systemctl enable --now cockpit.socket\" 启用它。"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:240
+msgid "Only $0 of $1 are used."
+msgstr "仅使用 $1 的 $0。"
+
+#: pkg/systemd/timer-dialog.jsx:164
+msgid "Only alphabets, numbers, : , _ , . , @ , - are allowed"
+msgstr "只允许使用字母, 数字, : , _ , . , @ , -"
+
+#: pkg/systemd/logs.jsx:65
+msgid "Only emergency"
+msgstr "只限紧急情况"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:112
+msgid "Only use approved and allowed algorithms when booting in FIPS mode."
+msgstr "仅在使用 FIPS 模式引导时才使用已批准和允许的算法。"
+
+#: pkg/shell/topnav.jsx:244
+msgid "Ooops!"
+msgstr "糟糕!"
+
+#: pkg/metrics/metrics.jsx:1450
+msgid "Open the pmproxy service in the firewall to share metrics."
+msgstr "在防火墙中打开 pmproxy 服务以共享指标。"
+
+#: pkg/storaged/jobs-panel.jsx:89
+msgid "Operation '$operation' on $target"
+msgstr "$target 上的操作 '$operation'"
+
+#: pkg/users/account-details.js:269 pkg/networkmanager/bridge.jsx:104
+#: pkg/sosreport/sosreport.jsx:319
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:223
+#: pkg/storaged/stratis/create-dialog.jsx:64
+#: pkg/storaged/crypto/encryption.jsx:234
+msgid "Options"
+msgstr "选项"
+
+#: pkg/static/login.html:49
+msgid "Or use a bundled browser"
+msgstr "或者使用捆绑的浏览器"
+
+#: pkg/lib/machine-info.js:60
+msgid "Other"
+msgstr "其他"
+
+#: pkg/users/account-create-dialog.js:131 pkg/users/account-details.js:279
+msgid ""
+"Other authentication methods are still available even when interactive "
+"password authentication is not allowed."
+msgstr "即使不允许交互式密码身份验证,仍可使用其他身份验证方法。"
+
+#: pkg/static/login.html:121
+msgid "Other options"
+msgstr "其他选项"
+
+#: pkg/metrics/metrics.jsx:814
+msgid "Out"
+msgstr "出"
+
+#: pkg/systemd/hwinfo.jsx:307 pkg/metrics/metrics.jsx:1999
+#: pkg/systemd/manifest.json:0
+msgid "Overview"
+msgstr "概览"
+
+#: pkg/storaged/block/format-dialog.jsx:369
+#: pkg/storaged/partitions/format-disk-dialog.jsx:61
+msgid "Overwrite"
+msgstr "覆盖"
+
+#: pkg/storaged/block/format-dialog.jsx:372
+#: pkg/storaged/partitions/format-disk-dialog.jsx:64
+msgid "Overwrite existing data with zeros (slower)"
+msgstr "用 0 覆盖现有数据(较慢)"
+
+#: pkg/systemd/hwinfo.jsx:273 pkg/systemd/hwinfo.jsx:326
+msgid "PCI"
+msgstr "PCI"
+
+#: pkg/storaged/dialog.jsx:1325
+msgid "PID"
+msgstr "PID"
+
+#: pkg/metrics/metrics.jsx:1814
+msgid "Package cockpit-pcp is missing for metrics history"
+msgstr "指标历史记录缺少软件包 cockpit-pcp"
+
+#: pkg/packagekit/updates.jsx:690 pkg/packagekit/updates.jsx:769
+msgid "Package information"
+msgstr "软件包信息"
+
+#: pkg/lib/packagekit.js:130
+msgid "PackageKit crashed"
+msgstr "PackageKit 已崩溃"
+
+#: pkg/packagekit/updates.jsx:1104
+msgid "PackageKit is not installed"
+msgstr "PackageKit 未安装"
+
+#: pkg/packagekit/updates.jsx:1305
+msgid "PackageKit reported error code $0"
+msgstr "PackageKit 已报告错误码 $0"
+
+#: pkg/packagekit/updates.jsx:364
+msgid "Packages"
+msgstr "软件包"
+
+#: pkg/shell/active-pages-modal.jsx:104
+msgid "Page name"
+msgstr "页名称"
+
+#: pkg/networkmanager/vlan.jsx:95
+msgid "Parent"
+msgstr "父"
+
+#: pkg/networkmanager/network-interface.jsx:546
+msgid "Parent $parent"
+msgstr "父 $parent"
+
+#: pkg/systemd/service-details.jsx:566
+msgid "Part of"
+msgstr "部分"
+
+#: pkg/networkmanager/interfaces.js:1385
+msgid "Part of $0"
+msgstr "$0 的部分"
+
+#: pkg/storaged/partitions/partition.jsx:83
+msgid "Partition"
+msgstr "分区"
+
+#: pkg/storaged/utils.js:364
+msgid "Partition of $0"
+msgstr "$0 的分区"
+
+#: pkg/storaged/partitions/partition.jsx:205
+msgid "Partition size is $0. Content size is $1."
+msgstr "分区大小是 $0。内容大小为 $1。"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:49
+msgid "Partitioning"
+msgstr "分区"
+
+#: pkg/storaged/partitions/partition-table.jsx:93
+#: pkg/storaged/partitions/partition-table.jsx:108
+msgid "Partitions"
+msgstr "分区"
+
+#: pkg/networkmanager/team.jsx:50
+msgid "Passive"
+msgstr "被动"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:262
+#: pkg/storaged/block/format-dialog.jsx:381
+#: pkg/storaged/block/format-dialog.jsx:409
+#: pkg/storaged/stratis/create-dialog.jsx:75
+#: pkg/storaged/stratis/stopped-pool.jsx:58
+#: pkg/storaged/stratis/stopped-pool.jsx:129 pkg/storaged/stratis/pool.jsx:205
+#: pkg/storaged/stratis/pool.jsx:382 pkg/storaged/stratis/pool.jsx:530
+#: pkg/storaged/crypto/actions.jsx:42 pkg/storaged/crypto/keyslots.jsx:428
+#: pkg/storaged/crypto/keyslots.jsx:748
+msgid "Passphrase"
+msgstr "密码"
+
+#: pkg/storaged/crypto/keyslots.jsx:571
+msgid "Passphrase can not be empty"
+msgstr "密码不能为空"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:265
+#: pkg/storaged/block/format-dialog.jsx:385
+#: pkg/storaged/block/format-dialog.jsx:413
+#: pkg/storaged/stratis/create-dialog.jsx:79 pkg/storaged/stratis/pool.jsx:208
+#: pkg/storaged/stratis/pool.jsx:383 pkg/storaged/stratis/pool.jsx:409
+#: pkg/storaged/stratis/pool.jsx:412 pkg/storaged/stratis/pool.jsx:467
+#: pkg/storaged/crypto/keyslots.jsx:143 pkg/storaged/crypto/keyslots.jsx:436
+#: pkg/storaged/crypto/keyslots.jsx:481 pkg/storaged/crypto/keyslots.jsx:485
+msgid "Passphrase cannot be empty"
+msgstr "密码不能为空"
+
+#: pkg/storaged/crypto/keyslots.jsx:593
+msgid "Passphrase from any other key slot"
+msgstr "来自其他密钥插槽的密码短语"
+
+#: pkg/storaged/stratis/pool.jsx:443 pkg/storaged/crypto/keyslots.jsx:584
+msgid "Passphrase removal may prevent unlocking $0."
+msgstr "删除密码可能会阻止解锁 $0。"
+
+#: pkg/storaged/block/format-dialog.jsx:394
+#: pkg/storaged/stratis/create-dialog.jsx:88 pkg/storaged/stratis/pool.jsx:385
+#: pkg/storaged/stratis/pool.jsx:414 pkg/storaged/crypto/keyslots.jsx:445
+#: pkg/storaged/crypto/keyslots.jsx:490
+msgid "Passphrases do not match"
+msgstr "密码不匹配"
+
+#: pkg/static/login.html:99 pkg/users/account-create-dialog.js:139
+#: pkg/users/account-details.js:296 pkg/storaged/iscsi/create-dialog.jsx:34
+#: pkg/storaged/iscsi/create-dialog.jsx:140 pkg/shell/credentials.jsx:119
+#: pkg/shell/credentials.jsx:262 pkg/shell/credentials.jsx:318
+#: pkg/shell/superuser.jsx:144 pkg/shell/hosts_dialog.jsx:845
+#: pkg/shell/hosts_dialog.jsx:854
+msgid "Password"
+msgstr "密码"
+
+#: pkg/shell/credentials.jsx:259
+msgid "Password changed successfully"
+msgstr "更改密码成功"
+
+#: pkg/users/expiration-dialogs.js:209
+msgid "Password expiration"
+msgstr "密码过期"
+
+#: pkg/users/account-create-dialog.js:358 pkg/users/password-dialogs.js:194
+msgid "Password is longer than 256 characters"
+msgstr "密码长度不能超过 256 个字符"
+
+#: pkg/lib/cockpit-components-password.jsx:51
+msgid "Password is not acceptable"
+msgstr "不接受该密码"
+
+#: pkg/lib/cockpit-components-password.jsx:45
+msgid "Password is too weak"
+msgstr "密码太弱"
+
+#: pkg/users/account-details.js:69
+msgid "Password must be changed"
+msgstr "密码必须被修改"
+
+#: pkg/lib/credentials.js:298
+msgid "Password not accepted"
+msgstr "密码未接受"
+
+#: pkg/shell/credentials.jsx:274
+msgid "Password tip"
+msgstr "密码提示"
+
+#: pkg/lib/cockpit-components-terminal.jsx:215
+msgid "Paste"
+msgstr "粘贴"
+
+#: pkg/lib/cockpit-components-terminal.jsx:223
+msgid "Paste error"
+msgstr "粘贴错误"
+
+#: pkg/networkmanager/wireguard.jsx:215
+msgid "Paste existing key"
+msgstr "粘贴现有密钥"
+
+#: pkg/users/authorized-keys-panel.js:41
+msgid "Paste the contents of your public SSH key file here"
+msgstr "在这里粘贴 SSH 公钥文件内容"
+
+#: pkg/systemd/service-details.jsx:660 pkg/systemd/abrtLog.jsx:128
+msgid "Path"
+msgstr "路径"
+
+#: pkg/networkmanager/bridgeport.jsx:83
+msgid "Path cost"
+msgstr "路径开销"
+
+#: pkg/networkmanager/network-interface.jsx:527
+msgid "Path cost $path_cost"
+msgstr "路径开销 $path_cost"
+
+#: pkg/storaged/nfs/nfs.jsx:145
+msgid "Path on server"
+msgstr "服务器上的路径"
+
+#: pkg/storaged/nfs/nfs.jsx:150
+msgid "Path on server cannot be empty."
+msgstr "服务器上的路径不能为空。"
+
+#: pkg/storaged/nfs/nfs.jsx:152
+msgid "Path on server must start with \"/\"."
+msgstr "服务器上的路径必须以“/”开头。"
+
+#: pkg/users/account-create-dialog.js:88
+msgid "Path to directory"
+msgstr "指向目录的路径"
+
+#: pkg/lib/cockpit-components-file-autocomplete.jsx:169
+#: pkg/shell/credentials.jsx:172
+msgid "Path to file"
+msgstr "文件路径"
+
+#: pkg/systemd/service-tabs.jsx:44
+msgid "Paths"
+msgstr "路径"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Pause"
+msgstr "暂停"
+
+#: pkg/networkmanager/wireguard.jsx:160
+msgid "Peer #$0 has invalid endpoint port. Port must be a number."
+msgstr "对等 #$0 有无效的端点端口。端口必须是一个数字。"
+
+#: pkg/networkmanager/wireguard.jsx:156
+msgid ""
+"Peer #$0 has invalid endpoint. It must be specified as host:port, e.g. "
+"1.2.3.4:51820 or example.com:51820"
+msgstr ""
+"对等 #$0 有无效的端点。它需要以 host:port 的形式指定,如 1.2.3.4:51820 或 "
+"example.com:51820"
+
+#: pkg/networkmanager/wireguard.jsx:268
+msgid "Peers"
+msgstr "对等"
+
+#: pkg/networkmanager/wireguard.jsx:273
+msgid ""
+"Peers are other machines that connect with this one. Public keys from other "
+"machines will be shared with each other."
+msgstr ""
+"对等(peer)是与这个系统连接的其他机器。来自其他机器中的公钥将相互共享。"
+
+#: pkg/metrics/metrics.jsx:1466
+msgid ""
+"Performance Co-Pilot collects and analyzes performance metrics from your "
+"system."
+msgstr "Performance Co-Pilot 会收集或分析来自您的系统的性能指标。"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:85
+msgid "Performance profile"
+msgstr "性能配置集"
+
+#: pkg/lib/machine-info.js:80
+msgid "Peripheral chassis"
+msgstr "外设机箱"
+
+#: pkg/networkmanager/dialogs-common.jsx:77
+msgid "Permanent"
+msgstr "永久的"
+
+#: pkg/users/delete-group-dialog.js:33
+msgid "Permanently delete $0 group?"
+msgstr "永久删除 $0 组?"
+
+#: pkg/storaged/lvm2/volume-group.jsx:93
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:119
+#: pkg/storaged/mdraid/mdraid.jsx:123 pkg/storaged/stratis/pool.jsx:149
+#: pkg/storaged/partitions/partition.jsx:54
+msgid "Permanently delete $0?"
+msgstr "永久删除 $0?"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:75
+msgid "Permanently delete logical volume $0/$1?"
+msgstr "永久删除逻辑卷 $0/$1?"
+
+#: pkg/storaged/btrfs/subvolume.jsx:224
+msgid "Permanently delete subvolume $0?"
+msgstr "永久删除子卷 $0?"
+
+#: pkg/static/login.js:933
+msgid "Permission denied"
+msgstr "权限被拒绝"
+
+#: pkg/selinux/setroubleshoot-view.jsx:263
+msgid "Permissive"
+msgstr "允许的"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:292
+msgid "Physical"
+msgstr "物理"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:130
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:180
+#: pkg/storaged/block/resize.jsx:436
+msgid "Physical Volumes"
+msgstr "物理卷"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:356
+#: pkg/storaged/lvm2/volume-group.jsx:390
+msgid "Physical volumes"
+msgstr "物理卷"
+
+#: pkg/storaged/block/resize.jsx:279
+msgid "Physical volumes can not be resized here"
+msgstr "此处无法调整物理卷的大小"
+
+#: pkg/users/expiration-dialogs.js:48
+#: pkg/lib/cockpit-components-shutdown.jsx:211 pkg/lib/serverTime.js:599
+#: pkg/systemd/timer-dialog.jsx:347
+msgid "Pick date"
+msgstr "选择日期"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Pin unit"
+msgstr "固定单元"
+
+#: pkg/networkmanager/team.jsx:200
+msgid "Ping interval"
+msgstr "Ping 周期"
+
+#: pkg/networkmanager/team.jsx:203
+msgid "Ping target"
+msgstr "Ping 目标"
+
+#: pkg/systemd/services-list.jsx:103 pkg/systemd/service-details.jsx:628
+msgid "Pinned unit"
+msgstr "固定单元"
+
+#: pkg/lib/machine-info.js:64
+msgid "Pizza box"
+msgstr "披萨盒"
+
+#: pkg/shell/superuser.jsx:143
+msgid "Please authenticate to gain administrative access"
+msgstr "请认证以获得管理员权限"
+
+#: pkg/static/login.html:34
+msgid "Please enable JavaScript to use the Web Console."
+msgstr "请启用 JavaScript 来使用 Web 控制台。"
+
+#: pkg/networkmanager/firewall.jsx:1033
+msgid "Please install the $0 package"
+msgstr "请安装 $0 软件包"
+
+#: pkg/packagekit/updates.jsx:1492
+msgid "Please resolve the issue and reload this page."
+msgstr "请解决这个问题并重新载入此页面。"
+
+#: pkg/users/expiration-dialogs.js:94
+msgid "Please specify an expiration date"
+msgstr "请指定一个过期时间"
+
+#: pkg/static/login.js:576
+msgid "Please specify the host to connect to"
+msgstr "请指定要连接的主机"
+
+#: pkg/storaged/filesystem/utils.jsx:132
+msgid "Please unmount them first."
+msgstr "请首先卸载它们。"
+
+#: pkg/storaged/utils.js:284
+msgid "Pool for thin logical volumes"
+msgstr "瘦逻辑卷池"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:98
+msgid "Pool for thinly provisioned LVM2 logical volumes"
+msgstr "用于精简置备的 LVM2 逻辑卷的池"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:58
+msgid "Pool for thinly provisioned volumes"
+msgstr "瘦分配配置卷的池"
+
+#: pkg/storaged/stratis/pool.jsx:464
+msgid "Pool passphrase"
+msgstr "矿池密码"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:111 pkg/shell/hosts_dialog.jsx:365
+msgid "Port"
+msgstr "端口"
+
+#: pkg/lib/machine-info.js:67
+msgid "Portable"
+msgstr "手提"
+
+#: pkg/networkmanager/bridge.jsx:101 pkg/networkmanager/team.jsx:159
+#: pkg/networkmanager/network-interface-members.jsx:203
+msgid "Ports"
+msgstr "端口"
+
+#: pkg/storaged/partitions/partition.jsx:116
+msgid "PowerPC PReP boot partition"
+msgstr "PowerPC PReP 引导分区"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+msgid "Prefix length"
+msgstr "前缀长度"
+
+#: pkg/networkmanager/ip-settings.jsx:194
+#: pkg/networkmanager/ip-settings.jsx:366
+msgid "Prefix length or netmask"
+msgstr "前缀长度或网络掩码"
+
+#: pkg/networkmanager/interfaces.js:795
+msgid "Preparing"
+msgstr "准备中"
+
+#: pkg/lib/machine-info.js:247 pkg/systemd/hw-detect.js:114
+msgid "Present"
+msgstr "当前"
+
+#: pkg/networkmanager/dialogs-common.jsx:78
+msgid "Preserve"
+msgstr "保留"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:288
+msgid "Pretty host name"
+msgstr "易读主机名"
+
+#: pkg/systemd/logs.jsx:59
+msgid "Previous boot"
+msgstr "以前启动"
+
+#: pkg/networkmanager/bond.jsx:177 pkg/networkmanager/team.jsx:174
+msgid "Primary"
+msgstr "主"
+
+#: pkg/networkmanager/bridgeport.jsx:80 pkg/networkmanager/teamport.jsx:84
+#: pkg/systemd/logs.jsx:208
+msgid "Priority"
+msgstr "优先级"
+
+#: pkg/networkmanager/network-interface.jsx:500
+#: pkg/networkmanager/network-interface.jsx:525
+msgid "Priority $priority"
+msgstr "优先级 $priority"
+
+#: pkg/networkmanager/wireguard.jsx:213
+msgid "Private key"
+msgstr "私钥"
+
+#: pkg/shell/superuser.jsx:194
+msgid "Problem becoming administrator"
+msgstr "成为管理员时出现问题"
+
+#: pkg/systemd/abrtLog.jsx:302
+msgid "Problem details"
+msgstr "问题详情"
+
+#: pkg/systemd/abrtLog.jsx:299
+msgid "Problem info"
+msgstr "问题信息"
+
+#: pkg/storaged/dialog.jsx:1167
+msgid "Processes using the location"
+msgstr "使用位置的进程"
+
+#: pkg/sosreport/sosreport.jsx:290
+msgid "Progress: $0"
+msgstr "进度:$0"
+
+#: pkg/shell/shell-modals.jsx:74
+msgid "Project website"
+msgstr "项目网站"
+
+#: pkg/users/password-dialogs.js:58
+msgid "Prompting via passwd timed out"
+msgstr "通过密码提示已超时"
+
+#: pkg/lib/credentials.js:285
+msgid "Prompting via ssh-add timed out"
+msgstr "通过 ssh-add 提示超时"
+
+#: pkg/lib/credentials.js:210
+msgid "Prompting via ssh-keygen timed out"
+msgstr "通过 ssh-keygen 提示超时"
+
+#: pkg/systemd/service-details.jsx:577
+msgid "Propagates reload to"
+msgstr "传播重新加载到"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:123
+msgid ""
+"Protects from anticipated near-term future attacks at the expense of "
+"interoperability."
+msgstr "防止近期被安全攻击会降低互操作性。"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:53
+msgid "Provide the passphrase for the pool on these block devices:"
+msgstr "为这些块设备上的池提供密码短语:"
+
+#: pkg/networkmanager/wireguard.jsx:237 pkg/networkmanager/wireguard.jsx:300
+#: pkg/shell/credentials.jsx:114
+msgid "Public key"
+msgstr "公钥"
+
+#: pkg/networkmanager/wireguard.jsx:240
+msgid "Public key will be generated when a valid private key is entered"
+msgstr "在输入有效私钥时将生成公钥"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:171
+msgid "Purpose"
+msgstr "目的"
+
+#: pkg/storaged/mdraid/mdraid.jsx:248
+msgid "RAID ($0)"
+msgstr "RAID ($0)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:242
+msgid "RAID 0"
+msgstr "RAID 0"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:57
+msgid "RAID 0 (stripe)"
+msgstr "RAID 0 (条带)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:243
+msgid "RAID 1"
+msgstr "RAID 1"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:61
+msgid "RAID 1 (mirror)"
+msgstr "RAID 1 (镜像)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:247
+msgid "RAID 10"
+msgstr "RAID 10"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:77
+msgid "RAID 10 (stripe of mirrors)"
+msgstr "RAID 10 (条带镜像)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:244
+msgid "RAID 4"
+msgstr "RAID 4"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:65
+msgid "RAID 4 (dedicated parity)"
+msgstr "RAID 4 (奇偶校验)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:245
+msgid "RAID 5"
+msgstr "RAID 5"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:69
+msgid "RAID 5 (distributed parity)"
+msgstr "RAID 5 (奇偶校验)"
+
+#: pkg/storaged/mdraid/mdraid.jsx:246
+msgid "RAID 6"
+msgstr "RAID 6"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:73
+msgid "RAID 6 (double distributed parity)"
+msgstr "RAID 6 (双倍奇偶校验)"
+
+#: pkg/lib/machine-info.js:81
+msgid "RAID chassis"
+msgstr "RAID 机箱"
+
+#: pkg/storaged/mdraid/create-dialog.jsx:51 pkg/storaged/mdraid/mdraid.jsx:289
+msgid "RAID level"
+msgstr "RAID 级别"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:188
+msgid "RAID10 needs an even number of physical volumes"
+msgstr "RAID10 需要有偶数数量的物理卷"
+
+#: pkg/metrics/metrics.jsx:888
+msgid "RAM"
+msgstr "RAM"
+
+#: pkg/lib/machine-info.js:82
+msgid "Rack mount chassis"
+msgstr "机架式机箱"
+
+#: pkg/networkmanager/dialogs-common.jsx:79
+msgid "Random"
+msgstr "随机"
+
+#: pkg/networkmanager/firewall.jsx:892
+msgid "Range"
+msgstr "范围"
+
+#: pkg/networkmanager/firewall.jsx:517
+msgid "Range must be strictly ordered"
+msgstr "范围必须排序"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Rank"
+msgstr "Rank"
+
+#: pkg/kdump/kdump-view.jsx:459
+msgid "Raw to a device"
+msgstr "格式化设备"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:922
+#: pkg/metrics/metrics.jsx:967 pkg/metrics/metrics.jsx:972
+msgid "Read"
+msgstr "读取"
+
+#: pkg/systemd/hwinfo.jsx:206 pkg/metrics/metrics.jsx:1472
+msgid "Read more..."
+msgstr "了解更多..."
+
+#: pkg/systemd/service-details.jsx:499
+msgid "Read-only"
+msgstr "只读"
+
+#: pkg/storaged/plot.jsx:96
+msgid "Reading"
+msgstr "读取中"
+
+#: pkg/kdump/kdump-view.jsx:483
+msgid "Reading..."
+msgstr "读取中..."
+
+#: pkg/playground/translate.html:19
+msgid "Ready"
+msgstr "就绪"
+
+#: pkg/playground/translate.html:24
+msgctxt "verb"
+msgid "Ready"
+msgstr "就绪"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:291
+msgid "Real host name"
+msgstr "实际主机名"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:258
+msgid ""
+"Real host name can only contain lower-case characters, digits, dashes, and "
+"periods (with populated subdomains)"
+msgstr "实际主机名仅包含小写字母、数字、破折号和句号(包括填充的子域)"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:256
+msgid "Real host name must be 64 characters or less"
+msgstr "实际主机名必须小于等于64个字符"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:191
+msgid "Reapply and reboot"
+msgstr "重新应用并重启"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:114
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192 pkg/systemd/overview.jsx:109
+#: pkg/systemd/overview.jsx:128
+msgid "Reboot"
+msgstr "重启"
+
+#: pkg/packagekit/updates.jsx:614
+msgid "Reboot after completion"
+msgstr "完成后重启"
+
+#: pkg/packagekit/updates.jsx:1525
+msgid "Reboot recommended"
+msgstr "推荐重新引导"
+
+#: pkg/packagekit/updates.jsx:685 pkg/packagekit/updates.jsx:760
+#: pkg/packagekit/updates.jsx:824
+msgid "Reboot system..."
+msgstr "重新引导系统......"
+
+#: pkg/networkmanager/plots.js:44 pkg/networkmanager/network-main.jsx:188
+#: pkg/networkmanager/network-main.jsx:203
+#: pkg/networkmanager/network-interface-members.jsx:205
+msgid "Receiving"
+msgstr "接收"
+
+#: pkg/static/login.html:165
+msgid "Recent hosts"
+msgstr "最近的主机"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:107
+msgid "Recommended, secure settings for current threat models."
+msgstr "建议的,当前威胁模型的安全设置。"
+
+#: pkg/shell/failures.jsx:74
+msgid "Reconnect"
+msgstr "重新连接"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Recovering"
+msgstr "恢复"
+
+#: pkg/storaged/jobs-panel.jsx:70
+msgid "Recovering MDRAID device $target"
+msgstr "恢复 MDRAID 设备 $target"
+
+#: pkg/packagekit/updates.jsx:98
+msgid "Refreshing package information"
+msgstr "刷新软件包信息"
+
+#: pkg/static/login.js:923
+msgid "Refusing to connect. Host is unknown"
+msgstr "拒绝连接。主机未知"
+
+#: pkg/static/login.js:925
+msgid "Refusing to connect. Hostkey does not match"
+msgstr "拒绝连接。主机密钥不匹配"
+
+#: pkg/static/login.js:921
+msgid "Refusing to connect. Hostkey is unknown"
+msgstr "拒绝连接。未知主机密钥"
+
+#: pkg/networkmanager/wireguard.jsx:224
+msgid "Regenerate"
+msgstr "重新生成"
+
+#: pkg/storaged/crypto/keyslots.jsx:275
+msgid "Regenerating initrd"
+msgstr "重新生成 initrd"
+
+#: pkg/packagekit/updates.jsx:1378
+msgid "Register…"
+msgstr "注册…"
+
+#: pkg/storaged/dialog.jsx:1255
+msgid "Related processes and services will be forcefully stopped."
+msgstr "相关进程和服务将被强制停止。"
+
+#: pkg/storaged/dialog.jsx:1257
+msgid "Related processes will be forcefully stopped."
+msgstr "相关进程将被强制停止。"
+
+#: pkg/storaged/dialog.jsx:1259
+msgid "Related services will be forcefully stopped."
+msgstr "相关服务将被强制停止。"
+
+#: pkg/systemd/service-details.jsx:132
+msgid "Reload"
+msgstr "重载"
+
+#: pkg/systemd/service-details.jsx:578
+msgid "Reload propagated from"
+msgstr "重新加载的传播来自"
+
+#: pkg/systemd/services.jsx:233
+msgid "Reloading"
+msgstr "重新加载中"
+
+#: pkg/packagekit/updates.jsx:510
+msgid "Reloading the state of remaining services"
+msgstr "重新载入剩余的服务状态"
+
+#: pkg/kdump/kdump-view.jsx:469
+msgid "Remote over CIFS/SMB"
+msgstr "通过 CIFS/SMB 远程"
+
+#: pkg/kdump/kdump-view.jsx:465
+msgid "Remote over FTP"
+msgstr "通过 FTP 远程"
+
+#: pkg/kdump/kdump-view.jsx:251
+msgid "Remote over NFS"
+msgstr "通过 NFS 远程"
+
+#: pkg/kdump/kdump-view.jsx:456
+msgid "Remote over NFS, $0"
+msgstr "通过 NFS 的远程, $0"
+
+#: pkg/kdump/kdump-view.jsx:467
+msgid "Remote over SFTP"
+msgstr "通过 SFTP 远程"
+
+#: pkg/kdump/kdump-view.jsx:249
+msgid "Remote over SSH"
+msgstr "通过 SSH 远程"
+
+#: pkg/kdump/kdump-view.jsx:453
+msgid "Remote over SSH, $0"
+msgstr "通过 SSH 的远程, $0"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:90
+msgid "Removals:"
+msgstr "移除:"
+
+#: pkg/users/authorized-keys-panel.js:143
+#: pkg/users/authorized-keys-panel.js:169 pkg/apps/application.jsx:50
+#: pkg/systemd/timer-dialog.jsx:361 pkg/storaged/lvm2/volume-group.jsx:347
+#: pkg/storaged/lvm2/physical-volume.jsx:88 pkg/storaged/nfs/nfs.jsx:315
+#: pkg/storaged/mdraid/mdraid-disk.jsx:98 pkg/storaged/stratis/pool.jsx:447
+#: pkg/storaged/stratis/pool.jsx:504 pkg/storaged/stratis/pool.jsx:539
+#: pkg/storaged/stratis/pool.jsx:557 pkg/storaged/crypto/keyslots.jsx:613
+#: pkg/storaged/crypto/keyslots.jsx:648 pkg/storaged/crypto/keyslots.jsx:733
+#: pkg/shell/hosts.jsx:170
+msgid "Remove"
+msgstr "删除"
+
+#: pkg/networkmanager/network-interface-members.jsx:110
+msgid "Remove $0"
+msgstr "移除 $0"
+
+#: pkg/networkmanager/firewall.jsx:976
+msgid "Remove $0 service from $1 zone"
+msgstr "从 $1 区中删除 $0 服务"
+
+#: pkg/storaged/stratis/pool.jsx:499 pkg/storaged/crypto/keyslots.jsx:632
+msgid "Remove $0?"
+msgstr "移除 $0?"
+
+#: pkg/storaged/stratis/pool.jsx:497 pkg/storaged/crypto/keyslots.jsx:642
+msgid "Remove Tang keyserver?"
+msgstr "删除 Tang keyserver?"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:228
+msgid "Remove device"
+msgstr "删除设备"
+
+#: pkg/static/login.js:634
+msgid "Remove host"
+msgstr "删除主机"
+
+#: pkg/networkmanager/ip-settings.jsx:226
+#: pkg/networkmanager/ip-settings.jsx:274
+#: pkg/networkmanager/ip-settings.jsx:322
+#: pkg/networkmanager/ip-settings.jsx:394
+msgid "Remove item"
+msgstr "删除条目"
+
+#: pkg/storaged/lvm2/volume-group.jsx:344
+msgid "Remove missing physical volumes?"
+msgstr "删除缺少的物理卷?"
+
+#: pkg/storaged/crypto/keyslots.jsx:606
+msgid "Remove passphrase in key slot $0?"
+msgstr "删除密钥插槽 $0 中的密码短语?"
+
+#: pkg/storaged/stratis/pool.jsx:441
+msgid "Remove passphrase?"
+msgstr "删除密码?"
+
+#: pkg/networkmanager/firewall.jsx:121
+msgid "Remove service $0"
+msgstr "删除服务 $0"
+
+#: pkg/networkmanager/firewall.jsx:182 pkg/networkmanager/firewall.jsx:961
+msgid "Remove zone $0"
+msgstr "移除区 $0"
+
+#: pkg/apps/application.jsx:44
+msgid "Removing"
+msgstr "正在移除"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:191
+#: pkg/storaged/crypto/keyslots.jsx:220
+msgid "Removing $0"
+msgstr "正在删除 $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:109
+msgid ""
+"Removing $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "删除 $0 将断开与服务器的连接,并将导致管理界面不可用。"
+
+#: pkg/storaged/jobs-panel.jsx:66
+msgid "Removing $target from MDRAID device"
+msgstr "从 MDRAID 设备中删除 $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:591
+msgid ""
+"Removing a passphrase without confirmation of another passphrase may prevent "
+"unlocking or key management, if other passphrases are forgotten or lost."
+msgstr ""
+"当删除密码短语而没有确认另外一个密码短语时,如果忘记或丢失了其他密码短语,则"
+"可能无法取消锁定或进行密钥管理。"
+
+#: pkg/storaged/jobs-panel.jsx:79
+msgid "Removing physical volume from $target"
+msgstr "从 $target 删除物理卷"
+
+#: pkg/networkmanager/firewall.jsx:975
+msgid ""
+"Removing the cockpit service might result in the web console becoming "
+"unreachable. Make sure that this zone does not apply to your current web "
+"console connection."
+msgstr ""
+"删除 cockpit 服务可能会导致 web 控制台无法被访问。请确认这个区不会应用到当前"
+"的 web 控制台连接。"
+
+#: pkg/networkmanager/firewall.jsx:960
+msgid "Removing the zone will remove all services within it."
+msgstr "删除区将删除其中的所有服务。"
+
+#: pkg/users/rename-group-dialog.jsx:69
+#: pkg/storaged/lvm2/block-logical-volume.jsx:53
+#: pkg/storaged/lvm2/block-logical-volume.jsx:242
+#: pkg/storaged/lvm2/volume-group.jsx:71
+#: pkg/storaged/stratis/filesystem.jsx:204 pkg/storaged/stratis/pool.jsx:177
+msgid "Rename"
+msgstr "重命名"
+
+#: pkg/storaged/stratis/pool.jsx:168
+msgid "Rename Stratis pool"
+msgstr "重命名 Stratis 池"
+
+#: pkg/storaged/stratis/filesystem.jsx:195
+msgid "Rename filesystem"
+msgstr "重命名文件系统"
+
+#: pkg/users/group-actions.jsx:39
+msgid "Rename group"
+msgstr "重命名组"
+
+#: pkg/users/rename-group-dialog.jsx:60
+msgid "Rename group $0"
+msgstr "重命名组 $0"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:47
+#: pkg/storaged/lvm2/block-logical-volume.jsx:236
+msgid "Rename logical volume"
+msgstr "重命名逻辑卷"
+
+#: pkg/storaged/lvm2/volume-group.jsx:62
+msgid "Rename volume group"
+msgstr "重命名卷组"
+
+#: pkg/storaged/jobs-panel.jsx:82
+msgid "Renaming $target"
+msgstr "重命名 $target"
+
+#: pkg/users/rename-group-dialog.jsx:39
+msgid "Renaming a group may affect sudo and similar rules"
+msgstr "重命名组可能会影响 sudo 和类似的规则"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:137
+#: pkg/storaged/lvm2/block-logical-volume.jsx:188
+msgid "Repair"
+msgstr "修复"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:126
+msgid "Repair logical volume $0"
+msgstr "修复逻辑卷 $0"
+
+#: pkg/storaged/jobs-panel.jsx:53
+msgid "Repairing $target"
+msgstr "正在修复 $target"
+
+#: pkg/systemd/timer-dialog.jsx:220 pkg/systemd/timer-dialog.jsx:241
+msgid "Repeat"
+msgstr "重复"
+
+#: pkg/systemd/timer-dialog.jsx:335
+msgid "Repeat monthly"
+msgstr "每月重复"
+
+#: pkg/storaged/crypto/keyslots.jsx:426 pkg/storaged/crypto/keyslots.jsx:439
+#: pkg/storaged/crypto/keyslots.jsx:488
+msgid "Repeat passphrase"
+msgstr "重复密码"
+
+#: pkg/systemd/timer-dialog.jsx:316
+msgid "Repeat weekly"
+msgstr "每周重复"
+
+#: pkg/sosreport/sosreport.jsx:501 pkg/systemd/reporting.jsx:392
+msgid "Report"
+msgstr "报告"
+
+#: pkg/sosreport/sosreport.jsx:306
+msgid "Report label"
+msgstr "报告标签"
+
+#: pkg/systemd/reporting.jsx:142
+msgid "Report to ABRT Analytics"
+msgstr "向ABRT Analytics 报告"
+
+#: pkg/systemd/reporting.jsx:373
+msgid "Reported; no links available"
+msgstr "已报告;没有可用的链接"
+
+#: pkg/systemd/reporting.jsx:324
+msgid "Reporting failed"
+msgstr "报告失败"
+
+#: pkg/systemd/reporting.jsx:225
+msgid "Reporting was canceled"
+msgstr "报告已取消"
+
+#: pkg/sosreport/sosreport.jsx:496
+msgid "Reports"
+msgstr "报告"
+
+#: pkg/systemd/reporting.jsx:371
+msgid "Reports:"
+msgstr "报告:"
+
+#: pkg/users/expiration-dialogs.js:175
+msgid "Require password change every $0 days"
+msgstr "要求每 $0 天修改密码"
+
+#: pkg/users/account-details.js:71
+msgid "Require password change on $0"
+msgstr "要求于 $0 修改密码"
+
+#: pkg/users/account-create-dialog.js:119
+msgid "Require password change on first login"
+msgstr "在第一次登录时需要更改密码"
+
+#: pkg/systemd/service-details.jsx:567
+msgid "Required by"
+msgstr "要求的"
+
+#: pkg/systemd/service-details.jsx:485
+msgid "Required by "
+msgstr "要求自 "
+
+#: pkg/systemd/service-details.jsx:562
+msgid "Requires"
+msgstr "要求"
+
+#: pkg/systemd/service-details.jsx:500
+msgid "Requires administration access to edit"
+msgstr "需要管理访问权限进行编辑"
+
+#: pkg/systemd/service-details.jsx:563
+msgid "Requisite"
+msgstr "必要"
+
+#: pkg/systemd/service-details.jsx:568
+msgid "Requisite of"
+msgstr "必备的"
+
+#: pkg/kdump/kdump-view.jsx:554
+msgid ""
+"Reserve memory at boot time by setting a '$0' option on the kernel command "
+"line. For example, append '$1' to $2 in $3 or use your distribution's "
+"kernel argument editor."
+msgstr ""
+"通过在内核命令行中设置 '$0' 选项在引导时保留内存。例如,在 $3 中将 '$1' 附加"
+"到 $2,或使用您使用的发行版本的内核参数编辑器。"
+
+#: pkg/kdump/kdump-view.jsx:610
+msgid "Reserved memory"
+msgstr "保留内存"
+
+#: pkg/systemd/logs.jsx:405 pkg/systemd/terminal.jsx:183
+msgid "Reset"
+msgstr "重置"
+
+#: pkg/users/password-dialogs.js:278
+msgid "Reset password"
+msgstr "重置密码"
+
+#: pkg/storaged/jobs-panel.jsx:45 pkg/storaged/jobs-panel.jsx:51
+#: pkg/storaged/jobs-panel.jsx:83
+msgid "Resizing $target"
+msgstr "调整大小 $target"
+
+#: pkg/storaged/block/resize.jsx:463 pkg/storaged/block/resize.jsx:624
+msgid ""
+"Resizing an encrypted filesystem requires unlocking the disk. Please provide "
+"a current disk passphrase."
+msgstr "对一个加密的文件系统调整大小需要解锁磁盘。请提供磁盘的密码。"
+
+#: pkg/systemd/service-details.jsx:136
+msgid "Restart"
+msgstr "重启"
+
+#: pkg/packagekit/updates.jsx:525 pkg/packagekit/updates.jsx:539
+msgid "Restart services"
+msgstr "重启服务"
+
+#: pkg/packagekit/updates.jsx:761 pkg/packagekit/updates.jsx:836
+msgid "Restart services..."
+msgstr "重启服务......"
+
+#: pkg/packagekit/updates.jsx:1573
+msgid "Restarting"
+msgstr "正在重启"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Restoring connection"
+msgstr "恢复连接"
+
+#: pkg/kdump/kdump-view.jsx:362
+msgid ""
+"Results of the crash will be copied through $0 to $1 as $2, if kdump is "
+"properly configured."
+msgstr "如果正确配置了 kdump,则崩溃结果将通过 $0 复制到 $1 作为 $2。"
+
+#: pkg/kdump/kdump-view.jsx:357
+msgid ""
+"Results of the crash will be stored in $0 as $1, if kdump is properly "
+"configured."
+msgstr "如果 kdump 配置正确,崩溃的结果将作为 $1 存储在 $0 中。"
+
+#: pkg/systemd/logs.jsx:263
+msgid "Resume"
+msgstr "恢复"
+
+#: pkg/storaged/block/format-dialog.jsx:255
+msgid "Reuse existing encryption"
+msgstr "重用现有的加密"
+
+#: pkg/storaged/block/format-dialog.jsx:252
+msgid "Reuse existing encryption ($0)"
+msgstr "重用现有的加密($0)"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:236
+msgid "Review cryptographic policy"
+msgstr "检查加密策略"
+
+#: pkg/systemd/manifest.json:0
+msgid "Reviewing logs"
+msgstr "回顾日志"
+
+#: pkg/networkmanager/bond.jsx:43 pkg/networkmanager/team.jsx:41
+msgid "Round robin"
+msgstr "轮循"
+
+#: pkg/networkmanager/ip-settings.jsx:333
+msgid "Routes"
+msgstr "路由"
+
+#: pkg/systemd/timer-dialog.jsx:252 pkg/systemd/timer-dialog.jsx:264
+msgid "Run at"
+msgstr "运行于"
+
+#: pkg/sosreport/sosreport.jsx:293
+msgid "Run new report"
+msgstr "运行新报告"
+
+#: pkg/systemd/timer-dialog.jsx:266
+msgid "Run on"
+msgstr "运行于"
+
+#: pkg/sosreport/sosreport.jsx:271 pkg/sosreport/sosreport.jsx:493
+msgid "Run report"
+msgstr "运行报告"
+
+#: pkg/shell/hosts_dialog.jsx:492
+msgid ""
+"Run this command over a trusted network or physically on the remote machine:"
+msgstr "通过一个可信网络运行这个命令,或在远程机器上运行这个命令:"
+
+#: pkg/networkmanager/team.jsx:162
+msgid "Runner"
+msgstr "运行者"
+
+#: pkg/systemd/services.jsx:232 pkg/systemd/services.jsx:236
+#: pkg/systemd/services.jsx:696 pkg/systemd/service-details.jsx:464
+#: pkg/storaged/mdraid/mdraid.jsx:290
+msgid "Running"
+msgstr "运行中"
+
+#: pkg/storaged/dialog.jsx:1328 pkg/storaged/dialog.jsx:1348
+msgid "Runtime"
+msgstr "运行时"
+
+#: pkg/selinux/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:5
+msgid "SELinux"
+msgstr "SELinux"
+
+#: pkg/selinux/setroubleshoot-view.jsx:324
+msgid "SELinux access control errors"
+msgstr "SELinux 访问控制错误"
+
+#: pkg/selinux/setroubleshoot-view.jsx:321
+msgid "SELinux is disabled on the system"
+msgstr "该系统上 SELinux 已禁用"
+
+#: pkg/selinux/setroubleshoot-view.jsx:246
+msgid "SELinux is disabled on the system."
+msgstr "该系统上 SELinux 已禁用。"
+
+#: pkg/selinux/setroubleshoot-view.jsx:260
+msgid "SELinux policy"
+msgstr "SELinux 策略"
+
+#: pkg/selinux/setroubleshoot-view.jsx:238
+msgid "SELinux system status is unknown."
+msgstr "SELinux 系统状态未知。"
+
+#: pkg/selinux/index.html:23
+msgid "SELinux troubleshoot"
+msgstr "SELinux 排错"
+
+#: pkg/storaged/crypto/tang.jsx:130
+msgid "SHA-1"
+msgstr "SHA-1"
+
+#: pkg/storaged/crypto/tang.jsx:127
+msgid "SHA-256"
+msgstr "SHA-256"
+
+#: pkg/storaged/jobs-panel.jsx:40
+msgid "SMART self-test of $target"
+msgstr "$target SMART 自检"
+
+#: pkg/sosreport/sosreport.jsx:302
+msgid ""
+"SOS reporting collects system information to help with diagnosing problems."
+msgstr "SOS 报告会收集系统信息,以帮助诊断问题。"
+
+#: pkg/kdump/kdump-view.jsx:295 pkg/shell/hosts_dialog.jsx:850
+msgid "SSH key"
+msgstr "SSH 密钥"
+
+#: pkg/kdump/kdump-view.jsx:164
+msgid "SSH key isn't a path"
+msgstr "SSH 密钥不是路径"
+
+#: pkg/shell/credentials.jsx:79 pkg/shell/credentials.jsx:95
+#: pkg/shell/topnav.jsx:218
+msgid "SSH keys"
+msgstr "SSH 密钥"
+
+#: pkg/networkmanager/bridge.jsx:110
+msgid "STP forward delay"
+msgstr "STP 转发延迟"
+
+#: pkg/networkmanager/bridge.jsx:113
+msgid "STP hello time"
+msgstr "STP Hello 时间"
+
+#: pkg/networkmanager/bridge.jsx:116
+msgid "STP maximum message age"
+msgstr "STP 最大消息有效期"
+
+#: pkg/networkmanager/bridge.jsx:107
+msgid "STP priority"
+msgstr "STP 优先级"
+
+#: pkg/shell/failures.jsx:46
+msgid ""
+"Safari users need to import and trust the certificate of the self-signing CA:"
+msgstr "Safari 用户需要导入并信任自签名 CA 证书:"
+
+#: pkg/systemd/timer-dialog.jsx:322 pkg/packagekit/autoupdates.jsx:314
+msgid "Saturdays"
+msgstr "周六"
+
+#: pkg/networkmanager/dialogs-common.jsx:160 pkg/systemd/timer-dialog.jsx:148
+#: pkg/packagekit/kpatch.jsx:307 pkg/storaged/filesystem/filesystem.jsx:126
+#: pkg/storaged/filesystem/mounting-dialog.jsx:279 pkg/storaged/nfs/nfs.jsx:188
+#: pkg/storaged/stratis/pool.jsx:388 pkg/storaged/stratis/pool.jsx:417
+#: pkg/storaged/stratis/pool.jsx:472 pkg/storaged/btrfs/volume.jsx:106
+#: pkg/storaged/crypto/encryption.jsx:168
+#: pkg/storaged/crypto/encryption.jsx:195 pkg/storaged/crypto/keyslots.jsx:495
+#: pkg/storaged/crypto/keyslots.jsx:514
+#: pkg/storaged/partitions/partition.jsx:189 pkg/metrics/metrics.jsx:1478
+msgid "Save"
+msgstr "保存"
+
+#: pkg/systemd/hwinfo.jsx:228
+msgid "Save and reboot"
+msgstr "保存并重启"
+
+#: pkg/kdump/kdump-view.jsx:229 pkg/systemd/overview-cards/motdCard.jsx:61
+#: pkg/packagekit/autoupdates.jsx:269
+msgid "Save changes"
+msgstr "保存更改"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:230
+msgid "Save space by compressing individual blocks with LZ4"
+msgstr "使用 LZ4 压缩独立块以节省空间"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:235
+msgid "Save space by storing identical data blocks just once"
+msgstr "通过对相同数据块只保存一次来节省空间"
+
+#: pkg/storaged/crypto/keyslots.jsx:399 pkg/storaged/crypto/keyslots.jsx:454
+#: pkg/storaged/crypto/keyslots.jsx:512 pkg/storaged/crypto/keyslots.jsx:540
+msgid ""
+"Saving a new passphrase requires unlocking the disk. Please provide a "
+"current disk passphrase."
+msgstr "保存新密码需要解锁磁盘。请提供当前的磁盘密码。"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:89
+msgid "Scheduled poweroff at $0"
+msgstr "计划在 $0 关闭电源"
+
+#: pkg/systemd/overview-cards/shutdownStatus.jsx:93
+msgid "Scheduled reboot at $0"
+msgstr "计划在 $0 重启"
+
+#: pkg/lib/machine-info.js:83
+msgid "Sealed-case PC"
+msgstr "密封式 PC"
+
+#: pkg/systemd/logs.jsx:406 pkg/shell/nav.jsx:139
+msgid "Search"
+msgstr "搜索"
+
+#: pkg/networkmanager/ip-settings.jsx:310
+msgid "Search domain"
+msgstr "搜索域"
+
+#: pkg/users/accounts-list.js:296
+msgid "Search for name or ID"
+msgstr "搜索名称或 ID"
+
+#: pkg/users/accounts-list.js:433
+msgid "Search for name, group or ID"
+msgstr "搜索用户名、组名或ID"
+
+#: pkg/systemd/timer-dialog.jsx:280
+msgid "Second needs to be a number between 0-59"
+msgstr "秒需要是 0-59 之间的数字"
+
+#: pkg/systemd/timer-dialog.jsx:210
+msgid "Seconds"
+msgstr "秒"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:92
+msgid "Secure shell keys"
+msgstr "安全 shell 密钥"
+
+#: pkg/storaged/jobs-panel.jsx:61
+msgid "Securely erasing $target"
+msgstr "安全擦除 $target"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:6
+msgid "Security Enhanced Linux configuration and troubleshooting"
+msgstr "Security Enhanced Linux 配置和故障排除"
+
+#: pkg/packagekit/updates.jsx:1435
+msgid "Security updates available"
+msgstr "可用的错误修复更新"
+
+#: pkg/packagekit/autoupdates.jsx:289
+msgid "Security updates only"
+msgstr "只包括安全更新"
+
+#: pkg/packagekit/autoupdates.jsx:365
+msgid "Security updates will be applied $0 at $1"
+msgstr "将在 $1 的 $0 处应用安全更新"
+
+#: pkg/shell/shell-modals.jsx:117
+msgid "Select"
+msgstr "选择"
+
+#: pkg/systemd/logs.jsx:321
+msgid "Select a identifier"
+msgstr "选择标识符"
+
+#: pkg/networkmanager/ip-settings.jsx:174
+msgid "Select method"
+msgstr "选择方法"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:127
+msgid ""
+"Select the physical volumes that should be used to repair the logical "
+"volume. At leat $0 are needed."
+msgstr "选择应该用来修复逻辑卷的物理卷。最少需要 $0。"
+
+#: pkg/systemd/reporting.jsx:265
+msgid "Send"
+msgstr "发送"
+
+#: pkg/networkmanager/network-main.jsx:187
+#: pkg/networkmanager/network-main.jsx:202
+#: pkg/networkmanager/network-interface-members.jsx:204
+msgid "Sending"
+msgstr "发送"
+
+#: pkg/storaged/drive/drive.jsx:123
+msgid "Serial number"
+msgstr "序列号"
+
+#: pkg/static/login.html:159 pkg/networkmanager/ip-settings.jsx:262
+#: pkg/kdump/kdump-view.jsx:267 pkg/kdump/kdump-view.jsx:289
+#: pkg/storaged/nfs/nfs.jsx:328
+msgid "Server"
+msgstr "服务器"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:31 pkg/storaged/nfs/nfs.jsx:136
+msgid "Server address"
+msgstr "服务器地址"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:32
+msgid "Server address cannot be empty."
+msgstr "服务器地址不能为空。"
+
+#: pkg/storaged/nfs/nfs.jsx:141
+msgid "Server cannot be empty."
+msgstr "名称不能为空。"
+
+#: pkg/lib/cockpit.js:3877
+msgid "Server has closed the connection."
+msgstr "服务器关闭了连接。"
+
+#: pkg/systemd/overview-cards/realmd.jsx:298
+msgid "Server software"
+msgstr "服务器软件"
+
+#: pkg/networkmanager/firewall.jsx:211 pkg/storaged/dialog.jsx:1345
+#: pkg/metrics/metrics.jsx:812 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:868 pkg/metrics/metrics.jsx:906
+#: pkg/metrics/metrics.jsx:966 pkg/metrics/metrics.jsx:972
+msgid "Service"
+msgstr "服务"
+
+#: pkg/kdump/kdump-view.jsx:563
+msgid "Service has an error"
+msgstr "服务有一个错误"
+
+#: pkg/systemd/service.jsx:166
+msgid "Service logs"
+msgstr "服务日志"
+
+#: pkg/systemd/services.html:4 pkg/networkmanager/firewall.jsx:632
+#: pkg/systemd/service-tabs.jsx:40 pkg/systemd/service.jsx:151
+#: pkg/systemd/manifest.json:0
+msgid "Services"
+msgstr "服务"
+
+#: pkg/storaged/dialog.jsx:1153
+msgid "Services using the location"
+msgstr "使用位置的服务"
+
+#: pkg/shell/topnav.jsx:273
+msgid "Session"
+msgstr "会话"
+
+#: pkg/shell/shell-modals.jsx:177
+msgid "Session is about to expire"
+msgstr "会话即将到期"
+
+#: pkg/shell/hosts_dialog.jsx:252
+msgid "Set"
+msgstr "设置"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+msgid "Set hostname"
+msgstr "设置主机名"
+
+#: pkg/storaged/partitions/partition.jsx:173
+msgid "Set partition type of $0"
+msgstr "设置分区类型 $0"
+
+#: pkg/users/password-dialogs.js:226 pkg/users/password-dialogs.js:233
+#: pkg/users/account-details.js:301
+msgid "Set password"
+msgstr "设置密码"
+
+#: pkg/lib/serverTime.js:588
+msgid "Set time"
+msgstr "设置时间"
+
+#: pkg/networkmanager/mtu.jsx:87
+msgid "Set to"
+msgstr "设置为"
+
+#: pkg/packagekit/updates.jsx:113
+msgid "Set up"
+msgstr "设置"
+
+#: pkg/users/password-dialogs.js:245
+msgid "Set weak password"
+msgstr "设置弱密码"
+
+#: pkg/selinux/setroubleshoot-view.jsx:255
+msgid ""
+"Setting deviates from the configured state and will revert on the next boot."
+msgstr "设置从配置的状态偏离并将在下一次重启恢复。"
+
+#: pkg/packagekit/updates.jsx:107
+msgid "Setting up"
+msgstr "设置"
+
+#: pkg/storaged/jobs-panel.jsx:56
+msgid "Setting up loop device $target"
+msgstr "创建 loop 设备 $target"
+
+#: pkg/packagekit/updates.jsx:919
+msgid "Settings"
+msgstr "设置"
+
+#: pkg/packagekit/updates.jsx:375 pkg/packagekit/updates.jsx:450
+msgid "Severity"
+msgstr "严重性"
+
+#: pkg/networkmanager/ip-settings.jsx:45
+msgid "Shared"
+msgstr "共享"
+
+#: pkg/users/account-create-dialog.js:93 pkg/users/account-details.js:329
+msgid "Shell"
+msgstr "Shell"
+
+#: pkg/lib/cockpit-components-modifications.jsx:100
+msgid "Shell script"
+msgstr "Shell 脚本"
+
+#: pkg/lib/cockpit-components-terminal.jsx:216
+msgid "Shift+Insert"
+msgstr "Shift+Insert"
+
+#: pkg/storaged/pages.jsx:693
+msgid "Show all $0 rows"
+msgstr "显示所有 $0 行"
+
+#: pkg/systemd/abrtLog.jsx:264
+msgid "Show all threads"
+msgstr "显示所有线程"
+
+#: pkg/lib/cockpit-components-password.jsx:179
+msgid "Show confirmation password"
+msgstr "显示确认密码"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:96
+msgid "Show fingerprints"
+msgstr "显示指纹"
+
+#: pkg/systemd/logs.jsx:379
+msgid "Show messages containing given string."
+msgstr "显示包含给定字符串的消息."
+
+#: pkg/systemd/logs.jsx:368
+msgid "Show messages for the specified systemd unit."
+msgstr "显示指定 systemd 单元的消息。"
+
+#: pkg/systemd/logs.jsx:357
+msgid "Show messages from a specific boot."
+msgstr "显示来自特定引导的消息."
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show more relationships"
+msgstr "显示更多关系"
+
+#: pkg/lib/cockpit-components-password.jsx:139
+msgid "Show password"
+msgstr "显示密码"
+
+#: pkg/systemd/service-details.jsx:686
+msgid "Show relationships"
+msgstr "显示关系"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:205
+#: pkg/storaged/block/resize.jsx:635 pkg/storaged/partitions/partition.jsx:93
+msgid "Shrink"
+msgstr "缩小"
+
+#: pkg/storaged/block/resize.jsx:551
+msgid "Shrink logical volume"
+msgstr "缩小逻辑卷"
+
+#: pkg/storaged/block/resize.jsx:557 pkg/storaged/partitions/partition.jsx:210
+msgid "Shrink partition"
+msgstr "缩小分区"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:280
+msgid "Shrink volume"
+msgstr "缩小卷"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:190
+#: pkg/lib/cockpit-components-shutdown.jsx:192
+msgid "Shut down"
+msgstr "关机"
+
+#: pkg/systemd/overview.jsx:114
+msgid "Shutdown"
+msgstr "关机"
+
+#: pkg/systemd/logs.jsx:334
+msgid "Since"
+msgstr "自从"
+
+#: pkg/lib/machine-info.js:238 pkg/systemd/hw-detect.js:90
+msgid "Single rank"
+msgstr "单 rank"
+
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/lvm2/block-logical-volume.jsx:291
+#: pkg/storaged/lvm2/vdo-pool.jsx:79
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:199
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:206
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:49
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:143
+#: pkg/storaged/pages.jsx:712 pkg/storaged/block/format-dialog.jsx:361
+#: pkg/storaged/block/resize.jsx:448 pkg/storaged/block/resize.jsx:581
+#: pkg/storaged/nfs/nfs.jsx:330 pkg/storaged/stratis/pool.jsx:97
+#: pkg/storaged/partitions/partition.jsx:227
+msgid "Size"
+msgstr "大小"
+
+#: pkg/storaged/dialog.jsx:1070
+msgid "Size cannot be negative"
+msgstr "大小不能为负数"
+
+#: pkg/storaged/dialog.jsx:1068
+msgid "Size cannot be zero"
+msgstr "大小不能为零"
+
+#: pkg/storaged/dialog.jsx:1072
+msgid "Size is too large"
+msgstr "大小太大"
+
+#: pkg/storaged/dialog.jsx:1066
+msgid "Size must be a number"
+msgstr "大小必须是一个数字"
+
+#: pkg/storaged/dialog.jsx:1074
+msgid "Size must be at least $0"
+msgstr "大小必须最小为 $0"
+
+#: pkg/shell/index.html:19
+msgid "Skip main navigation"
+msgstr "跳过主导航"
+
+#: pkg/shell/index.html:18
+msgid "Skip to content"
+msgstr "跳至内容"
+
+#: pkg/systemd/hwinfo.jsx:279
+msgid "Slot"
+msgstr "插槽"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:80 pkg/storaged/crypto/keyslots.jsx:721
+msgid "Slot $0"
+msgstr "插槽 $0"
+
+#: pkg/storaged/stratis/filesystem.jsx:178
+msgid "Snapshot"
+msgstr "快照"
+
+#: pkg/systemd/service-tabs.jsx:42
+msgid "Sockets"
+msgstr "套接字"
+
+#: pkg/packagekit/index.html:23 pkg/packagekit/manifest.json:0
+msgid "Software updates"
+msgstr "软件更新"
+
+#: pkg/systemd/hwinfo.jsx:244
+msgid ""
+"Software-based workarounds help prevent CPU security issues. These "
+"mitigations have the side effect of reducing performance. Change these "
+"settings at your own risk."
+msgstr ""
+"基于软件的临时解决方案可以帮助防止 CPU 安全问题。这些缓解方案会对性能有一定影"
+"响。改变这些设置可能会存在风险。"
+
+#: pkg/storaged/drive/drive.jsx:63
+msgid "Solid State Drive"
+msgstr "固态驱动器"
+
+#: pkg/selinux/setroubleshoot-view.jsx:89
+msgid "Solution applied successfully"
+msgstr "方案应用成功"
+
+#: pkg/selinux/setroubleshoot-view.jsx:95
+msgid "Solution failed"
+msgstr "方案失败"
+
+#: pkg/selinux/setroubleshoot-view.jsx:352
+msgid "Solutions"
+msgstr "方案"
+
+#: pkg/storaged/stratis/pool.jsx:334
+msgid ""
+"Some block devices of this pool have grown in size after the pool was "
+"created. The pool can be safely grown to use the newly available space."
+msgstr ""
+"创建池后,该池的某些块设备的大小已增大。 可以安全地扩大池以使用新的可用空间。"
+
+#: pkg/packagekit/updates.jsx:97
+msgid ""
+"Some other program is currently using the package manager, please wait..."
+msgstr "某些其他程序正在使用软件包管理器,请等待..."
+
+#: pkg/packagekit/updates.jsx:737 pkg/packagekit/updates.jsx:844
+#: pkg/packagekit/updates.jsx:1536
+msgid "Some software needs to be restarted manually"
+msgstr "有些软件需要手动重启"
+
+#: pkg/storaged/storage-controls.jsx:88
+msgid "Sorry"
+msgstr "抱歉"
+
+#: pkg/networkmanager/firewall.jsx:829
+msgid "Sorted from least to most trusted"
+msgstr "按最不被信任到最被信任的顺序排序"
+
+#: pkg/lib/machine-info.js:74
+msgid "Space-saving computer"
+msgstr "节省空间的计算机"
+
+#: pkg/networkmanager/network-interface.jsx:498
+msgid "Spanning tree protocol"
+msgstr "生成树协议"
+
+#: pkg/networkmanager/bridge.jsx:105
+msgid "Spanning tree protocol (STP)"
+msgstr "生成树协议 (STP)"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:70
+msgid "Spare"
+msgstr "备用"
+
+#: pkg/lib/cockpit-components-shutdown.jsx:183
+msgid "Specific time"
+msgstr "指定时间"
+
+#: pkg/systemd/hwinfo.jsx:291
+msgid "Speed"
+msgstr "速度"
+
+#: pkg/networkmanager/dialogs-common.jsx:80
+msgid "Stable"
+msgstr "稳定的"
+
+#: pkg/systemd/service-details.jsx:143 pkg/storaged/swap/swap.jsx:98
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:150
+#: pkg/storaged/mdraid/mdraid.jsx:213 pkg/storaged/stratis/stopped-pool.jsx:108
+msgid "Start"
+msgstr "启动"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Start and enable"
+msgstr "开始并启用"
+
+#: pkg/storaged/multipath.jsx:70
+msgid "Start multipath"
+msgstr "启用多路径"
+
+#: pkg/networkmanager/networkmanager.jsx:78 pkg/systemd/service-details.jsx:453
+msgid "Start service"
+msgstr "启动服务"
+
+#: pkg/systemd/logs.jsx:335
+msgid "Start showing entries on or newer than the specified date."
+msgstr "显示指定日期及以后的条目。"
+
+#: pkg/systemd/logs.jsx:346
+msgid "Start showing entries on or older than the specified date."
+msgstr "显示指定日期及以前的条目。"
+
+#: pkg/users/account-logs-panel.jsx:77
+msgid "Started"
+msgstr "启动"
+
+#: pkg/storaged/jobs-panel.jsx:64
+msgid "Starting MDRAID device $target"
+msgstr "启动 MDRAID 设备 $target"
+
+#: pkg/storaged/jobs-panel.jsx:46
+msgid "Starting swapspace $target"
+msgstr "启动交换空间 $target"
+
+#: pkg/systemd/services-list.jsx:40 pkg/systemd/services-list.jsx:46
+#: pkg/systemd/hwinfo.jsx:291 pkg/storaged/mdraid/mdraid.jsx:290
+msgid "State"
+msgstr "状态"
+
+#: pkg/systemd/services.jsx:252 pkg/systemd/services.jsx:688
+#: pkg/systemd/service-details.jsx:482
+msgid "Static"
+msgstr "静态"
+
+#: pkg/networkmanager/network-interface.jsx:265
+#: pkg/systemd/service-details.jsx:654 pkg/packagekit/updates.jsx:908
+msgid "Status"
+msgstr "状态"
+
+#: pkg/lib/machine-info.js:95
+msgid "Stick PC"
+msgstr "PC 棒"
+
+#: pkg/networkmanager/teamport.jsx:89
+msgid "Sticky"
+msgstr "粘性的"
+
+#: pkg/systemd/service-details.jsx:139 pkg/storaged/swap/swap.jsx:95
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:62
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:149
+#: pkg/storaged/mdraid/mdraid.jsx:292
+msgid "Stop"
+msgstr "停止"
+
+#: pkg/systemd/service-details.jsx:533
+msgid "Stop and disable"
+msgstr "停止并禁用"
+
+#: pkg/storaged/nfs/nfs.jsx:268
+msgid "Stop and remove"
+msgstr "停止并删除"
+
+#: pkg/storaged/nfs/nfs.jsx:245
+msgid "Stop and unmount"
+msgstr "停止并卸载"
+
+#: pkg/storaged/mdraid/mdraid.jsx:75
+msgid "Stop device"
+msgstr "停止设备"
+
+#: pkg/shell/hosts.jsx:215
+msgid "Stop editing hosts"
+msgstr "停止编辑主机"
+
+#: pkg/sosreport/sosreport.jsx:275
+msgid "Stop report"
+msgstr "停止报告"
+
+#: pkg/storaged/jobs-panel.jsx:63
+msgid "Stopping MDRAID device $target"
+msgstr "停止 MDRAID 设备 $target"
+
+#: pkg/storaged/jobs-panel.jsx:47
+msgid "Stopping swapspace $target"
+msgstr "停止启动交换空间 $target"
+
+#: pkg/storaged/index.html:23 pkg/storaged/overview/overview.jsx:56
+#: pkg/storaged/overview/overview.jsx:58 pkg/storaged/overview/overview.jsx:191
+#: pkg/storaged/manifest.json:0
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:5
+msgid "Storage"
+msgstr "存储"
+
+#: pkg/storaged/storaged.jsx:72
+msgid "Storage can not be managed on this system."
+msgstr "存储不能在这个系统上管理。"
+
+#: pkg/storaged/logs-panel.jsx:39
+msgid "Storage logs"
+msgstr "存储日志"
+
+#: pkg/storaged/block/format-dialog.jsx:406
+msgid "Store passphrase"
+msgstr "存储密码"
+
+#: pkg/storaged/crypto/encryption.jsx:158
+#: pkg/storaged/crypto/encryption.jsx:160
+#: pkg/storaged/crypto/encryption.jsx:231
+msgid "Stored passphrase"
+msgstr "保存的密码"
+
+#: pkg/storaged/stratis/blockdev.jsx:41
+msgid "Stratis block device"
+msgstr "Stratis 块设备"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:141 pkg/storaged/stratis/pool.jsx:570
+msgid "Stratis block devices"
+msgstr "Stratis 块设备"
+
+#: pkg/storaged/block/resize.jsx:187 pkg/storaged/block/resize.jsx:276
+msgid "Stratis blockdevs can not be made smaller"
+msgstr "Stratis blockdevs 无法变得更小"
+
+#: pkg/storaged/stratis/filesystem.jsx:159
+msgid "Stratis filesystem"
+msgstr "Stratis 文件系统"
+
+#: pkg/storaged/stratis/pool.jsx:300
+msgid "Stratis filesystems"
+msgstr "Stratis 文件系统"
+
+#: pkg/storaged/stratis/pool.jsx:361
+msgid "Stratis filesystems pool"
+msgstr "Stratis 文件系统池"
+
+#: pkg/storaged/stratis/blockdev.jsx:81
+#: pkg/storaged/stratis/stopped-pool.jsx:98 pkg/storaged/stratis/pool.jsx:275
+msgid "Stratis pool"
+msgstr "Stratis 池"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:260
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:72
+msgid "Striped (RAID 0)"
+msgstr "条带 (RAID 0)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:262
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:82
+msgid "Striped and mirrored (RAID 10)"
+msgstr "条带和镜像 (RAID 10)"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:399
+msgid "Stripes"
+msgstr "条带"
+
+#: pkg/lib/cockpit-components-password.jsx:97
+msgid "Strong password"
+msgstr "强密码"
+
+#: pkg/systemd/services.jsx:220
+msgid "Stub"
+msgstr "Stub"
+
+#: pkg/shell/topnav.jsx:184
+msgid "Style"
+msgstr "外观"
+
+#: pkg/lib/machine-info.js:78
+msgid "Sub-Chassis"
+msgstr "子机箱"
+
+#: pkg/lib/machine-info.js:73
+msgid "Sub-Notebook"
+msgstr "子笔记本"
+
+#: pkg/systemd/services.jsx:288
+msgid "Subscribing to systemd signals failed: $0"
+msgstr "订阅 systemd 信号失败:$0"
+
+#: pkg/storaged/btrfs/subvolume.jsx:285
+msgid "Subvolume needs to be mounted"
+msgstr "需要挂载的子卷"
+
+#: pkg/storaged/btrfs/subvolume.jsx:287
+msgid "Subvolume needs to be mounted writable"
+msgstr "子卷需要挂载为可写模式"
+
+#: pkg/systemd/logs.jsx:414
+msgid "Successfully copied to clipboard"
+msgstr "成功复制到剪贴板"
+
+#: pkg/storaged/crypto/tang.jsx:118
+msgid "Successfully copied to clipboard!"
+msgstr "成功复制到剪贴板!"
+
+#: pkg/systemd/timer-dialog.jsx:323 pkg/packagekit/autoupdates.jsx:315
+msgid "Sundays"
+msgstr "周日"
+
+#: pkg/storaged/swap/swap.jsx:88 pkg/metrics/metrics.jsx:130
+#: pkg/metrics/metrics.jsx:725 pkg/metrics/metrics.jsx:1950
+msgid "Swap"
+msgstr "交换空间"
+
+#: pkg/storaged/block/resize.jsx:292
+msgid "Swap can not be resized here"
+msgstr "无法在此处调整交换空间的大小"
+
+#: pkg/metrics/metrics.jsx:129
+msgid "Swap out"
+msgstr "交换出"
+
+#: pkg/networkmanager/network-interface-members.jsx:67
+msgid "Switch of $0"
+msgstr "关闭 $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:87
+#: pkg/networkmanager/network-interface.jsx:163
+msgid "Switch off $0"
+msgstr "关掉 $0"
+
+#: pkg/networkmanager/network-interface-members.jsx:78
+#: pkg/networkmanager/network-interface.jsx:144
+msgid "Switch on $0"
+msgstr "打开 $0"
+
+#: pkg/shell/superuser.jsx:153 pkg/shell/superuser.jsx:201
+msgid "Switch to administrative access"
+msgstr "切换到管理权限"
+
+#: pkg/shell/superuser.jsx:274 pkg/shell/superuser.jsx:345
+msgid "Switch to limited access"
+msgstr "切换到限制权限"
+
+#: pkg/networkmanager/network-interface-members.jsx:86
+#: pkg/networkmanager/network-interface.jsx:162
+msgid ""
+"Switching off $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "关掉 $0 将中断与服务器的连接,并将导致管理界面不可用。"
+
+#: pkg/networkmanager/network-interface-members.jsx:77
+#: pkg/networkmanager/network-interface.jsx:143
+msgid ""
+"Switching on $0 will break the connection to the server, and will make the "
+"administration UI unavailable."
+msgstr "打开 $0 将中断与服务器的连接,并将导致管理界面不可用。"
+
+#: pkg/lib/serverTime.js:448
+msgid "Synchronized"
+msgstr "已同步"
+
+#: pkg/lib/serverTime.js:450
+msgid "Synchronized with $0"
+msgstr "与 $0 同步"
+
+#: pkg/lib/serverTime.js:454
+msgid "Synchronizing"
+msgstr "同步"
+
+#: pkg/storaged/jobs-panel.jsx:71
+msgid "Synchronizing MDRAID device $target"
+msgstr "同步 MDRAID 设备 $target"
+
+#: pkg/systemd/services.jsx:913 pkg/shell/indexes.jsx:343 pkg/shell/nav.jsx:46
+msgid "System"
+msgstr "系统"
+
+#: pkg/sosreport/sosreport.jsx:520
+msgid "System diagnostics"
+msgstr "系统诊断"
+
+#: pkg/systemd/hwinfo.jsx:315
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:106
+msgid "System information"
+msgstr "系统信息"
+
+#: pkg/packagekit/updates.jsx:99
+msgid "System is up to date"
+msgstr "系统已更新到最新"
+
+#: pkg/selinux/setroubleshoot-view.jsx:425
+msgid "System modifications"
+msgstr "系统改变"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:75
+msgid "System time"
+msgstr "系统时间"
+
+#: pkg/systemd/services-list.jsx:50
+msgid "Systemd units"
+msgstr "Systemd 单元"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "TCP"
+msgstr "TCP"
+
+#: pkg/lib/machine-info.js:89
+msgid "Tablet"
+msgstr "平板"
+
+#: pkg/storaged/crypto/keyslots.jsx:429
+msgid "Tang keyserver"
+msgstr "Tang 密钥服务器"
+
+#: pkg/storaged/iscsi/session.jsx:93
+msgid "Target"
+msgstr "目标"
+
+#: pkg/systemd/service-tabs.jsx:41
+msgid "Targets"
+msgstr "目标"
+
+#: pkg/networkmanager/network-interface.jsx:175
+#: pkg/networkmanager/network-interface.jsx:189
+#: pkg/networkmanager/network-interface.jsx:453
+msgid "Team"
+msgstr "组"
+
+#: pkg/networkmanager/network-interface.jsx:483
+msgid "Team port"
+msgstr "组端口"
+
+#: pkg/networkmanager/teamport.jsx:82
+msgid "Team port settings"
+msgstr "组端口设置"
+
+#: pkg/systemd/terminal.html:4 pkg/systemd/manifest.json:0
+msgid "Terminal"
+msgstr "终端"
+
+#: pkg/users/account-details.js:222
+msgid "Terminate session"
+msgstr "终止会话"
+
+#: pkg/kdump/kdump-view.jsx:507 pkg/kdump/kdump-view.jsx:515
+msgid "Test configuration"
+msgstr "测试配置"
+
+#: pkg/kdump/kdump-view.jsx:511
+msgid "Test is only available while the kdump service is running."
+msgstr "测试仅在 kdump 服务已运行时可用。"
+
+#: pkg/kdump/kdump-view.jsx:371
+msgid "Test kdump settings"
+msgstr "测试内核转储设置"
+
+#: pkg/kdump/kdump-view.jsx:374
+msgid ""
+"Test kdump settings by crashing the kernel. This may take a while and the "
+"system might not automatically reboot. Do not purposefully crash the system "
+"while any important task is running."
+msgstr ""
+"通过使内核崩溃来测试 kdump 设置。 这可能需要一段时间,并且系统可能不会自动重"
+"新启动。 当任何重要任务正在运行时,请勿故意使系统崩溃。"
+
+#: pkg/networkmanager/networkmanager.jsx:65
+msgid "Testing connection"
+msgstr "测试连接"
+
+#: pkg/storaged/crypto/keyslots.jsx:240
+msgid "The $0 package is not available from any repository."
+msgstr "所有仓库都不提供 $0。"
+
+#: pkg/storaged/overview/overview.jsx:122
+msgid "The $0 package must be installed to create Stratis pools."
+msgstr "$0 软件包必须安装才能创建 Stratis 池。"
+
+#: pkg/storaged/crypto/keyslots.jsx:235 pkg/storaged/crypto/keyslots.jsx:251
+msgid "The $0 package must be installed."
+msgstr "必须安装 $0 软件包。"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:176
+msgid "The $0 package will be installed to create VDO devices."
+msgstr "$0 软件包必须安装才能创建 VDO 设备。"
+
+#: pkg/shell/hosts_dialog.jsx:159
+msgid "The IP address or hostname cannot contain whitespace."
+msgstr "IP 地址或主机名不能包含空格。"
+
+#: pkg/storaged/mdraid/mdraid.jsx:265
+msgid "The MDRAID device is in a degraded state"
+msgstr "MDRAID 设备处于降级状态"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:87
+msgid "The MDRAID device must be running"
+msgstr "MDRAID 设备必须正在运行"
+
+#: pkg/shell/hosts_dialog.jsx:828
+msgid "The SSH key $0 of $1 on $2 will be added to the $3 file of $4 on $5."
+msgstr "$2 上 $1 的 SSH 密钥 $0 将被添加到 $5 上 $4 的 $3 文件中。"
+
+#: pkg/shell/hosts_dialog.jsx:865
+msgid ""
+"The SSH key $0 will be made available for the remainder of the session and "
+"will be available for login to other hosts as well."
+msgstr "SSH 密钥 $0 将会在剩余的会话中可用,也可以在登录到其他主机时可用。"
+
+#: pkg/shell/hosts_dialog.jsx:773
+msgid ""
+"The SSH key for logging in to $0 is protected by a password, and the host "
+"does not allow logging in with a password. Please provide the password of "
+"the key at $1."
+msgstr ""
+"登录到 $0 的 SSH 密钥是受密码保护的,主机不允许使用密码登录。请在 $1 提供密钥"
+"的密码。"
+
+#: pkg/shell/hosts_dialog.jsx:778
+msgid ""
+"The SSH key for logging in to $0 is protected. You can log in with either "
+"your login password or by providing the password of the key at $1."
+msgstr ""
+"登录到 $0 的 SSH 密钥是受保护的。您可以用您的登录密码或通过在 $1 提供密钥的密"
+"码来登录。"
+
+#: pkg/users/password-dialogs.js:266
+msgid "The account '$0' will be forced to change their password on next login"
+msgstr "账号 '$0' 将在下次登录时被强制变更其密码"
+
+#: pkg/networkmanager/firewall.jsx:860
+msgid "The cockpit service is automatically included"
+msgstr "cockpit 服务被自动包括"
+
+#: pkg/selinux/setroubleshoot-view.jsx:253
+msgid "The configured state is unknown, it might change on the next boot."
+msgstr "配置状态未知,下一次启动时会改变。"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:226
+msgid ""
+"The creation of this VDO device did not finish and the device can't be used."
+msgstr "此 VDO 设备的创建未完成,无法使用该设备。"
+
+#: pkg/storaged/crypto/keyslots.jsx:694
+msgid ""
+"The currently logged in user is not permitted to see information about keys."
+msgstr "当前登录的用户不允许查看有关密钥的信息。"
+
+#: pkg/storaged/block/format-dialog.jsx:416
+msgid ""
+"The disk needs to be unlocked before formatting. Please provide a existing "
+"passphrase."
+msgstr "在格式化之前需要解锁磁盘。请提供现有的密码短语。"
+
+#: pkg/sosreport/sosreport.jsx:368
+msgid "The file $0 will be deleted."
+msgstr "文件 $0 将被删除。"
+
+#: pkg/storaged/filesystem/utils.jsx:191
+msgid "The filesystem has no assigned mount point."
+msgstr "文件系统没有分配挂载点。"
+
+#: pkg/storaged/filesystem/utils.jsx:195
+msgid "The filesystem has no permanent mount point."
+msgstr "文件系统没有永久的挂载点。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:217
+msgid ""
+"The filesystem is configured to be automatically mounted on boot but its "
+"encryption container will not be unlocked at that time."
+msgstr "文件系统被配置为在引导时自动挂载,但它的加密容器不会被解锁。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:209
+msgid ""
+"The filesystem is currently mounted but will not be mounted after the next "
+"boot."
+msgstr "该文件系统当前已被加载,但在下次引导后将不再被挂载。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:201
+msgid ""
+"The filesystem is currently mounted on $0 but will be mounted on $1 on the "
+"next boot."
+msgstr "该文件系统当前已被加载于 $0,但在下次引导后将被挂载到 $1。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:213
+msgid ""
+"The filesystem is currently mounted on $0 but will not be mounted after the "
+"next boot."
+msgstr "该文件系统当前已被加载于 $0,但在下次引导后将不再被挂载。"
+
+#: pkg/storaged/filesystem/mismounting.jsx:205
+msgid ""
+"The filesystem is currently not mounted but will be mounted on the next boot."
+msgstr "该文件系统当前没有被加载,但在下次引导后将被挂载。"
+
+#: pkg/storaged/filesystem/utils.jsx:197
+msgid "The filesystem is not mounted."
+msgstr "文件系统未挂载。"
+
+#: pkg/storaged/filesystem/utils.jsx:200
+msgid ""
+"The filesystem will be unlocked and mounted on the next boot. This might "
+"require inputting a passphrase."
+msgstr "文件系统将被解锁,并在下次引导时被挂载。这可能需要输入密码短语。"
+
+#: pkg/shell/hosts_dialog.jsx:494
+msgid "The fingerprint should match:"
+msgstr "指纹应匹配:"
+
+#: pkg/packagekit/updates.jsx:515
+msgid "The following service will be restarted:"
+msgid_plural "The following services will be restarted:"
+msgstr[0] "以下服务将会被重启:"
+
+#: pkg/users/account-create-dialog.js:172
+msgid "The full name must not contain colons."
+msgstr "全名不能含有冒号。"
+
+#: pkg/users/group-create-dialog.js:83
+msgid "The group ID must be positive integer"
+msgstr "组 ID 必须是正整数"
+
+#: pkg/users/group-create-dialog.js:66
+msgid ""
+"The group name can only consist of letters from a-z, digits, dots, dashes "
+"and underscores"
+msgstr "组名称只能由字母 a-z、数字、点、破折号和下划线组成"
+
+#: pkg/users/account-create-dialog.js:387
+msgid ""
+"The home directory $0 already exists. Its ownership will be changed to the "
+"new user."
+msgstr "主目录 $0 已存在。其所有权将更改为新用户。"
+
+#: pkg/storaged/crypto/keyslots.jsx:272
+msgid "The initrd must be regenerated."
+msgstr "必须重新生成 initrd。"
+
+#: pkg/shell/hosts_dialog.jsx:695
+msgid "The key password can not be empty"
+msgstr "密钥密码不能为空"
+
+#: pkg/shell/hosts_dialog.jsx:698 pkg/shell/hosts_dialog.jsx:704
+msgid "The key passwords do not match"
+msgstr "密钥密码不匹配"
+
+#: pkg/users/authorized-keys.js:112
+msgid "The key you provided was not valid."
+msgstr "您提供的 key 是无效的."
+
+#: pkg/storaged/crypto/keyslots.jsx:734
+msgid "The last key slot can not be removed"
+msgstr "无法删除最后一个密钥槽"
+
+#: pkg/storaged/dialog.jsx:1363
+msgid "The listed processes and services will be forcefully stopped."
+msgstr "列出的进程和服务将被强制停止。"
+
+#: pkg/storaged/dialog.jsx:1365
+msgid "The listed processes will be forcefully stopped."
+msgstr "列出的进程将被强制停止。"
+
+#: pkg/storaged/dialog.jsx:1367
+msgid "The listed services will be forcefully stopped."
+msgstr "列出的服务将被强制重启。"
+
+#: pkg/lib/cockpit-components-modifications.jsx:129
+msgid "The logged in user is not permitted to view system modifications"
+msgstr "登陆的用户没有权限查看系统改变"
+
+#: pkg/shell/indexes.jsx:459
+msgid "The machine is rebooting"
+msgstr "正在重启机器"
+
+#: pkg/storaged/dialog.jsx:1321
+msgid "The mount point $0 is in use by these processes:"
+msgstr "挂载点 $0 正在被这些进程使用:"
+
+#: pkg/storaged/dialog.jsx:1341
+msgid "The mount point $0 is in use by these services:"
+msgstr "挂载点 $0 正在被这些服务使用:"
+
+#: pkg/shell/hosts_dialog.jsx:701
+msgid "The new key password can not be empty"
+msgstr "新的密钥密码不能为空"
+
+#: pkg/shell/hosts_dialog.jsx:679
+msgid "The password can not be empty"
+msgstr "密钥密码不能为空"
+
+#: pkg/users/account-create-dialog.js:208 pkg/users/password-dialogs.js:191
+msgid "The passwords do not match"
+msgstr "密码不匹配"
+
+#: pkg/lib/credentials.js:194
+msgid "The passwords do not match."
+msgstr "密码不匹配。"
+
+#: pkg/static/login.html:89 pkg/shell/hosts_dialog.jsx:479
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email."
+msgstr "结果指纹可以通过公共方法(包括电子邮件)共享。"
+
+#: pkg/shell/hosts_dialog.jsx:484
+msgid ""
+"The resulting fingerprint is fine to share via public methods, including "
+"email. If you are asking someone else to do the verification for you, they "
+"can send the results using any method."
+msgstr ""
+"生成的指纹可以通过公共方法(包括电子邮件)共享。如果您需要其他人为您进行验"
+"证,他们可以使用任何方法发送结果。"
+
+#: pkg/static/login.js:915
+msgid ""
+"The server refused to authenticate '$0' using password authentication, and "
+"no other supported authentication methods are available."
+msgstr ""
+"服务器拒绝使用密码验证的方法来验证 '$0',并且没有其他支持的验证途径可以使用。"
+
+#: pkg/lib/cockpit.js:3861
+msgid "The server refused to authenticate using any supported methods."
+msgstr "服务器拒绝使用任何支持的方式来验证。"
+
+#: pkg/storaged/crypto/keyslots.jsx:389
+msgid ""
+"The system does not currently support unlocking a filesystem with a Tang "
+"keyserver during boot."
+msgstr "系统目前不支持在引导过程中解锁具有 Tang keyserver 的文件系统。"
+
+#: pkg/storaged/crypto/keyslots.jsx:388
+msgid ""
+"The system does not currently support unlocking the root filesystem with a "
+"Tang keyserver."
+msgstr "系统目前不支持解锁具有 Tang keyserver 的根文件系统。"
+
+#: pkg/systemd/hwinfo.jsx:67
+msgid "The user $0 is not permitted to change cpu security mitigations"
+msgstr "用户 $0 没有权限修改 cpu 安全缓解方案设置"
+
+#: pkg/systemd/overview-cards/cryptoPolicies.jsx:65
+msgid "The user $0 is not permitted to change cryptographic policies"
+msgstr "用户 $0 不允许修改加密策略"
+
+#: pkg/kdump/kdump-view.jsx:505
+msgid "The user $0 is not permitted to test crash the kernel"
+msgstr "用户 $0 不允许测试内核崩溃"
+
+#: pkg/users/account-details.js:469
+msgid ""
+"The user must log out and log back in for the new configuration to take "
+"effect."
+msgstr "用户必须退出登录并重新登录才能使新配置生效。"
+
+#: pkg/users/account-create-dialog.js:155
+msgid ""
+"The user name can only consist of letters from a-z, digits, dots, dashes and "
+"underscores."
+msgstr "用户名仅能包含 a-z、数字、点、破折号和下划线的字母。"
+
+#: pkg/static/login.js:228
+msgid ""
+"The web browser configuration prevents Cockpit from running (inaccessible $0)"
+msgstr "浏览器配置阻止 Cockpit 运行 (无法访问 $0)"
+
+#: pkg/shell/active-pages-modal.jsx:106
+msgid "There are currently no active pages"
+msgstr "当前没有激活的页面"
+
+#: pkg/storaged/multipath.jsx:71
+msgid ""
+"There are devices with multiple paths on the system, but the multipath "
+"service is not running."
+msgstr "该系统上有带有多路径的设备,但是多路径服务未运行。"
+
+#: pkg/networkmanager/firewall.jsx:215
+msgid "There are no active services in this zone"
+msgstr "该区中没有任何活跃的服务"
+
+#: pkg/users/authorized-keys-panel.js:107
+msgid "There are no authorized public keys for this account."
+msgstr "没有这个帐户的授权公钥."
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:113
+msgid ""
+"There is not enough space available that could be used for a repair. At "
+"least $0 are needed on physical volumes that are not already used for this "
+"logical volume."
+msgstr ""
+"无法进行修复所需的足够的可用空间。在物理卷上至少需要有还没有被这个逻辑卷使用"
+"的 $0 可用空间。"
+
+#: pkg/storaged/stratis/filesystem.jsx:77
+msgid ""
+"There is not enough space in the pool to make a snapshot of this filesystem. "
+"At least $0 are required but only $1 are available."
+msgstr ""
+"池中没有足够的空间来创建该文件系统的快照。 至少需要 $0,但只有 $1 可用。"
+
+#: pkg/shell/failures.jsx:42
+msgid "There was an unexpected error while connecting to the machine."
+msgstr "连接到机器时发生意外错误。"
+
+#: pkg/storaged/crypto/keyslots.jsx:393
+msgid "These additional steps are necessary:"
+msgstr "这些额外步骤是必需的:"
+
+#: pkg/storaged/dialog.jsx:1235
+msgid "These changes will be made:"
+msgstr "将做出这些更改:"
+
+#: pkg/storaged/utils.js:286
+msgid "Thin logical volume"
+msgstr "稀疏逻辑卷"
+
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:69
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:127
+msgid "Thinly provisioned LVM2 logical volumes"
+msgstr "精简置备的 LVM2 逻辑卷"
+
+#: pkg/storaged/mdraid/mdraid.jsx:277
+msgid ""
+"This MDRAID device has no write-intent bitmap. Such a bitmap can reduce "
+"sychronization times significantly."
+msgstr "这个 MDRAID 设备没有 write-intent 位图。这个位图可以显著减少同步时间。"
+
+#: pkg/storaged/nfs/nfs.jsx:111
+msgid "This NFS mount is in use and only its options can be changed."
+msgstr "此 NFS 挂载正在使用中,只能更改其选项。"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:239
+msgid "This VDO device does not use all of its backing device."
+msgstr "此 VDO 设备不使用其所有后台设备。"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:95
+#: pkg/storaged/block/format-dialog.jsx:293
+msgid "This device can not be used for the installation target."
+msgstr "这个设备不能用于安装目标。"
+
+#: pkg/networkmanager/network-interface.jsx:746
+msgid "This device cannot be managed here."
+msgstr "该设备不能在这里被管理。"
+
+#: pkg/storaged/dialog.jsx:1130
+msgid "This device is currently in use."
+msgstr "该设备正在被使用。"
+
+#: pkg/systemd/timer-dialog.jsx:164 pkg/systemd/timer-dialog.jsx:172
+#: pkg/systemd/timer-dialog.jsx:181
+msgid "This field cannot be empty"
+msgstr "该字段不能为空"
+
+#: pkg/users/delete-group-dialog.js:38
+msgid "This group is the primary group for the following users:"
+msgstr "这个组是以下用户的主组:"
+
+#: pkg/packagekit/autoupdates.jsx:328
+msgid "This host will reboot after updates are installed."
+msgstr "安装更新后此主机将重启。"
+
+#: pkg/sosreport/sosreport.jsx:303
+msgid "This information is stored only on the system."
+msgstr "这些信息只保存在系统中。"
+
+#: pkg/storaged/stratis/pool.jsx:556
+msgid ""
+"This keyserver is the only way to unlock the pool and can not be removed."
+msgstr "该密钥服务器是解锁池的唯一方法并且无法删除。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:312
+msgid ""
+"This logical volume has lost some of its physical volumes and can no longer "
+"be used. You need to delete it and create a new one to take its place."
+msgstr ""
+"这个逻辑卷已丢失其部分物理卷,无法再被使用。您需要删除它并创建一个新卷来代替"
+"它。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:314
+msgid ""
+"This logical volume has lost some of its physical volumes but has not lost "
+"any data yet. You should repair it to restore its original redundancy."
+msgstr ""
+"这个逻辑卷已丢失其部分物理卷,但还没有丢失任何数据。您应该修复它来恢复其原始"
+"的冗余功能。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:316
+msgid ""
+"This logical volume has lost some of its physical volumes but might not have "
+"lost any data yet. You might be able to repair it."
+msgstr ""
+"这个逻辑卷已丢失其部分物理卷,但可能还没有丢失任何数据。您可能可以修复它。"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:275
+msgid "This logical volume is not completely used by its content."
+msgstr "这个逻辑卷没有被它的内容完全使用。"
+
+#: pkg/shell/hosts_dialog.jsx:165
+msgid "This machine has already been added."
+msgstr "该主机已经被添加。"
+
+#: pkg/systemd/overview-cards/realmd.jsx:435
+msgid "This may take a while"
+msgstr "这可能需要一些时间"
+
+#: pkg/storaged/partitions/partition.jsx:204
+msgid "This partition is not completely used by its content."
+msgstr "此分区不会被其内容完全使用。"
+
+#: pkg/storaged/stratis/pool.jsx:538
+msgid ""
+"This passphrase is the only way to unlock the pool and can not be removed."
+msgstr "此密码是解锁矿池的唯一方法,无法删除。"
+
+#: pkg/storaged/stratis/pool.jsx:333
+msgid "This pool does not use all the space on its block devices."
+msgstr "该池不使用其块设备上的所有空间。"
+
+#: pkg/storaged/stratis/pool.jsx:348
+msgid "This pool is in a degraded state."
+msgstr "这个池处于降级状态。"
+
+#: pkg/packagekit/updates.jsx:1373
+msgid "This system is not registered"
+msgstr "该系统未注册"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:87
+msgid "This system is using a custom profile"
+msgstr "该系统正在使用自定义的配置集"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:85
+msgid "This system is using the recommended profile"
+msgstr "该系统正在使用推荐的配置集"
+
+#: src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in:8
+msgid ""
+"This tool configures the SELinux policy and can help with understanding and "
+"resolving policy violations."
+msgstr "这个工具配置 SELinux 策略,帮助理解和解决策略违规。"
+
+#: src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in:8
+msgid "This tool configures the system to write kernel crash dumps to disk."
+msgstr "这个工具配置系统,以将内核崩溃转储写入磁盘。"
+
+#: src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in:9
+msgid ""
+"This tool generates an archive of configuration and diagnostic information "
+"from the running system. The archive may be stored locally or centrally for "
+"recording or tracking purposes or may be sent to technical support "
+"representatives, developers or system administrators to assist with "
+"technical fault-finding and debugging."
+msgstr ""
+"此工具从正在运行的系统中生成配置和诊断信息的存档。出于记录或跟踪目的,存档可"
+"能被存储在本地或集中存储,或者被发送到技术支持代表、开发人员或系统管理员,以"
+"帮助技术故障查找和调试。"
+
+#: src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in:8
+msgid ""
+"This tool manages local storage, such as filesystems, LVM2 volume groups, "
+"and NFS mounts."
+msgstr "此工具管理本地存储,如文件系统、LVM2 卷组和 NFS 挂载。"
+
+#: src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in:8
+msgid ""
+"This tool manages networking such as bonds, bridges, teams, VLANs and "
+"firewalls using NetworkManager and Firewalld. NetworkManager is incompatible "
+"with Ubuntu's default systemd-networkd and Debian's ifupdown scripts."
+msgstr ""
+"此工具使用 NetworkManager 和 Firewalld 管理网络,如绑定、网桥、团队、VLAN 和"
+"防火墙等。NetworkManager 与 Ubuntu 的默认 systemd-networkd 和 Debian 的 "
+"ifupdown 脚本不兼容。"
+
+#: pkg/systemd/service-details.jsx:353
+msgid "This unit is not designed to be enabled explicitly."
+msgstr "该单元没有设计为需要明确启用。"
+
+#: pkg/users/account-create-dialog.js:160
+msgid "This user name already exists"
+msgstr "用户名已存在"
+
+#: pkg/storaged/lvm2/volume-group.jsx:372
+msgid "This volume group is missing some physical volumes."
+msgstr "这个卷组缺少一些物理卷。"
+
+#: pkg/static/login.js:212
+msgid "This web browser is too old to run the Web Console (missing $0)"
+msgstr "这个浏览器太老,无法运行 Web 控制台(缺少 $0)"
+
+#: pkg/systemd/logs.jsx:359
+msgid ""
+"This will add a match for '_BOOT_ID='. If not specified the logs for the "
+"current boot will be shown. If the boot ID is omitted, a positive offset "
+"will look up the boots starting from the beginning of the journal, and an "
+"equal-or-less-than zero offset will look up boots starting from the end of "
+"the journal. Thus, 1 means the first boot found in the journal in "
+"chronological order, 2 the second and so on; while -0 is the last boot, -1 "
+"the boot before last, and so on."
+msgstr ""
+"这将添加一个 '_BOOT_ID=' 匹配项。 如果未指定,则将显示来自当前引导的日志。 在"
+"没有指定 ID 时,正偏移将从日志开始查找启动,而等于或小于零的偏移将从日志末尾"
+"开始查找启动。因此,1 表示日志中第一次引导(按时间顺序排列),2 为第二个,以"
+"此类推;-0 是最后一次引导,-1 是最后一次引导前的一个,以此类推。"
+
+#: pkg/systemd/logs.jsx:370
+msgid ""
+"This will add match for '_SYSTEMD_UNIT=', 'COREDUMP_UNIT=' and 'UNIT=' to "
+"find all possible messages for the given unit. Can contain more units "
+"separated by comma. "
+msgstr ""
+"这将添加 '_SYSTEMD_UNIT='、'COREDUMP_UNIT=' 和 'UNIT=' 匹配项,以查找给定单元"
+"的所有可能消息。可以包含以逗号分隔的多个单元。 "
+
+#: pkg/shell/hosts_dialog.jsx:829
+msgid "This will allow you to log in without password in the future."
+msgstr "这样以后登陆时将不需要密码。"
+
+#: pkg/networkmanager/firewall.jsx:958
+msgid ""
+"This zone contains the cockpit service. Make sure that this zone does not "
+"apply to your current web console connection."
+msgstr ""
+"这个区包括 cockpit 服务。请确认这个区不会应用到您当前的 web 控制台连接。"
+
+#: pkg/systemd/timer-dialog.jsx:320 pkg/packagekit/autoupdates.jsx:312
+msgid "Thursdays"
+msgstr "周四"
+
+#: pkg/storaged/stratis/pool.jsx:194
+msgid "Tier"
+msgstr "层"
+
+#: pkg/systemd/logs.jsx:201 pkg/packagekit/history.jsx:112
+msgid "Time"
+msgstr "时间"
+
+#: pkg/lib/serverTime.js:577
+msgid "Time zone"
+msgstr "时区"
+
+#: pkg/systemd/timer-dialog.jsx:155
+msgid "Timer creation failed"
+msgstr "计时器创建失败"
+
+#: pkg/systemd/service-details.jsx:725
+msgid "Timer deletion failed"
+msgstr "计时器删除失败"
+
+#: pkg/systemd/service-tabs.jsx:43
+msgid "Timers"
+msgstr "计时器"
+
+#: pkg/shell/credentials.jsx:275
+msgid ""
+"Tip: Make your key password match your login password to automatically "
+"authenticate against other systems."
+msgstr "提示:确保您的关键密码匹配登录密码来自动验证其他系统。"
+
+#: pkg/static/login.html:84 pkg/shell/hosts_dialog.jsx:474
+msgid ""
+"To ensure that your connection is not intercepted by a malicious third-"
+"party, please verify the host key fingerprint:"
+msgstr "要确保您的连接没有被恶意第三方截取,请验证主机密钥指纹:"
+
+#: pkg/packagekit/updates.jsx:1376
+msgid ""
+"To get software updates, this system needs to be registered with Red Hat, "
+"either using the Red Hat Customer Portal or a local subscription server."
+msgstr ""
+"为了获取软件更新,该系统需要通过红帽客户门户网站或本地订阅服务器注册到红帽。"
+
+#: pkg/static/login.js:768 pkg/shell/hosts_dialog.jsx:477
+msgid ""
+"To verify a fingerprint, run the following on $0 while physically sitting at "
+"the machine or through a trusted network:"
+msgstr ""
+"要验证指纹,在 $0 上运行以下内容(在实际的物理机器上本地进行,或通过一个可信"
+"任的网络进行):"
+
+#: pkg/metrics/metrics.jsx:1875
+msgid "Today"
+msgstr "今天"
+
+#: pkg/shell/credentials.jsx:102
+msgid "Toggle"
+msgstr "切换"
+
+#: pkg/users/expiration-dialogs.js:49
+#: pkg/lib/cockpit-components-shutdown.jsx:212 pkg/lib/serverTime.js:600
+#: pkg/systemd/timer-dialog.jsx:348
+msgid "Toggle date picker"
+msgstr "切换日期选择器"
+
+#: pkg/systemd/logs.jsx:190 pkg/systemd/services.jsx:815
+msgid "Toggle filters"
+msgstr "切换过滤器"
+
+#: pkg/lib/cockpit.js:3883
+msgid "Too much data"
+msgstr "太多数据"
+
+#: pkg/shell/indexes.jsx:346
+msgid "Tools"
+msgstr "工具"
+
+#: pkg/metrics/metrics.jsx:865
+msgid "Top 5 CPU services"
+msgstr "最顶级的 5 个 CPU 服务"
+
+#: pkg/metrics/metrics.jsx:963
+msgid "Top 5 disk usage services"
+msgstr "前 5 个磁盘使用率服务"
+
+#: pkg/metrics/metrics.jsx:903
+msgid "Top 5 memory services"
+msgstr "最顶级的 5 个内存服务"
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:105
+msgid "Total size: $0"
+msgstr "总大小:$0"
+
+#: pkg/lib/machine-info.js:66
+msgid "Tower"
+msgstr "Tower"
+
+#: pkg/systemd/services.jsx:256
+msgid "Transient"
+msgstr "短暂的"
+
+#: pkg/networkmanager/plots.js:39
+msgid "Transmitting"
+msgstr "传输"
+
+#: pkg/systemd/timer-dialog.jsx:183 pkg/systemd/services-list.jsx:45
+msgid "Trigger"
+msgstr "触发器"
+
+#: pkg/systemd/service-details.jsx:558
+msgid "Triggered by"
+msgstr "触发于"
+
+#: pkg/systemd/service-details.jsx:557
+msgid "Triggers"
+msgstr "触发器"
+
+#: pkg/metrics/metrics.jsx:1836
+msgid "Troubleshoot"
+msgstr "排错"
+
+#: pkg/networkmanager/networkmanager.jsx:84
+msgid "Troubleshoot…"
+msgstr "检修…"
+
+#: pkg/shell/hosts_dialog.jsx:466
+msgid "Trust and add host"
+msgstr "信任并添加主机"
+
+#: pkg/storaged/stratis/utils.jsx:86 pkg/storaged/crypto/keyslots.jsx:542
+msgid "Trust key"
+msgstr "信任密钥"
+
+#: pkg/networkmanager/firewall.jsx:826
+msgid "Trust level"
+msgstr "信任级别"
+
+#: pkg/static/login.html:153
+msgid "Try again"
+msgstr "重试"
+
+#: pkg/lib/serverTime.js:455
+msgid "Trying to synchronize with $0"
+msgstr "正在尝试与 $0 同步"
+
+#: pkg/systemd/timer-dialog.jsx:318 pkg/packagekit/autoupdates.jsx:310
+msgid "Tuesdays"
+msgstr "周二"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:257
+msgid "Tuned has failed to start"
+msgstr "Tuned 启动失败"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:281
+msgid ""
+"Tuned is a service that monitors your system and optimizes the performance "
+"under certain workloads. The core of Tuned are profiles, which tune your "
+"system for different use cases."
+msgstr ""
+"Tuned 是一个监控您的系统并优化某些工作负载性能的服务。Tuned 的核心是配置集"
+"(profile),使用它为不同的用例调整您的系统。"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:79
+msgid "Tuned is not available"
+msgstr "Tuned 不可用"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:81
+msgid "Tuned is not running"
+msgstr "Tuned 未运行"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:83
+msgid "Tuned is off"
+msgstr "Tuned 已关闭"
+
+#: pkg/shell/superuser.jsx:345
+msgid "Turn on administrative access"
+msgstr "开启管理员权限"
+
+#: pkg/systemd/hwinfo.jsx:81 pkg/systemd/hwinfo.jsx:291
+#: pkg/packagekit/autoupdates.jsx:279 pkg/storaged/pages.jsx:710
+#: pkg/storaged/block/unrecognized-data.jsx:52
+#: pkg/storaged/block/format-dialog.jsx:356
+#: pkg/storaged/partitions/partition.jsx:175
+#: pkg/storaged/partitions/partition.jsx:221 pkg/shell/credentials.jsx:199
+msgid "Type"
+msgstr "类型"
+
+#: pkg/storaged/partitions/partition.jsx:165
+msgid "Type can only contain the characters 0 to 9, A to F, and \"-\"."
+msgstr "类型只能包含字符 0 到 9,A 到 F,和 \"-\"。"
+
+#: pkg/storaged/partitions/partition.jsx:168
+msgid "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN."
+msgstr "类型的格式为 NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN。"
+
+#: pkg/storaged/partitions/partition.jsx:152
+msgid "Type must contain exactly two hexadecimal characters (0 to 9, A to F)."
+msgstr "类型必须正好包含两个十六进制字符 (0 到 9,A 到 F)。"
+
+#: pkg/systemd/logs.jsx:402
+msgid "Type to filter"
+msgstr "输入内容来过滤"
+
+#: pkg/networkmanager/firewall.jsx:211
+msgid "UDP"
+msgstr "UDP"
+
+#: pkg/storaged/lvm2/volume-group.jsx:386
+#: pkg/storaged/lvm2/physical-volume.jsx:117 pkg/storaged/mdraid/mdraid.jsx:294
+#: pkg/storaged/stratis/stopped-pool.jsx:127 pkg/storaged/stratis/pool.jsx:523
+#: pkg/storaged/btrfs/device.jsx:81 pkg/storaged/btrfs/volume.jsx:126
+#: pkg/storaged/partitions/partition.jsx:220
+msgid "UUID"
+msgstr "UUID"
+
+#: pkg/selinux/setroubleshoot-view.jsx:114
+msgid "Unable to apply this solution automatically"
+msgstr "无法自动应用该方案"
+
+#: pkg/static/login.js:919
+msgid "Unable to connect to that address"
+msgstr "无法连接至该地址"
+
+#: pkg/shell/hosts_dialog.jsx:361
+msgid "Unable to contact $0."
+msgstr "无法联系 $0 。"
+
+#: pkg/shell/hosts_dialog.jsx:236
+msgid ""
+"Unable to contact the given host $0. Make sure it has ssh running on port "
+"$1, or specify another port in the address."
+msgstr ""
+"无法与指定主机 $0 联系。请确认已经在端口 $1 上运行 SSH,或者在地址中指定另一"
+"个端口。"
+
+#: pkg/selinux/setroubleshoot-view.jsx:60
+#: pkg/selinux/setroubleshoot-view.jsx:183
+msgid "Unable to get alert details."
+msgstr "无法获取警告详情。"
+
+#: pkg/shell/hosts_dialog.jsx:770
+msgid ""
+"Unable to log in to $0 using SSH key authentication. Please provide the "
+"password. You may want to set up your SSH keys for automatic login."
+msgstr ""
+"无法使用 SSH 密钥认证登录到 $0 。请提供密码。您可能需要为自动登录设置 SSH 密"
+"钥。"
+
+#: pkg/shell/hosts_dialog.jsx:768
+msgid ""
+"Unable to log in to $0. The host does not accept password login or any of "
+"your SSH keys."
+msgstr "无法登录到 $0 。主机不接受密码登录,或您的任何 SSH 密钥。"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:73
+msgid "Unable to reach server"
+msgstr "无法到达服务器"
+
+#: pkg/storaged/nfs/nfs.jsx:266
+msgid "Unable to remove mount"
+msgstr "无法删除挂载"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:112
+msgid "Unable to repair logical volume $0"
+msgstr "无法修复逻辑卷 $0"
+
+#: pkg/selinux/setroubleshoot-client.js:145
+msgid "Unable to run fix: $0"
+msgstr "无法运行修复:$0"
+
+#: pkg/kdump/kdump-view.jsx:209
+msgid "Unable to save settings"
+msgstr "无法保存设置"
+
+#: pkg/kdump/kdump-view.jsx:214
+msgid "Unable to save settings: $0"
+msgstr "无法保存设置:$0"
+
+#: pkg/selinux/setroubleshoot-client.js:49
+msgid "Unable to start setroubleshootd"
+msgstr "无法启动 setroubleshootd"
+
+#: pkg/storaged/nfs/nfs.jsx:243
+msgid "Unable to unmount filesystem"
+msgstr "无法卸载文件系统"
+
+#: pkg/playground/translate.html:34 pkg/playground/translate.html:39
+msgid "Unavailable"
+msgstr "不可用"
+
+#: pkg/packagekit/kpatch.jsx:238
+msgid "Unavailable packages"
+msgstr "不可用的软件包"
+
+#: pkg/users/account-details.js:470
+msgid "Undo"
+msgstr "撤消"
+
+#: pkg/storaged/crypto/keyslots.jsx:256
+msgid "Unexpected PackageKit error during installation of $0: $1"
+msgstr "安装 $0 过程中出现意外的 PackageKit 错误:$1"
+
+#: pkg/users/dialog-utils.js:65 pkg/networkmanager/interfaces.js:50
+#: pkg/shell/shell-modals.jsx:191
+msgid "Unexpected error"
+msgstr "意外的错误"
+
+#: pkg/storaged/block/unformatted-data.jsx:31
+msgid "Unformatted data"
+msgstr "未格式化的数据"
+
+#: pkg/systemd/logs.jsx:367 pkg/systemd/services-list.jsx:39
+#: pkg/systemd/services-list.jsx:44
+msgid "Unit"
+msgstr "单元"
+
+#: pkg/networkmanager/interfaces.js:510 pkg/networkmanager/interfaces.js:1035
+#: pkg/networkmanager/network-interface.jsx:199
+#: pkg/networkmanager/network-interface.jsx:201 pkg/lib/machine-info.js:61
+#: pkg/lib/machine-info.js:226 pkg/lib/machine-info.js:234
+#: pkg/lib/machine-info.js:236 pkg/lib/machine-info.js:243
+#: pkg/lib/machine-info.js:245 pkg/lib/machine-info.js:249
+#: pkg/systemd/hw-detect.js:85 pkg/systemd/hw-detect.js:94
+#: pkg/systemd/hw-detect.js:101 pkg/systemd/hw-detect.js:104
+#: pkg/systemd/hw-detect.js:111 pkg/systemd/hw-detect.js:112
+#: pkg/storaged/swap/swap.jsx:116
+msgid "Unknown"
+msgstr "未知"
+
+#: pkg/networkmanager/network-interface.jsx:183
+#: pkg/networkmanager/network-interface.jsx:197
+msgid "Unknown \"$0\""
+msgstr "未知 \"$0\""
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:73
+msgid "Unknown ($0)"
+msgstr "未知 ($0)"
+
+#: pkg/apps/application.jsx:103
+msgid "Unknown application"
+msgstr "未知应用"
+
+#: pkg/networkmanager/network-interface.jsx:287
+msgid "Unknown configuration"
+msgstr "未知配置"
+
+#: pkg/storaged/iscsi/create-dialog.jsx:69
+msgid "Unknown host name"
+msgstr "未知主机名"
+
+#: pkg/networkmanager/firewall.jsx:481
+msgid "Unknown service name"
+msgstr "未知的服务名"
+
+#: pkg/storaged/crypto/keyslots.jsx:758
+msgid "Unknown type"
+msgstr "未知类型"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:61 pkg/storaged/crypto/actions.jsx:40
+#: pkg/storaged/crypto/actions.jsx:45
+#: pkg/storaged/crypto/locked-encrypted-data.jsx:37
+#: pkg/shell/credentials.jsx:312
+msgid "Unlock"
+msgstr "解锁"
+
+#: pkg/storaged/filesystem/mismounting.jsx:219
+msgid "Unlock automatically on boot"
+msgstr "在引导时自动解锁"
+
+#: pkg/storaged/block/resize.jsx:246
+msgid "Unlock before resizing"
+msgstr "在调整大小前解锁"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:50
+msgid "Unlock encrypted Stratis pool"
+msgstr "解锁加密的 Stratis 池"
+
+#: pkg/shell/credentials.jsx:309
+msgid "Unlock key $0"
+msgstr "解锁密钥 $0"
+
+#: pkg/storaged/jobs-panel.jsx:42
+msgid "Unlocking $target"
+msgstr "解锁 $target"
+
+#: pkg/storaged/crypto/keyslots.jsx:186
+msgid "Unlocking disk"
+msgstr "解锁磁盘"
+
+#: pkg/networkmanager/network-main.jsx:195
+#: pkg/networkmanager/network-main.jsx:197
+msgid "Unmanaged interfaces"
+msgstr "未管理的接口"
+
+#: pkg/storaged/filesystem/filesystem.jsx:102
+#: pkg/storaged/filesystem/mounting-dialog.jsx:278 pkg/storaged/nfs/nfs.jsx:309
+#: pkg/storaged/stratis/filesystem.jsx:176 pkg/storaged/btrfs/subvolume.jsx:270
+msgid "Unmount"
+msgstr "卸载"
+
+#: pkg/storaged/filesystem/mounting-dialog.jsx:272
+msgid "Unmount filesystem $0"
+msgstr "卸载文件系统 $0"
+
+#: pkg/storaged/filesystem/mismounting.jsx:211
+#: pkg/storaged/filesystem/mismounting.jsx:215
+msgid "Unmount now"
+msgstr "现在卸载"
+
+#: pkg/storaged/jobs-panel.jsx:49
+msgid "Unmounting $target"
+msgstr "卸载 $target"
+
+#: pkg/users/authorized-keys-panel.js:135
+msgid "Unnamed"
+msgstr "未命名"
+
+#: pkg/systemd/service-details.jsx:184
+msgid "Unpin unit"
+msgstr "未固定单元"
+
+#: pkg/storaged/block/unrecognized-data.jsx:35
+msgid "Unrecognized data"
+msgstr "无法识别的数据"
+
+#: pkg/storaged/block/resize.jsx:306
+msgid "Unrecognized data can not be made smaller here"
+msgstr "未识别的数据无法在此处变得更小"
+
+#: pkg/storaged/block/resize.jsx:195
+msgid "Unrecognized data can not be made smaller here."
+msgstr "这里不能把无法识别的数据变小。"
+
+#: pkg/storaged/lvm2/unsupported-logical-volume.jsx:35
+msgid "Unsupported logical volume"
+msgstr "不支持的逻辑卷"
+
+#: pkg/systemd/logs.jsx:345
+msgid "Until"
+msgstr "直到"
+
+#: pkg/lib/cockpit.js:3863 pkg/lib/cockpit.js:3865
+msgid "Untrusted host"
+msgstr "不可信的主机"
+
+#: pkg/shell/hosts_dialog.jsx:357
+msgid "Update"
+msgstr "更新"
+
+#: pkg/packagekit/updates.jsx:754
+msgid "Update Success Table"
+msgstr "更新成功表"
+
+#: pkg/packagekit/updates.jsx:957
+msgid "Update history"
+msgstr "更新历史"
+
+#: pkg/apps/application-list.jsx:163
+msgid "Update package information"
+msgstr "更新软件包信息"
+
+#: pkg/packagekit/updates.jsx:679 pkg/packagekit/updates.jsx:749
+msgid "Update was successful"
+msgstr "更新成功"
+
+#: pkg/packagekit/updates.jsx:112
+msgid "Updated"
+msgstr "已更新"
+
+#: pkg/packagekit/updates.jsx:682
+msgid "Updated packages may require a reboot to take effect."
+msgstr "已更新的软件包可能需要重启生效。"
+
+#: pkg/packagekit/updates.jsx:1441
+msgid "Updates available"
+msgstr "可用的更新"
+
+#: pkg/packagekit/history.jsx:109
+msgid "Updates history"
+msgstr "更新历史"
+
+#: pkg/packagekit/autoupdates.jsx:366
+msgid "Updates will be applied $0 at $1"
+msgstr "将在 $1 的 $0 处应用更新"
+
+#: pkg/packagekit/updates.jsx:106
+msgid "Updating"
+msgstr "更新"
+
+#: pkg/systemd/service-details.jsx:528
+msgid "Updating status..."
+msgstr "更新状态 ..."
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:129
+msgid "Uptime"
+msgstr "运行时间"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:127
+#: pkg/storaged/lvm2/physical-volume.jsx:119
+#: pkg/storaged/filesystem/filesystem.jsx:152
+#: pkg/storaged/block/unrecognized-data.jsx:51
+#: pkg/storaged/stratis/filesystem.jsx:230 pkg/storaged/stratis/pool.jsx:525
+#: pkg/storaged/btrfs/device.jsx:83 pkg/storaged/btrfs/volume.jsx:128
+#: pkg/metrics/metrics.jsx:1949 pkg/metrics/metrics.jsx:1950
+#: pkg/metrics/metrics.jsx:1951 pkg/metrics/metrics.jsx:1952
+msgid "Usage"
+msgstr "用法"
+
+#: pkg/storaged/storage-controls.jsx:206
+msgid "Usage of $0"
+msgstr "$0 的用法"
+
+#: pkg/networkmanager/dialogs-common.jsx:103 pkg/storaged/dialog.jsx:624
+#: pkg/storaged/dialog.jsx:1135
+msgid "Use"
+msgstr "使用"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:85 pkg/storaged/legacy-vdo/legacy-vdo.jsx:328
+msgid "Use compression"
+msgstr "使用压缩"
+
+#: pkg/storaged/lvm2/vdo-pool.jsx:89 pkg/storaged/legacy-vdo/legacy-vdo.jsx:337
+msgid "Use deduplication"
+msgstr "使用 deduplication"
+
+#: pkg/shell/credentials.jsx:133
+msgid "Use key"
+msgstr "使用密钥"
+
+#: pkg/users/account-create-dialog.js:114
+msgid "Use password"
+msgstr "使用密码"
+
+#: pkg/shell/credentials.jsx:86
+msgid "Use the following keys to authenticate against other systems"
+msgstr "使用以下密钥来验证其他系统"
+
+#: pkg/sosreport/sosreport.jsx:322
+msgid "Use verbose logging"
+msgstr "使用详细日志记录"
+
+#: pkg/storaged/swap/swap.jsx:125 pkg/metrics/metrics.jsx:813
+#: pkg/metrics/metrics.jsx:907
+msgid "Used"
+msgstr "已使用"
+
+#: pkg/storaged/block/format-dialog.jsx:133
+msgid ""
+"Useful for mounts that are optional or need interaction (such as passphrases)"
+msgstr "对于可选或需要交互的挂载(如密码短语)很有用"
+
+#: pkg/systemd/services.jsx:917 pkg/storaged/dialog.jsx:1327
+msgid "User"
+msgstr "用户"
+
+#: pkg/users/account-create-dialog.js:104
+msgid "User ID"
+msgstr "用户 ID"
+
+#: pkg/users/account-create-dialog.js:191
+msgid "User ID is already used by another user"
+msgstr "用户 ID 已被其他用户使用"
+
+#: pkg/users/account-create-dialog.js:181
+msgid "User ID must be a positive integer"
+msgstr "用户 ID 必须是正整数"
+
+#: pkg/users/account-create-dialog.js:187
+msgid "User ID must not be higher than $0"
+msgstr "用户 ID 不得大于 $0"
+
+#: pkg/users/account-create-dialog.js:184
+msgid "User ID must not be lower than $0"
+msgstr "用户 ID 不得小于 $0"
+
+#: pkg/static/login.html:94 pkg/users/account-create-dialog.js:76
+#: pkg/users/account-details.js:262 pkg/shell/hosts_dialog.jsx:264
+msgid "User name"
+msgstr "用户名"
+
+#: pkg/static/login.js:574
+msgid "User name cannot be empty"
+msgstr "用户名不能为空"
+
+#: pkg/users/accounts-list.js:388 pkg/storaged/iscsi/create-dialog.jsx:33
+#: pkg/storaged/iscsi/create-dialog.jsx:138
+msgid "Username"
+msgstr "用户名"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using LUKS encryption"
+msgstr "使用 LUKS 加密"
+
+#: pkg/storaged/manifest.json:0
+msgid "Using Tang server"
+msgstr "使用 Tang 服务器"
+
+#: pkg/storaged/block/resize.jsx:177 pkg/storaged/block/resize.jsx:286
+msgid "VDO backing devices can not be made smaller"
+msgstr "VDO 后台设备不能更小"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:139 pkg/storaged/utils.js:345
+msgid "VDO device $0"
+msgstr "VDO 设备 $0"
+
+#: pkg/storaged/lvm2/create-logical-volume-dialog.jsx:101
+msgid "VDO filesystem volume (compression/deduplication)"
+msgstr "VDO 文件系统卷(压缩/去除重复)"
+
+#: pkg/networkmanager/network-interface.jsx:177
+#: pkg/networkmanager/network-interface.jsx:191
+#: pkg/networkmanager/network-interface.jsx:550
+msgid "VLAN"
+msgstr "VLAN"
+
+#: pkg/networkmanager/vlan.jsx:105
+msgid "VLAN ID"
+msgstr "VLAN ID"
+
+#: pkg/systemd/overview-cards/realmd.jsx:394
+msgid "Validating address"
+msgstr "验证地址"
+
+#: pkg/static/login.html:147
+msgid "Validating authentication token"
+msgstr "正在校验验证口令"
+
+#: pkg/systemd/hwinfo.jsx:278 pkg/storaged/drive/drive.jsx:120
+msgid "Vendor"
+msgstr "厂商"
+
+#: pkg/packagekit/updates.jsx:114
+msgid "Verified"
+msgstr "已验证"
+
+#: pkg/shell/hosts_dialog.jsx:489
+msgid "Verify fingerprint"
+msgstr "验证指纹"
+
+#: pkg/storaged/stratis/utils.jsx:83 pkg/storaged/crypto/keyslots.jsx:538
+msgid "Verify key"
+msgstr "验证密钥"
+
+#: pkg/packagekit/updates.jsx:108
+msgid "Verifying"
+msgstr "正在验证"
+
+#: pkg/systemd/hwinfo.jsx:91 pkg/packagekit/updates.jsx:449
+msgid "Version"
+msgstr "版本"
+
+#: pkg/storaged/jobs-panel.jsx:62
+msgid "Very securely erasing $target"
+msgstr "非常安全地擦除 $target"
+
+#: pkg/metrics/metrics.jsx:766 pkg/metrics/metrics.jsx:767
+msgid "View all CPUs"
+msgstr "查看所有 CPU"
+
+#: pkg/metrics/metrics.jsx:803
+msgid "View all disks"
+msgstr "查看所有磁盘"
+
+#: pkg/lib/cockpit-components-logs-panel.jsx:169
+msgid "View all logs"
+msgstr "查看所有日志"
+
+#: pkg/systemd/service-details.jsx:549
+msgid "View all services"
+msgstr "查看所有服务"
+
+#: pkg/lib/cockpit-components-modifications.jsx:154
+#: pkg/kdump/kdump-view.jsx:526
+msgid "View automation script"
+msgstr "查看自动化脚本"
+
+#: pkg/metrics/metrics.jsx:1147
+msgid "View detailed logs"
+msgstr "查看详细日志"
+
+#: pkg/systemd/overview-cards/systemInformationCard.jsx:139
+msgid "View hardware details"
+msgstr "查看硬件详细信息"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:136
+msgid "View login history"
+msgstr "查看登录历史记录"
+
+#: pkg/storaged/stratis/pool.jsx:351
+msgid "View logs"
+msgstr "查看日志"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:160
+msgid "View metrics and history"
+msgstr "查看指标和历史记录"
+
+#: pkg/metrics/metrics.jsx:804
+msgid "View per-disk throughput"
+msgstr "查看每个磁盘的吞吐量"
+
+#: pkg/apps/application.jsx:71
+msgid "View project website"
+msgstr "查看项目网站"
+
+#: pkg/systemd/reporting.jsx:361
+msgid "View report"
+msgstr "查看报告"
+
+#: pkg/packagekit/updates.jsx:619
+msgid "View update log"
+msgstr "查看更新日志"
+
+#: pkg/systemd/hwinfo.jsx:299
+msgid "Viewing memory information requires administrative access."
+msgstr "查看内存信息需要管理访问权限。"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:117
+#: pkg/lib/cockpit-components-firewalld-request.jsx:154
+msgid "Visit firewall"
+msgstr "访问防火墙"
+
+#: pkg/storaged/lvm2/physical-volume.jsx:108
+msgid "Volume group"
+msgstr "卷组"
+
+#: pkg/storaged/lvm2/volume-group.jsx:223
+#: pkg/storaged/lvm2/physical-volume.jsx:71
+msgid "Volume group is missing physical volumes"
+msgstr "卷组缺少物理卷"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:276
+msgid "Volume size is $0. Content size is $1."
+msgstr "卷大小为 $0。内容大小为 $1。"
+
+#: pkg/networkmanager/interfaces.js:805
+msgid "Waiting"
+msgstr "等待中"
+
+#: pkg/selinux/setroubleshoot-view.jsx:58
+#: pkg/selinux/setroubleshoot-view.jsx:181
+msgid "Waiting for details..."
+msgstr "等待详情..."
+
+#: pkg/systemd/reporting.jsx:240
+msgid "Waiting for input…"
+msgstr "等待输入…"
+
+#: pkg/apps/utils.jsx:56
+msgid "Waiting for other programs to finish using the package manager..."
+msgstr "正在等待其他程序来结束使用软件包管理器..."
+
+#: pkg/lib/cockpit-components-install-dialog.jsx:150
+#: pkg/lib/cockpit-components-install-dialog.jsx:185
+#: pkg/storaged/crypto/keyslots.jsx:214
+msgid "Waiting for other software management operations to finish"
+msgstr "等待其他软件管理操作完成"
+
+#: pkg/systemd/reporting.jsx:120 pkg/systemd/reporting.jsx:331
+msgid "Waiting to start…"
+msgstr "等待开始…"
+
+#: pkg/systemd/service-details.jsx:569
+msgid "Wanted by"
+msgstr "需要于"
+
+#: pkg/systemd/service-details.jsx:564
+msgid "Wants"
+msgstr "需要"
+
+#: pkg/systemd/logs.jsx:69
+msgid "Warning and above"
+msgstr "Warning 及更高级别"
+
+#: pkg/lib/cockpit-components-password.jsx:105
+msgid "Weak password"
+msgstr "弱密码"
+
+#: pkg/shell/shell-modals.jsx:64 pkg/shell/manifest.json:0
+msgid "Web Console"
+msgstr "Web 控制台"
+
+#: src/ws/cockpit.appdata.xml.in:8
+msgid "Web Console for Linux servers"
+msgstr "Linux 服务器的 Web 控制台"
+
+#: pkg/packagekit/updates.jsx:530 pkg/packagekit/updates.jsx:935
+msgid "Web Console will restart"
+msgstr "Web 控制台将重启"
+
+#: pkg/systemd/superuser-alert.jsx:40
+msgid "Web console is running in limited access mode."
+msgstr "Web 控制台正运行于限制访问模式。"
+
+#: pkg/shell/shell-modals.jsx:66
+msgid "Web console logo"
+msgstr "Web 控制台徽标"
+
+#: pkg/systemd/timer-dialog.jsx:319 pkg/packagekit/autoupdates.jsx:311
+msgid "Wednesdays"
+msgstr "周三"
+
+#: pkg/systemd/timer-dialog.jsx:246
+msgid "Weekly"
+msgstr "每周"
+
+#: pkg/systemd/timer-dialog.jsx:213
+msgid "Weeks"
+msgstr "周"
+
+#: pkg/packagekit/autoupdates.jsx:302
+msgid "When"
+msgstr "当"
+
+#: pkg/shell/hosts_dialog.jsx:267
+msgid "When empty, connect with the current user"
+msgstr "如果为空,与当前用户关联"
+
+#: pkg/packagekit/updates.jsx:533 pkg/packagekit/updates.jsx:940
+msgid ""
+"When the Web Console is restarted, you will no longer see progress "
+"information. However, the update process will continue in the background. "
+"Reconnect to continue watching the update process."
+msgstr ""
+"当 Web 控制台重启时,将不再能够看到进度信息。但是,更新过程会继续在后台进行。"
+"重新连接以继续监视更新过程。"
+
+#: pkg/storaged/stratis/create-dialog.jsx:116
+msgid ""
+"When this option is checked, the new pool will not allow overprovisioning. "
+"You need to specify a maximum size for each filesystem that is created in "
+"the pool. Filesystems can not be made larger after creation. Snapshots are "
+"fully allocated on creation. The sum of all maximum sizes can not exceed the "
+"size of the pool. The advantage of this is that filesystems in this pool can "
+"not run out of space in a surprising way. The disadvantage is that you need "
+"to know the maximum size for each filesystem in advance and creation of "
+"snapshots is limited."
+msgstr ""
+"选中此选项后,新池将不允许过度配置。 您需要为池中创建的每个文件系统指定最大大"
+"小。 文件系统创建后无法扩大。 快照在创建时已完全分配。 所有最大大小的总和不能"
+"超过池的大小。 这样做的优点是该池中的文件系统不会以令人惊讶的方式耗尽空间。 "
+"缺点是您需要提前知道每个文件系统的最大大小,并且快照的创建受到限制。"
+
+#: pkg/systemd/terminal.jsx:176
+msgid "White"
+msgstr "白"
+
+#: pkg/networkmanager/wireguard.jsx:247
+msgid "Will be set to \"Automatic\""
+msgstr "将设置为\"Automatic\""
+
+#: pkg/networkmanager/network-interface.jsx:563
+msgid "WireGuard"
+msgstr "WireGuard"
+
+#: pkg/storaged/drive/drive.jsx:124
+msgid "World wide name"
+msgstr "全局范围名称"
+
+#: pkg/metrics/metrics.jsx:785 pkg/metrics/metrics.jsx:926
+#: pkg/metrics/metrics.jsx:968 pkg/metrics/metrics.jsx:972
+msgid "Write"
+msgstr "写入"
+
+#: pkg/storaged/mdraid/mdraid-disk.jsx:71
+msgid "Write-mostly"
+msgstr "主要写"
+
+#: pkg/storaged/plot.jsx:101
+msgid "Writing"
+msgstr "写入中"
+
+#: pkg/static/login.js:929
+msgid "Wrong user name or password"
+msgstr "错误的用户名或密码"
+
+#: pkg/networkmanager/bond.jsx:45
+msgid "XOR"
+msgstr "XOR"
+
+#: pkg/systemd/timer-dialog.jsx:248
+msgid "Yearly"
+msgstr "每年"
+
+#: pkg/networkmanager/network-interface.jsx:241 pkg/systemd/reporting.jsx:281
+msgid "Yes"
+msgstr "是"
+
+#: pkg/static/login.js:765 pkg/shell/hosts_dialog.jsx:488
+msgid "You are connecting to $0 for the first time."
+msgstr "您第一次连接到 $0。"
+
+#: pkg/networkmanager/firewall-switch.jsx:60
+msgid "You are not authorized to modify the firewall."
+msgstr "您无权修改防火墙。"
+
+#: pkg/users/authorized-keys-panel.js:103
+msgid ""
+"You do not have permission to view the authorized public keys for this "
+"account."
+msgstr "您没有权限查看此帐户的授权公钥."
+
+#: pkg/shell/base_index.js:579
+msgid "You have been logged out due to inactivity."
+msgstr "您由于不活动而已注销。"
+
+#: pkg/systemd/logsJournal.jsx:323
+msgid "You may try to load older entries."
+msgstr "您可以尝试加载较早的条目。"
+
+#: pkg/shell/hosts_dialog.jsx:774 pkg/shell/hosts_dialog.jsx:779
+msgid "You may want to change the password of the key for automatic login."
+msgstr "您可能需要更改自动登录的密钥密码。"
+
+#: pkg/users/password-dialogs.js:79
+msgid "You must wait longer to change your password"
+msgstr "您需要等待更长时间来修改您的密码"
+
+#: pkg/metrics/metrics.jsx:1805
+msgid "You need to relogin to be able to see metrics history"
+msgstr "您需要重新登录才能查看指标历史数据"
+
+#: pkg/shell/superuser.jsx:100
+msgid "You now have administrative access."
+msgstr "您已经获取了管理员权限。"
+
+#: pkg/shell/base_index.js:555
+msgid "You will be logged out in $0 seconds."
+msgstr "您将在$0秒钟后登出。"
+
+#: pkg/users/accounts-list.js:185
+msgid "Your account"
+msgstr "您的账户"
+
+#: pkg/lib/cockpit-components-terminal.jsx:233
+msgid ""
+"Your browser does not allow paste from the context menu. You can use "
+"Shift+Insert."
+msgstr "您的浏览器不允许从上下文菜单中进行粘贴,您可以使用 Shift+Insert。"
+
+#: pkg/shell/superuser.jsx:279
+msgid "Your browser will remember your access level across sessions."
+msgstr "您的浏览器将会在不同会话之间记住您的访问级别。"
+
+#: pkg/packagekit/updates.jsx:1576
+msgid ""
+"Your server will close the connection soon. You can reconnect after it has "
+"restarted."
+msgstr "您的服务器将要关闭连接。您可以在其重启后重新连接。"
+
+#: pkg/lib/cockpit.js:3853
+msgid "Your session has been terminated."
+msgstr "会话被终止。"
+
+#: pkg/lib/cockpit.js:3855
+msgid "Your session has expired. Please log in again."
+msgstr "会话超时。请重新登录。"
+
+#: pkg/lib/cockpit-components-firewalld-request.jsx:132
+#: pkg/lib/cockpit-components-firewalld-request.jsx:135
+msgid "Zone"
+msgstr "区域"
+
+#: pkg/lib/journal.js:217
+msgid "[binary data]"
+msgstr "[二进制数据]"
+
+#: pkg/lib/journal.js:211
+msgid "[no data]"
+msgstr "[没有数据]"
+
+#: pkg/systemd/manifest.json:0
+msgid "abrt"
+msgstr "abrt"
+
+#: pkg/users/manifest.json:0
+msgid "access"
+msgstr "访问"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:56
+#: pkg/shell/active-pages-modal.jsx:79
+msgid "active"
+msgstr "激活"
+
+#: pkg/apps/manifest.json:0
+msgid "add-on"
+msgstr "插件"
+
+#: pkg/apps/manifest.json:0
+msgid "addon"
+msgstr "插件"
+
+#: pkg/storaged/filesystem/utils.jsx:177
+msgid "after network"
+msgstr "网络后"
+
+#: pkg/apps/manifest.json:0
+msgid "apps"
+msgstr "应用程序"
+
+#: pkg/packagekit/manifest.json:0
+msgid "apt-get"
+msgstr "apt-get"
+
+#: pkg/systemd/manifest.json:0
+msgid "asset tag"
+msgstr "资产标签"
+
+#: pkg/packagekit/autoupdates.jsx:318
+msgid "at"
+msgstr "在"
+
+#: pkg/selinux/manifest.json:0
+msgid "avc"
+msgstr "avc"
+
+#: pkg/metrics/metrics.jsx:752
+msgid "average: $0%"
+msgstr "平均:$0%"
+
+#: pkg/storaged/dialog.jsx:1112
+msgid "backing device for VDO device"
+msgstr "VDO 设备的备份设备"
+
+#: pkg/systemd/manifest.json:0
+msgid "bash"
+msgstr "bash"
+
+#: pkg/systemd/manifest.json:0
+msgid "bios"
+msgstr "bios"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bond"
+msgstr "绑定"
+
+#: pkg/systemd/manifest.json:0
+msgid "boot"
+msgstr "引导"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "bridge"
+msgstr "网桥"
+
+#: pkg/storaged/btrfs/device.jsx:42 pkg/storaged/btrfs/volume.jsx:136
+msgid "btrfs device"
+msgstr "btrfs 设备"
+
+#: pkg/storaged/btrfs/volume.jsx:133
+msgid "btrfs devices"
+msgstr "btrfs 设备"
+
+#: pkg/storaged/btrfs/subvolume.jsx:311
+msgid "btrfs subvolume"
+msgstr "btrfs 子卷"
+
+#: pkg/storaged/filesystem/utils.jsx:81
+msgid "btrfs subvolume $0 of $1"
+msgstr "btrfs 子卷 $0(共 $1)"
+
+#: pkg/storaged/btrfs/volume.jsx:74 pkg/storaged/btrfs/volume.jsx:148
+msgid "btrfs subvolumes"
+msgstr "btrfs 子卷"
+
+#: pkg/storaged/btrfs/device.jsx:72 pkg/storaged/btrfs/volume.jsx:62
+msgid "btrfs volume"
+msgstr "btrfs 卷"
+
+#: pkg/packagekit/updates.jsx:215 pkg/packagekit/updates.jsx:319
+msgid "bug fix"
+msgstr "程序漏洞修复"
+
+#: pkg/storaged/utils.js:175
+msgctxt "format-bytes"
+msgid "bytes"
+msgstr "字节"
+
+#: pkg/storaged/stratis/blockdev.jsx:57
+msgid "cache"
+msgstr "缓存"
+
+#: pkg/systemd/manifest.json:0
+msgid "cgroups"
+msgstr "用户组"
+
+#: pkg/users/account-details.js:337
+msgid "change"
+msgstr "变更"
+
+#: pkg/metrics/metrics.jsx:654
+msgid "cockpit-podman is not installed"
+msgstr "没有安装 cockpit-podman"
+
+#: pkg/systemd/manifest.json:0
+msgid "command"
+msgstr "命令"
+
+#: pkg/systemd/manifest.json:0
+msgid "console"
+msgstr "控制台"
+
+#: pkg/systemd/manifest.json:0
+msgid "coredump"
+msgstr "核心转储"
+
+#: pkg/systemd/manifest.json:0
+msgid "cpu"
+msgstr "中央处理器"
+
+#: pkg/kdump/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "crash"
+msgstr "崩溃"
+
+#: pkg/storaged/stratis/blockdev.jsx:55
+msgid "data"
+msgstr "数据"
+
+#: pkg/systemd/manifest.json:0
+msgid "date"
+msgstr "日期"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:147
+msgid "deactivate"
+msgstr "取消激活"
+
+#: pkg/systemd/manifest.json:0
+msgid "debug"
+msgstr "故障调试"
+
+#: pkg/storaged/lvm2/block-logical-volume.jsx:64
+#: pkg/storaged/lvm2/volume-group.jsx:81 pkg/storaged/lvm2/volume-group.jsx:331
+#: pkg/storaged/block/format-dialog.jsx:260
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:80 pkg/storaged/mdraid/mdraid.jsx:112
+#: pkg/storaged/stratis/filesystem.jsx:125 pkg/storaged/stratis/pool.jsx:137
+#: pkg/storaged/btrfs/subvolume.jsx:213
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+#: pkg/storaged/partitions/partition.jsx:43
+msgid "delete"
+msgstr "删除"
+
+#: pkg/storaged/dialog.jsx:1115
+msgid "device of btrfs volume"
+msgstr "btrfs 卷的设备"
+
+#: pkg/systemd/manifest.json:0
+msgid "dimm"
+msgstr "dimm"
+
+#: pkg/systemd/manifest.json:0
+msgid "disable"
+msgstr "禁用"
+
+#: pkg/storaged/manifest.json:0
+msgid "disk"
+msgstr "磁盘"
+
+#: pkg/systemd/manifest.json:0
+msgid "disks"
+msgstr "磁盘"
+
+#: pkg/packagekit/manifest.json:0
+msgid "dnf"
+msgstr "dnf"
+
+#: pkg/systemd/manifest.json:0
+msgid "domain"
+msgstr "域"
+
+#: pkg/storaged/manifest.json:0
+msgid "drive"
+msgstr "驱动"
+
+#: pkg/users/account-details.js:291 pkg/users/account-details.js:321
+#: pkg/networkmanager/dialogs-common.jsx:262
+#: pkg/networkmanager/network-interface.jsx:349
+#: pkg/systemd/overview-cards/configurationCard.jsx:55
+#: pkg/storaged/lvm2/block-logical-volume.jsx:288
+#: pkg/storaged/lvm2/volume-group.jsx:384
+#: pkg/storaged/lvm2/thin-pool-logical-volume.jsx:141
+#: pkg/storaged/filesystem/utils.jsx:221
+#: pkg/storaged/filesystem/filesystem.jsx:145
+#: pkg/storaged/stratis/filesystem.jsx:224 pkg/storaged/stratis/pool.jsx:521
+#: pkg/storaged/btrfs/volume.jsx:123 pkg/storaged/crypto/encryption.jsx:233
+#: pkg/storaged/crypto/encryption.jsx:236
+#: pkg/storaged/partitions/partition.jsx:224
+msgid "edit"
+msgstr "编辑"
+
+#: pkg/systemd/manifest.json:0
+msgid "enable"
+msgstr "启用"
+
+#: pkg/storaged/crypto/encryption.jsx:44
+msgid "encrypted"
+msgstr "已加密"
+
+#: pkg/storaged/manifest.json:0
+msgid "encryption"
+msgstr "加密"
+
+#: pkg/packagekit/updates.jsx:217 pkg/packagekit/updates.jsx:319
+msgid "enhancement"
+msgstr "性能强化"
+
+#: pkg/systemd/manifest.json:0
+msgid "error"
+msgstr "错误"
+
+#: pkg/packagekit/autoupdates.jsx:354
+msgid "every Friday"
+msgstr "每周五"
+
+#: pkg/packagekit/autoupdates.jsx:350
+msgid "every Monday"
+msgstr "每周一"
+
+#: pkg/packagekit/autoupdates.jsx:355
+msgid "every Saturday"
+msgstr "每周六"
+
+#: pkg/packagekit/autoupdates.jsx:356
+msgid "every Sunday"
+msgstr "每周日"
+
+#: pkg/packagekit/autoupdates.jsx:353
+msgid "every Thursday"
+msgstr "每周四"
+
+#: pkg/packagekit/autoupdates.jsx:351
+msgid "every Tuesday"
+msgstr "每周二"
+
+#: pkg/packagekit/autoupdates.jsx:352
+msgid "every Wednesday"
+msgstr "没周三"
+
+#: pkg/packagekit/autoupdates.jsx:308 pkg/packagekit/autoupdates.jsx:349
+msgid "every day"
+msgstr "每天"
+
+#: pkg/apps/manifest.json:0
+msgid "extension"
+msgstr "扩展"
+
+#: pkg/systemd/overview-cards/configurationCard.jsx:153
+msgid "failed to list ssh host keys: $0"
+msgstr "列出 SSH 主机密钥失败: $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "filesystem"
+msgstr "文件系统"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewall"
+msgstr "防火墙"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "firewalld"
+msgstr "firewalld"
+
+#: pkg/packagekit/kpatch.jsx:265
+msgid "for current and future kernels"
+msgstr "对于当前和将来的内核"
+
+#: pkg/packagekit/kpatch.jsx:270
+msgid "for current kernel only"
+msgstr "只对于当前的内核"
+
+#: pkg/storaged/block/format-dialog.jsx:260 pkg/storaged/manifest.json:0
+msgid "format"
+msgstr "格式"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:38
+msgctxt "from <host>"
+msgid "from $0"
+msgstr "来自 $0"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:36
+msgctxt "from <host> on <terminal>"
+msgid "from $0 on $1"
+msgstr "来自 $1 上的 $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "fstab"
+msgstr "fstab"
+
+#: pkg/systemd/manifest.json:0
+msgid "graphs"
+msgstr "图表"
+
+#: pkg/storaged/block/resize.jsx:420
+msgid "grow"
+msgstr "增长"
+
+#: pkg/systemd/manifest.json:0
+msgid "hardware"
+msgstr "硬件"
+
+#: pkg/systemd/manifest.json:0
+msgid "history"
+msgstr "历史"
+
+#: pkg/systemd/manifest.json:0
+msgid "host"
+msgstr "主机"
+
+#: pkg/storaged/drive/drive.jsx:65
+msgid "iSCSI Drive"
+msgstr "iSCSI 驱动器"
+
+#: pkg/storaged/iscsi/session.jsx:63 pkg/storaged/iscsi/session.jsx:80
+msgid "iSCSI drives"
+msgstr "iSCSI 驱动器"
+
+#: pkg/storaged/iscsi/session.jsx:45
+msgid "iSCSI portal"
+msgstr "iSCSI 门户"
+
+#: pkg/storaged/filesystem/utils.jsx:179
+msgid "ignore failure"
+msgstr "忽略失败"
+
+#: pkg/shell/shell-modals.jsx:199
+msgid "in most browsers"
+msgstr "在大多数浏览器中"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:57
+msgid "inconsistent"
+msgstr "不一致"
+
+#: pkg/storaged/partitions/format-disk-dialog.jsx:35
+msgid "initialize"
+msgstr "初始化"
+
+#: pkg/apps/manifest.json:0
+msgid "install"
+msgstr "安装"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "interface"
+msgstr "接口"
+
+#: pkg/kdump/kdump-view.jsx:446
+msgid "invalid: multiple targets defined"
+msgstr "无效:定义了多个目标"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv4"
+msgstr "ipv4"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "ipv6"
+msgstr "ipv6"
+
+#: pkg/storaged/manifest.json:0
+msgid "iscsi"
+msgstr "iscsi"
+
+#: pkg/systemd/manifest.json:0
+msgid "journal"
+msgstr "日志"
+
+#: pkg/systemd/logs.jsx:412
+msgid "journalctl manpage"
+msgstr "journalctl 手册页"
+
+#: pkg/kdump/manifest.json:0
+msgid "kdump"
+msgstr "kdump"
+
+#: pkg/kdump/kdump-view.jsx:539
+msgid "kdump status"
+msgstr "kdump 状态"
+
+#: pkg/users/manifest.json:0
+msgid "keys"
+msgstr "密钥"
+
+#: pkg/users/manifest.json:0
+msgid "login"
+msgstr "登录"
+
+#: pkg/storaged/manifest.json:0
+msgid "luks"
+msgstr "luks"
+
+#: pkg/storaged/manifest.json:0
+msgid "lvm2"
+msgstr "lvm2"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "mac"
+msgstr "mac"
+
+#: pkg/systemd/manifest.json:0
+msgid "machine"
+msgstr "机器"
+
+#: pkg/systemd/manifest.json:0
+msgid "mask"
+msgstr "屏蔽"
+
+#: pkg/metrics/metrics.jsx:753
+msgid "max: $0%"
+msgstr "最大:$0%"
+
+#: pkg/storaged/dialog.jsx:1111
+msgid "member of MDRAID device"
+msgstr "MDRAID 设备的成员"
+
+#: pkg/storaged/dialog.jsx:1113
+msgid "member of Stratis pool"
+msgstr "Stratis 池的成员"
+
+#: pkg/systemd/manifest.json:0
+msgid "memory"
+msgstr "内存"
+
+#: pkg/systemd/manifest.json:0
+msgid "metrics"
+msgstr "指标"
+
+#: pkg/systemd/manifest.json:0
+msgid "mitigation"
+msgstr "缓解方案"
+
+#: pkg/storaged/manifest.json:0
+msgid "mkfs"
+msgstr "mkfs"
+
+#: pkg/kdump/kdump-view.jsx:564
+msgid "more details"
+msgstr "更多详情"
+
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "mount"
+msgstr "挂载"
+
+#: pkg/storaged/manifest.json:0
+msgid "nbde"
+msgstr "nbde"
+
+#: pkg/networkmanager/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "network"
+msgstr "网络"
+
+#: pkg/storaged/filesystem/utils.jsx:175
+msgid "never mount at boot"
+msgstr "永远不会在引导时挂载"
+
+#: pkg/storaged/manifest.json:0
+msgid "nfs"
+msgstr "nfs"
+
+#: pkg/kdump/kdump-client.js:130
+msgid "nfs export is empty"
+msgstr "nfs 导出为空"
+
+#: pkg/kdump/kdump-client.js:125
+msgid "nfs server is empty"
+msgstr "nfs 服务器为空"
+
+#: pkg/kdump/kdump-client.js:128
+msgid "nfs server is not valid IPv6"
+msgstr "nfs 服务器不是有效的 IPv6"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "nice"
+msgstr "良好"
+
+#: pkg/systemd/overview-cards/tuned-dialog.jsx:89
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+#: pkg/storaged/stratis/stopped-pool.jsx:134
+#: pkg/storaged/crypto/encryption.jsx:232
+#: pkg/storaged/crypto/encryption.jsx:235
+msgid "none"
+msgstr "空"
+
+#: pkg/systemd/overview-cards/usageCard.jsx:123
+msgid "of $0 CPU"
+msgid_plural "of $0 CPUs"
+msgstr[0] "$0 CPU"
+
+#: pkg/systemd/overview-cards/lastLogin.jsx:40
+msgctxt "on <terminal>"
+msgid "on $0"
+msgstr "在 $0 上"
+
+#: pkg/systemd/manifest.json:0
+msgid "operating system"
+msgstr "操作系统"
+
+#: pkg/systemd/manifest.json:0
+msgid "os"
+msgstr "操作系统"
+
+#: pkg/packagekit/manifest.json:0
+msgid "package"
+msgstr "软件包"
+
+#: pkg/packagekit/manifest.json:0
+msgid "packagekit"
+msgstr "packagekit"
+
+#: pkg/storaged/manifest.json:0
+msgid "partition"
+msgstr "分区"
+
+#: pkg/users/manifest.json:0
+msgid "passwd"
+msgstr "passwd"
+
+#: pkg/users/manifest.json:0
+msgid "password"
+msgstr "密码"
+
+#: pkg/lib/cockpit-components-password.jsx:148
+msgid "password quality"
+msgstr "密码质量"
+
+#: pkg/packagekit/updates.jsx:344
+msgid "patches"
+msgstr "补丁"
+
+#: pkg/systemd/manifest.json:0
+msgid "path"
+msgstr "路径"
+
+#: pkg/systemd/manifest.json:0
+msgid "pci"
+msgstr "pci"
+
+#: pkg/systemd/manifest.json:0
+msgid "pcp"
+msgstr "pcp"
+
+#: pkg/systemd/manifest.json:0
+msgid "performance"
+msgstr "性能"
+
+#: pkg/storaged/dialog.jsx:1110
+msgid "physical volume of LVM2 volume group"
+msgstr "LVM2 卷组的物理卷"
+
+#: pkg/apps/manifest.json:0
+msgid "plugin"
+msgstr "插件"
+
+#: pkg/metrics/metrics.jsx:1833
+msgid "pmlogger.service has failed"
+msgstr "pmlogger.service 失败"
+
+#: pkg/metrics/metrics.jsx:1835
+msgid "pmlogger.service is failing to collect data"
+msgstr "pmlogger.service 收集数据失败"
+
+#: pkg/metrics/metrics.jsx:1826
+msgid "pmlogger.service is not running"
+msgstr "pmlogger.service 没有运行"
+
+#: pkg/metrics/metrics.jsx:648
+msgid "pod"
+msgstr "pod"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "port"
+msgstr "端口"
+
+#: pkg/systemd/manifest.json:0
+msgid "power"
+msgstr "电源"
+
+#: pkg/storaged/manifest.json:0
+msgid "raid"
+msgstr "raid"
+
+#: pkg/systemd/manifest.json:0
+msgid "ram"
+msgstr "ram"
+
+#: pkg/storaged/filesystem/utils.jsx:173
+msgid "read only"
+msgstr "只读"
+
+#: pkg/systemd/overview-cards/profiles-menu-dialog-body.jsx:55
+msgid "recommended"
+msgstr "推荐"
+
+#: pkg/storaged/utils.js:945
+msgid "remove from LVM2"
+msgstr "从 LVM2 中删除"
+
+#: pkg/storaged/utils.js:934
+msgid "remove from MDRAID"
+msgstr "从 MDRAID 中删除"
+
+#: pkg/storaged/utils.js:904
+msgid "remove from btrfs volume"
+msgstr "从 btrfs 卷中删除"
+
+#: pkg/systemd/manifest.json:0
+msgid "restart"
+msgstr "重启"
+
+#: pkg/users/manifest.json:0
+msgid "roles"
+msgstr "角色"
+
+#: pkg/systemd/overview.jsx:159
+msgid "running $0"
+msgstr "运行中 $0"
+
+#: pkg/packagekit/updates.jsx:213 pkg/packagekit/updates.jsx:311
+#: pkg/packagekit/manifest.json:0
+msgid "security"
+msgstr "安全"
+
+#: pkg/selinux/manifest.json:0
+msgid "semanage"
+msgstr "semanage"
+
+#: pkg/systemd/manifest.json:0
+msgid "serial"
+msgstr "序列"
+
+#: pkg/systemd/manifest.json:0
+msgid "service"
+msgstr "服务"
+
+#: pkg/selinux/manifest.json:0
+msgid "setroubleshoot"
+msgstr "setroubleshoot"
+
+#: pkg/systemd/manifest.json:0
+msgid "shell"
+msgstr "shell"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:61
+msgid "show less"
+msgstr "显示更少"
+
+#: pkg/lib/cockpit-components-inline-notification.jsx:59
+msgid "show more"
+msgstr "显示更多"
+
+#: pkg/storaged/block/resize.jsx:566
+msgid "shrink"
+msgstr "缩小"
+
+#: pkg/systemd/manifest.json:0
+msgid "shut"
+msgstr "关闭"
+
+#: pkg/systemd/manifest.json:0
+msgid "socket"
+msgstr "套接口"
+
+#: pkg/selinux/setroubleshoot-view.jsx:123
+#: pkg/selinux/setroubleshoot-view.jsx:144
+msgid "solution"
+msgstr "分辨率"
+
+#: pkg/selinux/setroubleshoot-view.jsx:161
+msgid "solution details"
+msgstr "方案详情"
+
+#: pkg/sosreport/manifest.json:0
+msgid "sos"
+msgstr "sos"
+
+#: pkg/sosreport/sosreport.jsx:183
+msgid "sos report failed"
+msgstr "sos 报告失败"
+
+#: pkg/users/manifest.json:0 pkg/systemd/manifest.json:0
+msgid "ssh"
+msgstr "ssh"
+
+#: pkg/kdump/kdump-client.js:135
+msgid "ssh key isn't a path"
+msgstr "ssh 密钥不是一个路径"
+
+#: pkg/kdump/kdump-client.js:133
+msgid "ssh server is empty"
+msgstr "ssh 服务器为空"
+
+#: pkg/storaged/legacy-vdo/legacy-vdo.jsx:46 pkg/storaged/mdraid/mdraid.jsx:59
+#: pkg/storaged/utils.js:923
+msgid "stop"
+msgstr "停止"
+
+#: pkg/storaged/filesystem/utils.jsx:181
+msgid "stop boot on failure"
+msgstr "在失败时停止引导"
+
+#: pkg/storaged/mdraid/mdraid.jsx:203 pkg/storaged/stratis/stopped-pool.jsx:99
+msgid "stopped"
+msgstr "停止"
+
+#: pkg/metrics/metrics.jsx:112
+msgid "sys"
+msgstr "sys"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemctl"
+msgstr "systemctl"
+
+#: pkg/systemd/manifest.json:0
+msgid "systemd"
+msgstr "systemd"
+
+#: pkg/storaged/manifest.json:0
+msgid "tang"
+msgstr "tang"
+
+#: pkg/systemd/manifest.json:0
+msgid "target"
+msgstr "目标"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "tcp"
+msgstr "tcp"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "team"
+msgstr "组"
+
+#: pkg/systemd/manifest.json:0
+msgid "time"
+msgstr "时间"
+
+#: pkg/systemd/manifest.json:0
+msgid "timer"
+msgstr "计时器"
+
+#: pkg/storaged/manifest.json:0
+msgid "udisks"
+msgstr "udisks"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "udp"
+msgstr "udp"
+
+#: pkg/systemd/manifest.json:0
+msgid "unit"
+msgstr "单元"
+
+#: pkg/systemd/services.jsx:555 pkg/systemd/services.jsx:565
+#: pkg/systemd/hw-detect.js:129
+msgid "unknown"
+msgstr "未知"
+
+#: pkg/storaged/jobs-panel.jsx:101
+msgid "unknown target"
+msgstr "未知目标"
+
+#: pkg/systemd/manifest.json:0
+msgid "unmask"
+msgstr "取消屏蔽"
+
+#: pkg/storaged/btrfs/subvolume.jsx:213 pkg/storaged/utils.js:873
+#: pkg/storaged/utils.js:886 pkg/storaged/manifest.json:0
+msgid "unmount"
+msgstr "卸载"
+
+#: pkg/storaged/utils.js:533
+msgid "unpartitioned space on $0"
+msgstr "$0 上未分区的空间"
+
+#: pkg/metrics/metrics.jsx:112 pkg/users/manifest.json:0
+msgid "user"
+msgstr "用户"
+
+#: pkg/users/manifest.json:0
+msgid "useradd"
+msgstr "useradd"
+
+#: pkg/users/manifest.json:0
+msgid "username"
+msgstr "用户名"
+
+#: pkg/storaged/stratis/stopped-pool.jsx:130
+msgid "using key description $0"
+msgstr "使用密钥描述 $0"
+
+#: pkg/storaged/manifest.json:0
+msgid "vdo"
+msgstr "vdo"
+
+#: pkg/systemd/manifest.json:0
+msgid "version"
+msgstr "版本"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "vlan"
+msgstr "vlan"
+
+#: pkg/storaged/manifest.json:0
+msgid "volume"
+msgstr "卷"
+
+#: pkg/systemd/manifest.json:0
+msgid "warning"
+msgstr "警告"
+
+#: pkg/networkmanager/wireguard.jsx:84
+msgid "wireguard-tools package is not installed"
+msgstr "未安装 WireGuard-tools 软件包"
+
+#: pkg/storaged/crypto/encryption.jsx:232
+msgid "yes"
+msgstr "是"
+
+#: pkg/packagekit/manifest.json:0
+msgid "yum"
+msgstr "yum"
+
+#: pkg/networkmanager/manifest.json:0
+msgid "zone"
+msgstr "区"
+
+#~ msgid ""
+#~ "Kdump service not installed. Please ensure package kexec-tools is "
+#~ "installed."
+#~ msgstr "Kdump 服务未安装。请保证软件包 kexec-tools 已安装。"
+
+#~ msgid ""
+#~ "No memory reserved. Append a crashkernel option to the kernel command "
+#~ "line (e.g. in /etc/default/grub) to reserve memory at boot time. Example: "
+#~ "crashkernel=512M"
+#~ msgstr ""
+#~ "没有内存预留。在内核命令行(例如在 /etc/default/grub 文件)中增加一个 "
+#~ "crashkernel 选项以在启动时预留内存。例如:crashkernel=512M"
+
+#~ msgid "Service is running"
+#~ msgstr "服务正在运行"
+
+#~ msgid "Service is starting"
+#~ msgstr "服务正在启动"
+
+#~ msgid "Service is stopped"
+#~ msgstr "服务已停止"
+
+#~ msgid "Service is stopping"
+#~ msgstr "服务正在停止"
+
+#~ msgid "This will test the kdump configuration by crashing the kernel."
+#~ msgstr "这将通过崩溃内核来测试 kdump 配置。"
+
+#~ msgid "crashkernel not configured in the kernel command line"
+#~ msgstr "在内核命令行中没有配置 crashkernel"
+
+#~ msgid "$0 (encrypted)"
+#~ msgstr "$0(已加密)"
+
+#~ msgid "$0 Stratis pool"
+#~ msgstr "$0 个 Stratis 池"
+
+#~ msgid "$0 cache"
+#~ msgstr "$0 缓存"
+
+#~ msgid "$0 of unknown tier"
+#~ msgstr "$0 个设备的层级未知"
+
+#~ msgid "$0, $1 free"
+#~ msgstr "容量为 $0,$1 可用"
+
+#~ msgid ""
+#~ "A spare disk needs to be added first before this disk can be removed."
+#~ msgstr "在移除该磁盘前需要添加一个备用磁盘。"
+
+#~ msgid "Backing device"
+#~ msgstr "后端设备"
+
+#~ msgid "Block"
+#~ msgstr "块"
+
+#~ msgctxt "storage"
+#~ msgid "Capacity"
+#~ msgstr "容量"
+
+#~ msgid "Content"
+#~ msgstr "内容"
+
+#~ msgid "Create devices"
+#~ msgstr "创建设备"
+
+#~ msgctxt "storage"
+#~ msgid "Device"
+#~ msgstr "设备"
+
+#~ msgctxt "storage"
+#~ msgid "Device file"
+#~ msgstr "设备文件"
+
+#~ msgid "Devices"
+#~ msgstr "设备"
+
+#~ msgid "Drives"
+#~ msgstr "驱动器"
+
+#~ msgid "Encrypted volumes need to be unlocked before they can be resized."
+#~ msgstr "加密卷需要先解锁才能调整大小。"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Filesystem (encrypted)"
+#~ msgstr "文件系统(加密的)"
+
+#~ msgid "Filesystems"
+#~ msgstr "文件系统"
+
+#~ msgid "Free"
+#~ msgstr "可用"
+
+#~ msgid ""
+#~ "Free up space in this group: Shrink or delete other logical volumes or "
+#~ "add another physical volume."
+#~ msgstr ""
+#~ "在这个组中释放更多可用空间:缩小或删除其它逻辑卷或添加另外一个物理卷。"
+
+#~ msgid "Inactive volume"
+#~ msgstr "暂停卷"
+
+#~ msgctxt "storage"
+#~ msgid "Keyserver"
+#~ msgstr "密钥服务器"
+
+#~ msgid "LVM2 member"
+#~ msgstr "LVM2 成员"
+
+#~ msgid "Locked devices"
+#~ msgstr "锁定的设备"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Locked encrypted data"
+#~ msgstr "锁住加密的数据"
+
+#~ msgctxt "storage"
+#~ msgid "Model"
+#~ msgstr "型号"
+
+#~ msgid "NFS mounts"
+#~ msgstr "NFS 挂载"
+
+#~ msgid "NFS support not installed"
+#~ msgstr "未安装 NFS 的支持"
+
+#~ msgid "No NFS mounts set up"
+#~ msgstr "没有设置 NFS 挂载"
+
+#~ msgid "No devices"
+#~ msgstr "无设备"
+
+#~ msgid "No drives attached"
+#~ msgstr "未连接驱动器"
+
+#~ msgid "No iSCSI targets set up"
+#~ msgstr "没有设置 iSCSI 目标"
+
+#~ msgid "Not enough space for new filesystems"
+#~ msgstr "没有足够的空间容纳新文件系统"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Other data"
+#~ msgstr "其他数据"
+
+#~ msgid "Partitioned block device"
+#~ msgstr "分区的块设备"
+
+#~ msgctxt "storage"
+#~ msgid "Passphrase"
+#~ msgstr "密码"
+
+#, fuzzy
+#~| msgid "Physical volumes can not be resized here."
+#~ msgid ""
+#~ "Physical volumes can not be removed while a volume group is missing "
+#~ "physical volumes."
+#~ msgstr "物理卷不能在这里改变大小。"
+
+#~ msgid "Pool"
+#~ msgstr "池"
+
+#~ msgid "Pool for thin volumes"
+#~ msgstr "瘦卷的池"
+
+#~ msgctxt "storage"
+#~ msgid "RAID level"
+#~ msgstr "RAID 级别"
+
+#~ msgid "RAID member"
+#~ msgstr "RAID 成员"
+
+#~ msgctxt "storage"
+#~ msgid "Removable drive"
+#~ msgstr "可移动驱动器"
+
+#~ msgid "Show $0 device"
+#~ msgid_plural "Show all $0 devices"
+#~ msgstr[0] "显示 $0 设备"
+
+#~ msgid "Show $0 drive"
+#~ msgid_plural "Show all $0 drives"
+#~ msgstr[0] "显示 $0 驱动"
+
+#~ msgid "Show all"
+#~ msgstr "显示全部"
+
+#~ msgid "Source"
+#~ msgstr "源"
+
+#~ msgid "Start pool"
+#~ msgstr "启动池"
+
+#~ msgid "Start pool to see filesystems."
+#~ msgstr "启动池以查看文件系统。"
+
+#~ msgctxt "storage"
+#~ msgid "State"
+#~ msgstr "状态"
+
+#~ msgid "Stopped Stratis pool"
+#~ msgstr "停止 Stratis 池"
+
+#~ msgid "Stratis member"
+#~ msgstr "Stratis 成员"
+
+#~ msgid "Stratis pool $0"
+#~ msgstr "Stratis 池 $0"
+
+#~ msgid "Support is installed."
+#~ msgstr "已安装支持。"
+
+#~ msgid "The RAID device must be running in order to add spare disks."
+#~ msgstr "为了添加备用的磁盘,RAID 设备必须在运行。"
+
+#~ msgid "The last disk of a RAID device cannot be removed."
+#~ msgstr "RAID 设备中的最后一个磁盘不能被移除。"
+
+#~ msgid "The last physical volume of a volume group cannot be removed."
+#~ msgstr "不能删除一个卷组的最后一个物理卷。"
+
+#~ msgid ""
+#~ "There is not enough free space elsewhere to remove this physical volume. "
+#~ "At least $0 more free space is needed."
+#~ msgstr ""
+#~ "其它地方没有足够的空闲空间来移除这个物理卷。至少需要 $0 或更多空闲空间。"
+
+#~ msgid "This disk cannot be removed while the device is recovering."
+#~ msgstr "当设备正在恢复时,该磁盘不能被移除。"
+
+#~ msgid "This volume needs to be activated before it can be resized."
+#~ msgstr "在调整大小之前,需要激活此卷。"
+
+#~ msgctxt "storage"
+#~ msgid "UUID"
+#~ msgstr "UUID"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "Unrecognized data"
+#~ msgstr "无法识别的数据"
+
+#~ msgctxt "storage"
+#~ msgid "Usage"
+#~ msgstr "用法"
+
+#~ msgid "Used for"
+#~ msgstr "用于"
+
+#~ msgctxt "storage-id-desc"
+#~ msgid "VDO backing"
+#~ msgstr "VDO支持"
+
+#~ msgid "VDO device"
+#~ msgstr "VDO 设备"
+
+#~ msgid "Volume"
+#~ msgstr "卷"
+
+#~ msgid "Automatic (DHCP)"
+#~ msgstr "自动 (DHCP)"
+
+#~ msgid "Accept key and connect"
+#~ msgstr "接受密钥并连接"
+
+#~ msgid "Encrypted volumes can not be resized here."
+#~ msgstr "加密卷不能在这里改变大小。"
+
+#~ msgid "Use a Tang keyserver"
+#~ msgstr "使用 Tang 密钥服务器"
+
+#~ msgid "Use a passphrase"
+#~ msgstr "使用密码"
+
+#~ msgctxt "storage"
+#~ msgid "Bitmap"
+#~ msgstr "Bitmap"
+
+#~ msgid ""
+#~ "This pool can not be unlocked here because its key description is not in "
+#~ "the expected format."
+#~ msgstr "此池在这里不能被解锁,因为其密钥描述不是预期的格式。"
+
+#~ msgid "Toggle bitmap"
+#~ msgstr "切换位图"
+
+#~ msgid ""
+#~ "Make sure the key hash from the Tang server matches one of the following:"
+#~ msgstr "确保来自 Tang 服务器的密钥哈希与以下之一匹配:"
+
+#~ msgid "Manually check with SSH: "
+#~ msgstr "用 SSH 手动检查: "
+
+#~ msgid "SHA1"
+#~ msgstr "SHA1"
+
+#~ msgid "SHA256"
+#~ msgstr "SHA256"
+
+#~ msgid "Port number and type do not match"
+#~ msgstr "端口号和类型不匹配"
+
+#~ msgid "Locked encrypted Stratis pool"
+#~ msgstr "锁住加密的 Stratis 池"
+
+#~ msgid "Error while deleting alert: $0"
+#~ msgstr "删除警告: $0 时出错"
+
+#~ msgid "Unable to get alert: $0"
+#~ msgstr "无法获取警告: $0"
+
+#~ msgid "[$0 bytes of binary data]"
+#~ msgstr "[$0 字节二进制数据]"
+
+#~ msgid "Disk I/O spike"
+#~ msgstr "磁盘 I/O 高负载"
+
+#~ msgid "Event"
+#~ msgstr "事件"
+
+#~ msgid "Event logs"
+#~ msgstr "事件日志"
+
+#~ msgid "Load spike"
+#~ msgstr "负载激增"
+
+#~ msgid "Memory spike"
+#~ msgstr "内存激增"
+
+#~ msgid "Network I/O spike"
+#~ msgstr "网络 I/O 激增"
+
+#~ msgid "ssh key"
+#~ msgstr "SSH 密钥"
+
+#~ msgid ""
+#~ "If this option is checked, the filesystem will not be mounted during the "
+#~ "next boot even if it was mounted before it. This is useful if mounting "
+#~ "during boot is not possible, such as when a passphrase is required to "
+#~ "unlock the filesystem but booting is unattended."
+#~ msgstr ""
+#~ "如果选中此选项,则下一次引导期间不会挂载该文件系统。即使之前已被挂载。如果"
+#~ "无法在引导期间进行挂载,比如需要密码来解锁文件系统,但引导是无人参与时,这"
+#~ "将非常有用。"
+
+#~ msgid "Never mount at boot"
+#~ msgstr "引导时从不挂载"
+
+#~ msgid "Listing unit files"
+#~ msgstr "列出单元文件"
+
+#~ msgid "Listing unit files failed: $0"
+#~ msgstr "列出单元文件失败:$0"
+
+#~ msgid "Unit not found"
+#~ msgstr "未找到单位"
+
+#~ msgid "Validating key"
+#~ msgstr "验证 key"
+
+#~ msgid "Delete $0 group"
+#~ msgstr "删除$0组"
+
+#~ msgid "Container administrator"
+#~ msgstr "容器管理员"
+
+#~ msgid "Image builder"
+#~ msgstr "图像生成器"
+
+#~ msgid "Roles"
+#~ msgstr "角色"
+
+#~ msgid "Server administrator"
+#~ msgstr "服务器管理员"
+
+#~ msgid "Unix group: $0"
+#~ msgstr "Unix 组:$0"
+
+#~ msgid "Successfully copied to keyboard"
+#~ msgstr "成功复制到键盘"
+
+#~ msgid "Diagnostic Reports"
+#~ msgstr "诊断报告"
+
+#~ msgid "Kernel Dump"
+#~ msgstr "内核转储"
+
+#~ msgid "Software Updates"
+#~ msgstr "软件更新"
+
+#~ msgid "Update log"
+#~ msgstr "更新日志"
+
+#~ msgid "nfs dump target isn't formatted as server:path"
+#~ msgstr "nfs dump 目标的格式不是 server:path"
+
+#, fuzzy
+#~| msgid "Zone"
+#~ msgid "$0 Zone"
+#~ msgstr "区域"
+
+#~ msgid "Create diagnostic report"
+#~ msgstr "创建诊断报告"
+
+#~ msgid "Done!"
+#~ msgstr "已完成!"
+
+#~ msgid "Download report"
+#~ msgstr "下载报告"
+
+#~ msgid "No archive has been created."
+#~ msgstr "没有档案被创建。"
+
+#~ msgid "Please confirm deletion of $0"
+#~ msgstr "请确认删除 $0"
+
+#~ msgid ""
+#~ "The generated archive contains data considered sensitive and its content "
+#~ "should be reviewed by the originating organization before being passed to "
+#~ "any third party."
+#~ msgstr ""
+#~ "生成的归档中包含被认为敏感的数据,其内容应在传递给任何第三方之前由原始组织"
+#~ "进行审核。"
+
+#~ msgid ""
+#~ "This tool will collect system configuration and diagnostic information "
+#~ "from this system for use with diagnosing problems with the system."
+#~ msgstr "该工具会从该系统搜集系统配置和诊断信息用于诊断系统问题。"
+
+#~ msgid "Server is missing the cockpit-system package"
+#~ msgstr "服务器缺少 cockpit-系统软件包"
+
+#~ msgid "This package is not compatible with this version of Cockpit"
+#~ msgstr "该软件包与该版本的 Cockpit 不兼容"
+
+#~ msgid "This package requires Cockpit version %s or later"
+#~ msgstr "该软件包需要 %s 版本或以后的 Cockpit"
+
+#~ msgid "Performance Metrics"
+#~ msgstr "性能指标数据"
+
+#, fuzzy
+#~| msgid "read only"
+#~ msgid "Save only"
+#~ msgstr "只读"
+
+#~ msgid "$0 GiB total"
+#~ msgstr "$0 GiB 总数"
+
+#~ msgid "Apply"
+#~ msgstr "应用"
+
+#~ msgid "Not authorized to remove zone $0"
+#~ msgstr "无权删除区 $0"
+
+#~ msgid "Click $0 again to use the password anyway."
+#~ msgstr "再次点 $0 以使用该密码。"
+
+#~ msgid "Access"
+#~ msgstr "访问"
+
+#~ msgid "DISK IS FAILING"
+#~ msgstr "磁盘错误"
+
+#~ msgid "Logical Size"
+#~ msgstr "逻辑大小"
+
+#~ msgid "Security updates "
+#~ msgstr "安全更新 "
+
+#~ msgid "Updates "
+#~ msgstr "更新 "
+
+#~ msgid "(Optional)"
+#~ msgstr "(可选)"
+
+#~ msgid "Active since"
+#~ msgstr "活跃自"
+
+#~ msgid "Affected locations"
+#~ msgstr "受影响的位置"
+
+#~ msgid "Process"
+#~ msgstr "流程"
+
+#~ msgid "Remove and delete"
+#~ msgstr "去掉并删除"
+
+#~ msgid "Remove and format"
+#~ msgstr "删除并格式化"
+
+#~ msgid "Remove and grow"
+#~ msgstr "删除并增大"
+
+#~ msgid "Remove and initialize"
+#~ msgstr "删除并初始化"
+
+#~ msgid "Remove and shrink"
+#~ msgstr "删除并缩小"
+
+#~ msgid "Remove and stop device"
+#~ msgstr "删除并停止设备"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions and system services. "
+#~ "Proceeding will stop these."
+#~ msgstr "登录会话和系统服务正在使用文件系统。继续进行将停止这些。"
+
+#~ msgid ""
+#~ "The filesystem is in use by login sessions. Proceeding will stop these."
+#~ msgstr "登录会话正在使用文件系统。继续进行将停止这些。"
+
+#~ msgid ""
+#~ "The filesystem is in use by system services. Proceeding will stop these."
+#~ msgstr "系统服务正在使用文件系统。继续进行将停止这些。"
+
+#~ msgid ""
+#~ "This device has filesystems that are currently in use. Proceeding will "
+#~ "unmount all filesystems on it."
+#~ msgstr "该设备有正在使用的文件系统。继续进行将卸载其上的所有文件系统。"
+
+#~ msgid "This device is currently used for LVM2 volume groups."
+#~ msgstr "该设备目前用于 LVM2 卷组。"
+
+#~ msgid ""
+#~ "This device is currently used for LVM2 volume groups. Proceeding will "
+#~ "remove it from its volume groups."
+#~ msgstr "该设备目前用于 LVM2 卷组。继续进行将从卷组中移除它。"
+
+#~ msgid "This device is currently used for RAID devices."
+#~ msgstr "该设备正在被 RAID 设备使用。"
+
+#~ msgid ""
+#~ "This device is currently used for RAID devices. Proceeding will remove it "
+#~ "from its RAID devices."
+#~ msgstr "该设备正在被 RAID 设备使用。继续操作将从 RAID 设备中移除它。"
+
+#~ msgid "This device is currently used for Stratis pools."
+#~ msgstr "该设备正在被Stratis 池使用。"
+
+#~ msgid "Unmount and delete"
+#~ msgstr "卸载并删除"
+
+#~ msgid "Unmount and format"
+#~ msgstr "卸载并格式化"
+
+#~ msgid "Unmount and grow"
+#~ msgstr "卸载并增加"
+
+#~ msgid "Unmount and initialize"
+#~ msgstr "卸载并初始化"
+
+#~ msgid "Unmount and shrink"
+#~ msgstr "卸载并缩小"
+
+#~ msgid "Unmount and stop device"
+#~ msgstr "卸载并停止设备"
+
+#~ msgid "$0 of $1"
+#~ msgstr "$1 的 $0"
+
+#~ msgid "Blockdev"
+#~ msgstr "块设备"
+
+#~ msgid "Blockdev of Stratis pool $0"
+#~ msgstr "Stratis 池 $0 的块设备"
+
+#~ msgid "Blockdev of locked Stratis pool $0"
+#~ msgstr "被锁定的Stratis 池 $0 的块设备"
+
+#~ msgid "LVM2 physical volume of $0"
+#~ msgstr "$0 的LVM2物理卷"
+
+#~ msgid "Member of RAID device $0"
+#~ msgstr "RAID 设备 $0 的成员"
+
+#~ msgid "Menu"
+#~ msgstr "菜单"
+
+#~ msgid "VDO backing"
+#~ msgstr "VDO支持"
+
+#, fuzzy
+#~| msgid "Create storage pool"
+#~ msgid "Create Stratis Pool"
+#~ msgstr "创建存储池"
+
+#, fuzzy
+#~| msgid "Mount options"
+#~ msgid "Mount Options"
+#~ msgstr "挂载选项"
+
+#, fuzzy
+#~| msgid "Reset Storage Pool"
+#~ msgid "Stratis Pool"
+#~ msgstr "重置存储池"
+
+#~ msgid "A disk is needed."
+#~ msgstr "需要一个磁盘。"
+
+#~ msgid "Disk"
+#~ msgstr "磁盘"
+
+#~ msgid "For legacy applications only. Reduces performance."
+#~ msgstr "只适用老的应用程序。性能会降低。"
+
+#~ msgid "Install VDO support"
+#~ msgstr "安装 VDO 支持"
+
+#~ msgid "Use 512 byte emulation"
+#~ msgstr "使用 512 字节模拟"
+
+#~ msgid "System services"
+#~ msgstr "系统服务"
+
+#~ msgid "Create diagnostic report "
+#~ msgstr "创建诊断报告 "
+
+#~ msgid ""
+#~ "Connecting simultaneously to more than {{ limit }} machines is "
+#~ "unsupported."
+#~ msgstr "不支持同时连接到超过 {{ limit }} 主机。"
+
+#~ msgid "Kerberos based SSO"
+#~ msgstr "基于 Kerberos 的 SSO"
+
+#~ msgid "Log in to {{host}}"
+#~ msgstr "登录到 {{host}}"
+
+#~ msgid "The new key passwords do not match"
+#~ msgstr "新的密钥密码不匹配"
+
+#~ msgid ""
+#~ "To verify a fingerprint, run the following on {{host}} while physically "
+#~ "sitting at the machine or through a trusted network:"
+#~ msgstr ""
+#~ "要验证指纹,在 {{host}} 上运行以下内容(在实际的物理机器上本地进行,或通过"
+#~ "一个可信任的网络进行):"
+
+#~ msgid "Unable to contact {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "无法连接 {{#strong}}{{host}}{{/strong}}。"
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. For more "
+#~ "authentication options and troubleshooting support please upgrade cockpit-"
+#~ "ws to a newer version."
+#~ msgstr ""
+#~ "无法登陆到 {{#strong}}{{host}}{{/strong}}。如需更多验证选项和排错支持,请"
+#~ "将 cockpit-ws 升级到一个新版本。"
+
+#~ msgid ""
+#~ "Unable to log in to {{#strong}}{{host}}{{/strong}}. To connect to this "
+#~ "host you will need to enable one of the following authentication methods "
+#~ "in the sshd config on {{#strong}}{{host}}{{/strong}}:"
+#~ msgstr ""
+#~ "无法登录到 {{#strong}}{{host}}{{/strong}}。 为了连接到这个主机,您将需要"
+#~ "在 {{#strong}}{{host}}{{/strong}} 上的 sshd 配置中启用以下验证方式之一:"
+
+#~ msgid "You are connecting to {{host}} for the first time."
+#~ msgstr "您第一次连接到 {{host}}。"
+
+#~ msgid "{{host}} key changed"
+#~ msgstr "{{host}} 密钥已更改"
+
+#~ msgid "Clear mount point configuration"
+#~ msgstr "清除挂载点配置"
+
+#~ msgid ""
+#~ "Creating this bond will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr "创建该绑定将会断开与服务器的连接,并且将会使管理界面不能访问。"
+
+#~ msgid ""
+#~ "Creating this bridge will break the connection to the server, and will "
+#~ "make the administration UI unavailable."
+#~ msgstr "创建该网桥将会中断与服务器的连接,并且将会导致管理界面不可用。"
+
+#~ msgid ""
+#~ "Creating this team will break the connection to the server, and will make "
+#~ "the administration UI unavailable."
+#~ msgstr "创建该组将断开与服务器的连接,并且将会导致管理界面不可用。"
+
+#~ msgid "IP settings"
+#~ msgstr "IP 设置"
+
+#~ msgid ""
+#~ "Switching off <b>$0</b> will break the connection to the server, and "
+#~ "will make the administration UI unavailable."
+#~ msgstr "关掉 <b>$0</b> 将中断与服务器的连接,并将导致管理界面不可用。"
+
+#~ msgid "Administrator password"
+#~ msgstr "管理员密码"
+
+#~ msgid "Computer OU"
+#~ msgstr "计算机 OU"
+
+#~ msgid "Deleting a RAID device will erase all data on it."
+#~ msgstr "删除 RAID 设备将擦除其中的所有数据."
+
+#~ msgid "Deleting a volume group will erase all data on it."
+#~ msgstr "删除卷组将擦除其中的所有数据。"
+
+#~ msgid "Don't overwrite existing data"
+#~ msgstr "不覆盖已存在数据"
+
+#~ msgid "Erase"
+#~ msgstr "擦除"
+
+#~ msgid "Format disk $0"
+#~ msgstr "格式化磁盘 $0"
+
+#~ msgid "Formatting a storage device will erase all data on it."
+#~ msgstr "格式化存储设备将擦除其中的所有数据."
+
+#~ msgid "Host name should not be changed in a domain"
+#~ msgstr "在域内的主机不可更改主机名"
+
+#~ msgid "More"
+#~ msgstr "更多"
+
+#~ msgid "Not joined"
+#~ msgstr "未被加入"
+
+#~ msgid "One time password"
+#~ msgstr "一次性密码"
+
+#~ msgctxt "<date> from <host> on <terminal>"
+#~ msgid "$0 from $1 on $2"
+#~ msgstr "$0 来自 $1 于 $2"
+
+#~ msgid "Hours must be a number between 0 and 23"
+#~ msgstr "小时数必须是 0 到 23 之间的数字"
+
+#~ msgid "Last login:"
+#~ msgstr "最后登录:"
+
+#~ msgid "Minutes must be a number between 0 and 59"
+#~ msgstr "分钟必须是 0 到 59 之间的数字"
+
+#~ msgid "There was $0 failed login attempt since the last successful login."
+#~ msgid_plural ""
+#~ "There were $0 failed login attempts since the last successful login."
+#~ msgstr[0] "自从最后一次成功登录以后有 $0 次失败的登录尝试。"
+
+#~ msgid "e.g. \"$0\""
+#~ msgstr "例如 \"$0\""
+
+#~ msgid "$0 active zones"
+#~ msgstr "$0活跃区"
+
+#~ msgid "Dismiss $0 alerts"
+#~ msgstr "忽略 $0 警报"
+
+#~ msgid "$0 occurrence"
+#~ msgid_plural "$0 occurrences"
+#~ msgstr[0] "$0 概率"
+
+#~ msgid "Licensed under:"
+#~ msgstr "授权于:"
+
+#~ msgid "Unlock at boot"
+#~ msgstr "在启动时解锁"
+
+#~ msgid "Unlock read only"
+#~ msgstr "只读解锁"
+
+#~ msgid "Search the logs with a combination of terms:"
+#~ msgstr "使用组合条件搜索日志:"
+
+#~ msgid "Show filters"
+#~ msgstr "显示过滤器"
+
+#~ msgid "Text"
+#~ msgstr "文本"
+
+#~ msgid "any free-form string as regular expression"
+#~ msgstr "任意自由形式的字符串作为正则表达式"
+
+#~ msgid "e.g."
+#~ msgstr "例如"
+
+#~ msgid "log fields"
+#~ msgstr "日志项"
+
+#~ msgid "qualifiers"
+#~ msgstr "合规者"
+
+#~ msgid "Toggle session settings"
+#~ msgstr "切换会话设置"
+
+#~ msgid "(none)"
+#~ msgstr "(无)"
+
+#~ msgid "Create timers"
+#~ msgstr "创建定时器"
+
+#~ msgid "Recommended default"
+#~ msgstr "推荐的默认"
+
+#~ msgid "Run"
+#~ msgstr "运行"
+
+#~ msgid "Select unit state"
+#~ msgstr "选择单元状态"
+
+#, fuzzy
+#~| msgid "Enable stored metrics"
+#~ msgid "Enable PCP metrics collector"
+#~ msgstr "启用保存的指标数据"
+
+#~ msgid "Enable stored metrics"
+#~ msgstr "启用保存的指标数据"
+
+#~ msgid "Force remove passphrase in $0"
+#~ msgstr "强制删除 $0 中的密码"
+
+#~ msgid "If tang-show-keys is not available, run the following:"
+#~ msgstr "如果没有 tang-show-keys,请运行以下命令:"
+
+#~ msgid "PCP"
+#~ msgstr "PCP"
+
+#~ msgid "What if tang-show-keys is not available?"
+#~ msgstr "如果没有 tang-show-keys?"
+
+#~ msgid "key slot $0"
+#~ msgstr "钥匙槽 $0"
+
+#~ msgid "Something went wrong"
+#~ msgstr "出现了一些错误"
+
+#~ msgid "This didn't work, please try again"
+#~ msgstr "操作失败,请重试"
+
+#~ msgid "You can not gain administrative access."
+#~ msgstr "您不能获取管理员权限。"
+
+#~ msgid "Automatic updates are not set up"
+#~ msgstr "自动更新没有被设置"
+
+#~ msgid "$0 CPU configuration"
+#~ msgstr "$0 CPU 配置"
+
+#~ msgid "$0 Network"
+#~ msgid_plural "$0 Networks"
+#~ msgstr[0] "$0 网络"
+
+#~ msgid ""
+#~ "$0 is available for most operating systems. To install it, search for it "
+#~ "in GNOME Software or run the following:"
+#~ msgstr ""
+#~ "$0 大多数操作系统可用。为了安装它,请在 GNOME 软件中心中搜索它,或运行以下"
+#~ "命令:"
+
+#~ msgid "$0 memory adjustment"
+#~ msgstr "$0 内存调整"
+
+#~ msgid "$0 network"
+#~ msgstr "$0 网络"
+
+#~ msgid "$0 vCPU"
+#~ msgid_plural "$0 vCPUs"
+#~ msgstr[0] "$0 vCPU"
+
+#~ msgid "$0 vCPU details"
+#~ msgstr "$0 vCPU 详情"
+
+#~ msgid "$0 virtual network interface settings"
+#~ msgstr "$0 虚拟网络接口设置"
+
+#~ msgid "Activate the storage pool to administer volumes"
+#~ msgstr "激活存储池以管理卷"
+
+#~ msgid "Add network interface"
+#~ msgstr "添加网络接口"
+
+#~ msgid "Add virtual network interface"
+#~ msgstr "添加虚拟网络接口"
+
+#~ msgid "Additional"
+#~ msgstr "额外"
+
+#~ msgid "Address not within subnet"
+#~ msgstr "地址不在子网内"
+
+#~ msgid "After deleting the snapshot, all its captured content will be lost."
+#~ msgstr "删除快照后,其所收集的内容将全部丢失。"
+
+#~ msgid "Always attach"
+#~ msgstr "保证连接"
+
+#~ msgid "Attaching it will make this disk shareable for every VM using it."
+#~ msgstr "附加它将会使这个磁盘在每个使用它的 VM 间可共享。"
+
+#~ msgid "Automatically start libvirt on boot"
+#~ msgstr "在引导时自动启动 libvirt"
+
+#~ msgid "Autostart"
+#~ msgstr "自动启动"
+
+#~ msgid "Boot order"
+#~ msgstr "启动顺序"
+
+#~ msgid "Boot order settings could not be saved"
+#~ msgstr "无法保存启动顺序"
+
+#~ msgid "Bus"
+#~ msgstr "总线"
+
+#~ msgid "CD/DVD disc"
+#~ msgstr "CD/DVD 盘"
+
+#~ msgid "CPU configuration could not be saved"
+#~ msgstr "CPU 配置不能被保存"
+
+#~ msgid "CPU type"
+#~ msgstr "CPU类型"
+
+#~ msgid "Change firmware"
+#~ msgstr "修改固件"
+
+#~ msgid "Changes will take effect after shutting down the VM"
+#~ msgstr "改变将在 VM 关机后生效"
+
+#~ msgid "Choose an operating system"
+#~ msgstr "选择一个操作系统"
+
+#~ msgid ""
+#~ "Clicking \"Launch remote viewer\" will download a .vv file and launch $0."
+#~ msgstr "点 \"Launch remote viewer\" 将下载一个 .vv 文件并启动 $0。"
+
+#~ msgid "Clone"
+#~ msgstr "克隆"
+
+#, fuzzy
+#~| msgid "Can't load image"
+#~ msgid "Cloud base image"
+#~ msgstr "无法加载镜像"
+
+#~ msgid "Confirm this action"
+#~ msgstr "确认此操作"
+
+#~ msgid "Connect"
+#~ msgstr "连接"
+
+#~ msgid "Connect with any viewer application for following protocols"
+#~ msgstr "使用任意 viewer 应用程序用于以下协议"
+
+#~ msgid "Connecting to virtualization service"
+#~ msgstr "连接虚拟化服务"
+
+#~ msgid "Connection"
+#~ msgstr "连接"
+
+#~ msgid "Console"
+#~ msgstr "控制台"
+
+#~ msgid "Cores per socket"
+#~ msgstr "每个插槽的内核数"
+
+#~ msgid "Could not revert to snapshot"
+#~ msgstr "无法恢复到快照"
+
+#~ msgid "Crashed"
+#~ msgstr "已崩溃"
+
+#~ msgid "Create VM"
+#~ msgstr "创建虚拟机"
+
+#~ msgid "Create a clone VM based on $0"
+#~ msgstr "基于 $0 创建一个克隆虚拟机"
+
+#~ msgid "Create new"
+#~ msgstr "新建"
+
+#~ msgid "Create new virtual machine"
+#~ msgstr "创建新的虚拟机"
+
+#~ msgid "Create virtual network"
+#~ msgstr "创建虚拟网络"
+
+#~ msgid "Creating VM"
+#~ msgstr "创建虚拟机"
+
+#~ msgid "Creating VM installation"
+#~ msgstr "创建虚拟机安装"
+
+#~ msgid "Creation of VM $0 failed"
+#~ msgstr "虚拟机$0创建失败"
+
+#~ msgid "Creation time"
+#~ msgstr "创建时间"
+
+#~ msgid "Ctrl+Alt+$0"
+#~ msgstr "Ctrl+Alt+$0"
+
+#~ msgid "Current allocation"
+#~ msgstr "当前分配"
+
+#~ msgid "Custom firmware: $0"
+#~ msgstr "自定义固件:$0"
+
+#~ msgid "DHCP range"
+#~ msgstr "DHCP 范围"
+
+#~ msgid "Delete associated storage files:"
+#~ msgstr "删除关联的存储文件:"
+
+#~ msgid "Delete storage pool $0"
+#~ msgstr "删除存储池$0"
+
+#~ msgid "Delete the volumes inside this pool"
+#~ msgstr "删除这个存储池中的卷"
+
+#~ msgid ""
+#~ "Deleting an inactive storage pool will only undefine the pool. Its "
+#~ "content will not be deleted."
+#~ msgstr "删除一个不活跃的存储池将只会取消定义这个池。它的内容不会被删除。"
+
+#~ msgid "Desktop viewer"
+#~ msgstr "Desktop Viewer"
+
+#~ msgid ""
+#~ "Detach the disks using this pool from any VMs before attempting deletion."
+#~ msgstr "尝试删除之前,从所有 VM 中分离使用这个池的磁盘。"
+
+#~ msgid "Disconnected from serial console. Click the connect button."
+#~ msgstr "与串行控制台断开连接。单击连接按钮。"
+
+#~ msgid "Disk $0 fail to get detached from VM $1"
+#~ msgstr "磁盘$0无法从虚拟机$1上分离"
+
+#~ msgid "Disk failed to be attached"
+#~ msgstr "挂载磁盘失败"
+
+#~ msgid "Disk failed to be created"
+#~ msgstr "创建磁盘失败"
+
+#~ msgid "Disk image file"
+#~ msgstr "磁盘镜像文件"
+
+#~ msgid "Disk settings could not be saved"
+#~ msgstr "磁盘设置不能被保存"
+
+#~ msgid "Disk-only snapshot"
+#~ msgstr "纯磁盘快照"
+
+#~ msgid "Domain has crashed"
+#~ msgstr "域已崩溃"
+
+#~ msgid "Domain is blocked on resource"
+#~ msgstr "域在资源上被阻塞"
+
+#~ msgid "Download an OS"
+#~ msgstr "下载一个 OS"
+
+#~ msgid "Dying"
+#~ msgstr "Dying"
+
+#~ msgid "Emulated machine"
+#~ msgstr "虚拟的机器"
+
+#~ msgid "End"
+#~ msgstr "结束"
+
+#~ msgid "End should not be empty"
+#~ msgstr "结束不应为空"
+
+#~ msgid "Existing disk image on host's file system"
+#~ msgstr "主机文件系统上存在的磁盘镜像"
+
+#~ msgid "Expand"
+#~ msgstr "展开"
+
+#~ msgid "Failed to change firmware"
+#~ msgstr "修改固件失败"
+
+#~ msgid "Failed to fetch the IP addresses of the interfaces present in $0"
+#~ msgstr "获取在 $0 中的接口的 IP 地址失败"
+
+#~ msgid "Failed to send key Ctrl+Alt+$0 to VM $1"
+#~ msgstr "发送按键 Ctrl+Alt+$0 到 VM $1 失败"
+
+#~ msgid "Fewer than the maximum number of virtual CPUs should be enabled."
+#~ msgstr "启用的虚拟 CPU 数量应少于最大虚拟 CPU 数量。"
+
+#~ msgid "File"
+#~ msgstr "文件"
+
+#~ msgid "Filter by name"
+#~ msgstr "根据名称过滤"
+
+#~ msgid "Firmware"
+#~ msgstr "固件"
+
+#~ msgid "Force shut down"
+#~ msgstr "强制关机"
+
+#~ msgid "Forward mode"
+#~ msgstr "转发模式"
+
+#~ msgid "Forwarding mode"
+#~ msgstr "转发模式"
+
+#~ msgid "Generate automatically"
+#~ msgstr "自动产生"
+
+#~ msgid "GiB"
+#~ msgstr "GiB"
+
+#~ msgid "Go to VMs list"
+#~ msgstr "进入 VM 列表"
+
+#~ msgid "Hide additional options"
+#~ msgstr "隐藏额外操作"
+
+#~ msgid "Host device"
+#~ msgstr "主机设备"
+
+#~ msgid "Host name"
+#~ msgstr "主机名"
+
+#~ msgid "Host should not be empty"
+#~ msgstr "主机不能为空"
+
+#~ msgid "Hypervisor details"
+#~ msgstr "Hypervisor 详情"
+
+#~ msgid "IP configuration"
+#~ msgstr "IP 配置"
+
+#~ msgid "IPv4 and IPv6"
+#~ msgstr "IPv4 和 IPv6"
+
+#~ msgid "IPv4 network"
+#~ msgstr "IPv4 网络"
+
+#~ msgid "IPv4 network should not be empty"
+#~ msgstr "IPv4 网络不能为空"
+
+#~ msgid "IPv4 only"
+#~ msgstr "仅 IPv4"
+
+#~ msgid "IPv6 address"
+#~ msgstr "IPv6 地址"
+
+#~ msgid "IPv6 network"
+#~ msgstr "IPv6 网络"
+
+#~ msgid "IPv6 network should not be empty"
+#~ msgstr "IPv6 网络不能为空"
+
+#~ msgid "IPv6 only"
+#~ msgstr "仅 IPv6"
+
+#~ msgid "Idle"
+#~ msgstr "休眠"
+
+#~ msgid "Immediately start VM"
+#~ msgstr "立即启动 VM"
+
+#~ msgid "Import"
+#~ msgstr "导入"
+
+#~ msgid "Import VM"
+#~ msgstr "导入 VM"
+
+#~ msgid "Import a virtual machine"
+#~ msgstr "导入一个虚拟机"
+
+#~ msgid ""
+#~ "In most configurations, macvtap does not work for host to guest network "
+#~ "communication."
+#~ msgstr "在多数配置中,macvtap 不能为主机到客户机的网络通信工作。"
+
+#~ msgid "Initiator"
+#~ msgstr "启动器"
+
+#~ msgid "Initiator IQN should not be empty"
+#~ msgstr "启动器 IQN 不能为空"
+
+#~ msgid "Installation source"
+#~ msgstr "安装源"
+
+#~ msgid "Installation source must not be empty"
+#~ msgstr "安装源不能为空"
+
+#~ msgid "Installation type"
+#~ msgstr "安装类型"
+
+#~ msgid "Interface type"
+#~ msgstr "接口类型"
+
+#~ msgid "Invalid IPv4 mask or prefix length"
+#~ msgstr "无效的 IPv4 掩码或前缀长度"
+
+#~ msgid "Invalid IPv6 address"
+#~ msgstr "无效的 IPv6 地址"
+
+#~ msgid "Invalid IPv6 prefix"
+#~ msgstr "无效的 IPv6 前缀"
+
+#~ msgid "Invalid filename"
+#~ msgstr "无效的文件名"
+
+#~ msgid "Launch remote viewer"
+#~ msgstr "启动 Remote Viewer"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a root account created"
+#~ msgstr "如果您不希望创建一个 root 账号,则将密码留空"
+
+#~ msgid ""
+#~ "Leave the password blank if you do not wish to have a user account created"
+#~ msgstr "如果你不希望创建一个用户账号,则将密码留空"
+
+#, fuzzy
+#~| msgid ""
+#~| "Leave the password blank if you do not wish to have a root account "
+#~| "created"
+#~ msgid "Leave the password blank if you do not wish to set a root password"
+#~ msgstr "如果您不希望创建一个 root 账号,则将密码留空"
+
+#~ msgid ""
+#~ "Libvirt did not detect any UEFI/OVMF firmware image installed on the host"
+#~ msgstr "Libvirt 未检测到安装在主机上的任何 UEFI/OVMF 固件镜像"
+
+#~ msgid "Libvirt or hypervisor does not support UEFI"
+#~ msgstr "Libvirt 或虚拟机管理器不支持 UEFI"
+
+#~ msgid "Loading resources"
+#~ msgstr "加载资源"
+
+#~ msgid "Machine must be shut off before changing bus type"
+#~ msgstr "更改总线类型前必须关闭机器"
+
+#~ msgid "Machine must be shut off before changing cache mode"
+#~ msgstr "更改缓存模式之前必须关闭机器"
+
+#~ msgid "Managing virtual machines"
+#~ msgstr "管理虚拟机"
+
+#~ msgid "Manual connection"
+#~ msgstr "手动连接"
+
+#~ msgid "Mask or prefix length"
+#~ msgstr "掩码或前缀长度"
+
+#~ msgid "Mask or prefix length should not be empty"
+#~ msgstr "掩码或前缀长度不能为空"
+
+#~ msgid "Maximum allocation"
+#~ msgstr "最大分配"
+
+#~ msgid "Maximum memory could not be saved"
+#~ msgstr "最大内存不能被保存"
+
+#~ msgid "Maximum number of virtual CPUs allocated for the guest OS"
+#~ msgstr "为客户端操作系统分配的最大虚拟 CPU 数"
+
+#~ msgid ""
+#~ "Maximum number of virtual CPUs allocated for the guest OS, which must be "
+#~ "between 1 and $0"
+#~ msgstr "为客户机操作系统分配的最大虚拟 CPU 数,必须介于 1 和 $0 之间"
+
+#~ msgid "Maximum transmission unit"
+#~ msgstr "最大传输单元"
+
+#~ msgid "Memory could not be saved"
+#~ msgstr "内存不能被保存"
+
+#~ msgid "Memory must not be 0"
+#~ msgstr "内存不能为 0"
+
+#~ msgid "MiB"
+#~ msgstr "MiB"
+
+#~ msgid "Model type"
+#~ msgstr "型号类型"
+
+#~ msgid "NAT to $0"
+#~ msgstr "NAT 到 $0"
+
+#~ msgid "NIC $0 of VM $1 failed to change state"
+#~ msgstr "VM $1 的 NIC $0 改变状态失败"
+
+#~ msgid "Name contains invalid characters"
+#~ msgstr "名称包含无效的字符"
+
+#~ msgid "Name must not be empty"
+#~ msgstr "名称不能为空"
+
+#~ msgid "Name should not be empty"
+#~ msgstr "名称不应为空"
+
+#~ msgid "Name: "
+#~ msgstr "名称: "
+
+#~ msgid "Netmask"
+#~ msgstr "网络掩码"
+
+#~ msgid "Network $0 failed to get activated"
+#~ msgstr "网络 $0 激活失败"
+
+#~ msgid "Network $0 failed to get deactivated"
+#~ msgstr "网络 $0 取消激活失败"
+
+#~ msgid "Network boot (PXE)"
+#~ msgstr "网络引导 (PXE)"
+
+#~ msgid "Network file system"
+#~ msgstr "网络文件系统"
+
+#~ msgid "Network interface settings could not be saved"
+#~ msgstr "网络接口设置不能被保存"
+
+#~ msgid "Network selection does not support PXE."
+#~ msgstr "网络选择不支持 PXE。"
+
+#~ msgid "Networks"
+#~ msgstr "网络"
+
+#~ msgid "New volume name"
+#~ msgstr "新卷名称"
+
+#~ msgid "No VM is running or defined on this host"
+#~ msgstr "该主机上没有定义或运行虚拟机"
+
+#~ msgid "No connection available"
+#~ msgstr "没有可用连接"
+
+#~ msgid "No disks defined for this VM"
+#~ msgstr "没有为该虚拟机定义磁盘"
+
+#~ msgid "No network devices"
+#~ msgstr "没有网络设备"
+
+#~ msgid "No network interfaces defined for this VM"
+#~ msgstr "没有为此 VM 定义网络接口"
+
+#~ msgid "No network is defined on this host"
+#~ msgstr "没有在这个主机上定义网络"
+
+#~ msgid "No networks available"
+#~ msgstr "没有可用的网络"
+
+#~ msgid "No parent"
+#~ msgstr "没有上级"
+
+#~ msgid "No snapshots defined for this VM"
+#~ msgstr "没有为这个虚拟机定义快照"
+
+#~ msgid "No state"
+#~ msgstr "无状态"
+
+#~ msgid "No storage pool is defined on this host"
+#~ msgstr "没有在这个主机上定义存储池"
+
+#~ msgid "No storage pools available"
+#~ msgstr "没有可用的存储池"
+
+#~ msgid "No storage volumes defined for this storage pool"
+#~ msgstr "没有为这个存储池定义存储卷"
+
+#~ msgid "No virtual networks"
+#~ msgstr "没有虚拟网络"
+
+#~ msgid ""
+#~ "Non-persistent network cannot be deleted. It ceases to exists when it's "
+#~ "deactivated."
+#~ msgstr "非持久性网络无法删除。停用后它不再存在。"
+
+#~ msgid ""
+#~ "Non-persistent storage pool cannot be deleted. It ceases to exists when "
+#~ "it's deactivated."
+#~ msgstr "非持久性网络池无法删除。停用后它不再存在。"
+
+#~ msgid "None (isolated network)"
+#~ msgstr "无 (隔离的网络)"
+
+#~ msgid ""
+#~ "One or more selected volumes are used by domains. Detach the disks first "
+#~ "to allow volume deletion."
+#~ msgstr "一个或多个卷被域使用。需要先分离磁盘后才可以删除卷。"
+
+#~ msgid "Only editable when the guest is shut off"
+#~ msgstr "只有在客户机关闭后才可以编辑"
+
+#~ msgid "Open"
+#~ msgstr "打开"
+
+#~ msgid "Operating system"
+#~ msgstr "操作系统"
+
+#~ msgid "Operation is in progress"
+#~ msgstr "操作进行中"
+
+#~ msgid "Parent snapshot"
+#~ msgstr "上级快照"
+
+#~ msgid "Path on host's filesystem"
+#~ msgstr "主机文件系统上的路径"
+
+#~ msgid "Path to ISO file on host's file system"
+#~ msgstr "主机文件系统中到 ISO 文件的路径"
+
+#, fuzzy
+#~| msgid "Path to file on host's file system"
+#~ msgid "Path to cloud image file on host's file system"
+#~ msgstr "到主机文件系统上的文件的路径"
+
+#~ msgid "Path to file on host's file system"
+#~ msgstr "到主机文件系统上的文件的路径"
+
+#~ msgid "Paused"
+#~ msgstr "已暂停"
+
+#~ msgid "Persistence"
+#~ msgstr "持久"
+
+#~ msgid "Physical disk device"
+#~ msgstr "物理磁盘设备"
+
+#~ msgid "Physical disk device on host"
+#~ msgstr "主机上的物理磁盘设备"
+
+#~ msgid "Please choose a storage pool"
+#~ msgstr "请选择一个存储池"
+
+#~ msgid "Please choose a volume"
+#~ msgstr "请选择一个卷"
+
+#~ msgid "Please enter new volume name"
+#~ msgstr "请输入新的卷名"
+
+#~ msgid "Please start the virtual machine to access its console."
+#~ msgstr "请启动虚拟机来访问其控制台。"
+
+#~ msgid "Plug"
+#~ msgstr "插"
+
+#~ msgid "Pool needs to be active to create volume"
+#~ msgstr "池需要激活才能创建卷"
+
+#~ msgid "Pool type doesn't support volume creation"
+#~ msgstr "池类型不支持创建卷"
+
+#~ msgid "Pool's volumes are used by VMs "
+#~ msgstr "池的卷被 VM 使用 "
+
+#~ msgid "Preferred number of sockets to expose to the guest."
+#~ msgstr "向客户机公开的首选插槽数。"
+
+#~ msgid "Prefix"
+#~ msgstr "前缀"
+
+#~ msgid "Prefix length should not be empty"
+#~ msgstr "前缀长度不能为空"
+
+#~ msgid ""
+#~ "Previously taken snapshots allow you to revert to an earlier state if "
+#~ "something goes wrong"
+#~ msgstr "提前创建的快照使得您可以在出现问题的时候回复到较早的状态"
+
+#~ msgid "Product"
+#~ msgstr "产品"
+
+#~ msgid "Profile"
+#~ msgstr "配置集"
+
+#~ msgid "Protocol"
+#~ msgstr "协议"
+
+#~ msgid "Remote URL"
+#~ msgstr "远程 URL"
+
+#~ msgid "Remote viewer details"
+#~ msgstr "Remote viewer 详情"
+
+#~ msgid "Revert"
+#~ msgstr "恢复"
+
+#~ msgid "Revert to snapshot $0"
+#~ msgstr "恢复到快照 $0"
+
+#~ msgid ""
+#~ "Reverting to this snapshot will take the VM back to the time of the "
+#~ "snapshot and the current state will be lost, along with any data not "
+#~ "captured in a snapshot"
+#~ msgstr ""
+#~ "恢复到此快照将使虚拟机返回到创建快照时的状态,当前状态将丢失,同时丢失快照"
+#~ "中未包括的任何数据"
+
+#~ msgid "Root password"
+#~ msgstr "Root 密码"
+
+#~ msgid "Route to $0"
+#~ msgstr "路由到 $0"
+
+#~ msgid "Run unattended installation"
+#~ msgstr "执行无人值守安装"
+
+#~ msgid "Run when host boots"
+#~ msgstr "在主机引导时运行"
+
+#~ msgid "SPICE TLS port"
+#~ msgstr "SPICE TLS 端口"
+
+#~ msgid "SPICE address"
+#~ msgstr "SPICE 地址"
+
+#~ msgid "SPICE port"
+#~ msgstr "SPICE 端口"
+
+#~ msgid "Select console type"
+#~ msgstr "选择控制台类型"
+
+#~ msgid "Send key"
+#~ msgstr "发送按键"
+
+#~ msgid "Send non-maskable interrupt"
+#~ msgstr "发送不可屏蔽中断"
+
+#~ msgid "Serial console"
+#~ msgstr "串行控制台"
+
+#~ msgid "Set DHCP range"
+#~ msgstr "设置 DHCP 范围"
+
+#~ msgid "Set manually"
+#~ msgstr "手工设置"
+
+#~ msgid ""
+#~ "Setting the user passwords for unattended installation requires starting "
+#~ "the VM when creating it"
+#~ msgstr "为无人值守安装设置用户密码要求在创建 VM 的时候启动它"
+
+#~ msgid "Show additional options"
+#~ msgstr "显示额外操作"
+
+#~ msgid "Shut off"
+#~ msgstr "关闭"
+
+#~ msgid "Shut off the VM in order to edit firmware configuration"
+#~ msgstr "关闭 VM 以编辑固件配置"
+
+#~ msgid "Shutting down"
+#~ msgstr "正在关机"
+
+#~ msgid "Snapshot failed to be created"
+#~ msgstr "创建快照失败"
+
+#~ msgid "Source format"
+#~ msgstr "源格式"
+
+#~ msgid "Source path"
+#~ msgstr "源路径"
+
+#~ msgid "Source path should not be empty"
+#~ msgstr "源路径不能为空"
+
+#~ msgid "Source should start with http, ftp or nfs protocol"
+#~ msgstr "源应该以 http、ftp 或 nfs 协议开头"
+
+#~ msgid "Source volume group"
+#~ msgstr "源卷组"
+
+#~ msgid "Start libvirt"
+#~ msgstr "启动 libvirt"
+
+#~ msgid "Start pool when host boots"
+#~ msgstr "在主机引导时启动池"
+
+#~ msgid "Start should not be empty"
+#~ msgstr "开始不能为空"
+
+#~ msgid "Startup"
+#~ msgstr "启动"
+
+#~ msgid "Storage pool $0 failed to get activated"
+#~ msgstr "存储池 $0 激活失败"
+
+#~ msgid "Storage pool $0 failed to get deactivated"
+#~ msgstr "存储池 $0 取消激活失败"
+
+#~ msgid "Storage pool failed to be created"
+#~ msgstr "创建存储池失败"
+
+#~ msgid "Storage pool name"
+#~ msgstr "存储池名"
+
+#~ msgid "Storage size must not be 0"
+#~ msgstr "储存空间大小不能为 0"
+
+#~ msgid ""
+#~ "Storage volume size must not exceed the storage pool's capacity ($0 $1)"
+#~ msgstr "存储卷大小不能超过存储池的容量($0 $1)"
+
+#~ msgid "Storage volumes could not be deleted"
+#~ msgstr "存储卷不能被删除"
+
+#~ msgid "Suspended (PM)"
+#~ msgstr "已挂起 (电源管理)"
+
+#~ msgid "Target path"
+#~ msgstr "目标路径"
+
+#~ msgid "Target path should not be empty"
+#~ msgstr "目标路径不能为空"
+
+#~ msgid "The VM is running and will be forced off before deletion."
+#~ msgstr "虚拟机正在运行并将在被删除前强制关闭。"
+
+#~ msgid "The VM needs to be running or shut off to detach this device"
+#~ msgstr "虚拟机需要运行或关闭才能分离该设备"
+
+#~ msgid "The directory on the server being exported"
+#~ msgstr "服务器上的目录被导出"
+
+#~ msgid "The pool is empty"
+#~ msgstr "池为空"
+
+#~ msgid ""
+#~ "The selected operating system does not support unattended installation"
+#~ msgstr "所选操作系统不支持无人值守安装"
+
+#~ msgid ""
+#~ "The selected operating system has minimum memory requirement of $0 $1"
+#~ msgstr "所选操作系统的最小内存要求是 $0 $1"
+
+#~ msgid ""
+#~ "The selected operating system has minimum storage size requirement of $0 "
+#~ "$1"
+#~ msgstr "所选操作系统的最小存储要求是 $0 $1"
+
+#~ msgid "The storage pool could not be deleted"
+#~ msgstr "存储池不能被删除"
+
+#~ msgid "This VM is transient. Shut it down if you wish to delete it."
+#~ msgstr "此 VM 是瞬态的。如果希望删除它,请将其关机。"
+
+#~ msgid "This volume is already used by: "
+#~ msgstr "这个卷已被使用: "
+
+#~ msgid "Threads per core"
+#~ msgstr "每个内核的线程数"
+
+#~ msgid "Transient VMs don't support editing firmware configuration"
+#~ msgstr "瞬态 VM 不支持编辑固件配置"
+
+#~ msgid "Type ID"
+#~ msgstr "类型 ID"
+
+#~ msgid "Unique name"
+#~ msgstr "唯一名称"
+
+#~ msgid "Unique network name"
+#~ msgstr "唯一的网络名称"
+
+#~ msgid "Unknown firmware"
+#~ msgstr "未知固件"
+
+#~ msgid "Unplug"
+#~ msgstr "拔"
+
+#~ msgid "Up to $0 $1 available on the default location"
+#~ msgstr "在默认位置有多达 $0 $1 可用"
+
+#~ msgid "Up to $0 $1 available on the host"
+#~ msgstr "在主机上多达 $0 $1 可用"
+
+#~ msgid "Url"
+#~ msgstr "Url"
+
+#~ msgid "User login must not be empty when user password is set"
+#~ msgstr "在用户密码设置后用户登录不能为空"
+
+#~ msgid "User password must not be empty when user login is set"
+#~ msgstr "用户登录设定后用户密码不能为空"
+
+#~ msgid "VCPU settings could not be saved"
+#~ msgstr "VCPU 设置不能被保存"
+
+#~ msgid "VM $0 already exists"
+#~ msgstr "VM $0 已存在"
+
+#~ msgid "VM $0 does not exist on $1 connection"
+#~ msgstr "VM $0 在 $1 连接中不存在"
+
+#~ msgid "VM $0 failed to force reboot"
+#~ msgstr "VM $0 强制重启失败"
+
+#~ msgid "VM $0 failed to force shutdown"
+#~ msgstr "VM $0 强制关闭失败"
+
+#~ msgid "VM $0 failed to get deleted"
+#~ msgstr "VM $0 删除失败"
+
+#~ msgid "VM $0 failed to get installed"
+#~ msgstr "VM $0 安装失败"
+
+#~ msgid "VM $0 failed to reboot"
+#~ msgstr "VM $0 重启失败"
+
+#~ msgid "VM $0 failed to resume"
+#~ msgstr "VM $0 恢复失败"
+
+#~ msgid "VM $0 failed to send NMI"
+#~ msgstr "VM $0 发送 NMI 失败"
+
+#~ msgid "VM $0 failed to shutdown"
+#~ msgstr "VM $0 关闭失败"
+
+#~ msgid "VM $0 failed to start"
+#~ msgstr "VM $0 启动失败"
+
+#~ msgid "VM state"
+#~ msgstr "VM 状态"
+
+#~ msgid "VNC TLS port"
+#~ msgstr "VNC TLS 端口"
+
+#~ msgid "VNC address"
+#~ msgstr "VNC 地址"
+
+#~ msgid "VNC console"
+#~ msgstr "VNC 控制台"
+
+#~ msgid "VNC port"
+#~ msgstr "VNC 端口"
+
+#~ msgid "Virtual Machines"
+#~ msgstr "虚拟机"
+
+#~ msgid "Virtual machines"
+#~ msgstr "虚拟机"
+
+#~ msgid "Virtual machines management"
+#~ msgstr "虚拟机管理"
+
+#~ msgid "Virtual network"
+#~ msgstr "虚拟网络"
+
+#~ msgid "Virtual network failed to be created"
+#~ msgstr "虚拟网络创建失败"
+
+#~ msgid "Virtualization service (libvirt) is not active"
+#~ msgstr "虚拟化服务(libvirt)未激活"
+
+#~ msgid "Volume group name should not be empty"
+#~ msgstr "卷组名称不应为空"
+
+#~ msgid "WWPN"
+#~ msgstr "WWPN"
+
+#~ msgid "Writeable"
+#~ msgstr "可写"
+
+#~ msgid "Writeable and shared"
+#~ msgstr "可写和共享"
+
+#~ msgid "Yesterday"
+#~ msgstr "昨天"
+
+#~ msgid "You need to select the most closely matching operating system"
+#~ msgstr "您需要选择最匹配的操作系统"
+
+#~ msgid "cdrom"
+#~ msgstr "光驱"
+
+#~ msgid "disabled"
+#~ msgstr "已禁用"
+
+#~ msgid "down"
+#~ msgstr "已关闭"
+
+#~ msgid "enabled"
+#~ msgstr "已启用"
+
+#~ msgid "ethernet"
+#~ msgstr "以太网"
+
+#~ msgid "host device"
+#~ msgstr "主机设备"
+
+#~ msgid "host passthrough"
+#~ msgstr "主机透传"
+
+#~ msgid "hostdev"
+#~ msgstr "hostdev"
+
+#~ msgid "iSCSI direct target"
+#~ msgstr "iSCSI 直接目标"
+
+#~ msgid "iSCSI initiator IQN"
+#~ msgstr "iSCSI initiator IQN"
+
+#~ msgid "iSCSI target IQN"
+#~ msgstr "iSCSI 目标 IQN"
+
+#~ msgid "iso"
+#~ msgstr "iso"
+
+#~ msgid "libvirt"
+#~ msgstr "libvirt"
+
+#~ msgid "mcast"
+#~ msgstr "MCAST"
+
+#~ msgid "no"
+#~ msgstr "否"
+
+#~ msgid "no state saved"
+#~ msgstr "无保存的状态"
+
+#~ msgid "pxe"
+#~ msgstr "pxe"
+
+#~ msgid "qcow2"
+#~ msgstr "qcow2"
+
+#~ msgid "qemu"
+#~ msgstr "qemu"
+
+#~ msgid "redirected device"
+#~ msgstr "重定向设备"
+
+#~ msgid "server"
+#~ msgstr "服务器"
+
+#~ msgid "up"
+#~ msgstr "运行中"
+
+#~ msgid "vCPU count"
+#~ msgstr "vCPU 数"
+
+#~ msgid "vCPU maximum"
+#~ msgstr "vCPU 的最大值"
+
+#~ msgid "vCPUs"
+#~ msgstr "vCPU"
+
+#~ msgid "vhostuser"
+#~ msgstr "vhostuser"
+
+#~ msgid "view more..."
+#~ msgstr "查看更多......"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "clone VMs"
+#~ msgstr "为了克隆虚拟机,需要在系统中安装 virt-install 软件包"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to "
+#~ "create new VMs"
+#~ msgstr "为了创建新虚拟机需要在系统上安装 virt-install 软件包"
+
+#~ msgid ""
+#~ "virt-install package needs to be installed on the system in order to edit "
+#~ "this attribute"
+#~ msgstr "为了编辑这个属性,需要在系统中安装 virt-install 软件包"
+
+#~ msgid "vm"
+#~ msgstr "vm"
+
+#~ msgid "Local install media"
+#~ msgstr "本地安装介质"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Please set a root password"
+#~ msgstr "请设置一个 root 用户的密码"
+
+#~ msgid "21st"
+#~ msgstr "第21"
+
+#~ msgid "22nd"
+#~ msgstr "第22"
+
+#~ msgid "23rd"
+#~ msgstr "第23"
+
+#~ msgctxt "time-delay"
+#~ msgid "After"
+#~ msgstr "后"
+
+#~ msgid "Friday"
+#~ msgstr "星期五"
+
+#~ msgid "Hour : Minute"
+#~ msgstr "小时:分钟"
+
+#~ msgid "Hour needs to be a number between 0-23"
+#~ msgstr "小时需要是在 0-23 间的数字"
+
+#~ msgid "Invalid date format."
+#~ msgstr "无效的数据格式。"
+
+#~ msgid "Invalid number."
+#~ msgstr "无效的编号。"
+
+#~ msgid "Monday"
+#~ msgstr "星期一"
+
+#~ msgid "Repeat hourly"
+#~ msgstr "每小时重复"
+
+#~ msgid "Repeat yearly"
+#~ msgstr "每年重复"
+
+#~ msgid "Saturday"
+#~ msgstr "星期六"
+
+#~ msgid "Sunday"
+#~ msgstr "星期日"
+
+#~ msgid ""
+#~ "This day doesn't exist in all months.<br> The timer will only be executed "
+#~ "in months that have 31st."
+#~ msgstr "这天不是所有月份都有。<br> 定时器将只会在有 31 号的月份执行。"
+
+#~ msgid "Thursday"
+#~ msgstr "星期四"
+
+#~ msgid "Tuesday"
+#~ msgstr "星期二"
+
+#~ msgid "$0 update"
+#~ msgid_plural "$0 updates"
+#~ msgstr[0] "$0 个更新"
+
+#~ msgid "$1 security fix"
+#~ msgid_plural "$1 security fixes"
+#~ msgstr[0] "$1 安全修正"
+
+#~ msgid "Managing storage devices"
+#~ msgstr "管理存储设备"
+
+#~ msgid "Restart now"
+#~ msgstr "立刻重启"
+
+#~ msgid "Configure"
+#~ msgstr "配置"
+
+#~ msgid "Network boot is available only when using system connection"
+#~ msgstr "网络引导仅在使用系统连接时可用"
+
+#~ msgctxt "page-title"
+#~ msgid "Networking"
+#~ msgstr "网络"
+
+#~ msgid "No snapshots"
+#~ msgstr "没有快照"
+
+#, fuzzy
+#~| msgid "No such file or directory '$0'"
+#~ msgid "No such file found in directory '$0'"
+#~ msgstr "没有该文件或目录 '$0'"
+
+#~ msgid "No updates pending"
+#~ msgstr "没有待完成的更新"
+
+#, fuzzy
+#~| msgid "ssh server is empty"
+#~ msgid "This directory is empty"
+#~ msgstr "ssh 服务器为空"
+
+#~ msgid "This pool type does not support storage volume creation"
+#~ msgstr "这个池类型不支持创建存储卷"
+
+#~ msgid "undefined"
+#~ msgstr "未定义"
+
+#~ msgid "Incorrect host key"
+#~ msgstr "不正确的主机密钥"
+
+#~ msgid ""
+#~ "The authenticity of host {{#strong}}{{host}}{{/strong}} can't be "
+#~ "established. Are you sure you want to continue connecting?"
+#~ msgstr ""
+#~ "主机 {{#strong}}{{host}}{{/strong}} 的认证无法完成。确认想要继续连接?"
+
+#~ msgid ""
+#~ "The key of {{#strong}}{{host}}{{/strong}} does not match the key "
+#~ "previously in use. Unless this machine was recently replaced, it is "
+#~ "likely that someone is trying to attack your connection to this machine."
+#~ msgstr ""
+#~ "{{#strong}}{{host}}{{/strong}} 的密钥与之前使用的不匹配。除非该主机最近被"
+#~ "替换,否则可能有人正在攻击与该主机的连接。"
+
+#~ msgid "Unknown host key"
+#~ msgstr "未知主机密钥"
+
+#~ msgid "Address:"
+#~ msgstr "地址:"
+
+#~ msgid "At least $0 disks are needed."
+#~ msgstr "至少需要 $0 块磁盘。"
+
+#~ msgid "Connect with any SPICE or VNC viewer application."
+#~ msgstr "使用任何 SPICE 或 VNC 查看器应用来连接。"
+
+#~ msgid "Download the MSI from $0"
+#~ msgstr "从 $0 下载 MSI"
+
+#~ msgid "Graphics console (VNC)"
+#~ msgstr "图形控制台(VNC)"
+
+#~ msgid "Graphics console in desktop viewer"
+#~ msgstr "Desktop Viewer 中的图形控制台"
+
+#~ msgid "No console defined for this virtual machine."
+#~ msgstr "没有为该虚拟机定义控制台。"
+
+#~ msgid "SPICE"
+#~ msgstr "SPICE"
+
+#~ msgid "VNC"
+#~ msgstr "VNC"
+
+#~ msgid "Entry"
+#~ msgstr "条目"
+
+#~ msgid ""
+#~ "Your browser will disconnect, but this does not affect the update "
+#~ "process. You can reconnect in a few moments to continue watching the "
+#~ "progress."
+#~ msgstr ""
+#~ "您的浏览器将断开连接,但这不会影响更新过程。您可以在几分钟后重新连接以继续"
+#~ "观察进度。"
+
+#~ msgid "and restart the machine automatically."
+#~ msgstr "并自动重启机器。"
+
+#~ msgid "Dashboard"
+#~ msgstr "仪表板"
+
+#~ msgid "Description input text"
+#~ msgstr "描述输入文本"
+
+#~ msgid "FAILED"
+#~ msgstr "失败"
+
+#~ msgid "Lost connection. Trying to reconnect"
+#~ msgstr "连接失败。请重新连接"
+
+#~ msgid "Name input text"
+#~ msgstr "名称输入文本"
+
+#~ msgctxt "label"
+#~ msgid "Network"
+#~ msgstr "网络"
+
+#~ msgid "Please enter new volume size"
+#~ msgstr "请输入新的卷大小"
+
+#~ msgid "Servers"
+#~ msgstr "服务器"
+
+#~ msgid ""
+#~ "You are currently connected directly to this server. You cannot delete it."
+#~ msgstr "您当前已直接连接到该服务器。您不能删除它。"
+
+#~ msgid "$0 bit"
+#~ msgid_plural "$0 bits"
+#~ msgstr[0] "$0 比特"
+
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 字节"
+
+#~ msgctxt "memory"
+#~ msgid "$0 byte"
+#~ msgid_plural "$0 bytes"
+#~ msgstr[0] "$0 字节"
+
+#, fuzzy
+#~| msgid "Bond settings"
+#~ msgid "Bond settings "
+#~ msgstr "绑定设置"
+
+#, fuzzy
+#~| msgid "IP settings"
+#~ msgid "IP settings "
+#~ msgstr "IP 设置"
+
+#~ msgid "${size} ${desc}"
+#~ msgstr "${size} ${desc}"
+
+#~ msgid "--"
+#~ msgstr "--"
+
+#~ msgid ""
+#~ "Cockpit had an unexpected internal error. <br/><br/>You can try "
+#~ "restarting Cockpit by pressing refresh in your browser. The javascript "
+#~ "console contains details about this error (<b>Ctrl-Shift-J</b> in most "
+#~ "browsers)."
+#~ msgstr ""
+#~ "Cockpit 遇到意外的内部错误。 <br/><br/>可以尝试通过刷新浏览器重启 "
+#~ "Cockpit。Javascript 控制台包含关于该错误的详情 (在大多数浏览器中按"
+#~ "<b>Ctrl-Shift-J</b> )。"
+
+#~ msgid "The VM crashed."
+#~ msgstr "虚拟机已崩溃。"
+
+#~ msgid "The VM is down."
+#~ msgstr "虚拟机已停止。"
+
+#~ msgid "The VM is going down."
+#~ msgstr "虚拟机将要关机。"
+
+#~ msgid "The VM is idle."
+#~ msgstr "虚拟机已休眠。"
+
+#~ msgid "The VM is in process of dying (shut down or crash is not completed)."
+#~ msgstr "虚拟机处于停止中(关机或崩溃未完成)。"
+
+#~ msgid "The VM is paused."
+#~ msgstr "虚拟机已暂停。"
+
+#~ msgid "The VM is running."
+#~ msgstr "虚拟机正在运行。"
+
+#~ msgid "The VM is suspended by guest power management."
+#~ msgstr "虚拟机已被客户机电源管理暂停。"
+
+#~ msgid "inactive"
+#~ msgstr "未激活"
+
+#~ msgid "Avatar"
+#~ msgstr "头像"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in user. "
+#~ "If you enter a different username, that user will always be used when "
+#~ "connecting to this machine."
+#~ msgstr ""
+#~ "以当前登录用户来连接到该主机则保留为空。如果输入另一个用户名,连接到该主机"
+#~ "时通常将会使用该用户。"
+
+#~ msgid "2 min"
+#~ msgstr "2 分钟"
+
+#~ msgid "3 min"
+#~ msgstr "3 分钟"
+
+#~ msgid "4 min"
+#~ msgstr "4 分钟"
+
+#~ msgid "CPU graph"
+#~ msgstr "CPU图"
+
+#~ msgctxt "page-title"
+#~ msgid "CPU status"
+#~ msgstr "CPU 状态"
+
+#~ msgid "Cached"
+#~ msgstr "已缓存"
+
+#~ msgid "Graphs"
+#~ msgstr "图表"
+
+#~ msgid "I/O wait"
+#~ msgstr "I/O 等待"
+
+#~ msgid "Kernel"
+#~ msgstr "内核"
+
+#~ msgctxt "page-title"
+#~ msgid "Memory"
+#~ msgstr "内存"
+
+#~ msgid "Memory & swap usage"
+#~ msgstr "内存和交换空间的使用"
+
+#~ msgid "Memory graph"
+#~ msgstr "内存图"
+
+#~ msgid "Network traffic"
+#~ msgstr "网络流量"
+
+#~ msgid "Nice"
+#~ msgstr "Nice"
+
+#~ msgid "Synchronize users"
+#~ msgstr "同步用户"
+
+#~ msgid "Usage graphs"
+#~ msgstr "使用图"
+
+#~ msgid "View graphs"
+#~ msgstr "查看图表"
+
+#~ msgid "idle"
+#~ msgstr "休眠"
+
+#~ msgid "paused"
+#~ msgstr "已暂停"
+
+#~ msgid "running"
+#~ msgstr "运行中"
+
+#~ msgid "shut off"
+#~ msgstr "关闭"
+
+#~ msgid "shutdown"
+#~ msgstr "关机"
+
+#~ msgid "Add a new host"
+#~ msgstr "添加一个新主机"
+
+#~ msgid "Available"
+#~ msgstr "可用的"
+
+#~ msgid "Enter IP address or hostname"
+#~ msgstr "输入 IP 地址或主机名"
+
+#~ msgid "Network interfaces"
+#~ msgstr "网络接口"
+
+#~ msgid ""
+#~ "This version of cockpit-ws does not support connecting to a host with an "
+#~ "alternate user or port"
+#~ msgstr "该版本的 cockpit-ws 不支持连接到有交替的用户或端口的主机"
+
+#~ msgid ""
+#~ "To try a different port you will need to upgrade cockpit-ws to a newer "
+#~ "version."
+#~ msgstr "为了尝试一个不同的端口,需要把 cockpit-ws 升级到一个新版本。"
+
+#~ msgid "$0 template"
+#~ msgstr "$0 模板"
+
+#~ msgid "Instance of template: "
+#~ msgstr "模板实例: "
+
+#~ msgid "Instantiate"
+#~ msgstr "实例"
+
+#~ msgid "$0 shares"
+#~ msgstr "$0 共享"
+
+#~ msgid "All In One"
+#~ msgstr "多合一"
+
+#~ msgid "Always"
+#~ msgstr "总是"
+
+#~ msgid "Author"
+#~ msgstr "作者"
+
+#~ msgid "Bad data passed for passwd1 mechanism"
+#~ msgstr "为 passwd1 传递了坏数据"
+
+#~ msgid "CPU priority"
+#~ msgstr "CPU 优先级"
+
+#~ msgid "Can&rsquo;t connect to Docker"
+#~ msgstr "无法连接到 Docker"
+
+#~ msgid "Change resource limits"
+#~ msgstr "修改资源限制"
+
+#~ msgid "Change resources limits"
+#~ msgstr "修改资源限制"
+
+#~ msgid "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}."
+#~ msgstr "Cockpit 无法登录到 {{#strong}}{{host}}{{/strong}}."
+
+#~ msgid ""
+#~ "Cockpit was unable to log into {{#strong}}{{host}}{{/strong}}. You can "
+#~ "change your authentication credentials below. {{#can_sync}}You may prefer "
+#~ "to {{#sync_link}}synchronize accounts and passwords{{/sync_link}}.{{/"
+#~ "can_sync}}"
+#~ msgstr ""
+#~ "Cockpit 无法登录到 {{#strong}}{{host}}{{/strong}}。可以变更以下验证凭证。"
+#~ "{{#can_sync}}您也可以尝试{{#sync_link}}同步账号和密码{{/sync_link}}。{{/"
+#~ "can_sync}}"
+
+#~ msgid "Combined memory usage"
+#~ msgstr "内存使用率总计"
+
+#~ msgid "Combined usage of $0 CPU core"
+#~ msgid_plural "Combined usage of $0 CPU cores"
+#~ msgstr[0] "总计使用 $0 CPU 内核"
+
+#~ msgid "Command can't be empty"
+#~ msgstr "命令不能为空"
+
+#~ msgid "Command:"
+#~ msgstr "命令:"
+
+#~ msgid "Commit"
+#~ msgstr "提交"
+
+#~ msgid "Commit image"
+#~ msgstr "提交镜像"
+
+#~ msgid "Connecting to Docker"
+#~ msgstr "正在连接 Docker"
+
+#~ msgid "Container name"
+#~ msgstr "容器名称"
+
+#~ msgid ""
+#~ "Container is currently marked as not running, but regular stopping failed."
+#~ msgstr "容器当前标记为未运行, 但正常停止失败。"
+
+#~ msgid "Container is currently running."
+#~ msgstr "当前容器正在运行。"
+
+#~ msgid "Container:"
+#~ msgstr "容器:"
+
+#~ msgid "Containers"
+#~ msgstr "容器"
+
+#~ msgctxt "page-title"
+#~ msgid "Containers"
+#~ msgstr "容器"
+
+#~ msgid "Couldn't change user groups"
+#~ msgstr "不能修改用户组"
+
+#~ msgid "Couldn't change user password"
+#~ msgstr "不能修改用户密码"
+
+#~ msgid "Couldn't connect to the machine"
+#~ msgstr "不能连接至主机"
+
+#~ msgid "Couldn't create new users"
+#~ msgstr "不能创建新用户"
+
+#~ msgid "Couldn't list local users"
+#~ msgstr "不能列出本地用户"
+
+#~ msgid "Couldn't list users"
+#~ msgstr "不能列出用户"
+
+#~ msgid "Couldn't load user data"
+#~ msgstr "不能加载用户数据"
+
+#~ msgid "Created:"
+#~ msgstr "创建于:"
+
+#~ msgid "Docker containers"
+#~ msgstr "Docker 容器"
+
+#~ msgid "Docker is not installed or activated on the system"
+#~ msgstr "系统中的 Docker 未安装或未激活"
+
+#~ msgid "Duplicate alias"
+#~ msgstr "重复别名"
+
+#~ msgid "Duplicate port"
+#~ msgstr "重复端口"
+
+#~ msgid "Enter passphrase to add identity to ssh-agent"
+#~ msgstr "输入密码以向 ssh-agent 添加身份"
+
+#~ msgid ""
+#~ "Entering a different password here means you will need to retype it every "
+#~ "time you reconnect to this machine"
+#~ msgstr "在这里输入不同的密码意味着将要在每次重新连接该主机时重新输入"
+
+#~ msgid "Environment"
+#~ msgstr "环境"
+
+#~ msgid "Error loading users: {{perm_failed}}"
+#~ msgstr "加载用户: {{perm_failed}} 出错"
+
+#~ msgid "Error message from Docker:"
+#~ msgstr "来自 Docker 的错误消息:"
+
+#~ msgid "Everything"
+#~ msgstr "所有"
+
+#~ msgid "Exited $ExitCode"
+#~ msgstr "已退出 $ExitCode"
+
+#~ msgid "Expose container ports"
+#~ msgstr "暴露容器端口"
+
+#~ msgid "Failed to start Docker: $0"
+#~ msgstr "启动 Docker 失败: $0"
+
+#~ msgid "Failed to stop Docker scope: $0"
+#~ msgstr "无法停止 Docker 范围: $0"
+
+#~ msgid "Get new image"
+#~ msgstr "获取新镜像"
+
+#~ msgid "IP address:"
+#~ msgstr "IP 地址:"
+
+#~ msgid "IP prefix length:"
+#~ msgstr "IP 前缀长度:"
+
+#~ msgid "Id"
+#~ msgstr "ID"
+
+#~ msgid "Id:"
+#~ msgstr "编号:"
+
+#~ msgid "Image"
+#~ msgstr "镜像"
+
+#~ msgid "Image $0"
+#~ msgstr "镜像 $0"
+
+#~ msgid "Image search"
+#~ msgstr "搜索镜像"
+
+#~ msgid "Image:"
+#~ msgstr "镜像:"
+
+#~ msgid "Images"
+#~ msgstr "镜像"
+
+#~ msgctxt "page-title"
+#~ msgid "Images"
+#~ msgstr "镜像"
+
+#~ msgid "Images and running containers"
+#~ msgstr "镜像和运行的容器"
+
+#~ msgid ""
+#~ "In order to synchronize users, you need to log in to {{#strong}}{{host}}"
+#~ "{{/strong}}."
+#~ msgstr "为了同步用户,需要登录到 {{#strong}}{{host}}{{/strong}}。"
+
+#~ msgid "Invalid port"
+#~ msgstr "无效端口"
+
+#~ msgid "Kerberos Ticket"
+#~ msgstr "Kerberos 权证"
+
+#~ msgid ""
+#~ "Leave blank to connect to this machine as the currently logged in "
+#~ "user{{#default_user}} ({{default_user}}){{/default_user}}. If you enter a "
+#~ "different username, that user will always be used connecting to this "
+#~ "machine."
+#~ msgstr ""
+#~ "以当前登录的用户 {{#default_user}} ({{default_user}}){{/default_user}} 来"
+#~ "连接该主机则保留为空。如果输入一个不同的用户名,将总会用那个用户连接该主"
+#~ "机。"
+
+#~ msgid "Link to another container"
+#~ msgstr "链接到另一个容器"
+
+#~ msgid "Links:"
+#~ msgstr "链接:"
+
+#~ msgid "Login Password"
+#~ msgstr "登录密码"
+
+#~ msgid "MAC address:"
+#~ msgstr "MAC 地址:"
+
+#~ msgid "Memory limit"
+#~ msgstr "内存限制"
+
+#~ msgid "Mount container volumes"
+#~ msgstr "挂载容器卷"
+
+#~ msgid "No container specified"
+#~ msgstr "未指定容器"
+
+#~ msgid "No containers"
+#~ msgstr "没有容器"
+
+#~ msgid "No containers that match the current filter"
+#~ msgstr "没有容器匹配当前的过滤条件"
+
+#~ msgid "No images"
+#~ msgstr "没有镜像"
+
+#~ msgid "No images that match the current filter"
+#~ msgstr "没有匹配当前过滤器的镜像"
+
+#~ msgid "No results for $0"
+#~ msgstr "没有 $0 的结果"
+
+#, fuzzy
+#~| msgid "No images that match the current filter"
+#~ msgid "No results that match the filter criteria"
+#~ msgstr "没有匹配当前过滤器的镜像"
+
+#~ msgid "No running containers"
+#~ msgstr "没有运行的容器"
+
+#~ msgid "No running containers that match the current filter"
+#~ msgstr "没有匹配当前过滤器的容器"
+
+#~ msgid "Not authorized to access Docker on this system"
+#~ msgstr "未授权访问该系统的 Docker"
+
+#~ msgid "On failure, retry $0 time"
+#~ msgid_plural "On failure, retry $0 times"
+#~ msgstr[0] "失败时, 重试 $0 次"
+
+#~ msgid "Please confirm forced deletion of $0"
+#~ msgstr "请确认强制删除 $0"
+
+#~ msgid "Please try another term"
+#~ msgstr "请尝试另一个终端"
+
+#~ msgid "Ports:"
+#~ msgstr "端口:"
+
+#~ msgid "Problems"
+#~ msgstr "问题"
+
+#~ msgid "ReadOnly"
+#~ msgstr "只读"
+
+#~ msgid "ReadWrite"
+#~ msgstr "读写"
+
+#~ msgid "Repository"
+#~ msgstr "仓库"
+
+#~ msgid "Restart policy:"
+#~ msgstr "重启策略:"
+
+#~ msgid "Retries:"
+#~ msgstr "重试次数:"
+
+#~ msgid "Reuse my password for remote connections"
+#~ msgstr "重用我的密码于远程连接"
+
+#~ msgid ""
+#~ "Select the users that you would like to be synchronized with {{#strong}}"
+#~ "{{host}}{{/strong}}"
+#~ msgstr "选择您需要与 {{#strong}}{{host}}{{/strong}} 同步的用户"
+
+#~ msgid "Set container environment variables"
+#~ msgstr "设置容器环境变量"
+
+#~ msgid "Severity:"
+#~ msgstr "严重性:"
+
+#~ msgid "Show all containers"
+#~ msgstr "显示所有容器"
+
+#~ msgid "Start Docker"
+#~ msgstr "启动 Docker"
+
+#~ msgid "State:"
+#~ msgstr "阶段:"
+
+#~ msgid "Synchronize"
+#~ msgstr "同步"
+
+#~ msgid "Tag"
+#~ msgstr "标签"
+
+#~ msgid "Tags"
+#~ msgstr "标记"
+
+#~ msgid ""
+#~ "The following containers depend on this image and will become unusable."
+#~ msgstr "以下容器依赖于此镜像,将变得不可用。"
+
+#~ msgid "This image does not exist."
+#~ msgstr "该镜像不存在。"
+
+#~ msgid "Type a password"
+#~ msgstr "输入密码"
+
+#~ msgid "Type to filter…"
+#~ msgstr "输入内容来过滤…"
+
+#~ msgid "Unless stopped"
+#~ msgstr "直到被停止"
+
+#~ msgid "Unsupported setup mechanism"
+#~ msgstr "不支持的设置机制"
+
+#~ msgid "Up since $0"
+#~ msgstr "运行自 $0"
+
+#~ msgid "Used by containers"
+#~ msgstr "被容器使用"
+
+#~ msgid "Using available credentials"
+#~ msgstr "使用可用的凭证"
+
+#~ msgid "Volumes"
+#~ msgstr "卷"
+
+#~ msgid "Volumes:"
+#~ msgstr "卷:"
+
+#~ msgid "With terminal"
+#~ msgstr "带有终端"
+
+#~ msgid ""
+#~ "You are connected to {{#strong}}{{host}}{{/strong}}, however in order to "
+#~ "synchronize users, a user with superuser privileges is required."
+#~ msgstr ""
+#~ "已经连接到 {{#strong}}{{host}}{{/strong}},然而为了同步用户,需要一个特权"
+#~ "用户。"
+
+#~ msgid "default"
+#~ msgstr "默认"
+
+#~ msgid "key"
+#~ msgstr "密钥"
+
+#~ msgid "search by name, namespace or description"
+#~ msgstr "通过名称、命名空间或描述来搜索"
+
+#~ msgid "shares"
+#~ msgstr "共享"
+
+#~ msgid "to host port"
+#~ msgstr "到主机端口"
+
+#~ msgid "value"
+#~ msgstr "值"
+
+#~ msgid "Master"
+#~ msgstr "主"
+
+#~ msgid "Password for $0"
+#~ msgstr "$0 的密码"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage servers"
+#~ msgstr "用户 <b>$0</b> 不允许管理服务器"
+
+#~ msgctxt "page-title"
+#~ msgid "Accounts"
+#~ msgstr "账户"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify accounts"
+#~ msgstr "用户 <b>$0</b> 不允许修改账户"
+
+#~ msgid "Unable to delete root account"
+#~ msgstr "不能删除 root 账户"
+
+#~ msgid "Unable to rename root account"
+#~ msgstr "不能重命名 root 账户"
+
+#~ msgid "Not authorized to add a new zone"
+#~ msgstr "无权添加新区"
+
+#~ msgid "Not authorized to add services to zone $0"
+#~ msgstr "无权将服务添加到区 $0"
+
+#~ msgid "Not authorized to remove service $0"
+#~ msgstr "无权删除服务 $0"
+
+#~ msgid "Account Settings"
+#~ msgstr "账户设置"
+
+#~ msgid "Add Machine to Dashboard"
+#~ msgstr "把机器添加到仪表板"
+
+#~ msgid "Machines"
+#~ msgstr "主机"
+
+#~ msgid "Login has escalated admin privileges"
+#~ msgstr "登录已升级管理员权限"
+
+#~ msgid ""
+#~ "Password not usable for privileged tasks or to connect to other machines"
+#~ msgstr "密码对特权任务和连接到其他主机不可用"
+
+#~ msgid "Privileged"
+#~ msgstr "有特权的"
+
+#~ msgid ""
+#~ "Reuse my password for privileged tasks and to connect to other machines"
+#~ msgstr "为特权任务和连接其他主机重用我的密码"
+
+#~ msgid "The user $0 is not permitted to modify hostnames"
+#~ msgstr "用户 $0 不允许修改主机名"
+
+#~ msgid "The user $0 is not permitted to shutdown or restart this server"
+#~ msgstr "用户 $0 不允许关闭或重启该服务器"
+
+#~ msgid "The user <b>$0</b> is not permitted to change profiles"
+#~ msgstr "用户 <b>$0</b> 不允许修改档案"
+
+#~ msgid "The user <b>$0</b> is not permitted to manage storage"
+#~ msgstr "用户 <b>$0</b> 不允许管理存储"
+
+#~ msgid "The user <b>$0</b> is not permitted to modify network settings"
+#~ msgstr "用户 <b>$0</b> 不允许修改网络设置"
+
+#, fuzzy
+#~| msgid "Required by "
+#~ msgid "Required by $0"
+#~ msgstr "要求自 "
+
+#~ msgid "Automatic Startup"
+#~ msgstr "自动启动"
+
+#~ msgid "Last Trigger"
+#~ msgstr "最近的触发器"
+
+#~ msgid "Next Run"
+#~ msgstr "下次运行"
+
+#~ msgid "The user <b>$0</b> does not have permissions for creating timers"
+#~ msgstr "用户 <b>$0</b> 没有权限创建定时器"
+
+#~ msgid "Connecting"
+#~ msgstr "连接"
+
+#~ msgid "About Cockpit"
+#~ msgstr "关于 Cockpit"
+
+#~ msgid " (shared with the OS)"
+#~ msgstr " (与操作系统共享)"
+
+#~ msgid "$0 Vulnerability"
+#~ msgid_plural "$0 Vulnerabilities"
+#~ msgstr[0] "$0 安全漏洞"
+
+#~ msgid "Add Additional Storage"
+#~ msgstr "添加额外的存储"
+
+#~ msgid "Add Storage"
+#~ msgstr "添加存储"
+
+#~ msgid "Additional Storage"
+#~ msgstr "额外的存储"
+
+#~ msgid ""
+#~ "All data on selected disks will be erased and disks will be added to the "
+#~ "storage pool."
+#~ msgstr "所选磁盘上的所有数据将会被删除,磁盘将会被添加到存储池。"
+
+#~ msgid "Configure storage..."
+#~ msgstr "配置存储..."
+
+#~ msgid "Could not add all disks"
+#~ msgstr "无法添加所有磁盘"
+
+#~ msgid "Erase containers and reset storage pool"
+#~ msgstr "擦除容器并重置存储池"
+
+#~ msgid "Information about the Docker storage pool is not available."
+#~ msgstr "关于容器存储池的信息不可用。"
+
+#~ msgid "Local Disks"
+#~ msgstr "本地磁盘"
+
+#~ msgid "No additional local storage found."
+#~ msgstr "没有找到额外的本地存储。"
+
+#~ msgid "Reformat and add disks"
+#~ msgstr "重新格式化并添加磁盘"
+
+#~ msgid ""
+#~ "Resetting the storage pool will erase all containers and release disks in "
+#~ "the pool."
+#~ msgstr "重置存储池将擦除所有容器并释放池中的磁盘。"
+
+#~ msgid "Security"
+#~ msgstr "安全性"
+
+#~ msgid "The Docker storage pool cannot be managed on this system."
+#~ msgstr "Docker 存储池不能在这个系统上被管理。"
+
+#~ msgid "The scan from $time ($type) found $count vulnerability:"
+#~ msgid_plural "The scan from $time ($type) found $count vulnerabilities:"
+#~ msgstr[0] "对 $time ($type) 的扫描找到 $count 安全漏洞:"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "从 $time ($type) 开始的扫描没有找到漏洞。"
+
+#~ msgid "The scan from $time ($type) was not successful."
+#~ msgstr "从 $time ($type) 开始的扫描未成功。"
+
+#~ msgid "Virtualization Service is Available"
+#~ msgstr "虚拟化服务可用"
+
+#~ msgid "You don't have permission to manage the Docker storage pool."
+#~ msgstr "您没有权限管理 Docker 存储池。"
+
+#~ msgid "$0 GiB / $1 GiB"
+#~ msgstr "$0 GiB / $1 GiB"
+
+#~ msgid "$mtu"
+#~ msgstr "$mtu"
+
+#~ msgid "${hip}:${hport} -> $cport"
+#~ msgstr "${hip}:${hport} -> $cport"
+
+#~ msgid "Hide Performance Options"
+#~ msgstr "隐藏性能选项"
+
+#~ msgid "Show Performance Options"
+#~ msgstr "显示性能选项"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..8e49e6c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,156 @@
+[build-system]
+requires = []
+backend-path = ['src']
+build-backend = 'build_backend'
+
+[tool.mypy]
+mypy_path = 'src:test/common'
+exclude = '_vendor'
+[[tool.mypy.overrides]]
+module = ["cockpit._vendor.*", "dbus", "gi.*", "task", "vdo.*"]
+follow_imports = 'silent'
+ignore_missing_imports = true
+
+[tool.pylint]
+max-line-length = 118
+disable = [
+ "C0114", # Missing module docstring
+ "C0115", # Missing class docstring
+ "C0116", # Missing function or method docstring
+ "R0902", # Too many instance attributes
+ "R0903", # Too few public methods
+ "R0913", # Too many arguments
+ "R1705", # Unnecessary "else" after "return"
+ "W0120", # Else clause on loop without a break statement
+ "W1113", # Keyword argument before variable positional arguments (PEP-570 is Python 3.8)
+]
+
+[tool.ruff]
+exclude = [
+ ".git/",
+ "modules/",
+ "node_modules/",
+]
+line-length = 118
+src = []
+
+[tool.ruff.lint]
+select = [
+ "A", # flake8-builtins
+ "B", # flake8-bugbear
+ "C4", # flake8-comprehensions
+ "D300", # pydocstyle: Forbid ''' in docstrings
+ "DTZ", # flake8-datetimez
+ "E", # pycodestyle
+ "EXE", # flake8-executable
+ "F", # pyflakes
+ "FBT", # flake8-boolean-trap
+ "G", # flake8-logging-format
+ "I", # isort
+ "ICN", # flake8-import-conventions
+ "ISC", # flake8-implicit-str-concat
+ "PIE", # flake8-pie
+ "PLE", # pylint errors
+ "PGH", # pygrep-hooks
+ "PT", # flake8-pytest-style
+ "RSE", # flake8-raise
+ "RUF", # ruff rules
+ "T10", # flake8-debugger
+ "TCH", # flake8-type-checking
+ "UP032", # f-string
+ "W", # warnings (mostly whitespace)
+ "YTT", # flake8-2020
+]
+ignore = [
+ "A003", # Class attribute is shadowing a python builtin
+ "B011", # Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`
+ "E731", # Do not assign a `lambda` expression, use a `def`
+ "PT011", # `pytest.raises(OSError)` is too broad
+ "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
+ "TCH001", # Move application import into a type-checking block
+ "TCH002", # Move third-party import `..packages.Packages` into a type-checking block
+]
+
+[tool.ruff.lint.flake8-pytest-style]
+fixture-parentheses = false
+mark-parentheses = false
+
+[tool.ruff.lint.isort]
+known-first-party = ["cockpit"]
+
+[tool.pytest.ini_options]
+addopts = ['--strict-markers'] # cf. https://github.com/cockpit-project/cockpit/pull/18584#issuecomment-1490243994
+pythonpath = ["src"]
+testpaths = ["test/pytest"]
+log_cli = true
+required_plugins = ["pytest-asyncio"]
+
+[tool.vulture]
+paths = [
+ "src",
+ "test/pytest",
+ "tools/vulture-suppressions",
+]
+ignore_names = [
+ "do_*",
+ "test[A-Z0-9]*",
+]
+ignore_decorators = [
+ "@*.getter",
+ "@*.register_function",
+ "@bus.Interface.Method",
+ "@pytest.hookimpl",
+]
+
+[tool.coverage.paths]
+source = ["src", "*/site-packages"]
+
+[tool.coverage.run]
+concurrency = ["multiprocessing"]
+source_pkgs = ["cockpit"]
+branch = true
+
+[tool.coverage.report]
+show_missing = true
+skip_covered = true
+exclude_lines = [
+ "pragma: no cover", # default
+ "raise NotImplementedError",
+]
+
+[tool.tox]
+legacy_tox_ini = """
+[tox]
+envlist = lint,pytest
+isolated_build = True
+labels =
+ venv = py311-lint, py3{6,7,8,9,10,11,12}-pytest
+
+# The default test environments use system packages and never PyPI.
+[testenv:{lint,pytest}]
+sitepackages = True
+install_command = python3 -m pip install --no-index --no-build-isolation {opts} {packages}
+wheel_build_env = pkg
+
+# All other environments (names like py311-lint, py36-pytest, etc) are isolated
+# from the system and get their packages from PyPI, according to the specific
+# test environment being requested. We build the wheel in a common environment.
+[testenv]
+package = wheel
+wheel_build_env = venv-pkg
+skip_install = lint: True
+deps =
+ lint: mypy
+ lint: flake8
+ lint: ruff
+ lint: vulture
+ pytest
+ pytest-asyncio
+ pytest: pytest-cov
+ pytest: pytest-timeout
+ pytest: pytest-xdist
+allowlist_externals = test/static-code
+commands =
+ pytest: python3 -m pytest -opythonpath= {posargs}
+ lint: test/static-code --tap
+"""
diff --git a/selinux/Makefile.am b/selinux/Makefile.am
new file mode 100644
index 0000000..4d92cd8
--- /dev/null
+++ b/selinux/Makefile.am
@@ -0,0 +1,35 @@
+if SELINUX_POLICY_ENABLED
+
+SELINUX_POLICY_FILES = \
+ selinux/cockpit.fc \
+ selinux/cockpit.if \
+ selinux/cockpit.te \
+ $(NULL)
+
+SELINUX_POLICY_MANPAGES = \
+ selinux/cockpit_session_selinux.8cockpit \
+ selinux/cockpit_ws_selinux.8cockpit \
+ $(NULL)
+
+dist_noinst_DATA += \
+ $(SELINUX_POLICY_FILES) \
+ $(SELINUX_POLICY_MANPAGES) \
+ $(NULL)
+
+cockpit.pp: $(SELINUX_POLICY_FILES)
+ $(AM_V_GEN) make -sf /usr/share/selinux/devel/Makefile cockpit.pp
+
+cockpit.pp.bz2: cockpit.pp
+ $(AM_V_GEN) bzip2 -9 < $< > $@.tmp && mv $@.tmp $@
+
+selinuxpackagesdir = $(datadir)/selinux/packages/$(SELINUX_POLICY_TYPE)/
+selinuxactivedir = $(sharedstatedir)/selinux/$(SELINUX_POLICY_TYPE)/active/modules/200/cockpit
+
+INSTALL_DATA_LOCAL_TARGETS += install-selinux
+install-selinux:
+ $(INSTALL) -d -m 700 $(DESTDIR)$(selinuxactivedir)
+
+selinuxpackages_DATA = cockpit.pp.bz2
+man_MANS += $(SELINUX_POLICY_MANPAGES)
+
+endif
diff --git a/selinux/cockpit.fc b/selinux/cockpit.fc
new file mode 100644
index 0000000..5fb9ca0
--- /dev/null
+++ b/selinux/cockpit.fc
@@ -0,0 +1,20 @@
+/usr/lib/systemd/system/cockpit.* -- gen_context(system_u:object_r:cockpit_unit_file_t,s0)
+/etc/systemd/system/cockpit.* -- gen_context(system_u:object_r:cockpit_unit_file_t,s0)
+
+/usr/libexec/cockpit-ws -- gen_context(system_u:object_r:cockpit_ws_exec_t,s0)
+/usr/libexec/cockpit-tls -- gen_context(system_u:object_r:cockpit_ws_exec_t,s0)
+/usr/libexec/cockpit-wsinstance-factory -- gen_context(system_u:object_r:cockpit_ws_exec_t,s0)
+
+/usr/libexec/cockpit-session -- gen_context(system_u:object_r:cockpit_session_exec_t,s0)
+/usr/libexec/cockpit-ssh -- gen_context(system_u:object_r:cockpit_session_exec_t,s0)
+
+/usr/share/cockpit/motd/update-motd -- gen_context(system_u:object_r:cockpit_ws_exec_t,s0)
+
+/var/lib/cockpit(/.*)? gen_context(system_u:object_r:cockpit_var_lib_t,s0)
+
+/var/run/cockpit(/.*)? gen_context(system_u:object_r:cockpit_var_run_t,s0)
+/run/cockpit(/.*)? gen_context(system_u:object_r:cockpit_var_run_t,s0)
+/var/run/cockpit-ws(/.*)? gen_context(system_u:object_r:cockpit_var_run_t,s0)
+/run/cockpit-ws(/.*)? gen_context(system_u:object_r:cockpit_var_run_t,s0)
+
+/etc/cockpit/ws-certs\.d(/.*)? gen_context(system_u:object_r:cert_t,s0)
diff --git a/selinux/cockpit.if b/selinux/cockpit.if
new file mode 100644
index 0000000..0fb6445
--- /dev/null
+++ b/selinux/cockpit.if
@@ -0,0 +1,279 @@
+## <summary>policy for cockpit</summary>
+
+########################################
+## <summary>
+## Execute TEMPLATE in the cockpit domin.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed to transition.
+## </summary>
+## </param>
+#
+interface(`cockpit_ws_domtrans',`
+ gen_require(`
+ type cockpit_ws_t, cockpit_ws_exec_t;
+ ')
+
+ corecmd_search_bin($1)
+ domtrans_pattern($1, cockpit_ws_exec_t, cockpit_ws_t)
+')
+
+########################################
+## <summary>
+## Execute TEMPLATE in the cockpit domin.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed to transition.
+## </summary>
+## </param>
+#
+interface(`cockpit_session_domtrans',`
+ gen_require(`
+ type cockpit_session_t, cockpit_session_exec_t;
+ ')
+
+ corecmd_search_bin($1)
+ domtrans_pattern($1, cockpit_session_exec_t, cockpit_session_t)
+')
+
+########################################
+## <summary>
+## Read and write cockpit_session_t unnamed pipes.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed access.
+## </summary>
+## </param>
+#
+interface(`cockpit_rw_pipes',`
+ gen_require(`
+ type cockpit_session_t;
+ ')
+
+ allow $1 cockpit_session_t:fifo_file rw_fifo_file_perms;
+')
+
+########################################
+## <summary>
+## Create cockpit unix_stream_sockets.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed access.
+## </summary>
+## </param>
+#
+interface(`cockpit_manage_unix_stream_sockets',`
+ gen_require(`
+ type cockpit_ws_t;
+ ')
+
+ allow $1 cockpit_ws_t:unix_stream_socket { create_stream_socket_perms connectto };
+')
+
+########################################
+## <summary>
+## Search cockpit lib directories.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed access.
+## </summary>
+## </param>
+#
+interface(`cockpit_search_lib',`
+ gen_require(`
+ type cockpit_var_lib_t;
+ ')
+
+ allow $1 cockpit_var_lib_t:dir search_dir_perms;
+ files_search_var_lib($1)
+')
+
+########################################
+## <summary>
+## Read cockpit lib files.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed access.
+## </summary>
+## </param>
+#
+interface(`cockpit_read_lib_files',`
+ gen_require(`
+ type cockpit_var_lib_t;
+ ')
+
+ files_search_var_lib($1)
+ read_files_pattern($1, cockpit_var_lib_t, cockpit_var_lib_t)
+')
+
+########################################
+## <summary>
+## Manage cockpit lib files.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed access.
+## </summary>
+## </param>
+#
+interface(`cockpit_manage_lib_files',`
+ gen_require(`
+ type cockpit_var_lib_t;
+ ')
+
+ files_search_var_lib($1)
+ manage_files_pattern($1, cockpit_var_lib_t, cockpit_var_lib_t)
+')
+
+########################################
+## <summary>
+## Manage cockpit lib directories.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed access.
+## </summary>
+## </param>
+#
+interface(`cockpit_manage_lib_dirs',`
+ gen_require(`
+ type cockpit_var_lib_t;
+ ')
+
+ files_search_var_lib($1)
+ manage_dirs_pattern($1, cockpit_var_lib_t, cockpit_var_lib_t)
+')
+
+########################################
+## <summary>
+## Read cockpit pid files.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed access.
+## </summary>
+## </param>
+#
+interface(`cockpit_read_pid_files',`
+ gen_require(`
+ type cockpit_var_run_t;
+ ')
+
+ read_files_pattern($1, cockpit_var_run_t, cockpit_var_run_t)
+ read_lnk_files_pattern($1, cockpit_var_run_t, cockpit_var_run_t)
+')
+
+########################################
+## <summary>
+## Manage cockpit pid dirs.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed access.
+## </summary>
+## </param>
+#
+interface(`cockpit_manage_pid_dirs',`
+ gen_require(`
+ type cockpit_var_run_t;
+ ')
+
+ manage_dirs_pattern($1, cockpit_var_run_t, cockpit_var_run_t)
+')
+
+########################################
+## <summary>
+## Manage cockpit pid dirs.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed access.
+## </summary>
+## </param>
+#
+interface(`cockpit_manage_pid_files',`
+ gen_require(`
+ type cockpit_var_run_t;
+ ')
+
+ manage_files_pattern($1, cockpit_var_run_t, cockpit_var_run_t)
+')
+
+########################################
+## <summary>
+## Execute cockpit server in the cockpit domain.
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed to transition.
+## </summary>
+## </param>
+#
+interface(`cockpit_systemctl',`
+ gen_require(`
+ type cockpit_ws_t;
+ type cockpit_unit_file_t;
+ ')
+
+ systemd_exec_systemctl($1)
+ init_reload_services($1)
+ systemd_read_fifo_file_passwd_run($1)
+ allow $1 cockpit_unit_file_t:file read_file_perms;
+ allow $1 cockpit_unit_file_t:service manage_service_perms;
+
+ ps_process_pattern($1, cockpit_ws_t)
+')
+
+
+########################################
+## <summary>
+## All of the rules required to administrate
+## an cockpit environment
+## </summary>
+## <param name="domain">
+## <summary>
+## Domain allowed access.
+## </summary>
+## </param>
+## <rolecap/>
+#
+interface(`cockpit_admin',`
+ gen_require(`
+ type cockpit_ws_t;
+ type cockpit_session_t;
+ type cockpit_var_lib_t;
+ type cockpit_var_run_t;
+ type cockpit_unit_file_t;
+ ')
+
+ allow $1 cockpit_ws_t:process { signal_perms };
+ ps_process_pattern($1, cockpit_ws_t)
+
+ allow $1 cockpit_session_t:process { signal_perms };
+ ps_process_pattern($1, cockpit_session_t)
+
+ tunable_policy(`deny_ptrace',`',`
+ allow $1 cockpit_ws_t:process ptrace;
+ allow $1 cockpit_session_t:process ptrace;
+ ')
+
+ files_search_var_lib($1)
+ admin_pattern($1, cockpit_var_lib_t)
+
+ files_search_pids($1)
+ admin_pattern($1, cockpit_var_run_t)
+
+ cockpit_systemctl($1)
+ admin_pattern($1, cockpit_unit_file_t)
+ allow $1 cockpit_unit_file_t:service all_service_perms;
+ optional_policy(`
+ systemd_passwd_agent_exec($1)
+ systemd_read_fifo_file_passwd_run($1)
+ ')
+')
diff --git a/selinux/cockpit.te b/selinux/cockpit.te
new file mode 100644
index 0000000..6df2321
--- /dev/null
+++ b/selinux/cockpit.te
@@ -0,0 +1,210 @@
+policy_module(cockpit, 1.0.0)
+
+########################################
+#
+# Declarations
+#
+
+type cockpit_ws_t;
+type cockpit_ws_exec_t;
+init_daemon_domain(cockpit_ws_t,cockpit_ws_exec_t)
+init_nnp_daemon_domain(cockpit_ws_t)
+
+type cockpit_tmp_t;
+files_tmp_file(cockpit_tmp_t)
+
+type cockpit_tmpfs_t;
+userdom_user_tmp_file(cockpit_tmpfs_t)
+
+type cockpit_var_run_t;
+files_pid_file(cockpit_var_run_t)
+systemd_mount_dir(cockpit_var_run_t)
+systemd_private_tmp(cockpit_var_run_t)
+
+type cockpit_unit_file_t;
+systemd_unit_file(cockpit_unit_file_t)
+
+type cockpit_var_lib_t;
+files_type(cockpit_var_lib_t)
+
+type cockpit_session_t;
+type cockpit_session_exec_t;
+domain_type(cockpit_session_t)
+domain_entry_file(cockpit_session_t,cockpit_session_exec_t)
+
+########################################
+#
+# cockpit_ws_t local policy
+#
+
+allow cockpit_ws_t self:capability net_admin;
+allow cockpit_ws_t self:process setrlimit;
+allow cockpit_ws_t self:tcp_socket create_stream_socket_perms;
+
+kernel_read_system_state(cockpit_ws_t)
+dev_read_sysfs(cockpit_ws_t)
+
+# cockpit-tls can execute cockpit-ws
+can_exec(cockpit_ws_t,cockpit_ws_exec_t)
+
+# systemd can execute cockpit-session
+can_exec(init_t,cockpit_session_exec_t)
+
+# cockpit-ws can execute cockpit-{ssh,session}
+can_exec(cockpit_ws_t,cockpit_session_exec_t)
+# ... and the real ssh (through beiboot/ferny)
+gen_require(`
+ type ssh_exec_t;
+')
+can_exec(cockpit_ws_t,ssh_exec_t)
+
+# cockpit-ws can read from /dev/urandom
+dev_read_urand(cockpit_ws_t) # for authkey
+dev_read_rand(cockpit_ws_t) # for libssh
+
+# cockpit-ws allows connections on websm port
+corenet_tcp_bind_websm_port(cockpit_ws_t)
+corenet_tcp_bind_generic_node(cockpit_ws_t)
+
+# cockpit-ws can connect to other hosts via ssh
+corenet_tcp_connect_ssh_port(cockpit_ws_t)
+
+# cockpit-ws can write to its temp files
+manage_dirs_pattern(cockpit_ws_t, cockpit_tmp_t, cockpit_tmp_t)
+manage_files_pattern(cockpit_ws_t, cockpit_tmp_t, cockpit_tmp_t)
+files_tmp_filetrans(cockpit_ws_t, cockpit_tmp_t, { dir file })
+
+manage_dirs_pattern(cockpit_ws_t, cockpit_tmpfs_t, cockpit_tmpfs_t)
+manage_files_pattern(cockpit_ws_t, cockpit_tmpfs_t, cockpit_tmpfs_t)
+fs_tmpfs_filetrans(cockpit_ws_t, cockpit_tmpfs_t, { file })
+
+manage_dirs_pattern(cockpit_ws_t, cockpit_var_run_t, cockpit_var_run_t)
+manage_files_pattern(cockpit_ws_t, cockpit_var_run_t, cockpit_var_run_t)
+manage_lnk_files_pattern(cockpit_ws_t, cockpit_var_run_t, cockpit_var_run_t)
+manage_sock_files_pattern(cockpit_ws_t, cockpit_var_run_t, cockpit_var_run_t)
+files_pid_filetrans(cockpit_ws_t, cockpit_var_run_t, { file dir sock_file })
+
+manage_files_pattern(cockpit_ws_t, cockpit_var_lib_t, cockpit_var_lib_t)
+manage_dirs_pattern(cockpit_ws_t, cockpit_var_lib_t, cockpit_var_lib_t)
+
+allow cockpit_ws_t cockpit_unit_file_t:service manage_service_perms;
+
+kernel_read_network_state(cockpit_ws_t)
+
+auth_use_nsswitch(cockpit_ws_t)
+
+corecmd_exec_bin(cockpit_ws_t)
+
+fs_getattr_xattr_fs(cockpit_ws_t)
+fs_read_efivarfs_files(cockpit_ws_t)
+
+init_read_state(cockpit_ws_t)
+init_stream_connect(cockpit_ws_t)
+
+logging_send_syslog_msg(cockpit_ws_t)
+
+sysnet_exec_ifconfig(cockpit_ws_t)
+
+# cockpit-ws launches cockpit-session
+cockpit_session_domtrans(cockpit_ws_t)
+allow cockpit_ws_t cockpit_session_t:process signal_perms;
+
+# cockpit-session communicates back with cockpit-ws
+allow cockpit_session_t cockpit_ws_t:unix_stream_socket rw_stream_socket_perms;
+
+# cockpit-tls and cockpit-ws communicate over a Unix socket
+allow cockpit_ws_t cockpit_ws_t:unix_stream_socket { connectto create_stream_socket_perms };
+
+optional_policy(`
+ hostname_exec(cockpit_ws_t)
+')
+
+optional_policy(`
+ kerberos_use(cockpit_ws_t)
+ kerberos_etc_filetrans_keytab(cockpit_ws_t)
+')
+
+optional_policy(`
+ miscfiles_dontaudit_map_generic_certs(cockpit_ws_t)
+')
+
+optional_policy(`
+ systemd_exec_systemctl(cockpit_ws_t)
+')
+
+optional_policy(`
+ ssh_read_user_home_files(cockpit_ws_t)
+')
+
+#########################################################
+#
+# cockpit-session local policy
+#
+
+# cockpit-session changes to the actual logged in user
+# pam_faillock chowns the state file to the target user
+allow cockpit_session_t self:capability { chown fowner dac_override dac_read_search setgid setuid sys_admin sys_resource };
+allow cockpit_session_t self:process { setexec setrlimit setsched signal_perms };
+
+read_files_pattern(cockpit_session_t, cockpit_var_lib_t, cockpit_var_lib_t)
+list_dirs_pattern(cockpit_session_t, cockpit_var_lib_t, cockpit_var_lib_t)
+
+manage_dirs_pattern(cockpit_session_t, cockpit_tmp_t, cockpit_tmp_t)
+manage_files_pattern(cockpit_session_t, cockpit_tmp_t, cockpit_tmp_t)
+manage_sock_files_pattern(cockpit_session_t, cockpit_tmp_t, cockpit_tmp_t)
+files_tmp_filetrans(cockpit_session_t, cockpit_tmp_t, { dir file sock_file })
+
+manage_dirs_pattern(cockpit_session_t, cockpit_tmpfs_t, cockpit_tmpfs_t)
+manage_files_pattern(cockpit_session_t, cockpit_tmpfs_t, cockpit_tmpfs_t)
+fs_tmpfs_filetrans(cockpit_session_t, cockpit_tmpfs_t, { file })
+
+read_files_pattern(cockpit_session_t, cockpit_var_run_t, cockpit_var_run_t)
+list_dirs_pattern(cockpit_session_t, cockpit_var_run_t, cockpit_var_run_t)
+
+kernel_read_network_state(cockpit_session_t)
+
+# cockpit-session runs a full pam stack, including pam_selinux.so
+auth_login_pgm_domain(cockpit_session_t)
+# cockpit-session resseting expired passwords
+auth_manage_passwd(cockpit_session_t)
+auth_manage_shadow(cockpit_session_t)
+auth_write_login_records(cockpit_session_t)
+
+corenet_tcp_bind_ssh_port(cockpit_session_t)
+corenet_tcp_connect_ssh_port(cockpit_session_t)
+
+# cockpit-session can execute cockpit-bridge as the user, without setting AT_SECURE
+userdom_spec_domtrans_all_users(cockpit_session_t)
+userdom_noatsecure_login_userdomain(cockpit_session_t)
+usermanage_read_crack_db(cockpit_session_t)
+
+# pam_google_authenticator needs to create and rename files in home dir
+userdom_manage_user_home_content(cockpit_session_t)
+
+optional_policy(`
+ ssh_agent_signal(cockpit_session_t)
+')
+
+optional_policy(`
+ sssd_dbus_chat(cockpit_session_t)
+')
+
+optional_policy(`
+ userdom_signal_all_users(cockpit_session_t)
+')
+
+optional_policy(`
+ unconfined_domtrans(cockpit_session_t)
+')
+
+# login may read motd file through pam
+optional_policy(`
+ gen_require(`
+ type local_login_t;
+ ')
+ cockpit_read_pid_files(local_login_t)
+')
+
+optional_policy(`
+ gnome_exec_keyringd(cockpit_session_t)
+')
diff --git a/selinux/cockpit_session_selinux.8cockpit b/selinux/cockpit_session_selinux.8cockpit
new file mode 100644
index 0000000..cbf7a1b
--- /dev/null
+++ b/selinux/cockpit_session_selinux.8cockpit
@@ -0,0 +1,255 @@
+.TH "cockpit_session_selinux" "8" "21-04-16" "cockpit_session" "SELinux Policy cockpit_session"
+.SH "NAME"
+cockpit_session_selinux \- Security Enhanced Linux Policy for the cockpit_session processes
+.SH "DESCRIPTION"
+
+Security-Enhanced Linux secures the cockpit_session processes via flexible mandatory access control.
+
+The cockpit_session processes execute with the cockpit_session_t SELinux type. You can check if you have these processes running by executing the \fBps\fP command with the \fB\-Z\fP qualifier.
+
+For example:
+
+.B ps -eZ | grep cockpit_session_t
+
+
+.SH "ENTRYPOINTS"
+
+The cockpit_session_t SELinux type can be entered via the \fBcockpit_session_exec_t\fP file type.
+
+The default entrypoint paths for the cockpit_session_t domain are the following:
+
+/usr/libexec/cockpit-ssh, /usr/libexec/cockpit-session
+.SH PROCESS TYPES
+SELinux defines process types (domains) for each process running on the system
+.PP
+You can see the context of a process using the \fB\-Z\fP option to \fBps\bP
+.PP
+Policy governs the access confined processes have to files.
+SELinux cockpit_session policy is very flexible allowing users to setup their cockpit_session processes in as secure a method as possible.
+.PP
+The following process types are defined for cockpit_session:
+
+.EX
+.B cockpit_session_t
+.EE
+.PP
+Note:
+.B semanage permissive -a cockpit_session_t
+can be used to make the process type cockpit_session_t permissive. SELinux does not deny access to permissive process types, but the AVC (SELinux denials) messages are still generated.
+
+.SH BOOLEANS
+SELinux policy is customizable based on least access required. cockpit_session policy is extremely flexible and has several booleans that allow you to manipulate the policy and run cockpit_session with the tightest access possible.
+
+
+.PP
+If you want to allow all domains to execute in fips_mode, you must turn on the fips_mode boolean. Enabled by default.
+
+.EX
+.B setsebool -P fips_mode 1
+
+.EE
+
+.PP
+If you want to allow confined applications to run with kerberos, you must turn on the kerberos_enabled boolean. Enabled by default.
+
+.EX
+.B setsebool -P kerberos_enabled 1
+
+.EE
+
+.PP
+If you want to allow system to run with NIS, you must turn on the nis_enabled boolean. Disabled by default.
+
+.EX
+.B setsebool -P nis_enabled 1
+
+.EE
+
+.PP
+If you want to enable polyinstantiated directory support, you must turn on the polyinstantiation_enabled boolean. Disabled by default.
+
+.EX
+.B setsebool -P polyinstantiation_enabled 1
+
+.EE
+
+.SH "MANAGED FILES"
+
+The SELinux process type cockpit_session_t can manage files labeled with the following file types. The paths listed are the default paths for these file types. Note the processes UID still need to have DAC permissions.
+
+.br
+.B auth_cache_t
+
+ /var/cache/coolkey(/.*)?
+.br
+
+.br
+.B auth_home_t
+
+ /root/\.yubico(/.*)?
+.br
+ /root/\.config/Yubico(/.*)?
+.br
+ /root/\.google_authenticator
+.br
+ /root/\.google_authenticator~
+.br
+ /home/[^/]+/\.yubico(/.*)?
+.br
+ /home/[^/]+/\.config/Yubico(/.*)?
+.br
+ /home/[^/]+/\.google_authenticator
+.br
+ /home/[^/]+/\.google_authenticator~
+.br
+
+.br
+.B faillog_t
+
+ /var/log/btmp.*
+.br
+ /var/log/faillog.*
+.br
+ /var/log/tallylog.*
+.br
+ /var/run/faillock(/.*)?
+.br
+
+.br
+.B initrc_var_run_t
+
+ /var/run/utmp
+.br
+ /var/run/random-seed
+.br
+ /var/run/runlevel\.dir
+.br
+ /var/run/setmixer_flag
+.br
+
+.br
+.B lastlog_t
+
+ /var/log/lastlog.*
+.br
+
+.br
+.B pam_var_run_t
+
+ /var/(db|adm)/sudo(/.*)?
+.br
+ /var/lib/sudo(/.*)?
+.br
+ /var/run/sudo(/.*)?
+.br
+ /var/run/pam_ssh(/.*)?
+.br
+ /var/run/sepermit(/.*)?
+.br
+ /var/run/pam_mount(/.*)?
+.br
+ /var/run/pam_timestamp(/.*)?
+.br
+
+.br
+.B security_t
+
+ /selinux
+.br
+
+.br
+.B shadow_t
+
+ /etc/shadow.*
+.br
+ /etc/gshadow.*
+.br
+ /etc/nshadow.*
+.br
+ /var/db/shadow.*
+.br
+ /etc/security/opasswd
+.br
+ /etc/security/opasswd\.old
+.br
+
+.br
+.B var_auth_t
+
+ /var/ace(/.*)?
+.br
+ /var/rsa(/.*)?
+.br
+ /var/lib/abl(/.*)?
+.br
+ /var/lib/rsa(/.*)?
+.br
+ /var/lib/pam_ssh(/.*)?
+.br
+ /var/lib/pam_shield(/.*)?
+.br
+ /var/opt/quest/vas/vasd(/.*)?
+.br
+ /var/lib/google-authenticator(/.*)?
+.br
+
+.br
+.B wtmp_t
+
+ /var/log/wtmp.*
+.br
+
+.SH FILE CONTEXTS
+SELinux requires files to have an extended attribute to define the file type.
+.PP
+You can see the context of a file using the \fB\-Z\fP option to \fBls\bP
+.PP
+Policy governs the access confined processes have to these files.
+SELinux cockpit_session policy is very flexible allowing users to setup their cockpit_session processes in as secure a method as possible.
+.PP
+
+.I The following file types are defined for cockpit_session:
+
+
+.EX
+.PP
+.B cockpit_session_exec_t
+.EE
+
+- Set files with the cockpit_session_exec_t type, if you want to transition an executable to the cockpit_session_t domain.
+
+.br
+.TP 5
+Paths:
+/usr/libexec/cockpit-ssh, /usr/libexec/cockpit-session
+
+.PP
+Note: File context can be temporarily modified with the chcon command. If you want to permanently change the file context you need to use the
+.B semanage fcontext
+command. This will modify the SELinux labeling database. You will need to use
+.B restorecon
+to apply the labels.
+
+.SH "COMMANDS"
+.B semanage fcontext
+can also be used to manipulate default file context mappings.
+.PP
+.B semanage permissive
+can also be used to manipulate whether or not a process type is permissive.
+.PP
+.B semanage module
+can also be used to enable/disable/install/remove policy modules.
+
+.B semanage boolean
+can also be used to manipulate the booleans
+
+.PP
+.B system-config-selinux
+is a GUI tool available to customize SELinux policy settings.
+
+.SH AUTHOR
+This manual page was auto-generated using
+.B "sepolicy manpage".
+
+.SH "SEE ALSO"
+selinux(8), cockpit_session(8), semanage(8), restorecon(8), chcon(1), sepolicy(8), setsebool(8) \ No newline at end of file
diff --git a/selinux/cockpit_ws_selinux.8cockpit b/selinux/cockpit_ws_selinux.8cockpit
new file mode 100644
index 0000000..515607e
--- /dev/null
+++ b/selinux/cockpit_ws_selinux.8cockpit
@@ -0,0 +1,207 @@
+.TH "cockpit_ws_selinux" "8" "21-04-16" "cockpit_ws" "SELinux Policy cockpit_ws"
+.SH "NAME"
+cockpit_ws_selinux \- Security Enhanced Linux Policy for the cockpit_ws processes
+.SH "DESCRIPTION"
+
+Security-Enhanced Linux secures the cockpit_ws processes via flexible mandatory access control.
+
+The cockpit_ws processes execute with the cockpit_ws_t SELinux type. You can check if you have these processes running by executing the \fBps\fP command with the \fB\-Z\fP qualifier.
+
+For example:
+
+.B ps -eZ | grep cockpit_ws_t
+
+
+.SH "ENTRYPOINTS"
+
+The cockpit_ws_t SELinux type can be entered via the \fBcockpit_ws_exec_t\fP file type.
+
+The default entrypoint paths for the cockpit_ws_t domain are the following:
+
+/usr/libexec/cockpit-ws, /usr/libexec/cockpit-tls, /usr/share/cockpit/motd/update-motd, /usr/libexec/cockpit-wsinstance-factory
+.SH PROCESS TYPES
+SELinux defines process types (domains) for each process running on the system
+.PP
+You can see the context of a process using the \fB\-Z\fP option to \fBps\bP
+.PP
+Policy governs the access confined processes have to files.
+SELinux cockpit_ws policy is very flexible allowing users to setup their cockpit_ws processes in as secure a method as possible.
+.PP
+The following process types are defined for cockpit_ws:
+
+.EX
+.B cockpit_ws_t
+.EE
+.PP
+Note:
+.B semanage permissive -a cockpit_ws_t
+can be used to make the process type cockpit_ws_t permissive. SELinux does not deny access to permissive process types, but the AVC (SELinux denials) messages are still generated.
+
+.SH BOOLEANS
+SELinux policy is customizable based on least access required. cockpit_ws policy is extremely flexible and has several booleans that allow you to manipulate the policy and run cockpit_ws with the tightest access possible.
+
+
+.PP
+If you want to allow all domains to execute in fips_mode, you must turn on the fips_mode boolean. Enabled by default.
+
+.EX
+.B setsebool -P fips_mode 1
+
+.EE
+
+.SH "MANAGED FILES"
+
+The SELinux process type cockpit_ws_t can manage files labeled with the following file types. The paths listed are the default paths for these file types. Note the processes UID still need to have DAC permissions.
+
+.br
+.B cluster_conf_t
+
+ /etc/cluster(/.*)?
+.br
+
+.br
+.B cluster_var_lib_t
+
+ /var/lib/pcsd(/.*)?
+.br
+ /var/lib/cluster(/.*)?
+.br
+ /var/lib/openais(/.*)?
+.br
+ /var/lib/pengine(/.*)?
+.br
+ /var/lib/corosync(/.*)?
+.br
+ /usr/lib/heartbeat(/.*)?
+.br
+ /var/lib/heartbeat(/.*)?
+.br
+ /var/lib/pacemaker(/.*)?
+.br
+
+.br
+.B cluster_var_run_t
+
+ /var/run/crm(/.*)?
+.br
+ /var/run/cman_.*
+.br
+ /var/run/rsctmp(/.*)?
+.br
+ /var/run/aisexec.*
+.br
+ /var/run/heartbeat(/.*)?
+.br
+ /var/run/pcsd-ruby.socket
+.br
+ /var/run/corosync-qnetd(/.*)?
+.br
+ /var/run/corosync-qdevice(/.*)?
+.br
+ /var/run/corosync\.pid
+.br
+ /var/run/cpglockd\.pid
+.br
+ /var/run/rgmanager\.pid
+.br
+ /var/run/cluster/rgmanager\.sk
+.br
+
+.br
+.B cockpit_var_lib_t
+
+ /var/lib/cockpit(/.*)?
+.br
+
+.br
+.B cockpit_var_run_t
+
+ /var/run/cockpit(/.*)?
+.br
+ /var/run/cockpit-ws(/.*)?
+.br
+
+.br
+.B krb5_keytab_t
+
+ /var/kerberos/krb5(/.*)?
+.br
+ /etc/krb5\.keytab
+.br
+ /etc/krb5kdc/kadm5\.keytab
+.br
+ /var/kerberos/krb5kdc/kadm5\.keytab
+.br
+
+.br
+.B root_t
+
+ /sysroot/ostree/deploy/.*-atomic/deploy(/.*)?
+.br
+ /
+.br
+ /initrd
+.br
+
+.br
+.B systemd_passwd_var_run_t
+
+ /var/run/systemd/ask-password(/.*)?
+.br
+ /var/run/systemd/ask-password-block(/.*)?
+.br
+
+.SH FILE CONTEXTS
+SELinux requires files to have an extended attribute to define the file type.
+.PP
+You can see the context of a file using the \fB\-Z\fP option to \fBls\bP
+.PP
+Policy governs the access confined processes have to these files.
+SELinux cockpit_ws policy is very flexible allowing users to setup their cockpit_ws processes in as secure a method as possible.
+.PP
+
+.I The following file types are defined for cockpit_ws:
+
+
+.EX
+.PP
+.B cockpit_ws_exec_t
+.EE
+
+- Set files with the cockpit_ws_exec_t type, if you want to transition an executable to the cockpit_ws_t domain.
+
+.br
+.TP 5
+Paths:
+/usr/libexec/cockpit-ws, /usr/libexec/cockpit-tls, /usr/share/cockpit/motd/update-motd, /usr/libexec/cockpit-wsinstance-factory
+
+.PP
+Note: File context can be temporarily modified with the chcon command. If you want to permanently change the file context you need to use the
+.B semanage fcontext
+command. This will modify the SELinux labeling database. You will need to use
+.B restorecon
+to apply the labels.
+
+.SH "COMMANDS"
+.B semanage fcontext
+can also be used to manipulate default file context mappings.
+.PP
+.B semanage permissive
+can also be used to manipulate whether or not a process type is permissive.
+.PP
+.B semanage module
+can also be used to enable/disable/install/remove policy modules.
+
+.B semanage boolean
+can also be used to manipulate the booleans
+
+.PP
+.B system-config-selinux
+is a GUI tool available to customize SELinux policy settings.
+
+.SH AUTHOR
+This manual page was auto-generated using
+.B "sepolicy manpage".
+
+.SH "SEE ALSO"
+selinux(8), cockpit_ws(8), semanage(8), restorecon(8), chcon(1), sepolicy(8), setsebool(8) \ No newline at end of file
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..452b494
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,89 @@
+BUILT_SOURCES =
+bin_PROGRAMS =
+libexec_PROGRAMS =
+libexec_SCRIPTS =
+sbin_PROGRAMS =
+
+# -----------------------------------------------------------------------------
+# Python
+
+# Will only be honoured if pytest-timeout plugin is installed
+export PYTEST_TIMEOUT = 120
+
+.PHONY: pytest
+pytest: $(BUILT_SOURCES) $(DIST_STAMP) $(MANIFESTS)
+ $(MAKE) test-server
+ cd '$(srcdir)' && abs_builddir='$(abs_builddir)' pytest
+
+.PHONY: pytest-cov
+pytest-cov: $(BUILT_SOURCES) $(DIST_STAMP) $(MANIFESTS)
+ $(MAKE) test-server
+ cd '$(srcdir)' && abs_builddir='$(abs_builddir)' pytest --cov
+
+if !WITH_OLD_BRIDGE
+INSTALL_DATA_LOCAL_TARGETS += install-python
+install-python:
+ @# wheel-based installation with .dist-info.
+ @# This needs to work on RHEL8 up through modern Fedora, offline, with
+ @# system packages available to the build.
+ python3 -m pip install --no-index --force-reinstall --root='$(DESTDIR)/' --prefix='$(prefix)' \
+ "$$(python3 '$(srcdir)'/src/build_backend.py --wheel '$(srcdir)' tmp/wheel)"
+ mkdir -p $(DESTDIR)$(libexecdir)
+ mv -t $(DESTDIR)$(libexecdir) $(DESTDIR)$(bindir)/cockpit-askpass
+
+UNINSTALL_LOCAL_TARGETS += uninstall-python
+uninstall-python:
+ rm -rf tmp/wheel
+ rm -f $(DESTDIR)$(libexecdir)/cockpit-askpass
+ rm -f $(DESTDIR)$(bindir)/cockpit-bridge
+ @# HACK: pip uninstall does not know about --root and --prefix
+ rm -r $(DESTDIR)$(prefix)/lib/python*/*-packages/cockpit \
+ $(DESTDIR)$(prefix)/lib/python*/*-packages/cockpit-*.dist-info
+endif
+
+
+# -----------------------------------------------------------------------------
+# C
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src \
+ -DDATADIR=\"$(datadir)\" \
+ -DLIBEXECDIR=\""$(libexecdir)"\" \
+ -DPACKAGE_SYSCONF_DIR=\""$(sysconfdir)"\" \
+ $(NULL)
+
+AM_CFLAGS = \
+ -std=gnu18 \
+ -pthread \
+ -Wall \
+ -Werror=strict-prototypes \
+ -Werror=missing-prototypes \
+ -Werror=implicit-function-declaration \
+ -Werror=implicit-int \
+ -Werror=int-conversion \
+ -Werror=old-style-definition \
+ -Werror=pointer-arith \
+ -Werror=init-self \
+ -Werror=format=2 \
+ -Werror=return-type \
+ -Werror=missing-include-dirs \
+ $(NULL)
+
+# -----------------------------------------------------------------------------
+# AppStream metadata
+#
+%.metainfo.xml: %.metainfo.xml.in
+ $(AM_V_GEN) mkdir -p $(dir $@) && msgfmt --xml -d $(top_srcdir)/po --template $< --output $@
+
+metainfodir = ${datarootdir}/metainfo
+nodist_metainfo_DATA = \
+ src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml \
+ src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml \
+ src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml \
+ src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml \
+ src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml \
+ $(NULL)
+
+metainfo_in = $(patsubst %,%.in,$(nodist_metainfo_DATA))
+EXTRA_DIST += $(metainfo_in)
+CLEANFILES += $(nodist_metainfo_DATA)
diff --git a/src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in b/src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in
new file mode 100644
index 0000000..e20e7ff
--- /dev/null
+++ b/src/appstream/org.cockpit-project.cockpit-kdump.metainfo.xml.in
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="addon">
+ <id>org.cockpit_project.cockpit_kdump</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <name>Kernel dump</name>
+ <summary>Collect kernel crash dumps</summary>
+ <description>
+ <p>
+ This tool configures the system to write kernel crash dumps to
+ disk.
+ </p>
+ </description>
+ <extends>org.cockpit_project.cockpit</extends>
+ <launchable type="cockpit-manifest">kdump</launchable>
+ <url type="homepage">https://cockpit-project.org/</url>
+ <update_contact>cockpit-devel_AT_lists.fedorahosted.org</update_contact>
+</component>
diff --git a/src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in b/src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in
new file mode 100644
index 0000000..ab410b0
--- /dev/null
+++ b/src/appstream/org.cockpit-project.cockpit-networkmanager.metainfo.xml.in
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="addon">
+ <id>org.cockpit_project.cockpit_networkmanager</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <name>Networking</name>
+ <summary>Cockpit configuration of NetworkManager and Firewalld</summary>
+ <description>
+ <p>
+ This tool manages networking such as bonds, bridges, teams, VLANs
+ and firewalls using NetworkManager and Firewalld.
+ NetworkManager is incompatible with Ubuntu's default systemd-networkd and
+ Debian's ifupdown scripts.
+ </p>
+ </description>
+ <extends>org.cockpit_project.cockpit</extends>
+ <launchable type="cockpit-manifest">networkmanager</launchable>
+ <url type="homepage">https://cockpit-project.org/</url>
+ <update_contact>cockpit-devel_AT_lists.fedorahosted.org</update_contact>
+</component>
diff --git a/src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in b/src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in
new file mode 100644
index 0000000..ac6897b
--- /dev/null
+++ b/src/appstream/org.cockpit-project.cockpit-selinux.metainfo.xml.in
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="addon">
+ <id>org.cockpit_project.cockpit_selinux</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <name>SELinux</name>
+ <summary>Security Enhanced Linux configuration and troubleshooting</summary>
+ <description>
+ <p>
+ This tool configures the SELinux policy and can help with
+ understanding and resolving policy violations.
+ </p>
+ </description>
+ <extends>org.cockpit_project.cockpit</extends>
+ <launchable type="cockpit-manifest">selinux</launchable>
+ <url type="homepage">https://cockpit-project.org/</url>
+ <update_contact>cockpit-devel_AT_lists.fedorahosted.org</update_contact>
+</component>
diff --git a/src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in b/src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in
new file mode 100644
index 0000000..87af268
--- /dev/null
+++ b/src/appstream/org.cockpit-project.cockpit-sosreport.metainfo.xml.in
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="addon">
+ <id>org.cockpit_project.cockpit_sosreport</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <name>Diagnostic reports</name>
+ <icon type="local" height="64" width="64">/usr/share/pixmaps/cockpit-sosreport.png</icon>
+ <summary>Collect and package diagnostic and support data</summary>
+ <description>
+ <p>
+ This tool generates an archive of configuration and diagnostic
+ information from the running system. The archive may be stored
+ locally or centrally for recording or tracking purposes or may
+ be sent to technical support representatives, developers or
+ system administrators to assist with technical fault-finding and
+ debugging.
+ </p>
+ </description>
+ <extends>org.cockpit_project.cockpit</extends>
+ <launchable type="cockpit-manifest">sosreport</launchable>
+ <url type="homepage">https://cockpit-project.org/</url>
+ <update_contact>cockpit-devel_AT_lists.fedorahosted.org</update_contact>
+</component>
diff --git a/src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in b/src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in
new file mode 100644
index 0000000..2d4fa50
--- /dev/null
+++ b/src/appstream/org.cockpit-project.cockpit-storaged.metainfo.xml.in
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="addon">
+ <id>org.cockpit_project.cockpit_storaged</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <name>Storage</name>
+ <summary>Manage storage</summary>
+ <description>
+ <p>
+ This tool manages local storage, such as filesystems, LVM2
+ volume groups, and NFS mounts.
+ </p>
+ </description>
+ <extends>org.cockpit_project.cockpit</extends>
+ <launchable type="cockpit-manifest">storage</launchable>
+ <url type="homepage">https://cockpit-project.org/</url>
+ <update_contact>cockpit-devel_AT_lists.fedorahosted.org</update_contact>
+</component>
diff --git a/src/branding/arch/Makefile.am b/src/branding/arch/Makefile.am
new file mode 100644
index 0000000..38996d2
--- /dev/null
+++ b/src/branding/arch/Makefile.am
@@ -0,0 +1,10 @@
+archbrandingdir = $(datadir)/cockpit/branding/arch
+
+dist_archbranding_DATA = \
+ src/branding/arch/branding.css \
+ $(NULL)
+
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/archlinux-logo.png $(DESTDIR)$(archbrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/archlinux-logo.png $(DESTDIR)$(archbrandingdir)/apple-touch-icon.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/archlinux-logo.png $(DESTDIR)$(archbrandingdir)/favicon.ico
diff --git a/src/branding/arch/branding.css b/src/branding/arch/branding.css
new file mode 100644
index 0000000..af4e9b8
--- /dev/null
+++ b/src/branding/arch/branding.css
@@ -0,0 +1,26 @@
+body.login-pf {
+ background: url("bg-plain.jpg") no-repeat 50% 0;
+ background-size: cover;
+ background-color: #101010;
+}
+
+#badge {
+ inline-size: 225px;
+ block-size: 80px;
+ background-image: url("logo.png");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#brand {
+ font-size: 18pt;
+ text-transform: uppercase;
+}
+
+#brand::before {
+ content: "${NAME} <b>${VARIANT}</b>";
+}
+
+#index-brand::before {
+ content: "${NAME} <b>${VARIANT}</b>";
+}
diff --git a/src/branding/centos/Makefile.am b/src/branding/centos/Makefile.am
new file mode 100644
index 0000000..4ac3076
--- /dev/null
+++ b/src/branding/centos/Makefile.am
@@ -0,0 +1,11 @@
+centosbrandingdir = $(datadir)/cockpit/branding/centos
+
+dist_centosbranding_DATA = \
+ src/branding/centos/branding.css \
+ $(NULL)
+
+# Opportunistically use fedora-logos
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/system-logo-white.png $(DESTDIR)$(centosbrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/fedora-logo-sprite.png $(DESTDIR)$(centosbrandingdir)/apple-touch-icon.png
+ ln -sTfr $(DESTDIR)/etc/favicon.png $(DESTDIR)$(centosbrandingdir)/favicon.ico
diff --git a/src/branding/centos/branding.css b/src/branding/centos/branding.css
new file mode 100644
index 0000000..be8b70d
--- /dev/null
+++ b/src/branding/centos/branding.css
@@ -0,0 +1,26 @@
+body.login-pf {
+ background: url("bg-plain.jpg") no-repeat 50% 0;
+ background-size: cover;
+ background-color: #101010;
+}
+
+#badge {
+ inline-size: 225px;
+ block-size: 80px;
+ background-image: url("logo.png");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#brand {
+ font-size: 18pt;
+ text-transform: uppercase;
+}
+
+#brand::before {
+ content: "${NAME}";
+}
+
+#index-brand::before {
+ content: "${NAME}";
+}
diff --git a/src/branding/debian/Makefile.am b/src/branding/debian/Makefile.am
new file mode 100644
index 0000000..364f500
--- /dev/null
+++ b/src/branding/debian/Makefile.am
@@ -0,0 +1,10 @@
+debianbrandingdir = $(datadir)/cockpit/branding/debian
+
+dist_debianbranding_DATA = \
+ src/branding/debian/branding.css \
+ $(NULL)
+
+# Opportunistically use debconf debian logos
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/debian-logo.png $(DESTDIR)$(debianbrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/debian-logo.png $(DESTDIR)$(debianbrandingdir)/favicon.ico
diff --git a/src/branding/debian/branding.css b/src/branding/debian/branding.css
new file mode 100644
index 0000000..68a1dcf
--- /dev/null
+++ b/src/branding/debian/branding.css
@@ -0,0 +1,26 @@
+body.login-pf {
+ background: url("bg-plain.jpg") no-repeat 50% 0;
+ background-size: cover;
+ background-color: #101010;
+}
+
+#badge {
+ inline-size: 80px;
+ block-size: 80px;
+ background-image: url("logo.png");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#brand {
+ font-size: 18pt;
+ text-transform: uppercase;
+}
+
+#brand::before {
+ content: "${NAME}";
+}
+
+#index-brand::before {
+ content: "${NAME}";
+}
diff --git a/src/branding/default/Makefile.am b/src/branding/default/Makefile.am
new file mode 100644
index 0000000..ca8cb52
--- /dev/null
+++ b/src/branding/default/Makefile.am
@@ -0,0 +1,12 @@
+defaultbrandingdir = $(datadir)/cockpit/branding/default
+
+dist_defaultbranding_DATA = \
+ src/branding/default/apple-touch-icon.png \
+ src/branding/default/bg-plain.jpg \
+ src/branding/default/branding.css \
+ src/branding/default/brand-large.png \
+ src/branding/default/favicon.ico \
+ src/branding/default/logo.png \
+ $(NULL)
+
+EXTRA_DIST += src/branding/default/logo.svg
diff --git a/src/branding/default/apple-touch-icon.png b/src/branding/default/apple-touch-icon.png
new file mode 100644
index 0000000..f6b78e4
--- /dev/null
+++ b/src/branding/default/apple-touch-icon.png
Binary files differ
diff --git a/src/branding/default/bg-plain.jpg b/src/branding/default/bg-plain.jpg
new file mode 100644
index 0000000..a874f34
--- /dev/null
+++ b/src/branding/default/bg-plain.jpg
Binary files differ
diff --git a/src/branding/default/brand-large.png b/src/branding/default/brand-large.png
new file mode 100644
index 0000000..81cff2d
--- /dev/null
+++ b/src/branding/default/brand-large.png
Binary files differ
diff --git a/src/branding/default/branding.css b/src/branding/default/branding.css
new file mode 100644
index 0000000..285a927
--- /dev/null
+++ b/src/branding/default/branding.css
@@ -0,0 +1,33 @@
+
+.login-note {
+ color: transparent;
+ position: relative;
+}
+
+body.login-pf {
+ background: url("bg-plain.jpg") no-repeat 50% 0;
+ background-size: cover;
+ background-color: #101010;
+}
+
+#badge {
+ inline-size: 73px;
+ block-size: 69px;
+ background-image: url("logo.png");
+ background-repeat: no-repeat;
+}
+
+#brand {
+ min-inline-size: 121px;
+ min-block-size: 18px;
+ background-image: url("brand-large.png");
+ background-repeat: no-repeat;
+}
+
+#index-brand {
+ font-weight: bold;
+}
+
+#index-brand::before {
+ content: "Cockpit";
+}
diff --git a/src/branding/default/favicon.ico b/src/branding/default/favicon.ico
new file mode 100644
index 0000000..0b0c855
--- /dev/null
+++ b/src/branding/default/favicon.ico
Binary files differ
diff --git a/src/branding/default/logo.png b/src/branding/default/logo.png
new file mode 100644
index 0000000..ecbd5a6
--- /dev/null
+++ b/src/branding/default/logo.png
Binary files differ
diff --git a/src/branding/default/logo.svg b/src/branding/default/logo.svg
new file mode 100755
index 0000000..456edd7
--- /dev/null
+++ b/src/branding/default/logo.svg
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="73"
+ height="69"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.4 r9939"
+ sodipodi:docname="logo.svg"
+ inkscape:export-filename="/home/andreasn/development/cockpit-project.github.io/favicon-152.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.959798"
+ inkscape:cx="95.788579"
+ inkscape:cy="66.652029"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:snap-nodes="false"
+ inkscape:snap-bbox="true"
+ inkscape:window-width="1600"
+ inkscape:window-height="834"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:snap-global="false"
+ showborder="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid6061"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-983.36218)">
+ <rect
+ style="opacity:0;color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect6164-3"
+ width="66.496529"
+ height="66.496529"
+ x="2.5824108"
+ y="984.9519"
+ inkscape:export-filename="/home/andreasn/SparkleShare/rhsc-design/cockpit-logo-square.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90" />
+ <path
+ style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#eeeeec;fill-opacity:1;stroke:none;stroke-width:11.64739989999999992;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans"
+ d="m 35.854067,988.0043 c -16.652534,0 -30.1979549,13.5454 -30.1979549,30.1979 0,16.6525 13.5454209,30.198 30.1979549,30.198 16.652534,0 30.197955,-13.5455 30.197955,-30.198 0,-16.6525 -13.545421,-30.1979 -30.197955,-30.1979 z m 0,4.52545 c 14.208664,0 25.684647,11.46385 25.684647,25.67245 0,14.2087 -11.475983,25.6725 -25.684647,25.6725 -14.208658,0 -25.672508,-11.4638 -25.672508,-25.6725 0,-14.2086 11.46385,-25.67245 25.672508,-25.67245 z"
+ id="path6019-8-6-5-3"
+ inkscape:connector-curvature="0" />
+ <path
+ style="fill:#eeeeec;fill-opacity:1;stroke:none;display:inline"
+ d="m 41.250175,1008.4734 -10.259145,10.2021 c -2.245135,2.2326 -3.280033,4.8453 -2.320427,5.8579 0.959575,1.0126 3.53955,0.03 5.784685,-2.2022 l 10.259115,-10.2021 c 2.245106,-2.2326 3.280035,-4.8452 2.320426,-5.8578 -0.959575,-1.0126 -3.53955,-0.03 -5.784654,2.2021 z"
+ id="rect13090-4-6-0-6-3-1-9-8"
+ inkscape:connector-curvature="0"
+ inkscape:export-filename="/home/andreasn/SparkleShare/rhsc-design/cockpit-logo-square.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90" />
+ <g
+ transform="matrix(-1.009087,-1.009087,-1.1060934,1.1060934,-588.46319,2122.6857)"
+ id="g13092-8-7-8-5-1-7-1-4"
+ style="fill:#eeeeec;fill-opacity:1;display:inline"
+ inkscape:export-filename="/home/andreasn/SparkleShare/rhsc-design/cockpit-logo-square.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <path
+ style="fill:#eeeeec;fill-opacity:1;stroke:none"
+ d="m 247.27748,-781.78902 -0.0349,1.94864 -8.91276,-1.10942 -8.97922,1.10942 -0.0349,-1.94864 8.98089,-4.07439 z"
+ id="path13094-9-3-5-6-5-9-7-8"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ </g>
+ <g
+ id="g13098-1-3-7-8-9-1-8-9"
+ transform="matrix(-0.46748244,-0.46748244,-0.82112713,0.82112713,-502.30752,1778.387)"
+ style="fill:#eeeeec;fill-opacity:1;display:inline"
+ inkscape:export-filename="/home/andreasn/SparkleShare/rhsc-design/cockpit-logo-square.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <path
+ style="fill:#eeeeec;fill-opacity:1;stroke:none"
+ d="m 238.3277,-786.83187 9.1156,4.4686 -0.0177,1.4777 -9.14819,-0.77654 -9.08929,0.79974 0.002,-1.47696 z"
+ id="path13100-6-3-1-2-5-0-8-3"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccc" />
+ </g>
+ </g>
+</svg>
diff --git a/src/branding/fedora/Makefile.am b/src/branding/fedora/Makefile.am
new file mode 100644
index 0000000..27e38fc
--- /dev/null
+++ b/src/branding/fedora/Makefile.am
@@ -0,0 +1,11 @@
+fedorabrandingdir = $(datadir)/cockpit/branding/fedora
+
+dist_fedorabranding_DATA = \
+ src/branding/fedora/branding.css \
+ $(NULL)
+
+# Opportunistically use fedora-logos
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/system-logo-white.png $(DESTDIR)$(fedorabrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/fedora-logo-sprite.png $(DESTDIR)$(fedorabrandingdir)/apple-touch-icon.png
+ ln -sTfr $(DESTDIR)/etc/favicon.png $(DESTDIR)$(fedorabrandingdir)/favicon.ico
diff --git a/src/branding/fedora/branding.css b/src/branding/fedora/branding.css
new file mode 100644
index 0000000..af4e9b8
--- /dev/null
+++ b/src/branding/fedora/branding.css
@@ -0,0 +1,26 @@
+body.login-pf {
+ background: url("bg-plain.jpg") no-repeat 50% 0;
+ background-size: cover;
+ background-color: #101010;
+}
+
+#badge {
+ inline-size: 225px;
+ block-size: 80px;
+ background-image: url("logo.png");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#brand {
+ font-size: 18pt;
+ text-transform: uppercase;
+}
+
+#brand::before {
+ content: "${NAME} <b>${VARIANT}</b>";
+}
+
+#index-brand::before {
+ content: "${NAME} <b>${VARIANT}</b>";
+}
diff --git a/src/branding/kubernetes/Makefile.am b/src/branding/kubernetes/Makefile.am
new file mode 100644
index 0000000..cb52acd
--- /dev/null
+++ b/src/branding/kubernetes/Makefile.am
@@ -0,0 +1,5 @@
+kubernetesbrandingdir = $(datadir)/cockpit/branding/kubernetes
+
+dist_kubernetesbranding_DATA = \
+ src/branding/kubernetes/branding.css \
+ $(NULL)
diff --git a/src/branding/kubernetes/branding.css b/src/branding/kubernetes/branding.css
new file mode 100644
index 0000000..787ee92
--- /dev/null
+++ b/src/branding/kubernetes/branding.css
@@ -0,0 +1,30 @@
+body.login-pf {
+ background: url("bg-plain.jpg") no-repeat 50% 0;
+ background-size: cover;
+ background-color: #101010;
+}
+
+#badge {
+ inline-size: 225px;
+ block-size: 80px;
+ background-image: url("logo.png");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#brand {
+ font-size: 18pt;
+ text-transform: uppercase;
+}
+
+#brand::before {
+ content: "${NAME}";
+}
+
+#index-brand {
+ font-weight: bold;
+}
+
+#index-brand::before {
+ content: "${NAME}";
+}
diff --git a/src/branding/opensuse/Makefile.am b/src/branding/opensuse/Makefile.am
new file mode 100644
index 0000000..6036617
--- /dev/null
+++ b/src/branding/opensuse/Makefile.am
@@ -0,0 +1,10 @@
+opensusebrandingdir = $(datadir)/cockpit/branding/opensuse
+
+dist_opensusebranding_DATA = \
+ src/branding/opensuse/branding.css \
+ $(NULL)
+
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/wallpapers/default-1920x1200.jpg $(DESTDIR)$(opensusebrandingdir)/default-1920x1200.jpg
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/distribution-logos/square-hicolor.svg $(DESTDIR)$(opensusebrandingdir)/square-hicolor.svg
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/distribution-logos/favicon.ico $(DESTDIR)$(opensusebrandingdir)/favicon.ico
diff --git a/src/branding/opensuse/branding.css b/src/branding/opensuse/branding.css
new file mode 100644
index 0000000..474c2d8
--- /dev/null
+++ b/src/branding/opensuse/branding.css
@@ -0,0 +1,30 @@
+:root {
+ --ct-color-host-accent: #73ba25 !important;
+}
+
+body.login-pf {
+ background: url("default-1920x1200.jpg") no-repeat 50% 0;
+ background-size: cover;
+ background-color: #42474c;
+}
+
+#badge {
+ inline-size: 5em;
+ block-size: 5em;
+ background-image: url("square-hicolor.svg");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#brand {
+ font-size: 18pt;
+ text-transform: uppercase;
+}
+
+#brand::before {
+ content: "${NAME}";
+}
+
+#index-brand::before {
+ content: "${NAME}";
+}
diff --git a/src/branding/registry/Makefile.am b/src/branding/registry/Makefile.am
new file mode 100644
index 0000000..766cb3e
--- /dev/null
+++ b/src/branding/registry/Makefile.am
@@ -0,0 +1,5 @@
+registrybrandingdir = $(datadir)/cockpit/branding/registry
+
+dist_registrybranding_DATA = \
+ src/branding/kubernetes/branding.css \
+ $(NULL)
diff --git a/src/branding/rhel/Makefile.am b/src/branding/rhel/Makefile.am
new file mode 100644
index 0000000..dbd493a
--- /dev/null
+++ b/src/branding/rhel/Makefile.am
@@ -0,0 +1,11 @@
+rhelbrandingdir = $(datadir)/cockpit/branding/rhel
+
+dist_rhelbranding_DATA = \
+ src/branding/rhel/branding.css \
+ $(NULL)
+
+# Opportunistically use redhat-logos ... yes they're called 'fedora'
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/system-logo-white.png $(DESTDIR)$(rhelbrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/fedora-logo-sprite.png $(DESTDIR)$(rhelbrandingdir)/apple-touch-icon.png
+ ln -sTfr $(DESTDIR)/etc/favicon.png $(DESTDIR)$(rhelbrandingdir)/favicon.ico
diff --git a/src/branding/rhel/branding.css b/src/branding/rhel/branding.css
new file mode 100644
index 0000000..514f39f
--- /dev/null
+++ b/src/branding/rhel/branding.css
@@ -0,0 +1,36 @@
+/* Red Hat Branding */
+
+:root {
+ --ct-color-host-accent: #c00 !important;
+}
+
+body.login-pf {
+ background: url("bg-plain.jpg") no-repeat 50% 0;
+ background-size: cover;
+ background-color: #101010;
+}
+
+#badge {
+ inline-size: 225px;
+ block-size: 80px;
+ background-image: url("logo.png");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#brand {
+ font-size: 18pt;
+ text-transform: uppercase;
+}
+
+#brand::before {
+ content: "${NAME}";
+}
+
+#index-brand {
+ font-weight: bold;
+}
+
+#index-brand::before {
+ content: "${NAME}";
+}
diff --git a/src/branding/scientific/Makefile.am b/src/branding/scientific/Makefile.am
new file mode 100644
index 0000000..c9be4d9
--- /dev/null
+++ b/src/branding/scientific/Makefile.am
@@ -0,0 +1,12 @@
+scientificbrandingdir = $(datadir)/cockpit/branding/scientific
+
+dist_scientificbranding_DATA = \
+ src/branding/scientific/branding.css \
+ $(NULL)
+
+# Opportunistically use Scientific Linux logos.
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/system-logo-white.png $(DESTDIR)$(scientificbrandingdir)/logo.png
+ ln -sTfr $(DESTDIR)/usr/share/pixmaps/fedora-logo-sprite.png $(DESTDIR)$(scientificbrandingdir)/apple-touch-icon.png
+ ln -sTfr $(DESTDIR)/etc/favicon.png $(DESTDIR)$(scientificbrandingdir)/favicon.ico
+
diff --git a/src/branding/scientific/branding.css b/src/branding/scientific/branding.css
new file mode 100644
index 0000000..4a3d33a
--- /dev/null
+++ b/src/branding/scientific/branding.css
@@ -0,0 +1,25 @@
+body.login-pf {
+ background-size: cover;
+ background-color: #777;
+}
+
+#badge {
+ inline-size: 225px;
+ block-size: 80px;
+ background-image: url("logo.png");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#brand {
+ font-size: 18pt;
+ text-transform: uppercase;
+}
+
+#brand::before {
+ content: "${NAME}";
+}
+
+#index-brand::before {
+ content: "${NAME}";
+}
diff --git a/src/branding/ubuntu/Makefile.am b/src/branding/ubuntu/Makefile.am
new file mode 100644
index 0000000..bdbfbfb
--- /dev/null
+++ b/src/branding/ubuntu/Makefile.am
@@ -0,0 +1,10 @@
+ubuntubrandingdir = $(datadir)/cockpit/branding/ubuntu
+
+dist_ubuntubranding_DATA = \
+ src/branding/ubuntu/branding.css \
+ src/branding/ubuntu/favicon.ico \
+ $(NULL)
+
+# Opportunistically use plymouth ubuntu logo
+install-data-hook::
+ ln -sTfr $(DESTDIR)/usr/share/plymouth/ubuntu-logo.png $(DESTDIR)$(ubuntubrandingdir)/logo.png
diff --git a/src/branding/ubuntu/branding.css b/src/branding/ubuntu/branding.css
new file mode 100644
index 0000000..e7d177e
--- /dev/null
+++ b/src/branding/ubuntu/branding.css
@@ -0,0 +1,30 @@
+body.login-pf {
+ background: url("bg-plain.jpg") no-repeat 50% 0;
+ background-size: cover;
+ background-color: #101010;
+}
+
+#badge {
+ inline-size: 220px;
+ block-size: 80px;
+ background-image: url("logo.png");
+ background-size: contain;
+ background-repeat: no-repeat;
+}
+
+#brand {
+ font-size: 18pt;
+ text-transform: uppercase;
+}
+
+#brand::before {
+ content: "${PRETTY_NAME}";
+}
+
+#index-brand {
+ content: "${NAME}";
+}
+
+#index-brand::before {
+ content: "${NAME}";
+}
diff --git a/src/branding/ubuntu/favicon.ico b/src/branding/ubuntu/favicon.ico
new file mode 100644
index 0000000..add4927
--- /dev/null
+++ b/src/branding/ubuntu/favicon.ico
Binary files differ
diff --git a/src/bridge/Makefile.am b/src/bridge/Makefile.am
new file mode 100644
index 0000000..91fec56
--- /dev/null
+++ b/src/bridge/Makefile.am
@@ -0,0 +1,316 @@
+# -----------------------------------------------------------------------------
+# libcockpit-metrics.a: code used in both cockpit-bridge and cockpit-pcp
+
+noinst_LIBRARIES += libcockpit-metrics.a
+
+libcockpit_metrics_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-bridge\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libcockpit_metrics_a_LIBS = \
+ libcockpit-metrics.a \
+ $(libcockpit_common_a_LIBS) \
+ $(fts_LIBS) \
+ -lm \
+ $(NULL)
+
+libcockpit_metrics_a_SOURCES = \
+ src/bridge/cockpitblocksamples.c \
+ src/bridge/cockpitblocksamples.h \
+ src/bridge/cockpitcgroupsamples.c \
+ src/bridge/cockpitcgroupsamples.h \
+ src/bridge/cockpitcpusamples.c \
+ src/bridge/cockpitcpusamples.h \
+ src/bridge/cockpitdisksamples.c \
+ src/bridge/cockpitdisksamples.h \
+ src/bridge/cockpitinternalmetrics.c \
+ src/bridge/cockpitinternalmetrics.h \
+ src/bridge/cockpitmemorysamples.c \
+ src/bridge/cockpitmemorysamples.h \
+ src/bridge/cockpitmetrics.c \
+ src/bridge/cockpitmetrics.h \
+ src/bridge/cockpitmountsamples.c \
+ src/bridge/cockpitmountsamples.h \
+ src/bridge/cockpitnetworksamples.c \
+ src/bridge/cockpitnetworksamples.h \
+ src/bridge/cockpitsamples.c \
+ src/bridge/cockpitsamples.h \
+ $(NULL)
+
+if WITH_OLD_BRIDGE
+
+# -----------------------------------------------------------------------------
+# libcockpit-bridge.a: code used in cockpit-bridge and its tests
+
+noinst_LIBRARIES += libcockpit-bridge.a
+
+libcockpit_bridge_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-bridge\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libcockpit_bridge_a_LIBS = \
+ libcockpit-bridge.a \
+ $(libcockpit_metrics_a_LIBS) \
+ $(libsystemd_LIBS) \
+ $(NULL)
+
+libcockpit_bridge_a_SOURCES = \
+ src/bridge/cockpitconnect.c \
+ src/bridge/cockpitconnect.h \
+ src/bridge/cockpitdbuscache.c \
+ src/bridge/cockpitdbuscache.h \
+ src/bridge/cockpitdbusconfig.c \
+ src/bridge/cockpitdbusinternal.c \
+ src/bridge/cockpitdbusinternal.h \
+ src/bridge/cockpitdbusjson.c \
+ src/bridge/cockpitdbusjson.h \
+ src/bridge/cockpitdbusloginmessages.c \
+ src/bridge/cockpitdbusmachines.c \
+ src/bridge/cockpitdbusmeta.c \
+ src/bridge/cockpitdbusmeta.h \
+ src/bridge/cockpitdbusprocess.c \
+ src/bridge/cockpitdbusrules.c \
+ src/bridge/cockpitdbusrules.h \
+ src/bridge/cockpitdbususer.c \
+ src/bridge/cockpitechochannel.c \
+ src/bridge/cockpitechochannel.h \
+ src/bridge/cockpitfslist.c \
+ src/bridge/cockpitfslist.h \
+ src/bridge/cockpitfsread.c \
+ src/bridge/cockpitfsread.h \
+ src/bridge/cockpitfsreplace.c \
+ src/bridge/cockpitfsreplace.h \
+ src/bridge/cockpitfswatch.c \
+ src/bridge/cockpitfswatch.h \
+ src/bridge/cockpithttpstream.c \
+ src/bridge/cockpithttpstream.h \
+ src/bridge/cockpitinteracttransport.c \
+ src/bridge/cockpitinteracttransport.h \
+ src/bridge/cockpitnullchannel.c \
+ src/bridge/cockpitnullchannel.h \
+ src/bridge/cockpitpackages.c \
+ src/bridge/cockpitpackages.h \
+ src/bridge/cockpitpacketchannel.c \
+ src/bridge/cockpitpacketchannel.h \
+ src/bridge/cockpitpaths.c \
+ src/bridge/cockpitpaths.h \
+ src/bridge/cockpitpeer.c \
+ src/bridge/cockpitpeer.h \
+ src/bridge/cockpitpipechannel.c \
+ src/bridge/cockpitpipechannel.h \
+ src/bridge/cockpitrouter.c \
+ src/bridge/cockpitrouter.h \
+ src/bridge/cockpitstream.c \
+ src/bridge/cockpitstream.h \
+ src/bridge/cockpitwebsocketstream.c \
+ src/bridge/cockpitwebsocketstream.h \
+ $(NULL)
+
+if WITH_POLKIT
+libcockpit_bridge_a_CPPFLAGS += $(polkit_CFLAGS)
+libcockpit_bridge_a_LIBS += $(polkit_LIBS)
+libcockpit_bridge_a_SOURCES += \
+ src/bridge/cockpitpolkitagent.c \
+ src/bridge/cockpitpolkitagent.h \
+ $(NULL)
+endif
+
+# -----------------------------------------------------------------------------
+# PROGRAMS
+
+bin_PROGRAMS += cockpit-bridge
+cockpit_bridge_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS)
+cockpit_bridge_LDADD = $(libcockpit_bridge_a_LIBS)
+cockpit_bridge_SOURCES = src/bridge/bridge.c
+
+libexec_PROGRAMS += cockpit-askpass
+cockpit_askpass_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS)
+cockpit_askpass_LDADD = $(libcockpit_bridge_a_LIBS)
+cockpit_askpass_SOURCES = src/bridge/askpass.c
+
+# -----------------------------------------------------------------------------
+# TESTS
+
+check_PROGRAMS += mock-bridge
+mock_bridge_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS)
+mock_bridge_LDADD = $(libcockpit_bridge_a_LIBS)
+mock_bridge_SOURCES = src/bridge/mock-bridge.c
+
+TEST_PROGRAM += test-bridge
+test_bridge_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_bridge_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_bridge_SOURCES = src/bridge/test-bridge.c
+
+TEST_PROGRAM += test-connect
+test_connect_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_connect_LDADD = $(test_bridge_LDADD) $(TEST_LIBS)
+test_connect_SOURCES = src/bridge/test-connect.c
+
+TEST_PROGRAM += test-dbus-meta
+test_dbus_meta_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_dbus_meta_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_dbus_meta_SOURCES = src/bridge/test-dbus-meta.c
+
+TEST_PROGRAM += test-fs
+test_fs_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_fs_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_fs_SOURCES = src/bridge/test-fs.c
+
+TEST_PROGRAM += test-httpstream
+test_httpstream_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_httpstream_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_httpstream_SOURCES = src/bridge/test-httpstream.c
+
+TEST_PROGRAM += test-metrics
+test_metrics_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_metrics_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_metrics_SOURCES = src/bridge/test-metrics.c
+
+TEST_PROGRAM += test-packages
+test_packages_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_packages_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_packages_SOURCES = src/bridge/test-packages.c
+
+TEST_PROGRAM += test-packet-channel
+test_packet_channel_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_packet_channel_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_packet_channel_SOURCES = src/bridge/test-packet-channel.c
+
+TEST_PROGRAM += test-paths
+test_paths_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_paths_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_paths_SOURCES = src/bridge/test-paths.c
+
+TEST_PROGRAM += test-peer
+test_peer_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_peer_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_peer_SOURCES = src/bridge/test-peer.c
+
+TEST_PROGRAM += test-pipe-channel
+test_pipe_channel_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_pipe_channel_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_pipe_channel_SOURCES = src/bridge/test-pipe-channel.c
+
+TEST_PROGRAM += test-process
+test_process_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_process_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_process_SOURCES = src/bridge/test-process.c
+
+TEST_PROGRAM += test-router
+test_router_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_router_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_router_SOURCES = src/bridge/test-router.c
+
+TEST_PROGRAM += test-rules
+test_rules_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_rules_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_rules_SOURCES = src/bridge/test-rules.c
+
+TEST_PROGRAM += test-stream
+test_stream_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_stream_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_stream_SOURCES = src/bridge/test-stream.c
+
+TEST_PROGRAM += test-websocketstream
+test_websocketstream_CPPFLAGS = $(libcockpit_bridge_a_CPPFLAGS) $(TEST_CPP)
+test_websocketstream_LDADD = $(libcockpit_bridge_a_LIBS) $(TEST_LIBS)
+test_websocketstream_SOURCES = src/bridge/test-websocketstream.c
+
+dist_check_DATA += \
+ src/bridge/mock-resource \
+ src/bridge/mock-pmda.c \
+ src/bridge/mock-pmns \
+ src/bridge/mock-client.crt \
+ src/bridge/mock-client.key \
+ src/bridge/mock-server.crt \
+ src/bridge/mock-server.key \
+ $(NULL)
+
+endif
+
+# -----------------------------------------------------------------------------
+# polkit
+
+# make sure this ends up in the tarball, even with --disable-ssh
+polkit_in_files = src/bridge/org.cockpit-project.cockpit-bridge.policy.in
+EXTRA_DIST += $(polkit_in_files)
+
+if WITH_POLKIT
+polkitdir = $(datadir)/polkit-1/actions
+polkit_DATA = $(polkit_in_files:.policy.in=.policy)
+
+%.policy: %.policy.in $(PO_FILES)
+ $(AM_V_GEN) GETTEXTDATADIRS=$(srcdir)/po msgfmt --xml -d $(top_srcdir)/po --template $< --output $@
+
+CLEANFILES += $(polkit_DATA)
+endif
+
+# -----------------------------------------------------------------------------
+# PCP
+
+if ENABLE_PCP
+
+pmlogconfdir = $(localstatedir)/lib/pcp/config/pmlogconf/tools
+dist_pmlogconf_DATA = src/bridge/pmlogconf/cockpit
+
+libexec_PROGRAMS += cockpit-pcp
+
+noinst_LIBRARIES += libcockpit-pcp.a
+
+libcockpit_pcp_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-pcp\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libcockpit_pcp_a_LIBS = \
+ libcockpit-pcp.a \
+ $(libcockpit_metrics_a_LIBS) \
+ -lpcp_pmda -lpcp -lpcp_import \
+ $(NULL)
+
+libcockpit_pcp_a_SOURCES = \
+ src/bridge/cockpitconnect.c \
+ src/bridge/cockpitconnect.h \
+ src/bridge/cockpitpcpmetrics.c \
+ src/bridge/cockpitpcpmetrics.h \
+ src/bridge/cockpitdbusinternal.c \
+ src/bridge/cockpitdbusinternal.h \
+ src/bridge/cockpitpeer.c \
+ src/bridge/cockpitpeer.h \
+ src/bridge/cockpitrouter.c \
+ src/bridge/cockpitrouter.h \
+ $(libcockpit_bridge_METRICS) \
+ $(NULL)
+
+cockpit_pcp_CPPFLAGS = $(libcockpit_pcp_a_CPPFLAGS)
+cockpit_pcp_LDADD = $(libcockpit_pcp_a_LIBS)
+cockpit_pcp_SOURCES = src/bridge/cockpitpcp.c
+
+check_PROGRAMS += mock-pmda.so
+mock_pmda_so_CFLAGS = -fPIC $(AM_CFLAGS)
+mock_pmda_so_LDFLAGS = -shared
+mock_pmda_so_LDADD = -lpcp_pmda -lpcp
+mock_pmda_so_SOURCES = src/bridge/mock-pmda.c
+
+TEST_PROGRAM += test-pcp
+test_pcp_CPPFLAGS = $(libcockpit_pcp_a_CFLAGS) $(TEST_CPP)
+test_pcp_LDADD = $(libcockpit_pcp_a_LIBS) $(TEST_LIBS) -ldl
+test_pcp_SOURCES = src/bridge/test-pcp.c
+
+TEST_PROGRAM += test-pcp-archives
+test_pcp_archives_CPPFLAGS = $(libcockpit_pcp_a_CPPFLAGS) $(TEST_CPP)
+test_pcp_archives_LDADD = $(libcockpit_pcp_a_LIBS) $(TEST_LIBS)
+test_pcp_archives_SOURCES = src/bridge/test-pcp-archives.c
+
+CLEANFILES += mock-archives/*
+
+endif
+
+EXTRA_DIST += \
+ src/bridge/cockpit.pam.insecure \
+ $(NULL)
diff --git a/src/bridge/askpass.c b/src/bridge/askpass.c
new file mode 100644
index 0000000..ee00e9a
--- /dev/null
+++ b/src/bridge/askpass.c
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Stef Walter <stefw@redhat.com>
+ */
+
+#include "config.h"
+
+#include "common/cockpitframe.h"
+#include "common/cockpithex.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitmemory.h"
+#include "common/cockpittransport.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void
+byte_array_clear_and_free (gpointer data)
+{
+ GByteArray *buffer = data;
+ cockpit_memory_clear (buffer->data, buffer->len);
+ g_byte_array_free (buffer, TRUE);
+}
+
+static JsonObject *
+read_control_message (int fd)
+{
+ JsonObject *options = NULL;
+ GBytes *payload = NULL;
+ GBytes *bytes = NULL;
+ gchar *channel = NULL;
+ guchar *data = NULL;
+ gssize length = 0;
+
+ length = cockpit_frame_read (fd, &data);
+ if (length < 0)
+ {
+ g_message ("couldn't read askpass authorize message: %s", g_strerror (errno));
+ length = 0;
+ }
+ else if (length > 0)
+ {
+ /* This could have a password, so clear it when freeing */
+ bytes = g_bytes_new_with_free_func (data, length, byte_array_clear_and_free,
+ g_byte_array_new_take (data, length));
+ payload = cockpit_transport_parse_frame (bytes, &channel);
+ data = NULL;
+ }
+
+ if (payload == NULL)
+ {
+ if (length > 0)
+ g_message ("askpass did not receive valid message");
+ }
+ else if (channel != NULL)
+ {
+ g_message ("askpass did not receive a control message");
+ }
+ else if (!cockpit_transport_parse_command (payload, NULL, NULL, &options))
+ {
+ g_message ("askpass did not receive a valid control message");
+ }
+
+ g_free (channel);
+
+ if (bytes)
+ g_bytes_unref (bytes);
+ if (payload)
+ g_bytes_unref (payload);
+ free (data);
+ return options;
+}
+
+static gboolean
+write_all (int fd,
+ const char *data,
+ gssize len)
+{
+ gssize res;
+ if (len < 0)
+ len = strlen (data);
+ res = cockpit_fd_write_all (fd, (guchar *)data, len);
+ if (res < 0)
+ {
+ g_message ("couldn't write in askpass: %s", g_strerror (errno));
+ return FALSE;
+ }
+ g_debug ("askpass wrote %d bytes", (gint)res);
+ return TRUE;
+}
+
+static gboolean
+write_control_message (int fd,
+ JsonObject *options)
+{
+ gboolean ret = TRUE;
+ gchar *payload;
+ gchar *prefixed;
+ gsize length;
+
+ payload = cockpit_json_write_object (options, &length);
+ prefixed = g_strdup_printf ("\n%s", payload);
+ if (cockpit_frame_write (fd, (unsigned char *)prefixed, length + 1) < 0)
+ {
+ g_message ("couldn't write authorize message: %s", g_strerror (errno));
+ ret = FALSE;
+ }
+ g_free (prefixed);
+ g_free (payload);
+
+ return ret;
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ GOptionContext *context;
+ JsonObject *request = NULL;
+ JsonObject *reply = NULL;
+ GError *error = NULL;
+ const gchar *env;
+ const gchar *command = NULL;
+ const gchar *field = NULL;
+ const gchar *response = NULL;
+ char *user = NULL;
+ gchar *cookie = NULL;
+ gchar *challenge = NULL;
+ gint ret = 1;
+
+ static GOptionEntry entries[] = {
+ { NULL }
+ };
+
+ context = g_option_context_new (NULL);
+ g_option_context_add_main_entries (context, entries, NULL);
+ g_option_context_set_description (context, "cockpit-bridge uses cockpit-askpass during password prompts.\n");
+
+ g_option_context_parse (context, &argc, &argv, &error);
+ g_option_context_free (context);
+
+ if (error)
+ {
+ g_printerr ("cockpit-askpass: %s\n", error->message);
+ g_error_free (error);
+ return 1;
+ }
+
+ if (isatty (0))
+ {
+ g_printerr ("cockpit-askpass: this command is not meant to be run directly\n");
+ return 2;
+ }
+
+ /*
+ * We don't send an init message. This is meant to be used either after an
+ * "init" message has been sent, or with a caller that makes an exception for
+ * the "authorize" command message.
+ */
+ env = g_getenv ("USER");
+ user = cockpit_hex_encode (env ? env : "", -1);
+ challenge = g_strdup_printf ("plain1:%s:", user);
+ cookie = g_strdup_printf ("askpass%u%u", (unsigned int)getpid (), (unsigned int)time (NULL));
+
+ request = cockpit_transport_build_json ("command", "authorize",
+ "challenge", challenge,
+ "cookie", cookie,
+ "prompt", argv[1],
+ NULL);
+
+ /* Yes, we write to stdin which we expect to be a socketpair() */
+ if (write_control_message (STDIN_FILENO, request))
+ {
+ reply = read_control_message (STDIN_FILENO);
+ if (reply)
+ {
+ if (cockpit_json_get_string (reply, "command", "", &command) &&
+ cockpit_json_get_string (reply, "cookie", "", &field) &&
+ cockpit_json_get_string (reply, "response", "", &response))
+ {
+ if (g_str_equal (field, cookie) && g_str_equal (command, "authorize"))
+ {
+ /* The password is written back on stdout */
+ if (write_all (STDOUT_FILENO, response, -1) && write_all (STDOUT_FILENO, "\n", 1))
+ ret = 0;
+ }
+ else
+ {
+ g_message ("askpass received unexpected %s control message", command);
+ }
+ }
+ else
+ {
+ g_message ("askpass response has invalid control message authorize fields");
+ }
+ }
+ }
+
+ g_free (cookie);
+ g_free (challenge);
+ free (user);
+
+ /* Clear the password memory owned by JsonObject */
+ if (response)
+ cockpit_memory_clear ((gchar *)response, -1);
+
+ if (request)
+ json_object_unref (request);
+ if (reply)
+ json_object_unref (reply);
+
+ return ret;
+}
diff --git a/src/bridge/bridge.c b/src/bridge/bridge.c
new file mode 100644
index 0000000..aaf2ae0
--- /dev/null
+++ b/src/bridge/bridge.c
@@ -0,0 +1,647 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+#include "config.h"
+
+#include "cockpitconnect.h"
+#include "cockpitdbusinternal.h"
+#include "cockpitdbusjson.h"
+#include "cockpitechochannel.h"
+#include "cockpitfslist.h"
+#include "cockpitfsread.h"
+#include "cockpitfswatch.h"
+#include "cockpitfsreplace.h"
+#include "cockpithttpstream.h"
+#include "cockpitinteracttransport.h"
+#include "cockpitnullchannel.h"
+#include "cockpitpackages.h"
+#include "cockpitpacketchannel.h"
+#include "cockpitpipechannel.h"
+#include "cockpitinternalmetrics.h"
+#include "cockpitpolkitagent.h"
+#include "cockpitrouter.h"
+#include "cockpitwebsocketstream.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpitfdpassing.h"
+#include "common/cockpithacks-glib.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitpipetransport.h"
+#include "common/cockpitsystem.h"
+#include "common/cockpitwebresponse.h"
+
+#include <sys/prctl.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <pwd.h>
+
+#include <glib-unix.h>
+#include <glib/gstdio.h>
+#include <gio/gunixsocketaddress.h>
+
+#include <systemd/sd-journal.h>
+
+/* This program is run on each managed server, with the credentials
+ of the user that is logged into the Server Console.
+*/
+
+static CockpitPackages *packages = NULL;
+
+static CockpitPayloadType payload_types[] = {
+ { "dbus-json3", cockpit_dbus_json_get_type },
+ { "http-stream1", cockpit_http_stream_get_type },
+ { "http-stream2", cockpit_http_stream_get_type },
+ { "stream", cockpit_pipe_channel_get_type },
+ { "packet", cockpit_packet_channel_get_type },
+ { "fsread1", cockpit_fsread_get_type },
+ { "fsreplace1", cockpit_fsreplace_get_type },
+ { "fswatch1", cockpit_fswatch_get_type },
+ { "fslist1", cockpit_fslist_get_type },
+ { "null", cockpit_null_channel_get_type },
+ { "echo", cockpit_echo_channel_get_type },
+ { "websocket-stream1", cockpit_web_socket_stream_get_type },
+ { NULL },
+};
+
+static void
+add_router_channels (CockpitRouter *router)
+{
+ JsonObject *match;
+
+ match = json_object_new ();
+ json_object_set_string_member (match, "payload", "metrics1");
+ json_object_set_string_member (match, "source", "internal");
+ cockpit_router_add_channel (router, match, cockpit_internal_metrics_get_type);
+ json_object_unref (match);
+}
+
+static void
+on_closed_set_flag (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gboolean *flag = user_data;
+ *flag = TRUE;
+}
+
+static void
+send_init_command (CockpitTransport *transport,
+ gboolean interactive)
+{
+ const gchar *checksum;
+ JsonObject *object;
+ JsonObject *block;
+ GHashTable *os_release;
+ gchar **names;
+ GBytes *bytes;
+ gint i;
+ gchar *session_id;
+
+ object = json_object_new ();
+ json_object_set_string_member (object, "command", "init");
+ json_object_set_int_member (object, "version", 1);
+
+ /*
+ * When in interactive mode pretend we received an init
+ * message, and don't print one out.
+ */
+ if (interactive)
+ {
+ json_object_set_string_member (object, "host", "localhost");
+ }
+ else
+ {
+ checksum = cockpit_packages_get_checksum (packages);
+ if (checksum)
+ json_object_set_string_member (object, "checksum", checksum);
+
+ /* This is encoded as an object to allow for future expansion */
+ block = json_object_new ();
+ names = cockpit_packages_get_names (packages);
+ for (i = 0; names && names[i] != NULL; i++)
+ json_object_set_null_member (block, names[i]);
+ json_object_set_object_member (object, "packages", block);
+ g_free (names);
+
+ os_release = cockpit_system_load_os_release ();
+ if (os_release)
+ {
+ block = cockpit_json_from_hash_table (os_release,
+ cockpit_system_os_release_fields ());
+ if (block)
+ json_object_set_object_member (object, "os-release", block);
+ g_hash_table_unref (os_release);
+ }
+
+ session_id = secure_getenv ("XDG_SESSION_ID");
+ if (session_id)
+ json_object_set_string_member (object, "session-id", session_id);
+
+ block = json_object_new ();
+ json_object_set_boolean_member (block, "explicit-superuser", TRUE);
+ json_object_set_object_member (object, "capabilities", block);
+ }
+
+ bytes = cockpit_json_write_bytes (object);
+ json_object_unref (object);
+
+ if (interactive)
+ cockpit_transport_emit_recv (transport, NULL, bytes);
+ else
+ cockpit_transport_send (transport, NULL, bytes);
+ g_bytes_unref (bytes);
+}
+
+static GSubprocess *
+start_helper_process (const gchar * const *argv,
+ const gchar *socket_pattern,
+ const gchar *socket_envvar)
+{
+ g_return_val_if_fail (argv[0] != NULL, NULL);
+
+ {
+ const gchar *env = g_getenv (socket_envvar);
+ if (env && env[0])
+ return NULL;
+ }
+
+ g_autoptr(GError) error = NULL;
+ /* The DBus daemon produces useless messages on stderr mixed in */
+ g_autoptr(GSubprocessLauncher) launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE |
+ G_SUBPROCESS_FLAGS_STDERR_SILENCE);
+ g_subprocess_launcher_unsetenv (launcher, "G_DEBUG");
+ g_autoptr(GSubprocess) process = g_subprocess_launcher_spawnv (launcher, argv, &error);
+
+ if (error != NULL)
+ {
+ if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT))
+ g_debug ("couldn't start %s: %s", argv[0], error->message);
+ else
+ g_message ("couldn't start %s: %s", argv[0], error->message);
+ return NULL;
+ }
+
+ g_debug ("launched %s: %s", argv[0], g_subprocess_get_identifier (process));
+
+ /* get the first line of output to figure out the socket address */
+ g_autoptr (GDataInputStream) stream = g_data_input_stream_new (g_subprocess_get_stdout_pipe (process));
+ g_autofree gchar *first_line = g_data_input_stream_read_line (stream, NULL, NULL, &error);
+ if (!first_line)
+ {
+ g_warning ("couldn't read address from %s: %s", argv[0], error->message);
+ g_subprocess_force_exit (process);
+ return NULL;
+ }
+
+ g_autoptr(GRegex) regex = g_regex_new (socket_pattern, G_REGEX_RAW, 0, &error);
+ g_assert_no_error (error);
+
+ g_autoptr(GMatchInfo) info = NULL;
+ if (!g_regex_match (regex, first_line, 0, &info))
+ {
+ g_warning ("output from %s didn't match expected pattern %s", argv[0], socket_pattern);
+ g_subprocess_force_exit (process);
+ return NULL;
+ }
+
+ g_autofree gchar *socket_address = g_match_info_fetch (info, 1);
+ cockpit_setenv_check (socket_envvar, socket_address, TRUE);
+
+ return g_steal_pointer (&process);
+}
+
+static GSubprocess *
+start_dbus_daemon (void)
+{
+ const char * const cmd[] = { "dbus-daemon", "--print-address", "--session", NULL };
+ return start_helper_process (cmd, "^(.*)$", "DBUS_SESSION_BUS_ADDRESS");
+}
+
+static GSubprocess *
+start_ssh_agent (void)
+{
+ const char * const cmd[] = { "ssh-agent", "-s", "-D", NULL };
+ return start_helper_process (cmd, "SSH_AUTH_SOCK=([^;]*);", "SSH_AUTH_SOCK");
+}
+
+static gboolean
+on_signal_done (gpointer data)
+{
+ gboolean *closed = data;
+ *closed = TRUE;
+ return TRUE;
+}
+
+static struct passwd *
+getpwuid_a (uid_t uid)
+{
+ int err;
+ long bufsize = sysconf (_SC_GETPW_R_SIZE_MAX);
+ struct passwd *ret = NULL;
+ struct passwd *buf;
+
+ if (bufsize <= 0)
+ bufsize = 8192;
+
+ buf = g_malloc (sizeof(struct passwd) + bufsize);
+ err = getpwuid_r (uid, buf, (char *)(buf + 1), bufsize, &ret);
+
+ if (ret == NULL)
+ {
+ free (buf);
+ if (err == 0)
+ err = ENOENT;
+ errno = err;
+ }
+
+ return ret;
+}
+
+static void
+update_router (CockpitRouter *router,
+ gboolean privileged_peer)
+{
+ if (!privileged_peer)
+ {
+ GList *bridges = cockpit_packages_get_bridges (packages);
+ cockpit_router_set_bridges (router, bridges);
+ g_list_free (bridges);
+ }
+}
+
+static CockpitRouter *
+setup_router (CockpitTransport *transport,
+ gboolean privileged_peer)
+{
+ CockpitRouter *router = NULL;
+
+ packages = cockpit_packages_new ();
+
+ router = cockpit_router_new (transport, payload_types, NULL);
+ add_router_channels (router);
+
+ /* This has to happen after add_router_channels as the
+ * packages based bridges should have priority.
+ */
+ update_router (router, privileged_peer);
+
+ return router;
+}
+
+struct CallUpdateRouterData {
+ CockpitRouter *router;
+ gboolean privileged_peer;
+};
+
+static void
+call_update_router (gconstpointer user_data)
+{
+ const struct CallUpdateRouterData *data = user_data;
+ update_router (data->router, data->privileged_peer);
+}
+
+static int
+run_bridge (const gchar *interactive,
+ gboolean privileged_peer)
+{
+ CockpitTransport *transport;
+ CockpitRouter *router;
+ gboolean terminated = FALSE;
+ gboolean interrupted = FALSE;
+ gboolean closed = FALSE;
+ const gchar *directory;
+ struct passwd *pwd;
+ g_autoptr (GSubprocess) dbus_daemon_process = NULL;
+ g_autoptr (GSubprocess) ssh_agent_process = NULL;
+ guint sig_term;
+ guint sig_int;
+ uid_t uid;
+ struct CallUpdateRouterData call_update_router_data;
+
+ cockpit_hacks_redirect_gdebug_to_stderr ();
+
+ /* Always set environment variables early */
+ uid = geteuid();
+ pwd = getpwuid_a (uid);
+ if (pwd == NULL)
+ {
+ g_message ("couldn't get user info: %s", g_strerror (errno));
+ }
+ else
+ {
+ cockpit_setenv_check ("USER", pwd->pw_name, TRUE);
+ cockpit_setenv_check ("HOME", pwd->pw_dir, TRUE);
+ cockpit_setenv_check ("SHELL", pwd->pw_shell, TRUE);
+ }
+
+ /* Set a path if nothing is set */
+ cockpit_setenv_check ("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", FALSE);
+
+ /*
+ * The bridge always runs from within $XDG_RUNTIME_DIR
+ * This makes it easy to create user sockets and/or files.
+ */
+ if (!privileged_peer)
+ {
+ directory = g_get_user_runtime_dir ();
+ if (g_mkdir_with_parents (directory, 0700) < 0)
+ g_warning ("couldn't create runtime dir: %s: %s", directory, g_strerror (errno));
+ else if (g_chdir (directory) < 0)
+ g_warning ("couldn't change to runtime dir: %s: %s", directory, g_strerror (errno));
+ }
+
+ /* Start daemons if necessary */
+ if (!interactive && !privileged_peer)
+ {
+ dbus_daemon_process = start_dbus_daemon ();
+ ssh_agent_process = start_ssh_agent ();
+ }
+
+ sig_term = g_unix_signal_add (SIGTERM, on_signal_done, &terminated);
+ sig_int = g_unix_signal_add (SIGINT, on_signal_done, &interrupted);
+
+ cockpit_dbus_internal_startup (interactive != NULL);
+
+ if (interactive)
+ {
+ /* Allow skipping the init message when interactive */
+ transport = cockpit_interact_transport_new (0, 1, interactive);
+ }
+ else
+ {
+ transport = cockpit_pipe_transport_new_fds ("stdio", 0, 1);
+ }
+
+ router = setup_router (transport, privileged_peer);
+
+#ifdef WITH_POLKIT
+ gpointer polkit_agent = NULL;
+ if (uid != 0)
+ {
+ if (!interactive)
+ polkit_agent = cockpit_polkit_agent_register (transport, router, NULL);
+ }
+#endif
+
+ cockpit_dbus_user_startup (pwd);
+ cockpit_dbus_process_startup ();
+ cockpit_dbus_machines_startup ();
+ cockpit_dbus_config_startup ();
+ cockpit_packages_dbus_startup (packages);
+ cockpit_dbus_login_messages_startup ();
+ cockpit_router_dbus_startup (router);
+
+ call_update_router_data.router = router;
+ call_update_router_data.privileged_peer = privileged_peer;
+ cockpit_packages_on_change (packages, call_update_router, &call_update_router_data);
+
+ g_free (pwd);
+ pwd = NULL;
+
+ g_signal_connect (transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+ send_init_command (transport, interactive ? TRUE : FALSE);
+
+ while (!terminated && !closed && !interrupted)
+ g_main_context_iteration (NULL, TRUE);
+
+#ifdef WITH_POLKIT
+ if (polkit_agent)
+ cockpit_polkit_agent_unregister (polkit_agent);
+#endif
+
+ g_object_unref (router);
+ g_object_unref (transport);
+
+ cockpit_packages_on_change (packages, NULL, NULL);
+
+ cockpit_dbus_machines_cleanup ();
+ cockpit_dbus_internal_cleanup ();
+
+ if (dbus_daemon_process)
+ g_subprocess_send_signal (dbus_daemon_process, SIGTERM);
+ if (ssh_agent_process)
+ g_subprocess_send_signal (ssh_agent_process, SIGTERM);
+
+ g_source_remove (sig_term);
+ g_source_remove (sig_int);
+
+ /* HACK: Valgrind contains a bug that causes it to hang when the main
+ * thread exits quickly in response to a signal received by a handler
+ * in another thread, when that other thread is waiting in a syscall.
+ * Avoid that situation by delaying our exit here, but only under
+ * Valgrind.
+ *
+ * Remove this when https://bugs.kde.org/show_bug.cgi?id=409367 is
+ * fixed and widely distributed.
+ */
+ if (strstr (g_getenv ("LD_PRELOAD") ?: "", "valgrind") != NULL)
+ g_usleep (5 * G_TIME_SPAN_SECOND);
+
+ /* So the caller gets the right signal */
+ if (terminated)
+ raise (SIGTERM);
+
+ return 0;
+}
+
+static void
+print_rules (gboolean opt_privileged)
+{
+ CockpitRouter *router = NULL;
+ CockpitTransport *transport = cockpit_interact_transport_new (0, 1, "--");
+
+ router = setup_router (transport, opt_privileged);
+
+ cockpit_router_dump_rules (router);
+
+ g_object_unref (router);
+ g_object_unref (transport);
+}
+
+static void
+print_version (void)
+{
+ gint i, offset, len;
+
+ g_print ("Version: %s\n", PACKAGE_VERSION);
+ g_print ("Protocol: 1\n");
+
+ g_print ("Payloads: ");
+ offset = 10;
+ for (i = 0; payload_types[i].name != NULL; i++)
+ {
+ len = strlen (payload_types[i].name);
+ if (offset + len > 70)
+ {
+ g_print ("\n");
+ offset = 0;
+ }
+
+ if (offset == 0)
+ {
+ g_print (" ");
+ offset = 4;
+ };
+
+ g_print ("%s ", payload_types[i].name);
+ offset += len + 1;
+ }
+ g_print ("\n");
+
+ g_print ("Authorization: crypt1\n");
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ GOptionContext *context;
+ GError *error = NULL;
+ int ret;
+
+ static gboolean opt_packages = FALSE;
+ static gboolean opt_rules = FALSE;
+ static gboolean opt_privileged = FALSE;
+ static gboolean opt_version = FALSE;
+ static gchar *opt_interactive = NULL;
+
+ static GOptionEntry entries[] = {
+ { "interact", 0, 0, G_OPTION_ARG_STRING, &opt_interactive, "Interact with the raw protocol", "boundary" },
+ { "privileged", 0, 0, G_OPTION_ARG_NONE, &opt_privileged, "Privileged copy of bridge", NULL },
+ { "packages", 0, 0, G_OPTION_ARG_NONE, &opt_packages, "Show Cockpit package information", NULL },
+ { "rules", 0, 0, G_OPTION_ARG_NONE, &opt_rules, "Show Cockpit bridge rules", NULL },
+ { "version", 0, 0, G_OPTION_ARG_NONE, &opt_version, "Show Cockpit version information", NULL },
+ { NULL }
+ };
+
+ signal (SIGPIPE, SIG_IGN);
+
+ if (g_strv_contains ((const gchar **) argv, "--privileged"))
+ {
+ /* We are being spawned, under sudo or pkexec, by the user's copy
+ * of the bridge. In that case, the first thing that will happen,
+ * if we receive our stderr via the socket that is our stdin.
+ */
+ const char *msg = "\n{\"command\": \"send-stderr\"}";
+ g_print ("%zu\n%s", strlen (msg), msg);
+
+ int parent_stderr_fd;
+ int r = cockpit_socket_receive_fd (STDIN_FILENO, &parent_stderr_fd);
+ if (r == 0)
+ {
+ /* on EOF, just silently exit */
+ return 0;
+ }
+ else if (r == -1)
+ {
+ g_printerr ("cockpit-bridge: recvmsg(stdin) failed: %m\n");
+ return 1;
+ }
+ else if (parent_stderr_fd == -1)
+ {
+ g_printerr ("cockpit-bridge: message from stdin contains no fd\n");
+ return 1;
+ }
+ else
+ {
+ r = dup2 (parent_stderr_fd, STDERR_FILENO);
+ g_assert (r == STDERR_FILENO); /* that should really never fail */
+ close (parent_stderr_fd);
+ }
+ }
+ else if (g_getenv ("SSH_CONNECTION") && !g_log_writer_is_journald (2) && ! isatty(2))
+ {
+ /* In case we are run via sshd and we have journald, make sure all
+ * logging output ends up in the journal on *this* machine, not sent
+ * back to the client.
+ */
+ int fd = sd_journal_stream_fd ("cockpit/ssh", LOG_WARNING, 0);
+
+ /* If it didn't work, then there's no journal. That's OK: we'll
+ * just send the output back to the client after all.
+ *
+ * If it did work, rename the fd to 2 (stderr).
+ */
+ if (fd >= 0)
+ {
+ int r = dup2 (fd, 2);
+ g_assert (r == 2); /* that should really never fail */
+ close (fd);
+ }
+ }
+
+ /*
+ * We have to tell GLib about an alternate default location for XDG_DATA_DIRS
+ * if we've been compiled with a different prefix. GLib caches that, so need
+ * to do this very early.
+ */
+ if (!g_getenv ("XDG_DATA_DIRS") && !g_str_equal (DATADIR, "/usr/share"))
+ cockpit_setenv_check ("XDG_DATA_DIRS", DATADIR, TRUE);
+
+ cockpit_setenv_check ("LANG", "C.UTF-8", FALSE);
+ cockpit_setenv_check ("GSETTINGS_BACKEND", "memory", TRUE);
+
+ context = g_option_context_new (NULL);
+ g_option_context_add_main_entries (context, entries, NULL);
+ g_option_context_set_description (context,
+ "cockpit-bridge is run automatically inside of a Cockpit session. When\n"
+ "run from the command line one of the options above must be specified.\n");
+
+ g_option_context_parse (context, &argc, &argv, &error);
+ g_option_context_free (context);
+
+ if (error)
+ {
+ g_printerr ("cockpit-bridge: %s\n", error->message);
+ g_error_free (error);
+ return 1;
+ }
+
+ if (opt_packages)
+ {
+ cockpit_packages_dump ();
+ return 0;
+ }
+ else if (opt_rules)
+ {
+ print_rules (opt_privileged);
+ return 0;
+ }
+ else if (opt_version)
+ {
+ print_version ();
+ return 0;
+ }
+
+ if (!opt_interactive && isatty (1))
+ {
+ g_printerr ("cockpit-bridge: no option specified\n");
+ return 2;
+ }
+
+ ret = run_bridge (opt_interactive, opt_privileged);
+
+ if (packages)
+ cockpit_packages_free (packages);
+
+ g_free (opt_interactive);
+ return ret;
+}
diff --git a/src/bridge/cockpit.pam.insecure b/src/bridge/cockpit.pam.insecure
new file mode 100644
index 0000000..a438ced
--- /dev/null
+++ b/src/bridge/cockpit.pam.insecure
@@ -0,0 +1,10 @@
+#%PAM-1.0
+auth required pam_unix.so
+
+account required pam_unix.so
+
+-session required pam_selinux.so close
+-session optional pam_systemd.so
+session required pam_unix.so
+-session required pam_selinux.so open
+session optional pam_keyinit.so force revoke
diff --git a/src/bridge/cockpitblocksamples.c b/src/bridge/cockpitblocksamples.c
new file mode 100644
index 0000000..b1368f4
--- /dev/null
+++ b/src/bridge/cockpitblocksamples.c
@@ -0,0 +1,118 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitblocksamples.h"
+
+#include <stdio.h>
+#include <string.h>
+
+void
+cockpit_block_samples (CockpitSamples *samples)
+{
+ gchar *contents = NULL;
+ gsize len;
+ GError *error = NULL;
+ gchar **lines = NULL;
+ guint n;
+ static gboolean not_supported = FALSE;
+
+ if (not_supported)
+ goto out;
+
+ if (!g_file_get_contents ("/proc/diskstats", &contents, &len, &error))
+ {
+ g_message ("error loading contents /proc/diskstats: %s", error->message);
+ not_supported = TRUE;
+ goto out;
+ }
+
+ lines = g_strsplit (contents, "\n", -1);
+ for (n = 0; lines != NULL && lines[n] != NULL; n++)
+ {
+ const gchar *line = lines[n];
+ guint num_parsed;
+ gint dev_major, dev_minor;
+ gchar dev_name[128];
+ guint64 num_reads, num_reads_merged, num_sectors_read, num_msec_reading;
+ guint64 num_writes, num_writes_merged, num_sectors_written, num_msec_writing;
+ guint64 num_io_in_progress, num_msec_doing_io, weighted_num_msec_doing_io;
+
+ if (strlen (line) == 0)
+ continue;
+
+ /* From http://www.kernel.org/doc/Documentation/iostats.txt
+ *
+ * Field 1 -- # of reads completed
+ * This is the total number of reads completed successfully.
+ * Field 2 -- # of reads merged, field 6 -- # of writes merged
+ * Reads and writes which are adjacent to each other may be merged for
+ * efficiency. Thus two 4K reads may become one 8K read before it is
+ * ultimately handed to the disk, and so it will be counted (and queued)
+ * as only one I/O. This field lets you know how often this was done.
+ * Field 3 -- # of sectors read
+ * This is the total number of sectors read successfully.
+ * Field 4 -- # of milliseconds spent reading
+ * This is the total number of milliseconds spent by all reads (as
+ * measured from __make_request() to end_that_request_last()).
+ * Field 5 -- # of writes completed
+ * This is the total number of writes completed successfully.
+ * Field 7 -- # of sectors written
+ * This is the total number of sectors written successfully.
+ * Field 8 -- # of milliseconds spent writing
+ * This is the total number of milliseconds spent by all writes (as
+ * measured from __make_request() to end_that_request_last()).
+ * Field 9 -- # of I/Os currently in progress
+ * The only field that should go to zero. Incremented as requests are
+ * given to appropriate struct request_queue and decremented as they finish.
+ * Field 10 -- # of milliseconds spent doing I/Os
+ * This field increases so long as field 9 is nonzero.
+ * Field 11 -- weighted # of milliseconds spent doing I/Os
+ * This field is incremented at each I/O start, I/O completion, I/O
+ * merge, or read of these stats by the number of I/Os in progress
+ * (field 9) times the number of milliseconds spent doing I/O since the
+ * last update of this field. This can provide an easy measure of both
+ * I/O completion time and the backlog that may be accumulating.
+ */
+
+ num_parsed = sscanf (line,
+ "%d %d %127s"
+ " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT
+ " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT
+ " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT,
+ &dev_major, &dev_minor, dev_name,
+ &num_reads, &num_reads_merged, &num_sectors_read, &num_msec_reading,
+ &num_writes, &num_writes_merged, &num_sectors_written, &num_msec_writing,
+ &num_io_in_progress, &num_msec_doing_io, &weighted_num_msec_doing_io);
+ if (num_parsed != 14)
+ {
+ g_message ("error parsing line %d of file /proc/diskstats (num_parsed = %d): %s", n, num_parsed, line);
+ continue;
+ }
+
+ cockpit_samples_sample (samples, "block.device.read", dev_name, num_sectors_read * 512);
+ cockpit_samples_sample (samples, "block.device.written", dev_name, num_sectors_written * 512);
+ }
+
+out:
+ g_clear_error (&error);
+ g_strfreev (lines);
+ g_free (contents);
+}
diff --git a/src/bridge/cockpitblocksamples.h b/src/bridge/cockpitblocksamples.h
new file mode 100644
index 0000000..e73d784
--- /dev/null
+++ b/src/bridge/cockpitblocksamples.h
@@ -0,0 +1,32 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_BLOCK_SAMPLES_H__
+#define COCKPIT_BLOCK_SAMPLES_H__
+
+#include "cockpitsamples.h"
+
+G_BEGIN_DECLS
+
+void cockpit_block_samples (CockpitSamples *samples);
+
+
+G_END_DECLS
+
+#endif /* COCKPIT_BLOCK_SAMPLES_H__ */
diff --git a/src/bridge/cockpitcgroupsamples.c b/src/bridge/cockpitcgroupsamples.c
new file mode 100644
index 0000000..ef97532
--- /dev/null
+++ b/src/bridge/cockpitcgroupsamples.c
@@ -0,0 +1,289 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitcgroupsamples.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <fts.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+const char *cockpit_cgroupv1_memory_root = "/sys/fs/cgroup/memory";
+const char *cockpit_cgroupv1_cpuacct_root = "/sys/fs/cgroup/cpuacct";
+const char *cockpit_cgroupv2_root = "/sys/fs/cgroup";
+
+static const char *
+read_file (int dirfd,
+ char *buf,
+ size_t bufsize,
+ const char *cgroup,
+ const char *fname)
+{
+ const char *ret = NULL;
+
+ const int fd = openat (dirfd, fname, O_RDONLY);
+ if (fd < 0)
+ {
+ if (errno == ENOENT || errno == ENODEV)
+ g_debug ("samples file not found: %s/%s", cgroup, fname);
+ else
+ g_message ("error opening file: %s/%s: %m", cgroup, fname);
+ goto out;
+ }
+
+ /* don't do fancy retry/error handling here -- we know what cgroupfs attributes look like,
+ * it's a virtual file system (does not block/no multiple reads), and it's ok to miss
+ * one sample due to EINTR or some race condition */
+ const ssize_t len = read (fd, buf, bufsize);
+ if (len < 0)
+ {
+ if (errno == ENODEV) /* similar to error at open() */
+ g_debug ("error loading file: %s/%s: %m", cgroup, fname);
+ else
+ g_message ("error loading file: %s/%s: %m", cgroup, fname);
+ goto out;
+ }
+ /* we really expect a much smaller read; if we get a full buffer, there's likely
+ * more data, and we are misinterpreting stuff */
+ if (len >= bufsize)
+ {
+ g_warning ("cgroupfs value %s/%s is too large", cgroup, fname);
+ goto out;
+ }
+ buf[len] = '\0';
+ ret = buf;
+
+out:
+ if (fd >= 0)
+ close (fd);
+ return ret;
+}
+
+
+static gint64
+read_int64 (int dirfd,
+ const char *cgroup,
+ const char *fname)
+{
+ char buf[30];
+ const char *contents = read_file (dirfd, buf, sizeof buf, cgroup, fname);
+
+ if (contents == NULL)
+ return -1;
+ /* no error checking; these often have values like "max" which we want to treat as "invalid/absent" */
+ return atoll(contents);
+}
+
+static gint64
+read_keyed_int64 (int dirfd,
+ const char *cgroup,
+ const char *fname,
+ const char *key)
+{
+ char buf[256];
+ const char *contents = read_file (dirfd, buf, sizeof buf, cgroup, fname);
+ const char *match;
+ size_t key_len = strlen (key);
+ char *endptr = NULL;
+ gint64 result;
+
+ if (contents == NULL)
+ return -1;
+
+ /* search for a word match of key */
+ match = contents;
+ for (;;)
+ {
+ match = strstr (match, key);
+ if (match == NULL)
+ return -1;
+ /* either matches at start of string, or after a line break */
+ if (match == contents || match[-1] == '\n')
+ break;
+ match += key_len;
+ }
+
+ result = strtoll (match + key_len, &endptr, 10);
+ if (!endptr || (*endptr != '\0' && *endptr != '\n'))
+ {
+ g_warning ("cgroupfs file %s/%s value '%s' is an invalid number", cgroup, fname, contents);
+ return -1;
+ }
+ return result;
+}
+
+static void
+collect_memory_v1 (CockpitSamples *samples,
+ int dirfd,
+ const char *cgroup)
+{
+ gint64 val;
+
+ val = read_int64 (dirfd, cgroup, "memory.usage_in_bytes");
+ if (val > 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.memory.usage", cgroup, val);
+
+ val = read_int64 (dirfd, cgroup, "memory.limit_in_bytes");
+ /* If at max for arch, then unlimited => zero */
+ if (val > 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.memory.limit", cgroup, val);
+
+ val = read_int64 (dirfd, cgroup, "memory.memsw.usage_in_bytes");
+ if (val >= 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.memory.sw-usage", cgroup, val);
+
+ val = read_int64 (dirfd, cgroup, "memory.memsw.limit_in_bytes");
+ /* If at max for arch, then unlimited => zero */
+ if (val > 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.memory.sw-limit", cgroup, val);
+}
+
+static void
+collect_cpu_v1 (CockpitSamples *samples,
+ int dirfd,
+ const char *cgroup)
+{
+ gint64 val;
+
+ val = read_int64 (dirfd, cgroup, "cpuacct.usage");
+ if (val >= 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.cpu.usage", cgroup, val/1000000);
+
+ val = read_int64 (dirfd, cgroup, "cpu.shares");
+ if (val > 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.cpu.shares", cgroup, val);
+}
+
+static void
+collect_v2 (CockpitSamples *samples,
+ int dirfd,
+ const char *cgroup)
+{
+ gint64 val;
+
+ /* memory.current: single unsigned value in bytes */
+ val = read_int64 (dirfd, cgroup, "memory.current");
+ if (val >= 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.memory.usage", cgroup, val);
+
+ /* memory.max: literally says "max" if there is no limit set, which ends up as "0" after integer conversion;
+ * only create samples for actually limited cgroups */
+ val = read_int64 (dirfd, cgroup, "memory.max");
+ if (val > 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.memory.limit", cgroup, val);
+
+ /* same as above for swap */
+ val = read_int64 (dirfd, cgroup, "memory.swap.current");
+ if (val >= 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.memory.sw-usage", cgroup, val);
+
+ val = read_int64 (dirfd, cgroup, "memory.swap.max");
+ if (val > 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.memory.sw-limit", cgroup, val);
+
+ /* cpu.weight: only exists if cpu controller is enabled; integer in range [1, 10000] */
+ val = read_int64 (dirfd, cgroup, "cpu.weight");
+ if (val > 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.cpu.shares", cgroup, val);
+
+ /* cpu.stat: keyed file:
+ usage_usec 50000
+ user_usec 40000
+ system_usec 10000
+ */
+ val = read_keyed_int64 (dirfd, cgroup, "cpu.stat", "usage_usec ");
+ if (val >= 0 && val < G_MAXINT64)
+ cockpit_samples_sample (samples, "cgroup.cpu.usage", cgroup, val/1000);
+}
+
+static void
+notice_cgroups_in_hierarchy (CockpitSamples *samples,
+ const char *root_dir,
+ void (* collect) (CockpitSamples *, int, const char *))
+{
+ const char *paths[] = { root_dir, NULL };
+ gsize root_dir_len = strlen (root_dir);
+ FTSENT *ent;
+ FTS *fs;
+
+ fs = fts_open ((char **)paths, FTS_NOCHDIR | FTS_COMFOLLOW, NULL);
+ if (fs)
+ {
+ while((ent = fts_read (fs)) != NULL)
+ {
+ if (ent->fts_info == FTS_D)
+ {
+ const char *f = ent->fts_path + root_dir_len;
+ int dfd;
+
+ if (*f == '/')
+ f++;
+ dfd = open (ent->fts_path, O_PATH | O_DIRECTORY);
+ if (dfd >= 0)
+ {
+ collect (samples, dfd, f);
+ close (dfd);
+ }
+ else if (errno != ENOENT)
+ {
+ g_message ("error opening cgroup directory: %s: %m", ent->fts_path);
+ }
+ }
+ }
+ fts_close (fs);
+ }
+}
+
+
+void
+cockpit_cgroup_samples (CockpitSamples *samples)
+{
+ static int cgroup_ver = 0; /* 0: uninitialized */
+
+ /* do we have cgroupv2? initialize this just once */
+ if (cgroup_ver == 0)
+ {
+ cgroup_ver = (access ("/sys/fs/cgroup/cgroup.controllers", F_OK) == 0) ? 2 : 1;
+ g_debug ("cgroup samples: detected cgroup version: %i", cgroup_ver);
+ }
+
+ if (cgroup_ver == 2)
+ {
+ /* For cgroupv2, the groups are directly in /sys/fs/cgroup/<name>/.../.
+ Inside, we are looking for files "memory.current" or "cpu.stat".
+ */
+ notice_cgroups_in_hierarchy (samples, cockpit_cgroupv2_root, collect_v2);
+ }
+ else
+ {
+ /* For cgroupv1, we are looking for files like
+
+ /sys/fs/cgroup/memory/.../memory.usage_in_bytes
+ /sys/fs/cgroup/memory/.../memory.limit_in_bytes
+ /sys/fs/cgroup/cpuacct/.../cpuacct.usage
+ */
+ notice_cgroups_in_hierarchy (samples, cockpit_cgroupv1_memory_root, collect_memory_v1);
+ notice_cgroups_in_hierarchy (samples, cockpit_cgroupv1_cpuacct_root, collect_cpu_v1);
+ }
+}
diff --git a/src/bridge/cockpitcgroupsamples.h b/src/bridge/cockpitcgroupsamples.h
new file mode 100644
index 0000000..4ee09b3
--- /dev/null
+++ b/src/bridge/cockpitcgroupsamples.h
@@ -0,0 +1,32 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_CGROUP_SAMPLES_H__
+#define COCKPIT_CGROUP_SAMPLES_H__
+
+#include "cockpitsamples.h"
+
+G_BEGIN_DECLS
+
+void cockpit_cgroup_samples (CockpitSamples *samples);
+
+
+G_END_DECLS
+
+#endif /* COCKPIT_CGROUP_SAMPLES_H__ */
diff --git a/src/bridge/cockpitconnect.c b/src/bridge/cockpitconnect.c
new file mode 100644
index 0000000..c0e6ee6
--- /dev/null
+++ b/src/bridge/cockpitconnect.c
@@ -0,0 +1,657 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitconnect.h"
+
+#include "common/cockpitjson.h"
+#include "common/cockpitloopback.h"
+
+#include <glib/gstdio.h>
+
+#include <gio/gunixsocketaddress.h>
+
+#include <errno.h>
+
+const gchar * cockpit_bridge_local_address = NULL;
+
+CockpitConnectable *
+cockpit_connectable_ref (CockpitConnectable *connectable)
+{
+ g_return_val_if_fail (connectable != NULL, NULL);
+
+ if (connectable->refs <= 0)
+ {
+ connectable = g_memdup (connectable, sizeof (CockpitConnectable));
+ g_object_ref (connectable->address);
+ if (connectable->tls_cert)
+ g_object_ref (connectable->tls_cert);
+ if (connectable->tls_database)
+ g_object_ref (connectable->tls_database);
+ connectable->name = g_strdup (connectable->name ? connectable->name : "connect");
+ connectable->refs = 1;
+ }
+ else
+ {
+ connectable->refs++;
+ }
+
+ return connectable;
+}
+
+void
+cockpit_connectable_unref (gpointer data)
+{
+ CockpitConnectable *connectable = data;
+
+ g_return_if_fail (connectable != NULL);
+ g_return_if_fail (connectable->refs > 0);
+
+ if (--connectable->refs == 0)
+ {
+ if (connectable->tls_cert)
+ g_object_unref (connectable->tls_cert);
+ if (connectable->tls_database)
+ g_object_unref (connectable->tls_database);
+ g_object_unref (connectable->address);
+ g_free (connectable->name);
+ g_free (connectable);
+ }
+}
+
+static void
+socket_client_event (GSocketClient *client,
+ GSocketClientEvent event,
+ GSocketConnectable *gconnectable,
+ GIOStream *connection,
+ gpointer user_data)
+{
+ CockpitConnectable *connectable = user_data;
+
+ switch (event)
+ {
+ case G_SOCKET_CLIENT_TLS_HANDSHAKING:
+ {
+ GTlsClientConnection *tls_client = G_TLS_CLIENT_CONNECTION (connection);
+
+ g_tls_client_connection_set_validation_flags (tls_client, connectable->tls_flags);
+
+ if (connectable->tls_cert)
+ g_tls_connection_set_certificate (G_TLS_CONNECTION (tls_client), connectable->tls_cert);
+
+ if (connectable->tls_database)
+ g_tls_connection_set_database (G_TLS_CONNECTION (tls_client), connectable->tls_database);
+
+ g_tls_connection_set_require_close_notify (G_TLS_CONNECTION (tls_client), FALSE);
+
+ return;
+ }
+
+ default:
+ return;
+ }
+}
+
+static void
+socket_client_connect_done (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+
+ g_autoptr(GSocketConnection) connection = g_socket_client_connect_finish (G_SOCKET_CLIENT (object), result, &error);
+ if (connection != NULL)
+ {
+ GIOStream *stream; /* weak */
+
+ if (G_IS_TCP_WRAPPER_CONNECTION (connection))
+ {
+ stream = g_tcp_wrapper_connection_get_base_io_stream (G_TCP_WRAPPER_CONNECTION (connection));
+ g_assert (G_IS_TLS_CONNECTION (stream));
+ }
+ else
+ stream = G_IO_STREAM (connection);
+
+ g_task_return_pointer (task, g_object_ref (stream), g_object_unref);
+ }
+ else
+ g_task_return_error (task, g_steal_pointer (&error));
+}
+
+void
+cockpit_connect_stream (GSocketConnectable *address,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ CockpitConnectable connectable = { .address = address };
+
+ g_return_if_fail (G_IS_SOCKET_CONNECTABLE (address));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ cockpit_connect_stream_full (&connectable, cancellable, callback, user_data);
+}
+
+void
+cockpit_connect_stream_full (CockpitConnectable *connectable,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (connectable != NULL);
+ g_return_if_fail (G_IS_SOCKET_CONNECTABLE (connectable->address));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ g_autoptr(GSocketClient) client = g_socket_client_new ();
+
+ /* otherwise, we'll go fishing around in GSettings... */
+ g_socket_client_set_enable_proxy (client, FALSE);
+
+ if (connectable->tls)
+ {
+ g_signal_connect_data (client, "event", G_CALLBACK (socket_client_event),
+ cockpit_connectable_ref (connectable),
+ (GClosureNotify) cockpit_connectable_unref, 0);
+
+ g_socket_client_set_tls (client, TRUE);
+ }
+
+ g_socket_client_connect_async (client, connectable->address, cancellable, socket_client_connect_done,
+ g_task_new (NULL, cancellable, callback, user_data));
+}
+
+GIOStream *
+cockpit_connect_stream_finish (GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, NULL), NULL);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static GSocketConnectable *
+parse_address (CockpitChannel *channel,
+ gchar **possible_name,
+ gboolean *local_address)
+{
+ GSocketConnectable *connectable = NULL;
+ const gchar *unix_path;
+ const gchar *address;
+ JsonObject *options;
+ gboolean local = FALSE;
+ GError *error = NULL;
+ const gchar *host;
+ gint64 port;
+ gchar *name = NULL;
+ gboolean open = FALSE;
+
+ options = cockpit_channel_get_options (channel);
+ if (!cockpit_json_get_string (options, "unix", NULL, &unix_path))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"unix\" option in channel");
+ goto out;
+ }
+ if (!cockpit_json_get_int (options, "port", G_MAXINT64, &port))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"port\" option in channel");
+ goto out;
+ }
+ if (!cockpit_json_get_string (options, "address", NULL, &address))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"address\" option in channel");
+ goto out;
+ }
+
+ if (port != G_MAXINT64 && unix_path)
+ {
+ cockpit_channel_fail (channel, "protocol-error", "cannot specify both \"port\" and \"unix\" options");
+ goto out;
+ }
+ else if (port != G_MAXINT64)
+ {
+ if (port <= 0 || port > 65535)
+ {
+ cockpit_channel_fail (channel, "protocol-error", "received invalid \"port\" option");
+ goto out;
+ }
+
+ if (address)
+ {
+ connectable = g_network_address_new (address, port);
+ host = address;
+
+ /* This isn't perfect, but matches the use case. Specify address => non-local */
+ local = FALSE;
+ }
+ else if (cockpit_bridge_local_address)
+ {
+ connectable = g_network_address_parse (cockpit_bridge_local_address, port, &error);
+ host = cockpit_bridge_local_address;
+ local = TRUE;
+ }
+ else
+ {
+ connectable = cockpit_loopback_new (port);
+ host = "localhost";
+ local = TRUE;
+ }
+
+ if (error != NULL)
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "couldn't parse local address: %s: %s", host, error->message);
+ goto out;
+ }
+ else
+ {
+ name = g_strdup_printf ("%s:%d", host, (gint)port);
+ }
+ }
+ else if (unix_path)
+ {
+ name = g_strdup (unix_path);
+ connectable = G_SOCKET_CONNECTABLE (g_unix_socket_address_new (unix_path));
+ local = FALSE;
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "no \"port\" or \"unix\" or other address option for channel");
+ goto out;
+ }
+
+ open = TRUE;
+
+out:
+ g_clear_error (&error);
+ if (open)
+ {
+ if (possible_name)
+ *possible_name = g_strdup (name);
+ if (local_address)
+ *local_address = local;
+ }
+ else
+ {
+ if (connectable)
+ g_object_unref (connectable);
+ connectable = NULL;
+ }
+
+ g_free (name);
+ return connectable;
+}
+
+GSocketAddress *
+cockpit_connect_parse_address (CockpitChannel *channel,
+ gchar **possible_name)
+{
+ GSocketAddress *address;
+ g_autoptr(GError) error = NULL;
+ g_autofree gchar *name = NULL;
+
+ g_autoptr(GSocketConnectable) connectable = parse_address (channel, &name, NULL);
+ if (!connectable)
+ return NULL;
+
+ /* This is sync, but realistically, it doesn't matter for current use cases */
+ g_autoptr(GSocketAddressEnumerator) enumerator = g_socket_connectable_enumerate (connectable);
+
+ address = g_socket_address_enumerator_next (enumerator, NULL, &error);
+
+ if (error != NULL)
+ {
+ cockpit_channel_fail (channel, "not-found", "couldn't find address: %s: %s", name, error->message);
+ return NULL;
+ }
+
+ if (possible_name)
+ *possible_name = g_steal_pointer (&name);
+
+ return address;
+}
+
+static gboolean
+parse_option_file_or_data (CockpitChannel *channel,
+ JsonObject *options,
+ const gchar *option,
+ const gchar **file,
+ const gchar **data)
+{
+ JsonObject *object;
+ JsonNode *node;
+
+ g_assert (file != NULL);
+ g_assert (data != NULL);
+
+ node = json_object_get_member (options, option);
+ if (!node)
+ {
+ *file = NULL;
+ *data = NULL;
+ return TRUE;
+ }
+
+ if (!JSON_NODE_HOLDS_OBJECT (node))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"%s\" tls option for channel", option);
+ return FALSE;
+ }
+
+ object = json_node_get_object (node);
+
+ if (!cockpit_json_get_string (object, "file", NULL, file))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"file\" %s option for channel", option);
+ }
+ else if (!cockpit_json_get_string (object, "data", NULL, data))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"data\" %s option for channel", option);
+ }
+ else if (!*file && !*data)
+ {
+ cockpit_channel_fail (channel, "not-supported", "missing or unsupported \"%s\" option for channel", option);
+ }
+ else if (*file && *data)
+ {
+ cockpit_channel_fail (channel, "protocol-error", "cannot specify both \"file\" and \"data\" in \"%s\" option for channel", option);
+ }
+ else
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+load_pem_contents (CockpitChannel *channel,
+ const gchar *filename,
+ const gchar *option,
+ GString *pem)
+{
+ GError *error = NULL;
+ gchar *contents = NULL;
+ gsize len;
+
+ if (!g_file_get_contents (filename, &contents, &len, &error))
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "couldn't load \"%s\" file: %s: %s", option, filename, error->message);
+ g_clear_error (&error);
+ return FALSE;
+ }
+ else
+ {
+ g_string_append_len (pem, contents, len);
+ g_string_append_c (pem, '\n');
+ g_free (contents);
+ return TRUE;
+ }
+}
+
+static gchar *
+expand_filename (const gchar *filename)
+{
+ if (!g_path_is_absolute (filename))
+ return g_build_filename (g_get_home_dir (), filename, NULL);
+ else
+ return g_strdup (filename);
+}
+
+static gboolean
+parse_cert_option_as_pem (CockpitChannel *channel,
+ JsonObject *options,
+ const gchar *option,
+ GString *pem)
+{
+ gboolean ret = TRUE;
+ const gchar *file;
+ const gchar *data;
+ gchar *path;
+
+ if (!parse_option_file_or_data (channel, options, option, &file, &data))
+ return FALSE;
+
+ if (file)
+ {
+ path = expand_filename (file);
+
+ /* For now we assume file contents are PEM */
+ ret = load_pem_contents (channel, path, option, pem);
+
+ g_free (path);
+ }
+ else if (data)
+ {
+ /* Format this as PEM of the given type */
+ g_string_append (pem, data);
+ g_string_append_c (pem, '\n');
+ }
+
+ return ret;
+}
+
+static gboolean
+parse_cert_option_as_database (CockpitChannel *channel,
+ JsonObject *options,
+ const gchar *option,
+ GTlsDatabase **database)
+{
+ gboolean temporary = FALSE;
+ GError *error = NULL;
+ gboolean ret = TRUE;
+ const gchar *file;
+ const gchar *data;
+ gchar *path;
+ gint fd;
+
+ if (!parse_option_file_or_data (channel, options, option, &file, &data))
+ return FALSE;
+
+ if (file)
+ {
+ path = expand_filename (file);
+ ret = TRUE;
+ }
+ else if (data)
+ {
+ temporary = TRUE;
+ path = g_build_filename (g_get_user_runtime_dir (), "cockpit-bridge-cert-authority.XXXXXX", NULL);
+ fd = g_mkstemp (path);
+ if (fd < 0)
+ {
+ ret = FALSE;
+ cockpit_channel_fail (channel, "internal-error",
+ "couldn't create temporary directory: %s: %s", path, g_strerror (errno));
+ }
+ else
+ {
+ close (fd);
+ if (!g_file_set_contents (path, data, -1, &error))
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "couldn't write temporary data to: %s: %s", path, error->message);
+ g_clear_error (&error);
+ ret = FALSE;
+ }
+ }
+ }
+ else
+ {
+ /* Not specified */
+ *database = NULL;
+ return TRUE;
+ }
+
+ if (ret)
+ {
+ *database = g_tls_file_database_new (path, &error);
+ if (error)
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "couldn't load certificate data: %s: %s", path, error->message);
+ g_clear_error (&error);
+ ret = FALSE;
+ }
+ }
+
+ /* Leave around when problem, for debugging */
+ if (temporary && ret == TRUE)
+ g_unlink (path);
+
+ g_free (path);
+
+ return ret;
+}
+
+static gboolean
+parse_stream_options (CockpitChannel *channel,
+ CockpitConnectable *connectable)
+{
+ gboolean ret = FALSE;
+ GTlsCertificate *cert = NULL;
+ GTlsDatabase *database = NULL;
+ gboolean use_tls = FALSE;
+ GError *error = NULL;
+ GString *pem = NULL;
+ JsonObject *options;
+ JsonNode *node;
+
+ /* No validation for local servers by default */
+ gboolean validate = !connectable->local;
+
+ options = cockpit_channel_get_options (channel);
+ node = json_object_get_member (options, "tls");
+ if (node && !JSON_NODE_HOLDS_OBJECT (node))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"tls\" option for channel");
+ goto out;
+ }
+ else if (node)
+ {
+ options = json_node_get_object (node);
+ use_tls = TRUE;
+
+ /*
+ * The only function in GLib to parse private keys takes
+ * them in PEM concatenated form. This is a limitation of GLib,
+ * rather than concatenated form being a decent standard for
+ * certificates and keys. So build a combined PEM as expected by
+ * GLib here.
+ */
+
+ pem = g_string_sized_new (8192);
+
+ if (!parse_cert_option_as_pem (channel, options, "certificate", pem))
+ goto out;
+
+ if (pem->len)
+ {
+ if (!parse_cert_option_as_pem (channel, options, "key", pem))
+ goto out;
+
+ cert = g_tls_certificate_new_from_pem (pem->str, pem->len, &error);
+ if (error != NULL)
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "invalid \"certificate\" or \"key\" content: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+ }
+
+ if (!parse_cert_option_as_database (channel, options, "authority", &database))
+ goto out;
+
+ if (!cockpit_json_get_bool (options, "validate", validate, &validate))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"validate\" option");
+ goto out;
+ }
+ }
+
+ ret = TRUE;
+
+out:
+ if (ret)
+ {
+ connectable->tls = use_tls;
+ connectable->tls_cert = cert;
+ cert = NULL;
+
+ if (database)
+ {
+ connectable->tls_database = database;
+ connectable->tls_flags = G_TLS_CERTIFICATE_VALIDATE_ALL;
+ if (!validate)
+ connectable->tls_flags &= ~(G_TLS_CERTIFICATE_INSECURE | G_TLS_CERTIFICATE_BAD_IDENTITY);
+ database = NULL;
+ }
+ else
+ {
+ if (validate)
+ connectable->tls_flags = G_TLS_CERTIFICATE_VALIDATE_ALL;
+ else
+ connectable->tls_flags = G_TLS_CERTIFICATE_GENERIC_ERROR;
+ }
+ }
+
+ if (pem)
+ g_string_free (pem, TRUE);
+ if (cert)
+ g_object_unref (cert);
+ if (database)
+ g_object_unref (database);
+
+ return ret;
+}
+
+CockpitConnectable *
+cockpit_connect_parse_stream (CockpitChannel *channel)
+{
+ CockpitConnectable *connectable;
+ GSocketConnectable *address;
+ gboolean local = FALSE;
+ gchar *name = NULL;
+
+ address = parse_address (channel, &name, &local);
+ if (!address)
+ {
+ g_free (name);
+ return NULL;
+ }
+
+ connectable = g_new0 (CockpitConnectable, 1);
+ connectable->address = address;
+ connectable->name = name;
+ connectable->refs = 1;
+ connectable->local = local;
+
+ if (!parse_stream_options (channel, connectable))
+ {
+ cockpit_connectable_unref (connectable);
+ connectable = NULL;
+ }
+
+ return connectable;
+}
+
diff --git a/src/bridge/cockpitconnect.h b/src/bridge/cockpitconnect.h
new file mode 100644
index 0000000..c15d153
--- /dev/null
+++ b/src/bridge/cockpitconnect.h
@@ -0,0 +1,75 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_CONNECT_H__
+#define __COCKPIT_CONNECT_H__
+
+#include <gio/gio.h>
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+typedef struct {
+ gint refs;
+ gchar *name;
+
+ /* Where to connect to */
+ GSocketConnectable *address;
+
+ /* TLS flags */
+ gboolean tls;
+ gboolean local;
+ GTlsCertificateFlags tls_flags;
+ GTlsCertificate *tls_cert;
+ GTlsDatabase *tls_database;
+} CockpitConnectable;
+
+CockpitConnectable * cockpit_connectable_ref (CockpitConnectable *connectable);
+
+void cockpit_connectable_unref (gpointer data);
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(CockpitConnectable, cockpit_connectable_unref)
+
+void cockpit_connect_stream (GSocketConnectable *address,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+
+void cockpit_connect_stream_full (CockpitConnectable *connectable,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+GIOStream * cockpit_connect_stream_finish (GAsyncResult *result,
+ GError **error);
+
+CockpitConnectable * cockpit_connect_parse_stream (CockpitChannel *self);
+
+GSocketAddress * cockpit_connect_parse_address (CockpitChannel *self,
+ gchar **possible_name);
+
+void cockpit_connect_add_internal_address (const gchar *name,
+ GSocketAddress *address);
+
+gboolean cockpit_connect_remove_internal_address (const gchar *name);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_STREAM_H__ */
diff --git a/src/bridge/cockpitcpusamples.c b/src/bridge/cockpitcpusamples.c
new file mode 100644
index 0000000..e6dab48
--- /dev/null
+++ b/src/bridge/cockpitcpusamples.c
@@ -0,0 +1,233 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitcpusamples.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define CPU_CORE_MAXLEN 8
+
+gint cockpit_cpu_user_hz = -1;
+
+static gint
+ensure_user_hz (void)
+{
+ if (cockpit_cpu_user_hz <= 0)
+ {
+ cockpit_cpu_user_hz = sysconf (_SC_CLK_TCK);
+ if (cockpit_cpu_user_hz == -1 || cockpit_cpu_user_hz == 0)
+ {
+ g_warning ("sysconf (_SC_CLK_TCK) returned %d - forcing user_hz to 100", cockpit_cpu_user_hz);
+ cockpit_cpu_user_hz = 100;
+ }
+ }
+ return cockpit_cpu_user_hz;
+}
+
+/* TODO: this should be optimized so we don't allocate memory and call open()/close() all the time */
+
+void
+cockpit_cpu_samples (CockpitSamples *samples)
+{
+ gchar *contents = NULL;
+ GError *error = NULL;
+ gchar **lines = NULL;
+ guint64 user_hz;
+ gsize len;
+ guint n;
+
+ if (!g_file_get_contents ("/proc/stat", &contents, &len, &error))
+ {
+ g_message ("error loading contents /proc/stat: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ /* see 'man proc' for the format of /proc/stat */
+
+ lines = g_strsplit (contents, "\n", -1);
+ for (n = 0; lines != NULL && lines[n] != NULL; n++)
+ {
+ const gchar *line = lines[n];
+ guint64 user;
+ guint64 nice;
+ guint64 system;
+ guint64 idle;
+ guint64 iowait;
+ gchar cpu_core[CPU_CORE_MAXLEN + 1];
+
+ if (!(g_str_has_prefix (line, "cpu")))
+ continue;
+
+ #define FMT64 "%" G_GUINT64_FORMAT " "
+ if (sscanf (line, "%" G_STRINGIFY (CPU_CORE_MAXLEN) "s " FMT64 FMT64 FMT64 FMT64 FMT64,
+ cpu_core,
+ &user,
+ &nice,
+ &system,
+ &idle,
+ &iowait) != 6)
+ {
+ g_warning ("Error parsing line %d of /proc/stat with content `%s'", n, line);
+ continue;
+ }
+
+ user_hz = ensure_user_hz ();
+ if (strlen (cpu_core) > 3)
+ {
+ cockpit_samples_sample (samples, "cpu.core.nice", cpu_core + 3, nice*1000/user_hz);
+ cockpit_samples_sample (samples, "cpu.core.user", cpu_core + 3, user*1000/user_hz);
+ cockpit_samples_sample (samples, "cpu.core.system", cpu_core + 3, system*1000/user_hz);
+ cockpit_samples_sample (samples, "cpu.core.iowait", cpu_core + 3, iowait*1000/user_hz);
+ }
+ else
+ {
+ cockpit_samples_sample (samples, "cpu.basic.nice", NULL, nice*1000/user_hz);
+ cockpit_samples_sample (samples, "cpu.basic.user", NULL, user*1000/user_hz);
+ cockpit_samples_sample (samples, "cpu.basic.system", NULL, system*1000/user_hz);
+ cockpit_samples_sample (samples, "cpu.basic.iowait", NULL, iowait*1000/user_hz);
+ }
+ }
+
+out:
+ g_strfreev (lines);
+ g_free (contents);
+}
+
+static gchar*
+read_file (const gchar *path)
+{
+ g_autoptr(GError) error = NULL;
+ gchar *content = NULL;
+
+ if (!g_file_get_contents (path, &content, NULL, &error))
+ {
+ // ENOENT is used to break loops, do not log it
+ if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ g_warning ("error reading file: %s", error->message);
+
+ return NULL;
+ }
+
+ return content;
+}
+
+static void
+sample_cpu_sensors (gchar *sensor_path,
+ CockpitSamples *samples)
+{
+ g_autofree gchar *temp_content = read_file (sensor_path);
+
+ if (temp_content == NULL)
+ return;
+
+ gint64 temperature = g_ascii_strtoll (temp_content, NULL, 10);
+ // overflow or invalid
+ if (temperature == G_MAXINT64 || temperature == G_MININT64 || temperature == 0)
+ {
+ g_debug("Invalid number in %s: %m", sensor_path);
+ return;
+ }
+
+ cockpit_samples_sample (samples, "cpu.temperature", sensor_path, temperature/1000);
+}
+
+static void
+detect_cpu_sensors (GList **devices,
+ const gchar *hwmon_name,
+ int hwmonID)
+{
+ g_autofree gchar *path = g_strdup_printf ("/sys/class/hwmon/hwmon%d", hwmonID);
+ g_autoptr(GDir) dir = g_dir_open (path, 0, NULL);
+ if (dir == NULL)
+ return;
+
+
+ const char *name;
+ while ((name = g_dir_read_name (dir)))
+ {
+ uint i;
+ if (!g_str_has_suffix (name, "_input") || (sscanf(name, "temp%d_input", &i) != 1))
+ continue;
+
+ g_autofree char *sensor_path = g_build_filename (path, name, NULL);
+ g_autofree gchar *label = g_strdup_printf ("/sys/class/hwmon/hwmon%d/temp%d_label", hwmonID, i);
+ g_autofree gchar *label_content = read_file (label);
+
+ if (label_content == NULL)
+ {
+ // labels aren't used on ARM
+ if (!g_str_equal (hwmon_name, "cpu_thermal"))
+ continue;
+ }
+ else
+ {
+ g_strchomp (label_content);
+
+ // only sample CPU Temperature in atk0110
+ if (!g_str_equal (label_content, "CPU Temperature") && g_str_equal (hwmon_name, "atk0110"))
+ continue;
+ }
+
+ *devices = g_list_append (*devices, g_steal_pointer (&sensor_path));
+ }
+}
+
+static void
+detect_hwmon_device (GList **devices)
+{
+ // iterate through all hwmon folders to find CPU sensors
+ for (int i = 0; TRUE; i++)
+ {
+ g_autofree gchar *path = g_strdup_printf ("/sys/class/hwmon/hwmon%d/name", i);
+ g_autofree gchar *name = read_file (path);
+
+ // end loop on error
+ if (name == NULL)
+ break;
+
+ g_strchomp (name);
+
+ // compare device name with CPU names
+ // Intel: coretemp, AMD: k8temp or k10temp, ARM: cpu_thermal, Asus motherboard: atk0110
+ if (g_str_equal (name, "coretemp") || g_str_equal (name, "cpu_thermal") ||
+ g_str_equal (name, "k8temp") || g_str_equal (name, "k10temp") || g_str_equal (name, "atk0110"))
+ {
+ // hwmon contains CPU info
+ detect_cpu_sensors (devices, name, i);
+ }
+ }
+}
+
+void
+cockpit_cpu_temperature (CockpitSamples *samples)
+{
+ static GList *devices = NULL;
+ static gboolean initialized = FALSE;
+ if (!initialized)
+ {
+ detect_hwmon_device (&devices);
+ initialized = TRUE;
+ }
+
+ g_list_foreach (devices, (GFunc)sample_cpu_sensors, samples);
+}
diff --git a/src/bridge/cockpitcpusamples.h b/src/bridge/cockpitcpusamples.h
new file mode 100644
index 0000000..f8b650d
--- /dev/null
+++ b/src/bridge/cockpitcpusamples.h
@@ -0,0 +1,33 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_CPU_SAMPLES_H__
+#define COCKPIT_CPU_SAMPLES_H__
+
+#include "cockpitsamples.h"
+
+G_BEGIN_DECLS
+
+void cockpit_cpu_samples (CockpitSamples *samples);
+
+void cockpit_cpu_temperature (CockpitSamples *samples);
+
+G_END_DECLS
+
+#endif /* COCKPIT_CPU_SAMPLES_H__ */
diff --git a/src/bridge/cockpitdbuscache.c b/src/bridge/cockpitdbuscache.c
new file mode 100644
index 0000000..6785655
--- /dev/null
+++ b/src/bridge/cockpitdbuscache.c
@@ -0,0 +1,2131 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbuscache.h"
+
+#include "cockpitdbusrules.h"
+#include "cockpitpaths.h"
+
+#include <string.h>
+
+#define DEBUG_BATCHES 0
+
+/*
+ * This is a cache of properties which tracks updates. The best way to do
+ * this is via ObjectManager. But it also does introspection and uses that
+ * to get both interface info, and information about which paths are present
+ * and the interfaces they implement.
+ *
+ * One big complication is that all of this needs to have ordering guarantees,
+ * including introspection. We keep track of which batch of properties we're
+ * working on, and associate barrier callbacks which can only happen once
+ * a given batch of properties has completed processing.
+ *
+ * Also information about an interface will be available before we notify
+ * about properties on an interface. This is a further ordering guarantee.
+ *
+ * Since there are lots of strings, to help with allocation churn, we have our
+ * own string intern table, where path, interface and property names are
+ * stored while the cache is active. Each time we get a path etc. from an
+ * external source source (such as GVariant) and we know we'll need it later
+ * then we intern it, so it sticks around.
+ */
+
+struct _CockpitDBusCache {
+ GObject parent;
+
+ /* Cancelled on dispose */
+ GCancellable *cancellable;
+
+ /* The connection to talk on */
+ GDBusConnection *connection;
+
+ /* The readable DBus name, and actual unique owner */
+ gchar *logname;
+ gchar *name;
+ gchar *name_owner;
+
+ /* Introspection stuff */
+ GHashTable *introspected;
+ GQueue *introspects;
+ GHashTable *introsent;
+ GList *trash;
+
+ /* The main data cache: paths > interfaces -> properties -> values */
+ GHashTable *cache;
+
+ /* The paths and interfaces we should watch */
+ CockpitDBusRules *rules;
+
+ /* Accumulated information about these various paths */
+ GTree *managed;
+ GTree *managed_not_ready;
+
+ /* Signal Subscriptions */
+ gboolean subscribed;
+ guint subscribe_properties;
+ guint subscribe_manager;
+
+ /* Barrier related stuff */
+ GQueue *batches;
+ GQueue *barriers;
+ guint number;
+ gboolean barrier_progressing;
+ guint barrier_progressing_rounds;
+ GHashTable *update;
+
+ /* Interned strings */
+ GHashTable *interned;
+};
+
+enum {
+ PROP_CONNECTION = 1,
+ PROP_NAME,
+ PROP_LOGNAME,
+ PROP_INTERFACE_INFO
+};
+
+static guint signal_meta;
+static guint signal_update;
+
+G_DEFINE_TYPE (CockpitDBusCache, cockpit_dbus_cache, G_TYPE_OBJECT);
+
+static void
+hash_table_unref_or_null (gpointer data)
+{
+ if (data)
+ g_hash_table_unref (data);
+}
+
+static const gchar *
+intern_string (CockpitDBusCache *self,
+ const gchar *string)
+{
+ const gchar * interned;
+ gchar *copy;
+
+ interned = g_hash_table_lookup (self->interned, string);
+ if (!interned)
+ {
+ copy = g_strdup (string);
+ g_hash_table_add (self->interned, copy);
+ interned = copy;
+ }
+
+ return interned;
+}
+
+typedef struct {
+ guint number;
+ CockpitDBusBarrierFunc callback;
+ gpointer user_data;
+} BarrierData;
+
+typedef struct {
+ gint refs;
+ guint number;
+ gboolean orphan;
+#if DEBUG_BATCHES
+ GSList *debug;
+#endif
+} BatchData;
+
+static void
+barrier_progress (CockpitDBusCache *self)
+{
+ BarrierData *barrier;
+ BatchData *batch;
+
+ if (self->barrier_progressing)
+ {
+ /* It might happen that barrier_progress is called
+ * recursively, while one of the barrier callbacks is
+ * running. We need to detect this and avoid calling the
+ * next barrier callback while there is already one
+ * running. Allowing it might violate the ordering
+ * guarantees if the running callback is doing more work
+ * after causing the recursive call to barrier_progress.
+ *
+ * Concretely, process_interfaces below can cause
+ * recursive calls in the middle of its work when it
+ * processes multiple interfaces.
+ */
+
+ self->barrier_progressing_rounds += 1;
+ return;
+ }
+
+ self->barrier_progressing = TRUE;
+ self->barrier_progressing_rounds = 1;
+
+ while (self->barrier_progressing_rounds > 0)
+ {
+ self->barrier_progressing_rounds -= 1;
+
+ batch = g_queue_peek_head (self->batches);
+
+ for (;;)
+ {
+ barrier = g_queue_peek_head (self->barriers);
+ if (!barrier)
+ break;
+
+ /*
+ * If there is a batch being processed, we must block
+ * barriers that have an equal or later batch number.
+ */
+ if (batch && batch->number <= barrier->number)
+ break;
+
+ g_queue_pop_head (self->barriers);
+ (barrier->callback) (self, barrier->user_data);
+ g_slice_free (BarrierData, barrier);
+ }
+ }
+
+ self->barrier_progressing = FALSE;
+}
+
+static void
+barrier_flush (CockpitDBusCache *self)
+{
+ BarrierData *barrier;
+
+ for (;;)
+ {
+ barrier = g_queue_pop_head (self->barriers);
+ if (!barrier)
+ return;
+ (barrier->callback) (self, barrier->user_data);
+ g_slice_free (BarrierData, barrier);
+ }
+}
+
+#if DEBUG_BATCHES
+static void
+batch_dump (BatchData *batch)
+{
+ GSList *l;
+ g_printerr ("BATCH %u (refs %d)\n", batch->number, batch->refs);
+ batch->debug = g_slist_reverse (batch->debug);
+ for (l = batch->debug; l != NULL; l = g_slist_next (l))
+ g_printerr (" * %s\n", (gchar *)l->data);
+ batch->debug = g_slist_reverse (batch->debug);
+}
+#endif /* DEBUG_BATCHES */
+
+static void
+batch_free (BatchData *batch)
+{
+#if DEBUG_BATCHES
+ g_slist_foreach (batch->debug, (GFunc)g_free, NULL);
+#endif
+ g_slice_free (BatchData, batch);
+}
+
+static void
+batch_progress (CockpitDBusCache *self)
+{
+ BatchData *batch;
+ GHashTable *update;
+
+ for (;;)
+ {
+ batch = g_queue_peek_head (self->batches);
+
+ /*
+ * Once a batch has completed it's refs field will be zero.
+ * This means we can send notify of any property changes,
+ * process any barriers waiting on this batch, and move on
+ * to the next batch.
+ */
+ if (!batch || batch->refs > 0)
+ return;
+
+ g_queue_pop_head (self->batches);
+ update = self->update;
+ self->update = NULL;
+
+ if (update)
+ {
+ g_signal_emit (self, signal_update, 0, update);
+ g_hash_table_unref (update);
+ }
+
+ batch_free (batch);
+ barrier_progress (self);
+ }
+}
+
+static void
+batch_flush (CockpitDBusCache *self)
+{
+ BatchData *batch;
+
+ for (;;)
+ {
+ batch = g_queue_pop_head (self->batches);
+ if (!batch)
+ return;
+ if (batch->refs == 0)
+ batch_free (batch);
+ else
+ batch->orphan = TRUE;
+ }
+}
+
+static BatchData *
+batch_create (CockpitDBusCache *self)
+{
+ BatchData *batch = g_slice_new0 (BatchData);
+ batch->refs = 1;
+ self->number++;
+ batch->number = self->number;
+ g_queue_push_tail (self->batches, batch);
+ return batch;
+}
+
+static BatchData *
+_batch_ref (BatchData *batch,
+ const gchar *function,
+ gint line)
+{
+ g_assert (batch != NULL);
+ batch->refs++;
+#if DEBUG_BATCHES
+ batch->debug = g_slist_prepend (batch->debug, g_strdup_printf (" * ref -> %d %s:%d",
+ batch->refs, function, line));
+#endif
+ return batch;
+}
+
+#define batch_ref(batch) \
+ (_batch_ref((batch), G_STRFUNC, __LINE__))
+
+static void
+_batch_unref (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *function,
+ gint line)
+{
+ g_assert (batch != NULL);
+#if DEBUG_BATCHES
+ if (!(batch->refs > 0))
+ batch_dump (batch);
+#endif
+ g_assert (batch->refs > 0);
+ batch->refs--;
+#if DEBUG_BATCHES
+ batch->debug = g_slist_prepend (batch->debug, g_strdup_printf (" * unref -> %d %s:%d",
+ batch->refs, function, line));
+#endif
+
+ if (batch->refs == 0 && batch->orphan)
+ batch_free (batch);
+ else
+ batch_progress (self);
+}
+
+#define batch_unref(self, batch) \
+ (_batch_unref((self), (batch), G_STRFUNC, __LINE__))
+
+static void
+cockpit_dbus_cache_init (CockpitDBusCache *self)
+{
+ self->number = 1;
+
+ self->cancellable = g_cancellable_new ();
+
+ self->managed = cockpit_paths_new ();
+ self->managed_not_ready = cockpit_paths_new ();
+
+ /* Becomes a whole tree of hash tables */
+ self->cache = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
+ (GDestroyNotify)g_hash_table_unref);
+
+ /* All of these are sets. ie: key and value identical */
+ self->rules = cockpit_dbus_rules_new ();
+
+ self->introspects = g_queue_new ();
+ self->introsent = g_hash_table_new (g_str_hash, g_str_equal);
+
+ self->batches = g_queue_new ();
+ self->barriers = g_queue_new ();
+
+ /* Put allocations we need to keep around, but can't handily track */
+ self->trash = NULL;
+ self->interned = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+}
+
+typedef struct {
+ const gchar *interface;
+ const gchar *path;
+ CockpitDBusIntrospectFunc callback;
+ gpointer user_data;
+ BatchData *batch;
+ gboolean introspecting;
+} IntrospectData;
+
+static void
+introspect_next (CockpitDBusCache *self);
+
+static void
+scrape_variant (CockpitDBusCache *self,
+ BatchData *batch,
+ GVariant *data);
+
+static void
+introspect_complete (CockpitDBusCache *self,
+ IntrospectData *id)
+{
+ GDBusInterfaceInfo *iface = NULL;
+
+ if (id->interface)
+ {
+ iface = cockpit_dbus_interface_info_lookup (self->introspected, id->interface);
+ if (!iface)
+ {
+ g_debug ("%s: introspect interface %s didn't work", self->logname, id->interface);
+
+ /*
+ * So we were expecting an interface that wasn't found at the expected
+ * object. This means that something is wrong with the introspection
+ * on the DBus service. We create a pretend empty interface, so that
+ * the ordering guarantees are met.
+ */
+
+ iface = g_new0 (GDBusInterfaceInfo, 1);
+ iface->ref_count = 1;
+ iface->name = g_strdup (id->interface);
+
+ cockpit_dbus_interface_info_push (self->introspected, iface);
+ g_dbus_interface_info_unref (iface);
+ }
+ }
+
+ if (id->callback)
+ (id->callback) (self, iface, id->user_data);
+
+ /* Mark as having called, as a double check */
+ id->callback = NULL;
+
+ batch_unref (self, id->batch);
+ g_assert (id->callback == NULL);
+ g_slice_free (IntrospectData, id);
+}
+
+static void
+process_introspect_node (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *path,
+ GDBusNodeInfo *node,
+ gboolean recursive);
+
+static gboolean
+dbus_error_matches_unknown (GError *error)
+{
+ gboolean ret = FALSE;
+
+ if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD) ||
+ g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED))
+ return TRUE;
+
+#if GLIB_CHECK_VERSION(2,42,0)
+ ret = g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_INTERFACE) ||
+ g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_OBJECT) ||
+ g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY);
+#else
+
+ gchar *remote = g_dbus_error_get_remote_error (error);
+ if (remote)
+ {
+ /*
+ * DBus used to only have the UnknownMethod error. It didn't have
+ * specific errors for UnknownObject and UnknownInterface. So we're
+ * pretty liberal on what we treat as an expected error here.
+ *
+ * HACK: GDBus also doesn't understand the newer error codes :S
+ *
+ * https://bugzilla.gnome.org/show_bug.cgi?id=727900
+ */
+ ret = (g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownMethod") ||
+ g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownObject") ||
+ g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownInterface") ||
+ g_str_equal (remote, "org.freedesktop.DBus.Error.UnknownProperty"));
+ g_free (remote);
+ }
+#endif
+
+ return ret;
+}
+
+static void
+on_introspect_reply (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CockpitDBusCache *self = user_data;
+ IntrospectData *id;
+ GDBusNodeInfo *node;
+ GError *error = NULL;
+ GVariant *retval;
+ const gchar *xml;
+
+ /* All done with this introspect */
+ id = g_queue_pop_head (self->introspects);
+
+ /* Introspects have been flushed */
+ if (!id)
+ {
+ g_object_unref (self);
+ return;
+ }
+
+ g_assert (id->introspecting);
+
+ retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
+
+ if (retval)
+ {
+ g_debug ("%s: reply from Introspect() at %s", self->logname, id->path);
+
+ g_variant_get (retval, "(&s)", &xml);
+
+ node = g_dbus_node_info_new_for_xml (xml, &error);
+ if (node)
+ {
+ process_introspect_node (self, id->batch, id->path, node, id->interface == NULL);
+ g_dbus_node_info_unref (node);
+ }
+ g_variant_unref (retval);
+ }
+
+ if (error)
+ {
+ if (!dbus_error_matches_unknown (error))
+ g_message ("%s: couldn't introspect %s: %s", self->logname, id->path, error->message);
+ g_error_free (error);
+ }
+
+ introspect_complete (self, id);
+ introspect_next (self);
+
+ g_object_unref (self);
+}
+
+static void
+introspect_next (CockpitDBusCache *self)
+{
+ IntrospectData *id;
+
+ id = g_queue_peek_head (self->introspects);
+ if (id && !id->introspecting)
+ {
+ if (g_cancellable_is_cancelled (self->cancellable))
+ {
+ g_queue_pop_head (self->introspects);
+ introspect_complete (self, id);
+ }
+ else
+ {
+ GDBusInterfaceInfo *iface = NULL;
+ if (id->interface)
+ iface = cockpit_dbus_interface_info_lookup (self->introspected, id->interface);
+ if (iface)
+ {
+ g_debug ("%s: not calling Introspect() on %s, already done",
+ self->logname, id->path);
+ g_queue_pop_head (self->introspects);
+ introspect_complete (self, id);
+ introspect_next (self);
+ }
+ else
+ {
+ g_debug ("%s: calling Introspect() on %s", self->logname, id->path);
+
+ id->introspecting = TRUE;
+ g_dbus_connection_call (self->connection, self->name, id->path,
+ "org.freedesktop.DBus.Introspectable", "Introspect",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(s)"),
+ G_DBUS_CALL_FLAGS_NONE, -1,
+ self->cancellable, on_introspect_reply,
+ g_object_ref (self));
+ }
+ }
+ }
+}
+
+static void
+introspect_flush (CockpitDBusCache *self)
+{
+ gboolean note = FALSE;
+ IntrospectData *id;
+ GQueue *queue;
+
+ queue = g_queue_new ();
+ for (;;)
+ {
+ /* Steal everything, more could be added by callback */
+ for (;;)
+ {
+ id = g_queue_pop_tail (self->introspects);
+ if (!id)
+ break;
+ g_queue_push_head (queue, id);
+ }
+
+ id = g_queue_pop_head (queue);
+ if (!id)
+ {
+ g_queue_free (queue);
+ return;
+ }
+
+ if (!note)
+ g_debug ("%s: flushing introspect queue", self->logname);
+ note = TRUE;
+ introspect_complete (self, id);
+ }
+}
+
+static void
+introspect_queue (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *path,
+ const gchar *interface,
+ CockpitDBusIntrospectFunc callback,
+ gpointer user_data)
+{
+ IntrospectData *id;
+
+ g_assert (path != NULL);
+ g_assert (batch != NULL);
+
+ id = g_slice_new0 (IntrospectData);
+ id->interface = interface;
+ id->batch = batch_ref (batch);
+ id->path = path;
+ id->callback = callback;
+ id->user_data = user_data;
+
+ g_debug ("%s: queueing introspect %s %s%s", self->logname, path,
+ interface ? "for " : "", interface ? interface : "");
+ g_queue_push_tail (self->introspects, id);
+
+ introspect_next (self);
+}
+
+static void
+introspect_maybe (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *path,
+ const gchar *interface,
+ CockpitDBusIntrospectFunc callback,
+ gpointer user_data)
+{
+ GDBusInterfaceInfo *iface;
+
+ g_assert (path);
+ g_assert (interface);
+
+ iface = cockpit_dbus_interface_info_lookup (self->introspected, interface);
+ if (iface)
+ {
+ (callback) (self, iface, user_data);
+ }
+ else
+ {
+ if (batch == NULL)
+ batch = batch_create (self);
+ else
+ batch = batch_ref (batch);
+
+ introspect_queue (self, batch,
+ intern_string (self, path),
+ intern_string (self, interface),
+ callback, user_data);
+
+ batch_unref (self, batch);
+ }
+}
+
+void
+cockpit_dbus_cache_introspect (CockpitDBusCache *self,
+ const gchar *path,
+ const gchar *interface,
+ CockpitDBusIntrospectFunc callback,
+ gpointer user_data)
+{
+ g_return_if_fail (self != NULL);
+ g_return_if_fail (path != NULL);
+
+ introspect_maybe (self, NULL, path, interface, callback, user_data);
+}
+
+static GHashTable *
+emit_interfaces (CockpitDBusCache *self,
+ const gchar *path)
+{
+ GHashTable *interfaces;
+
+ g_assert (path != NULL);
+
+ if (!self->update)
+ {
+ self->update = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, hash_table_unref_or_null);
+ }
+
+ interfaces = g_hash_table_lookup (self->update, path);
+ if (!interfaces)
+ {
+ interfaces = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, hash_table_unref_or_null);
+ g_hash_table_replace (self->update, (gchar *)path, interfaces);
+ }
+
+ return interfaces;
+}
+
+static void
+emit_remove (CockpitDBusCache *self,
+ const gchar *path,
+ const gchar *interface)
+{
+ GHashTable *interfaces = emit_interfaces (self, path);
+
+ g_assert (interface != NULL);
+ g_hash_table_replace (interfaces, (gchar *)interface, NULL);
+}
+
+static void
+emit_change (CockpitDBusCache *self,
+ const gchar *path,
+ GDBusInterfaceInfo *iface,
+ const gchar *property,
+ GVariant *value)
+{
+ GHashTable *interfaces = emit_interfaces (self, path);
+ GHashTable *properties;
+
+ g_assert (iface != NULL);
+
+ properties = g_hash_table_lookup (interfaces, iface->name);
+ if (!properties)
+ {
+ properties = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, (GDestroyNotify)g_variant_unref);
+ g_hash_table_replace (interfaces, iface->name, properties);
+ }
+
+ if (property)
+ {
+ g_assert (value != NULL);
+ g_hash_table_replace (properties, (gchar *)property, g_variant_ref (value));
+ }
+}
+
+static gboolean
+find_change (CockpitDBusCache *self,
+ const gchar *path,
+ GDBusInterfaceInfo *iface)
+{
+ GHashTable *interfaces;
+ GHashTable *properties;
+
+ if (self->update == NULL)
+ return FALSE;
+
+ interfaces = g_hash_table_lookup (self->update, path);
+ if (interfaces == NULL)
+ return FALSE;
+
+ properties = g_hash_table_lookup (interfaces, iface->name);
+ if (properties == NULL)
+ return FALSE;
+
+ return TRUE;
+}
+
+static GHashTable *
+ensure_interfaces (CockpitDBusCache *self,
+ const gchar *path)
+{
+ GHashTable *interfaces;
+
+ interfaces = g_hash_table_lookup (self->cache, path);
+ if (!interfaces)
+ {
+ interfaces = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, (GDestroyNotify)g_hash_table_unref);
+ g_hash_table_replace (self->cache, (gchar *)path, interfaces);
+ }
+
+ return interfaces;
+}
+
+static GHashTable *
+ensure_properties (CockpitDBusCache *self,
+ const gchar *path,
+ GDBusInterfaceInfo *iface)
+{
+ GHashTable *interfaces;
+ GHashTable *properties;
+ const gchar *name;
+
+ interfaces = ensure_interfaces (self, path);
+ properties = g_hash_table_lookup (interfaces, iface->name);
+ if (!properties)
+ {
+ properties = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, (GDestroyNotify)g_variant_unref);
+ g_hash_table_replace (interfaces, iface->name, properties);
+
+ g_debug ("%s: present %s at %s", self->logname, iface->name, path);
+ emit_change (self, path, iface, NULL, NULL);
+ }
+
+ name = intern_string (self, iface->name);
+ if (!g_hash_table_lookup (self->introsent, name))
+ {
+ g_hash_table_add (self->introsent, (gpointer)name);
+ g_signal_emit (self, signal_meta, 0, iface);
+ }
+
+ return properties;
+}
+
+static void
+process_value (CockpitDBusCache *self,
+ GHashTable *properties,
+ const gchar *path,
+ GDBusInterfaceInfo *iface,
+ const gchar *property,
+ GVariant *variant)
+{
+ gpointer prev;
+ GVariant *value;
+ gpointer key;
+
+ value = g_variant_get_variant (variant);
+
+ if (g_hash_table_lookup_extended (properties, property, &key, &prev))
+ {
+ if (g_variant_equal (prev, value))
+ {
+ g_variant_unref (value);
+ return;
+ }
+
+ g_hash_table_steal (properties, key);
+ g_hash_table_replace (properties, key, value);
+ g_variant_unref (prev);
+ }
+ else
+ {
+ g_hash_table_replace (properties, (gchar *)property, value);
+ }
+
+ g_debug ("%s: changed %s %s at %s", self->logname, iface->name, property, path);
+ emit_change (self, path, iface, property, value);
+}
+
+typedef struct {
+ CockpitDBusCache *self;
+ const gchar *path;
+ const gchar *property;
+ BatchData *batch;
+ GDBusInterfaceInfo *iface;
+} GetData;
+
+static void
+process_get (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *path,
+ GDBusInterfaceInfo *iface,
+ const gchar *property,
+ GVariant *retval)
+{
+ GHashTable *properties;
+ GVariant *variant;
+
+ g_variant_get (retval, "(@v)", &variant);
+
+ properties = ensure_properties (self, path, iface);
+ process_value (self, properties, path, iface, property, variant);
+ cockpit_dbus_cache_scrape (self, variant);
+ g_variant_unref (variant);
+}
+
+static void
+on_get_reply (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GetData *gd = user_data;
+ CockpitDBusCache *self = gd->self;
+ GVariant *retval;
+ GError *error = NULL;
+
+ retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
+ if (error)
+ {
+ if (!g_cancellable_is_cancelled (self->cancellable))
+ {
+ if (dbus_error_matches_unknown (error))
+ {
+ g_debug ("%s: couldn't get property %s %s at %s: %s", self->logname,
+ gd->iface->name, gd->property, gd->path, error->message);
+ }
+ else
+ {
+ g_message ("%s: couldn't get property %s %s at %s: %s", self->logname,
+ gd->iface->name, gd->property, gd->path, error->message);
+ }
+ }
+ g_error_free (error);
+ }
+
+ if (retval)
+ {
+ g_debug ("%s: reply from Get() on %s", self->logname, gd->path);
+
+ process_get (self, gd->batch, gd->path, gd->iface, gd->property, retval);
+ g_variant_unref (retval);
+ }
+
+ batch_unref (self, gd->batch);
+
+ g_object_unref (gd->self);
+ g_slice_free (GetData, gd);
+}
+
+static void
+process_properties (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *path,
+ GDBusInterfaceInfo *iface,
+ GVariant *dict)
+{
+ GHashTable *properties;
+ const gchar *property;
+ GVariant *variant;
+ GVariantIter iter;
+
+ properties = ensure_properties (self, path, iface);
+
+ g_variant_iter_init (&iter, dict);
+ while (g_variant_iter_loop (&iter, "{s@v}", &property, &variant))
+ {
+ process_value (self, properties, path, iface,
+ intern_string (self, property), variant);
+ }
+}
+
+typedef struct {
+ const gchar *path;
+ GVariant *body;
+ BatchData *batch;
+} PropertiesChangedData;
+
+static void
+process_properties_changed (CockpitDBusCache *self,
+ GDBusInterfaceInfo *iface,
+ gpointer user_data)
+{
+ PropertiesChangedData *pcd = user_data;
+ GetData *gd;
+ GVariantIter iter;
+ const gchar *property;
+ GVariant *changed;
+ GVariant *invalidated;
+
+ g_variant_get (pcd->body, "(@s@a{sv}@as)", NULL, &changed, &invalidated);
+
+ process_properties (self, pcd->batch, pcd->path, iface, changed);
+
+ /*
+ * These are properties which the service didn't want to broadcast because
+ * they're either calculated per-peer or expensive to calculate if nobody
+ * is listening to them. We want them ... so get them and include them
+ * in the current batch.
+ */
+
+ g_variant_iter_init (&iter, invalidated);
+ while (g_variant_iter_loop (&iter, "&s", &property))
+ {
+ g_debug ("%s: calling Get() for %s %s at %s", self->logname, iface->name, property, pcd->path);
+
+ gd = g_slice_new0 (GetData);
+ gd->self = g_object_ref (self);
+ gd->property = intern_string (self, property);
+ gd->batch = batch_ref (pcd->batch);
+ gd->path = pcd->path;
+ gd->iface = iface;
+
+ g_dbus_connection_call (self->connection, self->name, gd->path,
+ "org.freedesktop.DBus.Properties", "Get",
+ g_variant_new ("(ss)", iface->name, property),
+ G_VARIANT_TYPE ("(v)"),
+ G_DBUS_CALL_FLAGS_NONE, -1,
+ self->cancellable, on_get_reply, gd);
+ }
+
+ g_variant_unref (invalidated);
+ g_variant_unref (changed);
+
+ batch_unref (self, pcd->batch);
+
+ g_variant_unref (pcd->body);
+ g_slice_free (PropertiesChangedData, pcd);
+}
+
+static void
+process_properties_barrier (CockpitDBusCache *self,
+ gpointer user_data)
+{
+ PropertiesChangedData *pcd = user_data;
+ const gchar *interface;
+ GVariant *changed;
+ BatchData *batch;
+
+ g_variant_get (pcd->body, "(&s@a{sv}@as)", &interface, &changed, NULL);
+
+ batch = batch_create (self);
+
+ pcd->batch = batch_ref (batch);
+ introspect_maybe (self, pcd->batch, pcd->path, interface, process_properties_changed, pcd);
+
+ scrape_variant (self, batch, changed);
+ batch_unref (self, batch);
+
+ g_variant_unref (changed);
+}
+
+static void
+on_properties_signal (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *path,
+ const gchar *properties_iface,
+ const gchar *member,
+ GVariant *body,
+ gpointer user_data)
+{
+ CockpitDBusCache *self = user_data;
+ PropertiesChangedData *pcd;
+ const gchar *interface;
+
+ if (!g_variant_is_of_type (body, G_VARIANT_TYPE ("(sa{sv}as)")))
+ {
+ g_debug ("%s: received PropertiesChanged with bad type", self->logname);
+ return;
+ }
+
+ g_debug ("%s: signal PropertiesChanged at %s", self->logname, path);
+ g_variant_get (body, "(&s@a{sv}@as)", &interface, NULL, NULL);
+
+ /* We have to filter the sender ourselves; glib doesn't do that for
+ well-known names.
+ */
+ if (self->name_owner && g_strcmp0 (sender, self->name_owner) != 0)
+ return;
+
+ if (!cockpit_dbus_rules_match (self->rules, path, interface, NULL, NULL))
+ return;
+
+ pcd = g_slice_new0 (PropertiesChangedData);
+ pcd->body = g_variant_ref (body);
+ pcd->path = intern_string (self, path);
+ cockpit_dbus_cache_barrier (self, process_properties_barrier, pcd);
+}
+
+typedef struct {
+ const gchar *path;
+ GVariant *dict;
+ BatchData *batch;
+} ProcessInterfaceData;
+
+static void
+process_interface (CockpitDBusCache *self,
+ GDBusInterfaceInfo *iface,
+ gpointer user_data)
+{
+ ProcessInterfaceData *pid = user_data;
+
+ process_properties (self, pid->batch, pid->path, iface, pid->dict);
+
+ batch_unref (self, pid->batch);
+
+ g_variant_unref (pid->dict);
+ g_slice_free (ProcessInterfaceData, pid);
+}
+
+static void
+process_interfaces (CockpitDBusCache *self,
+ BatchData *batch,
+ GHashTable *snapshot,
+ const gchar *path,
+ GVariant *dict)
+{
+ ProcessInterfaceData *pid;
+ GVariant *inner;
+ const gchar *interface;
+ GVariantIter iter;
+
+ if (batch)
+ batch = batch_ref (batch);
+
+ g_variant_iter_init (&iter, dict);
+ while (g_variant_iter_loop (&iter, "{s@a{sv}}", &interface, &inner))
+ {
+ if (!cockpit_dbus_rules_match (self->rules, path, interface, NULL, NULL))
+ continue;
+
+ if (!batch)
+ batch = batch_create (self);
+
+ if (snapshot)
+ g_hash_table_remove (snapshot, interface);
+
+ pid = g_slice_new0 (ProcessInterfaceData);
+ pid->batch = batch_ref (batch);
+ pid->path = path;
+ pid->dict = g_variant_ref (inner);
+ cockpit_dbus_cache_introspect (self, path, interface, process_interface, pid);
+
+ scrape_variant (self, batch, inner);
+ }
+
+ if (batch)
+ batch_unref (self, batch);
+}
+
+typedef struct {
+ GVariant *body;
+ const gchar *manager_added;
+} ProcessInterfacesData;
+
+static void
+retrieve_managed_objects (CockpitDBusCache *self,
+ const gchar *namespace_path,
+ BatchData *batch);
+
+static void
+process_interfaces_added (CockpitDBusCache *self,
+ gpointer user_data)
+{
+ ProcessInterfacesData *pis = user_data;
+ BatchData *batch = NULL;
+ GVariant *interfaces;
+ const gchar *path;
+
+ /*
+ * We added a manager while processing this message, perform a full manager
+ * load as part of the same batch.
+ */
+ if (pis->manager_added)
+ {
+ batch = batch_create (self);
+ retrieve_managed_objects (self, pis->manager_added, batch);
+ }
+
+ g_variant_get (pis->body, "(&o@a{sa{sv}})", &path, &interfaces);
+ process_interfaces (self, batch, NULL, intern_string (self, path), interfaces);
+ g_variant_unref (interfaces);
+
+ if (batch)
+ batch_unref (self, batch);
+
+ g_variant_unref (pis->body);
+ g_slice_free (ProcessInterfacesData, pis);
+}
+
+static void
+process_removed (CockpitDBusCache *self,
+ const gchar *path,
+ const gchar *interface)
+{
+ GHashTable *interfaces;
+ GHashTable *properties;
+
+ interfaces = g_hash_table_lookup (self->cache, path);
+ if (!interfaces)
+ return;
+
+ properties = g_hash_table_lookup (interfaces, interface);
+ if (!properties)
+ return;
+
+ g_hash_table_remove (interfaces, interface);
+
+ g_debug ("%s: removed %s at %s", self->logname, interface, path);
+ emit_remove (self, path, interface);
+}
+
+static void
+process_interfaces_removed (CockpitDBusCache *self,
+ gpointer user_data)
+{
+ ProcessInterfacesData *pis = user_data;
+ GVariant *array;
+ const gchar *path;
+ const gchar *interface;
+ GVariantIter iter;
+ BatchData *batch;
+
+ batch = batch_create (self);
+
+ /*
+ * We added a manager while processing this message, perform a full manager
+ * load as part of the same batch.
+ */
+ if (pis->manager_added)
+ retrieve_managed_objects (self, pis->manager_added, batch);
+
+ g_variant_get (pis->body, "(&o@as)", &path, &array);
+ path = intern_string (self, path);
+
+ g_variant_iter_init (&iter, array);
+ while (g_variant_iter_loop (&iter, "&s", &interface))
+ process_removed (self, path, intern_string (self, interface));
+
+ batch_unref (self, batch);
+
+ g_variant_unref (array);
+ g_variant_unref (pis->body);
+ g_slice_free (ProcessInterfacesData, pis);
+}
+
+static void
+on_manager_signal (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *path,
+ const gchar *interface,
+ const gchar *member,
+ GVariant *body,
+ gpointer user_data)
+{
+ CockpitDBusCache *self = user_data;
+ CockpitDBusBarrierFunc barrier_func = NULL;
+ const gchar * manager_added;
+ ProcessInterfacesData *pis;
+
+ /* We have to filter the sender ourselves; glib doesn't do that for
+ well-known names.
+ */
+ if (self->name_owner && g_strcmp0 (sender, self->name_owner) != 0)
+ return;
+
+ /* Note that this is an ObjectManager */
+ manager_added = cockpit_paths_add (self->managed_not_ready, path);
+
+ if (g_str_equal (member, "InterfacesAdded"))
+ {
+ if (g_variant_is_of_type (body, G_VARIANT_TYPE ("(oa{sa{sv}})")))
+ {
+ g_debug ("%s: signal InterfacesAdded at %s", self->logname, path);
+ barrier_func = process_interfaces_added;
+ }
+ else
+ {
+ g_debug ("%s: received InterfacesAdded with bad type", self->logname);
+ }
+ }
+ else if (g_str_equal (member, "InterfacesRemoved"))
+ {
+ if (g_variant_is_of_type (body, G_VARIANT_TYPE ("(oas)")))
+ {
+ g_debug ("%s: signal InterfacesRemoved at %s", self->logname, path);
+ barrier_func = process_interfaces_removed;
+ }
+ else
+ {
+ g_debug ("%s: received InterfacesRemoved with bad type", self->logname);
+ }
+ }
+
+ if (barrier_func)
+ {
+ pis = g_slice_new0 (ProcessInterfacesData);
+ pis->body = g_variant_ref (body);
+ pis->manager_added = manager_added;
+ cockpit_dbus_cache_barrier (self, barrier_func, pis);
+ }
+}
+
+static void
+cockpit_dbus_cache_constructed (GObject *object)
+{
+ CockpitDBusCache *self = COCKPIT_DBUS_CACHE (object);
+
+ g_return_if_fail (self->connection != NULL);
+
+ if (!self->introspected)
+ self->introspected = cockpit_dbus_interface_info_new ();
+
+ self->subscribe_properties = g_dbus_connection_signal_subscribe (self->connection,
+ self->name,
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ NULL, /* object_path */
+ NULL, /* arg0 */
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ on_properties_signal,
+ self, NULL);
+
+ self->subscribe_manager = g_dbus_connection_signal_subscribe (self->connection,
+ self->name,
+ "org.freedesktop.DBus.ObjectManager",
+ NULL, /* member */
+ NULL, /* object_path */
+ NULL, /* arg0 */
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ on_manager_signal,
+ self, NULL);
+
+ self->subscribed = TRUE;
+}
+
+static void
+cockpit_dbus_cache_set_property (GObject *obj,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitDBusCache *self = COCKPIT_DBUS_CACHE (obj);
+
+ switch (prop_id)
+ {
+ case PROP_CONNECTION:
+ self->connection = g_value_dup_object (value);
+ break;
+ case PROP_NAME:
+ self->name = g_value_dup_string (value);
+ break;
+ case PROP_LOGNAME:
+ self->logname = g_value_dup_string (value);
+ break;
+ case PROP_INTERFACE_INFO:
+ self->introspected = g_value_dup_boxed (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+ break;
+ }
+}
+
+void
+cockpit_dbus_cache_set_name_owner (CockpitDBusCache *self,
+ const gchar *name_owner)
+{
+ g_free (self->name_owner);
+ self->name_owner = g_strdup (name_owner);
+}
+
+
+static void
+cockpit_dbus_cache_dispose (GObject *object)
+{
+ CockpitDBusCache *self = COCKPIT_DBUS_CACHE (object);
+
+ g_cancellable_cancel (self->cancellable);
+
+ if (self->subscribed)
+ {
+ g_dbus_connection_signal_unsubscribe (self->connection, self->subscribe_properties);
+ g_dbus_connection_signal_unsubscribe (self->connection, self->subscribe_manager);
+ self->subscribed = FALSE;
+ }
+
+ introspect_flush (self);
+ batch_flush (self);
+ barrier_flush (self);
+
+ G_OBJECT_CLASS (cockpit_dbus_cache_parent_class)->dispose (object);
+}
+
+static void
+cockpit_dbus_cache_finalize (GObject *object)
+{
+ CockpitDBusCache *self = COCKPIT_DBUS_CACHE (object);
+
+ g_clear_object (&self->connection);
+ g_object_unref (self->cancellable);
+
+ g_free (self->name_owner);
+ g_free (self->name);
+ g_free (self->logname);
+
+ cockpit_dbus_rules_free (self->rules);
+ g_tree_destroy (self->managed);
+ g_tree_destroy (self->managed_not_ready);
+
+ g_queue_free (self->batches);
+ g_queue_free (self->barriers);
+
+ g_assert (self->introspects->head == NULL);
+ g_queue_free (self->introspects);
+
+ g_hash_table_unref (self->introsent);
+ g_hash_table_unref (self->introspected);
+ g_hash_table_unref (self->cache);
+
+ g_hash_table_destroy (self->interned);
+ g_list_free_full (self->trash, g_free);
+
+ G_OBJECT_CLASS (cockpit_dbus_cache_parent_class)->finalize (object);
+}
+
+static void
+cockpit_dbus_cache_class_init (CockpitDBusCacheClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->constructed = cockpit_dbus_cache_constructed;
+ gobject_class->set_property = cockpit_dbus_cache_set_property;
+ gobject_class->dispose = cockpit_dbus_cache_dispose;
+ gobject_class->finalize = cockpit_dbus_cache_finalize;
+
+ signal_meta = g_signal_new ("meta", COCKPIT_TYPE_DBUS_CACHE, G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (CockpitDBusCacheClass, meta),
+ NULL, NULL, NULL, G_TYPE_NONE,
+ 1, G_TYPE_DBUS_INTERFACE_INFO);
+
+ signal_update = g_signal_new ("update", COCKPIT_TYPE_DBUS_CACHE, G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (CockpitDBusCacheClass, update),
+ NULL, NULL, NULL, G_TYPE_NONE,
+ 1, G_TYPE_HASH_TABLE);
+
+ g_object_class_install_property (gobject_class, PROP_CONNECTION,
+ g_param_spec_object ("connection", "connection", "connection", G_TYPE_DBUS_CONNECTION,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class, PROP_NAME,
+ g_param_spec_string ("name", "name", "name", NULL,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+ g_object_class_install_property (gobject_class, PROP_LOGNAME,
+ g_param_spec_string ("logname", "logname", "logname", "internal",
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+ g_object_class_install_property (gobject_class, PROP_INTERFACE_INFO,
+ g_param_spec_boxed ("interface-info", NULL, NULL, G_TYPE_HASH_TABLE,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+}
+
+static GHashTable *
+snapshot_string_keys (GHashTable *table)
+{
+ GHashTable *set;
+ GHashTableIter iter;
+ gpointer key;
+
+ set = g_hash_table_new (g_str_hash, g_str_equal);
+ if (table)
+ {
+ g_hash_table_iter_init (&iter, table);
+ while (g_hash_table_iter_next (&iter, &key, NULL))
+ g_hash_table_add (set, key);
+ }
+
+ return set;
+}
+
+static void
+process_get_all (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *path,
+ GDBusInterfaceInfo *iface,
+ GVariant *retval)
+{
+ GVariant *dict;
+
+ g_variant_get (retval, "(@a{sv})", &dict);
+
+ process_properties (self, batch, path, iface, dict);
+ scrape_variant (self, batch, dict);
+
+ g_variant_unref (dict);
+}
+
+static void
+process_removed_path (CockpitDBusCache *self,
+ const gchar *path)
+{
+ GHashTable *interfaces;
+ GHashTableIter iter;
+ gpointer interface;
+ GHashTable *snapshot;
+
+ interfaces = g_hash_table_lookup (self->cache, path);
+ if (interfaces)
+ {
+ snapshot = snapshot_string_keys (interfaces);
+ g_hash_table_iter_init (&iter, snapshot);
+ while (g_hash_table_iter_next (&iter, &interface, NULL))
+ process_removed (self, path, interface);
+ g_hash_table_destroy (snapshot);
+ }
+}
+
+static void
+process_paths (CockpitDBusCache *self,
+ BatchData *batch,
+ GHashTable *snapshot,
+ GVariant *dict)
+{
+ GVariant *inner;
+ GHashTable *snap;
+ const gchar *path;
+ GVariantIter iter;
+ GHashTableIter hter;
+ gpointer key;
+
+ g_variant_iter_init (&iter, dict);
+ while (g_variant_iter_loop (&iter, "{o@a{sa{sv}}}", &path, &inner))
+ {
+ snap = NULL;
+ if (snapshot)
+ {
+ g_hash_table_remove (snapshot, path);
+ snap = snapshot_string_keys (g_hash_table_lookup (self->cache, path));
+ }
+
+ process_interfaces (self, batch, snap, intern_string (self, path), inner);
+
+ if (snap)
+ {
+ g_hash_table_iter_init (&hter, snap);
+ while (g_hash_table_iter_next (&hter, &key, NULL))
+ process_removed (self, path, key);
+ g_hash_table_destroy (snap);
+ }
+ }
+}
+
+static void
+process_get_managed_objects (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *manager_path,
+ GVariant *retval)
+{
+ /*
+ * So here we handle things slightly differently than just pushing the
+ * result through all the properties update mechanics. We get
+ * indications of interfaces and entire paths disappearing here,
+ * so we have to handle that.
+ */
+
+ GVariant *inner;
+ GHashTableIter iter;
+ GHashTable *snapshot;
+ gpointer path;
+
+ /* Snapshot everything under control of the path of the object manager */
+ snapshot = g_hash_table_new (g_str_hash, g_str_equal);
+ g_hash_table_iter_init (&iter, self->cache);
+ while (g_hash_table_iter_next (&iter, &path, NULL))
+ {
+ if (cockpit_path_has_ancestor (path, manager_path))
+ g_hash_table_add (snapshot, path);
+ }
+
+ g_variant_get (retval, "(@a{oa{sa{sv}}})", &inner);
+ process_paths (self, batch, snapshot, inner);
+ g_variant_unref (inner);
+
+ g_hash_table_iter_init (&iter, snapshot);
+ while (g_hash_table_iter_next (&iter, &path, NULL))
+ process_removed_path (self, path);
+ g_hash_table_unref (snapshot);
+}
+
+static void
+process_introspect_children (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *parent_path,
+ GDBusNodeInfo *node)
+{
+ GDBusNodeInfo *child;
+ GHashTable *snapshot;
+ GHashTableIter iter;
+ gchar *child_path;
+ gpointer path;
+ guint i;
+
+ /* Snapshot all direct children of path */
+ snapshot = g_hash_table_new (g_str_hash, g_str_equal);
+ g_hash_table_iter_init (&iter, self->cache);
+ while (g_hash_table_iter_next (&iter, &path, NULL))
+ {
+ if (cockpit_path_has_parent (path, parent_path))
+ g_hash_table_add (snapshot, path);
+ }
+
+ /* Poke any additional child nodes discovered */
+ for (i = 0; node->nodes && node->nodes[i]; i++)
+ {
+ child = node->nodes[i];
+
+ /* If the child has no path then it's useless */
+ if (!child->path)
+ continue;
+
+ /* Figure out an object path for this node */
+ if (g_str_has_prefix (child->path, "/"))
+ child_path = g_strdup (child->path);
+ else if (g_str_equal (parent_path, "/"))
+ child_path = g_strdup_printf ("/%s", child->path);
+ else
+ child_path = g_strdup_printf ("%s/%s", parent_path, child->path);
+
+ /* Remove everything in the snapshot related to this child */
+ g_hash_table_remove (snapshot, child_path);
+
+ if (cockpit_dbus_rules_match (self->rules, child_path, NULL, NULL, NULL) &&
+ !cockpit_paths_contain_or_ancestor (self->managed, child_path))
+ {
+ /* Inline child interfaces are rare but possible */
+ if (child->interfaces && child->interfaces[0])
+ {
+ process_introspect_node (self, batch,
+ intern_string (self, child_path),
+ child, TRUE);
+ }
+
+ /* If we have no knowledge of this child, then introspect it */
+ else
+ {
+ introspect_queue (self, batch,
+ intern_string (self, child_path),
+ NULL, NULL, NULL);
+ }
+ }
+
+ g_free (child_path);
+ }
+
+ /* Anything remaining in snapshot stays */
+ g_hash_table_iter_init (&iter, snapshot);
+ while (g_hash_table_iter_next (&iter, &path, NULL))
+ process_removed_path (self, path);
+ g_hash_table_unref (snapshot);
+}
+
+typedef struct {
+ CockpitDBusCache *self;
+ const gchar *path;
+ GDBusInterfaceInfo *iface;
+ BatchData *batch;
+} GetAllData;
+
+static void
+on_get_all_reply (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GetAllData *gad = user_data;
+ CockpitDBusCache *self = gad->self;
+ GError *error = NULL;
+ GVariant *retval;
+
+ retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
+ if (error)
+ {
+ if (!g_cancellable_is_cancelled (self->cancellable))
+ {
+ if (dbus_error_matches_unknown (error))
+ {
+ g_debug ("%s: couldn't get all properties of %s at %s: %s", self->logname,
+ gad->iface->name, gad->path, error->message);
+ }
+ else
+ {
+ g_message ("%s: couldn't get all properties of %s at %s: %s", self->logname,
+ gad->iface->name, gad->path, error->message);
+ }
+ }
+ g_error_free (error);
+ }
+
+ if (retval)
+ {
+ g_debug ("%s: reply to GetAll() for %s at %s", self->logname, gad->iface->name, gad->path);
+ process_get_all (self, gad->batch, gad->path, gad->iface, retval);
+
+ g_variant_unref (retval);
+ }
+
+ /* Whether or not this failed, we know the interface exists */
+ ensure_properties (self, gad->path, gad->iface);
+ emit_change (self, gad->path, gad->iface, NULL, NULL);
+
+ batch_unref (self, gad->batch);
+
+ g_object_unref (gad->self);
+ g_slice_free (GetAllData, gad);
+}
+
+static void
+retrieve_properties (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *path,
+ GDBusInterfaceInfo *iface)
+{
+ GetAllData *gad;
+
+ /* Don't bother getting properties for this well known interface
+ * that doesn't have any. Also, NetworkManager returns an error.
+ */
+ if (g_strcmp0 (iface->name, "org.freedesktop.DBus.Properties") == 0)
+ return;
+
+ /* Only get properties once per batch.
+ */
+
+ if (find_change (self, path, iface))
+ {
+ g_debug ("%s: skipping GetAll for %s at %s, already in batch", self->logname, iface->name, path);
+ return;
+ }
+
+ emit_change (self, path, iface, NULL, NULL);
+
+ g_debug ("%s: calling GetAll() for %s at %s", self->logname, iface->name, path);
+
+ gad = g_slice_new0 (GetAllData);
+ gad->self = g_object_ref (self);
+ gad->batch = batch_ref (batch);
+ gad->path = path;
+ gad->iface = iface;
+
+ g_dbus_connection_call (self->connection, self->name, path,
+ "org.freedesktop.DBus.Properties", "GetAll",
+ g_variant_new ("(s)", iface->name), G_VARIANT_TYPE ("(a{sv})"),
+ G_DBUS_CALL_FLAGS_NONE, -1,
+ self->cancellable, on_get_all_reply, gad);
+}
+
+static void
+process_introspect_node (CockpitDBusCache *self,
+ BatchData *batch,
+ const gchar *path,
+ GDBusNodeInfo *node,
+ gboolean recursive)
+{
+ GDBusInterfaceInfo *iface;
+ GDBusInterfaceInfo *prev;
+ GHashTable *snapshot;
+ GHashTableIter iter;
+ gpointer interface;
+ guint i;
+
+ if (cockpit_paths_contain_or_ancestor (self->managed, path))
+ recursive = FALSE;
+
+ snapshot = snapshot_string_keys (g_hash_table_lookup (self->cache, path));
+
+ for (i = 0; node->interfaces && node->interfaces[i] != NULL; i++)
+ {
+ iface = node->interfaces[i];
+ if (!iface->name)
+ {
+ g_warning ("Received interface from %s at %s without name", self->logname, path);
+ continue;
+ }
+
+ /* Cache this interface for later use elsewhere */
+ prev = cockpit_dbus_interface_info_lookup (self->introspected, iface->name);
+ if (prev)
+ {
+ iface = prev;
+ }
+ else
+ {
+ cockpit_dbus_interface_info_push (self->introspected, iface);
+ }
+
+ /* Skip these interfaces */
+ if (g_str_has_prefix (iface->name, "org.freedesktop.DBus."))
+ {
+ /* But make sure we track the fact that something is here */
+ ensure_interfaces (self, path);
+ continue;
+ }
+
+ g_hash_table_remove (snapshot, iface->name);
+
+ if (recursive && cockpit_dbus_rules_match (self->rules, path, iface->name, NULL, NULL))
+ retrieve_properties (self, batch, path, iface);
+ }
+
+ /* Remove any interfaces not seen */
+ g_hash_table_iter_init (&iter, snapshot);
+ while (g_hash_table_iter_next (&iter, &interface, NULL))
+ process_removed (self, path, interface);
+ g_hash_table_destroy (snapshot);
+
+ if (recursive)
+ process_introspect_children (self, batch, path, node);
+}
+
+typedef struct {
+ CockpitDBusCache *self;
+ const gchar *path;
+ BatchData *batch;
+} GetManagedObjectsData;
+
+static void
+on_get_managed_objects_reply (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GetManagedObjectsData *gmod = user_data;
+ CockpitDBusCache *self = gmod->self;
+ GError *error = NULL;
+ GVariant *retval;
+
+ retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
+ if (error)
+ {
+ if (!g_cancellable_is_cancelled (self->cancellable))
+ {
+ /* Doesn't support ObjectManager? */
+ if (dbus_error_matches_unknown (error))
+ {
+ g_debug ("%s: no ObjectManager at %s", self->logname, gmod->path);
+ }
+ else
+ {
+ g_message ("%s: couldn't get managed objects at %s: %s",
+ self->logname, gmod->path, error->message);
+ }
+ }
+ g_error_free (error);
+ }
+
+ if (retval)
+ {
+ g_debug ("%s: reply from GetManagedObjects() on %s", self->logname, gmod->path);
+
+ /* Note that this is indeed an object manager */
+ cockpit_paths_add (self->managed, gmod->path);
+
+ process_get_managed_objects (self, gmod->batch, gmod->path, retval);
+ g_variant_unref (retval);
+ }
+
+ /*
+ * The ObjectManager itself still needs introspecting ... since the
+ * ObjectManager path itself cannot be included in the objects reported
+ * by the ObjectManager ... dumb design decision in the dbus spec IMO.
+ *
+ * But we delay on this so that any children are treated as part of
+ * object manager, and we don't go introspecting everything.
+ */
+ introspect_queue (self, gmod->batch, gmod->path, NULL, NULL, NULL);
+
+ batch_unref (self, gmod->batch);
+
+ g_object_unref (gmod->self);
+ g_slice_free (GetManagedObjectsData, gmod);
+}
+
+static void
+retrieve_managed_objects (CockpitDBusCache *self,
+ const gchar *namespace_path,
+ BatchData *batch)
+{
+ GetManagedObjectsData *gmod;
+
+ g_assert (namespace_path != NULL);
+
+ gmod = g_slice_new0 (GetManagedObjectsData);
+ gmod->batch = batch_ref (batch);
+ gmod->path = namespace_path;
+ gmod->self = g_object_ref (self);
+
+ g_debug ("%s: calling GetManagedObjects() on %s", self->logname, namespace_path);
+
+ g_dbus_connection_call (self->connection, self->name, namespace_path,
+ "org.freedesktop.DBus.ObjectManager", "GetManagedObjects",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(a{oa{sa{sv}}})"),
+ G_DBUS_CALL_FLAGS_NONE, -1, /* timeout */
+ self->cancellable, on_get_managed_objects_reply, gmod);
+}
+
+
+void
+cockpit_dbus_cache_watch (CockpitDBusCache *self,
+ const gchar *path,
+ gboolean is_namespace,
+ const gchar *interface)
+{
+ const gchar *namespace_path;
+ BatchData *batch;
+
+ if (!cockpit_dbus_rules_add (self->rules, path, is_namespace, interface, NULL, NULL))
+ return;
+
+ if (!path)
+ {
+ path = "/";
+ is_namespace = TRUE;
+ }
+
+ batch = batch_create (self);
+ path = intern_string (self, path);
+
+ namespace_path = is_namespace ? path : NULL;
+
+ if (!namespace_path)
+ namespace_path = cockpit_paths_contain_or_ancestor (self->managed, path);
+
+ if (namespace_path)
+ {
+ retrieve_managed_objects (self, namespace_path, batch);
+ }
+ else
+ {
+ introspect_queue (self, batch, path, NULL, NULL, NULL);
+ }
+
+ batch_unref (self, batch);
+}
+
+gboolean
+cockpit_dbus_cache_unwatch (CockpitDBusCache *self,
+ const gchar *path,
+ gboolean is_namespace,
+ const gchar *interface)
+{
+ return cockpit_dbus_rules_remove (self->rules, path, is_namespace, interface, NULL, NULL);
+}
+
+static void
+scrape_variant_paths (GVariant *data,
+ GHashTable *paths)
+{
+ GVariantIter iter;
+ GVariant *child;
+
+ if (g_variant_is_of_type (data, G_VARIANT_TYPE_OBJECT_PATH))
+ {
+ g_hash_table_add (paths, (gchar *)g_variant_get_string (data, NULL));
+ }
+ else if (g_variant_is_container (data))
+ {
+ g_variant_iter_init (&iter, data);
+ while ((child = g_variant_iter_next_value (&iter)) != NULL)
+ {
+ scrape_variant_paths (child, paths);
+ g_variant_unref (child);
+ }
+ }
+}
+
+void
+cockpit_dbus_cache_barrier (CockpitDBusCache *self,
+ CockpitDBusBarrierFunc callback,
+ gpointer user_data)
+{
+ BatchData *batch;
+ BarrierData *barrier;
+
+ g_return_if_fail (callback != NULL);
+
+ batch = g_queue_peek_tail (self->batches);
+ if (batch)
+ {
+ barrier = g_slice_new0 (BarrierData);
+ barrier->number = batch->number;
+ barrier->callback = callback;
+ barrier->user_data = user_data;
+ g_queue_push_tail (self->barriers, barrier);
+ }
+ else
+ {
+ (callback) (self, user_data);
+ }
+}
+
+static void
+scrape_variant (CockpitDBusCache *self,
+ BatchData *batch,
+ GVariant *data)
+{
+ GHashTable *paths;
+ GHashTableIter iter;
+ gpointer path;
+
+ paths = g_hash_table_new (g_str_hash, g_str_equal);
+ scrape_variant_paths (data, paths);
+
+ if (batch)
+ batch = batch_ref (batch);
+
+ g_hash_table_iter_init (&iter, paths);
+ while (g_hash_table_iter_next (&iter, &path, NULL))
+ {
+ /* Used as a NULL path, we never use it when scraped */
+ if (g_str_equal (path, "/"))
+ continue;
+
+ /* Do we have it already? */
+ if (g_hash_table_lookup (self->cache, path))
+ continue;
+
+ /* Is it a managed path? */
+ if (cockpit_paths_contain_or_ancestor (self->managed, path))
+ continue;
+
+ /* Does it fit our rules */
+ if (!cockpit_dbus_rules_match (self->rules, path, NULL, NULL, NULL))
+ continue;
+
+ if (!batch)
+ batch = batch_create (self);
+
+ introspect_queue (self, batch, intern_string (self, path), NULL, NULL, NULL);
+ }
+
+ if (batch)
+ batch_unref (self, batch);
+
+ g_hash_table_destroy (paths);
+}
+
+void
+cockpit_dbus_cache_scrape (CockpitDBusCache *self,
+ GVariant *data)
+{
+ scrape_variant (self, NULL, data);
+}
+
+typedef struct {
+ const gchar *path;
+ BatchData *batch;
+} PokeData;
+
+static void
+process_poke (CockpitDBusCache *self,
+ GDBusInterfaceInfo *iface,
+ gpointer user_data)
+{
+ PokeData *pd = user_data;
+
+ retrieve_properties (self, pd->batch, pd->path, iface);
+
+ batch_unref (self, pd->batch);
+ g_slice_free (PokeData, pd);
+}
+
+void
+cockpit_dbus_cache_poke (CockpitDBusCache *self,
+ const gchar *path,
+ const gchar *interface)
+{
+ GHashTable *interfaces;
+ BatchData *batch;
+ PokeData *pd;
+
+ /* Check if we have this thing */
+ interfaces = g_hash_table_lookup (self->cache, path);
+ if (interfaces)
+ {
+ if (!interface)
+ return;
+ else if (g_hash_table_lookup (interfaces, interface))
+ return;
+ }
+
+ /* Is it a managed path? */
+ if (cockpit_paths_contain_or_ancestor (self->managed, path))
+ return;
+
+ /* Does it fit our rules */
+ if (!cockpit_dbus_rules_match (self->rules, path, interface, NULL, NULL))
+ return;
+
+ batch = batch_create (self);
+ path = intern_string (self, path);
+
+ if (interface)
+ {
+ /*
+ * A specific interface was poked. This means that we don't have to
+ * go interspecting the entire path ... if we already have information
+ * about the interface itself. So try that route.
+ */
+
+ pd = g_slice_new0 (PokeData);
+ pd->path = path;
+ pd->batch = batch_ref (batch);
+ introspect_maybe (self, batch, path, interface, process_poke, pd);
+ }
+ else
+ {
+ /* The entire path was poked, must introspect to find out about it */
+ introspect_queue (self, batch, path, NULL, NULL, NULL);
+ }
+
+ batch_unref (self, batch);
+}
+
+CockpitDBusCache *
+cockpit_dbus_cache_new (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *logname,
+ GHashTable *interface_info)
+{
+ return g_object_new (COCKPIT_TYPE_DBUS_CACHE,
+ "connection", connection,
+ "name", name,
+ "logname", logname,
+ "interface-info", interface_info,
+ NULL);
+}
+
+GHashTable *
+cockpit_dbus_interface_info_new (void)
+{
+ /* The key is owned by the value */
+ return g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
+ (GDestroyNotify)g_dbus_interface_info_unref);
+}
+
+GDBusInterfaceInfo *
+cockpit_dbus_interface_info_lookup (GHashTable *interface_info,
+ const gchar *interface_name)
+{
+ g_return_val_if_fail (interface_info != NULL, NULL);
+ g_return_val_if_fail (interface_name != NULL, NULL);
+ return g_hash_table_lookup (interface_info, interface_name);
+}
+
+void
+cockpit_dbus_interface_info_push (GHashTable *interface_info,
+ GDBusInterfaceInfo *iface)
+{
+ g_return_if_fail (interface_info != NULL);
+ g_return_if_fail (iface != NULL);
+ g_hash_table_replace (interface_info, iface->name,
+ g_dbus_interface_info_ref (iface));
+}
diff --git a/src/bridge/cockpitdbuscache.h b/src/bridge/cockpitdbuscache.h
new file mode 100644
index 0000000..6e7a2fd
--- /dev/null
+++ b/src/bridge/cockpitdbuscache.h
@@ -0,0 +1,100 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_DBUS_CACHE_H
+#define __COCKPIT_DBUS_CACHE_H
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_DBUS_CACHE (cockpit_dbus_cache_get_type ())
+#define COCKPIT_DBUS_CACHE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_DBUS_CACHE, CockpitDBusCache))
+#define COCKPIT_IS_DBUS_CACHE(k) (G_TYPE_CHECK_INSTANCE_TYPE ((k), COCKPIT_TYPE_DBUS_CACHE))
+
+typedef struct _CockpitDBusCache CockpitDBusCache;
+typedef struct _CockpitDBusCacheClass CockpitDBusCacheClass;
+
+struct _CockpitDBusCacheClass {
+ GObjectClass parent;
+
+ void (* meta) (CockpitDBusCache *self,
+ GDBusInterfaceInfo *iface);
+
+ void (* update) (CockpitDBusCache *self,
+ const gchar *path,
+ GDBusInterfaceInfo *iface);
+};
+
+typedef void (* CockpitDBusIntrospectFunc) (CockpitDBusCache *cache,
+ GDBusInterfaceInfo *iface,
+ gpointer user_data);
+
+typedef void (* CockpitDBusBarrierFunc) (CockpitDBusCache *cache,
+ gpointer user_data);
+
+GType cockpit_dbus_cache_get_type (void) G_GNUC_CONST;
+
+CockpitDBusCache * cockpit_dbus_cache_new (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *logname,
+ GHashTable *interface_info);
+
+void cockpit_dbus_cache_barrier (CockpitDBusCache *self,
+ CockpitDBusBarrierFunc callback,
+ gpointer user_data);
+
+void cockpit_dbus_cache_poke (CockpitDBusCache *self,
+ const gchar *path,
+ const gchar *iface);
+
+void cockpit_dbus_cache_scrape (CockpitDBusCache *self,
+ GVariant *data);
+
+void cockpit_dbus_cache_watch (CockpitDBusCache *self,
+ const gchar *path,
+ gboolean is_namespace,
+ const gchar *interface);
+
+gboolean cockpit_dbus_cache_unwatch (CockpitDBusCache *self,
+ const gchar *path,
+ gboolean is_namespace,
+ const gchar *interface);
+
+void cockpit_dbus_cache_introspect (CockpitDBusCache *self,
+ const gchar *path,
+ const gchar *interface,
+ CockpitDBusIntrospectFunc callback,
+ gpointer user_data);
+
+void cockpit_dbus_cache_set_name_owner (CockpitDBusCache *self,
+ const gchar *name_owner);
+
+
+GHashTable * cockpit_dbus_interface_info_new (void);
+
+GDBusInterfaceInfo * cockpit_dbus_interface_info_lookup (GHashTable *interface_info,
+ const gchar *interface_name);
+
+void cockpit_dbus_interface_info_push (GHashTable *interface_info,
+ GDBusInterfaceInfo *interface);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_DBUS_CACHE_H */
diff --git a/src/bridge/cockpitdbusconfig.c b/src/bridge/cockpitdbusconfig.c
new file mode 100644
index 0000000..0d3fad1
--- /dev/null
+++ b/src/bridge/cockpitdbusconfig.c
@@ -0,0 +1,171 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbusinternal.h"
+#include "common/cockpitconf.h"
+
+static void
+config_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ if (g_str_equal (method_name, "Reload"))
+ {
+ cockpit_conf_cleanup ();
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ }
+ else if (g_str_equal (method_name, "GetString"))
+ {
+ const gchar *section, *key;
+ const char *result;
+
+ g_variant_get (parameters, "(&s&s)", &section, &key);
+
+ result = cockpit_conf_string (section, key);
+ if (result)
+ g_dbus_method_invocation_return_value (invocation, g_variant_new ("(s)", result));
+ else
+ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "key '%s' in section '%s' does not exist",
+ key, section);
+ }
+ else if (g_str_equal (method_name, "GetUInt"))
+ {
+ const gchar *section, *key;
+ unsigned _default, max, min;
+ unsigned result;
+
+ g_variant_get (parameters, "(&s&suuu)", &section, &key, &_default, &max, &min);
+ result = cockpit_conf_uint (section, key, _default, max, min);
+ g_dbus_method_invocation_return_value (invocation, g_variant_new ("(u)", result));
+ }
+ else
+ {
+ g_return_if_reached ();
+ }
+}
+
+static GDBusInterfaceVTable config_vtable = {
+ .method_call = config_method_call,
+};
+
+static GDBusArgInfo config_section_arg = {
+ -1, "section", "s", NULL
+};
+
+static GDBusArgInfo config_key_arg = {
+ -1, "key", "s", NULL
+};
+
+static GDBusArgInfo config_default_uint_arg = {
+ -1, "default", "u", NULL
+};
+
+static GDBusArgInfo config_max_uint_arg = {
+ -1, "max", "u", NULL
+};
+
+static GDBusArgInfo config_min_uint_arg = {
+ -1, "min", "u", NULL
+};
+
+static GDBusArgInfo *config_getstring_in_args[] = {
+ &config_section_arg,
+ &config_key_arg,
+ NULL
+};
+
+static GDBusArgInfo config_string_value_arg = {
+ -1, "value", "s", NULL
+};
+
+static GDBusArgInfo *config_string_value_out_args[] = {
+ &config_string_value_arg,
+ NULL
+};
+
+static GDBusArgInfo config_uint_value_arg = {
+ -1, "value", "u", NULL
+};
+
+static GDBusArgInfo *config_uint_value_out_args[] = {
+ &config_uint_value_arg,
+ NULL
+};
+
+static GDBusArgInfo *config_getuint_in_args[] = {
+ &config_section_arg,
+ &config_key_arg,
+ &config_default_uint_arg,
+ &config_max_uint_arg,
+ &config_min_uint_arg,
+ NULL
+};
+
+static GDBusMethodInfo config_reload_method = {
+ -1, "Reload", NULL, NULL, NULL
+};
+
+static GDBusMethodInfo config_getstring_method = {
+ -1, "GetString", config_getstring_in_args, config_string_value_out_args, NULL
+};
+
+static GDBusMethodInfo config_getuint_method = {
+ -1, "GetUInt", config_getuint_in_args, config_uint_value_out_args, NULL
+};
+
+static GDBusMethodInfo *config_methods[] = {
+ &config_reload_method,
+ &config_getstring_method,
+ &config_getuint_method,
+ NULL
+};
+
+static GDBusInterfaceInfo config_interface = {
+ -1, "cockpit.Config", config_methods, NULL, NULL, NULL
+};
+
+void
+cockpit_dbus_config_startup (void)
+{
+ GDBusConnection *connection;
+ GError *error = NULL;
+
+ connection = cockpit_dbus_internal_server ();
+ g_return_if_fail (connection != NULL);
+
+ g_dbus_connection_register_object (connection, "/config", &config_interface,
+ &config_vtable, NULL, NULL, &error);
+
+ if (error != NULL)
+ {
+ g_critical ("couldn't register DBus cockpit.Config object: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ g_object_unref (connection);
+}
diff --git a/src/bridge/cockpitdbusinternal.c b/src/bridge/cockpitdbusinternal.c
new file mode 100644
index 0000000..919c3af
--- /dev/null
+++ b/src/bridge/cockpitdbusinternal.c
@@ -0,0 +1,126 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitsocket.h"
+#include "cockpitdbusinternal.h"
+
+static GDBusConnection *the_server = NULL;
+static GDBusConnection *the_client = NULL;
+const gchar *the_name = NULL;
+
+GDBusConnection *
+cockpit_dbus_internal_client (void)
+{
+ g_return_val_if_fail (the_client != NULL, NULL);
+ return g_object_ref (the_client);
+}
+
+const gchar *
+cockpit_dbus_internal_name (void)
+{
+ return the_name;
+}
+
+GDBusConnection *
+cockpit_dbus_internal_server (void)
+{
+ g_return_val_if_fail (the_server != NULL, NULL);
+ return g_object_ref (the_server);
+}
+
+static void
+on_complete_get_result (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **ret = user_data;
+ g_assert (*ret == NULL);
+ *ret = g_object_ref (result);
+}
+
+void
+cockpit_dbus_internal_startup (gboolean interact)
+{
+ GAsyncResult *rclient = NULL;
+ GAsyncResult *rserver = NULL;
+ GError *error = NULL;
+ gchar *guid;
+
+ /*
+ * When in interactive mode, we allow poking and prodding our internal
+ * DBus interface. Therefore be on the session bus instead of peer-to-peer.
+ */
+ if (interact)
+ {
+ the_server = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
+ if (the_server)
+ {
+ the_name = g_dbus_connection_get_unique_name (the_server);
+ the_client = g_object_ref (the_server);
+ return;
+ }
+ else
+ {
+ g_message ("couldn't connect to session bus: %s", error->message);
+ g_clear_error (&error);
+ }
+ }
+
+ g_autoptr(GIOStream) one, two;
+ cockpit_socket_streampair (&one, &two);
+
+ guid = g_dbus_generate_guid ();
+ g_dbus_connection_new (one, guid,
+ G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_SERVER |
+ G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_ALLOW_ANONYMOUS,
+ NULL, NULL, on_complete_get_result, &rserver);
+
+ g_dbus_connection_new (two, NULL, G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,
+ NULL, NULL, on_complete_get_result, &rclient);
+
+ while (!rserver || !rclient)
+ g_main_context_iteration (NULL, TRUE);
+
+ the_server = g_dbus_connection_new_finish (rserver, &error);
+ if (the_server == NULL)
+ {
+ g_warning ("couldn't create internal connection: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ the_client = g_dbus_connection_new_finish (rclient, &error);
+ if (the_client == NULL)
+ {
+ g_warning ("couldn't create internal connection: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ g_object_unref (rclient);
+ g_object_unref (rserver);
+ g_free (guid);
+}
+
+void
+cockpit_dbus_internal_cleanup (void)
+{
+ g_clear_object (&the_client);
+ g_clear_object (&the_server);
+}
diff --git a/src/bridge/cockpitdbusinternal.h b/src/bridge/cockpitdbusinternal.h
new file mode 100644
index 0000000..61aba7f
--- /dev/null
+++ b/src/bridge/cockpitdbusinternal.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_DBUS_INTERNAL_H
+#define __COCKPIT_DBUS_INTERNAL_H
+
+#include <gio/gio.h>
+#include <pwd.h>
+
+G_BEGIN_DECLS
+
+GDBusConnection * cockpit_dbus_internal_client (void);
+
+GDBusConnection * cockpit_dbus_internal_server (void);
+
+const gchar * cockpit_dbus_internal_name (void);
+
+void cockpit_dbus_internal_startup (gboolean interact);
+
+void cockpit_dbus_internal_cleanup (void);
+
+void cockpit_dbus_user_startup (struct passwd *pwd);
+
+void cockpit_dbus_process_startup (void);
+
+void cockpit_dbus_machines_startup (void);
+
+void cockpit_dbus_machines_cleanup (void);
+
+void cockpit_dbus_config_startup (void);
+
+void cockpit_dbus_login_messages_startup (void);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_DBUS_INTERNAL_H */
diff --git a/src/bridge/cockpitdbusjson.c b/src/bridge/cockpitdbusjson.c
new file mode 100644
index 0000000..2c2175a
--- /dev/null
+++ b/src/bridge/cockpitdbusjson.c
@@ -0,0 +1,3264 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbusjson.h"
+
+#include "cockpitpipechannel.h"
+#include "cockpitdbuscache.h"
+#include "cockpitdbusinternal.h"
+#include "cockpitdbusmeta.h"
+#include "cockpitdbusrules.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpitjson.h"
+
+#include <json-glib/json-glib.h>
+
+#include <gio/gunixfdlist.h>
+
+#include <string.h>
+
+/**
+ * CockpitDBusJson:
+ *
+ * A #CockpitChannel that sends DBus messages with the dbus-json payload
+ * type.
+ */
+
+gboolean cockpit_dbus_json_allow_external = TRUE;
+
+/* The maximum number of DBus passed fds open without a channel */
+#define MAX_RECEIVED_DBUS_FDS 16
+
+#define COCKPIT_DBUS_JSON(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_DBUS_JSON, CockpitDBusJson))
+
+typedef struct {
+ CockpitChannel parent;
+ GDBusConnection *connection;
+ GBusType bus_type;
+
+ /* Talking to */
+ const gchar *logname;
+ const gchar *default_name;
+ guint default_watch;
+ gboolean default_watched;
+ gboolean default_appeared;
+
+ /* Call related */
+ GCancellable *cancellable;
+ GList *active_calls;
+ GHashTable *interface_info;
+
+ /* Per name information */
+ GHashTable *peers;
+
+ /* Invocations and publishing */
+ GHashTable *invocations;
+ GHashTable *registered;
+ guint last_invocation;
+
+ /* File descriptors */
+ GQueue *fd_channel_ids;
+} CockpitDBusJson;
+
+typedef struct {
+ gchar *name;
+ CockpitDBusJson *dbus_json;
+
+ guint subscribe_id;
+ gboolean subscribed;
+
+ /* Signal related */
+ CockpitDBusRules *rules;
+
+ /* Watch and introspection */
+ CockpitDBusCache *cache;
+ gulong meta_sig;
+ gulong update_sig;
+} CockpitDBusPeer;
+
+typedef struct {
+ CockpitChannelClass parent_class;
+} CockpitDBusJsonClass;
+
+G_DEFINE_TYPE (CockpitDBusJson, cockpit_dbus_json, COCKPIT_TYPE_CHANNEL);
+
+#if !GLIB_CHECK_VERSION(2,46,0)
+#define G_DBUS_MESSAGE_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION (1<<2)
+#endif
+
+/*
+ * Shared D-Bus connections.
+ *
+ * @group_connections maps "group:address" strings to GDBusConnection
+ * objects.
+ *
+ * @pending_group_connections maps "group:address" strings to GPtrArrays
+ * with CockpitDBusJson objects that are waiting for this connection to
+ * be established.
+ */
+static GHashTable *group_connections;
+static GHashTable *pending_group_connections;
+
+static const gchar *
+value_type_name (JsonNode *node)
+{
+ GType type = json_node_get_value_type (node);
+ if (type == G_TYPE_STRING)
+ return "string";
+ else if (type == G_TYPE_INT64)
+ return "int";
+ else if (type == G_TYPE_DOUBLE)
+ return "double";
+ else
+ return g_type_name (type);
+}
+
+static gboolean
+check_type (JsonNode *node,
+ JsonNodeType type,
+ GType sub_type,
+ GError **error)
+{
+ if (JSON_NODE_TYPE (node) != type ||
+ (type == JSON_NODE_VALUE && (json_node_get_value_type (node) != sub_type)))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Unexpected type '%s' in argument", value_type_name (node));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+check_int_type (JsonNode *node,
+ const GVariantType *type,
+ gint64 min,
+ gint64 max,
+ GError **error)
+{
+ gint64 value;
+
+ if (!check_type(node, JSON_NODE_VALUE, G_TYPE_INT64, error))
+ return FALSE;
+
+ value = json_node_get_int (node);
+ if (value < min || value > max)
+ {
+ g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Number '%" G_GINT64_FORMAT "' is not in range for the expected type '%.*s'", value,
+ (int) g_variant_type_get_string_length (type),
+ g_variant_type_peek_string (type));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static GVariant *
+parse_json (JsonNode *node,
+ const GVariantType *type,
+ GError **error);
+
+static GVariant *
+parse_json_tuple (JsonNode *node,
+ const GVariantType *child_type,
+ GError **error)
+{
+ GVariant *result = NULL;
+ GPtrArray *children;
+ GVariant *value;
+ JsonArray *array;
+ guint length;
+ guint i;
+
+ children = g_ptr_array_new ();
+
+ if (!check_type (node, JSON_NODE_ARRAY, 0, error))
+ goto out;
+
+ array = json_node_get_array (node);
+ length = json_array_get_length (array);
+
+ for (i = 0; i < length; i++)
+ {
+ value = NULL;
+ if (child_type == NULL)
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Too many values in tuple/struct");
+ }
+ else
+ {
+ value = parse_json (json_array_get_element (array, i),
+ child_type, error);
+ }
+
+ if (!value)
+ goto out;
+
+ g_ptr_array_add (children, value);
+ child_type = g_variant_type_next (child_type);
+ }
+
+ if (child_type)
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Too few values in tuple/struct");
+ goto out;
+ }
+
+ result = g_variant_new_tuple ((GVariant *const *)children->pdata,
+ children->len);
+ children->len = 0;
+
+out:
+ g_ptr_array_foreach (children, (GFunc)g_variant_unref, NULL);
+ g_ptr_array_free (children, TRUE);
+ return result;
+}
+
+static GVariant *
+parse_json_byte_array (JsonNode *node,
+ GError **error)
+{
+ static const char valid[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ GVariant *result = NULL;
+ const gchar *value;
+ gpointer data = NULL;
+ gsize length;
+ gsize pos;
+
+ if (!check_type (node, JSON_NODE_VALUE, G_TYPE_STRING, error))
+ goto out;
+
+ value = json_node_get_string (node);
+
+ pos = strspn (value, valid);
+ while (value[pos] == '=')
+ pos++;
+
+ if (pos == 0)
+ {
+ data = NULL;
+ length = 0;
+ }
+ else
+ {
+ /* base64 strings are always multiple of 3 */
+ if (pos % 3 == 0 || value[pos] == '\0')
+ data = g_base64_decode (value, &length);
+
+ if (!data)
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Invalid base64 in argument");
+ goto out;
+ }
+ }
+
+ result = g_variant_new_from_data (G_VARIANT_TYPE_BYTESTRING,
+ data, length, TRUE,
+ g_free, data);
+
+out:
+ return result;
+}
+
+static GVariant *
+parse_json_array (JsonNode *node,
+ const GVariantType *child_type,
+ GError **error)
+{
+ GVariant *result = NULL;
+ GPtrArray *children;
+ GVariant *child;
+ JsonArray *array;
+ guint length;
+ guint i;
+
+ children = g_ptr_array_new ();
+
+ if (!check_type (node, JSON_NODE_ARRAY, 0, error))
+ goto out;
+
+ array = json_node_get_array (node);
+ length = json_array_get_length (array);
+
+ for (i = 0; i < length; i++)
+ {
+ child = parse_json (json_array_get_element (array, i),
+ child_type, error);
+ if (!child)
+ goto out;
+
+ g_ptr_array_add (children, child);
+ }
+
+ result = g_variant_new_array (child_type,
+ (GVariant *const *)children->pdata,
+ children->len);
+ children->len = 0;
+
+out:
+ g_ptr_array_foreach (children, (GFunc)g_variant_unref, NULL);
+ g_ptr_array_free (children, TRUE);
+ return result;
+}
+
+static GVariant *
+parse_json_variant (JsonNode *node,
+ GError **error)
+{
+ GVariantType *inner_type;
+ JsonObject *object;
+ GVariant *inner;
+ JsonNode *val;
+ const gchar *sig;
+
+ if (!check_type (node, JSON_NODE_OBJECT, 0, error))
+ return NULL;
+
+ object = json_node_get_object (node);
+ val = json_object_get_member (object, "v");
+ if (val == NULL)
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Variant object did not contain a 'v' field");
+ return NULL;
+ }
+ if (!cockpit_json_get_string (object, "t", NULL, &sig) || !sig)
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Variant object did not contain valid 't' field");
+ return NULL;
+ }
+ if (!g_variant_type_string_is_valid (sig))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Variant 't' field '%s' is invalid", sig);
+ return NULL;
+ }
+
+ inner_type = g_variant_type_new (sig);
+ inner = parse_json (val, inner_type, error);
+ g_variant_type_free (inner_type);
+
+ if (!inner)
+ return NULL;
+
+ return g_variant_new_variant (inner);
+}
+
+static GVariant *
+parse_json_dictionary (JsonNode *node,
+ const GVariantType *entry_type,
+ GError **error)
+{
+ const GVariantType *key_type;
+ const GVariantType *value_type;
+ GVariant *result = NULL;
+ GPtrArray *children;
+ JsonObject *object;
+ JsonNode *key_node;
+ GList *members = NULL;
+ gboolean is_string;
+ GVariant *value;
+ GVariant *key;
+ GVariant *child;
+ GList *l;
+
+ children = g_ptr_array_new ();
+
+ if (!check_type (node, JSON_NODE_OBJECT, 0, error))
+ goto out;
+
+ object = json_node_get_object (node);
+ key_type = g_variant_type_key (entry_type);
+ value_type = g_variant_type_value (entry_type);
+
+ is_string = (g_variant_type_equal (key_type, G_VARIANT_TYPE_STRING) ||
+ g_variant_type_equal (key_type, G_VARIANT_TYPE_OBJECT_PATH) ||
+ g_variant_type_equal (key_type, G_VARIANT_TYPE_SIGNATURE));
+
+ members = json_object_get_members (object);
+ for (l = members; l != NULL; l = g_list_next (l))
+ {
+ if (is_string)
+ {
+ key_node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_string (key_node, l->data);
+ }
+ else
+ {
+ key_node = cockpit_json_parse (l->data, -1, NULL);
+ if (key_node == NULL)
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Unexpected key '%s' in dict entry", (gchar *)l->data);
+ goto out;
+ }
+ }
+
+ key = parse_json (key_node, key_type, error);
+ json_node_free (key_node);
+
+ if (!key)
+ goto out;
+
+ value = parse_json (json_object_get_member (object, l->data),
+ value_type, error);
+ if (!value)
+ {
+ g_variant_unref (key);
+ goto out;
+ }
+
+ child = g_variant_new_dict_entry (key, value);
+ g_ptr_array_add (children, child);
+ }
+
+ result = g_variant_new_array (entry_type,
+ (GVariant *const *)children->pdata,
+ children->len);
+ children->len = 0;
+
+out:
+ g_list_free (members);
+ g_ptr_array_foreach (children, (GFunc)g_variant_unref, NULL);
+ g_ptr_array_free (children, TRUE);
+ return result;
+}
+
+static GVariant *
+parse_json_object_path (JsonNode *node,
+ GError **error)
+{
+ GVariant *result = NULL;
+ const gchar *str;
+
+ if (!check_type (node, JSON_NODE_VALUE, G_TYPE_STRING, error))
+ return NULL;
+
+ str = json_node_get_string (node);
+ if (g_variant_is_object_path (str))
+ {
+ result = g_variant_new_object_path (str);
+ }
+ else
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Invalid object path '%s'", str);
+ }
+
+ return result;
+}
+
+static GVariant *
+parse_json_signature (JsonNode *node,
+ GError **error)
+{
+ const gchar *str;
+
+ if (!check_type (node, JSON_NODE_VALUE, G_TYPE_STRING, error))
+ return NULL;
+
+ str = json_node_get_string (node);
+ if (g_variant_is_signature (str))
+ return g_variant_new_signature (str);
+ else
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Invalid signature '%s'", str);
+ return NULL;
+ }
+}
+
+static void
+parse_not_supported (const GVariantType *type,
+ GError **error)
+{
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Type '%.*s' is unknown or not supported",
+ (int)g_variant_type_get_string_length (type),
+ g_variant_type_peek_string (type));
+}
+
+static GVariant *
+parse_json (JsonNode *node,
+ const GVariantType *type,
+ GError **error)
+{
+ const GVariantType *element_type;
+
+ if (!g_variant_type_is_definite (type))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "Indefinite type '%.*s' is not supported",
+ (int)g_variant_type_get_string_length (type),
+ g_variant_type_peek_string (type));
+ return NULL;
+ }
+
+ if (g_variant_type_is_basic (type))
+ {
+ if (g_variant_type_equal (type, G_VARIANT_TYPE_BOOLEAN))
+ {
+ if (check_type (node, JSON_NODE_VALUE, G_TYPE_BOOLEAN, error))
+ return g_variant_new_boolean (json_node_get_boolean (node));
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_BYTE))
+ {
+ if (check_int_type (node, type, 0, G_MAXUINT8, error))
+ return g_variant_new_byte (json_node_get_int (node));
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_INT16))
+ {
+ if (check_int_type (node, type, G_MININT16, G_MAXINT16, error))
+ return g_variant_new_int16 (json_node_get_int (node));
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_UINT16))
+ {
+ if (check_int_type (node, type, 0, G_MAXUINT16, error))
+ return g_variant_new_uint16 (json_node_get_int (node));
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_INT32))
+ {
+ if (check_int_type (node, type, G_MININT32, G_MAXINT32, error))
+ return g_variant_new_int32 (json_node_get_int (node));
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_UINT32))
+ {
+ if (check_int_type (node, type, 0, G_MAXUINT32, error))
+ return g_variant_new_uint32 (json_node_get_int (node));
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_INT64))
+ {
+ if (check_int_type (node, type, G_MININT64, G_MAXINT64, error))
+ return g_variant_new_int64 (json_node_get_int (node));
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_UINT64))
+ {
+ /* Can never be larger than signed int64, because json-glib
+ * only returns doubles or signed 64-bit integers when
+ * encountering a JSON number. */
+ if (check_int_type (node, type, 0, G_MAXINT64, error))
+ return g_variant_new_uint64 (json_node_get_int (node));
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_DOUBLE))
+ {
+ if (check_type (node, JSON_NODE_VALUE, G_TYPE_INT64, NULL))
+ return g_variant_new_double (json_node_get_int (node));
+ else if (check_type (node, JSON_NODE_VALUE, G_TYPE_DOUBLE, error))
+ return g_variant_new_double (json_node_get_double (node));
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_STRING))
+ {
+ if (check_type (node, JSON_NODE_VALUE, G_TYPE_STRING, error))
+ return g_variant_new_string (json_node_get_string (node));
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_OBJECT_PATH))
+ {
+ return parse_json_object_path (node, error);
+ }
+ else if (g_variant_type_equal (type, G_VARIANT_TYPE_SIGNATURE))
+ {
+ return parse_json_signature (node, error);
+ }
+ else
+ {
+ parse_not_supported (type, error);
+ }
+ }
+ else if (g_variant_type_is_variant (type))
+ {
+ return parse_json_variant (node, error);
+ }
+ else if (g_variant_type_is_array (type))
+ {
+ element_type = g_variant_type_element (type);
+ if (g_variant_type_equal (element_type, G_VARIANT_TYPE_BYTE))
+ return parse_json_byte_array (node, error);
+ else if (g_variant_type_is_dict_entry (element_type))
+ return parse_json_dictionary (node, element_type, error);
+ else
+ return parse_json_array (node, element_type, error);
+ }
+ else if (g_variant_type_is_tuple (type))
+ {
+ return parse_json_tuple (node, g_variant_type_first (type), error);
+ }
+ else
+ {
+ parse_not_supported (type, error);
+ }
+
+ return NULL;
+}
+
+typedef struct {
+ GUnixFDList *fdlist;
+ GQueue *fdids;
+} VariantContext;
+
+static JsonNode *
+build_json (GVariant *value,
+ VariantContext *context);
+
+static JsonObject *
+build_json_variant (GVariant *value,
+ VariantContext *context)
+{
+ GVariant *child;
+ JsonObject *object;
+
+ child = g_variant_get_variant (value);
+ object = json_object_new ();
+ json_object_set_string_member (object, "t", g_variant_get_type_string (child));
+ json_object_set_member (object, "v", build_json (child, context));
+
+ g_variant_unref (child);
+
+ return object;
+}
+
+static JsonNode *
+build_json_byte_array (GVariant *value)
+{
+ JsonNode *node;
+ gconstpointer data;
+ gsize length = 0;
+ gchar *string;
+
+ data = g_variant_get_fixed_array (value, &length, 1);
+ if (length > 0)
+ string = g_base64_encode (data, length);
+ else
+ string = NULL;
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_string (node, string ? string : "");
+ g_free (string); /* unfortunately :S */
+
+ return node;
+}
+
+static JsonArray *
+build_json_array_or_tuple (GVariant *value,
+ VariantContext *context)
+{
+ GVariantIter iter;
+ GVariant *child;
+ JsonArray *array;
+
+ array = json_array_new ();
+
+ g_variant_iter_init (&iter, value);
+ while ((child = g_variant_iter_next_value (&iter)) != NULL)
+ {
+ json_array_add_element (array, build_json (child, context));
+ g_variant_unref (child);
+ }
+
+ return array;
+}
+
+static JsonObject *
+build_json_dictionary (const GVariantType *entry_type,
+ GVariant *dict,
+ VariantContext *context)
+{
+ const GVariantType *key_type;
+ GVariantIter iter;
+ GVariant *child;
+ GVariant *key;
+ GVariant *value;
+ gboolean is_string;
+ gchar *key_string;
+ JsonObject *object;
+
+ object = json_object_new ();
+ key_type = g_variant_type_key (entry_type);
+
+ is_string = (g_variant_type_equal (key_type, G_VARIANT_TYPE_STRING) ||
+ g_variant_type_equal (key_type, G_VARIANT_TYPE_OBJECT_PATH) ||
+ g_variant_type_equal (key_type, G_VARIANT_TYPE_SIGNATURE));
+
+ g_variant_iter_init (&iter, dict);
+ while ((child = g_variant_iter_next_value (&iter)) != NULL)
+ {
+ key = g_variant_get_child_value (child, 0);
+ value = g_variant_get_child_value (child, 1);
+
+ if (is_string)
+ {
+ json_object_set_member (object, g_variant_get_string (key, NULL), build_json (value, context));
+ }
+ else
+ {
+ key_string = g_variant_print (key, FALSE);
+ json_object_set_member (object, key_string, build_json (value, context));
+ g_free (key_string);
+ }
+
+ g_variant_unref (key);
+ g_variant_unref (value);
+ }
+
+ return object;
+}
+
+static JsonNode *
+build_json_fd_channel (GVariant *value,
+ VariantContext *context)
+{
+ JsonObject *object;
+ GError *error = NULL;
+ JsonNode *node;
+ gint fd = -1;
+ gchar *old;
+ const gchar *id;
+
+ if (context->fdlist)
+ {
+ fd = g_unix_fd_list_get (context->fdlist, g_variant_get_handle (value), &error);
+ if (fd == -1)
+ {
+ g_warning ("couldn't dup file descriptor from DBus message: %s", error->message);
+ g_clear_error (&error);
+ }
+ }
+
+ if (fd < 0)
+ {
+ node = json_node_new (JSON_NODE_NULL);
+ }
+ else
+ {
+ g_assert (context->fdlist != NULL);
+ g_assert (context->fdids != NULL);
+
+ node = json_node_new (JSON_NODE_OBJECT);
+
+ /* Add a new internal channel name for this file descriptor */
+ id = cockpit_pipe_channel_add_internal_fd (fd);
+ g_queue_push_tail (context->fdids, (gpointer) g_strdup (id));
+
+ /* And only keep the last N ready for opening channels */
+ while (g_queue_get_length (context->fdids) > MAX_RECEIVED_DBUS_FDS)
+ {
+ old = (gchar *)g_queue_pop_head (context->fdids);
+ cockpit_pipe_channel_remove_internal_fd (old);
+
+ g_free (old);
+ }
+
+ /* This is sent back as the list of channel options to use */
+ object = json_object_new ();
+ json_object_set_string_member (object, "payload", "stream");
+ json_object_set_string_member (object, "internal", id);
+ json_node_set_object (node, object);
+ json_object_unref (object);
+ }
+
+ return node;
+}
+
+static JsonNode *
+build_json (GVariant *value,
+ VariantContext *context)
+{
+ const GVariantType *type;
+ const GVariantType *element_type;
+ JsonObject *object;
+ JsonArray *array;
+ JsonNode *node;
+
+ switch (g_variant_classify (value))
+ {
+ case G_VARIANT_CLASS_BOOLEAN:
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_boolean (node, g_variant_get_boolean (value));
+ return node;
+
+ case G_VARIANT_CLASS_BYTE:
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_int (node, g_variant_get_byte (value));
+ return node;
+
+ case G_VARIANT_CLASS_INT16:
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_int (node, g_variant_get_int16 (value));
+ return node;
+
+ case G_VARIANT_CLASS_UINT16:
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_int (node, g_variant_get_uint16 (value));
+ return node;
+
+ case G_VARIANT_CLASS_INT32:
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_int (node, g_variant_get_int32 (value));
+ return node;
+
+ case G_VARIANT_CLASS_UINT32:
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_int (node, g_variant_get_uint32 (value));
+ return node;
+
+ case G_VARIANT_CLASS_INT64:
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_int (node, g_variant_get_int64 (value));
+ return node;
+
+ case G_VARIANT_CLASS_UINT64:
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_int (node, g_variant_get_uint64 (value));
+ return node;
+
+ case G_VARIANT_CLASS_HANDLE:
+ node = build_json_fd_channel (value, context);
+ return node;
+
+ case G_VARIANT_CLASS_DOUBLE:
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_double (node, g_variant_get_double (value));
+ return node;
+
+ case G_VARIANT_CLASS_STRING: /* explicit fall-through */
+ case G_VARIANT_CLASS_OBJECT_PATH: /* explicit fall-through */
+ case G_VARIANT_CLASS_SIGNATURE:
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_string (node, g_variant_get_string (value, NULL));
+ return node;
+
+ case G_VARIANT_CLASS_VARIANT:
+ object = build_json_variant (value, context);
+ node = json_node_new (JSON_NODE_OBJECT);
+ json_node_take_object (node, object);
+ return node;
+
+ case G_VARIANT_CLASS_ARRAY:
+ type = g_variant_get_type (value);
+ element_type = g_variant_type_element (type);
+ if (g_variant_type_is_dict_entry (element_type))
+ {
+ object = build_json_dictionary (element_type, value, context);
+ node = json_node_new (JSON_NODE_OBJECT);
+ json_node_take_object (node, object);
+ }
+ else if (g_variant_type_equal (element_type, G_VARIANT_TYPE_BYTE))
+ {
+ node = build_json_byte_array (value);
+ }
+ else
+ {
+ array = build_json_array_or_tuple (value, context);
+ node = json_node_new (JSON_NODE_ARRAY);
+ json_node_set_array (node, array);
+ json_array_unref (array);
+ }
+ return node;
+
+ case G_VARIANT_CLASS_TUPLE:
+ array = build_json_array_or_tuple (value, context);
+ node = json_node_new (JSON_NODE_ARRAY);
+ json_node_set_array (node, array);
+ json_array_unref (array);
+ return node;
+
+ case G_VARIANT_CLASS_DICT_ENTRY:
+ case G_VARIANT_CLASS_MAYBE:
+ default:
+ g_return_val_if_reached (NULL);
+ break;
+ }
+}
+
+static void
+send_json_object (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ GBytes *bytes;
+
+ bytes = cockpit_json_write_bytes (object);
+ cockpit_channel_send (COCKPIT_CHANNEL (self), bytes, TRUE);
+ g_bytes_unref (bytes);
+}
+
+static JsonObject *
+build_json_error (GError *error)
+{
+ JsonObject *object;
+ JsonArray *reply;
+ JsonArray *args;
+ gchar *error_name;
+
+ object = json_object_new ();
+ reply = json_array_new ();
+ args = json_array_new ();
+
+ error_name = g_dbus_error_get_remote_error (error);
+ g_dbus_error_strip_remote_error (error);
+
+ json_array_add_string_element (reply, error_name != NULL ? error_name : "");
+ g_free (error_name);
+
+ if (error->message)
+ json_array_add_string_element (args, error->message);
+ json_array_add_array_element (reply, args);
+
+ json_object_set_array_member (object, "error", reply);
+
+ return object;
+}
+
+static gchar *
+build_signature (GVariant *variant)
+{
+ const GVariantType *type;
+ GString *sig;
+
+ sig = g_string_new ("");
+ type = g_variant_get_type (variant);
+ for (type = g_variant_type_first (type);
+ type != NULL;
+ type = g_variant_type_next (type))
+ {
+ g_string_append_len (sig, g_variant_type_peek_string (type),
+ g_variant_type_get_string_length (type));
+ }
+ return g_string_free (sig, FALSE);
+}
+
+static JsonNode *
+build_json_body (GVariant *body,
+ VariantContext *context,
+ gchar **type)
+{
+ if (body)
+ {
+ if (type)
+ *type = build_signature (body);
+ return build_json (body, context);
+ }
+ else
+ {
+ if (type)
+ *type = NULL;
+ return json_node_new (JSON_NODE_NULL);
+ }
+}
+
+static JsonObject *
+build_json_signal (const gchar *path,
+ const gchar *interface,
+ const gchar *member,
+ GVariant *body)
+{
+ JsonObject *object;
+ JsonArray *signal;
+ VariantContext context = { NULL };
+
+ object = json_object_new ();
+ signal = json_array_new ();
+ json_array_add_string_element (signal, path);
+ json_array_add_string_element (signal, interface);
+ json_array_add_string_element (signal, member);
+ json_array_add_element (signal, build_json_body (body, &context, NULL));
+ json_object_set_array_member (object, "signal", signal);
+
+ return object;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+typedef struct {
+ /* Cleared by dispose */
+ GList *link;
+ CockpitDBusJson *dbus_json;
+
+ /* Request data */
+ JsonObject *request;
+
+ /* Owned here */
+ GVariantType *param_type;
+
+ /* Owned by request */
+ const gchar *cookie;
+ const gchar *name;
+ const gchar *interface;
+ const gchar *method;
+ const gchar *path;
+ const gchar *type;
+ const gchar *flags;
+ gint timeout;
+ JsonNode *args;
+} CallData;
+
+static CockpitDBusPeer *
+ensure_peer (CockpitDBusJson *self,
+ const gchar *name);
+
+static void
+send_dbus_error (CockpitDBusJson *self,
+ CallData *call,
+ GError *error)
+{
+ JsonObject *object;
+
+ if (!call->cookie)
+ {
+ g_debug ("%s: dropping error without cookie: %s", self->logname, error->message);
+ return;
+ }
+
+ g_debug ("%s: failed %s", self->logname, call->method);
+
+ object = build_json_error (error);
+ json_object_set_string_member (object, "id", call->cookie);
+ send_json_object (self, object);
+ json_object_unref (object);
+}
+
+typedef struct {
+ CockpitDBusJson *dbus_json;
+ JsonObject *message;
+} WaitData;
+
+static void
+on_wait_complete (CockpitDBusCache *cache,
+ gpointer user_data)
+{
+ WaitData *wd = user_data;
+ CockpitDBusJson *self = wd->dbus_json;
+
+ if (!g_cancellable_is_cancelled (self->cancellable))
+ send_json_object (self, wd->message);
+
+ g_object_unref (wd->dbus_json);
+ json_object_unref (wd->message);
+ g_slice_free (WaitData, wd);
+}
+
+static void
+send_with_barrier (CockpitDBusJson *self,
+ CockpitDBusPeer *peer,
+ JsonObject *message)
+{
+ WaitData *wd = g_slice_new (WaitData);
+ wd->dbus_json = g_object_ref (self);
+ wd->message = json_object_ref (message);
+ cockpit_dbus_cache_barrier (peer->cache, on_wait_complete, wd);
+}
+
+static void
+send_dbus_reply (CockpitDBusJson *self,
+ CallData *call,
+ GDBusMessage *message)
+{
+ CockpitDBusPeer *peer;
+ VariantContext context = { NULL };
+ GVariant *scrape = NULL;
+ JsonObject *object;
+ GString *flags;
+
+ g_return_if_fail (call->cookie != NULL);
+
+ JsonArray *reply;
+ gchar *type = NULL;
+
+ object = json_object_new ();
+ reply = json_array_new ();
+ if (g_dbus_message_get_message_type (message) == G_DBUS_MESSAGE_TYPE_ERROR)
+ {
+ g_debug ("%s: errorc for %s", self->logname, call->method);
+ json_array_add_string_element (reply, g_dbus_message_get_error_name (message));
+ json_object_set_array_member (object, "error", reply);
+ }
+ else
+ {
+ g_debug ("%s: reply for %s", self->logname, call->method);
+ json_object_set_array_member (object, "reply", reply);
+ scrape = g_dbus_message_get_body (message);
+ }
+
+ context.fdlist = g_dbus_message_get_unix_fd_list (message);
+ if (context.fdlist)
+ {
+ if (!self->fd_channel_ids)
+ self->fd_channel_ids = g_queue_new ();
+ context.fdids = self->fd_channel_ids;
+ }
+
+ json_array_add_element (reply, build_json_body (g_dbus_message_get_body (message), &context,
+ call->type != NULL ? &type : NULL));
+
+ if (type)
+ {
+ json_object_set_string_member (object, "type", type);
+ g_free (type);
+ }
+
+ json_object_set_string_member (object, "id", call->cookie);
+
+ if (call->flags)
+ {
+ flags = g_string_new ("");
+ if (g_dbus_message_get_byte_order (message) == G_DBUS_MESSAGE_BYTE_ORDER_BIG_ENDIAN)
+ g_string_append_c (flags, '>');
+ else
+ g_string_append_c (flags, '<');
+ json_object_set_string_member (object, "flags", flags->str);
+ g_string_free (flags, TRUE);
+ }
+
+ peer = ensure_peer (self, call->name);
+ cockpit_dbus_cache_poke (peer->cache, call->path, call->interface);
+ if (scrape)
+ cockpit_dbus_cache_scrape (peer->cache, scrape);
+ send_with_barrier (self, peer, object);
+
+ json_object_unref (object);
+}
+
+static GVariantType *
+calculate_method_param_type (GDBusInterfaceInfo *info,
+ const gchar *iface,
+ const gchar *method,
+ GError **error)
+{
+ const GVariantType *arg_types[256];
+ GDBusMethodInfo *method_info = NULL;
+ guint n;
+
+ if (info)
+ method_info = g_dbus_interface_info_lookup_method (info, method);
+ if (method_info == NULL)
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD,
+ "Introspection data for method %s %s not available", iface, method);
+ return NULL;
+ }
+ else if (method_info->in_args)
+ {
+ for (n = 0; method_info->in_args[n] != NULL; n++)
+ {
+ /* DBus places a hard limit of 255 on signature length.
+ * therefore number of args must be less than 256.
+ */
+ if (n >= G_N_ELEMENTS (arg_types))
+ return NULL;
+
+ arg_types[n] = G_VARIANT_TYPE (method_info->in_args[n]->signature);
+
+ if G_UNLIKELY (arg_types[n] == NULL)
+ return NULL;
+ }
+ }
+ else
+ {
+ n = 0;
+ }
+ return g_variant_type_new_tuple (arg_types, n);
+}
+
+static void
+call_data_free (CallData *call)
+{
+ if (call->dbus_json)
+ call->dbus_json->active_calls = g_list_delete_link (call->dbus_json->active_calls, call->link);
+ if (call->request)
+ json_object_unref (call->request);
+ if (call->param_type)
+ g_variant_type_free (call->param_type);
+ g_slice_free (CallData, call);
+}
+
+static void
+on_send_message_reply (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CallData *call = user_data;
+ GError *error = NULL;
+ GDBusMessage *message;
+
+ message = g_dbus_connection_send_message_with_reply_finish (G_DBUS_CONNECTION (source),
+ result, &error);
+ if (call->dbus_json)
+ {
+ if (error)
+ {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT))
+ {
+ g_clear_error (&error);
+ g_set_error (&error, G_DBUS_ERROR, G_DBUS_ERROR_TIMEOUT,
+ "method call %s timed out", call->method);
+ }
+
+ send_dbus_error (call->dbus_json, call, error);
+ }
+ else
+ {
+ send_dbus_reply (call->dbus_json, call, message);
+ }
+ }
+
+ g_clear_error (&error);
+ g_clear_object (&message);
+ call_data_free (call);
+}
+
+
+static void
+handle_dbus_call_on_interface (CockpitDBusJson *self,
+ CallData *call)
+{
+ GVariant *parameters = NULL;
+ GError *error = NULL;
+ GDBusMessage *message = NULL;
+
+ g_return_if_fail (call->param_type != NULL);
+ parameters = parse_json (call->args, call->param_type, &error);
+
+ if (!parameters)
+ goto out;
+
+ g_debug ("%s: invoking %s %s at %s", self->logname, call->interface, call->method, call->path);
+
+ message = g_dbus_message_new_method_call (call->name,
+ call->path,
+ call->interface,
+ call->method);
+
+ /* When no flags or interactive flags not set */
+ if (!call->flags || strchr (call->flags, 'i'))
+ g_dbus_message_set_flags (message, G_DBUS_MESSAGE_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION);
+
+ g_dbus_message_set_body (message, parameters);
+ parameters = NULL;
+
+ g_dbus_connection_send_message_with_reply (self->connection,
+ message,
+ G_DBUS_SEND_MESSAGE_FLAGS_NONE,
+ call->timeout,
+ NULL, /* serial */
+ self->cancellable,
+ call->cookie ? on_send_message_reply : NULL,
+ call->cookie ? call : NULL);
+ if (call->cookie)
+ call = NULL; /* ownership assumed above */
+
+out:
+ if (error)
+ {
+ if (call)
+ send_dbus_error (self, call, error);
+ g_error_free (error);
+ }
+ if (call)
+ call_data_free (call);
+ if (message)
+ g_object_unref (message);
+}
+
+static void
+on_introspect_ready (CockpitDBusCache *cache,
+ GDBusInterfaceInfo *iface,
+ gpointer user_data)
+{
+ CallData *call = user_data;
+ CockpitDBusJson *self = call->dbus_json;
+ GError *error = NULL;
+
+ /* Cancelled? */
+ if (!call->dbus_json)
+ {
+ call_data_free (call);
+ return;
+ }
+
+ call->param_type = calculate_method_param_type (iface, call->interface,
+ call->method, &error);
+
+ if (error)
+ {
+ send_dbus_error (self, call, error);
+ g_error_free (error);
+ call_data_free (call);
+ }
+ else
+ {
+ handle_dbus_call_on_interface (self, call);
+ }
+}
+
+static const gchar *
+array_string_element (JsonArray *array,
+ guint i)
+{
+ JsonNode *node;
+
+ node = json_array_get_element (array, i);
+ if (node && JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) == G_TYPE_STRING)
+ return json_node_get_string (node);
+ return NULL;
+}
+
+static gboolean
+parse_json_method (CockpitDBusJson *self,
+ JsonNode *node,
+ const gchar *description,
+ const gchar **path,
+ const gchar **interface,
+ const gchar **method,
+ JsonNode **args)
+{
+ JsonArray *array;
+
+ g_assert (description != NULL);
+ g_assert (path != NULL);
+ g_assert (interface != NULL);
+ g_assert (method != NULL);
+ g_assert (args != NULL);
+
+ if (!JSON_NODE_HOLDS_ARRAY (node))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "incorrect '%s' field in dbus command", description);
+ return FALSE;
+ }
+
+ array = json_node_get_array (node);
+ *path = array_string_element (array, 0);
+ *interface = array_string_element (array, 1);
+ *method = array_string_element (array, 2);
+
+ *args = json_array_get_element (array, 3);
+ if (!*args || !JSON_NODE_HOLDS_ARRAY (*args))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "arguments field is invalid in dbus \"%s\"", description);
+ }
+ else if (!*path || !g_variant_is_object_path (*path))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "object path is invalid in dbus \"%s\": %s", description, *path);
+ }
+ else if (!*interface || !g_dbus_is_interface_name (*interface))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "interface name is invalid in dbus \"%s\": %s", description, *interface);
+ }
+ else if (!*method || !g_dbus_is_member_name (*method))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "member name is invalid in dbus \"%s\": %s", description, *method);
+ }
+ else
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+handle_dbus_call (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ CockpitDBusPeer *peer = NULL;
+ CallData *call;
+ JsonNode *node;
+ gchar *string;
+ gint64 timeout;
+
+ node = json_object_get_member (object, "call");
+ g_return_if_fail (node != NULL);
+ call = g_slice_new0 (CallData);
+
+ if (!parse_json_method (self, node, "call", &call->path, &call->interface, &call->method, &call->args))
+ {
+ /* fall through to call invalid */
+ }
+ else if (!cockpit_json_get_string (object, "name", self->default_name, &call->name))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the \"name\" field is invalid in dbus call");
+ }
+ else if (self->bus_type != G_BUS_TYPE_NONE && call->name == NULL)
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the \"name\" field is missing in dbus call");
+ }
+ else if (call->name != NULL && !g_dbus_is_name (call->name))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the \"name\" field in dbus call is not a valid bus name: %s", call->name);
+ }
+ else if (!cockpit_json_get_string (object, "id", NULL, &call->cookie))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the \"id\" field is invalid in call");
+ }
+ else if (!cockpit_json_get_string (object, "type", NULL, &call->type))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the \"type\" field is invalid in call");
+ }
+ else if (call->type && !g_variant_is_signature (call->type))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the \"type\" signature is not valid in dbus call: %s", call->type);
+ }
+ else if (!cockpit_json_get_string (object, "flags", NULL, &call->flags))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the \"flags\" field is invalid in dbus call");
+ }
+ else if (!cockpit_json_get_int(object, "timeout", G_MAXINT, &timeout) ||
+ timeout <= 0 || timeout > G_MAXINT)
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the \"timeout\" field is invalid in dbus call");
+ }
+ else
+ {
+ if (call->type)
+ {
+ string = g_strdup_printf ("(%s)", call->type);
+ call->param_type = g_variant_type_new (string);
+ g_free (string);
+ }
+
+ /* No arguments or zero arguments, can make call without introspecting */
+ if (!call->param_type)
+ {
+ if (json_array_get_length (json_node_get_array (call->args)) == 0)
+ call->param_type = g_variant_type_new ("()");
+ }
+
+ call->dbus_json = self;
+ call->request = json_object_ref (object);
+ self->active_calls = g_list_prepend (self->active_calls, call);
+ call->link = g_list_find (self->active_calls, call);
+ call->timeout = timeout;
+
+ if (call->param_type)
+ {
+ /* Frees call data when done */
+ handle_dbus_call_on_interface (self, call);
+ }
+ else
+ {
+ peer = ensure_peer (self, call->name);
+ cockpit_dbus_cache_introspect (peer->cache, call->path,
+ call->interface, on_introspect_ready, call);
+ }
+
+ /* Start processing call */
+ return;
+ }
+
+ /* call was invalid */
+ call_data_free (call);
+}
+
+static GVariantType *
+calculate_signal_param_type (CockpitDBusJson *self,
+ const gchar *iface,
+ const gchar *signal)
+{
+ GDBusInterfaceInfo *info;
+ const GVariantType *arg_types[256];
+ GDBusSignalInfo *signal_info = NULL;
+ guint n;
+
+ info = g_hash_table_lookup (self->interface_info, iface);
+ if (info)
+ signal_info = g_dbus_interface_info_lookup_signal (info, signal);
+ if (signal_info == NULL)
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "signal argument types for signal %s %s unknown", iface, signal);
+ return NULL;
+ }
+ else if (signal_info->args)
+ {
+ for (n = 0; signal_info->args[n] != NULL; n++)
+ {
+ /* DBus places a hard limit of 255 on signature length.
+ * therefore number of args must be less than 256.
+ */
+ if (n >= G_N_ELEMENTS (arg_types))
+ return NULL;
+
+ arg_types[n] = G_VARIANT_TYPE (signal_info->args[n]->signature);
+
+ if G_UNLIKELY (arg_types[n] == NULL)
+ return NULL;
+ }
+ }
+ else
+ {
+ n = 0;
+ }
+ return g_variant_type_new_tuple (arg_types, n);
+}
+
+static void
+handle_dbus_signal_on_interface (CockpitDBusJson *self,
+ GVariantType *param_type,
+ const gchar *destination,
+ const gchar *path,
+ const gchar *interface,
+ const gchar *signal,
+ JsonNode *args)
+{
+ CockpitChannel *channel;
+ GVariant *parameters = NULL;
+ GDBusMessage *message = NULL;
+ GError *error = NULL;
+
+ parameters = parse_json (args, param_type, &error);
+ if (parameters)
+ {
+ g_debug ("%s: signal %s %s at %s", self->logname, interface, signal, path);
+
+ message = g_dbus_message_new_signal (path, interface, signal);
+ g_dbus_message_set_body (message, parameters);
+ parameters = NULL;
+
+ if (destination)
+ g_dbus_message_set_destination (message, destination);
+
+ g_dbus_connection_send_message (self->connection, message,
+ G_DBUS_SEND_MESSAGE_FLAGS_NONE,
+ NULL, &error);
+ }
+
+ if (error)
+ {
+ channel = COCKPIT_CHANNEL (self);
+ if (error->code == G_IO_ERROR_INVALID_ARGUMENT ||
+ error->code == G_DBUS_ERROR_INVALID_ARGS)
+ {
+ cockpit_channel_fail (channel, "protocol-error", "%s", error->message);
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "internal-error", "%s", error->message);
+ }
+ g_error_free (error);
+ }
+
+ if (message)
+ g_object_unref (message);
+}
+
+static void
+handle_dbus_signal (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ GVariantType *param_type = NULL;
+ const gchar *interface;
+ const gchar *destination;
+ const gchar *path;
+ const gchar *type;
+ const gchar *flags;
+ const gchar *signal;
+ JsonNode *node;
+ JsonNode *args;
+ gchar *string;
+
+ node = json_object_get_member (object, "signal");
+ g_return_if_fail (node != NULL);
+
+ if (!parse_json_method (self, node, "signal", &path, &interface, &signal, &args))
+ {
+ /* fall through to call invalid */
+ }
+ else if (!cockpit_json_get_string (object, "name", NULL, &destination))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the 'name' field is invalid in signal");
+ }
+ else if (destination != NULL && !g_dbus_is_name (destination))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the 'name' field is not a valid bus name: %s", destination);
+ }
+ else if (!cockpit_json_get_string (object, "type", NULL, &type))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the 'type' field is invalid in dbus signal");
+ }
+ else if (type && !g_variant_is_signature (type))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "type signature is not valid in dbus signal: %s", type);
+ }
+ else if (!cockpit_json_get_string (object, "flags", NULL, &flags))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "protocol-error",
+ "the 'flags' field is invalid in call");
+ }
+ else
+ {
+ if (type)
+ {
+ string = g_strdup_printf ("(%s)", type);
+ param_type = g_variant_type_new (string);
+ g_free (string);
+ }
+ else
+ {
+ param_type = calculate_signal_param_type (self, interface, signal);
+ }
+
+ if (param_type)
+ {
+ handle_dbus_signal_on_interface (self, param_type, destination,
+ path, interface, signal, args);
+ }
+ }
+
+ g_variant_type_free (param_type);
+}
+
+static void
+on_add_match_ready (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CockpitDBusJson *self = COCKPIT_DBUS_JSON (user_data);
+ GError *error = NULL;
+ GVariant *retval;
+
+ retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
+ if (error)
+ {
+ if (!g_cancellable_is_cancelled (self->cancellable) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error",
+ "couldn't add match to bus: %s", error->message);
+ }
+ g_error_free (error);
+ }
+ if (retval)
+ g_variant_unref (retval);
+ g_object_unref (self);
+}
+
+static gboolean
+parse_json_rule (CockpitDBusJson *self,
+ JsonNode *node,
+ const gchar **name,
+ const gchar **path,
+ const gchar **path_namespace,
+ const gchar **interface,
+ const gchar **signal,
+ const gchar **arg0)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ JsonObject *object;
+ gboolean valid;
+ GList *names, *l;
+
+ if (!JSON_NODE_HOLDS_OBJECT (node))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "incorrect match field in dbus command");
+ return FALSE;
+ }
+
+ object = json_node_get_object (node);
+
+ if (name)
+ *name = NULL;
+ if (path)
+ *path = NULL;
+ if (path_namespace)
+ *path_namespace = NULL;
+ if (signal)
+ *signal = NULL;
+ if (interface)
+ *interface = NULL;
+ if (arg0)
+ *arg0 = NULL;
+
+ names = json_object_get_members (object);
+ for (l = names; l != NULL; l = g_list_next (l))
+ {
+ valid = FALSE;
+ if (name && g_str_equal (l->data, "name"))
+ valid = cockpit_json_get_string (object, "name", NULL, name);
+ if (interface && g_str_equal (l->data, "interface"))
+ valid = cockpit_json_get_string (object, "interface", NULL, interface);
+ else if (signal && g_str_equal (l->data, "member"))
+ valid = cockpit_json_get_string (object, "member", NULL, signal);
+ else if (path && g_str_equal (l->data, "path"))
+ valid = cockpit_json_get_string (object, "path", NULL, path);
+ else if (path_namespace && g_str_equal (l->data, "path_namespace"))
+ valid = cockpit_json_get_string (object, "path_namespace", NULL, path_namespace);
+ else if (arg0 && g_str_equal (l->data, "arg0"))
+ valid = cockpit_json_get_string (object, "arg0", NULL, arg0);
+
+ if (!valid)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid or unsupported match field: %s", (gchar *)l->data);
+ g_list_free (names);
+ return FALSE;
+ }
+ }
+ g_list_free (names);
+
+ valid = FALSE;
+ if (name && *name && !g_dbus_is_name (*name))
+ cockpit_channel_fail (channel, "protocol-error", "match \"name\" is not valid: %s", *name);
+ else if (path && *path && !g_variant_is_object_path (*path))
+ cockpit_channel_fail (channel, "protocol-error", "match path is not valid: %s", *path);
+ else if (path_namespace && *path_namespace && !g_variant_is_object_path (*path_namespace))
+ cockpit_channel_fail (channel, "protocol-error", "match path_namespace is not valid: %s", *path_namespace);
+ else if (interface && *interface && !g_dbus_is_interface_name (*interface))
+ cockpit_channel_fail (channel, "protocol-error", "match interface is not valid: %s", *interface);
+ else if (signal && *signal && !g_dbus_is_member_name (*signal))
+ cockpit_channel_fail (channel, "protocol-error", "match \"member\" is not valid: %s", *signal);
+ else if (arg0 && *arg0 && strchr (*arg0, '\'') != NULL)
+ cockpit_channel_fail (channel, "protocol-error", "match arg0 is not valid: %s", *arg0);
+ else if (path && path_namespace && *path && *path_namespace)
+ cockpit_channel_fail (channel, "protocol-error", "match cannot specify both path and path_namespace");
+ else
+ valid = TRUE;
+
+ if (name)
+ {
+ if (!*name)
+ *name = self->default_name;
+ if (!*name && self->bus_type != G_BUS_TYPE_NONE)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: no \"name\" specified in match", self->logname);
+ valid = FALSE;
+ }
+ }
+
+ return valid;
+}
+
+static gchar *
+build_dbus_match (CockpitDBusJson *self,
+ const gchar *name,
+ const gchar *path,
+ const gchar *path_namespace,
+ const gchar *interface,
+ const gchar *signal,
+ const gchar *arg0)
+{
+ GString *string = g_string_new ("type='signal'");
+ if (!name)
+ name = self->default_name;
+ if (name)
+ g_string_append_printf (string, ",sender='%s'", name);
+ if (path)
+ g_string_append_printf (string, ",path='%s'", path);
+ if (path_namespace && !g_str_equal (path_namespace, "/"))
+ g_string_append_printf (string, ",path_namespace='%s'", path_namespace);
+ if (interface)
+ g_string_append_printf (string, ",interface='%s'", interface);
+ if (signal)
+ g_string_append_printf (string, ",member='%s'", signal);
+ if (arg0)
+ g_string_append_printf (string, ",arg0='%s'", arg0);
+ return g_string_free (string, FALSE);
+}
+
+static void
+handle_dbus_add_match (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ CockpitDBusPeer *peer = NULL;
+ JsonNode *node;
+ const gchar *name;
+ const gchar *path;
+ const gchar *path_namespace;
+ const gchar *interface;
+ const gchar *signal;
+ const gchar *arg0;
+ gchar *match;
+
+ node = json_object_get_member (object, "add-match");
+ g_return_if_fail (node != NULL);
+
+ if (!parse_json_rule (self, node, &name, &path, &path_namespace, &interface, &signal, &arg0))
+ return;
+
+ peer = ensure_peer (self, name);
+ if (cockpit_dbus_rules_add (peer->rules,
+ path ? path : path_namespace,
+ path_namespace ? TRUE : FALSE,
+ interface, signal, arg0))
+ {
+ if (peer->name)
+ {
+ match = build_dbus_match (self, name, path, path_namespace, interface, signal, arg0);
+ g_dbus_connection_call (self->connection,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "AddMatch",
+ g_variant_new ("(s)", match),
+ NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1,
+ self->cancellable,
+ on_add_match_ready,
+ g_object_ref (self));
+ g_free (match);
+ }
+ }
+}
+
+static void
+on_remove_match_ready (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CockpitDBusJson *self = COCKPIT_DBUS_JSON (user_data);
+ GError *error = NULL;
+ GVariant *retval;
+
+ retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
+ if (error)
+ {
+ if (!g_cancellable_is_cancelled (self->cancellable) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED))
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error",
+ "couldn't remove match from bus: %s", error->message);
+ }
+ g_error_free (error);
+ }
+ if (retval)
+ g_variant_unref (retval);
+ g_object_unref (self);
+}
+
+static void
+handle_dbus_remove_match (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ CockpitDBusPeer *peer;
+ JsonNode *node;
+ const gchar *name;
+ const gchar *path;
+ const gchar *path_namespace;
+ const gchar *interface;
+ const gchar *signal;
+ const gchar *arg0;
+ gchar *match;
+
+ node = json_object_get_member (object, "remove-match");
+ g_return_if_fail (node != NULL);
+
+ if (!parse_json_rule (self, node, &name, &path, &path_namespace, &interface, &signal, &arg0))
+ return;
+
+ peer = ensure_peer (self, name);
+ if (cockpit_dbus_rules_remove (peer->rules,
+ path ? path : path_namespace,
+ path_namespace ? TRUE : FALSE,
+ interface, signal, arg0))
+ {
+ if (peer->name)
+ {
+ match = build_dbus_match (self, name, path, path_namespace, interface, signal, arg0);
+ g_dbus_connection_call (self->connection,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "RemoveMatch",
+ g_variant_new ("(s)", match),
+ NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1,
+ NULL, /* don't cancel removes */
+ on_remove_match_ready,
+ g_object_ref (self));
+ g_free (match);
+ }
+ }
+}
+
+static void
+maybe_include_name (CockpitDBusJson *self,
+ JsonObject *object,
+ const gchar *name)
+{
+ if (name && g_strcmp0 (name, self->default_name) != 0)
+ json_object_set_string_member (object, "name", name);
+}
+
+static void
+on_cache_meta (CockpitDBusCache *cache,
+ GDBusInterfaceInfo *iface,
+ gpointer user_data)
+{
+ CockpitDBusPeer *peer = user_data;
+ JsonObject *interface;
+ JsonObject *meta;
+ JsonObject *message;
+
+ interface = cockpit_dbus_meta_build (iface);
+
+ meta = json_object_new ();
+ json_object_set_object_member (meta, iface->name, interface);
+
+ message = json_object_new ();
+ json_object_set_object_member (message, "meta", meta);
+
+ maybe_include_name (peer->dbus_json, message, peer->name);
+ send_json_object (peer->dbus_json, message);
+ json_object_unref (message);
+}
+
+static void
+handle_dbus_meta (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ GDBusInterfaceInfo *iface;
+ JsonObject *interface;
+ GError *error = NULL;
+ JsonObject *meta;
+ GList *names, *l;
+ JsonNode *node;
+
+ node = json_object_get_member (object, "meta");
+ g_return_if_fail (node != NULL);
+
+ if (!JSON_NODE_HOLDS_OBJECT (node))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "incorrect \"meta\" field in dbus command");
+ return;
+ }
+
+ meta = json_node_get_object (node);
+ names = json_object_get_members (meta);
+ for (l = names; l != NULL; l = g_list_next (l))
+ {
+ if (!cockpit_json_get_object (meta, l->data, NULL, &interface))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid interface in dbus \"meta\" command");
+ break;
+ }
+
+ iface = cockpit_dbus_meta_parse (l->data, interface, &error);
+ if (iface)
+ {
+ cockpit_dbus_interface_info_push (self->interface_info, iface);
+ g_dbus_interface_info_unref (iface);
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "protocol-error", "%s", error->message);
+ g_error_free (error);
+ break;
+ }
+ }
+
+ g_list_free (names);
+}
+
+static JsonObject *
+build_json_update (GHashTable *paths)
+{
+ GHashTableIter i, j, k;
+ GHashTable *interfaces;
+ GHashTable *properties;
+ const gchar *interface;
+ const gchar *property;
+ const gchar *path;
+ JsonObject *notify;
+ JsonObject *object;
+ JsonObject *iface;
+ GVariant *value;
+
+ notify = json_object_new ();
+
+ g_hash_table_iter_init (&i, paths);
+ while (g_hash_table_iter_next (&i, (gpointer *)&path, (gpointer *)&interfaces))
+ {
+ object = json_object_new ();
+
+ g_hash_table_iter_init (&j, interfaces);
+ while (g_hash_table_iter_next (&j, (gpointer *)&interface, (gpointer *)&properties))
+ {
+ if (properties == NULL)
+ {
+ json_object_set_null_member (object, interface);
+ }
+ else
+ {
+ iface = json_object_new ();
+
+ g_hash_table_iter_init (&k, properties);
+ while (g_hash_table_iter_next (&k, (gpointer *)&property, (gpointer *)&value))
+ json_object_set_member (iface, property, build_json (value, NULL));
+
+ json_object_set_object_member (object, interface, iface);
+ }
+ }
+
+ json_object_set_object_member (notify, path, object);
+ }
+
+ return notify;
+}
+
+static void
+on_cache_update (CockpitDBusCache *cache,
+ GHashTable *update,
+ gpointer user_data)
+{
+ CockpitDBusPeer *peer = user_data;
+ JsonObject *object = json_object_new ();
+ maybe_include_name (peer->dbus_json, object, peer->name);
+ json_object_set_object_member (object, "notify", build_json_update (update));
+ send_json_object (peer->dbus_json, object);
+ json_object_unref (object);
+}
+
+static void
+handle_dbus_watch (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ CockpitDBusPeer *peer = NULL;
+ const gchar *name;
+ const gchar *path;
+ const gchar *path_namespace;
+ const gchar *interface;
+ gboolean is_namespace = FALSE;
+ const gchar *cookie;
+ JsonNode *node;
+
+ node = json_object_get_member (object, "watch");
+ g_return_if_fail (node != NULL);
+
+ if (!parse_json_rule (self, node, &name, &path, &path_namespace, &interface, NULL, NULL))
+ return;
+
+ if (path_namespace)
+ {
+ path = path_namespace;
+ is_namespace = TRUE;
+ }
+
+ peer = ensure_peer (self, name);
+ cockpit_dbus_cache_watch (peer->cache, path, is_namespace, interface);
+
+ if (!path)
+ path = "/";
+
+ /* Send back a reply when this has completed */
+ if (cockpit_json_get_string (object, "id", NULL, &cookie))
+ {
+ object = json_object_new ();
+ json_object_set_array_member (object, "reply", json_array_new ());
+ json_object_set_string_member (object, "id", cookie);
+ cockpit_dbus_cache_poke (peer->cache, path, NULL);
+ send_with_barrier (self, peer, object);
+ json_object_unref (object);
+ }
+}
+
+static void
+handle_dbus_unwatch (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ CockpitDBusPeer *peer;
+ const gchar *name;
+ const gchar *path;
+ const gchar *path_namespace;
+ const gchar *interface;
+ gboolean is_namespace = FALSE;
+ JsonNode *node;
+
+ node = json_object_get_member (object, "unwatch");
+ g_return_if_fail (node != NULL);
+
+ if (!parse_json_rule (self, node, &name, &path, &path_namespace, &interface, NULL, NULL))
+ return;
+
+ if (path_namespace)
+ {
+ path = path_namespace;
+ is_namespace = TRUE;
+ }
+
+ peer = ensure_peer (self, name);
+ cockpit_dbus_cache_unwatch (peer->cache, path, is_namespace, interface);
+}
+
+static GVariantType *
+calculate_reply_param_type (CockpitChannel *channel,
+ GDBusMethodInvocation *invocation)
+{
+ const GVariantType *arg_types[256];
+ const GDBusMethodInfo *method_info;
+ guint n;
+
+ method_info = g_dbus_method_invocation_get_method_info (invocation);
+ if (method_info == NULL)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "reply argument types for signal %s %s unknown",
+ g_dbus_method_invocation_get_interface_name (invocation),
+ g_dbus_method_invocation_get_method_name (invocation));
+ return NULL;
+ }
+ else if (method_info->out_args)
+ {
+ for (n = 0; method_info->out_args[n] != NULL; n++)
+ {
+ /* DBus places a hard limit of 255 on signature length.
+ * therefore number of args must be less than 256.
+ */
+ if (n >= G_N_ELEMENTS (arg_types))
+ g_return_val_if_reached (NULL);
+
+ arg_types[n] = G_VARIANT_TYPE (method_info->out_args[n]->signature);
+
+ if G_UNLIKELY (arg_types[n] == NULL)
+ g_return_val_if_reached (NULL);
+ }
+ }
+ else
+ {
+ n = 0;
+ }
+ return g_variant_type_new_tuple (arg_types, n);
+}
+
+static void
+handle_dbus_reply (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ gpointer invocation = NULL;
+ GVariantType *param_type;
+ const gchar *cookie;
+ GVariant *parameters;
+ GError *error = NULL;
+ JsonNode *args;
+ JsonNode *node;
+
+ node = json_object_get_member (object, "reply");
+ g_return_if_fail (node != NULL);
+
+ if (!JSON_NODE_HOLDS_ARRAY (node))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "incorrect \"reply\" field in dbus command");
+ }
+ else if (json_array_get_length (json_node_get_array (node)) < 1)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "missing values in dbus method \"reply\"");
+ }
+ else if (!cockpit_json_get_string (object, "id", NULL, &cookie))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"id\" in dbus method reply");
+ }
+ else if (cookie == NULL)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "missing \"id\" in dbus method reply");
+ }
+ else if (!self->invocations ||
+ !g_hash_table_lookup_extended (self->invocations, cookie, NULL, &invocation))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "unknown \"id\" in dbus method reply: %s", cookie);
+ }
+ else
+ {
+ param_type = calculate_reply_param_type (channel, invocation);
+ args = json_array_get_element (json_node_get_array (node), 0);
+ parameters = parse_json (args, param_type, &error);
+ g_variant_type_free (param_type);
+
+ if (error)
+ {
+ /* The invocation will be cleaned up by our dispose function */
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid argument in dbus method reply: %s", error->message);
+ g_error_free (error);
+ }
+ else
+ {
+ /* Reference to parameters is consumed */
+ g_dbus_method_invocation_return_value (invocation, parameters);
+ g_hash_table_remove (self->invocations, cookie);
+ }
+ }
+}
+
+static gboolean
+parse_json_error (CockpitChannel *channel,
+ JsonNode *node,
+ const gchar **error_name,
+ const gchar **error_message)
+{
+ JsonArray *array;
+ JsonNode *nested;
+ JsonArray *params;
+ guint length;
+
+ g_assert (error_name != NULL);
+ g_assert (error_message != NULL);
+
+ *error_name = NULL;
+ *error_message = NULL;
+
+ if (!JSON_NODE_HOLDS_ARRAY (node))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"error\" field in dbus command");
+ return FALSE;
+ }
+
+ array = json_node_get_array (node);
+ length = json_array_get_length (array);
+ if (length < 1)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "missing \"error\" error name in dbus command");
+ return FALSE;
+ }
+
+ *error_name = array_string_element (array, 0);
+ if (!*error_name)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"error\" error name in dbus command");
+ return FALSE;
+ }
+
+ if (length >= 2)
+ {
+ nested = json_array_get_element (array, 1);
+ if (!JSON_NODE_HOLDS_ARRAY (nested))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"error\" parameters in dbus command");
+ return FALSE;
+ }
+
+ params = json_node_get_array (nested);
+ if (json_array_get_length (params) > 0)
+ *error_message = array_string_element (params, 0);
+ }
+
+ if (!*error_message)
+ *error_message = *error_name;
+ return TRUE;
+}
+
+static void
+handle_dbus_error (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ const gchar *error_message = NULL;
+ const gchar *error_name = NULL;
+ const gchar *cookie = NULL;
+ gpointer invocation = NULL;
+ JsonNode *node;
+
+ node = json_object_get_member (object, "error");
+ g_return_if_fail (node != NULL);
+
+ if (!JSON_NODE_HOLDS_ARRAY (node))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "incorrect \"error\" field in dbus command");
+ }
+ else if (!parse_json_error (channel, node, &error_name, &error_message))
+ {
+ /* Fail */
+ }
+ else if (!cockpit_json_get_string (object, "id", NULL, &cookie))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"id\" in dbus method error");
+ }
+ else if (cookie == NULL)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "missing \"id\" in dbus method error");
+ }
+ else if (!self->invocations ||
+ !g_hash_table_lookup_extended (self->invocations, cookie, NULL, &invocation))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "unknown \"id\" in dbus method error: %s", cookie);
+ }
+ else
+ {
+ g_dbus_method_invocation_return_dbus_error (invocation, error_name, error_message);
+ g_hash_table_remove (self->invocations, cookie);
+ }
+}
+
+static void
+on_method_invocation (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ CockpitDBusJson *self = user_data;
+ GDBusMessageFlags flags;
+ GDBusMessage *message;
+ gchar *cookie = NULL;
+ JsonObject *object;
+ JsonArray *call;
+ VariantContext context = { NULL };
+
+ message = g_dbus_method_invocation_get_message (invocation);
+ flags = g_dbus_message_get_flags (message);
+
+ object = json_object_new ();
+ call = json_array_new ();
+ json_array_add_string_element (call, object_path);
+ json_array_add_string_element (call, interface_name);
+ json_array_add_string_element (call, method_name);
+ json_array_add_element (call, build_json (parameters, &context));
+ json_object_set_array_member (object, "call", call);
+
+ if (!(flags & G_DBUS_MESSAGE_FLAGS_NO_REPLY_EXPECTED))
+ {
+ g_assert (self->invocations != NULL);
+ cookie = g_strdup_printf ("%d", self->last_invocation++);
+ g_hash_table_insert (self->invocations, cookie, g_object_ref (invocation));
+ json_object_set_string_member (object, "id", cookie);
+ }
+
+ if (sender)
+ json_object_set_string_member (object, "name", sender);
+
+ send_json_object (self, object);
+ json_object_unref (object);
+}
+
+static gboolean
+parse_json_publish (CockpitChannel *channel,
+ JsonNode *node,
+ const gchar **object_path,
+ const gchar **interface_name)
+{
+ JsonArray *array;
+
+ g_assert (object_path != NULL);
+ g_assert (interface_name != NULL);
+
+ *object_path = NULL;
+ *interface_name = NULL;
+
+ if (!JSON_NODE_HOLDS_ARRAY (node))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid publish field in dbus command");
+ return FALSE;
+ }
+
+ array = json_node_get_array (node);
+ *object_path = array_string_element (array, 0);
+ *interface_name = array_string_element (array, 1);
+
+ if (!*object_path && !g_variant_is_object_path (*object_path))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "publish dbus object path is not valid: %s", *object_path);
+ }
+ else if (!*interface_name && !g_dbus_is_interface_name (*interface_name))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "publish dbus interface name is not valid: %s", *interface_name);
+ }
+ else
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+handle_dbus_publish (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ const gchar *interface_name = NULL;
+ const gchar *object_path = NULL;
+ GHashTable *registered = NULL;
+ const gchar *cookie = NULL;
+ GDBusInterfaceInfo *iface;
+ guint registration_id;
+ GError *error = NULL;
+ JsonNode *node;
+ gchar *key;
+
+ static const GDBusInterfaceVTable vtable = {
+ on_method_invocation,
+ NULL, NULL /* Property access handled as invocations */
+ };
+
+ node = json_object_get_member (object, "publish");
+ g_return_if_fail (node != NULL);
+
+ if (!parse_json_publish (channel, node, &object_path, &interface_name))
+ return;
+
+ iface = g_hash_table_lookup (self->interface_info, interface_name);
+ if (!iface)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "no \"meta\" introspection data available for interface: %s", interface_name);
+ return;
+ }
+
+ /* Start up the necessary hash tables for exporting objects */
+ registered = g_object_get_data (G_OBJECT (self->connection), "registered-objects");
+ if (!registered)
+ {
+ registered = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ g_object_set_data_full (G_OBJECT (self->connection), "registered-objects",
+ registered, (GDestroyNotify)g_hash_table_unref);
+ }
+ if (!self->registered)
+ self->registered = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ if (!self->invocations)
+ self->invocations = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+
+ /* If there's an old object registered here, unregister it */
+ key = g_strdup_printf ("%s\n%s", object_path, interface_name);
+ registration_id = GPOINTER_TO_UINT (g_hash_table_lookup (registered, key));
+ if (registration_id)
+ g_dbus_connection_unregister_object (self->connection, registration_id);
+
+ /* Register the object */
+ registration_id = g_dbus_connection_register_object (self->connection, object_path,
+ iface, &vtable, self, NULL, &error);
+
+ if (error)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "could not publish interface %s at %s: %s",
+ interface_name, object_path, error->message);
+ g_error_free (error);
+ g_free (key);
+ return;
+ }
+
+ /* All done mark this so we're able to later unpublish */
+ g_hash_table_insert (registered, g_strdup (key), GUINT_TO_POINTER (registration_id));
+ g_hash_table_insert (self->registered, key, GUINT_TO_POINTER (registration_id));
+
+ /* Caller wants a reply to know when published */
+ if (cockpit_json_get_string (object, "id", NULL, &cookie))
+ {
+ object = json_object_new ();
+ json_object_set_array_member (object, "reply", json_array_new ());
+ json_object_set_string_member (object, "id", cookie);
+ send_json_object (self, object);
+ json_object_unref (object);
+ }
+}
+
+static void
+handle_dbus_unpublish (CockpitDBusJson *self,
+ JsonObject *object)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ const gchar *interface_name = NULL;
+ const gchar *object_path = NULL;
+ GHashTable *registered;
+ JsonNode *node;
+ gchar *key;
+ guint id = 0;
+
+ node = json_object_get_member (object, "unpublish");
+ g_return_if_fail (node != NULL);
+
+ if (!parse_json_publish (channel, node, &object_path, &interface_name))
+ return;
+
+ key = g_strdup_printf ("%s\n%s", object_path, interface_name);
+ if (self->registered)
+ {
+ id = GPOINTER_TO_UINT (g_hash_table_lookup (self->registered, key));
+ g_hash_table_remove (self->registered, key);
+
+ /* A per connection list of all interfaces and objects registered */
+ registered = g_object_get_data (G_OBJECT (self->connection), "registered-objects");
+ g_hash_table_remove (registered, key);
+ }
+ g_free (key);
+
+ if (id != 0)
+ g_dbus_connection_unregister_object (self->connection, id);
+}
+
+
+static void
+cockpit_dbus_json_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ CockpitDBusJson *self = COCKPIT_DBUS_JSON (channel);
+ GError *error = NULL;
+ JsonObject *object = NULL;
+
+ object = cockpit_json_parse_bytes (message, &error);
+ if (!object)
+ {
+ cockpit_channel_fail (channel, "protocol-error", "failed to parse dbus request: %s", error->message);
+ g_clear_error (&error);
+ return;
+ }
+
+ if (json_object_has_member (object, "call"))
+ handle_dbus_call (self, object);
+ else if (json_object_has_member (object, "signal"))
+ handle_dbus_signal (self, object);
+ else if (json_object_has_member (object, "add-match"))
+ handle_dbus_add_match (self, object);
+ else if (json_object_has_member (object, "remove-match"))
+ handle_dbus_remove_match (self, object);
+ else if (json_object_has_member (object, "watch"))
+ handle_dbus_watch (self, object);
+ else if (json_object_has_member (object, "unwatch"))
+ handle_dbus_unwatch (self, object);
+ else if (json_object_has_member (object, "meta"))
+ handle_dbus_meta (self, object);
+ else if (json_object_has_member (object, "error"))
+ handle_dbus_error (self, object);
+ else if (json_object_has_member (object, "reply"))
+ handle_dbus_reply (self, object);
+ else if (json_object_has_member (object, "publish"))
+ handle_dbus_publish (self, object);
+ else if (json_object_has_member (object, "unpublish"))
+ handle_dbus_unpublish (self, object);
+ else
+ {
+ cockpit_channel_fail (channel, "protocol-error", "got unsupported dbus command");
+ }
+
+ json_object_unref (object);
+}
+
+static void
+on_signal_message (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *path,
+ const gchar *interface,
+ const gchar *signal,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ /*
+ * HACK: There is no way we can access the original GDBusMessage. This
+ * means things like flags and byte order are lost here.
+ *
+ * We cannot use a GDBusMessageFilterFunction and use that to subsribe
+ * to signals, because then the ordering guarantees are out the window.
+ */
+
+ CockpitDBusPeer *peer = user_data;
+ const gchar *arg0 = NULL;
+ JsonObject *object;
+
+ /* Unfortunately we also have to recalculate this */
+ if (parameters &&
+ g_variant_is_of_type (parameters, G_VARIANT_TYPE_TUPLE) &&
+ g_variant_n_children (parameters) > 0)
+ {
+ GVariant *item;
+ item = g_variant_get_child_value (parameters, 0);
+ if (g_variant_is_of_type (item, G_VARIANT_TYPE_STRING))
+ arg0 = g_variant_get_string (item, NULL);
+ g_variant_unref (item);
+ }
+
+ if (cockpit_dbus_rules_match (peer->rules, path, interface, signal, arg0))
+ {
+ object = build_json_signal (path, interface, signal, parameters);
+ cockpit_dbus_cache_poke (peer->cache, path, interface);
+ maybe_include_name (peer->dbus_json, object, peer->name);
+ send_with_barrier (peer->dbus_json, peer, object);
+ json_object_unref (object);
+ }
+}
+
+static void
+cockpit_dbus_json_closed (CockpitChannel *channel,
+ const gchar *problem)
+{
+ /* When closed disconnect from everything */
+ g_object_run_dispose (G_OBJECT (channel));
+}
+
+static void
+cockpit_dbus_json_init (CockpitDBusJson *self)
+{
+ self->cancellable = g_cancellable_new ();
+
+ self->peers = g_hash_table_new (g_str_hash, g_str_equal);
+ self->interface_info = cockpit_dbus_interface_info_new ();
+}
+
+static void
+send_owned (CockpitDBusJson *self,
+ const gchar *name,
+ const gchar *owner)
+{
+ JsonObject *object;
+ object = json_object_new ();
+ maybe_include_name (self, object, name);
+ json_object_set_string_member (object, "owner", owner);
+ send_json_object (self, object);
+ json_object_unref (object);
+}
+
+static void
+send_ready (CockpitDBusJson *self)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ const gchar *unique_name = NULL;
+ JsonObject *message;
+
+ message = json_object_new ();
+ unique_name = g_dbus_connection_get_unique_name (self->connection);
+ if (unique_name)
+ json_object_set_string_member (message, "unique-name", unique_name);
+
+ cockpit_channel_ready (channel, message);
+ json_object_unref (message);
+}
+
+static void
+on_name_appeared (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ CockpitDBusJson *self = COCKPIT_DBUS_JSON (user_data);
+
+ g_object_ref (self);
+
+ CockpitDBusPeer *peer = g_hash_table_lookup (self->peers, name);
+ if (peer)
+ cockpit_dbus_cache_set_name_owner (peer->cache, name_owner);
+
+ if (!self->default_appeared)
+ {
+ self->default_appeared = TRUE;
+ send_ready (self);
+ }
+
+ send_owned (self, name, name_owner);
+
+ g_object_unref (self);
+}
+
+static void
+on_name_vanished (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ CockpitDBusJson *self = COCKPIT_DBUS_JSON (user_data);
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+
+ send_owned (self, name, NULL);
+
+ CockpitDBusPeer *peer = g_hash_table_lookup (self->peers, name);
+ if (peer)
+ {
+ // Using the empty string as the name owner means that the cache
+ // will not process any signals until we set a real name owner
+ // again in on_name_appeared.
+ //
+ cockpit_dbus_cache_set_name_owner (peer->cache, "");
+ }
+
+ if (!G_IS_DBUS_CONNECTION (connection) || g_dbus_connection_is_closed (connection))
+ cockpit_channel_close (channel, "disconnected");
+ else if (!self->default_appeared)
+ cockpit_channel_close (channel, "not-found");
+}
+
+static CockpitDBusPeer *
+ensure_peer (CockpitDBusJson *self,
+ const gchar *name)
+{
+ CockpitDBusPeer *peer;
+
+ if (!name)
+ name = self->default_name;
+
+ peer = g_hash_table_lookup (self->peers, name ? name : "");
+ if (!peer)
+ {
+ peer = g_new0 (CockpitDBusPeer, 1);
+ peer->name = g_strdup (name);
+ peer->dbus_json = self;
+ peer->cache = cockpit_dbus_cache_new (self->connection, name, self->logname, self->interface_info);
+ peer->meta_sig = g_signal_connect (peer->cache, "meta", G_CALLBACK (on_cache_meta), peer);
+ peer->update_sig = g_signal_connect (peer->cache, "update", G_CALLBACK (on_cache_update), peer);
+ peer->rules = cockpit_dbus_rules_new ();
+
+ peer->subscribe_id = g_dbus_connection_signal_subscribe (self->connection,
+ name,
+ NULL, /* interface */
+ NULL, /* member */
+ NULL, /* object_path */
+ NULL, /* arg0 */
+ G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
+ on_signal_message, peer, NULL);
+
+ g_hash_table_insert (self->peers, peer->name ? peer->name : "", peer);
+ }
+
+ return peer;
+}
+
+static void
+subscribe_and_cache (CockpitDBusJson *self)
+{
+ g_dbus_connection_set_exit_on_close (self->connection, FALSE);
+ if (self->default_name || self->bus_type == G_BUS_TYPE_NONE)
+ ensure_peer (self, self->default_name);
+}
+
+static void
+process_connection (CockpitDBusJson *self,
+ GError *error)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ GBusNameWatcherFlags flags;
+ if (!self->connection)
+ {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
+ g_cancellable_is_cancelled (self->cancellable))
+ {
+ g_debug ("%s", error ? error->message : "(error is NULL)");
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "internal-error", "%s", error ? error->message : "(error is NULL)");
+ }
+ }
+ else
+ {
+ /* Yup, we don't want this */
+ g_dbus_connection_set_exit_on_close (self->connection, FALSE);
+ if (self->default_name)
+ {
+ flags = G_BUS_NAME_WATCHER_FLAGS_AUTO_START;
+ self->default_watch = g_bus_watch_name_on_connection (self->connection,
+ self->default_name, flags,
+ on_name_appeared,
+ on_name_vanished,
+ self, NULL);
+ self->default_watched = TRUE;
+ subscribe_and_cache (self);
+
+ /* Stop the cache from processing signals until we know its
+ bus name.
+ */
+ CockpitDBusPeer *peer = g_hash_table_lookup (self->peers, self->default_name);
+ if (peer)
+ cockpit_dbus_cache_set_name_owner (peer->cache, "");
+ }
+ else
+ {
+ subscribe_and_cache (self);
+ send_ready (self);
+ }
+ }
+}
+
+static void
+group_connections_weak_notify (gpointer data,
+ GObject *where_the_object_was)
+{
+ const gchar *key = data;
+
+ if (group_connections)
+ {
+ g_hash_table_remove (group_connections, key);
+ if (g_hash_table_size (group_connections) == 0)
+ g_clear_pointer (&group_connections, g_hash_table_unref);
+ }
+}
+
+static void
+on_connection_ready (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CockpitDBusJson *self = COCKPIT_DBUS_JSON (user_data);
+ g_autoptr(GError) error = NULL;
+
+ self->connection = g_dbus_connection_new_for_address_finish (result,
+ &error);
+ process_connection (self, error);
+ g_object_unref (self);
+}
+
+static void
+on_shared_connection_ready (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ gchar *key = user_data;
+ GDBusConnection *connection;
+ g_autoptr(GError) error = NULL;
+ GPtrArray *subscribers;
+
+ connection = g_dbus_connection_new_for_address_finish (result, &error);
+
+ /*
+ * Notify all CockpitDBusJson objects from the same group that were
+ * also waiting for this connection
+ */
+ subscribers = g_hash_table_lookup (pending_group_connections, key);
+ for (guint i = 0; i < subscribers->len; i++)
+ {
+ CockpitDBusJson *channel = g_ptr_array_index (subscribers, i);
+
+ if (connection)
+ channel->connection = g_object_ref (connection);
+
+ process_connection (channel, error);
+ }
+
+ g_hash_table_remove (pending_group_connections, key);
+ if (g_hash_table_size (pending_group_connections) == 0)
+ g_clear_pointer (&pending_group_connections, g_hash_table_unref);
+
+ if (connection)
+ {
+ if (!group_connections)
+ group_connections = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ /*
+ * Ensure that the key is removed when the last CockpitDBusJson
+ * object in its group drops the connection. group_connections
+ * takes ownership of 'key'.
+ */
+ g_object_weak_ref (G_OBJECT (connection), group_connections_weak_notify, key);
+
+ g_hash_table_insert (group_connections, key, connection);
+
+ g_object_unref (connection);
+ }
+ else
+ {
+ g_free (key);
+ }
+}
+
+static void
+cockpit_dbus_json_prepare (CockpitChannel *channel)
+{
+ CockpitDBusJson *self = COCKPIT_DBUS_JSON (channel);
+ JsonObject *options;
+ const gchar *bus;
+ const gchar *address;
+ const gchar *group;
+ gboolean internal = FALSE;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_dbus_json_parent_class)->prepare (channel);
+
+ options = cockpit_channel_get_options (channel);
+ if (!cockpit_json_get_string (options, "bus", NULL, &bus))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"bus\" option in dbus channel");
+ return;
+ }
+ if (!cockpit_json_get_string (options, "address", NULL, &address))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"address\" option in dbus channel");
+ return;
+ }
+ if (!cockpit_json_get_string (options, "group", "default", &group))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"group\" option in dbus channel");
+ return;
+ }
+
+ /*
+ * The default bus is the "user" bus which doesn't exist in many
+ * places yet, so use the session bus for now.
+ */
+ self->bus_type = G_BUS_TYPE_SESSION;
+ if (bus == NULL || g_str_equal (bus, "system"))
+ {
+ self->bus_type = G_BUS_TYPE_SYSTEM;
+ }
+ else if (g_str_equal (bus, "session") ||
+ g_str_equal (bus, "user"))
+ {
+ self->bus_type = G_BUS_TYPE_SESSION;
+ }
+ else if (g_str_equal (bus, "none"))
+ {
+ self->bus_type = G_BUS_TYPE_NONE;
+ if (address == NULL || g_str_equal (address, "internal"))
+ internal = TRUE;
+ }
+ else if (g_str_equal (bus, "internal"))
+ {
+ self->bus_type = G_BUS_TYPE_NONE;
+ internal = TRUE;
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"bus\" option in dbus channel: %s", bus);
+ return;
+ }
+
+ if (!internal && !cockpit_dbus_json_allow_external)
+ {
+ cockpit_channel_close (channel, "not-supported");
+ return;
+ }
+
+ /* An internal peer to peer connection to cockpit-bridge */
+ if (internal)
+ {
+ if (!cockpit_json_get_null (options, "name", NULL))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "do not specify \"name\" option in dbus channel when \"internal\"");
+ return;
+ }
+
+ self->connection = cockpit_dbus_internal_client ();
+ if (self->connection == NULL)
+ {
+ cockpit_channel_fail (channel, "internal-error", "no internal DBus connection");
+ return;
+ }
+
+ self->default_name = cockpit_dbus_internal_name ();
+ self->logname = "internal";
+
+ subscribe_and_cache (self);
+ send_ready (self);
+ }
+ else
+ {
+ GDBusConnectionFlags flags = G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT;
+
+ if (!cockpit_json_get_string (options, "name", NULL, &self->default_name))
+ {
+ self->default_name = NULL;
+ if (!cockpit_json_get_null (options, "name", NULL))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"name\" option in dbus channel");
+ return;
+ }
+ }
+ else if (self->default_name != NULL && !g_dbus_is_name (self->default_name))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "bad \"name\" option in dbus channel: %s", self->default_name);
+ return;
+ }
+
+ if (self->bus_type == G_BUS_TYPE_NONE && !g_dbus_is_address (address))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "bad \"address\" option in dbus channel: %s", address);
+ return;
+ }
+
+ if (self->default_name != NULL)
+ self->logname = self->default_name;
+ else if (address != NULL)
+ self->logname = address;
+ else
+ self->logname = bus;
+
+ if (self->bus_type == G_BUS_TYPE_NONE)
+ {
+ if (self->default_name)
+ flags = flags | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION;
+
+ g_dbus_connection_new_for_address (address,
+ flags,
+ NULL,
+ self->cancellable,
+ on_connection_ready,
+ g_object_ref (self));
+ }
+ else
+ {
+ gchar *bus_address = NULL;
+ gchar *key = NULL;
+ GError *error = NULL;
+ GDBusConnection *connection = NULL;
+
+ bus_address = g_dbus_address_get_for_bus_sync (self->bus_type, self->cancellable, &error);
+
+ if (bus_address == NULL)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "failed to connect to %s bus: %s",
+ self->bus_type == G_BUS_TYPE_SESSION ? "session" : "system",
+ error->message);
+ g_clear_error (&error);
+ return;
+ }
+
+ key = g_strdup_printf ("%s:%s", group, bus_address);
+
+ if (group_connections)
+ connection = g_hash_table_lookup (group_connections, key);
+
+ if (connection)
+ {
+ self->connection = g_object_ref (connection);
+ process_connection (self, NULL);
+ }
+ else
+ {
+ GPtrArray *subscribers = NULL;
+
+ if (!pending_group_connections)
+ {
+ pending_group_connections = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ (GDestroyNotify) g_ptr_array_unref);
+ }
+ else
+ {
+ subscribers = g_hash_table_lookup (pending_group_connections, key);
+ }
+
+ if (!subscribers)
+ {
+ subscribers = g_ptr_array_new_with_free_func (g_object_unref);
+ g_hash_table_insert (pending_group_connections, g_strdup (key), subscribers);
+
+ g_dbus_connection_new_for_address (bus_address,
+ flags | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
+ NULL,
+ self->cancellable,
+ on_shared_connection_ready,
+ g_strdup (key));
+ }
+
+ g_ptr_array_add (subscribers, g_object_ref (self));
+ }
+
+ g_free (key);
+ g_free (bus_address);
+ }
+ }
+}
+
+static void
+cockpit_dbus_json_dispose (GObject *object)
+{
+ CockpitDBusJson *self = COCKPIT_DBUS_JSON (object);
+ GHashTable *registered = NULL;
+ CockpitDBusPeer *peer;
+ GHashTableIter iter;
+ gpointer value;
+ gpointer key;
+ GList *l;
+
+ g_cancellable_cancel (self->cancellable);
+
+ if (self->default_watched)
+ {
+ g_bus_unwatch_name (self->default_watch);
+ self->default_watched = FALSE;
+ }
+
+ g_hash_table_iter_init (&iter, self->peers);
+ while (g_hash_table_iter_next (&iter, NULL, &value))
+ {
+ g_hash_table_iter_remove (&iter);
+ peer = value;
+
+ g_free (peer->name);
+
+ if (peer->cache)
+ {
+ g_signal_handler_disconnect (peer->cache, peer->meta_sig);
+ g_signal_handler_disconnect (peer->cache, peer->update_sig);
+ g_object_run_dispose (G_OBJECT (peer->cache));
+ g_object_unref (peer->cache);
+ }
+
+ cockpit_dbus_rules_free (peer->rules);
+
+ if (self->connection)
+ g_dbus_connection_signal_unsubscribe (self->connection, peer->subscribe_id);
+ g_free (peer);
+ }
+
+ /* Remove and cancel all pending invocations */
+ if (self->connection)
+ registered = g_object_get_data (G_OBJECT (self->connection), "registered-objects");
+ if (self->registered)
+ {
+ g_hash_table_iter_init (&iter, self->registered);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ if (registered)
+ g_hash_table_remove (registered, key);
+ g_dbus_connection_unregister_object (self->connection, GPOINTER_TO_UINT (value));
+ g_hash_table_iter_remove (&iter);
+ }
+ }
+ if (self->invocations)
+ {
+ g_hash_table_iter_init (&iter, self->invocations);
+ while (g_hash_table_iter_next (&iter, NULL, &value))
+ {
+ g_hash_table_iter_remove (&iter);
+ g_dbus_method_invocation_return_dbus_error (value, "org.freedesktop.DBus.Error.Failed",
+ "The DBus interface for this method has been disconnected");
+ }
+ }
+
+ if (self->fd_channel_ids)
+ {
+ while (!g_queue_is_empty (self->fd_channel_ids))
+ {
+ gchar *id;
+
+ id = (gchar *)g_queue_pop_head (self->fd_channel_ids);
+ cockpit_pipe_channel_remove_internal_fd (id);
+
+ g_free (id);
+ }
+
+ g_queue_free (self->fd_channel_ids);
+ self->fd_channel_ids = NULL;
+ }
+
+ /* Divorce ourselves the outstanding calls */
+ for (l = self->active_calls; l != NULL; l = g_list_next (l))
+ ((CallData *)l->data)->dbus_json = NULL;
+ g_list_free (self->active_calls);
+ self->active_calls = NULL;
+
+ G_OBJECT_CLASS (cockpit_dbus_json_parent_class)->dispose (object);
+}
+
+static void
+cockpit_dbus_json_finalize (GObject *object)
+{
+ CockpitDBusJson *self = COCKPIT_DBUS_JSON (object);
+
+ g_clear_object (&self->connection);
+ g_hash_table_unref (self->interface_info);
+ g_hash_table_unref (self->peers);
+ if (self->registered)
+ g_hash_table_unref (self->registered);
+ if (self->invocations)
+ g_hash_table_unref (self->invocations);
+ g_object_unref (self->cancellable);
+
+ G_OBJECT_CLASS (cockpit_dbus_json_parent_class)->finalize (object);
+}
+
+static void
+cockpit_dbus_json_constructed (GObject *object)
+{
+ const gchar *caps[] = { "address", NULL };
+
+ G_OBJECT_CLASS (cockpit_dbus_json_parent_class)->constructed (object);
+
+ g_object_set (object, "capabilities", &caps, NULL);
+}
+
+static void
+cockpit_dbus_json_class_init (CockpitDBusJsonClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->dispose = cockpit_dbus_json_dispose;
+ gobject_class->finalize = cockpit_dbus_json_finalize;
+ gobject_class->constructed = cockpit_dbus_json_constructed;
+
+ channel_class->prepare = cockpit_dbus_json_prepare;
+ channel_class->recv = cockpit_dbus_json_recv;
+ channel_class->closed = cockpit_dbus_json_closed;
+}
+
+/**
+ * cockpit_dbus_json_open:
+ * @transport: transport to send messages on
+ * @channel_id: the channel id
+ * @dbus_service: the DBus service name to talk to
+ *
+ * This function is mainly used by tests. The normal way to open
+ * channels is cockpit_channel_open().
+ *
+ * Guarantee: channel will not close immediately, even on invalid input.
+ *
+ * Returns: (transfer full): a new channel
+ */
+CockpitChannel *
+cockpit_dbus_json_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *dbus_service)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+
+ g_return_val_if_fail (channel_id != NULL, NULL);
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "bus", "session");
+ json_object_set_string_member (options, "service", dbus_service);
+ json_object_set_string_member (options, "payload", "dbus-json3");
+
+ channel = g_object_new (COCKPIT_TYPE_DBUS_JSON,
+ "transport", transport,
+ "id", channel_id,
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+ return channel;
+}
diff --git a/src/bridge/cockpitdbusjson.h b/src/bridge/cockpitdbusjson.h
new file mode 100644
index 0000000..0ce6ced
--- /dev/null
+++ b/src/bridge/cockpitdbusjson.h
@@ -0,0 +1,37 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_DBUS_JSON_H__
+#define COCKPIT_DBUS_JSON_H__
+
+#include <gio/gio.h>
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_DBUS_JSON (cockpit_dbus_json_get_type ())
+
+GType cockpit_dbus_json_get_type (void) G_GNUC_CONST;
+
+CockpitChannel * cockpit_dbus_json_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *dbus_service);
+
+#endif /* COCKPIT_DBUS_JSON_H__ */
diff --git a/src/bridge/cockpitdbusloginmessages.c b/src/bridge/cockpitdbusloginmessages.c
new file mode 100644
index 0000000..f94c730
--- /dev/null
+++ b/src/bridge/cockpitdbusloginmessages.c
@@ -0,0 +1,97 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitmemfdread.h"
+
+#include "cockpitdbusinternal.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#define INTROSPECTION_BLOB \
+ "<node>" \
+ "<interface name='cockpit.LoginMessages'>" \
+ "<method name='Get'>" \
+ "<arg name='messages' type='s' direction='out'/>" \
+ "</method>" \
+ "<method name='Dismiss'/>" \
+ "</interface>" \
+ "</node>"
+
+static gchar *login_messages;
+
+static void
+login_messages_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ if (g_str_equal (method_name, "Get"))
+ {
+ g_dbus_method_invocation_return_value (invocation,
+ g_variant_new ("(s)", login_messages ?: "{}"));
+ }
+ else if (g_str_equal (method_name, "Dismiss"))
+ {
+ g_free (login_messages);
+ login_messages = NULL;
+
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ }
+ else
+ g_assert_not_reached ();
+}
+
+void
+cockpit_dbus_login_messages_startup (void)
+{
+ static const GDBusInterfaceVTable vtable = {
+ .method_call = login_messages_method_call
+ };
+
+ g_autoptr(GError) error = NULL;
+
+ /* If we fail to read the messages, log the failure, but otherwise
+ * continue to register the service. We'll return '{}' in that case.
+ */
+ if (!cockpit_memfd_read_from_envvar (&login_messages, "COCKPIT_LOGIN_MESSAGES_MEMFD", &error))
+ {
+ g_warning ("Unable to read login messages data: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ g_autoptr(GDBusConnection) connection = cockpit_dbus_internal_server ();
+ g_return_if_fail (connection != NULL);
+
+ g_autoptr(GDBusNodeInfo) node = g_dbus_node_info_new_for_xml (INTROSPECTION_BLOB, &error);
+ g_assert_no_error (error);
+ GDBusInterfaceInfo *iface = g_dbus_node_info_lookup_interface (node, "cockpit.LoginMessages");
+ g_assert (iface != NULL);
+
+ g_dbus_connection_register_object (connection, "/LoginMessages", iface, &vtable, NULL, NULL, &error);
+ g_assert_no_error (error);
+}
diff --git a/src/bridge/cockpitdbusmachines.c b/src/bridge/cockpitdbusmachines.c
new file mode 100644
index 0000000..d10b4df
--- /dev/null
+++ b/src/bridge/cockpitdbusmachines.c
@@ -0,0 +1,253 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbusinternal.h"
+#include "common/cockpitmachinesjson.h"
+#include "common/cockpitsystem.h"
+
+#define MACHINES_SIG "a{sa{sv}}"
+
+/* counts the number of file change events that have not yet gotten a PropertiesChanged signal */
+static guint pending_updates;
+
+GFileMonitor *machines_monitor;
+
+/* returns a floating GVariant */
+static GVariant *
+get_machines (void)
+{
+ JsonNode *machines;
+ GError *error = NULL;
+ GVariant *variant;
+
+ machines = read_machines_json ();
+ variant = json_gvariant_deserialize (machines, MACHINES_SIG, &error);
+ /* if the signature does not match, we screwed up in the parser already */
+ g_assert (variant != NULL);
+ json_node_free (machines);
+ return variant;
+}
+
+static GVariant *
+machines_get_property (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *property_name,
+ GError **error,
+ gpointer user_data)
+{
+ g_return_val_if_fail (property_name != NULL, NULL);
+
+ if (g_str_equal (property_name, "Machines"))
+ return get_machines ();
+ else
+ g_return_val_if_reached (NULL);
+}
+
+static void
+machines_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ if (g_str_equal (method_name, "Update"))
+ {
+ const gchar *filename, *hostname;
+ GVariant * info_v;
+ JsonNode *info_json = NULL;
+ GError *error = NULL;
+
+ g_variant_get (parameters, "(&s&s@a{sv})", &filename, &hostname, &info_v);
+ info_json = json_gvariant_serialize (info_v);
+ g_debug ("Updating %s for machine %s", filename, hostname);
+ g_variant_unref (info_v);
+
+ if (update_machines_json (filename, hostname, info_json, &error))
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ else
+ g_dbus_method_invocation_take_error (invocation, error);
+ json_node_free (info_json);
+ }
+ else
+ g_return_if_reached ();
+}
+
+/**
+ * notify_properties:
+ * @user_data: GDBusConnection to which updates get sent
+ *
+ * Send a PropertiesChanged signal for invalidating the Machines property. This
+ * avoids parsing files and constructing the property when nobody is listening.
+ * This gets called in reaction to changed *.json configuration files.
+ */
+static gboolean
+notify_properties (gpointer user_data)
+{
+ GDBusConnection *connection = user_data;
+ GVariant *signal_value;
+ GVariantBuilder builder;
+ GError *error = NULL;
+
+ /* reset pending counter before we do any actual work, to avoid races */
+ pending_updates = 0;
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
+ g_variant_builder_add (&builder, "s", "Machines");
+ signal_value = g_variant_ref_sink (g_variant_new ("(sa{sv}as)", "cockpit.Machines", NULL, &builder));
+
+ g_dbus_connection_emit_signal (connection,
+ NULL,
+ "/machines",
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ signal_value,
+ &error);
+ if (error != NULL)
+ {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED))
+ g_critical ("failed to send PropertiesChanged signal: %s", error->message);
+ g_error_free (error);
+ }
+ g_variant_unref (signal_value);
+ return G_SOURCE_REMOVE;
+}
+
+
+static void
+on_machines_changed (GFileMonitor *monitor,
+ GFile *file,
+ GFile *other_file,
+ GFileMonitorEvent event_type,
+ gpointer user_data)
+{
+ gchar *path;
+
+ /* ignore uninteresting events; note that DELETED does not get a followup CHANGES_DONE_HINT */
+ if (event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT && event_type != G_FILE_MONITOR_EVENT_DELETED)
+ return;
+
+ path = g_file_get_path (file);
+ if (g_str_has_suffix (path, ".json"))
+ {
+ g_debug ("on_machines_changed: event type %i on %s", event_type, path);
+ /* change events tend to come in batches, so slightly delay the re-reading of
+ * files and sending PropertiesChanged; if we already have queued up an
+ * update, don't queue it again */
+ if (pending_updates++ == 0)
+ g_timeout_add (100, notify_properties, user_data);
+ }
+ else
+ {
+ g_debug ("on_machines_changed: ignoring event type %i on non-.json file %s", event_type, path);
+ }
+ g_free (path);
+}
+
+static GDBusInterfaceVTable machines_vtable = {
+ .method_call = machines_method_call,
+ .get_property = machines_get_property,
+};
+
+static GDBusPropertyInfo machines_property = {
+ -1, "Machines", MACHINES_SIG, G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo *machines_properties[] = {
+ &machines_property,
+ NULL
+};
+
+static GDBusArgInfo machines_update_filename_arg = {
+ -1, "filename", "s", NULL
+};
+
+static GDBusArgInfo machines_update_hostname_arg = {
+ -1, "hostname", "s", NULL
+};
+
+static GDBusArgInfo machines_update_info_arg = {
+ -1, "info", "a{sv}", NULL
+};
+
+static GDBusArgInfo *machines_update_args[] = {
+ &machines_update_filename_arg,
+ &machines_update_hostname_arg,
+ &machines_update_info_arg,
+ NULL
+};
+
+static GDBusMethodInfo machines_update_method = {
+ -1, "Update", machines_update_args, NULL, NULL
+};
+
+static GDBusMethodInfo *machines_methods[] = {
+ &machines_update_method,
+ NULL
+};
+
+static GDBusInterfaceInfo machines_interface = {
+ -1, "cockpit.Machines", machines_methods, NULL, machines_properties, NULL
+};
+
+void
+cockpit_dbus_machines_startup (void)
+{
+ GDBusConnection *connection;
+ GFile *machines_monitor_file;
+ GError *error = NULL;
+
+ connection = cockpit_dbus_internal_server ();
+ g_return_if_fail (connection != NULL);
+
+ g_dbus_connection_register_object (connection, "/machines", &machines_interface,
+ &machines_vtable, NULL, NULL, &error);
+
+ if (error != NULL)
+ {
+ g_critical ("couldn't register DBus cockpit.Machines object: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ /* watch for file changes and send D-Bus signal for it */
+ machines_monitor_file = g_file_new_for_path (get_machines_json_dir ());
+ machines_monitor = g_file_monitor (machines_monitor_file, G_FILE_MONITOR_NONE, NULL, &error);
+ g_object_unref (machines_monitor_file);
+ if (machines_monitor == NULL)
+ {
+ g_critical ("couldn't set up file watch: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+ g_signal_connect (machines_monitor, "changed", G_CALLBACK (on_machines_changed), connection);
+
+ g_object_unref (connection);
+}
+
+void
+cockpit_dbus_machines_cleanup (void)
+{
+ g_object_unref (machines_monitor);
+}
diff --git a/src/bridge/cockpitdbusmeta.c b/src/bridge/cockpitdbusmeta.c
new file mode 100644
index 0000000..c3cff20
--- /dev/null
+++ b/src/bridge/cockpitdbusmeta.c
@@ -0,0 +1,483 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbusmeta.h"
+
+#include "common/cockpitjson.h"
+
+#include <string.h>
+
+static JsonArray *
+build_meta_arguments (GDBusArgInfo **args)
+{
+ JsonArray *arguments = json_array_new ();
+ while (*args)
+ {
+ json_array_add_string_element (arguments, (*args)->signature);
+ args++;
+ }
+ return arguments;
+}
+
+static JsonObject *
+build_meta_method (GDBusMethodInfo *meth)
+{
+ JsonObject *method = json_object_new ();
+ if (meth->in_args)
+ {
+ json_object_set_array_member (method, "in",
+ build_meta_arguments (meth->in_args));
+ }
+ if (meth->out_args)
+ {
+ json_object_set_array_member (method, "out",
+ build_meta_arguments (meth->out_args));
+ }
+ return method;
+}
+
+static JsonObject *
+build_meta_signal (GDBusSignalInfo *sig)
+{
+ JsonObject *signal = json_object_new ();
+ if (sig->args)
+ {
+ json_object_set_array_member (signal, "in",
+ build_meta_arguments (sig->args));
+ }
+ return signal;
+}
+
+static JsonObject *
+build_meta_property (GDBusPropertyInfo *prop)
+{
+ JsonObject *property = json_object_new ();;
+ GString *flags = g_string_new ("");
+
+ if (prop->flags & G_DBUS_PROPERTY_INFO_FLAGS_READABLE)
+ g_string_append_c (flags, 'r');
+ if (prop->flags & G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE)
+ g_string_append_c (flags, 'w');
+ json_object_set_string_member (property, "flags", flags->str);
+ if (prop->signature)
+ json_object_set_string_member (property, "type", prop->signature);
+ g_string_free (flags, TRUE);
+ return property;
+}
+
+JsonObject *
+cockpit_dbus_meta_build (GDBusInterfaceInfo *iface)
+{
+ JsonObject *interface;
+ JsonObject *methods;
+ JsonObject *properties;
+ JsonObject *signals;
+ guint i;
+
+ g_return_val_if_fail (iface != NULL, NULL);
+
+ interface = json_object_new ();
+
+ if (iface->methods)
+ {
+ methods = json_object_new ();
+ for (i = 0; iface->methods[i] != NULL; i++)
+ {
+ json_object_set_object_member (methods, iface->methods[i]->name,
+ build_meta_method (iface->methods[i]));
+ }
+ json_object_set_object_member (interface, "methods", methods);
+ }
+
+ if (iface->properties)
+ {
+ properties = json_object_new ();
+ for (i = 0; iface->properties[i] != NULL; i++)
+ {
+ json_object_set_object_member (properties, iface->properties[i]->name,
+ build_meta_property (iface->properties[i]));
+ }
+ json_object_set_object_member (interface, "properties", properties);
+ }
+
+ if (iface->signals)
+ {
+ signals = json_object_new ();
+ for (i = 0; iface->signals[i] != NULL; i++)
+ {
+ json_object_set_object_member (signals, iface->signals[i]->name,
+ build_meta_signal (iface->signals[i]));
+ }
+ json_object_set_object_member (interface, "signals", signals);
+ }
+
+ return interface;
+}
+
+static GDBusArgInfo **
+parse_meta_arguments (JsonArray *arguments,
+ GError **error)
+{
+ const gchar *signature;
+ GDBusArgInfo *arg;
+ GPtrArray *args;
+ guint i, length;
+ JsonNode *node;
+
+ args = g_ptr_array_new ();
+ g_ptr_array_set_free_func (args, (GDestroyNotify)g_dbus_arg_info_unref);
+
+ length = json_array_get_length (arguments);
+ for (i = 0; i < length; i++)
+ {
+ node = json_array_get_element (arguments, i);
+ if (!JSON_NODE_HOLDS_VALUE(node) || json_node_get_value_type (node) != G_TYPE_STRING)
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid argument in dbus meta field");
+ break;
+ }
+
+ signature = json_node_get_string (node);
+ if (!g_variant_is_signature (signature))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "argument in dbus meta field has invalid signature: %s", signature);
+ break;
+ }
+
+ arg = g_new0 (GDBusArgInfo, 1);
+ arg->ref_count = 1;
+ arg->name = g_strdup_printf ("argument_%u", i);
+ arg->signature = g_strdup (signature);
+ g_ptr_array_add (args, arg);
+ }
+
+ if (i != length)
+ {
+ g_ptr_array_free (args, TRUE);
+ return NULL;
+ }
+ else
+ {
+ g_ptr_array_add (args, NULL);
+ return (GDBusArgInfo **)g_ptr_array_free (args, FALSE);
+ }
+}
+
+static GDBusMethodInfo *
+parse_meta_method (const gchar *method_name,
+ JsonObject *method,
+ GError **error)
+{
+ GDBusMethodInfo *ret = NULL;
+ GDBusMethodInfo *meth;
+ JsonArray *args;
+
+ meth = g_new0 (GDBusMethodInfo, 1);
+ meth->ref_count = 1;
+ meth->name = g_strdup (method_name);
+
+ if (!cockpit_json_get_array (method, "in", NULL, &args))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid \"in\" field in dbus meta method: %s", method_name);
+ goto out;
+ }
+
+ if (args && json_array_get_length (args) > 0)
+ {
+ meth->in_args = parse_meta_arguments (args, error);
+ if (!meth->in_args)
+ goto out;
+ }
+
+ if (!cockpit_json_get_array (method, "out", NULL, &args))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid \"out\" field in dbus meta method: %s", method_name);
+ goto out;
+ }
+
+ if (args && json_array_get_length (args) > 0)
+ {
+ meth->out_args = parse_meta_arguments (args, error);
+ if (!meth->out_args)
+ goto out;
+ }
+
+ ret = meth;
+ meth = NULL;
+
+out:
+ if (meth)
+ g_dbus_method_info_unref (meth);
+ return ret;
+}
+
+static GDBusSignalInfo *
+parse_meta_signal (const gchar *signal_name,
+ JsonObject *signal,
+ GError **error)
+{
+ GDBusSignalInfo *ret = NULL;
+ GDBusSignalInfo *sig;
+ JsonArray *args;
+
+ sig = g_new0 (GDBusSignalInfo, 1);
+ sig->ref_count = 1;
+ sig->name = g_strdup (signal_name);
+
+ if (!cockpit_json_get_array (signal, "in", NULL, &args))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid \"in\" field in dbus meta signal: %s", signal_name);
+ goto out;
+ }
+
+ if (args && json_array_get_length (args) > 0)
+ {
+ sig->args = parse_meta_arguments (args, error);
+ if (!sig->args)
+ goto out;
+ }
+
+ ret = sig;
+ sig = NULL;
+
+out:
+ if (sig)
+ g_dbus_signal_info_unref (sig);
+ return ret;
+}
+
+static GDBusPropertyInfo *
+parse_meta_property (const gchar *property_name,
+ JsonObject *property,
+ GError **error)
+{
+ GDBusPropertyInfo *prop;
+ GDBusPropertyInfo *ret = NULL;
+ const gchar *flags;
+ const gchar *type;
+
+ prop = g_new0 (GDBusPropertyInfo, 1);
+ prop->ref_count = 1;
+ prop->name = g_strdup (property_name);
+
+ if (!cockpit_json_get_string (property, "flags", NULL, &flags))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid \"flags\" field in dbus property: %s", property_name);
+ goto out;
+ }
+
+ if (flags && strchr (flags, 'r'))
+ prop->flags |= G_DBUS_PROPERTY_INFO_FLAGS_READABLE;
+ if (flags && strchr (flags, 'w'))
+ prop->flags |= G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE;
+
+ if (!cockpit_json_get_string (property, "type", NULL, &type))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid \"type\" field in dbus property: %s", property_name);
+ goto out;
+ }
+ else if (!type)
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "missing \"type\" field in dbus property: %s", property_name);
+ goto out;
+ }
+ else if (!g_variant_is_signature (type))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "the \"type\" field in dbus property is not a dbus signature: %s", type);
+ goto out;
+ }
+
+ prop->signature = g_strdup (type);
+
+ ret = prop;
+ prop = NULL;
+
+out:
+ if (prop)
+ g_dbus_property_info_unref (prop);
+ return ret;
+}
+
+GDBusInterfaceInfo *
+cockpit_dbus_meta_parse (const gchar *iface_name,
+ JsonObject *interface,
+ GError **error)
+{
+ GDBusInterfaceInfo *ret = NULL;
+ GDBusInterfaceInfo *iface;
+ GDBusMethodInfo *meth;
+ GDBusSignalInfo *sig;
+ GDBusPropertyInfo *prop;
+ JsonObject *methods;
+ JsonObject *method;
+ JsonObject *signals;
+ JsonObject *signal;
+ JsonObject *properties;
+ JsonObject *property;
+ GPtrArray *array = NULL;
+ GList *names = NULL, *l;
+
+ g_return_val_if_fail (iface_name != NULL, NULL);
+ g_return_val_if_fail (interface != NULL, NULL);
+ g_return_val_if_fail (!error || !*error, NULL);
+
+ iface = g_new0 (GDBusInterfaceInfo, 1);
+ iface->name = g_strdup (iface_name);
+ iface->ref_count = 1;
+
+ if (!cockpit_json_get_object (interface, "methods", NULL, &methods))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid \"methods\" field in dbus meta structure");
+ goto out;
+ }
+
+ if (methods)
+ {
+ array = g_ptr_array_new ();
+ g_ptr_array_set_free_func (array, (GDestroyNotify)g_dbus_method_info_unref);
+
+ names = json_object_get_members (methods);
+ for (l = names; l != NULL; l = g_list_next (l))
+ {
+ if (!cockpit_json_get_object (methods, l->data, NULL, &method))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid method field in dbus meta structure: %s",
+ (const gchar *)l->data);
+ goto out;
+ }
+
+ g_assert (method != NULL);
+ meth = parse_meta_method (l->data, method, error);
+ if (!meth)
+ goto out;
+
+ g_ptr_array_add (array, meth);
+ }
+
+ g_list_free (names);
+ names = NULL;
+
+ g_ptr_array_add (array, NULL);
+ iface->methods = (GDBusMethodInfo **)g_ptr_array_free (array, FALSE);
+ array = NULL;
+ }
+
+ if (!cockpit_json_get_object (interface, "signals", NULL, &signals))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid \"signals\" field in dbus meta structure");
+ goto out;
+ }
+
+ if (signals)
+ {
+ array = g_ptr_array_new ();
+ g_ptr_array_set_free_func (array, (GDestroyNotify)g_dbus_signal_info_unref);
+
+ names = json_object_get_members (signals);
+ for (l = names; l != NULL; l = g_list_next (l))
+ {
+ if (!cockpit_json_get_object (signals, l->data, NULL, &signal))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid signal field in dbus meta structure: %s",
+ (const gchar *)l->data);
+ goto out;
+ }
+
+ g_assert (signal != NULL);
+ sig = parse_meta_signal (l->data, signal, error);
+ if (!sig)
+ goto out;
+
+ g_ptr_array_add (array, sig);
+ }
+
+ g_list_free (names);
+ names = NULL;
+
+ g_ptr_array_add (array, NULL);
+ iface->signals = (GDBusSignalInfo **)g_ptr_array_free (array, FALSE);
+ array = NULL;
+ }
+
+ if (!cockpit_json_get_object (interface, "properties", NULL, &properties))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid \"properties\" field in dbus meta structure");
+ goto out;
+ }
+
+ if (properties)
+ {
+ array = g_ptr_array_new ();
+ g_ptr_array_set_free_func (array, (GDestroyNotify)g_dbus_property_info_unref);
+
+ names = json_object_get_members (properties);
+ for (l = names; l != NULL; l = g_list_next (l))
+ {
+ if (!cockpit_json_get_object (properties, l->data, NULL, &property))
+ {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
+ "invalid property field in dbus meta structure: %s",
+ (const gchar *)l->data);
+ goto out;
+ }
+
+ g_assert (property != NULL);
+ prop = parse_meta_property (l->data, property, error);
+ if (!prop)
+ goto out;
+
+ g_ptr_array_add (array, prop);
+ }
+
+ g_list_free (names);
+ names = NULL;
+
+ g_ptr_array_add (array, NULL);
+ iface->properties = (GDBusPropertyInfo **)g_ptr_array_free (array, FALSE);
+ array = NULL;
+ }
+
+ ret = iface;
+ iface = NULL;
+
+out:
+ if (iface)
+ g_dbus_interface_info_unref (iface);
+ if (array)
+ g_ptr_array_free (array, TRUE);
+ if (names)
+ g_list_free (names);
+ return ret;
+}
diff --git a/src/bridge/cockpitdbusmeta.h b/src/bridge/cockpitdbusmeta.h
new file mode 100644
index 0000000..f1ae592
--- /dev/null
+++ b/src/bridge/cockpitdbusmeta.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_DBUS_META_H__
+#define COCKPIT_DBUS_META_H__
+
+#include <gio/gio.h>
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+JsonObject * cockpit_dbus_meta_build (GDBusInterfaceInfo *iface);
+
+GDBusInterfaceInfo * cockpit_dbus_meta_parse (const gchar *iface_name,
+ JsonObject *interface,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* COCKPIT_DBUS_META_H__ */
diff --git a/src/bridge/cockpitdbusprocess.c b/src/bridge/cockpitdbusprocess.c
new file mode 100644
index 0000000..8e4a854
--- /dev/null
+++ b/src/bridge/cockpitdbusprocess.c
@@ -0,0 +1,166 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbusinternal.h"
+
+#include "common/cockpitsystem.h"
+
+#include <errno.h>
+#include <stdlib.h>
+
+static GVariant *
+build_environment (void)
+{
+ GVariantBuilder builder;
+ GVariant *variant;
+
+ gchar **environ = g_listenv ();
+ gint i;
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{ss}"));
+ for (i = 0; environ[i] != NULL; i++)
+ {
+ const gchar *value = g_getenv (environ[i]);
+ if (value)
+ g_variant_builder_add (&builder, "{ss}", environ[i], value);
+ }
+
+ g_strfreev (environ);
+ variant = g_variant_builder_end (&builder);
+ return variant;
+}
+
+static GVariant *
+lookup_session_id (void)
+{
+ char *session_id;
+ GVariant *variant;
+
+ session_id = cockpit_system_session_id ();
+ variant = g_variant_new_string (session_id ? session_id : "");
+ free (session_id);
+ return variant;
+}
+
+static GVariant *
+process_get_property (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *property_name,
+ GError **error,
+ gpointer user_data)
+{
+ g_return_val_if_fail (property_name != NULL, NULL);
+
+ if (g_str_equal (property_name, "Pid"))
+ return g_variant_new_uint32 (getpid ());
+ else if (g_str_equal (property_name, "Uid"))
+ return g_variant_new_int32 (getuid ());
+ else if (g_str_equal (property_name, "SessionId"))
+ return lookup_session_id ();
+ else if (g_str_equal (property_name, "StartTime"))
+ return g_variant_new_uint64 (cockpit_system_process_start_time ());
+ else if (g_str_equal (property_name, "Environment") || g_str_equal (property_name, "Variables"))
+ return build_environment ();
+ else
+ g_return_val_if_reached (NULL);
+}
+
+static GDBusInterfaceVTable process_vtable = {
+ .get_property = process_get_property,
+};
+
+static GDBusPropertyInfo pid_property = {
+ -1, "Pid", "u", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo uid_property = {
+ -1, "Uid", "i", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo start_time_property = {
+ -1, "StartTime", "t", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo session_id_property = {
+ -1, "SessionId", "s", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL,
+};
+
+static GDBusPropertyInfo environment_property = {
+ -1, "Environment", "a{ss}", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo *process_properties[] = {
+ &pid_property,
+ &uid_property,
+ &start_time_property,
+ &session_id_property,
+ &environment_property,
+ NULL
+};
+
+static GDBusInterfaceInfo process_interface = {
+ -1, "cockpit.Process", NULL, NULL, process_properties, NULL
+};
+
+static GDBusPropertyInfo variables_property = {
+ -1, "Variables", "a{ss}", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo *environment_properties[] = {
+ &variables_property,
+ NULL
+};
+
+static GDBusInterfaceInfo environment_interface = {
+ -1, "cockpit.Environment", NULL, NULL, environment_properties, NULL
+};
+
+void
+cockpit_dbus_process_startup (void)
+{
+ GDBusConnection *connection;
+ GError *error = NULL;
+
+ connection = cockpit_dbus_internal_server ();
+ g_return_if_fail (connection != NULL);
+
+ g_dbus_connection_register_object (connection, "/environment", &environment_interface,
+ &process_vtable, NULL, NULL, &error);
+
+ if (error != NULL)
+ {
+ g_critical ("couldn't register DBus cockpit.Environment object: %s", error->message);
+ g_error_free (error);
+ }
+
+ g_dbus_connection_register_object (connection, "/bridge", &process_interface,
+ &process_vtable, NULL, NULL, &error);
+
+ if (error != NULL)
+ {
+ g_critical ("couldn't register DBus cockpit.Process object: %s", error->message);
+ g_error_free (error);
+ }
+
+ g_object_unref (connection);
+}
diff --git a/src/bridge/cockpitdbusrules.c b/src/bridge/cockpitdbusrules.c
new file mode 100644
index 0000000..b552c17
--- /dev/null
+++ b/src/bridge/cockpitdbusrules.c
@@ -0,0 +1,341 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbusrules.h"
+
+#include "cockpitpaths.h"
+
+#include <gio/gio.h>
+
+#include <string.h>
+
+/*
+ * These are rules similar to what a dbus-daemon would use when processing
+ * AddMatch based forwarding. These are used to identify which signals a
+ * client wanted to subscribe to, and which paths/interfaces a client
+ * wanted to watch.
+ *
+ * We use several speed ups to quickly identify paths that are not matched
+ * by the rules ... before then going into matching each rule in turn.
+ *
+ * An empty rule set forwards nothing. It has a fast bypass flag which
+ * disables all the logic.
+ */
+
+typedef struct {
+ gint refs;
+ gchar *path;
+ gboolean is_namespace;
+ gchar *interface;
+ gchar *member;
+ gchar *arg0;
+} RuleData;
+
+static guint32
+rule_hash (gconstpointer data)
+{
+ const RuleData *rule = data;
+ return g_str_hash (rule->path) ^
+ g_int_hash (&rule->is_namespace) ^
+ (rule->interface ? g_str_hash (rule->interface) : 0) ^
+ (rule->member ? g_str_hash (rule->member) : 0) ^
+ (rule->arg0 ? g_str_hash (rule->arg0) : 0);
+}
+
+static gboolean
+rule_equal (gconstpointer one,
+ gconstpointer two)
+{
+ const RuleData *r1 = one;
+ const RuleData *r2 = two;
+ return r1->is_namespace == r2->is_namespace &&
+ g_str_equal (r1->path, r2->path) &&
+ g_strcmp0 (r1->interface, r2->interface) == 0;
+}
+
+static void
+rule_dump (RuleData *rule,
+ GString *string)
+{
+ g_string_append (string, "{ ");
+ if (rule->path)
+ g_string_append_printf (string, "%s: \"%s\", ", rule->is_namespace ? "path_namespace" : "path", rule->path);
+ if (rule->interface)
+ g_string_append_printf (string, "interface: \"%s\", ", rule->interface);
+ if (rule->arg0)
+ g_string_append_printf (string, "arg0: \"%s\", ", rule->arg0);
+ if (rule->member)
+ g_string_append_printf (string, "member: \"%s\", ", rule->member);
+ g_string_append (string, "}");
+}
+
+static void
+rule_free (gpointer data)
+{
+ RuleData *rule = data;
+ g_free (rule->path);
+ g_free (rule->interface);
+ g_free (rule->arg0);
+ g_free (rule->member);
+ g_slice_free (RuleData, rule);
+}
+
+struct _CockpitDBusRules {
+ GHashTable *all;
+ GHashTable *paths;
+ GTree *path_namespaces;
+ gboolean all_paths;
+ gboolean only_paths;
+ gboolean nothing;
+};
+
+gchar *
+cockpit_dbus_rules_to_string (CockpitDBusRules *rules)
+{
+ GHashTableIter iter;
+ GString *string;
+ RuleData *rule;
+
+ string = g_string_new ("[ ");
+ g_hash_table_iter_init (&iter, rules->all);
+ while (g_hash_table_iter_next (&iter, (gpointer *)&rule, NULL))
+ {
+ rule_dump (rule, string);
+ g_string_append (string, ", ");
+ }
+ g_string_append (string, "]");
+
+ return g_string_free (string, FALSE);
+}
+
+static gboolean
+rule_match (RuleData *rule,
+ const gchar *path,
+ const gchar *interface,
+ const gchar *member,
+ const gchar *arg0)
+{
+ if (!g_str_equal (rule->path, path))
+ {
+ if (!rule->is_namespace || !cockpit_path_equal_or_ancestor (path, rule->path))
+ return FALSE;
+ }
+ if (interface && rule->interface && strcmp (interface, rule->interface) != 0)
+ return FALSE;
+ if (member && rule->member && strcmp (member, rule->member) != 0)
+ return FALSE;
+
+ /* Note that if arg0 came in as NULL, it means message doesn't have arg0: no match */
+ if (rule->arg0 && g_strcmp0 (arg0, rule->arg0) != 0)
+ return FALSE;
+
+ return TRUE;
+}
+
+gboolean
+cockpit_dbus_rules_match (CockpitDBusRules *rules,
+ const gchar *path,
+ const gchar *interface,
+ const gchar *member,
+ const gchar *arg0)
+{
+ GHashTableIter iter;
+ RuleData *rule;
+
+ g_return_val_if_fail (path != NULL, FALSE);
+
+ if (rules->nothing)
+ return FALSE;
+
+ if (!rules->all_paths)
+ {
+ if (!cockpit_paths_contain_or_ancestor (rules->path_namespaces, path) &&
+ !g_hash_table_lookup (rules->paths, path))
+ return FALSE;
+ }
+
+ if (rules->only_paths)
+ return TRUE;
+
+ g_hash_table_iter_init (&iter, rules->all);
+ while (g_hash_table_iter_next (&iter, (gpointer *)&rule, NULL))
+ {
+ if (rule_match (rule, path, interface, member, arg0))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+CockpitDBusRules *
+cockpit_dbus_rules_new (void)
+{
+ CockpitDBusRules *rules = g_new0 (CockpitDBusRules, 1);
+ rules->nothing = TRUE;
+ return rules;
+}
+
+static RuleData *
+rule_lookup (CockpitDBusRules *rules,
+ const gchar *path,
+ gboolean is_namespace,
+ const gchar *interface,
+ const gchar *member,
+ const gchar *arg0)
+{
+ RuleData key = {
+ .refs = 0,
+ .path = (gchar *)path,
+ .is_namespace = is_namespace,
+ .interface = (gchar *)interface,
+ .member = (gchar *)member,
+ .arg0 = (gchar *)arg0
+ };
+
+ return g_hash_table_lookup (rules->all, &key);
+}
+
+static void
+recompile_rules (CockpitDBusRules *rules)
+{
+ GHashTableIter iter;
+ RuleData *rule;
+
+ if (rules->paths)
+ g_hash_table_remove_all (rules->paths);
+ else
+ rules->paths = g_hash_table_new (g_str_hash, g_str_equal);
+ if (rules->path_namespaces)
+ g_tree_destroy (rules->path_namespaces);
+ rules->path_namespaces = cockpit_paths_new ();
+
+ rules->all_paths = FALSE;
+ rules->nothing = TRUE;
+ rules->only_paths = TRUE;
+
+ g_hash_table_iter_init (&iter, rules->all);
+ while (g_hash_table_iter_next (&iter, (gpointer *)&rule, NULL))
+ {
+ rules->nothing = FALSE;
+
+ if (rule->is_namespace)
+ {
+ if (g_str_equal (rule->path, "/"))
+ rules->all_paths = TRUE;
+ cockpit_paths_add (rules->path_namespaces, rule->path);
+ }
+ else
+ {
+ g_hash_table_add (rules->paths, rule->path);
+ }
+
+ if (rule->interface || rule->member || rule->arg0)
+ rules->only_paths = FALSE;
+ }
+}
+
+gboolean
+cockpit_dbus_rules_add (CockpitDBusRules *rules,
+ const gchar *path,
+ gboolean is_namespace,
+ const gchar *interface,
+ const gchar *member,
+ const gchar *arg0)
+{
+ RuleData *rule = NULL;
+
+ if (!path)
+ {
+ path = "/";
+ is_namespace = TRUE;
+ }
+
+ if (!rules->nothing)
+ rule = rule_lookup (rules, path, is_namespace, interface, member, arg0);
+
+ if (rules->all == NULL)
+ rules->all = g_hash_table_new_full (rule_hash, rule_equal, rule_free, NULL);
+
+ if (rule == NULL)
+ {
+ rule = g_slice_new0 (RuleData);
+ rule->refs = 1;
+ rule->path = g_strdup (path);
+ rule->is_namespace = is_namespace;
+ rule->interface = g_strdup (interface);
+ rule->member = g_strdup (member);
+ rule->arg0 = g_strdup (arg0);
+ g_hash_table_add (rules->all, rule);
+ recompile_rules (rules);
+ return TRUE;
+ }
+ else
+ {
+ rule->refs++;
+ return FALSE;
+ }
+}
+
+gboolean
+cockpit_dbus_rules_remove (CockpitDBusRules *rules,
+ const gchar *path,
+ gboolean is_namespace,
+ const gchar *interface,
+ const gchar *member,
+ const gchar *arg0)
+{
+ RuleData *rule = NULL;
+
+ if (!path)
+ {
+ path = "/";
+ is_namespace = TRUE;
+ }
+
+ if (rules->nothing)
+ return FALSE;
+
+ rule = rule_lookup (rules, path, is_namespace, interface, member, arg0);
+ if (rule == NULL)
+ return FALSE;
+
+ rule->refs--;
+ if (rule->refs == 0)
+ {
+ g_hash_table_remove (rules->all, rule);
+ recompile_rules (rules);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+void
+cockpit_dbus_rules_free (CockpitDBusRules *rules)
+{
+ if (rules->all)
+ g_hash_table_destroy (rules->all);
+ if (rules->paths)
+ g_hash_table_destroy (rules->paths);
+ if (rules->path_namespaces)
+ g_tree_destroy (rules->path_namespaces);
+ g_free (rules);
+}
diff --git a/src/bridge/cockpitdbusrules.h b/src/bridge/cockpitdbusrules.h
new file mode 100644
index 0000000..afb1890
--- /dev/null
+++ b/src/bridge/cockpitdbusrules.h
@@ -0,0 +1,53 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_DBUS_RULES_H_
+#define COCKPIT_DBUS_RULES_H_
+
+#include <glib.h>
+
+typedef struct _CockpitDBusRules CockpitDBusRules;
+
+CockpitDBusRules * cockpit_dbus_rules_new (void);
+
+gboolean cockpit_dbus_rules_match (CockpitDBusRules *rules,
+ const gchar *path,
+ const gchar *interface,
+ const gchar *member,
+ const gchar *arg0);
+
+gboolean cockpit_dbus_rules_add (CockpitDBusRules *rules,
+ const gchar *path,
+ gboolean is_namespace,
+ const gchar *interface,
+ const gchar *member,
+ const gchar *arg0);
+
+gboolean cockpit_dbus_rules_remove (CockpitDBusRules *rules,
+ const gchar *path,
+ gboolean is_namespace,
+ const gchar *interface,
+ const gchar *member,
+ const gchar *arg0);
+
+gchar * cockpit_dbus_rules_to_string (CockpitDBusRules *rules);
+
+void cockpit_dbus_rules_free (CockpitDBusRules *rules);
+
+#endif /* COCKPIT_DBUS_RULES_H_ */
diff --git a/src/bridge/cockpitdbususer.c b/src/bridge/cockpitdbususer.c
new file mode 100644
index 0000000..019515d
--- /dev/null
+++ b/src/bridge/cockpitdbususer.c
@@ -0,0 +1,210 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbusinternal.h"
+
+#include <sys/types.h>
+#include <errno.h>
+#include <grp.h>
+#include <pwd.h>
+#include <string.h>
+
+static void
+variant_table_sink (GHashTable *props,
+ const gchar *name,
+ GVariant *value)
+{
+ g_hash_table_insert (props, (gchar *)name, g_variant_ref_sink (value));
+}
+
+static void
+populate_passwd_props (GHashTable *props,
+ struct passwd *pw)
+{
+ if (pw == NULL)
+ {
+ variant_table_sink (props, "Id", g_variant_new_int64 (geteuid ()));
+ variant_table_sink (props, "Name", g_variant_new_string (""));
+ variant_table_sink (props, "Full", g_variant_new_string (""));
+ variant_table_sink (props, "Home", g_variant_new_string (""));
+ variant_table_sink (props, "Shell", g_variant_new_string (""));
+ }
+ else
+ {
+ size_t n;
+ gchar *full;
+
+ n = strlen (pw->pw_gecos);
+ while (n > 0 && pw->pw_gecos[n - 1] == ',')
+ n--;
+ full = g_strndup (pw->pw_gecos, n);
+
+ variant_table_sink (props, "Id", g_variant_new_int64 (pw->pw_uid));
+ variant_table_sink (props, "Name", g_variant_new_string (pw->pw_name));
+ variant_table_sink (props, "Full", g_variant_new_string (full));
+ variant_table_sink (props, "Home", g_variant_new_string (pw->pw_dir));
+ variant_table_sink (props, "Shell", g_variant_new_string (pw->pw_shell));
+
+ g_free (full);
+ }
+}
+
+static void
+populate_group_prop (GHashTable *props)
+{
+ struct group *gr;
+ GPtrArray *array;
+ gid_t *list = NULL;
+ gid_t gid;
+ int ret, i;
+
+ gid = getegid ();
+
+ ret = getgroups (0, NULL);
+ if (ret > 0)
+ {
+ list = g_new (gid_t, ret);
+ ret = getgroups (ret, list);
+ }
+
+ if (ret < 0)
+ {
+ g_warning ("couldn't load list of groups: %s", g_strerror (errno));
+ ret = 0;
+ }
+
+ array = g_ptr_array_new ();
+
+ gr = getgrgid (gid);
+ if (gr == NULL)
+ g_warning ("couldn't load group info for %d: %s", (int)gid, g_strerror (errno));
+ else
+ g_ptr_array_add (array, g_variant_new_string (gr->gr_name));
+
+ for (i = 0; i < ret; i++)
+ {
+ if (list[i] == gid)
+ continue;
+
+ gr = getgrgid (list[i]);
+ if (gr == NULL)
+ {
+ g_warning ("couldn't load group info for %d: %s", (int)gid, g_strerror (errno));
+ continue;
+ }
+
+ g_ptr_array_add (array, g_variant_new_string (gr->gr_name));
+ }
+
+ variant_table_sink (props, "Groups", g_variant_new_array (G_VARIANT_TYPE_STRING,
+ (GVariant * const*)array->pdata,array->len));
+
+ g_ptr_array_free (array, TRUE);
+ g_free (list);
+}
+
+static GVariant *
+user_get_property (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *property_name,
+ GError **error,
+ gpointer user_data)
+{
+ GHashTable *props = user_data;
+
+ if (!g_hash_table_lookup (props, "Groups"))
+ populate_group_prop (props);
+
+ GVariant *value = g_hash_table_lookup (props, property_name);
+ g_return_val_if_fail (value != NULL, NULL);
+ return g_variant_ref (value);
+}
+
+static GDBusPropertyInfo user_name_property = {
+ -1, "Name", "s", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo user_full_property = {
+ -1, "Full", "s", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo user_id_property = {
+ -1, "Id", "x", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo user_home_property = {
+ -1, "Home", "s", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo user_shell_property = {
+ -1, "Shell", "s", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo user_groups_property = {
+ -1, "Groups", "as", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo *user_properties[] = {
+ &user_name_property,
+ &user_full_property,
+ &user_id_property,
+ &user_shell_property,
+ &user_home_property,
+ &user_groups_property,
+ NULL
+};
+
+static GDBusInterfaceInfo user_interface = {
+ -1, "cockpit.User", NULL, NULL, user_properties, NULL
+};
+
+static GDBusInterfaceVTable user_vtable = {
+ .get_property = user_get_property,
+};
+
+void
+cockpit_dbus_user_startup (struct passwd *pwd)
+{
+ GDBusConnection *connection;
+ GHashTable *props;
+ GError *error = NULL;
+
+ connection = cockpit_dbus_internal_server ();
+ g_return_if_fail (connection != NULL);
+
+ props = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)g_variant_unref);
+ populate_passwd_props (props, pwd);
+
+ g_dbus_connection_register_object (connection, "/user", &user_interface,
+ &user_vtable, props, (GDestroyNotify)g_hash_table_unref,
+ &error);
+
+ if (error != NULL)
+ {
+ g_critical ("couldn't register user object: %s", error->message);
+ g_hash_table_unref (props);
+ g_error_free (error);
+ }
+
+ g_object_unref (connection);
+}
diff --git a/src/bridge/cockpitdisksamples.c b/src/bridge/cockpitdisksamples.c
new file mode 100644
index 0000000..1244d4e
--- /dev/null
+++ b/src/bridge/cockpitdisksamples.c
@@ -0,0 +1,287 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdisksamples.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+typedef struct {
+ guint64 disk_read;
+ guint64 disk_write;
+} cgroup_values_t;
+
+/* TODO: this should be optimized so we don't allocate network and call open()/close() all the time */
+
+void
+cockpit_disk_samples (CockpitSamples *samples)
+{
+ gchar *contents = NULL;
+ GError *error = NULL;
+ gchar **lines = NULL;
+ guint64 bytes_read;
+ guint64 bytes_written;
+ gsize len;
+ guint n;
+ static gboolean not_supported = FALSE;
+
+ if (not_supported)
+ goto out;
+
+ if (!g_file_get_contents ("/proc/diskstats", &contents, &len, &error))
+ {
+ g_message ("error loading contents /proc/diskstats: %s", error->message);
+ g_error_free (error);
+ not_supported = TRUE;
+ goto out;
+ }
+
+ bytes_read = 0;
+ bytes_written = 0;
+
+ lines = g_strsplit (contents, "\n", -1);
+ for (n = 0; lines != NULL && lines[n] != NULL; n++)
+ {
+ const gchar *line = lines[n];
+ guint num_parsed;
+ gint dev_major, dev_minor;
+ gchar dev_name[128];
+ guint64 num_reads, num_reads_merged, num_sectors_read, num_msec_reading;
+ guint64 num_writes, num_writes_merged, num_sectors_written, num_msec_writing;
+ guint64 num_io_in_progress, num_msec_doing_io, weighted_num_msec_doing_io;
+
+ if (strlen (line) == 0)
+ continue;
+
+ /* From http://www.kernel.org/doc/Documentation/iostats.txt
+ *
+ * Field 1 -- # of reads completed
+ * This is the total number of reads completed successfully.
+ * Field 2 -- # of reads merged, field 6 -- # of writes merged
+ * Reads and writes which are adjacent to each other may be merged for
+ * efficiency. Thus two 4K reads may become one 8K read before it is
+ * ultimately handed to the disk, and so it will be counted (and queued)
+ * as only one I/O. This field lets you know how often this was done.
+ * Field 3 -- # of sectors read
+ * This is the total number of sectors read successfully.
+ * Field 4 -- # of milliseconds spent reading
+ * This is the total number of milliseconds spent by all reads (as
+ * measured from __make_request() to end_that_request_last()).
+ * Field 5 -- # of writes completed
+ * This is the total number of writes completed successfully.
+ * Field 7 -- # of sectors written
+ * This is the total number of sectors written successfully.
+ * Field 8 -- # of milliseconds spent writing
+ * This is the total number of milliseconds spent by all writes (as
+ * measured from __make_request() to end_that_request_last()).
+ * Field 9 -- # of I/Os currently in progress
+ * The only field that should go to zero. Incremented as requests are
+ * given to appropriate struct request_queue and decremented as they finish.
+ * Field 10 -- # of milliseconds spent doing I/Os
+ * This field increases so long as field 9 is nonzero.
+ * Field 11 -- weighted # of milliseconds spent doing I/Os
+ * This field is incremented at each I/O start, I/O completion, I/O
+ * merge, or read of these stats by the number of I/Os in progress
+ * (field 9) times the number of milliseconds spent doing I/O since the
+ * last update of this field. This can provide an easy measure of both
+ * I/O completion time and the backlog that may be accumulating.
+ */
+
+ num_parsed = sscanf (line,
+ "%d %d %127s"
+ " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT
+ " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT
+ " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT,
+ &dev_major, &dev_minor, dev_name,
+ &num_reads, &num_reads_merged, &num_sectors_read, &num_msec_reading,
+ &num_writes, &num_writes_merged, &num_sectors_written, &num_msec_writing,
+ &num_io_in_progress, &num_msec_doing_io, &weighted_num_msec_doing_io);
+ if (num_parsed != 14)
+ {
+ g_warning ("Error parsing line %d of file /proc/diskstats (num_parsed=%d): `%s'", n, num_parsed, line);
+ continue;
+ }
+
+ /* skip mapped devices and partitions... otherwise we'll count their
+ * I/O more than once
+ *
+ * TODO: the way we identify dm devices and partitions is not
+ * very elegant... we should consult sysfs via libgudev1
+ * instead.
+ */
+ if (dev_major == 253 /* device-mapper */
+ || dev_major == 9) /* md */
+ continue;
+
+ if ((g_str_has_prefix (dev_name, "sd")
+ || g_str_has_prefix (dev_name, "hd")
+ || g_str_has_prefix (dev_name, "vd"))
+ && g_ascii_isdigit (dev_name[strlen (dev_name) - 1]))
+ continue;
+
+ // ignore nvme partitions
+ if (g_str_has_prefix (dev_name, "nvme") && g_strrstr (dev_name, "p"))
+ continue;
+
+ bytes_read += num_sectors_read * 512;
+ bytes_written += num_sectors_written * 512;
+ cockpit_samples_sample (samples, "disk.dev.read", dev_name, num_sectors_read * 512);
+ cockpit_samples_sample (samples, "disk.dev.written", dev_name, num_sectors_written * 512);
+ }
+
+ cockpit_samples_sample (samples, "disk.all.read", NULL, bytes_read);
+ cockpit_samples_sample (samples, "disk.all.written", NULL, bytes_written);
+
+out:
+ g_strfreev (lines);
+ g_free (contents);
+}
+
+static FILE *
+open_file (int dirfd,
+ const char *name)
+{
+ const int fd = openat (dirfd, name, O_RDONLY);
+ if (fd < 0)
+ {
+ if (errno != EACCES && errno != ESRCH && errno != ENOENT)
+ g_message ("error opening file descriptor: %s: %m", name);
+ return NULL;
+ }
+
+ FILE *fp = fdopen (fd, "r");
+ if (!fp)
+ {
+ if (errno != ESRCH && errno != ENOENT)
+ g_message ("error opening file %s: %m", name);
+
+ close (fd);
+ return NULL;
+ }
+
+ return fp;
+}
+
+static void
+table_add_values(GHashTable *table,
+ const gchar *cgroup,
+ guint64 disk_read,
+ guint64 disk_write)
+{
+ cgroup_values_t *values = g_hash_table_lookup (table, cgroup);
+ if (values == NULL)
+ {
+ values = g_new0(cgroup_values_t, 1);
+ g_hash_table_insert (table, g_strdup (cgroup), values);
+ }
+
+ values->disk_read += disk_read;
+ values->disk_write += disk_write;
+}
+
+static void
+get_process_io (const int dirfd,
+ GHashTable *table)
+{
+ FILE *io_fp = open_file (dirfd, "io");
+ if (!io_fp)
+ return;
+
+ gchar *key;
+ guint64 disk_read = 0, disk_write = 0, value = 0;
+ while (fscanf (io_fp, "%m[^: ]: %" G_GUINT64_FORMAT "\n", &key, &value) == 2)
+ {
+ if (g_str_equal (key, "read_bytes"))
+ disk_read = value;
+ else if (g_str_equal (key, "write_bytes"))
+ disk_write = value;
+
+ free (key);
+ }
+
+ fclose (io_fp);
+
+ // get process cgroup
+ FILE *cgroup_fp = open_file (dirfd, "cgroup");
+ if (!cgroup_fp)
+ return;
+
+ gchar *cgroup = NULL;
+ if (fscanf (cgroup_fp, "%ms\n", &cgroup) != 1)
+ g_debug ("Failed to read cgroup name: %m");
+ else
+ table_add_values (table, cgroup, disk_read, disk_write);
+
+ g_free (cgroup);
+ fclose (cgroup_fp);
+}
+
+void
+cockpit_cgroup_disk_usage (CockpitSamples *samples)
+{
+ DIR *d = opendir ("/proc");
+ if (!d)
+ {
+ g_warning ("Error when opening /proc, %m");
+ return;
+ }
+ int proc_fd = dirfd (d);
+
+ GHashTable *table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+ struct dirent *de;
+ while ((de = readdir (d)))
+ {
+ // non-pid entries in proc are guaranteed to start with a character a-z
+ if (de->d_name[0] < '0' || '9' < de->d_name[0])
+ continue;
+
+ int dfd = openat(proc_fd, de->d_name, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (dfd < 0)
+ {
+ if (errno != ENOENT)
+ g_message ("failed to open /proc/%s, %m", de->d_name);
+ continue;
+ }
+ get_process_io (dfd, table);
+ close (dfd);
+ }
+
+ closedir (d);
+
+ GHashTableIter iter;
+ gpointer key, value;
+ g_hash_table_iter_init (&iter, table);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ const gchar *cgroup_name = key;
+ const cgroup_values_t *values = value;
+
+ // Skip ::0/
+ cgroup_name += 4;
+
+ cockpit_samples_sample ((CockpitSamples *)samples, "disk.cgroup.read", cgroup_name, values->disk_read);
+ cockpit_samples_sample ((CockpitSamples *)samples, "disk.cgroup.written", cgroup_name, values->disk_write);
+ }
+ g_hash_table_unref (table);
+}
diff --git a/src/bridge/cockpitdisksamples.h b/src/bridge/cockpitdisksamples.h
new file mode 100644
index 0000000..4171439
--- /dev/null
+++ b/src/bridge/cockpitdisksamples.h
@@ -0,0 +1,33 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_DISK_SAMPLES_H__
+#define COCKPIT_DISK_SAMPLES_H__
+
+#include "cockpitsamples.h"
+
+G_BEGIN_DECLS
+
+void cockpit_disk_samples (CockpitSamples *samples);
+
+void cockpit_cgroup_disk_usage (CockpitSamples *samples);
+
+G_END_DECLS
+
+#endif /* COCKPIT_DISK_SAMPLES_H__ */
diff --git a/src/bridge/cockpitechochannel.c b/src/bridge/cockpitechochannel.c
new file mode 100644
index 0000000..1039063
--- /dev/null
+++ b/src/bridge/cockpitechochannel.c
@@ -0,0 +1,89 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitechochannel.h"
+
+/**
+ * CockpitEchoChannel:
+ *
+ * A #CockpitChannel that never sends messages and ignores
+ * all messages it receives.
+ *
+ * The payload type for this channel is 'echo'.
+ */
+
+#define COCKPIT_ECHO_CHANNEL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_ECHO_CHANNEL, CockpitEchoChannel))
+
+typedef struct {
+ CockpitChannel parent;
+} CockpitEchoChannel;
+
+typedef struct {
+ CockpitChannelClass parent_class;
+} CockpitEchoChannelClass;
+
+G_DEFINE_TYPE (CockpitEchoChannel, cockpit_echo_channel, COCKPIT_TYPE_CHANNEL);
+
+static void
+cockpit_echo_channel_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ g_debug ("received echo channel payload");
+ cockpit_channel_send (channel, message, FALSE);
+}
+
+static gboolean
+cockpit_echo_channel_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *options)
+{
+ if (g_str_equal (command, "done"))
+ {
+ g_debug ("received echo channel done");
+ cockpit_channel_control (channel, command, options);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+cockpit_echo_channel_init (CockpitEchoChannel *self)
+{
+
+}
+
+static void
+cockpit_echo_channel_prepare (CockpitChannel *channel)
+{
+ COCKPIT_CHANNEL_CLASS (cockpit_echo_channel_parent_class)->prepare (channel);
+ cockpit_channel_ready (channel, NULL);
+}
+
+static void
+cockpit_echo_channel_class_init (CockpitEchoChannelClass *klass)
+{
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ channel_class->prepare = cockpit_echo_channel_prepare;
+ channel_class->control = cockpit_echo_channel_control;
+ channel_class->recv = cockpit_echo_channel_recv;
+}
diff --git a/src/bridge/cockpitechochannel.h b/src/bridge/cockpitechochannel.h
new file mode 100644
index 0000000..c8de014
--- /dev/null
+++ b/src/bridge/cockpitechochannel.h
@@ -0,0 +1,31 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_ECHO_CHANNEL_H__
+#define COCKPIT_ECHO_CHANNEL_H__
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_ECHO_CHANNEL (cockpit_echo_channel_get_type ())
+
+GType cockpit_echo_channel_get_type (void) G_GNUC_CONST;
+
+#endif /* COCKPIT_ECHO_CHANEL_H__ */
diff --git a/src/bridge/cockpitfslist.c b/src/bridge/cockpitfslist.c
new file mode 100644
index 0000000..012c2f8
--- /dev/null
+++ b/src/bridge/cockpitfslist.c
@@ -0,0 +1,358 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitfslist.h"
+#include "cockpitfswatch.h"
+
+#include "common/cockpitjson.h"
+
+#include <sys/wait.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <string.h>
+
+/**
+ * CockpitFslist:
+ *
+ * A #CockpitChannel that lists and optionally watches a directory.
+ *
+ * The payload type for this channel is 'fslist1'.
+ */
+
+#define COCKPIT_FSLIST(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_FSLIST, CockpitFslist))
+
+typedef struct {
+ CockpitChannel parent;
+ const gchar *path;
+ GFileMonitor *monitor;
+ guint sig_changed;
+ GCancellable *cancellable;
+} CockpitFslist;
+
+typedef struct {
+ CockpitChannelClass parent_class;
+} CockpitFslistClass;
+
+G_DEFINE_TYPE (CockpitFslist, cockpit_fslist, COCKPIT_TYPE_CHANNEL);
+
+static void
+cockpit_fslist_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ cockpit_channel_fail (channel, "protocol-error", "Received unexpected message in fslist1 channel");
+}
+
+static void
+cockpit_fslist_init (CockpitFslist *self)
+{
+}
+
+static const gchar *
+error_to_problem (GError *error)
+{
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED))
+ return "access-denied";
+ else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+ return "not-found";
+ else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY))
+ return "not-found";
+ else
+ return NULL;
+}
+
+static void
+on_files_listed (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ JsonObject *options;
+ GList *files;
+
+ files = g_file_enumerator_next_files_finish (G_FILE_ENUMERATOR (source_object), res, &error);
+ if (error)
+ {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ CockpitFslist *self = COCKPIT_FSLIST (user_data);
+ g_message ("%s: couldn't process files %s", COCKPIT_FSLIST(user_data)->path, error->message);
+ options = cockpit_channel_close_options (COCKPIT_CHANNEL (self));
+ json_object_set_string_member (options, "message", error->message);
+ cockpit_channel_close (COCKPIT_CHANNEL (self), "internal-error");
+ }
+ g_clear_error (&error);
+ goto out;
+ }
+
+ CockpitFslist *self = COCKPIT_FSLIST (user_data);
+
+ if (files == NULL)
+ {
+ g_clear_object (&self->cancellable);
+ g_object_unref (source_object);
+
+ cockpit_channel_ready (COCKPIT_CHANNEL (self), NULL);
+
+ if (self->monitor == NULL)
+ {
+ cockpit_channel_control (COCKPIT_CHANNEL (self), "done", NULL);
+ cockpit_channel_close (COCKPIT_CHANNEL (self), NULL);
+ }
+ goto out;
+ }
+
+ for (GList *l = files; l; l = l->next)
+ {
+ GFileInfo *info = G_FILE_INFO (l->data);
+ JsonObject *msg;
+ GBytes *msg_bytes;
+
+ msg = json_object_new ();
+ json_object_set_string_member (msg, "event", "present");
+ json_object_set_string_member
+ (msg, "path", g_file_info_get_attribute_byte_string (info, G_FILE_ATTRIBUTE_STANDARD_NAME));
+ json_object_set_string_member
+ (msg, "type", cockpit_file_type_to_string (g_file_info_get_file_type (info)));
+ json_object_set_string_member
+ (msg, "owner", g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_OWNER_USER));
+ json_object_set_string_member
+ (msg, "group", g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_OWNER_GROUP));
+ json_object_set_int_member
+ (msg, "size", g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_STANDARD_SIZE));
+ json_object_set_int_member
+ (msg, "modified", g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED));
+ msg_bytes = cockpit_json_write_bytes (msg);
+ json_object_unref (msg);
+ cockpit_channel_send (COCKPIT_CHANNEL(self), msg_bytes, FALSE);
+ g_bytes_unref (msg_bytes);
+ }
+
+ g_list_free_full (files, g_object_unref);
+
+ g_file_enumerator_next_files_async (G_FILE_ENUMERATOR (source_object),
+ 10,
+ G_PRIORITY_DEFAULT,
+ self->cancellable,
+ on_files_listed,
+ g_object_ref (self));
+out:
+ g_object_unref (user_data);
+}
+
+static void
+on_enumerator_ready (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ GFileEnumerator *enumerator;
+ JsonObject *options;
+ const gchar *problem;
+
+ enumerator = g_file_enumerate_children_finish (G_FILE (source_object), res, &error);
+ if (enumerator == NULL)
+ {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ CockpitFslist *self = COCKPIT_FSLIST (user_data);
+ problem = error_to_problem (error);
+ if (problem)
+ g_debug ("%s: couldn't list directory: %s", self->path, error->message);
+ else
+ g_warning ("%s: couldn't list directory: %s", self->path, error->message);
+ options = cockpit_channel_close_options (COCKPIT_CHANNEL (self));
+ json_object_set_string_member (options, "message", error->message);
+ cockpit_channel_close (COCKPIT_CHANNEL (self), problem ? problem : "internal-error");
+ }
+ g_clear_error (&error);
+ goto out;
+ }
+
+ CockpitFslist *self = COCKPIT_FSLIST (user_data);
+
+ g_file_enumerator_next_files_async (enumerator,
+ 10,
+ G_PRIORITY_DEFAULT,
+ self->cancellable,
+ on_files_listed,
+ g_object_ref (self));
+out:
+ g_object_unref (user_data);
+}
+
+static void
+on_changed (GFileMonitor *monitor,
+ GFile *file,
+ GFile *other_file,
+ GFileMonitorEvent event_type,
+ gpointer user_data)
+{
+ CockpitFslist *self = COCKPIT_FSLIST(user_data);
+ cockpit_fswatch_emit_event (COCKPIT_CHANNEL(self), file, other_file, event_type);
+}
+
+static void
+cockpit_fslist_prepare (CockpitChannel *channel)
+{
+ CockpitFslist *self = COCKPIT_FSLIST (channel);
+ JsonObject *options;
+ GError *error = NULL;
+ GFile *file = NULL;
+ gboolean watch;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_fslist_parent_class)->prepare (channel);
+
+ options = cockpit_channel_get_options (channel);
+ if (!cockpit_json_get_string (options, "path", NULL, &self->path))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"path\" option for fslist1 channel");
+ goto out;
+ }
+ if (self->path == NULL || *(self->path) == 0)
+ {
+ cockpit_channel_fail (channel, "protocol-error", "missing \"path\" option for fslist1 channel");
+ goto out;
+ }
+
+ if (!cockpit_json_get_bool (options, "watch", TRUE, &watch))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"watch\" option for fslist1 channel");
+ goto out;
+ }
+
+ self->cancellable = g_cancellable_new ();
+
+ file = g_file_new_for_path (self->path);
+
+ if (watch)
+ {
+ self->monitor = g_file_monitor_directory (file, 0, NULL, &error);
+ if (self->monitor == NULL)
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "%s: couldn't monitor directory: %s", self->path, error->message);
+ goto out;
+ }
+
+ self->sig_changed = g_signal_connect (self->monitor, "changed", G_CALLBACK (on_changed), self);
+ }
+
+ g_file_enumerate_children_async (file,
+ G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE ","
+ G_FILE_ATTRIBUTE_OWNER_USER "," G_FILE_ATTRIBUTE_OWNER_GROUP ","
+ G_FILE_ATTRIBUTE_STANDARD_SIZE "," G_FILE_ATTRIBUTE_TIME_MODIFIED,
+ G_FILE_QUERY_INFO_NONE,
+ G_PRIORITY_DEFAULT,
+ self->cancellable,
+ on_enumerator_ready,
+ g_object_ref (self));
+out:
+ g_clear_error (&error);
+ if (file)
+ g_object_unref (file);
+}
+
+static void
+cockpit_fslist_dispose (GObject *object)
+{
+ CockpitFslist *self = COCKPIT_FSLIST (object);
+
+ if (self->cancellable)
+ g_cancellable_cancel (self->cancellable);
+
+ if (self->monitor)
+ {
+ if (self->sig_changed)
+ g_signal_handler_disconnect (self->monitor, self->sig_changed);
+ self->sig_changed = 0;
+
+ /* HACK - It is not generally safe to just unref a GFileMonitor:
+ * https://gitlab.gnome.org/GNOME/glib/issues/1941
+ */
+ g_file_monitor_cancel (self->monitor);
+ }
+
+ G_OBJECT_CLASS (cockpit_fslist_parent_class)->dispose (object);
+}
+
+static void
+cockpit_fslist_finalize (GObject *object)
+{
+ CockpitFslist *self = COCKPIT_FSLIST (object);
+
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->monitor);
+
+ G_OBJECT_CLASS (cockpit_fslist_parent_class)->finalize (object);
+}
+
+static void
+cockpit_fslist_class_init (CockpitFslistClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->dispose = cockpit_fslist_dispose;
+ gobject_class->finalize = cockpit_fslist_finalize;
+
+ channel_class->prepare = cockpit_fslist_prepare;
+ channel_class->recv = cockpit_fslist_recv;
+}
+
+/**
+ * cockpit_fslist_open:
+ * @transport: the transport to send/receive messages on
+ * @channel_id: the channel id
+ * @path: the path name of the file to read
+ * @watch: boolean, watch the directory as well?
+ *
+ * This function is mainly used by tests. The usual way
+ * to get a #CockpitFslist is via cockpit_channel_open()
+ *
+ * Returns: (transfer full): the new channel
+ */
+CockpitChannel *
+cockpit_fslist_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *path,
+ const gboolean watch)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+
+ g_return_val_if_fail (channel_id != NULL, NULL);
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "path", path);
+ json_object_set_string_member (options, "payload", "fslist1");
+ json_object_set_boolean_member (options, "watch", watch);
+
+ channel = g_object_new (COCKPIT_TYPE_FSLIST,
+ "transport", transport,
+ "id", channel_id,
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+ return channel;
+}
diff --git a/src/bridge/cockpitfslist.h b/src/bridge/cockpitfslist.h
new file mode 100644
index 0000000..19dbbf7
--- /dev/null
+++ b/src/bridge/cockpitfslist.h
@@ -0,0 +1,38 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_FSLIST_H__
+#define COCKPIT_FSLIST_H__
+
+#include <gio/gio.h>
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_FSLIST (cockpit_fslist_get_type ())
+
+GType cockpit_fslist_get_type (void) G_GNUC_CONST;
+
+CockpitChannel * cockpit_fslist_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *path,
+ const gboolean watch);
+
+#endif /* COCKPIT_FSLIST_H__ */
diff --git a/src/bridge/cockpitfsread.c b/src/bridge/cockpitfsread.c
new file mode 100644
index 0000000..25558fc
--- /dev/null
+++ b/src/bridge/cockpitfsread.c
@@ -0,0 +1,382 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitfsread.h"
+
+#include "common/cockpitflow.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitpipe.h"
+
+#include <sys/wait.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <string.h>
+
+#define DEFAULT_MAX_READ_SIZE (16*1024*1024)
+
+/**
+ * CockpitFsread:
+ *
+ * A #CockpitChannel that reads the content of a file.
+ *
+ * The payload type for this channel is 'fsread1'.
+ */
+
+#define COCKPIT_FSREAD(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_FSREAD, CockpitFsread))
+
+typedef struct {
+ CockpitChannel parent;
+ const gchar *path;
+ gchar *start_tag;
+ int fd;
+
+ CockpitPipe *pipe;
+ gboolean open;
+ gboolean closing;
+ guint sig_read;
+ guint sig_close;
+} CockpitFsread;
+
+typedef struct {
+ CockpitChannelClass parent_class;
+} CockpitFsreadClass;
+
+G_DEFINE_TYPE (CockpitFsread, cockpit_fsread, COCKPIT_TYPE_CHANNEL);
+
+static void
+cockpit_fsread_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ cockpit_channel_fail (channel, "protocol-error", "received unexpected message in fsread channel");
+}
+
+static gchar *
+file_tag_from_stat (int res,
+ int err,
+ struct stat *buf)
+{
+ // The transaction tag is the inode and mtime of the file. Mtime is
+ // used to catch in-place modifications, and the inode to catch
+ // renames.
+
+ if (res >= 0)
+ return g_strdup_printf ("1:%lu-%lld.%ld",
+ (unsigned long)buf->st_ino,
+ (long long int)buf->st_mtim.tv_sec,
+ (long int)buf->st_mtim.tv_nsec);
+ else if (err == ENOENT)
+ return g_strdup ("-");
+ else
+ return NULL;
+}
+
+gchar *
+cockpit_get_file_tag (const gchar *path)
+{
+ struct stat buf;
+ int res = stat (path, &buf);
+ return file_tag_from_stat (res, errno, &buf);
+}
+
+gchar *
+cockpit_get_file_tag_from_fd (int fd)
+{
+ struct stat buf;
+ int res = fstat (fd, &buf);
+ return file_tag_from_stat (res, errno, &buf);
+}
+
+static void
+cockpit_fsread_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ CockpitFsread *self = COCKPIT_FSREAD (channel);
+
+ self->closing = TRUE;
+
+ /*
+ * If closed, call base class handler directly. Otherwise ask
+ * our pipe to close first, which will come back here.
+ */
+ if (self->open)
+ cockpit_pipe_close (self->pipe, problem);
+ else
+ COCKPIT_CHANNEL_CLASS (cockpit_fsread_parent_class)->close (channel, problem);
+}
+
+static void
+cockpit_fsread_init (CockpitFsread *self)
+{
+ self->fd = -1;
+}
+
+static void
+on_pipe_read (CockpitPipe *pipe,
+ GByteArray *data,
+ gboolean end_of_data,
+ gpointer user_data)
+{
+ CockpitFsread *self = user_data;
+ CockpitChannel *channel = user_data;
+ const gchar *problem;
+ JsonObject *options;
+ GBytes *message;
+ gchar *tag;
+
+ if (data->len)
+ {
+ /* When array is reffed, this just clears byte array */
+ g_byte_array_ref (data);
+ message = g_byte_array_free_to_bytes (data);
+ cockpit_channel_send (channel, message, FALSE);
+ g_bytes_unref (message);
+ }
+
+ if (end_of_data)
+ {
+ cockpit_channel_control (channel, "done", NULL);
+
+ problem = NULL;
+ if (self->fd >= 0 && self->start_tag)
+ {
+ tag = cockpit_get_file_tag_from_fd (self->fd);
+ if (g_strcmp0 (tag, self->start_tag) == 0)
+ {
+ options = cockpit_channel_close_options (channel);
+ json_object_set_string_member (options, "tag", tag);
+ }
+ else
+ {
+ problem = "change-conflict";
+ }
+ g_free (tag);
+ }
+
+ cockpit_channel_close (channel, problem);
+ }
+}
+
+static void
+on_pipe_close (CockpitPipe *pipe,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitFsread *self = COCKPIT_FSREAD (user_data);
+ CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
+
+ self->open = FALSE;
+ cockpit_channel_close (channel, problem);
+}
+
+static void
+cockpit_fsread_prepare (CockpitChannel *channel)
+{
+ CockpitFsread *self = COCKPIT_FSREAD (channel);
+ JsonObject *options;
+ gint64 max_read_size;
+ struct stat statbuf;
+ mode_t ifmt;
+ int fd;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_fsread_parent_class)->prepare (channel);
+
+ options = cockpit_channel_get_options (channel);
+
+ if (!cockpit_json_get_string (options, "path", NULL, &self->path))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"path\" option for fsread channel");
+ return;
+ }
+ if (self->path == NULL || *(self->path) == 0)
+ {
+ cockpit_channel_fail (channel, "protocol-error", "missing \"path\" option for fsread channel");
+ return;
+ }
+
+ if (!cockpit_json_get_int (options, "max_read_size", DEFAULT_MAX_READ_SIZE, &max_read_size))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"max_read_size\" option for fsread channel");
+ return;
+ }
+
+ if (self->closing)
+ return;
+
+ fd = open (self->path, O_RDONLY);
+ if (fd < 0)
+ {
+ int err = errno;
+ if (err == ENOENT)
+ {
+ options = cockpit_channel_close_options (channel);
+ json_object_set_string_member (options, "tag", "-");
+ cockpit_channel_close (channel, NULL);
+ }
+ else
+ {
+ if (err == EPERM || err == EACCES)
+ {
+ g_debug ("%s: couldn't open: %s", self->path, strerror (err));
+ cockpit_channel_close (channel, "access-denied");
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "%s: couldn't open: %s", self->path, strerror (err));
+ }
+ }
+ goto out;
+ }
+
+ if (fstat (fd, &statbuf) < 0)
+ {
+ cockpit_channel_fail (channel, "internal-error", "%s: couldn't stat: %s", self->path, strerror (errno));
+ goto out;
+ }
+
+ ifmt = (statbuf.st_mode & S_IFMT);
+ if (ifmt != S_IFREG && ifmt != S_IFBLK)
+ {
+ cockpit_channel_fail (channel, "internal-error", "%s: not a readable file", self->path);
+ goto out;
+ }
+ if (ifmt == S_IFREG && statbuf.st_size > max_read_size)
+ {
+ cockpit_channel_close (channel, "too-large");
+ goto out;
+ }
+
+ /* This owns the file descriptor */
+ self->pipe = cockpit_pipe_new (self->path, fd, -1);
+ self->fd = fd;
+ fd = -1;
+
+ self->start_tag = cockpit_get_file_tag_from_fd (self->fd);
+
+ /* Let the channel throttle the pipe's input flow*/
+ cockpit_flow_throttle (COCKPIT_FLOW (self->pipe), COCKPIT_FLOW (self));
+
+ /* Let the pipe input the channel peer's output flow */
+ cockpit_flow_throttle (COCKPIT_FLOW (channel), COCKPIT_FLOW (self->pipe));
+
+ self->sig_read = g_signal_connect (self->pipe, "read", G_CALLBACK (on_pipe_read), self);
+ self->sig_close = g_signal_connect (self->pipe, "close", G_CALLBACK (on_pipe_close), self);
+
+ const gchar *binary;
+ if (S_ISREG(statbuf.st_mode) && cockpit_json_get_string (options, "binary", "", &binary) && g_str_equal (binary, "raw"))
+ {
+ g_autoptr(JsonObject) message = json_object_new ();
+ json_object_set_int_member (message, "size-hint", statbuf.st_size);
+ cockpit_channel_ready (channel, message);
+ } else {
+ cockpit_channel_ready (channel, NULL);
+ }
+
+
+out:
+ if (fd >= 0)
+ close (fd);
+}
+
+static void
+cockpit_fsread_dispose (GObject *object)
+{
+ CockpitFsread *self = COCKPIT_FSREAD (object);
+
+ if (self->pipe)
+ {
+ if (self->open)
+ cockpit_pipe_close (self->pipe, "terminated");
+ if (self->sig_read)
+ g_signal_handler_disconnect (self->pipe, self->sig_read);
+ if (self->sig_close)
+ g_signal_handler_disconnect (self->pipe, self->sig_close);
+ self->sig_read = self->sig_close = 0;
+ }
+
+ G_OBJECT_CLASS (cockpit_fsread_parent_class)->dispose (object);
+}
+
+static void
+cockpit_fsread_finalize (GObject *object)
+{
+ CockpitFsread *self = COCKPIT_FSREAD (object);
+
+ g_free (self->start_tag);
+ g_clear_object (&self->pipe);
+
+ G_OBJECT_CLASS (cockpit_fsread_parent_class)->finalize (object);
+}
+
+static void
+cockpit_fsread_class_init (CockpitFsreadClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->dispose = cockpit_fsread_dispose;
+ gobject_class->finalize = cockpit_fsread_finalize;
+
+ channel_class->prepare = cockpit_fsread_prepare;
+ channel_class->recv = cockpit_fsread_recv;
+ channel_class->close = cockpit_fsread_close;
+}
+
+/**
+ * cockpit_fsread_open:
+ * @transport: the transport to send/receive messages on
+ * @channel_id: the channel id
+ * @path: the path name of the file to read
+ * @binary: set binary to "raw"
+ *
+ * This function is mainly used by tests. The usual way
+ * to get a #CockpitFsread is via cockpit_channel_open()
+ *
+ * Returns: (transfer full): the new channel
+ */
+CockpitChannel *
+cockpit_fsread_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *path,
+ gboolean binary)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+
+ g_return_val_if_fail (channel_id != NULL, NULL);
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "path", path);
+ json_object_set_string_member (options, "payload", "fsread1");
+ if (binary)
+ json_object_set_string_member (options, "binary", "raw");
+
+ channel = g_object_new (COCKPIT_TYPE_FSREAD,
+ "transport", transport,
+ "id", channel_id,
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+ return channel;
+}
diff --git a/src/bridge/cockpitfsread.h b/src/bridge/cockpitfsread.h
new file mode 100644
index 0000000..83160c1
--- /dev/null
+++ b/src/bridge/cockpitfsread.h
@@ -0,0 +1,42 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_FSREAD_H__
+#define COCKPIT_FSREAD_H__
+
+#include <gio/gio.h>
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_FSREAD (cockpit_fsread_get_type ())
+
+GType cockpit_fsread_get_type (void) G_GNUC_CONST;
+
+CockpitChannel * cockpit_fsread_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *path,
+ gboolean binary);
+
+gchar * cockpit_get_file_tag (const gchar *path);
+
+gchar * cockpit_get_file_tag_from_fd (int fd);
+
+#endif /* COCKPIT_FSREAD_H__ */
diff --git a/src/bridge/cockpitfsreplace.c b/src/bridge/cockpitfsreplace.c
new file mode 100644
index 0000000..bde3ee1
--- /dev/null
+++ b/src/bridge/cockpitfsreplace.c
@@ -0,0 +1,347 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitfsreplace.h"
+#include "cockpitfsread.h"
+
+#include "common/cockpitjson.h"
+
+#include <sys/wait.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+
+/**
+ * CockpitFsreplace:
+ *
+ * A #CockpitChannel that writes/replaces the content of a file.
+ *
+ * The payload type for this channel is 'fsreplace1'.
+ */
+
+#define COCKPIT_FSREPLACE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_FSREPLACE, CockpitFsreplace))
+
+typedef struct {
+ CockpitChannel parent;
+ const gchar *path;
+ gchar *tmp_path;
+ int fd;
+ gboolean got_content;
+ const gchar *expected_tag;
+ guint sig_close;
+} CockpitFsreplace;
+
+typedef struct {
+ CockpitChannelClass parent_class;
+} CockpitFsreplaceClass;
+
+G_DEFINE_TYPE (CockpitFsreplace, cockpit_fsreplace, COCKPIT_TYPE_CHANNEL);
+
+static void
+close_with_errno (CockpitFsreplace *self,
+ const gchar *diagnostic,
+ int err)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+
+ if (err == EPERM || err == EACCES)
+ {
+ g_debug ("%s: %s: %s", self->path, diagnostic, strerror (err));
+ cockpit_channel_close (channel, "access-denied");
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "%s: %s: %s", self->path, diagnostic, strerror (err));
+ }
+}
+
+static void
+cockpit_fsreplace_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ CockpitFsreplace *self = COCKPIT_FSREPLACE (channel);
+ gsize size;
+ const char *data = g_bytes_get_data (message, &size);
+
+ self->got_content = TRUE;
+
+ while (size > 0)
+ {
+ ssize_t n = write (self->fd, data, size);
+ if (n < 0)
+ {
+ if (errno == EINTR)
+ continue;
+
+ close_with_errno (self, "couldn't write", errno);
+ return;
+ }
+
+ g_return_if_fail (n > 0);
+ size -= n;
+ data += n;
+ }
+}
+
+static int
+xfsync (int fd)
+{
+ while (TRUE)
+ {
+ int res = fsync (fd);
+ if (res < 0 && errno == EINTR)
+ continue;
+
+ return res;
+ }
+}
+
+static int
+xclose (int fd)
+{
+ /* http://lkml.indiana.edu/hypermail/linux/kernel/0509.1/0877.html
+ */
+ int res = close (fd);
+ if (res < 0 && errno == EINTR)
+ return 0;
+ else
+ return res;
+}
+
+static gboolean
+cockpit_fsreplace_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *unused)
+{
+ CockpitFsreplace *self = COCKPIT_FSREPLACE (channel);
+ gchar *actual_tag = NULL;
+ gchar *new_tag = NULL;
+ JsonObject *options;
+
+ if (!g_str_equal (command, "done"))
+ return FALSE;
+
+ g_object_ref (self);
+
+ /* Commit the changes when there was no problem */
+ if (xfsync (self->fd) < 0 || xclose (self->fd) < 0)
+ {
+ close_with_errno (self, "couldn't sync", errno);
+ goto out;
+ }
+ else
+ {
+ actual_tag = cockpit_get_file_tag (self->path);
+ if (self->expected_tag && g_strcmp0 (self->expected_tag, actual_tag))
+ {
+ cockpit_channel_close (channel, "out-of-date");
+ goto out;
+ }
+ else
+ {
+ options = cockpit_channel_close_options (channel);
+ if (!self->got_content)
+ {
+ json_object_set_string_member (options, "tag", "-");
+ if (unlink (self->tmp_path) < 0)
+ g_message ("%s: couldn't remove temp file: %s", self->tmp_path, g_strerror (errno));
+ if (unlink (self->path) < 0 && errno != ENOENT)
+ {
+ close_with_errno (self, "couldn't unlink", errno);
+ goto out;
+ }
+ }
+ else
+ {
+ new_tag = cockpit_get_file_tag (self->tmp_path);
+ json_object_set_string_member (options, "tag", new_tag);
+ if (rename (self->tmp_path, self->path) < 0)
+ {
+ close_with_errno (self, "couldn't rename", errno);
+ goto out;
+ }
+ }
+ }
+ }
+
+ cockpit_channel_close (channel, NULL);
+
+out:
+ g_free (new_tag);
+ g_free (actual_tag);
+ self->fd = -1;
+ g_object_unref (self);
+ return TRUE;
+}
+
+static void
+cockpit_fsreplace_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ CockpitFsreplace *self = COCKPIT_FSREPLACE (channel);
+
+ if (self->fd != -1)
+ close (self->fd);
+ self->fd = -1;
+
+ /* Cleanup in case of problem */
+ if (problem)
+ {
+ if (self->tmp_path)
+ if (unlink (self->tmp_path) < 0 && errno != ENOENT)
+ g_message ("%s: couldn't remove temp file: %s", self->tmp_path, g_strerror (errno));
+ }
+
+ COCKPIT_CHANNEL_CLASS (cockpit_fsreplace_parent_class)->close (channel, problem);
+}
+
+static void
+cockpit_fsreplace_init (CockpitFsreplace *self)
+{
+ self->fd = -1;
+}
+
+static void
+cockpit_fsreplace_prepare (CockpitChannel *channel)
+{
+ CockpitFsreplace *self = COCKPIT_FSREPLACE (channel);
+ JsonObject *options;
+ gchar *actual_tag = NULL;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_fsreplace_parent_class)->prepare (channel);
+
+ options = cockpit_channel_get_options (channel);
+ if (!cockpit_json_get_string (options, "path", NULL, &self->path))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"path\" option for fsreplace1 channel");
+ goto out;
+ }
+ else if (self->path == NULL || g_str_equal (self->path, ""))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "missing \"path\" option for fsreplace1 channel");
+ goto out;
+ }
+
+ if (!cockpit_json_get_string (options, "tag", NULL, &self->expected_tag))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"tag\" option for fsreplace1 channel", self->path);
+ goto out;
+ }
+
+ actual_tag = cockpit_get_file_tag (self->path);
+ if (self->expected_tag && g_strcmp0 (self->expected_tag, actual_tag))
+ {
+ cockpit_channel_close (channel, "change-conflict");
+ goto out;
+ }
+
+ // TODO - delay the opening until the first content message. That
+ // way, we don't create a useless temporary file (which might even
+ // fail).
+
+ for (int i = 1; i < 10000; i++)
+ {
+ self->tmp_path = g_strdup_printf ("%s.%d", self->path, i);
+ self->fd = open (self->tmp_path, O_WRONLY | O_CREAT | O_EXCL, 0666);
+ if (self->fd >= 0 || errno != EEXIST)
+ break;
+ g_free (self->tmp_path);
+ self->tmp_path = NULL;
+ }
+
+ if (self->fd < 0)
+ close_with_errno (self, "couldn't open unique file", errno);
+ else
+ cockpit_channel_ready (channel, NULL);
+
+out:
+ g_free (actual_tag);
+}
+
+static void
+cockpit_fsreplace_finalize (GObject *object)
+{
+ CockpitFsreplace *self = COCKPIT_FSREPLACE (object);
+
+ g_free (self->tmp_path);
+
+ G_OBJECT_CLASS (cockpit_fsreplace_parent_class)->finalize (object);
+}
+
+static void
+cockpit_fsreplace_class_init (CockpitFsreplaceClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->finalize = cockpit_fsreplace_finalize;
+
+ channel_class->prepare = cockpit_fsreplace_prepare;
+ channel_class->control = cockpit_fsreplace_control;
+ channel_class->recv = cockpit_fsreplace_recv;
+ channel_class->close = cockpit_fsreplace_close;
+}
+
+/**
+ * cockpit_fsreplace_open:
+ * @transport: the transport to send/receive messages on
+ * @channel_id: the channel id
+ * @path: the path name of the file to write
+ * @tag: the expected tag, or NULL
+ *
+ * This function is mainly used by tests. The usual way
+ * to get a #CockpitFsreplace is via cockpit_channel_open()
+ *
+ * Returns: (transfer full): the new channel
+ */
+CockpitChannel *
+cockpit_fsreplace_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *path,
+ const gchar *tag)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+
+ g_return_val_if_fail (channel_id != NULL, NULL);
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "path", path);
+ if (tag)
+ json_object_set_string_member (options, "tag", tag);
+ json_object_set_string_member (options, "payload", "fsreplace1");
+
+ channel = g_object_new (COCKPIT_TYPE_FSREPLACE,
+ "transport", transport,
+ "id", channel_id,
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+ return channel;
+}
diff --git a/src/bridge/cockpitfsreplace.h b/src/bridge/cockpitfsreplace.h
new file mode 100644
index 0000000..1e5e3b8
--- /dev/null
+++ b/src/bridge/cockpitfsreplace.h
@@ -0,0 +1,38 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_FSREPLACE_H__
+#define COCKPIT_FSREPLACE_H__
+
+#include <gio/gio.h>
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_FSREPLACE (cockpit_fsreplace_get_type ())
+
+GType cockpit_fsreplace_get_type (void) G_GNUC_CONST;
+
+CockpitChannel * cockpit_fsreplace_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *path,
+ const gchar *tag);
+
+#endif /* COCKPIT_FSREPLACE_H__ */
diff --git a/src/bridge/cockpitfswatch.c b/src/bridge/cockpitfswatch.c
new file mode 100644
index 0000000..3d39165
--- /dev/null
+++ b/src/bridge/cockpitfswatch.c
@@ -0,0 +1,294 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitfswatch.h"
+#include "cockpitfsread.h"
+
+#include "common/cockpitjson.h"
+
+#include <sys/wait.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <string.h>
+
+/**
+ * CockpitFswatch:
+ *
+ * A #CockpitChannel that watches a file or directory.
+ *
+ * The payload type for this channel is 'fswatch1'.
+ */
+
+#define COCKPIT_FSWATCH(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_FSWATCH, CockpitFswatch))
+
+typedef struct {
+ CockpitChannel parent;
+ const gchar *path;
+ GFileMonitor *monitor;
+ guint sig_changed;
+} CockpitFswatch;
+
+typedef struct {
+ CockpitChannelClass parent_class;
+} CockpitFswatchClass;
+
+G_DEFINE_TYPE (CockpitFswatch, cockpit_fswatch, COCKPIT_TYPE_CHANNEL);
+
+static void
+cockpit_fswatch_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ cockpit_channel_fail (channel, "protocol-error",
+ "received unexpected message in fswatch channel");
+}
+
+static void
+cockpit_fswatch_init (CockpitFswatch *self)
+{
+}
+
+gchar *
+cockpit_file_type_to_string (GFileType file_type)
+{
+ switch (file_type) {
+ case G_FILE_TYPE_REGULAR:
+ return "file";
+ case G_FILE_TYPE_DIRECTORY:
+ return "directory";
+ case G_FILE_TYPE_SYMBOLIC_LINK:
+ return "link";
+ case G_FILE_TYPE_SPECIAL:
+ return "special";
+ default:
+ return "unknown";
+ }
+}
+
+static gchar *
+event_type_to_string (GFileMonitorEvent event_type)
+{
+ switch (event_type) {
+ case G_FILE_MONITOR_EVENT_CHANGED:
+ return "changed";
+ case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
+ return "done-hint";
+ case G_FILE_MONITOR_EVENT_DELETED:
+ return "deleted";
+ case G_FILE_MONITOR_EVENT_CREATED:
+ return "created";
+ case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
+ return "attribute-changed";
+ case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
+ return "pre-unmount";
+ case G_FILE_MONITOR_EVENT_UNMOUNTED:
+ return "unmounted";
+ case G_FILE_MONITOR_EVENT_MOVED:
+ return "moved";
+ default:
+ return "unknown";
+ }
+}
+
+void
+cockpit_fswatch_emit_event (CockpitChannel *channel,
+ GFile *file,
+ GFile *other_file,
+ GFileMonitorEvent event_type)
+{
+ JsonObject *msg;
+ GBytes *msg_bytes;
+
+ msg = json_object_new ();
+ json_object_set_string_member (msg, "event", event_type_to_string (event_type));
+ if (file)
+ {
+ char *p = g_file_get_path (file);
+ char *t = cockpit_get_file_tag (p);
+ json_object_set_string_member (msg, "path", p);
+ json_object_set_string_member (msg, "tag", t);
+ if (event_type == G_FILE_MONITOR_EVENT_CREATED)
+ {
+ GError *error = NULL;
+ GFileInfo *info = g_file_query_info (file,
+ G_FILE_ATTRIBUTE_STANDARD_TYPE,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+ NULL, &error);
+ if (info)
+ {
+ json_object_set_string_member
+ (msg, "type", cockpit_file_type_to_string (g_file_info_get_file_type (info)));
+ g_object_unref (info);
+ }
+
+ g_clear_error (&error);
+ }
+
+ g_free (p);
+ g_free (t);
+ }
+ if (other_file)
+ {
+ char *p = g_file_get_path (other_file);
+ json_object_set_string_member (msg, "other", p);
+ g_free (p);
+ }
+ msg_bytes = cockpit_json_write_bytes (msg);
+ json_object_unref (msg);
+ cockpit_channel_send (channel, msg_bytes, TRUE);
+ g_bytes_unref (msg_bytes);
+}
+
+static void
+on_changed (GFileMonitor *monitor,
+ GFile *file,
+ GFile *other_file,
+ GFileMonitorEvent event_type,
+ gpointer user_data)
+{
+ CockpitFswatch *self = COCKPIT_FSWATCH (user_data);
+ cockpit_fswatch_emit_event (COCKPIT_CHANNEL(self), file, other_file, event_type);
+}
+
+static void
+cockpit_fswatch_prepare (CockpitChannel *channel)
+{
+ CockpitFswatch *self = COCKPIT_FSWATCH (channel);
+ JsonObject *options;
+ GError *error = NULL;
+ const gchar *path;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_fswatch_parent_class)->prepare (channel);
+
+ options = cockpit_channel_get_options (channel);
+ if (!cockpit_json_get_string (options, "path", NULL, &path))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"path\" option for fswatch channel");
+ goto out;
+ }
+ else if (path == NULL || *path == 0)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "missing \"path\" option for fswatch channel");
+ goto out;
+ }
+
+ GFile *file = g_file_new_for_path (path);
+ GFileMonitor *monitor = g_file_monitor (file, 0, NULL, &error);
+ g_object_unref (file);
+
+ if (monitor == NULL)
+ {
+ cockpit_channel_fail (channel, "internal-error", "%s: %s", self->path, error->message);
+ goto out;
+ }
+
+ self->monitor = monitor;
+ self->sig_changed = g_signal_connect (self->monitor, "changed", G_CALLBACK (on_changed), self);
+
+ cockpit_channel_ready (channel, NULL);
+
+out:
+ g_clear_error (&error);
+}
+
+static void
+cockpit_fswatch_dispose (GObject *object)
+{
+ CockpitFswatch *self = COCKPIT_FSWATCH (object);
+
+ if (self->monitor)
+ {
+ if (self->sig_changed)
+ g_signal_handler_disconnect (self->monitor, self->sig_changed);
+ self->sig_changed = 0;
+
+ /* HACK - It is not generally safe to just unref a GFileMonitor:
+ * https://gitlab.gnome.org/GNOME/glib/issues/1941
+ */
+ g_file_monitor_cancel (self->monitor);
+
+ g_object_unref (self->monitor);
+ self->monitor = NULL;
+ }
+
+ G_OBJECT_CLASS (cockpit_fswatch_parent_class)->dispose (object);
+}
+
+static void
+cockpit_fswatch_finalize (GObject *object)
+{
+ CockpitFswatch *self = COCKPIT_FSWATCH (object);
+
+ g_clear_object (&self->monitor);
+
+ G_OBJECT_CLASS (cockpit_fswatch_parent_class)->finalize (object);
+}
+
+static void
+cockpit_fswatch_class_init (CockpitFswatchClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->dispose = cockpit_fswatch_dispose;
+ gobject_class->finalize = cockpit_fswatch_finalize;
+
+ channel_class->prepare = cockpit_fswatch_prepare;
+ channel_class->recv = cockpit_fswatch_recv;
+}
+
+/**
+ * cockpit_fswatch_open:
+ * @transport: the transport to send/receive messages on
+ * @channel_id: the channel id
+ * @path: the path name of the file to read
+ *
+ * This function is mainly used by tests. The usual way
+ * to get a #CockpitFswatch is via cockpit_channel_open()
+ *
+ * Returns: (transfer full): the new channel
+ */
+CockpitChannel *
+cockpit_fswatch_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *path)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+
+ g_return_val_if_fail (channel_id != NULL, NULL);
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "path", path);
+ json_object_set_string_member (options, "payload", "fswatch1");
+
+ channel = g_object_new (COCKPIT_TYPE_FSWATCH,
+ "transport", transport,
+ "id", channel_id,
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+ return channel;
+}
diff --git a/src/bridge/cockpitfswatch.h b/src/bridge/cockpitfswatch.h
new file mode 100644
index 0000000..87e6332
--- /dev/null
+++ b/src/bridge/cockpitfswatch.h
@@ -0,0 +1,45 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_FSWATCH_H__
+#define COCKPIT_FSWATCH_H__
+
+#include <gio/gio.h>
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_FSWATCH (cockpit_fswatch_get_type ())
+
+GType cockpit_fswatch_get_type (void) G_GNUC_CONST;
+
+CockpitChannel * cockpit_fswatch_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *path);
+
+gchar * cockpit_file_type_to_string (GFileType file_type);
+
+void
+cockpit_fswatch_emit_event (CockpitChannel *channel,
+ GFile *file,
+ GFile *other_file,
+ GFileMonitorEvent event_type);
+
+#endif /* COCKPIT_FSWATCH_H__ */
diff --git a/src/bridge/cockpithttpstream.c b/src/bridge/cockpithttpstream.c
new file mode 100644
index 0000000..d0faced
--- /dev/null
+++ b/src/bridge/cockpithttpstream.c
@@ -0,0 +1,1103 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpithttpstream.h"
+
+#include "cockpitpackages.h"
+#include "cockpitconnect.h"
+#include "cockpitstream.h"
+
+#include "common/cockpitflow.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitpipe.h"
+#include "common/cockpitwebresponse.h"
+
+#include "websocket/websocket.h"
+
+#include <gio/gunixsocketaddress.h>
+
+#include <string.h>
+
+/**
+ * CockpitHttpClient
+ *
+ * Information about a certain set of HTTP connections that
+ * have been given a connection name, grouping them together as
+ * a client. In this mode we cache connections and reuse them
+ * as well as share options and address info.
+ */
+
+typedef struct {
+ gint refs;
+ gchar *name;
+ gchar *hostname;
+ CockpitStream *stream;
+ gulong sig_close;
+ guint timeout;
+} CockpitHttpClient;
+
+static GHashTable *clients;
+
+static void
+cockpit_http_client_reset (CockpitHttpClient *client)
+{
+ if (client->timeout)
+ g_source_remove (client->timeout);
+ if (client->stream)
+ {
+ g_signal_handler_disconnect (client->stream, client->sig_close);
+ g_object_unref (client->stream);
+ }
+
+ client->sig_close = 0;
+ client->stream = NULL;
+ client->timeout = 0;
+}
+
+static void
+cockpit_http_client_unref (gpointer data)
+{
+ CockpitHttpClient *client = data;
+ if (--client->refs == 0)
+ {
+ cockpit_http_client_reset (client);
+ g_free (client->hostname);
+ g_free (client->name);
+ g_slice_free (CockpitHttpClient, client);
+ }
+}
+
+static CockpitHttpClient *
+cockpit_http_client_ref (CockpitHttpClient *client)
+{
+ client->refs++;
+ return client;
+}
+
+static void
+on_client_close (CockpitStream *stream,
+ const gchar *problem,
+ gpointer data)
+{
+ CockpitHttpClient *client = data;
+ g_debug ("%s: connection closed", client->name);
+ cockpit_http_client_reset (client);
+}
+
+static gboolean
+on_client_timeout (gpointer data)
+{
+ CockpitHttpClient *client = data;
+ g_debug ("%s: connection timed out", client->name);
+ cockpit_http_client_reset (client);
+ return FALSE;
+}
+
+static CockpitHttpClient *
+cockpit_http_client_ensure (const gchar *name)
+{
+ CockpitHttpClient *client = NULL;
+
+ if (name)
+ {
+ if (!clients)
+ clients = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, cockpit_http_client_unref);
+ client = g_hash_table_lookup (clients, name);
+ }
+
+ if (!client)
+ {
+ client = g_slice_new0 (CockpitHttpClient);
+ client->name = g_strdup (name);
+
+ if (clients && name)
+ {
+ g_debug ("%s: registering client", name);
+ g_hash_table_replace (clients, client->name, cockpit_http_client_ref (client));
+ }
+ }
+ else if (name)
+ {
+ g_debug ("%s: using client", name);
+ }
+
+ cockpit_http_client_ref (client);
+ return client;
+}
+
+static void
+cockpit_http_client_checkin (CockpitHttpClient *client,
+ CockpitStream *stream)
+{
+ cockpit_http_client_reset (client);
+ client->stream = g_object_ref (stream);
+ client->sig_close = g_signal_connect (stream, "close", G_CALLBACK (on_client_close), client);
+ client->timeout = g_timeout_add_seconds (10, on_client_timeout, client);
+}
+
+static CockpitStream *
+cockpit_http_client_checkout (CockpitHttpClient *client)
+{
+ CockpitStream *stream = NULL;
+
+ if (client->stream)
+ {
+ g_debug ("%s: reusing connection", client->name);
+
+ stream = g_object_ref (client->stream);
+ cockpit_http_client_reset (client);
+ }
+
+ return stream;
+}
+
+/**
+ * CockpitHttpStream:
+ *
+ * A #CockpitChannel that represents a HTTP request/response.
+ *
+ * The payload type for this channel is 'http-stream2'.
+ */
+
+/*
+ * Some things we should add later without breaking the payload:
+ *
+ * - Specifying the HTTP version of the request.
+ * - Chunked messages in the request when request is HTTP/1.1
+ * - Trans coding for non-UTF8 charsets.
+ */
+
+#define COCKPIT_HTTP_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_HTTP_STREAM, CockpitHttpStream))
+
+enum {
+ BUFFER_REQUEST,
+ RELAY_REQUEST,
+ RELAY_DATA,
+ FINISHED
+};
+
+typedef struct _CockpitHttpStream {
+ CockpitChannel parent;
+
+ /* The nickname for debugging and logging */
+ gchar *name;
+ CockpitHttpClient *client;
+
+ /* The connection */
+ CockpitStream *stream;
+ gulong sig_open;
+ gulong sig_read;
+ gulong sig_close;
+
+ gint state;
+ gboolean failed;
+ gboolean binary;
+ gboolean keep_alive;
+ gboolean headers_inline;
+
+ /* The request */
+ GList *request;
+
+ /* From parsing the response */
+ gboolean response_chunked;
+ gssize response_length;
+} CockpitHttpStream;
+
+typedef struct {
+ CockpitChannelClass parent_class;
+} CockpitHttpStreamClass;
+
+G_DEFINE_TYPE (CockpitHttpStream, cockpit_http_stream, COCKPIT_TYPE_CHANNEL);
+
+static gboolean
+parse_content_length (CockpitHttpStream *self,
+ CockpitChannel *channel,
+ guint status,
+ GHashTable *headers)
+{
+ const gchar *header;
+ guint64 value;
+ gchar *end;
+
+ if (status == 204)
+ {
+ self->response_length = 0;
+ return TRUE;
+ }
+
+ header = g_hash_table_lookup (headers, "Content-Length");
+ if (header == NULL)
+ {
+ self->response_length = -1;
+ return TRUE;
+ }
+
+ value = g_ascii_strtoull (header, &end, 10);
+ if (end[0] != '\0')
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received invalid Content-Length in HTTP stream response", self->name);
+ return FALSE;
+ }
+ else if (value > G_MAXSSIZE)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received Content-Length that was too big", self->name);
+ return FALSE;
+ }
+
+ self->response_length = value;
+ g_debug ("%s: content length is %" G_GSSIZE_FORMAT, self->name, self->response_length);
+
+ return TRUE;
+}
+
+static gboolean
+parse_transfer_encoding (CockpitHttpStream *self,
+ CockpitChannel *channel,
+ GHashTable *headers)
+{
+ const gchar *header;
+
+ header = g_hash_table_lookup (headers, "Transfer-Encoding");
+ if (header == NULL)
+ {
+ self->response_chunked = FALSE;
+ return TRUE;
+ }
+
+ if (!g_str_equal (header, "chunked"))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received unsupported Transfer-Encoding in HTTP response: %s",
+ self->name, header);
+ return FALSE;
+ }
+
+ self->response_chunked = TRUE;
+ g_debug ("%s: chunked encoding", self->name);
+
+ return TRUE;
+}
+
+gboolean
+cockpit_http_stream_parse_keep_alive (const gchar *version,
+ GHashTable *headers)
+{
+ const gchar *header;
+
+ header = g_hash_table_lookup (headers, "Connection");
+
+ if (!header)
+ {
+ g_debug ("got no \"Connection\" header on %s response", version);
+ if (version && g_ascii_strcasecmp (version, "HTTP/1.1") == 0)
+ header = "keep-alive";
+ }
+ else
+ {
+ g_debug ("got \"Connection\" header of %s on %s response", header, version);
+ }
+
+ /*
+ * This is pretty conservative. If a Connection header is present
+ * and it *doesn't* have the non-standard "keep-alive" value in
+ * it, then assume we can't keep alive. Either the connection is
+ * meant to close, or we have no idea what the server is trying
+ * to tell us.
+ */
+
+ return (header && strstr (header, "keep-alive") != NULL);
+}
+
+static gboolean
+parse_keep_alive (CockpitHttpStream *self,
+ CockpitChannel *channel,
+ const gchar *version,
+ GHashTable *headers)
+{
+ self->keep_alive = cockpit_http_stream_parse_keep_alive (version, headers);
+ return TRUE;
+}
+
+static gboolean
+relay_headers (CockpitHttpStream *self,
+ CockpitChannel *channel,
+ GByteArray *buffer)
+{
+ g_autoptr(GHashTable) headers = NULL;
+ g_autofree gchar *version = NULL;
+ g_autofree gchar *reason = NULL;
+ g_autoptr(JsonObject) object = NULL;
+ const gchar *data;
+ JsonObject *heads;
+ GHashTableIter iter;
+ gpointer key;
+ gpointer value;
+ guint status;
+ gsize length;
+ gssize offset;
+ gssize offset2;
+
+ data = (const gchar *)buffer->data;
+ length = buffer->len;
+
+ offset = web_socket_util_parse_status_line (data, length, &version, &status, &reason);
+ if (offset == 0)
+ return FALSE; /* want more data */
+
+ if (offset < 0)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received response with bad HTTP status line", self->name);
+ return TRUE;
+ }
+
+ offset2 = web_socket_util_parse_headers (data + offset, length - offset, &headers);
+ if (offset2 == 0)
+ return FALSE; /* want more data */
+
+ if (offset2 < 0)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received response with bad HTTP headers", self->name);
+ return TRUE;
+ }
+
+ g_debug ("%s: response: %u %s", self->name, status, reason);
+ g_hash_table_iter_init (&iter, headers);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ g_debug ("%s: header: %s %s", self->name, (gchar *)key, (gchar *)value);
+
+ if (!parse_transfer_encoding (self, channel, headers) ||
+ !parse_content_length (self, channel, status, headers) ||
+ !parse_keep_alive (self, channel, version, headers))
+ return TRUE;
+
+ cockpit_pipe_skip (buffer, offset + offset2);
+
+ if (!self->binary)
+ {
+ g_hash_table_remove (headers, "Content-Length");
+ g_hash_table_remove (headers, "Range");
+ }
+ g_hash_table_remove (headers, "Connection");
+ g_hash_table_remove (headers, "Transfer-Encoding");
+
+ /* Now serialize all the rest of this into JSON */
+ object = json_object_new ();
+ json_object_set_int_member (object, "status", status);
+ json_object_set_string_member (object, "reason", reason);
+
+ heads = json_object_new();
+ g_hash_table_iter_init (&iter, headers);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ json_object_set_string_member (heads, key, value);
+
+ json_object_set_object_member (object, "headers", heads);
+
+ if (self->headers_inline)
+ {
+ g_autoptr(GBytes) message = cockpit_json_write_bytes (object);
+ cockpit_channel_send (channel, message, TRUE);
+ }
+ else
+ {
+ cockpit_channel_control (channel, "response", object);
+ }
+
+ return TRUE;
+}
+
+static void
+relay_data (CockpitChannel *channel,
+ GBytes *data)
+{
+ GBytes *block;
+ gsize size;
+ gsize offset;
+ gsize length;
+
+ size = g_bytes_get_size (data);
+ if (size < 8192)
+ {
+ cockpit_channel_send (channel, data, FALSE);
+ }
+ else
+ {
+ for (offset = 0; offset < size; offset += 4096)
+ {
+ length = MIN (4096, size - offset);
+ block = g_bytes_new_from_bytes (data, offset, length);
+ cockpit_channel_send (channel, block, FALSE);
+ g_bytes_unref (block);
+ }
+ }
+}
+
+static gboolean
+relay_chunked (CockpitHttpStream *self,
+ CockpitChannel *channel,
+ GByteArray *buffer)
+{
+ GBytes *message;
+ const gchar *data;
+ const gchar *pos;
+ guint64 size;
+ gsize length;
+ gsize beg;
+ gchar *end;
+
+ data = (const gchar *)buffer->data;
+ length = buffer->len;
+
+ pos = memchr (data, '\r', length);
+ if (pos == NULL)
+ return FALSE; /* want more data */
+
+ beg = (pos + 2) - data;
+ if (length < beg)
+ {
+ /* have to have a least the ending chars */
+ return FALSE; /* want more data */
+ }
+
+ size = g_ascii_strtoull (data, &end, 16);
+ if (pos[1] != '\n' || end != pos)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received invalid HTTP chunk", self->name);
+ }
+ else if (size > G_MAXSSIZE)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received extremely large HTTP chunk", self->name);
+ }
+ else if (length < beg + size + 2)
+ {
+ return FALSE; /* want more data */
+ }
+ else if (data[beg + size] != '\r' || data[beg + size + 1] != '\n')
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received invalid HTTP chunk data", self->name);
+ }
+ else if (size == 0)
+ {
+ /* All done, yay */
+ g_debug ("%s: received last chunk", self->name);
+ cockpit_pipe_skip (buffer, beg + 2);
+ cockpit_channel_close (channel, NULL);
+ g_assert (self->state == FINISHED);
+ }
+ else
+ {
+ message = cockpit_pipe_consume (buffer, beg, size, 2);
+ relay_data (channel, message);
+ g_bytes_unref (message);
+ return TRUE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+relay_length (CockpitHttpStream *self,
+ CockpitChannel *channel,
+ GByteArray *buffer)
+{
+ GBytes *message = NULL;
+ gsize block;
+
+ g_assert (self->response_length >= 0);
+
+ if (self->response_length == 0)
+ {
+ /* All done, yay */
+ g_debug ("%s: received enough bytes", self->name);
+ cockpit_channel_close (channel, NULL);
+ g_assert (self->state == FINISHED);
+ }
+ else if (buffer->len == 0)
+ {
+ /* Not enough data? */
+ return FALSE;
+ }
+ else
+ {
+ block = MIN (buffer->len, self->response_length);
+ self->response_length -= block;
+
+ message = cockpit_pipe_consume (buffer, 0, block, 0);
+ relay_data (channel, message);
+ g_bytes_unref (message);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+relay_all (CockpitHttpStream *self,
+ CockpitChannel *channel,
+ GByteArray *buffer)
+{
+ GBytes *message;
+
+ if (buffer->len == 0)
+ {
+ /* Not enough data? */
+ return FALSE;
+ }
+
+ message = cockpit_pipe_consume (buffer, 0, buffer->len, 0);
+ relay_data (channel, message);
+ g_bytes_unref (message);
+
+ return TRUE;
+}
+
+static void
+on_stream_open (CockpitStream *stream,
+ gpointer user_data)
+{
+ CockpitChannel *channel = user_data;
+ cockpit_channel_ready (channel, NULL);
+}
+
+static void
+on_stream_read (CockpitStream *stream,
+ GByteArray *buffer,
+ gboolean end_of_data,
+ gpointer user_data)
+{
+ CockpitHttpStream *self = user_data;
+ CockpitChannel *channel = user_data;
+ gboolean ret;
+
+ g_object_ref (self);
+
+ if (self->state < RELAY_REQUEST)
+ {
+ if (buffer->len != 0)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received data before HTTP request was sent", self->name);
+ }
+ }
+ else if (self->state < RELAY_DATA)
+ {
+ /* Parse headers */
+ if (relay_headers (self, channel, buffer))
+ {
+ self->state = RELAY_DATA;
+ }
+ else if (end_of_data)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received truncated HTTP response", self->name);
+ }
+ }
+ while (self->state == RELAY_DATA)
+ {
+ if (self->response_chunked)
+ ret = relay_chunked (self, channel, buffer);
+ else if (self->response_length >= 0)
+ ret = relay_length (self, channel, buffer);
+ else
+ ret = relay_all (self, channel, buffer);
+ if (!ret)
+ break;
+ }
+
+ g_object_unref (self);
+}
+
+static void
+on_stream_close (CockpitStream *stream,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitHttpStream *self = user_data;
+ CockpitChannel *channel = user_data;
+
+ self->keep_alive = FALSE;
+ if (self->state != FINISHED)
+ {
+ if (problem)
+ {
+ cockpit_channel_close (channel, problem);
+ }
+ else if (self->state == RELAY_DATA &&
+ !self->response_chunked &&
+ self->response_length <= 0)
+ {
+ g_debug ("%s: end of stream is end of data", self->name);
+ cockpit_channel_close (channel, NULL);
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: received truncated HTTP response", self->name);
+ }
+ }
+}
+
+static gboolean
+disallowed_header (const gchar *name,
+ const gchar *value,
+ gboolean binary)
+{
+ static const gchar *bad_headers[] = {
+ "Content-Length",
+ "Content-MD5",
+ "TE",
+ "Trailer",
+ "Transfer-Encoding",
+ "Upgrade",
+ NULL
+ };
+
+ static const gchar *bad_text[] = {
+ "Accept-Encoding",
+ "Content-Encoding",
+ "Accept-Charset",
+ "Accept-Ranges",
+ "Content-Range",
+ "Range",
+ NULL
+ };
+
+ gint i;
+
+ for (i = 0; bad_headers[i] != NULL; i++)
+ {
+ if (g_ascii_strcasecmp (bad_headers[i], name) == 0)
+ return TRUE;
+ }
+
+ if (!binary)
+ {
+ for (i = 0; bad_text[i] != NULL; i++)
+ {
+ if (g_ascii_strcasecmp (bad_text[i], name) == 0)
+ return TRUE;
+ }
+ }
+
+ /* Only allow the caller to specify Connection: close */
+ if (g_ascii_strcasecmp ("Connection", name) == 0 &&
+ !g_strcmp0 (value, "close"))
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+send_http_request (CockpitHttpStream *self)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ JsonObject *options;
+ gboolean had_host;
+ gboolean had_encoding;
+ const gchar *method;
+ const gchar *path;
+ GString *string = NULL;
+ JsonNode *node;
+ JsonObject *headers;
+ const gchar *header;
+ const gchar *value;
+ GList *request = NULL;
+ GList *names = NULL;
+ GBytes *bytes;
+ GList *l;
+ gsize total;
+
+ options = cockpit_channel_get_options (channel);
+
+ /*
+ * The checks we do here for token validity are just enough to be able
+ * to format an HTTP response, without leaking across lines etc.
+ */
+
+ if (!cockpit_json_get_string (options, "path", NULL, &path))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: bad \"path\" field in HTTP stream request", self->name);
+ goto out;
+ }
+ else if (path == NULL)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: missing \"path\" field in HTTP stream request", self->name);
+ goto out;
+ }
+ else if (!cockpit_web_response_is_simple_token (path))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"path\" field in HTTP stream request", self->name);
+ goto out;
+ }
+
+ if (!cockpit_json_get_string (options, "method", NULL, &method))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: bad \"method\" field in HTTP stream request", self->name);
+ goto out;
+ }
+ else if (method == NULL)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: missing \"method\" field in HTTP stream request", self->name);
+ goto out;
+ }
+ else if (!cockpit_web_response_is_simple_token (method))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"method\" field in HTTP stream request", self->name);
+ goto out;
+ }
+
+ g_debug ("%s: sending %s request", self->name, method);
+
+ string = g_string_sized_new (128);
+ g_string_printf (string, "%s %s HTTP/1.1\r\n", method, path);
+
+ had_encoding = had_host = FALSE;
+
+ node = json_object_get_member (options, "headers");
+ if (node)
+ {
+ if (!JSON_NODE_HOLDS_OBJECT (node))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"headers\" field in HTTP stream request", self->name);
+ goto out;
+ }
+
+ headers = json_node_get_object (node);
+ names = json_object_get_members (headers);
+ for (l = names; l != NULL; l = g_list_next (l))
+ {
+ header = l->data;
+ if (!cockpit_web_response_is_simple_token (header))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid header in HTTP stream request: %s", self->name, header);
+ goto out;
+ }
+ node = json_object_get_member (headers, header);
+ if (!node || !JSON_NODE_HOLDS_VALUE (node) || json_node_get_value_type (node) != G_TYPE_STRING)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid header value in HTTP stream request: %s", self->name, header);
+ goto out;
+ }
+ value = json_node_get_string (node);
+ if (disallowed_header (header, value, self->binary))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: disallowed header in HTTP stream request: %s", self->name, header);
+ goto out;
+ }
+ if (!cockpit_web_response_is_header_value (value))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid header value in HTTP stream request: %s", self->name, header);
+ goto out;
+ }
+
+ g_string_append_printf (string, "%s: %s\r\n", (gchar *)l->data, value);
+ g_debug ("%s: sending header: %s %s", self->name, (gchar *)l->data, value);
+
+ if (g_ascii_strcasecmp (l->data, "Host") == 0)
+ had_host = TRUE;
+ if (g_ascii_strcasecmp (l->data, "Accept-Encoding") == 0)
+ had_encoding = TRUE;
+ }
+ }
+
+ if (!had_host)
+ {
+ g_string_append (string, "Host: ");
+ g_string_append_uri_escaped (string, self->client->hostname, "[]!%$&()*+,-.:;=\\_~", FALSE);
+ g_string_append (string, "\r\n");
+ }
+ if (!had_encoding)
+ g_string_append (string, "Accept-Encoding: identity\r\n");
+
+ if (!self->binary)
+ g_string_append (string, "Accept-Charset: UTF-8\r\n");
+
+ request = g_list_reverse (self->request);
+ self->request = NULL;
+
+ /* Calculate how much data we have to send */
+ total = 0;
+ for (l = request; l != NULL; l = g_list_next (l))
+ total += g_bytes_get_size (l->data);
+
+ if (request || g_ascii_strcasecmp (method, "POST") == 0)
+ g_string_append_printf (string, "Content-Length: %" G_GSIZE_FORMAT "\r\n", total);
+ g_string_append (string, "\r\n");
+
+ bytes = g_string_free_to_bytes (string);
+ string = NULL;
+
+ cockpit_stream_write (self->stream, bytes);
+ g_bytes_unref (bytes);
+
+ /* Now send all the data */
+ for (l = request; l != NULL; l = g_list_next (l))
+ cockpit_stream_write (self->stream, l->data);
+
+out:
+ g_list_free (names);
+ g_list_free_full (request, (GDestroyNotify)g_bytes_unref);
+ if (string)
+ g_string_free (string, TRUE);
+}
+
+static void
+cockpit_http_stream_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ CockpitHttpStream *self = (CockpitHttpStream *)channel;
+ self->request = g_list_prepend (self->request, g_bytes_ref (message));
+}
+
+static gboolean
+cockpit_http_stream_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *options)
+{
+ CockpitHttpStream *self = COCKPIT_HTTP_STREAM (channel);
+
+ if (g_str_equal (command, "done"))
+ {
+ g_return_val_if_fail (self->state == BUFFER_REQUEST, FALSE);
+ self->state = RELAY_REQUEST;
+ send_http_request (self);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+cockpit_http_stream_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ CockpitHttpStream *self = COCKPIT_HTTP_STREAM (channel);
+
+ if (problem)
+ {
+ self->failed = TRUE;
+ self->state = FINISHED;
+ COCKPIT_CHANNEL_CLASS (cockpit_http_stream_parent_class)->close (channel, problem);
+ }
+ else if (self->state == RELAY_DATA)
+ {
+ g_debug ("%s: relayed response", self->name);
+ self->state = FINISHED;
+ cockpit_channel_control (channel, "done", NULL);
+
+ /* Save this for another round? */
+ if (self->keep_alive)
+ {
+ if (self->sig_open)
+ g_signal_handler_disconnect (self->stream, self->sig_open);
+ g_signal_handler_disconnect (self->stream, self->sig_read);
+ g_signal_handler_disconnect (self->stream, self->sig_close);
+ cockpit_http_client_checkin (self->client, self->stream);
+ cockpit_flow_throttle (COCKPIT_FLOW (self->stream), NULL);
+ cockpit_flow_throttle (COCKPIT_FLOW (channel), NULL);
+ g_object_unref (self->stream);
+ self->stream = NULL;
+ }
+
+ COCKPIT_CHANNEL_CLASS (cockpit_http_stream_parent_class)->close (channel, NULL);
+ }
+ else if (self->state != FINISHED)
+ {
+ g_warn_if_reached ();
+ self->failed = TRUE;
+ self->state = FINISHED;
+ COCKPIT_CHANNEL_CLASS (cockpit_http_stream_parent_class)->close (channel, "internal-error");
+ }
+}
+
+static void
+cockpit_http_stream_init (CockpitHttpStream *self)
+{
+ self->response_length = -1;
+ self->keep_alive = FALSE;
+ self->state = BUFFER_REQUEST;
+}
+
+static void
+cockpit_http_stream_prepare (CockpitChannel *channel)
+{
+ CockpitHttpStream *self = COCKPIT_HTTP_STREAM (channel);
+ const gchar *payload;
+ const gchar *connection;
+ JsonObject *options;
+ const gchar *path;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_http_stream_parent_class)->prepare (channel);
+
+ if (self->failed)
+ return;
+
+ options = cockpit_channel_get_options (channel);
+ if (!cockpit_json_get_string (options, "connection", NULL, &connection))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "bad \"connection\" field in HTTP stream request");
+ return;
+ }
+
+ if (!cockpit_json_get_string (options, "path", "/", &path))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "bad \"path\" field in HTTP stream request");
+ return;
+ }
+
+ /*
+ * In http-stream1 the headers are sent as first message.
+ * In http-stream2 the headers are in a control message.
+ */
+ if (cockpit_json_get_string (options, "payload", NULL, &payload) &&
+ payload && g_str_equal (payload, "http-stream1"))
+ {
+ self->headers_inline = TRUE;
+ }
+
+ self->client = cockpit_http_client_ensure (connection);
+
+ self->stream = cockpit_http_client_checkout (self->client);
+ if (!self->stream)
+ {
+ const gchar *internal = "";
+ cockpit_json_get_string (options, "internal", "", &internal);
+
+ if (g_str_equal (internal, "packages"))
+ {
+ g_autoptr(GIOStream) packages_stream = cockpit_packages_connect ();
+ self->stream = cockpit_stream_new (self->name, packages_stream);
+ self->name = g_strdup_printf ("http://internal:packages%s", path);
+ self->client->hostname = g_strdup ("packages");
+ }
+ else
+ {
+ g_autoptr(CockpitConnectable) connectable = cockpit_connect_parse_stream (channel);
+ if (!connectable)
+ return;
+
+ self->name = g_strdup_printf ("%s://%s%s",
+ connectable->tls ? "https" : "http",
+ connectable->name, path);
+ self->client->hostname = g_strdup (connectable->name);
+
+ self->stream = cockpit_stream_connect (self->name, connectable);
+ self->sig_open = g_signal_connect (self->stream, "open", G_CALLBACK (on_stream_open), self);
+ }
+ }
+
+ /* Parsed elsewhere */
+ self->binary = json_object_has_member (options, "binary");
+
+ self->sig_read = g_signal_connect (self->stream, "read", G_CALLBACK (on_stream_read), self);
+ self->sig_close = g_signal_connect (self->stream, "close", G_CALLBACK (on_stream_close), self);
+
+ /* Let the channel throttle the stream's input flow*/
+ cockpit_flow_throttle (COCKPIT_FLOW (self->stream), COCKPIT_FLOW (self));
+
+ /* Let the stream throtlte the channel peer's output flow */
+ cockpit_flow_throttle (COCKPIT_FLOW (channel), COCKPIT_FLOW (self->stream));
+
+ /* If not waiting for open */
+ if (!self->sig_open)
+ cockpit_channel_ready (channel, NULL);
+}
+
+static void
+cockpit_http_stream_dispose (GObject *object)
+{
+ CockpitHttpStream *self = COCKPIT_HTTP_STREAM (object);
+
+ if (self->stream)
+ {
+ if (self->sig_open)
+ g_signal_handler_disconnect (self->stream, self->sig_open);
+ g_signal_handler_disconnect (self->stream, self->sig_read);
+ g_signal_handler_disconnect (self->stream, self->sig_close);
+ cockpit_stream_close (self->stream, NULL);
+ g_object_unref (self->stream);
+ }
+
+ g_list_free_full (self->request, (GDestroyNotify)g_bytes_unref);
+ self->request = NULL;
+
+ G_OBJECT_CLASS (cockpit_http_stream_parent_class)->dispose (object);
+}
+
+static void
+cockpit_http_stream_finalize (GObject *object)
+{
+ CockpitHttpStream *self = COCKPIT_HTTP_STREAM (object);
+
+ g_free (self->name);
+ if (self->client)
+ cockpit_http_client_unref (self->client);
+
+ G_OBJECT_CLASS (cockpit_http_stream_parent_class)->finalize (object);
+}
+
+static void
+cockpit_http_stream_constructed (GObject *object)
+{
+ const gchar *caps[] = { "tls-certificates",
+ "address",
+ NULL };
+
+ G_OBJECT_CLASS (cockpit_http_stream_parent_class)->constructed (object);
+
+ g_object_set (object, "capabilities", &caps, NULL);
+}
+
+static void
+cockpit_http_stream_class_init (CockpitHttpStreamClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->dispose = cockpit_http_stream_dispose;
+ gobject_class->finalize = cockpit_http_stream_finalize;
+ gobject_class->constructed = cockpit_http_stream_constructed;
+
+ channel_class->prepare = cockpit_http_stream_prepare;
+ channel_class->control = cockpit_http_stream_control;
+ channel_class->recv = cockpit_http_stream_recv;
+ channel_class->close = cockpit_http_stream_close;
+}
diff --git a/src/bridge/cockpithttpstream.h b/src/bridge/cockpithttpstream.h
new file mode 100644
index 0000000..7f26efb
--- /dev/null
+++ b/src/bridge/cockpithttpstream.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_HTTP_STREAM_H__
+#define COCKPIT_HTTP_STREAM_H__
+
+#include <gio/gio.h>
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_HTTP_STREAM (cockpit_http_stream_get_type ())
+
+GType cockpit_http_stream_get_type (void) G_GNUC_CONST;
+
+gboolean cockpit_http_stream_parse_keep_alive (const gchar *version,
+ GHashTable *headers);
+
+#endif /* COCKPIT_HTTP_STREAM_H__ */
diff --git a/src/bridge/cockpitinteracttransport.c b/src/bridge/cockpitinteracttransport.c
new file mode 100644
index 0000000..1a672c6
--- /dev/null
+++ b/src/bridge/cockpitinteracttransport.c
@@ -0,0 +1,289 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitinteracttransport.h"
+
+#include "common/cockpitpipe.h"
+
+#include <string.h>
+
+/**
+ * CockpitInteractTransport:
+ *
+ * A #CockpitTransport implementation that shuttles data over a
+ * #CockpitPipe connected to stdio and handles framing in a way
+ * that it's more usable for debugging channels.
+ */
+
+struct _CockpitInteractTransport {
+ CockpitTransport parent_instance;
+ gchar *name;
+ gchar *delimiter;
+ gsize delimiter_len;
+ gboolean colored;
+ CockpitPipe *pipe;
+ gulong read_sig;
+ gulong close_sig;
+};
+
+typedef struct {
+ CockpitTransportClass parent_class;
+} CockpitInteractTransportClass;
+
+enum {
+ PROP_0,
+ PROP_NAME,
+ PROP_PIPE,
+ PROP_BOUNDARY,
+ PROP_COLOR
+};
+
+G_DEFINE_TYPE (CockpitInteractTransport, cockpit_interact_transport, COCKPIT_TYPE_TRANSPORT);
+
+static void
+cockpit_interact_transport_init (CockpitInteractTransport *self)
+{
+
+}
+
+static void
+on_pipe_read (CockpitPipe *pipe,
+ GByteArray *input,
+ gboolean end_of_data,
+ gpointer user_data)
+{
+ CockpitInteractTransport *self = COCKPIT_INTERACT_TRANSPORT (user_data);
+
+ for (;;)
+ {
+ guint8 *pos = NULL;
+
+ if (input->len > 0)
+ pos = (guint8 *)g_strstr_len ((gchar *)input->data, input->len, self->delimiter);
+ if (!pos)
+ {
+ if (!end_of_data)
+ g_debug ("%s: want more data", self->name);
+ break;
+ }
+
+ guint32 size = pos - input->data;
+ g_autoptr(GBytes) message = cockpit_pipe_consume (input, 0, size, self->delimiter_len);
+
+ g_autofree gchar *channel = NULL;
+ g_autoptr(GBytes) payload = cockpit_transport_parse_frame (message, &channel);
+ if (payload)
+ {
+ g_debug ("%s: received a %d byte payload", self->name, (int)size);
+ cockpit_transport_emit_recv ((CockpitTransport *)self, channel, payload);
+ }
+ }
+
+ if (end_of_data)
+ cockpit_pipe_close (self->pipe, NULL);
+}
+
+static void
+on_pipe_close (CockpitPipe *pipe,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitInteractTransport *self = COCKPIT_INTERACT_TRANSPORT (user_data);
+
+ g_debug ("%s: closed%s%s", self->name,
+ problem ? ": " : "", problem ? problem : "");
+
+ cockpit_transport_emit_closed (COCKPIT_TRANSPORT (self), problem);
+}
+
+static void
+cockpit_interact_transport_constructed (GObject *object)
+{
+ CockpitInteractTransport *self = COCKPIT_INTERACT_TRANSPORT (object);
+
+ G_OBJECT_CLASS (cockpit_interact_transport_parent_class)->constructed (object);
+
+ g_return_if_fail (self->pipe != NULL);
+ g_object_get (self->pipe, "name", &self->name, NULL);
+ self->read_sig = g_signal_connect (self->pipe, "read", G_CALLBACK (on_pipe_read), self);
+ self->close_sig = g_signal_connect (self->pipe, "close", G_CALLBACK (on_pipe_close), self);
+
+ self->delimiter_len = strlen (self->delimiter);
+}
+
+static void
+cockpit_interact_transport_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitInteractTransport *self = COCKPIT_INTERACT_TRANSPORT (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ g_value_set_string (value, self->name);
+ break;
+ case PROP_PIPE:
+ g_value_set_object (value, self->pipe);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_interact_transport_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitInteractTransport *self = COCKPIT_INTERACT_TRANSPORT (object);
+
+ switch (prop_id)
+ {
+ case PROP_PIPE:
+ self->pipe = g_value_dup_object (value);
+ break;
+ case PROP_BOUNDARY:
+ self->delimiter = g_strdup_printf ("\n%s\n", g_value_get_string (value));
+ self->delimiter_len = strlen (self->delimiter);
+ break;
+ case PROP_COLOR:
+ self->colored = g_value_get_boolean (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_interact_transport_finalize (GObject *object)
+{
+ CockpitInteractTransport *self = COCKPIT_INTERACT_TRANSPORT (object);
+
+ g_signal_handler_disconnect (self->pipe, self->read_sig);
+ g_signal_handler_disconnect (self->pipe, self->close_sig);
+
+ g_free (self->name);
+ g_free (self->delimiter);
+ g_clear_object (&self->pipe);
+
+ G_OBJECT_CLASS (cockpit_interact_transport_parent_class)->finalize (object);
+}
+
+static void
+cockpit_interact_transport_send (CockpitTransport *transport,
+ const gchar *channel_id,
+ GBytes *payload)
+{
+ CockpitInteractTransport *self = COCKPIT_INTERACT_TRANSPORT (transport);
+ GBytes *prefix;
+ GBytes *suffix;
+ GBytes *color;
+ gchar *prefix_str;
+
+ if (self->colored)
+ {
+ color = g_bytes_new_static ("\x1b[1m", 4);
+ cockpit_pipe_write (self->pipe, color);
+ g_bytes_unref (color);
+ }
+
+ prefix_str = g_strdup_printf ("%s\n", channel_id ? channel_id : "");
+ prefix = g_bytes_new_take (prefix_str, strlen (prefix_str));
+ cockpit_pipe_write (self->pipe, prefix);
+ g_bytes_unref (prefix);
+
+ cockpit_pipe_write (self->pipe, payload);
+
+ suffix = g_bytes_new (self->delimiter, self->delimiter_len);
+ cockpit_pipe_write (self->pipe, suffix);
+ g_bytes_unref (suffix);
+
+ if (self->colored)
+ {
+ color = g_bytes_new_static ("\x1b[0m", 4);
+ cockpit_pipe_write (self->pipe, color);
+ g_bytes_unref (color);
+ }
+
+ g_debug ("%s: queued %d byte payload", self->name, (int)g_bytes_get_size (payload));
+}
+
+static void
+cockpit_interact_transport_close (CockpitTransport *transport,
+ const gchar *problem)
+{
+ CockpitInteractTransport *self = COCKPIT_INTERACT_TRANSPORT (transport);
+ cockpit_pipe_close (self->pipe, problem);
+}
+
+static void
+cockpit_interact_transport_class_init (CockpitInteractTransportClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitTransportClass *transport_class = COCKPIT_TRANSPORT_CLASS (klass);
+
+ transport_class->send = cockpit_interact_transport_send;
+ transport_class->close = cockpit_interact_transport_close;
+
+ gobject_class->constructed = cockpit_interact_transport_constructed;
+ gobject_class->get_property = cockpit_interact_transport_get_property;
+ gobject_class->set_property = cockpit_interact_transport_set_property;
+ gobject_class->finalize = cockpit_interact_transport_finalize;
+
+ g_object_class_override_property (gobject_class, PROP_NAME, "name");
+
+ g_object_class_install_property (gobject_class, PROP_PIPE,
+ g_param_spec_object ("pipe", NULL, NULL, COCKPIT_TYPE_PIPE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class, PROP_BOUNDARY,
+ g_param_spec_string ("boundary", NULL, NULL, "---",
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class, PROP_COLOR,
+ g_param_spec_boolean ("color", NULL, NULL, FALSE,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+}
+
+CockpitTransport *
+cockpit_interact_transport_new (gint in_fd,
+ gint out_fd,
+ const gchar *boundary)
+{
+ CockpitTransport *transport;
+ CockpitPipe *pipe;
+
+ pipe = cockpit_pipe_new ("interact", in_fd, out_fd);
+ transport = g_object_new (COCKPIT_TYPE_INTERACT_TRANSPORT,
+ "pipe", pipe,
+ "boundary", boundary,
+ "color", (gboolean)isatty (out_fd),
+ NULL);
+ g_object_unref (pipe);
+
+ return transport;
+}
+
diff --git a/src/bridge/cockpitinteracttransport.h b/src/bridge/cockpitinteracttransport.h
new file mode 100644
index 0000000..cc19347
--- /dev/null
+++ b/src/bridge/cockpitinteracttransport.h
@@ -0,0 +1,43 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_INTERACT_TRANSPORT_H__
+#define __COCKPIT_INTERACT_TRANSPORT_H__
+
+#include <gio/gio.h>
+
+#include "common/cockpittransport.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_INTERACT_TRANSPORT (cockpit_interact_transport_get_type ())
+#define COCKPIT_INTERACT_TRANSPORT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_INTERACT_TRANSPORT, CockpitInteractTransport))
+#define COCKPIT_IS_INTERACT_TRANSPORT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), COCKPIT_TYPE_INTERACT_TRANSPORT))
+
+typedef struct _CockpitInteractTransport CockpitInteractTransport;
+
+GType cockpit_interact_transport_get_type (void) G_GNUC_CONST;
+
+CockpitTransport * cockpit_interact_transport_new (gint in_fd,
+ gint out_fd,
+ const gchar *boundary);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_INTERACT_TRANSPORT_H__ */
diff --git a/src/bridge/cockpitinternalmetrics.c b/src/bridge/cockpitinternalmetrics.c
new file mode 100644
index 0000000..959b2f4
--- /dev/null
+++ b/src/bridge/cockpitinternalmetrics.c
@@ -0,0 +1,570 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <math.h>
+#include <sys/time.h>
+
+#include "cockpitmetrics.h"
+#include "cockpitinternalmetrics.h"
+#include "cockpitsamples.h"
+#include "cockpitcpusamples.h"
+#include "cockpitmemorysamples.h"
+#include "cockpitblocksamples.h"
+#include "cockpitnetworksamples.h"
+#include "cockpitmountsamples.h"
+#include "cockpitcgroupsamples.h"
+#include "cockpitdisksamples.h"
+
+#include "common/cockpitjson.h"
+
+/**
+ * CockpitInternalMetrics:
+ *
+ * A #CockpitMetrics channel that pulls data from internal sources
+ */
+
+static void cockpit_samples_interface_init (CockpitSamplesInterface *iface);
+
+#define COCKPIT_INTERNAL_METRICS(o) \
+ (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_INTERNAL_METRICS, CockpitInternalMetrics))
+
+typedef enum {
+ CPU_SAMPLER = 1 << 0,
+ MEMORY_SAMPLER = 1 << 1,
+ BLOCK_SAMPLER = 1 << 2,
+ NETWORK_SAMPLER = 1 << 3,
+ MOUNT_SAMPLER = 1 << 4,
+ CGROUP_SAMPLER = 1 << 5,
+ DISK_SAMPLER = 1 << 6,
+ THERMAL_SAMPLER = 1 << 7,
+ CGROUP_IO_SAMPLER = 1 << 8,
+} SamplerSet;
+
+typedef struct {
+ const gchar *name;
+ const gchar *units;
+ const gchar *semantics;
+ gboolean instanced;
+ SamplerSet sampler;
+} MetricDescription;
+
+static MetricDescription metric_descriptions[] = {
+ { "cpu.basic.nice", "millisec", "counter", FALSE, CPU_SAMPLER },
+ { "cpu.basic.user", "millisec", "counter", FALSE, CPU_SAMPLER },
+ { "cpu.basic.system", "millisec", "counter", FALSE, CPU_SAMPLER },
+ { "cpu.basic.iowait", "millisec", "counter", FALSE, CPU_SAMPLER },
+ { "cpu.core.nice", "millisec", "counter", TRUE, CPU_SAMPLER },
+ { "cpu.core.user", "millisec", "counter", TRUE, CPU_SAMPLER },
+ { "cpu.core.system", "millisec", "counter", TRUE, CPU_SAMPLER },
+ { "cpu.core.iowait", "millisec", "counter", TRUE, CPU_SAMPLER },
+
+ { "memory.free", "bytes", "instant", FALSE, MEMORY_SAMPLER },
+ { "memory.used", "bytes", "instant", FALSE, MEMORY_SAMPLER },
+ { "memory.cached", "bytes", "instant", FALSE, MEMORY_SAMPLER },
+ { "memory.swap-used", "bytes", "instant", FALSE, MEMORY_SAMPLER },
+
+ { "block.device.read", "bytes", "counter", TRUE, BLOCK_SAMPLER },
+ { "block.device.written", "bytes", "counter", TRUE, BLOCK_SAMPLER },
+
+ { "disk.all.read", "bytes", "counter", FALSE, DISK_SAMPLER },
+ { "disk.all.written", "bytes", "counter", FALSE, DISK_SAMPLER },
+ { "disk.dev.read", "bytes", "counter", TRUE, DISK_SAMPLER },
+ { "disk.dev.written", "bytes", "counter", TRUE, DISK_SAMPLER },
+
+ { "network.all.rx", "bytes", "counter", FALSE, NETWORK_SAMPLER }, /* deprecated */
+ { "network.all.tx", "bytes", "counter", FALSE, NETWORK_SAMPLER }, /* deprecated */
+ { "network.interface.rx", "bytes", "counter", TRUE, NETWORK_SAMPLER },
+ { "network.interface.tx", "bytes", "counter", TRUE, NETWORK_SAMPLER },
+
+ { "mount.total", "bytes", "instant", TRUE, MOUNT_SAMPLER },
+ { "mount.used", "bytes", "instant", TRUE, MOUNT_SAMPLER },
+
+ { "cgroup.memory.usage", "bytes", "instant", TRUE, CGROUP_SAMPLER },
+ { "cgroup.memory.limit", "bytes", "instant", TRUE, CGROUP_SAMPLER },
+ { "cgroup.memory.sw-usage", "bytes", "instant", TRUE, CGROUP_SAMPLER },
+ { "cgroup.memory.sw-limit", "bytes", "instant", TRUE, CGROUP_SAMPLER },
+ { "cgroup.cpu.usage", "millisec", "counter", TRUE, CGROUP_SAMPLER },
+ { "cgroup.cpu.shares", "count", "instant", TRUE, CGROUP_SAMPLER },
+
+ { "cpu.temperature", "celsius", "instant", TRUE, THERMAL_SAMPLER },
+
+ { "disk.cgroup.read", "bytes", "counter", TRUE, CGROUP_IO_SAMPLER },
+ { "disk.cgroup.written", "bytes", "counter", TRUE, CGROUP_IO_SAMPLER },
+
+ { NULL }
+};
+
+static MetricDescription *
+find_metric_description (const gchar *name)
+{
+ for (MetricDescription *d = metric_descriptions; d->name; d++)
+ {
+ if (g_strcmp0 (d->name, name) == 0)
+ return d;
+ }
+
+ return NULL;
+}
+
+typedef struct {
+ gboolean seen;
+ int index;
+ double value;
+} InstanceInfo;
+
+typedef struct {
+ MetricDescription *desc;
+ const gchar *derive;
+
+ GHashTable *instances;
+ double value;
+} MetricInfo;
+
+typedef struct {
+ CockpitMetrics parent;
+ const gchar *name;
+
+ gint64 interval;
+ int n_metrics;
+ MetricInfo *metrics;
+ const gchar **omit_instances;
+ SamplerSet samplers;
+
+ gboolean need_meta;
+} CockpitInternalMetrics;
+
+typedef struct {
+ CockpitMetricsClass parent_class;
+} CockpitInternalMetricsClass;
+
+G_DEFINE_TYPE_WITH_CODE (CockpitInternalMetrics, cockpit_internal_metrics, COCKPIT_TYPE_METRICS,
+ G_IMPLEMENT_INTERFACE (COCKPIT_TYPE_SAMPLES,
+ cockpit_samples_interface_init))
+
+static void
+cockpit_internal_metrics_init (CockpitInternalMetrics *self)
+{
+}
+
+static gint64
+timestamp_from_timeval (struct timeval *tv)
+{
+ return tv->tv_sec * 1000 + tv->tv_usec / 1000;
+}
+
+static void
+send_meta (CockpitInternalMetrics *self)
+{
+ JsonArray *metrics;
+ JsonObject *metric;
+ JsonObject *root;
+ struct timeval now_timeval;
+ gint64 now;
+
+ gettimeofday (&now_timeval, NULL);
+ now = timestamp_from_timeval (&now_timeval);
+
+ root = json_object_new ();
+ json_object_set_int_member (root, "timestamp", now);
+ json_object_set_int_member (root, "now", now);
+ json_object_set_int_member (root, "interval", self->interval);
+
+ metrics = json_array_new ();
+ for (int i = 0; i < self->n_metrics; i++)
+ {
+ MetricInfo *info = &self->metrics[i];
+ metric = json_object_new ();
+
+ /* Name and derivation mode
+ */
+ json_object_set_string_member (metric, "name", info->desc->name);
+ if (info->derive)
+ json_object_set_string_member (metric, "derive", info->derive);
+
+ /* Instances
+ */
+ if (info->desc->instanced)
+ {
+ GHashTableIter iter;
+ gpointer key, value;
+ int index;
+ JsonArray *instances = json_array_new ();
+
+ g_hash_table_iter_init (&iter, info->instances);
+ index = 0;
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ const gchar *name = key;
+ InstanceInfo *inst = value;
+
+ /* HACK: We can't use json_builder_add_string_value here since
+ it turns empty strings into 'null' values inside arrays.
+
+ https://bugzilla.gnome.org/show_bug.cgi?id=730803
+ */
+ {
+ JsonNode *string_element = json_node_alloc ();
+ json_node_init_string (string_element, name);
+ json_array_add_element (instances, string_element);
+ }
+
+ inst->index = index++;
+ }
+ json_object_set_array_member (metric, "instances", instances);
+ }
+
+ /* Units and semantics
+ */
+ json_object_set_string_member (metric, "units", info->desc->units);
+ json_object_set_string_member (metric, "semantics", info->desc->semantics);
+
+ json_array_add_object_element (metrics, metric);
+ }
+
+ json_object_set_array_member (root, "metrics", metrics);
+
+ cockpit_metrics_send_meta (COCKPIT_METRICS (self), root, FALSE);
+
+ json_object_unref (root);
+}
+
+static void
+cockpit_internal_metrics_sample (CockpitSamples *samples,
+ const gchar *metric,
+ const gchar *instance,
+ gint64 value)
+{
+ CockpitInternalMetrics *self = COCKPIT_INTERNAL_METRICS (samples);
+
+ if (self->omit_instances)
+ {
+ for (int i = 0; self->omit_instances[i]; i++)
+ {
+ if (g_strcmp0 (instance, self->omit_instances[i]) == 0)
+ return;
+ }
+ }
+
+ for (int i = 0; i < self->n_metrics; i++)
+ {
+ MetricInfo *info = &self->metrics[i];
+ if (g_strcmp0 (metric, info->desc->name) != 0)
+ continue;
+
+ if (info->desc->instanced)
+ {
+ InstanceInfo *inst = g_hash_table_lookup (info->instances, instance);
+ if (inst == NULL)
+ {
+ g_debug ("%s + %s", metric, instance);
+ inst = g_new0 (InstanceInfo, 1);
+ g_hash_table_insert (info->instances, g_strdup (instance), inst);
+ self->need_meta = TRUE;
+ }
+ inst->seen = TRUE;
+ inst->value = value;
+ }
+ else
+ info->value = value;
+ }
+}
+
+static void
+instance_reset (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ InstanceInfo *info = value;
+ info->seen = FALSE;
+}
+
+static gboolean
+instance_unseen (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ InstanceInfo *info = value;
+ return !info->seen;
+}
+
+static void
+cockpit_internal_metrics_tick (CockpitMetrics *metrics,
+ gint64 timestamp)
+{
+ CockpitInternalMetrics *self = (CockpitInternalMetrics *)metrics;
+ struct timeval now_timeval;
+ gint64 now;
+
+ gettimeofday (&now_timeval, NULL);
+ now = timestamp_from_timeval (&now_timeval);
+
+ /* Reset samples
+ */
+ for (int i = 0; i < self->n_metrics; i++)
+ {
+ MetricInfo *info = &self->metrics[i];
+ if (info->desc->instanced)
+ g_hash_table_foreach (info->instances, instance_reset, NULL);
+ else
+ info->value = NAN;
+ }
+
+ /* Sample
+ */
+ if (self->samplers & CPU_SAMPLER)
+ cockpit_cpu_samples (COCKPIT_SAMPLES (self));
+ if (self->samplers & MEMORY_SAMPLER)
+ cockpit_memory_samples (COCKPIT_SAMPLES (self));
+ if (self->samplers & BLOCK_SAMPLER)
+ cockpit_block_samples (COCKPIT_SAMPLES (self));
+ if (self->samplers & NETWORK_SAMPLER)
+ cockpit_network_samples (COCKPIT_SAMPLES (self));
+ if (self->samplers & MOUNT_SAMPLER)
+ cockpit_mount_samples (COCKPIT_SAMPLES (self));
+ if (self->samplers & CGROUP_SAMPLER)
+ cockpit_cgroup_samples (COCKPIT_SAMPLES (self));
+ if (self->samplers & DISK_SAMPLER)
+ cockpit_disk_samples (COCKPIT_SAMPLES (self));
+ if (self->samplers & THERMAL_SAMPLER)
+ cockpit_cpu_temperature (COCKPIT_SAMPLES (self));
+ if (self->samplers & CGROUP_IO_SAMPLER)
+ cockpit_cgroup_disk_usage (COCKPIT_SAMPLES (self));
+
+ /* Check for disappeared instances
+ */
+ for (int i = 0; i < self->n_metrics; i++)
+ {
+ MetricInfo *info = &self->metrics[i];
+ if (info->desc->instanced)
+ if (g_hash_table_foreach_remove (info->instances, instance_unseen, NULL) > 0)
+ self->need_meta = TRUE;
+ }
+
+ /* Send a meta message if necessary. This will also allocate a new
+ buffer and setup the instance indices.
+ */
+ if (self->need_meta)
+ {
+ send_meta (self);
+ self->need_meta = FALSE;
+ }
+
+ /* Ship them out
+ */
+ double **buffer = cockpit_metrics_get_data_buffer (COCKPIT_METRICS (self));
+ for (int i = 0; i < self->n_metrics; i++)
+ {
+ MetricInfo *info = &self->metrics[i];
+ if (info->desc->instanced)
+ {
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, info->instances);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ InstanceInfo *inst = value;
+ buffer[i][inst->index] = inst->value;
+ }
+ }
+ else
+ buffer[i][0] = info->value;
+ }
+
+ cockpit_metrics_send_data (COCKPIT_METRICS (self), now);
+ cockpit_metrics_flush_data (COCKPIT_METRICS (self));
+}
+
+static gboolean
+convert_metric_description (CockpitInternalMetrics *self,
+ JsonNode *node,
+ MetricInfo *info,
+ int index)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ const gchar *name;
+ const gchar *units;
+
+ if (json_node_get_node_type (node) == JSON_NODE_OBJECT)
+ {
+ if (!cockpit_json_get_string (json_node_get_object (node), "name", NULL, &name)
+ || name == NULL)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"metrics\" option was specified (no name for metric %d)", index);
+ return FALSE;
+ }
+
+ if (!cockpit_json_get_string (json_node_get_object (node), "units", NULL, &units))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid units for metric %s (not a string)", name);
+ return FALSE;
+ }
+
+ if (!cockpit_json_get_string (json_node_get_object (node), "derive", NULL, &info->derive))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid derivation mode for metric %s (not a string)", name);
+ return FALSE;
+ }
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"metrics\" option was specified (not an object for metric %d)", index);
+ return FALSE;
+ }
+
+ MetricDescription *desc = find_metric_description (name);
+ if (desc == NULL)
+ {
+ g_message ("unknown internal metric %s", name);
+ }
+ else
+ {
+ if (units && g_strcmp0 (desc->units, units) != 0)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s has units %s, not %s", name, desc->units, units);
+ return FALSE;
+ }
+
+ if (desc->instanced)
+ info->instances = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+ info->desc = desc;
+ self->samplers |= desc->sampler;
+ }
+
+ return TRUE;
+}
+
+static void
+cockpit_internal_metrics_prepare (CockpitChannel *channel)
+{
+ CockpitInternalMetrics *self = COCKPIT_INTERNAL_METRICS (channel);
+ JsonObject *options;
+ JsonArray *metrics;
+ int i;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_internal_metrics_parent_class)->prepare (channel);
+
+ options = cockpit_channel_get_options (channel);
+
+ /* "omit-instances" option */
+ if (!cockpit_json_get_strv (options, "omit-instances", NULL, &self->omit_instances))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"omit-instances\" option (not an array of strings)");
+ return;
+ }
+
+ /* "metrics" option */
+ self->n_metrics = 0;
+ if (!cockpit_json_get_array (options, "metrics", NULL, &metrics))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"metrics\" option was specified (not an array)");
+ return;
+ }
+ if (metrics)
+ self->n_metrics = json_array_get_length (metrics);
+
+ self->metrics = g_new0 (MetricInfo, self->n_metrics);
+ for (i = 0; i < self->n_metrics; i++)
+ {
+ MetricInfo *info = &self->metrics[i];
+ if (!convert_metric_description (self, json_array_get_element (metrics, i), info, i))
+ return;
+ if (!info->desc)
+ {
+ cockpit_channel_close (channel, "not-supported");
+ return;
+ }
+ }
+
+ /* "interval" option */
+ if (!cockpit_json_get_int (options, "interval", 1000, &self->interval))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"interval\" option");
+ return;
+ }
+ else if (self->interval <= 0 || self->interval > G_MAXINT)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"interval\" value: %" G_GINT64_FORMAT, self->interval);
+ return;
+ }
+
+ self->need_meta = TRUE;
+
+ cockpit_metrics_metronome (COCKPIT_METRICS (self), self->interval);
+ cockpit_channel_ready (channel, NULL);
+}
+
+static void
+cockpit_internal_metrics_dispose (GObject *object)
+{
+#if 0
+ CockpitInternalMetrics *self = COCKPIT_INTERNAL_METRICS (object);
+#endif
+ G_OBJECT_CLASS (cockpit_internal_metrics_parent_class)->dispose (object);
+}
+
+static void
+cockpit_internal_metrics_finalize (GObject *object)
+{
+ CockpitInternalMetrics *self = COCKPIT_INTERNAL_METRICS (object);
+
+ g_free (self->omit_instances);
+
+ for (int i = 0; i < self->n_metrics; i++)
+ {
+ MetricInfo *info = &self->metrics[i];
+ if (info->instances)
+ g_hash_table_unref (info->instances);
+ }
+
+ g_free (self->metrics);
+
+ G_OBJECT_CLASS (cockpit_internal_metrics_parent_class)->finalize (object);
+}
+
+static void
+cockpit_internal_metrics_class_init (CockpitInternalMetricsClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitMetricsClass *metrics_class = COCKPIT_METRICS_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->dispose = cockpit_internal_metrics_dispose;
+ gobject_class->finalize = cockpit_internal_metrics_finalize;
+
+ channel_class->prepare = cockpit_internal_metrics_prepare;
+ metrics_class->tick = cockpit_internal_metrics_tick;
+}
+
+static void
+cockpit_samples_interface_init (CockpitSamplesInterface *iface)
+{
+ iface->sample = cockpit_internal_metrics_sample;
+}
diff --git a/src/bridge/cockpitinternalmetrics.h b/src/bridge/cockpitinternalmetrics.h
new file mode 100644
index 0000000..4e9d9f2
--- /dev/null
+++ b/src/bridge/cockpitinternalmetrics.h
@@ -0,0 +1,31 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_INTERNAL_METRICS_H__
+#define COCKPIT_INTERNAL_METRICS_H__
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_INTERNAL_METRICS (cockpit_internal_metrics_get_type ())
+
+GType cockpit_internal_metrics_get_type (void) G_GNUC_CONST;
+
+#endif /* COCKPIT_INTERNAL_METRICS_H__ */
diff --git a/src/bridge/cockpitmemorysamples.c b/src/bridge/cockpitmemorysamples.c
new file mode 100644
index 0000000..2ea0ab8
--- /dev/null
+++ b/src/bridge/cockpitmemorysamples.c
@@ -0,0 +1,83 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitmemorysamples.h"
+
+#include <stdio.h>
+
+/* TODO: this should be optimized so we don't allocate memory and call open()/close() all the time */
+
+void
+cockpit_memory_samples (CockpitSamples *samples)
+{
+ gchar *contents = NULL;
+ GError *error = NULL;
+ gchar **lines = NULL;
+ gsize len;
+ guint n;
+
+ guint64 free_kb = 0;
+ guint64 total_kb = 0;
+ guint64 buffers_kb = 0;
+ guint64 cached_kb = 0;
+ guint64 available_kb = 0;
+ guint64 swap_total_kb = 0;
+ guint64 swap_free_kb = 0;
+
+ if (!g_file_get_contents ("/proc/meminfo", &contents, &len, &error))
+ {
+ g_message ("error loading contents /proc/meminfo: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ /* see 'man proc' for the format of /proc/stat */
+
+ lines = g_strsplit (contents, "\n", -1);
+ for (n = 0; lines != NULL && lines[n] != NULL; n++)
+ {
+ const gchar *line = lines[n];
+ if (g_str_has_prefix (line, "MemTotal:"))
+ g_warn_if_fail (sscanf (line + sizeof ("MemTotal:") - 1, "%" G_GUINT64_FORMAT, &total_kb) == 1);
+ else if (g_str_has_prefix (line, "MemFree:"))
+ g_warn_if_fail (sscanf (line + sizeof ("MemFree:") - 1, "%" G_GUINT64_FORMAT, &free_kb) == 1);
+ else if (g_str_has_prefix (line, "SwapTotal:"))
+ g_warn_if_fail (sscanf (line + sizeof ("SwapTotal:") - 1, "%" G_GUINT64_FORMAT, &swap_total_kb) == 1);
+ else if (g_str_has_prefix (line, "SwapFree:"))
+ g_warn_if_fail (sscanf (line + sizeof ("SwapFree:") - 1, "%" G_GUINT64_FORMAT, &swap_free_kb) == 1);
+ else if (g_str_has_prefix (line, "Buffers:"))
+ g_warn_if_fail (sscanf (line + sizeof ("Buffers:") - 1, "%" G_GUINT64_FORMAT, &buffers_kb) == 1);
+ else if (g_str_has_prefix (line, "Cached:"))
+ g_warn_if_fail (sscanf (line + sizeof ("Cached:") - 1, "%" G_GUINT64_FORMAT, &cached_kb) == 1);
+ else if (g_str_has_prefix (line, "MemAvailable:"))
+ g_warn_if_fail (sscanf (line + sizeof ("MemAvailable:") - 1, "%" G_GUINT64_FORMAT, &available_kb) == 1);
+
+ }
+
+ cockpit_samples_sample (samples, "memory.free", NULL, free_kb * 1024);
+ cockpit_samples_sample (samples, "memory.used", NULL, (total_kb - available_kb) * 1024);
+ cockpit_samples_sample (samples, "memory.cached", NULL, (buffers_kb + cached_kb) * 1024);
+ cockpit_samples_sample (samples, "memory.swap-used", NULL, (swap_total_kb - swap_free_kb) * 1024);
+
+out:
+ g_strfreev (lines);
+ g_free (contents);
+}
diff --git a/src/bridge/cockpitmemorysamples.h b/src/bridge/cockpitmemorysamples.h
new file mode 100644
index 0000000..f6e9dfc
--- /dev/null
+++ b/src/bridge/cockpitmemorysamples.h
@@ -0,0 +1,32 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_MEMORY_SAMPLES_H__
+#define COCKPIT_MEMORY_SAMPLES_H__
+
+#include "cockpitsamples.h"
+
+G_BEGIN_DECLS
+
+void cockpit_memory_samples (CockpitSamples *samples);
+
+
+G_END_DECLS
+
+#endif /* COCKPIT_MEMORY_SAMPLES_H__ */
diff --git a/src/bridge/cockpitmetrics.c b/src/bridge/cockpitmetrics.c
new file mode 100644
index 0000000..0701389
--- /dev/null
+++ b/src/bridge/cockpitmetrics.c
@@ -0,0 +1,613 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitmetrics.h"
+
+#include "common/cockpitjson.h"
+
+#include <math.h>
+
+enum {
+ DERIVE_NONE = 0,
+ DERIVE_DELTA = 1,
+ DERIVE_RATE = 2,
+};
+
+typedef struct {
+ gint derive;
+ gboolean has_instances;
+ gint n_last_instances;
+ gint n_next_instances;
+} MetricInfo;
+
+typedef struct {
+ gboolean interpolate;
+ gboolean compress;
+
+ guint timeout;
+ gint64 next;
+ gint64 interval;
+
+ gint64 meta_interval;
+ gboolean meta_reset;
+ JsonObject *last_meta;
+ JsonObject *next_meta;
+
+ gint n_metrics;
+ MetricInfo *metric_info;
+ gint64 last_timestamp;
+ gint64 next_timestamp;
+ double **last_data;
+ double **next_data;
+ gboolean derived_valid;
+ double **derived;
+
+ JsonArray *message;
+} CockpitMetricsPrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (CockpitMetrics, cockpit_metrics, COCKPIT_TYPE_CHANNEL,
+ G_ADD_PRIVATE (CockpitMetrics));
+
+#define GET_PRIV(self) ((CockpitMetricsPrivate *) cockpit_metrics_get_instance_private (self))
+
+static void
+cockpit_metrics_init (CockpitMetrics *self)
+{
+ GET_PRIV(self)->interpolate = TRUE;
+ GET_PRIV(self)->compress = TRUE;
+}
+
+static void
+cockpit_metrics_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ cockpit_channel_fail (channel, "protocol-error", "received unexpected metrics1 payload");
+}
+
+static void
+cockpit_metrics_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ CockpitMetrics *self = COCKPIT_METRICS (channel);
+
+ if (GET_PRIV(self)->timeout)
+ {
+ g_source_remove (GET_PRIV(self)->timeout);
+ GET_PRIV(self)->timeout = 0;
+ }
+
+ COCKPIT_CHANNEL_CLASS (cockpit_metrics_parent_class)->close (channel, problem);
+}
+
+static void
+cockpit_metrics_dispose (GObject *object)
+{
+ CockpitMetrics *self = COCKPIT_METRICS (object);
+
+ if (GET_PRIV(self)->timeout)
+ {
+ g_source_remove (GET_PRIV(self)->timeout);
+ GET_PRIV(self)->timeout = 0;
+ }
+
+ if (GET_PRIV(self)->last_meta)
+ {
+ json_object_unref (GET_PRIV(self)->last_meta);
+ GET_PRIV(self)->last_meta = NULL;
+ }
+
+ if (GET_PRIV(self)->last_data)
+ {
+ g_free (GET_PRIV(self)->last_data[0]);
+ g_free (GET_PRIV(self)->last_data);
+ GET_PRIV(self)->last_data = NULL;
+ }
+
+ if (GET_PRIV(self)->next_meta)
+ {
+ json_object_unref (GET_PRIV(self)->next_meta);
+ GET_PRIV(self)->next_meta = NULL;
+ }
+
+ if (GET_PRIV(self)->next_data)
+ {
+ g_free (GET_PRIV(self)->next_data[0]);
+ g_free (GET_PRIV(self)->next_data);
+ GET_PRIV(self)->next_data = NULL;
+ }
+
+ if (GET_PRIV(self)->derived)
+ {
+ g_free (GET_PRIV(self)->derived[0]);
+ g_free (GET_PRIV(self)->derived);
+ GET_PRIV(self)->derived = NULL;
+ }
+
+ g_free (GET_PRIV(self)->metric_info);
+ GET_PRIV(self)->metric_info = NULL;
+
+ G_OBJECT_CLASS (cockpit_metrics_parent_class)->dispose (object);
+}
+
+static void
+cockpit_metrics_class_init (CockpitMetricsClass *klass)
+{
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = cockpit_metrics_dispose;
+
+ channel_class->recv = cockpit_metrics_recv;
+ channel_class->close = cockpit_metrics_close;
+}
+
+static gboolean
+on_timeout_tick (gpointer data)
+{
+ CockpitMetrics *self = data;
+ CockpitMetricsClass *klass;
+ gint64 next_interval;
+
+ if (GET_PRIV(self)->timeout > 0)
+ {
+ g_source_remove (GET_PRIV(self)->timeout);
+ GET_PRIV(self)->timeout = 0;
+ }
+
+ klass = COCKPIT_METRICS_GET_CLASS (self);
+ if (klass->tick)
+ (klass->tick) (self, GET_PRIV(self)->next);
+
+ GET_PRIV(self)->next += GET_PRIV(self)->interval;
+ next_interval = GET_PRIV(self)->next - g_get_monotonic_time() / 1000;
+ if (next_interval < 0)
+ next_interval = 0;
+
+ if (next_interval <= G_MAXUINT)
+ GET_PRIV(self)->timeout = g_timeout_add (next_interval, on_timeout_tick, self);
+ else if (next_interval / 1000 <= G_MAXUINT)
+ GET_PRIV(self)->timeout = g_timeout_add_seconds (next_interval / 1000, on_timeout_tick, self);
+ else
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error",
+ "invalid metric timeout tick offset");
+ }
+
+ return FALSE;
+}
+
+void
+cockpit_metrics_metronome (CockpitMetrics *self,
+ gint64 interval)
+{
+ g_return_if_fail (GET_PRIV(self)->timeout == 0);
+ g_return_if_fail (interval > 0);
+
+ GET_PRIV(self)->next = g_get_monotonic_time() / 1000;
+ GET_PRIV(self)->interval = interval;
+ on_timeout_tick (self);
+}
+
+static void
+realloc_next_buffer (CockpitMetrics *self)
+{
+ int total_next_instances = 0;
+ for (int i = 0; i < GET_PRIV(self)->n_metrics; i++)
+ total_next_instances += GET_PRIV(self)->metric_info[i].n_next_instances;
+ g_free (GET_PRIV(self)->next_data[0]);
+ GET_PRIV(self)->next_data[0] = g_new (double, total_next_instances);
+ for (int i = 1; i < GET_PRIV(self)->n_metrics; i++)
+ GET_PRIV(self)->next_data[i] = GET_PRIV(self)->next_data[i-1] + GET_PRIV(self)->metric_info[i-1].n_next_instances;
+}
+
+static void
+realloc_derived_buffer (CockpitMetrics *self)
+{
+ int total_next_instances = 0;
+ for (int i = 0; i < GET_PRIV(self)->n_metrics; i++)
+ total_next_instances += GET_PRIV(self)->metric_info[i].n_next_instances;
+ g_free (GET_PRIV(self)->derived[0]);
+ GET_PRIV(self)->derived[0] = g_new (double, total_next_instances);
+ for (int i = 1; i < GET_PRIV(self)->n_metrics; i++)
+ GET_PRIV(self)->derived[i] = GET_PRIV(self)->derived[i-1] + GET_PRIV(self)->metric_info[i-1].n_next_instances;
+ GET_PRIV(self)->derived_valid = FALSE;
+}
+
+static gboolean
+update_for_meta (CockpitMetrics *self,
+ JsonObject *meta,
+ gboolean reset)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ JsonArray *array;
+ JsonObject *info;
+ JsonArray *instances;
+ guint length;
+ gchar const *derive;
+
+ array = json_object_get_array_member (meta, "metrics");
+ g_return_val_if_fail (array != NULL, FALSE);
+ length = json_array_get_length (array);
+
+ if (GET_PRIV(self)->metric_info == NULL)
+ {
+ GET_PRIV(self)->n_metrics = length;
+ GET_PRIV(self)->metric_info = g_new0 (MetricInfo, length);
+ GET_PRIV(self)->last_data = g_new0 (double *, length);
+ GET_PRIV(self)->next_data = g_new0 (double *, length);
+ GET_PRIV(self)->derived = g_new0 (double *, length);
+
+ reset = TRUE;
+ }
+ else if (GET_PRIV(self)->n_metrics != length)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "number of metrics must not change");
+ return FALSE;
+ }
+
+ for (int i = 0; i < length; i++)
+ {
+ info = json_array_get_object_element (array, i);
+ g_return_val_if_fail (info != NULL, FALSE);
+
+ if (!cockpit_json_get_string (info, "derive", NULL, &derive))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "unsupported derive value: not a string");
+ return FALSE;
+ }
+
+ if (!derive)
+ {
+ GET_PRIV(self)->metric_info[i].derive = DERIVE_NONE;
+ }
+ else if (g_str_equal (derive, "delta"))
+ {
+ GET_PRIV(self)->metric_info[i].derive = DERIVE_DELTA;
+ }
+ else if (g_str_equal (derive, "rate"))
+ {
+ GET_PRIV(self)->metric_info[i].derive = DERIVE_RATE;
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "unsupported derive function: %s", derive);
+ return FALSE;
+ }
+
+ if (!cockpit_json_get_array (info, "instances", NULL, &instances))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "unsupported instances value: not a string");
+ return FALSE;
+ }
+
+ if (instances)
+ {
+ GET_PRIV(self)->metric_info[i].has_instances = TRUE;
+ GET_PRIV(self)->metric_info[i].n_next_instances = json_array_get_length (instances);
+ }
+ else
+ {
+ GET_PRIV(self)->metric_info[i].has_instances = FALSE;
+ GET_PRIV(self)->metric_info[i].n_next_instances = 1;
+ }
+ }
+
+ realloc_next_buffer (self);
+ realloc_derived_buffer (self);
+
+ g_return_val_if_fail (cockpit_json_get_int (meta, "interval", 1000, &GET_PRIV(self)->meta_interval),
+ FALSE);
+
+ GET_PRIV(self)->meta_reset = reset;
+ return TRUE;
+}
+
+static void
+send_object (CockpitMetrics *self,
+ JsonObject *object)
+{
+ CockpitChannel *channel = (CockpitChannel *)self;
+ GBytes *bytes;
+
+ bytes = cockpit_json_write_bytes (object);
+ cockpit_channel_send (channel, bytes, TRUE);
+ g_bytes_unref (bytes);
+}
+
+/*
+ * cockpit_metrics_send_meta:
+ * @self: The CockpitMetrics
+ * @meta: An object containing metric meta data
+ *
+ * Send metrics meta data down the channel. If you use cockpit_metrics_send_data()
+ * then you must use this function instead of sending stuff on the channel directly.
+ */
+void
+cockpit_metrics_send_meta (CockpitMetrics *self,
+ JsonObject *meta,
+ gboolean reset)
+{
+ cockpit_metrics_flush_data (self);
+
+ if (GET_PRIV(self)->next_meta)
+ json_object_unref (GET_PRIV(self)->next_meta);
+ GET_PRIV(self)->next_meta = json_object_ref (meta);
+
+ if (update_for_meta (self, meta, reset))
+ send_object (self, meta);
+}
+
+static void
+send_array (CockpitMetrics *self,
+ JsonArray *array)
+{
+ CockpitChannel *channel = (CockpitChannel *)self;
+ GBytes *bytes;
+ JsonNode *node;
+ gsize length;
+ gchar *ret;
+
+ node = json_node_new (JSON_NODE_ARRAY);
+ json_node_set_array (node, array);
+ ret = cockpit_json_write (node, &length);
+ json_node_free (node);
+
+ bytes = g_bytes_new_take (ret, length);
+ cockpit_channel_send (channel, bytes, TRUE);
+ g_bytes_unref (bytes);
+}
+
+static JsonArray *
+push_array_at (JsonArray *array,
+ guint index,
+ JsonNode *node)
+{
+ if (array == NULL)
+ array = json_array_new ();
+
+ g_assert (index >= json_array_get_length (array));
+
+ while (index > json_array_get_length (array))
+ json_array_add_null_element (array);
+
+ if (node)
+ json_array_add_element (array, node);
+
+ return array;
+}
+
+static JsonArray *
+compute_and_maybe_push_value (CockpitMetrics *self,
+ double interpol_r,
+ int metric,
+ int next_instance,
+ int last_instance,
+ JsonArray *array,
+ int index)
+{
+ double val = GET_PRIV(self)->next_data[metric][next_instance];
+
+ if (last_instance >= 0)
+ {
+ double last_val = GET_PRIV(self)->last_data[metric][last_instance];
+
+ if (GET_PRIV(self)->interpolate && !isnan (last_val))
+ {
+ val = last_val * (1.0 - interpol_r) + val * interpol_r;
+ GET_PRIV(self)->next_data[metric][next_instance] = val;
+ }
+
+ switch (GET_PRIV(self)->metric_info[metric].derive)
+ {
+ case DERIVE_DELTA:
+ val = val - last_val;
+ break;
+ case DERIVE_RATE:
+ val = (val - last_val) / (GET_PRIV(self)->next_timestamp - GET_PRIV(self)->last_timestamp) * 1000;
+ break;
+ case DERIVE_NONE:
+ break;
+ }
+ }
+ else
+ {
+ if (GET_PRIV(self)->metric_info[metric].derive != DERIVE_NONE)
+ val = NAN;
+ }
+
+ if (GET_PRIV(self)->compress == FALSE
+ || next_instance != last_instance
+ || !GET_PRIV(self)->derived_valid
+ || val != GET_PRIV(self)->derived[metric][next_instance])
+ {
+ GET_PRIV(self)->derived[metric][next_instance] = val;
+
+ JsonNode *node = json_node_new (JSON_NODE_VALUE);
+ if (!isnan (val))
+ json_node_set_double (node, val);
+ else
+ json_node_set_boolean (node, FALSE);
+ array = push_array_at (array, index, node);
+ }
+
+ return array;
+}
+
+static int
+find_last_instance (CockpitMetrics *self,
+ int metric,
+ int instance)
+{
+ if (GET_PRIV(self)->meta_reset)
+ return -1;
+
+ if (GET_PRIV(self)->last_meta == GET_PRIV(self)->next_meta)
+ return instance;
+
+ JsonArray *last_metrics = json_object_get_array_member (GET_PRIV(self)->last_meta, "metrics");
+ JsonArray *next_metrics = json_object_get_array_member (GET_PRIV(self)->next_meta, "metrics");
+ if (last_metrics == NULL
+ || next_metrics == NULL
+ || json_array_get_length (last_metrics) <= metric
+ || json_array_get_length (next_metrics) <= metric)
+ return -1;
+
+ JsonObject *last_metric = json_array_get_object_element (last_metrics, metric);
+ JsonObject *next_metric = json_array_get_object_element (next_metrics, metric);
+ if (last_metric == NULL
+ || next_metric == NULL)
+ return -1;
+
+ JsonArray *last_instances = json_object_get_array_member (last_metric, "instances");
+ JsonArray *next_instances = json_object_get_array_member (next_metric, "instances");
+ if (last_instances == NULL
+ || next_instances == NULL
+ || json_array_get_length (next_instances) <= instance)
+ return -1;
+
+ int n_last_instances = json_array_get_length (last_instances);
+ const gchar *next_instance = json_array_get_string_element (next_instances, instance);
+ for (int i = 0; i < n_last_instances; i++)
+ {
+ if (g_strcmp0 (json_array_get_string_element (last_instances, i), next_instance) == 0)
+ return i;
+ }
+
+ return -1;
+}
+
+static JsonArray *
+build_json_data (CockpitMetrics *self, double interpol_r)
+{
+ JsonArray *output = NULL;
+ JsonNode *node;
+
+ for (int i = 0; i < GET_PRIV(self)->n_metrics; i++)
+ {
+ if (GET_PRIV(self)->metric_info[i].has_instances)
+ {
+ JsonArray *res = NULL;
+ for (int j = 0; j < GET_PRIV(self)->metric_info[i].n_next_instances; j++)
+ res = compute_and_maybe_push_value (self, interpol_r, i, j, find_last_instance (self, i, j), res, j);
+ node = json_node_new (JSON_NODE_ARRAY);
+ json_node_take_array (node, res ? res : json_array_new ());
+ output = push_array_at (output, i, node);
+ }
+ else
+ output = compute_and_maybe_push_value (self, interpol_r, i, 0, (GET_PRIV(self)->meta_reset? -1 : 0), output, i);
+ }
+
+ if (output == NULL)
+ output = json_array_new ();
+ return output;
+}
+
+double **
+cockpit_metrics_get_data_buffer (CockpitMetrics *self)
+{
+ return GET_PRIV(self)->next_data;
+}
+
+/*
+ * cockpit_metrics_send_data:
+ * @self: The CockpitMetrics
+ *
+ * Send metrics data down the channel, possibly doing interframe
+ * compression between what was sent last. The data to send comes
+ * from the buffer returned by @cockpit_metrics_get_data_buffer.
+ */
+void
+cockpit_metrics_send_data (CockpitMetrics *self, gint64 timestamp)
+{
+ JsonArray *res;
+ double interpol_r = 1.0;
+
+ if (GET_PRIV(self)->message == NULL)
+ GET_PRIV(self)->message = json_array_new ();
+
+ if (GET_PRIV(self)->interpolate && !GET_PRIV(self)->meta_reset)
+ {
+ double interval = ((double)(timestamp - GET_PRIV(self)->last_timestamp));
+ if (interval > 0)
+ {
+ interpol_r = GET_PRIV(self)->meta_interval / interval;
+ timestamp = GET_PRIV(self)->last_timestamp + GET_PRIV(self)->meta_interval;
+ }
+ }
+
+ GET_PRIV(self)->next_timestamp = timestamp;
+
+ res = build_json_data (self, interpol_r);
+ json_array_add_array_element (GET_PRIV(self)->message, res);
+
+ /* Now setup for the next round by swapping buffers and then making
+ sure that the new 'next' buffer has the right layout.
+ */
+
+ double **t = GET_PRIV(self)->last_data;
+ GET_PRIV(self)->last_data = GET_PRIV(self)->next_data;
+ GET_PRIV(self)->next_data = t;
+
+ if (GET_PRIV(self)->last_meta != GET_PRIV(self)->next_meta)
+ {
+ realloc_next_buffer (self);
+
+ for (int i = 0; i < GET_PRIV(self)->n_metrics; i++)
+ GET_PRIV(self)->metric_info[i].n_last_instances = GET_PRIV(self)->metric_info[i].n_next_instances;
+
+ if (GET_PRIV(self)->last_meta)
+ json_object_unref (GET_PRIV(self)->last_meta);
+ GET_PRIV(self)->last_meta = json_object_ref (GET_PRIV(self)->next_meta);
+ }
+
+ GET_PRIV(self)->derived_valid = TRUE;
+ GET_PRIV(self)->last_timestamp = GET_PRIV(self)->next_timestamp;
+ GET_PRIV(self)->meta_reset = FALSE;
+}
+
+void
+cockpit_metrics_flush_data (CockpitMetrics *self)
+{
+ if (GET_PRIV(self)->message)
+ {
+ send_array (self, GET_PRIV(self)->message);
+ json_array_unref (GET_PRIV(self)->message);
+ GET_PRIV(self)->message = NULL;
+ }
+}
+
+void
+cockpit_metrics_set_interpolate (CockpitMetrics *self,
+ gboolean interpolate)
+{
+ GET_PRIV(self)->interpolate = interpolate;
+}
+
+void
+cockpit_metrics_set_compress (CockpitMetrics *self,
+ gboolean compress)
+{
+ GET_PRIV(self)->compress = compress;
+}
diff --git a/src/bridge/cockpitmetrics.h b/src/bridge/cockpitmetrics.h
new file mode 100644
index 0000000..cc8df45
--- /dev/null
+++ b/src/bridge/cockpitmetrics.h
@@ -0,0 +1,97 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "common/cockpitchannel.h"
+
+#define COCKPIT_TYPE_METRICS (cockpit_metrics_get_type ())
+G_DECLARE_DERIVABLE_TYPE(CockpitMetrics, cockpit_metrics, COCKPIT, METRICS, CockpitChannel)
+
+struct _CockpitMetricsBuffer {
+ int n_elements;
+ double *data;
+};
+
+struct _CockpitMetricsClass {
+ CockpitChannelClass parent_class;
+
+ void (* tick) (CockpitMetrics *metrics,
+ gint64 current_monotic_time);
+
+};
+
+GType cockpit_metrics_get_type (void) G_GNUC_CONST;
+
+void cockpit_metrics_set_interpolate (CockpitMetrics *self,
+ gboolean interpolate);
+
+void cockpit_metrics_set_compress (CockpitMetrics *self,
+ gboolean compress);
+
+void cockpit_metrics_metronome (CockpitMetrics *self,
+ gint64 interval);
+
+/* Sending samples
+ *
+ * Derived classes need to call the following functions in a carefully
+ * orchestrated way in order to send out 'meta' and 'data' messages on
+ * the channel.
+ *
+ * The CockpitMetrics class inspects the 'meta' messages and adjusts
+ * its behavior and data structures to it. Derived classes then fill
+ * the 'data buffer' and the CockpitMetrics will post-process it as
+ * requested in the 'meta' message.
+ */
+
+/* - cockpit_metrics_send_meta (self, meta, reset)
+ *
+ * Send a 'meta' message. When 'reset' is TRUE, the next data message
+ * is treated as if it were the first on the channel: No compression,
+ * derivation, or interpolation is done for it.
+ *
+ * - buffer = cockpit_metrics_get_data_buffer (self)
+ *
+ * Returns a buffer for depositing sample values. The value for
+ * instance J of metric I should be placed into 'buffer[i][j]'. The
+ * number of metrics and the number of instances of each metric is
+ * determined by the "metrics" member of the meta object passed to the
+ * most recent call to cockpit_metrics_send_meta.
+ *
+ * - cockpit_metrics_send_data (self, timestamp)
+ *
+ * Post-processes the samples in the buffer and queues them for
+ * sending. The 'timestamp' is the number of milliseconds since an
+ * arbitrary epoch. If it is not exactly one interval later than the
+ * value in the previous call to this function, the sample values are
+ * 'warped' in time via linear interpolation. The expected interval
+ * is taken from the most recent 'meta' message.
+ *
+ * - cockpit_metrics_flush_data (self)
+ *
+ * Actually send out all queued samples in a 'data' message.
+ */
+
+void cockpit_metrics_send_meta (CockpitMetrics *self,
+ JsonObject *meta,
+ gboolean reset);
+
+double **cockpit_metrics_get_data_buffer (CockpitMetrics *self);
+void cockpit_metrics_send_data (CockpitMetrics *self, gint64 timestamp);
+void cockpit_metrics_flush_data (CockpitMetrics *self);
diff --git a/src/bridge/cockpitmountsamples.c b/src/bridge/cockpitmountsamples.c
new file mode 100644
index 0000000..5863a33
--- /dev/null
+++ b/src/bridge/cockpitmountsamples.c
@@ -0,0 +1,94 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitmountsamples.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <math.h>
+#include <fts.h>
+#include <ctype.h>
+#include <sys/statvfs.h>
+
+void
+cockpit_mount_samples (CockpitSamples *samples)
+{
+ gchar *contents = NULL;
+ GError *error = NULL;
+ gchar **lines = NULL;
+ gchar *line;
+ gchar *esc_dir, *dir;
+ struct statvfs buf;
+ gint64 total;
+ gsize len;
+ guint n;
+
+ if (!g_file_get_contents ("/proc/mounts", &contents, &len, &error))
+ {
+ g_message ("error loading contents /proc/mounts: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ lines = g_strsplit (contents, "\n", -1);
+ for (n = 0; lines != NULL && lines[n] != NULL; n++)
+ {
+ line = lines[n];
+
+ if (strlen (line) == 0)
+ continue;
+
+ /* Only look at real devices
+ */
+ if (line[0] != '/')
+ continue;
+
+ while (*line && !isspace (*line))
+ line++;
+ while (*line && isspace (*line))
+ line++;
+ esc_dir = line;
+ while (*line && !isspace (*line))
+ line++;
+ *line = '\0';
+
+ dir = g_strcompress (esc_dir);
+
+ if (statvfs (dir, &buf) >= 0)
+ {
+ // We explicitly store the fragment size as 64 bits so that
+ // computations with it don't overflow on 32 bit
+ // architectures.
+
+ gint64 frsize = buf.f_frsize;
+ total = frsize * buf.f_blocks;
+ cockpit_samples_sample (samples, "mount.total", dir, total);
+ cockpit_samples_sample (samples, "mount.used", dir, total - frsize * buf.f_bfree);
+ }
+
+ g_free (dir);
+ }
+
+ out:
+ g_strfreev (lines);
+ g_free (contents);
+}
diff --git a/src/bridge/cockpitmountsamples.h b/src/bridge/cockpitmountsamples.h
new file mode 100644
index 0000000..b9d9fbe
--- /dev/null
+++ b/src/bridge/cockpitmountsamples.h
@@ -0,0 +1,32 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_MOUNT_SAMPLES_H__
+#define COCKPIT_MOUNT_SAMPLES_H__
+
+#include "cockpitsamples.h"
+
+G_BEGIN_DECLS
+
+void cockpit_mount_samples (CockpitSamples *samples);
+
+
+G_END_DECLS
+
+#endif /* COCKPIT_MOUNT_SAMPLES_H__ */
diff --git a/src/bridge/cockpitnetworksamples.c b/src/bridge/cockpitnetworksamples.c
new file mode 100644
index 0000000..fa1f135
--- /dev/null
+++ b/src/bridge/cockpitnetworksamples.c
@@ -0,0 +1,106 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitnetworksamples.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+/* TODO: this should be optimized so we don't allocate network and call open()/close() all the time */
+
+void
+cockpit_network_samples (CockpitSamples *samples)
+{
+ gchar *contents = NULL;
+ GError *error = NULL;
+ gchar **lines = NULL;
+ gsize len;
+ guint n;
+
+ guint64 total_rx = 0;
+ guint64 total_tx = 0;
+
+ if (!g_file_get_contents ("/proc/net/dev", &contents, &len, &error))
+ {
+ g_warning ("error loading contents /proc/net/dev: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ lines = g_strsplit (contents, "\n", -1);
+ for (n = 0; lines != NULL && lines[n] != NULL; n++)
+ {
+ const gchar *line = lines[n];
+ gchar iface_name[64]; /* guaranteed to be max 16 chars */
+ guint64 bytes_rx, packets_rx, errors_rx, dropped_rx, fifo_rx, frame_rx, compressed_rx, multicast_rx;
+ guint64 bytes_tx, packets_tx, errors_tx, dropped_tx, fifo_tx, frame_tx, compressed_tx, multicast_tx;
+ gint num_parsed;
+ gchar *ptr;
+
+ /* Format is
+ *
+ * Inter-| Receive | Transmit
+ * face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
+ * lo: 2776770 11307 0 0 0 0 0 0 2776770 11307 0 0 0 0 0 0
+ * eth0: 1215645 2751 0 0 0 0 0 0 1782404 4324 0 0 0 427 0 0
+ * ppp0: 1622270 5552 1 0 0 0 0 0 354130 5669 0 0 0 0 0 0
+ * tap0: 7714 81 0 0 0 0 0 0 7714 81 0 0 0 0 0 0
+ */
+
+ if (n < 2 || strlen (line) == 0)
+ continue;
+
+ num_parsed = sscanf (line,
+ "%s %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT
+ " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT
+ " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT
+ " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT,
+ iface_name,
+ &bytes_rx, &packets_rx, &errors_rx, &dropped_rx,
+ &fifo_rx, &frame_rx, &compressed_rx, &multicast_rx,
+ &bytes_tx, &packets_tx, &errors_tx, &dropped_tx,
+ &fifo_tx, &frame_tx, &compressed_tx, &multicast_tx);
+ if (num_parsed != 17)
+ {
+ g_warning ("Error parsing line %d of file /proc/net/dev (num_parsed=%d): `%s'", n, num_parsed, line);
+ continue;
+ }
+
+ /* remove trailing ':' from interface name */
+ ptr = strrchr (iface_name, ':');
+ if (ptr)
+ *ptr = '\0';
+
+ cockpit_samples_sample (samples, "network.interface.rx", iface_name, bytes_rx);
+ cockpit_samples_sample (samples, "network.interface.tx", iface_name, bytes_tx);
+
+ total_rx += bytes_rx;
+ total_tx += bytes_tx;
+ }
+
+ cockpit_samples_sample (samples, "network.all.rx", NULL, total_rx);
+ cockpit_samples_sample (samples, "network.all.tx", NULL, total_tx);
+
+out:
+ g_strfreev (lines);
+ g_free (contents);
+}
diff --git a/src/bridge/cockpitnetworksamples.h b/src/bridge/cockpitnetworksamples.h
new file mode 100644
index 0000000..1aabd31
--- /dev/null
+++ b/src/bridge/cockpitnetworksamples.h
@@ -0,0 +1,32 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_NETWORK_SAMPLES_H__
+#define COCKPIT_NETWORK_SAMPLES_H__
+
+#include "cockpitsamples.h"
+
+G_BEGIN_DECLS
+
+void cockpit_network_samples (CockpitSamples *samples);
+
+
+G_END_DECLS
+
+#endif /* COCKPIT_NETWORK_SAMPLES_H__ */
diff --git a/src/bridge/cockpitnullchannel.c b/src/bridge/cockpitnullchannel.c
new file mode 100644
index 0000000..d3aa76c
--- /dev/null
+++ b/src/bridge/cockpitnullchannel.c
@@ -0,0 +1,72 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitnullchannel.h"
+
+/**
+ * CockpitNullChannel:
+ *
+ * A #CockpitChannel that never sends messages and ignores
+ * all messages it receives.
+ *
+ * The payload type for this channel is 'null'.
+ */
+
+#define COCKPIT_NULL_CHANNEL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_NULL_CHANNEL, CockpitNullChannel))
+
+typedef struct {
+ CockpitChannel parent;
+} CockpitNullChannel;
+
+typedef struct {
+ CockpitChannelClass parent_class;
+} CockpitNullChannelClass;
+
+G_DEFINE_TYPE (CockpitNullChannel, cockpit_null_channel, COCKPIT_TYPE_CHANNEL);
+
+static void
+cockpit_null_channel_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ g_debug ("received null channel payload");
+}
+
+static void
+cockpit_null_channel_init (CockpitNullChannel *self)
+{
+
+}
+
+static void
+cockpit_null_channel_prepare (CockpitChannel *channel)
+{
+ COCKPIT_CHANNEL_CLASS (cockpit_null_channel_parent_class)->prepare (channel);
+ cockpit_channel_ready (channel, NULL);
+}
+
+static void
+cockpit_null_channel_class_init (CockpitNullChannelClass *klass)
+{
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ channel_class->prepare = cockpit_null_channel_prepare;
+ channel_class->recv = cockpit_null_channel_recv;
+}
diff --git a/src/bridge/cockpitnullchannel.h b/src/bridge/cockpitnullchannel.h
new file mode 100644
index 0000000..b71a5bd
--- /dev/null
+++ b/src/bridge/cockpitnullchannel.h
@@ -0,0 +1,31 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_NULL_CHANNEL_H__
+#define COCKPIT_NULL_CHANNEL_H__
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_NULL_CHANNEL (cockpit_null_channel_get_type ())
+
+GType cockpit_null_channel_get_type (void) G_GNUC_CONST;
+
+#endif /* COCKPIT_NULL_CHANEL_H__ */
diff --git a/src/bridge/cockpitpackages.c b/src/bridge/cockpitpackages.c
new file mode 100644
index 0000000..35a8fd3
--- /dev/null
+++ b/src/bridge/cockpitpackages.c
@@ -0,0 +1,1678 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+
+#include "cockpitpackages.h"
+
+#include "cockpitconnect.h"
+#include "cockpitdbusinternal.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpitconf.h"
+#include "common/cockpithex.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitlocale.h"
+#include "common/cockpitsystem.h"
+#include "common/cockpittemplate.h"
+#include "common/cockpitversion.h"
+#include "common/cockpitwebinject.h"
+#include "common/cockpitwebresponse.h"
+#include "common/cockpitwebserver.h"
+
+#include <glib.h>
+
+#include <string.h>
+
+/* Overridable from tests */
+const gchar **cockpit_bridge_data_dirs = NULL; /* default */
+
+static CockpitPackages *packages_singleton = NULL;
+
+/* Packages might change while the bridge is running, and we support
+ that with slightly complicated handling of checksums.
+
+ The bridge reports a single checksum for the whole bundle of
+ packages. This is the checksum that ends up in URLs and cockpit-ws
+ makes routing decisions based on it.
+
+ When the packages change on disk, this bundle checksum also
+ changes. However, the bridge will not change what it reports; it
+ will keep reporting the original bundle checksum. This ensures
+ that URLs that use the original checksum continue to work.
+
+ The manifest for a package also contains a checksum, and this
+ checksum will change when the package changes. The shell can use
+ this second checksum to decide whether to reload a component, for
+ example.
+
+ The checksum of a package in its manifest is also a bundle
+ checksum. More precisely, it is the oldest bundle checksum that
+ the bridge has seen that includes the exact files of the given
+ package.
+
+ Thus, after the bridge has started, the reported checksum and all
+ manifest checksums are the same. If a new package appears but none
+ of the old packages are changed, the new package has the new bundle
+ checksum in its manifest, and all the old packages still have the
+ reported checksum.
+
+ In order to load the files of a new package, the shell should not
+ use the reported bridge checksum. The request might be routed to a
+ wrong host that has the same reported checksum but not the new
+ files. Loading might also succeed, but the files will then be
+ cached incorrectly. If the new package changes again, we would
+ still load its old files from the cache.
+
+ The shell should also not use the new checksum from the manifest.
+ Loading will not work because cockpit-ws does not know how to route
+ that checksum.
+
+ Thus, the shell needs to load a new (or updated) package with a
+ "@<host>" URL path.
+
+ In other words: The shell can treat the manifest checksum as a
+ per-package checksum for deciding which packages have been updated.
+ Furthermore, if the manifest checksum is equal to the reported
+ bridge checksum, the shell can (and should) use that checksum in
+ URLs to load files from that package.
+
+
+ In order to detect whether a package has changed or not, the bridge
+ also keeps track of per-package checksums. These never appear in
+ the API.
+*/
+
+struct _CockpitPackages {
+ CockpitWebServer *web_server;
+ GHashTable *listing;
+ gchar *checksum;
+ gchar *bundle_checksum;
+ JsonObject *json;
+ gchar *locale;
+
+ gboolean dbus_inited;
+ void (*on_change_callback) (gconstpointer data);
+ gconstpointer on_change_callback_data;
+ gboolean reload_hint;
+};
+
+struct _CockpitPackage {
+ gchar *name;
+ gchar *directory;
+ JsonObject *manifest;
+ GHashTable *paths;
+ gchar *unavailable;
+ gchar *content_security_policy;
+ gchar *own_checksum;
+ gchar *bundle_checksum;
+};
+
+/*
+ * Note that the way we construct checksums is not a stable part of our ABI. It
+ * can be changed, as long as it then produces a different set of checksums
+ *
+ * It is also *not* a security sensitive use case. The hashes are never shared
+ * or compared between different users, only the same user (with same credentials)
+ * on different machines.
+ */
+
+static gboolean package_walk_directory (GChecksum *own_checksum,
+ GChecksum *bundle_checksum,
+ GHashTable *paths,
+ const gchar *root,
+ const gchar *directory);
+
+static void
+cockpit_package_free (gpointer data)
+{
+ CockpitPackage *package = data;
+ g_debug ("%s: freeing package", package->name);
+ g_free (package->name);
+ g_free (package->directory);
+ g_free (package->content_security_policy);
+ if (package->paths)
+ g_hash_table_unref (package->paths);
+ if (package->manifest)
+ json_object_unref (package->manifest);
+ g_free (package->unavailable);
+ g_free (package->own_checksum);
+ g_free (package->bundle_checksum);
+ g_free (package);
+}
+
+static CockpitPackage *
+cockpit_package_new (const gchar *name)
+{
+ CockpitPackage *package = g_new0 (CockpitPackage, 1);
+ package->name = g_strdup (name);
+ return package;
+}
+
+static gboolean
+validate_package (const gchar *name)
+{
+ gsize len = strspn (name, COCKPIT_RESOURCE_PACKAGE_VALID);
+ return len && name[len] == '\0';
+}
+
+static gboolean
+validate_path (const gchar *name)
+{
+ static const gchar *allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.,@/";
+ gsize len = strspn (name, allowed);
+ return len && name[len] == '\0';
+}
+
+static gboolean
+package_walk_file (GChecksum *own_checksum,
+ GChecksum *bundle_checksum,
+ GHashTable *paths,
+ const gchar *root,
+ const gchar *filename)
+{
+ gchar *path = NULL;
+ gchar *string = NULL;
+ GError *error = NULL;
+ GMappedFile *mapped = NULL;
+ gboolean ret = FALSE;
+ GBytes *bytes;
+
+ /* Skip invalid files: we refuse to serve them (below) */
+ if (!validate_path (filename))
+ {
+ g_debug ("package has an invalid path name: %s", filename);
+ ret = TRUE;
+ goto out;
+ }
+
+ path = g_build_filename (root, filename, NULL);
+ if (g_file_test (path, G_FILE_TEST_IS_DIR))
+ {
+ ret = package_walk_directory (own_checksum, bundle_checksum, paths, root, filename);
+ goto out;
+ }
+
+ mapped = g_mapped_file_new (path, FALSE, &error);
+ if (error)
+ {
+ g_warning ("couldn't open file: %s: %s", path, error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ if (own_checksum && bundle_checksum)
+ {
+ bytes = g_mapped_file_get_bytes (mapped);
+ string = g_compute_checksum_for_bytes (G_CHECKSUM_SHA256, bytes);
+ g_bytes_unref (bytes);
+
+ /*
+ * Place file name and hex checksum into the checksums,
+ * include the null terminators so these values
+ * cannot be accidentally have a boundary discrepancy.
+ */
+ g_checksum_update (own_checksum, (const guchar *)filename,
+ strlen (filename) + 1);
+ g_checksum_update (own_checksum, (const guchar *)string,
+ strlen (string) + 1);
+ g_checksum_update (bundle_checksum, (const guchar *)filename,
+ strlen (filename) + 1);
+ g_checksum_update (bundle_checksum, (const guchar *)string,
+ strlen (string) + 1);
+ }
+
+ if (paths)
+ {
+ g_hash_table_add (paths, path);
+ path = NULL;
+ }
+
+ ret = TRUE;
+
+out:
+ if (mapped)
+ g_mapped_file_unref (mapped);
+ g_free (string);
+ g_free (path);
+ return ret;
+}
+
+static gint
+compare_filenames (gconstpointer v1,
+ gconstpointer v2)
+{
+ const gchar *const *s1 = v1;
+ const gchar *const *s2 = v2;
+
+ /* Just a simple byte compare, nothing fancy */
+ return strcmp (*s1, *s2);
+}
+
+static gchar **
+directory_filenames (const char *directory)
+{
+ GError *error = NULL;
+ GPtrArray *names;
+ const gchar *name;
+ GDir *dir;
+
+ dir = g_dir_open (directory, 0, &error);
+ if (error != NULL)
+ {
+ g_warning ("couldn't list directory: %s: %s", directory, error->message);
+ g_error_free (error);
+ return FALSE;
+ }
+
+ names = g_ptr_array_new ();
+ for (;;)
+ {
+ name = g_dir_read_name (dir);
+ if (!name)
+ break;
+ g_ptr_array_add (names, g_strdup (name));
+ }
+
+ g_dir_close (dir);
+
+ g_ptr_array_sort (names, compare_filenames);
+ g_ptr_array_add (names, NULL);
+
+ return (gchar **)g_ptr_array_free (names, FALSE);
+}
+
+static gboolean
+package_walk_directory (GChecksum *own_checksum,
+ GChecksum *bundle_checksum,
+ GHashTable *paths,
+ const gchar *root,
+ const gchar *directory)
+{
+ gboolean ret = FALSE;
+ gchar *path = NULL;
+ gchar **names = NULL;
+ gchar *filename;
+ gint i;
+
+ path = g_build_filename (root, directory, NULL);
+ names = directory_filenames (path);
+ if (!names)
+ goto out;
+
+ ret = TRUE;
+ for (i = 0; names[i] != NULL; i++)
+ {
+ if (directory)
+ filename = g_build_filename (directory, names[i], NULL);
+ else
+ filename = g_strdup (names[i]);
+ ret = package_walk_file (own_checksum, bundle_checksum, paths, root, filename);
+ g_free (filename);
+ if (!ret)
+ goto out;
+ }
+
+out:
+ g_free (path);
+ g_strfreev (names);
+ return ret;
+}
+
+static JsonObject *
+read_json_file (const gchar *path,
+ GError **error)
+{
+ g_autoptr(GMappedFile) mapped = g_mapped_file_new (path, FALSE, error);
+
+ if (!mapped)
+ return NULL;
+
+ g_autoptr(GBytes) bytes = g_mapped_file_get_bytes (mapped);
+ return cockpit_json_parse_bytes (bytes, error);
+}
+
+static GBytes *
+expand_libexec (const gchar *variable,
+ gpointer user_data)
+{
+ if (g_str_equal (variable, "libexecdir"))
+ return g_bytes_new (LIBEXECDIR, strlen (LIBEXECDIR));
+
+ return NULL;
+}
+
+static void
+apply_override (JsonObject *manifest,
+ const char *path)
+{
+ g_autoptr(GError) error = NULL;
+
+ g_autoptr(JsonObject) override = read_json_file (path, &error);
+ if (override)
+ {
+ cockpit_json_patch (manifest, override);
+ }
+ else
+ {
+ if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ g_debug ("no override found in %s", path);
+ else
+ g_warning ("couldn't read %s: %s", path, error->message);
+ }
+}
+
+static JsonObject *
+read_package_manifest (const gchar *directory,
+ const gchar *package)
+{
+ JsonObject *manifest = NULL;
+ GError *error = NULL;
+
+ g_autofree gchar *manifest_path = g_build_filename (directory, "manifest.json", NULL);
+ manifest = read_json_file (manifest_path, &error);
+ if (!manifest)
+ {
+ if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ g_debug ("%s: no manifest found", package);
+ else if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOTDIR))
+ g_warning ("%s: couldn't read manifest.json: %s", package, error->message);
+ g_clear_error (&error);
+ }
+ else if (!validate_package (package))
+ {
+ g_warning ("%s: package has invalid name", package);
+ json_object_unref (manifest);
+ return NULL;
+ }
+ else
+ {
+ /* possible override locations, in ascending priority */
+ g_autofree gchar *pkgdir_override = g_build_filename (directory, "override.json", NULL);
+ /* same directory as the package itself */
+ apply_override (manifest, pkgdir_override);
+
+ g_autofree gchar *package_override_name = g_strconcat (package, ".override.json", NULL);
+
+ const char * const *dirs = cockpit_conf_get_dirs ();
+ for (gint i = 0; dirs[i]; i++)
+ {
+ g_autofree gchar *path = g_build_filename (dirs[i], "cockpit", package_override_name, NULL);
+ apply_override (manifest, path);
+ }
+
+ g_autofree gchar *user_override = g_build_filename (g_get_user_config_dir (), "cockpit", package_override_name, NULL);
+ apply_override (manifest, user_override);
+
+ json_object_seal (manifest);
+
+ JsonObject *expanded = cockpit_template_expand_json (manifest, "${", "}",
+ expand_libexec, NULL);
+ json_object_unref (manifest);
+
+ manifest = expanded;
+ }
+
+ return manifest;
+}
+
+static const gchar *
+read_package_name (JsonObject *manifest,
+ const gchar *name)
+{
+ const gchar *value;
+
+ if (!cockpit_json_get_string (manifest, "name", name, &value))
+ {
+ g_warning ("%s: invalid \"name\" field in package manifest", name);
+ value = NULL;
+ }
+ else if (!validate_package (value))
+ {
+ g_warning ("%s: invalid package \"name\" field in manifest", name);
+ value = NULL;
+ }
+
+ return value;
+}
+
+static gint
+compar_manifest_priority (JsonObject *manifest1,
+ JsonObject *manifest2,
+ const gchar *name)
+{
+ gdouble priority1 = 1;
+ gdouble priority2 = 1;
+
+ if (!cockpit_json_get_double (manifest1, "priority", 1, &priority1) ||
+ !cockpit_json_get_double (manifest2, "priority", 1, &priority2))
+ {
+ g_message ("%s%sinvalid \"priority\" field in package manifest",
+ name ? name : "", name ? ": " : "");
+ }
+
+ if (priority1 == priority2)
+ return 0;
+ else if (priority1 < priority2)
+ return -1;
+ else
+ return 1;
+}
+
+static gint
+compar_package_priority (gconstpointer value1,
+ gconstpointer value2,
+ gpointer user_data)
+{
+ const CockpitPackage *package1 = value1;
+ const CockpitPackage *package2 = value2;
+ return compar_manifest_priority (package1->manifest, package2->manifest, user_data);
+}
+
+static gboolean
+check_package_compatible (CockpitPackage *package,
+ JsonObject *manifest)
+{
+ const gchar *minimum = NULL;
+ JsonObject *requires;
+ GList *l, *keys;
+
+ if (!cockpit_json_get_object (manifest, "requires", NULL, &requires))
+ {
+ g_warning ("%s: invalid \"requires\" field", package->name);
+ return FALSE;
+ }
+
+ if (!requires)
+ return TRUE;
+
+ if (!cockpit_json_get_string (requires, "cockpit", NULL, &minimum))
+ {
+ g_warning ("%s: invalid \"cockpit\" requirement field", package->name);
+ return FALSE;
+ }
+
+ /*
+ * This is the minimum version of the bridge and base package
+ * which should always be shipped together.
+ */
+ if (minimum && cockpit_version_compare (PACKAGE_VERSION, minimum) < 0)
+ {
+ g_message ("%s: package requires a later version of cockpit: %s > %s",
+ package->name, minimum, PACKAGE_VERSION);
+ package->unavailable = g_strdup_printf ("This package requires Cockpit version %s or later", minimum);
+ }
+
+ /* Look for any other unknown keys */
+ keys = json_object_get_members (requires);
+ for (l = keys; l != NULL; l = g_list_next (l))
+ {
+ /* All other requires are unknown until a later time */
+ if (!g_str_equal (l->data, "cockpit"))
+ {
+ g_message ("%s: package has an unknown requirement: %s", package->name, (gchar *)l->data);
+ package->unavailable = g_strdup ("This package is not compatible with this version of Cockpit");
+ }
+ }
+ g_list_free (keys);
+
+ return TRUE;
+}
+
+static gboolean
+setup_package_manifest (CockpitPackage *package,
+ JsonObject *manifest)
+{
+ const gchar *field = "content-security-policy";
+ const gchar *policy = NULL;
+
+ if (!check_package_compatible (package, manifest))
+ return FALSE;
+
+ if (!cockpit_json_get_string (manifest, field, NULL, &policy) ||
+ (policy && !cockpit_web_response_is_header_value (policy)))
+ {
+ g_warning ("%s: invalid %s: %s", package->name, field, policy);
+ return FALSE;
+ }
+
+ package->content_security_policy = g_strdup (policy);
+ json_object_remove_member (manifest, field);
+
+ package->manifest = json_object_ref (manifest);
+ return TRUE;
+}
+
+static gchar *
+calc_package_directory (JsonObject *manifest,
+ const gchar *name,
+ const gchar *path)
+{
+ const gchar *base = NULL;
+
+ /* See if the module override the base directory */
+ if (!cockpit_json_get_string (manifest, "base", NULL, &base))
+ {
+ g_warning ("%s: invalid 'base' field in manifest", name);
+ return NULL;
+ }
+
+ if (!base)
+ {
+ return g_strdup (path);
+ }
+ else if (g_path_is_absolute (base))
+ {
+ return g_strdup (base);
+ }
+ else
+ {
+ return g_build_filename (path, base, NULL);
+ }
+}
+
+static CockpitPackage *
+maybe_add_package (GHashTable *listing,
+ GHashTable *old_listing,
+ const gchar *parent,
+ const gchar *name,
+ GChecksum *bundle_checksum,
+ gboolean system)
+{
+ CockpitPackage *package = NULL;
+ gchar *path = NULL;
+ gchar *directory = NULL;
+ JsonObject *manifest = NULL;
+ GChecksum *own_checksum = NULL;
+ GHashTable *paths = NULL;
+ CockpitPackage *old_package;
+
+ path = g_build_filename (parent, name, NULL);
+
+ manifest = read_package_manifest (path, name);
+ if (!manifest)
+ goto out;
+
+ /* Manifest could specify a different name */
+ name = read_package_name (manifest, name);
+ if (!name)
+ goto out;
+
+ /* In case the package is already present */
+ package = g_hash_table_lookup (listing, name);
+ if (package)
+ {
+ if (compar_manifest_priority (manifest, package->manifest, name) <= 0)
+ {
+ package = NULL;
+ goto out;
+ }
+ else
+ {
+ package = NULL;
+ }
+ }
+
+ directory = calc_package_directory (manifest, name, path);
+
+ if (system)
+ paths = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ if (bundle_checksum)
+ own_checksum = g_checksum_new (G_CHECKSUM_SHA256);
+
+ if (bundle_checksum || paths)
+ {
+ if (!package_walk_directory (own_checksum, bundle_checksum, paths, directory, NULL))
+ goto out;
+ }
+
+ package = cockpit_package_new (name);
+ package->directory = directory;
+ directory = NULL;
+
+ if (own_checksum)
+ {
+ /* digest the whole final manifest, which may have overrides from external directories */
+ gsize manifest_len;
+ g_autofree gchar *manifest_str = cockpit_json_write_object (manifest, &manifest_len);
+ g_checksum_update (own_checksum, (guchar *) manifest_str, manifest_len);
+ g_checksum_update (bundle_checksum, (guchar *) manifest_str, manifest_len);
+
+ package->own_checksum = g_strdup (g_checksum_get_string (own_checksum));
+ }
+
+ // Keep the old bundle_checksum for this package if none of its
+ // files has changed.
+ if (old_listing)
+ {
+ old_package = g_hash_table_lookup (old_listing, name);
+ if (old_package &&
+ old_package->bundle_checksum &&
+ old_package->own_checksum &&
+ g_strcmp0 (old_package->own_checksum, package->own_checksum) == 0)
+ {
+ package->bundle_checksum = g_strdup (old_package->bundle_checksum);
+ }
+ }
+
+ if (paths)
+ package->paths = g_hash_table_ref (paths);
+
+ if (!setup_package_manifest (package, manifest))
+ {
+ cockpit_package_free (package);
+ package = NULL;
+ goto out;
+ }
+
+ g_hash_table_replace (listing, package->name, package);
+ g_debug ("%s: added package at %s", package->name, package->directory);
+
+out:
+ g_free (directory);
+ g_free (path);
+ if (manifest)
+ json_object_unref (manifest);
+ if (paths)
+ g_hash_table_unref (paths);
+ if (own_checksum)
+ g_checksum_free (own_checksum);
+ return package;
+}
+
+static gboolean
+build_package_listing (GHashTable *listing,
+ GChecksum *checksum,
+ GHashTable *old_listing)
+{
+ const gchar *const *directories;
+ gchar *directory = NULL;
+ gchar **packages;
+ gint i, j;
+
+ /* User package directory: no checksums */
+ if (!cockpit_bridge_data_dirs)
+ directory = g_build_filename (g_get_user_data_dir (), "cockpit", NULL);
+ if (directory && g_file_test (directory, G_FILE_TEST_IS_DIR))
+ {
+ packages = directory_filenames (directory);
+ for (j = 0; packages[j] != NULL; j++)
+ {
+ /* If any user packages installed, no checksum */
+ if (maybe_add_package (listing, old_listing, directory, packages[j], checksum, FALSE))
+ checksum = NULL;
+ }
+ g_strfreev (packages);
+ }
+ g_free (directory);
+
+ /* System package directories */
+ if (cockpit_bridge_data_dirs)
+ directories = cockpit_bridge_data_dirs;
+ else
+ directories = g_get_system_data_dirs ();
+
+ for (i = 0; directories[i] != NULL; i++)
+ {
+ directory = g_build_filename (directories[i], "cockpit", NULL);
+ if (g_file_test (directory, G_FILE_TEST_IS_DIR))
+ {
+ packages = directory_filenames (directory);
+ for (j = 0; packages && packages[j] != NULL; j++)
+ maybe_add_package (listing, old_listing, directory, packages[j], checksum, TRUE);
+ g_strfreev (packages);
+ }
+ g_free (directory);
+ }
+
+ return checksum != NULL;
+}
+
+static void
+build_packages (CockpitPackages *packages)
+{
+ GHashTable *old_listing;
+ JsonObject *root = NULL;
+ CockpitPackage *package;
+ GChecksum *checksum;
+ GList *names, *l;
+ const gchar *name;
+
+ old_listing = packages->listing;
+
+ packages->listing = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, cockpit_package_free);
+ g_free (packages->bundle_checksum);
+ packages->bundle_checksum = NULL;
+
+ checksum = g_checksum_new (G_CHECKSUM_SHA256);
+ if (build_package_listing (packages->listing, checksum, old_listing))
+ {
+ packages->bundle_checksum = g_strdup (g_checksum_get_string (checksum));
+ if (!packages->checksum)
+ packages->checksum = g_strdup (packages->bundle_checksum);
+ }
+ g_checksum_free (checksum);
+ if (old_listing)
+ g_hash_table_unref (old_listing);
+
+ /* Build JSON packages block and fixup checksums */
+ if (packages->json)
+ json_object_unref (packages->json);
+ packages->json = root = json_object_new ();
+ if (packages->checksum)
+ json_object_set_string_member (root, ".checksum", packages->checksum);
+
+ names = g_hash_table_get_keys (packages->listing);
+ for (l = names; l != NULL; l = g_list_next (l))
+ {
+ name = l->data;
+ package = g_hash_table_lookup (packages->listing, name);
+ if (package->manifest) {
+ json_object_set_object_member (root, name, json_object_ref (package->manifest));
+ if (!package->bundle_checksum)
+ package->bundle_checksum = g_strdup (packages->bundle_checksum);
+ if (package->bundle_checksum)
+ json_object_set_string_member (package->manifest, ".checksum", package->bundle_checksum);
+ }
+ }
+
+ g_list_free (names);
+}
+
+gchar *
+cockpit_packages_resolve (CockpitPackages *packages,
+ const gchar *name,
+ const gchar *path,
+ CockpitPackage **package)
+{
+ CockpitPackage *mod;
+
+ if (!path || !name)
+ return NULL;
+
+ /*
+ * This is *not* a security check. We're accessing files as the user.
+ * What this does is prevent package authors from drawing outside the
+ * lines. Keeps everyone honest.
+ */
+ if (strstr (path, "../") || strstr (path, "/..") || !validate_path (path))
+ {
+ g_message ("invalid 'path' used as a resource: %s", path);
+ return NULL;
+ }
+
+ if (!validate_package (name))
+ {
+ g_message ("invalid 'package' name: %s", name);
+ return NULL;
+ }
+
+ mod = g_hash_table_lookup (packages->listing, name);
+ if (mod == NULL)
+ {
+ g_debug ("resource package was not found: %s", name);
+ return NULL;
+ }
+
+ if (package)
+ *package = mod;
+ return g_build_filename (mod->directory, path, NULL);
+}
+
+static gboolean
+handle_package_checksum (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitPackages *packages)
+{
+ GHashTable *out_headers;
+ GBytes *content;
+
+ if (packages->checksum)
+ content = g_bytes_new (packages->checksum, strlen (packages->checksum));
+ else
+ content = g_bytes_new_static ("", 0);
+
+ out_headers = cockpit_web_server_new_table ();
+ g_hash_table_insert (out_headers, g_strdup ("Content-Type"), g_strdup ("text/plain"));
+
+ if (packages->checksum)
+ {
+ g_hash_table_insert (out_headers, g_strdup (COCKPIT_CHECKSUM_HEADER),
+ g_strdup (packages->checksum));
+ }
+
+ cockpit_web_response_content (response, out_headers, content, NULL);
+ g_bytes_unref (content);
+ g_hash_table_unref (out_headers);
+ return TRUE;
+}
+
+static void
+set_manifest_headers (CockpitWebResponse *response,
+ CockpitPackages *packages,
+ GHashTable *out_headers)
+{
+ if (packages->checksum)
+ {
+ g_hash_table_insert (out_headers, g_strdup (COCKPIT_CHECKSUM_HEADER),
+ g_strdup (packages->checksum));
+ g_hash_table_insert (out_headers, g_strdup ("ETag"),
+ g_strdup_printf ("\"$%s\"", packages->checksum));
+ }
+ else
+ {
+ cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_NO_CACHE);
+ }
+}
+
+static gboolean
+handle_package_manifests_js (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitPackages *packages)
+{
+ const gchar *template =
+ "(function (root, data) { if (typeof define === 'function' && define.amd) { define(data); }"
+ " if(typeof cockpit === 'object') { cockpit.manifests = data; }"
+ " else { root.manifests = data; } }(this, ";
+ GHashTable *out_headers;
+ GBytes *content;
+ GBytes *prefix;
+ GBytes *suffix;
+
+ prefix = g_bytes_new_static (template, strlen (template));
+ content = cockpit_json_write_bytes (packages->json);
+ suffix = g_bytes_new_static ("));", 3);
+
+ out_headers = cockpit_web_server_new_table ();
+
+ set_manifest_headers (response, packages, out_headers);
+ cockpit_web_response_content (response, out_headers, prefix, content, suffix, NULL);
+
+ g_hash_table_unref (out_headers);
+ g_bytes_unref (prefix);
+ g_bytes_unref (suffix);
+ g_bytes_unref (content);
+ return TRUE;
+}
+
+static gboolean
+handle_package_manifests_json (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitPackages *packages)
+{
+ GHashTable *out_headers;
+ GBytes *content;
+
+ out_headers = cockpit_web_server_new_table ();
+
+ content = cockpit_json_write_bytes (packages->json);
+
+ set_manifest_headers (response, packages, out_headers);
+ cockpit_web_response_content (response, out_headers, content, NULL);
+
+ g_hash_table_unref (out_headers);
+ g_bytes_unref (content);
+
+ return TRUE;
+}
+
+static gboolean
+package_content (CockpitPackages *packages,
+ CockpitWebResponse *response,
+ const gchar *name,
+ const gchar *path,
+ const gchar *language,
+ gboolean allow_gzipped,
+ const gchar *self_origin,
+ GHashTable *headers)
+{
+ GBytes *uncompressed = NULL;
+ CockpitPackage *package;
+ gboolean result = FALSE;
+ gchar *filename = NULL;
+ GError *error = NULL;
+ GBytes *bytes = NULL;
+ gboolean globbing;
+ gboolean gzipped = FALSE;
+ gboolean is_language_specific = FALSE;
+ const gchar *type;
+ gchar *policy;
+
+ if (!self_origin)
+ self_origin = cockpit_web_response_get_origin (response);
+
+ GList *names;
+ globbing = g_str_equal (name, "*");
+ if (globbing)
+ {
+ names = g_hash_table_get_keys (packages->listing);
+ names = g_list_sort (names, (GCompareFunc) g_strcmp0);
+
+ /* When globbing files together no gzip encoding is possible */
+ allow_gzipped = FALSE;
+ }
+ else
+ names = g_list_prepend (NULL, (gchar *) name);
+
+ for (GList *l = names; l != NULL; l = g_list_next (l))
+ {
+ name = l->data;
+ g_free (filename);
+ package = NULL;
+
+ /* Resolve the path name and check it */
+ filename = cockpit_packages_resolve (packages, name, path, &package);
+
+ if (!filename)
+ {
+ /* On the first round */
+ if (l == names)
+ {
+ /* cockpit_packages_resolve() only fails if the entire
+ * package is missing. Check if that's a package that
+ * ought to have been available and issue a more helpful
+ * message.
+ */
+ if (g_str_equal (name, "shell") || g_str_equal (name, "systemd"))
+ cockpit_web_response_error (response, 404, NULL, "Server is missing the cockpit-system package");
+ else
+ cockpit_web_response_error (response, 404, NULL, NULL);
+ }
+ else
+ cockpit_web_response_abort (response);
+ goto out;
+ }
+
+ if (bytes)
+ g_bytes_unref (bytes);
+
+ g_clear_error (&error);
+
+ bytes = cockpit_web_response_negotiation (filename, package ? package->paths : NULL, language, &is_language_specific, &gzipped, &error);
+
+ /* HACK: if a translation file is missing, just return empty
+ * content. This saves a whole lot of 404s in the developer
+ * console when trying to fetch po.js for English, for example.
+ * Note that error == NULL only in the 'not found' case.
+ */
+ if (bytes == NULL && error == NULL && g_str_has_suffix (filename, "/po.js"))
+ {
+ bytes = g_bytes_new_static ("", 0);
+ is_language_specific = TRUE;
+ gzipped = FALSE;
+ }
+
+ /* When globbing most errors result in a zero length block */
+ if (globbing)
+ {
+ if (error)
+ {
+ g_message ("%s", error->message);
+ bytes = g_bytes_new_static ("", 0);
+ gzipped = FALSE;
+ is_language_specific = FALSE;
+ }
+ }
+ else
+ {
+ if (error)
+ {
+ if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_ACCES) ||
+ g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_PERM))
+ {
+ g_message ("%s", error->message);
+ cockpit_web_response_error (response, 403, NULL, NULL);
+ }
+ else if (error)
+ {
+ g_message ("%s", error->message);
+ cockpit_web_response_error (response, 500, NULL, NULL);
+ }
+ goto out;
+ }
+ else if (!bytes)
+ {
+ cockpit_web_response_error (response, 404, NULL, NULL);
+ goto out;
+ }
+ else if (package && package->unavailable)
+ {
+ cockpit_web_response_error (response, 503, NULL, "%s", package->unavailable);
+ goto out;
+ }
+ }
+
+ /* If the response is language specific, don't cache the file. Caching "po.js" breaks
+ * changing the language in Chromium, as that does not respect `Vary: Cookie` properly.
+ * See https://github.com/cockpit-project/cockpit/issues/8160 */
+ if (is_language_specific || globbing)
+ cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_NO_CACHE);
+
+ /* Do we need to decompress this content? */
+ if (gzipped && !allow_gzipped)
+ {
+ g_clear_error (&error);
+ uncompressed = cockpit_web_response_gunzip (bytes, &error);
+ if (error)
+ {
+ g_message ("couldn't decompress: %s: %s", filename, error->message);
+ g_clear_error (&error);
+ uncompressed = g_bytes_new_static ("", 0);
+ }
+ g_bytes_unref (bytes);
+ bytes = uncompressed;
+ gzipped = FALSE;
+ }
+
+ /* The first one */
+ if (l == names)
+ {
+ if (gzipped)
+ g_hash_table_insert (headers, g_strdup ("Content-Encoding"), g_strdup ("gzip"));
+
+ type = cockpit_web_response_content_type (path);
+ if (type)
+ {
+ g_hash_table_insert (headers, g_strdup ("Content-Type"), g_strdup (type));
+ if (g_str_has_prefix (type, "text/html"))
+ {
+ if (package)
+ {
+ policy = cockpit_web_response_security_policy (package->content_security_policy,
+ self_origin);
+ g_hash_table_insert (headers, g_strdup ("Content-Security-Policy"), policy);
+ }
+ }
+ }
+
+ cockpit_web_response_headers_full (response, 200, "OK", -1, headers);
+ }
+
+ if (bytes && !cockpit_web_response_queue (response, bytes))
+ goto out;
+ }
+
+ cockpit_web_response_complete (response);
+ result = TRUE;
+
+out:
+ if (bytes)
+ g_bytes_unref (bytes);
+ g_list_free (names);
+ g_free (filename);
+ g_clear_error (&error);
+ return result;
+}
+
+static gboolean
+handle_packages (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *unused,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitPackages *packages)
+{
+ gchar *name;
+ const gchar *path;
+ GHashTable *out_headers = NULL;
+ gchar **languages = NULL;
+ gchar **encodings = NULL;
+ gchar *origin = NULL;
+ const gchar *protocol;
+ const gchar *accept;
+ const gchar *host;
+
+ name = cockpit_web_response_pop_path (response);
+ path = cockpit_web_response_get_path (response);
+
+ if (name == NULL || path == NULL)
+ {
+ cockpit_web_response_error (response, 404, NULL, NULL);
+ goto out;
+ }
+
+ out_headers = cockpit_web_server_new_table ();
+
+ accept = g_hash_table_lookup (headers, "Accept-Language");
+ languages = cockpit_web_server_parse_accept_list (accept, NULL);
+
+ /*
+ * This is how we find out about the frontends cockpitlang
+ * environment. We tell this process to update its locale
+ * if it has changed.
+ */
+ cockpit_locale_set_language (languages[0]);
+
+ if (packages->checksum)
+ {
+ g_hash_table_insert (out_headers, g_strdup (COCKPIT_CHECKSUM_HEADER),
+ g_strdup (packages->checksum));
+ }
+ else
+ {
+ cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_NO_CACHE);
+ }
+
+ protocol = g_hash_table_lookup (headers, "X-Forwarded-Proto");
+ host = g_hash_table_lookup (headers, "X-Forwarded-Host");
+ if (protocol && host)
+ origin = g_strdup_printf ("%s://%s", protocol, host);
+ if (origin)
+ g_hash_table_insert (out_headers, g_strdup ("Access-Control-Allow-Origin"), origin);
+
+ package_content (packages, response, name, path, languages[0],
+ cockpit_web_request_accepts_encoding (request, "gzip"),
+ origin, out_headers);
+
+out:
+ if (out_headers)
+ g_hash_table_unref (out_headers);
+ g_strfreev (languages);
+ g_strfreev (encodings);
+ g_free (name);
+ return TRUE;
+}
+
+CockpitPackages *
+cockpit_packages_new (void)
+{
+ CockpitPackages *packages = NULL;
+ GError *error = NULL;
+ gboolean ret = FALSE;
+ GSocketAddress *address = NULL;
+ GInetAddress *inet = NULL;
+ GSocket *socket = NULL;
+
+ g_assert (packages_singleton == NULL);
+
+ socket = g_socket_new (G_SOCKET_FAMILY_IPV4, G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_DEFAULT, &error);
+ if (socket == NULL)
+ {
+ g_warning ("couldn't create local ipv4 socket: %s", error->message);
+ goto out;
+ }
+
+ g_socket_set_listen_backlog (socket, 64);
+
+ inet = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV4);
+ address = g_inet_socket_address_new (inet, 0);
+ g_object_unref (inet);
+
+ if (!g_socket_bind (socket, address, TRUE, &error) ||
+ !g_socket_listen (socket, &error))
+ {
+ g_warning ("couldn't bind and listen to local ipv4 socket: %s", error->message);
+ goto out;
+ }
+
+ g_object_unref (address);
+ address = g_socket_get_local_address (socket, &error);
+ if (address == NULL)
+ {
+ g_warning ("couldn't get local ipv4 socket address: %s", error->message);
+ goto out;
+ }
+
+ packages = g_new0 (CockpitPackages, 1);
+
+ packages->web_server = cockpit_web_server_new (NULL, COCKPIT_WEB_SERVER_NONE);
+
+ g_signal_connect (packages->web_server, "handle-resource::/checksum",
+ G_CALLBACK (handle_package_checksum), packages);
+ g_signal_connect (packages->web_server, "handle-resource::/manifests.js",
+ G_CALLBACK (handle_package_manifests_js), packages);
+ g_signal_connect (packages->web_server, "handle-resource::/manifests.json",
+ G_CALLBACK (handle_package_manifests_json), packages);
+ g_signal_connect (packages->web_server, "handle-resource",
+ G_CALLBACK (handle_packages), packages);
+
+ build_packages (packages);
+ ret = TRUE;
+
+out:
+ g_clear_error (&error);
+ g_clear_object (&address);
+ g_clear_object (&socket);
+
+ if (ret)
+ packages_singleton = packages;
+ else
+ cockpit_packages_free (packages);
+
+ return packages_singleton;
+}
+
+GIOStream *
+cockpit_packages_connect (void)
+{
+ g_return_val_if_fail (packages_singleton != NULL, NULL);
+
+ return cockpit_web_server_connect (packages_singleton->web_server);
+}
+
+const gchar *
+cockpit_packages_get_checksum (CockpitPackages *packages)
+{
+ g_return_val_if_fail (packages != NULL, NULL);
+ return packages->checksum;
+}
+
+gchar **
+cockpit_packages_get_names (CockpitPackages *packages)
+{
+ GHashTableIter iter;
+ GPtrArray *array;
+ gpointer key;
+ gpointer value;
+ CockpitPackage *package;
+
+ g_return_val_if_fail (packages != NULL, NULL);
+
+ array = g_ptr_array_new ();
+ g_hash_table_iter_init (&iter, packages->listing);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ package = value;
+ if (!package->unavailable)
+ g_ptr_array_add (array, key);
+ }
+ g_ptr_array_add (array, NULL);
+
+ return (gchar **)g_ptr_array_free (array, FALSE);
+}
+
+/**
+ * cockpit_packages_get_bridges:
+ * @packages: The packages object
+ *
+ * Get a list of configured "bridges" JSON config objects in
+ * the order of priority. See doc/guide/ for the actual format
+ * of the JSON objects.
+ *
+ * Returns: (transfer container): A list of JSONObject each owned
+ * by CockpitPackages. Free with g_list_free() when done.
+ */
+GList *
+cockpit_packages_get_bridges (CockpitPackages *packages)
+{
+ CockpitPackage *package;
+ GList *l, *listing;
+ GList *result = NULL;
+ JsonArray *bridges;
+ JsonArray *bridge;
+ JsonObject *item;
+ JsonObject *match;
+ gboolean privileged;
+ const gchar *problem;
+ JsonNode *node;
+ guint i;
+
+ g_return_val_if_fail (packages != NULL, NULL);
+
+ listing = g_hash_table_get_values (packages->listing);
+ listing = g_list_sort_with_data (listing, compar_package_priority, NULL);
+ listing = g_list_reverse (listing);
+
+ /* Convert every package to the equivalent bridge listing */
+ for (l = listing; l != NULL; l = g_list_next (l))
+ {
+ package = l->data;
+ if (!cockpit_json_get_array (package->manifest, "bridges", NULL, &bridges))
+ {
+ g_message ("%s: invalid \"bridges\" field in package manifest", package->name);
+ continue;
+ }
+
+ for (i = 0; bridges && i < json_array_get_length (bridges); i++)
+ {
+ node = json_array_get_element (bridges, i);
+ if (!node || !JSON_NODE_HOLDS_OBJECT (node))
+ {
+ g_message ("%s: invalid bridge in \"bridges\" field in package manifest", package->name);
+ continue;
+ }
+
+ item = json_node_get_object (node);
+ if (!cockpit_json_get_array (item, "spawn", NULL, &bridge))
+ {
+ g_message ("%s: invalid \"spawn\" field in package manifest", package->name);
+ }
+ else if (!cockpit_json_get_array (item, "environ", NULL, &bridge))
+ {
+ g_message ("%s: invalid \"environ\" field in package manifest", package->name);
+ }
+ else if (!cockpit_json_get_object (item, "match", NULL, &match))
+ {
+ g_message ("%s: invalid \"match\" field in package manifest", package->name);
+ }
+ else if (!cockpit_json_get_bool (item, "privileged", FALSE, &privileged))
+ {
+ g_message ("%s: invalid \"privileged\" field in package manifest", package->name);
+ }
+ else if ((match == NULL) != privileged)
+ {
+ g_message ("%s: Exactly one of \"match\" or \"privileged\" required", package->name);
+ }
+ else if (!cockpit_json_get_string (item, "problem", NULL, &problem))
+ {
+ g_message ("%s: invalid \"problem\" field in package manifest", package->name);
+ }
+ else
+ {
+ result = g_list_prepend (result, item);
+ }
+ }
+ }
+
+ g_list_free (listing);
+ return g_list_reverse (result);
+}
+
+JsonObject *
+cockpit_packages_peek_json (CockpitPackages *packages)
+{
+ return packages->json;
+}
+
+void
+cockpit_packages_on_change (CockpitPackages *packages,
+ void (*callback) (gconstpointer user_data),
+ gconstpointer user_data)
+{
+ g_assert (callback == NULL || packages->on_change_callback == NULL);
+ packages->on_change_callback = callback;
+ packages->on_change_callback_data = user_data;
+}
+
+static void packages_emit_changed (CockpitPackages *packages);
+
+void
+cockpit_packages_reload (CockpitPackages *packages)
+{
+ build_packages (packages);
+ if (packages->on_change_callback)
+ packages->on_change_callback (packages->on_change_callback_data);
+ packages_emit_changed (packages);
+}
+
+void
+cockpit_packages_free (CockpitPackages *packages)
+{
+ if (!packages)
+ return;
+
+ g_assert (packages_singleton == packages);
+ packages_singleton = NULL;
+
+ if (packages->json)
+ json_object_unref (packages->json);
+ g_free (packages->bundle_checksum);
+ g_free (packages->checksum);
+ if (packages->listing)
+ g_hash_table_unref (packages->listing);
+ g_clear_object (&packages->web_server);
+ g_free (packages);
+}
+
+static void
+cockpit_packages_print_menu_labels (JsonObject *manifest,
+ const gchar *menu_key,
+ GString *result)
+{
+ JsonNode *node;
+ JsonObject *menu;
+ JsonObjectIter iter;
+ JsonNode *member_node;
+
+ node = json_object_get_member (manifest, menu_key);
+ if (!node || !JSON_NODE_HOLDS_OBJECT (node))
+ return;
+
+ menu = json_node_get_object (node);
+
+ json_object_iter_init (&iter, menu);
+ while (json_object_iter_next (&iter, NULL, &member_node))
+ {
+ JsonObject *item;
+ const gchar *label;
+
+ if (!JSON_NODE_HOLDS_OBJECT (member_node))
+ continue;
+
+ item = json_node_get_object (member_node);
+ if (!cockpit_json_get_string (item, "label", NULL, &label))
+ continue;
+
+ if (result->len > 0)
+ g_string_append (result, ", ");
+
+ g_string_append (result, label);
+ }
+}
+
+void
+cockpit_packages_dump (void)
+{
+ CockpitPackages *packages;
+ GHashTable *by_name;
+ GHashTableIter iter;
+ CockpitPackage *package;
+ GList *names, *l;
+
+ g_assert (packages_singleton == NULL);
+
+ packages = g_new0 (CockpitPackages, 1);
+ packages_singleton = packages;
+
+ build_packages (packages);
+
+ by_name = g_hash_table_new (g_str_hash, g_str_equal);
+
+ g_hash_table_iter_init (&iter, packages->listing);
+ while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&package))
+ g_hash_table_replace (by_name, package->name, package);
+
+ names = g_hash_table_get_keys (by_name);
+ names = g_list_sort (names, (GCompareFunc)strcmp);
+ for (l = names; l != NULL; l = g_list_next (l))
+ {
+ GString *menuitems = g_string_new (NULL);
+
+ package = g_hash_table_lookup (by_name, l->data);
+
+ cockpit_packages_print_menu_labels (package->manifest, "menu", menuitems);
+ cockpit_packages_print_menu_labels (package->manifest, "tools", menuitems);
+
+ g_print ("%-20.20s %-40.40s %s\n", package->name, menuitems->str, package->directory);
+
+ g_string_free (menuitems, TRUE);
+ }
+
+ if (packages->checksum)
+ g_print ("checksum = %s\n", packages->checksum);
+
+ g_list_free (names);
+ g_hash_table_unref (by_name);
+ cockpit_packages_free (packages);
+}
+
+/* D-Bus interface */
+
+static GVariant *
+packages_get_manifests (CockpitPackages *packages)
+{
+ GBytes *content = cockpit_json_write_bytes (packages->json);
+ GVariant *manifests = g_variant_new ("s", g_bytes_get_data (content, NULL));
+ g_bytes_unref (content);
+ return manifests;
+}
+
+static void
+packages_emit_changed (CockpitPackages *packages)
+{
+ if (!packages->dbus_inited)
+ return;
+
+ GDBusConnection *connection = cockpit_dbus_internal_server ();
+ if (!connection)
+ return;
+
+ GVariant *signal_value;
+ GVariantBuilder builder;
+ GError *error = NULL;
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}"));
+ g_variant_builder_add (&builder, "{sv}", "Manifests", packages_get_manifests (packages));
+ signal_value = g_variant_ref_sink (g_variant_new ("(sa{sv}as)", "cockpit.Packages", &builder, NULL));
+
+ g_dbus_connection_emit_signal (connection,
+ NULL,
+ "/packages",
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ signal_value,
+ &error);
+ if (error != NULL)
+ {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED))
+ g_critical ("failed to send PropertiesChanged signal: %s", error->message);
+ g_error_free (error);
+ }
+ g_variant_unref (signal_value);
+}
+
+static void
+packages_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ CockpitPackages *packages = user_data;
+
+ if (g_str_equal (method_name, "Reload"))
+ {
+ cockpit_packages_reload (packages);
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ }
+ else if (g_str_equal (method_name, "ReloadHint"))
+ {
+ if (packages->reload_hint)
+ cockpit_packages_reload (packages);
+ packages->reload_hint = TRUE;
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ }
+ else
+ g_return_if_reached ();
+}
+
+static GVariant *
+packages_get_property (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *property_name,
+ GError **error,
+ gpointer user_data)
+{
+ CockpitPackages *packages = user_data;
+
+ g_return_val_if_fail (property_name != NULL, NULL);
+
+ if (g_str_equal (property_name, "Manifests"))
+ return packages_get_manifests (packages);
+ else
+ g_return_val_if_reached (NULL);
+}
+
+static GDBusInterfaceVTable packages_vtable = {
+ .method_call = packages_method_call,
+ .get_property = packages_get_property,
+};
+
+static GDBusMethodInfo packages_reload_method = {
+ -1, "Reload", NULL, NULL, NULL
+};
+
+static GDBusMethodInfo packages_reload_hint_method = {
+ -1, "ReloadHint", NULL, NULL, NULL
+};
+
+static GDBusMethodInfo *packages_methods[] = {
+ &packages_reload_method,
+ &packages_reload_hint_method,
+ NULL
+};
+
+static GDBusPropertyInfo packages_manifests_property = {
+ -1, "Manifests", "s", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo *packages_properties[] = {
+ &packages_manifests_property,
+ NULL
+};
+
+static GDBusInterfaceInfo packages_interface = {
+ -1, "cockpit.Packages",
+ packages_methods,
+ NULL, /* signals */
+ packages_properties,
+ NULL /* annotations */
+};
+
+void
+cockpit_packages_dbus_startup (CockpitPackages *packages)
+{
+ GDBusConnection *connection;
+ GError *error = NULL;
+
+ connection = cockpit_dbus_internal_server ();
+ g_return_if_fail (connection != NULL);
+
+ g_dbus_connection_register_object (connection, "/packages", &packages_interface,
+ &packages_vtable, packages, NULL, &error);
+
+ g_object_unref (connection);
+
+ if (error != NULL)
+ {
+ g_critical ("couldn't register DBus cockpit.Packages object: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ packages->dbus_inited = TRUE;
+}
diff --git a/src/bridge/cockpitpackages.h b/src/bridge/cockpitpackages.h
new file mode 100644
index 0000000..45e4c81
--- /dev/null
+++ b/src/bridge/cockpitpackages.h
@@ -0,0 +1,59 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_PACKAGES_H_
+#define COCKPIT_PACKAGES_H_
+
+#include <glib.h>
+#include "common/cockpitjson.h"
+
+typedef struct _CockpitPackage CockpitPackage;
+typedef struct _CockpitPackages CockpitPackages;
+
+CockpitPackages * cockpit_packages_new (void);
+
+GIOStream * cockpit_packages_connect (void);
+
+const gchar * cockpit_packages_get_checksum (CockpitPackages *packages);
+
+gchar ** cockpit_packages_get_names (CockpitPackages *packages);
+
+GList * cockpit_packages_get_bridges (CockpitPackages *packages);
+
+gchar * cockpit_packages_resolve (CockpitPackages *packages,
+ const gchar *name,
+ const gchar *path,
+ CockpitPackage **package);
+
+void cockpit_packages_reload (CockpitPackages *packages);
+
+JsonObject * cockpit_packages_peek_json (CockpitPackages *packages);
+
+void cockpit_packages_dbus_startup (CockpitPackages *packages);
+
+void cockpit_packages_on_change (CockpitPackages *packages,
+ void (*callback) (gconstpointer user_data),
+ gconstpointer user_data);
+
+void cockpit_packages_free (CockpitPackages *packages);
+
+void cockpit_packages_dump (void);
+
+
+#endif /* COCKPIT_PACKAGES_H_ */
diff --git a/src/bridge/cockpitpacketchannel.c b/src/bridge/cockpitpacketchannel.c
new file mode 100644
index 0000000..50c7d8d
--- /dev/null
+++ b/src/bridge/cockpitpacketchannel.c
@@ -0,0 +1,679 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpacketchannel.h"
+
+#include "cockpitconnect.h"
+
+#include "common/cockpitflow.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitunicode.h"
+
+#include <gio/gunixsocketaddress.h>
+#include <glib-unix.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <string.h>
+#include <errno.h>
+
+/**
+ * CockpitPacketChannel:
+ *
+ * A #CockpitChannel that sends messages from a regular seqpacket socket.
+ * Support for dgram sockets should also fit in here rather well, but is
+ * not implemented at the current time.
+ *
+ * The payload type for this channel is 'stream'.
+ */
+
+#define DEF_PACKET_SIZE (64UL * 1024UL)
+
+/* Sadly this is limited by the max size of our WebSocket payload */
+#define MAX_PACKET_SIZE (128UL * 1024UL)
+
+/* Several megabytes is when we start to consider queue full enough */
+#define QUEUE_PRESSURE (128UL * DEF_PACKET_SIZE)
+
+enum {
+ CREATED = 0,
+ CONNECTING,
+ RELAYING,
+ CLOSED
+};
+
+typedef struct {
+ CockpitChannel parent;
+ GMainContext *context;
+ gchar *name;
+ gint state;
+ gsize max_size;
+
+ int fd;
+ GSource *in_source;
+ gboolean in_done;
+ GSource *out_source;
+ GQueue *out_queue;
+ gboolean out_done;
+ gsize out_queued;
+
+ /* Pressure which throttles input on this pipe */
+ CockpitFlow *pressure;
+ gulong pressure_sig;
+} CockpitPacketChannel;
+
+typedef struct {
+ CockpitChannelClass parent_class;
+} CockpitPacketChannelClass;
+
+static void start_input (CockpitPacketChannel *self);
+
+static void start_output (CockpitPacketChannel *self);
+
+static void cockpit_packet_channel_flow_iface (CockpitFlowInterface *iface);
+
+#define COCKPIT_PACKET_CHANNEL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_PACKET_CHANNEL, \
+ CockpitPacketChannel))
+
+G_DEFINE_TYPE_WITH_CODE (CockpitPacketChannel, cockpit_packet_channel, COCKPIT_TYPE_CHANNEL,
+ G_IMPLEMENT_INTERFACE (COCKPIT_TYPE_FLOW, cockpit_packet_channel_flow_iface));
+
+static void
+stop_output (CockpitPacketChannel *self)
+{
+ g_assert (self->out_source != NULL);
+ g_source_destroy (self->out_source);
+ g_source_unref (self->out_source);
+ self->out_source = NULL;
+}
+
+static void
+stop_input (CockpitPacketChannel *self)
+{
+ g_assert (self->in_source != NULL);
+ g_source_destroy (self->in_source);
+ g_source_unref (self->in_source);
+ self->in_source = NULL;
+}
+
+static void
+close_with_errno (CockpitPacketChannel *self,
+ const gchar *message,
+ int errn)
+{
+ const gchar *problem = NULL;
+
+ if (errn == EPERM || errn == EACCES)
+ problem = "access-denied";
+ else if (errn == ENOENT || errn == ECONNREFUSED)
+ problem = "not-found";
+
+ if (problem)
+ {
+ g_message ("%s: %s: %s", self->name, message, g_strerror (errn));
+ }
+ else
+ {
+ g_warning ("%s: %s: %s", self->name, message, g_strerror (errn));
+ problem = "internal-error";
+ }
+
+ cockpit_channel_close (COCKPIT_CHANNEL (self), problem);
+}
+
+static void
+close_maybe (CockpitPacketChannel *self)
+{
+ if (self->state < CLOSED)
+ {
+ if (self->in_done && self->out_done)
+ {
+ g_debug ("%s: input and output done", self->name);
+ cockpit_channel_close (COCKPIT_CHANNEL (self), NULL);
+ }
+ }
+}
+
+static gboolean
+dispatch_input (gint fd,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ CockpitPacketChannel *self = (CockpitPacketChannel *)user_data;
+ GByteArray *buffer;
+ GBytes *message;
+ gssize ret = 0;
+ int errn;
+
+ g_return_val_if_fail (self->in_source, FALSE);
+
+ buffer = g_byte_array_new ();
+
+ /*
+ * Enable clean shutdown by not reading when we just get
+ * G_IO_HUP. Note that when we get G_IO_ERR we do want to read
+ * just so we can get the appropriate detailed error message.
+ */
+ if (cond != G_IO_HUP)
+ {
+ g_byte_array_set_size (buffer, self->max_size);
+ g_debug ("%s: reading input %x", self->name, cond);
+ ret = recv (self->fd, buffer->data, buffer->len, 0);
+
+ errn = errno;
+ if (ret < 0)
+ {
+ if (errn == EAGAIN || errn == EINTR)
+ {
+ g_byte_array_free (buffer, TRUE);
+ return TRUE;
+ }
+ else if (errn == ECONNRESET)
+ {
+ g_debug ("couldn't read: %s", g_strerror (errn));
+ ret = 0;
+ }
+ else
+ {
+ g_byte_array_free (buffer, TRUE);
+ close_with_errno (self, "couldn't read", errn);
+ return FALSE;
+ }
+ }
+
+ g_byte_array_set_size (buffer, ret);
+ }
+
+ if (ret == 0)
+ {
+ g_debug ("%s: end of input", self->name);
+ cockpit_channel_control (COCKPIT_CHANNEL (self), "done", NULL);
+ self->in_done = TRUE;
+ stop_input (self);
+ }
+
+ g_object_ref (self);
+
+ message = g_byte_array_free_to_bytes (buffer);
+ cockpit_channel_send (COCKPIT_CHANNEL (self), message, FALSE);
+ g_bytes_unref (message);
+
+ if (self->in_done)
+ close_maybe (self);
+
+ g_object_unref (self);
+ return TRUE;
+}
+
+static gboolean
+dispatch_connect (CockpitPacketChannel *self)
+{
+ socklen_t slen;
+ int error;
+
+ slen = sizeof (error);
+ if (getsockopt (self->fd, SOL_SOCKET, SO_ERROR, &error, &slen) != 0)
+ {
+ g_warning ("%s: couldn't get connection result", self->name);
+ cockpit_channel_close (COCKPIT_CHANNEL (self), "internal-error");
+ }
+ else if (error == EINPROGRESS)
+ {
+ /* keep connecting */
+ }
+ else if (error != 0)
+ {
+ close_with_errno (self, "couldn't connect", error);
+ }
+ else
+ {
+ self->state = RELAYING;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+dispatch_output (gint fd,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ CockpitPacketChannel *self = (CockpitPacketChannel *)user_data;
+ gconstpointer data;
+ gsize before, size;
+ gssize ret;
+
+ /* A non-blocking connect is processed here */
+ if (self->state == CONNECTING && !dispatch_connect (self))
+ return TRUE;
+
+ g_return_val_if_fail (self->out_source, FALSE);
+
+ before = self->out_queued;
+
+ while (self->out_queue->head)
+ {
+ data = g_bytes_get_data (self->out_queue->head->data, &size);
+ ret = send (self->fd, data, size, 0);
+
+ if (ret < 0)
+ {
+ if (errno == EAGAIN || errno == EINTR || errno == ENOBUFS)
+ {
+ break;
+ }
+ else
+ {
+ close_with_errno (self, "couldn't write", errno);
+ return FALSE;
+ }
+ }
+ else
+ {
+ g_bytes_unref (g_queue_pop_head (self->out_queue));
+ g_assert (size <= self->out_queued);
+ self->out_queued -= size;
+ }
+ }
+
+ /*
+ * If we're controlling another flow, turn it on again when our output
+ * buffer size becomes less than the low mark.
+ */
+ if (before >= QUEUE_PRESSURE && self->out_queued < QUEUE_PRESSURE)
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), FALSE);
+
+ if (self->out_queue->head)
+ return TRUE;
+
+ g_debug ("%s: output queue empty", self->name);
+
+ /* If all messages are done, then stop polling out fd */
+ stop_output (self);
+
+ if (self->out_done)
+ {
+ g_debug ("%s: end of output", self->name);
+
+ /* And if closing, then we need to shutdown the output fd */
+ if (shutdown (self->fd, SHUT_WR) < 0)
+ close_with_errno (self, "couldn't shutdown fd", errno);
+ }
+
+ close_maybe (self);
+
+ return TRUE;
+}
+
+static void
+cockpit_packet_channel_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ CockpitPacketChannel *self = COCKPIT_PACKET_CHANNEL (channel);
+ gsize before, size;
+
+ if (self->state >= CLOSED)
+ return;
+
+ size = g_bytes_get_size (message);
+ before = self->out_queued;
+ g_return_if_fail (G_MAXSIZE - size > self->out_queued);
+ self->out_queued += size;
+ g_queue_push_tail (self->out_queue, g_bytes_ref (message));
+
+ /*
+ * If we have too much data queued, and are controlling another flow
+ * tell it to stop sending data, each time we cross over the high bound.
+ */
+ if (before < QUEUE_PRESSURE && self->out_queued >= QUEUE_PRESSURE)
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), TRUE);
+
+ if (!self->out_source && self->fd >= 0)
+ start_output (self);
+}
+
+static gboolean
+cockpit_packet_channel_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *message)
+{
+ CockpitPacketChannel *self = COCKPIT_PACKET_CHANNEL (channel);
+ gboolean ret = TRUE;
+ gint64 size = 0;
+
+ /* New set of options for channel */
+ if (g_str_equal (command, "options"))
+ {
+ if (!cockpit_json_get_int (message, "max-size", self->max_size, &size) ||
+ size < 1 || size > MAX_PACKET_SIZE)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"max-size\" option for channel");
+ goto out;
+ }
+
+ self->max_size = size;
+ }
+
+ /* Channel input from frontend is done */
+ else if (g_str_equal (command, "done"))
+ {
+ self->out_done = TRUE;
+ if (!self->out_source)
+ start_output (self);
+ }
+
+ else
+ {
+ ret = FALSE;
+ }
+
+out:
+ return ret;
+}
+
+static void
+cockpit_packet_channel_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ CockpitPacketChannel *self = COCKPIT_PACKET_CHANNEL (channel);
+
+ if (self->state >= CLOSED)
+ return;
+
+ self->state = CLOSED;
+
+ if (self->in_source)
+ stop_input (self);
+ self->in_done = TRUE;
+ if (self->out_source)
+ stop_output (self);
+ self->out_done = TRUE;
+
+ if (self->fd != -1)
+ {
+ close (self->fd);
+ self->fd = -1;
+ }
+
+ COCKPIT_CHANNEL_CLASS (cockpit_packet_channel_parent_class)->close (channel, problem);
+}
+
+static void
+cockpit_packet_channel_init (CockpitPacketChannel *self)
+{
+ self->fd = -1;
+ self->state = CREATED;
+ self->max_size = DEF_PACKET_SIZE;
+ self->out_queue = g_queue_new ();
+ self->context = g_main_context_ref_thread_default ();
+}
+
+static int
+packet_channel_connect (CockpitPacketChannel *self,
+ GSocketAddress *address)
+{
+ gsize native_len;
+ gpointer native;
+ int sock = -1;
+
+ g_return_val_if_fail (G_IS_SOCKET_ADDRESS (address), -1);
+
+ sock = socket (g_socket_address_get_family (address), SOCK_SEQPACKET, 0);
+ if (sock < 0)
+ {
+ close_with_errno (self, "couldn't open socket", errno);
+ }
+ else
+ {
+ if (!g_unix_set_fd_nonblocking (sock, TRUE, NULL))
+ {
+ close (sock);
+ g_return_val_if_reached (-1);
+ }
+
+ native_len = g_socket_address_get_native_size (address);
+ native = g_malloc (native_len);
+ if (!g_socket_address_to_native (address, native, native_len, NULL))
+ {
+ close (sock);
+ g_return_val_if_reached (-1);
+ }
+ if (connect (sock, native, native_len) < 0)
+ {
+ if (errno == EINPROGRESS)
+ {
+ self->state = CONNECTING;
+ }
+ else
+ {
+ close_with_errno (self, "couldn't connect", errno);
+ close (sock);
+ sock = -1;
+ }
+ }
+ else
+ {
+ self->state = RELAYING;
+ }
+ g_free (native);
+ }
+
+ return sock;
+}
+
+static void
+start_output (CockpitPacketChannel *self)
+{
+ g_assert (self->out_source == NULL);
+ self->out_source = g_unix_fd_source_new (self->fd, G_IO_OUT);
+ g_source_set_name (self->out_source, "packet-output");
+ g_source_set_callback (self->out_source, (GSourceFunc)dispatch_output, self, NULL);
+ g_source_attach (self->out_source, self->context);
+}
+
+static void
+start_input (CockpitPacketChannel *self)
+{
+ g_assert (self->in_source == NULL);
+ self->in_source = g_unix_fd_source_new (self->fd, G_IO_IN);
+ g_source_set_name (self->in_source, "packet-input");
+ g_source_set_callback (self->in_source, (GSourceFunc)dispatch_input, self, NULL);
+ g_source_attach (self->in_source, self->context);
+}
+
+static void
+on_throttle_pressure (GObject *object,
+ gboolean throttle,
+ gpointer user_data)
+{
+ CockpitPacketChannel *self = COCKPIT_PACKET_CHANNEL (user_data);
+ if (throttle)
+ {
+ if (self->in_source != NULL)
+ {
+ g_debug ("%s: applying back pressure in pipe", self->name);
+ stop_input (self);
+ }
+ }
+ else
+ {
+ if (self->in_source == NULL && !self->in_done)
+ {
+ g_debug ("%s: relieving back pressure in pipe", self->name);
+ start_input (self);
+ }
+ }
+}
+
+static void
+cockpit_packet_channel_throttle (CockpitFlow *flow,
+ CockpitFlow *controlling)
+{
+ CockpitPacketChannel *self = COCKPIT_PACKET_CHANNEL (flow);
+
+ if (self->pressure)
+ {
+ g_signal_handler_disconnect (self->pressure, self->pressure_sig);
+ g_object_remove_weak_pointer (G_OBJECT (self->pressure), (gpointer *)&self->pressure);
+ self->pressure = NULL;
+ }
+
+ if (controlling)
+ {
+ self->pressure = controlling;
+ g_object_add_weak_pointer (G_OBJECT (self->pressure), (gpointer *)&self->pressure);
+ self->pressure_sig = g_signal_connect (controlling, "pressure", G_CALLBACK (on_throttle_pressure), self);
+ }
+}
+
+static void
+cockpit_packet_channel_flow_iface (CockpitFlowInterface *iface)
+{
+ iface->throttle = cockpit_packet_channel_throttle;
+}
+
+static void
+cockpit_packet_channel_prepare (CockpitChannel *channel)
+{
+ CockpitPacketChannel *self = COCKPIT_PACKET_CHANNEL (channel);
+ GSocketAddress *address;
+ JsonObject *options;
+ int sock;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_packet_channel_parent_class)->prepare (channel);
+ options = cockpit_channel_get_options (channel);
+
+ /* Support our options in the open message too */
+ cockpit_packet_channel_control (channel, "options", options);
+ if (self->state >= CLOSED)
+ return;
+
+ address = cockpit_connect_parse_address (channel, &self->name);
+ if (!address)
+ {
+ cockpit_channel_close (channel, "internal-error");
+ return;
+ }
+
+ sock = packet_channel_connect (self, address);
+ g_object_unref (address);
+
+ if (sock < 0)
+ {
+ cockpit_channel_close (channel, "internal-error");
+ return;
+ }
+ else
+ {
+ self->fd = sock;
+ start_input (self);
+ start_output (self);
+ }
+
+ cockpit_channel_ready (channel, NULL);
+}
+
+static void
+cockpit_packet_channel_dispose (GObject *object)
+{
+ CockpitPacketChannel *self = COCKPIT_PACKET_CHANNEL (object);
+
+ cockpit_packet_channel_throttle (COCKPIT_FLOW (self), NULL);
+ g_assert (self->pressure == NULL);
+
+ if (self->state < CLOSED)
+ cockpit_channel_close (COCKPIT_CHANNEL (self), "terminated");
+
+ while (self->out_queue->head)
+ g_bytes_unref (g_queue_pop_head (self->out_queue));
+ self->out_queued = 0;
+
+ G_OBJECT_CLASS (cockpit_packet_channel_parent_class)->dispose (object);
+}
+
+static void
+cockpit_packet_channel_finalize (GObject *object)
+{
+ CockpitPacketChannel *self = COCKPIT_PACKET_CHANNEL (object);
+ g_assert (self->state == CLOSED);
+ g_assert (self->fd < 0);
+ g_assert (!self->in_source);
+ g_assert (!self->out_source);
+ g_queue_free (self->out_queue);
+ g_free (self->name);
+
+ if (self->context)
+ g_main_context_unref (self->context);
+
+ G_OBJECT_CLASS (cockpit_packet_channel_parent_class)->finalize (object);
+}
+
+static void
+cockpit_packet_channel_class_init (CockpitPacketChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->dispose = cockpit_packet_channel_dispose;
+ gobject_class->finalize = cockpit_packet_channel_finalize;
+
+ channel_class->prepare = cockpit_packet_channel_prepare;
+ channel_class->control = cockpit_packet_channel_control;
+ channel_class->recv = cockpit_packet_channel_recv;
+ channel_class->close = cockpit_packet_channel_close;
+}
+
+/**
+ * cockpit_packet_channel_open:
+ * @transport: the transport to send/receive messages on
+ * @channel_id: the channel id
+ * @unix_path: the UNIX socket path to communicate with
+ *
+ * This function is mainly used by tests. The usual way
+ * to get a #CockpitPacketChannel is via cockpit_channel_open()
+ *
+ * Returns: (transfer full): the new channel
+ */
+CockpitChannel *
+cockpit_packet_channel_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *unix_path)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+
+ g_return_val_if_fail (channel_id != NULL, NULL);
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "unix", unix_path);
+ json_object_set_string_member (options, "payload", "packet");
+
+ channel = g_object_new (COCKPIT_TYPE_PACKET_CHANNEL,
+ "transport", transport,
+ "id", channel_id,
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+ return channel;
+}
diff --git a/src/bridge/cockpitpacketchannel.h b/src/bridge/cockpitpacketchannel.h
new file mode 100644
index 0000000..fcf79ba
--- /dev/null
+++ b/src/bridge/cockpitpacketchannel.h
@@ -0,0 +1,37 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_PACKET_CHANNEL_H__
+#define COCKPIT_PACKET_CHANNEL_H__
+
+#include <gio/gio.h>
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_PACKET_CHANNEL (cockpit_packet_channel_get_type ())
+
+GType cockpit_packet_channel_get_type (void) G_GNUC_CONST;
+
+CockpitChannel * cockpit_packet_channel_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *unix_path);
+
+#endif /* COCKPIT_PACKET_CHANNEL_H__ */
diff --git a/src/bridge/cockpitpaths.c b/src/bridge/cockpitpaths.c
new file mode 100644
index 0000000..8ed916f
--- /dev/null
+++ b/src/bridge/cockpitpaths.c
@@ -0,0 +1,218 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpaths.h"
+
+#include <string.h>
+
+gboolean
+cockpit_path_has_parent (const gchar *path,
+ const gchar *parent)
+{
+ gsize length = strlen (parent);
+ const gchar *last;
+
+ if (length == 1 && parent[0] == '/' && path[0])
+ last = path + 1;
+
+ else if (strncmp (path, parent, length) == 0 && path[length] == '/')
+ last = path + length + 1;
+
+ else
+ return FALSE;
+
+ return strchr (last, '/') == NULL;
+}
+
+gboolean
+cockpit_path_equal_or_ancestor (const gchar *path,
+ const gchar *ancestor)
+{
+ gsize length = strlen (ancestor);
+ if (length == 1 && ancestor[0] == '/')
+ return TRUE;
+
+ if (strncmp (path, ancestor, length) == 0 &&
+ (path[length] == '/' || path[length] == '\0'))
+ return TRUE;
+
+ return FALSE;
+}
+
+gboolean
+cockpit_path_has_ancestor (const gchar *path,
+ const gchar *ancestor)
+{
+ gsize length = strlen (ancestor);
+ if (length == 1 && ancestor[0] == '/')
+ return TRUE;
+
+ if (strncmp (path, ancestor, length) == 0 &&
+ path[length] == '/')
+ return TRUE;
+
+ return FALSE;
+}
+
+typedef struct {
+ gsize len;
+ const gchar *data;
+} PathData;
+
+static gint
+tree_path_cmp (gconstpointer a,
+ gconstpointer b,
+ gpointer user_data)
+{
+ const PathData *pa = a;
+ const PathData *pb = b;
+ gsize la = pa->len;
+ gsize lb = pb->len;
+ gint ret;
+
+ ret = memcmp (pa->data, pb->data, MIN (la, lb));
+ if (ret == 0 && la != lb)
+ ret = (la < lb) ? -1 : 1;
+
+ return ret;
+}
+
+static gint
+tree_prefix_search (gconstpointer a,
+ gconstpointer b)
+{
+ const PathData *pa = a;
+ const PathData *pb = b;
+ gsize la = pa->len;
+ gsize lb = pb->len;
+ gint ret;
+
+ /* Yes, g_tree_search() means this is backwards */
+ ret = memcmp (pb->data, pa->data, MIN (la, lb));
+ if (ret == 0 && la != lb)
+ {
+ if (la > lb)
+ {
+ if ((lb == 1 && pb->data[0] == '/') || pa->data[lb] == '/')
+ ret = 0;
+ else
+ ret = -1;
+ }
+ else
+ {
+ ret = 1;
+ }
+ }
+ return ret;
+}
+
+GTree *
+cockpit_paths_new (void)
+{
+ return g_tree_new_full (tree_path_cmp, NULL, g_free, NULL);
+}
+
+/*
+ * cockpit_paths_add:
+ * @tree: The tree to add to
+ * @path: The path to add
+ *
+ * Adds the path if this path or a parent is not already
+ * in the tree. Will return %NULL if the path is already
+ * in the tree ... otherwise will return the internally
+ * reallocated path.
+ */
+const gchar *
+cockpit_paths_add (GTree *tree,
+ const gchar *path)
+{
+ PathData key = { strlen (path), path };
+ PathData *pd = g_tree_lookup (tree, &key);
+
+ if (!pd)
+ {
+ pd = g_malloc (sizeof (PathData) + key.len + 1);
+ pd->len = key.len;
+ pd->data = (gchar *)(pd + 1);
+ memcpy ((gchar *)pd->data, path, key.len + 1);
+ g_tree_replace (tree, pd, pd);
+ return pd->data;
+ }
+
+ return NULL;
+}
+
+gboolean
+cockpit_paths_remove (GTree *tree,
+ const gchar *path)
+{
+ PathData key = { strlen (path), path };
+ return g_tree_remove (tree, &key);
+}
+
+const gchar *
+cockpit_paths_contain (GTree *tree,
+ const gchar *path)
+{
+ PathData key = { strlen(path), path };
+ PathData *pd = g_tree_lookup (tree, &key);
+ return pd ? pd->data : NULL;
+}
+
+gboolean
+cockpit_paths_contain_or_descendant (GTree *tree,
+ const gchar *path)
+{
+ PathData key = { strlen (path), path };
+ PathData *pd = g_tree_search (tree, tree_prefix_search, &key);
+ return pd != NULL;
+}
+
+const gchar *
+cockpit_paths_contain_or_ancestor (GTree *tree,
+ const gchar *path)
+{
+ PathData key = { strlen (path), path };
+ gboolean last = FALSE;
+ const gchar *pos;
+ PathData *pd;
+
+ for (;;)
+ {
+ pd = g_tree_lookup (tree, &key);
+ if (pd)
+ return pd->data;
+ if (last)
+ return NULL;
+ pos = memrchr (path, '/', key.len);
+ if (!pos)
+ return NULL;
+ if (path == pos)
+ {
+ key.len = 1;
+ last = TRUE;
+ }
+ else
+ {
+ key.len = (pos - path);
+ }
+ }
+}
diff --git a/src/bridge/cockpitpaths.h b/src/bridge/cockpitpaths.h
new file mode 100644
index 0000000..17ff4c8
--- /dev/null
+++ b/src/bridge/cockpitpaths.h
@@ -0,0 +1,59 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_PATHS_H
+#define COCKPIT_PATHS_H
+
+#include <glib.h>
+
+/*
+ * These paths operate on normalized paths. Nothing relative, no
+ * .. or anything like that. Each path must start with '/' and the
+ * only path that can end with '/' is the root path.
+ */
+
+gboolean cockpit_path_has_ancestor (const gchar *path,
+ const gchar *ancestor);
+
+gboolean cockpit_path_has_parent (const gchar *path,
+ const gchar *parent);
+
+gboolean cockpit_path_equal_or_ancestor (const gchar *path,
+ const gchar *ancestor);
+
+GTree * cockpit_paths_new (void);
+
+const gchar * cockpit_paths_add (GTree *paths,
+ const gchar *path);
+
+gboolean cockpit_paths_remove (GTree *paths,
+ const gchar *path);
+
+const gchar * cockpit_paths_contain (GTree *paths,
+ const gchar *path);
+
+/* path is in paths or a descendant of path is in paths */
+gboolean cockpit_paths_contain_or_descendant (GTree *paths,
+ const gchar *path);
+
+/* path is in paths or an ancestor of path is in paths */
+const gchar * cockpit_paths_contain_or_ancestor (GTree *paths,
+ const gchar *path);
+
+#endif /* COCKPIT_PATHS_H */
diff --git a/src/bridge/cockpitpcp.c b/src/bridge/cockpitpcp.c
new file mode 100644
index 0000000..4159278
--- /dev/null
+++ b/src/bridge/cockpitpcp.c
@@ -0,0 +1,149 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitinternalmetrics.h"
+#include "cockpitpcpmetrics.h"
+#include "cockpitrouter.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpithacks-glib.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitpipetransport.h"
+#include "common/cockpitsystem.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <pwd.h>
+
+#include <glib-unix.h>
+
+/* This program is run on each managed server, with the credentials
+ of the user that is logged into the Server Console.
+*/
+
+static void
+send_init_command (CockpitTransport *transport)
+{
+ const gchar *response = "{ \"command\": \"init\", \"version\": 1 }";
+ GBytes *bytes = g_bytes_new_static (response, strlen (response));
+ cockpit_transport_send (transport, NULL, bytes);
+ g_bytes_unref (bytes);
+}
+
+static void
+add_router_channels (CockpitRouter *router)
+{
+ JsonObject *match;
+
+ match = json_object_new ();
+ json_object_set_string_member (match, "payload", "metrics1");
+ cockpit_router_add_channel (router, match, cockpit_pcp_metrics_get_type);
+ json_object_unref (match);
+
+ match = json_object_new ();
+ json_object_set_string_member (match, "payload", "metrics1");
+ json_object_set_string_member (match, "source", "internal");
+ cockpit_router_add_channel (router, match, cockpit_internal_metrics_get_type);
+ json_object_unref (match);
+}
+
+static gboolean
+on_signal_done (gpointer data)
+{
+ gboolean *closed = data;
+ *closed = TRUE;
+ return TRUE;
+}
+
+static void
+on_closed_set_flag (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gboolean *flag = user_data;
+ *flag = TRUE;
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ CockpitTransport *transport;
+ CockpitRouter *router;
+ gboolean terminated = FALSE;
+ gboolean closed = FALSE;
+ GOptionContext *context;
+ GError *error = NULL;
+ guint sig_term;
+
+ static GOptionEntry entries[] = {
+ { NULL }
+ };
+
+ signal (SIGPIPE, SIG_IGN);
+
+ cockpit_setenv_check ("GSETTINGS_BACKEND", "memory", TRUE);
+ cockpit_setenv_check ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE);
+ cockpit_setenv_check ("GIO_USE_VFS", "local", TRUE);
+
+ context = g_option_context_new (NULL);
+ g_option_context_add_main_entries (context, entries, NULL);
+ g_option_context_set_description (context, "cockpit-pcp is run automatically inside of a Cockpit session.\n");
+
+ g_option_context_parse (context, &argc, &argv, &error);
+ g_option_context_free (context);
+
+ if (error)
+ {
+ g_printerr ("cockpit-pcp: %s\n", error->message);
+ g_error_free (error);
+ return 1;
+ }
+
+ if (isatty (1))
+ {
+ g_printerr ("cockpit-pcp: only run from cockpit-bridge\n");
+ return 2;
+ }
+
+ cockpit_hacks_redirect_gdebug_to_stderr ();
+
+ sig_term = g_unix_signal_add (SIGTERM, on_signal_done, &terminated);
+
+ transport = cockpit_pipe_transport_new_fds ("stdio", 0, 1);
+
+ router = cockpit_router_new (transport, NULL, NULL);
+ add_router_channels (router);
+ g_signal_connect (transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+ send_init_command (transport);
+
+ while (!closed && !terminated)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (router);
+ g_object_unref (transport);
+
+ g_source_remove (sig_term);
+
+ return 0;
+}
diff --git a/src/bridge/cockpitpcpmetrics.c b/src/bridge/cockpitpcpmetrics.c
new file mode 100644
index 0000000..38213fb
--- /dev/null
+++ b/src/bridge/cockpitpcpmetrics.c
@@ -0,0 +1,1091 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#include "config.h"
+
+#include "cockpitmetrics.h"
+#include "cockpitpcpmetrics.h"
+
+#include "common/cockpitjson.h"
+
+#include <pcp/pmapi.h>
+#include <math.h>
+
+/**
+ * CockpitPcpMetrics:
+ *
+ * A #CockpitMetrics channel that pulls data from PCP
+ */
+
+#define COCKPIT_PCP_METRICS(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_PCP_METRICS, CockpitPcpMetrics))
+
+typedef struct {
+ const gchar *name;
+ const gchar *derive;
+ pmID id;
+ pmDesc desc;
+ pmUnits *units;
+ gdouble factor;
+
+ pmUnits units_buf;
+} MetricInfo;
+
+typedef struct {
+ int context;
+ gint64 start;
+} ArchiveInfo;
+
+typedef struct {
+ CockpitMetrics parent;
+ const gchar *name;
+ int direct_context;
+ int numpmid;
+ pmID *pmidlist;
+ MetricInfo *metrics;
+ gint64 interval;
+ gint64 limit;
+ guint idler;
+
+ GList *archives; /* of ArchiveInfo */
+ GList *cur_archive;
+
+ /* The previous samples sent */
+ pmResult *last;
+} CockpitPcpMetrics;
+
+typedef struct {
+ CockpitMetricsClass parent_class;
+} CockpitPcpMetricsClass;
+
+G_DEFINE_TYPE (CockpitPcpMetrics, cockpit_pcp_metrics, COCKPIT_TYPE_METRICS);
+
+static void
+cockpit_pcp_metrics_init (CockpitPcpMetrics *self)
+{
+ self->direct_context = -1;
+}
+
+static gboolean
+result_meta_equal (CockpitPcpMetrics *self,
+ pmResult *r1,
+ pmResult *r2)
+{
+ pmValueSet *vs1;
+ pmValueSet *vs2;
+ int i, j;
+
+ /* PCP guarantees that the result ids are same as requested */
+ for (i = 0; i < r1->numpmid; i++)
+ {
+ /* We only care about instanced metrics.
+ */
+ if (self->metrics[i].desc.indom == PM_INDOM_NULL)
+ continue;
+
+ vs1 = r1->vset[i];
+ vs2 = r2->vset[i];
+
+ g_assert (vs1 && vs2);
+
+ if (vs1->numval != vs2->numval)
+ return FALSE;
+
+ for (j = 0; j < vs1->numval; j++)
+ {
+ if (vs1->vlist[j].inst != vs2->vlist[j].inst)
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+static gint64
+timestamp_from_timeval (struct timeval *tv)
+{
+ return (gint64) tv->tv_sec * 1000 + tv->tv_usec / 1000;
+}
+
+static JsonObject *
+build_meta (CockpitPcpMetrics *self,
+ pmResult *result)
+{
+ JsonArray *metrics;
+ JsonObject *metric;
+ JsonArray *instances;
+ JsonObject *root;
+ pmValueSet *vs;
+ struct timeval now_timeval;
+ gint64 timestamp, now;
+ char *instance;
+ int i, j;
+ int rc;
+
+ gettimeofday (&now_timeval, NULL);
+
+ timestamp = timestamp_from_timeval (&result->timestamp);
+ now = timestamp_from_timeval (&now_timeval);
+
+ root = json_object_new ();
+ json_object_set_int_member (root, "timestamp", timestamp);
+ json_object_set_int_member (root, "now", now);
+ json_object_set_int_member (root, "interval", self->interval);
+
+ metrics = json_array_new ();
+ for (i = 0; i < result->numpmid; i++)
+ {
+ metric = json_object_new ();
+
+ /* Name and derivation mode
+ */
+ json_object_set_string_member (metric, "name", self->metrics[i].name);
+ if (self->metrics[i].derive)
+ json_object_set_string_member (metric, "derive", self->metrics[i].derive);
+
+ /* Instances
+ */
+ vs = result->vset[i];
+ if (vs->numval < 0 || self->metrics[i].desc.indom == PM_INDOM_NULL)
+ {
+ /* When negative numval is an error code ... we don't care */
+ }
+ else
+ {
+ instances = json_array_new ();
+
+ for (j = 0; j < vs->numval; j++)
+ {
+ /* PCP guarantees that the result is in the same order as requested */
+ rc = pmNameInDom (self->metrics[i].desc.indom, vs->vlist[j].inst, &instance);
+ if (rc != 0)
+ {
+ g_warning ("%s: instance name lookup failed: %s.%d: %s", self->name, self->metrics[i].name, vs->vlist[j].inst, pmErrStr (rc));
+ instance = NULL;
+ }
+
+ /* HACK: We can't use json_builder_add_string_value here since
+ it turns empty strings into 'null' values inside arrays.
+
+ https://bugzilla.gnome.org/show_bug.cgi?id=730803
+ */
+ {
+ JsonNode *string_element = json_node_alloc ();
+ json_node_init_string (string_element, instance? instance : "");
+ json_array_add_element (instances, string_element);
+ }
+
+ if (instance)
+ free (instance);
+ }
+ json_object_set_array_member (metric, "instances", instances);
+ }
+
+ /* Units
+ */
+ if (self->metrics[i].factor == 1.0)
+ {
+ json_object_set_string_member (metric, "units", pmUnitsStr(self->metrics[i].units));
+ }
+ else
+ {
+ gchar *name = g_strdup_printf ("%s*%g", pmUnitsStr(self->metrics[i].units), 1.0/self->metrics[i].factor);
+ json_object_set_string_member (metric, "units", name);
+ g_free (name);
+ }
+
+ /* Semantics
+ */
+ switch (self->metrics[i].desc.sem) {
+ case PM_SEM_COUNTER:
+ json_object_set_string_member (metric, "semantics", "counter");
+ break;
+ case PM_SEM_INSTANT:
+ json_object_set_string_member (metric, "semantics", "instant");
+ break;
+ case PM_SEM_DISCRETE:
+ json_object_set_string_member (metric, "semantics", "discrete");
+ break;
+ default:
+ break;
+ }
+
+ json_array_add_object_element (metrics, metric);
+ }
+
+ json_object_set_array_member (root, "metrics", metrics);
+ return root;
+}
+
+static JsonObject *
+build_meta_if_necessary (CockpitPcpMetrics *self,
+ pmResult *result)
+{
+ if (self->last)
+ {
+ /*
+ * If we've already sent the first meta message, then only send
+ * another when the set of instances in the results change.
+ */
+
+ if (result_meta_equal (self, self->last, result))
+ return NULL;
+ }
+
+ return build_meta (self, result);
+}
+
+static void
+build_sample (CockpitPcpMetrics *self,
+ double **buffer,
+ pmResult *result,
+ int metric,
+ int instance)
+{
+ MetricInfo *info = &self->metrics[metric];
+ int valfmt = result->vset[metric]->valfmt;
+ pmValue *value = &result->vset[metric]->vlist[instance];
+ pmAtomValue sample;
+
+ buffer[metric][instance] = NAN;
+
+ if (info->desc.type == PM_TYPE_AGGREGATE || info->desc.type == PM_TYPE_EVENT)
+ return;
+
+ if (result->vset[metric]->numval <= instance)
+ return;
+
+ /* Make sure we keep the least 48 significant bits of 64 bit numbers
+ since "delta" and "rate" derivation works on those, and the whole
+ 64 don't fit into a double.
+ */
+
+ if (info->desc.type == PM_TYPE_64)
+ {
+ if (pmExtractValue (valfmt, value, PM_TYPE_64, &sample, PM_TYPE_64) < 0)
+ return;
+
+ sample.d = (sample.ll << 16) >> 16;
+ }
+ else if (info->desc.type == PM_TYPE_U64)
+ {
+ if (pmExtractValue (valfmt, value, PM_TYPE_U64, &sample, PM_TYPE_U64) < 0)
+ return;
+
+ sample.d = (sample.ull << 16) >> 16;
+ }
+ else
+ {
+ if (pmExtractValue (valfmt, value, info->desc.type, &sample, PM_TYPE_DOUBLE) < 0)
+ return;
+ }
+
+ if (info->units != &info->desc.units)
+ {
+ if (pmConvScale (PM_TYPE_DOUBLE, &sample, &info->desc.units, &sample, info->units) < 0)
+ return;
+ sample.d *= info->factor;
+ }
+
+ buffer[metric][instance] = sample.d;
+}
+
+static void
+build_samples (CockpitPcpMetrics *self,
+ pmResult *result)
+{
+ double **buffer;
+ pmValueSet *vs;
+ int i, j;
+
+ buffer = cockpit_metrics_get_data_buffer (COCKPIT_METRICS (self));
+ for (i = 0; i < result->numpmid; i++)
+ {
+ vs = result->vset[i];
+
+ /* When negative numval is an error code ... we don't care */
+ if (vs->numval < 0)
+ {
+ ;
+ }
+ else if (self->metrics[i].desc.indom == PM_INDOM_NULL)
+ {
+ build_sample (self, buffer, result, i, 0);
+ }
+ else
+ {
+ for (j = 0; j < vs->numval; j++)
+ build_sample (self, buffer, result, i, j);
+ }
+ }
+}
+
+static void
+cockpit_pcp_metrics_tick (CockpitMetrics *metrics,
+ gint64 timestamp)
+{
+ CockpitPcpMetrics *self = (CockpitPcpMetrics *)metrics;
+ JsonObject *meta;
+ pmResult *result;
+ int rc;
+
+ if (pmUseContext (self->direct_context) < 0)
+ g_return_if_reached ();
+
+ rc = pmFetch (self->numpmid, self->pmidlist, &result);
+ if (rc < 0)
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error",
+ "%s: couldn't fetch metrics: %s", self->name, pmErrStr (rc));
+ return;
+ }
+
+ meta = build_meta_if_necessary (self, result);
+ if (meta)
+ {
+ cockpit_metrics_send_meta (metrics, meta, FALSE);
+ json_object_unref (meta);
+ }
+
+ /* Send one set of samples */
+ build_samples (self, result);
+ cockpit_metrics_send_data (metrics, timestamp_from_timeval (&result->timestamp));
+ cockpit_metrics_flush_data (metrics);
+
+ if (self->last)
+ pmFreeResult (self->last);
+ self->last = result;
+}
+
+static void next_archive (CockpitPcpMetrics *self);
+
+static gboolean
+on_idle_batch (gpointer user_data)
+{
+ const int archive_batch = 60;
+ CockpitPcpMetrics *self = user_data;
+ ArchiveInfo *info;
+ JsonObject *meta;
+ pmResult *result;
+ gint i;
+ int rc;
+
+ info = (ArchiveInfo *)(self->cur_archive->data);
+
+ if (pmUseContext (info->context) < 0)
+ {
+ self->idler = 0;
+ return FALSE;
+ }
+
+ for (i = 0; i < archive_batch; i++)
+ {
+ /* Sent enough samples? */
+ self->limit--;
+ if (self->limit < 0)
+ {
+ cockpit_metrics_flush_data (COCKPIT_METRICS (self));
+ cockpit_channel_close (COCKPIT_CHANNEL (self), NULL);
+ self->idler = 0;
+ return FALSE;
+ }
+
+ rc = pmFetch (self->numpmid, self->pmidlist, &result);
+ if (rc < 0)
+ {
+ self->idler = 0;
+
+ if (rc == PM_ERR_EOL)
+ {
+ cockpit_metrics_flush_data (COCKPIT_METRICS (self));
+ next_archive (self);
+ }
+ else
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error",
+ "%s: couldn't read from archive: %s", self->name, pmErrStr (rc));
+ }
+
+ return FALSE;
+ }
+
+ meta = build_meta_if_necessary (self, result);
+ if (meta)
+ {
+ cockpit_metrics_send_meta (COCKPIT_METRICS (self), meta, self->last == NULL);
+ json_object_unref (meta);
+ }
+
+ build_samples (self, result);
+ cockpit_metrics_send_data (COCKPIT_METRICS (self), timestamp_from_timeval (&result->timestamp));
+
+ if (self->last)
+ pmFreeResult (self->last);
+ self->last = result;
+ }
+
+ cockpit_metrics_flush_data (COCKPIT_METRICS (self));
+ return TRUE;
+}
+
+static gboolean
+units_equal (pmUnits *a,
+ pmUnits *b)
+{
+ return (a->scaleCount == b->scaleCount &&
+ a->scaleTime == b->scaleTime &&
+ a->scaleSpace == b->scaleSpace &&
+ a->dimCount == b->dimCount &&
+ a->dimTime == b->dimTime &&
+ a->dimSpace == b->dimSpace);
+}
+
+static gboolean
+units_convertible (pmUnits *a,
+ pmUnits *b)
+{
+ pmAtomValue dummy;
+ dummy.d = 0;
+ return pmConvScale (PM_TYPE_DOUBLE, &dummy, a, &dummy, b) >= 0;
+}
+
+static gboolean
+convert_metric_description (CockpitPcpMetrics *self,
+ JsonNode *node,
+ MetricInfo *info,
+ int index,
+ gboolean *not_found)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ const gchar *units;
+ char *errmsg;
+ int rc;
+
+ if (json_node_get_node_type (node) == JSON_NODE_OBJECT)
+ {
+ if (!cockpit_json_get_string (json_node_get_object (node), "name", NULL, &info->name)
+ || info->name == NULL)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"metrics\" option was specified (no name for metric %d)",
+ self->name, index);
+ return FALSE;
+ }
+
+ if (!cockpit_json_get_string (json_node_get_object (node), "units", NULL, &units))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid units for metric %s (not a string)",
+ self->name, info->name);
+ return FALSE;
+ }
+
+ if (!cockpit_json_get_string (json_node_get_object (node), "derive", NULL, &info->derive))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid derivation mode for metric %s (not a string)",
+ self->name, info->name);
+ return FALSE;
+ }
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"metrics\" option was specified (not an object for metric %d)",
+ self->name, index);
+ return FALSE;
+ }
+
+ // HACK: cast to `void *` as pcp-5.2.3 changed it from `char **` to `const char **`
+ rc = pmLookupName (1, (void *)&info->name, &info->id);
+ if (rc < 0)
+ {
+ if (not_found)
+ {
+ *not_found = TRUE;
+ g_message ("%s: no such metric: %s: %s", self->name, info->name, pmErrStr (rc));
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "not-found",
+ "%s: no such metric: %s: %s", self->name, info->name, pmErrStr (rc));
+ }
+ return FALSE;
+ }
+
+ rc = pmLookupDesc (info->id, &info->desc);
+ if (rc < 0)
+ {
+ if (not_found)
+ {
+ *not_found = TRUE;
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "not-found",
+ "%s: no such metric: %s: %s", self->name, info->name, pmErrStr (rc));
+ }
+ return FALSE;
+ }
+
+ if (units)
+ {
+ if (pmParseUnitsStr (units, &info->units_buf, &info->factor, &errmsg) < 0)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: failed to parse units %s: %s", self->name, units, errmsg);
+ free (errmsg);
+ return FALSE;
+ }
+
+ if (!units_convertible (&info->desc.units, &info->units_buf))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: can't convert metric %s to units %s", self->name, info->name, units);
+ return FALSE;
+ }
+
+ if (info->factor != 1.0 || !units_equal (&info->desc.units, &info->units_buf))
+ info->units = &info->units_buf;
+ }
+
+ if (!info->units)
+ {
+ info->units = &info->desc.units;
+ info->factor = 1.0;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+prepare_current_context (CockpitPcpMetrics *self,
+ gboolean *not_found)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ JsonObject *options;
+ g_autofree const gchar **instances = NULL;
+ g_autofree const gchar **omit_instances = NULL;
+ JsonArray *metrics;
+ int i;
+
+ g_free (self->metrics);
+ g_free (self->pmidlist);
+
+ self->numpmid = 0;
+ self->metrics = NULL;
+ self->pmidlist = NULL;
+
+ options = cockpit_channel_get_options (channel);
+
+ /* "instances" option */
+ if (!cockpit_json_get_strv (options, "instances", NULL, &instances))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"instances\" option (not an array of strings)", self->name);
+ return FALSE;
+ }
+
+ /* "omit-instances" option */
+ if (!cockpit_json_get_strv (options, "omit-instances", NULL, &omit_instances))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"omit-instances\" option (not an array of strings)", self->name);
+ return FALSE;
+ }
+
+ /* "metrics" option */
+ if (!cockpit_json_get_array (options, "metrics", NULL, &metrics))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"metrics\" option was specified (not an array)", self->name);
+ return FALSE;
+ }
+ if (metrics)
+ self->numpmid = json_array_get_length (metrics);
+
+ self->pmidlist = g_new0 (pmID, self->numpmid);
+ self->metrics = g_new0 (MetricInfo, self->numpmid);
+ for (i = 0; i < self->numpmid; i++)
+ {
+ MetricInfo *info = &self->metrics[i];
+ if (!convert_metric_description (self, json_array_get_element (metrics, i), info, i, not_found))
+ return FALSE;
+
+ self->pmidlist[i] = info->id;
+
+ if (info->desc.indom != PM_INDOM_NULL)
+ {
+ if (instances)
+ {
+ pmDelProfile (info->desc.indom, 0, NULL);
+ for (int i = 0; instances[i]; i++)
+ {
+ int instid = pmLookupInDom (info->desc.indom, instances[i]);
+ if (instid >= 0)
+ pmAddProfile (info->desc.indom, 1, &instid);
+ }
+ }
+ else if (omit_instances)
+ {
+ pmAddProfile (info->desc.indom, 0, NULL);
+ for (int i = 0; omit_instances[i]; i++)
+ {
+ int instid = pmLookupInDom (info->desc.indom, omit_instances[i]);
+ if (instid >= 0)
+ pmDelProfile (info->desc.indom, 1, &instid);
+ }
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+static void start_archive (CockpitPcpMetrics *self, gint64 timestamp);
+
+static void
+add_archive (CockpitPcpMetrics *self,
+ const gchar *name)
+{
+ ArchiveInfo *info;
+ pmLogLabel label;
+ int rc;
+
+ info = g_new0 (ArchiveInfo, 1);
+ info->context = pmNewContext (PM_CONTEXT_ARCHIVE, name);
+ if (info->context < 0)
+ {
+ if (info->context == -ENOENT)
+ {
+ g_debug ("%s: couldn't find pcp archive for %s", self->name, name);
+ }
+ else if (info->context != PM_ERR_NODATA)
+ {
+ g_warning ("%s: couldn't create pcp archive context for %s: %s (%d)",
+ self->name, name, pmErrStr (info->context), info->context);
+ }
+ g_free (info);
+ return;
+ }
+
+ rc = pmGetArchiveLabel (&label);
+ if (rc < 0)
+ {
+ g_warning ("%s: couldn't read archive label of %s: %s", self->name, name, pmErrStr (rc));
+ pmDestroyContext (info->context);
+ g_free (info);
+ return;
+ }
+
+ info->start = (gint64) label.ll_start.tv_sec * 1000 + label.ll_start.tv_usec / 1000;
+ self->archives = g_list_prepend (self->archives, info);
+ return;
+}
+
+static gint
+cmp_archive_start (gconstpointer a,
+ gconstpointer b)
+{
+ const ArchiveInfo *a_info = a;
+ const ArchiveInfo *b_info = b;
+
+ if (a_info->start > b_info->start)
+ return 1;
+ else if (a_info->start < b_info->start)
+ return -1;
+ else
+ return 0;
+}
+
+static gboolean
+prepare_archives (CockpitPcpMetrics *self,
+ const gchar *name,
+ gint64 timestamp)
+{
+ GDir *dir;
+ int count;
+ GError *error = NULL;
+
+ dir = g_dir_open (name, 0, &error);
+ if (dir)
+ {
+ const gchar *entry;
+ count = 0;
+ while ((entry = g_dir_read_name (dir)) && count < 200)
+ {
+ if (g_str_has_suffix (entry, ".index"))
+ {
+ gchar *path = g_build_filename (name, entry, NULL);
+ path[strlen(path)-strlen(".index")] = '\0';
+ add_archive (self, path);
+ g_free (path);
+ count += 1;
+ }
+ }
+ g_dir_close (dir);
+ }
+ else if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ {
+ g_clear_error (&error);
+ add_archive (self, name);
+ }
+ else
+ {
+ cockpit_channel_fail (COCKPIT_CHANNEL (self), "internal-error",
+ "%s: %s", name, error->message);
+ g_clear_error (&error);
+ return FALSE;
+ }
+
+ if (self->archives == NULL)
+ {
+ cockpit_channel_close (COCKPIT_CHANNEL (self), "not-found");
+ return FALSE;
+ }
+
+ self->archives = g_list_sort (self->archives, cmp_archive_start);
+
+ self->cur_archive = self->archives;
+ start_archive (self, timestamp);
+ return TRUE;
+}
+
+static void
+start_archive (CockpitPcpMetrics *self,
+ gint64 timestamp)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ ArchiveInfo *info;
+ struct timeval stamp;
+ gboolean not_found;
+ int rc;
+
+ while (self->cur_archive && self->cur_archive->next
+ && ((ArchiveInfo *)(self->cur_archive->next->data))->start < timestamp)
+ self->cur_archive = self->cur_archive->next;
+
+ again:
+ if (self->cur_archive == NULL)
+ {
+ cockpit_channel_close (channel, NULL);
+ return;
+ }
+
+ info = self->cur_archive->data;
+
+ if (timestamp < info->start)
+ timestamp = info->start;
+
+ stamp.tv_sec = (timestamp / 1000);
+ stamp.tv_usec = (timestamp % 1000) * 1000;
+
+ rc = pmUseContext (info->context);
+ if (rc < 0)
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "%s: couldn't switch pcp context: %s", self->name, pmErrStr (rc));
+ return;
+ }
+
+ rc = pmSetMode (PM_MODE_INTERP | PM_XTB_SET(PM_TIME_MSEC), &stamp, self->interval);
+ if (rc < 0)
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "%s: couldn't set pcp mode: %s", self->name, pmErrStr (rc));
+ return;
+ }
+
+ not_found = TRUE;
+ if (!prepare_current_context (self, &not_found))
+ {
+ if (not_found)
+ {
+ self->cur_archive = self->cur_archive->next;
+ goto again;
+ }
+ return;
+ }
+
+ /* Make sure we send a meta message.
+ */
+ if (self->last)
+ pmFreeResult (self->last);
+ self->last = NULL;
+
+ g_assert (self->idler == 0);
+ self->idler = g_idle_add (on_idle_batch, self);
+}
+
+static void
+next_archive (CockpitPcpMetrics *self)
+{
+ self->cur_archive = self->cur_archive->next;
+ start_archive (self, 0);
+}
+
+static gboolean
+ensure_pcp_conf (CockpitChannel *channel)
+{
+ gboolean res = TRUE;
+ gchar *prefix;
+ gchar *conf;
+ gchar *confpath = NULL;
+ FILE *fp = NULL;
+
+ /* Libpcp is prone to call exit(1) behind our backs when it can't
+ find its config file, so we check here first.
+ */
+
+ prefix = getenv("PCP_DIR");
+ conf = getenv("PCP_CONF");
+
+ if (conf == NULL)
+ {
+ if (prefix == NULL)
+ confpath = g_strdup ("/etc/pcp.conf");
+ else
+ confpath = g_strdup_printf ("%s/etc/pcp.conf", prefix);
+ conf = confpath;
+ }
+
+ if ((fp = fopen(conf, "r")) == NULL)
+ {
+ cockpit_channel_fail (channel, "internal-error", "could not access %s: %m", conf);
+ res = FALSE;
+ }
+
+ if (fp)
+ fclose (fp);
+ g_free (confpath);
+ return res;
+}
+
+static void
+cockpit_pcp_metrics_prepare (CockpitChannel *channel)
+{
+ CockpitPcpMetrics *self = COCKPIT_PCP_METRICS (channel);
+ JsonObject *options;
+ const gchar *source;
+ int type;
+ char *name = NULL;
+ gint64 timestamp;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_pcp_metrics_parent_class)->prepare (channel);
+
+ options = cockpit_channel_get_options (channel);
+
+ if (!ensure_pcp_conf (channel))
+ goto out;
+
+ /* "source" option */
+ if (!cockpit_json_get_string (options, "source", NULL, &source))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"source\" option for metrics channel");
+ goto out;
+ }
+ else if (!source)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "no \"source\" option specified for metrics channel");
+ goto out;
+ }
+ else if (g_str_has_prefix (source, "/"))
+ {
+ type = PM_CONTEXT_ARCHIVE;
+ name = g_strdup (source);
+ }
+ else if (g_str_has_prefix (source, "pcp-archive"))
+ {
+ gchar *dir = pmGetConfig("PCP_LOG_DIR");
+ gchar hostname[HOST_NAME_MAX + 1];
+ if (gethostname (hostname, HOST_NAME_MAX) < 0)
+ {
+ cockpit_channel_fail (channel, "internal-error", "error getting hostname: %m");
+ goto out;
+ }
+ hostname[HOST_NAME_MAX] = '\0';
+ type = PM_CONTEXT_ARCHIVE;
+ name = g_strdup_printf ("%s/pmlogger/%s", dir, hostname);
+ }
+ else if (g_str_equal (source, "direct"))
+ {
+ type = PM_CONTEXT_LOCAL;
+ name = NULL;
+ }
+ else if (g_str_equal (source, "pmcd"))
+ {
+ type = PM_CONTEXT_HOST;
+ name = g_strdup ("local:");
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "not-supported",
+ "unsupported \"source\" option specified for metrics: %s", source);
+ goto out;
+ }
+
+ self->name = source;
+
+ /* "timestamp" option */
+ if (!cockpit_json_get_int (options, "timestamp", 0, &timestamp))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"timestamp\" option", self->name);
+ goto out;
+ }
+ if (timestamp / 1000 < G_MINLONG || timestamp / 1000 > G_MAXLONG)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"timestamp\" value: %" G_GINT64_FORMAT, self->name, timestamp);
+ goto out;
+ }
+
+ if (timestamp < 0)
+ {
+ struct timeval now;
+ gettimeofday (&now, NULL);
+ timestamp = ((gint64) now.tv_sec * 1000 + now.tv_usec / 1000) + timestamp;
+ }
+
+ /* "limit" option */
+ if (!cockpit_json_get_int (options, "limit", G_MAXINT64, &self->limit))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"limit\" option", self->name);
+ goto out;
+ }
+ else if (self->limit <= 0)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"limit\" option value: %" G_GINT64_FORMAT, self->name, self->limit);
+ goto out;
+ }
+
+ /* "interval" option */
+ if (!cockpit_json_get_int (options, "interval", 1000, &self->interval))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"interval\" option", self->name);
+ goto out;
+ }
+ else if (self->interval <= 0 || self->interval > G_MAXINT)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"interval\" value: %" G_GINT64_FORMAT, self->name, self->interval);
+ goto out;
+ }
+
+ if (type == PM_CONTEXT_ARCHIVE)
+ {
+ if (!prepare_archives (self, name, timestamp))
+ goto out;
+ }
+ else
+ {
+ self->direct_context = pmNewContext(type, name);
+ if (self->direct_context < 0)
+ {
+ if (self->direct_context == -ENOENT)
+ {
+ g_debug ("%s: couldn't create PCP context: %s", self->name, pmErrStr (self->direct_context));
+ cockpit_channel_close (channel, "not-supported");
+ }
+ else
+ {
+ cockpit_channel_fail (channel, "internal-error",
+ "%s: couldn't create PCP context: %s",
+ self->name, pmErrStr (self->direct_context));
+ }
+ goto out;
+ }
+
+ if (!prepare_current_context (self, NULL))
+ goto out;
+ }
+
+ if (type != PM_CONTEXT_ARCHIVE)
+ cockpit_metrics_metronome (COCKPIT_METRICS (self), self->interval);
+ cockpit_channel_ready (channel, NULL);
+
+out:
+ g_free (name);
+}
+
+static void
+cockpit_pcp_metrics_dispose (GObject *object)
+{
+ CockpitPcpMetrics *self = COCKPIT_PCP_METRICS (object);
+
+ if (self->idler)
+ {
+ g_source_remove (self->idler);
+ self->idler = 0;
+ }
+
+ if (self->last)
+ {
+ pmFreeResult (self->last);
+ self->last = NULL;
+ }
+
+ for (GList *a = self->archives; a; a = a->next)
+ {
+ ArchiveInfo *info = a->data;
+ if (info->context >= 0)
+ pmDestroyContext (info->context);
+ g_free (info);
+ }
+ g_list_free (self->archives);
+ self->archives = NULL;
+
+ if (self->direct_context >= 0)
+ {
+ pmDestroyContext (self->direct_context);
+ self->direct_context = -1;
+ }
+
+ G_OBJECT_CLASS (cockpit_pcp_metrics_parent_class)->dispose (object);
+}
+
+static void
+cockpit_pcp_metrics_finalize (GObject *object)
+{
+ CockpitPcpMetrics *self = COCKPIT_PCP_METRICS (object);
+
+ g_free (self->metrics);
+ g_free (self->pmidlist);
+
+ G_OBJECT_CLASS (cockpit_pcp_metrics_parent_class)->finalize (object);
+}
+
+static void
+cockpit_pcp_metrics_class_init (CockpitPcpMetricsClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitMetricsClass *metrics_class = COCKPIT_METRICS_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->dispose = cockpit_pcp_metrics_dispose;
+ gobject_class->finalize = cockpit_pcp_metrics_finalize;
+
+ channel_class->prepare = cockpit_pcp_metrics_prepare;
+ metrics_class->tick = cockpit_pcp_metrics_tick;
+}
diff --git a/src/bridge/cockpitpcpmetrics.h b/src/bridge/cockpitpcpmetrics.h
new file mode 100644
index 0000000..ede263b
--- /dev/null
+++ b/src/bridge/cockpitpcpmetrics.h
@@ -0,0 +1,31 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_PCP_METRICS_H__
+#define COCKPIT_PCP_METRICS_H__
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_PCP_METRICS (cockpit_pcp_metrics_get_type ())
+
+GType cockpit_pcp_metrics_get_type (void) G_GNUC_CONST;
+
+#endif /* COCKPIT_PCP_METRICS_H__ */
diff --git a/src/bridge/cockpitpeer.c b/src/bridge/cockpitpeer.c
new file mode 100644
index 0000000..562d0f0
--- /dev/null
+++ b/src/bridge/cockpitpeer.c
@@ -0,0 +1,1172 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpeer.h"
+#include "cockpitrouter.h"
+
+#include "common/cockpitauthorize.h"
+#include "common/cockpitfdpassing.h"
+#include "common/cockpithex.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitmemory.h"
+#include "common/cockpitpipe.h"
+#include "common/cockpitpipetransport.h"
+#include "common/cockpittransport.h"
+
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <errno.h>
+#include <pty.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+struct _CockpitPeer {
+ GObject parent;
+
+ /* Our bridge configuration */
+ const gchar *name;
+ JsonObject *config;
+ guint timeout;
+
+ /* The channels we're dealing with */
+ GHashTable *channels;
+ GQueue *frozen;
+
+ /* Authorize types we will reply to */
+ GHashTable *authorize_values;
+ guint authorize_values_timeout;
+
+ /* first_host */
+ gboolean first_channel_done;
+ gchar *init_host;
+ gchar *init_superuser;
+
+ /* The transport we're routing from */
+ CockpitTransport *transport;
+ CockpitRouter *router;
+ gulong transport_recv;
+ gulong transport_control;
+ GBytes *last_init;
+
+ /* When open and ready */
+ CockpitTransport *other;
+ gulong other_recv;
+ gulong other_control;
+ gulong other_closed;
+ gboolean inited;
+ gboolean closed;
+ gchar *problem;
+ JsonObject *failure;
+
+ /* Startup */
+ gchar *startup_auth_cookie;
+ CockpitPeerDoneFunction *startup_done_function;
+ gpointer startup_done_data;
+};
+
+enum {
+ PROP_0,
+ PROP_TRANSPORT,
+ PROP_ROUTER,
+ PROP_CONFIG,
+};
+
+G_DEFINE_TYPE (CockpitPeer, cockpit_peer, G_TYPE_OBJECT);
+
+static gchar *
+startup_take_stderr (CockpitPeer *self)
+{
+ if (!self->other)
+ return NULL;
+
+ CockpitPipe *pipe = cockpit_pipe_transport_get_pipe (COCKPIT_PIPE_TRANSPORT (self->other));
+ return cockpit_pipe_take_stderr_as_utf8 (pipe);
+}
+
+static void
+startup_done (CockpitPeer *self,
+ const gchar *problem)
+{
+ gchar *stderr = startup_take_stderr (self);
+
+ if (self->other)
+ {
+ CockpitPipe *pipe = cockpit_pipe_transport_get_pipe (COCKPIT_PIPE_TRANSPORT (self->other));
+ cockpit_pipe_stop_stderr_capture (pipe);
+ }
+
+ if (self->startup_done_function)
+ {
+ self->startup_done_function (problem, stderr, self->startup_done_data);
+ self->startup_done_function = NULL;
+ }
+
+ g_free (stderr);
+}
+
+static void
+reply_channel_closed (CockpitPeer *self,
+ const gchar *channel,
+ const gchar *problem)
+{
+ JsonObject *object;
+ GBytes *message;
+ GList *l, *names;
+
+ object = json_object_new ();
+
+ /* Copy over any failures from a "problem" in an "init" message */
+ if (self->failure)
+ {
+ names = json_object_get_members (self->failure);
+ for (l = names; l != NULL; l = g_list_next (l))
+ json_object_set_member (object, l->data, json_object_dup_member (self->failure, l->data));
+ g_list_free (names);
+ }
+
+ json_object_set_string_member (object, "command", "close");
+ json_object_set_string_member (object, "channel", channel);
+ json_object_set_string_member (object, "problem", problem);
+
+ message = cockpit_json_write_bytes (object);
+ cockpit_transport_send (self->transport, NULL, message);
+
+ json_object_unref (object);
+ g_bytes_unref (message);
+}
+
+static void
+clear_authorize_value (gpointer pointer)
+{
+ char *data = pointer;
+ if (data)
+ cockpit_memory_clear (data, -1);
+ g_free (data);
+}
+
+static gboolean
+on_other_recv (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *payload,
+ gpointer user_data)
+{
+ CockpitPeer *self = user_data;
+
+ if (channel)
+ {
+ cockpit_transport_send (self->transport, channel, payload);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+on_timeout_reset (gpointer user_data)
+{
+ CockpitPeer *self = user_data;
+
+ self->timeout = 0;
+ if (g_hash_table_size (self->channels) == 0)
+ {
+ g_debug ("%s: peer timed out without channels", self->name);
+ cockpit_peer_reset (self);
+ }
+
+ return FALSE;
+}
+
+
+static void
+on_answer (const gchar *value,
+ gpointer user_data)
+{
+ CockpitPeer *self = user_data;
+
+ if (self->startup_auth_cookie)
+ {
+ GBytes *reply = cockpit_transport_build_control ("command", "authorize",
+ "cookie", self->startup_auth_cookie,
+ "response",
+ value,
+ NULL);
+ cockpit_transport_send (self->other, NULL, reply);
+ g_bytes_unref (reply);
+ g_free (self->startup_auth_cookie);
+ self->startup_auth_cookie = NULL;
+ }
+}
+
+static gboolean
+cockpit_peer_delete_authorize_values (gpointer user_data)
+{
+ CockpitPeer *self = user_data;
+ g_hash_table_remove_all (self->authorize_values);
+ self->authorize_values_timeout = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+on_other_control (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload,
+ gpointer user_data)
+{
+ CockpitPeer *self = user_data;
+ const gchar *problem = NULL;
+ const gchar *cookie = NULL;
+ const gchar *challenge = NULL;
+ const gchar *prompt;
+ gboolean privileged;
+ GBytes *reply;
+ gint64 timeout;
+ gint64 version;
+ char *type = NULL;
+ GList *l;
+
+ /* Got an init message thaw all channels */
+ if (g_str_equal (command, "init"))
+ {
+ JsonObject *capabilities;
+ gboolean explicit_superuser_capability = FALSE;
+
+ if (!cockpit_json_get_string (options, "problem", NULL, &problem))
+ {
+ g_warning ("%s: invalid \"problem\" field in init message", self->name);
+ problem = "protocol-error";
+ }
+ else if (problem)
+ {
+ if (self->failure)
+ json_object_unref (self->failure);
+ self->failure = json_object_ref (options);
+ json_object_remove_member (self->failure, "version");
+ }
+ else if (!cockpit_json_get_int (options, "version", -1, &version))
+ {
+ g_warning ("%s: invalid \"version\" field in init message", self->name);
+ problem = "protocol-error";
+ }
+ else if (version == -1)
+ {
+ g_warning ("%s: missing \"version\" field in init message", self->name);
+ problem = "protocol-error";
+ }
+ else if (version != 1)
+ {
+ g_message ("%s: unsupported \"version\" of cockpit protocol: %" G_GINT64_FORMAT,
+ self->name, version);
+ problem = "not-supported";
+ }
+
+ if (cockpit_json_get_object (options, "capabilities", NULL, &capabilities) && capabilities)
+ {
+ if (!cockpit_json_get_bool (capabilities, "explicit-superuser", FALSE, &explicit_superuser_capability))
+ g_warning ("invalid 'explicit-superuser' value in init message");
+ }
+
+ // Authorization for SSH is over now, but we still need the
+ // authorize_values for superuser initialization.
+ //
+ // If the peer has the explicit-superuser capability, we know it
+ // will send us a "superuser-init-done" message, and we can
+ // delete the creds at that time. For a legacy bridge without
+ // explicit-superuser, we give it two minutes to start up sudo.
+
+ if (self->authorize_values_timeout)
+ g_source_remove (self->authorize_values_timeout);
+ self->authorize_values_timeout = g_timeout_add (2*60*1000, cockpit_peer_delete_authorize_values, self);
+
+ if (problem)
+ {
+ startup_done (self, problem);
+ cockpit_transport_close (transport, problem);
+ }
+ else
+ {
+ g_debug ("%s: received init message from peer bridge", self->name);
+ self->inited = TRUE;
+ startup_done (self, NULL);
+
+ if (!self->last_init)
+ {
+ JsonObject *object = cockpit_transport_build_json ("command", "init", NULL);
+ json_object_set_int_member (object, "version", 1);
+ json_object_set_string_member (object, "host", self->init_host ? self->init_host : "localhost");
+
+ if (explicit_superuser_capability)
+ {
+ const gchar *superuser = "none";
+ if (self->init_superuser && *self->init_superuser)
+ superuser = self->init_superuser;
+
+ if (!g_str_equal (superuser, "none"))
+ {
+ JsonObject *superuser_options;
+
+ superuser_options = json_object_new ();
+ json_object_set_string_member (superuser_options, "id", superuser);
+ json_object_set_object_member (object, "superuser", superuser_options);
+ }
+ else
+ {
+ json_object_set_boolean_member (object, "superuser", FALSE);
+ g_hash_table_remove_all (self->authorize_values);
+ }
+ }
+
+ self->last_init = cockpit_json_write_bytes (object);
+ json_object_unref (object);
+ }
+ cockpit_transport_send (transport, NULL, self->last_init);
+
+ if (self->frozen)
+ {
+ for (l = self->frozen->head; l != NULL; l = g_list_next (l))
+ cockpit_transport_thaw (self->transport, l->data);
+ g_queue_free_full (self->frozen, g_free);
+ self->frozen = NULL;
+ }
+ }
+ }
+
+ else if (g_str_equal (command, "superuser-init-done"))
+ {
+ cockpit_peer_delete_authorize_values (self);
+ }
+
+ else if (g_str_equal (command, "authorize"))
+ {
+ if (!cockpit_json_get_string (options, "cookie", NULL, &cookie) || cookie == NULL)
+ {
+ g_message ("%s: received \"authorize\" request without a valid cookie", self->name);
+ }
+
+ else if (!cockpit_json_get_string (options, "challenge", NULL, &challenge))
+ {
+ g_message ("%s: received \"authorize\" request with a invalid challenge", self->name);
+ }
+ else if (!cockpit_json_get_string (options, "prompt", NULL, &prompt))
+ {
+ g_message ("%s: received \"authorize\" request with a invalid prompt", self->name);
+ }
+
+ /* Hook into the superuser startup mechanism for privileged bridges.
+ */
+ else if (cockpit_json_get_bool (self->config, "privileged", FALSE, &privileged) && privileged)
+ {
+ if (self->startup_auth_cookie)
+ g_warning ("%s: received overlapping \"authorize\" requests", self->name);
+ else if (!self->router)
+ g_warning ("%s: no router for answering \"authorize\" request", self->name);
+ else
+ {
+ gchar *user_hex;
+ char *user;
+ gchar *stderr;
+ self->startup_auth_cookie = g_strdup (cookie);
+ cockpit_authorize_subject (challenge, &user_hex);
+ user = cockpit_hex_decode (user_hex, -1, NULL);
+ stderr = startup_take_stderr (self);
+ cockpit_router_prompt (self->router, user, prompt, stderr, on_answer, self);
+ g_free (user);
+ free (user_hex);
+ g_free (stderr);
+ }
+ }
+
+ /* If we have info we can respond to basic authorize challenges.
+ This is used for remote machines.
+ */
+ else if (challenge && g_hash_table_contains (self->authorize_values, challenge))
+ {
+ reply = cockpit_transport_build_control ("command", "authorize",
+ "cookie", cookie,
+ "response",
+ g_hash_table_lookup (self->authorize_values, challenge),
+ NULL);
+ g_hash_table_remove (self->authorize_values, challenge);
+ cockpit_transport_send (transport, NULL, reply);
+ g_bytes_unref (reply);
+ }
+
+ /* Don't pass on "authorize" messages.
+ */
+ else
+ {
+ reply = cockpit_transport_build_control ("command", "authorize",
+ "cookie", cookie,
+ "response", "",
+ NULL);
+ cockpit_transport_send (transport, NULL, reply);
+ g_bytes_unref (reply);
+ }
+ }
+
+ /* cockpit-bridge --privileged expects to receive a copy of our stderr */
+ else if (g_str_equal (command, "send-stderr") &&
+ cockpit_json_get_bool (self->config, "privileged", FALSE, &privileged) && privileged)
+ {
+ CockpitPipe *pipe = cockpit_pipe_transport_get_pipe (COCKPIT_PIPE_TRANSPORT (transport));
+
+ int out_fd;
+ g_object_get (pipe, "out-fd", &out_fd, NULL);
+
+ if (!cockpit_socket_send_fd (out_fd, STDERR_FILENO))
+ {
+ g_critical ("sendmsg() with stderr fd failed: %m");
+ cockpit_transport_close (transport, "internal-error");
+ }
+ }
+
+ /* Otherwise we need an init message first */
+ else if (!self->inited)
+ {
+ g_warning ("%s: did not receive an \"init\" message first", self->name);
+ cockpit_transport_close (transport, "protocol-error");
+ }
+
+ /* A channel specific control message */
+ else if (channel)
+ {
+ /* Stop keeping track of channels that are closed */
+ if (g_str_equal (command, "close"))
+ {
+ g_hash_table_remove (self->channels, channel);
+ if (g_hash_table_size (self->channels) == 0)
+ {
+ g_debug ("%s: removed last channel for peer", self->name);
+ if (self->timeout)
+ g_source_remove (self->timeout);
+ self->timeout = 0;
+ if (cockpit_json_get_int (self->config, "timeout", -1, &timeout) && timeout >= 0)
+ self->timeout = g_timeout_add_seconds (timeout, on_timeout_reset, self);
+ }
+ }
+
+ /* All control messages with a channel get forwarded */
+ cockpit_transport_send (self->transport, NULL, payload);
+ }
+
+ g_free (type);
+ return TRUE;
+}
+
+static const gchar *
+fail_start_problem (CockpitPeer *self)
+{
+ const gchar *problem = NULL;
+
+ /* This might be a "problem" in an "init" message from other bridge */
+ if (self->failure)
+ {
+ if (!cockpit_json_get_string (self->failure, "problem", NULL, &problem))
+ problem = NULL;
+ }
+
+ if (!problem)
+ {
+ if (!cockpit_json_get_string (self->config, "problem", NULL, &problem))
+ problem = NULL;
+ }
+
+ g_free (self->problem);
+ self->problem = g_strdup (problem);
+
+ return self->problem;
+}
+
+static void
+on_other_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitPeer *self = COCKPIT_PEER (user_data);
+ const gchar *channel;
+ GList *l, *channels;
+ CockpitPipe *pipe;
+ gint status = 0;
+ gint64 timeout;
+ const gchar *startup_problem = problem;
+
+ /*
+ * If we haven't yet gotten an "init" message, then we use the
+ * problem code that is in the config. If no problem is configured
+ * then we don't close the channel, but let the channel be handled
+ * elsewhere or eventually fail with "not-supported".
+ */
+ if (!self->inited)
+ {
+ g_debug ("%s: bridge failed to start%s%s", self->name,
+ problem ? ": " : "", problem ? problem : "");
+ problem = fail_start_problem (self);
+ }
+
+ /*
+ * The peer has closed after we received an init message. It was
+ * up and running and now it's gone. We're more verbose here
+ * and end up closing channels that were open.
+ */
+ else if (!self->closed)
+ {
+ pipe = cockpit_pipe_transport_get_pipe (COCKPIT_PIPE_TRANSPORT (transport));
+
+ if (cockpit_pipe_get_pid (pipe, NULL))
+ status = cockpit_pipe_exit_status (pipe);
+
+ if (WIFSIGNALED (status) && (WTERMSIG (status) == SIGTERM || WTERMSIG (status) == SIGHUP))
+ {
+ g_debug ("%s: bridge was terminated", self->name);
+ if (!problem)
+ problem = "terminated";
+ }
+ else if (WIFSIGNALED (status))
+ {
+ g_message ("%s: bridge was killed: %d", self->name, (int)WTERMSIG (status));
+ if (!problem)
+ problem = "internal-error";
+ }
+ else if (WIFEXITED (status) && WEXITSTATUS (status) != 0)
+ {
+ g_message ("%s: bridge failed: %d", self->name, (int)WEXITSTATUS (status));
+ if (!problem)
+ problem = "internal-error";
+ }
+ else
+ {
+ g_debug ("%s: bridge exited", self->name);
+ if (!problem)
+ problem = "disconnected";
+ }
+ }
+
+ startup_done (self, problem ? problem : startup_problem);
+
+ g_signal_handler_disconnect (self->other, self->other_closed);
+ g_signal_handler_disconnect (self->other, self->other_recv);
+ g_signal_handler_disconnect (self->other, self->other_control);
+ g_object_unref (self->other);
+ self->other = NULL;
+
+ self->closed = TRUE;
+
+ /* Handle any remaining open channels */
+ channels = g_hash_table_get_values (self->channels);
+ g_hash_table_steal_all (self->channels);
+ for (l = channels; l != NULL; l = g_list_next (l))
+ {
+ channel = l->data;
+
+ /*
+ * If we have a problem code, that either means that we failed
+ * after the peer bridge came up ... or it didn't come up at
+ * all yet. See above. In these cases we close the channel.
+ */
+ if (problem)
+ reply_channel_closed (self, channel, problem);
+
+ /*
+ * When we don't have a problem code we want this channel
+ * to be handled elsewhere. So thaw it and let that happen.
+ */
+ else
+ g_assert (!self->inited);
+
+ cockpit_transport_thaw (self->transport, channel);
+ }
+ g_list_free_full (channels, g_free);
+
+ /* If the timeout is set, then expect that this bridge can cycle back up */
+ if (cockpit_json_get_int (self->config, "timeout", -1, &timeout) && timeout >= 0)
+ cockpit_peer_reset (self);
+}
+
+static gboolean
+on_transport_recv (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *payload,
+ gpointer user_data)
+{
+ CockpitPeer *self = COCKPIT_PEER (user_data);
+
+ if (self->other && channel && g_hash_table_lookup (self->channels, channel))
+ {
+ cockpit_transport_send (self->other, channel, payload);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+on_transport_control (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload,
+ gpointer user_data)
+{
+ CockpitPeer *self = user_data;
+ gboolean forward = FALSE;
+ gboolean handled = FALSE;
+
+ if (g_str_equal (command, "init"))
+ {
+ if (self->last_init)
+ g_bytes_unref (self->last_init);
+ self->last_init = g_bytes_ref (payload);
+ }
+ else if (channel && g_hash_table_lookup (self->channels, channel))
+ {
+ handled = forward = TRUE;
+ if (g_str_equal (command, "close"))
+ g_hash_table_remove (self->channels, channel);
+ }
+ else if (self->inited)
+ {
+ if (g_str_equal (command, "kill"))
+ {
+ forward = TRUE;
+ }
+ }
+
+ if (forward && self->other)
+ cockpit_transport_send (self->other, NULL, payload);
+
+ return handled;
+}
+
+static void
+cockpit_peer_init (CockpitPeer *self)
+{
+ self->channels = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ self->authorize_values = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, clear_authorize_value);
+}
+
+static void
+cockpit_peer_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitPeer *self = COCKPIT_PEER (object);
+
+ switch (prop_id)
+ {
+ case PROP_TRANSPORT:
+ g_value_set_object (value, self->transport);
+ break;
+ case PROP_ROUTER:
+ g_value_set_object (value, self->router);
+ break;
+ case PROP_CONFIG:
+ g_value_set_boxed (value, self->config);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_peer_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitPeer *self = COCKPIT_PEER (object);
+
+ switch (prop_id)
+ {
+ case PROP_TRANSPORT:
+ self->transport = g_value_dup_object (value);
+ break;
+ case PROP_ROUTER:
+ self->router = g_value_get_object (value);
+ if (self->router)
+ g_object_add_weak_pointer (G_OBJECT(self->router), (gpointer *)&self->router);
+ break;
+ case PROP_CONFIG:
+ self->config = g_value_dup_boxed (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_peer_dispose (GObject *object)
+{
+ CockpitPeer *self = COCKPIT_PEER (object);
+
+ cockpit_peer_reset (self);
+ self->closed = TRUE;
+
+ if (self->transport_recv)
+ {
+ g_signal_handler_disconnect (self->transport, self->transport_recv);
+ self->transport_recv = 0;
+ }
+ if (self->transport_control)
+ {
+ g_signal_handler_disconnect (self->transport, self->transport_control);
+ self->transport_control = 0;
+ }
+
+ G_OBJECT_CLASS (cockpit_peer_parent_class)->dispose (object);
+}
+
+static void
+cockpit_peer_finalize (GObject *object)
+{
+ CockpitPeer *self = COCKPIT_PEER (object);
+
+ g_hash_table_destroy (self->channels);
+ g_hash_table_destroy (self->authorize_values);
+
+ if (self->config)
+ json_object_unref (self->config);
+ if (self->transport)
+ g_object_unref (self->transport);
+ if (self->router)
+ {
+ cockpit_router_prompt_cancel (self->router, self);
+ g_object_remove_weak_pointer (G_OBJECT(self->router), (gpointer *)&self->router);
+ }
+ if (self->last_init)
+ g_bytes_unref (self->last_init);
+
+ g_free (self->problem);
+ g_free (self->init_host);
+ g_free (self->init_superuser);
+
+ G_OBJECT_CLASS (cockpit_peer_parent_class)->finalize (object);
+}
+
+static void
+cockpit_peer_constructed (GObject *object)
+{
+ CockpitPeer *self = COCKPIT_PEER (object);
+ JsonArray *array;
+ JsonNode *node;
+
+ G_OBJECT_CLASS (cockpit_peer_parent_class)->constructed (object);
+
+ g_return_if_fail (self->config != NULL);
+ g_return_if_fail (self->transport != NULL);
+
+ self->transport_recv = g_signal_connect (self->transport, "recv", G_CALLBACK (on_transport_recv), self);
+ self->transport_control = g_signal_connect (self->transport, "control", G_CALLBACK (on_transport_control), self);
+
+ /* Get a name */
+ if (!cockpit_json_get_array (self->config, "spawn", NULL, &array))
+ array = NULL;
+ if (array && json_array_get_length (array) > 0)
+ {
+ node = json_array_get_element (array, 0);
+ if (node && JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) == G_TYPE_STRING)
+ self->name = json_node_get_string (node);
+ }
+}
+
+static void
+cockpit_peer_class_init (CockpitPeerClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->get_property = cockpit_peer_get_property;
+ object_class->set_property = cockpit_peer_set_property;
+ object_class->constructed = cockpit_peer_constructed;
+ object_class->finalize = cockpit_peer_finalize;
+ object_class->dispose = cockpit_peer_dispose;
+
+ g_object_class_install_property (object_class, PROP_TRANSPORT,
+ g_param_spec_object ("transport", "transport", "transport",
+ COCKPIT_TYPE_TRANSPORT,
+ G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (object_class, PROP_ROUTER,
+ g_param_spec_object ("router", "router", "router",
+ COCKPIT_TYPE_ROUTER,
+ G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (object_class, PROP_CONFIG,
+ g_param_spec_boxed ("config", "config", "config",
+ JSON_TYPE_OBJECT,
+ G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+}
+
+/**
+ * cockpit_peer_new:
+ * @transport: Transport to talk to cockpit-ws with
+ * @config: The peer bridge configuration
+ *
+ * Create a new peer bridge object. The configuration is in the
+ * manifest.json format as documented in doc/guide/
+ *
+ * Returns: (transfer full): The new peer object.
+ */
+CockpitPeer *
+cockpit_peer_new (CockpitTransport *transport,
+ JsonObject *config)
+{
+ return g_object_new (COCKPIT_TYPE_PEER,
+ "transport", transport,
+ "config", config,
+ NULL);
+}
+
+static void
+spawn_setup (gpointer data)
+{
+ int fd = GPOINTER_TO_INT (data);
+
+ /* Send this signal to all direct child processes, when bridge dies */
+ prctl (PR_SET_PDEATHSIG, SIGHUP);
+
+ if (dup2 (fd, 0) < 0 || dup2 (fd, 1) < 0)
+ {
+ perror ("couldn't set peer stdin/stout file descriptors");
+ _exit (1);
+ }
+
+ close (fd);
+}
+
+static CockpitPipe *
+spawn_process_for_config (CockpitPeer *self,
+ gboolean capture_stderr)
+{
+ const gchar *default_argv[] = { "/bin/false", NULL };
+ CockpitPipe *pipe = NULL;
+ const gchar *directory = NULL;
+ const gchar **argv = NULL;
+ const gchar **envset = NULL;
+ gchar **env = NULL;
+ GError *error = NULL;
+ GPid pid = 0;
+ int fds[2];
+ int stderr_fd = -1;
+
+ if (socketpair (PF_LOCAL, SOCK_STREAM, 0, fds) < 0)
+ {
+ g_warning ("couldn't create loopback socket: %s", g_strerror (errno));
+ }
+ else if (!cockpit_json_get_string (self->config, "directory", NULL, &directory) ||
+ !cockpit_json_get_strv (self->config, "environ", NULL, &envset) ||
+ !cockpit_json_get_strv (self->config, "spawn", default_argv, &argv))
+ {
+ g_message ("%s: invalid bridge configuration, cannot spawn channel", self->name);
+ }
+ else
+ {
+ g_debug ("%s: spawning peer bridge process", self->name);
+
+ env = cockpit_pipe_get_environ (envset, NULL);
+ g_spawn_async_with_pipes (directory, (gchar **)argv, env,
+ G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH,
+ spawn_setup, GINT_TO_POINTER (fds[0]),
+ &pid, NULL, NULL, capture_stderr ? &stderr_fd : NULL, &error);
+
+ if (error)
+ {
+ if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT) ||
+ g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_PERM) ||
+ g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_ACCES))
+ {
+ g_debug ("%s: couldn't run %s: %s", self->name, argv[0], error->message);
+ }
+ else
+ {
+ g_message ("%s: couldn't run %s: %s", self->name, argv[0], error->message);
+ }
+ g_error_free (error);
+ }
+ else
+ {
+ pipe = g_object_new (COCKPIT_TYPE_PIPE,
+ "name", self->name,
+ "in-fd", fds[1],
+ "out-fd", fds[1],
+ "err-fd", stderr_fd,
+ "pid", pid,
+ NULL);
+ fds[1] = -1;
+ }
+ }
+
+ if (!pipe)
+ fail_start_problem (self);
+
+ if (fds[0] >= 0)
+ close (fds[0]);
+ if (fds[1] >= 0)
+ close (fds[1]);
+
+ g_free (envset);
+ g_free (argv);
+ g_strfreev (env);
+
+ return pipe;
+}
+
+/**
+ * cockpit_peer_handle:
+ * @peer: The peer object
+ * @channel: The channel to handle
+ * @options: The parsed "open" message
+ * @data: The raw payload for the "open" message
+ *
+ * Tell the peer bridge to handle this channel.
+ *
+ * Returns: TRUE if handled, FALSE if the peer bridge has closed.
+ */
+gboolean
+cockpit_peer_handle (CockpitPeer *self,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data)
+{
+ const gchar *user = NULL;
+ const gchar *password = NULL;
+ const gchar *host = NULL;
+ const gchar *host_key = NULL;
+ const gchar *superuser = NULL;
+
+ g_return_val_if_fail (COCKPIT_IS_PEER (self), FALSE);
+ g_return_val_if_fail (channel != NULL, FALSE);
+ g_return_val_if_fail (options != NULL, FALSE);
+ g_return_val_if_fail (data != NULL, FALSE);
+
+ if (!self->closed)
+ cockpit_peer_ensure (self);
+
+ if (self->closed)
+ {
+ /* There was an actual problem, close the channel */
+ if (self->problem)
+ {
+ g_debug ("%s: closing channel \"%s\" with \"%s\" because peer closed",
+ self->name, channel, self->problem);
+ reply_channel_closed (self, channel, self->problem);
+ return TRUE;
+ }
+
+ /* We failed to handle channels, let someone else do it */
+ g_debug ("%s: refusing to handle channel \"%s\" because peer closed", self->name, channel);
+ return FALSE;
+ }
+
+ /* If this is the first channel, we can cache data from it */
+ if (!self->first_channel_done)
+ {
+ self->first_channel_done = TRUE;
+
+ if (!self->init_host && cockpit_json_get_string (options, "host", NULL, &host))
+ self->init_host = g_strdup (host);
+
+ /* Setup authorize_values
+ * TODO: Should this be configurable?
+ */
+ if (cockpit_json_get_string (options, "user", NULL, &user) &&
+ cockpit_json_get_string (options, "password", NULL, &password) && password)
+ {
+ const gchar *at_host = self->init_host ? strchr (self->init_host, '@') : NULL;
+ gchar *user_at_host = NULL;
+
+ if (!user && at_host)
+ {
+ user_at_host = g_strndup (self->init_host, at_host - self->init_host);
+ user = user_at_host;
+ }
+
+ if (!user)
+ user = g_getenv ("USER");
+ // $USER is set in bridge main()
+ g_assert (user);
+
+ char *user_hex = cockpit_hex_encode (user, -1);
+ gchar *plain1_challenge = g_strdup_printf ("plain1:%s:", user_hex);
+
+ g_hash_table_insert (self->authorize_values, g_strdup ("basic"),
+ cockpit_authorize_build_basic (user, password));
+ g_hash_table_insert (self->authorize_values, plain1_challenge,
+ g_strdup (password));
+
+ free (user_hex);
+ g_free (user_at_host);
+ }
+
+ if (cockpit_json_get_string (options, "host-key", NULL, &host_key))
+ {
+ g_hash_table_insert (self->authorize_values, g_strdup ("x-host-key"),
+ host_key ? g_strdup_printf ("x-host-key %s", host_key) : g_strdup (""));
+ }
+
+ if (cockpit_json_get_string (options, "init-superuser", NULL, &superuser))
+ {
+ g_free (self->init_superuser);
+ self->init_superuser = g_strdup (superuser);
+ }
+ }
+
+ g_hash_table_add (self->channels, g_strdup (channel));
+
+ if (self->timeout)
+ {
+ g_source_remove (self->timeout);
+ self->timeout = 0;
+ }
+
+ /* If already inited send the message through */
+ if (self->inited)
+ {
+ g_debug ("%s: handling channel \"%s\" on peer", self->name, channel);
+ on_transport_control (self->transport, "open", channel, options, data, self);
+ }
+
+ /* Not yet inited, so freeze this channel and push back into the queue */
+ else
+ {
+ g_debug ("%s: trying to handle channel \"%s\" on peer", self->name, channel);
+ if (!self->frozen)
+ self->frozen = g_queue_new ();
+ g_queue_push_tail (self->frozen, g_strdup (channel));
+ cockpit_transport_freeze (self->transport, channel);
+ cockpit_transport_emit_recv (self->transport, NULL, data);
+ }
+
+ return TRUE;
+}
+
+/**
+ * cockpit_peer_ensure:
+ * @peer: The peer object
+ *
+ * Ensures that the peer is spawned and initialized, if that's not
+ * already the case. If the peer failed this will not restart it and
+ * this function will return NULL.
+ *
+ * Returns: (transfer none): The transport to talk to the peer, or NULL
+ */
+
+CockpitTransport *
+cockpit_peer_ensure (CockpitPeer *self)
+{
+ return cockpit_peer_ensure_with_done (self, NULL, NULL);
+}
+
+CockpitTransport *
+cockpit_peer_ensure_with_done (CockpitPeer *self,
+ CockpitPeerDoneFunction *done_function,
+ gpointer done_data)
+{
+ CockpitPipe *pipe;
+
+ g_return_val_if_fail (COCKPIT_IS_PEER (self), NULL);
+
+ if (!self->other)
+ {
+ self->startup_done_function = done_function;
+ self->startup_done_data = done_data;
+
+ pipe = spawn_process_for_config (self, done_function != NULL);
+ if (!pipe)
+ {
+ self->closed = TRUE;
+ startup_done (self, "spawn failed");
+ return NULL;
+ }
+
+ self->other = cockpit_pipe_transport_new (pipe);
+ g_object_unref (pipe);
+
+ self->other_recv = g_signal_connect (self->other, "recv", G_CALLBACK (on_other_recv), self);
+ self->other_closed = g_signal_connect (self->other, "closed", G_CALLBACK (on_other_closed), self);
+ self->other_control = g_signal_connect (self->other, "control", G_CALLBACK (on_other_control), self);
+ }
+ else
+ {
+ if (done_function)
+ done_function (NULL, NULL, done_data);
+ }
+
+ return self->other;
+}
+
+void
+cockpit_peer_reset (CockpitPeer *self)
+{
+ if (self->timeout)
+ {
+ g_source_remove (self->timeout);
+ self->timeout = 0;
+ }
+
+ if (self->other)
+ cockpit_transport_close (self->other, "terminated");
+ if (self->other)
+ on_other_closed (self->other, "terminated", self);
+ g_assert (self->other == NULL);
+
+ if (self->frozen)
+ g_queue_free_full (self->frozen, g_free);
+ self->frozen = NULL;
+
+ g_hash_table_remove_all (self->channels);
+ g_hash_table_remove_all (self->authorize_values);
+ if (self->authorize_values_timeout)
+ {
+ g_source_remove (self->authorize_values_timeout);
+ self->authorize_values_timeout = 0;
+ }
+
+ g_free (self->startup_auth_cookie);
+ self->startup_auth_cookie = NULL;
+
+ g_free (self->init_superuser);
+ self->init_superuser = NULL;
+
+ if (self->failure)
+ {
+ json_object_unref (self->failure);
+ self->failure = NULL;
+ }
+
+ g_free (self->problem);
+ self->problem = NULL;
+ self->closed = FALSE;
+ self->inited = FALSE;
+ self->first_channel_done = FALSE;
+}
diff --git a/src/bridge/cockpitpeer.h b/src/bridge/cockpitpeer.h
new file mode 100644
index 0000000..80d7ce7
--- /dev/null
+++ b/src/bridge/cockpitpeer.h
@@ -0,0 +1,65 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_PEER_H__
+#define __COCKPIT_PEER_H__
+
+#include "common/cockpittransport.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_PEER (cockpit_peer_get_type ())
+#define COCKPIT_PEER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_PEER, CockpitPeer))
+#define COCKPIT_IS_PEER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), COCKPIT_TYPE_PEER))
+
+typedef struct _CockpitPeer CockpitPeer;
+
+typedef void CockpitPeerDoneFunction (const gchar *error, const gchar *stderr, gpointer data);
+
+typedef struct _CockpitPeerClass {
+ GObjectClass parent_class;
+
+ /* signal */
+
+ void (* closed) (CockpitPeer *peer,
+ const gchar *problem);
+
+} CockpitPeerClass;
+
+GType cockpit_peer_get_type (void) G_GNUC_CONST;
+
+CockpitPeer * cockpit_peer_new (CockpitTransport *transport,
+ JsonObject *config);
+
+CockpitTransport * cockpit_peer_ensure (CockpitPeer *peer);
+
+CockpitTransport * cockpit_peer_ensure_with_done (CockpitPeer *peer,
+ CockpitPeerDoneFunction *done_function,
+ gpointer done_data);
+
+gboolean cockpit_peer_handle (CockpitPeer *peer,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data);
+
+void cockpit_peer_reset (CockpitPeer *peer);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_PEER_H__ */
diff --git a/src/bridge/cockpitpipechannel.c b/src/bridge/cockpitpipechannel.c
new file mode 100644
index 0000000..e9f8d09
--- /dev/null
+++ b/src/bridge/cockpitpipechannel.c
@@ -0,0 +1,635 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpipechannel.h"
+
+#include "cockpitconnect.h"
+
+#include "common/cockpitflow.h"
+#include "common/cockpitpipe.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitunicode.h"
+#include "common/cockpitunixsignal.h"
+
+#include <gio/gunixsocketaddress.h>
+
+#include <sys/wait.h>
+#include <sys/ioctl.h>
+#include <string.h>
+#include <errno.h>
+
+/**
+ * CockpitPipeChannel:
+ *
+ * A #CockpitChannel that sends messages from a regular socket
+ * or file descriptor. Any data is read in whatever chunks it
+ * shows up in read().
+ *
+ * Only UTF8 text data is transmitted. Anything else is
+ * forced into UTF8 by replacing invalid characters.
+ *
+ * The payload type for this channel is 'stream'.
+ */
+
+#define COCKPIT_PIPE_CHANNEL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_PIPE_CHANNEL, CockpitPipeChannel))
+
+typedef struct {
+ CockpitChannel parent;
+ CockpitPipe *pipe;
+ GSocket *sock;
+ gchar *name;
+ gboolean open;
+ gboolean closing;
+ guint sig_read;
+ guint sig_close;
+ gint64 batch;
+ gint64 latency;
+ guint timeout;
+ gboolean pty;
+} CockpitPipeChannel;
+
+typedef struct {
+ CockpitChannelClass parent_class;
+} CockpitPipeChannelClass;
+
+G_DEFINE_TYPE (CockpitPipeChannel, cockpit_pipe_channel, COCKPIT_TYPE_CHANNEL);
+
+GHashTable *internal_fds;
+
+static gboolean
+steal_internal_fd (const gchar *name,
+ gint *fdp)
+{
+ gpointer key;
+ gpointer value;
+
+ if (!internal_fds)
+ return FALSE;
+
+ if (!g_hash_table_lookup_extended (internal_fds, name, &key, &value))
+ return FALSE;
+
+ g_hash_table_steal (internal_fds, key);
+ g_free (key);
+
+ *fdp = GPOINTER_TO_INT (value);
+
+ return TRUE;
+}
+
+static void
+cockpit_pipe_channel_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ CockpitPipeChannel *self = COCKPIT_PIPE_CHANNEL (channel);
+ if (self->open)
+ cockpit_pipe_write (self->pipe, message);
+}
+
+static void
+process_pipe_buffer (CockpitPipeChannel *self,
+ GByteArray *data)
+{
+ CockpitChannel *channel = (CockpitChannel *)self;
+ GBytes *message;
+
+ if (!data && self->pipe)
+ data = cockpit_pipe_get_buffer (self->pipe);
+
+ if (!data)
+ return;
+
+ if (self->timeout)
+ {
+ g_source_remove (self->timeout);
+ self->timeout = 0;
+ }
+
+ if (data->len)
+ {
+ /* When array is reffed, this just clears byte array */
+ g_byte_array_ref (data);
+ message = g_byte_array_free_to_bytes (data);
+ cockpit_channel_send (channel, message, FALSE);
+ g_bytes_unref (message);
+ }
+}
+
+static gboolean
+cockpit_pipe_channel_read_window_size_options (JsonObject *options,
+ gushort default_rows,
+ gushort default_cols,
+ gushort *rowsp,
+ gushort *colsp)
+{
+ gint64 rows, cols;
+ JsonObject *window;
+
+ if (!cockpit_json_get_object (options, "window", NULL, &window))
+ return FALSE;
+
+ if (window == NULL)
+ {
+ *rowsp = default_rows;
+ *colsp = default_cols;
+ return TRUE;
+ }
+
+ if (cockpit_json_get_int (window, "rows", default_rows, &rows) &&
+ cockpit_json_get_int (window, "cols", default_cols, &cols))
+ {
+ *rowsp = (gushort) CLAMP (rows, 0, G_MAXUINT16);
+ *colsp = (gushort) CLAMP (cols, 0, G_MAXUINT16);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+cockpit_pipe_channel_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *message)
+{
+ CockpitPipeChannel *self = COCKPIT_PIPE_CHANNEL (channel);
+ gboolean ret = TRUE;
+
+ /* New set of options for channel */
+ if (g_str_equal (command, "options"))
+ {
+ if (!cockpit_json_get_int (message, "batch", self->batch, &self->batch))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"batch\" option for stream channel");
+ goto out;
+ }
+
+ if (!cockpit_json_get_int (message, "latency", self->latency, &self->latency) ||
+ self->latency < 0 || self->latency >= G_MAXUINT)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"latency\" option for stream channel");
+ goto out;
+ }
+
+ /* ignore size options if this channel is not a pty or we are in prepare() */
+ if (self->pty && self->pipe)
+ {
+ gushort rows, cols;
+
+ if (!cockpit_pipe_channel_read_window_size_options (message, 0, 0, &rows, &cols))
+ {
+ g_warning ("%s: invalid \"window.rows\" or \"window.cols\" option for stream channel", self->name);
+ goto out;
+ }
+
+ if (rows > 0 && cols > 0)
+ {
+ gint fd;
+
+ g_object_get (self->pipe, "in-fd", &fd, NULL);
+ if (fd >= 0)
+ {
+ struct winsize size = { rows, cols, 0, 0 };
+
+ if (ioctl (fd, TIOCSWINSZ, &size) < 0)
+ g_warning ("cannot set terminal size for stream channel: %s", g_strerror (errno));
+ }
+ }
+ }
+
+ process_pipe_buffer (self, NULL);
+ }
+
+ /* Channel input is done */
+ else if (g_str_equal (command, "done"))
+ {
+ self->closing = TRUE;
+ process_pipe_buffer (self, NULL);
+
+ /*
+ * If closed, call base class handler directly. Otherwise ask
+ * our pipe to close first, which will come back here.
+ */
+ if (self->open)
+ cockpit_pipe_close (self->pipe, NULL);
+ }
+
+ else
+ {
+ ret = FALSE;
+ }
+
+out:
+ return ret;
+}
+
+static void
+cockpit_pipe_channel_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ CockpitPipeChannel *self = COCKPIT_PIPE_CHANNEL (channel);
+
+ self->closing = TRUE;
+ process_pipe_buffer (self, NULL);
+
+ /*
+ * If closed, call base class handler directly. Otherwise ask
+ * our pipe to close first, which will come back here.
+ */
+ if (self->open)
+ cockpit_pipe_close (self->pipe, problem);
+ else
+ COCKPIT_CHANNEL_CLASS (cockpit_pipe_channel_parent_class)->close (channel, problem);
+}
+
+static gboolean
+on_batch_timeout (gpointer user_data)
+{
+ CockpitPipeChannel *self = user_data;
+ self->timeout = 0;
+ process_pipe_buffer (self, NULL);
+ return FALSE;
+}
+
+static void
+on_pipe_read (CockpitPipe *pipe,
+ GByteArray *data,
+ gboolean end_of_data,
+ gpointer user_data)
+{
+ CockpitPipeChannel *self = user_data;
+
+ if (!end_of_data && self->batch > 0 && data->len < self->batch)
+ {
+ /* Delay the processing of this data */
+ if (!self->timeout)
+ self->timeout = g_timeout_add (self->latency, on_batch_timeout, self);
+ }
+ else
+ {
+ process_pipe_buffer (self, data);
+ }
+
+ /* Close the pipe when writing is done */
+ if (end_of_data && self->open)
+ {
+ g_debug ("%s: end of data, closing pipe", self->name);
+ cockpit_pipe_close (pipe, NULL);
+ }
+}
+
+static void
+return_stderr_message (CockpitChannel *channel,
+ CockpitPipe *pipe)
+{
+ JsonObject *options;
+ gchar *data;
+
+ data = cockpit_pipe_take_stderr_as_utf8 (pipe);
+ if (data == NULL)
+ return;
+
+ options = cockpit_channel_close_options (channel);
+ json_object_set_string_member (options, "message", data);
+ g_free (data);
+}
+
+static void
+on_pipe_close (CockpitPipe *pipe,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitPipeChannel *self = user_data;
+ CockpitChannel *channel = user_data;
+ JsonObject *options;
+ gint status;
+ gchar *signal;
+
+ process_pipe_buffer (self, NULL);
+
+ self->open = FALSE;
+
+ if (cockpit_pipe_get_pid (pipe, NULL))
+ {
+ options = cockpit_channel_close_options (channel);
+ status = cockpit_pipe_exit_status (pipe);
+ if (WIFEXITED (status))
+ {
+ json_object_set_int_member (options, "exit-status", WEXITSTATUS (status));
+ }
+ else if (WIFSIGNALED (status))
+ {
+ signal = cockpit_strsignal (WTERMSIG (status));
+ json_object_set_string_member (options, "exit-signal", signal);
+ g_free (signal);
+ }
+ else if (status)
+ {
+ json_object_set_int_member (options, "exit-status", -1);
+ }
+ }
+
+ return_stderr_message (channel, pipe);
+
+ /*
+ * In theory we should plumb done handling all the way through to CockpitPipe.
+ * But we can do that later in a compatible way.
+ */
+ if (problem == NULL)
+ cockpit_channel_control (channel, "done", NULL);
+
+ cockpit_channel_close (channel, problem);
+}
+
+static void
+cockpit_pipe_channel_init (CockpitPipeChannel *self)
+{
+ /* Has no effect until batch is set */
+ self->latency = 75;
+}
+
+static gchar **
+parse_environ (CockpitChannel *channel,
+ JsonObject *options,
+ const gchar *directory)
+{
+ const gchar **envset = NULL;
+ gchar **env;
+
+ if (!cockpit_json_get_strv (options, "environ", NULL, &envset))
+ {
+ cockpit_channel_fail (channel, "protocol-error", "invalid \"environ\" option for stream channel");
+ return NULL;
+ }
+
+ env = cockpit_pipe_get_environ ((const gchar **)envset, directory);
+ g_free (envset);
+ return env;
+}
+
+static void
+cockpit_pipe_channel_prepare (CockpitChannel *channel)
+{
+ CockpitPipeChannel *self = COCKPIT_PIPE_CHANNEL (channel);
+ GSocketAddress *address;
+ CockpitPipeFlags flags;
+ JsonObject *options;
+ const gchar **argv = NULL;
+ gchar **env = NULL;
+ const gchar *internal = NULL;
+ const gchar *dir;
+ const gchar *error;
+ gint fd;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_pipe_channel_parent_class)->prepare (channel);
+
+ options = cockpit_channel_get_options (channel);
+
+ if (!cockpit_json_get_strv (options, "spawn", NULL, &argv))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"spawn\" option for stream channel");
+ goto out;
+ }
+ if (!cockpit_json_get_string (options, "internal", NULL, &internal))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"internal\" option for stream channel");
+ goto out;
+ }
+
+ /* Support our options in the open message too */
+ cockpit_pipe_channel_control (channel, "options", options);
+ if (self->closing)
+ goto out;
+
+ if (argv)
+ {
+ if (!cockpit_json_get_string (options, "err", NULL, &error))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"err\" options for stream channel");
+ goto out;
+ }
+
+ flags = COCKPIT_PIPE_FLAGS_NONE;
+ if (g_strcmp0 (error, "out") == 0)
+ flags = COCKPIT_PIPE_STDERR_TO_STDOUT;
+ else if (g_strcmp0 (error, "ignore") == 0)
+ flags = COCKPIT_PIPE_STDERR_TO_NULL;
+ else if (g_strcmp0 (error, "message") == 0)
+ flags = COCKPIT_PIPE_STDERR_TO_MEMORY;
+
+ self->name = g_strdup (argv[0]);
+ if (!self->name)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "\"spawn\" array must not be empty");
+ goto out;
+ }
+ if (!cockpit_json_get_string (options, "directory", NULL, &dir))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"directory\" option for stream channel");
+ goto out;
+ }
+ if (!cockpit_json_get_bool (options, "pty", FALSE, &self->pty))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "invalid \"pty\" option for stream channel");
+ goto out;
+ }
+ env = parse_environ (channel, options, dir);
+ if (!env)
+ goto out;
+ if (self->pty)
+ {
+ gushort rows, cols;
+
+ if (!cockpit_pipe_channel_read_window_size_options (options, 24, 80, &rows, &cols))
+ {
+ g_warning ("%s: invalid \"window.rows\" or \"window.cols\" option for stream channel", self->name);
+ goto out;
+ }
+
+ self->pipe = cockpit_pipe_pty ((const gchar **)argv, (const gchar **)env, dir, rows, cols);
+ }
+ else
+ {
+ self->pipe = cockpit_pipe_spawn ((const gchar **)argv, (const gchar **)env, dir, flags);
+ }
+ }
+ else if (internal)
+ {
+ if (!steal_internal_fd (internal, &fd))
+ {
+ cockpit_channel_close (channel, "not-found");
+ goto out;
+ }
+
+ self->pipe = cockpit_pipe_new_user_fd (internal, fd);
+ }
+ else
+ {
+ address = cockpit_connect_parse_address (channel, &self->name);
+ if (!address)
+ goto out;
+ self->pipe = cockpit_pipe_connect (self->name, address);
+ g_object_unref (address);
+ }
+
+ /* Let the channel throttle the pipe's input flow*/
+ cockpit_flow_throttle (COCKPIT_FLOW (self->pipe), COCKPIT_FLOW (self));
+
+ /* Let the pipe throttle the channel peer's output flow */
+ cockpit_flow_throttle (COCKPIT_FLOW (channel), COCKPIT_FLOW (self->pipe));
+
+ self->sig_read = g_signal_connect (self->pipe, "read", G_CALLBACK (on_pipe_read), self);
+ self->sig_close = g_signal_connect (self->pipe, "close", G_CALLBACK (on_pipe_close), self);
+ self->open = TRUE;
+ cockpit_channel_ready (channel, NULL);
+
+out:
+ g_free (argv);
+ g_strfreev (env);
+}
+
+static void
+cockpit_pipe_channel_dispose (GObject *object)
+{
+ CockpitPipeChannel *self = COCKPIT_PIPE_CHANNEL (object);
+
+ if (self->pipe)
+ {
+ if (self->open)
+ cockpit_pipe_close (self->pipe, "terminated");
+ if (self->sig_read)
+ g_signal_handler_disconnect (self->pipe, self->sig_read);
+ if (self->sig_close)
+ g_signal_handler_disconnect (self->pipe, self->sig_close);
+ self->sig_read = self->sig_close = 0;
+ }
+
+ G_OBJECT_CLASS (cockpit_pipe_channel_parent_class)->dispose (object);
+}
+
+static void
+cockpit_pipe_channel_finalize (GObject *object)
+{
+ CockpitPipeChannel *self = COCKPIT_PIPE_CHANNEL (object);
+
+ g_clear_object (&self->sock);
+ g_clear_object (&self->pipe);
+ g_free (self->name);
+
+ G_OBJECT_CLASS (cockpit_pipe_channel_parent_class)->finalize (object);
+}
+
+static void
+cockpit_pipe_channel_class_init (CockpitPipeChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->dispose = cockpit_pipe_channel_dispose;
+ gobject_class->finalize = cockpit_pipe_channel_finalize;
+
+ channel_class->prepare = cockpit_pipe_channel_prepare;
+ channel_class->control = cockpit_pipe_channel_control;
+ channel_class->recv = cockpit_pipe_channel_recv;
+ channel_class->close = cockpit_pipe_channel_close;
+}
+
+/**
+ * cockpit_pipe_channel_open:
+ * @transport: the transport to send/receive messages on
+ * @channel_id: the channel id
+ * @unix_path: the UNIX socket path to communicate with
+ *
+ * This function is mainly used by tests. The usual way
+ * to get a #CockpitPipeChannel is via cockpit_channel_open()
+ *
+ * Returns: (transfer full): the new channel
+ */
+CockpitChannel *
+cockpit_pipe_channel_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *unix_path)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+
+ g_return_val_if_fail (channel_id != NULL, NULL);
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "unix", unix_path);
+ json_object_set_string_member (options, "payload", "stream");
+
+ channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
+ "transport", transport,
+ "id", channel_id,
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+ return channel;
+}
+
+static void
+internal_fd_free (gpointer data)
+{
+ gint fd = GPOINTER_TO_INT (data);
+
+ close (fd);
+}
+
+const gchar *
+cockpit_pipe_channel_add_internal_fd (gint fd)
+{
+ /* We are not multi-threaded. Also don't make this look like normal fd numbers */
+ static guint64 unique = 911111;
+ gboolean inserted;
+
+ gchar *id;
+
+ if (!internal_fds)
+ internal_fds = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, internal_fd_free);
+
+ id = g_strdup_printf ("internal-stream-%" G_GUINT64_FORMAT, unique++);
+ inserted = g_hash_table_replace (internal_fds, id, GINT_TO_POINTER (fd));
+
+ g_assert (inserted);
+
+ return id;
+}
+
+gboolean
+cockpit_pipe_channel_remove_internal_fd (const gchar *id)
+{
+ if (internal_fds == NULL)
+ return FALSE;
+
+ if (!g_hash_table_remove (internal_fds, id))
+ return FALSE;
+
+ return TRUE;
+}
diff --git a/src/bridge/cockpitpipechannel.h b/src/bridge/cockpitpipechannel.h
new file mode 100644
index 0000000..560594f
--- /dev/null
+++ b/src/bridge/cockpitpipechannel.h
@@ -0,0 +1,41 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_PIPE_CHANNEL_H__
+#define COCKPIT_PIPE_CHANNEL_H__
+
+#include <gio/gio.h>
+
+#include "common/cockpitchannel.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_PIPE_CHANNEL (cockpit_pipe_channel_get_type ())
+
+GType cockpit_pipe_channel_get_type (void) G_GNUC_CONST;
+
+CockpitChannel * cockpit_pipe_channel_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ const gchar *unix_path);
+
+const gchar * cockpit_pipe_channel_add_internal_fd (gint fd);
+
+gboolean cockpit_pipe_channel_remove_internal_fd (const gchar *id);
+
+#endif /* COCKPIT_PIPE_CHANNEL_H__ */
diff --git a/src/bridge/cockpitpolkitagent.c b/src/bridge/cockpitpolkitagent.c
new file mode 100644
index 0000000..956cf62
--- /dev/null
+++ b/src/bridge/cockpitpolkitagent.c
@@ -0,0 +1,477 @@
+/*
+ * Copyright (C) 2008 Red Hat, Inc.
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: David Zeuthen <davidz@redhat.com>
+ * Cockpit Authors
+ */
+
+#include "config.h"
+
+#include "cockpitpolkitagent.h"
+#include "cockpitrouter.h"
+
+#include "common/cockpithex.h"
+#include "common/cockpitjson.h"
+#include "common/cockpittransport.h"
+
+#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE 1
+#include <polkitagent/polkitagent.h>
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <termios.h>
+
+#define COCKPIT_TYPE_POLKIT_AGENT (cockpit_polkit_agent_get_type())
+#define COCKPIT_POLKIT_AGENT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_POLKIT_AGENT, CockpitPolkitAgent))
+#define COCKPIT_IS_POLKIT_AGENT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), COCKPIT_TYPE_POLKIT_AGENT))
+
+typedef struct {
+ PolkitAgentListener parent_instance;
+ CockpitTransport *transport;
+ CockpitRouter *router;
+
+ /* Polkit helper sessions active */
+ GHashTable *callers;
+} CockpitPolkitAgent;
+
+typedef struct {
+ gchar *cookie;
+ gchar *user;
+
+ GTask *result;
+ CockpitPolkitAgent *self;
+
+ PolkitAgentSession *session;
+ gulong completed_sig;
+ gulong request_sig;
+ gulong info_sig;
+ gulong error_sig;
+
+ GCancellable *cancellable;
+ gulong cancel_sig;
+} ReauthorizeCaller;
+
+typedef struct {
+ PolkitAgentListenerClass parent_class;
+} CockpitPolkitAgentClass;
+
+enum {
+ PROP_0,
+ PROP_TRANSPORT,
+ PROP_ROUTER,
+};
+
+static GType cockpit_polkit_agent_get_type (void) G_GNUC_CONST;
+
+G_DEFINE_TYPE (CockpitPolkitAgent, cockpit_polkit_agent, POLKIT_AGENT_TYPE_LISTENER);
+
+static void
+caller_free (gpointer data)
+{
+ ReauthorizeCaller *caller = data;
+ if (caller->cancel_sig)
+ g_signal_handler_disconnect (caller->cancellable, caller->cancel_sig);
+ if (caller->cancellable)
+ g_object_unref (caller->cancellable);
+
+ if (caller->session)
+ {
+ g_signal_handler_disconnect (caller->session, caller->completed_sig);
+ g_signal_handler_disconnect (caller->session, caller->request_sig);
+ g_signal_handler_disconnect (caller->session, caller->info_sig);
+ g_signal_handler_disconnect (caller->session, caller->error_sig);
+ polkit_agent_session_cancel (caller->session);
+ g_object_unref (caller->session);
+ }
+
+ if (caller->result)
+ {
+ g_debug ("cancelling agent authentication");
+ g_task_return_new_error (caller->result, G_IO_ERROR, G_IO_ERROR_CANCELLED,
+ "Operation was cancelled");
+ g_object_unref (caller->result);
+ }
+
+ if (caller->self->router)
+ cockpit_router_prompt_cancel (caller->self->router, caller);
+
+ g_free (caller->cookie);
+ g_free (caller->user);
+ g_free (caller);
+}
+
+static void
+cockpit_polkit_agent_init (CockpitPolkitAgent *self)
+{
+ self->callers = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, caller_free);
+}
+
+static void
+cockpit_polkit_agent_constructed (GObject *object)
+{
+ G_OBJECT_CLASS (cockpit_polkit_agent_parent_class)->constructed (object);
+}
+
+ static void
+cockpit_polkit_agent_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitPolkitAgent *self = COCKPIT_POLKIT_AGENT (object);
+ switch (prop_id)
+ {
+ case PROP_TRANSPORT:
+ self->transport = g_value_dup_object (value);
+ break;
+ case PROP_ROUTER:
+ self->router = g_value_get_object (value);
+ if (self->router)
+ g_object_add_weak_pointer (G_OBJECT(self->router), (gpointer *)&self->router);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_polkit_agent_dispose (GObject *object)
+{
+ CockpitPolkitAgent *self = COCKPIT_POLKIT_AGENT (object);
+
+ g_hash_table_remove_all (self->callers);
+
+ G_OBJECT_CLASS (cockpit_polkit_agent_parent_class)->dispose (object);
+}
+
+static void
+cockpit_polkit_agent_finalize (GObject *object)
+{
+ CockpitPolkitAgent *self = COCKPIT_POLKIT_AGENT (object);
+
+ if (self->transport)
+ g_object_unref (self->transport);
+ if (self->router)
+ g_object_remove_weak_pointer (G_OBJECT(self->router), (gpointer *)&self->router);
+
+ g_hash_table_destroy (self->callers);
+
+ G_OBJECT_CLASS (cockpit_polkit_agent_parent_class)->finalize (object);
+}
+
+static void
+on_completed (PolkitAgentSession *session,
+ gboolean gained_authorization,
+ gpointer user_data)
+{
+ ReauthorizeCaller *caller = user_data;
+
+ g_debug ("polkit authentication completed");
+
+ g_warn_if_fail (g_hash_table_steal (caller->self->callers, caller->cookie));
+ g_task_return_boolean (caller->result, TRUE);
+
+ g_object_unref (caller->result);
+ caller->result = NULL;
+
+ caller_free (caller);
+}
+
+static void
+on_answer (const gchar *value,
+ gpointer user_data)
+{
+ ReauthorizeCaller *caller = user_data;
+ polkit_agent_session_response (caller->session, value ? value : "");
+}
+
+static void
+on_request (PolkitAgentSession *session,
+ const gchar *request,
+ gboolean echo_on,
+ gpointer user_data)
+{
+ ReauthorizeCaller *caller = user_data;
+
+ if (echo_on)
+ {
+ g_message ("ignoring polkit helper request: %s", request);
+ polkit_agent_session_response (session, "");
+ }
+ else if (caller->self->router)
+ cockpit_router_prompt (caller->self->router, caller->user, NULL, NULL, on_answer, caller);
+}
+
+static void
+on_show_error (PolkitAgentSession *session,
+ const gchar *text,
+ gpointer user_data)
+{
+ g_message ("polkit helper error: %s", text);
+}
+
+static void
+on_show_info (PolkitAgentSession *session,
+ const gchar *text,
+ gpointer user_data)
+{
+ g_message ("polkit helper info: %s", text);
+}
+
+static void
+on_cancelled (GCancellable *cancellable,
+ gpointer user_data)
+{
+ ReauthorizeCaller *caller = user_data;
+ g_debug ("cancelled agent authentication");
+ g_hash_table_remove (caller->self->callers, caller->cookie);
+}
+
+static void
+cockpit_polkit_agent_initiate_authentication (PolkitAgentListener *listener,
+ const gchar *action_id,
+ const gchar *message,
+ const gchar *icon_name,
+ PolkitDetails *details,
+ const gchar *cookie,
+ GList *identities,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ CockpitPolkitAgent *self = COCKPIT_POLKIT_AGENT (listener);
+ PolkitIdentity *identity = NULL;
+ GTask *result = NULL;
+ GString *unsupported = NULL;
+ ReauthorizeCaller *caller;
+ const gchar *name = NULL;
+ gchar *string;
+ uid_t uid;
+ GList *l;
+
+ g_debug ("polkit is requesting authentication");
+
+ result = g_task_new (G_OBJECT (self), NULL, callback, user_data);
+
+ if (!g_str_equal (action_id, "org.cockpit-project.cockpit.root-bridge"))
+ {
+ g_task_return_new_error (result, POLKIT_ERROR, POLKIT_ERROR_FAILED,
+ "Unsupported action");
+ goto out;
+ }
+
+ uid = getuid ();
+
+ unsupported = g_string_new ("");
+ for (l = identities; l != NULL; l = g_list_next (l))
+ {
+ if (POLKIT_IS_UNIX_USER (l->data))
+ {
+ if (polkit_unix_user_get_uid (l->data) == uid)
+ {
+ identity = g_object_ref (l->data);
+ name = polkit_unix_user_get_name (l->data);
+ break;
+ }
+ }
+
+ string = polkit_identity_to_string (l->data);
+ g_string_append_printf (unsupported, "%s ", string);
+ g_free (string);
+ }
+
+ if (!identity)
+ {
+ g_message ("cannot reauthorize identity(s): %s", unsupported->str);
+ g_task_return_new_error (result, POLKIT_ERROR, POLKIT_ERROR_FAILED,
+ "Reauthorization not supported for identity");
+ goto out;
+ }
+
+ caller = g_new0 (ReauthorizeCaller, 1);
+ caller->cookie = g_strdup (cookie);
+ caller->user = g_strdup (name);
+ caller->session = polkit_agent_session_new (identity, cookie);
+ caller->completed_sig = g_signal_connect (caller->session, "completed", G_CALLBACK (on_completed), caller);
+ caller->request_sig = g_signal_connect (caller->session, "request", G_CALLBACK (on_request), caller);
+ caller->info_sig = g_signal_connect (caller->session, "show-info", G_CALLBACK (on_show_info), NULL);
+ caller->error_sig = g_signal_connect (caller->session, "show-error", G_CALLBACK (on_show_error), NULL);
+ caller->cancellable = g_object_ref (cancellable);
+ caller->cancel_sig = g_cancellable_connect (cancellable, G_CALLBACK (on_cancelled), caller, NULL);
+
+ caller->result = g_object_ref (result);
+ caller->self = self;
+
+ polkit_agent_session_initiate (caller->session);
+ g_hash_table_replace (self->callers, caller->cookie, caller);
+
+ g_debug ("polkit helper starting");
+
+out:
+ if (unsupported)
+ g_string_free (unsupported, TRUE);
+ g_object_unref (result);
+ if (identity)
+ g_object_unref (identity);
+}
+
+static gboolean
+cockpit_polkit_agent_initiate_authentication_finish (PolkitAgentListener *listener,
+ GAsyncResult *res,
+ GError **error)
+{
+ g_warn_if_fail (g_task_is_valid (res, listener));
+
+ return g_task_propagate_boolean (G_TASK (res), error);
+}
+
+static void
+cockpit_polkit_agent_class_init (CockpitPolkitAgentClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ PolkitAgentListenerClass *listener_class = POLKIT_AGENT_LISTENER_CLASS (klass);
+
+ gobject_class->constructed = cockpit_polkit_agent_constructed;
+ gobject_class->set_property = cockpit_polkit_agent_set_property;
+ gobject_class->dispose = cockpit_polkit_agent_dispose;
+ gobject_class->finalize = cockpit_polkit_agent_finalize;
+
+ listener_class->initiate_authentication = cockpit_polkit_agent_initiate_authentication;
+ listener_class->initiate_authentication_finish = cockpit_polkit_agent_initiate_authentication_finish;
+
+ g_object_class_install_property (gobject_class, PROP_TRANSPORT,
+ g_param_spec_object ("transport", "transport", "transport", COCKPIT_TYPE_TRANSPORT,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class, PROP_ROUTER,
+ g_param_spec_object ("router", "router", "router", COCKPIT_TYPE_ROUTER,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+}
+
+typedef struct {
+ PolkitAgentListener *listener;
+ gpointer registration_handle;
+} CockpitPolkitRegistered;
+
+gpointer
+cockpit_polkit_agent_register (CockpitTransport *transport,
+ CockpitRouter *router,
+ GCancellable *cancellable)
+{
+ CockpitPolkitRegistered *registered;
+ PolkitAgentListener *listener = NULL;
+ PolkitAuthority *authority = NULL;
+ PolkitSubject *subject = NULL;
+ GVariant *options;
+ GError *error = NULL;
+ gpointer handle = NULL;
+ gchar *string;
+
+ g_return_val_if_fail (transport != NULL, NULL);
+
+ authority = polkit_authority_get_sync (cancellable, &error);
+ if (authority == NULL)
+ {
+ g_message ("couldn't get polkit authority: %s", error->message);
+ goto out;
+ }
+
+ subject = polkit_unix_session_new_for_process_sync (getpid (), cancellable, &error);
+ if (subject == NULL)
+ {
+ /*
+ * This can happen if there's a race between the polkit request and closing of
+ * Cockpit. So it's not unheard of. We can complain, but not too loudly.
+ */
+ g_message ("couldn't create polkit session subject: %s", error->message);
+ goto out;
+ }
+
+ listener = g_object_new (COCKPIT_TYPE_POLKIT_AGENT, "transport", transport, "router", router, NULL);
+ options = NULL;
+
+ handle = polkit_agent_listener_register_with_options (listener,
+ POLKIT_AGENT_REGISTER_FLAGS_NONE,
+ subject, NULL, options, cancellable, &error);
+
+ if (error != NULL)
+ {
+ if ((g_error_matches (error, POLKIT_ERROR, POLKIT_ERROR_FAILED) &&
+ error->message && strstr (error->message, "already exists")) ||
+ g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN))
+ {
+ g_debug ("couldn't register polkit agent: %s", error->message);
+ }
+ else
+ {
+ g_dbus_error_strip_remote_error (error);
+ g_message ("couldn't register polkit authentication agent: %s", error->message);
+ }
+ goto out;
+ }
+
+ string = polkit_subject_to_string (subject);
+
+ g_debug ("registered polkit authentication agent for subject: %s", string);
+ g_free (string);
+
+out:
+ if (subject)
+ g_object_unref (subject);
+ if (authority)
+ g_object_unref (authority);
+ g_clear_error (&error);
+
+ if (handle)
+ {
+ registered = g_new0 (CockpitPolkitRegistered, 1);
+ registered->registration_handle = handle;
+ registered->listener = listener;
+ return registered;
+ }
+ else
+ {
+ if (listener)
+ g_object_unref (listener);
+ return NULL;
+ }
+}
+
+void
+cockpit_polkit_agent_unregister (gpointer data)
+{
+ CockpitPolkitRegistered *registered = data;
+
+ if (!registered)
+ return;
+
+ /* Explicitly cancel all pending operations */
+ g_object_run_dispose (G_OBJECT (registered->listener));
+ g_object_unref (registered->listener);
+
+ /* Now unregister with polkit */
+ polkit_agent_listener_unregister (registered->registration_handle);
+
+ g_free (registered);
+}
diff --git a/src/bridge/cockpitpolkitagent.h b/src/bridge/cockpitpolkitagent.h
new file mode 100644
index 0000000..8de6446
--- /dev/null
+++ b/src/bridge/cockpitpolkitagent.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008 Red Hat, Inc.
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: David Zeuthen <davidz@redhat.com>
+ * Cockpit Authors
+ */
+
+#ifndef __COCKPIT_POLKIT_AGENT_H
+#define __COCKPIT_POLKIT_AGENT_H
+
+#include "common/cockpittransport.h"
+#include "cockpitrouter.h"
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+gpointer cockpit_polkit_agent_register (CockpitTransport *transport,
+ CockpitRouter *router,
+ GCancellable *cancellable);
+
+void cockpit_polkit_agent_unregister (gpointer handle);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_POLKIT_AGENT_H */
diff --git a/src/bridge/cockpitrouter.c b/src/bridge/cockpitrouter.c
new file mode 100644
index 0000000..34ecfd1
--- /dev/null
+++ b/src/bridge/cockpitrouter.c
@@ -0,0 +1,1735 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitrouter.h"
+
+#include "cockpitconnect.h"
+#include "cockpitpeer.h"
+#include "cockpitdbusinternal.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpitjson.h"
+#include "common/cockpittransport.h"
+#include "common/cockpitpipe.h"
+#include "common/cockpitpipetransport.h"
+#include "common/cockpittemplate.h"
+#include "common/cockpithex.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+typedef struct {
+ gchar *name;
+ GPatternSpec *glob;
+ JsonNode *node;
+} RouterMatch;
+
+typedef struct {
+ JsonObject *config;
+ RouterMatch *matches;
+ gboolean (* callback) (CockpitRouter *, const gchar *, JsonObject *, GBytes *, gpointer);
+ gpointer user_data;
+ GDestroyNotify destroy;
+} RouterRule;
+
+struct _CockpitRouter {
+ GObjectClass parent;
+
+ gboolean privileged;
+ gchar *init_host;
+ gulong signal_id;
+
+ /* The transport we're talking to */
+ CockpitTransport *transport;
+
+ /* Rules for how to open channels */
+ GList *rules;
+
+ /* All local channels are tracked here, value may be null */
+ GHashTable *channels;
+
+ /* Channel groups */
+ GHashTable *groups;
+ GHashTable *fences;
+ GQueue *fenced;
+
+ /* Superuser */
+ RouterRule *superuser_rule;
+ CockpitTransport *superuser_transport;
+
+ gboolean superuser_dbus_inited;
+ GDBusMethodInvocation *superuser_start_invocation;
+ GDBusMethodInvocation *superuser_stop_invocation;
+
+ gboolean superuser_init_in_progress;
+ gboolean superuser_legacy_init;
+
+ CockpitRouterPromptAnswerFunction *superuser_answer_function;
+ gpointer superuser_answer_data;
+};
+
+typedef struct _CockpitRouterClass {
+ GObjectClass parent_class;
+} CockpitRouterClass;
+
+G_DEFINE_TYPE (CockpitRouter, cockpit_router, G_TYPE_OBJECT);
+
+enum {
+ PROP_0,
+ PROP_TRANSPORT,
+};
+
+static void superuser_init (CockpitRouter *self, JsonObject *options);
+static void superuser_legacy_init (CockpitRouter *self);
+static void superuser_transport_closed (CockpitRouter *self);
+
+typedef struct {
+ JsonObject *config;
+ GHashTable *peers;
+} DynamicPeer;
+
+static DynamicPeer *
+dynamic_peer_create (JsonObject *config)
+{
+ DynamicPeer *p = g_new0 (DynamicPeer, 1);
+
+ p->peers = g_hash_table_new_full (json_object_hash, json_object_equal,
+ (GDestroyNotify) json_object_unref,
+ g_object_unref);
+ p->config = json_object_ref (config);
+
+ return p;
+}
+
+static void
+dynamic_peer_free (gpointer data)
+{
+ DynamicPeer *p = data;
+ json_object_unref (p->config);
+ g_hash_table_unref (p->peers);
+ g_free (p);
+}
+
+static void
+router_rule_compile (RouterRule *rule,
+ JsonObject *object)
+{
+ RouterMatch *match;
+ GList *names, *l;
+ JsonNode *node;
+ gint i;
+
+ g_assert (rule->matches == NULL);
+
+ if (object == NULL)
+ return;
+
+ names = json_object_get_members (object);
+ rule->matches = g_new0 (RouterMatch, g_list_length (names) + 1);
+ for (l = names, i = 0; l != NULL; l = g_list_next (l), i++)
+ {
+ match = &rule->matches[i];
+ match->name = g_strdup (l->data);
+ node = json_object_get_member (object, l->data);
+
+ /* A glob style string pattern */
+ if (JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) == G_TYPE_STRING)
+ match->glob = g_pattern_spec_new (json_node_get_string (node));
+
+ /* A null matches anything */
+ if (!JSON_NODE_HOLDS_NULL (node))
+ match->node = json_node_copy (node);
+ }
+
+ /* The last match has a null name */
+ g_list_free (names);
+}
+
+static gboolean
+router_rule_match (RouterRule *rule,
+ JsonObject *object)
+{
+ RouterMatch *match;
+ const gchar *value;
+ JsonNode *node;
+ guint i;
+
+ if (rule->matches == NULL)
+ return FALSE;
+
+ for (i = 0; rule->matches[i].name != NULL; i++)
+ {
+ match = &rule->matches[i];
+ if (match->glob)
+ {
+ if (!cockpit_json_get_string (object, match->name, NULL, &value) || !value ||
+ !g_pattern_match (match->glob, strlen (value), value, NULL))
+ return FALSE;
+ }
+ else if (match->node)
+ {
+ node = json_object_get_member (object, match->name);
+ if (!node || !cockpit_json_equal (match->node, node))
+ return FALSE;
+ }
+ else
+ {
+ if (!json_object_has_member (object, match->name))
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+static gboolean
+router_rule_invoke (RouterRule *rule,
+ CockpitRouter *self,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data)
+{
+ g_assert (rule->callback != NULL);
+ return (rule->callback) (self, channel, options, data, rule->user_data);
+}
+
+static RouterRule *
+router_rule_find (GList *rules,
+ JsonObject *config)
+{
+ for (GList *l = rules; l; l = g_list_next (l))
+ {
+ RouterRule *rule = l->data;
+ if (rule->config && cockpit_json_equal_object (rule->config, config))
+ return rule;
+ }
+ return NULL;
+}
+
+static void
+router_rule_destroy (RouterRule *rule)
+{
+ gint i;
+
+ if (rule->destroy)
+ (rule->destroy) (rule->user_data);
+ for (i = 0; rule->matches && rule->matches[i].name != NULL; i++)
+ {
+ g_free (rule->matches[i].name);
+ json_node_free (rule->matches[i].node);
+ if (rule->matches[i].glob)
+ g_pattern_spec_free (rule->matches[i].glob);
+ }
+ g_free (rule->matches);
+ if (rule->config)
+ json_object_unref (rule->config);
+ g_free (rule);
+}
+
+static void
+router_rule_dump (RouterRule *rule)
+{
+ RouterMatch *match;
+ gboolean privileged;
+ gchar *text;
+ guint i;
+
+ g_print ("rule:\n");
+ for (i = 0; rule->matches && rule->matches[i].name != NULL; i++)
+ {
+ match = &rule->matches[i];
+ if (match->node)
+ {
+ text = cockpit_json_write (match->node, NULL);
+ g_print (" %s: %s\n", match->name, text);
+ g_free (text);
+ }
+ else if (match->glob)
+ {
+ g_print (" %s: glob\n", match->name);
+ }
+ else
+ {
+ g_print (" %s\n", match->name);
+ }
+ }
+ if (rule->config && cockpit_json_get_bool (rule->config, "privileged", FALSE, &privileged) && privileged)
+ g_print (" privileged\n");
+}
+
+static void
+process_init (CockpitRouter *self,
+ CockpitTransport *transport,
+ JsonObject *options)
+{
+ const gchar *problem = NULL;
+ const gchar *host;
+ gint64 version = -1;
+
+ if (self->init_host)
+ {
+ g_warning ("caller already sent another 'init' message");
+ problem = "protocol-error";
+ }
+ else if (!cockpit_json_get_int (options, "version", -1, &version))
+ {
+ g_warning ("invalid 'version' field in init message");
+ problem = "protocol-error";
+ }
+ else if (version == -1)
+ {
+ g_warning ("missing 'version' field in init message");
+ problem = "protocol-error";
+ }
+ else if (!cockpit_json_get_string (options, "host", NULL, &host))
+ {
+ g_warning ("invalid 'host' field in init message");
+ problem = "protocol-error";
+ }
+ else if (host == NULL)
+ {
+ g_message ("missing 'host' field in init message");
+ problem = "protocol-error";
+ }
+ else if (version != 1)
+ {
+ g_message ("unsupported 'version' of cockpit protocol: %" G_GINT64_FORMAT, version);
+ problem = "not-supported";
+ }
+
+ if (problem)
+ {
+ cockpit_transport_close (transport, problem);
+ }
+ else
+ {
+ g_debug ("received init message");
+ g_assert (host != NULL);
+ self->init_host = g_strdup (host);
+ problem = NULL;
+
+ JsonNode *superuser_options = json_object_get_member (options, "superuser");
+ if (superuser_options)
+ {
+ if (JSON_NODE_HOLDS_OBJECT (superuser_options))
+ superuser_init (self, json_node_get_object (superuser_options));
+ }
+ else
+ superuser_legacy_init (self);
+ }
+}
+
+static void
+on_channel_closed (CockpitChannel *local,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitRouter *self = COCKPIT_ROUTER (user_data);
+ const gchar *channel;
+ GQueue *fenced;
+ GList *l;
+
+ channel = cockpit_channel_get_id (local);
+ g_hash_table_remove (self->channels, channel);
+ g_hash_table_remove (self->groups, channel);
+
+ /*
+ * If this was the last channel in the fence group then resume all other channels
+ * as there's no barrier preventing them from functioning.
+ */
+ if (!g_hash_table_remove (self->fences, channel) || g_hash_table_size (self->fences) != 0)
+ return;
+
+ fenced = self->fenced;
+ self->fenced = NULL;
+
+ if (!fenced)
+ return;
+
+ for (l = fenced->head; l != NULL; l = g_list_next (l))
+ cockpit_transport_thaw (self->transport, l->data);
+ g_queue_free_full (fenced, g_free);
+}
+
+static void
+create_channel (CockpitRouter *self,
+ const gchar *channel,
+ JsonObject *options,
+ GType type)
+{
+ CockpitChannel *local;
+
+ local = g_object_new (type,
+ "transport", self->transport,
+ "id", channel,
+ "options", options,
+ NULL);
+
+ /* This owns the local channel */
+ g_hash_table_replace (self->channels, (gpointer)cockpit_channel_get_id (local), local);
+ g_signal_connect (local, "closed", G_CALLBACK (on_channel_closed), self);
+}
+
+static gboolean
+is_empty (const gchar *s)
+{
+ return !s || s[0] == '\0';
+}
+
+/*
+ * For backwards compatibility we need to normalize some host params
+ * so they can be matched against.
+ *
+ * Some sessions shouldn't be shared by multiple channels, such as those that
+ * explicitly specify a host-key or specific user. This changed over time
+ * so modify things to make it a simple match.
+ *
+ * If the given user is the current user, remove it. Preserves the current
+ * behavior.
+ *
+ */
+static void
+cockpit_router_normalize_host_params (JsonObject *options)
+{
+ const gchar *shareable = NULL;
+ const gchar *user = NULL;
+ gboolean needs_private = FALSE;
+
+ if (!cockpit_json_get_string (options, "session", NULL, &shareable))
+ shareable = NULL;
+
+ if (!cockpit_json_get_string (options, "user", NULL, &user))
+ user = NULL;
+
+ if (!shareable)
+ {
+ /* Fallback to older ways of indicating this */
+ if (user || json_object_has_member (options, "host-key"))
+ needs_private = TRUE;
+
+ if (json_object_has_member (options, "temp-session"))
+ {
+ if (needs_private && !cockpit_json_get_bool (options, "temp-session",
+ TRUE, &needs_private))
+ needs_private = TRUE;
+ json_object_remove_member (options, "temp-session");
+ }
+ }
+
+ if (g_strcmp0 (user, g_get_user_name ()) == 0)
+ json_object_remove_member (options, "user");
+
+ if (needs_private)
+ json_object_set_string_member (options, "session", "private");
+}
+
+static gboolean
+cockpit_router_normalize_host (CockpitRouter *self,
+ JsonObject *options)
+{
+ const gchar *host;
+ gchar *actual_host = NULL;
+ gchar *key = NULL;
+ gchar **parts = NULL;
+
+ if (!cockpit_json_get_string (options, "host", self->init_host, &host))
+ return FALSE;
+
+ parts = g_strsplit (host, "+", 3);
+ if (g_strv_length (parts) == 3 && !is_empty (parts[0]) &&
+ !is_empty (parts[1]) && !is_empty (parts[2]))
+ {
+ key = g_strdup_printf ("host-%s", parts[1]);
+ if (!json_object_has_member (options, key))
+ {
+ json_object_set_string_member (options, key, parts[2]);
+ actual_host = parts[0];
+ }
+ }
+
+ if (!actual_host)
+ actual_host = (gchar *) host;
+
+ if (g_strcmp0 (self->init_host, actual_host) == 0)
+ json_object_remove_member (options, "host");
+ else if (g_strcmp0 (host, actual_host) != 0)
+ json_object_set_string_member (options, "host", actual_host);
+
+ g_strfreev (parts);
+ g_free (key);
+ return TRUE;
+}
+
+static gboolean
+process_open_channel (CockpitRouter *self,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data,
+ gpointer user_data)
+{
+ GType (* type_function) (void) = user_data;
+ GType channel_type = 0;
+ const gchar *group;
+
+ if (!cockpit_json_get_string (options, "group", "default", &group))
+ g_warning ("%s: caller specified invalid 'group' field in open message", channel);
+
+ g_assert (type_function != NULL);
+ channel_type = type_function ();
+
+ if (g_str_equal (group, "fence"))
+ g_hash_table_add (self->fences, g_strdup (channel));
+
+ g_hash_table_insert (self->groups, g_strdup (channel), g_strdup (group));
+
+ create_channel (self, channel, options, channel_type);
+ return TRUE;
+}
+
+static gboolean
+process_open_peer (CockpitRouter *self,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data,
+ gpointer user_data)
+{
+ CockpitPeer *peer = user_data;
+ return cockpit_peer_handle (peer, channel, options, data);
+}
+
+static GBytes *
+substitute_json_string (const gchar *variable,
+ gpointer user_data)
+{
+ const gchar *value;
+ JsonObject *options = user_data;
+ if (options && cockpit_json_get_string (options, variable, "", &value))
+ return g_bytes_new (value, strlen (value));
+ else if (options)
+ g_message ("Couldn't get argument for bridge: got invalid value for '%s'", variable);
+
+ return g_bytes_new ("", 0);
+}
+
+static gboolean
+process_open_dynamic_peer (CockpitRouter *self,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data,
+ gpointer user_data)
+{
+ DynamicPeer *dp = user_data;
+
+ g_autoptr(JsonObject) config = cockpit_template_expand_json (dp->config, "${", "}",
+ substitute_json_string, options);
+
+ CockpitPeer *peer = g_hash_table_lookup (dp->peers, config);
+ if (!peer)
+ {
+ peer = g_object_new (COCKPIT_TYPE_PEER,
+ "transport", self->transport,
+ "router", self,
+ "config", config,
+ NULL);
+
+ g_hash_table_insert (dp->peers, json_object_ref (config), peer);
+ }
+
+ return cockpit_peer_handle (peer, channel, options, data);
+}
+
+static gboolean
+process_open_not_supported (CockpitRouter *self,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data,
+ gpointer user_data)
+{
+ const gchar *payload;
+
+ if (!cockpit_json_get_string (options, "payload", NULL, &payload))
+ g_warning ("%s: caller specified invalid 'payload' field in open message", channel);
+ else if (payload == NULL)
+ g_warning ("%s: caller didn't provide a 'payload' field in open message", channel);
+ else
+ g_debug ("%s: bridge doesn't support channel: %s", channel, payload);
+
+ /* This creates a temporary channel that closes with not-supported */
+ create_channel (self, channel, options, COCKPIT_TYPE_CHANNEL);
+ return TRUE;
+}
+
+static void
+process_open_access_denied (CockpitRouter *self,
+ const gchar *channel)
+{
+ GBytes *control = cockpit_transport_build_control ("command", "close",
+ "channel", channel,
+ "problem", "access-denied",
+ NULL);
+ cockpit_transport_send (self->transport, NULL, control);
+ g_bytes_unref (control);
+}
+
+static void superuser_notify_property (CockpitRouter *self, const gchar *prop);
+
+static gboolean
+process_open_superuser (CockpitRouter *self,
+ CockpitTransport *transport,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data)
+{
+ const gchar *host = NULL;
+ const gchar *superuser_string;
+
+ /* If we are already privileged, let the normal rules handle everything.
+ */
+ if (self->privileged)
+ return FALSE;
+
+ /* Remote superuser is not handled here.
+ */
+ if (cockpit_json_get_string (options, "host", NULL, &host) && host)
+ return FALSE;
+
+ if (!cockpit_json_get_string (options, "superuser", NULL, &superuser_string))
+ {
+ gboolean superuser_boolean;
+ if (!cockpit_json_get_bool (options, "superuser", FALSE, &superuser_boolean))
+ superuser_boolean = FALSE;
+ superuser_string = superuser_boolean ? "require" : NULL;
+ }
+
+ if (superuser_string == NULL)
+ return FALSE;
+
+ if (!g_str_equal (superuser_string, "require") && self->superuser_rule == NULL)
+ return FALSE;
+
+ if (self->superuser_rule == NULL)
+ process_open_access_denied (self, channel);
+ else
+ {
+ GBytes *new_payload = cockpit_json_write_bytes (options);
+ router_rule_invoke (self->superuser_rule, self, channel, options, new_payload);
+ g_bytes_unref (new_payload);
+ }
+
+ return TRUE;
+}
+
+static void
+process_open (CockpitRouter *self,
+ CockpitTransport *transport,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data)
+{
+ GList *l;
+ GBytes *new_payload = NULL;
+
+ if (!channel)
+ {
+ g_warning ("Caller tried to open channel with invalid id");
+ cockpit_transport_close (transport, "protocol-error");
+ }
+
+ /* Check that this isn't a local channel */
+ else if (g_hash_table_lookup (self->channels, channel))
+ {
+ g_warning ("%s: caller tried to reuse a channel that's already in use", channel);
+ cockpit_transport_close (self->transport, "protocol-error");
+ return;
+ }
+
+ /* Request that this channel is frozen, and requeue its open message for later */
+ else if (g_hash_table_size (self->fences) > 0 && !g_hash_table_lookup (self->fences, channel))
+ {
+ if (!self->fenced)
+ self->fenced = g_queue_new ();
+ g_queue_push_tail (self->fenced, g_strdup (channel));
+ cockpit_transport_freeze (self->transport, channel);
+ cockpit_transport_emit_control (self->transport, "open", channel, options, data);
+ }
+
+ else if (!cockpit_router_normalize_host (self, options))
+ {
+ g_warning ("%s: caller specified invalid 'host' field in open message", channel);
+ process_open_not_supported (self, channel, options, data, NULL);
+ }
+
+ else if (process_open_superuser (self, transport, channel, options, data))
+ {
+ /* all done above */
+ }
+
+ /* Now go through the rules */
+ else
+ {
+ cockpit_router_normalize_host_params (options);
+ new_payload = cockpit_json_write_bytes (options);
+ for (l = self->rules; l != NULL; l = g_list_next (l))
+ {
+ if (router_rule_match (l->data, options) &&
+ router_rule_invoke (l->data, self, channel, options, new_payload))
+ {
+ break;
+ }
+ }
+ }
+ if (new_payload)
+ g_bytes_unref (new_payload);
+}
+
+static void
+process_kill (CockpitRouter *self,
+ JsonObject *options)
+{
+ GHashTableIter iter;
+ const gchar *group = NULL;
+ const gchar *host = NULL;
+ GList *list, *l;
+
+ if (!cockpit_json_get_string (options, "group", NULL, &group))
+ {
+ g_warning ("received invalid \"group\" field in kill command");
+ return;
+ }
+ else if (!cockpit_json_get_string (options, "host", NULL, &host))
+ {
+ g_warning ("received invalid \"host\" field in kill command");
+ return;
+ }
+
+ /* Killing on other hosts is handled elsewhere */
+ if (host && g_strcmp0 (host, self->init_host) != 0)
+ return;
+
+ list = NULL;
+ if (group)
+ {
+ gpointer id, channel_group;
+
+ g_hash_table_iter_init (&iter, self->groups);
+ while (g_hash_table_iter_next (&iter, &id, &channel_group))
+ {
+ CockpitChannel *channel;
+
+ if (!g_str_equal (group, channel_group))
+ continue;
+
+ channel = g_hash_table_lookup (self->channels, id);
+ if (channel)
+ list = g_list_prepend (list, g_object_ref (channel));
+ }
+ }
+ else
+ {
+ gpointer id, channel;
+
+ g_hash_table_iter_init (&iter, self->channels);
+ while (g_hash_table_iter_next (&iter, &id, &channel))
+ list = g_list_prepend (list, g_object_ref (channel));
+ }
+
+ for (l = list; l != NULL; l = g_list_next (l))
+ {
+ CockpitChannel *channel = l->data;
+
+ g_debug ("killing channel: %s", cockpit_channel_get_id (channel));
+ cockpit_channel_close (channel, "terminated");
+
+ g_object_unref (channel);
+ }
+
+ g_list_free (list);
+}
+
+static gboolean
+on_transport_control (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel_id,
+ JsonObject *options,
+ GBytes *message,
+ gpointer user_data)
+{
+ CockpitRouter *self = user_data;
+ const gchar *auth_cookie;
+ const gchar *auth_response;
+
+ if (g_str_equal (command, "authorize")
+ && cockpit_json_get_string (options, "cookie", NULL, &auth_cookie)
+ && g_str_equal (auth_cookie, "super1")
+ && cockpit_json_get_string (options, "response", NULL, &auth_response)
+ && self->superuser_answer_function)
+ {
+ self->superuser_answer_function (auth_response, self->superuser_answer_data);
+ self->superuser_answer_function = NULL;
+ self->superuser_answer_data = NULL;
+ return TRUE;
+ }
+
+ if (g_str_equal (command, "init"))
+ {
+ process_init (self, transport, options);
+ return TRUE;
+ }
+
+ if (!self->init_host)
+ {
+ g_warning ("caller did not send 'init' message first");
+ cockpit_transport_close (transport, "protocol-error");
+ return TRUE;
+ }
+
+ if (g_str_equal (command, "open"))
+ {
+ process_open (self, transport, channel_id, options, message);
+ return TRUE;
+ }
+ else if (g_str_equal (command, "kill"))
+ {
+ process_kill (self, options);
+ }
+ else if (g_str_equal (command, "close"))
+ {
+ if (!channel_id)
+ {
+ g_warning ("Caller tried to close channel without an id");
+ cockpit_transport_close (transport, "protocol-error");
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+object_unref_if_not_null (gpointer data)
+{
+ if (data)
+ g_object_unref (data);
+}
+
+static void
+cockpit_router_init (CockpitRouter *self)
+{
+ RouterRule *rule;
+ JsonObject *match = json_object_new ();
+
+ /* Owns the channels */
+ self->channels = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, object_unref_if_not_null);
+ self->groups = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+ self->fences = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ /* The rules, including a default */
+ rule = g_new0 (RouterRule, 1);
+ rule->callback = process_open_not_supported;
+ router_rule_compile (rule, match);
+
+ self->rules = g_list_prepend (self->rules, rule);
+ json_object_unref (match);
+}
+
+static void
+cockpit_router_ban_hosts (CockpitRouter *self)
+{
+ RouterRule *rule;
+ JsonObject *match = json_object_new ();
+
+ json_object_set_null_member (match, "host");
+ rule = g_new0 (RouterRule, 1);
+ rule->callback = process_open_not_supported;
+ router_rule_compile (rule, match);
+
+ self->rules = g_list_prepend (self->rules, rule);
+ json_object_unref (match);
+}
+
+static void
+cockpit_router_dispose (GObject *object)
+{
+ CockpitRouter *self = COCKPIT_ROUTER (object);
+
+ if (self->signal_id)
+ {
+ g_signal_handler_disconnect (self->transport, self->signal_id);
+ self->signal_id = 0;
+ }
+
+ g_hash_table_remove_all (self->channels);
+ g_hash_table_remove_all (self->groups);
+ g_hash_table_remove_all (self->fences);
+
+ g_list_free_full (self->rules, (GDestroyNotify)router_rule_destroy);
+ self->rules = NULL;
+}
+
+static void
+cockpit_router_finalize (GObject *object)
+{
+ CockpitRouter *self = COCKPIT_ROUTER (object);
+
+ if (self->transport)
+ g_object_unref (self->transport);
+
+ if (self->fenced)
+ g_queue_free_full (self->fenced, g_free);
+
+ g_free (self->init_host);
+ g_hash_table_destroy (self->channels);
+ g_hash_table_destroy (self->groups);
+ g_hash_table_destroy (self->fences);
+
+ G_OBJECT_CLASS (cockpit_router_parent_class)->finalize (object);
+}
+
+static void
+cockpit_router_set_property (GObject *obj,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitRouter *self = COCKPIT_ROUTER (obj);
+
+ switch (prop_id)
+ {
+ case PROP_TRANSPORT:
+ self->transport = g_value_dup_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_router_constructed (GObject *object)
+{
+ CockpitRouter *self = COCKPIT_ROUTER (object);
+
+ G_OBJECT_CLASS (cockpit_router_parent_class)->constructed (object);
+
+ g_return_if_fail (self->transport != NULL);
+
+ self->signal_id = g_signal_connect (self->transport, "control",
+ G_CALLBACK (on_transport_control),
+ self);
+ self->privileged = (geteuid() == 0);
+}
+
+static void
+cockpit_router_class_init (CockpitRouterClass *class)
+{
+ GObjectClass *object_class;
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->set_property = cockpit_router_set_property;
+ object_class->finalize = cockpit_router_finalize;
+ object_class->dispose = cockpit_router_dispose;
+ object_class->constructed = cockpit_router_constructed;
+
+ g_object_class_install_property (object_class, PROP_TRANSPORT,
+ g_param_spec_object ("transport", "transport", "transport",
+ COCKPIT_TYPE_TRANSPORT,
+ G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+}
+
+/**
+ * cockpit_router_new:
+ * @transport: Transport to talk to cockpit-ws with
+ * @payloads: List of payloads to handle, or NULL
+ * @bridges: List of peer bridge config, or NULL
+ *
+ * Create a new CockpitRouter. The @payloads to handle
+ * will be added via cockpit_router_add_channel().
+ *
+ * The @bridges if specified will be added via
+ * cockpit_router_add_bridge(). These will be added in
+ * reverse order so that the first bridge in the list
+ * would be the first one that matches in the router.
+ *
+ * Returns: (transfer full): A new CockpitRouter object.
+ */
+CockpitRouter *
+cockpit_router_new (CockpitTransport *transport,
+ CockpitPayloadType *payloads,
+ GList *bridges)
+{
+ CockpitRouter *router;
+ guint i;
+ JsonObject *match;
+
+ g_return_val_if_fail (transport != NULL, NULL);
+
+ router = g_object_new (COCKPIT_TYPE_ROUTER,
+ "transport", transport,
+ NULL);
+
+ for (i = 0; payloads && payloads[i].name != NULL; i++)
+ {
+ match = json_object_new ();
+ json_object_set_string_member (match, "payload", payloads[i].name);
+ cockpit_router_add_channel (router, match, payloads[i].function);
+ json_object_unref (match);
+ }
+
+ /* No hosts are allowed by default */
+ cockpit_router_ban_hosts (router);
+
+ cockpit_router_set_bridges (router, bridges);
+
+ // cockpit_router_dump_rules (router);
+
+ return router;
+}
+
+/**
+ * cockpit_router_add_channel:
+ * @self: The router object
+ * @match: JSON configuration on what to match
+ * @function: The function to get type from
+ *
+ * Add a channel handler to the router. The @match is a
+ * JSON match object as described in doc/guide/ which matches
+ * against "open" messages in order to determine whether to
+ * use this channel.
+ *
+ * The @function returns a GType to use for the channel.
+ */
+void
+cockpit_router_add_channel (CockpitRouter *self,
+ JsonObject *match,
+ GType (* function) (void))
+{
+ RouterRule *rule;
+
+ g_return_if_fail (COCKPIT_IS_ROUTER (self));
+ g_return_if_fail (match != NULL);
+ g_return_if_fail (function != NULL);
+
+ rule = g_new0 (RouterRule, 1);
+ rule->callback = process_open_channel;
+ rule->user_data = function;
+ router_rule_compile (rule, match);
+ self->rules = g_list_prepend (self->rules, rule);
+}
+
+/**
+ * cockpit_router_add_peer:
+ * @self: The router object
+ * @match: JSON configuration on what to match
+ * @peer: The CockpitPeer instance to route matches to
+ *
+ * Add a peer bridge to the router for handling channels.
+ * The @match JSON object as described in doc/guide/ and
+ * matches against "open" messages in order to determine whether
+ * to send channels to this peer bridge.
+ */
+void
+cockpit_router_add_peer (CockpitRouter *self,
+ JsonObject *match,
+ CockpitPeer *peer)
+{
+ RouterRule *rule;
+
+ g_return_if_fail (COCKPIT_IS_ROUTER (self));
+ g_return_if_fail (COCKPIT_IS_PEER (peer));
+ g_return_if_fail (match != NULL);
+
+ rule = g_new0 (RouterRule, 1);
+ rule->callback = process_open_peer;
+ rule->user_data = g_object_ref (peer);
+ rule->destroy = g_object_unref;
+ router_rule_compile (rule, match);
+
+ self->rules = g_list_prepend (self->rules, rule);
+}
+
+void
+cockpit_router_add_bridge (CockpitRouter *self,
+ JsonObject *config)
+{
+ RouterRule *rule;
+ JsonObject *match;
+ GList *output = NULL;
+ GBytes *bytes = NULL;
+
+ g_return_if_fail (COCKPIT_IS_ROUTER (self));
+ g_return_if_fail (config && json_object_is_immutable (config));
+
+ /* Actual descriptive warning displayed elsewhere */
+ if (!cockpit_json_get_object (config, "match", NULL, &match))
+ match = NULL;
+
+ /* See if we have any variables in the JSON */
+ bytes = cockpit_json_write_bytes (config);
+ output = cockpit_template_expand (bytes, "${", "}", substitute_json_string, NULL);
+ rule = g_new0 (RouterRule, 1);
+ rule->config = json_object_ref (config);
+
+ if (!output->next)
+ {
+ rule->callback = process_open_peer;
+ rule->user_data = g_object_new (COCKPIT_TYPE_PEER,
+ "transport", self->transport,
+ "router", self,
+ "config", config,
+ NULL);
+ rule->destroy = g_object_unref;
+ }
+ else
+ {
+ gboolean privileged;
+
+ if (cockpit_json_get_bool (rule->config, "privileged", FALSE, &privileged)
+ && privileged)
+ {
+ g_warning ("privileged bridges can't be dynamic");
+ json_object_unref (rule->config);
+ g_free (rule);
+ goto out;
+ }
+
+ rule->callback = process_open_dynamic_peer;
+ rule->user_data = dynamic_peer_create (config);
+ rule->destroy = dynamic_peer_free;
+ }
+
+ router_rule_compile (rule, match);
+ self->rules = g_list_prepend (self->rules, rule);
+
+ out:
+ g_bytes_unref (bytes);
+ g_list_free_full (output, (GDestroyNotify) g_bytes_unref);
+}
+
+/**
+ * cockpit_router_set_bridges:
+ * @self: The router object
+ * @configs: JSON configurations for the bridges
+ *
+ * Updates the rules for bridges to match @configs. All rules that
+ * have been previously added via @cockpit_router_add_bridge and
+ * @cockpit_router_set_bridges conceptually removed, and all rules
+ * specified by @configs are added as with @cockpit_add_bridge.
+ *
+ * External peers for rules that have not changed will be left
+ * running. Peers for rules that have disappeared will be terminated.
+ */
+void cockpit_router_set_bridges (CockpitRouter *self,
+ GList *bridges)
+{
+ GList *l;
+ JsonObject *config;
+ RouterRule *rule;
+ GList *old_rules;
+
+ /* Enumerated in reverse, since the last rule is matched first */
+
+ old_rules = self->rules;
+ self->rules = NULL;
+ for (l = g_list_last (bridges); l != NULL; l = g_list_previous (l))
+ {
+ config = l->data;
+
+ rule = router_rule_find (old_rules, config);
+ if (rule)
+ {
+ old_rules = g_list_remove (old_rules, rule);
+ self->rules = g_list_prepend (self->rules, rule);
+ }
+ else
+ {
+ cockpit_router_add_bridge (self, config);
+ }
+ }
+
+ for (l = old_rules; l; l = g_list_next (l))
+ {
+ rule = l->data;
+ if (rule->config)
+ {
+ if (rule == self->superuser_rule)
+ superuser_transport_closed (self);
+
+ router_rule_destroy (rule);
+ }
+ else
+ {
+ self->rules = g_list_append (self->rules, rule);
+ }
+ }
+ g_list_free (old_rules);
+}
+
+void
+cockpit_router_dump_rules (CockpitRouter *self)
+{
+ GList *l;
+ for (l = self->rules; l != NULL; l = g_list_next (l))
+ router_rule_dump (l->data);
+}
+
+/* Superuser rules */
+
+static gchar *
+rule_superuser_id (RouterRule *rule)
+{
+ gboolean privileged;
+
+ if (rule->config
+ && cockpit_json_get_bool (rule->config, "privileged", FALSE, &privileged)
+ && privileged) {
+ /* if bridge has a label, prefer that */
+ const gchar *label;
+ if (cockpit_json_get_string (rule->config, "label", NULL, &label) && label)
+ return g_strdup (label);
+
+ /* else, use program name */
+ g_autofree const gchar **spawn = NULL;
+ if (cockpit_json_get_strv (rule->config, "spawn", NULL, &spawn) && spawn)
+ return g_path_get_basename (spawn[0]);
+ }
+
+ return NULL;
+}
+
+/* D-Bus interface */
+
+static void
+superuser_notify_property (CockpitRouter *self, const gchar *prop)
+{
+ if (!self->superuser_dbus_inited)
+ return;
+
+ GDBusConnection *connection = cockpit_dbus_internal_server ();
+ GVariant *signal_value;
+ GVariantBuilder builder;
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
+ g_variant_builder_add (&builder, "s", prop);
+ signal_value = g_variant_ref_sink (g_variant_new ("(sa{sv}as)", "cockpit.Superuser", NULL, &builder));
+
+ g_dbus_connection_emit_signal (connection,
+ NULL,
+ "/superuser",
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ signal_value,
+ NULL);
+
+ g_variant_unref (signal_value);
+}
+
+static void
+superuser_start_done (const gchar *error, const gchar *stderr, gpointer user_data)
+{
+ CockpitRouter *router = user_data;
+
+ if (error)
+ {
+ const gchar *message;
+ if (g_strcmp0 (error, "cancelled") == 0 || stderr == NULL || *stderr == '\0')
+ message = error;
+ else
+ message = stderr;
+
+ router->superuser_rule = NULL;
+ g_dbus_method_invocation_return_error (router->superuser_start_invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "%s", message);
+ }
+ else
+ g_dbus_method_invocation_return_value (router->superuser_start_invocation, NULL);
+
+ router->superuser_answer_function = NULL;
+ router->superuser_answer_data = NULL;
+ router->superuser_start_invocation = NULL;
+ superuser_notify_property (router, "Current");
+ g_object_unref (router);
+}
+
+static void
+superuser_transport_closed (CockpitRouter *self)
+{
+ if (self->superuser_stop_invocation)
+ g_dbus_method_invocation_return_value (self->superuser_stop_invocation, NULL);
+ self->superuser_stop_invocation = NULL;
+ self->superuser_rule = NULL;
+ self->superuser_transport = NULL;
+ superuser_notify_property (self, "Current");
+}
+
+static void
+on_superuser_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitRouter *router = user_data;
+
+ if (router->superuser_transport == transport)
+ superuser_transport_closed (router);
+}
+
+static void
+superuser_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ CockpitRouter *router = user_data;
+
+ if (g_str_equal (method_name, "Start"))
+ {
+ const gchar *id;
+
+ if (router->superuser_rule)
+ {
+ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "Already started a superuser bridge");
+ return;
+ }
+
+ g_variant_get (parameters, "(&s)", &id);
+
+ for (GList *l = router->rules; l; l = g_list_next (l))
+ {
+ g_autofree gchar *rule_id = rule_superuser_id (l->data);
+ if (rule_id)
+ {
+ if (g_str_equal (id, rule_id))
+ {
+ router->superuser_start_invocation = invocation;
+ router->superuser_rule = l->data;
+ cockpit_peer_reset (router->superuser_rule->user_data);
+ router->superuser_transport = cockpit_peer_ensure_with_done (router->superuser_rule->user_data,
+ superuser_start_done,
+ g_object_ref (router));
+ if (router->superuser_transport)
+ g_signal_connect (router->superuser_transport, "closed",
+ G_CALLBACK (on_superuser_transport_closed), router);
+ return;
+ }
+ }
+ }
+
+ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED,
+ "No such superuser bridge");
+ }
+ else if (g_str_equal (method_name, "Stop"))
+ {
+ if (router->superuser_rule == NULL)
+ {
+ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "No superuser bridge running");
+ return;
+ }
+
+ router->superuser_stop_invocation = invocation;
+ cockpit_transport_close (router->superuser_transport,
+ router->superuser_start_invocation ? "cancelled" : "terminated");
+ }
+ else if (g_str_equal (method_name, "Answer"))
+ {
+ const gchar *value;
+
+ if (router->superuser_start_invocation == NULL)
+ {
+ g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "No bridge startup going on");
+ return;
+ }
+
+ g_variant_get (parameters, "(&s)", &value);
+ if (router->superuser_answer_function)
+ {
+ router->superuser_answer_function (value, router->superuser_answer_data);
+ router->superuser_answer_function = NULL;
+ router->superuser_answer_data = NULL;
+ }
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ }
+ else
+ g_return_if_reached ();
+}
+
+static GVariant *
+superuser_get_property (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *property_name,
+ GError **error,
+ gpointer user_data)
+{
+ CockpitRouter *router = user_data;
+
+ g_return_val_if_fail (property_name != NULL, NULL);
+
+ if (g_str_equal (property_name, "Bridges"))
+ {
+ GVariantBuilder bob;
+
+ g_variant_builder_init (&bob, G_VARIANT_TYPE("as"));
+ for (GList *l = router->rules; l; l = g_list_next (l))
+ {
+ gchar *id = rule_superuser_id (l->data);
+ if (id)
+ {
+ g_variant_builder_add (&bob, "s", id);
+ g_free (id);
+ }
+ }
+
+ return g_variant_new ("as", &bob);
+ }
+ if (g_str_equal (property_name, "Methods"))
+ {
+ GVariantBuilder bob;
+ GVariant *value;
+
+ g_variant_builder_init (&bob, G_VARIANT_TYPE("a{sv}"));
+ for (GList *l = router->rules; l; l = g_list_next (l))
+ {
+ RouterRule *rule = l->data;
+ gchar *id = rule_superuser_id (rule);
+ if (id)
+ {
+ GVariantBuilder config_builder;
+ const gchar *label;
+ if (cockpit_json_get_string (rule->config, "label", NULL, &label) && label) {
+ g_variant_builder_init (&config_builder, G_VARIANT_TYPE("a{sv}"));
+ g_variant_builder_add (&config_builder, "{sv}", "label", g_variant_new_string (label));
+ g_variant_builder_add (&bob, "{sv}", id, g_variant_builder_end (&config_builder));
+ }
+ g_free (id);
+ }
+ }
+
+ value = g_variant_builder_end (&bob);
+ if (g_variant_n_children (value) > 0)
+ return value;
+ else
+ {
+ g_variant_unref (value);
+ return NULL;
+ }
+ }
+ else if (g_str_equal (property_name, "Current"))
+ {
+ /* The Current property is either the "superuser id" of the
+ * current superuser rule, or one of the special values "none",
+ * "init", or "root", with the following meaning:
+ *
+ * "none": No superuser bridge running.
+ *
+ * "init": Bridge is still initializing and in the process of
+ * starting up a superuser bridge.
+ *
+ * "root": The whole session is running as root and there is no
+ * separate superuser bridge.
+ *
+ * The first value after the bridge starts is always one of
+ * "none", "init", or "root". "init" will change later on to a
+ * concrete superuser id, or to "none" when starting the
+ * superuser bridge has failed.
+ *
+ * When calling the Stop method, the value will change to "none"
+ * at some point before the method call finishes (or remain
+ * unchanged when the method call fails).
+ *
+ * When calling the Start method, the value will change to the
+ * concrete superuser id once the superuser bridge is running.
+ * During the whole startup it will remain "none", and will only
+ * change when the startup was successful.
+ *
+ * The motivation for having the special "init" value is to give
+ * pages enough information to manage their automatic reloading.
+ * They want to reload when a superuser bridge is actually
+ * started or stopped, but not when the startup during
+ * initialization fails.
+ */
+
+ if (router->privileged)
+ return g_variant_new ("s", "root");
+ else if (router->superuser_init_in_progress)
+ return g_variant_new ("s", "init");
+ else if (router->superuser_rule == NULL
+ || router->superuser_start_invocation)
+ return g_variant_new ("s", "none");
+ else
+ return g_variant_new_take_string (rule_superuser_id (router->superuser_rule));
+ }
+ else
+ g_return_val_if_reached (NULL);
+}
+
+static GDBusInterfaceVTable superuser_vtable = {
+ .method_call = superuser_method_call,
+ .get_property = superuser_get_property,
+};
+
+static GDBusArgInfo superuser_start_id_arg = {
+ -1, "id", "s", NULL
+};
+
+static GDBusArgInfo *superuser_start_args[] = {
+ &superuser_start_id_arg,
+ NULL
+};
+
+static GDBusMethodInfo superuser_start_method = {
+ -1, "Start", superuser_start_args, NULL, NULL
+};
+
+static GDBusMethodInfo superuser_stop_method = {
+ -1, "Stop", NULL, NULL, NULL
+};
+
+static GDBusArgInfo superuser_answer_value_arg = {
+ -1, "value", "s", NULL
+};
+
+static GDBusArgInfo *superuser_answer_args[] = {
+ &superuser_answer_value_arg,
+ NULL
+};
+
+static GDBusMethodInfo superuser_answer_method = {
+ -1, "Answer", superuser_answer_args, NULL, NULL
+};
+
+static GDBusMethodInfo *superuser_methods[] = {
+ &superuser_start_method,
+ &superuser_stop_method,
+ &superuser_answer_method,
+ NULL
+};
+
+static GDBusPropertyInfo superuser_bridges_property = {
+ -1, "Bridges", "as", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo superuser_methods_property = {
+ -1, "Methods", "a{sv}", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo superuser_current_property = {
+ -1, "Current", "s", G_DBUS_PROPERTY_INFO_FLAGS_READABLE, NULL
+};
+
+static GDBusPropertyInfo *superuser_properties[] = {
+ &superuser_bridges_property,
+ &superuser_methods_property,
+ &superuser_current_property,
+ NULL
+};
+
+static GDBusInterfaceInfo superuser_interface = {
+ -1, "cockpit.Superuser",
+ superuser_methods,
+ NULL, /* signals */
+ superuser_properties,
+ NULL /* annotations */
+};
+
+void
+cockpit_router_dbus_startup (CockpitRouter *router)
+{
+ GDBusConnection *connection;
+ GError *error = NULL;
+
+ connection = cockpit_dbus_internal_server ();
+ g_return_if_fail (connection != NULL);
+
+ g_dbus_connection_register_object (connection, "/superuser", &superuser_interface,
+ &superuser_vtable, router, NULL, &error);
+
+ g_object_unref (connection);
+
+ router->superuser_dbus_inited = TRUE;
+
+ if (error != NULL)
+ {
+ g_critical ("couldn't register DBus cockpit.Superuser object: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+}
+
+/* Superuser init */
+
+static void
+superuser_init_done (const gchar *error, const gchar *stderr, gpointer user_data)
+{
+ CockpitRouter *router = user_data;
+
+ if (error)
+ router->superuser_rule = NULL;
+
+ router->superuser_init_in_progress = FALSE;
+ superuser_notify_property (router, "Current");
+
+ if (!router->superuser_legacy_init)
+ {
+ GBytes *request = cockpit_transport_build_control ("command", "superuser-init-done",
+ NULL);
+ cockpit_transport_send (router->transport, NULL, request);
+ g_bytes_unref (request);
+ }
+
+ g_object_unref (router);
+}
+
+static void
+superuser_init_start (CockpitRouter *router,
+ const gchar *id)
+{
+ router->superuser_init_in_progress = TRUE;
+
+ if (!router->privileged)
+ {
+ for (GList *l = router->rules; l; l = g_list_next (l))
+ {
+ g_autofree gchar *rule_id = rule_superuser_id (l->data);
+ if (rule_id && (id == NULL || g_str_equal (id, rule_id)))
+ {
+ router->superuser_rule = l->data;
+ cockpit_peer_reset (router->superuser_rule->user_data);
+ router->superuser_transport = cockpit_peer_ensure_with_done (router->superuser_rule->user_data,
+ superuser_init_done,
+ g_object_ref (router));
+ g_signal_connect (router->superuser_transport, "closed",
+ G_CALLBACK (on_superuser_transport_closed), router);
+ return;
+ }
+ }
+
+ if (id)
+ g_warning ("No such superuser bridge: %s", id);
+ }
+
+ superuser_init_done (NULL, NULL, g_object_ref (router));
+}
+
+static void
+superuser_init (CockpitRouter *router,
+ JsonObject *options)
+{
+ const gchar *id;
+
+ if (!cockpit_json_get_string (options, "id", NULL, &id)
+ || id == NULL)
+ {
+ g_warning ("invalid superuser options in \"init\" message");
+ superuser_init_done (NULL, NULL, g_object_ref (router));
+ return;
+ }
+
+ if (g_str_equal (id, "any"))
+ id = NULL;
+
+ superuser_init_start (router, id);
+}
+
+static void
+superuser_legacy_init (CockpitRouter *router)
+{
+ router->superuser_legacy_init = TRUE;
+ superuser_init_start (router, NULL);
+}
+
+/* Prompting
+ */
+
+void
+cockpit_router_prompt (CockpitRouter *self,
+ const gchar *user,
+ const gchar *prompt,
+ const gchar *previous_error,
+ CockpitRouterPromptAnswerFunction *answer,
+ gpointer data)
+{
+ if (prompt == NULL)
+ prompt = "";
+
+ if (previous_error == NULL)
+ previous_error = "";
+
+ if (self->superuser_answer_function)
+ {
+ g_warning ("Overlapping prompts");
+ answer (NULL, data);
+ return;
+ }
+
+ if (self->superuser_start_invocation)
+ {
+ self->superuser_answer_function = answer;
+ self->superuser_answer_data = data;
+ g_dbus_connection_emit_signal (cockpit_dbus_internal_server (),
+ NULL,
+ "/superuser",
+ "cockpit.Superuser",
+ "Prompt",
+ g_variant_new ("(sssbs)", "", prompt, "", FALSE, previous_error),
+ NULL);
+ }
+ else if (self->superuser_init_in_progress)
+ {
+ self->superuser_answer_function = answer;
+ self->superuser_answer_data = data;
+
+ char *user_hex = cockpit_hex_encode (user, -1);
+ gchar *challenge = g_strdup_printf ("plain1:%s:", user_hex);
+ GBytes *request = cockpit_transport_build_control ("command", "authorize",
+ "challenge", challenge,
+ "cookie", "super1",
+ NULL);
+ cockpit_transport_send (self->transport, NULL, request);
+ g_bytes_unref (request);
+ g_free (challenge);
+ free (user_hex);
+ }
+ else
+ {
+ g_warning ("Out of context prompt");
+ answer (NULL, data);
+ }
+}
+
+void
+cockpit_router_prompt_cancel (CockpitRouter *self,
+ gpointer data)
+{
+ if (self->superuser_answer_data == data)
+ {
+ self->superuser_answer_function = NULL;
+ self->superuser_answer_data = NULL;
+ }
+}
diff --git a/src/bridge/cockpitrouter.h b/src/bridge/cockpitrouter.h
new file mode 100644
index 0000000..97dd5f7
--- /dev/null
+++ b/src/bridge/cockpitrouter.h
@@ -0,0 +1,77 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_ROUTER_H__
+#define __COCKPIT_ROUTER_H__
+
+#include "common/cockpittransport.h"
+#include "cockpitpeer.h"
+
+G_BEGIN_DECLS
+
+typedef struct {
+ const gchar *name;
+ GType (* function) (void);
+} CockpitPayloadType;
+
+#define COCKPIT_TYPE_ROUTER (cockpit_router_get_type ())
+#define COCKPIT_ROUTER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_ROUTER, CockpitRouter))
+#define COCKPIT_IS_ROUTER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), COCKPIT_TYPE_ROUTER))
+
+typedef struct _CockpitRouter CockpitRouter;
+
+typedef void CockpitRouterPromptAnswerFunction (const gchar *value,
+ gpointer data);
+
+GType cockpit_router_get_type (void) G_GNUC_CONST;
+
+CockpitRouter * cockpit_router_new (CockpitTransport *transport,
+ CockpitPayloadType *payloads,
+ GList *bridges);
+
+void cockpit_router_add_channel (CockpitRouter *self,
+ JsonObject *match,
+ GType (* function) (void));
+
+void cockpit_router_add_bridge (CockpitRouter *self,
+ JsonObject *config);
+
+void cockpit_router_add_peer (CockpitRouter *self,
+ JsonObject *match,
+ CockpitPeer *peer);
+void cockpit_router_set_bridges (CockpitRouter *self,
+ GList *bridge_configs);
+
+void cockpit_router_dump_rules (CockpitRouter *self);
+
+void cockpit_router_dbus_startup (CockpitRouter *self);
+
+void cockpit_router_prompt (CockpitRouter *self,
+ const gchar *user,
+ const gchar *prompt,
+ const gchar *previous_error,
+ CockpitRouterPromptAnswerFunction *answer,
+ gpointer data);
+
+void cockpit_router_prompt_cancel (CockpitRouter *self,
+ gpointer data);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_ROUTER_H__ */
diff --git a/src/bridge/cockpitsamples.c b/src/bridge/cockpitsamples.c
new file mode 100644
index 0000000..0280b15
--- /dev/null
+++ b/src/bridge/cockpitsamples.c
@@ -0,0 +1,45 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitsamples.h"
+
+G_DEFINE_INTERFACE (CockpitSamples, cockpit_samples, 0);
+
+static void
+cockpit_samples_default_init (CockpitSamplesInterface *iface)
+{
+
+}
+
+void
+cockpit_samples_sample (CockpitSamples *self,
+ const gchar *metric,
+ const gchar *instance,
+ gint64 value)
+{
+ CockpitSamplesInterface *iface;
+
+ iface = COCKPIT_SAMPLES_GET_IFACE (self);
+ g_return_if_fail (iface != NULL);
+
+ g_assert (iface->sample);
+ (iface->sample) (self, metric, instance, value);
+}
diff --git a/src/bridge/cockpitsamples.h b/src/bridge/cockpitsamples.h
new file mode 100644
index 0000000..6c30407
--- /dev/null
+++ b/src/bridge/cockpitsamples.h
@@ -0,0 +1,53 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_SAMPLES_H__
+#define COCKPIT_SAMPLES_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_SAMPLES (cockpit_samples_get_type ())
+#define COCKPIT_SAMPLES(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), COCKPIT_TYPE_SAMPLES, CockpitSamples))
+#define COCKPIT_IS_SAMPLES(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), COCKPIT_TYPE_SAMPLES))
+#define COCKPIT_SAMPLES_GET_IFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), COCKPIT_TYPE_SAMPLES, CockpitSamplesInterface))
+
+typedef struct _CockpitSamples CockpitSamples;
+typedef struct _CockpitSamplesInterface CockpitSamplesInterface;
+
+struct _CockpitSamplesInterface {
+ GTypeInterface parent_iface;
+
+ void (* sample) (CockpitSamples *samples,
+ const gchar *metric,
+ const gchar *instance,
+ gint64 value);
+};
+
+GType cockpit_samples_get_type (void) G_GNUC_CONST;
+
+void cockpit_samples_sample (CockpitSamples *self,
+ const gchar *metric,
+ const gchar *instance,
+ gint64 value);
+
+G_END_DECLS
+
+#endif /* COCKPIT_SAMPLES_H__ */
diff --git a/src/bridge/cockpitstream.c b/src/bridge/cockpitstream.c
new file mode 100644
index 0000000..ea6634e
--- /dev/null
+++ b/src/bridge/cockpitstream.c
@@ -0,0 +1,950 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitstream.h"
+
+#include "common/cockpitflow.h"
+#include "common/cockpitjson.h"
+
+#include <errno.h>
+#include <string.h>
+
+/**
+ * CockpitStream:
+ *
+ * A stream with queued input and output based on top of a GIOStream
+ *
+ * This stream can do flow control in two ways:
+ *
+ * - Its input can be throttled, it can listen to a "pressure" signal
+ * from another object passed into cockpit_stream_throttle()
+ * - It can optionally control another flow, by emitting a "pressure" signal
+ * when its output queue is too large
+ */
+
+enum {
+ PROP_0,
+ PROP_NAME,
+ PROP_IO_STREAM,
+ PROP_PROBLEM
+};
+
+typedef struct {
+ gchar *name;
+ GMainContext *context;
+
+ gboolean closed;
+ gboolean closing;
+ CockpitConnectable *connecting;
+ gchar *problem;
+
+ GIOStream *io;
+
+ GSource *out_source;
+ GQueue *out_queue;
+ gsize out_queued;
+ gsize out_partial;
+ gboolean out_closed;
+
+ gboolean in_done;
+ GSource *in_source;
+ GByteArray *in_buffer;
+ gboolean received;
+
+ /* Throttle this flow based on back pressure from another object */
+ CockpitFlow *pressure;
+ gulong pressure_sig;
+} CockpitStreamPrivate;
+
+/* A megabyte is when we start to consider queue full enough */
+#define QUEUE_PRESSURE 1024UL * 1024UL
+
+static guint cockpit_stream_sig_open;
+static guint cockpit_stream_sig_read;
+static guint cockpit_stream_sig_close;
+
+static void cockpit_close_later (CockpitStream *self);
+
+static void cockpit_stream_flow_iface_init (CockpitFlowInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CockpitStream, cockpit_stream, G_TYPE_OBJECT,
+ G_ADD_PRIVATE(CockpitStream)
+ G_IMPLEMENT_INTERFACE (COCKPIT_TYPE_FLOW, cockpit_stream_flow_iface_init));
+
+#define GET_PRIV(self) ((CockpitStreamPrivate *) cockpit_stream_get_instance_private(self))
+
+static void
+cockpit_stream_init (CockpitStream *self)
+{
+ GET_PRIV(self)->in_buffer = g_byte_array_new ();
+ GET_PRIV(self)->out_queue = g_queue_new ();
+
+ GET_PRIV(self)->context = g_main_context_ref_thread_default ();
+}
+
+static void
+stop_output (CockpitStream *self)
+{
+ g_assert (GET_PRIV(self)->out_source != NULL);
+ g_source_destroy (GET_PRIV(self)->out_source);
+ g_source_unref (GET_PRIV(self)->out_source);
+ GET_PRIV(self)->out_source = NULL;
+}
+
+static void
+stop_input (CockpitStream *self)
+{
+ g_assert (GET_PRIV(self)->in_source != NULL);
+ g_source_destroy (GET_PRIV(self)->in_source);
+ g_source_unref (GET_PRIV(self)->in_source);
+ GET_PRIV(self)->in_source = NULL;
+}
+
+static void
+close_immediately (CockpitStream *self,
+ const gchar *problem)
+{
+ GError *error = NULL;
+ GIOStream *io;
+
+ if (GET_PRIV(self)->closed)
+ return;
+
+ if (problem)
+ {
+ g_free (GET_PRIV(self)->problem);
+ GET_PRIV(self)->problem = g_strdup (problem);
+ }
+
+ if (GET_PRIV(self)->connecting)
+ {
+ cockpit_connectable_unref (GET_PRIV(self)->connecting);
+ GET_PRIV(self)->connecting = NULL;
+ }
+
+ GET_PRIV(self)->closed = TRUE;
+
+ g_debug ("%s: closing stream%s%s", GET_PRIV(self)->name,
+ GET_PRIV(self)->problem ? ": " : "",
+ GET_PRIV(self)->problem ? GET_PRIV(self)->problem : "");
+
+ if (GET_PRIV(self)->in_source)
+ stop_input (self);
+ if (GET_PRIV(self)->out_source)
+ stop_output (self);
+
+ if (GET_PRIV(self)->io)
+ {
+ io = GET_PRIV(self)->io;
+ GET_PRIV(self)->io = NULL;
+
+ g_io_stream_close (io, NULL, &error);
+ if (error)
+ {
+ g_message ("%s: close failed: %s", GET_PRIV(self)->name, error->message);
+ g_clear_error (&error);
+ }
+ g_object_unref (io);
+ }
+
+ g_debug ("%s: closed", GET_PRIV(self)->name);
+ g_signal_emit (self, cockpit_stream_sig_close, 0, GET_PRIV(self)->problem);
+}
+
+static void
+close_maybe (CockpitStream *self)
+{
+ if (!GET_PRIV(self)->closed)
+ {
+ if (GET_PRIV(self)->in_done && GET_PRIV(self)->out_closed)
+ {
+ g_debug ("%s: input and output done", GET_PRIV(self)->name);
+ close_immediately (self, NULL);
+ }
+ }
+}
+
+static void
+on_output_closed (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CockpitStream *self = COCKPIT_STREAM (user_data);
+ GError *error = NULL;
+
+ GET_PRIV(self)->out_closed = TRUE;
+
+ g_output_stream_close_finish (G_OUTPUT_STREAM (object), result, &error);
+ if (error)
+ {
+ g_warning ("%s: couldn't close output stream: %s", GET_PRIV(self)->name, error->message);
+ close_immediately (self, "internal-error");
+ }
+
+ close_maybe (self);
+ g_object_unref (self);
+}
+
+static void
+close_output (CockpitStream *self)
+{
+ if (GET_PRIV(self)->out_closed)
+ return;
+
+ g_debug ("%s: end of output", GET_PRIV(self)->name);
+
+ if (!GET_PRIV(self)->io)
+ {
+ close_maybe (self);
+ return;
+ }
+
+ g_output_stream_close_async (g_io_stream_get_output_stream (GET_PRIV(self)->io),
+ G_PRIORITY_DEFAULT, NULL, on_output_closed, g_object_ref (self));
+}
+
+#if !GLIB_CHECK_VERSION(2,43,2)
+#define G_IO_ERROR_CONNECTION_CLOSED G_IO_ERROR_BROKEN_PIPE
+#endif
+
+const gchar *
+cockpit_stream_problem (GError *error,
+ const gchar *name,
+ const gchar *summary,
+ JsonObject *options)
+{
+ const gchar *problem = NULL;
+ gchar *message;
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED))
+ problem = "access-denied";
+ else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_HOST_UNREACHABLE) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NETWORK_UNREACHABLE) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_HOST_NOT_FOUND) ||
+ g_error_matches (error, G_RESOLVER_ERROR, G_RESOLVER_ERROR_NOT_FOUND))
+ problem = "not-found";
+ else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_BROKEN_PIPE) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_CLOSED) ||
+ g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_EOF))
+ problem = "disconnected";
+#if !GLIB_CHECK_VERSION(2,43,2)
+ else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_FAILED) &&
+ strstr (error->message, g_strerror (ECONNRESET)))
+ problem = "disconnected";
+#endif
+ else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT))
+ problem = "timeout";
+ else if (g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_NOT_TLS) ||
+ g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_MISC))
+ problem = "protocol-error";
+ else if (g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE))
+ {
+ problem = "unknown-hostkey";
+ }
+
+ if (problem)
+ {
+ message = g_strdup_printf ("%s: %s: %s", name, summary, error->message);
+ }
+ else
+ {
+ message = g_strdup_printf ("%s: %s: %s", name, summary, error->message);
+ problem = "internal-error";
+ }
+ if (options)
+ {
+ if (!json_object_has_member (options, "message"))
+ json_object_set_string_member (options, "message", message);
+ }
+ g_message ("%s", message);
+ g_free (message);
+
+ return problem;
+}
+
+static void
+set_problem_from_error (CockpitStream *self,
+ const gchar *summary,
+ GError *error)
+{
+ const gchar *problem;
+
+ if (g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_MISC))
+ {
+ g_message ("%s: %s: %s", GET_PRIV(self)->name, summary, error->message);
+ if (GET_PRIV(self)->received)
+ problem = "disconnected";
+ else
+ problem = "protocol-error";
+ }
+ else
+ {
+ problem = cockpit_stream_problem (error, GET_PRIV(self)->name, summary, NULL);
+ }
+
+ g_free (GET_PRIV(self)->problem);
+ GET_PRIV(self)->problem = g_strdup (problem);
+}
+
+static gboolean
+dispatch_input (GPollableInputStream *is,
+ gpointer user_data)
+{
+ CockpitStream *self = (CockpitStream *)user_data;
+ GError *error = NULL;
+ gboolean read = FALSE;
+ gssize ret = 0;
+ gsize len;
+
+ for (;;)
+ {
+ g_return_val_if_fail (GET_PRIV(self)->in_source, FALSE);
+ len = GET_PRIV(self)->in_buffer->len;
+
+ g_byte_array_set_size (GET_PRIV(self)->in_buffer, len + 1024);
+ ret = g_pollable_input_stream_read_nonblocking (is, GET_PRIV(self)->in_buffer->data + len,
+ 1024, NULL, &error);
+
+ if (ret < 0)
+ {
+ g_byte_array_set_size (GET_PRIV(self)->in_buffer, len);
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+ {
+ g_error_free (error);
+ break;
+ }
+ else
+ {
+ set_problem_from_error (self, "couldn't read", error);
+ g_error_free (error);
+ close_immediately (self, NULL);
+ return FALSE;
+ }
+ }
+
+ g_byte_array_set_size (GET_PRIV(self)->in_buffer, len + ret);
+
+ if (ret == 0)
+ {
+ g_debug ("%s: end of input", GET_PRIV(self)->name);
+ GET_PRIV(self)->in_done = TRUE;
+ stop_input (self);
+ break;
+ }
+
+ g_debug ("%s: read %d bytes", GET_PRIV(self)->name, (int)ret);
+ GET_PRIV(self)->received = TRUE;
+ read = TRUE;
+ }
+
+ g_object_ref (self);
+
+ if (GET_PRIV(self)->in_done || read)
+ g_signal_emit (self, cockpit_stream_sig_read, 0, GET_PRIV(self)->in_buffer, GET_PRIV(self)->in_done);
+
+ if (GET_PRIV(self)->in_done)
+ close_maybe (self);
+
+ g_object_unref (self);
+ return TRUE;
+}
+
+static gboolean
+dispatch_output (GPollableOutputStream *os,
+ gpointer user_data)
+{
+ CockpitStream *self = (CockpitStream *)user_data;
+ GError *error = NULL;
+ const gint8 *data;
+ gsize len, size, before;
+ GBytes *popped;
+ gssize ret;
+
+ g_return_val_if_fail (GET_PRIV(self)->out_source, FALSE);
+ while (GET_PRIV(self)->out_queue->head)
+ {
+ data = g_bytes_get_data (GET_PRIV(self)->out_queue->head->data, &len);
+ g_assert (GET_PRIV(self)->out_partial <= len);
+
+ ret = g_pollable_output_stream_write_nonblocking (os, data + GET_PRIV(self)->out_partial,
+ len - GET_PRIV(self)->out_partial, NULL, &error);
+
+ if (ret < 0)
+ {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+ {
+ g_debug ("%s: output would block", GET_PRIV(self)->name);
+ g_error_free (error);
+ return TRUE;
+ }
+ else
+ {
+ set_problem_from_error (self, "couldn't write", error);
+ g_error_free (error);
+ close_immediately (self, NULL);
+ return FALSE;
+ }
+ }
+
+ GET_PRIV(self)->out_partial += ret;
+ if (GET_PRIV(self)->out_partial >= len)
+ {
+ before = GET_PRIV(self)->out_queued;
+
+ g_debug ("%s: wrote %d bytes", GET_PRIV(self)->name, (int)len);
+ popped = g_queue_pop_head (GET_PRIV(self)->out_queue);
+ size = g_bytes_get_size (popped);
+ g_assert (size <= GET_PRIV(self)->out_queued);
+ GET_PRIV(self)->out_queued -= size;
+ g_bytes_unref (popped);
+ GET_PRIV(self)->out_partial = 0;
+
+ if (before >= QUEUE_PRESSURE && GET_PRIV(self)->out_queued < QUEUE_PRESSURE)
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), FALSE);
+ }
+ else
+ {
+ if (ret > 0)
+ g_debug ("%s: partial write %d of %d bytes", GET_PRIV(self)->name, (int)ret, (int)len);
+ return TRUE;
+ }
+ }
+
+ g_debug ("%s: output queue empty", GET_PRIV(self)->name);
+
+ /* If all messages are done, then stop polling out fd */
+ stop_output (self);
+
+ if (GET_PRIV(self)->closing)
+ close_output (self);
+ else
+ close_maybe (self);
+
+ return TRUE;
+}
+
+static void
+start_output (CockpitStream *self)
+{
+ GOutputStream *os;
+
+ g_assert (GET_PRIV(self)->out_source == NULL);
+
+ if (GET_PRIV(self)->connecting || GET_PRIV(self)->out_closed || GET_PRIV(self)->closed)
+ return;
+
+ g_assert (GET_PRIV(self)->io);
+
+ os = g_io_stream_get_output_stream (GET_PRIV(self)->io);
+ GET_PRIV(self)->out_source = g_pollable_output_stream_create_source (G_POLLABLE_OUTPUT_STREAM (os), NULL);
+ g_source_set_name (GET_PRIV(self)->out_source, "stream-output");
+ g_source_set_callback (GET_PRIV(self)->out_source, (GSourceFunc)dispatch_output, self, NULL);
+ g_source_attach (GET_PRIV(self)->out_source, GET_PRIV(self)->context);
+}
+
+static void
+start_input (CockpitStream *self)
+{
+ GInputStream *is;
+
+ g_assert (GET_PRIV(self)->in_source == NULL);
+ g_assert (!GET_PRIV(self)->connecting);
+ g_assert (GET_PRIV(self)->io != NULL);
+
+ is = g_io_stream_get_input_stream (GET_PRIV(self)->io);
+ GET_PRIV(self)->in_source = g_pollable_input_stream_create_source (G_POLLABLE_INPUT_STREAM (is), NULL);
+ g_source_set_name (GET_PRIV(self)->in_source, "stream-input");
+ g_source_set_callback (GET_PRIV(self)->in_source, (GSourceFunc)dispatch_input, self, NULL);
+ g_source_attach (GET_PRIV(self)->in_source, GET_PRIV(self)->context);
+}
+
+static void
+initialize_io (CockpitStream *self)
+{
+ GInputStream *is;
+ GOutputStream *os;
+
+ g_return_if_fail (GET_PRIV(self)->in_source == NULL);
+
+ is = g_io_stream_get_input_stream (GET_PRIV(self)->io);
+ os = g_io_stream_get_output_stream (GET_PRIV(self)->io);
+
+ if (!G_IS_POLLABLE_INPUT_STREAM (is) ||
+ !g_pollable_input_stream_can_poll (G_POLLABLE_INPUT_STREAM (is)) ||
+ !G_IS_POLLABLE_OUTPUT_STREAM (os) ||
+ !g_pollable_output_stream_can_poll (G_POLLABLE_OUTPUT_STREAM (os)))
+ {
+ g_warning ("%s: stream is not pollable", GET_PRIV(self)->name);
+ close_immediately (self, "internal-error");
+ return;
+ }
+
+ if (GET_PRIV(self)->connecting)
+ {
+ cockpit_connectable_unref (GET_PRIV(self)->connecting);
+ GET_PRIV(self)->connecting = NULL;
+ }
+
+ start_input (self);
+
+ start_output (self);
+
+ g_signal_emit (self, cockpit_stream_sig_open, 0);
+}
+
+static void
+cockpit_stream_constructed (GObject *object)
+{
+ CockpitStream *self = COCKPIT_STREAM (object);
+
+ G_OBJECT_CLASS (cockpit_stream_parent_class)->constructed (object);
+
+ if (GET_PRIV(self)->io)
+ initialize_io (self);
+}
+
+static void
+cockpit_stream_set_property (GObject *obj,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitStream *self = COCKPIT_STREAM (obj);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ GET_PRIV(self)->name = g_value_dup_string (value);
+ break;
+ case PROP_IO_STREAM:
+ GET_PRIV(self)->io = g_value_dup_object (value);
+ break;
+ case PROP_PROBLEM:
+ GET_PRIV(self)->problem = g_value_dup_string (value);
+ if (GET_PRIV(self)->problem)
+ cockpit_close_later (self);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_stream_get_property (GObject *obj,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitStream *self = COCKPIT_STREAM (obj);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ g_value_set_string (value, GET_PRIV(self)->name);
+ break;
+ case PROP_IO_STREAM:
+ g_value_set_object (value, GET_PRIV(self)->io);
+ break;
+ case PROP_PROBLEM:
+ g_value_set_string (value, GET_PRIV(self)->problem);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_stream_dispose (GObject *object)
+{
+ CockpitStream *self = COCKPIT_STREAM (object);
+
+ if (!GET_PRIV(self)->closed)
+ close_immediately (self, "terminated");
+
+ cockpit_flow_throttle (COCKPIT_FLOW (self), NULL);
+ g_assert (GET_PRIV(self)->pressure == NULL);
+
+ while (GET_PRIV(self)->out_queue->head)
+ g_bytes_unref (g_queue_pop_head (GET_PRIV(self)->out_queue));
+ GET_PRIV(self)->out_queued = 0;
+
+ G_OBJECT_CLASS (cockpit_stream_parent_class)->dispose (object);
+}
+
+static void
+cockpit_stream_finalize (GObject *object)
+{
+ CockpitStream *self = COCKPIT_STREAM (object);
+
+ g_assert (GET_PRIV(self)->closed);
+ g_assert (!GET_PRIV(self)->in_source);
+ g_assert (!GET_PRIV(self)->out_source);
+
+ g_byte_array_unref (GET_PRIV(self)->in_buffer);
+ g_queue_free (GET_PRIV(self)->out_queue);
+ g_free (GET_PRIV(self)->problem);
+ g_free (GET_PRIV(self)->name);
+
+ if (GET_PRIV(self)->context)
+ g_main_context_unref (GET_PRIV(self)->context);
+
+ G_OBJECT_CLASS (cockpit_stream_parent_class)->finalize (object);
+}
+
+static void
+cockpit_stream_class_init (CockpitStreamClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->constructed = cockpit_stream_constructed;
+ gobject_class->get_property = cockpit_stream_get_property;
+ gobject_class->set_property = cockpit_stream_set_property;
+ gobject_class->dispose = cockpit_stream_dispose;
+ gobject_class->finalize = cockpit_stream_finalize;
+
+ /**
+ * CockpitStream:io-stream:
+ *
+ * The underlying io stream. The input and output streams should
+ * be pollable.
+ */
+ g_object_class_install_property (gobject_class, PROP_IO_STREAM,
+ g_param_spec_object ("io-stream", "io-stream", "io-stream", G_TYPE_IO_STREAM,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitStream:name:
+ *
+ * Pipe name used for debugging purposes.
+ */
+ g_object_class_install_property (gobject_class, PROP_NAME,
+ g_param_spec_string ("name", "name", "name", "<unnamed>",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitStream:problem:
+ *
+ * The problem that the pipe closed with. If used as a constructor argument then
+ * the pipe will be created in a closed/failed state. Although 'closed' signal will
+ * only fire once main loop is hit.
+ */
+ g_object_class_install_property (gobject_class, PROP_PROBLEM,
+ g_param_spec_string ("problem", "problem", "problem", NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitStream::open:
+ *
+ * Emitted when actually open and connected.
+ */
+ cockpit_stream_sig_open = g_signal_new ("open", COCKPIT_TYPE_STREAM, G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (CockpitStreamClass, open),
+ NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+ /**
+ * CockpitStream::read:
+ * @buffer: a GByteArray of the read data
+ * @eof: whether the pipe is done reading
+ *
+ * Emitted when data is read from the input file descriptor of the
+ * pipe.
+ *
+ * Data consumed from @buffer by the handler should be removed from
+ * the GByteArray. This can be done with the cockpit_stream_consume()
+ * function.
+ *
+ * This handler will only be called once with @eof set to TRUE. But
+ * in error conditions it may not be called with @eof set to TRUE
+ * at all, and the CockpitStream::close signal will simply fire.
+ */
+ cockpit_stream_sig_read = g_signal_new ("read", COCKPIT_TYPE_STREAM, G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (CockpitStreamClass, read),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 2, G_TYPE_BYTE_ARRAY, G_TYPE_BOOLEAN);
+
+ /**
+ * CockpitStream::close:
+ * @problem: problem string or %NULL
+ *
+ * Emitted when the pipe closes, whether due to a problem or a normal
+ * shutdown.
+ *
+ * @problem will be NULL if the pipe closed normally.
+ */
+ cockpit_stream_sig_close = g_signal_new ("close", COCKPIT_TYPE_STREAM, G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET (CockpitStreamClass, close),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1, G_TYPE_STRING);
+}
+
+/**
+ * cockpit_stream_write:
+ * @self: the pipe
+ * @data: the data to write
+ *
+ * Write @data to the pipe. This is not done immediately, it's
+ * queued and written when the pipe is ready.
+ *
+ * If you cockpit_stream_close() with a @problem, then queued data
+ * will be discarded.
+ *
+ * Calling this function on a closed or closing pipe (one on which
+ * cockpit_stream_close() has been called) is invalid.
+ *
+ * Zero length data blocks are ignored, it doesn't makes sense to
+ * write zero bytes to a pipe.
+ */
+void
+cockpit_stream_write (CockpitStream *self,
+ GBytes *data)
+{
+ gsize size, before;
+
+ g_return_if_fail (COCKPIT_IS_STREAM (self));
+ g_return_if_fail (!GET_PRIV(self)->closing);
+
+ g_return_if_fail (!GET_PRIV(self)->closed);
+
+ size = g_bytes_get_size (data);
+ if (size == 0)
+ {
+ g_debug ("%s: ignoring zero byte data block", GET_PRIV(self)->name);
+ return;
+ }
+
+ before = GET_PRIV(self)->out_queued;
+ g_return_if_fail (G_MAXSIZE - size > GET_PRIV(self)->out_queued);
+ GET_PRIV(self)->out_queued += size;
+ g_queue_push_tail (GET_PRIV(self)->out_queue, g_bytes_ref (data));
+
+ /* No longer have pressure in the queue? */
+ if (before < QUEUE_PRESSURE && GET_PRIV(self)->out_queued >= QUEUE_PRESSURE)
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), TRUE);
+
+ if (!GET_PRIV(self)->out_source && !GET_PRIV(self)->out_closed)
+ {
+ start_output (self);
+ }
+
+ /*
+ * If this becomes thread-safe, then something like this is needed:
+ * g_main_context_wakeup (g_source_get_context (GET_PRIV(self)->source));
+ */
+}
+
+/**
+ * cockpit_stream_close:
+ * @self: a pipe
+ * @problem: a problem or NULL
+ *
+ * Close the pipe. If @problem is non NULL, then it's treated
+ * as if an error occurred, and the pipe is closed immediately.
+ * Otherwise the pipe output is closed when all data has been sent.
+ *
+ * The 'close' signal will be fired when the pipe actually closes.
+ * This may be during this function call (esp. in the case of a
+ * non-NULL @problem) or later.
+ */
+void
+cockpit_stream_close (CockpitStream *self,
+ const gchar *problem)
+{
+ g_return_if_fail (COCKPIT_IS_STREAM (self));
+
+ GET_PRIV(self)->closing = TRUE;
+
+ if (problem)
+ close_immediately (self, problem);
+ else if (g_queue_is_empty (GET_PRIV(self)->out_queue))
+ close_output (self);
+}
+
+static gboolean
+on_later_close (gpointer user_data)
+{
+ close_immediately (user_data, NULL); /* problem already set */
+ return FALSE;
+}
+
+static void
+cockpit_close_later (CockpitStream *self)
+{
+ GSource *source = g_idle_source_new ();
+ g_source_set_priority (source, G_PRIORITY_HIGH);
+ g_source_set_callback (source, on_later_close, g_object_ref (self), g_object_unref);
+ g_source_attach (source, g_main_context_get_thread_default ());
+ g_source_unref (source);
+}
+
+static void
+on_connect_stream (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CockpitStream *self = COCKPIT_STREAM (user_data);
+ GError *error = NULL;
+ GIOStream *io;
+
+ io = cockpit_connect_stream_finish (result, &error);
+ if (error)
+ {
+ set_problem_from_error (self, "couldn't connect", error);
+ close_immediately (self, NULL);
+ g_error_free (error);
+ }
+ else if (!GET_PRIV(self)->closed)
+ {
+ GET_PRIV(self)->io = g_object_ref (io);
+ initialize_io (self);
+ }
+
+ g_clear_object (&io);
+ g_object_unref (self);
+}
+
+CockpitStream *
+cockpit_stream_connect (const gchar *name,
+ CockpitConnectable *connectable)
+{
+ CockpitStream *self;
+
+ g_return_val_if_fail (connectable != NULL, NULL);
+
+ self = g_object_new (COCKPIT_TYPE_STREAM,
+ "io-stream", NULL,
+ "name", name,
+ NULL);
+
+ GET_PRIV(self)->connecting = cockpit_connectable_ref (connectable);
+ cockpit_connect_stream_full (GET_PRIV(self)->connecting, NULL,
+ on_connect_stream, g_object_ref (self));
+
+ return self;
+}
+
+/**
+ * cockpit_stream_get_name:
+ * @self: a pipe
+ *
+ * Get the name of the pipe.
+ *
+ * This is used for logging.
+ *
+ * Returns: (transfer none): the name
+ */
+const gchar *
+cockpit_stream_get_name (CockpitStream *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_STREAM (self), NULL);
+ return GET_PRIV(self)->name;
+}
+
+/**
+ * cockpit_stream_get_buffer:
+ * @self: a pipe
+ *
+ * Get the input buffer for the pipe.
+ *
+ * This can change when the main loop is run. You can use
+ * cockpit_pipe_consume() to consume data from it.
+ *
+ * Returns: (transfer none): the buffer
+ */
+GByteArray *
+cockpit_stream_get_buffer (CockpitStream *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_STREAM (self), NULL);
+ return GET_PRIV(self)->in_buffer;
+}
+
+/**
+ * cockpit_stream_new:
+ * @name: a name for debugging
+ * @io_stream: A stream to wrap
+ *
+ * Create a stream for the given io stream
+ *
+ * Returns: (transfer full): a new CockpitStream
+ */
+CockpitStream *
+cockpit_stream_new (const gchar *name,
+ GIOStream *io_stream)
+{
+ return g_object_new (COCKPIT_TYPE_STREAM,
+ "name", name,
+ "io-stream", io_stream,
+ NULL);
+}
+
+static void
+on_throttle_pressure (GObject *object,
+ gboolean throttle,
+ gpointer user_data)
+{
+ CockpitStream *self = COCKPIT_STREAM (user_data);
+ if (throttle)
+ {
+ if (GET_PRIV(self)->in_source != NULL)
+ {
+ g_debug ("%s: applying back pressure in stream", GET_PRIV(self)->name);
+ stop_input (self);
+ }
+ }
+ else
+ {
+ if (GET_PRIV(self)->in_source == NULL && GET_PRIV(self)->io && !GET_PRIV(self)->connecting)
+ {
+ g_debug ("%s: relieving back pressure in stream", GET_PRIV(self)->name);
+ start_input (self);
+ }
+ }
+}
+
+static void
+cockpit_stream_throttle (CockpitFlow *flow,
+ CockpitFlow *controlling)
+{
+ CockpitStream *self = COCKPIT_STREAM (flow);
+
+ if (GET_PRIV(self)->pressure)
+ {
+ g_signal_handler_disconnect (GET_PRIV(self)->pressure, GET_PRIV(self)->pressure_sig);
+ g_object_remove_weak_pointer (G_OBJECT (GET_PRIV(self)->pressure), (gpointer *)&GET_PRIV(self)->pressure);
+ GET_PRIV(self)->pressure = NULL;
+ }
+
+ if (controlling)
+ {
+ GET_PRIV(self)->pressure = controlling;
+ g_object_add_weak_pointer (G_OBJECT (GET_PRIV(self)->pressure), (gpointer *)&GET_PRIV(self)->pressure);
+ GET_PRIV(self)->pressure_sig = g_signal_connect (controlling, "pressure", G_CALLBACK (on_throttle_pressure), self);
+ }
+}
+
+static void
+cockpit_stream_flow_iface_init (CockpitFlowInterface *iface)
+{
+ iface->throttle = cockpit_stream_throttle;
+}
diff --git a/src/bridge/cockpitstream.h b/src/bridge/cockpitstream.h
new file mode 100644
index 0000000..c6ea16e
--- /dev/null
+++ b/src/bridge/cockpitstream.h
@@ -0,0 +1,65 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "cockpitconnect.h"
+
+#include "common/cockpitjson.h"
+
+#define COCKPIT_TYPE_STREAM (cockpit_stream_get_type ())
+G_DECLARE_DERIVABLE_TYPE(CockpitStream, cockpit_stream, COCKPIT, STREAM, GObject)
+
+struct _CockpitStreamClass {
+ GObjectClass parent_class;
+
+ /* signals */
+
+ void (* open) (CockpitStream *stream);
+
+ void (* read) (CockpitStream *pipe,
+ GByteArray *buffer,
+ gboolean eof);
+
+ void (* close) (CockpitStream *pipe,
+ const gchar *problem);
+};
+
+CockpitStream * cockpit_stream_new (const gchar *name,
+ GIOStream *stream);
+
+CockpitStream * cockpit_stream_connect (const gchar *name,
+ CockpitConnectable *connectable);
+
+void cockpit_stream_write (CockpitStream *self,
+ GBytes *data);
+
+void cockpit_stream_close (CockpitStream *self,
+ const gchar *problem);
+
+const gchar * cockpit_stream_get_name (CockpitStream *self);
+
+GByteArray * cockpit_stream_get_buffer (CockpitStream *self);
+
+const gchar * cockpit_stream_problem (GError *error,
+ const gchar *name,
+ const gchar *summary,
+ JsonObject *object);
diff --git a/src/bridge/cockpitwebsocketstream.c b/src/bridge/cockpitwebsocketstream.c
new file mode 100644
index 0000000..97c8abd
--- /dev/null
+++ b/src/bridge/cockpitwebsocketstream.c
@@ -0,0 +1,408 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitwebsocketstream.h"
+
+#include "cockpitconnect.h"
+#include "cockpitstream.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpitflow.h"
+#include "common/cockpitjson.h"
+
+#include "websocket/websocket.h"
+
+#include <string.h>
+
+/**
+ * CockpitWebSocketStream:
+ *
+ * A #CockpitChannel that represents a WebSocket client
+ *
+ * The payload type for this channel is 'websocket-stream1'.
+ */
+
+#define COCKPIT_WEB_SOCKET_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_WEB_SOCKET_STREAM, CockpitWebSocketStream))
+
+typedef struct _CockpitWebSocketStream {
+ CockpitChannel parent;
+
+ /* The nickname for debugging and logging */
+ gchar *url;
+ gchar *origin;
+
+ /* The connection */
+ WebSocketConnection *client;
+ gulong sig_open;
+ gulong sig_message;
+ gulong sig_closing;
+ gulong sig_close;
+ gulong sig_error;
+
+ gboolean binary;
+ gboolean closed;
+ gushort last_error_code;
+
+} CockpitWebSocketStream;
+
+G_DEFINE_TYPE (CockpitWebSocketStream, cockpit_web_socket_stream, COCKPIT_TYPE_CHANNEL);
+
+static void
+cockpit_web_socket_stream_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (channel);
+ WebSocketDataType type;
+ WebSocketState state;
+
+ /* Should never be called before cockpit_channel_ready() */
+ g_return_if_fail (self->client != NULL);
+
+ state = web_socket_connection_get_ready_state (self->client);
+ g_return_if_fail (state >= WEB_SOCKET_STATE_OPEN);
+
+ if (state == WEB_SOCKET_STATE_OPEN)
+ {
+ type = self->binary ? WEB_SOCKET_DATA_BINARY : WEB_SOCKET_DATA_TEXT;
+ web_socket_connection_send (self->client, type, NULL, message);
+ }
+}
+
+static gboolean
+cockpit_web_socket_stream_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *options)
+{
+ CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (channel);
+
+ if (!g_str_equal (command, "done"))
+ return FALSE;
+
+ if (self->client && web_socket_connection_get_ready_state (self->client) == WEB_SOCKET_STATE_OPEN)
+ web_socket_connection_close (self->client, WEB_SOCKET_CLOSE_NORMAL, "disconnected");
+
+ return TRUE;
+}
+
+static void
+cockpit_web_socket_stream_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (channel);
+
+ self->closed = TRUE;
+ if (self->client && web_socket_connection_get_ready_state (self->client) < WEB_SOCKET_STATE_CLOSING)
+ {
+ if (problem)
+ web_socket_connection_close (self->client, WEB_SOCKET_CLOSE_ABNORMAL, problem);
+ else
+ web_socket_connection_close (self->client, WEB_SOCKET_CLOSE_NORMAL, "disconnected");
+ }
+ COCKPIT_CHANNEL_CLASS (cockpit_web_socket_stream_parent_class)->close (channel, problem);
+}
+
+static void
+cockpit_web_socket_stream_init (CockpitWebSocketStream *self)
+{
+
+}
+
+static void
+on_web_socket_open (WebSocketConnection *connection,
+ gpointer user_data)
+{
+ CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (user_data);
+ CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
+ JsonObject *object;
+ JsonObject *headers;
+ GHashTableIter iter;
+ gpointer key, value;
+
+ headers = json_object_new ();
+
+ g_hash_table_iter_init (&iter, web_socket_client_get_headers (WEB_SOCKET_CLIENT (self->client)));
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ json_object_set_string_member (headers, key, value);
+
+ object = json_object_new ();
+ json_object_set_object_member (object, "headers", headers);
+
+ cockpit_channel_control (channel, "response", object);
+ json_object_unref (object);
+
+ cockpit_channel_ready (channel, NULL);
+}
+
+static void
+on_web_socket_message (WebSocketConnection *connection,
+ WebSocketDataType type,
+ GBytes *message,
+ gpointer user_data)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
+ cockpit_channel_send (channel, message, type == WEB_SOCKET_DATA_TEXT);
+}
+
+static gboolean
+on_web_socket_closing (WebSocketConnection *connection,
+ gpointer user_data)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
+ cockpit_channel_control (channel, "done", NULL);
+ return TRUE;
+}
+
+static gboolean
+on_web_socket_error (WebSocketConnection *ws,
+ GError *error,
+ gpointer user_data)
+{
+ CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (user_data);
+ self->last_error_code = 0;
+ if (error && error->domain == WEB_SOCKET_ERROR)
+ self->last_error_code = error->code;
+
+ return TRUE;
+}
+
+static void
+on_web_socket_close (WebSocketConnection *connection,
+ gpointer user_data)
+{
+ CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (user_data);
+ CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
+ const gchar *problem;
+ gushort code;
+
+ code = web_socket_connection_get_close_code (connection);
+ problem = web_socket_connection_get_close_data (connection);
+
+ if (code == WEB_SOCKET_CLOSE_NORMAL || code == WEB_SOCKET_CLOSE_GOING_AWAY)
+ {
+ problem = NULL;
+ }
+ else if (problem == NULL || !problem[0])
+ {
+ /* If we don't have a code but have a last error
+ * use it's code */
+ if (code == 0)
+ code = self->last_error_code;
+
+ switch (code)
+ {
+ case WEB_SOCKET_CLOSE_NO_STATUS:
+ case WEB_SOCKET_CLOSE_ABNORMAL:
+ problem = "disconnected";
+ break;
+ case WEB_SOCKET_CLOSE_PROTOCOL:
+ case WEB_SOCKET_CLOSE_UNSUPPORTED_DATA:
+ case WEB_SOCKET_CLOSE_BAD_DATA:
+ case WEB_SOCKET_CLOSE_POLICY_VIOLATION:
+ case WEB_SOCKET_CLOSE_TOO_BIG:
+ case WEB_SOCKET_CLOSE_TLS_HANDSHAKE:
+ problem = "protocol-error";
+ break;
+ case WEB_SOCKET_CLOSE_NO_EXTENSION:
+ problem = "unsupported";
+ break;
+ default:
+ problem = "internal-error";
+ break;
+ }
+ }
+
+ cockpit_channel_close (channel, problem);
+}
+
+static void
+on_socket_connect (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(CockpitWebSocketStream) self = user_data; /* capture the ref passed to the async op */
+ CockpitChannel *channel = COCKPIT_CHANNEL (self);
+ g_autofree const gchar **protocols = NULL;
+ g_autoptr(GList) names = NULL;
+ g_autoptr(GError) error = NULL;
+
+ g_autoptr(GIOStream) io = cockpit_connect_stream_finish (result, &error);
+ if (error)
+ {
+ const char *problem = cockpit_stream_problem (error, self->origin, "couldn't connect",
+ cockpit_channel_close_options (channel));
+ cockpit_channel_close (channel, problem);
+ return;
+ }
+
+ JsonObject *options = cockpit_channel_get_options (channel);
+
+ if (!cockpit_json_get_strv (options, "protocols", NULL, &protocols))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"protocol\" value in WebSocket stream request", self->origin);
+ return;
+ }
+
+ self->client = web_socket_client_new_for_stream (self->url, self->origin, protocols, io);
+
+ JsonNode *node = json_object_get_member (options, "headers");
+ if (node)
+ {
+ if (!JSON_NODE_HOLDS_OBJECT (node))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid \"headers\" field in WebSocket stream request", self->origin);
+ return;
+ }
+
+ JsonObject *headers = json_node_get_object (node);
+ names = json_object_get_members (headers);
+ for (GList *l = names; l != NULL; l = g_list_next (l))
+ {
+ node = json_object_get_member (headers, l->data);
+ if (!node || !JSON_NODE_HOLDS_VALUE (node) || json_node_get_value_type (node) != G_TYPE_STRING)
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid header value in WebSocket stream request: %s",
+ self->origin, (gchar *)l->data);
+ return;
+ }
+ const gchar *value = json_node_get_string (node);
+
+ g_debug ("%s: sending header: %s %s", self->origin, (gchar *)l->data, value);
+ web_socket_client_include_header (WEB_SOCKET_CLIENT (self->client), l->data, value);
+ }
+ }
+
+ self->sig_open = g_signal_connect (self->client, "open", G_CALLBACK (on_web_socket_open), self);
+ self->sig_message = g_signal_connect (self->client, "message", G_CALLBACK (on_web_socket_message), self);
+ self->sig_closing = g_signal_connect (self->client, "closing", G_CALLBACK (on_web_socket_closing), self);
+ self->sig_close = g_signal_connect (self->client, "close", G_CALLBACK (on_web_socket_close), self);
+ self->sig_error = g_signal_connect (self->client, "error", G_CALLBACK (on_web_socket_error), self);
+
+ /* Let the channel throttle the websocket's input flow*/
+ cockpit_flow_throttle (COCKPIT_FLOW (self->client), COCKPIT_FLOW (self));
+
+ /* Let the websocket throtlte the channel peer's output flow */
+ cockpit_flow_throttle (COCKPIT_FLOW (channel), COCKPIT_FLOW (self->client));
+}
+
+static void
+cockpit_web_socket_stream_prepare (CockpitChannel *channel)
+{
+ CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (channel);
+ CockpitConnectable *connectable = NULL;
+ JsonObject *options;
+ const gchar *path;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_web_socket_stream_parent_class)->prepare (channel);
+
+ if (self->closed)
+ goto out;
+
+ connectable = cockpit_connect_parse_stream (channel);
+ if (!connectable)
+ goto out;
+
+ options = cockpit_channel_get_options (channel);
+ if (!cockpit_json_get_string (options, "path", NULL, &path))
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: bad \"path\" field in WebSocket stream request", self->origin);
+ goto out;
+ }
+ else if (path == NULL || path[0] != '/')
+ {
+ cockpit_channel_fail (channel, "protocol-error",
+ "%s: invalid or missing \"path\" field in WebSocket stream request", self->origin);
+ goto out;
+ }
+
+ self->url = g_strdup_printf ("%s://%s%s", connectable->tls ? "wss" : "ws", connectable->name, path);
+ self->origin = g_strdup_printf ("%s://%s", connectable->tls ? "https" : "http", connectable->name);
+
+ /* Parsed elsewhere */
+ self->binary = json_object_has_member (options, "binary");
+
+ cockpit_connect_stream_full (connectable, NULL, on_socket_connect, g_object_ref (self));
+
+out:
+ if (connectable)
+ cockpit_connectable_unref (connectable);
+}
+
+static void
+cockpit_web_socket_stream_dispose (GObject *object)
+{
+ CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (object);
+
+ if (self->client)
+ {
+ if (web_socket_connection_get_ready_state (self->client) < WEB_SOCKET_STATE_CLOSING)
+ web_socket_connection_close (self->client, WEB_SOCKET_CLOSE_GOING_AWAY, "disconnected");
+ g_signal_handler_disconnect (self->client, self->sig_open);
+ g_signal_handler_disconnect (self->client, self->sig_message);
+ g_signal_handler_disconnect (self->client, self->sig_closing);
+ g_signal_handler_disconnect (self->client, self->sig_close);
+ g_signal_handler_disconnect (self->client, self->sig_error);
+
+ g_object_unref (self->client);
+ self->client = NULL;
+ }
+
+ G_OBJECT_CLASS (cockpit_web_socket_stream_parent_class)->dispose (object);
+}
+
+static void
+cockpit_web_socket_stream_finalize (GObject *object)
+{
+ CockpitWebSocketStream *self = COCKPIT_WEB_SOCKET_STREAM (object);
+
+ g_free (self->url);
+ g_free (self->origin);
+ g_assert (self->client == NULL);
+
+ G_OBJECT_CLASS (cockpit_web_socket_stream_parent_class)->finalize (object);
+}
+
+static void
+cockpit_web_socket_stream_constructed (GObject *object)
+{
+ static const gchar *caps[] = { "tls-certificates", "address", NULL };
+ G_OBJECT_CLASS (cockpit_web_socket_stream_parent_class)->constructed (object);
+ g_object_set (object, "capabilities", &caps, NULL);
+}
+
+static void
+cockpit_web_socket_stream_class_init (CockpitWebSocketStreamClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+
+ gobject_class->dispose = cockpit_web_socket_stream_dispose;
+ gobject_class->finalize = cockpit_web_socket_stream_finalize;
+ gobject_class->constructed = cockpit_web_socket_stream_constructed;
+
+ channel_class->prepare = cockpit_web_socket_stream_prepare;
+ channel_class->control = cockpit_web_socket_stream_control;
+ channel_class->recv = cockpit_web_socket_stream_recv;
+ channel_class->close = cockpit_web_socket_stream_close;
+}
diff --git a/src/bridge/cockpitwebsocketstream.h b/src/bridge/cockpitwebsocketstream.h
new file mode 100644
index 0000000..fcc1591
--- /dev/null
+++ b/src/bridge/cockpitwebsocketstream.h
@@ -0,0 +1,26 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "common/cockpitchannel.h"
+
+#define COCKPIT_TYPE_WEB_SOCKET_STREAM (cockpit_web_socket_stream_get_type ())
+G_DECLARE_FINAL_TYPE (CockpitWebSocketStream, cockpit_web_socket_stream,
+ COCKPIT, WEB_SOCKET_STREAM, CockpitChannel)
diff --git a/src/bridge/mock-bridge.c b/src/bridge/mock-bridge.c
new file mode 100644
index 0000000..b93acb1
--- /dev/null
+++ b/src/bridge/mock-bridge.c
@@ -0,0 +1,352 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpithacks-glib.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitpipetransport.h"
+#include "common/cockpitsystem.h"
+
+#include <glib-unix.h>
+
+#include <signal.h>
+
+/*
+ * The bridge implements two channel types.
+ *
+ * 'uppercase': Make all data upper case
+ * 'lowercase': Make all data lower case
+ *
+ * By default only the first one is available. If run with --lower
+ * then the latter is available.
+ */
+
+static gboolean opt_lower;
+static gboolean opt_upper;
+static gboolean opt_show_count;
+static gboolean opt_problem;
+
+static GHashTable *channels;
+
+static GType mock_case_channel_get_type (void) G_GNUC_CONST;
+
+typedef struct {
+ CockpitChannel parent;
+ gchar (* function) (gchar);
+} MockCaseChannel;
+
+typedef CockpitChannelClass MockCaseChannelClass;
+
+G_DEFINE_TYPE (MockCaseChannel, mock_case_channel, COCKPIT_TYPE_CHANNEL);
+
+static void
+mock_case_channel_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ MockCaseChannel *self = (MockCaseChannel *)channel;
+ GByteArray *array = g_bytes_unref_to_array (g_bytes_ref (message));
+ GBytes *bytes;
+ gsize i;
+
+ for (i = 0; i < array->len; i++)
+ array->data[i] = self->function(array->data[i]);
+
+ bytes = g_byte_array_free_to_bytes (array);
+ cockpit_channel_send (channel, bytes, FALSE);
+ g_bytes_unref (bytes);
+}
+
+static gboolean
+mock_case_channel_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *options)
+{
+ if (g_strcmp0 (command, "close-later") == 0)
+ cockpit_channel_close (channel, "closed");
+
+ return TRUE;
+}
+
+static void
+mock_case_channel_init (MockCaseChannel *self)
+{
+
+}
+
+static void
+mock_case_channel_constructed (GObject *obj)
+{
+ MockCaseChannel *self = (MockCaseChannel *)obj;
+ const gchar *payload = NULL;
+ JsonObject *options;
+ JsonObject *ready = json_object_new ();
+
+ const gchar *env = g_getenv ("COCKPIT_TEST_PARAM_ENV");
+
+ G_OBJECT_CLASS (mock_case_channel_parent_class)->constructed (obj);
+
+ options = cockpit_channel_get_options (COCKPIT_CHANNEL (obj));
+ if (!cockpit_json_get_string (options, "payload", NULL, &payload))
+ g_assert_not_reached ();
+
+ if (g_strcmp0 (payload, "upper") == 0)
+ self->function = g_ascii_toupper;
+ else if (g_strcmp0 (payload, "lower") == 0)
+ self->function = g_ascii_tolower;
+ else
+ g_assert_not_reached ();
+
+ if (env)
+ json_object_set_string_member (ready, "test-env", env);
+
+ if (opt_show_count)
+ json_object_set_int_member (ready, "count", g_hash_table_size (channels));
+
+ cockpit_channel_ready (COCKPIT_CHANNEL (self), ready);
+ json_object_unref (ready);
+}
+
+static void
+mock_case_channel_class_init (MockCaseChannelClass *klass)
+{
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = mock_case_channel_constructed;
+ channel_class->recv = mock_case_channel_recv;
+ channel_class->control = mock_case_channel_control;
+}
+
+static gboolean init_received;
+static gboolean opt_lower;
+
+static void
+on_channel_closed (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ g_hash_table_remove (channels, cockpit_channel_get_id (channel));
+}
+
+static void
+process_init (CockpitTransport *transport,
+ JsonObject *options)
+{
+ gint64 version = -1;
+
+ if (!cockpit_json_get_int (options, "version", -1, &version))
+ {
+ g_warning ("invalid version field in init message");
+ cockpit_transport_close (transport, "protocol-error");
+ }
+
+ if (version == 1)
+ {
+ g_debug ("received init message");
+ init_received = TRUE;
+ }
+ else
+ {
+ g_message ("unsupported version of cockpit protocol: %" G_GINT64_FORMAT, version);
+ cockpit_transport_close (transport, "not-supported");
+ }
+}
+
+static void
+process_open (CockpitTransport *transport,
+ const gchar *channel_id,
+ JsonObject *options)
+{
+ CockpitChannel *channel;
+ GType channel_type;
+ const gchar *payload;
+
+ if (!channel_id)
+ {
+ g_warning ("Caller tried to open channel with invalid id");
+ cockpit_transport_close (transport, "protocol-error");
+ }
+ else if (g_hash_table_lookup (channels, channel_id))
+ {
+ g_warning ("Caller tried to reuse a channel that's already in use");
+ cockpit_transport_close (transport, "protocol-error");
+ }
+ else
+ {
+ if (!cockpit_json_get_string (options, "payload", NULL, &payload))
+ payload = NULL;
+
+ /* This will close with "not-supported" */
+ channel_type = COCKPIT_TYPE_CHANNEL;
+
+ if ((opt_lower && g_strcmp0 (payload, "lower") == 0) ||
+ (opt_upper && g_strcmp0 (payload, "upper") == 0))
+ channel_type = mock_case_channel_get_type ();
+
+ channel = g_object_new (channel_type,
+ "transport", transport,
+ "id", channel_id,
+ "options", options,
+ NULL);
+
+ g_hash_table_insert (channels, g_strdup (channel_id), channel);
+ g_signal_connect (channel, "closed", G_CALLBACK (on_channel_closed), NULL);
+ }
+}
+
+static gboolean
+on_transport_control (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel_id,
+ JsonObject *options,
+ GBytes *message,
+ gpointer user_data)
+{
+ if (g_str_equal (command, "init"))
+ {
+ process_init (transport, options);
+ return TRUE;
+ }
+ else if (!init_received)
+ {
+ g_warning ("caller did not send 'init' message first");
+ cockpit_transport_close (transport, "protocol-error");
+ return TRUE;
+ }
+ else if (g_str_equal (command, "open"))
+ {
+ process_open (transport, channel_id, options);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+on_closed_set_flag (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gboolean *flag = user_data;
+ *flag = TRUE;
+}
+
+static void
+send_init_command (CockpitTransport *transport)
+{
+ JsonObject *object;
+ GBytes *bytes;
+
+ object = json_object_new ();
+ json_object_set_string_member (object, "command", "init");
+ json_object_set_int_member (object, "version", 1);
+
+ if (opt_problem)
+ {
+ json_object_set_string_member (object, "problem", "canna-do-it");
+ json_object_set_string_member (object, "another-field", "extra");
+ }
+
+ bytes = cockpit_json_write_bytes (object);
+ json_object_unref (object);
+
+ cockpit_transport_send (transport, NULL, bytes);
+ g_bytes_unref (bytes);
+}
+
+static gboolean
+on_signal_done (gpointer data)
+{
+ gboolean *closed = data;
+ *closed = TRUE;
+ return TRUE;
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ CockpitTransport *transport;
+ gboolean terminated = FALSE;
+ gboolean interrupted = FALSE;
+ gboolean closed = FALSE;
+ GOptionContext *context;
+ GError *error = NULL;
+ guint sig_term;
+ guint sig_int;
+
+ static GOptionEntry entries[] = {
+ { "lower", 0, 0, G_OPTION_ARG_NONE, &opt_lower, "Lower case channel type", NULL },
+ { "count", 0, 0, G_OPTION_ARG_NONE, &opt_show_count, "Show open channels count in ready messages", NULL },
+ { "upper", 0, 0, G_OPTION_ARG_NONE, &opt_upper, "Upper case channel type", NULL },
+ { "problem", 0, 0, G_OPTION_ARG_NONE, &opt_problem, "Set a problem in \"init\" message", NULL },
+ { NULL }
+ };
+
+ signal (SIGPIPE, SIG_IGN);
+
+ cockpit_setenv_check ("GSETTINGS_BACKEND", "memory", TRUE);
+ cockpit_setenv_check ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE);
+ cockpit_setenv_check ("GIO_USE_VFS", "local", TRUE);
+
+ context = g_option_context_new (NULL);
+ g_option_context_add_main_entries (context, entries, NULL);
+ g_option_context_set_description (context, "mock-bridge as used in tests\n");
+
+ g_option_context_parse (context, &argc, &argv, &error);
+ g_option_context_free (context);
+
+ if (error)
+ {
+ g_printerr ("mock-bridge: %s\n", error->message);
+ g_error_free (error);
+ return 255;
+ }
+
+ cockpit_hacks_redirect_gdebug_to_stderr ();
+
+ sig_term = g_unix_signal_add (SIGTERM, on_signal_done, &terminated);
+ sig_int = g_unix_signal_add (SIGINT, on_signal_done, &interrupted);
+
+ transport = cockpit_pipe_transport_new_fds ("stdio", 0, 1);
+
+ g_signal_connect (transport, "control", G_CALLBACK (on_transport_control), NULL);
+ g_signal_connect (transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+ send_init_command (transport);
+
+ /* Owns the channels */
+ channels = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+
+ while (!terminated && !closed && !interrupted)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (transport);
+ g_hash_table_destroy (channels);
+
+ g_source_remove (sig_term);
+ g_source_remove (sig_int);
+
+ /* So the caller gets the right signal */
+ if (terminated)
+ raise (SIGTERM);
+
+ return 0;
+}
diff --git a/src/bridge/mock-client.crt b/src/bridge/mock-client.crt
new file mode 100644
index 0000000..f7c075d
--- /dev/null
+++ b/src/bridge/mock-client.crt
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICxzCCAa+gAwIBAgIJANDrBNw3XYJ0MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
+BAMMCWxvY2FsaG9zdDAgFw0xNTAzMjUxMDMzMzRaGA8yMTE1MDMwMTEwMzMzNFow
+FDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA8l1q01B5N/biaFDazUtuPuOrFsLOC67LX1iiE62guchEf9FyEagglGzt
+XOSCpY/qX0HWmIkE3Pqotb8lPQ0mUHleYCvzY85cFmj4mu+rDIPxK/lw37Xu00iP
+/rbcCA6K6dgMjp0TJzZvMnU2PywtFqDpw6ZchcMi517keMfLwscUC/7Y80lP0PGA
+1wTDaYoxuMlUhqTTfdLoBZ73eA9YzgqBeZ9ePxoUFk9AtJtlOlR60mGbEOweDUfc
+l1biKtarDW5SJYbVTFjWdPsCV6czZndfVKAAkDd+bsbFMcEiq/doHU092Yy3sZ9g
+hnOBw5sCq8iTXQ9cmejxUrsu/SvL3QIDAQABoxowGDAJBgNVHRMEAjAAMAsGA1Ud
+DwQEAwIF4DANBgkqhkiG9w0BAQsFAAOCAQEAalykXV+z1tQOv1ZRvJmppjEIYTa3
+pFehy97BiNGERTQJQDSzOgptIaCJb1vE34KNL349QEO4F8XTPWhwsCAXNTBN4yhm
+NJ6qbYkz0HbBmdM4k0MgbB9VG00Hy+TmwEt0zVryICZY4IomKmS1No0Lai5hOqdz
+afUMVIIYjVB1WYIsIaXXug7Mik/O+6K5hIbqm9HkwRwfoVaOLNG9EPUM14vFnN5p
+EyHSBByk0mOU8EUK/qsAnbTwABEKsMxCopmvPTguGHTwllEvxPgt5BcYMU9oXlvc
+cSvnU4a6M2qxQn3LUqxENh9QaQ8vV4l/avZBi1cFKVs1rza36eOGxrJxQw==
+-----END CERTIFICATE-----
diff --git a/src/bridge/mock-client.key b/src/bridge/mock-client.key
new file mode 100644
index 0000000..f526d9a
--- /dev/null
+++ b/src/bridge/mock-client.key
@@ -0,0 +1,29 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDyXWrTUHk39uJo
+UNrNS24+46sWws4LrstfWKITraC5yER/0XIRqCCUbO1c5IKlj+pfQdaYiQTc+qi1
+vyU9DSZQeV5gK/NjzlwWaPia76sMg/Er+XDfte7TSI/+ttwIDorp2AyOnRMnNm8y
+dTY/LC0WoOnDplyFwyLnXuR4x8vCxxQL/tjzSU/Q8YDXBMNpijG4yVSGpNN90ugF
+nvd4D1jOCoF5n14/GhQWT0C0m2U6VHrSYZsQ7B4NR9yXVuIq1qsNblIlhtVMWNZ0
++wJXpzNmd19UoACQN35uxsUxwSKr92gdTT3ZjLexn2CGc4HDmwKryJNdD1yZ6PFS
+uy79K8vdAgMBAAECggEAILEJH8fTEgFzOK7vVJHAJSuAgGl2cYz6Uboa4pyg+W5S
+DwupX0hWXK70tXr9RGfNLVwsHhcdWNFWwG0wELQdXu2AFWjYQ7YqJbuzDPMXF3EU
+ruHOn95igI1hHvJ7a3rKshA6YWI+myN0jFHTJ2JGEq9R2Nov0LspkhvypXgNvA/r
+JfFZ9IsPJZDWCnGXkPLlW2X1XEXw2BPs8ib+ZkbzGNiLsy/i4M/oA+g6lz4LU/ll
+J6cLhwPrBu02+PJt7MaYaNk5zqhyJs0AMjeBlNnXFIWAlTrIe/h8z/gL8ABrYWAA
+1kgZ11GO8bNAEfLOIUrA1/vq9aK00WDwFLXWJdVE4QKBgQD+R/J+AbYSImeoAj/3
+hfsFkaUNLyw1ZEO4LG2id0dnve1paL6Y/uXKKqxq0jiyMLT243Vi+1fzth7RNXOl
+ui0nnVWO7x68FsYcdIM7w+tryh2Y+UhCfwNCakM0GTohcXqFUEzHcwuOv8hAfRQ5
+jPBCwJdUHpIimVOo5/WRbQGW+wKBgQD0ANkof+jagdNqOkCvFnTPiFlPYrpDzeU5
+ZxhLlVxnr6G2MPoUO0IqTWVA7uCn29i0yUUXAtRHrkNI1EtKXRIUe2bChVegTBHx
+26PqXEOonSUJdpUzyzXVX2vSqICm0tTbqyZ0GbjP4y5qQOQHdTGFsHDfSTa5//P+
+0BLpci4RBwKBgQDBR8DrxLM3b41o6GTk6aNXpVBXCC9LWi4bVTH0l0PgeD54rBSM
+SNwz4mHyRF6yG1HChDybAz/kUN912HJSW4StIuuA3QN4prrpsCp8iDxvT09WEs25
+NcAtgIYamL5V42Lk6Jej1y/GzsIROsHfyOBrbObaGu6re+5aag5//uKBdwKBgQDp
+i4ZPBV7TBkBdBLS04UGdAly5Zz3xeDlW4B6Y+bUgaTLXN7mlc7K42qt3oyzUfdDF
++X9vrv2QPnOYWdpWqw6LHDIXLZnZi/YBEMGrp/P6h67Th/T3RiGYwWRqlW3OPy4N
+s5tytMv37vKWMNYRbVKhK2hdz63aCep4kqAHYYpGMQKBgF83LTyRFwGFos/wDrgY
+eieLiipmdXGvlrBq6SBzKglIYwNRSGiWkXAuHRzD/2S546ioQKZr7AKuijKGdLMz
+ABVl/bqqqRXSDbvf+XEdU2rJpxhYWxlsJZMFBFIwuxR2jRqmCgbCvoZQcbIr1ZLr
+02eC2pQ5eio2+CKqBfqxbnwk
+-----END PRIVATE KEY-----
+
diff --git a/src/bridge/mock-pmda.c b/src/bridge/mock-pmda.c
new file mode 100644
index 0000000..2068e91
--- /dev/null
+++ b/src/bridge/mock-pmda.c
@@ -0,0 +1,197 @@
+/*
+ * Trivial, configurable PMDA
+ *
+ * Copyright (c) 2014 Red Hat.
+ * Copyright (c) 1995,2004 Silicon Graphics, Inc. All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "config.h"
+
+#include <pcp/pmapi.h>
+#include <pcp/pmda.h>
+
+#if PM_VERSION_CURRENT < PM_VERSION(4,0,0)
+#include <pcp/impl.h>
+#define pmID_cluster(pmid) pmid_cluster(pmid)
+#define pmID_item(pmid) pmid_item(pmid)
+#endif
+
+static pmdaInstid inst_values[] = {
+ { 1, "red" }, { 2, "green" }, { 3, "blue" }
+};
+
+static pmdaIndom indomtab[] = {
+#define VALUES_INDOM 0
+ { VALUES_INDOM, sizeof(inst_values)/sizeof(inst_values[0]), inst_values },
+#define INSTANCES_INDOM 1
+ { INSTANCES_INDOM, 0, NULL }
+};
+
+static pmdaMetric metrictab[] = {
+ /* value */
+ { NULL,
+ { PMDA_PMID(0,0), PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_INSTANT,
+ PMDA_PMUNITS(0, 0, 0, 0, 0, 0) } },
+ /* values */
+ { NULL,
+ { PMDA_PMID(0,1), PM_TYPE_U32, VALUES_INDOM, PM_SEM_INSTANT,
+ PMDA_PMUNITS(0, 0, 0, 0, 0, 0) } },
+ /* instances */
+ { NULL,
+ { PMDA_PMID(0,2), PM_TYPE_U32, INSTANCES_INDOM, PM_SEM_INSTANT,
+ PMDA_PMUNITS(0, 0, 0, 0, 0, 0) } },
+ /* seconds */
+ { NULL,
+ { PMDA_PMID(0,3), PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_INSTANT,
+ PMDA_PMUNITS(0, 1, 0, 0, PM_TIME_SEC, 0) } },
+ /* string */
+ { NULL,
+ { PMDA_PMID(0,4), PM_TYPE_STRING, PM_INDOM_NULL, PM_SEM_INSTANT,
+ PMDA_PMUNITS(0, 0, 0, 0, 0, 0) } },
+ /* counter */
+ { NULL,
+ { PMDA_PMID(0,5), PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_COUNTER,
+ PMDA_PMUNITS(0, 0, 0, 0, 0, 0) } },
+ /* counter64 */
+ { NULL,
+ { PMDA_PMID(0,6), PM_TYPE_U64, PM_INDOM_NULL, PM_SEM_COUNTER,
+ PMDA_PMUNITS(0, 0, 0, 0, 0, 0) } }
+
+};
+
+static unsigned int values[4] = { 0, 0, 0 };
+
+static pmInDom instances_indom;
+
+static const char *string_value = "foobar";
+
+static int counter = 0;
+static int64_t counter64 = INT64_MAX - 100;
+
+static int
+mock_fetchCallBack(pmdaMetric *mdesc, unsigned int inst, pmAtomValue *atom)
+{
+ if (pmID_cluster(mdesc->m_desc.pmid) != 0)
+ return PM_ERR_PMID;
+
+ switch (pmID_item(mdesc->m_desc.pmid)) {
+ case 0:
+ if (inst != PM_IN_NULL)
+ return PM_ERR_INST;
+ atom->ul = values[0];
+ break;
+ case 1:
+ if (inst < 1 || inst > 3)
+ return PM_ERR_INST;
+ atom->ul = values[inst];
+ break;
+ case 2: {
+ void *val;
+ if (pmdaCacheLookup(instances_indom, inst, NULL, &val) != PMDA_CACHE_ACTIVE)
+ return PM_ERR_INST;
+ atom->ul = (intptr_t)val;
+ } break;
+ case 3:
+ if (inst != PM_IN_NULL)
+ return PM_ERR_INST;
+ atom->ul = 60;
+ break;
+ case 4:
+ if (inst != PM_IN_NULL)
+ return PM_ERR_INST;
+ atom->cp = (char *)string_value;
+ break;
+ case 5:
+ if (inst != PM_IN_NULL)
+ return PM_ERR_INST;
+ atom->ul = counter;
+ break;
+ case 6:
+ if (inst != PM_IN_NULL)
+ return PM_ERR_INST;
+ atom->ull = counter64;
+ break;
+ default:
+ return PM_ERR_PMID;
+ }
+ return 0;
+}
+
+void mock_control (const char *cmd, ...);
+
+void
+mock_control (const char *cmd, ...)
+{
+ va_list ap;
+ va_start (ap, cmd);
+
+ if (strcmp (cmd, "reset") == 0)
+ {
+ values[0] = values[1] = values[2] = values[3] = 0;
+ pmdaCacheOp (instances_indom, PMDA_CACHE_CULL);
+ string_value = "foobar";
+ counter = 0;
+ counter64 = INT64_MAX - 100;
+ }
+ else if (strcmp (cmd, "set-value") == 0)
+ {
+ int i = va_arg (ap, int);
+ int v = va_arg (ap, int);
+ values[i] = v;
+ }
+ else if (strcmp (cmd, "add-instance") == 0)
+ {
+ const char *n = va_arg (ap, const char *);
+ intptr_t val = va_arg (ap, int);
+ pmdaCacheStore (instances_indom, PMDA_CACHE_ADD, n, (void *)val);
+ }
+ else if (strcmp (cmd, "del-instance") == 0)
+ {
+ const char *n = va_arg (ap, const char *);
+ pmdaCacheStore (instances_indom, PMDA_CACHE_HIDE, n, NULL);
+ }
+ else if (strcmp (cmd, "set-string") == 0)
+ {
+ const char *n = va_arg (ap, const char *);
+ string_value = n;
+ }
+ else if (strcmp (cmd, "inc-counter") == 0)
+ {
+ int val = va_arg (ap, int);
+ counter += val;
+ }
+ else if (strcmp (cmd, "inc-counter64") == 0)
+ {
+ int val = va_arg (ap, int);
+ counter64 += val;
+ }
+ va_end(ap);
+}
+
+void mock_init (pmdaInterface *dp);
+
+void
+mock_init (pmdaInterface *dp)
+{
+ pmdaDSO(dp, PMDA_INTERFACE_2, "mock-pmda", NULL);
+
+ if (dp->status != 0)
+ return;
+
+ pmdaSetFetchCallBack(dp, mock_fetchCallBack);
+ pmdaInit(dp,
+ indomtab, sizeof(indomtab)/sizeof(indomtab[0]),
+ metrictab, sizeof(metrictab)/sizeof(metrictab[0]));
+
+ instances_indom = indomtab[INSTANCES_INDOM].it_indom;
+}
diff --git a/src/bridge/mock-pmns b/src/bridge/mock-pmns
new file mode 100644
index 0000000..6c0838d
--- /dev/null
+++ b/src/bridge/mock-pmns
@@ -0,0 +1,13 @@
+root {
+ mock
+}
+
+mock {
+ value 333:0:0
+ values 333:0:1
+ instances 333:0:2
+ seconds 333:0:3
+ string 333:0:4
+ counter 333:0:5
+ counter64 333:0:6
+}
diff --git a/src/bridge/mock-resource/bad-bridges/cockpit/broken-bridge/manifest.json b/src/bridge/mock-resource/bad-bridges/cockpit/broken-bridge/manifest.json
new file mode 100644
index 0000000..65186e7
--- /dev/null
+++ b/src/bridge/mock-resource/bad-bridges/cockpit/broken-bridge/manifest.json
@@ -0,0 +1,6 @@
+{
+ "priority": 1,
+ "bridges": [
+ [ "broken: bridge should not be array" ]
+ ]
+}
diff --git a/src/bridge/mock-resource/bad-bridges/cockpit/broken-bridges/manifest.json b/src/bridge/mock-resource/bad-bridges/cockpit/broken-bridges/manifest.json
new file mode 100644
index 0000000..11ba36a
--- /dev/null
+++ b/src/bridge/mock-resource/bad-bridges/cockpit/broken-bridges/manifest.json
@@ -0,0 +1,4 @@
+{
+ "priority": 10,
+ "bridges": "broken: bridges should be an array"
+}
diff --git a/src/bridge/mock-resource/bad-bridges/cockpit/broken-environ/manifest.json b/src/bridge/mock-resource/bad-bridges/cockpit/broken-environ/manifest.json
new file mode 100644
index 0000000..67d0c47
--- /dev/null
+++ b/src/bridge/mock-resource/bad-bridges/cockpit/broken-environ/manifest.json
@@ -0,0 +1,9 @@
+{
+ "priority": 27,
+ "bridges": [
+ {
+ "match": { },
+ "environ": { "broken": "this should be an array" }
+ }
+ ]
+}
diff --git a/src/bridge/mock-resource/bad-bridges/cockpit/broken-match/manifest.json b/src/bridge/mock-resource/bad-bridges/cockpit/broken-match/manifest.json
new file mode 100644
index 0000000..ba9086b
--- /dev/null
+++ b/src/bridge/mock-resource/bad-bridges/cockpit/broken-match/manifest.json
@@ -0,0 +1,8 @@
+{
+ "priority": 20,
+ "bridges": [
+ {
+ "match": "broken: match should be an object"
+ }
+ ]
+}
diff --git a/src/bridge/mock-resource/bad-bridges/cockpit/broken-problem/manifest.json b/src/bridge/mock-resource/bad-bridges/cockpit/broken-problem/manifest.json
new file mode 100644
index 0000000..1287c72
--- /dev/null
+++ b/src/bridge/mock-resource/bad-bridges/cockpit/broken-problem/manifest.json
@@ -0,0 +1,9 @@
+{
+ "priority": 30,
+ "bridges": [
+ {
+ "match": { },
+ "problem": [ "broken: problem should be a string" ]
+ }
+ ]
+}
diff --git a/src/bridge/mock-resource/bad-bridges/cockpit/broken-spawn/manifest.json b/src/bridge/mock-resource/bad-bridges/cockpit/broken-spawn/manifest.json
new file mode 100644
index 0000000..4c2d7d4
--- /dev/null
+++ b/src/bridge/mock-resource/bad-bridges/cockpit/broken-spawn/manifest.json
@@ -0,0 +1,9 @@
+{
+ "priority": 25,
+ "bridges": [
+ {
+ "match": { },
+ "spawn": { "broken": "this should be an array" }
+ }
+ ]
+}
diff --git a/src/bridge/mock-resource/bad-bridges/cockpit/missing-match/manifest.json b/src/bridge/mock-resource/bad-bridges/cockpit/missing-match/manifest.json
new file mode 100644
index 0000000..eaf7b60
--- /dev/null
+++ b/src/bridge/mock-resource/bad-bridges/cockpit/missing-match/manifest.json
@@ -0,0 +1,8 @@
+{
+ "priority": 40,
+ "bridges": [
+ {
+ "broken": "no match field"
+ }
+ ]
+}
diff --git a/src/bridge/mock-resource/bad-directory/cockpit/ok/file.txt b/src/bridge/mock-resource/bad-directory/cockpit/ok/file.txt
new file mode 100644
index 0000000..c5d7110
--- /dev/null
+++ b/src/bridge/mock-resource/bad-directory/cockpit/ok/file.txt
@@ -0,0 +1 @@
+This is a good package
diff --git a/src/bridge/mock-resource/bad-directory/cockpit/ok/manifest.json b/src/bridge/mock-resource/bad-directory/cockpit/ok/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/bad-directory/cockpit/ok/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/bad-directory/cockpit/package/+dirname/file.txt b/src/bridge/mock-resource/bad-directory/cockpit/package/+dirname/file.txt
new file mode 100644
index 0000000..d4a7a55
--- /dev/null
+++ b/src/bridge/mock-resource/bad-directory/cockpit/package/+dirname/file.txt
@@ -0,0 +1 @@
+This file is in a directory with a bad name
diff --git a/src/bridge/mock-resource/bad-directory/cockpit/package/manifest.json b/src/bridge/mock-resource/bad-directory/cockpit/package/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/bad-directory/cockpit/package/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/bad-file/cockpit/ok/file.txt b/src/bridge/mock-resource/bad-file/cockpit/ok/file.txt
new file mode 100644
index 0000000..c5d7110
--- /dev/null
+++ b/src/bridge/mock-resource/bad-file/cockpit/ok/file.txt
@@ -0,0 +1 @@
+This is a good package
diff --git a/src/bridge/mock-resource/bad-file/cockpit/ok/manifest.json b/src/bridge/mock-resource/bad-file/cockpit/ok/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/bad-file/cockpit/ok/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/bad-file/cockpit/package/+file.txt b/src/bridge/mock-resource/bad-file/cockpit/package/+file.txt
new file mode 100644
index 0000000..e31d79e
--- /dev/null
+++ b/src/bridge/mock-resource/bad-file/cockpit/package/+file.txt
@@ -0,0 +1 @@
+This file has a bad name
diff --git a/src/bridge/mock-resource/bad-file/cockpit/package/.txt b/src/bridge/mock-resource/bad-file/cockpit/package/.txt
new file mode 100644
index 0000000..e31d79e
--- /dev/null
+++ b/src/bridge/mock-resource/bad-file/cockpit/package/.txt
@@ -0,0 +1 @@
+This file has a bad name
diff --git a/src/bridge/mock-resource/bad-file/cockpit/package/manifest.json b/src/bridge/mock-resource/bad-file/cockpit/package/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/bad-file/cockpit/package/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/bad-package/cockpit/%%module/manifest.json b/src/bridge/mock-resource/bad-package/cockpit/%%module/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/bad-package/cockpit/%%module/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/bad-package/cockpit/ok/file.txt b/src/bridge/mock-resource/bad-package/cockpit/ok/file.txt
new file mode 100644
index 0000000..c5d7110
--- /dev/null
+++ b/src/bridge/mock-resource/bad-package/cockpit/ok/file.txt
@@ -0,0 +1 @@
+This is a good package
diff --git a/src/bridge/mock-resource/bad-package/cockpit/ok/manifest.json b/src/bridge/mock-resource/bad-package/cockpit/ok/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/bad-package/cockpit/ok/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/config-override/second.override.json b/src/bridge/mock-resource/config-override/second.override.json
new file mode 100644
index 0000000..11cebbb
--- /dev/null
+++ b/src/bridge/mock-resource/config-override/second.override.json
@@ -0,0 +1,4 @@
+{
+ "description": "overridden second description",
+ "note": "an extra field"
+}
diff --git a/src/bridge/mock-resource/csp/cockpit/strip/manifest.json b/src/bridge/mock-resource/csp/cockpit/strip/manifest.json
new file mode 100644
index 0000000..6a38097
--- /dev/null
+++ b/src/bridge/mock-resource/csp/cockpit/strip/manifest.json
@@ -0,0 +1,3 @@
+{
+ "content-security-policy": "img-src 'self'; default-src 'self'"
+}
diff --git a/src/bridge/mock-resource/csp/cockpit/strip/test.html b/src/bridge/mock-resource/csp/cockpit/strip/test.html
new file mode 100644
index 0000000..fb55639
--- /dev/null
+++ b/src/bridge/mock-resource/csp/cockpit/strip/test.html
@@ -0,0 +1,6 @@
+<html>
+<head>
+<title>Test</title>
+</head>
+<body>Test</body>
+</html>
diff --git a/src/bridge/mock-resource/glob/cockpit/a/file.txt b/src/bridge/mock-resource/glob/cockpit/a/file.txt
new file mode 100644
index 0000000..7898192
--- /dev/null
+++ b/src/bridge/mock-resource/glob/cockpit/a/file.txt
@@ -0,0 +1 @@
+a
diff --git a/src/bridge/mock-resource/glob/cockpit/a/manifest.json b/src/bridge/mock-resource/glob/cockpit/a/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/glob/cockpit/a/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/glob/cockpit/b/file.txt.gz b/src/bridge/mock-resource/glob/cockpit/b/file.txt.gz
new file mode 100644
index 0000000..4054685
--- /dev/null
+++ b/src/bridge/mock-resource/glob/cockpit/b/file.txt.gz
Binary files differ
diff --git a/src/bridge/mock-resource/glob/cockpit/b/manifest.json b/src/bridge/mock-resource/glob/cockpit/b/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/glob/cockpit/b/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/glob/cockpit/c/manifest.json b/src/bridge/mock-resource/glob/cockpit/c/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/glob/cockpit/c/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/gzip/cockpit/package/file.txt.gz b/src/bridge/mock-resource/gzip/cockpit/package/file.txt.gz
new file mode 100644
index 0000000..4218fca
--- /dev/null
+++ b/src/bridge/mock-resource/gzip/cockpit/package/file.txt.gz
Binary files differ
diff --git a/src/bridge/mock-resource/gzip/cockpit/package/manifest.json b/src/bridge/mock-resource/gzip/cockpit/package/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/gzip/cockpit/package/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/home/cockpit/another-renamed/manifest.json b/src/bridge/mock-resource/home/cockpit/another-renamed/manifest.json
new file mode 100644
index 0000000..3d632b2
--- /dev/null
+++ b/src/bridge/mock-resource/home/cockpit/another-renamed/manifest.json
@@ -0,0 +1,10 @@
+{
+ "name": "another",
+ "description": "another",
+ "bridges": [
+ {
+ "match": { "host": null },
+ "problem": "not-supported"
+ }
+ ]
+}
diff --git a/src/bridge/mock-resource/home/cockpit/another-renamed/test-file.min.txt.gz b/src/bridge/mock-resource/home/cockpit/another-renamed/test-file.min.txt.gz
new file mode 100644
index 0000000..bea8ed4
--- /dev/null
+++ b/src/bridge/mock-resource/home/cockpit/another-renamed/test-file.min.txt.gz
Binary files differ
diff --git a/src/bridge/mock-resource/home/cockpit/another-renamed/test.de.html b/src/bridge/mock-resource/home/cockpit/another-renamed/test.de.html
new file mode 100644
index 0000000..ae594a7
--- /dev/null
+++ b/src/bridge/mock-resource/home/cockpit/another-renamed/test.de.html
@@ -0,0 +1,6 @@
+<html>
+<head>
+<title>Im Home-Verzeichnis</title>
+</head>
+<body>Im Home-Verzeichnis</body>
+</html>
diff --git a/src/bridge/mock-resource/home/cockpit/another-renamed/test.html b/src/bridge/mock-resource/home/cockpit/another-renamed/test.html
new file mode 100644
index 0000000..d419dc2
--- /dev/null
+++ b/src/bridge/mock-resource/home/cockpit/another-renamed/test.html
@@ -0,0 +1,6 @@
+<html>
+<head>
+<title>In home dir</title>
+</head>
+<body>In home dir</body>
+</html>
diff --git a/src/bridge/mock-resource/home/cockpit/another-renamed/test.pig.html b/src/bridge/mock-resource/home/cockpit/another-renamed/test.pig.html
new file mode 100644
index 0000000..5f7b001
--- /dev/null
+++ b/src/bridge/mock-resource/home/cockpit/another-renamed/test.pig.html
@@ -0,0 +1,6 @@
+<html>
+<head>
+<title>Inlay omehay irday</title>
+</head>
+<body>Inlay omehay irday</body>
+</html>
diff --git a/src/bridge/mock-resource/home/cockpit/another-renamed/test.pig_PEN.html b/src/bridge/mock-resource/home/cockpit/another-renamed/test.pig_PEN.html
new file mode 100644
index 0000000..6e2cbd5
--- /dev/null
+++ b/src/bridge/mock-resource/home/cockpit/another-renamed/test.pig_PEN.html
@@ -0,0 +1,6 @@
+<html>
+<head>
+<title>Inway omeha irda</title>
+</head>
+<body>Inway omeha irda</body>
+</html>
diff --git a/src/bridge/mock-resource/home/cockpit/incompatible/manifest.json b/src/bridge/mock-resource/home/cockpit/incompatible/manifest.json
new file mode 100644
index 0000000..addcf78
--- /dev/null
+++ b/src/bridge/mock-resource/home/cockpit/incompatible/manifest.json
@@ -0,0 +1,6 @@
+{
+ "description": "incompatible package",
+ "requires": {
+ "cockpit": "999.5"
+ }
+}
diff --git a/src/bridge/mock-resource/home/cockpit/incompatible/test.html b/src/bridge/mock-resource/home/cockpit/incompatible/test.html
new file mode 100644
index 0000000..7455a4b
--- /dev/null
+++ b/src/bridge/mock-resource/home/cockpit/incompatible/test.html
@@ -0,0 +1,6 @@
+<html>
+<head>
+<title>In incompatible dir</title>
+</head>
+<body>In incompatible dir</body>
+</html>
diff --git a/src/bridge/mock-resource/home/cockpit/requires/manifest.json b/src/bridge/mock-resource/home/cockpit/requires/manifest.json
new file mode 100644
index 0000000..a0c1198
--- /dev/null
+++ b/src/bridge/mock-resource/home/cockpit/requires/manifest.json
@@ -0,0 +1,6 @@
+{
+ "description": "requires package",
+ "requires": {
+ "unknown": "requirement"
+ }
+}
diff --git a/src/bridge/mock-resource/home/cockpit/requires/test.html b/src/bridge/mock-resource/home/cockpit/requires/test.html
new file mode 100644
index 0000000..f8e1582
--- /dev/null
+++ b/src/bridge/mock-resource/home/cockpit/requires/test.html
@@ -0,0 +1,6 @@
+<html>
+<head>
+<title>In requires dir</title>
+</head>
+<body>In requires dir</body>
+</html>
diff --git a/src/bridge/mock-resource/reload.new/cockpit/new/manifest.json b/src/bridge/mock-resource/reload.new/cockpit/new/manifest.json
new file mode 100644
index 0000000..ffcd441
--- /dev/null
+++ b/src/bridge/mock-resource/reload.new/cockpit/new/manifest.json
@@ -0,0 +1 @@
+{ }
diff --git a/src/bridge/mock-resource/reload.new/cockpit/old/manifest.json b/src/bridge/mock-resource/reload.new/cockpit/old/manifest.json
new file mode 100644
index 0000000..1eb1875
--- /dev/null
+++ b/src/bridge/mock-resource/reload.new/cockpit/old/manifest.json
@@ -0,0 +1,2 @@
+{ }
+
diff --git a/src/bridge/mock-resource/reload.old/cockpit/old/manifest.json b/src/bridge/mock-resource/reload.old/cockpit/old/manifest.json
new file mode 100644
index 0000000..1eb1875
--- /dev/null
+++ b/src/bridge/mock-resource/reload.old/cockpit/old/manifest.json
@@ -0,0 +1,2 @@
+{ }
+
diff --git a/src/bridge/mock-resource/reload.updated/cockpit/old/manifest.json b/src/bridge/mock-resource/reload.updated/cockpit/old/manifest.json
new file mode 100644
index 0000000..3650fef
--- /dev/null
+++ b/src/bridge/mock-resource/reload.updated/cockpit/old/manifest.json
@@ -0,0 +1,3 @@
+{
+ "version": "2"
+}
diff --git a/src/bridge/mock-resource/system/cockpit/another/manifest.json b/src/bridge/mock-resource/system/cockpit/another/manifest.json
new file mode 100644
index 0000000..ddd3409
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/another/manifest.json
@@ -0,0 +1,3 @@
+{
+ "description": "dummy"
+}
diff --git a/src/bridge/mock-resource/system/cockpit/another/test.html b/src/bridge/mock-resource/system/cockpit/another/test.html
new file mode 100644
index 0000000..54265d0
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/another/test.html
@@ -0,0 +1,6 @@
+<html>
+<head>
+<title>In system dir</title>
+</head>
+<body>In system dir (overridden by same package in home)</body>
+</html>
diff --git a/src/bridge/mock-resource/system/cockpit/second/manifest.json b/src/bridge/mock-resource/system/cockpit/second/manifest.json
new file mode 100644
index 0000000..34e8976
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/second/manifest.json
@@ -0,0 +1,10 @@
+{
+ "description": "second dummy description",
+ "priority": 2,
+ "bridges": [
+ {
+ "match": { "second": null },
+ "problem": "never-a-second"
+ }
+ ]
+}
diff --git a/src/bridge/mock-resource/system/cockpit/second/test.html b/src/bridge/mock-resource/system/cockpit/second/test.html
new file mode 100644
index 0000000..e80ef9d
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/second/test.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>In system dir</title>
+</head>
+<body>
+A second package
+Depend on @@test@@
+</body>
+</html>
diff --git a/src/bridge/mock-resource/system/cockpit/static/branding.css b/src/bridge/mock-resource/system/cockpit/static/branding.css
new file mode 100644
index 0000000..1c66706
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/static/branding.css
@@ -0,0 +1,3 @@
+#badge {
+ background-image: url("logo.png");
+}
diff --git a/src/bridge/mock-resource/system/cockpit/test-priority/_modules/@testorg/toolkit.js b/src/bridge/mock-resource/system/cockpit/test-priority/_modules/@testorg/toolkit.js
new file mode 100644
index 0000000..803f4c9
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/test-priority/_modules/@testorg/toolkit.js
@@ -0,0 +1,2 @@
+/* global the */
+the.code();
diff --git a/src/bridge/mock-resource/system/cockpit/test-priority/manifest.json b/src/bridge/mock-resource/system/cockpit/test-priority/manifest.json
new file mode 100644
index 0000000..a6fbf69
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/test-priority/manifest.json
@@ -0,0 +1,17 @@
+{
+ "name": "test",
+ "priority": 15,
+ "overridden": "hello",
+ "description": "this gets loaded instead of package in test/",
+ "bridges": [
+ {
+ "match": { "blah": "test*" },
+ "spawn": [ "/usr/bin/cat" ],
+ "environ": [ "TEST_ENV=test" ]
+ },
+ {
+ "match": { "blah": "marmalade*" },
+ "problem": "bogus-channel"
+ }
+ ]
+}
diff --git a/src/bridge/mock-resource/system/cockpit/test-priority/override.json b/src/bridge/mock-resource/system/cockpit/test-priority/override.json
new file mode 100644
index 0000000..7558e88
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/test-priority/override.json
@@ -0,0 +1,4 @@
+{
+ "description": "dummy",
+ "overridden": null
+}
diff --git a/src/bridge/mock-resource/system/cockpit/test-priority/sub/COPYING b/src/bridge/mock-resource/system/cockpit/test-priority/sub/COPYING
new file mode 100644
index 0000000..4362b49
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/test-priority/sub/COPYING
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/src/bridge/mock-resource/system/cockpit/test-priority/sub/file.ext b/src/bridge/mock-resource/system/cockpit/test-priority/sub/file.ext
new file mode 100644
index 0000000..df62c09
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/test-priority/sub/file.ext
@@ -0,0 +1,2 @@
+These are the contents of file.ext
+Oh marmalaaade
diff --git a/src/bridge/mock-resource/system/cockpit/test-priority/sub/file.min.ext b/src/bridge/mock-resource/system/cockpit/test-priority/sub/file.min.ext
new file mode 100644
index 0000000..37f53f3
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/test-priority/sub/file.min.ext
@@ -0,0 +1 @@
+This is the minified file.ext Oh marmalaaade
diff --git a/src/bridge/mock-resource/system/cockpit/test/manifest.json b/src/bridge/mock-resource/system/cockpit/test/manifest.json
new file mode 100644
index 0000000..e73f284
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/test/manifest.json
@@ -0,0 +1,4 @@
+{
+ "priority": 1,
+ "description": "the test package in priority-high has a higher priority and therefore gets loaded"
+}
diff --git a/src/bridge/mock-resource/system/cockpit/test/sub/COPYING b/src/bridge/mock-resource/system/cockpit/test/sub/COPYING
new file mode 100644
index 0000000..4362b49
--- /dev/null
+++ b/src/bridge/mock-resource/system/cockpit/test/sub/COPYING
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/src/bridge/mock-server.crt b/src/bridge/mock-server.crt
new file mode 100644
index 0000000..f269c76
--- /dev/null
+++ b/src/bridge/mock-server.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIICxzCCAa+gAwIBAgIJAOxdyg+THvirMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
+BAMMCWxvY2FsaG9zdDAgFw0xNTA1MTUxOTU0MDhaGA8yMTE1MDQyMTE5NTQwOFow
+FDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAlSBlBu6zLj0UV9V6TMtIrMXyO+V85nLV/pwyIbiFtNeczeBC3IRWU3lM
+E0HyQ2kYls6KV9fxT0PS8+Rp9I2R4SiJ7bPjrzEonSqwVxmrsAoArn175/czxZeN
++9+nhaikOV2sTcfV2JK8cfwpHpgTMOEq4xYJYtEoZJkEHEWVtsiWGKSkqs/6uDIK
+k6hrnTKPFUjT7YTSMXHTqWBoAj30BiItUWl39QOYWk1sKsptGSGdHcEMuXTs0vrV
+C7wwYu5VEyyZeB8pc+nd7MTX5soIk2Friin+5jxHfNIHcUjYdLJc4EzVwfw/lP2t
+BU+vNGiKxTovjOepTv2wbsD6b9pAyQIDAQABoxowGDAJBgNVHRMEAjAAMAsGA1Ud
+DwQEAwIF4DANBgkqhkiG9w0BAQsFAAOCAQEAHT8ePOaGZa2SNPKkCiLjYGvSQyxZ
+4GNpaHVRGtB4gd9kZf5Uzw1OsYczC5Ai4+84mKGsq8zHPz3LKvdZh4mdgFOoG+0a
+LF+sjOr5Rt9cAZQyT92W9Cn0kMb8n6fHLnKw1BdntXXDM59rrvMduy7pzUMZh6nK
+qmqzF/SsShpvdXjKO41mgF/Tapz2OT8fsmxF8V17bC8FqxSgTots90kpNw8MGPwq
+9fcY0kJuW+ctIstMATkGqaYBBY73JMMRqj0JOLqjhyF75kevrpcANQLOToEScG9a
+hCIHUez984SILn2w32z6v304GAXrv2mfY+ZItbBF4CMRG6zyoThjjnm5rw==
+-----END CERTIFICATE-----
+
diff --git a/src/bridge/mock-server.key b/src/bridge/mock-server.key
new file mode 100644
index 0000000..70dc88e
--- /dev/null
+++ b/src/bridge/mock-server.key
@@ -0,0 +1,29 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVIGUG7rMuPRRX
+1XpMy0isxfI75XzmctX+nDIhuIW015zN4ELchFZTeUwTQfJDaRiWzopX1/FPQ9Lz
+5Gn0jZHhKInts+OvMSidKrBXGauwCgCufXvn9zPFl43736eFqKQ5XaxNx9XYkrxx
+/CkemBMw4SrjFgli0ShkmQQcRZW2yJYYpKSqz/q4MgqTqGudMo8VSNPthNIxcdOp
+YGgCPfQGIi1RaXf1A5haTWwqym0ZIZ0dwQy5dOzS+tULvDBi7lUTLJl4Hylz6d3s
+xNfmygiTYWuKKf7mPEd80gdxSNh0slzgTNXB/D+U/a0FT680aIrFOi+M56lO/bBu
+wPpv2kDJAgMBAAECggEAWC2vj0nV6aF0RfuVNZN+hasGcSRhlmKQ5ty1R/s0FJrp
+vVK+NmaKJRG0Si7UccfTIlk+tHKb3aLdOhn2DQUpHVo7NLMKDq2itkxnuRkUzCMe
+BA9vFNq3Yj8bVbQJ+p6dfM5G9DcV29noU/4m0lBaCK0cDusY4bWLjPBt3cnHnSxX
+gTq2dUDTcrXA6feIQKeWOItpfHLFTosmkaIQiiHR7llQqYn0r6b7TT3mH4gITuTC
+4a66AvMqZJtr+8KLwQuMHC0Fsyv2ZuHdID+s3p/v+Lk4qPcf1Jmhe/8Ii8O+r/i3
+GDidGKhwC/EMW6UAJRBtTombTw2OiUxcCGiYAHyTgQKBgQDE06OflktN3Q/OeWFa
+I8oN/zWiqAyhF+Qaea4WFtbMRaBeggZq8UbixYiemS30Pd7AfsrlqOhj7qsvXfvC
+gNnVjkwsJOacbMC5Zq4o+AvtzDKNCmdvc69Uxh9S+yC2G73uV4FoFdfGoqbkDCaB
+Dr1Lr51db38bGAX+hVDX4vEJRwKBgQDB9Zw9NLU9HC7J825EeOF7ls3XYFdp9N+a
++l4rk41xojQFVNAkhZAIUIhtZpq6qRL4D07Bel7/HcW1KzmCXWGSEiJevM1PYys6
+6vIGdimbxx9Sim/YuWlJqm8HQdxisV/8RNU5IdptL2zSMNrHw+UMJRq/WWLdICup
+vdwnxnxtbwKBgDVqootbBJDbH8EPXAZUlC98RZghN1w42aT9xOslw6Goe79qVkeL
+t4svxMYWGEyixeNEBdiSII+OZFL0lLDSu0uj1LReR1/Ie/1VCor/1mzw7/8rBatP
+oBPHuRSSJuyFVuRqRypyUx8LYXOegIyh7Wmu/WwwKDS4LlhG4gvVAUldAoGBAKjv
+vmfpErrpYy9vIentx6eX0C35semN6XzY+VDHMfl5qAnEt4437x3u1wVCyx4tyWHa
+I2SNq1RVoDCON4b+ws8xkFIn7ENrQuoMXUWXgeI262c/QO/pfnU1R4Nwr/4eNFZL
+tdQPUmqrVgM1njlEvKl5X/SubFfhf2ZXAfx6+0FtAoGAYpV3BPPXK2gBbKjxiORK
+dr2JcsoEsYbHvL+h3pF4MqtG6A4SeNi7fKpIHgCIPsNe0h7V4e9G+lRGnDg1BECW
+lfh3+Jg+0vU3OQqlz5M9UIT0VMqAqyIKCpGe/aaXtw5EeUwvSk+qFaeKQD2dZXqx
+FcHlDw9RlEEzXtkGvUrTqxg=
+-----END PRIVATE KEY-----
+
diff --git a/src/bridge/org.cockpit-project.cockpit-bridge.policy.in b/src/bridge/org.cockpit-project.cockpit-bridge.policy.in
new file mode 100644
index 0000000..8267f3b
--- /dev/null
+++ b/src/bridge/org.cockpit-project.cockpit-bridge.policy.in
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
+<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
+
+<policyconfig>
+ <vendor>Cockpit Project</vendor>
+ <vendor_url>https://www.cockpit-project.org</vendor_url>
+
+ <action id="org.cockpit-project.cockpit.root-bridge">
+ <description gettext-domain="cockpit">Administration with Cockpit Web Console</description>
+ <message gettext-domain="cockpit">Authentication is required to perform privileged tasks with the Cockpit Web Console</message>
+ <defaults>
+ <allow_any>auth_admin</allow_any>
+ <allow_inactive>auth_admin</allow_inactive>
+ <allow_active>auth_admin</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.exec.path">/usr/bin/cockpit-bridge</annotate>
+ </action>
+</policyconfig>
diff --git a/src/bridge/pmlogconf/cockpit b/src/bridge/pmlogconf/cockpit
new file mode 100644
index 0000000..9cd37e6
--- /dev/null
+++ b/src/bridge/pmlogconf/cockpit
@@ -0,0 +1,9 @@
+#pmlogconf-setup 2.0
+ident Metrics used by Cockpit
+force include
+ kernel.all.cpu.nice
+ kernel.all.cpu.user
+ kernel.all.cpu.sys
+ mem.util.used
+ network.interface.total.bytes
+ disk.dev.total_bytes
diff --git a/src/bridge/test-bridge.c b/src/bridge/test-bridge.c
new file mode 100644
index 0000000..fe3cb96
--- /dev/null
+++ b/src/bridge/test-bridge.c
@@ -0,0 +1,309 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitjson.h"
+#include "common/cockpitpipe.h"
+#include "common/cockpitpipetransport.h"
+#include "common/cockpitsystem.h"
+#include "testlib/cockpittest.h"
+
+#include <string.h>
+
+static gboolean
+on_recv_get_bytes (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *data,
+ gpointer user_data)
+{
+ GBytes **result = (GBytes **)user_data;
+ g_assert_cmpstr (channel, ==, NULL);
+ g_assert (result != NULL);
+ g_assert (*result == NULL);
+ *result = g_bytes_ref (data);
+ return TRUE;
+}
+
+static void
+on_closed_not_reached (CockpitTransport *transport,
+ const gchar *problem)
+{
+ g_assert_cmpstr (problem, ==, NULL);
+ g_assert_not_reached ();
+}
+
+static void
+test_bridge_init (void)
+{
+ CockpitTransport *transport;
+ CockpitPipe *pipe;
+ GBytes *bytes = NULL;
+ JsonObject *object;
+ JsonObject *os_release;
+ JsonObject *packages;
+ GError *error = NULL;
+ GList *list;
+
+ const gchar *argv[] = {
+ BUILDDIR "/cockpit-bridge",
+ NULL
+ };
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ transport = cockpit_pipe_transport_new (pipe);
+ g_object_unref (pipe);
+
+ g_signal_connect (transport, "recv", G_CALLBACK (on_recv_get_bytes), &bytes);
+ g_signal_connect (transport, "closed", G_CALLBACK (on_closed_not_reached), NULL);
+
+ while (bytes == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_signal_handlers_disconnect_by_func (transport, on_recv_get_bytes, &bytes);
+ g_signal_handlers_disconnect_by_func (transport, on_closed_not_reached, NULL);
+
+ g_object_unref (transport);
+
+ object = cockpit_json_parse_bytes (bytes, &error);
+ g_assert_no_error (error);
+ g_bytes_unref (bytes);
+
+ g_assert_cmpstr (json_object_get_string_member (object, "command"), ==, "init");
+
+ /* Make sure we've included /etc/os-release information */
+ os_release = json_object_get_object_member (object, "os-release");
+ g_assert (os_release != NULL);
+ g_assert (json_object_has_member (os_release, "NAME"));
+
+ /* Make sure we have right packages listed */
+ packages = json_object_get_object_member (object, "packages");
+ g_assert (packages != NULL);
+ list = json_object_get_members (packages);
+ list = g_list_sort (list, (GCompareFunc)strcmp);
+ g_assert_cmpuint (g_list_length (list), ==, 3);
+ g_assert_cmpstr (list->data, ==, "another");
+ g_assert_cmpstr (list->next->data, ==, "second");
+ g_assert_cmpstr (list->next->next->data, ==, "test");
+ g_list_free (list);
+
+ json_object_unref (object);
+}
+
+#if 0
+static void
+on_closed_get_problem (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gchar **result = (gchar **)user_data;
+ g_assert (result != NULL);
+ g_assert (*result == NULL);
+ g_assert (problem != NULL);
+ *result = g_strdup (problem);
+}
+#endif
+
+static void
+on_closed_set_flag (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gboolean *flag = (gboolean *)user_data;
+ *flag = TRUE;
+}
+
+static gboolean
+on_control_get_close (CockpitTransport *transport,
+ const gchar *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload,
+ gpointer user_data)
+{
+ JsonObject **result = (JsonObject **)user_data;
+ g_assert (command != NULL);
+
+ if (g_str_equal (command, "close"))
+ {
+ g_assert (result != NULL);
+ if (*result == NULL)
+ *result = json_object_ref (options);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+typedef struct {
+ const gchar *host;
+ guint64 version;
+} InitProblem;
+
+static void
+test_bridge_init_problem (gconstpointer user_data)
+{
+ const InitProblem *fixture = user_data;
+ CockpitTransport *transport;
+ CockpitPipe *pipe;
+ GBytes *bytes;
+ gboolean closed = FALSE;
+ JsonObject *input;
+
+ g_assert (fixture != NULL);
+
+ const gchar *argv[] = {
+ BUILDDIR "/cockpit-bridge",
+ NULL
+ };
+
+ /* missing version will spawn an expected warning in subprocess */
+ if (fixture->version == 0)
+ cockpit_test_allow_warnings ();
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ transport = cockpit_pipe_transport_new (pipe);
+ g_object_unref (pipe);
+
+ /* First send the actual init message */
+ input = json_object_new ();
+ json_object_set_string_member (input, "command", "init");
+ if (fixture->version != 0)
+ json_object_set_int_member (input, "version", fixture->version);
+ if (fixture->host)
+ json_object_set_string_member (input, "host", fixture->host);
+ bytes = cockpit_json_write_bytes (input);
+ cockpit_transport_send (transport, NULL, bytes);
+ g_bytes_unref (bytes);
+ json_object_unref (input);
+
+ /* The bridge should terminate */
+ g_signal_connect (transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+
+ while (!closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_test_reset_warnings ();
+
+ g_signal_handlers_disconnect_by_func (transport, on_closed_set_flag, &closed);
+
+ g_object_unref (transport);
+
+ /* Just checking that it closes by itself here */
+}
+
+typedef struct {
+ const gchar *host;
+ const gchar *open_host;
+ const gchar *problem;
+} OpenProblem;
+
+static void
+test_bridge_open_problem (gconstpointer user_data)
+{
+ const OpenProblem *fixture = user_data;
+ CockpitTransport *transport;
+ CockpitPipe *pipe;
+ GBytes *bytes;
+ JsonObject *object = NULL;
+ JsonObject *input;
+
+ g_assert (fixture != NULL);
+ g_assert (fixture->problem != NULL);
+
+ const gchar *argv[] = {
+ BUILDDIR "/cockpit-bridge",
+ NULL
+ };
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ transport = cockpit_pipe_transport_new (pipe);
+ g_object_unref (pipe);
+
+ /* First send the actual init message */
+ input = json_object_new ();
+ json_object_set_string_member (input, "command", "init");
+ json_object_set_int_member (input, "version", 1);
+ if (fixture->host)
+ json_object_set_string_member (input, "host", fixture->host);
+ bytes = cockpit_json_write_bytes (input);
+ cockpit_transport_send (transport, NULL, bytes);
+ g_bytes_unref (bytes);
+ json_object_unref (input);
+
+ /* Next maybe send an open message */
+ input = json_object_new ();
+ json_object_set_string_member (input, "command", "open");
+ json_object_set_string_member (input, "channel", "444");
+ json_object_set_string_member (input, "payload", "null");
+ if (fixture->open_host)
+ json_object_set_string_member (input, "host", fixture->open_host);
+ bytes = cockpit_json_write_bytes (input);
+ cockpit_transport_send (transport, NULL, bytes);
+ g_bytes_unref (bytes);
+ json_object_unref (input);
+
+ /* Listen for a close message */
+ g_signal_connect (transport, "control", G_CALLBACK (on_control_get_close), &object);
+
+ while (object == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_signal_handlers_disconnect_by_func (transport, on_control_get_close, &object);
+
+ g_object_unref (transport);
+
+ g_assert_cmpstr (json_object_get_string_member (object, "problem"), ==, fixture->problem);
+ json_object_unref (object);
+}
+
+static InitProblem bad_version = {
+ .version = 5,
+};
+
+static InitProblem missing_version = {
+ .version = 0,
+};
+
+static InitProblem missing_host = {
+ .version = 1,
+};
+
+static OpenProblem wrong_host = {
+ .host = "marmalade",
+ .open_host = "juggs",
+ .problem = "not-supported",
+};
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_setenv_check ("XDG_DATA_DIRS", SRCDIR "/src/bridge/mock-resource/system", TRUE);
+ cockpit_setenv_check ("XDG_DATA_HOME", SRCDIR "/src/bridge/mock-resource/home", TRUE);
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/bridge/init-message", test_bridge_init);
+ g_test_add_data_func ("/bridge/bad-version", &bad_version, test_bridge_init_problem);
+ g_test_add_data_func ("/bridge/missing-version", &missing_version, test_bridge_init_problem);
+ g_test_add_data_func ("/bridge/missing-host", &missing_host, test_bridge_init_problem);
+ g_test_add_data_func ("/bridge/wrong-host", &wrong_host, test_bridge_open_problem);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-connect.c b/src/bridge/test-connect.c
new file mode 100644
index 0000000..25d1479
--- /dev/null
+++ b/src/bridge/test-connect.c
@@ -0,0 +1,466 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitconnect.h"
+
+#include "common/cockpitloopback.h"
+#include "testlib/cockpittest.h"
+#include "testlib/mock-transport.h"
+
+#include <glib.h>
+#include <glib-unix.h>
+#include <glib/gstdio.h>
+#include <gio/gunixsocketaddress.h>
+#include <gio/gunixinputstream.h>
+#include <gio/gunixoutputstream.h>
+
+#include <sys/uio.h>
+#include <string.h>
+
+extern const gchar *cockpit_bridge_local_address;
+
+/* ----------------------------------------------------------------------------
+ * Mock
+ */
+
+static GType mock_echo_channel_get_type (void) G_GNUC_CONST;
+
+typedef struct {
+ CockpitChannel parent;
+ gboolean close_called;
+} MockEchoChannel;
+
+typedef CockpitChannelClass MockEchoChannelClass;
+
+G_DEFINE_TYPE (MockEchoChannel, mock_echo_channel, COCKPIT_TYPE_CHANNEL);
+
+static void
+mock_echo_channel_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ cockpit_channel_send (channel, message, FALSE);
+}
+
+static gboolean
+mock_echo_channel_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *options)
+{
+ cockpit_channel_control (channel, command, options);
+ return TRUE;
+}
+
+static void
+mock_echo_channel_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ MockEchoChannel *self = (MockEchoChannel *)channel;
+ self->close_called = TRUE;
+ COCKPIT_CHANNEL_CLASS (mock_echo_channel_parent_class)->close (channel, problem);
+}
+
+static void
+mock_echo_channel_init (MockEchoChannel *self)
+{
+
+}
+
+static void
+mock_echo_channel_class_init (MockEchoChannelClass *klass)
+{
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+ channel_class->recv = mock_echo_channel_recv;
+ channel_class->control = mock_echo_channel_control;
+ channel_class->close = mock_echo_channel_close;
+}
+
+/* ----------------------------------------------------------------------------
+ * Tests
+ */
+
+typedef struct {
+ GSocket *listen_sock;
+ GSource *listen_source;
+ GSocket *conn_sock;
+ GSource *conn_source;
+ GSocketAddress *address;
+ gboolean skip_ipv6_loopback;
+ guint16 port;
+} TestConnect;
+
+static void
+on_ready_get_result (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **retval = user_data;
+ g_assert (retval != NULL);
+ g_assert (*retval == NULL);
+ *retval = g_object_ref (result);
+}
+
+static gboolean
+on_socket_input (GSocket *socket,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ gchar buffer[1024];
+ GError *error = NULL;
+ gssize ret, wret;
+
+ ret = g_socket_receive (socket, buffer, sizeof (buffer), NULL, &error);
+ g_assert_no_error (error);
+
+ if (ret == 0)
+ {
+ g_socket_shutdown (socket, FALSE, TRUE, &error);
+ g_assert_no_error (error);
+ return FALSE;
+ }
+
+ g_assert (ret > 0);
+ wret = g_socket_send (socket, buffer, ret, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (wret == ret);
+ return TRUE;
+}
+
+static gboolean
+on_socket_connection (GSocket *socket,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ TestConnect *tc = user_data;
+ GError *error = NULL;
+
+ g_assert (tc->conn_source == NULL);
+ tc->conn_sock = g_socket_accept (tc->listen_sock, NULL, &error);
+ g_assert_no_error (error);
+
+ tc->conn_source = g_socket_create_source (tc->conn_sock, G_IO_IN, NULL);
+ g_source_set_callback (tc->conn_source, (GSourceFunc)on_socket_input, tc, NULL);
+ g_source_attach (tc->conn_source, NULL);
+
+ /* Only one connection */
+ return FALSE;
+}
+
+static void
+setup_connect (TestConnect *tc,
+ gconstpointer data)
+{
+ GError *error = NULL;
+ GInetAddress *inet;
+ GSocketAddress *address;
+ GSocketFamily family = GPOINTER_TO_INT (data);
+
+ if (family == G_SOCKET_FAMILY_INVALID)
+ family = G_SOCKET_FAMILY_IPV4;
+
+ inet = g_inet_address_new_loopback (family);
+ address = g_inet_socket_address_new (inet, 0);
+ g_object_unref (inet);
+
+ tc->listen_sock = g_socket_new (family, G_SOCKET_TYPE_STREAM,
+ G_SOCKET_PROTOCOL_DEFAULT, &error);
+ g_assert_no_error (error);
+
+ g_socket_bind (tc->listen_sock, address, TRUE, &error);
+ g_object_unref (address);
+
+ if (error != NULL && family == G_SOCKET_FAMILY_IPV6)
+ {
+ /* Some test runners don't have IPv6 loopback, strangely enough */
+ g_clear_error (&error);
+ tc->skip_ipv6_loopback = TRUE;
+ return;
+ }
+
+ g_assert_no_error (error);
+
+ tc->address = g_socket_get_local_address (tc->listen_sock, &error);
+ g_assert_no_error (error);
+
+ tc->port = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (tc->address));
+
+ g_socket_listen (tc->listen_sock, &error);
+ g_assert_no_error (error);
+
+ tc->listen_source = g_socket_create_source (tc->listen_sock, G_IO_IN, NULL);
+ g_source_set_callback (tc->listen_source, (GSourceFunc)on_socket_connection, tc, NULL);
+ g_source_attach (tc->listen_source, NULL);
+}
+
+static void
+teardown_connect (TestConnect *tc,
+ gconstpointer data)
+{
+ if (tc->address)
+ g_object_unref (tc->address);
+ if (tc->conn_source)
+ {
+ g_source_destroy (tc->conn_source);
+ g_source_unref (tc->conn_source);
+ }
+ if (tc->listen_source)
+ {
+ g_source_destroy (tc->listen_source);
+ g_source_unref (tc->listen_source);
+ }
+ g_clear_object (&tc->listen_sock);
+ g_clear_object (&tc->conn_sock);
+}
+
+static void
+test_connect (TestConnect *tc,
+ gconstpointer user_data)
+{
+ GAsyncResult *result = NULL;
+ GError *error = NULL;
+ GIOStream *io;
+
+ cockpit_connect_stream (G_SOCKET_CONNECTABLE (tc->address), NULL, on_ready_get_result, &result);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (result != NULL);
+ io = cockpit_connect_stream_finish (result, &error);
+ g_assert_no_error (error);
+ g_object_unref (result);
+ g_assert (io != NULL);
+
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (io);
+}
+
+static void
+test_connect_loopback (TestConnect *tc,
+ gconstpointer user_data)
+{
+ CockpitConnectable connectable = { 0 };
+ GAsyncResult *result = NULL;
+ GError *error = NULL;
+ GIOStream *io;
+
+ if (tc->skip_ipv6_loopback)
+ {
+ g_test_skip ("no loopback for ipv6 found");
+ return;
+ }
+
+ connectable.address = cockpit_loopback_new (tc->port);
+ cockpit_connect_stream_full (&connectable, NULL, on_ready_get_result, &result);
+ g_object_unref (connectable.address);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (result != NULL);
+ io = cockpit_connect_stream_finish (result, &error);
+ g_assert_no_error (error);
+ g_object_unref (result);
+ g_assert (io != NULL);
+
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (io);
+}
+
+static void
+test_fail_not_found (void)
+{
+ GAsyncResult *result = NULL;
+ GSocketAddress *address;
+ GError *error = NULL;
+ GIOStream *io;
+
+ address = g_unix_socket_address_new ("/non-existent");
+ cockpit_connect_stream (G_SOCKET_CONNECTABLE (address), NULL, on_ready_get_result, &result);
+ g_object_unref (address);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (result != NULL);
+ io = cockpit_connect_stream_finish (result, &error);
+ g_object_unref (result);
+ g_assert (io == NULL);
+
+ g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
+ g_error_free (error);
+}
+
+static void
+test_fail_access_denied (void)
+{
+ GAsyncResult *result = NULL;
+ GSocketAddress *address;
+ GError *error = NULL;
+ GIOStream *io;
+ gchar *unix_path;
+ gint fd;
+
+ if (geteuid () == 0)
+ {
+ g_test_skip ("running as root");
+ return;
+ }
+
+ unix_path = g_strdup ("/tmp/cockpit-test-XXXXXX.sock");
+ fd = g_mkstemp (unix_path);
+ g_assert_cmpint (fd, >=, 0);
+
+ address = g_unix_socket_address_new ("/non-existent");
+ cockpit_connect_stream (G_SOCKET_CONNECTABLE (address), NULL, on_ready_get_result, &result);
+ g_object_unref (address);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (result != NULL);
+ io = cockpit_connect_stream_finish (result, &error);
+ g_object_unref (result);
+ g_assert (io == NULL);
+
+ g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
+ g_error_free (error);
+
+ unlink (unix_path);
+ g_free (unix_path);
+ close (fd);
+}
+
+static void
+test_parse_port (void)
+{
+ JsonObject *options;
+ MockTransport *transport;
+ CockpitChannel *channel;
+ CockpitConnectable *connectable;
+ GSocketAddress *address;
+ GInetAddress *expected_ip;
+ GInetAddress *got_ip; // owned by address
+ gchar *name = NULL;
+
+ expected_ip = g_inet_address_new_from_string (cockpit_bridge_local_address);
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", 8090);
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", "55",
+ "options", options,
+ NULL);
+ json_object_unref (options);
+ connectable = cockpit_connect_parse_stream (channel);
+ g_assert (connectable != NULL);
+
+ address = cockpit_connect_parse_address (channel, &name);
+
+ g_assert (g_socket_address_get_family (address) == G_SOCKET_FAMILY_IPV4);
+ g_assert_cmpint (g_inet_socket_address_get_port ((GInetSocketAddress *)address),
+ ==, 8090);
+ got_ip = g_inet_socket_address_get_address ((GInetSocketAddress *)address);
+ g_assert (g_inet_address_equal (got_ip, expected_ip));
+
+ g_assert_cmpint (connectable->local, ==, TRUE);
+
+ g_object_unref (channel);
+ cockpit_connectable_unref (connectable);
+ g_object_unref (transport);
+ g_object_unref (address);
+ g_object_unref (expected_ip);
+ g_free (name);
+ cockpit_assert_expected ();
+}
+
+static void
+test_parse_address (void)
+{
+ JsonObject *options;
+ MockTransport *transport;
+ CockpitChannel *channel;
+ CockpitConnectable *connectable;
+ GSocketAddress *address;
+ GInetAddress *expected_ip;
+ GInetAddress *got_ip; // owned by address
+ gchar *name = NULL;
+
+ expected_ip = g_inet_address_new_from_string ("10.1.1.1");
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "address", "10.1.1.1");
+ json_object_set_int_member (options, "port", 8090);
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", "55",
+ "options", options,
+ NULL);
+ json_object_unref (options);
+ connectable = cockpit_connect_parse_stream (channel);
+ g_assert (connectable != NULL);
+
+ address = cockpit_connect_parse_address (channel, &name);
+
+ g_assert (g_socket_address_get_family (address) == G_SOCKET_FAMILY_IPV4);
+ g_assert_cmpint (g_inet_socket_address_get_port ((GInetSocketAddress *)address),
+ ==, 8090);
+ got_ip = g_inet_socket_address_get_address ((GInetSocketAddress *)address);
+ g_assert (g_inet_address_equal (got_ip, expected_ip));
+
+ g_assert_cmpint (connectable->local, ==, FALSE);
+
+ g_object_unref (channel);
+ cockpit_connectable_unref (connectable);
+ g_object_unref (transport);
+ g_object_unref (address);
+ g_object_unref (expected_ip);
+ g_free (name);
+ cockpit_assert_expected ();
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_bridge_local_address = "127.0.0.1";
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/connect/simple", TestConnect, NULL,
+ setup_connect, test_connect, teardown_connect);
+ g_test_add ("/connect/loopback-ipv4", TestConnect, GINT_TO_POINTER (G_SOCKET_FAMILY_IPV4),
+ setup_connect, test_connect_loopback, teardown_connect);
+ g_test_add ("/connect/loopback-ipv6", TestConnect, GINT_TO_POINTER (G_SOCKET_FAMILY_IPV6),
+ setup_connect, test_connect_loopback, teardown_connect);
+
+ g_test_add_func ("/connect/not-found", test_fail_not_found);
+ g_test_add_func ("/connect/access-denied", test_fail_access_denied);
+
+ g_test_add_func ("/channel/parse-port", test_parse_port);
+ g_test_add_func ("/channel/parse-address", test_parse_address);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-dbus-meta.c b/src/bridge/test-dbus-meta.c
new file mode 100644
index 0000000..b4b0b3a
--- /dev/null
+++ b/src/bridge/test-dbus-meta.c
@@ -0,0 +1,760 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbusmeta.h"
+
+#include "common/cockpitjson.h"
+#include "testlib/cockpittest.h"
+
+#include <string.h>
+
+typedef struct {
+ const GDBusInterfaceInfo *iface;
+ const gchar *result;
+} BuildFixture;
+
+static const GDBusArgInfo janitor_method_say_what = {
+ -1, (gchar *)"what", "s"
+};
+
+static const GDBusArgInfo janitor_method_say_how = {
+ -1, (gchar *)"how", "i",
+};
+
+static const GDBusArgInfo *janitor_method_say_in[] = {
+ &janitor_method_say_what,
+ &janitor_method_say_how,
+ NULL,
+};
+
+static const GDBusArgInfo janitor_method_say_said = {
+ -1, (gchar *)"said", "a{sv}",
+};
+
+static const GDBusArgInfo *janitor_method_say_out[] = {
+ &janitor_method_say_said,
+ NULL,
+};
+
+static const GDBusMethodInfo janitor_method_say = {
+ -1, (gchar *)"Say",
+ (GDBusArgInfo **) &janitor_method_say_in,
+ (GDBusArgInfo **) &janitor_method_say_out,
+};
+
+static const GDBusArgInfo janitor_method_mop_mess = {
+ -1, (gchar *)"mess", "sa{sa{sv}}a{sv}a{sv}a{sv}a{sv}a{sv}a{sv}ssa{sv}a{sv}b"
+};
+
+static const GDBusArgInfo *janitor_method_mop_out[] = {
+ &janitor_method_mop_mess,
+ NULL,
+};
+
+static const GDBusMethodInfo janitor_method_mop = {
+ -1, (gchar *)"Mop",
+ (GDBusArgInfo **) NULL,
+ (GDBusArgInfo **) &janitor_method_mop_out,
+};
+
+static const GDBusMethodInfo *janitor_methods[] = {
+ &janitor_method_say,
+ &janitor_method_mop,
+ NULL
+};
+
+static const GDBusArgInfo janitor_signal_oh_oh = {
+ -1, (gchar *)"oh", "v",
+};
+
+static const GDBusArgInfo janitor_signal_oh_marmalade = {
+ -1, (gchar *)"marmalade", "v"
+};
+
+static const GDBusArgInfo *janitor_signal_oh_args[] = {
+ &janitor_signal_oh_oh,
+ &janitor_signal_oh_marmalade,
+ NULL,
+};
+
+static const GDBusSignalInfo janitor_signal_oh = {
+ -1, (gchar *)"Oh",
+ (GDBusArgInfo **)&janitor_signal_oh_args
+};
+
+static const GDBusSignalInfo janitor_signal_boom = {
+ -1, (gchar *)"Oh",
+ (GDBusArgInfo **)&janitor_signal_oh_args
+};
+
+static const GDBusSignalInfo *janitor_signals[] = {
+ &janitor_signal_oh,
+ &janitor_signal_boom,
+ NULL
+};
+
+static const GDBusPropertyInfo janitor_property_name = {
+ -1, (gchar *)"Name", "s", G_DBUS_PROPERTY_INFO_FLAGS_READABLE
+};
+
+static const GDBusPropertyInfo janitor_property_habit = {
+ -1, (gchar *)"Habit", "a{sv}", G_DBUS_PROPERTY_INFO_FLAGS_READABLE | G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE
+};
+
+static const GDBusPropertyInfo janitor_property_hidden = {
+ -1, (gchar *)"Hidden", "b", G_DBUS_PROPERTY_INFO_FLAGS_WRITABLE
+};
+
+static const GDBusPropertyInfo *janitor_properties[] = {
+ &janitor_property_name,
+ &janitor_property_habit,
+ &janitor_property_hidden,
+ NULL
+};
+
+static const GDBusInterfaceInfo janitor_interface = {
+ -1, "planet.express.Janitor",
+ (GDBusMethodInfo **) &janitor_methods,
+ (GDBusSignalInfo **) &janitor_signals,
+ (GDBusPropertyInfo **) &janitor_properties,
+ NULL
+};
+
+static const gchar janitor_json[] = "{"
+ "\"methods\": {"
+ "\"Say\": {"
+ "\"in\": [\"s\",\"i\"],"
+ "\"out\":[\"a{sv}\"]"
+ "},"
+ "\"Mop\": {"
+ "\"out\":[\"sa{sa{sv}}a{sv}a{sv}a{sv}a{sv}a{sv}a{sv}ssa{sv}a{sv}b\"]"
+ "}"
+ "},"
+ "\"properties\": {"
+ "\"Name\": {"
+ "\"flags\": \"r\","
+ "\"type\": \"s\""
+ "},"
+ "\"Habit\": {"
+ "\"flags\": \"rw\","
+ "\"type\": \"a{sv}\""
+ "},"
+ "\"Hidden\": {"
+ "\"flags\": \"w\","
+ "\"type\": \"b\""
+ "}"
+ "},"
+ "\"signals\": {"
+ "\"Oh\": {"
+ "\"in\": [\"v\",\"v\"]"
+ "}"
+ "}"
+"}";
+
+static const GDBusInterfaceInfo no_methods_interface = {
+ -1, "planet.express.NoMethods",
+ (GDBusMethodInfo **) NULL,
+ (GDBusSignalInfo **) &janitor_signals,
+ (GDBusPropertyInfo **) &janitor_properties,
+ NULL
+};
+
+static const gchar no_methods_json[] = "{"
+ "\"properties\": {"
+ "\"Name\": {"
+ "\"flags\": \"r\","
+ "\"type\": \"s\""
+ "},"
+ "\"Habit\": {"
+ "\"flags\": \"rw\","
+ "\"type\": \"a{sv}\""
+ "},"
+ "\"Hidden\": {"
+ "\"flags\": \"w\","
+ "\"type\": \"b\""
+ "}"
+ "},"
+ "\"signals\": {"
+ "\"Oh\": {"
+ "\"in\": [\"v\",\"v\"]"
+ "}"
+ "}"
+"}";
+
+static const GDBusInterfaceInfo no_signals_interface = {
+ -1, "planet.express.NoSignals",
+ (GDBusMethodInfo **) &janitor_methods,
+ (GDBusSignalInfo **) NULL,
+ (GDBusPropertyInfo **) &janitor_properties,
+ NULL
+};
+
+static const gchar no_signals_json[] = "{"
+ "\"methods\": {"
+ "\"Say\": {"
+ "\"in\": [\"s\",\"i\"],"
+ "\"out\":[\"a{sv}\"]"
+ "},"
+ "\"Mop\": {"
+ "\"out\":[\"sa{sa{sv}}a{sv}a{sv}a{sv}a{sv}a{sv}a{sv}ssa{sv}a{sv}b\"]"
+ "}"
+ "},"
+ "\"properties\": {"
+ "\"Name\": {"
+ "\"flags\": \"r\","
+ "\"type\": \"s\""
+ "},"
+ "\"Habit\": {"
+ "\"flags\": \"rw\","
+ "\"type\": \"a{sv}\""
+ "},"
+ "\"Hidden\": {"
+ "\"flags\": \"w\","
+ "\"type\": \"b\""
+ "}"
+ "}"
+"}";
+
+static const GDBusInterfaceInfo no_properties_interface = {
+ -1, "planet.express.NoProperties",
+ (GDBusMethodInfo **) &janitor_methods,
+ (GDBusSignalInfo **) &janitor_signals,
+ (GDBusPropertyInfo **) NULL,
+ NULL
+};
+
+static const gchar no_properties_json[] = "{"
+ "\"methods\": {"
+ "\"Say\": {"
+ "\"in\": [\"s\",\"i\"],"
+ "\"out\":[\"a{sv}\"]"
+ "},"
+ "\"Mop\": {"
+ "\"out\":[\"sa{sa{sv}}a{sv}a{sv}a{sv}a{sv}a{sv}a{sv}ssa{sv}a{sv}b\"]"
+ "}"
+ "},"
+ "\"signals\": {"
+ "\"Oh\": {"
+ "\"in\": [\"v\",\"v\"]"
+ "}"
+ "}"
+"}";
+
+static const BuildFixture build_janitor_fixture = {
+ &janitor_interface,
+ janitor_json
+};
+
+static const BuildFixture build_no_methods_fixture = {
+ &no_methods_interface,
+ no_methods_json
+};
+
+static const BuildFixture build_no_signals_fixture = {
+ &no_signals_interface,
+ no_signals_json
+};
+
+static const BuildFixture build_no_properties_fixture = {
+ &no_properties_interface,
+ no_properties_json
+};
+
+static void
+test_build (gconstpointer data)
+{
+ const BuildFixture *fixture = data;
+ JsonObject *object;
+
+ object = cockpit_dbus_meta_build ((GDBusInterfaceInfo *)fixture->iface);
+ cockpit_assert_json_eq (object, fixture->result);
+ json_object_unref (object);
+}
+
+static void
+assert_equal_arg (GDBusArgInfo *one,
+ GDBusArgInfo *two)
+{
+ g_assert (one != NULL);
+ g_assert (two != NULL);
+ g_assert_cmpstr (one->signature, ==, two->signature);
+}
+
+static void
+assert_equal_args (GDBusArgInfo **one,
+ GDBusArgInfo **two)
+{
+ if (one == NULL || two == NULL)
+ {
+ g_assert (one == NULL && two == NULL);
+ return;
+ }
+
+ while (*one != NULL && *two != NULL)
+ {
+ assert_equal_arg (*one, *two);
+ one++;
+ two++;
+ }
+}
+
+static void
+assert_equal_method (GDBusMethodInfo *one,
+ GDBusMethodInfo *two)
+{
+ g_assert (one != NULL);
+ g_assert (two != NULL);
+ g_assert_cmpstr (one->name, ==, two->name);
+ assert_equal_args (one->in_args, two->in_args);
+ assert_equal_args (one->out_args, two->out_args);
+}
+
+static void
+assert_equal_methods (GDBusMethodInfo **one,
+ GDBusMethodInfo **two)
+{
+ if (one == NULL || two == NULL)
+ {
+ g_assert (one == NULL && two == NULL);
+ return;
+ }
+
+ while (*one != NULL && *two != NULL)
+ {
+ assert_equal_method (*one, *two);
+ one++;
+ two++;
+ }
+}
+
+static void
+assert_equal_signal (GDBusSignalInfo *one,
+ GDBusSignalInfo *two)
+{
+ g_assert (one != NULL);
+ g_assert (two != NULL);
+ g_assert_cmpstr (one->name, ==, two->name);
+ assert_equal_args (one->args, two->args);
+}
+
+static void
+assert_equal_signals (GDBusSignalInfo **one,
+ GDBusSignalInfo **two)
+{
+ if (one == NULL || two == NULL)
+ {
+ g_assert (one == NULL && two == NULL);
+ return;
+ }
+
+ while (*one != NULL && *two != NULL)
+ {
+ assert_equal_signal (*one, *two);
+ one++;
+ two++;
+ }
+}
+
+static void
+assert_equal_property (GDBusPropertyInfo *one,
+ GDBusPropertyInfo *two)
+{
+ g_assert (one != NULL);
+ g_assert (two != NULL);
+ g_assert_cmpstr (one->name, ==, two->name);
+ g_assert_cmpstr (one->signature, ==, two->signature);
+ g_assert_cmpuint (one->flags, ==, two->flags);
+}
+
+static void
+assert_equal_properties (GDBusPropertyInfo **one,
+ GDBusPropertyInfo **two)
+{
+ if (one == NULL || two == NULL)
+ {
+ g_assert (one == NULL && two == NULL);
+ return;
+ }
+
+ while (*one != NULL && *two != NULL)
+ {
+ assert_equal_property (*one, *two);
+ one++;
+ two++;
+ }
+}
+
+static void
+assert_equal_interface (GDBusInterfaceInfo *one,
+ GDBusInterfaceInfo *two)
+{
+ g_assert (one != NULL);
+ g_assert (two != NULL);
+ g_assert_cmpstr (one->name, ==, two->name);
+ assert_equal_methods (one->methods, two->methods);
+ assert_equal_signals (one->signals, two->signals);
+ assert_equal_properties (one->properties, two->properties);
+}
+
+typedef struct {
+ const gchar *name;
+ const gchar *input;
+ const GDBusInterfaceInfo *iface;
+} ParseFixture;
+
+static const ParseFixture parse_janitor_fixture = {
+ "planet.express.Janitor",
+ janitor_json,
+ &janitor_interface
+};
+
+static const ParseFixture parse_no_methods_fixture = {
+ "planet.express.NoMethods",
+ no_methods_json,
+ &no_methods_interface
+};
+
+static const ParseFixture parse_no_signals_fixture = {
+ "planet.express.NoSignals",
+ no_signals_json,
+ &no_signals_interface
+};
+
+static const ParseFixture parse_no_properties_fixture = {
+ "planet.express.NoProperties",
+ no_properties_json,
+ &no_properties_interface
+};
+
+static void
+test_parse (gconstpointer data)
+{
+ const ParseFixture *fixture = data;
+ GDBusInterfaceInfo *iface;
+ GError *error = NULL;
+ JsonObject *object;
+
+ object = cockpit_json_parse_object (fixture->input, -1, &error);
+ g_assert_no_error (error);
+
+ iface = cockpit_dbus_meta_parse (fixture->name, object, &error);
+ g_assert_no_error (error);
+
+ assert_equal_interface (iface, (GDBusInterfaceInfo *)fixture->iface);
+
+ g_dbus_interface_info_unref (iface);
+ json_object_unref (object);
+}
+
+typedef struct {
+ const gchar *input;
+ const gchar *message;
+} ErrorFixture;
+
+static const gchar invalid_in_argument_json[] = "{"
+ "\"methods\": {"
+ "\"BrokenMethod\": {"
+ "\"in\": [ true ]"
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_invalid_in_argument = {
+ invalid_in_argument_json,
+ "invalid argument in dbus meta field"
+};
+
+static const gchar invalid_out_argument_json[] = "{"
+ "\"methods\": {"
+ "\"BrokenMethod\": {"
+ "\"out\": [ true ]"
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_invalid_out_argument = {
+ invalid_out_argument_json,
+ "invalid argument in dbus meta field"
+};
+
+static const gchar invalid_signal_argument_json[] = "{"
+ "\"signals\": {"
+ "\"BrokenSignal\": {"
+ "\"in\": [ true ]"
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_invalid_signal_argument = {
+ invalid_signal_argument_json,
+ "invalid argument in dbus meta field"
+};
+
+static const gchar invalid_signature_argument_json[] = "{"
+ "\"methods\": {"
+ "\"BrokenMethod\": {"
+ "\"in\": [\"s\",\"!!!\"]"
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_signature_argument = {
+ invalid_signature_argument_json,
+ "argument in dbus meta field has invalid signature: !!!"
+};
+
+static const gchar invalid_in_method_json[] = "{"
+ "\"methods\": {"
+ "\"BrokenMethod\": {"
+ "\"in\": true,"
+ "\"out\":[\"a{sv}\"]"
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_invalid_in_method = {
+ invalid_in_method_json,
+ "invalid \"in\" field in dbus meta method: BrokenMethod"
+};
+
+static const gchar invalid_out_method_json[] = "{"
+ "\"methods\": {"
+ "\"BrokenMethod\": {"
+ "\"in\":[\"a{sv}\"],"
+ "\"out\": 5"
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_invalid_out_method = {
+ invalid_out_method_json,
+ "invalid \"out\" field in dbus meta method: BrokenMethod"
+};
+
+static const gchar invalid_in_signal_json[] = "{"
+ "\"signals\": {"
+ "\"BrokenSignal\": {"
+ "\"in\": { }"
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_in_signal_fixture = {
+ invalid_in_signal_json,
+ "invalid \"in\" field in dbus meta signal: BrokenSignal"
+};
+
+static const gchar invalid_flags_property_json[] = "{"
+ "\"properties\": {"
+ "\"BrokenProperty\": {"
+ "\"flags\": [ ],"
+ "\"type\": \"s\""
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_flags_property_fixture = {
+ invalid_flags_property_json,
+ "invalid \"flags\" field in dbus property: BrokenProperty"
+};
+
+static const gchar invalid_type_property_json[] = "{"
+ "\"properties\": {"
+ "\"BrokenProperty\": {"
+ "\"flags\": \"r\","
+ "\"type\": 555"
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_type_property_fixture = {
+ invalid_type_property_json,
+ "invalid \"type\" field in dbus property: BrokenProperty"
+};
+
+static const gchar missing_type_property_json[] = "{"
+ "\"properties\": {"
+ "\"BrokenProperty\": {"
+ "\"flags\": \"r\""
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_type_missing_fixture = {
+ missing_type_property_json,
+ "missing \"type\" field in dbus property: BrokenProperty"
+};
+
+static const gchar invalid_signature_property_json[] = "{"
+ "\"properties\": {"
+ "\"BrokenProperty\": {"
+ "\"flags\": \"r\","
+ "\"type\": \"???\""
+ "}"
+ "}"
+"}";
+
+static const ErrorFixture error_signature_property_fixture = {
+ invalid_signature_property_json,
+ "the \"type\" field in dbus property is not a dbus signature: ???"
+};
+
+static const gchar invalid_methods_json[] = "{"
+ "\"methods\": [ ]"
+"}";
+
+static const ErrorFixture error_methods_fixture = {
+ invalid_methods_json,
+ "invalid \"methods\" field in dbus meta structure"
+};
+
+static const gchar invalid_method_json[] = "{"
+ "\"methods\": {"
+ "\"BadMethod\": [ ]"
+ "}"
+"}";
+
+static const ErrorFixture error_method_json = {
+ invalid_method_json,
+ "invalid method field in dbus meta structure: BadMethod",
+};
+
+static const gchar invalid_signals_json[] = "{"
+ "\"signals\": 555"
+"}";
+
+static const ErrorFixture error_signals_json = {
+ invalid_signals_json,
+ "invalid \"signals\" field in dbus meta structure"
+};
+
+static const gchar invalid_signal_json[] = "{"
+ "\"signals\": {"
+ "\"BadSignal\": true"
+ "}"
+"}";
+
+static const ErrorFixture error_signal_json = {
+ invalid_signal_json,
+ "invalid signal field in dbus meta structure: BadSignal",
+};
+
+static const gchar invalid_properties_json[] = "{"
+ "\"properties\": [ ]"
+"}";
+
+static const ErrorFixture error_properties_json = {
+ invalid_properties_json,
+ "invalid \"properties\" field in dbus meta structure"
+};
+
+static const gchar invalid_property_json[] = "{"
+ "\"properties\": {"
+ "\"BadProperty\": true"
+ "}"
+"}";
+
+static const ErrorFixture error_property_json = {
+ invalid_property_json,
+ "invalid property field in dbus meta structure: BadProperty",
+};
+
+static void
+test_error (gconstpointer data)
+{
+ const ErrorFixture *fixture = data;
+ GDBusInterfaceInfo *iface;
+ GError *error = NULL;
+ JsonObject *object;
+
+ object = cockpit_json_parse_object (fixture->input, -1, &error);
+ g_assert_no_error (error);
+
+ iface = cockpit_dbus_meta_parse ("name.not.Important", object, &error);
+ g_assert (iface == NULL);
+ g_assert_error (error, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS);
+ g_assert_cmpstr (error->message, ==, fixture->message);
+
+ g_error_free (error);
+ json_object_unref (object);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_data_func ("/dbus-meta/build/basic",
+ &build_janitor_fixture, test_build);
+ g_test_add_data_func ("/dbus-meta/build/no-methods",
+ &build_no_methods_fixture, test_build);
+ g_test_add_data_func ("/dbus-meta/build/no-signals",
+ &build_no_signals_fixture, test_build);
+ g_test_add_data_func ("/dbus-meta/build/no-properties",
+ &build_no_properties_fixture, test_build);
+
+ g_test_add_data_func ("/dbus-meta/parse/basic",
+ &parse_janitor_fixture, test_parse);
+ g_test_add_data_func ("/dbus-meta/parse/no-methods",
+ &parse_no_methods_fixture, test_parse);
+ g_test_add_data_func ("/dbus-meta/parse/no-signals",
+ &parse_no_signals_fixture, test_parse);
+ g_test_add_data_func ("/dbus-meta/parse/no-properties",
+ &parse_no_properties_fixture, test_parse);
+
+ g_test_add_data_func ("/dbus-meta/parse/invalid-in-argument",
+ &error_invalid_in_argument, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-out-argument",
+ &error_invalid_out_argument, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-signal-argument",
+ &error_invalid_signal_argument, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-signature-argument",
+ &error_signature_argument, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-in-arguments",
+ &error_invalid_in_method, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-out-arguments",
+ &error_invalid_out_method, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-signal-arguments",
+ &error_in_signal_fixture, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-property-flags",
+ &error_flags_property_fixture, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-property-type",
+ &error_type_property_fixture, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/missing-property-type",
+ &error_type_missing_fixture, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-property-signature",
+ &error_signature_property_fixture, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-methods",
+ &error_methods_fixture, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-method",
+ &error_method_json, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-signals",
+ &error_signals_json, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-signal",
+ &error_signal_json, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-properties",
+ &error_properties_json, test_error);
+ g_test_add_data_func ("/dbus-meta/parse/invalid-property",
+ &error_property_json, test_error);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-fs.c b/src/bridge/test-fs.c
new file mode 100644
index 0000000..a55dca8
--- /dev/null
+++ b/src/bridge/test-fs.c
@@ -0,0 +1,1075 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitfsread.h"
+#include "cockpitfsreplace.h"
+#include "cockpitfswatch.h"
+#include "cockpitfslist.h"
+
+#include "common/cockpitjson.h"
+#include "testlib/cockpittest.h"
+#include "testlib/mock-transport.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/stat.h>
+
+#define TIMEOUT 30
+
+typedef struct {
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *test_dir;
+ gchar *test_path;
+ gchar *test_path_2;
+ gchar *test_link;
+ gchar *test_subdir;
+ gchar *problem;
+ gboolean channel_closed;
+} TestCase;
+
+static void
+on_channel_close (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ TestCase *tc = user_data;
+ g_assert (tc->channel_closed == FALSE);
+ tc->channel_closed = TRUE;
+}
+
+static void
+on_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ g_assert_not_reached ();
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ alarm (TIMEOUT);
+
+ tc->transport = mock_transport_new ();
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+ tc->channel = NULL;
+
+ tc->test_dir = g_dir_make_tmp (NULL, NULL);
+ g_assert (tc->test_dir != NULL);
+ tc->test_path = g_strdup_printf ("%s/%s", tc->test_dir, "foo");
+ tc->test_path_2 = g_strdup_printf ("%s/%s", tc->test_dir, "bar");
+ tc->test_subdir = g_strdup_printf ("%s/%s", tc->test_dir, "subdir");
+ tc->test_link = g_strdup_printf ("%s/%s", tc->test_dir, "foo-link");
+
+ g_assert (unlink (tc->test_path) >= 0 || errno == ENOENT);
+ g_assert (unlink (tc->test_path_2) >= 0 || errno == ENOENT);
+ g_assert (unlink (tc->test_link) >= 0 || errno == ENOENT);
+ g_assert (rmdir (tc->test_subdir) >= 0 || errno == ENOENT);
+}
+
+static void
+setup_fsread_channel (TestCase *tc,
+ const gchar *path, gboolean binary)
+{
+ tc->channel = cockpit_fsread_open (COCKPIT_TRANSPORT (tc->transport), "1234", path, binary);
+ tc->channel_closed = FALSE;
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_channel_close), tc);
+ cockpit_channel_prepare (tc->channel);
+}
+
+static void
+setup_fsreplace_channel (TestCase *tc,
+ const gchar *path,
+ const gchar *tag)
+{
+ tc->channel = cockpit_fsreplace_open (COCKPIT_TRANSPORT (tc->transport), "1234", path, tag);
+ tc->channel_closed = FALSE;
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_channel_close), tc);
+ cockpit_channel_prepare (tc->channel);
+}
+
+static void
+setup_fswatch_channel (TestCase *tc,
+ const gchar *path)
+{
+ tc->channel = cockpit_fswatch_open (COCKPIT_TRANSPORT (tc->transport), "1234", path);
+ tc->channel_closed = FALSE;
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_channel_close), tc);
+ cockpit_channel_prepare (tc->channel);
+}
+
+static void
+setup_fslist_channel (TestCase *tc,
+ const gchar *path,
+ const gboolean watch)
+{
+ tc->channel = cockpit_fslist_open (COCKPIT_TRANSPORT (tc->transport), "1234", path, watch);
+ tc->channel_closed = FALSE;
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_channel_close), tc);
+}
+
+static void
+send_string (TestCase *tc,
+ const gchar *str)
+{
+ GBytes *bytes = g_bytes_new_static (str, strlen (str));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "1234", bytes);
+ g_bytes_unref (bytes);
+}
+
+static void
+send_done (TestCase *tc)
+{
+ const gchar *message = "{ \"command\": \"done\", \"channel\": \"1234\" }";
+ GBytes *bytes = g_bytes_new_static (message, strlen (message));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), NULL, bytes);
+ g_bytes_unref (bytes);
+}
+
+static GBytes *
+recv_bytes (TestCase *tc)
+{
+ GBytes *msg;
+ while ((msg = mock_transport_pop_channel (tc->transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ return msg;
+}
+
+static JsonObject *
+recv_json (TestCase *tc)
+{
+ GBytes *msg = recv_bytes (tc);
+ JsonObject *res = cockpit_json_parse_bytes (msg, NULL);
+ g_assert (res != NULL);
+ return res;
+}
+
+static JsonObject *
+recv_control (TestCase *tc)
+{
+ JsonObject *msg;
+ while ((msg = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ return msg;
+}
+
+static void
+close_channel (TestCase *tc,
+ const gchar *problem)
+{
+ cockpit_channel_close (tc->channel, problem);
+}
+
+static void
+wait_channel_closed (TestCase *tc)
+{
+ while (tc->channel_closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ cockpit_assert_expected ();
+
+ g_object_unref (tc->transport);
+
+ if (tc->channel)
+ {
+ g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
+ g_object_unref (tc->channel);
+ g_assert (tc->channel == NULL);
+ }
+
+ g_assert (unlink (tc->test_path) >= 0 || errno == ENOENT);
+ g_assert (unlink (tc->test_path_2) >= 0 || errno == ENOENT);
+ g_assert (unlink (tc->test_link) >= 0 || errno == ENOENT);
+ g_assert (rmdir (tc->test_subdir) >= 0 || errno == ENOENT);
+ g_assert (rmdir (tc->test_dir) >= 0);
+
+ g_free (tc->test_path);
+ g_free (tc->test_path_2);
+ g_free (tc->test_link);
+ g_free (tc->test_subdir);
+ g_free (tc->test_dir);
+
+ g_free (tc->problem);
+
+ alarm (0);
+}
+
+static GBytes *
+combine_output (TestCase *tc,
+ guint *count)
+{
+ GByteArray *combined;
+ GBytes *block;
+
+ if (count)
+ *count = 0;
+
+ combined = g_byte_array_new ();
+ for (;;)
+ {
+ block = mock_transport_pop_channel (tc->transport, "1234");
+ if (!block)
+ break;
+
+ g_byte_array_append (combined, g_bytes_get_data (block, NULL), g_bytes_get_size (block));
+ if (count)
+ (*count)++;
+ }
+ return g_byte_array_free_to_bytes (combined);
+}
+
+static void
+assert_received (TestCase *tc,
+ const gchar *str)
+{
+ GBytes *data;
+
+ data = combine_output (tc, NULL);
+ cockpit_assert_bytes_eq (data, str, -1);
+ g_bytes_unref (data);
+}
+
+static void
+set_contents (const gchar *path,
+ const gchar *str)
+{
+ g_assert (g_file_set_contents (path, str, -1, NULL));
+}
+
+static void
+assert_contents (const gchar *path,
+ const gchar *str)
+{
+ gchar *contents;
+ g_assert (g_file_get_contents (path, &contents, NULL, NULL));
+ g_assert_cmpstr (contents, ==, str);
+ g_free (contents);
+}
+
+static void
+test_read_simple (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *control;
+
+ set_contents (tc->test_path, "Hello!");
+ tag = cockpit_get_file_tag (tc->test_path);
+
+ setup_fsread_channel (tc, tc->test_path, FALSE);
+ wait_channel_closed (tc);
+
+ assert_received (tc, "Hello!");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "done");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ /* binary only option */
+ g_assert (json_object_get_member (control, "size-hint") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, tag);
+ g_free (tag);
+}
+
+static void
+test_read_binary_size_hint (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *control;
+ struct stat statbuf;
+
+ set_contents (tc->test_path, "Hello!");
+ stat (tc->test_path, &statbuf);
+
+ setup_fsread_channel (tc, tc->test_path, TRUE);
+ wait_channel_closed (tc);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+ g_assert_cmpint (json_object_get_int_member (control, "size-hint"), ==, statbuf.st_size);
+}
+
+static void
+test_read_non_existent (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *control;
+
+ setup_fsread_channel (tc, "/non/existent", FALSE);
+ wait_channel_closed (tc);
+
+ assert_received (tc, "");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, "-");
+}
+
+static void
+test_read_denied (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *control;
+
+ if (geteuid () == 0)
+ {
+ g_test_skip ("running as root");
+ return;
+ }
+
+ set_contents (tc->test_path, "Hello!");
+ g_assert (chmod (tc->test_path, 0) >= 0);
+
+ setup_fsread_channel (tc, tc->test_path, FALSE);
+ wait_channel_closed (tc);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "problem"), ==, "access-denied");
+}
+
+static void
+test_read_changed (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *control;
+
+ set_contents (tc->test_path, "Hello!");
+ setup_fsread_channel (tc, tc->test_path, FALSE);
+
+ {
+ sleep(1);
+ FILE *f = fopen (tc->test_path, "w");
+ g_assert (f != NULL);
+ fputs ("Goodbye!", f);
+ fclose (f);
+ }
+
+ wait_channel_closed (tc);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "done");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "problem"), ==, "change-conflict");
+}
+
+static void
+test_read_replaced (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *control;
+
+ set_contents (tc->test_path, "Hello!");
+ tag = cockpit_get_file_tag (tc->test_path);
+
+ setup_fsread_channel (tc, tc->test_path, FALSE);
+
+ {
+ FILE *f = fopen (tc->test_path_2, "w");
+ g_assert (f != NULL);
+ g_assert (fputs ("Goodbye!", f) != EOF);
+ g_assert (fclose (f) != EOF);
+ g_assert (rename (tc->test_path_2, tc->test_path) >= 0);
+ }
+
+ wait_channel_closed (tc);
+
+ assert_received (tc, "Hello!");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "done");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, tag);
+ g_free (tag);
+}
+
+static void
+test_read_removed (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *control;
+
+ set_contents (tc->test_path, "Hello!");
+ tag = cockpit_get_file_tag (tc->test_path);
+
+ setup_fsread_channel (tc, tc->test_path, FALSE);
+
+ {
+ g_assert (unlink (tc->test_path) >= 0);
+ }
+
+ wait_channel_closed (tc);
+
+ assert_received (tc, "Hello!");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "done");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, tag);
+ g_free (tag);
+}
+
+static void
+test_read_non_mmappable (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *control;
+ const gchar *path = "/sys/power/state";
+
+ g_autofree gchar *tag = cockpit_get_file_tag (path);
+
+ if (g_strcmp0 (tag, "-") == 0)
+ {
+ g_test_skip ("No /sys/power/state");
+ return;
+ }
+
+ setup_fsread_channel (tc, path, FALSE);
+ wait_channel_closed (tc);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "done");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, tag);
+}
+
+static void
+test_write_simple (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *control;
+
+ setup_fsreplace_channel (tc, tc->test_path, NULL);
+ send_string (tc, "Hello!");
+ send_done (tc);
+ close_channel (tc, NULL);
+
+ wait_channel_closed (tc);
+
+ assert_contents (tc->test_path, "Hello!");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ tag = cockpit_get_file_tag (tc->test_path);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, tag);
+ g_free (tag);
+}
+
+static void
+test_write_multiple (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *control;
+
+ setup_fsreplace_channel (tc, tc->test_path, NULL);
+ send_string (tc, "Hel");
+ send_string (tc, "lo!");
+ send_done (tc);
+ close_channel (tc, NULL);
+
+ wait_channel_closed (tc);
+
+ assert_contents (tc->test_path, "Hello!");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ tag = cockpit_get_file_tag (tc->test_path);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, tag);
+ g_free (tag);
+}
+
+static void
+test_write_remove (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *control;
+
+ set_contents (tc->test_path, "Goodbye!");
+ tag = cockpit_get_file_tag (tc->test_path);
+ setup_fsreplace_channel (tc, tc->test_path, tag);
+ send_done (tc);
+ close_channel (tc, NULL);
+ g_free (tag);
+
+ wait_channel_closed (tc);
+
+ g_assert (g_file_test (tc->test_path, G_FILE_TEST_EXISTS) == FALSE);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, "-");
+}
+
+static void
+test_write_remove_nonexistent (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *control;
+
+ g_assert (g_file_test (tc->test_path, G_FILE_TEST_EXISTS) == FALSE);
+
+ setup_fsreplace_channel (tc, tc->test_path, "-");
+ send_done (tc);
+ close_channel (tc, NULL);
+
+ wait_channel_closed (tc);
+
+ g_assert (g_file_test (tc->test_path, G_FILE_TEST_EXISTS) == FALSE);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, "-");
+}
+
+static void
+test_write_empty (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *control;
+
+ set_contents (tc->test_path, "Goodbye!");
+ tag = cockpit_get_file_tag (tc->test_path);
+ setup_fsreplace_channel (tc, tc->test_path, tag);
+ send_string (tc, "");
+ send_done (tc);
+ close_channel (tc, NULL);
+ g_free (tag);
+
+ wait_channel_closed (tc);
+
+ assert_contents (tc->test_path, "");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ tag = cockpit_get_file_tag (tc->test_path);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, tag);
+ g_free (tag);
+}
+
+static void
+test_write_denied (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *control;
+
+ if (geteuid () == 0)
+ {
+ g_test_skip ("running as root");
+ return;
+ }
+
+ g_assert (chmod (tc->test_dir, 0) >= 0);
+
+ setup_fsreplace_channel (tc, tc->test_path, NULL);
+ send_string (tc, "Hello!");
+ send_done (tc);
+ wait_channel_closed (tc);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "problem"), ==, "access-denied");
+
+ g_assert (chmod (tc->test_dir, 0777) >= 0);
+}
+
+static void
+test_write_expect_non_existent (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *control;
+
+ setup_fsreplace_channel (tc, tc->test_path, "-");
+ send_string (tc, "Hello!");
+ send_done (tc);
+ close_channel (tc, NULL);
+
+ wait_channel_closed (tc);
+
+ assert_contents (tc->test_path, "Hello!");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ tag = cockpit_get_file_tag (tc->test_path);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, tag);
+ g_free (tag);
+}
+
+static void
+test_write_expect_non_existent_fail (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *control;
+
+ set_contents (tc->test_path, "Goodbye!");
+
+ setup_fsreplace_channel (tc, tc->test_path, "-");
+ send_string (tc, "Hello!");
+ send_done (tc);
+ close_channel (tc, NULL);
+
+ wait_channel_closed (tc);
+
+ assert_contents (tc->test_path, "Goodbye!");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "problem"), ==, "change-conflict");
+}
+
+static void
+test_write_expect_tag (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *control;
+
+ set_contents (tc->test_path, "Goodbye!");
+ tag = cockpit_get_file_tag (tc->test_path);
+ setup_fsreplace_channel (tc, tc->test_path, tag);
+ send_string (tc, "Hello!");
+ send_done (tc);
+ close_channel (tc, NULL);
+ g_free (tag);
+
+ wait_channel_closed (tc);
+
+ assert_contents (tc->test_path, "Hello!");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ tag = cockpit_get_file_tag (tc->test_path);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+ g_assert_cmpstr (json_object_get_string_member (control, "tag"), ==, tag);
+ g_free (tag);
+}
+
+static void
+test_write_expect_tag_fail (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *control;
+
+ set_contents (tc->test_path, "Goodbye!");
+ tag = cockpit_get_file_tag (tc->test_path);
+ setup_fsreplace_channel (tc, tc->test_path, tag);
+ send_string (tc, "Hello!");
+ set_contents (tc->test_path, "Tschüss!");
+ send_done (tc);
+ close_channel (tc, NULL);
+ g_free (tag);
+
+ wait_channel_closed (tc);
+
+ assert_contents (tc->test_path, "Tschüss!");
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ control = mock_transport_pop_control (tc->transport);
+ tag = cockpit_get_file_tag (tc->test_path);
+ g_assert_cmpstr (json_object_get_string_member (control, "problem"), ==, "out-of-date");
+ g_free (tag);
+}
+
+static void
+test_watch_simple (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *tag;
+ JsonObject *event;
+
+ setup_fswatch_channel (tc, tc->test_path);
+
+ set_contents (tc->test_path, "Wake up!");
+ tag = cockpit_get_file_tag (tc->test_path);
+
+ event = recv_json (tc);
+ g_assert (event != NULL);
+
+ /*
+ * HACK: https://bugzilla.redhat.com/show_bug.cgi?id=1259594
+ * Some versions of glib2 erroneously emit spurious "delete" events.
+ */
+ if (g_str_equal (json_object_get_string_member (event, "event"), "deleted"))
+ {
+ json_object_unref (event);
+ event = recv_json (tc);
+ }
+
+ g_assert_cmpstr (json_object_get_string_member (event, "event"), ==, "created");
+ g_assert_cmpstr (json_object_get_string_member (event, "path"), ==, tc->test_path);
+ g_assert_cmpstr (json_object_get_string_member (event, "tag"), ==, tag);
+ g_assert_cmpstr (json_object_get_string_member (event, "type"), ==, "file");
+ json_object_unref (event);
+ g_free (tag);
+}
+
+static void
+test_watch_remove (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *event;
+
+ set_contents (tc->test_path, "Hello!");
+
+ setup_fswatch_channel (tc, tc->test_path);
+
+ g_assert (unlink (tc->test_path) >= 0);
+
+ event = recv_json (tc);
+ g_assert_cmpstr (json_object_get_string_member (event, "event"), ==, "deleted");
+ g_assert_cmpstr (json_object_get_string_member (event, "path"), ==, tc->test_path);
+ g_assert_cmpstr (json_object_get_string_member (event, "tag"), ==, "-");
+ json_object_unref (event);
+}
+
+static void
+test_watch_directory (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *event;
+
+ setup_fswatch_channel (tc, tc->test_dir);
+
+ set_contents (tc->test_path, "Hello!");
+ g_assert (unlink (tc->test_path) >= 0);
+
+ /* We want to see at least "created" and "deleted" for the path, in
+ that order.
+ */
+
+ gboolean saw_created = FALSE;
+ gboolean saw_deleted = FALSE;
+
+ while (!(saw_created && saw_deleted) && !tc->channel_closed)
+ {
+ event = recv_json (tc);
+ if (g_strcmp0 (json_object_get_string_member (event, "path"), tc->test_path) == 0)
+ {
+ if (g_strcmp0 (json_object_get_string_member (event, "event"), "created") == 0)
+ {
+ g_assert (!saw_deleted);
+ saw_created = TRUE;
+ }
+ else if (g_strcmp0 (json_object_get_string_member (event, "event"), "deleted") == 0)
+ {
+ g_assert (saw_created);
+ saw_deleted= TRUE;
+ }
+ }
+ json_object_unref (event);
+ }
+
+ g_assert (saw_created && saw_deleted);
+}
+
+static void
+test_dir_simple (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *event, *control;
+ gchar *base = g_path_get_basename (tc->test_path);
+
+ set_contents (tc->test_path, "Hello!");
+
+ setup_fslist_channel (tc, tc->test_dir, TRUE);
+
+ event = recv_json (tc);
+ g_assert_cmpstr (json_object_get_string_member (event, "event"), ==, "present");
+ g_assert_cmpstr (json_object_get_string_member (event, "path"), ==, base);
+ g_assert_cmpstr (json_object_get_string_member (event, "type"), ==, "file");
+ g_assert_cmpstr (json_object_get_string_member (event, "owner"), ==, g_get_user_name());
+ g_assert_cmpstr (json_object_get_string_member (event, "group"), !=, NULL);
+ g_assert_cmpint (json_object_get_int_member (event, "size"), ==, 6);
+ g_assert_cmpint (json_object_get_int_member (event, "modified"), >, 1610000000);
+ json_object_unref (event);
+
+ control = recv_control (tc);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ g_free (base);
+
+ close_channel (tc, NULL);
+ wait_channel_closed (tc);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+}
+
+static void
+test_dir_simple_no_watch (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *event, *control;
+ gchar *base = g_path_get_basename (tc->test_path);
+
+ set_contents (tc->test_path, "Hello!");
+
+ setup_fslist_channel (tc, tc->test_dir, FALSE);
+
+ event = recv_json (tc);
+ g_assert_cmpstr (json_object_get_string_member (event, "event"), ==, "present");
+ g_assert_cmpstr (json_object_get_string_member (event, "path"), ==, base);
+ g_assert_cmpstr (json_object_get_string_member (event, "type"), ==, "file");
+ g_assert_cmpstr (json_object_get_string_member (event, "owner"), ==, g_get_user_name());
+ g_assert_cmpstr (json_object_get_string_member (event, "group"), !=, NULL);
+ g_assert_cmpint (json_object_get_int_member (event, "size"), ==, 6);
+ g_assert_cmpint (json_object_get_int_member (event, "modified"), >, 1610000000);
+ json_object_unref (event);
+
+ control = recv_control (tc);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ g_free (base);
+
+ // channel should be closed
+ g_assert (tc->channel_closed);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+}
+
+static void
+test_dir_early_close (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *control;
+
+ set_contents (tc->test_path, "Hello!");
+
+ setup_fslist_channel (tc, tc->test_dir, TRUE);
+ close_channel (tc, NULL);
+
+ wait_channel_closed (tc);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+}
+
+static void
+test_dir_watch (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *event, *control;
+
+ setup_fslist_channel (tc, tc->test_dir, TRUE);
+
+ control = recv_control (tc);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+
+ set_contents (tc->test_path, "Hello!");
+
+ GFile *dir = g_file_new_for_path (tc->test_subdir);
+ g_assert (g_file_make_directory (dir, NULL, NULL));
+ g_object_unref (dir);
+
+ GFile *link = g_file_new_for_path (tc->test_link);
+ g_assert (g_file_make_symbolic_link (link, tc->test_path, NULL, NULL));
+ g_object_unref (link);
+
+ gboolean saw_created = FALSE;
+ gboolean saw_created_dir = FALSE;
+ gboolean saw_created_link = FALSE;
+ gboolean saw_deleted = FALSE;
+
+ while (!(saw_created && saw_deleted && saw_created_dir && saw_created_link) && !tc->channel_closed)
+ {
+ event = recv_json (tc);
+ if (g_strcmp0 (json_object_get_string_member (event, "path"), tc->test_path) == 0)
+ {
+ if (g_strcmp0 (json_object_get_string_member (event, "event"), "created") == 0)
+ {
+ g_assert (!saw_deleted);
+ g_assert_cmpstr (json_object_get_string_member (event, "type"), ==, "file");
+ g_assert (unlink (tc->test_path) >= 0);
+ saw_created = TRUE;
+ }
+ else if (g_strcmp0 (json_object_get_string_member (event, "event"), "deleted") == 0)
+ {
+ g_assert (saw_created);
+ saw_deleted= TRUE;
+ }
+ }
+ if (g_strcmp0 (json_object_get_string_member (event, "path"), tc->test_link) == 0)
+ {
+ if (g_strcmp0 (json_object_get_string_member (event, "event"), "created") == 0)
+ {
+ g_assert_cmpstr (json_object_get_string_member (event, "type"), ==, "link");
+ g_assert (!saw_created_link);
+ saw_created_link = TRUE;
+ }
+ }
+ if (g_strcmp0 (json_object_get_string_member (event, "path"), tc->test_subdir) == 0)
+ {
+ if (g_strcmp0 (json_object_get_string_member (event, "event"), "created") == 0)
+ {
+ g_assert_cmpstr (json_object_get_string_member (event, "type"), ==, "directory");
+ g_assert (!saw_created_dir);
+ saw_created_dir = TRUE;
+ }
+ }
+ json_object_unref (event);
+ }
+
+ g_assert (saw_created && saw_deleted && saw_created_link && saw_created_dir);
+
+ close_channel (tc, NULL);
+ wait_channel_closed (tc);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert (json_object_get_member (control, "problem") == NULL);
+}
+
+static void
+test_dir_list_fail (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *control;
+ setup_fslist_channel (tc, tc->test_path, FALSE);
+
+ // Channel should close automatically
+ wait_channel_closed (tc);
+
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "problem"), ==, "not-found");
+}
+
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/fsread/simple", TestCase, NULL,
+ setup, test_read_simple, teardown);
+ g_test_add ("/fsread/binary", TestCase, NULL,
+ setup, test_read_binary_size_hint, teardown);
+ g_test_add ("/fsread/non-existent", TestCase, NULL,
+ setup, test_read_non_existent, teardown);
+ g_test_add ("/fsread/denied", TestCase, NULL,
+ setup, test_read_denied, teardown);
+ g_test_add ("/fsread/changed", TestCase, NULL,
+ setup, test_read_changed, teardown);
+ g_test_add ("/fsread/replaced", TestCase, NULL,
+ setup, test_read_replaced, teardown);
+ g_test_add ("/fsread/removed", TestCase, NULL,
+ setup, test_read_removed, teardown);
+ g_test_add ("/fsread/non-mmappable", TestCase, NULL,
+ setup, test_read_non_mmappable, teardown);
+
+ g_test_add ("/fsreplace/simple", TestCase, NULL,
+ setup, test_write_simple, teardown);
+ g_test_add ("/fsreplace/multiple", TestCase, NULL,
+ setup, test_write_multiple, teardown);
+ g_test_add ("/fsreplace/remove", TestCase, NULL,
+ setup, test_write_remove, teardown);
+ g_test_add ("/fsreplace/remove-nonexistent", TestCase, NULL,
+ setup, test_write_remove_nonexistent, teardown);
+ g_test_add ("/fsreplace/empty", TestCase, NULL,
+ setup, test_write_empty, teardown);
+ g_test_add ("/fsreplace/denied", TestCase, NULL,
+ setup, test_write_denied, teardown);
+ g_test_add ("/fsreplace/expect-non-existent", TestCase, NULL,
+ setup, test_write_expect_non_existent, teardown);
+ g_test_add ("/fsreplace/expect-non-existent-fail", TestCase, NULL,
+ setup, test_write_expect_non_existent_fail, teardown);
+ g_test_add ("/fsreplace/expect-tag", TestCase, NULL,
+ setup, test_write_expect_tag, teardown);
+ g_test_add ("/fsreplace/expect-tag-fail", TestCase, NULL,
+ setup, test_write_expect_tag_fail, teardown);
+
+ g_test_add ("/fswatch/simple", TestCase, NULL,
+ setup, test_watch_simple, teardown);
+ g_test_add ("/fswatch/remove", TestCase, NULL,
+ setup, test_watch_remove, teardown);
+ g_test_add ("/fswatch/directory", TestCase, NULL,
+ setup, test_watch_directory, teardown);
+
+ g_test_add ("/fslist/simple", TestCase, NULL,
+ setup, test_dir_simple, teardown);
+ g_test_add ("/fslist/simple_no_watch", TestCase, NULL,
+ setup, test_dir_simple_no_watch, teardown);
+ g_test_add ("/fslist/early-close", TestCase, NULL,
+ setup, test_dir_early_close, teardown);
+ g_test_add ("/fslist/watch", TestCase, NULL,
+ setup, test_dir_watch, teardown);
+ g_test_add ("/fslist/list_fail", TestCase, NULL,
+ setup, test_dir_list_fail, teardown);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-httpstream.c b/src/bridge/test-httpstream.c
new file mode 100644
index 0000000..0a5f12e
--- /dev/null
+++ b/src/bridge/test-httpstream.c
@@ -0,0 +1,904 @@
+
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpithttpstream.h"
+
+#include "common/cockpitjson.h"
+#include "common/cockpitwebresponse.h"
+#include "common/cockpitwebserver.h"
+#include "testlib/cockpittest.h"
+#include "testlib/mock-transport.h"
+
+#include <string.h>
+
+/* Declared in cockpitwebserver.c */
+extern gboolean cockpit_webserver_want_certificate;
+
+/* JSON dict snippet for headers that are present in every request */
+#define STATIC_HEADERS "\"Cross-Origin-Resource-Policy\":\"same-origin\",\"Referrer-Policy\":\"no-referrer\",\"X-Content-Type-Options\":\"nosniff\",\"X-DNS-Prefetch-Control\":\"off\",\"X-Frame-Options\":\"sameorigin\""
+
+static void
+on_closed_set_flag (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gboolean *flag = user_data;
+ g_assert (flag != NULL);
+ g_assert (*flag != TRUE);
+ *flag = TRUE;
+}
+
+typedef struct {
+ CockpitWebServer *web_server;
+ guint port;
+ MockTransport *transport;
+ const char *host;
+} TestGeneral;
+
+
+static gchar *
+non_local_ip (void)
+{
+ GInetAddress *inet;
+ gchar *str = NULL;
+ inet = cockpit_test_find_non_loopback_address ();
+ if (inet)
+ {
+ str = g_inet_address_to_string (inet);
+ g_object_unref (inet);
+ }
+
+ return str;
+}
+
+static void
+setup_general (TestGeneral *tt,
+ gconstpointer host_fixture)
+{
+ tt->web_server = cockpit_web_server_new (NULL, COCKPIT_WEB_SERVER_NONE);
+ tt->port = cockpit_web_server_add_inet_listener (tt->web_server, NULL, 0, NULL);
+ cockpit_web_server_start (tt->web_server);
+ tt->transport = mock_transport_new ();
+ tt->host = host_fixture;
+}
+
+static void
+teardown_general (TestGeneral *tt,
+ gconstpointer unused)
+{
+ g_object_unref (tt->web_server);
+ g_object_unref (tt->transport);
+
+ cockpit_assert_expected ();
+}
+
+static gboolean
+handle_host_header (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ gpointer user_data)
+{
+ TestGeneral *tt = user_data;
+ const gchar *data = "Da Da Da";
+ gchar *expected;
+ GBytes *bytes;
+
+ expected = g_strdup_printf ("%s:%d", tt->host, tt->port);
+ g_assert_cmpstr (g_hash_table_lookup (headers, "Host"), ==, expected);
+ g_free (expected);
+
+ bytes = g_bytes_new_static (data, strlen (data));
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+
+ return TRUE;
+}
+
+/* Compare subsequent "{ control JSON }" and "body" channel messages against expected values. */
+static void
+assert_channel_response (MockTransport *transport,
+ const gchar *channel_id,
+ const gchar *expected_control,
+ const gchar *expected_body)
+{
+ GBytes *data;
+
+ data = mock_transport_pop_channel (transport, channel_id);
+ JsonNode *node = cockpit_json_parse (g_bytes_get_data (data, NULL), -1, NULL);
+ cockpit_assert_json_eq (json_node_get_object (node), expected_control);
+ json_node_unref (node);
+
+ data = mock_transport_pop_channel (transport, channel_id);
+ cockpit_assert_bytes_eq (data, expected_body, -1);
+}
+
+static void
+test_host_header (TestGeneral *tt,
+ gconstpointer unused)
+{
+ CockpitChannel *channel;
+ GBytes *bytes;
+ JsonObject *options;
+ const gchar *control;
+ gboolean closed;
+
+ if (tt->host == NULL)
+ {
+ g_test_skip ("Couldn't determine non local ip");
+ return;
+ }
+
+ g_signal_connect (tt->web_server, "handle-resource::/", G_CALLBACK (handle_host_header), tt);
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", tt->port);
+ json_object_set_string_member (options, "payload", "http-stream1");
+ json_object_set_string_member (options, "method", "GET");
+ json_object_set_string_member (options, "path", "/");
+
+ if (g_strcmp0 (tt->host, "localhost") != 0)
+ json_object_set_string_member (options, "address", tt->host);
+
+ channel = g_object_new (COCKPIT_TYPE_HTTP_STREAM,
+ "transport", tt->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+
+ /* Tell HTTP we have no more data to send */
+ control = "{\"command\": \"done\", \"channel\": \"444\"}";
+ bytes = g_bytes_new_static (control, strlen (control));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tt->transport), NULL, bytes);
+ g_bytes_unref (bytes);
+
+ closed = FALSE;
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+ while (!closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ assert_channel_response (tt->transport, "444",
+ "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS "}}",
+ "Da Da Da");
+}
+
+static gboolean
+handle_default (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ gpointer user_data)
+{
+ const gchar *data = "Da Da Da";
+ GBytes *bytes;
+
+ bytes = g_bytes_new_static (data, strlen (data));
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+
+ return TRUE;
+}
+
+static void
+test_http_stream2 (TestGeneral *tt,
+ gconstpointer unused)
+{
+ CockpitChannel *channel;
+ GBytes *bytes;
+ JsonObject *options;
+ const gchar *control;
+ JsonObject *object;
+ gboolean closed;
+ GBytes *data;
+ guint count;
+
+ g_signal_connect (tt->web_server, "handle-resource::/", G_CALLBACK (handle_default), tt);
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", tt->port);
+ json_object_set_string_member (options, "payload", "http-stream2");
+ json_object_set_string_member (options, "method", "GET");
+ json_object_set_string_member (options, "path", "/");
+
+ channel = g_object_new (COCKPIT_TYPE_HTTP_STREAM,
+ "transport", tt->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+
+ /* Tell HTTP we have no more data to send */
+ control = "{\"command\": \"done\", \"channel\": \"444\"}";
+ bytes = g_bytes_new_static (control, strlen (control));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tt->transport), NULL, bytes);
+ g_bytes_unref (bytes);
+
+ closed = FALSE;
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+ while (!closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ object = mock_transport_pop_control (tt->transport);
+ cockpit_assert_json_eq (object, "{\"command\":\"ready\",\"channel\":\"444\"}");
+ object = mock_transport_pop_control (tt->transport);
+ cockpit_assert_json_eq (object, "{\"command\":\"response\",\"channel\":\"444\",\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS "}}");
+
+ data = mock_transport_combine_output (tt->transport, "444", &count);
+ cockpit_assert_bytes_eq (data, "Da Da Da", -1);
+ g_assert_cmpuint (count, ==, 1);
+ g_bytes_unref (data);
+}
+
+static void
+test_cannot_connect (TestGeneral *tt,
+ gconstpointer unused)
+{
+ CockpitChannel *channel;
+ GBytes *bytes;
+ JsonObject *options;
+ const gchar *control;
+ JsonObject *object;
+ gboolean closed;
+
+ cockpit_expect_log ("cockpit-bridge", G_LOG_LEVEL_MESSAGE, "*couldn't connect*");
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", 5555);
+ json_object_set_string_member (options, "payload", "http-stream2");
+ json_object_set_string_member (options, "method", "GET");
+ json_object_set_string_member (options, "path", "/");
+ json_object_set_string_member (options, "address", "0.0.0.0");
+
+ channel = g_object_new (COCKPIT_TYPE_HTTP_STREAM,
+ "transport", tt->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+
+ /* Tell HTTP we have no more data to send */
+ control = "{\"command\": \"done\", \"channel\": \"444\"}";
+ bytes = g_bytes_new_static (control, strlen (control));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tt->transport), NULL, bytes);
+ g_bytes_unref (bytes);
+
+ closed = FALSE;
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+ while (!closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ object = mock_transport_pop_control (tt->transport);
+ cockpit_assert_json_eq (object, "{\"command\":\"close\",\"channel\":\"444\",\"problem\":\"not-found\"}");
+}
+
+/* -----------------------------------------------------------------------------
+ * Test
+ */
+
+typedef struct {
+ gchar *problem;
+ gboolean done;
+} TestResult;
+
+/*
+ * Yes this is a magic number. It's the lowest number that would
+ * trigger a bug where chunked data would be rejected due to an incomplete read.
+ */
+const gint MAGIC_NUMBER = 3068;
+
+static gboolean
+handle_chunked (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ gpointer user_data)
+{
+ GBytes *bytes;
+ GHashTable *h = g_hash_table_new (g_str_hash, g_str_equal);
+
+ cockpit_web_response_headers_full (response, 200,
+ "OK", -1, h);
+ bytes = g_bytes_new_take (g_strdup_printf ("%0*d",
+ MAGIC_NUMBER, 0),
+ MAGIC_NUMBER);
+ cockpit_web_response_queue (response, bytes);
+ cockpit_web_response_complete (response);
+
+ g_bytes_unref (bytes);
+ g_hash_table_unref (h);
+ return TRUE;
+}
+
+static void
+on_channel_close (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ TestResult *tr = user_data;
+ g_assert (tr->done == FALSE);
+ tr->done = TRUE;
+ tr->problem = g_strdup (problem);
+}
+
+static void
+on_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ g_assert_not_reached ();
+}
+
+static void
+test_http_chunked (void)
+{
+ MockTransport *transport = NULL;
+ CockpitChannel *channel = NULL;
+ CockpitWebServer *web_server = NULL;
+ JsonObject *options = NULL;
+ JsonObject *headers = NULL;
+ TestResult *tr = g_slice_new (TestResult);
+
+ GBytes *bytes = NULL;
+
+ const gchar *control;
+
+ web_server = cockpit_web_server_new (NULL, COCKPIT_WEB_SERVER_NONE);
+ guint16 port = cockpit_web_server_add_inet_listener (web_server, NULL, 0, NULL);
+ g_assert (port != 0);
+ g_signal_connect (web_server, "handle-resource::/",
+ G_CALLBACK (handle_chunked), NULL);
+
+ cockpit_web_server_start (web_server);
+
+ transport = mock_transport_new ();
+ g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", port);
+ json_object_set_string_member (options, "payload", "http-stream1");
+ json_object_set_string_member (options, "method", "GET");
+ json_object_set_string_member (options, "path", "/");
+
+ headers = json_object_new ();
+ json_object_set_string_member (headers, "Pragma", "no-cache");
+ json_object_set_object_member (options, "headers", headers);
+
+ channel = g_object_new (COCKPIT_TYPE_HTTP_STREAM,
+ "transport", transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+
+ /* Tell HTTP we have no more data to send */
+ control = "{\"command\": \"done\", \"channel\": \"444\"}";
+ bytes = g_bytes_new_static (control, strlen (control));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), NULL, bytes);
+ g_bytes_unref (bytes);
+
+ tr->done = FALSE;
+ g_signal_connect (channel, "closed", G_CALLBACK (on_channel_close), tr);
+
+ while (tr->done == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tr->problem, ==, NULL);
+
+
+
+ g_autofree gchar *expected_body = g_strdup_printf ("%0*d", MAGIC_NUMBER, 0);
+
+ assert_channel_response (transport, "444",
+ "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS "}}",
+ expected_body);
+
+ g_object_unref (transport);
+ g_object_add_weak_pointer (G_OBJECT (channel), (gpointer *)&channel);
+ g_object_unref (channel);
+ g_assert (channel == NULL);
+ g_clear_object (&web_server);
+
+ g_free (tr->problem);
+ g_slice_free (TestResult, tr);
+}
+
+static void
+test_parse_keep_alive (void)
+{
+ const gchar *version;
+ GHashTable *headers;
+ gboolean keep_alive;
+
+ headers = g_hash_table_new (g_str_hash, g_str_equal);
+
+ version = "HTTP/1.1";
+ g_hash_table_insert (headers, "Connection", "keep-alive");
+
+ keep_alive = cockpit_http_stream_parse_keep_alive (version, headers);
+ g_assert (keep_alive == TRUE);
+
+ version = "HTTP/1.0";
+ keep_alive = cockpit_http_stream_parse_keep_alive (version, headers);
+ g_assert (keep_alive == TRUE);
+
+
+ g_hash_table_remove (headers, "Connection");
+
+ keep_alive = cockpit_http_stream_parse_keep_alive (version, headers);
+ g_assert (keep_alive == FALSE);
+
+ version = "HTTP/1.1";
+ keep_alive = cockpit_http_stream_parse_keep_alive (version, headers);
+ g_assert (keep_alive == TRUE);
+
+ g_hash_table_destroy (headers);
+}
+
+typedef struct {
+ GTlsCertificate *certificate;
+ CockpitWebServer *web_server;
+ guint port;
+ MockTransport *transport;
+ GTlsCertificate *peer;
+} TestTls;
+
+static gboolean
+handle_test (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ gpointer user_data)
+{
+ const gchar *data = "Oh Marmalaade!";
+ GTlsConnection *connection;
+ TestTls *test = user_data;
+ GBytes *bytes;
+
+ bytes = g_bytes_new_static (data, strlen (data));
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+
+ connection = G_TLS_CONNECTION (cockpit_web_response_get_stream (response));
+
+ g_clear_object (&test->peer);
+ test->peer = g_tls_connection_get_peer_certificate (connection);
+ if (test->peer)
+ g_object_ref (test->peer);
+
+ return TRUE;
+}
+
+static void
+setup_tls (TestTls *test,
+ gconstpointer data)
+{
+ GError *error = NULL;
+
+ /* don't require system SSL cert database in build environments */
+ cockpit_expect_possible_log ("GLib-Net", G_LOG_LEVEL_WARNING, "couldn't load TLS file database: * No such file or directory");
+
+ test->certificate = g_tls_certificate_new_from_files (SRCDIR "/src/bridge/mock-server.crt",
+ SRCDIR "/src/bridge/mock-server.key", &error);
+ g_assert_no_error (error);
+
+ test->web_server = cockpit_web_server_new (test->certificate, COCKPIT_WEB_SERVER_NONE);
+ test->port = cockpit_web_server_add_inet_listener (test->web_server, NULL, 0, &error);
+ g_assert_no_error (error);
+ g_assert (test->port != 0);
+
+ g_signal_connect (test->web_server, "handle-resource::/test", G_CALLBACK (handle_test), test);
+
+ cockpit_web_server_start (test->web_server);
+
+ test->transport = mock_transport_new ();
+ g_signal_connect (test->transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+}
+
+static void
+teardown_tls (TestTls *test,
+ gconstpointer data)
+{
+ g_object_unref (test->certificate);
+ g_object_unref (test->web_server);
+ g_object_unref (test->transport);
+ g_clear_object (&test->peer);
+ cockpit_webserver_want_certificate = FALSE;
+}
+
+static void
+test_tls_basic (TestTls *test,
+ gconstpointer unused)
+{
+ gboolean closed = FALSE;
+ CockpitChannel *channel;
+ JsonObject *options;
+ const gchar *control;
+ GBytes *bytes;
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", test->port);
+ json_object_set_string_member (options, "payload", "http-stream1");
+ json_object_set_string_member (options, "method", "GET");
+ json_object_set_string_member (options, "path", "/test");
+ json_object_set_object_member (options, "tls", json_object_new ());
+
+ channel = g_object_new (COCKPIT_TYPE_HTTP_STREAM,
+ "transport", test->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+
+ /* Tell HTTP we have no more data to send */
+ control = "{\"command\": \"done\", \"channel\": \"444\"}";
+ bytes = g_bytes_new_static (control, strlen (control));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (test->transport), NULL, bytes);
+ g_bytes_unref (bytes);
+
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+
+ while (closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ assert_channel_response (test->transport, "444",
+ "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS "}}",
+ "Oh Marmalaade!");
+
+ g_object_add_weak_pointer (G_OBJECT (channel), (gpointer *)&channel);
+ g_object_unref (channel);
+ g_assert (channel == NULL);
+}
+
+static const gchar fixture_tls_certificate_data[] =
+"{ \"certificate\": { \"data\": "
+"\"-----BEGIN CERTIFICATE-----\n"
+"MIICxzCCAa+gAwIBAgIJANDrBNw3XYJ0MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV\n"
+"BAMMCWxvY2FsaG9zdDAgFw0xNTAzMjUxMDMzMzRaGA8yMTE1MDMwMTEwMzMzNFow\n"
+"FDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n"
+"CgKCAQEA8l1q01B5N/biaFDazUtuPuOrFsLOC67LX1iiE62guchEf9FyEagglGzt\n"
+"XOSCpY/qX0HWmIkE3Pqotb8lPQ0mUHleYCvzY85cFmj4mu+rDIPxK/lw37Xu00iP\n"
+"/rbcCA6K6dgMjp0TJzZvMnU2PywtFqDpw6ZchcMi517keMfLwscUC/7Y80lP0PGA\n"
+"1wTDaYoxuMlUhqTTfdLoBZ73eA9YzgqBeZ9ePxoUFk9AtJtlOlR60mGbEOweDUfc\n"
+"l1biKtarDW5SJYbVTFjWdPsCV6czZndfVKAAkDd+bsbFMcEiq/doHU092Yy3sZ9g\n"
+"hnOBw5sCq8iTXQ9cmejxUrsu/SvL3QIDAQABoxowGDAJBgNVHRMEAjAAMAsGA1Ud\n"
+"DwQEAwIF4DANBgkqhkiG9w0BAQsFAAOCAQEAalykXV+z1tQOv1ZRvJmppjEIYTa3\n"
+"pFehy97BiNGERTQJQDSzOgptIaCJb1vE34KNL349QEO4F8XTPWhwsCAXNTBN4yhm\n"
+"NJ6qbYkz0HbBmdM4k0MgbB9VG00Hy+TmwEt0zVryICZY4IomKmS1No0Lai5hOqdz\n"
+"afUMVIIYjVB1WYIsIaXXug7Mik/O+6K5hIbqm9HkwRwfoVaOLNG9EPUM14vFnN5p\n"
+"EyHSBByk0mOU8EUK/qsAnbTwABEKsMxCopmvPTguGHTwllEvxPgt5BcYMU9oXlvc\n"
+"cSvnU4a6M2qxQn3LUqxENh9QaQ8vV4l/avZBi1cFKVs1rza36eOGxrJxQw==\n"
+"-----END CERTIFICATE-----\""
+"}, \"key\": { \"data\": "
+"\"-----BEGIN PRIVATE KEY-----\n"
+"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDyXWrTUHk39uJo\n"
+"UNrNS24+46sWws4LrstfWKITraC5yER/0XIRqCCUbO1c5IKlj+pfQdaYiQTc+qi1\n"
+"vyU9DSZQeV5gK/NjzlwWaPia76sMg/Er+XDfte7TSI/+ttwIDorp2AyOnRMnNm8y\n"
+"dTY/LC0WoOnDplyFwyLnXuR4x8vCxxQL/tjzSU/Q8YDXBMNpijG4yVSGpNN90ugF\n"
+"nvd4D1jOCoF5n14/GhQWT0C0m2U6VHrSYZsQ7B4NR9yXVuIq1qsNblIlhtVMWNZ0\n"
+"+wJXpzNmd19UoACQN35uxsUxwSKr92gdTT3ZjLexn2CGc4HDmwKryJNdD1yZ6PFS\n"
+"uy79K8vdAgMBAAECggEAILEJH8fTEgFzOK7vVJHAJSuAgGl2cYz6Uboa4pyg+W5S\n"
+"DwupX0hWXK70tXr9RGfNLVwsHhcdWNFWwG0wELQdXu2AFWjYQ7YqJbuzDPMXF3EU\n"
+"ruHOn95igI1hHvJ7a3rKshA6YWI+myN0jFHTJ2JGEq9R2Nov0LspkhvypXgNvA/r\n"
+"JfFZ9IsPJZDWCnGXkPLlW2X1XEXw2BPs8ib+ZkbzGNiLsy/i4M/oA+g6lz4LU/ll\n"
+"J6cLhwPrBu02+PJt7MaYaNk5zqhyJs0AMjeBlNnXFIWAlTrIe/h8z/gL8ABrYWAA\n"
+"1kgZ11GO8bNAEfLOIUrA1/vq9aK00WDwFLXWJdVE4QKBgQD+R/J+AbYSImeoAj/3\n"
+"hfsFkaUNLyw1ZEO4LG2id0dnve1paL6Y/uXKKqxq0jiyMLT243Vi+1fzth7RNXOl\n"
+"ui0nnVWO7x68FsYcdIM7w+tryh2Y+UhCfwNCakM0GTohcXqFUEzHcwuOv8hAfRQ5\n"
+"jPBCwJdUHpIimVOo5/WRbQGW+wKBgQD0ANkof+jagdNqOkCvFnTPiFlPYrpDzeU5\n"
+"ZxhLlVxnr6G2MPoUO0IqTWVA7uCn29i0yUUXAtRHrkNI1EtKXRIUe2bChVegTBHx\n"
+"26PqXEOonSUJdpUzyzXVX2vSqICm0tTbqyZ0GbjP4y5qQOQHdTGFsHDfSTa5//P+\n"
+"0BLpci4RBwKBgQDBR8DrxLM3b41o6GTk6aNXpVBXCC9LWi4bVTH0l0PgeD54rBSM\n"
+"SNwz4mHyRF6yG1HChDybAz/kUN912HJSW4StIuuA3QN4prrpsCp8iDxvT09WEs25\n"
+"NcAtgIYamL5V42Lk6Jej1y/GzsIROsHfyOBrbObaGu6re+5aag5//uKBdwKBgQDp\n"
+"i4ZPBV7TBkBdBLS04UGdAly5Zz3xeDlW4B6Y+bUgaTLXN7mlc7K42qt3oyzUfdDF\n"
+"+X9vrv2QPnOYWdpWqw6LHDIXLZnZi/YBEMGrp/P6h67Th/T3RiGYwWRqlW3OPy4N\n"
+"s5tytMv37vKWMNYRbVKhK2hdz63aCep4kqAHYYpGMQKBgF83LTyRFwGFos/wDrgY\n"
+"eieLiipmdXGvlrBq6SBzKglIYwNRSGiWkXAuHRzD/2S546ioQKZr7AKuijKGdLMz\n"
+"ABVl/bqqqRXSDbvf+XEdU2rJpxhYWxlsJZMFBFIwuxR2jRqmCgbCvoZQcbIr1ZLr\n"
+"02eC2pQ5eio2+CKqBfqxbnwk\n"
+"-----END PRIVATE KEY-----\""
+" } }";
+
+static const gchar fixture_tls_certificate_file[] =
+"{ \"certificate\": { \"file\": \"" SRCDIR "/src/bridge/mock-client.crt\" },"
+"\"key\": { \"file\": \"" SRCDIR "/src/bridge/mock-client.key\" } }";
+
+static const gchar fixture_tls_certificate_data_file[] =
+"{ \"certificate\": { \"data\": "
+"\"-----BEGIN CERTIFICATE-----\n"
+"MIICxzCCAa+gAwIBAgIJANDrBNw3XYJ0MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV\n"
+"BAMMCWxvY2FsaG9zdDAgFw0xNTAzMjUxMDMzMzRaGA8yMTE1MDMwMTEwMzMzNFow\n"
+"FDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n"
+"CgKCAQEA8l1q01B5N/biaFDazUtuPuOrFsLOC67LX1iiE62guchEf9FyEagglGzt\n"
+"XOSCpY/qX0HWmIkE3Pqotb8lPQ0mUHleYCvzY85cFmj4mu+rDIPxK/lw37Xu00iP\n"
+"/rbcCA6K6dgMjp0TJzZvMnU2PywtFqDpw6ZchcMi517keMfLwscUC/7Y80lP0PGA\n"
+"1wTDaYoxuMlUhqTTfdLoBZ73eA9YzgqBeZ9ePxoUFk9AtJtlOlR60mGbEOweDUfc\n"
+"l1biKtarDW5SJYbVTFjWdPsCV6czZndfVKAAkDd+bsbFMcEiq/doHU092Yy3sZ9g\n"
+"hnOBw5sCq8iTXQ9cmejxUrsu/SvL3QIDAQABoxowGDAJBgNVHRMEAjAAMAsGA1Ud\n"
+"DwQEAwIF4DANBgkqhkiG9w0BAQsFAAOCAQEAalykXV+z1tQOv1ZRvJmppjEIYTa3\n"
+"pFehy97BiNGERTQJQDSzOgptIaCJb1vE34KNL349QEO4F8XTPWhwsCAXNTBN4yhm\n"
+"NJ6qbYkz0HbBmdM4k0MgbB9VG00Hy+TmwEt0zVryICZY4IomKmS1No0Lai5hOqdz\n"
+"afUMVIIYjVB1WYIsIaXXug7Mik/O+6K5hIbqm9HkwRwfoVaOLNG9EPUM14vFnN5p\n"
+"EyHSBByk0mOU8EUK/qsAnbTwABEKsMxCopmvPTguGHTwllEvxPgt5BcYMU9oXlvc\n"
+"cSvnU4a6M2qxQn3LUqxENh9QaQ8vV4l/avZBi1cFKVs1rza36eOGxrJxQw==\n"
+"-----END CERTIFICATE-----\""
+"}, \"key\": { \"file\": \"" SRCDIR "/src/bridge/mock-client.key\""
+"} }";
+
+static const gchar fixture_tls_certificate_file_data[] =
+"{ \"certificate\": { \"file\": \"" SRCDIR "/src/bridge/mock-client.crt\""
+"}, \"key\": { \"data\": "
+"\"-----BEGIN PRIVATE KEY-----\n"
+"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDyXWrTUHk39uJo\n"
+"UNrNS24+46sWws4LrstfWKITraC5yER/0XIRqCCUbO1c5IKlj+pfQdaYiQTc+qi1\n"
+"vyU9DSZQeV5gK/NjzlwWaPia76sMg/Er+XDfte7TSI/+ttwIDorp2AyOnRMnNm8y\n"
+"dTY/LC0WoOnDplyFwyLnXuR4x8vCxxQL/tjzSU/Q8YDXBMNpijG4yVSGpNN90ugF\n"
+"nvd4D1jOCoF5n14/GhQWT0C0m2U6VHrSYZsQ7B4NR9yXVuIq1qsNblIlhtVMWNZ0\n"
+"+wJXpzNmd19UoACQN35uxsUxwSKr92gdTT3ZjLexn2CGc4HDmwKryJNdD1yZ6PFS\n"
+"uy79K8vdAgMBAAECggEAILEJH8fTEgFzOK7vVJHAJSuAgGl2cYz6Uboa4pyg+W5S\n"
+"DwupX0hWXK70tXr9RGfNLVwsHhcdWNFWwG0wELQdXu2AFWjYQ7YqJbuzDPMXF3EU\n"
+"ruHOn95igI1hHvJ7a3rKshA6YWI+myN0jFHTJ2JGEq9R2Nov0LspkhvypXgNvA/r\n"
+"JfFZ9IsPJZDWCnGXkPLlW2X1XEXw2BPs8ib+ZkbzGNiLsy/i4M/oA+g6lz4LU/ll\n"
+"J6cLhwPrBu02+PJt7MaYaNk5zqhyJs0AMjeBlNnXFIWAlTrIe/h8z/gL8ABrYWAA\n"
+"1kgZ11GO8bNAEfLOIUrA1/vq9aK00WDwFLXWJdVE4QKBgQD+R/J+AbYSImeoAj/3\n"
+"hfsFkaUNLyw1ZEO4LG2id0dnve1paL6Y/uXKKqxq0jiyMLT243Vi+1fzth7RNXOl\n"
+"ui0nnVWO7x68FsYcdIM7w+tryh2Y+UhCfwNCakM0GTohcXqFUEzHcwuOv8hAfRQ5\n"
+"jPBCwJdUHpIimVOo5/WRbQGW+wKBgQD0ANkof+jagdNqOkCvFnTPiFlPYrpDzeU5\n"
+"ZxhLlVxnr6G2MPoUO0IqTWVA7uCn29i0yUUXAtRHrkNI1EtKXRIUe2bChVegTBHx\n"
+"26PqXEOonSUJdpUzyzXVX2vSqICm0tTbqyZ0GbjP4y5qQOQHdTGFsHDfSTa5//P+\n"
+"0BLpci4RBwKBgQDBR8DrxLM3b41o6GTk6aNXpVBXCC9LWi4bVTH0l0PgeD54rBSM\n"
+"SNwz4mHyRF6yG1HChDybAz/kUN912HJSW4StIuuA3QN4prrpsCp8iDxvT09WEs25\n"
+"NcAtgIYamL5V42Lk6Jej1y/GzsIROsHfyOBrbObaGu6re+5aag5//uKBdwKBgQDp\n"
+"i4ZPBV7TBkBdBLS04UGdAly5Zz3xeDlW4B6Y+bUgaTLXN7mlc7K42qt3oyzUfdDF\n"
+"+X9vrv2QPnOYWdpWqw6LHDIXLZnZi/YBEMGrp/P6h67Th/T3RiGYwWRqlW3OPy4N\n"
+"s5tytMv37vKWMNYRbVKhK2hdz63aCep4kqAHYYpGMQKBgF83LTyRFwGFos/wDrgY\n"
+"eieLiipmdXGvlrBq6SBzKglIYwNRSGiWkXAuHRzD/2S546ioQKZr7AKuijKGdLMz\n"
+"ABVl/bqqqRXSDbvf+XEdU2rJpxhYWxlsJZMFBFIwuxR2jRqmCgbCvoZQcbIr1ZLr\n"
+"02eC2pQ5eio2+CKqBfqxbnwk\n"
+"-----END PRIVATE KEY-----\""
+" } }";
+
+static void
+test_tls_certificate (TestTls *test,
+ gconstpointer json)
+{
+ gboolean closed = FALSE;
+ CockpitChannel *channel;
+ JsonObject *options;
+ JsonObject *tls;
+ GError *error = NULL;
+ GTlsCertificate *cert;
+ const gchar *control;
+ GBytes *bytes;
+
+ /* tell server to request client cert */
+ cockpit_webserver_want_certificate = TRUE;
+
+ /* this happens twice, so once more in addition to the ignore in setup_tls */
+ cockpit_expect_possible_log ("GLib-Net", G_LOG_LEVEL_WARNING, "couldn't load TLS file database: * No such file or directory");
+
+ tls = cockpit_json_parse_object (json, -1, &error);
+ g_assert_no_error (error);
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", test->port);
+ json_object_set_string_member (options, "payload", "http-stream1");
+ json_object_set_string_member (options, "method", "GET");
+ json_object_set_string_member (options, "path", "/test");
+ json_object_set_object_member (options, "tls", tls);
+
+ channel = g_object_new (COCKPIT_TYPE_HTTP_STREAM,
+ "transport", test->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+
+ /* Tell HTTP we have no more data to send */
+ control = "{\"command\": \"done\", \"channel\": \"444\"}";
+ bytes = g_bytes_new_static (control, strlen (control));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (test->transport), NULL, bytes);
+ g_bytes_unref (bytes);
+
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+
+ while (closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ assert_channel_response (test->transport, "444",
+ "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS "}}",
+ "Oh Marmalaade!");
+
+ g_assert (test->peer != NULL);
+
+ /* Should have used our expected certificate */
+ cert = g_tls_certificate_new_from_files (SRCDIR "/src/bridge/mock-client.crt",
+ SRCDIR "/src/bridge/mock-client.key", &error);
+ g_assert_no_error (error);
+
+ g_assert (g_tls_certificate_is_same (test->peer, cert));
+ g_object_unref (cert);
+
+ g_object_add_weak_pointer (G_OBJECT (channel), (gpointer *)&channel);
+ g_object_unref (channel);
+ g_assert (channel == NULL);
+}
+
+static const gchar fixture_tls_authority_good[] =
+ "{ \"authority\": { \"file\": \"" SRCDIR "/src/bridge/mock-server.crt\" } }";
+
+static void
+test_tls_authority_good (TestTls *test,
+ gconstpointer json)
+{
+ gboolean closed = FALSE;
+ CockpitChannel *channel;
+ JsonObject *options;
+ JsonObject *tls;
+ GError *error = NULL;
+ const gchar *control;
+ GBytes *bytes;
+
+ tls = cockpit_json_parse_object (json, -1, &error);
+ g_assert_no_error (error);
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", test->port);
+ json_object_set_string_member (options, "payload", "http-stream1");
+ json_object_set_string_member (options, "method", "GET");
+ json_object_set_string_member (options, "path", "/test");
+ json_object_set_object_member (options, "tls", tls);
+
+ channel = g_object_new (COCKPIT_TYPE_HTTP_STREAM,
+ "transport", test->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+
+ /* Tell HTTP we have no more data to send */
+ control = "{\"command\": \"done\", \"channel\": \"444\"}";
+ bytes = g_bytes_new_static (control, strlen (control));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (test->transport), NULL, bytes);
+ g_bytes_unref (bytes);
+
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+
+ while (closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ assert_channel_response (test->transport, "444",
+ "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS "}}",
+ "Oh Marmalaade!");
+
+ g_object_add_weak_pointer (G_OBJECT (channel), (gpointer *)&channel);
+ g_object_unref (channel);
+ g_assert (channel == NULL);
+}
+
+static const gchar fixture_tls_authority_bad[] =
+ "{ \"certificate\": { \"file\": \"" SRCDIR "/src/bridge/mock-client.crt\" },"
+ " \"key\": { \"file\": \"" SRCDIR "/src/bridge/mock-client.key\" }, "
+ " \"authority\": { \"file\": \"" SRCDIR "/src/bridge/mock-client.crt\" } }";
+
+static void
+test_tls_authority_bad (TestTls *test,
+ gconstpointer json)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+ JsonObject *tls;
+ GError *error = NULL;
+ const gchar *control;
+ GBytes *bytes;
+ JsonObject *resp;
+
+ tls = cockpit_json_parse_object (json, -1, &error);
+ g_assert_no_error (error);
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", test->port);
+ json_object_set_string_member (options, "payload", "http-stream1");
+ json_object_set_string_member (options, "method", "GET");
+ json_object_set_string_member (options, "path", "/test");
+ json_object_set_object_member (options, "tls", tls);
+
+ channel = g_object_new (COCKPIT_TYPE_HTTP_STREAM,
+ "transport", test->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ cockpit_expect_log ("cockpit-bridge", G_LOG_LEVEL_MESSAGE,
+ "*Unacceptable TLS certificate");
+
+ json_object_unref (options);
+
+ /* Tell HTTP we have no more data to send */
+ control = "{\"command\": \"done\", \"channel\": \"444\"}";
+ bytes = g_bytes_new_static (control, strlen (control));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (test->transport), NULL, bytes);
+ g_bytes_unref (bytes);
+
+ while (mock_transport_count_sent (test->transport) < 1)
+ g_main_context_iteration (NULL, TRUE);
+
+ resp = mock_transport_pop_control (test->transport);
+ cockpit_assert_json_eq (resp, "{\"command\":\"close\",\"channel\":\"444\",\"problem\":\"unknown-hostkey\"}");
+
+ g_object_add_weak_pointer (G_OBJECT (channel), (gpointer *)&channel);
+ g_object_unref (channel);
+ g_assert (channel == NULL);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ char *ip = non_local_ip ();
+ int result;
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/http-stream/host-header", TestGeneral, "localhost",
+ setup_general, test_host_header, teardown_general);
+ g_test_add ("/http-stream/address-host-header", TestGeneral, ip,
+ setup_general, test_host_header, teardown_general);
+
+ g_test_add ("/http-stream/http-stream2", TestGeneral, NULL,
+ setup_general, test_http_stream2, teardown_general);
+ g_test_add ("/http-stream/cannot-connect", TestGeneral, NULL,
+ setup_general, test_cannot_connect, teardown_general);
+
+ g_test_add_func ("/http-stream/parse_keepalive", test_parse_keep_alive);
+ g_test_add_func ("/http-stream/http_chunked", test_http_chunked);
+
+ g_test_add ("/http-stream/tls/basic", TestTls, NULL,
+ setup_tls, test_tls_basic, teardown_tls);
+ g_test_add ("/http-stream/tls/certificate-data", TestTls, fixture_tls_certificate_data,
+ setup_tls, test_tls_certificate, teardown_tls);
+ g_test_add ("/http-stream/tls/certificate-file", TestTls, fixture_tls_certificate_file,
+ setup_tls, test_tls_certificate, teardown_tls);
+ g_test_add ("/http-stream/tls/certificate-data-file", TestTls, fixture_tls_certificate_data_file,
+ setup_tls, test_tls_certificate, teardown_tls);
+ g_test_add ("/http-stream/tls/certificate-file-data", TestTls, fixture_tls_certificate_file_data,
+ setup_tls, test_tls_certificate, teardown_tls);
+ g_test_add ("/http-stream/tls/authority-good", TestTls, fixture_tls_authority_good,
+ setup_tls, test_tls_authority_good, teardown_tls);
+ g_test_add ("/http-stream/tls/authority-bad", TestTls, fixture_tls_authority_bad,
+ setup_tls, test_tls_authority_bad, teardown_tls);
+
+ result = g_test_run ();
+ g_free (ip);
+ return result;
+}
diff --git a/src/bridge/test-metrics.c b/src/bridge/test-metrics.c
new file mode 100644
index 0000000..6aae2e7
--- /dev/null
+++ b/src/bridge/test-metrics.c
@@ -0,0 +1,998 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include <math.h>
+
+#include "cockpitmetrics.h"
+
+#include "cockpitinternalmetrics.h"
+
+#include "testlib/cockpittest.h"
+#include "common/cockpitjson.h"
+#include "testlib/mock-transport.h"
+
+#include <unistd.h>
+
+typedef struct {
+ MockTransport *transport;
+ CockpitMetrics *channel;
+ gchar *problem;
+ gboolean channel_closed;
+} TestCase;
+
+static void
+on_channel_close (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ TestCase *tc = user_data;
+ g_assert (tc->channel_closed == FALSE);
+ tc->problem = g_strdup (problem);
+ tc->channel_closed = TRUE;
+}
+
+static void
+on_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ g_assert_not_reached ();
+}
+
+typedef struct _CockpitMetrics MockMetrics;
+typedef struct _CockpitMetricsClass MockMetricsClass;
+
+GType mock_metrics_get_type (void);
+
+G_DEFINE_TYPE (MockMetrics, mock_metrics, COCKPIT_TYPE_METRICS);
+
+static void
+mock_metrics_init (MockMetrics *self)
+{
+ /* nothing */
+}
+
+static void
+mock_metrics_class_init (MockMetricsClass *self)
+{
+ /* nothing */
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ tc->transport = mock_transport_new ();
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+ tc->channel = g_object_new (mock_metrics_get_type (),
+ "transport", tc->transport,
+ "id", "1234",
+ NULL);
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_channel_close), tc);
+
+ /* Switch off compression by default. Compression is done by
+ * comparing two floating point values for exact equality, and we
+ * can't guarantee that we get the same behavior everywhere.
+ */
+ cockpit_metrics_set_compress (tc->channel, FALSE);
+}
+
+static GBytes *
+recv_bytes (MockTransport *transport)
+{
+ GBytes *msg;
+ while ((msg = mock_transport_pop_channel (transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ return msg;
+}
+
+static JsonObject *
+recv_object (MockTransport *transport)
+{
+ GBytes *msg = recv_bytes (transport);
+ JsonObject *res = cockpit_json_parse_bytes (msg, NULL);
+ g_assert (res != NULL);
+ return res;
+}
+
+static JsonArray *
+recv_array (MockTransport *transport)
+{
+ GBytes *msg;
+ GError *error = NULL;
+ JsonArray *array;
+ JsonNode *node;
+
+ msg = recv_bytes (transport);
+ node = cockpit_json_parse (g_bytes_get_data (msg, NULL), g_bytes_get_size (msg), &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (json_node_get_node_type (node), ==, JSON_NODE_ARRAY);
+
+ array = json_node_dup_array (node);
+ json_node_free (node);
+ return array;
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ cockpit_assert_expected ();
+
+ g_object_unref (tc->transport);
+
+ if (tc->channel)
+ {
+ g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
+ g_object_unref (tc->channel);
+ g_assert (tc->channel == NULL);
+ }
+
+ g_free (tc->problem);
+}
+
+static void
+assert_sample_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ TestCase *tc,
+ const gchar *json_str)
+{
+ JsonArray *array = recv_array (tc->transport);
+ _cockpit_assert_json_eq_msg (domain, file, line, func, array, json_str);
+ json_array_unref (array);
+}
+
+#define assert_sample(tc, json) \
+ (assert_sample_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (tc), (json)))
+
+static JsonObject *
+json_obj_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ const gchar *json_str)
+{
+ GError *error = NULL;
+ JsonObject *res = cockpit_json_parse_object (json_str, -1, &error);
+ if (error)
+ g_assertion_message_error (domain, file, line, func, "error", error, 0, 0);
+ return res;
+}
+
+#define json_obj(json_str) \
+ (json_obj_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (json_str)))
+
+static void
+send_sample (TestCase *tc, gint64 timestamp, int n, ...)
+{
+ va_list ap;
+ va_start (ap, n);
+
+ double **buffer;
+ buffer = cockpit_metrics_get_data_buffer (tc->channel);
+ for (int i = 0; i < n; i++)
+ buffer[i][0] = va_arg (ap, double);
+ cockpit_metrics_send_data (tc->channel, timestamp);
+ cockpit_metrics_flush_data (tc->channel);
+
+ va_end (ap);
+}
+
+static void
+send_instance_sample (TestCase *tc, gint64 timestamp, int n, ...)
+{
+ va_list ap;
+ va_start (ap, n);
+
+ double **buffer;
+ buffer = cockpit_metrics_get_data_buffer (tc->channel);
+ for (int i = 0; i < n; i++)
+ buffer[0][i] = va_arg (ap, double);
+ cockpit_metrics_send_data (tc->channel, timestamp);
+ cockpit_metrics_flush_data (tc->channel);
+
+ va_end (ap);
+}
+
+static void
+test_compression (TestCase *tc,
+ gconstpointer unused)
+{
+ cockpit_metrics_set_compress (tc->channel, TRUE);
+
+ JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo' },"
+ " { 'name': 'bar' }"
+ " ],"
+ " 'interval': 1000"
+ "}");
+ cockpit_metrics_send_meta (tc->channel, meta, FALSE);
+ json_object_unref (recv_object (tc->transport));
+
+ send_sample (tc, 0, 2, 0.0, 0.0);
+ assert_sample (tc, "[[0,0]]");
+ send_sample (tc, 1000, 2, 0.0, 0.0);
+ assert_sample (tc, "[[]]");
+ send_sample (tc, 2000, 2, 0.0, 0.0);
+ assert_sample (tc, "[[]]");
+
+ send_sample (tc, 3000, 2, 0.0, 1.0);
+ assert_sample (tc, "[[null, 1]]");
+
+ send_sample (tc, 4000, 2, 1.0, 1.0);
+ assert_sample (tc, "[[1]]");
+
+ json_object_unref (meta);
+}
+
+static void
+test_compression_reset (TestCase *tc,
+ gconstpointer unused)
+{
+ cockpit_metrics_set_compress (tc->channel, TRUE);
+
+ JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo' },"
+ " { 'name': 'bar' }"
+ " ],"
+ " 'interval': 1000"
+ "}");
+ cockpit_metrics_send_meta (tc->channel, meta, FALSE);
+ json_object_unref (recv_object (tc->transport));
+
+ send_sample (tc, 0, 2, 0.0, 0.0);
+ assert_sample (tc, "[[0,0]]");
+ send_sample (tc, 1000, 2, 0.0, 0.0);
+ assert_sample (tc, "[[]]");
+
+ cockpit_metrics_send_meta (tc->channel, meta, TRUE);
+ json_object_unref (recv_object (tc->transport));
+
+ send_sample (tc, 2000, 2, 0.0, 0.0);
+ assert_sample (tc, "[[0,0]]");
+ send_sample (tc, 3000, 2, 0.0, 0.0);
+ assert_sample (tc, "[[]]");
+
+ json_object_unref (meta);
+}
+
+static void
+test_derive_delta (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo',"
+ " 'derive': 'delta'"
+ " }"
+ " ],"
+ " 'interval': 100"
+ "}");
+ cockpit_metrics_send_meta (tc->channel, meta, FALSE);
+ json_object_unref (recv_object (tc->transport));
+
+ send_sample (tc, 0, 1, 0.0);
+ assert_sample (tc, "[[false]]");
+ send_sample (tc, 100, 1, 10.0);
+ assert_sample (tc, "[[10]]");
+ send_sample (tc, 200, 1, 20.0);
+ assert_sample (tc, "[[10]]");
+ send_sample (tc, 300, 1, 40.0);
+ assert_sample (tc, "[[20]]");
+ send_sample (tc, 400, 1, 30.0);
+ assert_sample (tc, "[[-10]]");
+ send_sample (tc, 500, 1, 30.0);
+ assert_sample (tc, "[[0]]");
+ send_sample (tc, 600, 1, 30.0);
+ assert_sample (tc, "[[0]]");
+ send_sample (tc, 700, 1, 30.0);
+ assert_sample (tc, "[[0]]");
+
+ cockpit_metrics_send_meta (tc->channel, meta, TRUE);
+ json_object_unref (recv_object (tc->transport));
+
+ send_sample (tc, 800, 1, 30.0);
+ assert_sample (tc, "[[false]]");
+ send_sample (tc, 900, 1, 30.0);
+ assert_sample (tc, "[[0]]");
+ send_sample (tc, 1000, 1, 30.0);
+ assert_sample (tc, "[[0]]");
+ send_sample (tc, 1100, 1, 40.0);
+ assert_sample (tc, "[[10]]");
+ send_sample (tc, 1200, 1, 40.0);
+ assert_sample (tc, "[[0]]");
+
+ json_object_unref (meta);
+}
+
+static void
+test_derive_rate_no_interpolate (TestCase *tc,
+ gconstpointer unused)
+{
+ cockpit_metrics_set_interpolate (tc->channel, FALSE);
+
+ JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo',"
+ " 'derive': 'rate'"
+ " }"
+ " ],"
+ " 'interval': 100"
+ "}");
+ cockpit_metrics_send_meta (tc->channel, meta, FALSE);
+ json_object_unref (recv_object (tc->transport));
+
+ send_sample (tc, 0, 1, 0.0);
+ assert_sample (tc, "[[false]]");
+ send_sample (tc, 100, 1, 10.0);
+ assert_sample (tc, "[[100]]");
+ send_sample (tc, 200, 1, 20.0);
+ assert_sample (tc, "[[100]]");
+ send_sample (tc, 300, 1, 40.0);
+ assert_sample (tc, "[[200]]");
+ send_sample (tc, 400, 1, 30.0);
+ assert_sample (tc, "[[-100]]");
+ send_sample (tc, 500, 1, 30.0);
+ assert_sample (tc, "[[0]]");
+ send_sample (tc, 600, 1, 30.0);
+ assert_sample (tc, "[[0]]");
+ send_sample (tc, 700, 1, 30.0);
+ assert_sample (tc, "[[0]]");
+
+ cockpit_metrics_send_meta (tc->channel, meta, TRUE);
+ json_object_unref (recv_object (tc->transport));
+
+ send_sample (tc, 800, 1, 30.0);
+ assert_sample (tc, "[[false]]");
+ send_sample (tc, 900, 1, 30.0);
+ assert_sample (tc, "[[0]]");
+ send_sample (tc, 1000, 1, 30.0);
+ assert_sample (tc, "[[0]]");
+ send_sample (tc, 1200, 1, 40.0); // double interval -> half rate
+ assert_sample (tc, "[[50]]");
+ send_sample (tc, 1200, 1, 40.0);
+ assert_sample (tc, "[[false]]"); // divide by zero -> NaN -> false
+ send_sample (tc, 1300, 1, 40.0);
+ assert_sample (tc, "[[0]]");
+
+ json_object_unref (meta);
+}
+
+/* Very specific functions to be used by test_interpolate for
+ approximate sample assertions. (The only reason why we don't do
+ this for all tests is that it is not fun to generalize this...)
+*/
+
+static gboolean
+approx_equal (double a, double b)
+{
+ return a == b || (fabs(a-b)/fmax(a, b) < 0.0001);
+}
+
+static void
+assert_2_approx_samples_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ TestCase *tc,
+ double val1,
+ double val2)
+{
+ JsonArray *array = recv_array (tc->transport);
+ JsonArray *sub_array;
+
+ if (json_array_get_length (array) != 1)
+ goto fail;
+ sub_array = json_array_get_array_element (array, 0);
+ if (json_array_get_length (sub_array) != 2)
+ goto fail;
+ if (!approx_equal (json_array_get_double_element (sub_array, 0), val1))
+ goto fail;
+ if (!approx_equal (json_array_get_double_element (sub_array, 1), val2))
+ goto fail;
+
+ goto out;
+
+ fail:
+ {
+ JsonNode *node;
+ gchar *escaped;
+ gchar *msg;
+
+ node = json_node_new (JSON_NODE_ARRAY);
+ json_node_set_array (node, array);
+ escaped = cockpit_json_write (node, NULL);
+ msg = g_strdup_printf ("%s does not approximately match [[%g,%g]]", escaped, val1, val2);
+ g_assertion_message (domain, file, line, func, msg);
+ g_free (msg);
+ g_free (escaped);
+ json_node_free (node);
+ }
+
+ out:
+ json_array_unref (array);
+}
+
+#define assert_2_approx_samples(tc, val1, val2) \
+ (assert_2_approx_samples_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (tc), (val1), (val2)))
+
+static void
+test_interpolate (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo'"
+ " },"
+ " {"
+ " 'name': 'bar',"
+ " 'derive': 'rate'"
+ " }"
+ " ],"
+ " 'interval': 100"
+ "}");
+ cockpit_metrics_send_meta (tc->channel, meta, FALSE);
+ json_object_unref (recv_object (tc->transport));
+
+ // rising by 10 for every 100 ms, with non-equally spaced samples
+
+ send_sample (tc, 0, 2, 0.0, 0.0);
+ assert_sample (tc, "[[0,false]]");
+ send_sample (tc, 100, 2, 10.0, 10.0);
+ assert_2_approx_samples (tc, 10, 100);
+ send_sample (tc, 250, 2, 25.0, 25.0);
+ assert_2_approx_samples (tc, 20, 100);
+ send_sample (tc, 300, 2, 30.0, 30.0);
+ assert_2_approx_samples (tc, 30, 100);
+ send_sample (tc, 500, 2, 50.0, 50.0);
+ assert_2_approx_samples (tc, 40, 100);
+ send_sample (tc, 500, 2, 50.0, 50.0);
+ assert_2_approx_samples (tc, 50, 100);
+
+ json_object_unref (meta);
+}
+
+static void
+test_instances (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo',"
+ " 'instances': [ 'a', 'b' ]"
+ " }"
+ " ],"
+ " 'interval': 1000"
+ "}");
+ cockpit_metrics_send_meta (tc->channel, meta, FALSE);
+ json_object_unref (recv_object (tc->transport));
+
+ send_instance_sample (tc, 0, 2, 0.0, 0.0);
+ assert_sample (tc, "[[[0,0]]]");
+ send_instance_sample (tc, 1000, 2, 0.0, 0.0);
+ assert_sample (tc, "[[[0,0]]]");
+ send_instance_sample (tc, 2000, 2, 0.0, 0.0);
+ assert_sample (tc, "[[[0,0]]]");
+
+ send_instance_sample (tc, 3000, 2, 0.0, 1.0);
+ assert_sample (tc, "[[[0, 1]]]");
+
+ send_instance_sample (tc, 4000, 2, 1.0, 1.0);
+ assert_sample (tc, "[[[1, 1]]]");
+
+ json_object_unref (meta);
+}
+
+static void
+test_dynamic_instances (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo',"
+ " 'instances': [ 'a' ],"
+ " 'derive': 'delta'"
+ " }"
+ " ],"
+ " 'interval': 100"
+ "}");
+ cockpit_metrics_send_meta (tc->channel, meta, FALSE);
+ json_object_unref (recv_object (tc->transport));
+
+ send_instance_sample (tc, 0, 1, 0.0);
+ assert_sample (tc, "[[[false]]]");
+ send_instance_sample (tc, 100, 1, 10.0);
+ assert_sample (tc, "[[[10]]]");
+ send_instance_sample (tc, 200, 1, 20.0);
+ assert_sample (tc, "[[[10]]]");
+
+ json_object_unref (meta);
+ meta = json_obj ("{ 'metrics': [ { 'name': 'foo',"
+ " 'instances': [ 'b', 'a' ],"
+ " 'derive': 'delta'"
+ " }"
+ " ],"
+ " 'interval': 100"
+ "}");
+ cockpit_metrics_send_meta (tc->channel, meta, FALSE);
+ json_object_unref (recv_object (tc->transport));
+
+ /* Instance 'a' is now at a different index. The 'delta' derivation
+ should continue to work, but no compression should happen.
+ */
+
+ send_instance_sample (tc, 300, 2, 0.0, 30.0);
+ assert_sample (tc, "[[[false,10]]]");
+ send_instance_sample (tc, 400, 2, 10.0, 20.0);
+ assert_sample (tc, "[[[10,-10]]]");
+ send_instance_sample (tc, 500, 2, 10.0, 40.0);
+ assert_sample (tc, "[[[0,20]]]");
+ send_instance_sample (tc, 600, 2, 10.0, 50.0);
+ assert_sample (tc, "[[[0,10]]]");
+ send_instance_sample (tc, 700, 2, 10.0, 60.0);
+ assert_sample (tc, "[[[0,10]]]");
+
+ json_object_unref (meta);
+}
+
+static void
+assert_not_root_mount (JsonArray *array,
+ guint index_,
+ JsonNode *element_node,
+ gpointer user_data)
+{
+ g_assert_cmpstr (json_node_get_string (element_node), !=, "/");
+}
+
+static void
+test_omit_instances (void)
+{
+ MockTransport *transport = mock_transport_new ();
+ CockpitChannel *channel;
+ JsonObject *options = json_obj ("{ 'metrics': [ { 'name': 'mount.total' } ],"
+ " 'omit-instances': [ '/' ],"
+ " 'interval': 1000"
+ "}");
+ GBytes *msg;
+ JsonObject *res, *mount_total;
+ JsonArray *metrics;
+
+ g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+
+ channel = g_object_new (cockpit_internal_metrics_get_type (),
+ "transport", transport,
+ "id", "1234",
+ "options", options,
+ NULL);
+
+ cockpit_channel_prepare (channel);
+
+ /* receive meta information */
+ while ((msg = mock_transport_pop_channel (transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ res = cockpit_json_parse_bytes (msg, NULL);
+ g_assert (res != NULL);
+ g_assert (json_object_has_member (res, "metrics"));
+
+ /* metrics should have the form [{"name":"mount.total","instances":["/boot",...]}] */
+ metrics = json_object_get_array_member (res, "metrics");
+ g_assert_cmpint (json_array_get_length (metrics), ==, 1);
+ mount_total = json_array_get_object_element (metrics, 0);
+ g_assert (mount_total);
+ g_assert_cmpstr (json_object_get_string_member (mount_total, "name"), ==, "mount.total");
+
+ /* instances should not contain the omitted "/" */
+ json_array_foreach_element (json_object_get_array_member (mount_total, "instances"),
+ assert_not_root_mount,
+ NULL);
+
+ json_object_unref (res);
+ g_object_unref (channel);
+ json_object_unref (options);
+ g_object_unref (transport);
+}
+
+static void
+on_close_get_problem (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gchar **result = user_data;
+ g_assert (result != NULL);
+ g_assert (*result == NULL);
+ *result = g_strdup (problem ? problem : "");
+}
+
+static void
+test_not_supported (void)
+{
+ MockTransport *transport;
+ CockpitMetrics *channel;
+ gchar *problem = NULL;
+ JsonObject *options;
+
+ cockpit_expect_message ("*unknown internal metric*");
+
+ transport = mock_transport_new ();
+ g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+ options = json_obj ("{ 'metrics': [ { 'name': 'invalid.metrics',"
+ " 'instances': [ 'b', 'a' ],"
+ " 'derive': 'delta'"
+ " }"
+ " ],"
+ " 'interval': 100"
+ "}");
+ channel = g_object_new (cockpit_internal_metrics_get_type (),
+ "transport", transport,
+ "id", "1234",
+ "options", options,
+ NULL);
+ json_object_unref (options);
+ g_signal_connect (channel, "closed", G_CALLBACK (on_close_get_problem), &problem);
+
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "not-supported");
+
+ g_object_add_weak_pointer (G_OBJECT (channel), (gpointer *)&channel);
+ g_object_unref (channel);
+ g_assert (channel == NULL);
+
+ g_object_unref (transport);
+ g_free (problem);
+}
+
+static void
+test_deprecated_net_all (void)
+{
+ MockTransport *transport = mock_transport_new ();
+ CockpitChannel *channel;
+ /* network.all.* is not being used any more in current cockpit, but in older
+ * Dashboards; ensure it keeps working */
+ JsonObject *options = json_obj ("{ 'metrics': [ { 'name': 'network.all.tx' }, { 'name': 'network.all.rx' } ],"
+ " 'interval': 100"
+ "}");
+ GBytes *msg;
+ JsonObject *res, *metric;
+ JsonNode *node;
+ JsonArray *metrics, *values;
+
+ g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+
+ channel = g_object_new (cockpit_internal_metrics_get_type (),
+ "transport", transport,
+ "id", "1234",
+ "options", options,
+ NULL);
+
+ cockpit_metrics_set_compress (COCKPIT_METRICS (channel), FALSE);
+ cockpit_channel_prepare (channel);
+
+ /* receive meta information */
+ while ((msg = mock_transport_pop_channel (transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ res = cockpit_json_parse_bytes (msg, NULL);
+ g_assert (res != NULL);
+ g_assert (json_object_has_member (res, "metrics"));
+
+ /* metrics should have the form [{"name":"network.all.tx","units":"bytes","semantics":"counter"}, ...] */
+ metrics = json_object_get_array_member (res, "metrics");
+ g_assert_cmpint (json_array_get_length (metrics), ==, 2);
+
+ metric = json_array_get_object_element (metrics, 0);
+ g_assert (metric);
+ g_assert_cmpstr (json_object_get_string_member (metric, "name"), ==, "network.all.tx");
+ g_assert_cmpstr (json_object_get_string_member (metric, "units"), ==, "bytes");
+ g_assert (!json_object_has_member (metric, "instances"));
+
+ metric = json_array_get_object_element (metrics, 1);
+ g_assert (metric);
+ g_assert_cmpstr (json_object_get_string_member (metric, "name"), ==, "network.all.rx");
+ g_assert_cmpstr (json_object_get_string_member (metric, "units"), ==, "bytes");
+ g_assert (!json_object_has_member (metric, "instances"));
+
+ json_object_unref (res);
+
+ /* receive data; should have the form [[123,456]] */
+ while ((msg = mock_transport_pop_channel (transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ node = cockpit_json_parse (g_bytes_get_data (msg, NULL), g_bytes_get_size (msg), NULL);
+ g_assert (node);
+ metrics = json_node_get_array (node);
+ g_assert (metrics != NULL);
+ g_assert_cmpint (json_array_get_length (metrics), ==, 1);
+ values = json_array_get_array_element (metrics, 0);
+ g_assert_cmpint (json_array_get_length (values), ==, 2);
+ g_assert_cmpint (json_array_get_int_element (values, 0), >=, 0);
+ g_assert_cmpint (json_array_get_int_element (values, 1), >=, 0);
+
+ json_node_free (node);
+
+ g_object_unref (channel);
+ json_object_unref (options);
+ g_object_unref (transport);
+}
+
+static void
+test_cgroup (void)
+{
+ MockTransport *transport = mock_transport_new ();
+ CockpitChannel *channel;
+ JsonObject *options = json_obj ("{ 'metrics': [ { 'name': 'cgroup.memory.usage' }, "
+ " { 'name': 'cgroup.memory.limit' }, "
+ " { 'name': 'cgroup.memory.sw-usage' }, "
+ " { 'name': 'cgroup.memory.sw-limit' }, "
+ " { 'name': 'cgroup.cpu.usage' }, "
+ " { 'name': 'cgroup.cpu.shares' } ], "
+ " 'interval': 1000"
+ "}");
+ GBytes *msg;
+ JsonObject *res, *description;
+ JsonArray *metrics, *instances;
+ JsonArray *samples;
+ GError *error = NULL;
+
+ g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+
+ channel = g_object_new (cockpit_internal_metrics_get_type (),
+ "transport", transport,
+ "id", "1234",
+ "options", options,
+ NULL);
+
+ cockpit_channel_prepare (channel);
+
+ /* receive meta information */
+ while ((msg = mock_transport_pop_channel (transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ res = cockpit_json_parse_bytes (msg, &error);
+ g_assert_no_error (error);
+ g_assert (res != NULL);
+ g_assert (json_object_has_member (res, "metrics"));
+
+ /* metrics should have the form [{"name":"...","instances":["name1", ...]}] */
+ metrics = json_object_get_array_member (res, "metrics");
+ g_assert_cmpint (json_array_get_length (metrics), ==, 6);
+ description = json_array_get_object_element (metrics, 0);
+ g_assert (description);
+ g_assert_cmpstr (json_object_get_string_member (description, "name"), ==, "cgroup.memory.usage");
+ g_assert_cmpstr (json_object_get_string_member (description, "units"), ==, "bytes");
+
+ /* we can't assert any contents about instances and samples, as build environments may not even have
+ * any cgroup controller; just make sure that the data structure is as expected */
+ instances = json_object_get_array_member (description, "instances");
+ g_assert_cmpint (json_array_get_length (instances), >=, 0);
+
+ /* next message should have some actual values; looks like [[...],[...],...]]] */
+ samples = recv_array (transport);
+ g_assert_cmpint (json_array_get_length (samples), ==, 1);
+ g_assert_cmpint (json_array_get_length (json_array_get_array_element (samples, 0)), ==, 6);
+
+ json_array_unref (samples);
+
+ json_object_unref (res);
+ g_object_unref (channel);
+ json_object_unref (options);
+ g_object_unref (transport);
+}
+
+static void
+test_cpu_cores (void)
+{
+ MockTransport *transport = mock_transport_new ();
+ CockpitChannel *channel;
+
+ JsonObject *options = json_obj ("{ 'metrics': [ { 'name': 'cpu.core.nice', 'derive': 'rate' }, "
+ " { 'name': 'cpu.core.user', 'derive': 'rate' }, "
+ " { 'name': 'cpu.core.system', 'derive': 'rate' }, "
+ " { 'name': 'cpu.core.iowait', 'derive': 'rate' } ], "
+ " 'interval': 1000"
+ "}");
+ GBytes *msg;
+ JsonObject *res, *description;
+ JsonArray *metrics, *instances, *all, *nice;
+ JsonArray *samples;
+ GError *error = NULL;
+
+ g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+
+ channel = g_object_new (cockpit_internal_metrics_get_type (),
+ "transport", transport,
+ "id", "1234",
+ "options", options,
+ NULL);
+
+ cockpit_channel_prepare (channel);
+
+ /* receive meta information */
+ while ((msg = mock_transport_pop_channel (transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ res = cockpit_json_parse_bytes (msg, &error);
+ g_assert_no_error (error);
+ g_assert (res != NULL);
+ g_assert (json_object_has_member (res, "metrics"));
+
+ /* metrics should have the form [{"name":"...","instances":[[1, 0, ...], ...]}] */
+ metrics = json_object_get_array_member (res, "metrics");
+ g_assert_cmpint (json_array_get_length (metrics), ==, 4);
+ description = json_array_get_object_element (metrics, 0);
+ g_assert (description);
+ g_assert_cmpstr (json_object_get_string_member (description, "name"), ==, "cpu.core.nice");
+ g_assert_cmpstr (json_object_get_string_member (description, "units"), ==, "millisec");
+
+ /* Array contains value for each core */
+ instances = json_object_get_array_member (description, "instances");
+ g_assert_cmpint (json_array_get_length (instances), ==, sysconf(_SC_NPROCESSORS_ONLN));
+
+ /* Value of each core is somewhere between 0 and 1000 */
+ samples = recv_array (transport);
+ g_assert_cmpint (json_array_get_length (samples), ==, 1);
+ all = json_array_get_array_element (samples, 0);
+ g_assert_cmpint (json_array_get_length (all), ==, 4);
+ nice = json_array_get_array_element (all, 0);
+ g_assert_cmpint (json_array_get_length (nice), ==, sysconf(_SC_NPROCESSORS_ONLN));
+ g_assert_cmpint (json_array_get_int_element (nice, 0), >=, 0);
+ g_assert_cmpint (json_array_get_int_element (nice, 0), <=, 1000);
+
+ json_array_unref (samples);
+
+ json_object_unref (res);
+ g_object_unref (channel);
+ json_object_unref (options);
+ g_object_unref (transport);
+}
+
+static void
+test_cpu_temperature (void)
+{
+ MockTransport *transport = mock_transport_new ();
+
+ g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+
+ g_autoptr(JsonObject) options = json_obj ("{ 'metrics': [ { 'name': 'cpu.temperature' } ],"
+ " 'interval': 1000"
+ "}");
+
+ CockpitChannel *channel = g_object_new (cockpit_internal_metrics_get_type (),
+ "transport", transport,
+ "id", "1234",
+ "options", options,
+ NULL);
+
+ cockpit_channel_prepare (channel);
+
+ g_autoptr(GBytes) msg = NULL;
+ /* receive meta information */
+ while ((msg = mock_transport_pop_channel (transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_autoptr(GError) error = NULL;
+ g_autoptr(JsonObject) res = cockpit_json_parse_bytes (msg, &error);
+ g_assert_no_error (error);
+ g_assert (res != NULL);
+ g_assert (json_object_has_member (res, "metrics"));
+
+ JsonArray *metrics = json_object_get_array_member (res, "metrics");
+ g_assert_cmpint (json_array_get_length (metrics), ==, 1);
+
+ JsonObject *description = json_array_get_object_element (metrics, 0);
+ g_assert (description);
+ g_assert_cmpstr (json_object_get_string_member (description, "name"), ==, "cpu.temperature");
+ g_assert_cmpstr (json_object_get_string_member (description, "units"), ==, "celsius");
+
+ g_autoptr(JsonArray) samples = recv_array (transport);
+ g_assert_cmpint (json_array_get_length (samples), ==, 1);
+ JsonArray *core = json_array_get_array_element (samples, 0);
+ g_assert_cmpint (json_array_get_length (core), ==, 1);
+ JsonArray *temperature = json_array_get_array_element (core, 0);
+
+ // file does not exist in virtual machines, skip value check
+ if (json_array_get_length (temperature) >= 1)
+ {
+ g_assert_cmpint (json_array_get_int_element (temperature, 0), >, 0);
+ g_assert_cmpint (json_array_get_int_element (temperature, 0), <, 150);
+ }
+}
+
+static void
+test_cgroup_disk_io (void)
+{
+ MockTransport *transport = mock_transport_new ();
+
+ g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+
+ g_autoptr(JsonObject) options = json_obj ("{ 'metrics': [ { 'name': 'disk.cgroup.read' }, { 'name': 'disk.cgroup.written' } ],"
+ " 'interval': 1000"
+ "}");
+
+ CockpitChannel *channel = g_object_new (cockpit_internal_metrics_get_type (),
+ "transport", transport,
+ "id", "1234",
+ "options", options,
+ NULL);
+
+ cockpit_channel_prepare (channel);
+
+ g_autoptr(GBytes) msg = NULL;
+ /* receive meta information */
+ while ((msg = mock_transport_pop_channel (transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_autoptr(GError) error = NULL;
+ g_autoptr(JsonObject) res = cockpit_json_parse_bytes (msg, &error);
+ g_assert_no_error (error);
+ g_assert (res != NULL);
+ g_assert (json_object_has_member (res, "metrics"));
+
+ JsonArray *metrics = json_object_get_array_member (res, "metrics");
+ g_assert_cmpint (json_array_get_length (metrics), ==, 2);
+
+ JsonObject *description = json_array_get_object_element (metrics, 0);
+ g_assert (description);
+ g_assert_cmpstr (json_object_get_string_member (description, "name"), ==, "disk.cgroup.read");
+ g_assert_cmpstr (json_object_get_string_member (description, "units"), ==, "bytes");
+
+ description = json_array_get_object_element (metrics, 1);
+ g_assert (description);
+ g_assert_cmpstr (json_object_get_string_member (description, "name"), ==, "disk.cgroup.written");
+ g_assert_cmpstr (json_object_get_string_member (description, "units"), ==, "bytes");
+
+ g_autoptr(JsonArray) samples = recv_array (transport);
+ g_assert_cmpint (json_array_get_length (samples), ==, 1);
+ JsonArray *core = json_array_get_array_element (samples, 0);
+ g_assert_cmpint (json_array_get_length (core), ==, 2);
+ JsonArray *read = json_array_get_array_element (core, 0);
+ JsonArray *write = json_array_get_array_element (core, 1);
+ guint length = json_array_get_length (read);
+ g_assert_cmpint (length, ==, json_array_get_length (write));
+ g_assert_cmpint (length, >, 0);
+ for (guint i = 0; i < length; i++)
+ {
+ g_assert_cmpint (json_array_get_int_element (read, i), >=, 0);
+ g_assert_cmpint (json_array_get_int_element (write, i), >=, 0);
+ }
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/metrics/compression", TestCase, NULL,
+ setup, test_compression, teardown);
+ g_test_add ("/metrics/compression-reset", TestCase, NULL,
+ setup, test_compression_reset, teardown);
+ g_test_add ("/metrics/derive-delta", TestCase, NULL,
+ setup, test_derive_delta, teardown);
+ g_test_add ("/metrics/derive-rate", TestCase, NULL,
+ setup, test_derive_rate_no_interpolate, teardown);
+ g_test_add ("/metrics/interpolate", TestCase, NULL,
+ setup, test_interpolate, teardown);
+
+ g_test_add ("/metrics/instances", TestCase, NULL,
+ setup, test_instances, teardown);
+ g_test_add ("/metrics/dynamic-instances", TestCase, NULL,
+ setup, test_dynamic_instances, teardown);
+ g_test_add_func ("/metrics/omit-instances", test_omit_instances);
+
+ g_test_add_func ("/metrics/not-supported", test_not_supported);
+
+ g_test_add_func ("/metrics/deprecated-net-all", test_deprecated_net_all);
+ g_test_add_func ("/metrics/cgroup-memory", test_cgroup);
+
+ g_test_add_func ("/metrics/cpu-cores", test_cpu_cores);
+ g_test_add_func ("/metrics/cpu-temperature", test_cpu_temperature);
+
+ g_test_add_func ("/metrics/cgroup-disk-io", test_cgroup_disk_io);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-packages.c b/src/bridge/test-packages.c
new file mode 100644
index 0000000..48a13a9
--- /dev/null
+++ b/src/bridge/test-packages.c
@@ -0,0 +1,1360 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "cockpithttpstream.h"
+#include "cockpitpackages.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitsystem.h"
+#include "testlib/cockpittest.h"
+#include "testlib/mock-transport.h"
+
+/*
+ * To recalculate the checksums found in this file, do something like:
+ * $ XDG_DATA_DIRS=$PWD/src/bridge/mock-resource/glob/ XDG_DATA_HOME=/nonexistent ./cockpit-bridge --packages
+ */
+#define CHECKSUM_GLOB "f73c058e343588a7ceaf12c4f129d324f10cc8eeb674dd098d888b619fa69cf1"
+#define CHECKSUM_GZIP "7f6449ce7a873614f4160cbcf03ee93346fd56ee7b82efe9c62193fefebe274d"
+#define CHECKSUM_BADPACKAGE "7171c55fbd2489334cda314546c670cc3d39d3a0827b212d522f39a32bf3d5de"
+#define CHECKSUM_RELOAD_OLD "16797c6330fb83dc2762d172fdf89d43e7f903841343bdf9a98e5a58f678f381"
+#define CHECKSUM_RELOAD_NEW "a90c11c111566ac87bca994acad2782b749d909738e2970f46e531d172ecbfb9"
+#define CHECKSUM_RELOAD_UPDATED "5ce8c2db35591659026e3dbb7e95c6dd0a06342138fabdb07ca90ddc2d00c338"
+#define CHECKSUM_CSP "f7fe957d0ec6457f2f5fe0a343f6422547188a867c1c3e1b10ef0e3eacfc1b06"
+
+/* JSON dict snippet for headers that are present in every request */
+#define STATIC_HEADERS "\"X-DNS-Prefetch-Control\":\"off\",\"Referrer-Policy\":\"no-referrer\",\"X-Content-Type-Options\":\"nosniff\",\"Cross-Origin-Resource-Policy\": \"same-origin\",\"X-Frame-Options\": \"sameorigin\""
+#define STATIC_HEADERS_CACHECONTROL STATIC_HEADERS ",\"Cache-Control\":\"no-cache, no-store\""
+
+extern const gchar **cockpit_bridge_data_dirs;
+extern const gchar *cockpit_bridge_local_address;
+
+const gchar *config_home;
+
+typedef struct {
+ CockpitPackages *packages;
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem;
+ gboolean closed;
+} TestCase;
+
+typedef struct {
+ const gchar *datadirs[8];
+ const gchar *cockpit_config;
+ const gchar *path;
+ const gchar *accept[8];
+ const gchar *expect;
+ const gchar *headers[8];
+ gboolean cacheable;
+ gboolean binary;
+ gboolean no_packages_init;
+} Fixture;
+
+static void
+on_channel_close (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ TestCase *tc = user_data;
+ g_assert (tc->closed == FALSE);
+ tc->closed = TRUE;
+ tc->problem = g_strdup (problem);
+}
+
+static void
+on_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ g_assert_not_reached ();
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ const Fixture *fixture = data;
+ JsonObject *options;
+ JsonObject *headers;
+ const gchar *control;
+ gchar *accept;
+ GBytes *bytes;
+ guint i;
+
+ g_assert (fixture != NULL);
+
+ if (fixture->expect)
+ cockpit_expect_warning (fixture->expect);
+
+ if (fixture->datadirs[0])
+ {
+ cockpit_bridge_data_dirs = (const gchar **)fixture->datadirs;
+ }
+ else
+ {
+ cockpit_expect_message ("incompatible: package requires a later version of cockpit: 999.5*");
+ cockpit_expect_message ("requires: package has an unknown requirement: unknown");
+ }
+
+ if (fixture->cockpit_config)
+ {
+ int fd = open (config_home, O_PATH|O_DIRECTORY);
+ g_assert (fd >= 0);
+ g_assert (symlinkat (fixture->cockpit_config, fd, "cockpit") == 0);
+ close (fd);
+ }
+
+ tc->packages = cockpit_packages_new ();
+
+ tc->transport = mock_transport_new ();
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "internal", "packages");
+ json_object_set_string_member (options, "payload", "http-stream1");
+ json_object_set_string_member (options, "method", "GET");
+ json_object_set_string_member (options, "path", fixture->path);
+
+ if (fixture->binary)
+ json_object_set_string_member (options, "binary", "raw");
+
+ headers = json_object_new ();
+ if (fixture->accept[0])
+ {
+ accept = g_strjoinv (", ", (gchar **)fixture->accept);
+ json_object_set_string_member (headers, "Accept-Language", accept);
+ g_free (accept);
+ }
+ if (!fixture->cacheable)
+ json_object_set_string_member (headers, "Pragma", "no-cache");
+ for (i = 0; i < G_N_ELEMENTS (fixture->headers); i += 2)
+ {
+ if (fixture->headers[i])
+ json_object_set_string_member (headers, fixture->headers[i], fixture->headers[i + 1]);
+ }
+ json_object_set_object_member (options, "headers", headers);
+
+ tc->channel = g_object_new (COCKPIT_TYPE_HTTP_STREAM,
+ "transport", tc->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+
+ /* Tell HTTP we have no more data to send */
+ control = "{\"command\": \"done\", \"channel\": \"444\"}";
+ bytes = g_bytes_new_static (control, strlen (control));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), NULL, bytes);
+ g_bytes_unref (bytes);
+
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_channel_close), tc);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ const Fixture *fixture = data;
+
+ cockpit_assert_expected ();
+
+ if (fixture && fixture->cockpit_config)
+ {
+ int fd = open (config_home, O_PATH|O_DIRECTORY);
+ g_assert (fd >= 0);
+ g_assert (unlinkat (fd, "cockpit", 0) == 0);
+ close (fd);
+ }
+
+ g_object_unref (tc->transport);
+
+ g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
+ g_object_unref (tc->channel);
+ g_assert (tc->channel == NULL);
+
+ g_free (tc->problem);
+
+ cockpit_packages_free (tc->packages);
+
+ cockpit_bridge_data_dirs = NULL;
+}
+
+static const Fixture fixture_simple = {
+ .path = "/test/sub/file.ext",
+};
+
+static void
+test_simple (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+ guint count;
+
+ g_assert (fixture == &fixture_simple);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS_CACHECONTROL "}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+ g_assert_cmpint (count, ==, 1);
+ cockpit_assert_bytes_eq (data, "These are the contents of file.ext\nOh marmalaaade\n", -1);
+ g_bytes_unref (data);
+}
+
+static const Fixture fixture_forwarded = {
+ .path = "/another/test.html",
+ .headers = { "X-Forwarded-Proto", "https", "X-Forwarded-Host", "blah:9090" },
+};
+
+static void
+test_forwarded (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+ guint count;
+
+ g_assert (fixture == &fixture_forwarded);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS_CACHECONTROL ",\"Content-Security-Policy\":\"default-src 'self' https://blah:9090; connect-src 'self' https://blah:9090 wss://blah:9090; form-action 'self' https://blah:9090; base-uri 'self' https://blah:9090; object-src 'none'; font-src 'self' https://blah:9090 data:; img-src 'self' https://blah:9090 data:; block-all-mixed-content\",\"Content-Type\":\"text/html\",\"Access-Control-Allow-Origin\":\"https://blah:9090\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+ g_assert_cmpint (count, ==, 1);
+ cockpit_assert_bytes_eq (data, "<html>\n<head>\n<title>In home dir</title>\n</head>\n<body>In home dir</body>\n</html>\n", -1);
+ g_bytes_unref (data);
+}
+
+static const Fixture fixture_pig = {
+ .path = "/another/test.html",
+ .accept = { "pig" },
+ .headers = { "Host", "blah:9090" },
+};
+
+static void
+test_localized_translated (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+ guint count;
+
+ g_assert (fixture == &fixture_pig);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS_CACHECONTROL ",\"Content-Security-Policy\":\"default-src 'self' http://blah:9090; connect-src 'self' http://blah:9090 ws://blah:9090; form-action 'self' http://blah:9090; base-uri 'self' http://blah:9090; object-src 'none'; font-src 'self' http://blah:9090 data:; img-src 'self' http://blah:9090 data:; block-all-mixed-content\",\"Content-Type\":\"text/html\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+ g_assert_cmpint (count, ==, 1);
+ cockpit_assert_bytes_eq (data, "<html>\n<head>\n<title>Inlay omehay irday</title>\n</head>\n<body>Inlay omehay irday</body>\n</html>\n", -1);
+ g_bytes_unref (data);
+}
+
+static const Fixture fixture_unknown = {
+ .path = "/another/test.html",
+ .accept = { "unknown" },
+ .headers = { "Host", "blah:9090" },
+};
+
+static void
+test_localized_unknown (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+ guint count;
+
+ g_assert (fixture == &fixture_unknown);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS_CACHECONTROL ",\"Content-Security-Policy\":\"default-src 'self' http://blah:9090; connect-src 'self' http://blah:9090 ws://blah:9090; form-action 'self' http://blah:9090; base-uri 'self' http://blah:9090; object-src 'none'; font-src 'self' http://blah:9090 data:; img-src 'self' http://blah:9090 data:; block-all-mixed-content\",\"Content-Type\":\"text/html\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+ g_assert_cmpint (count, ==, 1);
+ cockpit_assert_bytes_eq (data, "<html>\n<head>\n<title>In home dir</title>\n</head>\n<body>In home dir</body>\n</html>\n", -1);
+ g_bytes_unref (data);
+}
+
+static const Fixture fixture_prefer_region = {
+ .path = "/another/test.html",
+ .accept = { "pig-pen" },
+ .headers = { "Host", "blah:9090" },
+};
+
+static void
+test_localized_prefer_region (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+ guint count;
+
+ g_assert (fixture == &fixture_prefer_region);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS_CACHECONTROL ",\"Content-Security-Policy\":\"default-src 'self' http://blah:9090; connect-src 'self' http://blah:9090 ws://blah:9090; form-action 'self' http://blah:9090; base-uri 'self' http://blah:9090; object-src 'none'; font-src 'self' http://blah:9090 data:; img-src 'self' http://blah:9090 data:; block-all-mixed-content\",\"Content-Type\":\"text/html\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+ g_assert_cmpint (count, ==, 1);
+ cockpit_assert_bytes_eq (data, "<html>\n<head>\n<title>Inway omeha irda</title>\n</head>\n<body>Inway omeha irda</body>\n</html>\n", -1);
+ g_bytes_unref (data);
+}
+
+static const Fixture fixture_fallback = {
+ .path = "/another/test.html",
+ .accept = { "pig-barn" },
+ .headers = { "Host", "blah:9090" },
+};
+
+static void
+test_localized_fallback (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+ guint count;
+
+ g_assert (fixture == &fixture_fallback);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS_CACHECONTROL ",\"Content-Security-Policy\":\"default-src 'self' http://blah:9090; connect-src 'self' http://blah:9090 ws://blah:9090; form-action 'self' http://blah:9090; base-uri 'self' http://blah:9090; object-src 'none'; font-src 'self' http://blah:9090 data:; img-src 'self' http://blah:9090 data:; block-all-mixed-content\",\"Content-Type\":\"text/html\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+ g_assert_cmpint (count, ==, 1);
+ cockpit_assert_bytes_eq (data, "<html>\n<head>\n<title>Inlay omehay irday</title>\n</head>\n<body>Inlay omehay irday</body>\n</html>\n", -1);
+ g_bytes_unref (data);
+}
+
+static const Fixture fixture_version = {
+ .path = "/incompatible/test.html",
+};
+
+static void
+test_incompatible_version (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+ guint count;
+
+ g_assert (fixture == &fixture_version);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":503,\"reason\":\"This package requires Cockpit version 999.5 or later\",\"headers\":{" STATIC_HEADERS ",\"Content-Type\":\"text/html; charset=utf8\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+ cockpit_assert_bytes_eq (data, "<html><head><title>This package requires Cockpit version 999.5 or later</title></head><body>This package requires Cockpit version 999.5 or later</body></html>\n", -1);
+ g_bytes_unref (data);
+}
+
+static const Fixture fixture_requires = {
+ .path = "/requires/test.html",
+};
+
+static void
+test_incompatible_requires (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+ guint count;
+
+ g_assert (fixture == &fixture_requires);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":503,\"reason\":\"This package is not compatible with this version of Cockpit\",\"headers\":{" STATIC_HEADERS ",\"Content-Type\":\"text/html; charset=utf8\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+ cockpit_assert_bytes_eq (data, "<html><head><title>This package is not compatible with this version of Cockpit</title></head><body>This package is not compatible with this version of Cockpit</body></html>\n", -1);
+ g_bytes_unref (data);
+}
+
+static const Fixture fixture_large = {
+ .path = "/test/sub/COPYING",
+};
+
+static void
+test_large (TestCase *tc,
+ gconstpointer fixture)
+{
+ GError *error = NULL;
+ gchar *contents;
+ gsize length;
+ gsize prefixlength;
+ GBytes *data;
+ GBytes *sub;
+ guint count;
+ JsonObject *object;
+
+ g_assert (fixture == &fixture_large);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ g_assert (g_file_get_contents (SRCDIR "/src/bridge/mock-resource/system/cockpit/test/sub/COPYING",
+ &contents, &length, &error));
+ g_assert_no_error (error);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+
+ /* Should not have been sent as one block */
+ g_assert_cmpuint (count, ==, 8);
+ prefixlength = strcspn (g_bytes_get_data (data, NULL), "}}") + 2;
+ g_assert_cmpuint (g_bytes_get_size (data), >, prefixlength);
+ object = cockpit_json_parse_object (g_bytes_get_data (data, NULL), prefixlength, &error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS_CACHECONTROL "}}");
+ sub = g_bytes_new_from_bytes (data, prefixlength, g_bytes_get_size (data) - prefixlength);
+ cockpit_assert_bytes_eq (sub, contents, length);
+
+ json_object_unref (object);
+ g_bytes_unref (sub);
+ g_bytes_unref (data);
+ g_free (contents);
+}
+
+static const Fixture fixture_listing = {
+ .path = "/manifests.json",
+};
+
+static void
+test_listing (TestCase *tc,
+ gconstpointer fixture)
+{
+ JsonObject *object;
+ GError *error = NULL;
+ GBytes *message;
+ JsonNode *node;
+ guint count;
+
+ g_assert (fixture == &fixture_listing);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ message = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (message, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS_CACHECONTROL ",\"Content-Type\":\"application/json\"}}");
+ json_object_unref (object);
+
+ message = mock_transport_combine_output (tc->transport, "444", &count);
+ g_assert_cmpint (count, ==, 1);
+ node = cockpit_json_parse (g_bytes_get_data (message, NULL), g_bytes_get_size (message), &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (json_node_get_object (node),
+ "{"
+ " \"another\": {"
+ " \"name\" : \"another\","
+ " \"description\" : \"another\","
+ " \"bridges\": [{ \"match\": {\"host\": null },"
+ " \"problem\": \"not-supported\"}]"
+ " },"
+ " \"second\": {"
+ " \"description\": \"second dummy description\","
+ " \"priority\": 2,"
+ " \"bridges\": [{ \"match\": { \"second\": null }, \"problem\": \"never-a-second\"}]"
+ " },"
+ " \"test\": {"
+ " \"name\": \"test\","
+ " \"priority\": 15,"
+ " \"description\" : \"dummy\","
+ " \"bridges\": [{ \"match\": { \"blah\": \"test*\" },"
+ " \"spawn\": [\"/usr/bin/cat\"],"
+ " \"environ\": [\"TEST_ENV=test\"]},"
+ " { \"match\": { \"blah\": \"marmalade*\"},"
+ " \"problem\": \"bogus-channel\"}]"
+ " },"
+ " \"incompatible\": {"
+ " \"description\" : \"incompatible package\","
+ " \"requires\" : { \"cockpit\" : \"999.5\" }"
+ " },"
+ " \"requires\": {"
+ " \"description\" : \"requires package\","
+ " \"requires\" : { \"unknown\" : \"requirement\" }"
+ " }"
+ "}");
+ json_node_free (node);
+ g_bytes_unref (message);
+}
+
+static const Fixture fixture_override_config = {
+ .cockpit_config = SRCDIR "/src/bridge/mock-resource/config-override",
+ .path = "/manifests.json",
+};
+
+static void
+test_override_config (TestCase *tc,
+ gconstpointer fixture)
+{
+ GError *error = NULL;
+ GBytes *message;
+ guint count;
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ message = mock_transport_pop_channel (tc->transport, "444");
+ g_autoptr(JsonObject) object = cockpit_json_parse_bytes (message, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (json_object_get_int_member (object, "status"), ==, 200);
+
+ message = mock_transport_combine_output (tc->transport, "444", &count);
+ g_assert_cmpint (count, ==, 1);
+ g_autoptr(JsonNode) node = cockpit_json_parse (g_bytes_get_data (message, NULL), g_bytes_get_size (message), &error);
+ g_assert_no_error (error);
+ JsonObject *second = json_object_get_object_member (json_node_get_object (node), "second");
+ g_assert (second != NULL);
+ /* original priority from src/bridge/mock-resource/system/cockpit/second/manifest.json */
+ g_assert_cmpint (json_object_get_int_member (second, "priority"), ==, 2);
+ /* overridden description and added field from src/bridge/mock-resource/config-override/cockpit/second.override.json */
+ g_assert_cmpstr (json_object_get_string_member (second, "description"), ==, "overridden second description");
+ g_assert_cmpstr (json_object_get_string_member (second, "note"), ==, "an extra field");
+
+ g_bytes_unref (message);
+}
+
+static const Fixture fixture_not_found = {
+ .path = "/test/sub/not-found",
+};
+
+static void
+test_not_found (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+
+
+ g_assert (fixture == &fixture_not_found);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":404,\"reason\":\"Not Found\",\"headers\":{" STATIC_HEADERS ",\"Content-Type\":\"text/html; charset=utf8\"}}");
+ json_object_unref (object);
+}
+
+static const Fixture fixture_unknown_package = {
+ .path = "/unknownpackage/sub/not-found",
+};
+
+static void
+test_unknown_package (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+
+ g_assert (fixture == &fixture_unknown_package);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":404,\"reason\":\"Not Found\",\"headers\":{" STATIC_HEADERS ",\"Content-Type\":\"text/html; charset=utf8\"}}");
+ json_object_unref (object);
+}
+
+static const Fixture fixture_no_path = {
+ .path = "/test"
+};
+
+static void
+test_no_path (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+
+ g_assert (fixture == &fixture_no_path);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":404,\"reason\":\"Not Found\",\"headers\":{" STATIC_HEADERS ",\"Content-Type\":\"text/html; charset=utf8\"}}");
+ json_object_unref (object);
+}
+
+static const Fixture fixture_bad_path = {
+ .path = "/../test/sub/file.ext"
+};
+
+static void
+test_bad_path (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+
+ g_assert (fixture == &fixture_bad_path);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":404,\"reason\":\"Not Found\",\"headers\":{" STATIC_HEADERS ",\"Content-Type\":\"text/html; charset=utf8\"}}");
+ json_object_unref (object);
+}
+
+static const Fixture fixture_no_package = {
+ .path = "/test"
+};
+
+static void
+test_no_package (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+
+ g_assert (fixture == &fixture_no_package);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":404,\"reason\":\"Not Found\",\"headers\":{" STATIC_HEADERS ",\"Content-Type\":\"text/html; charset=utf8\"}}");
+ json_object_unref (object);
+}
+
+static const Fixture fixture_bad_package = {
+ .path = "/%%package/test"
+};
+
+static void
+test_bad_package (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+
+ g_assert (fixture == &fixture_bad_package);
+
+ cockpit_expect_message ("invalid 'package' name: %%package");
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":404,\"reason\":\"Not Found\",\"headers\":{" STATIC_HEADERS ",\"Content-Type\":\"text/html; charset=utf8\"}}");
+ json_object_unref (object);
+}
+
+static void
+test_bad_receive (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *bad;
+
+ cockpit_expect_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE, "444: channel received message after done");
+
+ /* A resource2 channel should never have payload sent to it */
+ bad = g_bytes_new_static ("bad", 3);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "444", bad);
+ g_bytes_unref (bad);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (tc->problem, ==, "protocol-error");
+}
+
+static const Fixture fixture_list_bad_name = {
+ .datadirs = { SRCDIR "/src/bridge/mock-resource/bad-package", NULL },
+ .expect = "*package*invalid*name*",
+ .path = "/manifests.json"
+};
+
+static void
+test_list_bad_name (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+ guint count;
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS ",\"Content-Type\":\"application/json\",\"X-Cockpit-Pkg-Checksum\":\"" CHECKSUM_BADPACKAGE "\",\"ETag\":\"\\\"$" CHECKSUM_BADPACKAGE "\\\"\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+ g_assert_cmpint (count, ==, 1);
+ cockpit_assert_bytes_eq (data, "{\".checksum\":\"" CHECKSUM_BADPACKAGE "\",\"ok\":{\".checksum\":\"" CHECKSUM_BADPACKAGE "\"}}", -1);
+ g_bytes_unref (data);
+}
+
+static const Fixture fixture_glob = {
+ .datadirs = { SRCDIR "/src/bridge/mock-resource/glob", NULL },
+ .path = "/*/file.txt"
+};
+
+static void
+test_glob (TestCase *tc,
+ gconstpointer fixture)
+{
+ GError *error = NULL;
+ GBytes *message;
+ JsonObject *object;
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ message = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (message, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS_CACHECONTROL ",\"X-Cockpit-Pkg-Checksum\":\"" CHECKSUM_GLOB "\",\"Content-Type\":\"text/plain\"}}");
+ json_object_unref (object);
+
+ message = mock_transport_pop_channel (tc->transport, "444");
+ cockpit_assert_bytes_eq (message, "a\n", 2);
+
+ message = mock_transport_pop_channel (tc->transport, "444");
+ cockpit_assert_bytes_eq (message, "b\n", 2);
+}
+
+static const Fixture fixture_with_gzip = {
+ .datadirs = { SRCDIR "/src/bridge/mock-resource/gzip", NULL },
+ .path = "/package/file.txt",
+ .binary = TRUE,
+ .headers = { "Accept-Encoding", "*" },
+};
+
+static void
+test_with_gzip (TestCase *tc,
+ gconstpointer fixture)
+{
+ GError *error = NULL;
+ GBytes *message;
+ JsonObject *object;
+ GBytes *data;
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ message = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (message, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS ",\"X-Cockpit-Pkg-Checksum\":\"" CHECKSUM_GZIP "\",\"Content-Encoding\":\"gzip\",\"Content-Type\":\"text/plain\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", NULL);
+ g_assert_cmpint (g_bytes_get_size (data), ==, 9377);
+ g_bytes_unref (data);
+}
+
+static const Fixture fixture_no_gzip = {
+ .datadirs = { SRCDIR "/src/bridge/mock-resource/gzip", NULL },
+ .path = "/package/file.txt",
+ .binary = TRUE,
+ .headers = { "Accept-Encoding", "identity" },
+};
+
+static void
+test_no_gzip (TestCase *tc,
+ gconstpointer fixture)
+{
+ GError *error = NULL;
+ GBytes *message;
+ JsonObject *object;
+ GBytes *data;
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ message = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (message, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS ",\"X-Cockpit-Pkg-Checksum\":\"" CHECKSUM_GZIP "\",\"Content-Type\":\"text/plain\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", NULL);
+ g_assert_cmpint (g_bytes_get_size (data), ==, 26530);
+ g_bytes_unref (data);
+}
+
+static void
+setup_basic (TestCase *tc,
+ gconstpointer data)
+{
+ const Fixture *fixture = data;
+
+ if (fixture && fixture->datadirs[0])
+ {
+ cockpit_bridge_data_dirs = (const gchar **)fixture->datadirs;
+ }
+ else
+ {
+ cockpit_expect_message ("incompatible: package requires a later version of cockpit: 999.5*");
+ cockpit_expect_message ("requires: package has an unknown requirement: unknown");
+ }
+
+ if (!fixture || !fixture->no_packages_init)
+ tc->packages = cockpit_packages_new ();
+}
+
+static void
+teardown_basic (TestCase *tc,
+ gconstpointer data)
+{
+ cockpit_assert_expected ();
+
+ cockpit_packages_free (tc->packages);
+
+ cockpit_bridge_data_dirs = NULL;
+}
+
+static void
+test_resolve (TestCase *tc,
+ gconstpointer fixture)
+{
+ gchar *path;
+
+ path = cockpit_packages_resolve (tc->packages, "test", "/sub/file.ext", NULL);
+ g_assert_cmpstr (SRCDIR "/src/bridge/mock-resource/system/cockpit/test-priority/sub/file.ext", ==, path);
+ g_free (path);
+
+ path = cockpit_packages_resolve (tc->packages, "test", "/_modules/@testorg/toolkit.js", NULL);
+ g_assert_cmpstr (SRCDIR "/src/bridge/mock-resource/system/cockpit/test-priority/_modules/@testorg/toolkit.js", ==, path);
+ g_free (path);
+}
+
+static void
+test_resolve_bad_dots (TestCase *tc,
+ gconstpointer fixture)
+{
+ gchar *path;
+
+ cockpit_expect_message ("invalid 'path' used as a resource: *");
+
+ path = cockpit_packages_resolve (tc->packages, "test", "../test/sub/file.ext", NULL);
+ g_assert (path == NULL);
+}
+
+static void
+test_resolve_bad_path (TestCase *tc,
+ gconstpointer fixture)
+{
+ gchar *path;
+
+ cockpit_expect_message ("invalid 'path' used as a resource: *");
+
+ path = cockpit_packages_resolve (tc->packages, "test", "/sub/#file.ext", NULL);
+ g_assert (path == NULL);
+}
+
+static void
+test_resolve_bad_package (TestCase *tc,
+ gconstpointer fixture)
+{
+ gchar *path;
+
+ cockpit_expect_message ("invalid 'package' name: *");
+
+ path = cockpit_packages_resolve (tc->packages, "#test", "/sub/file.ext", NULL);
+ g_assert (path == NULL);
+}
+
+static void
+test_resolve_not_found (TestCase *tc,
+ gconstpointer fixture)
+{
+ gchar *path;
+
+ path = cockpit_packages_resolve (tc->packages, "unknown", "/sub/file.ext", NULL);
+ g_assert (path == NULL);
+}
+
+static int
+compar_str (const void *pa,
+ const void *pb)
+{
+ return strcmp (*(const char**)pa, *(const char**)pb);
+}
+
+static void
+test_get_names (TestCase *tc,
+ gconstpointer fixture)
+{
+ gchar **names;
+ gchar *result;
+
+ names = cockpit_packages_get_names (tc->packages);
+ g_assert (names != NULL);
+
+ qsort (names, g_strv_length (names), sizeof (gchar *), compar_str);
+ result = g_strjoinv (", ", names);
+
+ /* Note that unavailable packages are not included */
+ g_assert_cmpstr (result, ==, "another, second, test");
+
+ g_free (result);
+ g_free (names);
+}
+
+static void
+test_get_bridges (TestCase *tc,
+ gconstpointer fixture)
+{
+ GList *bridges, *l;
+ JsonObject *bridge;
+ guint i;
+
+ bridges = cockpit_packages_get_bridges (tc->packages);
+ g_assert (bridges != NULL);
+
+ for (i = 0, l = bridges; l != NULL; l = g_list_next (l), i++)
+ {
+ bridge = l->data;
+ switch (i)
+ {
+ case 0:
+ cockpit_assert_json_eq (json_object_get_object_member (bridge, "match"),
+ "{ \"blah\": \"test*\" }");
+ cockpit_assert_json_eq (json_object_get_array_member (bridge, "environ"),
+ "[\"TEST_ENV=test\"]");
+ cockpit_assert_json_eq (json_object_get_array_member (bridge, "spawn"),
+ "[\"/usr/bin/cat\"]");
+ break;
+ case 1:
+ cockpit_assert_json_eq (json_object_get_object_member (bridge, "match"),
+ "{ \"blah\": \"marmalade*\" }");
+ g_assert_cmpstr (json_object_get_string_member (bridge, "problem"), ==, "bogus-channel");
+ break;
+ case 2:
+ cockpit_assert_json_eq (json_object_get_object_member (bridge, "match"),
+ "{ \"second\": null }");
+ g_assert_cmpstr (json_object_get_string_member (bridge, "problem"), ==, "never-a-second");
+ break;
+ case 3:
+ cockpit_assert_json_eq (json_object_get_object_member (bridge, "match"),
+ "{ \"host\": null }");
+ g_assert_cmpstr (json_object_get_string_member (bridge, "problem"), ==, "not-supported");
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+ }
+
+ g_assert_cmpint (i, ==, 4);
+ g_list_free (bridges);
+}
+
+static const Fixture fixture_bad_bridges = {
+ .datadirs = { SRCDIR "/src/bridge/mock-resource/bad-bridges", NULL },
+};
+
+static void
+test_get_bridges_broken (TestCase *tc,
+ gconstpointer fixture)
+{
+ GList *bridges;
+
+ g_assert (fixture == &fixture_bad_bridges);
+
+ cockpit_expect_message ("missing-match: Exactly one of \"match\" or \"privileged\" required");
+ cockpit_expect_message ("broken-problem: invalid \"problem\" field in package manifest");
+ cockpit_expect_message ("broken-environ: invalid \"environ\" field in package manifest");
+ cockpit_expect_message ("broken-spawn: invalid \"spawn\" field in package manifest");
+ cockpit_expect_message ("broken-match: invalid \"match\" field in package manifest");
+ cockpit_expect_message ("broken-bridges: invalid \"bridges\" field in package manifest");
+ cockpit_expect_message ("broken-bridge: invalid bridge in \"bridges\" field in package manifest");
+
+ bridges = cockpit_packages_get_bridges (tc->packages);
+ g_assert (bridges == NULL);
+}
+
+static const Fixture fixture_reload = {
+ .no_packages_init = TRUE,
+ .datadirs = { BUILDDIR "/src/bridge/mock-resource/reload", NULL },
+};
+
+__attribute__((format(printf, 1, 2)))
+static void
+systemf (const gchar *fmt, ...)
+{
+ gchar *cmd;
+
+ va_list ap;
+ va_start (ap, fmt);
+ cmd = g_strdup_vprintf (fmt, ap);
+ va_end (ap);
+
+ g_assert (system (cmd) == 0);
+
+ g_free (cmd);
+}
+
+static void
+setup_reload_packages (const gchar *datadir,
+ const gchar *variant)
+{
+ const gchar *srcdir = SRCDIR "/src/bridge/mock-resource/reload";
+ systemf ("mkdir -p $(dirname '%s') && rm -rf '%s' && ln -sf '%s.%s' '%s'",
+ datadir, datadir, srcdir, variant, datadir);
+}
+
+static void
+teardown_reload_packages (const gchar *datadir)
+{
+ systemf ("rm -f '%s'", datadir);
+}
+
+static void
+assert_manifest_checksum (TestCase *tc,
+ const gchar *name,
+ const gchar *expected)
+{
+ JsonObject *json;
+ const gchar *checksum;
+
+ json = cockpit_packages_peek_json (tc->packages);
+ if (name)
+ g_assert (cockpit_json_get_object (json, name, NULL, &json));
+ if (expected)
+ {
+ g_assert (cockpit_json_get_string (json, ".checksum", NULL, &checksum));
+ g_assert_cmpstr (checksum, ==, expected);
+ }
+ else
+ g_assert (json == NULL);
+}
+
+static void
+test_reload_added (TestCase *tc,
+ gconstpointer data)
+{
+ const Fixture *fixture = data;
+ const gchar *datadir;
+
+ cockpit_bridge_data_dirs = (const gchar **)fixture->datadirs;
+ datadir = cockpit_bridge_data_dirs[0];
+
+ setup_reload_packages (datadir, "old");
+ tc->packages = cockpit_packages_new ();
+
+ assert_manifest_checksum (tc, NULL, CHECKSUM_RELOAD_OLD);
+ assert_manifest_checksum (tc, "old", CHECKSUM_RELOAD_OLD);
+
+ setup_reload_packages (datadir, "new");
+ cockpit_packages_reload (tc->packages);
+
+ assert_manifest_checksum (tc, NULL, CHECKSUM_RELOAD_OLD);
+ assert_manifest_checksum (tc, "old", CHECKSUM_RELOAD_OLD);
+ assert_manifest_checksum (tc, "new", CHECKSUM_RELOAD_NEW);
+
+ teardown_reload_packages (datadir);
+}
+
+static void
+test_reload_removed (TestCase *tc,
+ gconstpointer data)
+{
+ const Fixture *fixture = data;
+ const gchar *datadir;
+
+ cockpit_bridge_data_dirs = (const gchar **)fixture->datadirs;
+ datadir = cockpit_bridge_data_dirs[0];
+
+ setup_reload_packages (datadir, "new");
+ tc->packages = cockpit_packages_new ();
+
+ assert_manifest_checksum (tc, NULL, CHECKSUM_RELOAD_NEW);
+ assert_manifest_checksum (tc, "old", CHECKSUM_RELOAD_NEW);
+ assert_manifest_checksum (tc, "new", CHECKSUM_RELOAD_NEW);
+
+ setup_reload_packages (datadir, "old");
+ cockpit_packages_reload (tc->packages);
+
+ assert_manifest_checksum (tc, NULL, CHECKSUM_RELOAD_NEW);
+ assert_manifest_checksum (tc, "old", CHECKSUM_RELOAD_NEW);
+ assert_manifest_checksum (tc, "new", NULL);
+
+ teardown_reload_packages (datadir);
+}
+
+static void
+test_reload_updated (TestCase *tc,
+ gconstpointer data)
+{
+ const Fixture *fixture = data;
+ const gchar *datadir;
+
+ cockpit_bridge_data_dirs = (const gchar **)fixture->datadirs;
+ datadir = cockpit_bridge_data_dirs[0];
+
+ setup_reload_packages (datadir, "old");
+ tc->packages = cockpit_packages_new ();
+
+ assert_manifest_checksum (tc, NULL, CHECKSUM_RELOAD_OLD);
+ assert_manifest_checksum (tc, "old", CHECKSUM_RELOAD_OLD);
+
+ setup_reload_packages (datadir, "updated");
+ cockpit_packages_reload (tc->packages);
+
+ assert_manifest_checksum (tc, NULL, CHECKSUM_RELOAD_OLD);
+ assert_manifest_checksum (tc, "old", CHECKSUM_RELOAD_UPDATED);
+
+ teardown_reload_packages (datadir);
+}
+
+static const Fixture fixture_csp_strip = {
+ .path = "/strip/test.html",
+ .datadirs = { SRCDIR "/src/bridge/mock-resource/csp", NULL },
+ .headers = { "Host", "blah:9090" },
+};
+
+static void
+test_csp_strip (TestCase *tc,
+ gconstpointer fixture)
+{
+ GBytes *data;
+ JsonObject *object;
+ GError *error = NULL;
+ guint count;
+
+ g_assert (fixture == &fixture_csp_strip);
+
+ while (tc->closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (tc->problem, ==, NULL);
+
+ data = mock_transport_pop_channel (tc->transport, "444");
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ cockpit_assert_json_eq (object, "{\"status\":200,\"reason\":\"OK\",\"headers\":{" STATIC_HEADERS ",\"Content-Security-Policy\":\"connect-src 'self' http://blah:9090 ws://blah:9090; form-action 'self' http://blah:9090; base-uri 'self' http://blah:9090; object-src 'none'; font-src 'self' http://blah:9090 data:; block-all-mixed-content; img-src 'self' http://blah:9090; default-src 'self' http://blah:9090\",\"Content-Type\":\"text/html\",\"X-Cockpit-Pkg-Checksum\":\"" CHECKSUM_CSP "\"}}");
+ json_object_unref (object);
+
+ data = mock_transport_combine_output (tc->transport, "444", &count);
+ g_assert_cmpint (count, ==, 1);
+ cockpit_assert_bytes_eq (data, "<html>\n<head>\n<title>Test</title>\n</head>\n<body>Test</body>\n</html>\n", -1);
+ g_bytes_unref (data);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_setenv_check ("XDG_DATA_DIRS", SRCDIR "/src/bridge/mock-resource/system", TRUE);
+ cockpit_setenv_check ("XDG_DATA_HOME", SRCDIR "/src/bridge/mock-resource/home", TRUE);
+
+ /* avoid looking at the real ~/.config and allow tests to add their own config */
+ config_home = g_dir_make_tmp ("config-home.XXXXXX", NULL);
+ g_assert (config_home != NULL);
+ cockpit_setenv_check ("XDG_CONFIG_HOME", config_home, TRUE);
+
+ cockpit_bridge_local_address = "127.0.0.1";
+
+ cockpit_test_init (&argc, &argv);
+
+ extern const gchar *cockpit_webresponse_fail_html_text;
+ cockpit_webresponse_fail_html_text =
+ "<html><head><title>@@message@@</title></head><body>@@message@@</body></html>\n";
+
+ g_test_add ("/packages/simple", TestCase, &fixture_simple,
+ setup, test_simple, teardown);
+ g_test_add ("/packages/forwarded", TestCase, &fixture_forwarded,
+ setup, test_forwarded, teardown);
+ g_test_add ("/packages/localized-translated", TestCase, &fixture_pig,
+ setup, test_localized_translated, teardown);
+ g_test_add ("/packages/localized-unknown", TestCase, &fixture_unknown,
+ setup, test_localized_unknown, teardown);
+ g_test_add ("/packages/localized-prefer-region", TestCase, &fixture_prefer_region,
+ setup, test_localized_prefer_region, teardown);
+ g_test_add ("/packages/localized-fallback", TestCase, &fixture_fallback,
+ setup, test_localized_fallback, teardown);
+ g_test_add ("/packages/incompatible/version", TestCase, &fixture_version,
+ setup, test_incompatible_version, teardown);
+ g_test_add ("/packages/incompatible/requires", TestCase, &fixture_requires,
+ setup, test_incompatible_requires, teardown);
+ g_test_add ("/packages/large", TestCase, &fixture_large,
+ setup, test_large, teardown);
+ g_test_add ("/packages/listing", TestCase, &fixture_listing,
+ setup, test_listing, teardown);
+ g_test_add ("/packages/override-config", TestCase, &fixture_override_config,
+ setup, test_override_config, teardown);
+ g_test_add ("/packages/not-found", TestCase, &fixture_not_found,
+ setup, test_not_found, teardown);
+ g_test_add ("/packages/unknown-package", TestCase, &fixture_unknown_package,
+ setup, test_unknown_package, teardown);
+ g_test_add ("/packages/bad-receive", TestCase, &fixture_large,
+ setup, test_bad_receive, teardown);
+ g_test_add ("/packages/no-path", TestCase, &fixture_no_path,
+ setup, test_no_path, teardown);
+ g_test_add ("/packages/bad-path", TestCase, &fixture_bad_path,
+ setup, test_bad_path, teardown);
+ g_test_add ("/packages/no-package", TestCase, &fixture_no_package,
+ setup, test_no_package, teardown);
+ g_test_add ("/packages/bad-package", TestCase, &fixture_bad_package,
+ setup, test_bad_package, teardown);
+
+ g_test_add ("/packages/listing-bad-name", TestCase, &fixture_list_bad_name,
+ setup, test_list_bad_name, teardown);
+
+ g_test_add ("/packages/glob", TestCase, &fixture_glob,
+ setup, test_glob, teardown);
+ g_test_add ("/packages/with-gzip", TestCase, &fixture_with_gzip,
+ setup, test_with_gzip, teardown);
+ g_test_add ("/packages/no-gzip", TestCase, &fixture_no_gzip,
+ setup, test_no_gzip, teardown);
+
+ g_test_add ("/packages/resolve/simple", TestCase, NULL,
+ setup_basic, test_resolve, teardown_basic);
+ g_test_add ("/packages/resolve/bad-dots", TestCase, NULL,
+ setup_basic, test_resolve_bad_dots, teardown_basic);
+ g_test_add ("/packages/resolve/bad-path", TestCase, NULL,
+ setup_basic, test_resolve_bad_path, teardown_basic);
+ g_test_add ("/packages/resolve/bad-package", TestCase, NULL,
+ setup_basic, test_resolve_bad_package, teardown_basic);
+ g_test_add ("/packages/resolve/not-found", TestCase, NULL,
+ setup_basic, test_resolve_not_found, teardown_basic);
+
+ g_test_add ("/packages/get-names", TestCase, NULL,
+ setup_basic, test_get_names, teardown_basic);
+
+ g_test_add ("/packages/get-bridges/normal", TestCase, NULL,
+ setup_basic, test_get_bridges, teardown_basic);
+ g_test_add ("/packages/get-bridges/broken", TestCase, &fixture_bad_bridges,
+ setup_basic, test_get_bridges_broken, teardown_basic);
+
+ g_test_add ("/packages/reload/added", TestCase, &fixture_reload,
+ setup_basic, test_reload_added, teardown_basic);
+ g_test_add ("/packages/reload/removed", TestCase, &fixture_reload,
+ setup_basic, test_reload_removed, teardown_basic);
+ g_test_add ("/packages/reload/updated", TestCase, &fixture_reload,
+ setup_basic, test_reload_updated, teardown_basic);
+
+ g_test_add ("/packages/csp/strip", TestCase, &fixture_csp_strip,
+ setup, test_csp_strip, teardown);
+
+ int result = g_test_run ();
+
+ rmdir (config_home);
+
+ return result;
+}
diff --git a/src/bridge/test-packet-channel.c b/src/bridge/test-packet-channel.c
new file mode 100644
index 0000000..8b64b4b
--- /dev/null
+++ b/src/bridge/test-packet-channel.c
@@ -0,0 +1,645 @@
+
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpacketchannel.h"
+
+#include "common/cockpitjson.h"
+#include "testlib/cockpittest.h"
+#include "testlib/mock-transport.h"
+
+#include <json-glib/json-glib.h>
+
+#include <glib/gstdio.h>
+#include <gio/gunixsocketaddress.h>
+
+#include <sys/socket.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+
+/* -----------------------------------------------------------------------------
+ * Test
+ */
+
+typedef struct {
+ GSocket *listen_sock;
+ GSource *listen_source;
+ guint listen_tag;
+ GSocket *conn_sock;
+ GSource *conn_source;
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *channel_problem;
+ gchar *unix_path;
+ gchar *temp_file;
+} TestCase;
+
+typedef struct {
+ const gchar *payload;
+ gboolean delay_listen;
+} Fixture;
+
+static gboolean
+on_socket_input (GSocket *socket,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ gchar buffer[128 * 1024];
+ GError *error = NULL;
+ gssize ret, wret;
+
+ ret = g_socket_receive (socket, buffer, sizeof (buffer), NULL, &error);
+ g_assert_no_error (error);
+
+ if (ret == 0)
+ {
+ g_socket_shutdown (socket, FALSE, TRUE, &error);
+ g_assert_no_error (error);
+ return FALSE;
+ }
+
+ g_assert (ret > 0);
+ wret = g_socket_send (socket, buffer, ret, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (wret == ret);
+ return TRUE;
+}
+
+static gboolean
+on_socket_connection (GSocket *socket,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ TestCase *tc = user_data;
+ GError *error = NULL;
+
+ g_assert (tc->conn_source == NULL);
+ tc->conn_sock = g_socket_accept (tc->listen_sock, NULL, &error);
+ g_assert_no_error (error);
+
+ tc->conn_source = g_socket_create_source (tc->conn_sock, G_IO_IN, NULL);
+ g_source_set_callback (tc->conn_source, (GSourceFunc)on_socket_input, tc, NULL);
+ g_source_attach (tc->conn_source, NULL);
+
+ /* Only one connection */
+ return FALSE;
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ const Fixture *fixture = data;
+ GSocketAddress *address;
+ GError *error = NULL;
+
+ tc->temp_file = g_strdup ("/tmp/cockpit-test-XXXXXX");
+ g_assert (close (g_mkstemp (tc->temp_file)) == 0);
+ tc->unix_path = g_strconcat (tc->temp_file, ".sock", NULL);
+
+ address = g_unix_socket_address_new (tc->unix_path);
+
+ tc->listen_sock = g_socket_new (G_SOCKET_FAMILY_UNIX, G_SOCKET_TYPE_SEQPACKET,
+ G_SOCKET_PROTOCOL_DEFAULT, &error);
+ g_assert_no_error (error);
+
+ g_socket_bind (tc->listen_sock, address, TRUE, &error);
+ g_assert_no_error (error);
+
+ g_socket_listen (tc->listen_sock, &error);
+ g_assert_no_error (error);
+
+ tc->listen_source = g_socket_create_source (tc->listen_sock, G_IO_IN, NULL);
+ g_source_set_callback (tc->listen_source, (GSourceFunc)on_socket_connection, tc, NULL);
+
+ if (!fixture || !fixture->delay_listen)
+ g_source_attach (tc->listen_source, NULL);
+
+ g_object_unref (address);
+
+ tc->transport = g_object_new (mock_transport_get_type (), NULL);
+}
+
+static void
+on_closed_get_problem (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gchar **retval = user_data;
+ g_assert (retval != NULL && *retval == NULL);
+ *retval = g_strdup (problem ? problem : "");
+}
+
+static void
+setup_channel (TestCase *tc,
+ gconstpointer data)
+{
+ setup (tc, data);
+
+ tc->channel = cockpit_packet_channel_open (COCKPIT_TRANSPORT (tc->transport), "548", tc->unix_path);
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_closed_get_problem), &tc->channel_problem);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ if (tc->conn_source)
+ {
+ g_source_destroy (tc->conn_source);
+ g_source_unref (tc->conn_source);
+ }
+ if (tc->listen_source)
+ {
+ g_source_destroy (tc->listen_source);
+ g_source_unref (tc->listen_source);
+ }
+ g_clear_object (&tc->listen_sock);
+ g_clear_object (&tc->conn_sock);
+
+ g_unlink (tc->unix_path);
+ g_free (tc->unix_path);
+ g_unlink (tc->temp_file);
+ g_free (tc->temp_file);
+
+ g_object_unref (tc->transport);
+
+ if (tc->channel)
+ {
+ g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
+ g_object_unref (tc->channel);
+ g_assert (tc->channel == NULL);
+ }
+
+ g_free (tc->channel_problem);
+
+ cockpit_assert_expected ();
+}
+
+static void
+expect_control_message (JsonObject *options,
+ const gchar *command,
+ const gchar *expected_channel,
+ ...) G_GNUC_NULL_TERMINATED;
+
+static void
+expect_control_message (JsonObject *options,
+ const gchar *expected_command,
+ const gchar *expected_channel,
+ ...)
+{
+ const gchar *expect_option;
+ const gchar *expect_value;
+ const gchar *value;
+ JsonNode *node;
+ va_list va;
+
+ g_assert (options != NULL);
+ g_assert_cmpstr (json_object_get_string_member (options, "command"), ==, expected_command);
+ g_assert_cmpstr (json_object_get_string_member (options, "channel"), ==, expected_channel);
+
+ va_start (va, expected_channel);
+ for (;;) {
+ expect_option = va_arg (va, const gchar *);
+ if (!expect_option)
+ break;
+ expect_value = va_arg (va, const gchar *);
+
+ value = NULL;
+ node = json_object_get_member (options, expect_option);
+ if (node && JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) == G_TYPE_STRING)
+ value = json_node_get_string (node);
+
+ g_assert_cmpstr (value, ==, expect_value);
+ }
+ va_end (va);
+}
+
+
+static void
+test_echo (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *payload;
+ GBytes *sent;
+
+ payload = g_bytes_new ("Marmalaade!", 11);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);
+ g_bytes_unref (payload);
+
+ while (mock_transport_count_sent (tc->transport) < 2)
+ g_main_context_iteration (NULL, TRUE);
+
+ sent = mock_transport_pop_channel (tc->transport, "548");
+ cockpit_assert_bytes_eq (sent, "Marmalaade!", 11);
+}
+
+static void
+test_large (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *object;
+ GBytes *sent;
+ GBytes *options;
+ GBytes *a, *b, *c;
+ gchar *big;
+ gchar *too_big;
+ gchar *maximum;
+
+ /* Send something big: Should make it through */
+ big = g_strnfill (32 * 1024, 'a');
+ a = g_bytes_new_take (big, strlen (big));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", a);
+
+ /* Send something too big: Should be truncated */
+ too_big = g_strnfill (80 * 1024, 'b');
+ b = g_bytes_new_take (too_big, strlen (too_big));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", b);
+
+ while (mock_transport_count_sent (tc->transport) < 3)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Set the max-size to massive */
+ object = cockpit_transport_build_json ("channel", "548", "command", "options", NULL);
+ json_object_set_int_member (object, "max-size", 128 * 1024);
+ options = cockpit_json_write_bytes (object);
+ json_object_unref (object);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), NULL, options);
+
+ /* Send something too big again: This time not truncated */
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", b);
+
+ /* Lastly send the full maximum */
+ maximum = g_strnfill (128 * 1024, 'c');
+ c = g_bytes_new_take (maximum, strlen (maximum));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", c);
+
+ while (mock_transport_count_sent (tc->transport) < 5)
+ g_main_context_iteration (NULL, TRUE);
+
+ sent = mock_transport_pop_channel (tc->transport, "548");
+ cockpit_assert_bytes_eq (sent, big, strlen (big));
+
+ sent = mock_transport_pop_channel (tc->transport, "548");
+ cockpit_assert_bytes_eq (sent, too_big, 64 * 1024); /* Truncated */
+
+ sent = mock_transport_pop_channel (tc->transport, "548");
+ cockpit_assert_bytes_eq (sent, too_big, strlen (too_big));
+
+ sent = mock_transport_pop_channel (tc->transport, "548");
+ cockpit_assert_bytes_eq (sent, maximum, strlen (maximum));
+
+ g_bytes_unref (a);
+ g_bytes_unref (b);
+ g_bytes_unref (c);
+ g_bytes_unref (options);
+}
+
+static const Fixture fixture_connect_in_progress = { .delay_listen = TRUE };
+
+static gboolean
+on_idle_listen (gpointer data)
+{
+ TestCase *tc = data;
+ g_source_attach (tc->listen_source, NULL);
+ return FALSE;
+}
+
+static void
+test_connect_in_progress (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *payload;
+ GBytes *sent;
+
+ payload = g_bytes_new ("Marmalaade!", 11);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);
+ g_bytes_unref (payload);
+
+ /* Attach listen source when idle */
+ g_idle_add (on_idle_listen, tc);
+
+ while (mock_transport_count_sent (tc->transport) < 2)
+ g_main_context_iteration (NULL, TRUE);
+
+ sent = mock_transport_pop_channel (tc->transport, "548");
+ cockpit_assert_bytes_eq (sent, "Marmalaade!", 11);
+}
+
+
+static void
+test_shutdown (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *payload;
+ GError *error = NULL;
+ JsonObject *sent;
+
+ payload = cockpit_transport_build_control ("channel", "548", "command", "done", NULL);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), NULL, payload);
+ g_bytes_unref (payload);
+
+ /* Wait until the socket has opened */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /*
+ * Close down the write end of the socket (what
+ * CockpitPacketChannel is reading from)
+ */
+ g_socket_shutdown (tc->conn_sock, FALSE, TRUE, &error);
+ g_assert_no_error (error);
+
+ while (tc->channel_problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (tc->channel_problem, ==, "");
+ sent = mock_transport_pop_control (tc->transport);
+ expect_control_message (sent, "ready", "548", NULL);
+ sent = mock_transport_pop_control (tc->transport);
+ expect_control_message (sent, "done", "548", NULL);
+
+ sent = mock_transport_pop_control (tc->transport);
+ expect_control_message (sent, "close", "548", "problem", NULL, NULL);
+}
+
+static void
+test_close_normal (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *payload;
+ GBytes *sent;
+ JsonObject *control;
+
+ /* Wait until the socket has opened */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ payload = g_bytes_new ("Marmalaade!", 11);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);
+ cockpit_channel_close (tc->channel, NULL);
+ g_bytes_unref (payload);
+
+ /* Wait until channel closes */
+ while (tc->channel_problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Shouldn't have had a chance to send message */
+ g_assert_cmpstr (tc->channel_problem, ==, "");
+ sent = mock_transport_pop_channel (tc->transport, "548");
+ g_assert (sent == NULL);
+
+ control = mock_transport_pop_control (tc->transport);
+ expect_control_message (control, "ready", "548", NULL);
+
+ control = mock_transport_pop_control (tc->transport);
+ expect_control_message (control, "close", "548", "problem", NULL, NULL);
+}
+
+static void
+test_close_problem (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *sent;
+
+ /* Wait until the socket has opened */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ sent = g_bytes_new ("Marmalaade!", 11);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", sent);
+ g_bytes_unref (sent);
+ cockpit_channel_close (tc->channel, "boooyah");
+
+ /* Wait until channel closes */
+ while (tc->channel_problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Should have sent no payload and control */
+ g_assert_cmpstr (tc->channel_problem, ==, "boooyah");
+ g_assert (mock_transport_pop_channel (tc->transport, "548") == NULL);
+ expect_control_message (mock_transport_pop_control (tc->transport), "ready", "548", NULL);
+ expect_control_message (mock_transport_pop_control (tc->transport),
+ "close", "548", "problem", "boooyah", NULL);
+}
+
+static void
+test_recv_invalid (TestCase *tc,
+ gconstpointer unused)
+{
+ GError *error = NULL;
+ GBytes *converted;
+
+ /* Wait until the socket has opened */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (g_socket_send (tc->conn_sock, "\x00Marmalaade!\x00", 13, NULL, &error), ==, 13);
+ g_assert_no_error (error);
+
+ while (mock_transport_count_sent (tc->transport) < 2)
+ g_main_context_iteration (NULL, TRUE);
+
+ converted = g_bytes_new ("\xef\xbf\xbd""Marmalaade!""\xef\xbf\xbd", 17);
+ g_assert (g_bytes_equal (converted, mock_transport_pop_channel (tc->transport, "548")));
+ g_bytes_unref (converted);
+}
+
+
+static gboolean
+add_remainder (gpointer user_data)
+{
+ GSocket *socket = user_data;
+ GError *error = NULL;
+ g_assert_cmpint (g_socket_send (socket, "\x94\x80", 2, NULL, &error), ==, 2);
+ g_assert_no_error (error);
+ return FALSE;
+}
+
+static void
+print_gbytes (GBytes *bytes)
+{
+ gsize i, len;
+ const char* data;
+
+ data = g_bytes_get_data (bytes, &len);
+ g_assert (data);
+ for (i = 0; i < len; ++i)
+ g_printf ("%X", data[i]);
+ puts("");
+}
+
+static void
+test_send_invalid (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *converted;
+ GBytes *sent;
+
+ sent = g_bytes_new ("Oh \x00Marma\x00laade!", 16);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", sent);
+ g_bytes_unref (sent);
+
+ while (mock_transport_count_sent (tc->transport) < 2)
+ g_main_context_iteration (NULL, TRUE);
+
+ converted = g_bytes_new ("Oh \xef\xbf\xbd""Marma""\xef\xbf\xbd""laade!", 20);
+ g_assert (g_bytes_equal (converted, mock_transport_pop_channel (tc->transport, "548")));
+ g_bytes_unref (converted);
+}
+
+static void
+test_recv_valid_batched (TestCase *tc,
+ gconstpointer unused)
+{
+ GError *error = NULL;
+ GBytes *converted;
+ GBytes *received;
+
+ /* Wait until the socket has opened */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (g_socket_send (tc->conn_sock, "Marmalaade!\xe2", 12, NULL, &error), ==, 12);
+ g_assert_no_error (error);
+
+ g_timeout_add (100, add_remainder, tc->conn_sock);
+
+ while (mock_transport_count_sent (tc->transport) < 2)
+ g_main_context_iteration (NULL, TRUE);
+
+ converted = g_bytes_new ("Marmalaade!\xe2\x94\x80", 14);
+ received = mock_transport_combine_output (tc->transport, "548", NULL);
+ if (!g_bytes_equal (converted, received))
+ {
+ g_test_fail ();
+ puts ("ERROR: unexpected output\nconverted:");
+ print_gbytes (converted);
+ puts ("received:");
+ print_gbytes (received);
+ }
+ g_bytes_unref (converted);
+ g_bytes_unref (received);
+}
+
+static void
+test_fail_not_found (void)
+{
+ CockpitTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem = NULL;
+
+ cockpit_expect_message ("*couldn't connect*");
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+ channel = cockpit_packet_channel_open (transport, "1", "/non-existent");
+ g_assert (channel != NULL);
+
+ /* Even through failure is on open, should not have closed yet */
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "not-found");
+ g_free (problem);
+ g_object_unref (channel);
+ g_object_unref (transport);
+
+ cockpit_assert_expected ();
+}
+
+static void
+test_fail_access_denied (void)
+{
+ CockpitTransport *transport;
+ CockpitChannel *channel;
+ gchar *unix_path;
+ gchar *problem = NULL;
+ gint fd;
+
+ if (geteuid () == 0)
+ {
+ g_test_skip ("running as root");
+ return;
+ }
+
+ cockpit_expect_message ("*couldn't connect*");
+
+ unix_path = g_strdup ("/tmp/cockpit-test-XXXXXX.sock");
+ fd = g_mkstemp (unix_path);
+ g_assert_cmpint (fd, >=, 0);
+
+ /* Take away all permissions from the file */
+ g_assert_cmpint (fchmod (fd, 0000), ==, 0);
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+ channel = cockpit_packet_channel_open (transport, "1", unix_path);
+ g_assert (channel != NULL);
+
+ /* Even through failure is on open, should not have closed yet */
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "access-denied");
+ g_free (problem);
+ g_free (unix_path);
+ close (fd);
+ g_object_unref (channel);
+ g_object_unref (transport);
+
+ cockpit_assert_expected ();
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/packet-channel/echo", TestCase, NULL,
+ setup_channel, test_echo, teardown);
+ g_test_add ("/packet-channel/large", TestCase, NULL,
+ setup_channel, test_large, teardown);
+ g_test_add ("/packet-channel/connect-in-progress",
+ TestCase, &fixture_connect_in_progress,
+ setup_channel, test_connect_in_progress, teardown);
+ g_test_add ("/packet-channel/shutdown", TestCase, NULL,
+ setup_channel, test_shutdown, teardown);
+ g_test_add ("/packet-channel/close-normal", TestCase, NULL,
+ setup_channel, test_close_normal, teardown);
+ g_test_add ("/packet-channel/close-problem", TestCase, NULL,
+ setup_channel, test_close_problem, teardown);
+ g_test_add ("/packet-channel/invalid-send", TestCase, NULL,
+ setup_channel, test_send_invalid, teardown);
+ g_test_add ("/packet-channel/invalid-recv", TestCase, NULL,
+ setup_channel, test_recv_invalid, teardown);
+ g_test_add ("/packet-channel/valid-recv-batched", TestCase, NULL,
+ setup_channel, test_recv_valid_batched, teardown);
+
+ g_test_add_func ("/packet-channel/fail/not-found", test_fail_not_found);
+ g_test_add_func ("/packet-channel/fail/access-denied", test_fail_access_denied);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-paths.c b/src/bridge/test-paths.c
new file mode 100644
index 0000000..3c5dedf
--- /dev/null
+++ b/src/bridge/test-paths.c
@@ -0,0 +1,246 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpaths.h"
+
+#include "testlib/cockpittest.h"
+
+#include <string.h>
+
+typedef struct {
+ const gchar *a;
+ const gchar *b;
+ gboolean expect;
+ const gchar *name;
+} CmpFixture;
+
+static const CmpFixture fixtures_has_parent[] = {
+ { "/c", "/c", FALSE, "equal" },
+ { "/c", "/c/d", FALSE, "child" },
+ { "/c", "/c/d/e", FALSE, "grand-child" },
+ { "/c/d", "/c", TRUE, "parent" },
+ { "/c/d/e", "/c", FALSE, "grand-parent" },
+ { "/c", "/peer", FALSE, "peer-after" },
+ { "/c", "/a", FALSE, "peer-before" },
+ { "/d", "/door", FALSE, "peer-prefix" },
+ { "/cat", "/c", FALSE, "peer-truncated" },
+ { "/", "/c", FALSE, "root-child" },
+ { "/", "/c/d", FALSE, "root-grand-child" },
+ { "/c", "/", TRUE, "root-parent" },
+ { "/c/d", "/", FALSE, "root-grand-parent" },
+};
+
+static void
+test_path_has_parent (gconstpointer data)
+{
+ const CmpFixture *fixture = data;
+ g_assert (cockpit_path_has_parent (fixture->a, fixture->b) == fixture->expect);
+}
+
+static const CmpFixture fixtures_has_ancestor[] = {
+ { "/c", "/c", FALSE, "equal" },
+ { "/c", "/c/d", FALSE, "child" },
+ { "/c", "/c/d/e", FALSE, "grand-child" },
+ { "/c/d", "/c", TRUE, "parent" },
+ { "/c/d/e", "/c", TRUE, "grand-parent" },
+ { "/c", "/peer", FALSE, "peer-after" },
+ { "/c", "/a", FALSE, "peer-before" },
+ { "/d", "/door", FALSE, "peer-prefix" },
+ { "/cat", "/c", FALSE, "peer-truncated" },
+ { "/", "/c", FALSE, "root-child" },
+ { "/", "/c/d", FALSE, "root-grand-child" },
+ { "/c", "/", TRUE, "root-parent" },
+ { "/c/d", "/", TRUE, "root-grand-parent" },
+};
+
+static void
+test_path_has_ancestor (gconstpointer data)
+{
+ const CmpFixture *fixture = data;
+ g_assert (cockpit_path_has_ancestor (fixture->a, fixture->b) == fixture->expect);
+}
+
+static const CmpFixture fixtures_equal_or_ancestor[] = {
+ { "/c", "/c", TRUE, "equal" },
+ { "/c", "/c/d", FALSE, "child" },
+ { "/c", "/c/d/e", FALSE, "grand-child" },
+ { "/c/d", "/c", TRUE, "parent" },
+ { "/c/d/e", "/c", TRUE, "grand-parent" },
+ { "/c", "/peer", FALSE, "peer-after" },
+ { "/c", "/a", FALSE, "peer-before" },
+ { "/d", "/door", FALSE, "peer-prefix" },
+ { "/cat", "/c", FALSE, "peer-truncated" },
+ { "/", "/c", FALSE, "root-child" },
+ { "/", "/c/d", FALSE, "root-grand-child" },
+ { "/c", "/", TRUE, "root-parent" },
+ { "/c/d", "/", TRUE, "root-grand-parent" },
+};
+
+static void
+test_path_equal_or_ancestor (gconstpointer data)
+{
+ const CmpFixture *fixture = data;
+ g_assert (cockpit_path_equal_or_ancestor (fixture->a, fixture->b) == fixture->expect);
+}
+
+static void
+test_paths_add_remove (void)
+{
+ GTree *paths;
+ const gchar *value;
+ const gchar *check;
+
+ paths = cockpit_paths_new ();
+
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/one"), ==, NULL);
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/two"), ==, NULL);
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/three/3"), ==, NULL);
+ g_assert_cmpint (g_tree_nnodes (paths), ==, 0);
+
+ /* Add first value */
+ value = "/one";
+ check = cockpit_paths_add (paths, value);
+ g_assert (value != check); /* reallocated */
+
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/one"), ==, "/one");
+ g_assert (cockpit_paths_contain (paths, "/one") == check);
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/two"), ==, NULL);
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/three/3"), ==, NULL);
+ g_assert_cmpint (g_tree_nnodes (paths), ==, 1);
+
+ /* Add another value */
+ value = cockpit_paths_add (paths, "/two");
+ g_assert (value != NULL);
+
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/one"), ==, "/one");
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/two"), ==, "/two");
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/three/3"), ==, NULL);
+ g_assert_cmpint (g_tree_nnodes (paths), ==, 2);
+
+ /* Add same one again */
+ check = cockpit_paths_add (paths, "/two");
+ g_assert (check == NULL); /* Already present */
+
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/one"), ==, "/one");
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/two"), ==, "/two");
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/three/3"), ==, NULL);
+ g_assert_cmpint (g_tree_nnodes (paths), ==, 2);
+
+ /* Remove first value */
+ g_assert (cockpit_paths_remove (paths, "/one") == TRUE); /* actually removed */
+
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/one"), ==, NULL);
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/two"), ==, "/two");
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/three/3"), ==, NULL);
+ g_assert_cmpint (g_tree_nnodes (paths), ==, 1);
+
+ /* Remove reference to second value */
+ g_assert (cockpit_paths_remove (paths, "/two") == TRUE); /* not actually */
+
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/one"), ==, NULL);
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/two"), ==, NULL);
+ g_assert_cmpstr (cockpit_paths_contain (paths, "/three/3"), ==, NULL);
+ g_assert_cmpint (g_tree_nnodes (paths), ==, 0);
+
+ /* Add something before destroy, to check destructors */
+ cockpit_paths_add (paths, "/three/3");
+
+ g_tree_destroy (paths);
+}
+
+static void
+test_paths_ancestor_descendant (void)
+{
+ GTree *paths;
+
+ paths = cockpit_paths_new ();
+
+ cockpit_paths_add (paths, "/a");
+ cockpit_paths_add (paths, "/b");
+ cockpit_paths_add (paths, "/c/3");
+
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/0") == FALSE);
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/z") == FALSE);
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/a") == TRUE);
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/a/1") == FALSE);
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/a1") == FALSE);
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/azzzzzz") == FALSE);
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/") == TRUE);
+
+ g_assert_cmpstr (cockpit_paths_contain_or_ancestor (paths, "/b"), ==, "/b");
+ g_assert_cmpstr (cockpit_paths_contain_or_ancestor (paths, "/b2"), ==, NULL);
+ g_assert_cmpstr (cockpit_paths_contain_or_ancestor (paths, "/b/2"), ==, "/b");
+ g_assert_cmpstr (cockpit_paths_contain_or_ancestor (paths, "/"), ==, NULL);
+
+ g_assert(cockpit_paths_contain_or_descendant (paths, "/c/3/4") == FALSE);
+ g_assert_cmpstr (cockpit_paths_contain_or_ancestor (paths, "/c"), ==, NULL);
+
+ /* Everything */
+ cockpit_paths_add (paths, "/");
+
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/a") == TRUE);
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/a/1") == FALSE);
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/a1") == FALSE);
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/") == TRUE);
+ g_assert_cmpstr (cockpit_paths_contain_or_ancestor (paths, "/b"), ==, "/b");
+ g_assert_cmpstr (cockpit_paths_contain_or_ancestor (paths, "/b2"), ==, "/");
+ g_assert_cmpstr (cockpit_paths_contain_or_ancestor (paths, "/b/2"), ==, "/b");
+ g_assert_cmpstr (cockpit_paths_contain_or_ancestor (paths, "/"), ==, "/");
+ g_assert (cockpit_paths_contain_or_descendant (paths, "/c/3/4") == FALSE);
+ g_assert_cmpstr (cockpit_paths_contain_or_ancestor (paths, "/c"), ==, "/");
+
+ g_tree_destroy (paths);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gchar *path;
+ guint i;
+
+ cockpit_test_init (&argc, &argv);
+
+ for (i = 0; i < G_N_ELEMENTS (fixtures_has_parent); i++)
+ {
+ path = g_strdup_printf ("/paths/has-parent/%s", fixtures_has_parent[i].name);
+ g_test_add_data_func (path, fixtures_has_parent + i, test_path_has_parent);
+ g_free (path);
+ }
+
+ for (i = 0; i < G_N_ELEMENTS (fixtures_has_parent); i++)
+ {
+ path = g_strdup_printf ("/paths/has-ancestor/%s", fixtures_has_ancestor[i].name);
+ g_test_add_data_func (path, fixtures_has_ancestor + i, test_path_has_ancestor);
+ g_free (path);
+ }
+ for (i = 0; i < G_N_ELEMENTS (fixtures_equal_or_ancestor); i++)
+ {
+ path = g_strdup_printf ("/paths/equal-or-ancestor/%s", fixtures_equal_or_ancestor[i].name);
+ g_test_add_data_func (path, fixtures_equal_or_ancestor + i, test_path_equal_or_ancestor);
+ g_free (path);
+ }
+
+ g_test_add_func ("/paths/add-remove", test_paths_add_remove);
+ g_test_add_func ("/paths/ancestor-descendant", test_paths_ancestor_descendant);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-pcp-archives.c b/src/bridge/test-pcp-archives.c
new file mode 100644
index 0000000..ef5cd5f
--- /dev/null
+++ b/src/bridge/test-pcp-archives.c
@@ -0,0 +1,439 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitmetrics.h"
+#include "cockpitpcpmetrics.h"
+
+#include "testlib/cockpittest.h"
+#include "common/cockpitjson.h"
+#include "testlib/mock-transport.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <time.h>
+#include <sys/stat.h>
+
+#include <pcp/pmapi.h>
+#include <pcp/impl.h>
+#include <pcp/import.h>
+
+static void
+init_mock_archives (void)
+{
+ g_assert (system ("rm -rf mock-archives && mkdir mock-archives") == 0);
+
+ g_assert (pmiStart ("mock-archives/0", 0) >= 0);
+ g_assert (pmiAddMetric ("mock.value", PM_ID_NULL,
+ PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_INSTANT,
+ pmiUnits (0, 0, 0, 0, 0, 0)) >= 0);
+ g_assert (pmiPutValue ("mock.value", NULL, "10") >= 0);
+ g_assert (pmiWrite (0, 0) >= 0);
+ g_assert (pmiPutValue ("mock.value", NULL, "11") >= 0);
+ g_assert (pmiWrite (1, 0) >= 0);
+ g_assert (pmiPutValue ("mock.value", NULL, "12") >= 0);
+ g_assert (pmiWrite (2, 0) >= 0);
+ g_assert (pmiEnd () >= 0);
+
+ g_assert (pmiStart ("mock-archives/1", 0) >= 0);
+ g_assert (pmiAddMetric ("mock.value", PM_ID_NULL,
+ PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_INSTANT,
+ pmiUnits (0, 0, 0, 0, 0, 0)) >= 0);
+ g_assert (pmiAddMetric ("mock.late", PM_ID_NULL,
+ PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_INSTANT,
+ pmiUnits (0, 0, 0, 0, 0, 0)) >= 0);
+ g_assert (pmiPutValue ("mock.value", NULL, "13") >= 0);
+ g_assert (pmiPutValue ("mock.late", NULL, "30") >= 0);
+ g_assert (pmiWrite (3, 0) >= 0);
+ g_assert (pmiPutValue ("mock.value", NULL, "14") >= 0);
+ g_assert (pmiPutValue ("mock.late", NULL, "31") >= 0);
+ g_assert (pmiWrite (4, 0) >= 0);
+ g_assert (pmiPutValue ("mock.value", NULL, "15") >= 0);
+ g_assert (pmiPutValue ("mock.late", NULL, "32") >= 0);
+ g_assert (pmiWrite (5, 0) >= 0);
+ g_assert (pmiEnd () >= 0);
+
+ // Broken archives should be skipped with a warning
+ g_assert (g_file_set_contents ("mock-archives/2.index", "not a pcp index file", -1, NULL));
+ g_assert (g_file_set_contents ("mock-archives/2.meta", "not a pcp meta file", -1, NULL));
+ g_assert (g_file_set_contents ("mock-archives/2.0", "not a pcp sample file", -1, NULL));
+}
+
+static void
+expect_broken_archive_warning (void)
+{
+ cockpit_expect_warning("*couldn't create pcp archive context for /*/mock-archives/2*");
+}
+
+typedef struct AtTeardown {
+ struct AtTeardown *link;
+ void (*func) (void *);
+ void *data;
+} AtTeardown;
+
+typedef struct {
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem;
+ gboolean channel_closed;
+
+ AtTeardown *at_teardown;
+} TestCase;
+
+static void
+on_channel_close (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ TestCase *tc = user_data;
+ g_assert (tc->channel_closed == FALSE);
+ tc->problem = g_strdup (problem);
+ tc->channel_closed = TRUE;
+}
+
+static void
+on_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ g_assert_not_reached ();
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ tc->transport = mock_transport_new ();
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+ tc->channel = NULL;
+ tc->at_teardown = NULL;
+}
+
+static void
+at_teardown (TestCase *tc, void *func, void *data)
+{
+ AtTeardown *item = g_new0 (AtTeardown, 1);
+
+ item->func = func;
+ item->data = data;
+ item->link = tc->at_teardown;
+ tc->at_teardown = item;
+}
+
+static void
+setup_metrics_channel_json (TestCase *tc, JsonObject *options)
+{
+ tc->channel = g_object_new (COCKPIT_TYPE_PCP_METRICS,
+ "transport", tc->transport,
+ "id", "1234",
+ "options", options,
+ NULL);
+ tc->channel_closed = FALSE;
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_channel_close), tc);
+ cockpit_channel_prepare (tc->channel);
+
+ /* Switch off compression by default. Compression is done by
+ * comparing two floating point values for exact equality, and we
+ * can't guarantee that we get the same behavior everywhere.
+ */
+ cockpit_metrics_set_compress (COCKPIT_METRICS (tc->channel), FALSE);
+}
+
+static GBytes *
+recv_bytes (TestCase *tc)
+{
+ GBytes *msg;
+ while ((msg = mock_transport_pop_channel (tc->transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ return msg;
+}
+
+static JsonObject *
+recv_json_object (TestCase *tc)
+{
+ GBytes *msg = recv_bytes (tc);
+ JsonObject *res = cockpit_json_parse_bytes (msg, NULL);
+ g_assert (res != NULL);
+ at_teardown (tc, json_object_unref, res);
+ return res;
+}
+
+static JsonNode *
+recv_json (TestCase *tc)
+{
+ GBytes *msg = recv_bytes (tc);
+ gsize length = g_bytes_get_size (msg);
+ JsonNode *res = cockpit_json_parse (g_bytes_get_data (msg, NULL), length, NULL);
+ g_assert (res != NULL);
+ at_teardown (tc, json_node_free, res);
+ return res;
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ cockpit_assert_expected ();
+
+ g_object_unref (tc->transport);
+
+ if (tc->channel)
+ {
+ g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
+ g_object_unref (tc->channel);
+ g_assert (tc->channel == NULL);
+ }
+
+ while (tc->at_teardown)
+ {
+ AtTeardown *item = tc->at_teardown;
+ tc->at_teardown = item->link;
+ item->func (item->data);
+ g_free (item);
+ }
+
+ g_free (tc->problem);
+}
+
+static JsonObject *
+json_obj (const gchar *str)
+{
+ GError *error = NULL;
+ JsonObject *res = cockpit_json_parse_object (str, -1, &error);
+ g_assert_no_error (error);
+ return res;
+}
+
+static void
+assert_sample_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ TestCase *tc,
+ const gchar *json_str)
+{
+ JsonNode *node = recv_json (tc);
+ g_assert_cmpint (json_node_get_node_type (node), ==, JSON_NODE_ARRAY);
+ _cockpit_assert_json_eq_msg (domain, file, line, func, json_node_get_array (node), json_str);
+}
+
+#define assert_sample(tc, json) \
+ (assert_sample_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (tc), (json)))
+
+static void
+test_metrics_single_archive (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': '" BUILDDIR "/mock-archives/0',"
+ " 'metrics': [ { 'name': 'mock.value' } ],"
+ " 'interval': 1000"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.value', 'units': '', 'semantics': 'instant' } ]");
+
+ assert_sample (tc, "[[10],[11],[12]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_archive_limit (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': '" BUILDDIR "/mock-archives/0',"
+ " 'metrics': [ { 'name': 'mock.value' } ],"
+ " 'interval': 1000,"
+ " 'limit': 2"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.value', 'units': '', 'semantics': 'instant' } ]");
+
+ assert_sample (tc, "[[10],[11]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_archive_timestamp (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': '" BUILDDIR "/mock-archives/0',"
+ " 'metrics': [ { 'name': 'mock.value' } ],"
+ " 'interval': 1000,"
+ " 'timestamp': 1000"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.value', 'units': '', 'semantics': 'instant' } ]");
+
+ assert_sample (tc, "[[11],[12]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_archive_timestamp_now (TestCase *tc,
+ gconstpointer unused)
+{
+ time_t now = time (NULL);
+
+ g_assert (pmiStart ("mock-archives/3", 0) >= 0);
+ g_assert (pmiAddMetric ("mock.now", PM_ID_NULL,
+ PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_INSTANT,
+ pmiUnits (0, 0, 0, 0, 0, 0)) >= 0);
+ /* one second in the past to one second in the future */
+ g_assert (pmiPutValue ("mock.now", NULL, "41") >= 0);
+ g_assert (pmiWrite (now - 1, 0) >= 0);
+ g_assert (pmiPutValue ("mock.now", NULL, "42") >= 0);
+ g_assert (pmiWrite (now, 0) >= 0);
+ g_assert (pmiPutValue ("mock.now", NULL, "43") >= 0);
+ g_assert (pmiWrite (now + 1, 0) >= 0);
+
+ g_autofree gchar* json = g_strdup_printf("{ 'source': '" BUILDDIR "/mock-archives/3',"
+ " 'metrics': [ { 'name': 'mock.now' } ],"
+ " 'interval': 1000,"
+ " 'timestamp': %lli000"
+ "}", (long long) now);
+ JsonObject *options = json_obj(json);
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.now', 'units': '', 'semantics': 'instant' } ]");
+
+ assert_sample (tc, "[[42],[43]]");
+
+ json_object_unref (options);
+}
+
+
+static void
+test_metrics_archive_directory (TestCase *tc,
+ gconstpointer unused)
+{
+ expect_broken_archive_warning();
+
+ JsonObject *meta;
+ JsonObject *options = json_obj("{ 'source': '" BUILDDIR "/mock-archives',"
+ " 'metrics': [ { 'name': 'mock.value' } ],"
+ " 'interval': 1000"
+ "}");
+ setup_metrics_channel_json (tc, options);
+
+ meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.value', 'units': '', 'semantics': 'instant' } ]");
+ assert_sample (tc, "[[10],[11],[12]]");
+
+ meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.value', 'units': '', 'semantics': 'instant' } ]");
+ assert_sample (tc, "[[13],[14],[15]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_archive_directory_timestamp (TestCase *tc,
+ gconstpointer unused)
+{
+ expect_broken_archive_warning();
+
+ JsonObject *meta;
+ JsonObject *options = json_obj("{ 'source': '" BUILDDIR "/mock-archives',"
+ " 'metrics': [ { 'name': 'mock.value' } ],"
+ " 'interval': 1000,"
+ " 'timestamp': 4000"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.value', 'units': '', 'semantics': 'instant' } ]");
+ assert_sample (tc, "[[14],[15]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_archive_directory_late_metric (TestCase *tc,
+ gconstpointer unused)
+{
+ expect_broken_archive_warning();
+ cockpit_expect_message ("*no such metric: mock.late: Unknown metric name*");
+
+ JsonObject *meta;
+ JsonObject *options = json_obj("{ 'source': '" BUILDDIR "/mock-archives',"
+ " 'metrics': [ { 'name': 'mock.late' } ],"
+ " 'interval': 1000"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.late', 'units': '', 'semantics': 'instant' } ]");
+ assert_sample (tc, "[[30],[31],[32]]");
+
+ json_object_unref (options);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ if (chdir (BUILDDIR) < 0)
+ g_assert_not_reached ();
+
+ init_mock_archives ();
+
+ g_test_add ("/metrics/single-archive", TestCase, NULL,
+ setup, test_metrics_single_archive, teardown);
+ g_test_add ("/metrics/archive-limit", TestCase, NULL,
+ setup, test_metrics_archive_limit, teardown);
+ g_test_add ("/metrics/archive-timestamp", TestCase, NULL,
+ setup, test_metrics_archive_timestamp, teardown);
+ g_test_add ("/metrics/archive-timestamp-now", TestCase, NULL,
+ setup, test_metrics_archive_timestamp_now, teardown);
+ g_test_add ("/metrics/archive-directory", TestCase, NULL,
+ setup, test_metrics_archive_directory, teardown);
+ g_test_add ("/metrics/archive-directory-timestamp", TestCase, NULL,
+ setup, test_metrics_archive_directory_timestamp, teardown);
+ g_test_add ("/metrics/archive-directory-late-metric", TestCase, NULL,
+ setup, test_metrics_archive_directory_late_metric, teardown);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-pcp.c b/src/bridge/test-pcp.c
new file mode 100644
index 0000000..38c1d38
--- /dev/null
+++ b/src/bridge/test-pcp.c
@@ -0,0 +1,639 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitmetrics.h"
+#include "cockpitpcpmetrics.h"
+
+#include "testlib/cockpittest.h"
+#include "common/cockpitjson.h"
+#include "testlib/mock-transport.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <dlfcn.h>
+
+#include <pcp/pmapi.h>
+#include <pcp/impl.h>
+
+#if PM_VERSION_CURRENT < PM_VERSION(4,0,0)
+#include <pcp/impl.h>
+#define pmSpecLocalPMDA(command) __pmSpecLocalPMDA(command)
+#endif
+
+void (*mock_pmda_control) (const char *cmd, ...);
+
+static void *
+init_mock_pmda (void)
+{
+ if (pmLoadNameSpace (SRCDIR "/src/bridge/mock-pmns") < 0)
+ {
+ g_test_skip ("No PCP\n");
+ exit (0);
+ }
+
+ g_assert (pmSpecLocalPMDA ("clear") == NULL);
+ g_assert (pmSpecLocalPMDA ("add,333,./mock-pmda.so,mock_init") == NULL);
+
+ void *handle = dlopen ("./mock-pmda.so", RTLD_NOW);
+ g_assert (handle != NULL);
+
+ mock_pmda_control = dlsym (handle, "mock_control");
+ g_assert (mock_pmda_control != NULL);
+
+ return handle;
+}
+
+typedef struct AtTeardown {
+ struct AtTeardown *link;
+ void (*func) (void *);
+ void *data;
+} AtTeardown;
+
+typedef struct {
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem;
+ gboolean channel_closed;
+
+ AtTeardown *at_teardown;
+} TestCase;
+
+static void
+on_channel_close (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ TestCase *tc = user_data;
+ g_assert (tc->channel_closed == FALSE);
+ tc->problem = g_strdup (problem);
+ tc->channel_closed = TRUE;
+}
+
+static void
+on_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ g_assert_not_reached ();
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ tc->transport = mock_transport_new ();
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_transport_closed), NULL);
+ tc->channel = NULL;
+ tc->at_teardown = NULL;
+
+ mock_pmda_control ("reset");
+}
+
+static void
+at_teardown (TestCase *tc, void *func, void *data)
+{
+ AtTeardown *item = g_new0 (AtTeardown, 1);
+
+ item->func = func;
+ item->data = data;
+ item->link = tc->at_teardown;
+ tc->at_teardown = item;
+}
+
+static void
+setup_metrics_channel_json (TestCase *tc, JsonObject *options)
+{
+ tc->channel = g_object_new (COCKPIT_TYPE_PCP_METRICS,
+ "transport", tc->transport,
+ "id", "1234",
+ "options", options,
+ NULL);
+ tc->channel_closed = FALSE;
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_channel_close), tc);
+ cockpit_channel_prepare (tc->channel);
+
+ /* We work with real timestamps here but we don't want the
+ interpolation to change any of our sample values.
+ */
+ cockpit_metrics_set_interpolate (COCKPIT_METRICS (tc->channel), FALSE);
+
+ /* Switch off compression by default. Compression is done by
+ * comparing two floating point values for exact equality, and we
+ * can't guarantee that we get the same behavior everywhere.
+ */
+ cockpit_metrics_set_compress (COCKPIT_METRICS (tc->channel), FALSE);
+}
+
+static GBytes *
+recv_bytes (TestCase *tc)
+{
+ GBytes *msg;
+ while ((msg = mock_transport_pop_channel (tc->transport, "1234")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ return msg;
+}
+
+static JsonObject *
+recv_json_object (TestCase *tc)
+{
+ GBytes *msg = recv_bytes (tc);
+ JsonObject *res = cockpit_json_parse_bytes (msg, NULL);
+ g_assert (res != NULL);
+ at_teardown (tc, json_object_unref, res);
+ return res;
+}
+
+static JsonNode *
+recv_json (TestCase *tc)
+{
+ GBytes *msg = recv_bytes (tc);
+ gsize length = g_bytes_get_size (msg);
+ JsonNode *res = cockpit_json_parse (g_bytes_get_data (msg, NULL), length, NULL);
+ g_assert (res != NULL);
+ at_teardown (tc, json_node_free, res);
+ return res;
+}
+
+static void
+wait_channel_closed (TestCase *tc)
+{
+ while (tc->channel_closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ cockpit_assert_expected ();
+
+ g_object_unref (tc->transport);
+
+ if (tc->channel)
+ {
+ g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
+ g_object_unref (tc->channel);
+ g_assert (tc->channel == NULL);
+ }
+
+ while (tc->at_teardown)
+ {
+ AtTeardown *item = tc->at_teardown;
+ tc->at_teardown = item->link;
+ item->func (item->data);
+ g_free (item);
+ }
+
+ g_free (tc->problem);
+}
+
+static JsonObject *
+json_obj (const gchar *str)
+{
+ GError *error = NULL;
+ JsonObject *res = cockpit_json_parse_object (str, -1, &error);
+ g_assert_no_error (error);
+ return res;
+}
+
+static void
+assert_sample_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ TestCase *tc,
+ const gchar *json_str)
+{
+ JsonNode *node = recv_json (tc);
+ g_assert_cmpint (json_node_get_node_type (node), ==, JSON_NODE_ARRAY);
+ _cockpit_assert_json_eq_msg (domain, file, line, func, json_node_get_array (node), json_str);
+}
+
+#define assert_sample(tc, json) \
+ (assert_sample_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (tc), (json)))
+
+static void
+test_metrics_compression (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.value' } ],"
+ " 'interval': 1"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+ cockpit_metrics_set_compress (COCKPIT_METRICS (tc->channel), TRUE);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.value', 'units': '', 'semantics': 'instant' } ]");
+
+ assert_sample (tc, "[[0]]");
+ assert_sample (tc, "[[]]");
+ assert_sample (tc, "[[]]");
+ mock_pmda_control ("set-value", 0, 1);
+ assert_sample (tc, "[[1]]");
+ assert_sample (tc, "[[]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_units (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.seconds' } ],"
+ " 'interval': 1"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.seconds', 'units': 'sec', 'semantics': 'instant' } ]");
+
+ assert_sample (tc, "[[60]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_units_conv (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.seconds', 'units': 'min' } ],"
+ " 'interval': 1"
+ "}");
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.seconds', 'units': 'min', 'semantics': 'instant' } ]");
+
+ assert_sample (tc, "[[1]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_units_noconv (TestCase *tc,
+ gconstpointer unused)
+{
+ cockpit_expect_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE,
+ "1234: direct: can't convert metric mock.seconds to units byte");
+
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.seconds', 'units': 'byte' } ],"
+ " 'interval': 1"
+ "}");
+ setup_metrics_channel_json (tc, options);
+
+ wait_channel_closed (tc);
+ g_assert_cmpstr (tc->problem, ==, "protocol-error");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_units_funny_conv (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.seconds', 'units': '2 min' } ],"
+ " 'interval': 1"
+ "}");
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.seconds', 'units': 'min*2', 'semantics': 'instant' } ]");
+
+ assert_sample (tc, "[[0.5]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_strings (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.string' } ],"
+ " 'interval': 1"
+ "}");
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.string', 'units': '', 'semantics': 'instant' } ]");
+
+ assert_sample (tc, "[[false]]");
+ assert_sample (tc, "[[false]]");
+
+ mock_pmda_control ("set-string", "barfoo");
+
+ assert_sample (tc, "[[false]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_simple_instances (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.values' } ],"
+ " 'interval': 1"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.values', 'units': '', 'semantics': 'instant', "
+ " 'instances': ['red', 'green', 'blue'] "
+ " } ]");
+
+ assert_sample (tc, "[[[0, 0, 0]]]");
+ mock_pmda_control ("set-value", 1, 1);
+ assert_sample (tc, "[[[1, 0, 0]]]");
+ mock_pmda_control ("set-value", 2, 1);
+ assert_sample (tc, "[[[1, 1, 0]]]");
+ mock_pmda_control ("set-value", 3, 1);
+ assert_sample (tc, "[[[1, 1, 1]]]");
+ assert_sample (tc, "[[[1, 1, 1]]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_instance_filter_include (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.values' } ],"
+ " 'instances': [ 'red', 'blue' ],"
+ " 'interval': 1"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.values', 'units': '', 'semantics': 'instant', "
+ " 'instances': ['red', 'blue'] "
+ " } ]");
+
+ assert_sample (tc, "[[[0, 0]]]");
+ mock_pmda_control ("set-value", 3, 1);
+ assert_sample (tc, "[[[0, 1]]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_instance_filter_omit (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.values' } ],"
+ " 'omit-instances': [ 'green' ],"
+ " 'interval': 1"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.values', 'units': '', 'semantics': 'instant', "
+ " 'instances': ['red', 'blue'] "
+ " } ]");
+
+ assert_sample (tc, "[[[0, 0]]]");
+ mock_pmda_control ("set-value", 3, 1);
+ assert_sample (tc, "[[[0, 1]]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_instance_dynamic (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *meta;
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.instances' } ],"
+ " 'interval': 1"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.instances', 'units': '', 'semantics': 'instant', "
+ " 'instances': [] "
+ " } ]");
+
+ assert_sample (tc, "[[[]]]");
+
+ mock_pmda_control ("add-instance", "bananas", 5);
+ mock_pmda_control ("add-instance", "milk", 3);
+
+ meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.instances', 'units': '', 'semantics': 'instant', "
+ " 'instances': [ 'bananas', 'milk' ] "
+ " } ]");
+ assert_sample (tc, "[[[ 5, 3 ]]]");
+ assert_sample (tc, "[[[ 5, 3 ]]]");
+
+ mock_pmda_control ("del-instance", "bananas");
+
+ meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.instances', 'units': '', 'semantics': 'instant', "
+ " 'instances': [ 'milk' ] "
+ " } ]");
+ assert_sample (tc, "[[[ 3 ]]]");
+
+ mock_pmda_control ("add-instance", "milk", 2);
+
+ assert_sample (tc, "[[[ 2 ]]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_counter (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.counter', 'derive': 'delta' } ],"
+ " 'interval': 1"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.counter', 'units': '', 'semantics': 'counter', 'derive': 'delta' } ]");
+
+ assert_sample (tc, "[[false]]");
+ assert_sample (tc, "[[0]]");
+ assert_sample (tc, "[[0]]");
+ mock_pmda_control ("inc-counter", 5);
+ assert_sample (tc, "[[5]]");
+ assert_sample (tc, "[[0]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_counter64 (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.counter64', 'derive': 'delta' } ],"
+ " 'interval': 1"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.counter64', 'units': '', 'semantics': 'counter', 'derive': 'delta' } ]");
+
+ assert_sample (tc, "[[false]]");
+ assert_sample (tc, "[[0]]");
+ assert_sample (tc, "[[0]]");
+ mock_pmda_control ("inc-counter64", 5);
+ assert_sample (tc, "[[5]]");
+ assert_sample (tc, "[[0]]");
+
+ json_object_unref (options);
+}
+
+static void
+test_metrics_counter_across_meta (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *options = json_obj("{ 'source': 'direct',"
+ " 'metrics': [ { 'name': 'mock.counter', 'derive': 'delta' },"
+ " { 'name': 'mock.instances' }"
+ " ],"
+ " 'interval': 1"
+ "}");
+
+ setup_metrics_channel_json (tc, options);
+
+ JsonObject *meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.counter',"
+ " 'units': '',"
+ " 'semantics': 'counter',"
+ " 'derive': 'delta'"
+ " },"
+ " { 'name': 'mock.instances',"
+ " 'units': '',"
+ " 'semantics': 'instant',"
+ " 'instances': [] }"
+ "]");
+
+ assert_sample (tc, "[[false,[]]]");
+ assert_sample (tc, "[[0,[]]]");
+
+ /* Add an instance, which triggers a meta message. The counter
+ should be unaffected and return '0'. Since it is still in the
+ same place in the arrays, it might also be compressed away but as
+ it happens, the channel will not compress over any meta message.
+ */
+ mock_pmda_control ("add-instance", "foo", 12);
+ meta = recv_json_object (tc);
+ cockpit_assert_json_eq (json_object_get_array_member (meta, "metrics"),
+ "[ { 'name': 'mock.counter',"
+ " 'units': '',"
+ " 'semantics': 'counter',"
+ " 'derive': 'delta'"
+ " },"
+ " { 'name': 'mock.instances',"
+ " 'units': '',"
+ " 'semantics': 'instant',"
+ " 'instances': [ 'foo' ] }"
+ "]");
+ assert_sample (tc, "[[0,[12]]]");
+
+ json_object_unref (options);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ void *handle;
+ int ret;
+
+ cockpit_test_init (&argc, &argv);
+
+ if (chdir (BUILDDIR) < 0)
+ g_assert_not_reached ();
+
+ handle = init_mock_pmda ();
+
+ g_test_add ("/metrics/compression", TestCase, NULL,
+ setup, test_metrics_compression, teardown);
+
+ g_test_add ("/metrics/units", TestCase, NULL,
+ setup, test_metrics_units, teardown);
+ g_test_add ("/metrics/units-conv", TestCase, NULL,
+ setup, test_metrics_units_conv, teardown);
+ g_test_add ("/metrics/units-noconv", TestCase, NULL,
+ setup, test_metrics_units_noconv, teardown);
+ g_test_add ("/metrics/units-funny-conv", TestCase, NULL,
+ setup, test_metrics_units_funny_conv, teardown);
+
+ g_test_add ("/metrics/strings", TestCase, NULL,
+ setup, test_metrics_strings, teardown);
+
+ g_test_add ("/metrics/simple-instances", TestCase, NULL,
+ setup, test_metrics_simple_instances, teardown);
+ g_test_add ("/metrics/instance-filter-include", TestCase, NULL,
+ setup, test_metrics_instance_filter_include, teardown);
+ g_test_add ("/metrics/instance-filter-omit", TestCase, NULL,
+ setup, test_metrics_instance_filter_omit, teardown);
+ g_test_add ("/metrics/instance-dynamic", TestCase, NULL,
+ setup, test_metrics_instance_dynamic, teardown);
+
+ g_test_add ("/metrics/counter", TestCase, NULL,
+ setup, test_metrics_counter, teardown);
+ g_test_add ("/metrics/counter64", TestCase, NULL,
+ setup, test_metrics_counter64, teardown);
+ g_test_add ("/metrics/counter-across-meta", TestCase, NULL,
+ setup, test_metrics_counter_across_meta, teardown);
+
+ ret = g_test_run ();
+
+ dlclose (handle);
+ return ret;
+}
diff --git a/src/bridge/test-peer.c b/src/bridge/test-peer.c
new file mode 100644
index 0000000..6ec3812
--- /dev/null
+++ b/src/bridge/test-peer.c
@@ -0,0 +1,618 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpeer.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpitjson.h"
+#include "testlib/cockpittest.h"
+#include "testlib/mock-transport.h"
+
+#include <json-glib/json-glib.h>
+
+#include <gio/gio.h>
+#include <glib/gstdio.h>
+
+#include <string.h>
+#include <unistd.h>
+
+static GType mock_echo_channel_get_type (void) G_GNUC_CONST;
+
+typedef struct {
+ CockpitChannel parent;
+ gboolean close_called;
+} MockEchoChannel;
+
+typedef CockpitChannelClass MockEchoChannelClass;
+
+G_DEFINE_TYPE (MockEchoChannel, mock_echo_channel, COCKPIT_TYPE_CHANNEL);
+
+static gboolean
+on_other_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer data)
+{
+ gboolean *flag = data;
+ g_assert (*flag == FALSE);
+ *flag = TRUE;
+ return FALSE;
+}
+
+static void
+mock_echo_channel_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ cockpit_channel_send (channel, message, FALSE);
+}
+
+static void
+mock_echo_channel_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ MockEchoChannel *self = (MockEchoChannel *)channel;
+ self->close_called = TRUE;
+ COCKPIT_CHANNEL_CLASS (mock_echo_channel_parent_class)->close (channel, problem);
+}
+
+static void
+mock_echo_channel_init (MockEchoChannel *self)
+{
+
+}
+
+static void
+mock_echo_channel_constructed (GObject *obj)
+{
+ G_OBJECT_CLASS (mock_echo_channel_parent_class)->constructed (obj);
+ cockpit_channel_ready (COCKPIT_CHANNEL (obj), NULL);
+}
+
+static void
+mock_echo_channel_class_init (MockEchoChannelClass *klass)
+{
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->constructed = mock_echo_channel_constructed;
+ channel_class->recv = mock_echo_channel_recv;
+ channel_class->close = mock_echo_channel_close;
+}
+
+static CockpitChannel *
+mock_echo_channel_open (CockpitTransport *transport,
+ const gchar *channel_id)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+
+ g_assert (channel_id != NULL);
+
+ options = json_object_new ();
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", channel_id,
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+ return channel;
+}
+
+typedef struct {
+ MockTransport *transport;
+ CockpitChannel *channel;
+ CockpitPeer *peer;
+} TestCase;
+
+static gboolean
+on_transport_control (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *message,
+ gpointer user_data)
+{
+ TestCase *tc = user_data;
+ const gchar *payload;
+ JsonObject *object;
+ GBytes *data;
+
+ g_assert (tc != NULL);
+
+ if (channel && g_str_equal (command, "open"))
+ {
+ if (tc->peer && cockpit_peer_handle (tc->peer, channel, options, message))
+ {
+ return TRUE;
+ }
+
+ /* Fallback to echo implementation */
+ else if (!tc->channel && cockpit_json_get_string (options, "payload", NULL, &payload) &&
+ payload && g_str_equal (payload, "upper"))
+ {
+ tc->channel = mock_echo_channel_open (transport, channel);
+ return TRUE;
+ }
+
+ else
+ {
+ object = json_object_new ();
+ json_object_set_string_member (object, "command", "close");
+ json_object_set_string_member (object, "channel", channel);
+ json_object_set_string_member (object, "problem", "not-supported");
+ data = cockpit_json_write_bytes (object);
+ cockpit_transport_send (transport, NULL, data);
+ json_object_unref (object);
+ g_bytes_unref (data);
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer unused)
+{
+ tc->transport = g_object_new (mock_transport_get_type (), NULL);
+ while (g_main_context_iteration (NULL, FALSE));
+
+ /* Connect to fallback implementation */
+ g_signal_connect (tc->transport, "control", G_CALLBACK (on_transport_control), tc);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer unused)
+{
+ cockpit_assert_expected ();
+
+ g_clear_object (&tc->channel);
+
+ if (tc->peer)
+ {
+ g_object_add_weak_pointer (G_OBJECT (tc->peer), (gpointer *)&tc->peer);
+ g_object_unref (tc->peer);
+ g_assert (tc->peer == NULL);
+ }
+
+ g_object_add_weak_pointer (G_OBJECT (tc->transport), (gpointer *)&tc->transport);
+ g_object_unref (tc->transport);
+ g_assert (tc->transport == NULL);
+}
+
+static CockpitPeer *
+peer_new (MockTransport *transport,
+ const gchar *bridge)
+{
+ CockpitPeer *peer;
+ JsonObject *object;
+ GError *error = NULL;
+
+ object = cockpit_json_parse_object (bridge, -1, &error);
+ g_assert_no_error (error);
+
+ peer = cockpit_peer_new (COCKPIT_TRANSPORT (transport), object);
+ json_object_unref (object);
+
+ return peer;
+}
+
+static CockpitPeer *
+mock_peer_simple_new (MockTransport *transport,
+ const gchar *payload)
+{
+ CockpitPeer *peer;
+ gchar *bridge;
+
+ bridge = g_strdup_printf ("{ \"match\": { \"payload\": \"%s\" }, \"spawn\": [ \"%s\", \"--%s\" ] }",
+ payload, BUILDDIR "/mock-bridge", payload);
+
+ peer = peer_new (transport, bridge);
+ g_free (bridge);
+
+ return peer;
+}
+
+static CockpitPeer *
+mock_peer_fail_new (MockTransport *transport,
+ const gchar *payload,
+ const gchar *problem)
+{
+ CockpitPeer *peer;
+ gchar *bridge;
+
+ if (problem)
+ {
+ bridge = g_strdup_printf ("{ \"match\": { \"payload\": \"%s\" }, \"spawn\": [ \"/non-existent\" ], \"problem\": \"%s\" }", payload, problem);
+ }
+ else
+ {
+ bridge = g_strdup_printf ("{ \"match\": { \"payload\": \"%s\" }, \"spawn\": [ \"/non-existent\" ] }", payload);
+ }
+
+ peer = peer_new (transport, bridge);
+ g_free (bridge);
+
+ return peer;
+}
+
+static void
+emit_string (TestCase *tc,
+ const gchar *channel,
+ const gchar *string)
+{
+ GBytes *bytes = g_bytes_new (string, strlen (string));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), channel, bytes);
+ g_bytes_unref (bytes);
+}
+
+static void
+test_simple (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *sent;
+
+ tc->peer = mock_peer_simple_new (tc->transport, "upper");
+
+ /* The filter should ignore this */
+ emit_string (tc, NULL, "{\"command\": \"hello\"}");
+
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ emit_string (tc, "a", "oh marmalade");
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE", -1);
+
+ /* The fallback channel was not created */
+ g_assert (tc->channel == NULL);
+}
+
+static void
+test_serial (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *sent;
+
+ tc->peer = mock_peer_simple_new (tc->transport, "upper");
+
+ /* The filter should ignore this */
+ emit_string (tc, NULL, "{\"command\": \"hello\"}");
+
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ emit_string (tc, "a", "oh marmalade");
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE", -1);
+
+ /* The fallback channel was not created */
+ g_assert (tc->channel == NULL);
+
+ /* Open a second channel */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"b\", \"payload\": \"upper\"}");
+ emit_string (tc, "b", "zero g");
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "b")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_bytes_eq (sent, "ZERO G", -1);
+
+ /* The fallback channel was not created */
+ g_assert (tc->channel == NULL);
+}
+
+static void
+test_parallel (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *sent;
+
+ tc->peer = mock_peer_simple_new (tc->transport, "upper");
+
+ /* The filter should ignore this */
+ emit_string (tc, NULL, "{\"command\": \"hello\"}");
+
+ /* Open two channels at the same time both bound for the peer */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"b\", \"payload\": \"upper\"}");
+ emit_string (tc, "b", "zero g");
+ emit_string (tc, "a", "oh marmalade");
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE", -1);
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "b")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_bytes_eq (sent, "ZERO G", -1);
+
+ /* The fallback channel was not created */
+ g_assert (tc->channel == NULL);
+}
+
+static void
+test_init_problem (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *sent;
+
+ /* The "lower" channel has no local implementation to fall back to */
+ tc->peer = mock_peer_simple_new (tc->transport, "problem");
+
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"problem\"}");
+ emit_string (tc, "a", "Oh Marmalade");
+
+ while ((sent = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_json_eq (sent, "{\"command\":\"close\",\"channel\":\"a\",\"problem\":\"canna-do-it\",\"another-field\":\"extra\"}");
+
+ /* The fallback channel was not created */
+ g_assert (tc->channel == NULL);
+}
+
+static void
+test_not_supported (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *sent;
+
+ /* The "lower" channel has no local implementation to fall back to */
+ tc->peer = mock_peer_fail_new (tc->transport, "lower", NULL);
+
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"lower\"}");
+ emit_string (tc, "a", "Oh Marmalade");
+
+ while ((sent = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_json_eq (sent, "{\"command\":\"close\",\"channel\":\"a\",\"problem\":\"not-supported\"}");
+
+ /* The fallback channel was not created */
+ g_assert (tc->channel == NULL);
+}
+
+static void
+test_fail_problem (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *sent;
+
+ tc->peer = mock_peer_fail_new (tc->transport, "lower", "access-denied");
+
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"lower\"}");
+ emit_string (tc, "a", "Oh Marmalade");
+
+ while ((sent = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_json_eq (sent, "{\"command\":\"close\",\"channel\":\"a\",\"problem\":\"access-denied\"}");
+
+ /* The fallback channel was not created */
+ g_assert (tc->channel == NULL);
+}
+
+static void
+test_fallback (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *sent;
+
+ /* The "upper" channel has a local implementation to fallback to */
+ tc->peer = mock_peer_fail_new (tc->transport, "upper", NULL);
+
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ emit_string (tc, "a", "Oh MarmaLade");
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* The fallback just echos */
+ cockpit_assert_bytes_eq (sent, "Oh MarmaLade", -1);
+
+ /* The fallback channel was created */
+ g_assert (tc->channel != NULL);
+}
+
+static void
+test_reopen (TestCase *tc,
+ gconstpointer unused)
+{
+ const gchar *bridge;
+ GBytes *sent;
+ JsonObject *control;
+
+ bridge = "{ \"match\": { \"payload\": \"upper\" }, \"spawn\": [ \"/" BUILDDIR "/mock-bridge" "\", \"--upper\", \"--count\" ] }";
+ tc->peer = peer_new (tc->transport, bridge);
+
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ emit_string (tc, "a", "Oh MarmaLade");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"ready\",\"channel\":\"a\",\"count\":0}");
+ control = NULL;
+
+ /* Get a good response */
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE", -1);
+
+ /* Reset the peer. This closes the channel. */
+ cockpit_peer_reset (tc->peer);
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"close\",\"channel\":\"a\",\"problem\":\"terminated\"}");
+ control = NULL;
+
+ /* Sending again reopens with count at zero */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ emit_string (tc, "a", "Oh MarmaLade");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"ready\",\"channel\":\"a\",\"count\":0}");
+ control = NULL;
+
+ /* Get a good response */
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE", -1);
+}
+
+static void
+test_timeout (TestCase *tc,
+ gconstpointer unused)
+{
+ const gchar *bridge;
+ GBytes *sent;
+ JsonObject *control;
+ CockpitTransport *other = NULL;
+ gboolean closed = FALSE;
+
+ bridge = "{ \"match\": { \"payload\": \"upper\" }, \"timeout\": 1, \"spawn\": [ \"/" BUILDDIR "/mock-bridge" "\", \"--upper\", \"--count\" ] }";
+ tc->peer = peer_new (tc->transport, bridge);
+
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ emit_string (tc, "a", "Oh MarmaLade");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"ready\",\"channel\":\"a\",\"count\":0}");
+ control = NULL;
+
+ /* Get a good response */
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE", -1);
+
+ /* Close the channel, timeout should close the peer transport */
+ other = g_object_ref (cockpit_peer_ensure (tc->peer));
+ g_signal_connect (other, "closed", G_CALLBACK (on_other_closed), &closed);
+ emit_string (tc, NULL, "{\"command\": \"close\", \"channel\": \"a\"}");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"close\",\"channel\":\"a\"}");
+ control = NULL;
+
+ while (!closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Sending again reopens peer with count at 0 */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ emit_string (tc, "a", "Oh MarmaLade");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"ready\",\"channel\":\"a\",\"count\":0}");
+ control = NULL;
+
+ /* Get a good response */
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE", -1);
+
+ g_object_unref (other);
+}
+
+static void
+test_reopen_fail (TestCase *tc,
+ gconstpointer unused)
+{
+ gchar *link;
+ gchar *bridge;
+ GBytes *sent;
+ JsonObject *control;
+
+ link = g_strdup_printf ("%s/mock-bridge", g_get_tmp_dir ());
+ bridge = g_strdup_printf("{ \"match\": { \"payload\": \"lower\" }, \"spawn\": [ \"%s\", \"--lower\" ], \"problem\": \"custom-problem\" }", link);
+ tc->peer = peer_new (tc->transport, bridge);
+
+ if (g_file_test (link, G_FILE_TEST_EXISTS))
+ g_assert (g_unlink (link) == 0);
+
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"lower\"}");
+ emit_string (tc, "a", "Oh Marmalade");
+
+ /* Bridge doesn't exist, get problem */
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"close\", \"channel\": \"a\", \"problem\":\"custom-problem\"}");
+ control = NULL;
+
+ cockpit_peer_reset (tc->peer);
+ g_assert (symlink (BUILDDIR "/mock-bridge", link) == 0);
+
+ /* Bridge exists, channel works */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"lower\"}");
+ emit_string (tc, "a", "Oh MarmaLade");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"ready\",\"channel\":\"a\"}");
+ control = NULL;
+
+ /* Get a good response */
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "oh marmalade", -1);
+
+ g_assert (g_unlink (link) == 0);
+ g_free (bridge);
+ g_free (link);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/peer/simple", TestCase, NULL,
+ setup, test_simple, teardown);
+ g_test_add ("/peer/serial", TestCase, NULL,
+ setup, test_serial, teardown);
+ g_test_add ("/peer/parallel", TestCase, NULL,
+ setup, test_parallel, teardown);
+ g_test_add ("/peer/not-supported", TestCase, NULL,
+ setup, test_not_supported, teardown);
+ g_test_add ("/peer/fail-problem", TestCase, NULL,
+ setup, test_fail_problem, teardown);
+ g_test_add ("/peer/init-problem", TestCase, NULL,
+ setup, test_init_problem, teardown);
+ g_test_add ("/peer/fallback", TestCase, NULL,
+ setup, test_fallback, teardown);
+ g_test_add ("/peer/reopen", TestCase, NULL,
+ setup, test_reopen, teardown);
+ g_test_add ("/peer/reopen-fail", TestCase, NULL,
+ setup, test_reopen_fail, teardown);
+ g_test_add ("/peer/timeout", TestCase, NULL,
+ setup, test_timeout, teardown);
+ return g_test_run ();
+}
diff --git a/src/bridge/test-pipe-channel.c b/src/bridge/test-pipe-channel.c
new file mode 100644
index 0000000..ca23d57
--- /dev/null
+++ b/src/bridge/test-pipe-channel.c
@@ -0,0 +1,970 @@
+
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpipechannel.h"
+
+#include "common/cockpitjson.h"
+#include "testlib/cockpittest.h"
+#include "testlib/mock-transport.h"
+
+#include <json-glib/json-glib.h>
+
+#include <glib/gstdio.h>
+#include <gio/gunixsocketaddress.h>
+
+#include <sys/socket.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+
+/* -----------------------------------------------------------------------------
+ * Test
+ */
+
+typedef struct {
+ GSocket *listen_sock;
+ GSource *listen_source;
+ GSocket *conn_sock;
+ GSource *conn_source;
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *channel_problem;
+ gchar *unix_path;
+ gchar *temp_file;
+} TestCase;
+
+static gboolean
+on_socket_input (GSocket *socket,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ gchar buffer[1024];
+ GError *error = NULL;
+ gssize ret, wret;
+
+ ret = g_socket_receive (socket, buffer, sizeof (buffer), NULL, &error);
+ g_assert_no_error (error);
+
+ if (ret == 0)
+ {
+ g_socket_shutdown (socket, FALSE, TRUE, &error);
+ g_assert_no_error (error);
+ return FALSE;
+ }
+
+ g_assert (ret > 0);
+ wret = g_socket_send (socket, buffer, ret, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (wret == ret);
+ return TRUE;
+}
+
+static gboolean
+on_socket_connection (GSocket *socket,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ TestCase *tc = user_data;
+ GError *error = NULL;
+
+ g_assert (tc->conn_source == NULL);
+ tc->conn_sock = g_socket_accept (tc->listen_sock, NULL, &error);
+ g_assert_no_error (error);
+
+ tc->conn_source = g_socket_create_source (tc->conn_sock, G_IO_IN, NULL);
+ g_source_set_callback (tc->conn_source, (GSourceFunc)on_socket_input, tc, NULL);
+ g_source_attach (tc->conn_source, NULL);
+
+ /* Only one connection */
+ return FALSE;
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ GSocketAddress *address;
+ GError *error = NULL;
+
+ tc->unix_path = g_strdup (data);
+ if (tc->unix_path == NULL)
+ {
+ tc->temp_file = g_strdup ("/tmp/cockpit-test-XXXXXX");
+ g_assert (close (g_mkstemp (tc->temp_file)) == 0);
+ tc->unix_path = g_strconcat (tc->temp_file, ".sock", NULL);
+ }
+
+ address = g_unix_socket_address_new (tc->unix_path);
+
+ tc->listen_sock = g_socket_new (G_SOCKET_FAMILY_UNIX, G_SOCKET_TYPE_STREAM,
+ G_SOCKET_PROTOCOL_DEFAULT, &error);
+ g_assert_no_error (error);
+
+ g_socket_bind (tc->listen_sock, address, TRUE, &error);
+ g_assert_no_error (error);
+
+ g_socket_listen (tc->listen_sock, &error);
+ g_assert_no_error (error);
+
+ tc->listen_source = g_socket_create_source (tc->listen_sock, G_IO_IN, NULL);
+ g_source_set_callback (tc->listen_source, (GSourceFunc)on_socket_connection, tc, NULL);
+ g_source_attach (tc->listen_source, NULL);
+
+ g_object_unref (address);
+
+ tc->transport = g_object_new (mock_transport_get_type (), NULL);
+}
+
+static void
+on_closed_get_problem (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gchar **retval = user_data;
+ g_assert (retval != NULL && *retval == NULL);
+ *retval = g_strdup (problem ? problem : "");
+}
+
+static void
+setup_channel (TestCase *tc,
+ gconstpointer data)
+{
+ setup (tc, data);
+ tc->channel = cockpit_pipe_channel_open (COCKPIT_TRANSPORT (tc->transport), "548", tc->unix_path);
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_closed_get_problem), &tc->channel_problem);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ if (tc->conn_source)
+ {
+ g_source_destroy (tc->conn_source);
+ g_source_unref (tc->conn_source);
+ }
+ if (tc->listen_source)
+ {
+ g_source_destroy (tc->listen_source);
+ g_source_unref (tc->listen_source);
+ }
+ g_clear_object (&tc->listen_sock);
+ g_clear_object (&tc->conn_sock);
+
+ g_unlink (tc->unix_path);
+ g_free (tc->unix_path);
+ g_unlink (tc->temp_file);
+ g_free (tc->temp_file);
+
+ g_object_unref (tc->transport);
+
+ if (tc->channel)
+ {
+ g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
+ g_object_unref (tc->channel);
+ g_assert (tc->channel == NULL);
+ }
+
+ g_free (tc->channel_problem);
+
+ cockpit_assert_expected ();
+}
+
+static void
+expect_control_message (JsonObject *options,
+ const gchar *command,
+ const gchar *expected_channel,
+ ...) G_GNUC_NULL_TERMINATED;
+
+static void
+expect_control_message (JsonObject *options,
+ const gchar *expected_command,
+ const gchar *expected_channel,
+ ...)
+{
+ const gchar *expect_option;
+ const gchar *expect_value;
+ const gchar *value;
+ JsonNode *node;
+ va_list va;
+
+ g_assert (options != NULL);
+ g_assert_cmpstr (json_object_get_string_member (options, "command"), ==, expected_command);
+ g_assert_cmpstr (json_object_get_string_member (options, "channel"), ==, expected_channel);
+
+ va_start (va, expected_channel);
+ for (;;) {
+ expect_option = va_arg (va, const gchar *);
+ if (!expect_option)
+ break;
+ expect_value = va_arg (va, const gchar *);
+
+ value = NULL;
+ node = json_object_get_member (options, expect_option);
+ if (node && JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) == G_TYPE_STRING)
+ value = json_node_get_string (node);
+
+ g_assert_cmpstr (value, ==, expect_value);
+ }
+ va_end (va);
+}
+
+
+static void
+test_echo (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *payload;
+ GBytes *sent;
+
+ payload = g_bytes_new ("Marmalaade!", 11);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);
+
+ while (mock_transport_count_sent (tc->transport) < 2)
+ g_main_context_iteration (NULL, TRUE);
+
+ sent = mock_transport_pop_channel (tc->transport, "548");
+ cockpit_assert_bytes_eq (sent, "Marmalaade!", 11);
+ g_bytes_unref (payload);
+}
+
+static void
+test_shutdown (TestCase *tc,
+ gconstpointer unused)
+{
+ GError *error = NULL;
+ JsonObject *sent;
+
+ /* Wait until the socket has opened */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /*
+ * Close down the write end of the socket (what
+ * CockpitTextStream is reading from)
+ */
+ g_socket_shutdown (tc->conn_sock, FALSE, TRUE, &error);
+ g_assert_no_error (error);
+
+ while (tc->channel_problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (tc->channel_problem, ==, "");
+ sent = mock_transport_pop_control (tc->transport);
+ expect_control_message (sent, "ready", "548", NULL);
+ sent = mock_transport_pop_control (tc->transport);
+ expect_control_message (sent, "done", "548", NULL);
+
+ sent = mock_transport_pop_control (tc->transport);
+ expect_control_message (sent, "close", "548", "problem", NULL, NULL);
+}
+
+static void
+test_close_normal (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *payload;
+ GBytes *sent;
+ JsonObject *control;
+
+ /* Wait until the socket has opened */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ payload = g_bytes_new ("Marmalaade!", 11);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", payload);
+ cockpit_channel_close (tc->channel, NULL);
+
+ /* Wait until channel closes */
+ while (tc->channel_problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Should have sent payload and control */
+ g_assert_cmpstr (tc->channel_problem, ==, "");
+ sent = mock_transport_pop_channel (tc->transport, "548");
+ g_assert (sent != NULL);
+ g_assert (g_bytes_equal (sent, payload));
+ g_bytes_unref (payload);
+
+ control = mock_transport_pop_control (tc->transport);
+ expect_control_message (control, "ready", "548", NULL);
+ control = mock_transport_pop_control (tc->transport);
+ expect_control_message (control, "done", "548", NULL);
+
+ control = mock_transport_pop_control (tc->transport);
+ expect_control_message (control, "close", "548", "problem", NULL, NULL);
+}
+
+static void
+test_close_problem (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *sent;
+
+ /* Wait until the socket has opened */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ sent = g_bytes_new ("Marmalaade!", 11);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", sent);
+ g_bytes_unref (sent);
+ cockpit_channel_close (tc->channel, "boooyah");
+
+ /* Wait until channel closes */
+ while (tc->channel_problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Should have sent no payload and control */
+ g_assert_cmpstr (tc->channel_problem, ==, "boooyah");
+ g_assert (mock_transport_pop_channel (tc->transport, "548") == NULL);
+ expect_control_message (mock_transport_pop_control (tc->transport), "ready", "548", NULL);
+ expect_control_message (mock_transport_pop_control (tc->transport),
+ "close", "548", "problem", "boooyah", NULL);
+}
+
+static void
+test_spawn_simple (void)
+{
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem = NULL;
+ JsonObject *options;
+ JsonArray *array;
+ GBytes *sent;
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ options = json_object_new ();
+ array = json_array_new ();
+ json_array_add_string_element (array, "/bin/cat");
+ json_object_set_array_member (options, "spawn", array);
+ json_object_set_string_member (options, "payload", "stream");
+
+ channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
+ "options", options,
+ "id", "548",
+ "transport", transport,
+ NULL);
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+ while (g_main_context_iteration (NULL, FALSE));
+ json_object_unref (options);
+
+ sent = g_bytes_new ("Marmalaade!", 11);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
+ cockpit_channel_close (channel, NULL);
+
+ while (mock_transport_count_sent (transport) < 2)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (g_bytes_equal (sent, mock_transport_pop_channel (transport, "548")));
+ g_bytes_unref (sent);
+
+ while (!problem)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "");
+ g_object_unref (channel);
+
+ g_free (problem);
+ g_object_unref (transport);
+}
+
+static void
+test_spawn_environ (void)
+{
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem = NULL;
+ JsonObject *options;
+ JsonArray *array;
+ GString *string;
+ gconstpointer data;
+ gsize len;
+ GBytes *sent;
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ options = json_object_new ();
+
+ array = json_array_new ();
+ json_array_add_string_element (array, "/bin/sh");
+ json_array_add_string_element (array, "-c");
+ json_array_add_string_element (array, "set");
+ json_object_set_array_member (options, "spawn", array);
+
+ array = json_array_new ();
+ json_array_add_string_element (array, "ENVIRON=Marmalaade");
+ json_object_set_array_member (options, "environ", array);
+
+ json_object_set_string_member (options, "payload", "stream");
+
+ channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
+ "options", options,
+ "id", "548",
+ "transport", transport,
+ NULL);
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+ json_object_unref (options);
+
+ string = g_string_new ("");
+ while (!problem)
+ {
+ g_main_context_iteration (NULL, TRUE);
+ sent = mock_transport_pop_channel (transport, "548");
+ if (sent)
+ {
+ data = g_bytes_get_data (sent, &len);
+ g_string_append_len (string, data, len);
+ }
+ }
+
+ g_assert_cmpstr (problem, ==, "");
+ g_free (problem);
+
+ cockpit_assert_strmatch (string->str, "*ENVIRON=*Marmalaade*");
+ g_string_free (string, TRUE);
+
+ g_object_unref (channel);
+ g_object_unref (transport);
+}
+
+static void
+test_spawn_status (void)
+{
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem = NULL;
+ JsonObject *options;
+ JsonArray *array;
+ JsonObject *control;
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ options = json_object_new ();
+
+ array = json_array_new ();
+ json_array_add_string_element (array, "/bin/sh");
+ json_array_add_string_element (array, "-c");
+ json_array_add_string_element (array, "exit 5");
+ json_object_set_array_member (options, "spawn", array);
+
+ json_object_set_string_member (options, "payload", "stream");
+
+ channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
+ "options", options,
+ "id", "548",
+ "transport", transport,
+ NULL);
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+ json_object_unref (options);
+
+ while (!problem)
+ g_main_context_iteration (NULL, TRUE);
+
+ control = mock_transport_pop_control (transport);
+ expect_control_message (control, "ready", "548", NULL);
+ control = mock_transport_pop_control (transport);
+ expect_control_message (control, "done", "548", NULL);
+
+ control = mock_transport_pop_control (transport);
+ expect_control_message (control, "close", "548", "problem", NULL, NULL);
+ g_assert_cmpint (json_object_get_int_member (control, "exit-status"), ==, 5);
+
+ g_free (problem);
+
+ g_object_unref (channel);
+ g_object_unref (transport);
+}
+
+static void
+test_spawn_signal (void)
+{
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem = NULL;
+ JsonObject *options;
+ JsonArray *array;
+ JsonObject *control;
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ options = json_object_new ();
+
+ array = json_array_new ();
+ json_array_add_string_element (array, "/bin/sh");
+ json_array_add_string_element (array, "-c");
+ json_array_add_string_element (array, "kill $$");
+ json_object_set_array_member (options, "spawn", array);
+
+ json_object_set_string_member (options, "payload", "stream");
+
+ channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
+ "options", options,
+ "id", "548",
+ "transport", transport,
+ NULL);
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+ json_object_unref (options);
+
+ while (!problem)
+ g_main_context_iteration (NULL, TRUE);
+
+ control = mock_transport_pop_control (transport);
+ expect_control_message (control, "ready", "548", NULL);
+ control = mock_transport_pop_control (transport);
+ expect_control_message (control, "done", "548", NULL);
+
+ control = mock_transport_pop_control (transport);
+ cockpit_assert_json_eq (control, "{ \"command\": \"close\", \"channel\": \"548\","
+ " \"exit-signal\": \"TERM\"}");
+
+ g_free (problem);
+
+ g_object_unref (channel);
+ g_object_unref (transport);
+}
+
+static void
+test_spawn_pty (void)
+{
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem = NULL;
+ JsonObject *options;
+ JsonArray *array;
+ GBytes *sent;
+ GString *received;
+ gconstpointer data;
+ gsize len;
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ options = json_object_new ();
+ array = json_array_new ();
+ json_array_add_string_element (array, "/bin/bash");
+ json_array_add_string_element (array, "-i");
+ json_object_set_array_member (options, "spawn", array);
+ json_object_set_string_member (options, "payload", "stream");
+ json_object_set_boolean_member (options, "pty", TRUE);
+
+ channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
+ "options", options,
+ "id", "548",
+ "transport", transport,
+ NULL);
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+ json_object_unref (options);
+
+ sent = g_bytes_new ("echo booyah\nexit\n", 17);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
+ g_bytes_unref (sent);
+
+ received = g_string_new ("");
+ while (!problem)
+ {
+ g_main_context_iteration (NULL, TRUE);
+ sent = mock_transport_pop_channel (transport, "548");
+ if (sent)
+ {
+ data = g_bytes_get_data (sent, &len);
+ g_string_append_len (received, data, len);
+ }
+ }
+
+ cockpit_assert_strmatch (received->str, "*booyah*");
+ g_string_free (received, TRUE);
+
+ g_assert_cmpstr (problem, ==, "");
+ g_object_unref (channel);
+
+ g_free (problem);
+ g_object_unref (transport);
+}
+
+static void
+test_spawn_errors (void)
+{
+ MockTransport *transport;
+ g_autofree gchar *problem = NULL;
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ // empty spawn array
+ g_autoptr(JsonObject) options = json_object_new ();
+ JsonArray *array = json_array_new ();
+ json_object_set_array_member (options, "spawn", array);
+ json_object_set_string_member (options, "payload", "stream");
+
+ g_autoptr(CockpitChannel) channel = g_object_new (
+ COCKPIT_TYPE_PIPE_CHANNEL,
+ "options", options,
+ "id", "548",
+ "transport", transport,
+ NULL);
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ while (!problem)
+ g_main_context_iteration (NULL, TRUE);
+
+ JsonObject *control = mock_transport_pop_control (transport);
+ expect_control_message (control, "close", "548", "problem", "protocol-error", NULL);
+
+ g_object_unref (transport);
+}
+
+
+/* Create GBytes with the contents of @s, without terminating \0 */
+static GBytes *
+bytes_from_string (const gchar *s)
+{
+ gsize len;
+
+ len = strlen (s);
+ return g_bytes_new (s, len);
+}
+
+static void
+test_spawn_pty_resize (void)
+{
+ MockTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem = NULL;
+ JsonObject *options;
+ JsonArray *array;
+ JsonObject *window;
+ GBytes *sent;
+ GString *received;
+ gconstpointer data;
+ gsize len;
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ options = json_object_new ();
+ array = json_array_new ();
+ json_array_add_string_element (array, "/bin/bash");
+ json_array_add_string_element (array, "-i");
+ json_object_set_array_member (options, "spawn", array);
+ json_object_set_string_member (options, "payload", "stream");
+ json_object_set_boolean_member (options, "pty", TRUE);
+ window = json_object_new ();
+ json_object_set_int_member (window, "rows", 1234);
+ json_object_set_int_member (window, "cols", 4567);
+ json_object_set_object_member (options, "window", window);
+
+ channel = g_object_new (COCKPIT_TYPE_PIPE_CHANNEL,
+ "options", options,
+ "id", "548",
+ "transport", transport,
+ NULL);
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+ json_object_unref (options);
+
+ /*
+ * Set shell option `checkwinsize`, which tells bash to update $LINES
+ * and $COLUMNS after each command.
+ */
+ sent = bytes_from_string ("echo -e \"\\x7b$(stty size)\\x7d\"\n");
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
+ g_bytes_unref (sent);
+
+ received = g_string_new ("");
+ while (!problem)
+ {
+ g_main_context_iteration (NULL, TRUE);
+ sent = mock_transport_pop_channel (transport, "548");
+ if (sent)
+ {
+ data = g_bytes_get_data (sent, &len);
+ g_string_append_len (received, data, len);
+
+ if (memchr (data, '}', len))
+ break;
+ }
+ }
+
+ cockpit_assert_strmatch (received->str, "*{1234 4567}*");
+
+ options = json_object_new ();
+ window = json_object_new ();
+ json_object_set_int_member (window, "rows", 24);
+ json_object_set_int_member (window, "cols", 42);
+ json_object_set_object_member (options, "window", window);
+
+ /* HACK: https://bugzilla.redhat.com/show_bug.cgi?id=1693179
+ * setting PTY size sometimes gets ignored, so retry a few times */
+ for (int retry = 1;; ++retry)
+ {
+ cockpit_transport_emit_control (COCKPIT_TRANSPORT (transport), "options", "548", options, NULL);
+
+ sent = bytes_from_string ("echo -e \"\\x7b$(stty size)\\x7d\"\n");
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
+ g_bytes_unref (sent);
+
+ g_string_truncate (received, 0);
+ while (!problem)
+ {
+ g_main_context_iteration (NULL, TRUE);
+ sent = mock_transport_pop_channel (transport, "548");
+ if (sent)
+ {
+ data = g_bytes_get_data (sent, &len);
+ g_string_append_len (received, data, len);
+ if (memchr (data, '}', len))
+ break;
+ }
+ }
+
+ if (strstr (received->str, "{24 42}"))
+ break;
+
+ g_message ("setting PTY size failed, retry #%i: %s", retry, received->str);
+
+ if (retry == 5)
+ {
+ g_warning ("repeatedly failed to set terminal size for stream channel: %s", received->str);
+ g_test_fail ();
+ break;
+ }
+ }
+
+ sent = bytes_from_string ("exit\n");
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (transport), "548", sent);
+ g_bytes_unref (sent);
+ while (!problem)
+ {
+ g_main_context_iteration (NULL, TRUE);
+ mock_transport_pop_channel (transport, "548");
+ }
+
+ json_object_unref (options);
+ g_string_free (received, TRUE);
+
+ g_assert_cmpstr (problem, ==, "");
+ g_object_unref (channel);
+
+ g_free (problem);
+ g_object_unref (transport);
+}
+
+static void
+test_send_invalid (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *converted;
+ GBytes *sent;
+
+ sent = g_bytes_new ("Oh \x00Marma\x00laade!", 16);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "548", sent);
+ g_bytes_unref (sent);
+
+ while (mock_transport_count_sent (tc->transport) < 2)
+ g_main_context_iteration (NULL, TRUE);
+
+ converted = g_bytes_new ("Oh \xef\xbf\xbd""Marma""\xef\xbf\xbd""laade!", 20);
+ g_assert (g_bytes_equal (converted, mock_transport_pop_channel (tc->transport, "548")));
+ g_bytes_unref (converted);
+}
+
+static void
+test_recv_invalid (TestCase *tc,
+ gconstpointer unused)
+{
+ GError *error = NULL;
+ GBytes *converted;
+
+ /* Wait until the socket has opened */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (g_socket_send (tc->conn_sock, "\x00Marmalaade!\x00", 13, NULL, &error), ==, 13);
+ g_assert_no_error (error);
+
+ while (mock_transport_count_sent (tc->transport) < 2)
+ g_main_context_iteration (NULL, TRUE);
+
+ converted = g_bytes_new ("\xef\xbf\xbd""Marmalaade!""\xef\xbf\xbd", 17);
+ g_assert (g_bytes_equal (converted, mock_transport_pop_channel (tc->transport, "548")));
+ g_bytes_unref (converted);
+}
+
+
+static gboolean
+add_remainder (gpointer user_data)
+{
+ GSocket *socket = user_data;
+ GError *error = NULL;
+ g_assert_cmpint (g_socket_send (socket, "\x94\x80", 2, NULL, &error), ==, 2);
+ g_assert_no_error (error);
+ return FALSE;
+}
+
+static void
+print_gbytes (GBytes *bytes)
+{
+ gsize i, len;
+ const char* data;
+
+ data = g_bytes_get_data (bytes, &len);
+ g_assert (data);
+ for (i = 0; i < len; ++i)
+ g_printf ("%X", data[i]);
+ puts("");
+}
+
+static void
+test_recv_valid_batched (TestCase *tc,
+ gconstpointer unused)
+{
+ GError *error = NULL;
+ GBytes *converted;
+ GBytes *received;
+
+ /* Wait until the socket has opened */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (g_socket_send (tc->conn_sock, "Marmalaade!\xe2", 12, NULL, &error), ==, 12);
+ g_assert_no_error (error);
+
+ g_timeout_add (100, add_remainder, tc->conn_sock);
+
+ while (mock_transport_count_sent (tc->transport) < 2)
+ g_main_context_iteration (NULL, TRUE);
+
+ converted = g_bytes_new ("Marmalaade!\xe2\x94\x80", 14);
+ received = mock_transport_combine_output (tc->transport, "548", NULL);
+ if (!g_bytes_equal (converted, received))
+ {
+ g_test_fail ();
+ puts ("ERROR: unexpected output\nconverted:");
+ print_gbytes (converted);
+ puts ("received:");
+ print_gbytes (received);
+ }
+ g_bytes_unref (converted);
+ g_bytes_unref (received);
+}
+
+static void
+test_fail_not_found (void)
+{
+ CockpitTransport *transport;
+ CockpitChannel *channel;
+ gchar *problem = NULL;
+
+ cockpit_expect_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE, "*couldn't connect*");
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+ channel = cockpit_pipe_channel_open (transport, "1", "/non-existent");
+ g_assert (channel != NULL);
+
+ /* Even through failure is on open, should not have closed yet */
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "not-found");
+ g_free (problem);
+ g_object_unref (channel);
+ g_object_unref (transport);
+
+ cockpit_assert_expected ();
+}
+
+static void
+test_fail_access_denied (void)
+{
+ CockpitTransport *transport;
+ CockpitChannel *channel;
+ gchar *unix_path;
+ gchar *problem = NULL;
+ gint fd;
+
+ if (geteuid () == 0)
+ {
+ g_test_skip ("running as root");
+ return;
+ }
+
+ cockpit_expect_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE, "*couldn't connect*");
+
+ unix_path = g_strdup ("/tmp/cockpit-test-XXXXXX.sock");
+ fd = g_mkstemp (unix_path);
+ g_assert_cmpint (fd, >=, 0);
+
+ /* Take away all permissions from the file */
+ g_assert_cmpint (fchmod (fd, 0000), ==, 0);
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+ channel = cockpit_pipe_channel_open (transport, "1", unix_path);
+ g_assert (channel != NULL);
+
+ /* Even through failure is on open, should not have closed yet */
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "access-denied");
+ g_free (problem);
+ g_free (unix_path);
+ close (fd);
+ g_object_unref (channel);
+ g_object_unref (transport);
+
+ cockpit_assert_expected ();
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/pipe-channel/echo", TestCase, NULL,
+ setup_channel, test_echo, teardown);
+ g_test_add ("/pipe-channel/shutdown", TestCase, NULL,
+ setup_channel, test_shutdown, teardown);
+ g_test_add ("/pipe-channel/close-normal", TestCase, NULL,
+ setup_channel, test_close_normal, teardown);
+ g_test_add ("/pipe-channel/close-problem", TestCase, NULL,
+ setup_channel, test_close_problem, teardown);
+ g_test_add ("/pipe-channel/invalid-send", TestCase, NULL,
+ setup_channel, test_send_invalid, teardown);
+ g_test_add ("/pipe-channel/invalid-recv", TestCase, NULL,
+ setup_channel, test_recv_invalid, teardown);
+ g_test_add ("/pipe-channel/valid-recv-batched", TestCase, NULL,
+ setup_channel, test_recv_valid_batched, teardown);
+
+ g_test_add_func ("/pipe-channel/spawn/signal", test_spawn_signal);
+ g_test_add_func ("/pipe-channel/spawn/simple", test_spawn_simple);
+ g_test_add_func ("/pipe-channel/spawn/status", test_spawn_status);
+ g_test_add_func ("/pipe-channel/spawn/environ", test_spawn_environ);
+ g_test_add_func ("/pipe-channel/spawn/pty", test_spawn_pty);
+ g_test_add_func ("/pipe-channel/spawn/pty-resize", test_spawn_pty_resize);
+ g_test_add_func ("/pipe-channel/spawn/errors", test_spawn_errors);
+
+ g_test_add_func ("/pipe-channel/fail/not-found", test_fail_not_found);
+ g_test_add_func ("/pipe-channel/fail/access-denied", test_fail_access_denied);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-process.c b/src/bridge/test-process.c
new file mode 100644
index 0000000..71ccb22
--- /dev/null
+++ b/src/bridge/test-process.c
@@ -0,0 +1,154 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbusinternal.h"
+
+#include "testlib/cockpittest.h"
+
+#include <gio/gio.h>
+#include <glib/gstdio.h>
+
+typedef struct {
+ GDBusConnection *connection;
+} TestCase;
+
+static void
+setup (TestCase *tc,
+ gconstpointer unused)
+{
+ cockpit_dbus_internal_startup (FALSE);
+
+ cockpit_dbus_process_startup ();
+ while (g_main_context_iteration (NULL, FALSE));
+
+ tc->connection = cockpit_dbus_internal_client();
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer unused)
+{
+ cockpit_assert_expected ();
+ g_object_unref (tc->connection);
+ cockpit_dbus_internal_cleanup ();
+}
+
+static void
+on_complete_get_result (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **ret = user_data;
+ g_assert (ret != NULL);
+ g_assert (*ret == NULL);
+ *ret = g_object_ref (result);
+}
+
+static GVariant *
+dbus_call_with_main_loop (TestCase *tc,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *reply_type,
+ GError **error)
+{
+ GAsyncResult *result = NULL;
+ GVariant *retval;
+
+ g_dbus_connection_call (tc->connection, NULL, object_path,
+ interface_name, method_name, parameters,
+ reply_type, G_DBUS_CALL_FLAGS_NONE, -1,
+ NULL, on_complete_get_result, &result);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ retval = g_dbus_connection_call_finish (tc->connection, result, error);
+ g_object_unref (result);
+
+ return retval;
+}
+
+static void
+test_get_properties (TestCase *tc,
+ gconstpointer unused)
+{
+ GVariant *retval;
+ GVariant *inner = NULL;
+ GVariant *variable = NULL;
+ gchar **environ = g_listenv ();
+ gint i;
+
+ GError *error = NULL;
+
+ retval = dbus_call_with_main_loop (tc, "/bridge", "org.freedesktop.DBus.Properties", "GetAll",
+ g_variant_new ("(s)", "cockpit.Process"),
+ G_VARIANT_TYPE ("(a{sv})"), &error);
+
+ g_assert_no_error (error);
+
+ inner = g_variant_get_child_value (retval, 0);
+ variable = g_variant_lookup_value (inner, "Environment", G_VARIANT_TYPE ("a{ss}"));
+ for (i = 0; environ[i] != NULL; i++)
+ {
+ gchar *value = NULL;
+ g_assert_true (g_variant_lookup (variable, environ[i], "&s", &value));
+ g_assert_cmpstr (g_getenv (environ[i]), ==, value);
+ }
+ g_variant_unref (variable);
+
+ variable = g_variant_lookup_value (inner, "Pid", G_VARIANT_TYPE ("u"));
+ g_assert (variable != NULL);
+ g_assert_cmpuint (g_variant_get_uint32 (variable), ==, getpid ());
+ g_variant_unref (variable);
+
+ variable = g_variant_lookup_value (inner, "Uid", G_VARIANT_TYPE ("i"));
+ g_assert (variable != NULL);
+ g_assert_cmpuint (g_variant_get_int32 (variable), ==, getuid ());
+ g_variant_unref (variable);
+
+ variable = g_variant_lookup_value (inner, "SessionId", G_VARIANT_TYPE ("s"));
+ g_assert (variable != NULL);
+ /* Not always a valid string during testing */
+ g_variant_unref (variable);
+
+ variable = g_variant_lookup_value (inner, "StartTime", G_VARIANT_TYPE ("t"));
+ g_assert (variable != NULL);
+ g_assert_cmpuint (g_variant_get_uint64 (variable), !=, 0);
+ g_variant_unref (variable);
+
+ g_variant_unref (inner);
+ g_variant_unref (retval);
+ g_strfreev (environ);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/process/get-properties", TestCase, NULL,
+ setup, test_get_properties, teardown);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-router.c b/src/bridge/test-router.c
new file mode 100644
index 0000000..243d839
--- /dev/null
+++ b/src/bridge/test-router.c
@@ -0,0 +1,895 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitrouter.h"
+#include "cockpitdbusinternal.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpitjson.h"
+#include "testlib/cockpittest.h"
+#include "testlib/mock-channel.h"
+#include "testlib/mock-transport.h"
+
+#include <json-glib/json-glib.h>
+
+#include <gio/gio.h>
+
+#include <string.h>
+
+/* Mock override from cockpitrouter.c */
+extern guint cockpit_router_bridge_timeout;
+
+typedef struct {
+ MockTransport *transport;
+ JsonObject *mock_match;
+ JsonObject *mock_config;
+ GDBusConnection *connection;
+} TestCase;
+
+typedef struct {
+ const gchar *payload;
+ gboolean with_env;
+ gboolean privileged;
+ const gchar *problem;
+ const gchar *bridge;
+} TestFixture;
+
+static void
+setup (TestCase *tc,
+ gconstpointer user_data)
+{
+ const TestFixture *fixture = user_data;
+ JsonArray *argv;
+ gchar *argument;
+ const gchar *payload;
+ const gchar *bridge;
+
+ tc->mock_config = json_object_new ();
+ argv = json_array_new ();
+
+ bridge = BUILDDIR "/mock-bridge";
+ if (fixture && fixture->bridge)
+ bridge = fixture->bridge;
+ json_array_add_string_element (argv, bridge);
+
+ payload = "upper";
+ if (fixture && fixture->payload)
+ payload = fixture->payload;
+ argument = g_strdup_printf ("--%s", payload);
+ json_array_add_string_element (argv, argument);
+ g_free (argument);
+ json_object_set_array_member (tc->mock_config, "spawn", argv);
+ if (fixture && fixture->privileged)
+ {
+ json_object_set_boolean_member (tc->mock_config, "privileged", TRUE);
+ }
+ json_object_seal (tc->mock_config);
+
+ tc->mock_match = json_object_new ();
+ json_object_set_string_member (tc->mock_match, "payload", payload);
+
+ tc->transport = g_object_new (mock_transport_get_type (), NULL);
+ while (g_main_context_iteration (NULL, FALSE));
+
+ cockpit_dbus_internal_startup (FALSE);
+ tc->connection = cockpit_dbus_internal_client();
+}
+
+static void
+setup_dynamic (TestCase *tc,
+ gconstpointer user_data)
+{
+ const TestFixture *fixture = user_data;
+ JsonArray *argv;
+ JsonArray *env;
+ JsonObject *match;
+
+ tc->mock_config = json_object_new ();
+ argv = json_array_new ();
+ match = json_object_new ();
+
+ json_array_add_string_element (argv, BUILDDIR "/mock-bridge");
+ json_array_add_string_element (argv, "--${payload}");
+ json_array_add_string_element (argv, "--count");
+
+ json_object_set_array_member (tc->mock_config, "spawn", argv);
+
+ if (fixture && fixture->problem)
+ json_object_set_string_member (tc->mock_config, "problem", fixture->problem);
+
+ if (fixture && fixture->with_env)
+ {
+ env = json_array_new ();
+ json_array_add_string_element (env, "COCKPIT_TEST_PARAM_ENV=${payload}");
+ json_object_set_array_member (tc->mock_config, "environ", env);
+ }
+ json_object_seal (tc->mock_config);
+
+ json_object_set_null_member (match, "payload");
+ json_object_set_object_member (tc->mock_config, "match", match);
+
+ tc->transport = g_object_new (mock_transport_get_type (), NULL);
+ while (g_main_context_iteration (NULL, FALSE));
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer unused)
+{
+ cockpit_assert_expected ();
+
+ g_object_add_weak_pointer (G_OBJECT (tc->transport), (gpointer *)&tc->transport);
+ g_object_unref (tc->transport);
+ g_assert (tc->transport == NULL);
+
+ json_object_unref (tc->mock_config);
+ if (tc->mock_match)
+ json_object_unref (tc->mock_match);
+}
+
+static void
+emit_string (TestCase *tc,
+ const gchar *channel,
+ const gchar *string)
+{
+ GBytes *bytes = g_bytes_new (string, strlen (string));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), channel, bytes);
+ g_bytes_unref (bytes);
+}
+
+static void
+on_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gchar **retval = user_data;
+ g_assert (*retval == NULL);
+ *retval = g_strdup (problem);
+}
+
+static void
+test_local_channel (TestCase *tc,
+ gconstpointer unused)
+{
+ CockpitRouter *router;
+ GBytes *sent;
+
+ static CockpitPayloadType payload_types[] = {
+ { "echo", mock_echo_channel_get_type },
+ { NULL },
+ };
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), payload_types, NULL);
+
+ emit_string (tc, NULL, "{\"command\": \"init\", \"version\": 1, \"host\": \"localhost\" }");
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"echo\"}");
+ emit_string (tc, "a", "oh marmalade");
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "oh marmalade", -1);
+
+ g_object_unref (router);
+}
+
+static void
+test_external_bridge (TestCase *tc,
+ gconstpointer unused)
+{
+ CockpitRouter *router;
+ GBytes *sent;
+ JsonObject *control;
+ CockpitTransport *shim_transport = NULL;
+ gchar *problem = NULL;
+ CockpitPeer *peer;
+
+ /* Same argv as used by mock_shim */
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ peer = cockpit_peer_new (COCKPIT_TRANSPORT (tc->transport), tc->mock_config);
+ cockpit_router_add_peer (router, tc->mock_match, peer);
+
+ emit_string (tc, NULL, "{\"command\": \"init\", \"version\": 1, \"host\": \"localhost\" }");
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"b\", \"payload\": \"upper\"}");
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"c\", \"payload\": \"upper\"}");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_json_eq (control, "{\"command\":\"ready\",\"channel\":\"a\"}");
+ control = NULL;
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_json_eq (control, "{\"command\":\"ready\",\"channel\":\"b\"}");
+ control = NULL;
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_json_eq (control, "{\"command\":\"ready\",\"channel\":\"c\"}");
+ control = NULL;
+
+ emit_string (tc, "a", "oh marmalade a");
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE A", -1);
+ sent = NULL;
+
+ /* Get a ref of the shim transport */
+ shim_transport = cockpit_peer_ensure (peer);
+ g_object_ref (shim_transport);
+ g_signal_connect (shim_transport, "closed", G_CALLBACK (on_transport_closed), &problem);
+
+ emit_string (tc, NULL, "{\"command\": \"close\", \"channel\": \"a\" }");
+ emit_string (tc, "b", "oh marmalade b");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"close\",\"channel\":\"a\"}");
+ control = NULL;
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "b")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE B", -1);
+ g_assert_null (problem);
+ sent = NULL;
+
+ emit_string (tc, NULL, "{\"command\": \"close-later\", \"channel\": \"b\" }");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"close\",\"channel\":\"b\",\"problem\":\"closed\"}");
+ control = NULL;
+
+ g_object_unref (peer);
+ g_object_unref (router);
+ g_object_unref (shim_transport);
+}
+
+static const TestFixture fixture_fail = {
+ .payload = "bad"
+};
+
+static void
+test_external_fail (TestCase *tc,
+ gconstpointer user_data)
+{
+ CockpitRouter *router;
+ JsonObject *received;
+ CockpitPeer *peer;
+
+ g_assert (user_data == &fixture_fail);
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ peer = cockpit_peer_new (COCKPIT_TRANSPORT (tc->transport), tc->mock_config);
+ cockpit_router_add_peer (router, tc->mock_match, peer);
+ g_object_unref (peer);
+
+ emit_string (tc, NULL, "{\"command\": \"init\", \"version\": 1, \"host\": \"localhost\" }");
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"bad\"}");
+ emit_string (tc, "a", "oh marmalade");
+
+ while ((received = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_json_eq (received, "{\"command\": \"close\", \"channel\": \"a\", \"problem\": \"not-supported\"}");
+
+ g_object_unref (router);
+}
+
+static const TestFixture fixture_dyn_fail = {
+ .problem = "bad"
+};
+
+static void
+test_dynamic_fail (TestCase *tc,
+ gconstpointer user_data)
+{
+ CockpitRouter *router;
+ JsonObject *received;
+
+ g_assert (user_data == &fixture_dyn_fail);
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ cockpit_router_add_bridge (router, tc->mock_config);
+
+ emit_string (tc, NULL, "{\"command\": \"init\", \"version\": 1, \"host\": \"localhost\" }");
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"bad\"}");
+ emit_string (tc, "a", "oh marmalade");
+
+ while ((received = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_json_eq (received, "{\"command\": \"close\", \"channel\": \"a\", \"problem\": \"bad\"}");
+
+ g_object_unref (router);
+}
+
+
+static const TestFixture fixture_env = {
+ .with_env = TRUE
+};
+
+static void
+check_ready (JsonObject *control,
+ const gchar *channel,
+ const gchar *payload,
+ gint count,
+ gboolean with_env)
+{
+ g_assert_cmpstr (json_object_get_string_member (control, "channel"), ==, channel);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+ g_assert_cmpint (json_object_get_int_member (control, "count"), ==, count);
+ if (with_env)
+ g_assert_cmpstr (json_object_get_string_member (control, "test-env"), ==, payload);
+}
+
+static void
+test_dynamic_bridge (TestCase *tc,
+ gconstpointer user_data)
+{
+ const TestFixture *fixture = user_data;
+ CockpitRouter *router;
+ GBytes *sent;
+ JsonObject *control;
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ cockpit_router_add_bridge (router, tc->mock_config);
+
+ emit_string (tc, NULL, "{\"command\": \"init\", \"version\": 1, \"host\": \"localhost\" }");
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"b\", \"payload\": \"upper\"}");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ check_ready (control, "a", "upper", 0, fixture ? fixture->with_env : FALSE);
+ control = NULL;
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ check_ready (control, "b", "upper", 1, fixture ? fixture->with_env : FALSE);
+ control = NULL;
+
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"c\", \"payload\": \"lower\"}");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ check_ready (control, "c", "lower", 0, fixture ? fixture->with_env : FALSE);
+ control = NULL;
+
+ emit_string (tc, "a", "oh marmalade a");
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE A", -1);
+ sent = NULL;
+
+ emit_string (tc, NULL, "{\"command\": \"close\", \"channel\": \"a\" }");
+ emit_string (tc, "b", "oh marmalade b");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"close\",\"channel\":\"a\"}");
+ control = NULL;
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "b")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "OH MARMALADE B", -1);
+ sent = NULL;
+
+ emit_string (tc, NULL, "{\"command\": \"close\", \"channel\": \"b\" }");
+ emit_string (tc, "c", "OH MARMALADE C");
+
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"close\",\"channel\":\"b\"}");
+ control = NULL;
+
+ while ((sent = mock_transport_pop_channel (tc->transport, "c")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "oh marmalade c", -1);
+ sent = NULL;
+
+ emit_string (tc, NULL, "{\"command\": \"close-later\", \"channel\": \"c\" }");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"close\",\"channel\":\"c\",\"problem\":\"closed\"}");
+ control = NULL;
+
+ g_object_unref (router);
+}
+
+static const TestFixture fixture_host = {
+ .payload = "host",
+ .bridge = BUILDDIR "/mock-echo"
+};
+
+static void
+check_unchanged_host (TestCase *tc,
+ const gchar *host)
+{
+ JsonObject *control;
+ gchar *msg = g_strdup_printf ("{\"command\": \"open\", \"channel\": \"a%s\", \"payload\": \"host\", \"host\": \"%s\"}", host, host);
+
+ emit_string (tc, NULL, msg);
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, msg);
+ control = NULL;
+ g_free (msg);
+}
+
+static void
+test_host_processing (TestCase *tc,
+ gconstpointer user_data)
+{
+ JsonObject *control;
+ CockpitRouter *router;
+ CockpitPeer *peer;
+
+ g_assert (user_data == &fixture_host);
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ peer = cockpit_peer_new (COCKPIT_TRANSPORT (tc->transport), tc->mock_config);
+ cockpit_router_add_peer (router, tc->mock_match, peer);
+ g_object_unref (peer);
+
+ emit_string (tc, NULL, "{\"command\": \"init\", \"version\": 1, \"host\": \"localhost\" }");
+ check_unchanged_host (tc, "host");
+ check_unchanged_host (tc, "host+");
+ check_unchanged_host (tc, "host+key");
+ check_unchanged_host (tc, "host+key+");
+
+ /* Test localhost is removed */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"host\":\"localhost\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\"}");
+ control = NULL;
+
+ /* Test host-key1 is set to value */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"host\":\"host+key1+value\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"open\",\"channel\":\"a\",\"payload\":\"host\",\"host\":\"host\",\"host-key1\":\"value\"}");
+ control = NULL;
+
+ /* Test with + in value */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"host\":\"host+key1+value+value\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"open\",\"channel\":\"a\",\"payload\":\"host\",\"host\":\"host\",\"host-key1\":\"value+value\"}");
+ control = NULL;
+
+ /* Test localhost is removed but host-key1 present */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"host\":\"localhost+key1+value\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"open\",\"channel\":\"a\",\"payload\":\"host\",\"host-key1\":\"value\"}");
+ control = NULL;
+
+ /* Test doesn't replace host-key1 */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"host\":\"localhost+key1+value\",\"host-key1\":\"extra\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\":\"open\",\"channel\":\"a\",\"payload\":\"host\",\"host\":\"localhost+key1+value\",\"host-key1\":\"extra\"}");
+ control = NULL;
+
+ g_object_unref (router);
+}
+
+static void
+test_sharable_processing (TestCase *tc,
+ gconstpointer user_data)
+{
+ JsonObject *control;
+ CockpitRouter *router;
+ CockpitPeer *peer;
+
+ g_assert (user_data == &fixture_host);
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ peer = cockpit_peer_new (COCKPIT_TRANSPORT (tc->transport), tc->mock_config);
+ cockpit_router_add_peer (router, tc->mock_match, peer);
+ g_object_unref (peer);
+
+ emit_string (tc, NULL, "{\"command\": \"init\", \"version\": 1, \"host\": \"localhost\" }");
+
+ /* Test host-key is private */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"host\":\"localhost\", \"host-key\": \"host-key\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"host-key\": \"host-key\", \"session\": \"private\"}");
+ control = NULL;
+
+ /* Test user is private */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"host\":\"localhost\", \"user\": \"user\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"user\": \"user\", \"session\": \"private\"}");
+ control = NULL;
+
+ /* Test user with temp-session false is not private */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"host\":\"localhost\", \"user\": \"user\", \"temp-session\": false}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"user\": \"user\"}");
+ control = NULL;
+
+ /* Test user with shareable is not touched */
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"host\":\"localhost\", \"user\": \"user\", \"session\": \"other\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"host\", \"user\": \"user\", \"session\": \"other\"}");
+ control = NULL;
+
+ g_object_unref (router);
+}
+
+static GList *
+make_bridge_configs (const gchar *payload, ...)
+{
+ GList *configs = NULL;
+ va_list ap;
+ va_start (ap, payload);
+
+ while (payload)
+ {
+ const gchar *arg = va_arg (ap, const gchar *);
+
+ JsonObject *match = json_object_new ();
+ json_object_set_string_member (match, "payload", payload);
+ JsonArray *spawn = json_array_new ();
+ json_array_add_string_element (spawn, BUILDDIR "/mock-bridge");
+ while (arg)
+ {
+ json_array_add_string_element (spawn, arg);
+ arg = va_arg (ap, const gchar *);
+ }
+
+ JsonObject *config = json_object_new ();
+ json_object_set_object_member (config, "match", match);
+ json_object_set_array_member (config, "spawn", spawn);
+ json_object_seal (config);
+
+ configs = g_list_prepend (configs, config);
+
+ payload = va_arg (ap, const gchar *);
+ }
+
+ va_end (ap);
+ return g_list_reverse (configs);
+}
+
+static void
+free_bridge_configs (GList *configs)
+{
+ g_list_free_full (configs, (GDestroyNotify)json_object_unref);
+}
+
+static void
+test_reload_add (TestCase *tc,
+ gconstpointer user_data)
+{
+ CockpitRouter *router;
+ GList *configs;
+ JsonObject *control;
+ GBytes *sent;
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ emit_string (tc, NULL, "{\"command\": \"init\", \"version\": 1, \"host\": \"localhost\" }");
+
+ // Configure only the "upper" payload
+ configs = make_bridge_configs ("upper", "--upper", NULL,
+ NULL);
+ cockpit_router_set_bridges (router, configs);
+ free_bridge_configs (configs);
+
+ // Open a "upper" channel
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (json_object_get_string_member (control, "channel"), ==, "a");
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+ control = NULL;
+
+ // And check that it works
+ emit_string (tc, "a", "before reload");
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "BEFORE RELOAD", -1);
+ sent = NULL;
+
+ // Try to open a "lower" channel and check that this is rejected
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"b\", \"payload\": \"lower\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (json_object_get_string_member (control, "channel"), ==, "b");
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "close");
+ g_assert_cmpstr (json_object_get_string_member (control, "problem"), ==, "not-supported");
+ control = NULL;
+
+ // Reconfigure and add the "lower" payload
+ configs = make_bridge_configs ("upper", "--upper", NULL,
+ "lower", "--lower", NULL,
+ NULL);
+ cockpit_router_set_bridges (router, configs);
+ free_bridge_configs (configs);
+
+ // Check that the "upper" channel still works
+ emit_string (tc, "a", "after reload");
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "AFTER RELOAD", -1);
+ sent = NULL;
+
+ // Open a "lower" channel
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"c\", \"payload\": \"lower\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (json_object_get_string_member (control, "channel"), ==, "c");
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+ control = NULL;
+
+ // And check that it now works
+ emit_string (tc, "c", "NEW PAYLOAD");
+ while ((sent = mock_transport_pop_channel (tc->transport, "c")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "new payload", -1);
+ sent = NULL;
+
+ g_object_unref (router);
+}
+
+static void
+test_reload_remove (TestCase *tc,
+ gconstpointer user_data)
+{
+ CockpitRouter *router;
+ GList *configs;
+ JsonObject *control;
+ GBytes *sent;
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ emit_string (tc, NULL, "{\"command\": \"init\", \"version\": 1, \"host\": \"localhost\" }");
+
+ // Configure the "upper" payload
+ configs = make_bridge_configs ("upper", "--upper", NULL,
+ NULL);
+ cockpit_router_set_bridges (router, configs);
+ free_bridge_configs (configs);
+
+ // Open a "upper" channel
+ emit_string (tc, NULL, "{\"command\": \"open\", \"channel\": \"a\", \"payload\": \"upper\"}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (json_object_get_string_member (control, "channel"), ==, "a");
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+ control = NULL;
+
+ // And check that it works
+ emit_string (tc, "a", "before reload");
+ while ((sent = mock_transport_pop_channel (tc->transport, "a")) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_bytes_eq (sent, "BEFORE RELOAD", -1);
+ sent = NULL;
+
+ // Reconfigure and remove the "upper" payload
+ configs = make_bridge_configs (NULL);
+ cockpit_router_set_bridges (router, configs);
+ free_bridge_configs (configs);
+
+ // Check that the "upper" channel has been closed
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpstr (json_object_get_string_member (control, "channel"), ==, "a");
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "close");
+ g_assert_cmpstr (json_object_get_string_member (control, "problem"), ==, "terminated");
+ control = NULL;
+
+ g_object_unref (router);
+}
+
+static void
+on_complete_get_result (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **ret = user_data;
+ g_assert (ret != NULL);
+ g_assert (*ret == NULL);
+ *ret = g_object_ref (result);
+}
+
+static GVariant *
+dbus_call_with_main_loop (TestCase *tc,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *reply_type,
+ GError **error)
+{
+ GAsyncResult *result = NULL;
+ GVariant *retval;
+
+ g_dbus_connection_call (tc->connection, NULL, object_path,
+ interface_name, method_name, parameters,
+ reply_type, G_DBUS_CALL_FLAGS_NONE, -1,
+ NULL, on_complete_get_result, &result);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ retval = g_dbus_connection_call_finish (tc->connection, result, error);
+ g_object_unref (result);
+
+ return retval;
+}
+
+static const TestFixture fixture_superuser = {
+ .privileged = TRUE
+};
+
+static void
+test_superuser_none (TestCase *tc,
+ gconstpointer user_data)
+{
+ CockpitRouter *router;
+ JsonObject *control;
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ cockpit_router_dbus_startup (router);
+
+ cockpit_router_add_bridge (router, tc->mock_config);
+ emit_string (tc, NULL, "{'command': 'init', 'version': 1, 'host': 'localhost', 'superuser': false }");
+
+ // Superuser channels should be rejected
+ //
+ emit_string (tc, NULL, "{'command': 'open', 'channel': 'a', 'payload': 'upper', 'superuser': true}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{'command':'close','channel':'a', 'problem':'access-denied'}");
+ control = NULL;
+
+ g_object_unref (router);
+}
+
+static void
+test_superuser_get_all (TestCase *tc,
+ gconstpointer user_data)
+{
+ CockpitRouter *router;
+ GError *error = NULL;
+ GVariant *retval;
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ cockpit_router_dbus_startup (router);
+
+ cockpit_router_add_bridge (router, tc->mock_config);
+ emit_string (tc, NULL, "{'command': 'init', 'version': 1, 'host': 'localhost', 'superuser': false }");
+
+ retval = dbus_call_with_main_loop (tc, "/superuser", "org.freedesktop.DBus.Properties", "GetAll",
+ g_variant_new ("(s)", "cockpit.Superuser"),
+ G_VARIANT_TYPE ("(a{sv})"), &error);
+
+ g_assert_no_error (error);
+ cockpit_assert_gvariant_eq (retval, "({'Bridges': <['mock-bridge']>, 'Current': <'none'>},)");
+
+ g_variant_unref (retval);
+ g_object_unref (router);
+}
+
+static void
+assert_superuser_current (TestCase *tc,
+ const gchar *expected)
+{
+ GVariant *retval;
+ GError *error = NULL;
+ gchar *expected_variant;
+
+ retval = dbus_call_with_main_loop (tc, "/superuser", "org.freedesktop.DBus.Properties", "Get",
+ g_variant_new ("(ss)", "cockpit.Superuser", "Current"),
+ G_VARIANT_TYPE ("(v)"), &error);
+ g_assert_no_error (error);
+ expected_variant = g_strdup_printf ("(<'%s'>,)", expected);
+ cockpit_assert_gvariant_eq (retval, expected_variant);
+ g_free (expected_variant);
+ g_variant_unref (retval);
+}
+
+static void
+test_superuser_start (TestCase *tc,
+ gconstpointer user_data)
+{
+ CockpitRouter *router;
+ GError *error = NULL;
+ JsonObject *control;
+ GVariant *retval;
+
+ router = cockpit_router_new (COCKPIT_TRANSPORT (tc->transport), NULL, NULL);
+ cockpit_router_dbus_startup (router);
+
+ cockpit_router_add_bridge (router, tc->mock_config);
+ emit_string (tc, NULL, "{'command': 'init', 'version': 1, 'host': 'localhost', 'superuser': false }");
+
+ assert_superuser_current (tc, "none");
+
+ retval = dbus_call_with_main_loop (tc, "/superuser", "cockpit.Superuser", "Start",
+ g_variant_new ("(s)", "mock-bridge"),
+ G_VARIANT_TYPE ("()"), &error);
+ g_assert_no_error (error);
+
+ assert_superuser_current (tc, "mock-bridge");
+
+ // Superuser channels should now work
+ //
+ emit_string (tc, NULL, "{'command': 'open', 'channel': 'a', 'payload': 'upper', 'superuser': true}");
+ while ((control = mock_transport_pop_control (tc->transport)) == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_assert_json_eq (control, "{'command':'ready','channel':'a'}");
+ control = NULL;
+
+ g_variant_unref (retval);
+ g_object_unref (router);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/router/local-channel", TestCase, NULL,
+ setup, test_local_channel, teardown);
+ g_test_add ("/router/external-bridge", TestCase, NULL,
+ setup, test_external_bridge, teardown);
+ g_test_add ("/router/external-fail", TestCase, &fixture_fail,
+ setup, test_external_fail, teardown);
+ g_test_add ("/router/dynamic-bridge-fail", TestCase, &fixture_dyn_fail,
+ setup_dynamic, test_dynamic_fail, teardown);
+ g_test_add ("/router/dynamic-bridge", TestCase, NULL,
+ setup_dynamic, test_dynamic_bridge, teardown);
+ g_test_add ("/router/dynamic-bridge-env", TestCase, &fixture_env,
+ setup_dynamic, test_dynamic_bridge, teardown);
+ g_test_add ("/router/host-processing", TestCase, &fixture_host,
+ setup, test_host_processing, teardown);
+ g_test_add ("/router/sharable-processing", TestCase, &fixture_host,
+ setup, test_sharable_processing, teardown);
+
+ g_test_add ("/router/reload/add", TestCase, NULL,
+ setup, test_reload_add, teardown);
+ g_test_add ("/router/reload/remove", TestCase, NULL,
+ setup, test_reload_remove, teardown);
+
+ g_test_add ("/router/superuser/none", TestCase, &fixture_superuser,
+ setup, test_superuser_none, teardown);
+ g_test_add ("/router/superuser/get-all", TestCase, &fixture_superuser,
+ setup, test_superuser_get_all, teardown);
+ g_test_add ("/router/superuser/start", TestCase, &fixture_superuser,
+ setup, test_superuser_start, teardown);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-rules.c b/src/bridge/test-rules.c
new file mode 100644
index 0000000..55e248c
--- /dev/null
+++ b/src/bridge/test-rules.c
@@ -0,0 +1,278 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitdbusrules.h"
+
+#include "testlib/cockpittest.h"
+
+#include <string.h>
+
+typedef struct {
+ CockpitDBusRules *rules;
+} TestCase;
+
+typedef struct {
+ const gchar *path;
+ gboolean is_namespace;
+ const gchar *interface;
+ const gchar *member;
+ const gchar *arg0;
+} TestRule;
+
+static const TestRule default_rules[] = {
+ { "/otree", TRUE, NULL, NULL, NULL },
+ { "/scruffy/the/janitor", FALSE, NULL, "Marmalade", NULL },
+ { "/planetexpress", TRUE, "org.PlanetExpress.Interface", NULL, NULL },
+ { "/arg", FALSE, NULL, NULL, "Durn" },
+ { NULL, }
+};
+
+static const TestRule empty_rules[] = {
+ { NULL, }
+};
+
+static const TestRule path_rules[] = {
+ { "/otree", TRUE, NULL, NULL, NULL },
+ { "/scruffy/the/janitor", FALSE, NULL, NULL, NULL },
+ { "/planetexpress", TRUE, NULL, NULL, NULL },
+ { "/arg", FALSE, NULL, NULL, NULL },
+ { NULL, }
+};
+
+static void
+setup (TestCase *test,
+ gconstpointer data)
+{
+ const TestRule *rules = data;
+ gint i;
+
+ if (!rules)
+ rules = default_rules;
+
+ test->rules = cockpit_dbus_rules_new ();
+ for (i = 0; rules[i].path != NULL; i++)
+ {
+ cockpit_dbus_rules_add (test->rules, rules[i].path, rules[i].is_namespace,
+ rules[i].interface, rules[i].member, rules[i].arg0);
+ }
+}
+
+static void
+teardown (TestCase *test,
+ gconstpointer data)
+{
+ cockpit_assert_expected ();
+
+ cockpit_dbus_rules_free (test->rules);
+}
+
+static void
+test_basics (TestCase *test,
+ gconstpointer fixture)
+{
+ /* Should all match, only based on path */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", "org.Interface", NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", "org.Interface", "Signal", NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", "org.Interface", NULL, "arg") == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", NULL, "Signal", "arg") == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", "org.Interface", "Signal", "arg") == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/bark", "org.Interface", "Signal", "arg") == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree", "org.Interface", "Signal", "arg") == TRUE);
+
+ /* Mismatched path */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", NULL, NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", "org.Interface", NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", "org.Interface", "Signal", NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", "org.Interface", NULL, "arg") == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", NULL, "Signal", "arg") == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", "org.Interface", "Signal", "arg") == FALSE);
+
+ /* Interfaces affect matching */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/planetexpress", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/planetexpress", "org.PlanetExpress.Interface", NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/planetexpress", "other.Interface", NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/planetexpress/sub", "org.PlanetExpress.Interface", NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/planetexpress/sub", "other.Interface", NULL, NULL) == FALSE);
+
+ /* Members affect matching */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/scruffy/the/janitor", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/scruffy/the/janitor", NULL, "Marmalade", NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/scruffy/the/janitor", NULL, "Other", NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/scruffy/the/janitor/sub", NULL, "Marmalade", NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/scruffy/the/janitor/sub", NULL, "Other", NULL) == FALSE);
+
+ /* Args affect matching */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/arg", NULL, NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/arg", NULL, NULL, "Durn") == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/arg", NULL, NULL, "other") == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/arg/sub", NULL, NULL, "Durn") == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/arg/sub", NULL, NULL, "other") == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/arg/sub", NULL, NULL, NULL) == FALSE);
+}
+
+static void
+test_nothing (TestCase *test,
+ gconstpointer fixture)
+{
+ /* No rules should never match anything */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/", NULL, NULL, NULL) == FALSE);
+
+ g_assert (cockpit_dbus_rules_remove (test->rules, NULL, FALSE, NULL, NULL, NULL) == FALSE);
+}
+
+static void
+test_path_only (TestCase *test,
+ gconstpointer fixture)
+{
+ /* Should all match, only based on path */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", "org.Interface", NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", "org.Interface", "Signal", NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", "org.Interface", NULL, "arg") == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", NULL, "Signal", "arg") == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", "org.Interface", "Signal", "arg") == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/bark", "org.Interface", "Signal", "arg") == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree", "org.Interface", "Signal", "arg") == TRUE);
+
+ /* Mismatched path */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", NULL, NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", "org.Interface", NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", "org.Interface", "Signal", NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", "org.Interface", NULL, "arg") == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", NULL, "Signal", "arg") == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/not", "org.Interface", "Signal", "arg") == FALSE);
+}
+
+static void
+test_all_paths (TestCase *test,
+ gconstpointer fixture)
+{
+ cockpit_dbus_rules_add (test->rules, "/", TRUE, NULL, NULL, NULL);
+
+ /* Should all match, only based on path */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/boring", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/tettot", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/aoenut", NULL, NULL, NULL) == TRUE);
+}
+
+static void
+test_null_path (TestCase *test,
+ gconstpointer fixture)
+{
+ /* Adds a global empty rule which should match everything */
+ cockpit_dbus_rules_add (test->rules, NULL, FALSE, NULL, NULL, NULL);
+
+ /* Should all match, only based on path */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/boring", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/tettot", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/aoenut", NULL, NULL, NULL) == TRUE);
+
+ cockpit_dbus_rules_remove (test->rules, NULL, FALSE, NULL, NULL, NULL);
+
+ g_assert (cockpit_dbus_rules_match (test->rules, "/otree/blah", NULL, NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/boring", NULL, NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/tettot", NULL, NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/aoenut", NULL, NULL, NULL) == FALSE);
+}
+
+static void
+test_root_only (TestCase *test,
+ gconstpointer fixture)
+{
+ /* This should only match the root path */
+ cockpit_dbus_rules_add (test->rules, "/", FALSE, NULL, NULL, NULL);
+
+ /* Should all match, only based on path */
+ g_assert (cockpit_dbus_rules_match (test->rules, "/", NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/boring", NULL, NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/tettot", NULL, NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_match (test->rules, "/aoenut", NULL, NULL, NULL) == FALSE);
+}
+
+static void
+test_add_ref_remove (TestCase *test,
+ gconstpointer fixture)
+{
+ const TestRule *rules = default_rules;
+ gint i;
+
+ /* Add all the rules once */
+ for (i = 0; rules[i].path != NULL; i++)
+ {
+ g_assert (cockpit_dbus_rules_add (test->rules, rules[i].path, rules[i].is_namespace,
+ rules[i].interface, rules[i].member, rules[i].arg0) == TRUE);
+ }
+
+ /* Add them again, should always return FALSE here */
+ for (i = 0; rules[i].path != NULL; i++)
+ {
+ g_assert (cockpit_dbus_rules_add (test->rules, rules[i].path, rules[i].is_namespace,
+ rules[i].interface, rules[i].member, rules[i].arg0) == FALSE);
+ }
+
+ /* Add another rule */
+ g_assert (cockpit_dbus_rules_remove (test->rules, "/booo", FALSE, NULL, NULL, NULL) == FALSE);
+ g_assert (cockpit_dbus_rules_add (test->rules, "/booo", FALSE, NULL, NULL, NULL) == TRUE);
+
+ /* Now remove them, the first time shouldn't actually remove */
+ for (i = 0; rules[i].path != NULL; i++)
+ {
+ g_assert (cockpit_dbus_rules_remove (test->rules, rules[i].path, rules[i].is_namespace,
+ rules[i].interface, rules[i].member, rules[i].arg0) == FALSE);
+ }
+
+ /* The second time actually removes */
+ for (i = 0; rules[i].path != NULL; i++)
+ {
+ g_assert (cockpit_dbus_rules_remove (test->rules, rules[i].path, rules[i].is_namespace,
+ rules[i].interface, rules[i].member, rules[i].arg0) == TRUE);
+ }
+
+ g_assert (cockpit_dbus_rules_remove (test->rules, "/booo", FALSE, NULL, NULL, NULL) == TRUE);
+ g_assert (cockpit_dbus_rules_remove (test->rules, "/booo", FALSE, NULL, NULL, NULL) == FALSE);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/rules/basics", TestCase, NULL,
+ setup, test_basics, teardown);
+ g_test_add ("/rules/nothing", TestCase, empty_rules,
+ setup, test_nothing, teardown);
+ g_test_add ("/rules/path-only", TestCase, path_rules,
+ setup, test_path_only, teardown);
+ g_test_add ("/rules/all-paths", TestCase, empty_rules,
+ setup, test_all_paths, teardown);
+ g_test_add ("/rules/root-only", TestCase, empty_rules,
+ setup, test_root_only, teardown);
+ g_test_add ("/rules/null-path", TestCase, empty_rules,
+ setup, test_null_path, teardown);
+ g_test_add ("/rules/add-ref-remove", TestCase, empty_rules,
+ setup, test_add_ref_remove, teardown);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-stream.c b/src/bridge/test-stream.c
new file mode 100644
index 0000000..227d729
--- /dev/null
+++ b/src/bridge/test-stream.c
@@ -0,0 +1,1070 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitstream.h"
+
+#include "common/cockpitloopback.h"
+#include "testlib/cockpittest.h"
+#include "testlib/mock-pressure.h"
+
+#include <glib.h>
+#include <glib-unix.h>
+#include <glib/gstdio.h>
+#include <gio/gunixsocketaddress.h>
+#include <gio/gunixinputstream.h>
+#include <gio/gunixoutputstream.h>
+
+#include <sys/uio.h>
+#include <string.h>
+
+/* ----------------------------------------------------------------------------
+ * Mock
+ */
+
+static GType mock_echo_stream_get_type (void) G_GNUC_CONST;
+
+typedef struct {
+ CockpitStream parent;
+ GByteArray *received;
+ gboolean closed;
+ gchar *problem;
+} MockEchoStream;
+
+typedef CockpitStreamClass MockEchoStreamClass;
+
+G_DEFINE_TYPE (MockEchoStream, mock_echo_stream, COCKPIT_TYPE_STREAM);
+
+static void
+mock_echo_stream_read (CockpitStream *stream,
+ GByteArray *buffer,
+ gboolean end_of_data)
+{
+ MockEchoStream *self = (MockEchoStream *)stream;
+ g_byte_array_append (self->received, buffer->data, buffer->len);
+ g_byte_array_set_size (buffer, 0);
+}
+
+static void
+mock_echo_stream_close (CockpitStream *stream,
+ const gchar *problem)
+{
+ MockEchoStream *self = (MockEchoStream *)stream;
+ g_assert (!self->closed);
+ self->closed = TRUE;
+ self->problem = g_strdup (problem);
+}
+
+static void
+mock_echo_stream_init (MockEchoStream *self)
+{
+ self->received = g_byte_array_new ();
+}
+
+static void
+mock_echo_stream_finalize (GObject *object)
+{
+ MockEchoStream *self = (MockEchoStream *)object;
+
+ g_byte_array_free (self->received, TRUE);
+ g_free (self->problem);
+
+ G_OBJECT_CLASS (mock_echo_stream_parent_class)->finalize (object);
+}
+
+static void
+mock_echo_stream_class_init (MockEchoStreamClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ CockpitStreamClass *stream_class = COCKPIT_STREAM_CLASS (klass);
+
+ object_class->finalize = mock_echo_stream_finalize;
+
+ stream_class->read = mock_echo_stream_read;
+ stream_class->close = mock_echo_stream_close;
+}
+
+/* ----------------------------------------------------------------------------
+ * Testing
+ */
+
+typedef struct {
+ CockpitStream *stream;
+ guint timeout;
+} TestCase;
+
+typedef struct {
+ const gchar *stream_type_name;
+ gboolean no_timeout;
+} TestFixture;
+
+static gboolean
+on_timeout_abort (gpointer unused)
+{
+ g_error ("timed out");
+ return FALSE;
+}
+
+static void
+setup_timeout (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ if (!fixture || !fixture->no_timeout)
+ tc->timeout = g_timeout_add_seconds (30, on_timeout_abort, tc);
+}
+
+static GIOStream *
+simple_io_stream_for_fds (int in_fd,
+ int out_fd)
+{
+ GInputStream *is;
+ GOutputStream *os;
+ GIOStream *io;
+
+ g_assert (g_unix_set_fd_nonblocking (in_fd, TRUE, NULL));
+ g_assert (g_unix_set_fd_nonblocking (out_fd, TRUE, NULL));
+
+ is = g_unix_input_stream_new (in_fd, TRUE);
+ os = g_unix_output_stream_new (out_fd, TRUE);
+
+ io = g_simple_io_stream_new (is, os);
+
+ g_object_unref (is);
+ g_object_unref (os);
+
+ return io;
+}
+
+static void
+setup_simple (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ const gchar *stream_type;
+ GIOStream *io;
+ int fds[2];
+
+ setup_timeout (tc, data);
+
+ stream_type = "MockEchoStream";
+ if (fixture && fixture->stream_type_name)
+ stream_type = fixture->stream_type_name;
+
+ if (pipe (fds) < 0)
+ g_assert_not_reached ();
+
+ io = simple_io_stream_for_fds (fds[0], fds[1]);
+
+ tc->stream = g_object_new (g_type_from_name (stream_type),
+ "name", "test",
+ "io-stream", io,
+ NULL);
+
+ g_object_unref (io);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ if (tc->stream)
+ {
+ g_object_add_weak_pointer (G_OBJECT (tc->stream),
+ (gpointer *)&tc->stream);
+ g_object_unref (tc->stream);
+
+ /* If this asserts, outstanding references to transport */
+ g_assert (tc->stream == NULL);
+ }
+
+ if (tc->timeout)
+ g_source_remove (tc->timeout);
+}
+
+static gboolean
+on_timeout_set_flag (gpointer user_data)
+{
+ gboolean *data = user_data;
+ g_assert (user_data);
+ g_assert (*data == FALSE);
+ *data = TRUE;
+ return FALSE;
+}
+
+static void
+test_echo_and_close (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoStream *echo_stream = (MockEchoStream *)tc->stream;
+ GBytes *sent, *bytes;
+
+ sent = g_bytes_new_static ("the message", 11);
+ cockpit_stream_write (tc->stream, sent);
+
+ while (echo_stream->received->len < 11)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_byte_array_ref (echo_stream->received);
+ bytes = g_byte_array_free_to_bytes (echo_stream->received);
+ g_assert (g_bytes_equal (bytes, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (bytes);
+
+ cockpit_stream_close (tc->stream, NULL);
+
+ while (!echo_stream->closed)
+ g_main_context_iteration (NULL, TRUE);
+}
+
+static void
+test_echo_queue (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoStream *echo_stream = (MockEchoStream *)tc->stream;
+ GBytes *sent;
+
+ sent = g_bytes_new_static ("one", 3);
+ cockpit_stream_write (tc->stream, sent);
+ g_bytes_unref (sent);
+ sent = g_bytes_new_static ("two", 3);
+ cockpit_stream_write (tc->stream, sent);
+ g_bytes_unref (sent);
+
+ /* Only closes after above are sent */
+ cockpit_stream_close (tc->stream, NULL);
+
+ while (!echo_stream->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (echo_stream->received->len, ==, 6);
+ g_assert (memcmp (echo_stream->received->data, "onetwo", 6) == 0);
+}
+
+static const TestFixture fixture_no_timeout = {
+ .no_timeout = TRUE
+};
+
+static void
+test_echo_large (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoStream *echo_stream = (MockEchoStream *)tc->stream;
+ GBytes *sent;
+
+ /* Medium length */
+ sent = g_bytes_new_take (g_strnfill (1020, '!'), 1020);
+ cockpit_stream_write (tc->stream, sent);
+ while (echo_stream->received->len < g_bytes_get_size (sent))
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (echo_stream->received->len, ==, g_bytes_get_size (sent));
+ g_assert (memcmp (echo_stream->received->data, g_bytes_get_data (sent, NULL), g_bytes_get_size (sent)) == 0);
+ g_bytes_unref (sent);
+
+ g_byte_array_set_size (echo_stream->received, 0);
+
+ /* Extra large */
+ sent = g_bytes_new_take (g_strnfill (10 * 1000 * 1000, '?'), 10 * 1000 * 1000);
+ cockpit_stream_write (tc->stream, sent);
+ while (echo_stream->received->len < g_bytes_get_size (sent))
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (echo_stream->received->len, ==, g_bytes_get_size (sent));
+ g_assert (memcmp (echo_stream->received->data, g_bytes_get_data (sent, NULL), g_bytes_get_size (sent)) == 0);
+ g_bytes_unref (sent);
+
+ g_byte_array_set_size (echo_stream->received, 0);
+
+ /* Double check that didn't csrew things up */
+ sent = g_bytes_new_static ("yello", 5);
+ cockpit_stream_write (tc->stream, sent);
+ while (echo_stream->received->len < g_bytes_get_size (sent))
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (echo_stream->received->len, ==, g_bytes_get_size (sent));
+ g_assert (memcmp (echo_stream->received->data, g_bytes_get_data (sent, NULL), g_bytes_get_size (sent)) == 0);
+ g_bytes_unref (sent);
+}
+
+static void
+test_close_problem (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoStream *echo_stream = (MockEchoStream *)tc->stream;
+
+ cockpit_stream_close (tc->stream, "right now");
+
+ while (!echo_stream->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (echo_stream->problem, ==, "right now");
+}
+
+static const TestFixture fixture_buffer = {
+ .stream_type_name = "CockpitStream"
+};
+
+static void
+test_buffer (TestCase *tc,
+ gconstpointer data)
+{
+ GByteArray *buffer;
+ GBytes *sent;
+
+ buffer = cockpit_stream_get_buffer (tc->stream);
+ g_assert (buffer != NULL);
+ g_assert_cmpuint (buffer->len, ==, 0);
+
+ /* Including null terminator */
+ sent = g_bytes_new_static ("blahdeedoo", 11);
+ cockpit_stream_write (tc->stream, sent);
+ g_bytes_unref (sent);
+
+ while (buffer->len == 0)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (buffer->len, ==, 11);
+ g_assert_cmpstr ((gchar *)buffer->data, ==, "blahdeedoo");
+}
+
+static void
+test_skip_zero (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoStream *echo_stream = (MockEchoStream *)tc->stream;
+ GBytes *sent;
+ GBytes *zero;
+
+ /* Including null terminator */
+ sent = g_bytes_new_static ("blah", 4);
+ zero = g_bytes_new_static ("", 0);
+ cockpit_stream_write (tc->stream, sent);
+ cockpit_stream_write (tc->stream, zero);
+ cockpit_stream_write (tc->stream, sent);
+ g_bytes_unref (zero);
+ g_bytes_unref (sent);
+
+ while (echo_stream->received->len < 8)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (echo_stream->received->len, ==, 8);
+ g_byte_array_append (echo_stream->received, (guint8 *)"", 1);
+ g_assert_cmpstr ((gchar *)echo_stream->received->data, ==, "blahblah");
+}
+
+static void
+on_pressure_set_throttle (CockpitStream *stream,
+ gboolean throttle,
+ gpointer user_data)
+{
+ gint *data = user_data;
+ g_assert (user_data != NULL);
+ *data = throttle ? 1 : 0;
+}
+
+static void
+test_pressure_queue (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoStream *echo_stream = (MockEchoStream *)tc->stream;
+ gint throttle = -1;
+ GBytes *sent;
+ gint i;
+
+ g_signal_connect (tc->stream, "pressure", G_CALLBACK (on_pressure_set_throttle), &throttle);
+ sent = g_bytes_new_take (g_strnfill (10 * 1000, '?'), 10 * 1000);
+
+ /* Sent this a thousand times */
+ for (i = 0; i < 1000; i++)
+ cockpit_stream_write (tc->stream, sent);
+
+ g_bytes_unref (sent);
+
+ /*
+ * This should have put way too much in the queue, and thus
+ * emitted the back-pressure signal. This signal would normally
+ * be used by others to slow down their queueing, but in this
+ * case we just check that it was fired.
+ */
+ g_assert_cmpint (throttle, ==, 1);
+ throttle = -1;
+
+ /*
+ * Now the queue is getting drained. At some point, it will be
+ * signaled that back pressure has been turned off
+ */
+ while (throttle == -1)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (throttle, ==, 0);
+ g_assert_cmpint (echo_stream->received->len, >, 10 * 1000);
+}
+
+static void
+test_pressure_throttle (TestCase *tc,
+ gconstpointer data)
+{
+ CockpitFlow *pressure = mock_pressure_new ();
+ MockEchoStream *echo_stream = (MockEchoStream *)tc->stream;
+ gboolean timeout = FALSE;
+ gsize received;
+ GBytes *sent;
+ gint i;
+
+ cockpit_flow_throttle (COCKPIT_FLOW (tc->stream), pressure);
+ sent = g_bytes_new_take (g_strnfill (1024, '?'), 1024);
+
+ /* Send this a 2048 (2MB total) times */
+ for (i = 0; i < 2048; i++)
+ cockpit_stream_write (tc->stream, sent);
+
+ g_bytes_unref (sent);
+
+ /*
+ * So we should start receiving the echoed data. But we apply
+ * the throttle pressure after receiving some data, and the rest
+ * just waits.
+ */
+ while (echo_stream->received->len == 0)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_flow_emit_pressure (pressure, TRUE);
+
+ received = echo_stream->received->len;
+ g_assert_cmpint (received, <, 2048 * 1024);
+
+ /* Now remaining data input should wait, no further data received*/
+ g_timeout_add_seconds (2, on_timeout_set_flag, &timeout);
+ while (timeout == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (received, ==, echo_stream->received->len);
+
+ /* Remove the pressure, and we should get more data */
+ cockpit_flow_emit_pressure (pressure, FALSE);
+ while (received < echo_stream->received->len)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Clearing the throttle should work too. This pressure signal has no effect */
+ cockpit_flow_throttle (COCKPIT_FLOW (tc->stream), NULL);
+ cockpit_flow_emit_pressure (pressure, TRUE);
+
+ /* Now wait for the remaining data */
+ while (echo_stream->received->len < 2048 * 1024)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (pressure);
+}
+
+static void
+test_read_error (void)
+{
+ MockEchoStream *echo_stream;
+ GIOStream *io;
+ int fds[2];
+ int out;
+
+ /* Just used so we have a valid fd */
+ if (pipe (fds) < 0)
+ g_assert_not_reached ();
+
+ out = dup (fds[1]); /* write end */
+ g_assert (out >= 0);
+
+ cockpit_expect_message ("*Bad file descriptor");
+
+ /* Using wrong end of the pipe */
+ io = simple_io_stream_for_fds (fds[1], out);
+
+ echo_stream = g_object_new (mock_echo_stream_get_type (),
+ "name", "read-error",
+ "io-stream", io,
+ NULL);
+
+ /* Close the file descriptor to cause error */
+ close (fds[1]);
+
+ g_object_unref (io);
+
+ while (!echo_stream->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_expected ();
+
+ g_assert_cmpstr (echo_stream->problem, ==, "internal-error");
+
+ close (fds[0]);
+
+ g_object_unref (echo_stream);
+
+ cockpit_assert_expected ();
+}
+
+static void
+test_write_error (void)
+{
+ MockEchoStream *echo_stream;
+ GIOStream *io;
+ GBytes *sent;
+ int fds[2];
+
+ /* Just used so we have a valid fd */
+ if (pipe (fds) < 0)
+ g_assert_not_reached ();
+
+ cockpit_expect_message ("*Bad file descriptor");
+
+ io = simple_io_stream_for_fds (fds[0], fds[1]);
+
+ /* Pass in a bad write descriptor */
+ echo_stream = g_object_new (mock_echo_stream_get_type (),
+ "name", "write-error",
+ "io-stream", io,
+ NULL);
+
+ /* Close the file descriptor to cause error */
+ close (fds[1]);
+
+ g_object_unref (io);
+
+ sent = g_bytes_new ("test", 4);
+ cockpit_stream_write (COCKPIT_STREAM (echo_stream), sent);
+ g_bytes_unref (sent);
+
+ while (!echo_stream->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_expected ();
+
+ g_assert_cmpstr (echo_stream->problem, ==, "internal-error");
+
+ g_object_unref (echo_stream);
+
+ cockpit_assert_expected ();
+}
+
+static void
+test_read_combined (void)
+{
+ MockEchoStream *echo_stream;
+ struct iovec iov[4];
+ GIOStream *io;
+ gint fds_a[2];
+ gint fds_b[2];
+ gint ret;
+
+ if (pipe(fds_a) < 0)
+ g_assert_not_reached ();
+ if (pipe(fds_b) < 0)
+ g_assert_not_reached ();
+
+ io = simple_io_stream_for_fds (fds_a[0], fds_b[1]);
+
+ /* Pass in a read end of the pipe */
+ echo_stream = g_object_new (mock_echo_stream_get_type (),
+ "name", "read-combined",
+ "io-stream", io,
+ NULL);
+
+ g_object_unref (io);
+
+ /* Write two messages to the stream at once */
+ iov[0].iov_base = "one";
+ iov[0].iov_len = 3;
+ iov[1].iov_base = "two";
+ iov[1].iov_len = 3;
+ iov[2].iov_base = "three";
+ iov[2].iov_len = 5;
+ iov[3].iov_base = "\0";
+ iov[3].iov_len = 1;
+ do
+ {
+ ret = writev (fds_a[1], iov, 4);
+ if (ret < 0 && (errno == EAGAIN || errno == EINTR))
+ continue;
+ if (ret < 0)
+ g_message ("writev failed with %d: %s", ret, g_strerror (errno));
+ g_assert_cmpint (ret, ==, 12);
+ break;
+ }
+ while (TRUE);
+
+ while (echo_stream->received->len < 12)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (echo_stream->received->len, ==, 12);
+ g_assert_cmpstr ((gchar *)echo_stream->received->data, ==, "onetwothree");
+
+ g_object_add_weak_pointer (G_OBJECT (echo_stream),
+ (gpointer *)&echo_stream);
+ g_object_unref (echo_stream);
+ g_assert (echo_stream == NULL);
+
+ close (fds_a[1]);
+ close (fds_b[0]);
+}
+
+static void
+test_properties (void)
+{
+ CockpitStream *tstream;
+ GIOStream *io;
+ gchar *name;
+ GIOStream *x;
+ int fds[2];
+
+ if (pipe(fds) < 0)
+ g_assert_not_reached ();
+
+ io = simple_io_stream_for_fds (fds[0], fds[1]);
+
+ tstream = g_object_new (mock_echo_stream_get_type (),
+ "name", "testo",
+ "io-stream", io,
+ NULL);
+
+ g_object_get (tstream, "name", &name, "io-stream", &x, NULL);
+ g_assert_cmpstr (name, ==, "testo");
+ g_free (name);
+ g_assert (io == x);
+
+ g_object_unref (x);
+ g_object_unref (io);
+
+ g_object_unref (tstream);
+}
+
+static void
+on_close_get_problem (CockpitStream *stream,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gchar **retval = user_data;
+ g_assert (retval != NULL && *retval == NULL);
+ *retval = g_strdup (problem ? problem : "");
+}
+
+typedef struct {
+ GSocket *listen_sock;
+ GSource *listen_source;
+ GSocket *conn_sock;
+ GSource *conn_source;
+ GSocketAddress *address;
+ gboolean skip_ipv6_loopback;
+ guint16 port;
+} TestConnect;
+
+static gboolean
+on_socket_input (GSocket *socket,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ gchar buffer[1024];
+ GError *error = NULL;
+ gssize ret, wret;
+
+ ret = g_socket_receive (socket, buffer, sizeof (buffer), NULL, &error);
+ g_assert_no_error (error);
+
+ if (ret == 0)
+ {
+ g_socket_shutdown (socket, FALSE, TRUE, &error);
+ g_assert_no_error (error);
+ return FALSE;
+ }
+
+ g_assert (ret > 0);
+ wret = g_socket_send (socket, buffer, ret, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (wret == ret);
+ return TRUE;
+}
+
+static gboolean
+on_socket_connection (GSocket *socket,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ TestConnect *tc = user_data;
+ GError *error = NULL;
+
+ g_assert (tc->conn_source == NULL);
+ tc->conn_sock = g_socket_accept (tc->listen_sock, NULL, &error);
+ g_assert_no_error (error);
+
+ tc->conn_source = g_socket_create_source (tc->conn_sock, G_IO_IN, NULL);
+ g_source_set_callback (tc->conn_source, (GSourceFunc)on_socket_input, tc, NULL);
+ g_source_attach (tc->conn_source, NULL);
+
+ /* Only one connection */
+ return FALSE;
+}
+
+static void
+setup_connect (TestConnect *tc,
+ gconstpointer data)
+{
+ GError *error = NULL;
+ GInetAddress *inet;
+ GSocketAddress *address;
+ GSocketFamily family = GPOINTER_TO_INT (data);
+
+ if (family == G_SOCKET_FAMILY_INVALID)
+ family = G_SOCKET_FAMILY_IPV4;
+
+ inet = g_inet_address_new_loopback (family);
+ address = g_inet_socket_address_new (inet, 0);
+ g_object_unref (inet);
+
+ tc->listen_sock = g_socket_new (family, G_SOCKET_TYPE_STREAM,
+ G_SOCKET_PROTOCOL_DEFAULT, &error);
+ g_assert_no_error (error);
+
+ g_socket_bind (tc->listen_sock, address, TRUE, &error);
+ g_object_unref (address);
+
+ if (error != NULL && family == G_SOCKET_FAMILY_IPV6)
+ {
+ /* Some test runners don't have IPv6 loopback, strangely enough */
+ g_clear_error (&error);
+ tc->skip_ipv6_loopback = TRUE;
+ return;
+ }
+
+ g_assert_no_error (error);
+
+ tc->address = g_socket_get_local_address (tc->listen_sock, &error);
+ g_assert_no_error (error);
+
+ tc->port = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (tc->address));
+
+ g_socket_listen (tc->listen_sock, &error);
+ g_assert_no_error (error);
+
+ tc->listen_source = g_socket_create_source (tc->listen_sock, G_IO_IN, NULL);
+ g_source_set_callback (tc->listen_source, (GSourceFunc)on_socket_connection, tc, NULL);
+ g_source_attach (tc->listen_source, NULL);
+}
+
+static void
+teardown_connect (TestConnect *tc,
+ gconstpointer data)
+{
+ if (tc->address)
+ g_object_unref (tc->address);
+ if (tc->conn_source)
+ {
+ g_source_destroy (tc->conn_source);
+ g_source_unref (tc->conn_source);
+ }
+ if (tc->listen_source)
+ {
+ g_source_destroy (tc->listen_source);
+ g_source_unref (tc->listen_source);
+ }
+ g_clear_object (&tc->listen_sock);
+ g_clear_object (&tc->conn_sock);
+}
+
+static void
+test_connect_and_read (TestConnect *tc,
+ gconstpointer user_data)
+{
+ CockpitConnectable connectable = { .address = G_SOCKET_CONNECTABLE (tc->address) };
+ CockpitStream *stream;
+ GError *error = NULL;
+ GByteArray *buffer;
+
+ stream = cockpit_stream_connect ("connect-and-read", &connectable);
+ g_assert (stream != NULL);
+
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Send the null terminator */
+ g_assert_cmpint (g_socket_send (tc->conn_sock, "eier", 5, NULL, &error), ==, 5);
+ g_assert_no_error (error);
+
+ buffer = cockpit_stream_get_buffer (stream);
+ while (buffer->len == 0)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpuint (buffer->len, ==, 5);
+ g_assert_cmpstr ((gchar *)buffer->data, ==, "eier");
+
+ g_object_unref (stream);
+}
+
+static void
+test_connect_early_close (TestConnect *tc,
+ gconstpointer user_data)
+{
+ CockpitConnectable connectable = { .address = G_SOCKET_CONNECTABLE (tc->address) };
+ CockpitStream *stream;
+
+ stream = cockpit_stream_connect ("connect-early-close", &connectable);
+ g_assert (stream != NULL);
+
+ cockpit_stream_close (stream, NULL);
+ g_object_unref (stream);
+
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+}
+
+static void
+test_connect_loopback (TestConnect *tc,
+ gconstpointer user_data)
+{
+ CockpitConnectable connectable = { 0 };
+ CockpitStream *stream;
+ GError *error = NULL;
+ GByteArray *buffer;
+
+
+ if (tc->skip_ipv6_loopback)
+ {
+ g_test_skip ("no loopback for ipv6 found");
+ return;
+ }
+
+ connectable.address = cockpit_loopback_new (tc->port);
+ stream = cockpit_stream_connect ("loopback", &connectable);
+ g_object_unref (connectable.address);
+ g_assert (stream != NULL);
+
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Send the null terminator */
+ g_assert_cmpint (g_socket_send (tc->conn_sock, "eier", 5, NULL, &error), ==, 5);
+ g_assert_no_error (error);
+
+ buffer = cockpit_stream_get_buffer (stream);
+ while (buffer->len == 0)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpuint (buffer->len, ==, 5);
+ g_assert_cmpstr ((gchar *)buffer->data, ==, "eier");
+
+ g_object_unref (stream);
+}
+
+static void
+test_connect_and_write (TestConnect *tc,
+ gconstpointer user_data)
+{
+ CockpitConnectable connectable = { .address = G_SOCKET_CONNECTABLE (tc->address) };
+ gchar buffer[8];
+ CockpitStream *stream;
+ GError *error = NULL;
+ GBytes *sent;
+ gssize ret;
+
+ stream = cockpit_stream_connect ("connect-and-write", &connectable);
+ g_assert (stream != NULL);
+
+ /* Sending on the stream before actually connected */
+ sent = g_bytes_new_static ("J", 1);
+ cockpit_stream_write (stream, sent);
+ g_bytes_unref (sent);
+ g_assert (tc->conn_sock == NULL);
+
+ /* Now we connect in main loop */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Read from the socket */
+ for (;;)
+ {
+ ret = g_socket_receive_with_blocking (tc->conn_sock, buffer, sizeof (buffer), FALSE, NULL, &error);
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+ {
+ g_assert_cmpint (ret, ==, -1);
+ g_main_context_iteration (NULL, TRUE);
+ g_clear_error (&error);
+ continue;
+ }
+ g_assert_no_error (error);
+ g_assert_cmpint (ret, ==, 1);
+ break;
+ }
+
+ g_assert_cmpint (buffer[0], ==, 'J');
+ g_object_unref (stream);
+}
+
+static void
+test_fail_not_found (void)
+{
+ CockpitConnectable connectable = { 0 };
+ CockpitStream *stream;
+ GSocketAddress *address;
+ gchar *problem = NULL;
+
+ cockpit_expect_message ("*No such file or directory");
+
+ address = g_unix_socket_address_new ("/non-existent");
+ connectable.address = G_SOCKET_CONNECTABLE (address);
+ stream = cockpit_stream_connect ("bad", &connectable);
+ g_object_unref (connectable.address);
+
+ /* Should not have closed at this point */
+ g_assert (stream != NULL);
+ g_signal_connect (stream, "close", G_CALLBACK (on_close_get_problem), &problem);
+
+ /* closes in main loop */
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_expected ();
+
+ g_assert_cmpstr (problem, ==, "not-found");
+ g_free (problem);
+ g_object_unref (stream);
+}
+
+static void
+test_fail_access_denied (void)
+{
+ CockpitConnectable connectable = { 0 };
+ CockpitStream *stream;
+ GSocketAddress *address;
+ gchar *unix_path;
+ gchar *problem = NULL;
+ gint fd;
+
+ if (geteuid () == 0)
+ {
+ g_test_skip ("running as root");
+ return;
+ }
+
+ unix_path = g_strdup ("/tmp/cockpit-test-XXXXXX.sock");
+ fd = g_mkstemp (unix_path);
+ g_assert_cmpint (fd, >=, 0);
+
+ /* Take away all permissions from the file */
+ g_assert_cmpint (fchmod (fd, 0000), ==, 0);
+
+ cockpit_expect_message ("*Permission denied");
+
+ address = g_unix_socket_address_new (unix_path);
+ connectable.address = G_SOCKET_CONNECTABLE (address);
+ stream = cockpit_stream_connect ("bad", &connectable);
+ g_object_unref (address);
+
+ /* Should not have closed at this point */
+ g_assert (stream != NULL);
+ g_signal_connect (stream, "close", G_CALLBACK (on_close_get_problem), &problem);
+
+ /* closes in main loop */
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_expected ();
+
+ g_assert_cmpstr (problem, ==, "access-denied");
+ g_free (unix_path);
+ g_free (problem);
+ g_object_unref (stream);
+}
+
+static void
+test_problem_later (void)
+{
+ gchar *problem = NULL;
+ gchar *check;
+ CockpitStream *stream;
+
+ stream = g_object_new (COCKPIT_TYPE_STREAM,
+ "problem", "i-have-a-problem",
+ NULL);
+ g_signal_connect (stream, "close", G_CALLBACK (on_close_get_problem), &problem);
+
+ g_object_get (stream, "problem", &check, NULL);
+ g_assert_cmpstr (check, ==, "i-have-a-problem");
+ g_free (check);
+ check = NULL;
+
+ g_assert (problem == NULL);
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "i-have-a-problem");
+ g_object_get (stream, "problem", &check, NULL);
+ g_assert_cmpstr (problem, ==, check);
+
+ g_object_unref (stream);
+ g_free (problem);
+ g_free (check);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/stream/properties", test_properties);
+
+ /*
+ * Fixture data is the GType name of the stream class
+ * so register these types here.
+ */
+ g_type_class_ref (mock_echo_stream_get_type ());
+ g_type_class_ref (cockpit_stream_get_type ());
+
+ g_test_add ("/stream/echo-message", TestCase, NULL,
+ setup_simple, test_echo_and_close, teardown);
+ g_test_add ("/stream/echo-queue", TestCase, NULL,
+ setup_simple, test_echo_queue, teardown);
+ g_test_add ("/stream/echo-large", TestCase, &fixture_no_timeout,
+ setup_simple, test_echo_large, teardown);
+ g_test_add ("/stream/close-problem", TestCase, NULL,
+ setup_simple, test_close_problem, teardown);
+ g_test_add ("/stream/buffer", TestCase, &fixture_buffer,
+ setup_simple, test_buffer, teardown);
+ g_test_add ("/stream/skip-zero", TestCase, NULL,
+ setup_simple, test_skip_zero, teardown);
+
+ g_test_add ("/stream/pressure/queue", TestCase, NULL,
+ setup_simple, test_pressure_queue, teardown);
+ g_test_add ("/stream/pressure/throttle", TestCase, NULL,
+ setup_simple, test_pressure_throttle, teardown);
+
+ g_test_add_func ("/stream/read-error", test_read_error);
+ g_test_add_func ("/stream/write-error", test_write_error);
+ g_test_add_func ("/stream/read-combined", test_read_combined);
+
+ g_test_add ("/stream/connect/and-read", TestConnect, NULL,
+ setup_connect, test_connect_and_read, teardown_connect);
+ g_test_add ("/stream/connect/early-close", TestConnect, NULL,
+ setup_connect, test_connect_early_close, teardown_connect);
+ g_test_add ("/stream/connect/and-write", TestConnect, NULL,
+ setup_connect, test_connect_and_write, teardown_connect);
+ g_test_add ("/stream/connect/loopback-ipv4", TestConnect, GINT_TO_POINTER (G_SOCKET_FAMILY_IPV4),
+ setup_connect, test_connect_loopback, teardown_connect);
+ g_test_add ("/stream/connect/loopback-ipv6", TestConnect, GINT_TO_POINTER (G_SOCKET_FAMILY_IPV6),
+ setup_connect, test_connect_loopback, teardown_connect);
+
+ g_test_add_func ("/stream/problem-later", test_problem_later);
+
+ g_test_add_func ("/stream/connect/not-found", test_fail_not_found);
+ g_test_add_func ("/stream/connect/access-denied", test_fail_access_denied);
+
+ return g_test_run ();
+}
diff --git a/src/bridge/test-websocketstream.c b/src/bridge/test-websocketstream.c
new file mode 100644
index 0000000..189ebf2
--- /dev/null
+++ b/src/bridge/test-websocketstream.c
@@ -0,0 +1,401 @@
+
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitwebsocketstream.h"
+
+#include "common/cockpitchannel.h"
+#include "testlib/cockpittest.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitwebresponse.h"
+#include "common/cockpitwebserver.h"
+#include "testlib/mock-transport.h"
+
+#include "websocket/websocketclient.h"
+
+#include <string.h>
+
+static void
+on_closed_get_problem (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gchar **result = user_data;
+ g_assert (problem != NULL);
+ g_assert (*result == NULL);
+ *result = g_strdup (problem);
+}
+
+typedef struct {
+ MockTransport *transport;
+ CockpitWebServer *server;
+ GIOStream *client;
+ guint port;
+ gchar *origin;
+ gchar *url;
+ gboolean ws_closed;
+} TestCase;
+
+static void
+on_socket_message (WebSocketConnection *self,
+ WebSocketDataType type,
+ GBytes *message,
+ gpointer user_data)
+{
+ GByteArray *array = g_bytes_unref_to_array (g_bytes_ref (message));
+ GBytes *payload;
+ guint i;
+
+ /* Capitalize and relay back */
+ for (i = 0; i < array->len; i++)
+ array->data[i] = g_ascii_toupper (array->data[i]);
+
+ payload = g_byte_array_free_to_bytes (array);
+ web_socket_connection_send (self, type, NULL, payload);
+ g_bytes_unref (payload);
+}
+
+static void
+on_socket_close (WebSocketConnection *ws,
+ gpointer user_data)
+{
+ TestCase *test = user_data;
+ g_object_unref (ws);
+ test->ws_closed = TRUE;
+}
+
+static gboolean
+handle_socket (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ gpointer data)
+{
+ const gchar *path = cockpit_web_request_get_path (request);
+ const gchar *method = cockpit_web_request_get_method (request);
+ GIOStream *io_stream = cockpit_web_request_get_io_stream (request);
+ GByteArray *input = cockpit_web_request_get_buffer (request);
+ GHashTable *headers = cockpit_web_request_get_headers (request);
+
+ const gchar *origins[] = { NULL, NULL };
+ const gchar *protocols[] = { "one", "two", "three", NULL };
+ WebSocketConnection *ws = NULL;
+ TestCase *test = data;
+
+ if (!g_str_equal (path, "/socket"))
+ return FALSE;
+ /* HEAD on a socket makes little sense, we don't test it */
+ g_assert (g_strcmp0 (method, "GET") == 0);
+
+ origins[0] = test->origin;
+ ws = web_socket_server_new_for_stream (test->url, (const gchar **)origins,
+ protocols, io_stream, headers, input);
+
+ g_signal_connect (ws, "message", G_CALLBACK (on_socket_message), NULL);
+ g_signal_connect (ws, "close", G_CALLBACK (on_socket_close), test);
+ return TRUE;
+}
+
+
+static void
+setup (TestCase *test,
+ gconstpointer data)
+{
+ test->server = cockpit_web_server_new (NULL, COCKPIT_WEB_SERVER_NONE);
+ test->port = cockpit_web_server_add_inet_listener (test->server, NULL, 0, NULL);
+ cockpit_web_server_start (test->server);
+ test->transport = mock_transport_new ();
+ test->ws_closed = FALSE;
+ test->origin = g_strdup_printf ("http://localhost:%u", test->port);
+ test->url = g_strdup_printf ("ws://localhost:%u/socket", test->port);
+ g_signal_connect (test->server, "handle-stream",
+ G_CALLBACK (handle_socket), test);
+}
+
+static void
+teardown (TestCase *test,
+ gconstpointer data)
+{
+ g_object_unref (test->server);
+ g_object_unref (test->transport);
+
+ g_free (test->origin);
+ g_free (test->url);
+ cockpit_assert_expected ();
+}
+
+static void
+test_basic (TestCase *test,
+ gconstpointer data)
+{
+ JsonObject *options;
+ CockpitChannel *channel;
+ GBytes *bytes = NULL;
+ GBytes *recv = NULL;
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", test->port);
+ json_object_set_string_member (options, "payload", "websocket-stream1");
+ json_object_set_string_member (options, "path", "/socket");
+
+ channel = g_object_new (COCKPIT_TYPE_WEB_SOCKET_STREAM,
+ "transport", test->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+ json_object_unref (options);
+
+ bytes = g_bytes_new ("Message", 7);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (test->transport), "444", bytes);
+ g_bytes_unref (bytes);
+
+ while (mock_transport_count_sent (test->transport) < 3)
+ g_main_context_iteration (NULL, TRUE);
+
+ recv = mock_transport_pop_channel (test->transport, "444");
+ cockpit_assert_bytes_eq (recv, "MESSAGE", 7);
+
+ cockpit_channel_close (channel, "ending");
+
+ while (!test->ws_closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (channel);
+}
+
+static void
+test_bad_port (TestCase *test,
+ gconstpointer data)
+{
+ test->port = 1023;
+
+ g_autoptr(JsonObject) options = json_object_new ();
+ json_object_set_int_member (options, "port", test->port);
+ json_object_set_string_member (options, "payload", "websocket-stream1");
+ json_object_set_string_member (options, "path", "/socket");
+
+ g_autoptr(CockpitChannel) channel = g_object_new (COCKPIT_TYPE_WEB_SOCKET_STREAM,
+ "transport", test->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ g_autofree gchar *problem = NULL;
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "not-found");
+}
+
+static void
+test_bad_origin (TestCase *test,
+ gconstpointer data)
+{
+ JsonObject *options;
+ CockpitChannel *channel;
+ gchar *problem = NULL;
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", test->port);
+ json_object_set_string_member (options, "payload", "websocket-stream1");
+ json_object_set_string_member (options, "path", "/socket");
+
+ g_free (test->origin);
+ test->origin = g_strdup ("bad-origin");
+
+ channel = g_object_new (COCKPIT_TYPE_WEB_SOCKET_STREAM,
+ "transport", test->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+ g_signal_connect (channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+ json_object_unref (options);
+
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "protocol-error");
+ while (!test->ws_closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (channel);
+ g_free (problem);
+}
+
+typedef struct {
+ GTlsCertificate *certificate;
+ MockTransport *transport;
+ CockpitWebServer *server;
+ guint port;
+ gchar *origin;
+ gchar *url;
+ gboolean ws_closed;
+} TestTls;
+
+static void
+setup_tls (TestTls *test,
+ gconstpointer data)
+{
+ GError *error = NULL;
+
+ test->certificate = g_tls_certificate_new_from_files (SRCDIR "/src/bridge/mock-server.crt",
+ SRCDIR "/src/bridge/mock-server.key", &error);
+ g_assert_no_error (error);
+ test->server = cockpit_web_server_new (test->certificate, COCKPIT_WEB_SERVER_NONE);
+ test->port = cockpit_web_server_add_inet_listener (test->server, NULL, 0, &error);
+ g_assert_no_error (error);
+ g_assert (test->port != 0);
+
+ cockpit_web_server_start (test->server);
+
+ test->transport = mock_transport_new ();
+ test->ws_closed = FALSE;
+ test->origin = g_strdup_printf ("https://localhost:%u", test->port);
+ test->url = g_strdup_printf ("wss://localhost:%u/socket", test->port);
+ g_signal_connect (test->server, "handle-stream",
+ G_CALLBACK (handle_socket), test);
+}
+
+static void
+teardown_tls (TestTls *test,
+ gconstpointer data)
+{
+ g_object_unref (test->certificate);
+ g_object_unref (test->server);
+ g_object_unref (test->transport);
+
+ g_free (test->origin);
+ g_free (test->url);
+}
+
+
+static const gchar fixture_tls_authority_good[] =
+ "{ \"authority\": { \"file\": \"" SRCDIR "/src/bridge/mock-server.crt\" } }";
+
+static void
+test_tls_authority_good (TestTls *test,
+ gconstpointer json)
+{
+ JsonObject *tls;
+ JsonObject *options;
+ CockpitChannel *channel;
+ GBytes *bytes = NULL;
+ GBytes *recv = NULL;
+ GError *error = NULL;
+
+ tls = cockpit_json_parse_object (json, -1, &error);
+ g_assert_no_error (error);
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", test->port);
+ json_object_set_string_member (options, "payload", "websocket-stream1");
+ json_object_set_string_member (options, "path", "/socket");
+ json_object_set_object_member (options, "tls", tls);
+
+ channel = g_object_new (COCKPIT_TYPE_WEB_SOCKET_STREAM,
+ "transport", test->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+
+ bytes = g_bytes_new ("Message", 7);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (test->transport), "444", bytes);
+ g_bytes_unref (bytes);
+
+ while (mock_transport_count_sent (test->transport) < 3)
+ g_main_context_iteration (NULL, TRUE);
+
+ recv = mock_transport_pop_channel (test->transport, "444");
+ cockpit_assert_bytes_eq (recv, "MESSAGE", 7);
+
+ cockpit_channel_close (channel, "ending");
+
+ while (!test->ws_closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (channel);
+}
+
+static const gchar fixture_tls_authority_bad[] =
+ "{ \"authority\": { \"file\": \"" SRCDIR "/src/bridge/mock-client.crt\" } }";
+
+static void
+test_tls_authority_bad (TestTls *test,
+ gconstpointer json)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+ JsonObject *tls;
+ GError *error = NULL;
+
+ GBytes *bytes;
+ JsonObject *resp;
+
+ tls = cockpit_json_parse_object (json, -1, &error);
+ g_assert_no_error (error);
+
+ options = json_object_new ();
+ json_object_set_int_member (options, "port", test->port);
+ json_object_set_string_member (options, "payload", "websocket-stream1");
+ json_object_set_string_member (options, "path", "/socket");
+ json_object_set_object_member (options, "tls", tls);
+
+ channel = g_object_new (COCKPIT_TYPE_WEB_SOCKET_STREAM,
+ "transport", test->transport,
+ "id", "444",
+ "options", options,
+ NULL);
+ json_object_unref (options);
+
+ bytes = g_bytes_new ("Message", 7);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (test->transport), "444", bytes);
+ g_bytes_unref (bytes);
+
+ while (mock_transport_count_sent (test->transport) < 1)
+ g_main_context_iteration (NULL, TRUE);
+
+ resp = mock_transport_pop_control (test->transport);
+ json_object_remove_member (resp, "message");
+ cockpit_assert_json_eq (resp, "{\"command\":\"close\",\"channel\":\"444\",\"problem\":\"unknown-hostkey\"}");
+
+ g_object_unref (channel);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/websocket-stream/test_basic", TestCase, NULL,
+ setup, test_basic, teardown);
+ g_test_add ("/websocket-stream/test_bad_port", TestCase, NULL,
+ setup, test_bad_port, teardown);
+ g_test_add ("/websocket-stream/test_bad_origin", TestCase, NULL,
+ setup, test_bad_origin, teardown);
+ g_test_add ("/websocket/tls/authority-good", TestTls, fixture_tls_authority_good,
+ setup_tls, test_tls_authority_good, teardown_tls);
+ g_test_add ("/websocket/tls/authority-bad", TestTls, fixture_tls_authority_bad,
+ setup_tls, test_tls_authority_bad, teardown_tls);
+ return g_test_run ();
+}
diff --git a/src/build_backend.py b/src/build_backend.py
new file mode 100644
index 0000000..07d2274
--- /dev/null
+++ b/src/build_backend.py
@@ -0,0 +1,140 @@
+import argparse
+import base64
+import gzip
+import hashlib
+import lzma
+import os
+import shutil
+import subprocess
+import tarfile
+import zipfile
+from typing import AnyStr, Dict, Iterable, Optional
+
+from cockpit import __version__
+
+VERSION = __version__ or '0'
+PACKAGE = f'cockpit-{VERSION}'
+TAG = 'py3-none-any'
+
+
+def find_sources(*, srcpkg: bool) -> Iterable[str]:
+ try:
+ subprocess.check_call(['vendor/checkout'], stdout=2) # Needed for git builds...
+ except FileNotFoundError: # ...but not present in tarball...
+ pass # ...and not needed either, because...
+ assert os.path.exists('src/cockpit/_vendor/ferny/__init__.py') # ...the code should exist there already.
+
+ if srcpkg:
+ yield from (
+ 'pyproject.toml',
+ 'src/build_backend.py',
+ )
+
+ for path, _dirs, files in os.walk('src', followlinks=True):
+ if '__init__.py' in files:
+ yield from [os.path.join(path, file) for file in files]
+
+
+def copy_sources(distdir: str) -> None:
+ for source in find_sources(srcpkg=True):
+ destination = os.path.join(distdir, source)
+ os.makedirs(os.path.dirname(destination), exist_ok=True)
+ shutil.copy(source, destination)
+
+
+def build_sdist(sdist_directory: str,
+ config_settings: Optional[Dict[str, object]] = None) -> str:
+ del config_settings
+ sdist_filename = f'{PACKAGE}.tar.gz'
+ # We do this manually to avoid adding timestamps. See https://bugs.python.org/issue31526
+ with gzip.GzipFile(f'{sdist_directory}/{sdist_filename}', mode='w', mtime=0) as gz:
+ with tarfile.open(fileobj=gz, mode='w|', dereference=True) as sdist:
+ for filename in find_sources(srcpkg=True):
+ sdist.add(filename, arcname=f'{PACKAGE}/{filename}', )
+ return sdist_filename
+
+
+def build_wheel(wheel_directory: str,
+ config_settings: Optional[Dict[str, object]] = None,
+ metadata_directory: Optional[str] = None) -> str:
+ del config_settings, metadata_directory
+ wheel_filename = f'{PACKAGE}-{TAG}.whl'
+ distinfo = {
+ 'WHEEL': [
+ 'Wheel-Version: 1.0',
+ 'Generator: cockpit build_backend',
+ 'Root-Is-Purelib: true',
+ f'Tag: {TAG}',
+ ],
+ 'METADATA': [
+ 'Metadata-Version: 2.1',
+ 'Name: cockpit',
+ f'Version: {VERSION}',
+ ],
+ 'entry_points.txt': [
+ '[console_scripts]',
+ 'cockpit-askpass = cockpit._vendor.ferny.interaction_client:main',
+ 'cockpit-bridge = cockpit.bridge:main',
+ ],
+ }
+
+ with zipfile.ZipFile(f'{wheel_directory}/{wheel_filename}', 'w') as wheel:
+ def write(filename: str, data: AnyStr) -> None:
+ # we do this manually to avoid adding timestamps
+ wheel.writestr(zipfile.ZipInfo(filename), data)
+
+ def beipack_self(main: str, args: str = '') -> bytes:
+ from cockpit._vendor.bei import beipack
+ contents = {name: wheel.read(name) for name in wheel.namelist()}
+ pack = beipack.pack(contents, main, args=args).encode('utf-8')
+ return lzma.compress(pack, preset=lzma.PRESET_EXTREME)
+
+ def write_distinfo(filename: str, lines: Iterable[str]) -> None:
+ write(f'{PACKAGE}.dist-info/{filename}', ''.join(f'{line}\n' for line in lines))
+
+ def record_lines() -> Iterable[str]:
+ for info in wheel.infolist():
+ digest = hashlib.sha256(wheel.read(info.filename)).digest()
+ b64_digest = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
+ yield f'{info.filename},sha256={b64_digest},{info.file_size}'
+ yield f'{PACKAGE}.dist-info/RECORD,,'
+
+ for filename in find_sources(srcpkg=False):
+ with open(filename, 'rb') as file:
+ write(os.path.relpath(filename, start='src'), file.read())
+
+ write('cockpit/data/cockpit-bridge.beipack.xz', beipack_self('cockpit.bridge:main', 'beipack=True'))
+
+ for filename, lines in distinfo.items():
+ write_distinfo(filename, lines)
+ write_distinfo('RECORD', record_lines())
+
+ return wheel_filename
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ group = parser.add_mutually_exclusive_group(required=True)
+ group.add_argument('--copy', action='store_true')
+ group.add_argument('--sdist', action='store_true')
+ group.add_argument('--wheel', action='store_true')
+ parser.add_argument('srcdir')
+ parser.add_argument('destdir')
+ args = parser.parse_args()
+
+ # We have to chdir() for PEP 517, so make sure dest is absolute
+ destdir = os.path.abspath(args.destdir)
+ os.chdir(args.srcdir)
+
+ os.makedirs(destdir, exist_ok=True)
+
+ if args.copy:
+ copy_sources(destdir)
+ elif args.sdist:
+ print(os.path.join(destdir, build_sdist(destdir)))
+ else:
+ print(os.path.join(destdir, build_wheel(destdir)))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/client/Makefile.am b/src/client/Makefile.am
new file mode 100644
index 0000000..4987a63
--- /dev/null
+++ b/src/client/Makefile.am
@@ -0,0 +1,35 @@
+# -----------------------------------------------------------------------------
+# Cockpit Client
+
+# We actually always install cockpit-client in libexec,
+# but ENABLE_COCKPIT_CLIENT controls whether we install
+# the desktop file, icons and a symlink in /usr/bin.
+# This is currently **EXPERIMENTAL**
+
+cockpitclientdir = $(libexecdir)
+dist_cockpitclient_SCRIPTS = src/client/cockpit-client
+dist_cockpitclient_DATA = src/client/cockpit-client.ui
+
+if ENABLE_COCKPIT_CLIENT
+
+INSTALL_EXEC_HOOK_TARGETS += install-cockpit-client-symlink
+install-cockpit-client-symlink:
+ mkdir -p $(DESTDIR)/$(bindir)
+ ln -sfTv $(cockpitclientdir)/cockpit-client $(DESTDIR)/$(bindir)/cockpit-client
+
+dbusservicesdir = $(datadir)/dbus-1/services
+dist_dbusservices_DATA = src/client/org.cockpit_project.CockpitClient.service
+
+applicationsdir = $(datadir)/applications
+dist_applications_DATA = src/client/org.cockpit_project.CockpitClient.desktop
+
+scalableicondir = $(datadir)/icons/hicolor/scalable/apps
+dist_scalableicon_DATA = src/client/cockpit-client.svg
+
+symbolicicondir = $(datadir)/icons/hicolor/symbolic/apps
+dist_symbolicicon_DATA = src/client/cockpit-client-symbolic.svg
+
+client_metainfodir = $(datadir)/metainfo
+dist_client_metainfo_DATA = src/client/org.cockpit_project.CockpitClient.metainfo.xml
+
+endif
diff --git a/src/client/cockpit-client b/src/client/cockpit-client
new file mode 100755
index 0000000..f4604d8
--- /dev/null
+++ b/src/client/cockpit-client
@@ -0,0 +1,344 @@
+#!/usr/bin/env python3
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2018-2021 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import configparser
+import ctypes
+import logging
+import os
+import signal
+import subprocess
+import sys
+import tempfile
+import textwrap
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import GLib, Gio, Gtk # noqa: I001, E402
+
+try:
+ gi.require_version("WebKit2", "4.1")
+ from gi.repository import WebKit2
+except (ValueError, ImportError):
+ gi.require_version("WebKit2", "4.0")
+ from gi.repository import WebKit2
+
+try:
+ gi.require_version("Handy", "1")
+ from gi.repository import Handy
+except (ValueError, ImportError):
+ Handy = None
+
+libexecdir = os.path.realpath(__file__ + '/..')
+
+libc6 = ctypes.cdll.LoadLibrary('libc.so.6')
+
+
+def prctl(*args):
+ if libc6.prctl(*args) != 0:
+ raise Exception('prctl() failed')
+
+
+def get_user_state_dir():
+ try:
+ # GLib ≥ 2.72
+ return GLib.get_user_state_dir()
+ except AttributeError:
+ try:
+ return os.environ["XDG_STATE_HOME"]
+ except KeyError:
+ return os.path.expanduser("~/.local/share")
+
+
+SET_PDEATHSIG = 1
+
+
+@Gtk.Template(filename=f'{libexecdir}/cockpit-client.ui')
+class CockpitClientWindow(Gtk.ApplicationWindow):
+ __gtype_name__ = 'CockpitClientWindow'
+ webview = Gtk.Template.Child()
+
+ def __init__(self, app, uri):
+ super().__init__(application=app)
+
+ self.add_action_entries([
+ ('reload', self.reload),
+ ('reload-bypass-cache', self.reload_bypass_cache),
+ ('go-back', self.go_back),
+ ('go-forward', self.go_forward),
+ ('zoom', self.zoom, 's'),
+ ('open-inspector', self.open_inspector),
+ ('run-js', self.run_js, 's', '""'),
+ ])
+ self.lookup_action('run-js').set_enabled(app.enable_run_js)
+
+ self.webview.get_settings().set_enable_developer_extras(enabled=True)
+ self.webview.connect('decide-policy', self.decide_policy)
+ self.webview.load_uri(uri)
+
+ history = self.webview.get_back_forward_list()
+ history.connect('changed', self.history_changed)
+ self.history_changed()
+
+ if app.no_ui:
+ self.set_titlebar(None)
+ self.webview.bind_property('title', self, 'title')
+
+ def history_changed(self, *args):
+ self.lookup_action('go-back').set_enabled(self.webview.can_go_back())
+ self.lookup_action('go-forward').set_enabled(self.webview.can_go_forward())
+
+ def reload(self, *args):
+ self.webview.reload()
+
+ def reload_bypass_cache(self, *args):
+ self.webview.reload_bypass_cache()
+
+ def go_back(self, *args):
+ self.webview.go_back()
+
+ def go_forward(self, *args):
+ self.webview.go_forward()
+
+ def decide_policy(self, _view, decision, decision_type):
+ if decision_type == WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION:
+ uri = decision.get_navigation_action().get_request().get_uri()
+ if uri.startswith('http://127'):
+ logging.error('warning: no support for pop-ups')
+ else:
+ # We can't get the timestamp from the request, so use Gdk.CURRENT_TIME (== 0)
+ Gtk.show_uri_on_window(self, uri, 0)
+
+ decision.ignore()
+ return True
+
+ return False
+
+ def zoom(self, _action, parameter, *_unused):
+ current = self.webview.get_zoom_level()
+ factors = {'in': current * 1.1, 'default': 1.0, 'out': current * 0.9}
+ self.webview.set_zoom_level(factors[parameter.get_string()])
+
+ def open_inspector(self, *_unused):
+ self.webview.get_inspector().show()
+
+ def run_js(self, action, parameter, *_unused):
+ """Run given JavaScript code in the current page/context
+
+ The JS code has to return a scalar value immediately. To process asynchronous
+ actions, this function installs signal handlers to wait until the code calls
+
+ window.webkit.messageHandlers.result.postMessage("some string")
+
+ or a page load happens.
+
+ This is because a page load (as reaction to clicking a link or other action)
+ impredictably and immediately stops the current context and execution of the
+ given JS code, with no reliable way of returning a result value.
+
+ The postMessage() string is returned via the action state.
+ If a page load happens instead, the action state will be "page-load".
+ If the JS code throws an error, the action state gets set to the error message.
+ """
+ def set_result(result):
+ action.set_state(GLib.Variant.new_string(result))
+ print(f"run-js async result: {result}")
+ self.webview.disconnect(on_load_handler)
+ uc_manager.disconnect(on_result_handler)
+ uc_manager.unregister_script_message_handler("result")
+
+ def run_js_ready(webview, result, _user_data):
+ try:
+ js_res = webview.run_javascript_finish(result).get_js_value()
+ print(f"run-js return value: {js_res.to_json(2)}")
+ except GLib.GError as e:
+ sys.stderr.write(f"run-js error: {e.message}\n")
+ set_result(str(e))
+
+ # zero the current state, to ensure that the result triggers an Actions.Changed signal
+ action.set_state(GLib.Variant.new_string(""))
+
+ uc_manager = self.webview.get_user_content_manager()
+ on_result_handler = uc_manager.connect(
+ "script-message-received::result",
+ lambda _mgr, result: set_result(result.get_js_value().to_string()))
+ uc_manager.register_script_message_handler("result")
+
+ # wait for loads to complete, to avoid races and ensure that it did not fail
+ on_load_handler = self.webview.connect(
+ "load-changed",
+ lambda webview, event: set_result("page-load") if event == WebKit2.LoadEvent.FINISHED else None)
+
+ self.webview.run_javascript(parameter.get_string(), None, run_js_ready, None)
+
+
+class CockpitClient(Gtk.Application):
+ def __init__(self):
+ super().__init__(application_id='org.cockpit_project.CockpitClient')
+
+ self.add_main_option('no-ui', 0, GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
+ 'Show only a window with a webview')
+ self.add_main_option('external-ws', 0, GLib.OptionFlags.NONE, GLib.OptionArg.STRING,
+ 'Connect to existing cockpit-ws on the given URL')
+ self.add_main_option('disable-uniqueness', 0, GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
+ 'Disable GApplication single-instance mode')
+ self.add_main_option('enable-run-js', 0, GLib.OptionFlags.HIDDEN, GLib.OptionArg.NONE,
+ 'Enable run-js action for testing')
+ self.add_main_option('wildly-insecure', 0, GLib.OptionFlags.HIDDEN, GLib.OptionArg.NONE,
+ '***DANGEROUS*** Allow anyone to use your ssh credentials')
+
+ def do_startup(self):
+ Gtk.Application.do_startup(self)
+
+ if Handy and hasattr(Handy, 'StyleManager'):
+ Handy.StyleManager.get_default().set_color_scheme(Handy.ColorScheme.PREFER_LIGHT)
+
+ # .add_action_entries() binding is broken for GApplication
+ # https://gitlab.gnome.org/GNOME/pygobject/-/issues/426
+ Gio.ActionMap.add_action_entries(self, [
+ ('new-window', self.new_window),
+ ('quit', self.quit_action),
+ ('open-path', self.open_path, 's'),
+ ])
+
+ self.set_accels_for_action("app.new-window", ["<Ctrl>N"])
+ self.set_accels_for_action("win.reload", ["<Primary>r"])
+ self.set_accels_for_action("win.reload-bypass-cache", ["<Primary><Shift>r"])
+ self.set_accels_for_action("win.go-back", ["<Alt>Left"])
+ self.set_accels_for_action("win.go-forward", ["<Alt>Right"])
+ self.set_accels_for_action("win.zoom::in", ["<Primary>equal"])
+ self.set_accels_for_action("win.zoom::out", ["<Primary>minus"])
+ self.set_accels_for_action("win.zoom::default", ["<Primary>0"])
+ self.set_accels_for_action("win.open-inspector", ["<Primary><Shift>i", "F12"])
+
+ context = WebKit2.WebContext.get_default()
+ data_manager = context.get_website_data_manager()
+ data_manager.set_network_proxy_settings(WebKit2.NetworkProxyMode.NO_PROXY, None)
+ context.set_sandbox_enabled(enabled=True)
+ context.set_cache_model(WebKit2.CacheModel.DOCUMENT_VIEWER)
+
+ cookiesFile = os.path.join(get_user_state_dir(), "cockpit-client", "cookies.txt")
+ cookies = context.get_cookie_manager()
+ cookies.set_persistent_storage(cookiesFile, WebKit2.CookiePersistentStorage.TEXT)
+
+ self.uri = self.ws.start()
+
+ def new_window(self, *args):
+ self.activate()
+
+ def quit_action(self, *args):
+ self.quit()
+
+ def open_path(self, action, parameter, *args):
+ CockpitClientWindow(self, self.uri + parameter.get_string()).present()
+
+ def do_activate(self):
+ CockpitClientWindow(self, self.uri).present()
+
+ def do_shutdown(self):
+ self.ws.stop()
+
+ Gtk.Application.do_shutdown(self)
+
+ def do_handle_local_options(self, options):
+ self.no_ui = options.lookup_value('no-ui') is not None
+
+ if options.lookup_value('disable-uniqueness'):
+ self.set_flags(Gio.ApplicationFlags.NON_UNIQUE)
+
+ self.enable_run_js = bool(options.lookup_value('enable-run-js'))
+
+ if flatpak_id := os.getenv('FLATPAK_ID'):
+ self.set_application_id(flatpak_id)
+
+ if external_ws := options.lookup_value('external-ws'):
+ self.ws = ExternalCockpitWs(external_ws.get_string())
+
+ elif self.safely_sandboxed() or options.lookup_value('wildly-insecure'):
+ self.ws = InternalCockpitWs()
+
+ else:
+ logging.error('Unable to detect any sandboxing: refusing to spawn cockpit-ws')
+ return 1
+
+ return -1
+
+ def safely_sandboxed(self):
+ flatpak_info = configparser.ConfigParser()
+
+ if not flatpak_info.read('/.flatpak-info'):
+ return False
+
+ shared = flatpak_info.get('Context', 'Shared', fallback='').lower().split(';')
+ if 'network' in shared:
+ return False
+
+ return True
+
+
+class ExternalCockpitWs:
+ def __init__(self, uri):
+ self.uri = uri
+
+ def start(self):
+ return self.uri
+
+ def stop(self):
+ pass
+
+
+class InternalCockpitWs:
+ def start(self):
+ self.write_config()
+ host = '127.0.0.90'
+ port = '9090'
+ self.ws = subprocess.Popen([f'{libexecdir}/cockpit-ws', f'--address={host}', f'--port={port}'],
+ preexec_fn=self.preexec_fn, pass_fds=[3])
+
+ return f'http://{host}:{port}/'
+
+ def preexec_fn(self):
+ os.environ['XDG_CONFIG_DIRS'] = self.config_dir.name
+ prctl(SET_PDEATHSIG, signal.SIGKILL)
+
+ def stop(self):
+ self.ws.kill()
+ self.config_dir.cleanup()
+
+ def write_config(self):
+ self.config_dir = tempfile.TemporaryDirectory(prefix='cockpit-client-', suffix='-etc')
+ os.mkdir(f'{self.config_dir.name}/cockpit')
+ with open(f'{self.config_dir.name}/cockpit/cockpit.conf', 'x') as cockpit_conf:
+ # avoid putting strings here that ought to be translated
+ config = """
+ # dynamically generated by cockpit-client
+ [WebService]
+ X-For-CockpitClient = true
+ LoginTo = true
+
+ [Ssh-Login]
+ Command = /usr/bin/env python3 -m cockpit.beiboot
+ """
+
+ cockpit_conf.write(textwrap.dedent(config))
+
+
+if __name__ == "__main__":
+ app = CockpitClient()
+ app.run(sys.argv)
diff --git a/src/client/cockpit-client-symbolic.svg b/src/client/cockpit-client-symbolic.svg
new file mode 100644
index 0000000..a9590a1
--- /dev/null
+++ b/src/client/cockpit-client-symbolic.svg
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <filter id="a" height="100%" width="100%" x="0%" y="0%">
+ <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
+ </filter>
+ <mask id="b">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.154911" height="16" width="16"/>
+ </g>
+ </mask>
+ <mask id="c">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.07214" height="16" width="16"/>
+ </g>
+ </mask>
+ <mask id="d">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.2" height="16" width="16"/>
+ </g>
+ </mask>
+ <mask id="e">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.3" height="16" width="16"/>
+ </g>
+ </mask>
+ <mask id="f">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.092655" height="16" width="16"/>
+ </g>
+ </mask>
+ <mask id="g">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.2" height="16" width="16"/>
+ </g>
+ </mask>
+ <clipPath id="h">
+ <rect height="152" width="192"/>
+ </clipPath>
+ <path d="m 8 0 c -2.050781 0 -4.101562 0.78125 -5.660156 2.339844 c -3.121094 3.121094 -3.121094 8.199218 0 11.320312 s 8.199218 3.121094 11.320312 0 s 3.121094 -8.199218 0 -11.320312 c -1.558594 -1.558594 -3.609375 -2.339844 -5.660156 -2.339844 z m 3.65625 3.710938 c 0.035156 0 0.066406 0 0.097656 0.003906 c 0.121094 0.003906 0.222656 0.039062 0.308594 0.097656 c 0 0 0.050781 0.035156 0.070312 0.054688 c 0.019532 0.019531 0.050782 0.0625 0.054688 0.070312 c 0.304688 0.449219 -0.117188 1.457031 -0.988281 2.324219 l -1.433594 1.433593 l 1.488281 4.429688 l -0.894531 0.863281 l -2.542969 -3.34375 l -0.742187 0.742188 c -0.261719 0.265625 -0.539063 0.484375 -0.808594 0.65625 l 0.152344 2.011719 l -0.503907 0.503906 l -1.128906 -1.542969 l -0.339844 -0.460937 l -0.460937 -0.339844 l -1.542969 -1.128906 l 0.503906 -0.503907 l 2.011719 0.152344 c 0.171875 -0.269531 0.390625 -0.546875 0.65625 -0.808594 l 0.742188 -0.742187 l -3.34375 -2.542969 l 0.863281 -0.894531 l 4.429688 1.488281 l 1.433593 -1.433594 c 0.660157 -0.664062 1.402344 -1.070312 1.917969 -1.089843 z m 0 0"/>
+ <image height="152" mask="url(#b)" transform="matrix(1 0 0 1 -168 -16)" width="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAFOElEQVR4nO3d61LjRhAG0DZ7yfu/7iag/GGg3e6RTVKFLj6nSiVjG8FS3yeNRoKNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAedNn6Gzi6ZVnan+Hlclm++3vh6xTgQbOgP0oh9kkB7lgJ/iM/u5vQK8K+KMCKEv76s/ovBVgilGBPFKAxCX5d19dvNtM8XkIJdkUBkgeC35Xg0QLURQl2QAHeNeGvwb80z+f1zSbjds//FkqwKwoQV+FfC/29ItxstlneQgl25efW38DWSvi75WXltSiPPzbbLG/p9bfyXjby9AV4Nwt9XndFyJ/bzfjk8NeijCNBLMtycRTYxlMX4H3vPwt+XWZHg0jreuI7Qn4p7+meYwNPW4BJ+MfyIx4rQUR/BKgnvW8R8VpeW5rP45s9ZQGacX8O/o+4LkEuQ3cUuNp0WucCvJb3KMFOPF0B7oT/Z3yWIJehOxJ0w5/ZrE+k94xtvOVtLMtiNmgDT1WAEv661x8F+Jkez4ZD92Z+uqFPpOfq8EvwN/IUBWgucuXhzAj/r/gsQC5CNwyaFSAHvwt/LREbO30BVub5857/V1pqAepQqCtADv1a+NeuJTgKbOC0BZjc2tCN+XP4f8d1CepJcT56XH25+Az9I+HPRvidB2zglAWYhH+EcAS6C//vuD4SdCfCNch57/8St9OdXehj8pyLYt/sdAVYGfLUk94R8hH+v2J+BBgl6E5ex/Iat6W4N1XKxk5VgEn46wWuOvT5XZbZEGgsefv1VodIH7+U525uh2Z7pynAnfB3U52zoc9Y1k6Asxz+e8Gvd4Iqw8ZOUYCV8OcT2DrF2YW+G/93w5+PL50e1yFPDv1rfJ4g5xJ02+EbHb4Akyu7dZqzhv/R4M/2/nnacnZBLE+H1ilSJdiJwxfg3docfw59Xa+N97u5+qEbxnShf22WtRKYBv1mhy7A5Apvneefhb6Gf3aPzzCGOPWGtxHsf5p1XXIJbs4BhP/7HboASTf2n530drc6zK7s1q8xXsvvyQX4u1keCb/gb+QMBVib8uzG9LOLW92vLkbc/02vHP4/cT/8fi94Rw5bgAdudeimQLtZnYjb8M/KUF97i+shzp+0zEpg+nNHDluAd3XM3hVg7Q7MWejX7visN77NCjBKMMLf7fmVYGNHL0A1uw2i6vb4l7idy+9OeutsTzf+z+HPBbgJv6HPts5SgLXZm3rSml9/ic/w18+tw6OI2190GUeA17ge8vyJz/DX+f+P70n4t3eWAnQzKvnWg3yjWn4t39sz1kt5XLeXt5mXvMcfR4Q68/Px/Qn/PpylAEM3Rh+3J1/iOvj5nGB21MjbrNutF7rq/P9s3C/8O3KmAszG6Pm1tfv667bqNvMR5d7V3nbOX/D35+gFmJ2gXqL/ZfTuz5t8Zbuzo0C3Fv4DOGwBLpfLkq4F1PBHej4Hf/bHrapHjgCzIuT3mercucMWIBmzON3Fq1yAGvz/UoA6xKpl6KZXjfl37AwFiLiephwf5/B3f4vnKwUYj2vIu6PD1V5f+PdtFoDDeODPm68Ff+3f302r1sB3z9vrH8jhCxAxLUE8+Hh102ndFaH7WPgP5CxDoCGH8pLW0azr47XtjcfdUeHqfcJ/LKc4Agxf+G9NvzIEqh/fhD5C8I/qVAWIuClB1j3/lWFQ+7HgH9vpCtD5H//b+zTcgn8OT1GAzkopWgIPAAAAAAAAAMDO/QuwFdmG+xz1wQAAAABJRU5ErkJggg=="/>
+ <image height="152" mask="url(#c)" transform="matrix(1 0 0 1 -168 -16)" width="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAEPklEQVR4nO3c627bRhAG0I+2XCdtir7/iyaxHZn9QY45WktpEKSWrJwDLHQjdQFmdmeXaycAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMD/bTr3F/jV5nk++ZumaZrf8rtw+d59ArSAn5LctDZl+33PSfbr7Zxklgwk7zgB1sC/SXKbZJfkjyR36/2b8fAswf8tyUOSx/X+vLZXJMjv4d0lQAv8XZL7td1lSYQphz1/sgV4Bfs+h4nw3M6pY5+zjRbPiYS4Vu8qAeZ5rsD/I8nH9fY2r0ueU+q4GiFqJOgBP7fH35I8re15mqb9L/w5XIB3kQBrrz9lCfgPa6vg74F/7PfUnKBGjCqR9lkS4HOWAK85Qk+Eeq6S4DFLIhgNrsTu3F/gv7Tgv8vS61fwH5vsjre363lVKtXvreBO1t49Wyl0k8O5wfgZT/M87yXBdbjoBGjBv8vrnr8HZg/8fs59lqS5z1b2vNT16zG7LElwuz5Xr9X7JdsIUufO8zwbCa7ARSfAqpcvNdkdg7+3WhX6kOSvbKNFTXJrSXTO8RGkl1HjEmv/bK7AxSZA6/1vcri8OQb8zdCqVPo7SxLs8vp6wLf18WOWRBjfsxwbWQT/FbnYBFj1nvdYT1+v3WWbIH9K8s96W71/n9TusyyBfl4/47E9X8mQ4X5/jity6QnQjRe97luri2D3WcqeT1mSodf1+xxOjitxPuYwAar16wLlZQ7wq38c53HpCdB7+/ssgV3r/1UWVTDvWqsRY1SjwGOWUaCv/Byr7SsJ+txhH1sprsbFJUCr/XsP/efaKvhPbXfowfothys/tY7/JUv587AeU+1Yzz6+3+OJ43inLiYBjgR+L21uswXquBLTb/fZLlolW/1fWx8q8B+zJcRDtmTowd3nDU/9GL3/9biIBFi3OPTJbF/uTLaJa93/0I6p5cx+8WrKttLTA733+JUQX/K6Zx83z73sGxL81+WsS3rDxrYK/HG9vS93Vq1/l+PJ0pdC73K49t+XQL9mCfyv6+P9keOe1tcfkuztA7pOZ0uAtdfva/xV1/cNa8cudB3b8z8N5/TtD3frR9aenq9r6/t/+hJp7/VrE5xe/0q9eQIMtf6u3Z4K+u9d9T11Fbfev1qfH/RNb31UqFLp5RiBf/3ONQcYy5y+D79vV+7P/UgCjPv5e1D3+UEd08uelzmAwP99vGkCDNsb+tXVMeB+JOir1UWrcQ//uNf/JAH/+zrnKlAP/B7w4/r+eE5ymDx90lorPP6Six/ypgkwTdM8z3Oy7cVPTpc89dq4Aa1PWKv5Y3d+yllWgVopNG5DGC9uJVv50mv2g7JJ0POzzr6193v/x+cYwQ4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcBb/AqrhdhxFC+qEAAAAAElFTkSuQmCC"/>
+ <image height="152" mask="url(#d)" transform="matrix(1 0 0 1 -168 -16)" width="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAMMElEQVR4nO2dyXNbxxGHP5CgxFWUZEm2LNmRrCV/eyqXHHPI2ZVrKlWusjbKkiXZ0cKdEhcQOfR0TePxYaFAAO8Bv69qCuACvEewfzPdMz09IIQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBiZmlM+gZmnAb5f9AA2qGJMSABTIYGMBeaC6ENnAIn6VGMGAlgvLjhzwPN0FwIbvwHwNGE7nGmaE76BmYIN/wF4BJwGVhMz5vpZy3gCyYUCWAMSACjx3v9Jmb0S8AKsAyspseF9PMTYD+9ZgvFAiNHAhgtDXKvv4QZ/BXgGrCenq+SR4ETYDe9dmPcNzuLTJMAYjxThZ7TjX8R6+XXgW+AG+nxevreKjYyuAu0g8UCP1ONv2OqmRYBNLC/pYEZzynZeCZhRNH4vcf/NrSbZAGscFYAX8hBsRgh0yCAaGzzmBvh04iTEIPfzxJm/LeA28Dd9PgdNgpcpTMGcIPfAz6N4T4F9RdANLar2N9ziM2gnDB+McT7Wcd6+x9T+wETwK10r1fonAXydQB/raaox0CdBVA0tttYT3oAfMaEME4xlN3PX4Cf0uNdzPivYW7PEnn2Zy7cxzFaDR4bdRaAuz3rmFtxH/Old7GpxIPQBhXD1xpdmfHfBx6k9kO6xxj0utvjK8Gk+ztCLtDYqKsAGpjrEI3/MVkALoJdzKceVAwtzp+P0834HwEPgXvpHq9i/r67PDEFgnTtQ+AD8DsKgMdCnQWwjM2muPE/xoxrL7RdbFalnxgOyUJoMbgYehn/4/T8Ntn4F9LvR8MnXe8Q2AR+A172uKa4QOosgDXgDuZjP0qPHgPsp+Yi6CeGfbqPDG3KxdDP+B9w1viju+O4378LvAWeYSIQY6DOAoiB5o/peRMz4C9YIHyAGXo/MeyQxdArgI5imKO38X+PBbwe7HqgG3Hj3wPeAy+AJ8A7NAKMhToLYBlbTLqRHj2l4BQzqiPMiAcRw3b4uugm+ftEMbQwo+5n/Mt0zvJE3Pj3gY9Yr/8UE8HWcB+PGJS6CgByjo33ru30Pc+0bGHGOogYXABRFL3EcIzNQPUz/gXK5/Pd+A8wv/93zPV5kp4fDPnZiAGpswBOMGPeTm2JPDJ4nn0UQi8xRIPfLbQYVLsQvmAxyDDG/5ls/M+BX7EEuI/pXsUYqKsA2pghfgDekF0fn86M040+UvjMTpkYXAgeELvB79ApiiiEqwxv/G8x43+SHt+l62sKdEzMT/oGvpIG5oK4gFup+c/izit/9OdNsuvk7tISOU9/NbW11K6Q05bXwuMdhjP+N+Se/1fM93+PjS4KgMdEnQUwjxnKEWZYx+QZGv8dF0Lcg+uvHUYMa1hqwzDG7zM+TzDX5w9sdGmVvF6MiLoKwDkhpw/4LI0vZPlUZVxxLQoiiiFuV4xbFpc5K4YrWAA8rPH7rM8fmLt1PPxHIs5DnQXQ5qwv76OB+/pQbvzFFis0uLvkI8NlysVwneGN33v+7XT/cn3GTN0FEAPbKIRuLlHZiED4+aBi8O2NvYwf+vf878jGL9dnAtRZAJBTE3wG6Jg8EsSFK59VicZfTEaL9BODu0i9jL+Nzej0cnu2saBXxj8h6i4AyALw0aDMJXIhQHlg7N8vo5sYmj1e4/wPm+cvM/4tbHSIo5QYM9MgACeOBMXguOgSdYsB+lEcPfrdj8/xlxm/9/wy/gkyTQKA7BKVxQXdXKI58s6si76X/6RWZvyeXCcmSF1XgnvhyXAxQPYMUU9/8MfD9HuLmE9/kbSxlernyPgryzQKAPIo4AbuIvAcIBfACfYZXMfm9y+afSwGkPFXlGkVAHTm7hdHA48L5rFFrXsjvI8d8myPjL9iTLMAnKJL5FOOTSyff4fRZF82sLWCefKWSxl/xZgFAUAeDY6woDfuCR7lTIxv1vH9CqJiDDL1Ny24AcZitcvkinIXTQMrjPVDuo6oIHUVQLcV3F7MYTM9a1hx2u9Su4at7I6CG1hpFB8FRMWo4z/FMzhjmytpxYWuS1jA+y0W9N7HNtPfwkaBfpzXhfGN+w+xvQOjEpkYgrrFAA1yLk670JxiuUNPi17FjN+3MT7CKkrcpP8agGd1uuAGZSld4z7wX2wmSFSIOgnAjd9TkmMOUJkY4tfzmDtyj7PbGNfT+3YjpjQvYKPFoCNnE3OzHmFu1xZKfKsUdRLAPLkUylXyzE5ZFbdTOgWygLk73er2dOvVi/n8bvyXGSwGmUv3+jBd7xUSQKWokwCamA//PebKxHl9F4Ibfvy6hRnsA4bfwL5KLm84SDYoZDfoJ8wN0saXClEnASxgboTX2ndDLwqg+PUJ1nP/FTP+O3zdTq4NzF26jH1uKwz2+ckNqjB1EsAlzI+/i/nyvrgUDb9MBC6Ah+m11+lt/PGs3i3yZpan5DqfXpFikDTq6AbdAV5jwhIVoE4C8NSFm1iP6udqQacQohg8/2cRc0O+ob/xF92euJklHmt0KV1/kHjAr+9ukEqfVIQ6CcDz9i9hBrVGLjzrQjgtaa30mpuY21Jm/C6gI8z4t7D9usUN7GvkTfGx6G2vrZFw1g3aRG5QJaiTAGI9zX1yCcRlsl/u52zF2aDT9LM1evf8X9J7b2PGv4EZfnEP70p6L3eFXAS9Pss5bPTyGOQVcoMqQZ12hC3S6b8XN6jH8iXFr711+3uPsKzQT3Qa/zM6N7P4DI6vLC/T6Q71igfmMJG9SE0CqAB1EkCTPA8fF7y89/W2EB4XCt/rxh4W7L7BTmd5lprX7YmbWdx1cf/fD7yLpz2W4cegvsVKIW6iOGDi1EkAPuPie3292kM8sKJYwiTmCfXy0T8Cv2DG/wLbxviSs8Yf4wrIWaVL5CNPu13L85I2sbjiLaoCPXHqJADIcUCs/ePVHorlEH1kGKSCwxvg31iPv4EdVhHr9vhmluJsk7tCXjUuxgRlyA2qGHUTQLHaQ1ldUC974sbph9L14gXwL8zwX2P1fHbovo0xTrX6dbygrk/PlonA3aB3yA2qBHUSQOx9u9UEjbV//BzhOE3ajafAPzC35APZ+LvtFov7jP1axXigTHjuBm1hAniD3KCJUicBONH4ogiKxXHh7KptN54Af8NmgfYYbKtkvA/SNeIus3gkamQuvb8XzVKK9ASp0zqAU1btweOCz6Ed07nlsVcacwMLhP11g+wT9rN9t7GefBlL1isKoPgZN7FNOPfS7+pAvAlSRwE4vQpgfU7PfdXWpyi7LYQtYotg/n6D+uWtdK1P2OLWarqmxwK+UFccaZexBbHVAa8jRkSdBQCdBbBigOxlSFaxXtn98jXK/+YVOkumnIcTbGX6PTaD5KIrG318Fus03csgWzHFCKm7AODsSe7uErXIbknM3SnbALPG1++PdqPexaZO/ewAHw3iqrVnme6k+5uGz7/WTNM/oOgSnWKLWX6m1yqdPXJ0hdxQD4e49hE5HohHKnk8sEJOsX6HxRxKiJsw0yQAODsa/InNtqxiyWgr5ClKjwfa5LyhvSGuXYwHoit0OV1nm5xi/YqvF5y4IKZNANApgh3yVsZ1chanpyzMh9+9iLIlLfL5xS/pFEEbc5E80e4FFjuICTKNAnB8mnIT6239dEePB5qY0R9iRturMsSglMUDPivUwlaZn2EC+A0JYOLUcSHsPBRLp/hi1QrWK89ho8SfwN8xF+WiruuzSl7OpY31+jHLdFSFecWATLsA4GwGZ8zbmcPyfjaAf2IGeREU9yp7DtAGOcvUT4c8LX8LMQ5mQQDQmcHZIB912sBckWfAzwwXBBeJK9Wev/QaG210OmRFmBUBFF0hT16LB9n9wsWnJ8dR4IicaKfTISvCrAgAOgtmeTxwggngOdY7H4/gujGDdRedDlkpZkkA0NkjNzCDf4md4zWqglVxhXrUB3KIczJrAoDOeOAIWyfYxNySURlm0QUTFWEWBRCD0yOs599n9AGpDL+CzKIAoPNUeU+DloGKmcL38p73qCUhpgYZvxBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEmBT/B3UepoTnVodaAAAAAElFTkSuQmCC"/>
+ <image height="152" mask="url(#e)" transform="matrix(1 0 0 1 -168 -16)" width="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAIbklEQVR4nO3da4xcZR3H8W93bItKJYBaipoQDRJQQkAUQoQK0ZiUKooi8UK5lNBEBWJ4AXKJxUTEFLRY70nFCxVoxRjWKwJGLSqmihqL1KKIQRAKRaGt2tv64neO025nZ+bMnNltdr+fpOnuzrk8L/7PnP95riBJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJksS0iS7AFDQdeDMwHzgS2B94ClgFLAc2T1zRpMGZBVwOPAGMABuBu4CVwM+Lv90HPH+iCigNwjTgHOBJYCcwDMwjT4JdzSOV4MLxLJw0SAcDPyKB/VPgmA7H3wv8eNCFksbDXOAx4FlgEd29b60E/jDIQknj4SxgG3A/cESF835CnhRS1y4GzgcOnOiCFC4iuf4d5MW3WzOAfwKfHUShNDldSfLrEWAr8H3gPOCACSrPpUVZVpGArmJ+ce68ugulyekSEjA3AkcD1wAP0qwM3yOtL/uPU3nK4F8BNHo4/zukiXR065C0hw+SNGMV8JxRn70KWAysIwG5HVhNUqUXDqg8/Qb/a4vzL6uzUJqczgV2AN9iz+AfrawMfyIB9h/SDr8A2K+m8vQb/EPAPcDjwL41lUmT1Fkk+H8AzKx4blkZ1rNnZXhBj+XpN/gB3l9c4309nq8p4kSSzmykvxafacDxwCeBv5Hg+zd5oryb7r+F6wj+VwObyMu71NYQafXZTjqLDq/hmtOAE4ClwCMkoLcA3wTOZOxxOXUE/37kPeXvwOwer6EpaC7wKAnU82q87hDweuAGEpTlk6FMk8onQx3BP5MMhNta3FOq5MU0x9h8DXhuzdfftTI8SvPJcH/x8630HvwN8oTZAby375JqymqQl9odwG+AVwzoPmVlWE2z020LzSdDleHLDfLkcNSnajOfTCh5GjhtQPcoO91WAm8CvkBzXP8zJKhPo32zbAP4enHOFQMqp6aoQ4Bfkc6xJXTuH6iiDP5bR123AbwR+BKwoTjmbWNcw+DXwM0ElpEg+xnwkhquOVbwjzaDVIIVLT4z+DWu3k5GVW4gc2971W3wl75Ixv7v+kJu8GtCvBL4HUmJrqV6i03V4IekQyPA6cXvBr8m1D4kPx8B7gYO6vK8XoIfEvCPAzdj8GsvsoAsNfIInTudeg3+0ueLe92Mwa+9yOHAWjJNcTFp2x+t3+CHZhpk8GuvMwu4hQTn7ew+UaaO4G8ANxXX+X3vxZQG6wLgv8DDwHHUF/xlzv9Lkga5wJX2WscCfyEpUZ3BfwXwhuLnM/oupTRAV9HM11fQ22ysVq09Q2Q06coayigNxK5pz4fIsOQHyGK13WrX1LmMpEFOcdRep1XOfxLNOQYLu7hGp3b+k4rPzuy3sFKd2r3wHkTW5RwhA+rG0k0nV5kG3dZPYaU6XUznmVwNsjLbCJmA3+rzcjz/pR3udwN5opgGacItoLmOUKdxQUNkttkmspJEqUrwQ3qdR8hEe2nCnEyaO++g++UKZ5MUZh3pQKsa/JAJ9w+T1Sakvk0nK7tV2dbpYOAfZBhE1VRkLqk4t1A9+EufIhPre113SALyzT1MgnAnGeu/jmwtNAx8BbiebEG0CHgH+ea/l6QyR/V4310X360a/JAlV0Zw8vteq86pg4Myg7SmnEqCfBNZ/fnA4t8c0n5/AGMvR/7b4ryNZKuiJ8k84mEycrOVBs21h7YDd/ZQ9l+QNOgMWs8Wk9oqv/l3km/2bo6fQxab3Qj8kYz9+TBwHVlJ+naywsMW2gd/mfYsJsMm/kpvy65fT5ZhrGstUk0ht5EgXFrxvLLJ87g2x6wmL8ajtXrhfR0ZQDdM9a1lj2fsZlWprWPJYrbbSH7fahx/K3+m82ZztwO/HvW3dq09ZaW6pMsylKYBDxX3kyqbRbPn9W7SstPOkcWx53Y47sskPy/NJP0E7V54V5HKeGKHa4+2hDxBxmuzDk1CC8iqCxvIC/FYLiRB3KmiLCEvxpBKdidZZe6iNufsS94rHqP7OcaQFGoEOLvCOdIeDiPLIO4kQw1adWx9jLx0dnIZCcqjyGrT2+j81IA8YTaTxW2rrDbxINkGSerLTBL8O4E1wKGjPl9Mhjd32kDjAlIBNpORoFXSmoXFuR+pcM4ninJN1AZ+mmTeStrzn2H33VVOZff1eVo5geYeYvdQLZ0p3UhSpm4X3zq2uN85PdxLaullZGPpEWA58DySltxLFq89nawPVB67cJfjnyr+P7nHe+8D3Ffcp9tlGNcD3+3xflJLDeBq0lu7lmw79HJSCcotU7fSHNKwnuwyWX4jv7OPex8K/Iv0+HYz0O7jmAZpQE4hufwmUgkaZNPpq4CPAh9g9+HNc0gF6KaHuZ130XkSTemY4tg6d7aR/m8OqQRrSTrUzgwSjJfXcN/PkJfydu8dpfW4KZ4GaC5Jh5Z3cewzZKxOv6aTkalPk/SrnWtIk+uLariv1NLV5Nv9PR2Oe4gMpa7DIeTFeg3tm2CPJmU7v6b7SnsYIh1Vz5Kl0seyhgxwq8t8kgp9bozPG6T5dISMLZIG5qVk2MQaxm6h+SFJXep0La1HfzaAr1K9A03q2TzyjXzdGJ9/g3SI1alBxhVtAo4o/jaDbI3qqtEad58mleAtLT5bRnqT6zabtEY9QGatfbsog2mPxt1MMoDuCfYcIbqYDGfodp5BFaeQ1qiNxT186dWEOYy8EN/F7sG+hKQlBw7ovleSSuBsME24s9k9B19E0pJh0o4/CENUnzwjDcxNpBNqKc3g7zRsWpo0ZpGhCCMY/JqiXkNWmzD4JUmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEn9+h950gu2YS2pzwAAAABJRU5ErkJggg=="/>
+ <image height="152" mask="url(#f)" transform="matrix(1 0 0 1 -168 -16)" width="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAEvUlEQVR4nO3dbVPbOBQG0JsApbv//7d2S0ni/WDf+FpRAu0seVmfM6Mx0JCG4T6ypMgmAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/p82t34B/LlhGD71+9tsNsNXv5ZHJQAPqBR+/f31fpdDPQrCKQF4IE3hZ4vm4+PDy/HYhGBJAB7EVPxt28ZpGI7f0rTD1ISgEIAHUIp/W47bOA1A/X3Wws/jfjoehGD0fOsXwGVN8T81xxqAbefbs/iz8I9PG/MQadUE4I51iv85TkPQnh0iluP/w/TYfXnsMAyDoVAIwL3Lwn6OufhrCD46A9Sx/1PMATnOB77wtT8EAbhTpffPon+Jufh7ATi3DFrnAOkQEbuveu2PRADu21OMhZ/tUgBaOc7fxmkA9hGxGUdB6x4GCcAdmnr/HPq8RMS3OA1Auwy6eIo4XQXaln97nj7ff+XP8QgE4D5lcb9ExGt8LgDtG185zt/EctUnA9B+3yoJwB0p7/Rm7/9aWg3AtmnHp4h52TPbobSIeUK8DcMgAbgXnSXPl4j4HhF/xRiAuhJU5wFtT54T3F1EvMcchBquSxPnVRGAO9Cs+GTxv8ZY/BmAc5PgLOg69NlFxK/p6+/1vyof1xA4A3AbZcLbTnqz+P+O/hyghuD4dDH29rtYBqMOgep8YBMrHwYJwA2V4s+1/m9Ty97/+9RyCPQSy3eC60Q4C3gfY6+fX8uhUMQchNWv/iQBuIFmZ2cd8tSWAeitAvU2w9V1/4i5+PN9gJwL7GIZgFXPAwTgys7s73mN5YT32/R5Fn1tbc9fCz7PAu0wKecF7zFPjBcXy6yVAFxfu9JTx/vZ4+fXs9jbFZtLKzjtTs9DzIX/HvMKkb1AIQBX1az2ZJFnz197/+zt6+7OQ/S3NbQ7P+syaA573mNcFfoVyzPA6q8SE4Drq8WfE97s+XOym0OXHLu3W53bi2DqEmiO82vRv8Vc/Nn7H+L0bLE6AnBdm1gudWYA8uO6zaEuYe5iWeC97c91hSeHOz+nVgOQZ4XVF3+EAFxNs+TZ7vDMzWntmv6h+bieDXq9fzvZ/TG12vvvy+NXPfyJEIBrq+/29rYzRCx75Vrc7VVfm85j6vDnLcbi/yfGAGTxH4c+ay/+CAG4hbp+H3H6Tm39t4jTgr904XtOfnPc/yPGIdBJ8f9nP82DE4Db6PXaeXY4F4D2+/NYd3vWye9bjL3/WywnvuOT6v0jQgBuoRZ93ZMzRH9I1O71z2Mbol4Afk6fH4tf4S8JwHW12xOeYjl8qZPh3n1+8ti7309v+TPH/or/DAG4ks1mMwzDUJcp6+1K9nG6GtQOhSL6lzrW3r9eB3Bc81f45wnAdWVP3y5r7qK/zfnSxe6HpuUZoL4LrPg/IADXVy9NbG9zeG6nZ9UbAtVhUL394SG4aNVbYW/hzE1u6w7Rusvzo1uetCFYXP+r9/+YANxA5/7+9XjphrfHp+g0b3D9AQG4seavvNQg9LZAR1xeEtXr/yYBuDOf/Osv7XYJhf+HBOBO+ftfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABf4l90JPJqSIYhvQAAAABJRU5ErkJggg=="/>
+ <g clip-path="url(#h)" mask="url(#g)" transform="matrix(1 0 0 1 -168 -16)">
+ <path d="m 68.078125 24.140625 c -13.199219 0.945313 -25.613281 6.613281 -34.96875 15.96875 c -9.804687 9.859375 -15.511719 23.070313 -15.96875 36.96875 c 0.945313 -13.199219 6.613281 -25.613281 15.96875 -34.96875 c 21.480469 -21.480469 56.300781 -21.480469 77.78125 0 c 10.0625 10.070313 15.828125 23.65625 16.085937 37.890625 c 0.273438 -14.929688 -5.53125 -29.328125 -16.085937 -39.890625 c -11.28125 -11.28125 -26.902344 -17.105469 -42.8125 -15.96875 z m 0 0"/>
+ </g>
+</svg>
diff --git a/src/client/cockpit-client.svg b/src/client/cockpit-client.svg
new file mode 100644
index 0000000..c146993
--- /dev/null
+++ b/src/client/cockpit-client.svg
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="128px" viewBox="0 0 128 128" width="128px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <filter id="a" height="100%" width="100%" x="0%" y="0%">
+ <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
+ </filter>
+ <mask id="b">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.154911" height="128" width="128"/>
+ </g>
+ </mask>
+ <radialGradient id="c" cx="77.536842" cy="-172.403473" fx="76.588287" fy="-172.156464" gradientTransform="matrix(0 -2.087957 4.629211 0.00009255 862.290587 284.865191)" gradientUnits="userSpaceOnUse" r="60">
+ <stop offset="0" stop-color="#62a0ea"/>
+ <stop offset="0.253849" stop-color="#1c71d8"/>
+ <stop offset="0.554184" stop-color="#1c71d8"/>
+ <stop offset="1" stop-color="#1a5fb4"/>
+ </radialGradient>
+ <mask id="d">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.07214" height="128" width="128"/>
+ </g>
+ </mask>
+ <mask id="e">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.2" height="128" width="128"/>
+ </g>
+ </mask>
+ <mask id="f">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.3" height="128" width="128"/>
+ </g>
+ </mask>
+ <radialGradient id="g" cx="66.2463" cy="88.106583" gradientTransform="matrix(-0.00616859 1.125713 -2.188675 -0.0119934 234.313391 1.273906)" gradientUnits="userSpaceOnUse" r="8.728516">
+ <stop offset="0" stop-color="#ffffff"/>
+ <stop offset="0.488048" stop-color="#fbfbfb"/>
+ <stop offset="1" stop-color="#deddda"/>
+ </radialGradient>
+ <radialGradient id="h" cx="55.734493" cy="69.732521" gradientTransform="matrix(1.30894 1.562048 -0.483034 0.404766 10.638455 -73.366668)" gradientUnits="userSpaceOnUse" r="18.540045">
+ <stop offset="0" stop-color="#ffffff"/>
+ <stop offset="0.370941" stop-color="#ffffff"/>
+ <stop offset="1" stop-color="#deddda"/>
+ </radialGradient>
+ <radialGradient id="i" cx="66.320023" cy="67.443039" gradientTransform="matrix(2.622328 2.265688 -16.346331 18.919404 999.146607 -1377.449098)" gradientUnits="userSpaceOnUse" r="3.671024">
+ <stop offset="0" stop-color="#ffffff"/>
+ <stop offset="0.456886" stop-color="#f2f1f0"/>
+ <stop offset="1" stop-color="#c0bfbc"/>
+ </radialGradient>
+ <mask id="j">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.092655" height="128" width="128"/>
+ </g>
+ </mask>
+ <mask id="k">
+ <g filter="url(#a)">
+ <rect fill-opacity="0.2" height="128" width="128"/>
+ </g>
+ </mask>
+ <clipPath id="l">
+ <rect height="152" width="192"/>
+ </clipPath>
+ <path d="m 21.574219 107.425781 c -23.433594 -23.429687 -23.433594 -61.421875 0 -84.851562 c 23.429687 -23.433594 61.421875 -23.433594 84.851562 0 c 23.433594 23.429687 23.433594 61.421875 0 84.851562 c -23.429687 23.433594 -61.421875 23.433594 -84.851562 0 z m 0 0" fill="#c0bfbc"/>
+ <path d="m 21.574219 105.425781 c -23.433594 -23.429687 -23.433594 -61.421875 0 -84.851562 c 23.429687 -23.433594 61.421875 -23.433594 84.851562 0 c 23.433594 23.429687 23.433594 61.421875 0 84.851562 c -23.429687 23.433594 -61.421875 23.433594 -84.851562 0 z m 0 0" fill="#deddda"/>
+ <image height="152" mask="url(#b)" transform="matrix(1 0 0 1 -8 -16)" width="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAFOElEQVR4nO3d61LjRhAG0DZ7yfu/7iag/GGg3e6RTVKFLj6nSiVjG8FS3yeNRoKNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAedNn6Gzi6ZVnan+Hlclm++3vh6xTgQbOgP0oh9kkB7lgJ/iM/u5vQK8K+KMCKEv76s/ovBVgilGBPFKAxCX5d19dvNtM8XkIJdkUBkgeC35Xg0QLURQl2QAHeNeGvwb80z+f1zSbjds//FkqwKwoQV+FfC/29ItxstlneQgl25efW38DWSvi75WXltSiPPzbbLG/p9bfyXjby9AV4Nwt9XndFyJ/bzfjk8NeijCNBLMtycRTYxlMX4H3vPwt+XWZHg0jreuI7Qn4p7+meYwNPW4BJ+MfyIx4rQUR/BKgnvW8R8VpeW5rP45s9ZQGacX8O/o+4LkEuQ3cUuNp0WucCvJb3KMFOPF0B7oT/Z3yWIJehOxJ0w5/ZrE+k94xtvOVtLMtiNmgDT1WAEv661x8F+Jkez4ZD92Z+uqFPpOfq8EvwN/IUBWgucuXhzAj/r/gsQC5CNwyaFSAHvwt/LREbO30BVub5857/V1pqAepQqCtADv1a+NeuJTgKbOC0BZjc2tCN+XP4f8d1CepJcT56XH25+Az9I+HPRvidB2zglAWYhH+EcAS6C//vuD4SdCfCNch57/8St9OdXehj8pyLYt/sdAVYGfLUk94R8hH+v2J+BBgl6E5ex/Iat6W4N1XKxk5VgEn46wWuOvT5XZbZEGgsefv1VodIH7+U525uh2Z7pynAnfB3U52zoc9Y1k6Asxz+e8Gvd4Iqw8ZOUYCV8OcT2DrF2YW+G/93w5+PL50e1yFPDv1rfJ4g5xJ02+EbHb4Akyu7dZqzhv/R4M/2/nnacnZBLE+H1ilSJdiJwxfg3docfw59Xa+N97u5+qEbxnShf22WtRKYBv1mhy7A5Apvneefhb6Gf3aPzzCGOPWGtxHsf5p1XXIJbs4BhP/7HboASTf2n530drc6zK7s1q8xXsvvyQX4u1keCb/gb+QMBVib8uzG9LOLW92vLkbc/02vHP4/cT/8fi94Rw5bgAdudeimQLtZnYjb8M/KUF97i+shzp+0zEpg+nNHDluAd3XM3hVg7Q7MWejX7visN77NCjBKMMLf7fmVYGNHL0A1uw2i6vb4l7idy+9OeutsTzf+z+HPBbgJv6HPts5SgLXZm3rSml9/ic/w18+tw6OI2190GUeA17ge8vyJz/DX+f+P70n4t3eWAnQzKvnWg3yjWn4t39sz1kt5XLeXt5mXvMcfR4Q68/Px/Qn/PpylAEM3Rh+3J1/iOvj5nGB21MjbrNutF7rq/P9s3C/8O3KmAszG6Pm1tfv667bqNvMR5d7V3nbOX/D35+gFmJ2gXqL/ZfTuz5t8Zbuzo0C3Fv4DOGwBLpfLkq4F1PBHej4Hf/bHrapHjgCzIuT3mercucMWIBmzON3Fq1yAGvz/UoA6xKpl6KZXjfl37AwFiLiephwf5/B3f4vnKwUYj2vIu6PD1V5f+PdtFoDDeODPm68Ff+3f302r1sB3z9vrH8jhCxAxLUE8+Hh102ndFaH7WPgP5CxDoCGH8pLW0azr47XtjcfdUeHqfcJ/LKc4Agxf+G9NvzIEqh/fhD5C8I/qVAWIuClB1j3/lWFQ+7HgH9vpCtD5H//b+zTcgn8OT1GAzkopWgIPAAAAAAAAAMDO/QuwFdmG+xz1wQAAAABJRU5ErkJggg=="/>
+ <path d="m 102.890625 24.109375 c 21.480469 21.480469 21.480469 56.300781 0 77.78125 s -56.300781 21.480469 -77.78125 0 s -21.480469 -56.300781 0 -77.78125 s 56.300781 -21.480469 77.78125 0 z m 0 0" fill="url(#c)"/>
+ <image height="152" mask="url(#d)" transform="matrix(1 0 0 1 -8 -16)" width="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAEPklEQVR4nO3c627bRhAG0I+2XCdtir7/iyaxHZn9QY45WktpEKSWrJwDLHQjdQFmdmeXaycAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMD/bTr3F/jV5nk++ZumaZrf8rtw+d59ArSAn5LctDZl+33PSfbr7Zxklgwk7zgB1sC/SXKbZJfkjyR36/2b8fAswf8tyUOSx/X+vLZXJMjv4d0lQAv8XZL7td1lSYQphz1/sgV4Bfs+h4nw3M6pY5+zjRbPiYS4Vu8qAeZ5rsD/I8nH9fY2r0ueU+q4GiFqJOgBP7fH35I8re15mqb9L/w5XIB3kQBrrz9lCfgPa6vg74F/7PfUnKBGjCqR9lkS4HOWAK85Qk+Eeq6S4DFLIhgNrsTu3F/gv7Tgv8vS61fwH5vsjre363lVKtXvreBO1t49Wyl0k8O5wfgZT/M87yXBdbjoBGjBv8vrnr8HZg/8fs59lqS5z1b2vNT16zG7LElwuz5Xr9X7JdsIUufO8zwbCa7ARSfAqpcvNdkdg7+3WhX6kOSvbKNFTXJrSXTO8RGkl1HjEmv/bK7AxSZA6/1vcri8OQb8zdCqVPo7SxLs8vp6wLf18WOWRBjfsxwbWQT/FbnYBFj1nvdYT1+v3WWbIH9K8s96W71/n9TusyyBfl4/47E9X8mQ4X5/jity6QnQjRe97luri2D3WcqeT1mSodf1+xxOjitxPuYwAar16wLlZQ7wq38c53HpCdB7+/ssgV3r/1UWVTDvWqsRY1SjwGOWUaCv/Byr7SsJ+txhH1sprsbFJUCr/XsP/efaKvhPbXfowfothys/tY7/JUv587AeU+1Yzz6+3+OJ43inLiYBjgR+L21uswXquBLTb/fZLlolW/1fWx8q8B+zJcRDtmTowd3nDU/9GL3/9biIBFi3OPTJbF/uTLaJa93/0I6p5cx+8WrKttLTA733+JUQX/K6Zx83z73sGxL81+WsS3rDxrYK/HG9vS93Vq1/l+PJ0pdC73K49t+XQL9mCfyv6+P9keOe1tcfkuztA7pOZ0uAtdfva/xV1/cNa8cudB3b8z8N5/TtD3frR9aenq9r6/t/+hJp7/VrE5xe/0q9eQIMtf6u3Z4K+u9d9T11Fbfev1qfH/RNb31UqFLp5RiBf/3ONQcYy5y+D79vV+7P/UgCjPv5e1D3+UEd08uelzmAwP99vGkCDNsb+tXVMeB+JOir1UWrcQ//uNf/JAH/+zrnKlAP/B7w4/r+eE5ymDx90lorPP6Six/ypgkwTdM8z3Oy7cVPTpc89dq4Aa1PWKv5Y3d+yllWgVopNG5DGC9uJVv50mv2g7JJ0POzzr6193v/x+cYwQ4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcBb/AqrhdhxFC+qEAAAAAElFTkSuQmCC"/>
+ <image height="152" mask="url(#e)" transform="matrix(1 0 0 1 -8 -16)" width="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAMMElEQVR4nO2dyXNbxxGHP5CgxFWUZEm2LNmRrCV/eyqXHHPI2ZVrKlWusjbKkiXZ0cKdEhcQOfR0TePxYaFAAO8Bv69qCuACvEewfzPdMz09IIQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBiZmlM+gZmnAb5f9AA2qGJMSABTIYGMBeaC6ENnAIn6VGMGAlgvLjhzwPN0FwIbvwHwNGE7nGmaE76BmYIN/wF4BJwGVhMz5vpZy3gCyYUCWAMSACjx3v9Jmb0S8AKsAyspseF9PMTYD+9ZgvFAiNHAhgtDXKvv4QZ/BXgGrCenq+SR4ETYDe9dmPcNzuLTJMAYjxThZ7TjX8R6+XXgW+AG+nxevreKjYyuAu0g8UCP1ONv2OqmRYBNLC/pYEZzynZeCZhRNH4vcf/NrSbZAGscFYAX8hBsRgh0yCAaGzzmBvh04iTEIPfzxJm/LeA28Dd9PgdNgpcpTMGcIPfAz6N4T4F9RdANLar2N9ziM2gnDB+McT7Wcd6+x9T+wETwK10r1fonAXydQB/raaox0CdBVA0tttYT3oAfMaEME4xlN3PX4Cf0uNdzPivYW7PEnn2Zy7cxzFaDR4bdRaAuz3rmFtxH/Old7GpxIPQBhXD1xpdmfHfBx6k9kO6xxj0utvjK8Gk+ztCLtDYqKsAGpjrEI3/MVkALoJdzKceVAwtzp+P0834HwEPgXvpHq9i/r67PDEFgnTtQ+AD8DsKgMdCnQWwjM2muPE/xoxrL7RdbFalnxgOyUJoMbgYehn/4/T8Ntn4F9LvR8MnXe8Q2AR+A172uKa4QOosgDXgDuZjP0qPHgPsp+Yi6CeGfbqPDG3KxdDP+B9w1viju+O4378LvAWeYSIQY6DOAoiB5o/peRMz4C9YIHyAGXo/MeyQxdArgI5imKO38X+PBbwe7HqgG3Hj3wPeAy+AJ8A7NAKMhToLYBlbTLqRHj2l4BQzqiPMiAcRw3b4uugm+ftEMbQwo+5n/Mt0zvJE3Pj3gY9Yr/8UE8HWcB+PGJS6CgByjo33ru30Pc+0bGHGOogYXABRFL3EcIzNQPUz/gXK5/Pd+A8wv/93zPV5kp4fDPnZiAGpswBOMGPeTm2JPDJ4nn0UQi8xRIPfLbQYVLsQvmAxyDDG/5ls/M+BX7EEuI/pXsUYqKsA2pghfgDekF0fn86M040+UvjMTpkYXAgeELvB79ApiiiEqwxv/G8x43+SHt+l62sKdEzMT/oGvpIG5oK4gFup+c/izit/9OdNsuvk7tISOU9/NbW11K6Q05bXwuMdhjP+N+Se/1fM93+PjS4KgMdEnQUwjxnKEWZYx+QZGv8dF0Lcg+uvHUYMa1hqwzDG7zM+TzDX5w9sdGmVvF6MiLoKwDkhpw/4LI0vZPlUZVxxLQoiiiFuV4xbFpc5K4YrWAA8rPH7rM8fmLt1PPxHIs5DnQXQ5qwv76OB+/pQbvzFFis0uLvkI8NlysVwneGN33v+7XT/cn3GTN0FEAPbKIRuLlHZiED4+aBi8O2NvYwf+vf878jGL9dnAtRZAJBTE3wG6Jg8EsSFK59VicZfTEaL9BODu0i9jL+Nzej0cnu2saBXxj8h6i4AyALw0aDMJXIhQHlg7N8vo5sYmj1e4/wPm+cvM/4tbHSIo5QYM9MgACeOBMXguOgSdYsB+lEcPfrdj8/xlxm/9/wy/gkyTQKA7BKVxQXdXKI58s6si76X/6RWZvyeXCcmSF1XgnvhyXAxQPYMUU9/8MfD9HuLmE9/kbSxlernyPgryzQKAPIo4AbuIvAcIBfACfYZXMfm9y+afSwGkPFXlGkVAHTm7hdHA48L5rFFrXsjvI8d8myPjL9iTLMAnKJL5FOOTSyff4fRZF82sLWCefKWSxl/xZgFAUAeDY6woDfuCR7lTIxv1vH9CqJiDDL1Ny24AcZitcvkinIXTQMrjPVDuo6oIHUVQLcV3F7MYTM9a1hx2u9Su4at7I6CG1hpFB8FRMWo4z/FMzhjmytpxYWuS1jA+y0W9N7HNtPfwkaBfpzXhfGN+w+xvQOjEpkYgrrFAA1yLk670JxiuUNPi17FjN+3MT7CKkrcpP8agGd1uuAGZSld4z7wX2wmSFSIOgnAjd9TkmMOUJkY4tfzmDtyj7PbGNfT+3YjpjQvYKPFoCNnE3OzHmFu1xZKfKsUdRLAPLkUylXyzE5ZFbdTOgWygLk73er2dOvVi/n8bvyXGSwGmUv3+jBd7xUSQKWokwCamA//PebKxHl9F4Ibfvy6hRnsA4bfwL5KLm84SDYoZDfoJ8wN0saXClEnASxgboTX2ndDLwqg+PUJ1nP/FTP+O3zdTq4NzF26jH1uKwz2+ckNqjB1EsAlzI+/i/nyvrgUDb9MBC6Ah+m11+lt/PGs3i3yZpan5DqfXpFikDTq6AbdAV5jwhIVoE4C8NSFm1iP6udqQacQohg8/2cRc0O+ob/xF92euJklHmt0KV1/kHjAr+9ukEqfVIQ6CcDz9i9hBrVGLjzrQjgtaa30mpuY21Jm/C6gI8z4t7D9usUN7GvkTfGx6G2vrZFw1g3aRG5QJaiTAGI9zX1yCcRlsl/u52zF2aDT9LM1evf8X9J7b2PGv4EZfnEP70p6L3eFXAS9Pss5bPTyGOQVcoMqQZ12hC3S6b8XN6jH8iXFr711+3uPsKzQT3Qa/zM6N7P4DI6vLC/T6Q71igfmMJG9SE0CqAB1EkCTPA8fF7y89/W2EB4XCt/rxh4W7L7BTmd5lprX7YmbWdx1cf/fD7yLpz2W4cegvsVKIW6iOGDi1EkAPuPie3292kM8sKJYwiTmCfXy0T8Cv2DG/wLbxviSs8Yf4wrIWaVL5CNPu13L85I2sbjiLaoCPXHqJADIcUCs/ePVHorlEH1kGKSCwxvg31iPv4EdVhHr9vhmluJsk7tCXjUuxgRlyA2qGHUTQLHaQ1ldUC974sbph9L14gXwL8zwX2P1fHbovo0xTrX6dbygrk/PlonA3aB3yA2qBHUSQOx9u9UEjbV//BzhOE3ajafAPzC35APZ+LvtFov7jP1axXigTHjuBm1hAniD3KCJUicBONH4ogiKxXHh7KptN54Af8NmgfYYbKtkvA/SNeIus3gkamQuvb8XzVKK9ASp0zqAU1btweOCz6Ed07nlsVcacwMLhP11g+wT9rN9t7GefBlL1isKoPgZN7FNOPfS7+pAvAlSRwE4vQpgfU7PfdXWpyi7LYQtYotg/n6D+uWtdK1P2OLWarqmxwK+UFccaZexBbHVAa8jRkSdBQCdBbBigOxlSFaxXtn98jXK/+YVOkumnIcTbGX6PTaD5KIrG318Fus03csgWzHFCKm7AODsSe7uErXIbknM3SnbALPG1++PdqPexaZO/ewAHw3iqrVnme6k+5uGz7/WTNM/oOgSnWKLWX6m1yqdPXJ0hdxQD4e49hE5HohHKnk8sEJOsX6HxRxKiJsw0yQAODsa/InNtqxiyWgr5ClKjwfa5LyhvSGuXYwHoit0OV1nm5xi/YqvF5y4IKZNANApgh3yVsZ1chanpyzMh9+9iLIlLfL5xS/pFEEbc5E80e4FFjuICTKNAnB8mnIT6239dEePB5qY0R9iRturMsSglMUDPivUwlaZn2EC+A0JYOLUcSHsPBRLp/hi1QrWK89ho8SfwN8xF+WiruuzSl7OpY31+jHLdFSFecWATLsA4GwGZ8zbmcPyfjaAf2IGeREU9yp7DtAGOcvUT4c8LX8LMQ5mQQDQmcHZIB912sBckWfAzwwXBBeJK9Wev/QaG210OmRFmBUBFF0hT16LB9n9wsWnJ8dR4IicaKfTISvCrAgAOgtmeTxwggngOdY7H4/gujGDdRedDlkpZkkA0NkjNzCDf4md4zWqglVxhXrUB3KIczJrAoDOeOAIWyfYxNySURlm0QUTFWEWBRCD0yOs599n9AGpDL+CzKIAoPNUeU+DloGKmcL38p73qCUhpgYZvxBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEmBT/B3UepoTnVodaAAAAAElFTkSuQmCC"/>
+ <image height="152" mask="url(#f)" transform="matrix(1 0 0 1 -8 -16)" width="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAIbklEQVR4nO3da4xcZR3H8W93bItKJYBaipoQDRJQQkAUQoQK0ZiUKooi8UK5lNBEBWJ4AXKJxUTEFLRY70nFCxVoxRjWKwJGLSqmihqL1KKIQRAKRaGt2tv64neO025nZ+bMnNltdr+fpOnuzrk8L/7PnP95riBJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJkiRJksS0iS7AFDQdeDMwHzgS2B94ClgFLAc2T1zRpMGZBVwOPAGMABuBu4CVwM+Lv90HPH+iCigNwjTgHOBJYCcwDMwjT4JdzSOV4MLxLJw0SAcDPyKB/VPgmA7H3wv8eNCFksbDXOAx4FlgEd29b60E/jDIQknj4SxgG3A/cESF835CnhRS1y4GzgcOnOiCFC4iuf4d5MW3WzOAfwKfHUShNDldSfLrEWAr8H3gPOCACSrPpUVZVpGArmJ+ce68ugulyekSEjA3AkcD1wAP0qwM3yOtL/uPU3nK4F8BNHo4/zukiXR065C0hw+SNGMV8JxRn70KWAysIwG5HVhNUqUXDqg8/Qb/a4vzL6uzUJqczgV2AN9iz+AfrawMfyIB9h/SDr8A2K+m8vQb/EPAPcDjwL41lUmT1Fkk+H8AzKx4blkZ1rNnZXhBj+XpN/gB3l9c4309nq8p4kSSzmykvxafacDxwCeBv5Hg+zd5oryb7r+F6wj+VwObyMu71NYQafXZTjqLDq/hmtOAE4ClwCMkoLcA3wTOZOxxOXUE/37kPeXvwOwer6EpaC7wKAnU82q87hDweuAGEpTlk6FMk8onQx3BP5MMhNta3FOq5MU0x9h8DXhuzdfftTI8SvPJcH/x8630HvwN8oTZAby375JqymqQl9odwG+AVwzoPmVlWE2z020LzSdDleHLDfLkcNSnajOfTCh5GjhtQPcoO91WAm8CvkBzXP8zJKhPo32zbAP4enHOFQMqp6aoQ4Bfkc6xJXTuH6iiDP5bR123AbwR+BKwoTjmbWNcw+DXwM0ElpEg+xnwkhquOVbwjzaDVIIVLT4z+DWu3k5GVW4gc2971W3wl75Ixv7v+kJu8GtCvBL4HUmJrqV6i03V4IekQyPA6cXvBr8m1D4kPx8B7gYO6vK8XoIfEvCPAzdj8GsvsoAsNfIInTudeg3+0ueLe92Mwa+9yOHAWjJNcTFp2x+t3+CHZhpk8GuvMwu4hQTn7ew+UaaO4G8ANxXX+X3vxZQG6wLgv8DDwHHUF/xlzv9Lkga5wJX2WscCfyEpUZ3BfwXwhuLnM/oupTRAV9HM11fQ22ysVq09Q2Q06coayigNxK5pz4fIsOQHyGK13WrX1LmMpEFOcdRep1XOfxLNOQYLu7hGp3b+k4rPzuy3sFKd2r3wHkTW5RwhA+rG0k0nV5kG3dZPYaU6XUznmVwNsjLbCJmA3+rzcjz/pR3udwN5opgGacItoLmOUKdxQUNkttkmspJEqUrwQ3qdR8hEe2nCnEyaO++g++UKZ5MUZh3pQKsa/JAJ9w+T1Sakvk0nK7tV2dbpYOAfZBhE1VRkLqk4t1A9+EufIhPre113SALyzT1MgnAnGeu/jmwtNAx8BbiebEG0CHgH+ea/l6QyR/V4310X360a/JAlV0Zw8vteq86pg4Myg7SmnEqCfBNZ/fnA4t8c0n5/AGMvR/7b4ryNZKuiJ8k84mEycrOVBs21h7YDd/ZQ9l+QNOgMWs8Wk9oqv/l3km/2bo6fQxab3Qj8kYz9+TBwHVlJ+naywsMW2gd/mfYsJsMm/kpvy65fT5ZhrGstUk0ht5EgXFrxvLLJ87g2x6wmL8ajtXrhfR0ZQDdM9a1lj2fsZlWprWPJYrbbSH7fahx/K3+m82ZztwO/HvW3dq09ZaW6pMsylKYBDxX3kyqbRbPn9W7SstPOkcWx53Y47sskPy/NJP0E7V54V5HKeGKHa4+2hDxBxmuzDk1CC8iqCxvIC/FYLiRB3KmiLCEvxpBKdidZZe6iNufsS94rHqP7OcaQFGoEOLvCOdIeDiPLIO4kQw1adWx9jLx0dnIZCcqjyGrT2+j81IA8YTaTxW2rrDbxINkGSerLTBL8O4E1wKGjPl9Mhjd32kDjAlIBNpORoFXSmoXFuR+pcM4ninJN1AZ+mmTeStrzn2H33VVOZff1eVo5geYeYvdQLZ0p3UhSpm4X3zq2uN85PdxLaullZGPpEWA58DySltxLFq89nawPVB67cJfjnyr+P7nHe+8D3Ffcp9tlGNcD3+3xflJLDeBq0lu7lmw79HJSCcotU7fSHNKwnuwyWX4jv7OPex8K/Iv0+HYz0O7jmAZpQE4hufwmUgkaZNPpq4CPAh9g9+HNc0gF6KaHuZ130XkSTemY4tg6d7aR/m8OqQRrSTrUzgwSjJfXcN/PkJfydu8dpfW4KZ4GaC5Jh5Z3cewzZKxOv6aTkalPk/SrnWtIk+uLariv1NLV5Nv9PR2Oe4gMpa7DIeTFeg3tm2CPJmU7v6b7SnsYIh1Vz5Kl0seyhgxwq8t8kgp9bozPG6T5dISMLZIG5qVk2MQaxm6h+SFJXep0La1HfzaAr1K9A03q2TzyjXzdGJ9/g3SI1alBxhVtAo4o/jaDbI3qqtEad58mleAtLT5bRnqT6zabtEY9QGatfbsog2mPxt1MMoDuCfYcIbqYDGfodp5BFaeQ1qiNxT186dWEOYy8EN/F7sG+hKQlBw7ovleSSuBsME24s9k9B19E0pJh0o4/CENUnzwjDcxNpBNqKc3g7zRsWpo0ZpGhCCMY/JqiXkNWmzD4JUmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEmSJEn9+h950gu2YS2pzwAAAABJRU5ErkJggg=="/>
+ <path d="m 54.851562 72.121094 l -19.667968 -1.46875 l -3.058594 3.058594 l 9.410156 6.890624 l 2.796875 2.070313 l 2.070313 2.796875 l 6.890625 9.40625 l 3.003906 -3.007812 l 0.050781 -0.046876 l -1.46875 -19.667968 l -0.023437 -0.003906 z m 0 0" fill="url(#g)"/>
+ <path d="m 74.515625 52.488281 l -33.65625 -11.304687 l -5.257813 5.4375 l 25.421876 19.359375 l 19.355468 25.417969 l 5.4375 -5.257813 z m 0 0" fill="url(#h)"/>
+ <path d="m 90.746094 35.472656 c -2.730469 -1.851562 -8.875 0.726563 -14.164063 6.042969 l -25.136719 25.136719 c -5.308593 5.273437 -7.878906 11.40625 -5.199218 14.101562 c 2.679687 2.691406 8.816406 0.113282 14.101562 -5.199218 l 25.136719 -25.136719 c 5.320313 -5.289063 7.898437 -11.4375 6.042969 -14.164063 c -0.023438 -0.039062 -0.21875 -0.300781 -0.351563 -0.433594 c -0.128906 -0.128906 -0.429687 -0.347656 -0.429687 -0.347656 z m 0 0" fill="url(#i)"/>
+ <image height="152" mask="url(#j)" transform="matrix(1 0 0 1 -8 -16)" width="192" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAACYCAYAAACyAeadAAAABmJLR0QA/wD/AP+gvaeTAAAEvUlEQVR4nO3dbVPbOBQG0JsApbv//7d2S0ni/WDf+FpRAu0seVmfM6Mx0JCG4T6ypMgmAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/p82t34B/LlhGD71+9tsNsNXv5ZHJQAPqBR+/f31fpdDPQrCKQF4IE3hZ4vm4+PDy/HYhGBJAB7EVPxt28ZpGI7f0rTD1ISgEIAHUIp/W47bOA1A/X3Wws/jfjoehGD0fOsXwGVN8T81xxqAbefbs/iz8I9PG/MQadUE4I51iv85TkPQnh0iluP/w/TYfXnsMAyDoVAIwL3Lwn6OufhrCD46A9Sx/1PMATnOB77wtT8EAbhTpffPon+Jufh7ATi3DFrnAOkQEbuveu2PRADu21OMhZ/tUgBaOc7fxmkA9hGxGUdB6x4GCcAdmnr/HPq8RMS3OA1Auwy6eIo4XQXaln97nj7ff+XP8QgE4D5lcb9ExGt8LgDtG185zt/EctUnA9B+3yoJwB0p7/Rm7/9aWg3AtmnHp4h52TPbobSIeUK8DcMgAbgXnSXPl4j4HhF/xRiAuhJU5wFtT54T3F1EvMcchBquSxPnVRGAO9Cs+GTxv8ZY/BmAc5PgLOg69NlFxK/p6+/1vyof1xA4A3AbZcLbTnqz+P+O/hyghuD4dDH29rtYBqMOgep8YBMrHwYJwA2V4s+1/m9Ty97/+9RyCPQSy3eC60Q4C3gfY6+fX8uhUMQchNWv/iQBuIFmZ2cd8tSWAeitAvU2w9V1/4i5+PN9gJwL7GIZgFXPAwTgys7s73mN5YT32/R5Fn1tbc9fCz7PAu0wKecF7zFPjBcXy6yVAFxfu9JTx/vZ4+fXs9jbFZtLKzjtTs9DzIX/HvMKkb1AIQBX1az2ZJFnz197/+zt6+7OQ/S3NbQ7P+syaA573mNcFfoVyzPA6q8SE4Drq8WfE97s+XOym0OXHLu3W53bi2DqEmiO82vRv8Vc/Nn7H+L0bLE6AnBdm1gudWYA8uO6zaEuYe5iWeC97c91hSeHOz+nVgOQZ4XVF3+EAFxNs+TZ7vDMzWntmv6h+bieDXq9fzvZ/TG12vvvy+NXPfyJEIBrq+/29rYzRCx75Vrc7VVfm85j6vDnLcbi/yfGAGTxH4c+ay/+CAG4hbp+H3H6Tm39t4jTgr904XtOfnPc/yPGIdBJ8f9nP82DE4Db6PXaeXY4F4D2+/NYd3vWye9bjL3/WywnvuOT6v0jQgBuoRZ93ZMzRH9I1O71z2Mbol4Afk6fH4tf4S8JwHW12xOeYjl8qZPh3n1+8ti7309v+TPH/or/DAG4ks1mMwzDUJcp6+1K9nG6GtQOhSL6lzrW3r9eB3Bc81f45wnAdWVP3y5r7qK/zfnSxe6HpuUZoL4LrPg/IADXVy9NbG9zeG6nZ9UbAtVhUL394SG4aNVbYW/hzE1u6w7Rusvzo1uetCFYXP+r9/+YANxA5/7+9XjphrfHp+g0b3D9AQG4seavvNQg9LZAR1xeEtXr/yYBuDOf/Osv7XYJhf+HBOBO+ftfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABf4l90JPJqSIYhvQAAAABJRU5ErkJggg=="/>
+ <g clip-path="url(#l)" mask="url(#k)" transform="matrix(1 0 0 1 -8 -16)">
+ <path d="m 68.078125 24.140625 c -13.199219 0.945313 -25.613281 6.613281 -34.96875 15.96875 c -9.804687 9.859375 -15.511719 23.070313 -15.96875 36.96875 c 0.945313 -13.199219 6.613281 -25.613281 15.96875 -34.96875 c 21.480469 -21.480469 56.300781 -21.480469 77.78125 0 c 10.0625 10.070313 15.828125 23.65625 16.085937 37.890625 c 0.273438 -14.929688 -5.53125 -29.328125 -16.085937 -39.890625 c -11.28125 -11.28125 -26.902344 -17.105469 -42.8125 -15.96875 z m 0 0"/>
+ </g>
+</svg>
diff --git a/src/client/cockpit-client.ui b/src/client/cockpit-client.ui
new file mode 100755
index 0000000..05aa746
--- /dev/null
+++ b/src/client/cockpit-client.ui
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.38.2 -->
+<interface>
+ <requires lib="gtk+" version="3.24"/>
+ <requires lib="webkit2gtk" version="2.28"/>
+ <template class="CockpitClientWindow" parent="GtkApplicationWindow">
+ <property name="width-request">800</property>
+ <property name="height-request">600</property>
+ <property name="can-focus">False</property>
+ <property name="default-width">1280</property>
+ <property name="default-height">800</property>
+ <child>
+ <object class="WebKitWebView" type-func="webkit_web_view_get_type" id="webview">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ </child>
+ <child type="titlebar">
+ <object class="GtkHeaderBar" id="headerbar">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="title" translatable="yes">Cockpit Client</property>
+ <property name="subtitle" translatable="yes" bind-source="webview" bind-property="title">Loading...</property>
+ <property name="show-close-button">True</property>
+ <child>
+ <object class="GtkButtonBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="homogeneous">True</property>
+ <property name="layout-style">expand</property>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="receives-default">True</property>
+ <property name="action-name">win.go-back</property>
+ <property name="always-show-image">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">go-previous-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="receives-default">True</property>
+ <property name="action-name">win.go-forward</property>
+ <property name="always-show-image">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">go-next-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="action-name">win.reload</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">view-refresh-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/client/org.cockpit_project.CockpitClient.desktop b/src/client/org.cockpit_project.CockpitClient.desktop
new file mode 100644
index 0000000..9897089
--- /dev/null
+++ b/src/client/org.cockpit_project.CockpitClient.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Type=Application
+Name=Cockpit Client
+Comment=Connect via ssh to servers running Cockpit
+Icon=cockpit-client
+Exec=cockpit-client
+Categories=Network;RemoteAccess
diff --git a/src/client/org.cockpit_project.CockpitClient.metainfo.xml b/src/client/org.cockpit_project.CockpitClient.metainfo.xml
new file mode 100644
index 0000000..0ff84f7
--- /dev/null
+++ b/src/client/org.cockpit_project.CockpitClient.metainfo.xml
@@ -0,0 +1,62 @@
+<?xml version='1.0' encoding='utf-8'?>
+<component type="desktop-application">
+ <id>org.cockpit_project.CockpitClient</id>
+
+ <name>Cockpit Client</name>
+ <summary>Connect via ssh to servers with Cockpit</summary>
+
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>LGPL-2.1-or-later</project_license>
+
+ <description>
+ <p>
+ Cockpit Client provides a graphical interface to your servers, containers, and virtual machines. Connections are made over SSH, using the SSH configuration of the local user (including aliases, known hosts, key files, hardware tokens, etc).
+ </p>
+ <p>
+ The server needs to have Python installed, but the Cockpit webserver doesn't need to be enabled, and no extra ports need to be opened.
+ </p>
+ </description>
+
+ <screenshots>
+ <screenshot>
+ <caption>Cockpit Client login screen</caption>
+ <image>https://cockpit-project.org/images/flatpak/flatpak-screenshot-login.png</image>
+ </screenshot>
+ <screenshot type="default">
+ <caption>Cockpit overview</caption>
+ <image>https://cockpit-project.org/images/flatpak/flatpak-screenshot-overview.png</image>
+ </screenshot>
+ <screenshot>
+ <caption>Cockpit terminal</caption>
+ <image>https://cockpit-project.org/images/flatpak/flatpak-screenshot-terminal.png</image>
+ </screenshot>
+ </screenshots>
+
+ <releases>
+ <release version="311" date="2024-02-14"/>
+ </releases>
+
+ <launchable type="desktop-id">org.cockpit_project.CockpitClient.desktop</launchable>
+
+ <categories>
+ <category>Network</category>
+ <category>RemoteAccess</category>
+ </categories>
+
+ <provides>
+ <binary>cockpit-client</binary>
+ </provides>
+
+ <requires>
+ <control>keyboard</control>
+ <control>pointing</control>
+ <display_length compare="ge">medium</display_length>
+ </requires>
+
+ <content_rating type="oars-1.0"/>
+
+ <url type="homepage">https://cockpit-project.org/</url>
+ <url type="bugtracker">https://github.com/cockpit-project/cockpit/issues</url>
+ <url type="help">https://cockpit-project.org/documentation.html</url>
+ <url type="translate">https://translate.fedoraproject.org/projects/cockpit</url>
+</component>
diff --git a/src/client/org.cockpit_project.CockpitClient.service b/src/client/org.cockpit_project.CockpitClient.service
new file mode 100644
index 0000000..af83d8c
--- /dev/null
+++ b/src/client/org.cockpit_project.CockpitClient.service
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.cockpit_project.CockpitClient
+Exec=cockpit-client --gapplication-service
diff --git a/src/cockpit/__init__.py b/src/cockpit/__init__.py
new file mode 100644
index 0000000..3a8d6d5
--- /dev/null
+++ b/src/cockpit/__init__.py
@@ -0,0 +1 @@
+from ._version import __version__ # noqa: F401
diff --git a/src/cockpit/_vendor/__init__.py b/src/cockpit/_vendor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/cockpit/_vendor/__init__.py
diff --git a/src/cockpit/_vendor/bei/__init__.py b/src/cockpit/_vendor/bei/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/cockpit/_vendor/bei/__init__.py
diff --git a/src/cockpit/_vendor/bei/beiboot.py b/src/cockpit/_vendor/bei/beiboot.py
new file mode 100644
index 0000000..79707c2
--- /dev/null
+++ b/src/cockpit/_vendor/bei/beiboot.py
@@ -0,0 +1,160 @@
+# beiboot - Remote bootloader for Python
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import asyncio
+import os
+import shlex
+import subprocess
+import sys
+import threading
+from typing import IO, List, Sequence, Tuple
+
+from .bootloader import make_bootloader
+
+
+def get_python_command(local: bool = False,
+ tty: bool = False,
+ sh: bool = False) -> Sequence[str]:
+ interpreter = sys.executable if local else 'python3'
+ command: Sequence[str]
+
+ if tty:
+ command = (interpreter, '-iq')
+ else:
+ command = (
+ interpreter, '-ic',
+ # https://github.com/python/cpython/issues/93139
+ '''" - beiboot - "; import sys; sys.ps1 = ''; sys.ps2 = '';'''
+ )
+
+ if sh:
+ command = (' '.join(shlex.quote(arg) for arg in command),)
+
+ return command
+
+
+def get_ssh_command(*args: str, tty: bool = False) -> Sequence[str]:
+ return ('ssh',
+ *(['-t'] if tty else ()),
+ *args,
+ *get_python_command(tty=tty, sh=True))
+
+
+def get_container_command(*args: str, tty: bool = False) -> Sequence[str]:
+ return ('podman', 'exec', '--interactive',
+ *(['--tty'] if tty else ()),
+ *args,
+ *get_python_command(tty=tty))
+
+
+def get_command(*args: str, tty: bool = False, sh: bool = False) -> Sequence[str]:
+ return (*args, *get_python_command(local=True, tty=tty, sh=sh))
+
+
+def splice_in_thread(src: int, dst: IO[bytes]) -> None:
+ def _thread() -> None:
+ # os.splice() only in Python 3.10
+ with dst:
+ block_size = 1 << 20
+ while True:
+ data = os.read(src, block_size)
+ if not data:
+ break
+ dst.write(data)
+ dst.flush()
+
+ threading.Thread(target=_thread, daemon=True).start()
+
+
+def send_and_splice(command: Sequence[str], script: bytes) -> None:
+ with subprocess.Popen(command, stdin=subprocess.PIPE) as proc:
+ assert proc.stdin is not None
+ proc.stdin.write(script)
+
+ splice_in_thread(0, proc.stdin)
+ sys.exit(proc.wait())
+
+
+def send_xz_and_splice(command: Sequence[str], script: bytes) -> None:
+ import ferny
+
+ class Responder(ferny.InteractionResponder):
+ async def do_custom_command(self,
+ command: str,
+ args: Tuple,
+ fds: List[int],
+ stderr: str) -> None:
+ assert proc.stdin is not None
+ if command == 'beiboot.provide':
+ proc.stdin.write(script)
+ proc.stdin.flush()
+
+ agent = ferny.InteractionAgent(Responder())
+ with subprocess.Popen(command, stdin=subprocess.PIPE, stderr=agent) as proc:
+ assert proc.stdin is not None
+ proc.stdin.write(make_bootloader([
+ ('boot_xz', ('script.py.xz', len(script), [], True)),
+ ], gadgets=ferny.BEIBOOT_GADGETS).encode())
+ proc.stdin.flush()
+
+ asyncio.run(agent.communicate())
+ splice_in_thread(0, proc.stdin)
+ sys.exit(proc.wait())
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--sh', action='store_true',
+ help='Pass Python interpreter command as shell-script')
+ parser.add_argument('--xz', help="the xz to run remotely")
+ parser.add_argument('--script',
+ help="the script to run remotely (must be repl-friendly)")
+ parser.add_argument('command', nargs='*')
+
+ args = parser.parse_args()
+ tty = not args.script and os.isatty(0)
+
+ if args.command == []:
+ command = get_python_command(tty=tty)
+ elif args.command[0] == 'ssh':
+ command = get_ssh_command(*args.command[1:], tty=tty)
+ elif args.command[0] == 'container':
+ command = get_container_command(*args.command[1:], tty=tty)
+ else:
+ command = get_command(*args.command, tty=tty, sh=args.sh)
+
+ if args.script:
+ with open(args.script, 'rb') as file:
+ script = file.read()
+
+ send_and_splice(command, script)
+
+ elif args.xz:
+ with open(args.xz, 'rb') as file:
+ script = file.read()
+
+ send_xz_and_splice(command, script)
+
+ else:
+ # If we're streaming from stdin then this is a lot easier...
+ os.execlp(command[0], *command)
+
+ # Otherwise, "full strength"
+
+if __name__ == '__main__':
+ main()
diff --git a/src/cockpit/_vendor/bei/beipack.py b/src/cockpit/_vendor/bei/beipack.py
new file mode 100644
index 0000000..e2dab53
--- /dev/null
+++ b/src/cockpit/_vendor/bei/beipack.py
@@ -0,0 +1,217 @@
+# beipack - Remote bootloader for Python
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import binascii
+import lzma
+import os
+import sys
+import tempfile
+import zipfile
+from typing import Dict, Iterable, List, Optional, Set, Tuple
+
+from .data import read_data_file
+
+
+def escape_string(data: str) -> str:
+ # Avoid mentioning ' ' ' literally, to make our own packing a bit prettier
+ triplequote = "'" * 3
+ if triplequote not in data:
+ return "r" + triplequote + data + triplequote
+ if '"""' not in data:
+ return 'r"""' + data + '"""'
+ return repr(data)
+
+
+def ascii_bytes_repr(data: bytes) -> str:
+ return 'b' + escape_string(data.decode('ascii'))
+
+
+def utf8_bytes_repr(data: bytes) -> str:
+ return escape_string(data.decode('utf-8')) + ".encode('utf-8')"
+
+
+def base64_bytes_repr(data: bytes, imports: Set[str]) -> str:
+ # base85 is smaller, but base64 is in C, and ~20x faster.
+ # when compressing with `xz -e` the size difference is marginal.
+ imports.add('from binascii import a2b_base64')
+ encoded = binascii.b2a_base64(data).decode('ascii').strip()
+ return f'a2b_base64("{encoded}")'
+
+
+def bytes_repr(data: bytes, imports: Set[str]) -> str:
+ # Strategy:
+ # if the file is ascii, encode it directly as bytes
+ # otherwise, if it's UTF-8, use a unicode string and encode
+ # otherwise, base64
+
+ try:
+ return ascii_bytes_repr(data)
+ except UnicodeDecodeError:
+ # it's not ascii
+ pass
+
+ # utf-8
+ try:
+ return utf8_bytes_repr(data)
+ except UnicodeDecodeError:
+ # it's not utf-8
+ pass
+
+ return base64_bytes_repr(data, imports)
+
+
+def dict_repr(contents: Dict[str, bytes], imports: Set[str]) -> str:
+ return ('{\n' +
+ ''.join(f' {repr(k)}: {bytes_repr(v, imports)},\n'
+ for k, v in contents.items()) +
+ '}')
+
+
+def pack(contents: Dict[str, bytes],
+ entrypoint: Optional[str] = None,
+ args: str = '') -> str:
+ """Creates a beipack with the given `contents`.
+
+ If `entrypoint` is given, it should be an entry point which is run as the
+ "main" function. It is given in the `package.module:func format` such that
+ the following code is emitted:
+
+ from package.module import func as main
+ main()
+
+ Additionally, if `args` is given, it is written verbatim between the parens
+ of the call to main (ie: it should already be in Python syntax).
+ """
+
+ loader = read_data_file('beipack_loader.py')
+ lines = [line for line in loader.splitlines() if line]
+ lines.append('')
+
+ imports = {'import sys'}
+ contents_txt = dict_repr(contents, imports)
+ lines.extend(imports)
+ lines.append(f'sys.meta_path.insert(0, BeipackLoader({contents_txt}))')
+
+ if entrypoint:
+ package, main = entrypoint.split(':')
+ lines.append(f'from {package} import {main} as main')
+ lines.append(f'main({args})')
+
+ return ''.join(f'{line}\n' for line in lines)
+
+
+def collect_contents(filenames: List[str],
+ relative_to: Optional[str] = None) -> Dict[str, bytes]:
+ contents: Dict[str, bytes] = {}
+
+ for filename in filenames:
+ with open(filename, 'rb') as file:
+ contents[os.path.relpath(filename, start=relative_to)] = file.read()
+
+ return contents
+
+
+def collect_module(name: str, *, recursive: bool) -> Dict[str, bytes]:
+ import importlib.resources
+ from importlib.resources.abc import Traversable
+
+ def walk(path: str, entry: Traversable) -> Iterable[Tuple[str, bytes]]:
+ for item in entry.iterdir():
+ itemname = f'{path}/{item.name}'
+ if item.is_file():
+ yield itemname, item.read_bytes()
+ elif recursive and item.name != '__pycache__':
+ yield from walk(itemname, item)
+
+ return dict(walk(name.replace('.', '/'), importlib.resources.files(name)))
+
+
+def collect_zip(filename: str) -> Dict[str, bytes]:
+ contents = {}
+
+ with zipfile.ZipFile(filename) as file:
+ for entry in file.filelist:
+ if '.dist-info/' in entry.filename:
+ continue
+ contents[entry.filename] = file.read(entry)
+
+ return contents
+
+
+def collect_pep517(path: str) -> Dict[str, bytes]:
+ with tempfile.TemporaryDirectory() as tmpdir:
+ import build
+ builder = build.ProjectBuilder(path)
+ wheel = builder.build('wheel', tmpdir)
+ return collect_zip(wheel)
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--python', '-p',
+ help="add a #!python3 interpreter line using the given path")
+ parser.add_argument('--xz', '-J', action='store_true',
+ help="compress the output with `xz`")
+ parser.add_argument('--topdir',
+ help="toplevel directory (paths are stored relative to this)")
+ parser.add_argument('--output', '-o',
+ help="write output to a file (default: stdout)")
+ parser.add_argument('--main', '-m', metavar='MODULE:FUNC',
+ help="use FUNC from MODULE as the main function")
+ parser.add_argument('--main-args', metavar='ARGS',
+ help="arguments to main() in Python syntax", default='')
+ parser.add_argument('--module', action='append', default=[],
+ help="collect installed modules (recursively)")
+ parser.add_argument('--zip', '-z', action='append', default=[],
+ help="include files from a zipfile (or wheel)")
+ parser.add_argument('--build', metavar='DIR', action='append', default=[],
+ help="PEP-517 from a given source directory")
+ parser.add_argument('files', nargs='*',
+ help="files to include in the beipack")
+ args = parser.parse_args()
+
+ contents = collect_contents(args.files, relative_to=args.topdir)
+
+ for file in args.zip:
+ contents.update(collect_zip(file))
+
+ for name in args.module:
+ contents.update(collect_module(name, recursive=True))
+
+ for path in args.build:
+ contents.update(collect_pep517(path))
+
+ result = pack(contents, args.main, args.main_args).encode('utf-8')
+
+ if args.python:
+ result = b'#!' + args.python.encode('ascii') + b'\n' + result
+
+ if args.xz:
+ result = lzma.compress(result, preset=lzma.PRESET_EXTREME)
+
+ if args.output:
+ with open(args.output, 'wb') as file:
+ file.write(result)
+ else:
+ if args.xz and os.isatty(1):
+ sys.exit('refusing to write compressed output to a terminal')
+ sys.stdout.buffer.write(result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/cockpit/_vendor/bei/bootloader.py b/src/cockpit/_vendor/bei/bootloader.py
new file mode 100644
index 0000000..9b0c217
--- /dev/null
+++ b/src/cockpit/_vendor/bei/bootloader.py
@@ -0,0 +1,101 @@
+# beiboot - Remote bootloader for Python
+#
+# Copyright (C) 2023 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import textwrap
+from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple
+
+GADGETS = {
+ "_frame": r"""
+ import sys
+ import traceback
+ try:
+ ...
+ except SystemExit:
+ raise
+ except BaseException:
+ command('beiboot.exc', traceback.format_exc())
+ sys.exit(37)
+ """,
+ "try_exec": r"""
+ import contextlib
+ import os
+ def try_exec(argv):
+ with contextlib.suppress(OSError):
+ os.execvp(argv[0], argv)
+ """,
+ "boot_xz": r"""
+ import lzma
+ import sys
+ def boot_xz(filename, size, args=[], send_end=False):
+ command('beiboot.provide', size)
+ src_xz = sys.stdin.buffer.read(size)
+ src = lzma.decompress(src_xz)
+ sys.argv = [filename, *args]
+ if send_end:
+ end()
+ exec(src, {
+ '__name__': '__main__',
+ '__self_source__': src_xz,
+ '__file__': filename})
+ sys.exit()
+ """,
+}
+
+
+def split_code(code: str, imports: Set[str]) -> Iterable[Tuple[str, str]]:
+ for line in textwrap.dedent(code).splitlines():
+ text = line.lstrip(" ")
+ if text.startswith("import "):
+ imports.add(text)
+ elif text:
+ spaces = len(line) - len(text)
+ assert (spaces % 4) == 0
+ yield "\t" * (spaces // 4), text
+
+
+def yield_body(user_gadgets: Dict[str, str],
+ steps: Sequence[Tuple[str, Sequence[object]]],
+ imports: Set[str]) -> Iterable[Tuple[str, str]]:
+ # Allow the caller to override our gadgets, but keep the original
+ # variable for use in the next step.
+ gadgets = dict(GADGETS, **user_gadgets)
+
+ # First emit the gadgets. Emit all gadgets provided by the caller,
+ # plus any referred to by the caller's list of steps.
+ provided_gadgets = set(user_gadgets)
+ step_gadgets = {name for name, _args in steps}
+ for name in provided_gadgets | step_gadgets:
+ yield from split_code(gadgets[name], imports)
+
+ # Yield functions mentioned in steps from the caller
+ for name, args in steps:
+ yield '', name + repr(tuple(args))
+
+
+def make_bootloader(steps: Sequence[Tuple[str, Sequence[object]]],
+ gadgets: Optional[Dict[str, str]] = None) -> str:
+ imports: Set[str] = set()
+ lines: List[str] = []
+
+ for frame_spaces, frame_text in split_code(GADGETS["_frame"], imports):
+ if frame_text == "...":
+ for spaces, text in yield_body(gadgets or {}, steps, imports):
+ lines.append(frame_spaces + spaces + text)
+ else:
+ lines.append(frame_spaces + frame_text)
+
+ return "".join(f"{line}\n" for line in [*imports, *lines]) + "\n"
diff --git a/src/cockpit/_vendor/bei/data/__init__.py b/src/cockpit/_vendor/bei/data/__init__.py
new file mode 100644
index 0000000..c4e4974
--- /dev/null
+++ b/src/cockpit/_vendor/bei/data/__init__.py
@@ -0,0 +1,13 @@
+import sys
+
+if sys.version_info >= (3, 9):
+ import importlib.abc
+ import importlib.resources
+
+ def read_data_file(filename: str) -> str:
+ return (importlib.resources.files(__name__) / filename).read_text()
+else:
+ def read_data_file(filename: str) -> str:
+ loader = __loader__ # type: ignore[name-defined]
+ data = loader.get_data(__file__.replace('__init__.py', filename))
+ return data.decode('utf-8')
diff --git a/src/cockpit/_vendor/bei/data/beipack_loader.py b/src/cockpit/_vendor/bei/data/beipack_loader.py
new file mode 100644
index 0000000..869dd21
--- /dev/null
+++ b/src/cockpit/_vendor/bei/data/beipack_loader.py
@@ -0,0 +1,85 @@
+# beipack https://github.com/allisonkarlitskaya/beipack
+
+import importlib.abc
+import importlib.util
+import io
+import sys
+from types import ModuleType
+from typing import BinaryIO, Dict, Iterator, Optional, Sequence
+
+
+class BeipackLoader(importlib.abc.SourceLoader, importlib.abc.MetaPathFinder):
+ if sys.version_info >= (3, 11):
+ from importlib.resources.abc import ResourceReader as AbstractResourceReader
+ else:
+ AbstractResourceReader = object
+
+ class ResourceReader(AbstractResourceReader):
+ def __init__(self, contents: Dict[str, bytes], filename: str) -> None:
+ self._contents = contents
+ self._dir = f'{filename}/'
+
+ def is_resource(self, resource: str) -> bool:
+ return f'{self._dir}{resource}' in self._contents
+
+ def open_resource(self, resource: str) -> BinaryIO:
+ return io.BytesIO(self._contents[f'{self._dir}{resource}'])
+
+ def resource_path(self, resource: str) -> str:
+ raise FileNotFoundError
+
+ def contents(self) -> Iterator[str]:
+ dir_length = len(self._dir)
+ result = set()
+
+ for filename in self._contents:
+ if filename.startswith(self._dir):
+ try:
+ next_slash = filename.index('/', dir_length)
+ except ValueError:
+ next_slash = None
+ result.add(filename[dir_length:next_slash])
+
+ return iter(result)
+
+ contents: Dict[str, bytes]
+ modules: Dict[str, str]
+
+ def __init__(self, contents: Dict[str, bytes]) -> None:
+ try:
+ contents[__file__] = __self_source__ # type: ignore[name-defined]
+ except NameError:
+ pass
+
+ self.contents = contents
+ self.modules = {
+ self.get_fullname(filename): filename
+ for filename in contents
+ if filename.endswith(".py")
+ }
+
+ def get_fullname(self, filename: str) -> str:
+ assert filename.endswith(".py")
+ filename = filename[:-3]
+ if filename.endswith("/__init__"):
+ filename = filename[:-9]
+ return filename.replace("/", ".")
+
+ def get_resource_reader(self, fullname: str) -> ResourceReader:
+ return BeipackLoader.ResourceReader(self.contents, fullname.replace('.', '/'))
+
+ def get_data(self, path: str) -> bytes:
+ return self.contents[path]
+
+ def get_filename(self, fullname: str) -> str:
+ return self.modules[fullname]
+
+ def find_spec(
+ self,
+ fullname: str,
+ path: Optional[Sequence[str]],
+ target: Optional[ModuleType] = None
+ ) -> Optional[importlib.machinery.ModuleSpec]:
+ if fullname not in self.modules:
+ return None
+ return importlib.util.spec_from_loader(fullname, self)
diff --git a/src/cockpit/_vendor/bei/spawn.py b/src/cockpit/_vendor/bei/spawn.py
new file mode 100644
index 0000000..478be1e
--- /dev/null
+++ b/src/cockpit/_vendor/bei/spawn.py
@@ -0,0 +1,37 @@
+"""Helper to create a beipack to spawn a command with files in a tmpdir"""
+
+import argparse
+import os
+import sys
+
+from . import pack, tmpfs
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--file', '-f', action='append')
+ parser.add_argument('command', nargs='+', help='The command to execute')
+ args = parser.parse_args()
+
+ contents = {
+ '_beitmpfs.py': tmpfs.__spec__.loader.get_data(tmpfs.__spec__.origin)
+ }
+
+ if args.file is not None:
+ files = args.file
+ else:
+ file = args.command[-1]
+ files = [file]
+ args.command[-1] = './' + os.path.basename(file)
+
+ for filename in files:
+ with open(filename, 'rb') as file:
+ basename = os.path.basename(filename)
+ contents[f'tmpfs/{basename}'] = file.read()
+
+ script = pack.pack(contents, '_beitmpfs:main', '*' + repr(args.command))
+ sys.stdout.write(script)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/cockpit/_vendor/bei/tmpfs.py b/src/cockpit/_vendor/bei/tmpfs.py
new file mode 100644
index 0000000..c28b4f9
--- /dev/null
+++ b/src/cockpit/_vendor/bei/tmpfs.py
@@ -0,0 +1,21 @@
+import os
+import subprocess
+import sys
+import tempfile
+
+
+def main(*command: str) -> None:
+ with tempfile.TemporaryDirectory() as tmpdir:
+ os.chdir(tmpdir)
+
+ for key, value in __loader__.get_contents().items():
+ if key.startswith('tmpfs/'):
+ subdir = os.path.dirname(key)
+ os.makedirs(subdir, exist_ok=True)
+ with open(key, 'wb') as fp:
+ fp.write(value)
+
+ os.chdir('tmpfs')
+
+ result = subprocess.run(command, check=False)
+ sys.exit(result.returncode)
diff --git a/src/cockpit/_vendor/ferny/__init__.py b/src/cockpit/_vendor/ferny/__init__.py
new file mode 100644
index 0000000..4c2a154
--- /dev/null
+++ b/src/cockpit/_vendor/ferny/__init__.py
@@ -0,0 +1,61 @@
+from .interaction_agent import (
+ BEIBOOT_GADGETS,
+ COMMAND_TEMPLATE,
+ AskpassHandler,
+ InteractionAgent,
+ InteractionError,
+ InteractionHandler,
+ temporary_askpass,
+ write_askpass_to_tmpdir,
+)
+from .session import Session
+from .ssh_askpass import (
+ AskpassPrompt,
+ SshAskpassResponder,
+ SshFIDOPINPrompt,
+ SshFIDOUserPresencePrompt,
+ SshHostKeyPrompt,
+ SshPassphrasePrompt,
+ SshPasswordPrompt,
+ SshPKCS11PINPrompt,
+)
+from .ssh_errors import (
+ SshAuthenticationError,
+ SshChangedHostKeyError,
+ SshError,
+ SshHostKeyError,
+ SshUnknownHostKeyError,
+)
+from .transport import FernyTransport, SubprocessError
+
+__all__ = [
+ 'AskpassHandler',
+ 'AskpassPrompt',
+ 'AuthenticationError',
+ 'BEIBOOT_GADGETS',
+ 'COMMAND_TEMPLATE',
+ 'ChangedHostKeyError',
+ 'FernyTransport',
+ 'HostKeyError',
+ 'InteractionAgent',
+ 'InteractionError',
+ 'InteractionHandler',
+ 'Session',
+ 'SshAskpassResponder',
+ 'SshAuthenticationError',
+ 'SshChangedHostKeyError',
+ 'SshError',
+ 'SshFIDOPINPrompt',
+ 'SshFIDOUserPresencePrompt',
+ 'SshHostKeyError',
+ 'SshHostKeyPrompt',
+ 'SshPKCS11PINPrompt',
+ 'SshPassphrasePrompt',
+ 'SshPasswordPrompt',
+ 'SshUnknownHostKeyError',
+ 'SubprocessError',
+ 'temporary_askpass',
+ 'write_askpass_to_tmpdir',
+]
+
+__version__ = '0'
diff --git a/src/cockpit/_vendor/ferny/askpass.py b/src/cockpit/_vendor/ferny/askpass.py
new file mode 100644
index 0000000..da938fa
--- /dev/null
+++ b/src/cockpit/_vendor/ferny/askpass.py
@@ -0,0 +1,4 @@
+from .interaction_client import main
+
+if __name__ == '__main__':
+ main()
diff --git a/src/cockpit/_vendor/ferny/interaction_agent.py b/src/cockpit/_vendor/ferny/interaction_agent.py
new file mode 100644
index 0000000..c15159f
--- /dev/null
+++ b/src/cockpit/_vendor/ferny/interaction_agent.py
@@ -0,0 +1,421 @@
+# ferny - asyncio SSH client library, using ssh(1)
+#
+# Copyright (C) 2023 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import array
+import ast
+import asyncio
+import contextlib
+import logging
+import os
+import re
+import socket
+import tempfile
+from typing import Any, Callable, ClassVar, Generator, Sequence
+
+from . import interaction_client
+
+logger = logging.getLogger(__name__)
+
+
+COMMAND_RE = re.compile(b'\0ferny\0([^\n]*)\0\0\n')
+COMMAND_TEMPLATE = '\0ferny\0{(command, args)!r}\0\0\n'
+
+BEIBOOT_GADGETS = {
+ "command": fr"""
+ import sys
+ def command(command, *args):
+ sys.stderr.write(f{COMMAND_TEMPLATE!r})
+ sys.stderr.flush()
+ """,
+ "end": r"""
+ def end():
+ command('ferny.end')
+ """,
+}
+
+
+class InteractionError(Exception):
+ pass
+
+
+try:
+ recv_fds = socket.recv_fds
+except AttributeError:
+ # Python < 3.9
+
+ def recv_fds(
+ sock: socket.socket, bufsize: int, maxfds: int, flags: int = 0
+ ) -> 'tuple[bytes, list[int], int, None]':
+ fds = array.array("i")
+ msg, ancdata, flags, addr = sock.recvmsg(bufsize, socket.CMSG_LEN(maxfds * fds.itemsize))
+ for cmsg_level, cmsg_type, cmsg_data in ancdata:
+ if (cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS):
+ fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
+ return msg, list(fds), flags, addr
+
+
+def get_running_loop() -> asyncio.AbstractEventLoop:
+ try:
+ return asyncio.get_running_loop()
+ except AttributeError:
+ # Python 3.6
+ return asyncio.get_event_loop()
+
+
+class InteractionHandler:
+ commands: ClassVar[Sequence[str]]
+
+ async def run_command(self, command: str, args: 'tuple[object, ...]', fds: 'list[int]', stderr: str) -> None:
+ raise NotImplementedError
+
+
+class AskpassHandler(InteractionHandler):
+ commands: ClassVar[Sequence[str]] = ('ferny.askpass',)
+
+ async def do_askpass(self, messages: str, prompt: str, hint: str) -> 'str | None':
+ """Prompt the user for an authentication or confirmation interaction.
+
+ 'messages' is data that was sent to stderr before the interaction was requested.
+ 'prompt' is the interaction prompt.
+
+ The expected response type depends on hint:
+
+ - "confirm": ask for permission, returning "yes" if accepted
+ - example: authorizing agent operation
+
+ - "none": show a request without need for a response
+ - example: please touch your authentication token
+
+ - otherwise: return a password or other form of text token
+ - examples: enter password, unlock private key
+
+ In any case, the function should properly handle cancellation. For the
+ "none" case, this will be the normal way to dismiss the dialog.
+ """
+ return None
+
+ async def do_hostkey(self, reason: str, host: str, algorithm: str, key: str, fingerprint: str) -> bool:
+ """Prompt the user for a decision regarding acceptance of a host key.
+
+ The "reason" will be either "HOSTNAME" or "ADDRESS" (if `CheckHostIP` is enabled).
+
+ The host, algorithm, and key parameters are the values in the form that
+ they would appear one a single line in the known hosts file. The
+ fingerprint is the key fingerprint in the format that ssh would
+ normally present it to the user.
+
+ In case the host key should be accepted, this function needs to return
+ True. Returning False means that ssh implements its default logic. To
+ interrupt the connection, raise an exception.
+ """
+ return False
+
+ async def do_custom_command(
+ self, command: str, args: 'tuple[object, ...]', fds: 'list[int]', stderr: str
+ ) -> None:
+ """Handle a custom command.
+
+ The command name, its arguments, the passed fds, and the stderr leading
+ up to the command invocation are all provided.
+
+ See doc/interaction-protocol.md
+ """
+
+ async def _askpass_command(self, args: 'tuple[object, ...]', fds: 'list[int]', stderr: str) -> None:
+ logger.debug('_askpass_command(%s, %s, %s)', args, fds, stderr)
+ try:
+ argv, env = args
+ assert isinstance(argv, list)
+ assert all(isinstance(arg, str) for arg in argv)
+ assert isinstance(env, dict)
+ assert all(isinstance(key, str) and isinstance(val, str) for key, val in env.items())
+ assert len(fds) == 2
+ except (ValueError, TypeError, AssertionError) as exc:
+ logger.error('Invalid arguments to askpass interaction: %s, %s: %s', args, fds, exc)
+ return
+
+ with open(fds.pop(0), 'w') as status, open(fds.pop(0), 'w') as stdout:
+ try:
+ loop = get_running_loop()
+ try:
+ task = asyncio.current_task()
+ except AttributeError:
+ task = asyncio.Task.current_task() # type:ignore[attr-defined] # (Python 3.6)
+ assert task is not None
+ loop.add_reader(status, task.cancel)
+
+ if len(argv) == 2:
+ # normal askpass
+ prompt = argv[1]
+ hint = env.get('SSH_ASKPASS_PROMPT', '')
+ logger.debug('do_askpass(%r, %r, %r)', stderr, prompt, hint)
+ answer = await self.do_askpass(stderr, prompt, hint)
+ logger.debug('do_askpass answer %r', answer)
+ if answer is not None:
+ print(answer, file=stdout)
+ print(0, file=status)
+
+ elif len(argv) == 6:
+ # KnownHostsCommand
+ argv0, reason, host, algorithm, key, fingerprint = argv
+ if reason in ['ADDRESS', 'HOSTNAME']:
+ logger.debug('do_hostkey(%r, %r, %r, %r, %r)', reason, host, algorithm, key, fingerprint)
+ if await self.do_hostkey(reason, host, algorithm, key, fingerprint):
+ print(host, algorithm, key, file=stdout)
+ else:
+ logger.debug('ignoring KnownHostsCommand reason %r', reason)
+
+ print(0, file=status)
+
+ else:
+ logger.error('Incorrect number of command-line arguments to ferny-askpass: %s', argv)
+ finally:
+ loop.remove_reader(status)
+
+ async def run_command(self, command: str, args: 'tuple[object, ...]', fds: 'list[int]', stderr: str) -> None:
+ logger.debug('run_command(%s, %s, %s, %s)', command, args, fds, stderr)
+ if command == 'ferny.askpass':
+ await self._askpass_command(args, fds, stderr)
+ else:
+ await self.do_custom_command(command, args, fds, stderr)
+
+
+class InteractionAgent:
+ _handlers: 'dict[str, InteractionHandler]'
+
+ _loop: asyncio.AbstractEventLoop
+
+ _tasks: 'set[asyncio.Task]'
+
+ _buffer: bytearray
+ _ours: socket.socket
+ _theirs: socket.socket
+
+ _completion_future: 'asyncio.Future[str]'
+ _pending_result: 'None | str | Exception' = None
+ _end: bool = False
+
+ def _consider_completion(self) -> None:
+ logger.debug('_consider_completion(%r)', self)
+
+ if self._pending_result is None or self._tasks:
+ logger.debug(' but not ready yet')
+
+ elif self._completion_future.done():
+ logger.debug(' but already complete')
+
+ elif isinstance(self._pending_result, str):
+ logger.debug(' submitting stderr (%r) to completion_future', self._pending_result)
+ self._completion_future.set_result(self._pending_result)
+
+ else:
+ logger.debug(' submitting exception (%r) to completion_future')
+ self._completion_future.set_exception(self._pending_result)
+
+ def _result(self, result: 'str | Exception') -> None:
+ logger.debug('_result(%r, %r)', self, result)
+
+ if self._pending_result is None:
+ self._pending_result = result
+
+ if self._ours.fileno() != -1:
+ logger.debug(' remove_reader(%r)', self._ours)
+ self._loop.remove_reader(self._ours.fileno())
+
+ for task in self._tasks:
+ logger.debug(' cancel(%r)', task)
+ task.cancel()
+
+ logger.debug(' closing sockets')
+ self._theirs.close() # idempotent
+ self._ours.close()
+
+ self._consider_completion()
+
+ def _invoke_command(self, stderr: bytes, command_blob: bytes, fds: 'list[int]') -> None:
+ logger.debug('_invoke_command(%r, %r, %r)', stderr, command_blob, fds)
+ try:
+ command, args = ast.literal_eval(command_blob.decode())
+ if not isinstance(command, str) or not isinstance(args, tuple):
+ raise TypeError('Invalid argument types')
+ except (UnicodeDecodeError, SyntaxError, ValueError, TypeError) as exc:
+ logger.error('Received invalid ferny command: %s: %s', command_blob, exc)
+ return
+
+ if command == 'ferny.end':
+ self._end = True
+ self._result(self._buffer.decode(errors='replace'))
+ return
+
+ try:
+ handler = self._handlers[command]
+ except KeyError:
+ logger.error('Received unhandled ferny command: %s', command)
+ return
+
+ # The task is responsible for the list of fds and removing itself
+ # from the set.
+ task_fds = list(fds)
+ task = self._loop.create_task(handler.run_command(command, args, task_fds, stderr.decode()))
+
+ def bottom_half(completed_task: asyncio.Task) -> None:
+ assert completed_task is task
+ while task_fds:
+ os.close(task_fds.pop())
+ self._tasks.remove(task)
+
+ try:
+ task.result()
+ logger.debug('%r completed cleanly', handler)
+ except asyncio.CancelledError:
+ # this is not an error — it just means ferny-askpass exited via signal
+ logger.debug('%r was cancelled', handler)
+ except Exception as exc:
+ logger.debug('%r raised %r', handler, exc)
+ self._result(exc)
+
+ self._consider_completion()
+
+ task.add_done_callback(bottom_half)
+ self._tasks.add(task)
+ fds[:] = []
+
+ def _got_data(self, data: bytes, fds: 'list[int]') -> None:
+ logger.debug('_got_data(%r, %r)', data, fds)
+
+ if data == b'':
+ self._result(self._buffer.decode(errors='replace'))
+ return
+
+ self._buffer.extend(data)
+
+ # Read zero or more "remote" messages
+ chunks = COMMAND_RE.split(self._buffer)
+ self._buffer = bytearray(chunks.pop())
+ while len(chunks) > 1:
+ self._invoke_command(chunks[0], chunks[1], [])
+ chunks = chunks[2:]
+
+ # Maybe read one "local" message
+ if fds:
+ assert self._buffer.endswith(b'\0'), self._buffer
+ stderr = self._buffer[:-1]
+ self._buffer = bytearray(b'')
+ with open(fds.pop(0), 'rb') as command_channel:
+ command = command_channel.read()
+ self._invoke_command(stderr, command, fds)
+
+ def _read_ready(self) -> None:
+ try:
+ data, fds, _flags, _addr = recv_fds(self._ours, 4096, 10, flags=socket.MSG_DONTWAIT)
+ except BlockingIOError:
+ return
+ except OSError as exc:
+ self._result(exc)
+ else:
+ self._got_data(data, fds)
+ finally:
+ while fds:
+ os.close(fds.pop())
+
+ def __init__(
+ self,
+ handlers: Sequence[InteractionHandler],
+ loop: 'asyncio.AbstractEventLoop | None' = None,
+ done_callback: 'Callable[[asyncio.Future[str]], None] | None' = None,
+ ) -> None:
+ self._loop = loop or get_running_loop()
+ self._completion_future = self._loop.create_future()
+ self._tasks = set()
+ self._handlers = {}
+
+ for handler in handlers:
+ for command in handler.commands:
+ self._handlers[command] = handler
+
+ if done_callback is not None:
+ self._completion_future.add_done_callback(done_callback)
+
+ self._theirs, self._ours = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
+ self._buffer = bytearray()
+
+ def fileno(self) -> int:
+ return self._theirs.fileno()
+
+ def start(self) -> None:
+ logger.debug('start(%r)', self)
+ if self._ours.fileno() != -1:
+ logger.debug(' add_reader(%r)', self._ours)
+ self._loop.add_reader(self._ours.fileno(), self._read_ready)
+ else:
+ logger.debug(' ...but agent is already finished.')
+
+ logger.debug(' close(%r)', self._theirs)
+ self._theirs.close()
+
+ def force_completion(self) -> None:
+ logger.debug('force_completion(%r)', self)
+
+ # read any residual data on stderr, but don't process commands, and
+ # don't block
+ try:
+ if self._ours.fileno() != -1:
+ logger.debug(' draining pending stderr data (non-blocking)')
+ with contextlib.suppress(BlockingIOError):
+ while True:
+ data = self._ours.recv(4096, socket.MSG_DONTWAIT)
+ logger.debug(' got %d bytes', len(data))
+ if not data:
+ break
+ self._buffer.extend(data)
+ except OSError as exc:
+ self._result(exc)
+ else:
+ self._result(self._buffer.decode(errors='replace'))
+
+ async def communicate(self) -> None:
+ logger.debug('_communicate(%r)', self)
+ try:
+ self.start()
+ # We assume that we are the only ones to write to
+ # self._completion_future. If we directly await it, though, it can
+ # also have a asyncio.CancelledError posted to it from outside.
+ # Shield it to prevent that from happening.
+ stderr = await asyncio.shield(self._completion_future)
+ logger.debug('_communicate(%r) stderr result is %r', self, stderr)
+ finally:
+ logger.debug('_communicate finished. Ensuring completion.')
+ self.force_completion()
+ if not self._end:
+ logger.debug('_communicate never saw ferny.end. raising InteractionError.')
+ raise InteractionError(stderr.strip())
+
+
+def write_askpass_to_tmpdir(tmpdir: str) -> str:
+ askpass_path = os.path.join(tmpdir, 'ferny-askpass')
+ fd = os.open(askpass_path, os.O_CREAT | os.O_WRONLY | os.O_CLOEXEC | os.O_EXCL | os.O_NOFOLLOW, 0o777)
+ try:
+ os.write(fd, __loader__.get_data(interaction_client.__file__)) # type: ignore
+ finally:
+ os.close(fd)
+ return askpass_path
+
+
+@contextlib.contextmanager
+def temporary_askpass(**kwargs: Any) -> Generator[str, None, None]:
+ with tempfile.TemporaryDirectory(**kwargs) as directory:
+ yield write_askpass_to_tmpdir(directory)
diff --git a/src/cockpit/_vendor/ferny/interaction_client.py b/src/cockpit/_vendor/ferny/interaction_client.py
new file mode 100755
index 0000000..ab57a59
--- /dev/null
+++ b/src/cockpit/_vendor/ferny/interaction_client.py
@@ -0,0 +1,41 @@
+#!/usr/bin/python3
+
+import array
+import io
+import os
+import socket
+import sys
+from typing import Sequence
+
+
+def command(stderr_fd: int, command: str, *args: object, fds: Sequence[int] = ()) -> None:
+ cmd_read, cmd_write = [io.open(*end) for end in zip(os.pipe(), 'rw')]
+
+ with cmd_write:
+ with cmd_read:
+ with socket.fromfd(stderr_fd, socket.AF_UNIX, socket.SOCK_STREAM) as sock:
+ fd_array = array.array('i', (cmd_read.fileno(), *fds))
+ sock.sendmsg([b'\0'], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fd_array)])
+
+ cmd_write.write(repr((command, args)))
+
+
+def askpass(stderr_fd: int, stdout_fd: int, args: 'list[str]', env: 'dict[str, str]') -> int:
+ ours, theirs = socket.socketpair()
+
+ with theirs:
+ command(stderr_fd, 'ferny.askpass', args, env, fds=(theirs.fileno(), stdout_fd))
+
+ with ours:
+ return int(ours.recv(16) or b'1')
+
+
+def main() -> None:
+ if len(sys.argv) == 1:
+ command(2, 'ferny.end', [])
+ else:
+ sys.exit(askpass(2, 1, sys.argv, dict(os.environ)))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/cockpit/_vendor/ferny/py.typed b/src/cockpit/_vendor/ferny/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/cockpit/_vendor/ferny/py.typed
diff --git a/src/cockpit/_vendor/ferny/session.py b/src/cockpit/_vendor/ferny/session.py
new file mode 100644
index 0000000..8cdc1d8
--- /dev/null
+++ b/src/cockpit/_vendor/ferny/session.py
@@ -0,0 +1,198 @@
+# ferny - asyncio SSH client library, using ssh(1)
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import asyncio
+import ctypes
+import functools
+import logging
+import os
+import shlex
+import signal
+import subprocess
+import tempfile
+from typing import Mapping, Sequence
+
+from . import ssh_errors
+from .interaction_agent import InteractionAgent, InteractionError, InteractionHandler, write_askpass_to_tmpdir
+
+prctl = ctypes.cdll.LoadLibrary('libc.so.6').prctl
+logger = logging.getLogger(__name__)
+PR_SET_PDEATHSIG = 1
+
+
+@functools.lru_cache()
+def has_feature(feature: str, teststr: str = 'x') -> bool:
+ try:
+ subprocess.check_output(['ssh', f'-o{feature} {teststr}', '-G', 'nonexisting'], stderr=subprocess.DEVNULL)
+ return True
+ except subprocess.CalledProcessError:
+ return False
+
+
+class SubprocessContext:
+ def wrap_subprocess_args(self, args: Sequence[str]) -> Sequence[str]:
+ """Return the args required to launch a process in the given context.
+
+ For example, this might return a vector with
+ ["sudo"]
+ or
+ ["flatpak-spawn", "--host"]
+ prepended.
+
+ It is also possible that more substantial changes may be performed.
+
+ This function is not permitted to modify its argument, although it may
+ (optionally) return it unmodified, if no changes are required.
+ """
+ return args
+
+ def wrap_subprocess_env(self, env: Mapping[str, str]) -> Mapping[str, str]:
+ """Return the envp required to launch a process in the given context.
+
+ For example, this might set the "SUDO_ASKPASS" environment variable, if
+ needed.
+
+ As with wrap_subprocess_args(), this function is not permitted to
+ modify its argument, although it may (optionally) return it unmodified
+ if no changes are required.
+ """
+ return env
+
+
+class Session(SubprocessContext, InteractionHandler):
+ # Set after .connect() called, even if failed
+ _controldir: 'tempfile.TemporaryDirectory | None' = None
+ _controlsock: 'str | None' = None
+
+ # Set if connected, else None
+ _process: 'asyncio.subprocess.Process | None' = None
+
+ async def connect(self,
+ destination: str,
+ handle_host_key: bool = False,
+ configfile: 'str | None' = None,
+ identity_file: 'str | None' = None,
+ login_name: 'str | None' = None,
+ options: 'Mapping[str, str] | None' = None,
+ pkcs11: 'str | None' = None,
+ port: 'int | None' = None,
+ interaction_responder: 'InteractionHandler | None' = None) -> None:
+ rundir = os.path.join(os.environ.get('XDG_RUNTIME_DIR', '/run'), 'ferny')
+ os.makedirs(rundir, exist_ok=True)
+ self._controldir = tempfile.TemporaryDirectory(dir=rundir)
+ self._controlsock = f'{self._controldir.name}/socket'
+
+ # In general, we can't guarantee an accessible and executable version
+ # of this file, but since it's small and we're making a temporary
+ # directory anyway, let's just copy it into place and use it from
+ # there.
+ askpass_path = write_askpass_to_tmpdir(self._controldir.name)
+
+ env = dict(os.environ)
+ env['SSH_ASKPASS'] = askpass_path
+ env['SSH_ASKPASS_REQUIRE'] = 'force'
+ # old SSH doesn't understand SSH_ASKPASS_REQUIRE and guesses based on DISPLAY instead
+ env['DISPLAY'] = '-'
+
+ args = [
+ '-M',
+ '-N',
+ '-S', self._controlsock,
+ '-o', 'PermitLocalCommand=yes',
+ '-o', f'LocalCommand={askpass_path}',
+ ]
+
+ if configfile is not None:
+ args.append(f'-F{configfile}')
+
+ if identity_file is not None:
+ args.append(f'-i{identity_file}')
+
+ if options is not None:
+ for key in options: # Note: Mapping may not have .items()
+ args.append(f'-o{key} {options[key]}')
+
+ if pkcs11 is not None:
+ args.append(f'-I{pkcs11}')
+
+ if port is not None:
+ args.append(f'-p{port}')
+
+ if login_name is not None:
+ args.append(f'-l{login_name}')
+
+ if handle_host_key and has_feature('KnownHostsCommand'):
+ args.extend([
+ '-o', f'KnownHostsCommand={askpass_path} %I %H %t %K %f',
+ '-o', 'StrictHostKeyChecking=yes',
+ ])
+
+ agent = InteractionAgent([interaction_responder] if interaction_responder is not None else [])
+
+ # SSH_ASKPASS_REQUIRE is not generally available, so use setsid
+ process = await asyncio.create_subprocess_exec(
+ *('/usr/bin/ssh', *args, destination), env=env,
+ start_new_session=True, stdin=asyncio.subprocess.DEVNULL,
+ stdout=asyncio.subprocess.DEVNULL, stderr=agent, # type: ignore
+ preexec_fn=lambda: prctl(PR_SET_PDEATHSIG, signal.SIGKILL))
+
+ # This is tricky: we need to clean up the subprocess, but only in case
+ # if failure. Otherwise, we keep it around.
+ try:
+ await agent.communicate()
+ assert os.path.exists(self._controlsock)
+ self._process = process
+ except InteractionError as exc:
+ await process.wait()
+ raise ssh_errors.get_exception_for_ssh_stderr(str(exc)) from None
+ except BaseException:
+ # If we get here because the InteractionHandler raised an
+ # exception then SSH might still be running, and may even attempt
+ # further interactions (ie: 2nd attempt for password). We already
+ # have our exception and don't need any more info. Kill it.
+ try:
+ process.kill()
+ except ProcessLookupError:
+ pass # already exited? good.
+ await process.wait()
+ raise
+
+ def is_connected(self) -> bool:
+ return self._process is not None
+
+ async def wait(self) -> None:
+ assert self._process is not None
+ await self._process.wait()
+
+ def exit(self) -> None:
+ assert self._process is not None
+ self._process.terminate()
+
+ async def disconnect(self) -> None:
+ self.exit()
+ await self.wait()
+
+ # Launching of processes
+ def wrap_subprocess_args(self, args: Sequence[str]) -> Sequence[str]:
+ assert self._controlsock is not None
+ # 1. We specify the hostname as the empty string: it will be ignored
+ # when ssh is trying to use the control socket, but in case the
+ # socket has stopped working, ssh will try to fall back to directly
+ # connecting, in which case an empty hostname will prevent that.
+ # 2. We need to quote the arguments — ssh will paste them together
+ # using only spaces, executing the result using the user's shell.
+ return ('ssh', '-S', self._controlsock, '', *map(shlex.quote, args))
diff --git a/src/cockpit/_vendor/ferny/ssh_askpass.py b/src/cockpit/_vendor/ferny/ssh_askpass.py
new file mode 100644
index 0000000..4bf5b50
--- /dev/null
+++ b/src/cockpit/_vendor/ferny/ssh_askpass.py
@@ -0,0 +1,199 @@
+import logging
+import re
+from typing import ClassVar, Match, Sequence
+
+from .interaction_agent import AskpassHandler
+
+logger = logging.getLogger(__name__)
+
+
+class AskpassPrompt:
+ """An askpass prompt resulting from a call to ferny-askpass.
+
+ stderr: the contents of stderr from before ferny-askpass was called.
+ Likely related to previous failed operations.
+ messages: all but the last line of the prompt as handed to ferny-askpass.
+ Usually contains context about the question.
+ prompt: the last line handed to ferny-askpass. The prompt itself.
+ """
+ stderr: str
+ messages: str
+ prompt: str
+
+ def __init__(self, prompt: str, messages: str, stderr: str) -> None:
+ self.stderr = stderr
+ self.messages = messages
+ self.prompt = prompt
+
+ def reply(self, response: str) -> None:
+ pass
+
+ def close(self) -> None:
+ pass
+
+ async def handle_via(self, responder: 'SshAskpassResponder') -> None:
+ try:
+ response = await self.dispatch(responder)
+ if response is not None:
+ self.reply(response)
+ finally:
+ self.close()
+
+ async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None':
+ return await responder.do_prompt(self)
+
+
+class SSHAskpassPrompt(AskpassPrompt):
+ # The valid answers to prompts of this type. If this is None then any
+ # answer is permitted. If it's a sequence then only answers from the
+ # sequence are permitted. If it's an empty sequence, then no answer is
+ # permitted (ie: the askpass callback should never return).
+ answers: 'ClassVar[Sequence[str] | None]' = None
+
+ # Patterns to capture. `_pattern` *must* match.
+ _pattern: ClassVar[str]
+ # `_extra_patterns` can fill in extra class attributes if they match.
+ _extra_patterns: ClassVar[Sequence[str]] = ()
+
+ def __init__(self, prompt: str, messages: str, stderr: str, match: Match) -> None:
+ super().__init__(prompt, messages, stderr)
+ self.__dict__.update(match.groupdict())
+
+ for pattern in self._extra_patterns:
+ extra_match = re.search(with_helpers(pattern), messages, re.M)
+ if extra_match is not None:
+ self.__dict__.update(extra_match.groupdict())
+
+
+# Specific prompts
+HELPERS = {
+ "%{algorithm}": r"(?P<algorithm>\b[-\w]+\b)",
+ "%{filename}": r"(?P<filename>.+)",
+ "%{fingerprint}": r"(?P<fingerprint>SHA256:[0-9A-Za-z+/]{43})",
+ "%{hostname}": r"(?P<hostname>[^ @']+)",
+ "%{pkcs11_id}": r"(?P<pkcs11_id>.+)",
+ "%{username}": r"(?P<username>[^ @']+)",
+}
+
+
+class SshPasswordPrompt(SSHAskpassPrompt):
+ _pattern = r"%{username}@%{hostname}'s password: "
+ username: 'str | None' = None
+ hostname: 'str | None' = None
+
+ async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None':
+ return await responder.do_password_prompt(self)
+
+
+class SshPassphrasePrompt(SSHAskpassPrompt):
+ _pattern = r"Enter passphrase for key '%{filename}': "
+ filename: str
+
+ async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None':
+ return await responder.do_passphrase_prompt(self)
+
+
+class SshFIDOPINPrompt(SSHAskpassPrompt):
+ _pattern = r"Enter PIN for %{algorithm} key %{filename}: "
+ algorithm: str
+ filename: str
+
+ async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None':
+ return await responder.do_fido_pin_prompt(self)
+
+
+class SshFIDOUserPresencePrompt(SSHAskpassPrompt):
+ _pattern = r"Confirm user presence for key %{algorithm} %{fingerprint}"
+ answers = ()
+ algorithm: str
+ fingerprint: str
+
+ async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None':
+ return await responder.do_fido_user_presence_prompt(self)
+
+
+class SshPKCS11PINPrompt(SSHAskpassPrompt):
+ _pattern = r"Enter PIN for '%{pkcs11_id}': "
+ pkcs11_id: str
+
+ async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None':
+ return await responder.do_pkcs11_pin_prompt(self)
+
+
+class SshHostKeyPrompt(SSHAskpassPrompt):
+ _pattern = r"Are you sure you want to continue connecting \(yes/no(/\[fingerprint\])?\)\? "
+ _extra_patterns = [
+ r"%{fingerprint}[.]$",
+ r"^%{algorithm} key fingerprint is",
+ r"^The fingerprint for the %{algorithm} key sent by the remote host is$"
+ ]
+ answers = ('yes', 'no')
+ algorithm: str
+ fingerprint: str
+
+ async def dispatch(self, responder: 'SshAskpassResponder') -> 'str | None':
+ return await responder.do_host_key_prompt(self)
+
+
+def with_helpers(pattern: str) -> str:
+ for name, helper in HELPERS.items():
+ pattern = pattern.replace(name, helper)
+
+ assert '%{' not in pattern
+ return pattern
+
+
+def categorize_ssh_prompt(string: str, stderr: str) -> AskpassPrompt:
+ classes = [
+ SshFIDOPINPrompt,
+ SshFIDOUserPresencePrompt,
+ SshHostKeyPrompt,
+ SshPKCS11PINPrompt,
+ SshPassphrasePrompt,
+ SshPasswordPrompt,
+ ]
+
+ # The last line is the line after the last newline character, excluding the
+ # optional final newline character. eg: "x\ny\nLAST\n" or "x\ny\nLAST"
+ second_last_newline = string.rfind('\n', 0, -1)
+ if second_last_newline >= 0:
+ last_line = string[second_last_newline + 1:]
+ extras = string[:second_last_newline + 1]
+ else:
+ last_line = string
+ extras = ''
+
+ for cls in classes:
+ pattern = with_helpers(cls._pattern)
+ match = re.fullmatch(pattern, last_line)
+ if match is not None:
+ return cls(last_line, extras, stderr, match)
+
+ return AskpassPrompt(last_line, extras, stderr)
+
+
+class SshAskpassResponder(AskpassHandler):
+ async def do_askpass(self, stderr: str, prompt: str, hint: str) -> 'str | None':
+ return await categorize_ssh_prompt(prompt, stderr).dispatch(self)
+
+ async def do_prompt(self, prompt: AskpassPrompt) -> 'str | None':
+ # Default fallback for unrecognised message types: unimplemented
+ return None
+
+ async def do_fido_pin_prompt(self, prompt: SshFIDOPINPrompt) -> 'str | None':
+ return await self.do_prompt(prompt)
+
+ async def do_fido_user_presence_prompt(self, prompt: SshFIDOUserPresencePrompt) -> 'str | None':
+ return await self.do_prompt(prompt)
+
+ async def do_host_key_prompt(self, prompt: SshHostKeyPrompt) -> 'str | None':
+ return await self.do_prompt(prompt)
+
+ async def do_pkcs11_pin_prompt(self, prompt: SshPKCS11PINPrompt) -> 'str | None':
+ return await self.do_prompt(prompt)
+
+ async def do_passphrase_prompt(self, prompt: SshPassphrasePrompt) -> 'str | None':
+ return await self.do_prompt(prompt)
+
+ async def do_password_prompt(self, prompt: SshPasswordPrompt) -> 'str | None':
+ return await self.do_prompt(prompt)
diff --git a/src/cockpit/_vendor/ferny/ssh_errors.py b/src/cockpit/_vendor/ferny/ssh_errors.py
new file mode 100644
index 0000000..f6d4784
--- /dev/null
+++ b/src/cockpit/_vendor/ferny/ssh_errors.py
@@ -0,0 +1,127 @@
+# ferny - asyncio SSH client library, using ssh(1)
+#
+# Copyright (C) 2023 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import ctypes
+import errno
+import os
+import re
+import socket
+from typing import ClassVar, Iterable, Match, Pattern
+
+
+class SshError(Exception):
+ PATTERN: ClassVar[Pattern]
+
+ def __init__(self, match: 'Match | None', stderr: str) -> None:
+ super().__init__(match.group(0) if match is not None else stderr)
+ self.stderr = stderr
+
+
+class SshAuthenticationError(SshError):
+ PATTERN = re.compile(r'^([^:]+): Permission denied \(([^()]+)\)\.$', re.M)
+
+ def __init__(self, match: Match, stderr: str) -> None:
+ super().__init__(match, stderr)
+ self.destination = match.group(1)
+ self.methods = match.group(2).split(',')
+ self.message = match.group(0)
+
+
+# generic host key error for OSes without KnownHostsCommand support
+class SshHostKeyError(SshError):
+ PATTERN = re.compile(r'^Host key verification failed.$', re.M)
+
+
+# specific errors for OSes with KnownHostsCommand
+class SshUnknownHostKeyError(SshHostKeyError):
+ PATTERN = re.compile(r'^No .* host key is known.*Host key verification failed.$', re.S | re.M)
+
+
+class SshChangedHostKeyError(SshHostKeyError):
+ PATTERN = re.compile(r'warning.*remote host identification has changed', re.I)
+
+
+# Functionality for mapping getaddrinfo()-family error messages to their
+# equivalent Python exceptions.
+def make_gaierror_map() -> 'Iterable[tuple[str, int]]':
+ libc = ctypes.CDLL(None)
+ libc.gai_strerror.restype = ctypes.c_char_p
+
+ for key in dir(socket):
+ if key.startswith('EAI_'):
+ errnum = getattr(socket, key)
+ yield libc.gai_strerror(errnum).decode('utf-8'), errnum
+
+
+gaierror_map = dict(make_gaierror_map())
+
+
+# Functionality for passing strerror() error messages to their equivalent
+# Python exceptions.
+# There doesn't seem to be an official API for turning an errno into the
+# correct subtype of OSError, and the list that cpython uses is hidden fairly
+# deeply inside of the implementation. This is basically copied from the
+# ADD_ERRNO() lines in _PyExc_InitState in cpython/Objects/exceptions.c
+oserror_subclass_map = dict((errnum, cls) for cls, errnum in [
+ (BlockingIOError, errno.EAGAIN),
+ (BlockingIOError, errno.EALREADY),
+ (BlockingIOError, errno.EINPROGRESS),
+ (BlockingIOError, errno.EWOULDBLOCK),
+ (BrokenPipeError, errno.EPIPE),
+ (BrokenPipeError, errno.ESHUTDOWN),
+ (ChildProcessError, errno.ECHILD),
+ (ConnectionAbortedError, errno.ECONNABORTED),
+ (ConnectionRefusedError, errno.ECONNREFUSED),
+ (ConnectionResetError, errno.ECONNRESET),
+ (FileExistsError, errno.EEXIST),
+ (FileNotFoundError, errno.ENOENT),
+ (IsADirectoryError, errno.EISDIR),
+ (NotADirectoryError, errno.ENOTDIR),
+ (InterruptedError, errno.EINTR),
+ (PermissionError, errno.EACCES),
+ (PermissionError, errno.EPERM),
+ (ProcessLookupError, errno.ESRCH),
+ (TimeoutError, errno.ETIMEDOUT),
+])
+
+
+def get_exception_for_ssh_stderr(stderr: str) -> Exception:
+ stderr = stderr.replace('\r\n', '\n') # fix line separators
+
+ # check for the specific error messages first, then for generic SshHostKeyError
+ for ssh_cls in [SshAuthenticationError, SshChangedHostKeyError, SshUnknownHostKeyError, SshHostKeyError]:
+ match = ssh_cls.PATTERN.search(stderr)
+ if match is not None:
+ return ssh_cls(match, stderr)
+
+ before, colon, after = stderr.rpartition(':')
+ if colon and after:
+ potential_strerror = after.strip()
+
+ # DNS lookup errors
+ if potential_strerror in gaierror_map:
+ errnum = gaierror_map[potential_strerror]
+ return socket.gaierror(errnum, stderr)
+
+ # Network connect errors
+ for errnum in errno.errorcode:
+ if os.strerror(errnum) == potential_strerror:
+ os_cls = oserror_subclass_map.get(errnum, OSError)
+ return os_cls(errnum, stderr)
+
+ # No match? Generic.
+ return SshError(None, stderr)
diff --git a/src/cockpit/_vendor/ferny/transport.py b/src/cockpit/_vendor/ferny/transport.py
new file mode 100644
index 0000000..ec9272b
--- /dev/null
+++ b/src/cockpit/_vendor/ferny/transport.py
@@ -0,0 +1,396 @@
+# ferny - asyncio SSH client library, using ssh(1)
+#
+# Copyright (C) 2023 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import asyncio
+import contextlib
+import logging
+import typing
+from typing import Any, Callable, Iterable, Sequence, TypeVar
+
+from .interaction_agent import InteractionAgent, InteractionHandler, get_running_loop
+from .ssh_errors import get_exception_for_ssh_stderr
+
+logger = logging.getLogger(__name__)
+
+P = TypeVar('P', bound=asyncio.Protocol)
+
+
+class SubprocessError(Exception):
+ returncode: int
+ stderr: str
+
+ def __init__(self, returncode: int, stderr: str) -> None:
+ super().__init__(returncode, stderr)
+ self.returncode = returncode
+ self.stderr = stderr
+
+
+class FernyTransport(asyncio.Transport, asyncio.SubprocessProtocol):
+ _agent: InteractionAgent
+ _exec_task: 'asyncio.Task[tuple[asyncio.SubprocessTransport, FernyTransport]]'
+ _is_ssh: bool
+ _protocol: asyncio.Protocol
+ _protocol_disconnected: bool = False
+
+ # These get initialized in connection_made() and once set, never get unset.
+ _subprocess_transport: 'asyncio.SubprocessTransport | None' = None
+ _stdin_transport: 'asyncio.WriteTransport | None' = None
+ _stdout_transport: 'asyncio.ReadTransport | None' = None
+
+ # We record events that might build towards a connection termination here
+ # and consider them from _consider_disconnect() in order to try to get the
+ # best possible Exception for the protocol, rather than just taking the
+ # first one (which is likely to be somewhat random).
+ _exception: 'Exception | None' = None
+ _stderr_output: 'str | None' = None
+ _returncode: 'int | None' = None
+ _transport_disconnected: bool = False
+ _closed: bool = False
+
+ @classmethod
+ def spawn(
+ cls: 'type[typing.Self]',
+ protocol_factory: Callable[[], P],
+ args: Sequence[str],
+ loop: 'asyncio.AbstractEventLoop | None' = None,
+ interaction_handlers: Sequence[InteractionHandler] = (),
+ is_ssh: bool = True,
+ **kwargs: Any
+ ) -> 'tuple[typing.Self, P]':
+ """Connects a FernyTransport to a protocol, using the given command.
+
+ This spawns an external command and connects the stdin and stdout of
+ the command to the protocol returned by the factory.
+
+ An instance of ferny.InteractionAgent is created and attached to the
+ stderr of the spawned process, using the provided handlers. It is the
+ responsibility of the caller to ensure that:
+ - a `ferny-askpass` client program is installed somewhere; and
+ - any relevant command-line arguments or environment variables are
+ passed correctly to the program to be spawned
+
+ This function returns immediately and never raises exceptions, assuming
+ all preconditions are met.
+
+ If spawning the process fails then connection_lost() will be
+ called with the relevant OSError, even before connection_made() is
+ called. This is somewhat non-standard behaviour, but is the easiest
+ way to report these errors without making this function async.
+
+ Once the process is successfully executed, connection_made() will be
+ called and the transport can be used as normal. connection_lost() will
+ be called if the process exits or another error occurs.
+
+ The return value of this function is the transport, but it exists in a
+ semi-initialized state. You can call .close() on it, but nothing else.
+ Once .connection_made() is called, you can call all the other
+ functions.
+
+ After you call this function, `.connection_lost()` will be called on
+ your Protocol, exactly once, no matter what. Until that happens, you
+ are responsible for holding a reference to the returned transport.
+
+ :param args: the full argv of the command to spawn
+ :param loop: the event loop to use. If none is provided, we use the
+ one which is (read: must be) currently running.
+ :param interaction_handlers: the handlers passed to the
+ InteractionAgent
+ :param is_ssh: whether we should attempt to interpret stderr as ssh
+ error messages
+ :param kwargs: anything else is passed through to `subprocess_exec()`
+ :returns: the usual `(Transport, Protocol)` pair
+ """
+ logger.debug('spawn(%r, %r, %r)', cls, protocol_factory, args)
+
+ protocol = protocol_factory()
+ self = cls(protocol)
+ self._is_ssh = is_ssh
+
+ if loop is None:
+ loop = get_running_loop()
+
+ self._agent = InteractionAgent(interaction_handlers, loop, self._interaction_completed)
+ kwargs.setdefault('stderr', self._agent.fileno())
+
+ # As of Python 3.12 this isn't really asynchronous (since it uses the
+ # subprocess module, which blocks while waiting for the exec() to
+ # complete in the child), but we have to deal with the complication of
+ # the async interface anyway. Since we, ourselves, want to export a
+ # non-async interface, that means that we need a task here and a
+ # bottom-half handler below.
+ self._exec_task = loop.create_task(loop.subprocess_exec(lambda: self, *args, **kwargs))
+
+ def exec_completed(task: asyncio.Task) -> None:
+ logger.debug('exec_completed(%r, %r)', self, task)
+ assert task is self._exec_task
+ try:
+ transport, me = task.result()
+ assert me is self
+ logger.debug(' success.')
+ except asyncio.CancelledError:
+ return # in that case, do nothing
+ except OSError as exc:
+ logger.debug(' OSError %r', exc)
+ self.close(exc)
+ return
+
+ # Our own .connection_made() handler should have gotten called by
+ # now. Make sure everything got filled in properly.
+ assert self._subprocess_transport is transport
+ assert self._stdin_transport is not None
+ assert self._stdout_transport is not None
+
+ # Ask the InteractionAgent to start processing stderr.
+ self._agent.start()
+
+ self._exec_task.add_done_callback(exec_completed)
+
+ return self, protocol
+
+ def __init__(self, protocol: asyncio.Protocol) -> None:
+ self._protocol = protocol
+
+ def _consider_disconnect(self) -> None:
+ logger.debug('_consider_disconnect(%r)', self)
+ # We cannot disconnect as long as any of these three things are happening
+ if not self._exec_task.done():
+ logger.debug(' exec_task still running %r', self._exec_task)
+ return
+
+ if self._subprocess_transport is not None and not self._transport_disconnected:
+ logger.debug(' transport still connected %r', self._subprocess_transport)
+ return
+
+ if self._stderr_output is None:
+ logger.debug(' agent still running')
+ return
+
+ # All conditions for disconnection are satisfied.
+ if self._protocol_disconnected:
+ logger.debug(' already disconnected')
+ return
+ self._protocol_disconnected = True
+
+ # Now we just need to determine what we report to the protocol...
+ if self._exception is not None:
+ # If we got an exception reported, that's our reason for closing.
+ logger.debug(' disconnect with exception %r', self._exception)
+ self._protocol.connection_lost(self._exception)
+ elif self._returncode == 0 or self._closed:
+ # If we called close() or have a zero return status, that's a clean
+ # exit, regardless of noise that might have landed in stderr.
+ logger.debug(' clean disconnect')
+ self._protocol.connection_lost(None)
+ elif self._is_ssh and self._returncode == 255:
+ # This is an error code due to an SSH failure. Try to interpret it.
+ logger.debug(' disconnect with ssh error %r', self._stderr_output)
+ self._protocol.connection_lost(get_exception_for_ssh_stderr(self._stderr_output))
+ else:
+ # Otherwise, report the stderr text and return code.
+ logger.debug(' disconnect with exit code %r, stderr %r', self._returncode, self._stderr_output)
+ # We surely have _returncode set here, since otherwise:
+ # - exec_task failed with an exception (which we handle above); or
+ # - we're still connected...
+ assert self._returncode is not None
+ self._protocol.connection_lost(SubprocessError(self._returncode, self._stderr_output))
+
+ def _interaction_completed(self, future: 'asyncio.Future[str]') -> None:
+ logger.debug('_interaction_completed(%r, %r)', self, future)
+ try:
+ self._stderr_output = future.result()
+ logger.debug(' stderr: %r', self._stderr_output)
+ except Exception as exc:
+ logger.debug(' exception: %r', exc)
+ self._stderr_output = '' # we need to set this in order to complete
+ self.close(exc)
+
+ self._consider_disconnect()
+
+ # BaseProtocol implementation
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ logger.debug('connection_made(%r, %r)', self, transport)
+ assert isinstance(transport, asyncio.SubprocessTransport)
+ self._subprocess_transport = transport
+
+ stdin_transport = transport.get_pipe_transport(0)
+ assert isinstance(stdin_transport, asyncio.WriteTransport)
+ self._stdin_transport = stdin_transport
+
+ stdout_transport = transport.get_pipe_transport(1)
+ assert isinstance(stdout_transport, asyncio.ReadTransport)
+ self._stdout_transport = stdout_transport
+
+ stderr_transport = transport.get_pipe_transport(2)
+ assert stderr_transport is None
+
+ logger.debug('calling connection_made(%r, %r)', self, self._protocol)
+ self._protocol.connection_made(self)
+
+ def connection_lost(self, exc: 'Exception | None') -> None:
+ logger.debug('connection_lost(%r, %r)', self, exc)
+ if self._exception is None:
+ self._exception = exc
+ self._transport_disconnected = True
+ self._consider_disconnect()
+
+ # SubprocessProtocol implementation
+ def pipe_data_received(self, fd: int, data: bytes) -> None:
+ logger.debug('pipe_data_received(%r, %r, %r)', self, fd, len(data))
+ assert fd == 1 # stderr is handled separately
+ self._protocol.data_received(data)
+
+ def pipe_connection_lost(self, fd: int, exc: 'Exception | None') -> None:
+ logger.debug('pipe_connection_lost(%r, %r, %r)', self, fd, exc)
+ assert fd in (0, 1) # stderr is handled separately
+
+ # We treat this as a clean close
+ if isinstance(exc, BrokenPipeError):
+ exc = None
+
+ # Record serious errors to propagate them to the protocol
+ # If this is a clean exit on stdout, report an EOF
+ if exc is not None:
+ self.close(exc)
+ elif fd == 1 and not self._closed:
+ if not self._protocol.eof_received():
+ self.close()
+
+ def process_exited(self) -> None:
+ logger.debug('process_exited(%r)', self)
+ assert self._subprocess_transport is not None
+ self._returncode = self._subprocess_transport.get_returncode()
+ logger.debug(' ._returncode = %r', self._returncode)
+ self._agent.force_completion()
+
+ def pause_writing(self) -> None:
+ logger.debug('pause_writing(%r)', self)
+ self._protocol.pause_writing()
+
+ def resume_writing(self) -> None:
+ logger.debug('resume_writing(%r)', self)
+ self._protocol.resume_writing()
+
+ # Transport implementation. Most of this is straight delegation.
+ def close(self, exc: 'Exception | None' = None) -> None:
+ logger.debug('close(%r, %r)', self, exc)
+ self._closed = True
+ if self._exception is None:
+ logger.debug(' setting exception %r', exc)
+ self._exception = exc
+ if not self._exec_task.done():
+ logger.debug(' cancelling _exec_task')
+ self._exec_task.cancel()
+ if self._subprocess_transport is not None:
+ logger.debug(' closing _subprocess_transport')
+ # https://github.com/python/cpython/issues/112800
+ with contextlib.suppress(PermissionError):
+ self._subprocess_transport.close()
+ self._agent.force_completion()
+
+ def is_closing(self) -> bool:
+ assert self._subprocess_transport is not None
+ return self._subprocess_transport.is_closing()
+
+ def get_extra_info(self, name: str, default: object = None) -> object:
+ assert self._subprocess_transport is not None
+ return self._subprocess_transport.get_extra_info(name, default)
+
+ def set_protocol(self, protocol: asyncio.BaseProtocol) -> None:
+ assert isinstance(protocol, asyncio.Protocol)
+ self._protocol = protocol
+
+ def get_protocol(self) -> asyncio.Protocol:
+ return self._protocol
+
+ def is_reading(self) -> bool:
+ assert self._stdout_transport is not None
+ try:
+ return self._stdout_transport.is_reading()
+ except NotImplementedError:
+ # This is (incorrectly) unimplemented before Python 3.11
+ return not self._stdout_transport._paused # type:ignore[attr-defined]
+ except AttributeError:
+ # ...and in Python 3.6 it's even worse
+ try:
+ selector = self._stdout_transport._loop._selector # type:ignore[attr-defined]
+ selector.get_key(self._stdout_transport._fileno) # type:ignore[attr-defined]
+ return True
+ except KeyError:
+ return False
+
+ def pause_reading(self) -> None:
+ assert self._stdout_transport is not None
+ self._stdout_transport.pause_reading()
+
+ def resume_reading(self) -> None:
+ assert self._stdout_transport is not None
+ self._stdout_transport.resume_reading()
+
+ def abort(self) -> None:
+ assert self._stdin_transport is not None
+ assert self._subprocess_transport is not None
+ self._stdin_transport.abort()
+ self._subprocess_transport.kill()
+
+ def can_write_eof(self) -> bool:
+ assert self._stdin_transport is not None
+ return self._stdin_transport.can_write_eof() # will always be True
+
+ def get_write_buffer_size(self) -> int:
+ assert self._stdin_transport is not None
+ return self._stdin_transport.get_write_buffer_size()
+
+ def get_write_buffer_limits(self) -> 'tuple[int, int]':
+ assert self._stdin_transport is not None
+ return self._stdin_transport.get_write_buffer_limits()
+
+ def set_write_buffer_limits(self, high: 'int | None' = None, low: 'int | None' = None) -> None:
+ assert self._stdin_transport is not None
+ return self._stdin_transport.set_write_buffer_limits(high, low)
+
+ def write(self, data: bytes) -> None:
+ assert self._stdin_transport is not None
+ return self._stdin_transport.write(data)
+
+ def writelines(self, list_of_data: Iterable[bytes]) -> None:
+ assert self._stdin_transport is not None
+ return self._stdin_transport.writelines(list_of_data)
+
+ def write_eof(self) -> None:
+ assert self._stdin_transport is not None
+ return self._stdin_transport.write_eof()
+
+ # We don't really implement SubprocessTransport, but provide these as
+ # "extras" to our user.
+ def get_pid(self) -> int:
+ assert self._subprocess_transport is not None
+ return self._subprocess_transport.get_pid()
+
+ def get_returncode(self) -> 'int | None':
+ return self._returncode
+
+ def kill(self) -> None:
+ assert self._subprocess_transport is not None
+ self._subprocess_transport.kill()
+
+ def send_signal(self, number: int) -> None:
+ assert self._subprocess_transport is not None
+ self._subprocess_transport.send_signal(number)
+
+ def terminate(self) -> None:
+ assert self._subprocess_transport is not None
+ self._subprocess_transport.terminate()
diff --git a/src/cockpit/_vendor/systemd_ctypes/__init__.py b/src/cockpit/_vendor/systemd_ctypes/__init__.py
new file mode 100644
index 0000000..87a2f5e
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/__init__.py
@@ -0,0 +1,39 @@
+# systemd_ctypes
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""systemd_ctypes"""
+
+__version__ = "0"
+
+from .bus import Bus, BusError, BusMessage
+from .bustypes import BusType, JSONEncoder, Variant
+from .event import Event, EventLoopPolicy, run_async
+from .pathwatch import Handle, PathWatch
+
+__all__ = [
+ "Bus",
+ "BusError",
+ "BusMessage",
+ "BusType",
+ "Event",
+ "EventLoopPolicy",
+ "Handle",
+ "JSONEncoder",
+ "PathWatch",
+ "Variant",
+ "run_async",
+]
diff --git a/src/cockpit/_vendor/systemd_ctypes/bus.py b/src/cockpit/_vendor/systemd_ctypes/bus.py
new file mode 100644
index 0000000..35a524d
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/bus.py
@@ -0,0 +1,861 @@
+# systemd_ctypes
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import asyncio
+import enum
+import logging
+import typing
+from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Union
+
+from . import bustypes, introspection, libsystemd
+from .librarywrapper import WeakReference, byref
+
+logger = logging.getLogger(__name__)
+
+
+class BusError(Exception):
+ """An exception corresponding to a D-Bus error message
+
+ This exception is raised by the method call methods. You can also raise it
+ from your own method handlers. It can also be passed directly to functions
+ such as Message.reply_method_error().
+
+ :name: the 'code' of the error, like org.freedesktop.DBus.Error.UnknownMethod
+ :message: a human-readable description of the error
+ """
+ def __init__(self, name: str, message: str):
+ super().__init__(f'{name}: {message}')
+ self.name = name
+ self.message = message
+
+
+class BusMessage(libsystemd.sd_bus_message):
+ """A message, received from or to be sent over D-Bus
+
+ This is the low-level interface to receiving and sending individual
+ messages over D-Bus. You won't normally need to use it.
+
+ A message is associated with a particular bus. You can create messages for
+ a bus with Bus.message_new_method_call() or Bus.message_new_signal(). You
+ can create replies to method calls with Message.new_method_return() or
+ Message.new_method_error(). You can append arguments with Message.append()
+ and send the message with Message.send().
+ """
+ def get_bus(self) -> 'Bus':
+ """Get the bus that a message is associated with.
+
+ This is the bus that a message came from or will be sent on. Every
+ message has an associated bus, and it cannot be changed.
+
+ :returns: the Bus
+ """
+ return Bus.ref(self._get_bus())
+
+ def get_error(self) -> Optional[BusError]:
+ """Get the BusError from a message.
+
+ :returns: a BusError for an error message, or None for a non-error message
+ """
+ error = self._get_error()
+ if error:
+ return BusError(*error.contents.get())
+ else:
+ return None
+
+ def new_method_return(self, signature: str = '', *args: Any) -> 'BusMessage':
+ """Create a new (successful) return message as a reply to this message.
+
+ This only makes sense when performed on a method call message.
+
+ :signature: The signature of the result, as a string.
+ :args: The values to send, conforming to the signature string.
+
+ :returns: the reply message
+ """
+ reply = BusMessage()
+ self._new_method_return(byref(reply))
+ reply.append(signature, *args)
+ return reply
+
+ def new_method_error(self, error: Union[BusError, OSError]) -> 'BusMessage':
+ """Create a new error message as a reply to this message.
+
+ This only makes sense when performed on a method call message.
+
+ :error: BusError or OSError of the error to send
+
+ :returns: the reply message
+ """
+ reply = BusMessage()
+ if isinstance(error, BusError):
+ self._new_method_errorf(byref(reply), error.name, "%s", error.message)
+ else:
+ assert isinstance(error, OSError)
+ self._new_method_errnof(byref(reply), error.errno, "%s", str(error))
+ return reply
+
+ def append_arg(self, typestring: str, arg: Any) -> None:
+ """Append a single argument to the message.
+
+ :typestring: a single typestring, such as 's', or 'a{sv}'
+ :arg: the argument to append, matching the typestring
+ """
+ type_, = bustypes.from_signature(typestring)
+ type_.writer(self, arg)
+
+ def append(self, signature: str, *args: Any) -> None:
+ """Append zero or more arguments to the message.
+
+ :signature: concatenated typestrings, such 'a{sv}' (one arg), or 'ss' (two args)
+ :args: one argument for each type string in the signature
+ """
+ types = bustypes.from_signature(signature)
+ assert len(types) == len(args), f'call args {args} have different length than signature {signature}'
+ for type_, arg in zip(types, args):
+ type_.writer(self, arg)
+
+ def get_body(self) -> Tuple[object, ...]:
+ """Gets the body of a message.
+
+ Possible return values are (), ('single',), or ('x', 'y'). If you
+ check the signature of the message using Message.has_signature() then
+ you can use tuple unpacking.
+
+ single, = message.get_body()
+
+ x, y = other_message.get_body()
+
+ :returns: an n-tuple containing one value per argument in the message
+ """
+ self.rewind(True)
+ types = bustypes.from_signature(self.get_signature(True))
+ return tuple(type_.reader(self) for type_ in types)
+
+ def send(self) -> bool: # Literal[True]
+ """Sends a message on the bus that it was created for.
+
+ :returns: True
+ """
+ self.get_bus().send(self, None)
+ return True
+
+ def reply_method_error(self, error: Union[BusError, OSError]) -> bool: # Literal[True]
+ """Sends an error as a reply to a method call message.
+
+ :error: A BusError or OSError
+
+ :returns: True
+ """
+ return self.new_method_error(error).send()
+
+ def reply_method_return(self, signature: str = '', *args: Any) -> bool: # Literal[True]
+ """Sends a return value as a reply to a method call message.
+
+ :signature: The signature of the result, as a string.
+ :args: The values to send, conforming to the signature string.
+
+ :returns: True
+ """
+ return self.new_method_return(signature, *args).send()
+
+ def _coroutine_task_complete(self, out_type: bustypes.MessageType, task: asyncio.Task) -> None:
+ try:
+ self.reply_method_function_return_value(out_type, task.result())
+ except (BusError, OSError) as exc:
+ self.reply_method_error(exc)
+
+ def reply_method_function_return_value(self,
+ out_type: bustypes.MessageType,
+ return_value: Any) -> bool: # Literal[True]:
+ """Sends the result of a function call as a reply to a method call message.
+
+ This call does a bit of magic: it adapts from the usual Python return
+ value conventions (where the return value is ``None``, a single value,
+ or a tuple) to the normal D-Bus return value conventions (where the
+ result is always a tuple).
+
+ Additionally, if the value is found to be a coroutine, a task is
+ created to run the coroutine to completion and return the result
+ (including exception handling).
+
+ :out_types: The types of the return values, as an iterable.
+ :return_value: The return value of a Python function call.
+
+ :returns: True
+ """
+ if asyncio.coroutines.iscoroutine(return_value):
+ task = asyncio.create_task(return_value)
+ task.add_done_callback(lambda task: self._coroutine_task_complete(out_type, task))
+ return True
+
+ reply = self.new_method_return()
+ # In the general case, a function returns an n-tuple, but...
+ if len(out_type) == 0:
+ # Functions with no return value return None.
+ assert return_value is None
+ elif len(out_type) == 1:
+ # Functions with a single return value return that value.
+ out_type.write(reply, return_value)
+ else:
+ # (general case) n return values are handled as an n-tuple.
+ assert len(out_type) == len(return_value)
+ out_type.write(reply, *return_value)
+ return reply.send()
+
+
+class Slot(libsystemd.sd_bus_slot):
+ def __init__(self, callback: Callable[[BusMessage], bool]):
+ def handler(message: WeakReference, _data: object, _err: object) -> int:
+ return 1 if callback(BusMessage.ref(message)) else 0
+ self.trampoline = libsystemd.sd_bus_message_handler_t(handler)
+
+
+if typing.TYPE_CHECKING:
+ FutureMessage = asyncio.Future[BusMessage]
+else:
+ # Python 3.6 can't subscript asyncio.Future
+ FutureMessage = asyncio.Future
+
+
+class PendingCall(Slot):
+ future: FutureMessage
+
+ def __init__(self) -> None:
+ future = asyncio.get_running_loop().create_future()
+
+ def done(message: BusMessage) -> bool:
+ error = message.get_error()
+ if future.cancelled():
+ return True
+ if error is not None:
+ future.set_exception(error)
+ else:
+ future.set_result(message)
+ return True
+
+ super().__init__(done)
+ self.future = future
+
+
+class Bus(libsystemd.sd_bus):
+ _default_system_instance = None
+ _default_user_instance = None
+
+ class NameFlags(enum.IntFlag):
+ DEFAULT = 0
+ REPLACE_EXISTING = 1 << 0
+ ALLOW_REPLACEMENT = 1 << 1
+ QUEUE = 1 << 2
+
+ @staticmethod
+ def new(
+ fd: Optional[int] = None,
+ address: Optional[str] = None,
+ bus_client: bool = False,
+ server: bool = False,
+ start: bool = True,
+ attach_event: bool = True
+ ) -> 'Bus':
+ bus = Bus()
+ Bus._new(byref(bus))
+ if address is not None:
+ bus.set_address(address)
+ if fd is not None:
+ bus.set_fd(fd, fd)
+ if bus_client:
+ bus.set_bus_client(True)
+ if server:
+ bus.set_server(True, libsystemd.sd_id128())
+ if address is not None or fd is not None:
+ if start:
+ bus.start()
+ if attach_event:
+ bus.attach_event(None, 0)
+ return bus
+
+ @staticmethod
+ def default_system(attach_event: bool = True) -> 'Bus':
+ if Bus._default_system_instance is None:
+ Bus._default_system_instance = Bus()
+ Bus._default_system(byref(Bus._default_system_instance))
+ if attach_event:
+ Bus._default_system_instance.attach_event(None, 0)
+ return Bus._default_system_instance
+
+ @staticmethod
+ def default_user(attach_event: bool = True) -> 'Bus':
+ if Bus._default_user_instance is None:
+ Bus._default_user_instance = Bus()
+ Bus._default_user(byref(Bus._default_user_instance))
+ if attach_event:
+ Bus._default_user_instance.attach_event(None, 0)
+ return Bus._default_user_instance
+
+ def message_new_method_call(
+ self,
+ destination: Optional[str],
+ path: str,
+ interface: str,
+ member: str,
+ types: str = '',
+ *args: object
+ ) -> BusMessage:
+ message = BusMessage()
+ self._message_new_method_call(byref(message), destination, path, interface, member)
+ message.append(types, *args)
+ return message
+
+ def message_new_signal(
+ self, path: str, interface: str, member: str, types: str = '', *args: object
+ ) -> BusMessage:
+ message = BusMessage()
+ self._message_new_signal(byref(message), path, interface, member)
+ message.append(types, *args)
+ return message
+
+ def call(self, message: BusMessage, timeout: Optional[int] = None) -> BusMessage:
+ reply = BusMessage()
+ error = libsystemd.sd_bus_error()
+ try:
+ self._call(message, timeout or 0, byref(error), byref(reply))
+ return reply
+ except OSError as exc:
+ raise BusError(*error.get()) from exc
+
+ def call_method(
+ self,
+ destination: str,
+ path: str,
+ interface: str,
+ member: str,
+ types: str = '',
+ *args: object,
+ timeout: Optional[int] = None
+ ) -> Tuple[object, ...]:
+ logger.debug('Doing sync method call %s %s %s %s %s %s',
+ destination, path, interface, member, types, args)
+ message = self.message_new_method_call(destination, path, interface, member, types, *args)
+ message = self.call(message, timeout)
+ return message.get_body()
+
+ async def call_async(
+ self,
+ message: BusMessage,
+ timeout: Optional[int] = None
+ ) -> BusMessage:
+ pending = PendingCall()
+ self._call_async(byref(pending), message, pending.trampoline, pending.userdata, timeout or 0)
+ return await pending.future
+
+ async def call_method_async(
+ self,
+ destination: Optional[str],
+ path: str,
+ interface: str,
+ member: str,
+ types: str = '',
+ *args: object,
+ timeout: Optional[int] = None
+ ) -> Tuple[object, ...]:
+ logger.debug('Doing async method call %s %s %s %s %s %s',
+ destination, path, interface, member, types, args)
+ message = self.message_new_method_call(destination, path, interface, member, types, *args)
+ message = await self.call_async(message, timeout)
+ return message.get_body()
+
+ def add_match(self, rule: str, handler: Callable[[BusMessage], bool]) -> Slot:
+ slot = Slot(handler)
+ self._add_match(byref(slot), rule, slot.trampoline, slot.userdata)
+ return slot
+
+ def add_object(self, path: str, obj: 'BaseObject') -> Slot:
+ slot = Slot(obj.message_received)
+ self._add_object(byref(slot), path, slot.trampoline, slot.userdata)
+ obj.registered_on_bus(self, path)
+ return slot
+
+
+class BaseObject:
+ """Base object type for exporting objects on the bus
+
+ This is the lowest-level class that can be passed to Bus.add_object().
+
+ If you want to directly subclass this, you'll need to implement
+ `message_received()`.
+
+ Subclassing from `bus.Object` is probably a better choice.
+ """
+ _dbus_bus: Optional[Bus] = None
+ _dbus_path: Optional[str] = None
+
+ def registered_on_bus(self, bus: Bus, path: str) -> None:
+ """Report that an instance was exported on a given bus and path.
+
+ This is used so that the instance knows where to send signals.
+ Bus.add_object() calls this: you probably shouldn't call this on your
+ own.
+ """
+ self._dbus_bus = bus
+ self._dbus_path = path
+
+ self.registered()
+
+ def registered(self) -> None:
+ """Called after an object has been registered on the bus
+
+ This is the correct method to implement to do some initial work that
+ needs to be done after registration. The default implementation does
+ nothing.
+ """
+ pass
+
+ def emit_signal(
+ self, interface: str, name: str, signature: str, *args: Any
+ ) -> bool:
+ """Emit a D-Bus signal on this object
+
+ The object must have been exported on the bus with Bus.add_object().
+
+ :interface: the interface of the signal
+ :name: the 'member' name of the signal to emit
+ :signature: the type signature, as a string
+ :args: the arguments, according to the signature
+ :returns: True
+ """
+ assert self._dbus_bus is not None
+ assert self._dbus_path is not None
+ return self._dbus_bus.message_new_signal(self._dbus_path, interface, name, signature, *args).send()
+
+ def message_received(self, message: BusMessage) -> bool:
+ """Called when a message is received for this object
+
+ This is the lowest level interface to the BaseObject. You need to
+ handle method calls, properties, and introspection.
+
+ You are expected to handle the message and return True. Normally this
+ means that you send a reply. If you don't want to handle the message,
+ return False and other handlers will have a chance to run. If no
+ handler handles the message, systemd will generate a suitable error
+ message and send that, instead.
+
+ :message: the message that was received
+ :returns: True if the message was handled
+ """
+ raise NotImplementedError
+
+
+class Interface:
+ """The high-level base class for defining D-Bus interfaces
+
+ This class provides high-level APIs for defining methods, properties, and
+ signals, as well as implementing introspection.
+
+ On its own, this class doesn't provide a mechanism for exporting anything
+ on the bus. The Object class does that, and you'll generally want to
+ subclass from it, as it contains several built-in standard interfaces
+ (introspection, properties, etc.).
+
+ The name of your class will be interpreted as a D-Bus interface name.
+ Underscores are converted to dots. No case conversion is performed. If
+ the interface name can't be represented using this scheme, or if you'd like
+ to name your class differently, you can provide an interface= kwarg to the
+ class definition.
+
+ class com_example_Interface(bus.Object):
+ pass
+
+ class MyInterface(bus.Object, interface='org.cockpit_project.Interface'):
+ pass
+
+ The methods, properties, and signals which are visible from D-Bus are
+ defined using helper classes with the corresponding names (Method,
+ Property, Signal). You should use normal Python snake_case conventions for
+ the member names: they will automatically be converted to CamelCase by
+ splitting on underscore and converting the first letter of each resulting
+ word to uppercase. For example, `method_name` becomes `MethodName`.
+
+ Each Method, Property, or Signal constructor takes an optional name= kwargs
+ to override the automatic name conversion convention above.
+
+ An example class might look like:
+
+ class com_example_MyObject(bus.Object):
+ created = bus.Interface.Signal('s', 'i')
+ renames = bus.Interface.Property('u', value=0)
+ name = bus.Interface.Property('s', 'undefined')
+
+ @bus.Interface.Method(out_types=(), in_types='s')
+ def rename(self, name):
+ self.renames += 1
+ self.name = name
+
+ def registered(self):
+ self.created('Hello', 42)
+
+ See the documentation for the Method, Property, and Signal classes for
+ more information and examples.
+ """
+
+ # Class variables
+ _dbus_interfaces: Dict[str, Dict[str, Dict[str, Any]]]
+ _dbus_members: Optional[Tuple[str, Dict[str, Dict[str, Any]]]]
+
+ # Instance variables: stored in Python form
+ _dbus_property_values: Optional[Dict[str, Any]] = None
+
+ @classmethod
+ def __init_subclass__(cls, interface: Optional[str] = None) -> None:
+ if interface is None:
+ assert '__' not in cls.__name__, 'Class name cannot contain sequential underscores'
+ interface = cls.__name__.replace('_', '.')
+
+ # This is the information for this subclass directly
+ members: Dict[str, Dict[str, Interface._Member]] = {'methods': {}, 'properties': {}, 'signals': {}}
+ for name, member in cls.__dict__.items():
+ if isinstance(member, Interface._Member):
+ member.setup(interface, name, members)
+
+ # We only store the information if something was actually defined
+ if sum(len(category) for category in members.values()) > 0:
+ cls._dbus_members = (interface, members)
+
+ # This is the information for this subclass, with all its ancestors
+ cls._dbus_interfaces = dict(ancestor.__dict__['_dbus_members']
+ for ancestor in cls.mro()
+ if '_dbus_members' in ancestor.__dict__)
+
+ @classmethod
+ def _find_interface(cls, interface: str) -> Dict[str, Dict[str, '_Member']]:
+ try:
+ return cls._dbus_interfaces[interface]
+ except KeyError as exc:
+ raise Object.Method.Unhandled from exc
+
+ @classmethod
+ def _find_category(cls, interface: str, category: str) -> Dict[str, '_Member']:
+ return cls._find_interface(interface)[category]
+
+ @classmethod
+ def _find_member(cls, interface: str, category: str, member: str) -> '_Member':
+ members = cls._find_category(interface, category)
+ try:
+ return members[member]
+ except KeyError as exc:
+ raise Object.Method.Unhandled from exc
+
+ class _Member:
+ _category: str # filled in from subclasses
+
+ _python_name: Optional[str] = None
+ _name: Optional[str] = None
+ _interface: Optional[str] = None
+ _description: Optional[Dict[str, Any]]
+
+ def __init__(self, name: Optional[str] = None) -> None:
+ self._python_name = None
+ self._interface = None
+ self._name = name
+
+ def setup(self, interface: str, name: str, members: Dict[str, Dict[str, 'Interface._Member']]) -> None:
+ self._python_name = name # for error messages
+ if self._name is None:
+ self._name = ''.join(word.title() for word in name.split('_'))
+ self._interface = interface
+ self._description = self._describe()
+ members[self._category][self._name] = self
+
+ def _describe(self) -> Dict[str, Any]:
+ raise NotImplementedError
+
+ def __getitem__(self, key: str) -> Any:
+ # Acts as an adaptor for dict accesses from introspection.to_xml()
+ assert self._description is not None
+ return self._description[key]
+
+ class Property(_Member):
+ """Defines a D-Bus property on an interface
+
+ There are two main ways to define properties: with and without getters.
+ If you define a property without a getter, then you must provide a
+ value (via the value= kwarg). In this case, the property value is
+ maintained internally and can be accessed from Python in the usual way.
+ Change signals are sent automatically.
+
+ class MyObject(bus.Object):
+ counter = bus.Interface.Property('i', value=0)
+
+ a = MyObject()
+ a.counter = 5
+ a.counter += 1
+ print(a.counter)
+
+ The other way to define properties is with a getter function. In this
+ case, you can read from the property in the normal way, but not write
+ to it. You are responsible for emitting change signals for yourself.
+ You must not provide the value= kwarg.
+
+ class MyObject(bus.Object):
+ _counter = 0
+
+ counter = bus.Interface.Property('i')
+ @counter.getter
+ def get_counter(self):
+ return self._counter
+
+ @counter.setter
+ def set_counter(self, value):
+ self._counter = value
+ self.property_changed('Counter')
+
+ In either case, you can provide a setter function. This function has
+ no impact on Python code, but makes the property writable from the view
+ of D-Bus. Your setter will be called when a Properties.Set() call is
+ made, and no other action will be performed. A trivial implementation
+ might look like:
+
+ class MyObject(bus.Object):
+ counter = bus.Interface.Property('i', value=0)
+ @counter.setter
+ def set_counter(self, value):
+ # we got a request to set the counter from D-Bus
+ self.counter = value
+
+ In all cases, the first (and only mandatory) argument to the
+ constructor is the D-Bus type of the property.
+
+ Your getter and setter functions can be provided by kwarg to the
+ constructor. You can also give a name= kwarg to override the default
+ name conversion scheme.
+ """
+ _category = 'properties'
+
+ _getter: Optional[Callable[[Any], Any]]
+ _setter: Optional[Callable[[Any, Any], None]]
+ _type: bustypes.Type
+ _value: Any
+
+ def __init__(self, type_string: str,
+ value: Any = None,
+ name: Optional[str] = None,
+ getter: Optional[Callable[[Any], Any]] = None,
+ setter: Optional[Callable[[Any, Any], None]] = None):
+ assert value is None or getter is None, 'A property cannot have both a value and a getter'
+
+ super().__init__(name=name)
+ self._getter = getter
+ self._setter = setter
+ self._type, = bustypes.from_signature(type_string)
+ self._value = value
+
+ def _describe(self) -> Dict[str, Any]:
+ return {'type': self._type.typestring, 'flags': 'r' if self._setter is None else 'w'}
+
+ def __get__(self, obj: 'Object', cls: Optional[type] = None) -> Any:
+ assert self._name is not None
+ if obj is None:
+ return self
+ if self._getter is not None:
+ return self._getter.__get__(obj, cls)()
+ elif self._value is not None:
+ if obj._dbus_property_values is not None:
+ return obj._dbus_property_values.get(self._name, self._value)
+ else:
+ return self._value
+ else:
+ raise AttributeError(f"'{obj.__class__.__name__}' property '{self._python_name}' "
+ f"was not properly initialised: use either the 'value=' kwarg or "
+ f"the @'{self._python_name}.getter' decorator")
+
+ def __set__(self, obj: 'Object', value: Any) -> None:
+ assert self._name is not None
+ if self._getter is not None:
+ raise AttributeError(f"Cannot directly assign '{obj.__class__.__name__}' "
+ "property '{self._python_name}' because it has a getter")
+ if obj._dbus_property_values is None:
+ obj._dbus_property_values = {}
+ obj._dbus_property_values[self._name] = value
+ if obj._dbus_bus is not None:
+ obj.properties_changed(self._interface, {self._name: bustypes.Variant(value, self._type)}, [])
+
+ def to_dbus(self, obj: 'Object') -> bustypes.Variant:
+ return bustypes.Variant(self.__get__(obj), self._type)
+
+ def from_dbus(self, obj: 'Object', value: bustypes.Variant) -> None:
+ if self._setter is None or self._type != value.type:
+ raise Object.Method.Unhandled
+ self._setter.__get__(obj)(value.value)
+
+ def getter(self, getter: Callable[[Any], Any]) -> Callable[[Any], Any]:
+ if self._value is not None:
+ raise ValueError('A property cannot have both a value and a getter')
+ if self._getter is not None:
+ raise ValueError('This property already has a getter')
+ self._getter = getter
+ return getter
+
+ def setter(self, setter: Callable[[Any, Any], None]) -> Callable[[Any, Any], None]:
+ self._setter = setter
+ return setter
+
+ class Signal(_Member):
+ """Defines a D-Bus signal on an interface
+
+ This is a callable which will result in the signal being emitted.
+
+ The constructor takes the types of the arguments, each one as a
+ separate parameter. For example:
+
+ properties_changed = Interface.Signal('s', 'a{sv}', 'as')
+
+ You can give a name= kwarg to override the default name conversion
+ scheme.
+ """
+ _category = 'signals'
+ _type: bustypes.MessageType
+
+ def __init__(self, *out_types: str, name: Optional[str] = None) -> None:
+ super().__init__(name=name)
+ self._type = bustypes.MessageType(out_types)
+
+ def _describe(self) -> Dict[str, Any]:
+ return {'in': self._type.typestrings}
+
+ def __get__(self, obj: 'Object', cls: Optional[type] = None) -> Callable[..., None]:
+ def emitter(obj: Object, *args: Any) -> None:
+ assert self._interface is not None
+ assert self._name is not None
+ assert obj._dbus_bus is not None
+ assert obj._dbus_path is not None
+ message = obj._dbus_bus.message_new_signal(obj._dbus_path, self._interface, self._name)
+ self._type.write(message, *args)
+ message.send()
+ return emitter.__get__(obj, cls)
+
+ class Method(_Member):
+ """Defines a D-Bus method on an interface
+
+ This is a function decorator which marks a given method for export.
+
+ The constructor takes two arguments: the type of the output arguments,
+ and the type of the input arguments. Both should be given as a
+ sequence.
+
+ @Interface.Method(['a{sv}'], ['s'])
+ def get_all(self, interface):
+ ...
+
+ You can give a name= kwarg to override the default name conversion
+ scheme.
+ """
+ _category = 'methods'
+
+ class Unhandled(Exception):
+ """Raised by a method to indicate that the message triggering that
+ method call remains unhandled."""
+ pass
+
+ def __init__(self, out_types: Sequence[str] = (), in_types: Sequence[str] = (), name: Optional[str] = None):
+ super().__init__(name=name)
+ self._out_type = bustypes.MessageType(out_types)
+ self._in_type = bustypes.MessageType(in_types)
+ self._func = None
+
+ def __get__(self, obj, cls=None):
+ return self._func.__get__(obj, cls)
+
+ def __call__(self, *args, **kwargs):
+ # decorator
+ self._func, = args
+ return self
+
+ def _describe(self) -> Dict[str, Any]:
+ return {'in': [item.typestring for item in self._in_type.item_types],
+ 'out': [item.typestring for item in self._out_type.item_types]}
+
+ def _invoke(self, obj, message):
+ args = self._in_type.read(message)
+ if args is None:
+ return False
+ try:
+ result = self._func.__get__(obj)(*args)
+ except (BusError, OSError) as error:
+ return message.reply_method_error(error)
+
+ return message.reply_method_function_return_value(self._out_type, result)
+
+
+class org_freedesktop_DBus_Peer(Interface):
+ @Interface.Method()
+ @staticmethod
+ def ping() -> None:
+ pass
+
+ @Interface.Method('s')
+ @staticmethod
+ def get_machine_id() -> str:
+ with open('/etc/machine-id', encoding='ascii') as file:
+ return file.read().strip()
+
+
+class org_freedesktop_DBus_Introspectable(Interface):
+ @Interface.Method('s')
+ @classmethod
+ def introspect(cls) -> str:
+ return introspection.to_xml(cls._dbus_interfaces)
+
+
+class org_freedesktop_DBus_Properties(Interface):
+ properties_changed = Interface.Signal('s', 'a{sv}', 'as')
+
+ @Interface.Method('v', 'ss')
+ def get(self, interface, name):
+ return self._find_member(interface, 'properties', name).to_dbus(self)
+
+ @Interface.Method(['a{sv}'], 's')
+ def get_all(self, interface):
+ properties = self._find_category(interface, 'properties')
+ return {name: prop.to_dbus(self) for name, prop in properties.items()}
+
+ @Interface.Method('', 'ssv')
+ def set(self, interface, name, value):
+ self._find_member(interface, 'properties', name).from_dbus(self, value)
+
+
+class Object(org_freedesktop_DBus_Introspectable,
+ org_freedesktop_DBus_Peer,
+ org_freedesktop_DBus_Properties,
+ BaseObject,
+ Interface):
+ """High-level base class for exporting objects on D-Bus
+
+ This is usually where you should start.
+
+ This provides a base for exporting objects on the bus, implements the
+ standard D-Bus interfaces, and allows you to add your own interfaces to the
+ mix. See the documentation for Interface to find out how to define and
+ implement your D-Bus interface.
+ """
+ def message_received(self, message: BusMessage) -> bool:
+ interface = message.get_interface()
+ name = message.get_member()
+
+ try:
+ method = self._find_member(interface, 'methods', name)
+ assert isinstance(method, Interface.Method)
+ return method._invoke(self, message)
+ except Object.Method.Unhandled:
+ return False
diff --git a/src/cockpit/_vendor/systemd_ctypes/bustypes.py b/src/cockpit/_vendor/systemd_ctypes/bustypes.py
new file mode 100644
index 0000000..9fe6b1b
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/bustypes.py
@@ -0,0 +1,551 @@
+# systemd_ctypes
+#
+# Copyright (C) 2023 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+# This file is responsible for creating functions to (de)serialize Python
+# objects into and out of BusMessage objects.
+#
+# Each Type corresponds to a (possibly complex) D-Bus type. It has a .reader
+# and a .writer property. The readers take a message and deserialize a single
+# value from it, returning the value:
+#
+# def reader(message: BusMessage) -> object:
+#
+# The writers take a message and a value, and append the value to the message.
+#
+# def writer(message: BusMessage, value: object) -> None:
+#
+# The necessary information for the specific type of object to be handled is
+# part of the function. No additional information needs to be provided.
+
+import binascii
+import ctypes
+import functools
+import inspect
+import json
+import re
+from enum import Enum
+from typing import Any, Callable, ClassVar, Dict, Iterable, List, Optional, Sequence, Tuple, TypeVar, Union
+
+from . import libsystemd, typing
+from .typing import Annotated, TypeGuard
+
+_object_path_re = re.compile(r'/|(/[A-Za-z0-9_]+)+')
+
+
+def is_object_path(candidate: str) -> TypeGuard['BusType.objectpath']:
+ return _object_path_re.fullmatch(candidate) is not None
+
+
+def is_signature(candidate: str) -> TypeGuard['BusType.signature']:
+ offset = 0
+
+ def maybe_pop(acceptable: str) -> Optional[str]:
+ nonlocal offset
+ char = candidate[offset]
+ if char in acceptable:
+ offset += 1
+ return char
+ else:
+ return None
+
+ def pop(acceptable: str) -> str:
+ char = maybe_pop(acceptable)
+ assert char is not None
+ return char
+
+ def find_next() -> None:
+ first = pop('ybnqiuxtsogdva(') # valid first characters
+ if first == 'a':
+ if maybe_pop('{'): # dict
+ pop('ybnqiuxtsogd') # key
+ find_next() # value
+ pop('}')
+ else: # array
+ find_next() # item
+ elif first == '(': # structure
+ find_next() # at least one item
+ while not maybe_pop(')'):
+ find_next() # and possibly more
+
+ try:
+ while offset < len(candidate):
+ find_next()
+ return True
+ except (AssertionError, IndexError):
+ return False
+
+
+def yield_base_helpers() -> Iterable[Tuple[str, object]]:
+ for method in ['enter_container', 'exit_container', 'open_container', 'close_container',
+ 'append_basic', 'read_basic', 'append_array', 'read_array']:
+ yield method, libsystemd.libsystemd[f'sd_bus_message_{method}']
+
+ for name in ['size_t', 'char_p']:
+ instance = getattr(ctypes, f'c_{name}')()
+ yield f'{name}', instance
+ yield f'{name}_ref', ctypes.byref(instance)
+ yield f'{name}_setter', instance.__class__.value.__set__
+
+ for char in 'aervy':
+ yield char, ctypes.c_char(ord(char))
+
+ # https://docs.python.org/3/c-api/unicode.html#c.PyUnicode_FromString
+ to_bytes = ctypes.pythonapi.PyBytes_FromStringAndSize
+ to_bytes.restype = ctypes.py_object
+ yield 'to_bytes', to_bytes
+
+
+_base_helpers = dict(yield_base_helpers())
+
+T = TypeVar('T')
+
+
+def call_with_kwargs(func: Callable[..., T], kwargs: Dict[str, Any]) -> T:
+ parameters = set(inspect.signature(func).parameters)
+ return func(**{key: value for key, value in kwargs.items() if key in parameters})
+
+
+class Type:
+ _cache: ClassVar[Dict[Tuple[type, Tuple[object, ...]], 'Type']] = {}
+
+ __slots__ = 'typestring', 'bytes_typestring', 'writer', 'reader'
+ typestring: str
+ bytes_typestring: bytes
+ reader: Callable[[libsystemd.sd_bus_message], object]
+ writer: Callable[[libsystemd.sd_bus_message, object], None]
+
+ def __new__(cls, *args: Any) -> 'Type':
+ instance = Type._cache.get((cls, args))
+ if instance is None:
+ instance = object.__new__(cls)
+ Type._cache[(cls, args)] = instance
+ return instance
+
+ def __init__(self, typestring: str, **kwargs: Any):
+ self.typestring = typestring
+ self.bytes_typestring = typestring.encode('ascii')
+
+ kwargs = dict(_base_helpers, **kwargs)
+ self.writer = call_with_kwargs(self.get_writer, kwargs)
+ self.reader = call_with_kwargs(self.get_reader, kwargs)
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}('{self.typestring}')"
+
+ def get_writer(self, **kwargs: object) -> Callable[[object, object], None]:
+ raise NotImplementedError
+
+ def get_reader(self, **kwargs: object) -> Callable[[object], object]:
+ raise NotImplementedError
+
+
+class BasicType(Type):
+ __slots__ = ()
+
+ def __init__(self, typestring: str, ctype, get_wrapper=None, **kwargs):
+ variable = ctype() # NB: not thread-safe
+ super().__init__(typestring, ctype=ctype, type_constant=ctypes.c_char(ord(typestring)),
+ getter=get_wrapper or ctype.value.__get__, setter=ctype.value.__set__,
+ variable=variable, reference=ctypes.byref(variable), **kwargs)
+
+ def get_reader(self, read_basic, type_constant, variable, reference, getter):
+ def basic_reader(message: libsystemd.sd_bus_message) -> object:
+ if read_basic(message, type_constant, reference) <= 0:
+ raise StopIteration
+ return getter(variable)
+ return basic_reader
+
+
+class FixedType(BasicType):
+ __slots__ = ()
+
+ def get_writer(self, append_basic, type_constant, variable, reference, setter, getter):
+ def fixed_writer(message: libsystemd.sd_bus_message, value: object) -> None:
+ setter(variable, value)
+ if getter(variable) != value:
+ raise TypeError(f"Cannot represent value {value} with type '{self.typestring}'")
+ append_basic(message, type_constant, reference)
+ return fixed_writer
+
+
+class StringLikeType(BasicType):
+ __slots__ = ()
+
+ @staticmethod
+ def get_guarded_conversion(typestring: str, guard: Callable[[str], bool]) -> Callable[[object], bytes]:
+ def convert(candidate: object) -> bytes:
+ if not isinstance(candidate, str):
+ raise TypeError(f"'{typestring}' encodes 'str', not '{candidate.__class__.__name__}'")
+ if not guard(candidate):
+ raise ValueError(f"Invalid value provided for type '{typestring}'")
+ return str.encode(candidate)
+ return convert
+
+ def __init__(self, typestring: str, guard: Optional[Callable[[str], bool]] = None):
+ # https://docs.python.org/3/c-api/unicode.html#c.PyUnicode_FromString
+ to_unicode = ctypes.pythonapi.PyUnicode_FromString
+ to_unicode.restype = ctypes.py_object
+
+ if guard is not None:
+ convert = StringLikeType.get_guarded_conversion(typestring, guard)
+ else:
+ convert = str.encode # type: ignore[assignment] # can throw TypeError on call
+
+ super().__init__(typestring, ctypes.c_char_p, to_unicode, convert=convert)
+
+ def get_writer(self, append_basic, type_constant, convert):
+ def string_writer(message: libsystemd.sd_bus_message, value: object) -> None:
+ append_basic(message, type_constant, convert(value))
+ return string_writer
+
+
+class BytestringType(Type):
+ __slots__ = ()
+
+ def get_writer(self, append_array, y, size_t_setter, size_t):
+ def bytes_writer(message: libsystemd.sd_bus_message, value: object) -> None:
+ if not isinstance(value, bytes):
+ if isinstance(value, str):
+ try:
+ value = binascii.a2b_base64(value.encode('ascii')) # or decode base64
+ except binascii.Error as exc:
+ raise ValueError("'ay' cannot encode invalid base64 string") from exc
+ elif isinstance(value, (memoryview, bytearray)):
+ value = bytes(value)
+ else:
+ raise TypeError("'ay' can only encode bytes-like or base64 string objects, "
+ f"not '{value.__class__.__name__}'.")
+ size_t_setter(size_t, len(value))
+ append_array(message, y, value, size_t)
+ return bytes_writer
+
+ def get_reader(self, read_array, y, to_bytes, char_p, char_p_ref, size_t, size_t_ref):
+ def bytes_reader(message):
+ if read_array(message, y, char_p_ref, size_t_ref) <= 0:
+ raise StopIteration
+ return to_bytes(char_p, size_t)
+ return bytes_reader
+
+
+class ContainerType(Type):
+ _typestring_template: ClassVar[str]
+ __slots__ = 'item_types'
+ item_types: Sequence[Type]
+
+ def __init__(self, *item_types: Type, **kwargs: Any):
+ assert len(item_types) > 0
+ item_typestrings = ''.join(item.typestring for item in item_types)
+ self.item_types = item_types
+ super().__init__(self._typestring_template.replace('_', item_typestrings),
+ type_contents=ctypes.c_char_p(item_typestrings.encode('ascii')),
+ **kwargs)
+
+
+class ArrayType(ContainerType):
+ _typestring_template = 'a_'
+ __slots__ = ()
+
+ def __init__(self, item_type: Type):
+ super().__init__(item_type,
+ item_writer=item_type.writer,
+ item_reader=item_type.reader,
+ list_append=list.append)
+
+ def get_reader(self, enter_container, exit_container, list_append, item_reader):
+ def array_reader(message: libsystemd.sd_bus_message) -> object:
+ if enter_container(message, 0, None) <= 0:
+ raise StopIteration
+ result: List[object] = []
+ try:
+ while True:
+ list_append(result, item_reader(message))
+ except StopIteration:
+ return result
+ finally:
+ exit_container(message)
+ return array_reader
+
+ def get_writer(self, a, type_contents, open_container, close_container, item_writer):
+ def array_writer(message: libsystemd.sd_bus_message, value: object) -> None:
+ open_container(message, a, type_contents)
+ for item in value: # type: ignore[attr-defined] # can throw TypeError
+ item_writer(message, item)
+ close_container(message)
+ return array_writer
+
+
+class StructType(ContainerType):
+ _typestring_template = '(_)'
+ __slots__ = ()
+
+ def get_reader(self, enter_container, exit_container):
+ item_readers = tuple(item_type.reader for item_type in self.item_types)
+
+ def array_reader(message: libsystemd.sd_bus_message) -> object:
+ if enter_container(message, 0, None) <= 0:
+ raise StopIteration
+ result = tuple(item_reader(message) for item_reader in item_readers)
+ exit_container(message)
+ return result
+ return array_reader
+
+ def get_writer(self, r, type_contents, open_container, close_container):
+ item_writers = tuple(item_type.writer for item_type in self.item_types)
+
+ def struct_writer(message: libsystemd.sd_bus_message, value: object) -> None:
+ n_items = len(value) # type: ignore[arg-type] # can throw TypeError
+ if n_items != len(item_writers):
+ raise TypeError(f"Wrong numbers of items ({n_items}) for structure type '{self.typestring}'")
+ open_container(message, r, type_contents)
+ for item_writer, item in zip(item_writers, value): # type: ignore[call-overload] # can throw TypeError
+ item_writer(message, item)
+ close_container(message)
+ return struct_writer
+
+
+class DictionaryType(ContainerType):
+ _typestring_template = 'a{_}'
+ __slots__ = ()
+
+ def __init__(self, key_type: Type, value_type: Type):
+ assert isinstance(key_type, BasicType)
+ item_type = '{' + key_type.typestring + value_type.typestring + '}'
+ super().__init__(key_type, value_type,
+ key_reader=key_type.reader, key_writer=key_type.writer,
+ value_reader=value_type.reader, value_writer=value_type.writer,
+ item_type=ctypes.c_char_p(item_type.encode('ascii')))
+
+ def get_reader(self, enter_container, exit_container, key_reader, value_reader):
+ def dict_reader(message: libsystemd.sd_bus_message) -> object:
+ if enter_container(message, 0, None) <= 0: # array
+ raise StopIteration
+ result = {}
+ while enter_container(message, 0, None) > 0: # entry
+ key = key_reader(message)
+ value = value_reader(message)
+ result[key] = value
+ exit_container(message)
+ exit_container(message)
+ return result
+ return dict_reader
+
+ def get_writer(self, a, item_type, e, type_contents, open_container, close_container, key_writer, value_writer):
+ def dict_writer(message: libsystemd.sd_bus_message, value: object) -> None:
+ open_container(message, a, item_type) # array
+ for key, val in value.items(): # type: ignore[attr-defined] # can raise AttributeError
+ open_container(message, e, type_contents) # entry
+ key_writer(message, key) # key
+ value_writer(message, val) # value
+ close_container(message) # end entry
+ close_container(message) # end array
+ return dict_writer
+
+
+class VariantType(Type):
+ __slots__ = ()
+
+ def get_reader(self, enter_container, exit_container):
+ def variant_reader(message: libsystemd.sd_bus_message) -> object:
+ if enter_container(message, 0, None) <= 0:
+ raise StopIteration
+ typestring = message.get_signature(False)
+ type_, = from_signature(typestring)
+ value = type_.reader(message)
+ exit_container(message)
+ return Variant(value, type_)
+ return variant_reader
+
+ def get_writer(self, open_container, close_container, v):
+ def variant_writer(message: libsystemd.sd_bus_message, value: object) -> None:
+ if isinstance(value, Variant):
+ type_ = value.type
+ contents = value.value
+ else:
+ try:
+ type_, = from_signature(value['t']) # type: ignore[index] # can throw TypeError
+ contents = value['v'] # type: ignore[index] # can throw TypeError
+ except KeyError as exc:
+ raise TypeError("'v' can encode Variant objects, or mappings with 't' and 'v' keys") from exc
+
+ open_container(message, v, type_.bytes_typestring)
+ type_.writer(message, contents)
+ close_container(message)
+ return variant_writer
+
+
+class Variant:
+ __slots__ = 'type', 'value'
+ type: Type
+ value: object
+
+ def __init__(self, value: object, hint: object = None):
+ if isinstance(hint, Type):
+ self.type = hint
+ elif isinstance(hint, str):
+ self.type, = from_signature(hint)
+ else:
+ self.type = from_annotation(hint or value.__class__)
+ self.value = value
+
+ def __repr__(self) -> str:
+ return f"systemd_ctypes.Variant({self.value}, '{self.type.typestring}')"
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, Variant):
+ return self.type == other.type and self.value == other.value
+ elif isinstance(other, dict):
+ return (self.type,) == from_signature(other['t']) and self.value == other['v']
+ else:
+ return False
+
+ def __hash__(self) -> int:
+ return hash(self.type) ^ hash(self.value)
+
+
+class BusType(Enum):
+ boolean = Annotated[bool, FixedType('b', ctypes.c_int, ctypes.c_int.__bool__)]
+ byte = Annotated[int, FixedType('y', ctypes.c_uint8)]
+ int16 = Annotated[int, FixedType('n', ctypes.c_int16)]
+ uint16 = Annotated[int, FixedType('q', ctypes.c_uint16)]
+ int32 = Annotated[int, FixedType('i', ctypes.c_int32)]
+ uint32 = Annotated[int, FixedType('u', ctypes.c_uint32)]
+ int64 = Annotated[int, FixedType('x', ctypes.c_int64)]
+ uint64 = Annotated[int, FixedType('t', ctypes.c_uint64)]
+ double = Annotated[float, FixedType('d', ctypes.c_double)]
+ string = Annotated[str, StringLikeType('s')]
+ objectpath = Annotated[str, StringLikeType('o', is_object_path)]
+ signature = Annotated[str, StringLikeType('g', is_signature)]
+ bytestring = Annotated[bytes, BytestringType('ay')]
+ variant = Annotated[dict, VariantType('v')]
+
+
+# mypy gets confused by enums, so just use Any
+_base_equivalence_map: Dict[type, Any] = {
+ bool: BusType.boolean,
+ bytes: BusType.bytestring,
+ int: BusType.int32,
+ str: BusType.string,
+ Variant: BusType.variant,
+}
+
+_factory_map: Dict[object, Callable[..., Type]] = {
+ dict: DictionaryType, Dict: DictionaryType,
+ list: ArrayType, List: ArrayType,
+ tuple: StructType, Tuple: StructType,
+}
+
+
+@functools.lru_cache()
+def from_annotation(annotation: Union[str, type, BusType]) -> Type:
+ # Simple Python types
+ if isinstance(annotation, str):
+ types = from_signature(annotation)
+ if len(types) != 1:
+ raise TypeError(f"Signature '{annotation}' invalid as a type string "
+ f"because it describes {len(types)} types, not one.")
+ return types[0]
+
+ if isinstance(annotation, type):
+ annotation = _base_equivalence_map.get(annotation, annotation)
+
+ # Our own BusType types
+ if isinstance(annotation, BusType):
+ bus_type = typing.get_args(annotation.value)[1]
+ assert isinstance(bus_type, Type)
+ return bus_type
+
+ # Container types
+ try:
+ factory = _factory_map[typing.get_origin(annotation)]
+ args = [from_annotation(arg) for arg in typing.get_args(annotation)]
+ return factory(*args)
+ except (AssertionError, AttributeError, KeyError, TypeError):
+ raise TypeError(f"Cannot interpret {annotation} as a dbus type") from None
+
+
+_base_typestring_map: Dict[str, Type] = {
+ bustype.typestring: bustype for bustype in (from_annotation(entry) for entry in BusType)
+}
+
+
+@functools.lru_cache()
+def from_signature(signature: str) -> Tuple[Type, ...]:
+ stack = list(reversed(signature))
+
+ def get_one() -> Type:
+ first = stack.pop()
+ if first == 'a':
+ if stack[-1] == 'y':
+ first += stack.pop()
+ elif stack[-1] == '{':
+ stack.pop()
+ return DictionaryType(*get_several('}'))
+ else:
+ return ArrayType(get_one())
+ elif first == '(':
+ return StructType(*get_several(')'))
+
+ return _base_typestring_map[first]
+
+ def get_several(end: str) -> Iterable[Type]:
+ yield get_one()
+ while stack[-1] != end:
+ yield get_one()
+ stack.pop()
+
+ def get_all() -> Iterable[Type]:
+ while stack:
+ yield get_one()
+
+ try:
+ return tuple(get_all())
+ except (AssertionError, IndexError, KeyError) as exc:
+ raise TypeError(f"Invalid type signature '{signature}'") from exc
+
+
+class MessageType:
+ item_types: Sequence[Type]
+ typestrings: List[str]
+ signature: str
+
+ def __init__(self, item_types: Sequence[Union[str, type, BusType]]):
+ self.item_types = [from_annotation(item_type) for item_type in item_types]
+ self.typestrings = [item_type.typestring for item_type in self.item_types]
+ self.signature = ''.join(self.typestrings)
+
+ def write(self, message: libsystemd.sd_bus_message, *items: object) -> None:
+ assert len(items) == len(self.item_types)
+ for item_type, item in zip(self.item_types, items):
+ item_type.writer(message, item)
+
+ def read(self, message: libsystemd.sd_bus_message) -> Optional[Tuple[object, ...]]:
+ if not message.has_signature(self.signature):
+ return None
+ return tuple(item_type.reader(message) for item_type in self.item_types)
+
+ def __len__(self) -> int:
+ return len(self.item_types)
+
+
+class JSONEncoder(json.JSONEncoder):
+ def default(self, obj: object) -> object:
+ if isinstance(obj, Variant):
+ return {"t": obj.type.typestring, "v": obj.value}
+ elif isinstance(obj, bytes):
+ return binascii.b2a_base64(obj, newline=False).decode('ascii')
+ return super().default(obj)
diff --git a/src/cockpit/_vendor/systemd_ctypes/event.py b/src/cockpit/_vendor/systemd_ctypes/event.py
new file mode 100644
index 0000000..885c625
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/event.py
@@ -0,0 +1,138 @@
+# systemd_ctypes
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import asyncio
+import selectors
+import sys
+from typing import Callable, ClassVar, Coroutine, List, Optional, Tuple
+
+from . import inotify, libsystemd
+from .librarywrapper import Reference, UserData, byref
+
+
+class Event(libsystemd.sd_event):
+ class Source(libsystemd.sd_event_source):
+ def cancel(self) -> None:
+ self._unref()
+ self.value = None
+
+ _default_instance: ClassVar[Optional['Event']] = None
+
+ @staticmethod
+ def default() -> 'Event':
+ if Event._default_instance is None:
+ Event._default_instance = Event()
+ Event._default(byref(Event._default_instance))
+ return Event._default_instance
+
+ InotifyHandler = Callable[[inotify.Event, int, Optional[bytes]], None]
+
+ class InotifySource(Source):
+ def __init__(self, handler: 'Event.InotifyHandler') -> None:
+ def callback(source: libsystemd.sd_event_source,
+ _event: Reference[inotify.inotify_event],
+ userdata: UserData) -> int:
+ event = _event.contents
+ handler(inotify.Event(event.mask), event.cookie, event.name)
+ return 0
+ self.trampoline = libsystemd.sd_event_inotify_handler_t(callback)
+
+ def add_inotify(self, path: str, mask: inotify.Event, handler: InotifyHandler) -> InotifySource:
+ source = Event.InotifySource(handler)
+ self._add_inotify(byref(source), path, mask, source.trampoline, source.userdata)
+ return source
+
+ def add_inotify_fd(self, fd: int, mask: inotify.Event, handler: InotifyHandler) -> InotifySource:
+ # HACK: sd_event_add_inotify_fd() got added in 250, which is too new. Fake it.
+ return self.add_inotify(f'/proc/self/fd/{fd}', mask, handler)
+
+
+# This is all a bit more awkward than it should have to be: systemd's event
+# loop chaining model is designed for glib's prepare/check/dispatch paradigm;
+# failing to call prepare() can lead to deadlocks, for example.
+#
+# Hack a selector subclass which calls prepare() before sleeping and this for us.
+class Selector(selectors.DefaultSelector):
+ def __init__(self, event: Optional[Event] = None) -> None:
+ super().__init__()
+ self.sd_event = event or Event.default()
+ self.key = self.register(self.sd_event.get_fd(), selectors.EVENT_READ)
+
+ def select(
+ self, timeout: Optional[float] = None
+ ) -> List[Tuple[selectors.SelectorKey, int]]:
+ # It's common to drop the last reference to a Source or Slot object on
+ # a dispatch of that same source/slot from the main loop. If we happen
+ # to garbage collect before returning, the trampoline could be
+ # destroyed before we're done using it. Provide a mechanism to defer
+ # the destruction of trampolines for as long as we might be
+ # dispatching. This gets cleared again at the bottom, before return.
+ libsystemd.Trampoline.deferred = []
+
+ while self.sd_event.prepare():
+ self.sd_event.dispatch()
+ ready = super().select(timeout)
+ # workaround https://github.com/systemd/systemd/issues/23826
+ # keep calling wait() until there's nothing left
+ while self.sd_event.wait(0):
+ self.sd_event.dispatch()
+ while self.sd_event.prepare():
+ self.sd_event.dispatch()
+
+ # We can be sure we're not dispatching callbacks anymore
+ libsystemd.Trampoline.deferred = None
+
+ # This could return zero events with infinite timeout, but nobody seems to mind.
+ return [(key, events) for (key, events) in ready if key != self.key]
+
+
+class EventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+ def new_event_loop(self) -> asyncio.AbstractEventLoop:
+ return asyncio.SelectorEventLoop(Selector())
+
+
+def run_async(main: Coroutine[None, None, None], debug: Optional[bool] = None) -> None:
+ asyncio.set_event_loop_policy(EventLoopPolicy())
+
+ polyfill = sys.version_info < (3, 7, 0) and not hasattr(asyncio, 'run')
+ if polyfill:
+ # Polyfills for Python 3.6:
+ loop = asyncio.get_event_loop()
+
+ assert not hasattr(asyncio, 'get_running_loop')
+ asyncio.get_running_loop = lambda: loop
+
+ assert not hasattr(asyncio, 'create_task')
+ asyncio.create_task = loop.create_task
+
+ assert not hasattr(asyncio, 'run')
+
+ def run(
+ main: Coroutine[None, None, None], debug: Optional[bool] = None
+ ) -> None:
+ if debug is not None:
+ loop.set_debug(debug)
+ loop.run_until_complete(main)
+
+ asyncio.run = run # type: ignore[assignment]
+
+ asyncio._systemd_ctypes_polyfills = True # type: ignore[attr-defined]
+
+ asyncio.run(main, debug=debug)
+
+ if polyfill:
+ del asyncio.create_task, asyncio.get_running_loop, asyncio.run
diff --git a/src/cockpit/_vendor/systemd_ctypes/inotify.py b/src/cockpit/_vendor/systemd_ctypes/inotify.py
new file mode 100644
index 0000000..6f02595
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/inotify.py
@@ -0,0 +1,74 @@
+# systemd_ctypes
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import ctypes
+from enum import IntFlag, auto
+from typing import Optional
+
+
+class inotify_event(ctypes.Structure):
+ _fields_ = (
+ ('wd', ctypes.c_int32),
+ ('mask', ctypes.c_uint32),
+ ('cookie', ctypes.c_uint32),
+ ('len', ctypes.c_uint32),
+ )
+
+ @property
+ def name(self) -> Optional[bytes]:
+ if self.len == 0:
+ return None
+
+ class event_with_name(ctypes.Structure):
+ _fields_ = (*inotify_event._fields_, ('name', ctypes.c_char * self.len))
+
+ name = ctypes.cast(ctypes.addressof(self), ctypes.POINTER(event_with_name)).contents.name
+ assert isinstance(name, bytes)
+ return name
+
+
+class Event(IntFlag):
+ ACCESS = auto()
+ MODIFY = auto()
+ ATTRIB = auto()
+ CLOSE_WRITE = auto()
+ CLOSE_NOWRITE = auto()
+ OPEN = auto()
+ MOVED_FROM = auto()
+ MOVED_TO = auto()
+ CREATE = auto()
+ DELETE = auto()
+ DELETE_SELF = auto()
+ MOVE_SELF = auto()
+
+ UNMOUNT = 1 << 13
+ Q_OVERFLOW = auto()
+ IGNORED = auto()
+
+ ONLYDIR = 1 << 24
+ DONT_FOLLOW = auto()
+ EXCL_UNLINK = auto()
+
+ MASK_CREATE = 1 << 28
+ MASK_ADD = auto()
+ ISDIR = auto()
+ ONESHOT = auto()
+
+ CLOSE = CLOSE_WRITE | CLOSE_NOWRITE
+ MOVE = MOVED_FROM | MOVED_TO
+ CHANGED = (MODIFY | ATTRIB | CLOSE_WRITE | MOVE |
+ CREATE | DELETE | DELETE_SELF | MOVE_SELF)
diff --git a/src/cockpit/_vendor/systemd_ctypes/introspection.py b/src/cockpit/_vendor/systemd_ctypes/introspection.py
new file mode 100644
index 0000000..f7bfd25
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/introspection.py
@@ -0,0 +1,92 @@
+# systemd_ctypes
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import xml.etree.ElementTree as ET
+
+
+def parse_method(method):
+ return {
+ "in": [tag.attrib['type'] for tag in method.findall("arg") if tag.get('direction', 'in') == 'in'],
+ "out": [tag.attrib['type'] for tag in method.findall("arg[@direction='out']")]
+ }
+
+
+def parse_property(prop):
+ return {
+ "flags": 'w' if prop.attrib.get('access') == 'write' else 'r',
+ "type": prop.attrib['type']
+ }
+
+
+def parse_signal(signal):
+ return {"in": [tag.attrib['type'] for tag in signal.findall("arg")]}
+
+
+def parse_interface(interface):
+ return {
+ "methods": {tag.attrib['name']: parse_method(tag) for tag in interface.findall('method')},
+ "properties": {tag.attrib['name']: parse_property(tag) for tag in interface.findall('property')},
+ "signals": {tag.attrib['name']: parse_signal(tag) for tag in interface.findall('signal')}
+ }
+
+
+def parse_xml(xml):
+ et = ET.fromstring(xml)
+ return {tag.attrib['name']: parse_interface(tag) for tag in et.findall('interface')}
+
+
+# Pretend like this is a little bit functional
+def element(tag, children=(), **kwargs):
+ tag = ET.Element(tag, kwargs)
+ tag.extend(children)
+ return tag
+
+
+def method_to_xml(name, method_info):
+ return element('method', name=name,
+ children=[
+ element('arg', type=arg_type, direction=direction)
+ for direction in ['in', 'out']
+ for arg_type in method_info[direction]
+ ])
+
+
+def property_to_xml(name, property_info):
+ return element('property', name=name,
+ access='write' if property_info['flags'] == 'w' else 'read',
+ type=property_info['type'])
+
+
+def signal_to_xml(name, signal_info):
+ return element('signal', name=name,
+ children=[
+ element('arg', type=arg_type) for arg_type in signal_info['in']
+ ])
+
+
+def interface_to_xml(name, interface_info):
+ return element('interface', name=name,
+ children=[
+ *(method_to_xml(name, info) for name, info in interface_info['methods'].items()),
+ *(property_to_xml(name, info) for name, info in interface_info['properties'].items()),
+ *(signal_to_xml(name, info) for name, info in interface_info['signals'].items()),
+ ])
+
+
+def to_xml(interfaces):
+ node = element('node', children=(interface_to_xml(name, members) for name, members in interfaces.items()))
+ return ET.tostring(node, encoding='unicode')
diff --git a/src/cockpit/_vendor/systemd_ctypes/librarywrapper.py b/src/cockpit/_vendor/systemd_ctypes/librarywrapper.py
new file mode 100644
index 0000000..12605e2
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/librarywrapper.py
@@ -0,0 +1,214 @@
+# systemd_ctypes
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import ctypes
+import inspect
+import logging
+import os
+import sys
+import types
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Generic,
+ NewType,
+ NoReturn,
+ Optional,
+ Tuple,
+ Type,
+ TypeVar,
+ Union,
+)
+
+from . import typing
+
+# First in 3.10, and conditional import gives type errors
+NoneType = type(None)
+
+logger = logging.getLogger(__name__)
+
+if typing.TYPE_CHECKING:
+ CType = TypeVar("CType", bound=ctypes._CData)
+ Callback = ctypes._FuncPointer
+else:
+ CType = TypeVar("CType")
+ Callback = ctypes.c_void_p
+
+
+if typing.TYPE_CHECKING:
+ class Reference(Generic[CType], ctypes._Pointer[CType]):
+ pass
+
+ def byref(x: CType) -> Reference[CType]:
+ raise NotImplementedError
+else:
+ class Reference(Generic[CType]):
+ pass
+
+ byref = ctypes.byref
+
+
+UserData = Optional[ctypes.c_void_p]
+
+
+class negative_errno(ctypes.c_int):
+ def errcheck(self, func: Callable[..., object], _args: Tuple[object, ...]) -> int:
+ result = self.value
+ if result < 0:
+ raise OSError(-result, f"{func.__name__}: {os.strerror(-result)}")
+ return result
+
+
+class utf8(ctypes.c_char_p):
+ def errcheck(self, func: Callable[..., object], _args: Tuple[object, ...]) -> str:
+ assert self.value is not None
+ return self.value.decode()
+
+ @classmethod
+ def from_param(cls, value: str) -> 'utf8':
+ return cls(value.encode())
+
+
+class utf8_or_null(ctypes.c_char_p):
+ def errcheck(self,
+ func: Callable[..., object],
+ _args: Tuple[object, ...]) -> Optional[str]:
+ return self.value.decode() if self.value is not None else None
+
+ @classmethod
+ def from_param(cls, value: Optional[str]) -> 'utf8_or_null':
+ return cls(value.encode() if value is not None else None)
+
+
+class boolint(ctypes.c_int):
+ def errcheck(self, func: Callable[..., object], _args: Tuple[object, ...]) -> bool:
+ return bool(self.value)
+
+
+WeakReference = NewType("WeakReference", int)
+Errno = typing.Annotated[NoReturn, "errno"]
+
+
+type_map = {
+ Union[None, Errno]: negative_errno, # technically returns int
+ Union[bool, Errno]: negative_errno, # technically returns int
+ Union[int, Errno]: negative_errno,
+ bool: boolint,
+ Optional[str]: utf8_or_null,
+ str: utf8,
+ int: ctypes.c_int,
+ WeakReference: ctypes.c_void_p
+}
+
+
+def map_type(annotation: Any, global_vars: Dict[str, object]) -> Any:
+ try:
+ return type_map[annotation]
+ except KeyError:
+ pass # ... and try more cases below
+
+ if isinstance(annotation, typing.ForwardRef):
+ annotation = annotation.__forward_arg__
+
+ if isinstance(annotation, str):
+ annotation = global_vars[annotation]
+
+ origin = typing.get_origin(annotation)
+ args = typing.get_args(annotation)
+
+ if origin is Reference:
+ return ctypes.POINTER(map_type(args[0], global_vars))
+
+ elif origin is Union and NoneType in args:
+ # the C pointer types are already nullable
+ other_arg, = set(args) - {NoneType}
+ return map_type(other_arg, global_vars)
+
+ elif origin is typing.Annotated:
+ return args[1]
+
+ else:
+ assert origin is None, origin
+ return annotation
+
+
+class ReferenceType(ctypes.c_void_p):
+ @classmethod
+ def _install_cfuncs(cls, cdll: ctypes.CDLL) -> None:
+ logger.debug('Installing stubs for %s:', cls)
+ stubs = tuple(cls.__dict__.items())
+ for name, stub in stubs:
+ if name.startswith("__"):
+ continue
+ cls._wrap(cdll, stub)
+
+ cls._wrap(cdll, cls._ref)
+ cls._wrap(cdll, cls._unref)
+
+ @classmethod
+ def _wrap(cls, cdll: ctypes.CDLL, stub: object) -> None:
+ stub_type = type(stub)
+ if isinstance(stub, staticmethod):
+ # In older Python versions, staticmethod() isn't considered
+ # callable, doesn't have a name, and can't be introspected with
+ # inspect.signature(). Unwrap it.
+ stub = stub.__func__
+ assert isinstance(stub, types.FunctionType)
+ name = stub.__name__
+ signature = inspect.signature(stub)
+ stub_globals = sys.modules.get(cls.__module__).__dict__
+
+ func = cdll[f'{cls.__name__}_{name.lstrip("_")}']
+ func.argtypes = tuple(
+ map_type(parameter.annotation, stub_globals)
+ for parameter in signature.parameters.values()
+ )
+ func.restype = map_type(signature.return_annotation, stub_globals)
+ errcheck = getattr(func.restype, 'errcheck', None)
+ if errcheck is not None:
+ func.errcheck = errcheck
+
+ logger.debug(' create wrapper %s.%s%s', cls.__name__, name, signature)
+ logger.debug(' args %s res %s', func.argtypes, func.restype)
+
+ # ctypes function pointer objects don't implement the usual function
+ # descriptor logic, which means they won't bind as methods. For static
+ # methods, that's good, but for instance methods, we add a wrapper as
+ # the easiest and most performant way to get the binding behaviour.
+ if stub_type is not staticmethod:
+ setattr(cls, name, lambda *args: func(*args))
+ else:
+ setattr(cls, name, func)
+
+ def _unref(self: 'ReferenceType') -> None:
+ ...
+
+ def _ref(self: 'ReferenceType') -> None:
+ ...
+
+ T = TypeVar("T", bound='ReferenceType')
+
+ @classmethod
+ def ref(cls: Type[T], origin: WeakReference) -> T:
+ self = cls(origin)
+ self._ref()
+ return self
+
+ def __del__(self) -> None:
+ if self.value is not None:
+ self._unref()
diff --git a/src/cockpit/_vendor/systemd_ctypes/libsystemd.py b/src/cockpit/_vendor/systemd_ctypes/libsystemd.py
new file mode 100644
index 0000000..7d2d636
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/libsystemd.py
@@ -0,0 +1,334 @@
+# systemd_ctypes
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import ctypes
+import os
+import sys
+from typing import ClassVar, List, Optional, Tuple, Union
+
+from .inotify import inotify_event
+from .librarywrapper import (
+ Callback,
+ Errno,
+ Reference,
+ ReferenceType,
+ UserData,
+ WeakReference,
+ byref,
+)
+from .typing import Annotated
+
+
+class Trampoline(ReferenceType):
+ deferred: 'ClassVar[list[Callback] | None]' = None
+ trampoline: Callback
+ userdata: UserData = None
+
+ def cancel(self) -> None:
+ self._unref()
+ self.value = None
+
+ def __del__(self) -> None:
+ # This might be the currently-dispatching callback — make sure we don't
+ # destroy the trampoline before we return. We drop the deferred list
+ # from the event loop when we're sure we're not doing any dispatches.
+ if Trampoline.deferred is not None:
+ Trampoline.deferred.append(self.trampoline)
+ if self.value is not None:
+ self._unref()
+
+
+class sd_bus_error(ctypes.Structure):
+ # This is ABI, so we are safe to assume it doesn't change.
+ # Unfortunately, we lack anything like sd_bus_error_new().
+ _fields_ = (
+ ("name", ctypes.c_char_p),
+ ("message", ctypes.c_char_p),
+ ("_need_free", ctypes.c_int),
+ )
+
+ def get(self) -> Tuple[str, str]:
+ return self.name.decode(), self.message.decode()
+
+ def set(self, name: str, message: str) -> None:
+ result = libsystemd.sd_bus_error_set(byref(self), name, message)
+ if result < 0:
+ raise OSError(-result, f"sd_bus_error_set: {os.strerror(-result)}")
+
+ def __del__(self) -> None:
+ if self._b_needsfree_:
+ libsystemd.sd_bus_error_free(byref(self))
+
+
+class sd_id128(ctypes.Structure):
+ # HACK: Pass-by-value of array-containing-structs is broken on Python
+ # 3.6. See https://bugs.python.org/issue22273
+ _fields_: List[Tuple[str, type]] = (
+ [("bytes", ctypes.c_uint8 * 16)]
+ if sys.version_info >= (3, 7, 0)
+ else [("one", ctypes.c_uint64), ("two", ctypes.c_uint64)]
+ )
+
+
+class sd_event_source(Trampoline):
+ ...
+
+
+class sd_event(ReferenceType):
+ def _add_inotify(
+ self: 'sd_event',
+ source: Reference[sd_event_source],
+ path: str,
+ event: int,
+ callback: Callback,
+ user_data: UserData,
+ ) -> Union[None, Errno]:
+ ...
+
+ def dispatch(self: 'sd_event') -> Union[None, Errno]:
+ ...
+
+ def get_fd(self: 'sd_event') -> Union[int, Errno]:
+ raise NotImplementedError
+
+ def get_state(self: 'sd_event') -> Union[int, Errno]:
+ raise NotImplementedError
+
+ def loop(self: 'sd_event') -> Union[None, Errno]:
+ ...
+
+ def prepare(self: 'sd_event') -> Union[None, Errno]:
+ ...
+
+ def wait(
+ self: 'sd_event', timeout: Annotated[int, ctypes.c_uint64]
+ ) -> Union[None, Errno]:
+ ...
+
+ @staticmethod
+ def _default(ret: Reference['sd_event']) -> Union[None, Errno]:
+ ...
+
+
+class sd_bus_slot(Trampoline):
+ ...
+
+
+class sd_bus_message(ReferenceType):
+ def rewind(self: 'sd_bus_message', complete: bool) -> Union[None, Errno]:
+ ...
+
+ def _get_error(self: 'sd_bus_message') -> Reference[sd_bus_error]:
+ raise NotImplementedError
+
+ def has_signature(self: 'sd_bus_message', signature: str) -> Union[bool, Errno]:
+ raise NotImplementedError
+
+ def is_method_error(self: 'sd_bus_message', name: str) -> Union[bool, Errno]:
+ raise NotImplementedError
+
+ def _new_method_errnof(
+ self: 'sd_bus_message',
+ message: Reference['sd_bus_message'],
+ error: int,
+ format_str: str,
+ first_arg: str
+ ) -> Union[None, Errno]:
+ ...
+
+ def _new_method_errorf(
+ self: 'sd_bus_message',
+ m: Reference['sd_bus_message'],
+ name: str,
+ format_str: str,
+ first_arg: str
+ ) -> Union[None, Errno]:
+ ...
+
+ def _new_method_return(
+ self: 'sd_bus_message', m: Reference['sd_bus_message']
+ ) -> Union[None, Errno]:
+ ...
+
+ def seal(
+ self: 'sd_bus_message',
+ cookie: Annotated[int, ctypes.c_uint64],
+ timeout: Annotated[int, ctypes.c_uint64],
+ ) -> Union[None, Errno]:
+ ...
+
+ def _get_bus(self: 'sd_bus_message') -> WeakReference:
+ raise NotImplementedError
+
+ def get_destination(self: 'sd_bus_message') -> str:
+ raise NotImplementedError
+
+ def get_interface(self: 'sd_bus_message') -> str:
+ raise NotImplementedError
+
+ def get_member(self: 'sd_bus_message') -> str:
+ raise NotImplementedError
+
+ def get_path(self: 'sd_bus_message') -> str:
+ raise NotImplementedError
+
+ def get_sender(self: 'sd_bus_message') -> Optional[str]:
+ raise NotImplementedError
+
+ def get_signature(self: 'sd_bus_message', complete: bool) -> str:
+ raise NotImplementedError
+
+
+class sd_bus(ReferenceType):
+ def _add_match(
+ self: 'sd_bus',
+ slot: Reference[sd_bus_slot],
+ match: str,
+ handler: Callback,
+ user_data: UserData,
+ ) -> Union[None, Errno]:
+ ...
+
+ def _add_match_async(
+ self: 'sd_bus',
+ slot: Reference[sd_bus_slot],
+ match: str,
+ callback: Callback,
+ install_callback: Callback,
+ user_data: UserData,
+ ) -> Union[None, Errno]:
+ ...
+
+ def _add_object(
+ self: 'sd_bus',
+ slot: Reference[sd_bus_slot],
+ path: str,
+ callback: Callback,
+ user_data: UserData,
+ ) -> Union[None, Errno]:
+ ...
+
+ def attach_event(
+ self: 'sd_bus', event: Optional[sd_event], priority: int
+ ) -> Union[None, Errno]:
+ ...
+
+ def _call(
+ self: 'sd_bus',
+ message: sd_bus_message,
+ timeout: Annotated[int, ctypes.c_uint64],
+ ret_error: Reference[sd_bus_error],
+ reply: Reference[sd_bus_message],
+ ) -> Union[None, Errno]:
+ ...
+
+ def _call_async(
+ self: 'sd_bus',
+ slot: Reference[sd_bus_slot],
+ message: sd_bus_message,
+ callback: Callback,
+ user_data: UserData,
+ timeout_usec: Annotated[int, ctypes.c_uint64],
+ ) -> Union[None, Errno]:
+ ...
+
+ def flush(self: 'sd_bus') -> Union[None, Errno]:
+ ...
+
+ def get_fd(self: 'sd_bus') -> Union[int, Errno]:
+ raise NotImplementedError
+
+ def _message_new_method_call(
+ self: 'sd_bus',
+ message: Reference[sd_bus_message],
+ destination: Optional[str],
+ path: str,
+ interface: str,
+ member: str,
+ ) -> Union[None, Errno]:
+ ...
+
+ def _message_new_signal(
+ self: 'sd_bus',
+ message: Reference[sd_bus_message],
+ path: str,
+ interface: str,
+ member: str,
+ ) -> Union[None, Errno]:
+ ...
+
+ def release_name(self: 'sd_bus', name: str) -> Union[None, Errno]:
+ ...
+
+ def request_name(
+ self: 'sd_bus', name: str, flags: Annotated[int, ctypes.c_uint64]
+ ) -> Union[None, Errno]:
+ ...
+
+ def set_address(self: 'sd_bus', address: str) -> Union[None, Errno]:
+ ...
+
+ def set_bus_client(self: 'sd_bus', b: bool) -> Union[None, Errno]:
+ ...
+
+ def set_fd(self: 'sd_bus', input_fd: int, output_fd: int) -> Union[None, Errno]:
+ ...
+
+ def set_server(self: 'sd_bus', b: bool, bus_d: sd_id128) -> Union[None, Errno]:
+ ...
+
+ def start(self: 'sd_bus') -> Union[None, Errno]:
+ ...
+
+ def wait(
+ self: 'sd_bus', timeout_usec: Annotated[int, ctypes.c_uint64]
+ ) -> Union[None, Errno]:
+ ...
+
+ def send(
+ self: 'sd_bus', message: sd_bus_message, cookie: Optional[Reference[ctypes.c_uint64]]
+ ) -> Union[None, Errno]:
+ ...
+
+ @staticmethod
+ def _default_system(ret: Reference['sd_bus']) -> Union[None, Errno]:
+ ...
+
+ @staticmethod
+ def _default_user(ret: Reference['sd_bus']) -> Union[None, Errno]:
+ ...
+
+ @staticmethod
+ def _new(ret: Reference['sd_bus']) -> Union[None, Errno]:
+ ...
+
+
+sd_bus_message_handler_t = ctypes.CFUNCTYPE(
+ ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(sd_bus_error))
+sd_event_inotify_handler_t = ctypes.CFUNCTYPE(
+ ctypes.c_int, ctypes.c_void_p, ctypes.POINTER(inotify_event), ctypes.c_void_p)
+
+
+libsystemd = ctypes.CDLL("libsystemd.so.0")
+for cls in {
+ sd_bus,
+ sd_bus_message,
+ sd_bus_slot,
+ sd_event,
+ sd_event_source,
+}:
+ cls._install_cfuncs(libsystemd)
diff --git a/src/cockpit/_vendor/systemd_ctypes/pathwatch.py b/src/cockpit/_vendor/systemd_ctypes/pathwatch.py
new file mode 100644
index 0000000..8bdf856
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/pathwatch.py
@@ -0,0 +1,282 @@
+# systemd_ctypes
+#
+# Copyright (C) 2022 Allison Karlitskaya <allison.karlitskaya@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import errno
+import logging
+import os
+import stat
+from typing import Any, List, Optional
+
+from .event import Event
+from .inotify import Event as IN
+
+logger = logging.getLogger(__name__)
+
+
+# inotify hard facts:
+#
+# DELETE_SELF doesn't get called until all references to an inode are gone
+# - including open fds
+# - including on directories
+#
+# ATTRIB gets called when unlinking files (because the link count changes) but
+# not on directories. When unlinking an open directory, no events at all
+# happen on the directory. ATTRIB also collects child events, which means we
+# get a lot of unwanted noise.
+#
+# There's nothing like UNLINK_SELF, unfortunately.
+#
+# Even if it was possible to take this approach, it might not work: after
+# you've opened the fd, it might get deleted before you can establish the watch
+# on it.
+#
+# Additionally, systemd makes it impossible to register those events on
+# symlinks (because it removes IN_DONT_FOLLOW in order to watch via
+# /proc/self/fd).
+#
+# For all of these reasons, unfortunately, the best way seems to be to watch
+# for CREATE|DELETE|MOVE events on each intermediate directory.
+#
+# Unfortunately there is no way to filter to only the name we're interested in,
+# so we're gonna get a lot of unnecessary wakeups.
+#
+# Also: due to the above-mentioned race about watching after opening the fd,
+# let's just always watch for both create and delete events *before* trying to
+# open the fd. We could try to reduce the mask after the fact, but meh...
+#
+# We use a WatchInvalidator utility class to fill the role of "Tell me when an
+# event happened on this (directory) fd which impacted the name file". We
+# build a series of these when setting up a watch in order to find out if any
+# part of the path leading to the monitored file changed.
+
+
+class Handle(int):
+ """An integer subclass that makes it easier to work with file descriptors"""
+
+ def __new__(cls, fd: int = -1) -> 'Handle':
+ return super(Handle, cls).__new__(cls, fd)
+
+ # separate __init__() to set _needs_close mostly to keep pylint quiet
+ def __init__(self, fd: int = -1):
+ super().__init__()
+ self._needs_close = fd != -1
+
+ def __bool__(self) -> bool:
+ return self != -1
+
+ def close(self) -> None:
+ if self._needs_close:
+ self._needs_close = False
+ os.close(self)
+
+ def __eq__(self, value: object) -> bool:
+ if int.__eq__(self, value): # also handles both == -1
+ return True
+
+ if not isinstance(value, int): # other object is not an int
+ return False
+
+ if not self or not value: # when only one == -1
+ return False
+
+ return os.path.sameopenfile(self, value)
+
+ def __del__(self) -> None:
+ if self._needs_close:
+ self.close()
+
+ def __enter__(self) -> 'Handle':
+ return self
+
+ def __exit__(self, _type: type, _value: object, _traceback: object) -> None:
+ self.close()
+
+ @classmethod
+ def open(cls, *args: Any, **kwargs: Any) -> 'Handle':
+ return cls(os.open(*args, **kwargs))
+
+ def steal(self) -> 'Handle':
+ self._needs_close = False
+ return self.__class__(int(self))
+
+
+class WatchInvalidator:
+ _name: bytes
+ _source: Optional[Event.Source]
+ _watch: Optional['PathWatch']
+
+ def event(self, mask: IN, _cookie: int, name: Optional[bytes]) -> None:
+ logger.debug('invalidator event %s %s', mask, name)
+ if self._watch is not None:
+ # If this node itself disappeared, that's definitely an
+ # invalidation. Otherwise, the name needs to match.
+ if IN.IGNORED in mask or self._name == name:
+ logger.debug('Invalidating!')
+ self._watch.invalidate()
+
+ def __init__(self, watch: 'PathWatch', event: Event, dirfd: int, name: str):
+ self._watch = watch
+ self._name = name.encode('utf-8')
+
+ # establishing invalidation watches is best-effort and can fail for a
+ # number of reasons, including search (+x) but not read (+r) permission
+ # on a particular path component, or exceeding limits on watches
+ try:
+ mask = IN.CREATE | IN.DELETE | IN.MOVE | IN.DELETE_SELF | IN.IGNORED
+ self._source = event.add_inotify_fd(dirfd, mask, self.event)
+ except OSError:
+ self._source = None
+
+ def close(self) -> None:
+ # This is a little bit tricky: systemd doesn't have a specific close
+ # API outside of unref, so let's make it as explicit as possible.
+ self._watch = None
+ self._source = None
+
+
+class PathStack(List[str]):
+ def add_path(self, pathname: str) -> None:
+ # TO DO: consider doing something reasonable with trailing slashes
+ # this is a stack, popped from the end: push components in reverse
+ self.extend(item for item in reversed(pathname.split('/')) if item)
+ if pathname.startswith('/'):
+ self.append('/')
+
+ def __init__(self, path: str):
+ super().__init__()
+ self.add_path(path)
+
+
+class Listener:
+ def do_inotify_event(self, mask: IN, cookie: int, name: Optional[bytes]) -> None:
+ raise NotImplementedError
+
+ def do_identity_changed(self, fd: Optional[Handle], errno: Optional[int]) -> None:
+ raise NotImplementedError
+
+
+class PathWatch:
+ _event: Event
+ _listener: Listener
+ _path: str
+ _invalidators: List[WatchInvalidator]
+ _errno: Optional[int]
+ _source: Optional[Event.Source]
+ _tag: Optional[None]
+ _fd: Handle
+
+ def __init__(self, path: str, listener: Listener, event: Optional[Event] = None):
+ self._event = event or Event.default()
+ self._path = path
+ self._listener = listener
+
+ self._invalidators = []
+ self._errno = None
+ self._source = None
+ self._tag = None
+ self._fd = Handle()
+
+ self.invalidate()
+
+ def got_event(self, mask: IN, cookie: int, name: Optional[bytes]) -> None:
+ logger.debug('target event %s: %s %s %s', self._path, mask, cookie, name)
+ self._listener.do_inotify_event(mask, cookie, name)
+
+ def invalidate(self) -> None:
+ for invalidator in self._invalidators:
+ invalidator.close()
+ self._invalidators = []
+
+ try:
+ fd = self.walk()
+ except OSError as error:
+ logger.debug('walk ended in error %d', error.errno)
+
+ if self._source or self._fd or self._errno != error.errno:
+ logger.debug('Ending existing watches.')
+ self._source = None
+ self._fd.close()
+ self._fd = Handle()
+ self._errno = error.errno
+
+ logger.debug('Notifying of new error state %d', self._errno)
+ self._listener.do_identity_changed(None, self._errno)
+
+ return
+
+ with fd:
+ logger.debug('walk successful. Got fd %d', fd)
+ if fd == self._fd:
+ logger.debug('fd seems to refer to same file. Doing nothing.')
+ return
+
+ logger.debug('This file is new for us. Removing old watch.')
+ self._source = None
+ self._fd.close()
+ self._fd = fd.steal()
+
+ try:
+ logger.debug('Establishing a new watch.')
+ self._source = self._event.add_inotify_fd(self._fd, IN.CHANGED, self.got_event)
+ logger.debug('Watching successfully. Notifying of new identity.')
+ self._listener.do_identity_changed(self._fd, None)
+ except OSError as error:
+ logger.debug('Watching failed (%d). Notifying of new identity.', error.errno)
+ self._listener.do_identity_changed(self._fd, error.errno)
+
+ def walk(self) -> Handle:
+ remaining_symlink_lookups = 40
+ remaining_components = PathStack(self._path)
+ dirfd = Handle()
+
+ try:
+ logger.debug('Starting path walk')
+
+ while remaining_components:
+ logger.debug('r=%s dfd=%s', remaining_components, dirfd)
+
+ name = remaining_components.pop()
+
+ if dirfd and name != '/':
+ self._invalidators.append(WatchInvalidator(self, self._event, dirfd, name))
+
+ with Handle.open(name, os.O_PATH | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=dirfd) as fd:
+ mode = os.fstat(fd).st_mode
+
+ if stat.S_ISLNK(mode):
+ if remaining_symlink_lookups == 0:
+ raise OSError(errno.ELOOP, os.strerror(errno.ELOOP))
+ remaining_symlink_lookups -= 1
+ linkpath = os.readlink('', dir_fd=fd)
+ logger.debug('%s is a symlink. adding %s to components', name, linkpath)
+ remaining_components.add_path(linkpath)
+
+ else:
+ dirfd.close()
+ dirfd = fd.steal()
+
+ return dirfd.steal()
+
+ finally:
+ dirfd.close()
+
+ def close(self) -> None:
+ for invalidator in self._invalidators:
+ invalidator.close()
+ self._invalidators = []
+ self._source = None
+ self._fd.close()
diff --git a/src/cockpit/_vendor/systemd_ctypes/py.typed b/src/cockpit/_vendor/systemd_ctypes/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/py.typed
diff --git a/src/cockpit/_vendor/systemd_ctypes/typing.py b/src/cockpit/_vendor/systemd_ctypes/typing.py
new file mode 100644
index 0000000..c2ebe74
--- /dev/null
+++ b/src/cockpit/_vendor/systemd_ctypes/typing.py
@@ -0,0 +1,58 @@
+import typing
+from typing import TYPE_CHECKING
+
+# The goal here is to continue to work on Python 3.6 while pretending to have
+# access to some modern typing features. The shims provided here are only
+# enough for what we need for systemd_ctypes to work at runtime.
+
+
+if TYPE_CHECKING:
+ # See https://github.com/python/mypy/issues/1153 for why we do this separately
+ from typing import Annotated, ForwardRef, TypeGuard, get_args, get_origin
+
+else:
+ # typing.get_args() and .get_origin() appeared in Python 3.8 but Annotated
+ # arrived in 3.9. Unfortunately, it's difficult to implement a mocked up
+ # version of Annotated which works with the real typing.get_args() and
+ # .get_origin() in Python 3.8, so we use our own versions there as well.
+ try:
+ from typing import Annotated, get_args, get_origin
+ except ImportError:
+ class AnnotatedMeta(type):
+ def __getitem__(cls, params):
+ class AnnotatedType:
+ __origin__ = Annotated
+ __args__ = params
+ return AnnotatedType
+
+ class Annotated(metaclass=AnnotatedMeta):
+ pass
+
+ def get_args(annotation: typing.Any) -> typing.Tuple[typing.Any]:
+ return getattr(annotation, '__args__', ())
+
+ def get_origin(annotation: typing.Any) -> typing.Any:
+ return getattr(annotation, '__origin__', None)
+
+ try:
+ from typing import ForwardRef
+ except ImportError:
+ from typing import _ForwardRef as ForwardRef
+
+ try:
+ from typing import TypeGuard
+ except ImportError:
+ T = typing.TypeVar('T')
+
+ class TypeGuard(typing.Generic[T]):
+ pass
+
+
+__all__ = (
+ 'Annotated',
+ 'ForwardRef',
+ 'TypeGuard',
+ 'get_args',
+ 'get_origin',
+ 'TYPE_CHECKING',
+)
diff --git a/src/cockpit/_version.py b/src/cockpit/_version.py
new file mode 100644
index 0000000..ef78210
--- /dev/null
+++ b/src/cockpit/_version.py
@@ -0,0 +1 @@
+__version__ = '311'
diff --git a/src/cockpit/beiboot.py b/src/cockpit/beiboot.py
new file mode 100644
index 0000000..7b4692e
--- /dev/null
+++ b/src/cockpit/beiboot.py
@@ -0,0 +1,340 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import argparse
+import asyncio
+import base64
+import importlib.resources
+import logging
+import os
+import shlex
+import sys
+from pathlib import Path
+from typing import Dict, Iterable, Optional, Sequence
+
+from cockpit import polyfills
+from cockpit._vendor import ferny
+from cockpit._vendor.bei import bootloader
+from cockpit.beipack import BridgeBeibootHelper
+from cockpit.bridge import setup_logging
+from cockpit.channel import ChannelRoutingRule
+from cockpit.channels import PackagesChannel
+from cockpit.jsonutil import JsonObject
+from cockpit.packages import Packages, PackagesLoader, patch_libexecdir
+from cockpit.peer import Peer
+from cockpit.protocol import CockpitProblem
+from cockpit.router import Router, RoutingRule
+from cockpit.transports import StdioTransport
+
+logger = logging.getLogger('cockpit.beiboot')
+
+
+def ensure_ferny_askpass() -> Path:
+ """Create askpass executable
+
+ We need this for the flatpak: ssh and thus the askpass program run on the host (via flatpak-spawn),
+ not the flatpak. Thus we cannot use the shipped cockpit-askpass program.
+ """
+ src_path = importlib.resources.files(ferny.__name__) / 'interaction_client.py'
+ src_data = src_path.read_bytes()
+
+ # Create the file in $XDG_CACHE_HOME, one of the few locations that a flatpak can write to
+ xdg_cache_home = os.environ.get('XDG_CACHE_HOME')
+ if xdg_cache_home is None:
+ xdg_cache_home = os.path.expanduser('~/.cache')
+ os.makedirs(xdg_cache_home, exist_ok=True)
+ dest_path = Path(xdg_cache_home, 'cockpit-client-askpass')
+
+ logger.debug("Checking if %s exists...", dest_path)
+
+ # Check first to see if we already wrote the current version
+ try:
+ if dest_path.read_bytes() != src_data:
+ logger.debug(" ... it exists but is not the same version...")
+ raise ValueError
+ if not dest_path.stat().st_mode & 0o100:
+ logger.debug(" ... it has the correct contents, but is not executable...")
+ raise ValueError
+ except (FileNotFoundError, ValueError):
+ logger.debug(" ... writing contents.")
+ dest_path.write_bytes(src_data)
+ dest_path.chmod(0o700)
+
+ return dest_path
+
+
+def get_interesting_files() -> Iterable[str]:
+ for manifest in PackagesLoader.load_manifests():
+ for condition in manifest.conditions:
+ if condition.name in ('path-exists', 'path-not-exists') and isinstance(condition.value, str):
+ yield condition.value
+
+
+class ProxyPackagesLoader(PackagesLoader):
+ file_status: Dict[str, bool]
+
+ def check_condition(self, condition: str, value: object) -> bool:
+ assert isinstance(value, str)
+ assert value in self.file_status
+
+ if condition == 'path-exists':
+ return self.file_status[value]
+ elif condition == 'path-not-exists':
+ return not self.file_status[value]
+ else:
+ raise KeyError
+
+ def __init__(self, file_status: Dict[str, bool]):
+ self.file_status = file_status
+
+
+BEIBOOT_GADGETS = {
+ "report_exists": r"""
+ import os
+ def report_exists(files):
+ command('cockpit.report-exists', {name: os.path.exists(name) for name in files})
+ """,
+ **ferny.BEIBOOT_GADGETS
+}
+
+
+class DefaultRoutingRule(RoutingRule):
+ peer: 'Peer | None'
+
+ def __init__(self, router: Router):
+ super().__init__(router)
+
+ def apply_rule(self, options: JsonObject) -> 'Peer | None':
+ return self.peer
+
+ def shutdown(self) -> None:
+ if self.peer is not None:
+ self.peer.close()
+
+
+class AuthorizeResponder(ferny.AskpassHandler):
+ commands = ('ferny.askpass', 'cockpit.report-exists')
+ router: Router
+
+ def __init__(self, router: Router):
+ self.router = router
+
+ async def do_askpass(self, messages: str, prompt: str, hint: str) -> Optional[str]:
+ if hint == 'none':
+ # We have three problems here:
+ #
+ # - we have no way to present a message on the login
+ # screen without presenting a prompt and a button
+ # - the login screen will not try to repost the login
+ # request because it doesn't understand that we are not
+ # waiting on input, which means that it won't notice
+ # that we've logged in successfully
+ # - cockpit-ws has an issue where if we retry the request
+ # again after login succeeded then it won't forward the
+ # init message to the client, stalling the login. This
+ # is a race and can't be fixed without -ws changes.
+ #
+ # Let's avoid all of that by just showing nothing.
+ return None
+
+ challenge = 'X-Conversation - ' + base64.b64encode(prompt.encode()).decode()
+ response = await self.router.request_authorization(challenge,
+ messages=messages,
+ prompt=prompt,
+ hint=hint,
+ echo=False)
+
+ b64 = response.removeprefix('X-Conversation -').strip()
+ passwd = base64.b64decode(b64.encode()).decode()
+ logger.debug('Returning a %d chars password', len(passwd))
+ return passwd
+
+ async def do_custom_command(self, command: str, args: tuple, fds: list[int], stderr: str) -> None:
+ logger.debug('Got ferny command %s %s %s', command, args, stderr)
+
+ if command == 'cockpit.report-exists':
+ file_status, = args
+ # FIXME: evil duck typing here -- this is a half-way Bridge
+ self.router.packages = Packages(loader=ProxyPackagesLoader(file_status)) # type: ignore[attr-defined]
+ self.router.routing_rules.insert(0, ChannelRoutingRule(self.router, [PackagesChannel]))
+
+
+class SshPeer(Peer):
+ always: bool
+
+ def __init__(self, router: Router, destination: str, args: argparse.Namespace):
+ self.destination = destination
+ self.always = args.always
+ super().__init__(router)
+
+ async def do_connect_transport(self) -> None:
+ beiboot_helper = BridgeBeibootHelper(self)
+
+ agent = ferny.InteractionAgent([AuthorizeResponder(self.router), beiboot_helper])
+
+ # We want to run a python interpreter somewhere...
+ cmd: Sequence[str] = ('python3', '-ic', '# cockpit-bridge')
+ env: Sequence[str] = ()
+
+ in_flatpak = os.path.exists('/.flatpak-info')
+
+ # Remote host? Wrap command with SSH
+ if self.destination != 'localhost':
+ if in_flatpak:
+ # we run ssh and thus the helper on the host, always use the xdg-cache helper
+ ssh_askpass = ensure_ferny_askpass()
+ else:
+ # outside of the flatpak we expect cockpit-ws and thus an installed helper
+ askpass = patch_libexecdir('${libexecdir}/cockpit-askpass')
+ assert isinstance(askpass, str)
+ ssh_askpass = Path(askpass)
+ if not ssh_askpass.exists():
+ logger.error("Could not find cockpit-askpass helper at %r", askpass)
+
+ env = (
+ f'SSH_ASKPASS={ssh_askpass!s}',
+ 'DISPLAY=x',
+ 'SSH_ASKPASS_REQUIRE=force',
+ )
+ host, _, port = self.destination.rpartition(':')
+ # catch cases like `host:123` but not cases like `[2001:abcd::1]
+ if port.isdigit():
+ host_args = ['-p', port, host]
+ else:
+ host_args = [self.destination]
+
+ cmd = ('ssh', *host_args, shlex.join(cmd))
+
+ # Running in flatpak? Wrap command with flatpak-spawn --host
+ if in_flatpak:
+ cmd = ('flatpak-spawn', '--host',
+ *(f'--env={kv}' for kv in env),
+ *cmd)
+ env = ()
+
+ logger.debug("Launching command: cmd=%s env=%s", cmd, env)
+ transport = await self.spawn(cmd, env, stderr=agent, start_new_session=True)
+
+ if not self.always:
+ exec_cockpit_bridge_steps = [('try_exec', (['cockpit-bridge'],))]
+ else:
+ exec_cockpit_bridge_steps = []
+
+ # Send the first-stage bootloader
+ stage1 = bootloader.make_bootloader([
+ *exec_cockpit_bridge_steps,
+ ('report_exists', [list(get_interesting_files())]),
+ *beiboot_helper.steps,
+ ], gadgets=BEIBOOT_GADGETS)
+ transport.write(stage1.encode())
+
+ # Wait for "init" or error, handling auth and beiboot requests
+ await agent.communicate()
+
+ def transport_control_received(self, command: str, message: JsonObject) -> None:
+ if command == 'authorize':
+ # We've disabled this for explicit-superuser bridges, but older
+ # bridges don't support that and will ask us anyway.
+ return
+
+ super().transport_control_received(command, message)
+
+
+class SshBridge(Router):
+ packages: Optional[Packages] = None
+ ssh_peer: SshPeer
+
+ def __init__(self, args: argparse.Namespace):
+ # By default, we route everything to the other host. We add an extra
+ # routing rule for the packages webserver only if we're running the
+ # beipack.
+ rule = DefaultRoutingRule(self)
+ super().__init__([rule])
+
+ # This needs to be created after Router.__init__ is called.
+ self.ssh_peer = SshPeer(self, args.destination, args)
+ rule.peer = self.ssh_peer
+
+ def do_send_init(self):
+ pass # wait for the peer to do it first
+
+ def do_init(self, message):
+ # https://github.com/cockpit-project/cockpit/issues/18927
+ #
+ # We tell cockpit-ws that we have the explicit-superuser capability and
+ # handle it ourselves (just below) by sending `superuser-init-done` and
+ # passing {'superuser': False} on to the actual bridge (Python or C).
+ if isinstance(message.get('superuser'), dict):
+ self.write_control(command='superuser-init-done')
+ message['superuser'] = False
+ self.ssh_peer.write_control(message)
+
+
+async def run(args) -> None:
+ logger.debug("Hi. How are you today?")
+
+ bridge = SshBridge(args)
+ StdioTransport(asyncio.get_running_loop(), bridge)
+
+ try:
+ message = dict(await bridge.ssh_peer.start())
+
+ # See comment in do_init() above: we tell cockpit-ws that we support
+ # this and then handle it ourselves when we get the init message.
+ capabilities = message.setdefault('capabilities', {})
+ if not isinstance(capabilities, dict):
+ bridge.write_control(command='init', problem='protocol-error', message='capabilities must be a dict')
+ return
+ assert isinstance(capabilities, dict) # convince mypy
+ capabilities['explicit-superuser'] = True
+
+ # only patch the packages line if we are in beiboot mode
+ if bridge.packages:
+ message['packages'] = {p: None for p in bridge.packages.packages}
+
+ bridge.write_control(message)
+ bridge.ssh_peer.thaw_endpoint()
+ except ferny.InteractionError as exc:
+ sys.exit(str(exc))
+ except CockpitProblem as exc:
+ bridge.write_control(exc.attrs, command='init')
+ return
+
+ logger.debug('Startup done. Looping until connection closes.')
+ try:
+ await bridge.communicate()
+ except BrokenPipeError:
+ # expected if the peer doesn't hang up cleanly
+ pass
+
+
+def main() -> None:
+ polyfills.install()
+
+ parser = argparse.ArgumentParser(description='cockpit-bridge is run automatically inside of a Cockpit session.')
+ parser.add_argument('--always', action='store_true', help="Never try to run cockpit-bridge from the system")
+ parser.add_argument('--debug', action='store_true')
+ parser.add_argument('destination', help="Name of the remote host to connect to, or 'localhost'")
+ args = parser.parse_args()
+
+ setup_logging(debug=args.debug)
+
+ asyncio.run(run(args), debug=args.debug)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/cockpit/beipack.py b/src/cockpit/beipack.py
new file mode 100644
index 0000000..c200195
--- /dev/null
+++ b/src/cockpit/beipack.py
@@ -0,0 +1,76 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2023 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import logging
+import lzma
+from typing import List, Sequence, Tuple
+
+from cockpit._vendor import ferny
+from cockpit._vendor.bei import beipack
+
+from .data import read_cockpit_data_file
+from .peer import Peer, PeerError
+
+logger = logging.getLogger(__name__)
+
+
+def get_bridge_beipack_xz() -> Tuple[str, bytes]:
+ try:
+ bridge_beipack_xz = read_cockpit_data_file('cockpit-bridge.beipack.xz')
+ logger.debug('Got pre-built cockpit-bridge.beipack.xz')
+ except FileNotFoundError:
+ logger.debug('Pre-built cockpit-bridge.beipack.xz; building our own.')
+ # beipack ourselves
+ cockpit_contents = beipack.collect_module('cockpit', recursive=True)
+ bridge_beipack = beipack.pack(cockpit_contents, entrypoint='cockpit.bridge:main', args='beipack=True')
+ bridge_beipack_xz = lzma.compress(bridge_beipack.encode())
+ logger.debug(' ... done!')
+
+ return 'cockpit/data/cockpit-bridge.beipack.xz', bridge_beipack_xz
+
+
+class BridgeBeibootHelper(ferny.InteractionHandler):
+ # ferny.InteractionHandler ClassVar
+ commands = ['beiboot.provide', 'beiboot.exc']
+
+ peer: Peer
+ payload: bytes
+ steps: Sequence[Tuple[str, Sequence[object]]]
+
+ def __init__(self, peer: Peer, args: Sequence[str] = ()) -> None:
+ filename, payload = get_bridge_beipack_xz()
+
+ self.peer = peer
+ self.payload = payload
+ self.steps = (('boot_xz', (filename, len(payload), tuple(args))),)
+
+ async def run_command(self, command: str, args: Tuple, fds: List[int], stderr: str) -> None:
+ logger.debug('Got ferny request %s %s %s %s', command, args, fds, stderr)
+ if command == 'beiboot.provide':
+ try:
+ size, = args
+ assert size == len(self.payload)
+ except (AssertionError, ValueError) as exc:
+ raise PeerError('internal-error', message=f'ferny interaction error {exc!s}') from exc
+
+ assert self.peer.transport is not None
+ logger.debug('Writing %d bytes of payload', len(self.payload))
+ self.peer.transport.write(self.payload)
+ elif command == 'beiboot.exc':
+ raise PeerError('internal-error', message=f'Remote exception: {args[0]}')
+ else:
+ raise PeerError('internal-error', message=f'Unexpected ferny interaction command {command}')
diff --git a/src/cockpit/bridge.py b/src/cockpit/bridge.py
new file mode 100644
index 0000000..2251765
--- /dev/null
+++ b/src/cockpit/bridge.py
@@ -0,0 +1,315 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import argparse
+import asyncio
+import contextlib
+import json
+import logging
+import os
+import pwd
+import shlex
+import socket
+import stat
+import subprocess
+from typing import Iterable, List, Optional, Sequence, Tuple, Type
+
+from cockpit._vendor.ferny import interaction_client
+from cockpit._vendor.systemd_ctypes import bus, run_async
+
+from . import polyfills
+from ._version import __version__
+from .channel import ChannelRoutingRule
+from .channels import CHANNEL_TYPES
+from .config import Config, Environment
+from .internal_endpoints import EXPORTS
+from .jsonutil import JsonError, JsonObject, get_dict
+from .packages import BridgeConfig, Packages, PackagesListener
+from .peer import PeersRoutingRule
+from .remote import HostRoutingRule
+from .router import Router
+from .superuser import SuperuserRoutingRule
+from .transports import StdioTransport
+
+logger = logging.getLogger(__name__)
+
+
+class InternalBus:
+ exportees: List[bus.Slot]
+
+ def __init__(self, exports: Iterable[Tuple[str, Type[bus.BaseObject]]]):
+ client_socket, server_socket = socket.socketpair()
+ self.client = bus.Bus.new(fd=client_socket.detach())
+ self.server = bus.Bus.new(fd=server_socket.detach(), server=True)
+ self.exportees = [self.server.add_object(path, cls()) for path, cls in exports]
+
+ def export(self, path: str, obj: bus.BaseObject) -> None:
+ self.exportees.append(self.server.add_object(path, obj))
+
+
+class Bridge(Router, PackagesListener):
+ internal_bus: InternalBus
+ packages: Optional[Packages]
+ bridge_configs: Sequence[BridgeConfig]
+ args: argparse.Namespace
+
+ def __init__(self, args: argparse.Namespace):
+ self.internal_bus = InternalBus(EXPORTS)
+ self.bridge_configs = []
+ self.args = args
+
+ self.superuser_rule = SuperuserRoutingRule(self, privileged=args.privileged)
+ self.internal_bus.export('/superuser', self.superuser_rule)
+
+ self.internal_bus.export('/config', Config())
+ self.internal_bus.export('/environment', Environment())
+
+ self.peers_rule = PeersRoutingRule(self)
+
+ if args.beipack:
+ # Some special stuff for beipack
+ self.superuser_rule.set_configs((
+ BridgeConfig({
+ "privileged": True,
+ "spawn": ["sudo", "-k", "-A", "python3", "-ic", "# cockpit-bridge", "--privileged"],
+ "environ": ["SUDO_ASKPASS=ferny-askpass"],
+ }),
+ ))
+ self.packages = None
+ elif args.privileged:
+ self.packages = None
+ else:
+ self.packages = Packages(self)
+ self.internal_bus.export('/packages', self.packages)
+ self.packages_loaded()
+
+ super().__init__([
+ HostRoutingRule(self),
+ self.superuser_rule,
+ ChannelRoutingRule(self, CHANNEL_TYPES),
+ self.peers_rule,
+ ])
+
+ @staticmethod
+ def get_os_release():
+ try:
+ file = open('/etc/os-release', encoding='utf-8')
+ except FileNotFoundError:
+ try:
+ file = open('/usr/lib/os-release', encoding='utf-8')
+ except FileNotFoundError:
+ logger.warning("Neither /etc/os-release nor /usr/lib/os-release exists")
+ return {}
+
+ os_release = {}
+ for line in file.readlines():
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+ try:
+ k, v = line.split('=')
+ (v_parsed, ) = shlex.split(v) # expect exactly one token
+ except ValueError:
+ logger.warning('Ignoring invalid line in os-release: %r', line)
+ continue
+ os_release[k] = v_parsed
+ return os_release
+
+ def do_init(self, message: JsonObject) -> None:
+ # we're only interested in the case where this is a dict, but
+ # 'superuser' may well be `False` and that's not an error
+ with contextlib.suppress(JsonError):
+ superuser = get_dict(message, 'superuser')
+ self.superuser_rule.init(superuser)
+
+ def do_send_init(self) -> None:
+ init_args = {
+ 'capabilities': {'explicit-superuser': True},
+ 'command': 'init',
+ 'os-release': self.get_os_release(),
+ 'version': 1,
+ }
+
+ if self.packages is not None:
+ init_args['packages'] = {p: None for p in self.packages.packages}
+
+ self.write_control(init_args)
+
+ # PackagesListener interface
+ def packages_loaded(self) -> None:
+ assert self.packages
+ bridge_configs = self.packages.get_bridge_configs()
+ if self.bridge_configs != bridge_configs:
+ self.superuser_rule.set_configs(bridge_configs)
+ self.peers_rule.set_configs(bridge_configs)
+ self.bridge_configs = bridge_configs
+
+
+async def run(args) -> None:
+ logger.debug("Hi. How are you today?")
+
+ # Unit tests require this
+ me = pwd.getpwuid(os.getuid())
+ os.environ['HOME'] = me.pw_dir
+ os.environ['SHELL'] = me.pw_shell
+ os.environ['USER'] = me.pw_name
+
+ logger.debug('Starting the router.')
+ router = Bridge(args)
+ StdioTransport(asyncio.get_running_loop(), router)
+
+ logger.debug('Startup done. Looping until connection closes.')
+
+ try:
+ await router.communicate()
+ except (BrokenPipeError, ConnectionResetError):
+ # not unexpected if the peer doesn't hang up cleanly
+ pass
+
+
+def try_to_receive_stderr():
+ try:
+ ours, theirs = socket.socketpair()
+ with ours:
+ with theirs:
+ interaction_client.command(2, 'cockpit.send-stderr', fds=[theirs.fileno()])
+ _msg, fds, _flags, _addr = socket.recv_fds(ours, 1, 1)
+ except OSError:
+ return
+
+ try:
+ stderr_fd, = fds
+ # We're about to abruptly drop our end of the stderr socketpair that we
+ # share with the ferny agent. ferny would normally treat that as an
+ # unexpected error. Instruct it to do a clean exit, instead.
+ interaction_client.command(2, 'ferny.end')
+ os.dup2(stderr_fd, 2)
+ finally:
+ for fd in fds:
+ os.close(fd)
+
+
+def setup_journald() -> bool:
+ # If stderr is a socket, prefer systemd-journal logging. This covers the
+ # case we're already connected to the journal but also the case where we're
+ # talking to the ferny agent, while leaving logging to file or terminal
+ # unaffected.
+ if not stat.S_ISSOCK(os.fstat(2).st_mode):
+ # not a socket? Don't redirect.
+ return False
+
+ try:
+ import systemd.journal # type: ignore[import]
+ except ImportError:
+ # No python3-systemd? Don't redirect.
+ return False
+
+ logging.root.addHandler(systemd.journal.JournalHandler())
+ return True
+
+
+def setup_logging(*, debug: bool) -> None:
+ """Setup our logger with optional filtering of modules if COCKPIT_DEBUG env is set"""
+
+ modules = os.getenv('COCKPIT_DEBUG', '')
+
+ # Either setup logging via journal or via formatted messages to stderr
+ if not setup_journald():
+ logging.basicConfig(format='%(name)s-%(levelname)s: %(message)s')
+
+ if debug or modules == 'all':
+ logging.getLogger().setLevel(level=logging.DEBUG)
+ elif modules:
+ for module in modules.split(','):
+ module = module.strip()
+ if not module:
+ continue
+
+ logging.getLogger(module).setLevel(logging.DEBUG)
+
+
+def start_ssh_agent() -> None:
+ # Launch the agent so that it goes down with us on EOF; PDEATHSIG would be more robust,
+ # but it gets cleared on setgid ssh-agent, which some distros still do
+ try:
+ proc = subprocess.Popen(['ssh-agent', 'sh', '-ec', 'echo SSH_AUTH_SOCK=$SSH_AUTH_SOCK; read a'],
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True)
+ assert proc.stdout is not None
+
+ # Wait for the agent to write at least one line and look for the
+ # listener socket. If we fail to find it, kill the agent — something
+ # went wrong.
+ for token in shlex.shlex(proc.stdout.readline(), punctuation_chars=True):
+ if token.startswith('SSH_AUTH_SOCK='):
+ os.environ['SSH_AUTH_SOCK'] = token.replace('SSH_AUTH_SOCK=', '', 1)
+ break
+ else:
+ proc.terminate()
+ proc.wait()
+
+ except FileNotFoundError:
+ logger.debug("Couldn't start ssh-agent (FileNotFoundError)")
+
+ except OSError as exc:
+ logger.warning("Could not start ssh-agent: %s", exc)
+
+
+def main(*, beipack: bool = False) -> None:
+ polyfills.install()
+
+ parser = argparse.ArgumentParser(description='cockpit-bridge is run automatically inside of a Cockpit session.')
+ parser.add_argument('--privileged', action='store_true', help='Privileged copy of the bridge')
+ parser.add_argument('--packages', action='store_true', help='Show Cockpit package information')
+ parser.add_argument('--bridges', action='store_true', help='Show Cockpit bridges information')
+ parser.add_argument('--debug', action='store_true', help='Enable debug output (very verbose)')
+ parser.add_argument('--version', action='store_true', help='Show Cockpit version information')
+ args = parser.parse_args()
+
+ # This is determined by who calls us
+ args.beipack = beipack
+
+ # If we were run with --privileged then our stderr is currently being
+ # consumed by the main bridge looking for startup-related error messages.
+ # Let's switch back to the original stderr stream, which has a side-effect
+ # of indicating that our startup is more or less complete. Any errors
+ # after this point will land in the journal.
+ if args.privileged:
+ try_to_receive_stderr()
+
+ setup_logging(debug=args.debug)
+
+ # Special modes
+ if args.packages:
+ Packages().show()
+ return
+ elif args.version:
+ print(f'Version: {__version__}\nProtocol: 1')
+ return
+ elif args.bridges:
+ print(json.dumps([config.__dict__ for config in Packages().get_bridge_configs()], indent=2))
+ return
+
+ # The privileged bridge doesn't need ssh-agent, but the main one does
+ if 'SSH_AUTH_SOCK' not in os.environ and not args.privileged:
+ start_ssh_agent()
+
+ # asyncio.run() shim for Python 3.6 support
+ run_async(run(args), debug=args.debug)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/cockpit/channel.py b/src/cockpit/channel.py
new file mode 100644
index 0000000..9589943
--- /dev/null
+++ b/src/cockpit/channel.py
@@ -0,0 +1,527 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import asyncio
+import json
+import logging
+from typing import BinaryIO, ClassVar, Dict, Generator, List, Optional, Sequence, Set, Tuple, Type
+
+from .jsonutil import JsonError, JsonObject, JsonValue, create_object, get_bool, get_str
+from .protocol import CockpitProblem
+from .router import Endpoint, Router, RoutingRule
+
+logger = logging.getLogger(__name__)
+
+
+class ChannelRoutingRule(RoutingRule):
+ table: Dict[str, List[Type['Channel']]]
+
+ def __init__(self, router: Router, channel_types: List[Type['Channel']]):
+ super().__init__(router)
+ self.table = {}
+
+ # Sort the channels into buckets by payload type
+ for cls in channel_types:
+ entry = self.table.setdefault(cls.payload, [])
+ entry.append(cls)
+
+ # Within each bucket, sort the channels so those with more
+ # restrictions are considered first.
+ for entry in self.table.values():
+ entry.sort(key=lambda cls: len(cls.restrictions), reverse=True)
+
+ def check_restrictions(self, restrictions: Sequence[Tuple[str, object]], options: JsonObject) -> bool:
+ for key, expected_value in restrictions:
+ our_value = options.get(key)
+
+ # If the match rule specifies that a value must be present and
+ # we don't have it, then fail.
+ if our_value is None:
+ return False
+
+ # If the match rule specified a specific expected value, and
+ # our value doesn't match it, then fail.
+ if expected_value is not None and our_value != expected_value:
+ return False
+
+ # Everything checked out
+ return True
+
+ def apply_rule(self, options: JsonObject) -> Optional['Channel']:
+ assert self.router is not None
+
+ payload = options.get('payload')
+ if not isinstance(payload, str):
+ return None
+
+ for cls in self.table.get(payload, []):
+ if self.check_restrictions(cls.restrictions, options):
+ return cls(self.router)
+ else:
+ return None
+
+ def shutdown(self):
+ pass # we don't hold any state
+
+
+class ChannelError(CockpitProblem):
+ pass
+
+
+class Channel(Endpoint):
+ # Values borrowed from C implementation
+ BLOCK_SIZE = 16 * 1024
+ SEND_WINDOW = 2 * 1024 * 1024
+
+ # Flow control book-keeping
+ _send_pings: bool = False
+ _out_sequence: int = 0
+ _out_window: int = SEND_WINDOW
+
+ # Task management
+ _tasks: Set[asyncio.Task]
+ _close_args: Optional[JsonObject] = None
+
+ # Must be filled in by the channel implementation
+ payload: ClassVar[str]
+ restrictions: ClassVar[Sequence[Tuple[str, object]]] = ()
+
+ # These get filled in from .do_open()
+ channel = ''
+ group = ''
+
+ # input
+ def do_control(self, command, message):
+ # Break the various different kinds of control messages out into the
+ # things that our subclass may be interested in handling. We drop the
+ # 'message' field for handlers that don't need it.
+ if command == 'open':
+ self._tasks = set()
+ self.channel = message['channel']
+ if get_bool(message, 'flow-control', default=False):
+ self._send_pings = True
+ self.group = get_str(message, 'group', 'default')
+ self.freeze_endpoint()
+ self.do_open(message)
+ elif command == 'ready':
+ self.do_ready()
+ elif command == 'done':
+ self.do_done()
+ elif command == 'close':
+ self.do_close()
+ elif command == 'ping':
+ self.do_ping(message)
+ elif command == 'pong':
+ self.do_pong(message)
+ elif command == 'options':
+ self.do_options(message)
+
+ def do_channel_control(self, channel: str, command: str, message: JsonObject) -> None:
+ # Already closing? Ignore.
+ if self._close_args is not None:
+ return
+
+ # Catch errors and turn them into close messages
+ try:
+ try:
+ self.do_control(command, message)
+ except JsonError as exc:
+ raise ChannelError('protocol-error', message=str(exc)) from exc
+ except ChannelError as exc:
+ self.close(exc.attrs)
+
+ def do_kill(self, host: 'str | None', group: 'str | None', _message: JsonObject) -> None:
+ # Already closing? Ignore.
+ if self._close_args is not None:
+ return
+
+ if host is not None:
+ return
+ if group is not None and self.group != group:
+ return
+ self.do_close()
+
+ # At least this one really ought to be implemented...
+ def do_open(self, options: JsonObject) -> None:
+ raise NotImplementedError
+
+ # ... but many subclasses may reasonably want to ignore some of these.
+ def do_ready(self) -> None:
+ pass
+
+ def do_done(self) -> None:
+ pass
+
+ def do_close(self) -> None:
+ self.close()
+
+ def do_options(self, message: JsonObject) -> None:
+ raise ChannelError('not-supported', message='This channel does not implement "options"')
+
+ # 'reasonable' default, overridden in other channels for receive-side flow control
+ def do_ping(self, message: JsonObject) -> None:
+ self.send_pong(message)
+
+ def do_channel_data(self, channel: str, data: bytes) -> None:
+ # Already closing? Ignore.
+ if self._close_args is not None:
+ return
+
+ # Catch errors and turn them into close messages
+ try:
+ self.do_data(data)
+ except ChannelError as exc:
+ self.close(exc.attrs)
+
+ def do_data(self, _data: bytes) -> None:
+ # By default, channels can't receive data.
+ self.close()
+
+ # output
+ def ready(self, **kwargs: JsonValue) -> None:
+ self.thaw_endpoint()
+ self.send_control(command='ready', **kwargs)
+
+ def done(self) -> None:
+ self.send_control(command='done')
+
+ # tasks and close management
+ def is_closing(self) -> bool:
+ return self._close_args is not None
+
+ def _close_now(self) -> None:
+ self.shutdown_endpoint(self._close_args)
+
+ def _task_done(self, task):
+ # Strictly speaking, we should read the result and check for exceptions but:
+ # - exceptions bubbling out of the task are programming errors
+ # - the only thing we'd do with it anyway, is to show it
+ # - Python already does that with its "Task exception was never retrieved" messages
+ self._tasks.remove(task)
+ if self._close_args is not None and not self._tasks:
+ self._close_now()
+
+ def create_task(self, coroutine, name=None):
+ """Create a task associated with the channel.
+
+ All tasks must exit before the channel can close. You may not create
+ new tasks after calling .close().
+ """
+ assert self._close_args is None
+ task = asyncio.create_task(coroutine)
+ self._tasks.add(task)
+ task.add_done_callback(self._task_done)
+ return task
+
+ def close(self, close_args: 'JsonObject | None' = None) -> None:
+ """Requests the channel to be closed.
+
+ After you call this method, you won't get anymore `.do_*()` calls.
+
+ This will wait for any running tasks to complete before sending the
+ close message.
+ """
+ if self._close_args is not None:
+ # close already requested
+ return
+ self._close_args = close_args or {}
+ if not self._tasks:
+ self._close_now()
+
+ def send_data(self, data: bytes) -> bool:
+ """Send data and handle book-keeping for flow control.
+
+ The flow control is "advisory". The data is sent immediately, even if
+ it's larger than the window. In general you should try to send packets
+ which are approximately Channel.BLOCK_SIZE in size.
+
+ Returns True if there is still room in the window, or False if you
+ should stop writing for now. In that case, `.do_resume_send()` will be
+ called later when there is more room.
+ """
+ self.send_channel_data(self.channel, data)
+
+ if self._send_pings:
+ out_sequence = self._out_sequence + len(data)
+ if self._out_sequence // Channel.BLOCK_SIZE != out_sequence // Channel.BLOCK_SIZE:
+ self.send_control(command='ping', sequence=out_sequence)
+ self._out_sequence = out_sequence
+
+ return self._out_sequence < self._out_window
+
+ def do_pong(self, message):
+ if not self._send_pings: # huh?
+ logger.warning("Got wild pong on channel %s", self.channel)
+ return
+
+ self._out_window = message['sequence'] + Channel.SEND_WINDOW
+ if self._out_sequence < self._out_window:
+ self.do_resume_send()
+
+ def do_resume_send(self) -> None:
+ """Called to indicate that the channel may start sending again."""
+ # change to `raise NotImplementedError` after everyone implements it
+
+ json_encoder: ClassVar[json.JSONEncoder] = json.JSONEncoder(indent=2)
+
+ def send_json(self, _msg: 'JsonObject | None' = None, **kwargs: JsonValue) -> bool:
+ pretty = self.json_encoder.encode(create_object(_msg, kwargs)) + '\n'
+ return self.send_data(pretty.encode())
+
+ def send_control(self, command: str, **kwargs: JsonValue) -> None:
+ self.send_channel_control(self.channel, command, None, **kwargs)
+
+ def send_pong(self, message: JsonObject) -> None:
+ self.send_channel_control(self.channel, 'pong', message)
+
+
+class ProtocolChannel(Channel, asyncio.Protocol):
+ """A channel subclass that implements the asyncio Protocol interface.
+
+ In effect, data sent to this channel will be written to the connected
+ transport, and vice-versa. Flow control is supported.
+
+ The default implementation of the .do_open() method calls the
+ .create_transport() abstract method. This method should return a transport
+ which will be used for communication on the channel.
+
+ Otherwise, if the subclass implements .do_open() itself, it is responsible
+ for setting up the connection and ensuring that .connection_made() is called.
+ """
+ _transport: Optional[asyncio.Transport]
+ _loop: Optional[asyncio.AbstractEventLoop]
+ _send_pongs: bool = True
+ _last_ping: Optional[JsonObject] = None
+ _create_transport_task = None
+
+ # read-side EOF handling
+ _close_on_eof: bool = False
+ _eof: bool = False
+
+ async def create_transport(self, loop: asyncio.AbstractEventLoop, options: JsonObject) -> asyncio.Transport:
+ """Creates the transport for this channel, according to options.
+
+ The event loop for the transport is passed to the function. The
+ protocol for the transport is the channel object, itself (self).
+
+ This needs to be implemented by the subclass.
+ """
+ raise NotImplementedError
+
+ def do_open(self, options: JsonObject) -> None:
+ loop = asyncio.get_running_loop()
+ self._create_transport_task = asyncio.create_task(self.create_transport(loop, options))
+ self._create_transport_task.add_done_callback(self.create_transport_done)
+
+ def create_transport_done(self, task: 'asyncio.Task[asyncio.Transport]') -> None:
+ assert task is self._create_transport_task
+ self._create_transport_task = None
+ try:
+ transport = task.result()
+ except ChannelError as exc:
+ self.close(exc.attrs)
+ return
+
+ self.connection_made(transport)
+ self.ready()
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ assert isinstance(transport, asyncio.Transport)
+ self._transport = transport
+
+ def _get_close_args(self) -> JsonObject:
+ return {}
+
+ def connection_lost(self, exc: Optional[Exception]) -> None:
+ self.close(self._get_close_args())
+
+ def do_data(self, data: bytes) -> None:
+ assert self._transport is not None
+ self._transport.write(data)
+
+ def do_done(self) -> None:
+ assert self._transport is not None
+ if self._transport.can_write_eof():
+ self._transport.write_eof()
+
+ def do_close(self) -> None:
+ if self._transport is not None:
+ self._transport.close()
+
+ def data_received(self, data: bytes) -> None:
+ assert self._transport is not None
+ if not self.send_data(data):
+ self._transport.pause_reading()
+
+ def do_resume_send(self) -> None:
+ assert self._transport is not None
+ self._transport.resume_reading()
+
+ def close_on_eof(self) -> None:
+ """Mark the channel to be closed on EOF.
+
+ Normally, ProtocolChannel tries to keep the channel half-open after
+ receiving EOF from the transport. This instructs that the channel
+ should be closed on EOF.
+
+ If EOF was already received, then calling this function will close the
+ channel immediately.
+
+ If you don't call this function, you are responsible for closing the
+ channel yourself.
+ """
+ self._close_on_eof = True
+ if self._eof:
+ assert self._transport is not None
+ self._transport.close()
+
+ def eof_received(self) -> bool:
+ self._eof = True
+ self.done()
+ return not self._close_on_eof
+
+ # Channel receive-side flow control
+ def do_ping(self, message):
+ if self._send_pongs:
+ self.send_pong(message)
+ else:
+ # we'll have to pong later
+ self._last_ping = message
+
+ def pause_writing(self) -> None:
+ # We can't actually stop writing, but we can stop replying to pings
+ self._send_pongs = False
+
+ def resume_writing(self) -> None:
+ self._send_pongs = True
+ if self._last_ping is not None:
+ self.send_pong(self._last_ping)
+ self._last_ping = None
+
+
+class AsyncChannel(Channel):
+ """A subclass for async/await-style implementation of channels, with flow control
+
+ This subclass provides asynchronous `read()` and `write()` calls for
+ subclasses, with familiar semantics. `write()` doesn't buffer, so the
+ `done()` method on the base channel class can be used in a way similar to
+ `shutdown()`. A high-level `sendfile()` method is available to send the
+ entire contents of a binary-mode file-like object.
+
+ The subclass must provide an async `run()` function, which will be spawned
+ as a task.
+
+ On the receiving side, the channel will respond to flow control pings to
+ indicate that it has received the data, but only after it has been consumed
+ by `read()`.
+
+ On the sending side, write() will block if the channel backs up.
+ """
+
+ # Receive-side flow control: intermix pings and data in the queue and reply
+ # to pings as we dequeue them. This is a buffer: since we need to handle
+ # do_data() without blocking, we have no choice.
+ receive_queue = None
+
+ # Send-side flow control
+ write_waiter = None
+
+ async def run(self, options):
+ raise NotImplementedError
+
+ async def run_wrapper(self, options):
+ try:
+ await self.run(options)
+ self.close()
+ except ChannelError as exc:
+ self.close(exc.attrs)
+
+ async def read(self):
+ while True:
+ item = await self.receive_queue.get()
+ if isinstance(item, bytes):
+ return item
+ self.send_pong(item)
+
+ async def write(self, data):
+ if not self.send_data(data):
+ self.write_waiter = asyncio.get_running_loop().create_future()
+ await self.write_waiter
+
+ async def sendfile(self, stream: BinaryIO) -> None:
+ loop = asyncio.get_running_loop()
+ with stream:
+ while True:
+ data = await loop.run_in_executor(None, stream.read, Channel.BLOCK_SIZE)
+ if data == b'':
+ break
+ await self.write(data)
+
+ self.done()
+
+ def do_resume_send(self) -> None:
+ if self.write_waiter is not None:
+ self.write_waiter.set_result(None)
+ self.write_waiter = None
+
+ def do_open(self, options):
+ self.receive_queue = asyncio.Queue()
+ self.create_task(self.run_wrapper(options), name=f'{self.__class__.__name__}.run_wrapper({options})')
+
+ def do_done(self):
+ self.receive_queue.put_nowait(b'')
+
+ def do_close(self):
+ # we might have already sent EOF for done, but two EOFs won't hurt anyone
+ self.receive_queue.put_nowait(b'')
+
+ def do_ping(self, message):
+ self.receive_queue.put_nowait(message)
+
+ def do_data(self, data):
+ if not isinstance(data, bytes):
+ # this will persist past this callback, so make sure we take our
+ # own copy, in case this was a memoryview into a bytearray.
+ data = bytes(data)
+
+ self.receive_queue.put_nowait(data)
+
+
+class GeneratorChannel(Channel):
+ """A trivial Channel subclass for sending data from a generator with flow control.
+
+ Calls the .do_yield_data() generator with the options from the open message
+ and sends the data which it yields. If the generator returns a value it
+ will be used for the close message.
+ """
+ DataGenerator = Generator[bytes, None, Optional[JsonObject]]
+ __generator: DataGenerator
+
+ def do_yield_data(self, options: JsonObject) -> 'DataGenerator':
+ raise NotImplementedError
+
+ def do_open(self, options: JsonObject) -> None:
+ self.__generator = self.do_yield_data(options)
+ self.do_resume_send()
+
+ def do_resume_send(self) -> None:
+ try:
+ while self.send_data(next(self.__generator)):
+ pass
+ except StopIteration as stop:
+ self.done()
+ self.close(stop.value)
diff --git a/src/cockpit/channels/__init__.py b/src/cockpit/channels/__init__.py
new file mode 100644
index 0000000..cf255aa
--- /dev/null
+++ b/src/cockpit/channels/__init__.py
@@ -0,0 +1,40 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from .dbus import DBusChannel
+from .filesystem import FsInfoChannel, FsListChannel, FsReadChannel, FsReplaceChannel, FsWatchChannel
+from .http import HttpChannel
+from .metrics import InternalMetricsChannel
+from .packages import PackagesChannel
+from .stream import SocketStreamChannel, SubprocessStreamChannel
+from .trivial import EchoChannel, NullChannel
+
+CHANNEL_TYPES = [
+ DBusChannel,
+ EchoChannel,
+ FsInfoChannel,
+ FsListChannel,
+ FsReadChannel,
+ FsReplaceChannel,
+ FsWatchChannel,
+ HttpChannel,
+ InternalMetricsChannel,
+ NullChannel,
+ PackagesChannel,
+ SubprocessStreamChannel,
+ SocketStreamChannel,
+]
diff --git a/src/cockpit/channels/dbus.py b/src/cockpit/channels/dbus.py
new file mode 100644
index 0000000..664f181
--- /dev/null
+++ b/src/cockpit/channels/dbus.py
@@ -0,0 +1,520 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+# Missing stuff compared to the C bridge that we should probably add:
+#
+# - removing matches
+# - removing watches
+# - emitting of signals
+# - publishing of objects
+# - failing more gracefully in some cases (during open, etc)
+#
+# Stuff we might or might not do:
+#
+# - using non-default service names
+#
+# Stuff we should probably not do:
+#
+# - emulation of ObjectManager via recursive introspection
+# - automatic detection of ObjectManager below the given path_namespace
+# - recursive scraping of properties for new object paths
+# (for path_namespace watches that don't hit an ObjectManager)
+
+import asyncio
+import errno
+import json
+import logging
+import traceback
+import xml.etree.ElementTree as ET
+
+from cockpit._vendor import systemd_ctypes
+from cockpit._vendor.systemd_ctypes import Bus, BusError, introspection
+
+from ..channel import Channel, ChannelError
+
+logger = logging.getLogger(__name__)
+
+# The dbusjson3 payload
+#
+# This channel payload type translates JSON encoded messages on a
+# Cockpit channel to D-Bus messages, in a mostly straightforward way.
+# See doc/protocol.md for a description of the basics.
+#
+# However, dbusjson3 offers some advanced features as well that are
+# meant to support the "magic" DBusProxy objects implemented by
+# cockpit.js. Those proxy objects "magically" expose all the methods
+# and properties of a D-Bus interface without requiring any explicit
+# binding code to be generated for a JavaScript client. A dbusjson3
+# channel does this by doing automatic introspection and property
+# retrieval without much direction from the JavaScript client.
+#
+# The details of what exactly is done is not specified very strictly,
+# and the Python bridge will likely differ from the C bridge
+# significantly. This will be informed by what existing code actually
+# needs, and we might end up with a more concrete description of what
+# a client can actually expect.
+#
+# Here is an example of a more complex scenario:
+#
+# - The client adds a "watch" for a path namespace. There is a
+# ObjectManager at the given path and the bridge emits "meta" and
+# "notify" messages to describe all interfaces and objects reported
+# by that ObjectManager.
+#
+# - The client makes a method call that causes a new object with a new
+# interface to appear at the ObjectManager. The bridge will send a
+# "meta" and "notify" message to describe this new object.
+#
+# - Since the InterfacesAdded signal was emitted before the method
+# reply, the bridge must send the "meta" and "notify" messages
+# before the method reply message.
+#
+# - However, in order to construct the "meta" message, the bridge must
+# perform a Introspect call, and consequently must delay sending the
+# method reply until that call has finished.
+#
+# The Python bridge implements this delaying of messages with
+# coroutines and a fair mutex. Every message coming from D-Bus will
+# wait on the mutex for its turn to send its message on the Cockpit
+# channel, and will keep that mutex locked until it is done with
+# sending. Since the mutex is fair, everyone will nicely wait in line
+# without messages getting re-ordered.
+#
+# The scenario above will play out like this:
+#
+# - While adding the initial "watch", the lock is held until the
+# "meta" and "notify" messages have been sent.
+#
+# - Later, when the InterfacesAdded signal comes in that has been
+# triggered by the method call, the mutex will be locked while the
+# necessary introspection is going on.
+#
+# - The method reply will likely come while the mutex is locked, and
+# the task for sending that reply on the Cockpit channel will enter
+# the wait queue of the mutex.
+#
+# - Once the introspection is done and the new "meta" and "notify"
+# messages have been sent, the mutex is unlocked, the method reply
+# task acquires it, and sends its message.
+
+
+class InterfaceCache:
+ def __init__(self):
+ self.cache = {}
+ self.old = set() # Interfaces already returned by get_interface_if_new
+
+ def inject(self, interfaces):
+ self.cache.update(interfaces)
+
+ async def introspect_path(self, bus, destination, object_path):
+ xml, = await bus.call_method_async(destination, object_path,
+ 'org.freedesktop.DBus.Introspectable',
+ 'Introspect')
+
+ et = ET.fromstring(xml)
+
+ interfaces = {tag.attrib['name']: introspection.parse_interface(tag) for tag in et.findall('interface')}
+
+ # Add all interfaces we found: we might use them later
+ self.inject(interfaces)
+
+ return interfaces
+
+ async def get_interface(self, interface_name, bus=None, destination=None, object_path=None):
+ try:
+ return self.cache[interface_name]
+ except KeyError:
+ pass
+
+ if bus and object_path:
+ try:
+ await self.introspect_path(bus, destination, object_path)
+ except BusError:
+ pass
+
+ return self.cache.get(interface_name)
+
+ async def get_interface_if_new(self, interface_name, bus, destination, object_path):
+ if interface_name in self.old:
+ return None
+ self.old.add(interface_name)
+ return await self.get_interface(interface_name, bus, destination, object_path)
+
+ async def get_signature(self, interface_name, method, bus=None, destination=None, object_path=None):
+ interface = await self.get_interface(interface_name, bus, destination, object_path)
+ if interface is None:
+ raise KeyError(f'Interface {interface_name} is not found')
+
+ return ''.join(interface['methods'][method]['in'])
+
+
+def notify_update(notify, path, interface_name, props):
+ notify.setdefault(path, {})[interface_name] = {k: v.value for k, v in props.items()}
+
+
+class DBusChannel(Channel):
+ json_encoder = systemd_ctypes.JSONEncoder(indent=2)
+ payload = 'dbus-json3'
+
+ matches = None
+ name = None
+ bus = None
+ owner = None
+
+ async def setup_name_owner_tracking(self):
+ def send_owner(owner):
+ # We must be careful not to send duplicate owner
+ # notifications. cockpit.js relies on that.
+ if self.owner != owner:
+ self.owner = owner
+ self.send_json(owner=owner)
+
+ def handler(message):
+ name, old, new = message.get_body()
+ send_owner(owner=new if new != "" else None)
+ self.add_signal_handler(handler,
+ sender='org.freedesktop.DBus',
+ path='/org/freedesktop/DBus',
+ interface='org.freedesktop.DBus',
+ member='NameOwnerChanged',
+ arg0=self.name)
+ try:
+ unique_name, = await self.bus.call_method_async("org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "GetNameOwner", "s", self.name)
+ except BusError as error:
+ if error.name == "org.freedesktop.DBus.Error.NameHasNoOwner":
+ # Try to start it. If it starts successfully, we will
+ # get a NameOwnerChanged signal (which will set
+ # self.owner) before StartServiceByName returns.
+ try:
+ await self.bus.call_method_async("org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "StartServiceByName", "su", self.name, 0)
+ except BusError as start_error:
+ logger.debug("Failed to start service '%s': %s", self.name, start_error.message)
+ self.send_json(owner=None)
+ else:
+ logger.debug("Failed to get owner of service '%s': %s", self.name, error.message)
+ else:
+ send_owner(unique_name)
+
+ def do_open(self, options):
+ self.cache = InterfaceCache()
+ self.name = options.get('name')
+ self.matches = []
+
+ bus = options.get('bus')
+ address = options.get('address')
+
+ try:
+ if address is not None:
+ if bus is not None and bus != 'none':
+ raise ChannelError('protocol-error', message='only one of "bus" and "address" can be specified')
+ logger.debug('get bus with address %s for %s', address, self.name)
+ self.bus = Bus.new(address=address, bus_client=self.name is not None)
+ elif bus == 'internal':
+ logger.debug('get internal bus for %s', self.name)
+ self.bus = self.router.internal_bus.client
+ else:
+ if bus == 'session':
+ logger.debug('get session bus for %s', self.name)
+ self.bus = Bus.default_user()
+ elif bus == 'system' or bus is None:
+ logger.debug('get system bus for %s', self.name)
+ self.bus = Bus.default_system()
+ else:
+ raise ChannelError('protocol-error', message=f'invalid bus "{bus}"')
+ except OSError as exc:
+ raise ChannelError('protocol-error', message=f'failed to connect to {bus} bus: {exc}') from exc
+
+ try:
+ self.bus.attach_event(None, 0)
+ except OSError as err:
+ if err.errno != errno.EBUSY:
+ raise
+
+ # This needs to be a fair mutex so that outgoing messages don't
+ # get re-ordered. asyncio.Lock is fair.
+ self.watch_processing_lock = asyncio.Lock()
+
+ if self.name is not None:
+ async def get_ready():
+ async with self.watch_processing_lock:
+ await self.setup_name_owner_tracking()
+ if self.owner:
+ self.ready(unique_name=self.owner)
+ else:
+ self.close({'problem': 'not-found'})
+ self.create_task(get_ready())
+ else:
+ self.ready()
+
+ def add_signal_handler(self, handler, **kwargs):
+ r = dict(**kwargs)
+ r['type'] = 'signal'
+ if 'sender' not in r and self.name is not None:
+ r['sender'] = self.name
+ # HACK - https://github.com/bus1/dbus-broker/issues/309
+ # path_namespace='/' in a rule does not work.
+ if r.get('path_namespace') == "/":
+ del r['path_namespace']
+
+ def filter_owner(message):
+ if self.owner is not None and self.owner == message.get_sender():
+ handler(message)
+
+ if self.name is not None and 'sender' in r and r['sender'] == self.name:
+ func = filter_owner
+ else:
+ func = handler
+ r_string = ','.join(f"{key}='{value}'" for key, value in r.items())
+ if not self.is_closing():
+ # this gets an EINTR very often especially on RHEL 8
+ while True:
+ try:
+ match = self.bus.add_match(r_string, func)
+ break
+ except InterruptedError:
+ pass
+
+ self.matches.append(match)
+
+ def add_async_signal_handler(self, handler, **kwargs):
+ def sync_handler(message):
+ self.create_task(handler(message))
+ self.add_signal_handler(sync_handler, **kwargs)
+
+ async def do_call(self, message):
+ path, iface, method, args = message['call']
+ cookie = message.get('id')
+ flags = message.get('flags')
+
+ timeout = message.get('timeout')
+ if timeout is not None:
+ # sd_bus timeout is µs, cockpit API timeout is ms
+ timeout *= 1000
+ else:
+ # sd_bus has no "indefinite" timeout, so use MAX_UINT64
+ timeout = 2 ** 64 - 1
+
+ # We have to figure out the signature of the call. Either we got told it:
+ signature = message.get('type')
+
+ # ... or there aren't any arguments
+ if signature is None and len(args) == 0:
+ signature = ''
+
+ # ... or we need to introspect
+ if signature is None:
+ try:
+ logger.debug('Doing introspection request for %s %s', iface, method)
+ signature = await self.cache.get_signature(iface, method, self.bus, self.name, path)
+ except BusError as error:
+ self.send_json(error=[error.name, [f'Introspection: {error.message}']], id=cookie)
+ return
+ except KeyError:
+ self.send_json(
+ error=[
+ "org.freedesktop.DBus.Error.UnknownMethod",
+ [f"Introspection data for method {iface} {method} not available"]],
+ id=cookie)
+ return
+ except Exception as exc:
+ self.send_json(error=['python.error', [f'Introspection: {exc!s}']], id=cookie)
+ return
+
+ try:
+ method_call = self.bus.message_new_method_call(self.name, path, iface, method, signature, *args)
+ reply = await self.bus.call_async(method_call, timeout=timeout)
+ # If the method call has kicked off any signals related to
+ # watch processing, wait for that to be done.
+ async with self.watch_processing_lock:
+ # TODO: stop hard-coding the endian flag here.
+ self.send_json(
+ reply=[reply.get_body()], id=cookie,
+ flags="<" if flags is not None else None,
+ type=reply.get_signature(True)) # noqa: FBT003
+ except BusError as error:
+ # actually, should send the fields from the message body
+ self.send_json(error=[error.name, [error.message]], id=cookie)
+ except Exception:
+ logger.exception("do_call(%s): generic exception", message)
+ self.send_json(error=['python.error', [traceback.format_exc()]], id=cookie)
+
+ async def do_add_match(self, message):
+ add_match = message['add-match']
+ logger.debug('adding match %s', add_match)
+
+ async def match_hit(message):
+ logger.debug('got match')
+ async with self.watch_processing_lock:
+ self.send_json(signal=[
+ message.get_path(),
+ message.get_interface(),
+ message.get_member(),
+ list(message.get_body())
+ ])
+
+ self.add_async_signal_handler(match_hit, **add_match)
+
+ async def setup_objectmanager_watch(self, path, interface_name, meta, notify):
+ # Watch the objects managed by the ObjectManager at "path".
+ # Properties are not watched, that is done by setup_path_watch
+ # below via recursive_props == True.
+
+ async def handler(message):
+ member = message.get_member()
+ if member == "InterfacesAdded":
+ (path, interface_props) = message.get_body()
+ logger.debug('interfaces added %s %s', path, interface_props)
+ meta = {}
+ notify = {}
+ async with self.watch_processing_lock:
+ for name, props in interface_props.items():
+ if interface_name is None or name == interface_name:
+ mm = await self.cache.get_interface_if_new(name, self.bus, self.name, path)
+ if mm:
+ meta.update({name: mm})
+ notify_update(notify, path, name, props)
+ self.send_json(meta=meta)
+ self.send_json(notify=notify)
+ elif member == "InterfacesRemoved":
+ (path, interfaces) = message.get_body()
+ logger.debug('interfaces removed %s %s', path, interfaces)
+ async with self.watch_processing_lock:
+ notify = {path: {name: None for name in interfaces}}
+ self.send_json(notify=notify)
+
+ self.add_async_signal_handler(handler,
+ path=path,
+ interface="org.freedesktop.DBus.ObjectManager")
+ objects, = await self.bus.call_method_async(self.name, path,
+ 'org.freedesktop.DBus.ObjectManager',
+ 'GetManagedObjects')
+ for p, ifaces in objects.items():
+ for iface, props in ifaces.items():
+ if interface_name is None or iface == interface_name:
+ mm = await self.cache.get_interface_if_new(iface, self.bus, self.name, p)
+ if mm:
+ meta.update({iface: mm})
+ notify_update(notify, p, iface, props)
+
+ async def setup_path_watch(self, path, interface_name, recursive_props, meta, notify):
+ # Watch a single object at "path", but maybe also watch for
+ # property changes for all objects below "path".
+
+ async def handler(message):
+ async with self.watch_processing_lock:
+ path = message.get_path()
+ name, props, invalids = message.get_body()
+ logger.debug('NOTIFY: %s %s %s %s', path, name, props, invalids)
+ for inv in invalids:
+ try:
+ reply, = await self.bus.call_method_async(self.name, path,
+ 'org.freedesktop.DBus.Properties', 'Get',
+ 'ss', name, inv)
+ except BusError as exc:
+ logger.debug('failed to fetch property %s.%s on %s %s: %s',
+ name, inv, self.name, path, str(exc))
+ continue
+ props[inv] = reply
+ notify = {}
+ notify_update(notify, path, name, props)
+ self.send_json(notify=notify)
+
+ this_meta = await self.cache.introspect_path(self.bus, self.name, path)
+ if interface_name is not None:
+ interface = this_meta.get(interface_name)
+ this_meta = {interface_name: interface}
+ meta.update(this_meta)
+ if recursive_props:
+ self.add_async_signal_handler(handler,
+ interface="org.freedesktop.DBus.Properties",
+ path_namespace=path)
+ else:
+ self.add_async_signal_handler(handler,
+ interface="org.freedesktop.DBus.Properties",
+ path=path)
+
+ for name in meta:
+ if name.startswith("org.freedesktop.DBus."):
+ continue
+ try:
+ props, = await self.bus.call_method_async(self.name, path,
+ 'org.freedesktop.DBus.Properties',
+ 'GetAll', 's', name)
+ notify_update(notify, path, name, props)
+ except BusError:
+ pass
+
+ async def do_watch(self, message):
+ watch = message['watch']
+ path = watch.get('path')
+ path_namespace = watch.get('path_namespace')
+ interface_name = watch.get('interface')
+ cookie = message.get('id')
+
+ path = path or path_namespace
+ recursive = path == path_namespace
+
+ if path is None or cookie is None:
+ logger.debug('ignored incomplete watch request %s', message)
+ self.send_json(error=['x.y.z', ['Not Implemented']], id=cookie)
+ self.send_json(reply=[], id=cookie)
+ return
+
+ try:
+ async with self.watch_processing_lock:
+ meta = {}
+ notify = {}
+ await self.setup_path_watch(path, interface_name, recursive, meta, notify)
+ if recursive:
+ await self.setup_objectmanager_watch(path, interface_name, meta, notify)
+ self.send_json(meta=meta)
+ self.send_json(notify=notify)
+ self.send_json(reply=[], id=message['id'])
+ except BusError as error:
+ logger.debug("do_watch(%s) caught D-Bus error: %s", message, error.message)
+ self.send_json(error=[error.name, [error.message]], id=cookie)
+
+ async def do_meta(self, message):
+ self.cache.inject(message['meta'])
+
+ def do_data(self, data):
+ message = json.loads(data)
+ logger.debug('receive dbus request %s %s', self.name, message)
+
+ if 'call' in message:
+ self.create_task(self.do_call(message))
+ elif 'add-match' in message:
+ self.create_task(self.do_add_match(message))
+ elif 'watch' in message:
+ self.create_task(self.do_watch(message))
+ elif 'meta' in message:
+ self.create_task(self.do_meta(message))
+ else:
+ logger.debug('ignored dbus request %s', message)
+ return
+
+ def do_close(self):
+ for slot in self.matches:
+ slot.cancel()
+ self.matches = []
+ self.close()
diff --git a/src/cockpit/channels/filesystem.py b/src/cockpit/channels/filesystem.py
new file mode 100644
index 0000000..9df461b
--- /dev/null
+++ b/src/cockpit/channels/filesystem.py
@@ -0,0 +1,549 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import asyncio
+import contextlib
+import enum
+import errno
+import fnmatch
+import functools
+import grp
+import logging
+import os
+import pwd
+import random
+import stat
+from typing import Callable, Iterable
+
+from cockpit._vendor.systemd_ctypes import Handle, PathWatch
+from cockpit._vendor.systemd_ctypes.inotify import Event as InotifyEvent
+from cockpit._vendor.systemd_ctypes.pathwatch import Listener as PathWatchListener
+
+from ..channel import Channel, ChannelError, GeneratorChannel
+from ..jsonutil import (
+ JsonDict,
+ JsonDocument,
+ JsonError,
+ JsonObject,
+ get_bool,
+ get_int,
+ get_str,
+ get_strv,
+ json_merge_and_filter_patch,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def tag_from_stat(buf):
+ return f'1:{buf.st_ino}-{buf.st_mtime}'
+
+
+def tag_from_path(path):
+ try:
+ return tag_from_stat(os.stat(path))
+ except FileNotFoundError:
+ return '-'
+ except OSError:
+ return None
+
+
+def tag_from_fd(fd):
+ try:
+ return tag_from_stat(os.fstat(fd))
+ except OSError:
+ return None
+
+
+class FsListChannel(Channel):
+ payload = 'fslist1'
+
+ def send_entry(self, event, entry):
+ if entry.is_symlink():
+ mode = 'link'
+ elif entry.is_file():
+ mode = 'file'
+ elif entry.is_dir():
+ mode = 'directory'
+ else:
+ mode = 'special'
+
+ self.send_json(event=event, path=entry.name, type=mode)
+
+ def do_open(self, options):
+ path = options.get('path')
+ watch = options.get('watch', True)
+
+ if watch:
+ raise ChannelError('not-supported', message='watching is not implemented, use fswatch1')
+
+ try:
+ scan_dir = os.scandir(path)
+ except FileNotFoundError as error:
+ raise ChannelError('not-found', message=str(error)) from error
+ except PermissionError as error:
+ raise ChannelError('access-denied', message=str(error)) from error
+ except OSError as error:
+ raise ChannelError('internal-error', message=str(error)) from error
+
+ self.ready()
+ for entry in scan_dir:
+ self.send_entry("present", entry)
+
+ if not watch:
+ self.done()
+ self.close()
+
+
+class FsReadChannel(GeneratorChannel):
+ payload = 'fsread1'
+
+ def do_yield_data(self, options: JsonObject) -> GeneratorChannel.DataGenerator:
+ path = get_str(options, 'path')
+ binary = get_str(options, 'binary', None)
+ max_read_size = get_int(options, 'max_read_size', None)
+
+ logger.debug('Opening file "%s" for reading', path)
+
+ try:
+ with open(path, 'rb') as filep:
+ buf = os.stat(filep.fileno())
+ if max_read_size is not None and buf.st_size > max_read_size:
+ raise ChannelError('too-large')
+
+ if binary and stat.S_ISREG(buf.st_mode):
+ self.ready(size_hint=buf.st_size)
+ else:
+ self.ready()
+
+ while True:
+ data = filep.read1(Channel.BLOCK_SIZE)
+ if data == b'':
+ break
+ logger.debug(' ...sending %d bytes', len(data))
+ if not binary:
+ data = data.replace(b'\0', b'').decode('utf-8', errors='ignore').encode('utf-8')
+ yield data
+
+ return {'tag': tag_from_stat(buf)}
+
+ except FileNotFoundError:
+ return {'tag': '-'}
+ except PermissionError as exc:
+ raise ChannelError('access-denied') from exc
+ except OSError as exc:
+ raise ChannelError('internal-error', message=str(exc)) from exc
+
+
+class FsReplaceChannel(Channel):
+ payload = 'fsreplace1'
+
+ _path = None
+ _tag = None
+ _tempfile = None
+ _temppath = None
+
+ def unlink_temppath(self):
+ try:
+ os.unlink(self._temppath)
+ except OSError:
+ pass # might have been removed from outside
+
+ def do_open(self, options):
+ self._path = options.get('path')
+ self._tag = options.get('tag')
+ self.ready()
+
+ def do_data(self, data):
+ if self._tempfile is None:
+ # keep this bounded, in case anything unexpected goes wrong
+ for _ in range(10):
+ suffix = ''.join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789_", k=6))
+ self._temppath = f'{self._path}.cockpit-tmp.{suffix}'
+ try:
+ fd = os.open(self._temppath, os.O_CREAT | os.O_WRONLY | os.O_EXCL, 0o666)
+ break
+ except FileExistsError:
+ continue
+ except PermissionError as exc:
+ raise ChannelError('access-denied') from exc
+ except FileNotFoundError as exc:
+ # directory of path does not exist
+ raise ChannelError('not-found') from exc
+ except OSError as exc:
+ raise ChannelError('internal-error', message=str(exc)) from exc
+ else:
+ raise ChannelError('internal-error',
+ message=f"Could not find unique file name for replacing {self._path}")
+
+ try:
+ self._tempfile = os.fdopen(fd, 'wb')
+ except OSError:
+ # Should Not Happen™, but let's be safe and avoid fd leak
+ os.close(fd)
+ self.unlink_temppath()
+ raise
+
+ self._tempfile.write(data)
+
+ def do_done(self):
+ if self._tempfile is None:
+ try:
+ os.unlink(self._path)
+ # crash on other errors, as they are unexpected
+ except FileNotFoundError:
+ pass
+ else:
+ self._tempfile.flush()
+
+ if self._tag and self._tag != tag_from_path(self._path):
+ raise ChannelError('change-conflict')
+
+ try:
+ os.rename(self._temppath, self._path)
+ # ensure to not leave the temp file behind
+ except FileNotFoundError as exc:
+ self.unlink_temppath()
+ raise ChannelError('not-found', message=str(exc)) from exc
+ except IsADirectoryError as exc:
+ self.unlink_temppath()
+ # not ideal, but the closest code we have
+ raise ChannelError('access-denied', message=str(exc)) from exc
+ except OSError as exc:
+ self.unlink_temppath()
+ raise ChannelError('internal-error', message=str(exc)) from exc
+
+ self._tempfile.close()
+ self._tempfile = None
+
+ self.done()
+ self.close({'tag': tag_from_path(self._path)})
+
+ def do_close(self):
+ if self._tempfile is not None:
+ self._tempfile.close()
+ self.unlink_temppath()
+ self._tempfile = None
+
+
+class FsWatchChannel(Channel):
+ payload = 'fswatch1'
+ _tag = None
+ _path = None
+ _watch = None
+
+ # The C bridge doesn't send the initial event, and the JS calls read()
+ # instead to figure out the initial state of the file. If we send the
+ # initial state then we cause the event to get delivered twice.
+ # Ideally we'll sort that out at some point, but for now, suppress it.
+ _active = False
+
+ @staticmethod
+ def mask_to_event_and_type(mask):
+ if (InotifyEvent.CREATE or InotifyEvent.MOVED_TO) in mask:
+ return 'created', 'directory' if InotifyEvent.ISDIR in mask else 'file'
+ elif InotifyEvent.MOVED_FROM in mask or InotifyEvent.DELETE in mask or InotifyEvent.DELETE_SELF in mask:
+ return 'deleted', None
+ elif InotifyEvent.ATTRIB in mask:
+ return 'attribute-changed', None
+ elif InotifyEvent.CLOSE_WRITE in mask:
+ return 'done-hint', None
+ else:
+ return 'changed', None
+
+ def do_inotify_event(self, mask, _cookie, name):
+ logger.debug("do_inotify_event(%s): mask %X name %s", self._path, mask, name)
+ event, type_ = self.mask_to_event_and_type(mask)
+ if name:
+ # file inside watched directory changed
+ path = os.path.join(self._path, name.decode())
+ tag = tag_from_path(path)
+ self.send_json(event=event, path=path, tag=tag, type=type_)
+ else:
+ # the watched path itself changed; filter out duplicate events
+ tag = tag_from_path(self._path)
+ if tag == self._tag:
+ return
+ self._tag = tag
+ self.send_json(event=event, path=self._path, tag=self._tag, type=type_)
+
+ def do_identity_changed(self, fd, err):
+ logger.debug("do_identity_changed(%s): fd %s, err %s", self._path, str(fd), err)
+ self._tag = tag_from_fd(fd) if fd else '-'
+ if self._active:
+ self.send_json(event='created' if fd else 'deleted', path=self._path, tag=self._tag)
+
+ def do_open(self, options):
+ self._path = options['path']
+ self._tag = None
+
+ self._active = False
+ self._watch = PathWatch(self._path, self)
+ self._active = True
+
+ self.ready()
+
+ def do_close(self):
+ self._watch.close()
+ self._watch = None
+ self.close()
+
+
+class Follow(enum.Enum):
+ NO = False
+ YES = True
+
+
+class FsInfoChannel(Channel, PathWatchListener):
+ payload = 'fsinfo'
+
+ # Options (all get set in `do_open()`)
+ path: str
+ attrs: 'set[str]'
+ fnmatch: str
+ targets: bool
+ follow: bool
+ watch: bool
+
+ # State
+ current_value: JsonDict
+ effective_fnmatch: str = ''
+ fd: 'Handle | None' = None
+ pending: 'set[str] | None' = None
+ path_watch: 'PathWatch | None' = None
+ getattrs: 'Callable[[int, str, Follow], JsonDocument]'
+
+ @staticmethod
+ def make_getattrs(attrs: Iterable[str]) -> 'Callable[[int, str, Follow], JsonDocument | None]':
+ # Cached for the duration of the closure we're creating
+ @functools.lru_cache()
+ def get_user(uid: int) -> 'str | int':
+ try:
+ return pwd.getpwuid(uid).pw_name
+ except KeyError:
+ return uid
+
+ @functools.lru_cache()
+ def get_group(gid: int) -> 'str | int':
+ try:
+ return grp.getgrgid(gid).gr_name
+ except KeyError:
+ return gid
+
+ stat_types = {stat.S_IFREG: 'reg', stat.S_IFDIR: 'dir', stat.S_IFLNK: 'lnk', stat.S_IFCHR: 'chr',
+ stat.S_IFBLK: 'blk', stat.S_IFIFO: 'fifo', stat.S_IFSOCK: 'sock'}
+ available_stat_getters = {
+ 'type': lambda buf: stat_types.get(stat.S_IFMT(buf.st_mode)),
+ 'tag': tag_from_stat,
+ 'mode': lambda buf: stat.S_IMODE(buf.st_mode),
+ 'size': lambda buf: buf.st_size,
+ 'uid': lambda buf: buf.st_uid,
+ 'gid': lambda buf: buf.st_gid,
+ 'mtime': lambda buf: buf.st_mtime,
+ 'user': lambda buf: get_user(buf.st_uid),
+ 'group': lambda buf: get_group(buf.st_gid),
+ }
+ stat_getters = tuple((key, available_stat_getters.get(key, lambda _: None)) for key in attrs)
+
+ def get_attrs(fd: int, name: str, follow: Follow) -> 'JsonDict | None':
+ try:
+ buf = os.stat(name, follow_symlinks=follow.value, dir_fd=fd) if name else os.fstat(fd)
+ except FileNotFoundError:
+ return None
+ except OSError:
+ return {name: None for name, func in stat_getters}
+
+ result = {key: func(buf) for key, func in stat_getters}
+
+ if 'target' in result and stat.S_IFMT(buf.st_mode) == stat.S_IFLNK:
+ with contextlib.suppress(OSError):
+ result['target'] = os.readlink(name, dir_fd=fd)
+
+ return result
+
+ return get_attrs
+
+ def send_update(self, updates: JsonDict, *, reset: bool = False) -> None:
+ if reset:
+ if set(self.current_value) & set(updates):
+ # if we have an overlap, we need to do a proper reset
+ self.send_json({name: None for name in self.current_value}, partial=True)
+ self.current_value = {'partial': True}
+ updates.update(partial=None)
+ else:
+ # otherwise there's no overlap: we can just remove the old keys
+ updates.update({key: None for key in self.current_value})
+
+ json_merge_and_filter_patch(self.current_value, updates)
+ if updates:
+ self.send_json(updates)
+
+ def process_update(self, updates: 'set[str]', *, reset: bool = False) -> None:
+ assert self.fd is not None
+
+ entries: JsonDict = {name: self.getattrs(self.fd, name, Follow.NO) for name in updates}
+
+ info = entries.pop('', {})
+ assert isinstance(info, dict) # fstat() will never fail with FileNotFoundError
+
+ if self.effective_fnmatch:
+ info['entries'] = entries
+
+ if self.targets:
+ info['targets'] = targets = {}
+ for name in {e.get('target') for e in entries.values() if isinstance(e, dict)}:
+ if isinstance(name, str) and ('/' in name or not self.interesting(name)):
+ # if this target is a string that we wouldn't otherwise
+ # report, then report it via our "targets" attribute.
+ targets[name] = self.getattrs(self.fd, name, Follow.YES)
+
+ self.send_update({'info': info}, reset=reset)
+
+ def process_pending_updates(self) -> None:
+ assert self.pending is not None
+ if self.pending:
+ self.process_update(self.pending)
+ self.pending = None
+
+ def interesting(self, name: str) -> bool:
+ if name == '':
+ return True
+ else:
+ # only report updates on entry filenames if we match them
+ return fnmatch.fnmatch(name, self.effective_fnmatch)
+
+ def schedule_update(self, name: str) -> None:
+ if not self.interesting(name):
+ return
+
+ if self.pending is None:
+ asyncio.get_running_loop().call_later(0.1, self.process_pending_updates)
+ self.pending = set()
+
+ self.pending.add(name)
+
+ def report_error(self, err: int) -> None:
+ if err == errno.ENOENT:
+ problem = 'not-found'
+ elif err in (errno.EPERM, errno.EACCES):
+ problem = 'access-denied'
+ elif err == errno.ENOTDIR:
+ problem = 'not-directory'
+ else:
+ problem = 'internal-error'
+
+ self.send_update({'error': {
+ 'problem': problem, 'message': os.strerror(err), 'errno': errno.errorcode[err]
+ }}, reset=True)
+
+ def flag_onlydir_error(self, fd: Handle) -> bool:
+ # If our requested path ended with '/' then make sure we got a
+ # directory, or else it's an error. open() will have already flagged
+ # that for us, but systemd_ctypes doesn't do that (yet).
+ if not self.watch or not self.path.endswith('/'):
+ return False
+
+ buf = os.fstat(fd) # this should never fail
+ if stat.S_IFMT(buf.st_mode) != stat.S_IFDIR:
+ self.report_error(errno.ENOTDIR)
+ return True
+
+ return False
+
+ def report_initial_state(self, fd: Handle) -> None:
+ if self.flag_onlydir_error(fd):
+ return
+
+ self.fd = fd
+
+ entries = {''}
+ if self.fnmatch:
+ try:
+ entries.update(os.listdir(f'/proc/self/fd/{self.fd}'))
+ self.effective_fnmatch = self.fnmatch
+ except OSError:
+ # If we failed to get an initial list, then report nothing from now on
+ self.effective_fnmatch = ''
+
+ self.process_update({e for e in entries if self.interesting(e)}, reset=True)
+
+ def do_inotify_event(self, mask: InotifyEvent, cookie: int, rawname: 'bytes | None') -> None:
+ logger.debug('do_inotify_event(%r, %r, %r)', mask, cookie, rawname)
+ name = (rawname or b'').decode(errors='surrogateescape')
+
+ self.schedule_update(name)
+
+ if name and mask | (InotifyEvent.CREATE | InotifyEvent.DELETE |
+ InotifyEvent.MOVED_TO | InotifyEvent.MOVED_FROM):
+ # These events change the mtime of the directory
+ self.schedule_update('')
+
+ def do_identity_changed(self, fd: 'Handle | None', err: 'int | None') -> None:
+ logger.debug('do_identity_changed(%r, %r)', fd, err)
+ # If there were previously pending changes, they are now irrelevant.
+ if self.pending is not None:
+ # Note: don't set to None, since the handler is still pending
+ self.pending.clear()
+
+ if err is None:
+ assert fd is not None
+ self.report_initial_state(fd)
+ else:
+ self.report_error(err)
+
+ def do_close(self) -> None:
+ # non-watch channels close immediately — if we get this, we're watching
+ assert self.path_watch is not None
+ self.path_watch.close()
+ self.close()
+
+ def do_open(self, options: JsonObject) -> None:
+ self.path = get_str(options, 'path')
+ if not os.path.isabs(self.path):
+ raise JsonError(options, '"path" must be an absolute path')
+
+ attrs = set(get_strv(options, 'attrs'))
+ self.getattrs = self.make_getattrs(attrs - {'targets', 'entries'})
+ self.fnmatch = get_str(options, 'fnmatch', '*' if 'entries' in attrs else '')
+ self.targets = 'targets' in attrs
+ self.follow = get_bool(options, 'follow', default=True)
+ self.watch = get_bool(options, 'watch', default=False)
+ if self.watch and not self.follow:
+ raise JsonError(options, '"watch: true" and "follow: false" are (currently) incompatible')
+ if self.targets and not self.follow:
+ raise JsonError(options, '`targets: "stat"` and `follow: false` are (currently) incompatible')
+
+ self.current_value = {}
+ self.ready()
+
+ if not self.watch:
+ try:
+ fd = Handle.open(self.path, os.O_PATH if self.follow else os.O_PATH | os.O_NOFOLLOW)
+ except OSError as exc:
+ self.report_error(exc.errno)
+ else:
+ self.report_initial_state(fd)
+ fd.close()
+
+ self.done()
+ self.close()
+
+ else:
+ # PathWatch will call do_identity_changed(), which does the same as
+ # above: calls either report_initial_state() or report_error(),
+ # depending on if it was provided with an fd or an error code.
+ self.path_watch = PathWatch(self.path, self)
diff --git a/src/cockpit/channels/http.py b/src/cockpit/channels/http.py
new file mode 100644
index 0000000..f0dea1e
--- /dev/null
+++ b/src/cockpit/channels/http.py
@@ -0,0 +1,158 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import asyncio
+import http.client
+import logging
+import socket
+import ssl
+
+from ..channel import AsyncChannel, ChannelError
+from ..jsonutil import JsonObject, get_dict, get_int, get_object, get_str, typechecked
+
+logger = logging.getLogger(__name__)
+
+
+class HttpChannel(AsyncChannel):
+ payload = 'http-stream2'
+
+ @staticmethod
+ def get_headers(response: http.client.HTTPResponse, binary: 'str | None') -> JsonObject:
+ # Never send these headers
+ remove = {'Connection', 'Transfer-Encoding'}
+
+ if binary != 'raw':
+ # Only send these headers for raw binary streams
+ remove.update({'Content-Length', 'Range'})
+
+ return {key: value for key, value in response.getheaders() if key not in remove}
+
+ @staticmethod
+ def create_client(options: JsonObject) -> http.client.HTTPConnection:
+ opt_address = get_str(options, 'address', 'localhost')
+ opt_tls = get_dict(options, 'tls', None)
+ opt_unix = get_str(options, 'unix', None)
+ opt_port = get_int(options, 'port', None)
+
+ if opt_tls is not None and opt_unix is not None:
+ raise ChannelError('protocol-error', message='TLS on Unix socket is not supported')
+ if opt_port is None and opt_unix is None:
+ raise ChannelError('protocol-error', message='no "port" or "unix" option for channel')
+ if opt_port is not None and opt_unix is not None:
+ raise ChannelError('protocol-error', message='cannot specify both "port" and "unix" options')
+
+ if opt_tls is not None:
+ authority = get_dict(opt_tls, 'authority', None)
+ if authority is not None:
+ data = get_str(authority, 'data', None)
+ if data is not None:
+ context = ssl.create_default_context(cadata=data)
+ else:
+ context = ssl.create_default_context(cafile=get_str(authority, 'file'))
+ else:
+ context = ssl.create_default_context()
+
+ if 'validate' in opt_tls and not opt_tls['validate']:
+ context.check_hostname = False
+ context.verify_mode = ssl.VerifyMode.CERT_NONE
+
+ # See https://github.com/python/typeshed/issues/11057
+ return http.client.HTTPSConnection(opt_address, port=opt_port, context=context) # type: ignore[arg-type]
+
+ else:
+ return http.client.HTTPConnection(opt_address, port=opt_port)
+
+ @staticmethod
+ def connect(connection: http.client.HTTPConnection, opt_unix: 'str | None') -> None:
+ # Blocks. Runs in a thread.
+ if opt_unix:
+ # create the connection's socket so that it won't call .connect() internally (which only supports TCP)
+ connection.sock = socket.socket(socket.AF_UNIX)
+ connection.sock.connect(opt_unix)
+ else:
+ # explicitly call connect(), so that we can do proper error handling
+ connection.connect()
+
+ @staticmethod
+ def request(
+ connection: http.client.HTTPConnection, method: str, path: str, headers: 'dict[str, str]', body: bytes
+ ) -> http.client.HTTPResponse:
+ # Blocks. Runs in a thread.
+ connection.request(method, path, headers=headers or {}, body=body)
+ return connection.getresponse()
+
+ async def run(self, options: JsonObject) -> None:
+ logger.debug('open %s', options)
+
+ binary = get_str(options, 'binary', None)
+ method = get_str(options, 'method')
+ path = get_str(options, 'path')
+ headers = get_object(options, 'headers', lambda d: {k: typechecked(v, str) for k, v in d.items()}, None)
+
+ if 'connection' in options:
+ raise ChannelError('protocol-error', message='connection sharing is not implemented on this bridge')
+
+ loop = asyncio.get_running_loop()
+ connection = self.create_client(options)
+
+ self.ready()
+
+ body = b''
+ while True:
+ data = await self.read()
+ if data == b'':
+ break
+ body += data
+
+ # Connect in a thread and handle errors
+ try:
+ await loop.run_in_executor(None, self.connect, connection, get_str(options, 'unix', None))
+ except ssl.SSLCertVerificationError as exc:
+ raise ChannelError('unknown-hostkey', message=str(exc)) from exc
+ except (OSError, IOError) as exc:
+ raise ChannelError('not-found', message=str(exc)) from exc
+
+ # Submit request in a thread and handle errors
+ try:
+ response = await loop.run_in_executor(None, self.request, connection, method, path, headers or {}, body)
+ except (http.client.HTTPException, OSError) as exc:
+ raise ChannelError('terminated', message=str(exc)) from exc
+
+ self.send_control(command='response',
+ status=response.status,
+ reason=response.reason,
+ headers=self.get_headers(response, binary))
+
+ # Receive the body and finish up
+ try:
+ while True:
+ block = await loop.run_in_executor(None, response.read1, self.BLOCK_SIZE)
+ if not block:
+ break
+ await self.write(block)
+
+ logger.debug('reading response done')
+ # this returns immediately and does not read anything more, but updates the http.client's
+ # internal state machine to "response done"
+ block = response.read()
+ assert block == b''
+
+ await loop.run_in_executor(None, connection.close)
+ except (http.client.HTTPException, OSError) as exc:
+ raise ChannelError('terminated', message=str(exc)) from exc
+
+ self.done()
diff --git a/src/cockpit/channels/metrics.py b/src/cockpit/channels/metrics.py
new file mode 100644
index 0000000..ffc402e
--- /dev/null
+++ b/src/cockpit/channels/metrics.py
@@ -0,0 +1,185 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import asyncio
+import json
+import logging
+import sys
+import time
+from collections import defaultdict
+from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union
+
+from ..channel import AsyncChannel, ChannelError
+from ..jsonutil import JsonList
+from ..samples import SAMPLERS, SampleDescription, Sampler, Samples
+
+logger = logging.getLogger(__name__)
+
+
+class MetricInfo(NamedTuple):
+ derive: Optional[str]
+ desc: SampleDescription
+
+
+class InternalMetricsChannel(AsyncChannel):
+ payload = 'metrics1'
+ restrictions = [('source', 'internal')]
+
+ metrics: List[MetricInfo]
+ samplers: Set
+ samplers_cache: Optional[Dict[str, Tuple[Sampler, SampleDescription]]] = None
+
+ interval: int = 1000
+ need_meta: bool = True
+ last_timestamp: float = 0
+ next_timestamp: float = 0
+
+ @classmethod
+ def ensure_samplers(cls):
+ if cls.samplers_cache is None:
+ cls.samplers_cache = {desc.name: (sampler, desc) for sampler in SAMPLERS for desc in sampler.descriptions}
+
+ def parse_options(self, options):
+ logger.debug('metrics internal open: %s, channel: %s', options, self.channel)
+
+ interval = options.get('interval', self.interval)
+ if not isinstance(interval, int) or interval <= 0 or interval > sys.maxsize:
+ raise ChannelError('protocol-error', message=f'invalid "interval" value: {interval}')
+
+ self.interval = interval
+
+ metrics = options.get('metrics')
+ if not isinstance(metrics, list) or len(metrics) == 0:
+ logger.error('invalid "metrics" value: %s', metrics)
+ raise ChannelError('protocol-error', message='invalid "metrics" option was specified (not an array)')
+
+ sampler_classes = set()
+ for metric in metrics:
+ # validate it's an object
+ name = metric.get('name')
+ units = metric.get('units')
+ derive = metric.get('derive')
+
+ try:
+ sampler, desc = self.samplers_cache[name]
+ except KeyError as exc:
+ logger.error('unsupported metric: %s', name)
+ raise ChannelError('not-supported', message=f'unsupported metric: {name}') from exc
+
+ if units and units != desc.units:
+ raise ChannelError('not-supported', message=f'{name} has units {desc.units}, not {units}')
+
+ sampler_classes.add(sampler)
+ self.metrics.append(MetricInfo(derive=derive, desc=desc))
+
+ self.samplers = {cls() for cls in sampler_classes}
+
+ def send_meta(self, samples: Samples, timestamp: float):
+ metrics: JsonList = []
+ for metricinfo in self.metrics:
+ if metricinfo.desc.instanced:
+ metrics.append({
+ 'name': metricinfo.desc.name,
+ 'units': metricinfo.desc.units,
+ 'instances': list(samples[metricinfo.desc.name].keys()),
+ 'semantics': metricinfo.desc.semantics
+ })
+ else:
+ metrics.append({
+ 'name': metricinfo.desc.name,
+ 'derive': metricinfo.derive, # type: ignore[dict-item]
+ 'units': metricinfo.desc.units,
+ 'semantics': metricinfo.desc.semantics
+ })
+
+ self.send_json(source='internal', interval=self.interval, timestamp=timestamp * 1000, metrics=metrics)
+ self.need_meta = False
+
+ def sample(self):
+ samples = defaultdict(dict)
+ for sampler in self.samplers:
+ sampler.sample(samples)
+ return samples
+
+ def calculate_sample_rate(self, value: float, old_value: Optional[float]) -> Union[float, bool]:
+ if old_value is not None and self.last_timestamp:
+ return (value - old_value) / (self.next_timestamp - self.last_timestamp)
+ else:
+ return False
+
+ def send_updates(self, samples: Samples, last_samples: Samples):
+ data: List[Union[float, List[Optional[Union[float, bool]]]]] = []
+ timestamp = time.time()
+ self.next_timestamp = timestamp
+
+ for metricinfo in self.metrics:
+ value = samples[metricinfo.desc.name]
+
+ if metricinfo.desc.instanced:
+ old_value = last_samples[metricinfo.desc.name]
+ assert isinstance(value, dict)
+ assert isinstance(old_value, dict)
+
+ # If we have less or more keys the data changed, send a meta message.
+ if value.keys() != old_value.keys():
+ self.need_meta = True
+
+ if metricinfo.derive == 'rate':
+ instances: List[Optional[Union[float, bool]]] = []
+ for key, val in value.items():
+ instances.append(self.calculate_sample_rate(val, old_value.get(key)))
+
+ data.append(instances)
+ else:
+ data.append(list(value.values()))
+ else:
+ old_value = last_samples.get(metricinfo.desc.name)
+ assert not isinstance(value, dict)
+ assert not isinstance(old_value, dict)
+
+ if metricinfo.derive == 'rate':
+ data.append(self.calculate_sample_rate(value, old_value))
+ else:
+ data.append(value)
+
+ if self.need_meta:
+ self.send_meta(samples, timestamp)
+
+ self.last_timestamp = self.next_timestamp
+ self.send_data(json.dumps([data]).encode())
+
+ async def run(self, options):
+ self.metrics = []
+ self.samplers = set()
+
+ InternalMetricsChannel.ensure_samplers()
+
+ self.parse_options(options)
+ self.ready()
+
+ last_samples = defaultdict(dict)
+ while True:
+ samples = self.sample()
+ self.send_updates(samples, last_samples)
+ last_samples = samples
+
+ try:
+ await asyncio.wait_for(self.read(), self.interval / 1000)
+ return
+ except asyncio.TimeoutError:
+ # Continue the while loop, we use wait_for as an interval timer.
+ continue
diff --git a/src/cockpit/channels/packages.py b/src/cockpit/channels/packages.py
new file mode 100644
index 0000000..2a9e481
--- /dev/null
+++ b/src/cockpit/channels/packages.py
@@ -0,0 +1,101 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import logging
+from typing import Optional
+
+from ..channel import AsyncChannel
+from ..data import read_cockpit_data_file
+from ..jsonutil import JsonObject, get_dict, get_str
+from ..packages import Packages
+
+logger = logging.getLogger(__name__)
+
+
+class PackagesChannel(AsyncChannel):
+ payload = 'http-stream1'
+ restrictions = [("internal", "packages")]
+
+ # used to carry data forward from open to done
+ options: Optional[JsonObject] = None
+
+ def http_error(self, status: int, message: str) -> None:
+ template = read_cockpit_data_file('fail.html')
+ self.send_json(status=status, reason='ERROR', headers={'Content-Type': 'text/html; charset=utf-8'})
+ self.send_data(template.replace(b'@@message@@', message.encode('utf-8')))
+ self.done()
+ self.close()
+
+ async def run(self, options: JsonObject) -> None:
+ packages: Packages = self.router.packages # type: ignore[attr-defined] # yes, this is evil
+
+ try:
+ if get_str(options, 'method') != 'GET':
+ raise ValueError(f'Unsupported HTTP method {options["method"]}')
+
+ self.ready()
+ if await self.read() != b'':
+ raise ValueError('Received unexpected data')
+
+ path = get_str(options, 'path')
+ headers = get_dict(options, 'headers')
+ document = packages.load_path(path, headers)
+
+ # Note: we can't cache documents right now. See
+ # https://github.com/cockpit-project/cockpit/issues/19071
+ # for future plans.
+ out_headers = {
+ 'Cache-Control': 'no-cache, no-store',
+ 'Content-Type': document.content_type,
+ }
+
+ if document.content_encoding is not None:
+ out_headers['Content-Encoding'] = document.content_encoding
+
+ if document.content_security_policy is not None:
+ policy = document.content_security_policy
+
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src
+ #
+ # Note: connect-src 'self' does not resolve to websocket
+ # schemes in all browsers, more info in this issue.
+ #
+ # https://github.com/w3c/webappsec-csp/issues/7
+ if "connect-src 'self';" in policy:
+ protocol = headers.get('X-Forwarded-Proto')
+ host = headers.get('X-Forwarded-Host')
+ if not isinstance(protocol, str) or not isinstance(host, str):
+ raise ValueError('Invalid host or protocol header')
+
+ websocket_scheme = "wss" if protocol == "https" else "ws"
+ websocket_origin = f"{websocket_scheme}://{host}"
+ policy = policy.replace("connect-src 'self';", f"connect-src {websocket_origin} 'self';")
+
+ out_headers['Content-Security-Policy'] = policy
+
+ except ValueError as exc:
+ self.http_error(400, str(exc))
+
+ except KeyError:
+ self.http_error(404, 'Not found')
+
+ except OSError as exc:
+ self.http_error(500, f'Internal error: {exc!s}')
+
+ else:
+ self.send_json(status=200, reason='OK', headers=out_headers)
+ await self.sendfile(document.data)
diff --git a/src/cockpit/channels/stream.py b/src/cockpit/channels/stream.py
new file mode 100644
index 0000000..6bb27f5
--- /dev/null
+++ b/src/cockpit/channels/stream.py
@@ -0,0 +1,120 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import asyncio
+import logging
+import os
+import subprocess
+from typing import Dict
+
+from ..channel import ChannelError, ProtocolChannel
+from ..jsonutil import JsonDict, JsonObject, get_bool, get_int, get_object, get_str, get_strv
+from ..transports import SubprocessProtocol, SubprocessTransport, WindowSize
+
+logger = logging.getLogger(__name__)
+
+
+class SocketStreamChannel(ProtocolChannel):
+ payload = 'stream'
+
+ async def create_transport(self, loop: asyncio.AbstractEventLoop, options: JsonObject) -> asyncio.Transport:
+ if 'unix' in options and 'port' in options:
+ raise ChannelError('protocol-error', message='cannot specify both "port" and "unix" options')
+
+ try:
+ # Unix
+ if 'unix' in options:
+ path = get_str(options, 'unix')
+ label = f'Unix socket {path}'
+ transport, _ = await loop.create_unix_connection(lambda: self, path)
+
+ # TCP
+ elif 'port' in options:
+ port = get_int(options, 'port')
+ host = get_str(options, 'address', 'localhost')
+ label = f'TCP socket {host}:{port}'
+
+ transport, _ = await loop.create_connection(lambda: self, host, port)
+ else:
+ raise ChannelError('protocol-error',
+ message='no "port" or "unix" or other address option for channel')
+
+ logger.debug('SocketStreamChannel: connected to %s', label)
+ except OSError as error:
+ logger.info('SocketStreamChannel: connecting to %s failed: %s', label, error)
+ if isinstance(error, ConnectionRefusedError):
+ problem = 'not-found'
+ else:
+ problem = 'terminated'
+ raise ChannelError(problem, message=str(error)) from error
+ self.close_on_eof()
+ assert isinstance(transport, asyncio.Transport)
+ return transport
+
+
+class SubprocessStreamChannel(ProtocolChannel, SubprocessProtocol):
+ payload = 'stream'
+ restrictions = (('spawn', None),)
+
+ def process_exited(self) -> None:
+ self.close_on_eof()
+
+ def _get_close_args(self) -> JsonObject:
+ assert isinstance(self._transport, SubprocessTransport)
+ args: JsonDict = {'exit-status': self._transport.get_returncode()}
+ stderr = self._transport.get_stderr()
+ if stderr is not None:
+ args['message'] = stderr
+ return args
+
+ def do_options(self, options):
+ window = get_object(options, 'window', WindowSize, None)
+ if window is not None:
+ self._transport.set_window_size(window)
+
+ async def create_transport(self, loop: asyncio.AbstractEventLoop, options: JsonObject) -> SubprocessTransport:
+ args = get_strv(options, 'spawn')
+ err = get_str(options, 'err', 'msg')
+ cwd = get_str(options, 'directory', '.')
+ pty = get_bool(options, 'pty', default=False)
+ window = get_object(options, 'window', WindowSize, None)
+ environ = get_strv(options, 'environ', [])
+
+ if err == 'out':
+ stderr = subprocess.STDOUT
+ elif err == 'ignore':
+ stderr = subprocess.DEVNULL
+ else:
+ stderr = subprocess.PIPE
+
+ env: Dict[str, str] = dict(os.environ)
+ try:
+ env.update(dict(e.split('=', 1) for e in environ))
+ except ValueError:
+ raise ChannelError('protocol-error', message='invalid "environ" option for stream channel') from None
+
+ try:
+ transport = SubprocessTransport(loop, self, args, pty=pty, window=window, env=env, cwd=cwd, stderr=stderr)
+ logger.debug('Spawned process args=%s pid=%i', args, transport.get_pid())
+ return transport
+ except FileNotFoundError as error:
+ raise ChannelError('not-found') from error
+ except PermissionError as error:
+ raise ChannelError('access-denied') from error
+ except OSError as error:
+ logger.info("Failed to spawn %s: %s", args, str(error))
+ raise ChannelError('internal-error') from error
diff --git a/src/cockpit/channels/trivial.py b/src/cockpit/channels/trivial.py
new file mode 100644
index 0000000..221dcbc
--- /dev/null
+++ b/src/cockpit/channels/trivial.py
@@ -0,0 +1,46 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import logging
+
+from ..channel import Channel
+
+logger = logging.getLogger(__name__)
+
+
+class EchoChannel(Channel):
+ payload = 'echo'
+
+ def do_open(self, options):
+ self.ready()
+
+ def do_data(self, data):
+ self.send_data(data)
+
+ def do_done(self):
+ self.done()
+ self.close()
+
+
+class NullChannel(Channel):
+ payload = 'null'
+
+ def do_open(self, options):
+ self.ready()
+
+ def do_close(self):
+ self.close()
diff --git a/src/cockpit/config.py b/src/cockpit/config.py
new file mode 100644
index 0000000..04d017e
--- /dev/null
+++ b/src/cockpit/config.py
@@ -0,0 +1,89 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2023 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import configparser
+import logging
+import os
+from pathlib import Path
+
+from cockpit._vendor.systemd_ctypes import bus
+
+logger = logging.getLogger(__name__)
+
+XDG_CONFIG_HOME = Path(os.getenv('XDG_CONFIG_HOME') or os.path.expanduser('~/.config'))
+DOT_CONFIG_COCKPIT = XDG_CONFIG_HOME / 'cockpit'
+
+
+def lookup_config(filename: str) -> Path:
+ config_dirs = os.environ.get('XDG_CONFIG_DIRS', '/etc').split(':')
+ fallback = None
+ for config_dir in config_dirs:
+ config_path = Path(config_dir, 'cockpit', filename)
+ if not fallback:
+ fallback = config_path
+ if config_path.exists():
+ logger.debug('lookup_config(%s): found %s', filename, config_path)
+ return config_path
+
+ # default to the first entry in XDG_CONFIG_DIRS; that's not according to the spec,
+ # but what Cockpit has done for years
+ logger.debug('lookup_config(%s): defaulting to %s', filename, fallback)
+ assert fallback # mypy; config_dirs always has at least one string
+ return fallback
+
+
+class Config(bus.Object, interface='cockpit.Config'):
+ def __init__(self):
+ self.reload()
+
+ @bus.Interface.Method(out_types='s', in_types='ss')
+ def get_string(self, section, key):
+ try:
+ return self.config[section][key]
+ except KeyError as exc:
+ raise bus.BusError('cockpit.Config.KeyError', f'key {key} in section {section} does not exist') from exc
+
+ @bus.Interface.Method(out_types='u', in_types='ssuuu')
+ def get_u_int(self, section, key, default, maximum, minimum):
+ try:
+ value = self.config[section][key]
+ except KeyError:
+ return default
+
+ try:
+ int_val = int(value)
+ except ValueError:
+ logger.warning('cockpit.conf: [%s] %s is not an integer', section, key)
+ return default
+
+ return min(max(int_val, minimum), maximum)
+
+ @bus.Interface.Method()
+ def reload(self):
+ self.config = configparser.ConfigParser(interpolation=None)
+ cockpit_conf = lookup_config('cockpit.conf')
+ logger.debug("cockpit.Config: loading %s", cockpit_conf)
+ # this may not exist, but it's ok to not have a config file and thus leave self.config empty
+ self.config.read(cockpit_conf)
+
+
+class Environment(bus.Object, interface='cockpit.Environment'):
+ variables = bus.Interface.Property('a{ss}')
+
+ @variables.getter
+ def get_variables(self):
+ return os.environ.copy()
diff --git a/src/cockpit/data/__init__.py b/src/cockpit/data/__init__.py
new file mode 100644
index 0000000..e0ac720
--- /dev/null
+++ b/src/cockpit/data/__init__.py
@@ -0,0 +1,18 @@
+import sys
+
+if sys.version_info >= (3, 11):
+ import importlib.resources
+
+ def read_cockpit_data_file(filename: str) -> bytes:
+ return (importlib.resources.files('cockpit.data') / filename).read_bytes()
+
+else:
+ import importlib.abc
+
+ def read_cockpit_data_file(filename: str) -> bytes:
+ # https://github.com/python/mypy/issues/4182
+ loader = __loader__ # type: ignore[name-defined]
+ assert isinstance(loader, importlib.abc.ResourceLoader)
+
+ path = __file__.replace('__init__.py', filename)
+ return loader.get_data(path)
diff --git a/src/cockpit/data/fail.html b/src/cockpit/data/fail.html
new file mode 100644
index 0000000..092f2f8
--- /dev/null
+++ b/src/cockpit/data/fail.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>@@message@@</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <style>
+ body {
+ margin: 0;
+ font-family: "RedHatDisplay", "Open Sans", Helvetica, Arial, sans-serif;
+ font-size: 12px;
+ line-height: 1.66666667;
+ color: #333333;
+ background-color: #f5f5f5;
+ }
+ img {
+ border: 0;
+ vertical-align: middle;
+ }
+ h1 {
+ font-weight: 300;
+ }
+ p {
+ margin: 0 0 10px;
+ }
+ @font-face {
+ font-family: 'RedHatDisplay';
+ font-style: normal;
+ font-weight: 300;
+ src: url('/cockpit/static/fonts/RedHatDisplay-Medium.woff2') format('woff2');
+ }
+ .blank-slate-pf {
+ text-align: center;
+ padding: 90px 120px;
+ }
+ </style>
+</head>
+<body>
+ <div class="blank-slate-pf">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAWGSURBVGiBzZtNaJxFGMd/z2R3YxHbtZa2pqdcPInS9CCUov3QiuhF2hIUjOZrd5NA9VoPuiJSTx6S0s27SVxJ8aOpPSgqpbZVlIIIJirqtRdNpBU3FUqhm53HQzZpPjfvm51J8oeFZd5n5v/8eWbemXfmGcETBgcHt1prm1S1CWgSkUZV3QIkgS0Vs5vAZOV3DRhV1VFr7Wh3d3fRh1/isrFcLrerrq7uKHBMVffW2P4fqjocj8fPtLe3jzty0Y3gfD7/pLX2hIjsB4yLNufAAleste90dXV9W2tjNQkOgmCfqr4lIgdrdSQMROSqtfbNTCZzedVtrKbSwMDAjnK5nBOR51dLXAtU9Yt4PJ5eTVePLDgIgmPAaWBb1LqOURSRV1Op1JkolUILHhkZSRSLxUHgpciu+cX7QCadTpfCGIcSPDQ0dF+pVDonIk/X5Jo/XAaOpNPpmysZrig4l8vtMsZcAB524ZlH/AI8k06nJ6oZVZ1Cent7NxtjvmTjiwV4FLhYKBSS1YyWjXBlzH4FHKrRkY82bdqUamlpuVXNaHh4+N7bt2/ngRdr5LsIPLfcmF42wsVisZ/axaKqV1YSC9DS0nJLVa/UygccZnoWWRJLCu7v728GWh2QIyKJCLb3uOAEOirT5yIsEhwEwTYR6XVEDFAfwdaVYIDTAwMDOxYWLhIsIjlgu0Pi0CKi9IYQ2Gat7VtYOE9wZW181CFpJBHWWpcRBjiWz+efmFswT7CIvO2YEFUN3aWNMVG6fyhYa9+YxzHzJwiCA6q63zUhEcawhwgjIgfnRnluhF93TVZBaBE+IgxgrT0xywEQBMGDwAEfZKq6nmMYABF5amhoqAHuRrgZqPNBFiVqviIMmFKpdAQqglW12RNRpJeWrwgDGGOaAczg4OBWEXnMF1GU1ZPHCKOqewuFQtJYa5twvHu5gCj0GI7SG1YBuXPnzm5jrd3jkSSSCIdr6eXa32NEZLdnktAiPEcYVd1tRKTRJwnr9/GwFBqNqlbdIXCAKIK9Rhi43zB91uMTGynCScPdgy1f2HARVs8kG0mwNUwfVfrERurSk4bpM1qfCCVYVQVwueOxFCY3TIT7+voSeFzxVVA0TJ+8+4TJZrOxEHa+xy+qes0Ao76Jdu7cueLiJpFIPOTbDxEZi6nqqIjfniQiP+bz+Z9VdWoZkzjQ5NWJaYzGYrHYWLlcVvyOn6Sn/bIo0EQiMWY6Ojr+BX5YZ2fWAldbW1snDYCInF1vb9YAZwFiAOVy+VNjzHu4z8ABQETax8fHP8hms3ap56oq+Xz+BeBDH/xAGTgPFYFdXV1/Ad94IqOaWAAR0YmJiU988QOXZw7KZyMqIu/6YqsmNorNamGtPTnzf1ZwKpW6BHzni3S9ICJX5ya0eT9bWm+IyNJnSzAb5c/X1COPEJHPOjs752UVLHorx+PxDuD6mnnlD/+USqXMwsJFgtva2m6o6mtr45M/iEhPT0/P3wvLl5x3M5nMxyIy7Io8m82uOL+HsYmAQiqVGlnqwbIkyWSy01FWDQ0NDa9UE6Sq0tDQ4Crz4BKQXu5h1Q+G3t7ezfX19d8Djzhyxjd+TyQS+1pbW5fd1KjajY4fP/6fMeZZ4DfnrrnHr7FY7HA1sRBi7dzZ2flnLBbbC1xw5pp7XAIeD5M/HTV9OA+8XItnHjAIdDtNH56LDZQgfl1EulOp1PkolVa1y5HL5baLSP96XQEAzsXj8Z62trYbUSvWfMkDyOIgCTUM1u2Sx0IEQXBARE6o6iHcbyKUga9V9WQmk6n5a87pxt2pU6ceiMfjR0SkxcFFrZ9E5IyqjqyU5R4F3nYqC4VCcmpqavYqnrW2UUSSTB/PzhzRzlzDK1K5igeMJhKJsZXm09XifyIP5eSF+BKpAAAAAElFTkSuQmC">
+ <h1>@@message@@</h1>
+ </div>
+</body>
+</html>
diff --git a/src/cockpit/internal_endpoints.py b/src/cockpit/internal_endpoints.py
new file mode 100644
index 0000000..6bc27d5
--- /dev/null
+++ b/src/cockpit/internal_endpoints.py
@@ -0,0 +1,157 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import asyncio
+import glob
+import grp
+import json
+import logging
+import os
+import pwd
+from pathlib import Path
+from typing import Dict, Optional
+
+from cockpit._vendor.systemd_ctypes import Variant, bus, inotify, pathwatch
+
+from . import config
+
+logger = logging.getLogger(__name__)
+
+
+class cockpit_LoginMessages(bus.Object):
+ messages: Optional[str] = None
+
+ def __init__(self):
+ fdstr = os.environ.pop('COCKPIT_LOGIN_MESSAGES_MEMFD', None)
+ if fdstr is None:
+ logger.debug("COCKPIT_LOGIN_MESSAGES_MEMFD wasn't set. No login messages today.")
+ return
+
+ logger.debug("Trying to read login messages from fd %s", fdstr)
+ try:
+ with open(int(fdstr), 'r') as login_messages:
+ login_messages.seek(0)
+ self.messages = login_messages.read()
+ except (ValueError, OSError, UnicodeDecodeError) as exc:
+ # ValueError - the envvar wasn't an int
+ # OSError - the fd wasn't open, or other read failure
+ # UnicodeDecodeError - didn't contain utf-8
+ # For all of these, we simply failed to get the message.
+ logger.debug("Reading login messages failed: %s", exc)
+ else:
+ logger.debug("Successfully read login messages: %s", self.messages)
+
+ @bus.Interface.Method(out_types=['s'])
+ def get(self):
+ return self.messages or '{}'
+
+ @bus.Interface.Method(out_types=[])
+ def dismiss(self):
+ self.messages = None
+
+
+class cockpit_Machines(bus.Object):
+ path: Path
+ watch: pathwatch.PathWatch
+ pending_notify: Optional[asyncio.Handle]
+
+ # D-Bus implementation
+ machines = bus.Interface.Property('a{sa{sv}}')
+
+ @machines.getter
+ def get_machines(self) -> Dict[str, Dict[str, Variant]]:
+ results: Dict[str, Dict[str, Variant]] = {}
+
+ for filename in glob.glob(f'{self.path}/*.json'):
+ with open(filename) as fp:
+ try:
+ contents = json.load(fp)
+ except json.JSONDecodeError:
+ logger.warning('Invalid JSON in file %s. Ignoring.', filename)
+ continue
+ # merge
+ for hostname, attrs in contents.items():
+ results[hostname] = {key: Variant(value) for key, value in attrs.items()}
+
+ return results
+
+ @bus.Interface.Method(in_types=['s', 's', 'a{sv}'])
+ def update(self, filename: str, hostname: str, attrs: Dict[str, Variant]) -> None:
+ try:
+ with self.path.joinpath(filename).open() as fp:
+ contents = json.load(fp)
+ except json.JSONDecodeError as exc:
+ # Refuse to replace corrupted file
+ raise bus.BusError('cockpit.Machines.Error', f'File {filename} is in invalid format: {exc}.') from exc
+ except FileNotFoundError:
+ # But an empty file is an expected case
+ contents = {}
+
+ contents.setdefault(hostname, {}).update({key: value.value for key, value in attrs.items()})
+
+ self.path.mkdir(parents=True, exist_ok=True)
+ with open(self.path.joinpath(filename), 'w') as fp:
+ json.dump(contents, fp, indent=2)
+
+ def notify(self):
+ def _notify_now():
+ self.properties_changed('cockpit.Machines', {}, ['Machines'])
+ self.pending_notify = None
+
+ # avoid a flurry of update notifications
+ if self.pending_notify is None:
+ self.pending_notify = asyncio.get_running_loop().call_later(1.0, _notify_now)
+
+ # inotify events
+ def do_inotify_event(self, mask: inotify.Event, cookie: int, name: Optional[str]) -> None:
+ self.notify()
+
+ def do_identity_changed(self, fd: Optional[int], errno: Optional[int]) -> None:
+ self.notify()
+
+ def __init__(self):
+ self.path = config.lookup_config('machines.d')
+
+ # ignore the first callback
+ self.pending_notify = ...
+ self.watch = pathwatch.PathWatch(str(self.path), self)
+ self.pending_notify = None
+
+
+class cockpit_User(bus.Object):
+ name = bus.Interface.Property('s', value='')
+ full = bus.Interface.Property('s', value='')
+ id = bus.Interface.Property('i', value=0)
+ home = bus.Interface.Property('s', value='')
+ shell = bus.Interface.Property('s', value='')
+ groups = bus.Interface.Property('as', value=[])
+
+ def __init__(self):
+ user = pwd.getpwuid(os.getuid())
+ self.name = user.pw_name
+ self.full = user.pw_gecos
+ self.id = user.pw_uid
+ self.home = user.pw_dir
+ self.shell = user.pw_shell
+ self.groups = [gr.gr_name for gr in grp.getgrall() if user.pw_name in gr.gr_mem]
+
+
+EXPORTS = [
+ ('/LoginMessages', cockpit_LoginMessages),
+ ('/machines', cockpit_Machines),
+ ('/user', cockpit_User),
+]
diff --git a/src/cockpit/jsonutil.py b/src/cockpit/jsonutil.py
new file mode 100644
index 0000000..0c4db3a
--- /dev/null
+++ b/src/cockpit/jsonutil.py
@@ -0,0 +1,180 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2023 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+from enum import Enum
+from typing import Callable, Dict, List, Mapping, Optional, Sequence, Type, TypeVar, Union
+
+JsonLiteral = Union[str, float, bool, None]
+
+# immutable
+JsonValue = Union['JsonObject', Sequence['JsonValue'], JsonLiteral]
+JsonObject = Mapping[str, JsonValue]
+
+# mutable
+JsonDocument = Union['JsonDict', 'JsonList', JsonLiteral]
+JsonDict = Dict[str, JsonDocument]
+JsonList = List[JsonDocument]
+
+
+DT = TypeVar('DT')
+T = TypeVar('T')
+
+
+class JsonError(Exception):
+ value: object
+
+ def __init__(self, value: object, msg: str):
+ super().__init__(msg)
+ self.value = value
+
+
+def typechecked(value: JsonValue, expected_type: Type[T]) -> T:
+ """Ensure a JSON value has the expected type, returning it if so."""
+ if not isinstance(value, expected_type):
+ raise JsonError(value, f'must have type {expected_type.__name__}')
+ return value
+
+
+# We can't use None as a sentinel because it's often the actual default value
+# EllipsisType is difficult because it's not available before 3.10.
+# See https://peps.python.org/pep-0484/#support-for-singleton-types-in-unions
+class _Empty(Enum):
+ TOKEN = 0
+
+
+_empty = _Empty.TOKEN
+
+
+def _get(obj: JsonObject, cast: Callable[[JsonValue], T], key: str, default: Union[DT, _Empty]) -> Union[T, DT]:
+ try:
+ return cast(obj[key])
+ except KeyError:
+ if default is not _empty:
+ return default
+ raise JsonError(obj, f"attribute '{key}' required") from None
+ except JsonError as exc:
+ target = f"attribute '{key}'" + (' elements:' if exc.value is not obj[key] else ':')
+ raise JsonError(obj, f"{target} {exc!s}") from exc
+
+
+def get_bool(obj: JsonObject, key: str, default: Union[DT, _Empty] = _empty) -> Union[DT, bool]:
+ return _get(obj, lambda v: typechecked(v, bool), key, default)
+
+
+def get_int(obj: JsonObject, key: str, default: Union[DT, _Empty] = _empty) -> Union[DT, int]:
+ return _get(obj, lambda v: typechecked(v, int), key, default)
+
+
+def get_str(obj: JsonObject, key: str, default: Union[DT, _Empty] = _empty) -> Union[DT, str]:
+ return _get(obj, lambda v: typechecked(v, str), key, default)
+
+
+def get_str_or_none(obj: JsonObject, key: str, default: Optional[str]) -> Optional[str]:
+ return _get(obj, lambda v: None if v is None else typechecked(v, str), key, default)
+
+
+def get_dict(obj: JsonObject, key: str, default: Union[DT, _Empty] = _empty) -> Union[DT, JsonObject]:
+ return _get(obj, lambda v: typechecked(v, dict), key, default)
+
+
+def get_object(
+ obj: JsonObject,
+ key: str,
+ constructor: Callable[[JsonObject], T],
+ default: Union[DT, _Empty] = _empty
+) -> Union[DT, T]:
+ return _get(obj, lambda v: constructor(typechecked(v, dict)), key, default)
+
+
+def get_strv(obj: JsonObject, key: str, default: Union[DT, _Empty] = _empty) -> Union[DT, Sequence[str]]:
+ def as_strv(value: JsonValue) -> Sequence[str]:
+ return tuple(typechecked(item, str) for item in typechecked(value, list))
+ return _get(obj, as_strv, key, default)
+
+
+def get_objv(obj: JsonObject, key: str, constructor: Callable[[JsonObject], T]) -> Union[DT, Sequence[T]]:
+ def as_objv(value: JsonValue) -> Sequence[T]:
+ return tuple(constructor(typechecked(item, dict)) for item in typechecked(value, list))
+ return _get(obj, as_objv, key, ())
+
+
+def create_object(message: 'JsonObject | None', kwargs: JsonObject) -> JsonObject:
+ """Constructs a JSON object based on message and kwargs.
+
+ If only message is given, it is returned, unmodified. If message is None,
+ it is equivalent to an empty dictionary. A copy is always made.
+
+ If kwargs are present, then any underscore ('_') present in a key name is
+ rewritten to a dash ('-'). This is intended to bridge between the required
+ Python syntax when providing kwargs and idiomatic JSON (which uses '-' for
+ attributes). These values override values in message.
+
+ The idea is that `message` should be used for passing data along, and
+ kwargs used for data originating at a given call site, possibly including
+ modifications to an original message.
+ """
+ result = dict(message or {})
+
+ for key, value in kwargs.items():
+ # rewrite '_' (necessary in Python syntax kwargs list) to '-' (idiomatic JSON)
+ json_key = key.replace('_', '-')
+ result[json_key] = value
+
+ return result
+
+
+def json_merge_patch(current: JsonObject, patch: JsonObject) -> JsonObject:
+ """Perform a JSON merge patch (RFC 7396) using 'current' and 'patch'.
+ Neither of the original dictionaries is modified — the result is returned.
+ """
+ # Always take a copy ('result') — we never modify the input ('current')
+ result = dict(current)
+ for key, patch_value in patch.items():
+ if isinstance(patch_value, Mapping):
+ current_value = current.get(key, None)
+ if not isinstance(current_value, Mapping):
+ current_value = {}
+ result[key] = json_merge_patch(current_value, patch_value)
+ elif patch_value is not None:
+ result[key] = patch_value
+ else:
+ result.pop(key, None)
+
+ return result
+
+
+def json_merge_and_filter_patch(current: JsonDict, patch: JsonDict) -> None:
+ """Perform a JSON merge patch (RFC 7396) modifying 'current' with 'patch'.
+ Also modifies 'patch' to remove redundant operations.
+ """
+ for key, patch_value in tuple(patch.items()):
+ current_value = current.get(key, None)
+
+ if isinstance(patch_value, dict):
+ if not isinstance(current_value, dict):
+ current[key] = current_value = {}
+ json_merge_and_filter_patch(current_value, patch_value)
+ else:
+ json_merge_and_filter_patch(current_value, patch_value)
+ if not patch_value:
+ del patch[key]
+ elif current_value == patch_value:
+ del patch[key]
+ elif patch_value is not None:
+ current[key] = patch_value
+ else:
+ del current[key]
diff --git a/src/cockpit/packages.py b/src/cockpit/packages.py
new file mode 100644
index 0000000..c9f2525
--- /dev/null
+++ b/src/cockpit/packages.py
@@ -0,0 +1,580 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import collections
+import contextlib
+import functools
+import gzip
+import io
+import itertools
+import json
+import logging
+import mimetypes
+import os
+import re
+import shutil
+from pathlib import Path
+from typing import (
+ BinaryIO,
+ Callable,
+ ClassVar,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ Pattern,
+ Sequence,
+ Tuple,
+ TypeVar,
+)
+
+from cockpit._vendor.systemd_ctypes import bus
+
+from . import config
+from ._version import __version__
+from .jsonutil import (
+ JsonError,
+ JsonObject,
+ JsonValue,
+ get_bool,
+ get_dict,
+ get_int,
+ get_objv,
+ get_str,
+ get_strv,
+ json_merge_patch,
+ typechecked,
+)
+
+logger = logging.getLogger(__name__)
+
+
+# In practice, this is going to get called over and over again with exactly the
+# same list. Let's try to cache the result.
+@functools.lru_cache()
+def parse_accept_language(accept_language: str) -> Sequence[str]:
+ """Parse the Accept-Language header, if it exists.
+
+ Returns an ordered list of languages, with fallbacks inserted, and
+ truncated to the position where 'en' would have otherwise appeared, if
+ applicable.
+
+ https://tools.ietf.org/html/rfc7231#section-5.3.5
+ https://datatracker.ietf.org/doc/html/rfc4647#section-3.4
+ """
+
+ logger.debug('parse_accept_language(%r)', accept_language)
+ locales_with_q = []
+ for entry in accept_language.split(','):
+ entry = entry.strip().lower()
+ logger.debug(' entry %r', entry)
+ locale, _, qstr = entry.partition(';q=')
+ try:
+ q = float(qstr or 1.0)
+ except ValueError:
+ continue # ignore malformed entry
+
+ while locale:
+ logger.debug(' adding %r q=%r', locale, q)
+ locales_with_q.append((locale, q))
+ # strip off '-detail' suffixes until there's nothing left
+ locale, _, _region = locale.rpartition('-')
+
+ # Sort the list by highest q value. Otherwise, this is a stable sort.
+ locales_with_q.sort(key=lambda pair: pair[1], reverse=True)
+ logger.debug(' sorted list is %r', locales_with_q)
+
+ # If we have 'en' anywhere in our list, ignore it and all items after it.
+ # This will result in us getting an untranslated (ie: English) version if
+ # none of the more-preferred languages are found, which is what we want.
+ # We also take the chance to drop duplicate items. Note: both of these
+ # things need to happen after sorting.
+ results = []
+ for locale, _q in locales_with_q:
+ if locale == 'en':
+ break
+ if locale not in results:
+ results.append(locale)
+
+ logger.debug(' results list is %r', results)
+ return tuple(results)
+
+
+def sortify_version(version: str) -> str:
+ """Convert a version string to a form that can be compared"""
+ # 0-pad each numeric component. Only supports numeric versions like 1.2.3.
+ return '.'.join(part.zfill(8) for part in version.split('.'))
+
+
+@functools.lru_cache()
+def get_libexecdir() -> str:
+ """Detect libexecdir on current machine
+
+ This only works for systems which have cockpit-ws installed.
+ """
+ for candidate in ['/usr/local/libexec', '/usr/libexec', '/usr/local/lib/cockpit', '/usr/lib/cockpit']:
+ if os.path.exists(os.path.join(candidate, 'cockpit-askpass')):
+ return candidate
+ else:
+ logger.warning('Could not detect libexecdir')
+ # give readable error messages
+ return '/nonexistent/libexec'
+
+
+# HACK: Type narrowing over Union types is not supported in the general case,
+# but this works for the case we care about: knowing that when we pass in an
+# JsonObject, we'll get an JsonObject back.
+J = TypeVar('J', JsonObject, JsonValue)
+
+
+def patch_libexecdir(obj: J) -> J:
+ if isinstance(obj, str):
+ if '${libexecdir}/cockpit-askpass' in obj:
+ # extra-special case: we handle this internally
+ abs_askpass = shutil.which('cockpit-askpass')
+ if abs_askpass is not None:
+ return obj.replace('${libexecdir}/cockpit-askpass', abs_askpass)
+ return obj.replace('${libexecdir}', get_libexecdir())
+ elif isinstance(obj, dict):
+ return {key: patch_libexecdir(value) for key, value in obj.items()}
+ elif isinstance(obj, list):
+ return [patch_libexecdir(item) for item in obj]
+ else:
+ return obj
+
+
+# A document is a binary stream with a Content-Type, optional Content-Encoding,
+# and optional Content-Security-Policy
+class Document(NamedTuple):
+ data: BinaryIO
+ content_type: str
+ content_encoding: Optional[str] = None
+ content_security_policy: Optional[str] = None
+
+
+class PackagesListener:
+ def packages_loaded(self) -> None:
+ """Called when the packages have been reloaded"""
+
+
+class BridgeConfig(dict, JsonObject):
+ def __init__(self, value: JsonObject):
+ super().__init__(value)
+
+ self.label = get_str(self, 'label', None)
+
+ self.privileged = get_bool(self, 'privileged', default=False)
+ self.match = get_dict(self, 'match', {})
+ if not self.privileged and not self.match:
+ raise JsonError(value, 'must have match rules or be privileged')
+
+ self.environ = get_strv(self, 'environ', ())
+ self.spawn = get_strv(self, 'spawn')
+ if not self.spawn:
+ raise JsonError(value, 'spawn vector must be non-empty')
+
+ self.name = self.label or self.spawn[0]
+
+
+class Condition:
+ def __init__(self, value: JsonObject):
+ try:
+ (self.name, self.value), = value.items()
+ except ValueError as exc:
+ raise JsonError(value, 'must contain exactly one key/value pair') from exc
+
+
+class Manifest(dict, JsonObject):
+ # Skip version check when running out of the git checkout (__version__ is None)
+ COCKPIT_VERSION = __version__ and sortify_version(__version__)
+
+ def __init__(self, path: Path, value: JsonObject):
+ super().__init__(value)
+ self.path = path
+ self.name = get_str(self, 'name', self.path.name)
+ self.bridges = get_objv(self, 'bridges', BridgeConfig)
+ self.priority = get_int(self, 'priority', 1)
+ self.csp = get_str(self, 'content-security-policy', '')
+ self.conditions = get_objv(self, 'conditions', Condition)
+
+ # Skip version check when running out of the git checkout (COCKPIT_VERSION is None)
+ if self.COCKPIT_VERSION is not None:
+ requires = get_dict(self, 'requires', {})
+ for name, version in requires.items():
+ if name != 'cockpit':
+ raise JsonError(name, 'non-cockpit requirement listed')
+ if sortify_version(typechecked(version, str)) > self.COCKPIT_VERSION:
+ raise JsonError(version, f'required cockpit version ({version}) not met')
+
+
+class Package:
+ # For po{,.manifest}.js files, the interesting part is the locale name
+ PO_JS_RE: ClassVar[Pattern] = re.compile(r'(po|po\.manifest)\.([^.]+)\.js(\.gz)?')
+
+ # immutable after __init__
+ manifest: Manifest
+ name: str
+ path: Path
+ priority: int
+
+ # computed later
+ translations: Optional[Dict[str, Dict[str, str]]] = None
+ files: Optional[Dict[str, str]] = None
+
+ def __init__(self, manifest: Manifest):
+ self.manifest = manifest
+ self.name = manifest.name
+ self.path = manifest.path
+ self.priority = manifest.priority
+
+ def ensure_scanned(self) -> None:
+ """Ensure that the package has been scanned.
+
+ This allows us to defer scanning the files of the package until we know
+ that we'll actually use it.
+ """
+
+ if self.files is not None:
+ return
+
+ self.files = {}
+ self.translations = {'po.js': {}, 'po.manifest.js': {}}
+
+ for file in self.path.rglob('*'):
+ name = str(file.relative_to(self.path))
+ if name in ['.', '..', 'manifest.json']:
+ continue
+
+ po_match = Package.PO_JS_RE.fullmatch(name)
+ if po_match:
+ basename = po_match.group(1)
+ locale = po_match.group(2)
+ # Accept-Language is case-insensitive and uses '-' to separate variants
+ lower_locale = locale.lower().replace('_', '-')
+
+ logger.debug('Adding translation %r %r -> %r', basename, lower_locale, name)
+ self.translations[f'{basename}.js'][lower_locale] = name
+ else:
+ # strip out trailing '.gz' components
+ basename = re.sub('.gz$', '', name)
+ logger.debug('Adding content %r -> %r', basename, name)
+ self.files[basename] = name
+
+ # If we see a filename like `x.min.js` we want to also offer it
+ # at `x.js`, but only if `x.js(.gz)` itself is not present.
+ # Note: this works for both the case where we found the `x.js`
+ # first (it's already in the map) and also if we find it second
+ # (it will be replaced in the map by the line just above).
+ # See https://github.com/cockpit-project/cockpit/pull/19716
+ self.files.setdefault(basename.replace('.min.', '.'), name)
+
+ # support old cockpit-po-plugin which didn't write po.manifest.??.js
+ if not self.translations['po.manifest.js']:
+ self.translations['po.manifest.js'] = self.translations['po.js']
+
+ def get_content_security_policy(self) -> str:
+ policy = {
+ "default-src": "'self'",
+ "connect-src": "'self'",
+ "form-action": "'self'",
+ "base-uri": "'self'",
+ "object-src": "'none'",
+ "font-src": "'self' data:",
+ "img-src": "'self' data:",
+ }
+
+ for item in self.manifest.csp.split(';'):
+ item = item.strip()
+ if item:
+ key, _, value = item.strip().partition(' ')
+ policy[key] = value
+
+ return ' '.join(f'{k} {v};' for k, v in policy.items()) + ' block-all-mixed-content'
+
+ def load_file(self, filename: str) -> Document:
+ content_type, content_encoding = mimetypes.guess_type(filename)
+ content_security_policy = None
+
+ if content_type is None:
+ content_type = 'text/plain'
+ elif content_type.startswith('text/html'):
+ content_security_policy = self.get_content_security_policy()
+
+ path = self.path / filename
+ logger.debug(' loading data from %s', path)
+
+ return Document(path.open('rb'), content_type, content_encoding, content_security_policy)
+
+ def load_translation(self, path: str, locales: Sequence[str]) -> Document:
+ self.ensure_scanned()
+ assert self.translations is not None
+
+ # First match wins
+ for locale in locales:
+ with contextlib.suppress(KeyError):
+ return self.load_file(self.translations[path][locale])
+
+ # We prefer to return an empty document than 404 in order to avoid
+ # errors in the console when a translation can't be found
+ return Document(io.BytesIO(), 'text/javascript')
+
+ def load_path(self, path: str, headers: JsonObject) -> Document:
+ self.ensure_scanned()
+ assert self.files is not None
+ assert self.translations is not None
+
+ if path in self.translations:
+ locales = parse_accept_language(get_str(headers, 'Accept-Language', ''))
+ return self.load_translation(path, locales)
+ else:
+ return self.load_file(self.files[path])
+
+
+class PackagesLoader:
+ CONDITIONS: ClassVar[Dict[str, Callable[[str], bool]]] = {
+ 'path-exists': os.path.exists,
+ 'path-not-exists': lambda p: not os.path.exists(p),
+ }
+
+ @classmethod
+ def get_xdg_data_dirs(cls) -> Iterable[str]:
+ try:
+ yield os.environ['XDG_DATA_HOME']
+ except KeyError:
+ yield os.path.expanduser('~/.local/share')
+
+ try:
+ yield from os.environ['XDG_DATA_DIRS'].split(':')
+ except KeyError:
+ yield from ('/usr/local/share', '/usr/share')
+
+ @classmethod
+ def patch_manifest(cls, manifest: JsonObject, parent: Path) -> JsonObject:
+ override_files = [
+ parent / 'override.json',
+ config.lookup_config(f'{parent.name}.override.json'),
+ config.DOT_CONFIG_COCKPIT / f'{parent.name}.override.json',
+ ]
+
+ for override_file in override_files:
+ try:
+ override: JsonValue = json.loads(override_file.read_bytes())
+ except FileNotFoundError:
+ continue
+ except json.JSONDecodeError as exc:
+ # User input error: report a warning
+ logger.warning('%s: %s', override_file, exc)
+
+ if not isinstance(override, dict):
+ logger.warning('%s: override file is not a dictionary', override_file)
+ continue
+
+ manifest = json_merge_patch(manifest, override)
+
+ return patch_libexecdir(manifest)
+
+ @classmethod
+ def load_manifests(cls) -> Iterable[Manifest]:
+ for datadir in cls.get_xdg_data_dirs():
+ logger.debug("Scanning for manifest files under %s", datadir)
+ for file in Path(datadir).glob('cockpit/*/manifest.json'):
+ logger.debug("Considering file %s", file)
+ try:
+ manifest = json.loads(file.read_text())
+ except json.JSONDecodeError as exc:
+ logger.error("%s: %s", file, exc)
+ continue
+ if not isinstance(manifest, dict):
+ logger.error("%s: json document isn't an object", file)
+ continue
+
+ parent = file.parent
+ manifest = cls.patch_manifest(manifest, parent)
+ try:
+ yield Manifest(parent, manifest)
+ except JsonError as exc:
+ logger.warning('%s %s', file, exc)
+
+ def check_condition(self, condition: str, value: object) -> bool:
+ check_fn = self.CONDITIONS[condition]
+
+ # All known predicates currently only work on strings
+ if not isinstance(value, str):
+ return False
+
+ return check_fn(value)
+
+ def check_conditions(self, manifest: Manifest) -> bool:
+ for condition in manifest.conditions:
+ try:
+ okay = self.check_condition(condition.name, condition.value)
+ except KeyError:
+ # do *not* ignore manifests with unknown predicates, for forward compatibility
+ logger.warning(' %s: ignoring unknown predicate in manifest: %s', manifest.path, condition.name)
+ continue
+
+ if not okay:
+ logger.debug(' hiding package %s as its %s condition is not met', manifest.path, condition)
+ return False
+
+ return True
+
+ def load_packages(self) -> Iterable[Tuple[str, Package]]:
+ logger.debug('Scanning for available package manifests:')
+ # Sort all available packages into buckets by to their claimed name
+ names: Dict[str, List[Manifest]] = collections.defaultdict(list)
+ for manifest in self.load_manifests():
+ logger.debug(' %s/manifest.json', manifest.path)
+ names[manifest.name].append(manifest)
+ logger.debug('done.')
+
+ logger.debug('Selecting packages to serve:')
+ for name, candidates in names.items():
+ # For each package name, iterate the candidates in descending
+ # priority order and select the first one which passes all checks
+ for candidate in sorted(candidates, key=lambda manifest: manifest.priority, reverse=True):
+ try:
+ if self.check_conditions(candidate):
+ logger.debug(' creating package %s -> %s', name, candidate.path)
+ yield name, Package(candidate)
+ break
+ except JsonError:
+ logger.warning(' %s: ignoring package with invalid manifest file', candidate.path)
+
+ logger.debug(' ignoring %s: unmet conditions', candidate.path)
+ logger.debug('done.')
+
+
+class Packages(bus.Object, interface='cockpit.Packages'):
+ loader: PackagesLoader
+ listener: Optional[PackagesListener]
+ packages: Dict[str, Package]
+ saw_first_reload_hint: bool
+
+ def __init__(self, listener: Optional[PackagesListener] = None, loader: Optional[PackagesLoader] = None):
+ self.listener = listener
+ self.loader = loader or PackagesLoader()
+ self.load()
+
+ # Reloading the Shell in the browser should reload the
+ # packages. This is implemented by having the Shell call
+ # reload_hint whenever it starts. The first call of this
+ # method in each session is ignored so that packages are not
+ # loaded twice right after logging in.
+ #
+ self.saw_first_reload_hint = False
+
+ def load(self) -> None:
+ self.packages = dict(self.loader.load_packages())
+ self.manifests = json.dumps({name: dict(package.manifest) for name, package in self.packages.items()})
+ logger.debug('Packages loaded: %s', list(self.packages))
+
+ def show(self):
+ for name in sorted(self.packages):
+ package = self.packages[name]
+ menuitems = []
+ for entry in itertools.chain(
+ package.manifest.get('menu', {}).values(),
+ package.manifest.get('tools', {}).values()):
+ with contextlib.suppress(KeyError):
+ menuitems.append(entry['label'])
+ print(f'{name:20} {", ".join(menuitems):40} {package.path}')
+
+ def get_bridge_configs(self) -> Sequence[BridgeConfig]:
+ def yield_configs():
+ for package in sorted(self.packages.values(), key=lambda package: -package.priority):
+ yield from package.manifest.bridges
+ return tuple(yield_configs())
+
+ # D-Bus Interface
+ manifests = bus.Interface.Property('s', value="{}")
+
+ @bus.Interface.Method()
+ def reload(self):
+ self.load()
+ if self.listener is not None:
+ self.listener.packages_loaded()
+
+ @bus.Interface.Method()
+ def reload_hint(self):
+ if self.saw_first_reload_hint:
+ self.reload()
+ self.saw_first_reload_hint = True
+
+ def load_manifests_js(self, headers: JsonObject) -> Document:
+ logger.debug('Serving /manifests.js')
+
+ chunks: List[bytes] = []
+
+ # Send the translations required for the manifest files, from each package
+ locales = parse_accept_language(get_str(headers, 'Accept-Language', ''))
+ for name, package in self.packages.items():
+ if name in ['static', 'base1']:
+ continue
+
+ # find_translation will always find at least 'en'
+ translation = package.load_translation('po.manifest.js', locales)
+ with translation.data:
+ if translation.content_encoding == 'gzip':
+ data = gzip.decompress(translation.data.read())
+ else:
+ data = translation.data.read()
+
+ chunks.append(data)
+
+ chunks.append(b"""
+ (function (root, data) {
+ if (typeof define === 'function' && define.amd) {
+ define(data);
+ }
+
+ if (typeof cockpit === 'object') {
+ cockpit.manifests = data;
+ } else {
+ root.manifests = data;
+ }
+ }(this, """ + self.manifests.encode() + b"""))""")
+
+ return Document(io.BytesIO(b'\n'.join(chunks)), 'text/javascript')
+
+ def load_manifests_json(self) -> Document:
+ logger.debug('Serving /manifests.json')
+ return Document(io.BytesIO(self.manifests.encode()), 'application/json')
+
+ PATH_RE = re.compile(
+ r'/' # leading '/'
+ r'(?:([^/]+)/)?' # optional leading path component
+ r'((?:[^/]+/)*[^/]+)' # remaining path components
+ )
+
+ def load_path(self, path: str, headers: JsonObject) -> Document:
+ logger.debug('packages: serving %s', path)
+
+ match = self.PATH_RE.fullmatch(path)
+ if match is None:
+ raise ValueError(f'Invalid HTTP path {path}')
+ packagename, filename = match.groups()
+
+ if packagename is not None:
+ return self.packages[packagename].load_path(filename, headers)
+ elif filename == 'manifests.js':
+ return self.load_manifests_js(headers)
+ elif filename == 'manifests.json':
+ return self.load_manifests_json()
+ else:
+ raise KeyError
diff --git a/src/cockpit/peer.py b/src/cockpit/peer.py
new file mode 100644
index 0000000..3009e3b
--- /dev/null
+++ b/src/cockpit/peer.py
@@ -0,0 +1,330 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import asyncio
+import logging
+import os
+from typing import Callable, List, Optional, Sequence
+
+from .jsonutil import JsonObject, JsonValue
+from .packages import BridgeConfig
+from .protocol import CockpitProblem, CockpitProtocol, CockpitProtocolError
+from .router import Endpoint, Router, RoutingRule
+from .transports import SubprocessProtocol, SubprocessTransport
+
+logger = logging.getLogger(__name__)
+
+
+class PeerError(CockpitProblem):
+ pass
+
+
+class PeerExited(Exception):
+ def __init__(self, exit_code: int):
+ self.exit_code = exit_code
+
+
+class Peer(CockpitProtocol, SubprocessProtocol, Endpoint):
+ done_callbacks: List[Callable[[], None]]
+ init_future: Optional[asyncio.Future]
+
+ def __init__(self, router: Router):
+ super().__init__(router)
+
+ # All Peers start out frozen — we only unfreeze after we see the first 'init' message
+ self.freeze_endpoint()
+
+ self.init_future = asyncio.get_running_loop().create_future()
+ self.done_callbacks = []
+
+ # Initialization
+ async def do_connect_transport(self) -> None:
+ raise NotImplementedError
+
+ async def spawn(self, argv: Sequence[str], env: Sequence[str], **kwargs) -> asyncio.Transport:
+ # Not actually async...
+ loop = asyncio.get_running_loop()
+ user_env = dict(e.split('=', 1) for e in env)
+ return SubprocessTransport(loop, self, argv, env=dict(os.environ, **user_env), **kwargs)
+
+ async def start(self, init_host: Optional[str] = None, **kwargs: JsonValue) -> JsonObject:
+ """Request that the Peer is started and connected to the router.
+
+ Creates the transport, connects it to the protocol, and participates in
+ exchanging of init messages. If anything goes wrong, the connection
+ will be closed and an exception will be raised.
+
+ The Peer starts out in a frozen state (ie: attempts to send messages to
+ it will initially be queued). If init_host is not None then an init
+ message is sent with the given 'host' field, plus any extra kwargs, and
+ the queue is thawed. Otherwise, the caller is responsible for sending
+ the init message and thawing the peer.
+
+ In any case, the return value is the init message from the peer.
+ """
+ assert self.init_future is not None
+
+ def _connect_task_done(task: asyncio.Task) -> None:
+ assert task is connect_task
+ try:
+ task.result()
+ except asyncio.CancelledError: # we did that (below)
+ pass # we want to ignore it
+ except Exception as exc:
+ self.close(exc)
+
+ connect_task = asyncio.create_task(self.do_connect_transport())
+ connect_task.add_done_callback(_connect_task_done)
+
+ try:
+ # Wait for something to happen:
+ # - exception from our connection function
+ # - receiving "init" from the other side
+ # - receiving EOF from the other side
+ # - .close() was called
+ # - other transport exception
+ init_message = await self.init_future
+
+ except (PeerExited, BrokenPipeError):
+ # These are fairly generic errors. PeerExited means that we observed the process exiting.
+ # BrokenPipeError means that we got EPIPE when attempting to write() to it. In both cases,
+ # the process is gone, but it's not clear why. If the connection process is still running,
+ # perhaps we'd get a better error message from it.
+ await connect_task
+ # Otherwise, re-raise
+ raise
+
+ finally:
+ self.init_future = None
+
+ # In any case (failure or success) make sure this is done.
+ if not connect_task.done():
+ connect_task.cancel()
+
+ if init_host is not None:
+ logger.debug(' sending init message back, host %s', init_host)
+ # Send "init" back
+ self.write_control(None, command='init', version=1, host=init_host, **kwargs)
+
+ # Thaw the queued messages
+ self.thaw_endpoint()
+
+ return init_message
+
+ # Background initialization
+ def start_in_background(self, init_host: Optional[str] = None, **kwargs: JsonValue) -> None:
+ def _start_task_done(task: asyncio.Task) -> None:
+ assert task is start_task
+
+ try:
+ task.result()
+ except (OSError, PeerExited, CockpitProblem, asyncio.CancelledError):
+ pass # Those are expected. Others will throw.
+
+ start_task = asyncio.create_task(self.start(init_host, **kwargs))
+ start_task.add_done_callback(_start_task_done)
+
+ # Shutdown
+ def add_done_callback(self, callback: Callable[[], None]) -> None:
+ self.done_callbacks.append(callback)
+
+ # Handling of interesting events
+ def do_superuser_init_done(self) -> None:
+ pass
+
+ def do_authorize(self, message: JsonObject) -> None:
+ pass
+
+ def transport_control_received(self, command: str, message: JsonObject) -> None:
+ if command == 'init' and self.init_future is not None:
+ logger.debug('Got init message with active init_future. Setting result.')
+ self.init_future.set_result(message)
+ elif command == 'authorize':
+ self.do_authorize(message)
+ elif command == 'superuser-init-done':
+ self.do_superuser_init_done()
+ else:
+ raise CockpitProtocolError(f'Received unexpected control message {command}')
+
+ def eof_received(self) -> bool:
+ # We always expect to be the ones to close the connection, so if we get
+ # an EOF, then we consider it to be an error. This allows us to
+ # distinguish close caused by unexpected EOF (but no errno from a
+ # syscall failure) vs. close caused by calling .close() on our side.
+ # The process is still running at this point, so keep it and handle
+ # the error in process_exited().
+ logger.debug('Peer %s received unexpected EOF', self.__class__.__name__)
+ return True
+
+ def do_closed(self, exc: Optional[Exception]) -> None:
+ logger.debug('Peer %s connection lost %s %s', self.__class__.__name__, type(exc), exc)
+
+ if exc is None:
+ self.shutdown_endpoint(problem='terminated')
+ elif isinstance(exc, PeerExited):
+ # a common case is that the called peer does not exist
+ if exc.exit_code == 127:
+ self.shutdown_endpoint(problem='no-cockpit')
+ else:
+ self.shutdown_endpoint(problem='terminated', message=f'Peer exited with status {exc.exit_code}')
+ elif isinstance(exc, CockpitProblem):
+ self.shutdown_endpoint(exc.attrs)
+ else:
+ self.shutdown_endpoint(problem='internal-error',
+ message=f"[{exc.__class__.__name__}] {exc!s}")
+
+ # If .start() is running, we need to make sure it stops running,
+ # raising the correct exception.
+ if self.init_future is not None and not self.init_future.done():
+ if exc is not None:
+ self.init_future.set_exception(exc)
+ else:
+ self.init_future.cancel()
+
+ for callback in self.done_callbacks:
+ callback()
+
+ def process_exited(self) -> None:
+ assert isinstance(self.transport, SubprocessTransport)
+ logger.debug('Peer %s exited, status %d', self.__class__.__name__, self.transport.get_returncode())
+ returncode = self.transport.get_returncode()
+ assert isinstance(returncode, int)
+ self.close(PeerExited(returncode))
+
+ # Forwarding data: from the peer to the router
+ def channel_control_received(self, channel: str, command: str, message: JsonObject) -> None:
+ if self.init_future is not None:
+ raise CockpitProtocolError('Received unexpected channel control message before init')
+ self.send_channel_control(channel, command, message)
+
+ def channel_data_received(self, channel: str, data: bytes) -> None:
+ if self.init_future is not None:
+ raise CockpitProtocolError('Received unexpected channel data before init')
+ self.send_channel_data(channel, data)
+
+ # Forwarding data: from the router to the peer
+ def do_channel_control(self, channel: str, command: str, message: JsonObject) -> None:
+ assert self.init_future is None
+ self.write_control(message)
+
+ def do_channel_data(self, channel: str, data: bytes) -> None:
+ assert self.init_future is None
+ self.write_channel_data(channel, data)
+
+ def do_kill(self, host: 'str | None', group: 'str | None', message: JsonObject) -> None:
+ assert self.init_future is None
+ self.write_control(message)
+
+ def do_close(self) -> None:
+ self.close()
+
+
+class ConfiguredPeer(Peer):
+ config: BridgeConfig
+ args: Sequence[str]
+ env: Sequence[str]
+
+ def __init__(self, router: Router, config: BridgeConfig):
+ self.config = config
+ self.args = config.spawn
+ self.env = config.environ
+ super().__init__(router)
+
+ async def do_connect_transport(self) -> None:
+ await self.spawn(self.args, self.env)
+
+
+class PeerRoutingRule(RoutingRule):
+ config: BridgeConfig
+ match: JsonObject
+ peer: Optional[Peer]
+
+ def __init__(self, router: Router, config: BridgeConfig):
+ super().__init__(router)
+ self.config = config
+ self.match = config.match
+ self.peer = None
+
+ def apply_rule(self, options: JsonObject) -> Optional[Peer]:
+ # Check that we match
+
+ for key, value in self.match.items():
+ if key not in options:
+ logger.debug(' rejecting because key %s is missing', key)
+ return None
+ if value is not None and options[key] != value:
+ logger.debug(' rejecting because key %s has wrong value %s (vs %s)', key, options[key], value)
+ return None
+
+ # Start the peer if it's not running already
+ if self.peer is None:
+ self.peer = ConfiguredPeer(self.router, self.config)
+ self.peer.add_done_callback(self.peer_closed)
+ assert self.router.init_host
+ self.peer.start_in_background(init_host=self.router.init_host)
+
+ return self.peer
+
+ def peer_closed(self):
+ self.peer = None
+
+ def shutdown(self):
+ if self.peer is not None:
+ self.peer.close()
+
+
+class PeersRoutingRule(RoutingRule):
+ rules: List[PeerRoutingRule] = []
+
+ def apply_rule(self, options: JsonObject) -> Optional[Endpoint]:
+ logger.debug(' considering %d rules', len(self.rules))
+ for rule in self.rules:
+ logger.debug(' considering %s', rule.config.name)
+ endpoint = rule.apply_rule(options)
+ if endpoint is not None:
+ logger.debug(' selected')
+ return endpoint
+ logger.debug(' no peer rules matched')
+ return None
+
+ def set_configs(self, bridge_configs: Sequence[BridgeConfig]) -> None:
+ old_rules = self.rules
+ self.rules = []
+
+ for config in bridge_configs:
+ # Those are handled elsewhere...
+ if config.privileged or 'host' in config.match:
+ continue
+
+ # Try to reuse an existing rule, if one exists...
+ for rule in list(old_rules):
+ if rule.config == config:
+ old_rules.remove(rule)
+ break
+ else:
+ # ... otherwise, create a new one.
+ rule = PeerRoutingRule(self.router, config)
+
+ self.rules.append(rule)
+
+ # close down the old rules that didn't get reclaimed
+ for rule in old_rules:
+ rule.shutdown()
+
+ def shutdown(self):
+ for rule in self.rules:
+ rule.shutdown()
diff --git a/src/cockpit/polkit.py b/src/cockpit/polkit.py
new file mode 100644
index 0000000..b59a6a2
--- /dev/null
+++ b/src/cockpit/polkit.py
@@ -0,0 +1,171 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2023 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import asyncio
+import locale
+import logging
+import os
+import pwd
+from typing import Dict, List, Sequence, Tuple
+
+from cockpit._vendor.ferny import AskpassHandler
+from cockpit._vendor.systemd_ctypes import Variant, bus
+
+# that path is valid on at least Debian, Fedora/RHEL, and Arch
+HELPER_PATH = '/usr/lib/polkit-1/polkit-agent-helper-1'
+
+AGENT_DBUS_PATH = '/PolkitAgent'
+
+logger = logging.getLogger(__name__)
+
+Identity = Tuple[str, Dict[str, Variant]]
+
+
+# https://www.freedesktop.org/software/polkit/docs/latest/eggdbus-interface-org.freedesktop.PolicyKit1.AuthenticationAgent.html
+
+# Note that we don't implement the CancelAuthentication() API. pkexec gets called in a way that has no opportunity to
+# cancel an ongoing authentication from the pkexec side. On the UI side cancellation is implemented via the standard
+# asyncio process mechanism. If we ever need CancelAuthentication(), we could keep a cookie → get_current_task()
+# mapping, but that method is not available for Python 3.6 yet.
+
+class org_freedesktop_PolicyKit1_AuthenticationAgent(bus.Object):
+ def __init__(self, responder: AskpassHandler):
+ super().__init__()
+ self.responder = responder
+
+ # confusingly named: this actually does the whole authentication dialog, see docs
+ @bus.Interface.Method('', ['s', 's', 's', 'a{ss}', 's', 'a(sa{sv})'])
+ async def begin_authentication(self, action_id: str, message: str, icon_name: str,
+ details: Dict[str, str], cookie: str, identities: Sequence[Identity]) -> None:
+ logger.debug('BeginAuthentication: action %s, message "%s", icon %s, details %s, cookie %s, identities %r',
+ action_id, message, icon_name, details, cookie, identities)
+ # only support authentication as ourselves, as we don't yet have the
+ # protocol plumbing and UI to select an admin user
+ my_uid = os.geteuid()
+ for (auth_type, subject) in identities:
+ if auth_type == 'unix-user' and 'uid' in subject and subject['uid'].value == my_uid:
+ logger.debug('Authentication subject %s matches our uid %d', subject, my_uid)
+ break
+ else:
+ logger.warning('Not supporting authentication as any of %s', identities)
+ return
+
+ user_name = pwd.getpwuid(my_uid).pw_name
+ process = await asyncio.create_subprocess_exec(HELPER_PATH, user_name, cookie,
+ stdin=asyncio.subprocess.PIPE,
+ stdout=asyncio.subprocess.PIPE)
+ try:
+ await self._communicate(process)
+ except asyncio.CancelledError:
+ logger.debug('Cancelled authentication')
+ process.terminate()
+ finally:
+ res = await process.wait()
+ logger.debug('helper exited with code %i', res)
+
+ async def _communicate(self, process: asyncio.subprocess.Process) -> None:
+ assert process.stdin
+ assert process.stdout
+
+ messages: List[str] = []
+
+ async for line in process.stdout:
+ logger.debug('Read line from helper: %s', line)
+ command, _, value = line.strip().decode().partition(' ')
+
+ # usually: PAM_PROMPT_ECHO_OFF Password: \n
+ if command.startswith('PAM_PROMPT'):
+ # Don't pass this to the UI if it's "Password" (the usual case),
+ # so that superuser.py uses the translated default
+ if value.startswith('Password'):
+ value = ''
+
+ # flush out accumulated info/error messages
+ passwd = await self.responder.do_askpass('\n'.join(messages), value, '')
+ messages.clear()
+ if passwd is None:
+ logger.debug('got PAM_PROMPT %s, but do_askpass returned None', value)
+ raise asyncio.CancelledError('no password given')
+ logger.debug('got PAM_PROMPT %s, do_askpass returned a password', value)
+ process.stdin.write(passwd.encode())
+ process.stdin.write(b'\n')
+ del passwd # don't keep this around longer than necessary
+ await process.stdin.drain()
+ logger.debug('got PAM_PROMPT, wrote password to helper')
+ elif command in ('PAM_TEXT_INFO', 'PAM_ERROR'):
+ messages.append(value)
+ elif command == 'SUCCESS':
+ logger.debug('Authentication succeeded')
+ break
+ elif command == 'FAILURE':
+ logger.warning('Authentication failed')
+ break
+ else:
+ logger.warning('Unknown line from helper, aborting: %s', line)
+ process.terminate()
+ break
+
+
+class PolkitAgent:
+ """Register polkit agent when required
+
+ Use this as a context manager to ensure that the agent gets unregistered again.
+ """
+ def __init__(self, responder: AskpassHandler):
+ self.responder = responder
+ self.agent_slot = None
+
+ async def __aenter__(self):
+ try:
+ self.system_bus = bus.Bus.default_system()
+ except OSError as e:
+ logger.warning('cannot connect to system bus, not registering polkit agent: %s', e)
+ return self
+
+ try:
+ # may refine that with a D-Bus call to logind
+ self.subject = ('unix-session', {'session-id': Variant(os.environ['XDG_SESSION_ID'], 's')})
+ except KeyError:
+ logger.debug('XDG_SESSION_ID not set, not registering polkit agent')
+ return self
+
+ agent_object = org_freedesktop_PolicyKit1_AuthenticationAgent(self.responder)
+ self.agent_slot = self.system_bus.add_object(AGENT_DBUS_PATH, agent_object)
+
+ # register agent
+ locale_name = locale.setlocale(locale.LC_MESSAGES, None)
+ await self.system_bus.call_method_async(
+ 'org.freedesktop.PolicyKit1',
+ '/org/freedesktop/PolicyKit1/Authority',
+ 'org.freedesktop.PolicyKit1.Authority',
+ 'RegisterAuthenticationAgent',
+ '(sa{sv})ss',
+ self.subject, locale_name, AGENT_DBUS_PATH)
+ logger.debug('Registered agent for %r and locale %s', self.subject, locale_name)
+ return self
+
+ async def __aexit__(self, _exc_type, _exc_value, _traceback):
+ if self.agent_slot:
+ await self.system_bus.call_method_async(
+ 'org.freedesktop.PolicyKit1',
+ '/org/freedesktop/PolicyKit1/Authority',
+ 'org.freedesktop.PolicyKit1.Authority',
+ 'UnregisterAuthenticationAgent',
+ '(sa{sv})s',
+ self.subject, AGENT_DBUS_PATH)
+ self.agent_slot.cancel()
+ logger.debug('Unregistered agent for %r', self.subject)
diff --git a/src/cockpit/polyfills.py b/src/cockpit/polyfills.py
new file mode 100644
index 0000000..6a85c77
--- /dev/null
+++ b/src/cockpit/polyfills.py
@@ -0,0 +1,57 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2023 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import contextlib
+import socket
+
+
+def install():
+ """Add shims for older Python versions"""
+
+ # introduced in 3.9
+ if not hasattr(socket, 'recv_fds'):
+ import array
+
+ import _socket
+
+ def recv_fds(sock, bufsize, maxfds, flags=0):
+ fds = array.array("i")
+ msg, ancdata, flags, addr = sock.recvmsg(bufsize, _socket.CMSG_LEN(maxfds * fds.itemsize))
+ for cmsg_level, cmsg_type, cmsg_data in ancdata:
+ if (cmsg_level == _socket.SOL_SOCKET and cmsg_type == _socket.SCM_RIGHTS):
+ fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
+ return msg, list(fds), flags, addr
+
+ socket.recv_fds = recv_fds
+
+ # introduced in 3.7
+ if not hasattr(contextlib, 'AsyncExitStack'):
+ class AsyncExitStack:
+ async def __aenter__(self):
+ self.cms = []
+ return self
+
+ async def enter_async_context(self, cm):
+ result = await cm.__aenter__()
+ self.cms.append(cm)
+ return result
+
+ async def __aexit__(self, exc_type, exc_value, traceback):
+ for cm in self.cms:
+ cm.__aexit__(exc_type, exc_value, traceback)
+
+ contextlib.AsyncExitStack = AsyncExitStack
diff --git a/src/cockpit/protocol.py b/src/cockpit/protocol.py
new file mode 100644
index 0000000..23b930a
--- /dev/null
+++ b/src/cockpit/protocol.py
@@ -0,0 +1,248 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import asyncio
+import json
+import logging
+import uuid
+
+from .jsonutil import JsonError, JsonObject, JsonValue, create_object, get_int, get_str, typechecked
+
+logger = logging.getLogger(__name__)
+
+
+class CockpitProblem(Exception):
+ """A type of exception that carries a problem code and a message.
+
+ Depending on the scope, this is used to handle shutting down:
+
+ - an individual channel (sends problem code in the close message)
+ - peer connections (sends problem code in close message for each open channel)
+ - the main stdio interaction with the bridge
+
+ It is usually thrown in response to some violation of expected protocol
+ when parsing messages, connecting to a peer, or opening a channel.
+ """
+ attrs: JsonObject
+
+ def __init__(self, problem: str, _msg: 'JsonObject | None' = None, **kwargs: JsonValue) -> None:
+ kwargs['problem'] = problem
+ self.attrs = create_object(_msg, kwargs)
+ super().__init__(get_str(self.attrs, 'message', problem))
+
+
+class CockpitProtocolError(CockpitProblem):
+ def __init__(self, message: str, problem: str = 'protocol-error'):
+ super().__init__(problem, message=message)
+
+
+class CockpitProtocol(asyncio.Protocol):
+ """A naive implementation of the Cockpit frame protocol
+
+ We need to use this because Python's SelectorEventLoop doesn't supported
+ buffered protocols.
+ """
+ transport: 'asyncio.Transport | None' = None
+ buffer = b''
+ _closed: bool = False
+ _communication_done: 'asyncio.Future[None] | None' = None
+
+ def do_ready(self) -> None:
+ pass
+
+ def do_closed(self, exc: 'Exception | None') -> None:
+ pass
+
+ def transport_control_received(self, command: str, message: JsonObject) -> None:
+ raise NotImplementedError
+
+ def channel_control_received(self, channel: str, command: str, message: JsonObject) -> None:
+ raise NotImplementedError
+
+ def channel_data_received(self, channel: str, data: bytes) -> None:
+ raise NotImplementedError
+
+ def frame_received(self, frame: bytes) -> None:
+ header, _, data = frame.partition(b'\n')
+
+ if header != b'':
+ channel = header.decode('ascii')
+ logger.debug('data received: %d bytes of data for channel %s', len(data), channel)
+ self.channel_data_received(channel, data)
+
+ else:
+ self.control_received(data)
+
+ def control_received(self, data: bytes) -> None:
+ try:
+ message = typechecked(json.loads(data), dict)
+ command = get_str(message, 'command')
+ channel = get_str(message, 'channel', None)
+
+ if channel is not None:
+ logger.debug('channel control received %s', message)
+ self.channel_control_received(channel, command, message)
+ else:
+ logger.debug('transport control received %s', message)
+ self.transport_control_received(command, message)
+
+ except (json.JSONDecodeError, JsonError) as exc:
+ raise CockpitProtocolError(f'control message: {exc!s}') from exc
+
+ def consume_one_frame(self, data: bytes) -> int:
+ """Consumes a single frame from view.
+
+ Returns positive if a number of bytes were consumed, or negative if no
+ work can be done because of a given number of bytes missing.
+ """
+
+ try:
+ newline = data.index(b'\n')
+ except ValueError as exc:
+ if len(data) < 10:
+ # Let's try reading more
+ return len(data) - 10
+ raise CockpitProtocolError("size line is too long") from exc
+
+ try:
+ length = int(data[:newline])
+ except ValueError as exc:
+ raise CockpitProtocolError("frame size is not an integer") from exc
+
+ start = newline + 1
+ end = start + length
+
+ if end > len(data):
+ # We need to read more
+ return len(data) - end
+
+ # We can consume a full frame
+ self.frame_received(data[start:end])
+ return end
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ logger.debug('connection_made(%s)', transport)
+ assert isinstance(transport, asyncio.Transport)
+ self.transport = transport
+ self.do_ready()
+
+ if self._closed:
+ logger.debug(' but the protocol already was closed, so closing transport')
+ transport.close()
+
+ def connection_lost(self, exc: 'Exception | None') -> None:
+ logger.debug('connection_lost')
+ assert self.transport is not None
+ self.transport = None
+ self.close(exc)
+
+ def close(self, exc: 'Exception | None' = None) -> None:
+ if self._closed:
+ return
+ self._closed = True
+
+ if self.transport:
+ self.transport.close()
+
+ self.do_closed(exc)
+
+ def write_channel_data(self, channel: str, payload: bytes) -> None:
+ """Send a given payload (bytes) on channel (string)"""
+ # Channel is certainly ascii (as enforced by .encode() below)
+ frame_length = len(channel + '\n') + len(payload)
+ header = f'{frame_length}\n{channel}\n'.encode('ascii')
+ if self.transport is not None:
+ logger.debug('writing to transport %s', self.transport)
+ self.transport.write(header + payload)
+ else:
+ logger.debug('cannot write to closed transport')
+
+ def write_control(self, _msg: 'JsonObject | None' = None, **kwargs: JsonValue) -> None:
+ """Write a control message. See jsonutil.create_object() for details."""
+ logger.debug('sending control message %r %r', _msg, kwargs)
+ pretty = json.dumps(create_object(_msg, kwargs), indent=2) + '\n'
+ self.write_channel_data('', pretty.encode())
+
+ def data_received(self, data: bytes) -> None:
+ try:
+ self.buffer += data
+ while self.buffer:
+ result = self.consume_one_frame(self.buffer)
+ if result <= 0:
+ return
+ self.buffer = self.buffer[result:]
+ except CockpitProtocolError as exc:
+ self.close(exc)
+
+ def eof_received(self) -> bool:
+ return False
+
+
+# Helpful functionality for "server"-side protocol implementations
+class CockpitProtocolServer(CockpitProtocol):
+ init_host: 'str | None' = None
+ authorizations: 'dict[str, asyncio.Future[str]] | None' = None
+
+ def do_send_init(self) -> None:
+ raise NotImplementedError
+
+ def do_init(self, message: JsonObject) -> None:
+ pass
+
+ def do_kill(self, host: 'str | None', group: 'str | None', message: JsonObject) -> None:
+ raise NotImplementedError
+
+ def transport_control_received(self, command: str, message: JsonObject) -> None:
+ if command == 'init':
+ if get_int(message, 'version') != 1:
+ raise CockpitProtocolError('incorrect version number')
+ self.init_host = get_str(message, 'host')
+ self.do_init(message)
+ elif command == 'kill':
+ self.do_kill(get_str(message, 'host', None), get_str(message, 'group', None), message)
+ elif command == 'authorize':
+ self.do_authorize(message)
+ else:
+ raise CockpitProtocolError(f'unexpected control message {command} received')
+
+ def do_ready(self) -> None:
+ self.do_send_init()
+
+ # authorize request/response API
+ async def request_authorization(
+ self, challenge: str, timeout: 'int | None' = None, **kwargs: JsonValue
+ ) -> str:
+ if self.authorizations is None:
+ self.authorizations = {}
+ cookie = str(uuid.uuid4())
+ future = asyncio.get_running_loop().create_future()
+ try:
+ self.authorizations[cookie] = future
+ self.write_control(None, command='authorize', challenge=challenge, cookie=cookie, **kwargs)
+ return await asyncio.wait_for(future, timeout)
+ finally:
+ self.authorizations.pop(cookie)
+
+ def do_authorize(self, message: JsonObject) -> None:
+ cookie = get_str(message, 'cookie')
+ response = get_str(message, 'response')
+
+ if self.authorizations is None or cookie not in self.authorizations:
+ logger.warning('no matching authorize request')
+ return
+
+ self.authorizations[cookie].set_result(response)
diff --git a/src/cockpit/remote.py b/src/cockpit/remote.py
new file mode 100644
index 0000000..ccbdd22
--- /dev/null
+++ b/src/cockpit/remote.py
@@ -0,0 +1,233 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import getpass
+import logging
+import re
+import socket
+from typing import Dict, List, Optional, Tuple
+
+from cockpit._vendor import ferny
+
+from .jsonutil import JsonObject, JsonValue, get_str, get_str_or_none
+from .peer import Peer, PeerError
+from .router import Router, RoutingRule
+
+logger = logging.getLogger(__name__)
+
+
+class PasswordResponder(ferny.AskpassHandler):
+ PASSPHRASE_RE = re.compile(r"Enter passphrase for key '(.*)': ")
+
+ password: Optional[str]
+
+ hostkeys_seen: List[Tuple[str, str, str, str, str]]
+ error_message: Optional[str]
+ password_attempts: int
+
+ def __init__(self, password: Optional[str]):
+ self.password = password
+
+ self.hostkeys_seen = []
+ self.error_message = None
+ self.password_attempts = 0
+
+ async def do_hostkey(self, reason: str, host: str, algorithm: str, key: str, fingerprint: str) -> bool:
+ self.hostkeys_seen.append((reason, host, algorithm, key, fingerprint))
+ return False
+
+ async def do_askpass(self, messages: str, prompt: str, hint: str) -> Optional[str]:
+ logger.debug('Got askpass(%s): %s', hint, prompt)
+
+ match = PasswordResponder.PASSPHRASE_RE.fullmatch(prompt)
+ if match is not None:
+ # We never unlock private keys — we rather need to throw a
+ # specially-formatted error message which will cause the frontend
+ # to load the named key into the agent for us and try again.
+ path = match.group(1)
+ logger.debug("This is a passphrase request for %s, but we don't do those. Abort.", path)
+ self.error_message = f'locked identity: {path}'
+ return None
+
+ assert self.password is not None
+ assert self.password_attempts == 0
+ self.password_attempts += 1
+ return self.password
+
+
+class SshPeer(Peer):
+ session: Optional[ferny.Session] = None
+ host: str
+ user: Optional[str]
+ password: Optional[str]
+ private: bool
+
+ async def do_connect_transport(self) -> None:
+ assert self.session is not None
+ logger.debug('Starting ssh session user=%s, host=%s, private=%s', self.user, self.host, self.private)
+
+ basename, colon, portstr = self.host.rpartition(':')
+ if colon and portstr.isdigit():
+ host = basename
+ port = int(portstr)
+ else:
+ host = self.host
+ port = None
+
+ responder = PasswordResponder(self.password)
+ options = {"StrictHostKeyChecking": 'yes'}
+
+ if self.password is not None:
+ options.update(NumberOfPasswordPrompts='1')
+ else:
+ options.update(PasswordAuthentication="no", KbdInteractiveAuthentication="no")
+
+ try:
+ await self.session.connect(host, login_name=self.user, port=port,
+ handle_host_key=self.private, options=options,
+ interaction_responder=responder)
+ except (OSError, socket.gaierror) as exc:
+ logger.debug('connecting to host %s failed: %s', host, exc)
+ raise PeerError('no-host', error='no-host', message=str(exc)) from exc
+
+ except ferny.SshHostKeyError as exc:
+ if responder.hostkeys_seen:
+ # If we saw a hostkey then we can issue a detailed error message
+ # containing the key that would need to be accepted. That will
+ # cause the front-end to present a dialog.
+ _reason, host, algorithm, key, fingerprint = responder.hostkeys_seen[0]
+ error_args = {'host-key': f'{host} {algorithm} {key}', 'host-fingerprint': fingerprint}
+ else:
+ error_args = {}
+
+ if isinstance(exc, ferny.SshChangedHostKeyError):
+ error = 'invalid-hostkey'
+ elif self.private:
+ error = 'unknown-hostkey'
+ else:
+ # non-private session case. throw a generic error.
+ error = 'unknown-host'
+
+ logger.debug('SshPeer got a %s %s; private %s, seen hostkeys %r; raising %s with extra args %r',
+ type(exc), exc, self.private, responder.hostkeys_seen, error, error_args)
+ raise PeerError(error, error_args, error=error, auth_method_results={}) from exc
+
+ except ferny.SshAuthenticationError as exc:
+ logger.debug('authentication to host %s failed: %s', host, exc)
+
+ results = {method: 'not-provided' for method in exc.methods}
+ if 'password' in results and self.password is not None:
+ if responder.password_attempts == 0:
+ results['password'] = 'not-tried'
+ else:
+ results['password'] = 'denied'
+
+ raise PeerError('authentication-failed',
+ error=responder.error_message or 'authentication-failed',
+ auth_method_results=results) from exc
+
+ except ferny.SshError as exc:
+ logger.debug('unknown failure connecting to host %s: %s', host, exc)
+ raise PeerError('internal-error', message=str(exc)) from exc
+
+ args = self.session.wrap_subprocess_args(['cockpit-bridge'])
+ await self.spawn(args, [])
+
+ def do_kill(self, host: 'str | None', group: 'str | None', message: JsonObject) -> None:
+ if host == self.host:
+ self.close()
+ elif host is None:
+ super().do_kill(host, group, message)
+
+ def do_authorize(self, message: JsonObject) -> None:
+ if get_str(message, 'challenge').startswith('plain1:'):
+ cookie = get_str(message, 'cookie')
+ self.write_control(command='authorize', cookie=cookie, response=self.password or '')
+ self.password = None # once is enough...
+
+ def do_superuser_init_done(self) -> None:
+ self.password = None
+
+ def __init__(self, router: Router, host: str, user: Optional[str], options: JsonObject, *, private: bool) -> None:
+ super().__init__(router)
+ self.host = host
+ self.user = user
+ self.password = get_str(options, 'password', None)
+ self.private = private
+
+ self.session = ferny.Session()
+
+ superuser: JsonValue
+ init_superuser = get_str_or_none(options, 'init-superuser', None)
+ if init_superuser in (None, 'none'):
+ superuser = False
+ else:
+ superuser = {'id': init_superuser}
+
+ self.start_in_background(init_host=host, superuser=superuser)
+
+
+class HostRoutingRule(RoutingRule):
+ remotes: Dict[Tuple[str, Optional[str], Optional[str]], Peer]
+
+ def __init__(self, router):
+ super().__init__(router)
+ self.remotes = {}
+
+ def apply_rule(self, options: JsonObject) -> Optional[Peer]:
+ assert self.router is not None
+ assert self.router.init_host is not None
+
+ host = get_str(options, 'host', self.router.init_host)
+ if host == self.router.init_host:
+ return None
+
+ user = get_str(options, 'user', None)
+ # HACK: the front-end relies on this for tracking connections without an explicit user name;
+ # the user will then be determined by SSH (`User` in the config or the current user)
+ # See cockpit_router_normalize_host_params() in src/bridge/cockpitrouter.c
+ if user == getpass.getuser():
+ user = None
+ if not user:
+ user_from_host, _, _ = host.rpartition('@')
+ user = user_from_host or None # avoid ''
+
+ if get_str(options, 'session', None) == 'private':
+ nonce = get_str(options, 'channel')
+ else:
+ nonce = None
+
+ assert isinstance(host, str)
+ assert user is None or isinstance(user, str)
+ assert nonce is None or isinstance(nonce, str)
+
+ key = host, user, nonce
+
+ logger.debug('Request for channel %s is remote.', options)
+ logger.debug('key=%s', key)
+
+ if key not in self.remotes:
+ logger.debug('%s is not among the existing remotes %s. Opening a new connection.', key, self.remotes)
+ peer = SshPeer(self.router, host, user, options, private=nonce is not None)
+ peer.add_done_callback(lambda: self.remotes.__delitem__(key))
+ self.remotes[key] = peer
+
+ return self.remotes[key]
+
+ def shutdown(self):
+ for peer in set(self.remotes.values()):
+ peer.close()
diff --git a/src/cockpit/router.py b/src/cockpit/router.py
new file mode 100644
index 0000000..884682f
--- /dev/null
+++ b/src/cockpit/router.py
@@ -0,0 +1,266 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import asyncio
+import collections
+import logging
+from typing import Dict, List, Optional
+
+from .jsonutil import JsonObject, JsonValue
+from .protocol import CockpitProblem, CockpitProtocolError, CockpitProtocolServer
+
+logger = logging.getLogger(__name__)
+
+
+class ExecutionQueue:
+ """Temporarily delay calls to a given set of class methods.
+
+ Functions by replacing the named function at the instance __dict__
+ level, effectively providing an override for exactly one instance
+ of `method`'s object.
+ Queues the invocations. Run them later with .run(), which also reverses
+ the redirection by deleting the named methods from the instance.
+ """
+ def __init__(self, methods):
+ self.queue = collections.deque()
+ self.methods = methods
+
+ for method in self.methods:
+ self._wrap(method)
+
+ def _wrap(self, method):
+ # NB: this function is stored in the instance dict and therefore
+ # doesn't function as a descriptor, isn't a method, doesn't get bound,
+ # and therefore doesn't receive a self parameter
+ setattr(method.__self__, method.__func__.__name__, lambda *args: self.queue.append((method, args)))
+
+ def run(self):
+ logger.debug('ExecutionQueue: Running %d queued method calls', len(self.queue))
+ for method, args in self.queue:
+ method(*args)
+
+ for method in self.methods:
+ delattr(method.__self__, method.__func__.__name__)
+
+
+class Endpoint:
+ router: 'Router'
+ __endpoint_frozen_queue: Optional[ExecutionQueue] = None
+
+ def __init__(self, router: 'Router'):
+ router.add_endpoint(self)
+ self.router = router
+
+ def freeze_endpoint(self):
+ assert self.__endpoint_frozen_queue is None
+ logger.debug('Freezing endpoint %s', self)
+ self.__endpoint_frozen_queue = ExecutionQueue({self.do_channel_control, self.do_channel_data, self.do_kill})
+
+ def thaw_endpoint(self):
+ assert self.__endpoint_frozen_queue is not None
+ logger.debug('Thawing endpoint %s', self)
+ self.__endpoint_frozen_queue.run()
+ self.__endpoint_frozen_queue = None
+
+ # interface for receiving messages
+ def do_close(self):
+ raise NotImplementedError
+
+ def do_channel_control(self, channel: str, command: str, message: JsonObject) -> None:
+ raise NotImplementedError
+
+ def do_channel_data(self, channel: str, data: bytes) -> None:
+ raise NotImplementedError
+
+ def do_kill(self, host: 'str | None', group: 'str | None', message: JsonObject) -> None:
+ raise NotImplementedError
+
+ # interface for sending messages
+ def send_channel_data(self, channel: str, data: bytes) -> None:
+ self.router.write_channel_data(channel, data)
+
+ def send_channel_control(
+ self, channel: str, command: str, _msg: 'JsonObject | None', **kwargs: JsonValue
+ ) -> None:
+ self.router.write_control(_msg, channel=channel, command=command, **kwargs)
+ if command == 'close':
+ self.router.endpoints[self].remove(channel)
+ self.router.drop_channel(channel)
+
+ def shutdown_endpoint(self, _msg: 'JsonObject | None' = None, **kwargs: JsonValue) -> None:
+ self.router.shutdown_endpoint(self, _msg, **kwargs)
+
+
+class RoutingError(CockpitProblem):
+ pass
+
+
+class RoutingRule:
+ router: 'Router'
+
+ def __init__(self, router: 'Router'):
+ self.router = router
+
+ def apply_rule(self, options: JsonObject) -> Optional[Endpoint]:
+ """Check if a routing rule applies to a given 'open' message.
+
+ This should inspect the options dictionary and do one of the following three things:
+
+ - return an Endpoint to handle this channel
+ - raise a RoutingError to indicate that the open should be rejected
+ - return None to let the next rule run
+ """
+ raise NotImplementedError
+
+ def shutdown(self):
+ raise NotImplementedError
+
+
+class Router(CockpitProtocolServer):
+ routing_rules: List[RoutingRule]
+ open_channels: Dict[str, Endpoint]
+ endpoints: 'dict[Endpoint, set[str]]'
+ no_endpoints: asyncio.Event # set if endpoints dict is empty
+ _eof: bool = False
+
+ def __init__(self, routing_rules: List[RoutingRule]):
+ for rule in routing_rules:
+ rule.router = self
+ self.routing_rules = routing_rules
+ self.open_channels = {}
+ self.endpoints = {}
+ self.no_endpoints = asyncio.Event()
+ self.no_endpoints.set() # at first there are no endpoints
+
+ def check_rules(self, options: JsonObject) -> Endpoint:
+ for rule in self.routing_rules:
+ logger.debug(' applying rule %s', rule)
+ endpoint = rule.apply_rule(options)
+ if endpoint is not None:
+ logger.debug(' resulting endpoint is %s', endpoint)
+ return endpoint
+ else:
+ logger.debug(' No rules matched')
+ raise RoutingError('not-supported')
+
+ def drop_channel(self, channel: str) -> None:
+ try:
+ self.open_channels.pop(channel)
+ logger.debug('router dropped channel %s', channel)
+ except KeyError:
+ logger.error('trying to drop non-existent channel %s from %s', channel, self.open_channels)
+
+ def add_endpoint(self, endpoint: Endpoint) -> None:
+ self.endpoints[endpoint] = set()
+ self.no_endpoints.clear()
+
+ def shutdown_endpoint(self, endpoint: Endpoint, _msg: 'JsonObject | None' = None, **kwargs: JsonValue) -> None:
+ channels = self.endpoints.pop(endpoint)
+ logger.debug('shutdown_endpoint(%s, %s) will close %s', endpoint, kwargs, channels)
+ for channel in channels:
+ self.write_control(_msg, command='close', channel=channel, **kwargs)
+ self.drop_channel(channel)
+
+ if not self.endpoints:
+ self.no_endpoints.set()
+
+ # were we waiting to exit?
+ if self._eof:
+ logger.debug(' endpoints remaining: %r', self.endpoints)
+ if not self.endpoints and self.transport:
+ logger.debug(' close transport')
+ self.transport.close()
+
+ def do_kill(self, host: 'str | None', group: 'str | None', message: JsonObject) -> None:
+ endpoints = set(self.endpoints)
+ logger.debug('do_kill(%s, %s). Considering %d endpoints.', host, group, len(endpoints))
+ for endpoint in endpoints:
+ endpoint.do_kill(host, group, message)
+
+ def channel_control_received(self, channel: str, command: str, message: JsonObject) -> None:
+ # If this is an open message then we need to apply the routing rules to
+ # figure out the correct endpoint to connect. If it's not an open
+ # message, then we expect the endpoint to already exist.
+ if command == 'open':
+ if channel in self.open_channels:
+ raise CockpitProtocolError('channel is already open')
+
+ try:
+ logger.debug('Trying to find endpoint for new channel %s payload=%s', channel, message.get('payload'))
+ endpoint = self.check_rules(message)
+ except RoutingError as exc:
+ self.write_control(exc.attrs, command='close', channel=channel)
+ return
+
+ self.open_channels[channel] = endpoint
+ self.endpoints[endpoint].add(channel)
+ else:
+ try:
+ endpoint = self.open_channels[channel]
+ except KeyError:
+ # sending to a non-existent channel can happen due to races and is not an error
+ return
+
+ # At this point, we have the endpoint. Route the message.
+ endpoint.do_channel_control(channel, command, message)
+
+ def channel_data_received(self, channel: str, data: bytes) -> None:
+ try:
+ endpoint = self.open_channels[channel]
+ except KeyError:
+ return
+
+ endpoint.do_channel_data(channel, data)
+
+ def eof_received(self) -> bool:
+ logger.debug('eof_received(%r)', self)
+
+ endpoints = set(self.endpoints)
+ for endpoint in endpoints:
+ endpoint.do_close()
+
+ self._eof = True
+ logger.debug(' endpoints remaining: %r', self.endpoints)
+ return bool(self.endpoints)
+
+ _communication_done: Optional[asyncio.Future] = None
+
+ def do_closed(self, exc: Optional[Exception]) -> None:
+ # If we didn't send EOF yet, do it now.
+ if not self._eof:
+ self.eof_received()
+
+ if self._communication_done is not None:
+ if exc is None:
+ self._communication_done.set_result(None)
+ else:
+ self._communication_done.set_exception(exc)
+
+ async def communicate(self) -> None:
+ """Wait until communication is complete on the router and all endpoints are done."""
+ assert self._communication_done is None
+ self._communication_done = asyncio.get_running_loop().create_future()
+ try:
+ await self._communication_done
+ except (BrokenPipeError, ConnectionResetError):
+ pass # these are normal occurrences when closed from the other side
+ finally:
+ self._communication_done = None
+
+ # In an orderly exit, this is already done, but in case it wasn't
+ # orderly, we need to make sure the endpoints shut down anyway...
+ await self.no_endpoints.wait()
diff --git a/src/cockpit/samples.py b/src/cockpit/samples.py
new file mode 100644
index 0000000..b821ad7
--- /dev/null
+++ b/src/cockpit/samples.py
@@ -0,0 +1,438 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import errno
+import logging
+import os
+import re
+from typing import Any, DefaultDict, Iterable, List, NamedTuple, Optional, Tuple
+
+from cockpit._vendor.systemd_ctypes import Handle
+
+USER_HZ = os.sysconf(os.sysconf_names['SC_CLK_TCK'])
+MS_PER_JIFFY = 1000 / (USER_HZ if (USER_HZ > 0) else 100)
+HWMON_PATH = '/sys/class/hwmon'
+
+# we would like to do this, but mypy complains; https://github.com/python/mypy/issues/2900
+# Samples = collections.defaultdict[str, Union[float, Dict[str, Union[float, None]]]]
+Samples = DefaultDict[str, Any]
+
+logger = logging.getLogger(__name__)
+
+
+def read_int_file(rootfd: int, statfile: str, default: Optional[int] = None, key: bytes = b'') -> Optional[int]:
+ # Not every stat is available, such as cpu.weight
+ try:
+ fd = os.open(statfile, os.O_RDONLY, dir_fd=rootfd)
+ except FileNotFoundError:
+ return None
+
+ try:
+ data = os.read(fd, 1024)
+ except OSError as e:
+ # cgroups can disappear between the open and read
+ if e.errno != errno.ENODEV:
+ logger.warning('Failed to read %s: %s', statfile, e)
+ return None
+ finally:
+ os.close(fd)
+
+ if key:
+ start = data.index(key) + len(key)
+ end = data.index(b'\n', start)
+ data = data[start:end]
+
+ try:
+ # 0 often means "none", so replace it with default value
+ return int(data) or default
+ except ValueError:
+ # Some samples such as "memory.max" contains "max" when there is a no limit
+ return None
+
+
+class SampleDescription(NamedTuple):
+ name: str
+ units: str
+ semantics: str
+ instanced: bool
+
+
+class Sampler:
+ descriptions: List[SampleDescription]
+
+ def sample(self, samples: Samples) -> None:
+ raise NotImplementedError
+
+
+class CPUSampler(Sampler):
+ descriptions = [
+ SampleDescription('cpu.basic.nice', 'millisec', 'counter', instanced=False),
+ SampleDescription('cpu.basic.user', 'millisec', 'counter', instanced=False),
+ SampleDescription('cpu.basic.system', 'millisec', 'counter', instanced=False),
+ SampleDescription('cpu.basic.iowait', 'millisec', 'counter', instanced=False),
+
+ SampleDescription('cpu.core.nice', 'millisec', 'counter', instanced=True),
+ SampleDescription('cpu.core.user', 'millisec', 'counter', instanced=True),
+ SampleDescription('cpu.core.system', 'millisec', 'counter', instanced=True),
+ SampleDescription('cpu.core.iowait', 'millisec', 'counter', instanced=True),
+ ]
+
+ def sample(self, samples: Samples) -> None:
+ with open('/proc/stat') as stat:
+ for line in stat:
+ if not line.startswith('cpu'):
+ continue
+ cpu, user, nice, system, _idle, iowait = line.split()[:6]
+ core = cpu[3:] or None
+ if core:
+ prefix = 'cpu.core'
+ samples[f'{prefix}.nice'][core] = int(nice) * MS_PER_JIFFY
+ samples[f'{prefix}.user'][core] = int(user) * MS_PER_JIFFY
+ samples[f'{prefix}.system'][core] = int(system) * MS_PER_JIFFY
+ samples[f'{prefix}.iowait'][core] = int(iowait) * MS_PER_JIFFY
+ else:
+ prefix = 'cpu.basic'
+ samples[f'{prefix}.nice'] = int(nice) * MS_PER_JIFFY
+ samples[f'{prefix}.user'] = int(user) * MS_PER_JIFFY
+ samples[f'{prefix}.system'] = int(system) * MS_PER_JIFFY
+ samples[f'{prefix}.iowait'] = int(iowait) * MS_PER_JIFFY
+
+
+class MemorySampler(Sampler):
+ descriptions = [
+ SampleDescription('memory.free', 'bytes', 'instant', instanced=False),
+ SampleDescription('memory.used', 'bytes', 'instant', instanced=False),
+ SampleDescription('memory.cached', 'bytes', 'instant', instanced=False),
+ SampleDescription('memory.swap-used', 'bytes', 'instant', instanced=False),
+ ]
+
+ def sample(self, samples: Samples) -> None:
+ with open('/proc/meminfo') as meminfo:
+ items = {k: int(v.strip(' kB\n')) for line in meminfo for k, v in [line.split(':', 1)]}
+
+ samples['memory.free'] = 1024 * items['MemFree']
+ samples['memory.used'] = 1024 * (items['MemTotal'] - items['MemAvailable'])
+ samples['memory.cached'] = 1024 * (items['Buffers'] + items['Cached'])
+ samples['memory.swap-used'] = 1024 * (items['SwapTotal'] - items['SwapFree'])
+
+
+class CPUTemperatureSampler(Sampler):
+ # Cache found sensors, as they can't be hotplugged.
+ sensors: Optional[List[str]] = None
+
+ descriptions = [
+ SampleDescription('cpu.temperature', 'celsius', 'instant', instanced=True),
+ ]
+
+ @staticmethod
+ def detect_cpu_sensors(dir_fd: int) -> Iterable[str]:
+ # Read the name file to decide what to do with this directory
+ try:
+ with Handle.open('name', os.O_RDONLY, dir_fd=dir_fd) as fd:
+ name = os.read(fd, 1024).decode().strip()
+ except FileNotFoundError:
+ return
+
+ if name == 'atk0110':
+ # only sample 'CPU Temperature' in atk0110
+ predicate = (lambda label: label == 'CPU Temperature')
+ elif name == 'cpu_thermal':
+ # labels are not used on ARM
+ predicate = None
+ elif name == 'coretemp':
+ # accept all labels on Intel
+ predicate = None
+ elif name in ['k8temp', 'k10temp']:
+ predicate = None
+ else:
+ # Not a CPU sensor
+ return
+
+ # Now scan the directory for inputs
+ for input_filename in os.listdir(dir_fd):
+ if not input_filename.endswith('_input'):
+ continue
+
+ if predicate:
+ # We need to check the label
+ try:
+ label_filename = input_filename.replace('_input', '_label')
+ with Handle.open(label_filename, os.O_RDONLY, dir_fd=dir_fd) as fd:
+ label = os.read(fd, 1024).decode().strip()
+ except FileNotFoundError:
+ continue
+
+ if not predicate(label):
+ continue
+
+ yield input_filename
+
+ @staticmethod
+ def scan_sensors() -> Iterable[str]:
+ try:
+ top_fd = Handle.open(HWMON_PATH, os.O_RDONLY | os.O_DIRECTORY)
+ except FileNotFoundError:
+ return
+
+ with top_fd:
+ for hwmon_name in os.listdir(top_fd):
+ with Handle.open(hwmon_name, os.O_RDONLY | os.O_DIRECTORY, dir_fd=top_fd) as subdir_fd:
+ for sensor in CPUTemperatureSampler.detect_cpu_sensors(subdir_fd):
+ yield f'{HWMON_PATH}/{hwmon_name}/{sensor}'
+
+ def sample(self, samples: Samples) -> None:
+ if self.sensors is None:
+ self.sensors = list(CPUTemperatureSampler.scan_sensors())
+
+ for sensor_path in self.sensors:
+ with open(sensor_path) as sensor:
+ temperature = int(sensor.read().strip())
+ if temperature == 0:
+ return
+
+ samples['cpu.temperature'][sensor_path] = temperature / 1000
+
+
+class DiskSampler(Sampler):
+ descriptions = [
+ SampleDescription('disk.all.read', 'bytes', 'counter', instanced=False),
+ SampleDescription('disk.all.written', 'bytes', 'counter', instanced=False),
+ SampleDescription('disk.dev.read', 'bytes', 'counter', instanced=True),
+ SampleDescription('disk.dev.written', 'bytes', 'counter', instanced=True),
+ ]
+
+ def sample(self, samples: Samples) -> None:
+ with open('/proc/diskstats') as diskstats:
+ all_read_bytes = 0
+ all_written_bytes = 0
+
+ for line in diskstats:
+ # https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
+ fields = line.strip().split()
+ dev_major = fields[0]
+ dev_name = fields[2]
+ num_sectors_read = fields[5]
+ num_sectors_written = fields[9]
+
+ # ignore mdraid
+ if dev_major == '9':
+ continue
+
+ # ignore device-mapper
+ if dev_name.startswith('dm-'):
+ continue
+
+ # Skip partitions
+ if dev_name[:2] in ['sd', 'hd', 'vd'] and dev_name[-1].isdigit():
+ continue
+
+ # Ignore nvme partitions
+ if dev_name.startswith('nvme') and 'p' in dev_name:
+ continue
+
+ read_bytes = int(num_sectors_read) * 512
+ written_bytes = int(num_sectors_written) * 512
+
+ all_read_bytes += read_bytes
+ all_written_bytes += written_bytes
+
+ samples['disk.dev.read'][dev_name] = read_bytes
+ samples['disk.dev.written'][dev_name] = written_bytes
+
+ samples['disk.all.read'] = all_read_bytes
+ samples['disk.all.written'] = all_written_bytes
+
+
+class CGroupSampler(Sampler):
+ descriptions = [
+ SampleDescription('cgroup.memory.usage', 'bytes', 'instant', instanced=True),
+ SampleDescription('cgroup.memory.limit', 'bytes', 'instant', instanced=True),
+ SampleDescription('cgroup.memory.sw-usage', 'bytes', 'instant', instanced=True),
+ SampleDescription('cgroup.memory.sw-limit', 'bytes', 'instant', instanced=True),
+ SampleDescription('cgroup.cpu.usage', 'millisec', 'counter', instanced=True),
+ SampleDescription('cgroup.cpu.shares', 'count', 'instant', instanced=True),
+ ]
+
+ cgroups_v2: Optional[bool] = None
+
+ def sample(self, samples: Samples) -> None:
+ if self.cgroups_v2 is None:
+ self.cgroups_v2 = os.path.exists('/sys/fs/cgroup/cgroup.controllers')
+
+ if self.cgroups_v2:
+ cgroups_v2_path = '/sys/fs/cgroup/'
+ for path, _, _, rootfd in os.fwalk(cgroups_v2_path):
+ cgroup = path.replace(cgroups_v2_path, '')
+
+ if not cgroup:
+ continue
+
+ samples['cgroup.memory.usage'][cgroup] = read_int_file(rootfd, 'memory.current', 0)
+ samples['cgroup.memory.limit'][cgroup] = read_int_file(rootfd, 'memory.max')
+ samples['cgroup.memory.sw-usage'][cgroup] = read_int_file(rootfd, 'memory.swap.current', 0)
+ samples['cgroup.memory.sw-limit'][cgroup] = read_int_file(rootfd, 'memory.swap.max')
+ samples['cgroup.cpu.shares'][cgroup] = read_int_file(rootfd, 'cpu.weight')
+ usage_usec = read_int_file(rootfd, 'cpu.stat', 0, key=b'usage_usec')
+ if usage_usec:
+ samples['cgroup.cpu.usage'][cgroup] = usage_usec / 1000
+ else:
+ memory_path = '/sys/fs/cgroup/memory/'
+ for path, _, _, rootfd in os.fwalk(memory_path):
+ cgroup = path.replace(memory_path, '')
+
+ if not cgroup:
+ continue
+
+ samples['cgroup.memory.usage'][cgroup] = read_int_file(rootfd, 'memory.usage_in_bytes', 0)
+ samples['cgroup.memory.limit'][cgroup] = read_int_file(rootfd, 'memory.limit_in_bytes')
+ samples['cgroup.memory.sw-usage'][cgroup] = read_int_file(rootfd, 'memory.memsw.usage_in_bytes', 0)
+ samples['cgroup.memory.sw-limit'][cgroup] = read_int_file(rootfd, 'memory.memsw.limit_in_bytes')
+
+ cpu_path = '/sys/fs/cgroup/cpu/'
+ for path, _, _, rootfd in os.fwalk(cpu_path):
+ cgroup = path.replace(cpu_path, '')
+
+ if not cgroup:
+ continue
+
+ samples['cgroup.cpu.shares'][cgroup] = read_int_file(rootfd, 'cpu.shares')
+ usage_nsec = read_int_file(rootfd, 'cpuacct.usage')
+ if usage_nsec:
+ samples['cgroup.cpu.usage'][cgroup] = usage_nsec / 1000000
+
+
+class CGroupDiskIO(Sampler):
+ IO_RE = re.compile(rb'\bread_bytes: (?P<read>\d+).*\nwrite_bytes: (?P<write>\d+)', flags=re.S)
+ descriptions = [
+ SampleDescription('disk.cgroup.read', 'bytes', 'counter', instanced=True),
+ SampleDescription('disk.cgroup.written', 'bytes', 'counter', instanced=True),
+ ]
+
+ @staticmethod
+ def get_cgroup_name(fd: int) -> str:
+ with Handle.open('cgroup', os.O_RDONLY, dir_fd=fd) as cgroup_fd:
+ cgroup_name = os.read(cgroup_fd, 2048).decode().strip()
+
+ # Skip leading ::0/
+ return cgroup_name[4:]
+
+ @staticmethod
+ def get_proc_io(fd: int) -> Tuple[int, int]:
+ with Handle.open('io', os.O_RDONLY, dir_fd=fd) as io_fd:
+ data = os.read(io_fd, 4096)
+
+ match = re.search(CGroupDiskIO.IO_RE, data)
+ if match:
+ proc_read = int(match.group('read'))
+ proc_write = int(match.group('write'))
+
+ return proc_read, proc_write
+
+ return 0, 0
+
+ def sample(self, samples: Samples):
+ with Handle.open('/proc', os.O_RDONLY | os.O_DIRECTORY) as proc_fd:
+ reads = samples['disk.cgroup.read']
+ writes = samples['disk.cgroup.written']
+
+ for path in os.listdir(proc_fd):
+ # non-pid entries in proc are guaranteed to start with a character a-z
+ if path[0] < '0' or path[0] > '9':
+ continue
+
+ try:
+ with Handle.open(path, os.O_PATH, dir_fd=proc_fd) as pid_fd:
+ cgroup_name = self.get_cgroup_name(pid_fd)
+ proc_read, proc_write = self.get_proc_io(pid_fd)
+ except (FileNotFoundError, PermissionError, ProcessLookupError):
+ continue
+
+ reads[cgroup_name] = reads.get(cgroup_name, 0) + proc_read
+ writes[cgroup_name] = writes.get(cgroup_name, 0) + proc_write
+
+
+class NetworkSampler(Sampler):
+ descriptions = [
+ SampleDescription('network.interface.tx', 'bytes', 'counter', instanced=True),
+ SampleDescription('network.interface.rx', 'bytes', 'counter', instanced=True),
+ ]
+
+ def sample(self, samples: Samples) -> None:
+ with open("/proc/net/dev") as network_samples:
+ for line in network_samples:
+ fields = line.split()
+
+ # Skip header line
+ if fields[0][-1] != ':':
+ continue
+
+ iface = fields[0][:-1]
+ samples['network.interface.rx'][iface] = int(fields[1])
+ samples['network.interface.tx'][iface] = int(fields[9])
+
+
+class MountSampler(Sampler):
+ descriptions = [
+ SampleDescription('mount.total', 'bytes', 'instant', instanced=True),
+ SampleDescription('mount.used', 'bytes', 'instant', instanced=True),
+ ]
+
+ def sample(self, samples: Samples) -> None:
+ with open('/proc/mounts') as mounts:
+ for line in mounts:
+ # Only look at real devices
+ if line[0] != '/':
+ continue
+
+ path = line.split()[1]
+ try:
+ res = os.statvfs(path)
+ except OSError:
+ continue
+ frsize = res.f_frsize
+ total = frsize * res.f_blocks
+ samples['mount.total'][path] = total
+ samples['mount.used'][path] = total - frsize * res.f_bfree
+
+
+class BlockSampler(Sampler):
+ descriptions = [
+ SampleDescription('block.device.read', 'bytes', 'counter', instanced=True),
+ SampleDescription('block.device.written', 'bytes', 'counter', instanced=True),
+ ]
+
+ def sample(self, samples: Samples) -> None:
+ with open('/proc/diskstats') as diskstats:
+ for line in diskstats:
+ # https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
+ [_, _, dev_name, _, _, sectors_read, _, _, _, sectors_written, *_] = line.strip().split()
+
+ samples['block.device.read'][dev_name] = int(sectors_read) * 512
+ samples['block.device.written'][dev_name] = int(sectors_written) * 512
+
+
+SAMPLERS = [
+ BlockSampler,
+ CGroupSampler,
+ CGroupDiskIO,
+ CPUSampler,
+ CPUTemperatureSampler,
+ DiskSampler,
+ MemorySampler,
+ MountSampler,
+ NetworkSampler,
+]
diff --git a/src/cockpit/superuser.py b/src/cockpit/superuser.py
new file mode 100644
index 0000000..317ee98
--- /dev/null
+++ b/src/cockpit/superuser.py
@@ -0,0 +1,245 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import array
+import asyncio
+import contextlib
+import getpass
+import logging
+import os
+import socket
+from tempfile import TemporaryDirectory
+from typing import List, Optional, Sequence, Tuple
+
+from cockpit._vendor import ferny
+from cockpit._vendor.bei.bootloader import make_bootloader
+from cockpit._vendor.systemd_ctypes import Variant, bus
+
+from .beipack import BridgeBeibootHelper
+from .jsonutil import JsonObject, get_str
+from .packages import BridgeConfig
+from .peer import ConfiguredPeer, Peer, PeerError
+from .polkit import PolkitAgent
+from .router import Router, RoutingError, RoutingRule
+
+logger = logging.getLogger(__name__)
+
+
+class SuperuserPeer(ConfiguredPeer):
+ responder: ferny.AskpassHandler
+
+ def __init__(self, router: Router, config: BridgeConfig, responder: ferny.AskpassHandler):
+ super().__init__(router, config)
+ self.responder = responder
+
+ async def do_connect_transport(self) -> None:
+ async with contextlib.AsyncExitStack() as context:
+ if 'pkexec' in self.args:
+ logger.debug('connecting polkit superuser peer transport %r', self.args)
+ await context.enter_async_context(PolkitAgent(self.responder))
+ else:
+ logger.debug('connecting non-polkit superuser peer transport %r', self.args)
+
+ responders: 'list[ferny.InteractionHandler]' = [self.responder]
+
+ if '# cockpit-bridge' in self.args:
+ logger.debug('going to beiboot superuser bridge %r', self.args)
+ helper = BridgeBeibootHelper(self, ['--privileged'])
+ responders.append(helper)
+ stage1 = make_bootloader(helper.steps, gadgets=ferny.BEIBOOT_GADGETS).encode()
+ else:
+ stage1 = None
+
+ agent = ferny.InteractionAgent(responders)
+
+ if 'SUDO_ASKPASS=ferny-askpass' in self.env:
+ tmpdir = context.enter_context(TemporaryDirectory())
+ ferny_askpass = ferny.write_askpass_to_tmpdir(tmpdir)
+ env: Sequence[str] = [f'SUDO_ASKPASS={ferny_askpass}']
+ else:
+ env = self.env
+
+ transport = await self.spawn(self.args, env, stderr=agent, start_new_session=True)
+
+ if stage1 is not None:
+ transport.write(stage1)
+
+ try:
+ await agent.communicate()
+ except ferny.InteractionError as exc:
+ raise PeerError('authentication-failed', message=str(exc)) from exc
+
+
+class CockpitResponder(ferny.AskpassHandler):
+ commands = ('ferny.askpass', 'cockpit.send-stderr')
+
+ async def do_custom_command(self, command: str, args: Tuple, fds: List[int], stderr: str) -> None:
+ if command == 'cockpit.send-stderr':
+ with socket.socket(fileno=fds[0]) as sock:
+ fds.pop(0)
+ # socket.send_fds(sock, [b'\0'], [2]) # New in Python 3.9
+ sock.sendmsg([b'\0'], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", [2]))])
+
+
+class AuthorizeResponder(CockpitResponder):
+ def __init__(self, router: Router):
+ self.router = router
+
+ async def do_askpass(self, messages: str, prompt: str, hint: str) -> str:
+ hexuser = ''.join(f'{c:02x}' for c in getpass.getuser().encode('ascii'))
+ return await self.router.request_authorization(f'plain1:{hexuser}')
+
+
+class SuperuserRoutingRule(RoutingRule, CockpitResponder, bus.Object, interface='cockpit.Superuser'):
+ superuser_configs: Sequence[BridgeConfig] = ()
+ pending_prompt: Optional[asyncio.Future]
+ peer: Optional[SuperuserPeer]
+
+ # D-Bus signals
+ prompt = bus.Interface.Signal('s', 's', 's', 'b', 's') # message, prompt, default, echo, error
+
+ # D-Bus properties
+ bridges = bus.Interface.Property('as', value=[])
+ current = bus.Interface.Property('s', value='none')
+ methods = bus.Interface.Property('a{sv}', value={})
+
+ # RoutingRule
+ def apply_rule(self, options: JsonObject) -> Optional[Peer]:
+ superuser = options.get('superuser')
+
+ if not superuser or self.current == 'root':
+ # superuser not requested, or already superuser? Next rule.
+ return None
+ elif self.peer or superuser == 'try':
+ # superuser requested and active? Return it.
+ # 'try' requested? Either return the peer, or None.
+ return self.peer
+ else:
+ # superuser requested, but not active? That's an error.
+ raise RoutingError('access-denied')
+
+ # ferny.AskpassHandler
+ async def do_askpass(self, messages: str, prompt: str, hint: str) -> Optional[str]:
+ assert self.pending_prompt is None
+ echo = hint == "confirm"
+ self.pending_prompt = asyncio.get_running_loop().create_future()
+ try:
+ logger.debug('prompting for %s', prompt)
+ # with sudo, all stderr messages are treated as warning/errors by the UI
+ # (such as the lecture or "wrong password"), so pass them in the "error" field
+ self.prompt('', prompt, '', echo, messages)
+ return await self.pending_prompt
+ finally:
+ self.pending_prompt = None
+
+ def __init__(self, router: Router, *, privileged: bool = False):
+ super().__init__(router)
+
+ self.pending_prompt = None
+ self.peer = None
+ self.startup = None
+
+ if privileged or os.getuid() == 0:
+ self.current = 'root'
+
+ def peer_done(self):
+ self.current = 'none'
+ self.peer = None
+
+ async def go(self, name: str, responder: ferny.AskpassHandler) -> None:
+ if self.current != 'none':
+ raise bus.BusError('cockpit.Superuser.Error', 'Superuser bridge already running')
+
+ assert self.peer is None
+ assert self.startup is None
+
+ for config in self.superuser_configs:
+ if name in (config.name, 'any'):
+ break
+ else:
+ raise bus.BusError('cockpit.Superuser.Error', f'Unknown superuser bridge type "{name}"')
+
+ self.current = 'init'
+ self.peer = SuperuserPeer(self.router, config, responder)
+ self.peer.add_done_callback(self.peer_done)
+
+ try:
+ await self.peer.start(init_host=self.router.init_host)
+ except asyncio.CancelledError:
+ raise bus.BusError('cockpit.Superuser.Error.Cancelled', 'Operation aborted') from None
+ except (OSError, PeerError) as exc:
+ raise bus.BusError('cockpit.Superuser.Error', str(exc)) from exc
+
+ self.current = self.peer.config.name
+
+ def set_configs(self, configs: Sequence[BridgeConfig]):
+ logger.debug("set_configs() with %d items", len(configs))
+ configs = [config for config in configs if config.privileged]
+ self.superuser_configs = tuple(configs)
+ self.bridges = [config.name for config in self.superuser_configs]
+ self.methods = {c.label: Variant({'label': Variant(c.label)}, 'a{sv}') for c in configs if c.label}
+
+ logger.debug(" bridges are now %s", self.bridges)
+
+ # If the currently active bridge config is not in the new set of configs, stop it
+ if self.peer is not None:
+ if self.peer.config not in self.superuser_configs:
+ logger.debug(" stopping superuser bridge '%s': it disappeared from configs", self.peer.config.name)
+ self.stop()
+
+ def cancel_prompt(self):
+ if self.pending_prompt is not None:
+ self.pending_prompt.cancel()
+ self.pending_prompt = None
+
+ def shutdown(self):
+ self.cancel_prompt()
+
+ if self.peer is not None:
+ self.peer.close()
+
+ # close() should have disconnected the peer immediately
+ assert self.peer is None
+
+ # Connect-on-startup functionality
+ def init(self, params: JsonObject) -> None:
+ name = get_str(params, 'id', 'any')
+ responder = AuthorizeResponder(self.router)
+ self._init_task = asyncio.create_task(self.go(name, responder))
+ self._init_task.add_done_callback(self._init_done)
+
+ def _init_done(self, task: 'asyncio.Task[None]') -> None:
+ logger.debug('superuser init done! %s', task.exception())
+ self.router.write_control(command='superuser-init-done')
+ del self._init_task
+
+ # D-Bus methods
+ @bus.Interface.Method(in_types=['s'])
+ async def start(self, name: str) -> None:
+ await self.go(name, self)
+
+ @bus.Interface.Method()
+ def stop(self) -> None:
+ self.shutdown()
+
+ @bus.Interface.Method(in_types=['s'])
+ def answer(self, reply: str) -> None:
+ if self.pending_prompt is not None:
+ logger.debug('responding to pending prompt')
+ self.pending_prompt.set_result(reply)
+ else:
+ logger.debug('got Answer, but no prompt pending')
diff --git a/src/cockpit/transports.py b/src/cockpit/transports.py
new file mode 100644
index 0000000..8aabc7d
--- /dev/null
+++ b/src/cockpit/transports.py
@@ -0,0 +1,552 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+"""Bi-directional asyncio.Transport implementations based on file descriptors."""
+
+import asyncio
+import collections
+import ctypes
+import errno
+import fcntl
+import logging
+import os
+import select
+import signal
+import struct
+import subprocess
+import termios
+from typing import Any, ClassVar, Sequence
+
+from .jsonutil import JsonObject, get_int
+
+libc6 = ctypes.cdll.LoadLibrary('libc.so.6')
+
+
+def prctl(*args: int) -> None:
+ if libc6.prctl(*args) != 0:
+ raise OSError('prctl() failed')
+
+
+SET_PDEATHSIG = 1
+
+
+logger = logging.getLogger(__name__)
+IOV_MAX = 1024 # man 2 writev
+
+
+class _Transport(asyncio.Transport):
+ BLOCK_SIZE: ClassVar[int] = 1024 * 1024
+
+ # A transport always has a loop and a protocol
+ _loop: asyncio.AbstractEventLoop
+ _protocol: asyncio.Protocol
+
+ _queue: 'collections.deque[bytes] | None'
+ _in_fd: int
+ _out_fd: int
+ _closing: bool
+ _is_reading: bool
+ _eof: bool
+ _eio_is_eof: bool = False
+
+ def __init__(self,
+ loop: asyncio.AbstractEventLoop,
+ protocol: asyncio.Protocol,
+ in_fd: int = -1, out_fd: int = -1,
+ extra: 'dict[str, object] | None' = None):
+ super().__init__(extra)
+
+ self._loop = loop
+ self._protocol = protocol
+
+ logger.debug('Created transport %s for protocol %s, fds %d %d', self, protocol, in_fd, out_fd)
+
+ self._queue = None
+ self._is_reading = False
+ self._eof = False
+ self._closing = False
+
+ self._in_fd = in_fd
+ self._out_fd = out_fd
+
+ os.set_blocking(in_fd, False)
+ if out_fd != in_fd:
+ os.set_blocking(out_fd, False)
+
+ self._protocol.connection_made(self)
+ self.resume_reading()
+
+ def _read_ready(self) -> None:
+ logger.debug('Read ready on %s %s %d', self, self._protocol, self._in_fd)
+ try:
+ data = os.read(self._in_fd, _Transport.BLOCK_SIZE)
+ except BlockingIOError: # pragma: no cover
+ return
+ except OSError as exc:
+ if self._eio_is_eof and exc.errno == errno.EIO:
+ # PTY devices return EIO to mean "EOF"
+ data = b''
+ else:
+ # Other errors: terminate the connection
+ self.abort(exc)
+ return
+
+ if data != b'':
+ logger.debug(' read %d bytes', len(data))
+ self._protocol.data_received(data)
+ else:
+ logger.debug(' got EOF')
+ self._close_reader()
+ keep_open = self._protocol.eof_received()
+ if not keep_open:
+ self.close()
+
+ def is_reading(self) -> bool:
+ return self._is_reading
+
+ def _close_reader(self) -> None:
+ self.pause_reading()
+ self._in_fd = -1
+
+ def pause_reading(self) -> None:
+ if self._is_reading:
+ self._loop.remove_reader(self._in_fd)
+ self._is_reading = False
+
+ def resume_reading(self) -> None:
+ # It's possible that the Protocol could decide to attempt to unpause
+ # reading after _close_reader() got called. Check that the fd is != -1
+ # before actually resuming.
+ if not self._is_reading and self._in_fd != -1:
+ self._loop.add_reader(self._in_fd, self._read_ready)
+ self._is_reading = True
+
+ def _close(self) -> None:
+ pass
+
+ def abort(self, exc: 'Exception | None' = None) -> None:
+ self._closing = True
+ self._close_reader()
+ self._remove_write_queue()
+ self._protocol.connection_lost(exc)
+ self._close()
+
+ def can_write_eof(self) -> bool:
+ raise NotImplementedError
+
+ def write_eof(self) -> None:
+ assert not self._eof
+ self._eof = True
+ if self._queue is None:
+ logger.debug('%s got EOF. closing backend.', self)
+ self._write_eof_now()
+ else:
+ logger.debug('%s got EOF. bytes in queue, deferring close', self)
+
+ def get_write_buffer_size(self) -> int:
+ if self._queue is None:
+ return 0
+ return sum(len(block) for block in self._queue)
+
+ def get_write_buffer_limits(self) -> 'tuple[int, int]':
+ return (0, 0)
+
+ def set_write_buffer_limits(self, high: 'int | None' = None, low: 'int | None' = None) -> None:
+ assert high is None or high == 0
+ assert low is None or low == 0
+
+ def _write_eof_now(self) -> None:
+ raise NotImplementedError
+
+ def _write_ready(self) -> None:
+ logger.debug('%s _write_ready', self)
+ assert self._queue is not None
+
+ try:
+ n_bytes = os.writev(self._out_fd, self._queue)
+ except BlockingIOError: # pragma: no cover
+ n_bytes = 0
+ except OSError as exc:
+ self.abort(exc)
+ return
+
+ logger.debug(' successfully wrote %d bytes from the queue', n_bytes)
+
+ while n_bytes:
+ block = self._queue.popleft()
+ if len(block) > n_bytes:
+ # This block wasn't completely written.
+ logger.debug(' incomplete block. Stop.')
+ self._queue.appendleft(block[n_bytes:])
+ break
+ n_bytes -= len(block)
+ logger.debug(' removed complete block. %d remains.', n_bytes)
+
+ if not self._queue:
+ logger.debug('%s queue drained.')
+ self._remove_write_queue()
+ if self._eof:
+ logger.debug('%s queue drained. closing backend now.')
+ self._write_eof_now()
+ if self._closing:
+ self.abort()
+
+ def _remove_write_queue(self) -> None:
+ if self._queue is not None:
+ self._protocol.resume_writing()
+ self._loop.remove_writer(self._out_fd)
+ self._queue = None
+
+ def _create_write_queue(self, data: bytes) -> None:
+ logger.debug('%s creating write queue for fd %s', self, self._out_fd)
+ assert self._queue is None
+ self._loop.add_writer(self._out_fd, self._write_ready)
+ self._queue = collections.deque((data,))
+ self._protocol.pause_writing()
+
+ def write(self, data: bytes) -> None:
+ # this is a race condition with subprocesses: if we get and process the the "exited"
+ # event before seeing BrokenPipeError, we'll try to write to a closed pipe.
+ # Do what the standard library does and ignore, instead of assert
+ if self._closing:
+ logger.debug('ignoring write() to closing transport fd %i', self._out_fd)
+ return
+
+ assert not self._eof
+
+ if self._queue is not None:
+ self._queue.append(data)
+
+ # writev() will complain if the queue is too long. Consolidate it.
+ if len(self._queue) > IOV_MAX:
+ all_data = b''.join(self._queue)
+ self._queue.clear()
+ self._queue.append(all_data)
+
+ return
+
+ try:
+ n_bytes = os.write(self._out_fd, data)
+ except BlockingIOError:
+ n_bytes = 0
+ except OSError as exc:
+ self.abort(exc)
+ return
+
+ if n_bytes != len(data):
+ self._create_write_queue(data[n_bytes:])
+
+ def close(self) -> None:
+ if self._closing:
+ return
+
+ self._closing = True
+ self._close_reader()
+
+ if self._queue is not None:
+ # abort() will be called from _write_ready() when it's done
+ return
+
+ self.abort()
+
+ def get_protocol(self) -> asyncio.BaseProtocol:
+ return self._protocol
+
+ def is_closing(self) -> bool:
+ return self._closing
+
+ def set_protocol(self, protocol: asyncio.BaseProtocol) -> None:
+ raise NotImplementedError
+
+ def __del__(self) -> None:
+ self._close()
+
+
+class SubprocessProtocol(asyncio.Protocol):
+ """An extension to asyncio.Protocol for use with SubprocessTransport."""
+ def process_exited(self) -> None:
+ """Called when subprocess has exited."""
+ raise NotImplementedError
+
+
+class WindowSize:
+ def __init__(self, value: JsonObject):
+ self.rows = get_int(value, 'rows')
+ self.cols = get_int(value, 'cols')
+
+
+class SubprocessTransport(_Transport, asyncio.SubprocessTransport):
+ """A bi-directional transport speaking with stdin/out of a subprocess.
+
+ Note: this is not really a normal SubprocessTransport. Although it
+ implements the entire API of asyncio.SubprocessTransport, it is not
+ designed to be used with asyncio.SubprocessProtocol objects. Instead, it
+ pair with normal Protocol objects which also implement the
+ SubprocessProtocol defined in this module (which only has a
+ process_exited() method). Whatever the protocol writes is sent to stdin,
+ and whatever comes from stdout is given to the Protocol via the
+ .data_received() function.
+
+ If stderr is configured as a pipe, the transport will separately collect
+ data from it, making it available via the .get_stderr() method.
+ """
+
+ _returncode: 'int | None' = None
+
+ _pty_fd: 'int | None' = None
+ _process: 'subprocess.Popen[bytes] | None' = None
+ _stderr: 'Spooler | None'
+
+ @staticmethod
+ def _create_watcher() -> asyncio.AbstractChildWatcher:
+ try:
+ os.close(os.pidfd_open(os.getpid(), 0)) # check for kernel support
+ return asyncio.PidfdChildWatcher()
+ except (AttributeError, OSError):
+ pass
+
+ return asyncio.SafeChildWatcher()
+
+ @staticmethod
+ def _get_watcher(loop: asyncio.AbstractEventLoop) -> asyncio.AbstractChildWatcher:
+ quark = '_cockpit_transports_child_watcher'
+ watcher = getattr(loop, quark, None)
+
+ if watcher is None:
+ watcher = SubprocessTransport._create_watcher()
+ watcher.attach_loop(loop)
+ setattr(loop, quark, watcher)
+
+ return watcher
+
+ def get_stderr(self, *, reset: bool = False) -> str:
+ if self._stderr is not None:
+ return self._stderr.get(reset=reset).decode(errors='replace')
+ else:
+ return ''
+
+ def _exited(self, pid: int, code: int) -> None:
+ # NB: per AbstractChildWatcher API, this handler should be thread-safe,
+ # but we only ever use non-threaded child watcher implementations, so
+ # we can assume we'll always be called in the main thread.
+
+ # NB: the subprocess is going to want to waitpid() itself as well, but
+ # will get ECHILD since we already reaped it. Fortunately, since
+ # Python 3.2 this is supported, and process gets a return status of
+ # zero. For that reason, we need to store our own copy of the return
+ # status. See https://github.com/python/cpython/issues/59960
+ assert isinstance(self._protocol, SubprocessProtocol)
+ assert self._process is not None
+ assert self._process.pid == pid
+ self._returncode = code
+ logger.debug('Process exited with status %d', self._returncode)
+ if not self._closing:
+ self._protocol.process_exited()
+
+ def __init__(self,
+ loop: asyncio.AbstractEventLoop,
+ protocol: SubprocessProtocol,
+ args: Sequence[str],
+ *,
+ pty: bool = False,
+ window: 'WindowSize | None' = None,
+ **kwargs: Any):
+
+ # go down as a team -- we don't want any leaked processes when the bridge terminates
+ def preexec_fn() -> None:
+ prctl(SET_PDEATHSIG, signal.SIGTERM)
+ if pty:
+ fcntl.ioctl(0, termios.TIOCSCTTY, 0)
+
+ if pty:
+ self._pty_fd, session_fd = os.openpty()
+
+ if window is not None:
+ self.set_window_size(window)
+
+ kwargs['stderr'] = session_fd
+ self._process = subprocess.Popen(args,
+ stdin=session_fd, stdout=session_fd,
+ preexec_fn=preexec_fn, start_new_session=True, **kwargs)
+ os.close(session_fd)
+
+ in_fd, out_fd = self._pty_fd, self._pty_fd
+ self._eio_is_eof = True
+
+ else:
+ self._process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ preexec_fn=preexec_fn, **kwargs)
+ assert self._process.stdin
+ assert self._process.stdout
+ in_fd = self._process.stdout.fileno()
+ out_fd = self._process.stdin.fileno()
+
+ if self._process.stderr is not None:
+ self._stderr = Spooler(loop, self._process.stderr.fileno())
+ else:
+ self._stderr = None
+
+ super().__init__(loop, protocol, in_fd, out_fd)
+
+ self._get_watcher(loop).add_child_handler(self._process.pid, self._exited)
+
+ def set_window_size(self, size: WindowSize) -> None:
+ assert self._pty_fd is not None
+ fcntl.ioctl(self._pty_fd, termios.TIOCSWINSZ, struct.pack('2H4x', size.rows, size.cols))
+
+ def can_write_eof(self) -> bool:
+ assert self._process is not None
+ return self._process.stdin is not None
+
+ def _write_eof_now(self) -> None:
+ assert self._process is not None
+ assert self._process.stdin is not None
+ self._process.stdin.close()
+ self._out_fd = -1
+
+ def get_pid(self) -> int:
+ assert self._process is not None
+ return self._process.pid
+
+ def get_returncode(self) -> 'int | None':
+ return self._returncode
+
+ def get_pipe_transport(self, fd: int) -> asyncio.Transport:
+ raise NotImplementedError
+
+ def send_signal(self, sig: signal.Signals) -> None: # type: ignore[override] # mypy/issues/13885
+ assert self._process is not None
+ # We try to avoid using subprocess.send_signal(). It contains a call
+ # to waitpid() internally to avoid signalling the wrong process (if a
+ # PID gets reused), but:
+ #
+ # - we already detect the process exiting via our PidfdChildWatcher
+ #
+ # - the check is actually harmful since collecting the process via
+ # waitpid() prevents the PidfdChildWatcher from doing the same,
+ # resulting in an error.
+ #
+ # It's on us now to check it, but that's easy:
+ if self._returncode is not None:
+ logger.debug("won't attempt %s to process %i. It exited already.", sig, self._process.pid)
+ return
+
+ try:
+ os.kill(self._process.pid, sig)
+ logger.debug('sent %s to process %i', sig, self._process.pid)
+ except ProcessLookupError:
+ # already gone? fine
+ logger.debug("can't send %s to process %i. It's exited just now.", sig, self._process.pid)
+
+ def terminate(self) -> None:
+ self.send_signal(signal.SIGTERM)
+
+ def kill(self) -> None:
+ self.send_signal(signal.SIGKILL)
+
+ def _close(self) -> None:
+ if self._pty_fd is not None:
+ os.close(self._pty_fd)
+ self._pty_fd = None
+
+ if self._process is not None:
+ if self._process.stdin is not None:
+ self._process.stdin.close()
+ self._process.stdin = None
+ try:
+ self.terminate() # best effort...
+ except PermissionError:
+ logger.debug("can't kill %i due to EPERM", self._process.pid)
+
+
+class StdioTransport(_Transport):
+ """A bi-directional transport that corresponds to stdin/out.
+
+ Can talk to just about anything:
+ - files
+ - pipes
+ - character devices (including terminals)
+ - sockets
+ """
+
+ def __init__(self, loop: asyncio.AbstractEventLoop, protocol: asyncio.Protocol, stdin: int = 0, stdout: int = 1):
+ super().__init__(loop, protocol, stdin, stdout)
+
+ def can_write_eof(self) -> bool:
+ return False
+
+ def _write_eof_now(self) -> None:
+ raise RuntimeError("Can't write EOF to stdout")
+
+
+class Spooler:
+ """Consumes data from an fd, storing it in a buffer.
+
+ This makes a copy of the fd, so you don't have to worry about holding it
+ open.
+ """
+
+ _loop: asyncio.AbstractEventLoop
+ _fd: int
+ _contents: 'list[bytes]'
+
+ def __init__(self, loop: asyncio.AbstractEventLoop, fd: int):
+ self._loop = loop
+ self._fd = -1 # in case dup() raises an exception
+ self._contents = []
+
+ self._fd = os.dup(fd)
+
+ os.set_blocking(self._fd, False)
+ loop.add_reader(self._fd, self._read_ready)
+
+ def _read_ready(self) -> None:
+ try:
+ data = os.read(self._fd, 8192)
+ except BlockingIOError: # pragma: no cover
+ return
+ except OSError:
+ # all other errors -> EOF
+ data = b''
+
+ if data != b'':
+ self._contents.append(data)
+ else:
+ self.close()
+
+ def _is_ready(self) -> bool:
+ if self._fd == -1:
+ return False
+ return select.select([self._fd], [], [], 0) != ([], [], [])
+
+ def get(self, *, reset: bool = False) -> bytes:
+ while self._is_ready():
+ self._read_ready()
+
+ result = b''.join(self._contents)
+ if reset:
+ self._contents = []
+ return result
+
+ def close(self) -> None:
+ if self._fd != -1:
+ self._loop.remove_reader(self._fd)
+ os.close(self._fd)
+ self._fd = -1
+
+ def __del__(self) -> None:
+ self.close()
diff --git a/src/common/Makefile-common.am b/src/common/Makefile-common.am
new file mode 100644
index 0000000..e7d1ca0
--- /dev/null
+++ b/src/common/Makefile-common.am
@@ -0,0 +1,219 @@
+# -----------------------------------------------------------------------------
+# libcockpit-common-nodeps.a: code that has no dependencies other than libc
+
+noinst_LIBRARIES += libcockpit-common-nodeps.a
+
+libcockpit_common_nodeps_a_LIBS = libcockpit-common-nodeps.a
+
+libcockpit_common_nodeps_a_SOURCES = \
+ src/common/cockpitauthorize.c \
+ src/common/cockpitauthorize.h \
+ src/common/cockpitbase64.c \
+ src/common/cockpitbase64.h \
+ src/common/cockpitconf.h \
+ src/common/cockpitconf.c \
+ src/common/cockpitfdpassing.c \
+ src/common/cockpitfdpassing.h \
+ src/common/cockpitframe.c \
+ src/common/cockpitframe.h \
+ src/common/cockpithacks.h \
+ src/common/cockpithex.c \
+ src/common/cockpithex.h \
+ src/common/cockpitjsonprint.c \
+ src/common/cockpitjsonprint.h \
+ src/common/cockpitmemory.c \
+ src/common/cockpitmemory.h \
+ src/common/cockpitwebcertificate.h \
+ src/common/cockpitwebcertificate.c \
+ $(NULL)
+
+# -----------------------------------------------------------------------------
+# libcockpit-common.a: code that has other dependencies, like glib or libsystemd
+
+noinst_LIBRARIES += libcockpit-common.a
+
+libcockpit_common_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-protocol\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libcockpit_common_a_LIBS = \
+ libcockpit-common.a \
+ $(libcockpit_common_nodeps_a_LIBS) \
+ $(libwebsocket_a_LIBS) \
+ $(json_glib_LIBS) \
+ $(libsystemd_LIBS) \
+ -lutil \
+ $(NULL)
+
+libcockpit_common_a_SOURCES = \
+ src/common/cockpitchannel.c \
+ src/common/cockpitchannel.h \
+ src/common/cockpitclosefrom.c \
+ src/common/cockpitcontrolmessages.c \
+ src/common/cockpitcontrolmessages.h \
+ src/common/cockpiterror.c \
+ src/common/cockpiterror.h \
+ src/common/cockpitflow.c \
+ src/common/cockpitflow.h \
+ src/common/cockpithacks-glib.h \
+ src/common/cockpithash.c \
+ src/common/cockpithash.h \
+ src/common/cockpitjson.c \
+ src/common/cockpitjson.h \
+ src/common/cockpitlocale.c \
+ src/common/cockpitlocale.h \
+ src/common/cockpitloopback.c \
+ src/common/cockpitloopback.h \
+ src/common/cockpitmachinesjson.c \
+ src/common/cockpitmachinesjson.h \
+ src/common/cockpitmemfdread.c \
+ src/common/cockpitmemfdread.h \
+ src/common/cockpitpipe.c \
+ src/common/cockpitpipe.h \
+ src/common/cockpitpipetransport.c \
+ src/common/cockpitpipetransport.h \
+ src/common/cockpitsocket.c \
+ src/common/cockpitsocket.h \
+ src/common/cockpitsystem.c \
+ src/common/cockpitsystem.h \
+ src/common/cockpittemplate.c \
+ src/common/cockpittemplate.h \
+ src/common/cockpittransport.c \
+ src/common/cockpittransport.h \
+ src/common/cockpitunicode.c \
+ src/common/cockpitunicode.h \
+ src/common/cockpitunixsignal.c \
+ src/common/cockpitunixsignal.h \
+ src/common/cockpitversion.c \
+ src/common/cockpitversion.h \
+ src/common/cockpitwebfilter.c \
+ src/common/cockpitwebfilter.h \
+ src/common/cockpitwebinject.c \
+ src/common/cockpitwebinject.h \
+ src/common/cockpitwebrequest-private.h \
+ src/common/cockpitwebresponse.c \
+ src/common/cockpitwebresponse.h \
+ src/common/cockpitwebserver.c \
+ src/common/cockpitwebserver.h \
+ $(NULL)
+
+# libcockpit-common.a static-links an HTML template to use on failures
+nodist_libcockpit_common_a_SOURCES = src/common/fail.html.c
+src/common/fail.html.c: src/common/fail.html
+ $(AM_V_GEN) $(top_srcdir)/tools/escape-to-c cockpit_webresponse_fail_html_text < $< > $@.tmp && mv $@.tmp $@
+CLEANFILES += src/common/fail.html.c
+EXTRA_DIST += src/common/fail.html
+
+# -----------------------------------------------------------------------------
+# Unit tests
+
+dist_check_SCRIPTS += src/common/mock-stderr
+dist_check_DATA += src/common/mock-content
+
+# preload wrapper library for unit tests that need a temp home dir
+check_PROGRAMS += libpreload-temp-home.so
+libpreload_temp_home_so_SOURCES = src/common/preload-temp-home.c
+libpreload_temp_home_so_CFLAGS = -fPIC $(AM_CFLAGS)
+libpreload_temp_home_so_LDFLAGS = -shared
+
+TEST_PROGRAM += test-authorize
+test_authorize_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_authorize_LDADD = $(TEST_LIBS)
+test_authorize_SOURCES = src/common/test-authorize.c
+
+TEST_PROGRAM += test-base64
+test_base64_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_base64_LDADD = $(TEST_LIBS)
+test_base64_SOURCES = src/common/test-base64.c
+
+TEST_PROGRAM += test-channel
+test_channel_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_channel_LDADD = $(TEST_LIBS)
+test_channel_SOURCES = src/common/test-channel.c
+
+TEST_PROGRAM += test-config
+test_config_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_config_LDADD = $(TEST_LIBS)
+test_config_SOURCES = src/common/test-config.c
+
+TEST_PROGRAM += test-frame
+test_frame_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_frame_LDADD = $(TEST_LIBS)
+test_frame_SOURCES = src/common/test-frame.c
+
+TEST_PROGRAM += test-hash
+test_hash_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_hash_LDADD = $(TEST_LIBS)
+test_hash_SOURCES = src/common/test-hash.c
+
+TEST_PROGRAM += test-hex
+test_hex_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_hex_LDADD = $(TEST_LIBS)
+test_hex_SOURCES = src/common/test-hex.c
+
+TEST_PROGRAM += test-json
+test_json_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_json_LDADD = $(TEST_LIBS)
+test_json_SOURCES = src/common/test-json.c
+
+TEST_PROGRAM += test-jsonfds
+test_jsonfds_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_jsonfds_LDADD = $(TEST_LIBS)
+test_jsonfds_SOURCES = src/common/test-jsonfds.c
+
+TEST_PROGRAM += test-locale
+test_locale_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_locale_LDADD = $(TEST_LIBS)
+test_locale_SOURCES = src/common/test-locale.c
+
+TEST_PROGRAM += test-pipe
+test_pipe_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_pipe_LDADD = $(TEST_LIBS)
+test_pipe_SOURCES = src/common/test-pipe.c
+
+TEST_PROGRAM += test-system
+test_system_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_system_LDADD = $(TEST_LIBS)
+test_system_SOURCES = src/common/test-system.c
+
+TEST_PROGRAM += test-template
+test_template_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_template_LDADD = $(TEST_LIBS)
+test_template_SOURCES = src/common/test-template.c
+
+TEST_PROGRAM += test-transport
+test_transport_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_transport_LDADD = $(TEST_LIBS)
+test_transport_SOURCES = src/common/test-transport.c
+
+TEST_PROGRAM += test-unicode
+test_unicode_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_unicode_LDADD = $(TEST_LIBS)
+test_unicode_SOURCES = src/common/test-unicode.c
+
+TEST_PROGRAM += test-unixsignal
+test_unixsignal_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_unixsignal_LDADD = $(TEST_LIBS)
+test_unixsignal_SOURCES = src/common/test-unixsignal.c
+
+TEST_PROGRAM += test-version
+test_version_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_version_LDADD = $(TEST_LIBS)
+test_version_SOURCES = src/common/test-version.c
+
+TEST_PROGRAM += test-webcertificate
+test_webcertificate_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_webcertificate_LDADD = $(TEST_LIBS)
+test_webcertificate_SOURCES = src/common/test-webcertificate.c
+
+TEST_PROGRAM += test-webresponse
+test_webresponse_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_webresponse_LDADD = $(TEST_LIBS)
+test_webresponse_SOURCES = src/common/test-webresponse.c
+
+TEST_PROGRAM += test-webserver
+test_webserver_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+test_webserver_LDADD = $(TEST_LIBS)
+test_webserver_SOURCES = src/common/test-webserver.c
diff --git a/src/common/cockpitauthorize.c b/src/common/cockpitauthorize.c
new file mode 100644
index 0000000..b0404b9
--- /dev/null
+++ b/src/common/cockpitauthorize.c
@@ -0,0 +1,598 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitauthorize.h"
+#include "cockpitbase64.h"
+#include "cockpitmemory.h"
+
+#include <assert.h>
+#include <ctype.h>
+#include <crypt.h>
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/* ----------------------------------------------------------------------------
+ * Tools
+ */
+
+#ifndef debug
+#define debug(format, ...) \
+ do { if (logger_verbose) \
+ message ("debug: " format, ##__VA_ARGS__); \
+ } while (0)
+#endif
+
+static int logger_verbose = 0;
+static void (* logger) (const char *data);
+
+#ifndef message
+#if __GNUC__ > 2
+static void
+message (const char *format, ...)
+__attribute__((__format__(__printf__, 1, 2)));
+#endif
+
+static void
+message (const char *format, ...)
+{
+ va_list va;
+ char *data;
+ int res;
+
+ if (!logger)
+ return;
+
+ /* Fast path for simple messages */
+ if (!strchr (format, '%'))
+ {
+ logger (format);
+ return;
+ }
+
+ va_start (va, format);
+ res = vasprintf (&data, format, va);
+ va_end (va);
+
+ if (res < 0)
+ {
+ logger ("out of memory printing message");
+ return;
+ }
+
+ logger (data);
+ free (data);
+}
+#endif
+
+void
+cockpit_authorize_logger (void (* func) (const char *data),
+ int verbose)
+{
+ logger_verbose = verbose;
+ logger = func;
+}
+
+void *
+cockpit_authorize_nonce (size_t length)
+{
+ unsigned char *key;
+ int errn = 0;
+ int fd;
+ ssize_t read_bytes;
+ ssize_t read_result;
+
+ fd = open ("/dev/urandom", O_RDONLY, 0);
+ if (fd < 0)
+ return NULL;
+
+ key = malloc (length);
+ if (!key)
+ {
+ close (fd);
+ errno = ENOMEM;
+ return NULL;
+ }
+
+ read_bytes = 0;
+ do
+ {
+ errno = 0;
+ read_result = read (fd, key + read_bytes, length - read_bytes);
+ if (read_result <= 0)
+ {
+ if (errno == EAGAIN || errno == EINTR)
+ continue;
+ errn = errno;
+ break;
+ }
+ read_bytes += read_result;
+ }
+ while (read_bytes < length);
+ close (fd);
+
+ if (read_bytes < length)
+ {
+ free (key);
+ key = NULL;
+ errno = errn;
+ }
+
+ return key;
+}
+
+const char *
+cockpit_authorize_type (const char *challenge,
+ char **type)
+{
+ size_t i, len = 0;
+
+ /*
+ * Either a space or a colon is the delimiter
+ * that splits the type from the remainder
+ * of the content.
+ */
+ if (challenge)
+ len = strcspn (challenge, ": ");
+ if (len == 0)
+ {
+ debug ("invalid \"authorize\" message");
+ errno = EINVAL;
+ return NULL;
+ }
+
+ if (type)
+ {
+ *type = strndup (challenge, len);
+ if (*type == NULL)
+ {
+ message ("couldn't allocate memory for \"authorize\" challenge");
+ errno = ENOMEM;
+ return NULL;
+ }
+ for (i = 0; i < len; i++)
+ (*type)[i] = tolower ((*type)[i]);
+ }
+
+ if (challenge[len])
+ len++;
+ while (challenge[len] == ' ')
+ len++;
+ return challenge + len;
+}
+
+const char *
+cockpit_authorize_subject (const char *challenge,
+ char **subject)
+{
+ size_t len;
+
+ challenge = cockpit_authorize_type (challenge, NULL);
+ if (!challenge)
+ return NULL;
+
+ len = strcspn (challenge, ": ");
+ if (len == 0)
+ {
+ message ("invalid \"authorize\" message \"challenge\": no subject");
+ errno = EINVAL;
+ return NULL;
+ }
+
+ if (subject)
+ {
+ *subject = strndup (challenge, len);
+ if (!*subject)
+ {
+ message ("couldn't allocate memory for \"authorize\" message \"challenge\"");
+ errno = ENOMEM;
+ return NULL;
+ }
+ }
+
+ if (challenge[len])
+ len++;
+ while (challenge[len] == ' ')
+ len++;
+ return challenge + len;
+}
+
+char *
+cockpit_authorize_parse_basic (const char *challenge,
+ char **user)
+{
+ unsigned char *buf = NULL;
+ char *type = NULL;
+ size_t len;
+ ssize_t res;
+ int errn = 0;
+ size_t off;
+
+ challenge = cockpit_authorize_type (challenge, &type);
+ if (!challenge)
+ return NULL;
+
+ if (strcmp (type, "basic") != 0)
+ {
+ message ("invalid prefix in Basic header");
+ errn = EINVAL;
+ goto out;
+ }
+
+ len = strcspn (challenge, " ");
+ buf = malloc (len + 1);
+ if (!buf)
+ {
+ message ("couldn't allocate memory for Basic header");
+ errn = ENOMEM;
+ goto out;
+ }
+
+ /* No value */
+ if (len == 0)
+ {
+ buf[0] = 0;
+ if (user)
+ *user = NULL;
+ goto out;
+ }
+
+ /* Decode and find split point */
+ res = cockpit_base64_pton (challenge, len, buf, len);
+ if (res < 0)
+ {
+ message ("invalid base64 data in Basic header");
+ errn = EINVAL;
+ goto out;
+ }
+ assert (res <= len);
+ buf[res] = 0;
+
+ off = strcspn ((char *)buf, ":");
+ if (off == res)
+ {
+ message ("invalid base64 data in Basic header");
+ errn = EINVAL;
+ goto out;
+ }
+
+ if (user)
+ {
+ *user = strndup ((char *)buf, off);
+ if (!*user)
+ {
+ message ("couldn't allocate memory for user name");
+ errn = ENOMEM;
+ goto out;
+ }
+ }
+
+ memmove (buf, buf + off + 1, res - off);
+
+out:
+ free (type);
+ if (errn != 0)
+ {
+ errno = errn;
+ free (buf);
+ buf = NULL;
+ }
+ return (char *)buf;
+}
+
+char *
+cockpit_authorize_build_basic (const char *user,
+ const char *password)
+{
+ char *content = NULL;
+ char *encoded = NULL;
+ char *response = NULL;
+ size_t elen, clen;
+ int errn = 0;
+
+ if (!user)
+ user = "";
+ if (!password)
+ password = "";
+
+ if (asprintf (&content, "%s:%s", user, password) < 0)
+ {
+ errn = errno;
+ message ("could not build basic response");
+ goto out;
+ }
+
+ clen = strlen (content);
+ elen = cockpit_base64_size (clen);
+ encoded = malloc (elen + 1);
+ if (!encoded)
+ {
+ errn = ENOMEM;
+ message ("could not allocate memory for basic response");
+ goto out;
+ }
+ if (cockpit_base64_ntop ((unsigned char *)content, clen, encoded, elen) < 0)
+ {
+ errn = errno;
+ message ("could not encode basic response");
+ goto out;
+ }
+
+ if (asprintf (&response, "Basic %s", encoded) < 0)
+ {
+ errn = errno;
+ message ("could not build basic response");
+ response = NULL;
+ goto out;
+ }
+
+out:
+ free (encoded);
+ if (content)
+ cockpit_memory_clear (content, -1);
+ free (content);
+ if (!response)
+ errno = errn;
+ return response;
+}
+
+void *
+cockpit_authorize_parse_negotiate (const char *challenge,
+ size_t *length)
+{
+ unsigned char *buf = NULL;
+ size_t len;
+ ssize_t res;
+ int negotiate;
+ char *type;
+
+ challenge = cockpit_authorize_type (challenge, &type);
+ if (!challenge)
+ return NULL;
+
+ negotiate = strcmp (type, "negotiate") == 0;
+ free (type);
+
+ if (!negotiate)
+ {
+ message ("invalid prefix in Negotiate header");
+ errno = EINVAL;
+ return NULL;
+ }
+
+ len = strcspn (challenge, " ");
+ buf = malloc (len + 1);
+ if (!buf)
+ {
+ message ("couldn't allocate memory for Negotiate header");
+ errno = ENOMEM;
+ return NULL;
+ }
+
+ /* Decode data */
+ res = cockpit_base64_pton (challenge, len, buf, len);
+ if (res < 0)
+ {
+ message ("invalid base64 data in Negotiate header");
+ free (buf);
+ errno = EINVAL;
+ return NULL;
+ }
+
+ if (length)
+ *length = res;
+ return buf;
+}
+
+char *
+cockpit_authorize_build_negotiate (const void *input,
+ size_t length)
+{
+ char *encoded = NULL;
+ char *response = NULL;
+ size_t elen;
+ int errn = 0;
+
+ if (!input)
+ length = 0;
+
+ if (length > 0)
+ {
+ elen = cockpit_base64_size (length);
+ encoded = malloc (elen);
+ if (!encoded)
+ {
+ errn = ENOMEM;
+ message ("could not allocate memory for negotiate challenge");
+ goto out;
+ }
+ if (cockpit_base64_ntop ((unsigned char *)input, length, encoded, elen) < 0)
+ {
+ errn = errno;
+ message ("could not encode negotiate prompt");
+ goto out;
+ }
+ }
+
+ if (asprintf (&response, "Negotiate%s%s", encoded ? " " : "", encoded ? encoded : "") < 0)
+ {
+ errn = errno;
+ message ("could not build negotiate challenge");
+ response = NULL;
+ goto out;
+ }
+
+out:
+ free (encoded);
+ if (!response)
+ errno = errn;
+ return response;
+}
+
+char *
+cockpit_authorize_parse_x_conversation (const char *challenge,
+ char **conversation)
+{
+ unsigned char *buf = NULL;
+ int x_conversation;
+ char *type;
+ size_t len;
+ ssize_t res;
+
+ if (!cockpit_authorize_type (challenge, &type))
+ return NULL;
+
+ x_conversation = strcmp (type, "x-conversation") == 0;
+ free (type);
+
+ if (!x_conversation)
+ {
+ message ("invalid prefix in X-Conversation header");
+ errno = EINVAL;
+ return NULL;
+ }
+
+ challenge = cockpit_authorize_subject (challenge, conversation);
+ if (!challenge)
+ return NULL;
+
+ len = strcspn (challenge, " ");
+ buf = malloc (len + 1);
+ if (!buf)
+ {
+ message ("couldn't allocate memory for X-Conversation header");
+ errno = ENOMEM;
+ return NULL;
+ }
+
+ res = cockpit_base64_pton (challenge, len, buf, len);
+ if (res < 0)
+ {
+ message ("invalid base64 data in X-Conversation header");
+ free (buf);
+ errno = EINVAL;
+ return NULL;
+ }
+
+ /* Null terminate the thing */
+ buf[res] = '\0';
+ return (char *)buf;
+}
+
+char *
+cockpit_authorize_build_x_conversation (const char *prompt,
+ char **conversation)
+{
+ const size_t nlen = 128;
+ unsigned char *nonce = NULL;
+ char *encoded = NULL;
+ char *response = NULL;
+ char *conv = NULL;
+ size_t plen, elen, clen;
+ char *alloc = NULL;
+ int errn = 0;
+
+ if (!prompt)
+ prompt = "";
+
+ /* Reuse a conversation we get */
+ if (conversation)
+ conv = *conversation;
+
+ if (!conv)
+ {
+ nonce = cockpit_authorize_nonce (nlen);
+ if (!nonce)
+ {
+ errn = errno;
+ message ("could not generate nonce");
+ goto out;
+ }
+
+ clen = cockpit_base64_size (nlen);
+ conv = alloc = malloc (clen);
+ if (!conv)
+ {
+ errn = ENOMEM;
+ message ("could not allocate memory for conversation");
+ goto out;
+ }
+ if (cockpit_base64_ntop (nonce, nlen, conv, clen) < 0)
+ {
+ errn = errno;
+ message ("could not encode conversation nonce");
+ goto out;
+ }
+ }
+
+ if (strlen (conv) == 0)
+ {
+ message ("invalid conversation nonce");
+ errn = EINVAL;
+ goto out;
+ }
+
+ plen = strlen (prompt);
+ if (plen > 0)
+ {
+ elen = cockpit_base64_size (plen);
+ encoded = malloc (elen);
+ if (!encoded)
+ {
+ errn = ENOMEM;
+ message ("could not allocate memory for conversation");
+ goto out;
+ }
+ if (cockpit_base64_ntop ((unsigned char *)prompt, plen, encoded, elen) < 0)
+ {
+ errn = errno;
+ message ("could not encode conversation prompt");
+ goto out;
+ }
+ }
+
+ if (asprintf (&response, "X-Conversation %s%s%s", conv, encoded ? " " : "", encoded ? encoded : "") < 0)
+ {
+ errn = errno;
+ message ("could not build conversation challenge");
+ response = NULL;
+ goto out;
+ }
+
+ if (conversation)
+ {
+ *conversation = conv;
+ conv = alloc = NULL;
+ }
+
+out:
+ free (nonce);
+ free (alloc);
+ free (encoded);
+ if (!response)
+ errno = errn;
+ return response;
+}
diff --git a/src/common/cockpitauthorize.h b/src/common/cockpitauthorize.h
new file mode 100644
index 0000000..ef4bd61
--- /dev/null
+++ b/src/common/cockpitauthorize.h
@@ -0,0 +1,54 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_AUTHORIZE_H__
+#define COCKPIT_AUTHORIZE_H__
+
+#include <sys/types.h>
+
+void cockpit_authorize_logger (void (* func) (const char *data),
+ int verbose);
+
+void * cockpit_authorize_nonce (size_t length);
+
+const char * cockpit_authorize_type (const char *challenge,
+ char **type);
+
+const char * cockpit_authorize_subject (const char *challenge,
+ char **subject);
+
+char * cockpit_authorize_parse_basic (const char *challenge,
+ char **user);
+
+char * cockpit_authorize_build_basic (const char *user,
+ const char *password);
+
+void * cockpit_authorize_parse_negotiate (const char *challenge,
+ size_t *length);
+
+char * cockpit_authorize_build_negotiate (const void *input,
+ size_t length);
+
+char * cockpit_authorize_parse_x_conversation (const char *challenge,
+ char **conversation);
+
+char * cockpit_authorize_build_x_conversation (const char *prompt,
+ char **conversation);
+
+#endif /* COCKPIT_AUTHORIZE_H__ */
diff --git a/src/common/cockpitbase64.c b/src/common/cockpitbase64.c
new file mode 100644
index 0000000..96129b8
--- /dev/null
+++ b/src/common/cockpitbase64.c
@@ -0,0 +1,263 @@
+/*
+ * Copyright (c) 1996, 1998 by Internet Software Consortium.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS
+ * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE
+ * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+ * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+ * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+ * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
+ * SOFTWARE.
+ */
+
+/*
+ * Portions Copyright (c) 1995 by International Business Machines, Inc.
+ *
+ * International Business Machines, Inc. (hereinafter called IBM) grants
+ * permission under its copyrights to use, copy, modify, and distribute this
+ * Software with or without fee, provided that the above copyright notice and
+ * all paragraphs of this notice appear in all copies, and that the name of IBM
+ * not be used in connection with the marketing of any product incorporating
+ * the Software or modifications thereof, without specific, written prior
+ * permission.
+ *
+ * To the extent it has a right to do so, IBM grants an immunity from suit
+ * under its patents, if any, for the use, sale or manufacture of products to
+ * the extent that such products are used for performing Domain Name System
+ * dynamic updates in TCP/IP networks by means of the Software. No immunity is
+ * granted for any product per se or for any other function of any product.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES,
+ * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE. IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL,
+ * DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN
+ * IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ */
+
+#include "config.h"
+
+#include "cockpitbase64.h"
+
+#include <assert.h>
+#include <ctype.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+static const char Base64[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+static const char Pad64 = '=';
+
+/* skips all whitespace anywhere.
+ converts characters, four at a time, starting at (or after)
+ src from base - 64 numbers into three 8 bit bytes in the target area.
+ it returns the number of data bytes stored at the target, or -1 on error.
+ */
+
+ssize_t
+cockpit_base64_pton (const char *src,
+ size_t length,
+ unsigned char *target,
+ size_t targsize)
+{
+ int tarindex, state, ch;
+ char *pos;
+ const char *end;
+
+ state = 0;
+ tarindex = 0;
+ end = src + length;
+
+ /* We can't rely on the null terminator */
+ #define next_char(src, end) \
+ (((src) == (end)) ? '\0': *(src)++)
+
+ while ((ch = next_char (src, end)) != '\0')
+ {
+ if (isspace ((unsigned char) ch)) /* Skip whitespace anywhere. */
+ continue;
+
+ if (ch == Pad64)
+ break;
+
+ pos = strchr (Base64, ch);
+ if (pos == 0) /* A non-base64 character */
+ return (-1);
+
+ switch (state)
+ {
+ case 0:
+ if (target)
+ {
+ if ((size_t)tarindex >= targsize)
+ return (-1);
+ target[tarindex] = (pos - Base64) << 2;
+ }
+ state = 1;
+ break;
+ case 1:
+ if (target)
+ {
+ if ((size_t) tarindex + 1 >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64) >> 4;
+ target[tarindex + 1] = ((pos - Base64) & 0x0f) << 4;
+ }
+ tarindex++;
+ state = 2;
+ break;
+ case 2:
+ if (target)
+ {
+ if ((size_t) tarindex + 1 >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64) >> 2;
+ target[tarindex + 1] = ((pos - Base64) & 0x03) << 6;
+ }
+ tarindex++;
+ state = 3;
+ break;
+ case 3:
+ if (target)
+ {
+ if ((size_t) tarindex >= targsize)
+ return (-1);
+ target[tarindex] |= (pos - Base64);
+ }
+ tarindex++;
+ state = 0;
+ break;
+ default:
+ abort();
+ }
+ }
+
+ /*
+ * We are done decoding Base-64 chars. Let's see if we ended
+ * on a byte boundary, and/or with erroneous trailing characters.
+ */
+
+ if (ch == Pad64)
+ { /* We got a pad char. */
+ ch = next_char (src, end); /* Skip it, get next. */
+ switch (state)
+ {
+ case 0: /* Invalid = in first position */
+ case 1: /* Invalid = in second position */
+ return (-1);
+
+ case 2: /* Valid, means one byte of info */
+ /* Skip any number of spaces. */
+ for ((void) NULL; ch != '\0'; ch = next_char (src, end))
+ {
+ if (!isspace((unsigned char) ch))
+ break;
+ }
+ /* Make sure there is another trailing = sign. */
+ if (ch != Pad64)
+ return (-1);
+ ch = next_char (src, end); /* Skip the = */
+ /* Fall through to "single trailing =" case. */
+ /* FALLTHROUGH */
+
+ case 3: /* Valid, means two bytes of info */
+ /*
+ * We know this char is an =. Is there anything but
+ * whitespace after it?
+ */
+ for ((void)NULL; src != end; ch = next_char (src, end))
+ {
+ if (!isspace((unsigned char) ch))
+ return (-1);
+ }
+
+ /*
+ * Now make sure for cases 2 and 3 that the "extra"
+ * bits that slopped past the last full byte were
+ * zeros. If we don't check them, they become a
+ * subliminal channel.
+ */
+ if (target && target[tarindex] != 0)
+ return (-1);
+ }
+ }
+ else
+ {
+ /*
+ * We ended by seeing the end of the string. Make sure we
+ * have no partial bytes lying around.
+ */
+ if (state != 0)
+ return (-1);
+ }
+
+ return (tarindex);
+}
+
+ssize_t
+cockpit_base64_ntop (const unsigned char *src,
+ size_t srclength,
+ char *target,
+ size_t targsize)
+{
+ size_t len = 0;
+ unsigned char input[3];
+ unsigned char output[4];
+ size_t i;
+
+ while (srclength > 0)
+ {
+ if (srclength >= 3)
+ {
+ input[0] = *src++;
+ input[1] = *src++;
+ input[2] = *src++;
+ srclength -= 3;
+
+ output[0] = input[0] >> 2;
+ output[1] = ((input[0] & 0x03) << 4) + (input[1] >> 4);
+ output[2] = ((input[1] & 0x0f) << 2) + (input[2] >> 6);
+ output[3] = input[2] & 0x3f;
+
+ }
+ else
+ {
+ /* srclength 1 or 2: Get what's left. */
+ input[0] = input[1] = input[2] = '\0';
+ for (i = 0; i < srclength; i++)
+ input[i] = *src++;
+
+ output[0] = input[0] >> 2;
+ output[1] = ((input[0] & 0x03) << 4) + (input[1] >> 4);
+ if (srclength == 1)
+ output[2] = 255;
+ else
+ output[2] = ((input[1] & 0x0f) << 2) + (input[2] >> 6);
+ output[3] = 255;
+
+ srclength = 0;
+ }
+
+ for (i = 0; i < 4; i++)
+ {
+ assert(output[i] == 255 || output[i] < 64);
+ assert (len + 1 < targsize);
+
+ if (output[i] == 255)
+ target[len++] = Pad64;
+ else
+ target[len++] = Base64[output[i]];
+ }
+ }
+
+ assert (len < targsize);
+ target[len] = '\0'; /* Returned value doesn't count \0. */
+ return len;
+}
diff --git a/src/common/cockpitbase64.h b/src/common/cockpitbase64.h
new file mode 100644
index 0000000..2127231
--- /dev/null
+++ b/src/common/cockpitbase64.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 1996, 1998 by Internet Software Consortium.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS
+ * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE
+ * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
+ * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
+ * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+ * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
+ * SOFTWARE.
+ */
+
+/*
+ * Portions Copyright (c) 1995 by International Business Machines, Inc.
+ *
+ * International Business Machines, Inc. (hereinafter called IBM) grants
+ * permission under its copyrights to use, copy, modify, and distribute this
+ * Software with or without fee, provided that the above copyright notice and
+ * all paragraphs of this notice appear in all copies, and that the name of IBM
+ * not be used in connection with the marketing of any product incorporating
+ * the Software or modifications thereof, without specific, written prior
+ * permission.
+ *
+ * To the extent it has a right to do so, IBM grants an immunity from suit
+ * under its patents, if any, for the use, sale or manufacture of products to
+ * the extent that such products are used for performing Domain Name System
+ * dynamic updates in TCP/IP networks by means of the Software. No immunity is
+ * granted for any product per se or for any other function of any product.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES,
+ * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE. IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL,
+ * DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN
+ * IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ */
+
+#ifndef COCKPIT_BASE64_H_
+#define COCKPIT_BASE64_H_
+
+#include <sys/types.h>
+
+#define cockpit_base64_size(len) ((len) * 4 / 3 + 9)
+
+ssize_t cockpit_base64_pton (const char *src,
+ size_t length,
+ unsigned char *target,
+ size_t targsize);
+
+ssize_t cockpit_base64_ntop (const unsigned char *src,
+ size_t srclength,
+ char *target,
+ size_t targsize);
+
+#endif /* COCKPIT_BASE64_H_ */
diff --git a/src/common/cockpitchannel.c b/src/common/cockpitchannel.c
new file mode 100644
index 0000000..1d30275
--- /dev/null
+++ b/src/common/cockpitchannel.c
@@ -0,0 +1,1177 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitchannel.h"
+
+#include "common/cockpitflow.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitunicode.h"
+
+#include <json-glib/json-glib.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ * CockpitChannel:
+ *
+ * A base class for channels. Derived classes implement the actual payload
+ * contents, opening the channel etc...
+ *
+ * A CockpitChannel is the base class for C code where messages on
+ * the internal protocol are translated to IO of another type, be
+ * that HTTP, stdio, DBus, WebSocket, file access, or whatever. Another
+ * similar analogue to this code is the javascript cockpit.channel code
+ * in cockpit.js.
+ *
+ * Most uses of this code are in the bridges, but cockpit-ws also (rarely)
+ * uses CockpitChannel to translate the frontend side of channels to HTTP
+ * or WebSocket responses.
+ *
+ * The channel queues messages received until unfrozen. The caller can
+ * start off a channel as frozen, and then the implementation later
+ * indicates that it's open and ready to receive messages.
+ *
+ * A channel sends messages over a #CockpitTransport. If the transport
+ * closes then the channel closes, but the channel can also close
+ * individually either for failure reasons, or with an orderly shutdown.
+ *
+ * See doc/protocol.md for information about channels.
+ *
+ * A channel can do flow control in two ways:
+ *
+ * - It can throttle its peer sending data, by delaying responding to "ping"
+ * messages. It can listen to a "pressure" signal to control this.
+ * - It can optionally control another flow, by emitting a "pressure" signal
+ * when its peer receiving data does not respond to "ping" messages within
+ * a given window.
+ */
+
+/* Every 16K Send a ping */
+#define CHANNEL_FLOW_PING (16L * 1024L)
+
+/* Allow up to 1MB of data to be sent without ack */
+#define CHANNEL_FLOW_WINDOW (2L * 1024L * 1024L)
+
+typedef struct {
+ gulong recv_sig;
+ gulong close_sig;
+ gulong control_sig;
+
+ /* Construct arguments */
+ CockpitTransport *transport;
+ gchar *id;
+ JsonObject *open_options;
+ gchar **capabilities;
+
+ /* Queued messages before channel is ready */
+ gboolean prepared;
+ guint prepare_tag;
+
+ /* Whether we've sent a closed message */
+ gboolean sent_close;
+
+ /* Whether we called the close vfunc */
+ gboolean emitted_close;
+
+ /* Whether the transport closed (before we did) */
+ gboolean transport_closed;
+
+ /* EOF flags */
+ gboolean sent_done;
+ gboolean received_done;
+
+ /* Binary options */
+ gboolean binary_ok;
+
+ /* Other state */
+ JsonObject *close_options;
+
+ /* Buffer for incomplete unicode bytes */
+ GBytes *out_buffer;
+ gint buffer_timeout;
+
+ /* The number of bytes sent, and current flow control window */
+ gint64 out_sequence;
+ gint64 out_window;
+
+ /* Another object giving back-pressure on received data */
+ gboolean flow_control;
+ CockpitFlow *pressure;
+ gulong pressure_sig;
+ GQueue *throttled;
+} CockpitChannelPrivate;
+
+enum {
+ PROP_0,
+ PROP_TRANSPORT,
+ PROP_ID,
+ PROP_OPTIONS,
+ PROP_CAPABILITIES,
+};
+
+static guint cockpit_channel_sig_closed;
+
+static void cockpit_channel_flow_iface_init (CockpitFlowInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CockpitChannel, cockpit_channel, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (COCKPIT_TYPE_FLOW, cockpit_channel_flow_iface_init)
+ G_ADD_PRIVATE (CockpitChannel));
+
+static gboolean
+on_idle_prepare (gpointer data)
+{
+ CockpitChannel *self = COCKPIT_CHANNEL (data);
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ g_object_ref (self);
+ priv->prepare_tag = 0;
+ cockpit_channel_prepare (self);
+ g_object_unref (self);
+ return FALSE;
+}
+
+static void
+cockpit_channel_init (CockpitChannel *self)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ priv->out_sequence = 0;
+ priv->out_window = CHANNEL_FLOW_WINDOW;
+}
+
+static void
+process_recv (CockpitChannel *self,
+ GBytes *payload)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ CockpitChannelClass *klass;
+
+ if (priv->received_done)
+ {
+ cockpit_channel_fail (self, "protocol-error", "channel received message after done");
+ }
+ else
+ {
+ klass = COCKPIT_CHANNEL_GET_CLASS (self);
+ if (klass->recv)
+ (klass->recv) (self, payload);
+ }
+}
+
+static gboolean
+on_transport_recv (CockpitTransport *transport,
+ const gchar *channel_id,
+ GBytes *data,
+ gpointer user_data)
+{
+ CockpitChannel *self = user_data;
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ if (g_strcmp0 (channel_id, priv->id) != 0)
+ return FALSE;
+
+ process_recv (self, data);
+ return TRUE;
+}
+
+static gboolean
+process_ping (CockpitChannel *self,
+ JsonObject *ping)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ GBytes *payload;
+
+ if (priv->throttled)
+ {
+ g_debug ("%s: received ping while throttled", priv->id);
+ g_queue_push_tail (priv->throttled, json_object_ref (ping));
+ return FALSE;
+ }
+ else
+ {
+ g_debug ("%s: replying to ping with pong", priv->id);
+ json_object_set_string_member (ping, "command", "pong");
+ payload = cockpit_json_write_bytes (ping);
+ cockpit_transport_send (priv->transport, NULL, payload);
+ g_bytes_unref (payload);
+ return TRUE;
+ }
+}
+
+static void
+process_pong (CockpitChannel *self,
+ JsonObject *pong)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ gint64 sequence;
+
+ if (!priv->flow_control)
+ return;
+
+ if (!cockpit_json_get_int (pong, "sequence", -1, &sequence))
+ {
+ g_message ("%s: received invalid \"pong\" \"sequence\" field", priv->id);
+ sequence = -1;
+ }
+
+ g_debug ("%s: received pong with sequence: %" G_GINT64_FORMAT, priv->id, sequence);
+ if (sequence > priv->out_window + (CHANNEL_FLOW_WINDOW * 10))
+ {
+ g_message ("%s: received a flow control ack with a suspiciously large sequence: %" G_GINT64_FORMAT,
+ priv->id, sequence);
+ }
+
+ if (sequence >= priv->out_window)
+ {
+ /* Up to this point has been confirmed received */
+ priv->out_window = sequence + CHANNEL_FLOW_WINDOW;
+
+ /* If our sent bytes are within the window, no longer under pressure */
+ if (priv->out_sequence <= priv->out_window)
+ {
+ g_debug ("%s: got acknowledge of enough data, relieving back pressure", priv->id);
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), FALSE);
+ }
+ }
+}
+
+static void
+process_control (CockpitChannel *self,
+ const gchar *command,
+ JsonObject *options)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ CockpitChannelClass *klass;
+ const gchar *problem;
+
+ if (g_str_equal (command, "close"))
+ {
+ g_debug ("close channel %s", priv->id);
+ if (!cockpit_json_get_string (options, "problem", NULL, &problem))
+ problem = NULL;
+ cockpit_channel_close (self, problem);
+ return;
+ }
+
+ if (g_str_equal (command, "ping"))
+ {
+ process_ping (self, options);
+ return;
+ }
+ else if (g_str_equal (command, "pong"))
+ {
+ process_pong (self, options);
+ return;
+ }
+ else if (g_str_equal (command, "done"))
+ {
+ if (priv->received_done)
+ cockpit_channel_fail (self, "protocol-error", "channel received second done");
+ else
+ priv->received_done = TRUE;
+ }
+
+ klass = COCKPIT_CHANNEL_GET_CLASS (self);
+ if (klass->control)
+ (klass->control) (self, command, options);
+}
+
+static gboolean
+on_transport_control (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel_id,
+ JsonObject *options,
+ GBytes *payload,
+ gpointer user_data)
+{
+ CockpitChannel *self = user_data;
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ if (g_strcmp0 (channel_id, priv->id) != 0)
+ return FALSE;
+
+ process_control (self, command, options);
+ return TRUE;
+}
+
+static void
+on_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitChannel *self = COCKPIT_CHANNEL (user_data);
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ priv->transport_closed = TRUE;
+ if (problem == NULL)
+ problem = "disconnected";
+ if (!priv->emitted_close)
+ cockpit_channel_close (self, problem);
+}
+
+static void
+cockpit_channel_actual_send (CockpitChannel *self,
+ GBytes *payload,
+ gboolean trust_is_utf8)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ GBytes *validated = NULL;
+ guint64 out_sequence;
+ JsonObject *ping;
+ gsize size;
+
+ g_return_if_fail (priv->out_buffer == NULL);
+ g_return_if_fail (priv->buffer_timeout == 0);
+
+ if (!trust_is_utf8)
+ {
+ if (!priv->binary_ok)
+ payload = validated = cockpit_unicode_force_utf8 (payload);
+ }
+
+ cockpit_transport_send (priv->transport, priv->id, payload);
+
+ /* A wraparound of our gint64 size? */
+ if (priv->flow_control)
+ {
+ gboolean trigger_pressure;
+ size = g_bytes_get_size (payload);
+ g_return_if_fail (G_MAXINT64 - size > priv->out_sequence);
+
+ /* How many bytes have been sent (queued) */
+ out_sequence = priv->out_sequence + size;
+
+ /* If we've sent more than the window, we just got under pressure;
+ * do an edge trigger instead of level trigger to avoid ping/signal loops */
+ trigger_pressure = (priv->out_sequence <= priv->out_window) && (out_sequence > priv->out_window);
+
+ /* Every CHANNEL_FLOW_PING bytes we send a ping; also when applying back
+ * pressure as there is otherwise nothing more to send and generate pings for */
+ if ((out_sequence / CHANNEL_FLOW_PING != priv->out_sequence / CHANNEL_FLOW_PING) || trigger_pressure)
+ {
+ ping = json_object_new ();
+ json_object_set_int_member (ping, "sequence", out_sequence);
+ cockpit_channel_control (self, "ping", ping);
+ g_debug ("%s: sending ping with sequence: %" G_GINT64_FORMAT, priv->id, out_sequence);
+ json_object_unref (ping);
+ }
+
+ priv->out_sequence = out_sequence;
+
+ if (trigger_pressure)
+ {
+ g_debug ("%s: sent too much data without acknowledgement, emitting back pressure until %"
+ G_GINT64_FORMAT, priv->id, priv->out_window);
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), TRUE);
+ }
+ }
+
+ if (validated)
+ g_bytes_unref (validated);
+}
+
+static gboolean
+flush_buffer (gpointer user_data)
+{
+ CockpitChannel *self = user_data;
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ GBytes *payload;
+
+ if (priv->out_buffer)
+ {
+ payload = g_bytes_ref (priv->out_buffer);
+ g_bytes_unref (priv->out_buffer);
+ priv->out_buffer = NULL;
+ if (priv->buffer_timeout)
+ g_source_remove(priv->buffer_timeout);
+ priv->buffer_timeout = 0;
+
+ cockpit_channel_actual_send (self, payload, FALSE);
+ g_bytes_unref (payload);
+ }
+
+ return FALSE;
+}
+
+static void
+cockpit_channel_constructed (GObject *object)
+{
+ CockpitChannel *self = COCKPIT_CHANNEL (object);
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ G_OBJECT_CLASS (cockpit_channel_parent_class)->constructed (object);
+
+ g_return_if_fail (priv->id != NULL);
+ g_return_if_fail (priv->transport != NULL);
+
+ priv->capabilities = NULL;
+ priv->recv_sig = g_signal_connect (priv->transport, "recv",
+ G_CALLBACK (on_transport_recv), self);
+ priv->control_sig = g_signal_connect (priv->transport, "control",
+ G_CALLBACK (on_transport_control), self);
+ priv->close_sig = g_signal_connect (priv->transport, "closed",
+ G_CALLBACK (on_transport_closed), self);
+
+ /* Freeze this channel's messages until ready */
+ cockpit_transport_freeze (priv->transport, priv->id);
+ priv->prepare_tag = g_idle_add_full (G_PRIORITY_HIGH, on_idle_prepare, self, NULL);
+}
+
+static gboolean
+cockpit_channel_ensure_capable (CockpitChannel *self,
+ JsonObject *options)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ const gchar **capabilities = NULL;
+ JsonObject *close_options = NULL; // owned by channel
+ gboolean missing = FALSE;
+ gboolean ret = FALSE;
+ gint len;
+ gint i;
+
+ if (!cockpit_json_get_strv (options, "capabilities", NULL, &capabilities))
+ {
+ cockpit_channel_fail (self, "protocol-error", "got invalid capabilities field in open message");
+ goto out;
+ }
+
+ if (!capabilities)
+ {
+ ret = TRUE;
+ goto out;
+ }
+
+ len = g_strv_length ((gchar **) capabilities);
+ for (i = 0; i < len; i++)
+ {
+ if (priv->capabilities == NULL || !g_strv_contains((const gchar **) priv->capabilities, capabilities[i]))
+ {
+ g_message ("%s: unsupported capability required: %s", priv->id, capabilities[i]);
+ missing = TRUE;
+ }
+ }
+
+ if (missing)
+ {
+ JsonArray *arr = json_array_new (); // owned by closed options
+
+ if (priv->capabilities != NULL)
+ {
+ len = g_strv_length (priv->capabilities);
+ for (i = 0; i < len; i++)
+ json_array_add_string_element (arr, priv->capabilities[i]);
+ }
+
+ close_options = cockpit_channel_close_options (self);
+ json_object_set_array_member (close_options, "capabilities", arr);
+ cockpit_channel_close (self, "not-supported");
+ }
+
+ ret = !missing;
+
+out:
+ g_free (capabilities);
+ return ret;
+}
+
+static void
+cockpit_channel_real_prepare (CockpitChannel *self)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ JsonObject *options;
+ const gchar *binary;
+
+ options = cockpit_channel_get_options (self);
+
+ if (!cockpit_channel_ensure_capable (self, options))
+ return;
+
+ if (G_OBJECT_TYPE (self) == COCKPIT_TYPE_CHANNEL)
+ {
+ cockpit_channel_close (self, "not-supported");
+ return;
+ }
+
+ if (!cockpit_json_get_string (options, "binary", NULL, &binary))
+ {
+ cockpit_channel_fail (self, "protocol-error", "channel has invalid \"binary\" option");
+ }
+ else if (binary != NULL)
+ {
+ priv->binary_ok = TRUE;
+ if (!g_str_equal (binary, "raw"))
+ {
+ cockpit_channel_fail (self, "protocol-error",
+ "channel has invalid \"binary\" option: %s", binary);
+ }
+ }
+
+ /*
+ * The default here, can change from FALSE to TRUE over time once we assume that all
+ * cockpit-ws participants have been upgraded sufficiently. The default when we're
+ * on the channel creation side is to handle flow control.
+ */
+ if (!cockpit_json_get_bool (options, "flow-control", FALSE, &priv->flow_control))
+ {
+ cockpit_channel_fail (self, "protocol-error", "channel has invalid \"flow-control\" option");
+ }
+}
+
+static void
+cockpit_channel_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitChannel *self = COCKPIT_CHANNEL (object);
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_TRANSPORT:
+ g_value_set_object (value, priv->transport);
+ break;
+ case PROP_ID:
+ g_value_set_string (value, priv->id);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_channel_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitChannel *self = COCKPIT_CHANNEL (object);
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_TRANSPORT:
+ priv->transport = g_value_dup_object (value);
+ break;
+ case PROP_ID:
+ priv->id = g_value_dup_string (value);
+ break;
+ case PROP_OPTIONS:
+ priv->open_options = g_value_dup_boxed (value);
+ break;
+ case PROP_CAPABILITIES:
+ g_return_if_fail (priv->capabilities == NULL);
+ priv->capabilities = g_value_dup_boxed (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_channel_dispose (GObject *object)
+{
+ CockpitChannel *self = COCKPIT_CHANNEL (object);
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ /*
+ * This object was destroyed before going to the main loop
+ * no need to wait until later before we fire various signals.
+ */
+ if (priv->prepare_tag)
+ {
+ g_source_remove (priv->prepare_tag);
+ priv->prepare_tag = 0;
+ }
+
+ if (priv->recv_sig)
+ g_signal_handler_disconnect (priv->transport, priv->recv_sig);
+ priv->recv_sig = 0;
+
+ if (priv->control_sig)
+ g_signal_handler_disconnect (priv->transport, priv->control_sig);
+ priv->control_sig = 0;
+
+ if (priv->close_sig)
+ g_signal_handler_disconnect (priv->transport, priv->close_sig);
+ priv->close_sig = 0;
+
+ if (!priv->emitted_close)
+ cockpit_channel_close (self, "terminated");
+
+ if (priv->buffer_timeout)
+ g_source_remove(priv->buffer_timeout);
+ priv->buffer_timeout = 0;
+
+ if (priv->out_buffer)
+ g_bytes_unref (priv->out_buffer);
+ priv->out_buffer = NULL;
+
+ cockpit_flow_throttle (COCKPIT_FLOW (self), NULL);
+ g_assert (priv->pressure == NULL);
+ if (priv->throttled)
+ g_queue_free_full (priv->throttled, (GDestroyNotify)json_object_unref);
+ priv->throttled = NULL;
+
+ G_OBJECT_CLASS (cockpit_channel_parent_class)->dispose (object);
+}
+
+static void
+cockpit_channel_finalize (GObject *object)
+{
+ CockpitChannel *self = COCKPIT_CHANNEL (object);
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ g_object_unref (priv->transport);
+ if (priv->open_options)
+ json_object_unref (priv->open_options);
+ if (priv->close_options)
+ json_object_unref (priv->close_options);
+
+ g_strfreev (priv->capabilities);
+ g_free (priv->id);
+
+ G_OBJECT_CLASS (cockpit_channel_parent_class)->finalize (object);
+}
+
+static void
+cockpit_channel_real_close (CockpitChannel *self,
+ const gchar *problem)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ JsonObject *object;
+ GBytes *message;
+
+ if (priv->sent_close)
+ return;
+
+ priv->sent_close = TRUE;
+
+ if (!priv->transport_closed)
+ {
+ flush_buffer (self);
+
+ if (priv->close_options)
+ {
+ object = priv->close_options;
+ priv->close_options = NULL;
+ }
+ else
+ {
+ object = json_object_new ();
+ }
+
+ json_object_set_string_member (object, "command", "close");
+ json_object_set_string_member (object, "channel", priv->id);
+ if (problem)
+ json_object_set_string_member (object, "problem", problem);
+
+ message = cockpit_json_write_bytes (object);
+ json_object_unref (object);
+
+ cockpit_transport_send (priv->transport, NULL, message);
+ g_bytes_unref (message);
+ }
+
+ g_signal_emit (self, cockpit_channel_sig_closed, 0, problem);
+}
+
+static void
+cockpit_channel_class_init (CockpitChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->constructed = cockpit_channel_constructed;
+ gobject_class->get_property = cockpit_channel_get_property;
+ gobject_class->set_property = cockpit_channel_set_property;
+ gobject_class->dispose = cockpit_channel_dispose;
+ gobject_class->finalize = cockpit_channel_finalize;
+
+ klass->prepare = cockpit_channel_real_prepare;
+ klass->close = cockpit_channel_real_close;
+
+ /**
+ * CockpitChannel:transport:
+ *
+ * The transport to send and receive messages over.
+ */
+ g_object_class_install_property (gobject_class, PROP_TRANSPORT,
+ g_param_spec_object ("transport", "transport", "transport", COCKPIT_TYPE_TRANSPORT,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitChannel:channel:
+ *
+ * The numeric channel to receive and send messages on.
+ */
+ g_object_class_install_property (gobject_class, PROP_ID,
+ g_param_spec_string ("id", "id", "id", NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitChannel:options:
+ *
+ * The JSON options used to open this channel. The exact contents are
+ * dependent on the derived channel class ... but this must at the
+ * very least contain a 'payload' field describing what kind of channel
+ * this should be.
+ */
+ g_object_class_install_property (gobject_class, PROP_OPTIONS,
+ g_param_spec_boxed ("options", "options", "options", JSON_TYPE_OBJECT,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitChannel:capabilities:
+ *
+ * The capabilities that this channel supports.
+ */
+ g_object_class_install_property (gobject_class, PROP_CAPABILITIES,
+ g_param_spec_boxed ("capabilities",
+ "Capabilities",
+ "Channel Capabilities",
+ G_TYPE_STRV,
+ G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitChannel::closed:
+ *
+ * Emitted when the channel closes. This is similar to CockpitTransport::closed
+ * but only applies to the individual channel.
+ *
+ * The channel will also be closed when the transport closes.
+ */
+ cockpit_channel_sig_closed = g_signal_new ("closed", COCKPIT_TYPE_CHANNEL, G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (CockpitChannelClass, closed),
+ NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
+}
+
+/**
+ * cockpit_channel_close:
+ * @self: a channel
+ * @problem: the problem or NULL
+ *
+ * Close the channel. This can be called multiple times.
+ *
+ * It may be that the channel doesn't close immediately.
+ * The channel will emit the CockpitChannel::closed signal when the
+ * channel actually closes.
+ *
+ * If this is called immediately after or during construction then
+ * the closing will happen after the main loop so that handlers
+ * can connect appropriately.
+ *
+ * A @problem of NULL represents an orderly close.
+ */
+void
+cockpit_channel_close (CockpitChannel *self,
+ const gchar *problem)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ CockpitChannelClass *klass;
+
+ g_return_if_fail (COCKPIT_IS_CHANNEL (self));
+
+ /* No further messages should be received */
+ if (priv->recv_sig)
+ g_signal_handler_disconnect (priv->transport, priv->recv_sig);
+ priv->recv_sig = 0;
+
+ if (priv->control_sig)
+ g_signal_handler_disconnect (priv->transport, priv->control_sig);
+ priv->control_sig = 0;
+
+ if (priv->close_sig)
+ g_signal_handler_disconnect (priv->transport, priv->close_sig);
+ priv->close_sig = 0;
+
+ klass = COCKPIT_CHANNEL_GET_CLASS (self);
+ g_assert (klass->close != NULL);
+ priv->emitted_close = TRUE;
+ (klass->close) (self, problem);
+}
+
+/*
+ * cockpit_channel_fail:
+ * @self: a channel
+ * @problem: the problem
+ *
+ * Close the channel with a @problem. In addition a "message" field
+ * will be set on the channel, using the @format argument to build
+ * the message. The message will also be logged.
+ *
+ * See cockpit_channel_close() for further info.
+ */
+void
+cockpit_channel_fail (CockpitChannel *self,
+ const gchar *problem,
+ const gchar *format,
+ ...)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ JsonObject *options;
+ gchar *message;
+ va_list va;
+
+ g_return_if_fail (problem != NULL);
+ g_return_if_fail (COCKPIT_IS_CHANNEL (self));
+
+ va_start (va, format);
+ message = g_strdup_vprintf (format, va);
+ va_end (va);
+
+ options = cockpit_channel_close_options (self);
+ if (!json_object_has_member (options, "message"))
+ json_object_set_string_member (options, "message", message);
+ g_message ("%s: %s", priv->id, message);
+ g_free (message);
+
+ cockpit_channel_close (self, problem);
+}
+
+/* Used by implementations */
+
+/**
+ * cockpit_channel_ready:
+ * @self: a pipe
+ * @message: an optional control message, or NULL
+ *
+ * Called by channel implementations to signal when they're
+ * ready. Any messages received before the channel was ready
+ * will be delivered to the channel's recv() vfunc in the order
+ * that they were received.
+ *
+ * If this is called immediately after or during construction then
+ * the closing will happen after the main loop so that handlers
+ * can connect appropriately.
+ */
+void
+cockpit_channel_ready (CockpitChannel *self,
+ JsonObject *message)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ g_object_ref (self);
+
+ cockpit_transport_thaw (priv->transport, priv->id);
+ cockpit_channel_control (self, "ready", message);
+
+ g_object_unref (self);
+}
+
+/**
+ * cockpit_channel_send:
+ * @self: a pipe
+ * @payload: the message payload to send
+ * @trust_is_utf8: set to true if sure data is UTF8
+ *
+ * Called by implementations to send a message over the transport
+ * on the right channel.
+ *
+ * This message is queued, and sent once the transport can.
+ */
+void
+cockpit_channel_send (CockpitChannel *self,
+ GBytes *payload,
+ gboolean trust_is_utf8)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ const guint8 *data;
+ gsize length;
+ GBytes *send_data = payload;
+ GByteArray *combined;
+
+ if (priv->buffer_timeout)
+ g_source_remove(priv->buffer_timeout);
+ priv->buffer_timeout = 0;
+
+ if (priv->out_buffer)
+ {
+ combined = g_bytes_unref_to_array (priv->out_buffer);
+ priv->out_buffer = NULL;
+
+ data = g_bytes_get_data (payload, &length);
+ g_byte_array_append (combined, data, length);
+ send_data = g_byte_array_free_to_bytes (combined);
+
+ trust_is_utf8 = FALSE;
+ }
+
+ if (!trust_is_utf8 && !priv->binary_ok)
+ {
+ if (cockpit_unicode_has_incomplete_ending (send_data))
+ {
+ priv->out_buffer = g_bytes_ref (send_data);
+ priv->buffer_timeout = g_timeout_add (500, flush_buffer, self);
+ }
+ }
+
+ if (!priv->buffer_timeout)
+ cockpit_channel_actual_send (self, send_data, trust_is_utf8);
+
+ if (send_data != payload)
+ g_bytes_unref (send_data);
+}
+
+/**
+ * cockpit_channel_get_option:
+ * @self: a channel
+ *
+ * Called by implementations to get the channel's open options.
+ *
+ * Returns: (transfer none): the open options, should not be NULL
+ */
+JsonObject *
+cockpit_channel_get_options (CockpitChannel *self)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ g_return_val_if_fail (COCKPIT_IS_CHANNEL (self), NULL);
+ return priv->open_options;
+}
+
+/**
+ * cockpit_channel_get_transport:
+ * @self: a channel
+ *
+ * Called by implementations to get the channel's transtport.
+ *
+ * Returns: (transfer none): the transport, should not be NULL
+ */
+CockpitTransport *
+cockpit_channel_get_transport (CockpitChannel *self)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ g_return_val_if_fail (COCKPIT_IS_CHANNEL (self), NULL);
+ return priv->transport;
+}
+
+/**
+ * cockpit_channel_close_options
+ * @self: a channel
+ *
+ * Called by implementations to get the channel's close options.
+ *
+ * Returns: (transfer none): the close options, should not be NULL
+ */
+JsonObject *
+cockpit_channel_close_options (CockpitChannel *self)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ g_return_val_if_fail (COCKPIT_IS_CHANNEL (self), NULL);
+ if (!priv->close_options)
+ priv->close_options = json_object_new ();
+ return priv->close_options;
+}
+
+/**
+ * cockpit_channel_get_id:
+ * @self a channel
+ *
+ * Get the identifier for this channel.
+ *
+ * Returns: (transfer none): the identifier
+ */
+const gchar *
+cockpit_channel_get_id (CockpitChannel *self)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ g_return_val_if_fail (COCKPIT_IS_CHANNEL (self), NULL);
+ return priv->id;
+}
+
+/**
+ * cockpit_channel_prepare:
+ * @self: the channel
+ *
+ * Usually this is automatically called after the channel is
+ * created and control returns to the mainloop. However you
+ * can preempt that by calling this function. In the case of
+ * a frozen channel, this method needs to be called to set
+ * things in motion.
+ */
+void
+cockpit_channel_prepare (CockpitChannel *self)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ CockpitChannelClass *klass;
+
+ g_return_if_fail (COCKPIT_IS_CHANNEL (self));
+
+ if (priv->prepared)
+ return;
+
+ if (priv->prepare_tag)
+ {
+ g_source_remove (priv->prepare_tag);
+ priv->prepare_tag = 0;
+ }
+
+ priv->prepared = TRUE;
+ if (!priv->emitted_close)
+ {
+ klass = COCKPIT_CHANNEL_GET_CLASS (self);
+ g_assert (klass->prepare);
+ (klass->prepare) (self);
+ }
+}
+
+/**
+ * cockpit_channel_control:
+ * @self: the channel
+ * @command: the control command
+ * @options: optional control message or NULL
+ *
+ * Send a control message to the other side.
+ *
+ * If @options is not NULL, then it may be modified by this code.
+ *
+ * With @command of "done" will send an EOF to the other side. This
+ * should only be called once. Whether an EOF should be sent or not
+ * depends on the payload type.
+ */
+void
+cockpit_channel_control (CockpitChannel *self,
+ const gchar *command,
+ JsonObject *options)
+{
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ JsonObject *object;
+ GBytes *message;
+ const gchar *problem = NULL;
+ gchar *problem_copy = NULL;
+
+ g_return_if_fail (COCKPIT_IS_CHANNEL (self));
+ g_return_if_fail (command != NULL);
+
+ if (g_str_equal (command, "done"))
+ {
+ g_return_if_fail (priv->sent_done == FALSE);
+ priv->sent_done = TRUE;
+ }
+
+ /* If closing save the close options
+ * and let close send the message */
+ else if (g_str_equal (command, "close"))
+ {
+ if (options)
+ {
+ if (!priv->close_options)
+ {
+ /* Ref for close_options, freed in parent */
+ priv->close_options = json_object_ref (options);
+ }
+
+ if (!cockpit_json_get_string (options, "problem", NULL, &problem))
+ problem = NULL;
+ }
+
+ /* Use a problem copy so it out lasts the value in close_options */
+ problem_copy = g_strdup (problem);
+ cockpit_channel_close (self, problem_copy);
+ goto out;
+ }
+
+ if (options)
+ object = json_object_ref (options);
+ else
+ object = json_object_new ();
+
+ json_object_set_string_member (object, "command", command);
+ json_object_set_string_member (object, "channel", priv->id);
+
+ message = cockpit_json_write_bytes (object);
+ json_object_unref (object);
+
+ cockpit_transport_send (priv->transport, NULL, message);
+ g_bytes_unref (message);
+
+out:
+ g_free (problem_copy);
+}
+
+static void
+on_throttle_pressure (GObject *object,
+ gboolean throttle,
+ gpointer user_data)
+{
+ CockpitChannel *self = COCKPIT_CHANNEL (user_data);
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+ GQueue *throttled;
+ JsonObject *ping;
+
+ if (throttle)
+ {
+ if (!priv->throttled)
+ priv->throttled = g_queue_new ();
+ }
+ else
+ {
+ throttled = priv->throttled;
+ priv->throttled = NULL;
+ while (throttled)
+ {
+ ping = g_queue_pop_head (throttled);
+ if (!ping)
+ {
+ g_queue_free (throttled);
+ throttled = NULL;
+ }
+ else
+ {
+ if (!process_ping (self, ping))
+ g_assert_not_reached (); /* Because throttle is FALSE */
+ json_object_unref (ping);
+ }
+ }
+ }
+}
+
+static void
+cockpit_channel_throttle (CockpitFlow *flow,
+ CockpitFlow *controlling)
+{
+ CockpitChannel *self = COCKPIT_CHANNEL (flow);
+ CockpitChannelPrivate *priv = cockpit_channel_get_instance_private (self);
+
+ if (priv->pressure)
+ {
+ g_signal_handler_disconnect (priv->pressure, priv->pressure_sig);
+ g_object_remove_weak_pointer (G_OBJECT (priv->pressure), (gpointer *)&priv->pressure);
+ priv->pressure = NULL;
+ }
+
+ if (controlling)
+ {
+ priv->pressure = controlling;
+ g_object_add_weak_pointer (G_OBJECT (priv->pressure), (gpointer *)&priv->pressure);
+ priv->pressure_sig = g_signal_connect (controlling, "pressure", G_CALLBACK (on_throttle_pressure), self);
+ }
+}
+
+static void
+cockpit_channel_flow_iface_init (CockpitFlowInterface *iface)
+{
+ iface->throttle = cockpit_channel_throttle;
+}
diff --git a/src/common/cockpitchannel.h b/src/common/cockpitchannel.h
new file mode 100644
index 0000000..65254f9
--- /dev/null
+++ b/src/common/cockpitchannel.h
@@ -0,0 +1,90 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_CHANNEL_H__
+#define __COCKPIT_CHANNEL_H__
+
+#include <glib-object.h>
+#include <json-glib/json-glib.h>
+
+#include "common/cockpittransport.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_CHANNEL (cockpit_channel_get_type ())
+G_DECLARE_DERIVABLE_TYPE(CockpitChannel, cockpit_channel, COCKPIT, CHANNEL, GObject)
+
+struct _CockpitChannelClass
+{
+ GObjectClass parent_class;
+
+ /* signal */
+
+ void (* closed) (CockpitChannel *channel,
+ const gchar *problem);
+
+ /* vfuncs */
+
+ void (* prepare) (CockpitChannel *channel);
+
+ void (* recv) (CockpitChannel *channel,
+ GBytes *message);
+
+ gboolean (* control) (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *options);
+
+ void (* close) (CockpitChannel *channel,
+ const gchar *problem);
+};
+
+void cockpit_channel_prepare (CockpitChannel *self);
+
+void cockpit_channel_close (CockpitChannel *self,
+ const gchar *problem);
+
+void cockpit_channel_fail (CockpitChannel *self,
+ const gchar *problem,
+ const gchar *format,
+ ...) G_GNUC_PRINTF(3, 4);
+
+const gchar * cockpit_channel_get_id (CockpitChannel *self);
+
+CockpitTransport * cockpit_channel_get_transport (CockpitChannel *self);
+
+/* Used by implementations */
+
+void cockpit_channel_control (CockpitChannel *self,
+ const gchar *command,
+ JsonObject *message);
+
+void cockpit_channel_ready (CockpitChannel *self,
+ JsonObject *message);
+
+void cockpit_channel_send (CockpitChannel *self,
+ GBytes *payload,
+ gboolean valid_utf8);
+
+JsonObject * cockpit_channel_get_options (CockpitChannel *self);
+
+JsonObject * cockpit_channel_close_options (CockpitChannel *self);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_CHANNEL_H__ */
diff --git a/src/common/cockpitclosefrom.c b/src/common/cockpitclosefrom.c
new file mode 100644
index 0000000..3eb23fb
--- /dev/null
+++ b/src/common/cockpitclosefrom.c
@@ -0,0 +1,85 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpithacks.h"
+
+#ifndef HAVE_CLOSEFROM
+
+#include <assert.h>
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <limits.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/resource.h>
+
+void
+cockpit_closefrom (int lowfd)
+{
+ DIR *d;
+
+ if ((d = opendir ("/proc/self/fd")))
+ {
+ struct dirent *de;
+
+ while ((de = readdir (d))) {
+ char *e;
+
+ errno = 0;
+ long l = strtol (de->d_name, &e, 10);
+ if (errno != 0 || !e || *e)
+ continue;
+
+ int fd = (int) l;
+
+ if ((long) fd != l)
+ continue;
+
+ if (fd == dirfd (d))
+ continue;
+
+ /* don't bother about error checking; On Linux, EINTR still closes the fd, and with EBADF we already have a closed fd */
+ if (fd >= lowfd)
+ close (fd);
+ }
+
+ closedir (d);
+ }
+ else
+ {
+ /* If /proc is not mounted or not accessible we fall back to the old
+ * rlimit trick */
+ struct rlimit rl;
+ int open_max;
+
+ if (getrlimit (RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY)
+ open_max = rl.rlim_max;
+ else
+ open_max = sysconf (_SC_OPEN_MAX);
+
+ for (int fd = lowfd; fd < open_max; fd++)
+ close (fd);
+ }
+}
+
+#endif
diff --git a/src/common/cockpitconf.c b/src/common/cockpitconf.c
new file mode 100644
index 0000000..9d6f24c
--- /dev/null
+++ b/src/common/cockpitconf.c
@@ -0,0 +1,391 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitconf.h"
+#include "cockpitmemory.h"
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <libgen.h>
+#include <limits.h>
+#include <regex.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* #define DEBUG 1 */
+#if DEBUG
+#define debug(fmt, ...) (fprintf (stderr, "cockpitconf: " fmt "\n", ##__VA_ARGS__))
+#else
+#define debug(...)
+#endif
+
+
+typedef struct Entry {
+ char *section;
+ char *key;
+ char *value;
+ char *strv_value; /* copy of value with all strv_delimiters replaced with \0 */
+ char strv_delimiter;
+ const char **strv_cache; /* value split by strv_delimiter; points into strv_value */
+ struct Entry *next;
+} Entry;
+
+static bool cockpit_conf_loaded = false;
+static Entry *cockpit_conf = NULL;
+
+const char *cockpit_config_file = "cockpit.conf";
+const char *cockpit_config_dirs[] = { PACKAGE_SYSCONF_DIR, NULL };
+
+/*
+ * some helper functions for safe memory allocation
+ */
+
+static void
+regcompx (regex_t *preg, const char *regex, int cflags)
+{
+ int ret = regcomp (preg, regex, cflags);
+ if (ret != 0)
+ {
+ char err[1024];
+ regerror (ret, preg, err, sizeof (err));
+ fprintf (stderr, "failed to compile regular expression: %s\n", err);
+ abort ();
+ }
+}
+
+/* For optimization, this modifies string; the returned array has pointers into string
+ * The array itself gets allocated and must be freed after use. */
+static const char **
+strsplit (char *string, char delimiter)
+{
+ const char ** parts = reallocarrayx (NULL, 2, sizeof (char*));
+ char *cur = string;
+ bool done = false;
+ unsigned len = 0;
+
+ /* backwards compatible special case: a totally empty string gives [], while ":" splits into ["", ""] */
+ if (string && *string)
+ {
+ while (!done)
+ {
+ char *next_delim = strchr (cur, delimiter);
+
+ if (next_delim)
+ *next_delim = '\0';
+ else
+ done = true;
+
+ parts = reallocarrayx (parts, len + 2, sizeof (char*));
+ parts[len++] = cur;
+
+ if (next_delim)
+ cur = next_delim + 1;
+ }
+ }
+
+ parts[len] = NULL;
+ return parts;
+}
+
+/*
+ * internal logic/helpers
+ */
+
+/* See https://developer.gnome.org/glib/stable/glib-Key-value-file-parser.html for the spec */
+static bool
+load_key_file (const char *file_path)
+{
+ FILE *f = NULL;
+ char *cur_section = NULL;
+ regex_t re_section, re_keyval, re_ignore;
+ char *line = NULL;
+ bool ret = true;
+ size_t line_size = 0;
+
+ cockpit_conf_loaded = true;
+
+ f = fopen (file_path, "r");
+ if (!f)
+ {
+ if (errno != ENOENT)
+ warnx ("couldn't load configuration file: %s: %m\n", file_path);
+ return false;
+ }
+
+ regcompx (&re_section, "^[[:space:]]*\\[([^][[:cntrl:]]+)\\][[:space:]]*$", REG_EXTENDED|REG_NEWLINE);
+ regcompx (&re_keyval, "^[[:space:]]*([[:alnum:]-]+)[[:space:]]*=[[:space:]]*(.*)$", REG_EXTENDED|REG_NEWLINE);
+ regcompx (&re_ignore, "^[[:space:]]*(#.*)?$", REG_EXTENDED|REG_NOSUB);
+
+ for (;;)
+ {
+ /* getline returns with -1 and not setting errno on EOL */
+ errno = 0;
+ if (getline (&line, &line_size, f) < 0)
+ {
+ if (errno != 0)
+ {
+ perror ("failed to read line from config file");
+ abort ();
+ }
+ else
+ {
+ break; /* EOL */
+ }
+ }
+
+ /* maximum number of () matches that we want to capture from the above REs, + 1 for the entire string (group 0) */
+ const int MAX_MATCH = 3;
+ regmatch_t matches[MAX_MATCH];
+
+ if (regexec (&re_section, line, MAX_MATCH, matches, 0) == 0)
+ {
+ free (cur_section);
+ cur_section = strndupx (line + matches[1].rm_so, matches[1].rm_eo - matches[1].rm_so);
+ }
+
+ else if (regexec (&re_keyval, line, 3, matches, 0) == 0)
+ {
+ Entry *e;
+
+ if (!cur_section)
+ {
+ warnx ("%s: key=val line not in any section: %s", file_path, line);
+ ret = false;
+ break;
+ }
+
+ e = mallocx (sizeof (Entry));
+ e->section = strdupx (cur_section);
+ e->key = strndupx (line + matches[1].rm_so, matches[1].rm_eo - matches[1].rm_so);
+ e->value = strndupx (line + matches[2].rm_so, matches[2].rm_eo - matches[2].rm_so);
+ e->strv_value = NULL;
+ e->strv_cache = NULL;
+ /* prepend new Entry to cockpit_conf; that way, later values win over earlier ones in a forward search */
+ e->next = cockpit_conf;
+ cockpit_conf = e;
+ }
+
+ else if (regexec (&re_ignore, line, 0, NULL, 0) == 0)
+ {
+ /* comment or empty line */
+ }
+ else
+ {
+ warnx ("%s: invalid line: %s", file_path, line);
+ ret = false;
+ break;
+ }
+ }
+
+ free (line);
+ regfree (&re_section);
+ regfree (&re_keyval);
+ regfree (&re_ignore);
+ fclose (f);
+ free (cur_section);
+
+ if (ret)
+ debug ("Loaded configuration from: %s\n", file_path);
+ else
+ cockpit_conf_cleanup ();
+
+ return ret;
+}
+
+static Entry*
+cockpit_conf_lookup (const char *section,
+ const char *field)
+{
+ Entry *e;
+
+ if (section == NULL || field == NULL)
+ return NULL;
+
+ if (!cockpit_conf_loaded)
+ cockpit_conf_init ();
+
+ for (e = cockpit_conf; e; e = e->next)
+ {
+ /* that cockpit.conf has traditionally been case insensitive for section and key names */
+ if (strcasecmp (e->section, section) == 0 && strcasecmp (e->key, field) == 0)
+ break;
+ }
+
+ return e;
+}
+
+/*
+ * external API
+ */
+
+void
+cockpit_conf_init (void)
+{
+ if (!cockpit_config_file)
+ {
+ debug ("No configuration to load");
+ return;
+ }
+
+ if (strchr (cockpit_config_file, '/'))
+ {
+ load_key_file (cockpit_config_file);
+ }
+ else
+ {
+ const char *const *dirs;
+
+ for (dirs = cockpit_conf_get_dirs (); *dirs; ++dirs)
+ {
+ char *file = NULL;
+ asprintfx (&file, "%s/cockpit/%s", *dirs, cockpit_config_file);
+ load_key_file (file);
+ free (file);
+ }
+ }
+}
+
+void
+cockpit_conf_cleanup (void)
+{
+ Entry *e, *enext = NULL;
+
+ for (e = cockpit_conf; e; e = enext)
+ {
+ free (e->section);
+ free (e->key);
+ free (e->value);
+ free (e->strv_value);
+ free (e->strv_cache);
+ enext = e->next;
+ free (e);
+ }
+
+ cockpit_conf = NULL;
+ cockpit_conf_loaded = false;
+}
+
+const char * const *
+cockpit_conf_get_dirs (void)
+{
+ static const char ** system_config_dirs = NULL;
+ static bool initialized = false;
+
+ if (!initialized)
+ {
+ static char *env;
+
+ initialized = true;
+ env = getenv ("XDG_CONFIG_DIRS");
+ if (env && env[0])
+ {
+ /* strsplit() modifies the string inline, so copy and keep a ref */
+ env = strdup (env);
+ system_config_dirs = strsplit (env, ':');
+ }
+ }
+
+ return (const char * const *) system_config_dirs ?: cockpit_config_dirs;
+}
+
+const char *
+cockpit_conf_string (const char *section,
+ const char *field)
+{
+ const Entry *entry = cockpit_conf_lookup (section, field);
+ return entry ? entry->value : NULL;
+}
+
+bool
+cockpit_conf_bool (const char *section,
+ const char *field,
+ bool defawlt)
+{
+ const char *value = cockpit_conf_string (section, field);
+ if (value)
+ return strcasecmp (value, "yes") == 0 || strcasecmp (value, "true") == 0 || strcmp (value, "1") == 0;
+ return defawlt;
+}
+
+const char * const *
+cockpit_conf_strv (const char *section,
+ const char *field,
+ char delimiter)
+{
+ Entry *entry = cockpit_conf_lookup (section, field);
+
+ if (!entry || !entry->value)
+ return NULL;
+
+ if (entry->strv_cache)
+ {
+ if (delimiter != entry->strv_delimiter)
+ errx (1, "cockpitconf: Looking up strv with different delimiters is not supported");
+ }
+ else
+ {
+ /* strip off trailing whitespace (leading whitespace is already stripped by regexp) */
+ entry->strv_value = strdupx (entry->value);
+ for (char *c = entry->strv_value + strlen (entry->strv_value) - 1; c >= entry->strv_value && isspace (*c); --c)
+ *c = '\0';
+ entry->strv_cache = strsplit (entry->strv_value, delimiter);
+ entry->strv_delimiter = delimiter;
+ }
+
+ return entry->strv_cache;
+}
+
+unsigned
+cockpit_conf_uint (const char *section,
+ const char *field,
+ unsigned default_value,
+ unsigned max,
+ unsigned min)
+{
+ unsigned val = default_value;
+ long long conf_val;
+ char *endptr = NULL;
+
+ const char* conf = cockpit_conf_string (section, field);
+ if (conf)
+ {
+ errno = 0;
+ conf_val = strtoll (conf, &endptr, 10);
+ if ((conf_val == LLONG_MIN || conf_val == LLONG_MAX || conf_val == 0) &&
+ (errno == ERANGE || errno == EINVAL))
+ val = default_value;
+ else if (endptr && endptr[0] != '\0')
+ val = default_value;
+ else if (conf_val > max)
+ val = max;
+ else if (conf_val < min)
+ val = min;
+ else
+ val = (unsigned)conf_val;
+
+ if (conf_val != val)
+ warnx ("Invalid %s %s value '%s', setting to %u", section, field, conf, val);
+ }
+
+ return val;
+}
diff --git a/src/common/cockpitconf.h b/src/common/cockpitconf.h
new file mode 100644
index 0000000..d6d421f
--- /dev/null
+++ b/src/common/cockpitconf.h
@@ -0,0 +1,53 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_CONF_H__
+#define COCKPIT_CONF_H__
+
+#define COCKPIT_CONF_SSH_SECTION "Ssh-Login"
+
+#include <stdbool.h>
+#include <stdint.h>
+
+const char * cockpit_conf_string (const char *section,
+ const char *field);
+
+
+const char * const *
+ cockpit_conf_strv (const char *section,
+ const char *field,
+ char delimiter);
+
+bool cockpit_conf_bool (const char *section,
+ const char *field,
+ bool defawlt);
+
+unsigned cockpit_conf_uint (const char *section,
+ const char *field,
+ unsigned default_value,
+ unsigned max,
+ unsigned min);
+
+const char * const * cockpit_conf_get_dirs (void);
+
+void cockpit_conf_cleanup (void);
+
+void cockpit_conf_init (void);
+
+#endif /* COCKPIT_CONF_H__ */
diff --git a/src/common/cockpitcontrolmessages.c b/src/common/cockpitcontrolmessages.c
new file mode 100644
index 0000000..9c64e5f
--- /dev/null
+++ b/src/common/cockpitcontrolmessages.c
@@ -0,0 +1,98 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitcontrolmessages.h"
+
+#include <gio/gunixfdmessage.h>
+
+void
+cockpit_control_messages_clear (CockpitControlMessages *ccm)
+{
+ for (gint i = 0; i < ccm->n_messages; i++)
+ g_object_unref (ccm->messages[i]);
+ g_free (ccm->messages);
+
+ ccm->messages = NULL;
+ ccm->n_messages = 0;
+}
+
+gboolean
+cockpit_control_messages_empty (CockpitControlMessages *ccm)
+{
+ return ccm->n_messages == 0;
+}
+
+gpointer
+cockpit_control_messages_get_single_message (CockpitControlMessages *ccm,
+ GType message_type,
+ GError **error)
+{
+ if (ccm->n_messages != 1)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "Unexpectedly received %d control messages (one message of type %s expected)",
+ ccm->n_messages, g_type_name (message_type));
+ return NULL;
+ }
+
+ if (!G_TYPE_CHECK_INSTANCE_TYPE (ccm->messages[0], message_type))
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "Unexpectedly received control message of type %s (type %s expected)",
+ G_OBJECT_TYPE_NAME (ccm->messages[0]), g_type_name (message_type));
+ return NULL;
+ }
+
+ return ccm->messages[0];
+}
+
+const gint *
+cockpit_control_messages_peek_fd_list (CockpitControlMessages *ccm,
+ gint *n_fds,
+ GError **error)
+{
+ GUnixFDMessage *message = cockpit_control_messages_get_single_message (ccm, G_TYPE_UNIX_FD_MESSAGE, error);
+
+ if (message == NULL)
+ return NULL;
+
+ return g_unix_fd_list_peek_fds (g_unix_fd_message_get_fd_list (message), n_fds);
+}
+
+gint
+cockpit_control_messages_peek_single_fd (CockpitControlMessages *ccm,
+ GError **error)
+{
+ int n_fds;
+ const gint *fds = cockpit_control_messages_peek_fd_list (ccm, &n_fds, error);
+
+ if (fds == NULL)
+ return -1;
+
+ if (n_fds != 1)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "Unexpectedly received %d file descriptors (1 expected)", n_fds);
+ return -1;
+ }
+
+ return fds[0];
+}
diff --git a/src/common/cockpitcontrolmessages.h b/src/common/cockpitcontrolmessages.h
new file mode 100644
index 0000000..b8ea3b2
--- /dev/null
+++ b/src/common/cockpitcontrolmessages.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+typedef struct
+{
+ GSocketControlMessage **messages;
+ int n_messages;
+} CockpitControlMessages;
+
+#define COCKPIT_CONTROL_MESSAGES_INIT {}
+
+void
+cockpit_control_messages_clear (CockpitControlMessages *ccm);
+
+gboolean
+cockpit_control_messages_empty (CockpitControlMessages *ccm);
+
+gpointer
+cockpit_control_messages_get_single_message (CockpitControlMessages *ccm,
+ GType message_type,
+ GError **error);
+
+const gint *
+cockpit_control_messages_peek_fd_list (CockpitControlMessages *ccm,
+ gint *n_fds,
+ GError **error);
+
+gint
+cockpit_control_messages_peek_single_fd (CockpitControlMessages *ccm,
+ GError **error);
+
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(CockpitControlMessages, cockpit_control_messages_clear)
diff --git a/src/common/cockpiterror.c b/src/common/cockpiterror.c
new file mode 100644
index 0000000..d2d66ee
--- /dev/null
+++ b/src/common/cockpiterror.c
@@ -0,0 +1,32 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpiterror.h"
+
+/**
+ * SECTION:cockpiterror
+ * @title: CockpitError
+ * @short_description: Possible errors that can be returned
+ *
+ * Error codes.
+ */
+
+G_DEFINE_QUARK(cockpit-error, cockpit_error)
diff --git a/src/common/cockpiterror.h b/src/common/cockpiterror.h
new file mode 100644
index 0000000..6094e1c
--- /dev/null
+++ b/src/common/cockpiterror.h
@@ -0,0 +1,53 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_ERROR_H__
+#define __COCKPIT_ERROR_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+/**
+ * COCKPIT_ERROR:
+ *
+ * Error domain for Cockpit. Errors in this domain will be form the
+ * #CockpitError enumeration. See #GError for more information on error
+ * domains.
+ */
+#define COCKPIT_ERROR (cockpit_error_quark ())
+
+/**
+ * CockpitError:
+ * @COCKPIT_ERROR_FAILED: The operation failed.
+ *
+ * Error codes for the #COCKPIT_ERROR error domain and the corresponding
+ * D-Bus error names.
+ */
+typedef enum {
+ COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ COCKPIT_ERROR_PERMISSION_DENIED,
+ COCKPIT_ERROR_FAILED,
+} CockpitError;
+
+GQuark cockpit_error_quark (void);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_ERROR_H__ */
diff --git a/src/common/cockpitfdpassing.c b/src/common/cockpitfdpassing.c
new file mode 100644
index 0000000..3ae3cae
--- /dev/null
+++ b/src/common/cockpitfdpassing.c
@@ -0,0 +1,156 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitfdpassing.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <poll.h>
+#include <string.h>
+#include <unistd.h>
+
+/**
+ * cockpit_socket_msghdr_add_fd:
+ * @msg: a msghdr to be passed to a future call to sendmsg
+ * @cmsg: a buffer
+ * @cmsgsize: sizeof cmsg
+ * @fd: the fd to add to the msghdr
+ *
+ * Adds a cmsg to the given msghdr structure, transmitting @fd.
+ *
+ * @cmsg should be a pointer to a variable of type `struct cmsghdr[2]`
+ * and @cmsgsize should be the sizeof this variable. This is used for
+ * temporary storage and needs to stay around until the sendmsg() call.
+ *
+ * This can't fail.
+ */
+void
+cockpit_socket_msghdr_add_fd (struct msghdr *msg,
+ struct cmsghdr *cmsg,
+ size_t cmsgsize,
+ int fd)
+{
+ /* make sure user's buffer is big enough */
+ assert (CMSG_SPACE(sizeof fd) <= cmsgsize);
+
+ /* make sure nothing else is there */
+ assert (msg->msg_control == NULL);
+ assert (msg->msg_controllen == 0);
+
+ /* we have an fd to send: create an SCM_RIGHTS cmsg. */
+ cmsg->cmsg_len = CMSG_LEN(sizeof fd);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ memcpy (CMSG_DATA(cmsg), &fd, sizeof fd);
+
+ /* attach it to the msghdr */
+ msg->msg_control = cmsg;
+ msg->msg_controllen = CMSG_LEN(sizeof fd);
+}
+
+/**
+ * cockpit_socket_send_fd:
+ * @socket_fd: a unix socket
+ * @fd: the fd to send
+ *
+ * Calls sendmsg() to write a single nul byte, plus a single file
+ * descriptor, @fd.
+ *
+ * If sendmsg() is successful then this function returns %true.
+ * Otherwise, %false is returned and errno will be set.
+ */
+bool
+cockpit_socket_send_fd (int socket_fd,
+ int fd)
+{
+ struct msghdr msg = { .msg_iov = (struct iovec[]){ {(char[]){""}, 1 } },
+ .msg_iovlen = 1 };
+
+ struct cmsghdr cmsg[2];
+ cockpit_socket_msghdr_add_fd (&msg, cmsg, sizeof cmsg, fd);
+
+ ssize_t s;
+ do
+ s = sendmsg (socket_fd, &msg, MSG_NOSIGNAL);
+ while (s == -1 && errno == EINTR);
+
+ return s != -1;
+}
+
+/**
+ * cockpit_socket_receive_fd:
+ * @socket_fd: a unix socket
+ * @out_fd: the received file descriptor
+ *
+ * Calls recvmsg() to receive a single byte and (hopefully) a single file
+ * descriptor. The byte is discarded. The return value of this
+ * function is equal to the return value of the recvmsg() call.
+ *
+ * A return value of -1 indicates a syscall fail (with errno set). 0
+ * means EOF. A return value of 1 means that we received a message, and
+ * will set @fd to any file descriptor we received with the message (or
+ * -1 otherwise).
+ */
+int
+cockpit_socket_receive_fd (int socket_fd,
+ int *out_fd)
+{
+ struct cmsghdr cmsg[2];
+ struct msghdr msg = { .msg_iov = (struct iovec[]){ {(char[]){""}, 1 } },
+ .msg_iovlen = 1,
+ .msg_control = &cmsg,
+ .msg_controllen = CMSG_LEN (sizeof *out_fd) };
+ assert (msg.msg_controllen <= sizeof cmsg);
+
+ /* recvmsg() has no MSG_DONTWAIT, and e.g. sudo makes stdin non-blocking with `log_output` option */
+ struct pollfd socket_pfd = { .fd = socket_fd, .events = POLLIN };
+ int ret;
+ do
+ ret = poll (&socket_pfd, 1, -1);
+ while (ret == -1 && errno == EINTR);
+ if (ret <= 0) /* error or timeout; the latter should not happen, but pass it on as "EOF" */
+ return ret;
+
+ ssize_t s;
+ do
+ s = recvmsg (socket_fd, &msg, 0);
+ while (s == -1 && errno == EINTR);
+
+ if (s == 1)
+ {
+ if (msg.msg_controllen &&
+ cmsg->cmsg_level == SOL_SOCKET &&
+ cmsg->cmsg_type == SCM_RIGHTS)
+ {
+ /* We originally set .msg_controllen to exactly the space
+ * required to receive a single fd, so if we received any
+ * SCM_RIGHTS messages, then it must surely only be a single
+ * fd.
+ */
+ assert (cmsg->cmsg_len == CMSG_LEN (sizeof *out_fd));
+ memcpy (out_fd, CMSG_DATA(cmsg), sizeof *out_fd);
+ }
+ else
+ *out_fd = -1;
+ }
+
+ return s;
+}
diff --git a/src/common/cockpitfdpassing.h b/src/common/cockpitfdpassing.h
new file mode 100644
index 0000000..0531e2f
--- /dev/null
+++ b/src/common/cockpitfdpassing.h
@@ -0,0 +1,37 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <sys/socket.h>
+
+void
+cockpit_socket_msghdr_add_fd (struct msghdr *msg,
+ struct cmsghdr *cmsg,
+ size_t cmsgsize,
+ int fd);
+
+bool
+cockpit_socket_send_fd (int socket_fd,
+ int fd);
+
+int
+cockpit_socket_receive_fd (int socket_fd,
+ int *out_fd);
diff --git a/src/common/cockpitflow.c b/src/common/cockpitflow.c
new file mode 100644
index 0000000..bc6d56c
--- /dev/null
+++ b/src/common/cockpitflow.c
@@ -0,0 +1,96 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * CockpitFlow:
+ *
+ * An interface representing a bidirectional flow. Implementors are
+ * CockpitPipe and CockpitStream. Currently the interface functionality
+ * is limited to flow control.
+ *
+ * - Its input can be throttled, it can listen to a "pressure" signal
+ * from another object passed into cockpit_pipe_throttle()
+ * - It can optionally control another flow, by emitting a "pressure" signal
+ * when it's output queue is too large
+ */
+
+#include "config.h"
+
+#include "cockpitflow.h"
+
+G_DEFINE_INTERFACE (CockpitFlow, cockpit_flow, 0);
+
+static guint cockpit_flow_signal_pressure = 0;
+
+static void
+cockpit_flow_default_init (CockpitFlowInterface *iface)
+{
+ /**
+ * CockpitFlow::pressure:
+ * @throttle: Pressure on or off
+ *
+ * Emitted when the pipe wants to give back-pressure to other feeding
+ * streams. It does this when its output queue is too long and should
+ * slow down.
+ */
+ cockpit_flow_signal_pressure = g_signal_new ("pressure", COCKPIT_TYPE_FLOW, G_SIGNAL_RUN_FIRST,
+ 0, NULL, NULL, g_cclosure_marshal_VOID__BOOLEAN,
+ G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+}
+
+/**
+ * cockpit_flow_throttle:
+ * @flow: The flow to have input throttled
+ * @controlling: A controlling flow that throttles this one
+ *
+ * When the @controlling flow has pressure, it will slow input on this
+ * flow. If @controlling is NULL
+ */
+void
+cockpit_flow_throttle (CockpitFlow *flow,
+ CockpitFlow *controlling)
+{
+ CockpitFlowInterface *iface;
+
+ g_return_if_fail (COCKPIT_IS_FLOW (flow));
+ g_return_if_fail (controlling == NULL || COCKPIT_IS_FLOW (controlling));
+
+ iface = COCKPIT_FLOW_GET_IFACE (flow);
+ g_return_if_fail (iface->throttle != NULL);
+ (iface->throttle) (flow, controlling);
+}
+
+/**
+ * cockpit_flow_emit_pressure:
+ * @flow: The flow
+ * @ressure: Whether to emit back-pressure or release it.
+ *
+ * Emit a "pressure" signal, which indicates back-pressure or
+ * releases it. Used by implementations of CockpitFlow
+ *
+ * This is used to throttle another flow's input if this
+ * flow is the controlling flow.
+ */
+void
+cockpit_flow_emit_pressure (CockpitFlow *flow,
+ gboolean pressure)
+{
+ g_return_if_fail (COCKPIT_IS_FLOW (flow));
+ g_signal_emit (flow, cockpit_flow_signal_pressure, 0, pressure);
+}
diff --git a/src/common/cockpitflow.h b/src/common/cockpitflow.h
new file mode 100644
index 0000000..97cba0d
--- /dev/null
+++ b/src/common/cockpitflow.h
@@ -0,0 +1,45 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_FLOW_H__
+#define COCKPIT_FLOW_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_FLOW (cockpit_flow_get_type ())
+G_DECLARE_INTERFACE(CockpitFlow, cockpit_flow, COCKPIT, FLOW, GObject)
+
+struct _CockpitFlowInterface {
+ GTypeInterface parent_iface;
+
+ void (* throttle) (CockpitFlow *flow,
+ CockpitFlow *controlling);
+};
+
+void cockpit_flow_throttle (CockpitFlow *flow,
+ CockpitFlow *controller);
+
+void cockpit_flow_emit_pressure (CockpitFlow *flow,
+ gboolean pressure);
+
+G_END_DECLS
+
+#endif /* COCKPIT_FLOW_H__ */
diff --git a/src/common/cockpitframe.c b/src/common/cockpitframe.c
new file mode 100644
index 0000000..337f068
--- /dev/null
+++ b/src/common/cockpitframe.c
@@ -0,0 +1,302 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitframe.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define MAX_FRAME_SIZE_BYTES 8
+
+/**
+ * cockpit_frame_parse:
+ * @input: An buffer of bytes
+ * @length: The length of @input buffer
+ * @consumed: Number of bytes consumed from @input
+ *
+ * Parse message framing length string from the top
+ * of the @input buffer. These are used by Cockpit transport framing
+ * over a stream based protocol.
+ *
+ * Returns: The length, zero if more data is needed, or -1 if an error.
+ */
+ssize_t
+cockpit_frame_parse (unsigned char *input,
+ size_t length,
+ size_t *consumed)
+{
+ size_t size = 0;
+ size_t i;
+
+ assert (input != NULL || length == 0);
+
+ size = 0;
+ for (i = 0; i < length; i++)
+ {
+ /* Check invalid characters, prevent integer overflow, limit max length */
+ if (i >= MAX_FRAME_SIZE_BYTES || (char)(input[i]) < '0' || (char)(input[i]) > '9')
+ break;
+ size *= 10;
+ size += (char)(input[i]) - '0';
+ }
+
+ /* Want more data */
+ if (i == length)
+ return 0;
+
+ /* Improperly formatted if any of the following cases:
+ * - no digits read
+ * - digits not followed by newline
+ * - size had a leading zero
+ */
+ if (size == 0 || input[i] != '\n' || input[0] == '0')
+ return -1;
+
+ if (consumed)
+ *consumed = i + 1;
+ return size;
+}
+
+ssize_t
+cockpit_fd_write_all (int fd,
+ unsigned char *data,
+ size_t length)
+{
+ ssize_t written = 0;
+ ssize_t res;
+
+ assert (data != NULL || length == 0);
+
+ while (length > 0)
+ {
+ res = write (fd, data, length);
+ if (res < 0)
+ {
+ if (errno != EAGAIN && errno != EINTR)
+ return -1;
+ }
+ else
+ {
+ data += res;
+ length -= res;
+ written += res;
+ }
+ }
+
+ return written;
+}
+
+ssize_t
+cockpit_frame_write (int fd,
+ unsigned char *input,
+ size_t length)
+{
+ char *prefix = NULL;
+ ssize_t ret = -1;
+ int errn = 0;
+
+ assert (length > 0);
+ assert (input != NULL);
+
+ if (asprintf (&prefix, "%u\n", (unsigned int)length) < 0)
+ {
+ errn = ENOMEM;
+ goto out;
+ }
+
+ ret = cockpit_fd_write_all (fd, (unsigned char *)prefix, strlen (prefix));
+ if (ret > 0)
+ ret = cockpit_fd_write_all (fd, input, length);
+ if (ret < 0)
+ errn = errno;
+
+out:
+ free (prefix);
+ if (ret < 0)
+ errno = errn;
+ return ret;
+}
+
+/* read_exactly:
+ * @fd: a blocking file descriptor
+ * @buffer: where to read to
+ * @required_size: the exact number of bytes to read
+ * @was_eof: if EOF was encountered
+ *
+ * Reads exactly @required_size bytes from @fd into @buffer. If the
+ * correct number of bytes are read, then %TRUE is returned.
+ *
+ * On failure, returns %FALSE. If the failure was due to an underlying
+ * read error, then this error will be stored into errno. If the
+ * failure was due to an incorrect number of bytes being read, then
+ * errno will be set to %EBADMSG.
+ *
+ * The only permitted exception is if @was_eof is non-%NULL. In that
+ * case, if exactly 0 bytes are read from @fd (ie: EOF at the start of
+ * the message) then @was_eof will be set to %TRUE and the call will
+ * return successfully. Otherwise, @was_eof will be set to %FALSE on
+ * successful completion.
+ *
+ * If the required number of bytes couldn't be read, then any number of
+ * bytes may have been read from @fd. The only reasonable thing to do
+ * at this point is to treat the connection as broken, and close @fd.
+ * In particular, this is why @fd should never be non-blocking.
+ */
+static bool
+read_exactly (int fd,
+ unsigned char *buffer,
+ size_t required_size,
+ bool *was_eof)
+{
+ size_t offset = 0;
+
+ while (offset < required_size)
+ {
+ ssize_t n = read (fd, buffer + offset, required_size - offset);
+ if (n == -1)
+ {
+ if (errno == EINTR)
+ continue;
+
+ if (errno != ECONNRESET)
+ return false;
+
+ /* ECONNRESET is treated as EOF */
+ n = 0;
+ }
+
+ if (n == 0)
+ {
+ if (was_eof != NULL && offset == 0)
+ {
+ *was_eof = true;
+ return true;
+ }
+
+ errno = EBADMSG;
+ return false;
+ }
+
+ offset += n;
+ }
+
+ if (was_eof)
+ *was_eof = false;
+
+ return true;
+}
+
+ssize_t
+cockpit_frame_read (int fd,
+ unsigned char **output)
+{
+ /* We first need to read the size of the frame, followed by the
+ * content of the frame. We want to do this efficiently as possible,
+ * while avoiding to read() more than the frame (since we can't put
+ * bytes back). MSG_PEEK is also not always available, since we're
+ * often talking to a pipe.
+ *
+ * Fortunately, we have a reasonable approach for this.
+ *
+ * Empty frames are invalid (cockpit_frame_parse() rejects size == 0),
+ * so the smallest possible frame has a length of at least 3: the
+ * single-digit size, the newline, then the single byte of body.
+ * Therefore it's always safe to read 3 bytes ("the initial read").
+ *
+ * Conveniently, reading three bytes is also always enough to tell us
+ * how many bytes it's safe to read in order to determine the size of
+ * the frame:
+ *
+ * - if we read a digit or two, followed by a newline, then we
+ * already know the size of the entire frame
+ *
+ * - if we read three digits, we know that the frame body is at
+ * least 100 bytes long. Since the maximum size of the length
+ * field is 8 (+1 for newline), we can safely read this entire
+ * amount.
+ *
+ * - if we get something other than digits or newlines, it's an
+ * error
+ */
+ size_t n_read = 3;
+ unsigned char headerbuf[MAX_FRAME_SIZE_BYTES + 1];
+ bool eof;
+
+ if (!read_exactly (fd, headerbuf, n_read, &eof))
+ return -1;
+
+ if (eof)
+ {
+ if (output)
+ *output = NULL;
+ return 0;
+ }
+
+ size_t n_consumed;
+ ssize_t size = cockpit_frame_parse (headerbuf, n_read, &n_consumed);
+ if (size == 0)
+ {
+ /* cockpit_frame_parse() asked to read more data. As explained
+ * above, it's safe to read the rest of the buffer now (6 bytes).
+ * This should always result in a defined (non-zero) result.
+ */
+ if (!read_exactly (fd, headerbuf + n_read, sizeof headerbuf - n_read, NULL))
+ return -1;
+
+ n_read = sizeof headerbuf;
+ size = cockpit_frame_parse (headerbuf, n_read, &n_consumed);
+ assert (size != 0);
+ }
+
+ if (size == -1)
+ {
+ errno = EBADMSG;
+ return -1;
+ }
+
+ /* We now have size equal to the number of bytes we need to return. */
+ unsigned char *buffer = malloc (size);
+ if (buffer == NULL)
+ return -1; /* ENOMEM */
+
+ /* Copy the non-consumed bytes from the header (might be zero) */
+ size_t bytes_from_header = n_read - n_consumed;
+ memcpy (buffer, headerbuf + n_consumed, bytes_from_header);
+
+ /* Get the rest of the body (might be zero) */
+ if (!read_exactly (fd, buffer + bytes_from_header, size - bytes_from_header, NULL))
+ {
+ free (buffer);
+ return -1;
+ }
+
+ if (output)
+ *output = buffer;
+ else
+ free (buffer);
+
+ return size;
+}
diff --git a/src/common/cockpitframe.h b/src/common/cockpitframe.h
new file mode 100644
index 0000000..52dbe56
--- /dev/null
+++ b/src/common/cockpitframe.h
@@ -0,0 +1,40 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_FRAME_H__
+#define __COCKPIT_FRAME_H__
+
+#include <sys/types.h>
+
+ssize_t cockpit_frame_parse (unsigned char *input,
+ size_t length,
+ size_t *consumed);
+
+ssize_t cockpit_frame_read (int fd,
+ unsigned char **output);
+
+ssize_t cockpit_frame_write (int fd,
+ unsigned char *input,
+ size_t length);
+
+ssize_t cockpit_fd_write_all (int fd,
+ unsigned char *input,
+ size_t length);
+
+#endif /* __COCKPIT_FRAME_H__ */
diff --git a/src/common/cockpithacks-glib.h b/src/common/cockpithacks-glib.h
new file mode 100644
index 0000000..6b137aa
--- /dev/null
+++ b/src/common/cockpithacks-glib.h
@@ -0,0 +1,80 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/* See the comment in cockpithacks.h for an explanation of why this file
+ * exists.
+ *
+ * This file is intended to be included from code which is built against
+ * GLib. For the "pure C" parts of cockpit, include cockpithacks.h
+ * directly.
+ */
+
+#include "cockpithacks.h"
+#include <glib.h>
+
+/* g_debug() defaults to writing its output to stdout, which doesn't
+ * really work for us. GLib 2.67.0 introduced an API to change this
+ * behaviour, and we want to use this API if it's available.
+ *
+ * Otherwise, we know that earlier versions of GLib use the `stdout`
+ * `FILE *` to do its write, while we write directly to fd 1, so we can
+ * replace the value of `stdout` to cause GLib to write elsewhere. This
+ * is specifically not allowed by POSIX, but works on Linux.
+ */
+#include <stdio.h>
+
+static inline void
+cockpit_hacks_redirect_gdebug_to_stderr (void)
+{
+#if GLIB_CHECK_VERSION(2,67,0)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+ g_log_writer_default_set_use_stderr (TRUE);
+#pragma GCC diagnostic pop
+#else
+ stdout = stderr;
+#endif
+}
+
+/* g_assert_no_errno: Remove this after we depend on GLib 2.66
+ *
+ * Even if GLib does define this, we want to undefine it and define our
+ * own copy so that we can remove the version warning.
+ */
+#ifdef g_assert_no_errno
+#undef g_assert_no_errno
+#endif
+
+#include <errno.h>
+/* Direct copy, minus 'GLIB_AVAILABLE_MACRO_IN_2_66' */
+#define g_assert_no_errno(expr) G_STMT_START { \
+ int __ret, __errsv; \
+ errno = 0; \
+ __ret = expr; \
+ __errsv = errno; \
+ if (__ret < 0) \
+ { \
+ gchar *__msg; \
+ __msg = g_strdup_printf ("assertion failed (" #expr " >= 0): errno %i: %s", __errsv, g_strerror (__errsv)); \
+ g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, __msg); \
+ g_free (__msg); \
+ } \
+ } G_STMT_END
diff --git a/src/common/cockpithacks.h b/src/common/cockpithacks.h
new file mode 100644
index 0000000..2d2acdf
--- /dev/null
+++ b/src/common/cockpithacks.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/* Here's where we try to implement "temporary" workaround code:
+ * anything that might be labelled TODO, FIXME, HACK, XXX, etc. Having
+ * the workarounds in one place gives at least two advantages:
+ *
+ * - common workarounds made in several places can be given a single
+ * function/macro/etc. defined in this file, giving a common place
+ * to document the workaround, links to related bug reports, etc.
+ * This also makes removing the workaround easier: remove it from
+ * this file and the compiler will help us find all the places where
+ * we were using it.
+ *
+ * For this reason, we even add small functions here for totally
+ * trivial stuff.
+ *
+ * - having all of our hacks in a central place gives us one place to
+ * periodically check, allowing us to remove hacks that are not
+ * longer relevant (because the bug got fixed, etc).
+ *
+ * This file is intended to be included from code which isn't using
+ * GLib, and therefore must only contain "pure libc" C. For code that
+ * depends on GLib (and friends), there's cockpithacks-glib.h.
+ */
+
+#ifndef PACKAGE_VERSION
+#error config.h should be included from the top of every .c file
+#endif
+
+#ifndef HAVE_CLOSEFROM
+#define closefrom(lowfd) cockpit_closefrom(lowfd)
+void cockpit_closefrom (int lowfd);
+#endif
diff --git a/src/common/cockpithash.c b/src/common/cockpithash.c
new file mode 100644
index 0000000..9858e3e
--- /dev/null
+++ b/src/common/cockpithash.c
@@ -0,0 +1,41 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpithash.h"
+
+guint
+cockpit_str_case_hash (gconstpointer v)
+{
+ /* A case agnostic version of g_str_hash */
+ const signed char *p;
+ guint32 h = 5381;
+ for (p = v; *p != '\0'; p++)
+ h = (h << 5) + h + g_ascii_tolower (*p);
+ return h;
+}
+
+gboolean
+cockpit_str_case_equal (gconstpointer v1,
+ gconstpointer v2)
+{
+ /* A case agnostic version of g_str_equal */
+ return g_ascii_strcasecmp (v1, v2) == 0;
+}
diff --git a/src/common/cockpithash.h b/src/common/cockpithash.h
new file mode 100644
index 0000000..585e062
--- /dev/null
+++ b/src/common/cockpithash.h
@@ -0,0 +1,34 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_HASH_H__
+#define __COCKPIT_HASH_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+guint cockpit_str_case_hash (gconstpointer v);
+
+gboolean cockpit_str_case_equal (gconstpointer v1,
+ gconstpointer v2);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_HASH_H__ */
diff --git a/src/common/cockpithex.c b/src/common/cockpithex.c
new file mode 100644
index 0000000..8afcc28
--- /dev/null
+++ b/src/common/cockpithex.c
@@ -0,0 +1,86 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpithex.h"
+
+#include "cockpitmemory.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+static const char HEX[] = "0123456789abcdef";
+
+char *
+cockpit_hex_encode (const void * data,
+ ssize_t length)
+{
+ const unsigned char *in = data;
+ char *out;
+ size_t i;
+
+ if (length < 0)
+ length = strlen (data);
+
+ out = mallocx (length * 2 + 1);
+ for (i = 0; i < length; i++)
+ {
+ out[i * 2] = HEX[in[i] >> 4];
+ out[i * 2 + 1] = HEX[in[i] & 0xf];
+ }
+ out[i * 2] = '\0';
+ return out;
+}
+
+void *
+cockpit_hex_decode (const char *hex,
+ ssize_t hexlen,
+ size_t *length)
+{
+ const char *hpos;
+ const char *lpos;
+ char *out;
+ int i;
+
+ if (hexlen < 0)
+ hexlen = strlen (hex);
+ if (hexlen % 2 != 0)
+ return NULL;
+
+ out = mallocx (hexlen / 2 + 1);
+ for (i = 0; i < hexlen / 2; i++)
+ {
+ hpos = strchr (HEX, hex[i * 2]);
+ lpos = strchr (HEX, hex[i * 2 + 1]);
+ if (hpos == NULL || lpos == NULL)
+ {
+ free (out);
+ return NULL;
+ }
+ out[i] = ((hpos - HEX) << 4) | ((lpos - HEX) & 0xf);
+ }
+
+ /* A convenience null termination */
+ out[i] = '\0';
+
+ if (length)
+ *length = i;
+ return out;
+}
diff --git a/src/common/cockpithex.h b/src/common/cockpithex.h
new file mode 100644
index 0000000..9dcb930
--- /dev/null
+++ b/src/common/cockpithex.h
@@ -0,0 +1,32 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_HEX_H__
+#define __COCKPIT_HEX_H__
+
+#include <sys/types.h>
+
+char * cockpit_hex_encode (const void *data,
+ ssize_t length);
+
+void * cockpit_hex_decode (const char *hex,
+ ssize_t hexlen,
+ size_t *length);
+
+#endif
diff --git a/src/common/cockpitjson.c b/src/common/cockpitjson.c
new file mode 100644
index 0000000..9052dbb
--- /dev/null
+++ b/src/common/cockpitjson.c
@@ -0,0 +1,1131 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitjson.h"
+
+#include <math.h>
+#include <string.h>
+
+gboolean
+cockpit_json_get_int (JsonObject *object,
+ const gchar *name,
+ gint64 defawlt,
+ gint64 *value)
+{
+ JsonNode *node;
+
+ node = json_object_get_member (object, name);
+ if (!node)
+ {
+ if (value)
+ *value = defawlt;
+ return TRUE;
+ }
+ else if (json_node_get_value_type (node) == G_TYPE_INT64 ||
+ json_node_get_value_type (node) == G_TYPE_DOUBLE)
+ {
+ if (value)
+ *value = json_node_get_int (node);
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+gboolean
+cockpit_json_get_double (JsonObject *object,
+ const gchar *name,
+ gdouble defawlt,
+ gdouble *value)
+{
+ JsonNode *node;
+
+ node = json_object_get_member (object, name);
+ if (!node)
+ {
+ if (value)
+ *value = defawlt;
+ return TRUE;
+ }
+ else if (json_node_get_value_type (node) == G_TYPE_INT64 ||
+ json_node_get_value_type (node) == G_TYPE_DOUBLE)
+ {
+ if (value)
+ *value = json_node_get_double (node);
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+gboolean
+cockpit_json_get_bool (JsonObject *object,
+ const gchar *name,
+ gboolean defawlt,
+ gboolean *value)
+{
+ JsonNode *node;
+
+ node = json_object_get_member (object, name);
+ if (!node)
+ {
+ if (value)
+ *value = defawlt;
+ return TRUE;
+ }
+ else if (json_node_get_value_type (node) == G_TYPE_BOOLEAN)
+ {
+ if (value)
+ *value = json_node_get_boolean (node);
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+
+gboolean
+cockpit_json_get_string (JsonObject *options,
+ const gchar *name,
+ const gchar *defawlt,
+ const gchar **value)
+{
+ JsonNode *node;
+
+ node = json_object_get_member (options, name);
+ if (!node)
+ {
+ if (value)
+ *value = defawlt;
+ return TRUE;
+ }
+ else if (json_node_get_value_type (node) == G_TYPE_STRING)
+ {
+ if (value)
+ *value = json_node_get_string (node);
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+gboolean
+cockpit_json_get_array (JsonObject *options,
+ const gchar *name,
+ JsonArray *defawlt,
+ JsonArray **value)
+{
+ JsonNode *node;
+
+ node = json_object_get_member (options, name);
+ if (!node)
+ {
+ if (value)
+ *value = defawlt;
+ return TRUE;
+ }
+ else if (json_node_get_node_type (node) == JSON_NODE_ARRAY)
+ {
+ if (value)
+ *value = json_node_get_array (node);
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+gboolean
+cockpit_json_get_object (JsonObject *options,
+ const gchar *member,
+ JsonObject *defawlt,
+ JsonObject **value)
+{
+ JsonNode *node;
+
+ node = json_object_get_member (options, member);
+ if (!node)
+ {
+ if (value)
+ *value = defawlt;
+ return TRUE;
+ }
+ else if (json_node_get_node_type (node) == JSON_NODE_OBJECT)
+ {
+ if (value)
+ *value = json_node_get_object (node);
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+/**
+ * cockpit_json_get_strv:
+ * @options: the json object
+ * @member: the member name
+ * @defawlt: defawlt value
+ * @value: returned value
+ *
+ * Gets a string array member from a JSON object. Validates
+ * that the member is an array, and that all elements in the
+ * array are strings. If these fail, then will return %FALSE.
+ *
+ * If @member does not exist in @options, returns the values
+ * in @defawlt.
+ *
+ * The returned value in @value should be freed with g_free()
+ * but the actual strings are owned by the JSON object.
+ *
+ * Returns: %FALSE if invalid member, %TRUE if valid or missing.
+ */
+gboolean
+cockpit_json_get_strv (JsonObject *options,
+ const gchar *member,
+ const gchar **defawlt,
+ const gchar ***value)
+{
+ gboolean valid = FALSE;
+ JsonArray *array;
+ JsonNode *node;
+ guint length, i;
+ const gchar **val = NULL;
+
+ node = json_object_get_member (options, member);
+ if (!node)
+ {
+ if (defawlt)
+ val = g_memdup (defawlt, sizeof (gchar *) * (g_strv_length ((gchar **)defawlt) + 1));
+ valid = TRUE;
+ }
+ else if (json_node_get_node_type (node) == JSON_NODE_ARRAY)
+ {
+ valid = TRUE;
+ array = json_node_get_array (node);
+ length = json_array_get_length (array);
+ val = g_new (const gchar *, length + 1);
+ for (i = 0; i < length; i++)
+ {
+ node = json_array_get_element (array, i);
+ if (json_node_get_value_type (node) == G_TYPE_STRING)
+ {
+ val[i] = json_node_get_string (node);
+ }
+ else
+ {
+ valid = FALSE;
+ break;
+ }
+ }
+ val[length] = NULL;
+ }
+
+ if (valid && value)
+ *value = val;
+ else
+ g_free (val);
+
+ return valid;
+}
+
+gboolean
+cockpit_json_get_null (JsonObject *object,
+ const gchar *member,
+ gboolean *present)
+{
+ JsonNode *node;
+
+ node = json_object_get_member (object, member);
+ if (!node)
+ {
+ if (present)
+ *present = FALSE;
+ return TRUE;
+ }
+ else if (json_node_get_node_type (node) == JSON_NODE_NULL)
+ {
+ if (present)
+ *present = TRUE;
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+}
+
+gboolean
+cockpit_json_equal_object (JsonObject *previous,
+ JsonObject *current)
+{
+ const gchar *name = NULL;
+ gboolean ret = TRUE;
+ GList *names;
+ GList *l;
+
+ names = json_object_get_members (previous);
+ names = g_list_concat (names, json_object_get_members (current));
+ names = g_list_sort (names, (GCompareFunc)strcmp);
+
+ for (l = names; l != NULL; l = g_list_next (l))
+ {
+ if (name && g_str_equal (name, l->data))
+ continue;
+
+ name = l->data;
+ if (!cockpit_json_equal (json_object_get_member (previous, name),
+ json_object_get_member (current, name)))
+ {
+ ret = FALSE;
+ break;
+ }
+ }
+
+ g_list_free (names);
+ return ret;
+}
+
+static gboolean
+cockpit_json_equal_array (JsonArray *previous,
+ JsonArray *current)
+{
+ guint len_previous;
+ guint len_current;
+ guint i;
+
+ len_previous = json_array_get_length (previous);
+ len_current = json_array_get_length (current);
+
+ if (len_previous != len_current)
+ return FALSE;
+
+ /* Look for something that has changed */
+ for (i = 0; i < len_previous; i++)
+ {
+ if (!cockpit_json_equal (json_array_get_element (previous, i),
+ json_array_get_element (current, i)))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * cockpit_json_equal:
+ * @previous: first JSON thing or %NULL
+ * @current: second JSON thing or %NULL
+ *
+ * Compares whether two JSON nodes are equal or not. Accepts
+ * %NULL for either parameter, and if both are %NULL is equal.
+ *
+ * The keys of objects do not have to be in the same order.
+ *
+ * If nodes have different types or value types then equality
+ * is FALSE.
+ *
+ * Returns: whether equal or not
+ */
+gboolean
+cockpit_json_equal (JsonNode *previous,
+ JsonNode *current)
+
+{
+ JsonNodeType type = 0;
+ GType gtype = 0;
+
+ if (previous == current)
+ return TRUE;
+ if (!previous || !current)
+ return FALSE;
+
+ type = json_node_get_node_type (previous);
+ if (type != json_node_get_node_type (current))
+ return FALSE;
+ if (type == JSON_NODE_VALUE)
+ {
+ gtype = json_node_get_value_type (previous);
+ if (gtype != json_node_get_value_type (current))
+ return FALSE;
+ }
+
+ /* Now compare values */
+ switch (type)
+ {
+ case JSON_NODE_OBJECT:
+ return cockpit_json_equal_object (json_node_get_object (previous),
+ json_node_get_object (current));
+ case JSON_NODE_ARRAY:
+ return cockpit_json_equal_array (json_node_get_array (previous),
+ json_node_get_array (current));
+ case JSON_NODE_NULL:
+ return TRUE;
+
+ case JSON_NODE_VALUE:
+ if (gtype == G_TYPE_INT64)
+ return json_node_get_int (previous) == json_node_get_int (current);
+ else if (gtype == G_TYPE_DOUBLE)
+ return json_node_get_double (previous) == json_node_get_double (current);
+ else if (gtype == G_TYPE_BOOLEAN)
+ return json_node_get_boolean (previous) == json_node_get_boolean (current);
+ else if (gtype == G_TYPE_STRING)
+ return g_strcmp0 (json_node_get_string (previous), json_node_get_string (current)) == 0;
+ else
+ return TRUE;
+
+ default:
+ return FALSE;
+ }
+}
+
+/**
+ * cockpit_json_override:
+ * @target: a JSON object
+ * @override: a JSON object to override from
+ *
+ * Override the values in @target with the members in @override.
+ * Any members of @override are set on @target. If both contain
+ * objects for a given member, then these are overridden in turn.
+ *
+ * Any members that are set to null in the @override will be
+ * removed from the @target.
+ */
+void
+cockpit_json_patch (JsonObject *target,
+ JsonObject *override)
+{
+ GList *l, *members;
+ JsonNode *node, *other;
+
+ members = json_object_get_members (override);
+ for (l = members; l != NULL; l = g_list_next (l))
+ {
+ node = json_object_get_member (override, l->data);
+ if (JSON_NODE_HOLDS_NULL (node))
+ {
+ json_object_remove_member (target, l->data);
+ continue;
+ }
+ else if (JSON_NODE_HOLDS_OBJECT (node))
+ {
+ other = json_object_get_member (target, l->data);
+ if (other && JSON_NODE_HOLDS_OBJECT (other))
+ {
+ cockpit_json_patch (json_node_get_object (other),
+ json_node_get_object (node));
+ continue;
+ }
+ }
+
+ json_object_set_member (target, l->data, json_node_copy (node));
+ }
+ g_list_free (members);
+}
+
+/**
+ * cockpit_json_int_hash:
+ * @v: pointer to a gint64
+ *
+ * Hash a pointer to a gint64. This is like g_int_hash()
+ * but for gint64.
+ *
+ * Returns: the hash
+ */
+guint
+cockpit_json_int_hash (gconstpointer v)
+{
+ return (guint)*((const guint64 *)v);
+}
+
+/**
+ * cockpit_json_int_equal:
+ * @v1: pointer to a gint64
+ * @v2: pointer to a gint64
+ *
+ * Compare pointers to a gint64. This is like g_int_equal()
+ * but for gint64.
+ *
+ * Returns: the hash
+ */
+gboolean
+cockpit_json_int_equal (gconstpointer v1,
+ gconstpointer v2)
+{
+ return *((const guint64 *)v1) == *((const guint64 *)v2);
+}
+
+/**
+ * cockpit_json_parse:
+ * @data: string data to parse
+ * @length: length of @data or -1
+ * @error: optional location to return an error
+ *
+ * Parses JSON into a JsonNode.
+ *
+ * Returns: (transfer full): the parsed node or %NULL
+ */
+JsonNode *
+cockpit_json_parse (const gchar *data,
+ gssize length,
+ GError **error)
+{
+ static GPrivate cached_parser = G_PRIVATE_INIT (g_object_unref);
+ JsonParser *parser;
+ JsonNode *root;
+
+ parser = g_private_get (&cached_parser);
+ if (parser == NULL)
+ {
+ parser = json_parser_new ();
+ g_private_set (&cached_parser, parser);
+ }
+
+ if (!json_parser_load_from_data (parser, data, length, error))
+ return NULL;
+
+ root = json_parser_steal_root (parser);
+ if (root == NULL)
+ {
+ g_set_error (error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE,
+ "JSON data was empty");
+ return NULL;
+ }
+
+ return root;
+}
+
+/**
+ * cockpit_json_parse_object:
+ * @data: string data to parse
+ * @length: length of @data or -1
+ * @error: optional location to return an error
+ *
+ * Parses JSON GBytes into a JsonObject. This is a helper function
+ * combining cockpit_json_parse(), json_node_get_type() and
+ * json_node_get_object().
+ *
+ * Returns: (transfer full): the parsed object or %NULL
+ */
+JsonObject *
+cockpit_json_parse_object (const gchar *data,
+ gssize length,
+ GError **error)
+{
+ JsonNode *node;
+ JsonObject *object;
+
+ node = cockpit_json_parse (data, length, error);
+ if (!node)
+ return NULL;
+
+ if (json_node_get_node_type (node) != JSON_NODE_OBJECT)
+ {
+ object = NULL;
+ g_set_error (error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_UNKNOWN, "Not a JSON object");
+ }
+ else
+ {
+ object = json_node_dup_object (node);
+ }
+
+ json_node_free (node);
+ return object;
+}
+
+/**
+ * cockpit_json_parse_bytes:
+ * @data: data to parse
+ * @error: optional location to return an error
+ *
+ * Parses JSON GBytes into a JsonObject. This is a helper function
+ * combining cockpit_json_parse(), json_node_get_type() and
+ * json_node_get_object().
+ *
+ * Returns: (transfer full): the parsed object or %NULL
+ */
+JsonObject *
+cockpit_json_parse_bytes (GBytes *data,
+ GError **error)
+{
+ gsize length = g_bytes_get_size (data);
+
+ if (length == 0)
+ {
+ g_set_error (error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE,
+ "JSON data was empty");
+ return NULL;
+ }
+
+ return cockpit_json_parse_object (g_bytes_get_data (data, NULL), length, error);
+}
+
+/**
+ * cockpit_json_write_bytes:
+ * @object: object to write
+ *
+ * Encode a JsonObject to a GBytes.
+ *
+ * Returns: (transfer full): the encoded data
+ */
+GBytes *
+cockpit_json_write_bytes (JsonObject *object)
+{
+ gchar *data;
+ gsize length;
+
+ data = cockpit_json_write_object (object, &length);
+ return g_bytes_new_take (data, length);
+}
+
+/**
+ * cockpit_json_write_object:
+ * @object: object to write
+ * @length: optionally a location to return the length
+ *
+ * Encode a JsonObject to a string.
+ *
+ * Returns: (transfer full): the encoded data
+ */
+gchar *
+cockpit_json_write_object (JsonObject *object,
+ gsize *length)
+{
+ JsonNode *node;
+ gchar *ret;
+
+ node = json_node_new (JSON_NODE_OBJECT);
+ json_node_set_object (node, object);
+ ret = cockpit_json_write (node, length);
+ json_node_free (node);
+
+ return ret;
+}
+
+/*
+ * HACK: JsonGenerator is completely borked, so we've copied it\
+ * here until we can rely on a fixed version.
+ *
+ * https://bugzilla.gnome.org/show_bug.cgi?id=727593
+ */
+
+static gchar *dump_value (const gchar *name,
+ JsonNode *node,
+ gsize *length);
+static gchar *dump_array (const gchar *name,
+ JsonArray *array,
+ gsize *length);
+static gchar *dump_object (const gchar *name,
+ JsonObject *object,
+ gsize *length);
+
+static gchar *
+json_strescape (const gchar *str)
+{
+ const gchar *p;
+ const gchar *end;
+ GString *output;
+ gsize len;
+
+ len = strlen (str);
+ end = str + len;
+ output = g_string_sized_new (len);
+
+ for (p = str; p < end; p++)
+ {
+ if (*p == '\\' || *p == '"')
+ {
+ g_string_append_c (output, '\\');
+ g_string_append_c (output, *p);
+ }
+ else if ((*p > 0 && *p < 0x1f) || *p == 0x7f)
+ {
+ switch (*p)
+ {
+ case '\b':
+ g_string_append (output, "\\b");
+ break;
+ case '\f':
+ g_string_append (output, "\\f");
+ break;
+ case '\n':
+ g_string_append (output, "\\n");
+ break;
+ case '\r':
+ g_string_append (output, "\\r");
+ break;
+ case '\t':
+ g_string_append (output, "\\t");
+ break;
+ default:
+ g_string_append_printf (output, "\\u00%02x", (guint)*p);
+ break;
+ }
+ }
+ else
+ {
+ g_string_append_c (output, *p);
+ }
+ }
+
+ return g_string_free (output, FALSE);
+}
+
+static gchar *
+dump_value (const gchar *name,
+ JsonNode *node,
+ gsize *length)
+{
+ g_autoptr(GString) buffer = g_string_new ("");
+
+ if (name)
+ g_string_append_printf (buffer, "\"%s\":", name);
+
+ GType type = json_node_get_value_type (node);
+ if (type == G_TYPE_INT64)
+ {
+ g_string_append_printf (buffer, "%" G_GINT64_FORMAT, json_node_get_int (node));
+ }
+ else if (type == G_TYPE_DOUBLE)
+ {
+ gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
+ gdouble d = json_node_get_double (node);
+
+ if (fpclassify (d) == FP_NAN || fpclassify (d) == FP_INFINITE)
+ {
+ g_string_append (buffer, "null");
+ }
+ else
+ {
+ g_string_append (buffer, g_ascii_dtostr (buf, sizeof (buf), d));
+ }
+ }
+ else if (type == G_TYPE_BOOLEAN)
+ {
+ g_string_append (buffer, json_node_get_boolean (node) ? "true" : "false");
+ }
+ else if (type == G_TYPE_STRING)
+ {
+ gchar *tmp;
+
+ tmp = json_strescape (json_node_get_string (node));
+ g_string_append_c (buffer, '"');
+ g_string_append (buffer, tmp);
+ g_string_append_c (buffer, '"');
+
+ g_free (tmp);
+ }
+ else
+ {
+ if (length)
+ *length = 0;
+ g_return_val_if_reached (NULL);
+ }
+
+ if (length)
+ *length = buffer->len;
+
+ return g_string_free (g_steal_pointer (&buffer), FALSE);
+}
+
+static gchar *
+dump_array (const gchar *name,
+ JsonArray *array,
+ gsize *length)
+{
+ guint array_len = json_array_get_length (array);
+ guint i;
+ GString *buffer;
+
+ buffer = g_string_new ("");
+
+ if (name)
+ g_string_append_printf (buffer, "\"%s\":", name);
+
+ g_string_append_c (buffer, '[');
+
+ for (i = 0; i < array_len; i++)
+ {
+ JsonNode *cur = json_array_get_element (array, i);
+ gchar *value;
+
+ switch (JSON_NODE_TYPE (cur))
+ {
+ case JSON_NODE_NULL:
+ g_string_append (buffer, "null");
+ break;
+
+ case JSON_NODE_VALUE:
+ value = dump_value (NULL, cur, NULL);
+ g_string_append (buffer, value);
+ g_free (value);
+ break;
+
+ case JSON_NODE_ARRAY:
+ value = dump_array (NULL, json_node_get_array (cur), NULL);
+ g_string_append (buffer, value);
+ g_free (value);
+ break;
+
+ case JSON_NODE_OBJECT:
+ value = dump_object (NULL, json_node_get_object (cur), NULL);
+ g_string_append (buffer, value);
+ g_free (value);
+ break;
+ }
+
+ if ((i + 1) != array_len)
+ g_string_append_c (buffer, ',');
+ }
+
+ g_string_append_c (buffer, ']');
+
+ if (length)
+ *length = buffer->len;
+
+ return g_string_free (buffer, FALSE);
+}
+
+static gchar *
+dump_object (const gchar *name,
+ JsonObject *object,
+ gsize *length)
+{
+ GList *members, *l;
+ GString *buffer;
+
+ buffer = g_string_new ("");
+
+ if (name)
+ g_string_append_printf (buffer, "\"%s\":", name);
+
+ g_string_append_c (buffer, '{');
+
+ members = json_object_get_members (object);
+
+ for (l = members; l != NULL; l = l->next)
+ {
+ const gchar *member_name = l->data;
+ gchar *escaped_name = json_strescape (member_name);
+ JsonNode *cur = json_object_get_member (object, member_name);
+ gchar *value;
+
+ switch (JSON_NODE_TYPE (cur))
+ {
+ case JSON_NODE_NULL:
+ g_string_append_printf (buffer, "\"%s\":null", escaped_name);
+ break;
+
+ case JSON_NODE_VALUE:
+ value = dump_value (escaped_name, cur, NULL);
+ g_string_append (buffer, value);
+ g_free (value);
+ break;
+
+ case JSON_NODE_ARRAY:
+ value = dump_array (escaped_name, json_node_get_array (cur), NULL);
+ g_string_append (buffer, value);
+ g_free (value);
+ break;
+
+ case JSON_NODE_OBJECT:
+ value = dump_object (escaped_name, json_node_get_object (cur), NULL);
+ g_string_append (buffer, value);
+ g_free (value);
+ break;
+ }
+
+ if (l->next != NULL)
+ g_string_append_c (buffer, ',');
+
+ g_free (escaped_name);
+ }
+
+ g_list_free (members);
+
+ g_string_append_c (buffer, '}');
+
+ if (length)
+ *length = buffer->len;
+
+ return g_string_free (buffer, FALSE);
+}
+
+/**
+ * cockpit_json_write:
+ * @node: the node to encode
+ * @length: optional place to return length
+ *
+ * Encode a JsonNode to a string.
+ *
+ * Returns: (transfer full): the encoded string
+ */
+gchar *
+cockpit_json_write (JsonNode *node,
+ gsize *length)
+{
+ gchar *retval = NULL;
+
+ if (!node)
+ {
+ if (length)
+ *length = 0;
+ return NULL;
+ }
+
+ switch (JSON_NODE_TYPE (node))
+ {
+ case JSON_NODE_ARRAY:
+ retval = dump_array (NULL, json_node_get_array (node), length);
+ break;
+
+ case JSON_NODE_OBJECT:
+ retval = dump_object (NULL, json_node_get_object (node), length);
+ break;
+
+ case JSON_NODE_NULL:
+ retval = g_strdup ("null");
+ if (length)
+ *length = 4;
+ break;
+
+ case JSON_NODE_VALUE:
+ retval = dump_value (NULL, node, length);
+ break;
+ }
+
+ return retval;
+}
+
+JsonObject *
+cockpit_json_from_hash_table (GHashTable *hash_table,
+ const gchar **fields)
+{
+ JsonObject *block = NULL;
+ gint i;
+ const gchar *value;
+
+ if (hash_table)
+ {
+ block = json_object_new ();
+ for (i = 0; fields[i] != NULL; i++)
+ {
+ value = g_hash_table_lookup (hash_table, fields[i]);
+ if (value)
+ json_object_set_string_member (block, fields[i], value);
+ else
+ json_object_set_null_member (block, fields[i]);
+ }
+ }
+
+ return block;
+}
+
+GHashTable *
+cockpit_json_to_hash_table (JsonObject *object,
+ const gchar **fields)
+{
+ gint i;
+ GHashTable *hash_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+ for (i = 0; fields[i] != NULL; i++)
+ {
+ const gchar *value;
+ if (!cockpit_json_get_string (object, fields[i], NULL, &value))
+ continue;
+
+ if (value)
+ g_hash_table_insert (hash_table, g_strdup (fields[i]), g_strdup (value));
+ }
+
+ return hash_table;
+}
+
+typedef const struct {
+ CockpitJsonWalkCallback callback;
+ gpointer user_data;
+} WalkClosure;
+
+static JsonNode *
+cockpit_json_walk_node (JsonNode *node,
+ WalkClosure *closure);
+
+static JsonArray *
+cockpit_json_walk_array (JsonArray *array,
+ WalkClosure *closure)
+{
+ gint n = json_array_get_length (array);
+ JsonArray *copy = NULL;
+
+ for (gint i = 0; i < n; i++)
+ {
+ JsonNode *element = json_array_get_element (array, i);
+ JsonNode *node = cockpit_json_walk_node (element, closure);
+
+ if (node && !copy)
+ {
+ /* We've decided we need to copy now, so revisit everything up
+ * to this point and add it to the copy.
+ */
+ copy = json_array_sized_new (n);
+ for (gint j = 0; j < i; j++)
+ json_array_add_element (copy, json_array_dup_element (array, j));
+ }
+
+ if (copy)
+ json_array_add_element (copy, node ?: json_node_ref (element));
+ }
+
+ return copy;
+}
+
+static JsonObject *
+cockpit_json_walk_object (JsonObject *object,
+ WalkClosure *closure)
+{
+ JsonObject *copy = NULL;
+ JsonObjectIter iter;
+ const gchar *key;
+ JsonNode *value;
+
+ json_object_iter_init (&iter, object);
+ while (json_object_iter_next (&iter, &key, &value))
+ {
+ JsonNode *node = cockpit_json_walk_node (value, closure);
+
+ if (node && !copy)
+ {
+ const gchar *this_key = key;
+
+ /* We've decided we need to copy now, so revisit everything up
+ * to this point and add it to the copy.
+ */
+ copy = json_object_new ();
+ json_object_iter_init (&iter, object);
+ while (json_object_iter_next (&iter, &key, &value) && key != this_key)
+ json_object_set_member (copy, key, json_node_ref (value));
+
+ /* ...now key and value will be where they were before we entered */
+ g_assert (key == this_key);
+ }
+
+ if (copy)
+ json_object_set_member (copy, key, node ?: json_node_ref (value));
+ }
+
+ return copy;
+}
+
+static JsonNode *
+cockpit_json_walk_node (JsonNode *node,
+ WalkClosure *closure)
+{
+ JsonNode *new_node = NULL;
+
+ switch (json_node_get_node_type (node))
+ {
+ case JSON_NODE_ARRAY:
+ {
+ JsonArray *array = json_node_get_array (node);
+ JsonArray *copy = cockpit_json_walk_array (array, closure);
+
+ if (copy)
+ {
+ new_node = json_node_new (JSON_NODE_ARRAY);
+ json_node_take_array (new_node, copy);
+ }
+
+ break;
+ }
+
+ case JSON_NODE_OBJECT:
+ {
+ JsonObject *object = json_node_get_object (node);
+ JsonObject *copy = cockpit_json_walk_object (object, closure);
+
+ if (copy)
+ {
+ new_node = json_node_new (JSON_NODE_OBJECT);
+ json_node_take_object (new_node, copy);
+ }
+
+ break;
+ }
+
+ default:
+ {
+ new_node = (* closure->callback) (node, closure->user_data);
+ break;
+ }
+ }
+
+ return new_node;
+}
+
+/**
+ * cockpit_json_walk:
+ * @object: an immutable #JsonObject
+ * @callback: a #CockpitJsonWalkCallback
+ * @user_data: user data for @callback
+ *
+ * Recursively visits each node contained in @object, calling @callback
+ * on each simple value type to allow point replacements of values.
+ * @object must be immutable; the resulting value will be returned.
+ *
+ * If @callback returns %NULL then no replacement is performed.
+ * Otherwise, the #JsonNode returned by @callback is used in place of
+ * the value which was previously found.
+ *
+ * @object must be immutable, and the result will also be immutable.
+ * The two trees will be shared to the extent possible, which means that
+ * if a particular branch of the original tree contained no
+ * replacements, then that branch will be used. In particular, if no
+ * replacements are performed at all, then this function is equivalent
+ * to json_object_ref().
+ *
+ * Returns: (transfer full): the resulting tree (immutable)
+ */
+JsonObject *
+cockpit_json_walk (JsonObject *object,
+ CockpitJsonWalkCallback callback,
+ gpointer user_data)
+{
+ g_return_val_if_fail (json_object_is_immutable (object), NULL);
+
+ WalkClosure closure = { callback, user_data };
+ JsonObject *copy = cockpit_json_walk_object (object, &closure);
+
+ if (!copy)
+ return json_object_ref (object);
+
+ json_object_seal (copy);
+
+ return copy;
+}
diff --git a/src/common/cockpitjson.h b/src/common/cockpitjson.h
new file mode 100644
index 0000000..59f60d8
--- /dev/null
+++ b/src/common/cockpitjson.h
@@ -0,0 +1,113 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_JSON_H__
+#define COCKPIT_JSON_H__
+
+#include <glib.h>
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+JsonNode * cockpit_json_parse (const char *data,
+ gssize length,
+ GError **error);
+
+JsonObject * cockpit_json_parse_object (const gchar *data,
+ gssize length,
+ GError **error);
+
+JsonObject * cockpit_json_parse_bytes (GBytes *data,
+ GError **error);
+
+gchar * cockpit_json_write (JsonNode *node,
+ gsize *length);
+
+gchar * cockpit_json_write_object (JsonObject *object,
+ gsize *length);
+
+GBytes * cockpit_json_write_bytes (JsonObject *object);
+
+gboolean cockpit_json_equal (JsonNode *previous,
+ JsonNode *current);
+
+gboolean cockpit_json_equal_object (JsonObject *previous,
+ JsonObject *current);
+
+void cockpit_json_patch (JsonObject *target,
+ JsonObject *patch);
+
+gboolean cockpit_json_get_int (JsonObject *object,
+ const gchar *member,
+ gint64 defawlt,
+ gint64 *value);
+
+gboolean cockpit_json_get_double (JsonObject *object,
+ const gchar *member,
+ gdouble defawlt,
+ gdouble *value);
+
+gboolean cockpit_json_get_bool (JsonObject *object,
+ const gchar *member,
+ gboolean defawlt,
+ gboolean *value);
+
+gboolean cockpit_json_get_string (JsonObject *object,
+ const gchar *member,
+ const gchar *defawlt,
+ const gchar **value);
+
+gboolean cockpit_json_get_strv (JsonObject *object,
+ const gchar *member,
+ const gchar **defawlt,
+ const gchar ***value);
+
+gboolean cockpit_json_get_array (JsonObject *object,
+ const gchar *member,
+ JsonArray *defawlt,
+ JsonArray **value);
+
+gboolean cockpit_json_get_object (JsonObject *options,
+ const gchar *member,
+ JsonObject *defawlt,
+ JsonObject **value);
+
+gboolean cockpit_json_get_null (JsonObject *object,
+ const gchar *member,
+ gboolean *present);
+
+guint cockpit_json_int_hash (gconstpointer v);
+
+gboolean cockpit_json_int_equal (gconstpointer v1,
+ gconstpointer v2);
+
+JsonObject * cockpit_json_from_hash_table (GHashTable *hash_table,
+ const gchar **fields);
+
+GHashTable * cockpit_json_to_hash_table (JsonObject *object,
+ const gchar **fields);
+
+typedef JsonNode * (* CockpitJsonWalkCallback) (JsonNode *node, gpointer user_data);
+
+JsonObject * cockpit_json_walk (JsonObject *object,
+ CockpitJsonWalkCallback callback,
+ gpointer user_data);
+
+#endif /* COCKPIT_JSON_H__ */
diff --git a/src/common/cockpitjsonprint.c b/src/common/cockpitjsonprint.c
new file mode 100644
index 0000000..399eb19
--- /dev/null
+++ b/src/common/cockpitjsonprint.c
@@ -0,0 +1,247 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitjsonprint.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <string.h>
+#include <sys/mman.h>
+
+static bool
+char_needs_json_escape (unsigned char c)
+{
+ /* we escape:
+ * - ascii controls
+ * - backslash
+ * - double quote
+ * - ascii del
+ * - all non-ascii
+ */
+ return c < ' ' || c == '\\' || c == '"' || c >= 0x7f;
+}
+
+static bool
+json_escape_char (FILE *stream,
+ unsigned char c)
+{
+ if (c == '\\')
+ return fputs ("\\\\", stream) >= 0;
+ else if (c == '"')
+ return fputs ("\\\"", stream) >= 0;
+ else if (c >= 0x80) /* non-ascii */
+ return fputc ('?', stream) >= 0;
+ else
+ return fprintf (stream, "\\u%04x", c) == 6;
+}
+
+static bool
+json_escape_string (FILE *stream,
+ const char *str,
+ size_t maxlen)
+{
+ size_t offset = 0;
+
+ while (offset < maxlen && str[offset])
+ {
+ size_t start = offset;
+
+ while (offset < maxlen && str[offset] && !char_needs_json_escape (str[offset]))
+ offset++;
+
+ /* print the non-escaped prefix, if there is one */
+ if (offset != start)
+ {
+ size_t length = offset - start;
+ if (fwrite (str + start, 1, length, stream) != length)
+ return false;
+ }
+
+ /* print the escaped character, if there is one */
+ if (offset < maxlen && str[offset])
+ {
+ if (!json_escape_char (stream, str[offset]))
+ return false;
+
+ offset++;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * cockpit_json_print_string_property:
+ * @stream: a stdio stream open for writing
+ * @key: the JSON key name
+ * @value: the string value to write
+ * @maxlen: the maximum length of @value
+ *
+ * Adds a string key/value pair to a JSON object.
+ *
+ * @key and @value should both be plain ASCII. @key is copied directly
+ * to the stream and must not contain any characters that would require
+ * escapes. @value is escaped, if necessary (including replacing
+ * non-ASCII characters with '?').
+ *
+ * @maxlen can be -1 if @value is nul-terminated. Otherwise, @maxlen is
+ * a maximum: the actual number of characters escaped and written is the
+ * lesser of the length of the string or @maxlen.
+ *
+ * Returns true if the value was correctly written.
+ */
+bool
+cockpit_json_print_string_property (FILE *stream,
+ const char *key,
+ const char *value,
+ ssize_t maxlen)
+{
+ size_t expected = strlen (key) + 7;
+
+ return fprintf (stream, ", \"%s\": \"", key) == expected &&
+ json_escape_string (stream, value, maxlen) &&
+ fputc ('"', stream) >= 0;
+}
+
+/**
+ * cockpit_json_print_bool_property:
+ * @stream: a stdio stream open for writing
+ * @key: the JSON key name
+ * @value: the boolean value to write
+ *
+ * Adds a boolean key/value pair to a JSON object. The boolean value is
+ * formatted as either the string "true" or "false".
+ *
+ * Returns true if the value was correctly written.
+ */
+bool
+cockpit_json_print_bool_property (FILE *stream,
+ const char *key,
+ bool value)
+{
+ size_t expected = 6 + strlen (key) + (value ? 4 : 5); /* "true" or "false" */
+
+ return fprintf (stream, ", \"%s\": %s", key, value ? "true" : "false") == expected;
+}
+
+/**
+ * cockpit_json_print_integer_property:
+ * @stream: a stdio stream open for writing
+ * @key: the JSON key name
+ * @value: the unsigned integer value to write
+ *
+ * Adds an integer key/value pair to a JSON object.
+ *
+ * Returns true if the value was correctly written.
+ */
+bool
+cockpit_json_print_integer_property (FILE *stream,
+ const char *key,
+ uint64_t value)
+{
+ /* too much effort to figure out the expected length exactly */
+ return fprintf (stream, ", \"%s\": %"PRIu64, key, value) > 6;
+}
+
+/**
+ * cockpit_json_print_open_memfd:
+ * @name: passed to memfd_create, gets displayed in /proc/.../fd
+ * @version: if not negative then a "version" field will be added
+ *
+ * Creates a memfd, wraps it in a stdio stream, and starts the printing
+ * of a JSON object into it by writing a '{' character and an optional
+ * version field.
+ *
+ * If you don't write the version field, you need to take care to write
+ * something else before first using the other cockpit_json_print_*
+ * functions, because they all prepend commas.
+ *
+ * This function always returns a valid stream. In case of any errors,
+ * the program is aborted.
+ */
+FILE *
+cockpit_json_print_open_memfd (const char *name,
+ int version)
+{
+ int fd;
+ /* current kernels moan about not specifying exec mode */
+#ifdef MFD_NOEXEC_SEAL
+ fd = memfd_create ("cockpit login messages", MFD_ALLOW_SEALING | MFD_CLOEXEC | MFD_NOEXEC_SEAL);
+ /* fallback for older kernels */
+ if (fd == -1 && errno == EINVAL)
+#endif
+ fd = memfd_create ("cockpit login messages", MFD_ALLOW_SEALING | MFD_CLOEXEC);
+ assert (fd != -1);
+
+ FILE *stream = fdopen (fd, "w");
+ assert (stream != NULL);
+
+ if (version >= 0)
+ fprintf (stream, "{\"version\": %d", version);
+ else
+ fputc ('{', stream);
+
+ return stream;
+}
+
+/**
+ * cockpit_json_print_finish_memfd:
+ * @stream: the pointer to where a stream created by
+ * cockpit_json_print_open_memfd() is stored.
+ *
+ * Finishes off the printing of a JSON object to a memfd by writing
+ * the closing '}', sealing the memfd, and reopening it readonly.
+ *
+ * @stream is closed, and set to %NULL.
+ *
+ * This function always returns a valid readonly file descriptor
+ * pointing to the sealed memfd. In case of any errors, the program is
+ * aborted.
+ */
+int
+cockpit_json_print_finish_memfd (FILE **stream)
+{
+ int r = fputc ('}', *stream);
+ assert (r == '}');
+
+ r = fflush (*stream);
+ assert (r == 0);
+
+ int fd = fileno (*stream);
+
+ const int seals = F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE;
+ r = fcntl (fd, F_ADD_SEALS, seals);
+ assert (r == 0);
+
+ char fd_name[] = "/proc/self/fd/xxxxxx";
+ r = snprintf (fd_name, sizeof fd_name, "/proc/self/fd/%d", fd);
+ assert (r < sizeof fd_name);
+
+ int readonly_fd = open (fd_name, O_RDONLY);
+ assert (readonly_fd != -1);
+
+ fclose (*stream);
+ *stream = NULL;
+
+ return readonly_fd;
+}
diff --git a/src/common/cockpitjsonprint.h b/src/common/cockpitjsonprint.h
new file mode 100644
index 0000000..8c79469
--- /dev/null
+++ b/src/common/cockpitjsonprint.h
@@ -0,0 +1,47 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+
+bool
+cockpit_json_print_string_property (FILE *stream,
+ const char *key,
+ const char *value,
+ ssize_t maxlen);
+
+bool
+cockpit_json_print_bool_property (FILE *stream,
+ const char *key,
+ bool value);
+
+bool
+cockpit_json_print_integer_property (FILE *stream,
+ const char *key,
+ uint64_t value);
+
+FILE *
+cockpit_json_print_open_memfd (const char *name,
+ int version);
+
+int
+cockpit_json_print_finish_memfd (FILE **stream);
diff --git a/src/common/cockpitlocale.c b/src/common/cockpitlocale.c
new file mode 100644
index 0000000..76ae960
--- /dev/null
+++ b/src/common/cockpitlocale.c
@@ -0,0 +1,111 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitlocale.h"
+#include "common/cockpitsystem.h"
+
+#include <locale.h>
+#include <string.h>
+
+gchar *
+cockpit_locale_from_language (const gchar *value,
+ const gchar *encoding,
+ gchar **shorter)
+{
+ const gchar *spot;
+ gchar *country = NULL;
+ gchar *lang = NULL;
+ gchar *result = NULL;
+ const gchar *dot;
+
+ if (value == NULL)
+ value = "C";
+
+ dot = ".";
+ if (!encoding)
+ dot = encoding = "";
+
+ spot = strchr (value, '-');
+ if (spot)
+ {
+ country = g_ascii_strup (spot + 1, -1);
+ lang = g_ascii_strdown (value, spot - value);
+ result = g_strconcat (lang, "_", country, dot, encoding, NULL);
+ if (shorter)
+ {
+ *shorter = lang;
+ lang = NULL;
+ }
+ }
+ else
+ {
+ result = g_strconcat (value, dot, encoding, NULL);
+ if (shorter)
+ *shorter = g_strdup (value);
+ }
+
+ g_free (country);
+ g_free (lang);
+ return result;
+}
+
+/* The most recently set language */
+static gchar previous[32] = { '\0' };
+
+void
+cockpit_locale_set_language (const gchar *value)
+{
+ const gchar *encoding = NULL;
+ gchar *locale = NULL;
+
+ if (value == NULL)
+ value = "C";
+ else
+ encoding = "UTF-8";
+
+ if (strlen (value) > sizeof (previous) - 1)
+ {
+ g_printerr ("invalid language: %s\n", value);
+ return;
+ }
+
+ /* Already set our locale to this language? */
+ if (g_strcmp0 (value, previous) == 0)
+ return;
+
+ locale = cockpit_locale_from_language (value, encoding, NULL);
+ g_assert (locale != NULL);
+
+ if (setlocale (LC_ALL, locale) == NULL)
+ {
+ /* Note we use g_printerr directly, since we want no gettext invocations on this line */
+ g_printerr ("invalid or unusable locale: %s\n", locale);
+ }
+ else
+ {
+ g_debug ("set bridge locale to: %s", locale);
+ cockpit_setenv_check ("LANG", locale, TRUE);
+ }
+
+ strncpy (previous, value, sizeof (previous) - 1);
+ previous[sizeof (previous) - 1] = '\0';
+ g_free (locale);
+}
diff --git a/src/common/cockpitlocale.h b/src/common/cockpitlocale.h
new file mode 100644
index 0000000..0cbfe47
--- /dev/null
+++ b/src/common/cockpitlocale.h
@@ -0,0 +1,35 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_LOCALE_H__
+#define __COCKPIT_LOCALE_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+gchar * cockpit_locale_from_language (const gchar *language,
+ const gchar *encoding,
+ gchar **shorter);
+
+void cockpit_locale_set_language (const gchar *language);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_LOCALE_H__ */
diff --git a/src/common/cockpitloopback.c b/src/common/cockpitloopback.c
new file mode 100644
index 0000000..2a2cb6f
--- /dev/null
+++ b/src/common/cockpitloopback.c
@@ -0,0 +1,147 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitloopback.h"
+
+struct _CockpitLoopback {
+ GSocketAddressEnumerator parent;
+ GQueue addresses;
+};
+
+static void cockpit_loopback_connectable_iface (GSocketConnectableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CockpitLoopback, cockpit_loopback, G_TYPE_SOCKET_ADDRESS_ENUMERATOR,
+ G_IMPLEMENT_INTERFACE (G_TYPE_SOCKET_CONNECTABLE, cockpit_loopback_connectable_iface)
+)
+
+static void
+cockpit_loopback_init (CockpitLoopback *self)
+{
+ g_queue_init (&self->addresses);
+}
+
+static void
+cockpit_loopback_finalize (GObject *object)
+{
+ CockpitLoopback *self = COCKPIT_LOOPBACK (object);
+
+ while (!g_queue_is_empty (&self->addresses))
+ g_object_unref (g_queue_pop_head (&self->addresses));
+ g_queue_clear (&self->addresses);
+
+ G_OBJECT_CLASS (cockpit_loopback_parent_class)->finalize (object);
+}
+
+static GSocketAddress *
+cockpit_loopback_next (GSocketAddressEnumerator *enumerator,
+ GCancellable *cancellable,
+ GError **error)
+{
+ CockpitLoopback *self = COCKPIT_LOOPBACK (enumerator);
+ return g_queue_pop_head (&self->addresses);
+}
+
+static void
+cockpit_loopback_next_async (GSocketAddressEnumerator *enumerator,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ CockpitLoopback *self = COCKPIT_LOOPBACK (enumerator);
+ GTask *res;
+ GSocketAddress *address;
+ GError *error = NULL;
+
+ address = cockpit_loopback_next (enumerator, cancellable, &error);
+ g_assert (error == NULL);
+
+ res = g_task_new (G_OBJECT (self), NULL, callback, user_data);
+ g_task_return_pointer (res, address, g_object_unref);
+ g_object_unref (res);
+}
+
+static GSocketAddress *
+cockpit_loopback_next_finish (GSocketAddressEnumerator *enumerator,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_warn_if_fail (g_task_is_valid (result, enumerator));
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+cockpit_loopback_class_init (CockpitLoopbackClass *klass)
+{
+ GSocketAddressEnumeratorClass *enumerator_class = G_SOCKET_ADDRESS_ENUMERATOR_CLASS (klass);
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->finalize = cockpit_loopback_finalize;
+
+ enumerator_class->next = cockpit_loopback_next;
+ enumerator_class->next_async = cockpit_loopback_next_async;
+ enumerator_class->next_finish = cockpit_loopback_next_finish;
+}
+
+static void
+ref_to_queue (gpointer data,
+ gpointer user_data)
+{
+ g_queue_push_tail (user_data, g_object_ref (data));
+}
+
+static GSocketAddressEnumerator *
+cockpit_loopback_enumerate (GSocketConnectable *connectable)
+{
+ CockpitLoopback *self = COCKPIT_LOOPBACK (connectable);
+ CockpitLoopback *copy;
+
+ copy = g_object_new (COCKPIT_TYPE_LOOPBACK, NULL);
+ g_queue_foreach (&self->addresses, ref_to_queue, &copy->addresses);
+
+ return G_SOCKET_ADDRESS_ENUMERATOR (copy);
+}
+
+static void
+cockpit_loopback_connectable_iface (GSocketConnectableIface *iface)
+{
+ iface->enumerate = cockpit_loopback_enumerate;
+ iface->proxy_enumerate = cockpit_loopback_enumerate;
+}
+
+GSocketConnectable *
+cockpit_loopback_new (guint16 port)
+{
+ CockpitLoopback *self;
+ GInetAddress *addr;
+
+ self = g_object_new (COCKPIT_TYPE_LOOPBACK, NULL);
+
+ addr = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV6);
+ g_queue_push_tail (&self->addresses, g_inet_socket_address_new (addr, port));
+ g_object_unref (addr);
+
+ addr = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV4);
+ g_queue_push_tail (&self->addresses, g_inet_socket_address_new (addr, port));
+ g_object_unref (addr);
+
+ return G_SOCKET_CONNECTABLE (self);
+}
diff --git a/src/common/cockpitloopback.h b/src/common/cockpitloopback.h
new file mode 100644
index 0000000..478d8a1
--- /dev/null
+++ b/src/common/cockpitloopback.h
@@ -0,0 +1,34 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_LOOPBACK_H__
+#define __COCKPIT_LOOPBACK_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_LOOPBACK (cockpit_loopback_get_type ())
+G_DECLARE_FINAL_TYPE(CockpitLoopback, cockpit_loopback, COCKPIT, LOOPBACK, GSocketAddressEnumerator)
+
+GSocketConnectable * cockpit_loopback_new (guint16 port);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_LOOPBACK_H__ */
diff --git a/src/common/cockpitmachinesjson.c b/src/common/cockpitmachinesjson.c
new file mode 100644
index 0000000..11b3a25
--- /dev/null
+++ b/src/common/cockpitmachinesjson.c
@@ -0,0 +1,237 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitmachinesjson.h"
+#include "common/cockpitconf.h"
+
+#include <errno.h>
+#include <glob.h>
+
+static int
+glob_err_func (const char *epath,
+ int eerrno)
+{
+ /* Should Not Happen™ -- log the error for debugging */
+ if (eerrno != ENOENT)
+ g_warning ("%s: cannot read: %s", epath, g_strerror (eerrno));
+ return 0;
+}
+
+static JsonNode *
+new_object_node (void)
+{
+ JsonNode *n = json_node_new (JSON_NODE_OBJECT);
+ json_node_take_object (n, json_object_new ());
+ return n;
+}
+
+static JsonNode *
+parse_json_file (const char *path)
+{
+ JsonParser *parser = NULL;
+ GError *error = NULL;
+ gboolean success;
+ JsonNode *result = NULL;
+
+ parser = json_parser_new ();
+ success = json_parser_load_from_file (parser, path, &error);
+ if (success)
+ {
+ result = json_parser_get_root (parser);
+ /* root is NULL if the file is empty */
+ if (result != NULL)
+ {
+ if (JSON_NODE_HOLDS_OBJECT (result))
+ {
+ result = json_node_copy (result);
+ }
+ else
+ {
+ g_message ("%s: does not contain a JSON object, ignoring", path);
+ result = NULL;
+ }
+ }
+ }
+ else
+ {
+ if (error->code != G_FILE_ERROR_NOENT)
+ g_message ("%s: invalid JSON: %s", path, error->message);
+ g_error_free (error);
+ }
+
+ g_object_unref (parser);
+ return result;
+}
+
+static gboolean
+write_json_file (JsonNode *config, const char *path, GError **error)
+{
+ JsonGenerator *json_gen;
+ gboolean res;
+
+ json_gen = json_generator_new ();
+ json_generator_set_root (json_gen, config);
+ json_generator_set_pretty (json_gen, TRUE); /* bikeshed zone */
+ res = json_generator_to_file (json_gen, path, error);
+ g_object_unref (json_gen);
+ return res;
+}
+
+static void
+merge_config (JsonObject *machines,
+ JsonObject *delta,
+ const char *path)
+{
+ GList *hosts = json_object_get_members (delta);
+ for (GList *i = g_list_first (hosts); i; i = g_list_next (i))
+ {
+ const char *hostname = i->data;
+ JsonNode *delta_props = json_object_get_member (delta, hostname);
+
+ if (!JSON_NODE_HOLDS_OBJECT (delta_props))
+ {
+ g_message ("%s: host name definition %s does not contain a JSON object, ignoring", path, hostname);
+ continue;
+ }
+
+ /* merge delta properties info existing machines host */
+ if (!json_object_has_member (machines, hostname))
+ json_object_set_member (machines, hostname, new_object_node ());
+ JsonObject *machine_props = json_object_get_object_member (machines, hostname);
+
+ g_debug ("%s: merging updates to host name %s", path, hostname);
+ GList *proplist = json_object_get_members (json_node_get_object (delta_props));
+ for (GList *p = g_list_first (proplist); p; p = g_list_next (p))
+ {
+ const char *propname = p->data;
+ JsonNode *prop_node = json_object_get_member (json_node_get_object (delta_props), propname);
+
+ if (!JSON_NODE_HOLDS_VALUE (prop_node))
+ {
+ g_message ("%s: host name definition %s: property %s does not contain a simple value, ignoring", path, hostname, propname);
+ continue;
+ }
+
+ g_debug ("%s: host name %s: merging property %s", path, hostname, propname);
+ json_object_set_member (machine_props, propname, json_node_copy (prop_node));
+ }
+
+ g_list_free (proplist);
+ }
+
+ g_list_free (hosts);
+}
+
+const char *
+get_machines_json_dir (void)
+{
+ static gchar *path = NULL;
+ if (path == NULL)
+ path = g_build_filename (cockpit_conf_get_dirs ()[0], "cockpit", "machines.d", NULL);
+ return path;
+}
+
+JsonNode *
+read_machines_json (void)
+{
+ gchar *glob_str;
+ glob_t conf_glob;
+ int res;
+ JsonNode *machines = NULL;
+
+ /* find json config files */
+ glob_str = g_build_filename (get_machines_json_dir (), "*.json", NULL);
+ res = glob (glob_str, 0, glob_err_func, &conf_glob);
+ if (G_UNLIKELY (res != 0 && res != GLOB_NOMATCH))
+ {
+ g_critical ("glob %s failed with return code %i", glob_str, res);
+ globfree (&conf_glob);
+ g_free (glob_str);
+ return NULL;
+ }
+
+ /* start with an empty object */
+ machines = new_object_node ();
+
+ for (size_t i = 0; i < conf_glob.gl_pathc; ++i)
+ {
+ JsonNode *j = parse_json_file (conf_glob.gl_pathv[i]);
+ if (j)
+ {
+ merge_config (json_node_get_object (machines), json_node_get_object (j), conf_glob.gl_pathv[i]);
+ json_node_free (j);
+ }
+ }
+
+ globfree (&conf_glob);
+ g_free (glob_str);
+
+ return machines;
+}
+
+/* iterator function for update_machines_json() */
+static void
+update_machine_property (JsonObject *object,
+ const gchar *member_name,
+ JsonNode *member_node,
+ gpointer user_data)
+{
+ json_object_set_member ((JsonObject *) user_data, member_name, json_node_copy (member_node));
+}
+
+gboolean
+update_machines_json (const char *filename,
+ const char *hostname,
+ JsonNode *info,
+ GError **error)
+{
+ gchar *path;
+ JsonNode *cur_config;
+ JsonObject *cur_config_obj;
+ JsonNode *cur_props;
+ gboolean res;
+
+ g_assert (JSON_NODE_HOLDS_OBJECT (info));
+
+ path = g_build_filename (get_machines_json_dir (), filename, NULL);
+ cur_config = parse_json_file (path);
+ if (cur_config == NULL)
+ cur_config = new_object_node ();
+ cur_config_obj = json_node_get_object (cur_config);
+ cur_props = json_object_get_member (cur_config_obj, hostname);
+
+ if (cur_props)
+ {
+ /* update settings for hostname */
+ g_assert (JSON_NODE_HOLDS_OBJECT (cur_props));
+ json_object_foreach_member (json_node_get_object (info), update_machine_property, json_node_get_object (cur_props));
+ }
+ else
+ {
+ /* create new entry for host name */
+ json_object_set_member (cur_config_obj, hostname, json_node_copy (info));
+ }
+
+ res = write_json_file (cur_config, path, error);
+ g_free (path);
+ json_node_free (cur_config);
+ return res;
+}
diff --git a/src/common/cockpitmachinesjson.h b/src/common/cockpitmachinesjson.h
new file mode 100644
index 0000000..89efc42
--- /dev/null
+++ b/src/common/cockpitmachinesjson.h
@@ -0,0 +1,39 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_MACHINES_JSON_H__
+#define __COCKPIT_MACHINES_JSON_H__
+
+#include <glib.h>
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+const char * get_machines_json_dir (void);
+
+JsonNode * read_machines_json (void);
+
+gboolean update_machines_json (const char *filename,
+ const char *hostname,
+ JsonNode *info,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_MACHINES_JSON_H__ */
diff --git a/src/common/cockpitmemfdread.c b/src/common/cockpitmemfdread.c
new file mode 100644
index 0000000..f465231
--- /dev/null
+++ b/src/common/cockpitmemfdread.c
@@ -0,0 +1,168 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitmemfdread.h"
+
+#include "cockpitjson.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+gchar *
+cockpit_memfd_read (int fd,
+ GError **error)
+{
+ int seals = fcntl (fd, F_GET_SEALS);
+ if (seals == -1)
+ {
+ g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
+ "could not query seals on fd %d: not memfd?: %m", fd);
+ return NULL;
+ }
+
+ const guint expected_seals = F_SEAL_WRITE | F_SEAL_GROW | F_SEAL_SHRINK;
+ if ((seals & expected_seals) != expected_seals)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "memfd fd %d has incorrect seals set: %u (instead of %u)\n",
+ fd, seals & expected_seals, expected_seals);
+ return NULL;
+ }
+
+ struct stat buf;
+ if (fstat (fd, &buf) != 0)
+ {
+ g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
+ "Failed to stat memfd %d: %m", fd);
+ return NULL;
+ }
+
+ if (buf.st_size < 1)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "memfd %d must not be empty", fd);
+ return NULL;
+ }
+
+ /* This number is completely arbitrary: it's much larger than anything
+ * we're ever going to receive, but it's much smaller than any value
+ * that would ever cause us problems (with integer overflow, or malloc
+ * failing).
+ */
+ if (buf.st_size > 10000)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "memfd %d is unreasonably large (%"PRId64" bytes)", fd, (gint64) buf.st_size);
+ return NULL;
+ }
+
+ g_autofree gchar *content = g_malloc (buf.st_size + 1);
+ gssize s = pread (fd, content, buf.st_size + 1, 0);
+ if (s == -1)
+ {
+ g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno),
+ "failed to read memfd %d: %m", fd);
+ return NULL;
+ }
+ else if (s != buf.st_size)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "memfd %d changed size from %zu to %zu bytes", fd, (gssize) buf.st_size, s);
+ return NULL;
+ }
+
+ for (gint i = 0; i < buf.st_size; i++)
+ if (((signed char) content[i]) <= 0)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "memfd %d contains %s character", fd, content[i] ? "non-ASCII" : "nul");
+ return NULL;
+ }
+
+ content[buf.st_size] = '\0';
+
+ return g_steal_pointer (&content);
+}
+
+gboolean
+cockpit_memfd_read_from_envvar (gchar **result,
+ const char *envvar,
+ GError **error)
+{
+ const gchar *fd_str = g_getenv (envvar);
+
+ if (fd_str == NULL)
+ {
+ /* Environment variable unset is a valid (empty) result. */
+ *result = NULL;
+ return TRUE;
+ }
+
+ char *end;
+ long value = strtol (fd_str, &end, 10);
+ if (*end || value < 0 || value >= INT_MAX)
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "invalid value for %s environment variable: %s", envvar, fd_str);
+ return FALSE;
+ }
+ int fd = (int) value;
+ g_unsetenv (envvar);
+
+ gchar *content = cockpit_memfd_read (fd, error);
+ close (fd);
+
+ if (content == NULL)
+ return FALSE;
+
+ *result = content;
+ return TRUE;
+}
+
+JsonObject *
+cockpit_memfd_read_json (gint fd,
+ GError **error)
+{
+ g_autofree gchar *content = cockpit_memfd_read (fd, error);
+
+ if (content == NULL)
+ return NULL;
+
+ return cockpit_json_parse_object (content, -1, error);
+}
+
+JsonObject *
+cockpit_memfd_read_json_from_control_messages (CockpitControlMessages *ccm,
+ GError **error)
+{
+ if (ccm->n_messages == 0)
+ return NULL;
+
+ gint peeked_fd = cockpit_control_messages_peek_single_fd (ccm, error);
+
+ if (peeked_fd == -1)
+ return NULL;
+
+ return cockpit_memfd_read_json (peeked_fd, error);
+}
diff --git a/src/common/cockpitmemfdread.h b/src/common/cockpitmemfdread.h
new file mode 100644
index 0000000..d8696ad
--- /dev/null
+++ b/src/common/cockpitmemfdread.h
@@ -0,0 +1,43 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+#include "cockpitcontrolmessages.h"
+
+#include <json-glib/json-glib.h>
+#include <glib.h>
+
+gchar *
+cockpit_memfd_read (gint fd,
+ GError **error);
+
+gboolean
+cockpit_memfd_read_from_envvar (gchar **result,
+ const char *envvar,
+ GError **error);
+
+JsonObject *
+cockpit_memfd_read_json (gint fd,
+ GError **error);
+
+JsonObject *
+cockpit_memfd_read_json_from_control_messages (CockpitControlMessages *ccm,
+ GError **error);
diff --git a/src/common/cockpitmemory.c b/src/common/cockpitmemory.c
new file mode 100644
index 0000000..741b21d
--- /dev/null
+++ b/src/common/cockpitmemory.c
@@ -0,0 +1,143 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitmemory.h"
+
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+/**
+ * cockpit_memory_clear:
+ *
+ * The cockpit_memory_clear function overwrites LEN bytes of memory
+ * pointed to by DATA with non-sensitive values. When LEN is -1, DATA
+ * must be zero-terminated and all bytes until the zero are
+ * overwritten.
+ *
+ * This is very similar to memset but we take extra measures to
+ * prevent the compiler from optimizing it away.
+ */
+
+void
+cockpit_memory_clear (void * data,
+ ssize_t len)
+{
+ if (len < 0)
+ len = strlen (data);
+
+ explicit_bzero (data, len);
+}
+
+static void
+abort_errno (const char *msg)
+{
+ perror (msg);
+ abort ();
+}
+
+void *
+mallocx (size_t size)
+{
+ void *r = malloc (size);
+ if (r == NULL)
+ abort_errno ("failed to allocate memory");
+ return r;
+}
+
+void *
+callocx (size_t nmemb, size_t size)
+{
+ void *r = calloc (nmemb, size);
+ if (r == NULL)
+ abort_errno ("failed to allocate memory");
+ return r;
+}
+
+char *
+strdupx (const char *s)
+{
+ char *r = strdup (s);
+ if (r == NULL)
+ abort_errno ("failed to allocate memory for strdup");
+ return r;
+}
+
+char *
+strndupx (const char *s,
+ size_t n)
+{
+ char *r = strndup (s, n);
+ if (r == NULL)
+ abort_errno ("failed to allocate memory for strndup");
+ return r;
+}
+
+int
+asprintfx (char **strp,
+ const char *fmt, ...)
+{
+ va_list args;
+ int r;
+
+ va_start (args, fmt);
+ r = vasprintf (strp, fmt, args);
+ va_end (args);
+ if (r < 0)
+ {
+ fprintf (stderr, "Cannot allocate memory for asprintf\n");
+ abort ();
+ }
+ return r;
+}
+
+void *
+reallocx (void *ptr,
+ size_t size)
+{
+ void *r = realloc (ptr, size);
+ if (r == NULL)
+ abort_errno ("failed to allocate memory");
+ return r;
+}
+
+/* this is like reallocarray(3), but this does not yet exist everywhere; plus
+ * abort() on ENOMEM */
+void *
+reallocarrayx (void *ptr,
+ size_t nmemb,
+ size_t size)
+{
+ void *r;
+
+ if (nmemb >= SIZE_MAX / size)
+ {
+ fprintf (stderr, "reallocarr: overflow (nmemb %zu)\n", nmemb);
+ abort ();
+ }
+ r = realloc (ptr, nmemb * size);
+ if (r == NULL)
+ abort_errno ("failed to allocate memory for realloc");
+ return r;
+}
diff --git a/src/common/cockpitmemory.h b/src/common/cockpitmemory.h
new file mode 100644
index 0000000..d1ba4cd
--- /dev/null
+++ b/src/common/cockpitmemory.h
@@ -0,0 +1,47 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_MEMORY_H__
+#define __COCKPIT_MEMORY_H__
+
+#include <sys/types.h>
+
+void cockpit_memory_clear (void *data,
+ ssize_t length);
+
+/* variants of glibc functions that abort() on ENOMEM */
+void * mallocx (size_t size);
+void * callocx (size_t nmemb, size_t size);
+char * strdupx (const char *s);
+
+char * strndupx (const char *s,
+ size_t n);
+
+__attribute__((__format__ (__printf__, 2, 3)))
+int asprintfx (char **strp,
+ const char *fmt, ...);
+
+void * reallocx (void *ptr,
+ size_t size);
+
+void * reallocarrayx (void *ptr,
+ size_t nmemb,
+ size_t size);
+
+#endif /* __COCKPIT_MEMORY_H__ */
diff --git a/src/common/cockpitpipe.c b/src/common/cockpitpipe.c
new file mode 100644
index 0000000..810f737
--- /dev/null
+++ b/src/common/cockpitpipe.c
@@ -0,0 +1,1855 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpipe.h"
+
+#include "cockpitflow.h"
+#include "cockpithacks.h"
+#include "cockpitunicode.h"
+
+#include <glib-unix.h>
+
+#include <sys/socket.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+
+#include <dirent.h>
+#include <errno.h>
+#include <poll.h>
+#include <pty.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifdef __linux
+#include <sys/prctl.h>
+#endif
+
+/**
+ * CockpitPipe:
+ *
+ * A pipe with queued input and output similar in concept to a
+ * a unix shell pipe or pipe().
+ *
+ * When talking to a process the CockpitPipe:pid property
+ * will be non-zero. In that case the transport waits for the child
+ * process to exit before it closes.
+ *
+ * This pipe can do flow control in two ways:
+ *
+ * - Its input can be throttled, it can listen to a "pressure" signal
+ * from another object passed into cockpit_flow_throttle()
+ * - It can optionally control another flow, by emitting a "pressure" signal
+ * when its output queue is too large
+ */
+
+#define DEF_PACKET_SIZE (64UL * 1024UL)
+
+enum {
+ PROP_0,
+ PROP_NAME,
+ PROP_IN_FD,
+ PROP_OUT_FD,
+ PROP_ERR_FD,
+ PROP_PID,
+ PROP_PROBLEM,
+};
+
+typedef struct {
+ gchar *name;
+ GMainContext *context;
+
+ gboolean closed;
+ gboolean closing;
+ gboolean connecting;
+ gchar *problem;
+
+ GPid pid;
+ GSource *child;
+ gboolean exited;
+ gint status;
+ CockpitPipe **watch_arg;
+ gboolean is_process;
+
+ int out_fd;
+ gboolean out_done;
+ GSource *out_source;
+ GQueue *out_queue;
+ gsize out_queued;
+ gsize out_partial;
+
+ int in_fd;
+ gboolean in_done;
+ GSource *in_source;
+ GByteArray *in_buffer;
+
+ int err_fd;
+ gboolean err_done;
+ GSource *err_source;
+ GByteArray *err_buffer;
+ gboolean err_forward_to_log;
+
+ gboolean is_user_fd;
+
+ /* Pressure which throttles input on this pipe */
+ CockpitFlow *pressure;
+ gulong pressure_sig;
+} CockpitPipePrivate;
+
+typedef struct {
+ GSource source;
+ CockpitPipe *pipe;
+} CockpitPipeSource;
+
+/* A megabyte is when we start to consider queue full enough */
+#define QUEUE_PRESSURE 1024UL * 1024UL
+
+static guint cockpit_pipe_sig_read;
+static guint cockpit_pipe_sig_close;
+
+static void start_input (CockpitPipe *self);
+
+static void start_output (CockpitPipe *self);
+
+static void close_output (CockpitPipe *self);
+
+static void cockpit_close_later (CockpitPipe *self);
+
+static void set_problem_from_errno (CockpitPipe *self,
+ const gchar *message,
+ int errn);
+
+static void cockpit_pipe_throttle (CockpitFlow *flow,
+ CockpitFlow *controlling);
+
+static void cockpit_pipe_flow_iface_init (CockpitFlowInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CockpitPipe, cockpit_pipe, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (COCKPIT_TYPE_FLOW, cockpit_pipe_flow_iface_init)
+ G_ADD_PRIVATE (CockpitPipe));
+
+static void
+cockpit_pipe_init (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ priv->in_buffer = g_byte_array_new ();
+ priv->in_fd = -1;
+ priv->out_queue = g_queue_new ();
+ priv->out_fd = -1;
+ priv->err_fd = -1;
+ priv->status = -1;
+
+ priv->context = g_main_context_ref_thread_default ();
+}
+
+static void
+stop_output (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_assert (priv->out_source != NULL);
+ g_source_destroy (priv->out_source);
+ g_source_unref (priv->out_source);
+ priv->out_source = NULL;
+}
+
+static void
+stop_input (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_assert (priv->in_source != NULL);
+ g_source_destroy (priv->in_source);
+ g_source_unref (priv->in_source);
+ priv->in_source = NULL;
+}
+
+static void
+stop_error (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_assert (priv->err_source != NULL);
+ g_source_destroy (priv->err_source);
+ g_source_unref (priv->err_source);
+ priv->err_source = NULL;
+}
+
+static void
+close_immediately (CockpitPipe *self,
+ const gchar *problem)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ if (priv->closed)
+ return;
+
+ if (problem)
+ {
+ g_free (priv->problem);
+ priv->problem = g_strdup (problem);
+ }
+
+ priv->closed = TRUE;
+
+ g_debug ("%s: closing pipe%s%s", priv->name,
+ priv->problem ? ": " : "",
+ priv->problem ? priv->problem : "");
+
+ if (priv->in_source)
+ stop_input (self);
+ priv->in_done = TRUE;
+ if (priv->out_source)
+ stop_output (self);
+ priv->out_done = TRUE;
+ if (priv->err_source)
+ stop_error (self);
+ priv->err_done = TRUE;
+
+ if (priv->in_fd != -1)
+ {
+ close (priv->in_fd);
+ priv->in_fd = -1;
+ }
+ if (priv->out_fd != -1)
+ {
+ close (priv->out_fd);
+ priv->out_fd = -1;
+ }
+ if (priv->err_fd != -1)
+ {
+ close (priv->err_fd);
+ priv->err_fd = -1;
+ }
+
+
+ if (problem && priv->pid && !priv->exited)
+ {
+ g_debug ("%s: killing child: %d", priv->name, (int)priv->pid);
+ kill (priv->pid, SIGTERM);
+ }
+
+ /* If not tracking a pid, then we are now closed. */
+ if (!priv->child)
+ {
+ g_debug ("%s: no child process to wait for: closed", priv->name);
+ g_signal_emit (self, cockpit_pipe_sig_close, 0, priv->problem);
+ }
+}
+
+static void
+close_maybe (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ if (!priv->closed)
+ {
+ if (priv->in_done && priv->out_done && priv->err_done)
+ {
+ g_debug ("%s: input and output done", priv->name);
+ close_immediately (self, NULL);
+ }
+ }
+}
+
+static void
+on_child_reap (GPid pid,
+ gint status,
+ gpointer user_data)
+{
+ CockpitPipe **arg = user_data;
+ CockpitPipe *self = *arg;
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ /* This happens if this child watch outlasts the pipe */
+ if (!self)
+ return;
+
+ priv->status = status;
+ priv->exited = TRUE;
+ priv->watch_arg = NULL;
+
+ /* Release our reference on watch handler */
+ g_source_unref (priv->child);
+ priv->child = NULL;
+
+ /*
+ * We need to wait until both the process has exited *and*
+ * the output has closed before we fire our close signal.
+ */
+
+ g_debug ("%s: child process quit:%s %d %d", priv->name,
+ priv->closed ? " closed:" : "", (int)pid, status);
+
+ /* Start input and output to get to completion */
+ if (!priv->out_done)
+ close_output (self);
+ else if (priv->closed)
+ g_signal_emit (self, cockpit_pipe_sig_close, 0, priv->problem);
+}
+
+static gboolean
+dispatch_input (gint fd,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ CockpitPipe *self = (CockpitPipe *)user_data;
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+ gssize ret = 0;
+ gsize len;
+ int errn;
+
+ g_return_val_if_fail (priv->in_source, FALSE);
+ len = priv->in_buffer->len;
+
+ /*
+ * Enable clean shutdown by not reading when we just get
+ * G_IO_HUP. Note that when we get G_IO_ERR we do want to read
+ * just so we can get the appropriate detailed error message.
+ */
+ if (cond != G_IO_HUP)
+ {
+ g_byte_array_set_size (priv->in_buffer, len + DEF_PACKET_SIZE);
+ g_debug ("%s: reading input %x", priv->name, cond);
+ ret = read (priv->in_fd, priv->in_buffer->data + len, DEF_PACKET_SIZE);
+
+ errn = errno;
+ if (ret < 0)
+ {
+ g_byte_array_set_size (priv->in_buffer, len);
+ if (errn == EAGAIN || errn == EINTR)
+ {
+ return TRUE;
+ }
+ else if (errn == ECONNRESET)
+ {
+ g_debug ("couldn't read: %s", g_strerror (errn));
+ ret = 0;
+ }
+ else
+ {
+ set_problem_from_errno (self, "couldn't read", errn);
+ close_immediately (self, NULL); /* problem already set */
+ return FALSE;
+ }
+ }
+ }
+
+ g_byte_array_set_size (priv->in_buffer, len + ret);
+
+ if (ret == 0)
+ {
+ g_debug ("%s: end of input", priv->name);
+ priv->in_done = TRUE;
+ stop_input (self);
+ }
+
+ g_object_ref (self);
+
+ g_signal_emit (self, cockpit_pipe_sig_read, 0, priv->in_buffer, priv->in_done);
+
+ if (priv->in_done)
+ close_maybe (self);
+
+ g_object_unref (self);
+ return TRUE;
+}
+
+static void
+forward_error (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ if (priv->err_buffer->len > 0)
+ {
+ g_warning ("%s: unexpected stderr output: %.*s", priv->name, priv->err_buffer->len, priv->err_buffer->data);
+ g_byte_array_set_size (priv->err_buffer, 0);
+ }
+}
+
+static gboolean
+dispatch_error (gint fd,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ CockpitPipe *self = (CockpitPipe *)user_data;
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+ gssize ret = 0;
+ gsize len;
+
+ g_return_val_if_fail (priv->err_source, FALSE);
+ len = priv->err_buffer->len;
+
+ /*
+ * Enable clean shutdown by not reading when we just get
+ * G_IO_HUP. Note that when we get G_IO_ERR we do want to read
+ * just so we can get the appropriate detailed error message.
+ */
+ if (cond != G_IO_HUP)
+ {
+ g_debug ("%s: reading error", priv->name);
+
+ g_byte_array_set_size (priv->err_buffer, len + 1024);
+ ret = read (priv->err_fd, priv->err_buffer->data + len, 1024);
+ if (ret < 0)
+ {
+ g_byte_array_set_size (priv->err_buffer, len);
+ if (errno != EAGAIN && errno != EINTR)
+ {
+ g_warning ("%s: couldn't read error: %s", priv->name, g_strerror (errno));
+ close_immediately (self, "internal-error");
+ return FALSE;
+ }
+
+ if (priv->err_forward_to_log)
+ forward_error (self);
+
+ return TRUE;
+ }
+ }
+
+ g_byte_array_set_size (priv->err_buffer, len + ret);
+
+ if (priv->err_forward_to_log)
+ forward_error (self);
+
+ if (ret == 0)
+ {
+ g_debug ("%s: end of error", priv->name);
+ priv->err_done = TRUE;
+ stop_error (self);
+ }
+
+ g_object_ref (self);
+
+ if (priv->err_done)
+ close_maybe (self);
+
+ g_object_unref (self);
+ return TRUE;
+}
+
+static gboolean
+fd_readable (int fd)
+{
+ struct pollfd pfd = { .fd = fd, .events = POLLIN };
+ return poll (&pfd, 1, 0) > 0 && (pfd.revents & POLLIN);
+}
+
+static void
+drain_error (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ while (priv->err_source && fd_readable (priv->err_fd))
+ dispatch_error (priv->err_fd, G_IO_IN, self);
+}
+
+static void
+close_output (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ priv->out_done = TRUE;
+
+ if (priv->out_fd != -1)
+ {
+ g_debug ("%s: end of output", priv->name);
+
+ /* And if closing, then we need to shutdown the output fd */
+ if (shutdown (priv->out_fd, SHUT_WR) < 0)
+ {
+ if (errno == ENOTSOCK)
+ {
+ g_debug ("%s: not a socket, closing entirely", priv->name);
+ close (priv->out_fd);
+
+ if (priv->in_fd == priv->out_fd)
+ {
+ priv->in_done = TRUE;
+ priv->in_fd = -1;
+ if (priv->in_source)
+ {
+ g_debug ("%s: and closing input because same fd", priv->name);
+ stop_input (self);
+ }
+ }
+
+ priv->out_fd = -1;
+ }
+ else
+ {
+ g_warning ("%s: couldn't shutdown fd: %s", priv->name, g_strerror (errno));
+ close_immediately (self, "internal-error");
+ }
+ }
+ }
+
+ close_maybe (self);
+}
+
+static void
+set_problem_from_errno (CockpitPipe *self,
+ const gchar *message,
+ int errn)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+ const gchar *problem = NULL;
+
+ if (errn == EPERM || errn == EACCES)
+ problem = "access-denied";
+ else if (errn == ENOENT || errn == ECONNREFUSED)
+ problem = "not-found";
+ /* only warn about Cockpit-internal fds, not opaque user ones */
+ else if (errn == EBADF && priv->is_user_fd)
+ problem = "protocol-error";
+
+ g_free (priv->problem);
+
+ if (problem)
+ {
+ g_message ("%s: %s: %s", priv->name, message, g_strerror (errn));
+ priv->problem = g_strdup (problem);
+ }
+ else
+ {
+ g_warning ("%s: %s: %s", priv->name, message, g_strerror (errn));
+ priv->problem = g_strdup ("internal-error");
+ }
+}
+
+static gboolean
+dispatch_connect (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+ socklen_t slen;
+ int error;
+
+ priv->connecting = FALSE;
+
+ slen = sizeof (error);
+ if (getsockopt (priv->out_fd, SOL_SOCKET, SO_ERROR, &error, &slen) != 0)
+ {
+ g_warning ("%s: couldn't get connection result", priv->name);
+ close_immediately (self, "internal-error");
+ }
+ else if (error == EINPROGRESS)
+ {
+ /* keep connecting */
+ priv->connecting = TRUE;
+ }
+ else if (error != 0)
+ {
+ set_problem_from_errno (self, "couldn't connect", error);
+ close_immediately (self, NULL); /* problem already set */
+ }
+ else
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+dispatch_output (gint fd,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ CockpitPipe *self = (CockpitPipe *)user_data;
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+ struct iovec iov[4];
+ gsize partial, size, before;
+ GBytes *popped;
+ gssize ret;
+ gint i, count;
+ GList *l;
+
+ /* A non-blocking connect is processed here */
+ if (priv->connecting && !dispatch_connect (self))
+ return TRUE;
+
+ g_return_val_if_fail (priv->out_source, FALSE);
+
+ before = priv->out_queued;
+
+ /* Note we fall through when nothing to write */
+ partial = priv->out_partial;
+ for (l = priv->out_queue->head, i = 0;
+ i < G_N_ELEMENTS (iov) && l != NULL;
+ i++, l = g_list_next (l))
+ {
+ iov[i].iov_base = (gpointer)g_bytes_get_data (l->data, &iov[i].iov_len);
+
+ if (partial)
+ {
+ g_assert (partial < iov[i].iov_len);
+ iov[i].iov_len -= partial;
+ iov[i].iov_base = ((gchar *)iov[i].iov_base) + partial;
+ partial = 0;
+ }
+ }
+ count = i;
+
+ if (count == 0)
+ ret = 0;
+ else
+ ret = writev (priv->out_fd, iov, count);
+ if (ret < 0)
+ {
+ if (errno != EAGAIN && errno != EINTR)
+ {
+ if (errno == EPIPE)
+ {
+ g_debug ("%s: couldn't write: %s", priv->name, g_strerror (errno));
+ close_immediately (self, "terminated");
+ }
+ else
+ {
+ set_problem_from_errno (self, "couldn't write", errno);
+ close_immediately (self, NULL); /* already set */
+ }
+ }
+ return FALSE;
+ }
+
+ /* Figure out what was written */
+ for (i = 0; ret > 0 && i < count; i++)
+ {
+ if (ret >= iov[i].iov_len)
+ {
+ g_debug ("%s: wrote %d bytes", priv->name, (int)iov[i].iov_len);
+ popped = g_queue_pop_head (priv->out_queue);
+ size = g_bytes_get_size (popped);
+ g_assert (size <= priv->out_queued);
+ priv->out_queued -= size;
+ g_bytes_unref (popped);
+ priv->out_partial = 0;
+ ret -= iov[i].iov_len;
+ }
+ else
+ {
+ g_debug ("%s: partial write %d of %d bytes", priv->name,
+ (int)ret, (int)iov[i].iov_len);
+ priv->out_partial += ret;
+ ret = 0;
+ }
+ }
+
+ /*
+ * If we're controlling another flow, turn it on again when our output
+ * buffer size becomes less than the low mark.
+ */
+ if (before >= QUEUE_PRESSURE && priv->out_queued < QUEUE_PRESSURE)
+ {
+ g_debug ("%s: have %" G_GSIZE_FORMAT " bytes queued, releasing pressure", priv->name, priv->out_queued);
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), FALSE);
+ }
+
+ if (priv->out_queue->head)
+ return TRUE;
+
+ g_debug ("%s: output queue empty", priv->name);
+
+ /* If all messages are done, then stop polling out fd */
+ stop_output (self);
+
+ if (priv->closing)
+ close_output (self);
+ else
+ close_maybe (self);
+
+ return TRUE;
+}
+
+static void
+start_output (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_assert (priv->out_source == NULL);
+ priv->out_source = g_unix_fd_source_new (priv->out_fd, G_IO_OUT);
+ g_source_set_name (priv->out_source, "pipe-output");
+ g_source_set_callback (priv->out_source, (GSourceFunc)dispatch_output, self, NULL);
+ g_source_attach (priv->out_source, priv->context);
+}
+
+static void
+start_input (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_assert (priv->in_source == NULL);
+ priv->in_source = g_unix_fd_source_new (priv->in_fd, G_IO_IN);
+ g_source_set_name (priv->in_source, "pipe-input");
+ g_source_set_callback (priv->in_source, (GSourceFunc)dispatch_input, self, NULL);
+ g_source_attach (priv->in_source, priv->context);
+}
+
+static void
+cockpit_pipe_constructed (GObject *object)
+{
+ CockpitPipe *self = COCKPIT_PIPE (object);
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+ GError *error = NULL;
+
+ G_OBJECT_CLASS (cockpit_pipe_parent_class)->constructed (object);
+
+ if (priv->name == NULL)
+ priv->name = g_strdup ("pipe");
+
+ if (priv->in_fd >= 0)
+ {
+ if (!g_unix_set_fd_nonblocking (priv->in_fd, TRUE, &error))
+ {
+ g_warning ("%s: couldn't set file descriptor to non-blocking: %s",
+ priv->name, error->message);
+ g_clear_error (&error);
+ }
+
+ start_input (self);
+ }
+ else
+ {
+ priv->in_done = TRUE;
+ }
+
+ if (priv->out_fd >= 0)
+ {
+ if (!g_unix_set_fd_nonblocking (priv->out_fd, TRUE, &error))
+ {
+ g_warning ("%s: couldn't set file descriptor to non-blocking: %s",
+ priv->name, error->message);
+ g_clear_error (&error);
+ }
+ start_output (self);
+ }
+ else
+ {
+ priv->out_done = TRUE;
+ }
+
+ if (priv->err_fd >= 0)
+ {
+ if (!g_unix_set_fd_nonblocking (priv->err_fd, TRUE, &error))
+ {
+ g_warning ("%s: couldn't set file descriptor to non-blocking: %s",
+ priv->name, error->message);
+ g_clear_error (&error);
+ }
+
+ priv->err_buffer = g_byte_array_new ();
+ priv->err_source = g_unix_fd_source_new (priv->err_fd, G_IO_IN);
+ g_source_set_name (priv->err_source, "pipe-error");
+ g_source_set_callback (priv->err_source, (GSourceFunc)dispatch_error, self, NULL);
+ g_source_attach (priv->err_source, priv->context);
+ }
+ else
+ {
+ priv->err_done = TRUE;
+ }
+
+ if (priv->pid)
+ {
+ priv->is_process = TRUE;
+
+ /* We may need this watch to outlast this process ... */
+ priv->watch_arg = g_new0 (CockpitPipe *, 1);
+ *(priv->watch_arg) = self;
+
+ priv->child = g_child_watch_source_new (priv->pid);
+ g_source_set_callback (priv->child, (GSourceFunc)on_child_reap,
+ priv->watch_arg, g_free);
+ g_source_attach (priv->child, priv->context);
+ }
+}
+
+static void
+cockpit_pipe_set_property (GObject *obj,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitPipe *self = COCKPIT_PIPE (obj);
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ priv->name = g_value_dup_string (value);
+ break;
+ case PROP_IN_FD:
+ priv->in_fd = g_value_get_int (value);
+ break;
+ case PROP_OUT_FD:
+ priv->out_fd = g_value_get_int (value);
+ break;
+ case PROP_ERR_FD:
+ priv->err_fd = g_value_get_int (value);
+ break;
+ case PROP_PID:
+ priv->pid = g_value_get_int (value);
+ break;
+ case PROP_PROBLEM:
+ priv->problem = g_value_dup_string (value);
+ if (priv->problem)
+ cockpit_close_later (self);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_pipe_get_property (GObject *obj,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitPipe *self = COCKPIT_PIPE (obj);
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ g_value_set_string (value, priv->name);
+ break;
+ case PROP_IN_FD:
+ g_value_set_int (value, priv->in_fd);
+ break;
+ case PROP_OUT_FD:
+ g_value_set_int (value, priv->out_fd);
+ break;
+ case PROP_ERR_FD:
+ g_value_set_int (value, priv->err_fd);
+ break;
+ case PROP_PID:
+ g_value_set_int (value, priv->pid);
+ break;
+ case PROP_PROBLEM:
+ g_value_set_string (value, priv->problem);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_pipe_dispose (GObject *object)
+{
+ CockpitPipe *self = COCKPIT_PIPE (object);
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ if (priv->pid && !priv->exited)
+ {
+ g_debug ("%s: killing child: %d", priv->name, (int)priv->pid);
+ kill (priv->pid, SIGTERM);
+ }
+
+ if (!priv->closed)
+ close_immediately (self, "terminated");
+
+ cockpit_pipe_throttle (COCKPIT_FLOW (self), NULL);
+ g_assert (priv->pressure == NULL);
+
+ while (priv->out_queue->head)
+ g_bytes_unref (g_queue_pop_head (priv->out_queue));
+ priv->out_queued = 0;
+
+ G_OBJECT_CLASS (cockpit_pipe_parent_class)->dispose (object);
+}
+
+static void
+cockpit_pipe_finalize (GObject *object)
+{
+ CockpitPipe *self = COCKPIT_PIPE (object);
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_assert (priv->closed);
+ g_assert (!priv->in_source);
+ g_assert (!priv->out_source);
+
+ /* Release our reference on watch handler */
+ if (priv->child)
+ g_source_unref (priv->child);
+
+ /*
+ * Tell the child watch that we've gone away ...
+ * But note that if the child watch hasn't fired, it'll continue to wait
+ */
+ if (priv->watch_arg)
+ *(priv->watch_arg) = NULL;
+
+ g_byte_array_unref (priv->in_buffer);
+ if (priv->err_buffer)
+ g_byte_array_unref (priv->err_buffer);
+ g_queue_free (priv->out_queue);
+ g_free (priv->problem);
+ g_free (priv->name);
+
+ if (priv->context)
+ g_main_context_unref (priv->context);
+
+ G_OBJECT_CLASS (cockpit_pipe_parent_class)->finalize (object);
+}
+
+static void
+cockpit_pipe_class_init (CockpitPipeClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->constructed = cockpit_pipe_constructed;
+ gobject_class->get_property = cockpit_pipe_get_property;
+ gobject_class->set_property = cockpit_pipe_set_property;
+ gobject_class->dispose = cockpit_pipe_dispose;
+ gobject_class->finalize = cockpit_pipe_finalize;
+
+ /**
+ * CockpitPipe:in-fd:
+ *
+ * The file descriptor the pipe reads from. The pipe owns the
+ * file descriptor and will close it.
+ */
+ g_object_class_install_property (gobject_class, PROP_IN_FD,
+ g_param_spec_int ("in-fd", "in-fd", "in-fd", -1, G_MAXINT, -1,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitPipe:out-fd:
+ *
+ * The file descriptor the pipe writes to. The pipe owns the
+ * file descriptor and will close it.
+ */
+ g_object_class_install_property (gobject_class, PROP_OUT_FD,
+ g_param_spec_int ("out-fd", "out-fd", "out-fd", -1, G_MAXINT, -1,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitPipe:err-fd:
+ *
+ * The file descriptor the pipe reads error output from. The pipe owns the
+ * file descriptor and will close it.
+ */
+ g_object_class_install_property (gobject_class, PROP_ERR_FD,
+ g_param_spec_int ("err-fd", "err-fd", "err-fd", -1, G_MAXINT, -1,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitPipe:pid:
+ *
+ * The process id of the pipe, if the pipe is talking to a process.
+ * Otherwise set to zero.
+ *
+ * If you use cockpit_pipe_transport_spawn() to create the
+ * #CockpitPipe then this will be non zero.
+ */
+ g_object_class_install_property (gobject_class, PROP_PID,
+ g_param_spec_int ("pid", "pid", "pid", 0, G_MAXINT, 0,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+ /**
+ * CockpitPipe:name:
+ *
+ * Pipe name used for debugging purposes.
+ */
+ g_object_class_install_property (gobject_class, PROP_NAME,
+ g_param_spec_string ("name", "name", "name", "<unnamed>",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitPipe:problem:
+ *
+ * The problem that the pipe closed with. If used as a constructor argument then
+ * the pipe will be created in a closed/failed state. Although 'closed' signal will
+ * only fire once main loop is hit.
+ */
+ g_object_class_install_property (gobject_class, PROP_PROBLEM,
+ g_param_spec_string ("problem", "problem", "problem", NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * CockpitPipe::read:
+ * @buffer: a GByteArray of the read data
+ * @eof: whether the pipe is done reading
+ *
+ * Emitted when data is read from the input file descriptor of the
+ * pipe.
+ *
+ * Data consumed from @buffer by the handler should be removed from
+ * the GByteArray. This can be done with the cockpit_pipe_consume()
+ * function.
+ *
+ * This handler will only be called once with @eof set to TRUE. But
+ * in error conditions it may not be called with @eof set to TRUE
+ * at all, and the CockpitPipe::close signal will simply fire.
+ */
+ cockpit_pipe_sig_read = g_signal_new ("read", COCKPIT_TYPE_PIPE, G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (CockpitPipeClass, read),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 2, G_TYPE_BYTE_ARRAY, G_TYPE_BOOLEAN);
+
+ /**
+ * CockpitPipe::close:
+ * @problem: problem string or %NULL
+ *
+ * Emitted when the pipe closes, whether due to a problem or a normal
+ * shutdown.
+ *
+ * @problem will be NULL if the pipe closed normally.
+ */
+ cockpit_pipe_sig_close = g_signal_new ("close", COCKPIT_TYPE_PIPE, G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET (CockpitPipeClass, close),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1, G_TYPE_STRING);
+}
+
+/**
+ * cockpit_pipe_write:
+ * @self: the pipe
+ * @data: the data to write
+ *
+ * Write @data to the pipe. This is not done immediately, it's
+ * queued and written when the pipe is ready.
+ *
+ * If you cockpit_pipe_close() with a @problem, then queued data
+ * will be discarded.
+ *
+ * Calling this function on a closed or closing pipe (one on which
+ * cockpit_pipe_close() has been called) is invalid.
+ *
+ * Zero length data blocks are ignored, it doesn't makes sense to
+ * write zero bytes to a pipe.
+ */
+void
+_cockpit_pipe_write (CockpitPipe *self,
+ GBytes *data,
+ const gchar *caller,
+ int line)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+ gsize size, before;
+
+ g_return_if_fail (COCKPIT_IS_PIPE (self));
+
+ /* If priv->io is already gone but we are still waiting for the
+ child to exit, then we haven't emitted the "close" signal yet
+ and it isn't an error to try to send more messages. We drop them
+ here.
+ */
+ if (priv->closed && priv->child && priv->pid != 0)
+ {
+ g_debug ("%s: dropping message while waiting for child to exit", priv->name);
+ return;
+ }
+
+ /*
+ * Debugging this issue so have made thingcs more verbose.
+ * HACK: https://github.com/cockpit-project/cockpit/issues/2978
+ */
+ if (priv->closed)
+ {
+ g_critical ("assertion priv->closed check failed at %s %d (%p %d)",
+ caller, line, priv->child, priv->pid);
+ return;
+ }
+
+ size = g_bytes_get_size (data);
+ if (size == 0)
+ {
+ g_debug ("%s: ignoring zero byte data block", priv->name);
+ return;
+ }
+
+ before = priv->out_queued;
+ g_return_if_fail (G_MAXSIZE - size > priv->out_queued);
+ priv->out_queued += size;
+ g_queue_push_tail (priv->out_queue, g_bytes_ref (data));
+
+ /*
+ * If we have too much data queued, and are controlling another flow
+ * tell it to stop sending data, each time we cross over the high bound.
+ */
+ if (before < QUEUE_PRESSURE && priv->out_queued >= QUEUE_PRESSURE)
+ {
+ g_debug ("%s: have %" G_GSIZE_FORMAT "bytes queued, emitting pressure", priv->name, priv->out_queued);
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), TRUE);
+ }
+
+ if (!priv->out_source && priv->out_fd >= 0)
+ {
+ start_output (self);
+ }
+
+ /*
+ * If this becomes thread-safe, then something like this is needed:
+ * g_main_context_wakeup (g_source_get_context (priv->source));
+ */
+}
+
+/**
+ * cockpit_pipe_close:
+ * @self: a pipe
+ * @problem: a problem or NULL
+ *
+ * Close the pipe. If @problem is non NULL, then it's treated
+ * as if an error occurred, and the pipe is closed immediately.
+ * Otherwise the pipe output is closed when all data has been sent.
+ *
+ * The 'close' signal will be fired when the pipe actually closes.
+ * This may be during this function call (esp. in the case of a
+ * non-NULL @problem) or later.
+ */
+void
+cockpit_pipe_close (CockpitPipe *self,
+ const gchar *problem)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_return_if_fail (COCKPIT_IS_PIPE (self));
+
+ priv->closing = TRUE;
+
+ if (problem)
+ close_immediately (self, problem);
+ else if (g_queue_is_empty (priv->out_queue))
+ close_output (self);
+ else
+ g_debug ("%s: pipe closing when output queue empty", priv->name);
+}
+
+static gboolean
+on_later_close (gpointer user_data)
+{
+ close_immediately (user_data, NULL); /* problem already set */
+ return FALSE;
+}
+
+static void
+cockpit_close_later (CockpitPipe *self)
+{
+ GSource *source = g_idle_source_new ();
+ g_source_set_priority (source, G_PRIORITY_HIGH);
+ g_source_set_callback (source, on_later_close, g_object_ref (self), g_object_unref);
+ g_source_attach (source, g_main_context_get_thread_default ());
+ g_source_unref (source);
+}
+
+/**
+ * cockpit_pipe_connect:
+ * @name: name for pipe, for debugging
+ * @address: socket address to connect to
+ *
+ * Create a new pipe connected as a client to the given socket
+ * address, which can be a unix or inet address. Will connect
+ * in stream mode.
+ *
+ * If the connection fails, a pipe is still returned. It will
+ * close once the main loop is run with an appropriate problem.
+ *
+ * Returns: (transfer full): newly allocated CockpitPipe.
+ */
+CockpitPipe *
+cockpit_pipe_connect (const gchar *name,
+ GSocketAddress *address)
+{
+ gboolean connecting = FALSE;
+ gsize native_len;
+ gpointer native;
+ CockpitPipe *pipe;
+ CockpitPipePrivate *priv;
+ int errn = 0;
+ int sock;
+
+ g_return_val_if_fail (G_IS_SOCKET_ADDRESS (address), NULL);
+
+ sock = socket (g_socket_address_get_family (address), SOCK_STREAM, 0);
+ if (sock < 0)
+ {
+ errn = errno;
+ }
+ else
+ {
+ if (!g_unix_set_fd_nonblocking (sock, TRUE, NULL))
+ {
+ close (sock);
+ g_return_val_if_reached (NULL);
+ }
+
+ native_len = g_socket_address_get_native_size (address);
+ native = g_malloc (native_len);
+ if (!g_socket_address_to_native (address, native, native_len, NULL))
+ {
+ close (sock);
+ g_return_val_if_reached (NULL);
+ }
+ if (connect (sock, native, native_len) < 0)
+ {
+ if (errno == EINPROGRESS)
+ {
+ connecting = TRUE;
+ }
+ else
+ {
+ errn = errno;
+ close (sock);
+ sock = -1;
+ }
+ }
+ g_free (native);
+ }
+
+ pipe = g_object_new (COCKPIT_TYPE_PIPE,
+ "in-fd", sock,
+ "out-fd", sock,
+ "name", name,
+ NULL);
+ priv = cockpit_pipe_get_instance_private (pipe);
+
+ priv->connecting = connecting;
+ if (errn != 0)
+ {
+ set_problem_from_errno (pipe, "couldn't connect", errn);
+ cockpit_close_later (pipe);
+ }
+
+ return pipe;
+}
+
+static GSpawnFlags
+calculate_spawn_flags (const gchar **env,
+ CockpitPipeFlags pflags)
+{
+ GSpawnFlags flags = G_SPAWN_DO_NOT_REAP_CHILD;
+ gboolean path_flag = FALSE;
+
+ for (; env && env[0]; env++)
+ {
+ if (g_str_has_prefix (env[0], "PATH="))
+ {
+ flags |= G_SPAWN_SEARCH_PATH_FROM_ENVP;
+ path_flag = TRUE;
+ break;
+ }
+ }
+
+ if (!path_flag)
+ flags |= G_SPAWN_SEARCH_PATH;
+
+ if (pflags & COCKPIT_PIPE_STDERR_TO_NULL)
+ flags |= G_SPAWN_STDERR_TO_DEV_NULL;
+
+ return flags;
+}
+
+static void
+spawn_setup (gpointer data)
+{
+ CockpitPipeFlags flags = GPOINTER_TO_INT (data);
+
+ /* Send this signal to all direct child processes, when bridge dies */
+#ifdef __linux
+ prctl (PR_SET_PDEATHSIG, SIGHUP);
+#endif
+
+ if (flags & COCKPIT_PIPE_STDERR_TO_STDOUT) {
+ int r = dup2 (1, 2);
+ g_assert (r == 2); /* that should really never fail */
+ }
+}
+
+/**
+ * cockpit_pipe_spawn:
+ * @argv: null terminated string array of command arguments
+ * @env: optional null terminated string array of child environment
+ * @directory: optional working directory of child process
+ * @flags: flags pertaining to stderr
+ *
+ * Launch a child process and create a CockpitPipe for it. Standard
+ * in and standard out are connected to the pipe. The default location
+ * for standard error usually goes to the journal.
+ *
+ * If the spawn fails, a pipe is still returned. It will
+ * close once the main loop is run with an appropriate problem.
+ *
+ * NOTE: Although we could probably implement this as construct arguments
+ * and without the @pipe_gtype argument, this is just simpler
+ * for now.
+ *
+ * Returns: (transfer full): newly allocated CockpitPipe.
+ */
+CockpitPipe *
+cockpit_pipe_spawn (const gchar **argv,
+ const gchar **env,
+ const gchar *directory,
+ CockpitPipeFlags flags)
+{
+ CockpitPipe *pipe = NULL;
+ CockpitPipePrivate *priv;
+ int session_stdin = -1;
+ int session_stdout = -1;
+ int session_stderr = -1;
+ GError *error = NULL;
+ const gchar *problem = NULL;
+ int *with_stderr = NULL;
+ gchar *name;
+ GPid pid = 0;
+
+ if (flags & COCKPIT_PIPE_STDERR_TO_MEMORY)
+ with_stderr = &session_stderr;
+
+ g_spawn_async_with_pipes (directory, (gchar **)argv, (gchar **)env,
+ calculate_spawn_flags (env, flags),
+ spawn_setup, GINT_TO_POINTER (flags),
+ &pid, &session_stdin, &session_stdout, with_stderr, &error);
+
+ name = g_path_get_basename (argv[0]);
+ if (name == NULL)
+ name = g_strdup (argv[0]);
+
+ pipe = g_object_new (COCKPIT_TYPE_PIPE,
+ "name", name,
+ "in-fd", session_stdout,
+ "out-fd", session_stdin,
+ "err-fd", session_stderr,
+ "pid", pid,
+ NULL);
+
+ priv = cockpit_pipe_get_instance_private (pipe);
+
+ /* Regardless of whether spawn succeeded or not */
+ priv->is_process = TRUE;
+
+ if (error)
+ {
+ if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT))
+ problem = "not-found";
+ else if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_PERM) ||
+ g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_ACCES))
+ problem = "access-denied";
+
+ if (problem)
+ {
+ g_debug ("%s: couldn't run %s: %s", name, argv[0], error->message);
+ }
+ else
+ {
+ g_message ("%s: couldn't run %s: %s", name, argv[0], error->message);
+ problem = "internal-error";
+ }
+ priv->problem = g_strdup (problem);
+ cockpit_close_later (pipe);
+ g_error_free (error);
+ }
+ else
+ {
+ g_debug ("%s: spawned: %s", name, argv[0]);
+ }
+
+ g_free (name);
+
+ return pipe;
+}
+
+
+/**
+ * cockpit_pipe_pty:
+ * @argv: null terminated string array of command arguments
+ * @env: optional null terminated string array of child environment
+ * @directory: optional working directory of child process
+ * @window_rows: initial number of rows in the window
+ * @window_cols: initial number of columns in the window
+ *
+ * Launch a child pty and create a CockpitPipe for it.
+ *
+ * If the pty or exec fails, a pipe is still returned. It will
+ * close once the main loop is run with an appropriate problem.
+ *
+ * Returns: (transfer full): newly allocated CockpitPipe.
+ */
+CockpitPipe *
+cockpit_pipe_pty (const gchar **argv,
+ const gchar **env,
+ const gchar *directory,
+ guint16 window_rows,
+ guint16 window_cols)
+{
+ CockpitPipe *pipe = NULL;
+ CockpitPipePrivate *priv;
+ const gchar *path = NULL;
+ GPid pid = 0;
+ int fd;
+ struct winsize winsz = { window_rows, window_cols, 0, 0 };
+
+ if (env)
+ path = g_environ_getenv ((gchar **)env, "PATH");
+
+ pid = forkpty (&fd, NULL, NULL, &winsz);
+ if (pid == 0)
+ {
+ closefrom (3);
+
+ if (directory)
+ {
+ if (chdir (directory) < 0)
+ {
+ g_printerr ("couldn't change to directory: %s\n", g_strerror (errno));
+ _exit (127);
+ }
+ }
+ /* Allow the commands below to act on $PATH */
+ if (path)
+ putenv ((gchar *)path);
+ if (env)
+ execvpe (argv[0], (char *const *)argv, (char *const *)env);
+ else
+ execvp (argv[0], (char *const *)argv);
+ g_printerr ("couldn't execute: %s: %s\n", argv[0], g_strerror (errno));
+ _exit (127);
+ }
+ else if (pid < 0)
+ {
+ g_warning ("forkpty failed: %s", g_strerror (errno));
+ pid = 0;
+ fd = -1;
+ }
+
+ pipe = g_object_new (COCKPIT_TYPE_PIPE,
+ "name", argv[0],
+ "in-fd", fd,
+ "out-fd", fd,
+ "pid", pid,
+ NULL);
+
+ priv = cockpit_pipe_get_instance_private (pipe);
+
+ if (fd < 0)
+ {
+ priv->problem = g_strdup ("internal-error");
+ cockpit_close_later (pipe);
+ }
+
+ return pipe;
+}
+
+
+/**
+ * cockpit_pipe_get_pid:
+ * @self: a pipe
+ *
+ * Get the pid of this pipe or zero if not a process
+ * pipe.
+ *
+ * Returns: the pid or zero
+ */
+gboolean
+cockpit_pipe_get_pid (CockpitPipe *self,
+ GPid *pid)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_return_val_if_fail (COCKPIT_IS_PIPE (self), FALSE);
+ if (!priv->is_process)
+ return FALSE;
+ if (pid)
+ *pid = priv->pid;
+ return TRUE;
+}
+
+/**
+ * cockpit_pipe_is_closed:
+ * @self: a pipe
+ *
+ * Returns: TRUE if the pipe is closed
+ */
+gboolean
+cockpit_pipe_is_closed (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_return_val_if_fail (COCKPIT_IS_PIPE (self), FALSE);
+
+ return priv->closed;
+}
+
+/**
+ * cockpit_pipe_get_name:
+ * @self: a pipe
+ *
+ * Get the name of the pipe.
+ *
+ * This is used for logging.
+ *
+ * Returns: (transfer none): the name
+ */
+const gchar *
+cockpit_pipe_get_name (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_return_val_if_fail (COCKPIT_IS_PIPE (self), NULL);
+ return priv->name;
+}
+
+/**
+ * cockpit_pipe_get_buffer:
+ * @self: a pipe
+ *
+ * Get the input buffer for the pipe.
+ *
+ * This can change when the main loop is run. You can use
+ * cockpit_pipe_consume() to consume data from it.
+ *
+ * Returns: (transfer none): the buffer
+ */
+GByteArray *
+cockpit_pipe_get_buffer (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_return_val_if_fail (COCKPIT_IS_PIPE (self), NULL);
+ return priv->in_buffer;
+}
+
+GByteArray *
+cockpit_pipe_get_stderr (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_return_val_if_fail (COCKPIT_IS_PIPE (self), NULL);
+ return priv->err_buffer;
+}
+
+gchar *
+cockpit_pipe_take_stderr_as_utf8 (CockpitPipe *self)
+{
+ GByteArray *buffer;
+ GBytes *clean;
+ gchar *data;
+ gsize length;
+
+ drain_error (self);
+
+ buffer = cockpit_pipe_get_stderr (self);
+ if (!buffer)
+ return NULL;
+
+ /* A little more complicated to avoid big copies */
+ g_byte_array_ref (buffer);
+ g_byte_array_append (buffer, (guint8 *)"x", 1); /* place holder for null terminate */
+
+ {
+ g_autoptr(GBytes) bytes = g_byte_array_free_to_bytes (buffer);
+ clean = cockpit_unicode_force_utf8 (bytes);
+ }
+
+ data = g_bytes_unref_to_data (clean, &length);
+
+ /* Fill in null terminate, for x above */
+ g_assert (length > 0);
+ data[length - 1] = '\0';
+
+ return data;
+}
+
+void
+cockpit_pipe_stop_stderr_capture (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ if (priv->err_buffer)
+ {
+ priv->err_forward_to_log = TRUE;
+ forward_error (self);
+ }
+}
+
+/**
+ * cockpit_pipe_exit_status:
+ * @self: a pipe
+ *
+ * Get the exit status of a process pipe. This is only
+ * valid if this pipe has a CockpitPipe:pid property
+ * and the CockpitPipe::closed signal has fired.
+ *
+ * This is the raw exit status from waitpid() and friends
+ * and needs to be checked if it's a signal or exit return
+ * value.
+ *
+ * Returns: the exit signal.
+ */
+gint
+cockpit_pipe_exit_status (CockpitPipe *self)
+{
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ g_return_val_if_fail (COCKPIT_IS_PIPE (self), -1);
+ return priv->status;
+}
+
+/**
+ * cockpit_pipe_consume:
+ * @buffer: a data buffer
+ * @before: amount of preceding bytes to discard
+ * @length: length of data to consume
+ * @after: amount of trailing bytes to discard
+ *
+ * Used to consume data from the buffer passed to the
+ * read signal.
+ *
+ * @before + @length + @after bytes will be removed from the @buffer,
+ * and @length bytes will be returned.
+ *
+ * As an omptimization of @before + @length + @after is equal to the
+ * entire length of the buffer, then the data will not
+ * be copied but ownership will be transferred to the returned
+ * bytes.
+ *
+ * Returns: (transfer full): the read bytes
+ */
+GBytes *
+cockpit_pipe_consume (GByteArray *buffer,
+ gsize before,
+ gsize length,
+ gsize after)
+{
+ GBytes *bytes;
+ guint8 *buf;
+
+ g_return_val_if_fail (buffer != NULL, NULL);
+
+ /* Optimize when we match full buffer length */
+ if (buffer->len == before + length + after)
+ {
+ /* When array is reffed, this just clears byte array */
+ g_byte_array_ref (buffer);
+ buf = g_byte_array_free (buffer, FALSE);
+ bytes = g_bytes_new_with_free_func (buf + before, length, g_free, buf);
+ }
+ else
+ {
+ bytes = g_bytes_new (buffer->data + before, length);
+ g_byte_array_remove_range (buffer, 0, before + length + after);
+ }
+
+ return bytes;
+}
+
+/**
+ * cockpit_pipe_skip:
+ * @buffer: a data buffer
+ * @skip: amount of bytes to skip
+ *
+ * Used to remove data from the front of the buffer.
+ * @skip should be less than the number of bytes in
+ * the buffer.
+ */
+void
+cockpit_pipe_skip (GByteArray *buffer,
+ gsize skip)
+{
+ g_return_if_fail (buffer != NULL);
+ g_byte_array_remove_range (buffer, 0, skip);
+}
+
+/**
+ * cockpit_pipe_new:
+ * @name: a name for debugging
+ * @in_fd: the input file descriptor
+ * @out_fd: the output file descriptor
+ *
+ * Create a pipe for the given file descriptors.
+ *
+ * Returns: (transfer full): a new CockpitPipe
+ */
+CockpitPipe *
+cockpit_pipe_new (const gchar *name,
+ gint in_fd,
+ gint out_fd)
+{
+ return g_object_new (COCKPIT_TYPE_PIPE,
+ "name", name,
+ "in-fd", in_fd,
+ "out-fd", out_fd,
+ NULL);
+}
+
+/**
+ * cockpit_pipe_new_user_fd:
+ * @name: a name for debugging
+ * @fd: the file descriptor (might be input or output or both)
+ *
+ * Create a pipe for the given user-supplied opaque file descriptor. This is
+ * not being read/written by cockpit itself, but intended for passing fds.
+ *
+ * Returns: (transfer full): a new CockpitPipe
+ */
+CockpitPipe *
+cockpit_pipe_new_user_fd (const gchar *name,
+ gint fd)
+{
+ CockpitPipe *p = g_object_new (COCKPIT_TYPE_PIPE,
+ "name", name,
+ "in-fd", fd,
+ "out-fd", fd,
+ NULL);
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (p);
+ priv->is_user_fd = TRUE;
+ return p;
+}
+
+static gint
+environ_find (gchar **env,
+ const gchar *variable)
+{
+ gint len, x;
+ gchar *pos;
+
+ pos = strchr (variable, '=');
+ if (pos == NULL)
+ len = strlen (variable);
+ else
+ len = pos - variable;
+
+ for (x = 0; env && env[x]; x++)
+ {
+ if (strncmp (env[x], variable, len) == 0 &&
+ env[x][len] == '=')
+ return x;
+ }
+
+ return -1;
+}
+
+/**
+ * cockpit_pipe_get_environ:
+ * @input: Input environment array
+ * @directory: Working directory to put in environment
+ *
+ * Prepares an environment for spawning a CockpitPipe process.
+ * This merges the fields in @input with the current process
+ * environment.
+ *
+ * This is the standard way of processing an "environ" field
+ * in either an "open" message or a "bridges" definition in
+ * manifest.json.
+ *
+ * The current working @directory for the new process is
+ * optionally specified. It will set a $PWD environment
+ * variable as expected by shells.
+ *
+ * Returns: (transfer full): A new environment block to
+ * be freed with g_strfreev().
+ */
+gchar **
+cockpit_pipe_get_environ (const gchar **input,
+ const gchar *directory)
+{
+ gchar **env = g_get_environ ();
+ gsize length = g_strv_length (env);
+ gboolean had_pwd = FALSE;
+ gint i, x;
+
+ for (i = 0; input && input[i] != NULL; i++)
+ {
+ if (g_str_has_prefix (input[i], "PWD="))
+ had_pwd = TRUE;
+ x = environ_find (env, input[i]);
+ if (x != -1)
+ {
+ g_free (env[x]);
+ env[x] = g_strdup (input[i]);
+ }
+ else
+ {
+ env = g_renew (gchar *, env, length + 2);
+ env[length] = g_strdup (input[i]);
+ env[length + 1] = NULL;
+ length++;
+ }
+ }
+
+ /*
+ * The kernel only knows about the inode of the current directory.
+ * So when we spawn a shell, it won't know the directory it's
+ * meant to display. Pass it the path we care about in $PWD
+ */
+ if (!had_pwd && directory)
+ env = g_environ_setenv (env, "PWD", directory, TRUE);
+
+ return env;
+}
+
+static void
+on_throttle_pressure (GObject *object,
+ gboolean throttle,
+ gpointer user_data)
+{
+ CockpitPipe *self = COCKPIT_PIPE (user_data);
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ if (throttle)
+ {
+ if (priv->in_source != NULL)
+ {
+ g_debug ("%s: applying back pressure in pipe", priv->name);
+ stop_input (self);
+ }
+ }
+ else
+ {
+ if (priv->in_source == NULL && !priv->in_done)
+ {
+ g_debug ("%s: relieving back pressure in pipe", priv->name);
+ start_input (self);
+ }
+ }
+}
+
+static void
+cockpit_pipe_throttle (CockpitFlow *flow,
+ CockpitFlow *controlling)
+{
+ CockpitPipe *self = COCKPIT_PIPE (flow);
+ CockpitPipePrivate *priv = cockpit_pipe_get_instance_private (self);
+
+ if (priv->pressure)
+ {
+ g_signal_handler_disconnect (priv->pressure, priv->pressure_sig);
+ g_object_remove_weak_pointer (G_OBJECT (priv->pressure), (gpointer *)&priv->pressure);
+ priv->pressure = NULL;
+ }
+
+ if (controlling)
+ {
+ priv->pressure = controlling;
+ g_object_add_weak_pointer (G_OBJECT (priv->pressure), (gpointer *)&priv->pressure);
+ priv->pressure_sig = g_signal_connect (controlling, "pressure", G_CALLBACK (on_throttle_pressure), self);
+ }
+}
+
+static void
+cockpit_pipe_flow_iface_init (CockpitFlowInterface *iface)
+{
+ iface->throttle = cockpit_pipe_throttle;
+}
diff --git a/src/common/cockpitpipe.h b/src/common/cockpitpipe.h
new file mode 100644
index 0000000..e72b33d
--- /dev/null
+++ b/src/common/cockpitpipe.h
@@ -0,0 +1,112 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_PIPE_H__
+#define __COCKPIT_PIPE_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+ COCKPIT_PIPE_FLAGS_NONE = 0,
+ COCKPIT_PIPE_STDERR_TO_STDOUT = 1 << 1,
+ COCKPIT_PIPE_STDERR_TO_NULL = 1 << 2,
+ COCKPIT_PIPE_STDERR_TO_MEMORY = 1 << 3,
+} CockpitPipeFlags;
+
+#define COCKPIT_TYPE_PIPE (cockpit_pipe_get_type ())
+G_DECLARE_DERIVABLE_TYPE(CockpitPipe, cockpit_pipe, COCKPIT, PIPE, GObject)
+
+struct _CockpitPipeClass {
+ GObjectClass parent_class;
+
+ /* signals */
+
+ void (* read) (CockpitPipe *pipe,
+ GByteArray *buffer,
+ gboolean eof);
+
+ void (* close) (CockpitPipe *pipe,
+ const gchar *problem);
+};
+
+CockpitPipe * cockpit_pipe_new (const gchar *name,
+ gint in_fd,
+ gint out_fd);
+
+CockpitPipe * cockpit_pipe_new_user_fd (const gchar *name,
+ gint fd);
+
+CockpitPipe * cockpit_pipe_spawn (const gchar **argv,
+ const gchar **env,
+ const gchar *directory,
+ CockpitPipeFlags flags);
+
+CockpitPipe * cockpit_pipe_pty (const gchar **argv,
+ const gchar **env,
+ const gchar *directory,
+ guint16 window_rows,
+ guint16 window_cols);
+
+CockpitPipe * cockpit_pipe_connect (const gchar *name,
+ GSocketAddress *address);
+
+/* HACK: Trying to debug self->priv->closed assertion */
+#define cockpit_pipe_write(s, d) (_cockpit_pipe_write (s, d, G_STRFUNC, __LINE__))
+
+void _cockpit_pipe_write (CockpitPipe *self,
+ GBytes *data,
+ const gchar *caller,
+ gint line);
+
+void cockpit_pipe_close (CockpitPipe *self,
+ const gchar *problem);
+
+gint cockpit_pipe_exit_status (CockpitPipe *self);
+
+const gchar * cockpit_pipe_get_name (CockpitPipe *self);
+
+GByteArray * cockpit_pipe_get_buffer (CockpitPipe *self);
+
+GByteArray * cockpit_pipe_get_stderr (CockpitPipe *self);
+
+gchar * cockpit_pipe_take_stderr_as_utf8 (CockpitPipe *self);
+
+void cockpit_pipe_stop_stderr_capture (CockpitPipe *self);
+
+gboolean cockpit_pipe_get_pid (CockpitPipe *self,
+ GPid *pid);
+
+gboolean cockpit_pipe_is_closed (CockpitPipe *self);
+
+void cockpit_pipe_skip (GByteArray *buffer,
+ gsize skip);
+
+GBytes * cockpit_pipe_consume (GByteArray *buffer,
+ gsize before,
+ gsize length,
+ gsize after);
+
+gchar ** cockpit_pipe_get_environ (const gchar **set,
+ const gchar *directory);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_PIPE_H__ */
diff --git a/src/common/cockpitpipetransport.c b/src/common/cockpitpipetransport.c
new file mode 100644
index 0000000..fc9d084
--- /dev/null
+++ b/src/common/cockpitpipetransport.c
@@ -0,0 +1,393 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpipetransport.h"
+
+#include "cockpitframe.h"
+#include "cockpitpipe.h"
+
+#include <glib-unix.h>
+
+#include <sys/socket.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+/**
+ * CockpitPipeTransport:
+ *
+ * A #CockpitTransport implementation that shuttles data over a
+ * #CockpitPipe. See doc/protocol.md for information on how the
+ * framing looks ... including the MSB length prefix.
+ */
+
+struct _CockpitPipeTransport {
+ CockpitTransport parent_instance;
+ gchar *name;
+ CockpitPipe *pipe;
+ gboolean closed;
+ gulong read_sig;
+ gulong close_sig;
+};
+
+enum {
+ PROP_0,
+ PROP_NAME,
+ PROP_PIPE,
+};
+
+static void cockpit_transport_read_from_pipe (CockpitTransport *self,
+ const gchar *logname,
+ CockpitPipe *pipe,
+ gboolean *closed,
+ GByteArray *input,
+ gboolean end_of_data);
+
+G_DEFINE_TYPE (CockpitPipeTransport, cockpit_pipe_transport, COCKPIT_TYPE_TRANSPORT);
+
+static void
+cockpit_pipe_transport_init (CockpitPipeTransport *self)
+{
+
+}
+
+static void
+on_pipe_read (CockpitPipe *pipe,
+ GByteArray *input,
+ gboolean end_of_data,
+ gpointer user_data)
+{
+ CockpitPipeTransport *self = COCKPIT_PIPE_TRANSPORT (user_data);
+ cockpit_transport_read_from_pipe (COCKPIT_TRANSPORT (self), self->name,
+ pipe, &self->closed, input, end_of_data);
+
+ if (end_of_data)
+ cockpit_pipe_close (self->pipe, NULL);
+}
+
+static void
+on_pipe_close (CockpitPipe *pipe,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitPipeTransport *self = COCKPIT_PIPE_TRANSPORT (user_data);
+ gboolean is_cockpit;
+ GError *error = NULL;
+ gint status;
+
+ self->closed = TRUE;
+
+ /* This function is called by the base class when it is closed */
+ if (cockpit_pipe_get_pid (pipe, NULL))
+ {
+ is_cockpit = g_str_equal (self->name, "cockpit-bridge") ||
+ g_str_has_suffix (self->name, "/cockpit-session");
+
+ if (problem == NULL ||
+ g_str_equal (problem, "internal-error"))
+ {
+ status = cockpit_pipe_exit_status (pipe);
+ // HUP: bridge child cleaned up by our PR_SET_PDEATHSIG
+ if (WIFSIGNALED (status) && (WTERMSIG (status) == SIGTERM || WTERMSIG (status) == SIGHUP))
+ problem = "terminated";
+ else if (is_cockpit && WIFEXITED (status) && WEXITSTATUS (status) == 127)
+ problem = "no-cockpit"; // cockpit-bridge not installed
+ else if (is_cockpit && WIFEXITED (status) && WEXITSTATUS (status) == 1)
+ problem = "access-denied"; // disabled user shells like /bin/false or /sbin/nologin exit 1
+ else if (WIFEXITED (status) && WEXITSTATUS (status) == 255)
+ problem = "terminated"; // failed or got a signal, etc.
+ else if (!g_spawn_check_exit_status (status, &error))
+ {
+ problem = "internal-error";
+ if (is_cockpit)
+ g_warning ("%s: bridge program failed: %s", self->name, error->message);
+ else
+ g_debug ("%s: process failed: %s", self->name, error->message);
+ g_error_free (error);
+ }
+ }
+ else if (g_str_equal (problem, "not-found"))
+ {
+ if (is_cockpit)
+ {
+ g_message ("%s: failed to execute bridge: not found", self->name);
+ problem = "no-cockpit";
+ }
+ else
+ {
+ g_debug ("%s: failed to run: not found", self->name);
+ }
+ }
+ }
+
+ g_debug ("%s: closed%s%s", self->name,
+ problem ? ": " : "", problem ? problem : "");
+
+ cockpit_transport_emit_closed (COCKPIT_TRANSPORT (self), problem);
+}
+
+static void
+cockpit_pipe_transport_constructed (GObject *object)
+{
+ CockpitPipeTransport *self = COCKPIT_PIPE_TRANSPORT (object);
+
+ G_OBJECT_CLASS (cockpit_pipe_transport_parent_class)->constructed (object);
+
+ g_return_if_fail (self->pipe != NULL);
+ g_object_get (self->pipe, "name", &self->name, NULL);
+ self->read_sig = g_signal_connect (self->pipe, "read", G_CALLBACK (on_pipe_read), self);
+ self->close_sig = g_signal_connect (self->pipe, "close", G_CALLBACK (on_pipe_close), self);
+}
+
+static void
+cockpit_pipe_transport_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitPipeTransport *self = COCKPIT_PIPE_TRANSPORT (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ g_value_set_string (value, self->name);
+ break;
+ case PROP_PIPE:
+ g_value_set_object (value, cockpit_pipe_transport_get_pipe (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_pipe_transport_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitPipeTransport *self = COCKPIT_PIPE_TRANSPORT (object);
+
+ switch (prop_id)
+ {
+ case PROP_PIPE:
+ self->pipe = g_value_dup_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_pipe_transport_finalize (GObject *object)
+{
+ CockpitPipeTransport *self = COCKPIT_PIPE_TRANSPORT (object);
+
+ g_signal_handler_disconnect (self->pipe, self->read_sig);
+ g_signal_handler_disconnect (self->pipe, self->close_sig);
+
+ g_free (self->name);
+ g_clear_object (&self->pipe);
+
+ G_OBJECT_CLASS (cockpit_pipe_transport_parent_class)->finalize (object);
+}
+
+static void
+cockpit_pipe_transport_send (CockpitTransport *transport,
+ const gchar *channel_id,
+ GBytes *payload)
+{
+ CockpitPipeTransport *self = COCKPIT_PIPE_TRANSPORT (transport);
+ GBytes *prefix;
+ gchar *prefix_str;
+ gsize payload_len;
+ gsize channel_len;
+
+ if (self->closed)
+ {
+ g_debug ("dropping message on closed transport");
+ return;
+ }
+
+ channel_len = channel_id ? strlen (channel_id) : 0;
+ payload_len = g_bytes_get_size (payload);
+
+ prefix_str = g_strdup_printf ("%" G_GSIZE_FORMAT "\n%s\n",
+ channel_len + 1 + payload_len,
+ channel_id ? channel_id : "");
+ prefix = g_bytes_new_take (prefix_str, strlen (prefix_str));
+
+ cockpit_pipe_write (self->pipe, prefix);
+ cockpit_pipe_write (self->pipe, payload);
+ g_bytes_unref (prefix);
+
+ g_debug ("%s: queued %" G_GSIZE_FORMAT " byte payload", self->name, payload_len);
+}
+
+static void
+cockpit_pipe_transport_close (CockpitTransport *transport,
+ const gchar *problem)
+{
+ CockpitPipeTransport *self = COCKPIT_PIPE_TRANSPORT (transport);
+ cockpit_pipe_close (self->pipe, problem);
+}
+
+static void
+cockpit_pipe_transport_class_init (CockpitPipeTransportClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ CockpitTransportClass *transport_class = COCKPIT_TRANSPORT_CLASS (klass);
+
+ transport_class->send = cockpit_pipe_transport_send;
+ transport_class->close = cockpit_pipe_transport_close;
+
+ gobject_class->constructed = cockpit_pipe_transport_constructed;
+ gobject_class->get_property = cockpit_pipe_transport_get_property;
+ gobject_class->set_property = cockpit_pipe_transport_set_property;
+ gobject_class->finalize = cockpit_pipe_transport_finalize;
+
+ g_object_class_override_property (gobject_class, PROP_NAME, "name");
+
+ g_object_class_install_property (gobject_class, PROP_PIPE,
+ g_param_spec_object ("pipe", NULL, NULL,
+ COCKPIT_TYPE_PIPE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+}
+
+/**
+ * cockpit_pipe_transport_new:
+ * @pipe: the pipe to send data over
+ *
+ * Create a new CockpitPipeTransport for a pipe
+ *
+ * Returns: (transfer full): the new transport
+ */
+CockpitTransport *
+cockpit_pipe_transport_new (CockpitPipe *pipe)
+{
+ return g_object_new (COCKPIT_TYPE_PIPE_TRANSPORT,
+ "pipe", pipe,
+ NULL);
+}
+
+/**
+ * cockpit_pipe_transport_new_fds:
+ * @name: name for debugging
+ * @in_fd: the file descriptor to read from
+ * @out_fd: the file descriptor to write to
+ *
+ * Create a new CockpitPipeTransport for a pair
+ * of file descriptors.
+ *
+ * Returns: (transfer full): the new transport
+ */
+CockpitTransport *
+cockpit_pipe_transport_new_fds (const gchar *name,
+ gint in_fd,
+ gint out_fd)
+{
+ CockpitTransport *transport;
+ CockpitPipe *pipe;
+
+ pipe = cockpit_pipe_new (name, in_fd, out_fd);
+ transport = cockpit_pipe_transport_new (pipe);
+ g_object_unref (pipe);
+
+ return transport;
+}
+
+CockpitPipe *
+cockpit_pipe_transport_get_pipe (CockpitPipeTransport *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_PIPE_TRANSPORT (self), NULL);
+ return self->pipe;
+}
+
+/**
+ * cockpit_transport_read_from_pipe:
+ *
+ * Meant to be used in a "read" handler for a #CockpitPipe
+ * Closed is pointer to a boolean value that may be updated
+ * during the read and parse loop.
+ */
+static void
+cockpit_transport_read_from_pipe (CockpitTransport *self,
+ const gchar *logname,
+ CockpitPipe *pipe,
+ gboolean *closed,
+ GByteArray *input,
+ gboolean end_of_data)
+{
+ /* This may be updated during the loop. */
+ g_assert (closed != NULL);
+ g_object_ref (self);
+
+ while (!*closed)
+ {
+ gsize i;
+ gssize size = cockpit_frame_parse (input->data, input->len, &i);
+
+ if (size == 0)
+ {
+ if (!end_of_data)
+ g_debug ("%s: want more data", logname);
+ break;
+ }
+ else if (size < 0)
+ {
+ g_warning ("%s: incorrect protocol: received invalid length prefix", logname);
+ cockpit_pipe_close (pipe, "protocol-error");
+ break;
+ }
+ else if (input->len < i + size)
+ {
+ g_debug ("%s: want more data 2", logname);
+ break;
+ }
+
+ g_autoptr(GBytes) message = cockpit_pipe_consume (input, i, size, 0);
+ g_autofree gchar *channel = NULL;
+ g_autoptr(GBytes) payload = cockpit_transport_parse_frame (message, &channel);
+ if (payload)
+ {
+ g_debug ("%s: received a %d byte payload", logname, (int)size);
+ cockpit_transport_emit_recv (self, channel, payload);
+ }
+ }
+
+ if (end_of_data)
+ {
+ /* Received a partial message */
+ if (input->len > 0)
+ {
+ g_debug ("%s: received truncated %d byte frame", logname, input->len);
+ cockpit_pipe_close (pipe, "disconnected");
+ }
+ }
+
+ g_object_unref (self);
+}
diff --git a/src/common/cockpitpipetransport.h b/src/common/cockpitpipetransport.h
new file mode 100644
index 0000000..6cf87f2
--- /dev/null
+++ b/src/common/cockpitpipetransport.h
@@ -0,0 +1,43 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_PIPE_TRANSPORT_H__
+#define __COCKPIT_PIPE_TRANSPORT_H__
+
+#include <gio/gio.h>
+
+#include "cockpitpipe.h"
+#include "cockpittransport.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_PIPE_TRANSPORT (cockpit_pipe_transport_get_type ())
+G_DECLARE_FINAL_TYPE(CockpitPipeTransport, cockpit_pipe_transport, COCKPIT, PIPE_TRANSPORT, CockpitTransport)
+
+CockpitTransport * cockpit_pipe_transport_new (CockpitPipe *pipe);
+
+CockpitTransport * cockpit_pipe_transport_new_fds (const gchar *name,
+ gint in_fd,
+ gint out_fd);
+
+CockpitPipe * cockpit_pipe_transport_get_pipe (CockpitPipeTransport *self);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_PIPE_TRANSPORT_H__ */
diff --git a/src/common/cockpitsocket.c b/src/common/cockpitsocket.c
new file mode 100644
index 0000000..437ea3e
--- /dev/null
+++ b/src/common/cockpitsocket.c
@@ -0,0 +1,90 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitsocket.h"
+
+#include <sys/socket.h>
+#include <gio/gio.h>
+
+/* _always_ takes ownership of fd, even in error case */
+static gpointer
+cockpit_socket_new_take_fd (int fd,
+ GError **error)
+{
+ GSocket *socket = g_socket_new_from_fd (fd, error);
+ if (socket == NULL)
+ close (fd);
+
+ return socket;
+}
+
+static gpointer
+cockpit_socket_connection_new_take_fd (GType expected_type,
+ int fd,
+ GError **error)
+{
+ g_autoptr(GSocket) socket = cockpit_socket_new_take_fd (fd, error);
+ if (socket == NULL)
+ return NULL;
+
+ g_autoptr(GSocketConnection) connection = g_socket_connection_factory_create_connection (socket);
+ if (!G_TYPE_CHECK_INSTANCE_TYPE (connection, expected_type))
+ {
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "connection has type %s, not %s as expected",
+ G_OBJECT_TYPE_NAME (connection), g_type_name (expected_type));
+ return NULL;
+ }
+
+ return g_steal_pointer (&connection);
+}
+
+void
+cockpit_socket_socketpair (GSocket **one,
+ GSocket **two)
+{
+ int sv[2];
+ if (socketpair (AF_LOCAL, SOCK_STREAM, 0, sv))
+ g_error ("socketpair(AF_LOCAL, SOCK_STREAM) failed: %m");
+
+ g_autoptr(GError) error = NULL;
+ *one = g_socket_new_from_fd (sv[0], &error);
+ g_assert_no_error (error);
+
+ *two = g_socket_new_from_fd (sv[1], &error);
+ g_assert_no_error (error);
+}
+
+void
+cockpit_socket_streampair (GIOStream **one,
+ GIOStream **two)
+{
+ int sv[2];
+ if (socketpair (AF_LOCAL, SOCK_STREAM, 0, sv))
+ g_error ("socketpair(AF_LOCAL, SOCK_STREAM) failed: %m");
+
+ g_autoptr(GError) error = NULL;
+ *one = cockpit_socket_connection_new_take_fd (G_TYPE_IO_STREAM, sv[0], &error);
+ g_assert_no_error (error);
+
+ *two = cockpit_socket_connection_new_take_fd (G_TYPE_IO_STREAM, sv[1], &error);
+ g_assert_no_error (error);
+}
diff --git a/src/common/cockpitsocket.h b/src/common/cockpitsocket.h
new file mode 100644
index 0000000..89e6bf6
--- /dev/null
+++ b/src/common/cockpitsocket.h
@@ -0,0 +1,30 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+void
+cockpit_socket_socketpair (GSocket **one,
+ GSocket **two);
+
+void
+cockpit_socket_streampair (GIOStream **one,
+ GIOStream **two);
diff --git a/src/common/cockpitsystem.c b/src/common/cockpitsystem.c
new file mode 100644
index 0000000..4c5c286
--- /dev/null
+++ b/src/common/cockpitsystem.c
@@ -0,0 +1,211 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitsystem.h"
+
+#include <glib/gstdio.h>
+
+#include <systemd/sd-login.h>
+
+#include <sys/types.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+/* Used to override from tests */
+const gchar *cockpit_system_proc_base = "/proc";
+
+static const gchar *os_release_fields[] = {
+ "NAME",
+ "VERSION",
+ "ID",
+ "VERSION_ID",
+ "PRETTY_NAME",
+ "VARIANT",
+ "VARIANT_ID",
+ "CPE_NAME",
+ "DOCUMENTATION_URL",
+ NULL
+};
+
+const gchar **
+cockpit_system_os_release_fields (void)
+{
+ return os_release_fields;
+}
+
+GHashTable *
+cockpit_system_load_os_release (void)
+{
+ GError *error = NULL;
+ GHashTable *result = NULL;
+ gchar *contents = NULL;
+ gsize len;
+ gchar **lines = NULL;
+ guint n;
+ gchar *line, *val;
+
+ g_file_get_contents ("/etc/os-release", &contents, &len, &error);
+
+ if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ {
+ g_clear_error (&error);
+ g_file_get_contents ("/usr/lib/os-release", &contents, &len, &error);
+ }
+
+ if (error)
+ {
+ g_message ("error loading contents of os-release: %s", error->message);
+ goto out;
+ }
+
+ result = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ lines = g_strsplit (contents, "\n", -1);
+ for (n = 0; lines != NULL && lines[n] != NULL; n++)
+ {
+ line = lines[n];
+ val = strchr (line, '=');
+
+ if (val)
+ {
+ *val = '\0';
+ val++;
+
+ /* Remove quotes from value */
+ len = strlen (val);
+ if (len && val[0] == '\"' && val[len - 1] == '\"')
+ {
+ val[len - 1] = '\0';
+ val++;
+ }
+
+ g_hash_table_replace (result, line, val);
+ }
+ else
+ {
+ g_free (line);
+ }
+ }
+
+ out:
+ g_clear_error (&error);
+ g_free (lines);
+ g_free (contents);
+
+ return result;
+}
+
+guint64
+cockpit_system_process_start_time (void)
+{
+ GError *error = NULL;
+ gchar *filename = NULL;
+ gchar *contents = NULL;
+ gchar **tokens = NULL;
+ gchar *endp = NULL;
+ size_t length;
+ guint num_tokens;
+ gchar *p;
+
+ guint64 start_time = 0;
+
+ filename = g_strdup_printf ("%s/%d/stat", cockpit_system_proc_base, getpid ());
+ if (!g_file_get_contents (filename, &contents, &length, &error))
+ {
+ g_warning ("couldn't read start time: %s", error->message);
+ goto out;
+ }
+
+ /*
+ * Start time is the token at index 19 after the '(process name)' entry - since only this
+ * field can contain the ')' character, search backwards for this
+ */
+ p = strrchr (contents, ')');
+ if (p == NULL)
+ {
+ g_warning ("error parsing stat command: %s", filename);
+ goto out;
+ }
+ p += 2; /* skip ') ' */
+ if (p - contents >= (int) length)
+ {
+ g_warning ("error parsing stat command: %s", filename);
+ goto out;
+ }
+
+ tokens = g_strsplit (p, " ", 0);
+ num_tokens = g_strv_length (tokens);
+ if (num_tokens < 20)
+ {
+ g_warning ("error parsing stat tokens: %s", filename);
+ goto out;
+ }
+
+ start_time = g_ascii_strtoull (tokens[19], &endp, 10);
+ if (!endp || endp == tokens[19] || *endp)
+ {
+ start_time = 0;
+ g_warning ("error parsing start time: %s'", filename);
+ goto out;
+ }
+
+out:
+ if (error)
+ g_error_free (error);
+ g_strfreev (tokens);
+ g_free (filename);
+ g_free (contents);
+
+ return start_time;
+}
+
+char *
+cockpit_system_session_id (void)
+{
+ char *session_id;
+ pid_t pid;
+ int res;
+
+ pid = getppid ();
+ res = sd_pid_get_session (pid, &session_id);
+ if (res == 0)
+ {
+ return session_id;
+ }
+ else
+ {
+ if (res != -ENODATA && res != -ENXIO)
+ g_message ("could not look up session id for bridge process: %u: %s", pid, g_strerror (-res));
+ return NULL;
+ }
+}
+
+void
+cockpit_setenv_check (const char *variable,
+ const char *value,
+ gboolean overwrite)
+{
+ if (!g_setenv (variable, value, overwrite))
+ g_error("could not set $%s to %s", variable, value);
+}
diff --git a/src/common/cockpitsystem.h b/src/common/cockpitsystem.h
new file mode 100644
index 0000000..aab87b2
--- /dev/null
+++ b/src/common/cockpitsystem.h
@@ -0,0 +1,42 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_SYSTEM_H__
+#define __COCKPIT_SYSTEM_H__
+
+#include <glib.h>
+#include "cockpitjson.h"
+
+G_BEGIN_DECLS
+
+GHashTable * cockpit_system_load_os_release (void);
+
+const gchar ** cockpit_system_os_release_fields (void);
+
+guint64 cockpit_system_process_start_time (void);
+
+char * cockpit_system_session_id (void);
+
+void cockpit_setenv_check (const char *variable,
+ const char *value,
+ gboolean overwrite);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_SYSTEM_H__ */
diff --git a/src/common/cockpittemplate.c b/src/common/cockpittemplate.c
new file mode 100644
index 0000000..2befc75
--- /dev/null
+++ b/src/common/cockpittemplate.c
@@ -0,0 +1,216 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpittemplate.h"
+
+#include "cockpitjson.h"
+
+#include <glib.h>
+
+#include <string.h>
+
+#define VARCHARS "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"
+
+static gchar *
+find_variable (const gchar *start_marker,
+ const gchar *end_marker,
+ const gchar *data,
+ const gchar *end,
+ const gchar **before,
+ const gchar **after)
+{
+ const gchar *a;
+ const gchar *b;
+ const gchar *c;
+ const gchar *d;
+
+ for (;;)
+ {
+ /* Look for start_marker to end_marker */
+ a = memmem (data, strlen(data), start_marker, strlen (start_marker));
+ if (a == NULL)
+ return NULL;
+
+ data = a + strlen (start_marker);
+ b = data;
+
+ c = memmem (data, strlen(data), end_marker, strlen (end_marker));
+ if (c == NULL)
+ return NULL;
+
+ data = c + strlen (end_marker);
+ d = data;
+
+ /*
+ * We've found a variable like this:
+ *
+ * Some text @@variable.part@@ trailing.
+ * a b c d
+ *
+ * Check that the name makes sense.
+ */
+ if (b != c && b + strspn (b, VARCHARS) == c)
+ break;
+ }
+
+ if (before)
+ *before = a;
+ if (after)
+ *after = d;
+ return g_strndup (b, c - b);
+}
+
+GList *
+cockpit_template_expand (GBytes *input,
+ const gchar *start_marker,
+ const gchar *end_marker,
+ CockpitTemplateFunc func,
+ gpointer user_data)
+{
+ GList *output = NULL;
+ const gchar *data;
+ const gchar *end;
+ const gchar *before;
+ const gchar *after;
+ GBytes *bytes;
+ gchar *name;
+ gboolean escaped;
+ gint before_len;
+
+ g_return_val_if_fail (func != NULL, NULL);
+
+ data = g_bytes_get_data (input, NULL);
+ end = data + g_bytes_get_size (input);
+
+ for (;;)
+ {
+ escaped = FALSE;
+ name = find_variable (start_marker, end_marker, data, end, &before, &after);
+ if (name == NULL)
+ break;
+
+ if (before != data)
+ {
+ g_assert (before > data);
+
+ /* Check if the char before the match is the escape char '/' */
+ before_len = before - data;
+ if (data[before_len - 1] == '\\')
+ {
+ escaped = TRUE;
+ before_len--;
+ }
+
+ bytes = g_bytes_new_with_free_func (data, before_len,
+ (GDestroyNotify)g_bytes_unref,
+ g_bytes_ref (input));
+ output = g_list_prepend (output, bytes);
+ }
+
+ if (!escaped)
+ bytes = (func) (name, user_data);
+ else
+ bytes = NULL; // Set bytes to null so it's treated as skipped
+ g_free (name);
+
+ if (!bytes)
+ {
+ g_assert (after > before);
+ bytes = g_bytes_new_with_free_func (before, after - before,
+ (GDestroyNotify)g_bytes_unref,
+ g_bytes_ref (input));
+ }
+ if (g_bytes_get_size (bytes) > 0)
+ output = g_list_prepend (output, bytes);
+ else
+ g_bytes_unref (bytes);
+
+ g_assert (after <= end);
+ data = after;
+ }
+
+ if (data != end)
+ {
+ g_assert (end > data);
+ bytes = g_bytes_new_with_free_func (data, end - data,
+ (GDestroyNotify)g_bytes_unref,
+ g_bytes_ref (input));
+ output = g_list_prepend (output, bytes);
+ }
+
+ return g_list_reverse (output);
+}
+
+typedef struct
+{
+ const gchar *start;
+ const gchar *end;
+ CockpitTemplateFunc func;
+ gpointer user_data;
+} TemplateClosure;
+
+static JsonNode *
+template_walk_func (JsonNode *node,
+ gpointer user_data)
+{
+ TemplateClosure *closure = user_data;
+ const gchar *string = json_node_get_string (node);
+
+ if (string == NULL)
+ return NULL;
+
+ if (strstr (string, closure->start) == NULL)
+ return NULL;
+
+ g_autoptr(GBytes) input = g_bytes_new_with_free_func (string, strlen (string),
+ (GDestroyNotify) json_node_unref,
+ json_node_ref (node));
+
+ GList *output = cockpit_template_expand (input, closure->start, closure->end, closure->func, closure->user_data);
+
+ g_autoptr(GString) result = g_string_new (NULL);
+ while (output)
+ {
+ g_autoptr(GBytes) fragment = output->data;
+ g_string_append_len (result, g_bytes_get_data (fragment, NULL), g_bytes_get_size (fragment));
+ output = g_list_delete_link (output, output);
+ }
+
+ if (g_str_equal (result->str, string))
+ return NULL;
+
+ node = json_node_new (JSON_NODE_VALUE);
+ json_node_set_string (node, result->str);
+
+ return node;
+}
+
+JsonObject *
+cockpit_template_expand_json (JsonObject *object,
+ const gchar *start_marker,
+ const gchar *end_marker,
+ CockpitTemplateFunc func,
+ gpointer user_data)
+{
+ TemplateClosure closure = { start_marker, end_marker, func, user_data };
+
+ return cockpit_json_walk (object, template_walk_func, &closure);
+}
diff --git a/src/common/cockpittemplate.h b/src/common/cockpittemplate.h
new file mode 100644
index 0000000..e831e0e
--- /dev/null
+++ b/src/common/cockpittemplate.h
@@ -0,0 +1,41 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_TEMPLATE_H__
+#define COCKPIT_TEMPLATE_H__
+
+#include <glib.h>
+#include <json-glib/json-glib.h>
+
+typedef GBytes * (* CockpitTemplateFunc) (const gchar *variable,
+ gpointer user_data);
+
+GList * cockpit_template_expand (GBytes *input,
+ const gchar *start_marker,
+ const gchar *end_marker,
+ CockpitTemplateFunc func,
+ gpointer user_data);
+
+JsonObject * cockpit_template_expand_json (JsonObject *object,
+ const gchar *start_marker,
+ const gchar *end_marker,
+ CockpitTemplateFunc func,
+ gpointer user_data);
+
+#endif /* COCKPIT_TEMPLATE_H__ */
diff --git a/src/common/cockpittransport.c b/src/common/cockpittransport.c
new file mode 100644
index 0000000..cc94337
--- /dev/null
+++ b/src/common/cockpittransport.c
@@ -0,0 +1,530 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpittransport.h"
+
+#include "common/cockpitjson.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+typedef struct {
+ gconstpointer channel;
+ JsonObject *control;
+ GBytes *data;
+} FrozenMessage;
+
+static void
+frozen_message_free (gpointer data)
+{
+ FrozenMessage *frozen = data;
+ if (frozen->data)
+ g_bytes_unref (frozen->data);
+ if (frozen->control)
+ json_object_unref (frozen->control);
+ g_slice_free (FrozenMessage, frozen);
+}
+
+enum {
+ RECV,
+ CONTROL,
+ CLOSED,
+ NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS];
+
+typedef struct {
+ GHashTable *freeze;
+ GQueue *frozen;
+} CockpitTransportPrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (CockpitTransport, cockpit_transport, G_TYPE_OBJECT,
+ G_ADD_PRIVATE (CockpitTransport));
+
+static void
+cockpit_transport_init (CockpitTransport *self)
+{
+}
+
+static void
+cockpit_transport_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ /* Should be overridden by derived abstract classes */
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static gboolean
+maybe_freeze_message (CockpitTransport *self,
+ const gchar *channel,
+ JsonObject *control,
+ GBytes *data)
+{
+ CockpitTransportPrivate *priv = cockpit_transport_get_instance_private (self);
+ FrozenMessage *frozen = NULL;
+
+ if (priv->freeze && channel)
+ {
+ /* Note that we dig out the real value for the channel */
+ channel = g_hash_table_lookup (priv->freeze, channel);
+ if (channel)
+ {
+ frozen = g_slice_new0 (FrozenMessage);
+ frozen->channel = channel; /* owned by hashtable */
+ frozen->data = g_bytes_ref (data);
+ if (control)
+ frozen->control = json_object_ref (control);
+ if (!priv->frozen)
+ priv->frozen = g_queue_new ();
+ g_queue_push_tail (priv->frozen, frozen);
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static gboolean
+cockpit_transport_default_recv (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *payload)
+{
+ const gchar *inner_channel;
+ JsonObject *options;
+ const gchar *command = NULL;
+
+ /* Our default handler parses control channel and fires control signal */
+ if (channel)
+ return FALSE;
+
+ /* Read out the actual command and channel this message is about */
+ if (!cockpit_transport_parse_command (payload, &command, &inner_channel, &options))
+ {
+ /* Warning already logged */
+ cockpit_transport_close (transport, "protocol-error");
+ return TRUE;
+ }
+
+ cockpit_transport_emit_control (transport, command, inner_channel, options, payload);
+ json_object_unref (options);
+
+ return TRUE;
+}
+
+static gboolean
+cockpit_transport_default_control (CockpitTransport *transport,
+ const gchar *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload)
+{
+ GBytes *message;
+
+ if (channel != NULL)
+ return FALSE;
+
+ /* A single hop ping. Respond to it right here, immediately */
+ if (g_str_equal (command, "ping"))
+ {
+ json_object_set_string_member (options, "command", "pong");
+ message = cockpit_json_write_bytes (options);
+ cockpit_transport_send (transport, NULL, message);
+ g_bytes_unref (message);
+ return TRUE;
+ }
+ else if (g_str_equal (command, "pong"))
+ {
+ /* Ignore pong commands */
+ return TRUE;
+ }
+
+ /* Not handled */
+ return FALSE;
+}
+
+static void
+cockpit_transport_finalize (GObject *object)
+{
+ CockpitTransport *self = COCKPIT_TRANSPORT (object);
+ CockpitTransportPrivate *priv = cockpit_transport_get_instance_private (self);
+
+ if (priv->freeze)
+ g_hash_table_destroy (priv->freeze);
+ if (priv->frozen)
+ g_queue_free_full (priv->frozen, frozen_message_free);
+
+ G_OBJECT_CLASS (cockpit_transport_parent_class)->finalize (object);
+}
+
+static void
+cockpit_transport_class_init (CockpitTransportClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ klass->recv = cockpit_transport_default_recv;
+ klass->control = cockpit_transport_default_control;
+
+ object_class->get_property = cockpit_transport_get_property;
+ object_class->finalize = cockpit_transport_finalize;
+
+ g_object_class_install_property (object_class, 1,
+ g_param_spec_string ("name", "name", "name", NULL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ signals[RECV] = g_signal_new ("recv", COCKPIT_TYPE_TRANSPORT, G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (CockpitTransportClass, recv),
+ g_signal_accumulator_true_handled, NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 2, G_TYPE_STRING, G_TYPE_BYTES);
+
+ signals[CONTROL] = g_signal_new ("control", COCKPIT_TYPE_TRANSPORT, G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (CockpitTransportClass, control),
+ g_signal_accumulator_true_handled, NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 4,
+ G_TYPE_STRING, G_TYPE_STRING, JSON_TYPE_OBJECT, G_TYPE_BYTES);
+
+ signals[CLOSED] = g_signal_new ("closed", COCKPIT_TYPE_TRANSPORT, G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET (CockpitTransportClass, closed),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 1, G_TYPE_STRING);
+}
+
+void
+cockpit_transport_send (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *data)
+{
+ CockpitTransportClass *klass;
+
+ g_return_if_fail (COCKPIT_IS_TRANSPORT (transport));
+
+ klass = COCKPIT_TRANSPORT_GET_CLASS (transport);
+ g_return_if_fail (klass && klass->send);
+ klass->send (transport, channel, data);
+}
+
+void
+cockpit_transport_close (CockpitTransport *transport,
+ const gchar *problem)
+{
+ CockpitTransportClass *klass;
+
+ g_return_if_fail (COCKPIT_IS_TRANSPORT (transport));
+
+ klass = COCKPIT_TRANSPORT_GET_CLASS (transport);
+ g_return_if_fail (klass && klass->close);
+ klass->close (transport, problem);
+}
+
+void
+cockpit_transport_emit_recv (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *data)
+{
+ gboolean result = FALSE;
+
+ g_return_if_fail (COCKPIT_IS_TRANSPORT (transport));
+
+ if (maybe_freeze_message (transport, channel, NULL, data))
+ return;
+
+ g_signal_emit (transport, signals[RECV], 0, channel, data, &result);
+
+ if (!result)
+ g_debug ("no handler for received message in channel %s", channel);
+}
+
+void
+cockpit_transport_emit_control (CockpitTransport *transport,
+ const gchar *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data)
+{
+ gboolean result = FALSE;
+
+ g_return_if_fail (COCKPIT_IS_TRANSPORT (transport));
+
+ if (maybe_freeze_message (transport, channel, options, data))
+ return;
+
+ g_signal_emit (transport, signals[CONTROL], 0, command, channel, options, data, &result);
+
+ if (!result)
+ g_debug ("received unknown control command: %s", command);
+}
+
+void
+cockpit_transport_emit_closed (CockpitTransport *transport,
+ const gchar *problem)
+{
+ g_return_if_fail (COCKPIT_IS_TRANSPORT (transport));
+ g_signal_emit (transport, signals[CLOSED], 0, problem);
+}
+
+void
+cockpit_transport_freeze (CockpitTransport *self,
+ const gchar *channel)
+{
+ CockpitTransportPrivate *priv = cockpit_transport_get_instance_private (self);
+
+ g_return_if_fail (COCKPIT_IS_TRANSPORT (self));
+ g_return_if_fail (channel != NULL);
+
+ if (!priv->freeze)
+ priv->freeze = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ g_hash_table_add (priv->freeze, g_strdup (channel));
+}
+
+void
+cockpit_transport_thaw (CockpitTransport *self,
+ const gchar *channel)
+{
+ CockpitTransportPrivate *priv = cockpit_transport_get_instance_private (self);
+ FrozenMessage *frozen;
+ const gchar *command;
+ gchar *stolen = NULL;
+ GList *l, *flush;
+
+ g_return_if_fail (COCKPIT_IS_TRANSPORT (self));
+ g_return_if_fail (channel != NULL);
+
+ if (priv->freeze)
+ stolen = g_hash_table_lookup (priv->freeze, channel);
+ if (stolen)
+ g_hash_table_steal (priv->freeze, channel);
+
+ for (l = priv->frozen ? priv->frozen->head : NULL; l != NULL; )
+ {
+ frozen = l->data;
+ flush = (stolen == frozen->channel) ? l : NULL;
+ l = g_list_next (l);
+
+ if (flush)
+ {
+ if (frozen->control)
+ {
+ command = NULL;
+ cockpit_json_get_string (frozen->control, "command", NULL, &command);
+ cockpit_transport_emit_control (self, command, stolen, frozen->control, frozen->data);
+ }
+ else
+ {
+ cockpit_transport_emit_recv (self, stolen, frozen->data);
+ }
+ g_queue_delete_link (priv->frozen, flush);
+ frozen_message_free (frozen);
+ }
+ }
+
+ g_free (stolen);
+}
+
+static GBytes *
+parse_frame (GBytes *message,
+ gboolean expect,
+ gchar **channel)
+{
+ const gchar *data;
+ gsize length;
+ const gchar *line;
+ gsize channel_len;
+
+ g_return_val_if_fail (message != NULL, NULL);
+
+ data = g_bytes_get_data (message, &length);
+ line = memchr (data, '\n', length);
+ if (!line)
+ {
+ if (expect)
+ g_message ("received invalid message without channel prefix");
+ return NULL;
+ }
+
+ channel_len = line - data;
+ if (memchr (data, '\0', channel_len) != NULL)
+ {
+ if (expect)
+ g_message ("received massage with invalid channel prefix");
+ return NULL;
+ }
+
+ if (channel_len)
+ *channel = g_strndup (data, channel_len);
+ else
+ *channel = NULL;
+
+ channel_len++;
+ return g_bytes_new_from_bytes (message, channel_len, length - channel_len);
+}
+
+/**
+ * cockpit_transport_parse_frame:
+ * @message: message to parse
+ * @channel: location to return the channel
+ *
+ * Parse a message into a channel and payload.
+ * @channel will be set to NULL if a control channel
+ * message. @channel must be freed.
+ *
+ * Will return NULL if invalid message.
+ *
+ * Returns: (transfer full): the payload or NULL.
+ */
+GBytes *
+cockpit_transport_parse_frame (GBytes *message,
+ gchar **channel)
+{
+ return parse_frame (message, TRUE, channel);
+}
+
+GBytes *
+cockpit_transport_maybe_frame (GBytes *message,
+ gchar **channel)
+{
+ return parse_frame (message, FALSE, channel);
+}
+
+/**
+ * cockpit_transport_parse_command:
+ * @payload: command JSON payload to parse
+ * @command: a location to return the command
+ * @channel: location to return the channel
+ * @options: location to return the options
+ *
+ * Parse a command and return various values from the
+ * command. The @options value is transferred with ownership,
+ * so you should free it after done. @command and @channel are owned by
+ * @options. @channel will be NULL for a missing channel.
+ *
+ * On failure, message has already been printed.
+ *
+ * Returns: whether command parsed or not.
+ */
+gboolean
+cockpit_transport_parse_command (GBytes *payload,
+ const gchar **command,
+ const gchar **channel,
+ JsonObject **options)
+{
+ GError *error = NULL;
+ gboolean ret = FALSE;
+ JsonObject *object;
+ gboolean valid;
+
+ object = cockpit_json_parse_bytes (payload, &error);
+ if (!object)
+ {
+ g_warning ("Received unparsable control message: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ /* Parse out the command */
+ if (command)
+ {
+ if (!cockpit_json_get_string (object, "command", NULL, command) ||
+ *command == NULL || g_str_equal (*command, ""))
+ {
+ g_warning ("Received invalid control message: invalid or missing command");
+ goto out;
+ }
+ }
+
+ /* Parse out the channel */
+ if (channel)
+ {
+ valid = cockpit_json_get_string (object, "channel", NULL, channel);
+ if (valid && *channel)
+ {
+ valid = (!g_str_equal ("", *channel) &&
+ strcspn (*channel, "\n") == strlen (*channel));
+ }
+ if (!valid)
+ {
+ g_warning ("Received invalid control message: invalid channel");
+ goto out;
+ }
+ }
+
+ *options = json_object_ref (object);
+ ret = TRUE;
+
+out:
+ if (object)
+ json_object_unref (object);
+ return ret;
+}
+
+static JsonObject *
+build_json_va (const gchar *name,
+ va_list va)
+{
+ JsonObject *object;
+ const gchar *value;
+
+ object = json_object_new ();
+
+ while (name)
+ {
+ value = va_arg (va, const gchar *);
+ if (value)
+ json_object_set_string_member (object, name, value);
+ name = va_arg (va, const gchar *);
+ }
+
+ return object;
+}
+
+JsonObject *
+cockpit_transport_build_json (const gchar *name,
+ ...)
+{
+ JsonObject *object;
+ va_list va;
+
+ va_start (va, name);
+ object = build_json_va (name, va);
+ va_end (va);
+
+ return object;
+}
+
+GBytes *
+cockpit_transport_build_control (const gchar *name,
+ ...)
+{
+ JsonObject *object;
+ GBytes *message;
+ va_list va;
+
+ va_start (va, name);
+ object = build_json_va (name, va);
+ va_end (va);
+
+ message = cockpit_json_write_bytes (object);
+ json_object_unref (object);
+ return message;
+}
diff --git a/src/common/cockpittransport.h b/src/common/cockpittransport.h
new file mode 100644
index 0000000..5b4f327
--- /dev/null
+++ b/src/common/cockpittransport.h
@@ -0,0 +1,111 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_TRANSPORT_H__
+#define __COCKPIT_TRANSPORT_H__
+
+#include <glib-object.h>
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_TRANSPORT (cockpit_transport_get_type ())
+G_DECLARE_DERIVABLE_TYPE(CockpitTransport, cockpit_transport, COCKPIT, TRANSPORT, GObject)
+
+struct _CockpitTransportClass
+{
+ GObjectClass parent_class;
+
+ /* signals */
+
+ /*
+ * Fired when the transport receives a new message.
+ */
+ gboolean (* recv) (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *data);
+
+ gboolean (* control) (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload);
+
+ void (* closed) (CockpitTransport *transport,
+ const gchar *problem);
+
+ /* vfuncs */
+
+ /*
+ * Called when transport should queue a new message to send.
+ */
+ void (* send) (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *data);
+
+ void (* close) (CockpitTransport *transport,
+ const gchar *problem);
+};
+
+void cockpit_transport_send (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *data);
+
+void cockpit_transport_close (CockpitTransport *transport,
+ const gchar *problem);
+
+void cockpit_transport_emit_recv (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *data);
+
+void cockpit_transport_emit_control (CockpitTransport *transport,
+ const gchar *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *data);
+
+void cockpit_transport_emit_closed (CockpitTransport *transport,
+ const gchar *problem);
+
+void cockpit_transport_freeze (CockpitTransport *transport,
+ const gchar *channel);
+
+void cockpit_transport_thaw (CockpitTransport *transport,
+ const gchar *channel);
+
+GBytes * cockpit_transport_parse_frame (GBytes *message,
+ gchar **channel);
+
+GBytes * cockpit_transport_maybe_frame (GBytes *message,
+ gchar **channel);
+
+gboolean cockpit_transport_parse_command (GBytes *payload,
+ const gchar **command,
+ const gchar **channel,
+ JsonObject **options);
+
+JsonObject *cockpit_transport_build_json (const gchar *name,
+ ...) G_GNUC_NULL_TERMINATED;
+
+GBytes * cockpit_transport_build_control (const gchar *name,
+ ...) G_GNUC_NULL_TERMINATED;
+
+G_END_DECLS
+
+#endif /* __COCKPIT_TRANSPORT_H__ */
diff --git a/src/common/cockpitunicode.c b/src/common/cockpitunicode.c
new file mode 100644
index 0000000..c50d97c
--- /dev/null
+++ b/src/common/cockpitunicode.c
@@ -0,0 +1,76 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitunicode.h"
+
+gboolean
+cockpit_unicode_has_incomplete_ending (GBytes *input)
+{
+ const gchar *data;
+ const gchar *end;
+ gsize length;
+
+ data = g_bytes_get_data (input, &length);
+ if (g_utf8_validate (data, length, &end))
+ return FALSE;
+
+ do
+ {
+ length -= (end - data) + 1;
+ data = end + 1;
+ }
+ while (!g_utf8_validate (data, length, &end));
+
+ return length == 0;
+}
+
+GBytes *
+cockpit_unicode_force_utf8 (GBytes *input)
+{
+ const gchar *data;
+ const gchar *end;
+ gsize length;
+ GString *string;
+
+ data = g_bytes_get_data (input, &length);
+ if (g_utf8_validate (data, length, &end))
+ return g_bytes_ref (input);
+
+ string = g_string_sized_new (length + 16);
+ do
+ {
+ /* Valid part of the string */
+ g_string_append_len (string, data, end - data);
+
+ /* Replacement character */
+ g_string_append (string, "\xef\xbf\xbd");
+
+ length -= (end - data) + 1;
+ data = end + 1;
+ }
+ while (!g_utf8_validate (data, length, &end));
+
+ if (length)
+ g_string_append_len (string, data, length);
+
+ return g_string_free_to_bytes (string);
+}
+
diff --git a/src/common/cockpitunicode.h b/src/common/cockpitunicode.h
new file mode 100644
index 0000000..bb1ae16
--- /dev/null
+++ b/src/common/cockpitunicode.h
@@ -0,0 +1,33 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_UNICODE_H__
+#define __COCKPIT_UNICODE_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+GBytes * cockpit_unicode_force_utf8 (GBytes *input);
+
+gboolean cockpit_unicode_has_incomplete_ending (GBytes *input);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_UNICODE_H__ */
diff --git a/src/common/cockpitunixsignal.c b/src/common/cockpitunixsignal.c
new file mode 100644
index 0000000..06f21ca
--- /dev/null
+++ b/src/common/cockpitunixsignal.c
@@ -0,0 +1,120 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Portions Copyright (C) 2014 Red Hat, Inc.
+ *
+ * This code is based on code in util-linux.
+ *
+ * Copyright (c) 1988, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ * must display the following acknowledgment:
+ * This product includes software developed by the University of
+ * California, Berkeley and its contributors.
+ * 4. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * Copyright (C) 2014 Sami Kerola <kerolasa@iki.fi>
+ * Copyright (C) 2014 Karel Zak <kzak@redhat.com>
+ */
+
+#include "config.h"
+
+#include <signal.h>
+#include "cockpitunixsignal.h"
+
+struct signv {
+ const char *name;
+ int val;
+} sys_signame[] = {
+ /* POSIX signals */
+ { "HUP", SIGHUP }, /* 1 */
+ { "INT", SIGINT }, /* 2 */
+ { "QUIT", SIGQUIT }, /* 3 */
+ { "ILL", SIGILL }, /* 4 */
+ { "TRAP", SIGTRAP }, /* 5 */
+ { "ABRT", SIGABRT }, /* 6 */
+ { "IOT", SIGIOT }, /* 6, same as SIGABRT */
+#ifdef SIGEMT
+ { "EMT", SIGEMT }, /* 7 (mips,alpha,sparc*) */
+#endif
+ { "BUS", SIGBUS }, /* 7 (arm,i386,m68k,ppc), 10 (mips,alpha,sparc*) */
+ { "FPE", SIGFPE }, /* 8 */
+ { "KILL", SIGKILL }, /* 9 */
+ { "USR1", SIGUSR1 }, /* 10 (arm,i386,m68k,ppc), 30 (alpha,sparc*), 16 (mips) */
+ { "SEGV", SIGSEGV }, /* 11 */
+ { "USR2", SIGUSR2 }, /* 12 (arm,i386,m68k,ppc), 31 (alpha,sparc*), 17 (mips) */
+ { "PIPE", SIGPIPE }, /* 13 */
+ { "ALRM", SIGALRM }, /* 14 */
+ { "TERM", SIGTERM }, /* 15 */
+#ifdef SIGSTKFLT
+ { "STKFLT", SIGSTKFLT }, /* 16 (arm,i386,m68k,ppc) */
+#endif
+ { "CHLD", SIGCHLD }, /* 17 (arm,i386,m68k,ppc), 20 (alpha,sparc*), 18 (mips) */
+#ifdef SIGCLD
+ { "CLD", SIGCLD }, /* same as SIGCHLD (mips, musl libc) */
+#endif
+ { "CONT", SIGCONT }, /* 18 (arm,i386,m68k,ppc), 19 (alpha,sparc*), 25 (mips) */
+ { "STOP", SIGSTOP }, /* 19 (arm,i386,m68k,ppc), 17 (alpha,sparc*), 23 (mips) */
+ { "TSTP", SIGTSTP }, /* 20 (arm,i386,m68k,ppc), 18 (alpha,sparc*), 24 (mips) */
+ { "TTIN", SIGTTIN }, /* 21 (arm,i386,m68k,ppc,alpha,sparc*), 26 (mips) */
+ { "TTOU", SIGTTOU }, /* 22 (arm,i386,m68k,ppc,alpha,sparc*), 27 (mips) */
+ { "URG", SIGURG }, /* 23 (arm,i386,m68k,ppc), 16 (alpha,sparc*), 21 (mips) */
+ { "XCPU", SIGXCPU }, /* 24 (arm,i386,m68k,ppc,alpha,sparc*), 30 (mips) */
+ { "XFSZ", SIGXFSZ }, /* 25 (arm,i386,m68k,ppc,alpha,sparc*), 31 (mips) */
+ { "VTALRM", SIGVTALRM }, /* 26 (arm,i386,m68k,ppc,alpha,sparc*), 28 (mips) */
+ { "PROF", SIGPROF }, /* 27 (arm,i386,m68k,ppc,alpha,sparc*), 29 (mips) */
+ { "WINCH", SIGWINCH }, /* 28 (arm,i386,m68k,ppc,alpha,sparc*), 20 (mips) */
+ { "IO", SIGIO }, /* 29 (arm,i386,m68k,ppc), 23 (alpha,sparc*), 22 (mips) */
+ { "POLL", SIGPOLL }, /* same as SIGIO */
+#ifdef SIGINFO
+ { "INFO", SIGINFO }, /* 29 (alpha) */
+#endif
+#ifdef SIGLOST
+ { "LOST", SIGLOST }, /* 29 (arm,i386,m68k,ppc,sparc*) */
+#endif
+ { "PWR", SIGPWR }, /* 30 (arm,i386,m68k,ppc), 29 (alpha,sparc*), 19 (mips) */
+#ifdef SIGUNUSED
+ { "UNUSED", SIGUNUSED }, /* 31 (arm,i386,m68k,ppc) */
+#endif
+ { "SYS", SIGSYS }, /* 31 (mips,alpha,sparc*) */
+};
+
+gchar *
+cockpit_strsignal (int signum)
+{
+ size_t n;
+
+ for (n = 0; n < G_N_ELEMENTS (sys_signame); n++)
+ if (sys_signame[n].val == signum)
+ return g_strdup (sys_signame[n].name);
+
+#ifdef SIGRTMIN
+ if (SIGRTMIN <= signum && signum <= SIGRTMAX)
+ return g_strdup_printf ("RT%d", signum - SIGRTMIN);
+#endif
+
+ return g_strdup ("UNKNOWN");
+}
diff --git a/src/common/cockpitunixsignal.h b/src/common/cockpitunixsignal.h
new file mode 100644
index 0000000..af2c982
--- /dev/null
+++ b/src/common/cockpitunixsignal.h
@@ -0,0 +1,31 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_UNIX_SIGNAL_H__
+#define __COCKPIT_UNIX_SIGNAL_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+gchar * cockpit_strsignal (int signum);
+
+G_END_DECLS
+
+#endif
diff --git a/src/common/cockpitversion.c b/src/common/cockpitversion.c
new file mode 100644
index 0000000..160a51e
--- /dev/null
+++ b/src/common/cockpitversion.c
@@ -0,0 +1,76 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitversion.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+gint
+cockpit_version_compare (const gchar *one,
+ const gchar *two)
+{
+ gchar **p1 = g_strsplit (one, ".", -1);
+ gchar **p2 = g_strsplit (two, ".", -1);
+ gchar *e1, *e2;
+ gulong v1, v2;
+ gint ret = 0;
+ gint i = 0;
+
+ while (p1[i] != NULL && p2[i] != NULL)
+ {
+ e1 = e2 = NULL;
+ v1 = strtoul (p1[i], &e1, 10);
+ v2 = strtoul (p2[i], &e2, 10);
+
+ /* Compare as numbers */
+ if (e1 && e1[0] == '\0' && e2 && e2[0] == '\0')
+ {
+ if (v1 < v2)
+ {
+ ret = -1;
+ break;
+ }
+ else if (v1 > v2)
+ {
+ ret = 1;
+ break;
+ }
+ }
+ else
+ {
+ ret = strcmp (p1[i], p2[i]);
+ if (ret != 0)
+ break;
+ }
+
+ i++;
+ }
+
+ if (p1[i] && !p2[i])
+ ret = 1;
+ else if (!p1[i] && p2[i])
+ ret = -1;
+
+ g_strfreev (p1);
+ g_strfreev (p2);
+ return ret;
+}
diff --git a/src/common/cockpitversion.h b/src/common/cockpitversion.h
new file mode 100644
index 0000000..117a972
--- /dev/null
+++ b/src/common/cockpitversion.h
@@ -0,0 +1,32 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_VERSION_H__
+#define __COCKPIT_VERSION_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+gint cockpit_version_compare (const gchar *one,
+ const gchar *two);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_VERSION_H__ */
diff --git a/src/common/cockpitwebcertificate.c b/src/common/cockpitwebcertificate.c
new file mode 100644
index 0000000..a42b6a2
--- /dev/null
+++ b/src/common/cockpitwebcertificate.c
@@ -0,0 +1,142 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <assert.h>
+#include <dirent.h>
+#include <errno.h>
+#include <err.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "common/cockpitconf.h"
+#include "common/cockpitmemory.h"
+#include "common/cockpitwebcertificate.h"
+
+static int
+filter_cert (const struct dirent *entry)
+{
+ int len = strlen (entry->d_name);
+
+ /* check if entry ends with .crt or .cert */
+ return (len > 4 && strcmp (entry->d_name + len - 4, ".crt") == 0) ||
+ (len > 5 && strcmp (entry->d_name + len - 5, ".cert") == 0);
+}
+
+static char *
+load_cert_from_dir (const char *dir_name,
+ char **error)
+{
+ struct dirent **certs;
+ int n;
+ char *ret = NULL;
+
+ n = scandir (dir_name, &certs, filter_cert, alphasort);
+ if (n < 0)
+ {
+ if (errno != ENOENT)
+ asprintfx (error, "Error loading certificates from %s: %m", dir_name);
+ return NULL;
+ }
+
+ if (n > 0)
+ asprintfx (&ret, "%s/%s", dir_name, certs[n-1]->d_name);
+ while (n--)
+ free (certs[n]);
+ free (certs);
+ return ret;
+}
+
+/**
+ * cockpit_certificate_locate:
+ * @missing_ok: if "no certificate" is a valid result
+ * @error: a pointer to a place to store an error string
+ *
+ * Find Cockpit web server certificate in $XDG_CONFIG_DIRS/cockpit/ws-certs.d/.
+ * The asciibetically latest *.crt or *.cert file wins.
+ *
+ * Return certificate path on success, or %NULL if none is found or
+ * another error occurs (such as a permissions problem, etc).
+ *
+ * @error must be a pointer to a `char *` originally containing %NULL.
+ * It will be set to the error message in case of errors, and %NULL will
+ * be returned. If the error is "no certificate was found" and
+ * @missing_ok is %TRUE then %NULL will be returned, but @error will be
+ * left unset.
+ */
+char *
+cockpit_certificate_locate (bool missing_ok,
+ char **error)
+{
+ const char * const *dirs = cockpit_conf_get_dirs ();
+
+ assert (error != NULL);
+ assert (*error == NULL);
+
+ for (int i = 0; dirs[i]; i++)
+ {
+ char *cert_dir;
+ char *cert_path;
+
+ asprintfx (&cert_dir, "%s/cockpit/ws-certs.d", dirs[i]);
+ cert_path = load_cert_from_dir (cert_dir, error);
+ free (cert_dir);
+
+ if (*error != NULL)
+ return NULL;
+
+ if (cert_path)
+ return cert_path;
+ }
+
+ if (!missing_ok)
+ asprintfx (error, "No certificate found in dir: %s/cockpit/ws-certs.d", dirs[0]);
+
+ return NULL;
+}
+
+/**
+ * cockpit_certificate_key_path:
+ *
+ * Return key file path for given certfile, i. e. replace ".crt" or ".cert"
+ * suffix with ".key". Invalid names exit the program. All usages of this
+ * function in our code control the file name, so that should not happen.
+ */
+char *
+cockpit_certificate_key_path (const char *certfile)
+{
+ int len = strlen (certfile);
+ char *keypath = NULL;
+
+ /* .cert suffix case: chop off suffix, append ".key" */
+ if (len > 5 && strcmp (certfile + len - 5, ".cert") == 0)
+ asprintfx (&keypath, "%.*s.key", len - 5, certfile);
+ /* *.crt suffix case */
+ else if (len > 4 && strcmp (certfile + len - 4, ".crt") == 0)
+ asprintfx (&keypath, "%.*s.key", len - 4, certfile);
+ else
+ errx (EXIT_FAILURE, "internal error: invalid certificate file name: %s", certfile);
+
+ return keypath;
+}
diff --git a/src/common/cockpitwebcertificate.h b/src/common/cockpitwebcertificate.h
new file mode 100644
index 0000000..334727c
--- /dev/null
+++ b/src/common/cockpitwebcertificate.h
@@ -0,0 +1,28 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_WEBCERTIFICATE_H__
+#define __COCKPIT_WEBCERTIFICATE_H__
+
+#include <stdbool.h>
+
+char * cockpit_certificate_locate (bool missing_ok, char **error);
+char * cockpit_certificate_key_path (const char *certfile);
+
+#endif /* __COCKPIT_WEBCERTIFICATE_H__ */
diff --git a/src/common/cockpitwebfilter.c b/src/common/cockpitwebfilter.c
new file mode 100644
index 0000000..59acbc6
--- /dev/null
+++ b/src/common/cockpitwebfilter.c
@@ -0,0 +1,63 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitwebfilter.h"
+
+/**
+ * CockpitWebFilter
+ *
+ * A filter used to filter the output of a CockpitWebResponse.
+ */
+
+G_DEFINE_INTERFACE (CockpitWebFilter, cockpit_web_filter, 0);
+
+static void
+cockpit_web_filter_default_init (CockpitWebFilterInterface *iface)
+{
+
+}
+
+/**
+ * cockpit_web_filter_push:
+ * @filter: filter to push a block of bytes into
+ * @queue: block of bytes to filter
+ * @function: filter calls this function with bytes generated
+ * @data: value to pass to function
+ *
+ * Called to send data through a filter. The filter should call
+ * the @function with any data it generates. If the filter wants
+ * to pass through the @queue data, then it needs to call @function
+ * with it.
+ */
+void
+cockpit_web_filter_push (CockpitWebFilter *filter,
+ GBytes *queue,
+ void (* function) (gpointer, GBytes *),
+ gpointer data)
+{
+ CockpitWebFilterInterface *iface;
+
+ iface = COCKPIT_WEB_FILTER_GET_IFACE (filter);
+ g_return_if_fail (iface != NULL);
+
+ g_assert (iface->push);
+ (iface->push) (filter, queue, function, data);
+}
diff --git a/src/common/cockpitwebfilter.h b/src/common/cockpitwebfilter.h
new file mode 100644
index 0000000..462906f
--- /dev/null
+++ b/src/common/cockpitwebfilter.h
@@ -0,0 +1,46 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_WEB_FILTER_H__
+#define COCKPIT_WEB_FILTER_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_WEB_FILTER (cockpit_web_filter_get_type ())
+G_DECLARE_INTERFACE(CockpitWebFilter, cockpit_web_filter, COCKPIT, WEB_FILTER, GObject)
+
+struct _CockpitWebFilterInterface {
+ GTypeInterface parent_iface;
+
+ void (* push) (CockpitWebFilter *filter,
+ GBytes *block,
+ void (* function) (gpointer, GBytes *),
+ gpointer data);
+};
+
+void cockpit_web_filter_push (CockpitWebFilter *filter,
+ GBytes *queue,
+ void (* function) (gpointer, GBytes *),
+ gpointer data);
+
+G_END_DECLS
+
+#endif /* COCKPIT_WEB_FILTER_H__ */
diff --git a/src/common/cockpitwebinject.c b/src/common/cockpitwebinject.c
new file mode 100644
index 0000000..3ef3d3c
--- /dev/null
+++ b/src/common/cockpitwebinject.c
@@ -0,0 +1,224 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitwebinject.h"
+
+#include <string.h>
+
+/**
+ * CockpitWebInject
+ *
+ * This is a CockpitWebFilter which looks for a marker data
+ * and inject additional data after that point. The data is
+ * not injected more than the specified number of times.
+ */
+struct _CockpitWebInject {
+ GObject parent;
+ /* partial_matches stores the lengths of partial matches, size is (marker length) - 1
+ * e.g. "ABA" with marker "ABAC" will result in [TRUE, FALSE, TRUE]
+ */
+ GArray *partial_matches;
+ GBytes *marker;
+ GBytes *inject;
+
+ guint maximum;
+ guint injected;
+};
+
+static void cockpit_web_filter_inject_iface (CockpitWebFilterInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CockpitWebInject, cockpit_web_inject, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (COCKPIT_TYPE_WEB_FILTER, cockpit_web_filter_inject_iface)
+)
+
+static void
+cockpit_web_inject_init (CockpitWebInject *self)
+{
+
+}
+
+static void
+cockpit_web_inject_finalize (GObject *object)
+{
+ CockpitWebInject *self = COCKPIT_WEB_INJECT (object);
+
+ if (self->partial_matches)
+ g_array_unref (self->partial_matches);
+ if (self->marker)
+ g_bytes_unref (self->marker);
+ if (self->inject)
+ g_bytes_unref (self->inject);
+
+ G_OBJECT_CLASS (cockpit_web_inject_parent_class)->finalize (object);
+}
+
+static void
+cockpit_web_inject_class_init (CockpitWebInjectClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->finalize = cockpit_web_inject_finalize;
+}
+
+static void
+cockpit_web_inject_push (CockpitWebFilter *filter,
+ GBytes *block,
+ void (* function) (gpointer, GBytes *),
+ gpointer func_data)
+{
+ CockpitWebInject *self = (CockpitWebInject *)filter;
+ const gchar *mark, *data, *pos = NULL;
+ gsize mark_len, data_len, at;
+ GBytes *bytes;
+ gsize written;
+ gint partial_len, remaining_len;
+
+ mark = g_bytes_get_data (self->marker, &mark_len);
+ data = g_bytes_get_data (block, &data_len);
+
+ if (data_len == 0)
+ return;
+
+ written = at = 0;
+
+ /* look at our partial matches first
+ * longest partial matches have precedence (they either get longer or don't pan out)
+ * only do this if we haven't reached the maximum yet
+ */
+ if (self->injected < self->maximum)
+ {
+ for (partial_len = self->partial_matches->len - 1; partial_len >= 0; --partial_len)
+ {
+ if (g_array_index (self->partial_matches, gboolean, partial_len))
+ {
+ /* our match can only grow longer */
+ g_array_index (self->partial_matches, gboolean, partial_len) = FALSE;
+ remaining_len = mark_len - partial_len;
+ /* our current block might be too short */
+ if (remaining_len > data_len)
+ {
+ if (memcmp (mark + partial_len, data, data_len) == 0)
+ g_array_index (self->partial_matches, gboolean, partial_len + data_len) = TRUE;
+ }
+ else if (memcmp (mark + partial_len, data, remaining_len) == 0)
+ {
+ /* we have a match */
+ at = remaining_len;
+ pos = data + at;
+ /* reset partials */
+ g_array_set_size(self->partial_matches, 0);
+ g_array_set_size(self->partial_matches, mark_len);
+ break;
+ }
+ }
+ }
+ }
+
+ /* keep searching until we have found the maximum number of allowed matches or reached the end */
+ for(;;)
+ {
+ if (at != written)
+ {
+ bytes = g_bytes_new_from_bytes (block, written, at - written);
+ function (func_data, bytes);
+ g_bytes_unref (bytes);
+ written = at;
+
+ /* did we have a match? */
+ if (pos == (data + at) && self->injected < self->maximum)
+ {
+ function (func_data, self->inject);
+ self->injected++;
+ }
+ }
+
+ if (at >= data_len)
+ break;
+
+ /* if there are enough chars left, try to find a complete match */
+ if (self->injected < self->maximum &&
+ data_len >= (at + mark_len) &&
+ (pos = memmem (data + at, data_len - at, mark, mark_len)))
+ {
+ /* we found a match, but we want to write out the mark also before we inject */
+ pos += mark_len;
+ at = pos - data;
+ }
+ else
+ {
+ /* nothing found, forward */
+ at = data_len;
+ pos = NULL;
+ }
+ }
+
+ /* if we haven't reached our max number of injections, look for partial matches at the end */
+ partial_len = mark_len - 1;
+ if (partial_len > data_len)
+ partial_len = data_len;
+ while (partial_len > 0)
+ {
+ if (memcmp (mark, data + data_len - partial_len, partial_len) == 0)
+ g_array_index (self->partial_matches, gboolean, partial_len) = TRUE;
+ partial_len--;
+ }
+}
+
+static void
+cockpit_web_filter_inject_iface (CockpitWebFilterInterface *iface)
+{
+ iface->push = cockpit_web_inject_push;
+}
+
+/**
+ * cockpit_web_filter_new:
+ * @marker: marker to search for
+ * @inject: bytes to inject after marker
+ * @count: number of times to inject
+ *
+ * Create a new CockpitWebFilter which injects @inject bytes
+ * after the @marker. It injects the data once.
+ *
+ * Returns: A new CockpitWebFilter
+ */
+CockpitWebFilter *
+cockpit_web_inject_new (const gchar *marker,
+ GBytes *inject,
+ guint count)
+{
+ CockpitWebInject *self;
+ gsize len;
+
+ g_return_val_if_fail (marker != NULL, NULL);
+ g_return_val_if_fail (inject != NULL, NULL);
+
+ len = strlen (marker);
+ g_return_val_if_fail (len > 0, NULL);
+
+ self = g_object_new (COCKPIT_TYPE_WEB_INJECT, NULL);
+ self->marker = g_bytes_new (marker, len);
+ self->partial_matches = g_array_sized_new(FALSE, TRUE, sizeof(gboolean), len);
+ g_array_set_size(self->partial_matches, len);
+ self->inject = g_bytes_ref (inject);
+ self->maximum = count;
+
+ return COCKPIT_WEB_FILTER (self);
+}
diff --git a/src/common/cockpitwebinject.h b/src/common/cockpitwebinject.h
new file mode 100644
index 0000000..a03e742
--- /dev/null
+++ b/src/common/cockpitwebinject.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_WEB_INJECT_H__
+#define COCKPIT_WEB_INJECT_H__
+
+#include "common/cockpitwebfilter.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_WEB_INJECT (cockpit_web_inject_get_type ())
+G_DECLARE_FINAL_TYPE(CockpitWebInject, cockpit_web_inject, COCKPIT, WEB_INJECT, GObject)
+
+CockpitWebFilter * cockpit_web_inject_new (const gchar *marker,
+ GBytes *inject,
+ guint count);
+
+G_END_DECLS
+
+#endif /* COCKPIT_WEB_INJECT_H__ */
diff --git a/src/common/cockpitwebrequest-private.h b/src/common/cockpitwebrequest-private.h
new file mode 100644
index 0000000..305f66e
--- /dev/null
+++ b/src/common/cockpitwebrequest-private.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "cockpitwebserver.h"
+
+struct _CockpitWebRequest {
+ int state;
+ GIOStream *io;
+ GByteArray *buffer;
+ gint delayed_reply;
+ CockpitWebServer *web_server;
+ gboolean eof_okay;
+ GSource *source;
+ GSource *timeout;
+ gboolean check_tls_redirect;
+
+ GHashTable *headers;
+ const gchar *original_path;
+ const gchar *path;
+ const gchar *host;
+ const gchar *query;
+ const gchar *method;
+};
+
+#define WebRequest(...) (&(CockpitWebRequest) {__VA_ARGS__})
diff --git a/src/common/cockpitwebresponse.c b/src/common/cockpitwebresponse.c
new file mode 100644
index 0000000..04aab57
--- /dev/null
+++ b/src/common/cockpitwebresponse.c
@@ -0,0 +1,1950 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+/* This gets logged as part of the (more verbose) protocol logging */
+#ifdef G_LOG_DOMAIN
+#undef G_LOG_DOMAIN
+#endif
+#define G_LOG_DOMAIN "cockpit-protocol"
+
+#include "cockpitwebresponse.h"
+#include "cockpitwebfilter.h"
+
+#include "common/cockpitconf.h"
+#include "common/cockpiterror.h"
+#include "common/cockpitflow.h"
+#include "common/cockpitlocale.h"
+#include "common/cockpittemplate.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ * CockpitWebResponse:
+ *
+ * A response sent back to an HTTP client. You can use the high level one
+ * shot APIs, like cockpit_web_response_content() and
+ * cockpit_web_response_error() * or low level builder APIs:
+ *
+ * cockpit_web_response_headers() send the headers
+ * cockpit_web_response_queue() send a block of data.
+ * cockpit_web_response_complete() finish.
+ */
+
+struct _CockpitWebResponse {
+ GObject parent;
+ GIOStream *io;
+ const gchar *logname;
+ const gchar *path;
+ gchar *full_path;
+ gchar *url_root;
+ gchar *method;
+ gchar *origin;
+
+ gchar *protocol;
+ CockpitCacheType cache_type;
+
+ /* The output queue */
+ GPollableOutputStream *out;
+ GQueue *queue;
+ gsize out_queued;
+ gsize out_queueable;
+ gsize partial_offset;
+ GSource *source;
+
+ /* Status flags */
+ guint count;
+ gboolean complete;
+ gboolean failed;
+ gboolean done;
+ gboolean chunked;
+ gboolean keep_alive;
+
+ GList *filters;
+};
+
+/* A megabyte is when we start to consider queue full enough */
+#define QUEUE_PRESSURE 1024UL * 1024UL
+
+static guint signal__done;
+
+static void cockpit_web_response_flow_iface_init (CockpitFlowInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CockpitWebResponse, cockpit_web_response, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (COCKPIT_TYPE_FLOW, cockpit_web_response_flow_iface_init));
+
+static void
+cockpit_web_response_init (CockpitWebResponse *self)
+{
+ self->queue = g_queue_new ();
+ self->out_queueable = G_MAXSIZE;
+ self->cache_type = COCKPIT_WEB_RESPONSE_CACHE_UNSET;
+}
+
+static void
+cockpit_web_response_done (CockpitWebResponse *self)
+{
+ gboolean reusable = FALSE;
+
+ g_object_ref (self);
+
+ g_assert (!self->done);
+ self->done = TRUE;
+
+ if (self->source)
+ {
+ g_source_destroy (self->source);
+ g_source_unref (self->source);
+ self->source = NULL;
+ }
+
+ if (self->complete)
+ {
+ reusable = !self->failed && self->keep_alive;
+ g_object_unref (self);
+ }
+ else if (!self->failed)
+ {
+ g_critical ("A CockpitWebResponse was freed without being completed properly. "
+ "This is a programming error.");
+ }
+
+ g_signal_emit (self, signal__done, 0, reusable);
+
+ g_object_unref (self->io);
+ self->io = NULL;
+ self->out = NULL;
+
+ g_object_unref (self);
+}
+
+static void
+cockpit_web_response_dispose (GObject *object)
+{
+ CockpitWebResponse *self = COCKPIT_WEB_RESPONSE (object);
+
+ if (!self->done)
+ cockpit_web_response_done (self);
+ g_list_free_full (self->filters, g_object_unref);
+ self->filters = NULL;
+
+ G_OBJECT_CLASS (cockpit_web_response_parent_class)->dispose (object);
+}
+
+static void
+cockpit_web_response_finalize (GObject *object)
+{
+ CockpitWebResponse *self = COCKPIT_WEB_RESPONSE (object);
+
+ g_free (self->protocol);
+ g_free (self->full_path);
+ g_free (self->url_root);
+ g_free (self->method);
+ g_free (self->origin);
+ g_assert (self->io == NULL);
+ g_assert (self->out == NULL);
+ g_queue_free_full (self->queue, (GDestroyNotify)g_bytes_unref);
+ self->out_queued = 0;
+
+ G_OBJECT_CLASS (cockpit_web_response_parent_class)->finalize (object);
+}
+
+static void
+cockpit_web_response_class_init (CockpitWebResponseClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->dispose = cockpit_web_response_dispose;
+ gobject_class->finalize = cockpit_web_response_finalize;
+
+ signal__done = g_signal_new ("done", COCKPIT_TYPE_WEB_RESPONSE,
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+}
+
+/**
+ * cockpit_web_response_new:
+ * @io: the stream to send on
+ * @path: the path resource or NULL
+ * @in_headers: input headers or NULL
+ * @flags: in #COCKPIT_WEB_RESPONSE_FOR_TLS_PROXY mode, the origin is assumed to
+ * be https://<host> even for a non-HTTPS connection.
+ *
+ * Create a new web response.
+ *
+ * The returned reference belongs to the caller. Additionally
+ * once cockpit_web_response_complete() is called, an additional
+ * reference is held until the response is sent and flushed.
+ *
+ * Returns: (transfer full): the new response, unref when done with it
+ */
+CockpitWebResponse *
+cockpit_web_response_new (GIOStream *io,
+ const gchar *original_path,
+ const gchar *path,
+ GHashTable *in_headers,
+ const gchar *method,
+ const gchar *protocol)
+{
+ CockpitWebResponse *self;
+ GOutputStream *out;
+ const gchar *connection;
+ const gchar *host = NULL;
+ gint offset;
+
+ /* Trying to be a somewhat performant here, avoiding properties */
+ self = g_object_new (COCKPIT_TYPE_WEB_RESPONSE, NULL);
+ self->io = g_object_ref (io);
+
+ out = g_io_stream_get_output_stream (io);
+ if (G_IS_POLLABLE_OUTPUT_STREAM (out))
+ {
+ self->out = (GPollableOutputStream *)out;
+ }
+ else if (out)
+ {
+ g_critical ("Cannot send web response over non-pollable output stream: %s",
+ G_OBJECT_TYPE_NAME (out));
+ }
+ else
+ {
+ g_critical ("Cannot send web response: no output stream available");
+ }
+
+ self->url_root = NULL;
+ self->full_path = g_strdup (path);
+ self->path = self->full_path;
+
+ g_assert (method != NULL);
+ self->method = g_strdup (method);
+
+ if (path && original_path)
+ {
+ offset = strlen (original_path) - strlen (path);
+ if (offset > 0 && g_strcmp0 (original_path + offset, path) == 0)
+ self->url_root = g_strndup (original_path, offset);
+ }
+
+ if (self->path)
+ self->logname = self->path;
+ else
+ self->logname = "response";
+
+ self->keep_alive = TRUE;
+ if (in_headers)
+ {
+ connection = g_hash_table_lookup (in_headers, "Connection");
+ if (connection)
+ self->keep_alive = g_str_equal (connection, "keep-alive");
+ host = g_hash_table_lookup (in_headers, "Host");
+ }
+
+ self->protocol = g_strdup (protocol ?: "http");
+ if (host)
+ self->origin = g_strdup_printf ("%s://%s", self->protocol, host);
+
+ return self;
+}
+
+/**
+ * cockpit_web_response_get_path:
+ * @self: the response
+ *
+ * Returns: the resource path for response
+ */
+const gchar *
+cockpit_web_response_get_path (CockpitWebResponse *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), NULL);
+ return self->path;
+}
+
+/**
+ * cockpit_web_response_get_url_root:
+ * @self: the response
+ *
+ * Returns: The url root portion of the original path that was removed
+ */
+const gchar *
+cockpit_web_response_get_url_root (CockpitWebResponse *self) {
+ return self->url_root;
+}
+
+/**
+ * cockpit_web_response_get_stream:
+ * @self: the response
+ *
+ * Returns: the stream we're sending on
+ */
+GIOStream *
+cockpit_web_response_get_stream (CockpitWebResponse *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), NULL);
+ return self->io;
+}
+
+#if !GLIB_CHECK_VERSION(2,43,2)
+#define G_IO_ERROR_CONNECTION_CLOSED G_IO_ERROR_BROKEN_PIPE
+#endif
+
+gboolean
+cockpit_web_should_suppress_output_error (const gchar *logname,
+ GError *error)
+{
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_CLOSED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_BROKEN_PIPE))
+ {
+ g_debug ("%s: output error: %s", logname, error->message);
+ return TRUE;
+ }
+
+#if !GLIB_CHECK_VERSION(2,43,2)
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_FAILED) &&
+ strstr (error->message, g_strerror (ECONNRESET)))
+ {
+ g_debug ("%s: output error: %s", logname, error->message);
+ return TRUE;
+ }
+#endif
+
+ return FALSE;
+}
+
+static void
+on_output_flushed (GObject *stream,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CockpitWebResponse *self = COCKPIT_WEB_RESPONSE (user_data);
+ GOutputStream *output = G_OUTPUT_STREAM (stream);
+ GError *error = NULL;
+
+ if (g_output_stream_flush_finish (output, result, &error))
+ {
+ g_debug ("%s: flushed output", self->logname);
+ }
+ else
+ {
+ if (!cockpit_web_should_suppress_output_error (self->logname, error))
+ g_message ("%s: couldn't flush web output: %s", self->logname, error->message);
+ self->failed = TRUE;
+ g_error_free (error);
+ }
+
+ cockpit_web_response_done (self);
+ g_object_unref (self);
+}
+
+static gboolean
+on_response_output (GObject *pollable,
+ gpointer user_data)
+{
+ CockpitWebResponse *self = user_data;
+ GError *error = NULL;
+ const guint8 *data;
+ GBytes *block;
+ gssize count;
+ gsize before, size, len;
+
+ block = g_queue_peek_head (self->queue);
+ if (block)
+ {
+ data = g_bytes_get_data (block, &len);
+ g_assert (len == 0 || self->partial_offset < len);
+ data += self->partial_offset;
+ len -= self->partial_offset;
+
+ before = self->out_queued;
+
+ if (len > 0)
+ {
+ count = g_pollable_output_stream_write_nonblocking (self->out, data, len,
+ NULL, &error);
+ }
+ else
+ {
+ count = 0;
+ }
+
+ if (count < 0)
+ {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+ {
+ g_error_free (error);
+ return TRUE;
+ }
+
+ if (!cockpit_web_should_suppress_output_error (self->logname, error))
+ g_message ("%s: couldn't write web output: %s", self->logname, error->message);
+
+ self->failed = TRUE;
+ cockpit_web_response_done (self);
+
+ g_error_free (error);
+ return FALSE;
+ }
+
+ if (count == len)
+ {
+ g_debug ("%s: sent %d bytes", self->logname, (int)len);
+ self->partial_offset = 0;
+ block = g_queue_pop_head (self->queue);
+ size = g_bytes_get_size (block);
+ g_assert (size <= self->out_queued);
+ self->out_queued -= size;
+ g_bytes_unref (block);
+ }
+ else
+ {
+ g_debug ("%s: sent %d partial", self->logname, (int)count);
+ g_assert (count < len);
+ self->partial_offset += count;
+ }
+
+ /*
+ * If we're controlling another flow, turn it on again when our output
+ * buffer size becomes less than the low mark.
+ */
+ if (before >= QUEUE_PRESSURE && self->out_queued < QUEUE_PRESSURE)
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), FALSE);
+
+ return TRUE;
+ }
+ else
+ {
+ g_source_destroy (self->source);
+ g_source_unref (self->source);
+ self->source = NULL;
+
+ if (self->complete)
+ {
+ g_debug ("%s: complete flushing output", self->logname);
+ g_output_stream_flush_async (G_OUTPUT_STREAM (self->out), G_PRIORITY_DEFAULT,
+ NULL, on_output_flushed, g_object_ref (self));
+ }
+
+ return FALSE;
+ }
+}
+
+static void
+queue_bytes (CockpitWebResponse *self,
+ GBytes *block)
+{
+ gsize size, before;
+
+ size = g_bytes_get_size (block);
+ before = self->out_queued;
+ g_return_if_fail (G_MAXSIZE - size > self->out_queued);
+ self->out_queued += size;
+
+ g_queue_push_tail (self->queue, g_bytes_ref (block));
+
+ self->count++;
+
+ if (!self->source)
+ {
+ self->source = g_pollable_output_stream_create_source (self->out, NULL);
+ g_source_set_callback (self->source, (GSourceFunc)on_response_output, self, NULL);
+ g_source_attach (self->source, NULL);
+ }
+
+ if (before < QUEUE_PRESSURE && self->out_queued >= QUEUE_PRESSURE)
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), TRUE);
+}
+
+static void
+queue_block (CockpitWebResponse *self,
+ GBytes *block)
+{
+ gsize length = g_bytes_get_size (block);
+ GBytes *bytes;
+ gchar *data;
+
+ /*
+ * We cannot queue chunks of length zero. Besides being silly, this
+ * messes with chunked encoding. The 0 length block means end of
+ * response.
+ */
+ if (length == 0)
+ return;
+
+ if (self->out_queueable < length)
+ {
+ g_critical ("Too much data queuing in HTTP response. This is a programmer error.");
+ return;
+ }
+
+ self->out_queueable -= length;
+ g_debug ("%s: queued %d bytes", self->logname, (int)length);
+
+ if (!self->chunked)
+ {
+ queue_bytes (self, block);
+ }
+ else
+ {
+ /* Required for chunked transfer encoding. */
+ data = g_strdup_printf ("%x\r\n", (unsigned int)length);
+ bytes = g_bytes_new_take (data, strlen (data));
+ queue_bytes (self, bytes);
+ g_bytes_unref (bytes);
+
+ queue_bytes (self, block);
+
+ bytes = g_bytes_new_static ("\r\n", 2);
+ queue_bytes (self, bytes);
+ g_bytes_unref (bytes);
+ }
+}
+
+typedef struct {
+ CockpitWebResponse *response;
+ GList *filters;
+} QueueStep;
+
+static void
+queue_filter (gpointer data,
+ GBytes *bytes)
+{
+ QueueStep *qs = data;
+ QueueStep qn = { .response = qs->response };
+
+ g_return_if_fail (bytes != NULL);
+
+ if (qs->filters)
+ {
+ qn.filters = qs->filters->next;
+ cockpit_web_filter_push (qs->filters->data, bytes, queue_filter, &qn);
+ }
+ else
+ {
+ queue_block (qs->response, bytes);
+ }
+}
+
+/**
+ * cockpit_web_response_queue:
+ * @self: the response
+ * @block: the block of data to queue
+ *
+ * Queue a single block of data on the response. Will be sent
+ * during the main loop.
+ *
+ * See cockpit_web_response_content() for a simple way to
+ * avoid queueing individual blocks.
+ *
+ * If this function returns %FALSE, then the response has failed
+ * or has been completed elsewhere. The block was ignored and
+ * queuing more blocks doesn't makes sense.
+ *
+ * After done queuing all your blocks call
+ * cockpit_web_response_complete().
+*
+ * Returns: Whether queuing more blocks makes sense
+ */
+gboolean
+cockpit_web_response_queue (CockpitWebResponse *self,
+ GBytes *block)
+{
+ QueueStep qn = { .response = self };
+
+ g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), FALSE);
+ g_return_val_if_fail (block != NULL, FALSE);
+ g_return_val_if_fail (self->complete == FALSE, FALSE);
+
+ if (self->failed)
+ {
+ g_debug ("%s: ignoring queued block after failure", self->logname);
+ return FALSE;
+ }
+
+ if (g_str_equal (self->method, "HEAD"))
+ {
+ g_debug ("%s: ignoring queued block for method HEAD", self->logname);
+ return TRUE;
+ }
+
+ qn.filters = self->filters;
+ queue_filter (&qn, block);
+ return TRUE;
+}
+
+/**
+ * cockpit_web_response_complete:
+ * @self: the response
+ *
+ * See cockpit_web_response_content() for easy to use stuff.
+ *
+ * Tell the response that all the data has been queued.
+ * The response will hold a reference to itself until the
+ * data is actually sent, so you can unref it.
+ */
+void
+cockpit_web_response_complete (CockpitWebResponse *self)
+{
+ GBytes *bytes;
+
+ g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self));
+ g_return_if_fail (self->complete == FALSE);
+
+ if (self->failed)
+ return;
+
+ /* Hold a reference until cockpit_web_response_done() */
+ g_object_ref (self);
+ self->complete = TRUE;
+
+ if (self->chunked)
+ {
+ bytes = g_bytes_new_static ("0\r\n\r\n", 5);
+ queue_bytes (self, bytes);
+ g_bytes_unref (bytes);
+ }
+
+ if (self->source)
+ {
+ g_debug ("%s: queueing complete", self->logname);
+ }
+ else
+ {
+ g_debug ("%s: complete closing io", self->logname);
+ g_output_stream_flush_async (G_OUTPUT_STREAM (self->out), G_PRIORITY_DEFAULT,
+ NULL, on_output_flushed, g_object_ref (self));
+ }
+}
+
+/**
+ * cockpit_web_response_abort:
+ * @self: the response
+ *
+ * This function is used when streaming content, and at
+ * some point we can't provide the remainder of the content
+ *
+ * This completes the response and terminates the connection.
+ */
+void
+cockpit_web_response_abort (CockpitWebResponse *self)
+{
+ g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self));
+ g_return_if_fail (self->complete == FALSE);
+
+ if (self->failed)
+ return;
+
+ /* Hold a reference until cockpit_web_response_done() */
+ g_object_ref (self);
+
+ self->complete = TRUE;
+ self->failed = TRUE;
+
+ g_debug ("%s: aborted", self->logname);
+ cockpit_web_response_done (self);
+}
+
+/**
+ * CockpitWebResponding:
+ * @COCKPIT_WEB_RESPONSE_READY: nothing queued or sent yet
+ * @COCKPIT_WEB_RESPONSE_QUEUING: started and still queuing data on response
+ * @COCKPIT_WEB_RESPONSE_COMPLETE: all data is queued or aborted
+ * @COCKPIT_WEB_RESPONSE_SENT: data is completely sent
+ *
+ * Various states of the web response.
+ */
+
+/**
+ * cockpit_web_response_get_state:
+ * @self: the web response
+ *
+ * Return the state of the web response.
+ */
+CockpitWebResponding
+cockpit_web_response_get_state (CockpitWebResponse *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), 0);
+
+ if (self->done)
+ return COCKPIT_WEB_RESPONSE_SENT;
+ else if (self->complete)
+ return COCKPIT_WEB_RESPONSE_COMPLETE;
+ else if (self->count == 0)
+ return COCKPIT_WEB_RESPONSE_READY;
+ else
+ return COCKPIT_WEB_RESPONSE_QUEUING;
+}
+
+gboolean
+cockpit_web_response_is_simple_token (const gchar *string)
+{
+ string += strcspn (string, " \t\r\n\v");
+ return string[0] == '\0';
+}
+
+gboolean
+cockpit_web_response_is_header_value (const gchar *string)
+{
+ string += strcspn (string, "\r\n\v");
+ return string[0] == '\0';
+}
+
+enum {
+ HEADER_CONTENT_TYPE = 1 << 0,
+ HEADER_CONTENT_ENCODING = 1 << 1,
+ HEADER_VARY = 1 << 2,
+ HEADER_CACHE_CONTROL = 1 << 3,
+ HEADER_DNS_PREFETCH_CONTROL = 1 << 4,
+ HEADER_REFERRER_POLICY = 1 << 5,
+ HEADER_CONTENT_TYPE_OPTIONS = 1 << 6,
+ HEADER_CROSS_ORIGIN_RESOURCE_POLICY = 1 << 7,
+ HEADER_X_FRAME_OPTIONS = 1 << 8,
+};
+
+static GString *
+begin_headers (CockpitWebResponse *response,
+ guint status,
+ const gchar *reason)
+{
+ GString *string;
+
+ string = g_string_sized_new (1024);
+ g_string_printf (string, "HTTP/1.1 %d %s\r\n", status, reason);
+
+ return string;
+}
+
+static guint
+append_header (GString *string,
+ const gchar *name,
+ const gchar *value)
+{
+ if (value)
+ {
+ g_return_val_if_fail (cockpit_web_response_is_simple_token (name), 0);
+ g_return_val_if_fail (cockpit_web_response_is_header_value (value), 0);
+ g_string_append_printf (string, "%s: %s\r\n", name, value);
+ }
+ if (g_ascii_strcasecmp ("Content-Type", name) == 0)
+ return HEADER_CONTENT_TYPE;
+ if (g_ascii_strcasecmp ("Cache-Control", name) == 0)
+ return HEADER_CACHE_CONTROL;
+ if (g_ascii_strcasecmp ("Vary", name) == 0)
+ return HEADER_VARY;
+ if (g_ascii_strcasecmp ("Content-Encoding", name) == 0)
+ return HEADER_CONTENT_ENCODING;
+ if (g_ascii_strcasecmp ("X-DNS-Prefetch-Control", name) == 0)
+ return HEADER_DNS_PREFETCH_CONTROL;
+ if (g_ascii_strcasecmp ("Referrer-Policy", name) == 0)
+ return HEADER_REFERRER_POLICY;
+ if (g_ascii_strcasecmp ("X-Content-Type-Options", name) == 0)
+ return HEADER_CONTENT_TYPE_OPTIONS;
+ if (g_ascii_strcasecmp ("Cross-Origin-Resource-Policy", name) == 0)
+ return HEADER_CROSS_ORIGIN_RESOURCE_POLICY;
+ if (g_ascii_strcasecmp ("X-Frame-Options", name) == 0)
+ return HEADER_X_FRAME_OPTIONS;
+ if (g_ascii_strcasecmp ("Content-Length", name) == 0 ||
+ g_ascii_strcasecmp ("Transfer-Encoding", name) == 0 ||
+ g_ascii_strcasecmp ("Connection", name) == 0)
+ {
+ g_critical ("Don't set %s header manually. This is a programmer error.", name);
+ }
+ return 0;
+}
+
+static guint
+append_table (GString *string,
+ GHashTable *headers)
+{
+ GHashTableIter iter;
+ gpointer key;
+ gpointer value;
+ guint seen = 0;
+
+ if (headers)
+ {
+ g_hash_table_iter_init (&iter, headers);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ seen |= append_header (string, key, value);
+ }
+
+ return seen;
+}
+
+static guint
+append_va (GString *string,
+ va_list va)
+{
+ const gchar *name;
+ const gchar *value;
+ guint seen = 0;
+
+ for (;;)
+ {
+ name = va_arg (va, const gchar *);
+ if (!name)
+ break;
+ value = va_arg (va, const gchar *);
+ seen |= append_header (string, name, value);
+ }
+
+ return seen;
+}
+
+static GBytes *
+finish_headers (CockpitWebResponse *self,
+ GString *string,
+ gssize length,
+ gint status,
+ guint seen)
+{
+ const gchar *content_type;
+
+ /* Automatically figure out content type */
+ if ((seen & HEADER_CONTENT_TYPE) == 0 &&
+ self->full_path != NULL && status >= 200 && status <= 299)
+ {
+ content_type = cockpit_web_response_content_type (self->full_path);
+ if (content_type)
+ g_string_append_printf (string, "Content-Type: %s\r\n", content_type);
+ }
+
+ if (status != 304)
+ {
+ if (length < 0 || seen & HEADER_CONTENT_ENCODING || self->filters)
+ {
+ self->chunked = TRUE;
+ g_string_append_printf (string, "Transfer-Encoding: chunked\r\n");
+ }
+ else
+ {
+ self->chunked = FALSE;
+ g_string_append_printf (string, "Content-Length: %" G_GSSIZE_FORMAT "\r\n", length);
+ self->out_queueable = length;
+ }
+ }
+
+ if ((seen & HEADER_CACHE_CONTROL) == 0 && status >= 200 && status <= 299)
+ {
+ if (self->cache_type == COCKPIT_WEB_RESPONSE_NO_CACHE)
+ g_string_append (string, "Cache-Control: no-cache, no-store\r\n");
+ else if (self->cache_type == COCKPIT_WEB_RESPONSE_CACHE)
+ g_string_append (string, "Cache-Control: max-age=86400, private\r\n");
+ }
+
+ if ((seen & HEADER_VARY) == 0 && status >= 200 && status <= 299 &&
+ self->cache_type == COCKPIT_WEB_RESPONSE_CACHE)
+ {
+ g_string_append (string, "Vary: Cookie\r\n");
+ }
+
+ if (!self->keep_alive)
+ g_string_append (string, "Connection: close\r\n");
+
+ /* Some blanket security headers */
+ if ((seen & HEADER_DNS_PREFETCH_CONTROL) == 0)
+ g_string_append (string, "X-DNS-Prefetch-Control: off\r\n");
+ if ((seen & HEADER_REFERRER_POLICY) == 0)
+ g_string_append (string, "Referrer-Policy: no-referrer\r\n");
+ if ((seen & HEADER_CONTENT_TYPE_OPTIONS) == 0)
+ g_string_append (string, "X-Content-Type-Options: nosniff\r\n");
+ /* Be very strict here -- there is no reason that external web sites should
+ * be able to read any resource. This does *not* affect embedding with <iframe> */
+ if ((seen & HEADER_CROSS_ORIGIN_RESOURCE_POLICY) == 0)
+ g_string_append (string, "Cross-Origin-Resource-Policy: same-origin\r\n");
+ /* This is the counterpart for iframe embedding, line of defence against clickjacking */
+ if ((seen & HEADER_X_FRAME_OPTIONS) == 0)
+ g_string_append (string, "X-Frame-Options: sameorigin\r\n");
+
+ g_string_append (string, "\r\n");
+ return g_string_free_to_bytes (string);
+}
+
+/**
+ * cockpit_web_response_set_cache_type:
+ * @self: the response
+ * @cache_type: Ensures the appropriate cache headers are returned for
+ the given cache type.
+ */
+void
+cockpit_web_response_set_cache_type (CockpitWebResponse *self,
+ CockpitCacheType cache_type)
+{
+ self->cache_type = cache_type;
+}
+
+/**
+ * cockpit_web_response_headers:
+ * @self: the response
+ * @status: the HTTP status code
+ * @reason: the HTTP reason
+ * @length: the combined length of data blocks to follow, or -1
+ *
+ * See cockpit_web_response_content() for an easy to use function.
+ *
+ * Queue the headers of the response. No data blocks must yet be
+ * queued on the response.
+ *
+ * Specify header name/value pairs in the var args, and end with
+ * a NULL name. If value is NULL, then that header won't be sent.
+ *
+ * Don't specify Content-Length or Connection headers.
+ *
+ * If @length is zero or greater, then it must represent the
+ * number of queued blocks to follow.
+ */
+void
+cockpit_web_response_headers (CockpitWebResponse *self,
+ guint status,
+ const gchar *reason,
+ gssize length,
+ ...)
+{
+ GString *string;
+ GBytes *block;
+ va_list va;
+
+ g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self));
+
+ if (self->count > 0)
+ {
+ g_critical ("Headers should be sent first. This is a programmer error.");
+ return;
+ }
+
+ string = begin_headers (self, status, reason);
+
+ va_start (va, length);
+ block = finish_headers (self, string, length, status,
+ append_va (string, va));
+ va_end (va);
+
+ queue_bytes (self, block);
+ g_bytes_unref (block);
+}
+
+/**
+ * cockpit_web_response_headers:
+ * @self: the response
+ * @status: the HTTP status code
+ * @reason: the HTTP reason
+ * @length: the combined length of data blocks to follow, or -1
+ * @headers: headers to include or NULL
+ *
+ * See cockpit_web_response_content() for an easy to use function.
+ *
+ * Queue the headers of the response. No data blocks must yet be
+ * queued on the response.
+ *
+ * Don't put Content-Length or Connection in @headers.
+ *
+ * If @length is zero or greater, then it must represent the
+ * number of queued blocks to follow.
+ */
+void
+cockpit_web_response_headers_full (CockpitWebResponse *self,
+ guint status,
+ const gchar *reason,
+ gssize length,
+ GHashTable *headers)
+{
+ GString *string;
+ GBytes *block;
+
+ g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self));
+
+ if (self->count > 0)
+ {
+ g_critical ("Headers should be sent first. This is a programmer error.");
+ return;
+ }
+
+ string = begin_headers (self, status, reason);
+
+ block = finish_headers (self, string, length, status,
+ append_table (string, headers));
+
+ queue_bytes (self, block);
+ g_bytes_unref (block);
+}
+
+/**
+ * cockpit_web_response_content:
+ * @self: the response
+ * @headers: headers to include or NULL
+ * @block: first block to send
+ *
+ * This is a simple way to send an HTTP response as a single
+ * call. The response will be complete after this call, and will
+ * send in the main-loop.
+ *
+ * The var args are additional GBytes* blocks to send, followed by
+ * a trailing NULL.
+ *
+ * Don't include Content-Length or Connection in @headers.
+ *
+ * This calls cockpit_web_response_headers_full(),
+ * cockpit_web_response_queue() and cockpit_web_response_complete()
+ * internally.
+ */
+void
+cockpit_web_response_content (CockpitWebResponse *self,
+ GHashTable *headers,
+ GBytes *block,
+ ...)
+{
+ GBytes *first;
+ gsize length = 0;
+ va_list va;
+ va_list va2;
+
+ g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self));
+
+ first = block;
+ va_start (va, block);
+ va_copy (va2, va);
+
+ while (block)
+ {
+ length += g_bytes_get_size (block);
+ block = va_arg (va, GBytes *);
+ }
+ va_end (va);
+
+ cockpit_web_response_headers_full (self, 200, "OK", length, headers);
+
+ block = first;
+ for (;;)
+ {
+ if (!block)
+ {
+ cockpit_web_response_complete (self);
+ break;
+ }
+ if (!cockpit_web_response_queue (self, block))
+ break;
+ block = va_arg (va2, GBytes *);
+ }
+ va_end (va2);
+}
+
+static GBytes *
+substitute_message (const gchar *variable,
+ gpointer user_data)
+{
+ const gchar *message = user_data;
+ if (g_str_equal (variable, "message"))
+ return g_bytes_new (message, strlen (message));
+ return NULL;
+}
+
+static GBytes *
+substitute_hash_value (const gchar *variable,
+ gpointer user_data)
+{
+ GHashTable *data = user_data;
+ gchar *value = g_hash_table_lookup (data, variable);
+ if (value)
+ return g_bytes_new (value, strlen (value));
+ return g_bytes_new ("", 0);
+}
+
+/**
+ * cockpit_web_response_error:
+ * @self: the response
+ * @status: the HTTP status code
+ * @headers: headers to include or NULL
+ * @format: printf format of error message
+ *
+ * Send an error message with a basic HTML page containing
+ * the error.
+ */
+void
+cockpit_web_response_error (CockpitWebResponse *self,
+ guint code,
+ GHashTable *headers,
+ const gchar *format,
+ ...)
+{
+ va_list var_args;
+ g_autofree gchar *reason = NULL;
+ g_autofree gchar *escaped = NULL;
+ const gchar *message;
+
+ g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self));
+
+ if (format)
+ {
+ va_start (var_args, format);
+ reason = g_strdup_vprintf (format, var_args);
+ va_end (var_args);
+ message = reason;
+ }
+ else
+ {
+ switch (code)
+ {
+ case 400:
+ message = "Bad request";
+ break;
+ case 401:
+ message = "Not Authorized";
+ break;
+ case 403:
+ message = "Forbidden";
+ break;
+ case 404:
+ message = "Not Found";
+ break;
+ case 405:
+ message = "Method Not Allowed";
+ break;
+ case 413:
+ message = "Request Entity Too Large";
+ break;
+ case 502:
+ message = "Remote Page is Unavailable";
+ break;
+ case 500:
+ message = "Internal Server Error";
+ break;
+ default:
+ if (code < 100)
+ reason = g_strdup_printf ("%u Continue", code);
+ else if (code < 200)
+ reason = g_strdup_printf ("%u OK", code);
+ else if (code < 300)
+ reason = g_strdup_printf ("%u Moved", code);
+ else
+ reason = g_strdup_printf ("%u Failed", code);
+ message = reason;
+ break;
+ }
+ }
+
+ g_debug ("%s: returning error: %u %s", self->logname, code, message);
+
+ /* If sending arbitrary messages, make sure they're escaped */
+ if (reason)
+ {
+ g_strstrip (reason);
+ escaped = g_uri_escape_string (reason, " :", FALSE);
+ message = escaped;
+ }
+
+ if (headers)
+ {
+ if (!g_hash_table_lookup (headers, "Content-Type"))
+ g_hash_table_replace (headers, g_strdup ("Content-Type"), g_strdup ("text/html; charset=utf8"));
+ cockpit_web_response_headers_full (self, code, message, -1, headers);
+ }
+ else
+ {
+ cockpit_web_response_headers (self, code, message, -1, "Content-Type", "text/html; charset=utf8", NULL);
+ }
+
+ if (!g_str_equal (self->method, "HEAD"))
+ {
+ extern const char *cockpit_webresponse_fail_html_text;
+ g_autoptr(GBytes) input = g_bytes_new_static (cockpit_webresponse_fail_html_text, strlen (cockpit_webresponse_fail_html_text));
+ g_autolist(GBytes) output = cockpit_template_expand (input, "@@", "@@", substitute_message, (gpointer) message);
+
+ for (GList *l = output; l != NULL; l = g_list_next (l))
+ {
+ if (!cockpit_web_response_queue (self, l->data))
+ /* error: early exit */
+ return;
+ }
+ }
+
+ cockpit_web_response_complete (self);
+}
+
+/**
+ * cockpit_web_response_error:
+ * @self: the response
+ * @headers: headers to include or NULL
+ * @error: the error
+ *
+ * Send an error message with a basic HTML page containing
+ * the error.
+ */
+void
+cockpit_web_response_gerror (CockpitWebResponse *self,
+ GHashTable *headers,
+ GError *error)
+{
+ int code;
+
+ g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self));
+
+ if (g_error_matches (error,
+ COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED))
+ code = 401;
+ else if (g_error_matches (error,
+ COCKPIT_ERROR, COCKPIT_ERROR_PERMISSION_DENIED))
+ code = 403;
+ else if (g_error_matches (error,
+ G_IO_ERROR, G_IO_ERROR_INVALID_DATA))
+ code = 400;
+ else if (g_error_matches (error,
+ G_IO_ERROR, G_IO_ERROR_NO_SPACE))
+ code = 413;
+ else
+ code = 500;
+
+ cockpit_web_response_error (self, code, headers, "%s", error->message);
+}
+
+static gboolean
+path_has_prefix (const gchar *path,
+ const gchar *prefix)
+{
+ gsize len;
+ if (prefix == NULL)
+ return FALSE;
+ len = strlen (prefix);
+ if (len == 0)
+ return FALSE;
+ if (!g_str_has_prefix (path, prefix))
+ return FALSE;
+ if (prefix[len - 1] == '/' ||
+ path[len] == '/')
+ return TRUE;
+ return FALSE;
+}
+
+gchar **
+cockpit_web_response_resolve_roots (const gchar **input)
+{
+ GPtrArray *roots;
+ char *path;
+ gint i;
+
+ roots = g_ptr_array_new ();
+ for (i = 0; input && input[i]; i++)
+ {
+ path = realpath (input[i], NULL);
+ if (path == NULL)
+ g_debug ("couldn't resolve document root: %s: %m", input[i]);
+ else
+ g_ptr_array_add (roots, path);
+ }
+ g_ptr_array_add (roots, NULL);
+ return (gchar **)g_ptr_array_free (roots, FALSE);
+}
+
+static void
+web_response_file (CockpitWebResponse *response,
+ const gchar *escaped,
+ const gchar **roots,
+ gboolean search_gzip,
+ gboolean accept_gzip,
+ CockpitTemplateFunc template_func,
+ gpointer user_data)
+{
+ g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (response));
+
+ if (!escaped)
+ escaped = cockpit_web_response_get_path (response);
+
+ g_return_if_fail (escaped != NULL);
+
+ /* Someone is trying to escape the root directory, or access hidden files? */
+ g_autofree gchar *unescaped = g_uri_unescape_string (escaped, "/");
+ if (!unescaped || strstr (unescaped, "/.") || strstr (unescaped, "../") || strstr (unescaped, "//"))
+ {
+ g_debug ("%s: invalid path request", escaped);
+ cockpit_web_response_error (response, 404, NULL, "Not Found");
+ return;
+ }
+
+ gboolean is_gzip = FALSE;
+ g_autoptr(GMappedFile) file = NULL;
+ for (gint i = 0; roots[i]; i++)
+ {
+ const gchar *root = roots[i];
+ g_autofree gchar *path = g_build_filename (root, unescaped, NULL);
+
+ if (g_file_test (path, G_FILE_TEST_IS_DIR))
+ {
+ cockpit_web_response_error (response, 403, NULL, "Directory Listing Denied");
+ return;
+ }
+
+ /* As a double check of above behavior */
+ g_assert (path_has_prefix (path, root));
+
+ g_autoptr(GError) error = NULL;
+ file = g_mapped_file_new (path, FALSE, &error);
+
+ if (file == NULL && search_gzip &&
+ g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ {
+ g_debug ("%s: file not found in root: %s, looking for .gz", escaped, root);
+ g_clear_error (&error);
+ g_autofree gchar *old_path = g_steal_pointer (&path);
+ path = g_strconcat (old_path, ".gz", NULL);
+ file = g_mapped_file_new (path, FALSE, &error);
+ is_gzip = file != NULL;
+ }
+
+ if (file != NULL)
+ break;
+
+ if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT) ||
+ g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NAMETOOLONG))
+ {
+ g_debug ("%s: file not found in root: %s", escaped, root);
+ }
+ else if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_PERM) ||
+ g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_ACCES) ||
+ g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_ISDIR))
+ {
+ cockpit_web_response_error (response, 403, NULL, "Access denied");
+ return;
+ }
+ else
+ {
+ g_warning ("%s: %s", path, error->message);
+ cockpit_web_response_error (response, 500, NULL, "Internal server error");
+ return;
+ }
+ }
+
+ if (file == NULL)
+ {
+ cockpit_web_response_error (response, 404, NULL, "Not Found");
+ return;
+ }
+
+ g_autoptr(GBytes) body = g_mapped_file_get_bytes (file);
+
+ if (is_gzip && (!accept_gzip || template_func))
+ {
+ /* We have gzipped content, but the client won't accept it, or
+ * template expansion was requested. Decompress.
+ */
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GBytes) body_gz = g_steal_pointer (&body);
+ body = cockpit_web_response_gunzip (body_gz, &error);
+ if (body == NULL)
+ {
+ g_warning ("%s", error->message);
+ cockpit_web_response_error (response, 500, NULL, "Internal server error");
+ return;
+ }
+ is_gzip = FALSE;
+ }
+
+ GList *output;
+ gint content_length = -1;
+ if (template_func)
+ {
+ output = cockpit_template_expand (body, "${", "}", template_func, user_data);
+ }
+ else
+ {
+ output = g_list_prepend (NULL, g_bytes_ref (body));
+ content_length = g_bytes_get_size (body);
+ }
+
+ GString *string = begin_headers (response, 200, "OK");
+ guint seen = 0;
+
+ if (response->origin)
+ seen |= append_header (string, "Access-Control-Allow-Origin", response->origin);
+
+ /*
+ * The default Content-Security-Policy for .html files allows
+ * the site to have inline <script> and <style> tags. This code
+ * is only used for static resources that do not use the session.
+ */
+ if (g_str_has_suffix (unescaped, ".html"))
+ {
+ const gchar *default_policy = "default-src 'self' 'unsafe-inline';";
+ g_autofree gchar *policy = cockpit_web_response_security_policy (default_policy, response->origin);
+ seen |= append_header (string, "Content-Security-Policy", policy);
+ }
+
+ if (is_gzip)
+ seen |= append_header (string, "Content-Encoding", "gzip");
+
+ g_autoptr(GBytes) headers_block = finish_headers (response, string, content_length, 200, seen);
+ queue_bytes (response, headers_block);
+
+ GList *l;
+ for (l = output; l != NULL; l = g_list_next (l))
+ {
+ if (!cockpit_web_response_queue (response, l->data))
+ break;
+ }
+ if (l == NULL)
+ cockpit_web_response_complete (response);
+
+ g_list_free_full (output, (GDestroyNotify)g_bytes_unref);
+}
+
+/**
+ * cockpit_web_response_file:
+ * @response: the response
+ * @path: escaped path, or NULL to get from response
+ * @roots: directories to look for file in
+ *
+ * Serve a file from disk as an HTTP response.
+ */
+void
+cockpit_web_response_file (CockpitWebResponse *response,
+ const gchar *escaped,
+ const gchar **roots)
+{
+ web_response_file (response, escaped, roots, FALSE, FALSE, NULL, NULL);
+}
+
+void
+cockpit_web_response_template (CockpitWebResponse *response,
+ const gchar *escaped,
+ const gchar **roots,
+ GHashTable *values)
+{
+ web_response_file (response, escaped, roots, FALSE, FALSE, substitute_hash_value, values);
+}
+
+void
+cockpit_web_response_file_or_gz (CockpitWebResponse *response,
+ gboolean accepts_gzip,
+ const gchar *escaped,
+ const gchar **roots)
+{
+ web_response_file (response, escaped, roots, TRUE, accepts_gzip, NULL, NULL);
+}
+
+static gboolean
+response_next_path (CockpitWebResponse *self,
+ gchar **component)
+{
+ const gchar *beg = NULL;
+ const gchar *path;
+
+ g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), FALSE);
+
+ path = self->path;
+
+ if (path && path[0] == '/')
+ {
+ beg = path + 1;
+ path = strchr (beg, '/');
+ }
+ else
+ {
+ path = NULL;
+ }
+
+ if (!beg || path == beg)
+ return FALSE;
+
+ self->path = path;
+
+ if (self->path)
+ {
+ if (component)
+ *component = g_strndup (beg, path - beg);
+ }
+ else if (beg && beg[0])
+ {
+ if (component)
+ *component = g_strdup (beg);
+ }
+ else
+ {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+cockpit_web_response_skip_path (CockpitWebResponse *self)
+{
+ return response_next_path (self, NULL);
+}
+
+gchar *
+cockpit_web_response_pop_path (CockpitWebResponse *self)
+{
+ gchar *component = NULL;
+ if (!response_next_path (self, &component))
+ return NULL;
+ return component;
+}
+
+void
+cockpit_web_response_add_filter (CockpitWebResponse *self,
+ CockpitWebFilter *filter)
+{
+ g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (self));
+ g_return_if_fail (COCKPIT_IS_WEB_FILTER (filter));
+ g_return_if_fail (self->count == 0);
+ self->filters = g_list_append (self->filters, g_object_ref (filter));
+}
+
+/**
+ * cockpit_web_response_gunzip:
+ * @bytes: the compressed bytes
+ * @error: place to put an error
+ *
+ * Perform gzip decompression on the @bytes.
+ *
+ * Returns: the uncompressed bytes, caller owns return value.
+ */
+GBytes *
+cockpit_web_response_gunzip (GBytes *bytes,
+ GError **error)
+{
+ GConverter *converter;
+ GConverterResult result;
+ const guint8 *in;
+ gsize inl, outl, read, written;
+ GByteArray *out;
+
+ converter = G_CONVERTER (g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP));
+
+ in = g_bytes_get_data (bytes, &inl);
+ out = g_byte_array_new ();
+
+ do
+ {
+ outl = out->len;
+ g_byte_array_set_size (out, outl + inl);
+
+ result = g_converter_convert (converter, in, inl, out->data + outl, inl,
+ G_CONVERTER_INPUT_AT_END, &read, &written, error);
+ if (result == G_CONVERTER_ERROR)
+ break;
+
+ g_byte_array_set_size (out, outl + written);
+ in += read;
+ inl -= read;
+ }
+ while (result != G_CONVERTER_FINISHED);
+
+ g_object_unref (converter);
+
+ if (result != G_CONVERTER_FINISHED)
+ {
+ g_byte_array_unref (out);
+ return NULL;
+ }
+ else
+ {
+ return g_byte_array_free_to_bytes (out);
+ }
+}
+
+static const gchar *
+find_extension (const gchar *path)
+{
+ const gchar *dot;
+ const gchar *slash;
+
+ dot = strrchr (path, '.');
+ slash = strrchr (path, '/');
+
+ /* Dots before the last slash don't count */
+ if (dot && slash && dot < slash)
+ dot = NULL;
+
+ /* Leading dots on the filename don't count */
+ if (dot && (dot == path || dot == slash + 1))
+ dot = NULL;
+
+ return dot;
+}
+
+static GBytes *
+load_file (const gchar *filename,
+ GError **error)
+{
+ GError *local_error = NULL;
+
+ g_autoptr(GMappedFile) mapped = g_mapped_file_new (filename, FALSE, &local_error);
+
+ if (mapped)
+ /* success! */
+ return g_mapped_file_get_bytes (mapped);
+
+ if (g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT) ||
+ g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_ISDIR) ||
+ g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NAMETOOLONG) ||
+ g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_LOOP) ||
+ g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_INVAL))
+ {
+ g_clear_error (&local_error);
+ }
+
+ /* A real error to stop on */
+ else
+ {
+ g_propagate_error (error, local_error);
+ }
+
+ return NULL;
+}
+
+/**
+ * cockpit_web_response_negotiation:
+ * @path: likely filesystem path
+ * @existing: a table of existing files
+ * @language: requested client language
+ * @out_is_language_specific: a pointer to a gboolean whether the actual file is specific to @language
+ * @out_is_compressed: a pointer to a gboolean whether the actual file is compressed
+ * @error: a failure
+ *
+ * Find a file to serve based on the suffixes. We prune off extra
+ * extensions while looking for a file that's present. We append
+ * .min and .gz when looking for files. We also check for the language
+ * before the extensions if set.
+ *
+ * The @existing may be NULL, if non-null it'll be used to check if
+ * files exist.
+ */
+GBytes *
+cockpit_web_response_negotiation (const gchar *path,
+ GHashTable *existing,
+ const gchar *language,
+ gboolean *out_is_language_specific,
+ gboolean *out_is_compressed,
+ GError **error)
+{
+ gchar *base = NULL;
+ const gchar *ext;
+ gchar *dot;
+ gchar *name = NULL;
+ GBytes *bytes = NULL;
+ GError *local_error = NULL;
+ gchar *locale = NULL;
+ gchar *shorter = NULL;
+ gchar *lang = NULL;
+ gchar *lang_region = NULL;
+ gboolean is_language_specific, is_compressed;
+
+ gint i;
+
+ if (language)
+ locale = cockpit_locale_from_language (language, NULL, &shorter);
+
+ ext = find_extension (path);
+ if (ext)
+ {
+ base = g_strndup (path, ext - path);
+ }
+ else
+ {
+ ext = "";
+ base = g_strdup (path);
+ }
+
+ while (!bytes)
+ {
+ /* For a request for a file named "base.ext" and locale "lang_REGION", We try the following variants, in
+ order, and serve the first that is found:
+
+ base.lang_REGION.ext
+ base.lang_REGION.ext.gz
+ base.lang.ext
+ base.lang.ext.gz
+ base.ext
+ base.min.ext
+ base.ext.gz
+ base.ext.min.gz
+
+ If no locale is requested, or a locale without region, those variants are left out by starting
+ further down in the list.
+
+ If none of the variants are found, and the base of the file name has internal dots, these internal
+ extensions are dropped one by one from the right. For example, for a file named "foo.bar.js", we
+ first try "foo.bar" with extension ".js", and then "foo" with extension ".js".
+ */
+
+ if (locale && shorter && g_strcmp0 (locale, shorter) != 0) {
+ lang = shorter;
+ lang_region = locale;
+ i = 0;
+ } else if (locale) {
+ lang = locale;
+ i = 2;
+ } else {
+ i = 4;
+ }
+
+ for (; i < 8; i++)
+ {
+ g_free (name);
+ switch (i)
+ {
+ case 0:
+ name = g_strconcat (base, ".", lang_region, ext, NULL);
+ is_language_specific = TRUE;
+ is_compressed = FALSE;
+ break;
+ case 1:
+ name = g_strconcat (base, ".", lang_region, ext, ".gz", NULL);
+ is_language_specific = TRUE;
+ is_compressed = TRUE;
+ break;
+ case 2:
+ name = g_strconcat (base, ".", lang, ext, NULL);
+ is_language_specific = TRUE;
+ is_compressed = FALSE;
+ break;
+ case 3:
+ name = g_strconcat (base, ".", lang, ext, ".gz", NULL);
+ is_language_specific = TRUE;
+ is_compressed = TRUE;
+ break;
+ case 4:
+ name = g_strconcat (base, ext, NULL);
+ is_language_specific = FALSE;
+ is_compressed = FALSE;
+ break;
+ case 5:
+ name = g_strconcat (base, ".min", ext, NULL);
+ is_language_specific = FALSE;
+ is_compressed = FALSE;
+ break;
+ case 6:
+ name = g_strconcat (base, ext, ".gz", NULL);
+ is_language_specific = FALSE;
+ is_compressed = TRUE;
+ break;
+ case 7:
+ name = g_strconcat (base, ".min", ext, ".gz", NULL);
+ is_language_specific = FALSE;
+ is_compressed = TRUE;
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ if (existing)
+ {
+ if (!g_hash_table_lookup (existing, name))
+ continue;
+ }
+
+ bytes = load_file (name, &local_error);
+ if (bytes)
+ break;
+ if (local_error)
+ goto out;
+ }
+
+ /* Pop one level off the file name */
+ dot = (gchar *)find_extension (base);
+ if (!dot)
+ break;
+
+ dot[0] = '\0';
+ }
+
+out:
+ if (local_error)
+ g_propagate_error (error, local_error);
+ if (bytes)
+ {
+ if (out_is_language_specific)
+ *out_is_language_specific = is_language_specific;
+ if (out_is_compressed)
+ *out_is_compressed = is_compressed;
+ }
+ g_free (name);
+ g_free (base);
+ g_free (locale);
+ g_free (shorter);
+ return bytes;
+}
+
+const gchar *
+cockpit_web_response_content_type (const gchar *path)
+{
+ static const struct {
+ const gchar *extension;
+ const gchar *content_type;
+ } content_types[] = {
+ { ".css", "text/css" },
+ { ".gif", "image/gif" },
+ { ".eot", "application/vnd.ms-fontobject" },
+ { ".html", "text/html" },
+ /* { ".ico", "image/vnd.microsoft.icon" }, */
+ { ".jpg", "image/jpg" },
+ { ".js", "application/javascript" },
+ { ".json", "application/json" },
+ { ".otf", "font/opentype" },
+ { ".png", "image/png" },
+ { ".svg", "image/svg+xml" },
+ { ".ttf", "application/octet-stream" }, /* unassigned */
+ { ".txt", "text/plain" },
+ { ".wasm", "application/wasm" },
+ { ".woff", "application/font-woff" },
+ { ".xml", "text/xml" },
+ };
+
+ gint i;
+
+ for (i = 0; i < G_N_ELEMENTS (content_types); i++)
+ {
+ if (g_str_has_suffix (path, content_types[i].extension))
+ return content_types[i].content_type;
+ }
+
+ return NULL;
+}
+
+static gboolean
+strv_have_prefix (gchar **strv,
+ const gchar *prefix)
+{
+ gint i;
+
+ for (i = 0; strv && strv[i] != NULL; i++)
+ {
+ if (g_str_has_prefix (strv[i], prefix))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+string_inject_origin (GString *string,
+ const gchar *origin)
+{
+ const gchar *found;
+ gsize pos = 0;
+
+ for (;;)
+ {
+ found = strstr (string->str + pos, "'self'");
+ if (!found)
+ break;
+
+ pos = (found - string->str) + 6;
+ g_string_insert (string, pos, " ");
+ g_string_insert (string, pos + 1, origin);
+ pos += strlen (origin) + 1;
+ }
+}
+
+/**
+ * cockpit_web_response_content_security_policy:
+ * @content_security_policy: the raw security policy or %NULL for a default
+ * @self_origin: our own web origin or %NULL
+ *
+ * Calculates the security policy.
+ *
+ * Returns: A calculated security policy, filled with defaults if necessary.
+ */
+gchar *
+cockpit_web_response_security_policy (const gchar *content_security_policy,
+ const gchar *self_origin)
+{
+ const gchar *default_src = "default-src 'self'";
+ const gchar *form_action = "form-action 'self'";
+ const gchar *base_uri = "base-uri 'self'";
+ const gchar *object_src = "object-src 'none'";
+ const gchar *font_src = "font-src 'self' data:";
+ const gchar *img_src = "img-src 'self' data:";
+ const gchar *block_all_mixed_content = "block-all-mixed-content";
+ gchar **parts = NULL;
+ GString *result;
+ gint i;
+
+ result = g_string_sized_new (128);
+
+ /*
+ * Note that browsers need to be explicitly told they can connect
+ * to a WebSocket. This is non-obvious, but it stems from the fact
+ * that some browsers treat 'https' and 'wss' as different protocols.
+ *
+ * Since each component could establish a WebSocket connection back to
+ * cockpit-ws, we need to insert that into the policy.
+ */
+
+ if (content_security_policy)
+ parts = g_strsplit (content_security_policy, ";", -1);
+
+ for (i = 0; parts && parts[i] != NULL; i++)
+ g_strstrip (parts[i]);
+
+ if (!strv_have_prefix (parts, "default-src "))
+ g_string_append_printf (result, "%s; ", default_src);
+ if (!strv_have_prefix (parts, "connect-src "))
+ {
+ g_string_append (result, "connect-src 'self'");
+ if (self_origin && g_str_has_prefix (self_origin, "http"))
+ g_string_append_printf (result, " ws%s", self_origin + 4);
+ g_string_append (result, "; ");
+ }
+ if (!strv_have_prefix (parts, "form-action "))
+ g_string_append_printf (result, "%s; ", form_action);
+ if (!strv_have_prefix (parts, "base-uri "))
+ g_string_append_printf (result, "%s; ", base_uri);
+ if (!strv_have_prefix (parts, "object-src "))
+ g_string_append_printf (result, "%s; ", object_src);
+ if (!strv_have_prefix (parts, "font-src "))
+ g_string_append_printf (result, "%s; ", font_src);
+ if (!strv_have_prefix (parts, "img-src "))
+ g_string_append_printf (result, "%s; ", img_src);
+ if (!strv_have_prefix (parts, "block-all-mixed-content"))
+ g_string_append_printf (result, "%s; ", block_all_mixed_content);
+
+ for (i = 0; parts && parts[i] != NULL; i++)
+ g_string_append_printf (result, "%s; ", parts[i]);
+
+ g_strfreev (parts);
+
+ /* Remove trailing semicolon */
+ g_string_set_size (result, result->len - 2);
+
+ /* Put in our own origin */
+ if (self_origin)
+ string_inject_origin (result, self_origin);
+
+ return g_string_free (result, FALSE);
+}
+
+const gchar *
+cockpit_web_response_get_origin (CockpitWebResponse *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_RESPONSE (self), NULL);
+ return self->origin;
+}
+
+const gchar *
+cockpit_web_response_get_protocol (CockpitWebResponse *self)
+{
+ return self->protocol;
+}
+
+static void
+cockpit_web_response_flow_iface_init (CockpitFlowInterface *iface)
+{
+ /* No implementation */
+}
diff --git a/src/common/cockpitwebresponse.h b/src/common/cockpitwebresponse.h
new file mode 100644
index 0000000..64dbb38
--- /dev/null
+++ b/src/common/cockpitwebresponse.h
@@ -0,0 +1,157 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_WEB_RESPONSE_H__
+#define __COCKPIT_WEB_RESPONSE_H__
+
+#include <gio/gio.h>
+
+#include "cockpitwebfilter.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_RESOURCE_PACKAGE_VALID "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
+
+#define COCKPIT_TYPE_WEB_RESPONSE (cockpit_web_response_get_type ())
+G_DECLARE_FINAL_TYPE(CockpitWebResponse, cockpit_web_response, COCKPIT, WEB_RESPONSE, GObject)
+
+typedef enum {
+ COCKPIT_WEB_RESPONSE_READY = 1,
+ COCKPIT_WEB_RESPONSE_QUEUING,
+ COCKPIT_WEB_RESPONSE_COMPLETE,
+ COCKPIT_WEB_RESPONSE_SENT,
+} CockpitWebResponding;
+
+typedef enum {
+ COCKPIT_WEB_RESPONSE_CACHE_UNSET,
+ COCKPIT_WEB_RESPONSE_NO_CACHE,
+ COCKPIT_WEB_RESPONSE_CACHE,
+} CockpitCacheType;
+
+#define COCKPIT_CHECKSUM_HEADER "X-Cockpit-Pkg-Checksum"
+
+typedef struct _CockpitWebResponse CockpitWebResponse;
+
+extern const gchar * cockpit_web_exception_escape_root;
+
+CockpitWebResponse * cockpit_web_response_new (GIOStream *io,
+ const gchar *original_path,
+ const gchar *path,
+ GHashTable *in_headers,
+ const gchar *method,
+ const gchar *protocol);
+
+
+const gchar * cockpit_web_response_get_path (CockpitWebResponse *self);
+
+GIOStream * cockpit_web_response_get_stream (CockpitWebResponse *self);
+
+CockpitWebResponding cockpit_web_response_get_state (CockpitWebResponse *self);
+
+gboolean cockpit_web_response_skip_path (CockpitWebResponse *self);
+
+gchar * cockpit_web_response_pop_path (CockpitWebResponse *self);
+
+void cockpit_web_response_add_filter (CockpitWebResponse *self,
+ CockpitWebFilter *filter);
+
+void cockpit_web_response_headers (CockpitWebResponse *self,
+ guint status,
+ const gchar *reason,
+ gssize length,
+ ...) G_GNUC_NULL_TERMINATED;
+
+void cockpit_web_response_headers_full (CockpitWebResponse *self,
+ guint status,
+ const gchar *reason,
+ gssize length,
+ GHashTable *headers);
+
+gboolean cockpit_web_response_queue (CockpitWebResponse *self,
+ GBytes *block);
+
+void cockpit_web_response_complete (CockpitWebResponse *self);
+
+void cockpit_web_response_abort (CockpitWebResponse *self);
+
+void cockpit_web_response_content (CockpitWebResponse *self,
+ GHashTable *headers,
+ GBytes *block,
+ ...) G_GNUC_NULL_TERMINATED;
+
+void cockpit_web_response_error (CockpitWebResponse *self,
+ guint status,
+ GHashTable *headers,
+ const char *format,
+ ...) G_GNUC_PRINTF (4, 5);
+
+void cockpit_web_response_gerror (CockpitWebResponse *self,
+ GHashTable *headers,
+ GError *error);
+
+gchar ** cockpit_web_response_resolve_roots (const gchar **roots);
+
+void cockpit_web_response_file (CockpitWebResponse *response,
+ const gchar *escaped,
+ const gchar **roots);
+
+void cockpit_web_response_file_or_gz (CockpitWebResponse *response,
+ gboolean accepts_gz,
+ const gchar *escaped,
+ const gchar **roots);
+
+GBytes * cockpit_web_response_gunzip (GBytes *bytes,
+ GError **error);
+
+GBytes * cockpit_web_response_negotiation (const gchar *path,
+ GHashTable *existing,
+ const gchar *language,
+ gboolean *out_is_language_specific,
+ gboolean *out_is_compressed,
+ GError **error);
+
+const gchar * cockpit_web_response_content_type (const gchar *path);
+
+gboolean cockpit_web_should_suppress_output_error (const gchar *logname,
+ GError *error);
+
+gboolean cockpit_web_response_is_simple_token (const gchar *string);
+
+gboolean cockpit_web_response_is_header_value (const gchar *string);
+
+void cockpit_web_response_set_cache_type (CockpitWebResponse *self,
+ CockpitCacheType cache_type);
+
+const gchar * cockpit_web_response_get_url_root (CockpitWebResponse *response);
+
+const gchar * cockpit_web_response_get_origin (CockpitWebResponse *response);
+
+const gchar * cockpit_web_response_get_protocol (CockpitWebResponse *response);
+
+void cockpit_web_response_template (CockpitWebResponse *response,
+ const gchar *escaped,
+ const gchar **roots,
+ GHashTable *values);
+
+gchar * cockpit_web_response_security_policy (const gchar *content_security_policy,
+ const gchar *self_origin);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_RESPONSE_H__ */
diff --git a/src/common/cockpitwebserver.c b/src/common/cockpitwebserver.c
new file mode 100644
index 0000000..c6a7492
--- /dev/null
+++ b/src/common/cockpitwebserver.c
@@ -0,0 +1,1437 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013-2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitwebserver.h"
+
+#include "cockpitconf.h"
+#include "cockpithash.h"
+#include "cockpitjson.h"
+#include "cockpitmemfdread.h"
+#include "cockpitmemory.h"
+#include "cockpitsocket.h"
+#include "cockpitwebresponse.h"
+
+#include "websocket/websocket.h"
+
+#include <sys/socket.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "cockpitwebrequest-private.h"
+
+/* Used during testing */
+gboolean cockpit_webserver_want_certificate = FALSE;
+
+guint cockpit_webserver_request_timeout = 30;
+const gsize cockpit_webserver_request_maximum = 8192;
+
+struct _CockpitWebServer {
+ GObject parent_instance;
+
+ GTlsCertificate *certificate;
+ GString *ssl_exception_prefix;
+ GString *url_root;
+ gint request_timeout;
+ gint request_max;
+ CockpitWebServerFlags flags;
+
+ gchar *protocol_header;
+ gchar *forwarded_for_header;
+
+ GSocketService *socket_service;
+ GMainContext *main_context;
+ GHashTable *requests;
+};
+
+enum
+{
+ PROP_0,
+ PROP_CERTIFICATE,
+ PROP_SSL_EXCEPTION_PREFIX,
+ PROP_FLAGS,
+ PROP_URL_ROOT,
+};
+
+static gint sig_handle_stream = 0;
+static gint sig_handle_resource = 0;
+
+static void cockpit_web_request_free (gpointer data);
+
+static void cockpit_web_request_start (CockpitWebServer *web_server,
+ GIOStream *stream,
+ gboolean first);
+
+G_DEFINE_TYPE (CockpitWebServer, cockpit_web_server, G_TYPE_OBJECT)
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+on_incoming (GSocketService *service,
+ GSocketConnection *connection,
+ GObject *source_object,
+ gpointer user_data)
+{
+ CockpitWebServer *self = COCKPIT_WEB_SERVER (user_data);
+ cockpit_web_request_start (self, G_IO_STREAM (connection), TRUE);
+
+ /* handled */
+ return TRUE;
+}
+
+static void
+cockpit_web_server_init (CockpitWebServer *server)
+{
+ server->requests = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+ cockpit_web_request_free, NULL);
+ server->main_context = g_main_context_ref_thread_default ();
+ server->ssl_exception_prefix = g_string_new ("");
+ server->url_root = g_string_new ("");
+
+ server->socket_service = g_socket_service_new ();
+
+ /* The web server has to be explicitly started */
+ g_socket_service_stop (server->socket_service);
+
+ g_signal_connect (server->socket_service, "incoming",
+ G_CALLBACK (on_incoming), server);
+}
+
+static void
+cockpit_web_server_dispose (GObject *object)
+{
+ CockpitWebServer *self = COCKPIT_WEB_SERVER (object);
+
+ g_hash_table_remove_all (self->requests);
+
+ G_OBJECT_CLASS (cockpit_web_server_parent_class)->dispose (object);
+}
+
+static void
+cockpit_web_server_finalize (GObject *object)
+{
+ CockpitWebServer *server = COCKPIT_WEB_SERVER (object);
+
+ g_clear_object (&server->certificate);
+ g_hash_table_destroy (server->requests);
+ if (server->main_context)
+ g_main_context_unref (server->main_context);
+ g_string_free (server->ssl_exception_prefix, TRUE);
+ g_string_free (server->url_root, TRUE);
+ g_clear_object (&server->socket_service);
+ g_free (server->protocol_header);
+ g_free (server->forwarded_for_header);
+
+ G_OBJECT_CLASS (cockpit_web_server_parent_class)->finalize (object);
+}
+
+static void
+cockpit_web_server_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitWebServer *server = COCKPIT_WEB_SERVER (object);
+
+ switch (prop_id)
+ {
+ case PROP_CERTIFICATE:
+ g_value_set_object (value, server->certificate);
+ break;
+
+ case PROP_SSL_EXCEPTION_PREFIX:
+ g_value_set_string (value, server->ssl_exception_prefix->str);
+ break;
+
+ case PROP_URL_ROOT:
+ if (server->url_root->len)
+ g_value_set_string (value, server->url_root->str);
+ else
+ g_value_set_string (value, NULL);
+ break;
+
+ case PROP_FLAGS:
+ g_value_set_int (value, server->flags);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_web_server_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitWebServer *server = COCKPIT_WEB_SERVER (object);
+ GString *str;
+
+ switch (prop_id)
+ {
+ case PROP_CERTIFICATE:
+ server->certificate = g_value_dup_object (value);
+ break;
+
+ case PROP_SSL_EXCEPTION_PREFIX:
+ g_string_assign (server->ssl_exception_prefix, g_value_get_string (value));
+ break;
+
+ case PROP_URL_ROOT:
+ str = g_string_new (g_value_get_string (value));
+
+ while (str->str[0] == '/')
+ g_string_erase (str, 0, 1);
+
+ if (str->len)
+ {
+ while (str->str[str->len - 1] == '/')
+ g_string_truncate (str, str->len - 1);
+ }
+
+ if (str->len)
+ g_string_printf (server->url_root, "/%s", str->str);
+ else
+ g_string_assign (server->url_root, str->str);
+
+ g_string_free (str, TRUE);
+ break;
+
+ case PROP_FLAGS:
+ server->flags = g_value_get_int (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+on_io_closed (GObject *stream,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GError *error = NULL;
+
+ if (!g_io_stream_close_finish (G_IO_STREAM (stream), result, &error))
+ {
+ if (!cockpit_web_should_suppress_output_error ("http", error))
+ g_message ("http close error: %s", error->message);
+ g_error_free (error);
+ }
+}
+
+static void
+close_io_stream (GIOStream *io)
+{
+ g_io_stream_close_async (io, G_PRIORITY_DEFAULT, NULL, on_io_closed, NULL);
+}
+
+static void
+on_web_response_done (CockpitWebResponse *response,
+ gboolean reusable,
+ gpointer user_data)
+{
+ CockpitWebServer *self = user_data;
+ GIOStream *io;
+
+ io = cockpit_web_response_get_stream (response);
+ if (reusable)
+ cockpit_web_request_start (self, io, FALSE);
+ else
+ close_io_stream (io);
+}
+
+static gboolean
+cockpit_web_server_default_handle_resource (CockpitWebServer *self,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response)
+{
+ cockpit_web_response_error (response, 404, NULL, NULL);
+ return TRUE;
+}
+
+static gboolean
+cockpit_web_server_default_handle_stream (CockpitWebServer *self,
+ CockpitWebRequest *request)
+{
+ CockpitWebResponse *response;
+ gboolean claimed = FALSE;
+ GQuark detail = 0;
+
+ /* TODO: Correct HTTP version for response */
+ response = cockpit_web_request_respond (request);
+ g_signal_connect_data (response, "done", G_CALLBACK (on_web_response_done),
+ g_object_ref (self), (GClosureNotify)g_object_unref, 0);
+
+ /*
+ * If the path has more than one component, then we search
+ * for handlers registered under the detail like this:
+ *
+ * /component/
+ *
+ * Otherwise we search for handlers registered under detail
+ * of the entire path:
+ *
+ * /component
+ *
+ * We only bother to calculate the detail if it would have a length of
+ * less than 100: nobody is going to register a signal handler for a
+ * longer path than that.
+ */
+ g_assert (request->path[0] == '/');
+ gsize component_end = 1 + strcspn (request->path + 1, "/");
+ if (request->path[component_end] == '/')
+ component_end++;
+ if (component_end < 100)
+ {
+ gchar buffer[component_end + 1];
+ memcpy (buffer, request->path, component_end);
+ buffer[component_end] = '\0';
+ detail = g_quark_try_string (buffer);
+ }
+
+ /* See if we have any takers... */
+ g_signal_emit (self,
+ sig_handle_resource, detail,
+ request,
+ request->path,
+ request->headers,
+ response,
+ &claimed);
+
+ if (!claimed)
+ claimed = cockpit_web_server_default_handle_resource (self, request, request->path, request->headers, response);
+
+ /* TODO: Here is where we would plug keep-alive into response */
+ g_object_unref (response);
+
+ return claimed;
+}
+
+static void
+cockpit_web_server_class_init (CockpitWebServerClass *klass)
+{
+ GObjectClass *gobject_class;
+
+ gobject_class = G_OBJECT_CLASS (klass);
+ gobject_class->dispose = cockpit_web_server_dispose;
+ gobject_class->finalize = cockpit_web_server_finalize;
+ gobject_class->set_property = cockpit_web_server_set_property;
+ gobject_class->get_property = cockpit_web_server_get_property;
+
+ g_object_class_install_property (gobject_class,
+ PROP_CERTIFICATE,
+ g_param_spec_object ("certificate", NULL, NULL,
+ G_TYPE_TLS_CERTIFICATE,
+ G_PARAM_READABLE |
+ G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class, PROP_SSL_EXCEPTION_PREFIX,
+ g_param_spec_string ("ssl-exception-prefix", NULL, NULL, "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class, PROP_URL_ROOT,
+ g_param_spec_string ("url-root", NULL, NULL, "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class, PROP_FLAGS,
+ g_param_spec_int ("flags", NULL, NULL, 0, COCKPIT_WEB_SERVER_FLAGS_MAX, 0,
+ G_PARAM_READABLE |
+ G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ sig_handle_stream = g_signal_new ("handle-stream",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST,
+ 0, /* class offset */
+ g_signal_accumulator_true_handled,
+ NULL, /* accu_data */
+ g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN,
+ 1,
+ COCKPIT_TYPE_WEB_REQUEST | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+ sig_handle_resource = g_signal_new ("handle-resource",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
+ 0, /* class offset */
+ g_signal_accumulator_true_handled,
+ NULL, /* accu_data */
+ g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN,
+ 4,
+ COCKPIT_TYPE_WEB_REQUEST | G_SIGNAL_TYPE_STATIC_SCOPE,
+ G_TYPE_STRING,
+ G_TYPE_HASH_TABLE,
+ COCKPIT_TYPE_WEB_RESPONSE);
+}
+
+CockpitWebServer *
+cockpit_web_server_new (GTlsCertificate *certificate,
+ CockpitWebServerFlags flags)
+{
+ return g_object_new (COCKPIT_TYPE_WEB_SERVER,
+ "certificate", certificate,
+ "flags", flags,
+ NULL);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+CockpitWebServerFlags
+cockpit_web_server_get_flags (CockpitWebServer *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_SERVER (self), COCKPIT_WEB_SERVER_NONE);
+
+ return self->flags;
+}
+
+GHashTable *
+cockpit_web_server_new_table (void)
+{
+ return g_hash_table_new_full (cockpit_str_case_hash, cockpit_str_case_equal, g_free, g_free);
+}
+
+gchar *
+cockpit_web_server_parse_cookie (GHashTable *headers,
+ const gchar *name)
+{
+ const gchar *header;
+ const gchar *pos;
+ const gchar *value;
+ const gchar *end;
+ gboolean at_start = TRUE;
+ gchar *decoded;
+ gint diff;
+ gint offset;
+
+ header = g_hash_table_lookup (headers, "Cookie");
+ if (!header)
+ return NULL;
+
+ for (;;)
+ {
+ pos = strstr (header, name);
+ if (!pos)
+ return NULL;
+
+ if (pos != header)
+ {
+ diff = strlen (header) - strlen (pos);
+ offset = 1;
+ at_start = FALSE;
+ while (offset < diff)
+ {
+ if (!g_ascii_isspace (*(pos - offset)))
+ {
+ at_start = *(pos - offset) == ';';
+ break;
+ }
+ offset++;
+ }
+ }
+
+ pos += strlen (name);
+ if (*pos == '=' && at_start)
+ {
+ value = pos + 1;
+ end = strchr (value, ';');
+ if (end == NULL)
+ end = value + strlen (value);
+
+ decoded = g_uri_unescape_segment (value, end, NULL);
+ if (!decoded)
+ g_debug ("invalid cookie encoding");
+
+ return decoded;
+ }
+ else
+ {
+ at_start = FALSE;
+ }
+ header = pos;
+ }
+}
+
+typedef struct {
+ double qvalue;
+ const gchar *value;
+} Language;
+
+static gint
+sort_qvalue (gconstpointer a,
+ gconstpointer b)
+{
+ const Language *la = *((Language **)a);
+ const Language *lb = *((Language **)b);
+ if (lb->qvalue == la->qvalue)
+ return 0;
+ return lb->qvalue < la->qvalue ? -1 : 1;
+}
+
+gchar **
+cockpit_web_server_parse_accept_list (const gchar *accept,
+ const gchar *defawlt)
+{
+ Language *lang;
+ GPtrArray *langs;
+ GPtrArray *ret;
+ gchar *copy;
+ gchar *value;
+ gchar *next;
+ gchar *pos;
+ guint i;
+
+ langs = g_ptr_array_new_with_free_func (g_free);
+
+ if (defawlt)
+ {
+ lang = g_new0 (Language, 1);
+ lang->qvalue = 0.1;
+ lang->value = defawlt;
+ g_ptr_array_add (langs, lang);
+ }
+
+ /* First build up an array we can sort */
+ accept = copy = g_strdup (accept);
+
+ while (accept)
+ {
+ next = strchr (accept, ',');
+ if (next)
+ {
+ *next = '\0';
+ next++;
+ }
+
+ lang = g_new0 (Language, 1);
+ lang->qvalue = 1;
+
+ pos = strchr (accept, ';');
+ if (pos)
+ {
+ *pos = '\0';
+ if (strncmp (pos + 1, "q=", 2) == 0)
+ {
+ lang->qvalue = g_ascii_strtod (pos + 3, NULL);
+ if (lang->qvalue < 0)
+ lang->qvalue = 0;
+ }
+ }
+
+ lang->value = accept;
+ g_ptr_array_add (langs, lang);
+ accept = next;
+ }
+
+ g_ptr_array_sort (langs, sort_qvalue);
+
+ /* Now in the right order add all the prefs */
+ ret = g_ptr_array_new ();
+ for (i = 0; i < langs->len; i++)
+ {
+ lang = langs->pdata[i];
+ if (lang->qvalue > 0)
+ {
+ value = g_strstrip (g_ascii_strdown (lang->value, -1));
+ g_ptr_array_add (ret, value);
+ }
+ }
+
+ /* Add base languages after that */
+ for (i = 0; i < langs->len; i++)
+ {
+ lang = langs->pdata[i];
+ if (lang->qvalue > 0)
+ {
+ pos = strchr (lang->value, '-');
+ if (pos)
+ {
+ value = g_strstrip (g_ascii_strdown (lang->value, pos - lang->value));
+ g_ptr_array_add (ret, value);
+ }
+ }
+ }
+
+ g_free (copy);
+ g_ptr_array_add (ret, NULL);
+ g_ptr_array_free (langs, TRUE);
+ return (gchar **)g_ptr_array_free (ret, FALSE);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+guint16
+cockpit_web_server_add_inet_listener (CockpitWebServer *self,
+ const gchar *address,
+ guint16 port,
+ GError **error)
+{
+ if (address != NULL)
+ {
+ g_autoptr(GSocketAddress) socket_address = g_inet_socket_address_new_from_string (address, port);
+ if (socket_address == NULL)
+ {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ "Couldn't parse IP address from `%s`", address);
+ return 0;
+ }
+
+ g_autoptr(GSocketAddress) result_address = NULL;
+ if (!g_socket_listener_add_address (G_SOCKET_LISTENER (self->socket_service), socket_address,
+ G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_DEFAULT,
+ NULL, &result_address, error))
+ return 0;
+
+ port = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (result_address));
+ g_assert (port != 0);
+
+ return port;
+ }
+
+ else if (port > 0)
+ {
+ if (g_socket_listener_add_inet_port (G_SOCKET_LISTENER (self->socket_service), port, NULL, error))
+ return port;
+ else
+ return 0;
+ }
+ else
+ return g_socket_listener_add_any_inet_port (G_SOCKET_LISTENER (self->socket_service), NULL, error);
+}
+
+gboolean
+cockpit_web_server_add_fd_listener (CockpitWebServer *self,
+ int fd,
+ GError **error)
+{
+ g_autoptr(GSocket) socket = g_socket_new_from_fd (fd, error);
+ if (socket == NULL)
+ {
+ g_prefix_error (error, "Failed to acquire passed socket %i: ", fd);
+ return FALSE;
+ }
+
+ if (!g_socket_listener_add_socket (G_SOCKET_LISTENER (self->socket_service), socket, NULL, error))
+ {
+ g_prefix_error (error, "Failed to add listener for socket %i: ", fd);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+void
+cockpit_web_server_start (CockpitWebServer *self)
+{
+ g_return_if_fail (COCKPIT_IS_WEB_SERVER (self));
+ g_socket_service_start (self->socket_service);
+}
+
+GIOStream *
+cockpit_web_server_connect (CockpitWebServer *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_SERVER (self), NULL);
+
+ g_autoptr(GIOStream) server = NULL;
+ g_autoptr(GIOStream) client = NULL;
+
+ cockpit_socket_streampair (&client, &server);
+
+ cockpit_web_request_start (self, server, TRUE);
+
+ return g_steal_pointer (&client);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+void
+cockpit_web_server_set_protocol_header (CockpitWebServer *self,
+ const gchar *protocol_header)
+{
+ g_free (self->protocol_header);
+ self->protocol_header = g_strdup (protocol_header);
+}
+
+void
+cockpit_web_server_set_forwarded_for_header (CockpitWebServer *self,
+ const gchar *forwarded_for_header)
+{
+ g_free (self->forwarded_for_header);
+ self->forwarded_for_header = g_strdup (forwarded_for_header);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static CockpitWebRequest *
+never_copy (CockpitWebRequest *self)
+{
+ g_assert_not_reached ();
+}
+
+static void
+never_free (CockpitWebRequest *self)
+{
+ g_assert_not_reached ();
+}
+
+G_DEFINE_BOXED_TYPE(CockpitWebRequest, cockpit_web_request, never_copy, never_free);
+
+static void
+cockpit_web_request_free (gpointer data)
+{
+ CockpitWebRequest *self = data;
+ if (self->timeout)
+ {
+ g_source_destroy (self->timeout);
+ g_source_unref (self->timeout);
+ }
+ if (self->source)
+ {
+ g_source_destroy (self->source);
+ g_source_unref (self->source);
+ }
+
+ /*
+ * Request memory is either cleared or used elsewhere, by
+ * handle-stream handlers (eg: the default handler. Don't
+ * clear it here. The buffer may still be in use.
+ */
+ g_byte_array_unref (self->buffer);
+ g_object_unref (self->io);
+ g_free (self);
+}
+
+static void
+cockpit_web_request_finish (CockpitWebRequest *self)
+{
+ g_hash_table_remove (self->web_server->requests, self);
+}
+
+static void
+cockpit_web_request_process_delayed_reply (CockpitWebRequest *self,
+ const gchar *path,
+ GHashTable *headers)
+{
+ g_assert (self->delayed_reply > 299);
+
+ g_autoptr(CockpitWebResponse) response = cockpit_web_request_respond (self);
+ g_signal_connect_data (response, "done", G_CALLBACK (on_web_response_done),
+ g_object_ref (self->web_server), (GClosureNotify)g_object_unref, 0);
+
+ if (self->delayed_reply == 301)
+ {
+ const gchar *host = g_hash_table_lookup (headers, "Host");
+ g_autofree gchar *url = g_strdup_printf ("https://%s%s", host != NULL ? host : "", path);
+ cockpit_web_response_headers (response, 301, "Moved Permanently", 0, "Location", url, NULL);
+ cockpit_web_response_complete (response);
+ }
+ else
+ {
+ cockpit_web_response_error (response, self->delayed_reply, NULL, NULL);
+ }
+}
+
+static gboolean
+path_has_prefix (const gchar *path,
+ GString *prefix)
+{
+ return prefix->len > 0 &&
+ strncmp (path, prefix->str, prefix->len) == 0 &&
+ (path[prefix->len] == '\0' || path[prefix->len] == '/');
+}
+
+static gboolean
+is_localhost_connection (GSocketConnection *conn)
+{
+ g_autoptr (GSocketAddress) addr = g_socket_connection_get_local_address (conn, NULL);
+ if (G_IS_INET_SOCKET_ADDRESS (addr))
+ {
+ GInetAddress *inet = g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (addr));
+ return g_inet_address_get_is_loopback (inet);
+ }
+
+ return FALSE;
+}
+
+static void
+cockpit_web_request_process (CockpitWebRequest *self,
+ const gchar *method,
+ const gchar *path,
+ const gchar *host,
+ GHashTable *headers)
+{
+ gboolean claimed = FALSE;
+
+ if (self->web_server->url_root->len &&
+ !path_has_prefix (path, self->web_server->url_root))
+ {
+ self->delayed_reply = 404;
+ }
+
+ /* Redirect to TLS? */
+ if (!self->delayed_reply && self->check_tls_redirect)
+ {
+ self->check_tls_redirect = FALSE;
+
+ /* Certain paths don't require us to redirect */
+ if (!path_has_prefix (path, self->web_server->ssl_exception_prefix))
+ {
+ if (!is_localhost_connection (G_SOCKET_CONNECTION (self->io)))
+ {
+ g_debug ("redirecting request from Host: %s to TLS", host);
+ self->delayed_reply = 301;
+ }
+ }
+ }
+
+ self->method = method;
+
+ if (self->delayed_reply)
+ {
+ cockpit_web_request_process_delayed_reply (self, path, headers);
+ return;
+ }
+
+ g_autofree gchar *path_copy = g_strdup (path);
+
+ self->original_path = path_copy;
+ self->path = path_copy + self->web_server->url_root->len;
+ self->headers = headers;
+ self->host = host;
+
+ gchar *query = strchr (path_copy, '?');
+ if (query)
+ {
+ *query = '\0';
+ self->query = query + 1;
+ }
+ else
+ self->query = "";
+
+ /* See if we have any takers... */
+ g_signal_emit (self->web_server, sig_handle_stream, 0, self, &claimed);
+
+ if (!claimed)
+ claimed = cockpit_web_server_default_handle_stream (self->web_server, self);
+
+ self->original_path = NULL;
+ self->path = NULL;
+ self->query = NULL;
+
+ if (!claimed)
+ g_critical ("no handler responded to request: %s", self->path);
+}
+
+static gboolean
+cockpit_web_request_parse_and_process (CockpitWebRequest *self)
+{
+ gboolean again = FALSE;
+ GHashTable *headers = NULL;
+ gchar *method = NULL;
+ gchar *path = NULL;
+ const gchar *str;
+ gchar *end = NULL;
+ gssize off1;
+ gssize off2;
+ guint64 length;
+
+ /* The hard input limit, we just terminate the connection */
+ if (self->buffer->len > cockpit_webserver_request_maximum * 2)
+ {
+ g_message ("received HTTP request that was too large");
+ goto out;
+ }
+
+ off1 = web_socket_util_parse_req_line ((const gchar *)self->buffer->data,
+ self->buffer->len,
+ &method,
+ &path);
+ if (off1 == 0)
+ {
+ again = TRUE;
+ goto out;
+ }
+ if (off1 < 0)
+ {
+ g_message ("received invalid HTTP request line");
+ self->delayed_reply = 400;
+ goto out;
+ }
+ if (!path || path[0] != '/')
+ {
+ g_message ("received invalid HTTP path");
+ self->delayed_reply = 400;
+ goto out;
+ }
+
+ off2 = web_socket_util_parse_headers ((const gchar *)self->buffer->data + off1,
+ self->buffer->len - off1,
+ &headers);
+ if (off2 == 0)
+ {
+ again = TRUE;
+ goto out;
+ }
+ if (off2 < 0)
+ {
+ g_message ("received invalid HTTP request headers");
+ self->delayed_reply = 400;
+ goto out;
+ }
+
+ /* If we get a Content-Length then verify it is zero */
+ length = 0;
+ str = g_hash_table_lookup (headers, "Content-Length");
+ if (str != NULL)
+ {
+ end = NULL;
+ length = g_ascii_strtoull (str, &end, 10);
+ if (!end || end[0])
+ {
+ g_message ("received invalid Content-Length");
+ self->delayed_reply = 400;
+ goto out;
+ }
+
+ /* The soft limit, we return 413 */
+ if (length != 0)
+ {
+ g_debug ("received non-zero Content-Length");
+ self->delayed_reply = 413;
+ }
+ }
+
+ /* Not enough data yet */
+ if (self->buffer->len < off1 + off2 + length)
+ {
+ again = TRUE;
+ goto out;
+ }
+
+ if (!g_str_equal (method, "GET") && !g_str_equal (method, "HEAD"))
+ {
+ g_message ("received unsupported HTTP method");
+ self->delayed_reply = 405;
+ }
+
+ str = g_hash_table_lookup (headers, "Host");
+ if (!str || g_str_equal (str, ""))
+ {
+ g_message ("received HTTP request without Host header");
+ self->delayed_reply = 400;
+ }
+
+ g_byte_array_remove_range (self->buffer, 0, off1 + off2);
+ cockpit_web_request_process (self, method, path, str, headers);
+
+out:
+ if (headers)
+ g_hash_table_unref (headers);
+ g_free (method);
+ g_free (path);
+ if (!again)
+ cockpit_web_request_finish (self);
+ return again;
+}
+
+#if !GLIB_CHECK_VERSION(2,43,2)
+#define G_IO_ERROR_CONNECTION_CLOSED G_IO_ERROR_BROKEN_PIPE
+#endif
+
+static gboolean
+should_suppress_request_error (GError *error,
+ gsize received)
+{
+ if (g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_EOF) ||
+ g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_NOT_TLS))
+ {
+ g_debug ("request error: %s", error->message);
+ return TRUE;
+ }
+
+ /* If no bytes received, then don't worry about ECONNRESET and friends */
+ if (received > 0)
+ return FALSE;
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_CLOSED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_BROKEN_PIPE))
+ {
+ g_debug ("request error: %s", error->message);
+ return TRUE;
+ }
+
+#if !GLIB_CHECK_VERSION(2,43,2)
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_FAILED) &&
+ strstr (error->message, g_strerror (ECONNRESET)))
+ {
+ g_debug ("request error: %s", error->message);
+ return TRUE;
+ }
+#endif
+
+ return FALSE;
+}
+
+static gboolean
+cockpit_web_request_on_input (GObject *pollable_input,
+ gpointer user_data)
+{
+ GPollableInputStream *input = (GPollableInputStream *)pollable_input;
+ CockpitWebRequest *self = user_data;
+ GError *error = NULL;
+ gsize length;
+ gssize count;
+
+ length = self->buffer->len;
+
+ /* With a GTlsServerConnection, the GSource callback is not called again if
+ * there is still pending data in GnuTLS'es buffer.
+ * (https://gitlab.gnome.org/GNOME/glib-networking/issues/20). Thus read up
+ * to our allowed maximum size to ensure we got everything that's pending.
+ * Add one extra byte so that cockpit_web_request_parse_and_process()
+ * correctly rejects requests that are > maximum, instead of hanging.
+ *
+ * FIXME: This may still hang for several large requests that are pipelined;
+ * for these this needs to be changed into a loop.
+ */
+ g_byte_array_set_size (self->buffer, length + cockpit_webserver_request_maximum + 1);
+
+ count = g_pollable_input_stream_read_nonblocking (input, self->buffer->data + length,
+ cockpit_webserver_request_maximum + 1, NULL, &error);
+ if (count < 0)
+ {
+ g_byte_array_set_size (self->buffer, length);
+
+ /* Just wait and try again */
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+ {
+ g_error_free (error);
+ return TRUE;
+ }
+
+ if (!should_suppress_request_error (error, length))
+ g_message ("couldn't read from connection: %s", error->message);
+
+ cockpit_web_request_finish (self);
+ g_error_free (error);
+ return FALSE;
+ }
+
+ g_byte_array_set_size (self->buffer, length + count);
+
+ if (count == 0)
+ {
+ if (self->eof_okay)
+ close_io_stream (self->io);
+ else
+ g_debug ("caller closed connection early");
+ cockpit_web_request_finish (self);
+ return FALSE;
+ }
+
+ /* Once we receive data EOF is unexpected (until possible next request) */
+ self->eof_okay = FALSE;
+
+ return cockpit_web_request_parse_and_process (self);
+}
+
+static void
+cockpit_web_request_start_input (CockpitWebRequest *self)
+{
+ GPollableInputStream *poll_in;
+ GInputStream *in;
+
+ /* Both GSocketConnection and GTlsServerConnection are pollable */
+ in = g_io_stream_get_input_stream (self->io);
+ poll_in = NULL;
+ if (G_IS_POLLABLE_INPUT_STREAM (in))
+ poll_in = (GPollableInputStream *)in;
+
+ if (!poll_in || !g_pollable_input_stream_can_poll (poll_in))
+ {
+ if (in)
+ g_critical ("cannot use a non-pollable input stream: %s", G_OBJECT_TYPE_NAME (in));
+ else
+ g_critical ("no input stream available");
+
+ cockpit_web_request_finish (self);
+ return;
+ }
+
+ /* Replace with a new source */
+ if (self->source)
+ {
+ g_source_destroy (self->source);
+ g_source_unref (self->source);
+ }
+
+ self->source = g_pollable_input_stream_create_source (poll_in, NULL);
+ g_source_set_callback (self->source, (GSourceFunc)cockpit_web_request_on_input, self, NULL);
+ g_source_attach (self->source, self->web_server->main_context);
+}
+
+static gboolean
+cockpit_web_request_on_accept_certificate (GTlsConnection *conn,
+ GTlsCertificate *peer_cert,
+ GTlsCertificateFlags errors,
+ gpointer user_data)
+{
+ /* Only used during testing */
+ g_assert (cockpit_webserver_want_certificate == TRUE);
+ return TRUE;
+}
+
+static gboolean
+cockpit_web_request_on_socket_input (GSocket *socket,
+ GIOCondition condition,
+ gpointer user_data)
+{
+ CockpitWebRequest *self = user_data;
+ guchar first_byte;
+ GInputVector vector[1] = { { &first_byte, 1 } };
+ gint flags = G_SOCKET_MSG_PEEK;
+ GError *error = NULL;
+ GIOStream *tls_stream;
+ gssize num_read;
+ g_auto(CockpitControlMessages) ccm = COCKPIT_CONTROL_MESSAGES_INIT;
+
+ num_read = g_socket_receive_message (socket,
+ NULL, /* out GSocketAddress */
+ vector,
+ 1,
+ &ccm.messages,
+ &ccm.n_messages,
+ &flags,
+ NULL, /* GCancellable* */
+ &error);
+
+ if (num_read < 0)
+ {
+ /* Just wait and try again */
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+ {
+ g_error_free (error);
+ return TRUE;
+ }
+
+ if (!should_suppress_request_error (error, 0))
+ g_message ("couldn't read from socket: %s", error->message);
+
+ cockpit_web_request_finish (self);
+ g_error_free (error);
+ return FALSE;
+ }
+
+ JsonObject *metadata = cockpit_memfd_read_json_from_control_messages (&ccm, &error);
+ if (metadata)
+ {
+ g_assert (G_IS_SOCKET_CONNECTION (self->io));
+ g_object_set_qdata_full (G_OBJECT (self->io),
+ g_quark_from_static_string ("metadata"),
+ metadata, (GDestroyNotify) json_object_unref);
+ }
+ else if (error != NULL)
+ {
+ g_warning ("Failed while reading metadata from new connection: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ /*
+ * TLS streams are guaranteed to start with octet 22.. this way we can distinguish them
+ * from regular HTTP requests
+ */
+ if (first_byte == 22 || first_byte == 0x80)
+ {
+ if (self->web_server->certificate == NULL)
+ {
+ g_warning ("Received unexpected TLS connection and no certificate was configured");
+ cockpit_web_request_finish (self);
+ return FALSE;
+ }
+
+ tls_stream = g_tls_server_connection_new (self->io,
+ self->web_server->certificate,
+ &error);
+ if (tls_stream == NULL)
+ {
+ g_warning ("couldn't create new TLS stream: %s", error->message);
+ cockpit_web_request_finish (self);
+ g_error_free (error);
+ return FALSE;
+ }
+
+ if (cockpit_webserver_want_certificate)
+ {
+ g_object_set (tls_stream, "authentication-mode", G_TLS_AUTHENTICATION_REQUESTED, NULL);
+ g_signal_connect (tls_stream, "accept-certificate", G_CALLBACK (cockpit_web_request_on_accept_certificate), NULL);
+ }
+
+ g_object_unref (self->io);
+ self->io = G_IO_STREAM (tls_stream);
+ }
+ else
+ {
+ if (self->web_server->certificate || self->web_server->flags & COCKPIT_WEB_SERVER_REDIRECT_TLS)
+ {
+ /* non-TLS stream; defer redirection check until after header parsing */
+ if (cockpit_web_server_get_flags (self->web_server) & COCKPIT_WEB_SERVER_REDIRECT_TLS)
+ self->check_tls_redirect = TRUE;
+ }
+ }
+
+ cockpit_web_request_start_input (self);
+
+ /* No longer run *this* source */
+ return FALSE;
+}
+
+static gboolean
+cockpit_web_request_on_timeout (gpointer data)
+{
+ CockpitWebRequest *self = data;
+ if (self->eof_okay)
+ g_debug ("request timed out, closing");
+ else
+ g_message ("request timed out, closing");
+ cockpit_web_request_finish (self);
+ return FALSE;
+}
+
+static void
+cockpit_web_request_start (CockpitWebServer *web_server,
+ GIOStream *io,
+ gboolean first)
+{
+ GSocketConnection *connection;
+ GSocket *socket;
+
+ CockpitWebRequest *self = g_new0 (CockpitWebRequest, 1);
+ self->web_server = web_server;
+ self->io = g_object_ref (io);
+ self->buffer = g_byte_array_new ();
+
+ /* Right before a request, EOF is not unexpected */
+ self->eof_okay = TRUE;
+
+ self->timeout = g_timeout_source_new_seconds (cockpit_webserver_request_timeout);
+ g_source_set_callback (self->timeout, cockpit_web_request_on_timeout, self, NULL);
+ g_source_attach (self->timeout, web_server->main_context);
+
+ if (first)
+ {
+ connection = G_SOCKET_CONNECTION (io);
+ socket = g_socket_connection_get_socket (connection);
+ g_socket_set_blocking (socket, FALSE);
+
+ self->source = g_socket_create_source (g_socket_connection_get_socket (connection),
+ G_IO_IN, NULL);
+ g_source_set_callback (self->source, (GSourceFunc)cockpit_web_request_on_socket_input, self, NULL);
+ g_source_attach (self->source, web_server->main_context);
+ }
+ else
+ cockpit_web_request_start_input (self);
+
+ /* Owns the request */
+ g_hash_table_add (web_server->requests, self);
+}
+
+CockpitWebResponse *
+cockpit_web_request_respond (CockpitWebRequest *self)
+{
+ return cockpit_web_response_new (self->io, self->original_path, self->path, self->headers,
+ self->method, cockpit_web_request_get_protocol (self));
+}
+
+const gchar *
+cockpit_web_request_get_original_path (CockpitWebRequest *self)
+{
+ return self->original_path;
+}
+
+const gchar *
+cockpit_web_request_get_path (CockpitWebRequest *self)
+{
+ return self->path;
+}
+
+const gchar *
+cockpit_web_request_get_query (CockpitWebRequest *self)
+{
+ return self->query;
+}
+
+const gchar *
+cockpit_web_request_get_method (CockpitWebRequest *self)
+{
+ return self->method;
+}
+
+GByteArray *
+cockpit_web_request_get_buffer (CockpitWebRequest *self)
+{
+ return self->buffer;
+}
+
+GHashTable *
+cockpit_web_request_get_headers (CockpitWebRequest *self)
+{
+ return self->headers;
+}
+
+const gchar *
+cockpit_web_request_lookup_header (CockpitWebRequest *self,
+ const gchar *header)
+{
+ if (!self->headers)
+ return NULL;
+
+ return g_hash_table_lookup (self->headers, header);
+}
+
+gchar *
+cockpit_web_request_parse_cookie (CockpitWebRequest *self,
+ const gchar *name)
+{
+ if (!self->headers)
+ return NULL;
+
+ return cockpit_web_server_parse_cookie (self->headers, name);
+}
+
+GIOStream *
+cockpit_web_request_get_io_stream (CockpitWebRequest *self)
+{
+ return self->io;
+}
+
+const gchar *
+cockpit_web_request_get_host (CockpitWebRequest *self)
+{
+ return self->host;
+}
+
+const gchar *
+cockpit_web_request_get_protocol (CockpitWebRequest *self)
+{
+ if (G_IS_TLS_CONNECTION (self->io))
+ return "https";
+
+ if (self->web_server && self->web_server->flags & COCKPIT_WEB_SERVER_FOR_TLS_PROXY)
+ return "https";
+
+ if (self->web_server && self->web_server->protocol_header)
+ {
+ const gchar *protocol = g_hash_table_lookup (self->headers, self->web_server->protocol_header);
+ if (protocol)
+ return protocol;
+ }
+
+ return "http";
+}
+
+gchar *
+cockpit_web_request_get_remote_address (CockpitWebRequest *self)
+{
+ if (self->web_server && self->web_server->forwarded_for_header)
+ {
+ const gchar *forwarded_header = g_hash_table_lookup (self->headers, self->web_server->forwarded_for_header);
+ if (forwarded_header && forwarded_header[0])
+ {
+ /* This isn't really standardised, but in practice, it's a
+ * space separated list and the last item is from the
+ * immediately upstream server.
+ */
+ const gchar *last_space = strrchr (forwarded_header, ' ');
+ if (last_space)
+ return g_strdup (last_space + 1);
+ else
+ return g_strdup (forwarded_header);
+ }
+ }
+
+ if (self->io == NULL)
+ return NULL;
+
+ JsonObject *metadata = g_object_get_qdata (G_OBJECT (self->io), g_quark_from_static_string ("metadata"));
+ if (metadata)
+ {
+ const gchar *tmp;
+ if (cockpit_json_get_string (metadata, "origin-ip", NULL, &tmp))
+ return g_strdup (tmp);
+ }
+
+ g_autoptr(GIOStream) base = NULL;
+ if (G_IS_TLS_CONNECTION (self->io))
+ g_object_get (self->io, "base-io-stream", &base, NULL);
+ else
+ base = g_object_ref (self->io);
+
+ /* This is definitely a socket */
+ g_return_val_if_fail (G_IS_SOCKET_CONNECTION (base), NULL);
+
+ /* ...but it might be a unix socket. NB: GInetSocketAddress includes IPv6. */
+ g_autoptr(GSocketAddress) remote = g_socket_connection_get_remote_address (G_SOCKET_CONNECTION (base), NULL);
+ if (remote && G_IS_INET_SOCKET_ADDRESS (remote))
+ return g_inet_address_to_string (g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (remote)));
+
+ return NULL;
+}
+
+const gchar *
+cockpit_web_request_get_client_certificate (CockpitWebRequest *self)
+{
+ if (self->io == NULL)
+ return NULL;
+
+ JsonObject *metadata = g_object_get_qdata (G_OBJECT (self->io), g_quark_from_static_string ("metadata"));
+ if (metadata == NULL)
+ return NULL;
+
+ const gchar *client_certificate = NULL;
+ cockpit_json_get_string (metadata, "client-certificate", NULL, &client_certificate);
+ return client_certificate;
+}
+
+gboolean
+cockpit_web_request_accepts_encoding (CockpitWebRequest *self,
+ const gchar *encoding)
+{
+ const gchar *accept = g_hash_table_lookup (self->headers, "Accept-Encoding");
+ if (!accept)
+ return TRUE;
+ g_auto(GStrv) encodings = cockpit_web_server_parse_accept_list (accept, NULL);
+ return g_strv_contains ((const gchar **) encodings, encoding) ||
+ g_strv_contains ((const gchar **) encodings, "*");
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
diff --git a/src/common/cockpitwebserver.h b/src/common/cockpitwebserver.h
new file mode 100644
index 0000000..30c02bd
--- /dev/null
+++ b/src/common/cockpitwebserver.h
@@ -0,0 +1,139 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_WEB_SERVER_H__
+#define __COCKPIT_WEB_SERVER_H__
+
+#include <gio/gio.h>
+
+#include "cockpitwebresponse.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_WEB_REQUEST (cockpit_web_request_get_type ())
+typedef struct _CockpitWebRequest CockpitWebRequest;
+
+GType
+cockpit_web_request_get_type (void);
+
+CockpitWebResponse *
+cockpit_web_request_respond (CockpitWebRequest *self);
+
+const gchar *
+cockpit_web_request_get_original_path (CockpitWebRequest *self);
+
+const gchar *
+cockpit_web_request_get_path (CockpitWebRequest *self);
+
+const gchar *
+cockpit_web_request_get_query (CockpitWebRequest *self);
+
+const gchar *
+cockpit_web_request_get_method (CockpitWebRequest *self);
+
+GHashTable *
+cockpit_web_request_get_headers (CockpitWebRequest *self);
+
+const gchar *
+cockpit_web_request_lookup_header (CockpitWebRequest *self,
+ const gchar *header);
+
+gchar *
+cockpit_web_request_parse_cookie (CockpitWebRequest *self,
+ const gchar *name);
+
+GIOStream *
+cockpit_web_request_get_io_stream (CockpitWebRequest *self);
+
+GHashTable *
+cockpit_web_request_get_headers (CockpitWebRequest *self);
+
+GByteArray *
+cockpit_web_request_get_buffer (CockpitWebRequest *self);
+
+const gchar *
+cockpit_web_request_get_host (CockpitWebRequest *self);
+
+const gchar *
+cockpit_web_request_get_protocol (CockpitWebRequest *self);
+
+gchar *
+cockpit_web_request_get_remote_address (CockpitWebRequest *self);
+
+const gchar *
+cockpit_web_request_get_client_certificate (CockpitWebRequest *self);
+
+gboolean
+cockpit_web_request_accepts_encoding (CockpitWebRequest *self,
+ const gchar *encoding);
+
+#define COCKPIT_TYPE_WEB_SERVER (cockpit_web_server_get_type ())
+G_DECLARE_FINAL_TYPE(CockpitWebServer, cockpit_web_server, COCKPIT, WEB_SERVER, GObject)
+
+extern guint cockpit_webserver_request_timeout;
+
+typedef enum {
+ COCKPIT_WEB_SERVER_NONE = 0,
+ COCKPIT_WEB_SERVER_FOR_TLS_PROXY = 1 << 0,
+ /* http → https redirection for non-localhost addresses */
+ COCKPIT_WEB_SERVER_REDIRECT_TLS = 1 << 1,
+ COCKPIT_WEB_SERVER_FLAGS_MAX = 1 << 2
+} CockpitWebServerFlags;
+
+
+CockpitWebServer * cockpit_web_server_new (GTlsCertificate *certificate,
+ CockpitWebServerFlags flags);
+
+void cockpit_web_server_start (CockpitWebServer *self);
+
+GHashTable * cockpit_web_server_new_table (void);
+
+gchar * cockpit_web_server_parse_cookie (GHashTable *headers,
+ const gchar *name);
+
+gchar ** cockpit_web_server_parse_accept_list (const gchar *accept,
+ const gchar *first);
+
+CockpitWebServerFlags cockpit_web_server_get_flags (CockpitWebServer *self);
+
+guint16
+cockpit_web_server_add_inet_listener (CockpitWebServer *self,
+ const gchar *address,
+ guint16 port,
+ GError **error);
+
+gboolean
+cockpit_web_server_add_fd_listener (CockpitWebServer *self,
+ int fd,
+ GError **error);
+
+GIOStream *
+cockpit_web_server_connect (CockpitWebServer *self);
+
+void
+cockpit_web_server_set_protocol_header (CockpitWebServer *self,
+ const gchar *protocol_header);
+
+void
+cockpit_web_server_set_forwarded_for_header (CockpitWebServer *self,
+ const gchar *forwarded_for_header);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_WEB_SERVER_H__ */
diff --git a/src/common/fail.html b/src/common/fail.html
new file mode 100644
index 0000000..092f2f8
--- /dev/null
+++ b/src/common/fail.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>@@message@@</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <style>
+ body {
+ margin: 0;
+ font-family: "RedHatDisplay", "Open Sans", Helvetica, Arial, sans-serif;
+ font-size: 12px;
+ line-height: 1.66666667;
+ color: #333333;
+ background-color: #f5f5f5;
+ }
+ img {
+ border: 0;
+ vertical-align: middle;
+ }
+ h1 {
+ font-weight: 300;
+ }
+ p {
+ margin: 0 0 10px;
+ }
+ @font-face {
+ font-family: 'RedHatDisplay';
+ font-style: normal;
+ font-weight: 300;
+ src: url('/cockpit/static/fonts/RedHatDisplay-Medium.woff2') format('woff2');
+ }
+ .blank-slate-pf {
+ text-align: center;
+ padding: 90px 120px;
+ }
+ </style>
+</head>
+<body>
+ <div class="blank-slate-pf">
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAWGSURBVGiBzZtNaJxFGMd/z2R3YxHbtZa2pqdcPInS9CCUov3QiuhF2hIUjOZrd5NA9VoPuiJSTx6S0s27SVxJ8aOpPSgqpbZVlIIIJirqtRdNpBU3FUqhm53HQzZpPjfvm51J8oeFZd5n5v/8eWbemXfmGcETBgcHt1prm1S1CWgSkUZV3QIkgS0Vs5vAZOV3DRhV1VFr7Wh3d3fRh1/isrFcLrerrq7uKHBMVffW2P4fqjocj8fPtLe3jzty0Y3gfD7/pLX2hIjsB4yLNufAAleste90dXV9W2tjNQkOgmCfqr4lIgdrdSQMROSqtfbNTCZzedVtrKbSwMDAjnK5nBOR51dLXAtU9Yt4PJ5eTVePLDgIgmPAaWBb1LqOURSRV1Op1JkolUILHhkZSRSLxUHgpciu+cX7QCadTpfCGIcSPDQ0dF+pVDonIk/X5Jo/XAaOpNPpmysZrig4l8vtMsZcAB524ZlH/AI8k06nJ6oZVZ1Cent7NxtjvmTjiwV4FLhYKBSS1YyWjXBlzH4FHKrRkY82bdqUamlpuVXNaHh4+N7bt2/ngRdr5LsIPLfcmF42wsVisZ/axaKqV1YSC9DS0nJLVa/UygccZnoWWRJLCu7v728GWh2QIyKJCLb3uOAEOirT5yIsEhwEwTYR6XVEDFAfwdaVYIDTAwMDOxYWLhIsIjlgu0Pi0CKi9IYQ2Gat7VtYOE9wZW181CFpJBHWWpcRBjiWz+efmFswT7CIvO2YEFUN3aWNMVG6fyhYa9+YxzHzJwiCA6q63zUhEcawhwgjIgfnRnluhF93TVZBaBE+IgxgrT0xywEQBMGDwAEfZKq6nmMYABF5amhoqAHuRrgZqPNBFiVqviIMmFKpdAQqglW12RNRpJeWrwgDGGOaAczg4OBWEXnMF1GU1ZPHCKOqewuFQtJYa5twvHu5gCj0GI7SG1YBuXPnzm5jrd3jkSSSCIdr6eXa32NEZLdnktAiPEcYVd1tRKTRJwnr9/GwFBqNqlbdIXCAKIK9Rhi43zB91uMTGynCScPdgy1f2HARVs8kG0mwNUwfVfrERurSk4bpM1qfCCVYVQVwueOxFCY3TIT7+voSeFzxVVA0TJ+8+4TJZrOxEHa+xy+qes0Ao76Jdu7cueLiJpFIPOTbDxEZi6nqqIjfniQiP+bz+Z9VdWoZkzjQ5NWJaYzGYrHYWLlcVvyOn6Sn/bIo0EQiMWY6Ojr+BX5YZ2fWAldbW1snDYCInF1vb9YAZwFiAOVy+VNjzHu4z8ABQETax8fHP8hms3ap56oq+Xz+BeBDH/xAGTgPFYFdXV1/Ad94IqOaWAAR0YmJiU988QOXZw7KZyMqIu/6YqsmNorNamGtPTnzf1ZwKpW6BHzni3S9ICJX5ya0eT9bWm+IyNJnSzAb5c/X1COPEJHPOjs752UVLHorx+PxDuD6mnnlD/+USqXMwsJFgtva2m6o6mtr45M/iEhPT0/P3wvLl5x3M5nMxyIy7Io8m82uOL+HsYmAQiqVGlnqwbIkyWSy01FWDQ0NDa9UE6Sq0tDQ4Crz4BKQXu5h1Q+G3t7ezfX19d8Djzhyxjd+TyQS+1pbW5fd1KjajY4fP/6fMeZZ4DfnrrnHr7FY7HA1sRBi7dzZ2flnLBbbC1xw5pp7XAIeD5M/HTV9OA+8XItnHjAIdDtNH56LDZQgfl1EulOp1PkolVa1y5HL5baLSP96XQEAzsXj8Z62trYbUSvWfMkDyOIgCTUM1u2Sx0IEQXBARE6o6iHcbyKUga9V9WQmk6n5a87pxt2pU6ceiMfjR0SkxcFFrZ9E5IyqjqyU5R4F3nYqC4VCcmpqavYqnrW2UUSSTB/PzhzRzlzDK1K5igeMJhKJsZXm09XifyIP5eSF+BKpAAAAAElFTkSuQmC">
+ <h1>@@message@@</h1>
+ </div>
+</body>
+</html>
diff --git a/src/common/mock-content/directory/.empty b/src/common/mock-content/directory/.empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/common/mock-content/directory/.empty
diff --git a/src/common/mock-content/large.min.js.gz b/src/common/mock-content/large.min.js.gz
new file mode 100644
index 0000000..1b9a4b0
--- /dev/null
+++ b/src/common/mock-content/large.min.js.gz
Binary files differ
diff --git a/src/common/mock-content/test-file.txt b/src/common/mock-content/test-file.txt
new file mode 100644
index 0000000..a8508d5
--- /dev/null
+++ b/src/common/mock-content/test-file.txt
@@ -0,0 +1 @@
+A small test file
diff --git a/src/common/mock-content/test-file.txt.gz b/src/common/mock-content/test-file.txt.gz
new file mode 100644
index 0000000..bea8ed4
--- /dev/null
+++ b/src/common/mock-content/test-file.txt.gz
Binary files differ
diff --git a/src/common/mock-content/test-file.zh_CN.txt b/src/common/mock-content/test-file.zh_CN.txt
new file mode 100644
index 0000000..4c2ade7
--- /dev/null
+++ b/src/common/mock-content/test-file.zh_CN.txt
@@ -0,0 +1 @@
+A translated test file
diff --git a/src/common/mock-content/test.css b/src/common/mock-content/test.css
new file mode 100644
index 0000000..712b327
--- /dev/null
+++ b/src/common/mock-content/test.css
@@ -0,0 +1,3 @@
+#brand {
+ content: "${NAME} <b>${VARIANT}</b>";
+}
diff --git a/src/common/mock-content/test.wasm b/src/common/mock-content/test.wasm
new file mode 100644
index 0000000..731c5f7
--- /dev/null
+++ b/src/common/mock-content/test.wasm
@@ -0,0 +1 @@
+bl0b
diff --git a/src/common/mock-stderr b/src/common/mock-stderr
new file mode 100755
index 0000000..6775595
--- /dev/null
+++ b/src/common/mock-stderr
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+/usr/bin/printf 'line one\n' >&2
+/usr/bin/printf 'line ' >&2
+/usr/bin/printf 'two\nline ' >&2
+/usr/bin/printf 'three\nline four\nline five' >&2
+/usr/bin/printf '\nline six' >&2
diff --git a/src/common/preload-temp-home.c b/src/common/preload-temp-home.c
new file mode 100644
index 0000000..90b547e
--- /dev/null
+++ b/src/common/preload-temp-home.c
@@ -0,0 +1,62 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <dlfcn.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <assert.h>
+
+static void *
+get_libc_func(const char *f)
+{
+ void *fp;
+ fp = dlsym(RTLD_NEXT, f);
+ assert(fp);
+ return fp;
+}
+
+#define libc_func(name, rettype, ...) \
+ static rettype (*_ ## name) (__VA_ARGS__) = NULL; \
+ if (_ ## name == NULL) \
+ _ ## name = get_libc_func(#name);
+
+/**
+ * Change pw_dir to the value of $HOME for the current uid.
+ * This is useful for libssh's expansion of ~ to point to our temporary test
+ * $HOME instead of the real one from /etc/passwd.
+ *
+ */
+int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf, size_t buflen, struct passwd **result)
+{
+ int res;
+ libc_func(getpwuid_r, int, uid_t, struct passwd *, char*, size_t, struct passwd **);
+ res = _getpwuid_r(uid, pwd, buf, buflen, result);
+ if (res == 0 && uid == getuid()) {
+ /* fprintf(stderr, "temp-home wrapped getpwuid_r(uid %i): changing original home %s to $HOME %s\n", (int) uid, pwd->pw_dir, getenv("HOME")); */
+ /* note: in theory the caller might change this and thus change the
+ * environment, but this is only for the unit tests where we know that
+ * libssh doesn't do that, so avoid any unnecessary copying here */
+ pwd->pw_dir = getenv("HOME");
+ }
+ return res;
+}
diff --git a/src/common/test-authorize.c b/src/common/test-authorize.c
new file mode 100644
index 0000000..5ba5cb1
--- /dev/null
+++ b/src/common/test-authorize.c
@@ -0,0 +1,438 @@
+/*
+ * Copyright (c) 2014 Red Hat Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ * * Redistributions in binary form must reproduce the
+ * above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * * The names of contributors to this software may not be
+ * used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * Author: Stef Walter <stefw@redhat.com>
+ */
+
+#include "config.h"
+
+#include "testlib/retest.h"
+
+#include "cockpitauthorize.h"
+
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <err.h>
+#include <errno.h>
+#include <pwd.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+static const char *expect_message;
+static char *user;
+
+static void
+test_logger (const char *msg)
+{
+ assert (msg != NULL);
+
+ if (expect_message)
+ {
+ assert_str_contains (msg, expect_message);
+ expect_message = NULL;
+ }
+ else
+ {
+ warnx ("%s", msg);
+ }
+}
+
+static void
+setup (void *arg)
+{
+ struct passwd *pw;
+
+ expect_message = NULL;
+
+ pw = getpwuid (getuid ());
+ assert (pw != NULL);
+ user = strdup (pw->pw_name);
+ assert (user != NULL);
+}
+
+static void
+teardown (void *arg)
+{
+ if (expect_message)
+ assert_fail ("message didn't get logged", expect_message);
+ free (user);
+ user = NULL;
+}
+
+typedef struct {
+ const char *input;
+ const char *expected;
+ const char *ret;
+ int errn;
+} ChallengeFixture;
+
+static ChallengeFixture type_fixtures[] = {
+ { "valid", "valid", "", 0 },
+ { ":invalid", NULL, NULL, EINVAL },
+ { "Basic more-data", "basic", "more-data", 0 },
+ { "Basic more-data", "basic", "more-data", 0 },
+ { "valid:test", "valid", "test", 0 },
+ { "valid1:", "valid1", "", 0 },
+ { "valid2:test:test", "valid2", "test:test", 0 },
+ { NULL },
+};
+
+static void
+test_type (void *data)
+{
+ ChallengeFixture *fix = data;
+ const char *result;
+ char *type;
+
+ result = cockpit_authorize_type (fix->input, &type);
+ if (fix->errn != 0)
+ assert_num_eq (errno, fix->errn);
+ if (fix->ret)
+ {
+ assert_str_eq (result, fix->ret);
+ assert_str_eq (type, fix->expected);
+ free (type);
+ }
+ else
+ {
+ assert (result == NULL);
+ }
+}
+
+static ChallengeFixture subject_fixtures[] = {
+ { "valid:73637275666679:", "73637275666679", "", 0 },
+ { "valid:73637275666679:more-data", "73637275666679", "more-data", 0 },
+ { "valid:scruffy:", "scruffy", "", 0 },
+ { "X-Conversation conversationtoken more-data", "conversationtoken", "more-data", 0 },
+ { "X-Conversation conversationtoken more-data", "conversationtoken", "more-data", 0 },
+ { "invalid:", "73637275666679", NULL, EINVAL },
+ { "invalid", NULL, NULL, EINVAL },
+ { NULL },
+};
+
+static void
+test_subject (void *data)
+{
+ ChallengeFixture *fix = data;
+ const char *result;
+ char *subject = NULL;
+
+ if (fix->ret == NULL)
+ expect_message = "\"authorize\" message";
+
+ result = cockpit_authorize_subject (fix->input, &subject);
+ if (fix->errn != 0)
+ assert_num_eq (errno, fix->errn);
+ if (fix->ret)
+ {
+ assert_str_eq (result, fix->ret);
+ assert_str_eq (subject, fix->expected);
+ free (subject);
+ }
+ else
+ {
+ assert (result == NULL);
+ }
+}
+
+static ChallengeFixture basic_fixtures[] = {
+ { "Basic c2NydWZmeTp6ZXJvZw==", "scruffy", "zerog", 0 },
+ { "Basic!c2NydWZmeTp6ZXJvZw==", NULL, NULL, EINVAL },
+ { "Basic c2NydWZ!!eXplcm9n", NULL, NULL, EINVAL },
+ { "Basic c2NydWZmeXplcm9n", NULL, NULL, EINVAL },
+ { "Basic!c2NydWZmeTp6ZXJvZw==", NULL, NULL, EINVAL },
+ { "Basic", NULL, "", 0 },
+ { NULL },
+};
+
+static void
+test_parse_basic (void *data)
+{
+ ChallengeFixture *fix = data;
+ char *user = "blah";
+ char *password = NULL;
+
+ if (fix->ret == NULL)
+ expect_message = "invalid";
+
+ password = cockpit_authorize_parse_basic (fix->input, &user);
+ if (fix->errn != 0)
+ assert_num_eq (errno, fix->errn);
+ if (fix->ret)
+ {
+ assert_str_eq (password, fix->ret);
+ if (fix->expected)
+ assert_str_eq (user, fix->expected);
+ else
+ assert (user == NULL);
+ free (password);
+ free (user);
+ }
+ else
+ {
+ assert (password == NULL);
+ assert_str_eq (user, "blah"); /* not reassigned */
+ }
+
+ if (fix->ret == NULL)
+ expect_message = "invalid";
+
+ password = cockpit_authorize_parse_basic (fix->input, NULL);
+ if (fix->errn != 0)
+ assert_num_eq (errno, fix->errn);
+ if (fix->ret)
+ {
+ assert_str_eq (password, fix->ret);
+ free (password);
+ }
+ else
+ {
+ assert (password == NULL);
+ }
+}
+
+typedef struct {
+ const char *input;
+ size_t length;
+ const char *ret;
+ int errn;
+} NegotiateFixture;
+
+static NegotiateFixture parse_negotiate_fixtures[] = {
+ { "Negotiate c2NydWZmeTp6ZXJvZw==", 13, "scruffy:zerog", 0 },
+ { "Negotiate!c2NydWZmeTp6ZXJvZw==", 0, NULL, EINVAL },
+ { "Negotiate c2Nyd!!ZmeTp6ZXJvZw==", 0, NULL, EINVAL },
+ { "Negotiate!c2NydWZmeTp6ZXJvZw==", 0, NULL, EINVAL },
+ { "Negotiate", 0, "", 0 },
+ { NULL },
+};
+
+static void
+test_parse_negotiate (void *data)
+{
+ NegotiateFixture *fix = data;
+ size_t length = 0xFFFFDD;
+ void *result = NULL;
+
+ if (fix->ret == NULL)
+ expect_message = "invalid";
+
+ result = cockpit_authorize_parse_negotiate (fix->input, &length);
+ if (fix->errn != 0)
+ assert_num_eq (errno, fix->errn);
+ if (fix->ret)
+ {
+ assert_num_eq (fix->length, length);
+ assert (memcmp (result, fix->ret, length) == 0);
+ free (result);
+ }
+ else
+ {
+ assert (result == NULL);
+ assert_num_eq (length, 0xFFFFDD); /* not reassigned */
+ }
+
+ if (fix->ret == NULL)
+ expect_message = "invalid";
+
+ result = cockpit_authorize_parse_negotiate (fix->input, NULL);
+ if (fix->errn != 0)
+ assert_num_eq (errno, fix->errn);
+ if (fix->ret)
+ {
+ assert (memcmp (result, fix->ret, length) == 0);
+ free (result);
+ }
+ else
+ {
+ assert (result == NULL);
+ }
+}
+
+static NegotiateFixture build_negotiate_fixtures[] = {
+ { "scruffy:zerog", 13, "Negotiate c2NydWZmeTp6ZXJvZw==", 0 },
+ { NULL, 0, "Negotiate", 0, },
+ { NULL },
+};
+
+static void
+test_build_negotiate (void *data)
+{
+ NegotiateFixture *fix = data;
+ char *result = NULL;
+
+ if (fix->ret == NULL)
+ expect_message = "invalid";
+
+ result = cockpit_authorize_build_negotiate (fix->input, fix->length);
+ if (fix->errn != 0)
+ assert_num_eq (errno, fix->errn);
+ if (fix->ret)
+ {
+ assert_str_eq (result, fix->ret);
+ free (result);
+ }
+ else
+ {
+ assert (result == NULL);
+ }
+}
+
+typedef struct {
+ const char *input;
+ const char *conversation;
+ const char *ret;
+ int errn;
+} XConversationFixture;
+
+static XConversationFixture parse_x_conversation_fixtures[] = {
+ { "X-Conversation abcdefghi c2NydWZmeTp6ZXJvZw==", NULL, "scruffy:zerog", 0 },
+ { "X-Conversation abcdefghi", NULL, "", 0 },
+ { "X-Conversation abcdefghi c2NydW!!meTp6ZXJvZw==", NULL, NULL, EINVAL },
+ { NULL },
+};
+
+static void
+test_parse_x_conversation (void *data)
+{
+ XConversationFixture *fix = data;
+ char *result = NULL;
+
+ if (fix->ret == NULL)
+ expect_message = "invalid";
+
+ result = cockpit_authorize_parse_x_conversation (fix->input, NULL);
+ if (fix->errn != 0)
+ assert_num_eq (errno, fix->errn);
+ if (fix->ret)
+ {
+ assert_str_eq (result, fix->ret);
+ free (result);
+ }
+ else
+ {
+ assert (result == NULL);
+ }
+}
+
+static XConversationFixture build_x_conversation_fixtures[] = {
+ { "scruffy:zerog", "abcdefghi", "X-Conversation abcdefghi c2NydWZmeTp6ZXJvZw==", 0 },
+ { "scruffy:zerog", NULL, " c2NydWZmeTp6ZXJvZw==", 0 },
+ { "", "abcdefghi", "X-Conversation abcdefghi", 0 },
+ { "scruffy:zerog", "", NULL, EINVAL },
+ { NULL },
+};
+
+static void
+test_build_x_conversation (void *data)
+{
+ XConversationFixture *fix = data;
+ char *conversation = NULL;
+ char *result = NULL;
+
+ if (fix->ret == NULL)
+ expect_message = "invalid";
+
+ if (fix->conversation)
+ conversation = strdup (fix->conversation);
+
+ result = cockpit_authorize_build_x_conversation (fix->input, &conversation);
+ if (fix->errn != 0)
+ assert_num_eq (errno, fix->errn);
+ if (fix->ret)
+ {
+ if (strstr (fix->ret, "X-Conversation"))
+ assert_str_eq (result, fix->ret);
+ else
+ assert (strstr (result, fix->ret));
+ free (result);
+ }
+ else
+ {
+ assert (result == NULL);
+ }
+
+ free (conversation);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ int i;
+
+ /* Some initial preparation */
+ signal (SIGPIPE, SIG_IGN);
+ cockpit_authorize_logger (test_logger, 0);
+
+ re_fixture (setup, teardown);
+
+ for (i = 0; type_fixtures[i].input != NULL; i++)
+ {
+ re_testx (test_type, type_fixtures + i,
+ "/authorize/type/%s", type_fixtures[i].input);
+ }
+ for (i = 0; subject_fixtures[i].input != NULL; i++)
+ {
+ re_testx (test_subject, subject_fixtures + i,
+ "/authorize/subject/%s", subject_fixtures[i].input);
+ }
+ for (i = 0; basic_fixtures[i].input != NULL; i++)
+ {
+ re_testx (test_parse_basic, basic_fixtures + i,
+ "/authorize/basic/%s", basic_fixtures[i].input);
+ }
+ for (i = 0; parse_negotiate_fixtures[i].input != NULL; i++)
+ {
+ re_testx (test_parse_negotiate, parse_negotiate_fixtures + i,
+ "/authorize/negotiate/parse/%s", parse_negotiate_fixtures[i].input);
+ }
+ for (i = 0; build_negotiate_fixtures[i].ret != NULL; i++)
+ {
+ re_testx (test_build_negotiate, build_negotiate_fixtures + i,
+ "/authorize/negotiate/build/%s", build_negotiate_fixtures[i].ret);
+ }
+ for (i = 0; parse_x_conversation_fixtures[i].input != NULL; i++)
+ {
+ re_testx (test_parse_x_conversation, parse_x_conversation_fixtures + i,
+ "/authorize/x-conversation/parse/%s", parse_x_conversation_fixtures[i].input);
+ }
+ for (i = 0; build_x_conversation_fixtures[i].input || build_x_conversation_fixtures[i].ret; i++)
+ {
+ re_testx (test_build_x_conversation, build_x_conversation_fixtures + i,
+ "/authorize/x-conversation/build/%s", build_x_conversation_fixtures[i].input);
+ }
+
+ return re_test_run (argc, argv);
+}
diff --git a/src/common/test-base64.c b/src/common/test-base64.c
new file mode 100644
index 0000000..2eae54a
--- /dev/null
+++ b/src/common/test-base64.c
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2013 Red Hat Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ * * Redistributions in binary form must reproduce the
+ * above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * * The names of contributors to this software may not be
+ * used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * Author: Stef Walter <stefw@redhat.com>
+ */
+
+#include "config.h"
+#include "testlib/retest.h"
+
+#include "cockpitbase64.h"
+
+#include <assert.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+static void
+check_decode_msg (const char *file,
+ int line,
+ const char *function,
+ const char *input,
+ ssize_t input_len,
+ const unsigned char *expected,
+ ssize_t expected_len)
+{
+ unsigned char decoded[8192];
+ int length;
+
+ if (input_len < 0)
+ input_len = strlen (input);
+ if (expected_len < 0)
+ expected_len = strlen ((char *)expected);
+ length = cockpit_base64_pton (input, input_len, decoded, sizeof (decoded));
+
+ if (expected == NULL)
+ {
+ if (length >= 0)
+ re_test_fail (file, line, function, "decoding should have failed");
+
+ }
+ else
+ {
+ if (length < 0)
+ re_test_fail (file, line, function, "decoding failed");
+ if (expected_len != length)
+ {
+ re_test_fail (file, line, function, "wrong length: (%lu != %lu)",
+ (unsigned long)expected_len, (unsigned long)length);
+ }
+ if (memcmp (decoded, expected, length) != 0)
+ re_test_fail (file, line, function, "decoded wrong");
+ }
+}
+
+#define check_decode_success(input, input_len, expected, expected_len) \
+ check_decode_msg (__FILE__, __LINE__, __FUNCTION__, input, input_len, expected, expected_len)
+
+#define check_decode_failure(input, input_len) \
+ check_decode_msg (__FILE__, __LINE__, __FUNCTION__, input, input_len, NULL, 0)
+
+static void
+test_decode_simple (void)
+{
+ check_decode_success ("", 0, (unsigned char *)"", 0);
+ check_decode_success ("MQ==", 0, (unsigned char *)"1", 0);
+ check_decode_success ("YmxhaAo=", -1, (unsigned char *)"blah\n", -1);
+ check_decode_success ("bGVlbGEK", -1, (unsigned char *)"leela\n", -1);
+ check_decode_success ("bGVlbG9vCg==", -1, (unsigned char *)"leeloo\n", -1);
+}
+
+static void
+test_decode_thawte (void)
+{
+ const char *input =
+ "MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB"
+ "rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf"
+ "Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw"
+ "MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV"
+ "BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa"
+ "Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl"
+ "LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u"
+ "MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl"
+ "ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz"
+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm"
+ "gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8"
+ "YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf"
+ "b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9"
+ "9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S"
+ "zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk"
+ "OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV"
+ "HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA"
+ "2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW"
+ "oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu"
+ "t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c"
+ "KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM"
+ "m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu"
+ "MdRAGmI0Nj81Aa6sY6A=";
+
+ const unsigned char output[] = {
+ 0x30, 0x82, 0x04, 0x2a, 0x30, 0x82, 0x03, 0x12, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x60,
+ 0x01, 0x97, 0xb7, 0x46, 0xa7, 0xea, 0xb4, 0xb4, 0x9a, 0xd6, 0x4b, 0x2f, 0xf7, 0x90, 0xfb, 0x30,
+ 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x81,
+ 0xae, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31, 0x15,
+ 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0c, 0x74, 0x68, 0x61, 0x77, 0x74, 0x65, 0x2c,
+ 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, 0x28, 0x30, 0x26, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x1f,
+ 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x53, 0x65,
+ 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x44, 0x69, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x31,
+ 0x38, 0x30, 0x36, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x2f, 0x28, 0x63, 0x29, 0x20, 0x32, 0x30,
+ 0x30, 0x38, 0x20, 0x74, 0x68, 0x61, 0x77, 0x74, 0x65, 0x2c, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x20,
+ 0x2d, 0x20, 0x46, 0x6f, 0x72, 0x20, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64,
+ 0x20, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x31, 0x24, 0x30, 0x22, 0x06, 0x03, 0x55,
+ 0x04, 0x03, 0x13, 0x1b, 0x74, 0x68, 0x61, 0x77, 0x74, 0x65, 0x20, 0x50, 0x72, 0x69, 0x6d, 0x61,
+ 0x72, 0x79, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2d, 0x20, 0x47, 0x33, 0x30,
+ 0x1e, 0x17, 0x0d, 0x30, 0x38, 0x30, 0x34, 0x30, 0x32, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a,
+ 0x17, 0x0d, 0x33, 0x37, 0x31, 0x32, 0x30, 0x31, 0x32, 0x33, 0x35, 0x39, 0x35, 0x39, 0x5a, 0x30,
+ 0x81, 0xae, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x55, 0x53, 0x31,
+ 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x13, 0x0c, 0x74, 0x68, 0x61, 0x77, 0x74, 0x65,
+ 0x2c, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x31, 0x28, 0x30, 0x26, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13,
+ 0x1f, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x53,
+ 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x20, 0x44, 0x69, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e,
+ 0x31, 0x38, 0x30, 0x36, 0x06, 0x03, 0x55, 0x04, 0x0b, 0x13, 0x2f, 0x28, 0x63, 0x29, 0x20, 0x32,
+ 0x30, 0x30, 0x38, 0x20, 0x74, 0x68, 0x61, 0x77, 0x74, 0x65, 0x2c, 0x20, 0x49, 0x6e, 0x63, 0x2e,
+ 0x20, 0x2d, 0x20, 0x46, 0x6f, 0x72, 0x20, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65,
+ 0x64, 0x20, 0x75, 0x73, 0x65, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x31, 0x24, 0x30, 0x22, 0x06, 0x03,
+ 0x55, 0x04, 0x03, 0x13, 0x1b, 0x74, 0x68, 0x61, 0x77, 0x74, 0x65, 0x20, 0x50, 0x72, 0x69, 0x6d,
+ 0x61, 0x72, 0x79, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, 0x41, 0x20, 0x2d, 0x20, 0x47, 0x33,
+ 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01,
+ 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01,
+ 0x00, 0xb2, 0xbf, 0x27, 0x2c, 0xfb, 0xdb, 0xd8, 0x5b, 0xdd, 0x78, 0x7b, 0x1b, 0x9e, 0x77, 0x66,
+ 0x81, 0xcb, 0x3e, 0xbc, 0x7c, 0xae, 0xf3, 0xa6, 0x27, 0x9a, 0x34, 0xa3, 0x68, 0x31, 0x71, 0x38,
+ 0x33, 0x62, 0xe4, 0xf3, 0x71, 0x66, 0x79, 0xb1, 0xa9, 0x65, 0xa3, 0xa5, 0x8b, 0xd5, 0x8f, 0x60,
+ 0x2d, 0x3f, 0x42, 0xcc, 0xaa, 0x6b, 0x32, 0xc0, 0x23, 0xcb, 0x2c, 0x41, 0xdd, 0xe4, 0xdf, 0xfc,
+ 0x61, 0x9c, 0xe2, 0x73, 0xb2, 0x22, 0x95, 0x11, 0x43, 0x18, 0x5f, 0xc4, 0xb6, 0x1f, 0x57, 0x6c,
+ 0x0a, 0x05, 0x58, 0x22, 0xc8, 0x36, 0x4c, 0x3a, 0x7c, 0xa5, 0xd1, 0xcf, 0x86, 0xaf, 0x88, 0xa7,
+ 0x44, 0x02, 0x13, 0x74, 0x71, 0x73, 0x0a, 0x42, 0x59, 0x02, 0xf8, 0x1b, 0x14, 0x6b, 0x42, 0xdf,
+ 0x6f, 0x5f, 0xba, 0x6b, 0x82, 0xa2, 0x9d, 0x5b, 0xe7, 0x4a, 0xbd, 0x1e, 0x01, 0x72, 0xdb, 0x4b,
+ 0x74, 0xe8, 0x3b, 0x7f, 0x7f, 0x7d, 0x1f, 0x04, 0xb4, 0x26, 0x9b, 0xe0, 0xb4, 0x5a, 0xac, 0x47,
+ 0x3d, 0x55, 0xb8, 0xd7, 0xb0, 0x26, 0x52, 0x28, 0x01, 0x31, 0x40, 0x66, 0xd8, 0xd9, 0x24, 0xbd,
+ 0xf6, 0x2a, 0xd8, 0xec, 0x21, 0x49, 0x5c, 0x9b, 0xf6, 0x7a, 0xe9, 0x7f, 0x55, 0x35, 0x7e, 0x96,
+ 0x6b, 0x8d, 0x93, 0x93, 0x27, 0xcb, 0x92, 0xbb, 0xea, 0xac, 0x40, 0xc0, 0x9f, 0xc2, 0xf8, 0x80,
+ 0xcf, 0x5d, 0xf4, 0x5a, 0xdc, 0xce, 0x74, 0x86, 0xa6, 0x3e, 0x6c, 0x0b, 0x53, 0xca, 0xbd, 0x92,
+ 0xce, 0x19, 0x06, 0x72, 0xe6, 0x0c, 0x5c, 0x38, 0x69, 0xc7, 0x04, 0xd6, 0xbc, 0x6c, 0xce, 0x5b,
+ 0xf6, 0xf7, 0x68, 0x9c, 0xdc, 0x25, 0x15, 0x48, 0x88, 0xa1, 0xe9, 0xa9, 0xf8, 0x98, 0x9c, 0xe0,
+ 0xf3, 0xd5, 0x31, 0x28, 0x61, 0x11, 0x6c, 0x67, 0x96, 0x8d, 0x39, 0x99, 0xcb, 0xc2, 0x45, 0x24,
+ 0x39, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x42, 0x30, 0x40, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x1d,
+ 0x13, 0x01, 0x01, 0xff, 0x04, 0x05, 0x30, 0x03, 0x01, 0x01, 0xff, 0x30, 0x0e, 0x06, 0x03, 0x55,
+ 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, 0x03, 0x02, 0x01, 0x06, 0x30, 0x1d, 0x06, 0x03, 0x55,
+ 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, 0xad, 0x6c, 0xaa, 0x94, 0x60, 0x9c, 0xed, 0xe4, 0xff, 0xfa,
+ 0x3e, 0x0a, 0x74, 0x2b, 0x63, 0x03, 0xf7, 0xb6, 0x59, 0xbf, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86,
+ 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x1a, 0x40,
+ 0xd8, 0x95, 0x65, 0xac, 0x09, 0x92, 0x89, 0xc6, 0x39, 0xf4, 0x10, 0xe5, 0xa9, 0x0e, 0x66, 0x53,
+ 0x5d, 0x78, 0xde, 0xfa, 0x24, 0x91, 0xbb, 0xe7, 0x44, 0x51, 0xdf, 0xc6, 0x16, 0x34, 0x0a, 0xef,
+ 0x6a, 0x44, 0x51, 0xea, 0x2b, 0x07, 0x8a, 0x03, 0x7a, 0xc3, 0xeb, 0x3f, 0x0a, 0x2c, 0x52, 0x16,
+ 0xa0, 0x2b, 0x43, 0xb9, 0x25, 0x90, 0x3f, 0x70, 0xa9, 0x33, 0x25, 0x6d, 0x45, 0x1a, 0x28, 0x3b,
+ 0x27, 0xcf, 0xaa, 0xc3, 0x29, 0x42, 0x1b, 0xdf, 0x3b, 0x4c, 0xc0, 0x33, 0x34, 0x5b, 0x41, 0x88,
+ 0xbf, 0x6b, 0x2b, 0x65, 0xaf, 0x28, 0xef, 0xb2, 0xf5, 0xc3, 0xaa, 0x66, 0xce, 0x7b, 0x56, 0xee,
+ 0xb7, 0xc8, 0xcb, 0x67, 0xc1, 0xc9, 0x9c, 0x1a, 0x18, 0xb8, 0xc4, 0xc3, 0x49, 0x03, 0xf1, 0x60,
+ 0x0e, 0x50, 0xcd, 0x46, 0xc5, 0xf3, 0x77, 0x79, 0xf7, 0xb6, 0x15, 0xe0, 0x38, 0xdb, 0xc7, 0x2f,
+ 0x28, 0xa0, 0x0c, 0x3f, 0x77, 0x26, 0x74, 0xd9, 0x25, 0x12, 0xda, 0x31, 0xda, 0x1a, 0x1e, 0xdc,
+ 0x29, 0x41, 0x91, 0x22, 0x3c, 0x69, 0xa7, 0xbb, 0x02, 0xf2, 0xb6, 0x5c, 0x27, 0x03, 0x89, 0xf4,
+ 0x06, 0xea, 0x9b, 0xe4, 0x72, 0x82, 0xe3, 0xa1, 0x09, 0xc1, 0xe9, 0x00, 0x19, 0xd3, 0x3e, 0xd4,
+ 0x70, 0x6b, 0xba, 0x71, 0xa6, 0xaa, 0x58, 0xae, 0xf4, 0xbb, 0xe9, 0x6c, 0xb6, 0xef, 0x87, 0xcc,
+ 0x9b, 0xbb, 0xff, 0x39, 0xe6, 0x56, 0x61, 0xd3, 0x0a, 0xa7, 0xc4, 0x5c, 0x4c, 0x60, 0x7b, 0x05,
+ 0x77, 0x26, 0x7a, 0xbf, 0xd8, 0x07, 0x52, 0x2c, 0x62, 0xf7, 0x70, 0x63, 0xd9, 0x39, 0xbc, 0x6f,
+ 0x1c, 0xc2, 0x79, 0xdc, 0x76, 0x29, 0xaf, 0xce, 0xc5, 0x2c, 0x64, 0x04, 0x5e, 0x88, 0x36, 0x6e,
+ 0x31, 0xd4, 0x40, 0x1a, 0x62, 0x34, 0x36, 0x3f, 0x35, 0x01, 0xae, 0xac, 0x63, 0xa0,
+ };
+
+ check_decode_success (input, -1, output, sizeof (output));
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ re_test (test_decode_simple, "/base64/decode-simple");
+ re_test (test_decode_thawte, "/base64/decode-thawte");
+ return re_test_run (argc, argv);
+}
diff --git a/src/common/test-channel.c b/src/common/test-channel.c
new file mode 100644
index 0000000..cc5b0a8
--- /dev/null
+++ b/src/common/test-channel.c
@@ -0,0 +1,806 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitchannel.h"
+#include "cockpitjson.h"
+#include "cockpitpipe.h"
+#include "cockpitpipetransport.h"
+
+#include "testlib/cockpittest.h"
+#include "testlib/mock-pressure.h"
+#include "testlib/mock-transport.h"
+
+#include <json-glib/json-glib.h>
+
+#include <gio/gio.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <string.h>
+
+/* ----------------------------------------------------------------------------
+ * Mock
+ */
+
+static GType mock_echo_channel_get_type (void) G_GNUC_CONST;
+
+typedef struct {
+ CockpitChannel parent;
+ gboolean close_called;
+} MockEchoChannel;
+
+typedef CockpitChannelClass MockEchoChannelClass;
+
+G_DEFINE_TYPE (MockEchoChannel, mock_echo_channel, COCKPIT_TYPE_CHANNEL);
+
+static void
+mock_echo_channel_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ cockpit_channel_send (channel, message, FALSE);
+}
+
+static gboolean
+mock_echo_channel_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *options)
+{
+ cockpit_channel_control (channel, command, options);
+ return TRUE;
+}
+
+static void
+mock_echo_channel_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ MockEchoChannel *self = (MockEchoChannel *)channel;
+ self->close_called = TRUE;
+ COCKPIT_CHANNEL_CLASS (mock_echo_channel_parent_class)->close (channel, problem);
+}
+
+static void
+mock_echo_channel_init (MockEchoChannel *self)
+{
+
+}
+
+static void
+mock_echo_channel_class_init (MockEchoChannelClass *klass)
+{
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+ channel_class->recv = mock_echo_channel_recv;
+ channel_class->control = mock_echo_channel_control;
+ channel_class->close = mock_echo_channel_close;
+}
+
+static CockpitChannel *
+mock_echo_channel_open (CockpitTransport *transport,
+ const gchar *channel_id)
+{
+ CockpitChannel *channel;
+ JsonObject *options;
+
+ g_assert (channel_id != NULL);
+
+ options = json_object_new ();
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", channel_id,
+ "options", options,
+ NULL);
+
+ json_object_unref (options);
+ return channel;
+}
+
+static GType mock_null_channel_get_type (void) G_GNUC_CONST;
+
+typedef CockpitChannel MockNullChannel;
+typedef CockpitChannelClass MockNullChannelClass;
+
+G_DEFINE_TYPE (MockNullChannel, mock_null_channel, COCKPIT_TYPE_CHANNEL);
+
+static void
+mock_null_channel_init (MockNullChannel *self)
+{
+
+}
+
+static void
+mock_null_channel_class_init (MockNullChannelClass *klass)
+{
+
+}
+
+/* ----------------------------------------------------------------------------
+ * Testing
+ */
+
+typedef struct {
+ MockTransport *transport;
+ CockpitChannel *channel;
+} TestCase;
+
+static void
+setup (TestCase *tc,
+ gconstpointer unused)
+{
+ tc->transport = g_object_new (mock_transport_get_type (), NULL);
+ tc->channel = mock_echo_channel_open (COCKPIT_TRANSPORT (tc->transport), "554");
+ while (g_main_context_iteration (NULL, FALSE));
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer unused)
+{
+ g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel);
+ g_object_add_weak_pointer (G_OBJECT (tc->transport), (gpointer *)&tc->transport);
+ g_object_unref (tc->channel);
+ g_object_unref (tc->transport);
+ g_assert (tc->channel == NULL);
+ g_assert (tc->transport == NULL);
+}
+
+static void
+test_recv_and_send (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *sent;
+ GBytes *payload;
+
+ /* Ready to go */
+ cockpit_channel_ready (tc->channel, NULL);
+
+ payload = g_bytes_new ("Yeehaw!", 7);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "554", payload);
+
+ sent = mock_transport_pop_channel (tc->transport, "554");
+ g_assert (sent != NULL);
+
+ g_assert (g_bytes_equal (payload, sent));
+ g_bytes_unref (payload);
+}
+
+static void
+test_recv_and_queue (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *payload;
+ GBytes *control;
+ const gchar *data;
+ GBytes *sent;
+ JsonObject *object;
+
+ payload = g_bytes_new ("Yeehaw!", 7);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "554", payload);
+
+ data = "{ \"command\": \"blah\", \"channel\": \"554\" }";
+ control = g_bytes_new_static (data, strlen (data));
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), NULL, control);
+
+ /* Shouldn't have received it yet */
+ g_assert_cmpuint (mock_transport_count_sent (tc->transport), ==, 0);
+
+ /* Ready to go */
+ cockpit_channel_ready (tc->channel, NULL);
+
+ /* The control message */
+ object = mock_transport_pop_control (tc->transport);
+ g_assert (object != NULL);
+ cockpit_assert_json_eq (object, data);
+ g_bytes_unref (control);
+
+ sent = mock_transport_pop_channel (tc->transport, "554");
+ g_assert (sent != NULL);
+ g_assert (g_bytes_equal (payload, sent));
+ g_bytes_unref (payload);
+}
+
+static void
+test_ready_message (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *message;
+ JsonObject *sent;
+
+ message = json_object_new ();
+ json_object_set_string_member (message, "mop", "bucket");
+
+ /* Ready to go */
+ cockpit_channel_ready (tc->channel, message);
+ json_object_unref (message);
+
+ sent = mock_transport_pop_control (tc->transport);
+ cockpit_assert_json_eq (sent,
+ "{ \"command\": \"ready\", \"channel\": \"554\", \"mop\": \"bucket\" }");
+}
+
+static void
+test_close_immediately (TestCase *tc,
+ gconstpointer unused)
+{
+ GBytes *payload;
+ JsonObject *sent;
+
+ payload = g_bytes_new ("Yeehaw!", 7);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "554", payload);
+ g_bytes_unref (payload);
+
+ /* Shouldn't have received it yet */
+ g_assert_cmpuint (mock_transport_count_sent (tc->transport), ==, 0);
+
+ /* Now close without getting anything */
+ cockpit_channel_close (tc->channel, "bad-boy");
+
+ g_assert (mock_transport_pop_channel (tc->transport, "554") == NULL);
+ g_assert_cmpuint (mock_transport_count_sent (tc->transport), ==, 1);
+
+ sent = mock_transport_pop_control (tc->transport);
+ g_assert (sent != NULL);
+
+ cockpit_assert_json_eq (sent,
+ "{ \"command\": \"close\", \"channel\": \"554\", \"problem\": \"bad-boy\"}");
+}
+
+static void
+test_close_option (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *sent;
+ JsonObject *options;
+
+ options = cockpit_channel_close_options (tc->channel);
+ json_object_set_string_member (options, "option", "four");
+ cockpit_channel_close (tc->channel, "bad-boy");
+
+ g_assert_cmpuint (mock_transport_count_sent (tc->transport), ==, 1);
+
+ sent = mock_transport_pop_control (tc->transport);
+ g_assert (sent != NULL);
+
+ cockpit_assert_json_eq (sent,
+ "{ \"command\": \"close\", \"channel\": \"554\", \"problem\": \"bad-boy\", \"option\": \"four\" }");
+}
+
+static void
+test_close_json_option (TestCase *tc,
+ gconstpointer unused)
+{
+ JsonObject *sent;
+ JsonObject *obj;
+ JsonObject *options;
+
+ obj = json_object_new ();
+ json_object_set_string_member (obj, "test", "value");
+ options = cockpit_channel_close_options (tc->channel);
+ json_object_set_object_member (options, "option", obj);
+
+ cockpit_channel_close (tc->channel, "bad-boy");
+
+ g_assert_cmpuint (mock_transport_count_sent (tc->transport), ==, 1);
+
+ sent = mock_transport_pop_control (tc->transport);
+ g_assert (sent != NULL);
+
+ cockpit_assert_json_eq (sent,
+ "{ \"command\": \"close\", \"channel\": \"554\", \"problem\": \"bad-boy\", \"option\": { \"test\": \"value\" } }");
+}
+
+static void
+on_closed_get_problem (CockpitChannel *channel,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gchar **retval = user_data;
+ g_assert (*retval == NULL);
+ *retval = g_strdup (problem);
+}
+
+static void
+test_close_transport (TestCase *tc,
+ gconstpointer unused)
+{
+ MockEchoChannel *chan;
+ JsonObject *control;
+ GBytes *sent;
+ gchar *problem = NULL;
+
+ chan = (MockEchoChannel *)tc->channel;
+ cockpit_channel_ready (tc->channel, NULL);
+
+ sent = g_bytes_new ("Yeehaw!", 7);
+ cockpit_transport_emit_recv (COCKPIT_TRANSPORT (tc->transport), "554", sent);
+ g_bytes_unref (sent);
+
+ g_assert (chan->close_called == FALSE);
+
+ g_signal_connect (tc->channel, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+ cockpit_transport_close (COCKPIT_TRANSPORT (tc->transport), "boooo");
+
+ g_assert (chan->close_called == TRUE);
+
+ g_assert_cmpstr (problem, ==, "boooo");
+ control = mock_transport_pop_control (tc->transport);
+ g_assert_cmpstr (json_object_get_string_member (control, "command"), ==, "ready");
+ g_assert (mock_transport_pop_control (tc->transport) == NULL);
+
+ g_free (problem);
+}
+
+static void
+test_get_option (void)
+{
+ JsonObject *options;
+ CockpitTransport *transport;
+ CockpitChannel *channel;
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "scruffy", "janitor");
+ json_object_set_int_member (options, "age", 5);
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", "55",
+ "options", options,
+ NULL);
+ g_object_unref (transport);
+ json_object_unref (options);
+
+ options = cockpit_channel_get_options (channel);
+ g_assert_cmpstr (json_object_get_string_member (options, "scruffy"), ==, "janitor");
+ g_assert_cmpint (json_object_get_int_member (options, "age"), ==, 5);
+ g_assert (json_object_get_member (options, "marmalade") == NULL);
+
+ g_object_unref (channel);
+}
+
+static void
+test_properties (void)
+{
+ JsonObject *options;
+ CockpitTransport *transport;
+ CockpitTransport *check;
+ CockpitChannel *channel;
+ gchar *channel_id;
+
+ options = json_object_new ();
+ transport = g_object_new (mock_transport_get_type (), NULL);
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", "55",
+ "options", options,
+ NULL);
+ g_object_unref (transport);
+ json_object_unref (options);
+
+ g_object_get (channel, "transport", &check, "id", &channel_id, NULL);
+ g_assert (check == transport);
+ g_assert_cmpstr (cockpit_channel_get_id (channel), ==, "55");
+ g_assert_cmpstr (channel_id, ==, "55");
+ g_free (channel_id);
+
+ g_object_unref (channel);
+}
+
+static void
+test_close_not_capable (void)
+{
+ JsonObject *options;
+ JsonObject *sent;
+ JsonArray *capabilities;
+ MockTransport *transport;
+ CockpitChannel *channel;
+ CockpitChannel *channel2;
+ const gchar *cap[] = { "supported", NULL };
+
+ cockpit_expect_message ("55: unsupported capability required: unsupported1");
+ cockpit_expect_message ("55: unsupported capability required: unsupported2");
+ cockpit_expect_message ("55: unsupported capability required: unsupported1");
+ cockpit_expect_message ("55: unsupported capability required: unsupported2");
+
+ options = json_object_new ();
+ capabilities = json_array_new ();
+ json_array_add_string_element (capabilities, "unsupported1");
+ json_array_add_string_element (capabilities, "unsupported2");
+ json_object_set_array_member (options, "capabilities", capabilities);
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", "55",
+ "options", options,
+ NULL);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ sent = mock_transport_pop_control (transport);
+ g_assert (sent != NULL);
+
+ cockpit_assert_json_eq (sent,
+ "{ \"command\": \"close\", \"channel\": \"55\", \"problem\": \"not-supported\", \"capabilities\":[]}");
+ g_object_unref (channel);
+
+ channel2 = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", "55",
+ "options", options,
+ "capabilities", cap,
+ NULL);
+ json_object_unref (options);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ sent = mock_transport_pop_control (transport);
+ g_assert (sent != NULL);
+
+ cockpit_assert_json_eq (sent,
+ "{ \"command\": \"close\", \"channel\": \"55\", \"problem\": \"not-supported\", \"capabilities\":[\"supported\"]}");
+
+ g_object_unref (channel2);
+ g_object_unref (transport);
+}
+
+static void
+test_capable (void)
+{
+ JsonObject *options;
+ JsonObject *sent;
+ JsonArray *capabilities;
+ MockTransport *transport;
+ CockpitChannel *channel;
+ const gchar *cap[] = { "supported", NULL };
+
+ options = json_object_new ();
+ capabilities = json_array_new ();
+ json_array_add_string_element (capabilities, "supported");
+ json_object_set_array_member (options, "capabilities", capabilities);
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", "55",
+ "options", options,
+ "capabilities", cap,
+ NULL);
+ json_object_unref (options);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ sent = mock_transport_pop_control (transport);
+ g_assert (sent == NULL);
+
+ g_object_unref (channel);
+ g_object_unref (transport);
+}
+
+static void
+test_null_close_control (void)
+{
+ MockTransport *transport;
+ CockpitChannel *channel;
+
+ transport = g_object_new (mock_transport_get_type (), NULL);
+
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", "55",
+ NULL);
+
+ /* Make sure the NULL here works */
+ cockpit_channel_control (channel, "close", NULL);
+
+ g_object_unref (channel);
+ g_object_unref (transport);
+}
+
+static void
+test_ping_channel (void)
+{
+ JsonObject *reply = NULL;
+ JsonObject *options;
+ MockTransport *mock;
+ CockpitTransport *transport;
+ CockpitChannel *channel;
+ GBytes *sent;
+
+ mock = mock_transport_new ();
+ transport = COCKPIT_TRANSPORT (mock);
+
+ options = json_object_new ();
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", "55",
+ "options", options,
+ NULL);
+ cockpit_channel_ready (channel, NULL);
+ json_object_unref (options);
+
+ sent = cockpit_transport_build_control ("command", "ping", "channel", "55", "other", "marmalade", NULL);
+ cockpit_transport_emit_recv (transport, NULL, sent);
+ g_bytes_unref (sent);
+
+ reply = mock_transport_pop_control (mock);
+ g_assert (reply != NULL);
+ cockpit_assert_json_eq (reply, "{ \"command\": \"ready\", \"channel\": \"55\" }");
+
+ reply = mock_transport_pop_control (mock);
+ g_assert (reply != NULL);
+ cockpit_assert_json_eq (reply, "{ \"command\": \"pong\", \"channel\": \"55\", \"other\": \"marmalade\" }");
+
+ g_object_unref (channel);
+ g_object_unref (mock);
+}
+
+static void
+test_ping_no_channel (void)
+{
+ JsonObject *reply = NULL;
+ JsonObject *options;
+ MockTransport *mock;
+ CockpitTransport *transport;
+ CockpitChannel *channel;
+ GBytes *sent;
+
+ cockpit_expect_message ("received unknown control command: ping");
+
+ mock = mock_transport_new ();
+ transport = COCKPIT_TRANSPORT (mock);
+
+ options = json_object_new ();
+ channel = g_object_new (mock_echo_channel_get_type (),
+ "transport", transport,
+ "id", "55",
+ "options", options,
+ NULL);
+ json_object_unref (options);
+
+ /*
+ * Sending a "ping" on an unknown channel. There should be nothing that
+ * responds to this and returns a "pong" message.
+ */
+ sent = cockpit_transport_build_control ("command", "ping", "channel", "unknown", "other", "marmalade", NULL);
+ cockpit_transport_emit_recv (transport, NULL, sent);
+ g_bytes_unref (sent);
+
+ cockpit_channel_ready (channel, NULL);
+
+ /* Should just get a ready message back */
+ reply = mock_transport_pop_control (mock);
+ g_assert (reply != NULL);
+ cockpit_assert_json_eq (reply, "{ \"command\": \"ready\", \"channel\": \"55\" }");
+
+ reply = mock_transport_pop_control (mock);
+ g_assert (reply == NULL);
+
+ g_object_unref (channel);
+ g_object_unref (mock);
+}
+
+typedef struct {
+ CockpitTransport *transport_a;
+ CockpitChannel *channel_a;
+ CockpitTransport *transport_b;
+ CockpitChannel *channel_b;
+} TestPairCase;
+
+static void
+setup_pair (TestPairCase *tc,
+ gconstpointer data)
+{
+ CockpitPipe *pipe;
+ JsonObject *options;
+ int sv[2];
+
+ if (socketpair (PF_LOCAL, SOCK_STREAM, 0, sv) < 0)
+ g_assert_not_reached ();
+
+ pipe = cockpit_pipe_new ("a", sv[0], sv[0]);
+ tc->transport_a = cockpit_pipe_transport_new (pipe);
+ g_object_unref (pipe);
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "command", "open");
+ json_object_set_string_member (options, "channel", "999");
+ json_object_set_boolean_member (options, "flow-control", TRUE);
+ tc->channel_a = g_object_new (mock_null_channel_get_type (),
+ "id", "999",
+ "options", options,
+ "transport", tc->transport_a,
+ NULL);
+ cockpit_channel_prepare (tc->channel_a);
+ json_object_unref (options);
+
+ pipe = cockpit_pipe_new ("b", sv[1], sv[1]);
+ tc->transport_b = cockpit_pipe_transport_new (pipe);
+ g_object_unref (pipe);
+
+ options = json_object_new ();
+ json_object_set_string_member (options, "channel", "999");
+ json_object_set_boolean_member (options, "flow-control", TRUE);
+ tc->channel_b = g_object_new (mock_null_channel_get_type (),
+ "id", "999",
+ "options", options,
+ "transport", tc->transport_b,
+ NULL);
+ cockpit_channel_prepare (tc->channel_b);
+ json_object_unref (options);
+}
+
+static void
+teardown_pair (TestPairCase *tc,
+ gconstpointer data)
+{
+ g_object_add_weak_pointer (G_OBJECT (tc->channel_a), (gpointer *)&tc->channel_a);
+ g_object_add_weak_pointer (G_OBJECT (tc->transport_a), (gpointer *)&tc->transport_a);
+ g_object_add_weak_pointer (G_OBJECT (tc->channel_b), (gpointer *)&tc->channel_b);
+ g_object_add_weak_pointer (G_OBJECT (tc->transport_b), (gpointer *)&tc->transport_b);
+ g_object_unref (tc->channel_a);
+ g_object_unref (tc->channel_b);
+ g_object_unref (tc->transport_a);
+ g_object_unref (tc->transport_b);
+ g_assert (tc->channel_a == NULL);
+ g_assert (tc->transport_a == NULL);
+ g_assert (tc->channel_b == NULL);
+ g_assert (tc->transport_b == NULL);
+}
+
+static gboolean
+on_timeout_set_flag (gpointer user_data)
+{
+ gboolean *data = user_data;
+ g_assert (user_data);
+ g_assert (*data == FALSE);
+ *data = TRUE;
+ return FALSE;
+}
+
+static void
+on_pressure_set_throttle (CockpitChannel *channel,
+ gboolean throttle,
+ gpointer user_data)
+{
+ gint *data = user_data;
+ g_assert (user_data != NULL);
+ *data = throttle ? 1 : 0;
+}
+
+static void
+test_pressure_window (TestPairCase *tc,
+ gconstpointer data)
+{
+ gint throttle = -1;
+ GBytes *sent;
+ gint i;
+
+ /* Ready to go */
+ cockpit_channel_ready (tc->channel_a, NULL);
+ cockpit_channel_ready (tc->channel_b, NULL);
+ g_signal_connect (tc->channel_a, "pressure", G_CALLBACK (on_pressure_set_throttle), &throttle);
+
+ /* Sent this a thousand times */
+ sent = g_bytes_new_take (g_strnfill (1000 * 1000, '?'), 1000 * 1000);
+ for (i = 0; i < 10; i++)
+ cockpit_channel_send (tc->channel_a, sent, TRUE);
+ g_bytes_unref (sent);
+
+ /*
+ * This should have put way too much in the queue, and thus
+ * emitted the back-pressure signal. This signal would normally
+ * be used by others to slow down their queueing, but in this
+ * case we just check that it was fired.
+ */
+ g_assert_cmpint (throttle, ==, 1);
+
+ /*
+ * Now the queue is getting drained. At some point, it will be
+ * signaled that back pressure has been turned off
+ */
+ while (throttle != 0)
+ g_main_context_iteration (NULL, TRUE);
+}
+
+static void
+test_pressure_throttle (TestPairCase *tc,
+ gconstpointer data)
+{
+ CockpitFlow *pressure = mock_pressure_new ();
+ gboolean timeout = FALSE;
+ gboolean throttle = 0;
+ GBytes *sent;
+
+ cockpit_channel_ready (tc->channel_a, NULL);
+ cockpit_channel_ready (tc->channel_b, NULL);
+
+ g_signal_connect (tc->channel_a, "pressure", G_CALLBACK (on_pressure_set_throttle), &throttle);
+
+ /* Send this a thousand times over the echo pipe */
+ sent = g_bytes_new_take (g_strnfill (400 * 1000, '?'), 400 * 1000);
+
+ /* Turn on pressure on the remote side */
+ cockpit_flow_throttle (COCKPIT_FLOW (tc->channel_b), pressure);
+ cockpit_flow_emit_pressure (pressure, TRUE);
+
+ /* In spite of us running the main loop, no we should have pressure */
+ g_timeout_add_seconds (2, on_timeout_set_flag, &timeout);
+ while (timeout == FALSE)
+ {
+ if (throttle != 1)
+ cockpit_channel_send (tc->channel_a, sent, TRUE);
+ g_main_context_iteration (NULL, throttle == 1);
+ }
+
+ g_assert_cmpint (throttle, ==, 1);
+
+ /* Now lets turn off the pressure on the remote side */
+ cockpit_flow_emit_pressure (pressure, FALSE);
+
+ /* And we should see the pressure here go down too */
+ while (throttle == 1)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (throttle, ==, 0);
+
+ cockpit_flow_throttle (COCKPIT_FLOW (tc->channel_b), NULL);
+ g_object_unref (pressure);
+ g_bytes_unref (sent);
+}
+
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/channel/get-option", test_get_option);
+ g_test_add_func ("/channel/properties", test_properties);
+ g_test_add_func ("/channel/test-null-close-control", test_null_close_control);
+ g_test_add_func ("/channel/test_close_not_capable",
+ test_close_not_capable);
+ g_test_add_func ("/channel/test_capable",
+ test_capable);
+ g_test_add ("/channel/recv-send", TestCase, NULL,
+ setup, test_recv_and_send, teardown);
+ g_test_add ("/channel/recv-queue", TestCase, NULL,
+ setup, test_recv_and_queue, teardown);
+ g_test_add ("/channel/ready-message", TestCase, NULL,
+ setup, test_ready_message, teardown);
+ g_test_add ("/channel/close-immediately", TestCase, NULL,
+ setup, test_close_immediately, teardown);
+ g_test_add ("/channel/close-option", TestCase, NULL,
+ setup, test_close_option, teardown);
+ g_test_add ("/channel/close-json-option", TestCase, NULL,
+ setup, test_close_json_option, teardown);
+ g_test_add ("/channel/close-transport", TestCase, NULL,
+ setup, test_close_transport, teardown);
+
+ g_test_add ("/channel/pressure/window", TestPairCase, NULL,
+ setup_pair, test_pressure_window, teardown_pair);
+ g_test_add ("/channel/pressure/throttle", TestPairCase, NULL,
+ setup_pair, test_pressure_throttle, teardown_pair);
+
+ g_test_add_func ("/channel/ping/normal", test_ping_channel);
+ g_test_add_func ("/channel/ping/no-channel", test_ping_no_channel);
+
+ return g_test_run ();
+}
diff --git a/src/common/test-config.c b/src/common/test-config.c
new file mode 100644
index 0000000..30fb9fc
--- /dev/null
+++ b/src/common/test-config.c
@@ -0,0 +1,167 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitconf.h"
+
+#include "cockpitsystem.h"
+
+#include "testlib/cockpittest.h"
+
+#include <glib.h>
+
+/* Mock override cockpitconf.c */
+extern const gchar *cockpit_config_file;
+
+static void
+test_get_strings (void)
+{
+ cockpit_config_file = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf";
+
+ g_assert_null (cockpit_conf_string ("bad-section", "value"));
+ g_assert_null (cockpit_conf_string ("Section1", "value"));
+ g_assert_cmpstr (cockpit_conf_string ("Section2", "value1"),
+ ==, "string");
+ g_assert_cmpstr (cockpit_conf_string ("Section2", "value2"),
+ ==, "commas, or spaces");
+
+ /* Case insensitive */
+ g_assert_cmpstr (cockpit_conf_string ("sectiON2", "Value2"),
+ ==, "commas, or spaces");
+
+ cockpit_conf_cleanup ();
+}
+
+static void
+test_get_bool (void)
+{
+ cockpit_config_file = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf";
+
+ g_assert_true (cockpit_conf_bool ("bad-section", "value", TRUE));
+ g_assert_false (cockpit_conf_bool ("bad-section", "value", FALSE));
+ g_assert_false (cockpit_conf_bool ("Section2", "missing", FALSE));
+
+ g_assert_true (cockpit_conf_bool ("Section2", "true", FALSE));
+ g_assert_true (cockpit_conf_bool ("Section2", "truelower", FALSE));
+ g_assert_true (cockpit_conf_bool ("Section2", "one", FALSE));
+ g_assert_true (cockpit_conf_bool ("Section2", "yes", FALSE));
+
+ g_assert_false (cockpit_conf_bool ("Section2", "value1", TRUE));
+
+ cockpit_conf_cleanup ();
+}
+
+static void
+test_get_uint (void)
+{
+ cockpit_config_file = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf";
+
+ g_assert_cmpuint (cockpit_conf_uint ("bad-section", "value", 1, 999, 0), ==, 1);
+ g_assert_cmpuint (cockpit_conf_uint ("Section2", "missing", 1, 999, 0), ==, 1);
+ g_assert_cmpuint (cockpit_conf_uint ("Section2", "mixed", 10, 999, 0), ==, 10);
+ g_assert_cmpuint (cockpit_conf_uint ("Section2", "value1", 10, 999, 0), ==, 10);
+ g_assert_cmpuint (cockpit_conf_uint ("Section2", "toolarge", 10, 999, 0), ==, 10);
+ g_assert_cmpuint (cockpit_conf_uint ("Section2", "one", 10, 999, 0), ==, 1);
+ g_assert_cmpuint (cockpit_conf_uint ("Section2", "one", 1, 999, 2), ==, 2);
+ g_assert_cmpuint (cockpit_conf_uint ("Section2", "one", 1, 0, 0), ==, 0);
+ cockpit_conf_cleanup ();
+}
+
+static void
+test_get_strvs (void)
+{
+ const gchar * const *list = NULL;
+
+ cockpit_config_file = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf";
+
+ g_assert_null (cockpit_conf_strv ("bad-section", "value", ' '));
+ g_assert_null (cockpit_conf_strv ("Section1", "value", ' '));
+
+ /* empty list */
+ list = cockpit_conf_strv ("Section2", "empty", ':');
+ g_assert_null (list[0]);
+
+ /* list with one element */
+ list = cockpit_conf_strv ("Section2", "value1", ' ');
+ g_assert_cmpstr (list[0], ==, "string");
+ g_assert_null (list[1]);
+
+ /* list with space separator */
+ list = cockpit_conf_strv ("Section2", "value2", ' ');
+ g_assert_cmpstr (list[0], ==, "commas,");
+ g_assert_cmpstr (list[1], ==, "or");
+ g_assert_cmpstr (list[2], ==, "spaces");
+ g_assert_null (list[3]);
+
+ /* list with comma separator */
+ list = cockpit_conf_strv ("Section2", "value3", ',');
+ g_assert_cmpstr (list[0], ==, "commas");
+ g_assert_cmpstr (list[1], ==, " or spaces");
+ g_assert_null (list[2]);
+
+ /* list with only a separator */
+ list = cockpit_conf_strv ("Section2", "emptystrv", ':');
+ g_assert_cmpstr (list[0], ==, "");
+ g_assert_cmpstr (list[1], ==, "");
+ g_assert_null (list[2]);
+
+ /* list with empty value in the middle */
+ list = cockpit_conf_strv ("Section2", "strvemptyitems", ':');
+ g_assert_cmpstr (list[0], ==, "one");
+ g_assert_cmpstr (list[1], ==, "");
+ g_assert_cmpstr (list[2], ==, "three");
+ g_assert_null (list[3]);
+
+ cockpit_conf_cleanup ();
+}
+
+static void
+test_load_dir (void)
+{
+ cockpit_setenv_check("XDG_CONFIG_DIRS", "/does-not-exist:" SRCDIR "/src/ws/mock-config", TRUE);
+ cockpit_config_file = "cockpit.conf";
+
+ g_assert_cmpstr (cockpit_conf_string ("Section2", "value1"), ==, "string");
+ g_assert_cmpstr (cockpit_conf_get_dirs ()[1], ==, SRCDIR "/src/ws/mock-config");
+ cockpit_conf_cleanup ();
+}
+
+static void
+test_fail_load (void)
+{
+ cockpit_config_file = SRCDIR "/does-not-exist";
+ g_assert_null (cockpit_conf_string ("Section2", "value1"));
+ cockpit_conf_cleanup ();
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/conf/test-bool", test_get_bool);
+ g_test_add_func ("/conf/test-uint", test_get_uint);
+ g_test_add_func ("/conf/test-strings", test_get_strings);
+ g_test_add_func ("/conf/test-strvs", test_get_strvs);
+ g_test_add_func ("/conf/fail_load", test_fail_load);
+ g_test_add_func ("/conf/load_dir", test_load_dir);
+ return g_test_run ();
+}
diff --git a/src/common/test-frame.c b/src/common/test-frame.c
new file mode 100644
index 0000000..35ebd82
--- /dev/null
+++ b/src/common/test-frame.c
@@ -0,0 +1,204 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitframe.h"
+#include "testlib/cockpittest.h"
+
+#include <glib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+typedef struct
+{
+ FILE *write_fp;
+ int read_fd;
+} Fixture;
+
+typedef struct
+{
+ const char *input;
+ int expect_errno;
+} TestCase;
+
+static void
+fixture_setup (Fixture *self,
+ const TestCase *tc)
+{
+ int pipefd[2];
+
+ int r = pipe (pipefd);
+ g_assert_cmpint (r, ==, 0);
+
+ self->write_fp = fdopen (pipefd[1], "w");
+ g_assert (self->write_fp != NULL);
+ self->read_fd = pipefd[0];
+
+ if (tc->input)
+ {
+ fprintf (self->write_fp, "%s", tc->input);
+ fflush (self->write_fp);
+ }
+}
+
+static void
+fixture_close_write (Fixture *self)
+{
+ if (self->write_fp)
+ {
+ fclose (self->write_fp);
+ self->write_fp = NULL;
+ }
+}
+
+static void
+fixture_close_read (Fixture *self)
+{
+ if (self->read_fd != -1)
+ {
+ close (self->read_fd);
+ self->read_fd = -1;
+ }
+}
+
+static void
+fixture_teardown (Fixture *self,
+ const TestCase *tc)
+{
+ if (tc->expect_errno)
+ {
+ alarm (10);
+
+ g_autofree unsigned char *output = NULL;
+ ssize_t size = cockpit_frame_read (self->read_fd, &output);
+
+ g_assert_cmpint (size, ==, -1);
+ g_assert_cmpint (errno, ==, tc->expect_errno);
+ g_assert (output == NULL);
+
+ alarm (0);
+ }
+
+ fixture_close_write (self);
+ fixture_close_read (self);
+}
+
+static void
+test_valid (Fixture *pipe, const TestCase *tc)
+{
+ /* Try sending valid frames of various sizes */
+ for (gint i = 1; i < 1000; i++)
+ {
+ /* Write a frame consisting of `i` spaces. After the frame, write
+ * a pattern that we can use to detect that only the correct
+ * amount of bytes were read.
+ */
+ fprintf (pipe->write_fp, "%u\n%*sTHEEND", i, i, "");
+ fflush (pipe->write_fp);
+
+ /* Read it back and see what happens */
+ g_autofree unsigned char *output = NULL;
+ ssize_t size = cockpit_frame_read (pipe->read_fd, &output);
+
+ g_assert_cmpint (size, ==, i);
+ for (gint j = 0; j < size; j++)
+ g_assert (output[j] == ' ');
+
+ /* Make sure our pattern is there */
+ char buffer[7];
+ size = read (pipe->read_fd, buffer, sizeof buffer);
+ g_assert_cmpint (size, ==, 6);
+ g_assert (memcmp (buffer, "THEEND", 6) == 0);
+ }
+}
+
+static void
+test_fail_badfd (Fixture *fixture, const TestCase *tc)
+{
+ /* cause cockpit_frame_read() to read from -1 */
+ fixture_close_read (fixture);
+}
+
+static void
+test_fail_short (Fixture *fixture, const TestCase *tc)
+{
+ /* cause cockpit_frame_read() to read the message, then EOF */
+ fixture_close_write (fixture);
+}
+
+static void
+test_fail_nonblocking (Fixture *pipe, const TestCase *tc)
+{
+ /* cause cockpit_frame_read() to read the message, then EAGAIN */
+ (void) fcntl (pipe->read_fd, F_SETFL, O_NONBLOCK);
+}
+
+/* many of the testcases are driven entirely by the fixture setup/teardown */
+static void nil (void) { }
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ /* kwargs hack, plus avoid casting gconstpointer */
+#define PIPE_TEST(name,func,...) g_test_add_vtable(name, sizeof(Fixture), \
+ &(TestCase) { __VA_ARGS__ }, \
+ (GTestFixtureFunc) fixture_setup, \
+ (GTestFixtureFunc) func, \
+ (GTestFixtureFunc) fixture_teardown)
+
+ PIPE_TEST("/frame/read-frame/valid", test_valid);
+
+ PIPE_TEST("/frame/read-frame/fail/badfd", test_fail_badfd,
+ .expect_errno=EBADF);
+
+ PIPE_TEST("/frame/read-frame/fail/short", test_fail_short,
+ .input="10\nabc", .expect_errno=EBADMSG);
+
+ PIPE_TEST("/frame/read-frame/fail/nonblocking", test_fail_nonblocking,
+ .input="10\nabc", .expect_errno=EAGAIN);
+
+ /* This valid message should fail because we get EAGAIN while trying to read it... */
+ PIPE_TEST("/frame/read-frame/fail/nonblocking-big", test_fail_nonblocking,
+ .input="99999999\nabc", .expect_errno=EAGAIN);
+ /* ...but add one byte more, and it's now an invalid message. */
+ PIPE_TEST("/frame/read-frame/fail/nonblocking-toobig", test_fail_nonblocking,
+ .input="100000000\nabc", .expect_errno=EBADMSG);
+
+ /* Some generic failures due to broken messages */
+ PIPE_TEST("/frame/read-frame/fail/non-numeric", nil,
+ .input="abc\nabc", .expect_errno=EBADMSG);
+ PIPE_TEST("/frame/read-frame/fail/semi-numeric", nil,
+ .input="1000abc\nabc", .expect_errno=EBADMSG);
+ PIPE_TEST("/frame/read-frame/fail/toobig", nil,
+ .input="100000000\nabc", .expect_errno=EBADMSG);
+ PIPE_TEST("/frame/read-frame/fail/toobig-nonnumeric", nil,
+ .input="10000000a\nabc", .expect_errno=EBADMSG);
+ PIPE_TEST("/frame/read-frame/fail/leading-zero", nil,
+ .input="03\nabc", .expect_errno=EBADMSG);
+ PIPE_TEST("/frame/read-frame/fail/empty-header", nil,
+ .input="\nabc", .expect_errno=EBADMSG);
+
+ return g_test_run ();
+}
diff --git a/src/common/test-hash.c b/src/common/test-hash.c
new file mode 100644
index 0000000..c09d581
--- /dev/null
+++ b/src/common/test-hash.c
@@ -0,0 +1,53 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpithash.h"
+
+#include "testlib/cockpittest.h"
+
+static void
+test_case_hash (void)
+{
+ GHashTable *table;
+
+ table = g_hash_table_new_full (cockpit_str_case_hash, cockpit_str_case_equal, g_free, g_free);
+
+ /* Case insensitive keys */
+ g_hash_table_insert (table, g_strdup ("Blah"), g_strdup ("value"));
+ g_hash_table_insert (table, g_strdup ("blah"), g_strdup ("another"));
+ g_hash_table_insert (table, g_strdup ("Different"), g_strdup ("One"));
+
+ g_assert_cmpstr (g_hash_table_lookup (table, "BLAH"), ==, "another");
+ g_assert_cmpstr (g_hash_table_lookup (table, "differeNT"), ==, "One");
+
+ g_hash_table_destroy (table);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/hash/case-hash", test_case_hash);
+
+ return g_test_run ();
+}
diff --git a/src/common/test-hex.c b/src/common/test-hex.c
new file mode 100644
index 0000000..1654c4d
--- /dev/null
+++ b/src/common/test-hex.c
@@ -0,0 +1,82 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpithex.h"
+
+#include "testlib/retest.h"
+
+#include <stdlib.h>
+
+static void
+test_decode_success (void)
+{
+ void * decoded;
+ size_t length;
+
+ decoded = cockpit_hex_decode ("6d61726d616c616465", -1, &length);
+ assert_str_cmp (decoded, ==, "marmalade");
+ assert_num_cmp (length, ==, 9);
+ free (decoded);
+}
+
+static void
+test_decode_part (void)
+{
+ void * decoded;
+ size_t length;
+
+ decoded = cockpit_hex_decode ("6d61726d616c616465", 8, &length);
+ assert_str_cmp (decoded, ==, "marm");
+ assert_num_cmp (length, ==, 4);
+ free (decoded);
+}
+
+static void
+test_decode_no_length (void)
+{
+ void *decoded;
+
+ decoded = cockpit_hex_decode ("6d61726d616c616465", -1, NULL);
+ assert_str_cmp (decoded, ==, "marmalade");
+ free (decoded);
+}
+
+static void
+test_decode_fail (void)
+{
+ void *decoded;
+ size_t length;
+
+ decoded = cockpit_hex_decode ("abcdefghijklmn", -1, &length);
+ assert (decoded == NULL);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ re_test (test_decode_success, "/hex/decode-success");
+ re_test (test_decode_part, "/hex/decode-part");
+ re_test (test_decode_no_length, "/hex/decode-no-length");
+ re_test (test_decode_fail, "/hex/decode-fail");
+
+ return re_test_run (argc, argv);
+}
diff --git a/src/common/test-json.c b/src/common/test-json.c
new file mode 100644
index 0000000..480f3b7
--- /dev/null
+++ b/src/common/test-json.c
@@ -0,0 +1,893 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitjson.h"
+
+#include "testlib/cockpittest.h"
+
+#include <math.h>
+#include <string.h>
+
+static const gchar *test_data =
+ "{"
+ " \"string\": \"value\","
+ " \"number\": 55.4,"
+ " \"array\": [ \"one\", \"two\", \"three\" ],"
+ " \"object\": { \"test\": \"one\" },"
+ " \"bool\": true,"
+ " \"null\": null"
+ "}";
+
+typedef struct {
+ JsonObject *root;
+} TestCase;
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ GError *error = NULL;
+ JsonNode *node;
+
+ node = cockpit_json_parse (test_data, -1, &error);
+ g_assert_no_error (error);
+
+ g_assert (json_node_get_node_type (node) == JSON_NODE_OBJECT);
+ tc->root = json_node_dup_object (node);
+ json_node_free (node);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ json_object_unref (tc->root);
+}
+
+static void
+test_get_string (TestCase *tc,
+ gconstpointer data)
+{
+ gboolean ret;
+ const gchar *value;
+
+ ret = cockpit_json_get_string (tc->root, "string", NULL, &value);
+ g_assert (ret == TRUE);
+ g_assert_cmpstr (value, ==, "value");
+
+ ret = cockpit_json_get_string (tc->root, "string", NULL, NULL);
+ g_assert (ret == TRUE);
+
+ ret = cockpit_json_get_string (tc->root, "unknown", NULL, &value);
+ g_assert (ret == TRUE);
+ g_assert_cmpstr (value, ==, NULL);
+
+ ret = cockpit_json_get_string (tc->root, "unknown", "default", &value);
+ g_assert (ret == TRUE);
+ g_assert_cmpstr (value, ==, "default");
+
+ ret = cockpit_json_get_string (tc->root, "number", NULL, &value);
+ g_assert (ret == FALSE);
+
+ ret = cockpit_json_get_string (tc->root, "number", NULL, NULL);
+ g_assert (ret == FALSE);
+}
+
+static void
+test_get_int (TestCase *tc,
+ gconstpointer data)
+{
+ gboolean ret;
+ gint64 value;
+
+ ret = cockpit_json_get_int (tc->root, "number", 0, &value);
+ g_assert (ret == TRUE);
+ g_assert_cmpint (value, ==, 55);
+
+ ret = cockpit_json_get_int (tc->root, "number", 0, NULL);
+ g_assert (ret == TRUE);
+
+ ret = cockpit_json_get_int (tc->root, "unknown", 66, &value);
+ g_assert (ret == TRUE);
+ g_assert_cmpint (value, ==, 66);
+
+ ret = cockpit_json_get_int (tc->root, "string", 66, &value);
+ g_assert (ret == FALSE);
+
+ ret = cockpit_json_get_int (tc->root, "string", 66, NULL);
+ g_assert (ret == FALSE);
+}
+
+static void
+test_get_double (TestCase *tc,
+ gconstpointer data)
+{
+ gboolean ret;
+ gdouble value;
+
+ ret = cockpit_json_get_double (tc->root, "number", 0, &value);
+ g_assert (ret == TRUE);
+ g_assert_cmpfloat (value, ==, 55.4);
+
+ ret = cockpit_json_get_double (tc->root, "number", 0, NULL);
+ g_assert (ret == TRUE);
+
+ ret = cockpit_json_get_double (tc->root, "unknown", 66.4, &value);
+ g_assert (ret == TRUE);
+ g_assert_cmpfloat (value, ==, 66.4);
+
+ ret = cockpit_json_get_double (tc->root, "string", 66.4, &value);
+ g_assert (ret == FALSE);
+
+ ret = cockpit_json_get_double (tc->root, "string", 66.4, NULL);
+ g_assert (ret == FALSE);
+}
+
+static void
+test_get_bool (TestCase *tc,
+ gconstpointer data)
+{
+ gboolean ret;
+ gboolean value;
+
+ ret = cockpit_json_get_bool (tc->root, "bool", FALSE, &value);
+ g_assert (ret == TRUE);
+ g_assert_cmpint (value, ==, TRUE);
+
+ ret = cockpit_json_get_bool (tc->root, "bool", FALSE, NULL);
+ g_assert (ret == TRUE);
+
+ ret = cockpit_json_get_bool (tc->root, "unknown", TRUE, &value);
+ g_assert (ret == TRUE);
+ g_assert_cmpint (value, ==, TRUE);
+
+ ret = cockpit_json_get_bool (tc->root, "unknown", FALSE, &value);
+ g_assert (ret == TRUE);
+ g_assert_cmpint (value, ==, FALSE);
+
+ ret = cockpit_json_get_bool (tc->root, "string", FALSE, &value);
+ g_assert (ret == FALSE);
+
+ ret = cockpit_json_get_bool (tc->root, "string", FALSE, NULL);
+ g_assert (ret == FALSE);
+}
+
+static void
+test_get_null (TestCase *tc,
+ gconstpointer data)
+{
+ gboolean ret;
+ gboolean present;
+
+ ret = cockpit_json_get_null (tc->root, "null", NULL);
+ g_assert (ret == TRUE);
+
+ present = FALSE;
+ ret = cockpit_json_get_null (tc->root, "null", &present);
+ g_assert (ret == TRUE);
+ g_assert (present == TRUE);
+
+ ret = cockpit_json_get_null (tc->root, "unknown", NULL);
+ g_assert (ret == TRUE);
+
+ ret = cockpit_json_get_null (tc->root, "unknown", &present);
+ g_assert (ret == TRUE);
+ g_assert (present == FALSE);
+
+ ret = cockpit_json_get_null (tc->root, "number", NULL);
+ g_assert (ret == FALSE);
+}
+
+static void
+test_get_strv (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *defawlt[] = { "1", "2", NULL };
+ gboolean ret;
+ const gchar **value;
+
+ ret = cockpit_json_get_strv (tc->root, "array", NULL, &value);
+ g_assert (ret == TRUE);
+ g_assert (value != NULL);
+ g_assert_cmpstr (value[0], ==, "one");
+ g_assert_cmpstr (value[1], ==, "two");
+ g_assert_cmpstr (value[2], ==, "three");
+ g_assert_cmpstr (value[3], ==, NULL);
+ g_free (value);
+
+ ret = cockpit_json_get_strv (tc->root, "unknown", NULL, &value);
+ g_assert (ret == TRUE);
+ g_assert (value == NULL);
+
+ ret = cockpit_json_get_strv (tc->root, "unknown", defawlt, &value);
+ g_assert (ret == TRUE);
+ g_assert (value != NULL);
+ g_assert_cmpstr (value[0], ==, "1");
+ g_assert_cmpstr (value[1], ==, "2");
+ g_assert_cmpstr (value[2], ==, NULL);
+ g_free (value);
+
+ ret = cockpit_json_get_strv (tc->root, "number", NULL, &value);
+ g_assert (ret == FALSE);
+}
+
+static void
+test_get_array (TestCase *tc,
+ gconstpointer data)
+{
+ JsonArray *defawlt = json_array_new ();
+ gboolean ret;
+ JsonArray *value;
+
+ ret = cockpit_json_get_array (tc->root, "array", NULL, &value);
+ g_assert (ret == TRUE);
+ g_assert (value != NULL);
+ g_assert_cmpstr (json_array_get_string_element (value, 0), ==, "one");
+ g_assert_cmpstr (json_array_get_string_element (value, 1), ==, "two");
+ g_assert_cmpstr (json_array_get_string_element (value, 2), ==, "three");
+
+ ret = cockpit_json_get_array (tc->root, "array", NULL, NULL);
+ g_assert (ret == TRUE);
+
+ ret = cockpit_json_get_array (tc->root, "unknown", NULL, &value);
+ g_assert (ret == TRUE);
+ g_assert (value == NULL);
+
+ ret = cockpit_json_get_array (tc->root, "unknown", defawlt, &value);
+ g_assert (ret == TRUE);
+ g_assert (value == defawlt);
+
+ ret = cockpit_json_get_array (tc->root, "number", NULL, &value);
+ g_assert (ret == FALSE);
+
+ ret = cockpit_json_get_array (tc->root, "number", NULL, NULL);
+ g_assert (ret == FALSE);
+
+ json_array_unref (defawlt);
+}
+
+static void
+test_get_object (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *defawlt = json_object_new ();
+ gboolean ret;
+ JsonObject *value;
+
+ ret = cockpit_json_get_object (tc->root, "object", NULL, &value);
+ g_assert (ret == TRUE);
+ g_assert (value != NULL);
+ g_assert_cmpstr (json_object_get_string_member (value, "test"), ==, "one");
+
+ ret = cockpit_json_get_object (tc->root, "object", NULL, NULL);
+ g_assert (ret == TRUE);
+
+ ret = cockpit_json_get_object (tc->root, "unknown", NULL, &value);
+ g_assert (ret == TRUE);
+ g_assert (value == NULL);
+
+ ret = cockpit_json_get_object (tc->root, "unknown", defawlt, &value);
+ g_assert (ret == TRUE);
+ g_assert (value == defawlt);
+
+ ret = cockpit_json_get_object (tc->root, "number", NULL, &value);
+ g_assert (ret == FALSE);
+
+ ret = cockpit_json_get_object (tc->root, "array", NULL, NULL);
+ g_assert (ret == FALSE);
+
+ json_object_unref (defawlt);
+}
+
+static void
+test_hashtable_objects (void)
+{
+ JsonObject *object = json_object_new ();
+ GHashTable *ht = NULL;
+ const gchar *fields[] = {
+ "test",
+ "test2",
+ "test4",
+ "test5",
+ NULL,
+ };
+
+ json_object_set_string_member (object, "test", "one");
+ json_object_set_string_member (object, "test2", "two");
+ json_object_set_string_member (object, "test3", "three");
+ json_object_set_null_member (object, "test4");
+ json_object_set_string_member (object, "test5", "five");
+
+ ht = cockpit_json_to_hash_table (object, fields);
+ g_assert_cmpstr (g_hash_table_lookup (ht, "test"), ==, "one");
+ g_assert_cmpstr (g_hash_table_lookup (ht, "test2"), ==, "two");
+ g_assert_cmpstr (g_hash_table_lookup (ht, "test5"), ==, "five");
+ g_assert_false (g_hash_table_contains (ht, "test3"));
+ g_assert_false (g_hash_table_contains (ht, "test4"));
+ json_object_unref (object);
+
+ object = cockpit_json_from_hash_table (ht, fields);
+ g_assert_cmpstr (json_object_get_string_member (object, "test"), ==, "one");
+ g_assert_cmpstr (json_object_get_string_member (object, "test2"), ==, "two");
+ g_assert_cmpstr (json_object_get_string_member (object, "test5"), ==, "five");
+ g_assert_false (json_object_has_member (object, "test3"));
+ g_assert_true (json_object_get_null_member (object, "test4"));
+
+ json_object_unref (object);
+ g_hash_table_unref (ht);
+}
+
+static void
+test_int_hash (void)
+{
+ gint64 one = 1;
+ gint64 two = G_MAXINT;
+ gint64 copy = 1;
+
+ g_assert_cmpuint (cockpit_json_int_hash (&one), !=, cockpit_json_int_hash (&two));
+ g_assert_cmpuint (cockpit_json_int_hash (&one), ==, cockpit_json_int_hash (&one));
+ g_assert_cmpuint (cockpit_json_int_hash (&one), ==, cockpit_json_int_hash (&copy));
+}
+
+static void
+test_int_equal (void)
+{
+ gint64 one = 1;
+ gint64 two = G_MAXINT;
+ gint64 copy = 1;
+
+ g_assert (!cockpit_json_int_equal (&one, &two));
+ /* coverity[copy_paste_error : FALSE] */
+ g_assert (cockpit_json_int_equal (&one, &one));
+ g_assert (cockpit_json_int_equal (&one, &copy));
+}
+
+static void
+test_parser_trims (void)
+{
+ GError *error = NULL;
+ JsonNode *node;
+
+ /* Test that the parser trims whitespace, as long as something is present */
+
+ node = cockpit_json_parse (" 55 ", -1, &error);
+ g_assert_no_error (error);
+ g_assert (node);
+ g_assert_cmpint (json_node_get_node_type (node), ==, JSON_NODE_VALUE);
+ g_assert_cmpint (json_node_get_value_type (node), ==, G_TYPE_INT64);
+ json_node_free (node);
+
+ node = cockpit_json_parse (" \"xx\" ", -1, &error);
+ g_assert_no_error (error);
+ g_assert (node);
+ g_assert_cmpint (json_node_get_node_type (node), ==, JSON_NODE_VALUE);
+ g_assert_cmpint (json_node_get_value_type (node), ==, G_TYPE_STRING);
+ json_node_free (node);
+
+ node = cockpit_json_parse (" {\"xx\":5} ", -1, &error);
+ g_assert_no_error (error);
+ g_assert (node);
+ g_assert_cmpint (json_node_get_node_type (node), ==, JSON_NODE_OBJECT);
+ json_node_free (node);
+}
+
+static void
+test_parser_empty (void)
+{
+ GError *error = NULL;
+ JsonNode *node;
+
+ node = cockpit_json_parse ("", 0, &error);
+ g_assert_error (error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_PARSE);
+ g_error_free (error);
+ g_assert (node == NULL);
+}
+
+typedef struct {
+ const gchar *name;
+ gboolean equal;
+ const gchar *a;
+ const gchar *b;
+} FixtureEqual;
+
+static const FixtureEqual equal_fixtures[] = {
+ { "nulls", TRUE,
+ NULL,
+ NULL },
+ { "null-non-null", FALSE,
+ NULL,
+ "555" },
+ { "non-null-null", FALSE,
+ "555",
+ NULL },
+ { "number-string", FALSE,
+ "555",
+ "\"str\"" },
+ { "string-string", TRUE,
+ "\"str\"",
+ "\"str\"" },
+ { "string-string-ne", FALSE,
+ "\"xxxx\"",
+ "\"str\"" },
+ { "int-int", TRUE,
+ "555",
+ "555" },
+ { "int-int-ne", FALSE,
+ "555",
+ "556" },
+ { "double-double", TRUE,
+ "555.0",
+ "555.00" },
+ { "boolean-boolean", TRUE,
+ "true",
+ "true" },
+ { "boolean-boolean-ne", FALSE,
+ "true",
+ "false" },
+ { "null-null", TRUE,
+ "null",
+ "null" },
+ { "array-string", FALSE,
+ "[]",
+ "\"str\"" },
+ { "array-array", TRUE,
+ "[1, 2.0, 3]",
+ "[1, 2.00, 3]" },
+ { "array-array-ne", FALSE,
+ "[1, 2.0, 3]",
+ "[1, 4.00, 3]" },
+ { "array-array-length", FALSE,
+ "[1, 2.0, 3]",
+ "[1]" },
+ { "object-object", TRUE,
+ "{\"one\": 1, \"two\": \"2.0\"}",
+ "{\"one\": 1, \"two\": \"2.0\"}" },
+ { "object-object-order", TRUE,
+ "{\"one\": 1, \"two\": \"2.0\"}",
+ "{\"two\": \"2.0\", \"one\": 1}" },
+ { "object-object-missing", FALSE,
+ "{\"one\": 1, \"two\": \"2.0\"}",
+ "{\"two\": \"2.0\"}" },
+ { "object-object-value", FALSE,
+ "{\"one\": 1, \"two\": \"2.0\"}",
+ "{\"one\": 1, \"two\": \"2\"}" },
+};
+
+static void
+test_equal (gconstpointer data)
+{
+ const FixtureEqual *fixture = data;
+ JsonNode *a = NULL;
+ JsonNode *b = NULL;
+ GError *error = NULL;
+
+ if (fixture->a)
+ a = cockpit_json_parse (fixture->a, -1, &error);
+ if (fixture->b)
+ b = cockpit_json_parse (fixture->b, -1, &error);
+
+ g_assert (cockpit_json_equal (a, b) == fixture->equal);
+
+ json_node_free (a);
+ json_node_free (b);
+}
+
+static void
+test_utf8_invalid (void)
+{
+ const gchar *input = "\"\xff\xff\"";
+ GError *error = NULL;
+
+ if (cockpit_json_parse (input, -1, &error))
+ g_assert_not_reached ();
+
+ g_assert_error (error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_INVALID_DATA);
+ g_error_free (error);
+}
+
+typedef struct {
+ const gchar *str;
+ const gchar *expect;
+} FixtureString;
+
+static const FixtureString string_fixtures[] = {
+ { "abc", "\"abc\"" },
+ { "a\x7fxc", "\"a\\u007fxc\"" },
+ { "a\033xc", "\"a\\u001bxc\"" },
+ { "a\nxc", "\"a\\nxc\"" },
+ { "a\\xc", "\"a\\\\xc\"" },
+ { "Barney B\303\244r", "\"Barney B\303\244r\"" },
+};
+
+static void
+test_string_encode (gconstpointer data)
+{
+ const FixtureString *fixture = data;
+ JsonNode *node;
+ gsize length;
+ gchar *output;
+
+ node = json_node_init_string (json_node_alloc (), fixture->str);
+ output = cockpit_json_write (node, &length);
+ g_assert_cmpstr (output, ==, fixture->expect);
+ g_assert_cmpuint (length, ==, strlen (fixture->expect));
+ g_free (output);
+ json_node_free (node);
+}
+
+static const gchar *patch_data =
+ "{"
+ " \"string\": \"value\","
+ " \"number\": 55,"
+ " \"array\": [ \"one\", \"two\", \"three\", 4, \"five\", \"six\" ],"
+ " \"bool\": true,"
+ " \"null\": null,"
+ " \"object\": {"
+ " \"one\": 1,"
+ " \"two\": 2,"
+ " \"nested\": {"
+ " \"three\": 3"
+ " }"
+ " }"
+ "}";
+
+typedef struct {
+ const gchar *name;
+ const gchar *patch;
+ const gchar *result;
+} PatchFixture;
+
+static PatchFixture patch_fixtures[] = {
+ {
+ "simple-value",
+ "{\"string\": 5}",
+ "{"
+ " \"string\": 5,"
+ " \"number\": 55,"
+ " \"array\": [ \"one\", \"two\", \"three\", 4, \"five\", \"six\" ],"
+ " \"bool\": true,"
+ " \"null\": null,"
+ " \"object\": {"
+ " \"one\": 1,"
+ " \"two\": 2,"
+ " \"nested\": {"
+ " \"three\": 3"
+ " }"
+ " }"
+ "}",
+ },
+ {
+ "multi-value",
+ "{"
+ " \"array\": [ 5 ],"
+ " \"number\": { \"test\": true }"
+ "}",
+ "{"
+ " \"string\": \"value\","
+ " \"number\": { \"test\": true },"
+ " \"array\": [ 5 ],"
+ " \"bool\": true,"
+ " \"null\": null,"
+ " \"object\": {"
+ " \"one\": 1,"
+ " \"two\": 2,"
+ " \"nested\": {"
+ " \"three\": 3"
+ " }"
+ " }"
+ "}",
+ },
+ {
+ "add-and-remove",
+ "{"
+ " \"array\": null,"
+ " \"number\": null,"
+ " \"object\": null,"
+ " \"added\": 42"
+ "}",
+ "{"
+ " \"string\": \"value\","
+ " \"bool\": true,"
+ " \"null\": null,"
+ " \"added\": 42"
+ "}",
+ },
+ {
+ "nested-objects",
+ "{"
+ " \"object\": {"
+ " \"one\": \"uno\","
+ " \"nested\": null,"
+ " \"three\": \"tres\""
+ " }"
+ "}",
+ "{"
+ " \"string\": \"value\","
+ " \"number\": 55,"
+ " \"array\": [ \"one\", \"two\", \"three\", 4, \"five\", \"six\" ],"
+ " \"bool\": true,"
+ " \"null\": null,"
+ " \"object\": {"
+ " \"one\": \"uno\","
+ " \"two\": 2,"
+ " \"three\": \"tres\""
+ " }"
+ "}",
+ }
+};
+
+static void
+test_patch (gconstpointer data)
+{
+ const PatchFixture *fixture = data;
+ GError *error = NULL;
+ JsonObject *object;
+ JsonObject *with;
+
+ object = cockpit_json_parse_object (patch_data, -1, &error);
+ g_assert_no_error (error);
+
+ with = cockpit_json_parse_object (fixture->patch, -1, &error);
+ g_assert_no_error (error);
+
+ cockpit_json_patch (object, with);
+
+ cockpit_assert_json_eq (object, fixture->result);
+ json_object_unref (object);
+ json_object_unref (with);
+}
+
+static void
+test_write_infinite_nan (void)
+{
+ JsonArray *array;
+ gchar *string;
+ JsonNode *node;
+
+ array = json_array_new ();
+ json_array_add_double_element (array, 3.0); /* number */
+ json_array_add_double_element (array, 1.0/0.0); /* INFINITY */
+ json_array_add_double_element (array, NAN); /* NaN */
+
+ node = json_node_new (JSON_NODE_ARRAY);
+ json_node_take_array (node, array);
+ string = cockpit_json_write (node, NULL);
+
+ g_assert_cmpstr (string, ==, "[3,null,null]");
+
+ json_node_free (node);
+ g_free (string);
+}
+
+static JsonNode *
+flip_integer (JsonNode *node,
+ gpointer user_data)
+{
+ int *counter = user_data;
+
+ if (json_node_get_value_type (node) != G_TYPE_INT64)
+ return NULL;
+
+ int value = json_node_get_int (node);
+
+ (*counter)++;
+
+ return json_node_init_int (json_node_alloc(), -value);
+}
+
+/* replace all three-character strings with the word 'three' */
+static JsonNode *
+three_subst (JsonNode *node,
+ gpointer user_data)
+{
+ int *counter = user_data;
+
+ const gchar *value = json_node_get_string (node);
+
+ if (value == NULL || strlen (value) != 3)
+ return NULL;
+
+ (*counter)++;
+
+ return json_node_init_string (json_node_alloc(), "three");
+}
+
+static JsonNode *
+null_subst (JsonNode *node,
+ gpointer user_data)
+{
+ int *counter = user_data;
+
+ (*counter)++;
+
+ return NULL;
+}
+
+static void
+test_walk (void)
+{
+ GError *error = NULL;
+
+ g_autoptr(JsonObject) source = cockpit_json_parse_object (patch_data, -1, &error);
+ g_assert_no_error (error);
+ json_object_seal (source);
+
+ /* for the nul case, we should get exactly the same object back */
+ {
+ int counter = 0;
+ g_autoptr(JsonObject) result = cockpit_json_walk (source, null_subst, &counter);
+ g_assert_cmpint (counter, ==, 13);
+ g_assert (result == source);
+ }
+
+ /* try flipping some ints */
+ {
+ const char expected[] =
+ "{"
+ " \"string\": \"value\","
+ " \"number\": -55,"
+ " \"array\": [ \"one\", \"two\", \"three\", -4, \"five\", \"six\" ],"
+ " \"bool\": true,"
+ " \"null\": null,"
+ " \"object\": {"
+ " \"one\": -1,"
+ " \"two\": -2,"
+ " \"nested\": {"
+ " \"three\": -3"
+ " }"
+ " }"
+ "}";
+ int counter = 0;
+ g_autoptr(JsonObject) result = cockpit_json_walk (source, flip_integer, &counter);
+ g_assert_cmpint (counter, ==, 5);
+ cockpit_assert_json_eq (result, expected);
+ }
+
+ /* try replacing some strings */
+ {
+ const char expected[] =
+ "{"
+ " \"string\": \"value\","
+ " \"number\": 55,"
+ " \"array\": [ \"three\", \"three\", \"three\", 4, \"five\", \"three\" ],"
+ " \"bool\": true,"
+ " \"null\": null,"
+ " \"object\": {"
+ " \"one\": 1,"
+ " \"two\": 2,"
+ " \"nested\": {"
+ " \"three\": 3"
+ " }"
+ " }"
+ "}";
+ int counter = 0;
+ g_autoptr(JsonObject) result = cockpit_json_walk (source, three_subst, &counter);
+ g_assert_cmpint (counter, ==, 3);
+ cockpit_assert_json_eq (result, expected);
+ }
+
+ /* try stacking multiple operations */
+ {
+ const char expected[] =
+ "{"
+ " \"string\": \"value\","
+ " \"number\": -55,"
+ " \"array\": [ \"three\", \"three\", \"three\", -4, \"five\", \"three\" ],"
+ " \"bool\": true,"
+ " \"null\": null,"
+ " \"object\": {"
+ " \"one\": -1,"
+ " \"two\": -2,"
+ " \"nested\": {"
+ " \"three\": -3"
+ " }"
+ " }"
+ "}";
+
+ {
+ int counter = 0;
+ g_autoptr(JsonObject) result1 = cockpit_json_walk (source, three_subst, &counter);
+ g_assert_cmpint (counter, ==, 3);
+ counter = 0;
+ g_autoptr(JsonObject) result2 = cockpit_json_walk (result1, flip_integer, &counter);
+ g_assert_cmpint (counter, ==, 5);
+ cockpit_assert_json_eq (result2, expected);
+ }
+
+ /* ...and the other way around */
+ {
+ int counter = 0;
+ g_autoptr(JsonObject) result1 = cockpit_json_walk (source, flip_integer, &counter);
+ g_assert_cmpint (counter, ==, 5);
+ counter = 0;
+ g_autoptr(JsonObject) result2 = cockpit_json_walk (result1, three_subst, &counter);
+ g_assert_cmpint (counter, ==, 3);
+ cockpit_assert_json_eq (result2, expected);
+ }
+ }
+
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gchar *escaped;
+ gchar *name;
+ gint i;
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/json/int-equal", test_int_equal);
+ g_test_add_func ("/json/int-hash", test_int_hash);
+
+ g_test_add_func ("/json/utf8-invalid", test_utf8_invalid);
+
+ g_test_add ("/json/get-string", TestCase, NULL,
+ setup, test_get_string, teardown);
+ g_test_add ("/json/get-int", TestCase, NULL,
+ setup, test_get_int, teardown);
+ g_test_add ("/json/get-double", TestCase, NULL,
+ setup, test_get_double, teardown);
+ g_test_add ("/json/get-bool", TestCase, NULL,
+ setup, test_get_bool, teardown);
+ g_test_add ("/json/get-null", TestCase, NULL,
+ setup, test_get_null, teardown);
+ g_test_add ("/json/get-strv", TestCase, NULL,
+ setup, test_get_strv, teardown);
+ g_test_add ("/json/get-array", TestCase, NULL,
+ setup, test_get_array, teardown);
+ g_test_add ("/json/get-object", TestCase, NULL,
+ setup, test_get_object, teardown);
+
+ g_test_add_func ("/json/parser-trims", test_parser_trims);
+ g_test_add_func ("/json/parser-empty", test_parser_empty);
+
+ for (i = 0; i < G_N_ELEMENTS (equal_fixtures); i++)
+ {
+ name = g_strdup_printf ("/json/equal/%s", equal_fixtures[i].name);
+ g_test_add_data_func (name, equal_fixtures + i, test_equal);
+ g_free (name);
+ }
+
+ for (i = 0; i < G_N_ELEMENTS (string_fixtures); i++)
+ {
+ escaped = g_strcanon (g_strdup (string_fixtures[i].str), COCKPIT_TEST_CHARS, '_');
+ name = g_strdup_printf ("/json/string/%s%d", escaped, i);
+ g_test_add_data_func (name, string_fixtures + i, test_string_encode);
+ g_free (escaped);
+ g_free (name);
+ }
+
+ for (i = 0; i < G_N_ELEMENTS (patch_fixtures); i++)
+ {
+ name = g_strdup_printf ("/json/patch/%s", patch_fixtures[i].name);
+ g_test_add_data_func (name, patch_fixtures + i, test_patch);
+ g_free (name);
+ }
+
+ g_test_add_func ("/json/write/infinite-nan", test_write_infinite_nan);
+ g_test_add_func ("/json/hashtable-objects", test_hashtable_objects);
+
+ g_test_add_func ("/json/walk", test_walk);
+
+ return g_test_run ();
+}
diff --git a/src/common/test-jsonfds.c b/src/common/test-jsonfds.c
new file mode 100644
index 0000000..a18bd5d
--- /dev/null
+++ b/src/common/test-jsonfds.c
@@ -0,0 +1,962 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitcontrolmessages.h"
+#include "cockpitfdpassing.h"
+#include "cockpitjsonprint.h"
+#include "cockpitmemfdread.h"
+#include "cockpitsocket.h"
+
+#include "testlib/cockpittest.h"
+
+#include <gio/gunixfdmessage.h>
+#include <gio/gunixcredentialsmessage.h>
+#include <glib-unix.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/mman.h>
+#include <stdio.h>
+
+/* --- testing of printing --- */
+
+typedef struct
+{
+ char buffer[1024];
+ FILE *stream;
+
+ char expected_buffer[1024];
+ gchar *expected_end;
+
+ size_t pagesize;
+ char *accessible;
+ char *inaccessible;
+} TestFixture;
+
+static int
+memfd_create_noexec (const char *name,
+ unsigned int flags)
+{
+ /* current kernels moan about not specifying exec mode */
+#ifdef MFD_NOEXEC_SEAL
+ int fd = memfd_create (name, flags | MFD_NOEXEC_SEAL);
+ /* fallback for older kernels */
+ if (fd != -1 || errno != EINVAL)
+ return fd;
+#endif
+ return memfd_create (name, flags);
+}
+
+static void
+test_fixture_setup (TestFixture *fixture,
+ gconstpointer user_data)
+{
+ /* set up the stream */
+ fixture->stream = fmemopen (fixture->buffer, sizeof fixture->buffer, "w");
+ setbuf (fixture->stream, NULL);
+ rewind (fixture->stream);
+
+ /* create a range of accessible bytes surrounded by memory that will
+ * cause a crash if accessed.
+ */
+ fixture->pagesize = sysconf(_SC_PAGE_SIZE);
+
+ /* allocate 3 pages of memory that will crash when accessed (PROT_NONE) */
+ char *region = mmap (NULL, 3 * fixture->pagesize, PROT_NONE,
+ MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+ g_assert (region != MAP_FAILED);
+
+ /* punch a read/writable hole in the middle of the 3 pages */
+ int r = mprotect (region + fixture->pagesize, fixture->pagesize, PROT_READ | PROT_WRITE);
+ g_assert (r == 0);
+
+ /* record the first and last(+1) accessible bytes. accessing the
+ * bytes immediately outside of this range is guaranteed to crash.
+ * this allows us to ensure proper memory behaviour of the code we're
+ * testing.
+ */
+ fixture->accessible = region + fixture->pagesize;
+ fixture->inaccessible = fixture->accessible + fixture->pagesize;
+
+ /* setup the expected buffer, empty to start */
+ fixture->expected_end = fixture->expected_buffer;
+}
+
+static void
+test_fixture_teardown (TestFixture *fixture,
+ gconstpointer user_data)
+{
+ char *region = fixture->accessible - fixture->pagesize;
+ munmap (region, 3 * fixture->pagesize);
+ fixture->accessible = NULL;
+ fixture->inaccessible = NULL;
+
+ fixture->expected_end = NULL;
+
+ fclose (fixture->stream);
+ fixture->stream = NULL;
+}
+
+static void
+test_fixture_expect (TestFixture *fixture,
+ const gchar *expected)
+{
+ fixture->expected_end = stpcpy (fixture->expected_end, expected);
+}
+
+static void
+test_fixture_compare_expected (TestFixture *fixture)
+{
+ /* Ensure that both strings are nul terminated */
+ fixture->buffer[ftell (fixture->stream)] = '\0';
+ *fixture->expected_end = '\0';
+
+ /* Ensure that neither string has embedded nuls */
+ g_assert_cmpint (strlen (fixture->buffer), ==, ftell (fixture->stream));
+ g_assert (strchr (fixture->expected_buffer, '\0') == fixture->expected_end);
+
+ /* Compare! */
+ g_assert_cmpstr (fixture->buffer, ==, fixture->expected_buffer);
+}
+
+static void
+test_print_string (TestFixture *fixture,
+ gconstpointer user_data)
+{
+ /* create a string with every possible byte in it and check that
+ * everything is correctly escaped
+ */
+ char buffer[256];
+ int offset = 0;
+ char tmp[10];
+
+ test_fixture_expect (fixture, ", \"key\": \"");
+
+ for (int c = 1; c < 32; c++) /* control characters (before space) */
+ {
+ /* ascii control characters printed as unicode escapes */
+ snprintf (tmp, sizeof tmp, "\\u%04x", c);
+ test_fixture_expect (fixture, tmp);
+
+ buffer[offset++] = c;
+ }
+
+ test_fixture_expect (fixture, " !");
+ buffer[offset++] = 32; /* space */
+ buffer[offset++] = 33; /* ! */
+
+ test_fixture_expect (fixture, "\\\"");
+ buffer[offset++] = 34; /* " */
+
+ for (int c = 35; c < 92; c++) /* # through [ */
+ {
+ snprintf (tmp, sizeof tmp, "%c", c);
+ test_fixture_expect (fixture, tmp);
+ buffer[offset++] = c;
+ }
+
+ test_fixture_expect (fixture, "\\\\");
+ buffer[offset++] = 92; /* \ */
+
+ for (int c = 93; c < 127; c++) /* ] through ~ */
+ {
+ snprintf (tmp, sizeof tmp, "%c", c);
+ test_fixture_expect (fixture, tmp);
+ buffer[offset++] = c;
+ }
+
+ test_fixture_expect (fixture, "\\u007f");
+ buffer[offset++] = 127; /* DEL */
+
+ for (int c = 128; c < 256; c++) /* non-ascii */
+ {
+ test_fixture_expect (fixture, "?");
+ buffer[offset++] = c;
+ }
+
+ buffer[offset++] = '\0';
+ g_assert_cmpint (offset, ==, sizeof buffer);
+ test_fixture_expect (fixture, "\"");
+
+ /* print it with -1, correct length, and "too big" length */
+ cockpit_json_print_string_property (fixture->stream, "key", buffer, -1);
+ test_fixture_compare_expected (fixture);
+ rewind (fixture->stream);
+
+ cockpit_json_print_string_property (fixture->stream, "key", buffer, 255);
+ test_fixture_compare_expected (fixture);
+ rewind (fixture->stream);
+
+ cockpit_json_print_string_property (fixture->stream, "key", buffer, 256);
+ test_fixture_compare_expected (fixture);
+ rewind (fixture->stream);
+}
+
+/* The following test tries to catch bad behaviour from the scanner that
+ * finds the groups of unescaped characters for fast printing. It
+ * ensures that:
+ *
+ * 1) the groups are always scanned correctly and correct escaped
+ * output is produced
+ *
+ * 2) a nul is always honoured, regardless of a larger given
+ * max_length parameter
+ *
+ * 3) max_length is always honoured, regardless of if a nul is present
+ * or not.
+ *
+ * In particular, we use our fixture to ensure that we never touch
+ * memory past either the final nul character, or past the specified
+ * max_length.
+ *
+ * We test strings composed of a given number 'groups'. Each group
+ * consists of a certain number of repetitions ('reps') of a given
+ * character.
+ *
+ * We use a sequence counter to determine the character and the number
+ * of reps for each group. At each step we take the modulus of a
+ * division to make a decision about the given variable, using the whole
+ * result as a residual for future decisions. This effectively
+ * implements a counter with an arbitrary radix at each position. Once
+ * we see a non-zero resitual, we know that we've surely exhausted all
+ * possible combinations.
+ *
+ * These constants could easily be a bit higher, but the running time
+ * explodes pretty quickly, and this test benefits from being run under
+ * valgrind.
+ */
+#define MIN_GROUPS 1
+#define MAX_GROUPS 3
+#define MAX_REPS 5
+
+static guint
+divmod (guint *residual,
+ guint divisor)
+{
+ guint result = *residual % divisor;
+
+ *residual /= divisor;
+
+ return result;
+}
+
+static void
+test_print_string_memory_safety (TestFixture *fixture,
+ gconstpointer user_data)
+{
+ gchar characters[] = { '\n', ' ', 'a', '\\', '\"', 0xcc };
+ const gchar *escaped[] = { "\\u000a", " ", "a", "\\\\", "\\\"", "?" };
+ gchar buffer[MAX_GROUPS * MAX_REPS];
+
+ for (gint n_groups = MIN_GROUPS; n_groups <= MAX_GROUPS; n_groups++)
+ {
+ for (guint seq = 0;; seq++)
+ {
+ guint residual = seq;
+ gint length = 0;
+ int reps;
+
+ fixture->expected_end = stpcpy (fixture->expected_buffer, ", \"key\": \"");
+
+ for (gint group = 0; group < n_groups; group++)
+ {
+ gint c = divmod (&residual, sizeof characters);
+ reps = divmod (&residual, MAX_REPS) + 1;
+
+ memset (buffer + length, characters[c], reps);
+ length += reps;
+
+ for (gint i = 0; i < reps; i++)
+ test_fixture_expect (fixture, escaped[c]);
+ }
+
+ g_assert_cmpint (length, <=, sizeof buffer);
+
+ if (residual)
+ /* non-zero residual → we've already tried all cases */
+ break;
+
+ test_fixture_expect (fixture, "\"");
+
+ /* Test various cases of the string not being nul terminated.
+ * We avoid starting from 0 each time in order to avoid
+ * effectively testing fewer groups. `reps` is leftover from
+ * the last iteration of the loop above. Starting at:
+ *
+ * length - reps + 1
+ *
+ * makes sure that we see at least one character from this
+ * final group.
+ *
+ * We position the subset of the string at `region` (at a
+ * negative offset to the inaccessible area in the fixture) to
+ * ensure that we don't read more than the requested `i`
+ * characters.
+ *
+ * This test doesn't ensure that the correct output is
+ * produced. It's difficult to cut the expected string to the
+ * correct length, given the different lengths of escaped
+ * characters.
+ */
+ for (gint i = length - reps + 1; i <= length; i++)
+ {
+ gchar *region = fixture->inaccessible - i;
+
+ memcpy (region, buffer, i);
+ cockpit_json_print_string_property (fixture->stream, "key", region, i);
+ rewind (fixture->stream);
+ }
+
+ /* These ones test a complete nul-terminated string. As such,
+ * we configure the region to be exactly large enough to hold
+ * the nul-terminated string. Then we try giving different
+ * lengths.
+ */
+ gchar *region = fixture->inaccessible - (length + 1);
+ memcpy (region, buffer, length);
+ region[length] = '\0';
+
+ /* First with -1 */
+ cockpit_json_print_string_property (fixture->stream, "key", region, -1);
+ test_fixture_compare_expected (fixture);
+ rewind (fixture->stream);
+
+ /* Then the exact length */
+ cockpit_json_print_string_property (fixture->stream, "key", region, length);
+ test_fixture_compare_expected (fixture);
+ rewind (fixture->stream);
+
+ /* Then lengths bigger than the string (keeping in mind that
+ * this is a *max* length parameter).
+ */
+ for (gint i = length + 1; i <= length + 3; i++)
+ {
+ cockpit_json_print_string_property (fixture->stream, "key", region, i);
+ test_fixture_compare_expected (fixture);
+ rewind (fixture->stream);
+ }
+ }
+ }
+}
+
+static void
+test_print_numeric (TestFixture *fixture,
+ gconstpointer user_data)
+{
+ cockpit_json_print_integer_property (fixture->stream, "zero", 0);
+ test_fixture_expect (fixture, ", \"zero\": 0");
+
+ cockpit_json_print_integer_property (fixture->stream, "one", 1);
+ test_fixture_expect (fixture, ", \"one\": 1");
+
+ cockpit_json_print_integer_property (fixture->stream, "million", 1000000);
+ test_fixture_expect (fixture, ", \"million\": 1000000");
+
+ /* check that numbers that can't be encoded in double still work */
+ guint64 extra_big = 9007199254740993ull; /* 2^53 + 1 */
+ g_assert_cmpint (extra_big, !=, (guint64) (double) extra_big);
+ cockpit_json_print_integer_property (fixture->stream, "extrabig", extra_big);
+ test_fixture_expect (fixture, ", \"extrabig\": 9007199254740993");
+
+ /* check these special values to make sure they're being handled as uint64 */
+ cockpit_json_print_integer_property (fixture->stream, "intmax", INT64_MAX);
+ test_fixture_expect (fixture, ", \"intmax\": 9223372036854775807");
+ cockpit_json_print_integer_property (fixture->stream, "intmaxplusone", INT64_MAX + 1ull);
+ test_fixture_expect (fixture, ", \"intmaxplusone\": 9223372036854775808");
+ cockpit_json_print_integer_property (fixture->stream, "uintmax", UINT64_MAX);
+ test_fixture_expect (fixture, ", \"uintmax\": 18446744073709551615");
+ cockpit_json_print_integer_property (fixture->stream, "minus1", -1);
+ test_fixture_expect (fixture, ", \"minus1\": 18446744073709551615");
+
+ /* make sure it all worked out */
+ test_fixture_compare_expected (fixture);
+}
+
+static void
+test_print_boolean (TestFixture *fixture,
+ gconstpointer user_data)
+{
+ test_fixture_expect (fixture, ", \"true\": true, \"false\": false, \"alsotrue\": true");
+
+ cockpit_json_print_bool_property (fixture->stream, "true", true);
+ cockpit_json_print_bool_property (fixture->stream, "false", false);
+ cockpit_json_print_bool_property (fixture->stream, "alsotrue", 123456);
+
+ test_fixture_compare_expected (fixture);
+}
+
+/* --- testing of reading --- */
+
+static void
+test_memfd_simple (void)
+{
+ FILE *stream;
+
+ stream = cockpit_json_print_open_memfd ("test", 1);
+ gint fd = cockpit_json_print_finish_memfd (&stream);
+
+ g_autoptr(GError) error = NULL;
+ g_autofree gchar *content = cockpit_memfd_read (fd, &error);
+ g_assert_no_error (error);
+ close (fd);
+
+ g_assert_cmpstr (content, ==, "{\"version\": 1}");
+}
+
+static void
+test_memfd_error_cases (void)
+{
+ g_autoptr(GError) error = NULL;
+ g_autofree gchar *content = NULL;
+ FILE *stream;
+ gint fd;
+ gint r;
+
+ /* not a memfd */
+ fd = open ("/dev/null", O_RDONLY);
+
+ content = cockpit_memfd_read (fd, &error);
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*not memfd?*");
+ g_assert (content == NULL);
+ g_clear_error (&error);
+ close (fd);
+
+
+ /* memfd is not properly sealed */
+ fd = memfd_create_noexec ("xyz", MFD_CLOEXEC);
+
+ content = cockpit_memfd_read (fd, &error);
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*incorrect seals set*");
+ g_assert (content == NULL);
+ g_clear_error (&error);
+ close (fd);
+
+ /* memfd is empty */
+ fd = memfd_create_noexec ("xyz", MFD_ALLOW_SEALING | MFD_CLOEXEC);
+ r = fcntl (fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE);
+ g_assert (r == 0);
+
+ content = cockpit_memfd_read (fd, &error);
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*empty*");
+ g_assert (content == NULL);
+ g_clear_error (&error);
+ close (fd);
+
+
+ /* memfd is too big */
+ stream = cockpit_json_print_open_memfd ("xyz", 1);
+ fprintf (stream, "%20000s", "");
+ fd = cockpit_json_print_finish_memfd (&stream);
+
+ content = cockpit_memfd_read (fd, &error);
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*unreasonably large*");
+ g_assert (content == NULL);
+ g_clear_error (&error);
+ close (fd);
+
+
+ /* memfd can't be read */
+ stream = cockpit_json_print_open_memfd ("xyz", 1);
+ int tmpfd = cockpit_json_print_finish_memfd (&stream);
+ gchar procfile[80];
+ snprintf (procfile, sizeof procfile, "/proc/self/fd/%d", tmpfd);
+ fd = open (procfile, O_WRONLY);
+ g_assert_cmpint (fd, !=, -1);
+ r = close (tmpfd);
+ g_assert_cmpint (r, ==, 0);
+
+ content = cockpit_memfd_read (fd, &error);
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_BADF, "*failed to read*");
+ g_assert (content == NULL);
+ g_clear_error (&error);
+ close (fd);
+
+
+ /* memfd contains a nul */
+ stream = cockpit_json_print_open_memfd ("xyz", 1);
+ fputc (0, stream);
+ fd = cockpit_json_print_finish_memfd (&stream);
+
+ content = cockpit_memfd_read (fd, &error);
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*contains nul*");
+ g_assert (content == NULL);
+ g_clear_error (&error);
+ close (fd);
+
+ /* memfd contains non-ascii */
+ stream = cockpit_json_print_open_memfd ("xyz", 1);
+ fputc (0xcc, stream);
+ fd = cockpit_json_print_finish_memfd (&stream);
+
+ content = cockpit_memfd_read (fd, &error);
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*contains non-ASCII*");
+ g_assert (content == NULL);
+ g_clear_error (&error);
+ close (fd);
+}
+
+static void
+test_memfd_json (void)
+{
+ FILE *stream;
+
+ stream = cockpit_json_print_open_memfd ("test", 1);
+ cockpit_json_print_string_property (stream, "hello", "world", -1);
+ cockpit_json_print_integer_property (stream, "size", 200);
+ cockpit_json_print_bool_property (stream, "truth", true);
+ cockpit_json_print_bool_property (stream, "falsth", false);
+ gint fd = cockpit_json_print_finish_memfd (&stream);
+
+ g_autoptr(GError) error = NULL;
+ g_autoptr(JsonObject) object = cockpit_memfd_read_json (fd, &error);
+ g_assert_no_error (error);
+ close (fd);
+
+ g_assert_cmpint (json_object_get_int_member (object, "version"), ==, 1);
+ g_assert_cmpstr (json_object_get_string_member (object, "hello"), ==, "world");
+ g_assert_cmpint (json_object_get_int_member (object, "size"), ==, 200);
+ g_assert_cmpint (json_object_get_boolean_member (object, "truth"), ==, TRUE);
+ g_assert_cmpint (json_object_get_boolean_member (object, "falsth"), ==, FALSE);
+}
+
+static void
+test_memfd_json_error_cases (void)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(JsonObject) object = NULL;
+ gint fd;
+ gint r;
+
+ /* invalid json */
+ fd = memfd_create_noexec ("xyz", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+ g_assert_cmpint (write (fd, "beh", 3), ==, 3);
+ r = fcntl (fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE);
+ g_assert (r == 0);
+ object = cockpit_memfd_read_json (fd, &error);
+ g_assert (object == NULL);
+ cockpit_assert_error_matches (error, JSON_PARSER_ERROR, JSON_PARSER_ERROR_INVALID_BAREWORD, "*unexpected identifier*");
+ g_clear_error (&error);
+ close (fd);
+
+ /* valid json, but not an object */
+ fd = memfd_create_noexec ("xyz", MFD_CLOEXEC | MFD_ALLOW_SEALING);
+ g_assert_cmpint (write (fd, "[]", 2), ==, 2);
+ r = fcntl (fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE);
+ g_assert (r == 0);
+ object = cockpit_memfd_read_json (fd, &error);
+ g_assert (object == NULL);
+ cockpit_assert_error_matches (error, JSON_PARSER_ERROR, -1, "*Not a JSON object*");
+ close (fd);
+
+}
+
+/* --- unix socket testing --- */
+
+static GSList *live_control_messages;
+
+static void
+assert_live_control_messages (gint expected)
+{
+ g_assert_cmpint (g_slist_length (live_control_messages), ==, expected);
+}
+
+static void
+remove_message_from_list (gpointer data,
+ GObject *where_the_object_was)
+{
+ for (GSList **n = &live_control_messages; *n; n = &(*n)->next)
+ if ((*n)->data == where_the_object_was)
+ {
+ *n = g_slist_delete_link (*n, *n);
+ return;
+ }
+
+ g_error ("Couldn't find control message %p in list", where_the_object_was);
+
+}
+
+static void
+receive_cmsgs (GSocket *socket,
+ CockpitControlMessages *ccm)
+{
+ gchar buffer[1];
+ GInputVector vector = { buffer, sizeof buffer };
+ GError *error = NULL;
+ g_socket_receive_message (socket,
+ NULL, /* address */
+ &vector, 1,
+ &ccm->messages, &ccm->n_messages,
+ NULL, NULL,
+ &error);
+
+ /* Use this to make sure all messages are getting properly freed */
+ for (gint i = 0; i < ccm->n_messages; i++)
+ {
+ live_control_messages = g_slist_prepend (live_control_messages, ccm->messages[i]);
+ g_object_weak_ref (G_OBJECT (ccm->messages[i]), remove_message_from_list, NULL);
+ }
+
+ g_assert_no_error (error);
+}
+
+static void
+receive_nothing (GSocket *socket)
+{
+ g_auto(CockpitControlMessages) ccm = COCKPIT_CONTROL_MESSAGES_INIT;
+
+ receive_cmsgs (socket, &ccm);
+
+ g_assert (cockpit_control_messages_empty (&ccm));
+}
+
+static gint *
+receive_fds (GSocket *socket,
+ gint *out_nfds,
+ GError **error)
+{
+ g_auto(CockpitControlMessages) ccm = COCKPIT_CONTROL_MESSAGES_INIT;
+
+ receive_cmsgs (socket, &ccm);
+
+ int n_fds;
+ const gint *fds = cockpit_control_messages_peek_fd_list (&ccm, &n_fds, error);
+
+ if (fds == NULL)
+ return NULL;
+
+ gint *result = g_new (int, n_fds + 1);
+ for (gint i = 0; i < n_fds; i++)
+ result[i] = dup (fds[i]);
+ result[n_fds] = -1;
+ *out_nfds = n_fds;
+ return result;
+}
+
+static void
+free_fds (gint **inout_fds,
+ gint *inout_nfds)
+{
+ gint *fds = *inout_fds;
+ gint nfds = *inout_nfds;
+
+ for (gint i = 0; i < nfds; i++)
+ {
+ g_assert (fds[i] != -1);
+ int r = close (fds[i]);
+ g_assert (r == 0);
+ }
+ g_assert (fds[nfds] == -1);
+
+ g_free (fds);
+
+ *inout_fds = NULL;
+ *inout_nfds = 0;
+}
+
+static gint
+receive_fd (GSocket *socket,
+ GError **error)
+{
+ g_auto(CockpitControlMessages) ccm = COCKPIT_CONTROL_MESSAGES_INIT;
+
+ receive_cmsgs (socket, &ccm);
+
+ int fd = cockpit_control_messages_peek_single_fd (&ccm, error);
+
+ if (fd == -1)
+ return -1;
+
+ return dup (fd);
+}
+
+static void
+send_cmsgs (GSocket *socket,
+ GSocketControlMessage **messages,
+ gint n_messages,
+ gint n_bytes)
+{
+ const gchar buffer[100] = "";
+ g_assert_cmpint(n_bytes, <=, sizeof buffer);
+ GOutputVector vector = { buffer, n_bytes };
+ GError *error = NULL;
+ g_socket_send_message (socket,
+ NULL, /* address */
+ &vector, 1,
+ messages, n_messages,
+ 0, NULL, &error);
+ g_assert_no_error (error);
+}
+
+static void
+send_nothing (GSocket *socket,
+ gint n_bytes)
+{
+ send_cmsgs (socket, NULL, 0, n_bytes);
+}
+
+static GSocketControlMessage *
+make_fd_message (const gint *fds,
+ gint n_fds)
+{
+ g_autoptr(GUnixFDList) fdl = g_unix_fd_list_new ();
+
+ for (gint i = 0; i < n_fds; i++)
+ {
+ GError *error = NULL;
+ g_unix_fd_list_append (fdl, fds[i], &error);
+ g_assert_no_error (error);
+ }
+
+ return g_unix_fd_message_new_with_fd_list (fdl);
+}
+
+static void
+send_fds (GSocket *socket,
+ const gint *fds,
+ gint n_fds)
+{
+ g_autoptr(GSocketControlMessage) fdm = make_fd_message (fds, n_fds);
+ send_cmsgs (socket, &fdm, 1, 1);
+}
+
+static void
+send_fd (GSocket *socket,
+ gint fd)
+{
+ send_fds (socket, &fd, 1);
+}
+
+static void
+assert_base_state (GSocket *one,
+ GSocket *two)
+{
+ assert_live_control_messages (0);
+ g_assert (g_socket_condition_check (one, G_IO_IN | G_IO_OUT) == G_IO_OUT);
+ g_assert (g_socket_condition_check (two, G_IO_IN | G_IO_OUT) == G_IO_OUT);
+}
+
+static void
+test_unix_socket_simple (void)
+{
+ g_autoptr(GSocket) one, two;
+
+ cockpit_socket_socketpair (&one, &two);
+ assert_base_state (one, two);
+
+ /* boring */
+ send_nothing (one, 1);
+ receive_nothing (two);
+ assert_base_state (one, two);
+
+ send_nothing (two, 1);
+ receive_nothing (one);
+ assert_base_state (one, two);
+
+ /* try a single fd */
+ send_fd (one, 1);
+ GError *error = NULL;
+ gint fd = receive_fd (two, &error);
+ g_assert_no_error (error);
+ g_assert (fd != -1);
+ close (fd);
+ assert_base_state (one, two);
+
+ /* try multiple fds */
+ send_fds (one, (gint []){ 0, 1, 2}, 3);
+ gint n_fds = 0; /* gcc is unhappy without this... */
+ gint *fds = receive_fds (two, &n_fds, &error);
+ g_assert_no_error (error);
+ g_assert (fds != NULL);
+ g_assert_cmpint (n_fds, ==, 3);
+ free_fds (&fds, &n_fds);
+ assert_base_state (one, two);
+
+ /* mix-and-match with cockpitfdpassing */
+ int two_fd = g_socket_get_fd (two);
+ g_unix_set_fd_nonblocking (two_fd, FALSE, &error);
+ g_assert_no_error (error);
+
+ /* one -> two */
+ send_fd (one, 1);
+ int r = cockpit_socket_receive_fd (two_fd, &fd);
+ g_assert_cmpint (r, ==, 1);
+ g_assert (fd != -1);
+ close (fd);
+
+ /* two -> one */
+ cockpit_socket_send_fd (two_fd, 1);
+ fd = receive_fd (one, &error);
+ g_assert_no_error (error);
+ g_assert (fd != -1);
+ close (fd);
+ assert_base_state (one, two);
+}
+
+static void
+test_unix_socket_partial_read (void)
+{
+ g_autoptr(GSocket) one, two;
+
+ cockpit_socket_socketpair (&one, &two);
+ assert_base_state (one, two);
+
+ /* test unspecified behaviour, which we rely on: the cmsg should be
+ * read with the first byte of the message with which it was sent.
+ *
+ * we depend on this because we start on the cockpit-ws side by
+ * reading a single byte, but we will send the json blob as part of
+ * the first full packet from cockpit-tls.
+ */
+ send_nothing (one, 10);
+ int fd1 = 1;
+ g_autoptr(GSocketControlMessage) fdm = make_fd_message (&fd1, 1);
+ send_cmsgs (one, &fdm, 1, 10);
+
+ for (gint i = 0; i < 20; i++)
+ {
+ g_autoptr(GError) error = NULL;
+ gint fd = receive_fd (two, &error);
+
+ if (fd != -1)
+ {
+ /* we expect to get this at the 11th try */
+ g_assert_cmpint (i, ==, 10);
+ close (fd);
+ }
+ else
+ {
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*0 control message*");
+ }
+ }
+}
+
+static void
+test_unix_socket_error_cases (void)
+{
+ g_autoptr(GSocket) one, two;
+
+ cockpit_socket_socketpair (&one, &two);
+ assert_base_state (one, two);
+
+ /* try receiving an fd when nothing was sent */
+ send_nothing (one, 1);
+ GError *error = NULL;
+ int fd = receive_fd (two, &error);
+ g_assert (fd == -1);
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*0 control message*");
+ g_clear_error (&error);
+ assert_base_state (one, two);
+
+ /* see what happens if we send more fds than expected */
+ send_fds (one, (const gint []){ 0, 1, 2}, 3);
+ fd = receive_fd (two, &error);
+ g_assert (fd == -1);
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "*received 3*1 expected*");
+ g_clear_error (&error);
+ assert_base_state (one, two);
+
+ /* The remaining tests rely on receiving SCM_CREDENTIALS. We need to
+ * enable SO_PASSCRED for that.
+ */
+ int truth = 1;
+ int r = setsockopt (g_socket_get_fd (two), SOL_SOCKET, SO_PASSCRED, &truth, sizeof truth);
+ g_assert (r == 0);
+
+ /* see what happens if we send the wrong message type */
+ g_autoptr(GSocketControlMessage) creds = g_unix_credentials_message_new ();
+ send_cmsgs (one, &creds, 1, 1);
+ fd = receive_fd (two, &error);
+ g_assert (fd == -1);
+ cockpit_assert_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_INVAL,
+ "*GUnixCredentialsMessage*GUnixFDMessage expected*");
+ g_clear_error (&error);
+ assert_base_state (one, two);
+
+ /* see what happens if we send too many messages */
+ g_autoptr(GUnixFDList) fdl = g_unix_fd_list_new ();
+ g_unix_fd_list_append (fdl, 1, &error);
+ g_assert_no_error (error);
+ g_autoptr(GSocketControlMessage) fdm = g_unix_fd_message_new_with_fd_list (fdl);
+ GSocketControlMessage *messages[] = { creds, fdm };
+ send_cmsgs (one, messages, G_N_ELEMENTS (messages), 1);
+ fd = receive_fd (two, &error);
+ g_assert (fd == -1);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert (strstr (error->message, "2 control messages (one message"));
+ g_clear_error (&error);
+ assert_base_state (one, two);
+}
+
+/* --- putting it all together (unix sockets) --- */
+
+static void
+test_unix_socket_combined (void)
+{
+ g_autoptr(GSocket) one, two;
+
+ cockpit_socket_socketpair (&one, &two);
+ assert_base_state (one, two);
+
+ FILE *stream = cockpit_json_print_open_memfd ("xyz", 1);
+ cockpit_json_print_string_property (stream, "test", "it worked!", -1);
+ gint fd = cockpit_json_print_finish_memfd (&stream);
+
+ send_fd (one, fd);
+ close (fd);
+
+ g_auto(CockpitControlMessages) ccm = COCKPIT_CONTROL_MESSAGES_INIT;
+ receive_cmsgs (two, &ccm);
+
+ g_autoptr(GError) error = NULL;
+ g_autoptr(JsonObject) json = cockpit_memfd_read_json_from_control_messages (&ccm, &error);
+ g_assert_no_error (error);
+
+ g_assert_cmpint (json_object_get_int_member (json, "version"), ==, 1);
+ g_assert_cmpstr (json_object_get_string_member (json, "test"), ==, "it worked!");
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/json/fd/print/string", TestFixture, NULL,
+ test_fixture_setup, test_print_string, test_fixture_teardown);
+ g_test_add ("/json/fd/print/string/memory-safety", TestFixture, NULL,
+ test_fixture_setup, test_print_string_memory_safety, test_fixture_teardown);
+ g_test_add ("/json/fd/print/numeric", TestFixture, NULL,
+ test_fixture_setup, test_print_numeric, test_fixture_teardown);
+ g_test_add ("/json/fd/print/boolean", TestFixture, NULL,
+ test_fixture_setup, test_print_boolean, test_fixture_teardown);
+
+ g_test_add_func ("/json/fd/memfd/simple", test_memfd_simple);
+ g_test_add_func ("/json/fd/memfd/error-cases", test_memfd_error_cases);
+ g_test_add_func ("/json/fd/memfd/json", test_memfd_json);
+ g_test_add_func ("/json/fd/memfd/json/error-cases", test_memfd_json_error_cases);
+ g_test_add_func ("/json/fd/unix-socket/simple", test_unix_socket_simple);
+ g_test_add_func ("/json/fd/unix-socket/partial-read", test_unix_socket_partial_read);
+ g_test_add_func ("/json/fd/unix-socket/error-cases", test_unix_socket_error_cases);
+ g_test_add_func ("/json/fd/unix-socket/combined", test_unix_socket_combined);
+
+ return g_test_run ();
+}
diff --git a/src/common/test-locale.c b/src/common/test-locale.c
new file mode 100644
index 0000000..5145d7e
--- /dev/null
+++ b/src/common/test-locale.c
@@ -0,0 +1,200 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitlocale.h"
+
+#include "testlib/cockpittest.h"
+
+#include <libintl.h>
+#include <string.h>
+
+typedef struct {
+ const gchar *language;
+ const gchar *encoding;
+ const gchar *locale;
+ const gchar *shorter;
+} FromFixture;
+
+static FromFixture from_fixtures[] = {
+ { "en", NULL, "en", "en" },
+ { "en-us", NULL, "en_US", "en" },
+ { "en-us", "UTF-8", "en_US.UTF-8", "en" },
+ { "zh-cn", NULL, "zh_CN", "zh" },
+ { "zh-cn", "UTF-8", "zh_CN.UTF-8", "zh" },
+ { NULL, NULL, "C", "C" },
+};
+
+static void
+test_from_language (gconstpointer data)
+{
+ const FromFixture *fixture = data;
+ gchar *locale;
+ gchar *shorter;
+
+ locale = cockpit_locale_from_language (fixture->language, fixture->encoding, &shorter);
+ g_assert_cmpstr (locale, ==, fixture->locale);
+ if (locale)
+ g_assert_cmpstr (shorter, ==, fixture->shorter);
+ g_free (locale);
+ g_free (shorter);
+
+ locale = cockpit_locale_from_language (fixture->language, fixture->encoding, NULL);
+ g_assert_cmpstr (locale, ==, fixture->locale);
+ g_free (locale);
+}
+
+typedef struct {
+ const gchar *language;
+ const gchar *lang;
+} SetFixture;
+
+static SetFixture set_fixtures[] = {
+ { "en-us", "en_US.UTF-8" },
+ { "de-de", "de_DE.UTF-8" },
+ { "zh-cn", "zh_CN.UTF-8" },
+ { "__xx;%%%", NULL },
+ { NULL, "C" },
+ { "abcdefghijklmnopqrstuvwxyz-abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", NULL },
+};
+
+static gboolean
+locale_available (const gchar *locale)
+{
+ GError *error = NULL;
+ gchar *dot;
+ gchar *output;
+ gint status;
+ gchar *copy;
+ gboolean ret;
+
+ g_spawn_command_line_sync ("locale -a", &output, NULL, &status, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (status, ==, 0);
+
+ copy = g_strdup_printf ("\n%s", locale);
+ dot = strchr (copy, '.');
+ if (dot)
+ dot[1] = '\0';
+
+ ret = strstr (output, copy) ? TRUE : FALSE;
+
+ g_free (output);
+ g_free (copy);
+
+ return ret;
+}
+
+static void
+verify_lc_messages_locale (const gchar *expected)
+{
+ g_autofree gchar *expected_line = g_strdup_printf ("LC_MESSAGES=\"%s\"\n", expected);
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GSubprocess) subprocess = g_subprocess_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error, "locale", NULL);
+ g_assert_no_error (error);
+ g_autofree gchar *output = NULL;
+ g_subprocess_communicate_utf8 (subprocess, NULL, NULL, &output, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (strstr (output, expected_line) != NULL);
+}
+
+static void
+test_set_language (const gconstpointer data)
+{
+ const SetFixture *fixture = data;
+ const gchar *old;
+
+ /* Check if the locale is available on the test system */
+ if (fixture->lang && !locale_available (fixture->lang))
+ {
+ g_test_skip ("locale not available");
+ return;
+ }
+
+ old = g_getenv ("LANG");
+
+ cockpit_locale_set_language (fixture->language);
+
+ /* Was supposed to fail */
+ if (fixture->lang == NULL)
+ {
+ g_assert_cmpstr (old, ==, g_getenv ("LANG"));
+ }
+
+ /* Was supposed to succeed */
+ else
+ {
+ g_assert_cmpstr (fixture->lang, ==, g_getenv ("LANG"));
+ verify_lc_messages_locale (fixture->lang);
+ }
+
+ /* A second time, exercise cache code */
+ cockpit_locale_set_language (fixture->language);
+
+ /* Was supposed to fail */
+ if (fixture->lang == NULL)
+ {
+ g_assert_cmpstr (old, ==, g_getenv ("LANG"));
+ }
+
+ /* Was supposed to succeed */
+ else
+ {
+ g_assert_cmpstr (fixture->lang, ==, g_getenv ("LANG"));
+ verify_lc_messages_locale (fixture->lang);
+ }
+
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ const gchar *val;
+ gchar *name;
+ gint i;
+
+ g_unsetenv ("LANGUAGE");
+ g_unsetenv ("LANG");
+ g_unsetenv ("LC_ALL");
+ g_unsetenv ("LC_MESSAGES");
+
+ bindtextdomain ("test", BUILDDIR "/src/common/mock-locale");
+ cockpit_test_init (&argc, &argv);
+
+ for (i = 0; i < G_N_ELEMENTS (from_fixtures); i++)
+ {
+ name = g_strdup_printf ("/locale/from-language/%s", from_fixtures[i].locale);
+ g_test_add_data_func (name, from_fixtures + i, test_from_language);
+ g_free (name);
+ }
+
+ for (i = 0; i < G_N_ELEMENTS (set_fixtures); i++)
+ {
+ val = set_fixtures[i].language;
+ if (!val)
+ val = "null";
+ name = g_strdup_printf ("/locale/set-language/%s", val);
+ g_test_add_data_func (name, set_fixtures + i, test_set_language);
+ g_free (name);
+ }
+
+ return g_test_run ();
+}
diff --git a/src/common/test-pipe.c b/src/common/test-pipe.c
new file mode 100644
index 0000000..446f368
--- /dev/null
+++ b/src/common/test-pipe.c
@@ -0,0 +1,1360 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitpipe.h"
+
+#include "cockpitsystem.h"
+
+#include "testlib/cockpittest.h"
+#include "testlib/mock-pressure.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gio/gunixsocketaddress.h>
+
+#include <sys/uio.h>
+#include <sys/wait.h>
+#include <string.h>
+
+/* ----------------------------------------------------------------------------
+ * Mock
+ */
+
+static GType mock_echo_pipe_get_type (void) G_GNUC_CONST;
+
+typedef struct {
+ CockpitPipe parent;
+ GByteArray *received;
+ gboolean closed;
+ gchar *problem;
+} MockEchoPipe;
+
+typedef CockpitPipeClass MockEchoPipeClass;
+
+G_DEFINE_TYPE (MockEchoPipe, mock_echo_pipe, COCKPIT_TYPE_PIPE);
+
+static void
+mock_echo_pipe_read (CockpitPipe *pipe,
+ GByteArray *buffer,
+ gboolean end_of_data)
+{
+ MockEchoPipe *self = (MockEchoPipe *)pipe;
+ g_byte_array_append (self->received, buffer->data, buffer->len);
+ g_byte_array_set_size (buffer, 0);
+}
+
+static void
+mock_echo_pipe_close (CockpitPipe *pipe,
+ const gchar *problem)
+{
+ MockEchoPipe *self = (MockEchoPipe *)pipe;
+ g_assert (!self->closed);
+ self->closed = TRUE;
+ self->problem = g_strdup (problem);
+}
+
+static void
+mock_echo_pipe_init (MockEchoPipe *self)
+{
+ self->received = g_byte_array_new ();
+}
+
+static void
+mock_echo_pipe_finalize (GObject *object)
+{
+ MockEchoPipe *self = (MockEchoPipe *)object;
+
+ g_byte_array_free (self->received, TRUE);
+ g_free (self->problem);
+
+ G_OBJECT_CLASS (mock_echo_pipe_parent_class)->finalize (object);
+}
+static void
+mock_echo_pipe_class_init (MockEchoPipeClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ CockpitPipeClass *pipe_class = COCKPIT_PIPE_CLASS (klass);
+
+ object_class->finalize = mock_echo_pipe_finalize;
+
+ pipe_class->read = mock_echo_pipe_read;
+ pipe_class->close = mock_echo_pipe_close;
+}
+
+/* ----------------------------------------------------------------------------
+ * Testing
+ */
+
+typedef struct {
+ CockpitPipe *pipe;
+ guint timeout;
+} TestCase;
+
+typedef struct {
+ const gchar *pipe_type_name;
+ const gchar *command;
+ gboolean no_timeout;
+} TestFixture;
+
+static gboolean
+on_timeout_abort (gpointer unused)
+{
+ g_error ("timed out");
+ return FALSE;
+}
+
+static void
+setup_timeout (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ if (!fixture || !fixture->no_timeout)
+ tc->timeout = g_timeout_add_seconds (10, on_timeout_abort, tc);
+}
+
+static void
+setup_simple (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ const gchar *pipe_type;
+ GError *error = NULL;
+ gchar **argv;
+ int fds[2];
+ GPid pid = 0;
+
+ setup_timeout (tc, data);
+
+ pipe_type = "MockEchoPipe";
+ if (fixture && fixture->pipe_type_name)
+ pipe_type = fixture->pipe_type_name;
+
+ if (fixture && fixture->command)
+ {
+ g_shell_parse_argv (fixture->command, NULL, &argv, &error);
+ g_assert_no_error (error);
+
+ g_spawn_async_with_pipes (NULL, argv, NULL,
+ G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH, NULL, NULL,
+ &pid, fds + 1, fds + 0, NULL,
+ &error);
+ g_assert_no_error (error);
+ g_strfreev (argv);
+ }
+ else
+ {
+ if (pipe (fds) < 0)
+ g_assert_not_reached ();
+ }
+
+ tc->pipe = g_object_new (g_type_from_name (pipe_type),
+ "name", "test",
+ "in-fd", fds[0],
+ "out-fd", fds[1],
+ "pid", pid,
+ NULL);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ if (tc->pipe)
+ {
+ g_object_add_weak_pointer (G_OBJECT (tc->pipe),
+ (gpointer *)&tc->pipe);
+ g_object_unref (tc->pipe);
+
+ /* If this asserts, outstanding references to transport */
+ g_assert (tc->pipe == NULL);
+ }
+
+ if (tc->timeout)
+ g_source_remove (tc->timeout);
+}
+
+static gboolean
+on_timeout_set_flag (gpointer user_data)
+{
+ gboolean *data = user_data;
+ g_assert (user_data);
+ g_assert (*data == FALSE);
+ *data = TRUE;
+ return FALSE;
+}
+
+static void
+test_echo_and_close (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+ GBytes *sent, *bytes;
+
+ sent = g_bytes_new_static ("the message", 11);
+ cockpit_pipe_write (tc->pipe, sent);
+
+ while (echo_pipe->received->len < 11)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_byte_array_ref (echo_pipe->received);
+ bytes = g_byte_array_free_to_bytes (echo_pipe->received);
+ g_assert (g_bytes_equal (bytes, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (bytes);
+
+ cockpit_pipe_close (tc->pipe, NULL);
+
+ while (!echo_pipe->closed)
+ g_main_context_iteration (NULL, TRUE);
+}
+
+static void
+test_echo_queue (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+ GBytes *sent;
+
+ sent = g_bytes_new_static ("one", 3);
+ cockpit_pipe_write (tc->pipe, sent);
+ g_bytes_unref (sent);
+ sent = g_bytes_new_static ("two", 3);
+ cockpit_pipe_write (tc->pipe, sent);
+ g_bytes_unref (sent);
+
+ /* Only closes after above are sent */
+ cockpit_pipe_close (tc->pipe, NULL);
+
+ while (!echo_pipe->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (echo_pipe->received->len, ==, 6);
+ g_assert (memcmp (echo_pipe->received->data, "onetwo", 6) == 0);
+}
+
+static const TestFixture fixture_no_timeout = {
+ .no_timeout = TRUE
+};
+
+static void
+test_echo_large (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+ GBytes *sent;
+
+ /* Medium length */
+ sent = g_bytes_new_take (g_strnfill (1020, '!'), 1020);
+ cockpit_pipe_write (tc->pipe, sent);
+ while (echo_pipe->received->len < g_bytes_get_size (sent))
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (echo_pipe->received->len, ==, g_bytes_get_size (sent));
+ g_assert (memcmp (echo_pipe->received->data, g_bytes_get_data (sent, NULL), g_bytes_get_size (sent)) == 0);
+ g_bytes_unref (sent);
+
+ g_byte_array_set_size (echo_pipe->received, 0);
+
+ /* Extra large */
+ sent = g_bytes_new_take (g_strnfill (10 * 1000 * 1000, '?'), 10 * 1000 * 1000);
+ cockpit_pipe_write (tc->pipe, sent);
+ while (echo_pipe->received->len < g_bytes_get_size (sent))
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (echo_pipe->received->len, ==, g_bytes_get_size (sent));
+ g_assert (memcmp (echo_pipe->received->data, g_bytes_get_data (sent, NULL), g_bytes_get_size (sent)) == 0);
+ g_bytes_unref (sent);
+
+ g_byte_array_set_size (echo_pipe->received, 0);
+
+ /* Double check that didn't csrew things up */
+ sent = g_bytes_new_static ("yello", 5);
+ cockpit_pipe_write (tc->pipe, sent);
+ while (echo_pipe->received->len < g_bytes_get_size (sent))
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (echo_pipe->received->len, ==, g_bytes_get_size (sent));
+ g_assert (memcmp (echo_pipe->received->data, g_bytes_get_data (sent, NULL), g_bytes_get_size (sent)) == 0);
+ g_bytes_unref (sent);
+}
+
+static void
+test_close_problem (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+
+ cockpit_pipe_close (tc->pipe, "right now");
+
+ while (!echo_pipe->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (echo_pipe->problem, ==, "right now");
+}
+
+static const TestFixture fixture_pid = {
+ .command = "cat"
+};
+
+static void
+test_pid (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+ GPid pid;
+ GPid check;
+
+ g_assert (cockpit_pipe_get_pid (tc->pipe, &pid));
+ g_assert (pid != 0);
+
+ /* Test it's real */
+ g_assert_cmpint (kill (pid, SIGTERM), ==, 0);
+
+ /* Should still be available after closing */
+ while (!echo_pipe->closed)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (cockpit_pipe_get_pid (tc->pipe, &check));
+ g_assert_cmpuint (pid, ==, check);
+}
+
+static void
+on_pressure_set_throttle (CockpitPipe *pipe,
+ gboolean throttle,
+ gpointer user_data)
+{
+ gint *data = user_data;
+ g_assert (user_data != NULL);
+ *data = throttle ? 1 : 0;
+}
+
+static void
+test_pressure_queue (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+ gint throttle = -1;
+ GBytes *sent;
+ gint i;
+
+ g_signal_connect (tc->pipe, "pressure", G_CALLBACK (on_pressure_set_throttle), &throttle);
+ sent = g_bytes_new_take (g_strnfill (10 * 1000, '?'), 10 * 1000);
+
+ /* Sent this a thousand times */
+ for (i = 0; i < 1000; i++)
+ cockpit_pipe_write (tc->pipe, sent);
+
+ g_bytes_unref (sent);
+
+ /*
+ * This should have put way too much in the queue, and thus
+ * emitted the back-pressure signal. This signal would normally
+ * be used by others to slow down their queueing, but in this
+ * case we just check that it was fired.
+ */
+ g_assert_cmpint (throttle, ==, 1);
+ throttle = -1;
+
+ /*
+ * Now the queue is getting drained. At some point, it will be
+ * signaled that back pressure has been turned off
+ */
+ while (throttle == -1)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (throttle, ==, 0);
+ g_assert_cmpint (echo_pipe->received->len, >, 10 * 1000);
+}
+
+static void
+test_pressure_throttle (TestCase *tc,
+ gconstpointer data)
+{
+ CockpitFlow *pressure = mock_pressure_new ();
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+ gboolean timeout = FALSE;
+ gsize received;
+ GBytes *sent;
+ gint i;
+
+ cockpit_flow_throttle (COCKPIT_FLOW (tc->pipe), pressure);
+ sent = g_bytes_new_take (g_strnfill (1024, '?'), 1024);
+
+ /* Send 2MB over the echo pipe */
+ for (i = 0; i < 2048; i++)
+ cockpit_pipe_write (tc->pipe, sent);
+
+ g_bytes_unref (sent);
+
+ /*
+ * So we should start receiving the echoed data. But we apply
+ * the throttle pressure after receiving some data, and the rest
+ * just waits.
+ */
+ while (echo_pipe->received->len == 0)
+ g_main_context_iteration (NULL, TRUE);
+ cockpit_flow_emit_pressure (pressure, TRUE);
+
+ received = echo_pipe->received->len;
+ g_assert_cmpint (received, <, 2048 * 1024);
+
+ /* Now remaining data input should wait, no further data received*/
+ g_timeout_add_seconds (2, on_timeout_set_flag, &timeout);
+ while (timeout == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (received, ==, echo_pipe->received->len);
+
+ /* Remove the pressure, and we should get more data */
+ cockpit_flow_emit_pressure (pressure, FALSE);
+ while (received < echo_pipe->received->len)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Clearing the throttle should work too. This pressure signal has no effect */
+ cockpit_flow_throttle (COCKPIT_FLOW (tc->pipe), NULL);
+ cockpit_flow_emit_pressure (pressure, TRUE);
+
+ /* Now wait for the remaining data */
+ while (echo_pipe->received->len < 2048 * 1024)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (pressure);
+}
+
+static const TestFixture fixture_buffer = {
+ .pipe_type_name = "CockpitPipe"
+};
+
+static void
+test_buffer (TestCase *tc,
+ gconstpointer data)
+{
+ GByteArray *buffer;
+ GBytes *sent;
+
+ buffer = cockpit_pipe_get_buffer (tc->pipe);
+ g_assert (buffer != NULL);
+ g_assert_cmpuint (buffer->len, ==, 0);
+
+ /* Including null terminator */
+ sent = g_bytes_new_static ("blahdeedoo", 11);
+ cockpit_pipe_write (tc->pipe, sent);
+ g_bytes_unref (sent);
+
+ while (buffer->len == 0)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (buffer->len, ==, 11);
+ g_assert_cmpstr ((gchar *)buffer->data, ==, "blahdeedoo");
+}
+
+static void
+test_skip_zero (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+ GBytes *sent;
+ GBytes *zero;
+
+ /* Including null terminator */
+ sent = g_bytes_new_static ("blah", 4);
+ zero = g_bytes_new_static ("", 0);
+ cockpit_pipe_write (tc->pipe, sent);
+ cockpit_pipe_write (tc->pipe, zero);
+ cockpit_pipe_write (tc->pipe, sent);
+ g_bytes_unref (zero);
+ g_bytes_unref (sent);
+
+ while (echo_pipe->received->len < 8)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (echo_pipe->received->len, ==, 8);
+ g_byte_array_append (echo_pipe->received, (guint8 *)"", 1);
+ g_assert_cmpstr ((gchar *)echo_pipe->received->data, ==, "blahblah");
+}
+
+static const TestFixture fixture_exit_success = {
+ .command = "true"
+};
+
+static void
+test_exit_success (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+ gint status;
+
+ while (!echo_pipe->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ status = cockpit_pipe_exit_status (tc->pipe);
+ g_assert (WIFEXITED (status));
+ g_assert_cmpint (WEXITSTATUS (status), ==, 0);
+
+}
+
+static const TestFixture fixture_exit_fail = {
+ .command = "sh -c 'exit 5'"
+};
+
+static void
+test_exit_fail (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+ gint status;
+
+ while (!echo_pipe->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ status = cockpit_pipe_exit_status (tc->pipe);
+ g_assert (WIFEXITED (status));
+ g_assert_cmpint (WEXITSTATUS (status), ==, 5);
+}
+
+static const TestFixture fixture_exit_signal = {
+ .command = "cat"
+};
+
+static void
+test_exit_signal (TestCase *tc,
+ gconstpointer data)
+{
+ MockEchoPipe *echo_pipe = (MockEchoPipe *)tc->pipe;
+ gint status;
+ GPid pid = 0;
+
+ g_object_get (tc->pipe, "pid", &pid, NULL);
+ g_assert (pid != 0);
+
+ kill (pid, SIGINT);
+
+ while (!echo_pipe->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ status = cockpit_pipe_exit_status (tc->pipe);
+ g_assert (!WIFEXITED (status));
+ g_assert (WIFSIGNALED (status));
+ g_assert_cmpint (WTERMSIG (status), ==, SIGINT);
+}
+
+static void
+test_read_error (void)
+{
+ MockEchoPipe *echo_pipe;
+ int out;
+
+ /* Assuming FD 1000 is not taken */
+ g_assert (write (1000, "1", 1) < 0);
+
+ out = dup (2);
+ g_assert (out >= 0);
+
+ cockpit_expect_warning ("*Bad file descriptor");
+ cockpit_expect_warning ("*Bad file descriptor");
+
+ /* Pass in a bad read descriptor */
+ echo_pipe = g_object_new (mock_echo_pipe_get_type (),
+ "name", "test",
+ "in-fd", 1000,
+ "out-fd", out,
+ NULL);
+
+ while (!echo_pipe->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_expected ();
+
+ g_assert_cmpstr (echo_pipe->problem, ==, "internal-error");
+
+ g_object_unref (echo_pipe);
+}
+
+static void
+test_write_error (void)
+{
+ MockEchoPipe *echo_pipe;
+ GBytes *sent;
+ int fds[2];
+
+ /* Just used so we have a valid fd */
+ if (pipe(fds) < 0)
+ g_assert_not_reached ();
+
+ cockpit_expect_warning ("*Bad file descriptor");
+ cockpit_expect_warning ("*Bad file descriptor");
+
+ /* Pass in a bad write descriptor */
+ echo_pipe = g_object_new (mock_echo_pipe_get_type (),
+ "name", "test",
+ "in-fd", fds[0],
+ "out-fd", 1000,
+ NULL);
+
+ sent = g_bytes_new ("test", 4);
+ cockpit_pipe_write (COCKPIT_PIPE (echo_pipe), sent);
+ g_bytes_unref (sent);
+
+ while (!echo_pipe->closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_expected ();
+
+ g_assert_cmpstr (echo_pipe->problem, ==, "internal-error");
+
+ close (fds[1]);
+
+ g_object_unref (echo_pipe);
+}
+
+static void
+test_read_combined (void)
+{
+ MockEchoPipe *echo_pipe;
+ struct iovec iov[4];
+ gint fds[2];
+ int out;
+
+ if (pipe(fds) < 0)
+ g_assert_not_reached ();
+
+ out = dup (2);
+ g_assert (out >= 0);
+
+ /* Pass in a read end of the pipe */
+ echo_pipe = g_object_new (mock_echo_pipe_get_type (),
+ "name", "test",
+ "in-fd", fds[0],
+ "out-fd", out,
+ NULL);
+
+ /* Write two messages to the pipe at once */
+ iov[0].iov_base = "one";
+ iov[0].iov_len = 3;
+ iov[1].iov_base = "two";
+ iov[1].iov_len = 3;
+ iov[2].iov_base = "three";
+ iov[2].iov_len = 5;
+ iov[3].iov_base = "\0";
+ iov[3].iov_len = 1;
+ g_assert_cmpint (writev (fds[1], iov, 4), ==, 12);
+
+ while (echo_pipe->received->len < 12)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (echo_pipe->received->len, ==, 12);
+ g_assert_cmpstr ((gchar *)echo_pipe->received->data, ==, "onetwothree");
+
+ close (fds[1]);
+ g_object_unref (echo_pipe);
+}
+
+static void
+test_consume_entire (void)
+{
+ GByteArray *buffer;
+ GBytes *bytes;
+
+ buffer = g_byte_array_new ();
+ g_byte_array_append (buffer, (guint8 *)"Marmaalaaaade!", 15);
+
+ bytes = cockpit_pipe_consume (buffer, 0, 15, 0);
+ g_assert_cmpuint (buffer->len, ==, 0);
+ g_byte_array_free (buffer, TRUE);
+
+ g_assert_cmpuint (g_bytes_get_size (bytes), ==, 15);
+ g_assert_cmpstr (g_bytes_get_data (bytes, NULL), ==, "Marmaalaaaade!");
+ g_bytes_unref (bytes);
+}
+
+static void
+test_consume_partial (void)
+{
+ GByteArray *buffer;
+ GBytes *bytes;
+
+ buffer = g_byte_array_new ();
+ g_byte_array_append (buffer, (guint8 *)"Marmaalaaaade!", 15);
+
+ bytes = cockpit_pipe_consume (buffer, 0, 7, 0);
+ g_assert_cmpuint (buffer->len, ==, 8);
+ g_assert_cmpstr ((gchar *)buffer->data, ==, "aaaade!");
+ g_byte_array_free (buffer, TRUE);
+
+ g_assert_cmpuint (g_bytes_get_size (bytes), ==, 7);
+ g_assert (memcmp (g_bytes_get_data (bytes, NULL), "Marmaal", 7) == 0);
+ g_bytes_unref (bytes);
+}
+
+static void
+test_consume_skip (void)
+{
+ GByteArray *buffer;
+ GBytes *bytes;
+
+ buffer = g_byte_array_new ();
+ g_byte_array_append (buffer, (guint8 *)"Marmaalaaaade!", 15);
+
+ bytes = cockpit_pipe_consume (buffer, 7, 8, 0);
+ g_assert_cmpuint (buffer->len, ==, 0);
+ g_byte_array_free (buffer, TRUE);
+
+ g_assert_cmpuint (g_bytes_get_size (bytes), ==, 8);
+ g_assert_cmpstr (g_bytes_get_data (bytes, NULL), ==, "aaaade!");
+ g_bytes_unref (bytes);
+}
+
+static void
+test_buffer_skip (void)
+{
+ GByteArray *buffer;
+
+ buffer = g_byte_array_new ();
+ g_byte_array_append (buffer, (guint8 *)"Marmaalaaaade!", 15);
+
+ cockpit_pipe_skip (buffer, 7);
+ g_assert_cmpuint (buffer->len, ==, 8);
+
+ g_assert_cmpstr ((char *)buffer->data, ==, "aaaade!");
+ g_byte_array_free (buffer, TRUE);
+}
+
+static void
+test_properties (void)
+{
+ CockpitPipe *tpipe;
+ gchar *name;
+ gint in;
+ gint out;
+ int fds[2];
+
+ if (pipe(fds) < 0)
+ g_assert_not_reached ();
+
+ tpipe = g_object_new (mock_echo_pipe_get_type (),
+ "name", "testo",
+ "in-fd", fds[0],
+ "out-fd", fds[1],
+ NULL);
+
+ g_object_get (tpipe, "name", &name, "in-fd", &in, "out-fd", &out, NULL);
+ g_assert_cmpstr (name, ==, "testo");
+ g_free (name);
+ g_assert_cmpint (in, ==, fds[0]);
+ g_assert_cmpint (out, ==, fds[1]);
+
+ g_object_unref (tpipe);
+}
+
+static void
+on_close_get_flag (CockpitPipe *pipe,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gboolean *retval = user_data;
+ g_assert (*retval == FALSE);
+ *retval = TRUE;
+}
+
+static void
+on_close_get_problem (CockpitPipe *pipe,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gchar **retval = user_data;
+ g_assert (retval != NULL && *retval == NULL);
+ *retval = g_strdup (problem ? problem : "");
+}
+
+static void
+test_spawn_and_read (void)
+{
+ gboolean closed = FALSE;
+ GByteArray *buffer;
+ CockpitPipe *pipe;
+
+ const gchar *argv[] = { "/bin/sh", "-c", "set", NULL };
+ const gchar *env[] = { "ENVIRON=Marmalaaade", NULL, };
+
+ pipe = cockpit_pipe_spawn (argv, env, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ g_assert (pipe != NULL);
+ g_signal_connect (pipe, "close", G_CALLBACK (on_close_get_flag), &closed);
+
+ while (closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ buffer = cockpit_pipe_get_buffer (pipe);
+ g_byte_array_append (buffer, (const guint8 *)"\0", 1);
+
+ cockpit_assert_strmatch ((gchar *)buffer->data, "*ENVIRON*Marmalaaade*");
+
+ buffer = cockpit_pipe_get_stderr (pipe);
+ g_assert (buffer == NULL);
+
+ g_object_unref (pipe);
+}
+
+static void
+test_spawn_and_write (void)
+{
+ CockpitPipe *pipe;
+ GByteArray *buffer;
+ GBytes *sent;
+
+ const gchar *argv[] = { "/bin/cat", NULL };
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ g_assert (pipe != NULL);
+
+ /* Sending on the pipe before actually connected */
+ sent = g_bytes_new_static ("jola", 5);
+ cockpit_pipe_write (pipe, sent);
+ g_bytes_unref (sent);
+
+ buffer = cockpit_pipe_get_buffer (pipe);
+ while (buffer->len == 0)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpuint (buffer->len, ==, 5);
+ g_assert_cmpstr ((gchar *)buffer->data, ==, "jola");
+
+ g_object_unref (pipe);
+}
+
+static void
+test_spawn_and_fail (void)
+{
+ gchar *problem = NULL;
+ CockpitPipe *pipe;
+
+ const gchar *argv[] = { "/non-existent", NULL };
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ g_assert (pipe != NULL);
+ g_signal_connect (pipe, "close", G_CALLBACK (on_close_get_problem), &problem);
+
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "not-found");
+ g_free (problem);
+ g_object_unref (pipe);
+}
+
+static void
+test_spawn_close_terminate (TestCase *tc,
+ gconstpointer unused)
+{
+ CockpitPipe *pipe;
+ gboolean closed = FALSE;
+ gint status;
+
+ const gchar *argv[] = { "/bin/sleep", "500", NULL };
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ g_assert (pipe != NULL);
+
+ g_signal_connect (pipe, "close", G_CALLBACK (on_close_get_flag), &closed);
+ cockpit_pipe_close (pipe, "terminate");
+
+ while (!closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ status = cockpit_pipe_exit_status (pipe);
+ g_assert (WIFSIGNALED (status));
+ g_assert_cmpint (WTERMSIG (status), ==, SIGTERM);
+
+ g_object_unref (pipe);
+}
+
+static void
+test_spawn_close_clean (TestCase *tc,
+ gconstpointer unused)
+{
+ CockpitPipe *pipe;
+ gboolean closed = FALSE;
+ gint status;
+
+ const gchar *argv[] = { "/bin/cat", NULL };
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ g_assert (pipe != NULL);
+
+ g_signal_connect (pipe, "close", G_CALLBACK (on_close_get_flag), &closed);
+ cockpit_pipe_close (pipe, NULL);
+
+ while (!closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ status = cockpit_pipe_exit_status (pipe);
+ g_assert (!WIFSIGNALED (status));
+ g_assert_cmpint (WEXITSTATUS (status), ==, 0);
+
+ g_object_unref (pipe);
+}
+
+static void
+test_spawn_and_buffer_stderr (void)
+{
+ gboolean closed = FALSE;
+ GByteArray *buffer;
+ CockpitPipe *pipe;
+
+ const gchar *argv[] = { "/bin/sh", "-c", "echo error >&2; echo output; echo error2 >&2", NULL };
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_STDERR_TO_MEMORY);
+ g_assert (pipe != NULL);
+ g_signal_connect (pipe, "close", G_CALLBACK (on_close_get_flag), &closed);
+
+ while (closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ buffer = cockpit_pipe_get_buffer (pipe);
+ g_assert (buffer != NULL);
+
+ g_byte_array_append (buffer, (const guint8 *)"\0", 1);
+ g_assert_cmpstr ((gchar *)buffer->data, ==, "output\n");
+
+ buffer = cockpit_pipe_get_stderr (pipe);
+ g_assert (buffer != NULL);
+
+ g_byte_array_append (buffer, (const guint8 *)"\0", 1);
+ g_assert_cmpstr ((gchar *)buffer->data, ==, "error\nerror2\n");
+
+ g_object_unref (pipe);
+}
+
+static void
+test_pty_shell (void)
+{
+ gboolean closed = FALSE;
+ GByteArray *buffer;
+ CockpitPipe *pipe;
+ GBytes *sent;
+
+ const gchar *argv[] = { "/bin/bash", "-i", NULL };
+
+ pipe = cockpit_pipe_pty (argv, NULL, NULL, 24, 80);
+ g_assert (pipe != NULL);
+
+ sent = g_bytes_new_static ("echo booyah\nexit\n", 17);
+ cockpit_pipe_write (pipe, sent);
+ g_bytes_unref (sent);
+
+ g_signal_connect (pipe, "close", G_CALLBACK (on_close_get_flag), &closed);
+
+ while (closed == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ buffer = cockpit_pipe_get_buffer (pipe);
+ g_byte_array_append (buffer, (const guint8 *)"\0", 1);
+
+ cockpit_assert_strmatch ((gchar *)buffer->data, "*booyah*");
+ g_object_unref (pipe);
+}
+
+typedef struct {
+ GSocket *listen_sock;
+ GSource *listen_source;
+ GSocket *conn_sock;
+ GSource *conn_source;
+ GSocketAddress *address;
+} TestConnect;
+
+static gboolean
+on_socket_input (GSocket *socket,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ gchar buffer[1024];
+ GError *error = NULL;
+ gssize ret, wret;
+
+ ret = g_socket_receive (socket, buffer, sizeof (buffer), NULL, &error);
+ g_assert_no_error (error);
+
+ if (ret == 0)
+ {
+ g_socket_shutdown (socket, FALSE, TRUE, &error);
+ g_assert_no_error (error);
+ return FALSE;
+ }
+
+ g_assert (ret > 0);
+ wret = g_socket_send (socket, buffer, ret, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (wret == ret);
+ return TRUE;
+}
+
+static gboolean
+on_socket_connection (GSocket *socket,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ TestConnect *tc = user_data;
+ GError *error = NULL;
+
+ g_assert (tc->conn_source == NULL);
+ tc->conn_sock = g_socket_accept (tc->listen_sock, NULL, &error);
+ g_assert_no_error (error);
+
+ tc->conn_source = g_socket_create_source (tc->conn_sock, G_IO_IN, NULL);
+ g_source_set_callback (tc->conn_source, (GSourceFunc)on_socket_input, tc, NULL);
+ g_source_attach (tc->conn_source, NULL);
+
+ /* Only one connection */
+ return FALSE;
+}
+
+static void
+setup_connect (TestConnect *tc,
+ gconstpointer data)
+{
+ GError *error = NULL;
+ GInetAddress *inet;
+ GSocketAddress *address;
+
+ inet = g_inet_address_new_loopback (G_SOCKET_FAMILY_IPV4);
+ address = g_inet_socket_address_new (inet, 0);
+ g_object_unref (inet);
+
+ tc->listen_sock = g_socket_new (G_SOCKET_FAMILY_IPV4, G_SOCKET_TYPE_STREAM,
+ G_SOCKET_PROTOCOL_DEFAULT, &error);
+ g_assert_no_error (error);
+
+ g_socket_bind (tc->listen_sock, address, TRUE, &error);
+ g_object_unref (address);
+ g_assert_no_error (error);
+
+ tc->address = g_socket_get_local_address (tc->listen_sock, &error);
+ g_assert_no_error (error);
+
+ g_socket_listen (tc->listen_sock, &error);
+ g_assert_no_error (error);
+
+ tc->listen_source = g_socket_create_source (tc->listen_sock, G_IO_IN, NULL);
+ g_source_set_callback (tc->listen_source, (GSourceFunc)on_socket_connection, tc, NULL);
+ g_source_attach (tc->listen_source, NULL);
+}
+
+static void
+teardown_connect (TestConnect *tc,
+ gconstpointer data)
+{
+ g_object_unref (tc->address);
+ if (tc->conn_source)
+ {
+ g_source_destroy (tc->conn_source);
+ g_source_unref (tc->conn_source);
+ }
+ if (tc->listen_source)
+ {
+ g_source_destroy (tc->listen_source);
+ g_source_unref (tc->listen_source);
+ }
+ g_clear_object (&tc->listen_sock);
+ g_clear_object (&tc->conn_sock);
+}
+
+static void
+test_connect_and_read (TestConnect *tc,
+ gconstpointer user_data)
+{
+ CockpitPipe *pipe;
+ GError *error = NULL;
+ GByteArray *buffer;
+
+ pipe = cockpit_pipe_connect ("broooo", tc->address);
+ g_assert (pipe != NULL);
+
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Send the null terminator */
+ g_assert_cmpint (g_socket_send (tc->conn_sock, "eier", 5, NULL, &error), ==, 5);
+ g_assert_no_error (error);
+
+ buffer = cockpit_pipe_get_buffer (pipe);
+ while (buffer->len == 0)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpuint (buffer->len, ==, 5);
+ g_assert_cmpstr ((gchar *)buffer->data, ==, "eier");
+
+ g_object_unref (pipe);
+}
+
+static void
+test_connect_and_write (TestConnect *tc,
+ gconstpointer user_data)
+{
+ gchar buffer[8];
+ CockpitPipe *pipe;
+ GError *error = NULL;
+ GBytes *sent;
+
+ pipe = cockpit_pipe_connect ("broooo", tc->address);
+ g_assert (pipe != NULL);
+
+ /* Sending on the pipe before actually connected */
+ sent = g_bytes_new_static ("jola", 5);
+ cockpit_pipe_write (pipe, sent);
+ g_bytes_unref (sent);
+ g_assert (tc->conn_sock == NULL);
+
+ /* Now we connect in main loop */
+ while (tc->conn_sock == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Read from the socket */
+ g_assert_cmpint (g_socket_receive (tc->conn_sock, buffer, sizeof (buffer), NULL, &error), ==, 5);
+ g_assert_no_error (error);
+
+ g_assert_cmpstr (buffer, ==, "jola");
+ g_object_unref (pipe);
+}
+
+static void
+test_fail_not_found (void)
+{
+ CockpitPipe *pipe;
+ GSocketAddress *address;
+ gchar *problem = NULL;
+
+ cockpit_expect_message ("*No such file or directory");
+
+ address = g_unix_socket_address_new ("/non-existent");
+ pipe = cockpit_pipe_connect ("bad", address);
+ g_object_unref (address);
+
+ /* Should not have closed at this point */
+ g_assert (pipe != NULL);
+ g_signal_connect (pipe, "close", G_CALLBACK (on_close_get_problem), &problem);
+
+ /* closes in main loop */
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_expected ();
+
+ g_assert_cmpstr (problem, ==, "not-found");
+ g_free (problem);
+ g_object_unref (pipe);
+}
+
+static void
+test_fail_access_denied (void)
+{
+ CockpitPipe *pipe;
+ GSocketAddress *address;
+ gchar *unix_path;
+ gchar *problem = NULL;
+ gint fd;
+
+ if (geteuid () == 0)
+ {
+ g_test_skip ("running as root");
+ return;
+ }
+
+ unix_path = g_strdup ("/tmp/cockpit-test-XXXXXX.sock");
+ fd = g_mkstemp (unix_path);
+ g_assert_cmpint (fd, >=, 0);
+
+ /* Take away all permissions from the file */
+ g_assert_cmpint (fchmod (fd, 0000), ==, 0);
+
+ cockpit_expect_message ("*Permission denied");
+
+ address = g_unix_socket_address_new (unix_path);
+ pipe = cockpit_pipe_connect ("bad", address);
+ g_object_unref (address);
+
+ /* Should not have closed at this point */
+ g_assert (pipe != NULL);
+ g_signal_connect (pipe, "close", G_CALLBACK (on_close_get_problem), &problem);
+
+ /* closes in main loop */
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_assert_expected ();
+
+ g_assert_cmpstr (problem, ==, "access-denied");
+ g_free (unix_path);
+ g_free (problem);
+ g_object_unref (pipe);
+}
+
+static void
+test_problem_later (void)
+{
+ gchar *problem = NULL;
+ gchar *check;
+ CockpitPipe *pipe;
+
+ pipe = g_object_new (COCKPIT_TYPE_PIPE,
+ "problem", "i-have-a-problem",
+ NULL);
+ g_signal_connect (pipe, "close", G_CALLBACK (on_close_get_problem), &problem);
+
+ g_object_get (pipe, "problem", &check, NULL);
+ g_assert_cmpstr (check, ==, "i-have-a-problem");
+ g_free (check);
+ check = NULL;
+
+ g_assert (problem == NULL);
+ while (problem == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpstr (problem, ==, "i-have-a-problem");
+ g_object_get (pipe, "problem", &check, NULL);
+ g_assert_cmpstr (problem, ==, check);
+
+ g_object_unref (pipe);
+ g_free (problem);
+ g_free (check);
+}
+
+static void
+test_get_environ (void)
+{
+ const gchar *input[] = { "ENVIRON=Marmalaaade", "ANOTHER=zerog", NULL };
+ gchar **environ;
+
+ cockpit_setenv_check ("BLAH", "exists", TRUE);
+ cockpit_setenv_check ("ANOTHER", "original", TRUE);
+
+ environ = cockpit_pipe_get_environ (input, "/directory");
+
+ g_assert_cmpstr (g_environ_getenv (environ, "ENVIRON"), ==, "Marmalaaade");
+ g_assert_cmpstr (g_environ_getenv (environ, "ANOTHER"), ==, "zerog");
+ g_assert_cmpstr (g_environ_getenv (environ, "BLAH"), ==, "exists");
+ g_assert_cmpstr (g_environ_getenv (environ, "PWD"), ==, "/directory");
+
+ g_strfreev (environ);
+}
+
+static void
+test_get_environ_with_pwd (void)
+{
+ const gchar *input[] = { "ENVIRON=Marmalaaade", "PWD=/mine", NULL };
+ gchar **environ;
+
+ environ = cockpit_pipe_get_environ (input, "/directory");
+
+ g_assert_cmpstr (g_environ_getenv (environ, "ENVIRON"), ==, "Marmalaaade");
+ g_assert_cmpstr (g_environ_getenv (environ, "PWD"), ==, "/mine");
+
+ g_strfreev (environ);
+}
+
+static void
+test_get_environ_null (void)
+{
+ gchar **environ;
+
+ cockpit_setenv_check ("BLAH", "exists", TRUE);
+ cockpit_setenv_check ("ANOTHER", "original", TRUE);
+
+ environ = cockpit_pipe_get_environ (NULL, NULL);
+
+ g_assert_cmpstr (g_environ_getenv (environ, "BLAH"), ==, "exists");
+ g_assert_cmpstr (g_environ_getenv (environ, "ANOTHER"), ==, "original");
+
+ g_strfreev (environ);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/pipe/buffer/consume-entire", test_consume_entire);
+ g_test_add_func ("/pipe/buffer/consume-partial", test_consume_partial);
+ g_test_add_func ("/pipe/buffer/consume-skip", test_consume_skip);
+ g_test_add_func ("/pipe/buffer/skip", test_buffer_skip);
+
+ g_test_add_func ("/pipe/properties", test_properties);
+
+ /*
+ * Fixture data is the GType name of the pipe class
+ * so register these types here.
+ */
+ g_type_class_ref (mock_echo_pipe_get_type ());
+ g_type_class_ref (cockpit_pipe_get_type ());
+
+ g_test_add ("/pipe/echo-message", TestCase, NULL,
+ setup_simple, test_echo_and_close, teardown);
+ g_test_add ("/pipe/echo-queue", TestCase, NULL,
+ setup_simple, test_echo_queue, teardown);
+ g_test_add ("/pipe/echo-large", TestCase, &fixture_no_timeout,
+ setup_simple, test_echo_large, teardown);
+ g_test_add ("/pipe/close-problem", TestCase, NULL,
+ setup_simple, test_close_problem, teardown);
+ g_test_add ("/pipe/buffer", TestCase, &fixture_buffer,
+ setup_simple, test_buffer, teardown);
+ g_test_add ("/pipe/skip-zero", TestCase, NULL,
+ setup_simple, test_skip_zero, teardown);
+ g_test_add ("/pipe/pid", TestCase, &fixture_pid,
+ setup_simple, test_pid, teardown);
+
+ g_test_add ("/pipe/pressure/queue", TestCase, NULL,
+ setup_simple, test_pressure_queue, teardown);
+ g_test_add ("/pipe/pressure/throttle", TestCase, NULL,
+ setup_simple, test_pressure_throttle, teardown);
+
+ g_test_add ("/pipe/exit-success", TestCase, &fixture_exit_success,
+ setup_simple, test_exit_success, teardown);
+ g_test_add ("/pipe/exit-fail", TestCase, &fixture_exit_fail,
+ setup_simple, test_exit_fail, teardown);
+ g_test_add ("/pipe/exit-signal", TestCase, &fixture_exit_signal,
+ setup_simple, test_exit_signal, teardown);
+
+ g_test_add_func ("/pipe/read-error", test_read_error);
+ g_test_add_func ("/pipe/write-error", test_write_error);
+ g_test_add_func ("/pipe/read-combined", test_read_combined);
+
+ g_test_add_func ("/pipe/spawn/and-read", test_spawn_and_read);
+ g_test_add_func ("/pipe/spawn/and-write", test_spawn_and_write);
+ g_test_add_func ("/pipe/spawn/and-fail", test_spawn_and_fail);
+ g_test_add_func ("/pipe/spawn/buffer-stderr", test_spawn_and_buffer_stderr);
+
+ g_test_add ("/pipe/spawn/close-clean", TestCase, NULL,
+ setup_timeout, test_spawn_close_clean, teardown);
+ g_test_add ("/pipe/spawn/close-terminate", TestCase, NULL,
+ setup_timeout, test_spawn_close_terminate, teardown);
+
+ g_test_add_func ("/pipe/pty/shell", test_pty_shell);
+
+ g_test_add ("/pipe/connect/and-read", TestConnect, NULL,
+ setup_connect, test_connect_and_read, teardown_connect);
+ g_test_add ("/pipe/connect/and-write", TestConnect, NULL,
+ setup_connect, test_connect_and_write, teardown_connect);
+
+ g_test_add_func ("/pipe/problem-later", test_problem_later);
+
+ g_test_add_func ("/pipe/connect/not-found", test_fail_not_found);
+ g_test_add_func ("/pipe/connect/access-denied", test_fail_access_denied);
+
+ g_test_add_func ("/pipe/environ/simple", test_get_environ);
+ g_test_add_func ("/pipe/environ/pwd", test_get_environ_with_pwd);
+ g_test_add_func ("/pipe/environ/null", test_get_environ_null);
+
+ return g_test_run ();
+}
diff --git a/src/common/test-system.c b/src/common/test-system.c
new file mode 100644
index 0000000..3689820
--- /dev/null
+++ b/src/common/test-system.c
@@ -0,0 +1,148 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitsystem.h"
+
+#include "testlib/cockpittest.h"
+
+#include <glib/gstdio.h>
+
+#include <string.h>
+
+/* Defined in cockpit-system.c */
+extern const gchar *cockpit_system_proc_base;
+
+typedef struct {
+ const gchar *name;
+ guint64 result;
+ const gchar *warning;
+ const gchar *contents;
+} StartFixture;
+
+static const StartFixture start_fixtures[] = {
+ {
+ "real-world",
+ 1286773,
+ NULL,
+ "25429 (bash) S 25423 25429 25429 34816 28241 4210688 15410 80646 0 0 18 5 51 35 20 0 1 0 1286773 126083072 1827 18446744073709551615 93932014997504 93932016010716 140725640184064 140725640182696 140221933127530 0 65536 3670020 1266777851 1 0 0 17 0 0 0 0 0 0 93932018110120 93932018156904 93932029841408 140725640190162 140725640190167 140725640190167 140725640191982 0"
+ },
+ {
+ "spaces-in-command",
+ 1286773,
+ NULL,
+ "25429 (bash command spaces) S 25423 25429 25429 34816 28241 4210688 15410 80646 0 0 18 5 51 35 20 0 1 0 1286773 126083072 1827 18446744073709551615 93932014997504 93932016010716 140725640184064 140725640182696 140221933127530 0 65536 3670020 1266777851 1 0 0 17 0 0 0 0 0 0 93932018110120 93932018156904 93932029841408 140725640190162 140725640190167 140725640190167 140725640191982 0"
+ },
+ {
+ "missing-file",
+ 0,
+ "couldn't read start time*",
+ NULL,
+ },
+ {
+ "missing-command",
+ 0,
+ "error parsing stat command*",
+ "25429 xxxx S 25423 25429 25429 34816 28241 4210688 15410 80646 0 0 18 5 51 35 20 0 1 0 1286773 126083072 1827 18446744073709551615 93932014997504 93932016010716 140725640184064 140725640182696 140221933127530 0 65536 3670020 1266777851 1 0 0 17 0 0 0 0 0 0 93932018110120 93932018156904 93932029841408 140725640190162 140725640190167 140725640190167 140725640191982 0",
+ },
+ {
+ "truncate-command",
+ 0,
+ "error parsing stat command*",
+ "25429 (bash)",
+ },
+ {
+ "not-enough-tokens",
+ 0,
+ "error parsing stat tokens*",
+ "25429 (bash) S 25423 25429 25429 34816 28241 4210688 15410 80646"
+ },
+ {
+ "invalid-time-value",
+ 0,
+ "error parsing start time*",
+ "25429 (bash) S 25423 25429 25429 34816 28241 4210688 15410 80646 0 0 18 5 51 35 20 0 1 0 1286773x 126083072 1827 18446744073709551615 93932014997504 93932016010716 140725640184064 140725640182696 140221933127530 0 65536 3670020 1266777851 1 0 0 17 0 0 0 0 0 0 93932018110120 93932018156904 93932029841408 140725640190162 140725640190167 140725640190167 140725640191982 0"
+ },
+};
+
+static void
+test_start_time (gconstpointer data)
+{
+ const StartFixture *fixture = data;
+ GError *error = NULL;
+ gchar *filename = NULL;
+ gchar *directory;
+ gchar *base;
+ guint64 result;
+
+ base = g_strdup ("/tmp/test-cockpit-system.XXXXXX");
+ base = g_mkdtemp (base);
+ g_assert (base != NULL);
+ cockpit_system_proc_base = base;
+
+ directory = g_strdup_printf ("%s/%d", base, getpid ());
+ g_assert_cmpint (g_mkdir (directory, 0700), ==, 0);
+
+ if (fixture->contents)
+ {
+ filename = g_strdup_printf ("%s/stat", directory);
+ g_assert (g_file_set_contents (filename, fixture->contents, -1, &error));
+ g_assert_no_error (error);
+ }
+ if (fixture->warning)
+ cockpit_expect_warning (fixture->warning);
+
+ result = cockpit_system_process_start_time ();
+
+ /* g_printerr ("%lu\n", result); */
+ g_assert (result == fixture->result);
+
+ if (fixture->warning)
+ cockpit_assert_expected ();
+
+ if (filename)
+ g_assert_cmpint (g_unlink (filename), ==, 0);
+ g_assert_cmpint (g_rmdir (directory), ==, 0);
+ g_assert_cmpint (g_rmdir (base), ==, 0);
+
+ cockpit_system_proc_base = "/proc";
+ g_free (directory);
+ g_free (filename);
+ g_free (base);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gchar *name;
+ int i;
+
+ cockpit_test_init (&argc, &argv);
+
+ for (i = 0; i < G_N_ELEMENTS (start_fixtures); i++)
+ {
+ name = g_strdup_printf ("/system/start-time/%s", start_fixtures[i].name);
+ g_test_add_data_func (name, start_fixtures + i, test_start_time);
+ g_free (name);
+ }
+
+ return g_test_run ();
+}
diff --git a/src/common/test-template.c b/src/common/test-template.c
new file mode 100644
index 0000000..7ae97a4
--- /dev/null
+++ b/src/common/test-template.c
@@ -0,0 +1,200 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpittemplate.h"
+#include "cockpitjson.h"
+
+#include "testlib/cockpittest.h"
+
+#include <string.h>
+
+typedef struct {
+ GHashTable *variables;
+} TestCase;
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ tc->variables = g_hash_table_new (g_str_hash, g_str_equal);
+ g_hash_table_insert (tc->variables, "Scruffy", "janitor");
+ g_hash_table_insert (tc->variables, "oh", "marmalade");
+ g_hash_table_insert (tc->variables, "oh-dash", "dash-marmalade");
+ g_hash_table_insert (tc->variables, "empty", "");
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ g_hash_table_destroy (tc->variables);
+}
+
+static GBytes *
+lookup_table (const char *name,
+ gpointer user_data)
+{
+ GHashTable *variables = user_data;
+ const gchar *value;
+
+ value = g_hash_table_lookup (variables, name);
+ if (!value)
+ return NULL;
+ return g_bytes_new (value, strlen (value));
+}
+
+typedef struct {
+ const char *start;
+ const char *end;
+ const char *name;
+ const char *input;
+ const char *output[8];
+} Fixture;
+
+static const Fixture expand_fixtures[] = {
+ { "@@", "@@", "empty-string", "", { NULL } },
+ { "@@", "@@", "no-vars", "Test no vars", { "Test no vars", NULL } },
+ { "@@", "@@", "only-var", "@@oh@@", { "marmalade", NULL } },
+ { "@@", "@@", "only-vars", "@@oh@@@@oh@@", { "marmalade", "marmalade", NULL } },
+ { "@@", "@@", "simple", "Test @@oh@@ suffix", { "Test ", "marmalade", " suffix", NULL } },
+ { "@@", "@@", "extra-at", "Te@st @@oh@@ suffix", { "Te@st ", "marmalade", " suffix", NULL } },
+ { "@@", "@@", "no-ending", "Test @@oh@@ su@@ffix", { "Test ", "marmalade", " su@@ffix", NULL } },
+ { "@@", "@@", "extra-at-after", "Test @@oh@@ su@@ff@ix", { "Test ", "marmalade", " su@@ff@ix", NULL } },
+ { "@@", "@@", "unknown", "Test @@unknown@@ suffix", { "Test ", "@@unknown@@", " suffix", NULL } },
+ { "@@", "@@", "escaped", "Test \\@@oh@@ @@oh@@ suffix", { "Test ", "@@oh@@", " ", "marmalade", " suffix", NULL } },
+ { "@@", "@@", "dash", "Test @@oh-dash@@ suffix", { "Test ", "dash-marmalade", " suffix", NULL } },
+ { "@@", "@@", "lots", "Oh @@oh@@ says Scruffy @@empty@@ the @@Scruffy@@",
+ { "Oh ", "marmalade", " says Scruffy ", " the ", "janitor", NULL }
+ },
+ { "${", "}", "brackets-empty-string", "", { NULL } },
+ { "${", "}", "brackets-no-vars", "Test no vars", { "Test no vars", NULL } },
+ { "${", "}", "brackets-only-var", "${oh}", { "marmalade", NULL } },
+ { "${", "}", "brackets-only-vars", "${oh}${oh}", { "marmalade", "marmalade", NULL } },
+ { "${", "}", "brackets-simple", "Test ${oh} suffix", { "Test ", "marmalade", " suffix", NULL } },
+ { "${", "}", "brackets-not-full", "Te$st ${oh} suffix", { "Te$st ", "marmalade", " suffix", NULL } },
+ { "${", "}", "brackets-no-ending", "Test ${oh} su${ffix", { "Test ", "marmalade", " su${ffix", NULL } },
+ { "${", "}", "brackets-unknown", "Test ${unknown} suffix", { "Test ", "${unknown}", " suffix", NULL } },
+ { "${", "}", "brackets-escaped", "Test \\${oh} ${oh} suffix", { "Test ", "${oh}", " ", "marmalade", " suffix", NULL } },
+ { "${", "}", "brackets-lots", "Oh ${oh} says Scruffy ${empty} the ${Scruffy}",
+ { "Oh ", "marmalade", " says Scruffy ", " the ", "janitor", NULL }
+ },
+};
+
+static void
+test_expand (TestCase *tc,
+ gconstpointer data)
+{
+ const Fixture *fixture = data;
+ GBytes *input;
+ GList *output;
+ GList *l;
+ int i;
+
+ input = g_bytes_new_static (fixture->input, strlen (fixture->input));
+
+ output = cockpit_template_expand (input, fixture->start, fixture->end, lookup_table, tc->variables);
+ g_bytes_unref (input);
+
+ for (i = 0, l = output; l && fixture->output[i] != NULL; i++, l = g_list_next (l))
+ cockpit_assert_bytes_eq (l->data, fixture->output[i], -1);
+ g_assert_cmpint (g_list_length (output), ==, i);
+
+ g_list_free_full (output, (GDestroyNotify)g_bytes_unref);
+}
+
+static void
+test_json (TestCase *tc,
+ gconstpointer data)
+{
+ g_autoptr(JsonObject) input = json_object_new ();
+ g_autoptr(JsonObject) expected_at = json_object_new ();
+ g_autoptr(JsonObject) expected_brackets = json_object_new ();
+ g_autoptr(JsonObject) expected_both = json_object_new ();
+
+ for (int i = 0; i < G_N_ELEMENTS (expand_fixtures); i++)
+ {
+ const Fixture *fixture = &expand_fixtures[i];
+ g_autofree gchar *output = g_strjoinv ("", (gchar **) fixture->output);
+
+ json_object_set_string_member (input, fixture->name, fixture->input);
+
+ if (g_str_equal (fixture->start, "@@"))
+ {
+ /* ${...} won't expand anything here */
+ json_object_set_string_member (expected_brackets, fixture->name, fixture->input);
+
+ /* the other cases will */
+ json_object_set_string_member (expected_at, fixture->name, output);
+ json_object_set_string_member (expected_both, fixture->name, output);
+ }
+ else
+ {
+ g_assert (g_str_equal (fixture->start, "${"));
+
+ /* @@...@@ won't expand anything here */
+ json_object_set_string_member (expected_at, fixture->name, fixture->input);
+
+ /* the other cases will */
+ json_object_set_string_member (expected_brackets, fixture->name, output);
+ json_object_set_string_member (expected_both, fixture->name, output);
+ }
+ }
+
+ json_object_seal (input);
+
+ /* Let's try the cases now */
+ g_autoptr(JsonObject) at_results = cockpit_template_expand_json (input, "@@", "@@",
+ lookup_table, tc->variables);
+ g_assert (json_object_equal (at_results, expected_at));
+
+ g_autoptr(JsonObject) bracket_results = cockpit_template_expand_json (input, "${", "}",
+ lookup_table, tc->variables);
+ g_assert (json_object_equal (at_results, expected_at));
+
+ g_autoptr(JsonObject) bracket_at_results = cockpit_template_expand_json (bracket_results, "@@", "@@",
+ lookup_table, tc->variables);
+ g_assert (json_object_equal (bracket_at_results, expected_both));
+
+ g_autoptr(JsonObject) at_bracket_results = cockpit_template_expand_json (at_results, "${", "}",
+ lookup_table, tc->variables);
+ g_assert (json_object_equal (at_bracket_results, expected_both));
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gchar *name;
+ int i;
+
+ cockpit_test_init (&argc, &argv);
+
+ for (i = 0; i < G_N_ELEMENTS (expand_fixtures); i++)
+ {
+ name = g_strdup_printf ("/template/expand/%s", expand_fixtures[i].name);
+ g_test_add (name, TestCase, expand_fixtures + i, setup, test_expand, teardown);
+ g_free (name);
+ }
+
+ g_test_add ("/template/expand/json", TestCase, NULL, setup, test_json, teardown);
+
+ return g_test_run ();
+}
diff --git a/src/common/test-transport.c b/src/common/test-transport.c
new file mode 100644
index 0000000..bb07bce
--- /dev/null
+++ b/src/common/test-transport.c
@@ -0,0 +1,799 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpittransport.h"
+#include "cockpitpipe.h"
+#include "cockpitpipetransport.h"
+
+#include "testlib/cockpittest.h"
+#include "testlib/mock-transport.h"
+
+#include "websocket/websocket.h"
+
+#include <glib.h>
+
+#include <string.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+
+#define WAIT_UNTIL(cond) \
+ G_STMT_START \
+ while (!(cond)) g_main_context_iteration (NULL, TRUE); \
+ G_STMT_END
+
+typedef struct {
+ CockpitTransport *transport;
+ CockpitPipe *pipe;
+} TestCase;
+
+static void
+setup_with_child (TestCase *tc,
+ gconstpointer data)
+{
+ gchar *argv[] = { (gchar *)data, NULL };
+ GError *error = NULL;
+ GPid pid;
+ int in;
+ int out;
+
+ g_spawn_async_with_pipes (NULL, argv, NULL, G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
+ NULL, NULL, &pid, &in, &out, NULL, &error);
+ g_assert_no_error (error);
+
+ tc->pipe = g_object_new (COCKPIT_TYPE_PIPE,
+ "name", argv[0],
+ "in-fd", out,
+ "out-fd", in,
+ "pid", pid,
+ NULL);
+ tc->transport = cockpit_pipe_transport_new (tc->pipe);
+}
+
+static void
+setup_no_child (TestCase *tc,
+ gconstpointer data)
+{
+ int sv[2];
+
+ if (socketpair (PF_LOCAL, SOCK_STREAM, 0, sv) < 0)
+ g_assert_not_reached ();
+
+ tc->pipe = cockpit_pipe_new ("mock", sv[0], sv[1]);
+ tc->transport = cockpit_pipe_transport_new (tc->pipe);
+}
+
+static void
+teardown_transport (TestCase *tc,
+ gconstpointer data)
+{
+ cockpit_assert_expected ();
+
+ g_object_add_weak_pointer (G_OBJECT (tc->transport),
+ (gpointer *)&tc->transport);
+ g_object_unref (tc->transport);
+
+ /* If this asserts, outstanding references to transport */
+ g_assert (tc->transport == NULL);
+
+ g_object_add_weak_pointer (G_OBJECT (tc->pipe),
+ (gpointer *)&tc->pipe);
+ g_object_unref (tc->pipe);
+
+ /* If this asserts, outstanding references to transport */
+ g_assert (tc->pipe == NULL);
+}
+
+static gboolean
+on_recv_get_payload (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *message,
+ gpointer user_data)
+{
+ GBytes **received = user_data;
+ if (channel == NULL)
+ return FALSE;
+ g_assert_cmpstr (channel, ==, "546");
+ g_assert (*received == NULL);
+ *received = g_bytes_ref (message);
+ return TRUE;
+}
+
+
+static gboolean
+on_recv_multiple (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *message,
+ gpointer user_data)
+{
+ gint *state = user_data;
+ GBytes *check = NULL;
+
+ if (channel == NULL)
+ return FALSE;
+ g_assert_cmpstr (channel, ==, "9");
+
+ if (*state == 0)
+ check = g_bytes_new_static ("one", 3);
+ else if (*state == 1)
+ check = g_bytes_new_static ("two", 3);
+ else
+ g_assert_not_reached ();
+
+ (*state)++;
+ g_assert (g_bytes_equal (message, check));
+ g_bytes_unref (check);
+ return TRUE;
+}
+
+static void
+on_closed_set_flag (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gboolean *flag = user_data;
+ g_assert (problem == NULL);
+ g_assert (*flag == FALSE);
+ *flag = TRUE;
+}
+
+static void
+test_properties (TestCase *tc,
+ gconstpointer data)
+{
+ CockpitPipe *pipe;
+
+ g_assert (cockpit_pipe_transport_get_pipe (COCKPIT_PIPE_TRANSPORT (tc->transport)) == tc->pipe);
+
+ g_object_get (tc->transport, "pipe", &pipe, NULL);
+ g_assert (pipe == tc->pipe);
+ g_object_unref (pipe);
+}
+
+static void
+test_echo_and_close (TestCase *tc,
+ gconstpointer data)
+{
+
+ GBytes *received = NULL;
+ GBytes *sent;
+ gboolean closed = FALSE;
+
+ sent = g_bytes_new_static ("the message", 11);
+ g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_get_payload), &received);
+ cockpit_transport_send (tc->transport, "546", sent);
+
+ WAIT_UNTIL (received != NULL);
+
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+ cockpit_transport_close (tc->transport, NULL);
+
+ WAIT_UNTIL (closed == TRUE);
+}
+
+static void
+test_ping_pong (void)
+{
+ JsonObject *reply = NULL;
+ MockTransport *mock;
+ CockpitTransport *transport;
+ GBytes *sent;
+
+ mock = mock_transport_new ();
+ transport = COCKPIT_TRANSPORT (mock);
+
+ /*
+ * A "ping" without a channel is not forwarded, but responded to directly
+ * by the peer transport. Here we test that CockpitTransport does
+ * that response properly.
+ */
+ sent = cockpit_transport_build_control ("command", "ping", "value", "blah", "other", "marmalade", NULL);
+ cockpit_transport_emit_recv (transport, NULL, sent);
+ g_bytes_unref (sent);
+
+ reply = mock_transport_pop_control (mock);
+
+ cockpit_assert_json_eq (reply, "{ \"command\": \"pong\", \"value\": \"blah\", \"other\": \"marmalade\" }");
+ g_object_unref (mock);
+}
+
+static void
+test_ping_channel (void)
+{
+ JsonObject *reply = NULL;
+ MockTransport *mock;
+ CockpitTransport *transport;
+ GBytes *sent;
+
+ mock = mock_transport_new ();
+ transport = COCKPIT_TRANSPORT (mock);
+
+ /* This is a "ping" in a channel. But that channel is not open, so its ignored */
+ sent = cockpit_transport_build_control ("command", "ping", "channel", "444", NULL);
+ cockpit_transport_emit_recv (transport, NULL, sent);
+ g_bytes_unref (sent);
+
+ /* This is a single hop, non-forwarded ping, responded to by CockpitTransport */
+ sent = cockpit_transport_build_control ("command", "ping", "value", "blah", "other", "marmalade", NULL);
+ cockpit_transport_emit_recv (transport, NULL, sent);
+ g_bytes_unref (sent);
+
+ reply = mock_transport_pop_control (mock);
+
+ /* Note that we don't get a pong for the first one, since it's for a channel, and no such channel exists */
+ cockpit_assert_json_eq (reply, "{ \"command\": \"pong\", \"value\": \"blah\", \"other\": \"marmalade\" }");
+ g_object_unref (mock);
+}
+
+static void
+test_echo_queue (TestCase *tc,
+ gconstpointer data)
+{
+ GBytes *sent;
+ gint state = 0;
+ gboolean closed = FALSE;
+
+ g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_multiple), &state);
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+
+ sent = g_bytes_new_static ("one", 3);
+ cockpit_transport_send (tc->transport, "9", sent);
+ g_bytes_unref (sent);
+ sent = g_bytes_new_static ("two", 3);
+ cockpit_transport_send (tc->transport, "9", sent);
+ g_bytes_unref (sent);
+
+ /* Only closes after above are sent */
+ cockpit_transport_close (tc->transport, NULL);
+
+ WAIT_UNTIL (state == 2 && closed == TRUE);
+}
+
+static void
+test_echo_large (TestCase *tc,
+ gconstpointer data)
+{
+ GBytes *received = NULL;
+ GBytes *sent;
+
+ g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_get_payload), &received);
+
+ /* Medium length */
+ sent = g_bytes_new_take (g_strnfill (1020, '!'), 1020);
+ cockpit_transport_send (tc->transport, "546", sent);
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ /* Extra large */
+ sent = g_bytes_new_take (g_strnfill (10 * 1000 * 1000, '?'), 10 * 1000 * 1000);
+ cockpit_transport_send (tc->transport, "546", sent);
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ /* Double check that didn't csrew things up */
+ sent = g_bytes_new_static ("yello", 5);
+ cockpit_transport_send (tc->transport, "546", sent);
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+}
+
+static void
+on_closed_get_problem (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ const gchar **ret = user_data;
+ g_assert (problem != NULL);
+ g_assert (*ret == NULL);
+ *ret = g_strdup (problem);
+}
+
+static void
+test_close_problem (TestCase *tc,
+ gconstpointer data)
+{
+ gchar *problem = NULL;
+
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+ cockpit_transport_close (tc->transport, "right now");
+
+ WAIT_UNTIL (problem != NULL);
+
+ g_assert_cmpstr (problem, ==, "right now");
+ g_free (problem);
+}
+
+static void
+test_terminate_problem (TestCase *tc,
+ gconstpointer data)
+{
+ gchar *problem = NULL;
+ GPid pid;
+
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ g_assert (cockpit_pipe_get_pid (tc->pipe, &pid));
+ g_assert (pid != 0);
+ kill (pid, SIGTERM);
+
+ WAIT_UNTIL (problem != NULL);
+
+ g_assert_cmpstr (problem, ==, "terminated");
+ g_free (problem);
+}
+
+static void
+test_exception_problem (TestCase *tc,
+ gconstpointer data)
+{
+ gchar *problem = NULL;
+
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ WAIT_UNTIL (problem != NULL);
+ g_assert_cmpstr (problem, ==, "internal-error");
+ g_free (problem);
+}
+
+static void
+test_nocockpit_problem (TestCase *tc,
+ gconstpointer data)
+{
+ gchar *problem = NULL;
+
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ WAIT_UNTIL (problem != NULL);
+ g_assert_cmpstr (problem, ==, "no-cockpit");
+ g_free (problem);
+}
+
+static void
+test_read_error (void)
+{
+ CockpitTransport *transport;
+ gchar *problem = NULL;
+ gint fds[2];
+
+ /* Assuming FD 1000 is not taken */
+ g_assert (write (1000, "1", 1) < 0);
+
+ g_assert_cmpint (pipe (fds), ==, 0);
+
+
+ cockpit_expect_warning ("*Bad file descriptor");
+ cockpit_expect_warning ("*Bad file descriptor");
+
+ /* Pass in a bad read descriptor */
+ transport = cockpit_pipe_transport_new_fds ("test", 1000, fds[0]);
+
+ g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ WAIT_UNTIL (problem != NULL);
+ g_assert_cmpstr (problem, ==, "internal-error");
+ g_free (problem);
+
+ cockpit_assert_expected ();
+
+ g_object_unref (transport);
+ close (fds[1]);
+}
+
+static void
+test_write_error (void)
+{
+ CockpitTransport *transport;
+ gchar *problem = NULL;
+ GBytes *sent;
+ int fds[2];
+
+ /* Just used so we have a valid fd */
+ if (pipe(fds) < 0)
+ g_assert_not_reached ();
+
+ /* Assuming FD 1000 is not taken */
+ g_assert (write (1000, "1", 1) < 0);
+
+ cockpit_expect_warning ("*Bad file descriptor");
+ cockpit_expect_warning ("*Bad file descriptor");
+
+ /* Pass in a bad write descriptor */
+ transport = cockpit_pipe_transport_new_fds ("test", fds[0], 1000);
+
+ sent = g_bytes_new ("test", 4);
+ cockpit_transport_send (transport, "3333", sent);
+ g_bytes_unref (sent);
+
+ g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ WAIT_UNTIL (problem != NULL);
+ g_assert_cmpstr (problem, ==, "internal-error");
+ g_free (problem);
+
+ close (fds[0]);
+ close (fds[1]);
+
+ g_object_unref (transport);
+
+ cockpit_assert_expected ();
+}
+
+static void
+test_read_combined (void)
+{
+ CockpitTransport *transport;
+ struct iovec iov[4];
+ gint state = 0;
+ gint fds[2];
+ gint out;
+
+ if (pipe(fds) < 0)
+ g_assert_not_reached ();
+
+ out = dup (2);
+ g_assert (out >= 0);
+
+ /* Pass in a read end of the pipe */
+ transport = cockpit_pipe_transport_new_fds ("test", fds[0], out);
+ g_signal_connect (transport, "recv", G_CALLBACK (on_recv_multiple), &state);
+
+ /* Write two messages to the pipe at once */
+ iov[0].iov_base = "5\n";
+ iov[0].iov_len = 2;
+ iov[1].iov_base = "9\none";
+ iov[1].iov_len = 5;
+ iov[2].iov_base = "5\n";
+ iov[2].iov_len = 2;
+ iov[3].iov_base = "9\ntwo";
+ iov[3].iov_len = 5;
+ g_assert_cmpint (writev (fds[1], iov, 4), ==, 14);
+
+ WAIT_UNTIL (state == 2);
+
+ close (fds[1]);
+ g_object_unref (transport);
+}
+
+static void
+test_read_truncated (void)
+{
+ CockpitTransport *transport;
+ gchar *problem = NULL;
+ gint fds[2];
+ gint out;
+
+ if (pipe(fds) < 0)
+ g_assert_not_reached ();
+
+ out = dup (2);
+ g_assert (out >= 0);
+
+ /* Pass in a read end of the pipe */
+ transport = cockpit_pipe_transport_new_fds ("test", fds[0], out);
+ g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ /* Not a full 4 byte length (ie: truncated) */
+ g_assert_cmpint (write (fds[1], "5", 1), ==, 1);
+ g_assert_cmpint (close (fds[1]), ==, 0);
+
+ WAIT_UNTIL (problem != NULL);
+
+ g_assert_cmpstr (problem, ==, "disconnected");
+ g_free (problem);
+
+ g_object_unref (transport);
+
+ cockpit_assert_expected ();
+}
+
+static void
+test_incorrect_protocol (void)
+{
+ CockpitTransport *transport;
+ gchar *problem = NULL;
+ gint fds[2];
+ gint out;
+
+ if (pipe(fds) < 0)
+ g_assert_not_reached ();
+
+ out = dup (2);
+ g_assert (out >= 0);
+
+ cockpit_expect_warning ("*received invalid length prefix");
+
+ /* Pass in a read end of the pipe */
+ transport = cockpit_pipe_transport_new_fds ("test", fds[0], out);
+ g_signal_connect (transport, "closed", G_CALLBACK (on_closed_get_problem), &problem);
+
+ /* Not a full 4 byte length (ie: truncated) */
+ g_assert_cmpint (write (fds[1], "X", 1), ==, 1);
+ g_assert_cmpint (close (fds[1]), ==, 0);
+
+ WAIT_UNTIL (problem != NULL);
+
+ g_assert_cmpstr (problem, ==, "protocol-error");
+ g_free (problem);
+
+ g_object_unref (transport);
+
+ cockpit_assert_expected ();
+}
+
+static void
+test_parse_frame (void)
+{
+ GBytes *message;
+ GBytes *payload;
+ gchar *channel;
+
+ message = g_bytes_new_static ("134\ntest", 8);
+
+ payload = cockpit_transport_parse_frame (message, &channel);
+ g_assert (payload != NULL);
+ g_assert_cmpstr (g_bytes_get_data (payload, NULL), ==, "test");
+ g_assert_cmpstr (channel, ==, "134");
+
+ g_bytes_unref (payload);
+ g_bytes_unref (message);
+ g_free (channel);
+}
+
+static void
+test_parse_frame_bad (void)
+{
+ gchar *channel = NULL;
+ GBytes *message;
+ GBytes *payload;
+
+ cockpit_expect_message ("*invalid channel prefix");
+
+ message = g_bytes_new_static ("b\x00y\ntest", 8);
+ payload = cockpit_transport_parse_frame (message, &channel);
+ g_assert (payload == NULL);
+ g_bytes_unref (message);
+ g_free (channel);
+
+ cockpit_assert_expected ();
+
+ cockpit_expect_message ("*invalid message without channel prefix");
+
+ channel = NULL;
+ message = g_bytes_new_static ("test", 4);
+ payload = cockpit_transport_parse_frame (message, &channel);
+ g_assert (payload == NULL);
+ g_bytes_unref (message);
+ g_free (channel);
+
+ cockpit_assert_expected ();
+}
+
+static void
+test_parse_frame_maybe (void)
+{
+ gchar *channel = NULL;
+ GBytes *message;
+ GBytes *payload;
+
+ message = g_bytes_new_static ("b\x00y\ntest", 8);
+ payload = cockpit_transport_maybe_frame (message, &channel);
+ g_assert (payload == NULL);
+ g_bytes_unref (message);
+ g_free (channel);
+
+ channel = NULL;
+ message = g_bytes_new_static ("test", 4);
+ payload = cockpit_transport_maybe_frame (message, &channel);
+ g_assert (payload == NULL);
+ g_bytes_unref (message);
+ g_free (channel);
+}
+
+static void
+test_parse_command (void)
+{
+ const gchar *input = "{ \"command\": \"test\", \"channel\": \"66\", \"opt\": \"one\" }";
+ GBytes *message;
+ const gchar *channel;
+ const gchar *command;
+ JsonObject *options;
+ gboolean ret;
+
+ message = g_bytes_new_static (input, strlen (input));
+
+ ret = cockpit_transport_parse_command (message, &command, &channel, &options);
+ g_bytes_unref (message);
+
+ g_assert (ret == TRUE);
+ g_assert_cmpstr (command, ==, "test");
+ g_assert_cmpstr (channel, ==, "66");
+ g_assert_cmpstr (json_object_get_string_member (options, "opt"), ==, "one");
+
+ json_object_unref (options);
+}
+
+static void
+test_parse_command_no_channel (void)
+{
+ const gchar *input = "{ \"command\": \"test\", \"opt\": \"one\" }";
+ GBytes *message;
+ const gchar *channel;
+ const gchar *command;
+ JsonObject *options;
+ gboolean ret;
+
+ message = g_bytes_new_static (input, strlen (input));
+
+ ret = cockpit_transport_parse_command (message, &command, &channel, &options);
+ g_bytes_unref (message);
+
+ g_assert (ret == TRUE);
+ g_assert_cmpstr (command, ==, "test");
+ g_assert_cmpstr (channel, ==, NULL);
+ g_assert_cmpstr (json_object_get_string_member (options, "opt"), ==, "one");
+
+ json_object_unref (options);
+}
+
+static void
+test_parse_command_nulls (void)
+{
+ const gchar *input = "{ \"command\": \"test\", \"opt\": \"one\" }";
+ GBytes *message;
+ JsonObject *options;
+ gboolean ret;
+
+ message = g_bytes_new_static (input, strlen (input));
+
+ ret = cockpit_transport_parse_command (message, NULL, NULL, &options);
+ g_bytes_unref (message);
+
+ g_assert (ret == TRUE);
+ g_assert_cmpstr (json_object_get_string_member (options, "opt"), ==, "one");
+
+ json_object_unref (options);
+}
+
+struct {
+ const char *name;
+ const char *json;
+} bad_command_payloads[] = {
+ { "no-command", "{ \"no-command\": \"test\" }", },
+ { "empty-command", "{ \"command\": \"\" }", },
+ { "invalid-json", "{ xxxxxxxxxxxxxxxxxxxxx", },
+ { "not-an-object", "55", },
+ { "number-channel", "{ \"command\": \"test\", \"channel\": 0 }", },
+ { "empty-channel", "{ \"command\": \"test\", \"channel\": \"\" }", },
+ { "newline-channel", "{ \"command\": \"test\", \"channel\": \"blah\nline\" }", },
+};
+
+static void
+test_parse_command_bad (gconstpointer input)
+{
+ GBytes *message;
+ const gchar *channel;
+ const gchar *command;
+ JsonObject *options;
+ gboolean ret;
+
+ cockpit_expect_warning ("*");
+
+ message = g_bytes_new_static (input, strlen (input));
+
+ ret = cockpit_transport_parse_command (message, &command, &channel, &options);
+ g_bytes_unref (message);
+
+ g_assert (ret == FALSE);
+
+ cockpit_assert_expected ();
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gint i;
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/transport/parse-frame/ok", test_parse_frame);
+ g_test_add_func ("/transport/parse-frame/bad", test_parse_frame_bad);
+ g_test_add_func ("/transport/parse-frame/maybe", test_parse_frame_maybe);
+
+ g_test_add_func ("/transport/parse-command/normal", test_parse_command);
+ g_test_add_func ("/transport/parse-command/no-channel", test_parse_command_no_channel);
+ g_test_add_func ("/transport/parse-command/nulls", test_parse_command_nulls);
+
+ for (i = 0; i < G_N_ELEMENTS (bad_command_payloads); i++)
+ {
+ gchar *name = g_strdup_printf ("/transport/parse-command/%s", bad_command_payloads[i].name);
+ g_test_add_data_func (name, bad_command_payloads[i].json, test_parse_command_bad);
+ g_free (name);
+ }
+
+ g_test_add ("/transport/properties", TestCase, NULL,
+ setup_no_child, test_properties, teardown_transport);
+
+ g_test_add ("/transport/echo-message/child", TestCase,
+ BUILDDIR "/mock-echo", setup_with_child,
+ test_echo_and_close, teardown_transport);
+ g_test_add ("/transport/echo-message/no-child", TestCase,
+ NULL, setup_no_child,
+ test_echo_and_close, teardown_transport);
+ g_test_add ("/transport/echo-queue/child", TestCase,
+ BUILDDIR "/mock-echo", setup_with_child,
+ test_echo_queue, teardown_transport);
+ g_test_add ("/transport/echo-queue/no-child", TestCase,
+ NULL, setup_no_child,
+ test_echo_queue, teardown_transport);
+ g_test_add ("/transport/echo-large/child", TestCase,
+ "cat", setup_with_child,
+ test_echo_large, teardown_transport);
+ g_test_add ("/transport/echo-large/no-child", TestCase,
+ NULL, setup_no_child,
+ test_echo_large, teardown_transport);
+
+ g_test_add ("/transport/close-problem/child", TestCase,
+ BUILDDIR "/mock-echo", setup_with_child,
+ test_close_problem, teardown_transport);
+ g_test_add ("/transport/close-problem/no-child", TestCase,
+ NULL, setup_no_child,
+ test_close_problem, teardown_transport);
+
+ g_test_add ("/transport/terminate-problem", TestCase,
+ BUILDDIR "/mock-echo", setup_with_child,
+ test_terminate_problem, teardown_transport);
+
+ g_test_add ("/transport/exception-problem", TestCase,
+ SRCDIR "/src/ws/mock-pipes/someprogram", setup_with_child,
+ test_exception_problem, teardown_transport);
+
+ g_test_add ("/transport/nocockpit-problem", TestCase,
+ SRCDIR "/src/ws/mock-pipes/cockpit-session", setup_with_child,
+ test_nocockpit_problem, teardown_transport);
+
+ g_test_add_func ("/transport/ping/pong", test_ping_pong);
+ g_test_add_func ("/transport/ping/channel", test_ping_channel);
+
+ g_test_add_func ("/transport/read-error", test_read_error);
+ g_test_add_func ("/transport/write-error", test_write_error);
+ g_test_add_func ("/transport/read-combined", test_read_combined);
+ g_test_add_func ("/transport/read-truncated", test_read_truncated);
+ g_test_add_func ("/transport/read-incorrect", test_incorrect_protocol);
+
+ return g_test_run ();
+}
diff --git a/src/common/test-unicode.c b/src/common/test-unicode.c
new file mode 100644
index 0000000..3cfda84
--- /dev/null
+++ b/src/common/test-unicode.c
@@ -0,0 +1,112 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitunicode.h"
+
+#include "testlib/cockpittest.h"
+
+#include <string.h>
+
+typedef struct {
+ const gchar *input;
+ const gchar *output;
+ gboolean incomplete;
+} Fixture;
+
+static void
+test_incomplete_utf8 (gconstpointer data)
+{
+ const Fixture *fixture = data;
+ GBytes *input;
+ gboolean result;
+
+ g_assert (data != NULL);
+
+ input = g_bytes_new_static (fixture->input, strlen (fixture->input));
+ result = cockpit_unicode_has_incomplete_ending (input);
+ g_assert (result == fixture->incomplete);
+
+ g_bytes_unref (input);
+}
+
+static void
+test_force_utf8 (gconstpointer data)
+{
+ const Fixture *fixture = data;
+ const gchar *expect;
+ GBytes *input;
+ GBytes *output;
+
+ g_assert (data != NULL);
+
+ input = g_bytes_new_static (fixture->input, strlen (fixture->input));
+ output = cockpit_unicode_force_utf8 (input);
+
+ expect = fixture->output ? fixture->output : fixture->input;
+ cockpit_assert_bytes_eq (output, expect, -1);
+
+ if (!fixture->output)
+ g_assert (input == output);
+ else
+ g_assert (input != output);
+
+ g_bytes_unref (input);
+ g_bytes_unref (output);
+}
+
+static const Fixture fixtures[] = {
+ { "this is a ascii", NULL, FALSE },
+ { "this is \303\244 utf8", NULL, FALSE },
+ { "this is \303 invalid", "this is \357\277\275 invalid", FALSE },
+ { "this is invalid \303", "this is invalid \357\277\275", TRUE },
+ { "\303 this is \303 invalid \303", "\357\277\275 this is \357\277\275 invalid \357\277\275", TRUE },
+ { "\303 this is \303 invalid \303\303", "\357\277\275 this is \357\277\275 invalid \357\277\275\357\277\275", TRUE },
+ { "\303 this is \303 invalid \303\303a", "\357\277\275 this is \357\277\275 invalid \357\277\275\357\277\275a", FALSE },
+ { "Marmalaade!""\xe2\x94\x80", NULL, FALSE },
+};
+
+int
+main (int argc,
+ char *argv[])
+{
+ gchar *escaped;
+ gchar *name;
+ gchar *name2;
+ gint i;
+
+ cockpit_test_init (&argc, &argv);
+
+ for (i = 0; i < G_N_ELEMENTS (fixtures); i++)
+ {
+ g_assert (fixtures[i].input != NULL);
+ escaped = g_strcanon (g_strdup (fixtures[i].input), COCKPIT_TEST_CHARS, '_');
+ name = g_strdup_printf ("/unicode/force-utf8/%s", escaped);
+ name2 = g_strdup_printf ("/unicode/incomplete-utf8/%s", escaped);
+ g_free (escaped);
+
+ g_test_add_data_func (name, fixtures + i, test_force_utf8);
+ g_test_add_data_func (name2, fixtures + i, test_incomplete_utf8);
+ g_free (name);
+ g_free (name2);
+ }
+
+ return g_test_run ();
+}
diff --git a/src/common/test-unixsignal.c b/src/common/test-unixsignal.c
new file mode 100644
index 0000000..f452aae
--- /dev/null
+++ b/src/common/test-unixsignal.c
@@ -0,0 +1,69 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitunixsignal.h"
+
+#include "testlib/cockpittest.h"
+
+#include <glib.h>
+
+static void
+test_posix_signal (void)
+{
+ gchar *signal;
+
+ signal = cockpit_strsignal (SIGVTALRM);
+ g_assert_cmpstr (signal, ==, "VTALRM");
+ g_free (signal);
+}
+
+static void
+test_rt_signal (void)
+{
+ gchar *signal;
+
+ signal = cockpit_strsignal (SIGRTMIN);
+ g_assert_cmpstr (signal, ==, "RT0");
+ g_free (signal);
+}
+
+static void
+test_other_signal (void)
+{
+ gchar *signal;
+
+ signal = cockpit_strsignal (0xffffffff);
+ g_assert_cmpstr (signal, ==, "UNKNOWN");
+ g_free (signal);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/unixsignal/posix-signal", test_posix_signal);
+ g_test_add_func ("/unixsignal/realtime-signal", test_rt_signal);
+ g_test_add_func ("/unixsignal/other-signal", test_other_signal);
+
+ return g_test_run ();
+}
diff --git a/src/common/test-version.c b/src/common/test-version.c
new file mode 100644
index 0000000..94ecd98
--- /dev/null
+++ b/src/common/test-version.c
@@ -0,0 +1,95 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitversion.h"
+
+#include "testlib/cockpittest.h"
+
+#include <string.h>
+
+typedef struct {
+ const gchar *one;
+ const gchar *two;
+ const gint result;
+} Fixture;
+
+static void
+test_compare_version (gconstpointer data)
+{
+ const Fixture *fixture = data;
+ gint res;
+
+ g_assert (data != NULL);
+
+ res = cockpit_version_compare (fixture->one, fixture->two);
+
+ /* Normalize */
+ if (res < 0)
+ res = -1;
+ else if (res > 0)
+ res = 1;
+
+ g_assert_cmpint (res, ==, fixture->result);
+}
+
+static const Fixture fixtures[] = {
+ { "", "", 0 },
+ { "0", "", 1 },
+ { "", "5", -1 },
+ { "0", "0", 0 },
+ { "0", "0.1", -1 },
+ { "0.2", "0", 1 },
+ { "0.2.3", "0", 1 },
+ { "1.0", "1.0", 0 },
+ { "1.0", "1.1", -1 },
+ { "1.3", "1.1", 1 },
+ { "1.2.3", "1.2.3", 0 },
+ { "1.2.3", "1.2.5", -1 },
+ { "1.2.8", "1.2.5", 1 },
+ { "55", "55", 0 },
+ { "5abc", "5abc", 0 },
+ { "5abc", "5abcd", -1 },
+ { "5xyz", "5abcd", 1 },
+ { "abc", "abc", 0 },
+ { "xyz", "abc", 1 },
+};
+
+int
+main (int argc,
+ char *argv[])
+{
+ gchar *name;
+ gint i;
+
+ cockpit_test_init (&argc, &argv);
+
+ for (i = 0; i < G_N_ELEMENTS (fixtures); i++)
+ {
+ g_assert (fixtures[i].one != NULL);
+ g_assert (fixtures[i].two != NULL);
+ name = g_strdup_printf ("/version/compare/%s_%s", fixtures[i].one, fixtures[i].two);
+
+ g_test_add_data_func (name, fixtures + i, test_compare_version);
+ g_free (name);
+ }
+
+ return g_test_run ();
+}
diff --git a/src/common/test-webcertificate.c b/src/common/test-webcertificate.c
new file mode 100644
index 0000000..701e57f
--- /dev/null
+++ b/src/common/test-webcertificate.c
@@ -0,0 +1,125 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "cockpitwebcertificate.h"
+
+#include "cockpitsystem.h"
+
+#include "testlib/cockpittest.h"
+
+static void
+do_locate_test (int dirfd, const char *certname, const char *expected_path, const char *expected_error)
+{
+ char *error = NULL;
+ gchar *path;
+
+ if (certname)
+ {
+ int fd = openat (dirfd, certname, O_CREAT | O_WRONLY, 0666);
+ g_assert_cmpint (fd, >=, 0);
+ close (fd);
+ }
+
+ path = cockpit_certificate_locate (false, &error);
+
+ if (expected_path)
+ cockpit_assert_strmatch (path, expected_path);
+ else
+ g_assert_cmpstr (path, ==, NULL);
+
+ if (expected_error)
+ cockpit_assert_strmatch (error, expected_error);
+ else
+ g_assert_cmpstr (error, ==, NULL);
+
+ g_free (error);
+ g_free (path);
+ if (certname)
+ g_assert_cmpint (unlinkat (dirfd, certname, 0), ==, 0);
+}
+
+static void
+test_locate (void)
+{
+ g_autofree gchar *workdir = g_strdup ("/tmp/test-cockpit-webcertificate.XXXXXX");
+ g_autofree gchar *cert_dir = NULL;
+ int cert_dir_fd;
+
+ g_assert (g_mkdtemp (workdir) == workdir);
+ cockpit_setenv_check ("XDG_CONFIG_DIRS", workdir, TRUE);
+
+ /* nonexisting dir, nothing found */
+ do_locate_test (-1, NULL, NULL, "No certificate found in dir: */ws-certs.d");
+
+ /* empty dir, nothing found */
+ cert_dir = g_build_filename (workdir, "cockpit", "ws-certs.d", NULL);
+ g_assert_cmpint (g_mkdir_with_parents (cert_dir, 0777), ==, 0);
+ do_locate_test (-1, NULL, NULL, "No certificate found in dir: */ws-certs.d");
+
+ /* one unrelated file */
+ cert_dir_fd = open (cert_dir, O_PATH);
+ g_assert_cmpint (cert_dir_fd, >=, 0);
+ do_locate_test (cert_dir_fd, "noise.zrt", NULL, "No certificate found in dir: */ws-certs.d");
+
+ /* one good file */
+ do_locate_test (cert_dir_fd, "01-first.cert", "*/cockpit/ws-certs.d/01-first.cert", NULL);
+
+ /* asciibetically last one wins */
+ do_locate_test (cert_dir_fd, "50-better.cert", "*/cockpit/ws-certs.d/50-better.cert", NULL);
+
+ /* *.crt works, too */
+ do_locate_test (cert_dir_fd, "60-best.crt", "*/cockpit/ws-certs.d/60-best.crt", NULL);
+
+ close (cert_dir_fd);
+ g_unsetenv ("XDG_CONFIG_DIRS");
+ rmdir (cert_dir);
+ rmdir (workdir);
+}
+
+static void
+test_keypath (void)
+{
+ char *path;
+
+ path = cockpit_certificate_key_path ("/etc/cockpit/ws-certs.d/50-good.cert");
+ g_assert_cmpstr (path, ==, "/etc/cockpit/ws-certs.d/50-good.key");
+ g_free (path);
+ path = cockpit_certificate_key_path ("a.cert");
+ g_assert_cmpstr (path, ==, "a.key");
+ g_free (path);
+ path = cockpit_certificate_key_path ("a.crt");
+ g_assert_cmpstr (path, ==, "a.key");
+ g_free (path);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/webcertificate/locate", test_locate);
+ g_test_add_func ("/webcertificate/keypath", test_keypath);
+
+ return g_test_run ();
+}
diff --git a/src/common/test-webresponse.c b/src/common/test-webresponse.c
new file mode 100644
index 0000000..abea862
--- /dev/null
+++ b/src/common/test-webresponse.c
@@ -0,0 +1,1558 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitwebinject.h"
+#include "cockpitwebresponse.h"
+#include "cockpitwebserver.h"
+
+#include "testlib/cockpittest.h"
+
+#include "websocket/websocket.h"
+
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+/* headers that are present in every request */
+#define STATIC_HEADERS "X-DNS-Prefetch-Control: off\r\nReferrer-Policy: no-referrer\r\nX-Content-Type-Options: nosniff\r\nCross-Origin-Resource-Policy: same-origin\r\nX-Frame-Options: sameorigin\r\n\r\n"
+static gchar *srcdir;
+
+typedef struct {
+ CockpitWebResponse *response;
+ GOutputStream *output;
+ gchar *scratch;
+ gboolean response_done;
+ gulong sig_done;
+} TestCase;
+
+typedef struct {
+ const gchar *path;
+ const gchar *header;
+ const gchar *value;
+ const gchar *method;
+ const gchar *expected_content_type;
+ CockpitCacheType cache;
+ gboolean for_tls_proxy;
+} TestFixture;
+
+static void
+on_response_done (CockpitWebResponse *response,
+ gboolean reusable,
+ gpointer user_data)
+{
+ gboolean *response_done = user_data;
+ g_assert (response_done != NULL);
+ g_assert (*response_done == FALSE);
+ *response_done = TRUE;
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ const gchar *path = NULL;
+ GHashTable *headers = NULL;
+ GInputStream *input;
+ GIOStream *io;
+
+ if (fixture)
+ path = fixture->path;
+
+ input = g_memory_input_stream_new ();
+ tc->output = g_memory_output_stream_new (NULL, 0, g_realloc, g_free);
+ io = g_simple_io_stream_new (input, tc->output);
+ g_object_unref (input);
+
+ if (fixture && fixture->header)
+ {
+ headers = cockpit_web_server_new_table ();
+ g_hash_table_insert (headers, g_strdup (fixture->header), g_strdup (fixture->value));
+ }
+
+ tc->response = cockpit_web_response_new (io, path, path, headers,
+ (fixture && fixture->method) ? fixture->method : "GET",
+ (fixture && fixture->for_tls_proxy) ? "https" : "http");
+
+ if (headers)
+ g_hash_table_unref (headers);
+ g_object_unref (io);
+
+ tc->sig_done = g_signal_connect (tc->response, "done",
+ G_CALLBACK (on_response_done),
+ &tc->response_done);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ while (g_main_context_iteration (NULL, FALSE));
+ g_assert (tc->response_done);
+
+ g_signal_handler_disconnect (tc->response, tc->sig_done);
+ g_clear_object (&tc->output);
+ g_clear_object (&tc->response);
+ g_free (tc->scratch);
+}
+
+static const gchar *
+output_as_string (TestCase *tc)
+{
+ while (!tc->response_done)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_free (tc->scratch);
+ tc->scratch = g_strndup (g_memory_output_stream_get_data (G_MEMORY_OUTPUT_STREAM (tc->output)),
+ g_memory_output_stream_get_data_size (G_MEMORY_OUTPUT_STREAM (tc->output)));
+ return tc->scratch;
+}
+
+static void
+test_return_content (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GBytes *content;
+
+ content = g_bytes_new_static ("the content", 11);
+ cockpit_web_response_content (tc->response, NULL, content, NULL);
+ g_bytes_unref (content);
+
+ resp = output_as_string (tc);
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n" STATIC_HEADERS "the content");
+}
+
+static void
+test_return_content_headers (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GHashTable *headers;
+ GBytes *content;
+
+ headers = cockpit_web_server_new_table ();
+ g_hash_table_insert (headers, g_strdup ("My-header"), g_strdup ("my-value"));
+
+ content = g_bytes_new_static ("the content", 11);
+ cockpit_web_response_content (tc->response, headers, content, NULL);
+ g_bytes_unref (content);
+ g_hash_table_destroy (headers);
+
+ resp = output_as_string (tc);
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nMy-header: my-value\r\nContent-Length: 11\r\n" STATIC_HEADERS "the content");
+}
+
+
+static void
+test_return_error (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+
+ cockpit_web_response_error (tc->response, 500, NULL, "Reason here: %s", "booyah");
+
+ resp = output_as_string (tc);
+ g_assert_cmpstr (resp, ==,
+ "HTTP/1.1 500 Reason here: booyah\r\n"
+ "Content-Type: text/html; charset=utf8\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ STATIC_HEADERS
+ "13\r\n<html><head><title>\r\n"
+ "13\r\nReason here: booyah\r\n"
+ "15\r\n</title></head><body>\r\n"
+ "13\r\nReason here: booyah\r\n"
+ "f\r\n</body></html>\n\r\n"
+ "0\r\n\r\n");
+}
+
+static void
+test_return_error_headers (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GHashTable *headers;
+
+ headers = cockpit_web_server_new_table ();
+ g_hash_table_insert (headers, g_strdup ("Header1"), g_strdup ("value1"));
+
+ cockpit_web_response_error (tc->response, 500, headers, "Reason here: %s", "booyah");
+
+ g_hash_table_destroy (headers);
+
+ resp = output_as_string (tc);
+ cockpit_assert_strmatch(resp,"HTTP/1.1 500 Reason here: booyah\r*\n"
+ "Header1: value1\r*\n"
+ "\r\n");
+}
+
+static void
+test_return_gerror_headers (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GHashTable *headers;
+ GError *error;
+
+ headers = cockpit_web_server_new_table ();
+ g_hash_table_insert (headers, g_strdup ("Header1"), g_strdup ("value1"));
+
+ error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, "Reason here: %s", "booyah");
+ cockpit_web_response_gerror (tc->response, headers, error);
+
+ g_error_free (error);
+ g_hash_table_destroy (headers);
+
+ resp = output_as_string (tc);
+ cockpit_assert_strmatch(resp,"HTTP/1.1 500 Reason here: booyah\r*\n"
+ "Header1: value1\r*\n"
+ "\r\n");
+}
+
+static void
+test_file_not_found (TestCase *tc,
+ gconstpointer user_data)
+{
+ const gchar *roots[] = { srcdir, NULL };
+ cockpit_web_response_file (tc->response, "/non-existent", roots);
+ cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 404 Not Found*");
+}
+
+static void
+test_file_directory_denied (TestCase *tc,
+ gconstpointer user_data)
+{
+ const gchar *roots[] = { srcdir, NULL };
+ cockpit_web_response_file (tc->response, "/src", roots);
+ cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 403 Directory Listing Denied*");
+}
+
+static void
+test_file_access_denied (TestCase *tc,
+ gconstpointer user_data)
+{
+ const gchar *roots[] = { "/tmp", NULL };
+ gchar templ[] = "/tmp/test-temp.XXXXXX";
+
+ if (!g_mkdtemp_full (templ, 0000))
+ g_assert_not_reached ();
+
+ cockpit_web_response_file (tc->response, templ + 4, roots);
+ cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 403*");
+
+ g_unlink (templ);
+}
+
+static void
+test_file_breakout_denied (TestCase *tc,
+ gconstpointer user_data)
+{
+ gchar *root = realpath ( SRCDIR "/src", NULL);
+ const gchar *roots[] = { root, NULL };
+ const gchar *breakout = "/../Makefile.am";
+ gchar *check = g_build_filename (roots[0], breakout, NULL);
+ g_assert (root);
+ g_assert (g_file_test (check, G_FILE_TEST_EXISTS));
+ g_free (check);
+ cockpit_web_response_file (tc->response, breakout, roots);
+ cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 404*");
+ free (root);
+}
+
+static void
+test_file_encoding_denied (TestCase *tc,
+ gconstpointer user_data)
+{
+ gchar *root = realpath ( SRCDIR "/src", NULL);
+ const gchar *roots[] = { root, NULL };
+ const gchar *breakout = "/common/Makefile-common.am%00";
+ gchar *check = g_build_filename (roots[0], "common", "Makefile-common.am", NULL);
+ g_assert (root);
+ g_assert (g_file_test (check, G_FILE_TEST_EXISTS));
+ g_free (check);
+ cockpit_web_response_file (tc->response, breakout, roots);
+ cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 404*");
+ free (root);
+}
+
+static void
+test_file_slash_denied (TestCase *tc,
+ gconstpointer user_data)
+{
+ gchar *root = realpath ( SRCDIR "/src", NULL);
+ const gchar *roots[] = { root, NULL };
+ const gchar *breakout = "/common%2fMakefile-common.am";
+ gchar *check = g_build_filename (roots[0], "common", "Makefile-common.am", NULL);
+ g_assert (root);
+ g_assert (g_file_test (check, G_FILE_TEST_EXISTS));
+ g_free (check);
+ cockpit_web_response_file (tc->response, breakout, roots);
+ cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 404*");
+ free (root);
+}
+
+static void
+test_file_breakout_non_existant (TestCase *tc,
+ gconstpointer user_data)
+{
+ gchar *root = realpath ( SRCDIR "/src", NULL);
+ const gchar *roots[] = { root, NULL };
+ const gchar *breakout = "/../non-existent";
+ gchar *check = g_build_filename (roots[0], breakout, NULL);
+ g_assert (root);
+ g_assert (!g_file_test (check, G_FILE_TEST_EXISTS));
+ g_free (check);
+ cockpit_web_response_file (tc->response, breakout, roots);
+ cockpit_assert_strmatch (output_as_string (tc), "HTTP/1.1 404*");
+ free (root);
+}
+
+static const TestFixture content_type_fixture_html = {
+ .path = "/pkg/shell/index.html",
+ .expected_content_type = "text/html",
+};
+
+static const TestFixture content_type_fixture_png = {
+ .path = "/pkg/shell/images/server-small.png",
+ .expected_content_type = "image/png",
+};
+
+static const TestFixture content_type_fixture_wasm = {
+ .path = "/src/common/mock-content/test.wasm",
+ .expected_content_type = "application/wasm",
+};
+
+static const TestFixture fixture_head = {
+ .method = "HEAD",
+};
+
+static const TestFixture fixture_unsupported_method = {
+ .method = "PATCH",
+};
+
+static void
+test_content_type (TestCase *tc,
+ gconstpointer user_data)
+{
+ const TestFixture *fixture = user_data;
+ const gchar *roots[] = { srcdir, NULL };
+ GHashTable *headers;
+ const gchar *resp;
+ gsize length;
+ guint status;
+ gssize off;
+
+ cockpit_web_response_file (tc->response, NULL, roots);
+
+ resp = output_as_string (tc);
+ length = strlen (resp);
+
+ off = web_socket_util_parse_status_line (resp, length, NULL, &status, NULL);
+ g_assert_cmpuint (off, >, 0);
+ g_assert_cmpint (status, ==, 200);
+
+ off = web_socket_util_parse_headers (resp + off, length - off, &headers);
+ g_assert_cmpuint (off, >, 0);
+
+ g_assert_cmpstr (g_hash_table_lookup (headers, "Content-Type"), ==, fixture->expected_content_type);
+
+ g_hash_table_unref (headers);
+}
+
+static const TestFixture template_fixture = {
+ .path = "/test.css"
+};
+
+static void
+test_template (TestCase *tc,
+ gconstpointer user_data)
+{
+ const gchar *roots[] = { SRCDIR "/src/common/mock-content/", NULL };
+ const gchar *resp;
+ GHashTable *data = g_hash_table_new (g_str_hash, g_str_equal);
+
+ g_hash_table_insert (data, "NAME", "test");
+ g_hash_table_insert (data, "VARIANT", "VALUE");
+ cockpit_web_response_template (tc->response, NULL, roots, data);
+
+ resp = output_as_string (tc);
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Type: text/css\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS "17\r\n#brand {\n content: \"\r\n4\r\ntest\r\n4\r\n <b>\r\n5\r\nVALUE\r\n9\r\n</b>\";\n}\n\r\n0\r\n\r\n");
+ g_hash_table_unref (data);
+}
+
+static const TestFixture cache_none_fixture = {
+ .path = "/pkg/shell/index.html",
+ .cache = COCKPIT_WEB_RESPONSE_NO_CACHE
+};
+
+static const TestFixture cache_fixture = {
+ .path = "/pkg/shell/index.html",
+ .cache = COCKPIT_WEB_RESPONSE_CACHE
+};
+
+static const TestFixture cache_unset_fixture = {
+ .path = "/pkg/shell/index.html",
+ .cache = COCKPIT_WEB_RESPONSE_CACHE_UNSET
+};
+
+static void
+test_cache (TestCase *tc,
+ gconstpointer user_data)
+{
+ const TestFixture *fixture = user_data;
+ const gchar *roots[] = { srcdir, NULL };
+ GHashTable *headers;
+ const gchar *resp;
+ gsize length;
+ guint status;
+ gssize off;
+
+ cockpit_web_response_set_cache_type (tc->response, fixture->cache);
+ cockpit_web_response_file (tc->response, NULL, roots);
+
+ resp = output_as_string (tc);
+ length = strlen (resp);
+
+ off = web_socket_util_parse_status_line (resp, length, NULL, &status, NULL);
+ g_assert_cmpuint (off, >, 0);
+ g_assert_cmpint (status, ==, 200);
+
+ off = web_socket_util_parse_headers (resp + off, length - off, &headers);
+ g_assert_cmpuint (off, >, 0);
+
+ if (fixture->cache == COCKPIT_WEB_RESPONSE_CACHE)
+ g_assert_cmpstr (g_hash_table_lookup (headers, "Vary"), ==, "Cookie");
+ else
+ g_assert_null (g_hash_table_lookup (headers, "Vary"));
+
+ if (fixture->cache == COCKPIT_WEB_RESPONSE_NO_CACHE)
+ g_assert_cmpstr (g_hash_table_lookup (headers, "Cache-Control"), ==, "no-cache, no-store");
+ else if (fixture->cache == COCKPIT_WEB_RESPONSE_CACHE)
+ g_assert_cmpstr (g_hash_table_lookup (headers, "Cache-Control"), ==, "max-age=86400, private");
+ else
+ g_assert_null (g_hash_table_lookup (headers, "Cache-Control"));
+ g_hash_table_unref (headers);
+}
+
+static void
+test_content_encoding (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GBytes *content;
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);
+
+ cockpit_web_response_headers (tc->response, 200, "OK", 50,
+ "Content-Encoding", "blah",
+ NULL);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ content = g_bytes_new_static ("Cockpit is perfect for new sysadmins, ", 38);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ cockpit_web_response_complete (tc->response);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Encoding: blah\r\n"
+ "Transfer-Encoding: chunked\r\n" STATIC_HEADERS
+ "26\r\nCockpit is perfect for new sysadmins, \r\n0\r\n\r\n");
+}
+
+static void
+test_stream (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GBytes *content;
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);
+
+ cockpit_web_response_headers (tc->response, 200, "OK", 11, NULL);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ content = g_bytes_new_static ("the content", 11);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+
+ cockpit_web_response_complete (tc->response);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n" STATIC_HEADERS "the content");
+}
+
+static void
+on_pressure_set_throttle (CockpitWebResponse *response,
+ gboolean throttle,
+ gpointer user_data)
+{
+ gint *data = user_data;
+ g_assert (user_data != NULL);
+ *data = throttle ? 1 : 0;
+}
+
+static void
+test_pressure (TestCase *tc,
+ gconstpointer data)
+{
+ GBytes *sent;
+ gint throttle = -1;
+ gint i;
+
+ g_signal_connect (tc->response, "pressure", G_CALLBACK (on_pressure_set_throttle), &throttle);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);
+
+ cockpit_web_response_headers (tc->response, 200, "OK", -1, NULL);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ /* Sent this a thousand times */
+ sent = g_bytes_new_take (g_strnfill (10 * 1000, '?'), 10 * 1000);
+ for (i = 0; i < 1000; i++)
+ cockpit_web_response_queue (tc->response, sent);
+ g_bytes_unref (sent);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+
+ /*
+ * This should have put way too much in the queue, and thus
+ * emitted the back-pressure signal. This signal would normally
+ * be used by others to slow down their queueing, but in this
+ * case we just check that it was fired.
+ */
+ g_assert_cmpint (throttle, ==, 1);
+ throttle = -1;
+
+ cockpit_web_response_complete (tc->response);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);
+
+ /*
+ * Now the queue is getting drained. At some point, it will be
+ * signaled that back pressure has been turned off
+ */
+ while (throttle == -1)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (throttle, ==, 0);
+
+ while (!tc->response_done)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (g_memory_output_stream_get_data_size (G_MEMORY_OUTPUT_STREAM (tc->output)), >, 10 * 1000 * 1000);
+}
+
+static void
+test_head (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GBytes *content;
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);
+
+ cockpit_web_response_headers (tc->response, 200, "OK", 19, NULL);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ content = g_bytes_new_static ("I shall not be seen", 19);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+
+ cockpit_web_response_complete (tc->response);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Length: 19\r\n" STATIC_HEADERS);
+}
+
+static void
+test_unsupported_method (TestCase *tc,
+ gconstpointer data)
+{
+ cockpit_web_response_error (tc->response, 405, NULL, "Unsupported method");
+
+ const gchar *resp = output_as_string (tc);
+ g_assert (g_str_has_prefix (resp, "HTTP/1.1 405 Unsupported method\r\n"));
+ /* not a HEAD request, thus has body */
+ g_assert (strstr (resp, "<body>"));
+}
+
+static void
+test_chunked_transfer_encoding (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GBytes *content;
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);
+
+ cockpit_web_response_headers (tc->response, 200, "OK", -1, NULL);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ content = g_bytes_new_static ("Cockpit is perfect for new sysadmins, ", 38);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ content = g_bytes_new_static ("allowing them to easily perform simple tasks such as storage administration, ", 77);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ content = g_bytes_new_static ("inspecting journals and starting and stopping services.", 55);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+
+ cockpit_web_response_complete (tc->response);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
+ "26\r\nCockpit is perfect for new sysadmins, \r\n"
+ "4d\r\nallowing them to easily perform simple tasks such as storage administration, \r\n"
+ "37\r\ninspecting journals and starting and stopping services.\r\n0\r\n\r\n");
+}
+
+static void
+test_chunked_zero_length (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GBytes *content;
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_READY);
+
+ cockpit_web_response_headers (tc->response, 200, "OK", -1, NULL);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ content = g_bytes_new_static ("Cockpit is perfect for new sysadmins, ", 38);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ content = g_bytes_new_static ("", 0);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ content = g_bytes_new_static ("inspecting journals and starting and stopping services.", 55);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ content = g_bytes_new_static ("", 0);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_QUEUING);
+ cockpit_web_response_complete (tc->response);
+
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_COMPLETE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
+ "26\r\nCockpit is perfect for new sysadmins, \r\n"
+ "37\r\ninspecting journals and starting and stopping services.\r\n0\r\n\r\n");
+}
+
+static GBytes *
+bytes_static (const gchar *data)
+{
+ return g_bytes_new_static (data, strlen (data));
+}
+
+static void
+test_web_filter_simple (TestCase *tc,
+ gconstpointer data)
+{
+ CockpitWebFilter *filter;
+ const gchar *resp;
+ GBytes *content;
+ GBytes *inject;
+
+ inject = bytes_static ("<meta inject>");
+ filter = cockpit_web_inject_new ("<head>", inject, 1);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ content = bytes_static ("<html><head><title>The Title</title></head></html>");
+ cockpit_web_response_content (tc->response, NULL, content, NULL);
+ g_bytes_unref (content);
+
+ while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
+ g_main_context_iteration (NULL, TRUE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
+ "c\r\n<html><head>\r\n"
+ "d\r\n<meta inject>\r\n"
+ "26\r\n<title>The Title</title></head></html>\r\n"
+ "0\r\n\r\n");
+}
+
+static void
+test_web_filter_multiple (TestCase *tc,
+ gconstpointer data)
+{
+ CockpitWebFilter *filter;
+ const gchar *resp;
+ GBytes *content;
+ GBytes *inject;
+
+ inject = bytes_static ("<meta inject>");
+ filter = cockpit_web_inject_new ("<head>", inject, 1);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ inject = bytes_static ("<body>Body</body>");
+ filter = cockpit_web_inject_new ("</head>", inject, 1);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ inject = bytes_static ("Prefix ");
+ filter = cockpit_web_inject_new ("<title>", inject, 1);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ inject = bytes_static (" ");
+ filter = cockpit_web_inject_new (">", inject, 3);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ content = bytes_static ("<html><head><title>The Title</title></head></html>");
+ cockpit_web_response_content (tc->response, NULL, content, NULL);
+ g_bytes_unref (content);
+
+ while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
+ g_main_context_iteration (NULL, TRUE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
+ "6\r\n<html>\r\n"
+ "1\r\n \r\n"
+ "6\r\n<head>\r\n"
+ "1\r\n \r\n"
+ "d\r\n<meta inject>\r\n"
+ "1\r\n \r\n"
+ "7\r\n<title>\r\n"
+ "7\r\nPrefix \r\n"
+ "18\r\nThe Title</title></head>\r\n"
+ "11\r\n<body>Body</body>\r\n"
+ "7\r\n</html>\r\n"
+ "0\r\n\r\n");
+}
+
+static void
+test_web_filter_split (TestCase *tc,
+ gconstpointer data)
+{
+ CockpitWebFilter *filter;
+ const gchar *string;
+ const gchar *resp;
+ GBytes *inject;
+ GBytes *block;
+ gsize i, x, len;
+
+ inject = bytes_static ("<meta inject>");
+ filter = cockpit_web_inject_new ("<head>", inject, 1);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ inject = bytes_static ("<body>Body</body>");
+ filter = cockpit_web_inject_new ("</head>", inject, 1);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ inject = bytes_static ("Prefix ");
+ filter = cockpit_web_inject_new ("<title>", inject, 1);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ cockpit_web_response_headers (tc->response, 200, "OK", -1, NULL);
+
+ string = "<html><head><title>The Title</title></head></html>";
+ len = strlen (string);
+
+ for (i = 0, x = 1; i < len; i += x, x = 1 + (i % 4))
+ {
+ block = g_bytes_new_static (string + i, MIN (x, strlen (string + i)));
+ g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
+ g_bytes_unref (block);
+ }
+
+ cockpit_web_response_complete (tc->response);
+
+ while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
+ g_main_context_iteration (NULL, TRUE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
+ "1\r\n<\r\n"
+ "2\r\nht\r\n"
+ "4\r\nml><\r\n"
+ "4\r\nhead\r\n"
+ "1\r\n>\r\n"
+ "d\r\n<meta inject>\r\n"
+ "3\r\n<ti\r\n"
+ "4\r\ntle>\r\n"
+ "7\r\nPrefix \r\n"
+ "4\r\nThe \r\n"
+ "4\r\nTitl\r\n"
+ "4\r\ne</t\r\n"
+ "4\r\nitle\r\n"
+ "4\r\n></h\r\n"
+ "4\r\nead>\r\n"
+ "11\r\n<body>Body</body>\r\n"
+ "4\r\n</ht\r\n"
+ "3\r\nml>\r\n"
+ "0\r\n\r\n");
+}
+
+static void
+test_web_filter_shift (TestCase *tc,
+ gconstpointer data)
+{
+ CockpitWebFilter *filter;
+ const gchar *resp;
+ GBytes *block;
+ GBytes *inject;
+
+ inject = bytes_static ("injected");
+ filter = cockpit_web_inject_new ("foofn", inject, 1);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ cockpit_web_response_headers_full (tc->response, 200, "OK", -1, NULL);
+
+ /* Total content is foofoofn and split after the first 4 characters */
+ block = bytes_static ("foof");
+ g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
+ g_bytes_unref (block);
+ block = bytes_static ("oofn");
+ g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
+ g_bytes_unref (block);
+ cockpit_web_response_complete (tc->response);
+
+ while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
+ g_main_context_iteration (NULL, TRUE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
+ "4\r\nfoof\r\n"
+ "4\r\noofn\r\n"
+ "8\r\ninjected\r\n"
+ "0\r\n\r\n");
+}
+
+static void
+test_web_filter_shift_three (TestCase *tc,
+ gconstpointer data)
+{
+ CockpitWebFilter *filter;
+ const gchar *resp;
+ GBytes *block;
+ GBytes *inject;
+
+ inject = bytes_static ("injected");
+ filter = cockpit_web_inject_new ("foofn", inject, 1);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ cockpit_web_response_headers_full (tc->response, 200, "OK", -1, NULL);
+
+ /* Total content is foofoofn and split across multiple packets after the first 4 characters */
+ block = bytes_static ("foof");
+ g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
+ g_bytes_unref (block);
+ block = bytes_static ("o");
+ g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
+ g_bytes_unref (block);
+ block = bytes_static ("of");
+ g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
+ g_bytes_unref (block);
+ block = bytes_static ("n");
+ g_assert (cockpit_web_response_queue (tc->response, block) == TRUE);
+ g_bytes_unref (block);
+ cockpit_web_response_complete (tc->response);
+
+ while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
+ g_main_context_iteration (NULL, TRUE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
+ "4\r\nfoof\r\n"
+ "1\r\no\r\n"
+ "2\r\nof\r\n"
+ "1\r\nn\r\n"
+ "8\r\ninjected\r\n"
+ "0\r\n\r\n");
+}
+
+static void
+test_web_filter_passthrough (TestCase *tc,
+ gconstpointer data)
+{
+ CockpitWebFilter *filter;
+ const gchar *resp;
+ GBytes *content;
+ GBytes *inject;
+
+ inject = bytes_static ("<meta inject>");
+ filter = cockpit_web_inject_new ("<unknown>", inject, 1);
+ cockpit_web_response_add_filter (tc->response, filter);
+ g_object_unref (filter);
+ g_bytes_unref (inject);
+
+ content = bytes_static ("<html><head><title>The Title</title></head></html>");
+ cockpit_web_response_content (tc->response, NULL, content, NULL);
+ g_bytes_unref (content);
+
+ while (cockpit_web_response_get_state (tc->response) != COCKPIT_WEB_RESPONSE_COMPLETE)
+ g_main_context_iteration (NULL, TRUE);
+
+ resp = output_as_string (tc);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS
+ "32\r\n<html><head><title>The Title</title></head></html>\r\n"
+ "0\r\n\r\n");
+}
+
+static void
+on_response_done_not_resuable (CockpitWebResponse *response,
+ gboolean reusable,
+ gpointer user_data)
+{
+ g_assert (reusable == FALSE);
+}
+
+static void
+test_abort (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GBytes *content;
+
+ cockpit_web_response_headers (tc->response, 200, "OK", 11, NULL);
+ g_signal_connect (tc->response, "done", G_CALLBACK (on_response_done_not_resuable), NULL);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ content = g_bytes_new_static ("the content", 11);
+ cockpit_web_response_queue (tc->response, content);
+ g_bytes_unref (content);
+
+ cockpit_web_response_abort (tc->response);
+ g_assert_cmpint (cockpit_web_response_get_state (tc->response), ==, COCKPIT_WEB_RESPONSE_SENT);
+
+ resp = output_as_string (tc);
+
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n" STATIC_HEADERS);
+}
+
+static const TestFixture fixture_connection_close = {
+ .header = "Connection",
+ .value = "close",
+};
+
+static void
+test_connection_close (TestCase *tc,
+ gconstpointer data)
+{
+ const gchar *resp;
+ GBytes *content;
+
+ g_assert (data == &fixture_connection_close);
+
+ g_signal_connect (tc->response, "done", G_CALLBACK (on_response_done_not_resuable), NULL);
+
+ content = g_bytes_new_static ("the content", 11);
+ cockpit_web_response_content (tc->response, NULL, content, NULL);
+ g_bytes_unref (content);
+
+ resp = output_as_string (tc);
+ g_assert_cmpstr (resp, ==, "HTTP/1.1 200 OK\r\nContent-Length: 11\r\nConnection: close\r\n"
+ STATIC_HEADERS "the content");
+}
+
+static const TestFixture fixture_origin_default = {
+ .header = "Host",
+ .value = "somemachine",
+ .for_tls_proxy = FALSE,
+};
+
+static const TestFixture fixture_origin_tls_proxy = {
+ .header = "Host",
+ .value = "somemachine",
+ .for_tls_proxy = TRUE,
+};
+
+static void
+test_origin (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ GBytes *content;
+
+ if (fixture->for_tls_proxy)
+ g_assert_cmpstr (cockpit_web_response_get_origin (tc->response), ==, "https://somemachine");
+ else
+ g_assert_cmpstr (cockpit_web_response_get_origin (tc->response), ==, "http://somemachine");
+
+ content = g_bytes_new_static ("the content", 11);
+ cockpit_web_response_content (tc->response, NULL, content, NULL);
+ g_bytes_unref (content);
+ output_as_string (tc);
+}
+
+typedef struct {
+ GHashTable *headers;
+ GIOStream *io;
+} TestPlain;
+
+static void
+setup_plain (TestPlain *tc,
+ gconstpointer unused)
+{
+ GInputStream *input;
+ GOutputStream *output;
+
+ input = g_memory_input_stream_new ();
+ output = g_memory_output_stream_new (NULL, 0, g_realloc, g_free);
+
+ tc->io = g_simple_io_stream_new (input, output);
+
+ g_object_unref (input);
+ g_object_unref (output);
+
+ tc->headers = cockpit_web_server_new_table ();
+}
+
+static void
+teardown_plain (TestPlain *tc,
+ gconstpointer unused)
+{
+ g_object_unref (tc->io);
+ g_hash_table_unref (tc->headers);
+}
+
+static void
+test_pop_path (TestPlain *tc,
+ gconstpointer unused)
+{
+ CockpitWebResponse *response;
+ gchar *part;
+ const gchar *start = "/cockpit/@localhost/another/test.html";
+
+ response = cockpit_web_response_new (tc->io, start, start, tc->headers, "GET", NULL);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, start);
+ g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, NULL);
+
+ part = cockpit_web_response_pop_path (response);
+ g_assert_cmpstr (part, ==, "cockpit");
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/@localhost/another/test.html");
+ g_free (part);
+
+ part = cockpit_web_response_pop_path (response);
+ g_assert_cmpstr (part, ==, "@localhost");
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/another/test.html");
+ g_free (part);
+
+ part = cockpit_web_response_pop_path (response);
+ g_assert_cmpstr (part, ==, "another");
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/test.html");
+ g_free (part);
+
+ part = cockpit_web_response_pop_path (response);
+ g_assert_cmpstr (part, ==, "test.html");
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, NULL);
+ g_free (part);
+
+ part = cockpit_web_response_pop_path (response);
+ g_assert (part == NULL);
+ g_assert (cockpit_web_response_get_path (response) == NULL);
+ g_free (part);
+
+ cockpit_web_response_abort (response);
+ g_object_unref (response);
+}
+
+static void
+test_pop_path_root (TestPlain *tc,
+ gconstpointer unused)
+{
+ CockpitWebResponse *response;
+ gchar *part;
+
+ response = cockpit_web_response_new (tc->io, "/", "/", tc->headers, "GET", NULL);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/");
+
+ part = cockpit_web_response_pop_path (response);
+ g_assert_cmpstr (part, ==, NULL);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, NULL);
+ g_free (part);
+
+ cockpit_web_response_abort (response);
+ g_object_unref (response);
+}
+
+static void
+test_skip_path (TestPlain *tc,
+ gconstpointer unused)
+{
+ CockpitWebResponse *response;
+ const gchar *start = "/cockpit/@localhost/another/test.html";
+
+ response = cockpit_web_response_new (tc->io, start, start, tc->headers, "GET", NULL);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/cockpit/@localhost/another/test.html");
+
+ g_assert (cockpit_web_response_skip_path (response) == TRUE);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/@localhost/another/test.html");
+
+ g_assert (cockpit_web_response_skip_path (response) == TRUE);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/another/test.html");
+
+ g_assert (cockpit_web_response_skip_path (response) == TRUE);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/test.html");
+
+ g_assert (cockpit_web_response_skip_path (response) == TRUE);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, NULL);
+
+ g_assert (cockpit_web_response_skip_path (response) == FALSE);
+ g_assert (cockpit_web_response_get_path (response) == NULL);
+
+ cockpit_web_response_abort (response);
+ g_object_unref (response);
+}
+
+static void
+test_skip_path_root (TestPlain *tc,
+ gconstpointer unused)
+{
+ CockpitWebResponse *response;
+
+ response = cockpit_web_response_new (tc->io, "/", "/", tc->headers, "GET", NULL);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/");
+
+ g_assert (cockpit_web_response_skip_path (response) == FALSE);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, NULL);
+
+ cockpit_web_response_abort (response);
+ g_object_unref (response);
+}
+
+static void
+test_removed_prefix (TestPlain *tc,
+ gconstpointer unused)
+{
+ CockpitWebResponse *response;
+
+ response = cockpit_web_response_new (tc->io, "/", "/", tc->headers, "GET", NULL);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/");
+ g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, NULL);
+ cockpit_web_response_abort (response);
+ g_clear_object (&response);
+
+ response = cockpit_web_response_new (tc->io, "/path/", "/path/", tc->headers, "GET", NULL);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/path/");
+ g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, NULL);
+ cockpit_web_response_abort (response);
+ g_clear_object (&response);
+
+ response = cockpit_web_response_new (tc->io, "/path/path2/", "/path2/", tc->headers, "GET", NULL);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/path2/");
+ g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, "/path");
+ cockpit_web_response_abort (response);
+ g_clear_object (&response);
+
+ response = cockpit_web_response_new (tc->io, "/mis/", "/match/", tc->headers, "GET", NULL);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, "/match/");
+ g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, NULL);
+ cockpit_web_response_abort (response);
+ g_clear_object (&response);
+
+ response = cockpit_web_response_new (tc->io, NULL, NULL, tc->headers, "GET", NULL);
+ g_assert_cmpstr (cockpit_web_response_get_path (response), ==, NULL);
+ g_assert_cmpstr (cockpit_web_response_get_url_root (response), ==, NULL);
+ cockpit_web_response_abort (response);
+ g_clear_object (&response);
+}
+
+static void
+test_gunzip_small (void)
+{
+ GError *error = NULL;
+ GMappedFile *file;
+ GBytes *compressed;
+ GBytes *bytes;
+
+ file = g_mapped_file_new (SRCDIR "/src/common/mock-content/test-file.txt.gz", FALSE, &error);
+ g_assert_no_error (error);
+
+ compressed = g_mapped_file_get_bytes (file);
+ g_mapped_file_unref (file);
+
+ bytes = cockpit_web_response_gunzip (compressed, &error);
+ g_assert_no_error (error);
+ g_bytes_unref (compressed);
+
+ cockpit_assert_bytes_eq (bytes, "A small test file\n", -1);
+ g_bytes_unref (bytes);
+}
+
+static void
+test_gunzip_large (void)
+{
+ GError *error = NULL;
+ GMappedFile *file;
+ GBytes *compressed;
+ GBytes *bytes;
+ gchar *checksum;
+
+ file = g_mapped_file_new (SRCDIR "/src/common/mock-content/large.min.js.gz", FALSE, &error);
+ g_assert_no_error (error);
+
+ compressed = g_mapped_file_get_bytes (file);
+ g_mapped_file_unref (file);
+
+ bytes = cockpit_web_response_gunzip (compressed, &error);
+ g_assert_no_error (error);
+ g_bytes_unref (compressed);
+
+ checksum = g_compute_checksum_for_bytes (G_CHECKSUM_MD5, bytes);
+ g_assert_cmpstr (checksum, ==, "5ca7582261c421482436dfdf3af9bffe");
+ g_free (checksum);
+
+ g_bytes_unref (bytes);
+}
+
+static void
+test_gunzip_invalid (void)
+{
+ GError *error = NULL;
+ GBytes *compressed;
+ GBytes *bytes;
+
+ compressed = g_bytes_new_static ("invalid", 7);
+
+ bytes = cockpit_web_response_gunzip (compressed, &error);
+ g_assert (bytes == NULL);
+ g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA);
+ g_error_free (error);
+
+ g_bytes_unref (compressed);
+}
+
+static void
+test_negotiation_first (void)
+{
+ gboolean is_language_specific, is_compressed;
+ GError *error = NULL;
+ GBytes *bytes;
+
+ bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/test-file.txt",
+ NULL, NULL, &is_language_specific, &is_compressed, &error);
+
+ cockpit_assert_bytes_eq (bytes, "A small test file\n", -1);
+ g_assert_no_error (error);
+ g_bytes_unref (bytes);
+
+ g_assert (!is_language_specific);
+ g_assert (!is_compressed);
+}
+
+static void
+test_negotiation_last (void)
+{
+ gboolean is_language_specific, is_compressed;
+ GError *error = NULL;
+ gchar *checksum;
+ GBytes *bytes;
+
+ bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/large.js",
+ NULL, NULL, &is_language_specific, &is_compressed, &error);
+
+ g_assert_no_error (error);
+ g_assert (!is_language_specific);
+ g_assert (is_compressed);
+
+ checksum = g_compute_checksum_for_bytes (G_CHECKSUM_MD5, bytes);
+ g_assert_cmpstr (checksum, ==, "e5284b625b7665fc04e082827de3436c");
+ g_free (checksum);
+
+ g_bytes_unref (bytes);
+}
+
+static void
+test_negotiation_prune (void)
+{
+ GError *error = NULL;
+ GBytes *bytes;
+
+ bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/test-file.extra.extension.txt",
+ NULL, NULL, NULL, NULL, &error);
+
+ cockpit_assert_bytes_eq (bytes, "A small test file\n", -1);
+ g_assert_no_error (error);
+ g_bytes_unref (bytes);
+}
+
+static void
+test_negotiation_with_listing (void)
+{
+ GHashTable *existing;
+ GError *error = NULL;
+ GBytes *bytes;
+
+ /* Lie and say that only the .gz file exists */
+ existing = g_hash_table_new (g_str_hash, g_str_equal);
+ g_hash_table_add (existing, SRCDIR "/src/common/mock-content/test-file.txt.gz");
+
+ bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/test-file.txt",
+ existing, NULL, NULL, NULL, &error);
+
+ cockpit_assert_bytes_eq (bytes, "\x1F\x8B\x08\x08N1\x03U\x00\x03test-file.txt\x00"
+ "sT(\xCEM\xCC\xC9Q(I-.QH\xCB\xCCI\xE5\x02\x00>PjG\x12\x00\x00\x00", 52);
+ g_assert_no_error (error);
+ g_bytes_unref (bytes);
+
+ g_hash_table_unref (existing);
+}
+
+static void
+test_negotiation_locale (void)
+{
+ gboolean is_language_specific, is_compressed;
+ GError *error = NULL;
+ GBytes *bytes;
+
+ bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/test-file.txt",
+ NULL, "zh-cn", &is_language_specific, &is_compressed, &error);
+
+ cockpit_assert_bytes_eq (bytes, "A translated test file\n", -1);
+ g_assert_no_error (error);
+ g_bytes_unref (bytes);
+
+ g_assert (is_language_specific);
+ g_assert (!is_compressed);
+}
+
+static void
+test_negotiation_notfound (void)
+{
+ GError *error = NULL;
+ GBytes *bytes;
+
+ bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/non-existent",
+ NULL, NULL, NULL, NULL, &error);
+
+ g_assert_no_error (error);
+ g_assert (bytes == NULL);
+}
+
+static void
+test_negotiation_failure (void)
+{
+ GError *error = NULL;
+ GBytes *bytes;
+
+ bytes = cockpit_web_response_negotiation (SRCDIR "/src/common/mock-content/directory",
+ NULL, NULL, NULL, NULL, &error);
+
+ g_assert (error != NULL);
+ g_error_free (error);
+
+ g_assert (bytes == NULL);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ extern const gchar *cockpit_webresponse_fail_html_text;
+ cockpit_webresponse_fail_html_text =
+ "<html><head><title>@@message@@</title></head><body>@@message@@</body></html>\n";
+
+ gint ret;
+
+ srcdir = realpath (SRCDIR, NULL);
+ g_assert (srcdir != NULL);
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/web-response/return-content", TestCase, NULL,
+ setup, test_return_content, teardown);
+ g_test_add ("/web-response/return-content-headers", TestCase, NULL,
+ setup, test_return_content_headers, teardown);
+ g_test_add ("/web-response/return-error", TestCase, NULL,
+ setup, test_return_error, teardown);
+ g_test_add ("/web-response/return-error-headers", TestCase, NULL,
+ setup, test_return_error_headers, teardown);
+ g_test_add ("/web-response/return-gerror-headers", TestCase, NULL,
+ setup, test_return_gerror_headers, teardown);
+ g_test_add ("/web-response/file/not-found", TestCase, NULL,
+ setup, test_file_not_found, teardown);
+ g_test_add ("/web-response/file/directory-denied", TestCase, NULL,
+ setup, test_file_directory_denied, teardown);
+ g_test_add ("/web-response/file/access-denied", TestCase, NULL,
+ setup, test_file_access_denied, teardown);
+ g_test_add ("/web-response/file/breakout-denied", TestCase, NULL,
+ setup, test_file_breakout_denied, teardown);
+ g_test_add ("/web-response/file/invalid-encoding-denied", TestCase, NULL,
+ setup, test_file_encoding_denied, teardown);
+ g_test_add ("/web-response/file/file-slash-denied", TestCase, NULL,
+ setup, test_file_slash_denied, teardown);
+ g_test_add ("/web-response/file/breakout-non-existant", TestCase, NULL,
+ setup, test_file_breakout_non_existant, teardown);
+ g_test_add ("/web-reponse/file/template", TestCase, &template_fixture,
+ setup, test_template, teardown);
+ g_test_add ("/web-response/content-type/html", TestCase, &content_type_fixture_html,
+ setup, test_content_type, teardown);
+ g_test_add ("/web-response/content-type/png", TestCase, &content_type_fixture_png,
+ setup, test_content_type, teardown);
+ g_test_add ("/web-response/content-type/wasm", TestCase, &content_type_fixture_wasm,
+ setup, test_content_type, teardown);
+ g_test_add ("/web-response/content-encoding", TestCase, NULL,
+ setup, test_content_encoding, teardown);
+ g_test_add ("/web-response/stream", TestCase, NULL,
+ setup, test_stream, teardown);
+ g_test_add ("/web-response/pressure", TestCase, NULL,
+ setup, test_pressure, teardown);
+ g_test_add ("/web-response/head", TestCase, &fixture_head,
+ setup, test_head, teardown);
+ g_test_add ("/web-response/unsupported-method", TestCase, &fixture_unsupported_method,
+ setup, test_unsupported_method, teardown);
+ g_test_add ("/web-response/chunked-transfer-encoding", TestCase, NULL,
+ setup, test_chunked_transfer_encoding, teardown);
+ g_test_add ("/web-response/chunked-zero-length", TestCase, NULL,
+ setup, test_chunked_zero_length, teardown);
+ g_test_add ("/web-response/abort", TestCase, NULL,
+ setup, test_abort, teardown);
+ g_test_add ("/web-response/connection-close", TestCase, &fixture_connection_close,
+ setup, test_connection_close, teardown);
+ g_test_add ("/web-response/origin-default", TestCase, &fixture_origin_default,
+ setup, test_origin, teardown);
+ g_test_add ("/web-response/origin-tls-proxy", TestCase, &fixture_origin_tls_proxy,
+ setup, test_origin, teardown);
+
+ g_test_add ("/web-response/cache", TestCase, &cache_fixture,
+ setup, test_cache, teardown);
+ g_test_add ("/web-response/cache-none", TestCase, &cache_none_fixture,
+ setup, test_cache, teardown);
+ g_test_add ("/web-response/cache-unset", TestCase, &cache_unset_fixture,
+ setup, test_cache, teardown);
+
+ g_test_add ("/web-response/filter/simple", TestCase, NULL,
+ setup, test_web_filter_simple, teardown);
+ g_test_add ("/web-response/filter/multiple", TestCase, NULL,
+ setup, test_web_filter_multiple, teardown);
+ g_test_add ("/web-response/filter/passthrough", TestCase, NULL,
+ setup, test_web_filter_passthrough, teardown);
+ g_test_add ("/web-response/filter/split", TestCase, NULL,
+ setup, test_web_filter_split, teardown);
+ g_test_add ("/web-response/filter/shift", TestCase, NULL,
+ setup, test_web_filter_shift, teardown);
+ g_test_add ("/web-response/filter/shift_three", TestCase, NULL,
+ setup, test_web_filter_shift_three, teardown);
+
+ g_test_add ("/web-response/path/pop", TestPlain, NULL,
+ setup_plain, test_pop_path, teardown_plain);
+ g_test_add ("/web-response/path/pop-root", TestPlain, NULL,
+ setup_plain, test_pop_path_root, teardown_plain);
+ g_test_add ("/web-response/path/skip", TestPlain, NULL,
+ setup_plain, test_skip_path, teardown_plain);
+ g_test_add ("/web-response/path/skip-root", TestPlain, NULL,
+ setup_plain, test_skip_path_root, teardown_plain);
+ g_test_add ("/web-response/path/removed-prefix", TestPlain, NULL,
+ setup_plain, test_removed_prefix, teardown_plain);
+
+ g_test_add_func ("/web-response/gunzip/small", test_gunzip_small);
+ g_test_add_func ("/web-response/gunzip/large", test_gunzip_large);
+ g_test_add_func ("/web-response/gunzip/invalid", test_gunzip_invalid);
+
+ g_test_add_func ("/web-response/negotiation/first", test_negotiation_first);
+ g_test_add_func ("/web-response/negotiation/last", test_negotiation_last);
+ g_test_add_func ("/web-response/negotiation/locale", test_negotiation_locale);
+ g_test_add_func ("/web-response/negotiation/prune", test_negotiation_prune);
+ g_test_add_func ("/web-response/negotiation/with-listing", test_negotiation_with_listing);
+ g_test_add_func ("/web-response/negotiation/notfound", test_negotiation_notfound);
+ g_test_add_func ("/web-response/negotiation/failure", test_negotiation_failure);
+
+ ret = g_test_run ();
+
+ free (srcdir);
+
+ return ret;
+}
diff --git a/src/common/test-webserver.c b/src/common/test-webserver.c
new file mode 100644
index 0000000..9cdd708
--- /dev/null
+++ b/src/common/test-webserver.c
@@ -0,0 +1,1184 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitwebserver.h"
+#include "cockpitwebresponse.h"
+
+#include "common/cockpitsystem.h"
+#include "testlib/cockpittest.h"
+
+#include "websocket/websocket.h"
+#include "websocket/websocketprivate.h"
+
+#include <string.h>
+
+typedef struct {
+ CockpitWebServer *web_server;
+ gchar *localport;
+ gchar *hostport;
+
+ const gchar *expected_protocol;
+ gchar *expected_remote;
+} Fixture;
+
+typedef struct {
+ gboolean use_cert;
+ gboolean local_only;
+ gboolean inet_only;
+ CockpitWebServerFlags server_flags;
+ const gchar *expected_protocol;
+ const gchar *expected_remote;
+
+ const gchar *forwarded_for_header;
+ const gchar *protocol_header;
+ const gchar *extra_headers;
+} TestCase;
+
+#define SKIP_NO_HOSTPORT if (!fixture->hostport) { g_test_skip ("No non-loopback network interface available"); return; }
+
+static gboolean
+verify_request (CockpitWebServer *web_server,
+ CockpitWebRequest *request,
+ gpointer user_data)
+{
+ Fixture *fixture = user_data;
+
+ g_assert_cmpstr (cockpit_web_request_get_protocol (request), ==, fixture->expected_protocol);
+
+ g_autofree gchar *remote_address = cockpit_web_request_get_remote_address (request);
+ if (fixture->expected_remote)
+ g_assert_cmpstr (remote_address, ==, fixture->expected_remote);
+
+ /* We didn't handle this. Keep going. */
+ return FALSE;
+}
+
+static void
+fixture_setup (Fixture *fixture,
+ const TestCase *test_case)
+{
+ GTlsCertificate *cert = NULL;
+ GError *error = NULL;
+ GInetAddress *inet;
+ gchar *str = NULL;
+ gint port;
+
+ inet = cockpit_test_find_non_loopback_address ();
+ /* this can fail in environments with only localhost */
+ if (inet != NULL)
+ str = g_inet_address_to_string (inet);
+
+ if (test_case->use_cert)
+ {
+ cert = g_tls_certificate_new_from_file (SRCDIR "/src/ws/mock-combined.crt", &error);
+ g_assert_no_error (error);
+
+ /* don't require system SSL cert database in build environments */
+ cockpit_expect_possible_log ("GLib-Net", G_LOG_LEVEL_WARNING, "couldn't load TLS file database: * No such file or directory");
+ }
+
+ gchar *address;
+ if (test_case->local_only)
+ address = "127.0.0.1";
+ else if (test_case->inet_only)
+ address = str;
+ else
+ address = NULL;
+
+ fixture->web_server = cockpit_web_server_new (cert, test_case->server_flags);
+ g_clear_object (&cert);
+
+ if (test_case && test_case->forwarded_for_header)
+ cockpit_web_server_set_forwarded_for_header (fixture->web_server, test_case->forwarded_for_header);
+ if (test_case && test_case->protocol_header)
+ cockpit_web_server_set_protocol_header (fixture->web_server, test_case->protocol_header);
+
+ /* We want to check all incoming requests to ensure that they match
+ * our expectations about remote hostname and protocol. Add a
+ * "handler" that does that, but never claims to handle anything.
+ */
+ if (test_case && test_case->expected_remote)
+ fixture->expected_remote = g_strdup (test_case->expected_remote);
+ else
+ fixture->expected_remote = g_strdup (address);
+ if (test_case && test_case->expected_protocol)
+ fixture->expected_protocol = test_case->expected_protocol;
+ else
+ fixture->expected_protocol = "http";
+ g_signal_connect (fixture->web_server, "handle-stream", G_CALLBACK (verify_request), fixture);
+
+ port = cockpit_web_server_add_inet_listener (fixture->web_server, address, 0, &error);
+ g_assert_no_error (error);
+ g_assert (port != 0);
+
+ cockpit_web_server_start (fixture->web_server);
+
+ /* HACK: this should be "localhost", but this fails on COPR; https://github.com/cockpit-project/cockpit/issues/12423 */
+ fixture->localport = g_strdup_printf ("127.0.0.1:%d", port);
+ if (str)
+ fixture->hostport = g_strdup_printf ("[%s]:%d", str, port);
+ if (inet)
+ g_object_unref (inet);
+ g_free (str);
+}
+
+static void
+fixture_teardown (Fixture *fixture,
+ const TestCase *test_case)
+{
+ cockpit_assert_expected ();
+
+ /* Verifies that we're not leaking the web server */
+ g_object_add_weak_pointer (G_OBJECT (fixture->web_server), (gpointer *)&fixture->web_server);
+ g_object_unref (fixture->web_server);
+ g_assert (fixture->web_server == NULL);
+
+ g_free (fixture->expected_remote);
+ g_free (fixture->localport);
+ g_free (fixture->hostport);
+}
+
+static void
+test_table (void)
+{
+ GHashTable *table;
+
+ table = cockpit_web_server_new_table ();
+
+ /* Case insensitive keys */
+ g_hash_table_insert (table, g_strdup ("Blah"), g_strdup ("value"));
+ g_hash_table_insert (table, g_strdup ("blah"), g_strdup ("another"));
+ g_hash_table_insert (table, g_strdup ("Different"), g_strdup ("One"));
+
+ g_assert_cmpstr (g_hash_table_lookup (table, "BLAH"), ==, "another");
+ g_assert_cmpstr (g_hash_table_lookup (table, "differeNT"), ==, "One");
+
+ g_hash_table_destroy (table);
+}
+
+static void
+test_cookie_simple (void)
+{
+ GHashTable *table = cockpit_web_server_new_table ();
+ gchar *result;
+
+ g_hash_table_insert (table, g_strdup ("Cookie"), g_strdup ("cookie1=value"));
+
+ result = cockpit_web_server_parse_cookie (table, "cookie1");
+ g_assert_cmpstr (result, ==, "value");
+
+ g_free (result);
+ g_hash_table_unref (table);
+}
+
+static void
+test_cookie_multiple (void)
+{
+ GHashTable *table = cockpit_web_server_new_table ();
+ gchar *result;
+
+ g_hash_table_insert (table, g_strdup ("Cookie"), g_strdup ("cookie1=value;cookie2=value2; cookie23=value3"));
+
+ result = cockpit_web_server_parse_cookie (table, "cookie1");
+ g_assert_cmpstr (result, ==, "value");
+ g_free (result);
+
+ result = cockpit_web_server_parse_cookie (table, "cookie2");
+ g_assert_cmpstr (result, ==, "value2");
+ g_free (result);
+
+ result = cockpit_web_server_parse_cookie (table, "cookie23");
+ g_assert_cmpstr (result, ==, "value3");
+ g_free (result);
+
+ g_hash_table_unref (table);
+}
+
+static void
+test_cookie_overlap (void)
+{
+ GHashTable *table = cockpit_web_server_new_table ();
+ gchar *result;
+
+ g_hash_table_insert (table, g_strdup ("Cookie"), g_strdup ("cookie1cookie1cookie1=value;cookie1=cookie23-value2; cookie2=a value for cookie23=inline; cookie23=value3"));
+
+ result = cockpit_web_server_parse_cookie (table, "cookie1cookie1cookie1");
+ g_assert_cmpstr (result, ==, "value");
+ g_free (result);
+
+ result = cockpit_web_server_parse_cookie (table, "cookie1");
+ g_assert_cmpstr (result, ==, "cookie23-value2");
+ g_free (result);
+
+ result = cockpit_web_server_parse_cookie (table, "cookie2");
+ g_assert_cmpstr (result, ==, "a value for cookie23=inline");
+ g_free (result);
+
+ result = cockpit_web_server_parse_cookie (table, "cookie23");
+ g_assert_cmpstr (result, ==, "value3");
+ g_free (result);
+
+ g_hash_table_unref (table);
+}
+
+static void
+test_cookie_no_header (void)
+{
+ GHashTable *table = cockpit_web_server_new_table ();
+ gchar *result;
+
+ result = cockpit_web_server_parse_cookie (table, "cookie2");
+ g_assert_cmpstr (result, ==, NULL);
+
+ g_hash_table_unref (table);
+}
+
+static void
+test_cookie_substring (void)
+{
+ GHashTable *table = cockpit_web_server_new_table ();
+ gchar *result;
+
+ g_hash_table_insert (table, g_strdup ("Cookie"), g_strdup ("cookie1=value; cookie2=value2; cookie23=value3"));
+
+ result = cockpit_web_server_parse_cookie (table, "okie2");
+ g_assert_cmpstr (result, ==, NULL);
+
+ result = cockpit_web_server_parse_cookie (table, "cookie");
+ g_assert_cmpstr (result, ==, NULL);
+
+ result = cockpit_web_server_parse_cookie (table, "ook");
+ g_assert_cmpstr (result, ==, NULL);
+
+ g_hash_table_unref (table);
+}
+
+static void
+test_cookie_decode (void)
+{
+ GHashTable *table = cockpit_web_server_new_table ();
+ gchar *result;
+
+ g_hash_table_insert (table, g_strdup ("Cookie"), g_strdup ("cookie1=val%20ue"));
+
+ result = cockpit_web_server_parse_cookie (table, "cookie1");
+ g_assert_cmpstr (result, ==, "val ue");
+ g_free (result);
+
+ g_hash_table_unref (table);
+}
+
+static void
+test_cookie_decode_bad (void)
+{
+ GHashTable *table = cockpit_web_server_new_table ();
+ gchar *result;
+
+ g_hash_table_insert (table, g_strdup ("Cookie"), g_strdup ("cookie1=val%"));
+
+ result = cockpit_web_server_parse_cookie (table, "cookie1");
+ g_assert_cmpstr (result, ==, NULL);
+
+ g_hash_table_unref (table);
+}
+
+static void
+test_accept_list_simple (void)
+{
+ gchar **result;
+ gchar *string;
+
+ result = cockpit_web_server_parse_accept_list ("en-us,en, de", NULL);
+ g_assert (result != NULL);
+
+ string = g_strjoinv (", ", result);
+ g_assert_cmpstr (string, ==, "en-us, en, de, en");
+
+ g_free (string);
+ g_strfreev (result);
+}
+
+static void
+test_accept_list_cookie (void)
+{
+ gchar **result;
+ gchar *string;
+
+ result = cockpit_web_server_parse_accept_list ("en-us,en, de", "pig");
+ g_assert (result != NULL);
+
+ string = g_strjoinv (", ", result);
+ g_assert_cmpstr (string, ==, "en-us, en, de, pig, en");
+
+ g_free (string);
+ g_strfreev (result);
+}
+
+static void
+test_accept_list_no_header (void)
+{
+ gchar **result;
+
+ result = cockpit_web_server_parse_accept_list (NULL, NULL);
+ g_assert (result != NULL);
+ g_assert (result[0] == NULL);
+
+ g_strfreev (result);
+}
+
+static void
+test_accept_list_order (void)
+{
+ gchar **result;
+ gchar *string;
+
+ result = cockpit_web_server_parse_accept_list ("de;q=xx, en-us;q=0.1,en;q=1,in;q=5", NULL);
+ g_assert (result != NULL);
+
+ string = g_strjoinv (", ", result);
+ g_assert_cmpstr (string, ==, "in, en, en-us, en");
+
+ g_free (string);
+ g_strfreev (result);
+}
+
+static void
+on_ready_get_result (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **retval = user_data;
+ g_assert (retval && *retval == NULL);
+ *retval = g_object_ref (result);
+}
+
+static gchar *
+perform_request (const gchar *hostport,
+ const gchar *request,
+ gsize *length,
+ gboolean tls)
+{
+ GSocketConnectable *connectable;
+ GSocketClient *client;
+ GSocketConnection *conn;
+ GAsyncResult *result;
+ GIOStream *tls_conn = NULL;
+ GInputStream *input;
+ GOutputStream *output;
+ GError *error = NULL;
+ GString *reply;
+ gsize len;
+ gssize ret;
+
+ connectable = g_network_address_parse (hostport, 0, &error);
+ g_assert_no_error (error);
+
+ client = g_socket_client_new ();
+
+ result = NULL;
+ g_socket_client_connect_async (client, connectable, NULL, on_ready_get_result, &result);
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ conn = g_socket_client_connect_finish (client, result, &error);
+ g_object_unref (result);
+ g_assert_no_error (error);
+
+ if (tls)
+ {
+ tls_conn = g_tls_client_connection_new (G_IO_STREAM (conn), connectable, &error);
+ g_tls_client_connection_set_validation_flags (G_TLS_CLIENT_CONNECTION (tls_conn), 0);
+ output = g_io_stream_get_output_stream (G_IO_STREAM (tls_conn));
+ input = g_io_stream_get_input_stream (G_IO_STREAM (tls_conn));
+ }
+ else
+ {
+ output = g_io_stream_get_output_stream (G_IO_STREAM (conn));
+ input = g_io_stream_get_input_stream (G_IO_STREAM (conn));
+ }
+
+ result = NULL;
+ g_output_stream_write_all_async (output, request, strlen (request), G_PRIORITY_DEFAULT, NULL,
+ on_ready_get_result, &result);
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_output_stream_write_all_finish (output, result, NULL, &error);
+ g_object_unref (result);
+ g_assert_no_error (error);
+
+ if (tls)
+ {
+ g_output_stream_close (output, NULL, &error);
+ g_assert_no_error (error);
+ }
+
+ g_socket_shutdown (g_socket_connection_get_socket (conn), FALSE, TRUE, &error);
+ g_assert_no_error (error);
+
+ reply = g_string_new ("");
+ for (;;)
+ {
+ result = NULL;
+ len = reply->len;
+ g_string_set_size (reply, len + 1024);
+ g_input_stream_read_async (input, reply->str + len, 1024, G_PRIORITY_DEFAULT,
+ NULL, on_ready_get_result, &result);
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ ret = g_input_stream_read_finish (input, result, &error);
+ g_object_unref (result);
+ g_assert_no_error (error);
+ g_assert (ret >= 0);
+ g_string_set_size (reply, len + ret);
+ if (ret == 0)
+ break;
+ }
+
+ if (tls)
+ g_object_unref (tls_conn);
+ g_object_unref (conn);
+ g_object_unref (client);
+ g_object_unref (connectable);
+
+ if (length)
+ *length = reply->len;
+ return g_string_free (reply, FALSE);
+}
+
+static gchar *
+perform_http_request (const gchar *hostport,
+ const gchar *request,
+ gsize *length)
+{
+ return perform_request (hostport, request, length, FALSE);
+}
+
+static gchar *
+perform_https_request (const gchar *hostport,
+ const gchar *request,
+ gsize *length)
+{
+ return perform_request (hostport, request, length, TRUE);
+}
+
+static gboolean
+on_shell_index_html (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ gpointer user_data)
+{
+ GBytes *bytes;
+ const gchar *data;
+
+ g_assert_cmpstr (path, ==, "/shell/index.html");
+ data = "<!DOCTYPE html><html><body>index.html</body></html>";
+ bytes = g_bytes_new_static (data, strlen (data));
+
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+ return TRUE;
+}
+
+static void
+test_with_query_string (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gchar *resp;
+ gsize length;
+
+ g_signal_connect (fixture->web_server, "handle-resource", G_CALLBACK (on_shell_index_html), NULL);
+ resp = perform_http_request (fixture->localport, "GET /shell/index.html?blah HTTP/1.0\r\nHost:test\r\n\r\n", &length);
+ g_assert (resp != NULL);
+ g_assert_cmpuint (length, >, 0);
+
+ cockpit_assert_strmatch (resp, "HTTP/* 200 *\r\nContent-Length: *\r\n\r\n<!DOCTYPE html>*");
+ g_free (resp);
+}
+
+static void
+test_webserver_not_found (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gchar *resp;
+ gsize length;
+ guint status;
+ gssize off;
+
+ resp = perform_http_request (fixture->localport, "GET /non-existent HTTP/1.0\r\nHost:test\r\n\r\n", &length);
+ g_assert (resp != NULL);
+ g_assert_cmpuint (length, >, 0);
+
+ off = web_socket_util_parse_status_line (resp, length, NULL, &status, NULL);
+ g_assert_cmpuint (off, >, 0);
+ g_assert_cmpint (status, ==, 404);
+
+ g_free (resp);
+}
+
+static void
+test_webserver_tls (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gchar *resp;
+ gsize length;
+
+ g_signal_connect (fixture->web_server, "handle-resource", G_CALLBACK (on_shell_index_html), NULL);
+ resp = perform_https_request (fixture->localport, "GET /shell/index.html HTTP/1.0\r\nHost:test\r\n\r\n", &length);
+ g_assert (resp != NULL);
+ g_assert_cmpuint (length, >, 0);
+
+ cockpit_assert_strmatch (resp, "HTTP/* 200 *\r\nContent-Length: *\r\n\r\n<!DOCTYPE html>*");
+ g_free (resp);
+}
+
+static gboolean
+on_big_header (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ gpointer user_data)
+{
+ GBytes *bytes;
+ const gchar *big_header;
+
+ big_header = g_hash_table_lookup (headers, "BigHeader");
+ g_assert (big_header);
+ g_assert_cmpint (strlen (big_header), ==, 7000);
+ g_assert_cmpint (big_header[strlen (big_header) - 1], ==, '1');
+
+ bytes = g_bytes_new_static ("OK", 2);
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+ return TRUE;
+}
+
+static void
+test_webserver_tls_big_header (Fixture *fixture,
+ const TestCase *test_case)
+{
+ g_autofree gchar *req = NULL;
+ g_autofree gchar *resp = NULL;
+ gsize length;
+
+ /* max request size is 8KiB (2 * cockpit_webserver_request_maximum), stay slightly below that */
+ req = g_strdup_printf ("GET /test HTTP/1.0\r\nHost:test\r\nBigHeader: %07000i\r\n\r\n", 1);
+
+ g_signal_connect (fixture->web_server, "handle-resource", G_CALLBACK (on_big_header), NULL);
+ resp = perform_https_request (fixture->localport, req, &length);
+ g_assert (resp != NULL);
+ g_assert_cmpuint (length, >, 0);
+
+ cockpit_assert_strmatch (resp, "HTTP/* 200 *\r\nContent-Length: 2\r\n*\r\n\r\nOK");
+}
+
+static void
+test_webserver_tls_request_too_large (Fixture *fixture,
+ const TestCase *test_case)
+{
+ g_autofree gchar *req = NULL;
+ g_autofree gchar *resp = NULL;
+ gsize length;
+
+ /* request bigger than 16 KiB should be rejected */
+ /* FIXME: This really should be 8 KiB, but due to pipelining we reserve twice
+ * that amount in the buffer */
+ cockpit_expect_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE, "received HTTP request that was too large");
+ req = g_strdup_printf ("GET /test HTTP/1.0\r\nHost:test\r\nBigHeader: %016500i\r\n\r\n", 1);
+ resp = perform_https_request (fixture->localport, req, &length);
+ g_assert (resp != NULL);
+ g_assert_cmpuint (length, ==, 0);
+ g_assert_cmpstr (resp, ==, "");
+}
+
+static void
+test_webserver_redirect_notls (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gchar *resp;
+
+ SKIP_NO_HOSTPORT;
+
+ g_assert (cockpit_web_server_get_flags (fixture->web_server) == COCKPIT_WEB_SERVER_REDIRECT_TLS);
+
+ g_signal_connect (fixture->web_server, "handle-resource", G_CALLBACK (on_shell_index_html), NULL);
+ resp = perform_http_request (fixture->hostport, "GET /shell/index.html HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ cockpit_assert_strmatch (resp, "HTTP/* 301 *\r\nLocation: https://*");
+ g_free (resp);
+}
+
+static void
+test_webserver_noredirect_localhost (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gchar *resp;
+
+ g_assert (cockpit_web_server_get_flags (fixture->web_server) == COCKPIT_WEB_SERVER_REDIRECT_TLS);
+
+ g_signal_connect (fixture->web_server, "handle-resource", G_CALLBACK (on_shell_index_html), NULL);
+ resp = perform_http_request (fixture->localport, "GET /shell/index.html HTTP/1.0\r\nHost: localhost\r\n\r\n", NULL);
+ cockpit_assert_strmatch (resp, "HTTP/* 200 *\r\n*");
+ g_free (resp);
+}
+
+static void
+test_webserver_noredirect_exception (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gchar *resp;
+
+ SKIP_NO_HOSTPORT;
+
+ g_object_set (fixture->web_server, "ssl-exception-prefix", "/shell", NULL);
+ g_signal_connect (fixture->web_server, "handle-resource", G_CALLBACK (on_shell_index_html), NULL);
+ resp = perform_http_request (fixture->hostport, "GET /shell/index.html HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ cockpit_assert_strmatch (resp, "HTTP/* 200 *\r\n*");
+ g_free (resp);
+}
+
+static void
+test_webserver_noredirect_override (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gchar *resp;
+
+ SKIP_NO_HOSTPORT;
+
+ g_signal_connect (fixture->web_server, "handle-resource", G_CALLBACK (on_shell_index_html), NULL);
+ resp = perform_http_request (fixture->hostport, "GET /shell/index.html HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ cockpit_assert_strmatch (resp, "HTTP/* 200 *\r\n*");
+ g_free (resp);
+}
+
+static gboolean
+on_oh_resource (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ const gchar **invoked)
+{
+ gchar *data;
+ GBytes *bytes;
+
+ g_assert (*invoked == NULL);
+ *invoked = "oh";
+
+ data = g_strdup_printf ("Scruffy says: %s", path);
+ bytes = g_bytes_new_take (data, strlen (data));
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+ return TRUE;
+}
+
+static gboolean
+on_scruffy_resource (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ const gchar **invoked)
+{
+ const gchar *data;
+ GBytes *bytes;
+
+ g_assert (*invoked == NULL);
+ *invoked = "scruffy";
+
+ data = "Scruffy is here";
+ bytes = g_bytes_new_static (data, strlen (data));
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+ return TRUE;
+}
+
+static gboolean
+on_index_resource (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ const gchar **invoked)
+{
+ const gchar *data;
+ GBytes *bytes;
+
+ g_assert (*invoked == NULL);
+ *invoked = "index";
+
+ data = "Yello from index";
+ bytes = g_bytes_new_static (data, strlen (data));
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+ return TRUE;
+}
+
+static gboolean
+on_default_resource (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ const gchar **invoked)
+{
+ GBytes *bytes;
+
+ g_assert (*invoked == NULL);
+ *invoked = "default";
+
+ bytes = g_bytes_new_static ("default", 7);
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+ return TRUE;
+}
+
+
+static void
+test_handle_resource (Fixture *fixture,
+ const TestCase *test_case)
+{
+ const gchar *invoked = NULL;
+ gchar *resp;
+
+ g_signal_connect (fixture->web_server, "handle-resource::/oh/",
+ G_CALLBACK (on_oh_resource), &invoked);
+ g_signal_connect (fixture->web_server, "handle-resource::/scruffy",
+ G_CALLBACK (on_scruffy_resource), &invoked);
+ g_signal_connect (fixture->web_server, "handle-resource::/",
+ G_CALLBACK (on_index_resource), &invoked);
+ g_signal_connect (fixture->web_server, "handle-resource",
+ G_CALLBACK (on_default_resource), &invoked);
+
+ /* Should call the /oh/ handler */
+ resp = perform_http_request (fixture->localport, "GET /oh/marmalade HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ g_assert_cmpstr (invoked, ==, "oh");
+ invoked = NULL;
+ cockpit_assert_strmatch (resp, "*Scruffy says: /oh/marmalade");
+ g_free (resp);
+
+ /* Should call the /oh/ handler */
+ resp = perform_http_request (fixture->localport, "GET /oh/ HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ g_assert_cmpstr (invoked, ==, "oh");
+ cockpit_assert_strmatch (resp, "*Scruffy says: /oh/");
+ invoked = NULL;
+ g_free (resp);
+
+ /* Should call the default handler */
+ g_free (perform_http_request (fixture->localport, "GET /oh HTTP/1.0\r\nHost:test\r\n\r\n", NULL));
+ g_assert_cmpstr (invoked, ==, "default");
+ invoked = NULL;
+
+ /* Should call the scruffy handler */
+ resp = perform_http_request (fixture->localport, "GET /scruffy HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ g_assert_cmpstr (invoked, ==, "scruffy");
+ invoked = NULL;
+ cockpit_assert_strmatch (resp, "*Scruffy is here");
+ g_free (resp);
+
+ /* Should call the default handler */
+ g_free (perform_http_request (fixture->localport, "GET /scruffy/blah HTTP/1.0\r\nHost:test\r\n\r\n", NULL));
+ g_assert_cmpstr (invoked, ==, "default");
+ invoked = NULL;
+
+ /* Should call the index handler */
+ resp = perform_http_request (fixture->localport, "GET / HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ g_assert_cmpstr (invoked, ==, "index");
+ invoked = NULL;
+ cockpit_assert_strmatch (resp, "*Yello from index");
+ g_free (resp);
+
+ /* Should call the default handler */
+ g_free (perform_http_request (fixture->localport, "GET /oooo HTTP/1.0\r\nHost:test\r\n\r\n", NULL));
+ g_assert_cmpstr (invoked, ==, "default");
+ invoked = NULL;
+}
+
+static void
+test_webserver_host_header (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gsize length;
+ guint status;
+ gssize off;
+ gchar *resp;
+
+ cockpit_expect_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE, "received HTTP request without Host header");
+ resp = perform_http_request (fixture->localport, "GET /index.html HTTP/1.0\r\n\r\n", &length);
+ g_assert (resp != NULL);
+ g_assert_cmpuint (length, >, 0);
+
+ off = web_socket_util_parse_status_line (resp, length, NULL, &status, NULL);
+ g_assert_cmpuint (off, >, 0);
+ g_assert_cmpint (status, ==, 400);
+
+ g_free (resp);
+}
+
+static void
+test_url_root (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gchar *url_root = NULL;
+
+ g_object_get (fixture->web_server, "url-root", &url_root, NULL);
+ g_assert (url_root == NULL);
+
+ g_object_set (fixture->web_server, "url-root", "/", NULL);
+ g_object_get (fixture->web_server, "url-root", &url_root, NULL);
+ g_assert (url_root == NULL);
+
+ g_object_set (fixture->web_server, "url-root", "/path/", NULL);
+ g_object_get (fixture->web_server, "url-root", &url_root, NULL);
+ g_assert_cmpstr (url_root, ==, "/path");
+ g_free (url_root);
+ url_root = NULL;
+
+ g_object_set (fixture->web_server, "url-root", "//path//", NULL);
+ g_object_get (fixture->web_server, "url-root", &url_root, NULL);
+ g_assert_cmpstr (url_root, ==, "/path");
+ g_free (url_root);
+ url_root = NULL;
+
+ g_object_set (fixture->web_server, "url-root", "path/", NULL);
+ g_object_get (fixture->web_server, "url-root", &url_root, NULL);
+ g_assert_cmpstr (url_root, ==, "/path");
+ g_free (url_root);
+ url_root = NULL;
+
+ g_object_set (fixture->web_server, "url-root", "path", NULL);
+ g_object_get (fixture->web_server, "url-root", &url_root, NULL);
+ g_assert_cmpstr (url_root, ==, "/path");
+ g_free (url_root);
+ url_root = NULL;
+}
+
+
+static void
+test_handle_resource_url_root (Fixture *fixture,
+ const TestCase *test_case)
+{
+ const gchar *invoked = NULL;
+ gchar *resp;
+
+ g_object_set (fixture->web_server, "url-root", "/path/", NULL);
+
+ g_signal_connect (fixture->web_server, "handle-resource::/oh/",
+ G_CALLBACK (on_oh_resource), &invoked);
+ g_signal_connect (fixture->web_server, "handle-resource::/scruffy",
+ G_CALLBACK (on_scruffy_resource), &invoked);
+ g_signal_connect (fixture->web_server, "handle-resource::/",
+ G_CALLBACK (on_index_resource), &invoked);
+ g_signal_connect (fixture->web_server, "handle-resource",
+ G_CALLBACK (on_default_resource), &invoked);
+
+ /* Should call the /oh/ handler */
+ resp = perform_http_request (fixture->localport, "GET /path/oh/marmalade HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ g_assert_cmpstr (invoked, ==, "oh");
+ invoked = NULL;
+ cockpit_assert_strmatch (resp, "*Scruffy says: /oh/marmalade");
+ g_free (resp);
+
+ /* Should call the /oh/ handler */
+ resp = perform_http_request (fixture->localport, "GET /path/oh/ HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ g_assert_cmpstr (invoked, ==, "oh");
+ cockpit_assert_strmatch (resp, "*Scruffy says: /oh/");
+ invoked = NULL;
+ g_free (resp);
+
+ /* Should call the default handler */
+ g_free (perform_http_request (fixture->localport, "GET /path/oh HTTP/1.0\r\nHost:test\r\n\r\n", NULL));
+ g_assert_cmpstr (invoked, ==, "default");
+ invoked = NULL;
+
+ /* Should call the scruffy handler */
+ resp = perform_http_request (fixture->localport, "GET /path/scruffy HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ g_assert_cmpstr (invoked, ==, "scruffy");
+ invoked = NULL;
+ cockpit_assert_strmatch (resp, "*Scruffy is here");
+ g_free (resp);
+
+ /* Should call the default handler */
+ g_free (perform_http_request (fixture->localport, "GET /path/scruffy/blah HTTP/1.0\r\nHost:test\r\n\r\n", NULL));
+ g_assert_cmpstr (invoked, ==, "default");
+ invoked = NULL;
+
+ /* Should call the index handler */
+ resp = perform_http_request (fixture->localport, "GET /path/ HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ g_assert_cmpstr (invoked, ==, "index");
+ invoked = NULL;
+ cockpit_assert_strmatch (resp, "*Yello from index");
+ g_free (resp);
+
+ /* Should call the default handler */
+ g_free (perform_http_request (fixture->localport, "GET /path/oooo HTTP/1.0\r\nHost:test\r\n\r\n", NULL));
+ g_assert_cmpstr (invoked, ==, "default");
+ invoked = NULL;
+
+ /* Should fail */
+ if (fixture->hostport)
+ {
+ resp = perform_http_request (fixture->hostport, "GET /oooo HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ cockpit_assert_strmatch (resp, "HTTP/* 404 *\r\n");
+ g_free (resp);
+ g_assert (invoked == NULL);
+ }
+}
+
+static void
+assert_cannot_connect (const gchar *hostport)
+{
+ GSocketClient *client;
+ GSocketConnection *conn;
+ GAsyncResult *result;
+ GError *error = NULL;
+
+ client = g_socket_client_new ();
+
+ result = NULL;
+ g_socket_client_connect_to_host_async (client, hostport, 1, NULL, on_ready_get_result, &result);
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ conn = g_socket_client_connect_to_host_finish (client, result, &error);
+ g_object_unref (result);
+ g_assert_null (conn);
+ g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED);
+ g_clear_error (&error);
+ g_object_unref (client);
+}
+
+static void
+test_address (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gchar *resp = NULL;
+
+ g_signal_connect (fixture->web_server, "handle-resource", G_CALLBACK (on_shell_index_html), NULL);
+ if (test_case->local_only)
+ {
+ resp = perform_http_request (fixture->localport, "GET /shell/index.html HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ cockpit_assert_strmatch (resp, "HTTP/* 200 *\r\n*");
+ g_free (resp);
+ resp = NULL;
+ }
+ else
+ {
+ /* If there is only one interface, then cockpit_web_server_new will get a NULL address and thus do listen on loopback */
+ if (fixture->hostport)
+ assert_cannot_connect (fixture->localport);
+ }
+
+ if (fixture->hostport)
+ {
+ if (test_case->inet_only)
+ {
+ resp = perform_http_request (fixture->hostport, "GET /shell/index.html HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ cockpit_assert_strmatch (resp, "HTTP/* 200 *\r\n*");
+ g_free (resp);
+ resp = NULL;
+ }
+ else
+ {
+ assert_cannot_connect (fixture->hostport);
+ }
+ }
+}
+
+static void
+test_bad_address (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gint port;
+
+ g_autoptr(CockpitWebServer) server = cockpit_web_server_new (NULL, COCKPIT_WEB_SERVER_NONE);
+ g_autoptr(GError) error = NULL;
+ port = cockpit_web_server_add_inet_listener (server, "bad", 0, &error);
+ g_assert (port == 0);
+ cockpit_assert_error_matches (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ "Couldn't parse IP address from `bad`");
+}
+
+static void
+test_webserver_for_tls_proxy (Fixture *fixture,
+ const TestCase *test_case)
+{
+ gchar *resp;
+
+ g_assert (cockpit_web_server_get_flags (fixture->web_server) == COCKPIT_WEB_SERVER_FOR_TLS_PROXY);
+
+ g_signal_connect (fixture->web_server, "handle-resource", G_CALLBACK (on_shell_index_html), &test_case);
+ resp = perform_http_request (fixture->localport, "GET /shell/index.html HTTP/1.0\r\nHost:test\r\n\r\n", NULL);
+ cockpit_assert_strmatch (resp, "HTTP/* 200 *\r\n*");
+ g_free (resp);
+}
+
+static void
+test_webserver_with_headers (Fixture *fixture,
+ const TestCase *test_case)
+{
+ g_signal_connect (fixture->web_server, "handle-resource", G_CALLBACK (on_shell_index_html), &test_case);
+
+ g_autofree gchar *request = g_strdup_printf ("GET /shell/index.html HTTP/1.0\r\n"
+ "Host: test\r\n"
+ "%s\r\n", test_case->extra_headers ?: "");
+ g_autofree gchar *resp = perform_http_request (fixture->localport, request, NULL);
+ cockpit_assert_strmatch (resp, "HTTP/* 200 *\r\n*");
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_setenv_check ("GSETTINGS_BACKEND", "memory", TRUE);
+ cockpit_setenv_check ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE);
+ cockpit_setenv_check ("GIO_USE_VFS", "local", TRUE);
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/web-server/table", test_table);
+
+ g_test_add_func ("/web-server/cookie/simple", test_cookie_simple);
+ g_test_add_func ("/web-server/cookie/multiple", test_cookie_multiple);
+ g_test_add_func ("/web-server/cookie/overlap", test_cookie_overlap);
+ g_test_add_func ("/web-server/cookie/no-header", test_cookie_no_header);
+ g_test_add_func ("/web-server/cookie/substring", test_cookie_substring);
+ g_test_add_func ("/web-server/cookie/decode", test_cookie_decode);
+ g_test_add_func ("/web-server/cookie/decode-bad", test_cookie_decode_bad);
+
+ g_test_add_func ("/web-server/accept-list/simple", test_accept_list_simple);
+ g_test_add_func ("/web-server/accept-listlanguages/cookie", test_accept_list_cookie);
+ g_test_add_func ("/web-server/accept-list/no-header", test_accept_list_no_header);
+ g_test_add_func ("/web-server/accept-list/order", test_accept_list_order);
+
+ cockpit_test_add ("/web-server/query-string", test_with_query_string);
+ cockpit_test_add ("/web-server/host-header", test_webserver_host_header);
+ cockpit_test_add ("/web-server/not-found", test_webserver_not_found);
+
+ cockpit_test_add ("/web-server/tls", test_webserver_tls,
+ .use_cert=TRUE, .expected_protocol="https");
+ cockpit_test_add ("/web-server/tls-big-header", test_webserver_tls_big_header,
+ .use_cert=TRUE, .expected_protocol="https");
+ cockpit_test_add ("/web-server/tls-request-too-large", test_webserver_tls_request_too_large,
+ .use_cert=TRUE, .expected_protocol="https");
+
+ cockpit_test_add ("/web-server/redirect-notls", test_webserver_redirect_notls, .use_cert=TRUE,
+ .server_flags=COCKPIT_WEB_SERVER_REDIRECT_TLS);
+ cockpit_test_add ("/web-server/no-redirect-localhost", test_webserver_noredirect_localhost, .use_cert=TRUE,
+ .server_flags=COCKPIT_WEB_SERVER_REDIRECT_TLS);
+ cockpit_test_add ("/web-server/no-redirect-exception", test_webserver_noredirect_exception, .use_cert=TRUE,
+ .server_flags=COCKPIT_WEB_SERVER_REDIRECT_TLS);
+ cockpit_test_add ("/web-server/no-redirect-override", test_webserver_noredirect_override, .use_cert=TRUE,
+ .server_flags=COCKPIT_WEB_SERVER_NONE);
+
+ cockpit_test_add ("/web-server/handle-resource", test_handle_resource);
+
+ cockpit_test_add ("/web-server/url-root", test_url_root);
+ cockpit_test_add ("/web-server/url-root-handlers", test_handle_resource_url_root);
+
+ cockpit_test_add ("/web-server/local-address-only", test_address, .local_only=TRUE);
+ cockpit_test_add ("/web-server/inet-address-only", test_address, .inet_only=TRUE);
+ cockpit_test_add ("/web-server/bad-address", test_bad_address);
+
+ cockpit_test_add ("/web-server/for-tls-proxy", test_webserver_for_tls_proxy,
+ .local_only=TRUE, .server_flags=COCKPIT_WEB_SERVER_FOR_TLS_PROXY, .expected_protocol="https");
+
+ /* X-Forwarded-Proto */
+
+ /* Header is enabled, but not passed. Default to "http". */
+ cockpit_test_add ("/web-server/x-forwarded-proto/empty", test_webserver_with_headers,
+ .protocol_header="X-Forwarded-Proto", .expected_protocol="http");
+
+ /* Header is enabled and passed as "http". Result: "http" */
+ cockpit_test_add ("/web-server/x-forwarded-proto/http", test_webserver_with_headers,
+ .protocol_header="X-Forwarded-Proto", .extra_headers="X-Forwarded-Proto: http\r\n",
+ .expected_protocol="http");
+
+ /* Header is enabled and passed as "https". Result: "https" */
+ cockpit_test_add ("/web-server/x-forwarded-proto/https", test_webserver_with_headers,
+ .protocol_header="X-Forwarded-Proto", .extra_headers="X-Forwarded-Proto: https\r\n",
+ .expected_protocol="https");
+
+ /* Header is passed as "https", but we never enabled it, so it ought to be ignored */
+ cockpit_test_add ("/web-server/x-forwarded-proto/ignore", test_webserver_with_headers,
+ .extra_headers="X-Forwarded-Proto: https\r\n", .expected_protocol="http");
+
+ /* X-Forwarded-For */
+
+ /* Header is enabled, but not passed. */
+ cockpit_test_add ("/web-server/x-forwarded-for/empty", test_webserver_with_headers,
+ .forwarded_for_header="X-Forwarded-For",
+ .expected_remote="127.0.0.1");
+
+ /* Header enabled, and passed an IPv4 address */
+ cockpit_test_add ("/web-server/x-forwarded-for/v4", test_webserver_with_headers,
+ .forwarded_for_header="X-Forwarded-For",
+ .extra_headers="X-Forwarded-For: 1.2.3.4\r\n",
+ .expected_remote="1.2.3.4");
+
+ /* Header enabled, and passed an IPv6 address */
+ cockpit_test_add ("/web-server/x-forwarded-for/v6", test_webserver_with_headers,
+ .forwarded_for_header="X-Forwarded-For",
+ .extra_headers="X-Forwarded-For: 2001::1\r\n",
+ .expected_remote="2001::1");
+
+ /* Header enabled, and passed 'unknown' */
+ cockpit_test_add ("/web-server/x-forwarded-for/unknown", test_webserver_with_headers,
+ .forwarded_for_header="X-Forwarded-For",
+ .extra_headers="X-Forwarded-For: unknown\r\n",
+ .expected_remote = "unknown");
+
+ /* Header enabled, and passed multiple IPs */
+ cockpit_test_add ("/web-server/x-forwarded-for/multiple", test_webserver_with_headers,
+ .forwarded_for_header="X-Forwarded-For",
+ .extra_headers="X-Forwarded-For: 6.6.6.6 2.2.2.2 1.2.3.4\r\n",
+ .expected_remote="1.2.3.4");
+
+ /* Header enabled, and passed multiple IPs, and junk */
+ cockpit_test_add ("/web-server/x-forwarded-for/junk", test_webserver_with_headers,
+ .forwarded_for_header="X-Forwarded-For",
+ .extra_headers="X-Forwarded-For: !@{}\"#%^&*()<>?`~\\|'$\t $whatever; ;; ,,, 1.2.3.4\r\n",
+ .expected_remote="1.2.3.4");
+
+ /* Header enabled, and passed IP with extra space (should be stripped) */
+ cockpit_test_add ("/web-server/x-forwarded-for/extra-whitespace", test_webserver_with_headers,
+ .forwarded_for_header="X-Forwarded-For",
+ .extra_headers="X-Forwarded-For: 1.2.3.4 \r\n",
+ .expected_remote="1.2.3.4");
+
+ /* Header enabled, and passed only space */
+ cockpit_test_add ("/web-server/x-forwarded-for/only-whitespace", test_webserver_with_headers,
+ .forwarded_for_header="X-Forwarded-For",
+ .extra_headers="X-Forwarded-For: \r\n",
+ .expected_remote="127.0.0.1");
+
+ /* Header enabled, passed the header with an empty value */
+ cockpit_test_add ("/web-server/x-forwarded-for/header", test_webserver_with_headers,
+ .forwarded_for_header="X-Forwarded-For",
+ .extra_headers="X-Forwarded-For:\r\n",
+ .expected_remote="127.0.0.1");
+
+ /* We passed an IP, but the header wasn't enabled */
+ cockpit_test_add ("/web-server/x-forwarded-for/ignore", test_webserver_with_headers,
+ .extra_headers="X-Forwarded-For: 1.2.3.4\r\n",
+ .expected_remote="127.0.0.1");
+
+ return g_test_run ();
+}
diff --git a/src/pam-ssh-add/Makefile.am b/src/pam-ssh-add/Makefile.am
new file mode 100644
index 0000000..46ff254
--- /dev/null
+++ b/src/pam-ssh-add/Makefile.am
@@ -0,0 +1,39 @@
+# -----------------------------------------------------------------------------
+# libpam_ssh_add.a: code used in pam_ssh_add.so and its tests
+
+noinst_LIBRARIES += libpam_ssh_add.a
+
+libpam_ssh_add_a_CFLAGS = -fPIC $(AM_CFLAGS)
+
+libpam_ssh_add_a_LIBS = \
+ libpam_ssh_add.a \
+ -lpam \
+ $(NULL)
+
+libpam_ssh_add_a_SOURCES = \
+ src/pam-ssh-add/pam-ssh-add.c \
+ src/pam-ssh-add/pam-ssh-add.h \
+ $(NULL)
+
+# -----------------------------------------------------------------------------
+# pam_ssh_add.so
+
+pam_PROGRAMS = pam_ssh_add.so
+pam_ssh_add_so_CFLAGS = -fPIC $(AM_CFLAGS)
+pam_ssh_add_so_LDFLAGS = -shared
+pam_ssh_add_so_LDADD = $(libpam_ssh_add_a_LIBS)
+pam_ssh_add_so_SOURCES = src/pam-ssh-add/pam-ssh-add.c
+
+# -----------------------------------------------------------------------------
+# Unit tests
+
+TEST_PROGRAM += test-ssh-add
+test_ssh_add_CPPFLAGS = $(TEST_CPP)
+test_ssh_add_LDADD = $(libpam_ssh_add_a_LIBS) $(TEST_LIBS)
+test_ssh_add_SOURCES = src/pam-ssh-add/test-ssh-add.c
+
+dist_check_DATA += \
+ src/pam-ssh-add/mock-ssh-agent \
+ src/pam-ssh-add/mock-ssh-add \
+ src/pam-ssh-add/mock-environment \
+ $(NULL)
diff --git a/src/pam-ssh-add/mock-environment b/src/pam-ssh-add/mock-environment
new file mode 100755
index 0000000..43e6f70
--- /dev/null
+++ b/src/pam-ssh-add/mock-environment
@@ -0,0 +1,12 @@
+#!/bin/sh
+for key in XDG_RUNTIME_DIR HOME PATH LC_ALL OTHER SSH_AUTH_SOCK
+do
+ value=$(/usr/bin/printenv "$key")
+ result=$?
+ if [ $result -eq 0 ]; then
+ echo "$key=$value" 1>&2
+ else
+ echo "NO $key" 1>&2
+ fi
+done
+exit 1;
diff --git a/src/pam-ssh-add/mock-ssh-add b/src/pam-ssh-add/mock-ssh-add
new file mode 100755
index 0000000..1c2c841
--- /dev/null
+++ b/src/pam-ssh-add/mock-ssh-add
@@ -0,0 +1,40 @@
+#!/bin/sh
+PASSWORD="foobar"
+
+password_good=0
+password_bad=0
+password_blanks=0
+
+case "$1" in
+ "no-socket")
+ exit 2;;
+ *)
+ for dummy_key in 0 1 2
+ do
+ echo "Enter passphrase for $dummy_key" >&2
+ while true
+ do
+ read answer
+ case $answer in
+ "$PASSWORD")
+ password_good=$(expr "$password_good" + 1)
+ break;;
+ "")
+ password_blanks=$(expr "$password_blanks" + 1)
+ break;;
+ *)
+ password_bad=$(expr "$password_bad" + 1)
+ echo "Bad passphrase, try again for $dummy_key" >&2
+ continue;;
+ esac
+ done
+ done
+
+ echo "Correct password $password_good, bad password $password_bad, password_blanks $password_blanks" >&2
+
+ if [ $password_good -eq 3 ]; then
+ exit 0;
+ else
+ exit 1;
+ fi
+esac
diff --git a/src/pam-ssh-add/mock-ssh-agent b/src/pam-ssh-add/mock-ssh-agent
new file mode 100755
index 0000000..bbe1ec8
--- /dev/null
+++ b/src/pam-ssh-add/mock-ssh-agent
@@ -0,0 +1,17 @@
+#!/bin/sh
+case "$1" in
+ "good-vars")
+ echo "EXTRA=var; export EXTRA"
+ echo "SSH_AUTH_SOCKET=socket; export SSH_AUTH_SOCKET"
+ echo "SSH_AGENT_PID=100; export SSH_AGENT_PID"
+ echo "EXTRA2=100; export EXTRA2"
+ exit 0;;
+ "bad-vars")
+ echo "Just a bunch of nothing"
+ echo "TEST=var; export TEST"
+ exit 0;;
+ *)
+ echo "Normal output"
+ echo "Bad things" 1>&2
+ exit 1;;
+esac
diff --git a/src/pam-ssh-add/pam-ssh-add.c b/src/pam-ssh-add/pam-ssh-add.c
new file mode 100644
index 0000000..a9159d7
--- /dev/null
+++ b/src/pam-ssh-add/pam-ssh-add.c
@@ -0,0 +1,1004 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Inspired by gnome-keyring:
+ * Stef Walter <stef@memberwebs.com>
+ */
+
+
+#include "config.h"
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <signal.h>
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <ctype.h>
+#include <syslog.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <grp.h>
+
+#include <security/pam_modules.h>
+#include <security/pam_modutil.h>
+
+#include "pam-ssh-add.h"
+
+/* programs that can be overwidden in tests */
+const char *pam_ssh_agent_program = PATH_SSH_AGENT;
+const char *pam_ssh_agent_arg = NULL;
+
+const char *pam_ssh_add_program = PATH_SSH_ADD;
+const char *pam_ssh_add_arg = NULL;
+
+/* Environment */
+#define ENVIRON_SIZE 5
+#define PATH "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
+
+/* ssh-agent output variables we care about */
+static const char *agent_vars[] = {
+ "SSH_AUTH_SOCK",
+ "SSH_AGENT_PID",
+ NULL
+};
+
+/* pre-set file descriptors */
+#define STDIN 0
+#define STDOUT 1
+#define STDERR 2
+
+/* read & write ends of a pipe */
+#define READ_END 0
+#define WRITE_END 1
+
+/* pre-set file descriptors */
+#define STDIN 0
+#define STDOUT 1
+#define STDERR 2
+
+/* attribute for stored auth */
+#define STORED_AUTHTOK "pam_ssh_add_authtok"
+
+#ifndef debug
+#define debug(format, ...) \
+ do { if (pam_ssh_add_verbose_mode) \
+ syslog (LOG_INFO | LOG_AUTHPRIV, "pam_ssh_add: " format, ##__VA_ARGS__); \
+ } while (0)
+#endif
+
+#ifndef error
+#define error(format, ...) \
+ do { message_handler (LOG_ERR, "pam_ssh_add: " format, ##__VA_ARGS__); \
+ } while (0)
+#endif
+
+#ifndef message
+#define message(format, ...) \
+ do { message_handler (LOG_WARNING, "pam_ssh_add: " format, ##__VA_ARGS__); \
+ } while (0)
+#endif
+
+typedef int (* line_cb) (char *line, void *arg);
+int pam_ssh_add_verbose_mode = 0;
+pam_ssh_add_logger pam_ssh_add_log_handler = NULL;
+
+#ifndef message_handler
+#if __GNUC__ > 2
+static void
+message_handler (int level, const char *format, ...)
+__attribute__((__format__(__printf__, 2, 3)));
+#endif
+
+static void
+default_logger (int level, const char *str)
+{
+ if (level == LOG_INFO)
+ debug ("%s", str);
+ else if (level == LOG_ERR)
+ syslog (LOG_ERR, "%s", str);
+ else
+ syslog (LOG_WARNING, "%s", str);
+}
+
+static void
+message_handler (int level,
+ const char *format, ...)
+{
+ va_list va;
+ char *data;
+ int res;
+
+ if (!pam_ssh_add_log_handler)
+ pam_ssh_add_log_handler = &default_logger;
+
+ /* Fast path for simple messages */
+ if (!strchr (format, '%'))
+ {
+ pam_ssh_add_log_handler (level, format);
+ return;
+ }
+
+ va_start (va, format);
+ res = vasprintf (&data, format, va);
+ va_end (va);
+
+ if (res > 0)
+ pam_ssh_add_log_handler (level, data);
+
+ free (data);
+}
+#endif
+
+static void
+close_safe (int fd)
+{
+ if (fd != -1)
+ close (fd);
+}
+
+static char *
+strbtrim (char *data)
+{
+ assert (data);
+ while (*data && isspace (*data))
+ ++data;
+ return (char*)data;
+}
+
+static int
+foreach_line (char *lines,
+ line_cb cb,
+ void *arg)
+{
+ char *line, *ctx;
+ int ret = 1;
+
+ assert (lines);
+
+ /* Call cb for each line in the text block */
+ for (line = strtok_r (lines, "\n", &ctx); line != NULL;
+ line = strtok_r (NULL, "\n", &ctx))
+ {
+ ret = (cb) (line, arg);
+ if (!ret)
+ return ret;
+ }
+ return ret;
+}
+
+static char *
+read_string (int fd,
+ int consume)
+{
+ /* We only accept a max of 8K */
+ #define MAX_LENGTH 8192
+ #define BLOCK 256
+
+ char *ret = NULL;
+ int r, len = 0;
+
+ for (;;)
+ {
+ char *n = realloc (ret, len + BLOCK);
+ if (!n)
+ {
+ free (ret);
+ errno = ENOMEM;
+ return NULL;
+ }
+
+ memset (n + len, 0, BLOCK);
+ ret = n;
+
+ r = read (fd, ret + len, BLOCK-1);
+ if (r < 0)
+ {
+ if (errno == EAGAIN || errno == EINTR)
+ continue;
+
+ free (ret);
+ return NULL;
+ }
+ else
+ {
+ len = len + r;
+ }
+
+ if (r == 0 || len > MAX_LENGTH || consume == 0)
+ break;
+ }
+
+ return ret;
+}
+
+static int
+write_string (int fd,
+ const char *buf)
+{
+ size_t bytes = 0;
+ int res, len = strlen (buf);
+
+ while (bytes < len)
+ {
+ res = write (fd, buf + bytes, len - bytes);
+ if (res < 0)
+ {
+ if (errno != EINTR && errno != EAGAIN)
+ return -1;
+ }
+ else
+ {
+ bytes += res;
+ }
+ }
+
+ return 0;
+}
+
+static int
+log_problem (char *line,
+ void *arg)
+{
+ /*
+ * Called for each stderr output line from the daemon.
+ * Send it all to the log.
+ */
+
+ int *success;
+
+ assert (line);
+ assert (arg);
+
+ success = (int*)arg;
+ if (*success)
+ message ("%s", line);
+ else
+ error ("%s", line);
+
+ return 1;
+}
+
+static const char *
+get_optional_env (const char *name,
+ const char *override)
+{
+ if (override)
+ return override;
+
+ return getenv (name);
+}
+
+static int
+build_environment (char **env,
+ const char *first_key, ...)
+{
+ int i = 0;
+ int res = 0;
+ const char *key = first_key;
+ va_list va;
+
+ va_start (va, first_key);
+
+ while (key != NULL)
+ {
+ const char *value = va_arg (va, char*);
+ if (value != NULL)
+ {
+ if (asprintf (env + (i++), "%s=%s", key, value) < 0)
+ {
+ error ("couldn't allocate environment");
+ goto out;
+ }
+ }
+ key = va_arg (va, char*);
+ }
+ res = 1;
+
+out:
+ va_end (va);
+ return res;
+}
+
+static void
+setup_child (pam_handle_t *pamh,
+ const char **args,
+ char **env,
+ struct passwd *pwd,
+ int inp[2],
+ int outp[2],
+ int errp[2])
+{
+ assert (pwd);
+ assert (pwd->pw_dir);
+
+ /* Fix up our end of the pipes */
+ if (dup2 (inp[READ_END], STDIN) < 0 ||
+ dup2 (outp[WRITE_END], STDOUT) < 0 ||
+ dup2 (errp[WRITE_END], STDERR) < 0)
+ {
+ error ("couldn't setup pipes: %m");
+ exit (EXIT_FAILURE);
+ }
+
+ pam_modutil_sanitize_helper_fds (pamh,
+ PAM_MODUTIL_IGNORE_FD,
+ PAM_MODUTIL_IGNORE_FD,
+ PAM_MODUTIL_IGNORE_FD);
+
+ /* Close unnecessary file descriptors */
+ close (inp[READ_END]);
+ close (inp[WRITE_END]);
+ close (outp[READ_END]);
+ close (outp[WRITE_END]);
+ close (errp[READ_END]);
+ close (errp[WRITE_END]);
+
+ /* Start a new session, to detach from tty */
+ if (setsid() < 0)
+ {
+ error ("failed to detach child process");
+ exit (EXIT_FAILURE);
+ }
+
+ /* We may be running effective as another user, revert that */
+ if (setegid (getgid ()) < 0 || seteuid (getuid ()) < 0)
+ error ("failed to restore credentials");
+
+ /* Setup process credentials; if we actually change the group, drop any auxiliary groups too */
+ if ((getegid() != pwd->pw_gid ? initgroups(pwd->pw_name, pwd->pw_gid) < 0 : 0) ||
+ setgid (pwd->pw_gid) < 0 || setuid (pwd->pw_uid) < 0 ||
+ setegid (pwd->pw_gid) < 0 || seteuid (pwd->pw_uid) < 0)
+ {
+ error ("couldn't setup credentials: %m");
+ exit (EXIT_FAILURE);
+ }
+
+ /* Now actually execute the process */
+ execve (args[0], (char **) args, env);
+ error ("couldn't run %s: %m", args[0]);
+ _exit (EXIT_FAILURE);
+}
+
+static void
+ignore_signals (struct sigaction *defsact,
+ struct sigaction *oldsact,
+ struct sigaction *ignpipe,
+ struct sigaction *oldpipe)
+{
+ /*
+ * Make sure that SIGCHLD occurs. Otherwise our waitpid below
+ * doesn't work properly. We need to wait on the process to
+ * get the daemon exit status.
+ */
+ memset (defsact, 0, sizeof (*defsact));
+ memset (oldsact, 0, sizeof (*oldsact));
+ defsact->sa_handler = SIG_DFL;
+ sigaction (SIGCHLD, defsact, oldsact);
+
+ /*
+ * Make sure we don't exit with a SIGPIPE while doing this, that
+ * would be very annoying to a user trying to log in.
+ */
+ memset (ignpipe, 0, sizeof (*ignpipe));
+ memset (oldpipe, 0, sizeof (*oldpipe));
+ ignpipe->sa_handler = SIG_IGN;
+ sigaction (SIGPIPE, ignpipe, oldpipe);
+}
+
+static void
+restore_signals (struct sigaction *oldsact,
+ struct sigaction *oldpipe)
+{
+ /* Restore old handler */
+ sigaction (SIGCHLD, oldsact, NULL);
+ sigaction (SIGPIPE, oldpipe, NULL);
+}
+
+static pid_t
+run_as_user (pam_handle_t *pamh,
+ const char **args,
+ char **env,
+ struct passwd *pwd,
+ int inp[2],
+ int outp[2],
+ int errp[2])
+{
+ pid_t pid = -1;
+
+ /* Start up daemon child process */
+ switch (pid = fork ())
+ {
+ case -1:
+ error ("couldn't fork: %m");
+ goto done;
+
+ /* This is the child */
+ case 0:
+ setup_child (pamh, args, env, pwd, inp, outp, errp);
+ /* Should never be reached */
+ break;
+
+ /* This is the parent */
+ default:
+ break;
+ };
+
+done:
+ return pid;
+}
+
+static int
+get_environ_vars_from_agent (char *line,
+ void *arg)
+{
+ /*
+ * ssh-agent outputs commands for exporting it's environment
+ * variables. We want to return these variables so parse
+ * them out and store them.
+ */
+
+ char *c = NULL;
+ int i;
+ int ret = 1;
+ const char sep[] = "; export";
+
+ char **ret_array = (char**)arg;
+
+ assert (line);
+ assert (arg);
+
+ line = strbtrim (line);
+ debug ("got line: %s", line);
+ c = strstr (line, sep);
+ if (c)
+ {
+ *c = '\0';
+ debug ("name/value is: %s", line);
+ for (i = 0; agent_vars[i] != NULL; i++)
+ {
+ if (strstr(line, agent_vars[i]))
+ {
+ if (asprintf (ret_array + (i), "%s", line) < 0)
+ {
+ error ("Error allocating output variable");
+ ret = 0;
+ }
+ break;
+ }
+ }
+ }
+
+ return ret;
+}
+
+int
+pam_ssh_add_load (pam_handle_t *pamh,
+ struct passwd *pwd,
+ const char *agent_socket,
+ const char *password)
+{
+ struct sigaction defsact, oldsact, ignpipe, oldpipe;
+ int i;
+ int inp[2] = { -1, -1 };
+ int outp[2] = { -1, -1 };
+ int errp[2] = { -1, -1 };
+
+ char *env[ENVIRON_SIZE] = { NULL };
+ const char *args[] = { "/bin/sh", "-c", "$0 $1",
+ pam_ssh_add_program,
+ pam_ssh_add_arg,
+ NULL };
+
+ pid_t pid;
+ int success = 0;
+ int force_stderr_debug = 1;
+
+ siginfo_t result;
+
+ ignore_signals (&defsact, &oldsact, &ignpipe, &oldpipe);
+
+ assert (pwd);
+ if (!agent_socket)
+ {
+ message ("ssh-add requires an agent socket");
+ goto done;
+ }
+
+ if (!build_environment (env,
+ "PATH", PATH,
+ "LC_ALL", "C",
+ "HOME", pwd->pw_dir,
+ "SSH_AUTH_SOCK", agent_socket,
+ NULL))
+ goto done;
+
+ /* Create the necessary pipes */
+ if (pipe (inp) < 0 || pipe (outp) < 0 || pipe (errp) < 0)
+ {
+ error ("couldn't create pipes: %m");
+ goto done;
+ }
+
+ pid = run_as_user (pamh, args, env, pwd,
+ inp, outp, errp);
+ if (pid < 1)
+ goto done;
+
+ /* in the parent, close our unneeded ends of the pipes */
+ close (inp[READ_END]);
+ close (outp[WRITE_END]);
+ close (errp[WRITE_END]);
+ inp[READ_END] = outp[WRITE_END] = errp[WRITE_END] = -1;
+ for (;;)
+ {
+ /* ssh-add asks for password on stderr */
+ char *outerr = read_string (errp[READ_END], 0);
+ if (outerr == NULL || outerr[0] == '\0')
+ {
+ free (outerr);
+ break;
+ }
+
+ if (strstr (outerr, "Enter passphrase") != NULL)
+ {
+ debug ("Got password request");
+ if (password != NULL)
+ write_string (inp[WRITE_END], password);
+ write_string (inp[WRITE_END], "\n");
+ }
+ else if (strstr (outerr, "Bad passphrase"))
+ {
+ debug ("sent bad password");
+ write_string (inp[WRITE_END], "\n");
+ }
+ else
+ {
+ foreach_line (outerr, log_problem,
+ &force_stderr_debug);
+ }
+
+ free (outerr);
+ }
+
+ /* Wait for the initial process to exit */
+ if (waitid (P_PID, pid, &result, WEXITED) < 0)
+ {
+ error ("couldn't wait on ssh-add process: %m");
+ goto done;
+ }
+
+ success = result.si_code == CLD_EXITED && result.si_status == 0;
+ /* Failure from process */
+ if (!success)
+ {
+ /* key loading failed, don't report as an error */
+ if (result.si_code == 1)
+ {
+ success = 1;
+ message ("Failed adding some keys");
+ }
+ else
+ {
+ message ("Failed adding keys: %d", result.si_status);
+ }
+ }
+
+done:
+ restore_signals (&oldsact, &oldpipe);
+
+ close_safe (inp[0]);
+ close_safe (inp[1]);
+ close_safe (outp[0]);
+ close_safe (outp[1]);
+ close_safe (errp[0]);
+ close_safe (errp[1]);
+
+ for (i = 0; env[i] != NULL; i++)
+ free (env[i]);
+
+ return success;
+}
+
+int
+pam_ssh_add_start_agent (pam_handle_t *pamh,
+ struct passwd *pwd,
+ const char *xdg_runtime_overide,
+ char **out_auth_sock_var,
+ char **out_agent_pid_var)
+{
+ char *env[ENVIRON_SIZE] = { NULL };
+ const char *xdg_runtime;
+
+ struct sigaction defsact, oldsact, ignpipe, oldpipe;
+ siginfo_t result;
+
+ int inp[2] = { -1, -1 };
+ int outp[2] = { -1, -1 };
+ int errp[2] = { -1, -1 };
+ pid_t pid;
+
+ const char *args[] = { "/bin/sh", "-c", "$0 $1",
+ pam_ssh_agent_program,
+ pam_ssh_agent_arg,
+ NULL };
+
+ char *output = NULL;
+ char *outerr = NULL;
+ int success = 0;
+ int i = 0;
+
+ char *save_vars[N_ELEMENTS (agent_vars)] = { NULL, };
+
+ assert (pwd);
+ xdg_runtime = get_optional_env ("XDG_RUNTIME_DIR",
+ xdg_runtime_overide);
+ if (!build_environment (env,
+ "PATH", PATH,
+ "LC_ALL", "C",
+ "HOME", pwd->pw_dir,
+ "XDG_RUNTIME_DIR", xdg_runtime,
+ NULL))
+ goto done;
+
+ ignore_signals (&defsact, &oldsact, &ignpipe, &oldpipe);
+ /* Create the necessary pipes */
+ if (pipe (inp) < 0 || pipe (outp) < 0 || pipe (errp) < 0)
+ {
+ error ("couldn't create pipes: %m");
+ goto done;
+ }
+
+ pid = run_as_user (pamh, args, env, pwd,
+ inp, outp, errp);
+ if (pid < 1)
+ goto done;
+
+ /* in the parent, close our unneeded ends of the pipes */
+ close (inp[READ_END]);
+ close (outp[WRITE_END]);
+ close (errp[WRITE_END]);
+ close (inp[WRITE_END]);
+
+ inp[READ_END] = outp[WRITE_END] = errp[WRITE_END] = -1;
+
+ /* Read any stdout and stderr data */
+ output = read_string (outp[READ_END], 1);
+ outerr = read_string (errp[READ_END], 0);
+ if (!output || !outerr)
+ {
+ error ("couldn't read data from ssh-agent: %m");
+ goto done;
+ }
+
+ /* Wait for the initial process to exit */
+ if (waitid (P_PID, pid, &result, WEXITED) < 0)
+ {
+ error ("couldn't wait on ssh-agent process: %m");
+ goto done;
+ }
+
+ success = result.si_code == CLD_EXITED && result.si_status == 0;
+
+ if (outerr && outerr[0])
+ foreach_line (outerr, log_problem, &success);
+
+ foreach_line (output, get_environ_vars_from_agent, save_vars);
+
+ /* Failure from process */
+ if (!success)
+ {
+ error ("Failed to start ssh-agent");
+ }
+ /* Failure to find vars */
+ else if (!save_vars[0] || !save_vars[1])
+ {
+ message ("Expected agent environment variables not found");
+ success = 0;
+ }
+
+ if (out_auth_sock_var && save_vars[0])
+ *out_auth_sock_var = strdup (save_vars[0]);
+
+ if (out_agent_pid_var && save_vars[1])
+ *out_agent_pid_var = strdup (save_vars[1]);
+
+done:
+ restore_signals (&oldsact, &oldpipe);
+
+ close_safe (inp[0]);
+ close_safe (inp[1]);
+ close_safe (outp[0]);
+ close_safe (outp[1]);
+ close_safe (errp[0]);
+ close_safe (errp[1]);
+
+ free (output);
+ free (outerr);
+
+ /* save_vars may contain NULL
+ * values use agent_vars as the
+ * marker instead
+ */
+ for (i = 0; agent_vars[i] != NULL; i++)
+ free (save_vars[i]);
+
+ for (i = 0; env[i] != NULL; i++)
+ free (env[i]);
+
+ return success;
+}
+
+/* --------------------------------------------------------------------------------
+ * PAM Module
+ */
+
+static void
+parse_args (int argc,
+ const char **argv)
+{
+ int i;
+
+ pam_ssh_add_verbose_mode = 0;
+
+ /* Parse the arguments */
+ for (i = 0; i < argc; i++)
+ {
+ if (strcmp (argv[i], "debug") == 0)
+ {
+ pam_ssh_add_verbose_mode = 1;
+ }
+ else
+ {
+ message ("invalid option: %s", argv[i]);
+ continue;
+ }
+ }
+}
+
+static void
+free_password (char *password)
+{
+ volatile char *vp;
+ size_t len;
+
+ if (!password)
+ return;
+
+ /* Defeats some optimizations */
+ len = strlen (password);
+ memset (password, 0xAA, len);
+ memset (password, 0xBB, len);
+
+ /* Defeats others */
+ vp = (volatile char*)password;
+ while (*vp)
+ *(vp++) = 0xAA;
+
+ free (password);
+}
+
+static void
+cleanup_free_password (pam_handle_t *pamh,
+ void *data,
+ int pam_end_status)
+{
+ free_password (data);
+}
+
+static char *
+strdupx (const char *string)
+{
+ char *copy = strdup (string);
+ if (copy != NULL)
+ return copy;
+
+ warn ("failed to allocate memory for strdup");
+ abort ();
+}
+
+static int
+stash_password_for_session (pam_handle_t *pamh,
+ const char *password)
+{
+ char *password_copy = strdupx (password);
+ if (pam_set_data (pamh, STORED_AUTHTOK, password_copy,
+ cleanup_free_password) != PAM_SUCCESS)
+ {
+ free_password (password_copy);
+ message ("error stashing password for session");
+ return PAM_AUTHTOK_RECOVER_ERR;
+ }
+
+ /* coverity[leaked_storage : FALSE] */
+ return PAM_SUCCESS;
+}
+
+static int
+start_agent (pam_handle_t *pamh,
+ struct passwd *auth_pwd)
+{
+ char *auth_socket = NULL;
+ char *auth_pid = NULL;
+ int success = 0;
+ int res;
+
+ success = pam_ssh_add_start_agent (pamh, auth_pwd,
+ pam_getenv (pamh, "XDG_RUNTIME_DIR"),
+ &auth_socket,
+ &auth_pid);
+
+ /* Store pid and socket environment vars */
+ if (!success || !auth_socket || !auth_pid)
+ {
+ res = PAM_SERVICE_ERR;
+ }
+ else
+ {
+ res = pam_putenv (pamh, auth_socket);
+ if (res == PAM_SUCCESS)
+ res = pam_putenv (pamh, auth_pid);
+
+ if (res != PAM_SUCCESS)
+ {
+ error ("couldn't set agent environment: %s",
+ pam_strerror (pamh, res));
+ }
+ }
+
+ free (auth_socket);
+ free (auth_pid);
+
+ return res;
+}
+
+static int
+load_keys (pam_handle_t *pamh,
+ struct passwd *auth_pwd)
+{
+ const char *password;
+ int success = 0;
+
+ /* Get the stored authtok here */
+ if (pam_get_data (pamh, STORED_AUTHTOK,
+ (const void**)&password) != PAM_SUCCESS)
+ {
+ password = NULL;
+ }
+
+ success = pam_ssh_add_load (pamh, auth_pwd,
+ pam_getenv (pamh, "SSH_AUTH_SOCK"),
+ password);
+
+ return success ? PAM_SUCCESS : PAM_SERVICE_ERR;
+}
+
+PAM_EXTERN int
+pam_sm_open_session (pam_handle_t *pamh,
+ int flags,
+ int argc,
+ const char *argv[])
+{
+ int res;
+ int o_res;
+
+ struct passwd *auth_pwd;
+ const char *user;
+
+ parse_args (argc, argv);
+
+ /* Lookup the user */
+ res = pam_get_user (pamh, &user, NULL);
+ if (res != PAM_SUCCESS)
+ {
+ message ("couldn't get pam user: %s", pam_strerror (pamh, res));
+ goto out;
+ }
+
+ auth_pwd = getpwnam (user);
+ if (!auth_pwd)
+ {
+ error ("error looking up user information");
+ res = PAM_SERVICE_ERR;
+ goto out;
+ }
+
+ res = start_agent (pamh, auth_pwd);
+
+ if (res == PAM_SUCCESS)
+ res = load_keys (pamh, auth_pwd);
+
+out:
+ /* Delete the stored password,
+ unless we are not in start mode
+ then we might still need it.
+ */
+ o_res = pam_set_data (pamh, STORED_AUTHTOK,
+ NULL, cleanup_free_password);
+ if (o_res != PAM_SUCCESS)
+ {
+ message ("couldn't delete stored authtok: %s",
+ pam_strerror (pamh, o_res));
+ }
+
+ return res;
+}
+
+PAM_EXTERN int
+pam_sm_close_session (pam_handle_t *pamh,
+ int flags,
+ int argc,
+ const char *argv[])
+{
+ const char *s_pid;
+ int pid = 0;
+ parse_args (argc, argv);
+
+ /* Kill the ssh agent we started */
+ s_pid = pam_getenv (pamh, "SSH_AGENT_PID");
+ if (s_pid)
+ pid = atoi (s_pid);
+
+ if (pid > 0)
+ {
+ debug ("Closing %d", pid);
+ kill (pid, SIGTERM);
+ }
+ return PAM_SUCCESS;
+}
+
+PAM_EXTERN int
+pam_sm_authenticate (pam_handle_t *pamh,
+ int unused,
+ int argc,
+ const char **argv)
+{
+ const char *password;
+ int ret;
+
+ parse_args (argc, argv);
+
+ /* Look up the password and store it for later */
+ ret = pam_get_item (pamh, PAM_AUTHTOK,
+ (const void**)&password);
+ if (ret != PAM_SUCCESS)
+ message ("no password is available: %s",
+ pam_strerror (pamh, ret));
+
+ if (password != NULL)
+ stash_password_for_session (pamh, password);
+
+ /* We're not an authentication module */
+ return PAM_CRED_INSUFFICIENT;
+}
+
+PAM_EXTERN int
+pam_sm_setcred (pam_handle_t *pamh,
+ int flags,
+ int argc,
+ const char *argv[])
+{
+ return PAM_SUCCESS;
+}
diff --git a/src/pam-ssh-add/pam-ssh-add.h b/src/pam-ssh-add/pam-ssh-add.h
new file mode 100644
index 0000000..1b6c87e
--- /dev/null
+++ b/src/pam-ssh-add/pam-ssh-add.h
@@ -0,0 +1,48 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef PAM_SSH_ADD_H__
+#define PAM_SSH_ADD_H__
+
+#include <pwd.h>
+#include <security/pam_modules.h>
+
+#define N_ELEMENTS(x) (sizeof(x) / sizeof (x)[0])
+
+extern const char *pam_ssh_agent_program;
+extern const char *pam_ssh_agent_arg;
+extern const char *pam_ssh_add_program;
+extern const char *pam_ssh_add_arg;
+extern int pam_ssh_add_verbose_mode;
+
+typedef void (*pam_ssh_add_logger) (int level, const char *data);
+extern pam_ssh_add_logger pam_ssh_add_log_handler;
+
+int pam_ssh_add_start_agent (pam_handle_t *pamh,
+ struct passwd *pwd,
+ const char *xdg_runtime_overide,
+ char **out_auth_sock_var,
+ char **out_agent_pid_var);
+
+int pam_ssh_add_load (pam_handle_t *pamh,
+ struct passwd *pwd,
+ const char *agent_socket,
+ const char *password);
+
+#endif /* PAM_SSH_ADD_H__ */
diff --git a/src/pam-ssh-add/test-ssh-add.c b/src/pam-ssh-add/test-ssh-add.c
new file mode 100644
index 0000000..75a16a0
--- /dev/null
+++ b/src/pam-ssh-add/test-ssh-add.c
@@ -0,0 +1,426 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "pam-ssh-add.h"
+
+#include "testlib/retest.h"
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/queue.h>
+
+#include <err.h>
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+static int unexpected_message;
+
+/* Environment variables we set */
+static const char *env_names[] = {
+ "XDG_RUNTIME_DIR",
+ "HOME",
+ "PATH",
+ "LC_ALL",
+ "SSH_AUTH_SOCK",
+ NULL
+};
+
+
+/* Holds environment values to set in pam context */
+static char *env_saved[N_ELEMENTS (env_names)] = { NULL, };
+
+typedef struct {
+ const char *ssh_add;
+ const char *ssh_add_arg;
+ const char *ssh_agent;
+ const char *ssh_agent_arg;
+ const char *password;
+ struct passwd *pw;
+} Fixture;
+
+struct _ExpectedMessage {
+ const char *line;
+ TAILQ_ENTRY (_ExpectedMessage) messages;
+};
+
+TAILQ_HEAD (ExpectedList, _ExpectedMessage) el_head;
+
+typedef struct _ExpectedMessage ExpectedMessage;
+
+static void
+expect_message (const char *msg)
+{
+ ExpectedMessage *em = NULL;
+ em = (ExpectedMessage *) malloc(sizeof(ExpectedMessage));
+ if (em == NULL)
+ assert_not_reached ("expected message allocation failed");
+ em->line = msg;
+ TAILQ_INSERT_TAIL (&el_head, em, messages);
+}
+
+static void
+test_logger (int level, const char *msg)
+{
+ assert (msg != NULL);
+ if (el_head.tqh_first != NULL)
+ {
+ ExpectedMessage *em = el_head.tqh_first;
+ assert_str_contains (msg, em->line);
+ TAILQ_REMOVE (&el_head, el_head.tqh_first, messages);
+ free (em);
+ }
+ else
+ {
+ warnx ("%s", msg);
+ unexpected_message = 1;
+ }
+}
+
+static void
+save_environment (void)
+{
+ int i;
+
+ for (i = 0; env_names[i] != NULL; i++)
+ env_saved[i] = getenv (env_names[i]);
+}
+
+static void
+restore_environment (void)
+{
+ int i;
+ for (i = 0; env_names[i] != NULL; i++)
+ {
+ if (env_saved[i])
+ setenv (env_names[i], env_saved[i], 1);
+ else
+ unsetenv (env_names[i]);
+ }
+}
+
+static void
+setup (void *arg)
+{
+ Fixture *fix = arg;
+ unexpected_message = 0;
+ if (!fix->ssh_add)
+ fix->ssh_add = SRCDIR "/src/pam-ssh-add/mock-ssh-add";
+
+ if (!fix->ssh_agent)
+ fix->ssh_agent = SRCDIR "/src/pam-ssh-add/mock-ssh-agent";
+
+ pam_ssh_add_program = fix->ssh_add;
+ pam_ssh_add_arg = fix->ssh_add_arg;
+ pam_ssh_agent_program = fix->ssh_agent;
+ pam_ssh_agent_arg = fix->ssh_agent_arg;
+ fix->pw = getpwuid (getuid ());
+}
+
+static void
+teardown (void *arg)
+{
+ int missed = 0;
+
+ // restore original environment
+ restore_environment ();
+
+ while (el_head.tqh_first != NULL)
+ {
+ ExpectedMessage *em = el_head.tqh_first;
+ warnx ("message didn't get logged: %s", em->line);
+ TAILQ_REMOVE (&el_head, el_head.tqh_first, messages);
+ free (em);
+ missed = 1;
+ }
+
+ if (missed)
+ assert_not_reached ("expected messages didn't get logged");
+
+ if (unexpected_message)
+ assert_not_reached ("got unexpected messages");
+
+}
+
+static Fixture default_fixture = {
+};
+
+static Fixture environment_fixture = {
+ .ssh_agent = SRCDIR "/src/pam-ssh-add/mock-environment",
+ .ssh_agent_arg = NULL
+};
+
+static void
+run_test_agent_environment (void *data,
+ const char *xdg_runtime,
+ const char *xdg_runtime_expect)
+{
+ Fixture *fix = data;
+ int ret;
+ char *xdg_expect = NULL;
+ char *home_expect = NULL;
+
+ if (xdg_runtime_expect)
+ {
+ if (asprintf (&xdg_expect, "XDG_RUNTIME_DIR=%s",
+ xdg_runtime_expect) < 0)
+ warnx ("Couldn't allocate XDG_RUNTIME_DIR expect variable");
+ }
+ else
+ {
+ xdg_expect = strdup ("NO XDG_RUNTIME_DIR");
+ }
+
+ if (asprintf (&home_expect, "HOME=%s", fix->pw->pw_dir) < 0)
+ warnx ("Couldn't allocate HOME expect variable");
+
+ expect_message (xdg_expect);
+ expect_message (home_expect);
+
+ expect_message ("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin");
+ expect_message ("LC_ALL=C");
+ expect_message ("NO OTHER");
+
+ expect_message ("NO SSH_AUTH_SOCK");
+ expect_message ("Failed to start ssh-agent");
+
+ ret = pam_ssh_add_start_agent (NULL, fix->pw, xdg_runtime, NULL, NULL);
+
+ assert_num_eq (0, ret);
+
+ free (xdg_expect);
+ free (home_expect);
+}
+
+static void
+test_environment (void *data)
+{
+ run_test_agent_environment (data, NULL, getenv ("XDG_RUNTIME_DIR"));
+}
+
+static void
+test_environment_env_overides (void *data)
+{
+ setenv ("PATH", "bad", 1);
+ setenv ("LC_ALL", "bad", 1);
+ setenv ("HOME", "bad", 1);
+ setenv ("XDG_RUNTIME_DIR", "", 1);
+ setenv ("SSH_AUTH_SOCK", "bad", 1);
+ setenv ("OTHER", "bad", 1);
+
+ run_test_agent_environment (data, NULL, "");
+}
+
+static void
+test_environment_overides (void *data)
+{
+ setenv ("XDG_RUNTIME_DIR", "bad", 1);
+ run_test_agent_environment (data, "xdgover", "xdgover");
+}
+
+static void
+test_failed_agent (void *data)
+{
+ Fixture *fix = data;
+ char *sock = NULL;
+ char *pid = NULL;
+ int ret;
+
+ expect_message ("Bad things");
+ expect_message ("Failed to start ssh-agent");
+ ret = pam_ssh_add_start_agent (NULL, fix->pw, NULL, &sock, &pid);
+
+ assert_num_eq (0, ret);
+ assert_ptr_eq (sock, NULL);
+ assert_ptr_eq (pid, NULL);
+
+ free (sock);
+ free (pid);
+}
+
+static Fixture bad_agent_fixture = {
+ .ssh_agent_arg = "bad-vars",
+};
+
+static void
+test_bad_agent_vars (void *data)
+{
+ Fixture *fix = data;
+ char *sock = NULL;
+ char *pid = NULL;
+ int ret;
+
+ expect_message ("Expected agent environment variables not found");
+ ret = pam_ssh_add_start_agent (NULL, fix->pw, NULL, &sock, &pid);
+
+ assert_num_eq (0, ret);
+ assert_ptr_eq (sock, NULL);
+ assert_ptr_eq (pid, NULL);
+
+ free (sock);
+ free (pid);
+}
+
+static Fixture good_agent_fixture = {
+ .ssh_agent_arg = "good-vars",
+};
+
+static void
+test_good_agent_vars (void *data)
+{
+ Fixture *fix = data;
+ char *sock = NULL;
+ char *pid = NULL;
+ int ret;
+
+ ret = pam_ssh_add_start_agent (NULL, fix->pw, NULL, &sock, &pid);
+
+ assert_num_eq (1, ret);
+ assert_str_cmp (sock, ==, "SSH_AUTH_SOCKET=socket");
+ assert_str_cmp (pid, ==, "SSH_AGENT_PID=100");
+
+ free (sock);
+ free (pid);
+}
+
+static Fixture keys_password_fixture = {
+ .ssh_add_arg = NULL,
+ .password = "foobar",
+};
+
+static Fixture keys_no_password_fixture = {
+ .ssh_add_arg = NULL,
+ .password = NULL,
+};
+
+static Fixture keys_bad_password_fixture = {
+ .ssh_add_arg = NULL,
+ .password = "bad",
+};
+
+static void
+test_keys (void *data)
+{
+ int ret;
+ int expect = 1;
+ Fixture *fix = data;
+ const char *key_add_result;
+
+ if (fix->password == NULL)
+ {
+ key_add_result = "Correct password 0, bad password 0, password_blanks 3";
+ }
+ else if (strcmp (fix->password, "foobar") == 0)
+ {
+ expect = 0;
+ key_add_result = "Correct password 3, bad password 0, password_blanks 0";
+ }
+ else
+ {
+ key_add_result = "Correct password 0, bad password 3, password_blanks 3";
+ }
+
+ expect_message (key_add_result);
+ if (expect)
+ expect_message ("Failed adding some keys");
+
+ ret = pam_ssh_add_load (NULL, fix->pw, "mock-socket", fix->password);
+
+ assert_num_eq (1, ret);
+}
+
+static Fixture keys_environment_fixture = {
+ .ssh_add = SRCDIR "/src/pam-ssh-add/mock-environment",
+ .ssh_add_arg = NULL
+};
+
+static void
+test_key_environment (void *data)
+{
+ Fixture *fix = data;
+ int ret;
+ char *home_expect = NULL;
+
+ expect_message ("ssh-add requires an agent socket");
+ ret = pam_ssh_add_load (NULL, fix->pw, NULL, NULL);
+ assert_num_eq (0, ret);
+
+ if (asprintf (&home_expect, "HOME=%s", fix->pw->pw_dir) < 0)
+ warnx ("Couldn't allocate HOME expect variable");
+
+ expect_message ("NO XDG_RUNTIME_DIR");
+ expect_message (home_expect);
+
+ expect_message ("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin");
+ expect_message ("LC_ALL=C");
+ expect_message ("NO OTHER");
+
+ expect_message ("SSH_AUTH_SOCK=mock-socket");
+ expect_message ("Failed adding some keys");
+
+ ret = pam_ssh_add_load (NULL, fix->pw, "mock-socket", NULL);
+
+ assert_num_eq (1, ret);
+
+ free (home_expect);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ signal (SIGPIPE, SIG_IGN);
+
+ TAILQ_INIT(&el_head);
+
+ save_environment ();
+
+ re_fixture (setup, teardown);
+
+ pam_ssh_add_log_handler = &test_logger;
+ pam_ssh_add_verbose_mode = 0;
+
+ re_testx (test_key_environment, &keys_environment_fixture,
+ "/pam-ssh-add/add-key-environment");
+ re_testx (test_keys, &keys_no_password_fixture,
+ "/pam-ssh-add/add-key-no-password");
+ re_testx (test_keys, &keys_bad_password_fixture,
+ "/pam-ssh-add/add-key-bad-password");
+ re_testx (test_keys, &keys_password_fixture,
+ "/pam-ssh-add/add-key-password");
+
+ re_testx (test_environment, &environment_fixture,
+ "/pam-ssh-add/environment");
+ re_testx (test_environment_env_overides, &environment_fixture,
+ "/pam-ssh-add/environment-env-overides");
+ re_testx (test_environment_overides, &environment_fixture,
+ "/pam-ssh-add/environment-overides");
+ re_testx (test_good_agent_vars, &good_agent_fixture,
+ "/pam-ssh-add/good-agent-vars");
+ re_testx (test_bad_agent_vars, &bad_agent_fixture,
+ "/pam-ssh-add/bad-agent-vars");
+ re_testx (test_failed_agent, &default_fixture,
+ "/pam-ssh-add/test-failed-agent");
+
+ return re_test_run (argc, argv);
+}
diff --git a/src/session/Makefile-session.am b/src/session/Makefile-session.am
new file mode 100644
index 0000000..eea4a34
--- /dev/null
+++ b/src/session/Makefile-session.am
@@ -0,0 +1,28 @@
+# -----------------------------------------------------------------------------
+# cockpit-session
+
+libexec_PROGRAMS += cockpit-session
+
+cockpit_session_LDADD = \
+ $(libcockpit_common_nodeps_a_LIBS) \
+ $(krb5_LIBS) \
+ $(libsystemd_LIBS) \
+ -lpam \
+ $(NULL)
+
+cockpit_session_SOURCES = \
+ src/common/cockpitclosefrom.c \
+ src/common/cockpithacks.h \
+ src/session/client-certificate.h \
+ src/session/client-certificate.c \
+ src/session/client-certificate.h \
+ src/session/session-utils.c \
+ src/session/session-utils.h \
+ src/session/session.c \
+ $(NULL)
+
+# If running cockpit-ws as a non-standard user, we also set up
+# cockpit-session to be setuid root, but only runnable by cockpit-session
+install-exec-hook::
+ chown -f root:$(COCKPIT_WSINSTANCE_GROUP) $(DESTDIR)$(libexecdir)/cockpit-session || true
+ test "$(COCKPIT_USER)" != "root" && chmod -f 4750 $(DESTDIR)$(libexecdir)/cockpit-session || true
diff --git a/src/session/client-certificate.c b/src/session/client-certificate.c
new file mode 100644
index 0000000..b96668b
--- /dev/null
+++ b/src/session/client-certificate.c
@@ -0,0 +1,355 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "client-certificate.h"
+
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <regex.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include <systemd/sd-bus.h>
+
+#include "session-utils.h"
+
+#define CLIENT_CERTIFICATE_DIRECTORY "/run/cockpit/tls/clients"
+
+/* This is a bit lame, but having a hard limit on peer certificates is
+ * desirable: Let's not get DoSed by huge certs */
+#define MAX_PEER_CERT_SIZE 100000
+
+/* Reads the cgroupsv2-style /proc/[pid]/cgroup file of the process,
+ * including "0::" prefix and newline.
+ *
+ * In case of cgroupsv1, look for the name=systemd controller, and fake
+ * it.
+ */
+static char *
+read_proc_self_cgroup (size_t *out_length)
+{
+ FILE *fp = fopen ("/proc/self/cgroup", "r");
+
+ if (fp == NULL)
+ {
+ warn ("Failed to open /proc/self/cgroup");
+ return NULL;
+ }
+
+ /* Support cgroups v1 by looping.
+ * Once we no longer need this support, we can drop the loop, switch
+ * to fread(), and just return the entire content of the file.
+ *
+ * NB: the kernel doesn't allow newlines in cgroup names.
+ */
+ char buffer[1024];
+ char *result = NULL;
+ while (fgets (buffer, sizeof buffer, fp))
+ {
+ if (strncmp (buffer, "0::", 3) == 0)
+ {
+ /* cgroupsv2 (or hybrid) case. Return the entire line. */
+ result = strdupx (buffer);
+ break;
+ }
+ else if (strncmp (buffer, "1:name=systemd:", 15) == 0)
+ {
+ /* cgroupsv1. Rewrite to what we'd expect from cgroupsv2. */
+ asprintfx (&result, "0::%s", buffer + 15);
+ break;
+ }
+ }
+
+ fclose (fp);
+
+ assert (result != NULL);
+
+ *out_length = strlen (result);
+
+ /* Make sure we have a non-empty result, and that it ends with a
+ * newline: this could only fail if the kernel returned something
+ * unexpected.
+ */
+ assert (*out_length >= 5); /* "0::/\n" */
+ assert (result[*out_length - 1] == '\n');
+
+ return result;
+}
+
+/* valid_256_bit_hex_string:
+ * @str: a string
+ *
+ * Ensures that str is a hexadecimal character string, exactly 64
+ * characters in length.
+ */
+static bool
+valid_256_bit_hex_string (const char *str)
+{
+ size_t length = strspn (str, "0123456789abcdef");
+
+ return str[length] == '\0' && length == 64;
+}
+
+/**
+ * read_cert_file:
+ * @contents: a buffer to read the certificate into
+ * @contents_size: the size of @contents
+ *
+ * Reads the contents of the certificate file into @contents (of size @contents_size).
+ * The buffer must be large enough for the contents of the certificate file, plus
+ * a nul terminator (which will be added).
+ *
+ * On success, the size of the certificate file (excluding nul
+ * terminator) is returned. This value is never 0. On error, -1 is
+ * returned with errno not guaranteed to be set (but a message will be
+ * logged).
+ */
+static ssize_t
+read_cert_file (const char *filename,
+ char *contents,
+ size_t contents_size)
+{
+ int dirfd = -1, filefd = -1;
+ ssize_t result = -1;
+ struct stat buf;
+ ssize_t r;
+
+ /* No tricky stuff, please */
+ if (!valid_256_bit_hex_string (filename))
+ {
+ warnx ("tls-cert authentication token is invalid");
+ goto out;
+ }
+
+ dirfd = open (CLIENT_CERTIFICATE_DIRECTORY, O_PATH | O_DIRECTORY | O_NOFOLLOW);
+ if (dirfd == -1)
+ {
+ warn ("Failed to open " CLIENT_CERTIFICATE_DIRECTORY);
+ goto out;
+ }
+
+ filefd = openat (dirfd, filename, O_RDONLY | O_NOFOLLOW);
+ if (filefd == -1)
+ {
+ warn ("Failed to open certificate file %s/%s",
+ CLIENT_CERTIFICATE_DIRECTORY, filename);
+ goto out;
+ }
+
+ if (fstat (filefd, &buf) != 0)
+ {
+ warn ("Failed to stat certificate file %s/%s",
+ CLIENT_CERTIFICATE_DIRECTORY, filename);
+ goto out;
+ }
+
+ if (!S_ISREG (buf.st_mode))
+ {
+ warnx ("Could not read certificate: %s/%s is not a regular file",
+ CLIENT_CERTIFICATE_DIRECTORY, filename);
+ goto out;
+ }
+
+ if (buf.st_size == 0)
+ {
+ warnx ("Could not read certificate: %s/%s is empty",
+ CLIENT_CERTIFICATE_DIRECTORY, filename);
+ goto out;
+ }
+
+ /* Strictly less than, since we will add a nul */
+ if (!(buf.st_size < contents_size))
+ {
+ warnx ("Insufficient space in read buffer for %s/%s",
+ CLIENT_CERTIFICATE_DIRECTORY, filename);
+ goto out;
+ }
+
+ do
+ r = pread (filefd, contents, buf.st_size, 0);
+ while (r == -1 && errno == EINTR);
+ if (r == -1)
+ {
+ warn ("Could not read certificate file %s/%s",
+ CLIENT_CERTIFICATE_DIRECTORY, filename);
+ goto out;
+ }
+ if (r != buf.st_size)
+ {
+ warnx ("Read incomplete contents of certificate file %s/%s: %zu of %zu bytes",
+ CLIENT_CERTIFICATE_DIRECTORY, filename, r, (size_t) buf.st_size);
+ goto out;
+ }
+
+ contents[buf.st_size] = '\0';
+
+ if (strlen (contents) != buf.st_size)
+ {
+ warnx ("Certificate file %s/%s contains nul characters",
+ CLIENT_CERTIFICATE_DIRECTORY, filename);
+ goto out;
+ }
+
+ result = buf.st_size;
+
+out:
+ if (filefd != -1)
+ close (filefd);
+
+ if (dirfd != -1)
+ close (dirfd);
+
+ return result;
+}
+
+static bool
+sssd_map_certificate (const char *certificate, char** username)
+{
+ int result = false;
+ sd_bus_error err = SD_BUS_ERROR_NULL;
+ sd_bus *bus = NULL;
+ sd_bus_message *user_obj_msg = NULL;
+ const char *user_obj_path = NULL;
+ int r;
+
+ assert (username);
+ assert (!*username);
+
+ r = sd_bus_open_system (&bus);
+ if (r < 0)
+ {
+ warnx ("Failed to connect to system bus: %s", strerror (-r));
+ goto out;
+ }
+
+ /* sssd 2.6.1 introduces certificate validation against the configured CA. This version is in all supported distros */
+ r = sd_bus_call_method (bus,
+ "org.freedesktop.sssd.infopipe",
+ "/org/freedesktop/sssd/infopipe/Users",
+ "org.freedesktop.sssd.infopipe.Users",
+ "FindByValidCertificate",
+ &err,
+ &user_obj_msg,
+ "s",
+ certificate);
+
+ if (r < 0)
+ {
+ /* The error name is a bit confusing, and this is the common case; translate to readable error */
+ if (sd_bus_error_has_name (&err, "sbus.Error.NotFound"))
+ {
+ warnx ("No matching user for certificate");
+ goto out;
+ }
+
+ warnx ("Failed to map certificate to user: [%s] %s", err.name, err.message);
+ goto out;
+ }
+
+ assert (user_obj_msg);
+
+ r = sd_bus_message_read (user_obj_msg, "o", &user_obj_path);
+ if (r < 0)
+ {
+ warnx ("Failed to parse response message: %s", strerror (-r));
+ goto out;
+ }
+
+ debug ("certificate mapped to user object path %s", user_obj_path);
+
+ r = sd_bus_get_property_string (bus,
+ "org.freedesktop.sssd.infopipe",
+ user_obj_path,
+ "org.freedesktop.sssd.infopipe.Users.User",
+ "name",
+ &err,
+ username);
+
+ if (r < 0)
+ {
+ warnx ("Failed to map user object to name: [%s] %s", err.name, err.message);
+ goto out;
+ }
+
+ assert (*username);
+ debug ("mapped certificate to user %s", *username);
+ result = true;
+
+out:
+ sd_bus_error_free (&err);
+ sd_bus_message_unref (user_obj_msg);
+ sd_bus_unref (bus);
+ return result;
+}
+
+/**
+ * cockpit_session_client_certificate_map_user
+ *
+ * Read the given certificate file, ensure that it belongs to our own cgroup, and ask
+ * sssd to map it to a user. If everything matches as expected, return the user name.
+ * Otherwise return %NULL, a warning message will already have been logged.
+ */
+char *
+cockpit_session_client_certificate_map_user (const char *client_certificate_filename)
+{
+ char cert_pem[MAX_PEER_CERT_SIZE];
+ char *sssd_user = NULL;
+
+ /* read the certificate file from disk */
+ if (read_cert_file (client_certificate_filename, cert_pem, sizeof cert_pem) < 0)
+ {
+ warnx ("No https instance certificate present");
+ return NULL;
+ }
+
+ size_t my_cgroup_length;
+ char *my_cgroup = read_proc_self_cgroup (&my_cgroup_length);
+ if (my_cgroup == NULL)
+ {
+ warnx ("Could not determine cgroup of this process");
+ return NULL;
+ }
+ /* A simple prefix comparison is appropriate here because my_cgroup
+ * will contain exactly one newline (at the end), and the expected
+ * value of my_cgroup is on the first line in cert_pem.
+ */
+ if (strncmp (cert_pem, my_cgroup, my_cgroup_length) != 0)
+ {
+ warnx ("This client certificate is only meant to be used from another cgroup");
+ free (my_cgroup);
+ return NULL;
+ }
+ free (my_cgroup);
+
+ /* ask sssd to map cert to a user */
+ if (!sssd_map_certificate (cert_pem + my_cgroup_length, &sssd_user))
+ return NULL;
+
+ return sssd_user;
+}
diff --git a/src/session/client-certificate.h b/src/session/client-certificate.h
new file mode 100644
index 0000000..01bb855
--- /dev/null
+++ b/src/session/client-certificate.h
@@ -0,0 +1,23 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+char *
+cockpit_session_client_certificate_map_user (const char *client_certificate_filename);
diff --git a/src/session/session-utils.c b/src/session/session-utils.c
new file mode 100644
index 0000000..4e40502
--- /dev/null
+++ b/src/session/session-utils.c
@@ -0,0 +1,606 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "session-utils.h"
+
+#include "common/cockpitframe.h"
+#include "common/cockpitjsonprint.h"
+#include "common/cockpithacks.h"
+
+#include <fcntl.h>
+#include <paths.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <sys/param.h>
+#include <time.h>
+#include <utmp.h>
+
+#ifndef _PATH_BTMP
+#define _PATH_BTMP "/var/log/btmp"
+#endif
+
+const char *program_name;
+struct passwd *pwd;
+pid_t child;
+int want_session = 1;
+char *last_err_msg = NULL;
+
+static char *auth_prefix = NULL;
+static size_t auth_prefix_size = 0;
+static char *auth_msg = NULL;
+static size_t auth_msg_size = 0;
+static FILE *authf = NULL;
+
+char *
+read_authorize_response (const char *what)
+{
+ const char *auth_response = ",\"response\":\"";
+ size_t auth_response_size = 13;
+ const char *auth_suffix = "\"}";
+ size_t auth_suffix_size = 2;
+ unsigned char *message;
+ ssize_t len;
+
+ debug ("reading %s authorize message", what);
+
+ len = cockpit_frame_read (STDIN_FILENO, &message);
+ if (len < 0)
+ err (EX, "couldn't read %s", what);
+
+ /*
+ * The authorize messages we receive always have an exact prefix and suffix:
+ *
+ * \n{"command":"authorize","cookie":"NNN","response":"...."}
+ */
+ if (len <= auth_prefix_size + auth_response_size + auth_suffix_size ||
+ memcmp (message, auth_prefix, auth_prefix_size) != 0 ||
+ memcmp (message + auth_prefix_size, auth_response, auth_response_size) != 0 ||
+ memcmp (message + (len - auth_suffix_size), auth_suffix, auth_suffix_size) != 0)
+ {
+ errx (EX, "didn't receive expected \"authorize\" message");
+ }
+
+ len -= auth_prefix_size + auth_response_size + auth_suffix_size;
+ memmove (message, message + auth_prefix_size + auth_response_size, len);
+ message[len] = '\0';
+ return (char *)message;
+}
+
+void
+write_control_string (const char *field,
+ const char *str)
+{
+ cockpit_json_print_string_property (authf, field, str, -1);
+}
+
+void
+write_control_bool (const char *field,
+ bool val)
+{
+ cockpit_json_print_bool_property (authf, field, val);
+}
+
+void
+write_authorize_begin (void)
+{
+ assert (authf == NULL);
+ assert (auth_msg_size == 0);
+ assert (auth_msg == NULL);
+
+ debug ("writing auth challenge");
+
+ if (auth_prefix)
+ {
+ free (auth_prefix);
+ auth_prefix = NULL;
+ }
+
+ if (asprintf (&auth_prefix, "\n{\"command\":\"authorize\",\"cookie\":\"session%u%u\"",
+ (unsigned int)getpid(), (unsigned int)time (NULL)) < 0)
+ {
+ errx (EX, "out of memory allocating string");
+ }
+ auth_prefix_size = strlen (auth_prefix);
+
+ authf = open_memstream (&auth_msg, &auth_msg_size);
+ if (!authf)
+ err (EX, "failed to open_memstream()");
+ fprintf (authf, "%s", auth_prefix);
+}
+
+void
+write_control_end (void)
+{
+ assert (authf != NULL);
+
+ fprintf (authf, "}\n");
+ fflush (authf);
+ fclose (authf);
+
+ assert (auth_msg_size > 0);
+ assert (auth_msg != NULL);
+
+ if (cockpit_frame_write (STDOUT_FILENO, (unsigned char *)auth_msg, auth_msg_size) < 0)
+ err (EX, "couldn't write auth request");
+
+ debug ("finished auth request");
+ free (auth_msg);
+ auth_msg = NULL;
+ authf = NULL;
+ auth_msg_size = 0;
+}
+
+void
+build_string (char **buf,
+ size_t *size,
+ const char *str,
+ size_t len)
+{
+ if (*size == 0)
+ return;
+
+ if (len > *size - 1)
+ len = *size - 1;
+
+ memcpy (*buf, str, len);
+ (*buf)[len] = '\0';
+ *buf += len;
+ *size -= len;
+}
+
+static bool
+do_lastlog (uid_t uid,
+ const struct timeval *now,
+ const char *rhost,
+ time_t *out_last_login,
+ FILE *messages)
+{
+ struct lastlog entry;
+ bool result = false;
+ int fd = -1;
+ ssize_t r;
+
+ fd = open (_PATH_LASTLOG, O_RDWR);
+ if (fd == -1)
+ {
+ warn ("failed to open %s", _PATH_LASTLOG);
+ goto out;
+ }
+
+ r = pread (fd, &entry, sizeof entry, uid * sizeof entry);
+ if (r == sizeof entry && entry.ll_time != 0)
+ {
+ /* got an entry for the user */
+
+ /* the ll_host and ll_line fields can be nul-terminated, but they
+ * can also extend to the full length of the field without
+ * nul-termination. use the maxlen parameter to help with that.
+ */
+ if (!cockpit_json_print_integer_property (messages, "last-login-time", entry.ll_time) ||
+ !cockpit_json_print_string_property (messages, "last-login-host", entry.ll_host, UT_HOSTSIZE) ||
+ !cockpit_json_print_string_property (messages, "last-login-line", entry.ll_line, UT_LINESIZE))
+ {
+ warnx ("failed to print last-login details to messages memfd");
+ goto out;
+ }
+
+ if (out_last_login)
+ *out_last_login = entry.ll_time;
+ }
+ else if (r == sizeof entry)
+ {
+ /* read the entry, but it's nul. user never logged in. */
+ *out_last_login = 0;
+ }
+ else if (r == 0)
+ {
+ /* no such entry in file: never logged in? */
+ *out_last_login = 0;
+ }
+ else if (r < 0)
+ {
+ /* error */
+ warn ("failed to pread() %s for uid %u", _PATH_LASTLOG, (unsigned) uid);
+ goto out;
+ }
+ else
+ {
+ /* some other size (incomplete read) */
+ warnx ("incomplete pread() %s for uid %u: %zu of %zu bytes",
+ _PATH_LASTLOG, (unsigned) uid, r, sizeof entry);
+ goto out;
+ }
+
+ /* XXX: We'd really like to use strncpy() here, which is perfectly
+ * designed for what we need to do: copy a string up to N characters
+ * into a fixed width field, adding nul bytes if the string is shorter
+ * than N.
+ *
+ * Unfortunately, when you use it in this way, GCC is convinced that
+ * you don't know what you're doing and gives a warning that's very
+ * difficult to get rid of. We tried using #pragma here before, but
+ * after several attempts, it was difficult to get the
+ * conditionalising (for the compiler version) correct.
+ *
+ * Let's just nul out the struct and use memcpy(). Sigh.
+ *
+ * strncpy (entry.ll_host, rhost, sizeof entry.ll_host);
+ * strncpy (entry.ll_line, "web console", sizeof entry.ll_line);
+ *
+ * See also https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94615
+ * See also https://sourceware.org/bugzilla/show_bug.cgi?id=25844
+ */
+ memset (&entry, 0, sizeof entry);
+ memcpy (entry.ll_host, rhost, MIN (strlen (rhost), sizeof entry.ll_host));
+ const char * const line = "web console";
+ memcpy (entry.ll_line, line, MIN (strlen (line), sizeof entry.ll_line));
+
+ entry.ll_time = now->tv_sec;
+
+ r = pwrite (fd, &entry, sizeof entry, uid * sizeof entry);
+ if (r == -1)
+ {
+ /* error */
+ warn ("failed to pwrite() %s for uid %u", _PATH_LASTLOG, (unsigned) uid);
+ goto out;
+ }
+ else if (r != sizeof entry)
+ {
+ /* incomplete write */
+ warnx ("incomplete pwrite() %s for uid %u: %zu or %zu bytes",
+ _PATH_LASTLOG, (unsigned) uid, r, sizeof entry);
+ goto out;
+ }
+
+ result = true;
+
+out:
+ if (fd != -1)
+ close (fd);
+
+ return result;
+}
+
+static bool
+scan_btmp (const char *username,
+ time_t last_success,
+ FILE *messages)
+{
+ bool success = false;
+ int fail_count = 0;
+ struct utmp last;
+ int fd;
+
+ fd = open (_PATH_BTMP, O_RDONLY | O_CLOEXEC);
+ if (fd == -1)
+ {
+ if (errno == ENOENT)
+ {
+ /* no btmp → no failed attempts */
+ success = true;
+ goto out;
+ }
+
+ warn ("open(%s) failed", _PATH_BTMP);
+ goto out;
+ }
+
+ while (true)
+ {
+ struct utmp entry;
+ ssize_t r;
+
+ do
+ r = read (fd, &entry, sizeof entry);
+ while (r == -1 && errno != EINTR);
+
+ if (r == 0)
+ break;
+
+ if (r < 0)
+ {
+ warn ("read(%s) failed", _PATH_BTMP);
+ goto out;
+ }
+ if (r != sizeof entry)
+ {
+ warnx ("read(%s) returned partial result (%zu of %zu bytes)",
+ _PATH_BTMP, r, sizeof entry);
+ goto out;
+ }
+
+ if (entry.ut_tv.tv_sec > last_success &&
+ strncmp (entry.ut_user, username, sizeof entry.ut_user) == 0)
+ {
+ last = entry;
+ fail_count++;
+ }
+ }
+
+ if (fail_count == 0)
+ {
+ success = true;
+ goto out;
+ }
+
+ /* only print messages if we actually have failures */
+ success = cockpit_json_print_integer_property (messages, "fail-count", fail_count) &&
+ cockpit_json_print_integer_property (messages, "last-fail-time", last.ut_tv.tv_sec) &&
+ cockpit_json_print_string_property (messages, "last-fail-host", last.ut_host, UT_HOSTSIZE) &&
+ cockpit_json_print_string_property (messages, "last-fail-line", last.ut_line, UT_LINESIZE);
+
+out:
+ if (fd > -1)
+ close (fd);
+
+ return success;
+}
+
+void
+utmp_log (int login,
+ const char *rhost,
+ FILE *messages)
+{
+ char id[UT_LINESIZE + 1];
+ struct utmp ut;
+ struct timeval tv;
+ int pid;
+
+ pid = getpid ();
+
+ snprintf (id, UT_LINESIZE, "%d", pid);
+
+ assert (pwd != NULL);
+ utmpname (_PATH_UTMP);
+ setutent ();
+
+ memset (&ut, 0, sizeof(ut));
+
+ strncpy (ut.ut_id, id, sizeof (ut.ut_id));
+ ut.ut_id[sizeof (ut.ut_id) - 1] = 0;
+
+ strncpy (ut.ut_line, "web console", sizeof ut.ut_line);
+ ut.ut_line[sizeof ut.ut_line - 1] = 0;
+
+ if (login)
+ {
+ strncpy (ut.ut_user, pwd->pw_name, sizeof(ut.ut_user));
+ ut.ut_user[sizeof (ut.ut_user) - 1] = 0;
+ strncpy (ut.ut_host, rhost, sizeof(ut.ut_host));
+ ut.ut_host[sizeof (ut.ut_host) - 1] = 0;
+ }
+
+ gettimeofday (&tv, NULL);
+ ut.ut_tv.tv_sec = tv.tv_sec;
+ ut.ut_tv.tv_usec = tv.tv_usec;
+
+ ut.ut_type = login ? USER_PROCESS : DEAD_PROCESS;
+ ut.ut_pid = pid;
+
+ pututline (&ut);
+ endutent ();
+
+ updwtmp (_PATH_WTMP, &ut);
+
+ if (login)
+ {
+ time_t last_success;
+
+ if (do_lastlog (pwd->pw_uid, &tv, rhost, &last_success, messages))
+ scan_btmp (pwd->pw_name, last_success, messages);
+ }
+}
+
+void
+btmp_log (const char *username,
+ const char *rhost)
+{
+ struct timeval tv;
+
+ /* the `tv` in the utmp struct is not actually a `struct timeval`, so
+ * we need to read into a temporary variable and then copy the fields.
+ */
+ gettimeofday (&tv, NULL);
+
+ struct utmp entry = {
+ .ut_line = "web console",
+ .ut_pid = getpid (),
+ .ut_tv.tv_sec = tv.tv_sec,
+ .ut_tv.tv_usec = tv.tv_usec,
+ .ut_type = LOGIN_PROCESS,
+ };
+
+ /* see utmp(5), it is ok to not null-terminate these if they have maximum size */
+ /* add coverity markers for older glibcs: https://sourceware.org/bugzilla/show_bug.cgi?id=24899 */
+ /* coverity[buffer_size_warning : FALSE] */
+ strncpy (entry.ut_host, rhost, sizeof entry.ut_host);
+ /* coverity[buffer_size_warning : FALSE] */
+ strncpy (entry.ut_user, username, sizeof entry.ut_user);
+
+ int fd = open (_PATH_BTMP, O_WRONLY | O_APPEND);
+ if (fd == -1)
+ {
+ warn ("open(%s) failed", _PATH_BTMP);
+ goto out;
+ }
+
+ ssize_t r = write (fd, &entry, sizeof entry);
+ if (r < 0)
+ {
+ warn ("write() %s failed", _PATH_BTMP);
+ goto out;
+ }
+ else if (r != sizeof entry)
+ {
+ warnx ("incomplete write() %s: %zu of %zu bytes",
+ _PATH_BTMP, r, sizeof entry);
+ goto out;
+ }
+
+out:
+ if (fd != -1)
+ close (fd);
+}
+
+void
+authorize_logger (const char *data)
+{
+ warnx ("%s", data);
+}
+
+/* signal- and after-fork()-safe function to format a string, print it
+ * to stderr and abort execution. Never returns.
+ */
+static noreturn void
+__attribute__ ((format (printf, 1, 2)))
+abort_with_message (const char *format,
+ ...)
+{
+ char buffer[1024];
+ va_list ap;
+
+ va_start (ap, format);
+ size_t length = vsnprintf (buffer, sizeof buffer, format, ap);
+ va_end (ap);
+
+ size_t ofs = 0;
+ while (ofs != length)
+ {
+ ssize_t r;
+ do
+ r = write (STDERR_FILENO, buffer + ofs, length - ofs);
+ while (r == -1 && errno == EINTR);
+
+ if (0 <= r && r <= length - ofs)
+ ofs += r;
+ else
+ break; /* something went wrong, but we can't deal with it */
+ }
+
+ abort ();
+}
+
+/* signal- and after-fork()-safe function to remap file descriptors
+ * according to a specified array. All other file descriptors are
+ * closed.
+ *
+ * Commonly used after fork() and before exec().
+ */
+static void
+fd_remap (const int *remap_fds,
+ int n_remap_fds)
+{
+ if (n_remap_fds < 0 || n_remap_fds > 1024)
+ abort_with_message ("requested to fd_remap() too many fds!");
+
+ int *fds = alloca (sizeof (int) * n_remap_fds);
+ memcpy (fds, remap_fds, sizeof (int) * n_remap_fds);
+
+ /* we need to get all of the remap-fds to be numerically above
+ * n_remap_fds in order to make sure that we don't overwrite them in
+ * the middle of the dup2() loop below, and also avoid the case that
+ * dup2() is a no-op (which could fail to clear the O_CLOEXEC flag,
+ * for example).
+ */
+ for (int i = 0; i < n_remap_fds; i++)
+ if (fds[i] != -1 && fds[i] < n_remap_fds)
+ {
+ int new_fd = fcntl (fds[i], F_DUPFD, n_remap_fds); /* returns >= n_remap_fds */
+
+ if (new_fd == -1)
+ abort_with_message ("fcntl(%d, F_DUPFD) failed: %m", fds[i]);
+
+ fds[i] = new_fd;
+ }
+
+ /* now we can map the fds into their final spot */
+ for (int i = 0; i < n_remap_fds; i++)
+ if (fds[i] != -1) /* no-op */
+ if (dup2 (fds[i], i) != i)
+ abort_with_message ("dup2(%d, %d) failed: %m", fds[i], i);
+
+ /* close everything else */
+ closefrom (n_remap_fds);
+}
+
+int
+spawn_and_wait (const char **argv, const char **envp,
+ const int *remap_fds, int n_remap_fds,
+ uid_t uid, gid_t gid)
+{
+ pid_t child;
+
+ child = fork ();
+ if (child == -1)
+ abort_with_message ("cockpit-session: fork() failed: %m");
+
+ if (child == 0)
+ {
+ /* This is the child process. Do preparation, and exec(). */
+ if (setresgid (gid, gid, gid) != 0)
+ abort_with_message ("setresgid: couldn't set gid to %u: %m\n", (int) gid);
+
+ if (setresuid (uid, uid, uid) != 0)
+ abort_with_message ("setresgid: couldn't set uid to %u: %m\n", (int) gid);
+
+ /* paranoid */
+ {
+ uid_t real, effective, saved;
+ int r;
+
+ r = getresuid (&real, &effective, &saved);
+ assert (r == 0 && real == uid && effective == uid && saved == uid);
+ }
+
+ {
+ gid_t real, effective, saved;
+ int r;
+
+ r = getresgid (&real, &effective, &saved);
+ assert (r == 0 && real == gid && effective == gid && saved == gid);
+ }
+
+ if (n_remap_fds != -1)
+ fd_remap (remap_fds, n_remap_fds);
+
+ execvpe (argv[0], (char **) argv, (char **) envp);
+ _exit(127);
+ }
+
+ else
+ {
+ /* This is the parent process. Wait for the child to exit. */
+ int wstatus;
+ int r;
+
+ do
+ r = waitpid (child, &wstatus, 0);
+ while (r == -1 && errno == EINTR);
+
+ if (r == -1)
+ abort_with_message ("waitpid(%d) on cockpit-bridge process failed: %m", (int) child);
+
+ /* 0 can only be returned of WNOHANG was given */
+ assert (r == child);
+
+ return wstatus;
+ }
+}
diff --git a/src/session/session-utils.h b/src/session/session-utils.h
new file mode 100644
index 0000000..8085665
--- /dev/null
+++ b/src/session/session-utils.h
@@ -0,0 +1,78 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "config.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <err.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdnoreturn.h>
+#include <string.h>
+#include <pwd.h>
+#include <grp.h>
+#include <errno.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "common/cockpitauthorize.h"
+#include "common/cockpitmemory.h"
+
+#define DEBUG_SESSION 0
+
+#if DEBUG_SESSION
+#define debug(fmt, ...) (fprintf (stderr, "%s: " fmt "\n", program_name, ##__VA_ARGS__))
+#else
+#define debug(...)
+#endif
+
+#define EX 127
+
+extern const char *program_name;
+extern struct passwd *pwd;
+extern char *last_err_msg;
+extern char *last_err_msg;
+extern int want_session;
+extern pid_t child;
+
+void build_string (char **buf, size_t *size, const char *str, size_t len);
+void authorize_logger (const char *data);
+void utmp_log (int login, const char *rhost, FILE *messages);
+void btmp_log (const char *username, const char *rhost);
+
+char* read_authorize_response (const char *what);
+void write_authorize_begin (void);
+void write_control_string (const char *field, const char *str);
+void write_control_bool (const char *field, bool val);
+void write_control_end (void);
+
+int
+spawn_and_wait (const char **argv,
+ const char **envp,
+ const int *remap_fds,
+ int n_remap_fds,
+ uid_t uid,
+ gid_t gid);
diff --git a/src/session/session.c b/src/session/session.c
new file mode 100644
index 0000000..e571816
--- /dev/null
+++ b/src/session/session.c
@@ -0,0 +1,1056 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitframe.h"
+#include "common/cockpitjsonprint.h"
+#include "common/cockpitmemory.h"
+
+#include "client-certificate.h"
+#include "session-utils.h"
+
+#include <security/pam_appl.h>
+#include <gssapi/gssapi.h>
+#include <gssapi/gssapi_generic.h>
+#include <gssapi/gssapi_krb5.h>
+#include <fcntl.h>
+
+static char *last_txt_msg = NULL;
+static char *conversation = NULL;
+
+/* This program opens a session for a given user and runs the bridge in
+ * it. It is used to manage localhost; for remote hosts sshd does
+ * this job.
+ */
+
+#define COCKPIT_KTAB PACKAGE_SYSCONF_DIR "/cockpit/krb5.keytab"
+
+static gss_cred_id_t creds = GSS_C_NO_CREDENTIAL;
+
+/* Environment variables to transfer */
+static const char *env_names[] = {
+ "G_DEBUG",
+ "G_MESSAGES_DEBUG",
+ "G_SLICE",
+ "PATH",
+ "COCKPIT_REMOTE_PEER",
+ NULL
+};
+
+/* Holds environment values to set in pam context */
+static char *env_saved[sizeof (env_names) / sizeof (env_names)[0]] = { NULL, };
+
+static const char *
+gssapi_strerror (gss_OID mech_type,
+ OM_uint32 major_status,
+ OM_uint32 minor_status)
+{
+ static char buffer[1024];
+ OM_uint32 major, minor;
+ OM_uint32 ctx;
+ gss_buffer_desc status;
+ char *buf;
+ size_t len;
+ int had_major;
+ int had_minor;
+
+ debug ("gssapi: major_status: %8.8x, minor_status: %8.8x",
+ major_status, minor_status);
+
+ buf = buffer;
+ len = sizeof (buffer);
+ buf[0] = '\0';
+ had_major = 0;
+ ctx = 0;
+
+ if (major_status != GSS_S_FAILURE || minor_status == 0)
+ {
+ for (;;)
+ {
+ major = gss_display_status (&minor, major_status, GSS_C_GSS_CODE,
+ GSS_C_NO_OID, &ctx, &status);
+ if (GSS_ERROR (major))
+ break;
+
+ if (had_major)
+ build_string (&buf, &len, ": ", 2);
+ had_major = 1;
+
+ build_string (&buf, &len, status.value, status.length);
+ gss_release_buffer (&minor, &status);
+
+ if (!ctx)
+ break;
+ }
+ }
+
+ ctx = 0;
+ had_minor = 0;
+ for (;;)
+ {
+ major = gss_display_status (&minor, minor_status, GSS_C_MECH_CODE,
+ mech_type, &ctx, &status);
+ if (GSS_ERROR (major))
+ break;
+
+ if (had_minor)
+ build_string (&buf, &len, ", ", 2);
+ else if (had_major)
+ build_string (&buf, &len, " (", 2);
+ had_minor = 1;
+ build_string (&buf, &len, status.value, status.length);
+
+ gss_release_buffer (&minor, &status);
+
+ if (!ctx)
+ break;
+ }
+
+ if (had_major && had_minor)
+ build_string (&buf, &len, ")", 1);
+
+ return buffer;
+}
+
+static int
+pam_conv_func (int num_msg,
+ const struct pam_message **msg,
+ struct pam_response **ret_resp,
+ void *appdata_ptr)
+{
+ char **password = (char **)appdata_ptr;
+ char *authorization = NULL;
+ char *prompt_resp = NULL;
+
+ /* For keeping track of messages returned by PAM */
+ char *err_msg = NULL;
+ char *txt_msg = NULL;
+ char *buf, **msgp;
+
+ struct pam_response *resp;
+ char *prompt = NULL;
+ int success = 1;
+ int i;
+
+ /* Any messages from the last conversation pass? */
+ txt_msg = last_txt_msg;
+ last_txt_msg = NULL;
+ err_msg = last_err_msg;
+ last_err_msg = NULL;
+
+ resp = callocx (sizeof (struct pam_response), num_msg);
+
+ for (i = 0; i < num_msg; i++)
+ {
+ if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF &&
+ *password != NULL)
+ {
+ debug ("answered pam password prompt");
+ resp[i].resp = *password;
+ resp[i].resp_retcode = 0;
+ *password = NULL;
+ }
+ else if (msg[i]->msg_style == PAM_ERROR_MSG || msg[i]->msg_style == PAM_TEXT_INFO)
+ {
+ if (msg[i]->msg_style == PAM_ERROR_MSG)
+ msgp = &err_msg;
+ else
+ msgp = &txt_msg;
+
+ if (*msgp)
+ {
+ buf = *msgp;
+ asprintfx (msgp, "%s\n%s", buf, msg[i]->msg);
+ free (buf);
+ }
+ else
+ {
+ asprintfx (msgp, "%s", msg[i]->msg);
+ }
+ warnx ("pam: %s", msg[i]->msg);
+ }
+ else
+ {
+ debug ("prompt for more data");
+ write_authorize_begin ();
+ prompt = cockpit_authorize_build_x_conversation (msg[i]->msg, &conversation);
+ if (!prompt)
+ err (EX, "couldn't generate prompt");
+
+ write_control_string ("challenge", prompt);
+ free (prompt);
+
+ if (txt_msg)
+ write_control_string ("message", txt_msg);
+ if (err_msg)
+ write_control_string ("error", err_msg);
+ write_control_bool ("echo", msg[i]->msg_style != PAM_PROMPT_ECHO_OFF);
+ write_control_end ();
+
+ if (err_msg)
+ {
+ free (err_msg);
+ err_msg = NULL;
+ }
+
+ if (txt_msg)
+ {
+ free (txt_msg);
+ txt_msg = NULL;
+ }
+
+ authorization = read_authorize_response (msg[i]->msg);
+ prompt_resp = cockpit_authorize_parse_x_conversation (authorization, NULL);
+
+ debug ("got prompt response");
+ if (prompt_resp)
+ {
+ resp[i].resp = prompt_resp;
+ resp[i].resp_retcode = 0;
+ prompt_resp = NULL;
+ }
+ else
+ {
+ success = 0;
+ }
+
+ if (authorization)
+ cockpit_memory_clear (authorization, -1);
+ free (authorization);
+ }
+ }
+
+ if (!success)
+ {
+ for (i = 0; i < num_msg; i++)
+ free (resp[i].resp);
+ free (resp);
+ return PAM_CONV_ERR;
+ }
+
+ if (err_msg)
+ last_err_msg = err_msg;
+ if (txt_msg)
+ last_txt_msg = txt_msg;
+
+ *ret_resp = resp;
+ return PAM_SUCCESS;
+}
+
+static int
+open_session (pam_handle_t *pamh)
+{
+ const char *name;
+ int res;
+ static struct passwd pwd_buf;
+ static char pwd_string_buf[8192];
+ static char home_env_buf[8192];
+ int i;
+
+ name = NULL;
+ pwd = NULL;
+
+ res = pam_get_item (pamh, PAM_USER, (const void **)&name);
+ if (res != PAM_SUCCESS)
+ {
+ warnx ("couldn't load user from pam");
+ return res;
+ }
+
+ res = getpwnam_r (name, &pwd_buf, pwd_string_buf, sizeof (pwd_string_buf), &pwd);
+ if (pwd == NULL)
+ {
+ warnx ("couldn't load user info for: %s: %s", name,
+ res == 0 ? "not found" : strerror (res));
+ return PAM_SYSTEM_ERR;
+ }
+
+ if (pwd->pw_shell == NULL)
+ {
+ warnx ("user %s has no shell", name);
+ return PAM_SYSTEM_ERR;
+ }
+
+ /*
+ * If we're already running as the right user, and have authenticated
+ * then skip starting a new session. This is used when testing, or
+ * running as your own user.
+ */
+
+ want_session = !(geteuid () != 0 &&
+ geteuid () == pwd->pw_uid &&
+ getuid () == pwd->pw_uid &&
+ getegid () == pwd->pw_gid &&
+ getgid () == pwd->pw_gid);
+
+ if (want_session)
+ {
+ debug ("checking access for %s", name);
+ res = pam_acct_mgmt (pamh, 0);
+ if (res == PAM_NEW_AUTHTOK_REQD)
+ {
+ warnx ("user account or password has expired: %s: %s", name, pam_strerror (pamh, res));
+
+ /*
+ * Certain PAM implementations return PAM_AUTHTOK_ERR if the users input does not
+ * match criteria. Let the conversation happen three times in that case.
+ */
+ for (i = 0; i < 3; i++) {
+ res = pam_chauthtok (pamh, PAM_CHANGE_EXPIRED_AUTHTOK);
+ if (res != PAM_SUCCESS)
+ warnx ("unable to change expired account or password: %s: %s", name, pam_strerror (pamh, res));
+ if (res != PAM_AUTHTOK_ERR)
+ break;
+ }
+ }
+ else if (res != PAM_SUCCESS)
+ {
+ warnx ("user account access failed: %d %s: %s", res, name, pam_strerror (pamh, res));
+ }
+
+ if (res != PAM_SUCCESS)
+ {
+ /* We change PAM_AUTH_ERR to PAM_PERM_DENIED so that we can
+ * distinguish between failures here and in *
+ * pam_authenticate.
+ */
+ if (res == PAM_AUTH_ERR)
+ res = PAM_PERM_DENIED;
+
+ return res;
+ }
+
+ debug ("opening pam session for %s", name);
+
+ res = snprintf (home_env_buf, sizeof (home_env_buf), "HOME=%s", pwd->pw_dir);
+ /* this really can't fail, as the buffer for the entire pwd is not larger, but make double sure */
+ assert (res < sizeof (home_env_buf));
+
+ pam_putenv (pamh, "XDG_SESSION_CLASS=user");
+ pam_putenv (pamh, "XDG_SESSION_TYPE=web");
+ pam_putenv (pamh, home_env_buf);
+
+ res = pam_setcred (pamh, PAM_ESTABLISH_CRED);
+ if (res != PAM_SUCCESS)
+ {
+ warnx ("establishing credentials failed: %s: %s", name, pam_strerror (pamh, res));
+ return res;
+ }
+
+ res = pam_open_session (pamh, 0);
+ if (res != PAM_SUCCESS)
+ {
+ warnx ("couldn't open session: %s: %s", name, pam_strerror (pamh, res));
+ return res;
+ }
+
+ res = pam_setcred (pamh, PAM_REINITIALIZE_CRED);
+ if (res != PAM_SUCCESS)
+ {
+ warnx ("reinitializing credentials failed: %s: %s", name, pam_strerror (pamh, res));
+ return res;
+ }
+ }
+
+ return PAM_SUCCESS;
+}
+
+__attribute__((__noreturn__)) static void
+exit_init_problem (int result_code)
+{
+ const char *problem = NULL;
+ const char *message = NULL;
+ char *payload = NULL;
+
+ assert (result_code != PAM_SUCCESS);
+
+ debug ("writing init problem %d", result_code);
+
+ if (result_code == PAM_AUTH_ERR || result_code == PAM_USER_UNKNOWN)
+ problem = "authentication-failed";
+ else if (result_code == PAM_PERM_DENIED)
+ problem = "access-denied";
+ else if (result_code == PAM_AUTHINFO_UNAVAIL)
+ problem = "authentication-unavailable";
+ else
+ problem = "internal-error";
+
+ if (last_err_msg)
+ message = last_err_msg;
+ else
+ message = pam_strerror (NULL, result_code);
+
+ if (asprintf (&payload, "\n{\"command\":\"init\",\"version\":1,\"problem\":\"%s\",\"message\":\"%s\"}",
+ problem, message) < 0)
+ errx (EX, "couldn't allocate memory for message");
+
+ if (cockpit_frame_write (STDOUT_FILENO, (unsigned char *)payload, strlen (payload)) < 0)
+ err (EX, "couldn't write init message");
+
+ free (payload);
+ exit (5);
+}
+
+static pam_handle_t *
+perform_basic (const char *rhost,
+ const char *authorization)
+{
+ struct pam_conv conv = { pam_conv_func, };
+ pam_handle_t *pamh;
+ char *password = NULL;
+ char *user = NULL;
+ int res;
+
+
+ debug ("basic authentication");
+
+ /* The input should be a user:password */
+ password = cockpit_authorize_parse_basic (authorization, &user);
+ if (password == NULL)
+ {
+ debug ("bad basic auth input");
+ exit_init_problem (PAM_BUF_ERR);
+ }
+
+ conv.appdata_ptr = &password;
+
+ res = pam_start ("cockpit", user, &conv, &pamh);
+ if (res != PAM_SUCCESS)
+ errx (EX, "couldn't start pam: %s", pam_strerror (NULL, res));
+
+ if (pam_set_item (pamh, PAM_RHOST, rhost) != PAM_SUCCESS)
+ errx (EX, "couldn't setup pam");
+
+ debug ("authenticating");
+
+ res = pam_authenticate (pamh, 0);
+ if (res == PAM_SUCCESS)
+ res = open_session (pamh);
+ else
+ btmp_log (user, rhost);
+
+ free (user);
+ if (password)
+ {
+ cockpit_memory_clear (password, strlen (password));
+ free (password);
+ }
+
+ /* Our exit code is a PAM code */
+ if (res != PAM_SUCCESS)
+ exit_init_problem (res);
+
+ return pamh;
+}
+
+static char *
+map_gssapi_to_local (gss_name_t name,
+ gss_OID mech_type)
+{
+ gss_buffer_desc local = GSS_C_EMPTY_BUFFER;
+ gss_buffer_desc display = GSS_C_EMPTY_BUFFER;
+ OM_uint32 major, minor;
+ char *str = NULL;
+
+ major = gss_localname (&minor, name, mech_type, &local);
+ if (major == GSS_S_COMPLETE)
+ {
+ minor = 0;
+ str = strndupx (local.value, local.length); /* user names are not allowed to contain \0 */
+ if (getpwnam (str))
+ {
+ debug ("mapped gssapi name to local user '%s'", str);
+ }
+ else
+ {
+ debug ("ignoring non-existent gssapi local user '%s'", str);
+
+ /* If the local user doesn't exist, pretend gss_localname() failed */
+ free (str);
+ str = NULL;
+ major = GSS_S_FAILURE;
+ minor = KRB5_NO_LOCALNAME;
+ }
+ }
+
+ /* Try a more pragmatic approach */
+ if (!str)
+ {
+ if (minor == (OM_uint32)KRB5_NO_LOCALNAME ||
+ minor == (OM_uint32)KRB5_LNAME_NOTRANS ||
+ minor == (OM_uint32)ENOENT)
+ {
+ major = gss_display_name (&minor, name, &display, NULL);
+ if (GSS_ERROR (major))
+ {
+ warnx ("couldn't get gssapi display name: %s", gssapi_strerror (mech_type, major, minor));
+ }
+ else
+ {
+ assert (display.value != NULL);
+ str = strndupx (display.value, display.length); /* display names are not allowed to contain \0 */
+ if (getpwnam (str))
+ {
+ debug ("no local user mapping for gssapi name '%s'", str);
+ }
+ else
+ {
+ warnx ("non-existent local user '%s'", str);
+ free (str);
+ str = NULL;
+ }
+ }
+ }
+ else
+ {
+ warnx ("couldn't map gssapi name to local user: %s", gssapi_strerror (mech_type, major, minor));
+ }
+ }
+
+ if (display.value)
+ gss_release_buffer (&minor, &display);
+ if (local.value)
+ gss_release_buffer (&minor, &local);
+
+ return str;
+}
+
+static bool
+acquire_service_credentials (gss_OID mech_type, gss_cred_usage_t usage, gss_cred_id_t *cred)
+{
+ /* custom credential store with our cockpit keytab */
+ gss_key_value_element_desc store_elements[] = {
+ { .key = (usage == GSS_C_INITIATE) ? "client_keytab" : "keytab", .value = COCKPIT_KTAB, },
+ { .key = "ccache", .value = "MEMORY:", }
+ };
+ const gss_key_value_set_desc cockpit_ktab_store = { .count = 2, .elements = store_elements };
+ OM_uint32 major, minor;
+
+ debug ("acquiring cockpit service credentials");
+ major = gss_acquire_cred_from (&minor, GSS_C_NO_NAME, GSS_C_INDEFINITE, GSS_C_NO_OID_SET, usage,
+ (!getenv ("COCKPIT_TEST_KEEP_KTAB") && access (COCKPIT_KTAB, F_OK) == 0) ? &cockpit_ktab_store : NULL,
+ cred, NULL, NULL);
+
+ if (GSS_ERROR (major))
+ {
+ const char *msg = gssapi_strerror (mech_type, major, minor);
+ /* don't litter journal with error message if keytab was not set up, as that's expected */
+ /* older krb versions hide the interesting bits behind the generic GSS_S_FAILURE and an uncomparable
+ * minor code, so do string comparison for these */
+ if (major != GSS_S_NO_CRED && !strstr (msg, "nonexistent or empty") && !strstr (msg, "No Kerberos credentials available"))
+ warnx ("couldn't acquire server credentials: %o %s", major, msg);
+ return false;
+ }
+
+ return true;
+}
+
+static pam_handle_t *
+perform_gssapi (const char *rhost,
+ const char *authorization)
+{
+ struct pam_conv conv = { pam_conv_func, };
+ OM_uint32 major, minor;
+ gss_cred_id_t client = GSS_C_NO_CREDENTIAL;
+ gss_cred_id_t server = GSS_C_NO_CREDENTIAL;
+ gss_buffer_desc input = GSS_C_EMPTY_BUFFER;
+ gss_buffer_desc output = GSS_C_EMPTY_BUFFER;
+ gss_buffer_desc export = GSS_C_EMPTY_BUFFER;
+ gss_name_t name = GSS_C_NO_NAME;
+ gss_ctx_id_t context = GSS_C_NO_CONTEXT;
+ gss_OID mech_type = GSS_C_NO_OID;
+ pam_handle_t *pamh = NULL;
+ char *response = NULL;
+ char *challenge;
+ OM_uint32 flags = 0;
+ char *str = NULL;
+ OM_uint32 caps = 0;
+ int res;
+
+ res = PAM_AUTH_ERR;
+
+ debug ("reading kerberos auth from cockpit-ws");
+ input.value = cockpit_authorize_parse_negotiate (authorization, &input.length);
+
+ if (!acquire_service_credentials (mech_type, GSS_C_ACCEPT, &server))
+ {
+ res = PAM_AUTHINFO_UNAVAIL;
+ goto out;
+ }
+
+ for (;;)
+ {
+ debug ("gssapi negotiation");
+
+ if (client != GSS_C_NO_CREDENTIAL)
+ gss_release_cred (&minor, &client);
+ if (name != GSS_C_NO_NAME)
+ gss_release_name (&minor, &name);
+ if (output.value)
+ gss_release_buffer (&minor, &output);
+
+ if (input.length > 0)
+ {
+ major = gss_accept_sec_context (&minor, &context, server, &input,
+ GSS_C_NO_CHANNEL_BINDINGS, &name, &mech_type,
+ &output, &flags, &caps, &client);
+ }
+ else
+ {
+ debug ("initial gssapi negotiate output");
+ major = GSS_S_CONTINUE_NEEDED;
+ }
+
+ /* Our exit code is a PAM result code */
+ if (GSS_ERROR (major))
+ {
+ res = PAM_AUTH_ERR;
+ warnx ("gssapi auth failed: %s", gssapi_strerror (mech_type, major, minor));
+ goto out;
+ }
+
+ if ((major & GSS_S_CONTINUE_NEEDED) == 0)
+ break;
+
+ challenge = cockpit_authorize_build_negotiate (output.value, output.length);
+ if (!challenge)
+ errx (EX, "couldn't encode negotiate challenge");
+ write_authorize_begin ();
+ write_control_string ("challenge", challenge);
+ write_control_end ();
+ cockpit_memory_clear (challenge, -1);
+ free (challenge);
+
+ /*
+ * The GSSAPI mechanism can require multiple challenge response
+ * iterations ... so do that here.
+ */
+ free (input.value);
+ input.length = 0;
+
+ debug ("need to continue gssapi negotiation");
+ response = read_authorize_response ("negotiate");
+ input.value = cockpit_authorize_parse_negotiate (response, &input.length);
+ if (response)
+ cockpit_memory_clear (response, -1);
+ free (response);
+ }
+
+ str = map_gssapi_to_local (name, mech_type);
+ if (!str)
+ goto out;
+
+ res = pam_start ("cockpit", str, &conv, &pamh);
+
+ if (res != PAM_SUCCESS)
+ errx (EX, "couldn't start pam: %s", pam_strerror (NULL, res));
+ if (pam_set_item (pamh, PAM_RHOST, rhost) != PAM_SUCCESS)
+ errx (EX, "couldn't setup pam");
+
+ res = open_session (pamh);
+ if (res != PAM_SUCCESS)
+ {
+ btmp_log (str, rhost);
+ goto out;
+ }
+
+ /* The creds are used and cleaned up later */
+ creds = client;
+
+out:
+ if (output.value)
+ gss_release_buffer (&minor, &output);
+ if (export.value)
+ gss_release_buffer (&minor, &export);
+ if (server != GSS_C_NO_CREDENTIAL)
+ gss_release_cred (&minor, &server);
+ if (name != GSS_C_NO_NAME)
+ gss_release_name (&minor, &name);
+ if (context != GSS_C_NO_CONTEXT)
+ gss_delete_sec_context (&minor, &context, GSS_C_NO_BUFFER);
+ free (input.value);
+ free (str);
+
+ if (res != PAM_SUCCESS)
+ exit_init_problem (res);
+
+ return pamh;
+}
+
+static int
+pam_conv_func_dummy (int num_msg,
+ const struct pam_message **msg,
+ struct pam_response **ret_resp,
+ void *appdata_ptr)
+{
+ /* we don't expect (nor can handle) any actual auth conversation here, but
+ * PAM sometimes sends messages like "Creating home directory for USER" */
+ for (int i = 0; i < num_msg; ++i)
+ debug ("got PAM conversation message, ignoring: %s", msg[i]->msg);
+ return PAM_CONV_ERR;
+}
+
+static void
+create_s4u_ticket (const char *username)
+{
+ OM_uint32 major, minor;
+ gss_cred_id_t server_cred = GSS_C_NO_CREDENTIAL;
+ gss_name_t impersonee = GSS_C_NO_NAME;
+
+ debug ("Attempting to create an S4U ticket for user %s", username);
+
+ if (!acquire_service_credentials (GSS_C_NO_OID, GSS_C_INITIATE, &server_cred))
+ goto out;
+
+ gss_buffer_desc user_buf = { .length = strlen (username), .value = (char *) username };
+ major = gss_import_name (&minor, &user_buf, GSS_KRB5_NT_PRINCIPAL_NAME, &impersonee);
+ if (GSS_ERROR (major))
+ {
+ warnx ("Failed to import user name %s: %s", username, gssapi_strerror (GSS_C_NO_OID, major, minor));
+ goto out;
+ }
+
+ /* store credentials into global creds; they will be put into the session and cleaned up in main() */
+ major = gss_acquire_cred_impersonate_name (&minor, server_cred, impersonee,
+ GSS_C_INDEFINITE, GSS_C_NO_OID_SET, GSS_C_INITIATE,
+ &creds, NULL, NULL);
+ if (GSS_ERROR (major))
+ {
+ warnx ("Failed to impersonate %s: %s", username, gssapi_strerror (GSS_C_NO_OID, major, minor));
+ goto out;
+ }
+
+ debug ("S4U ticket for user %s created successfully", username);
+
+out:
+ if (server_cred != GSS_C_NO_CREDENTIAL)
+ gss_release_cred (&minor, &server_cred);
+ if (impersonee != GSS_C_NO_NAME)
+ gss_release_name(&minor, &impersonee);
+}
+
+static pam_handle_t *
+perform_tlscert (const char *rhost,
+ const char *authorization)
+{
+ struct pam_conv conv = { pam_conv_func_dummy, };
+ pam_handle_t *pamh;
+ int res;
+
+ debug ("start tls-cert authentication for cockpit-ws %u", getppid ());
+
+ /* True, otherwise we wouldn't be here. */
+ assert (strncmp (authorization, "tls-cert ", 9) == 0);
+ const char *client_certificate_filename = authorization + 9;
+
+ char *username = cockpit_session_client_certificate_map_user (client_certificate_filename);
+ if (username == NULL)
+ exit_init_problem (PAM_AUTH_ERR);
+
+ res = pam_start ("cockpit", username, &conv, &pamh);
+ if (res != PAM_SUCCESS)
+ errx (EX, "couldn't start pam: %s", pam_strerror (NULL, res));
+
+ if (pam_set_item (pamh, PAM_RHOST, rhost) != PAM_SUCCESS)
+ errx (EX, "couldn't setup pam rhost");
+
+ res = open_session (pamh);
+
+ create_s4u_ticket (username);
+
+ free (username);
+
+ /* Our exit code is a PAM code */
+ if (res != PAM_SUCCESS)
+ exit_init_problem (res);
+
+ return pamh;
+}
+
+
+/* Return path of ccache file (including FILE: prefix), clean this up at session end */
+static char *
+store_krb_credentials (gss_cred_id_t creds, uid_t uid, gid_t gid)
+{
+ gss_key_value_set_desc store;
+ struct gss_key_value_element_struct element;
+ OM_uint32 major, minor;
+ char *ccache;
+
+ assert (creds != GSS_C_NO_CREDENTIAL);
+
+ bool was_root = getuid() == 0;
+
+ /* We want to do this before the fork(), so we need to temporarily
+ * change our euid/egid
+ */
+ if (setresgid (gid, gid, -1) != 0 || setresuid (uid, uid, -1) != 0)
+ err (127, "Unable to temporarily drop permissions to store gss credentials");
+
+ assert (geteuid () == uid && getegid() == gid);
+
+ /* The ccache path needs to be unique per cockpit session; as cockpit-session runs throughout the
+ * lifetime of sessions, our pid is unique. We expect the cache to be cleaned up at the end, but
+ * if not, and the PID gets recycled, this just overwrites the old obsolete one, which is good. */
+ asprintfx (&ccache, "FILE:/run/user/%u/cockpit-session-%u.ccache", uid, getpid ());
+ debug ("storing kerberos credentials in session: %s", ccache);
+
+ store.count = 1;
+ store.elements = &element;
+ element.key = "ccache";
+ element.value = ccache;
+
+ major = gss_store_cred_into (&minor, creds, GSS_C_INITIATE, GSS_C_NULL_OID, 1, 1, &store, NULL, NULL);
+ if (GSS_ERROR (major))
+ warnx ("couldn't store gssapi credentials: %s", gssapi_strerror (GSS_C_NO_OID, major, minor));
+
+ if (was_root && (setresuid (0, 0, 0) != 0 || setresgid (0, 0, 0) != 0))
+ err (127, "Unable to restore permissions after storing gss credentials");
+
+ return ccache;
+}
+
+static void
+release_krb_credentials (char *ccache)
+{
+ /* strip off FILE: prefix for deleting */
+ assert (strncmp (ccache, "FILE:", 5) == 0);
+ if (unlink (ccache + 5) != 0)
+ warn ("couldn't clean up kerberos ticket cache %s", ccache);
+ free (ccache);
+}
+
+static bool
+user_has_valid_login_shell (const char **envp)
+{
+ /* <lis> >>> random.randint(0,127)
+ * <lis> 71
+ * <pitti> https://xkcd.com/221/
+ */
+ const char *argv[] = { pwd->pw_shell, "-c", "exit 71;", NULL };
+ assert (argv[0] != NULL);
+
+ int devnull = open ("/dev/null", O_RDONLY);
+ if (devnull < 0)
+ err (EX, "couldn't open /dev/null");
+
+ const int remap_fds[] = { devnull, 2, -1 }; /* send stdout to stderr */
+ int wstatus;
+
+ wstatus = spawn_and_wait (argv, envp, remap_fds, 3, pwd->pw_uid, pwd->pw_gid);
+ debug ("user_has_valid_login_shell: exited with status %x", wstatus);
+ close (devnull);
+ return WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 71;
+}
+
+static void
+save_environment (void)
+{
+ const char *value;
+ int i, j;
+
+ /* Force save our default path */
+ if (!getenv ("COCKPIT_TEST_KEEP_PATH"))
+ setenv ("PATH", DEFAULT_SESSION_PATH, 1);
+
+ for (i = 0, j = 0; env_names[i] != NULL; i++)
+ {
+ value = getenv (env_names[i]);
+ if (value)
+ {
+ if (asprintf (env_saved + (j++), "%s=%s", env_names[i], value) < 0)
+ errx (42, "couldn't allocate environment");
+ }
+ }
+
+ env_saved[j] = NULL;
+}
+
+static void
+pass_to_child (int signo)
+{
+ if (child > 0)
+ kill (child, signo);
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ pam_handle_t *pamh = NULL;
+ OM_uint32 minor;
+ const char *rhost;
+ char *authorization;
+ char *type = NULL;
+ const char **env;
+ char *ccache = NULL;
+ int status;
+ int res;
+ int i;
+
+ if (isatty (0))
+ errx (2, "this command is not meant to be run from the console");
+
+ /* COMPAT: argv[1] used to be used, but is now ignored */
+ if (argc != 1 && argc != 2)
+ errx (2, "invalid arguments to cockpit-session");
+
+ program_name = basename (argv[0]);
+
+ rhost = getenv ("COCKPIT_REMOTE_PEER") ?: "";
+
+ save_environment ();
+
+ /* When setuid root, make sure our group is also root */
+ if (geteuid () == 0)
+ {
+ /* Always clear the environment */
+ if (clearenv () != 0)
+ err (1, "couldn't clear environment");
+
+ /* set a minimal environment */
+ setenv ("PATH", DEFAULT_SESSION_PATH, 1);
+
+ if (setgid (0) != 0 || setuid (0) != 0)
+ err (1, "couldn't switch permissions correctly");
+ }
+
+ signal (SIGALRM, SIG_DFL);
+ signal (SIGQUIT, SIG_DFL);
+
+ cockpit_authorize_logger (authorize_logger, DEBUG_SESSION);
+
+ /* Request authorization header */
+ write_authorize_begin ();
+ write_control_string ("challenge", "*");
+ write_control_end ();
+
+ /* And get back the authorization header */
+ authorization = read_authorize_response ("authorization");
+ if (!cockpit_authorize_type (authorization, &type))
+ errx (EX, "invalid authorization header received");
+
+ if (strcmp (type, "basic") == 0)
+ pamh = perform_basic (rhost, authorization);
+ else if (strcmp (type, "negotiate") == 0)
+ pamh = perform_gssapi (rhost, authorization);
+ else if (strcmp (type, "tls-cert") == 0)
+ pamh = perform_tlscert (rhost, authorization);
+
+ cockpit_memory_clear (authorization, -1);
+ free (authorization);
+
+ if (!pamh)
+ errx (2, "unrecognized authentication method: %s", type);
+
+ free (type);
+
+ for (i = 0; env_saved[i] != NULL; i++)
+ pam_putenv (pamh, env_saved[i]);
+
+ if (want_session) /* no session → no login messages or XDG_RUNTIME_DIR → no memfd or session ccache */
+ {
+ if (pam_putenv (pamh, "COCKPIT_LOGIN_MESSAGES_MEMFD=3") != PAM_SUCCESS)
+ errx (EX, "Failed to set COCKPIT_LOGIN_MESSAGES_MEMFD=3 in PAM environment");
+
+ if (creds != GSS_C_NO_CREDENTIAL)
+ {
+ ccache = store_krb_credentials (creds, pwd->pw_uid, pwd->pw_gid);
+ char *ccache_env = NULL;
+ asprintfx (&ccache_env, "KRB5CCNAME=%s", ccache);
+
+ if (pam_putenv (pamh, ccache_env) != PAM_SUCCESS)
+ errx (EX, "Failed to set KRB5CCNAME in PAM environment");
+ free (ccache_env);
+ }
+ }
+
+ env = (const char **) pam_getenvlist (pamh);
+ if (env == NULL)
+ errx (EX, "get pam environment failed");
+
+ const char *bridge_argv[] = { "cockpit-bridge", NULL };
+
+ if (want_session)
+ {
+ assert (pwd != NULL);
+
+ if (initgroups (pwd->pw_name, pwd->pw_gid) < 0)
+ err (EX, "%s: can't init groups", pwd->pw_name);
+
+ if (!user_has_valid_login_shell (env))
+ exit_init_problem (PAM_PERM_DENIED);
+
+ signal (SIGTERM, pass_to_child);
+ signal (SIGINT, pass_to_child);
+ signal (SIGQUIT, pass_to_child);
+ signal (SIGHUP, pass_to_child);
+
+ FILE *login_messages = cockpit_json_print_open_memfd ("cockpit login messages", 1);
+
+ utmp_log (1, rhost, login_messages);
+
+ int login_messages_fd = cockpit_json_print_finish_memfd (&login_messages);
+
+ const int remap_fds[] = { -1, -1, -1, login_messages_fd };
+ status = spawn_and_wait (bridge_argv, env, remap_fds, 4, pwd->pw_uid, pwd->pw_gid);
+
+ utmp_log (0, rhost, NULL);
+
+ signal (SIGTERM, SIG_DFL);
+ signal (SIGINT, SIG_DFL);
+ signal (SIGQUIT, SIG_DFL);
+ signal (SIGHUP, SIG_DFL);
+
+ close (login_messages_fd);
+
+ res = pam_setcred (pamh, PAM_DELETE_CRED);
+ if (res != PAM_SUCCESS)
+ err (EX, "%s: couldn't delete creds: %s", pwd->pw_name, pam_strerror (pamh, res));
+ res = pam_close_session (pamh, 0);
+ if (res != PAM_SUCCESS)
+ err (EX, "%s: couldn't close session: %s", pwd->pw_name, pam_strerror (pamh, res));
+ if (ccache)
+ release_krb_credentials (ccache);
+ }
+ else
+ {
+ status = spawn_and_wait (bridge_argv, env, NULL, -1, pwd->pw_uid, pwd->pw_gid);
+ }
+
+ pam_end (pamh, PAM_SUCCESS);
+
+ free (last_err_msg);
+ last_err_msg = NULL;
+ free (last_txt_msg);
+ last_txt_msg = NULL;
+ free (conversation);
+ conversation = NULL;
+
+ if (creds != GSS_C_NO_CREDENTIAL)
+ gss_release_cred (&minor, &creds);
+
+ if (WIFEXITED(status))
+ exit (WEXITSTATUS(status));
+ else if (WIFSIGNALED(status))
+ raise (WTERMSIG(status));
+ else
+ exit (EX);
+}
diff --git a/src/ssh/Makefile-ssh.am b/src/ssh/Makefile-ssh.am
new file mode 100644
index 0000000..973f6e7
--- /dev/null
+++ b/src/ssh/Makefile-ssh.am
@@ -0,0 +1,86 @@
+if WITH_COCKPIT_SSH
+
+# -----------------------------------------------------------------------------
+# libcockpit-ssh.a: code used in cockpit-ssh and its tests
+
+noinst_LIBRARIES += libcockpit-ssh.a
+
+libcockpit_ssh_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-ssh\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(libssh_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libcockpit_ssh_a_LIBS = \
+ libcockpit-ssh.a \
+ $(libcockpit_common_a_LIBS) \
+ $(libssh_LIBS) \
+ $(NULL)
+
+libcockpit_ssh_a_SOURCES = \
+ src/ssh/cockpitsshoptions.c \
+ src/ssh/cockpitsshoptions.h \
+ src/ssh/cockpitsshrelay.h \
+ src/ssh/cockpitsshrelay.c \
+ $(NULL)
+
+# -----------------------------------------------------------------------------
+# cockpit-ssh
+
+libexec_PROGRAMS += cockpit-ssh
+cockpit_ssh_CPPFLAGS = $(libcockpit_ssh_a_CPPFLAGS)
+cockpit_ssh_LDADD = $(libcockpit_ssh_a_LIBS)
+cockpit_ssh_SOURCES = src/ssh/ssh.c
+
+# -----------------------------------------------------------------------------
+# C bridge config; Python bridge handles it internally
+
+if WITH_OLD_BRIDGE
+sshmanifestdir = $(datadir)/cockpit/ssh
+dist_sshmanifest_DATA = src/ssh/manifest.json
+endif
+
+# -----------------------------------------------------------------------------
+# mock-ssh
+
+check_PROGRAMS += mock-sshd
+mock_sshd_CPPFLAGS = $(libcockpit_ssh_a_CPPFLAGS) $(TEST_CPP)
+mock_sshd_LDADD = $(libcockpit_ssh_a_LIBS) $(TEST_LIBS)
+mock_sshd_SOURCES = src/ssh/mock-sshd.c
+
+# -----------------------------------------------------------------------------
+# Unit tests
+
+dist_check_DATA += \
+ src/ssh/mock_rsa_key \
+ src/ssh/mock_ecdsa_key \
+ src/ssh/test_rsa \
+ src/ssh/test_rsa.pub \
+ src/ssh/mock_known_hosts \
+ src/ssh/mock-pid-cat \
+ src/ssh/mock-config \
+ src/ssh/invalid_known_hosts \
+ $(NULL)
+
+TEST_PROGRAM += test-sshbridge
+test_sshbridge_CPPFLAGS = $(libcockpit_ssh_a_CPPFLAGS) $(TEST_CPP)
+test_sshbridge_LDADD = $(libcockpit_ssh_a_LIBS) $(TEST_LIBS)
+test_sshbridge_SOURCES = src/ssh/test-sshbridge.c
+
+TEST_PROGRAM += test-sshoptions
+test_sshoptions_CPPFLAGS = $(libcockpit_ssh_a_CPPFLAGS) $(TEST_CPP)
+test_sshoptions_LDADD = $(libcockpit_ssh_a_LIBS) $(TEST_LIBS)
+test_sshoptions_SOURCES = src/ssh/test-sshoptions.c
+
+check_DATA += test_rsa_key
+CLEANFILES += test_rsa_key
+test_rsa_key: src/ssh/test_rsa
+ $(AM_V_GEN) cp $< $@ && chmod 600 $@
+
+update-known-hosts:
+ cat $(srcdir)/src/ssh/mock_*.pub | \
+ sed -ne 's/\(.*\) [^ ]\+$$/[localhost]:*,[127.0.0.1]:* \1/p' > \
+ $(srcdir)/src/ssh/mock_known_hosts
+
+endif
diff --git a/src/ssh/cockpitsshoptions.c b/src/ssh/cockpitsshoptions.c
new file mode 100644
index 0000000..44faf1e
--- /dev/null
+++ b/src/ssh/cockpitsshoptions.c
@@ -0,0 +1,127 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitconf.h"
+
+#include "cockpitsshoptions.h"
+
+static const gchar *default_command = "cockpit-bridge";
+
+static gboolean
+has_environment_val (gchar **env,
+ const gchar *name)
+{
+ const gchar *v = g_environ_getenv (env, name);
+ return v != NULL && v[0] != '\0';
+}
+
+static const gchar *
+get_environment_val (gchar **env,
+ const gchar *name,
+ const gchar *defawlt)
+{
+ if (has_environment_val (env, name))
+ return g_environ_getenv (env, name);
+ else
+ return defawlt;
+}
+
+static gchar **
+set_environment_val (gchar **env,
+ const gchar *name,
+ const gchar *val)
+{
+ return g_environ_setenv (env, name, val ? val : "", TRUE);
+}
+
+static gboolean
+get_environment_bool (gchar **env,
+ const gchar *name,
+ gboolean defawlt)
+{
+ const gchar *value = get_environment_val (env, name, NULL);
+
+ if (!value)
+ return defawlt;
+
+ return g_strcmp0 (value, "yes") == 0 ||
+ g_strcmp0 (value, "true") == 0 ||
+ g_strcmp0 (value, "1") == 0;
+}
+
+static gchar **
+set_environment_bool (gchar **env,
+ const gchar *name,
+ gboolean val)
+{
+ return g_environ_setenv (env, name, val ? "1" : "", TRUE);
+}
+
+static gboolean
+get_connect_to_unknown_hosts (gchar **env)
+{
+ /* Fallback to deprecated allowUnknown option; use cockpit_conf_string() to
+ * test for existence as _bool() cannot tell apart "unset" from "set to false". */
+ if (!cockpit_conf_string (COCKPIT_CONF_SSH_SECTION, "connectToUnknownHosts") &&
+ cockpit_conf_bool (COCKPIT_CONF_SSH_SECTION, "allowUnknown", FALSE))
+ return TRUE;
+
+ if (cockpit_conf_bool (COCKPIT_CONF_SSH_SECTION, "connectToUnknownHosts", FALSE))
+ return TRUE;
+
+ if (!has_environment_val (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS"))
+ return get_environment_bool (env, "COCKPIT_SSH_ALLOW_UNKNOWN", FALSE);
+ return get_environment_bool (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS", FALSE);
+}
+
+CockpitSshOptions *
+cockpit_ssh_options_from_env (gchar **env)
+{
+
+ CockpitSshOptions *options = g_new0 (CockpitSshOptions, 1);
+ options->knownhosts_file = get_environment_val (env, "COCKPIT_SSH_KNOWN_HOSTS_FILE", NULL);
+ options->command = get_environment_val (env, "COCKPIT_SSH_BRIDGE_COMMAND", default_command);
+ options->remote_peer = get_environment_val (env, "COCKPIT_REMOTE_PEER", "localhost");
+ options->connect_to_unknown_hosts = get_connect_to_unknown_hosts (env);
+
+ return options;
+}
+
+gchar **
+cockpit_ssh_options_to_env (CockpitSshOptions *options,
+ gchar **env)
+{
+ env = set_environment_bool (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS",
+ options->connect_to_unknown_hosts);
+ env = set_environment_val (env, "COCKPIT_SSH_KNOWN_HOSTS_FILE",
+ options->knownhosts_file);
+ env = set_environment_val (env, "COCKPIT_REMOTE_PEER",
+ options->remote_peer);
+
+ /* Don't reset these vars unless we have values for them */
+ if (options->command)
+ {
+ env = set_environment_val (env, "COCKPIT_SSH_BRIDGE_COMMAND",
+ options->command);
+ }
+
+ return env;
+}
diff --git a/src/ssh/cockpitsshoptions.h b/src/ssh/cockpitsshoptions.h
new file mode 100644
index 0000000..c96869e
--- /dev/null
+++ b/src/ssh/cockpitsshoptions.h
@@ -0,0 +1,41 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_SSH_OPTIONS_H__
+#define __COCKPIT_SSH_OPTIONS_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+typedef struct {
+ const gchar *knownhosts_file;
+ const gchar *command;
+ const gchar *remote_peer;
+ gboolean connect_to_unknown_hosts;
+} CockpitSshOptions;
+
+CockpitSshOptions * cockpit_ssh_options_from_env (gchar **env);
+
+gchar ** cockpit_ssh_options_to_env (CockpitSshOptions *options,
+ gchar **env);
+
+G_END_DECLS
+
+#endif
diff --git a/src/ssh/cockpitsshrelay.c b/src/ssh/cockpitsshrelay.c
new file mode 100644
index 0000000..8df6ef2
--- /dev/null
+++ b/src/ssh/cockpitsshrelay.c
@@ -0,0 +1,2303 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitauthorize.h"
+#include "common/cockpitconf.h"
+#include "common/cockpithex.h"
+#include "common/cockpitframe.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitmemory.h"
+#include "common/cockpitpipe.h"
+#include "common/cockpittransport.h"
+
+#include "cockpitsshrelay.h"
+#include "cockpitsshoptions.h"
+
+#include <libssh/libssh.h>
+#include <libssh/callbacks.h>
+
+#include <krb5/krb5.h>
+#include <gssapi/gssapi.h>
+#include <gssapi/gssapi_krb5.h>
+#include <gssapi/gssapi_ext.h>
+
+#include <glib/gstdio.h>
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <time.h>
+
+typedef struct {
+ const gchar *logname;
+ gchar *initial_auth_data;
+ gchar *auth_type;
+
+ gchar **env;
+ CockpitSshOptions *ssh_options;
+
+ gchar *username;
+ gboolean in_bridge;
+
+ ssh_session session;
+
+ gchar *conversation;
+
+ gchar *host_key;
+ gchar *host_fingerprint;
+ const gchar *host_key_type;
+ GHashTable *auth_results;
+ gchar *user_known_hosts;
+
+ gchar *problem_error;
+} CockpitSshData;
+
+static gchar *tmp_knownhost_file;
+
+static const gchar*
+exit_code_problem (int exit_code)
+{
+ switch (exit_code)
+ {
+ case 0:
+ return NULL;
+ case AUTHENTICATION_FAILED:
+ return "authentication-failed";
+ case DISCONNECTED:
+ return "disconnected";
+ case TERMINATED:
+ return "terminated";
+ case NO_COCKPIT:
+ return "no-cockpit";
+ default:
+ return "internal-error";
+ }
+}
+
+static const gchar *
+auth_method_description (int method)
+{
+ if (method == SSH_AUTH_METHOD_NONE)
+ return "none";
+ else if (method == SSH_AUTH_METHOD_PASSWORD || method == SSH_AUTH_METHOD_INTERACTIVE)
+ return "password";
+ else if (method == SSH_AUTH_METHOD_PUBLICKEY)
+ return "public-key";
+ else if (method == SSH_AUTH_METHOD_HOSTBASED)
+ return "host-based";
+ else if (method == SSH_AUTH_METHOD_GSSAPI_MIC)
+ return "gssapi-mic";
+ else
+ return "unknown";
+}
+
+static gchar *
+auth_methods_line (int methods)
+{
+ GString *string;
+ int i = 0;
+ int check[6] = {
+ SSH_AUTH_METHOD_NONE,
+ SSH_AUTH_METHOD_INTERACTIVE,
+ SSH_AUTH_METHOD_PASSWORD,
+ SSH_AUTH_METHOD_PUBLICKEY,
+ SSH_AUTH_METHOD_HOSTBASED,
+ SSH_AUTH_METHOD_GSSAPI_MIC
+ };
+
+ string = g_string_new ("");
+ for (i = 0; i < G_N_ELEMENTS (check); i++)
+ {
+ if (methods & check[i])
+ {
+ g_string_append (string, auth_method_description (check[i]));
+ g_string_append (string, " ");
+ }
+ }
+
+ return g_string_free (string, FALSE);
+}
+
+static gboolean
+ssh_msg_is_disconnected (const gchar *msg)
+{
+ return msg && (strstr (msg, "disconnected") ||
+ strstr (msg, "SSH_MSG_DISCONNECT") ||
+ strstr (msg, "Socket error: Success") ||
+ strstr (msg, "Socket error: Connection reset by peer"));
+}
+
+static gboolean
+write_control_message (int fd,
+ JsonObject *options)
+{
+ gboolean ret = TRUE;
+ gchar *payload;
+ gchar *prefixed;
+ gsize length;
+
+ payload = cockpit_json_write_object (options, &length);
+ prefixed = g_strdup_printf ("\n%s", payload);
+ if (cockpit_frame_write (fd, (unsigned char *)prefixed, length + 1) < 0)
+ {
+ g_message ("couldn't write control message: %s", g_strerror (errno));
+ ret = FALSE;
+ }
+ g_free (prefixed);
+ g_free (payload);
+
+ return ret;
+}
+
+static void
+byte_array_clear_and_free (gpointer data)
+{
+ GByteArray *buffer = data;
+ cockpit_memory_clear (buffer->data, buffer->len);
+ g_byte_array_free (buffer, TRUE);
+}
+
+static JsonObject *
+read_control_message (int fd)
+{
+ JsonObject *options = NULL;
+ GBytes *payload = NULL;
+ GBytes *bytes = NULL;
+ gchar *channel = NULL;
+ guchar *data = NULL;
+ gssize length = 0;
+
+ length = cockpit_frame_read (fd, &data);
+ if (length < 0)
+ {
+ g_message ("couldn't read control message: %s", g_strerror (errno));
+ length = 0;
+ }
+ else if (length > 0)
+ {
+ /* This could have a password, so clear it when freeing */
+ bytes = g_bytes_new_with_free_func (data, length, byte_array_clear_and_free,
+ g_byte_array_new_take (data, length));
+ payload = cockpit_transport_parse_frame (bytes, &channel);
+ data = NULL;
+ }
+
+ if (payload == NULL)
+ {
+ if (length > 0)
+ g_message ("cockpit-ssh did not receive valid message");
+ }
+ else if (channel != NULL)
+ {
+ g_message ("cockpit-ssh did not receive a control message");
+ }
+ else if (!cockpit_transport_parse_command (payload, NULL, NULL, &options))
+ {
+ g_message ("cockpit-ssh did not receive a valid control message");
+ }
+
+ g_free (channel);
+
+ if (bytes)
+ g_bytes_unref (bytes);
+ if (payload)
+ g_bytes_unref (payload);
+ free (data);
+ return options;
+}
+
+static void
+send_authorize_challenge (const gchar *challenge)
+{
+ gchar *cookie = NULL;
+ JsonObject *object = json_object_new ();
+
+ cookie = g_strdup_printf ("session%u%u",
+ (unsigned int)getpid(),
+ (unsigned int)time (NULL));
+ json_object_set_string_member (object, "command", "authorize");
+ json_object_set_string_member (object, "challenge", challenge);
+ json_object_set_string_member (object, "cookie", cookie);
+
+ write_control_message (STDOUT_FILENO, object);
+
+ g_free (cookie);
+ json_object_unref (object);
+}
+
+static gchar *
+challenge_for_auth_data (const gchar *challenge,
+ gchar **ret_type)
+{
+ const gchar *response = NULL;
+ const gchar *command;
+ gchar *ptr = NULL;
+ gchar *type = NULL;
+ JsonObject *reply;
+
+ send_authorize_challenge (challenge ? challenge : "*");
+ reply = read_control_message (STDIN_FILENO);
+ if (!reply)
+ goto out;
+
+ if (!cockpit_json_get_string (reply, "command", "", &command) ||
+ !g_str_equal (command, "authorize"))
+ {
+ g_message ("received \"%s\" control message instead of \"authorize\"", command);
+ }
+ else if (!cockpit_json_get_string (reply, "response", NULL, &response))
+ {
+ g_message ("received unexpected \"authorize\" control message: %s", response);
+ }
+
+ if (response)
+ cockpit_authorize_type (response, &type);
+
+out:
+ if (ret_type)
+ *ret_type = type;
+ else
+ g_free (type);
+
+ if (response && !g_str_equal (response, ""))
+ ptr = g_strdup (response);
+
+ if (reply)
+ json_object_unref (reply);
+ return ptr;
+}
+
+static gchar *
+challenge_for_knownhosts_data (CockpitSshData *data)
+{
+ const gchar *value = NULL;
+ gchar *ret = NULL;
+ gchar *response = NULL;
+
+ response = challenge_for_auth_data ("x-host-key", NULL);
+ if (response)
+ {
+ value = cockpit_authorize_type (response, NULL);
+ /* Legacy blank string means force fail */
+ if (value && value[0] == '\0')
+ ret = g_strdup ("* invalid key");
+ else
+ ret = g_strdup (value);
+ }
+
+
+ g_free (response);
+ return ret;
+}
+
+static gchar *
+prompt_with_authorize (CockpitSshData *data,
+ const gchar *prompt,
+ const gchar *msg,
+ const gchar *default_value,
+ const gchar *host_key,
+ gboolean echo)
+{
+ JsonObject *request = NULL;
+ JsonObject *reply = NULL;
+ const gchar *command = NULL;
+ const char *response = NULL;
+ char *challenge = NULL;
+ gchar *result = NULL;
+ gboolean ret;
+
+ challenge = cockpit_authorize_build_x_conversation (prompt, &data->conversation);
+ if (!challenge)
+ return NULL;
+
+ request = json_object_new ();
+ json_object_set_string_member (request, "command", "authorize");
+ json_object_set_string_member (request, "cookie", data->conversation);
+ json_object_set_string_member (request, "challenge", challenge);
+ cockpit_memory_clear (challenge, -1);
+ free (challenge);
+
+ if (msg)
+ json_object_set_string_member (request, "message", msg);
+ if (default_value)
+ json_object_set_string_member (request, "default", default_value);
+ if (host_key)
+ json_object_set_string_member (request, "host-key", host_key);
+
+ json_object_set_boolean_member (request, "echo", echo);
+
+ ret = write_control_message (STDOUT_FILENO, request);
+ json_object_unref (request);
+
+ if (!ret)
+ return NULL;
+
+ reply = read_control_message (STDIN_FILENO);
+ if (!reply)
+ return NULL;
+
+ if (!cockpit_json_get_string (reply, "command", "", &command) ||
+ !g_str_equal (command, "authorize"))
+ {
+ g_message ("received \"%s\" control message instead of \"authorize\"", command);
+ }
+ else if (!cockpit_json_get_string (reply, "response", "", &response))
+ {
+ g_message ("received unexpected \"authorize\" control message");
+ }
+ else if (!g_str_equal (response, ""))
+ {
+ result = cockpit_authorize_parse_x_conversation (response, NULL);
+ if (!result)
+ g_message ("received unexpected \"authorize\" control message \"response\"");
+ }
+
+ json_object_unref (reply);
+ return result;
+}
+
+static const gchar *
+prompt_for_host_key (CockpitSshData *data)
+{
+ const gchar *ret;
+ gchar *host = NULL;
+ guint port = 22;
+ gchar *message = NULL;
+ gchar *prompt = NULL;
+ gchar *reply = NULL;
+
+ if (ssh_options_get (data->session, SSH_OPTIONS_HOST, &host) < 0)
+ {
+ g_warning ("Failed to get host");
+ goto out;
+ }
+
+ if (ssh_options_get_port (data->session, &port) < 0)
+ {
+ g_warning ("Failed to get port");
+ goto out;
+ }
+
+ message = g_strdup_printf ("The authenticity of host '%s:%d' can't be established. Do you want to proceed this time?",
+ host, port);
+ prompt = g_strdup_printf ("SHA256 Fingerprint (%s):", data->host_key_type);
+
+ reply = prompt_with_authorize (data, prompt, message, data->host_fingerprint, data->host_key, TRUE);
+
+out:
+ if (g_strcmp0 (reply, data->host_fingerprint) == 0 || g_strcmp0 (reply, data->host_key) == 0)
+ ret = NULL;
+ else
+ ret = "unknown-hostkey";
+
+ g_free (reply);
+ g_free (message);
+ g_free (prompt);
+ g_free (host);
+ return ret;
+}
+
+static void cleanup_knownhosts_file (void)
+{
+ if (tmp_knownhost_file)
+ {
+ g_unlink (tmp_knownhost_file);
+ g_free (tmp_knownhost_file);
+ }
+}
+
+static gboolean
+write_tmp_knownhosts_file (CockpitSshData *data,
+ const gchar *content,
+ const gchar **problem)
+{
+ int fd;
+ g_autoptr(GError) error = NULL;
+
+ fd = g_file_open_tmp ("known-hosts.XXXXXX", &tmp_knownhost_file, &error);
+ if (fd < 0)
+ {
+ g_warning ("%s: couldn't open temporary known host file for data: %s",
+ data->logname, error->message);
+ *problem = "internal-error";
+ return FALSE;
+ }
+ /* now we own the file; let g_file_set_contents() do the safe writing, instead of bothering with a write() loop */
+ close (fd);
+
+ atexit (cleanup_knownhosts_file);
+
+ if (!g_file_set_contents (tmp_knownhost_file, content, -1, &error))
+ {
+ g_warning ("%s: couldn't write data to temporary known host file %s: %s", data->logname, tmp_knownhost_file, error->message);
+ *problem = "internal-error";
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+session_has_known_host_in_file (const gchar *file,
+ CockpitSshData *data,
+ const gchar *host,
+ const guint port)
+{
+ /* HACK - https://gitlab.com/libssh/libssh-mirror/-/issues/156
+
+ Calling ssh_session_has_known_hosts_entry will call
+ ssh_options_apply, after which the ssh_session structure can no
+ longer be used with ssh_session_connect. So we make a copy and
+ call ssh_session_has_known_hosts_entry on that.
+ */
+
+ ssh_session tmp_session;
+ gboolean result;
+ g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_KNOWNHOSTS, file) == 0);
+ ssh_options_copy (data->session, &tmp_session);
+ result = ssh_session_has_known_hosts_entry (tmp_session) == SSH_KNOWN_HOSTS_OK;
+ ssh_free (tmp_session);
+ return result;
+}
+
+static gboolean
+is_localhost (const char *host)
+{
+ return g_strcmp0 (host, "127.0.0.1") == 0 ||
+ g_strcmp0 (host, "::1") == 0 ||
+ g_strcmp0 (host, "localhost") == 0 ||
+ g_strcmp0 (host, "localhost4") == 0 ||
+ g_strcmp0 (host, "localhost6") == 0;
+}
+
+/**
+ * set_knownhosts_file:
+ *
+ * Check the various ssh known hosts locations and set the appropriate one into
+ * SSH_OPTIONS_KNOWNHOSTS.
+ *
+ * Returns: error string or %NULL on success.
+ */
+static const gchar *
+set_knownhosts_file (CockpitSshData *data,
+ const gchar* host,
+ const guint port)
+{
+ gboolean host_known;
+ const gchar *problem = NULL;
+ gchar *sout = NULL;
+ gchar *serr = NULL;
+ gchar *authorize_knownhosts_data = NULL;
+
+ /* first check the libssh defaults including local and global file */
+ host_known = session_has_known_host_in_file (NULL, data, host, port);
+
+ /* check file set by COCKPIT_SSH_KNOWN_HOSTS_FILE */
+ if (!host_known)
+ host_known = session_has_known_host_in_file (data->ssh_options->knownhosts_file, data, host, port);
+
+ if (!host_known)
+ {
+ authorize_knownhosts_data = challenge_for_knownhosts_data (data);
+ if (authorize_knownhosts_data)
+ {
+ if (write_tmp_knownhosts_file (data, authorize_knownhosts_data, &problem))
+ {
+ host_known = session_has_known_host_in_file (tmp_knownhost_file, data, host, port);
+ if (host_known)
+ data->ssh_options->knownhosts_file = tmp_knownhost_file;
+ else
+ g_warning ("authorize challenge reported key for %s:%u which is not known to cockpit_is_host_known()", host, port);
+ }
+ else
+ goto out;
+ }
+ }
+
+ g_debug ("%s: using known hosts file %s; host known: %i; connect to unknown hosts: %i",
+ data->logname, data->ssh_options->knownhosts_file, host_known, data->ssh_options->connect_to_unknown_hosts);
+ if (!data->ssh_options->connect_to_unknown_hosts && !host_known && !is_localhost (host))
+ {
+ g_message ("%s: refusing to connect to unknown host: %s:%d",
+ data->logname, host, port);
+ problem = "unknown-host";
+ goto out;
+ }
+
+ problem = NULL;
+out:
+ g_free (authorize_knownhosts_data);
+ g_free (sout);
+ g_free (serr);
+ return problem;
+}
+
+static const gchar *
+verify_knownhost (CockpitSshData *data,
+ const gchar* host,
+ const guint port)
+{
+ const gchar *ret = "invalid-hostkey";
+ ssh_key key = NULL;
+ unsigned char *hash = NULL;
+ enum ssh_known_hosts_e state;
+ gsize len;
+
+ g_warn_if_fail (ssh_session_export_known_hosts_entry(data->session, &data->host_key) == SSH_OK);
+ if (data->host_key == NULL)
+ {
+ ret = "internal-error";
+ goto done;
+ }
+
+ if (ssh_get_server_publickey (data->session, &key) != SSH_OK)
+ {
+ g_warning ("Couldn't look up ssh host key");
+ ret = "internal-error";
+ goto done;
+ }
+
+ data->host_key_type = ssh_key_type_to_char (ssh_key_type (key));
+ if (data->host_key_type == NULL)
+ {
+ g_warning ("Couldn't lookup host key type");
+ ret = "internal-error";
+ goto done;
+ }
+
+ if (ssh_get_publickey_hash (key, SSH_PUBLICKEY_HASH_SHA256, &hash, &len) < 0)
+ {
+ g_warning ("Couldn't hash ssh public key");
+ ret = "internal-error";
+ goto done;
+ }
+ else
+ {
+ data->host_fingerprint = ssh_get_fingerprint_hash (SSH_PUBLICKEY_HASH_SHA256, hash, len);
+ ssh_clean_pubkey_hash (&hash);
+ }
+
+ state = ssh_session_is_known_server (data->session);
+ if (state == SSH_KNOWN_HOSTS_OK)
+ {
+ g_debug ("%s: verified host key", data->logname);
+ ret = NULL; /* success */
+ goto done;
+ }
+ else if (state == SSH_KNOWN_HOSTS_ERROR)
+ {
+ g_warning ("%s: couldn't check host key: %s", data->logname,
+ ssh_get_error (data->session));
+ ret = "internal-error";
+ goto done;
+ }
+
+ switch (state)
+ {
+ case SSH_KNOWN_HOSTS_OK:
+ case SSH_KNOWN_HOSTS_ERROR:
+ g_assert_not_reached ();
+ break;
+ case SSH_KNOWN_HOSTS_CHANGED:
+ g_message ("%s: %s host key for server has changed to: %s",
+ data->logname, data->host_key_type, data->host_fingerprint);
+ break;
+ case SSH_KNOWN_HOSTS_OTHER:
+ g_message ("%s: host key for this server changed key type: %s",
+ data->logname, data->host_key_type);
+ break;
+ case SSH_KNOWN_HOSTS_NOT_FOUND:
+ g_debug ("%s: Couldn't find the known hosts file", data->logname);
+ /* fall through */
+ case SSH_KNOWN_HOSTS_UNKNOWN:
+ ret = prompt_for_host_key (data);
+ if (ret)
+ {
+ g_message ("%s: %s host key for server is not known: %s",
+ data->logname, data->host_key_type, data->host_fingerprint);
+ }
+ break;
+ }
+
+done:
+ if (key)
+ ssh_key_free (key);
+ return ret;
+}
+
+static const gchar *
+auth_result_string (int rc)
+{
+ switch (rc)
+ {
+ case SSH_AUTH_SUCCESS:
+ return "succeeded";
+ case SSH_AUTH_DENIED:
+ return "denied";
+ case SSH_AUTH_PARTIAL:
+ return "partial";
+ break;
+ case SSH_AUTH_AGAIN:
+ return "again";
+ default:
+ return "error";
+ }
+}
+
+static gchar *
+parse_auth_password (const gchar *auth_type,
+ const gchar *auth_data)
+{
+ gchar *password = NULL;
+
+ g_assert (auth_data != NULL);
+ g_assert (auth_type != NULL);
+
+ if (g_strcmp0 (auth_type, "basic") == 0)
+ password = cockpit_authorize_parse_basic (auth_data, NULL);
+ else
+ password = g_strdup (cockpit_authorize_type (auth_data, NULL));
+
+ if (password == NULL)
+ password = g_strdup ("");
+
+ return password;
+}
+
+static int
+do_interactive_auth (CockpitSshData *data)
+{
+ int rc;
+ gboolean sent_pw = FALSE;
+ gchar *password = NULL;
+
+ password = parse_auth_password (data->auth_type,
+ data->initial_auth_data);
+ rc = ssh_userauth_kbdint (data->session, NULL, NULL);
+ while (rc == SSH_AUTH_INFO)
+ {
+ const gchar *msg;
+ int n, i;
+
+ msg = ssh_userauth_kbdint_getinstruction (data->session);
+ n = ssh_userauth_kbdint_getnprompts (data->session);
+
+ for (i = 0; i < n && rc == SSH_AUTH_INFO; i++)
+ {
+ const char *prompt;
+ char *answer = NULL;
+ char echo = '\0';
+ int status = 0;
+ prompt = ssh_userauth_kbdint_getprompt (data->session, i, &echo);
+ g_debug ("%s: Got prompt %s prompt", data->logname, prompt);
+ if (!sent_pw)
+ {
+ status = ssh_userauth_kbdint_setanswer (data->session, i, password);
+ sent_pw = TRUE;
+ }
+ else
+ {
+ answer = prompt_with_authorize (data, prompt, msg, NULL, NULL, echo != '\0');
+ if (answer)
+ status = ssh_userauth_kbdint_setanswer (data->session, i, answer);
+ else
+ rc = SSH_AUTH_ERROR;
+
+ g_free (answer);
+ }
+
+ if (status < 0)
+ {
+ g_warning ("%s: failed to set answer for %s", data->logname, prompt);
+ rc = SSH_AUTH_ERROR;
+ }
+ }
+
+ if (rc == SSH_AUTH_INFO)
+ rc = ssh_userauth_kbdint (data->session, NULL, NULL);
+ }
+
+ cockpit_memory_clear (password, strlen (password));
+ g_free (password);
+ return rc;
+}
+
+static int
+do_password_auth (CockpitSshData *data)
+{
+ gchar *password = NULL;
+ const gchar *msg;
+ int rc;
+
+ password = parse_auth_password (data->auth_type,
+ data->initial_auth_data);
+
+ rc = ssh_userauth_password (data->session, NULL, password);
+ switch (rc)
+ {
+ case SSH_AUTH_SUCCESS:
+ g_debug ("%s: password auth succeeded", data->logname);
+ break;
+ case SSH_AUTH_DENIED:
+ g_debug ("%s: password auth failed", data->logname);
+ break;
+ case SSH_AUTH_PARTIAL:
+ g_message ("%s: password auth worked, but server wants more authentication",
+ data->logname);
+ break;
+ case SSH_AUTH_AGAIN:
+ g_message ("%s: password auth failed: server asked for retry",
+ data->logname);
+ break;
+ default:
+ msg = ssh_get_error (data->session);
+ g_message ("%s: couldn't authenticate: %s", data->logname, msg);
+ }
+
+ cockpit_memory_clear (password, strlen (password));
+ g_free (password);
+ return rc;
+}
+
+#ifdef HAVE_SSH_USERAUTH_PUBLICKEY_AUTO_GET_CURRENT_IDENTITY
+
+static int
+intercept_prompt (const char *prompt, char *buf, size_t len,
+ int echo, int verify, void *userdata)
+{
+ CockpitSshData *data = userdata;
+ char *identity = NULL;
+ if (ssh_userauth_publickey_auto_get_current_identity (data->session, &identity) == SSH_OK)
+ {
+ data->problem_error = g_strdup_printf ("locked identity: %s", identity);
+ ssh_string_free_char (identity);
+ }
+ return -1;
+}
+
+static int
+do_auto_auth (CockpitSshData *data)
+{
+ struct ssh_callbacks_struct cb = { .userdata = data, .auth_function = intercept_prompt };
+ ssh_callbacks_init (&cb);
+ ssh_set_callbacks (data->session, &cb);
+ int rc = ssh_userauth_publickey_auto (data->session, NULL, NULL);
+ ssh_set_callbacks (data->session, NULL);
+ return rc;
+}
+
+#else
+
+/* When prompting for a key passphrase, versions of libssh without
+ ssh_userauth_publickey_auto_get_current_identity don't provide
+ enough information to say which key it is for. We need that
+ information since Cockpit will offer to load the key into the agent
+ in order to log in.
+
+ Thus, we have to reimplement ssh_userauth_publickey_auto to get the
+ necessary information.
+
+ We would like to iterate over all configured identities, the same
+ way that the real ssh_userauth_publickey does, but there is no
+ API to do that either. So we hard code all the names, based on
+ what ssh-add would add to the agent.
+*/
+
+struct CockpitSshPromptData {
+ CockpitSshData *data;
+ const gchar *identity;
+ gboolean did_prompt;
+};
+
+/* We don't support unlocking identities within cockpit-ssh so fail here */
+static int
+prompt_for_identity_password (const char *prompt, char *buf, size_t len,
+ int echo, int verify, void *userdata)
+{
+ struct CockpitSshPromptData *prompt_data = userdata;
+ prompt_data->data->problem_error = g_strdup_printf ("locked identity: %s", prompt_data->identity);
+ prompt_data->did_prompt = TRUE;
+ return -1;
+}
+
+static int
+do_auto_auth (CockpitSshData *data)
+{
+
+ int rc;
+ const gchar *msg;
+
+ rc = ssh_userauth_agent (data->session, NULL);
+ if (rc == SSH_AUTH_SUCCESS ||
+ rc == SSH_AUTH_PARTIAL ||
+ rc == SSH_AUTH_AGAIN ) {
+ return rc;
+ }
+
+ /* See "man ssh-add" for the list of default identities.
+ */
+ gchar *libssh_identity = NULL;
+ gchar *default_identities[] = { "id_dsa", "id_ecdsa", "id_ecdsa_sk", "id_ed25519", "id_ed25519_sk", "id_rsa", NULL };
+
+ rc = ssh_options_get (data->session, SSH_OPTIONS_IDENTITY, &libssh_identity);
+ if (rc != SSH_OK)
+ {
+ g_debug ("Unable to get identity from config");
+ return rc;
+ }
+
+ for (int i = -1; i < 0 || default_identities[i]; i++)
+ {
+ g_autofree gchar *identity = NULL;
+ g_autofree gchar *pub_key_path = NULL;
+ ssh_key priv_key = NULL;
+ ssh_key pub_key = NULL;
+
+ if (i == -1)
+ identity = g_strdup (libssh_identity);
+ else
+ {
+ identity = g_strdup_printf ("%s/.ssh/%s", g_get_home_dir (), default_identities[i]);
+ // No need to try the libssh identity twice, and we need to
+ // be precious with our tries because when we run into
+ // MaxAuthTries, libssh will hang.
+ if (g_strcmp0 (identity, libssh_identity) == 0)
+ continue;
+ }
+
+ pub_key_path = g_strconcat (identity, ".pub", NULL);
+ rc = ssh_pki_import_pubkey_file (pub_key_path, &pub_key);
+ /* If the public key file exist and is readable, see if the identity is accepted by the server */
+ if (rc == SSH_OK)
+ {
+ rc = ssh_userauth_try_publickey (data->session, NULL, pub_key);
+ if (rc != SSH_AUTH_SUCCESS)
+ {
+ g_debug ("%s isn't accepted by the server", identity);
+ ssh_key_free (pub_key);
+ continue;
+ }
+ }
+ else if (rc == SSH_EOF)
+ {
+ g_debug ("Public key file %s doesn't exist or isn't readable", pub_key_path);
+ }
+ else
+ {
+ msg = ssh_get_error (data->session);
+ g_warning ("Error importing public key %s: %s", pub_key_path, msg);
+ }
+
+ struct CockpitSshPromptData pd = { data, identity, FALSE };
+ rc = ssh_pki_import_privkey_file (identity, NULL, prompt_for_identity_password, &pd, &priv_key);
+ if (rc == SSH_ERROR)
+ {
+ if (pd.did_prompt)
+ rc = SSH_AUTH_DENIED;
+ }
+ else if (rc == SSH_EOF)
+ {
+ rc = SSH_AUTH_DENIED;
+ }
+ else if (rc == SSH_OK)
+ {
+ rc = ssh_userauth_publickey (data->session, NULL, priv_key);
+ ssh_key_free (priv_key);
+
+ if (rc == SSH_AUTH_SUCCESS)
+ {
+ g_debug ("%s: key auth succeeded", data->logname);
+ ssh_key_free (pub_key);
+ break;
+ }
+ else
+ {
+ switch (rc)
+ {
+ case SSH_AUTH_DENIED:
+ g_debug ("%s: key auth failed", data->logname);
+ break;
+ case SSH_AUTH_PARTIAL:
+ g_message ("%s: key auth worked, but server wants more authentication",
+ data->logname);
+ break;
+ case SSH_AUTH_AGAIN:
+ g_message ("%s: key auth failed: server asked for retry",
+ data->logname);
+ break;
+ default:
+ msg = ssh_get_error (data->session);
+ g_message ("%s: couldn't key authenticate: %s", data->logname, msg);
+ }
+ }
+ }
+
+ ssh_key_free (pub_key);
+ }
+
+ ssh_string_free_char (libssh_identity);
+ return rc;
+}
+
+#endif
+
+static int
+do_key_auth (CockpitSshData *data)
+{
+ int rc;
+ const gchar *msg;
+
+ g_assert (data->initial_auth_data != NULL);
+
+ rc = do_auto_auth (data);
+ if (rc != SSH_AUTH_SUCCESS)
+ {
+ const gchar *key_data;
+ ssh_key key;
+
+ key_data = cockpit_authorize_type (data->initial_auth_data, NULL);
+ if (!key_data)
+ {
+ g_message ("%s: Got invalid private-key data, %s", data->logname, data->initial_auth_data);
+ return SSH_AUTH_DENIED;
+ }
+
+ rc = ssh_pki_import_privkey_base64 (key_data, NULL, NULL, NULL, &key);
+ if (rc != SSH_OK)
+ {
+ g_message ("%s: Got invalid key data: %s\n%s", data->logname, ssh_get_error (data->session), data->initial_auth_data);
+ return rc;
+ }
+ rc = ssh_userauth_publickey (data->session, NULL, key);
+ ssh_key_free (key);
+ }
+
+ switch (rc)
+ {
+ case SSH_AUTH_SUCCESS:
+ g_debug ("%s: key auth succeeded", data->logname);
+ break;
+ case SSH_AUTH_DENIED:
+ g_debug ("%s: key auth failed", data->logname);
+ break;
+ case SSH_AUTH_PARTIAL:
+ g_message ("%s: key auth worked, but server wants more authentication",
+ data->logname);
+ break;
+ case SSH_AUTH_AGAIN:
+ g_message ("%s: key auth failed: server asked for retry",
+ data->logname);
+ break;
+ default:
+ msg = ssh_get_error (data->session);
+ g_message ("%s: couldn't key authenticate: %s", data->logname, msg);
+ }
+
+ return rc;
+}
+
+static int
+do_gss_auth (CockpitSshData *data)
+{
+ int rc;
+ const gchar *msg;
+
+ rc = ssh_userauth_gssapi (data->session);
+
+ switch (rc)
+ {
+ case SSH_AUTH_SUCCESS:
+ g_debug ("%s: gssapi auth succeeded", data->logname);
+ break;
+ case SSH_AUTH_DENIED:
+ g_debug ("%s: gssapi auth failed", data->logname);
+ break;
+ case SSH_AUTH_PARTIAL:
+ g_message ("%s: gssapi auth worked, but server wants more authentication",
+ data->logname);
+ break;
+ default:
+ msg = ssh_get_error (data->session);
+ g_message ("%s: couldn't authenticate: %s", data->logname, msg);
+ }
+
+ return rc;
+}
+
+static gboolean
+has_password (CockpitSshData *data)
+{
+ if (data->auth_type == NULL &&
+ data->initial_auth_data == NULL)
+ {
+ data->initial_auth_data = challenge_for_auth_data ("basic", &data->auth_type);
+ }
+
+ return (data->initial_auth_data != NULL &&
+ (g_strcmp0 (data->auth_type, "basic") == 0 ||
+ g_strcmp0 (data->auth_type, "password") == 0));
+}
+
+static const gchar *
+cockpit_ssh_authenticate (CockpitSshData *data)
+{
+ const gchar *problem;
+ gboolean have_final_result = FALSE;
+ gchar *description;
+ const gchar *msg;
+ int rc;
+ int methods_server;
+ int methods_tried = 0;
+ int methods_to_try = SSH_AUTH_METHOD_INTERACTIVE |
+ SSH_AUTH_METHOD_GSSAPI_MIC |
+ SSH_AUTH_METHOD_PUBLICKEY;
+
+ problem = "authentication-failed";
+
+ rc = ssh_userauth_none (data->session, NULL);
+ if (rc == SSH_AUTH_ERROR)
+ {
+ g_message ("%s: server authentication handshake failed: %s",
+ data->logname, ssh_get_error (data->session));
+ problem = "internal-error";
+ goto out;
+ }
+
+ if (rc == SSH_AUTH_SUCCESS)
+ {
+ problem = NULL;
+ goto out;
+ }
+
+ methods_server = ssh_userauth_list (data->session, NULL);
+
+ /* If interactive isn't supported try password instead */
+ if (!(methods_server & SSH_AUTH_METHOD_INTERACTIVE))
+ {
+ methods_to_try = methods_to_try | SSH_AUTH_METHOD_PASSWORD;
+ methods_to_try = methods_to_try & ~SSH_AUTH_METHOD_INTERACTIVE;
+ }
+
+ while (methods_to_try != 0)
+ {
+ int (*auth_func)(CockpitSshData *data);
+ const gchar *result_string;
+ int method;
+ gboolean has_creds = FALSE;
+
+ if (methods_to_try & SSH_AUTH_METHOD_PUBLICKEY)
+ {
+ method = SSH_AUTH_METHOD_PUBLICKEY;
+ if (g_strcmp0 (data->auth_type, "private-key") == 0)
+ {
+ auth_func = do_key_auth;
+ has_creds = data->initial_auth_data != NULL;
+ }
+ else
+ {
+ auth_func = do_auto_auth;
+ has_creds = TRUE;
+ }
+ }
+ else if (methods_to_try & SSH_AUTH_METHOD_INTERACTIVE)
+ {
+ auth_func = do_interactive_auth;
+ method = SSH_AUTH_METHOD_INTERACTIVE;
+ has_creds = has_password(data);
+ }
+ else if (methods_to_try & SSH_AUTH_METHOD_PASSWORD)
+ {
+ auth_func = do_password_auth;
+ method = SSH_AUTH_METHOD_PASSWORD;
+ has_creds = has_password(data);
+ }
+ else
+ {
+ auth_func = do_gss_auth;
+ method = SSH_AUTH_METHOD_GSSAPI_MIC;
+ has_creds = TRUE;
+ }
+
+ methods_to_try = methods_to_try & ~method;
+
+ if (!(methods_server & method))
+ {
+ result_string = "no-server-support";
+ }
+ else if (!has_creds)
+ {
+ result_string = "not-provided";
+ methods_tried = methods_tried | method;
+ }
+ else
+ {
+ methods_tried = methods_tried | method;
+ if (!have_final_result)
+ {
+ rc = auth_func (data);
+ result_string = auth_result_string (rc);
+
+ if (rc == SSH_AUTH_SUCCESS)
+ {
+ have_final_result = TRUE;
+ problem = NULL;
+ }
+ else if (rc == SSH_AUTH_ERROR)
+ {
+ have_final_result = TRUE;
+ msg = ssh_get_error (data->session);
+ g_message ("%s: couldn't authenticate: %s", data->logname, msg);
+
+ if (ssh_msg_is_disconnected (msg))
+ problem = "terminated";
+ else
+ problem = "internal-error";
+ }
+ }
+ else
+ {
+ result_string = "not-tried";
+ }
+ }
+
+ g_hash_table_insert (data->auth_results,
+ g_strdup (auth_method_description (method)),
+ g_strdup (result_string));
+ }
+
+ if (have_final_result)
+ goto out;
+
+ if (methods_tried == 0)
+ {
+ if (methods_server == 0)
+ {
+ g_message ("%s: server offered no authentication methods", data->logname);
+ }
+ else
+ {
+ description = auth_methods_line (methods_server);
+ g_message ("%s: server offered unsupported authentication methods: %s",
+ data->logname, description);
+ g_free (description);
+ }
+ }
+
+out:
+ return problem;
+}
+
+static gboolean
+send_auth_reply (CockpitSshData *data,
+ const gchar *problem)
+{
+ GHashTableIter auth_iter;
+ JsonObject *auth_json = NULL; // consumed by object
+ JsonObject *object = NULL;
+ gboolean ret;
+ gpointer hkey;
+ gpointer hvalue;
+ object = json_object_new ();
+ auth_json = json_object_new ();
+
+ g_assert (problem != NULL);
+
+ json_object_set_string_member (object, "command", "init");
+ if (data->host_key)
+ json_object_set_string_member (object, "host-key", data->host_key);
+ if (data->host_fingerprint)
+ json_object_set_string_member (object, "host-fingerprint", data->host_fingerprint);
+
+ json_object_set_string_member (object, "problem", problem);
+ if (data->problem_error)
+ json_object_set_string_member (object, "error", data->problem_error);
+ else
+ json_object_set_string_member (object, "error", problem);
+
+ if (data->auth_results)
+ {
+ g_hash_table_iter_init (&auth_iter, data->auth_results);
+ while (g_hash_table_iter_next (&auth_iter, &hkey, &hvalue))
+ json_object_set_string_member (auth_json, hkey, hvalue);
+ }
+
+ json_object_set_object_member (object, "auth-method-results", auth_json);
+ ret = write_control_message (STDOUT_FILENO, object);
+ json_object_unref (object);
+
+ if (!ret)
+ g_message ("couldn't write authorize message: %s", g_strerror (errno));
+
+ return ret;
+}
+
+static gboolean
+parse_host (const gchar *host,
+ gchar **hostname,
+ gchar **username,
+ guint *port)
+{
+ GError *error = NULL;
+ g_autoptr (GRegex) regex = g_regex_new ("^"
+ "(?:(.+)@)?" /* optional username */
+ "(?|" /* one of... */
+ "\\[([^]@]+)\\]" /* hostname in square brackets, no @ */
+ "(?::([1-9][0-9]*))?" /* optional port number */
+ "|" /* or */
+ "([^@:]+)" /* hostname with no : or @ */
+ "(?::([1-9][0-9]*))?" /* optional port number */
+ "|" /* or */
+ "([^@]+)" /* hostname with no @ but : (IPv6 address), and no port */
+ ")" /* . */
+ "$",
+ 0, 0, &error);
+ g_assert_no_error (error);
+
+ g_autoptr(GMatchInfo) info = NULL;
+
+ if (g_regex_match (regex, host, 0, &info))
+ {
+ g_autofree gchar *port_str = g_match_info_fetch (info, 3);
+ /* regexp makes sure that it's a positive number, so don't need much error checking */
+ guint value = atoi (port_str ?: "");
+ if (value < 65536)
+ {
+ *port = value;
+ }
+ else
+ {
+ g_message ("invalid port: %s", port_str);
+ return FALSE;
+ }
+
+ *hostname = g_match_info_fetch (info, 2);
+
+ *username = g_match_info_fetch (info, 1);
+ if ((*username)[0] == '\0')
+ {
+ g_free (*username);
+ *username = g_strdup (g_get_user_name ());
+ }
+
+ return TRUE;
+ }
+ else
+ {
+ g_message ("invalid host: %s", host);
+ return FALSE;
+ }
+}
+
+static gchar *
+username_from_basic (const gchar *basic_data)
+{
+ gchar *user = NULL;
+ gchar *password;
+
+ password = cockpit_authorize_parse_basic (basic_data, &user);
+ if (password)
+ {
+ cockpit_memory_clear (password, -1);
+ free (password);
+ }
+ return user;
+}
+
+static const gchar*
+cockpit_ssh_connect (CockpitSshData *data,
+ const gchar *host_arg,
+ ssh_channel *out_channel)
+{
+ const gchar *ignore_hostkey;
+ gboolean host_is_whitelisted;
+ const gchar *problem;
+ g_autofree gchar *username = NULL;
+
+ guint port = 0;
+ gchar *host = NULL;
+
+ ssh_channel channel;
+ int rc;
+
+ if (!parse_host (host_arg, &host, &data->username, &port))
+ {
+ problem = "no-host";
+ goto out;
+ }
+ g_debug ("%s: host argument '%s', host '%s', username '%s', port '%u'", data->logname, host_arg, host, data->username, port);
+
+ g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_HOST, host) == 0);
+ g_warn_if_fail (ssh_options_parse_config (data->session, NULL) == 0);
+
+ if (strrchr (host_arg, '@'))
+ {
+ g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_USER, data->username) == 0);
+ }
+ else if (ssh_options_get (data->session, SSH_OPTIONS_USER, &username) != 0)
+ {
+ /* User comes from auth message when using basic if it's not set in ssh config */
+ if (g_strcmp0 (data->auth_type, "basic") == 0)
+ {
+ g_free (data->username);
+ data->username = username_from_basic (data->initial_auth_data);
+ }
+
+ if (!data->username || *data->username == '\0')
+ {
+ g_message ("%s: No username provided", data->logname);
+ problem = "authentication-failed";
+ goto out;
+ }
+ g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_USER, data->username) == 0);
+ }
+
+ /* If the user specifies a port explicitly, overwrite the config */
+ if (port != 0)
+ g_warn_if_fail (ssh_options_set (data->session, SSH_OPTIONS_PORT, &port) == 0);
+
+ /* Parsing the config might have changed the host or port */
+ gchar *new_host;
+ if (ssh_options_get (data->session, SSH_OPTIONS_HOST, &new_host) == 0)
+ {
+ g_free (host);
+ host = new_host;
+ }
+ g_warn_if_fail (ssh_options_get_port (data->session, &port) == 0);
+
+ /* This is a single host, for which we have been told to ignore the host key */
+ ignore_hostkey = cockpit_conf_string (COCKPIT_CONF_SSH_SECTION, "host");
+ if (!ignore_hostkey)
+ ignore_hostkey = "127.0.0.1";
+ host_is_whitelisted = g_str_equal (ignore_hostkey, host);
+
+ if (!host_is_whitelisted)
+ {
+ problem = set_knownhosts_file (data, host, port);
+ if (problem != NULL)
+ goto out;
+ }
+
+ rc = ssh_connect (data->session);
+ if (rc != SSH_OK)
+ {
+ g_message ("%s: %d couldn't connect: %s '%s' '%d'", data->logname, rc,
+ ssh_get_error (data->session), host, port);
+ problem = "no-host";
+ goto out;
+ }
+
+ g_debug ("%s: connected", data->logname);
+ if (!host_is_whitelisted)
+ {
+ problem = verify_knownhost (data, host, port);
+ if (problem != NULL)
+ goto out;
+ }
+
+ /* The problem returned when auth failure */
+ problem = cockpit_ssh_authenticate (data);
+ if (problem != NULL)
+ goto out;
+
+ channel = ssh_channel_new (data->session);
+ rc = ssh_channel_open_session (channel);
+ if (rc != SSH_OK)
+ {
+ g_message ("%s: couldn't open session: %s", data->logname,
+ ssh_get_error (data->session));
+ problem = "internal-error";
+ goto out;
+ }
+
+ if (data->ssh_options->remote_peer)
+ {
+ /* Try to set the remote peer env var, this will
+ * often fail as ssh servers have to be configured
+ * to allow it.
+ */
+ rc = ssh_channel_request_env (channel, "COCKPIT_REMOTE_PEER",
+ data->ssh_options->remote_peer);
+ if (rc != SSH_OK)
+ {
+ g_debug ("%s: Couldn't set COCKPIT_REMOTE_PEER: %s",
+ data->logname,
+ ssh_get_error (data->session));
+ }
+ }
+
+ g_debug ("%s: opened channel", data->logname);
+
+ *out_channel = channel;
+out:
+ g_free (host);
+ return problem;
+}
+
+static void
+cockpit_ssh_data_free (CockpitSshData *data)
+{
+ if (data->initial_auth_data)
+ {
+ memset (data->initial_auth_data, 0, strlen (data->initial_auth_data));
+ free (data->initial_auth_data);
+ }
+
+ g_free (data->host_key);
+ if (data->host_fingerprint)
+ ssh_string_free_char (data->host_fingerprint);
+
+ if (data->auth_results)
+ g_hash_table_destroy (data->auth_results);
+
+ g_free (data->problem_error);
+ g_free (data->conversation);
+ g_free (data->username);
+ g_free (data->ssh_options);
+ g_free (data->user_known_hosts);
+ g_free (data->auth_type);
+ g_strfreev (data->env);
+ g_free (data);
+}
+
+
+#define COCKPIT_SSH_RELAY(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_SSH_RELAY, CockpitSshRelay))
+
+struct _CockpitSshRelay {
+ GObject parent_instance;
+
+ CockpitSshData *ssh_data;
+
+ gboolean sent_disconnect;
+ gboolean received_eof;
+ gboolean received_frame;
+ gboolean received_close;
+ gboolean received_exit;
+
+ gboolean sent_close;
+ gboolean sent_eof;
+
+ guint exit_code;
+ guint sig_read;
+ guint sig_close;
+ gboolean pipe_closed;
+ CockpitPipe *pipe;
+
+ GQueue *queue;
+ gsize partial;
+
+ gchar *logname;
+ gchar *connection_string;
+
+ ssh_session session;
+ ssh_channel channel;
+ ssh_event event;
+
+ GSource *io;
+
+ struct ssh_channel_callbacks_struct channel_cbs;
+};
+
+struct _CockpitSshRelayClass {
+ GObjectClass parent_class;
+};
+
+static guint sig_disconnect = 0;
+
+enum {
+ PROP_0,
+ PROP_CONNECTION_STRING
+};
+
+G_DEFINE_TYPE (CockpitSshRelay, cockpit_ssh_relay, G_TYPE_OBJECT);
+
+static void
+cockpit_ssh_relay_dispose (GObject *object)
+{
+ CockpitSshRelay *self = COCKPIT_SSH_RELAY (object);
+
+ g_assert (self->ssh_data == NULL);
+
+ if (self->sig_read > 0)
+ g_signal_handler_disconnect (self->pipe, self->sig_read);
+ self->sig_read = 0;
+
+ if (self->sig_close > 0)
+ g_signal_handler_disconnect (self->pipe, self->sig_close);
+ self->sig_close = 0;
+
+ if (self->io)
+ g_source_destroy (self->io);
+
+ G_OBJECT_CLASS (cockpit_ssh_relay_parent_class)->dispose (object);
+}
+
+static void
+cockpit_ssh_relay_finalize (GObject *object)
+{
+ CockpitSshRelay *self = COCKPIT_SSH_RELAY (object);
+
+ if (self->pipe)
+ g_object_unref (self->pipe);
+
+ g_queue_free_full (self->queue, (GDestroyNotify)g_bytes_unref);
+
+ if (self->event)
+ ssh_event_free (self->event);
+
+ /* libssh channels like to hang around even after they're freed */
+ if (self->channel)
+ memset (&self->channel_cbs, 0, sizeof (self->channel_cbs));
+
+ g_free (self->logname);
+ g_free (self->connection_string);
+
+ if (self->io)
+ g_source_unref (self->io);
+
+ ssh_disconnect (self->session);
+ ssh_free (self->session);
+
+ G_OBJECT_CLASS (cockpit_ssh_relay_parent_class)->finalize (object);
+}
+
+static gboolean
+emit_disconnect (gpointer user_data)
+{
+ CockpitSshRelay *self = user_data;
+
+ if (!self->sent_disconnect)
+ {
+ self->sent_disconnect = TRUE;
+ g_signal_emit (self, sig_disconnect, 0);
+ }
+
+ return FALSE;
+}
+
+static void
+cockpit_relay_disconnect (CockpitSshRelay *self,
+ const gchar *problem)
+{
+ if (self->ssh_data)
+ {
+ send_auth_reply (self->ssh_data, problem ? problem : exit_code_problem (self->exit_code));
+ cockpit_ssh_data_free (self->ssh_data);
+ self->ssh_data = NULL;
+ }
+
+ /* libssh channels like to hang around even after they're freed */
+ if (self->channel)
+ memset (&self->channel_cbs, 0, sizeof (self->channel_cbs));
+ self->channel = NULL;
+
+ if (self->io)
+ g_source_destroy (self->io);
+
+ g_timeout_add (0, emit_disconnect, self);
+}
+
+static int
+on_channel_data (ssh_session session,
+ ssh_channel channel,
+ void *data,
+ uint32_t len,
+ int is_stderr,
+ void *userdata)
+{
+ CockpitSshRelay *self = userdata;
+ gint ret = 0;
+ guint8 *bdata = data;
+
+ if (!self->received_frame && !is_stderr)
+ {
+ guint32 i;
+
+ for (i = 0; i < len; i++)
+ {
+ /* Check invalid characters, prevent integer overflow, limit max length */
+ if (i > 7 || bdata[i] < '0' || bdata[i] > '9')
+ break;
+ }
+
+ /* If we don't have enough data return 0 bytes processed
+ * so that this data will be included in the next callback
+ */
+ if (i == len)
+ goto out;
+
+ /*
+ * So we may be talking to a process that's not cockpit-bridge. How does
+ * that happen? ssh always executes commands inside of a shell ... and
+ * bash prints its 'cockpit-bridge: not found' message on stdout (!)
+ *
+ * So we degrade gracefully in this case, and start to treat output as
+ * error output.
+ */
+ if (bdata[i] != '\n')
+ {
+ self->exit_code = NO_COCKPIT;
+ }
+ else
+ {
+ self->received_frame = TRUE;
+ cockpit_ssh_data_free (self->ssh_data);
+ self->ssh_data = NULL;
+ }
+ }
+
+ if (is_stderr || self->exit_code == NO_COCKPIT)
+ {
+ g_printerr ("%.*s", (int) len, bdata);
+ ret = len;
+ }
+ else if (self->received_frame)
+ {
+ if (!self->pipe_closed)
+ {
+ g_autoptr(GBytes) bytes = g_bytes_new (bdata, len);
+ cockpit_pipe_write (self->pipe, bytes);
+ ret = len;
+ }
+ else
+ {
+ g_debug ("%s: dropping %d incoming bytes, pipe is closed", self->logname, len);
+ ret = len;
+ }
+ }
+out:
+ return ret;
+}
+
+static void
+on_channel_eof (ssh_session session,
+ ssh_channel channel,
+ void *userdata)
+{
+ CockpitSshRelay *self = userdata;
+ g_debug ("%s: received eof", self->logname);
+ self->received_eof = TRUE;
+}
+
+static void
+on_channel_close (ssh_session session,
+ ssh_channel channel,
+ void *userdata)
+{
+ CockpitSshRelay *self = userdata;
+ g_debug ("%s: received close", self->logname);
+ self->received_close = TRUE;
+}
+
+static void
+on_channel_exit_signal (ssh_session session,
+ ssh_channel channel,
+ const char *signal,
+ int core,
+ const char *errmsg,
+ const char *lang,
+ void *userdata)
+{
+ CockpitSshRelay *self = userdata;
+ guint exit_code;
+ g_return_if_fail (signal != NULL);
+ self->received_exit = TRUE;
+
+ if (g_ascii_strcasecmp (signal, "TERM") == 0 ||
+ g_ascii_strcasecmp (signal, "Terminated") == 0)
+ {
+ g_debug ("%s: received TERM signal", self->logname);
+ exit_code = TERMINATED;
+ }
+ else
+ {
+ g_warning ("%s: bridge killed%s%s%s%s", self->logname,
+ signal ? " by signal " : "", signal ? signal : "",
+ errmsg && errmsg[0] ? ": " : "", errmsg ? errmsg : "");
+ exit_code = INTERNAL_ERROR;
+ }
+
+ if (!self->exit_code)
+ self->exit_code = exit_code;
+
+ cockpit_relay_disconnect (self, NULL);
+}
+
+static void
+on_channel_signal (ssh_session session,
+ ssh_channel channel,
+ const char *signal,
+ void *userdata)
+{
+ /*
+ * HACK: So it looks like libssh is buggy and is confused about
+ * the difference between "exit-signal" and "signal" in section 6.10
+ * of the RFC. Accept signal as a usable substitute
+ */
+ if (g_ascii_strcasecmp (signal, "TERM") == 0 ||
+ g_ascii_strcasecmp (signal, "Terminated") == 0)
+ on_channel_exit_signal (session, channel, signal, 0, NULL, NULL, userdata);
+}
+
+static void
+on_channel_exit_status (ssh_session session,
+ ssh_channel channel,
+ int exit_status,
+ void *userdata)
+{
+ CockpitSshRelay *self = userdata;
+ guint exit_code = 0;
+
+ self->received_exit = TRUE;
+ if (exit_status == 127)
+ {
+ g_debug ("%s: received exit status %d", self->logname, exit_status);
+ exit_code = NO_COCKPIT; /* cockpit-bridge not installed */
+ }
+ else if (!self->received_frame)
+ {
+ g_message ("%s: spawning remote bridge failed with %d status", self->logname, exit_status);
+ exit_code = NO_COCKPIT;
+ }
+ else if (exit_status)
+ {
+ g_message ("%s: remote bridge exited with %d status", self->logname, exit_status);
+ exit_code = INTERNAL_ERROR;
+ }
+ if (!self->exit_code && exit_code)
+ self->exit_code = exit_code;
+
+ cockpit_relay_disconnect (self, NULL);
+}
+
+static gboolean
+dispatch_queue (CockpitSshRelay *self)
+{
+ GBytes *block;
+ const guchar *data;
+ const gchar *msg;
+ gsize length;
+ gsize want;
+ int rc;
+
+ if (self->sent_eof)
+ return FALSE;
+ if (self->received_close)
+ return FALSE;
+
+ for (;;)
+ {
+ block = g_queue_peek_head (self->queue);
+ if (!block)
+ return FALSE;
+
+ data = g_bytes_get_data (block, &length);
+ g_assert (self->partial <= length);
+
+ want = length - self->partial;
+ rc = ssh_channel_write (self->channel, data + self->partial, want);
+ if (rc < 0)
+ {
+ msg = ssh_get_error (self->session);
+ if (ssh_get_error_code (self->session) == SSH_REQUEST_DENIED)
+ {
+ g_debug ("%s: couldn't write: %s", self->logname, msg);
+ return FALSE;
+ }
+ else if (ssh_msg_is_disconnected (msg))
+ {
+ g_message ("%s: couldn't write: %s", self->logname, msg);
+ self->received_close = TRUE;
+ self->received_eof = TRUE;
+ return FALSE;
+ }
+ else
+ {
+ g_warning ("%s: couldn't write: %s", self->logname, msg);
+ return FALSE;
+ }
+ break;
+ }
+
+ if (rc == want)
+ {
+ g_debug ("%s: wrote %d bytes", self->logname, rc);
+ g_queue_pop_head (self->queue);
+ g_bytes_unref (block);
+ self->partial = 0;
+ }
+ else
+ {
+ g_debug ("%s: wrote %d of %d bytes", self->logname, rc, (int)want);
+ g_return_val_if_fail (rc < want, FALSE);
+ self->partial += rc;
+ if (rc == 0)
+ break;
+ }
+ }
+
+ return TRUE;
+}
+
+static void
+dispatch_close (CockpitSshRelay *self)
+{
+ g_assert (!self->sent_close);
+
+ switch (ssh_channel_close (self->channel))
+ {
+ case SSH_AGAIN:
+ g_debug ("%s: will send close later", self->logname);
+ break;
+ case SSH_OK:
+ g_debug ("%s: sent close", self->logname);
+ self->sent_close = TRUE;
+ break;
+ default:
+ if (ssh_get_error_code (self->session) == SSH_REQUEST_DENIED)
+ {
+ g_debug ("%s: couldn't send close: %s", self->logname,
+ ssh_get_error (self->session));
+ self->sent_close = TRUE; /* channel is already closed */
+ }
+ else
+ {
+ g_warning ("%s: couldn't send close: %s", self->logname,
+ ssh_get_error (self->session));
+ self->received_exit = TRUE;
+ if (!self->exit_code)
+ self->exit_code = INTERNAL_ERROR;
+ cockpit_relay_disconnect (self, NULL);
+ }
+ break;
+ }
+}
+
+static void
+dispatch_eof (CockpitSshRelay *self)
+{
+ g_assert (!self->sent_eof);
+
+ switch (ssh_channel_send_eof (self->channel))
+ {
+ case SSH_AGAIN:
+ g_debug ("%s: will send eof later", self->logname);
+ break;
+ case SSH_OK:
+ g_debug ("%s: sent eof", self->logname);
+ self->sent_eof = TRUE;
+ break;
+ default:
+ if (ssh_get_error_code (self->session) == SSH_REQUEST_DENIED)
+ {
+ g_debug ("%s: couldn't send eof: %s", self->logname,
+ ssh_get_error (self->session));
+ self->sent_eof = TRUE; /* channel is already closed */
+ }
+ else
+ {
+ g_warning ("%s: couldn't send eof: %s", self->logname,
+ ssh_get_error (self->session));
+ self->received_exit = TRUE;
+ if (!self->exit_code)
+ self->exit_code = INTERNAL_ERROR;
+ cockpit_relay_disconnect (self, NULL);
+ }
+ break;
+ }
+}
+
+static void
+on_pipe_read (CockpitPipe *pipe,
+ GByteArray *input,
+ gboolean end_of_data,
+ gpointer user_data)
+{
+ CockpitSshRelay *self = user_data;
+ GByteArray *buf = NULL;
+
+ buf = cockpit_pipe_get_buffer (pipe);
+ g_byte_array_ref (buf);
+
+ if (!self->sent_eof && !self->received_close && buf->len > 0)
+ {
+ g_debug ("%s: queued %d bytes", self->logname, buf->len);
+ g_queue_push_tail (self->queue, g_byte_array_free_to_bytes (buf));
+ }
+ else
+ {
+ g_debug ("%s: dropping %d bytes", self->logname, buf->len);
+ g_byte_array_free (buf, TRUE);
+ }
+
+ if (end_of_data)
+ cockpit_pipe_close (pipe, NULL);
+}
+
+static void
+on_pipe_close (CockpitPipe *pipe,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitSshRelay *self = user_data;
+
+ self->pipe_closed = TRUE;
+ // Pipe closing before data was received doesn't mean no-cockpit
+ self->received_frame = TRUE;
+
+ if (!self->received_eof)
+ dispatch_eof (self);
+
+ cockpit_relay_disconnect (self, NULL);
+}
+
+typedef struct {
+ GSource source;
+ GPollFD pfd;
+ CockpitSshRelay *relay;
+} CockpitSshSource;
+
+static gboolean
+cockpit_ssh_source_check (GSource *source)
+{
+ CockpitSshSource *cs = (CockpitSshSource *)source;
+ return (cs->pfd.events & cs->pfd.revents) != 0;
+}
+
+static gboolean
+cockpit_ssh_source_prepare (GSource *source,
+ gint *timeout)
+{
+ CockpitSshSource *cs = (CockpitSshSource *)source;
+ CockpitSshRelay *self = cs->relay;
+ gint status;
+
+ *timeout = 1;
+
+ status = ssh_get_status (self->session);
+
+ cs->pfd.revents = 0;
+ cs->pfd.events = G_IO_IN | G_IO_ERR | G_IO_NVAL | G_IO_HUP;
+
+ /* libssh has something in its buffer: want to write */
+ if (status & SSH_WRITE_PENDING)
+ cs->pfd.events |= G_IO_OUT;
+
+ /* We have something in our queue: want to write */
+ else if (!g_queue_is_empty (self->queue))
+ cs->pfd.events |= G_IO_OUT;
+
+ /* We are closing and need to send eof: want to write */
+ else if (self->pipe_closed && !self->sent_eof)
+ cs->pfd.events |= G_IO_OUT;
+
+ /* Need to reply to an EOF or close */
+ if ((self->received_eof && self->sent_eof && !self->sent_close) ||
+ (self->received_close && !self->sent_close))
+ cs->pfd.events |= G_IO_OUT;
+
+ return cockpit_ssh_source_check (source);
+}
+
+static gboolean
+cockpit_ssh_source_dispatch (GSource *source,
+ GSourceFunc callback,
+ gpointer user_data)
+{
+ CockpitSshSource *cs = (CockpitSshSource *)source;
+ int rc;
+ const gchar *msg;
+ gboolean ret = TRUE;
+ CockpitSshRelay *self = cs->relay;
+ GIOCondition cond = cs->pfd.revents;
+
+ if (cond & (G_IO_HUP | G_IO_ERR))
+ {
+ if (self->sent_close || self->sent_eof)
+ {
+ self->received_eof = TRUE;
+ self->received_close = TRUE;
+ }
+ }
+
+ if (self->received_exit)
+ return FALSE;
+
+ g_return_val_if_fail ((cond & G_IO_NVAL) == 0, FALSE);
+
+ /*
+ * HACK: Yes this is another poll() call. The async support in
+ * libssh is quite hacky right now.
+ *
+ * https://red.libssh.org/issues/155
+ */
+ rc = ssh_event_dopoll (self->event, 0);
+ switch (rc)
+ {
+ case SSH_OK:
+ case SSH_AGAIN:
+ break;
+ case SSH_ERROR:
+ msg = ssh_get_error (self->session);
+
+ /*
+ * HACK: There doesn't seem to be a way to get at the original socket errno
+ * here. So we have to screen scrape.
+ *
+ * https://red.libssh.org/issues/158
+ */
+ if (ssh_msg_is_disconnected (msg))
+ {
+ g_debug ("%s: failed to process channel: %s", self->logname, msg);
+ self->received_exit = TRUE;
+ if (!self->exit_code)
+ self->exit_code = TERMINATED;
+ }
+ else
+ {
+ g_message ("%s: failed to process channel: %s", self->logname, msg);
+ self->received_exit = TRUE;
+ if (!self->exit_code)
+ self->exit_code = INTERNAL_ERROR;
+ }
+ ret = FALSE;
+ break;
+ default:
+ self->received_exit = TRUE;
+ if (!self->exit_code)
+ self->exit_code = INTERNAL_ERROR;
+ g_critical ("%s: ssh_event_dopoll() returned %d", self->logname, rc);
+ ret = FALSE;
+ }
+
+ if (!ret)
+ goto out;
+
+ if (cond & G_IO_ERR)
+ {
+ g_message ("%s: error reading from ssh", self->logname);
+ ret = FALSE;
+ self->received_exit = TRUE;
+ if (!self->exit_code)
+ self->exit_code = DISCONNECTED;
+ goto out;
+ }
+
+ if (cond & G_IO_OUT)
+ {
+ if (!dispatch_queue (self) && self->pipe_closed && !self->sent_eof)
+ dispatch_eof (self);
+ if (self->received_eof && self->sent_eof && !self->sent_close)
+ dispatch_close (self);
+ if (self->received_eof && !self->received_close && !self->sent_close)
+ dispatch_close (self);
+ }
+
+out:
+ if (self->received_exit)
+ cockpit_relay_disconnect (self, NULL);
+ return ret;
+}
+
+static GSource *
+cockpit_ssh_relay_start_source (CockpitSshRelay *self) {
+ static GSourceFuncs source_funcs = {
+ cockpit_ssh_source_prepare,
+ cockpit_ssh_source_check,
+ cockpit_ssh_source_dispatch,
+ NULL,
+ };
+ GSource *source = g_source_new (&source_funcs, sizeof (CockpitSshSource));
+ CockpitSshSource *cs = (CockpitSshSource *)source;
+ cs->relay = self;
+ cs->pfd.fd = ssh_get_fd (self->session);
+ g_source_add_poll (source, &cs->pfd);
+ g_source_attach (source, g_main_context_default ());
+
+ return source;
+}
+
+static void
+cockpit_ssh_relay_start (CockpitSshRelay *self)
+{
+ const gchar *problem;
+ int in;
+ int out;
+ int rc;
+
+ static struct ssh_channel_callbacks_struct channel_cbs = {
+ .channel_data_function = on_channel_data,
+ .channel_eof_function = on_channel_eof,
+ .channel_close_function = on_channel_close,
+ .channel_signal_function = on_channel_signal,
+ .channel_exit_signal_function = on_channel_exit_signal,
+ .channel_exit_status_function = on_channel_exit_status,
+ };
+
+ self->ssh_data->initial_auth_data = challenge_for_auth_data ("*", &self->ssh_data->auth_type);
+
+ problem = cockpit_ssh_connect (self->ssh_data, self->connection_string, &self->channel);
+ if (problem)
+ goto out;
+
+ self->event = ssh_event_new ();
+ memcpy (&self->channel_cbs, &channel_cbs, sizeof (channel_cbs));
+ self->channel_cbs.userdata = self;
+ ssh_callbacks_init (&self->channel_cbs);
+ ssh_set_channel_callbacks (self->channel, &self->channel_cbs);
+ ssh_set_blocking (self->session, 0);
+ ssh_event_add_session (self->event, self->session);
+
+ in = dup (0);
+ g_assert (in >= 0);
+ out = dup (1);
+ g_assert (out >= 0);
+
+ self->pipe = g_object_new (COCKPIT_TYPE_PIPE,
+ "in-fd", in,
+ "out-fd", out,
+ "name", self->logname,
+ NULL);
+ self->sig_read = g_signal_connect (self->pipe,
+ "read",
+ G_CALLBACK (on_pipe_read),
+ self);
+ self->sig_close = g_signal_connect (self->pipe,
+ "close",
+ G_CALLBACK (on_pipe_close),
+ self);
+
+ for (rc = SSH_AGAIN; rc == SSH_AGAIN; )
+ rc = ssh_channel_request_exec (self->channel, self->ssh_data->ssh_options->command);
+
+ if (rc != SSH_OK)
+ {
+ g_message ("%s: couldn't execute command: %s: %s", self->logname,
+ self->ssh_data->ssh_options->command,
+ ssh_get_error (self->session));
+ problem = "internal-error";
+ goto out;
+ }
+
+ self->io = cockpit_ssh_relay_start_source (self);
+
+out:
+ if (problem)
+ {
+ self->exit_code = AUTHENTICATION_FAILED;
+ cockpit_relay_disconnect (self, problem);
+ }
+}
+
+static void
+cockpit_ssh_relay_init (CockpitSshRelay *self)
+{
+ const gchar *debug;
+
+ ssh_init ();
+
+ self->queue = g_queue_new ();
+ debug = g_getenv ("G_MESSAGES_DEBUG");
+
+ if (debug && (strstr (debug, "libssh") || g_strcmp0 (debug, "all") == 0))
+ ssh_set_log_level (SSH_LOG_FUNCTIONS);
+}
+
+static void
+cockpit_ssh_relay_set_property (GObject *obj,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CockpitSshRelay *self = COCKPIT_SSH_RELAY (obj);
+
+ switch (prop_id)
+ {
+ case PROP_CONNECTION_STRING:
+ self->connection_string = g_value_dup_string (value);
+ self->logname = g_strdup_printf ("cockpit-ssh %s", self->connection_string);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cockpit_ssh_relay_constructed (GObject *object)
+{
+ CockpitSshRelay *self = COCKPIT_SSH_RELAY (object);
+
+ G_OBJECT_CLASS (cockpit_ssh_relay_parent_class)->constructed (object);
+
+ self->session = ssh_new ();
+ self->ssh_data = g_new0 (CockpitSshData, 1);
+ self->ssh_data->env = g_get_environ ();
+ self->ssh_data->session = self->session;
+ self->ssh_data->logname = self->logname;
+ self->ssh_data->auth_results = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+ self->ssh_data->ssh_options = cockpit_ssh_options_from_env (self->ssh_data->env);
+ self->ssh_data->user_known_hosts = g_build_filename (g_get_home_dir (), ".ssh/known_hosts", NULL);
+}
+
+static void
+authorize_logger (const char *data)
+{
+ g_message ("%s", data);
+}
+
+static void
+cockpit_ssh_relay_class_init (CockpitSshRelayClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = cockpit_ssh_relay_dispose;
+ object_class->finalize = cockpit_ssh_relay_finalize;
+ object_class->constructed = cockpit_ssh_relay_constructed;
+ object_class->set_property = cockpit_ssh_relay_set_property;
+
+ g_object_class_install_property (object_class, PROP_CONNECTION_STRING,
+ g_param_spec_string ("connection-string", NULL, NULL, "localhost",
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ sig_disconnect = g_signal_new ("disconnect", COCKPIT_TYPE_SSH_RELAY,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ cockpit_authorize_logger (authorize_logger, 0);
+}
+
+CockpitSshRelay *
+cockpit_ssh_relay_new (const gchar *connection_string)
+{
+
+ CockpitSshRelay *self = g_object_new (COCKPIT_TYPE_SSH_RELAY,
+ "connection-string", connection_string,
+ NULL);
+ cockpit_ssh_relay_start (self);
+ return self;
+}
+
+gint
+cockpit_ssh_relay_result (CockpitSshRelay* self)
+{
+ return self->exit_code;
+}
diff --git a/src/ssh/cockpitsshrelay.h b/src/ssh/cockpitsshrelay.h
new file mode 100644
index 0000000..9473f99
--- /dev/null
+++ b/src/ssh/cockpitsshrelay.h
@@ -0,0 +1,48 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_SSH_RELAY_H__
+#define __COCKPIT_SSH_RELAY_H__
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/* EXIT CODE CONSTANTS */
+#define INTERNAL_ERROR 1
+#define AUTHENTICATION_FAILED 2
+#define DISCONNECTED 254
+#define TERMINATED 255
+#define NO_COCKPIT 127
+
+#define COCKPIT_TYPE_SSH_RELAY (cockpit_ssh_relay_get_type ())
+
+typedef struct _CockpitSshRelay CockpitSshRelay;
+typedef struct _CockpitSshRelay CockpitSshRelayClass;
+
+GType cockpit_ssh_relay_get_type (void) G_GNUC_CONST;
+
+CockpitSshRelay * cockpit_ssh_relay_new (const gchar *connection_string);
+
+gint cockpit_ssh_relay_result (CockpitSshRelay* self);
+
+G_END_DECLS
+
+#endif
diff --git a/src/ssh/invalid_known_hosts b/src/ssh/invalid_known_hosts
new file mode 100644
index 0000000..a84193e
--- /dev/null
+++ b/src/ssh/invalid_known_hosts
@@ -0,0 +1 @@
+[localhost]:*,[127.0.0.1]:* ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+Q8SP0Zf0jS//d3EJQHvdOA5iPGRcH+bZ5wqdRiZi0TCCKsCicKBlEwC8HtRo/l7pT+k0Je5n5SBLTJTq2oRcPsxDhhmKI1TVFot7+BDbU0THbuGEIZNk4spxJ84W/Znvs5RxdKinY9nFDB5hofX5EHuGiM53tlyE3+0gHG95rklKD/TAsLwubrmCkJkLEbZM2B/Kai+fKtgHjgefASbiMIxTi4/ryHT4Pb3tRLihbBylyoTZLgs7FrOPDUGu+e2mrC2rrbi1RvVBVgzF0ekJHtC7zjevddKlLgHCuc6SztJuDKC0eIqCiBjleG7tmqxM4O3PIn3nRgPSxydjjWod
diff --git a/src/ssh/manifest.json b/src/ssh/manifest.json
new file mode 100644
index 0000000..8e6a4b4
--- /dev/null
+++ b/src/ssh/manifest.json
@@ -0,0 +1,33 @@
+{
+ "priority": 100,
+ "bridges": [
+ {
+ "match": { "session": "private", "user": null, "host": null },
+ "environ": [ "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS=true",
+ "COCKPIT_PRIVATE_${channel}=${channel}" ],
+ "spawn": [ "${libexecdir}/cockpit-ssh", "${user}@${host}" ],
+ "timeout": 30,
+ "problem": "not-supported"
+ },
+ {
+ "match": { "session": "private", "host": null },
+ "environ": [ "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS=true",
+ "COCKPIT_PRIVATE_${channel}=${channel}" ],
+ "spawn": [ "${libexecdir}/cockpit-ssh", "${host}" ],
+ "timeout": 30,
+ "problem": "not-supported"
+ },
+ {
+ "match": { "user": null, "host": null },
+ "spawn": [ "${libexecdir}/cockpit-ssh", "${user}@${host}" ],
+ "timeout": 30,
+ "problem": "not-supported"
+ },
+ {
+ "match": { "host": null },
+ "spawn": [ "${libexecdir}/cockpit-ssh", "${host}" ],
+ "timeout": 30,
+ "problem": "not-supported"
+ }
+ ]
+}
diff --git a/src/ssh/mock-config/cockpit/cockpit.conf b/src/ssh/mock-config/cockpit/cockpit.conf
new file mode 100644
index 0000000..1e95240
--- /dev/null
+++ b/src/ssh/mock-config/cockpit/cockpit.conf
@@ -0,0 +1,2 @@
+[Ssh-Login]
+host = 127.0.0.2
diff --git a/src/ssh/mock-pid-cat b/src/ssh/mock-pid-cat
new file mode 100755
index 0000000..ece4023
--- /dev/null
+++ b/src/ssh/mock-pid-cat
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+# Here we send our PID as a message on channel '11x', which we happen to know will be
+# 31 bytes long. After that we echo anything sent
+
+/usr/bin/printf '37\n\n{ \"command\" : \"init\", \"version\": 1 }31\n11x\n{ "pid": % 16s }' $$
+exec /bin/cat
diff --git a/src/ssh/mock-sshd.c b/src/ssh/mock-sshd.c
new file mode 100644
index 0000000..476a269
--- /dev/null
+++ b/src/ssh/mock-sshd.c
@@ -0,0 +1,1096 @@
+/* Based on the sample implementation of a libssh based SSH server:
+
+ https://git.libssh.org/projects/libssh.git/plain/examples/ssh_server.c?id=23cebfadea156aea377462eaf5971955c77d2d61
+
+ The main changes are:
+
+ - Command line options and server configuration have been changed
+ to match our established mock-sshd conventions.
+
+ - The port is printed on stdout and stdout is closed afterwards.
+
+ - The specific interactive authorization that is expected by the
+ tests is implemented by hooking into the message callbacks.
+ There doesn't seem to be a dedicated callback for this.
+
+ - If this child exits with a signal, this is also reported back.
+
+ - Dead locks while writing to the child stdin are avoided by
+ polling for writability.
+*/
+
+/*
+Copyright 2014 Audrius Butkevicius
+
+This file is part of the SSH Library
+
+You are free to copy this file, modify it in any way, consider it being public
+domain. This does not apply to the rest of the library though, but it is
+allowed to cut-and-paste working code from this file to any license of
+program.
+The goal is to show the API in action.
+*/
+
+#include "config.h"
+#define HAVE_ARGP_H
+#define HAVE_PTY_H
+#define HAVE_UTMP_H
+#define WITH_FORK
+
+#include <libssh/callbacks.h>
+#include <libssh/server.h>
+
+#include <poll.h>
+#ifdef HAVE_ARGP_H
+#include <argp.h>
+#endif
+#include <fcntl.h>
+#ifdef HAVE_LIBUTIL_H
+#include <libutil.h>
+#endif
+#include <pthread.h>
+#ifdef HAVE_PTY_H
+#include <pty.h>
+#endif
+#include <signal.h>
+#include <stdlib.h>
+#ifdef HAVE_UTMP_H
+#include <utmp.h>
+#endif
+#ifdef HAVE_UTIL_H
+#include <util.h>
+#endif
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <stdio.h>
+#include <stdbool.h>
+
+#ifndef BUF_SIZE
+#define BUF_SIZE 1048576
+#endif
+
+#ifndef KEYS_FOLDER
+#ifdef _WIN32
+#define KEYS_FOLDER
+#else
+#define KEYS_FOLDER "/etc/ssh/"
+#endif
+#endif
+
+#define SESSION_END (SSH_CLOSED | SSH_CLOSED_ERROR)
+#define SFTP_SERVER_PATH "/usr/lib/sftp-server"
+
+#define DEF_STR_SIZE 1024
+bool broken_auth = false;
+bool multi_step = false;
+char authorizedkeys[DEF_STR_SIZE] = {0};
+char username[128] = "myuser";
+char password[128] = "mypassword";
+#ifdef HAVE_ARGP_H
+const char *argp_program_version = "libssh server example "
+SSH_STRINGIFY(LIBSSH_VERSION);
+const char *argp_program_bug_address = "<libssh@libssh.org>";
+
+/* Program documentation. */
+static char doc[] = "libssh -- a Secure Shell protocol implementation";
+
+/* The options we understand. */
+static struct argp_option options[] = {
+ {
+ .name = "port",
+ .key = 'p',
+ .arg = "PORT",
+ .flags = 0,
+ .doc = "Set the port to bind.",
+ .group = 0
+ },
+ {
+ .name = "bind",
+ .key = 'b',
+ .arg = "BIND",
+ .flags = 0,
+ .doc = "Set the address to bind.",
+ .group = 0
+ },
+ {
+ .name = "hostkey",
+ .key = 'k',
+ .arg = "FILE",
+ .flags = 0,
+ .doc = "Set a host key. Can be used multiple times. "
+ "Implies no default keys.",
+ .group = 0
+ },
+ {
+ .name = "dsakey",
+ .key = 'd',
+ .arg = "FILE",
+ .flags = 0,
+ .doc = "Set the dsa key.",
+ .group = 0
+ },
+ {
+ .name = "rsakey",
+ .key = 'r',
+ .arg = "FILE",
+ .flags = 0,
+ .doc = "Set the rsa key.",
+ .group = 0
+ },
+ {
+ .name = "ecdsakey",
+ .key = 'e',
+ .arg = "FILE",
+ .flags = 0,
+ .doc = "Set the ecdsa key.",
+ .group = 0
+ },
+ {
+ .name = "import-pubkey",
+ .key = 'a',
+ .arg = "FILE",
+ .flags = 0,
+ .doc = "Set the authorized keys file.",
+ .group = 0
+ },
+ {
+ .name = "user",
+ .key = 'u',
+ .arg = "USERNAME",
+ .flags = 0,
+ .doc = "Set expected username.",
+ .group = 0
+ },
+ {
+ .name = "password",
+ .key = 'P',
+ .arg = "PASSWORD",
+ .flags = 0,
+ .doc = "Set expected password.",
+ .group = 0
+ },
+ {
+ .name = "broken-auth",
+ .key = 't',
+ .arg = NULL,
+ .flags = 0,
+ .doc = "Break authentication",
+ .group = 0
+ },
+ {
+ .name = "multi-step",
+ .key = 'm',
+ .arg = NULL,
+ .flags = 0,
+ .doc = "Multi Step Auth",
+ .group = 0
+ },
+ {
+ .name = "verbose",
+ .key = 'v',
+ .arg = NULL,
+ .flags = 0,
+ .doc = "Get verbose output.",
+ .group = 0
+ },
+ {NULL, 0, NULL, 0, NULL, 0}
+};
+
+/* Parse a single option. */
+static error_t parse_opt (int key, char *arg, struct argp_state *state) {
+ /* Get the input argument from argp_parse, which we
+ * know is a pointer to our arguments structure. */
+ ssh_bind sshbind = state->input;
+
+ switch (key) {
+ case 'p':
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDPORT_STR, arg);
+ break;
+ case 'b':
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDADDR, arg);
+ break;
+ case 'd':
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_DSAKEY, arg);
+ break;
+ case 'k':
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HOSTKEY, arg);
+ break;
+ case 'r':
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, arg);
+ break;
+ case 'e':
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_ECDSAKEY, arg);
+ break;
+ case 'a':
+ strncpy(authorizedkeys, arg, DEF_STR_SIZE-1);
+ break;
+ case 'u':
+ strncpy(username, arg, sizeof(username) - 1);
+ break;
+ case 'P':
+ strncpy(password, arg, sizeof(password) - 1);
+ break;
+ case 't':
+ broken_auth = true;
+ break;
+ case 'm':
+ multi_step = true;
+ break;
+ case 'v':
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_LOG_VERBOSITY_STR,
+ "3");
+ break;
+ case ARGP_KEY_ARG:
+ /* Too many arguments. */
+ argp_usage (state);
+ break;
+ case ARGP_KEY_END:
+ break;
+ default:
+ return ARGP_ERR_UNKNOWN;
+ }
+ return 0;
+}
+
+/* Our argp parser. */
+static struct argp argp = {options, parse_opt, NULL, doc, NULL, NULL, NULL};
+#else
+static int parse_opt(int argc, char **argv, ssh_bind sshbind) {
+ int no_default_keys = 0;
+ int rsa_already_set = 0;
+ int dsa_already_set = 0;
+ int ecdsa_already_set = 0;
+ int key;
+
+ while((key = getopt(argc, argv, "a:d:e:k:np:P:r:u:v")) != -1) {
+ if (key == 'n') {
+ no_default_keys = 1;
+ } else if (key == 'p') {
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDPORT_STR, optarg);
+ } else if (key == 'd') {
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_DSAKEY, optarg);
+ dsa_already_set = 1;
+ } else if (key == 'k') {
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HOSTKEY, optarg);
+ /* We can't track the types of keys being added with this
+ option, so let's ensure we keep the keys we're adding
+ by just not setting the default keys */
+ no_default_keys = 1;
+ } else if (key == 'r') {
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, optarg);
+ rsa_already_set = 1;
+ } else if (key == 'e') {
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_ECDSAKEY, optarg);
+ ecdsa_already_set = 1;
+ } else if (key == 'a') {
+ strncpy(authorizedkeys, optarg, DEF_STR_SIZE-1);
+ } else if (key == 'u') {
+ strncpy(username, optarg, sizeof(username) - 1);
+ } else if (key == 'P') {
+ strncpy(password, optarg, sizeof(password) - 1);
+ } else if (key == 'v') {
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_LOG_VERBOSITY_STR,
+ "3");
+ } else {
+ break;
+ }
+ }
+
+ if (key != -1) {
+ printf("Usage: %s [OPTION...] BINDADDR\n"
+ "libssh %s -- a Secure Shell protocol implementation\n"
+ "\n"
+ " -a, --authorizedkeys=FILE Set the authorized keys file.\n"
+ " -d, --dsakey=FILE Set the dsa key.\n"
+ " -e, --ecdsakey=FILE Set the ecdsa key.\n"
+ " -k, --hostkey=FILE Set a host key. Can be used multiple times.\n"
+ " Implies no default keys.\n"
+ " -n, --no-default-keys Do not set default key locations.\n"
+ " -p, --port=PORT Set the port to bind.\n"
+ " -P, --pass=PASSWORD Set expected password.\n"
+ " -r, --rsakey=FILE Set the rsa key.\n"
+ " -u, --user=USERNAME Set expected username.\n"
+ " -v, --verbose Get verbose output.\n"
+ " -?, --help Give this help list\n"
+ "\n"
+ "Mandatory or optional arguments to long options are also mandatory or optional\n"
+ "for any corresponding short options.\n"
+ "\n"
+ "Report bugs to <libssh@libssh.org>.\n",
+ argv[0], SSH_STRINGIFY(LIBSSH_VERSION));
+ return -1;
+ }
+
+ if (optind != argc - 1) {
+ printf("Usage: %s [OPTION...] BINDADDR\n", argv[0]);
+ return -1;
+ }
+
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_BINDADDR, argv[optind]);
+
+ if (!no_default_keys) {
+ set_default_keys(sshbind,
+ rsa_already_set,
+ dsa_already_set,
+ ecdsa_already_set);
+ }
+
+ return 0;
+}
+#endif /* HAVE_ARGP_H */
+
+/* A userdata struct for channel. */
+struct channel_data_struct {
+ /* pid of the child process the channel will spawn. */
+ pid_t pid;
+ /* For PTY allocation */
+ socket_t pty_master;
+ socket_t pty_slave;
+ /* For communication with the child process. */
+ socket_t child_stdin;
+ socket_t child_stdout;
+ /* Only used for subsystem and exec requests. */
+ socket_t child_stderr;
+ /* Event which is used to poll the above descriptors. */
+ ssh_event event;
+ /* Terminal size struct. */
+ struct winsize *winsize;
+ /* Data we want to send */
+ uint8_t *stdin_buf;
+ uint32_t stdin_len;
+};
+
+/* A userdata struct for session. */
+struct session_data_struct {
+ /* Pointer to the channel the session will allocate. */
+ ssh_channel channel;
+ int auth_attempts;
+ int authenticated;
+ int multi_step_state;
+};
+
+static int process_child_stdin(socket_t fd, int revents, void *userdata) {
+ struct channel_data_struct *cdata = (struct channel_data_struct *) userdata;
+ if (cdata->stdin_len == 0)
+ return 0;
+
+ fcntl (cdata->child_stdin, F_SETFL, O_NONBLOCK);
+ int ret = write(cdata->child_stdin, cdata->stdin_buf, cdata->stdin_len);
+ if (ret > 0) {
+ memmove (cdata->stdin_buf, cdata->stdin_buf + ret, cdata->stdin_len - ret);
+ cdata->stdin_len -= ret;
+ if (cdata->stdin_len == 0)
+ ssh_event_remove_fd(cdata->event, cdata->child_stdin);
+ }
+
+ return 0;
+}
+
+static void queue_child_stdin(struct channel_data_struct *cdata, void *data, uint32_t len)
+{
+ fcntl (cdata->child_stdin, F_SETFL, O_NONBLOCK);
+ int ret = write(cdata->child_stdin, data, len);
+ if (ret < 0) {
+ if (errno == EAGAIN) {
+ ret = 0;
+ } else {
+ perror("write");
+ exit(1);
+ }
+ }
+ if (ret >= 0 && ret < len) {
+ len -= ret;
+ data = (uint8_t *)data + ret;
+ cdata->stdin_buf = realloc (cdata->stdin_buf, cdata->stdin_len + len);
+ memcpy (cdata->stdin_buf + cdata->stdin_len, data, len);
+ if (cdata->stdin_len == 0)
+ ssh_event_add_fd(cdata->event, cdata->child_stdin, POLLOUT, process_child_stdin, cdata);
+ cdata->stdin_len += len;
+ }
+}
+
+static int data_function(ssh_session session, ssh_channel channel, void *data,
+ uint32_t len, int is_stderr, void *userdata) {
+ struct channel_data_struct *cdata = (struct channel_data_struct *) userdata;
+
+ (void) session;
+ (void) channel;
+ (void) is_stderr;
+
+ if (len == 0 || cdata->pid < 1 || kill(cdata->pid, 0) < 0) {
+ return 0;
+ }
+
+ queue_child_stdin (cdata, data, len);
+ return len;
+}
+
+static int pty_request(ssh_session session, ssh_channel channel,
+ const char *term, int cols, int rows, int py, int px,
+ void *userdata) {
+ struct channel_data_struct *cdata = (struct channel_data_struct *)userdata;
+
+ (void) session;
+ (void) channel;
+ (void) term;
+
+ cdata->winsize->ws_row = rows;
+ cdata->winsize->ws_col = cols;
+ cdata->winsize->ws_xpixel = px;
+ cdata->winsize->ws_ypixel = py;
+
+ if (openpty(&cdata->pty_master, &cdata->pty_slave, NULL, NULL,
+ cdata->winsize) != 0) {
+ fprintf(stderr, "Failed to open pty\n");
+ return SSH_ERROR;
+ }
+ return SSH_OK;
+}
+
+static int pty_resize(ssh_session session, ssh_channel channel, int cols,
+ int rows, int py, int px, void *userdata) {
+ struct channel_data_struct *cdata = (struct channel_data_struct *)userdata;
+
+ (void) session;
+ (void) channel;
+
+ cdata->winsize->ws_row = rows;
+ cdata->winsize->ws_col = cols;
+ cdata->winsize->ws_xpixel = px;
+ cdata->winsize->ws_ypixel = py;
+
+ if (cdata->pty_master != -1) {
+ return ioctl(cdata->pty_master, TIOCSWINSZ, cdata->winsize);
+ }
+
+ return SSH_ERROR;
+}
+
+static int exec_pty(const char *mode, const char *command,
+ struct channel_data_struct *cdata) {
+ switch(cdata->pid = fork()) {
+ case -1:
+ close(cdata->pty_master);
+ close(cdata->pty_slave);
+ fprintf(stderr, "Failed to fork\n");
+ return SSH_ERROR;
+ case 0:
+ close(cdata->pty_master);
+ if (login_tty(cdata->pty_slave) != 0) {
+ exit(1);
+ }
+ execl("/bin/sh", "sh", mode, command, NULL);
+ exit(0);
+ default:
+ close(cdata->pty_slave);
+ /* pty fd is bi-directional */
+ cdata->child_stdout = cdata->child_stdin = cdata->pty_master;
+ }
+ return SSH_OK;
+}
+
+static int exec_nopty(const char *command, struct channel_data_struct *cdata) {
+ int in[2], out[2], err[2];
+
+ /* Do the plumbing to be able to talk with the child process. */
+ if (pipe(in) != 0) {
+ goto stdin_failed;
+ }
+ if (pipe(out) != 0) {
+ goto stdout_failed;
+ }
+ if (pipe(err) != 0) {
+ goto stderr_failed;
+ }
+
+ switch(cdata->pid = fork()) {
+ case -1:
+ goto fork_failed;
+ case 0:
+ /* Finish the plumbing in the child process. */
+ close(in[1]);
+ close(out[0]);
+ close(err[0]);
+ dup2(in[0], STDIN_FILENO);
+ dup2(out[1], STDOUT_FILENO);
+ dup2(err[1], STDERR_FILENO);
+ close(in[0]);
+ close(out[1]);
+ close(err[1]);
+ /* exec the requested command. */
+ execl("/bin/sh", "sh", "-c", command, NULL);
+ exit(0);
+ }
+
+ close(in[0]);
+ close(out[1]);
+ close(err[1]);
+
+ cdata->child_stdin = in[1];
+ cdata->child_stdout = out[0];
+ cdata->child_stderr = err[0];
+
+ return SSH_OK;
+
+fork_failed:
+ close(err[0]);
+ close(err[1]);
+stderr_failed:
+ close(out[0]);
+ close(out[1]);
+stdout_failed:
+ close(in[0]);
+ close(in[1]);
+stdin_failed:
+ return SSH_ERROR;
+}
+
+static int exec_request(ssh_session session, ssh_channel channel,
+ const char *command, void *userdata) {
+ struct channel_data_struct *cdata = (struct channel_data_struct *) userdata;
+
+
+ (void) session;
+ (void) channel;
+
+ if(cdata->pid > 0) {
+ return SSH_ERROR;
+ }
+
+ if (cdata->pty_master != -1 && cdata->pty_slave != -1) {
+ return exec_pty("-c", command, cdata);
+ }
+ return exec_nopty(command, cdata);
+}
+
+static int shell_request(ssh_session session, ssh_channel channel,
+ void *userdata) {
+ struct channel_data_struct *cdata = (struct channel_data_struct *) userdata;
+
+ (void) session;
+ (void) channel;
+
+ if(cdata->pid > 0) {
+ return SSH_ERROR;
+ }
+
+ if (cdata->pty_master != -1 && cdata->pty_slave != -1) {
+ return exec_pty("-l", NULL, cdata);
+ }
+ /* Client requested a shell without a pty, let's pretend we allow that */
+ return SSH_OK;
+}
+
+static int subsystem_request(ssh_session session, ssh_channel channel,
+ const char *subsystem, void *userdata) {
+ /* subsystem requests behave simillarly to exec requests. */
+ if (strcmp(subsystem, "sftp") == 0) {
+ return exec_request(session, channel, SFTP_SERVER_PATH, userdata);
+ }
+ return SSH_ERROR;
+}
+
+static int auth_password(ssh_session session, const char *user,
+ const char *pass, void *userdata) {
+ struct session_data_struct *sdata = (struct session_data_struct *) userdata;
+
+ (void) session;
+
+ if (strcmp(user, username) == 0 && strcmp(pass, password) == 0) {
+ sdata->authenticated = 1;
+ return SSH_AUTH_SUCCESS;
+ }
+
+ sdata->auth_attempts++;
+ return SSH_AUTH_DENIED;
+}
+
+static int auth_publickey(ssh_session session,
+ const char *user,
+ struct ssh_key_struct *pubkey,
+ char signature_state,
+ void *userdata)
+{
+ struct session_data_struct *sdata = (struct session_data_struct *) userdata;
+
+ (void) user;
+ (void) session;
+
+ if (signature_state == SSH_PUBLICKEY_STATE_NONE) {
+ return SSH_AUTH_SUCCESS;
+ }
+
+ if (signature_state != SSH_PUBLICKEY_STATE_VALID) {
+ return SSH_AUTH_DENIED;
+ }
+
+ // valid so far. Now look through authorized keys for a match
+ if (authorizedkeys[0]) {
+ ssh_key key = NULL;
+ int result;
+ struct stat buf;
+
+ if (stat(authorizedkeys, &buf) == 0) {
+ result = ssh_pki_import_pubkey_file( authorizedkeys, &key );
+ if ((result != SSH_OK) || (key==NULL)) {
+ fprintf(stderr,
+ "Unable to import public key file %s\n",
+ authorizedkeys);
+ } else {
+ result = ssh_key_cmp( key, pubkey, SSH_KEY_CMP_PUBLIC );
+ ssh_key_free(key);
+ if (result == 0) {
+ sdata->authenticated = 1;
+ return SSH_AUTH_SUCCESS;
+ }
+ }
+ }
+ }
+
+ // no matches
+ sdata->authenticated = 0;
+ return SSH_AUTH_DENIED;
+}
+
+static int
+auth_message_callback (ssh_session session,
+ ssh_message message,
+ void *user_data)
+{
+ static const char *prompts[2] = { "Password", "Token" };
+ static char echo[] = { 0, 1 };
+ static const char *again[1] = { "So Close" };
+ static char again_echo[] = { 0 };
+
+ struct session_data_struct *sdata = (struct session_data_struct *) user_data;
+
+ if (ssh_message_type (message) != SSH_REQUEST_AUTH
+ || ssh_message_subtype (message) != SSH_AUTH_METHOD_INTERACTIVE)
+ return 1;
+
+ switch (sdata->multi_step_state) {
+ case 1:
+ if (strcmp (ssh_message_auth_user (message), username) == 0)
+ {
+ ssh_message_auth_interactive_request (message, "Test Interactive",
+ "Password and Token",
+ 2, prompts, echo);
+ sdata->multi_step_state = 2;
+ return 0;
+ }
+ else
+ return 1;
+ break;
+
+ case 2:
+ if (ssh_userauth_kbdint_getnanswers(session) != 2)
+ break;
+
+ if (strcmp (ssh_userauth_kbdint_getanswer(session, 0), password) != 0)
+ break;
+
+ if (strcmp (ssh_userauth_kbdint_getanswer(session, 1), "5") == 0) {
+ ssh_message_auth_reply_success (message, 0);
+ sdata->authenticated = 1;
+ return 0;
+ } else if (strcmp (ssh_userauth_kbdint_getanswer(session, 1), "6") == 0) {
+ ssh_message_auth_interactive_request (message, "Test Interactive",
+ "Again", 1, again, again_echo);
+ sdata->multi_step_state = 3;
+ return 0;
+ }
+
+ break;
+
+ case 3:
+ if (ssh_userauth_kbdint_getnanswers(session) != 1)
+ break;
+
+ if (strcmp (ssh_userauth_kbdint_getanswer(session, 0), "5") == 0) {
+ ssh_message_auth_reply_success (message, 0);
+ sdata->authenticated = 1;
+ return 0;
+ }
+
+ break;
+ }
+
+ return 1;
+}
+
+static ssh_channel channel_open(ssh_session session, void *userdata) {
+ struct session_data_struct *sdata = (struct session_data_struct *) userdata;
+
+ sdata->channel = ssh_channel_new(session);
+ return sdata->channel;
+}
+
+static int process_stdout(socket_t fd, int revents, void *userdata) {
+ char buf[BUF_SIZE];
+ int n = -1;
+ ssh_channel channel = (ssh_channel) userdata;
+
+ if (channel != NULL && (revents & POLLIN) != 0) {
+ n = read(fd, buf, BUF_SIZE);
+ if (n > 0) {
+ ssh_channel_write(channel, buf, n);
+ }
+ }
+
+ return n;
+}
+
+static int process_stderr(socket_t fd, int revents, void *userdata) {
+ char buf[BUF_SIZE];
+ int n = -1;
+ ssh_channel channel = (ssh_channel) userdata;
+
+ if (channel != NULL && (revents & POLLIN) != 0) {
+ n = read(fd, buf, BUF_SIZE);
+ if (n > 0) {
+ ssh_channel_write_stderr(channel, buf, n);
+ }
+ }
+
+ return n;
+}
+
+static void handle_session(ssh_event event, ssh_session session) {
+ int n;
+ int rc = 0;
+
+ /* Structure for storing the pty size. */
+ struct winsize wsize = {
+ .ws_row = 0,
+ .ws_col = 0,
+ .ws_xpixel = 0,
+ .ws_ypixel = 0
+ };
+
+ /* Our struct holding information about the channel. */
+ struct channel_data_struct cdata = {
+ .pid = 0,
+ .pty_master = -1,
+ .pty_slave = -1,
+ .child_stdin = -1,
+ .child_stdout = -1,
+ .child_stderr = -1,
+ .event = NULL,
+ .winsize = &wsize,
+ .stdin_buf = NULL,
+ .stdin_len = 0,
+ };
+
+ /* Our struct holding information about the session. */
+ struct session_data_struct sdata = {
+ .channel = NULL,
+ .auth_attempts = 0,
+ .authenticated = 0,
+ .multi_step_state = 1,
+ };
+
+ struct ssh_channel_callbacks_struct channel_cb = {
+ .userdata = &cdata,
+ .channel_pty_request_function = pty_request,
+ .channel_pty_window_change_function = pty_resize,
+ .channel_shell_request_function = shell_request,
+ .channel_exec_request_function = exec_request,
+ .channel_data_function = data_function,
+ .channel_subsystem_request_function = subsystem_request
+ };
+
+ struct ssh_server_callbacks_struct server_cb = {
+ .userdata = &sdata,
+ .auth_password_function = auth_password,
+ .channel_open_request_session_function = channel_open,
+ };
+
+ int auth_methods = SSH_AUTH_METHOD_PASSWORD;
+ if (broken_auth) {
+ auth_methods = SSH_AUTH_METHOD_HOSTBASED;
+ } else {
+ if (authorizedkeys[0]) {
+ server_cb.auth_pubkey_function = auth_publickey;
+ auth_methods |= SSH_AUTH_METHOD_PUBLICKEY;
+ }
+ if (multi_step)
+ auth_methods |= SSH_AUTH_METHOD_INTERACTIVE;
+ }
+
+ ssh_set_auth_methods (session, auth_methods);
+
+ ssh_callbacks_init(&server_cb);
+ ssh_callbacks_init(&channel_cb);
+
+ /* The server callbacks handle password and publickey
+ authentication, the message callback handles interactive
+ authentication.
+ */
+ ssh_set_server_callbacks(session, &server_cb);
+ ssh_set_message_callback (session, auth_message_callback, &sdata);
+
+ if (ssh_handle_key_exchange(session) != SSH_OK) {
+ fprintf(stderr, "%s\n", ssh_get_error(session));
+ return;
+ }
+
+ ssh_event_add_session(event, session);
+
+ n = 0;
+ while (sdata.authenticated == 0 || sdata.channel == NULL) {
+ /* If the user has used up all attempts, or if he hasn't been able to
+ * authenticate in 10 seconds (n * 100ms), disconnect. */
+ if (sdata.auth_attempts >= 3 || n >= 100) {
+ return;
+ }
+
+ if (ssh_event_dopoll(event, 100) == SSH_ERROR) {
+ fprintf(stderr, "%s\n", ssh_get_error(session));
+ return;
+ }
+ n++;
+ }
+
+ ssh_set_channel_callbacks(sdata.channel, &channel_cb);
+ ssh_set_message_callback (session, NULL, NULL);
+
+ do {
+ /* Poll the main event which takes care of the session, the channel and
+ * even our child process's stdout/stderr (once it's started). */
+ if (ssh_event_dopoll(event, -1) == SSH_ERROR) {
+ ssh_channel_close(sdata.channel);
+ }
+
+ /* If child process's stdout/stderr has been registered with the event,
+ * or the child process hasn't started yet, continue. */
+ if (cdata.event != NULL || cdata.pid == 0) {
+ continue;
+ }
+ /* Executed only once, once the child process starts. */
+ cdata.event = event;
+ /* If stdout valid, add stdout to be monitored by the poll event. */
+ if (cdata.child_stdout != -1) {
+ if (ssh_event_add_fd(event, cdata.child_stdout, POLLIN, process_stdout,
+ sdata.channel) != SSH_OK) {
+ fprintf(stderr, "Failed to register stdout to poll context\n");
+ ssh_channel_close(sdata.channel);
+ }
+ }
+
+ /* If stderr valid, add stderr to be monitored by the poll event. */
+ if (cdata.child_stderr != -1){
+ if (ssh_event_add_fd(event, cdata.child_stderr, POLLIN, process_stderr,
+ sdata.channel) != SSH_OK) {
+ fprintf(stderr, "Failed to register stderr to poll context\n");
+ ssh_channel_close(sdata.channel);
+ }
+ }
+ } while(ssh_channel_is_open(sdata.channel) &&
+ (cdata.pid == 0 || waitpid(cdata.pid, &rc, WNOHANG) == 0));
+
+ close(cdata.pty_master);
+ close(cdata.child_stdin);
+ close(cdata.child_stdout);
+ close(cdata.child_stderr);
+
+ /* Remove the descriptors from the polling context, since they are now
+ * closed, they will always trigger during the poll calls. */
+ ssh_event_remove_fd(event, cdata.child_stdout);
+ ssh_event_remove_fd(event, cdata.child_stderr);
+
+ /* If the child process exited. */
+ if (kill(cdata.pid, 0) < 0 && (WIFEXITED(rc) || WIFSIGNALED(rc))) {
+ if (WIFSIGNALED (rc))
+ ssh_channel_request_send_exit_signal (sdata.channel, strsignal (WTERMSIG (rc)), 0, "", "");
+ else
+ ssh_channel_request_send_exit_status (sdata.channel, WEXITSTATUS (rc));
+ /* If client terminated the channel or the process did not exit nicely,
+ * but only if something has been forked. */
+ } else if (cdata.pid > 0) {
+ kill(cdata.pid, SIGKILL);
+ }
+
+ ssh_channel_send_eof(sdata.channel);
+ ssh_channel_close(sdata.channel);
+
+ /* Wait up to 5 seconds for the client to terminate the session. */
+ for (n = 0; n < 50 && (ssh_get_status(session) & SESSION_END) == 0; n++) {
+ ssh_event_dopoll(event, 100);
+ }
+}
+
+#ifdef WITH_FORK
+/* SIGCHLD handler for cleaning up dead children. */
+static void sigchld_handler(int signo) {
+ (void) signo;
+ while (waitpid(-1, NULL, WNOHANG) > 0);
+}
+#else
+static void *session_thread(void *arg) {
+ ssh_session session = arg;
+ ssh_event event;
+
+ event = ssh_event_new();
+ if (event != NULL) {
+ /* Blocks until the SSH session ends by either
+ * child thread exiting, or client disconnecting. */
+ handle_session(event, session);
+ ssh_event_free(event);
+ } else {
+ fprintf(stderr, "Could not create polling context\n");
+ }
+ ssh_disconnect(session);
+ ssh_free(session);
+ return NULL;
+}
+#endif
+
+int main(int argc, char **argv) {
+ ssh_bind sshbind;
+ ssh_session session;
+ int rc;
+#ifdef WITH_FORK
+ struct sigaction sa;
+
+ /* Set up SIGCHLD handler. */
+ sa.sa_handler = sigchld_handler;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
+ if (sigaction(SIGCHLD, &sa, NULL) != 0) {
+ fprintf(stderr, "Failed to register SIGCHLD handler\n");
+ return 1;
+ }
+#endif
+
+ rc = ssh_init();
+ if (rc < 0) {
+ fprintf(stderr, "ssh_init failed\n");
+ return 1;
+ }
+
+ sshbind = ssh_bind_new();
+ if (sshbind == NULL) {
+ fprintf(stderr, "ssh_bind_new failed\n");
+ ssh_finalize();
+ return 1;
+ }
+
+ {
+ // Set mock defaults
+ int port = 0;
+ ssh_bind_options_set (sshbind, SSH_BIND_OPTIONS_BINDPORT, &port);
+ ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_RSAKEY, SRCDIR "/src/ssh/mock_rsa_key");
+ strncpy(authorizedkeys, SRCDIR "/src/ssh/test_rsa.pub", DEF_STR_SIZE-1);
+ }
+
+#ifdef HAVE_ARGP_H
+ argp_parse(&argp, argc, argv, 0, 0, sshbind);
+#else
+ if (parse_opt(argc, argv, sshbind) < 0) {
+ ssh_bind_free(sshbind);
+ ssh_finalize();
+ return 1;
+ }
+#endif /* HAVE_ARGP_H */
+
+ if(ssh_bind_listen(sshbind) < 0) {
+ fprintf(stderr, "%s\n", ssh_get_error(sshbind));
+ ssh_bind_free(sshbind);
+ ssh_finalize();
+ return 1;
+ }
+
+ /* Print out the port */
+ {
+ int bind_fd;
+ int r;
+ char portname[16];
+ char addrname[16];
+ struct sockaddr_storage addr;
+ socklen_t addrlen;
+
+ bind_fd = ssh_bind_get_fd (sshbind);
+
+ addrlen = sizeof (addr);
+ if (getsockname (bind_fd, (struct sockaddr *)&addr, &addrlen) < 0)
+ {
+ fprintf (stderr, "couldn't get local address: %s\n", strerror (errno));
+ return 1;
+ }
+ r = getnameinfo ((struct sockaddr *)&addr, addrlen, addrname, sizeof (addrname),
+ portname, sizeof (portname), NI_NUMERICHOST | NI_NUMERICSERV);
+ if (r != 0)
+ {
+ fprintf (stderr, "couldn't get local port: %s\n", gai_strerror (r));
+ return 1;
+ }
+
+ /* Caller wants to know the port */
+ printf ("%s\n", portname);
+ }
+
+ /* Close stdout to signal startup is complete (once above info is printed) */
+ fflush(stdout);
+ close (1);
+
+ while (1) {
+ session = ssh_new();
+ if (session == NULL) {
+ fprintf(stderr, "Failed to allocate session\n");
+ continue;
+ }
+
+ /* Blocks until there is a new incoming connection. */
+ if(ssh_bind_accept(sshbind, session) != SSH_ERROR) {
+#ifdef WITH_FORK
+ ssh_event event;
+
+ switch(fork()) {
+ case 0:
+ /* Remove the SIGCHLD handler inherited from parent. */
+ sa.sa_handler = SIG_DFL;
+ sigaction(SIGCHLD, &sa, NULL);
+ /* Remove socket binding, which allows us to restart the
+ * parent process, without terminating existing sessions. */
+ ssh_bind_free(sshbind);
+
+ event = ssh_event_new();
+ if (event != NULL) {
+ /* Blocks until the SSH session ends by either
+ * child process exiting, or client disconnecting. */
+ handle_session(event, session);
+ ssh_event_free(event);
+ } else {
+ fprintf(stderr, "Could not create polling context\n");
+ }
+ ssh_disconnect(session);
+ ssh_free(session);
+
+ exit(0);
+ case -1:
+ fprintf(stderr, "Failed to fork\n");
+ }
+#else
+ pthread_t tid;
+
+ rc = pthread_create(&tid, NULL, session_thread, session);
+ if (rc == 0) {
+ pthread_detach(tid);
+ continue;
+ }
+ fprintf(stderr, "Failed to pthread_create\n");
+#endif
+ } else {
+ fprintf(stderr, "%s\n", ssh_get_error(sshbind));
+ }
+ /* Since the session has been passed to a child fork, do some cleaning
+ * up at the parent process. */
+ ssh_disconnect(session);
+ ssh_free(session);
+ }
+
+ ssh_bind_free(sshbind);
+ ssh_finalize();
+ return 0;
+}
diff --git a/src/ssh/mock_ecdsa_key b/src/ssh/mock_ecdsa_key
new file mode 100644
index 0000000..6cab9f6
--- /dev/null
+++ b/src/ssh/mock_ecdsa_key
@@ -0,0 +1,9 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSuD2S/4gae/4UIUb7AYejIP1LC1xiE
+RVk3pBatZyV5twpAZMrGtycYvTFJDaNISAx5ctmzjCgijUqTdlOohgo8AAAAqDO+d0Uzvn
+dFAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBK4PZL/iBp7/hQhR
+vsBh6Mg/UsLXGIRFWTekFq1nJXm3CkBkysa3Jxi9MUkNo0hIDHly2bOMKCKNSpN2U6iGCj
+wAAAAhAOviSdIlwJIeAAbittvzOo4OmXrWyvLYt1VwEtmYScTSAAAADm1hcnRpbkB0b29s
+Ym94AQ==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/src/ssh/mock_known_hosts b/src/ssh/mock_known_hosts
new file mode 100644
index 0000000..820bc28
--- /dev/null
+++ b/src/ssh/mock_known_hosts
@@ -0,0 +1 @@
+[localhost]:*,[127.0.0.1]:*,[::1]:* ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCYzo07OA0H6f7orVun9nIVjGYrkf8AuPDScqWGzlKpAqSipoQ9oY/mwONwIOu4uhKh7FTQCq5p+NaOJ6+Q4z++xBzSOLFseKX+zyLxgNG28jnF06WSmrMsSfvPdNuZKt9rZcQFKn9fRNa8oixa+RsqEEVEvTYhGtRf7w2wsV49xIoIza/bln1ABX1YLaCByZow+dK3ZlHn/UU0r4ewpAIZhve4vCvAsMe5+6KJH8ft/OKXXQY06h6jCythLV4h18gY/sYosOa+/4XgpmBiE7fDeFRKVjP3mvkxMpxce+ckOFae2+aJu51h513S9kxY2PmKaV/JU9HBYO+yO4j+j24v
diff --git a/src/ssh/mock_rsa_key b/src/ssh/mock_rsa_key
new file mode 100644
index 0000000..df9ea2c
--- /dev/null
+++ b/src/ssh/mock_rsa_key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAmM6NOzgNB+n+6K1bp/ZyFYxmK5H/ALjw0nKlhs5SqQKkoqaE
+PaGP5sDjcCDruLoSoexU0AquafjWjievkOM/vsQc0jixbHil/s8i8YDRtvI5xdOl
+kpqzLEn7z3TbmSrfa2XEBSp/X0TWvKIsWvkbKhBFRL02IRrUX+8NsLFePcSKCM2v
+25Z9QAV9WC2ggcmaMPnSt2ZR5/1FNK+HsKQCGYb3uLwrwLDHufuiiR/H7fzil10G
+NOoeowsrYS1eIdfIGP7GKLDmvv+F4KZgYhO3w3hUSlYz95r5MTKcXHvnJDhWntvm
+ibudYedd0vZMWNj5imlfyVPRwWDvsjuI/o9uLwIDAQABAoIBAGIbc2fLA+rJ3ITN
+EOTVAMg+/TYKJVv0YYHsY8QaYc3rSYK4QH1FZpuzyhKqwE05Ak996bIsuXCGeFKK
+vlja3ol/ZjW+eoN3LrRbj0bY+0xnVpph2ZM3ycOsuISotXkwooNUsjbS4zZqfyhb
+QvkhqMQn8CFDDibRD/uMAxEnv6cNs8zyJu+RgfA1kxW/BAKjKg0mf5EwltMwfLCc
+O+3tQZR0P8w6UXSBLrGZvPUvWjLZHuhSAzeRiA4deEerX5tEOwkx/iUbyma2HOh/
+p9BKpaJ67RH+UdwysV6x8JRRXAhTt8jy+5T1OdaOYYisn71M4PVnajmWwTbZ7Vy6
+XJx1MVECgYEAxlTXF1JPyMoxqgWhV222f+UONyuNnB1mKNP5kSI1WIBrsWSkOqvQ
+a6pihSGKaQFLMVzBkfrOAkSGJTtJuOrhA3lk3thJsG4ACuX+MgfXi/xqQpvX3SBx
+QS23YfxC86HOBkDUKr/xHSHJPKidNVlOtQbyDioxuqR2Vk2IrsvJmZkCgYEAxT0B
+7mP0JypL576pW1CE2LhrqTmmTBy3qVPAjV4fnv7KduH2I/a88WVmOWRSDd14RCBf
+h1JG/mUkWgKaiY8Dh9dCGiJBe06OHKRFjmC+AXx/R0IwOHD1i1kVIRz5t5nzNhLs
+cXWYNIFDxW9zN+z8aNiRZ0+0oGvD6MGZI5FA8wcCgYAgEO+F0cUcjbRh7O8dF5v+
+KaaWvO/0Ybx2tW8QTBub54eB8ueqpMTZ435yT+309ESYAos5cveD3lhiBKfywecH
+jMUGUqn3YJGZMX5b0HDFLVZw1omcMvactyuDKCobAfoxgKHNF6OANko3CHwCeCIF
+ms3gBGpt5tFLOtXyjPhXYQKBgHwzt6SbkqKbEuNi/5h98rnaIAmXQO4K42igUc9z
+puvjPDFyhyIMfNxx4tZfIwCSJuqXjDBVklkd7a80loXq5elDlt+IFL4GMJ0+oIJQ
+zzV2ZVvFBUJ1d1oBhbmtWl7QdgPmFLg5udfOUpPgY6ii9is7vQxWd8LRObO3PazV
+ChY/AoGBAI9N9tF49xDcyJDEmS7X2LQu81QqGQuKRlv0/z/jqUjqWsDVnvNdKePC
+bs+yHRQciDYQB/g+FSUHE7bh8CN0qrQrttEk69sfm7gK8rusK16RIrQ7cHo2WnjG
+82WGZ9YvmnUq3qkCzyS/po1TI3Mr1sHnY2xwcTS/DOzAnD2wNsU0
+-----END RSA PRIVATE KEY-----
diff --git a/src/ssh/ssh.c b/src/ssh/ssh.c
new file mode 100644
index 0000000..ecb7186
--- /dev/null
+++ b/src/ssh/ssh.c
@@ -0,0 +1,105 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <glib-unix.h>
+
+#include "common/cockpithacks-glib.h"
+#include "common/cockpitsystem.h"
+
+#include "cockpitsshrelay.h"
+
+static gboolean
+on_exit_signal (gpointer data)
+{
+ GMainLoop *loop = data;
+ g_debug ("Received exit signal, shutting down");
+ g_main_loop_quit (loop);
+ return TRUE;
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gint ret = 1;
+ CockpitSshRelay *relay;
+ GOptionContext *context;
+ GError *error = NULL;
+ GMainLoop *loop = NULL;
+
+ cockpit_hacks_redirect_gdebug_to_stderr ();
+
+ signal (SIGALRM, SIG_DFL);
+ signal (SIGQUIT, SIG_DFL);
+ signal (SIGTSTP, SIG_IGN);
+ signal (SIGHUP, SIG_IGN);
+ signal (SIGPIPE, SIG_IGN);
+
+ cockpit_setenv_check ("GSETTINGS_BACKEND", "memory", TRUE);
+ cockpit_setenv_check ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE);
+ cockpit_setenv_check ("GIO_USE_VFS", "local", TRUE);
+
+ context = g_option_context_new ("- cockpit-ssh [user@]host[:port]");
+
+ if (!g_option_context_parse (context, &argc, &argv, &error))
+ {
+ ret = INTERNAL_ERROR;
+ goto out;
+ }
+
+ if (argc != 2)
+ {
+ g_printerr ("cockpit-ssh: unexpected additional arguments, see --help\n");
+ ret = INTERNAL_ERROR;
+ goto out;
+ }
+
+ loop = g_main_loop_new (NULL, FALSE);
+
+ relay = cockpit_ssh_relay_new (argv[1]);
+ g_signal_connect_swapped (relay, "disconnect", G_CALLBACK (g_main_loop_quit), loop);
+
+ guint sig_term = g_unix_signal_add (SIGTERM, on_exit_signal, loop);
+ guint sig_int = g_unix_signal_add (SIGINT, on_exit_signal, loop);
+
+ g_main_loop_run (loop);
+
+ ret = cockpit_ssh_relay_result (relay);
+ g_object_unref (relay);
+
+ g_source_remove (sig_term);
+ g_source_remove (sig_int);
+
+out:
+ g_option_context_free (context);
+
+ if (error)
+ {
+ g_printerr ("cockpit-ssh: %s\n", error->message);
+ g_error_free (error);
+ }
+
+ if (loop)
+ g_main_loop_unref (loop);
+
+ return ret;
+}
diff --git a/src/ssh/test-sshbridge.c b/src/ssh/test-sshbridge.c
new file mode 100644
index 0000000..9c561e2
--- /dev/null
+++ b/src/ssh/test-sshbridge.c
@@ -0,0 +1,1485 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitauthorize.h"
+#include "testlib/cockpittest.h"
+#include "common/cockpiterror.h"
+#include "common/cockpitpipe.h"
+#include "common/cockpitpipetransport.h"
+#include "common/cockpitjson.h"
+
+#include <sys/wait.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#define TIMEOUT 120
+
+#define WAIT_UNTIL(cond) \
+ G_STMT_START \
+ while (!(cond)) g_main_context_iteration (NULL, TRUE); \
+ G_STMT_END
+
+#define PASSWORD "this is the password"
+
+#define INVALID_KEY ""
+
+typedef struct {
+ CockpitTransport *transport;
+ gboolean closed;
+
+ /* setup_mock_sshd */
+ GPid mock_sshd;
+ guint16 ssh_port;
+ gchar *home_dir;
+ gchar *home_ssh_dir;
+ gchar *home_knownhosts_file;
+ gchar *home_ssh_config_file;
+} TestCase;
+
+typedef struct {
+ const char *ssh_command;
+ const char *mock_sshd_arg;
+ const char *mock_sshd_arg_value;
+ const char *client_password;
+ const char *hostname;
+ const char *username;
+ const char *knownhosts_file;
+ const char *knownhosts_home;
+ const char *host_key_authorize; /* authorize x-host-key response for test_problem() */
+ const char *config;
+ const char *problem;
+ const char *ssh_config_identity_file;
+ gboolean allow_unknown;
+ gboolean test_home_ssh_config;
+ enum { USER_NONE = 0, USER_INVALID, USER_INVALID_HOST_PRIORITY, USER_ME } ssh_config_user;
+ enum { PORT_VALID = 0, PORT_INVALID_HOST_PRIORITY } ssh_config_port;
+} TestFixture;
+
+/* check if /proc/net/if_inet6 is non-empty, otherwise there is no IPv6 support */
+static gboolean
+have_ipv6 (void)
+{
+ int fd;
+ gboolean avail = FALSE;
+
+ fd = open ("/proc/net/if_inet6", O_RDONLY);
+ if (fd >= 0)
+ {
+ char c;
+ avail = read (fd, &c, 1) == 1;
+ close (fd);
+ }
+ return avail;
+}
+
+static GString *
+read_all_into_string (int fd)
+{
+ GString *input = g_string_new ("");
+ gsize len;
+ gssize ret;
+
+ for (;;)
+ {
+ len = input->len;
+ g_string_set_size (input, len + 256);
+ ret = read (fd, input->str + len, 256);
+ if (ret < 0)
+ {
+ if (errno != EAGAIN)
+ {
+ g_critical ("couldn't read from mock input: %s", g_strerror (errno));
+ g_string_free (input, TRUE);
+ return NULL;
+ }
+ }
+ else if (ret == 0)
+ {
+ return input;
+ }
+ else
+ {
+ input->len = len + ret;
+ input->str[input->len] = '\0';
+ }
+ }
+}
+
+static void
+spawn_setup (gpointer data)
+{
+ int fd = GPOINTER_TO_INT (data);
+
+ /* Send this signal to all direct child processes, when bridge dies */
+ prctl (PR_SET_PDEATHSIG, SIGHUP);
+
+ g_assert_cmpint (dup2 (fd, 0), >, -1);
+ g_assert_cmpint (dup2 (fd, 1), >, -1);
+
+ close (fd);
+}
+
+static void
+setup_mock_sshd (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ GError *error = NULL;
+ GString *port;
+ gchar *endptr;
+ guint64 value;
+ gint out_fd;
+
+ const gchar *argv[] = {
+ BUILDDIR "/mock-sshd",
+ "--bind", fixture->hostname ?: "127.0.0.1",
+ "--user", g_get_user_name (),
+ "--password", PASSWORD,
+ fixture->mock_sshd_arg, fixture->mock_sshd_arg_value,
+ NULL
+ };
+
+ g_spawn_async_with_pipes (BUILDDIR, (gchar **)argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL,
+ &tc->mock_sshd, NULL, &out_fd, NULL, &error);
+ g_assert_no_error (error);
+
+ /*
+ * mock-sshd prints its port on stdout, and then closes stdout
+ * This also lets us know when it has initialized.
+ */
+
+ port = read_all_into_string (out_fd);
+ g_assert (port != NULL);
+ close (out_fd);
+ g_assert_no_error (error);
+
+ g_strstrip (port->str);
+ value = g_ascii_strtoull (port->str, &endptr, 10);
+ if (!endptr || *endptr != '\0' || value == 0 || value > G_MAXUSHORT)
+ g_critical ("invalid port printed by mock-sshd: %s", port->str);
+
+ tc->ssh_port = (gushort)value;
+ g_string_free (port, TRUE);
+}
+
+static const TestFixture fixture_mock_echo = {
+ .ssh_command = BUILDDIR "/mock-echo"
+};
+
+static const TestFixture fixture_cat = {
+ .ssh_command = SRCDIR "/src/ws/mock-cat-with-init"
+};
+
+static const TestFixture fixture_ipv6_address = {
+ .ssh_command = BUILDDIR "/mock-echo",
+ .hostname = "::1",
+};
+
+static gchar **
+setup_env (const TestFixture *fix)
+{
+ const gchar *command;
+ const gchar *knownhosts_file;
+ const gchar *config;
+ gchar **env = g_get_environ ();
+
+ config = fix ? fix->config : NULL;
+ if (!config)
+ config = SRCDIR "/src/ssh/mock-config";
+ env = g_environ_setenv (env, "XDG_CONFIG_DIRS", config, TRUE);
+
+ command = fix ? fix->ssh_command : NULL;
+ if (!command)
+ command = fixture_cat.ssh_command;
+ env = g_environ_setenv (env, "COCKPIT_SSH_BRIDGE_COMMAND", command, TRUE);
+
+ if (fix && fix->allow_unknown)
+ {
+ env = g_environ_setenv (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS",
+ "true", TRUE);
+ }
+
+ knownhosts_file = fix ? fix->knownhosts_file : NULL;
+ if (!knownhosts_file)
+ knownhosts_file = SRCDIR "/src/ssh/mock_known_hosts";
+
+ env = g_environ_setenv (env, "COCKPIT_SSH_KNOWN_HOSTS_FILE",
+ knownhosts_file, TRUE);
+ return env;
+}
+
+static CockpitTransport *
+start_bridge (gchar **env,
+ gchar **argv)
+{
+ GError *error = NULL;
+ int fds[2];
+
+ g_assert_cmpint (socketpair (PF_LOCAL, SOCK_STREAM, 0, fds), ==, 0);
+ g_spawn_async_with_pipes (BUILDDIR, argv, env,
+ G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH,
+ spawn_setup, GINT_TO_POINTER (fds[0]),
+ NULL, NULL, NULL, NULL, &error);
+ g_assert_no_error (error);
+ close (fds[0]);
+
+ return cockpit_pipe_transport_new_fds ("test-ssh", fds[1], fds[1]);
+}
+
+static void
+on_closed_set_flag (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ gboolean *flag = user_data;
+ g_assert_cmpstr (problem, ==, NULL);
+ g_assert (*flag == FALSE);
+ *flag = TRUE;
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ const gchar *argv[] = { BUILDDIR "/cockpit-ssh", NULL, NULL };
+ const gchar *hostname = fixture->hostname ?: "127.0.0.1";
+ gchar **env = NULL;
+ gchar *host = NULL;
+ gchar *path = NULL;
+
+ alarm (TIMEOUT);
+
+ g_assert (fixture != NULL);
+
+ env = setup_env (fixture);
+ setup_mock_sshd (tc, data);
+
+ if (tc->ssh_port && strchr (hostname, ':') != NULL) /* bracket IPv6 addresses */
+ host = g_strdup_printf ("[%s]:%d", hostname, tc->ssh_port);
+ else if (tc->ssh_port)
+ host = g_strdup_printf ("%s:%d", hostname, tc->ssh_port);
+ else
+ host = g_strdup (hostname);
+ argv[1] = host;
+
+ /* run our tests with temp home dir, to avoid influence from the real ~/.ssh */
+ tc->home_dir = g_dir_make_tmp ("home.XXXXXX", NULL);
+ g_assert (tc->home_dir != NULL);
+ env = g_environ_setenv (env, "HOME", tc->home_dir, TRUE);
+ /* use preload library to bend getpwuid_r home dir to the temporary one */
+ env = g_environ_setenv (env, "LD_PRELOAD", BUILDDIR "/libpreload-temp-home.so", TRUE);
+
+ tc->home_ssh_dir = g_build_filename (tc->home_dir, ".ssh", NULL);
+ g_assert (tc->home_ssh_dir != NULL);
+
+ if (fixture->knownhosts_home)
+ {
+ gchar *content;
+
+ tc->home_knownhosts_file = g_build_filename (tc->home_ssh_dir, "known_hosts", NULL);
+ g_assert (tc->home_knownhosts_file != NULL);
+ g_assert_cmpint (mkdir (tc->home_ssh_dir, 0700), ==, 0);
+
+ content = g_strdup_printf ("[%s]:%d %s\n",
+ fixture->hostname ?: "127.0.0.1",
+ (int)tc->ssh_port,
+ fixture->knownhosts_home);
+
+ g_assert (g_file_set_contents (tc->home_knownhosts_file, content, -1, NULL));
+
+ g_free (content);
+ }
+
+ if (fixture->test_home_ssh_config)
+ {
+ g_autoptr(GString) content = g_string_new (NULL);
+ g_autoptr(GString) new_host = g_string_new (NULL);
+
+ tc->home_ssh_config_file = g_build_filename (tc->home_ssh_dir, "config", NULL);
+ if (!fixture->knownhosts_home)
+ g_assert_cmpint (mkdir (tc->home_ssh_dir, 0700), ==, 0);
+
+ g_string_append (content, "Host somehost\n");
+ g_string_append_printf (content, "\tHostname %s\n", hostname);
+
+ if (fixture->ssh_config_port == PORT_VALID)
+ g_string_append_printf (content, "\tPort %hu\n", tc->ssh_port);
+ else if (fixture->ssh_config_port == PORT_INVALID_HOST_PRIORITY)
+ g_string_append_printf (content, "\tPort %d\n", (tc->ssh_port - 1));
+
+ if (fixture->ssh_config_user == USER_ME)
+ g_string_append_printf (content, "\tUser %s\n", g_get_user_name ());
+ else if (fixture->ssh_config_user == USER_INVALID || fixture->ssh_config_user == USER_INVALID_HOST_PRIORITY)
+ g_string_append (content, "\tUser invalid\n");
+
+ if (fixture->ssh_config_identity_file)
+ g_string_append_printf (content, "\tIdentityFile %s\n", fixture->ssh_config_identity_file);
+
+ g_assert (g_file_set_contents (tc->home_ssh_config_file, content->str, -1, NULL));
+
+ g_free (host);
+ /* The user in host should take priority over the user in ssh config */
+ if (fixture->ssh_config_user == USER_INVALID_HOST_PRIORITY)
+ g_string_append_printf (new_host, "%s@", g_get_user_name ());
+ /* Host in the ssh config file */
+ g_string_append (new_host, "somehost");
+ /* The port in host should take priority over the port in ssh config */
+ if (fixture->ssh_config_port == PORT_INVALID_HOST_PRIORITY)
+ g_string_append_printf (new_host, ":%hu", tc->ssh_port);
+
+ host = g_strdup (new_host->str);
+ argv[1] = host;
+ }
+
+ tc->transport = start_bridge (env, (gchar **) argv);
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &tc->closed);
+ g_strfreev (env);
+ g_free (host);
+ g_free (path);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ if (tc->home_knownhosts_file)
+ {
+ unlink (tc->home_knownhosts_file);
+ g_free (tc->home_knownhosts_file);
+ }
+ if (tc->home_ssh_config_file)
+ {
+ unlink (tc->home_ssh_config_file);
+ g_free (tc->home_ssh_config_file);
+ }
+ if (tc->home_ssh_dir)
+ {
+ rmdir (tc->home_ssh_dir);
+ g_free (tc->home_ssh_dir);
+ }
+ rmdir (tc->home_dir);
+ g_free (tc->home_dir);
+
+ WAIT_UNTIL (tc->closed == TRUE);
+ g_object_add_weak_pointer (G_OBJECT (tc->transport), (gpointer*)&tc->transport);
+ g_object_unref (tc->transport);
+
+ /* If this asserts, outstanding references */
+ g_assert (tc->transport == NULL);
+
+ if (tc->mock_sshd)
+ {
+ kill (tc->mock_sshd, SIGTERM);
+ g_assert_cmpint (waitpid (tc->mock_sshd, 0, 0), ==, tc->mock_sshd);
+ g_spawn_close_pid (tc->mock_sshd);
+ }
+
+ alarm (0);
+}
+
+static gboolean
+on_recv_get_payload (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *message,
+ gpointer user_data)
+{
+ GBytes **received = user_data;
+ if (channel == NULL)
+ return FALSE;
+ g_assert_cmpstr (channel, ==, "546");
+ g_assert (*received == NULL);
+ *received = g_bytes_ref (message);
+ return TRUE;
+}
+
+static gboolean
+on_recv_multiple (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *message,
+ gpointer user_data)
+{
+ gint *state = user_data;
+ GBytes *check = NULL;
+
+ if (channel == NULL)
+ return FALSE;
+
+ g_assert_cmpstr (channel, ==, "9");
+
+ if (*state == 0)
+ check = g_bytes_new_static ("one", 3);
+ else if (*state == 1)
+ check = g_bytes_new_static ("two", 3);
+ else
+ g_assert_not_reached ();
+
+ (*state)++;
+ g_assert (g_bytes_equal (message, check));
+ g_bytes_unref (check);
+
+ return TRUE;
+}
+
+static gboolean
+on_control_get_options (CockpitTransport *transport,
+ const gchar *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload,
+ gpointer user_data)
+{
+ JsonObject **ret_options = user_data;
+ g_assert (ret_options);
+ g_assert (*ret_options == NULL);
+ *ret_options = json_object_ref (options);
+ return TRUE;
+}
+
+static void
+do_auth_response (CockpitTransport *transport,
+ const gchar *challenge,
+ const gchar *response)
+{
+ JsonObject *auth = NULL;
+ GBytes *payload = NULL;
+ const gchar *cookie;
+ guint sig = 0;
+
+ sig = g_signal_connect (transport, "control",
+ G_CALLBACK (on_control_get_options),
+ &auth);
+ WAIT_UNTIL (auth != NULL);
+ g_signal_handler_disconnect (transport, sig);
+ g_assert (cockpit_json_get_string (auth, "cookie", NULL, &cookie));
+ g_assert_cmpstr (json_object_get_string_member (auth, "command"),
+ ==, "authorize");
+ g_assert_cmpstr (json_object_get_string_member (auth, "challenge"),
+ ==, challenge);
+ g_assert_cmpstr (cookie, !=, NULL);
+
+ payload = cockpit_transport_build_control ("command", "authorize",
+ "cookie", cookie,
+ "response", response,
+ NULL);
+ cockpit_transport_send (transport, NULL, payload);
+ g_bytes_unref (payload);
+
+ json_object_unref (auth);
+}
+
+static void
+do_basic_auth (CockpitTransport *transport,
+ const gchar *challenge,
+ const gchar *user,
+ const gchar *password)
+{
+ gchar *userpass = NULL;
+ gchar *encoded = NULL;
+ gchar *response = NULL;
+
+ userpass = g_strdup_printf ("%s:%s", user, password);
+ encoded = g_base64_encode ((guchar *)userpass, strlen (userpass));
+ response = g_strdup_printf ("Basic %s", encoded);
+
+ do_auth_response (transport, challenge, response);
+
+ g_free (userpass);
+ g_free (response);
+ g_free (encoded);
+}
+
+static void
+do_fixture_auth (CockpitTransport *transport,
+ gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ const gchar *user;
+ const gchar *password;
+
+ password = fixture->client_password ? fixture->client_password : PASSWORD;
+ user = fixture->username ? fixture->username : g_get_user_name ();
+ do_basic_auth (transport, "*", user, password);
+}
+
+static JsonObject *
+wait_until_transport_init (CockpitTransport *transport,
+ const gchar *expect_problem)
+{
+ JsonObject *init = NULL;
+ guint sig;
+ const gchar *problem;
+
+ sig = g_signal_connect (transport, "control",
+ G_CALLBACK (on_control_get_options),
+ &init);
+ WAIT_UNTIL (init != NULL);
+ g_signal_handler_disconnect (transport, sig);
+
+ g_assert_cmpstr (json_object_get_string_member (init, "command"),
+ ==, "init");
+ g_assert (cockpit_json_get_string (init, "problem", NULL, &problem));
+ g_assert_cmpstr (problem, ==, expect_problem);
+ return init;
+}
+
+static void
+do_echo_and_close (TestCase *tc)
+{
+ GBytes *received = NULL;
+ GBytes *sent;
+ gboolean closed = FALSE;
+
+ sent = g_bytes_new_static ("the message", 11);
+ g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_get_payload), &received);
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+ cockpit_transport_send (tc->transport, "546", sent);
+
+ while (received == NULL && !closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert (!closed);
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ cockpit_transport_close (tc->transport, NULL);
+
+ while (received == NULL && !closed)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert (closed);
+ g_assert (received == NULL);
+}
+
+static void
+test_echo_and_close (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+
+ do_fixture_auth (tc->transport, data);
+ init = wait_until_transport_init (tc->transport, NULL);
+ do_echo_and_close (tc);
+ json_object_unref (init);
+}
+
+static void
+test_echo_queue (TestCase *tc,
+ gconstpointer data)
+{
+ GBytes *sent;
+ gint state = 0;
+ gboolean closed = FALSE;
+ JsonObject *init = NULL;
+
+ do_fixture_auth (tc->transport, data);
+ init = wait_until_transport_init (tc->transport, NULL);
+
+ g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_multiple), &state);
+ g_signal_connect (tc->transport, "closed", G_CALLBACK (on_closed_set_flag), &closed);
+
+ sent = g_bytes_new_static ("one", 3);
+ cockpit_transport_send (tc->transport, "9", sent);
+ g_bytes_unref (sent);
+ sent = g_bytes_new_static ("two", 3);
+ cockpit_transport_send (tc->transport, "9", sent);
+ g_bytes_unref (sent);
+
+ while (state != 2)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Only closes after above are sent */
+ cockpit_transport_close (tc->transport, NULL);
+
+ while (!closed)
+ g_main_context_iteration (NULL, TRUE);
+ json_object_unref (init);
+}
+
+static void
+test_echo_large (TestCase *tc,
+ gconstpointer data)
+{
+ GBytes *received = NULL;
+ GBytes *sent;
+ JsonObject *init = NULL;
+
+ /* HACK: TODO: find out exactly why this test is so slow under Valgrind */
+ if (cockpit_test_skip_slow ())
+ {
+ tc->closed = TRUE;
+ return;
+ }
+
+ do_fixture_auth (tc->transport, data);
+ init = wait_until_transport_init (tc->transport, NULL);
+
+ g_signal_connect (tc->transport, "recv", G_CALLBACK (on_recv_get_payload), &received);
+
+ /* Medium length */
+ sent = g_bytes_new_take (g_strnfill (1020, '!'), 1020);
+ cockpit_transport_send (tc->transport, "546", sent);
+ while (received == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ /* Extra large */
+ sent = g_bytes_new_take (g_strnfill (10 * 1000 * 1000, '?'), 10 * 1000 * 1000);
+ cockpit_transport_send (tc->transport, "546", sent);
+ while (received == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ /* Double check that didn't screw things up */
+ sent = g_bytes_new_static ("yello", 5);
+ cockpit_transport_send (tc->transport, "546", sent);
+ while (received == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ cockpit_transport_close (tc->transport, NULL);
+ json_object_unref (init);
+}
+
+#define MOCK_RSA_KEY "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCYzo07OA0H6f7orVun9nIVjGYrkf8AuPDScqWGzlKpAqSipoQ9oY/mwONwIOu4uhKh7FTQCq5p+NaOJ6+Q4z++xBzSOLFseKX+zyLxgNG28jnF06WSmrMsSfvPdNuZKt9rZcQFKn9fRNa8oixa+RsqEEVEvTYhGtRf7w2wsV49xIoIza/bln1ABX1YLaCByZow+dK3ZlHn/UU0r4ewpAIZhve4vCvAsMe5+6KJH8ft/OKXXQY06h6jCythLV4h18gY/sYosOa+/4XgpmBiE7fDeFRKVjP3mvkxMpxce+ckOFae2+aJu51h513S9kxY2PmKaV/JU9HBYO+yO4j+j24v\n"
+#define MOCK_RSA_KEY_INVALID "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7YmnYAJaC579hyNFzcszH+ZFQeDuR8I2li1vCgKeM0lOIkV5TwCY4Tl1lbXI7NNffDACQnUrJfNNm6FamdhVzFEvyQAk+iQz/Wz6lHbDlY2dVvoVaJzNWyqXu/qaYs8Mb2QUmNXKtYk4IuM8PH88z5L4JwZXRbOEPOxnJNcaazP9pBhN/0TrHALaXwW29BR0SIJicJqK2r/mPuDovg/SWs8NdgY9DTAAfzdELshTigVXlc1AX6vo71x3O9NWMaPKZuy88o0BeQNI+mkVeV04Pewm3bUlDsr3VeEcd4D+Ixdyfg4+S57K1in0kHQD4PXrd/x5GoCZekxgUuBoE7HVB\n"
+
+static const gchar MOCK_RSA_FP[] = "SHA256:XQ8a7zGxMFstDrGecBRUP9OMnOUXd/T3vkNGtYShs2w";
+#define SSH_PUBLICKEY_HASH_NAME "SHA256"
+
+#define MOCK_ECDSA_PUB_KEY "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBK4PZL/iBp7/hQhRvsBh6Mg/UsLXGIRFWTekFq1nJXm3CkBkysa3Jxi9MUkNo0hIDHly2bOMKCKNSpN2U6iGCjw="
+
+static void
+do_auth_conversation (CockpitTransport *transport,
+ const gchar *expect_prompt,
+ const gchar *expect_json,
+ const gchar *response,
+ gboolean add_header)
+{
+ JsonObject *auth = NULL;
+ GBytes *payload = NULL;
+ const gchar *cookie;
+ const gchar *challenge = NULL;
+ guint sig = 0;
+ gchar *encoded = NULL;
+ gchar *full = NULL;
+ gchar *result = NULL;
+
+ if (add_header)
+ {
+ encoded = g_base64_encode ((guchar *)response, strlen (response));
+ full = g_strdup_printf ("x-conversation id %s", encoded);
+ }
+ else
+ {
+ full = g_strdup (response);
+ }
+
+ sig = g_signal_connect (transport, "control",
+ G_CALLBACK (on_control_get_options),
+ &auth);
+ WAIT_UNTIL (auth != NULL);
+ g_signal_handler_disconnect (transport, sig);
+
+ g_assert (cockpit_json_get_string (auth, "cookie", NULL, &cookie));
+ g_assert_cmpstr (json_object_get_string_member (auth, "command"),
+ ==, "authorize");
+ g_assert_cmpstr (cookie, !=, NULL);
+
+ challenge = json_object_get_string_member (auth, "challenge");
+ result = cockpit_authorize_parse_x_conversation (challenge, NULL);
+ g_assert_cmpstr (result, ==, expect_prompt);
+
+ json_object_remove_member (auth, "cookie");
+ json_object_remove_member (auth, "command");
+ json_object_remove_member (auth, "challenge");
+ cockpit_assert_json_eq (auth, expect_json);
+
+ payload = cockpit_transport_build_control ("command", "authorize",
+ "cookie", "cookie",
+ "response", full,
+ NULL);
+ cockpit_transport_send (transport, NULL, payload);
+ g_bytes_unref (payload);
+ g_free (full);
+ g_free (encoded);
+ g_free (result);
+ json_object_unref (auth);
+}
+
+static void
+do_hostkey_conversation (TestCase *tc,
+ const gchar *response,
+ gboolean add_header)
+{
+ gchar *expect_json = NULL;
+ expect_json = g_strdup_printf ("{\"message\": \"The authenticity of host '127.0.0.1:%d' can't be established. Do you want to proceed this time?\", \"default\": \"%s\", \"host-key\": \"[127.0.0.1]:%d %s\", \"echo\": true }",
+ (int)tc->ssh_port, MOCK_RSA_FP,
+ (int)tc->ssh_port, MOCK_RSA_KEY);
+
+ do_auth_conversation (tc->transport, SSH_PUBLICKEY_HASH_NAME " Fingerprint (ssh-rsa):",
+ expect_json, response, add_header);
+ g_free (expect_json);
+}
+
+static void
+check_host_key_values (TestCase *tc,
+ JsonObject *init,
+ const char *hostname)
+{
+ gchar *knownhosts = g_strdup_printf ("[%s]:%d %s",
+ hostname ?: "127.0.0.1",
+ (int)tc->ssh_port,
+ MOCK_RSA_KEY);
+
+ g_assert_cmpstr (json_object_get_string_member (init, "host-key"),
+ ==, knownhosts);
+ g_assert_cmpstr (json_object_get_string_member (init, "host-fingerprint"),
+ ==, MOCK_RSA_FP);
+
+ g_free (knownhosts);
+}
+
+static void
+test_problem (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ const TestFixture *fix = data;
+
+ do_fixture_auth (tc->transport, data);
+ if (fix->host_key_authorize)
+ do_auth_response (tc->transport, "x-host-key", fix->host_key_authorize);
+ init = wait_until_transport_init (tc->transport, fix->problem);
+ json_object_unref (init);
+}
+
+static const TestFixture fixture_unknown_localhost = {
+ .knownhosts_file = "/dev/null",
+ .host_key_authorize = INVALID_KEY,
+ .ssh_command = BUILDDIR "/mock-echo"
+};
+
+static const TestFixture fixture_unknown_host = {
+ .knownhosts_file = "/dev/null",
+ .hostname = "127.0.0.99",
+ .host_key_authorize = INVALID_KEY,
+ .problem = "unknown-host"
+};
+
+static const TestFixture fixture_known_host_home = {
+ .knownhosts_file = "/dev/null",
+ .knownhosts_home = MOCK_RSA_KEY,
+ .ssh_command = BUILDDIR "/mock-echo"
+};
+
+static const TestFixture fixture_home_ssh_config = {
+ .knownhosts_file = "/dev/null",
+ .test_home_ssh_config = TRUE,
+ .knownhosts_home = MOCK_RSA_KEY,
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo"
+};
+
+static const TestFixture fixture_ssh_config_valid_user = {
+ .knownhosts_file = "/dev/null",
+ .test_home_ssh_config = TRUE,
+ .ssh_config_user = USER_ME,
+ .knownhosts_home = MOCK_RSA_KEY,
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo"
+};
+
+static const TestFixture fixture_ssh_config_invalid_user = {
+ .knownhosts_file = "/dev/null",
+ .test_home_ssh_config = TRUE,
+ .ssh_config_user = USER_INVALID,
+ .knownhosts_home = MOCK_RSA_KEY,
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo",
+ .problem = "authentication-failed"
+};
+
+static const TestFixture fixture_ssh_config_invalid_user_host_priority = {
+ .knownhosts_file = "/dev/null",
+ .test_home_ssh_config = TRUE,
+ .ssh_config_user = USER_INVALID_HOST_PRIORITY,
+ .knownhosts_home = MOCK_RSA_KEY,
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo",
+ .problem = "authentication-failed"
+};
+
+static const TestFixture fixture_ssh_config_invalid_port_host_priority = {
+ .knownhosts_file = "/dev/null",
+ .test_home_ssh_config = TRUE,
+ .knownhosts_home = MOCK_RSA_KEY,
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo",
+ .ssh_config_port = PORT_INVALID_HOST_PRIORITY
+};
+
+static const TestFixture fixture_ssh_config_good_key = {
+ .knownhosts_file = "/dev/null",
+ .test_home_ssh_config = TRUE,
+ .ssh_config_user = USER_ME,
+ .ssh_config_identity_file = SRCDIR "/src/ssh/test_rsa",
+ .client_password = "bad password", /* we don't need this password because the key will work */
+ .knownhosts_home = MOCK_RSA_KEY,
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo",
+};
+
+static const TestFixture fixture_ssh_config_good_key_password_protected = {
+ .knownhosts_file = "/dev/null",
+ .test_home_ssh_config = TRUE,
+ .ssh_config_user = USER_ME,
+ .ssh_config_identity_file = SRCDIR "/src/ssh/test_rsa_password_protected",
+ .client_password = "bad password",
+ .knownhosts_home = MOCK_RSA_KEY,
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo",
+ .mock_sshd_arg = "--import-pubkey",
+ .mock_sshd_arg_value = SRCDIR "/src/ssh/test_rsa_password_protected.pub",
+ .problem = "authentication-failed",
+};
+
+static const TestFixture fixture_ssh_config_bad_key = {
+ .knownhosts_file = "/dev/null",
+ .test_home_ssh_config = TRUE,
+ .ssh_config_user = USER_ME,
+ .ssh_config_identity_file = SRCDIR "/src/ssh/mock_rsa_key",
+ .client_password = "bad password",
+ .knownhosts_home = MOCK_RSA_KEY,
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo",
+ .problem = "authentication-failed",
+};
+
+static const TestFixture fixture_ssh_config_key_password_fallback = {
+ .knownhosts_file = "/dev/null",
+ .test_home_ssh_config = TRUE,
+ .ssh_config_user = USER_ME,
+ .ssh_config_identity_file = SRCDIR "/src/ssh/mock_rsa_key",
+ .knownhosts_home = MOCK_RSA_KEY,
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo",
+ .problem = "authentication-failed",
+};
+
+static const TestFixture fixture_knownhost_challenge_preconnect = {
+ .knownhosts_file = "/dev/null",
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo"
+};
+
+static const TestFixture fixture_host_key_invalid = {
+ .knownhosts_file = SRCDIR "/src/ssh/invalid_known_hosts",
+};
+
+static const TestFixture fixture_prompt_host_key = {
+ .knownhosts_file = "/dev/null",
+ .allow_unknown = TRUE,
+ .ssh_command = BUILDDIR "/mock-echo"
+};
+
+static void
+test_invalid_knownhost (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fix = data;
+ JsonObject *init = NULL;
+
+ g_assert_cmpstr (fix->knownhosts_file, ==, SRCDIR "/src/ssh/invalid_known_hosts");
+ do_auth_response (tc->transport, "*", "");
+
+ init = wait_until_transport_init (tc->transport, "invalid-hostkey");
+
+ json_object_unref (init);
+}
+
+static void
+test_knownhost_data_prompt (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fix = data;
+ JsonObject *init = NULL;
+ gchar *knownhosts = g_strdup_printf ("x-host-key [%s]:%d %s",
+ fix->hostname ?: "127.0.0.1",
+ (int)tc->ssh_port,
+ MOCK_RSA_KEY);
+
+ g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
+
+ do_fixture_auth (tc->transport, data);
+ do_auth_response (tc->transport, "x-host-key", knownhosts);
+
+ init = wait_until_transport_init (tc->transport, NULL);
+ do_echo_and_close (tc);
+
+ json_object_unref (init);
+ g_free (knownhosts);
+}
+
+static void
+test_hostkey_unknown (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fix = data;
+ JsonObject *init = NULL;
+
+ g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
+
+ do_auth_response (tc->transport, "*", "");
+ do_auth_response (tc->transport, "x-host-key", INVALID_KEY);
+ do_hostkey_conversation (tc, "", FALSE);
+
+ init = wait_until_transport_init (tc->transport, "unknown-hostkey");
+ check_host_key_values (tc, init, fix->hostname);
+ json_object_unref (init);
+}
+
+static void
+test_hostkey_conversation (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fix = data;
+ JsonObject *init = NULL;
+
+ g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
+
+ do_fixture_auth (tc->transport, data);
+ do_auth_response (tc->transport, "x-host-key", INVALID_KEY);
+ do_hostkey_conversation (tc, MOCK_RSA_FP, TRUE);
+ init = wait_until_transport_init (tc->transport, NULL);
+ do_echo_and_close (tc);
+
+ json_object_unref (init);
+}
+
+static void
+test_hostkey_conversation_bad (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fix = data;
+ JsonObject *init = NULL;
+
+ g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
+
+ do_auth_response (tc->transport, "*", "");
+ do_auth_response (tc->transport, "x-host-key", INVALID_KEY);
+ do_hostkey_conversation (tc, "other-value", TRUE);
+ init = wait_until_transport_init (tc->transport, "unknown-hostkey");
+ check_host_key_values (tc, init, fix->hostname);
+ json_object_unref (init);
+}
+
+static void
+test_hostkey_conversation_invalid (TestCase *tc,
+ gconstpointer data)
+{
+ const TestFixture *fix = data;
+ JsonObject *init = NULL;
+
+ g_assert_cmpstr (fix->knownhosts_file, ==, "/dev/null");
+
+ do_auth_response (tc->transport, "*", "");
+ do_auth_response (tc->transport, "x-host-key", INVALID_KEY);
+ do_hostkey_conversation (tc, "other-value", FALSE);
+ init = wait_until_transport_init (tc->transport, "unknown-hostkey");
+ check_host_key_values (tc, init, fix->hostname);
+ json_object_unref (init);
+}
+
+/* The output from this will go to stderr */
+static const TestFixture fixture_bad_command = {
+ .ssh_command = "/nonexistent",
+ .problem = "no-cockpit"
+};
+
+/* Yes this makes a difference with bash, output goes to stdout */
+static const TestFixture fixture_command_not_found = {
+ .ssh_command = "nonexistant-command",
+ .problem = "no-cockpit"
+};
+
+/* A valid command that exits with 0 */
+static const TestFixture fixture_command_exits = {
+ .ssh_command = "/usr/bin/true",
+ .problem = "no-cockpit"
+};
+
+/* A valid command that exits with 1 */
+static const TestFixture fixture_command_fails = {
+ .ssh_command = "/usr/bin/false",
+ .problem = "no-cockpit"
+};
+
+/* An ssh command that just kills itself with SIGTERM */
+static const TestFixture fixture_terminate_problem = {
+ .ssh_command = "kill $$",
+ .problem = "terminated"
+};
+
+
+static const TestFixture fixture_unsupported_auth = {
+ .mock_sshd_arg = "--broken-auth",
+};
+
+static void
+test_unsupported_auth (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ JsonObject *auth_results = NULL;
+
+ do_fixture_auth (tc->transport, data);
+ init = wait_until_transport_init (tc->transport, "authentication-failed");
+ auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{\"password\":\"no-server-support\",\"public-key\":\"no-server-support\",\"gssapi-mic\":\"no-server-support\"}");
+
+ json_object_unref (init);
+}
+
+
+static const TestFixture fixture_auth_failed = {
+ .client_password = "bad password",
+};
+
+static void
+test_auth_failed (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ JsonObject *auth_results = NULL;
+
+ do_fixture_auth (tc->transport, data);
+ init = wait_until_transport_init (tc->transport, "authentication-failed");
+ auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{\"password\":\"denied\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
+
+ json_object_unref (init);
+}
+
+static void
+test_cannot_connect (void)
+{
+ const gchar *argv[] = {
+ BUILDDIR "/cockpit-ssh",
+ "localhost:65533",
+ NULL,
+ };
+
+ JsonObject *init = NULL;
+ gchar **env = setup_env (NULL);
+ CockpitTransport *transport = start_bridge (env, (gchar **) argv);
+ do_basic_auth (transport, "*", "user", "unused");
+ init = wait_until_transport_init (transport, "no-host");
+
+ g_object_unref (transport);
+ json_object_unref (init);
+ g_strfreev (env);
+}
+
+static void
+test_key_good (TestCase *tc,
+ gconstpointer data)
+{
+ g_autofree gchar *privkey = NULL;
+ g_assert (g_file_get_contents (SRCDIR "/src/ssh/test_rsa", &privkey, NULL, NULL));
+
+ g_autofree gchar *msg = g_strdup_printf ("private-key %s", privkey);
+
+ do_auth_response (tc->transport, "*", msg);
+ g_autoptr(JsonObject) init = wait_until_transport_init (tc->transport, NULL);
+ do_echo_and_close (tc);
+}
+
+static void
+test_key_fail (TestCase *tc,
+ gconstpointer data)
+{
+ g_autofree gchar *privkey = NULL;
+ g_assert (g_file_get_contents (SRCDIR "/src/ssh/mock_ecdsa_key", &privkey, NULL, NULL));
+
+ g_autofree gchar *msg = g_strdup_printf ("private-key %s", privkey);
+
+ do_auth_response (tc->transport, "*", msg);
+ g_autoptr(JsonObject) init = wait_until_transport_init (tc->transport, "authentication-failed");
+ JsonObject *auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{\"password\":\"not-provided\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
+}
+
+static void
+test_key_invalid (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ JsonObject *auth_results = NULL;
+
+ do_auth_response (tc->transport, "*", "private-key invalid");
+ init = wait_until_transport_init (tc->transport, "internal-error");
+ auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{\"password\":\"not-provided\",\"public-key\":\"error\",\"gssapi-mic\":\"no-server-support\"}");
+
+ json_object_unref (init);
+}
+
+static void
+test_password_good (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ gchar *msg = g_strdup_printf ("password %s", PASSWORD);
+
+ do_auth_response (tc->transport, "*", msg);
+ init = wait_until_transport_init (tc->transport, NULL);
+ do_echo_and_close (tc);
+
+ json_object_unref (init);
+ g_free (msg);
+}
+
+static void
+test_password_fail (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ JsonObject *auth_results = NULL;
+
+ do_auth_response (tc->transport, "*", "password bad");
+ init = wait_until_transport_init (tc->transport, "authentication-failed");
+ auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{\"password\":\"denied\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
+
+ json_object_unref (init);
+}
+
+static void
+test_basic_no_user (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ JsonObject *auth_results = NULL;
+
+ do_basic_auth (tc->transport, "*", "", PASSWORD);
+ init = wait_until_transport_init (tc->transport, "authentication-failed");
+ auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{}");
+
+ json_object_unref (init);
+}
+
+static void
+test_basic_user_mismatch (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ JsonObject *auth_results = NULL;
+
+ /* Auth fails because user doesn't match */
+ do_basic_auth (tc->transport, "*", "other", PASSWORD);
+ init = wait_until_transport_init (tc->transport, "authentication-failed");
+ auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{\"password\":\"denied\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
+
+ json_object_unref (init);
+}
+
+static void
+test_basic_secondary_no_user (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+
+ do_auth_response (tc->transport, "*", "");
+ /* Auth succeeds because user is already set */
+ do_basic_auth (tc->transport, "basic", "", PASSWORD);
+ init = wait_until_transport_init (tc->transport, NULL);
+ do_echo_and_close (tc);
+
+ json_object_unref (init);
+}
+
+
+static void
+test_basic_secondary_user_mismatch (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+
+ do_auth_response (tc->transport, "*", "");
+ /* Auth succeeds because secondary user is ignored */
+ do_basic_auth (tc->transport, "basic", "bad-user", PASSWORD);
+ init = wait_until_transport_init (tc->transport, NULL);
+ do_echo_and_close (tc);
+
+ json_object_unref (init);
+}
+
+static const TestFixture fixture_multi_auth = {
+ .mock_sshd_arg = "--multi-step",
+};
+
+static void
+test_multi_auth (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+
+ do_fixture_auth (tc->transport, data);
+ do_auth_conversation (tc->transport, "Token",
+ "{\"message\":\"Password and Token\",\"echo\":true}",
+ "5", TRUE);
+ init = wait_until_transport_init (tc->transport, NULL);
+ do_echo_and_close (tc);
+
+ json_object_unref (init);
+}
+
+static void
+test_multi_auth_fail (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ JsonObject *auth_results = NULL;
+
+ do_fixture_auth (tc->transport, data);
+ do_auth_conversation (tc->transport, "Token",
+ "{\"message\":\"Password and Token\",\"echo\":true}",
+ "4", TRUE);
+ init = wait_until_transport_init (tc->transport, "authentication-failed");
+ auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{\"password\":\"denied\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
+
+ json_object_unref (init);
+}
+
+static void
+test_multi_auth_empty (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ JsonObject *auth_results = NULL;
+
+ do_fixture_auth (tc->transport, data);
+ do_auth_conversation (tc->transport, "Token",
+ "{\"message\":\"Password and Token\",\"echo\":true}",
+ "", FALSE);
+ init = wait_until_transport_init (tc->transport, "internal-error");
+ auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{\"password\":\"error\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
+
+ json_object_unref (init);
+}
+
+static void
+test_multi_auth_bad (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ JsonObject *auth_results = NULL;
+
+ do_fixture_auth (tc->transport, data);
+ do_auth_conversation (tc->transport, "Token",
+ "{\"message\":\"Password and Token\",\"echo\":true}",
+ "invalid", FALSE);
+ init = wait_until_transport_init (tc->transport, "internal-error");
+ auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{\"password\":\"error\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
+
+ json_object_unref (init);
+}
+
+static void
+test_multi_auth_3 (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+
+ do_fixture_auth (tc->transport, data);
+ do_auth_conversation (tc->transport, "Token",
+ "{\"message\":\"Password and Token\",\"echo\":true}",
+ "6", TRUE);
+ do_auth_conversation (tc->transport, "So Close",
+ "{\"message\":\"Again\",\"echo\":false}",
+ "5", TRUE);
+ init = wait_until_transport_init (tc->transport, NULL);
+ do_echo_and_close (tc);
+
+ json_object_unref (init);
+}
+
+static void
+test_multi_auth_3_fail (TestCase *tc,
+ gconstpointer data)
+{
+ JsonObject *init = NULL;
+ JsonObject *auth_results = NULL;
+
+ do_fixture_auth (tc->transport, data);
+ do_auth_conversation (tc->transport, "Token",
+ "{\"message\":\"Password and Token\",\"echo\":true}",
+ "6", TRUE);
+ do_auth_conversation (tc->transport, "So Close",
+ "{\"message\":\"Again\",\"echo\":false}",
+ "4", TRUE);
+ init = wait_until_transport_init (tc->transport, "authentication-failed");
+ auth_results = json_object_get_object_member (init, "auth-method-results");
+ cockpit_assert_json_eq (auth_results, "{\"password\":\"denied\",\"public-key\":\"denied\",\"gssapi-mic\":\"no-server-support\"}");
+
+ json_object_unref (init);
+}
+
+
+int
+main (int argc,
+ char *argv[])
+{
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/ssh-bridge/echo-message", TestCase, &fixture_mock_echo,
+ setup, test_echo_and_close, teardown);
+ g_test_add ("/ssh-bridge/echo-queue", TestCase, &fixture_mock_echo,
+ setup, test_echo_queue, teardown);
+ g_test_add ("/ssh-bridge/echo-large", TestCase, &fixture_cat,
+ setup, test_echo_large, teardown);
+
+ if (have_ipv6 ())
+ g_test_add ("/ssh-bridge/ipv6-address", TestCase, &fixture_ipv6_address,
+ setup, test_echo_and_close, teardown);
+ else
+ g_message ("No IPv6 support, skipping IPv6 tests");
+
+ g_test_add ("/ssh-bridge/bad-command", TestCase, &fixture_bad_command,
+ setup, test_problem, teardown);
+ g_test_add ("/ssh-bridge/command-not-found", TestCase, &fixture_command_not_found,
+ setup, test_problem, teardown);
+ g_test_add ("/ssh-bridge/command-not-cockpit", TestCase, &fixture_command_exits,
+ setup, test_problem, teardown);
+ g_test_add ("/ssh-bridge/command-just-fails", TestCase, &fixture_command_fails,
+ setup, test_problem, teardown);
+ g_test_add_func ("/ssh-bridge/cannot-connect", test_cannot_connect);
+ g_test_add ("/ssh-bridge/ssh-config-home", TestCase, &fixture_home_ssh_config,
+ setup, test_echo_and_close, teardown);
+ g_test_add ("/ssh-bridge/ssh-config-valid-user", TestCase, &fixture_ssh_config_valid_user,
+ setup, test_echo_and_close, teardown);
+ g_test_add ("/ssh-bridge/ssh-config-invalid-user", TestCase, &fixture_ssh_config_invalid_user,
+ setup, test_problem, teardown);
+ g_test_add ("/ssh-bridge/ssh-config-host-user-priority", TestCase, &fixture_ssh_config_invalid_user_host_priority,
+ setup, test_echo_and_close, teardown);
+ g_test_add ("/ssh-bridge/ssh-config-host-port-priority", TestCase, &fixture_ssh_config_invalid_port_host_priority,
+ setup, test_echo_and_close, teardown);
+ g_test_add ("/ssh-bridge/ssh-config-home-good-key", TestCase, &fixture_ssh_config_good_key,
+ setup, test_echo_and_close, teardown);
+ g_test_add ("/ssh-bridge/ssh-config-home-good-key-password-protected", TestCase, &fixture_ssh_config_good_key_password_protected,
+ setup, test_problem, teardown);
+ g_test_add ("/ssh-bridge/ssh-config-home-bad-key", TestCase, &fixture_ssh_config_bad_key,
+ setup, test_problem, teardown);
+ g_test_add ("/ssh-bridge/ssh-config-home-bad-key-good-key-fallback", TestCase, &fixture_ssh_config_bad_key,
+ setup, test_key_good, teardown);
+ g_test_add ("/ssh-bridge/ssh-config-home-key-password-fallback", TestCase, &fixture_ssh_config_key_password_fallback,
+ setup, test_echo_and_close, teardown);
+
+ g_test_add ("/ssh-bridge/terminate-problem", TestCase, &fixture_terminate_problem,
+ setup, test_problem, teardown);
+ g_test_add ("/ssh-bridge/unsupported-auth", TestCase, &fixture_unsupported_auth,
+ setup, test_unsupported_auth, teardown);
+ g_test_add ("/ssh-bridge/auth-failed", TestCase,
+ &fixture_auth_failed, setup,
+ test_auth_failed, teardown);
+ g_test_add ("/ssh-bridge/key-good", TestCase, &fixture_mock_echo,
+ setup, test_key_good, teardown);
+ g_test_add ("/ssh-bridge/key-invalid", TestCase, &fixture_mock_echo,
+ setup, test_key_invalid, teardown);
+ g_test_add ("/ssh-bridge/key-fail", TestCase, &fixture_mock_echo,
+ setup, test_key_fail, teardown);
+ g_test_add ("/ssh-bridge/password-fail", TestCase, &fixture_mock_echo,
+ setup, test_password_fail, teardown);
+ g_test_add ("/ssh-bridge/password-good", TestCase, &fixture_mock_echo,
+ setup, test_password_good, teardown);
+ g_test_add ("/ssh-bridge/basic-no-user", TestCase, &fixture_mock_echo,
+ setup, test_basic_no_user, teardown);
+ g_test_add ("/ssh-bridge/basic-secondary-no-user", TestCase, &fixture_mock_echo,
+ setup, test_basic_secondary_no_user, teardown);
+ g_test_add ("/ssh-bridge/basic-user-mismatch", TestCase, &fixture_mock_echo,
+ setup, test_basic_user_mismatch, teardown);
+ g_test_add ("/ssh-bridge/basic-secondary-user-mismatch", TestCase, &fixture_mock_echo,
+ setup, test_basic_secondary_user_mismatch, teardown);
+ g_test_add ("/ssh-bridge/kb-multi-bad", TestCase,
+ &fixture_multi_auth,
+ setup, test_multi_auth_bad, teardown);
+ g_test_add ("/ssh-bridge/kb-multi-empty", TestCase,
+ &fixture_multi_auth,
+ setup, test_multi_auth_empty, teardown);
+ g_test_add ("/ssh-bridge/kb-multi-fail", TestCase,
+ &fixture_multi_auth,
+ setup, test_multi_auth_fail, teardown);
+ g_test_add ("/ssh-bridge/kb-multi-echo-message", TestCase,
+ &fixture_multi_auth,
+ setup, test_multi_auth, teardown);
+ g_test_add ("/ssh-bridge/kb-multi-3-fail", TestCase,
+ &fixture_multi_auth,
+ setup, test_multi_auth_3_fail, teardown);
+ g_test_add ("/ssh-bridge/kb-multi-3-echo-message", TestCase,
+ &fixture_multi_auth,
+ setup, test_multi_auth_3, teardown);
+
+ g_test_add ("/ssh-bridge/unknown-host", TestCase, &fixture_unknown_host,
+ setup, test_problem, teardown);
+ g_test_add ("/ssh-bridge/unknown-localhost", TestCase, &fixture_unknown_localhost,
+ setup, test_hostkey_unknown, teardown);
+ g_test_add ("/ssh-bridge/knownhost-challenge-preconnect", TestCase,
+ &fixture_knownhost_challenge_preconnect,
+ setup, test_knownhost_data_prompt, teardown);
+ g_test_add ("/ssh-bridge/knownhost-invalid", TestCase, &fixture_host_key_invalid,
+ setup, test_invalid_knownhost, teardown);
+ g_test_add ("/ssh-bridge/knownhost-home", TestCase, &fixture_known_host_home,
+ setup, test_echo_and_close, teardown);
+
+ g_test_add ("/ssh-bridge/hostkey-unknown", TestCase, &fixture_prompt_host_key,
+ setup, test_hostkey_unknown, teardown);
+ g_test_add ("/ssh-bridge/hostkey-conversation", TestCase, &fixture_prompt_host_key,
+ setup, test_hostkey_conversation, teardown);
+ g_test_add ("/ssh-bridge/hostkey-conversation-bad", TestCase, &fixture_prompt_host_key,
+ setup, test_hostkey_conversation_bad, teardown);
+ g_test_add ("/ssh-bridge/hostkey-conversation-invalid", TestCase, &fixture_prompt_host_key,
+ setup, test_hostkey_conversation_invalid, teardown);
+
+ return g_test_run ();
+}
diff --git a/src/ssh/test-sshoptions.c b/src/ssh/test-sshoptions.c
new file mode 100644
index 0000000..9842bc3
--- /dev/null
+++ b/src/ssh/test-sshoptions.c
@@ -0,0 +1,150 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitconf.h"
+#include "testlib/cockpittest.h"
+
+#include "cockpitsshoptions.h"
+
+/* Mock override these from other files */
+extern const gchar *cockpit_config_file;
+
+static void
+test_ssh_options (void)
+{
+ gchar **env = NULL;
+ CockpitSshOptions *options = NULL;
+
+ options = cockpit_ssh_options_from_env (env);
+ g_assert_cmpstr (options->remote_peer, ==, "localhost");
+ g_assert_cmpstr (options->knownhosts_file, ==, NULL);
+ g_assert_cmpstr (options->command, ==, "cockpit-bridge");
+
+ options->knownhosts_file = "other-known";
+ options->command = "other-command";
+ options->remote_peer = "other";
+
+ env = cockpit_ssh_options_to_env (options, NULL);
+
+ g_assert_cmpstr (g_environ_getenv (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS"), ==, "");
+ g_assert_cmpstr (g_environ_getenv (env, "COCKPIT_SSH_KNOWN_HOSTS_FILE"), ==, "other-known");
+ g_assert_cmpstr (g_environ_getenv (env, "COCKPIT_SSH_BRIDGE_COMMAND"), ==, "other-command");
+ g_assert_cmpstr (g_environ_getenv (env, "COCKPIT_REMOTE_PEER"), ==, "other");
+
+ options->connect_to_unknown_hosts = TRUE;
+
+ g_strfreev (env);
+ env = cockpit_ssh_options_to_env (options, NULL);
+ g_assert_cmpstr (g_environ_getenv (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS"), ==, "1");
+
+ g_free (options);
+ g_strfreev (env);
+
+ /* Start with a clean env */
+ env = g_environ_setenv (NULL, "COCKPIT_SSH_KNOWN_HOSTS_FILE", "other-known", TRUE);
+ env = g_environ_setenv (env, "COCKPIT_SSH_BRIDGE_COMMAND", "other-command", TRUE);
+ env = g_environ_setenv (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS", "", TRUE);
+
+ options = cockpit_ssh_options_from_env (env);
+ g_assert_false (options->connect_to_unknown_hosts);
+ g_assert_cmpstr (options->knownhosts_file, ==, "other-known");
+ g_assert_cmpstr (options->command, ==, "other-command");
+
+ g_free (options);
+
+ env = g_environ_setenv (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS", "bogus", TRUE);
+ options = cockpit_ssh_options_from_env (env);
+ g_assert_false (options->connect_to_unknown_hosts);
+ g_free (options);
+
+ env = g_environ_setenv (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS", "yes", TRUE);
+ options = cockpit_ssh_options_from_env (env);
+ g_assert_true (options->connect_to_unknown_hosts);
+ g_free (options);
+
+ env = g_environ_setenv (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS", "", TRUE);
+ options = cockpit_ssh_options_from_env (env);
+ g_assert_false (options->connect_to_unknown_hosts);
+ g_free (options);
+ g_strfreev (env);
+}
+
+static void
+test_ssh_options_deprecated (void)
+{
+ gchar **env = NULL;
+ CockpitSshOptions *options = NULL;
+
+ env = g_environ_setenv (NULL, "COCKPIT_SSH_ALLOW_UNKNOWN", "yes", TRUE);
+ options = cockpit_ssh_options_from_env (env);
+ g_assert_true (options->connect_to_unknown_hosts);
+ g_free (options);
+
+ env = g_environ_setenv (env, "COCKPIT_SSH_KNOWN_HOSTS_DATA", "authorize", TRUE);
+ options = cockpit_ssh_options_from_env (env);
+ g_free (options);
+
+ env = g_environ_setenv (env, "COCKPIT_SSH_KNOWN_HOSTS_DATA", "", TRUE);
+ options = cockpit_ssh_options_from_env (env);
+ g_free (options);
+
+ g_strfreev (env);
+}
+
+static void
+test_ssh_options_alt_conf (void)
+{
+ CockpitSshOptions *options = NULL;
+
+ cockpit_config_file = SRCDIR "/src/ws/mock-config/cockpit/cockpit-alt.conf";
+ cockpit_conf_cleanup ();
+
+ options = cockpit_ssh_options_from_env (NULL);
+ g_assert_true (options->connect_to_unknown_hosts);
+ g_free (options);
+}
+
+static void
+test_ssh_options_conf_deprecated (void)
+{
+ CockpitSshOptions *options = NULL;
+
+ cockpit_config_file = SRCDIR "/src/ws/mock-config/cockpit/cockpit-deprecated.conf";
+ cockpit_conf_cleanup ();
+
+ options = cockpit_ssh_options_from_env (NULL);
+ g_assert_true (options->connect_to_unknown_hosts);
+ g_free (options);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/ssh-options/basic", test_ssh_options);
+ g_test_add_func ("/ssh-options/deprecated", test_ssh_options_deprecated);
+ g_test_add_func ("/ssh-options/alt-conf", test_ssh_options_alt_conf);
+ g_test_add_func ("/ssh-options/deprecated-conf", test_ssh_options_conf_deprecated);
+
+ return g_test_run ();
+}
diff --git a/src/ssh/test_rsa b/src/ssh/test_rsa
new file mode 100644
index 0000000..b1ab61d
--- /dev/null
+++ b/src/ssh/test_rsa
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAvkPEj9GX9I0v/3dxCUB73TgOYjxkXB/m2ecKnUYmYtEwgirA
+onCgZRMAvB7UaP5e6U/pNCXuZ+UgS0yU6tqEXD7MQ4YZiiNU1RaLe/gQ21NEx27h
+hCGTZOLKcSfOFv2Z77OUcXSop2PZxQweYaH1+RB7hojOd7ZchN/tIBxvea5JSg/0
+wLC8Lm65gpCZCxG2TNgfymovnyrYB44HnwEm4jCMU4uP68h0+D297US4oWwcpcqE
+2S4LOxazjw1Brvntpqwtq624tUb1QVYMxdHpCR7Qu843r3XSpS4BwrnOks7Sbgyg
+tHiKgogY5Xhu7ZqsTODtzyJ950YD0scnY41qHQIDAQABAoIBAFlQHnkUfixCCoH1
+Y45gQsS5h6b9im7kWs128ziYsXQ5lnfD8eFO1TwdC39DSZpvrcX/yQy9sYf7uoke
+Tdlg8jkLEX+w91Qs+al9h8SN0fvivqqPljUcPcBh5X3wnYGVUil/NvN7O6A38wXY
+hnp2OKzN2+5vUdxIMm39X6ZvMrT/FyQjvdp393G4f0blYl7Npdc+HYPNnhHdgi4I
+NUa32pG3ypoWkQRAYApaG2RXPTWQXTM2w4CFK5uJx/pB3r5NidU/H0XAl4TAuw9M
+V9hrIPAOh5zKvHcPv8xOwR0Bt36F+/QATjO9pvlzQO6Rn3x2dyAVdaFMgdYTNpQQ
+t0ZYsYECgYEA8yAhKUnArEQ4A+AI+pCtZuftzkXmnQ5SHNUtF2GeR5tRZ1PBF/tp
+zoVRW+5ge1hI2VEx3ziGHEIBr7FfVej7twQ3URv5ILYj6CoNOf+HxkZgkTDGpYdj
+AVvyjeD5qJEwCSeJ2bxD5LmxS9is8b8rXjVKRuPxwLeWqEjemPb0KNUCgYEAyFcL
+TdN9cZghuzLZ0vfP4k9Hratunskz5njTFKnJx90riE7VqPH9OHvTeHn1xJ5WACnb
+mFpAUG1v7BmC+WLEIPnKRKvuzL5C1yr+mntwTZsrwsLDdT/nfTS9hWzk9U6ykhJA
+De8nNfxHuCoqM++CNvh+rA4W2Zc6WmE0uCwXYCkCgYEA70KMP+Sb3yvXcEDWtTcR
+3raZ+agitib0ufk0QdFIgbGhH6111lMOIjZjBbSGcHxGXM8h5Ens+PwgSrWkW5hH
+tylIAuMjfYShu4U+tPf6ty5lNB0rMJUW4qyI/AUNzEztV+T4LTWwHvR7PWgDcniu
+hiytZyxFqmFBu2TS4vgM+e0CgYAvAL0WNVhpHlhLo1KXvKx5XEBk7qO1fV8/43ki
+j/NXgPyFrnlSefP/HI4w5exThRKIV0m+JO6R8BsiOZoRCKsbUX+zPON6BemIsf2q
+IOvoSU+rEibpi2S0a3tLopDVPPGIc9+zZTi94cKx4rKkHL1gSEzv8R5LTr/SFJxZ
+2X5igQKBgBTkIeB0yI2PGf1drI+YbhDfgIphEeSCPbWxcUzPCcwLqXGo41cr8RXY
+TgWtKk0gXhJWkMSIIXrfucCvXHTkk8wlqqgAVwrTgq4Q16LfBuucLwSe4TLp4SJZ
+Lko5CzOq+EIv6DIlZ3tRHeDFatWe+41w27KhrV9yxB6Ay0MalP4i
+-----END RSA PRIVATE KEY-----
diff --git a/src/ssh/test_rsa.pub b/src/ssh/test_rsa.pub
new file mode 100644
index 0000000..6707911
--- /dev/null
+++ b/src/ssh/test_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+Q8SP0Zf0jS//d3EJQHvdOA5iPGRcH+bZ5wqdRiZi0TCCKsCicKBlEwC8HtRo/l7pT+k0Je5n5SBLTJTq2oRcPsxDhhmKI1TVFot7+BDbU0THbuGEIZNk4spxJ84W/Znvs5RxdKinY9nFDB5hofX5EHuGiM53tlyE3+0gHG95rklKD/TAsLwubrmCkJkLEbZM2B/Kai+fKtgHjgefASbiMIxTi4/ryHT4Pb3tRLihbBylyoTZLgs7FrOPDUGu+e2mrC2rrbi1RvVBVgzF0ekJHtC7zjevddKlLgHCuc6SztJuDKC0eIqCiBjleG7tmqxM4O3PIn3nRgPSxydjjWod
diff --git a/src/systemd/Makefile.am b/src/systemd/Makefile.am
new file mode 100644
index 0000000..511c6e5
--- /dev/null
+++ b/src/systemd/Makefile.am
@@ -0,0 +1,57 @@
+# -----------------------------------------------------------------------------
+# all systemd units, tmpfiles, and related helpers
+
+nodist_systemdunit_DATA = \
+ src/systemd/cockpit-motd.service \
+ src/systemd/cockpit.service \
+ src/systemd/cockpit.socket \
+ src/systemd/cockpit-session@.service \
+ src/systemd/cockpit-session.socket \
+ src/systemd/cockpit-wsinstance-http.service \
+ src/systemd/cockpit-wsinstance-http.socket \
+ src/systemd/cockpit-wsinstance-https-factory@.service \
+ src/systemd/cockpit-wsinstance-https-factory.socket \
+ src/systemd/cockpit-wsinstance-https@.service \
+ src/systemd/cockpit-wsinstance-https@.socket \
+ $(NULL)
+
+dist_systemdunit_DATA = \
+ src/systemd/system-cockpithttps.slice \
+ $(NULL)
+
+motddir = $(datadir)/$(PACKAGE)/motd/
+dist_motd_DATA = src/systemd/inactive.motd
+dist_motd_SCRIPTS = src/systemd/update-motd
+
+# Automake: 'Variables using ... ‘sysconf’ ... are installed by install-exec.'
+install-exec-hook::
+ mkdir -p $(DESTDIR)$(sysconfdir)/motd.d
+ ln -sTfr $(DESTDIR)/run/cockpit/motd $(DESTDIR)$(sysconfdir)/motd.d/cockpit
+ mkdir -p $(DESTDIR)$(sysconfdir)/issue.d
+ ln -sTfr $(DESTDIR)/run/cockpit/motd $(DESTDIR)$(sysconfdir)/issue.d/cockpit.issue
+
+tempconfdir = $(prefix)/lib/tmpfiles.d
+nodist_tempconf_DATA = src/systemd/cockpit-tempfiles.conf
+
+# we can't generate these with config.status because,
+# eg. it does "@libexecdir@" -> "${exec_prefix}/libexec"
+src/systemd/%: src/systemd/%.in
+ $(AM_V_GEN) mkdir -p $(dir $@) && sed \
+ -e 's,[@]PACKAGE[@],$(PACKAGE),g' \
+ -e 's,[@]admin_group[@],$(admin_group),g' \
+ -e 's,[@]datadir[@],$(datadir),g' \
+ -e 's,[@]group[@],$(COCKPIT_GROUP),g' \
+ -e 's,[@]libexecdir[@],$(libexecdir),g' \
+ -e 's,[@]user[@],$(COCKPIT_USER),g' \
+ -e 's,[@]wsinstancegroup[@],$(COCKPIT_WSINSTANCE_GROUP),g' \
+ -e 's,[@]wsinstanceuser[@],$(COCKPIT_WSINSTANCE_USER),g' \
+ $< > $@.tmp && mv -f $@.tmp $@
+
+systemdgenerated = \
+ $(nodist_systemdunit_DATA) \
+ $(nodist_tempconf_DATA) \
+ $(NULL)
+systemdgenerated_in = $(patsubst %,%.in,$(systemdgenerated))
+
+EXTRA_DIST += $(systemdgenerated_in)
+CLEANFILES += $(systemdgenerated)
diff --git a/src/systemd/cockpit-motd.service.in b/src/systemd/cockpit-motd.service.in
new file mode 100644
index 0000000..9161f58
--- /dev/null
+++ b/src/systemd/cockpit-motd.service.in
@@ -0,0 +1,9 @@
+[Unit]
+Description=Cockpit motd updater service
+Documentation=man:cockpit-ws(8)
+Wants=network-online.target
+After=network-online.target cockpit.socket
+
+[Service]
+Type=oneshot
+ExecStart=-@datadir@/@PACKAGE@/motd/update-motd
diff --git a/src/systemd/cockpit-session.socket.in b/src/systemd/cockpit-session.socket.in
new file mode 100644
index 0000000..a6ecfd4
--- /dev/null
+++ b/src/systemd/cockpit-session.socket.in
@@ -0,0 +1,9 @@
+[Unit]
+Description=Initiator socket for Cockpit sessions
+
+[Socket]
+ListenStream=/run/cockpit/session
+SocketUser=root
+SocketGroup=@wsinstancegroup@
+SocketMode=0660
+Accept=yes
diff --git a/src/systemd/cockpit-session@.service.in b/src/systemd/cockpit-session@.service.in
new file mode 100644
index 0000000..23660c6
--- /dev/null
+++ b/src/systemd/cockpit-session@.service.in
@@ -0,0 +1,9 @@
+[Unit]
+Description=Cockpit session %I
+
+[Service]
+ExecStart=@libexecdir@/cockpit-session
+StandardInput=socket
+StandardOutput=inherit
+StandardError=journal
+User=root
diff --git a/src/systemd/cockpit-tempfiles.conf.in b/src/systemd/cockpit-tempfiles.conf.in
new file mode 100644
index 0000000..0dcddc2
--- /dev/null
+++ b/src/systemd/cockpit-tempfiles.conf.in
@@ -0,0 +1,3 @@
+C /run/cockpit/inactive.motd 0640 root @admin_group@ - @datadir@/@PACKAGE@/motd/inactive.motd
+f /run/cockpit/active.motd 0640 root @admin_group@ -
+L+ /run/cockpit/motd - - - - inactive.motd
diff --git a/src/systemd/cockpit-wsinstance-http.service.in b/src/systemd/cockpit-wsinstance-http.service.in
new file mode 100644
index 0000000..bb64302
--- /dev/null
+++ b/src/systemd/cockpit-wsinstance-http.service.in
@@ -0,0 +1,9 @@
+[Unit]
+Description=Cockpit Web Service http instance
+BindsTo=cockpit.service
+Documentation=man:cockpit-ws(8)
+
+[Service]
+ExecStart=@libexecdir@/cockpit-ws --no-tls --port=0
+User=@wsinstanceuser@
+Group=@wsinstancegroup@
diff --git a/src/systemd/cockpit-wsinstance-http.socket.in b/src/systemd/cockpit-wsinstance-http.socket.in
new file mode 100644
index 0000000..24ce9cc
--- /dev/null
+++ b/src/systemd/cockpit-wsinstance-http.socket.in
@@ -0,0 +1,9 @@
+[Unit]
+Description=Socket for Cockpit Web Service http instance
+BindsTo=cockpit.service
+Documentation=man:cockpit-ws(8)
+
+[Socket]
+ListenStream=/run/cockpit/wsinstance/http.sock
+SocketUser=@user@
+SocketMode=0600
diff --git a/src/systemd/cockpit-wsinstance-https-factory.socket.in b/src/systemd/cockpit-wsinstance-https-factory.socket.in
new file mode 100644
index 0000000..b64d3e2
--- /dev/null
+++ b/src/systemd/cockpit-wsinstance-https-factory.socket.in
@@ -0,0 +1,10 @@
+[Unit]
+Description=Socket for Cockpit Web Service https instance factory
+BindsTo=cockpit.service
+Documentation=man:cockpit-ws(8)
+
+[Socket]
+ListenStream=/run/cockpit/wsinstance/https-factory.sock
+Accept=yes
+SocketUser=@user@
+SocketMode=0600
diff --git a/src/systemd/cockpit-wsinstance-https-factory@.service.in b/src/systemd/cockpit-wsinstance-https-factory@.service.in
new file mode 100644
index 0000000..ede2758
--- /dev/null
+++ b/src/systemd/cockpit-wsinstance-https-factory@.service.in
@@ -0,0 +1,7 @@
+[Unit]
+Description=Cockpit Web Service https instance factory
+Documentation=man:cockpit-ws(8)
+
+[Service]
+ExecStart=@libexecdir@/cockpit-wsinstance-factory
+User=root
diff --git a/src/systemd/cockpit-wsinstance-https@.service.in b/src/systemd/cockpit-wsinstance-https@.service.in
new file mode 100644
index 0000000..7837467
--- /dev/null
+++ b/src/systemd/cockpit-wsinstance-https@.service.in
@@ -0,0 +1,10 @@
+[Unit]
+Description=Cockpit Web Service https instance %I
+BindsTo=cockpit.service
+Documentation=man:cockpit-ws(8)
+
+[Service]
+Slice=system-cockpithttps.slice
+ExecStart=@libexecdir@/cockpit-ws --for-tls-proxy --port=0
+User=@wsinstanceuser@
+Group=@wsinstancegroup@
diff --git a/src/systemd/cockpit-wsinstance-https@.socket.in b/src/systemd/cockpit-wsinstance-https@.socket.in
new file mode 100644
index 0000000..5e1922f
--- /dev/null
+++ b/src/systemd/cockpit-wsinstance-https@.socket.in
@@ -0,0 +1,13 @@
+[Unit]
+Description=Socket for Cockpit Web Service https instance %I
+BindsTo=cockpit.service
+# clean up the socket after the service exits, to prevent fd leak
+# this also effectively prevents a DoS by starting arbitrarily many sockets, as
+# the services are resource-limited by system-cockpithttps.slice
+BindsTo=cockpit-wsinstance-https@%i.service
+Documentation=man:cockpit-ws(8)
+
+[Socket]
+ListenStream=/run/cockpit/wsinstance/https@%i.sock
+SocketUser=@user@
+SocketMode=0600
diff --git a/src/systemd/cockpit.service.in b/src/systemd/cockpit.service.in
new file mode 100644
index 0000000..1fea0de
--- /dev/null
+++ b/src/systemd/cockpit.service.in
@@ -0,0 +1,23 @@
+[Unit]
+Description=Cockpit Web Service
+Documentation=man:cockpit-ws(8)
+Requires=cockpit.socket
+Requires=cockpit-wsinstance-http.socket cockpit-wsinstance-https-factory.socket
+After=cockpit-wsinstance-http.socket cockpit-wsinstance-https-factory.socket
+
+[Service]
+RuntimeDirectory=cockpit/tls
+# systemd ≥ 241 sets this automatically
+Environment=RUNTIME_DIRECTORY=/run/cockpit/tls
+ExecStartPre=+@libexecdir@/cockpit-certificate-ensure --for-cockpit-tls
+ExecStart=@libexecdir@/cockpit-tls
+User=@user@
+Group=@group@
+NoNewPrivileges=true
+ProtectSystem=strict
+ProtectHome=true
+PrivateTmp=true
+PrivateDevices=true
+ProtectKernelTunables=true
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+MemoryDenyWriteExecute=true
diff --git a/src/systemd/cockpit.socket.in b/src/systemd/cockpit.socket.in
new file mode 100644
index 0000000..97ca6b3
--- /dev/null
+++ b/src/systemd/cockpit.socket.in
@@ -0,0 +1,13 @@
+[Unit]
+Description=Cockpit Web Service Socket
+Documentation=man:cockpit-ws(8)
+Wants=cockpit-motd.service
+
+[Socket]
+ListenStream=9090
+ExecStartPost=-@datadir@/@PACKAGE@/motd/update-motd '' localhost
+ExecStartPost=-/bin/ln -snf active.motd /run/cockpit/motd
+ExecStopPost=-/bin/ln -snf inactive.motd /run/cockpit/motd
+
+[Install]
+WantedBy=sockets.target
diff --git a/src/systemd/inactive.motd b/src/systemd/inactive.motd
new file mode 100644
index 0000000..ddb7cb5
--- /dev/null
+++ b/src/systemd/inactive.motd
@@ -0,0 +1,2 @@
+Activate the web console with: systemctl enable --now cockpit.socket
+
diff --git a/src/systemd/system-cockpithttps.slice b/src/systemd/system-cockpithttps.slice
new file mode 100644
index 0000000..ea24002
--- /dev/null
+++ b/src/systemd/system-cockpithttps.slice
@@ -0,0 +1,9 @@
+[Unit]
+Description=Resource limits for all cockpit-ws-https@.service instances
+
+[Slice]
+# each instance contains cockpit-ws (with a few threads) and a short-lived
+# cockpit-session; the actual user sessions live in user.slice
+TasksMax=200
+MemoryHigh=75%
+MemoryMax=90%
diff --git a/src/systemd/update-motd b/src/systemd/update-motd
new file mode 100644
index 0000000..67e0fb6
--- /dev/null
+++ b/src/systemd/update-motd
@@ -0,0 +1,24 @@
+#!/bin/sh -e
+
+# syntax: update-motd [port [hostname [ipaddr [protocol]]]]
+# each argument can be given as the empty string to use the default
+
+# port number from cmdline, then systemctl file, then 9090
+# take the last Listen line; this will be the user-specified one
+port=${1:-$(systemctl show --property Listen cockpit.socket |
+ sed -E '$!d;$s/.*[^0-9]([0-9]+).*/\1/;')}
+port=${port:-9090}
+
+# hostname from cmdline, then `hostname -f`
+hostname=${2:-$(hostname -f || hostname)}
+
+# ip addr from cmdline, then default route source addr
+ip=${3:-$(ip -o route get 255.0 2>/dev/null | sed -e 's/.*src \([^ ]*\) .*/\1/')}
+
+# protocol from cmdline, then https
+protocol=${4:-https}
+
+hostname_url="${protocol}://${hostname}:${port}/"
+ip_url="${ip:+ or ${protocol}://${ip}:${port}/}"
+
+printf 'Web console: %s%s\n\n' "${hostname_url}" "${ip_url}" > /run/cockpit/active.motd
diff --git a/src/testlib/Makefile.am b/src/testlib/Makefile.am
new file mode 100644
index 0000000..b7cfc6b
--- /dev/null
+++ b/src/testlib/Makefile.am
@@ -0,0 +1,60 @@
+# -----------------------------------------------------------------------------
+# Common cpp and ld flags for tests: these will be used an awful lot
+
+TEST_CPP = \
+ -DSRCDIR=\"$(abs_srcdir)\" \
+ -DBUILDDIR=\"$(abs_builddir)\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+TEST_LIBS = \
+ libcockpit-test.a \
+ $(libcockpit_common_a_LIBS) \
+ $(NULL)
+
+# -----------------------------------------------------------------------------
+# Some helpful variables to get tests into TESTS and check_* in one go
+
+# Unfortunately, we can't call these _PROGRAMS or _SCRIPTS because otherwise
+# automake will treat them differently, which we don't want.
+
+TEST_PROGRAM =
+TESTS += $(TEST_PROGRAM)
+check_PROGRAMS = $(TEST_PROGRAM)
+
+TEST_SCRIPT =
+TESTS += $(TEST_SCRIPT)
+check_SCRIPTS = $(TEST_SCRIPT)
+
+dist_TEST_SCRIPT =
+TESTS += $(dist_TEST_SCRIPT)
+dist_check_SCRIPTS = $(dist_TEST_SCRIPT)
+
+# Testing assets should add themselves here (not EXTRA_DIST)
+check_DATA =
+dist_check_DATA =
+
+# -----------------------------------------------------------------------------
+# libcockpit-test.a: code used only for unit tests
+
+check_LIBRARIES = libcockpit-test.a
+
+libcockpit_test_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-test\" \
+ $(TEST_CPP)
+
+libcockpit_test_a_SOURCES = \
+ src/testlib/cockpittest.c \
+ src/testlib/cockpittest.h \
+ src/testlib/mock-auth.c \
+ src/testlib/mock-auth.h \
+ src/testlib/mock-channel.c \
+ src/testlib/mock-channel.h \
+ src/testlib/mock-pressure.c \
+ src/testlib/mock-pressure.h \
+ src/testlib/mock-transport.c \
+ src/testlib/mock-transport.h \
+ src/testlib/retest.c \
+ src/testlib/retest.h \
+ $(NULL)
diff --git a/src/testlib/cockpittest.c b/src/testlib/cockpittest.c
new file mode 100644
index 0000000..2b7064c
--- /dev/null
+++ b/src/testlib/cockpittest.c
@@ -0,0 +1,621 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpittest.h"
+
+#include "common/cockpitconf.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitsystem.h"
+
+#include <glib-object.h>
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <netinet/ip6.h>
+#include <ifaddrs.h>
+
+/*
+ * HACK: We can't yet use g_test_expect_message() and friends.
+ * They were pretty broken until GLib 2.40 if you have any debug
+ * or info messages ... which we do.
+ *
+ * https://bugzilla.gnome.org/show_bug.cgi?id=661926
+ */
+
+static gboolean cockpit_test_init_was_called = FALSE;
+static const gchar *orig_g_debug;
+
+/* In cockpitconf.c */
+extern const gchar *cockpit_config_file;
+
+G_LOCK_DEFINE (expected);
+
+typedef struct {
+ gchar *log_domain;
+ GLogLevelFlags log_level;
+ gchar *pattern;
+ const gchar *file;
+ int line;
+ const gchar *func;
+ gboolean skipable;
+ gboolean optional;
+} ExpectedMessage;
+
+static void
+expected_message_free (gpointer data)
+{
+ ExpectedMessage *expected = data;
+ g_free (expected->log_domain);
+ g_free (expected->pattern);
+ g_free (expected);
+}
+
+static gint ignore_fatal_count = 0;
+static GSList *expected_messages = NULL;
+static GLogFunc gtest_default_log_handler = NULL;
+
+static const gchar *
+calc_prefix (gint level)
+{
+ switch (level)
+ {
+ case G_LOG_LEVEL_ERROR:
+ return "ERROR";
+ case G_LOG_LEVEL_CRITICAL:
+ return "CRITICAL";
+ case G_LOG_LEVEL_WARNING:
+ return "WARNING";
+ case G_LOG_LEVEL_MESSAGE:
+ return "Message";
+ case G_LOG_LEVEL_INFO:
+ return "INFO";
+ case G_LOG_LEVEL_DEBUG:
+ return "DEBUG";
+ default:
+ return "Unknown";
+ }
+}
+
+static gboolean
+expected_fatal_handler (const gchar *log_domain,
+ GLogLevelFlags log_level,
+ const gchar *message,
+ gpointer user_data)
+{
+ gboolean ret = TRUE;
+
+ if (log_level & G_LOG_FLAG_FATAL)
+ {
+ G_LOCK (expected);
+
+ if (ignore_fatal_count > 0)
+ {
+ ignore_fatal_count--;
+ ret = FALSE;
+ }
+
+ G_UNLOCK (expected);
+ }
+
+ return ret;
+}
+
+static void
+expected_message_handler (const gchar *log_domain,
+ GLogLevelFlags log_level,
+ const gchar *message,
+ gpointer user_data)
+{
+ gint level = log_level & G_LOG_LEVEL_MASK;
+ ExpectedMessage *expected = NULL;
+ GSList *l = NULL;
+ gchar *expected_message;
+ gboolean skip = FALSE;
+
+ G_LOCK (expected);
+
+ if (level && expected_messages &&
+ (level & G_LOG_LEVEL_DEBUG) == 0)
+ {
+ if (log_level & G_LOG_FLAG_FATAL)
+ {
+ ignore_fatal_count = 1;
+
+ /* This handler is reset for each test, so set it right before we need it */
+ g_test_log_set_fatal_handler (expected_fatal_handler, NULL);
+ }
+
+ /* Loop until we find a non-skipable message or have a match */
+ for (l = expected_messages; l != NULL; l = l->next)
+ {
+ expected = l->data;
+ if (g_strcmp0 (expected->log_domain, log_domain) == 0 &&
+ ((log_level & expected->log_level) == expected->log_level) &&
+ g_pattern_match_simple (expected->pattern, message))
+ {
+ expected_messages = g_slist_delete_link (expected_messages, l);
+ expected_message_free (expected);
+ skip = TRUE;
+ break;
+ }
+ else if (!expected->skipable)
+ {
+ break;
+ }
+ }
+ }
+
+ G_UNLOCK (expected);
+
+ if (skip)
+ return;
+
+ gtest_default_log_handler (log_domain, log_level, message, NULL);
+
+ if (expected)
+ {
+ expected_message = g_strdup_printf ("Got unexpected message: %s instead of %s-%s: %s",
+ message,
+ expected->log_domain,
+ calc_prefix (expected->log_level),
+ expected->pattern);
+ g_assertion_message (expected->log_domain, expected->file, expected->line,
+ expected->func, expected_message);
+ g_free (expected_message);
+ }
+}
+
+/**
+ * cockpit_test_init:
+ *
+ * Call this instead of g_test_init() to setup a cocpit test.
+ * Enables use of cockpit_expect_xxx() functions.
+ *
+ * Sets up cleaner logging during testing.
+ *
+ * Also calls g_test_init() for you.
+ */
+void
+cockpit_test_init (int *argc,
+ char ***argv)
+{
+ static gchar path[4096];
+ gchar *basename;
+
+ signal (SIGPIPE, SIG_IGN);
+
+ cockpit_setenv_check ("GIO_USE_VFS", "local", TRUE);
+ cockpit_setenv_check ("GSETTINGS_BACKEND", "memory", TRUE);
+ cockpit_setenv_check ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE);
+
+ g_assert (g_snprintf (path, sizeof (path), "%s:%s", BUILDDIR, g_getenv ("PATH")) < sizeof (path));
+ cockpit_setenv_check ("PATH", path, TRUE);
+
+ /* For our process (children are handled through $G_DEBUG) */
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING);
+
+ // System cockpit configuration file should not be loaded
+ cockpit_config_file = NULL;
+
+ if (*argc > 0)
+ {
+ basename = g_path_get_basename ((*argv)[0]);
+ g_set_prgname (basename);
+ g_free (basename);
+ }
+
+ g_test_init (argc, argv, NULL);
+
+ /* Chain to the gtest log handler */
+ gtest_default_log_handler = g_log_set_default_handler (expected_message_handler, NULL);
+ g_assert (gtest_default_log_handler != NULL);
+
+ cockpit_test_init_was_called = TRUE;
+}
+
+void
+_cockpit_expect_logged_msg (const char *domain,
+ const gchar *file,
+ int line,
+ const gchar *func,
+ GLogLevelFlags log_level,
+ const gchar *pattern,
+ gboolean skipable,
+ gboolean optional)
+{
+ ExpectedMessage *expected;
+
+ g_assert (cockpit_test_init_was_called);
+
+ g_return_if_fail (log_level != 0);
+ g_return_if_fail (pattern != NULL);
+ g_return_if_fail (~log_level & G_LOG_LEVEL_ERROR);
+ g_return_if_fail (log_level & G_LOG_LEVEL_MASK);
+
+ expected = g_new (ExpectedMessage, 1);
+ expected->log_domain = g_strdup (domain);
+ expected->log_level = log_level & G_LOG_LEVEL_MASK;
+ expected->pattern = g_strdup (pattern);
+ expected->file = file;
+ expected->line = line;
+ expected->func = func;
+ expected->skipable = optional ? TRUE : skipable;
+ expected->optional = optional;
+
+ G_LOCK (expected);
+ expected_messages = g_slist_append (expected_messages, expected);
+ G_UNLOCK (expected);
+}
+
+/**
+ * cockpit_assert_expected:
+ *
+ * Assert that all the things we were expecting in a test
+ * happened. This should be called in a teardown() function
+ * or after a cockpit_expect_xxx() function.
+ */
+void
+cockpit_assert_expected (void)
+{
+ ExpectedMessage *expected = NULL;
+ gchar *message = NULL;
+ GSList *l = NULL;
+ g_assert (cockpit_test_init_was_called);
+
+ G_LOCK (expected);
+
+ if (expected_messages)
+ {
+ for (l = expected_messages; l != NULL; l = l->next)
+ {
+ expected = l->data;
+ if (!expected->optional)
+ {
+ message = g_strdup_printf ("Did not see expected %s-%s: %s",
+ expected->log_domain,
+ calc_prefix (expected->log_level),
+ expected->pattern);
+ break;
+ }
+ }
+ }
+
+ G_UNLOCK (expected);
+
+ if (message)
+ {
+ g_assertion_message (expected->log_domain, expected->file, expected->line,
+ expected->func, message);
+ g_free (message);
+ }
+
+ g_slist_free_full (expected_messages, expected_message_free);
+ expected_messages = NULL;
+ ignore_fatal_count = 0;
+}
+
+/**
+ * cockpit_assert_strmatch:
+ * @str: the string
+ * @pattern: to match
+ *
+ * Checks that @str matches the wildcard style @pattern
+ */
+void
+_cockpit_assert_strmatch_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ const gchar *string,
+ const gchar *pattern)
+{
+ const gchar *suffix;
+ gchar *escaped;
+ gchar *msg;
+ int len;
+
+ if (!string || !g_pattern_match_simple (pattern, string))
+ {
+ escaped = g_strescape (pattern, "");
+ if (!string)
+ {
+ msg = g_strdup_printf ("'%s' does not match: (null)", escaped);
+ }
+ else
+ {
+ suffix = "";
+ len = strlen (string);
+
+ /* To avoid insane output */
+ if (len > 8192)
+ {
+ len = 8192;
+ suffix = "\n...\n";
+ }
+
+ msg = g_strdup_printf ("'%s' does not match: %.*s%s", escaped, len, string, suffix);
+ }
+ g_assertion_message (domain, file, line, func, msg);
+ g_free (escaped);
+ g_free (msg);
+ }
+}
+
+void
+_cockpit_assert_json_eq_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ gpointer object_or_array,
+ const gchar *expect)
+{
+ GError *error = NULL;
+ JsonNode *node;
+ JsonNode *exnode;
+ gchar *escaped;
+ gchar *msg;
+
+ if (expect[0] == '[')
+ {
+ node = json_node_new (JSON_NODE_ARRAY);
+ json_node_set_array (node, object_or_array);
+ }
+ else
+ {
+ node = json_node_new (JSON_NODE_OBJECT);
+ json_node_set_object (node, object_or_array);
+ }
+
+ exnode = cockpit_json_parse (expect, -1, &error);
+ if (error)
+ g_assertion_message_error (domain, file, line, func, "error", error, 0, 0);
+ g_assert (exnode);
+
+ if (!cockpit_json_equal (exnode, node))
+ {
+ escaped = cockpit_json_write (node, NULL);
+
+ msg = g_strdup_printf ("%s != %s", escaped, expect);
+ g_assertion_message (domain, file, line, func, msg);
+ g_free (escaped);
+ g_free (msg);
+ }
+ json_node_free (node);
+ json_node_free (exnode);
+}
+
+void
+_cockpit_assert_gvariant_eq_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ GVariant *actual,
+ const gchar *expected)
+{
+ GVariant *expected_variant = g_variant_parse (NULL, expected, NULL, NULL, NULL);
+ if (!g_variant_equal (actual, expected_variant))
+ {
+ gchar *actual_string = g_variant_print (actual, TRUE);
+ gchar *msg = g_strdup_printf ("%s != %s", actual_string, expected);
+ g_assertion_message (domain, file, line, func, msg);
+ g_free (msg);
+ g_free (actual_string);
+ }
+ g_variant_unref (expected_variant);
+}
+
+static gchar *
+test_escape_data (const guchar *data,
+ gssize n_data)
+{
+ static const char HEXC[] = "0123456789ABCDEF";
+ GString *result;
+ gchar c;
+ gsize i;
+ guchar j;
+
+ if (!data)
+ return g_strdup ("NULL");
+
+ result = g_string_sized_new (n_data * 2 + 1);
+ for (i = 0; i < n_data; ++i)
+ {
+ c = data[i];
+ if (g_ascii_isprint (c) && !strchr ("\n\r\v", c))
+ {
+ g_string_append_c (result, c);
+ }
+ else
+ {
+ g_string_append (result, "\\x");
+ j = c >> 4 & 0xf;
+ g_string_append_c (result, HEXC[j]);
+ j = c & 0xf;
+ g_string_append_c (result, HEXC[j]);
+ }
+ }
+
+ return g_string_free (result, FALSE);
+}
+
+void
+_cockpit_assert_data_eq_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ gconstpointer data,
+ gssize len,
+ gconstpointer expect,
+ gssize exp_len)
+{
+ char *a1, *a2, *s;
+ if (!data && !expect)
+ return;
+ if (len < 0)
+ len = strlen (data ?: "");
+ if (exp_len < 0)
+ exp_len = strlen (expect ?: "");
+
+ if (len == exp_len)
+ {
+ if (len == 0)
+ return;
+ else if (data && expect && memcmp (data, expect, len) == 0)
+ return;
+ }
+
+ a1 = test_escape_data (data, len);
+ a2 = test_escape_data (expect, exp_len);
+ s = g_strdup_printf ("data is not the same (%s != %s)", a1, a2);
+ g_free (a1);
+ g_free (a2);
+ g_assertion_message (domain, file, line, func, s);
+ g_free (s);
+}
+
+void
+_cockpit_assert_bytes_eq_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ GBytes *data,
+ gconstpointer expect,
+ gssize exp_len)
+{
+ _cockpit_assert_data_eq_msg (domain, file, line, func,
+ g_bytes_get_data (data, NULL),
+ g_bytes_get_size (data),
+ expect, exp_len);
+}
+
+GInetAddress *
+cockpit_test_find_non_loopback_address (void)
+{
+ GInetAddress *inet = NULL;
+ struct ifaddrs *ifas, *ifa;
+ gpointer bytes;
+
+ g_assert_cmpint (getifaddrs (&ifas), ==, 0);
+ for (ifa = ifas; ifa != NULL; ifa = ifa->ifa_next)
+ {
+ if (!(ifa->ifa_flags & IFF_UP))
+ continue;
+ if (ifa->ifa_addr == NULL)
+ continue;
+ if (ifa->ifa_addr->sa_family == AF_INET)
+ {
+ bytes = &(((struct sockaddr_in *)ifa->ifa_addr)->sin_addr);
+ inet = g_inet_address_new_from_bytes (bytes, G_SOCKET_FAMILY_IPV4);
+ }
+ else if (ifa->ifa_addr->sa_family == AF_INET6)
+ {
+ bytes = &(((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr);
+ inet = g_inet_address_new_from_bytes (bytes, G_SOCKET_FAMILY_IPV6);
+ }
+ if (inet)
+ {
+ if (!g_inet_address_get_is_loopback (inet))
+ break;
+ g_object_unref (inet);
+ inet = NULL;
+ }
+ }
+
+ freeifaddrs (ifas);
+ return inet;
+}
+
+void
+cockpit_test_allow_warnings (void)
+{
+ /* make some noise if this gets called twice */
+ g_return_if_fail (orig_g_debug == NULL);
+ orig_g_debug = g_getenv ("G_DEBUG");
+ cockpit_setenv_check ("G_DEBUG", "fatal-criticals", TRUE);
+}
+
+void
+cockpit_test_reset_warnings (void)
+{
+ if (orig_g_debug != NULL)
+ {
+ cockpit_setenv_check ("G_DEBUG", orig_g_debug, TRUE);
+ orig_g_debug = NULL;
+ }
+}
+
+gboolean
+cockpit_test_skip_slow (void)
+{
+ if (g_getenv ("COCKPIT_SKIP_SLOW_TESTS"))
+ {
+ g_test_skip ("Skipping slow tests");
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+void
+cockpit_assertion_message_error_matches (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ const char *expr,
+ const GError *error,
+ GQuark error_domain,
+ int error_code,
+ const char *message_pattern)
+{
+ /* loosely based on g_assertion_message_error() */
+ g_autoptr(GString) gstring = g_string_new ("assertion failed ");
+
+ g_string_append_printf (gstring, "%s =~ GError(", expr);
+
+ if (error_domain)
+ g_string_append_printf (gstring, "domain=%s", g_quark_to_string (error_domain));
+ else
+ g_string_append (gstring, "domain=any");
+
+ g_string_append (gstring, ", ");
+
+ if (error_code != -1)
+ g_string_append_printf (gstring, "code=%d", error_code);
+ else
+ g_string_append (gstring, "code=any");
+
+ g_string_append (gstring, ", ");
+
+ if (message_pattern)
+ g_string_append_printf (gstring, "message=~'%s'", message_pattern);
+ else
+ g_string_append (gstring, "message=any");
+
+ g_string_append (gstring, ")): ");
+
+ if (error)
+ g_string_append_printf (gstring, "%s (%s, %d)", error->message,
+ g_quark_to_string (error->domain), error->code);
+ else
+ g_string_append_printf (gstring, "%s is NULL", expr);
+
+ g_assertion_message (domain, file, line, func, gstring->str);
+}
diff --git a/src/testlib/cockpittest.h b/src/testlib/cockpittest.h
new file mode 100644
index 0000000..4e279f5
--- /dev/null
+++ b/src/testlib/cockpittest.h
@@ -0,0 +1,175 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#ifndef __COCKPIT_TEST_H__
+#define __COCKPIT_TEST_H__
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TEST_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+
+void cockpit_test_init (int *argc,
+ char ***argv);
+
+void _cockpit_expect_logged_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ GLogLevelFlags log_level,
+ const gchar *pattern,
+ gboolean skipable,
+ gboolean optional);
+
+#define cockpit_expect_log(domain, level, pattern) \
+ (_cockpit_expect_logged_msg ((domain), __FILE__, __LINE__, G_STRFUNC, (level), (pattern), FALSE, FALSE))
+
+#define cockpit_expect_unordered_log(domain, level, pattern) \
+ (_cockpit_expect_logged_msg ((domain), __FILE__, __LINE__, G_STRFUNC, (level), (pattern), TRUE, FALSE))
+
+#define cockpit_expect_possible_log(domain, level, pattern) \
+ (_cockpit_expect_logged_msg ((domain), __FILE__, __LINE__, G_STRFUNC, (level), (pattern), TRUE, TRUE))
+
+#define cockpit_expect_warning(pattern) \
+ (_cockpit_expect_logged_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, G_LOG_LEVEL_WARNING, (pattern), FALSE, FALSE))
+
+#define cockpit_expect_critical(pattern) \
+ (_cockpit_expect_logged_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, G_LOG_LEVEL_CRITICAL, (pattern), FALSE, FALSE))
+
+#define cockpit_expect_message(pattern) \
+ (_cockpit_expect_logged_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, G_LOG_LEVEL_MESSAGE, (pattern), FALSE, FALSE))
+
+#define cockpit_expect_info(pattern) \
+ (_cockpit_expect_logged_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, G_LOG_LEVEL_INFO, (pattern), FALSE, FALSE))
+
+void cockpit_assert_expected (void);
+
+void _cockpit_assert_strmatch_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ const gchar *string,
+ const gchar *pattern);
+
+#define cockpit_assert_strmatch(str, pattern) \
+ (_cockpit_assert_strmatch_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (str), (pattern)))
+
+void _cockpit_assert_json_eq_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ gpointer object_or_array,
+ const gchar *json);
+
+#define cockpit_assert_json_eq(obj_or_arr, json) \
+ (_cockpit_assert_json_eq_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (obj_or_arr), (json)))
+
+void _cockpit_assert_gvariant_eq_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ GVariant *actual,
+ const gchar *expected);
+
+#define cockpit_assert_gvariant_eq(actual, expected) \
+ (_cockpit_assert_gvariant_eq_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (actual), (expected)))
+
+void _cockpit_assert_data_eq_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ gconstpointer data,
+ gssize len,
+ gconstpointer expect,
+ gssize exp_len);
+
+#define cockpit_assert_data_eq(data, len, exp, elen) \
+ (_cockpit_assert_data_eq_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (data), (len), (exp), (elen)))
+
+void _cockpit_assert_bytes_eq_msg (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ GBytes *data,
+ gconstpointer expect,
+ gssize exp_len);
+
+#define cockpit_assert_bytes_eq(data, exp, len) \
+ (_cockpit_assert_bytes_eq_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (data), (exp), (len)))
+
+
+void cockpit_test_signal_backtrace (int sig);
+
+GInetAddress * cockpit_test_find_non_loopback_address (void);
+
+void cockpit_test_allow_warnings (void);
+void cockpit_test_reset_warnings (void);
+
+gboolean cockpit_test_skip_slow (void);
+
+void cockpit_assertion_message_error_matches (const char *domain,
+ const char *file,
+ int line,
+ const char *func,
+ const char *expr,
+ const GError *error,
+ GQuark error_domain,
+ int error_code,
+ const char *error_pattern);
+#define cockpit_assert_error_matches(err, dom, c, message_pattern) \
+ G_STMT_START { \
+ if ((err) == NULL || \
+ (dom != 0 && (error)->domain != dom) || \
+ (c != -1 && (error)->code != c) || \
+ (message_pattern && !g_pattern_match_simple (message_pattern, (err)->message))) \
+ cockpit_assertion_message_error_matches (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+ #err, err, dom, c, message_pattern); \
+ } G_STMT_END
+
+
+#define cockpit_test_add_full(Fixture, fixture, TestCase, testpath, function, ...) \
+ G_STMT_START { \
+ typedef void (*CockpitTestFixtureFunc) (Fixture *, const TestCase *); \
+ struct { \
+ CockpitTestFixtureFunc _setup; \
+ CockpitTestFixtureFunc _teardown; \
+ CockpitTestFixtureFunc _test; \
+ } _cockpit_test_vtable = { \
+ ._setup = fixture ## _setup, \
+ ._teardown = fixture ## _teardown, \
+ ._test = function, \
+ }; \
+ static const TestCase _cockpit_test_case = { \
+ __VA_ARGS__ \
+ }; \
+ g_test_add_vtable (testpath, sizeof (Fixture), &_cockpit_test_case, \
+ (GTestFixtureFunc) _cockpit_test_vtable._setup, \
+ (GTestFixtureFunc) _cockpit_test_vtable._test, \
+ (GTestFixtureFunc) _cockpit_test_vtable._teardown); \
+ } G_STMT_END
+
+#define cockpit_test_add(testpath, function, ...) \
+ cockpit_test_add_full(Fixture, fixture, TestCase, testpath, function, __VA_ARGS__)
+
+G_END_DECLS
+
+#endif /* __COCKPIT_TEST_H__ */
diff --git a/src/testlib/mock-auth.c b/src/testlib/mock-auth.c
new file mode 100644
index 0000000..e94a287
--- /dev/null
+++ b/src/testlib/mock-auth.c
@@ -0,0 +1,70 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "mock-auth.h"
+
+#include "common/cockpitauthorize.h"
+
+#include "websocket/websocket.h"
+
+#include <string.h>
+
+GHashTable *
+mock_auth_basic_header (const gchar *user,
+ const gchar *password)
+{
+ GHashTable *headers;
+ gchar *userpass;
+ gchar *encoded;
+ gchar *header;
+
+ userpass = g_strdup_printf ("%s:%s", user, password);
+ encoded = g_base64_encode ((guchar *)userpass, strlen (userpass));
+ header = g_strdup_printf ("Basic %s", encoded);
+
+ g_free (userpass);
+ g_free (encoded);
+
+ headers = web_socket_util_new_headers ();
+ g_hash_table_insert (headers, g_strdup ("Authorization"), header);
+ return headers;
+}
+
+void
+mock_auth_include_cookie_as_if_client (GHashTable *resp_headers,
+ GHashTable *req_headers,
+ const gchar *cookie_name)
+{
+ gchar *cookie;
+ gchar *end;
+ gchar *expected = g_strdup_printf ("%s=", cookie_name);
+
+ cookie = g_strdup (g_hash_table_lookup (resp_headers, "Set-Cookie"));
+ g_assert (cookie != NULL);
+ end = strchr (cookie, ';');
+ g_assert (end != NULL);
+ end[0] = '\0';
+
+ g_assert (strncmp (cookie, expected, strlen(expected)) == 0);
+
+ g_hash_table_insert (req_headers, g_strdup ("Cookie"), cookie);
+ g_free (expected);
+}
diff --git a/src/testlib/mock-auth.h b/src/testlib/mock-auth.h
new file mode 100644
index 0000000..fc54457
--- /dev/null
+++ b/src/testlib/mock-auth.h
@@ -0,0 +1,35 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __MOCK_AUTH_H__
+#define __MOCK_AUTH_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+GHashTable * mock_auth_basic_header (const gchar *user,
+ const gchar *password);
+
+void mock_auth_include_cookie_as_if_client (GHashTable *resp_headers,
+ GHashTable *req_headers,
+ const gchar *cookie_name);
+G_END_DECLS
+
+#endif
diff --git a/src/testlib/mock-channel.c b/src/testlib/mock-channel.c
new file mode 100644
index 0000000..b5b3389
--- /dev/null
+++ b/src/testlib/mock-channel.c
@@ -0,0 +1,75 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "mock-channel.h"
+
+typedef CockpitChannelClass MockEchoChannelClass;
+
+G_DEFINE_TYPE (MockEchoChannel, mock_echo_channel, COCKPIT_TYPE_CHANNEL);
+
+static void
+mock_echo_channel_recv (CockpitChannel *channel,
+ GBytes *message)
+{
+ cockpit_channel_send (channel, message, FALSE);
+}
+
+static gboolean
+mock_echo_channel_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *options)
+{
+ cockpit_channel_control (channel, command, options);
+ return TRUE;
+}
+
+static void
+mock_echo_channel_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ MockEchoChannel *self = (MockEchoChannel *)channel;
+ self->close_called = TRUE;
+ COCKPIT_CHANNEL_CLASS (mock_echo_channel_parent_class)->close (channel, problem);
+}
+
+static void
+mock_echo_channel_init (MockEchoChannel *self)
+{
+
+}
+
+static void
+mock_echo_channel_constructed (GObject *obj)
+{
+ G_OBJECT_CLASS (mock_echo_channel_parent_class)->constructed (obj);
+ cockpit_channel_ready (COCKPIT_CHANNEL (obj), NULL);
+}
+
+static void
+mock_echo_channel_class_init (MockEchoChannelClass *klass)
+{
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->constructed = mock_echo_channel_constructed;
+ channel_class->recv = mock_echo_channel_recv;
+ channel_class->control = mock_echo_channel_control;
+ channel_class->close = mock_echo_channel_close;
+}
diff --git a/src/testlib/mock-channel.h b/src/testlib/mock-channel.h
new file mode 100644
index 0000000..eac17c6
--- /dev/null
+++ b/src/testlib/mock-channel.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MOCK_CHANNEL_H
+#define MOCK_CHANNEL_H
+
+#include "common/cockpitchannel.h"
+
+#define MOCK_TYPE_ECHO_CHANNEL (mock_echo_channel_get_type ())
+#define MOCK_ECHO_CHANNEL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), MOCK_TYPE_ECHO_CHANNEL, MockEchoChannel))
+#define MOCK_IS_ECHO_CHANNEL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), MOCK_TYPE_ECHO_CHANNEL))
+
+typedef struct {
+ CockpitChannel parent;
+ gboolean close_called;
+} MockEchoChannel;
+
+GType mock_echo_channel_get_type (void);
+
+#endif /* MOCK_CHANNEL_H */
diff --git a/src/testlib/mock-pressure.c b/src/testlib/mock-pressure.c
new file mode 100644
index 0000000..7eb16a1
--- /dev/null
+++ b/src/testlib/mock-pressure.c
@@ -0,0 +1,54 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "mock-pressure.h"
+
+typedef GObject MockPressure;
+typedef GObjectClass MockPressureClass;
+
+static void mock_pressure_flow_iface_init (CockpitFlowInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (MockPressure, mock_pressure, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (COCKPIT_TYPE_FLOW, mock_pressure_flow_iface_init));
+
+static void
+mock_pressure_init (MockPressure *self)
+{
+
+}
+
+static void
+mock_pressure_class_init (MockPressureClass *klass)
+{
+
+}
+
+static void
+mock_pressure_flow_iface_init (CockpitFlowInterface *iface)
+{
+ /* No implementation */
+}
+
+CockpitFlow *
+mock_pressure_new (void)
+{
+ return g_object_new (MOCK_TYPE_PRESSURE, NULL);
+}
diff --git a/src/testlib/mock-pressure.h b/src/testlib/mock-pressure.h
new file mode 100644
index 0000000..407b3fd
--- /dev/null
+++ b/src/testlib/mock-pressure.h
@@ -0,0 +1,33 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MOCK_PRESSURE_H
+#define MOCK_PRESSURE_H
+
+#include <glib-object.h>
+
+#include "common/cockpitflow.h"
+
+#define MOCK_TYPE_PRESSURE (mock_pressure_get_type ())
+
+GType mock_pressure_get_type (void);
+
+CockpitFlow * mock_pressure_new (void);
+
+#endif /* MOCK_PRESSURE_H */
diff --git a/src/testlib/mock-transport.c b/src/testlib/mock-transport.c
new file mode 100644
index 0000000..8ff0892
--- /dev/null
+++ b/src/testlib/mock-transport.c
@@ -0,0 +1,221 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "mock-transport.h"
+
+#include "common/cockpitjson.h"
+
+#include <gio/gio.h>
+
+typedef CockpitTransportClass MockTransportClass;
+
+typedef struct {
+ gpointer data;
+ GDestroyNotify func;
+} Trash;
+
+G_DEFINE_TYPE (MockTransport, mock_transport, COCKPIT_TYPE_TRANSPORT);
+
+static void
+trash_free (gpointer data)
+{
+ Trash *trash = data;
+ (trash->func) (trash->data);
+ g_free (trash);
+}
+
+static void
+trash_push (MockTransport *self,
+ gpointer data,
+ GDestroyNotify func)
+{
+ Trash *trash = g_new0 (Trash, 1);
+ g_assert (func != NULL);
+ trash->data = data;
+ trash->func = func;
+ self->trash = g_list_prepend (self->trash, trash);
+}
+
+static void
+mock_transport_init (MockTransport *self)
+{
+ self->channels = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, (GDestroyNotify)g_queue_free);
+ self->control = g_queue_new ();
+}
+
+static void
+mock_transport_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (prop_id)
+ {
+ case 1:
+ g_value_set_string (value, "mock-name");
+ break;
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+}
+
+static void
+mock_transport_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (prop_id)
+ {
+ case 1:
+ break;
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+}
+
+static void
+mock_transport_finalize (GObject *object)
+{
+ MockTransport *self = (MockTransport *)object;
+
+ g_free (self->problem);
+ g_queue_free (self->control);
+ g_list_free_full (self->trash, trash_free);
+ g_hash_table_destroy (self->channels);
+
+ G_OBJECT_CLASS (mock_transport_parent_class)->finalize (object);
+}
+
+static void
+mock_transport_send (CockpitTransport *transport,
+ const gchar *channel_id,
+ GBytes *data)
+{
+ MockTransport *self = (MockTransport *)transport;
+ JsonObject *object;
+ GError *error = NULL;
+ GQueue *queue;
+
+ if (!channel_id)
+ {
+ object = cockpit_json_parse_bytes (data, &error);
+ g_assert_no_error (error);
+ g_queue_push_tail (self->control, object);
+ trash_push (self, object, (GDestroyNotify)json_object_unref);
+ }
+ else
+ {
+ queue = g_hash_table_lookup (self->channels, channel_id);
+ if (!queue)
+ {
+ queue = g_queue_new ();
+ g_hash_table_insert (self->channels, g_strdup (channel_id), queue);
+ }
+ g_queue_push_tail (queue, g_bytes_ref (data));
+ trash_push (self, data, (GDestroyNotify)g_bytes_unref);
+ }
+ self->count++;
+}
+
+static void
+mock_transport_close (CockpitTransport *transport,
+ const gchar *problem)
+{
+ MockTransport *self = (MockTransport *)transport;
+ g_assert (!self->closed);
+ self->problem = g_strdup (problem);
+ self->closed = TRUE;
+ cockpit_transport_emit_closed (transport, problem);
+}
+
+static void
+mock_transport_class_init (MockTransportClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ CockpitTransportClass *transport_class = COCKPIT_TRANSPORT_CLASS (klass);
+ object_class->finalize = mock_transport_finalize;
+ object_class->get_property = mock_transport_get_property;
+ object_class->set_property = mock_transport_set_property;
+ g_object_class_override_property (object_class, 1, "name");
+ transport_class->send = mock_transport_send;
+ transport_class->close = mock_transport_close;
+}
+
+MockTransport *
+mock_transport_new (void)
+{
+ return g_object_new (MOCK_TYPE_TRANSPORT, NULL);
+}
+
+GBytes *
+mock_transport_pop_channel (MockTransport *mock,
+ const gchar *channel_id)
+{
+ GQueue *queue;
+
+ g_assert (channel_id != NULL);
+
+ queue = g_hash_table_lookup (mock->channels, channel_id);
+ if (queue)
+ return g_queue_pop_head (queue);
+ return NULL;
+}
+
+JsonObject *
+mock_transport_pop_control (MockTransport *mock)
+{
+ return g_queue_pop_head (mock->control);
+}
+
+guint
+mock_transport_count_sent (MockTransport *mock)
+{
+ return mock->count;
+}
+
+GBytes *
+mock_transport_combine_output (MockTransport *transport,
+ const gchar *channel_id,
+ guint *count)
+{
+ GByteArray *combined;
+ GBytes *block;
+
+ if (count)
+ *count = 0;
+
+ combined = g_byte_array_new ();
+ for (;;)
+ {
+ block = mock_transport_pop_channel (transport, channel_id);
+ if (!block)
+ break;
+
+ g_byte_array_append (combined, g_bytes_get_data (block, NULL), g_bytes_get_size (block));
+ if (count)
+ (*count)++;
+ }
+ return g_byte_array_free_to_bytes (combined);
+}
diff --git a/src/testlib/mock-transport.h b/src/testlib/mock-transport.h
new file mode 100644
index 0000000..e824051
--- /dev/null
+++ b/src/testlib/mock-transport.h
@@ -0,0 +1,56 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MOCK_TRANSPORT_H
+#define MOCK_TRANSPORT_H
+
+#include "common/cockpittransport.h"
+
+#include <gio/gio.h>
+
+#define MOCK_TYPE_TRANSPORT (mock_transport_get_type ())
+#define MOCK_TRANSPORT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), MOCK_TYPE_TRANSPORT, MockTransport))
+#define MOCK_IS_TRANSPORT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), MOCK_TYPE_TRANSPORT))
+
+typedef struct {
+ CockpitTransport parent;
+ gboolean closed;
+ gchar *problem;
+ guint count;
+ GQueue *control;
+ GHashTable *channels;
+ GList *trash;
+} MockTransport;
+
+GType mock_transport_get_type (void);
+
+MockTransport * mock_transport_new (void);
+
+guint mock_transport_count_sent (MockTransport *mock);
+
+JsonObject * mock_transport_pop_control (MockTransport *mock);
+
+GBytes * mock_transport_pop_channel (MockTransport *mock,
+ const gchar *channel);
+
+GBytes * mock_transport_combine_output (MockTransport *transport,
+ const gchar *channel_id,
+ guint *count);
+
+#endif /* MOCK_TRANSPORT_H */
diff --git a/src/testlib/retest.c b/src/testlib/retest.c
new file mode 100644
index 0000000..108cde2
--- /dev/null
+++ b/src/testlib/retest.c
@@ -0,0 +1,422 @@
+/*
+ * Copyright (c) 2013, Red Hat Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ * * Redistributions in binary form must reproduce the
+ * above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * * The names of contributors to this software may not be
+ * used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * Author: Stef Walter <stefw@redhat.com>
+ */
+
+#include "config.h"
+
+#include "retest.h"
+#undef assert
+
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <assert.h>
+#include <errno.h>
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+enum {
+ FIXTURE,
+ TEST,
+};
+
+typedef void (*func_with_arg) (void *);
+
+typedef struct _test_item {
+ int type;
+
+ union {
+ struct {
+ char name[1024];
+ func_with_arg func;
+ void *argument;
+ int done;
+ } test;
+ struct {
+ func_with_arg setup;
+ func_with_arg teardown;
+ } fix;
+ } x;
+
+ struct _test_item *next;
+} test_item;
+
+struct {
+ test_item *suite;
+ test_item *last;
+ int number;
+ jmp_buf jump;
+
+ /* forking */
+ int am_child;
+ pid_t child_pid;
+ int child_status;
+} gl = { NULL, NULL, 0, };
+
+GNUC_NORETURN static void
+exit_child (int code)
+{
+ fflush (stderr);
+ fflush (stdout);
+ _exit (code);
+}
+
+void
+re_test_fail (const char *filename,
+ int line,
+ const char *function,
+ const char *message,
+ ...)
+{
+ const char *pos;
+ char *output;
+ char *from;
+ char *next;
+ va_list va;
+
+ assert (gl.last != NULL);
+ assert (gl.last->type == TEST);
+
+ if (gl.child_status == 0)
+ {
+ gl.last->x.test.done = 1;
+
+ printf ("not ok %d %s\n", gl.number, gl.last->x.test.name);
+
+ va_start (va, message);
+ if (vasprintf (&output, message, va) < 0)
+ assert (0 && "vasprintf failed");
+ va_end (va);
+
+ for (from = output; from != NULL; )
+ {
+ next = strchr (from, '\n');
+ if (next)
+ {
+ next[0] = '\0';
+ next += 1;
+ }
+
+ printf ("# %s\n", from);
+ from = next;
+ }
+
+ pos = strrchr (filename, '/');
+ if (pos != NULL && pos[1] != '\0')
+ filename = pos + 1;
+
+ printf ("# in %s() at %s:%d\n", function, filename, line);
+
+ free (output);
+ }
+
+ if (gl.child_pid)
+ kill (gl.child_pid, SIGPIPE);
+
+ /* Let coverity know we're not supposed to return from here */
+#ifdef __COVERITY__
+ abort();
+#endif
+
+ if (!gl.am_child)
+ longjmp (gl.jump, 1);
+ exit_child (67);
+}
+
+void
+re_test_skip (const char *reason)
+{
+ assert (gl.last != NULL);
+ assert (gl.last->type == TEST);
+
+ if (gl.child_status == 0)
+ {
+ gl.last->x.test.done = 1;
+ printf ("ok %d # skip -- %s\n", gl.number, reason);
+ }
+
+ if (gl.child_pid)
+ kill (gl.child_pid, SIGPIPE);
+
+ /* Let coverity know we're not supposed to return from here */
+#ifdef __COVERITY__
+ abort();
+#endif
+
+ if (gl.am_child)
+ exit_child (77);
+ else
+ longjmp (gl.jump, 1);
+}
+
+static void
+test_push (test_item *it)
+{
+ test_item *item;
+
+ item = calloc (1, sizeof (test_item));
+ assert (item != NULL);
+ memcpy (item, it, sizeof (test_item));
+
+ if (!gl.suite)
+ gl.suite = item;
+ if (gl.last)
+ gl.last->next = item;
+ gl.last = item;
+}
+
+void
+re_test (void (* function) (void),
+ const char *name,
+ ...)
+{
+ test_item item = { TEST, };
+ va_list va;
+
+ item.x.test.func = (func_with_arg)function;
+
+ va_start (va, name);
+ vsnprintf (item.x.test.name, sizeof (item.x.test.name), name, va);
+ va_end (va);
+
+ test_push (&item);
+}
+
+void
+re_testx (void (* function) (void *),
+ void *argument,
+ const char *name,
+ ...)
+{
+ test_item item = { TEST, };
+ va_list va;
+
+ item.type = TEST;
+ item.x.test.func = function;
+ item.x.test.argument = argument;
+
+ va_start (va, name);
+ vsnprintf (item.x.test.name, sizeof (item.x.test.name), name, va);
+ va_end (va);
+
+ test_push (&item);
+}
+
+void
+re_fixture (void (* setup) (void *),
+ void (* teardown) (void *))
+{
+ test_item item;
+
+ item.type = FIXTURE;
+ item.x.fix.setup = setup;
+ item.x.fix.teardown = teardown;
+
+ test_push (&item);
+}
+
+int
+re_test_run (int argc,
+ char **argv)
+{
+ test_item *fixture = NULL;
+ test_item *item;
+ test_item *next;
+ int count;
+ int setup;
+
+ assert (gl.number == 0);
+ gl.last = NULL;
+
+ for (item = gl.suite, count = 0; item != NULL; item = item->next)
+ {
+ if (item->type == TEST)
+ count++;
+ }
+
+ if (count == 0)
+ {
+ printf ("1..0 # No tests\n");
+ return 0;
+ }
+
+ printf ("1..%d\n", count);
+
+ for (item = gl.suite, gl.number = 0; item != NULL; item = item->next)
+ {
+ if (item->type == FIXTURE)
+ {
+ fixture = item;
+ continue;
+ }
+
+ assert (item->type == TEST);
+ gl.last = item;
+ gl.am_child = 0;
+ gl.child_status = 0;
+ gl.child_pid = 0;
+ gl.number++;
+ setup = 0;
+
+ if (setjmp (gl.jump) == 0)
+ {
+ if (fixture && fixture->x.fix.setup)
+ (fixture->x.fix.setup) (item->x.test.argument);
+
+ setup = 1;
+
+ assert (item->x.test.func);
+ (item->x.test.func)(item->x.test.argument);
+
+ /* child success path */
+ if (gl.am_child)
+ exit_child (0);
+ }
+
+ /* parent checks on child */
+ if (gl.child_pid)
+ {
+ if (waitpid (gl.child_pid, &gl.child_status, 0) < 0)
+ assert (0 && "waitpid failed");
+ gl.child_pid = 0;
+ }
+ if (gl.child_status != 0)
+ {
+ if (WIFEXITED (gl.child_status) &&
+ WEXITSTATUS (gl.child_status) != 77 &&
+ WEXITSTATUS (gl.child_status) != 67)
+ printf ("not ok %d %s\n", gl.number, item->x.test.name);
+ gl.last->x.test.done = 1;
+ }
+
+ if (setup)
+ {
+ if (setjmp (gl.jump) == 0)
+ {
+ if (fixture && fixture->x.fix.teardown)
+ (fixture->x.fix.teardown) (item->x.test.argument);
+ }
+ }
+
+ if (!gl.last->x.test.done)
+ printf ("ok %d %s\n", gl.number, item->x.test.name);
+
+ gl.last = NULL;
+ }
+
+ for (item = gl.suite; item != NULL; item = next)
+ {
+ next = item->next;
+ free (item);
+ }
+
+ gl.suite = NULL;
+ gl.last = 0;
+ gl.number = 0;
+ return 0;
+}
+
+char *
+re_test_directory (const char *prefix)
+{
+ char *directory;
+
+ if (asprintf (&directory, "%s.XXXXXX", prefix) < 0)
+ assert (0 && "allocation failed");
+
+ if (!mkdtemp (directory)) {
+ printf ("# couldn't create temp directory: %s: %s\n",
+ directory, strerror (errno));
+ free (directory);
+ assert (0 && "mkdtemp failed");
+ }
+
+ return directory;
+}
+
+static void
+child_handler (int sig)
+{
+ pid_t pid;
+ int status;
+
+ pid = waitpid (gl.child_pid, &status, WNOHANG);
+ if (pid < 0)
+ {
+ if (errno == ECHILD || errno == EAGAIN)
+ return;
+ assert (pid >= 0);
+ }
+ if (pid != 0 && gl.child_pid == pid)
+ {
+ gl.child_status = status;
+ gl.child_pid = 0;
+ }
+}
+
+int
+re_test_fork (void)
+{
+ struct sigaction sa;
+
+ assert (!gl.am_child);
+ assert (!gl.child_pid);
+
+ fflush (stdout);
+ fflush (stderr);
+
+ gl.child_pid = fork();
+ assert (gl.child_pid >= 0);
+
+ if (gl.child_pid == 0)
+ {
+ if (signal (SIGCHLD, SIG_DFL) == SIG_ERR)
+ assert (0 && "signal failed");
+ gl.am_child = 1;
+ }
+ else
+ {
+ /* Remove SA_RESTART from SIGCHLD */
+ sa.sa_handler = child_handler;
+ sigemptyset (&sa.sa_mask);
+ sa.sa_flags = SA_NOCLDSTOP;
+ if (sigaction (SIGCHLD, &sa, 0) < 0)
+ assert (0 && "sigaction failed");
+ }
+
+ return gl.am_child;
+}
diff --git a/src/testlib/retest.h b/src/testlib/retest.h
new file mode 100644
index 0000000..bc421d2
--- /dev/null
+++ b/src/testlib/retest.h
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2013, Red Hat Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ * * Redistributions in binary form must reproduce the
+ * above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * * The names of contributors to this software may not be
+ * used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * Author: Stef Walter <stefw@redhat.com>
+ */
+
+#ifndef RE_TEST_H_
+#define RE_TEST_H_
+
+#ifndef GNUC_PRINTF
+#if __GNUC__ > 2
+#define GNUC_PRINTF(x, y) __attribute__((__format__(__printf__, x, y)))
+#else
+#define GNUC_PRINTF(x, y)
+#endif
+#endif
+
+#ifndef __has_feature
+#define __has_feature(x) 0
+#endif
+
+#if __has_feature(attribute_analyzer_noreturn) && defined(__clang_analyzer__)
+#define GNUC_NORETURN __attribute__((analyzer_noreturn))
+#elif __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ > 4)
+#define GNUC_NORETURN __attribute__((__noreturn__))
+#else
+#define GNUC_NORETURN
+#endif
+
+#include <string.h>
+
+#ifdef assert_not_reached
+#undef assert_not_reached
+#endif
+
+#ifdef assert
+#undef assert
+#endif
+
+#define assert(expr) \
+ assert_true(expr)
+#define assert_true(expr) \
+ do { if (expr) ; else \
+ re_test_fail (__FILE__, __LINE__, __FUNCTION__, "assertion failed (%s)", #expr); \
+ } while (0)
+#define assert_false(expr) \
+ do { if (expr) \
+ re_test_fail (__FILE__, __LINE__, __FUNCTION__, "assertion failed (!(%s))", #expr); \
+ } while (0)
+#define assert_fail(msg, detail) \
+ do { const char *__s = (detail); \
+ re_test_fail (__FILE__, __LINE__, __FUNCTION__, "%s%s%s", (msg), __s ? ": ": "", __s ? __s : ""); \
+ } while (0)
+#define assert_not_reached(msg) \
+ do { \
+ re_test_fail (__FILE__, __LINE__, __FUNCTION__, "code should not be reached"); \
+ } while (0)
+#define assert_ptr_not_null(ptr) \
+ do { if ((ptr) != NULL) ; else \
+ re_test_fail (__FILE__, __LINE__, __FUNCTION__, "assertion failed (%s != NULL)", #ptr); \
+ } while (0)
+#define assert_num_cmp(a1, cmp, a2) \
+ do { long long __n1 = (long long)(a1); \
+ long long __n2 = (long long)(a2); \
+ if (__n1 cmp __n2) ; else \
+ re_test_fail (__FILE__, __LINE__, __FUNCTION__, "assertion failed (%s %s %s): (%lld %s %lld)", \
+ #a1, #cmp, #a2, __n1, #cmp, __n2); \
+ } while (0)
+#define assert_num_eq(a1, a2) \
+ assert_num_cmp(a1, ==, a2)
+#define assert_str_cmp(a1, cmp, a2) \
+ do { const char *__s1 = (a1); \
+ const char *__s2 = (a2); \
+ if (__s1 && __s2 && strcmp (__s1, __s2) cmp 0) ; else \
+ re_test_fail (__FILE__, __LINE__, __FUNCTION__, "assertion failed (%s %s %s): (%s %s %s)", \
+ #a1, #cmp, #a2, __s1 ? __s1 : "(null)", #cmp, __s2 ? __s2 : "(null)"); \
+ } while (0)
+#define assert_str_eq(a1, a2) \
+ assert_str_cmp(a1, ==, a2)
+#define assert_ptr_eq(a1, a2) \
+ do { const void *__p1 = (a1); \
+ const void *__p2 = (a2); \
+ if (__p1 == __p2) ; else \
+ re_test_fail (__FILE__, __LINE__, __FUNCTION__, "assertion failed (%s == %s): (0x%08lx == 0x%08lx)", \
+ #a1, #a2, (unsigned long)(size_t)__p1, (unsigned long)(size_t)__p2); \
+ } while (0)
+
+#define assert_str_contains(expr, needle) \
+ do { const char *__str = (expr); \
+ if (__str && strstr (__str, needle)) ; else \
+ re_test_fail (__FILE__, __LINE__, __FUNCTION__, "assertion failed (%s): '%s' does not contain '%s'", \
+ #expr, __str, needle); \
+ } while (0)
+
+
+void re_test_fail (const char *filename,
+ int line,
+ const char *function,
+ const char *message,
+ ...) GNUC_PRINTF(4, 5) GNUC_NORETURN;
+
+void re_test_skip (const char *reason);
+
+void re_test (void (* function) (void),
+ const char *name,
+ ...) GNUC_PRINTF(2, 3);
+
+void re_testx (void (* function) (void *),
+ void *argument,
+ const char *name,
+ ...) GNUC_PRINTF(3, 4);
+
+void re_fixture (void (* setup) (void *),
+ void (* teardown) (void *));
+
+int re_test_run (int argc,
+ char **argv);
+
+int re_test_fork (void);
+
+char * re_test_directory (const char *prefix);
+
+#endif /* RE_TEST_H_ */
diff --git a/src/tls/Makefile-tls.am b/src/tls/Makefile-tls.am
new file mode 100644
index 0000000..2c5f075
--- /dev/null
+++ b/src/tls/Makefile-tls.am
@@ -0,0 +1,102 @@
+# -----------------------------------------------------------------------------
+# libcockpit-tls.a: code used in cockpit-tls, helpers, and tests
+
+noinst_LIBRARIES += libcockpit-tls.a
+
+libcockpit_tls_a_LIBS = \
+ libcockpit-tls.a \
+ $(libcockpit_common_nodeps_a_LIBS) \
+ $(gnutls_LIBS) \
+ -lpthread \
+ $(NULL)
+
+libcockpit_tls_a_SOURCES = \
+ src/tls/certificate.c \
+ src/tls/certificate.h \
+ src/tls/client-certificate.c \
+ src/tls/client-certificate.h \
+ src/tls/connection.c \
+ src/tls/connection.h \
+ src/tls/httpredirect.c \
+ src/tls/httpredirect.h \
+ src/tls/server.c \
+ src/tls/server.h \
+ src/tls/socket-io.c \
+ src/tls/socket-io.h \
+ src/tls/testing.h \
+ src/tls/utils.h \
+ $(NULL)
+
+# -----------------------------------------------------------------------------
+# cockpit-tls
+
+libexec_PROGRAMS += cockpit-tls
+cockpit_tls_LDADD = $(libcockpit_tls_a_LIBS) $(argp_LIBS)
+cockpit_tls_SOURCES = src/tls/main.c
+
+# -----------------------------------------------------------------------------
+# cockpit-certificate-ensure
+
+libexec_PROGRAMS += cockpit-certificate-ensure
+cockpit_certificate_ensure_LDADD = $(libcockpit_tls_a_LIBS)
+cockpit_certificate_ensure_SOURCES = src/tls/cockpit-certificate-ensure.c
+
+libexec_SCRIPTS += src/tls/cockpit-certificate-helper
+
+# -----------------------------------------------------------------------------
+# cockpit-wsinstance-factory
+
+libexec_PROGRAMS += cockpit-wsinstance-factory
+
+cockpit_wsinstance_factory_CPPFLAGS = \
+ $(libsystemd_CPPFLAGS) \
+ $(AM_CPPFLAGS)
+
+cockpit_wsinstance_factory_LDADD = \
+ $(libcockpit_tls_a_LIBS) \
+ $(libsystemd_LIBS) \
+ $(NULL)
+
+cockpit_wsinstance_factory_SOURCES = src/tls/wsinstance-factory.c
+
+# -----------------------------------------------------------------------------
+# Unit tests
+
+dist_check_DATA += \
+ src/tls/ca/alice-expired.pem \
+ src/tls/ca/alice.key \
+ src/tls/ca/alice.p12 \
+ src/tls/ca/alice.pem \
+ src/tls/ca/bob.key \
+ src/tls/ca/bob.p12 \
+ src/tls/ca/bob.pem \
+ src/tls/ca/ca.conf \
+ src/tls/ca/ca.key \
+ src/tls/ca/ca.pem \
+ src/tls/ca/generate.sh \
+ $(NULL)
+
+check_PROGRAMS += wsinstance-start
+wsinstance_start_LDADD = $(libcockpit_tls_a_LIBS)
+wsinstance_start_SOURCES = src/tls/wsinstance-start.c
+
+check_PROGRAMS += socket-activation-helper
+socket_activation_helper_LDADD = $(libcockpit_tls_a_LIBS)
+socket_activation_helper_SOURCES = src/tls/socket-activation-helper.c
+
+dist_TEST_SCRIPT += src/tls/test-socket-activation-helper.sh
+
+TEST_PROGRAM += test-cockpit-certificate-ensure
+test_cockpit_certificate_ensure_CPPFLAGS = $(TEST_CPP)
+test_cockpit_certificate_ensure_LDADD = $(libcockpit_tls_a_LIBS) $(TEST_LIBS)
+test_cockpit_certificate_ensure_SOURCES = src/tls/test-cockpit-certificate-ensure.c
+
+TEST_PROGRAM += test-tls-connection
+test_tls_connection_CPPFLAGS = $(TEST_CPP)
+test_tls_connection_LDADD = $(libcockpit_tls_a_LIBS) $(TEST_LIBS)
+test_tls_connection_SOURCES = src/tls/test-connection.c
+
+TEST_PROGRAM += test-tls-server
+test_tls_server_CPPFLAGS = $(TEST_CPP)
+test_tls_server_LDADD = $(libcockpit_tls_a_LIBS) $(TEST_LIBS)
+test_tls_server_SOURCES = src/tls/test-server.c
diff --git a/src/tls/ca/alice-expired.pem b/src/tls/ca/alice-expired.pem
new file mode 100644
index 0000000..83be0dd
--- /dev/null
+++ b/src/tls/ca/alice-expired.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDATCCAemgAwIBAgIBAzANBgkqhkiG9w0BAQsFADAuMRAwDgYDVQQKDAdDb2Nr
+cGl0MQ0wCwYDVQQLDAR0ZXN0MQswCQYDVQQDDAJDQTAeFw0yMDA3MTYxMDAwMDRa
+Fw0yMDA3MTcxMDAwMDRaMD4xDjAMBgNVBAMMBWFsaWNlMRcwFQYKCZImiZPyLGQB
+GRYHQ09DS1BJVDETMBEGCgmSJomT8ixkARkWA0xBTjCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAKCyJitfhk/xbb+Y9vOX5qbNu6ZKggvmDvT7NLv8PZoV
+zi8GYtDhCCBEj80tfAbFlf34Vk/TtLFEbuETFpeMKjgX15dTtvUHOYfUQ1pSOIAa
+rDPrf57rcbtxkTUHEgVg21RxzglTl17VQjl7JzS3F/pr/JifleGKxgTWDNAfpQJF
+tppLkanzSOfNDGTy2cxv82I20SSI5AhAXKz7h3NWnXCBdinFbhtlkAl/j9zlZJou
+dCht6qA92ZvbOjl6ta+DlykCg0fMuRiy4X96DRwCdCW8Bht/g1x0AlUXUI5MKH68
+xmnb2xeYwFrT5Tf0p20kVIQaBjOD9mmywYNnlfWu/nsCAwEAAaMaMBgwCQYDVR0T
+BAIwADALBgNVHQ8EBAMCA6gwDQYJKoZIhvcNAQELBQADggEBAAlw3KdjUyhGJoQ9
+E4rMFkZcEpY1T9sNvk1NKqzFgk9IV0uMvefs0r6Q+B790U2JbjcXfRXkfN/0+VEf
+k+bx2ReHXkNhRoiP2bNhSPelm9HXdCzEtSegat8o6ze5/Dp74ALLBxsPyZehO2h/
+uNL4d3tJ6Rl3rc4yv6Ap0sqSSi9nvjoAAJtJp9XuHTzRlWNeDYU+8Tc/bhJYk1jA
+KAmmrFpuMnE/JC3eJ63foM8oiww3dCYK4efq5UXMz844QswZjk9YTGSMal3/V01V
+Wy26r7soTOCPBRoPynF4A/JatWBBa/9TbYWO/+bJe6ctophkFC77jC8MJCz9r0JC
+3eTDbwI=
+-----END CERTIFICATE-----
diff --git a/src/tls/ca/alice.key b/src/tls/ca/alice.key
new file mode 100644
index 0000000..95a921c
--- /dev/null
+++ b/src/tls/ca/alice.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAoLImK1+GT/Ftv5j285fmps27pkqCC+YO9Ps0u/w9mhXOLwZi
+0OEIIESPzS18BsWV/fhWT9O0sURu4RMWl4wqOBfXl1O29Qc5h9RDWlI4gBqsM+t/
+nutxu3GRNQcSBWDbVHHOCVOXXtVCOXsnNLcX+mv8mJ+V4YrGBNYM0B+lAkW2mkuR
+qfNI580MZPLZzG/zYjbRJIjkCEBcrPuHc1adcIF2KcVuG2WQCX+P3OVkmi50KG3q
+oD3Zm9s6OXq1r4OXKQKDR8y5GLLhf3oNHAJ0JbwGG3+DXHQCVRdQjkwofrzGadvb
+F5jAWtPlN/SnbSRUhBoGM4P2abLBg2eV9a7+ewIDAQABAoIBAHC66QnuVlPIo1wQ
+m9VhD36evvA2PqrINl/nxiPGHRT2DRFyImo0IzY3wsWGDgbWH5Xt2+beNllRD5O3
+h1bDDm8RUr2nDacw7Uv7PVCkc1xy2bEGmR3MP5nmXp5bmmnJe7PurkJesMbnRjD6
+0xMpCpoznR0YftWdO7Ly0kRCnshLMO6cOnJ6jDAMobQ7MfzAMKXU8VDZT+zwDGQQ
+Fnkg4J0D8i2+cr/np/obySPP/jjtJY2s52DqY6ZVMtJuwllszfPZCpQYkrTID4KJ
+f+AcIRznLccEwR5zZ7wz46d6DmPODidbhzkxa9BwclEdvXz5EvZ2cuuCA7yNR9g+
+qLNR7wECgYEAyvwYw3xCJGr/bInJw5itrAB6uNnGOTatptZwyMPDQQHwQVKUJx4g
+FENJJAVuJ7Hg/rW9U7TMZ6HhFIxqJx6sEJTn18bgObQsEuZCZU9XYk0UiJ0TZUtk
+GWP9HPi4WDUHN1hkv61CyLraJlFvAL+DS6qefeHWAfN9XTl2MNyNLoECgYEAyqqM
+RSEB7a/l/33PODl1tSuz6hDNefpqkpROF7LWL96uQT4oLn4ftvomHyLCQjhQ8RvK
+DBffmq0i31h5aHvJml1Lxh559yy9Whc/vP6Qs+0zD10wJvrynrPdae2ldzgVT4P7
+yLHyBdjTbSk9jkB/3WoMbsn1ojGAfQrV0j87ZvsCgYBsDpgAOP3c9TjURuWpwnVx
+wy+RiB0GCB2ZWz7fIZen26hSnetQh7D8GHWvP1TMvNzRu132WaEW6g4wKi+4c5OV
+oC9rputmzItJ5FokJICYTABKWZQhsGixO5FbAuzfyBr3U48OmuWahh3rmB5Hf6wd
+c867pKWQlFaAcj6A9GgUAQKBgAb2AqlhL9rFHuY+oP5yYsudO6m/d/9HjZ+JPoFr
+4BV21Y9iHikypc44G1UYcYmqu4T8il0N+N0sMzVuqYTgM5V8vNyKMXA+9iYBBGxu
+ZfP4IrfVYJEGL40p6mH4CqXkpD697sj+66wtvV1TfEzFhF7LhK/NegVfI+WnmJ6H
+E3g/AoGAP8S97Bj5ApjiksMETgPxVY6cGuV3ydJrZW7aprspUjgsdjxW3+t1la6i
+iFNT3bAlNYxdye5vHAfvVFLGREDxT9aYhSNDr+8Sw3ffBbJHp1ydTJ1MwdNyKHMb
+70DxHvukL2r2X/ucZeSsAdeFf0KuonSKbyfdpe5LuuZsKKgyHrU=
+-----END RSA PRIVATE KEY-----
diff --git a/src/tls/ca/alice.p12 b/src/tls/ca/alice.p12
new file mode 100644
index 0000000..b048ec1
--- /dev/null
+++ b/src/tls/ca/alice.p12
Binary files differ
diff --git a/src/tls/ca/alice.pem b/src/tls/ca/alice.pem
new file mode 100644
index 0000000..3a35b0f
--- /dev/null
+++ b/src/tls/ca/alice.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDATCCAemgAwIBAgIBATANBgkqhkiG9w0BAQsFADAuMRAwDgYDVQQKDAdDb2Nr
+cGl0MQ0wCwYDVQQLDAR0ZXN0MQswCQYDVQQDDAJDQTAeFw0yMDA3MTYxMDAwMDRa
+Fw0zMDA3MTQxMDAwMDRaMD4xDjAMBgNVBAMMBWFsaWNlMRcwFQYKCZImiZPyLGQB
+GRYHQ09DS1BJVDETMBEGCgmSJomT8ixkARkWA0xBTjCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAKCyJitfhk/xbb+Y9vOX5qbNu6ZKggvmDvT7NLv8PZoV
+zi8GYtDhCCBEj80tfAbFlf34Vk/TtLFEbuETFpeMKjgX15dTtvUHOYfUQ1pSOIAa
+rDPrf57rcbtxkTUHEgVg21RxzglTl17VQjl7JzS3F/pr/JifleGKxgTWDNAfpQJF
+tppLkanzSOfNDGTy2cxv82I20SSI5AhAXKz7h3NWnXCBdinFbhtlkAl/j9zlZJou
+dCht6qA92ZvbOjl6ta+DlykCg0fMuRiy4X96DRwCdCW8Bht/g1x0AlUXUI5MKH68
+xmnb2xeYwFrT5Tf0p20kVIQaBjOD9mmywYNnlfWu/nsCAwEAAaMaMBgwCQYDVR0T
+BAIwADALBgNVHQ8EBAMCA6gwDQYJKoZIhvcNAQELBQADggEBAILcT4LCN1PxN/kB
+OLOQWeJyO2B63q0IRB7b6KwtI9nWP9FMB9Obhlw0EHFIvznbR4VmiXcZUmxfelJM
+nLU2yLClQOTUI+JVDE2ZrVJgJzxaf2JLgTAqNvH8ZRpaRtAI81lJkLROVRJuXldS
+aANbIpToVgPZTAA8wjNoUbVZsAHP4z+hE8ZOaJ4LvCU/+QTGyBdLcJTmZH3CWUcJ
+zUEGmTj+tgNZw1X0L6l9JfStwvHNn2Mirytw6L2r50Rqx5OyU8rcERGJOeQq1kj1
+P5xsqZEkzaV4Qz9CaJc9/OnOGBgJ+eJftzTY8ODWjIkI3yQfHLx1EWaea4DYeprK
+y4f1exM=
+-----END CERTIFICATE-----
diff --git a/src/tls/ca/bob.key b/src/tls/ca/bob.key
new file mode 100644
index 0000000..3109e0f
--- /dev/null
+++ b/src/tls/ca/bob.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpgIBAAKCAQEAuM6nsDPMX4eEVEaJeP8nS0hvMI0e11lGQZx5GONci6jUuH7H
+uyfgst34wvnutghtX3rRvzDO4j8m+VcUnY6UNenpYaJ5BqHosXuLwRKxjA3BvdKy
+e9Ewl3iiCY7/a/pYexalAZ0yyGYyDb7OmCTvGXYn4RVrY33hXU0i8yEeyzE3UPjS
+NwYXguSaxjj3xymVgEOYBAmsiae7xXdptkC6pz0cKwBaSwJLunJQvpABSkeh+rZV
+yvt6hCP3pF5ZqI0DT3o42Plot9M5hGOXgGs7olEu/sHaztIhhM8QDIQN1i776HqN
+xsgZtRIP5D2k1hfnXnZNjQFBqw9tFdJOzcl01wIDAQABAoIBAQCKXF37c6xtUR5n
+oi/GYPFgUai9ZspSXEpXUPjxAzgEb7BjOLf8jQyTndBi2ZA4xPJt65whUj9x7+oT
+/3g1xct82dWa/XMy3gFC78ZgFMjMnHgS0C5EuQdyQCTAl0jv5IxeC0Qag0DD/I84
+70PpcK16GJYe1FSENniFy/yAnqlZYgfIlSnJfu2HiJ/fuTFHwbmOiYIv3CLRt8BL
+qIZz5ndxIXoA0VFscx6gtT0u0X74ppSmCfIOS/cqGqSUGOrApA9CGlEfE2CdfzfE
+o7pll9KA8CPB9OKWhY36pCLtVCSpdTPXLweM46mimQ6DJETbHYXjdLqvds1iM1jz
+s3jrGp5hAoGBAN7Ek8jtlSIEMnbCTK6M05ZaV77b2w0P4Qi4WL/1u0zX/o5/FyTA
+wYAqnXzYvKzZXMQp2dPXp5rVgHCz539KuBVsRYHrAZ9afODmsDektVju5G+o5Koz
+7W4njHgvoLcvxubwcpmcKMGUuOz6otptBjZ6YX4TFjOH9Qgc8c9+6NcfAoGBANRg
+YA6QQmSag7qu8iKCZO7A5S00ANIN9aVlH6dhjwBOu8Q+oSpIL8/Z+41jgAzAnH5P
+7KELmWIzrLTOsGWs0eSphhxsPQNzhdbecQtuzM+B0FWuaVP7h8u/6/b4M3qPBcng
+jEZ07Fc/m4yDTphIxWK1FvA9q2srb9Gy2JBDCUNJAoGBAKPeNNwcMJT6Q2jq2t+X
+xlmWkW9baRJ9uMriWJ60k5geVynZQVwO0wiF0J2zDW+U4VGHe64CuE8EeNvu6v7P
+JwTTRXohNmtTdAM4jy4PYjtOWAnvUm4FjsV2IDWUy4OViJn/DD4FClIEJdhlMoC8
+rFJMygSk47L6cvwJ1rMNJxh/AoGBAIjDt5gpVlgrGQGzf8KxbgZCulMt1glva+2i
+/YtwzAJKsahjHK555jBYFCKtHN0ZfK3rEltzeMdAt8uH/xi6/j/e27qaCYCbeGky
+vmG6bLKEyoEi3Dl1FbIkWfSlSaF3USb8L9l/mmNlVupZ9Nckpdg2/hqf3DAbvNqx
+7byZLvHhAoGBANBm6KT1BHQzQD03ONn2Tl++P2p4uT3BEPYZLMdrhO0dYYpttCYW
+gh3qWs1PvM90sWjcDohjWrCipZbZY9qZs4HAkdOw+4bRiE9jEfpz20XcKzLQx0en
+BxVNri6FyYYRYJ1I9kixWHKx2do8iBK7odogJt7uEoz2+Ai+JfSufpQK
+-----END RSA PRIVATE KEY-----
diff --git a/src/tls/ca/bob.p12 b/src/tls/ca/bob.p12
new file mode 100644
index 0000000..7c954b1
--- /dev/null
+++ b/src/tls/ca/bob.p12
Binary files differ
diff --git a/src/tls/ca/bob.pem b/src/tls/ca/bob.pem
new file mode 100644
index 0000000..fdb5ff0
--- /dev/null
+++ b/src/tls/ca/bob.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIC/zCCAeegAwIBAgIBAjANBgkqhkiG9w0BAQsFADAuMRAwDgYDVQQKDAdDb2Nr
+cGl0MQ0wCwYDVQQLDAR0ZXN0MQswCQYDVQQDDAJDQTAeFw0yMDA3MTYxMDAwMDRa
+Fw0zMDA3MTQxMDAwMDRaMDwxDDAKBgNVBAMMA2JvYjEXMBUGCgmSJomT8ixkARkW
+B0NPQ0tQSVQxEzARBgoJkiaJk/IsZAEZFgNMQU4wggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQC4zqewM8xfh4RURol4/ydLSG8wjR7XWUZBnHkY41yLqNS4
+fse7J+Cy3fjC+e62CG1fetG/MM7iPyb5VxSdjpQ16elhonkGoeixe4vBErGMDcG9
+0rJ70TCXeKIJjv9r+lh7FqUBnTLIZjINvs6YJO8ZdifhFWtjfeFdTSLzIR7LMTdQ
++NI3BheC5JrGOPfHKZWAQ5gECayJp7vFd2m2QLqnPRwrAFpLAku6clC+kAFKR6H6
+tlXK+3qEI/ekXlmojQNPejjY+Wi30zmEY5eAazuiUS7+wdrO0iGEzxAMhA3WLvvo
+eo3GyBm1Eg/kPaTWF+dedk2NAUGrD20V0k7NyXTXAgMBAAGjGjAYMAkGA1UdEwQC
+MAAwCwYDVR0PBAQDAgOoMA0GCSqGSIb3DQEBCwUAA4IBAQBpOAwzVpw4Ox1V1vZj
+Jq7c81/nZljW6ntsmmE/IBVmBythVEpwzNyR8Rr2vgEAx8Scjt/gHmrJqzQtpdGi
+m8/Oo6PRpa+Wvmyu7wPG8ekcDLdFfDug4JN168saDF6pNsc+Ylg0TojZr50HVvlz
+50pgsr5nBii00fxKx7sh58M04w/7W7vYItNmmBdOELqKHYZqwBC98Q831bGB3ryY
+a6FQD6k0kHlXLp3wozlc4bRN+8NclWfH9X8D7mbkBmc5BPrvnbxKJDVwN6AqvBxl
+CQX5zIxoHrnhMyPH3wRSDX0KkLlAaZ2bS8oYbT+rNvyj3LfQ6NmaUqaB3IrzyMNk
+Ez8c
+-----END CERTIFICATE-----
diff --git a/src/tls/ca/ca.conf b/src/tls/ca/ca.conf
new file mode 100644
index 0000000..43669d3
--- /dev/null
+++ b/src/tls/ca/ca.conf
@@ -0,0 +1,30 @@
+[ ca ]
+default_ca = cockpit_lan
+
+[ cockpit_lan ]
+certificate = ca.pem
+private_key = ca.key
+database = index.txt
+new_certs_dir = certs
+serial = serial
+default_md = sha256
+policy = cockpit_lan_policy
+
+[ req ]
+default_bits = 2048
+default_keyfile = ca.key
+distinguished_name = root_ca
+x509_extensions = root_ca
+req_extensions = usr_cert
+
+[ cockpit_lan_policy ]
+commonName = supplied
+DC = supplied
+
+[ root_ca ]
+basicConstraints = CA:true
+keyUsage = critical, digitalSignature, keyCertSign, cRLSign
+
+[ usr_cert ]
+basicConstraints=CA:FALSE
+keyUsage=digitalSignature,keyEncipherment,keyAgreement
diff --git a/src/tls/ca/ca.key b/src/tls/ca/ca.key
new file mode 100644
index 0000000..abbec84
--- /dev/null
+++ b/src/tls/ca/ca.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDNj1H4uMEyMv96
+wW/0HJRzlr0znpdKVUKN65SSqr89OptxfwOa8I53nJcpinpcQSFtYHp9MUbttqVc
+wni88AmCp3W1mazdMiaylSLwVPcsD86SI5YtflKTztjnFMEcZKySvO86uEkYufWz
+DgpifzYPGN+tJJMez83SQZm38KDKbC5T0xHiMaLt5KZ+qWi/AjVYqR+21Km3GkUc
+JPI4pz//oqKdSIETI4NYMH6RF+ft1zRJDvWUqupF0zTP/S3/zNnIpaC0ljiQWAvJ
+682fj+YhzSHrFGVIim223bHooG1oUdd7hOf+naPrmKgvByaV+BweU2CX1oTy28qp
+VzLsh52JAgMBAAECggEAX44NG+K+pFhCnMzI/yA9/hGHeDRzvbEulAx770YCSjMx
+SSwy/9ZtU7ruJfhicvov0Ml1hzbd8ujhq+IXADVRJnQz6qw6SETk6kUbjb7he5EJ
+hLOYPyOmvWD9QUZLkqhV9IxxZMl/6UegqJCRUUIzXgdBCxqibUNKOUAXdgMcq5uS
+TxLo4q0iw5DzBXEeEO54+v/JTctltklAmbR8Q/MeeIijXirKMgEHCKxJMOGkOhNr
+OF/mwNilr4M3XYOIuwfd5XKV2HJQsxTL/ifI1SGlnnOHYw9H4OfLlfiKOKkBT2k1
+QbBskgTTmQ1ESp7JNKzG2jh37pd7AZ5N0N6fJEv+RQKBgQD5rcg9Rt9wHUq3M99t
+Ovq9eDdhINlT08TmvMEGLtHFkE4BdKcI4gJs/wW99k6Oz1KttSq44hlZZSIY1qTX
+wmh89GjM+DoYoCcF7TaXtISzASMy/trMSBmbQzIYrYV1dX4jaWLSoanQ5h3F1+iv
+xhtucRrZQU8tTsFtUCtOxZ3FKwKBgQDSw5gYWPQOFmQvBJJXZQi4l+TfYvpBcite
++DdtjTANNMKZ/hWn6i4cnHKkU6wwQeOlaQ01n88/nleGZr7k/JdAl5AzNhxei9q/
+b7Am7YB3hPyr4urLRpv1mpnKcjGurbUNPsgKzhrkukx6uzDRYovjAK100S04/oAy
+rzT7twZ2GwKBgQDaZKzrFIwq+CQ7nY8Ib3eVtYrTVbVjgrAPl/lLHDb3FaN12ICm
+33J5vpRaNhE/D4/lS7uj3nmH3VN24IFuDKny6qKk71d55NRddcGz7v3Hye3tFEl0
+OHjVe3Bdb6J2PKBp/yumzCvLAw312Ua6QZILB7oRYPQN0H6WzHIlQ+wd4wKBgQC7
+21a/rNmrjM2zEtIVT5qcXdbEPYZtuwNpXLiaztNbkgE6HLzUs+hx3Aha67F4OeJc
+VuxNNaoWPF4rD31qwLFYAz+mRb63jtRk+Xn62Io+/avw0QsSMFdmbGxTg8FAvte3
+fJP0/Yv+/waYBi0XFE03Ai2ZB7DVMhK2GnCRQ6tgrQKBgQDWiZiHpSJwYHqqfj1G
+p4f1KX7valjkF76VtAIALvaGyeyQ6qfZlqLqZWjGbJkj6UyXPC+aUvcDXZPADlaf
+IGBUvnhwShjOIk9pv4zYdKe811tKyAQexn9LaO3z1/gAmkPzAVHO+fq1v5f+gUc3
+ov8RSG7uCzZb2jfwqlTgOJyqHA==
+-----END PRIVATE KEY-----
diff --git a/src/tls/ca/ca.pem b/src/tls/ca/ca.pem
new file mode 100644
index 0000000..60b484a
--- /dev/null
+++ b/src/tls/ca/ca.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDCjCCAfKgAwIBAgIUfKVWGSC8QTRbJMyRxcifIe+rwLAwDQYJKoZIhvcNAQEL
+BQAwLjEQMA4GA1UECgwHQ29ja3BpdDENMAsGA1UECwwEdGVzdDELMAkGA1UEAwwC
+Q0EwHhcNMjAwNzE2MTAwMDA0WhcNMzAwNzE0MTAwMDA0WjAuMRAwDgYDVQQKDAdD
+b2NrcGl0MQ0wCwYDVQQLDAR0ZXN0MQswCQYDVQQDDAJDQTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAM2PUfi4wTIy/3rBb/QclHOWvTOel0pVQo3rlJKq
+vz06m3F/A5rwjneclymKelxBIW1gen0xRu22pVzCeLzwCYKndbWZrN0yJrKVIvBU
+9ywPzpIjli1+UpPO2OcUwRxkrJK87zq4SRi59bMOCmJ/Ng8Y360kkx7PzdJBmbfw
+oMpsLlPTEeIxou3kpn6paL8CNVipH7bUqbcaRRwk8jinP/+iop1IgRMjg1gwfpEX
+5+3XNEkO9ZSq6kXTNM/9Lf/M2ciloLSWOJBYC8nrzZ+P5iHNIesUZUiKbbbdseig
+bWhR13uE5/6do+uYqC8HJpX4HB5TYJfWhPLbyqlXMuyHnYkCAwEAAaMgMB4wDAYD
+VR0TBAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEBAL8q
+G73FziVKmeMEztswD8tGpHLuPTAm6s/40mi7I3my5+IcrqhGd4+8tw07Qbc+Lkg/
+yEujQ9/QECsXTawAjj7kEfvjROaXA59o+vGqgUR5keaNF7dM87JcfVsjLx5UGx6P
+UvX3+H2x4l++1yn1yJZ74cuT681gbdnkJzcG9yInkJyXBO4q7Yq52uTfH4Tyy8Nw
+McBzCI3TBNZ0DdS7UghDrDaQrUmdjP/ravamLAnD73oGQ9L9g6O0KzogRNXZ1Czx
+5R+Ofrxno6P2rHi2GMv4hsFPMaocs0JhhnJr4Kh6A0jTXxnLwi9IFEN4ojn7JeaU
+wsXT0A2hJugWn2oDSGw=
+-----END CERTIFICATE-----
diff --git a/src/tls/ca/generate.sh b/src/tls/ca/generate.sh
new file mode 100755
index 0000000..90da2f9
--- /dev/null
+++ b/src/tls/ca/generate.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+# Generate CA and certificates for testing
+set -eux
+openssl req -config ca.conf -x509 -newkey rsa:2048 -out ca.pem -subj '/O=Cockpit/OU=test/CN=CA/' -nodes -days 3650
+mkdir certs
+touch index.txt
+echo 01 > serial
+
+for user in alice bob; do
+ openssl genrsa -out ${user}.key 2048
+
+ openssl req -new -key ${user}.key -out ${user}.csr -config ca.conf -subj "/CN=${user}/DC=COCKPIT/DC=LAN/"
+ openssl req -in ${user}.csr -text
+ openssl ca -batch -config ca.conf -in ${user}.csr -days 3650 -notext -extensions usr_cert -out ${user}.pem -subj "/CN=${user}/DC=COCKPIT/DC=LAN/"
+ # for browser or smart card import
+ openssl pkcs12 -export -password pass:foo -in ${user}.pem -inkey ${user}.key -out ${user}.p12
+done
+
+# reset so that we can re-build cert with different lifetime
+rm index.txt*
+touch index.txt
+# there is no way to generate an immediately expired cert with "openssl ca", so this has to age for a day to really be expired; or
+# temporarily set back your clock one day
+openssl ca -batch -config ca.conf -in alice.csr -days 1 -notext -extensions usr_cert -out alice-expired.pem -subj "/CN=alice/DC=COCKPIT/DC=LAN/"
+
+rm -r certs
+rm index.txt* serial* *.csr
diff --git a/src/tls/certificate.c b/src/tls/certificate.c
new file mode 100644
index 0000000..6005386
--- /dev/null
+++ b/src/tls/certificate.c
@@ -0,0 +1,96 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "certificate.h"
+
+#include <assert.h>
+#include <err.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <gnutls/x509.h>
+
+#include "common/cockpitmemory.h"
+#include "common/cockpitwebcertificate.h"
+
+#include "utils.h"
+
+struct _Certificate
+{
+ gnutls_certificate_credentials_t creds;
+ int ref_count;
+};
+
+static Certificate *
+certificate_new (gnutls_certificate_credentials_t creds)
+{
+ Certificate *self = mallocx (sizeof (Certificate));
+ self->creds = creds;
+ self->ref_count = 1;
+
+ return self;
+}
+
+Certificate *
+certificate_ref (Certificate *self)
+{
+ self->ref_count++;
+
+ return self;
+}
+
+void
+certificate_unref (Certificate *self)
+{
+ if (--self->ref_count == 0)
+ {
+ gnutls_certificate_free_credentials (self->creds);
+ free (self);
+ }
+}
+
+gnutls_certificate_credentials_t
+certificate_get_credentials (Certificate *self)
+{
+ return self->creds;
+}
+
+Certificate *
+certificate_load (const char *certificate_filename,
+ const char *key_filename)
+{
+ gnutls_certificate_credentials_t creds;
+ int ret;
+
+ debug (SERVER, "Using certificate %s", certificate_filename);
+
+ ret = gnutls_certificate_allocate_credentials (&creds);
+ assert (ret == GNUTLS_E_SUCCESS);
+
+ ret = gnutls_certificate_set_x509_key_file (creds,
+ certificate_filename, key_filename,
+ GNUTLS_X509_FMT_PEM);
+
+ if (ret != GNUTLS_E_SUCCESS)
+ errx (EXIT_FAILURE, "Failed to initialize server certificate: %s", gnutls_strerror (ret));
+
+ return certificate_new (creds);
+}
diff --git a/src/tls/certificate.h b/src/tls/certificate.h
new file mode 100644
index 0000000..d57c6c2
--- /dev/null
+++ b/src/tls/certificate.h
@@ -0,0 +1,37 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gnutls/gnutls.h>
+
+typedef struct _Certificate Certificate;
+
+Certificate *
+certificate_ref (Certificate *self);
+
+void
+certificate_unref (Certificate *self);
+
+gnutls_certificate_credentials_t
+certificate_get_credentials (Certificate *self);
+
+Certificate *
+certificate_load (const char *certificate_filename,
+ const char *key_filename);
diff --git a/src/tls/client-certificate.c b/src/tls/client-certificate.c
new file mode 100644
index 0000000..513ffbb
--- /dev/null
+++ b/src/tls/client-certificate.c
@@ -0,0 +1,381 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * This file deals with authentication based on client certificates. It
+ * contains a peer certificate verification function and a series of
+ * functions for dealing with the session-scoped client certificate file
+ * we store on the disk.
+ *
+ * This file is responsible for determining the cockpit-ws instance
+ * identifiers: the client certificate files are limited in scope to a
+ * particular cgroup, which is determined based on the instance
+ * identifier: that logic is also in this file.
+ *
+ * Higher layers (cockpit-tls → cockpit-ws → cockpit-session) are
+ * responsible for transporting the client certificate filename from
+ * here to the counterpart of this file which performs the actual
+ * checks: src/session/client-certificate.c. The filename is
+ * required information for authentication, but it's not sufficient:
+ * the cgroup of the wsinstance must also match the one found in the
+ * client certificate file.
+ */
+
+#include "config.h"
+
+#include "client-certificate.h"
+
+#include "common/cockpitmemory.h"
+#include "common/cockpithex.h"
+#include "tls/utils.h" /* for SHA256_NIL */
+
+#include <assert.h>
+#include <err.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/random.h>
+#include <unistd.h>
+
+
+/**
+ * client_certificate_verify: Custom client certificate validation function
+ *
+ * cockpit-tls ignores CA/trusted owner and leaves that to e. g. sssd. But
+ * validate the other properties such as expiry, unsafe algorithms, etc.
+ * This combination cannot be done with gnutls_session_set_verify_cert().
+ */
+int
+client_certificate_verify (gnutls_session_t session)
+{
+ unsigned status;
+ int ret;
+
+ do
+ ret = gnutls_certificate_verify_peers2 (session, &status);
+ while (ret == GNUTLS_E_INTERRUPTED);
+
+ if (ret == 0)
+ {
+ /* ignore CA/trusted owner and leave that to e. g. sssd */
+ status &= ~(GNUTLS_CERT_INVALID | GNUTLS_CERT_SIGNER_NOT_FOUND | GNUTLS_CERT_SIGNER_NOT_CA);
+ if (status != 0)
+ {
+ gnutls_datum_t msg;
+ ret = gnutls_certificate_verification_status_print (status, gnutls_certificate_type_get (session), &msg, 0);
+ if (ret != GNUTLS_E_SUCCESS)
+ errx (EXIT_FAILURE, "Failed to print verification status: %s", gnutls_strerror (ret));
+ warnx ("Invalid TLS peer certificate: %s", msg.data);
+ gnutls_free (msg.data);
+#ifdef GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR
+ return GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR;
+#else /* fallback for GnuTLS < 3.4.4 */
+ return GNUTLS_E_CERTIFICATE_ERROR;
+#endif
+ }
+ }
+ else if (ret != GNUTLS_E_NO_CERTIFICATE_FOUND)
+ {
+ warnx ("Verifying TLS peer failed: %s", gnutls_strerror (ret));
+ return ret;
+ }
+
+ return GNUTLS_E_SUCCESS;
+}
+
+/**
+ * client_certificate_get_wsinstance:
+ * @certificate: the certificate presented by the peer
+ *
+ * Determines the correct cockpit-ws instance for handling connections
+ * for this @certificate (must be non-%NULL).
+ *
+ * Currently, the full SHA256 fingerprint of the peer certificate is
+ * used. This is a pure design decision that nothing else depends on,
+ * and it could be changed to something else.
+ *
+ * This function never fails. Any internal failure will abort the
+ * program.
+ *
+ * The return value of this function needs to be free()d.
+ */
+static char *
+client_certificate_get_wsinstance (const gnutls_datum_t *certificate)
+{
+ unsigned char digest_data[256 / 8];
+ size_t digest_size = sizeof digest_data;
+ int r = gnutls_fingerprint (GNUTLS_DIG_SHA256, certificate, digest_data, &digest_size);
+ if (r != GNUTLS_E_SUCCESS)
+ errx (EXIT_FAILURE, "Could not generate fingerprint of peer certificate: %s",
+ gnutls_strerror (r));
+ assert (digest_size == sizeof digest_data);
+
+ return cockpit_hex_encode (digest_data, digest_size);
+}
+
+/**
+ * client_certificate_random_filename:
+ *
+ * Generates a (high quality) random hexadecimal string to use as a
+ * client certificate filename.
+ *
+ * Currently, the filename will be 64 characters in length. This is a
+ * pure design decision that nothing else depends on, and it could be
+ * changed to something else.
+ *
+ * This function never fails. Any internal failure will abort the
+ * program.
+ *
+ * The return value of this function needs to be free()d.
+ */
+static char *
+client_certificate_random_filename (void)
+{
+ /* This is guaranteed to succeed, but we check it anyway. */
+ unsigned char random_data[256 / 8];
+ ssize_t s = getrandom (random_data, sizeof random_data, 0);
+ if (s != sizeof random_data)
+ err (EXIT_FAILURE, "Could not read random data from the kernel");
+
+ return cockpit_hex_encode (random_data, sizeof random_data);
+}
+
+/**
+ * client_certificate_write_data:
+ * @fd: a writable file descriptor
+ * @data: data to write
+ * @size: the length of @data
+ * @description: description of @data, added to error messages
+ *
+ * Writes @data to @fd.
+ *
+ * There's no EINTR handling or support for partial writes. Any kind of
+ * result other than a complete success on the first try is treated as
+ * an error — this is tmpfs, after all.
+ *
+ * In case of failure, a message will be written to stderr.
+ */
+static bool
+client_certificate_write_data (int fd,
+ const void *data,
+ size_t size,
+ const char *description)
+{
+ ssize_t s = write (fd, data, size);
+
+ if (s == size)
+ return true;
+
+ if (s == -1)
+ warn ("Couldn't write %s to certificate file", description);
+ else
+ warnx ("Partial write of %s to certificate file: %zu of %zu", description, s, size);
+
+ return false;
+}
+
+static bool
+client_certificate_write_cgroup_header (int fd,
+ const char *wsinstance)
+{
+ char header[200];
+ int s = snprintf (header, sizeof header,
+ "0::/system.slice/system-cockpithttps.slice/cockpit-wsinstance-https@%s.service\n",
+ wsinstance);
+ assert (s < sizeof header);
+
+ return client_certificate_write_data (fd, header, s, "cgroup header");
+}
+
+static bool
+client_certificate_write_pem (int fd,
+ const gnutls_datum_t *der)
+{
+ gnutls_datum_t pem = { NULL, 0 };
+ int r = gnutls_pem_base64_encode2 ("CERTIFICATE", der, &pem);
+ if (r != GNUTLS_E_SUCCESS)
+ {
+ warnx ("Couldn't base64 encode certificate: %s", gnutls_strerror (r));
+ return false;
+ }
+
+ bool result = client_certificate_write_data (fd, pem.data, pem.size, "PEM data");
+
+ /* Make sure we get the function version and not the weird
+ * side-effecting macro version.
+ */
+ (gnutls_free) (pem.data);
+
+ return result;
+}
+
+/**
+ * client_certificate_link_fd_to_random_name:
+ * @dirfd: the directory to link the file
+ * @fd: the file created with O_TMPFILE
+ * @out_filename: the filename that was chosen
+ *
+ * Links the O_TMPFILE referred to by @fd to a random filename in
+ * @dirfd.
+ *
+ * On success, %true is returned and @out_filename is set to the
+ * filename that was used. It must be free()d.
+ *
+ * On failure, %false is returned and a message will have been logged.
+ */
+static bool
+client_certificate_link_fd_to_random_name (int dirfd,
+ int fd,
+ char **out_filename)
+{
+ char *filename = client_certificate_random_filename ();
+
+ /* "the usual tricks" — see openat(2) and linkat(2) */
+ char path[PATH_MAX];
+ snprintf(path, PATH_MAX, "/proc/self/fd/%d", fd);
+
+ if (linkat (AT_FDCWD, path, dirfd, filename, AT_SYMLINK_FOLLOW) == 0)
+ {
+ *out_filename = filename;
+ return true;
+ }
+ else
+ {
+ warn ("Unable to link client certificate file to /run/cockpit/tls/%s", filename);
+ free (filename);
+ return false;
+ }
+}
+
+/**
+ * client_certificate_create_tmpfile:
+ * @dirfd: the directory that will eventually contain the file
+ * @out_fd: a write-mode fd for the created file
+ */
+static bool
+client_certificate_create_tmpfile (int dirfd,
+ int *out_fd)
+{
+ int fd = openat (dirfd, ".", O_TMPFILE | O_WRONLY, 0400);
+ if (fd == -1)
+ {
+ warn ("Couldn't create temporary file for client certificate");
+ return false;
+ }
+
+ *out_fd = fd;
+ return true;
+}
+
+/**
+ * client_certificate_accept:
+ * @session: a post-handshake gnutls session
+ * @dirfd: the directory for session-scoped client certificates
+ * @out_wsinstance: the instance of cockpit-ws to connect to
+ * @out_filename: the filename where the client certificate was written
+ *
+ * Called immediately after completing the handshake with an incoming
+ * HTTPS connection.
+ *
+ * If no client certificate was presented, this function writes %NULL to
+ * @out_filename, but still provides a (hard-coded) instance identifier
+ * to @out_wsinstance.
+ *
+ * If a client certificate was presented, the @out_wsinstance will
+ * correspond to the SHA256 of the peer certificate. In this case, a
+ * file with a random filename will be written to the directory
+ * referenced by @dirfd. This file will contain the expected cgroup of
+ * the cockpit-ws instance in question, plus the client certificate.
+ * That data is interpreted by the counterpart to this code, living in
+ * src/ws/cockpit-session-client-certificate.c
+ *
+ * In any case, %true will be returned in case of success, and %false
+ * will be returned in case of an error. In case of success, any values
+ * returned in @out_wsinstance or @out_filename need to be free()d. In
+ * case of error, the connection should be terminated: a message will
+ * already have been logged.
+ */
+bool
+client_certificate_accept (gnutls_session_t session,
+ int dirfd,
+ char **out_wsinstance,
+ char **out_filename)
+{
+ const gnutls_datum_t *peer_certificate = gnutls_certificate_get_peers (session, NULL);
+
+ if (peer_certificate == NULL)
+ {
+ *out_wsinstance = strdupx (SHA256_NIL);
+ *out_filename = NULL;
+
+ return true;
+ }
+
+ char *wsinstance = client_certificate_get_wsinstance (peer_certificate);
+
+ int fd = -1;
+ bool success =
+ client_certificate_create_tmpfile (dirfd, &fd) &&
+ client_certificate_write_cgroup_header (fd, wsinstance) &&
+ client_certificate_write_pem (fd, peer_certificate) &&
+ client_certificate_link_fd_to_random_name (dirfd, fd, out_filename);
+
+ if (fd != -1)
+ close (fd);
+
+ if (success)
+ *out_wsinstance = wsinstance;
+ else
+ free (wsinstance);
+
+ if (!success)
+ warnx ("Disconnecting client due to above failure.");
+
+ return success;
+}
+
+/**
+ * client_certificate_unlink_and_free:
+ * @dirfd: the directory for session-scoped client certificates
+ * @inout_filename: the name of the client certificate file
+ *
+ * Glorified wrapper around unlinkat().
+ *
+ * Frees @inout_filename.
+ *
+ * If the operation fails, the program will be aborted.
+ */
+void
+client_certificate_unlink_and_free (int dirfd,
+ char *filename)
+{
+ if (unlinkat (dirfd, filename, 0) != 0)
+ {
+ /* We can't leave stale certificate files hanging around after
+ * they should have been deleted, and we're really not expecting a
+ * failure here, so let's abort the entire service. This should
+ * cause any running -ws instances to be terminated, and will
+ * cause systemd to delete the entire runtime directory as well.
+ */
+ err (EXIT_FAILURE, "Failed to unlink client certificate file %s", filename);
+ }
+
+ free (filename);
+}
diff --git a/src/tls/client-certificate.h b/src/tls/client-certificate.h
new file mode 100644
index 0000000..d14edf4
--- /dev/null
+++ b/src/tls/client-certificate.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gnutls/gnutls.h>
+#include <stdbool.h>
+
+int
+client_certificate_verify (gnutls_session_t session);
+
+bool
+client_certificate_accept (gnutls_session_t session,
+ int dirfd,
+ char **out_wsinstance,
+ char **out_filename);
+
+void
+client_certificate_unlink_and_free (int dirfd,
+ char *filename);
diff --git a/src/tls/cockpit-certificate-ensure.c b/src/tls/cockpit-certificate-ensure.c
new file mode 100644
index 0000000..529d4da
--- /dev/null
+++ b/src/tls/cockpit-certificate-ensure.c
@@ -0,0 +1,442 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <gnutls/gnutls.h>
+#include <gnutls/x509.h>
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <spawn.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <common/cockpitwebcertificate.h>
+#include <common/cockpitmemory.h>
+
+#include "certificate.h"
+#include "utils.h"
+
+#define COCKPIT_CERTIFICATE_HELPER LIBEXECDIR "/cockpit-certificate-helper"
+
+// Cross-reference with cockpit-certificate-helper.in
+#define COCKPIT_SELFSIGNED_FILENAME "/cockpit/ws-certs.d/0-self-signed.cert"
+#define COCKPIT_SELFSIGNED_PATH PACKAGE_SYSCONF_DIR COCKPIT_SELFSIGNED_FILENAME
+
+// Renew certificates with less than 30 days validity
+#define EXPIRY_THRESHOLD (30 * 24 * 60 * 60)
+
+// We used to issue 100 year certificates, but browsers no longer accept
+// those. Make sure we reissue those as well: anything with an expiry
+// of more than ~5 years from now was surely generated by the old code.
+#define MAX_EXPIRY (5 * 365 * 24 * 60 * 60)
+
+// We tolerate the deprecated merged cert/key files only for cockpit-tls.
+static bool tolerate_merged_cert_key;
+
+typedef struct
+{
+ char *certificate_filename;
+ gnutls_datum_t certificate;
+ char *key_filename;
+ gnutls_datum_t key;
+
+ char *filename_for_errors;
+} CertificateKeyPair;
+
+static void
+read_file (const char *filename,
+ gnutls_datum_t *result)
+{
+ int fd = open (filename, O_RDONLY);
+ if (fd == -1)
+ err (EXIT_FAILURE, "open: %s", filename);
+
+ struct stat buf;
+ if (fstat (fd, &buf) != 0)
+ err (EXIT_FAILURE, "fstat: %s", filename);
+
+ if (!S_ISREG (buf.st_mode))
+ errx (EXIT_FAILURE, "%s: not a regular file", filename);
+
+ result->size = buf.st_size;
+ result->data = mallocx (result->size + 1);
+
+ ssize_t s = read (fd, result->data, result->size);
+ if (s == -1)
+ err (EXIT_FAILURE, "read: %s", filename);
+ if (s != result->size)
+ errx (EXIT_FAILURE, "read: %s: got %zu bytes, expecting %zu",
+ filename, s, (size_t) result->size);
+
+ result->data[s] = '\0';
+
+ close (fd);
+}
+
+static void
+write_file (int dirfd,
+ const char *dirfd_filename,
+ const char *filename,
+ const gnutls_datum_t *data,
+ uid_t uid,
+ gid_t gid)
+{
+ /* Just open the file directly: it doesn't exist yet and nobody will
+ * look at it until after we're done here.
+ */
+ int fd = openat (dirfd, filename, O_CREAT | O_EXCL | O_WRONLY, 0400);
+
+ if (fd == -1)
+ err (EXIT_FAILURE, "%s/%s: creat", dirfd_filename, filename);
+
+ size_t s = write (fd, data->data, data->size);
+ if (s == -1)
+ err (EXIT_FAILURE, "%s/%s: write", dirfd_filename, filename);
+ if (s != data->size)
+ errx (EXIT_FAILURE, "%s/%s: write: wrote %zu bytes, expecting %zu",
+ dirfd_filename, filename, s, (size_t) data->size);
+
+ /* This is actually making the file more accessible, to do it last */
+ if (fchown (fd, uid, gid) != 0)
+ err (EXIT_FAILURE, "%s/%s: fchown", dirfd_filename, filename);
+
+ close (fd);
+}
+
+static bool
+is_selfsigned (const char *certificate_filename)
+{
+ return strstr (certificate_filename, COCKPIT_SELFSIGNED_FILENAME) != NULL;
+}
+
+static bool
+check_expiry (gnutls_certificate_credentials_t creds,
+ const char *certificate_filename)
+{
+ gnutls_x509_crt_t *crt_list;
+ unsigned int crt_list_size;
+
+ int ret = gnutls_certificate_get_x509_crt (creds, 0, &crt_list, &crt_list_size);
+ assert (ret == GNUTLS_E_SUCCESS);
+
+ if (crt_list_size != 1)
+ errx (EXIT_FAILURE, "unable to check expiry of chained certificates");
+
+ time_t expires = gnutls_x509_crt_get_expiration_time (crt_list[0]);
+ gnutls_x509_crt_deinit (crt_list[0]);
+ gnutls_free (crt_list);
+
+ debug (ENSURE, "Certificate %s expires %ld", certificate_filename, (long) expires);
+
+ time_t now = time (NULL);
+ if (expires > now + MAX_EXPIRY)
+ {
+ debug (ENSURE, "Certificate %s expires %ld, too far in the future",
+ certificate_filename, (long) expires);
+
+ return true;
+ }
+
+ time_t last_valid_expiry = now + EXPIRY_THRESHOLD;
+ if (expires < last_valid_expiry)
+ {
+ debug (ENSURE, "Certificate %s expires %ld, which is before %ld",
+ certificate_filename, (long) expires, (long) last_valid_expiry);
+
+ return true;
+ }
+
+ debug (ENSURE, "Certificate %s expires %ld, which is after %ld",
+ certificate_filename, (long) expires, (long) last_valid_expiry);
+
+ return false;
+}
+
+static void
+certificate_and_key_clear (CertificateKeyPair *self)
+{
+ /* gnutls_datum_free is side-effecting and sets ->data = NULL */
+ free (self->certificate.data);
+ self->certificate.data = NULL;
+ self->certificate.size = 0;
+
+ free (self->certificate_filename);
+ self->certificate_filename = NULL;
+
+ free (self->key.data);
+ self->key.data = NULL;
+ self->key.size = 0;
+
+ free (self->key_filename);
+ self->key_filename = NULL;
+
+ free (self->filename_for_errors);
+ self->filename_for_errors = NULL;
+}
+
+static void
+certificate_and_key_write (const CertificateKeyPair *self,
+ const char *directory)
+{
+ int dirfd = open (directory, O_PATH | O_DIRECTORY | O_NOFOLLOW);
+ if (dirfd == -1)
+ err (EXIT_FAILURE, "open: %s", directory);
+
+ struct stat buf;
+ if (fstat (dirfd, &buf) != 0)
+ err (EXIT_FAILURE, "fstat: %s", directory);
+
+ int r = mkdirat (dirfd, "server", 0700);
+ if (r != 0)
+ err (EXIT_FAILURE, "mkdir: %s/%s", directory, "server");
+
+ /* fchown() won't accept file descriptors opened O_PATH */
+ int fd = openat (dirfd, "server", O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
+ if (fd == -1)
+ err (EXIT_FAILURE, "open: %s", directory);
+
+ /* Copy the owner/group from the parent directory */
+ if (fchown (fd, buf.st_uid, buf.st_gid) != 0)
+ err (EXIT_FAILURE, "%s: fchown", directory);
+
+ if (symlinkat (self->certificate_filename, fd, "cert.source") != 0)
+ err (EXIT_FAILURE, "%s/%s: symlinkat", directory, "certificate.source");
+
+ if (symlinkat (self->key_filename, fd, "key.source") != 0)
+ err (EXIT_FAILURE, "%s/%s: symlinkat", directory, "key.source");
+
+ write_file (fd, directory, "cert", &self->certificate, buf.st_uid, buf.st_gid);
+
+ write_file (fd, directory, "key", &self->key, buf.st_uid, buf.st_gid);
+
+ close (dirfd);
+ close (fd);
+}
+
+static bool
+certificate_and_key_split (CertificateKeyPair *self)
+{
+ const char *pairs[][2] = {
+ { "-----BEGIN RSA PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----" },
+ /* this is slightly asymmetrical -- parameters and private key occur in the same file */
+ { "-----BEGIN EC PARAMETERS-----", "-----END EC PRIVATE KEY-----" },
+ { "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----" },
+ };
+
+ for (int i = 0; i < N_ELEMENTS (pairs); i++)
+ {
+ char *start = strstr ((const char *) self->certificate.data, pairs[i][0]);
+
+ if (!start)
+ continue;
+
+ char *end = strstr ((const char *) self->certificate.data, pairs[i][1]);
+ if (!end)
+ errx (EXIT_FAILURE, "%s: found '%s' but not '%s'",
+ self->certificate_filename, pairs[i][0], pairs[i][1]);
+
+ /* Consume the footer and any extra newlines */
+ end += strlen (pairs[i][1]);
+ while (*end == '\r' || *end == '\n')
+ end++;
+
+ /* Cut out the private key */
+ self->key.data = (unsigned char *) strndupx (start, end - start);
+ self->key.size = end - start;
+
+ /* Everything else before and after is the public key */
+ memmove (start, end, strlen (end) + 1);
+ self->certificate.size -= end - start;
+
+ return true;
+ }
+
+ return false;
+}
+
+static void
+certificate_and_key_read (CertificateKeyPair *self,
+ const char *certificate_filename)
+{
+ self->certificate_filename = strdupx (certificate_filename);
+ read_file (self->certificate_filename, &self->certificate);
+
+ if (certificate_and_key_split (self))
+ {
+ self->key_filename = strdupx (self->certificate_filename);
+ warnx ("%s: merged certificate and key files are %s. "
+ "Please use a separate .cert and .key file.\n",
+ certificate_filename,
+ tolerate_merged_cert_key ? "deprecated" : "unsupported");
+
+ if (!tolerate_merged_cert_key)
+ exit (EXIT_FAILURE);
+ }
+ else
+ {
+ self->key_filename = cockpit_certificate_key_path (self->certificate_filename);
+ read_file (self->key_filename, &self->key);
+ }
+
+ if (self->key_filename)
+ asprintfx (&self->filename_for_errors, "%s/.key", self->certificate_filename);
+ else
+ self->filename_for_errors = strdupx (self->certificate_filename);
+}
+
+static gnutls_certificate_credentials_t
+certificate_and_key_parse_to_creds (CertificateKeyPair *self)
+{
+ gnutls_certificate_credentials_t creds = NULL;
+
+ int r = gnutls_certificate_allocate_credentials (&creds);
+ assert (r == GNUTLS_E_SUCCESS);
+
+ r = gnutls_certificate_set_x509_key_mem (creds,
+ &self->certificate, &self->key,
+ GNUTLS_X509_FMT_PEM);
+
+ if (r != GNUTLS_E_SUCCESS)
+ errx (EXIT_FAILURE, "%s: %s", self->filename_for_errors, gnutls_strerror (r));
+
+ return creds;
+}
+
+static bool
+cockpit_certificate_find (CertificateKeyPair *result,
+ bool verbose)
+{
+ char *error = NULL;
+ char *certificate_filename = cockpit_certificate_locate (true, &error);
+
+ if (error != NULL)
+ errx (EXIT_FAILURE, "%s", error);
+
+ if (certificate_filename == NULL)
+ {
+ if (verbose)
+ printf ("Unable to find any certificate file\n");
+
+ return false;
+ }
+
+ certificate_and_key_read (result, certificate_filename);
+
+ gnutls_certificate_credentials_t creds = certificate_and_key_parse_to_creds (result);
+
+ bool expired = is_selfsigned (certificate_filename) && check_expiry (creds, certificate_filename);
+ if (expired)
+ {
+ if (verbose)
+ printf ("Found self-signed %s, but it needs to be reissued\n",
+ result->filename_for_errors);
+
+ certificate_and_key_clear (result);
+ }
+
+ gnutls_certificate_free_credentials (creds);
+ free (certificate_filename);
+
+ return !expired;
+}
+
+static void
+cockpit_certificate_selfsign (CertificateKeyPair *result)
+{
+ pid_t pid;
+ int r = posix_spawn (&pid, COCKPIT_CERTIFICATE_HELPER, NULL, NULL,
+ (char *[]){ COCKPIT_CERTIFICATE_HELPER, "selfsign", NULL },
+ NULL);
+
+ if (r != 0)
+ errx (EXIT_FAILURE, "posix_spawn: %s: %s",
+ COCKPIT_CERTIFICATE_HELPER, strerror (r));
+
+ int status;
+ do
+ r = waitpid (pid, &status, 0);
+ while (r == -1 && errno == EINTR);
+
+ if (r < 0)
+ err (EXIT_FAILURE, "wait: %s", COCKPIT_CERTIFICATE_HELPER);
+
+ if (!WIFEXITED (status) || WEXITSTATUS (status) != 0)
+ errx (EXIT_FAILURE, "%s exited with non-zero status %d",
+ COCKPIT_CERTIFICATE_HELPER, WEXITSTATUS (status));
+
+ certificate_and_key_read (result, COCKPIT_SELFSIGNED_PATH);
+
+ /* We just generated this ourselves, so we don't bother to check it
+ * for validity.
+ */
+}
+
+int
+main (int argc, char **argv)
+{
+ CertificateKeyPair result = { };
+ bool check = false;
+ bool for_cockpit_tls = false;
+
+ if (argc == 1)
+ ;
+ else if (argc == 2 && strcmp (argv[1], "--check") == 0)
+ check = true;
+ else if (argc == 2 && strcmp (argv[1], "--for-cockpit-tls") == 0)
+ for_cockpit_tls = true;
+ else
+ errx (EXIT_FAILURE, "usage: %s [--check]", argv[0]);
+
+ if (for_cockpit_tls)
+ tolerate_merged_cert_key = true;
+
+ if (!cockpit_certificate_find (&result, check))
+ {
+ if (check)
+ {
+ printf ("Would create a self-signed certificate\n");
+ return 1;
+ }
+
+ cockpit_certificate_selfsign (&result);
+ }
+
+ if (check)
+ printf ("Would use certificate %s\n", result.certificate_filename);
+
+ if (for_cockpit_tls)
+ {
+ const char *runtime_directory = getenv ("RUNTIME_DIRECTORY");
+ if (runtime_directory == NULL)
+ errx (EXIT_FAILURE, "--for-cockpit-tls cannot be used unless RUNTIME_DIRECTORY is set");
+
+ certificate_and_key_write (&result, runtime_directory);
+ }
+
+ certificate_and_key_clear (&result);
+
+ return 0;
+}
diff --git a/src/tls/cockpit-certificate-helper.in b/src/tls/cockpit-certificate-helper.in
new file mode 100644
index 0000000..e109bcd
--- /dev/null
+++ b/src/tls/cockpit-certificate-helper.in
@@ -0,0 +1,186 @@
+#!/bin/bash
+
+set -eu
+
+# prefix= is set because the default @sysconfdir@ contains "${prefix}"
+prefix="@prefix@"
+COCKPIT_CONFIG="@sysconfdir@/cockpit"
+COCKPIT_WS_CERTS_D="${COCKPIT_CONFIG}/ws-certs.d"
+COCKPIT_RUNTIME_DIR="/run/cockpit"
+
+install_cert() {
+ local destination="${COCKPIT_WS_CERTS_D}/$1"
+ mv -Z "$1" "${destination}"
+
+ # The certificate should be world-readable
+ chmod a+r "${destination}"
+}
+
+install_key() {
+ local destination="${COCKPIT_WS_CERTS_D}/$1"
+ mv -Z "$1" "${destination}"
+}
+
+selfsign_sscg() {
+ sscg --quiet \
+ --lifetime "${DAYS}" \
+ --key-strength 2048 \
+ --cert-key-file "${KEYFILE}" \
+ --cert-file "${CERTFILE}" \
+ --ca-file "${CA_FILE}" \
+ --hostname "${HOSTNAME}" \
+ --organization "${MACHINE_ID:-unspecified}" \
+ --subject-alt-name localhost \
+ --subject-alt-name IP:127.0.0.1/255.255.255.255
+}
+
+selfsign_openssl() {
+ openssl req -x509 \
+ -days "${DAYS}" \
+ -newkey rsa:2048 \
+ -keyout "${KEYFILE}" \
+ -keyform PEM \
+ -nodes \
+ -out "${CERTFILE}" \
+ -outform PEM \
+ -subj "${MACHINE_ID:+/O=${MACHINE_ID}}/CN=${HOSTNAME}" \
+ -config - \
+ -extensions v3_req << EOF
+ [ req ]
+ req_extensions = v3_req
+ extensions = v3_req
+ distinguished_name = req_distinguished_name
+ [ req_distinguished_name ]
+ [ v3_req ]
+ subjectAltName=IP:127.0.0.1,DNS:localhost
+ basicConstraints = critical, CA:TRUE
+ keyUsage = critical, digitalSignature,cRLSign,keyCertSign,keyEncipherment,keyAgreement
+ extendedKeyUsage = serverAuth
+EOF
+}
+
+cmd_selfsign() {
+ # Common variables used by both methods
+ local MACHINE_ID
+ if [ -e /etc/machine-id ]; then
+ MACHINE_ID="$(tr -d -c '[:xdigit:]' < /etc/machine-id)"
+ fi
+ local HOSTNAME="${HOSTNAME:-$(hostname)}"
+ local CERTFILE="0-self-signed.cert"
+ local KEYFILE="0-self-signed.key"
+ local CA_FILE="0-self-signed-ca.pem"
+
+ # We renew certificates up to 30 days before expiry, so give ourselves a
+ # year, plus 30 days. The maximum is variously mentioned to be 397 or 398.
+ local DAYS=395
+
+ # If sscg fails, try openssl
+ selfsign_sscg || selfsign_openssl
+
+ # Install the files and set permissions ($CA_FILE is only created by sscg)
+ test ! -e "${CA_FILE}" || install_cert "${CA_FILE}"
+ install_cert "${CERTFILE}"
+ install_key "${KEYFILE}"
+}
+
+cmd_ipa_request() {
+ local USER="$1"
+
+ # IPA operations require auth; read password from stdin to avoid quoting issues
+ # if kinit fails, we can't handle this setup, exit cleanly
+ kinit "${USER}@${REALM}" || exit 0
+
+ # ensure this gets run with a non-C locale; ipa fails otherwise
+ if [ "$(sh -c 'eval `locale`; echo $LC_CTYPE')" = 'C' ]; then
+ export LC_CTYPE=C.UTF-8
+ fi
+
+ # create a kerberos Service Principal Name for cockpit-ws, unless already present
+ ipa service-show "${SERVICE}" || \
+ ipa service-add --ok-as-delegate=true --ok-to-auth-as-delegate=true --force "${SERVICE}"
+
+ # add cockpit-ws key, unless already present
+ klist -k "${KEYTAB}" | grep -qF "${SERVICE}" || \
+ ipa-getkeytab -p "HTTP/${HOST}" -k "${KEYTAB}"
+
+ # request the certificate and put it into our certificate directory, so that auto-refresh works
+ ipa-getcert request -f "${COCKPIT_WS_CERTS_D}/10-ipa.cert" -k "${COCKPIT_WS_CERTS_D}/10-ipa.key" -K "HTTP/${HOST}" -m 640 -o root:root -M 644 -w -v
+}
+
+cmd_ipa_cleanup() {
+ # clean up keytab
+ if [ -e "${KEYTAB}" ]; then
+ ipa-rmkeytab -k "${KEYTAB}" -p "${SERVICE}"
+ fi
+
+ # clean up certificate; support both "copy" and "direct" modes from cmd_ipa_request()
+ if [ -e "${COCKPIT_WS_CERTS_D}/10-ipa.key" ]; then
+ rm "${COCKPIT_WS_CERTS_D}/10-ipa.cert" "${COCKPIT_WS_CERTS_D}/10-ipa.key"
+ ipa-getcert stop-tracking -f "${COCKPIT_WS_CERTS_D}/10-ipa.cert" -k "${COCKPIT_WS_CERTS_D}/10-ipa.key" || \
+ ipa-getcert stop-tracking -f /run/cockpit/certificate-helper/10-ipa.cert -k /run/cockpit/certificate-helper/10-ipa.key
+ fi
+}
+
+cmd_ipa() {
+ local REALM="$2"
+
+ local HOST
+ HOST="$(hostname -f)"
+ local SERVICE="HTTP/${HOST}@${REALM}"
+ local KEYTAB="${COCKPIT_CONFIG}/krb5.keytab"
+
+ # use a temporary keytab to avoid interfering with the system one
+ export KRB5CCNAME=/run/cockpit/keytab-setup
+
+ # not an IPA setup? cannot handle this
+ if [ -z "$(which ipa)" ]; then
+ echo 'ipa must be installed for this command'
+ exit 1
+ fi
+
+ case "$1" in
+ request)
+ cmd_ipa_request "$3"
+ ;;
+ cleanup)
+ cmd_ipa_cleanup
+ ;;
+ *)
+ echo 'unknown subcommand'
+ exit 1
+ ;;
+ esac
+}
+
+main() {
+ # ipa-getkeytab needs root to create the file, same for cert installation
+ if [ "$(id -u)" != "0" ]; then
+ echo 'must be run as root'
+ exit 1
+ fi
+
+ # Create a private working directory
+ mkdir -p "${COCKPIT_RUNTIME_DIR}"
+ WORKDIR="${COCKPIT_RUNTIME_DIR}/certificate-helper"
+ mkdir -m 700 "${WORKDIR}" # we expect that not to have existed
+ trap 'exit' INT QUIT PIPE TERM
+ trap 'rm -rf "${WORKDIR}"' EXIT
+ cd "${WORKDIR}"
+
+ # Dispatch subcommand
+ case "$1" in
+ selfsign)
+ cmd_selfsign
+ ;;
+ ipa)
+ shift
+ cmd_ipa "$@"
+ ;;
+ *)
+ echo 'unknown subcommand'
+ exit 1
+ ;;
+ esac
+}
+
+main "$@"
diff --git a/src/tls/connection.c b/src/tls/connection.c
new file mode 100644
index 0000000..6de1546
--- /dev/null
+++ b/src/tls/connection.c
@@ -0,0 +1,914 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "connection.h"
+
+#include <arpa/inet.h>
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <poll.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/timerfd.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#include <gnutls/gnutls.h>
+#include <gnutls/x509.h>
+
+#include <common/cockpitfdpassing.h>
+#include <common/cockpitjsonprint.h>
+#include <common/cockpitmemory.h>
+#include <common/cockpitwebcertificate.h>
+
+#include "certificate.h"
+#include "client-certificate.h"
+#include "httpredirect.h"
+#include "socket-io.h"
+#include "utils.h"
+
+/* cockpit-tls TCP server state (singleton) */
+static struct {
+ gnutls_certificate_request_t request_mode;
+ Certificate *certificate;
+ bool require_https;
+ int wsinstance_sockdir;
+ int cert_session_dir;
+} parameters = {
+ .wsinstance_sockdir = -1,
+ .cert_session_dir = -1
+};
+
+typedef struct
+{
+ char buffer[16u << 10]; /* 16KiB */
+ unsigned start, end;
+ bool eof, shut_rd, shut_wr;
+#ifdef DEBUG
+ const char *name;
+#endif
+} Buffer;
+
+/* a single TCP connection between the client (browser) and cockpit-tls */
+typedef struct {
+ int client_fd;
+ int ws_fd;
+
+ gnutls_session_t tls;
+
+ Buffer client_to_ws_buffer;
+ Buffer ws_to_client_buffer;
+
+ char *client_cert_filename;
+ char *wsinstance;
+ int metadata_fd;
+} Connection;
+
+#define BUFFER_SIZE (sizeof ((Buffer *) 0)->buffer)
+#define BUFFER_MASK (BUFFER_SIZE - 1)
+
+static_assert (!(BUFFER_SIZE & BUFFER_MASK), "buffer size not a power of 2");
+static_assert ((typeof (((Buffer *) 0)->start)) BUFFER_SIZE, "buffer is too big");
+
+
+static inline bool
+buffer_full (Buffer *self)
+{
+ return self->end - self->start == BUFFER_SIZE;
+}
+
+static inline bool
+buffer_empty (Buffer *self)
+{
+ return self->end == self->start;
+}
+
+static inline bool
+buffer_can_read (Buffer *self)
+{
+ return !self->shut_rd && !buffer_full (self);
+}
+
+static inline bool
+buffer_can_write (Buffer *self)
+{
+ return !self->shut_wr && !buffer_empty (self);
+}
+
+static inline bool
+buffer_needs_shut_rd (Buffer *self)
+{
+ return self->eof && !self->shut_rd;
+}
+
+static inline bool
+buffer_needs_shut_wr (Buffer *self)
+{
+ return self->eof && buffer_empty (self) && !self->shut_wr;
+}
+
+static inline bool
+buffer_alive (Buffer *self)
+{
+ return !self->shut_rd || !self->shut_wr;
+}
+
+static void
+buffer_shut_rd (Buffer *self)
+{
+ self->shut_rd = true;
+}
+
+static void
+buffer_shut_wr (Buffer *self)
+{
+ self->shut_wr = true;
+}
+
+static void
+buffer_eof (Buffer *self)
+{
+ self->eof = true;
+}
+
+static void
+buffer_epipe (Buffer *self)
+{
+ self->start = self->end;
+ self->eof = true;
+}
+
+static inline bool
+buffer_valid (Buffer *self)
+{
+ return self->end - self->start <= BUFFER_SIZE;
+}
+
+static short
+calculate_events (Buffer *reader,
+ Buffer *writer)
+{
+ return buffer_can_read (reader) * POLLIN | buffer_can_write (writer) * POLLOUT;
+}
+
+static short
+calculate_revents (Buffer *reader,
+ Buffer *writer)
+{
+ return buffer_needs_shut_rd (reader) * POLLIN | buffer_needs_shut_wr (writer) * POLLOUT;
+}
+
+static int
+get_iovecs (struct iovec *iov,
+ int iov_length,
+ char *buffer,
+ unsigned start,
+ unsigned end)
+{
+ int i = 0;
+
+ debug (IOVEC, " get_iovecs (%p, %i, %p, 0x%x, 0x%x)", iov, iov_length, buffer, start, end);
+ assert (end - start <= BUFFER_SIZE);
+
+ for (i = 0; i < iov_length && start != end; i++)
+ {
+ unsigned start_offset = start & BUFFER_MASK;
+
+ iov[i].iov_base = &buffer[start_offset];
+ iov[i].iov_len = MIN(BUFFER_SIZE - start_offset, end - start);
+ start += iov[i].iov_len;
+
+ debug (IOVEC, " iov[%i] = { 0x%zx, 0x%zx }; start = 0x%x;", i,
+ ((char *) iov[i].iov_base - buffer), iov[i].iov_len, start);
+ }
+
+ debug (IOVEC, " return %i;", i);
+
+ return i;
+}
+
+static void
+buffer_write_to_fd (Buffer *self,
+ int fd,
+ int *fd_to_send)
+{
+ struct iovec iov[2];
+ ssize_t s;
+
+ debug (BUFFER, "buffer_write_to_fd (%s/0x%x/0x%x, %i)", self->name, self->start, self->end, fd);
+
+ struct msghdr msg = { .msg_iov = iov };
+ msg.msg_iovlen = get_iovecs (iov, 2, self->buffer, self->start, self->end);
+
+ if (msg.msg_iovlen)
+ {
+ struct cmsghdr cmsg[2];
+
+ if (fd_to_send && *fd_to_send != -1)
+ cockpit_socket_msghdr_add_fd (&msg, cmsg, sizeof cmsg, *fd_to_send);
+
+ do
+ s = sendmsg (fd, &msg, MSG_NOSIGNAL | MSG_DONTWAIT);
+ while (s == -1 && errno == EINTR);
+
+ debug (BUFFER, " sendmsg returns %zi %s", s, (s == -1) ? strerror (errno) : "");
+
+ if (fd_to_send && *fd_to_send != -1 && s != -1)
+ {
+ close (*fd_to_send);
+ *fd_to_send = -1;
+ }
+
+ if (s == -1)
+ {
+ if (errno != EAGAIN)
+ /* Includes the expected case of EPIPE */
+ buffer_epipe (self);
+ }
+ else
+ self->start += s;
+ }
+
+ if (buffer_needs_shut_wr (self))
+ {
+ shutdown (fd, SHUT_WR);
+ buffer_shut_wr (self);
+ }
+
+ assert (buffer_valid (self));
+}
+
+static void
+buffer_read_from_fd (Buffer *self,
+ int fd)
+{
+ debug (BUFFER, "buffer_read_from_fd (%s/0x%x/0x%x, %i)", self->name, self->start, self->end, fd);
+
+ if (buffer_needs_shut_rd (self))
+ {
+ shutdown (fd, SHUT_RD);
+ buffer_shut_rd (self);
+ return;
+ }
+
+ struct iovec iov[2];
+ ssize_t s;
+ int iovcnt = get_iovecs (iov, 2, self->buffer, self->end, self->start + BUFFER_SIZE);
+ assert (iovcnt > 0);
+
+ do
+ s = readv (fd, iov, iovcnt);
+ while (s == -1 && errno == EINTR);
+
+ debug (BUFFER, " readv returns %zi %s", s, (s == -1) ? strerror (errno) : "");
+
+ if (s == -1)
+ {
+ if (errno != EAGAIN)
+ buffer_eof (self);
+ }
+ else if (s == 0)
+ buffer_eof (self);
+ else
+ self->end += s;
+
+ assert (buffer_valid (self));
+}
+
+static void
+buffer_write_to_tls (Buffer *self,
+ gnutls_session_t tls)
+{
+ struct iovec iov;
+ ssize_t s;
+
+ debug (BUFFER, "buffer_write_to_tls (%s/0x%x/0x%x, %p)", self->name, self->start, self->end, tls);
+
+ if (get_iovecs (&iov, 1, self->buffer, self->start, self->end))
+ {
+ do
+ s = gnutls_record_send (tls, iov.iov_base, iov.iov_len);
+ while (s == GNUTLS_E_INTERRUPTED);
+
+ debug (BUFFER, " gnutls_record_send returns %zi %s", s, (s < 0) ? gnutls_strerror (-s) : "");
+
+ if (s < 0)
+ {
+ if (s != GNUTLS_E_AGAIN)
+ buffer_epipe (self);
+ }
+ else
+ self->start += s;
+ }
+
+ if (buffer_needs_shut_wr (self))
+ {
+ gnutls_bye (tls, GNUTLS_SHUT_WR);
+ buffer_shut_wr (self);
+ }
+
+ assert (buffer_valid (self));
+}
+
+static void
+buffer_read_from_tls (Buffer *self,
+ gnutls_session_t tls)
+{
+ struct iovec iov;
+ ssize_t s;
+
+ debug (BUFFER, "buffer_read_from_tls (%s/0x%x/0x%x, %p)", self->name, self->start, self->end, tls);
+
+ if (buffer_needs_shut_rd (self))
+ {
+ /* There's not GNUTLS_SHUT_RD, so do the shutdown() on the
+ * underlying fd.
+ */
+ shutdown (gnutls_transport_get_int (tls), SHUT_RD);
+ buffer_shut_rd (self);
+ return;
+ }
+
+ int iovcnt = get_iovecs (&iov, 1, self->buffer, self->end, self->start + BUFFER_SIZE);
+ assert (iovcnt == 1);
+
+ do
+ s = gnutls_record_recv (tls, iov.iov_base, iov.iov_len);
+ while (s == GNUTLS_E_INTERRUPTED);
+
+ debug (BUFFER, " gnutls_record_recv returns %zi %s", s, (s < 0) ? gnutls_strerror (-s) : "");
+
+ if (s <= 0)
+ {
+ if (s != GNUTLS_E_AGAIN)
+ buffer_epipe (self);
+ }
+ else
+ self->end += s;
+
+ assert (buffer_valid (self));
+}
+
+static bool
+request_dynamic_wsinstance (const char *fingerprint)
+{
+ bool status = false;
+ char reply[20];
+ int fd;
+
+ debug (CONNECTION, "requesting dynamic wsinstance for %s:\n", fingerprint);
+
+ fd = socket (AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (fd == -1)
+ {
+ warn ("socket() failed");
+ goto out;
+ }
+
+ debug (CONNECTION, " -> connecting to https-factory.sock");
+ if (af_unix_connectat (fd, parameters.wsinstance_sockdir, "https-factory.sock") != 0)
+ {
+ warn ("connect(https-factory.sock) failed");
+ goto out;
+ }
+
+ /* send the fingerprint */
+ debug (CONNECTION, " -> success; sending fingerprint...");
+ if (!send_all (fd, fingerprint, strlen (fingerprint), 5 * 1000000))
+ goto out;
+
+ debug (CONNECTION, " -> success; waiting for reply...");
+
+ /* wait for the systemd job status reply */
+ if (!recv_alnum (fd, reply, sizeof reply, 30 * 1000000))
+ goto out;
+
+ debug (CONNECTION, " -> got reply '%s'...", reply);
+ status = strcmp (reply, "done") == 0;
+
+out:
+ debug (CONNECTION, " -> %s.", status ? "success" : "fail");
+
+ if (fd != -1)
+ close (fd);
+
+ return status;
+}
+
+static bool
+connection_connect_to_dynamic_wsinstance (Connection *self)
+{
+ char sockname[80];
+ int r;
+
+ assert (self->tls != NULL);
+
+ r = snprintf (sockname, sizeof sockname, "https@%s.sock", self->wsinstance);
+ assert (0 < r && r < sizeof sockname);
+
+ debug (CONNECTION, "Connecting to dynamic https instance %s...", sockname);
+
+ /* fast path: the socket already exists, so we can just connect to it */
+ if (af_unix_connectat (self->ws_fd, parameters.wsinstance_sockdir, sockname) == 0)
+ return true;
+
+ if (errno != ENOENT && errno != ECONNREFUSED)
+ warn ("connect(%s) failed on the first attempt", sockname);
+
+ debug (CONNECTION, " -> failed (%m). Requesting activation.");
+ /* otherwise, ask for the instance to be started */
+ if (!request_dynamic_wsinstance (self->wsinstance))
+ return false;
+
+ /* ... and try one more time. */
+ debug (CONNECTION, " -> trying again");
+ if (af_unix_connectat (self->ws_fd, parameters.wsinstance_sockdir, sockname) != 0)
+ {
+ warn ("connect(%s) failed on the second attempt", sockname);
+ return false;
+ }
+
+ /* otherwise, we're now connected */
+ debug (CONNECTION, " -> success!");
+ return true;
+}
+
+static bool
+connection_is_to_localhost (Connection *self)
+{
+ struct sockaddr_storage address;
+ socklen_t address_len = sizeof address;
+
+ /* NB: We check our own socket name, not the peer. That lets us find
+ * out if the connection was made to 127.0.0.1, or some other address.
+ *
+ * In the case that the client connects to 127.0.0.2, for example, the
+ * peer socket is still 127.0.0.1.
+ */
+ if (getsockname (self->client_fd, (struct sockaddr *) &address, &address_len) != 0)
+ return false;
+
+ switch (address.ss_family)
+ {
+ case AF_UNIX:
+ return true;
+
+ case AF_INET:
+ {
+ struct in_addr *addr4 = &((struct sockaddr_in *) &address)->sin_addr;
+ return addr4->s_addr == htonl (INADDR_LOOPBACK);
+ }
+
+ case AF_INET6:
+ {
+ struct in6_addr *addr6 = &((struct sockaddr_in6 *) &address)->sin6_addr;
+
+ /* Need to handle both ::ffff:127.0.0.1 as well as ::1
+ * This is ugly, but there doesn't seem to be a better (static) way... */
+ const struct in6_addr v4_loopback = { { { 0,0,0,0,0,0,0,0,0,0,0xff,0xff,127,0,0,1 } } };
+ return IN6_IS_ADDR_LOOPBACK (addr6) || IN6_ARE_ADDR_EQUAL(addr6, &v4_loopback);
+ }
+
+ default:
+ return false;
+ }
+}
+
+static bool
+connection_connect_to_wsinstance (Connection *self)
+{
+ if (self->tls == NULL && parameters.require_https && !connection_is_to_localhost (self))
+ {
+ /* server is expecting https connections */
+ self->ws_fd = http_redirect_connect ();
+ if (self->ws_fd == -1)
+ {
+ warn ("failed to connect to httpredirect");
+ return false;
+ }
+
+ return true;
+ }
+
+ self->ws_fd = socket (AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (self->ws_fd == -1)
+ {
+ warn ("failed to create cockpit-ws client socket");
+ return false;
+ }
+
+ if (self->tls == NULL)
+ {
+ /* server is expecting http connections, or localhost is exempt */
+ if (af_unix_connectat (self->ws_fd, parameters.wsinstance_sockdir, "http.sock") != 0)
+ {
+ warn ("connect(http.sock) failed");
+ return false;
+ }
+
+ return true;
+ }
+ else
+ return connection_connect_to_dynamic_wsinstance (self);
+}
+
+/**
+ * connection_handshake: Handle first event on client fd
+ *
+ * Check the very first byte of a new connection to tell apart TLS from plain
+ * HTTP. Initialize TLS.
+ */
+static bool
+connection_handshake (Connection *self)
+{
+ char b;
+ int ret;
+
+ assert (self->ws_fd == -1);
+
+ /* Wait for up to 30 seconds to receive the first byte before shutting
+ * down the connection.
+ */
+ struct pollfd pfd = { .fd = self->client_fd, .events = POLLIN };
+ do
+ ret = poll (&pfd, 1, 30000); /* timeout is wrong on syscall restart, but it's fine */
+ while (ret == -1 && errno == EINTR);
+
+ if (ret < 0)
+ err (EXIT_FAILURE, "poll() failed on client connection");
+
+ if (ret == 0)
+ {
+ debug (CONNECTION, "client sent no data in 30 seconds, dropping connection.");
+ return false;
+ }
+
+ /* peek the first byte and see if it's a TLS connection (starting with 22).
+ We can assume that there is some data to read, as this is called in response
+ to an epoll event. */
+ ret = recv (self->client_fd, &b, 1, MSG_PEEK);
+
+ if (ret < 0)
+ {
+ debug (CONNECTION, "could not read first byte: %s", strerror (errno));
+ return false;
+ }
+
+ if (ret == 0) /* EOF */
+ {
+ debug (CONNECTION, "client disconnected without sending any data");
+ return false;
+ }
+
+ if (b == 22)
+ {
+ debug (CONNECTION, "first byte is %i, initializing TLS", (int) b);
+
+ if (parameters.certificate == NULL)
+ {
+ warnx ("got TLS connection, but our server does not have a certificate/key; refusing");
+ return false;
+ }
+
+ ret = gnutls_init (&self->tls, GNUTLS_SERVER | GNUTLS_NO_SIGNAL);
+ if (ret != GNUTLS_E_SUCCESS)
+ {
+ warnx ("gnutls_init failed: %s", gnutls_strerror (ret));
+ return false;
+ }
+
+ ret = gnutls_set_default_priority (self->tls);
+ if (ret != GNUTLS_E_SUCCESS)
+ {
+ warnx ("gnutls_set_default_priority failed: %s", gnutls_strerror (ret));
+ return false;
+ }
+
+ ret = gnutls_credentials_set (self->tls, GNUTLS_CRD_CERTIFICATE,
+ certificate_get_credentials (parameters.certificate));
+ if (ret != GNUTLS_E_SUCCESS)
+ {
+ warnx ("gnutls_credentials_set failed: %s", gnutls_strerror (ret));
+ return false;
+ }
+
+ gnutls_session_set_verify_function (self->tls, client_certificate_verify);
+ gnutls_certificate_server_set_request (self->tls, parameters.request_mode);
+ gnutls_handshake_set_timeout (self->tls, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
+ gnutls_transport_set_int (self->tls, self->client_fd);
+
+ debug (CONNECTION, "TLS is initialised; doing handshake");
+
+ do
+ ret = gnutls_handshake (self->tls);
+ while (ret == GNUTLS_E_INTERRUPTED);
+
+ if (ret != GNUTLS_E_SUCCESS)
+ {
+ warnx ("gnutls_handshake failed: %s", gnutls_strerror (ret));
+ return false;
+ }
+
+ debug (CONNECTION, "TLS handshake completed");
+
+ if (!client_certificate_accept (self->tls, parameters.cert_session_dir,
+ &self->wsinstance, &self->client_cert_filename))
+ return false;
+ }
+
+ return true;
+}
+
+static void
+connection_thread_loop (Connection *self)
+{
+ while (buffer_alive (&self->client_to_ws_buffer) || buffer_alive (&self->ws_to_client_buffer))
+ {
+ short client_events, ws_events;
+ short client_revents, ws_revents;
+ int n_ready;
+
+ client_events = calculate_events (&self->client_to_ws_buffer, &self->ws_to_client_buffer);
+ ws_events = calculate_events (&self->ws_to_client_buffer, &self->client_to_ws_buffer);
+ client_revents = calculate_revents (&self->client_to_ws_buffer, &self->ws_to_client_buffer);
+ ws_revents = calculate_revents (&self->ws_to_client_buffer, &self->client_to_ws_buffer);
+
+ if (self->tls && buffer_can_read (&self->client_to_ws_buffer))
+ client_revents |= POLLIN * gnutls_record_check_pending (self->tls);
+
+ debug (POLL, "poll | client %d/x%x/x%x | ws %d/x%x/x%x |",
+ self->client_fd, client_events, client_revents,
+ self->ws_fd, ws_events, ws_revents);
+
+ do
+ {
+ /* don't poll for no events, we'd spin in a POLLHUP loop otherwise */
+ struct pollfd fds[] = { { client_events ? self->client_fd : -1, client_events },
+ { ws_events ? self->ws_fd : -1, ws_events }};
+
+ n_ready = poll (fds, N_ELEMENTS (fds), (client_revents | ws_revents) ? 0 : -1);
+
+ client_revents |= fds[0].revents;
+ ws_revents |= fds[1].revents;
+ }
+ while (n_ready == -1 && errno == EINTR);
+
+ if (n_ready == -1)
+ {
+ if (errno == EINVAL) /* ran out of fds */
+ return;
+ err (EXIT_FAILURE, "poll failed");
+ }
+
+ debug (POLL, "poll result %i | client %d/x%x | ws %d/x%x |", n_ready,
+ self->client_fd, client_revents, self->ws_fd, ws_revents);
+
+ if (self->tls)
+ {
+ if (client_revents & POLLIN)
+ buffer_read_from_tls (&self->client_to_ws_buffer, self->tls);
+
+ if (client_revents & POLLOUT)
+ buffer_write_to_tls (&self->ws_to_client_buffer, self->tls);
+ }
+ else
+ {
+ if (client_revents & POLLIN)
+ buffer_read_from_fd (&self->client_to_ws_buffer, self->client_fd);
+
+ if (client_revents & POLLOUT)
+ buffer_write_to_fd (&self->ws_to_client_buffer, self->client_fd, NULL);
+ }
+
+ if (ws_revents & POLLIN)
+ buffer_read_from_fd (&self->ws_to_client_buffer, self->ws_fd);
+
+ if (ws_revents & POLLOUT)
+ buffer_write_to_fd (&self->client_to_ws_buffer, self->ws_fd, &self->metadata_fd);
+ }
+}
+
+static bool
+connection_create_metadata (Connection *self)
+{
+ struct sockaddr_storage addr;
+ socklen_t addrsize = sizeof addr;
+ if (getpeername (self->client_fd, (struct sockaddr *) &addr, &addrsize))
+ {
+ debug (CONNECTION, "getpeername(%i) failed: %m. Disconnecting.", self->client_fd);
+ return false;
+ }
+
+ /* maximum we're going to see */
+ char ip[INET6_ADDRSTRLEN + 1 + IF_NAMESIZE + 1];
+ in_port_t port;
+
+ switch (addr.ss_family)
+ {
+ case AF_INET:
+ {
+ struct sockaddr_in *in_addr = (struct sockaddr_in *) &addr;
+
+ port = in_addr->sin_port;
+ const char *r = inet_ntop (AF_INET, &in_addr->sin_addr, ip, sizeof ip);
+ assert (r != NULL);
+ }
+ break;
+
+ case AF_INET6:
+ {
+ struct sockaddr_in6 *in6_addr = (struct sockaddr_in6 *) &addr;
+
+ port = in6_addr->sin6_port;
+ const char *r = inet_ntop (AF_INET6, &in6_addr->sin6_addr, ip, sizeof ip);
+ assert (r != NULL);
+
+ if (in6_addr->sin6_scope_id)
+ {
+ size_t iplen = strlen (ip);
+
+ ip[iplen++] = '%';
+
+ assert (IF_NAMESIZE < sizeof ip - iplen);
+ if (!if_indextoname (in6_addr->sin6_scope_id, ip + iplen))
+ {
+ /* fallback: just write the index */
+ int r = snprintf (ip + iplen, IF_NAMESIZE, "%u", in6_addr->sin6_scope_id);
+ assert (r < IF_NAMESIZE);
+ }
+
+ /* both snprintf() and if_indextoname() will have added a nul. */
+ }
+ }
+ break;
+
+ case AF_UNIX:
+ /* only used in testing */
+ ip[0] = '\0';
+ port = 0;
+ break;
+
+ default:
+ debug (CONNECTION, "Connection fd %i had unknown peer address family %d. Disconnecting.",
+ self->client_fd, (int) addr.ss_family);
+ return false;
+ }
+
+ debug (CONNECTION, "Connection fd %i is from %s:%d", self->client_fd, ip, port);
+
+ FILE *stream = cockpit_json_print_open_memfd ("cockpit-tls metadata", 1);
+
+ cockpit_json_print_string_property (stream, "origin-ip", ip, -1);
+ cockpit_json_print_integer_property (stream, "origin-port", port);
+
+ if (self->client_cert_filename)
+ cockpit_json_print_string_property (stream, "client-certificate", self->client_cert_filename, -1);
+
+ self->metadata_fd = cockpit_json_print_finish_memfd (&stream);
+
+ return true;
+}
+
+void
+connection_thread_main (int fd)
+{
+ Connection self = { .client_fd = fd, .ws_fd = -1, .metadata_fd = -1 };
+
+ assert (!buffer_can_write (&self.client_to_ws_buffer));
+ assert (!buffer_can_write (&self.ws_to_client_buffer));
+ assert (!self.tls);
+
+#ifdef DEBUG
+ self.client_to_ws_buffer.name = "client-to-ws";
+ self.ws_to_client_buffer.name = "ws-to-client";
+#endif
+
+ debug (CONNECTION, "New thread for fd %i", fd);
+
+ if (connection_handshake (&self) &&
+ connection_create_metadata (&self) &&
+ connection_connect_to_wsinstance (&self))
+ connection_thread_loop (&self);
+
+ debug (CONNECTION, "Thread for fd %i is going to exit now", fd);
+
+ free (self.wsinstance);
+
+ if (self.client_cert_filename)
+ client_certificate_unlink_and_free (parameters.cert_session_dir, self.client_cert_filename);
+
+ if (self.tls)
+ gnutls_deinit (self.tls);
+
+ if (self.client_fd != -1)
+ close (self.client_fd);
+
+ if (self.ws_fd != -1)
+ close (self.ws_fd);
+
+ if (self.metadata_fd != -1)
+ close (self.metadata_fd);
+}
+
+/**
+ * connection_crypto_init: Initialise TLS support
+ *
+ * This should be called after server_init() in order to enable TLS
+ * support for connections. If this function is not called, the server
+ * will only be able to handle http requests.
+ *
+ * The certificate file must either contain the key as well, or end with
+ * "*.crt" or "*.cert" and have a corresponding "*.key" file.
+ *
+ * @certfile: Server TLS certificate file; cannot be %NULL
+ * @request_mode: Whether to ask for client certificates
+ */
+void
+connection_crypto_init (const char *certificate_filename,
+ const char *key_filename,
+ bool allow_unencrypted,
+ gnutls_certificate_request_t request_mode)
+{
+ parameters.certificate = certificate_load (certificate_filename, key_filename);
+ parameters.request_mode = request_mode;
+ /* If we aren't called, then require_https is false */
+ parameters.require_https = !allow_unencrypted;
+}
+
+void
+connection_set_directories (const char *wsinstance_sockdir,
+ const char *runtime_directory)
+{
+ assert (parameters.wsinstance_sockdir == -1);
+ assert (parameters.cert_session_dir == -1);
+
+ assert (wsinstance_sockdir != NULL);
+ assert (runtime_directory != NULL);
+
+ parameters.wsinstance_sockdir = open (wsinstance_sockdir, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (parameters.wsinstance_sockdir == -1)
+ err (EXIT_FAILURE, "Unable to open wsinstance sockdir %s", wsinstance_sockdir);
+
+ int runtimedir_fd = open (runtime_directory, O_PATH | O_DIRECTORY | O_NOFOLLOW);
+ if (runtimedir_fd == -1)
+ err (EXIT_FAILURE, "Unable to open runtime directory %s", runtime_directory);
+
+ if (mkdirat (runtimedir_fd, "clients", 0700) != 0)
+ err (EXIT_FAILURE, "mkdir: %s/clients", runtime_directory);
+
+ parameters.cert_session_dir = openat (runtimedir_fd, "clients", O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ if (parameters.cert_session_dir == -1)
+ err (EXIT_FAILURE, "Unable to open certificate directory %s/clients", runtime_directory);
+
+ close (runtimedir_fd);
+}
+
+void
+connection_cleanup (void)
+{
+ assert (parameters.wsinstance_sockdir != -1);
+ assert (parameters.cert_session_dir != -1);
+
+ if (parameters.certificate)
+ {
+ certificate_unref (parameters.certificate);
+ parameters.certificate = NULL;
+ }
+
+ parameters.require_https = false;
+
+ close (parameters.cert_session_dir);
+ parameters.cert_session_dir = -1;
+
+ close (parameters.wsinstance_sockdir);
+ parameters.wsinstance_sockdir = -1;
+}
diff --git a/src/tls/connection.h b/src/tls/connection.h
new file mode 100644
index 0000000..b4b307a
--- /dev/null
+++ b/src/tls/connection.h
@@ -0,0 +1,43 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <gnutls/gnutls.h>
+
+/* init/teardown */
+void
+connection_set_directories (const char *wsinstance_sockdir,
+ const char *runtime_directory);
+
+void
+connection_crypto_init (const char *certificate_filename,
+ const char *key_filename,
+ bool allow_unencrypted,
+ gnutls_certificate_request_t request_mode);
+
+void
+connection_cleanup (void);
+
+/* handle a new connection */
+void
+connection_thread_main (int fd);
diff --git a/src/tls/httpredirect.c b/src/tls/httpredirect.c
new file mode 100644
index 0000000..85bc2cb
--- /dev/null
+++ b/src/tls/httpredirect.c
@@ -0,0 +1,174 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "httpredirect.h"
+
+#include <sys/socket.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+
+static char *
+read_line (FILE *stream,
+ char *buffer,
+ size_t sizeof_buffer,
+ size_t *offset)
+{
+ if (*offset >= sizeof_buffer)
+ return NULL;
+
+ char *line = fgets (buffer + *offset, sizeof_buffer - *offset, stream);
+
+ if (line == NULL)
+ return NULL;
+
+ /* Make sure we're terminated with \r\n or \n */
+ size_t line_length = strcspn (line, "\r\n");
+ const char *ending = line + line_length;
+ if (strcmp (ending, "\r\n") != 0 && strcmp (ending, "\n") != 0)
+ return NULL;
+
+ /* Discard the ending */
+ line[line_length++] = '\0';
+
+ *offset += line_length;
+
+ return line;
+}
+
+static bool
+write_error (FILE *output)
+{
+ fprintf (output, "HTTP/1.1 400 Client Error\r\n"
+ "\r\n"
+ "Incorrect request.\r\n");
+
+ return false;
+}
+
+static bool
+http_redirect (FILE *input,
+ FILE *output)
+{
+ char buffer[10000];
+ size_t offset = 0;
+
+ char *request_line = read_line (input, buffer, sizeof buffer, &offset);
+ if (request_line == NULL)
+ return write_error (output);
+
+ char *path = strchr (request_line, ' ');
+ if (path == NULL)
+ return write_error (output);
+ path++;
+
+ char *end_path = strchr (path, ' ');
+ if (end_path == NULL)
+ return write_error (output);
+ *end_path = '\0';
+
+ const char *host = NULL;
+ const char *header;
+ do
+ {
+ header = read_line (input, buffer, sizeof buffer, &offset);
+ if (header == NULL)
+ return write_error (output);
+
+#define HOST_HEADER "Host:"
+ if (strncmp (header, HOST_HEADER, strlen (HOST_HEADER)) == 0)
+ {
+ if (host != NULL)
+ return write_error (output);
+
+ host = header + strlen (HOST_HEADER);
+ host += strspn (host, " \t");
+ }
+ }
+ while (header[0] != '\0');
+
+ if (!host)
+ return write_error (output);
+
+ fprintf (output, "HTTP/1.1 301 Moved Permanently\r\n"
+ "Content-Type: text/html\r\n"
+ "Location: https://%s%s\r\n"
+ "\r\n", host, path);
+
+ return true;
+}
+
+static void *
+http_redirect_start (void *arg)
+{
+ FILE *stream = arg;
+
+ http_redirect (stream, stream);
+
+ fclose (stream);
+
+ return NULL;
+}
+
+int
+http_redirect_connect (void)
+{
+ int sv[2];
+ int r = socketpair (AF_UNIX, SOCK_STREAM, 0, sv);
+ if (r != 0)
+ return -1;
+
+ /* At this point we're going to succeed and return sv[1] to the
+ * caller. We need to make sure that sv[0] gets closed one way or
+ * another:
+ * - if we fail to create the stream, close()
+ * - if we fail to spawn the thread, fclose()
+ * - otherwise, the thread calls fclose()
+ */
+ FILE *stream = fdopen (sv[0], "r+");
+ if (stream == NULL)
+ close (sv[0]);
+ else
+ {
+ pthread_attr_t attr;
+ pthread_t thread;
+
+ pthread_attr_init (&attr);
+ pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
+
+ if (pthread_create (&thread, &attr, &http_redirect_start, stream))
+ fclose (stream);
+
+ pthread_attr_destroy (&attr);
+ }
+
+ return sv[1];
+}
+
+#ifdef HTTP_REDIRECT_STANDALONE
+int
+main (void)
+{
+ return http_redirect (stdin, stdout) ? 0 : 1;
+}
+#endif
diff --git a/src/tls/httpredirect.h b/src/tls/httpredirect.h
new file mode 100644
index 0000000..12f7fb9
--- /dev/null
+++ b/src/tls/httpredirect.h
@@ -0,0 +1,23 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+int
+http_redirect_connect (void);
diff --git a/src/tls/main.c b/src/tls/main.c
new file mode 100644
index 0000000..9f4edc5
--- /dev/null
+++ b/src/tls/main.c
@@ -0,0 +1,139 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <argp.h>
+#include <err.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <common/cockpitconf.h>
+#include <common/cockpitwebcertificate.h>
+#include "utils.h"
+#include "server.h"
+#include "connection.h"
+
+/* CLI arguments */
+struct arguments {
+ uint16_t port;
+ bool no_tls;
+ int idle_timeout;
+};
+
+#define OPT_NO_TLS 1000
+#define OPT_IDLE_TIMEOUT 1001
+
+static int
+arg_parse_int (char *arg, struct argp_state *state, int min, int max, const char *error_msg)
+{
+ char *endptr = NULL;
+ long num = strtol (arg, &endptr, 10);
+
+ if (!*arg || *endptr != '\0' || num < min || num > max)
+ argp_error (state, "%s: %s", error_msg, arg);
+ return (int) num;
+}
+
+/* Parse a single option. */
+static error_t
+parse_opt (int key, char *arg, struct argp_state *state)
+{
+ struct arguments *arguments = state->input;
+
+ switch (key)
+ {
+ case OPT_NO_TLS:
+ arguments->no_tls = true;
+ break;
+ case 'p':
+ arguments->port = arg_parse_int (arg, state, 1, UINT16_MAX, "Invalid port");
+ break;
+ case OPT_IDLE_TIMEOUT:
+ arguments->idle_timeout = arg_parse_int (arg, state, 0, INT_MAX, "Invalid idle timeout");
+ break;
+ default:
+ return ARGP_ERR_UNKNOWN;
+ }
+ return 0;
+}
+
+static struct argp_option options[] = {
+ {"no-tls", OPT_NO_TLS, 0, 0, "Don't use TLS" },
+ {"port", 'p', "PORT", 0, "Local port to bind to (9090 if unset)" },
+ {"idle-timeout", OPT_IDLE_TIMEOUT, "SECONDS", 0, "Time after which to exit if there are no connections; 0 to run forever (default: 90)" },
+ { 0 }
+};
+
+static const struct argp argp = {
+ .options = options,
+ .parser = parse_opt,
+ .doc = "cockpit-tls -- TLS terminating proxy for cockpit-ws",
+};
+
+int
+main (int argc, char **argv)
+{
+ struct arguments arguments;
+ gnutls_certificate_request_t client_cert_mode = GNUTLS_CERT_IGNORE;
+ const char *runtimedir;
+
+ /* default option values */
+ arguments.no_tls = false;
+ arguments.port = 9090;
+ arguments.idle_timeout = 90;
+
+ argp_parse (&argp, argc, argv, 0, 0, &arguments);
+
+ runtimedir = secure_getenv ("RUNTIME_DIRECTORY");
+ if (!runtimedir)
+ errx (EXIT_FAILURE, "$RUNTIME_DIRECTORY environment variable must be set to a private directory");
+
+ server_init ("/run/cockpit/wsinstance", runtimedir, arguments.idle_timeout, arguments.port);
+
+ if (!arguments.no_tls)
+ {
+ char *error = NULL;
+
+ if (error)
+ errx (EXIT_FAILURE, "Could not locate server certificate: %s", error);
+
+ if (cockpit_conf_bool ("WebService", "ClientCertAuthentication", false))
+ client_cert_mode = GNUTLS_CERT_REQUEST;
+
+ bool allow_unencrypted = cockpit_conf_bool ("WebService", "AllowUnencrypted", false);
+
+ connection_crypto_init ("/run/cockpit/tls/server/cert",
+ "/run/cockpit/tls/server/key",
+ allow_unencrypted, client_cert_mode);
+
+ /* There's absolutely no need to keep these around */
+ if (unlink ("/run/cockpit/tls/server/cert") != 0)
+ err (EXIT_FAILURE, "unlink: /run/cockpit/tls/server/cert");
+
+ if (unlink ("/run/cockpit/tls/server/key") != 0)
+ err (EXIT_FAILURE, "unlink: /run/cockpit/tls/server/key");
+ }
+
+ server_run ();
+ server_cleanup ();
+
+ return 0;
+}
diff --git a/src/tls/server.c b/src/tls/server.c
new file mode 100644
index 0000000..ef1a8e9
--- /dev/null
+++ b/src/tls/server.c
@@ -0,0 +1,375 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "server.h"
+
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <sys/timerfd.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "connection.h"
+#include "utils.h"
+
+/* cockpit-tls TCP server state (singleton) */
+static struct {
+ /* only used from main thread */
+ bool initialized;
+ int first_listener;
+ int last_listener;
+ int epollfd;
+
+ /* rw, protected by mutex */
+ pthread_mutex_t connection_mutex;
+ unsigned int connection_count;
+ int idle_timerfd;
+ struct itimerspec idle_timeout;
+} server;
+
+/**
+ * check_sd_listen_pid: Verify that systemd-activated socket is for us
+ *
+ * See sd_listen_fds(3).
+ */
+static bool
+check_sd_listen_pid (void)
+{
+ const char *pid_str = secure_getenv ("LISTEN_PID");
+ long pid;
+ char *endptr = NULL;
+
+ if (!pid_str)
+ {
+ warnx ("$LISTEN_PID not set, not accepting socket activation");
+ return false;
+ }
+
+ pid = strtol (pid_str, &endptr, 10);
+ if (pid <= 0 || *endptr != '\0')
+ errx (EXIT_FAILURE, "$LISTEN_PID contains invalid value '%s'", pid_str);
+ if ((pid_t) pid != getpid ())
+ {
+ warnx ("$LISTEN_PID %li is not for us, ignoring", pid);
+ return false;
+ }
+
+ return true;
+}
+
+static void *
+server_connection_thread_start_routine (void *data)
+{
+ int fd = (uintptr_t) data;
+
+ connection_thread_main (fd);
+
+ /* teardown */
+ {
+ pthread_mutex_lock (&server.connection_mutex);
+
+ server.connection_count--;
+
+ debug (CONNECTION, "Server.connection_count decreased to %i", server.connection_count);
+
+ if (server.connection_count == 0 && server.idle_timerfd != -1)
+ {
+ debug (CONNECTION, " -> setting idle timeout");
+ timerfd_settime (server.idle_timerfd, 0, &server.idle_timeout, NULL);
+ }
+
+ pthread_mutex_unlock (&server.connection_mutex);
+ }
+
+ return NULL;
+}
+
+/**
+ * handle_accept: Handle event on listening fd
+ *
+ * I. e. accepting new connections
+ */
+static void
+handle_accept (int listen_fd)
+{
+ int fd;
+ pthread_attr_t attr;
+ pthread_t thread;
+
+ debug (CONNECTION, "epoll_wait event on server listen fd %i", listen_fd);
+
+ /* accept and create new connection */
+ fd = accept4 (listen_fd, NULL, NULL, SOCK_CLOEXEC);
+ if (fd < 0)
+ {
+ if (errno != EINTR)
+ warn ("failed to accept connection");
+ return;
+ }
+
+ debug (CONNECTION, "New connection accepted, fd %i", fd);
+
+ {
+ pthread_mutex_lock (&server.connection_mutex);
+
+ if (server.connection_count == 0 && server.idle_timerfd != -1)
+ {
+ const struct itimerspec zero = { { 0 }, };
+ debug (CONNECTION, " -> clearing idle timeout.");
+ timerfd_settime (server.idle_timerfd, 0, &zero, NULL);
+ }
+
+ server.connection_count++;
+
+ debug (CONNECTION, " -> server.connection_count is now %i", server.connection_count);
+
+ pthread_mutex_unlock (&server.connection_mutex);
+ }
+
+ pthread_attr_init (&attr);
+ pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
+
+ int r = pthread_create (&thread, &attr,
+ server_connection_thread_start_routine,
+ (void *) (uintptr_t) fd);
+
+ if (r != 0)
+ {
+ errno = r;
+ warn ("pthread_create() failed. dropping connection");
+ close (fd);
+ }
+
+ pthread_attr_destroy (&attr);
+}
+
+/***********************************
+ *
+ * Public API
+ *
+ ***********************************/
+
+/**
+ * server_init: Initialize cockpit TLS proxy server
+ *
+ * There is only one instance of this. Trying to initialize it more than once
+ * is an error.
+ *
+ * @wsinstance_sockdir: Path to cockpit-wsinstance sockets directory
+ * @cert_session_dir: Path to store session certificates
+ * @idle_timeout: When positive, stop server after given number of seconds with
+ * no connections
+ * @port: Port to listen to; ignored when the listening socket is handed over
+ * through the systemd socket activation protocol
+ */
+void
+server_init (const char *wsinstance_sockdir,
+ const char *cert_session_dir,
+ int idle_timeout,
+ uint16_t port)
+{
+ const char *env_listen_fds;
+ struct epoll_event ev = { .events = EPOLLIN };
+
+ assert (!server.initialized);
+ server.initialized = true;
+ server.idle_timerfd = -1;
+
+ connection_set_directories (wsinstance_sockdir, cert_session_dir);
+
+ pthread_mutex_init (&server.connection_mutex, NULL);
+
+ /* systemd socket activated? */
+ env_listen_fds = secure_getenv ("LISTEN_FDS");
+ if (env_listen_fds && check_sd_listen_pid ())
+ {
+ char *endptr = NULL;
+ unsigned long n = strtoul (env_listen_fds, &endptr, 10);
+
+ if (n < 1 || n > INT_MAX || *endptr != '\0')
+ errx (EXIT_FAILURE, "Invalid $LISTEN_FDS value '%s'", env_listen_fds);
+
+ server.first_listener = SD_LISTEN_FDS_START;
+ server.last_listener = SD_LISTEN_FDS_START + (n - 1);
+ }
+ else
+ {
+ struct sockaddr_in sa_serv;
+ int optval = 1;
+
+ /* Listen to our port; on the command line and our API we just support one */
+ server.first_listener = socket (AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (server.first_listener < 0)
+ err (EXIT_FAILURE, "failed to create server listening fd");
+ server.last_listener = server.first_listener;
+
+ memset (&sa_serv, '\0', sizeof (sa_serv));
+ sa_serv.sin_family = AF_INET;
+ sa_serv.sin_addr.s_addr = INADDR_ANY;
+ sa_serv.sin_port = htons (port);
+
+ if (setsockopt (server.first_listener, SOL_SOCKET, SO_REUSEADDR, (void *) &optval, sizeof (int)) < 0)
+ err (EXIT_FAILURE, "failed to set socket option");
+ if (bind (server.first_listener, (struct sockaddr *) &sa_serv, sizeof (sa_serv)) < 0)
+ err (EXIT_FAILURE, "failed to bind to port %hu", port);
+ if (listen (server.first_listener, 1024) < 0)
+ err (EXIT_FAILURE, "failed to listen to server port");
+ debug (SERVER, "Server ready. Listening on port %hu, fd %i", port, server.first_listener);
+ }
+
+ /* epoll the listening fds */
+ server.epollfd = epoll_create1 (EPOLL_CLOEXEC);
+ if (server.epollfd < 0)
+ err (EXIT_FAILURE, "Failed to create epoll fd");
+ for (int fd = server.first_listener; fd <= server.last_listener; fd++)
+ {
+ ev.data.fd = fd;
+ if (epoll_ctl (server.epollfd, EPOLL_CTL_ADD, fd, &ev) < 0)
+ err (EXIT_FAILURE, "Failed to epoll server listening fd");
+ }
+
+ /* we use timerfd for idle timeout. epoll that too. */
+ if (idle_timeout > 0)
+ {
+ server.idle_timerfd = timerfd_create (CLOCK_MONOTONIC, TFD_CLOEXEC);
+ if (server.idle_timerfd == -1)
+ err (EXIT_FAILURE, "Failed to create timerfd");
+
+ server.idle_timeout.it_value.tv_sec = idle_timeout;
+ if (timerfd_settime (server.idle_timerfd, 0, &server.idle_timeout, NULL) != 0)
+ err (EXIT_FAILURE, "Failed to set timerfd");
+
+ ev.data.fd = server.idle_timerfd;
+ if (epoll_ctl (server.epollfd, EPOLL_CTL_ADD, server.idle_timerfd, &ev) < 0)
+ err (EXIT_FAILURE, "Failed to epoll idle timerfd");
+ }
+}
+
+int
+server_get_listener (void)
+{
+ assert (server.first_listener == server.last_listener);
+ return server.first_listener;
+}
+
+/**
+ * server_cleanup: Free all resources to the cockpit TLS proxy server
+ *
+ * There is only one instance of this. Trying to free it more than once
+ * is an error.
+ */
+void
+server_cleanup (void)
+{
+ assert (server.initialized);
+ assert (server.connection_count == 0);
+
+ if (server.idle_timerfd != -1)
+ close (server.idle_timerfd);
+
+ for (int fd = server.first_listener; fd <= server.last_listener; fd++)
+ close (fd);
+
+ close (server.epollfd);
+
+ pthread_mutex_destroy (&server.connection_mutex);
+
+ connection_cleanup ();
+
+ memset (&server, 0, sizeof server);
+}
+
+/**
+ * server_poll_event: Wait for and process one event
+ *
+ * @timeout: number of milliseconds to wait for an event to happen; after that,
+ * the function will return false. -1 will to block until an event occurs.
+ *
+ * This can be an event on a listening socket, or the idle timeout if no
+ * clients are connected.
+ *
+ * Returns: false on timeout, true if some (other) event was handled.
+ */
+bool
+server_poll_event (int timeout)
+{
+ int ret;
+ struct epoll_event ev;
+
+ assert (server.initialized);
+
+ ret = epoll_wait (server.epollfd, &ev, 1, timeout);
+ if (ret == 0)
+ return false; /* hit timeout */
+
+ if (ret == 1)
+ {
+ int fd = ev.data.fd;
+
+ if (fd == server.idle_timerfd)
+ {
+ /* hit the idle timeout */
+ debug (SERVER, "server_poll_event(): idle timer elapsed, returning immediately");
+ return false;
+ }
+
+ assert (server.first_listener <= fd && fd <= server.last_listener);
+
+ handle_accept (fd);
+ }
+ else if (errno != EINTR)
+ err (EXIT_FAILURE, "Failed to epoll_wait");
+
+ return true; /* did something */
+}
+
+/**
+ * server_run: Server main loop
+ *
+ * Returns if the server reached the idle timeout, otherwise runs forever.
+ */
+void
+server_run (void)
+{
+ while (server_poll_event (-1))
+ ;
+}
+
+unsigned
+server_num_connections (void)
+{
+ unsigned count;
+
+ pthread_mutex_lock (&server.connection_mutex);
+ count = server.connection_count;
+ pthread_mutex_unlock (&server.connection_mutex);
+
+ return count;
+}
diff --git a/src/tls/server.h b/src/tls/server.h
new file mode 100644
index 0000000..f36fee0
--- /dev/null
+++ b/src/tls/server.h
@@ -0,0 +1,47 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include <gnutls/gnutls.h>
+
+void
+server_init (const char *wsinstance_sockdir,
+ const char *cert_session_dir,
+ int idle_timeout,
+ uint16_t port);
+
+void
+server_run (void);
+
+void
+server_cleanup (void);
+
+int
+server_get_listener (void);
+
+/* these are for unit tests only */
+bool
+server_poll_event (int timeout);
+
+unsigned
+server_num_connections (void);
diff --git a/src/tls/socket-activation-helper.c b/src/tls/socket-activation-helper.c
new file mode 100644
index 0000000..bfe5b56
--- /dev/null
+++ b/src/tls/socket-activation-helper.c
@@ -0,0 +1,324 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+#include "config.h"
+
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/poll.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "socket-io.h"
+#include "testing.h"
+#include "utils.h"
+
+#define MAX_COCKPIT_WS_ARGS 5
+
+static struct instance_type
+{
+ const char *sockname;
+ const char *argv[MAX_COCKPIT_WS_ARGS];
+} instance_types[] = {
+ {"https-factory.sock", {}}, /* treated specially */
+ /* support up to 2 ws instances (+1 special); increase this if the unit test needs more */
+ {"https@" SHA256_NIL ".sock", {"--for-tls-proxy", "--port=0"}},
+ {"https@" CLIENT_CERT_FINGERPRINT ".sock", {"--for-tls-proxy", "--port=0"}},
+ {"https@" ALTERNATE_FINGERPRINT ".sock", {}}, /* treated specially */
+ {"http.sock", {"--no-tls", "--port", "0"}},
+};
+
+static int socket_to_pid[N_ELEMENTS (instance_types)];
+static struct pollfd ws_pollfds[N_ELEMENTS (instance_types)];
+
+volatile sig_atomic_t terminated = 0;
+
+static void
+term (int signum)
+{
+ debug (HELPER, "SIGTERM received");
+ terminated = 1;
+}
+
+
+static void
+handle_cockpit_ws_exited (int sigid)
+{
+ pid_t pid;
+ debug (HELPER, "SIGCHLD received");
+ while ((pid = waitpid (-1, NULL, WNOHANG)) > 0)
+ {
+ bool found = false;
+ debug (HELPER, "SIGCHLD received for pid %u", pid);
+ for (size_t i = 0; i < N_ELEMENTS (instance_types); i++)
+ {
+ if (pid == socket_to_pid[i])
+ {
+ debug (HELPER, "-> ws instance type %zu, cleaning up reference", i);
+ ws_pollfds[i].events = POLLIN;
+ socket_to_pid[i] = 0;
+ found = true;
+ break;
+ }
+ }
+
+ /* This can only be reached in case cockpit-ws exited fast and the parent
+ * process did not manage to insert its PID into the array
+ */
+ if (!found)
+ warnx ("Could not find the process in socket_to_pid array");
+ }
+
+ if (pid < 0 && errno != ECHILD)
+ err (EXIT_FAILURE, "waitpid failed");
+}
+
+static pid_t
+spawn_cockpit_ws (const char *ws_path, int fd,
+ const char **cockpit_ws_args)
+{
+ pid_t pid = fork ();
+
+ if (pid == -1)
+ {
+ err (EXIT_FAILURE, "fork() failed");
+ }
+ else if (pid > 0)
+ {
+ debug (HELPER, "spawned cockpit-ws instance pid %u", pid);
+ return pid;
+ }
+ else
+ {
+ const char *args[MAX_COCKPIT_WS_ARGS + 1] = { ws_path };
+ int res;
+ int duped_fd;
+ char pid_str[10];
+
+ /* pass the socket to ws like systemd activation does, see sd_listen_fds(3) */
+ /* fd is CLOEXEC, so dup2 on our first fd 3 will be a no-op; force duping */
+ duped_fd = dup (fd);
+ if (duped_fd < 0)
+ err (EXIT_FAILURE, "dup() failed");
+
+ if (duped_fd != SD_LISTEN_FDS_START)
+ {
+ if (dup2 (duped_fd, SD_LISTEN_FDS_START) < 0)
+ err (EXIT_FAILURE, "dup2() failed");
+ assert (close (duped_fd) == 0);
+ }
+
+ setenv ("LISTEN_FDS", "1", 1);
+ res = snprintf (pid_str, sizeof (pid_str), "%i", getpid ());
+ assert (res < sizeof (pid_str));
+ setenv ("LISTEN_PID", pid_str, 1);
+
+ for (int i = 0; cockpit_ws_args[i] != NULL; i++)
+ args[i + 1] = cockpit_ws_args[i];
+
+ execv (ws_path, (char **) args);
+
+ err (EXIT_FAILURE, "spawning cockpit-ws instance failed");
+ }
+}
+
+/* keep this in sync with src/ws/cockpit-wsinstance-https-factory@.service.in */
+/* this is blocking! if this program ever stops being an unit-test only thing
+* and gets used in production, rewrite as proper child process */
+static void
+handle_https_factory (int listen_fd)
+{
+ int fd = accept4 (listen_fd, NULL, NULL, SOCK_CLOEXEC);
+ char instance[WSINSTANCE_MAX];
+ const char *result;
+
+ debug (HELPER, "connection to https-factory.sock:");
+
+ if (fd < 0)
+ err (EXIT_FAILURE, "accept connection to https-factory.sock");
+
+ debug (HELPER, " -> reading instance name... ");
+ if (!recv_alnum (fd, instance, sizeof instance, 10 * 1000000))
+ errx (EXIT_FAILURE, "failed to read instance name");
+
+ debug (HELPER, " -> success: '%s'", instance);
+ if (strcmp (instance, SHA256_NIL) == 0) /* we check this value from the tests */
+ result = "done";
+ else
+ result = "fail";
+
+ debug (HELPER, " -> sending reply '%s'", result);
+ if (!send_all (fd, result, strlen (result), 10 * 1000000))
+ errx (EXIT_FAILURE, "failed to write https-factory.sock response");
+ debug (HELPER, " -> done.");
+
+ close (fd);
+}
+
+static void *
+handle_alternate_thread (void *fd_as_ptr)
+{
+ int fd = (intptr_t) fd_as_ptr;
+ ssize_t s;
+ char b;
+
+ do
+ s = write (fd, "hello", 5);
+ while (s == -1 && errno == EINTR);
+ assert (s == 5);
+
+
+ do
+ s = read (fd, &b, 1);
+ while (s == -1 && errno == EINTR);
+ assert (s == 0);
+
+ close (fd);
+
+ return NULL;
+}
+
+static void
+handle_alternate (int listen_fd)
+{
+ int fd = accept4 (listen_fd, NULL, NULL, SOCK_CLOEXEC);
+ pthread_attr_t attr;
+ pthread_t thread;
+
+ /* This is used from a testcase which spins up a whole bunch of
+ * parallel connections, so we need to handle this asynchronously.
+ * Use threads.
+ */
+ pthread_attr_init (&attr);
+ pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
+ pthread_create (&thread, &attr, handle_alternate_thread, (void *) (intptr_t) fd);
+ pthread_attr_destroy (&attr);
+}
+
+int
+main (int argc, char *argv[])
+{
+ if (argc != 3)
+ errx (EXIT_FAILURE, "Usage: socket-activation-helper $WS_PATH $SOCKETS_DIR");
+
+ const char *ws_path = argv[1];
+ const char *socket_dir = argv[2];
+ int socket_dir_fd = open (socket_dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (socket_dir_fd < 0)
+ err (EXIT_FAILURE, "failed to open %s", socket_dir);
+
+ for (size_t i = 0; i < N_ELEMENTS (instance_types); i++)
+ ws_pollfds[i].fd = -1;
+
+ /* Set up signal handler for when cockpit-ws has exited */
+ struct sigaction sa;
+ sigemptyset (&sa.sa_mask);
+ sa.sa_flags = 0;
+ sa.sa_handler = handle_cockpit_ws_exited;
+ sigaction (SIGCHLD, &sa, NULL);
+
+ /* clean up ws child processes on SIGTERM */
+ sa.sa_handler = term;
+ sigaction (SIGTERM, &sa, NULL);
+
+ for (size_t i = 0; i < N_ELEMENTS (instance_types); i++)
+ {
+ int listen_fd;
+
+ if (unlinkat (socket_dir_fd, instance_types[i].sockname, 0) < 0 && errno != ENOENT)
+ err (EXIT_FAILURE, "unlink() failed");
+
+ /* create a listening socket for each cockpit-ws mode */
+ listen_fd = socket (AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (listen_fd < 0)
+ err (EXIT_FAILURE, "socket() failed");
+
+ if (af_unix_bindat (listen_fd, socket_dir_fd, instance_types[i].sockname) < 0)
+ err (EXIT_FAILURE, "%s/%s: bind() failed", socket_dir, instance_types[i].sockname);
+
+ if (listen (listen_fd, 32) < 0)
+ err (EXIT_FAILURE, "%s/%s: listen() failed", socket_dir, instance_types[i].sockname);
+
+ ws_pollfds[i].fd = listen_fd;
+ ws_pollfds[i].events = POLLIN;
+ }
+
+ /* signal the unit test that we are ready to connect */
+ int fd = openat (socket_dir_fd, "ready", O_CREAT | O_WRONLY | O_EXCL, 0666);
+ if (fd < 0)
+ err (EXIT_FAILURE, "failed to create %s/stamp", socket_dir);
+ close (fd);
+ close (socket_dir_fd);
+
+ /* Loop waiting for incoming connects on any of the connected sockets. */
+ while (!terminated)
+ {
+ int rv = poll (ws_pollfds, N_ELEMENTS (instance_types), -1);
+
+ if (rv == -1)
+ {
+ if (errno == EINTR)
+ continue;
+ err (EXIT_FAILURE, "poll() failed");
+ }
+ else
+ {
+ debug (HELPER, "got %i poll() events", rv);
+ for (size_t i = 0; i < N_ELEMENTS (instance_types); i++)
+ {
+ if (ws_pollfds[i].revents == POLLIN)
+ {
+ /* is this the https factory? */
+ if (strstr (instance_types[i].sockname, "factory"))
+ {
+ debug (HELPER, "got POLLIN on fd %i https factory", ws_pollfds[i].fd);
+ handle_https_factory (ws_pollfds[i].fd);
+ continue;
+ }
+ else if (strstr (instance_types[i].sockname, ALTERNATE_FINGERPRINT))
+ {
+ debug (HELPER, "got POLLIN on fd %i alternate cert socket", ws_pollfds[i].fd);
+ handle_alternate (ws_pollfds[i].fd);
+ continue;
+ }
+
+ ws_pollfds[i].events = 0;
+ debug (HELPER, "got POLLIN on fd %i, spawning ws for %s",
+ ws_pollfds[i].fd, instance_types[i].sockname);
+ socket_to_pid[i] = spawn_cockpit_ws (ws_path, ws_pollfds[i].fd,
+ instance_types[i].argv);
+ }
+ }
+ }
+ }
+
+ debug (HELPER, "exiting, cleaning up cockpit-ws children");
+ for (size_t i = 0; i < N_ELEMENTS (socket_to_pid); i++)
+ if (socket_to_pid[i] > 0)
+ kill (socket_to_pid[i], SIGTERM);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/tls/socket-io.c b/src/tls/socket-io.c
new file mode 100644
index 0000000..ed03b1a
--- /dev/null
+++ b/src/tls/socket-io.c
@@ -0,0 +1,419 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "socket-io.h"
+
+#include <assert.h>
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include "utils.h"
+
+static uint64_t
+get_elapsed_time (struct timespec *start)
+{
+ struct timespec now;
+ int r;
+
+ r = clock_gettime (CLOCK_MONOTONIC, &now);
+ assert (r == 0);
+
+ if (start->tv_sec == 0 && start->tv_nsec == 0)
+ *start = now;
+
+ int64_t elapsed = ((int64_t) now.tv_sec - start->tv_sec) * 1000000 +
+ ((int64_t) now.tv_nsec - start->tv_nsec) / 1000;
+
+ assert (elapsed >= 0);
+
+ return elapsed;
+}
+
+/**
+ * get_remaining_timeout:
+ * @start: a timespec struct, initially initialised to { 0, 0 }
+ * @timeout_remaining: out-value for timeout remaining, in microseconds
+ * @timeout_us: the total timeout value in microseconds
+ *
+ * Uses @start to keep track of how much time of an initial timeout is
+ * remaining.
+ *
+ * This is useful to keep track of multiple-syscall IO operations with
+ * one global timeout, in the presence of multiple read() or write()
+ * calls, poll(), and the possibility of EINTR.
+ *
+ * On the first call (when @start is filled with zeros), @start is
+ * initialised and @timeout_remaining will be set to the value of
+ * @timeout_us. On successive calls (which should usually have the same
+ * value of @timeout_us), smaller values will be returned in line with
+ * the passage of time, until there is no timeout remaining.
+ *
+ * Returns #true when there has been a non-negative value written into
+ * @timeout_remaining, and returns #false when the timeout has expired.
+ */
+bool
+get_remaining_timeout (struct timespec *start,
+ uint64_t *timeout_remaining,
+ uint64_t timeout_us)
+{
+ uint64_t elapsed = get_elapsed_time (start);
+
+ debug (SOCKET_IO, " -> %lld of %lld elapsed", (long long) elapsed, (long long) timeout_us);
+
+ if (timeout_us < elapsed)
+ return false;
+
+ *timeout_remaining = timeout_us - elapsed;
+
+ return true;
+}
+
+static int
+wait_for_io (struct timespec *start,
+ int fd,
+ short events,
+ uint64_t timeout_us)
+{
+ struct pollfd pfd = { .fd = fd, .events = events };
+ uint64_t remaining;
+ int r;
+
+ debug (SOCKET_IO, "wait_for_io(%d, %u, %ju):", fd, (unsigned) events, (uintmax_t) timeout_us);
+
+ if (!get_remaining_timeout (start, &remaining, timeout_us))
+ return 0;
+
+ debug (SOCKET_IO, " -> waiting for %jd", (uintmax_t) remaining);
+
+ do
+ r = poll (&pfd, 1, (remaining + 999) / 1000);
+ while (r == -1 && errno == ENOENT);
+
+ debug (SOCKET_IO, " -> result is %d/%s", r, r < 0 ? strerror (errno) : "-");
+
+ return r;
+}
+
+/**
+ * recv_all:
+ * @fd: a file descriptor for a connected stream socket
+ * @buffer: a buffer
+ * @size: the size of @buffer
+ * @timeout: a timeout, in microseconds
+ *
+ * Attempts to read up to @size - 1 bytes from the connected stream
+ * socket @fd, followed by EOF. On success, a nul terminator is
+ * inserted after the last byte and the number of bytes read (which
+ * might be less than @size - 1) is returned, excluding the nul
+ * terminator. 0 is a valid result.
+ *
+ * On failure (socket errors, timeout, or simply too much data read
+ * without EOF), -1 is returned.
+ *
+ * This function is meant to be used with send_all() on the other side.
+ */
+static ssize_t
+recv_all (int fd,
+ char *buffer,
+ size_t size,
+ int timeout)
+{
+ struct timespec start = { 0, 0 };
+ size_t count = 0;
+
+ debug (SOCKET_IO, "read_all(fd=%d, size=%zu, timeout=%d)", fd, size, timeout);
+
+ /* We need to see recv() return 0 in order to know that we have EOF.
+ * In order to see that 0, we need to call recv() with a non-empty
+ * buffer. Conveniently, we can use the byte at the end of the buffer
+ * that we will write the nul terminator byte into. Without this
+ * extra byte, we'd need to have a separate throwaway variable and a
+ * separately-coded function call.
+ */
+ while (count < size && wait_for_io (&start, fd, POLLIN, timeout) == 1)
+ {
+ ssize_t s = recv (fd, buffer + count, size - count, MSG_DONTWAIT);
+ debug (SOCKET_IO, " -> recv returned %zd/%m", s);
+
+ if (s == -1)
+ {
+ if (errno == EINTR || errno == EAGAIN)
+ continue;
+
+ warn ("recv_all() failed");
+ return -1;
+ }
+
+ if (s == 0)
+ {
+ /* EOF → success */
+ debug (SOCKET_IO, " -> successfully received %zu bytes and EOF.", count);
+ buffer[count] = '\0';
+ return count;
+ }
+
+ count += s;
+ }
+
+ /* either the buffer overflowed or we timed out */
+ warnx ("recv_all() failed: buffer is full and no EOF received");
+ return -1;
+}
+
+/**
+ * recv_alnum:
+ * @fd: a file descriptor for a connected stream socket
+ * @buffer: a buffer
+ * @size: the size of @buffer
+ * @timeout: a timeout, in microseconds
+ *
+ * Attempts to read a non-empty alphanumeric string up to @size - 1
+ * bytes from the connected stream socket @fd, followed by EOF. On
+ * success, a nul terminator is inserted after the last byte and true is
+ * returned. The empty string is not a valid result.
+ *
+ * On failure (socket errors, timeout, too much data read, no data read,
+ * or in case the data is not alphanumeric), false is returned.
+ */
+bool
+recv_alnum (int fd,
+ char *buffer,
+ size_t size,
+ int timeout)
+{
+ ssize_t r;
+ size_t i;
+
+ r = recv_all (fd, buffer, size, timeout);
+
+ /* we need to have read at least one byte */
+ if (r < 1)
+ return false;
+
+ for (i = 0; i < r; i++)
+ if (!isalnum (buffer[i]))
+ return false;
+
+ return true;
+}
+
+/**
+ * send_all:
+ * @fd: a file descriptor for a connected stream socket
+ * @buffer: a buffer
+ * @size: the size of @buffer
+ * @timeout: a timeout, in microseconds
+ *
+ * Writes exactly @size bytes of @buffer to @fd, followed by EOF (ie:
+ * SHUT_WR).
+ *
+ * If all the bytes are written and the shutdown is successful, #true is
+ * returned. On failure (socket errors, or timeout) #false is returned.
+ *
+ * This function is meant to be used with recv_all() on the other side.
+ */
+bool
+send_all (int fd,
+ const char *buffer,
+ size_t size,
+ int timeout)
+{
+ struct timespec start = { 0, 0 };
+ size_t count = 0;
+
+ while (count < size && wait_for_io (&start, fd, POLLOUT, timeout) == 1)
+ {
+ ssize_t s = send (fd, buffer + count, size - count, MSG_DONTWAIT | MSG_NOSIGNAL);
+
+ if (s == -1)
+ {
+ if (errno == EINTR || errno == EAGAIN)
+ continue;
+
+ warn ("send_all() failed");
+ return false;
+ }
+
+ count += s;
+ }
+
+ if (count != size)
+ {
+ warnx ("send_all() timed out");
+ return false;
+ }
+
+ if (shutdown (fd, SHUT_WR) != 0)
+ {
+ warn ("send_all(): shutdown(SHUT_WR)");
+ return false;
+ }
+
+ debug (SOCKET_IO, " -> successfully sent all %zu bytes and EOF.", count);
+ return true;
+}
+
+/**
+ * af_unix_fill_sockaddr_at:
+ * @addr: a (probably uninitialised) struct sockaddr_in
+ * @dirfd: a directory fd, or AT_FDCWD
+ * @pathname: a pathname
+ *
+ * Fills in @addr (both family and name) fields to be used with a
+ * connect() or bind() call on a AF_UNIX socket.
+ *
+ * If the pathname given in @pathname is relative, then it is interpreted
+ * relative to the directory referred to bythe file descriptor dirfd
+ * (rather than relative to the current working directory of the calling
+ * process, as is done by connect() or bind() for a relative pathname).
+ * This is accomplished by building a pathname based on the symlinks in
+ * /proc/self/fd/ (so @dirfd needs to remain open for the actual call to
+ * connect() or bind()).
+ *
+ * If @pathname is relative and dirfd is the special value AT_FDCWD,
+ * then @pathname is interpreted relative to the current working
+ * directory of the calling process (like connect() or bind()).
+ *
+ * If @pathname is absolute, then dirfd is ignored.
+ *
+ * @dirfd is never actually inspected in any way during this call: it is
+ * assumed to be a valid open directory file descriptor. If it's not,
+ * then this error won't be detected.
+ *
+ * Returns: %true on success, or %false (and errno == ENOMEM) in case it
+ * was not possible to fit the formatted string into the `struct
+ * sockaddr_un`.
+ */
+static bool
+af_unix_fill_sockaddr_at (struct sockaddr_un *addr,
+ int dirfd,
+ const char *pathname)
+{
+ int r;
+
+ addr->sun_family = AF_UNIX;
+
+ if (pathname[0] != '/' && dirfd != AT_FDCWD)
+ r = snprintf (addr->sun_path, sizeof addr->sun_path, "/proc/self/fd/%d/%s", dirfd, pathname);
+ else
+ r = snprintf (addr->sun_path, sizeof addr->sun_path, "%s", pathname);
+
+ if (0 < r && r < sizeof addr->sun_path)
+ return true;
+
+ errno = ENOMEM;
+ return false;
+}
+
+/**
+ * af_unix_connectat:
+ * @sockfd: a socket file descriptor
+ * @dirfd: a directory file descriptor
+ * @pathname: a pathname
+ *
+ * Connects @sockfd to the unix domain socket at @pathname (relative to
+ * @dirfd).
+ *
+ * This call operates in exactly the same way as connect(), except for
+ * the differences described here.
+ *
+ * If the pathname given in @pathname is relative, then it is
+ * interpreted relative to the directory referred to bythe file
+ * descriptor dirfd (rather than relative to the current working
+ * directory of the calling process, as is done by connect() for a
+ * relative pathname).
+ *
+ * If @pathname is relative and dirfd is the special value AT_FDCWD,
+ * then @pathname is interpreted relative to the current working
+ * directory of the calling process (like connect()).
+ *
+ * If @pathname is absolute, then dirfd is ignored.
+ *
+ * An additional error of %ENOMEM can be returned in the event that it
+ * was not possible to fit the filename into a `struct sockaddr_un`.
+ *
+ * Returns: 0 on success, or -1 (and errno set) on error
+ */
+int
+af_unix_connectat (int sockfd,
+ int dirfd,
+ const char *pathname)
+{
+ struct sockaddr_un addr;
+
+ if (!af_unix_fill_sockaddr_at (&addr, dirfd, pathname))
+ return -1;
+
+ debug (SOCKET_IO, "af_unix_connectat(%d, %s) to '%s'", dirfd, pathname, addr.sun_path);
+
+ return connect (sockfd, (struct sockaddr *) &addr, sizeof addr);
+}
+
+/**
+ * af_unix_bindat:
+ * @sockfd: a socket file descriptor
+ * @dirfd: a directory file descriptor
+ * @pathname: a pathname
+ *
+ * Binds @sockfd to the filesystem at @pathname (relative to @dirfd).
+ *
+ * This call operates in exactly the same way as bind(), except for
+ * the differences described here.
+ *
+ * If the pathname given in @pathname is relative, then it is
+ * interpreted relative to the directory referred to bythe file
+ * descriptor dirfd (rather than relative to the current working
+ * directory of the calling process, as is done by bind() for a
+ * relative pathname).
+ *
+ * If @pathname is relative and dirfd is the special value AT_FDCWD,
+ * then @pathname is interpreted relative to the current working
+ * directory of the calling process (like bind()).
+ *
+ * If @pathname is absolute, then dirfd is ignored.
+ *
+ * An additional error of %ENOMEM can be returned in the event that it
+ * was not possible to fit the filename into a `struct sockaddr_un`.
+ *
+ * Returns: 0 on success, or -1 (and errno set) on error
+ */
+int
+af_unix_bindat (int sockfd,
+ int dirfd,
+ const char *pathname)
+{
+ struct sockaddr_un addr;
+
+ if (!af_unix_fill_sockaddr_at (&addr, dirfd, pathname))
+ return -1;
+
+ debug (SOCKET_IO, "af_unix_bindat(%d, %s) to '%s'", dirfd, pathname, addr.sun_path);
+
+ return bind (sockfd, (struct sockaddr *) &addr, sizeof addr);
+}
diff --git a/src/tls/socket-io.h b/src/tls/socket-io.h
new file mode 100644
index 0000000..c2b8ed3
--- /dev/null
+++ b/src/tls/socket-io.h
@@ -0,0 +1,53 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <sys/types.h>
+#include <time.h>
+
+bool
+get_remaining_timeout (struct timespec *start,
+ uint64_t *timeout_remaining,
+ uint64_t timeout_us);
+
+bool
+recv_alnum (int fd,
+ char *buffer,
+ size_t size,
+ int timeout);
+
+bool
+send_all (int fd,
+ const char *buffer,
+ size_t size,
+ int timeout);
+
+int
+af_unix_connectat (int sockfd,
+ int dirfd,
+ const char *pathname);
+
+int
+af_unix_bindat (int sockfd,
+ int dirfd,
+ const char *pathname);
diff --git a/src/tls/test-cockpit-certificate-ensure.c b/src/tls/test-cockpit-certificate-ensure.c
new file mode 100644
index 0000000..e7b7a8a
--- /dev/null
+++ b/src/tls/test-cockpit-certificate-ensure.c
@@ -0,0 +1,494 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitsystem.h"
+#include "testlib/cockpittest.h"
+
+#include <fcntl.h>
+#include <glib.h>
+#include <sys/stat.h>
+
+#include "common/cockpithacks-glib.h"
+
+#define CERTIFICATE_HELPER BUILDDIR "/cockpit-certificate-ensure"
+
+typedef struct {
+ GSubprocessLauncher *launcher;
+
+ gchar *config_dir;
+ int config_dir_fd;
+
+ char *runtime_dir;
+ int runtime_dir_fd;
+} Fixture;
+
+typedef struct {
+ const gchar **files;
+
+ const gchar *check_stdout;
+ const gchar *check_stderr;
+ int check_exit;
+
+ const gchar *copy_stdout;
+ const gchar *copy_stderr;
+ int copy_exit;
+ const gchar *key_source;
+ const gchar *cert_source;
+} TestCase;
+
+static void
+delete_all_files (int fd,
+ const char *path)
+{
+ int dirfd = openat (fd, path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ g_assert_no_errno (dirfd);
+ DIR *dirp = fdopendir (dirfd); /* owns dirfd */
+
+ const struct dirent *entry;
+ while ((entry = readdir (dirp)))
+ if (entry->d_name[0] != '.')
+ {
+ int r = unlinkat (dirfd, entry->d_name, 0);
+ g_assert_no_errno (r);
+ }
+
+ closedir (dirp);
+}
+
+static void
+fixture_teardown (Fixture *self,
+ gconstpointer data)
+{
+ g_clear_object (&self->launcher);
+
+ delete_all_files (self->config_dir_fd, "cockpit/ws-certs.d");
+
+ int r = unlinkat (self->config_dir_fd, "cockpit/ws-certs.d", AT_REMOVEDIR);
+ g_assert_no_errno (r);
+
+ r = unlinkat (self->config_dir_fd, "cockpit", AT_REMOVEDIR);
+ g_assert_no_errno (r);
+
+ r = rmdir (self->config_dir);
+ g_assert_no_errno (r);
+
+ close (self->config_dir_fd);
+ g_free (self->config_dir);
+
+ /* "server/" is only created for successful copy */
+ if (faccessat (self->runtime_dir_fd, "server", F_OK, 0) == 0)
+ {
+ delete_all_files (self->runtime_dir_fd, "server");
+
+ r = unlinkat (self->runtime_dir_fd, "server", AT_REMOVEDIR);
+ g_assert_no_errno (r);
+ }
+
+ r = rmdir (self->runtime_dir);
+ g_assert_no_errno (r);
+
+ close (self->runtime_dir_fd);
+ free (self->runtime_dir);
+}
+
+static void
+fixture_setup (Fixture *self,
+ gconstpointer data)
+{
+ const TestCase *tc = data;
+ g_autoptr(GError) error = NULL;
+
+ /* runtime dir */
+ self->runtime_dir = g_dir_make_tmp ("cockpit-test-runtime.XXXXXX", &error);
+ g_assert_no_error (error);
+
+ self->runtime_dir_fd = open (self->runtime_dir, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ g_assert_no_errno (self->runtime_dir_fd);
+
+ /* config dir */
+ self->config_dir = g_dir_make_tmp ("cockpit-test-config.XXXXXX", &error);
+ g_assert_no_error (error);
+
+ self->config_dir_fd = open (self->config_dir, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ g_assert_no_errno (self->config_dir_fd);
+
+ int r = mkdirat (self->config_dir_fd, "cockpit", 0700);
+ g_assert_no_errno (r);
+
+ r = mkdirat (self->config_dir_fd, "cockpit/ws-certs.d", 0700);
+ g_assert_no_errno (r);
+
+ /* populate ws-certs.d */
+ for (int i = 0; tc->files[i]; i++)
+ {
+ g_autofree gchar *linkname = g_strconcat ("cockpit/ws-certs.d", strrchr (tc->files[i], '/'), NULL);
+ r = symlinkat (tc->files[i], self->config_dir_fd, linkname);
+ g_assert_no_errno (r);
+ }
+
+ /* launcher */
+ self->launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE);
+ g_subprocess_launcher_setenv (self->launcher, "XDG_CONFIG_DIRS", self->config_dir, TRUE);
+ g_subprocess_launcher_setenv (self->launcher, "RUNTIME_DIRECTORY", self->runtime_dir, TRUE);
+}
+
+static void
+test_check (Fixture *fixture,
+ gconstpointer data)
+{
+ const TestCase *tc = data;
+ g_autoptr(GError) error = NULL;
+
+ g_autoptr(GSubprocess) helper = g_subprocess_launcher_spawn (fixture->launcher, &error,
+ CERTIFICATE_HELPER, "--check", NULL);
+ g_assert_no_error (error);
+
+ g_autofree gchar *stdout_str = NULL;
+ g_autofree gchar *stderr_str = NULL;
+ gboolean result = g_subprocess_communicate_utf8 (helper, NULL, NULL, &stdout_str, &stderr_str, &error);
+ g_assert_no_error (error);
+ g_assert (result);
+
+ g_assert (g_subprocess_get_if_exited (helper));
+
+ cockpit_assert_strmatch (stdout_str, tc->check_stdout);
+ cockpit_assert_strmatch (stderr_str, tc->check_stderr);
+ g_assert_cmpint (g_subprocess_get_exit_status (helper), ==, tc->check_exit);
+}
+
+static gchar *
+areadlinkat (int dirfd,
+ const char *filename)
+{
+ char buffer[PATH_MAX];
+ ssize_t s = readlinkat (dirfd, filename, buffer, sizeof buffer);
+ g_assert_cmpint (s, <, sizeof buffer);
+ if (s == -1 && errno == ENOENT)
+ s = 0; /* results in returning empty string */
+ else
+ g_assert_no_errno (s);
+ buffer[s] = '\0';
+ return g_strdup (buffer);
+}
+
+static void
+append_file_to_string (GString *string,
+ const gchar *filename)
+{
+ g_autoptr(GError) error = NULL;
+ g_autofree gchar *content = NULL;
+ gsize size;
+
+ g_file_get_contents (filename, &content, &size, &error);
+ g_assert_no_error (error);
+
+ g_string_append_len (string, content, size);
+}
+
+static void
+test_copy (Fixture *fixture,
+ gconstpointer data)
+{
+ const TestCase *tc = data;
+ g_autoptr(GError) error = NULL;
+
+ g_autoptr(GSubprocess) helper = g_subprocess_launcher_spawn (fixture->launcher, &error,
+ CERTIFICATE_HELPER, "--for-cockpit-tls", NULL);
+ g_assert_no_error (error);
+
+ g_autofree gchar *stdout_str = NULL;
+ g_autofree gchar *stderr_str = NULL;
+ gboolean result = g_subprocess_communicate_utf8 (helper, NULL, NULL, &stdout_str, &stderr_str, &error);
+ g_assert_no_error (error);
+ g_assert (result);
+
+ g_assert (g_subprocess_get_if_exited (helper));
+
+ cockpit_assert_strmatch (stdout_str, tc->copy_stdout);
+ cockpit_assert_strmatch (stderr_str, tc->copy_stderr);
+ g_assert_cmpint (g_subprocess_get_exit_status (helper), ==, tc->copy_exit);
+
+ g_autofree gchar *cert_source = areadlinkat (fixture->runtime_dir_fd, "server/cert.source");
+ cockpit_assert_strmatch (cert_source, tc->cert_source);
+
+ g_autofree gchar *key_source = areadlinkat (fixture->runtime_dir_fd, "server/key.source");
+ cockpit_assert_strmatch (key_source, tc->key_source);
+
+ if (tc->copy_exit == EXIT_SUCCESS)
+ {
+ /* Check to make sure the input is the same as the output */
+ g_autoptr(GString) input_data = g_string_new (NULL);
+ append_file_to_string (input_data, cert_source);
+ if (!g_str_equal (key_source, cert_source))
+ append_file_to_string (input_data, key_source);
+ g_autoptr(GTlsCertificate) input = g_tls_certificate_new_from_pem (input_data->str, input_data->len, &error);
+ g_assert_no_error (error);
+
+ g_autofree gchar *certfile = g_build_filename (fixture->runtime_dir, "server", "cert", NULL);
+ g_autofree gchar *keyfile = g_build_filename (fixture->runtime_dir, "server", "key", NULL);
+ g_autoptr(GTlsCertificate) output = g_tls_certificate_new_from_files (certfile, keyfile, &error);
+#if !GLIB_CHECK_VERSION(2,58,0)
+ /* Older GLib (RHEL 8) doesn't know how to find EC private keys */
+ if (error &&
+ strstr (error->message, "No PEM-encoded private key found") &&
+ strstr (input_data->str, "BEGIN EC PRIVATE KEY"))
+ {
+ g_test_skip ("EC private keys unsupported");
+ return;
+ }
+#endif
+ g_assert_no_error (error);
+
+ /* NB: doesn't check key, and there's no way to read it back :( */
+ g_assert (g_tls_certificate_is_same (input, output));
+ }
+}
+
+const gchar *no_files[] = { NULL };
+const gchar *good_rsa_files[] = { SRCDIR "/src/bridge/mock-server.crt",
+ SRCDIR "/src/bridge/mock-server.key", NULL };
+const gchar *good_ecc_files[] = { SRCDIR "/src/ws/mock-ecc.crt",
+ SRCDIR "/src/ws/mock-ecc.key", NULL };
+const gchar *bad_files[] = { SRCDIR "/bad", NULL };
+const gchar *bad_files2[] = { SRCDIR "/src/bridge/mock-server.crt", SRCDIR "/bad2", NULL };
+const gchar *invalid_files1[] = { SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf",
+ SRCDIR "/src/ws/mock-config/cockpit/cockpit-alt.conf", NULL };
+const gchar *invalid_files2[] = { SRCDIR "/src/bridge/mock-server.crt",
+ SRCDIR "/src/bridge/mock-client.crt", NULL };
+const gchar *invalid_files3[] = { SRCDIR "/src/bridge/mock-client.key", NULL };
+
+static const TestCase case_good_rsa_file = {
+ .files = good_rsa_files,
+
+ .check_stdout = "Would use */mock-server.crt*",
+ .check_stderr = "",
+ .check_exit = EXIT_SUCCESS,
+
+ .copy_stdout = "",
+ .copy_stderr = "",
+ .copy_exit = EXIT_SUCCESS,
+ .cert_source = "*/cockpit/ws-certs.d/mock-server.crt",
+ .key_source = "*/cockpit/ws-certs.d/mock-server.key",
+};
+
+static const TestCase case_good_ecc_file = {
+ .files = good_ecc_files,
+
+ .check_stdout = "Would use */mock-ecc.crt*",
+ .check_stderr = "",
+ .check_exit = EXIT_SUCCESS,
+
+ .copy_stdout = "",
+ .copy_stderr = "",
+ .copy_exit = EXIT_SUCCESS,
+ .cert_source = "*/cockpit/ws-certs.d/mock-ecc.crt",
+ .key_source = "*/cockpit/ws-certs.d/mock-ecc.key",
+};
+
+static const TestCase case_bad_file = {
+ .files = bad_files,
+
+ .check_stdout = "Unable to find*Would create*",
+ .check_stderr = "",
+ .check_exit = EXIT_FAILURE
+};
+
+static const TestCase case_bad_file2 = {
+ .files = bad_files2,
+
+ .check_stdout = "",
+ .check_stderr = "*open*mock-server.key*No such file*",
+ .check_exit = EXIT_FAILURE,
+
+ .copy_stdout = "",
+ .copy_stderr = "*open*mock-server.key*No such file*",
+ .copy_exit = EXIT_FAILURE,
+};
+
+static const TestCase case_invalid1 = {
+ .files = invalid_files1,
+
+ .check_stdout = "Unable to find*Would create*",
+ .check_stderr = "",
+ .check_exit = EXIT_FAILURE
+};
+
+static const TestCase case_invalid2 = {
+ .files = invalid_files2,
+
+ .check_stdout = "",
+ .check_stderr = "*open*mock-server.key*No such file*",
+ .check_exit = EXIT_FAILURE,
+
+ .copy_stdout = "",
+ .copy_stderr = "*open*mock-server.key*No such file*",
+ .copy_exit = EXIT_FAILURE,
+};
+
+static const TestCase case_invalid3 = {
+ .files = invalid_files3,
+
+ .check_stdout = "Unable to find*Would create*",
+ .check_stderr = "",
+ .check_exit = EXIT_FAILURE
+};
+
+static const TestCase case_create = {
+ .files = no_files,
+
+ .check_stdout = "Unable to find*Would create*",
+ .check_stderr = "",
+ .check_exit = EXIT_FAILURE
+};
+
+static const TestCase case_invalid_validity = {
+ .files = (const gchar *[]) { SRCDIR "/test/data/100years/0-self-signed.cert",
+ SRCDIR "/test/data/100years/0-self-signed.key",
+ NULL },
+ .check_stdout = "Found*self-signed*but it needs to be reissued*",
+ .check_stderr = "",
+ .check_exit = EXIT_FAILURE
+};
+
+static const TestCase case_expired = {
+ .files = (const gchar *[]) { SRCDIR "/test/data/expired/0-self-signed.cert",
+ SRCDIR "/test/data/expired/0-self-signed.key",
+ NULL },
+ .check_stdout = "Found*self-signed*but it needs to be reissued*",
+ .check_stderr = "",
+ .check_exit = EXIT_FAILURE
+};
+
+static const TestCase case_mismatched = {
+ .files = (const gchar *[]) { SRCDIR "/test/data/expired/0-self-signed.cert",
+ SRCDIR "/test/data/100years/0-self-signed.key",
+ NULL },
+ .check_stdout = "",
+ .check_stderr = "*certificate and the given key do not match*",
+ .check_exit = EXIT_FAILURE,
+
+ .copy_stdout = "",
+ .copy_stderr = "*certificate and the given key do not match*",
+ .copy_exit = EXIT_FAILURE,
+ .cert_source = "",
+ .key_source = ""
+};
+
+static const TestCase expired_not_selfsign = {
+ .files = (const gchar *[]) { SRCDIR "/test/data/expired/1.cert",
+ SRCDIR "/test/data/expired/1.key",
+ NULL },
+ .check_stdout = "Would use*1.cert*",
+ .check_stderr = "",
+ .check_exit = EXIT_SUCCESS,
+
+ .copy_stdout = "",
+ .copy_stderr = "",
+ .copy_exit = EXIT_SUCCESS,
+ .cert_source = "*/cockpit/ws-certs.d/1.cert",
+ .key_source = "*/cockpit/ws-certs.d/1.key",
+};
+
+static const TestCase expired_combined = {
+ .files = (const gchar *[]) { SRCDIR "/test/data/expired/combined.cert",
+ NULL },
+ .check_stdout = "",
+ .check_stderr = "*merged certificate and key files are unsupported*",
+ .check_exit = EXIT_FAILURE,
+
+ .copy_stdout = "",
+ .copy_stderr = "*merged certificate and key files are deprecated*",
+ .copy_exit = EXIT_SUCCESS,
+ .key_source = "*/cockpit/ws-certs.d/combined.cert",
+ .cert_source = "*/cockpit/ws-certs.d/combined.cert"
+};
+
+static const TestCase many_files = {
+ .files = (const gchar *[]) { SRCDIR "/test/data/expired/0-self-signed.cert",
+ SRCDIR "/test/data/expired/0-self-signed.key",
+ SRCDIR "/test/data/expired/1.cert",
+ SRCDIR "/test/data/expired/1.key",
+ SRCDIR "/test/data/expired/combined.cert",
+ NULL },
+ .check_stdout = "",
+ .check_stderr = "*merged certificate and key files are unsupported*",
+ .check_exit = EXIT_FAILURE,
+
+ .copy_stdout = "",
+ .copy_stderr = "*merged certificate and key files are deprecated*",
+ .copy_exit = EXIT_SUCCESS,
+ .key_source = "*/cockpit/ws-certs.d/combined.cert",
+ .cert_source = "*/cockpit/ws-certs.d/combined.cert"
+};
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/cockpit-certificate-ensure/check/good-rsa",
+ Fixture, &case_good_rsa_file, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/copy/good-rsa",
+ Fixture, &case_good_rsa_file, fixture_setup, test_copy, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/check/good-ecc",
+ Fixture, &case_good_ecc_file, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/copy/good-ecc",
+ Fixture, &case_good_ecc_file, fixture_setup, test_copy, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/bad-file",
+ Fixture, &case_bad_file, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/bad-file2",
+ Fixture, &case_bad_file2, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/not-valid",
+ Fixture, &case_invalid1, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/no-key",
+ Fixture, &case_invalid2, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/no-cert",
+ Fixture, &case_invalid3, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/create",
+ Fixture, &case_create, fixture_setup, test_check, fixture_teardown);
+
+ g_test_add ("/cockpit-certificate-ensure/invalid-validity",
+ Fixture, &case_invalid_validity, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/expired",
+ Fixture, &case_expired, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/check/mismatched",
+ Fixture, &case_mismatched, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/copy/mismatched",
+ Fixture, &case_mismatched, fixture_setup, test_copy, fixture_teardown);
+
+ g_test_add ("/cockpit-certificate-ensure/check/expired-not-self-signed",
+ Fixture, &expired_not_selfsign, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/copy/expired-not-self-signed",
+ Fixture, &expired_not_selfsign, fixture_setup, test_copy, fixture_teardown);
+
+ g_test_add ("/cockpit-certificate-ensure/check/expired-combined",
+ Fixture, &expired_combined, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/copy/expired-combined",
+ Fixture, &expired_combined, fixture_setup, test_copy, fixture_teardown);
+
+ g_test_add ("/cockpit-certificate-ensure/check/many-files",
+ Fixture, &many_files, fixture_setup, test_check, fixture_teardown);
+ g_test_add ("/cockpit-certificate-ensure/copy/many-files",
+ Fixture, &many_files, fixture_setup, test_copy, fixture_teardown);
+
+ return g_test_run ();
+}
diff --git a/src/tls/test-connection.c b/src/tls/test-connection.c
new file mode 100644
index 0000000..de1b1a9
--- /dev/null
+++ b/src/tls/test-connection.c
@@ -0,0 +1,31 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "testlib/cockpittest.h"
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ return g_test_run ();
+}
diff --git a/src/tls/test-server.c b/src/tls/test-server.c
new file mode 100644
index 0000000..5f6c17d
--- /dev/null
+++ b/src/tls/test-server.c
@@ -0,0 +1,827 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <netinet/in.h>
+#include <sys/poll.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gnutls/x509.h>
+
+#include "connection.h"
+#include "testing.h"
+#include "server.h"
+#include "utils.h"
+#include "testlib/cockpittest.h"
+#include "common/cockpithacks-glib.h"
+
+#define SOCKET_ACTIVATION_HELPER BUILDDIR "/socket-activation-helper"
+#define COCKPIT_WS BUILDDIR "/cockpit-ws"
+/* this has a corresponding mock-server.key */
+#define CERTFILE SRCDIR "/src/bridge/mock-server.crt"
+#define KEYFILE SRCDIR "/src/bridge/mock-server.key"
+
+#define CLIENT_CERTFILE SRCDIR "/src/tls/ca/alice.pem"
+#define CLIENT_KEYFILE SRCDIR "/src/tls/ca/alice.key"
+#define ALTERNATE_CERTFILE SRCDIR "/src/tls/ca/bob.pem"
+#define ALTERNATE_KEYFILE SRCDIR "/src/tls/ca/bob.key"
+#define CLIENT_EXPIRED_CERTFILE SRCDIR "/src/tls/ca/alice-expired.pem"
+
+typedef struct {
+ gchar *ws_socket_dir;
+ gchar *runtime_dir;
+ gchar *clients_dir;
+ gchar *cgroup_line;
+ GPid ws_spawner;
+ struct sockaddr_in server_addr;
+} TestCase;
+
+typedef struct {
+ const char *certfile;
+ const char *keyfile;
+ int cert_request_mode;
+ int idle_timeout;
+ const char *client_crt;
+ const char *client_key;
+ const char *client_fingerprint;
+} TestFixture;
+
+static const TestFixture fixture_separate_crt_key = {
+ .certfile = CERTFILE,
+ .keyfile = KEYFILE,
+};
+
+static const TestFixture fixture_separate_crt_key_client_cert = {
+ .certfile = CERTFILE,
+ .keyfile = KEYFILE,
+ .cert_request_mode = GNUTLS_CERT_REQUEST,
+ .client_crt = CLIENT_CERTFILE,
+ .client_key = CLIENT_KEYFILE,
+ .client_fingerprint = CLIENT_CERT_FINGERPRINT,
+};
+
+static const TestFixture fixture_expired_client_cert = {
+ .certfile = CERTFILE,
+ .keyfile = KEYFILE,
+ .cert_request_mode = GNUTLS_CERT_REQUEST,
+ .client_crt = CLIENT_EXPIRED_CERTFILE,
+ .client_key = CLIENT_KEYFILE,
+ .client_fingerprint = CLIENT_CERT_FINGERPRINT,
+};
+
+static const TestFixture fixture_alternate_client_cert = {
+ .certfile = CERTFILE,
+ .keyfile = KEYFILE,
+ .cert_request_mode = GNUTLS_CERT_REQUEST,
+ .client_crt = ALTERNATE_CERTFILE,
+ .client_key = ALTERNATE_KEYFILE,
+ .client_fingerprint = ALTERNATE_FINGERPRINT,
+};
+
+static const TestFixture fixture_run_idle = {
+ .idle_timeout = 1,
+};
+
+/* for forking test cases, where server's SIGCHLD handling gets in the way */
+static void
+block_sigchld (void)
+{
+ const struct sigaction child_action = { .sa_handler = SIG_DFL };
+ g_assert_cmpint (sigaction (SIGCHLD, &child_action, NULL), ==, 0);
+}
+
+/* check if we have a client certificate for a given cgroup */
+static bool
+check_for_certfile (TestCase *tc,
+ char **out_contents)
+{
+ g_autoptr(GDir) dir = g_dir_open (tc->clients_dir, 0, NULL);
+ g_assert (dir != NULL);
+
+ const char *name;
+ while ((name = g_dir_read_name (dir)))
+ {
+ g_autofree char *filename = g_build_filename (tc->clients_dir, name, NULL);
+
+ g_autofree char *contents = NULL;
+ g_autoptr(GError) error = NULL;
+ if (!g_file_get_contents (filename, &contents, NULL, &error))
+ {
+ /* files are flying around all the time: this might reasonably fail */
+ if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ continue;
+ g_assert_no_error (error);
+ g_assert_not_reached ();
+ }
+
+ if (g_str_has_prefix (contents, tc->cgroup_line))
+ {
+ if (out_contents)
+ *out_contents = g_steal_pointer (&contents);
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static int
+do_connect (TestCase *tc)
+{
+ int fd = socket (AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ g_assert_cmpint (fd, >, 0);
+ if (connect (fd, (struct sockaddr *) &tc->server_addr, sizeof (tc->server_addr)) < 0)
+ {
+ close (fd);
+ return -errno;
+ }
+ else
+ {
+ return fd;
+ }
+}
+
+static void
+send_request (int fd, const char *request)
+{
+ g_assert_cmpint (fd, >, 0);
+ g_assert_cmpint (write (fd, request, strlen (request)), ==, strlen (request));
+}
+
+static const char*
+recv_reply (int fd, char *buf, size_t buflen)
+{
+ ssize_t len;
+
+ len = recv (fd, buf, buflen - 1, MSG_DONTWAIT);
+ close (fd);
+ if (len < 0)
+ g_error ("recv_reply: unexpected error: %m");
+ g_assert_cmpint (len, >=, 50);
+ buf[len] = '\0'; /* so that we can use string functions on it */
+
+ return buf;
+}
+
+static const char*
+do_request (TestCase *tc, const char *request)
+{
+ static char buf[4096];
+ int fd = do_connect (tc);
+ int res;
+
+ send_request (fd, request);
+ /* wait until data is available */
+ for (int timeout = 0; timeout < 100; ++timeout) {
+ res = recv (fd, buf, 100, MSG_PEEK | MSG_DONTWAIT);
+ if (res >= 50)
+ return recv_reply (fd, buf, sizeof (buf));
+
+ server_poll_event (100);
+ }
+
+ g_error ("timed out waiting for enough data to become available: res=%d, error: %m", res);
+}
+
+static void
+assert_http (TestCase *tc)
+{
+ const char *res = do_request (tc, "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n");
+ /* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */
+ if (strstr (res, "200 OK"))
+ {
+ cockpit_assert_strmatch (res, "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html\r\n"
+ "Content-Security-Policy: connect-src 'self' http://localhost ws://localhost;*");
+ }
+ else
+ {
+ cockpit_assert_strmatch (res, "HTTP/1.1 404 Not Found\r\nContent-Type: text/html*");
+ }
+}
+
+static void
+assert_https_outcome (TestCase *tc,
+ const TestFixture *fixture,
+ unsigned expected_server_certs,
+ bool expect_tls_failure)
+{
+ pid_t pid;
+ int status = -1;
+
+ block_sigchld ();
+
+ /* do the connection in a subprocess, as gnutls_handshake is synchronous */
+ pid = fork ();
+ if (pid < 0)
+ g_error ("failed to fork: %m");
+ if (pid == 0)
+ {
+ const char request[] = "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n";
+ char buf[4096];
+ gnutls_session_t session;
+ gnutls_certificate_credentials_t xcred;
+ const gnutls_datum_t *server_certs;
+ unsigned server_certs_len;
+ ssize_t len;
+ int ret;
+ int fd = do_connect (tc);
+
+ g_assert_cmpint (fd, >, 0);
+
+ g_assert_cmpint (gnutls_init (&session, GNUTLS_CLIENT), ==, GNUTLS_E_SUCCESS);
+ gnutls_transport_set_int (session, fd);
+ g_assert_cmpint (gnutls_set_default_priority (session), ==, GNUTLS_E_SUCCESS);
+ gnutls_handshake_set_timeout(session, 5000);
+ g_assert_cmpint (gnutls_certificate_allocate_credentials (&xcred), ==, GNUTLS_E_SUCCESS);
+ g_assert_cmpint (gnutls_certificate_set_x509_system_trust (xcred), >=, 0);
+ if (fixture && fixture->client_crt)
+ {
+ g_assert (fixture->client_key);
+ g_assert_cmpint (gnutls_certificate_set_x509_key_file (xcred,
+ fixture->client_crt,
+ fixture->client_key,
+ GNUTLS_X509_FMT_PEM),
+ ==, GNUTLS_E_SUCCESS);;
+ }
+ g_assert_cmpint (gnutls_credentials_set (session, GNUTLS_CRD_CERTIFICATE, xcred), ==, GNUTLS_E_SUCCESS);
+
+ ret = gnutls_handshake (session);
+ if (ret != GNUTLS_E_SUCCESS)
+ {
+ if (expect_tls_failure)
+ exit (0);
+ else
+ g_error ("Handshake failed: %s", gnutls_strerror (ret));
+ }
+
+ /* check server certificate */
+ server_certs = gnutls_certificate_get_peers (session, &server_certs_len);
+ g_assert (server_certs);
+ g_assert_cmpuint (server_certs_len, ==, expected_server_certs);
+
+ /* send request, read response */
+ len = gnutls_record_send (session, request, sizeof (request));
+ if (len < 0 && expect_tls_failure)
+ exit (0);
+ g_assert_cmpint (len, ==, sizeof (request));
+
+ len = gnutls_record_recv (session, buf, sizeof (buf) - 1);
+ if (len < 0 && expect_tls_failure)
+ exit (0);
+ g_assert_cmpint (len, >=, 100);
+ g_assert_cmpint (len, <=, sizeof (buf) - 1);
+
+ buf[len] = '\0'; /* so that we can use string functions on it */
+ /* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */
+ if (strstr (buf, "200 OK"))
+ {
+ cockpit_assert_strmatch (buf, "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html\r\n"
+ "Content-Security-Policy: connect-src 'self' https://localhost wss://localhost;*");
+ }
+ else
+ {
+ cockpit_assert_strmatch (buf, "HTTP/1.1 404 Not Found\r\nContent-Type: text/html*");
+ }
+
+ /* check client certificate in state dir */
+ if (fixture && fixture->client_crt && tc->cgroup_line)
+ {
+ if (fixture->cert_request_mode != GNUTLS_CERT_IGNORE)
+ {
+ g_autofree char *cert_file = NULL;
+ g_autofree char *expected_pem = NULL;
+
+ g_assert (check_for_certfile (tc, &cert_file));
+ g_assert (g_file_get_contents (fixture->client_crt, &expected_pem, NULL, NULL));
+ g_assert (g_str_has_suffix (cert_file, expected_pem));
+ }
+ else
+ {
+ g_assert (!check_for_certfile (tc, NULL));
+ }
+ }
+
+ g_assert_cmpint (gnutls_bye (session, GNUTLS_SHUT_RDWR), ==, GNUTLS_E_SUCCESS);
+
+ g_assert_false (expect_tls_failure);
+
+ close (fd);
+ exit (0);
+ }
+
+ for (int retry = 0; retry < 100 && waitpid (pid, &status, WNOHANG) <= 0; ++retry)
+ server_poll_event (200);
+ g_assert_cmpint (status, ==, 0);
+
+ /* cleans up client certificate after closing connection */
+ g_assert (!check_for_certfile (tc, NULL));
+}
+
+static void
+assert_https (TestCase *tc, const TestFixture *fixture, unsigned expected_server_certs)
+{
+ assert_https_outcome (tc, fixture, expected_server_certs, false);
+}
+
+static void
+setup (TestCase *tc, gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ g_autoptr(GError) error = NULL;
+
+ alarm (120);
+
+ tc->ws_socket_dir = g_dir_make_tmp ("server.wssock.XXXXXX", NULL);
+ g_assert (tc->ws_socket_dir);
+
+ /* This absolutely must be on a real filesystem: overlayfs (as often
+ * seen for /tmp in containers) doesn't work. /dev/shm is always
+ * tmpfs, which works nicely (and matches what we expect to be at /run
+ * when we use this code in production).
+ */
+ char runtime_dir_template[] = "/dev/shm/server.runtime.XXXXXX";
+ tc->runtime_dir = g_mkdtemp (runtime_dir_template);
+ g_assert (tc->runtime_dir);
+ tc->runtime_dir = g_strdup (tc->runtime_dir);
+ tc->clients_dir = g_build_filename (tc->runtime_dir, "clients", NULL);
+
+ if (fixture && fixture->client_fingerprint)
+ tc->cgroup_line = g_strdup_printf ("0::/system.slice/system-cockpithttps.slice/cockpit-wsinstance-https@%s.service\n", fixture->client_fingerprint);
+
+ gchar* sah_argv[] = { SOCKET_ACTIVATION_HELPER, COCKPIT_WS, tc->ws_socket_dir, NULL };
+ if (!g_spawn_async (NULL, sah_argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &tc->ws_spawner, &error))
+ g_error ("Failed to spawn " SOCKET_ACTIVATION_HELPER ": %s", error->message);
+
+ /* wait until socket activation helper is ready */
+ int socket_dir_fd = open (tc->ws_socket_dir, O_RDONLY | O_DIRECTORY);
+ g_assert_cmpint (socket_dir_fd, >=, 0);
+ for (int retry = 0; retry < 200; ++retry)
+ {
+ if (faccessat (socket_dir_fd, "ready", F_OK, 0) == 0)
+ break;
+ g_usleep (10000);
+ }
+ close (socket_dir_fd);
+
+ /* Let the kernel assign a port */
+ server_init (tc->ws_socket_dir, tc->runtime_dir, fixture ? fixture->idle_timeout : 0, 0);
+
+ if (fixture && fixture->certfile)
+ connection_crypto_init (fixture->certfile, fixture->keyfile, false, fixture->cert_request_mode);
+
+ /* Figure out the socket address we ought to connect to */
+ socklen_t addrlen = sizeof tc->server_addr;
+ int r = getsockname (server_get_listener (), (struct sockaddr *) &tc->server_addr, &addrlen);
+ g_assert_no_errno (r);
+
+ /* Sanity check */
+ g_assert_cmpint (addrlen, ==, sizeof tc->server_addr);
+ g_assert_cmpint (tc->server_addr.sin_family, ==, AF_INET);
+}
+
+static void
+teardown (TestCase *tc, gconstpointer data)
+{
+ for (int i = 0; i < 100 && server_num_connections (); i++) /* 10s */
+ g_usleep (100000); /* 0.1s */
+
+ server_cleanup ();
+ g_assert_cmpint (kill (tc->ws_spawner, SIGTERM), ==, 0);
+ g_assert_cmpint (waitpid (tc->ws_spawner, NULL, 0), ==, tc->ws_spawner);
+
+ /* all children got cleaned up */
+ g_assert_cmpint (wait (NULL), ==, -1);
+ g_assert_cmpint (errno, ==, ECHILD);
+ /* connection should fail */
+ /* coverity[leaked_handle : FALSE] */
+ g_assert_cmpint (do_connect (tc), ==, -ECONNREFUSED);
+ g_unsetenv ("COCKPIT_WS_PROCESS_IDLE");
+
+ int socket_dir_fd = open (tc->ws_socket_dir, O_RDONLY | O_DIRECTORY);
+ g_assert_cmpint (socket_dir_fd, >=, 0);
+ g_assert_cmpint (unlinkat (socket_dir_fd, "http.sock", 0), ==, 0);
+ g_assert_cmpint (unlinkat (socket_dir_fd, "https-factory.sock", 0), ==, 0);
+ g_assert_cmpint (unlinkat (socket_dir_fd, "https@" SHA256_NIL ".sock", 0), ==, 0);
+ g_assert_cmpint (unlinkat (socket_dir_fd, "https@" CLIENT_CERT_FINGERPRINT ".sock", 0), ==, 0);
+ g_assert_cmpint (unlinkat (socket_dir_fd, "https@" ALTERNATE_FINGERPRINT ".sock", 0), ==, 0);
+ g_assert_cmpint (unlinkat (socket_dir_fd, "ready", 0), ==, 0);
+ close (socket_dir_fd);
+ g_assert_cmpint (g_rmdir (tc->ws_socket_dir), ==, 0);
+ g_free (tc->ws_socket_dir);
+
+ g_free (tc->cgroup_line);
+
+ g_assert_cmpint (g_rmdir (tc->clients_dir), ==, 0);
+ g_free (tc->clients_dir);
+
+ g_assert_cmpint (g_rmdir (tc->runtime_dir), ==, 0);
+ g_free (tc->runtime_dir);
+
+ alarm (0);
+}
+
+static void
+test_no_tls_single (TestCase *tc, gconstpointer data)
+{
+ g_assert_cmpuint (server_num_connections (), ==, 0);
+ assert_http (tc);
+
+ /* let the server process "peer has closed connection" */
+ for (int retries = 0; retries < 10 && server_num_connections () == 1; ++retries)
+ server_poll_event (100);
+ g_assert_cmpuint (server_num_connections (), ==, 0);
+}
+
+static void
+test_no_tls_many_serial (TestCase *tc, gconstpointer data)
+{
+ for (int i = 0; i < 20; ++i)
+ assert_http (tc);
+}
+
+static void
+test_tls_blocked_handshake (TestCase *tc, gconstpointer data)
+{
+ block_sigchld ();
+
+ pid_t pid = fork ();
+ if (pid == -1)
+ g_error ("fork failed: %m");
+
+ if (pid == 0)
+ {
+ /* child */
+ gint first_fd = do_connect (tc);
+ send_request (first_fd, "\x16"); /* start the TLS handshake */
+
+ /* Make sure the byte gets there before the next connection request */
+ sleep (1);
+
+ /* make sure we can do a second connection while the first one is
+ * blocked in the handshake
+ */
+ gint second_fd = do_connect (tc);
+ send_request (second_fd, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
+
+ /* wait 10 seconds for the reply */
+ struct pollfd pfd = { .fd = second_fd, .events = POLLIN };
+ g_assert_cmpint (poll (&pfd, 1, 10000), ==, 1);
+ close (second_fd);
+
+ close (first_fd);
+ exit (0);
+ }
+ else
+ {
+ /* parent */
+ int status;
+
+ while (waitpid (pid, &status, WNOHANG) <= 0)
+ server_poll_event (50);
+ g_assert_cmpint (status, ==, 0);
+ }
+}
+
+static void
+test_no_tls_many_parallel (TestCase *tc, gconstpointer data)
+{
+ int i;
+
+ block_sigchld ();
+
+ for (i = 0; i < 20; ++i)
+ {
+ pid_t pid = fork ();
+ if (pid < 0)
+ g_error ("failed to fork: %m");
+
+ if (pid > 0)
+ continue;
+
+ /* child */
+ char buf[4096];
+ int fd = do_connect (tc);
+
+ server_cleanup ();
+
+ send_request (fd, "GET / HTTP/1.0\r\nHost: localhost\r\n\r\n");
+ /* wait until data is available */
+ for (int timeout = 0; timeout < 10 && recv (fd, buf, 100, MSG_PEEK | MSG_DONTWAIT) < 100; ++timeout)
+ sleep (1);
+ recv_reply (fd, buf, sizeof (buf));
+ /* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */
+ if (strstr (buf, "200 OK"))
+ cockpit_assert_strmatch (buf, "HTTP/1.1 200 OK*");
+ else
+ cockpit_assert_strmatch (buf, "HTTP/1.1 404 Not Found*");
+ exit (0);
+ }
+
+ /* wait until all i child processes have finished */
+ while (i > 0)
+ {
+ int status;
+ int r = waitpid (-1, &status, WNOHANG);
+ g_assert_cmpint (r, >=, 0);
+ if (r == 0)
+ {
+ server_poll_event (50);
+ }
+ else
+ {
+ g_assert_cmpint (status, ==, 0);
+ --i;
+ }
+ }
+}
+
+static void
+test_no_tls_redirect (TestCase *tc, gconstpointer data)
+{
+ /* Make sure we connect on something other than localhost */
+ tc->server_addr.sin_addr.s_addr = htonl (INADDR_LOOPBACK + 1);
+
+ /* without TLS support it should not redirect */
+ const char *res = do_request (tc, "GET / HTTP/1.0\r\nHost: some.remote:1234\r\n\r\n");
+ /* This succeeds (200 OK) when building in-tree, but fails with dist-check due to missing doc root */
+ if (strstr (res, "200 OK"))
+ cockpit_assert_strmatch (res, "HTTP/1.1 200 OK*");
+ else
+ cockpit_assert_strmatch (res, "HTTP/1.1 404 Not Found*");
+}
+
+static void
+test_tls_no_client_cert (TestCase *tc, gconstpointer data)
+{
+ assert_https (tc, data, 1);
+}
+
+static void
+test_tls_no_server_cert (TestCase *tc, gconstpointer data)
+{
+ assert_http (tc);
+ assert_https_outcome (tc, data, 0, true);
+ assert_http (tc);
+}
+
+static void
+test_tls_redirect (TestCase *tc, gconstpointer data)
+{
+ /* Make sure we connect on something other than localhost */
+ tc->server_addr.sin_addr.s_addr = htonl (INADDR_LOOPBACK + 1);
+
+ /* with TLS support it should redirect */
+ const char *res = do_request (tc, "GET / HTTP/1.0\r\nHost: some.remote:1234\r\n\r\n");
+ cockpit_assert_strmatch (res, "HTTP/1.1 301 Moved Permanently*");
+}
+
+static void
+test_tls_client_cert (TestCase *tc, gconstpointer data)
+{
+ assert_https (tc, data, 1);
+ /* no-cert case is handled by separate ws; pass NULL fixture to not use a client cert */
+ assert_https (tc, NULL, 1);
+ assert_https (tc, data, 1);
+}
+
+static void
+test_tls_client_cert_disabled (TestCase *tc, gconstpointer data)
+{
+ assert_https (tc, data, 1);
+ /* no-cert case is handled by same ws, as client certs are disabled server-side;
+ pass NULL fixture to not use a client cert */
+ assert_https (tc, NULL, 1);
+}
+
+static void
+test_tls_client_cert_expired (TestCase *tc, gconstpointer data)
+{
+ /* expect_tls_failure==true only does a coarse-grained check that the request
+ * fails anywhere during handshake or the first send/recv. GnuTLS 3.6.4
+ * introduces TLS 1.3 by default, which has only a two-step handshake: that
+ * does not pick up the server's late failing handshake from the verify
+ * function, only the next read/write attempt does */
+ assert_https_outcome (tc, data, 1, true);
+}
+
+static void
+test_tls_client_cert_parallel (TestCase *tc, gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ gboolean alternate = strcmp (fixture->client_fingerprint, ALTERNATE_FINGERPRINT) == 0;
+ pid_t pid;
+ int status;
+
+ /* HACK: This testcase runs slowly under valgrind and sometimes fails
+ * inexplicably. It's not likely that we're going to find leaks in
+ * the code with this test, so running it under valgrind is just
+ * costing us pain. Disable it for now.
+ */
+ if (cockpit_test_skip_slow ())
+ return;
+
+ block_sigchld ();
+
+ /* do the connection in a subprocess, as gnutls_handshake is synchronous */
+ pid = fork ();
+ if (pid < 0)
+ g_error ("failed to fork: %m");
+ if (pid == 0)
+ {
+ gnutls_certificate_credentials_t xcred;
+ const unsigned n_connections = 20;
+ int fds[n_connections];
+ gnutls_session_t sessions[n_connections];
+
+ g_assert_cmpint (gnutls_certificate_allocate_credentials (&xcred), ==, GNUTLS_E_SUCCESS);
+ g_assert_cmpint (gnutls_certificate_set_x509_system_trust (xcred), >=, 0);
+ g_assert_cmpint (gnutls_certificate_set_x509_key_file (xcred,
+ fixture->client_crt,
+ fixture->client_key,
+ GNUTLS_X509_FMT_PEM),
+ ==, GNUTLS_E_SUCCESS);
+
+ g_assert (!check_for_certfile (tc, NULL));
+
+ /* start parallel connections; we don't actually need to send/receive anything (i. e. talk to cockpit-ws) --
+ * certificate export and refcounting is entirely done on the client → cockpit-tls side */
+ for (unsigned i = 0; i < n_connections; ++i)
+ {
+ fds[i] = do_connect (tc);
+ g_assert_cmpint (fds[i], >, 0);
+
+ g_assert_cmpint (gnutls_init (&sessions[i], GNUTLS_CLIENT), ==, GNUTLS_E_SUCCESS);
+ gnutls_transport_set_int (sessions[i], fds[i]);
+ g_assert_cmpint (gnutls_set_default_priority (sessions[i]), ==, GNUTLS_E_SUCCESS);
+ g_assert_cmpint (gnutls_credentials_set (sessions[i], GNUTLS_CRD_CERTIFICATE, xcred), ==, GNUTLS_E_SUCCESS);
+ gnutls_handshake_set_timeout(sessions[i], 5000);
+ g_assert_cmpint (gnutls_handshake (sessions[i]), ==, GNUTLS_E_SUCCESS);
+
+ /* the file should be written on first connection, and just exist for the next ones.
+ *
+ * In the "alternate" mode we will receive a "hello" message to tell us that the
+ * server is active (by which time the file will have been created). For the
+ * other case, we have to wait for it to appear.
+ * */
+ if (alternate)
+ {
+ char buffer[6];
+ ssize_t s;
+
+ do
+ s = gnutls_record_recv (sessions[i], buffer, sizeof buffer);
+ while (s == GNUTLS_E_INTERRUPTED);
+ g_assert_cmpint (s, ==, 5);
+ g_assert (memcmp (buffer, "hello", 5) == 0);
+ }
+ else
+ {
+ if (i == 0)
+ {
+ for (int retry = 0; retry < 100; ++retry)
+ {
+ if (check_for_certfile (tc, NULL))
+ break;
+ g_usleep (10000);
+ }
+ }
+ }
+
+ g_assert (check_for_certfile (tc, NULL));
+ }
+
+ /* close the connections again, all but the last one */
+ for (unsigned i = 0; i < n_connections - 1; ++i)
+ {
+ g_assert_cmpint (gnutls_bye (sessions[i], GNUTLS_SHUT_RDWR), ==, GNUTLS_E_SUCCESS);
+ close (fds[i]);
+ }
+
+ if (!alternate)
+ {
+ /* The certificate file should still exist for the last connection, but it might
+ * not *yet* exist (if the last connection failed to initialise before all the
+ * other connections exited, which is a race that we've seen in practice). Wait
+ * for it, as above.
+ */
+ for (int retry = 0;; ++retry)
+ {
+ g_assert_cmpint (retry, <, 100);
+
+ if (check_for_certfile (tc, NULL))
+ break;
+ g_usleep (10000);
+ }
+ }
+ else
+ {
+ /* In the "alternate" case there should be no such strange
+ * races.
+ */
+ g_assert (check_for_certfile (tc, NULL));
+ }
+
+ /* closing last connection removes it */
+ g_assert_cmpint (gnutls_bye (sessions[n_connections - 1], GNUTLS_SHUT_RDWR), ==, GNUTLS_E_SUCCESS);
+ close (fds[n_connections - 1]);
+ for (int retry = 0; retry < 100; ++retry)
+ {
+ if (!check_for_certfile (tc, NULL))
+ break;
+ g_usleep (10000);
+ }
+ g_assert (!check_for_certfile (tc, NULL));
+ exit (0);
+ }
+
+ for (int retry = 0; retry < 200 && waitpid (pid, &status, WNOHANG) <= 0; ++retry)
+ server_poll_event (100);
+ g_assert_cmpint (status, ==, 0);
+}
+
+static void
+test_mixed_protocols (TestCase *tc, gconstpointer data)
+{
+ assert_https (tc, data, 1);
+ assert_http (tc);
+ assert_https (tc, data, 1);
+ assert_http (tc);
+}
+
+static void
+test_run_idle (TestCase *tc, gconstpointer data)
+{
+ /* exits after idle without any connections */
+ server_run ();
+
+ /* exits after idle after processing an event */
+ assert_http (tc);
+ server_run ();
+}
+
+int
+main (int argc, char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/server/no-tls/single-request", TestCase, NULL,
+ setup, test_no_tls_single, teardown);
+ g_test_add ("/server/no-tls/many-serial", TestCase, NULL,
+ setup, test_no_tls_many_serial, teardown);
+ g_test_add ("/server/no-tls/many-parallel", TestCase, NULL,
+ setup, test_no_tls_many_parallel, teardown);
+ g_test_add ("/server/no-tls/redirect", TestCase, NULL,
+ setup, test_no_tls_redirect, teardown);
+ g_test_add ("/server/tls/no-client-cert", TestCase, &fixture_separate_crt_key,
+ setup, test_tls_no_client_cert, teardown);
+ g_test_add ("/server/tls/client-cert", TestCase, &fixture_separate_crt_key_client_cert,
+ setup, test_tls_client_cert, teardown);
+ g_test_add ("/server/tls/client-cert-disabled", TestCase, &fixture_separate_crt_key,
+ setup, test_tls_client_cert_disabled, teardown);
+ g_test_add ("/server/tls/client-cert-expired", TestCase, &fixture_expired_client_cert,
+ setup, test_tls_client_cert_expired, teardown);
+ g_test_add ("/server/tls/client-cert-parallel", TestCase, &fixture_separate_crt_key_client_cert,
+ setup, test_tls_client_cert_parallel, teardown);
+ g_test_add ("/server/tls/client-cert-parallel/alternate", TestCase, &fixture_alternate_client_cert,
+ setup, test_tls_client_cert_parallel, teardown);
+ g_test_add ("/server/tls/no-server-cert", TestCase, NULL,
+ setup, test_tls_no_server_cert, teardown);
+ g_test_add ("/server/tls/redirect", TestCase, &fixture_separate_crt_key,
+ setup, test_tls_redirect, teardown);
+ g_test_add ("/server/tls/blocked-handshake", TestCase, &fixture_separate_crt_key,
+ setup, test_tls_blocked_handshake, teardown);
+ g_test_add ("/server/mixed-protocols", TestCase, &fixture_separate_crt_key,
+ setup, test_mixed_protocols, teardown);
+ g_test_add ("/server/run-idle", TestCase, &fixture_run_idle,
+ setup, test_run_idle, teardown);
+
+ return g_test_run ();
+}
diff --git a/src/tls/test-socket-activation-helper.sh b/src/tls/test-socket-activation-helper.sh
new file mode 100755
index 0000000..f871fed
--- /dev/null
+++ b/src/tls/test-socket-activation-helper.sh
@@ -0,0 +1,74 @@
+#!/bin/sh
+set -eu
+
+if ! type curl >/dev/null 2>&1; then
+ echo "1..0 # SKIP: curl not installed"
+ exit 0
+fi
+
+echo "1..5"
+
+# start activation helper
+SOCKET_DIR=$(mktemp -d --tmpdir socks.XXXXXX)
+COCKPIT_WS_PROCESS_IDLE=1 ./socket-activation-helper ./cockpit-ws "$SOCKET_DIR" &
+HELPER_PID=$!
+trap "kill $HELPER_PID; rm -r '$SOCKET_DIR'" EXIT INT QUIT PIPE
+
+# wait until it is ready
+for timeout in `seq 50`; do
+ curl --silent --head --unix-socket "$SOCKET_DIR/http.sock" http://dummy/cockpit/login >/dev/null && break
+ sleep 0.2
+done
+
+# expected results; we can't really login, but it should get sufficiently far
+SUCCESS="HTTP/1.1 [45]0"
+REDIRECT="HTTP/1.1 301"
+
+# args: <socketname> <expected output>
+expect_curl() {
+ OUT=$(curl --silent --show-error --head --unix-socket "$SOCKET_DIR/$1" http://dummy/cockpit/login)
+ if ! echo "$OUT" | grep -q "$2"; then
+ echo "FAIL: output does not contain $2" >&2
+ echo "$OUT" >&2
+ exit 1
+ fi
+}
+
+# args: <instance> <expected output>
+expect_start() {
+ OUT=$(./wsinstance-start "$1" "$SOCKET_DIR")
+ if ! echo "$OUT" | grep -q "$2"; then
+ echo "FAIL: output does not contain $2" >&2
+ echo "$OUT" >&2
+ exit 1
+ fi
+}
+
+SHA256_CERT=$(sed -n '/CLIENT_CERT_FINGERPRINT/ { s/^[^"]*//; s/"//g; p }' $srcdir/src/tls/testing.h)
+SHA256_NIL="$(sha256sum < /dev/null | cut -c1-64)"
+
+expect_curl http.sock "$SUCCESS"
+# second call to existing instance
+expect_curl http.sock "$SUCCESS"
+# wait for idle timeout
+sleep 2
+expect_curl http.sock "$SUCCESS"
+echo "ok 1 http.sock"
+
+
+expect_start $SHA256_NIL "^done$"
+echo "ok 2 https-factory/success"
+
+expect_start "junk" "^fail$"
+echo "ok 3 https-factory/fail"
+
+expect_curl https@$SHA256_NIL.sock "$SUCCESS"
+# second call to existing instance
+expect_curl https@$SHA256_NIL.sock "$SUCCESS"
+# wait for idle timeout
+sleep 2
+expect_curl https@$SHA256_NIL.sock "$SUCCESS"
+echo "ok 4 https@$SHA256_NIL.sock"
+
+expect_curl https@$SHA256_CERT.sock "$SUCCESS"
+echo "ok 5 https@$SHA256_CERT.sock"
diff --git a/src/tls/testing.h b/src/tls/testing.h
new file mode 100644
index 0000000..484ac68
--- /dev/null
+++ b/src/tls/testing.h
@@ -0,0 +1,23 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#define CLIENT_CERT_FINGERPRINT "64b541b74b5c45cac5842677ff22619c0bed9b2bf8e91f65b1379141df0cba17"
+#define ALTERNATE_FINGERPRINT "8206a4f89a4ed1e734190a13c0b6a403b01ace0599f4bcb291884d000a5c9ec7"
diff --git a/src/tls/utils.h b/src/tls/utils.h
new file mode 100644
index 0000000..210f81a
--- /dev/null
+++ b/src/tls/utils.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdio.h>
+
+/* define to 1 to enable debug messages; very verbose! */
+#define DEBUG 0
+
+/* messages can be disabled per-domain */
+#define DEBUG_POLL 0
+#define DEBUG_BUFFER 0
+#define DEBUG_IOVEC 0
+#define DEBUG_CONNECTION 1
+#define DEBUG_SERVER 1
+#define DEBUG_FACTORY 1
+#define DEBUG_SOCKET_IO 1
+
+/* socket-activation-helper.c */
+#define DEBUG_HELPER 1
+
+/* cockpit-certificate-ensure.c */
+#define DEBUG_ENSURE 1
+
+/* testcases */
+#define DEBUG_TESTS 1
+
+#if DEBUG
+#define debug(domain, fmt, ...) do if (DEBUG_##domain) fprintf (stderr, __FILE__ ": " fmt "\n", ##__VA_ARGS__); while (0)
+#else
+#define debug(...)
+#endif
+
+#define N_ELEMENTS(arr) (sizeof (arr) / sizeof ((arr)[0]))
+
+#define SD_LISTEN_FDS_START 3 /* sd_listen_fds(3) */
+
+#define SHA256_NIL "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+#define WSINSTANCE_MAX (64 + 1)
+
diff --git a/src/tls/wsinstance-factory.c b/src/tls/wsinstance-factory.c
new file mode 100644
index 0000000..5815232
--- /dev/null
+++ b/src/tls/wsinstance-factory.c
@@ -0,0 +1,152 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <assert.h>
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <systemd/sd-bus.h>
+#include <systemd/sd-daemon.h>
+
+#include "socket-io.h"
+#include "utils.h"
+
+#define UNIT_MAX 256
+
+static int
+match_job_removed (sd_bus_message *message,
+ void *user_data,
+ sd_bus_error *error)
+{
+ const char **job_path = user_data;
+ const char *result;
+ const char *path;
+
+ debug (FACTORY, "Received JobRemoved signal:");
+
+ if (sd_bus_message_read (message, "uoss", NULL, &path, NULL, &result) < 0)
+ return 0;
+
+ debug (FACTORY, " -> path: %s, result: %s", path, result);
+
+ if (!*job_path || strcmp (path, *job_path) != 0)
+ return 0;
+
+ /* This is our job. */
+ debug (FACTORY, " -> sending result.");
+ send_all (SD_LISTEN_FDS_START, result, strlen (result), 5 * 1000000);
+ *job_path = NULL;
+
+ return 0;
+}
+
+int
+main (void)
+{
+ char instance[WSINSTANCE_MAX];
+ sd_bus_error error = SD_BUS_ERROR_NULL;
+ sd_bus *bus = NULL;
+ char unit[UNIT_MAX + 1];
+ sd_bus_message *reply = NULL;
+ const char *job_path = NULL;
+ char **fdnames;
+ int r;
+
+ if (sd_listen_fds_with_names (false, &fdnames) != 1 || strcmp (fdnames[0], "connection") != 0)
+ errx (EXIT_FAILURE, "Must be spawned from a systemd service on a socket with Accept=yes %s", fdnames[0]);
+
+ if (!recv_alnum (SD_LISTEN_FDS_START, instance, sizeof instance, 10 * 1000000))
+ errx (EXIT_FAILURE, "Didn't receive fingerprint");
+
+ r = sd_bus_open_system (&bus);
+ if (r < 0)
+ errx (EXIT_FAILURE, "Failed to connect to system bus: %s", strerror (-r));
+
+ /* We use the job_path variable to communicate with the match function
+ * in two ways:
+ *
+ * - we set it to the path of the job that we're waiting to exit so
+ * that the match function knows which signal is for us
+ *
+ * - once the job is removed, the match function clears the variable
+ * back to NULL. that's how we know when to stop waiting.
+ *
+ * In effect, the duration of job_path being set to non-NULL is more
+ * or less equal to the duration of the existence of a job object at
+ * that path.
+ */
+ r = sd_bus_match_signal_async (bus, NULL,
+ "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager", "JobRemoved",
+ match_job_removed, NULL, &job_path);
+ if (r < 0)
+ errx (EXIT_FAILURE, "Failed to install match rule: %s", strerror (-r));
+
+ /* can't fail, because instance is small */
+ r = snprintf (unit, sizeof unit, "cockpit-wsinstance-https@%s.socket", instance);
+ assert (0 < r && r < sizeof unit);
+
+ debug (FACTORY, "Requesting start of unit %s", unit);
+ r = sd_bus_call_method (bus,
+ "org.freedesktop.systemd1", "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager", "StartUnit",
+ &error, &reply, "ss", unit, "replace");
+ if (r < 0)
+ errx (EXIT_FAILURE, "Method call failed: %s", error.message);
+
+ r = sd_bus_message_read (reply, "o", &job_path);
+ if (r < 0)
+ errx (EXIT_FAILURE, "Invalid message response: %s", strerror (-r));
+
+ debug (FACTORY, " -> job is %s", job_path);
+ debug (FACTORY, "Waiting for signal.");
+
+ do
+ r = sd_bus_process (bus, NULL);
+ while (r > 0);
+ if (r < 0)
+ errx (EXIT_FAILURE, "sd_bus_process() failed: %s", strerror (-r));
+
+ struct timespec start = { 0, 0 };
+ uint64_t remaining;
+ while (job_path && get_remaining_timeout (&start, &remaining, 20 * 1000000))
+ {
+ debug (FACTORY, "sd_bus_wait(%llu)", (long long) remaining);
+ r = sd_bus_wait (bus, remaining);
+ if (r < 0)
+ errx (EXIT_FAILURE, "Error while waiting for bus: %s", strerror (-r));
+
+ debug (FACTORY, "sd_bus_process():");
+ do
+ r = sd_bus_process (bus, NULL);
+ while (r > 0);
+ if (r < 0)
+ errx (EXIT_FAILURE, "sd_bus_process() failed: %s", strerror (-r));
+ debug (FACTORY, " -> done.");
+ }
+
+ sd_bus_message_unref (reply);
+ sd_bus_close (bus);
+ sd_bus_unref (bus);
+
+ return 0;
+}
diff --git a/src/tls/wsinstance-start.c b/src/tls/wsinstance-start.c
new file mode 100644
index 0000000..3799446
--- /dev/null
+++ b/src/tls/wsinstance-start.c
@@ -0,0 +1,70 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <err.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include "socket-io.h"
+
+int
+main (int argc,
+ char **argv)
+{
+ const char *wsinstance_sockdir = "/run/cockpit/wsinstance";
+ int dirfd;
+ char result[20];
+ int fd;
+
+ if (argc != 2 && argc != 3)
+ errx (EXIT_FAILURE, "usage: ./wsinstance-start [instanceid] [wsinstance_sockdir]");
+
+ if (argc == 3)
+ wsinstance_sockdir = argv[2];
+
+ dirfd = open (wsinstance_sockdir, O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (dirfd == -1)
+ err (EXIT_FAILURE, "Couldn't open wsinstance_sockdir %s", wsinstance_sockdir);
+
+ fd = socket (AF_UNIX, SOCK_STREAM, 0);
+ if (fd == -1)
+ err (EXIT_FAILURE, "Couldn't create AF_UNIX socket");
+
+ if (af_unix_connectat (fd, dirfd, "https-factory.sock"))
+ err (EXIT_FAILURE, "Couldn't connect to factory socket");
+
+ if (!send_all (fd, argv[1], strlen (argv[1]), 50 * 1000000))
+ errx (EXIT_FAILURE, "Couldn't send instance name");
+
+ if (!recv_alnum (fd, result, sizeof result, 30 * 1000000))
+ errx (EXIT_FAILURE, "Failed to receive result");
+
+ printf ("%s\n", result);
+
+ close (dirfd);
+ close (fd);
+
+ return 0;
+}
diff --git a/src/websocket/Makefile-websocket.am b/src/websocket/Makefile-websocket.am
new file mode 100644
index 0000000..c173eb4
--- /dev/null
+++ b/src/websocket/Makefile-websocket.am
@@ -0,0 +1,65 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+# Note: Because of its use of cockpitflow, libwebsocket_a_LIBS should
+# rightfully include a reference to libcockpit_common_a_LIBS, but it
+# can't do this because libcockpit-common.a depends, in turn, on
+# libwebsocket.
+#
+# At this point, libwebsocket should probably just be merged into
+# libcockpit-common, but we can't do that because the log domain is
+# different, and some of the tests depend on that.
+
+# -----------------------------------------------------------------------------
+# libwebsocket.a: low-level websocket handling code
+
+noinst_LIBRARIES += libwebsocket.a
+
+libwebsocket_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"WebSocket\" \
+ $(glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libwebsocket_a_LIBS = \
+ libwebsocket.a \
+ $(glib_LIBS) \
+ $(NULL)
+
+libwebsocket_a_SOURCES = \
+ src/websocket/websocket.h \
+ src/websocket/websocket.c \
+ src/websocket/websocketclient.h \
+ src/websocket/websocketclient.c \
+ src/websocket/websocketserver.h \
+ src/websocket/websocketserver.c \
+ src/websocket/websocketconnection.h \
+ src/websocket/websocketconnection.c \
+ src/websocket/websocketprivate.h \
+ $(NULL)
+
+# -----------------------------------------------------------------------------
+# Unit tests
+
+check_PROGRAMS += frob-websocket
+frob_websocket_CPPFLAGS = $(libwebsocket_a_CPPFLAGS) $(TEST_CPP)
+frob_websocket_LDADD = $(libwebsocket_a_LIBS) $(TEST_LIBS)
+frob_websocket_SOURCES = src/websocket/frob-websocket.c
+
+TEST_PROGRAM += test-websocket
+test_websocket_CPPFLAGS = $(libwebsocket_a_CPPFLAGS) $(TEST_CPP)
+test_websocket_LDADD = $(libwebsocket_a_LIBS) $(TEST_LIBS)
+test_websocket_SOURCES = src/websocket/test-websocket.c
diff --git a/src/websocket/frob-websocket.c b/src/websocket/frob-websocket.c
new file mode 100644
index 0000000..34935fe
--- /dev/null
+++ b/src/websocket/frob-websocket.c
@@ -0,0 +1,164 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "websocket.h"
+
+static WebSocketConnection *web_socket = NULL;
+static GString *buffer = NULL;
+static GMainLoop *loop = NULL;
+
+static void
+on_release_buffer (gpointer user_data)
+{
+ if (buffer == NULL)
+ buffer = user_data;
+ else
+ g_string_free (user_data, TRUE);
+}
+
+static gboolean
+on_input_data (GIOChannel *channel,
+ GIOCondition cond,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ GIOStatus status;
+ GBytes *msg;
+ gsize line;
+
+ if (buffer == NULL)
+ buffer = g_string_sized_new (1024);
+ status = g_io_channel_read_line_string (channel, buffer, &line, &error);
+ switch (status)
+ {
+ case G_IO_STATUS_ERROR:
+ g_critical ("Failed to read input: %s", error->message);
+ g_error_free (error);
+ g_main_loop_quit (loop);
+ return FALSE;
+ case G_IO_STATUS_EOF:
+ web_socket_connection_close (web_socket, WEB_SOCKET_CLOSE_GOING_AWAY, "going away");
+ return FALSE;
+ case G_IO_STATUS_AGAIN:
+ return TRUE;
+ case G_IO_STATUS_NORMAL:
+ msg = g_bytes_new_with_free_func (buffer->str, line, on_release_buffer, buffer);
+ buffer = NULL;
+ web_socket_connection_send (web_socket, WEB_SOCKET_DATA_TEXT, NULL, msg);
+ g_bytes_unref (msg);
+ return TRUE;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+on_web_socket_open (WebSocketConnection *ws,
+ gpointer unused)
+{
+ GIOChannel *channel;
+
+ g_printerr ("WebSocket: opened %s with %s\n",
+ web_socket_connection_get_protocol (ws),
+ web_socket_connection_get_url (ws));
+
+ channel = g_io_channel_unix_new (0);
+ g_io_add_watch (channel, G_IO_IN, on_input_data, NULL);
+ g_io_channel_unref (channel);
+}
+
+static void
+on_web_socket_message (WebSocketConnection *ws,
+ WebSocketDataType type,
+ GBytes *message)
+{
+ const gchar *data;
+ gsize len;
+
+ g_printerr ("WebSocket: message 0x%x\n", (int)type);
+
+ data = g_bytes_get_data (message, &len);
+ g_print ("%.*s\n", (int)len, data);
+}
+
+static void
+on_web_socket_close (WebSocketConnection *ws)
+{
+ gushort code;
+
+ code = web_socket_connection_get_close_code (ws);
+ if (code != 0)
+ g_printerr ("WebSocket: close: %d %s\n", code,
+ web_socket_connection_get_close_data (ws));
+ else
+ g_printerr ("WebSocket: close\n");
+
+ g_main_loop_quit (loop);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ GOptionContext *options;
+ gchar **protocols = NULL;
+ gchar *origin = NULL;
+ GError *error = NULL;
+
+ GOptionEntry entries[] = {
+ { "origin", 0, 0, G_OPTION_ARG_STRING, &origin, "Web Socket Origin", "url" },
+ { "protocol", 0, 0, G_OPTION_ARG_STRING_ARRAY, &protocols, "Web Socket Protocols", "proto" },
+ { NULL }
+ };
+
+ signal (SIGPIPE, SIG_IGN);
+ options = g_option_context_new ("URL");
+ g_option_context_add_main_entries (options, entries, NULL);
+ if (!g_option_context_parse (options, &argc, &argv, &error))
+ {
+ g_printerr ("frob-websocket: %s\n", error->message);
+ return 2;
+ }
+
+ if (argc != 2)
+ {
+ g_printerr ("frob-websocket: specify the url to connect to\n");
+ return 2;
+ }
+
+ loop = g_main_loop_new (NULL, FALSE);
+
+ web_socket = web_socket_client_new (argv[1], origin, (const gchar **)protocols);
+ g_signal_connect (web_socket, "open", G_CALLBACK (on_web_socket_open), NULL);
+ g_signal_connect (web_socket, "message", G_CALLBACK (on_web_socket_message), NULL);
+ g_signal_connect (web_socket, "close", G_CALLBACK (on_web_socket_close), NULL);
+
+ g_main_loop_run (loop);
+
+ g_option_context_free (options);
+ g_object_unref (web_socket);
+ g_free (origin);
+ if (buffer)
+ g_string_free (buffer, TRUE);
+ g_strfreev (protocols);
+
+ return 0;
+}
diff --git a/src/websocket/test-websocket.c b/src/websocket/test-websocket.c
new file mode 100644
index 0000000..f3eb514
--- /dev/null
+++ b/src/websocket/test-websocket.c
@@ -0,0 +1,1334 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "websocket.h"
+#include "websocketprivate.h"
+
+#include "common/cockpitflow.h"
+#include "common/cockpitsocket.h"
+#include "testlib/mock-pressure.h"
+
+#include <string.h>
+
+typedef struct {
+ WebSocketConnection *client;
+ WebSocketConnection *server;
+} Test;
+
+static void
+null_log_handler (const gchar *log_domain,
+ GLogLevelFlags log_level,
+ const gchar *message,
+ gpointer user_data)
+{
+ /*
+ * HACK: Use g_test_expect_message() to quieten things down once this lands:
+ * https://bugzilla.gnome.org/show_bug.cgi?id=710991
+ */
+}
+
+#define WAIT_UNTIL(cond) \
+ G_STMT_START \
+ while (!(cond)) g_main_context_iteration (NULL, TRUE); \
+ G_STMT_END
+
+static void
+test_parse_url (void)
+{
+ gboolean ret;
+ GError *error = NULL;
+ gchar *host;
+ gchar *path;
+ gchar *scheme;
+
+ ret = _web_socket_util_parse_url ("scheme://host:port/path/part",
+ &scheme, &host, &path, &error);
+ g_assert_no_error (error);
+ g_assert (ret == TRUE);
+ g_assert_cmpstr (scheme, ==, "scheme");
+ g_assert_cmpstr (host, ==, "host:port");
+ g_assert_cmpstr (path, ==, "/path/part");
+ g_free (scheme);
+ g_free (host);
+ g_free (path);
+}
+
+static void
+test_parse_url_no_out (void)
+{
+ gboolean ret;
+ GError *error = NULL;
+
+ ret = _web_socket_util_parse_url ("scheme://host:port/path/part",
+ NULL, NULL, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (ret == TRUE);
+}
+
+static void
+test_parse_url_bad (void)
+{
+ const gchar *bads[] = {
+ "/host:port/path/part",
+ "http://@/",
+ "http:///",
+ "http://",
+ };
+
+ gboolean ret;
+ GError *error = NULL;
+ gint i;
+
+ for (i = 0; i < G_N_ELEMENTS (bads); i++)
+ {
+ ret = _web_socket_util_parse_url (bads[i], NULL, NULL, NULL, &error);
+ g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT);
+ g_assert (ret == FALSE);
+ g_clear_error (&error);
+ }
+}
+
+static void
+test_parse_url_no_path (void)
+{
+ gboolean ret;
+ gchar *path;
+
+ ret = _web_socket_util_parse_url ("scheme://host:port",
+ NULL, NULL, &path, NULL);
+ g_assert (ret == TRUE);
+ g_assert_cmpstr (path, ==, "/");
+ g_free (path);
+}
+
+static void
+test_parse_url_with_user (void)
+{
+ gboolean ret;
+ gchar *host;
+
+ ret = _web_socket_util_parse_url ("scheme://user:password@host",
+ NULL, &host, NULL, NULL);
+ g_assert (ret == TRUE);
+ g_assert_cmpstr (host, ==, "host");
+ g_free (host);
+}
+
+static void
+test_parse_req (void)
+{
+ const gchar *reqs[] = {
+ "GET /path/part HTTP/1.0\r\n ",
+ "GET /path/part HTTP/1.0\n ",
+ "GET /path/part HTTP/1.0 \r\n ",
+ };
+
+ for (gint i = 0; i < G_N_ELEMENTS (reqs); i++)
+ {
+ gchar *path;
+ gchar *method;
+
+ gssize ret = web_socket_util_parse_req_line (reqs[i], strlen (reqs[i]), &method, &path);
+ g_assert_cmpint (ret, ==, strlen (reqs[i]) - 2);
+ g_assert_cmpstr (method, ==, "GET");
+ g_assert_cmpstr (path, ==, "/path/part");
+ g_free (method);
+ g_free (path);
+ }
+}
+
+static void
+test_parse_req_no_out (void)
+{
+ const gchar *data = "GET /path/part HTTP/1.0\r\n ";
+ gssize ret;
+
+ ret = web_socket_util_parse_req_line (data, strlen (data), NULL, NULL);
+ g_assert_cmpint (ret, ==, 25);
+}
+
+static void
+test_parse_req_not_enough (void)
+{
+ const gchar *data = "GET /path/par";
+ gssize ret;
+
+ ret = web_socket_util_parse_req_line (data, strlen (data), NULL, NULL);
+ g_assert_cmpint (ret, ==, 0);
+}
+
+static void
+test_parse_req_bad (void)
+{
+ const gchar *bads[] = {
+ " GET /path/part HTTP/1.0\r\n ",
+ "GET /path/part\r\n ",
+ "GET /path/part HTTP/4.4\r\n ",
+ "GET /path/part HTTP/1.0X\r\n ",
+ "GET /path/part XXX/2\r\n ",
+ "TESTONE\r\n ",
+ };
+
+ gssize ret;
+ gint i;
+
+ for (i = 0; i < G_N_ELEMENTS (bads); i++)
+ {
+ ret = web_socket_util_parse_req_line (bads[i], strlen (bads[i]), NULL, NULL);
+ g_assert_cmpint (ret, ==, -1);
+ }
+}
+
+static void
+test_parse_status (void)
+{
+ const gchar *lines[] = {
+ "HTTP/1.0 101 Switching Protocols\r\n ",
+ "HTTP/1.0 101 Switching Protocols\n ",
+ "HTTP/1.1 101 Switching Protocols \r\n ",
+ };
+
+ for (gint i = 0; i < G_N_ELEMENTS (lines); i++)
+ {
+ guint status;
+ gchar *reason;
+
+ gssize ret = web_socket_util_parse_status_line (lines[i], strlen (lines[i]), NULL, &status, &reason);
+ g_assert_cmpint (ret, ==, strlen (lines[i]) - 2);
+ g_assert_cmpuint (status, ==, 101);
+ g_assert_cmpstr (reason, ==, "Switching Protocols");
+ g_free (reason);
+ }
+}
+
+static void
+test_parse_status_no_out (void)
+{
+ const gchar *line = "HTTP/1.0 101 Switching Protocols\r\n ";
+ gssize ret;
+
+ ret = web_socket_util_parse_status_line (line, strlen (line), NULL, NULL, NULL);
+ g_assert_cmpint (ret, ==, strlen (line) - 2);
+}
+
+static void
+test_parse_status_not_enough (void)
+{
+ const gchar *data = "HTTP/";
+ gssize ret;
+
+ ret = web_socket_util_parse_status_line (data, strlen (data), NULL, NULL, NULL);
+ g_assert_cmpint (ret, ==, 0);
+}
+
+static void
+test_parse_status_bad (void)
+{
+ const gchar *lines[] = {
+ " HTTP/1.0 101 Switching Protocols\r\n ",
+ "HTTP/1.0 101\r\n ",
+ "HTTP/1.1 1A01 Switching Protocols \r\n ",
+ "TESTONE\r\n ",
+ };
+
+ gssize ret;
+ gint i;
+
+ for (i = 0; i < G_N_ELEMENTS (lines); i++)
+ {
+ ret = web_socket_util_parse_status_line (lines[i], strlen (lines[i]), NULL, NULL, NULL);
+ g_assert_cmpint (ret, ==, -1);
+ }
+}
+
+static void
+test_parse_version_1_0 (void)
+{
+ gchar *version;
+ const gchar *line;
+ gssize ret;
+
+ line = "HTTP/1.0 101 Switching Protocols\r\n ";
+ ret = web_socket_util_parse_status_line (line, strlen (line), &version, NULL, NULL);
+ g_assert_cmpint (ret, ==, strlen (line) - 2);
+ g_assert_cmpstr (version, ==, "HTTP/1.0");
+ g_free (version);
+}
+
+static void
+test_parse_version_1_1 (void)
+{
+ gchar *version;
+ const gchar *line;
+ gssize ret;
+
+ line = "HTTP/1.1 101 Switching Protocols\r\n ";
+ ret = web_socket_util_parse_status_line (line, strlen (line), &version, NULL, NULL);
+ g_assert_cmpint (ret, ==, strlen (line) - 2);
+ g_assert_cmpstr (version, ==, "HTTP/1.1");
+ g_free (version);
+}
+
+static void
+test_parse_headers (void)
+{
+ GHashTable *headers;
+ gssize ret;
+
+ const gchar *input =
+ "Header1: value3\r\n"
+ "Header2: field\r\n"
+ "Head3: Another \r\n"
+ "Host:https://cockpit-project.org\r\n"
+ "Funny: a☺b\r\n"
+ "\r\n"
+ "BODY ";
+
+ ret = web_socket_util_parse_headers (input, strlen (input), &headers);
+ g_assert_cmpint (ret, ==, strlen (input) - 6);
+ g_assert_cmpstr (g_hash_table_lookup (headers, "header1"), ==, "value3");
+ g_assert_cmpstr (g_hash_table_lookup (headers, "Header2"), ==, "field");
+ g_assert_cmpstr (g_hash_table_lookup (headers, "hEAD3"), ==, "Another");
+ g_assert_cmpstr (g_hash_table_lookup (headers, "Host"), ==, "https://cockpit-project.org");
+ g_assert_cmpstr (g_hash_table_lookup (headers, "Funny"), ==, "a☺b");
+ g_assert (g_hash_table_lookup (headers, "Something else") == NULL);
+ g_hash_table_unref (headers);
+}
+
+static void
+test_parse_duplicate_headers (void)
+{
+ GHashTable *headers;
+ gssize ret;
+
+ const gchar *input =
+ "header1: value2\r\n"
+ "Header1: value3\r\n"
+ "\r\n"
+ "BODY ";
+
+ ret = web_socket_util_parse_headers (input, strlen (input), &headers);
+ g_assert_cmpint (ret, ==, strlen (input) - 6);
+ g_assert_cmpstr (g_hash_table_lookup (headers, "header1"), ==, "value3");
+ g_assert (g_hash_table_lookup (headers, "Something else") == NULL);
+ g_hash_table_unref (headers);
+}
+
+static void
+test_parse_headers_no_out (void)
+{
+ const gchar *input =
+ "Header1: value3\r\n"
+ "Header2: field\r\n"
+ "Head3: Another \r\n"
+ "\r\n"
+ "BODY ";
+
+ gssize ret;
+
+ ret = web_socket_util_parse_headers (input, strlen (input), NULL);
+ g_assert_cmpint (ret, ==, strlen (input) - 6);
+}
+
+static void
+test_parse_headers_not_enough (void)
+{
+ const gchar *input =
+ "Header1: value3\r\n"
+ "Header2: field\r\n"
+ "Head3: Another";
+
+ gssize ret;
+
+ ret = web_socket_util_parse_headers (input, strlen (input), NULL);
+ g_assert_cmpint (ret, ==, 0);
+}
+
+static void
+test_parse_headers_bad (void)
+{
+ const gchar *input[] = {
+ /* missing : */
+ "Header1 value3\r\n"
+ "\r\n"
+ "BODY ",
+
+ /* binary garbage (not even UTF8) */
+ "Header1: a\xFF\x01b\r\n"
+ "\r\n"
+ "BODY ",
+ };
+
+ gssize ret;
+ gint i;
+
+ for (i = 0; i < G_N_ELEMENTS (input); i++)
+ {
+ ret = web_socket_util_parse_headers (input[i], strlen (input[i]), NULL);
+ g_assert_cmpint (ret, ==, -1);
+ }
+}
+
+static void
+test_header_equals (void)
+{
+ GHashTable *headers = web_socket_util_new_headers ();
+ g_hash_table_insert (headers, g_strdup ("Blah"), g_strdup ("VALUE"));
+ g_hash_table_insert (headers, g_strdup ("Funny"), g_strdup ("a☺b"));
+
+ g_assert (_web_socket_util_header_equals (headers, "blah", "Value"));
+ g_assert (_web_socket_util_header_equals (headers, "Funny", "a☺b"));
+
+ g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE,
+ "received invalid or missing Blah header*");
+ g_assert (!_web_socket_util_header_equals (headers, "Blah", "test"));
+
+ g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE,
+ "received invalid or missing Extra header*");
+ g_assert (!_web_socket_util_header_equals (headers, "Extra", "test"));
+ g_hash_table_unref (headers);
+}
+
+static void
+test_header_contains (void)
+{
+ GHashTable *headers = web_socket_util_new_headers ();
+ g_hash_table_insert (headers, g_strdup ("Blah"), g_strdup ("one two three"));
+
+ g_assert (_web_socket_util_header_contains (headers, "blah", "one"));
+ g_assert (_web_socket_util_header_contains (headers, "blah", "two"));
+ g_assert (_web_socket_util_header_contains (headers, "blah", "three"));
+
+ g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE,
+ "received invalid or missing Blah header*");
+ g_assert (!_web_socket_util_header_contains (headers, "Blah", "thre"));
+
+ g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE,
+ "received invalid or missing Blah header*");
+ g_assert (!_web_socket_util_header_contains (headers, "Blah", "four"));
+
+ g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE,
+ "received invalid or missing Extra header*");
+ g_assert (!_web_socket_util_header_contains (headers, "Extra", "test"));
+ g_hash_table_unref (headers);
+}
+
+static void
+test_header_empty (void)
+{
+ GHashTable *headers = web_socket_util_new_headers ();
+ g_hash_table_insert (headers, g_strdup ("Empty"), g_strdup (""));
+ g_hash_table_insert (headers, g_strdup ("Blah"), g_strdup ("value"));
+
+ g_assert (_web_socket_util_header_empty (headers, "empty"));
+ g_assert (_web_socket_util_header_empty (headers, "Another"));
+
+ g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE,
+ "received unsupported Blah header*");
+ g_assert (!_web_socket_util_header_empty (headers, "Blah"));
+ g_hash_table_unref (headers);
+}
+
+static gboolean
+on_error_not_reached (WebSocketConnection *ws,
+ GError *error,
+ gpointer user_data)
+{
+ /* At this point we know this will fail, but is informative */
+ g_assert_no_error (error);
+ return TRUE;
+}
+
+static gboolean
+on_error_copy (WebSocketConnection *ws,
+ GError *error,
+ gpointer user_data)
+{
+ GError **copy = user_data;
+ g_assert (*copy == NULL);
+ *copy = g_error_copy (error);
+ return TRUE;
+}
+
+static void
+setup_pair (Test *test,
+ gconstpointer data)
+{
+ GIOStream *ioc;
+ GIOStream *ios;
+
+ cockpit_socket_streampair (&ioc, &ios);
+
+ test->server = web_socket_server_new_for_stream ("ws://localhost/unix", NULL, NULL, ios, NULL, NULL);
+ test->client = web_socket_client_new_for_stream ("ws://localhost/unix", NULL, NULL, ioc);
+
+ g_signal_connect (test->server, "error", G_CALLBACK (on_error_not_reached), NULL);
+
+ g_object_unref (ioc);
+ g_object_unref (ios);
+}
+
+static void
+teardown (Test *test,
+ gconstpointer data)
+{
+ g_clear_object (&test->client);
+ g_clear_object (&test->server);
+}
+
+static gboolean
+on_timeout_set_flag (gpointer user_data)
+{
+ gboolean *data = user_data;
+ g_assert (user_data);
+ g_assert (*data == FALSE);
+ *data = TRUE;
+ return FALSE;
+}
+
+static void
+on_text_message (WebSocketConnection *ws,
+ WebSocketDataType type,
+ GBytes *message,
+ gpointer user_data)
+{
+ GBytes **receive = user_data;
+ g_assert_cmpint (type, ==, WEB_SOCKET_DATA_TEXT);
+ g_assert (*receive == NULL);
+ g_assert (message != NULL);
+ *receive = g_bytes_ref (message);
+}
+
+static void
+on_message_append (WebSocketConnection *ws,
+ WebSocketDataType type,
+ GBytes *message,
+ gpointer user_data)
+{
+ GByteArray *received = user_data;
+ g_assert (received != NULL);
+ g_byte_array_append (received, g_bytes_get_data (message, NULL), g_bytes_get_size (message));
+}
+
+static void
+on_close_set_flag (WebSocketConnection *ws,
+ gpointer user_data)
+{
+ gboolean *flag = user_data;
+ g_assert (*flag == FALSE);
+ *flag = TRUE;
+}
+
+static void
+on_open_set_flag (WebSocketConnection *ws,
+ gpointer user_data)
+{
+ gboolean *flag = user_data;
+ g_assert (*flag == FALSE);
+ *flag = TRUE;
+}
+
+static void
+test_handshake (Test *test,
+ gconstpointer data)
+{
+ gboolean open_event_client = FALSE;
+ gboolean open_event_server = FALSE;
+ GHashTable *headers;
+
+ g_signal_connect (test->client, "open", G_CALLBACK (on_open_set_flag), &open_event_client);
+ g_signal_connect (test->server, "open", G_CALLBACK (on_open_set_flag), &open_event_server);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->client), ==, WEB_SOCKET_STATE_OPEN);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_OPEN);
+
+ headers = web_socket_client_get_headers (WEB_SOCKET_CLIENT (test->client));
+ g_assert (headers != NULL);
+#if 0
+ GHashTableIter iter;
+ gpointer key, value;
+ g_hash_table_iter_init (&iter, headers);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ g_printerr ("headers: %s %s\n", (gchar *)key, (gchar *)value);
+#endif
+ g_assert_cmpstr (g_hash_table_lookup (headers, "connection"), ==, "Upgrade");
+
+ g_assert (open_event_client);
+ g_assert (open_event_server);
+}
+
+static void
+test_send_client_to_server (Test *test,
+ gconstpointer data)
+{
+ GBytes *sent = NULL;
+ GBytes *received = NULL;
+ const gchar *contents;
+ gsize len;
+
+ g_signal_connect (test->server, "message", G_CALLBACK (on_text_message), &received);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->client), ==, WEB_SOCKET_STATE_OPEN);
+
+ sent = g_bytes_new ("this is a test", 14);
+ web_socket_connection_send (test->client, WEB_SOCKET_DATA_TEXT, NULL, sent);
+
+ WAIT_UNTIL (received != NULL);
+
+ g_assert (g_bytes_equal (sent, received));
+
+ /* Received messages should be null terminated (outside of len) */
+ contents = g_bytes_get_data (received, &len);
+ g_assert (contents[len] == '\0');
+
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+}
+
+static void
+test_send_server_to_client (Test *test,
+ gconstpointer data)
+{
+ GBytes *sent = NULL;
+ GBytes *received = NULL;
+ const gchar *contents;
+ gsize len;
+
+ g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_OPEN);
+
+ sent = g_bytes_new ("this is a test", 14);
+ web_socket_connection_send (test->server, WEB_SOCKET_DATA_TEXT, NULL, sent);
+
+ WAIT_UNTIL (received != NULL);
+
+ g_assert (g_bytes_equal (sent, received));
+
+ /* Received messages should be null terminated (outside of len) */
+ contents = g_bytes_get_data (received, &len);
+ g_assert (contents[len] == '\0');
+
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+}
+
+static void
+test_send_big_packets (Test *test,
+ gconstpointer data)
+{
+ GBytes *sent = NULL;
+ GBytes *received = NULL;
+
+ g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_OPEN);
+
+ sent = g_bytes_new_take (g_strnfill (400, '!'), 400);
+ web_socket_connection_send (test->server, WEB_SOCKET_DATA_TEXT, NULL, sent);
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (sent, received));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ sent = g_bytes_new_take (g_strnfill (100 * 1000, '?'), 100 * 1000);
+ web_socket_connection_send (test->server, WEB_SOCKET_DATA_TEXT, NULL, sent);
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (sent, received));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+}
+
+static void
+on_pressure_set_throttle (WebSocketConnection *socket,
+ gboolean throttle,
+ gpointer user_data)
+{
+ gint *data = user_data;
+ g_assert (user_data != NULL);
+ *data = throttle ? 1 : 0;
+}
+
+static void
+test_pressure_queue (Test *test,
+ gconstpointer data)
+{
+ GBytes *sent = NULL;
+ gint throttle = -1;
+ gint i;
+
+ g_signal_connect (test->server, "pressure", G_CALLBACK (on_pressure_set_throttle), &throttle);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_OPEN);
+
+ sent = g_bytes_new_take (g_strnfill (10 * 1000, '!'), 10 * 1000);
+ for (i = 0; i < 1000; i++)
+ web_socket_connection_send (test->server, WEB_SOCKET_DATA_TEXT, NULL, sent);
+ g_bytes_unref (sent);
+
+ /*
+ * This should have put way too much in the queue, and thus
+ * emitted the back-pressure signal. This signal would normally
+ * be used by others to slow down their queueing, but in this
+ * case we just check that it was fired.
+ */
+ g_assert_cmpint (throttle, ==, 1);
+ throttle = -1;
+
+ /*
+ * Now the queue is getting drained. At some point, it will be
+ * signaled that back pressure has been turned off
+ */
+ while (throttle == -1)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_cmpint (throttle, ==, 0);
+}
+
+static void
+test_pressure_throttle (Test *test,
+ gconstpointer data)
+{
+ CockpitFlow *pressure = mock_pressure_new ();
+ GBytes *sent = NULL;
+ GByteArray *received = NULL;
+ gboolean timeout = FALSE;
+ gsize length;
+ gint i;
+
+ received = g_byte_array_new ();
+ cockpit_flow_throttle (COCKPIT_FLOW (test->client), pressure);
+ g_signal_connect (test->client, "message", G_CALLBACK (on_message_append), received);
+
+ while (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_CONNECTING)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_OPEN);
+
+ /* Send this a thousand times over the socket */
+ sent = g_bytes_new_take (g_strnfill (10 * 1000, '?'), 10 * 1000);
+ for (i = 0; i < 1000; i++)
+ web_socket_connection_send (test->server, WEB_SOCKET_DATA_TEXT, NULL, sent);
+ g_bytes_unref (sent);
+
+ /*
+ * So we should start receiving the echoed data. But we apply
+ * the throttle pressure after receiving some data, and the rest
+ * just waits.
+ */
+ while (received->len == 0)
+ g_main_context_iteration (NULL, TRUE);
+ g_signal_emit_by_name (pressure, "pressure", TRUE);
+
+ length = received->len;
+ g_assert_cmpint (length, <, 10 * 1000 * 1000);
+
+ /* Now remaining data input should wait, no further data received*/
+ g_timeout_add_seconds (2, on_timeout_set_flag, &timeout);
+ while (timeout == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (length, ==, received->len);
+
+ /* Remove the pressure, and we should get more data */
+ g_signal_emit_by_name (pressure, "pressure", FALSE);
+ while (length < received->len)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Clearing the throttle should work too. This pressure signal has no effect */
+ cockpit_flow_throttle (COCKPIT_FLOW (test->client), NULL);
+ g_signal_emit_by_name (pressure, "pressure", TRUE);
+
+ /* Now wait for the remaining data */
+ while (received->len < 10 * 1000 * 1000)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_byte_array_free (received, TRUE);
+ g_object_unref (pressure);
+}
+
+static void
+test_send_prefixed (Test *test,
+ gconstpointer data)
+{
+ GBytes *prefix = NULL;
+ GBytes *payload = NULL;
+ GBytes *received = NULL;
+
+ g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_OPEN);
+
+ prefix = g_bytes_new_static ("funny ", 6);
+ payload = g_bytes_new_static ("thing", 5);
+
+ web_socket_connection_send (test->server, WEB_SOCKET_DATA_TEXT, prefix, payload);
+ WAIT_UNTIL (received != NULL);
+ g_assert_cmpstr (g_bytes_get_data (received, NULL), ==, "funny thing");
+ g_assert_cmpint (g_bytes_get_size (received), ==, 11);
+ g_bytes_unref (payload);
+ g_bytes_unref (prefix);
+ g_bytes_unref (received);
+}
+
+static void
+test_send_bad_data (Test *test,
+ gconstpointer unused)
+{
+ GError *error = NULL;
+ GIOStream *io;
+ gsize written;
+ const gchar *frame;
+ guint logid;
+
+ g_signal_handlers_disconnect_by_func (test->server, on_error_not_reached, NULL);
+ g_signal_connect (test->server, "error", G_CALLBACK (on_error_copy), &error);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING);
+
+ io = web_socket_connection_get_io_stream (test->client);
+
+ logid = g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, null_log_handler, NULL);
+
+ /* Bad UTF-8 raw frames */
+ frame = "\x81\x04\xEE\xEE\xEE\xEE";
+
+ if (!g_output_stream_write_all (g_io_stream_get_output_stream (io),
+ frame, 6, &written, NULL, NULL))
+ g_assert_not_reached ();
+ g_assert_cmpuint (written, ==, 6);
+
+ WAIT_UNTIL (error != NULL);
+ g_assert_error (error, WEB_SOCKET_ERROR, WEB_SOCKET_CLOSE_BAD_DATA);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_CLOSED);
+
+ g_assert_cmpuint (web_socket_connection_get_close_code (test->client), ==, WEB_SOCKET_CLOSE_BAD_DATA);
+ g_error_free (error);
+
+ g_log_remove_handler (G_LOG_DOMAIN, logid);
+}
+
+static void
+test_protocol_negotiate (Test *test,
+ gconstpointer unused)
+{
+ const gchar *server_protocols[] = { "aaa", "bbb", "ccc", NULL };
+ const gchar *client_protocols[] = { "bbb", "ccc", NULL };
+
+ g_object_set (test->server, "protocols", server_protocols, NULL);
+ g_object_set (test->client, "protocols", client_protocols, NULL);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpstr (web_socket_connection_get_protocol (test->client), ==, "bbb");
+ g_assert_cmpstr (web_socket_connection_get_protocol (test->server), ==, "bbb");
+}
+
+static void
+test_protocol_mismatch (Test *test,
+ gconstpointer unused)
+{
+ GError *error = NULL;
+ guint logid;
+
+ const gchar *server_protocols[] = { "aaa", "bbb", "ccc", NULL };
+ const gchar *client_protocols[] = { "ddd", NULL };
+
+ g_signal_handlers_disconnect_by_func (test->client, on_error_not_reached, NULL);
+ g_signal_handlers_disconnect_by_func (test->server, on_error_not_reached, NULL);
+ g_signal_connect (test->client, "error", G_CALLBACK (on_error_copy), &error);
+
+ logid = g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, null_log_handler, NULL);
+
+ g_object_set (test->server, "protocols", server_protocols, NULL);
+ g_object_set (test->client, "protocols", client_protocols, NULL);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING);
+
+ g_assert_error (error, WEB_SOCKET_ERROR, WEB_SOCKET_CLOSE_PROTOCOL);
+ g_error_free (error);
+
+ g_log_remove_handler (G_LOG_DOMAIN, logid);
+}
+
+static void
+test_protocol_server_any (Test *test,
+ gconstpointer unused)
+{
+ GError *error = NULL;
+
+ /* Server accepts any protocol */
+ const gchar *client_protocols[] = { "aaa", "bbb", "ccc", NULL };
+
+ g_signal_handlers_disconnect_by_func (test->client, on_error_not_reached, NULL);
+ g_signal_connect (test->client, "error", G_CALLBACK (on_error_copy), &error);
+
+ g_object_set (test->client, "protocols", client_protocols, NULL);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpstr (web_socket_connection_get_protocol (test->client), ==, "aaa");
+ g_assert_cmpstr (web_socket_connection_get_protocol (test->server), ==, "aaa");
+
+ g_clear_error (&error);
+}
+
+static void
+test_protocol_client_any (Test *test,
+ gconstpointer unused)
+{
+ GError *error = NULL;
+
+ /* Client accepts any protocol */
+ const gchar *server_protocols[] = { "aaa", "bbb", "ccc", NULL };
+
+ g_signal_handlers_disconnect_by_func (test->client, on_error_not_reached, NULL);
+ g_signal_connect (test->client, "error", G_CALLBACK (on_error_copy), &error);
+
+ g_object_set (test->server, "protocols", server_protocols, NULL);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpstr (web_socket_connection_get_protocol (test->client), ==, "aaa");
+ g_assert_cmpstr (web_socket_connection_get_protocol (test->server), ==, "aaa");
+
+ g_clear_error (&error);
+}
+
+static void
+test_close_clean_client (Test *test,
+ gconstpointer data)
+{
+ gboolean close_event_client = FALSE;
+ gboolean close_event_server = FALSE;
+
+ g_signal_connect (test->client, "close", G_CALLBACK (on_close_set_flag), &close_event_client);
+ g_signal_connect (test->server, "close", G_CALLBACK (on_close_set_flag), &close_event_server);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_OPEN);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_OPEN);
+
+ web_socket_connection_close (test->client, WEB_SOCKET_CLOSE_GOING_AWAY, "give me a reason");
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->client), ==, WEB_SOCKET_STATE_CLOSING);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_CLOSED);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_CLOSED);
+
+ g_assert (close_event_client);
+ g_assert (close_event_server);
+
+ g_assert_cmpint (web_socket_connection_get_close_code (test->client), ==, WEB_SOCKET_CLOSE_GOING_AWAY);
+ g_assert_cmpint (web_socket_connection_get_close_code (test->server), ==, WEB_SOCKET_CLOSE_GOING_AWAY);
+ g_assert_cmpstr (web_socket_connection_get_close_data (test->server), ==, "give me a reason");
+}
+
+static void
+test_close_clean_server (Test *test,
+ gconstpointer data)
+{
+ gboolean close_event_client = FALSE;
+ gboolean close_event_server = FALSE;
+
+ g_signal_connect (test->client, "close", G_CALLBACK (on_close_set_flag), &close_event_client);
+ g_signal_connect (test->server, "close", G_CALLBACK (on_close_set_flag), &close_event_server);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_OPEN);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_OPEN);
+
+ web_socket_connection_close (test->server, WEB_SOCKET_CLOSE_GOING_AWAY, "another reason");
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->server), ==, WEB_SOCKET_STATE_CLOSING);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_CLOSED);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_CLOSED);
+
+ g_assert (close_event_client);
+ g_assert (close_event_server);
+
+ g_assert_cmpint (web_socket_connection_get_close_code (test->server), ==, WEB_SOCKET_CLOSE_GOING_AWAY);
+ g_assert_cmpint (web_socket_connection_get_close_code (test->client), ==, WEB_SOCKET_CLOSE_GOING_AWAY);
+ g_assert_cmpstr (web_socket_connection_get_close_data (test->client), ==, "another reason");
+}
+
+static void
+test_close_immediately (void)
+{
+ WebSocketConnection *client;
+ gboolean close_event = FALSE;
+
+ client = web_socket_client_new ("ws://localhost/unix", NULL, NULL);
+ g_signal_connect (client, "close", G_CALLBACK (on_close_set_flag), &close_event);
+ g_assert_cmpint (web_socket_connection_get_ready_state (client), ==, WEB_SOCKET_STATE_CONNECTING);
+
+ web_socket_connection_close (client, 0, NULL);
+ g_assert_cmpint (web_socket_connection_get_ready_state (client), ==, WEB_SOCKET_STATE_CLOSED);
+ g_assert (close_event == TRUE);
+
+ g_object_unref (client);
+}
+
+static gboolean
+on_idle_real_close (gpointer data)
+{
+ WebSocketConnection *ws = data;
+ web_socket_connection_close (ws, 0, NULL);
+ return FALSE;
+}
+
+static gboolean
+on_closing_send_message (WebSocketConnection *ws,
+ gpointer data)
+{
+ GBytes *message = data;
+ web_socket_connection_send (ws, WEB_SOCKET_DATA_TEXT, NULL, message);
+ g_signal_handlers_disconnect_by_func (ws, on_closing_send_message, data);
+ g_idle_add (on_idle_real_close, ws);
+ return TRUE;
+}
+
+static void
+test_message_after_closing (Test *test,
+ gconstpointer data)
+{
+ gboolean close_event_client = FALSE;
+ gboolean close_event_server = FALSE;
+ GBytes *received = NULL;
+ GBytes *message;
+
+ message = g_bytes_new ("another test because", 20);
+ g_signal_connect (test->client, "close", G_CALLBACK (on_close_set_flag), &close_event_client);
+ g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
+ g_signal_connect (test->server, "close", G_CALLBACK (on_close_set_flag), &close_event_server);
+ g_signal_connect (test->server, "closing", G_CALLBACK (on_closing_send_message), message);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_OPEN);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_OPEN);
+
+ web_socket_connection_close (test->client, WEB_SOCKET_CLOSE_GOING_AWAY, "another reason");
+ g_assert_cmpint (web_socket_connection_get_ready_state (test->client), ==, WEB_SOCKET_STATE_CLOSING);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->server) == WEB_SOCKET_STATE_CLOSED);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (test->client) == WEB_SOCKET_STATE_CLOSED);
+
+ g_assert (close_event_client);
+ g_assert (close_event_server);
+
+ g_assert (received != NULL);
+ g_assert (g_bytes_equal (message, received));
+
+ g_bytes_unref (received);
+ g_bytes_unref (message);
+}
+
+static void
+mock_perform_handshake (GIOStream *io)
+{
+ GHashTable *headers;
+ gchar buffer[1024];
+ gssize count;
+ gssize ret;
+ const gchar *key;
+ gchar *accept;
+ gsize written;
+
+ /* Assumes client codes sends headers as a single write() */
+ count = g_input_stream_read (g_io_stream_get_input_stream (io),
+ buffer, sizeof (buffer), NULL, NULL);
+ g_assert (count > 0);
+
+ /* Parse the incoming request */
+ ret = web_socket_util_parse_req_line (buffer, count, NULL, NULL);
+ g_assert_cmpint (ret, >, 0);
+ ret = web_socket_util_parse_headers (buffer + ret, count - ret, &headers);
+ g_assert_cmpint (ret, >, 0);
+
+ key = g_hash_table_lookup (headers, "Sec-WebSocket-Key");
+ accept = _web_socket_complete_accept_key_rfc6455 (key);
+
+ count = g_snprintf (buffer, sizeof (buffer),
+ "HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: %s\r\n"
+ "\r\n", accept);
+ g_free (accept);
+
+ if (!g_output_stream_write_all (g_io_stream_get_output_stream (io),
+ buffer, count, &written, NULL, NULL))
+ g_assert_not_reached ();
+ g_assert_cmpuint (count, ==, written);
+
+ g_hash_table_unref (headers);
+}
+
+static gpointer
+handshake_then_timeout_server_thread (gpointer user_data)
+{
+ GIOStream *io = user_data;
+ mock_perform_handshake (io);
+ return NULL;
+}
+
+static void
+test_close_after_timeout (void)
+{
+ WebSocketConnection *client;
+ gboolean close_event = FALSE;
+ GIOStream *io_a;
+ GIOStream *io_b;
+ GThread *thread;
+
+ /* Note that no server is around in this test, so no close happens */
+ cockpit_socket_streampair (&io_a, &io_b);
+ thread = g_thread_new ("timeout-thread", handshake_then_timeout_server_thread, io_a);
+
+ client = web_socket_client_new_for_stream ("ws://localhost/unix", NULL, NULL, io_b);
+
+ g_signal_connect (client, "close", G_CALLBACK (on_close_set_flag), &close_event);
+ g_signal_connect (client, "error", G_CALLBACK (on_error_not_reached), NULL);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (client) == WEB_SOCKET_STATE_OPEN);
+
+ /* Now try and close things */
+ web_socket_connection_close (client, 0, NULL);
+ g_assert_cmpint (web_socket_connection_get_ready_state (client), ==, WEB_SOCKET_STATE_CLOSING);
+
+#if 0
+ /* g_test_expect_message is pretty much incompatible with g_debug() */
+ g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE, "server did not close io when expected");
+#endif
+ WAIT_UNTIL (web_socket_connection_get_ready_state (client) == WEB_SOCKET_STATE_CLOSED);
+
+ g_assert (close_event == TRUE);
+ g_object_unref (client);
+
+ /* Now actually close the server side stream */
+ g_thread_join (thread);
+ g_object_unref (io_a);
+ g_object_unref (io_b);
+}
+
+static gpointer
+send_fragments_server_thread (gpointer user_data)
+{
+ GIOStream *io = user_data;
+ gsize written;
+
+ const gchar fragments[] = "\x01\x04""one " /* !fin | opcode */
+ "\x00\x04""two " /* !fin | no opcode */
+ "\x80\x05""three"; /* fin | no opcode */
+
+ mock_perform_handshake (io);
+
+ if (!g_output_stream_write_all (g_io_stream_get_output_stream (io),
+ fragments, sizeof (fragments) -1, &written, NULL, NULL))
+ g_assert_not_reached ();
+ g_assert_cmpuint (written, ==, sizeof (fragments) - 1);
+
+ return NULL;
+}
+
+static void
+test_receive_fragmented (void)
+{
+ WebSocketConnection *client;
+ GIOStream *io_a;
+ GIOStream *io_b;
+ GThread *thread;
+ GBytes *received = NULL;
+ GBytes *expect;
+
+ /* Note that no server is around in this test, so no close happens */
+ cockpit_socket_streampair (&io_a, &io_b);
+ thread = g_thread_new ("fragment-thread", send_fragments_server_thread, io_a);
+
+ client = web_socket_client_new_for_stream ("ws://localhost/unix", NULL, NULL, io_b);
+ g_signal_connect (client, "error", G_CALLBACK (on_error_not_reached), NULL);
+ g_signal_connect (client, "message", G_CALLBACK (on_text_message), &received);
+
+ WAIT_UNTIL (received != NULL);
+ expect = g_bytes_new ("one two three", 13);
+ g_assert (g_bytes_equal (expect, received));
+ g_bytes_unref (expect);
+ g_bytes_unref (received);
+
+ g_thread_join (thread);
+ g_object_unref (client);
+ g_object_unref (io_a);
+ g_object_unref (io_b);
+}
+
+static gpointer
+client_thread (gpointer data)
+{
+ GIOStream *io = data;
+ GMainContext *context;
+ WebSocketConnection *client;
+
+ context = g_main_context_new ();
+ g_main_context_push_thread_default (context);
+
+ client = web_socket_client_new_for_stream ("ws://localhost/unix", NULL, NULL, io);
+ g_signal_connect (client, "error", G_CALLBACK (on_error_not_reached), NULL);
+
+ while (web_socket_connection_get_ready_state (client) != WEB_SOCKET_STATE_CLOSED)
+ g_main_context_iteration (context, TRUE);
+
+ g_main_context_pop_thread_default (context);
+ g_main_context_unref (context);
+
+ g_object_unref (client);
+ return NULL;
+}
+
+static void
+test_handshake_with_buffer_and_headers (void)
+{
+ WebSocketConnection *server;
+ GHashTable *headers;
+ GByteArray *input;
+ gchar buffer[1024];
+ GIOStream *ioc;
+ GIOStream *ios;
+ gssize count;
+ gssize in1, in2;
+ GThread *thread;
+
+ cockpit_socket_streampair (&ioc, &ios);
+
+ thread = g_thread_new ("client-thread", client_thread, ioc);
+
+ count = g_input_stream_read (g_io_stream_get_input_stream (ios), buffer,
+ sizeof (buffer), NULL, NULL);
+ g_assert_cmpint (count, >, 0);
+
+ /* Parse the incoming request */
+ in1 = web_socket_util_parse_req_line (buffer, count, NULL, NULL);
+ g_assert_cmpint (in1, >, 0);
+ in2 = web_socket_util_parse_headers (buffer + in1, count - in1, &headers);
+ g_assert_cmpint (in2, >, 0);
+
+ /* Make a buffer for the rest */
+ input = g_byte_array_new ();
+ g_byte_array_append (input, (guchar *)buffer + (in1 + in2), count - (in1 + in2));
+
+ server = web_socket_server_new_for_stream ("ws://localhost/unix", NULL,
+ NULL, ios, headers, input);
+ g_signal_connect (server, "error", G_CALLBACK (on_error_not_reached), NULL);
+
+ WAIT_UNTIL (web_socket_connection_get_ready_state (server) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert_cmpint (web_socket_connection_get_ready_state (server), ==, WEB_SOCKET_STATE_OPEN);
+
+ web_socket_connection_close (server, 0, NULL);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (server) == WEB_SOCKET_STATE_CLOSED);
+
+ g_byte_array_unref (input);
+ g_hash_table_unref (headers);
+ g_object_unref (server);
+ g_thread_join (thread);
+
+ g_object_unref (ioc);
+ g_object_unref (ios);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gchar *name;
+ gint j;
+
+ struct {
+ void (* func) (Test *, gconstpointer);
+ const gchar *name;
+ } tests_with_client_server_pair[] = {
+ { test_handshake, "handshake" },
+ { test_send_client_to_server, "send-client-to-server" },
+ { test_send_server_to_client, "send-server-to-client" },
+ { test_send_big_packets, "send-big-packets" },
+ { test_send_prefixed, "send-prefixed" },
+ { test_send_bad_data, "send-bad-data" },
+ { test_pressure_queue, "pressure-queue" },
+ { test_pressure_throttle, "pressure-throttle" },
+ { test_protocol_negotiate, "protocol-negotiate" },
+ { test_protocol_mismatch, "protocol-mismatch" },
+ { test_protocol_server_any, "protocol-server-any" },
+ { test_protocol_client_any, "protocol-client-any" },
+ { test_close_clean_client, "close-clean-client" },
+ { test_close_clean_server, "close-clean-server" },
+ };
+
+ signal (SIGPIPE, SIG_IGN);
+ g_assert (g_setenv ("GSETTINGS_BACKEND", "memory", TRUE));
+ g_assert (g_setenv ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE));
+ g_assert (g_setenv ("GIO_USE_VFS", "local", TRUE));
+
+ g_set_prgname ("test-websocket");
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/web-socket/parse-url", test_parse_url);
+ g_test_add_func ("/web-socket/parse-url-no-out", test_parse_url_no_out);
+ g_test_add_func ("/web-socket/parse-url-bad", test_parse_url_bad);
+ g_test_add_func ("/web-socket/parse-url-no-path", test_parse_url_no_path);
+ g_test_add_func ("/web-socket/parse-url-with-user", test_parse_url_with_user);
+ g_test_add_func ("/web-socket/parse-req", test_parse_req);
+ g_test_add_func ("/web-socket/parse-req-no-out", test_parse_req_no_out);
+ g_test_add_func ("/web-socket/parse-req-not-enough", test_parse_req_not_enough);
+ g_test_add_func ("/web-socket/parse-req-bad", test_parse_req_bad);
+ g_test_add_func ("/web-socket/parse-status", test_parse_status);
+ g_test_add_func ("/web-socket/parse-status-no-out", test_parse_status_no_out);
+ g_test_add_func ("/web-socket/parse-status-not-enough", test_parse_status_not_enough);
+ g_test_add_func ("/web-socket/parse-status-bad", test_parse_status_bad);
+ g_test_add_func ("/web-socket/parse-version-1-0", test_parse_version_1_0);
+ g_test_add_func ("/web-socket/parse-version-1-1", test_parse_version_1_1);
+ g_test_add_func ("/web-socket/parse-headers", test_parse_headers);
+ g_test_add_func ("/web-socket/parse-duplicate-headers", test_parse_duplicate_headers);
+ g_test_add_func ("/web-socket/parse-headers-no-out", test_parse_headers_no_out);
+ g_test_add_func ("/web-socket/parse-headers-bad", test_parse_headers_bad);
+ g_test_add_func ("/web-socket/parse-headers-not-enough", test_parse_headers_not_enough);
+ g_test_add_func ("/web-socket/header-equals", test_header_equals);
+ g_test_add_func ("/web-socket/header-contains", test_header_contains);
+ g_test_add_func ("/web-socket/header-empty", test_header_empty);
+
+ for (j = 0; j < G_N_ELEMENTS (tests_with_client_server_pair); j++)
+ {
+ name = g_strdup_printf ("/web-socket/%s", tests_with_client_server_pair[j].name);
+ g_test_add (name, Test, NULL, setup_pair, tests_with_client_server_pair[j].func, teardown);
+ g_free (name);
+ }
+
+ g_test_add_func ("/web-socket/close-immediately", test_close_immediately);
+ if (g_test_slow ())
+ g_test_add_func ("/web-socket/close-after-timeout", test_close_after_timeout);
+ g_test_add_func ("/web-socket/receive-fragmented", test_receive_fragmented);
+ g_test_add_func ("/web-socket/handshake-with-buffer-headers", test_handshake_with_buffer_and_headers);
+
+ g_test_add ("/web-socket/message-after-closing", Test, NULL, setup_pair, test_message_after_closing, teardown);
+
+ return g_test_run ();
+}
diff --git a/src/websocket/websocket.c b/src/websocket/websocket.c
new file mode 100644
index 0000000..4465a09
--- /dev/null
+++ b/src/websocket/websocket.c
@@ -0,0 +1,517 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "websocket.h"
+#include "websocketprivate.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ * WebSocketState:
+ * @WEB_SOCKET_STATE_CONNECTING: the WebSocket is not yet ready to send messages
+ * @WEB_SOCKET_STATE_OPEN: the Websocket is ready to send messages
+ * @WEB_SOCKET_STATE_CLOSING: the Websocket is in the process of closing down, no further messages sent
+ * @WEB_SOCKET_STATE_CLOSED: the Websocket is completely closed down
+ *
+ * The WebSocket is in the %WEB_SOCKET_STATE_CONNECTING state during initial
+ * connection setup, and handshaking. If the handshake or connection fails it
+ * can go directly to the %WEB_SOCKET_STATE_CLOSED state from here.
+ *
+ * Once the WebSocket handshake completes successfully it will be in the
+ * %WEB_SOCKET_STATE_OPEN state. During this state, and only during this state
+ * can WebSocket messages be sent.
+ *
+ * WebSocket messages can be received during either the %WEB_SOCKET_STATE_OPEN
+ * or %WEB_SOCKET_STATE_CLOSING states.
+ *
+ * The WebSocket goes into the %WEB_SOCKET_STATE_CLOSING state once it has
+ * successfully sent a close request to the peer. If we had not yet received
+ * an earlier close request from the peer, then the WebSocket waits for a
+ * response to the close request (until a timeout).
+ *
+ * Once actually closed completely down the WebSocket state is
+ * %WEB_SOCKET_STATE_CLOSED. No communication is possible during this state.
+ */
+
+GQuark
+web_socket_error_get_quark (void)
+{
+ return g_quark_from_static_string ("web-socket-error-quark");
+}
+
+static inline const gchar *
+strskip (const gchar *start,
+ const gchar c,
+ const gchar *end)
+{
+ while (start != end && start[0] == c)
+ start++;
+ return start;
+}
+
+static gsize
+parse_version (const gchar *data,
+ gsize length,
+ gchar **version)
+{
+ if (length < 8)
+ return 0;
+ if (memcmp (data, "HTTP/1.0", 8) != 0 &&
+ memcmp (data, "HTTP/1.1", 8) != 0)
+ return 0;
+ if (version)
+ *version = g_strndup (data, 8);
+ return 8;
+}
+
+gboolean
+_web_socket_util_parse_url (const gchar *url,
+ gchar **out_scheme,
+ gchar **out_host,
+ gchar **out_path,
+ GError **error)
+{
+ const gchar *colon;
+ const gchar *host;
+ const gchar *path;
+
+ colon = strchr (url, ':');
+ if (colon == NULL ||
+ (colon[1] != '/' && colon[2] != '/'))
+ {
+ /* The same error as g_network_address_parse_uri() */
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ "Invalid URI '%s'", url);
+ return FALSE;
+ }
+ path = strchr (colon + 3, '/');
+ host = strchr (colon + 3, '@');
+ if (host && (!path || host < path))
+ {
+ host++;
+ }
+ else
+ {
+ host = colon + 3;
+ path = strchr (host, '/');
+ }
+
+ if (host[0] == '\0' || host == path)
+ {
+ /* The same error as g_network_address_parse_uri() */
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ "Invalid URI '%s'", url);
+ return FALSE;
+ }
+
+ if (out_scheme)
+ *out_scheme = g_strndup (url, colon - url);
+ if (out_host)
+ {
+ if (path)
+ *out_host = g_strndup (host, path - host);
+ else
+ *out_host = g_strdup (host);
+ }
+
+ if (out_path)
+ {
+ if (!path)
+ path = "/";
+ *out_path = g_strdup (path);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+is_valid_line (const gchar *string,
+ gssize length)
+{
+ gint i;
+
+ if (length < 0)
+ length = strlen (string);
+
+ for (i = 0; i < length; i++)
+ {
+ if (string[i] != '\t')
+ {
+ if (string[i] < ' ' || string[i] & 0x80)
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * web_socket_util_parse_req_line:
+ * @data: (array length=length): the input data
+ * @length: length of data
+ * @method: (out): location to place HTTP method, or %NULL
+ * @resource: (out): location to place HTTP resource path, or %NULL
+ *
+ * Parse an HTTP request line.
+ *
+ * The number of bytes parsed will be returned if parsing succeeds, including
+ * the new line at the end of the request line. A negative value will be
+ * returned if parsing fails.
+ *
+ * If the HTTP request line was truncated (ie: not all of it was present
+ * within @length) then zero will be returned.
+ *
+ * The @method and @resource should point to string pointers. The values
+ * returned should be freed by the caller using g_free().
+ *
+ * Return value: zero if truncated, negative if fails, or number of
+ * characters parsed
+ */
+gssize
+web_socket_util_parse_req_line (const gchar *data,
+ gsize length,
+ gchar **method,
+ gchar **resource)
+{
+ const gchar *end;
+ const gchar *method_end;
+ const gchar *path_beg;
+ const gchar *path_end;
+ const gchar *version;
+ const gchar *last;
+ gsize n;
+
+ /*
+ * Here we parse a line like:
+ *
+ * GET /path/to/file HTTP/1.1
+ */
+
+ g_return_val_if_fail (data != NULL || length == 0, -1);
+
+ if (length == 0)
+ return 0;
+
+ end = memchr (data, '\n', length);
+ if (end == NULL)
+ return 0; /* need more data */
+
+ if (data[0] == ' ')
+ return -1;
+
+ method_end = memchr (data, ' ', (end - data));
+ if (method_end == NULL)
+ return -1;
+
+ path_beg = strskip (method_end + 1, ' ', end);
+ path_end = memchr (path_beg, ' ', (end - path_beg));
+ if (path_end == NULL)
+ return -1;
+
+ version = strskip (path_end + 1, ' ', end);
+
+ /* Returns number of characters consumed */
+ n = parse_version (version, (end - version), NULL);
+ if (n == 0)
+ return -1;
+
+ last = version + n;
+ while (last != end)
+ {
+ /* Acceptable trailing characters */
+ if (!strchr ("\r ", last[0]))
+ return -1;
+ last++;
+ }
+
+ if (!is_valid_line (data, (method_end - data)) ||
+ !is_valid_line (data, (path_end - path_beg)))
+ return -1;
+
+ if (method)
+ *method = g_strndup (data, (method_end - data));
+ if (resource)
+ *resource = g_strndup (path_beg, (path_end - path_beg));
+ return (end - data) + 1;
+}
+
+static guint
+str_case_hash (gconstpointer v)
+{
+ /* A case agnostic version of g_str_hash */
+ const signed char *p;
+ guint32 h = 5381;
+ for (p = v; *p != '\0'; p++)
+ h = (h << 5) + h + g_ascii_tolower (*p);
+ return h;
+}
+
+static gboolean
+str_case_equal (gconstpointer v1,
+ gconstpointer v2)
+{
+ /* A case agnostic version of g_str_equal */
+ return g_ascii_strcasecmp (v1, v2) == 0;
+}
+
+/**
+ * web_socket_util_new_headers:
+ *
+ * Create a new hashtable for HTTP headers.
+ *
+ * The GHashTable contains allocated null-terminated strings, as would
+ * be returned by g_strdup(). The headers are indexed by the header names
+ * in a case insensitive way.
+ *
+ * It is not necessary to worry about case headers in this GHashTable.
+ *
+ * Return value: (transfer full): a new header hashtable
+ */
+GHashTable *
+web_socket_util_new_headers (void)
+{
+ return g_hash_table_new_full (str_case_hash, str_case_equal, g_free, g_free);
+}
+
+/**
+ * web_socket_util_parse_headers:
+ * @data: (array length=length): the input data
+ * @length: length of data
+ * @headers: (out): location to place HTTP header hash table
+ *
+ * Parse HTTP headers.
+ *
+ * The number of bytes parsed will be returned if parsing succeeds, including
+ * the new line at the end of the request line. A negative value will be
+ * returned if parsing fails.
+ *
+ * If the HTTP request line was truncated (ie: not all of it was present
+ * within @length) then zero will be returned.
+ *
+ * The @headers returned will be allocated using web_socket_util_new_headers(),
+ * and should be freed by the caller using g_free().
+ *
+ * Return value: zero if truncated, negative if fails, or number of
+ * characters parsed
+ */
+gssize
+web_socket_util_parse_headers (const gchar *data,
+ gsize length,
+ GHashTable **headers)
+{
+ GHashTable *parsed_headers;
+ const gchar *line;
+ const gchar *colon;
+ gsize consumed = 0;
+ gboolean end = FALSE;
+ gsize line_len;
+
+ parsed_headers = web_socket_util_new_headers ();
+
+ while (!end)
+ {
+ line = memchr (data, '\n', length);
+
+ /* No line ending: need more data */
+ if (line == NULL)
+ {
+ consumed = 0;
+ break;
+ }
+
+ line++;
+ line_len = (line - data);
+
+ /* An empty line, all done */
+ if ((data[0] == '\r' && data[1] == '\n') || data[0] == '\n')
+ {
+ end = TRUE;
+ }
+
+ /* A header line */
+ else
+ {
+ colon = memchr (data, ':', length);
+ if (!colon || colon >= line)
+ {
+ g_debug ("received invalid header line: %.*s", (gint)line_len, data);
+ consumed = -1;
+ break;
+ }
+
+ g_autofree gchar *name = g_strndup (data, colon - data);
+ g_strstrip (name);
+ g_autofree gchar *value = g_strndup (colon + 1, line - (colon + 1));
+ g_strstrip (value);
+
+ if (!is_valid_line (name, -1) || !g_utf8_validate (value, -1, NULL))
+ {
+ g_debug ("received invalid header");
+ consumed = -1;
+ break;
+ }
+ g_hash_table_insert (parsed_headers, g_steal_pointer (&name), g_steal_pointer (&value));
+ }
+
+ consumed += line_len;
+ data += line_len;
+ length -= line_len;
+ }
+
+ if (consumed > 0)
+ {
+ if (headers)
+ *headers = g_hash_table_ref (parsed_headers);
+ }
+
+ g_hash_table_unref (parsed_headers);
+
+ return consumed;
+}
+
+gboolean
+_web_socket_util_header_equals (GHashTable *headers,
+ const gchar *name,
+ const gchar *want)
+{
+ const gchar *value;
+
+ value = g_hash_table_lookup (headers, name);
+ if (value != NULL && g_ascii_strcasecmp (value, want) == 0)
+ return TRUE;
+
+ g_message ("received invalid or missing %s header: %s", name, value);
+ return FALSE;
+}
+
+gboolean
+_web_socket_util_header_contains (GHashTable *headers,
+ const gchar *name,
+ const gchar *word)
+{
+ const gchar *value;
+ const gchar *at;
+
+ value = g_hash_table_lookup (headers, name);
+ if (value != NULL)
+ {
+ /* The word must be present, and not part of another word */
+ at = strcasestr (value, word);
+ if (at != NULL &&
+ (at == value || !g_ascii_isalnum (*(at - 1))) &&
+ !g_ascii_isalnum (at[strlen (word)]))
+ return TRUE;
+ }
+
+ g_message ("received invalid or missing %s header: %s", name, value);
+ return FALSE;
+}
+
+gboolean
+_web_socket_util_header_empty (GHashTable *headers,
+ const gchar *name)
+{
+ const gchar *value;
+
+ value = g_hash_table_lookup (headers, name);
+ if (value == NULL || value[0] == '\0')
+ return TRUE;
+
+ g_message ("received unsupported %s header: %s", name, value);
+ return FALSE;
+}
+
+/**
+ * web_socket_util_parse_status_line:
+ * @data: (array length=length): the input data
+ * @length: length of data
+ * @version: (out): location to place HTTP version, or %NULL
+ * @status: (out): location to place HTTP status, or %NULL
+ * @reason: (out): location to place HTTP message, or %NULL
+ *
+ * Parse an HTTP status line.
+ *
+ * The number of bytes parsed will be returned if parsing succeeds, including
+ * the new line at the end of the status line. A negative value will be
+ * returned if parsing fails.
+ *
+ * If the HTTP request line was truncated (ie: not all of it was present
+ * within @length) then zero will be returned.
+ *
+ * @reason should point to a string pointer. The value
+ * returned should be freed by the caller using g_free().
+ *
+ * Return value: zero if truncated, negative if fails, or number of
+ * characters parsed
+ */
+gssize
+web_socket_util_parse_status_line (const gchar *data,
+ gsize length,
+ gchar **version,
+ guint *status,
+ gchar **reason)
+{
+ const gchar *at;
+ const gchar *end;
+ gsize n;
+ guint64 num;
+ gchar *ep;
+
+ /*
+ * Here we parse a line like:
+ *
+ * HTTP/1.1 101 Switching protocols
+ */
+
+ at = data;
+ end = memchr (at, '\n', length);
+ if (end == NULL)
+ return 0; /* need more data */
+
+ n = parse_version (at, (end - at), version);
+ if (n == 0 || at[n] != ' ')
+ return -1;
+ at += n;
+
+ /* Extra spaces */
+ at = strskip (at, ' ', end);
+
+ /* First check for space after status */
+ if (memchr (at, ' ', (end - at)) == NULL)
+ return -1;
+
+ /* This will stop at above space */
+ num = g_ascii_strtoull (at, &ep, 10);
+ if (num == 0 || num > G_MAXUINT || *ep != ' ')
+ return -1;
+
+ at = strskip (ep, ' ', end);
+
+ if (reason)
+ {
+ *reason = g_strndup (at, (end - at));
+ g_strstrip (*reason);
+ }
+ if (status)
+ *status = (guint)num;
+ return (end - data) + 1;
+}
diff --git a/src/websocket/websocket.h b/src/websocket/websocket.h
new file mode 100644
index 0000000..d819fcc
--- /dev/null
+++ b/src/websocket/websocket.h
@@ -0,0 +1,88 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __WEB_SOCKET_H__
+#define __WEB_SOCKET_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define WEB_SOCKET_ERROR (web_socket_error_get_quark ())
+
+GQuark web_socket_error_get_quark (void) G_GNUC_CONST;
+
+GHashTable * web_socket_util_new_headers (void);
+
+gssize web_socket_util_parse_headers (const gchar *data,
+ gsize length,
+ GHashTable **headers);
+
+gssize web_socket_util_parse_req_line (const gchar *data,
+ gsize length,
+ gchar **method,
+ gchar **resource);
+
+gssize web_socket_util_parse_status_line (const gchar *data,
+ gsize length,
+ gchar **version,
+ guint *status,
+ gchar **reason);
+
+typedef enum {
+ WEB_SOCKET_DATA_TEXT = 0x01,
+ WEB_SOCKET_DATA_BINARY = 0x02,
+} WebSocketDataType;
+
+typedef enum {
+ WEB_SOCKET_CLOSE_NORMAL = 1000,
+ WEB_SOCKET_CLOSE_GOING_AWAY = 1001,
+ WEB_SOCKET_CLOSE_NO_STATUS = 1005,
+ WEB_SOCKET_CLOSE_ABNORMAL = 1006,
+ WEB_SOCKET_CLOSE_PROTOCOL = 1002,
+ WEB_SOCKET_CLOSE_UNSUPPORTED_DATA = 1003,
+ WEB_SOCKET_CLOSE_BAD_DATA = 1007,
+ WEB_SOCKET_CLOSE_POLICY_VIOLATION = 1008,
+ WEB_SOCKET_CLOSE_TOO_BIG = 1009,
+ WEB_SOCKET_CLOSE_NO_EXTENSION = 1010,
+ WEB_SOCKET_CLOSE_SERVER_ERROR = 1011,
+ WEB_SOCKET_CLOSE_TLS_HANDSHAKE = 1015,
+} WebSocketCloseCodes;
+
+typedef enum {
+ WEB_SOCKET_STATE_CONNECTING = 0,
+ WEB_SOCKET_STATE_OPEN = 1,
+ WEB_SOCKET_STATE_CLOSING = 2,
+ WEB_SOCKET_STATE_CLOSED = 3,
+} WebSocketState;
+
+typedef struct _WebSocketConnection WebSocketConnection;
+typedef struct _WebSocketConnectionClass WebSocketConnectionClass;
+typedef struct _WebSocketClient WebSocketClient;
+typedef struct _WebSocketClientClass WebSocketClientClass;
+typedef struct _WebSocketServer WebSocketServer;
+typedef struct _WebSocketServerClass WebSocketServerClass;
+
+#include "websocketconnection.h"
+#include "websocketclient.h"
+#include "websocketserver.h"
+
+G_END_DECLS
+
+#endif /* __WEB_SOCKET_H__ */
diff --git a/src/websocket/websocketclient.c b/src/websocket/websocketclient.c
new file mode 100644
index 0000000..3e46e48
--- /dev/null
+++ b/src/websocket/websocketclient.c
@@ -0,0 +1,567 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "websocketclient.h"
+#include "websocketprivate.h"
+
+#include <string.h>
+
+enum {
+ PROP_0,
+ PROP_ORIGIN,
+ PROP_PROTOCOLS,
+};
+
+struct _WebSocketClient
+{
+ WebSocketConnection parent;
+
+ gboolean handshake_started;
+ gchar *origin;
+ gchar **possible_protocols;
+ gpointer accept_key;
+ GHashTable *include_headers;
+ GHashTable *response_headers;
+ GCancellable *cancellable;
+ GSource *idle_start;
+};
+
+struct _WebSocketClientClass
+{
+ WebSocketConnectionClass parent;
+};
+
+G_DEFINE_TYPE (WebSocketClient, web_socket_client, WEB_SOCKET_TYPE_CONNECTION);
+
+static void
+web_socket_client_init (WebSocketClient *self)
+{
+
+}
+
+static void
+protocol_error_and_close (WebSocketConnection *conn)
+{
+ GError *error = g_error_new_literal (WEB_SOCKET_ERROR,
+ WEB_SOCKET_CLOSE_PROTOCOL,
+ "Received invalid WebSocket handshake from the server");
+ _web_socket_connection_error_and_close (conn, error, TRUE);
+}
+
+static gboolean
+verify_handshake_rfc6455 (WebSocketClient *self,
+ WebSocketConnection *conn,
+ GHashTable *headers)
+{
+ const gchar *value;
+
+ /*
+ * This is a client verifying a handshake response it's received
+ * from the server.
+ */
+
+ if (!_web_socket_util_header_equals (headers, "Upgrade", "websocket") ||
+ !_web_socket_util_header_contains (headers, "Connection", "upgrade") ||
+ !_web_socket_connection_choose_protocol (conn, (const gchar **)self->possible_protocols,
+ g_hash_table_lookup (headers, "Sec-Websocket-Protocol")) ||
+ !_web_socket_util_header_empty (headers, "Sec-WebSocket-Extensions"))
+ {
+ protocol_error_and_close (conn);
+ return FALSE;
+ }
+
+ /*
+ * We filled in accept_key when we did a handshake request
+ * earlier in request_handshake_rfc6455().
+ */
+ value = g_hash_table_lookup (headers, "Sec-WebSocket-Accept");
+ if (value == NULL || self->accept_key == NULL ||
+ g_ascii_strcasecmp (self->accept_key, value))
+ {
+ g_message ("received invalid or missing Sec-WebSocket-Accept header: %s", value);
+ protocol_error_and_close (conn);
+ return FALSE;
+ }
+
+ g_debug ("verified rfc6455 handshake");
+ return TRUE;
+}
+
+static gboolean
+parse_handshake_response (WebSocketClient *self,
+ WebSocketConnection *conn,
+ GByteArray *incoming)
+{
+ GHashTable *headers;
+ gchar *reason;
+ gboolean verified;
+ guint status;
+ GError *error = NULL;
+ gssize in1, in2;
+ gssize consumed;
+
+ /* Parse the handshake response received from the server */
+ in1 = web_socket_util_parse_status_line ((const gchar *)incoming->data,
+ incoming->len, NULL, &status, &reason);
+ if (in1 < 0)
+ {
+ g_message ("received invalid status line");
+ protocol_error_and_close (conn);
+ }
+ else if (in1 == 0)
+ g_debug ("waiting for more handshake data");
+ if (in1 <= 0)
+ return FALSE;
+
+ in2 = web_socket_util_parse_headers ((const gchar *)incoming->data + in1,
+ incoming->len - in1, &headers);
+ if (in2 < 0)
+ {
+ g_message ("received invalid response headers");
+ protocol_error_and_close (conn);
+ }
+ else if (in2 == 0)
+ g_debug ("waiting for more handshake data");
+ if (in2 <= 0)
+ {
+ g_free (reason);
+ return FALSE;
+ }
+
+ consumed = in1 + in2;
+
+ if (self->response_headers)
+ g_hash_table_unref (self->response_headers);
+ self->response_headers = headers;
+
+ /*
+ * TODO: We could handle the following codes here:
+ * 401: authentication
+ * 3xx: redirect
+ */
+
+ if (status == 101)
+ {
+ verified = verify_handshake_rfc6455 (self, conn, headers);
+ if (verified)
+ {
+ /* Handshake is successful */
+ g_debug ("open: handshake completed");
+ }
+ }
+ else
+ {
+ verified = FALSE;
+ g_message ("received unexpected status: %d %s", status, reason);
+ if (reason == NULL)
+ error = g_error_new (WEB_SOCKET_ERROR,
+ WEB_SOCKET_CLOSE_PROTOCOL,
+ "Handshake failed: %u", status);
+ else
+ error = g_error_new (WEB_SOCKET_ERROR,
+ WEB_SOCKET_CLOSE_PROTOCOL,
+ "%s", reason);
+ _web_socket_connection_error_and_close (conn, error, FALSE);
+ }
+
+ g_free (reason);
+ if (consumed > 0)
+ g_byte_array_remove_range (incoming, 0, consumed);
+ return verified;
+}
+
+static void
+include_custom_headers (WebSocketClient *self,
+ GString *handshake)
+{
+ GHashTableIter iter;
+ gpointer name;
+ gpointer value;
+
+ if (!self->include_headers)
+ return;
+
+ g_hash_table_iter_init (&iter, self->include_headers);
+ while (g_hash_table_iter_next (&iter, &name, &value))
+ {
+ if (value == NULL)
+ continue;
+ g_debug ("including custom header: %s: %s", (gchar *)name, (gchar *)value);
+ g_string_append_printf (handshake, "%s: %s\r\n",
+ (gchar *)name, (gchar *)value);
+ }
+}
+
+static void
+request_handshake_rfc6455 (WebSocketClient *self,
+ WebSocketConnection *conn,
+ const gchar *host,
+ const gchar *path)
+{
+ gchar *key;
+ gchar *protocols;
+ GString *handshake;
+ guint32 raw[4];
+ gsize len;
+
+ raw[0] = g_random_int ();
+ raw[1] = g_random_int ();
+ raw[2] = g_random_int ();
+ raw[3] = g_random_int ();
+ G_STATIC_ASSERT (sizeof (raw) == 16);
+ key = g_base64_encode ((const guchar *)raw, sizeof (raw));
+
+ /* Save this for verify_handshake_rfc6455() */
+ g_free (self->accept_key);
+ self->accept_key = _web_socket_complete_accept_key_rfc6455 (key);
+
+ handshake = g_string_new ("");
+ g_string_printf (handshake, "GET %s HTTP/1.1\r\n"
+ "Host: %s\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Key: %s\r\n"
+ "Sec-WebSocket-Version: 13\r\n",
+ path, host, key);
+
+ /* RFC 6454 talks about 'null' */
+ g_string_append_printf (handshake, "Origin: %s\r\n", self->origin ? self->origin : "null");
+
+ if (self->possible_protocols)
+ {
+ protocols = g_strjoinv (", ", self->possible_protocols);
+ g_string_append_printf (handshake, "Sec-WebSocket-Protocol: %s\r\n", protocols);
+ g_free (protocols);
+ }
+
+ include_custom_headers (self, handshake);
+ g_string_append (handshake, "\r\n");
+
+ g_free (key);
+
+ len = handshake->len;
+ _web_socket_connection_queue (conn, WEB_SOCKET_QUEUE_URGENT,
+ g_string_free (handshake, FALSE), len, 0);
+ g_debug ("queued rfc6455 handshake request");
+}
+
+static void
+request_handshake (WebSocketClient *self,
+ WebSocketConnection *conn)
+{
+ GError *error = NULL;
+ const gchar *url;
+ gchar *host;
+ gchar *path;
+
+ self->handshake_started = TRUE;
+
+ url = web_socket_connection_get_url (conn);
+ if (!_web_socket_util_parse_url (url, NULL, &host, &path, &error))
+ {
+ _web_socket_connection_error_and_close (conn, error, TRUE);
+ return;
+ }
+
+ request_handshake_rfc6455 (self, conn, host, path);
+
+ g_free (host);
+ g_free (path);
+}
+
+static gpointer
+on_idle_do_handshake (gpointer user_data)
+{
+ WebSocketClient *self = WEB_SOCKET_CLIENT (user_data);
+ WebSocketConnection *conn = WEB_SOCKET_CONNECTION (user_data);
+ g_source_unref (self->idle_start);
+ self->idle_start = NULL;
+ request_handshake (self, conn);
+ return FALSE;
+}
+
+static void
+on_connect_to_uri (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ WebSocketClient *self = WEB_SOCKET_CLIENT (user_data);
+ WebSocketConnection *conn = WEB_SOCKET_CONNECTION (user_data);
+ GSocketConnection *connection;
+ GError *error = NULL;
+
+ connection = g_socket_client_connect_to_uri_finish (G_SOCKET_CLIENT (source),
+ result, &error);
+
+ if (error == NULL)
+ {
+ _web_socket_connection_take_io_stream (conn, G_IO_STREAM (connection));
+ request_handshake (self, conn);
+ }
+ else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_error_free (error);
+ }
+ else
+ {
+ _web_socket_connection_error_and_close (conn, error, TRUE);
+ }
+
+ g_object_unref (self);
+}
+
+static void
+web_socket_client_constructed (GObject *object)
+{
+ WebSocketClient *self = WEB_SOCKET_CLIENT (object);
+ WebSocketConnection *conn = WEB_SOCKET_CONNECTION (object);
+ GSocketClient *client;
+ const gchar *url;
+ guint16 default_port;
+ gchar *scheme;
+
+ G_OBJECT_CLASS (web_socket_client_parent_class)->constructed (object);
+
+ if (web_socket_connection_get_io_stream (conn))
+ {
+ /* Start handshake from the main context */
+ self->idle_start = g_idle_source_new ();
+ g_source_set_priority (self->idle_start, G_PRIORITY_HIGH);
+ g_source_set_callback (self->idle_start, (GSourceFunc)on_idle_do_handshake,
+ self, NULL);
+ g_source_attach (self->idle_start, _web_socket_connection_get_main_context (conn));
+ }
+ else
+ {
+ client = g_socket_client_new ();
+ self->cancellable = g_cancellable_new ();
+
+ url = web_socket_connection_get_url (WEB_SOCKET_CONNECTION (self));
+ scheme = g_uri_parse_scheme (url);
+ if (scheme && (g_str_equal (scheme, "wss") || g_str_equal (scheme, "https")))
+ {
+ g_socket_client_set_tls (client, TRUE);
+ default_port = 443;
+ }
+ else
+ {
+ default_port = 80;
+ }
+ g_free (scheme);
+
+ g_socket_client_connect_to_uri_async (client, url, default_port,
+ self->cancellable, on_connect_to_uri,
+ g_object_ref (self));
+ g_object_unref (client);
+ }
+}
+
+static gboolean
+web_socket_client_handshake (WebSocketConnection *conn,
+ GByteArray *incoming)
+{
+ WebSocketClient *self = WEB_SOCKET_CLIENT (conn);
+ return parse_handshake_response (self, conn, incoming);
+}
+
+static void
+web_socket_client_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ WebSocketClient *self = WEB_SOCKET_CLIENT (object);
+
+ switch (prop_id)
+ {
+ case PROP_ORIGIN:
+ g_return_if_fail (self->origin == NULL);
+ self->origin = g_value_dup_string (value);
+ break;
+
+ case PROP_PROTOCOLS:
+ g_return_if_fail (self->handshake_started == FALSE);
+ g_strfreev (self->possible_protocols);
+ self->possible_protocols = g_value_dup_boxed (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+web_socket_client_close (WebSocketConnection *conn)
+{
+ WebSocketClient *self = WEB_SOCKET_CLIENT (conn);
+
+ if (self->cancellable)
+ g_cancellable_cancel (self->cancellable);
+ if (self->idle_start)
+ {
+ g_source_destroy (self->idle_start);
+ g_source_unref (self->idle_start);
+ }
+}
+
+static void
+web_socket_client_finalize (GObject *object)
+{
+ WebSocketClient *self = WEB_SOCKET_CLIENT (object);
+
+ g_strfreev (self->possible_protocols);
+ g_free (self->origin);
+ g_free (self->accept_key);
+ if (self->include_headers)
+ g_hash_table_unref (self->include_headers);
+ if (self->response_headers)
+ g_hash_table_unref (self->response_headers);
+ if (self->cancellable)
+ g_object_unref (self->cancellable);
+ g_assert (self->idle_start == NULL);
+
+ G_OBJECT_CLASS (web_socket_client_parent_class)->finalize (object);
+}
+
+static void
+web_socket_client_class_init (WebSocketClientClass *klass)
+{
+ WebSocketConnectionClass *conn_class = WEB_SOCKET_CONNECTION_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = web_socket_client_constructed;
+ object_class->set_property = web_socket_client_set_property;
+ object_class->finalize = web_socket_client_finalize;
+
+ conn_class->server_behavior = FALSE;
+ conn_class->handshake = web_socket_client_handshake;
+ conn_class->close = web_socket_client_close;
+
+ /**
+ * WebSocketClient:origin:
+ *
+ * The WebSocket origin. Client WebSockets will send this to the server. If
+ * set on a server, then only clients with the matching origin will be accepted.
+ */
+ g_object_class_install_property (object_class, PROP_ORIGIN,
+ g_param_spec_string ("origin", "Origin", "The WebSocket origin", NULL,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * WebSocketClient:protocols:
+ *
+ * The possible protocols to negotiate with the peer.
+ */
+ g_object_class_install_property (object_class, PROP_PROTOCOLS,
+ g_param_spec_boxed ("protocols", "Protocol", "The desired WebSocket protocols", G_TYPE_STRV,
+ G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+}
+
+/**
+ * web_socket_client_new:
+ * @url: the url address of the WebSocket
+ * @origin: (allow-none): the origin to report to server
+ * @protocols: (allow-none): possible protocols to negotiate
+ *
+ * Create a new client side WebSocket connection to communicate with a server.
+ *
+ * The WebSocket will establish a connection to the server using HTTP
+ * or HTTPS at the address specified in the @url.
+ *
+ * If @protocols are specified then these are used to negotiate a protocol
+ * with the client.
+ *
+ * Returns: (transfer full): a new WebSocket
+ */
+WebSocketConnection *
+web_socket_client_new (const gchar *url,
+ const gchar *origin,
+ const gchar **protocols)
+{
+ return g_object_new (WEB_SOCKET_TYPE_CLIENT,
+ "url", url,
+ "origin", origin,
+ "protocols", protocols,
+ NULL);
+}
+
+/**
+ * web_socket_client_new_for_stream:
+ * @url: the url address of the WebSocket
+ * @origin: (allow-none): the origin to report to server
+ * @protocols: (allow-none): possible protocols to negotiate
+ * @io_stream: the IO stream to communicate over
+ *
+ * Create a new client side WebSocket connection to communicate with a server.
+ *
+ * Use this function if you've already opened up a IO stream to the server
+ * and now wish to communicate over it. The input and output streams of the
+ * @io_stream must be pollable.
+ *
+ * If @protocols are specified then these are used to negotiate a protocol
+ * with the client.
+ *
+ * Returns: (transfer full): a new WebSocket
+ */
+WebSocketConnection *
+web_socket_client_new_for_stream (const gchar *url,
+ const gchar *origin,
+ const gchar **protocols,
+ GIOStream *io_stream)
+{
+ return g_object_new (WEB_SOCKET_TYPE_CLIENT,
+ "url", url,
+ "origin", origin,
+ "protocols", protocols,
+ "io-stream", io_stream,
+ NULL);
+}
+
+/**
+ * web_socket_client_include_header:
+ * @self: the client
+ * @name: the header name
+ * @value: the header value
+ *
+ * Add an HTTP header (eg: for authentication) to the
+ * HTTP request.
+ */
+void
+web_socket_client_include_header (WebSocketClient *self,
+ const gchar *name,
+ const gchar *value)
+{
+ g_return_if_fail (WEB_SOCKET_IS_CLIENT (self));
+ g_return_if_fail (self->handshake_started == FALSE);
+
+ if (!self->include_headers)
+ self->include_headers = web_socket_util_new_headers ();
+ g_hash_table_insert (self->include_headers,
+ g_strdup (name), g_strdup (value));
+}
+
+GHashTable *
+web_socket_client_get_headers (WebSocketClient *self)
+{
+ g_return_val_if_fail (WEB_SOCKET_IS_CLIENT (self), NULL);
+ return self->response_headers;
+}
diff --git a/src/websocket/websocketclient.h b/src/websocket/websocketclient.h
new file mode 100644
index 0000000..9dfad40
--- /dev/null
+++ b/src/websocket/websocketclient.h
@@ -0,0 +1,54 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __WEB_SOCKET_CLIENT_H__
+#define __WEB_SOCKET_CLIENT_H__
+
+#include "websocketconnection.h"
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define WEB_SOCKET_TYPE_CLIENT (web_socket_client_get_type ())
+#define WEB_SOCKET_CLIENT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), WEB_SOCKET_TYPE_CLIENT, WebSocketClient))
+#define WEB_SOCKET_IS_CLIENT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), WEB_SOCKET_TYPE_CLIENT))
+#define WEB_SOCKET_CLIENT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), WEB_SOCKET_TYPE_CLIENT, WebSocketClientClass))
+#define WEB_SOCKET_IS_CLIENT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), WEB_SOCKET_TYPE_CLIENT))
+
+GType web_socket_client_get_type (void) G_GNUC_CONST;
+
+WebSocketConnection * web_socket_client_new (const gchar *url,
+ const gchar *origin,
+ const gchar **protocols);
+
+WebSocketConnection * web_socket_client_new_for_stream (const gchar *url,
+ const gchar *origin,
+ const gchar **protocols,
+ GIOStream *io_stream);
+
+void web_socket_client_include_header (WebSocketClient *self,
+ const gchar *name,
+ const gchar *value);
+
+GHashTable * web_socket_client_get_headers (WebSocketClient *self);
+
+G_END_DECLS
+
+#endif /* __WEB_SOCKET_CLIENT_H__ */
diff --git a/src/websocket/websocketconnection.c b/src/websocket/websocketconnection.c
new file mode 100644
index 0000000..f0782dc
--- /dev/null
+++ b/src/websocket/websocketconnection.c
@@ -0,0 +1,1930 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "websocket.h"
+#include "websocketprivate.h"
+
+#include "common/cockpitflow.h"
+
+#include <string.h>
+
+/*
+ * SECTION:websocketconnection
+ * @title: WebSocketConnection
+ * @short_description: A WebSocket connection
+ *
+ * A #WebSocketConnection is a WebSocket connection to a peer. This API is modeled
+ * after the W3C API for interacting with WebSockets.
+ *
+ * Use the #WebSocketClient or #WebSocketServer derived classes on the
+ * appropriate sides. As a client, to connect to a Websocket server, use the
+ * web_socket_client_new() function. To handle a WebSocket connection from
+ * a client, you can use the web_socket_server_new_for_stream() function.
+ *
+ * The #WebSocketConnection:ready-state property will indicate the state of the
+ * connection. You can only send messages once the connection is in the
+ * %WEB_SOCKET_STATE_OPEN state. The #WebSocketConnection::open signal will fire
+ * when transitioning to this state.
+ *
+ * Use web_socket_connection_send() to send a message to the peer. When a
+ * message is received the #WebSocketConnection::message signal will fire.
+ *
+ * The web_socket_connection_close() function will perform an orderly close
+ * of the connection. The #WebSocketConnection::close signal will fire once
+ * the connection closes, whether it was initiated by this side or the peer.
+ *
+ * Connect to the #WebSocketConnection::closing signal to detect when either
+ * peer begins closing the connection. You can prevent closure of this side
+ * by returning %FALSE from the signal handler. You should in that case
+ * call the web_socket_connection_close() function at a later time to complete
+ * the close.
+ */
+
+/**
+ * WebSocketConnection:
+ *
+ * An abstract base class representing a WebSocket connection. Use
+ * instances of the derived #WebSocketClient or #WebSocketServer classes.
+ */
+
+/**
+ * WebSocketConnectionClass:
+ * @server_behavior: set by #WebSocketServer to %TRUE
+ * @handshake: used by derived classes to handle received HTTP handshake
+ * @open: default handler for the #WebSocketConnection::open signal
+ * @message: default handler for the #WebSocketConnection::message signal
+ * @error: default handler for the #WebSocketConnection::error signal
+ * @closing: the default handler for the #WebSocketConnection:closing signal
+ * @close: default handler for the #WebSocketConnection::close signal
+ *
+ * The abstract base class for #WebSocketConnection
+ */
+
+enum {
+ PROP_0,
+ PROP_URL,
+ PROP_PROTOCOL,
+ PROP_READY_STATE,
+ PROP_BUFFERED_AMOUNT,
+ PROP_IO_STREAM,
+};
+
+enum {
+ OPEN,
+ MESSAGE,
+ ERROR,
+ CLOSING,
+ CLOSE,
+ NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+typedef struct {
+ GBytes *data;
+ gboolean last;
+ gsize sent;
+ gsize amount;
+} Frame;
+
+typedef struct
+{
+ /* FALSE if client, TRUE if server */
+ gboolean server_side;
+
+ /*
+ * On the client this is the url we connect to,
+ * on the server, the one the socket lives at
+ */
+ gchar *url;
+ gchar *chosen_protocol;
+
+ GSource *start_idle;
+ gboolean handshake_done;
+
+ gushort peer_close_code;
+ gchar *peer_close_data;
+ gboolean close_sent;
+ gboolean close_received;
+ gboolean dirty_close;
+ GSource *close_timeout;
+
+ GMainContext *main_context;
+
+ GIOStream *io_stream;
+ gboolean io_open;
+ gboolean io_closed;
+
+ GPollableInputStream *input;
+ GSource *input_source;
+ GByteArray *incoming;
+
+ GPollableOutputStream *output;
+ GSource *output_source;
+ gsize output_queued;
+ GQueue outgoing;
+
+ /* Current message being assembled */
+ guint8 message_opcode;
+ GByteArray *message_data;
+
+ /* Pressure which throttles input on this web socket */
+ CockpitFlow *pressure;
+ gulong pressure_sig;
+} WebSocketConnectionPrivate;
+
+#define MAX_PAYLOAD 128 * 1024
+
+/* The queue size above which we consider applying back pressure */
+#define QUEUE_PRESSURE 1UL * 1024UL * 1024UL /* 1 megabyte */
+
+static void web_socket_connection_flow_iface_init (CockpitFlowInterface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (WebSocketConnection, web_socket_connection, G_TYPE_OBJECT,
+ G_ADD_PRIVATE(WebSocketConnection)
+ G_IMPLEMENT_INTERFACE (COCKPIT_TYPE_FLOW, web_socket_connection_flow_iface_init));
+
+#define GET_PRIV(self) ((WebSocketConnectionPrivate *) web_socket_connection_get_instance_private(self))
+
+static void
+frame_free (gpointer data)
+{
+ Frame *frame = data;
+ if (frame)
+ {
+ g_bytes_unref (frame->data);
+ g_slice_free (Frame, frame);
+ }
+}
+
+static void
+web_socket_connection_init (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ g_queue_init (&pv->outgoing);
+ pv->main_context = g_main_context_ref_thread_default ();
+}
+
+static void
+on_iostream_closed (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ WebSocketConnection *self = user_data;
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ GError *error = NULL;
+ gboolean unused;
+
+ /* We treat connection as closed even if close fails */
+ pv->io_closed = TRUE;
+ g_io_stream_close_finish (pv->io_stream, result, &error);
+
+ if (error)
+ {
+ g_message ("error closing web socket stream: %s", error->message);
+ if (!pv->dirty_close)
+ g_signal_emit (self, signals[ERROR], 0, error, &unused);
+ pv->dirty_close = TRUE;
+ g_error_free (error);
+ }
+
+ g_assert (web_socket_connection_get_ready_state (self) == WEB_SOCKET_STATE_CLOSED);
+ g_debug ("closed: completed io stream close");
+ g_signal_emit (self, signals[CLOSE], 0);
+
+ g_object_unref (self);
+}
+
+static void
+stop_input (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ if (pv->input_source)
+ {
+ g_debug ("stopping input source");
+ g_source_destroy (pv->input_source);
+ g_source_unref (pv->input_source);
+ pv->input_source = NULL;
+ }
+}
+
+static void
+stop_output (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ if (pv->output_source)
+ {
+ g_debug ("stopping output source");
+ g_source_destroy (pv->output_source);
+ g_source_unref (pv->output_source);
+ pv->output_source = NULL;
+ }
+}
+
+static void
+close_io_stop_timeout (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ if (pv->close_timeout)
+ {
+ g_source_destroy (pv->close_timeout);
+ g_source_unref (pv->close_timeout);
+ pv->close_timeout = NULL;
+ }
+
+ if (pv->start_idle)
+ {
+ g_source_destroy (pv->start_idle);
+ g_source_unref (pv->start_idle);
+ pv->start_idle = NULL;
+ }
+}
+
+static void
+close_io_stream (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ close_io_stop_timeout (self);
+
+ /* Close a connection that's not yet open */
+ if (!pv->io_stream && !pv->io_closed)
+ {
+ pv->io_closed = TRUE;
+ g_assert (web_socket_connection_get_ready_state (self) == WEB_SOCKET_STATE_CLOSED);
+ g_debug ("closed: no stream was opened");
+ g_signal_emit (self, signals[CLOSE], 0);
+ }
+
+ /* Close an open stream, which is not yet close_async'ing */
+ else if (pv->io_open)
+ {
+ stop_input (self);
+ stop_output (self);
+ pv->io_open = FALSE;
+ g_debug ("closing io stream");
+ g_io_stream_close_async (pv->io_stream, G_PRIORITY_DEFAULT,
+ NULL, on_iostream_closed, g_object_ref (self));
+ }
+
+ g_object_notify (G_OBJECT (self), "ready-state");
+}
+
+static void
+shutdown_wr_io_stream (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ GSocket *socket;
+ GError *error = NULL;
+
+ stop_output (self);
+
+ if (G_IS_SOCKET_CONNECTION (pv->io_stream))
+ {
+ socket = g_socket_connection_get_socket (G_SOCKET_CONNECTION (pv->io_stream));
+ g_socket_shutdown (socket, FALSE, TRUE, &error);
+ if (error != NULL)
+ {
+ g_message ("error shutting down io stream: %s", error->message);
+ g_error_free (error);
+ }
+ }
+
+ g_object_notify (G_OBJECT (self), "ready-state");
+}
+
+static gboolean
+on_timeout_close_io (gpointer user_data)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (user_data);
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ pv->close_timeout = 0;
+ g_message ("peer did not close io when expected");
+
+ close_io_stream (self);
+
+ return FALSE;
+}
+
+static void
+close_io_after_timeout (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ const gint timeout = 5;
+
+ if (pv->close_timeout)
+ return;
+
+ g_debug ("waiting %d seconds for peer to close io", timeout);
+ pv->close_timeout = g_timeout_source_new_seconds (timeout);
+ g_source_set_callback (pv->close_timeout, on_timeout_close_io, self, NULL);
+ g_source_attach (pv->close_timeout, pv->main_context);
+}
+
+static void
+xor_with_mask_rfc6455 (const guint8 *mask,
+ guint8 *data,
+ gsize len)
+{
+ g_assert (mask != NULL);
+ g_assert (data != NULL);
+
+ /* Do the masking */
+ for (gsize n = 0; n < len; n++)
+ data[n] ^= mask[n & 3];
+}
+
+static void
+send_prefixed_message_rfc6455 (WebSocketConnection *self,
+ WebSocketQueueFlags flags,
+ guint8 opcode,
+ const guint8 *prefix,
+ gsize prefix_len,
+ const guint8 *payload,
+ gsize payload_len)
+{
+ gsize amount;
+ GByteArray *bytes;
+ gsize frame_len;
+ guint8 *outer;
+ guint8 *mask = 0;
+ guint8 *at;
+ gsize len;
+ guint64 size;
+
+ len = payload_len + prefix_len;
+ amount = len;
+
+ bytes = g_byte_array_sized_new (14 + len);
+ outer = bytes->data;
+ outer[0] = 0x80 | opcode;
+
+ /* If control message, truncate payload */
+ if (opcode & 0x08)
+ {
+ if (len > 125)
+ {
+ g_warning ("Truncating WebSocket control message payload");
+ if (prefix_len > 125)
+ prefix_len = 125;
+ payload_len = 125 - prefix_len;
+ len = 125;
+ }
+
+ /* Buffered amount of bytes is zero for control messages */
+ amount = 0;
+ }
+
+ size = len;
+ if (size < 126)
+ {
+ outer[1] = (0xFF & size); /* mask | 7-bit-len */
+ bytes->len = 2;
+ }
+ else if (size < 65536)
+ {
+ outer[1] = 126; /* mask | 16-bit-len */
+ outer[2] = (size >> 8) & 0xFF;
+ outer[3] = (size >> 0) & 0xFF;
+ bytes->len = 4;
+ }
+ else
+ {
+ outer[1] = 127; /* mask | 64-bit-len */
+ outer[2] = (size >> 56) & 0xFF;
+ outer[3] = (size >> 48) & 0xFF;
+ outer[4] = (size >> 40) & 0xFF;
+ outer[5] = (size >> 32) & 0xFF;
+ outer[6] = (size >> 24) & 0xFF;
+ outer[7] = (size >> 16) & 0xFF;
+ outer[8] = (size >> 8) & 0xFF;
+ outer[9] = (size >> 0) & 0xFF;
+ bytes->len = 10;
+ }
+
+ /*
+ * The server side doesn't need to mask, so we don't. There's
+ * probably a client somewhere that's not expecting it.
+ */
+ const gboolean is_client_side = !GET_PRIV(self)->server_side;
+ if (is_client_side)
+ {
+ guint32 rand = g_random_int ();
+ outer[1] |= 0x80;
+ mask = outer + bytes->len;
+ memcpy (mask, &rand, sizeof (guint32));
+ bytes->len += 4;
+ }
+
+ at = bytes->data + bytes->len;
+ g_byte_array_append (bytes, prefix, prefix_len);
+ g_byte_array_append (bytes, payload, payload_len);
+
+ if (is_client_side)
+ xor_with_mask_rfc6455 (mask, at, len);
+
+ frame_len = bytes->len;
+ _web_socket_connection_queue (self, flags, g_byte_array_free (bytes, FALSE),
+ frame_len, amount);
+ g_debug ("queued rfc6455 %d frame of len %u", (gint)opcode, (guint)frame_len);
+}
+
+static void
+send_message_rfc6455 (WebSocketConnection *self,
+ WebSocketQueueFlags flags,
+ guint8 opcode,
+ const guint8 *payload,
+ gsize payload_len)
+{
+ return send_prefixed_message_rfc6455 (self, flags, opcode, NULL, 0, payload, payload_len);
+}
+
+static void
+send_close_rfc6455 (WebSocketConnection *self,
+ WebSocketQueueFlags flags,
+ gushort code,
+ const gchar *reason)
+{
+ /* Note that send_message truncates as expected */
+ gchar buffer[128];
+ gsize len = 0;
+
+ if (code != 0)
+ {
+ buffer[len++] = code >> 8;
+ buffer[len++] = code & 0xFF;
+ if (reason)
+ len += g_strlcpy (buffer + len, reason, sizeof (buffer) - len);
+ }
+
+ send_message_rfc6455 (self, flags, 0x08, (guint8 *)buffer, len);
+ GET_PRIV(self)->close_sent = TRUE;
+}
+
+gboolean
+_web_socket_connection_error (WebSocketConnection *self,
+ GError *error)
+{
+ gboolean unused;
+
+ if (web_socket_connection_get_ready_state (self) != WEB_SOCKET_STATE_CLOSED)
+ {
+ if (error)
+ {
+ GET_PRIV(self)->dirty_close = TRUE;
+ g_signal_emit (self, signals[ERROR], 0, error, &unused);
+ }
+
+ g_error_free (error);
+ return TRUE;
+ }
+
+ g_error_free (error);
+ return FALSE;
+}
+
+void
+_web_socket_connection_error_and_close (WebSocketConnection *self,
+ GError *error,
+ gboolean prejudice)
+{
+ gboolean ignore = FALSE;
+ gushort code;
+
+ if (error && error->domain == WEB_SOCKET_ERROR)
+ code = error->code;
+ else
+ code = WEB_SOCKET_CLOSE_GOING_AWAY;
+
+ if (!GET_PRIV(self)->server_side && error && error->domain == G_TLS_ERROR)
+ {
+ GET_PRIV(self)->peer_close_code = WEB_SOCKET_CLOSE_TLS_HANDSHAKE;
+ if (g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_NOT_TLS) ||
+ g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_MISC))
+ {
+ GET_PRIV(self)->peer_close_data = g_strdup ("protocol-error");
+ }
+ else if (g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE))
+ {
+ GET_PRIV(self)->peer_close_data = g_strdup ("unknown-hostkey");
+ }
+ }
+
+ if (!_web_socket_connection_error (self, error))
+ return;
+
+ if (!GET_PRIV(self)->handshake_done)
+ prejudice = TRUE;
+
+ /* If already closing, so just ignore this stuff */
+ switch (web_socket_connection_get_ready_state (self))
+ {
+ case WEB_SOCKET_STATE_CLOSED:
+ ignore = TRUE;
+ break;
+ case WEB_SOCKET_STATE_CLOSING:
+ ignore = !prejudice;
+ break;
+ default:
+ break;
+ }
+
+ if (ignore)
+ {
+ g_debug ("already closing/closed, ignoring error");
+ }
+ else if (prejudice)
+ {
+ g_debug ("forcing close due to error");
+ close_io_stream (self);
+ }
+ else
+ {
+ g_debug ("requesting close due to error");
+ send_close_rfc6455 (self, WEB_SOCKET_QUEUE_URGENT | WEB_SOCKET_QUEUE_LAST, code, NULL);
+ }
+}
+
+static void
+protocol_error_and_close_full (WebSocketConnection *self,
+ gboolean prejudice)
+{
+ GError *error = g_error_new_literal (WEB_SOCKET_ERROR,
+ WEB_SOCKET_CLOSE_PROTOCOL,
+ GET_PRIV(self)->server_side ?
+ "Received invalid WebSocket response from the server" :
+ "Received invalid WebSocket response from the client");
+ _web_socket_connection_error_and_close (self, error, prejudice);
+}
+
+static void
+protocol_error_and_close (WebSocketConnection *self)
+{
+ protocol_error_and_close_full (self, FALSE);
+}
+
+static void
+bad_data_error_and_close (WebSocketConnection *self)
+{
+ GError *error = g_error_new_literal (WEB_SOCKET_ERROR,
+ WEB_SOCKET_CLOSE_BAD_DATA,
+ GET_PRIV(self)->server_side ?
+ "Received invalid WebSocket data from the server" :
+ "Received invalid WebSocket data from the client");
+ _web_socket_connection_error_and_close (self, error, FALSE);
+}
+
+static void
+too_big_error_and_close (WebSocketConnection *self,
+ gsize payload_len)
+{
+ GError *error = g_error_new_literal (WEB_SOCKET_ERROR,
+ WEB_SOCKET_CLOSE_TOO_BIG,
+ GET_PRIV(self)->server_side ?
+ "Received extremely large WebSocket data from the server" :
+ "Received extremely large WebSocket data from the client");
+ g_message ("%s is trying to frame of size %" G_GSIZE_FORMAT " or greater, but max supported size is 128KiB",
+ GET_PRIV(self)->server_side ? "server" : "client", payload_len);
+ _web_socket_connection_error_and_close (self, error, TRUE);
+
+ /* The input is in an invalid state now */
+ stop_input (self);
+}
+
+static gboolean
+web_socket_connection_default_error (WebSocketConnection *connection,
+ GError *error)
+{
+ if (g_error_matches (error, G_TLS_ERROR, G_TLS_ERROR_EOF))
+ g_debug ("web socket error: %s", error->message);
+ else
+ g_message ("%s", error->message);
+ return TRUE;
+}
+
+static gboolean
+web_socket_connection_default_closing (WebSocketConnection *self)
+{
+ return TRUE;
+}
+
+static void
+receive_close_rfc6455 (WebSocketConnection *self,
+ const guint8 *data,
+ gsize len)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ pv->peer_close_code = 0;
+ g_free (pv->peer_close_data);
+ pv->peer_close_data = NULL;
+ pv->close_received = TRUE;
+
+ /* Store the code/data payload */
+ if (len >= 2)
+ {
+ pv->peer_close_code = (guint16)data[0] << 8 | data[1];
+ }
+ if (len > 2)
+ {
+ data += 2;
+ len -= 2;
+ if (g_utf8_validate ((gchar *)data, len, NULL))
+ pv->peer_close_data = g_strndup ((gchar *)data, len);
+ else
+ g_message ("received non-UTF8 close data: %d '%.*s' %d", (int)len, (int)len, (gchar *)data, (int)data[0]);
+ }
+
+ /* Once we receive close response on server, close immediately */
+ if (pv->close_sent)
+ {
+ shutdown_wr_io_stream (self);
+ if (pv->server_side)
+ close_io_stream (self);
+ }
+
+ else
+ {
+ /* Send back the response */
+ web_socket_connection_close (self, pv->peer_close_code, NULL);
+ }
+}
+
+static void
+receive_ping_rfc6455 (WebSocketConnection *self,
+ const guint8 *data,
+ gsize len)
+{
+ /* Send back a pong with same data */
+ g_debug ("received ping, responding");
+ send_message_rfc6455 (self, WEB_SOCKET_QUEUE_URGENT, 0x0A, data, len);
+}
+
+static void
+process_contents_rfc6455 (WebSocketConnection *self,
+ gboolean control,
+ gboolean fin,
+ guint8 opcode,
+ gconstpointer payload,
+ gsize payload_len)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ GBytes *message;
+
+ if (control)
+ {
+ /* Control frames must never be fragmented */
+ if (!fin)
+ {
+ g_message ("received fragmented control frame");
+ protocol_error_and_close (self);
+ return;
+ }
+
+ g_debug ("received control frame %d with %d payload", (int)opcode, (int)payload_len);
+
+ switch (opcode)
+ {
+ case 0x08:
+ receive_close_rfc6455 (self, payload, payload_len);
+ break;
+ case 0x09:
+ receive_ping_rfc6455 (self, payload, payload_len);
+ break;
+ case 0x0A:
+ break;
+ default:
+ g_message ("received unsupported control frame: %d", (gint)opcode);
+ break;
+ }
+ }
+
+ else if (pv->close_received)
+ {
+ g_message ("received message after close was received");
+ }
+
+ /* A message frame */
+ else
+ {
+ /* Initial fragment of a message */
+ if (!fin && opcode)
+ {
+ if (pv->message_data)
+ {
+ g_message ("received out of order initial message fragment");
+ protocol_error_and_close (self);
+ return;
+ }
+ g_debug ("received initial fragment frame %d with %d payload", (int)opcode, (int)payload_len);
+ }
+
+ /* Middle fragment of a message */
+ else if (!fin && !opcode)
+ {
+ if (!pv->message_data)
+ {
+ g_message ("received out of order middle message fragment");
+ protocol_error_and_close (self);
+ return;
+ }
+ g_debug ("received middle fragment frame with %d payload", (int)payload_len);
+ }
+
+ /* Last fragment of a message */
+ else if (fin && !opcode)
+ {
+ if (!pv->message_data)
+ {
+ g_message ("received out of order ending message fragment");
+ protocol_error_and_close (self);
+ return;
+ }
+ g_debug ("received last fragment frame with %d payload", (int)payload_len);
+ }
+
+ /* An unfragmented message */
+ else
+ {
+ g_assert (opcode != 0);
+ if (pv->message_data)
+ {
+ g_message ("received unfragmented message when fragment was expected");
+ protocol_error_and_close (self);
+ return;
+ }
+ g_debug ("received frame %d with %d payload", (int)opcode, (int)payload_len);
+ }
+
+ if (opcode)
+ {
+ pv->message_opcode = opcode;
+ pv->message_data = g_byte_array_sized_new (payload_len);
+ }
+
+ switch (pv->message_opcode)
+ {
+ case 0x01:
+ if (!g_utf8_validate ((gchar *)payload, payload_len, NULL))
+ {
+ g_message ("received invalid non-UTF8 text data");
+
+ /* Discard the entire message */
+ g_byte_array_unref (pv->message_data);
+ pv->message_data = NULL;
+ pv->message_opcode = 0;
+
+ bad_data_error_and_close (self);
+ return;
+ }
+ /* fall through */
+ case 0x02:
+ g_byte_array_append (pv->message_data, payload, payload_len);
+ break;
+ default:
+ g_debug ("received unknown data frame: %d", (gint)opcode);
+ break;
+ }
+
+ /* Actually deliver the message? */
+ if (fin)
+ {
+ /* Always null terminate, as a convenience */
+ g_byte_array_append (pv->message_data, (guchar *)"\0", 1);
+
+ /* But don't include the null terminator in the byte count */
+ pv->message_data->len--;
+
+ opcode = pv->message_opcode;
+ message = g_byte_array_free_to_bytes (pv->message_data);
+ pv->message_data = NULL;
+ pv->message_opcode = 0;
+ g_debug ("message: delivering %d with %d length",
+ (int)opcode, (int)g_bytes_get_size (message));
+ g_signal_emit (self, signals[MESSAGE], 0, (int)opcode, message);
+ g_bytes_unref (message);
+ }
+ }
+}
+
+static gboolean
+process_frame_rfc6455 (WebSocketConnection *self)
+{
+ guint8 *header;
+ guint8 *payload;
+ guint64 payload_len;
+ guint8 *mask;
+ gboolean fin;
+ gboolean control;
+ gboolean masked;
+ guint8 opcode;
+ gsize len;
+ gsize at;
+
+ len = GET_PRIV(self)->incoming->len;
+ if (len < 2)
+ return FALSE; /* need more data */
+
+ header = GET_PRIV(self)->incoming->data;
+ fin = ((header[0] & 0x80) != 0);
+ control = header[0] & 0x08;
+ opcode = header[0] & 0x0f;
+ masked = ((header[1] & 0x80) != 0);
+
+ switch (header[1] & 0x7f)
+ {
+ case 126:
+ at = 4;
+ if (len < at)
+ return FALSE; /* need more data */
+ payload_len = ((guint16)header[2] << 8) |
+ ((guint16)header[3] << 0);
+ break;
+ case 127:
+ at = 10;
+ if (len < at)
+ return FALSE; /* need more data */
+ payload_len = ((guint64)header[2] << 56) |
+ ((guint64)header[3] << 48) |
+ ((guint64)header[4] << 40) |
+ ((guint64)header[5] << 32) |
+ ((guint64)header[6] << 24) |
+ ((guint64)header[7] << 16) |
+ ((guint64)header[8] << 8) |
+ ((guint64)header[9] << 0);
+ break;
+ default:
+ payload_len = header[1] & 0x7f;
+ at = 2;
+ break;
+ }
+
+ /* Safety valve */
+ if (payload_len >= MAX_PAYLOAD)
+ {
+ too_big_error_and_close (self, payload_len);
+ return FALSE;
+ }
+
+ if (len < at + payload_len)
+ return FALSE; /* need more data */
+
+ payload = header + at;
+
+ if (masked)
+ {
+ mask = header + at;
+ payload += 4;
+ at += 4;
+
+ if (len < at + payload_len)
+ return FALSE; /* need more data */
+
+ xor_with_mask_rfc6455 (mask, payload, payload_len);
+ }
+
+ /*
+ * Note that now that we've unmasked, we've modified the buffer, we can
+ * only return below via discarding or processing the message
+ */
+ process_contents_rfc6455 (self, control, fin, opcode, payload, payload_len);
+
+ /* Move past the parsed frame */
+ g_byte_array_remove_range (GET_PRIV(self)->incoming, 0, at + payload_len);
+ return TRUE;
+}
+
+static void
+process_incoming (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ WebSocketConnectionClass *klass;
+ gboolean more;
+
+ if (!pv->handshake_done)
+ {
+ klass = WEB_SOCKET_CONNECTION_GET_CLASS (self);
+ g_assert (klass->handshake != NULL);
+ if ((klass->handshake) (self, pv->incoming))
+ {
+ pv->handshake_done = TRUE;
+ g_object_notify (G_OBJECT (self), "ready-state");
+ g_signal_emit (self, signals[OPEN], 0);
+ }
+ }
+
+ if (pv->handshake_done)
+ {
+ do
+ {
+ more = process_frame_rfc6455 (self);
+ }
+ while (more);
+ }
+}
+
+static gboolean
+on_web_socket_input (GObject *pollable_stream,
+ gpointer user_data)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (user_data);
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ GError *error = NULL;
+ gboolean end = FALSE;
+ gssize count;
+ gsize len;
+
+ do
+ {
+ len = pv->incoming->len;
+ g_byte_array_set_size (pv->incoming, len + 1024);
+
+ count = g_pollable_input_stream_read_nonblocking (pv->input,
+ pv->incoming->data + len,
+ 1024, NULL, &error);
+
+ if (count < 0)
+ {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+ {
+ g_error_free (error);
+ count = 0;
+ }
+ else
+ {
+ _web_socket_connection_error_and_close (self, error, TRUE);
+ return TRUE;
+ }
+ }
+ else if (count == 0)
+ {
+ end = TRUE;
+ }
+
+ pv->incoming->len = len + count;
+ }
+ while (count > 0);
+
+ process_incoming (self);
+
+ if (end)
+ {
+ if (!pv->close_sent || !pv->close_received)
+ {
+ pv->dirty_close = TRUE;
+ g_message ("connection unexpectedly closed by peer");
+ }
+ else
+ {
+ g_debug ("peer has closed socket");
+ }
+
+ close_io_stream (self);
+ }
+
+ return TRUE;
+}
+
+static void
+start_input (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ g_debug ("starting input source");
+ pv->input_source = g_pollable_input_stream_create_source (pv->input, NULL);
+ g_source_set_callback (pv->input_source, (GSourceFunc)on_web_socket_input, self, NULL);
+ g_source_attach (pv->input_source, pv->main_context);
+}
+
+static gboolean
+on_web_socket_output (GObject *pollable_stream,
+ gpointer user_data)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (user_data);
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ const guint8 *data;
+ GError *error = NULL;
+ gsize before;
+ Frame *frame;
+ gssize count;
+ gsize len;
+
+ frame = g_queue_peek_head (&pv->outgoing);
+
+ /* No more frames to send */
+ if (frame == NULL)
+ {
+ stop_output (self);
+ return TRUE;
+ }
+
+ data = g_bytes_get_data (frame->data, &len);
+ g_assert (len > 0);
+ g_assert (len > frame->sent);
+
+ count = g_pollable_output_stream_write_nonblocking (pv->output,
+ data + frame->sent,
+ len - frame->sent,
+ NULL, &error);
+
+ if (count < 0)
+ {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+ {
+ g_clear_error (&error);
+ count = 0;
+ }
+ else
+ {
+ _web_socket_connection_error_and_close (self, error, TRUE);
+ return FALSE;
+ }
+ }
+
+ before = pv->output_queued;
+
+ frame->sent += count;
+ if (frame->sent >= len)
+ {
+ g_debug ("sent frame");
+ g_queue_pop_head (&pv->outgoing);
+ g_assert (len <= pv->output_queued);
+ pv->output_queued -= len;
+
+ if (frame->last)
+ {
+ if (pv->server_side)
+ {
+ close_io_stream (self);
+ }
+ else
+ {
+ shutdown_wr_io_stream (self);
+ close_io_after_timeout (self);
+ }
+ }
+ frame_free (frame);
+ }
+
+ /*
+ * If we're controlling another flow, turn off back pressure when
+ * our output buffer size becomes less than the low mark.
+ */
+ if (before >= QUEUE_PRESSURE && GET_PRIV(self)->output_queued < QUEUE_PRESSURE)
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), FALSE);
+
+ return TRUE;
+}
+
+static void
+start_output (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ if (pv->output_source)
+ return;
+
+ g_debug ("starting output source");
+ pv->output_source = g_pollable_output_stream_create_source (pv->output, NULL);
+ g_source_set_callback (pv->output_source, (GSourceFunc)on_web_socket_output, self, NULL);
+ g_source_attach (pv->output_source, pv->main_context);
+}
+
+void
+_web_socket_connection_queue (WebSocketConnection *self,
+ WebSocketQueueFlags flags,
+ gpointer data,
+ gsize len,
+ gsize amount)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ gsize before;
+ Frame *frame;
+ Frame *prev;
+
+ g_return_if_fail (WEB_SOCKET_IS_CONNECTION (self));
+ g_return_if_fail (pv->close_sent == FALSE);
+ g_return_if_fail (data != NULL);
+ g_return_if_fail (len > 0);
+
+ frame = g_slice_new0 (Frame);
+ frame->data = g_bytes_new_take (data, len);
+ frame->amount = amount;
+ frame->last = (flags & WEB_SOCKET_QUEUE_LAST) ? TRUE : FALSE;
+
+ /* If urgent put at front of queue */
+ if (flags & WEB_SOCKET_QUEUE_URGENT)
+ {
+ /* But we can't interrupt a message already partially sent */
+ prev = g_queue_pop_head (&pv->outgoing);
+ if (prev == NULL)
+ {
+ g_queue_push_head (&pv->outgoing, frame);
+ }
+ else if (prev->sent > 0)
+ {
+ g_queue_push_head (&pv->outgoing, frame);
+ g_queue_push_head (&pv->outgoing, prev);
+ }
+ else
+ {
+ g_queue_push_head (&pv->outgoing, prev);
+ g_queue_push_head (&pv->outgoing, frame);
+ }
+ }
+ else
+ {
+ g_queue_push_tail (&pv->outgoing, frame);
+ }
+
+ before = pv->output_queued;
+ g_return_if_fail (G_MAXSIZE - len > pv->output_queued);
+ pv->output_queued += len;
+
+ /*
+ * If we have two much data queued, and are controlling another flow
+ * tell it to stop sending data, each time we cross over the high bound.
+ */
+ if (before < QUEUE_PRESSURE && GET_PRIV(self)->output_queued >= QUEUE_PRESSURE)
+ cockpit_flow_emit_pressure (COCKPIT_FLOW (self), TRUE);
+
+ start_output (self);
+}
+
+static gboolean
+check_streams (WebSocketConnection *self)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ if (!pv->input || !g_pollable_input_stream_can_poll (pv->input))
+ {
+ g_critical ("WebSocket input stream is invalid or cannot poll");
+ return FALSE;
+ }
+
+ if (!pv->output || !g_pollable_output_stream_can_poll (pv->output))
+ {
+ g_critical ("WebSocket output stream is invalid or cannot poll");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+void
+_web_socket_connection_take_incoming (WebSocketConnection *self,
+ GByteArray *input_buffer)
+{
+ g_return_if_fail (WEB_SOCKET_IS_CONNECTION (self));
+
+ g_return_if_fail (GET_PRIV(self)->incoming == NULL);
+ GET_PRIV(self)->incoming = input_buffer;
+}
+
+static gboolean
+on_idle_start_input (gpointer user_data)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (user_data);
+
+ g_source_unref (GET_PRIV(self)->start_idle);
+ GET_PRIV(self)->start_idle = NULL;
+
+ if (check_streams (self))
+ {
+ start_input (self);
+ process_incoming (self);
+ }
+
+ return FALSE;
+}
+
+void
+_web_socket_connection_take_io_stream (WebSocketConnection *self,
+ GIOStream *io_stream)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ GInputStream *is;
+ GOutputStream *os;
+
+ g_return_if_fail (WEB_SOCKET_IS_CONNECTION (self));
+ g_return_if_fail (G_IS_IO_STREAM (io_stream));
+
+ g_return_if_fail (pv->io_stream == NULL);
+ pv->io_stream = io_stream;
+
+ is = g_io_stream_get_input_stream (io_stream);
+ os = g_io_stream_get_output_stream (io_stream);
+
+ if (G_IS_POLLABLE_INPUT_STREAM (is))
+ pv->input = G_POLLABLE_INPUT_STREAM (is);
+ if (G_IS_POLLABLE_OUTPUT_STREAM (os))
+ pv->output = G_POLLABLE_OUTPUT_STREAM (os);
+
+ pv->io_open = TRUE;
+ g_object_notify (G_OBJECT (self), "io-stream");
+
+ /* Start handshake from the main context */
+ pv->start_idle = g_idle_source_new ();
+ g_source_set_priority (pv->start_idle, G_PRIORITY_HIGH);
+ g_source_set_callback (pv->start_idle, (GSourceFunc)on_idle_start_input,
+ g_object_ref (self), g_object_unref);
+ g_source_attach (pv->start_idle, pv->main_context);
+}
+
+static void
+web_socket_connection_constructed (GObject *object)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (object);
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ WebSocketConnectionClass *klass;
+
+ G_OBJECT_CLASS (web_socket_connection_parent_class)->constructed (object);
+
+ /*
+ * Here we choose a side to be based on our derived class. The handshake
+ * is different on either client/server side, as is the expectation of
+ * how to mask data.
+ */
+ klass = WEB_SOCKET_CONNECTION_GET_CLASS (self);
+ pv->server_side = klass->server_behavior;
+
+ if (!pv->incoming)
+ pv->incoming = g_byte_array_sized_new (1024);
+}
+
+static void
+web_socket_connection_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (object);
+
+ switch (prop_id)
+ {
+ case PROP_URL:
+ g_value_set_string (value, web_socket_connection_get_url (self));
+ break;
+
+ case PROP_PROTOCOL:
+ g_value_set_string (value, web_socket_connection_get_protocol (self));
+ break;
+
+ case PROP_READY_STATE:
+ g_value_set_int (value, web_socket_connection_get_ready_state (self));
+ break;
+
+ case PROP_BUFFERED_AMOUNT:
+ g_value_set_ulong (value, web_socket_connection_get_buffered_amount (self));
+ break;
+
+ case PROP_IO_STREAM:
+ g_value_set_object (value, web_socket_connection_get_io_stream (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+web_socket_connection_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (object);
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ GIOStream *io_stream;
+
+ switch (prop_id)
+ {
+ case PROP_URL:
+ g_return_if_fail (pv->url == NULL);
+ pv->url = g_value_dup_string (value);
+ break;
+
+ case PROP_IO_STREAM:
+ g_return_if_fail (pv->io_stream == NULL);
+ io_stream = g_value_dup_object (value);
+ if (io_stream)
+ _web_socket_connection_take_io_stream (self, io_stream);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+web_socket_connection_dispose (GObject *object)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (object);
+
+ GET_PRIV(self)->dirty_close = TRUE;
+ close_io_stream (self);
+
+ cockpit_flow_throttle (COCKPIT_FLOW (self), NULL);
+ g_assert (GET_PRIV(self)->pressure == NULL);
+
+ G_OBJECT_CLASS (web_socket_connection_parent_class)->dispose (object);
+}
+
+static void
+web_socket_connection_finalize (GObject *object)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (object);
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+
+ g_free (pv->url);
+ g_free (pv->chosen_protocol);
+ g_free (pv->peer_close_data);
+
+ g_main_context_unref (pv->main_context);
+
+ if (pv->incoming)
+ g_byte_array_free (pv->incoming, TRUE);
+ while (!g_queue_is_empty (&pv->outgoing))
+ frame_free (g_queue_pop_head (&pv->outgoing));
+ pv->output_queued = 0;
+
+ g_clear_object (&pv->io_stream);
+ g_assert (!pv->input_source);
+ g_assert (!pv->output_source);
+ g_assert (!pv->io_open);
+ g_assert (pv->io_closed);
+ g_assert (!pv->close_timeout);
+
+ if (pv->start_idle)
+ g_source_unref (pv->start_idle);
+ if (pv->message_data)
+ g_byte_array_free (pv->message_data, TRUE);
+
+ G_OBJECT_CLASS (web_socket_connection_parent_class)->finalize (object);
+}
+
+static void
+web_socket_connection_class_init (WebSocketConnectionClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->constructed = web_socket_connection_constructed;
+ gobject_class->get_property = web_socket_connection_get_property;
+ gobject_class->set_property = web_socket_connection_set_property;
+ gobject_class->dispose = web_socket_connection_dispose;
+ gobject_class->finalize = web_socket_connection_finalize;
+
+ /**
+ * WebSocketConnection:url:
+ *
+ * The URL of the WebSocket.
+ *
+ * For servers this represents the address of the WebSocket, and
+ * for clients it is the address connected to. This is required
+ * as a construct property.
+ */
+ g_object_class_install_property (gobject_class, PROP_URL,
+ g_param_spec_string ("url", "URL", "The WebSocket URL", NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * WebSocketConnection:protocol:
+ *
+ * The chosen protocol. Only becomes valid after the #WebSocketConnection:open
+ * signal has been fired, and when we are in the %WEB_SOCKET_STATE_OPEN state.
+ *
+ * May be NULL if neither peer cares about protocols.
+ */
+ g_object_class_install_property (gobject_class, PROP_PROTOCOL,
+ g_param_spec_string ("protocol", "Protocol", "The chosen WebSocket protocol", NULL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * WebSocketConnection:ready-state:
+ *
+ * The current state of the WebSocket.
+ */
+ g_object_class_install_property (gobject_class, PROP_READY_STATE,
+ g_param_spec_int ("ready-state", "Ready state", "Ready state ",
+ WEB_SOCKET_STATE_CONNECTING, WEB_SOCKET_STATE_CLOSED, WEB_SOCKET_STATE_CONNECTING,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * WebSocketConnection:buffered-amount:
+ *
+ * This represents caller provided data passed into the
+ * web_socket_connection_send() function, which has been queued but not
+ * yet been sent.
+ */
+ g_object_class_install_property (gobject_class, PROP_BUFFERED_AMOUNT,
+ g_param_spec_ulong ("buffered-amount", "Buffered amount", "Outstanding amount of data buffered",
+ 0, G_MAXULONG, 0,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * WebSocketConnection:io-stream:
+ *
+ * The underlying IO stream the WebSocket is communicating over. For servers
+ * this must be specified as a construct property. For clients, this may be
+ * specified, if you have a stream that you've already connected to.
+ *
+ * The input and output streams must be pollable streams.
+ */
+ g_object_class_install_property (gobject_class, PROP_IO_STREAM,
+ g_param_spec_object ("io-stream", "IO Stream", "Underlying io stream", G_TYPE_IO_STREAM,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * WebSocketConnection::open:
+ * @self: the WebSocket
+ *
+ * Emitted when the connection opens and is ready for communication.
+ *
+ * This will be emitted at most once. But if the connection fails during
+ * connecting, then this signal will not be emitted.
+ */
+ signals[OPEN] = g_signal_new ("open",
+ WEB_SOCKET_TYPE_CONNECTION,
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET (WebSocketConnectionClass, open),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 0);
+
+ /**
+ * WebSocketConnection::message:
+ * @self: the WebSocket
+ * @type: the type of message contents
+ * @message: the message data
+ *
+ * Emitted when we receive a message from the peer.
+ *
+ * As a convenience, the @message data will always be null-terminated, but
+ * the null-terminator will not be included in the length count.
+ * This signal may emitted multiple times.
+ */
+ signals[MESSAGE] = g_signal_new ("message",
+ WEB_SOCKET_TYPE_CONNECTION,
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET (WebSocketConnectionClass, message),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_BYTES);
+
+ /**
+ * WebSocketConnection::error:
+ * @self: the WebSocket
+ * @error: the error that occurred
+ *
+ * Emitted when an error occurred on the WebSocket. This may be fired
+ * multiple times. Fatal errors will be followed by the #WebSocketConnection::close
+ * signal being emitted.
+ */
+ signals[ERROR] = g_signal_new ("error",
+ WEB_SOCKET_TYPE_CONNECTION,
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (WebSocketConnectionClass, error),
+ g_signal_accumulator_true_handled, NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 1, G_TYPE_ERROR);
+
+ /**
+ * WebSocketConnection::closing:
+ * @self: the WebSocket
+ *
+ * This signal will be emitted during an orderly close
+ */
+ signals[CLOSING] = g_signal_new ("closing",
+ WEB_SOCKET_TYPE_CONNECTION,
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (WebSocketConnectionClass, closing),
+ g_signal_accumulator_true_handled, NULL, g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 0);
+
+ klass->error = web_socket_connection_default_error;
+ klass->closing = web_socket_connection_default_closing;
+
+ /**
+ * WebSocketConnection::close:
+ * @self: the WebSocket
+ *
+ * Emitted when the connection has completely closed, either due to an
+ * orderly close from the peer, one initiated via web_socket_connection_close()
+ * or a fatal error condition that caused a close.
+ *
+ * This signal will be emitted once.
+ */
+ signals[CLOSE] = g_signal_new ("close",
+ WEB_SOCKET_TYPE_CONNECTION,
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET (WebSocketConnectionClass, close),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 0);
+}
+
+/**
+ * web_socket_connection_get_url:
+ * @self: the WebSocket
+ *
+ * Get the URL of the WebSocket.
+ *
+ * For servers this represents the address of the WebSocket, and
+ * for clients it is the address connected to.
+ *
+ * Returns: the URL
+ */
+const gchar *
+web_socket_connection_get_url (WebSocketConnection *self)
+{
+ g_return_val_if_fail (WEB_SOCKET_IS_CONNECTION (self), NULL);
+ return GET_PRIV(self)->url;
+}
+
+/**
+ * web_socket_connection_get_protocol:
+ * @self: the WebSocket
+ *
+ * Get the protocol chosen via negotiation with the peer.
+ *
+ * A list of possible protocols is provided when creating a #WebSocketClient
+ * or #WebSocketServer, and one is negotiated during the handshake.
+ *
+ * This will be %NULL until the WebSocket is in the %WEB_SOCKET_STATE_OPEN
+ * state.
+ *
+ * Returns: the chosen protocol or %NULL
+ */
+const gchar *
+web_socket_connection_get_protocol (WebSocketConnection *self)
+{
+ g_return_val_if_fail (WEB_SOCKET_IS_CONNECTION (self), NULL);
+ return GET_PRIV(self)->chosen_protocol;
+}
+
+/**
+ * web_socket_connection_get_ready_state:
+ * @self: the WebSocket
+ *
+ * Get the current state of the WebSocket.
+ *
+ * Returns: the state
+ */
+WebSocketState
+web_socket_connection_get_ready_state (WebSocketConnection *self)
+{
+ g_return_val_if_fail (WEB_SOCKET_IS_CONNECTION (self), 0);
+
+ if (GET_PRIV(self)->io_closed)
+ return WEB_SOCKET_STATE_CLOSED;
+ else if ((GET_PRIV(self)->io_stream && !GET_PRIV(self)->io_open) || GET_PRIV(self)->close_sent)
+ return WEB_SOCKET_STATE_CLOSING;
+ else if (GET_PRIV(self)->handshake_done)
+ return WEB_SOCKET_STATE_OPEN;
+ else
+ return WEB_SOCKET_STATE_CONNECTING;
+}
+
+/**
+ * web_socket_connection_get_buffered_amount:
+ * @self: the WebSocket
+ *
+ * Get the amount of buffered data not yet sent.
+ *
+ * This represents caller provided data passed into the
+ * web_socket_connection_send() function.
+ *
+ * Returns: the amount of buffered data
+ */
+gsize
+web_socket_connection_get_buffered_amount (WebSocketConnection *self)
+{
+ gsize amount = 0;
+ Frame *frame;
+ GList *l;
+
+ g_return_val_if_fail (WEB_SOCKET_IS_CONNECTION (self), 0);
+
+ for (l = GET_PRIV(self)->outgoing.head; l != NULL; l = g_list_next (l))
+ {
+ frame = l->data;
+ amount += frame->amount;
+ }
+
+ return amount;
+}
+
+/**
+ * web_socket_connection_get_io_stream:
+ * @self: the WebSocket
+ *
+ * Get the IO stream the WebSocket is communicating over.
+ *
+ * Returns: (transfer none): the amount of buffered data
+ */
+GIOStream *
+web_socket_connection_get_io_stream (WebSocketConnection *self)
+{
+ g_return_val_if_fail (WEB_SOCKET_IS_CONNECTION (self), NULL);
+ return GET_PRIV(self)->io_stream;
+}
+
+/**
+ * web_socket_connection_get_close_code:
+ * @self: the WebSocket
+ *
+ * Get the close code received from the WebSocket peer.
+ *
+ * This only becomes valid once the WebSocket is in the
+ * %WEB_SOCKET_STATE_CLOSED state. The value will often be in the
+ * #WebSocketCloseCodes enumeration, but may also be an application
+ * defined close code.
+ *
+ * Returns: the close code or zero.
+ */
+gushort
+web_socket_connection_get_close_code (WebSocketConnection *self)
+{
+ g_return_val_if_fail (WEB_SOCKET_IS_CONNECTION (self), 0);
+ return GET_PRIV(self)->peer_close_code;
+}
+
+/**
+ * web_socket_connection_get_close_data:
+ * @self: the WebSocket
+ *
+ * Get the close data received from the WebSocket peer.
+ *
+ * This only becomes valid once the WebSocket is in the
+ * %WEB_SOCKET_STATE_CLOSED state. The data may be freed once
+ * the main loop is run, so copy it if you need to keep it around.
+ *
+ * Returns: the close data or %NULL
+ */
+const gchar *
+web_socket_connection_get_close_data (WebSocketConnection *self)
+{
+ g_return_val_if_fail (WEB_SOCKET_IS_CONNECTION (self), NULL);
+ return GET_PRIV(self)->peer_close_data;
+}
+
+/**
+ * web_socket_connection_send:
+ * @self: the WebSocket
+ * @type: the data type of message
+ * @prefix: (allow-none): an optional prefix prepended to the message
+ * @message: the message contents
+ *
+ * Send a message to the peer.
+ *
+ * The @type parameter describes whether this is a binary or text message.
+ * If a text message then the contents must be UTF-8 valid.
+ *
+ * The message is queued to be sent and will be sent when the main loop
+ * is run.
+ *
+ * The optional @prefix can be a canned header to be prefixed to the message.
+ * It can be specified as a separate argument for efficiency.
+ */
+void
+web_socket_connection_send (WebSocketConnection *self,
+ WebSocketDataType type,
+ GBytes *prefix,
+ GBytes *message)
+{
+ gconstpointer pref = NULL;
+ gsize prefix_len = 0;
+ gconstpointer payload;
+ gsize payload_len;
+ guint8 opcode;
+
+ g_return_if_fail (WEB_SOCKET_IS_CONNECTION (self));
+ g_return_if_fail (message != NULL);
+
+ if (web_socket_connection_get_ready_state (self) != WEB_SOCKET_STATE_OPEN)
+ {
+ g_critical ("Can only send messages when WebSocket is open");
+ return;
+ }
+
+ if (prefix)
+ pref = g_bytes_get_data (prefix, &prefix_len);
+ payload = g_bytes_get_data (message, &payload_len);
+
+ switch (type)
+ {
+ case WEB_SOCKET_DATA_TEXT:
+ opcode = 0x01;
+ if (!g_utf8_validate (pref, prefix_len, NULL) ||
+ !g_utf8_validate (payload, payload_len, NULL))
+ {
+ g_critical ("invalid non-UTF8 @data passed as text to web_socket_connection_send()");
+ return;
+ }
+ break;
+ case WEB_SOCKET_DATA_BINARY:
+ opcode = 0x02;
+ break;
+ default:
+ g_critical ("invalid @type argument for web_socket_connection_send()");
+ return;
+ }
+
+ send_prefixed_message_rfc6455 (self, WEB_SOCKET_QUEUE_NORMAL, opcode,
+ pref, prefix_len, payload, payload_len);
+
+ g_object_notify (G_OBJECT (self), "buffered-amount");
+}
+
+/**
+ * web_socket_connection_close:
+ * @self: the WebSocket
+ * @code: close code
+ * @data: (allow-none): close data
+ *
+ * Close the connection in an orderly fashion.
+ *
+ * Note that until the #WebSocketConnection::close signal fires, the connection
+ * is not yet completely closed. The close message is not even sent until the
+ * main loop runs.
+ *
+ * The @code and @data are sent to the peer along with the close request.
+ * Note that the @data must be UTF-8 valid.
+ */
+void
+web_socket_connection_close (WebSocketConnection *self,
+ gushort code,
+ const gchar *data)
+{
+ WebSocketQueueFlags flags;
+ gboolean handled = FALSE;
+
+ g_return_if_fail (WEB_SOCKET_IS_CONNECTION (self));
+ g_return_if_fail (!GET_PRIV(self)->close_sent);
+
+ g_signal_emit (self, signals[CLOSING], 0, &handled);
+ if (!handled)
+ return;
+
+ if (GET_PRIV(self)->close_received)
+ g_debug ("responding to close request");
+
+ if (GET_PRIV(self)->handshake_done)
+ {
+ flags = 0;
+ if (GET_PRIV(self)->server_side && GET_PRIV(self)->close_received)
+ flags |= WEB_SOCKET_QUEUE_LAST;
+ send_close_rfc6455 (self, flags, code, data);
+ close_io_after_timeout (self);
+ }
+ else
+ {
+ close_io_stream (self);
+ }
+}
+
+static void
+on_throttle_pressure (GObject *object,
+ gboolean throttle,
+ gpointer user_data)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (user_data);
+ if (throttle)
+ {
+ if (GET_PRIV(self)->io_open && GET_PRIV(self)->input_source != NULL)
+ {
+ g_debug ("applying back pressure in web socket");
+ stop_input (self);
+ }
+ }
+ else
+ {
+ if (GET_PRIV(self)->io_open && GET_PRIV(self)->input_source == NULL)
+ {
+ g_debug ("relieving back pressure in web socket");
+ start_input (self);
+ }
+ }
+}
+
+static void
+web_socket_connection_throttle (CockpitFlow *flow,
+ CockpitFlow *controlling)
+{
+ WebSocketConnection *self = WEB_SOCKET_CONNECTION (flow);
+
+ if (GET_PRIV(self)->pressure)
+ {
+ g_signal_handler_disconnect (GET_PRIV(self)->pressure, GET_PRIV(self)->pressure_sig);
+ g_object_remove_weak_pointer (G_OBJECT (GET_PRIV(self)->pressure), (gpointer *)&GET_PRIV(self)->pressure);
+ GET_PRIV(self)->pressure = NULL;
+ }
+
+ if (controlling)
+ {
+ GET_PRIV(self)->pressure = controlling;
+ g_object_add_weak_pointer (G_OBJECT (GET_PRIV(self)->pressure), (gpointer *)&GET_PRIV(self)->pressure);
+ GET_PRIV(self)->pressure_sig = g_signal_connect (controlling, "pressure", G_CALLBACK (on_throttle_pressure), self);
+ }
+}
+
+gboolean
+_web_socket_connection_choose_protocol (WebSocketConnection *self,
+ const gchar **protocols,
+ const gchar *value)
+{
+ WebSocketConnectionPrivate *pv = web_socket_connection_get_instance_private (self);
+ gboolean chosen = FALSE;
+ gchar **values;
+ gint i, j;
+
+ g_free (pv->chosen_protocol);
+ pv->chosen_protocol = NULL;
+
+ /* Automatically select one */
+ if (!value)
+ {
+ if (protocols)
+ {
+ pv->chosen_protocol = g_strdup (protocols[0]);
+ g_debug ("automatically selected protocol: %s", pv->chosen_protocol);
+ }
+ g_object_notify (G_OBJECT (self), "protocol");
+ return TRUE;
+ }
+
+ /* Choose one from what client/server agree on */
+ if (!g_str_is_ascii (value))
+ {
+ /* splitting into words by comma might interfere with multi-byte characters,
+ * and they are invalid here anyway */
+ g_message ("received invalid Sec-WebSocket-Protocol, must be ASCII: %s", value);
+ return FALSE;
+ }
+ values = g_strsplit_set (value, ", ", -1);
+
+ /* Accept any protocol */
+ if (!protocols)
+ {
+ pv->chosen_protocol = g_strdup (values[0]);
+ g_debug ("automatically selected protocol: %s", pv->chosen_protocol);
+ chosen = TRUE;
+ }
+
+ for (j = 0; !chosen && values[j] != NULL; j++)
+ {
+ for (i = 0; protocols[i] != NULL; i++)
+ {
+ if (g_str_equal (protocols[i], values[j]))
+ {
+ pv->chosen_protocol = g_strdup (values[j]);
+ g_debug ("agreed on protocol: %s", pv->chosen_protocol);
+ chosen = TRUE;
+ }
+ }
+ }
+ g_strfreev (values);
+
+ if (chosen)
+ g_object_notify (G_OBJECT (self), "protocol");
+ else
+ g_message ("received invalid or unsupported Sec-WebSocket-Protocol: %s", value);
+
+ return chosen;
+}
+
+GMainContext *
+_web_socket_connection_get_main_context (WebSocketConnection *self)
+{
+ g_return_val_if_fail (WEB_SOCKET_IS_CONNECTION (self), NULL);
+ return GET_PRIV(self)->main_context;
+}
+
+static void
+web_socket_connection_flow_iface_init (CockpitFlowInterface *iface)
+{
+ iface->throttle = web_socket_connection_throttle;
+}
+
diff --git a/src/websocket/websocketconnection.h b/src/websocket/websocketconnection.h
new file mode 100644
index 0000000..434c46f
--- /dev/null
+++ b/src/websocket/websocketconnection.h
@@ -0,0 +1,80 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "websocket.h"
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define WEB_SOCKET_TYPE_CONNECTION (web_socket_connection_get_type ())
+G_DECLARE_DERIVABLE_TYPE(WebSocketConnection, web_socket_connection, WEB_SOCKET, CONNECTION, GObject)
+
+struct _WebSocketConnectionClass
+{
+ GObjectClass parent;
+
+ /* set by derived */
+ gboolean server_behavior;
+
+ /* vfuncs */
+ gboolean (* handshake) (WebSocketConnection *self,
+ GByteArray *incoming);
+
+ /* signals */
+ void (* open) (WebSocketConnection *self);
+
+ void (* message) (WebSocketConnection *self,
+ WebSocketDataType type,
+ GBytes *message);
+
+ gboolean (* error) (WebSocketConnection *self,
+ GError *error);
+
+ gboolean (* closing) (WebSocketConnection *self);
+
+ void (* close) (WebSocketConnection *self);
+};
+
+GType web_socket_connection_get_type (void) G_GNUC_CONST;
+
+const gchar * web_socket_connection_get_url (WebSocketConnection *self);
+
+const gchar * web_socket_connection_get_protocol (WebSocketConnection *self);
+
+WebSocketState web_socket_connection_get_ready_state (WebSocketConnection *self);
+
+gsize web_socket_connection_get_buffered_amount (WebSocketConnection *self);
+
+gushort web_socket_connection_get_close_code (WebSocketConnection *self);
+
+const gchar * web_socket_connection_get_close_data (WebSocketConnection *self);
+
+GIOStream * web_socket_connection_get_io_stream (WebSocketConnection *self);
+
+void web_socket_connection_send (WebSocketConnection *self,
+ WebSocketDataType type,
+ GBytes *prefix,
+ GBytes *payload);
+
+void web_socket_connection_close (WebSocketConnection *self,
+ gushort code,
+ const gchar *data);
diff --git a/src/websocket/websocketprivate.h b/src/websocket/websocketprivate.h
new file mode 100644
index 0000000..0f73d39
--- /dev/null
+++ b/src/websocket/websocketprivate.h
@@ -0,0 +1,79 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __WEB_SOCKET_PRIVATE_H__
+#define __WEB_SOCKET_PRIVATE_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+gboolean _web_socket_util_parse_url (const gchar *url,
+ gchar **out_scheme,
+ gchar **out_host,
+ gchar **out_path,
+ GError **error);
+
+gboolean _web_socket_util_header_equals (GHashTable *headers,
+ const gchar *name,
+ const gchar *want);
+
+gboolean _web_socket_util_header_contains (GHashTable *headers,
+ const gchar *name,
+ const gchar *word);
+
+gboolean _web_socket_util_header_empty (GHashTable *headers,
+ const gchar *name);
+
+typedef enum {
+ WEB_SOCKET_QUEUE_NORMAL = 0,
+ WEB_SOCKET_QUEUE_URGENT = 1 << 0,
+ WEB_SOCKET_QUEUE_LAST = 1 << 1,
+} WebSocketQueueFlags;
+
+void _web_socket_connection_queue (WebSocketConnection *conn,
+ WebSocketQueueFlags flags,
+ gpointer frame,
+ gsize length,
+ gsize buffered_amount);
+
+GMainContext * _web_socket_connection_get_main_context (WebSocketConnection *self);
+
+gboolean _web_socket_connection_error (WebSocketConnection *self,
+ GError *error);
+
+void _web_socket_connection_error_and_close (WebSocketConnection *self,
+ GError *error,
+ gboolean prejudice);
+
+void _web_socket_connection_take_io_stream (WebSocketConnection *self,
+ GIOStream *io_stream);
+
+void _web_socket_connection_take_incoming (WebSocketConnection *self,
+ GByteArray *input_buffer);
+
+gboolean _web_socket_connection_choose_protocol (WebSocketConnection *self,
+ const gchar **protocols,
+ const gchar *value);
+
+gchar * _web_socket_complete_accept_key_rfc6455 (const gchar *key);
+
+G_END_DECLS
+
+#endif /* __WEB_SOCKET_PRIVATE_H__ */
diff --git a/src/websocket/websocketserver.c b/src/websocket/websocketserver.c
new file mode 100644
index 0000000..dbfe983
--- /dev/null
+++ b/src/websocket/websocketserver.c
@@ -0,0 +1,493 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "websocketserver.h"
+#include "websocketprivate.h"
+
+#include <string.h>
+
+enum {
+ PROP_0,
+ PROP_ORIGINS,
+ PROP_PROTOCOLS,
+ PROP_REQUEST_HEADERS,
+ PROP_INPUT_BUFFER,
+};
+
+struct _WebSocketServer
+{
+ WebSocketConnection parent;
+
+ gboolean protocol_chosen;
+ gchar **allowed_origins;
+ gchar **allowed_protocols;
+ GHashTable *request_headers;
+};
+
+struct _WebSocketServerClass
+{
+ WebSocketConnectionClass parent;
+};
+
+G_DEFINE_TYPE (WebSocketServer, web_socket_server, WEB_SOCKET_TYPE_CONNECTION);
+
+static void
+web_socket_server_init (WebSocketServer *self)
+{
+
+}
+
+static void
+respond_handshake_forbidden (WebSocketConnection *conn)
+{
+ GError *error;
+
+ const gchar *bad_request = "HTTP/1.1 403 Forbidden\r\n"
+ "Connection: close\r\n"
+ "\r\n"
+ "<html><head><title>403 Forbidden</title></head>\r\n"
+ "<body>Received invalid WebSocket request</body></html>\r\n";
+
+ _web_socket_connection_queue (conn, WEB_SOCKET_QUEUE_URGENT | WEB_SOCKET_QUEUE_LAST,
+ g_strdup (bad_request), strlen (bad_request), 0);
+ g_debug ("queued: forbidden request response");
+
+ error = g_error_new_literal (WEB_SOCKET_ERROR,
+ WEB_SOCKET_CLOSE_PROTOCOL,
+ "Received invalid handshake request from the client");
+ _web_socket_connection_error (conn, error);
+}
+
+static void
+respond_handshake_bad (WebSocketConnection *conn)
+{
+ GError *error;
+
+ const gchar *bad_request = "HTTP/1.1 400 Bad Request\r\n"
+ "Connection: close\r\n"
+ "\r\n"
+ "<html><head><title>400 Bad Request</title></head>\r\n"
+ "<body>Received invalid WebSocket request</body></html>\r\n";
+
+ _web_socket_connection_queue (conn, WEB_SOCKET_QUEUE_URGENT | WEB_SOCKET_QUEUE_LAST,
+ g_strdup (bad_request), strlen (bad_request), 0);
+ g_debug ("queued: bad request response");
+
+ error = g_error_new_literal (WEB_SOCKET_ERROR,
+ WEB_SOCKET_CLOSE_PROTOCOL,
+ "Received invalid handshake request from the client");
+ _web_socket_connection_error (conn, error);
+}
+
+gchar *
+_web_socket_complete_accept_key_rfc6455 (const gchar *key)
+{
+ gsize digest_len = 20;
+ guchar digest[digest_len];
+ GChecksum *checksum;
+
+ checksum = g_checksum_new (G_CHECKSUM_SHA1);
+ g_return_val_if_fail (checksum != NULL, NULL);
+
+ g_checksum_update (checksum, (guchar *)key, -1);
+
+ /* magic from: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 */
+ g_checksum_update (checksum, (guchar *)"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", -1);
+
+ g_checksum_get_digest (checksum, digest, &digest_len);
+ g_checksum_free (checksum);
+
+ g_assert (digest_len == 20);
+
+ return g_base64_encode (digest, digest_len);
+}
+
+static gboolean
+validate_rfc6455_websocket_key (const gchar *key)
+{
+ /* The key must be 16 bytes base64 encoded */
+ guchar *decoded;
+ gsize length, len;
+ len = strlen (key);
+ if (len == 0 || len > 1024)
+ return FALSE;
+ decoded = g_base64_decode (key, &length);
+ if (!decoded)
+ return FALSE;
+ g_free (decoded);
+ return length == 16;
+}
+
+static gboolean
+respond_handshake_rfc6455 (WebSocketServer *self,
+ WebSocketConnection *conn,
+ GHashTable *headers)
+{
+ const gchar *protocol;
+ const gchar *origin;
+ const gchar *host;
+ gchar *accept_key;
+ gchar *key;
+ GString *handshake;
+ gsize len;
+ guint i;
+
+ if (!_web_socket_util_header_equals (headers, "Upgrade", "websocket") ||
+ !_web_socket_util_header_contains (headers, "Connection", "upgrade") ||
+ !_web_socket_util_header_equals (headers, "Sec-WebSocket-Version", "13") ||
+ !_web_socket_connection_choose_protocol (conn, (const gchar **)self->allowed_protocols,
+ g_hash_table_lookup (headers, "Sec-WebSocket-Protocol")))
+ {
+ respond_handshake_bad (conn);
+ return FALSE;
+ }
+
+ self->protocol_chosen = TRUE;
+
+ key = g_hash_table_lookup (headers, "Sec-WebSocket-Key");
+ if (key == NULL)
+ {
+ g_message ("received missing Sec-WebSocket-Key header");
+ respond_handshake_bad (conn);
+ return FALSE;
+ }
+ if (!validate_rfc6455_websocket_key (key))
+ {
+ g_message ("received invalid Sec-WebSocket-Key header: %s", key);
+ respond_handshake_bad (conn);
+ return FALSE;
+ }
+
+ host = g_hash_table_lookup (headers, "Host");
+ if (host == NULL)
+ {
+ g_message ("received request without Host");
+ respond_handshake_bad (conn);
+ return FALSE;
+ }
+
+ if (self->allowed_origins)
+ {
+ origin = g_hash_table_lookup (headers, "Origin");
+ if (!origin)
+ {
+ g_message ("received request without Origin");
+ respond_handshake_forbidden (conn);
+ return FALSE;
+ }
+ for (i = 0; self->allowed_origins[i] != NULL; i++)
+ {
+ if (g_ascii_strcasecmp (origin, self->allowed_origins[i]) == 0)
+ break;
+ }
+ if (self->allowed_origins[i] == NULL)
+ {
+ g_message ("received request from bad Origin: %s", origin);
+ respond_handshake_forbidden (conn);
+ return FALSE;
+ }
+ }
+
+ accept_key = _web_socket_complete_accept_key_rfc6455 (key);
+
+ handshake = g_string_new ("");
+ g_string_printf (handshake, "HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: %s\r\n",
+ (gchar *)accept_key);
+
+ g_free (accept_key);
+
+ protocol = web_socket_connection_get_protocol (conn);
+ if (protocol)
+ g_string_append_printf (handshake, "Sec-WebSocket-Protocol: %s\r\n", protocol);
+
+ g_string_append (handshake, "\r\n");
+
+ len = handshake->len;
+ _web_socket_connection_queue (conn, WEB_SOCKET_QUEUE_URGENT,
+ g_string_free (handshake, FALSE), len, 0);
+ g_debug ("queued response to rfc6455 handshake");
+
+ return TRUE;
+}
+
+static gboolean
+parse_handshake_request (WebSocketServer *self,
+ WebSocketConnection *conn,
+ GByteArray *incoming)
+{
+ GHashTable *headers;
+ g_autofree gchar *method = NULL;
+ g_autofree gchar *resource = NULL;
+ gboolean valid;
+ gssize in1, in2;
+ gssize consumed;
+ const gchar *url;
+
+ /* Headers already passed from caller */
+ if (self->request_headers)
+ {
+ headers = self->request_headers;
+ self->request_headers = NULL;
+ method = g_strdup ("GET");
+
+ url = web_socket_connection_get_url (conn);
+ if (!_web_socket_util_parse_url (url, NULL, NULL, &resource, NULL))
+ resource = g_strdup ("/");
+
+ consumed = 0;
+ }
+ else
+ {
+ /* Parse the handshake response received from the server */
+ in1 = web_socket_util_parse_req_line ((const gchar *)incoming->data,
+ incoming->len, &method, &resource);
+ if (in1 < 0)
+ {
+ g_message ("received invalid request line");
+ respond_handshake_bad (conn);
+ }
+ else if (in1 == 0)
+ g_debug ("waiting for more handshake data");
+ if (in1 <= 0)
+ return FALSE;
+
+ /* Read in the handshake request from the client */
+ in2 = web_socket_util_parse_headers ((const gchar *)incoming->data + in1,
+ incoming->len - in1, &headers);
+
+ if (in2 < 0)
+ {
+ g_message ("received invalid response headers");
+ respond_handshake_bad (conn);
+ }
+ else if (in2 == 0)
+ g_debug ("waiting for more handshake data");
+ if (in2 <= 0)
+ return FALSE;
+
+ consumed = in1 + in2;
+ }
+
+ if (!g_str_equal (method, "GET"))
+ {
+ g_message ("received unexpected method: %s %s", method, resource);
+ valid = FALSE;
+ }
+ else
+ {
+ valid = respond_handshake_rfc6455 (self, conn, headers);
+ }
+
+ if (valid)
+ {
+ /* Handshake is successful */
+ g_debug ("open: responded to handshake");
+ }
+
+ if (consumed > 0)
+ g_byte_array_remove_range (incoming, 0, consumed);
+ g_hash_table_unref (headers);
+
+ return valid;
+}
+
+static gboolean
+web_socket_server_handshake (WebSocketConnection *conn,
+ GByteArray *incoming)
+{
+ WebSocketServer *self = WEB_SOCKET_SERVER (conn);
+ return parse_handshake_request (self, conn, incoming);
+}
+
+static void
+web_socket_server_constructed (GObject *object)
+{
+ WebSocketConnection *conn = WEB_SOCKET_CONNECTION (object);
+ GIOStream *io_stream;
+
+ G_OBJECT_CLASS (web_socket_server_parent_class)->constructed (object);
+
+ io_stream = web_socket_connection_get_io_stream (conn);
+ if (io_stream == NULL)
+ {
+ g_critical ("server-side WebSocketConnection must be created "
+ "with a io-stream property");
+ }
+}
+
+static void
+web_socket_server_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ WebSocketServer *self = WEB_SOCKET_SERVER (object);
+
+ switch (prop_id)
+ {
+ case PROP_ORIGINS:
+ g_return_if_fail (self->allowed_origins == FALSE);
+ self->allowed_origins = g_value_dup_boxed (value);
+ break;
+
+ case PROP_PROTOCOLS:
+ g_return_if_fail (self->protocol_chosen == FALSE);
+ g_strfreev (self->allowed_protocols);
+ self->allowed_protocols = g_value_dup_boxed (value);
+ break;
+
+ case PROP_REQUEST_HEADERS:
+ g_return_if_fail (self->request_headers == NULL);
+ self->request_headers = g_value_dup_boxed (value);
+ break;
+
+ case PROP_INPUT_BUFFER:
+ _web_socket_connection_take_incoming (WEB_SOCKET_CONNECTION (self),
+ g_value_dup_boxed (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+
+ }
+}
+
+static void
+web_socket_server_finalize (GObject *object)
+{
+ WebSocketServer *self = WEB_SOCKET_SERVER (object);
+
+ g_strfreev (self->allowed_origins);
+ g_strfreev (self->allowed_protocols);
+ if (self->request_headers)
+ g_hash_table_unref (self->request_headers);
+
+ G_OBJECT_CLASS (web_socket_server_parent_class)->finalize (object);
+}
+
+static void
+web_socket_server_class_init (WebSocketServerClass *klass)
+{
+ WebSocketConnectionClass *conn_class = WEB_SOCKET_CONNECTION_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = web_socket_server_constructed;
+ object_class->set_property = web_socket_server_set_property;
+ object_class->finalize = web_socket_server_finalize;
+
+ conn_class->server_behavior = TRUE;
+ conn_class->handshake = web_socket_server_handshake;
+
+ /**
+ * WebSocketServer:origins:
+ *
+ * The allowed origins to receive client requests from.
+ */
+ g_object_class_install_property (object_class, PROP_ORIGINS,
+ g_param_spec_boxed ("origins", "Possible Origins", "The possible HTTP origins", G_TYPE_STRV,
+ G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+ /**
+ * WebSocketServer:protocols:
+ *
+ * The allowed protocols to negotiate with the client.
+ */
+ g_object_class_install_property (object_class, PROP_PROTOCOLS,
+ g_param_spec_boxed ("protocols", "Possible Protocol", "The possible WebSocket protocols", G_TYPE_STRV,
+ G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * WebSocketServer:request-headers:
+ *
+ * If headers have already been parsed, passed in here.
+ */
+ g_object_class_install_property (object_class, PROP_REQUEST_HEADERS,
+ g_param_spec_boxed ("request-headers", "Request Headers", "Already parsed headers", G_TYPE_HASH_TABLE,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * WebSocketServer:input-buffer:
+ *
+ * When specifying a #WebSocketConnection:io-stream during construction,
+ * if you've already read bytes (ie: containing an HTTP header) out of the
+ * input stream, then you must pass in a buffer containing those initial bytes,
+ * so the WebSocket can consume them.
+ *
+ * This is usually only useful for WebSocket server connections. See
+ * web_socket_server_new_for_stream()
+ */
+ g_object_class_install_property (object_class, PROP_INPUT_BUFFER,
+ g_param_spec_boxed ("input-buffer", "Input buffer", "Input buffer with seed data", G_TYPE_BYTE_ARRAY,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+}
+
+/**
+ * web_socket_server_new_for_stream:
+ * @url: the url address of the WebSocket
+ * @origins: (allow-none): the origin to expect the client to report
+ * @protocols: (allow-none): possible protocols for the client to
+ * @io_stream: the IO stream to communicate over
+ * @request_headers: (allow-none): already parsed headers, or %NULL
+ * @input_buffer: (allow-none): initial bytes already read from the input stream, or %NULL
+ *
+ * Create a new server side WebSocket connection to communicate with a client.
+ *
+ * Since callers may have already read some bytes from the inputstream (ie:
+ * the HTTP header Request-Line) those bytes should be included in the
+ * @input_buffer argument so that the WebSocket can consume them.
+ *
+ * If @protocols are specified then these are used to negotiate a protocol
+ * with the client.
+ *
+ * The input and output streams of the @io_stream must be pollable.
+ *
+ * If the input stream on the @io_stream has already been read, those
+ * read bytes should be passed in the @input_buffer byte array.
+ *
+ * In addition if the HTTP headers have already been parsed, they should be
+ * passed in using the @request_headers hash table. This should be a hash table
+ * setup for case-insensitive lookups, as created by web_socket_util_new_headers().
+ * When passing in headers, fill in @input_buffer with any of the HTTP body
+ * read from the input stream (ie: after the \r\n\r\n).
+ *
+ * Returns: (transfer full): a new WebSocket
+ */
+WebSocketConnection *
+web_socket_server_new_for_stream (const gchar *url,
+ const gchar * const *origins,
+ const gchar * const *protocols,
+ GIOStream *io_stream,
+ GHashTable *request_headers,
+ GByteArray *input_buffer)
+{
+ return g_object_new (WEB_SOCKET_TYPE_SERVER,
+ "url", url,
+ "origins", origins,
+ "protocols", protocols,
+ "io-stream", io_stream,
+ "request-headers", request_headers,
+ "input-buffer", input_buffer,
+ NULL);
+}
diff --git a/src/websocket/websocketserver.h b/src/websocket/websocketserver.h
new file mode 100644
index 0000000..e9232ec
--- /dev/null
+++ b/src/websocket/websocketserver.h
@@ -0,0 +1,46 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __WEB_SOCKET_SERVER_H__
+#define __WEB_SOCKET_SERVER_H__
+
+#include "websocketconnection.h"
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define WEB_SOCKET_TYPE_SERVER (web_socket_server_get_type ())
+#define WEB_SOCKET_SERVER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), WEB_SOCKET_TYPE_SERVER, WebSocketServer))
+#define WEB_SOCKET_IS_SERVER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), WEB_SOCKET_TYPE_SERVER))
+#define WEB_SOCKET_SERVER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), WEB_SOCKET_TYPE_SERVER, WebSocketServerClass))
+#define WEB_SOCKET_IS_SERVER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), WEB_SOCKET_TYPE_SERVER))
+
+GType web_socket_server_get_type (void) G_GNUC_CONST;
+
+WebSocketConnection * web_socket_server_new_for_stream (const gchar *url,
+ const gchar * const *origins,
+ const gchar * const *protocols,
+ GIOStream *io_stream,
+ GHashTable *request_headers,
+ GByteArray *input_buffer);
+
+G_END_DECLS
+
+#endif /* __WEB_SOCKET_SERVER_H__ */
diff --git a/src/ws/Makefile-ws.am b/src/ws/Makefile-ws.am
new file mode 100644
index 0000000..ac14d95
--- /dev/null
+++ b/src/ws/Makefile-ws.am
@@ -0,0 +1,204 @@
+# -----------------------------------------------------------------------------
+# libcockpit-ws.a: code used in cockpit-ws and its tests
+
+noinst_LIBRARIES += libcockpit-ws.a
+
+libcockpit_ws_a_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"cockpit-ws\" \
+ $(glib_CFLAGS) \
+ $(json_glib_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+libcockpit_ws_a_LIBS = \
+ libcockpit-ws.a \
+ $(libcockpit_common_a_LIBS) \
+ $(libsystemd_LIBS) \
+ -lcrypt \
+ $(NULL)
+
+libcockpit_ws_a_SOURCES = \
+ src/ws/cockpitws.h \
+ src/ws/cockpithandlers.h \
+ src/ws/cockpithandlers.c \
+ src/ws/cockpitauth.h \
+ src/ws/cockpitauth.c \
+ src/ws/cockpitcompat.c \
+ src/ws/cockpitcompat.h \
+ src/ws/cockpitbranding.h \
+ src/ws/cockpitbranding.c \
+ src/ws/cockpitchannelresponse.h \
+ src/ws/cockpitchannelresponse.c \
+ src/ws/cockpitchannelsocket.h \
+ src/ws/cockpitchannelsocket.c \
+ src/ws/cockpitcreds.h src/ws/cockpitcreds.c \
+ src/ws/cockpitwebservice.h \
+ src/ws/cockpitwebservice.c \
+ $(NULL)
+
+# -----------------------------------------------------------------------------
+# cockpit-ws
+
+cockpitwsdir = $(libexecdir)
+cockpitws_PROGRAMS = cockpit-ws
+
+cockpit_ws_SOURCES = \
+ src/ws/main.c \
+ $(NULL)
+
+cockpit_ws_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS)
+cockpit_ws_LDADD = $(libcockpit_ws_a_LIBS)
+
+# -----------------------------------------------------------------------------
+# pam_cockpit_cert.so: Client certificate authentication PAM module
+
+pam_PROGRAMS += pam_cockpit_cert.so
+pam_cockpit_cert_so_CFLAGS = -fPIC $(AM_CFLAGS)
+pam_cockpit_cert_so_LDADD = -lpam
+pam_cockpit_cert_so_LDFLAGS = -shared
+pam_cockpit_cert_so_SOURCES = src/ws/pam_cockpit_cert.c
+
+# -----------------------------------------------------------------------------
+# test-server: server for running the html/browser unit tests against
+
+check_PROGRAMS += test-server
+
+test_server_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_server_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+
+test_server_SOURCES = \
+ src/ws/mock-service.c \
+ src/ws/mock-service.h \
+ src/ws/test-server.c \
+ $(NULL)
+
+test_server_CPPFLAGS += -I$(top_builddir)/src/ws
+nodist_test_server_SOURCES = $(GDBUS_CODEGEN_GENERATED)
+
+BUILT_SOURCES += $(GDBUS_CODEGEN_GENERATED)
+CLEANFILES += $(GDBUS_CODEGEN_GENERATED)
+GDBUS_CODEGEN_GENERATED = \
+ src/ws/mock-dbus-tests.h \
+ src/ws/mock-dbus-tests.c \
+ $(NULL)
+
+EXTRA_DIST += $(GDBUS_CODEGEN_XML)
+GDBUS_CODEGEN_XML = $(srcdir)/src/ws/com.redhat.Cockpit.DBusTests.xml
+
+GDBUS_CODEGEN_INVOCATION = \
+ $(AM_V_GEN) gdbus-codegen \
+ --interface-prefix com.redhat.Cockpit.DBusTests \
+ --c-namespace Test \
+ --c-generate-object-manager \
+ $(NULL)
+
+src/ws/mock-dbus-tests.h: $(GDBUS_CODEGEN_XML)
+ $(GDBUS_CODEGEN_INVOCATION) --header --output $@ $<
+
+src/ws/mock-dbus-tests.c: $(GDBUS_CODEGEN_XML)
+ $(GDBUS_CODEGEN_INVOCATION) --body --output $@ $<
+
+# -----------------------------------------------------------------------------
+# Unit tests
+
+check_SCRIPTS += src/ws/mock-cat-with-init
+
+check_PROGRAMS += mock-echo
+mock_echo_CPPFLAGS = $(glib_CFLAGS) $(AM_CPPFLAGS)
+mock_echo_LDADD = $(glib_LIBS)
+mock_echo_SOURCES = src/ws/mock-echo.c
+
+check_PROGRAMS += mock-auth-command
+mock_auth_command_CPPFLAGS = $(libcockpit_common_a_CPPFLAGS) $(TEST_CPP)
+mock_auth_command_LDADD = $(libcockpit_common_a_LIBS) $(TEST_LIBS)
+mock_auth_command_SOURCES = src/ws/mock-auth-command.c
+
+TEST_PROGRAM += test-auth
+test_auth_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_auth_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_auth_SOURCES = src/ws/test-auth.c
+
+TEST_PROGRAM += test-compat
+test_compat_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_compat_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_compat_SOURCES = src/ws/test-compat.c
+
+TEST_PROGRAM += test-creds
+test_creds_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_creds_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_creds_SOURCES = src/ws/test-creds.c
+
+TEST_PROGRAM += test-kerberos
+test_kerberos_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_kerberos_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS) $(krb5_LIBS)
+test_kerberos_SOURCES = src/ws/test-kerberos.c
+
+if WITH_OLD_BRIDGE
+
+# These are -ws tests but they involve invoking ./cockpit-bridge.
+
+TEST_PROGRAM += test-channelresponse
+test_channelresponse_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_channelresponse_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_channelresponse_SOURCES = src/ws/test-channelresponse.c
+
+TEST_PROGRAM += test-handlers
+test_handlers_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_handlers_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_handlers_SOURCES = src/ws/test-handlers.c
+
+TEST_PROGRAM += test-webservice
+test_webservice_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_webservice_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_webservice_SOURCES = src/ws/test-webservice.c
+
+if WITH_COCKPIT_SSH
+
+TEST_PROGRAM += test-authssh
+test_authssh_CPPFLAGS = $(libcockpit_ws_a_CPPFLAGS) $(TEST_CPP)
+test_authssh_LDADD = $(libcockpit_ws_a_LIBS) $(TEST_LIBS)
+test_authssh_SOURCES = src/ws/test-authssh.c
+
+endif
+endif
+
+noinst_PROGRAMS += mock-pam-conv-mod.so
+mock_pam_conv_mod_so_SOURCES = src/ws/mock-pam-conv-mod.c
+mock_pam_conv_mod_so_CFLAGS = -fPIC $(AM_CFLAGS)
+mock_pam_conv_mod_so_LDFLAGS = -shared
+mock_pam_conv_mod_so_LDADD = $(PAM_LIBS)
+
+install-tests::
+ mkdir -p $(DESTDIR)$(pamdir) $(DESTDIR)/etc/cockpit
+ $(INSTALL_PROGRAM) mock-pam-conv-mod.so $(DESTDIR)$(pamdir)/
+
+install-exec-hook::
+ mkdir -p $(DESTDIR)$(sysconfdir)/cockpit/ws-certs.d $(DESTDIR)$(sysconfdir)/cockpit/machines.d
+ chmod 755 $(DESTDIR)$(sysconfdir)/cockpit/ws-certs.d $(DESTDIR)$(sysconfdir)/cockpit/machines.d
+
+dist_check_DATA += \
+ src/ws/mock-combined.crt \
+ src/ws/mock-ecc.crt \
+ src/ws/mock-ecc.key \
+ src/ws/mock-cat-with-init \
+ src/ws/mock-kdc \
+ src/ws/mock-krb5.conf.in \
+ src/ws/mock-kdc.conf.in \
+ src/ws/mock-static \
+ src/ws/mock-config \
+ src/ws/mock-pipes/exit127 \
+ src/ws/mock-pipes/someprogram \
+ src/ws/mock-pipes/cockpit-session \
+ $(NULL)
+
+appdatadir = $(datadir)/metainfo
+nodist_appdata_DATA = src/ws/cockpit.appdata.xml
+appdata_in = src/ws/cockpit.appdata.xml.in
+$(nodist_appdata_DATA): $(appdata_in) $(PO_FILES)
+ $(AM_V_GEN) msgfmt --xml -d $(top_srcdir)/po --template $< --output $@
+CLEANFILES += $(nodist_appdata_DATA)
+EXTRA_DIST += $(appdata_in)
+
+pixmapdir = $(datadir)/pixmaps
+dist_pixmap_DATA = src/ws/cockpit.png
+
+libexec_SCRIPTS += src/ws/cockpit-desktop
diff --git a/src/ws/cockpit-desktop.in b/src/ws/cockpit-desktop.in
new file mode 100644
index 0000000..eda22b0
--- /dev/null
+++ b/src/ws/cockpit-desktop.in
@@ -0,0 +1,139 @@
+#!/bin/bash
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2018 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+# Run a local bridge, web server, and browser for a particular Cockpit page.
+# This is useful for integration into .desktop files, for systems which don't
+# have cockpit.socket enabled. The web server and browser run in an unshared
+# network namespace, and thus are totally isolated from everything else.
+#
+# Examples:
+# cockpit-desktop /cockpit/@localhost/system/index.html
+# cockpit-desktop network/firewall
+# cockpit-desktop users
+#
+# As an experimental/demo feature, the bridge can also be started on a remote
+# ssh host. The host name is given as (optional) second argument, which is
+# passed verbatim to ssh.
+#
+# Example:
+# cockpit-desktop system svr1
+# cockpit-desktop / username@svr1
+set -eu
+
+# exec_prefix= is set because the default @libexecdir@ contains "${exec_prefix}"
+exec_prefix="@prefix@"
+libexecdir="@libexecdir@"
+
+# find suitable browser, unless already set by $BROWSER
+# We can't use xdg-open, it does too much magic behind the back to connect to
+# existing instances (outside of our namespace) and does not allow us to reduce
+# the UI, or pass options like chromium's --no-sandbox.
+detect_browser()
+{
+ [ -z "${BROWSER:-}" ] || return 0
+
+ # First choice, but it depends on gi.repository WebKit2, so check it
+ if @libexecdir@/cockpit-client --help >/dev/null 2>/dev/null; then
+ BROWSER="@libexecdir@/cockpit-client --disable-uniqueness --no-ui --external-ws"
+ return 0
+ fi
+
+ for browser in chromium-browser chromium google-chrome; do
+ if type $browser >/dev/null 2>&1; then
+ # need to disable sandboxing in user namespace, but that already isolates
+ # TODO: Find a way to disable the URL bar
+ BROWSER="$browser --no-sandbox --disable-infobars"
+ return 0
+ fi
+ done
+
+ if type firefox >/dev/null 2>&1; then
+ # TODO: Find a way to disable the privacy notice tab, via mozilla.cfg?
+ # TODO: Find a way to disable the URL bar
+ BROWSER="firefox --no-remote"
+ return 0
+ fi
+
+ # TODO: is there a simple way to use webkitgtk?
+ echo "No suitable browser found (Chromium/Chrome, or Firefox)" >&2
+ exit 1
+}
+
+
+if [ -z "${1:-}" ]; then
+ echo "Usage: $0 <Cockpit path> [ssh host]" >&2
+ exit 1
+fi
+
+# Expand the commandline argument into a url
+case "$1" in
+ /*)
+ URL_PATH="$1"
+ ;;
+ */)
+ URL_PATH="/cockpit/@localhost/$1index.html"
+ ;;
+ */*)
+ URL_PATH="/cockpit/@localhost/$1.html"
+ ;;
+ *)
+ URL_PATH="/cockpit/@localhost/$1/index.html"
+ ;;
+esac
+
+detect_browser
+
+# start the bridge; this needs to run in the normal user session/namespace
+coproc ${2:+ssh "$2"} cockpit-bridge
+trap "kill $COPROC_PID; wait $COPROC_PID || true" EXIT INT QUIT PIPE
+
+# start ws and browser in a detached network namespace
+SCRIPT='
+set -eu
+# new namespaces have lo down by default
+ip link set lo up >&2
+
+# start browser in a temporary home dir, so that it does not interfere with your real one
+export BROWSER_HOME=$(mktemp --directory --tmpdir cockpit.desktop.XXXXXX)
+
+# forward parent stdin and stdout (from bridge) to cockpit-ws
+# it pretty well does not matter which port we use in our own namespace, so use standard http
+# disable /etc/cockpit/
+XDG_CONFIG_DIRS="$BROWSER_HOME" COCKPIT_SUPERUSER="pkexec" '${COCKPIT_WS:-@libexecdir@/cockpit-ws}' -p 80 -a 127.0.0.90 --local-session=- <&0 >&1 &
+WS_PID=$!
+# ... and stop using that stdin/out for everything else
+exec 0</dev/null
+exec 1>&2
+
+trap "set +e; kill $WS_PID; wait $WS_PID; rm -rf $BROWSER_HOME" EXIT INT QUIT PIPE
+
+# if we have netcat, use it for waiting until ws is up
+if type nc >/dev/null 2>&1; then
+ for retry in `seq 10`; do
+ nc -z 127.0.0.90 80 && break
+ sleep 0.5;
+ done
+else
+ # otherwise, just wait a bit
+ sleep 3
+fi
+
+HOME="$BROWSER_HOME" '$BROWSER' http://127.0.0.90'"$URL_PATH"'
+'
+unshare --user --map-root-user --net /bin/bash -c "$SCRIPT" <&${COPROC[0]} >&${COPROC[1]}
diff --git a/src/ws/cockpit.appdata.xml.in b/src/ws/cockpit.appdata.xml.in
new file mode 100644
index 0000000..13f1f65
--- /dev/null
+++ b/src/ws/cockpit.appdata.xml.in
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2014 Richard Hughes <richard@hughsie.com> -->
+<component type="web-application">
+ <id>org.cockpit_project.cockpit</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>LGPL-2.0+</project_license>
+ <name>Cockpit</name>
+ <summary>Web Console for Linux servers</summary>
+ <description>
+ <p>
+ Cockpit is a server manager that makes it easy to administer your Linux
+ servers via a web browser.
+ Jumping between the terminal and the web tool is no problem.
+ A service started via Cockpit can be stopped via the terminal.
+ Likewise, if an error occurs in the terminal, it can be seen in the
+ Cockpit journal interface.
+ </p>
+ <p>
+ Cockpit is perfect for new sysadmins, allowing them to easily perform
+ simple tasks such as storage administration, inspecting journals and
+ starting and stopping services.
+ You can monitor and administer several servers at the same time.
+ Just add them with a single click and your machines will look after its
+ buddies.
+ </p>
+ <p>
+ Once Cockpit is installed, enable it with "systemctl enable --now cockpit.socket".
+ </p>
+ </description>
+ <categories>
+ <category>System</category>
+ </categories>
+ <icon type="local" width="128" height="128">/usr/share/pixmaps/cockpit.png</icon>
+ <launchable type="url">https://localhost:9090</launchable>
+ <screenshots>
+ <screenshot type="default">
+ <image>https://cockpit-project.org/images/screenshot/overview-f33.webp</image>
+ </screenshot>
+ <screenshot>
+ <image>https://cockpit-project.org/images/screenshot/network-overview.webp</image>
+ </screenshot>
+ </screenshots>
+ <url type="homepage">https://cockpit-project.org/</url>
+ <url type="help">https://cockpit-project.org/running.html</url>
+ <update_contact>cockpit-devel_AT_lists.fedorahosted.org</update_contact>
+</component>
diff --git a/src/ws/cockpit.png b/src/ws/cockpit.png
new file mode 100644
index 0000000..36b5ccb
--- /dev/null
+++ b/src/ws/cockpit.png
Binary files differ
diff --git a/src/ws/cockpitauth.c b/src/ws/cockpitauth.c
new file mode 100644
index 0000000..99f5c33
--- /dev/null
+++ b/src/ws/cockpitauth.c
@@ -0,0 +1,1708 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitauth.h"
+
+#include "cockpitws.h"
+
+#include "websocket/websocket.h"
+
+#include "common/cockpitauthorize.h"
+#include "common/cockpitconf.h"
+#include "common/cockpiterror.h"
+#include "common/cockpithacks.h"
+#include "common/cockpithex.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitmemory.h"
+#include "common/cockpitpipe.h"
+#include "common/cockpitpipetransport.h"
+#include "common/cockpitsystem.h"
+#include "common/cockpitwebserver.h"
+
+#include <sys/socket.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+
+#include <gio/gunixsocketaddress.h>
+
+#define ACTION_SSH "remote-login-ssh"
+#define ACTION_NONE "none"
+#define LOCAL_SESSION "local-session"
+
+/* Some tunables that can be set from tests */
+const gchar *cockpit_ws_session_program = LIBEXECDIR "/cockpit-session";
+const gchar *cockpit_ws_ssh_program = LIBEXECDIR "/cockpit-ssh";
+
+/* Timeout of authenticated session when no connections */
+guint cockpit_ws_service_idle = 15;
+
+/* The amount of time a spawned process has to complete authentication */
+guint cockpit_ws_auth_process_timeout = 30;
+guint cockpit_ws_auth_response_timeout = 60;
+
+/* Maximum number of pending authentication requests */
+const gchar *cockpit_ws_max_startups = NULL;
+
+static guint max_startups = 10;
+
+static guint sig__idling = 0;
+
+/* Tristate tracking whether gssapi works properly */
+static gint gssapi_available = -1;
+
+static guint
+get_process_idle (void)
+{
+ static guint seconds = 0;
+ if (seconds == 0) /* lazy initialization */
+ {
+ const char *val = g_getenv ("COCKPIT_WS_PROCESS_IDLE");
+
+ seconds = 90; /* default value */
+ if (val)
+ {
+ char *endptr;
+ gint64 x = g_ascii_strtoll (val, &endptr, 10);
+ if (*endptr == '\0' && x > 0 && x <= G_MAXUINT)
+ seconds = (guint) x;
+ else
+ g_warning ("Invalid value for COCKPIT_WS_PROCESS_IDLE, ignoring: %s", val);
+ }
+ }
+
+ return seconds;
+}
+
+G_DEFINE_TYPE (CockpitAuth, cockpit_auth, G_TYPE_OBJECT)
+
+typedef struct {
+ gint refs;
+ gchar *name;
+
+ gchar *cookie;
+ CockpitAuth *auth;
+
+ CockpitWebService *service;
+ gboolean initialized;
+ guint timeout_tag;
+ gulong idling_sig;
+ gulong destroy_sig;
+
+ /* Used during authentication */
+ CockpitTransport *transport;
+ gulong control_sig;
+ gulong close_sig;
+
+ guint client_timeout;
+ guint authorize_timeout;
+
+ /* An open /login request from client */
+ GTask *login_task;
+
+ /* An authorization header from client */
+ gchar *authorization;
+
+ /* An authorize challenge from session */
+ JsonObject *authorize;
+
+ /* The conversation in progress */
+ gchar *conversation;
+} CockpitSession;
+
+static void
+cockpit_session_reset (gpointer data)
+{
+ CockpitSession *session = data;
+ char *conversation;
+ CockpitAuth *self;
+ char *cookie;
+
+ if (session->login_task)
+ {
+ g_autoptr(GTask) task = g_steal_pointer (&session->login_task);
+ g_task_return_boolean (task, TRUE);
+ }
+
+ if (session->authorization)
+ {
+ cockpit_memory_clear (session->authorization, -1);
+ g_free (session->authorization);
+ session->authorization = NULL;
+ }
+
+ conversation = session->conversation;
+ session->conversation = NULL;
+ cookie = session->cookie;
+ session->cookie = NULL;
+ self = session->auth;
+
+ /* No accessing session after this point */
+
+ if (cookie)
+ {
+ g_hash_table_remove (self->sessions, cookie);
+ g_free (cookie);
+ }
+
+ if (conversation)
+ {
+ g_hash_table_remove (self->conversations, conversation);
+ g_free (conversation);
+ }
+}
+
+static CockpitSession *
+cockpit_session_ref (CockpitSession *session)
+{
+ session->refs++;
+ return session;
+}
+
+static void
+on_web_service_gone (gpointer data,
+ GObject *where_the_object_was)
+{
+ CockpitSession *session = data;
+ session->service = NULL;
+ cockpit_session_reset (session);
+}
+
+static void
+cockpit_session_unref (gpointer data)
+{
+ CockpitSession *session = data;
+ CockpitCreds *creds;
+ GObject *object;
+
+ session->refs--;
+ if (session->refs > 0)
+ return;
+
+ cockpit_session_reset (data);
+
+ g_free (session->name);
+ g_free (session->cookie);
+
+ if (session->authorize)
+ json_object_unref (session->authorize);
+
+ if (session->transport)
+ {
+ if (session->control_sig)
+ g_signal_handler_disconnect (session->transport, session->control_sig);
+ if (session->close_sig)
+ g_signal_handler_disconnect (session->transport, session->close_sig);
+ g_object_unref (session->transport);
+ }
+
+ if (session->service)
+ {
+ creds = cockpit_web_service_get_creds (session->service);
+ object = G_OBJECT (session->service);
+ session->service = NULL;
+ if (creds)
+ cockpit_creds_poison (creds);
+ if (session->idling_sig)
+ g_signal_handler_disconnect (object, session->idling_sig);
+ if (session->destroy_sig)
+ g_signal_handler_disconnect (object, session->destroy_sig);
+ g_object_weak_unref (object, on_web_service_gone, session);
+ g_object_run_dispose (object);
+ g_object_unref (object);
+ }
+
+ if (session->timeout_tag)
+ g_source_remove (session->timeout_tag);
+
+ g_free (session);
+}
+
+static void
+byte_array_clear_and_free (gpointer data)
+{
+ GByteArray *buffer = data;
+ cockpit_memory_clear (buffer->data, buffer->len);
+ g_byte_array_free (buffer, TRUE);
+}
+
+static void
+cockpit_auth_finalize (GObject *object)
+{
+ CockpitAuth *self = COCKPIT_AUTH (object);
+ if (self->timeout_tag)
+ g_source_remove (self->timeout_tag);
+ g_bytes_unref (self->key);
+ g_hash_table_remove_all (self->sessions);
+ g_hash_table_remove_all (self->conversations);
+ g_hash_table_destroy (self->sessions);
+ g_hash_table_destroy (self->conversations);
+ G_OBJECT_CLASS (cockpit_auth_parent_class)->finalize (object);
+}
+
+static gboolean
+on_process_timeout (gpointer data)
+{
+ CockpitAuth *self = COCKPIT_AUTH (data);
+
+ self->timeout_tag = 0;
+ if (g_hash_table_size (self->sessions) == 0)
+ {
+ g_debug ("auth is idle");
+ g_signal_emit (self, sig__idling, 0);
+ }
+
+ return FALSE;
+}
+
+static void
+cockpit_auth_init (CockpitAuth *self)
+{
+ static const gsize key_len = 128;
+ gpointer key;
+
+ key = cockpit_authorize_nonce (key_len);
+ if (!key)
+ g_error ("couldn't read random key, startup aborted");
+
+ self->key = g_bytes_new_take (key, key_len);
+
+ self->sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, cockpit_session_unref);
+
+ self->conversations = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, cockpit_session_unref);
+
+ self->timeout_tag = g_timeout_add_seconds (get_process_idle (),
+ on_process_timeout, self);
+
+ self->startups = 0;
+ self->max_startups = max_startups;
+ self->max_startups_begin = max_startups;
+ self->max_startups_rate = 100;
+}
+
+gchar *
+cockpit_auth_nonce (CockpitAuth *self)
+{
+ const guchar *key;
+ gsize len;
+ guint64 seed;
+
+ seed = self->nonce_seed++;
+ key = g_bytes_get_data (self->key, &len);
+ return g_compute_hmac_for_data (G_CHECKSUM_SHA256, key, len,
+ (guchar *)&seed, sizeof (seed));
+}
+
+static gchar *
+cockpit_auth_steal_authorization (CockpitWebRequest *request,
+ gchar **ret_type,
+ gchar **ret_conversation)
+{
+ char *type = NULL;
+ gchar *ret = NULL;
+ gchar *line;
+ gpointer key;
+
+ g_assert (request != NULL);
+ g_assert (ret_conversation != NULL);
+ g_assert (ret_type != NULL);
+
+ /* Avoid copying as it can contain passwords */
+ GHashTable *headers = cockpit_web_request_get_headers (request);
+ g_assert (headers != NULL);
+ if (g_hash_table_lookup_extended (headers, "Authorization", &key, (gpointer *)&line))
+ {
+ g_hash_table_steal (headers, "Authorization");
+ g_free (key);
+
+ /* This is being parsed heavily, enforce ASCII */
+ if (!g_str_is_ascii (line))
+ {
+ g_message ("received invalid Authorize header, must be ASCII");
+ goto out;
+ }
+ }
+ else
+ {
+ /*
+ * If we don't yet know that Negotiate authentication is possible
+ * or not, then we ask our session to try to do Negotiate auth
+ * but without any input data.
+ */
+ if (gssapi_available != 0)
+ line = g_strdup ("Negotiate");
+ else
+ return NULL;
+ }
+
+ /* Dig out the authorization type */
+ if (!cockpit_authorize_type (line, &type))
+ goto out;
+
+ /* It's never valid for a "tls-cert" to come via Authorization: */
+ if (g_str_equal (type, "tls-cert"))
+ {
+ g_message ("received invalid 'Authorization: tls-cert' header");
+ goto out;
+ }
+
+ GIOStream *connection = cockpit_web_request_get_io_stream (request);
+
+ /* If this is a conversation, get that part out too */
+ if (g_str_equal (type, "x-conversation"))
+ {
+ if (!cockpit_authorize_subject (line, ret_conversation))
+ goto out;
+ }
+
+ /*
+ * So for negotiate authentication, conversation happens on a
+ * single connection. Yes that's right, GSSAPI, NTLM, and all
+ * those nice mechanisms are keep-alive based, not HTTP request based.
+ */
+ else if (g_str_equal (type, "negotiate"))
+ {
+ /* Resume an already running conversation? */
+ if (ret_conversation && connection)
+ *ret_conversation = g_strdup (g_object_get_data (G_OBJECT (connection), type));
+ }
+
+
+ if (ret_type)
+ {
+ *ret_type = type;
+ type = NULL;
+ }
+
+ ret = line;
+ line = NULL;
+
+out:
+ g_free (line);
+ g_free (type);
+ return ret;
+}
+
+static guint
+timeout_option (const gchar *name,
+ const gchar *type,
+ guint default_value)
+{
+ return cockpit_conf_uint (type, name, default_value,
+ MAX_AUTH_TIMEOUT, MIN_AUTH_TIMEOUT);
+}
+
+static const gchar *
+application_parse_host (const gchar *application)
+{
+ const gchar *prefix = "cockpit+=";
+ gint len = strlen (prefix);
+
+ g_return_val_if_fail (application != NULL, NULL);
+
+ if (g_str_has_prefix (application, prefix) && application[len] != '\0')
+ return application + len;
+ else
+ return NULL;
+}
+
+static gchar *
+application_cookie_name (const gchar *application)
+{
+ const gchar *host = application_parse_host (application);
+ gchar *cookie_name = NULL;
+
+ if (host)
+ cookie_name = g_strdup_printf ("machine-cockpit+%s", host);
+ else
+ cookie_name = g_strdup (application);
+
+ return cookie_name;
+}
+
+/* Struct for adding tty later */
+typedef struct {
+ int io;
+} ChildData;
+
+static void
+session_child_setup (gpointer data)
+{
+ ChildData *child = data;
+
+ if (dup2 (child->io, 0) < 0 || dup2 (child->io, 1) < 0)
+ {
+ g_printerr ("couldn't set child stdin/stout file descriptors\n");
+ _exit (127);
+ }
+
+ close (child->io);
+
+ closefrom (3);
+}
+
+static CockpitPipe *
+session_start_process (const gchar **argv,
+ const gchar **env,
+ gboolean capture_stderr)
+{
+ GError *error = NULL;
+ ChildData child;
+ gboolean ret;
+ GPid pid = 0;
+ int fds[2];
+
+ g_return_val_if_fail (argv[0] != NULL, NULL);
+
+ g_debug ("spawning %s", argv[0]);
+
+ /* The main stdin/stdout for the socket ... both are read/writable */
+ if (socketpair (PF_LOCAL, SOCK_STREAM, 0, fds) < 0)
+ {
+ g_warning ("couldn't create loopback socket: %s", g_strerror (errno));
+ return NULL;
+ }
+
+ int stderr_fd = -1;
+ child.io = fds[0];
+ ret = g_spawn_async_with_pipes (NULL, (gchar **)argv, (gchar **)env,
+ G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_LEAVE_DESCRIPTORS_OPEN,
+ session_child_setup, &child,
+ &pid, NULL, NULL, capture_stderr ? &stderr_fd : NULL, &error);
+
+ close (fds[0]);
+
+ if (!ret)
+ {
+ g_message ("couldn't launch cockpit session: %s: %s", argv[0], error->message);
+ g_error_free (error);
+ close (fds[1]);
+ return NULL;
+ }
+
+ return g_object_new (COCKPIT_TYPE_PIPE,
+ "in-fd", fds[1],
+ "out-fd", fds[1],
+ "err-fd", stderr_fd,
+ "pid", pid,
+ "name", argv[0],
+ NULL);
+}
+
+static void
+send_authorize_reply (CockpitTransport *transport,
+ const gchar *cookie,
+ const gchar *authorization)
+{
+ const gchar *fields[] = {
+ "command", "authorize",
+ "cookie", cookie,
+ "response", authorization,
+ NULL
+ };
+
+ GBytes *payload;
+ const gchar *delim;
+ GByteArray *buffer;
+ JsonNode *node;
+ gchar *encoded;
+ gsize length;
+ guint i;
+
+ buffer = g_byte_array_new ();
+ for (i = 0; fields[i] != NULL; i++)
+ {
+ if (i % 2 == 0)
+ delim = buffer->len == 0 ? "{" : ",";
+ else
+ delim = ":";
+ g_byte_array_append (buffer, (guchar *)delim, 1);
+
+ node = json_node_init_string (json_node_new (JSON_NODE_VALUE), fields[i]);
+ encoded = cockpit_json_write (node, &length);
+ g_byte_array_append (buffer, (guchar *)encoded, length);
+ cockpit_memory_clear ((guchar *)json_node_get_string (node), -1);
+ json_node_free (node);
+ g_free (encoded);
+ }
+
+ g_byte_array_append (buffer, (guchar *)"}", 1);
+ payload = g_bytes_new_with_free_func (buffer->data, buffer->len,
+ byte_array_clear_and_free, buffer);
+
+ cockpit_transport_send (transport, NULL, payload);
+ g_bytes_unref (payload);
+}
+
+static gboolean
+reply_authorize_challenge (CockpitSession *session)
+{
+ const gchar *challenge = NULL;
+ char *authorize_type = NULL;
+ char *authorization_type = NULL;
+ const gchar *cookie = NULL;
+ const gchar *response = NULL;
+ JsonObject *login_data = NULL;
+ gboolean ret = FALSE;
+
+ if (!session->authorize)
+ goto out;
+
+ if (!cockpit_json_get_string (session->authorize, "cookie", NULL, &cookie) ||
+ !cockpit_json_get_string (session->authorize, "challenge", NULL, &challenge) ||
+ !cockpit_json_get_string (session->authorize, "response", NULL, &response))
+ goto out;
+
+ if (response && !cookie)
+ {
+ if (session->authorization)
+ {
+ cockpit_memory_clear (session->authorization, -1);
+ g_free (session->authorization);
+ }
+
+ session->authorization = g_strdup (response);
+ ret = TRUE;
+ goto out;
+ }
+
+ if (!challenge || !cookie)
+ goto out;
+
+ if (!cockpit_authorize_type (challenge, &authorize_type))
+ goto out;
+
+ /* Handle prompting for login data */
+ if (g_str_equal (authorize_type, "x-login-data"))
+ {
+ if (cockpit_json_get_object (session->authorize, "login-data", NULL, &login_data) && login_data)
+ cockpit_creds_set_login_data (cockpit_web_service_get_creds (session->service), login_data);
+ ret = TRUE;
+ goto out;
+ }
+
+ if (!session->authorization)
+ goto out;
+
+ if (cockpit_authorize_type (session->authorization, &authorization_type) &&
+ (g_str_equal (authorize_type, "*") || g_str_equal (authorize_type, authorization_type)))
+ {
+ send_authorize_reply (session->transport, cookie, session->authorization);
+ cockpit_memory_clear (session->authorization, -1);
+ g_free (session->authorization);
+ session->authorization = NULL;
+ ret = TRUE;
+ }
+
+out:
+ free (authorize_type);
+ free (authorization_type);
+ return ret;
+}
+
+static gboolean
+on_authorize_timeout (gpointer data)
+{
+ CockpitSession *session = data;
+ CockpitTransport *transport = cockpit_web_service_get_transport (session->service);
+ session->timeout_tag = 0;
+ g_message ("%s: session timed out during authentication", session->name);
+ cockpit_transport_close (transport, "timeout");
+ return FALSE;
+}
+
+static void
+reset_authorize_timeout (CockpitSession *session,
+ gboolean waiting_for_client)
+{
+ guint seconds = waiting_for_client ? session->client_timeout : session->authorize_timeout;
+ if (session->timeout_tag)
+ g_source_remove (session->timeout_tag);
+ session->timeout_tag = g_timeout_add_seconds (seconds, on_authorize_timeout, session);
+}
+
+static void
+propagate_problem_to_error (CockpitSession *session,
+ JsonObject *options,
+ const gchar *problem,
+ const gchar *message,
+ GError **error)
+{
+ char *type = NULL;
+ const char *pw_result = NULL;
+ JsonObject *auth_results = NULL;
+
+ g_return_if_fail (error != NULL);
+
+ if (g_str_equal (problem, "authentication-unavailable") &&
+ cockpit_authorize_type (session->authorization, &type) &&
+ g_str_equal (type, "negotiate"))
+ {
+ g_debug ("%s: negotiate authentication not available", session->name);
+ g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Negotiate authentication not available");
+ gssapi_available = 0;
+ }
+ else if (g_str_equal (problem, "authentication-failed") ||
+ g_str_equal (problem, "authentication-unavailable"))
+ {
+ cockpit_json_get_object (options, "auth-method-results", NULL, &auth_results);
+ if (auth_results)
+ {
+ cockpit_json_get_string (auth_results, "password", NULL, &pw_result);
+ if (!pw_result || g_strcmp0 (pw_result, "no-server-support") == 0)
+ {
+ g_clear_error (error);
+ g_set_error (error, COCKPIT_ERROR,
+ COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Authentication failed: authentication-not-supported");
+ }
+ }
+
+ if (*error == NULL)
+ {
+ g_debug ("%s: %s %s", session->name, problem, message);
+ g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Authentication failed");
+ }
+ }
+ else if (g_str_equal (problem, "no-host") ||
+ g_str_equal (problem, "invalid-hostkey") ||
+ g_str_equal (problem, "unknown-hostkey") ||
+ g_str_equal (problem, "unknown-host") ||
+ g_str_equal (problem, "terminated"))
+ {
+ g_debug ("%s: %s", session->name, problem);
+ g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Authentication failed: %s", problem);
+ }
+ else if (g_str_equal (problem, "access-denied"))
+ {
+ g_debug ("permission denied %s", message);
+ g_set_error_literal (error, COCKPIT_ERROR, COCKPIT_ERROR_PERMISSION_DENIED,
+ message ? message : "Permission denied");
+ }
+ else
+ {
+ g_debug ("%s: errored %s: %s", session->name, problem, message);
+ if (message)
+ {
+ g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
+ "Authentication failed: %s: %s", problem, message);
+ }
+ else
+ {
+ g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
+ "Authentication failed: %s", problem);
+ }
+
+ }
+
+ free (type);
+}
+
+static gboolean
+on_transport_control (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload,
+ gpointer user_data)
+{
+ CockpitSession *session = user_data;
+ const gchar *problem = NULL;
+ const gchar *session_id = NULL;
+ const gchar *message = NULL;
+ GError *error = NULL;
+ gboolean ret = TRUE;
+
+ if (g_str_equal (command, "init"))
+ {
+ g_debug ("session initialized");
+ g_signal_handler_disconnect (session->transport, session->control_sig);
+ g_signal_handler_disconnect (session->transport, session->close_sig);
+ session->control_sig = session->close_sig = 0;
+ session->initialized = TRUE;
+
+ if (cockpit_json_get_string (options, "problem", NULL, &problem) && problem)
+ {
+ if (!cockpit_json_get_string (options, "message", NULL, &message))
+ message = NULL;
+ propagate_problem_to_error (session, options, problem, message, &error);
+ if (session->login_task == NULL)
+ {
+ g_message ("ignoring failure from session process: %s", error->message);
+ g_error_free (error);
+ }
+ }
+
+ if (cockpit_json_get_string (options, "session-id", NULL, &session_id) && session_id)
+ cockpit_web_service_set_id (session->service, session_id);
+
+ ret = FALSE; /* Let this message be handled elsewhere */
+ }
+ else if (g_str_equal (command, "authorize"))
+ {
+ const gchar *challenge;
+
+ /* handle x-host-key challenge */
+ if (cockpit_json_get_string (options, "challenge", NULL, &challenge) && g_strcmp0 (challenge, "x-host-key") == 0)
+ {
+ const gchar *cookie;
+ g_return_val_if_fail (cockpit_json_get_string (options, "cookie", NULL, &cookie), FALSE);
+
+ /* return a negative answer; we handle unknown hosts interactively, or want to fail on them */
+ g_debug ("received x-host-key authorize challenge");
+ send_authorize_reply (session->transport, cookie, "");
+ return TRUE;
+ }
+
+ /* handle login ("*") challenge */
+ g_debug ("received authorize challenge");
+ if (session->authorize)
+ json_object_unref (session->authorize);
+ session->authorize = json_object_ref (options);
+
+ if (reply_authorize_challenge (session))
+ return TRUE;
+ }
+ else
+ {
+ g_message ("unexpected \"%s\" control message from session before \"init\"", command);
+ cockpit_transport_close (transport, "protocol-error");
+ }
+
+ if (session->login_task)
+ {
+ g_autoptr(GTask) task = g_steal_pointer (&session->login_task);
+ if (error)
+ g_task_return_error (task, error);
+ else
+ g_task_return_boolean (task, TRUE);
+ }
+
+ return ret;
+}
+
+static void
+on_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitSession *session = user_data;
+ CockpitPipe *pipe;
+ GError *error = NULL;
+ gint status = 0;
+
+ if (g_strcmp0 (problem, "timeout") == 0)
+ {
+ g_message ("%s: authentication timed out", session->name);
+ g_set_error (&error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
+ "Authentication failed: Timeout");
+ }
+ else if (!session->initialized)
+ {
+ pipe = cockpit_pipe_transport_get_pipe (COCKPIT_PIPE_TRANSPORT (transport));
+ g_autofree gchar *captured_error = cockpit_pipe_take_stderr_as_utf8 (pipe);
+
+ if (cockpit_pipe_get_pid (pipe, NULL))
+ status = cockpit_pipe_exit_status (pipe);
+ g_debug ("%s: authentication process exited: %d; problem %s", session->name, status, problem);
+
+ if (captured_error)
+ {
+ g_set_error (&error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "captured-stderr:%s", captured_error);
+ }
+ /* we get "access-denied" both if cockpit-session cannot execute cockpit-bridge (common case)
+ * and if cockpit-session itself is not executable (corner case, messed up install) */
+ else if (problem && (!session->authorize || g_strcmp0 (problem, "access-denied") != 0))
+ {
+ g_set_error (&error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
+ g_strcmp0 (problem, "no-cockpit") == 0
+ ? "The cockpit package is not installed"
+ : "Internal error in login process");
+ }
+ else
+ {
+ g_set_error (&error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Authentication failed");
+ }
+ }
+
+ if (!error)
+ {
+ g_message ("%s: authentication process failed", session->name);
+ g_set_error (&error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
+ "Authentication internal error");
+ }
+
+ if (session->login_task)
+ {
+ g_autoptr(GTask) task = g_steal_pointer (&session->login_task);
+ g_task_return_error (task, error);
+ }
+ else
+ {
+ g_message ("ignoring failure from session process: %s", error->message);
+ cockpit_session_reset (session);
+ g_error_free (error);
+ }
+}
+
+static CockpitCreds *
+build_session_credentials (CockpitAuth *self,
+ CockpitWebRequest *request,
+ const char *application,
+ const char *host,
+ const char *type,
+ const char *authorization)
+{
+ CockpitCreds *creds;
+ const gchar *superuser = NULL;
+
+ char *user = NULL;
+ char *raw = NULL;
+
+ GBytes *password = NULL;
+ gchar *remote_peer = NULL;
+ gchar *csrf_token = NULL;
+
+ superuser = cockpit_web_request_lookup_header (request, "X-Superuser");
+ if (!superuser)
+ superuser = "none";
+
+ /* Prepare various credentials */
+ if (g_strcmp0 (type, "basic") == 0)
+ {
+ raw = cockpit_authorize_parse_basic (authorization, &user);
+
+ /* If we are not root, we might want to keep the password around
+ so that the session can immediately gain admin privileges by
+ reusing the password.
+
+ We keep it when X-Superuser is not none, but also when
+ directly connecting to a remote machine since that machine
+ might run a old version of Cockpit that will never do the
+ things necessary to set X-Superuser to anything but "none".
+ We still want to give it the password because otherwise
+ people can never get admin privs on that machine.
+
+ When the remote machine is new enough, we will remove the
+ password immediately after the session has been initialized.
+ */
+
+ if (user && strcmp (user, "root") != 0 && (strcmp (superuser, "none") != 0 || host) && raw)
+ {
+ password = g_bytes_new_take (raw, strlen (raw));
+ raw = NULL;
+ }
+ }
+
+ remote_peer = cockpit_web_request_get_remote_address (request);
+ csrf_token = cockpit_auth_nonce (self);
+
+ creds = cockpit_creds_new (application,
+ COCKPIT_CRED_USER, user,
+ COCKPIT_CRED_PASSWORD, password,
+ COCKPIT_CRED_RHOST, remote_peer,
+ COCKPIT_CRED_CSRF_TOKEN, csrf_token,
+ COCKPIT_CRED_SUPERUSER, superuser,
+ NULL);
+
+ g_free (remote_peer);
+ if (raw)
+ {
+ cockpit_memory_clear (raw, strlen (raw));
+ free (raw);
+ }
+ g_free (csrf_token);
+ if (password)
+ g_bytes_unref (password);
+ free (user);
+
+ return creds;
+}
+
+static gboolean
+on_session_timeout (gpointer data)
+{
+ CockpitSession *session = data;
+
+ session->timeout_tag = 0;
+
+ if (!session->service || cockpit_web_service_get_idling (session->service))
+ {
+ g_info ("session timed out");
+ cockpit_session_reset (session);
+ }
+
+ return FALSE;
+}
+
+static void
+on_web_service_idling (CockpitWebService *service,
+ gpointer data)
+{
+ CockpitSession *session = data;
+
+ if (session->timeout_tag)
+ g_source_remove (session->timeout_tag);
+
+ g_debug ("session is idle");
+
+ /*
+ * The minimum amount of time before a request uses this new web service,
+ * otherwise it will just go away.
+ */
+ session->timeout_tag = g_timeout_add_seconds (cockpit_ws_service_idle,
+ on_session_timeout,
+ session);
+
+ /*
+ * Also reset the timer which checks whether anything is going on in the
+ * entire process or not.
+ */
+ if (session->auth->timeout_tag)
+ g_source_remove (session->auth->timeout_tag);
+
+ session->auth->timeout_tag = g_timeout_add_seconds (get_process_idle (),
+ on_process_timeout, session->auth);
+}
+
+static void
+on_web_service_destroy (CockpitWebService *service,
+ gpointer data)
+{
+ on_web_service_idling (service, data);
+ cockpit_session_reset (data);
+}
+
+static CockpitSession *
+cockpit_session_create (CockpitAuth *self,
+ const gchar *name,
+ CockpitCreds *creds,
+ CockpitTransport *transport)
+{
+ CockpitSession *session;
+
+ session = g_new0 (CockpitSession, 1);
+ session->refs = 1;
+ session->name = g_path_get_basename (name);
+ session->auth = self;
+
+ session->service = cockpit_web_service_new (creds, transport);
+
+ session->idling_sig = g_signal_connect (session->service, "idling",
+ G_CALLBACK (on_web_service_idling), session);
+ session->destroy_sig = g_signal_connect (session->service, "destroy",
+ G_CALLBACK (on_web_service_destroy), session);
+
+ g_object_weak_ref (G_OBJECT (session->service), on_web_service_gone, session);
+
+ session->transport = g_object_ref (transport);
+ session->control_sig = g_signal_connect (transport, "control", G_CALLBACK (on_transport_control), session);
+ session->close_sig = g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), session);
+
+ return session;
+}
+
+static CockpitSession *
+cockpit_session_launch (CockpitAuth *self,
+ CockpitWebRequest *request,
+ const gchar *type,
+ const gchar *authorization,
+ const gchar *application,
+ GError **error)
+{
+ g_return_val_if_fail (type != NULL, NULL);
+
+ const gchar *host = application_parse_host (application);
+ const gchar *action = cockpit_conf_string (type, "action");
+ if (g_strcmp0 (action, ACTION_NONE) == 0)
+ {
+ g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Authentication disabled");
+ return NULL;
+ }
+
+ /* this might be unset, which means "allow if cockpit-ssh is installed"; if it isn't, this will fail later on */
+ if (host && !cockpit_conf_bool ("WebService", "LoginTo", TRUE)) {
+ g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Direct remote login is disabled");
+ return NULL;
+ }
+
+ /* These are the credentials we'll carry around for this session */
+ g_autoptr(CockpitCreds) creds = build_session_credentials (self, request, application, host, type, authorization);
+
+ const gchar *section;
+ if (host)
+ section = COCKPIT_CONF_SSH_SECTION;
+ else if (self->login_loopback && g_str_equal (type, "basic"))
+ section = COCKPIT_CONF_SSH_SECTION;
+ else if (g_strcmp0 (action, ACTION_SSH) == 0)
+ section = COCKPIT_CONF_SSH_SECTION;
+ else
+ section = type;
+
+ const gchar *command = cockpit_conf_string (section, "Command");
+ const gchar *unix_path = cockpit_conf_string (section, "UnixPath");
+
+ gboolean capture_stderr = FALSE;
+ if (g_str_equal (section, COCKPIT_CONF_SSH_SECTION))
+ {
+ if (!host)
+ host = cockpit_conf_string (COCKPIT_CONF_SSH_SECTION, "host") ?: "127.0.0.1";
+
+ /* We capture stderr only for Cockpit Client; we don't want to
+ * send log messages to potential remote attackers.
+ *
+ * Only do that if COCKPIT_DEBUG is off, though: otherwise the
+ * stderr is going to be too long to make sense of in the browser.
+ */
+ if (g_getenv("COCKPIT_DEBUG") == NULL)
+ capture_stderr = cockpit_conf_bool ("WebService", "X-For-CockpitClient", FALSE);
+
+ if (command == NULL && unix_path == NULL)
+ command = cockpit_ws_ssh_program;
+ }
+ else if (g_str_equal (type, "basic") ||
+ g_str_equal (type, "negotiate") ||
+ g_str_equal (type, "tls-cert"))
+ {
+ if (command == NULL && unix_path == NULL)
+ command = cockpit_ws_session_program;
+ }
+
+ g_autoptr(CockpitPipe) pipe = NULL;
+ if (command != NULL)
+ {
+ g_auto(GStrv) env = g_get_environ ();
+ if (cockpit_creds_get_rhost (creds))
+ {
+ env = g_environ_setenv (env, "COCKPIT_REMOTE_PEER",
+ cockpit_creds_get_rhost (creds),
+ TRUE);
+ }
+ if (g_strcmp0 (cockpit_web_request_lookup_header (request, "X-SSH-Connect-Unknown-Hosts"), "yes") == 0)
+ {
+ env = g_environ_setenv (env, "COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS",
+ "1",
+ TRUE);
+ }
+
+ /* split the command from the config */
+ gint argc;
+ g_auto(GStrv) argv = NULL;
+ if (!g_shell_parse_argv (command, &argc, &argv, error))
+ return NULL;
+
+ /* append the host */
+ argv = g_renew (char *, argv, argc + 1 + 1);
+ argv[argc++] = g_strdup (host ?: "localhost");
+ argv[argc] = NULL;
+
+ pipe = session_start_process ((const gchar **) argv, (const gchar **)env, capture_stderr);
+ }
+ else if (unix_path != NULL)
+ {
+ g_autoptr(GSocketAddress) address = g_unix_socket_address_new (unix_path);
+ pipe = cockpit_pipe_connect (unix_path, address);
+ }
+ else
+ {
+ g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Authentication disabled");
+ return NULL;
+ }
+
+ if (!pipe)
+ {
+ g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
+ "Authentication failed to start");
+ return NULL;
+ }
+
+ g_autoptr(CockpitTransport) transport = cockpit_pipe_transport_new (pipe);
+ CockpitSession *session = cockpit_session_create (self, cockpit_pipe_get_name (pipe), creds, transport);
+
+ /* How long to wait for the auth process to send some data */
+ session->authorize_timeout = timeout_option ("timeout", section, cockpit_ws_auth_process_timeout);
+
+ /* How long to wait for a response from the client to a auth prompt */
+ session->client_timeout = timeout_option ("response-timeout", section, cockpit_ws_auth_response_timeout);
+
+ return session;
+}
+
+static gboolean
+build_authorize_challenge (CockpitAuth *self,
+ JsonObject *authorize,
+ GIOStream *connection,
+ GHashTable *headers,
+ JsonObject **body,
+ gchar **conversation)
+{
+ const gchar *challenge = NULL;
+ gchar *type = NULL;
+ GList *l, *names;
+
+ if (!cockpit_json_get_string (authorize, "challenge", NULL, &challenge) ||
+ !cockpit_authorize_type (challenge, &type))
+ {
+ g_message ("invalid \"challenge\" field in \"authorize\" message");
+ return FALSE;
+ }
+
+ g_hash_table_replace (headers, g_strdup ("WWW-Authenticate"), g_strdup (challenge));
+ *conversation = NULL;
+
+ if (g_str_equal (type, "negotiate"))
+ {
+ gssapi_available = 1;
+ *conversation = cockpit_auth_nonce (self);
+ if (connection)
+ g_object_set_data_full (G_OBJECT (connection), "negotiate", g_strdup (*conversation), g_free);
+ }
+ else if (g_str_equal (type, "x-conversation"))
+ {
+ cockpit_authorize_subject (challenge, conversation);
+ }
+
+ g_autoptr(JsonObject) object = json_object_new ();
+ names = json_object_get_members (authorize);
+ for (l = names; l != NULL; l = g_list_next (l))
+ {
+ if (!g_str_equal (l->data, "challenge") && !g_str_equal (l->data, "cookie"))
+ json_object_set_member (object, l->data, json_object_dup_member (authorize, l->data));
+ }
+ if (body)
+ *body = g_steal_pointer (&object);
+
+ g_list_free (names);
+ g_free (type);
+ return TRUE;
+}
+
+static void
+authorize_logger (const char *data)
+{
+ g_message ("%s", data);
+}
+
+static void
+cockpit_auth_class_init (CockpitAuthClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->finalize = cockpit_auth_finalize;
+
+ sig__idling = g_signal_new ("idling", COCKPIT_TYPE_AUTH, G_SIGNAL_RUN_FIRST,
+ 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+ cockpit_authorize_logger (authorize_logger, 0);
+}
+
+static gchar *
+base64_decode_string (const char *enc)
+{
+ gchar *dec;
+ gsize len;
+
+ if (enc == NULL)
+ return NULL;
+
+ dec = (gchar *)g_base64_decode (enc, &len);
+ if (dec)
+ dec[len] = '\0';
+
+ return dec;
+}
+
+static CockpitSession *
+session_for_request (CockpitAuth *self,
+ CockpitWebRequest *request)
+{
+ gchar *cookie = NULL;
+ gchar *raw = NULL;
+ const char *prefix = "v=2;k=";
+ CockpitSession *ret = NULL;
+ gchar *application;
+ gchar *cookie_name = NULL;
+
+ g_return_val_if_fail (self != NULL, FALSE);
+
+ application = cockpit_auth_parse_application (cockpit_web_request_get_path (request), NULL);
+ if (!application)
+ return NULL;
+
+ cookie_name = application_cookie_name (application);
+ raw = cockpit_web_request_parse_cookie (request, cookie_name);
+ if (raw)
+ {
+ cookie = base64_decode_string (raw);
+ if (cookie != NULL)
+ {
+ if (g_str_has_prefix (cookie, prefix))
+ ret = g_hash_table_lookup (self->sessions, cookie);
+ else
+ g_debug ("invalid or unsupported cookie: %s", cookie);
+
+ /* We must never find the default session based on a cookie */
+ g_assert (!ret || !g_str_equal (ret->cookie, LOCAL_SESSION));
+ g_assert (!ret || !g_str_equal (ret->name, LOCAL_SESSION));
+ g_free (cookie);
+ }
+ g_free (raw);
+ }
+
+ /* Check for a default session for auto-login */
+ if (!ret)
+ ret = g_hash_table_lookup (self->sessions, LOCAL_SESSION);
+
+ g_free (application);
+ g_free (cookie_name);
+ return ret;
+}
+
+CockpitWebService *
+cockpit_auth_check_cookie (CockpitAuth *self,
+ CockpitWebRequest *request)
+{
+ CockpitSession *session;
+ CockpitCreds *creds;
+
+ session = session_for_request (self, request);
+ if (session)
+ {
+ creds = cockpit_web_service_get_creds (session->service);
+ g_debug ("received %s credential cookie for session",
+ cockpit_creds_get_application (creds));
+ return g_object_ref (session->service);
+ }
+ else
+ {
+ g_debug ("received unknown/invalid credential cookie");
+ return NULL;
+ }
+}
+
+void
+cockpit_auth_local_async (CockpitAuth *self,
+ const gchar *user,
+ CockpitPipe *pipe,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ CockpitTransport *transport;
+ CockpitSession *session;
+ CockpitCreds *creds;
+ gchar *csrf_token;
+
+ g_return_if_fail (COCKPIT_IS_AUTH (self));
+ g_return_if_fail (COCKPIT_IS_PIPE (pipe));
+ g_return_if_fail (user != NULL);
+
+ transport = cockpit_pipe_transport_new (pipe);
+
+ csrf_token = cockpit_auth_nonce (self);
+ creds = cockpit_creds_new ("cockpit",
+ COCKPIT_CRED_USER, user,
+ COCKPIT_CRED_RHOST, "localhost",
+ COCKPIT_CRED_CSRF_TOKEN, csrf_token,
+ NULL);
+
+ session = cockpit_session_create (self, cockpit_pipe_get_name (pipe), creds, transport);
+
+ session->cookie = g_strdup (LOCAL_SESSION);
+ g_hash_table_insert (self->sessions, session->cookie, session);
+
+ session->login_task = g_task_new (self, NULL, callback, user_data);
+
+ g_free (csrf_token);
+ g_object_unref (transport);
+ cockpit_creds_unref (creds);
+}
+
+gboolean
+cockpit_auth_local_finish (CockpitAuth *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+/*
+ * returns TRUE if auth can proceed, FALSE otherwise.
+ * dropping starts at connection max_startups_begin with a probability
+ * of (max_startups_rate/100). the probability increases linearly until
+ * all connections are dropped for startups > max_startups
+ */
+
+static gboolean
+can_start_auth (CockpitAuth *self)
+{
+ int p, r;
+
+ /* 0 means unlimited */
+ if (self->max_startups == 0)
+ return TRUE;
+
+ /* Under soft limit */
+ if (self->startups <= self->max_startups_begin)
+ return TRUE;
+
+ /* Over hard limit */
+ if (self->startups > self->max_startups)
+ return FALSE;
+
+ /* If rate is 100, soft limit is hard limit */
+ if (self->max_startups_rate == 100)
+ return FALSE;
+
+ p = 100 - self->max_startups_rate;
+ p *= self->startups - self->max_startups_begin;
+ p /= self->max_startups - self->max_startups_begin;
+ p += self->max_startups_rate;
+ r = g_random_int_range (0, 100);
+
+ g_debug ("calculating if auth can start: (%u:%u:%u): p %d, r %d",
+ self->max_startups_begin, self->max_startups_rate,
+ self->max_startups, p, r);
+ return (r < p) ? FALSE : TRUE;
+}
+
+void
+cockpit_auth_login_async (CockpitAuth *self,
+ CockpitWebRequest *request,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ CockpitSession *session;
+ GError *error = NULL;
+ g_autofree gchar *type = NULL;
+ g_autofree gchar *conversation = NULL;
+ g_autofree gchar *authorization = NULL;
+ g_autofree gchar *application = NULL;
+
+ g_return_if_fail (request != NULL);
+
+ self->startups++;
+
+ g_autoptr(GTask) task = g_task_new (self, NULL, callback, user_data);
+
+ if (!can_start_auth (self))
+ {
+ g_message ("Request dropped; too many startup connections: %u", self->startups);
+ g_task_return_new_error (task, COCKPIT_ERROR, COCKPIT_ERROR_FAILED,
+ "Connection closed by host");
+ goto out;
+ }
+
+ const gchar *path = cockpit_web_request_get_path (request);
+ application = cockpit_auth_parse_application (path, NULL);
+
+ /* If the client sends a TLS certificate to cockpit-tls, treat this as a
+ * definitive login type, and don't just silently fall back to other types */
+ const gchar *client_certificate;
+ if ((client_certificate = cockpit_web_request_get_client_certificate (request)))
+ {
+ g_debug ("TLS connection has peer certificate, using tls-cert auth type");
+ type = g_strdup ("tls-cert");
+ /* This is a client certificate *filename*. On its own, it's
+ * insufficient for logging in: cockpit-session will check that
+ * the file exists on disk and is valid.
+ */
+ authorization = g_strdup_printf ("tls-cert %s", client_certificate);
+ }
+ else
+ {
+ g_debug ("No peer certificate");
+ authorization = cockpit_auth_steal_authorization (request, &type, &conversation);
+
+ if (!authorization)
+ {
+ g_task_return_new_error (task, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Authentication required");
+ goto out;
+ }
+ }
+
+ if (!application)
+ {
+ g_task_return_new_error (task, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Application required");
+ goto out;
+ }
+
+ if (conversation)
+ {
+ session = g_hash_table_lookup (self->conversations, conversation);
+ if (!session)
+ {
+ g_task_return_new_error (task, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Invalid conversation token");
+ goto out;
+ }
+
+ g_task_set_task_data (task, cockpit_session_ref (session), cockpit_session_unref);
+ }
+ else
+ {
+ session = cockpit_session_launch (self, request, type, authorization, application, &error);
+ if (!session)
+ {
+ g_task_return_error (task, error);
+ goto out;
+ }
+ g_task_set_task_data (task, session, cockpit_session_unref);
+ }
+
+ cockpit_session_reset (session);
+ session->login_task = g_steal_pointer (&task);
+
+ session->authorization = authorization;
+ authorization = NULL;
+
+ if (conversation && !reply_authorize_challenge (session))
+ {
+ g_autoptr(GTask) task = g_steal_pointer (&session->login_task);
+ g_task_return_new_error (task, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Invalid conversation reply");
+ goto out;
+ }
+
+ reset_authorize_timeout (session, FALSE);
+
+out:
+ if (authorization)
+ cockpit_memory_clear (authorization, -1);
+}
+
+JsonObject *
+cockpit_auth_login_finish (CockpitAuth *self,
+ GAsyncResult *result,
+ GIOStream *connection,
+ GHashTable *headers,
+ GError **error)
+{
+ JsonObject *body = NULL;
+ CockpitCreds *creds = NULL;
+ CockpitSession *session = NULL;
+ gboolean force_secure;
+ gchar *cookie_name;
+ gchar *cookie_b64;
+ gchar *header;
+ gchar *id;
+
+ g_return_val_if_fail (g_task_is_valid (result, self), NULL);
+
+ if (!g_task_propagate_boolean (G_TASK (result), error))
+ goto out;
+
+ session = g_task_get_task_data (G_TASK (result));
+ g_return_val_if_fail (session != NULL, NULL);
+ g_return_val_if_fail (session->login_task == NULL, NULL);
+
+ cockpit_session_reset (session);
+
+ if (session->authorize)
+ {
+ if (build_authorize_challenge (self, session->authorize, connection,
+ headers, &body, &session->conversation))
+ {
+ if (session->conversation)
+ {
+ reset_authorize_timeout (session, TRUE);
+ g_hash_table_replace (self->conversations, session->conversation, cockpit_session_ref (session));
+ }
+ }
+ }
+
+ if (session->initialized)
+ {
+ /* Start off in the idling state, and begin a timeout during which caller must do something else */
+ on_web_service_idling (session->service, session);
+ creds = cockpit_web_service_get_creds (session->service);
+
+ id = cockpit_auth_nonce (self);
+ session->cookie = g_strdup_printf ("v=2;k=%s", id);
+ g_hash_table_insert (self->sessions, session->cookie, cockpit_session_ref (session));
+ g_free (id);
+
+ if (headers)
+ {
+ if (self->flags & COCKPIT_AUTH_FOR_TLS_PROXY)
+ force_secure = TRUE;
+ else
+ force_secure = connection ? !G_IS_SOCKET_CONNECTION (connection) : TRUE;
+ cookie_name = application_cookie_name (cockpit_creds_get_application (creds));
+ cookie_b64 = g_base64_encode ((guint8 *)session->cookie, strlen (session->cookie));
+ header = g_strdup_printf ("%s=%s; Path=/; SameSite=Strict;%s HttpOnly",
+ cookie_name, cookie_b64,
+ force_secure ? " Secure;" : "");
+ g_free (cookie_b64);
+ g_free (cookie_name);
+ g_hash_table_insert (headers, g_strdup ("Set-Cookie"), header);
+ }
+
+ if (body)
+ json_object_unref (body);
+ body = cockpit_creds_to_json (creds);
+ }
+ else
+ {
+ g_set_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ "Authentication failed");
+ }
+
+out:
+ self->startups--;
+
+ /* Successful login */
+ if (creds)
+ g_info ("User %s logged into session %s",
+ cockpit_creds_get_user (creds),
+ cockpit_web_service_get_id (session->service));
+
+ return body;
+}
+
+CockpitAuth *
+cockpit_auth_new (gboolean login_loopback,
+ CockpitAuthFlags flags)
+{
+ CockpitAuth *self = g_object_new (COCKPIT_TYPE_AUTH, NULL);
+ const gchar *max_startups_conf;
+ gint count = 0;
+
+ self->flags = flags;
+ self->login_loopback = login_loopback;
+
+ if (cockpit_ws_max_startups == NULL)
+ max_startups_conf = cockpit_conf_string ("WebService", "MaxStartups");
+ else
+ max_startups_conf = cockpit_ws_max_startups;
+
+ self->max_startups = max_startups;
+ self->max_startups_begin = max_startups;
+ self->max_startups_rate = 100;
+
+ if (max_startups_conf)
+ {
+ count = sscanf (max_startups_conf, "%u:%u:%u",
+ &self->max_startups_begin,
+ &self->max_startups_rate,
+ &self->max_startups);
+
+ /* If all three numbers are not given use the
+ * first as a hard limit */
+ if (count == 1 || count == 2)
+ {
+ self->max_startups = self->max_startups_begin;
+ self->max_startups_rate = 100;
+ }
+
+ if (count < 1 || count > 3 ||
+ self->max_startups_begin > self->max_startups ||
+ self->max_startups_rate > 100 || self->max_startups_rate < 1)
+ {
+ g_warning ("Illegal MaxStartups spec: %s. Reverting to defaults", max_startups_conf);
+ self->max_startups = max_startups;
+ self->max_startups_begin = max_startups;
+ self->max_startups_rate = 100;
+ }
+ }
+
+ return self;
+}
+
+gchar *
+cockpit_auth_parse_application (const gchar *path,
+ gboolean *is_host)
+{
+ const gchar *pos;
+ gchar *tmp = NULL;
+ gchar *val = NULL;
+
+ g_return_val_if_fail (path != NULL, NULL);
+ g_return_val_if_fail (path[0] == '/', NULL);
+
+ path += 1;
+
+ /* We are being embedded as a specific application */
+ if (g_str_has_prefix (path, "cockpit+") && path[8] != '\0')
+ {
+ pos = strchr (path, '/');
+ if (pos)
+ val = g_strndup (path, pos - path);
+ else
+ val = g_strdup (path);
+ }
+ else if (path[0] == '=' && path[1] != '\0')
+ {
+ pos = strchr (path, '/');
+ if (pos)
+ {
+ tmp = g_strndup (path, pos - path);
+ val = g_strdup_printf ("cockpit+%s", tmp);
+ }
+ else
+ {
+ val = g_strdup_printf ("cockpit+%s", path);
+ }
+
+ }
+ else
+ {
+ val = g_strdup ("cockpit");
+ }
+
+ if (is_host)
+ *is_host = application_parse_host (val) != NULL;
+
+ g_free (tmp);
+ return val;
+}
+
+gchar *
+cockpit_auth_empty_cookie_value (const gchar *path, gboolean secure)
+{
+ gchar *application = cockpit_auth_parse_application (path, NULL);
+ gchar *cookie = application_cookie_name (application);
+
+ /* this is completely security irrelevant, but security scanners complain
+ * about the lack of Secure (rhbz#1677767) */
+ gchar *cookie_line = g_strdup_printf ("%s=deleted; PATH=/; SameSite=strict;%s HttpOnly",
+ cookie,
+ secure ? " Secure;" : "");
+
+ g_free (application);
+ g_free (cookie);
+
+ return cookie_line;
+}
diff --git a/src/ws/cockpitauth.h b/src/ws/cockpitauth.h
new file mode 100644
index 0000000..038d5a2
--- /dev/null
+++ b/src/ws/cockpitauth.h
@@ -0,0 +1,113 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_AUTH_H__
+#define __COCKPIT_AUTH_H__
+
+#include <pwd.h>
+#include <gio/gio.h>
+
+#include "cockpitcreds.h"
+#include "cockpitwebservice.h"
+
+#include "common/cockpitpipe.h"
+#include "common/cockpittransport.h"
+#include "common/cockpitwebserver.h"
+
+G_BEGIN_DECLS
+
+typedef enum {
+ COCKPIT_AUTH_NONE = 0,
+ COCKPIT_AUTH_FOR_TLS_PROXY = 1 << 0,
+} CockpitAuthFlags;
+
+#define MAX_AUTH_TIMEOUT 900
+#define MIN_AUTH_TIMEOUT 1
+
+#define COCKPIT_TYPE_AUTH (cockpit_auth_get_type ())
+#define COCKPIT_AUTH(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_AUTH, CockpitAuth))
+#define COCKPIT_IS_AUTH(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), COCKPIT_TYPE_AUTH))
+#define COCKPIT_AUTH_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), COCKPIT_TYPE_AUTH, CockpitAuthClass))
+#define COCKPIT_IS_AUTH_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), COCKPIT_TYPE_AUTH))
+
+typedef struct _CockpitAuth CockpitAuth;
+typedef struct _CockpitAuthClass CockpitAuthClass;
+
+struct _CockpitAuth
+{
+ GObject parent_instance;
+
+ CockpitAuthFlags flags;
+ GBytes *key;
+ GHashTable *sessions;
+ GHashTable *conversations;
+
+ guint64 nonce_seed;
+ gboolean login_loopback;
+ gulong timeout_tag;
+ guint startups;
+ guint max_startups;
+ guint max_startups_begin;
+ guint max_startups_rate;
+};
+
+struct _CockpitAuthClass
+{
+ GObjectClass parent_class;
+};
+
+GType cockpit_auth_get_type (void) G_GNUC_CONST;
+
+CockpitAuth * cockpit_auth_new (gboolean login_loopback, CockpitAuthFlags flags);
+
+gchar * cockpit_auth_nonce (CockpitAuth *self);
+
+void cockpit_auth_login_async (CockpitAuth *self,
+ CockpitWebRequest *request,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+JsonObject * cockpit_auth_login_finish (CockpitAuth *self,
+ GAsyncResult *result,
+ GIOStream *connection,
+ GHashTable *out_headers,
+ GError **error);
+
+void cockpit_auth_local_async (CockpitAuth *self,
+ const gchar *user,
+ CockpitPipe *pipe,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean cockpit_auth_local_finish (CockpitAuth *self,
+ GAsyncResult *result,
+ GError **error);
+
+CockpitWebService * cockpit_auth_check_cookie (CockpitAuth *self,
+ CockpitWebRequest *request);
+
+gchar * cockpit_auth_parse_application (const gchar *path,
+ gboolean *is_host);
+
+gchar * cockpit_auth_empty_cookie_value (const gchar *path,
+ gboolean secure);
+
+G_END_DECLS
+
+#endif
diff --git a/src/ws/cockpitbranding.c b/src/ws/cockpitbranding.c
new file mode 100644
index 0000000..137d8a8
--- /dev/null
+++ b/src/ws/cockpitbranding.c
@@ -0,0 +1,211 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitwebserver.h"
+#include "common/cockpittransport.h"
+#include "common/cockpitsystem.h"
+
+#include "cockpitauth.h"
+#include "cockpitbranding.h"
+#include "cockpitwebservice.h"
+
+static const gchar * const*
+get_system_data_dirs (void)
+{
+ const gchar *env;
+
+ env = g_getenv ("XDG_DATA_DIRS");
+ if (env && env[0])
+ return g_get_system_data_dirs ();
+
+ return NULL;
+}
+
+static void
+add_system_dirs (GPtrArray *dirs)
+{
+ const gchar * const* system;
+ system = get_system_data_dirs ();
+ while (system && system[0])
+ {
+ g_ptr_array_add (dirs, g_build_filename (system[0], "cockpit", "static", NULL));
+ system++;
+ }
+}
+
+gchar **
+cockpit_branding_calculate_static_roots (const gchar *os_id,
+ const gchar *os_variant_id,
+ const gchar *os_id_like,
+ gboolean is_local)
+{
+ GPtrArray *dirs;
+ gchar **roots;
+
+ dirs = g_ptr_array_new_with_free_func (g_free);
+
+ if (is_local)
+ add_system_dirs (dirs);
+
+ if (os_id)
+ {
+ if (os_variant_id)
+ g_ptr_array_add (dirs, g_strdup_printf (DATADIR "/cockpit/branding/%s-%s", os_id, os_variant_id));
+ g_ptr_array_add (dirs, g_strdup_printf (DATADIR "/cockpit/branding/%s", os_id));
+ }
+
+ if (os_id_like)
+ {
+ gchar **ids;
+
+ ids = g_strsplit_set (os_id_like, " ", -1);
+ for (gint i = 0; ids[i]; i += 1)
+ g_ptr_array_add (dirs, g_strdup_printf (DATADIR "/cockpit/branding/%s", ids[i]));
+
+ g_strfreev (ids);
+ }
+
+ if (!is_local)
+ add_system_dirs (dirs);
+
+ g_ptr_array_add (dirs, g_strdup (DATADIR "/cockpit/branding/default"));
+ g_ptr_array_add (dirs, g_strdup (DATADIR "/cockpit/static"));
+ g_ptr_array_add (dirs, NULL);
+
+ roots = cockpit_web_response_resolve_roots ((const gchar **)dirs->pdata);
+
+ g_ptr_array_free (dirs, TRUE);
+ return roots;
+}
+
+static void
+serve_branding_css_file (CockpitWebResponse *response,
+ const gchar *path,
+ const gchar **roots,
+ GHashTable *os_release)
+{
+ if (os_release)
+ cockpit_web_response_template (response, path, roots, os_release);
+ else
+ cockpit_web_response_file (response, path, roots);
+}
+
+typedef struct {
+ const gchar *path;
+ CockpitWebResponse *response;
+} CockpitBrandingData;
+
+
+static void
+serve_branding_css_with_init_data (CockpitWebService *service,
+ CockpitWebResponse *response,
+ const gchar *path)
+{
+ CockpitTransport *transport = NULL;
+ GHashTable *os_release = NULL;
+ gchar **roots = NULL;
+ JsonObject *os = NULL;
+ gboolean responded = FALSE;
+ JsonObject *init = NULL;
+
+ init = cockpit_web_service_get_init (service);
+ if (!init)
+ goto out;
+
+ transport = cockpit_web_service_get_transport (service);
+ if (!transport)
+ goto out;
+
+ roots = g_object_get_data (G_OBJECT (transport), "static-roots");
+ if (!roots)
+ {
+ if (cockpit_json_get_object (init, "os-release", NULL, &os) && os)
+ os_release = cockpit_json_to_hash_table (os, cockpit_system_os_release_fields ());
+
+ if (os_release)
+ {
+ roots = cockpit_branding_calculate_static_roots (g_hash_table_lookup (os_release, "ID"),
+ g_hash_table_lookup (os_release, "VARIANT_ID"),
+ g_hash_table_lookup (os_release, "ID_LIKE"),
+ FALSE);
+ g_object_set_data_full (G_OBJECT (transport), "os-release", os_release,
+ (GDestroyNotify) g_hash_table_unref);
+ }
+ else
+ {
+ roots = cockpit_branding_calculate_static_roots (NULL, NULL, NULL, FALSE);
+ }
+
+ g_object_set_data_full (G_OBJECT (transport), "static-roots", roots,
+ (GDestroyNotify) g_strfreev);
+ }
+ else
+ {
+ os_release = g_object_get_data (G_OBJECT (transport), "os-release");
+ }
+
+ serve_branding_css_file (response, path, (const gchar **)roots, os_release);
+ responded = TRUE;
+
+out:
+ if (!responded)
+ cockpit_web_response_error (response, 502, NULL, NULL);
+}
+
+void
+cockpit_branding_serve (CockpitWebService *service,
+ CockpitWebResponse *response,
+ const gchar *full_path,
+ const gchar *static_path,
+ GHashTable *local_os_release,
+ const gchar **local_roots)
+{
+ gboolean is_host = FALSE;
+ gchar *application = cockpit_auth_parse_application (full_path, &is_host);
+
+ /* Must be logged in to use a host url */
+ if (is_host && !service)
+ {
+ cockpit_web_response_error (response, 403, NULL, NULL);
+ goto out;
+ }
+
+ cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_CACHE);
+
+ if (g_str_has_suffix (static_path, ".css"))
+ {
+ if (!is_host)
+ {
+ serve_branding_css_file (response, static_path, local_roots, local_os_release);
+ }
+ else
+ {
+ serve_branding_css_with_init_data (service, response, static_path);
+ }
+ }
+ else
+ {
+ cockpit_web_response_file (response, static_path, local_roots);
+ }
+
+out:
+ g_free (application);
+}
diff --git a/src/ws/cockpitbranding.h b/src/ws/cockpitbranding.h
new file mode 100644
index 0000000..43cca04
--- /dev/null
+++ b/src/ws/cockpitbranding.h
@@ -0,0 +1,41 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_BRANDING_H__
+#define __COCKPIT_BRANDING_H__
+
+G_BEGIN_DECLS
+
+#include "cockpitwebservice.h"
+
+gchar ** cockpit_branding_calculate_static_roots (const gchar *os_id,
+ const gchar *os_variant_id,
+ const gchar *os_id_like,
+ gboolean is_local);
+
+void cockpit_branding_serve (CockpitWebService *service,
+ CockpitWebResponse *response,
+ const gchar *full_path,
+ const gchar *static_path,
+ GHashTable *local_os_release,
+ const gchar **local_roots);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_BRANDING_H__ */
diff --git a/src/ws/cockpitchannelresponse.c b/src/ws/cockpitchannelresponse.c
new file mode 100644
index 0000000..abe643c
--- /dev/null
+++ b/src/ws/cockpitchannelresponse.c
@@ -0,0 +1,789 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitchannelresponse.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpitflow.h"
+#include "common/cockpitwebinject.h"
+#include "common/cockpitwebserver.h"
+#include "common/cockpitwebresponse.h"
+
+#include <string.h>
+
+typedef struct {
+ CockpitWebService *service;
+ gchar *base_path;
+ gchar *host;
+} CockpitChannelInject;
+
+static void
+cockpit_channel_inject_free (gpointer data)
+{
+ CockpitChannelInject *inject = data;
+
+ if (inject)
+ {
+ if (inject->service)
+ g_object_remove_weak_pointer (G_OBJECT (inject->service), (gpointer *)&inject->service);
+ g_free (inject->base_path);
+ g_free (inject->host);
+ g_free (inject);
+ }
+}
+
+static CockpitChannelInject *
+cockpit_channel_inject_new (CockpitWebService *service,
+ const gchar *path,
+ const gchar *host)
+{
+ CockpitChannelInject *inject = g_new (CockpitChannelInject, 1);
+ inject->service = service;
+ g_object_add_weak_pointer (G_OBJECT (inject->service), (gpointer *)&inject->service);
+ inject->base_path = g_strdup (path);
+ inject->host = g_strdup (host);
+ return inject;
+}
+
+static void
+cockpit_channel_inject_update_checksum (CockpitChannelInject *inject,
+ GHashTable *headers)
+{
+ const gchar *checksum = g_hash_table_lookup (headers, COCKPIT_CHECKSUM_HEADER);
+
+ if (checksum)
+ cockpit_web_service_set_host_checksum (inject->service, inject->host, checksum);
+
+ /* No need to send our custom header outside of cockpit */
+ g_hash_table_remove (headers, COCKPIT_CHECKSUM_HEADER);
+}
+
+static void
+cockpit_channel_inject_perform (CockpitChannelInject *inject,
+ CockpitWebResponse *response,
+ CockpitTransport *transport)
+{
+ static const gchar *marker = "<head>";
+ g_autofree gchar *prefixed_application = NULL;
+
+ const gchar *url_root = cockpit_web_response_get_url_root (response);
+
+ if (!url_root && !inject->base_path)
+ return;
+
+ g_autoptr(GString) str = g_string_new ("");
+ CockpitCreds *creds = cockpit_web_service_get_creds (inject->service);
+ if (url_root)
+ {
+ g_string_append_printf (str, "\n <meta name=\"url-root\" content=\"%s\">", url_root);
+ prefixed_application = g_strdup_printf ("%s/%s",
+ url_root,
+ cockpit_creds_get_application (creds));
+ }
+ else
+ {
+ prefixed_application = g_strdup_printf ("/%s", cockpit_creds_get_application (creds));
+ }
+
+ if (inject->base_path)
+ {
+ const gchar *checksum = cockpit_web_service_get_checksum (inject->service, inject->host);
+ if (checksum)
+ {
+ g_string_append_printf (str, "\n <base href=\"%s/$%s%s\">",
+ prefixed_application,
+ checksum, inject->base_path);
+ }
+ else
+ {
+ g_string_append_printf (str, "\n <base href=\"%s/@%s%s\">",
+ prefixed_application, inject->host, inject->base_path);
+ }
+ }
+
+ g_autoptr(GBytes) content = g_string_free_to_bytes (g_steal_pointer(&str));
+ g_autoptr(CockpitWebFilter) filter = cockpit_web_inject_new (marker, content, 1);
+ cockpit_web_response_add_filter (response, filter);
+}
+
+#define COCKPIT_TYPE_CHANNEL_RESPONSE (cockpit_channel_response_get_type ())
+#define COCKPIT_CHANNEL_RESPONSE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_CHANNEL_RESPONSE, CockpitChannelResponse))
+#define COCKPIT_IS_CHANNEL_RESPONSE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), COCKPIT_TYPE_CHANNEL_RESPONSE))
+
+typedef struct {
+ CockpitChannel parent;
+
+ const gchar *logname;
+ CockpitWebResponse *response;
+ GHashTable *headers;
+
+ /* We can handle http-stream1 and http-stream2 */
+ gboolean http_stream1_prefix;
+ gboolean http_stream2;
+
+ /* Set when injecting data into response */
+ CockpitChannelInject *inject;
+} CockpitChannelResponse;
+
+typedef struct {
+ CockpitChannelClass parent;
+} CockpitChannelResponseClass;
+
+GType cockpit_channel_response_get_type (void);
+
+G_DEFINE_TYPE (CockpitChannelResponse, cockpit_channel_response, COCKPIT_TYPE_CHANNEL);
+
+static void
+cockpit_channel_response_init (CockpitChannelResponse *self)
+{
+
+}
+
+static void
+cockpit_channel_response_finalize (GObject *object)
+{
+ CockpitChannelResponse *self = COCKPIT_CHANNEL_RESPONSE (object);
+
+ g_object_unref (self->response);
+ g_hash_table_unref (self->headers);
+ cockpit_channel_inject_free (self->inject);
+
+ G_OBJECT_CLASS (cockpit_channel_response_parent_class)->finalize (object);
+}
+
+static gboolean
+ensure_headers (CockpitChannelResponse *self,
+ guint status,
+ const gchar *reason,
+ gsize length)
+{
+
+ if (cockpit_web_response_get_state (self->response) == COCKPIT_WEB_RESPONSE_READY)
+ {
+ if (self->inject && self->inject->service)
+ {
+ cockpit_channel_inject_update_checksum (self->inject, self->headers);
+ cockpit_channel_inject_perform (self->inject, self->response,
+ cockpit_channel_get_transport (COCKPIT_CHANNEL (self)));
+ }
+ cockpit_web_response_headers_full (self->response, status, reason, length, self->headers);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+cockpit_channel_response_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ CockpitChannelResponse *self = COCKPIT_CHANNEL_RESPONSE (channel);
+ CockpitWebResponding state;
+
+ /* The web response should not yet be complete */
+ state = cockpit_web_response_get_state (self->response);
+
+ if (problem == NULL)
+ {
+ /* Closed without any data */
+ if (state == COCKPIT_WEB_RESPONSE_READY)
+ {
+ ensure_headers (self, 204, "OK", 0);
+ cockpit_web_response_complete (self->response);
+ g_debug ("%s: no content in external channel", self->logname);
+ }
+ else if (state < COCKPIT_WEB_RESPONSE_COMPLETE)
+ {
+ g_message ("%s: truncated data in external channel", self->logname);
+ cockpit_web_response_abort (self->response);
+ }
+ else
+ {
+ g_debug ("%s: completed serving external channel", self->logname);
+ }
+ }
+ else if (state == COCKPIT_WEB_RESPONSE_READY)
+ {
+ if (g_str_equal (problem, "not-found"))
+ {
+ g_debug ("%s: not found", self->logname);
+ cockpit_web_response_error (self->response, 404, NULL, NULL);
+ }
+ else if (g_str_equal (problem, "access-denied"))
+ {
+ g_debug ("%s: forbidden", self->logname);
+ cockpit_web_response_error (self->response, 403, NULL, NULL);
+ }
+ else if (g_str_equal (problem, "no-host") ||
+ g_str_equal (problem, "no-cockpit") ||
+ g_str_equal (problem, "unknown-hostkey") ||
+ g_str_equal (problem, "unknown-host") ||
+ g_str_equal (problem, "authentication-failed") ||
+ g_str_equal (problem, "disconnected"))
+ {
+ g_debug ("%s: remote server unavailable: %s", self->logname, problem);
+ cockpit_web_response_error (self->response, 502, NULL, "%s", problem);
+ }
+ else
+ {
+ g_message ("%s: external channel failed: %s", self->logname, problem);
+ cockpit_web_response_error (self->response, 500, NULL, "%s", problem);
+ }
+ }
+ else
+ {
+ if (g_str_equal (problem, "disconnected") || g_str_equal (problem, "terminated"))
+ g_debug ("%s: failure while serving external channel: %s", self->logname, problem);
+ else
+ g_message ("%s: failure while serving external channel: %s", self->logname, problem);
+ if (state < COCKPIT_WEB_RESPONSE_COMPLETE)
+ cockpit_web_response_abort (self->response);
+ }
+}
+
+static void
+object_to_headers (JsonObject *object,
+ const gchar *header,
+ JsonNode *node,
+ gpointer user_data)
+{
+ GHashTable *headers = user_data;
+ const gchar *value = json_node_get_string (node);
+
+ g_return_if_fail (value != NULL);
+
+ /* Remove hop-by-hop headers. See RFC 2068 */
+ if (g_ascii_strcasecmp (header, "Connection") == 0 ||
+ g_ascii_strcasecmp (header, "Keep-Alive") == 0 ||
+ g_ascii_strcasecmp (header, "Public") == 0 ||
+ g_ascii_strcasecmp (header, "Proxy-Authenticate") == 0 ||
+ g_ascii_strcasecmp (header, "Transfer-Encoding") == 0 ||
+ g_ascii_strcasecmp (header, "Upgrade") == 0)
+ return;
+
+ g_hash_table_insert (headers, g_strdup (header), g_strdup (value));
+}
+
+static gboolean
+parse_httpstream_response (CockpitChannelResponse *self,
+ JsonObject *object,
+ gint64 *status,
+ const gchar **reason,
+ gssize *length)
+{
+ const gchar *content_length = NULL;
+ JsonNode *node;
+
+ if (!cockpit_json_get_int (object, "status", 200, status) ||
+ !cockpit_json_get_string (object, "reason", NULL, reason))
+ {
+ g_warning ("%s: received invalid httpstream response", self->logname);
+ return FALSE;
+ }
+
+ node = json_object_get_member (object, "headers");
+ if (node)
+ {
+ if (!JSON_NODE_HOLDS_OBJECT (node))
+ {
+ g_warning ("%s: received invalid httpstream headers", self->logname);
+ return FALSE;
+ }
+ json_object_foreach_member (json_node_get_object (node), object_to_headers, self->headers);
+ }
+
+
+ /* Default to unknown length */
+ *length = -1;
+
+ content_length = g_hash_table_lookup (self->headers, "Content-Length");
+ if (content_length)
+ {
+ gchar *endptr = NULL;
+ gint64 result = g_ascii_strtoll (content_length, &endptr, 10);
+ if (result > 0 && result <= G_MAXSIZE && endptr && *endptr == '\0')
+ *length = (gssize)result;
+
+ /* We don't relay Content-Length directly, but expect CockpitWebResponse to set it again */
+ g_hash_table_remove (self->headers, "Content-Length");
+ }
+
+ return TRUE;
+}
+
+static void
+process_httpstream1_recv (CockpitChannelResponse *self,
+ GBytes *payload)
+{
+ GError *error = NULL;
+ JsonObject *object;
+ gint64 status;
+ const gchar *reason;
+ gssize length;
+
+ g_return_if_fail (cockpit_web_response_get_state (self->response) == COCKPIT_WEB_RESPONSE_READY);
+
+ object = cockpit_json_parse_bytes (payload, &error);
+ if (error)
+ {
+ g_warning ("%s: couldn't parse http-stream1 header payload: %s", self->logname, error->message);
+ cockpit_web_response_error (self->response, 500, NULL, NULL);
+ g_error_free (error);
+ return;
+ }
+
+ if (parse_httpstream_response (self, object, &status, &reason, &length))
+ {
+ if (!ensure_headers (self, status, reason, length))
+ g_return_if_reached ();
+ }
+ else
+ {
+ cockpit_web_response_error (self->response, 500, NULL, NULL);
+ }
+
+ json_object_unref (object);
+}
+
+static void
+cockpit_channel_response_recv (CockpitChannel *channel,
+ GBytes *payload)
+{
+ CockpitChannelResponse *self = COCKPIT_CHANNEL_RESPONSE (channel);
+
+ /* First response payload message is meta data, then switch to actual data */
+ if (self->http_stream1_prefix)
+ {
+ process_httpstream1_recv (self, payload);
+ self->http_stream1_prefix = FALSE;
+ return;
+ }
+
+ ensure_headers (self, 200, "OK", -1);
+ cockpit_web_response_queue (self->response, payload);
+}
+
+static gboolean
+cockpit_channel_response_control (CockpitChannel *channel,
+ const gchar *command,
+ JsonObject *options)
+{
+ CockpitChannelResponse *self = COCKPIT_CHANNEL_RESPONSE (channel);
+ gint64 status;
+ const gchar *reason;
+ gssize length;
+
+ if (self->http_stream2)
+ {
+ if (g_str_equal (command, "response"))
+ {
+ if (parse_httpstream_response (self, options, &status, &reason, &length))
+ {
+ if (!ensure_headers (self, status, reason, length))
+ g_return_val_if_reached (FALSE);
+ }
+ else
+ {
+ cockpit_web_response_error (self->response, 500, NULL, NULL);
+ }
+ return TRUE;
+ }
+ }
+
+ if (g_str_equal (command, "ready"))
+ {
+ gint64 content_length;
+ if (cockpit_json_get_int (options, "size-hint", -1, &content_length) && content_length != -1)
+ ensure_headers (self, 200, "OK", content_length);
+
+ return TRUE;
+ }
+
+ if (g_str_equal (command, "done"))
+ {
+ ensure_headers (self, 200, "OK", 0);
+ cockpit_web_response_complete (self->response);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+cockpit_channel_response_prepare (CockpitChannel *channel)
+{
+ CockpitChannelResponse *self = COCKPIT_CHANNEL_RESPONSE (channel);
+ const gchar *payload;
+ JsonObject *open;
+
+ COCKPIT_CHANNEL_CLASS (cockpit_channel_response_parent_class)->prepare (channel);
+
+ /*
+ * Tell the transport to throttle incoming flow on the given channel based on
+ * output pressure in the web response.
+ */
+ cockpit_flow_throttle (COCKPIT_FLOW (channel), COCKPIT_FLOW (self->response));
+
+ open = cockpit_channel_get_options (channel);
+ cockpit_json_get_string (open, "path", NULL, &self->logname);
+ if (!self->logname)
+ self->logname = cockpit_channel_get_id (channel);
+
+ payload = json_object_get_string_member (open, "payload");
+
+ /* Special handling for http-stream1, splice in headers, handle injection */
+ if (g_strcmp0 (payload, "http-stream1") == 0)
+ self->http_stream1_prefix = TRUE;
+
+ /* Special handling for http-stream2, splice in headers, handle injection */
+ if (g_strcmp0 (payload, "http-stream2") == 0)
+ self->http_stream2 = TRUE;
+
+ /* Send the open message across the transport */
+ cockpit_channel_control (channel, "open", open);
+
+ /* Tell the channel we're ready */
+ cockpit_channel_ready (channel, NULL);
+
+ /* Indicate we are done sending input, we support no POST or PUT */
+ cockpit_channel_control (channel, "done", NULL);
+}
+
+static void
+cockpit_channel_response_class_init (CockpitChannelResponseClass *klass)
+{
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = cockpit_channel_response_finalize;
+
+ channel_class->prepare = cockpit_channel_response_prepare;
+ channel_class->recv = cockpit_channel_response_recv;
+ channel_class->control = cockpit_channel_response_control;
+ channel_class->close = cockpit_channel_response_close;
+}
+
+static CockpitChannelResponse *
+cockpit_channel_response_new (CockpitWebService *service,
+ CockpitWebResponse *response,
+ CockpitTransport *transport,
+ GHashTable *headers,
+ JsonObject *options)
+{
+ CockpitChannelResponse *self;
+ gchar *id;
+
+ id = cockpit_web_service_unique_channel (service);
+ self = g_object_new (COCKPIT_TYPE_CHANNEL_RESPONSE,
+ "transport", transport,
+ "options", options,
+ "id", id,
+ NULL);
+
+ self->response = g_object_ref (response);
+ self->headers = g_hash_table_ref (headers);
+
+ g_free (id);
+ return self;
+}
+
+static gboolean
+is_resource_a_package_file (const gchar *path)
+{
+ return path && path[0] && strchr (path + 1, '/') != NULL;
+}
+
+static gboolean
+parse_host_and_etag (CockpitWebService *service,
+ GHashTable *headers,
+ const gchar *where,
+ const gchar *path,
+ const gchar **host,
+ gchar **etag)
+{
+ const gchar *accept = NULL;
+ gchar **languages = NULL;
+ gboolean translatable;
+ gchar *language;
+
+ /* Parse the language out of the CockpitLang cookie and set Accept-Language */
+ language = cockpit_web_server_parse_cookie (headers, "CockpitLang");
+ if (language)
+ g_hash_table_replace (headers, g_strdup ("Accept-Language"), language);
+
+ if (!where)
+ {
+ *host = "localhost";
+ *etag = NULL;
+ return TRUE;
+ }
+ if (where[0] == '@')
+ {
+ *host = where + 1;
+ *etag = NULL;
+ return TRUE;
+ }
+
+ if (!where || where[0] != '$')
+ return FALSE;
+
+ *host = cockpit_web_service_get_host (service, where + 1);
+ if (!*host)
+ return FALSE;
+
+ /* Top level resources (like the /manifests) are not translatable */
+ translatable = is_resource_a_package_file (path);
+
+ /* The ETag contains the language setting */
+ if (translatable)
+ {
+ accept = g_hash_table_lookup (headers, "Accept-Language");
+ languages = cockpit_web_server_parse_accept_list (accept, "C");
+ *etag = g_strdup_printf ("\"%s-%s\"", where, languages[0]);
+ g_strfreev (languages);
+ }
+ else
+ {
+ *etag = g_strdup_printf ("\"%s\"", where);
+ }
+
+ return TRUE;
+}
+
+void
+cockpit_channel_response_serve (CockpitWebService *service,
+ GHashTable *in_headers,
+ CockpitWebResponse *response,
+ const gchar *where,
+ const gchar *path)
+{
+ CockpitChannelResponse *self = NULL;
+ CockpitTransport *transport = NULL;
+ CockpitCacheType cache_type = COCKPIT_WEB_RESPONSE_CACHE;
+ const gchar *injecting_base_path = NULL;
+ const gchar *host = NULL;
+ const gchar *pragma;
+ gchar *quoted_etag = NULL;
+ GHashTable *out_headers = NULL;
+ gchar *val = NULL;
+ gboolean handled = FALSE;
+ GHashTableIter iter;
+ JsonObject *object = NULL;
+ JsonObject *heads;
+ const gchar *protocol;
+ const gchar *http_host = "localhost";
+ gchar *channel = NULL;
+ gpointer key;
+ gpointer value;
+
+ g_return_if_fail (COCKPIT_IS_WEB_SERVICE (service));
+ g_return_if_fail (in_headers != NULL);
+ g_return_if_fail (COCKPIT_IS_WEB_RESPONSE (response));
+ g_return_if_fail (path != NULL);
+
+ /* Where might be NULL, but that's still valid */
+ if (!parse_host_and_etag (service, in_headers, where, path, &host, &quoted_etag))
+ {
+ /* Did not recognize the where */
+ goto out;
+ }
+
+ if (quoted_etag)
+ {
+ cache_type = COCKPIT_WEB_RESPONSE_CACHE;
+ pragma = g_hash_table_lookup (in_headers, "Pragma");
+
+ if ((!pragma || !strstr (pragma, "no-cache")) &&
+ g_strcmp0 (g_hash_table_lookup (in_headers, "If-None-Match"), quoted_etag) == 0)
+ {
+ cockpit_web_response_headers (response, 304, "Not Modified", 0, "ETag", quoted_etag, NULL);
+ cockpit_web_response_complete (response);
+ handled = TRUE;
+ goto out;
+ }
+ }
+
+ cockpit_web_response_set_cache_type (response, cache_type);
+ object = cockpit_transport_build_json ("command", "open",
+ "payload", "http-stream1",
+ "internal", "packages",
+ "method", "GET",
+ "host", host,
+ "path", path,
+ "binary", "raw",
+ NULL);
+
+ transport = cockpit_web_service_get_transport (service);
+ if (!transport)
+ goto out;
+
+ out_headers = cockpit_web_server_new_table ();
+
+ channel = cockpit_web_service_unique_channel (service);
+ json_object_set_string_member (object, "channel", channel);
+ json_object_set_boolean_member (object, "flow-control", TRUE);
+
+ if (quoted_etag)
+ {
+ /*
+ * If we have a checksum, then use it as an ETag. It is intentional that
+ * a cockpit-bridge version could (in the future) override this.
+ */
+ g_hash_table_insert (out_headers, g_strdup ("ETag"), quoted_etag);
+ quoted_etag = NULL;
+ }
+
+ heads = json_object_new ();
+
+ g_hash_table_iter_init (&iter, in_headers);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ val = NULL;
+
+ if (g_ascii_strcasecmp (key, "Cookie") == 0 ||
+ g_ascii_strcasecmp (key, "Referer") == 0 ||
+ g_ascii_strcasecmp (key, "Connection") == 0 ||
+ g_ascii_strcasecmp (key, "Pragma") == 0 ||
+ g_ascii_strcasecmp (key, "Cache-Control") == 0 ||
+ g_ascii_strcasecmp (key, "User-Agent") == 0 ||
+ g_ascii_strcasecmp (key, "Accept-Charset") == 0 ||
+ g_ascii_strcasecmp (key, "Accept-Ranges") == 0 ||
+ g_ascii_strcasecmp (key, "Content-Length") == 0 ||
+ g_ascii_strcasecmp (key, "Content-MD5") == 0 ||
+ g_ascii_strcasecmp (key, "Content-Range") == 0 ||
+ g_ascii_strcasecmp (key, "Range") == 0 ||
+ g_ascii_strcasecmp (key, "TE") == 0 ||
+ g_ascii_strcasecmp (key, "Trailer") == 0 ||
+ g_ascii_strcasecmp (key, "Upgrade") == 0 ||
+ g_ascii_strcasecmp (key, "Transfer-Encoding") == 0 ||
+ g_ascii_strcasecmp (key, "X-Forwarded-For") == 0 ||
+ g_ascii_strcasecmp (key, "X-Forwarded-Host") == 0 ||
+ g_ascii_strcasecmp (key, "X-Forwarded-Protocol") == 0)
+ continue;
+
+ if (g_ascii_strcasecmp (key, "Host") == 0)
+ http_host = (gchar *) value;
+ else
+ json_object_set_string_member (heads, key, value);
+
+ g_free (val);
+ }
+
+ /* Send along the HTTP scheme the package should assume is accessing things */
+ protocol = cockpit_web_response_get_protocol (response);
+
+ json_object_set_string_member (heads, "Host", host);
+ json_object_set_string_member (heads, "X-Forwarded-Proto", protocol);
+ json_object_set_string_member (heads, "X-Forwarded-Host", http_host);
+
+ /* We only inject a <base> if root level request */
+ injecting_base_path = where ? NULL : path;
+ if (injecting_base_path)
+ {
+ /* If we are injecting a <base> element, then we don't allow gzip compression */
+ json_object_set_string_member (heads, "Accept-Encoding", "identity");
+ }
+
+ json_object_set_object_member (object, "headers", heads);
+
+ self = cockpit_channel_response_new (service, response, transport,
+ out_headers, object);
+
+ self->inject = cockpit_channel_inject_new (service, injecting_base_path, host);
+ handled = TRUE;
+
+ /* Unref when the channel closes */
+ g_signal_connect_after (self, "closed", G_CALLBACK (g_object_unref), NULL);
+
+out:
+ if (object)
+ json_object_unref (object);
+ g_free (quoted_etag);
+ if (out_headers)
+ g_hash_table_unref (out_headers);
+ g_free (channel);
+
+ if (!handled)
+ cockpit_web_response_error (response, 404, NULL, NULL);
+}
+
+void
+cockpit_channel_response_open (CockpitWebService *service,
+ CockpitWebRequest *request,
+ JsonObject *open)
+{
+ CockpitChannelResponse *self;
+ CockpitTransport *transport;
+ WebSocketDataType data_type;
+ GHashTable *headers;
+ const gchar *content_type;
+ const gchar *content_encoding;
+ const gchar *content_disposition;
+
+ g_autoptr(CockpitWebResponse) response = cockpit_web_request_respond (request);
+
+ /* Parse the external */
+ if (!cockpit_web_service_parse_external (open, &content_type, &content_encoding, &content_disposition, NULL))
+ {
+ cockpit_web_response_error (response, 400, NULL, "Bad channel request");
+ return;
+ }
+
+ transport = cockpit_web_service_get_transport (service);
+ if (!transport)
+ {
+ cockpit_web_response_error (response, 502, NULL, "Failed to open channel transport");
+ return;
+ }
+
+ headers = cockpit_web_server_new_table ();
+
+ if (content_disposition)
+ g_hash_table_insert (headers, g_strdup ("Content-Disposition"), g_strdup (content_disposition));
+
+ if (!json_object_has_member (open, "binary"))
+ json_object_set_string_member (open, "binary", "raw");
+
+ json_object_set_boolean_member (open, "flow-control", TRUE);
+
+ if (!content_type)
+ {
+ if (!cockpit_web_service_parse_binary (open, &data_type))
+ g_return_if_reached ();
+ if (data_type == WEB_SOCKET_DATA_TEXT)
+ content_type = "text/plain";
+ else
+ content_type = "application/octet-stream";
+ }
+ g_hash_table_insert (headers, g_strdup ("Content-Type"), g_strdup (content_type));
+
+ if (content_encoding)
+ g_hash_table_insert (headers, g_strdup ("Content-Encoding"), g_strdup (content_encoding));
+
+ /* We shouldn't need to send this part further */
+ json_object_remove_member (open, "external");
+
+ self = cockpit_channel_response_new (service, response, transport, headers, open);
+ g_hash_table_unref (headers);
+
+ /* Unref when the channel closes */
+ g_signal_connect_after (self, "closed", G_CALLBACK (g_object_unref), NULL);
+}
diff --git a/src/ws/cockpitchannelresponse.h b/src/ws/cockpitchannelresponse.h
new file mode 100644
index 0000000..e04de1d
--- /dev/null
+++ b/src/ws/cockpitchannelresponse.h
@@ -0,0 +1,41 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_CHANNEL_RESPONSE_H__
+#define __COCKPIT_CHANNEL_RESPONSE_H__
+
+#include "common/cockpitwebserver.h"
+#include "cockpitwebservice.h"
+
+G_BEGIN_DECLS
+
+
+void cockpit_channel_response_serve (CockpitWebService *service,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ const gchar *where,
+ const gchar *path);
+
+void cockpit_channel_response_open (CockpitWebService *service,
+ CockpitWebRequest *request,
+ JsonObject *open);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_CHANNEL_RESPONSE_H__ */
diff --git a/src/ws/cockpitchannelsocket.c b/src/ws/cockpitchannelsocket.c
new file mode 100644
index 0000000..0921f37
--- /dev/null
+++ b/src/ws/cockpitchannelsocket.c
@@ -0,0 +1,233 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitchannelsocket.h"
+
+#include "common/cockpitchannel.h"
+#include "common/cockpitflow.h"
+
+#include "websocket/websocket.h"
+
+#include <string.h>
+
+#define COCKPIT_TYPE_CHANNEL_SOCKET (cockpit_channel_socket_get_type ())
+#define COCKPIT_CHANNEL_SOCKET(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_CHANNEL_SOCKET, CockpitChannelSocket))
+#define COCKPIT_IS_CHANNEL_SOCKET(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), COCKPIT_TYPE_CHANNEL_SOCKET))
+
+typedef struct {
+ CockpitChannel parent;
+ gboolean closed;
+
+ /* The WebSocket side of things */
+ WebSocketConnection *socket;
+ WebSocketDataType data_type;
+ gulong socket_open;
+ gulong socket_message;
+ gulong socket_close;
+} CockpitChannelSocket;
+
+typedef struct {
+ CockpitChannelClass parent;
+} CockpitChannelSocketClass;
+
+GType cockpit_channel_socket_get_type (void);
+
+G_DEFINE_TYPE (CockpitChannelSocket, cockpit_channel_socket, COCKPIT_TYPE_CHANNEL);
+
+static void
+cockpit_channel_socket_init (CockpitChannelSocket *self)
+{
+
+}
+
+static void
+cockpit_channel_socket_recv (CockpitChannel *channel,
+ GBytes *payload)
+{
+ CockpitChannelSocket *self = COCKPIT_CHANNEL_SOCKET (channel);
+
+ if (web_socket_connection_get_ready_state (self->socket) == WEB_SOCKET_STATE_OPEN)
+ web_socket_connection_send (self->socket, self->data_type, NULL, payload);
+}
+
+static void
+cockpit_channel_socket_finalize (GObject *object)
+{
+ CockpitChannelSocket *self = COCKPIT_CHANNEL_SOCKET (object);
+
+ g_signal_handler_disconnect (self->socket, self->socket_open);
+ g_signal_handler_disconnect (self->socket, self->socket_message);
+ g_signal_handler_disconnect (self->socket, self->socket_close);
+ g_object_unref (self->socket);
+
+ G_OBJECT_CLASS (cockpit_channel_socket_parent_class)->finalize (object);
+}
+
+static void
+cockpit_channel_socket_close (CockpitChannel *channel,
+ const gchar *problem)
+{
+ CockpitChannelSocket *self = COCKPIT_CHANNEL_SOCKET (channel);
+ gushort code;
+
+ self->closed = TRUE;
+
+ if (web_socket_connection_get_ready_state (self->socket) < WEB_SOCKET_STATE_CLOSING)
+ {
+ if (problem)
+ code = WEB_SOCKET_CLOSE_GOING_AWAY;
+ else
+ code = WEB_SOCKET_CLOSE_NORMAL;
+ web_socket_connection_close (self->socket, code, problem);
+ }
+}
+
+static void
+on_socket_open (WebSocketConnection *connection,
+ gpointer user_data)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
+ JsonObject *open;
+
+ /*
+ * Actually open the channel. We wait until the WebSocket is open
+ * before doing this, so we don't receive messages from the bridge
+ * before the websocket is open.
+ */
+
+ open = cockpit_channel_get_options (channel);
+ cockpit_channel_control (channel, "open", open);
+
+ /* Tell the channel we're ready */
+ cockpit_channel_ready (channel, NULL);
+}
+
+static void
+on_socket_message (WebSocketConnection *connection,
+ WebSocketDataType data_type,
+ GBytes *payload,
+ gpointer user_data)
+{
+ CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
+ cockpit_channel_send (channel, payload, data_type == WEB_SOCKET_DATA_TEXT);
+}
+
+static void
+on_socket_close (WebSocketConnection *socket,
+ gpointer user_data)
+{
+ CockpitChannelSocket *self = COCKPIT_CHANNEL_SOCKET (user_data);
+ CockpitChannel *channel = COCKPIT_CHANNEL (user_data);
+ const gchar *problem = NULL;
+ gushort code;
+
+ if (self->closed)
+ return;
+
+ code = web_socket_connection_get_close_code (socket);
+ if (code == WEB_SOCKET_CLOSE_NORMAL)
+ {
+ cockpit_channel_control (channel, "done", NULL);
+ }
+ else
+ {
+ problem = web_socket_connection_get_close_data (socket);
+ if (problem == NULL)
+ problem = "disconnected";
+ }
+
+ cockpit_channel_close (channel, problem);
+}
+
+static void
+respond_with_error (CockpitWebRequest *request,
+ guint status,
+ const gchar *message)
+{
+ CockpitWebResponse *response;
+
+ response = cockpit_web_request_respond (request);
+ cockpit_web_response_error (response, status, NULL, "%s", message);
+ g_object_unref (response);
+}
+
+static void
+cockpit_channel_socket_class_init (CockpitChannelSocketClass *klass)
+{
+ CockpitChannelClass *channel_class = COCKPIT_CHANNEL_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = cockpit_channel_socket_finalize;
+
+ channel_class->recv = cockpit_channel_socket_recv;
+ channel_class->close = cockpit_channel_socket_close;
+}
+
+void
+cockpit_channel_socket_open (CockpitWebService *service,
+ JsonObject *open,
+ CockpitWebRequest *request)
+{
+ CockpitChannelSocket *self = NULL;
+ WebSocketDataType data_type;
+ CockpitTransport *transport;
+ g_autofree const gchar **protocols = NULL;
+ g_autofree gchar *id = NULL;
+
+ if (!cockpit_web_service_parse_external (open, NULL, NULL, NULL, &protocols) ||
+ !cockpit_web_service_parse_binary (open, &data_type))
+ {
+ respond_with_error (request, 400, "Bad channel request");
+ return;
+ }
+
+ transport = cockpit_web_service_get_transport (service);
+ if (!transport)
+ {
+ respond_with_error (request, 502, "Failed to open channel transport");
+ return;
+ }
+
+ json_object_set_boolean_member (open, "flow-control", TRUE);
+
+ id = cockpit_web_service_unique_channel (service);
+ self = g_object_new (COCKPIT_TYPE_CHANNEL_SOCKET,
+ "transport", transport,
+ "options", open,
+ "id", id,
+ NULL);
+
+ self->data_type = data_type;
+
+ self->socket = cockpit_web_service_create_socket (protocols, request);
+ self->socket_open = g_signal_connect (self->socket, "open", G_CALLBACK (on_socket_open), self);
+ self->socket_message = g_signal_connect (self->socket, "message", G_CALLBACK (on_socket_message), self);
+ self->socket_close = g_signal_connect (self->socket, "close", G_CALLBACK (on_socket_close), self);
+
+ /* Unref when the channel closes */
+ g_signal_connect_after (self, "closed", G_CALLBACK (g_object_unref), NULL);
+
+ /* Tell the channel to throttle based on back pressure from socket */
+ cockpit_flow_throttle (COCKPIT_FLOW (self), COCKPIT_FLOW (self->socket));
+
+ /* Tell the socket peer's output to throttle based on back pressure */
+ cockpit_flow_throttle (COCKPIT_FLOW (self->socket), COCKPIT_FLOW (self));
+}
diff --git a/src/ws/cockpitchannelsocket.h b/src/ws/cockpitchannelsocket.h
new file mode 100644
index 0000000..4ebc1dc
--- /dev/null
+++ b/src/ws/cockpitchannelsocket.h
@@ -0,0 +1,36 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_CHANNEL_SOCKET_H__
+#define __COCKPIT_CHANNEL_SOCKET_H__
+
+#include "common/cockpitwebserver.h"
+
+#include "cockpitwebservice.h"
+
+G_BEGIN_DECLS
+
+
+void cockpit_channel_socket_open (CockpitWebService *service,
+ JsonObject *open,
+ CockpitWebRequest *request);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_CHANNEL_SOCKET_H__ */
diff --git a/src/ws/cockpitcompat.c b/src/ws/cockpitcompat.c
new file mode 100644
index 0000000..f184b6a
--- /dev/null
+++ b/src/ws/cockpitcompat.c
@@ -0,0 +1,149 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitcompat.h"
+
+#include "common/cockpitauthorize.h"
+#include "common/cockpitmemory.h"
+
+#include <glib.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <crypt.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static void
+secfree (void *data,
+ ssize_t len)
+{
+ if (!data)
+ return;
+
+ cockpit_memory_clear (data, len);
+ g_free (data);
+}
+
+static ssize_t
+parse_salt (const char *input)
+{
+ const char *pos;
+ const char *end;
+
+ /*
+ * Parse a encrypted secret produced by crypt() using one
+ * of the additional algorithms. Return the length of
+ * the salt or -1.
+ */
+
+ if (input[0] != '$')
+ return -1;
+ pos = strchr (input + 1, '$');
+ if (pos == NULL || pos == input + 1)
+ return -1;
+ end = strchr (pos + 1, '$');
+ if (end == NULL || end < pos + 8)
+ return -1;
+
+ /* Full length of the salt */
+ return (end - input) + 1;
+}
+
+char *
+cockpit_compat_reply_crypt1 (const char *challenge,
+ const char *password)
+{
+ struct crypt_data *cd = NULL;
+ char *response = NULL;
+ char *nonce = NULL;
+ char *salt = NULL;
+ const char *npos;
+ const char *spos;
+ char *secret;
+ char *resp;
+ int errn = 0;
+
+ challenge = cockpit_authorize_subject (challenge, NULL);
+ if (!challenge)
+ return NULL;
+
+ npos = challenge;
+ spos = strchr (npos, ':');
+
+ if (spos == NULL)
+ {
+ g_message ("couldn't parse \"authorize\" message \"challenge\"");
+ errn = EINVAL;
+ goto out;
+ }
+
+ nonce = g_strndup (npos, spos - npos);
+ salt = g_strdup (spos + 1);
+
+ if (parse_salt (nonce) < 0 ||
+ parse_salt (salt) < 0)
+ {
+ g_message ("\"authorize\" message \"challenge\" has bad nonce or salt");
+ errn = EINVAL;
+ goto out;
+ }
+
+ cd = g_new0 (struct crypt_data, 2);
+
+ /*
+ * This is what we're generating here:
+ *
+ * response = "crypt1:" crypt(crypt(password, salt), nonce)
+ */
+
+ secret = crypt_r (password, salt, cd + 0);
+ if (secret == NULL)
+ {
+ errn = errno;
+ g_message ("couldn't hash password via crypt: %m");
+ goto out;
+ }
+
+ resp = crypt_r (secret, nonce, cd + 1);
+ if (resp == NULL)
+ {
+ errn = errno;
+ g_message ("couldn't hash secret via crypt: %m");
+ goto out;
+ }
+
+ response = g_strdup_printf ("crypt1:%s", resp);
+
+out:
+ g_free (nonce);
+ g_free (salt);
+ secfree (cd, sizeof (struct crypt_data) * 2);
+
+ if (!response)
+ errno = errn;
+
+ return response;
+}
diff --git a/src/ws/cockpitcompat.h b/src/ws/cockpitcompat.h
new file mode 100644
index 0000000..cdfef39
--- /dev/null
+++ b/src/ws/cockpitcompat.h
@@ -0,0 +1,26 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef COCKPIT_COMPAT_H__
+#define COCKPIT_COMPAT_H__
+
+char * cockpit_compat_reply_crypt1 (const char *challenge,
+ const char *password);
+
+#endif /* COCKPIT_COMPAT_H__ */
diff --git a/src/ws/cockpitcreds.c b/src/ws/cockpitcreds.c
new file mode 100644
index 0000000..e8854bf
--- /dev/null
+++ b/src/ws/cockpitcreds.c
@@ -0,0 +1,281 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitcreds.h"
+
+#include "common/cockpitmemory.h"
+#include "common/cockpitjson.h"
+
+#include <string.h>
+
+struct _CockpitCreds {
+ gint refs;
+ gint poisoned;
+ gchar *user;
+ gchar *application;
+ GBytes *password;
+ gchar *rhost;
+ gchar *csrf_token;
+ JsonObject *login_data;
+ GList *bytes;
+ gchar *superuser;
+};
+
+G_DEFINE_BOXED_TYPE (CockpitCreds, cockpit_creds, cockpit_creds_ref, cockpit_creds_unref);
+
+static void
+cockpit_creds_free (gpointer data)
+{
+ CockpitCreds *creds = data;
+
+ cockpit_creds_poison (creds);
+
+ g_list_free_full (creds->bytes, (GDestroyNotify)g_bytes_unref);
+
+ g_free (creds->user);
+ g_free (creds->application);
+ g_free (creds->rhost);
+ g_free (creds->csrf_token);
+ g_free (creds->superuser);
+
+ if (creds->login_data)
+ json_object_unref (creds->login_data);
+
+ g_free (creds);
+}
+
+/**
+ * cockpit_creds_new:
+ * @application: the application the creds are for
+ * @...: multiple credentials, followed by NULL
+ *
+ * Create a new set of credentials for a user. Each vararg should be
+ * a COCKPIT_CRED_PASSWORD, COCKPIT_CRED_RHOST, or similar constant
+ * followed by the value.
+ *
+ * COCKPIT_CRED_PASSWORD is a GBytes and should contain a null terminated
+ * string with the terminator not included in the count.
+ *
+ * Returns: (transfer full): the new set of credentials.
+ */
+CockpitCreds *
+cockpit_creds_new (const gchar *application,
+ ...)
+{
+ GBytes *password = NULL;
+ CockpitCreds *creds;
+ const char *type;
+ va_list va;
+
+ g_return_val_if_fail (application != NULL, NULL);
+ g_return_val_if_fail (!g_str_equal (application, ""), NULL);
+
+ creds = g_new0 (CockpitCreds, 1);
+ creds->application = g_strdup (application);
+ creds->login_data = NULL;
+
+ va_start (va, application);
+ for (;;)
+ {
+ type = va_arg (va, const char *);
+ if (type == NULL)
+ break;
+ else if (g_str_equal (type, COCKPIT_CRED_USER))
+ cockpit_creds_set_user (creds, va_arg (va, const char *));
+ else if (g_str_equal (type, COCKPIT_CRED_PASSWORD))
+ password = va_arg (va, GBytes *);
+ else if (g_str_equal (type, COCKPIT_CRED_RHOST))
+ creds->rhost = g_strdup (va_arg (va, const char *));
+ else if (g_str_equal (type, COCKPIT_CRED_CSRF_TOKEN))
+ creds->csrf_token = g_strdup (va_arg (va, const char *));
+ else if (g_str_equal (type, COCKPIT_CRED_SUPERUSER))
+ creds->superuser = g_strdup (va_arg (va, const char *));
+ else
+ g_assert_not_reached ();
+ }
+ va_end (va);
+
+ if (password)
+ cockpit_creds_set_password (creds, password);
+
+ creds->refs = 1;
+ creds->poisoned = 0;
+ return creds;
+}
+
+CockpitCreds *
+cockpit_creds_ref (CockpitCreds *creds)
+{
+ g_return_val_if_fail (creds != NULL, NULL);
+ g_atomic_int_inc (&creds->refs);
+ return creds;
+}
+
+void
+cockpit_creds_unref (gpointer creds)
+{
+ CockpitCreds *c = creds;
+ g_return_if_fail (creds != NULL);
+ if (g_atomic_int_dec_and_test (&c->refs))
+ cockpit_creds_free (c);
+}
+
+void
+cockpit_creds_poison (CockpitCreds *creds)
+{
+ g_return_if_fail (creds != NULL);
+ g_atomic_int_set (&creds->poisoned, 1);
+ cockpit_creds_set_password (creds, NULL);
+}
+
+const gchar *
+cockpit_creds_get_user (CockpitCreds *creds)
+{
+ g_return_val_if_fail (creds != NULL, NULL);
+ return creds->user;
+}
+
+void
+cockpit_creds_set_user (CockpitCreds *creds,
+ const gchar *user)
+{
+ g_return_if_fail (creds != NULL);
+ if (user != creds->user)
+ {
+ g_free (creds->user);
+ creds->user = g_strdup (user);
+ }
+}
+
+const gchar *
+cockpit_creds_get_application (CockpitCreds *creds)
+{
+ g_return_val_if_fail (creds != NULL, NULL);
+ return creds->application;
+}
+
+GBytes *
+cockpit_creds_get_password (CockpitCreds *creds)
+{
+ g_return_val_if_fail (creds != NULL, NULL);
+ if (g_atomic_int_get (&creds->poisoned))
+ return NULL;
+ return creds->password;
+}
+
+void
+cockpit_creds_set_password (CockpitCreds *creds,
+ GBytes *password)
+{
+ gpointer data;
+ gsize length;
+
+ g_return_if_fail (creds != NULL);
+
+ if (creds->password)
+ {
+ data = (gpointer)g_bytes_get_data (creds->password, &length);
+ cockpit_memory_clear (data, length);
+ creds->password = NULL;
+ }
+ if (password)
+ {
+ data = (gpointer)g_bytes_get_data (password, &length);
+ g_assert (((gchar *)data)[length] == '\0');
+ creds->password = g_bytes_ref (password);
+ creds->bytes = g_list_prepend (creds->bytes, creds->password);
+ }
+}
+
+const gchar *
+cockpit_creds_get_csrf_token (CockpitCreds *creds)
+{
+ g_return_val_if_fail (creds != NULL, NULL);
+ return creds->csrf_token;
+}
+
+const gchar *
+cockpit_creds_get_superuser (CockpitCreds *creds)
+{
+ g_return_val_if_fail (creds != NULL, NULL);
+ return creds->superuser;
+}
+
+/**
+ * cockpit_creds_get_login_data
+ * @creds: the credentials
+ *
+ * Get any login data, or NULL
+ * if none present.
+ *
+ * Returns: A JsonObject (transfer none) or NULL
+ */
+JsonObject *
+cockpit_creds_get_login_data (CockpitCreds *creds)
+{
+ g_return_val_if_fail (creds != NULL, NULL);
+ return creds->login_data;
+}
+
+void
+cockpit_creds_set_login_data (CockpitCreds *creds,
+ JsonObject *login_data)
+{
+ g_return_if_fail (creds != NULL);
+ if (login_data)
+ json_object_ref (login_data);
+ if (creds->login_data)
+ json_object_unref (creds->login_data);
+ creds->login_data = login_data;
+}
+
+
+/**
+ * cockpit_creds_get_rhost:
+ * @creds: the credentials
+ *
+ * Get the remote host credential, or NULL
+ * if none present.
+ *
+ * Returns: the remote host or NULL
+ */
+const gchar *
+cockpit_creds_get_rhost (CockpitCreds *creds)
+{
+ g_return_val_if_fail (creds != NULL, NULL);
+ return creds->rhost;
+}
+
+JsonObject *
+cockpit_creds_to_json (CockpitCreds *creds)
+{
+ JsonObject *object = NULL;
+ JsonObject *login_data = NULL;
+
+ object = json_object_new ();
+ json_object_set_string_member (object, "csrf-token", cockpit_creds_get_csrf_token (creds));
+
+ login_data = cockpit_creds_get_login_data (creds);
+ if (login_data)
+ json_object_set_object_member (object, "login-data", json_object_ref (login_data));
+
+ return object;
+}
diff --git a/src/ws/cockpitcreds.h b/src/ws/cockpitcreds.h
new file mode 100644
index 0000000..4d597c1
--- /dev/null
+++ b/src/ws/cockpitcreds.h
@@ -0,0 +1,80 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_CREDS_H__
+#define __COCKPIT_CREDS_H__
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <json-glib/json-glib.h>
+
+G_BEGIN_DECLS
+
+typedef struct _CockpitCreds CockpitCreds;
+
+#define COCKPIT_CRED_USER "user"
+#define COCKPIT_CRED_PASSWORD "password"
+#define COCKPIT_CRED_RHOST "rhost"
+#define COCKPIT_CRED_CSRF_TOKEN "csrf-token"
+#define COCKPIT_CRED_SUPERUSER "superuser"
+
+#define COCKPIT_TYPE_CREDS (cockpit_creds_get_type ())
+
+GType cockpit_creds_get_type (void) G_GNUC_CONST;
+
+CockpitCreds * cockpit_creds_new (const gchar *application,
+ ...) G_GNUC_NULL_TERMINATED;
+
+CockpitCreds * cockpit_creds_ref (CockpitCreds *creds);
+
+void cockpit_creds_unref (gpointer creds);
+
+void cockpit_creds_poison (CockpitCreds *creds);
+
+const gchar * cockpit_creds_get_user (CockpitCreds *creds);
+
+void cockpit_creds_set_user (CockpitCreds *creds,
+ const gchar *user);
+
+GBytes * cockpit_creds_get_password (CockpitCreds *creds);
+
+void cockpit_creds_set_password (CockpitCreds *creds,
+ GBytes *password);
+
+const gchar * cockpit_creds_get_rhost (CockpitCreds *creds);
+
+const gchar * cockpit_creds_get_csrf_token (CockpitCreds *creds);
+
+const gchar * cockpit_creds_get_superuser (CockpitCreds *creds);
+
+const gchar * cockpit_creds_get_application (CockpitCreds *creds);
+
+void cockpit_creds_set_login_data (CockpitCreds *creds,
+ JsonObject *login_data);
+
+JsonObject * cockpit_creds_get_login_data (CockpitCreds *creds);
+
+JsonObject * cockpit_creds_to_json (CockpitCreds *creds);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(CockpitCreds, cockpit_creds_unref)
+
+G_END_DECLS
+
+#endif
diff --git a/src/ws/cockpithandlers.c b/src/ws/cockpithandlers.c
new file mode 100644
index 0000000..c866e1c
--- /dev/null
+++ b/src/ws/cockpithandlers.c
@@ -0,0 +1,757 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpithandlers.h"
+
+#include "cockpitbranding.h"
+#include "cockpitchannelresponse.h"
+#include "cockpitchannelsocket.h"
+#include "cockpitwebservice.h"
+#include "cockpitws.h"
+
+#include "common/cockpitconf.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitwebcertificate.h"
+#include "common/cockpitwebinject.h"
+
+#include "websocket/websocket.h"
+
+#include <json-glib/json-glib.h>
+
+#include <gio/gio.h>
+
+#include <string.h>
+
+/* For overriding during tests */
+const gchar *cockpit_ws_shell_component = "/shell/index.html";
+
+static gchar *
+locate_selfsign_ca (void)
+{
+ g_autofree gchar *cert_path = NULL;
+ gchar *ca_path = NULL;
+ gchar *error = NULL;
+
+ cert_path = cockpit_certificate_locate (true, &error);
+ if (cert_path && g_str_has_suffix (cert_path, "/0-self-signed.cert"))
+ {
+ g_autofree gchar *dir = g_path_get_dirname (cert_path);
+ ca_path = g_build_filename (dir, "0-self-signed-ca.pem", NULL);
+ if (!g_file_test (ca_path, G_FILE_TEST_EXISTS))
+ {
+ g_free (ca_path);
+ ca_path = NULL;
+ }
+ }
+
+ return ca_path;
+}
+
+static void
+on_web_socket_noauth (WebSocketConnection *connection,
+ gpointer data)
+{
+ GBytes *payload;
+ GBytes *prefix;
+
+ g_debug ("closing unauthenticated web socket");
+
+ payload = cockpit_transport_build_control ("command", "init", "problem", "no-session", NULL);
+ prefix = g_bytes_new_static ("\n", 1);
+
+ web_socket_connection_send (connection, WEB_SOCKET_DATA_TEXT, prefix, payload);
+ web_socket_connection_close (connection, WEB_SOCKET_CLOSE_GOING_AWAY, "no-session");
+
+ g_bytes_unref (prefix);
+ g_bytes_unref (payload);
+}
+
+static void
+handle_noauth_socket (CockpitWebRequest *request)
+{
+ WebSocketConnection *connection;
+
+ connection = cockpit_web_service_create_socket (NULL, request);
+
+ g_signal_connect (connection, "open", G_CALLBACK (on_web_socket_noauth), NULL);
+
+ /* Unreferences connection when it closes */
+ g_signal_connect (connection, "close", G_CALLBACK (g_object_unref), NULL);
+}
+
+/* Called by @server when handling HTTP requests to /cockpit/socket */
+gboolean
+cockpit_handler_socket (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ CockpitHandlerData *ws)
+{
+ const gchar *path = cockpit_web_request_get_path (request);
+ const gchar *method = cockpit_web_request_get_method (request);
+ GHashTable *headers = cockpit_web_request_get_headers (request);
+
+ CockpitWebService *service = NULL;
+ const gchar *segment = NULL;
+
+ /*
+ * Socket requests should come in on /cockpit/socket or /cockpit+app/socket.
+ * However older javascript may connect on /socket, so we continue to support that.
+ */
+
+ if (path && path[0])
+ segment = strchr (path + 1, '/');
+ if (!segment)
+ segment = path;
+
+ if (!segment || !g_str_equal (segment, "/socket"))
+ return FALSE;
+
+ /* don't support HEAD on a socket, it makes little sense */
+ if (g_strcmp0 (method, "GET") != 0)
+ return FALSE;
+
+ if (headers && ws)
+ service = cockpit_auth_check_cookie (ws->auth, request);
+ if (service)
+ {
+ cockpit_web_service_socket (service, request);
+ }
+ else
+ {
+ handle_noauth_socket (request);
+ }
+
+ return TRUE;
+}
+
+gboolean
+cockpit_handler_external (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ CockpitHandlerData *ws)
+{
+ const gchar *path = cockpit_web_request_get_path (request);
+ GHashTable *headers = cockpit_web_request_get_headers (request);
+
+ CockpitWebResponse *response = NULL;
+ CockpitWebService *service = NULL;
+ const gchar *segment = NULL;
+ JsonObject *open = NULL;
+ CockpitCreds *creds;
+ const gchar *expected;
+ const gchar *upgrade;
+ guchar *decoded;
+ GBytes *bytes;
+ gsize length;
+
+ /* The path must start with /cockpit+xxx/channel/csrftoken? or similar */
+ if (path && path[0])
+ segment = strchr (path + 1, '/');
+ if (!segment)
+ return FALSE;
+ if (!g_str_has_prefix (segment, "/channel/"))
+ return FALSE;
+ segment += 9;
+
+ /* Make sure we are authenticated, otherwise 404 */
+ service = cockpit_auth_check_cookie (ws->auth, request);
+ if (!service)
+ return FALSE;
+
+ creds = cockpit_web_service_get_creds (service);
+ g_return_val_if_fail (creds != NULL, FALSE);
+
+ expected = cockpit_creds_get_csrf_token (creds);
+ g_return_val_if_fail (expected != NULL, FALSE);
+
+ /* No such path is valid */
+ if (!g_str_equal (segment, expected))
+ {
+ g_message ("invalid csrf token");
+ return FALSE;
+ }
+
+ decoded = g_base64_decode (cockpit_web_request_get_query (request), &length);
+ if (decoded)
+ {
+ bytes = g_bytes_new_take (decoded, length);
+ if (!cockpit_transport_parse_command (bytes, NULL, NULL, &open))
+ {
+ open = NULL;
+ g_message ("invalid external channel query");
+ }
+ g_bytes_unref (bytes);
+ }
+
+ if (!open)
+ {
+ response = cockpit_web_request_respond (request);
+ cockpit_web_response_error (response, 400, NULL, NULL);
+ g_object_unref (response);
+ }
+ else
+ {
+ upgrade = g_hash_table_lookup (headers, "Upgrade");
+ if (upgrade && g_ascii_strcasecmp (upgrade, "websocket") == 0)
+ {
+ cockpit_channel_socket_open (service, open, request);
+ }
+ else
+ {
+ cockpit_channel_response_open (service, request, open);
+ }
+ json_object_unref (open);
+ }
+
+ g_object_unref (service);
+
+ return TRUE;
+}
+
+
+static void
+add_oauth_to_environment (JsonObject *environment)
+{
+ static const gchar *url;
+ JsonObject *object;
+
+ url = cockpit_conf_string ("OAuth", "URL");
+
+ if (url)
+ {
+ object = json_object_new ();
+ json_object_set_string_member (object, "URL", url);
+ json_object_set_string_member (object, "ErrorParam",
+ cockpit_conf_string ("oauth", "ErrorParam"));
+ json_object_set_string_member (object, "TokenParam",
+ cockpit_conf_string ("oauth", "TokenParam"));
+ json_object_set_object_member (environment, "OAuth", object);
+ }
+}
+
+static void
+add_page_to_environment (JsonObject *object,
+ gboolean is_cockpit_client)
+{
+ static gint page_login_to = -1;
+ gboolean require_host = FALSE;
+ JsonObject *page;
+ const gchar *value;
+
+ page = json_object_new ();
+
+ value = cockpit_conf_string ("WebService", "LoginTitle");
+ if (value)
+ json_object_set_string_member (page, "title", value);
+
+ if (page_login_to < 0)
+ {
+ page_login_to = cockpit_conf_bool ("WebService", "LoginTo",
+ g_file_test (cockpit_ws_ssh_program,
+ G_FILE_TEST_IS_EXECUTABLE));
+ }
+
+ require_host = is_cockpit_client || cockpit_conf_bool ("WebService", "RequireHost", FALSE);
+
+ json_object_set_boolean_member (page, "connect", page_login_to);
+ json_object_set_boolean_member (page, "require_host", require_host);
+ json_object_set_object_member (object, "page", page);
+}
+
+static GBytes *
+build_environment (GHashTable *os_release)
+{
+ /*
+ * We don't include entirety of os-release into the
+ * environment for the login.html page. There could
+ * be unexpected things in here.
+ *
+ * However since we are displaying branding based on
+ * the OS name variant flavor and version, including
+ * the corresponding information is not a leak.
+ */
+ static const gchar *release_fields[] = {
+ "NAME", "ID", "PRETTY_NAME", "VARIANT", "VARIANT_ID", "CPE_NAME", "ID_LIKE", "DOCUMENTATION_URL"
+ };
+
+ static const gchar *prefix = "\n <script>\nvar environment = ";
+ static const gchar *suffix = ";\n </script>";
+
+ GByteArray *buffer;
+ GBytes *bytes;
+ JsonObject *object;
+ const gchar *value;
+ gchar *hostname;
+ JsonObject *osr;
+ gint i;
+
+ object = json_object_new ();
+
+ gboolean is_cockpit_client = cockpit_conf_bool ("WebService", "X-For-CockpitClient", FALSE);
+ json_object_set_boolean_member (object, "is_cockpit_client", is_cockpit_client);
+
+ add_page_to_environment (object, is_cockpit_client);
+
+ hostname = g_malloc0 (HOST_NAME_MAX + 1);
+ gethostname (hostname, HOST_NAME_MAX);
+ hostname[HOST_NAME_MAX] = '\0';
+ json_object_set_string_member (object, "hostname", hostname);
+ g_free (hostname);
+
+ if (os_release)
+ {
+ osr = json_object_new ();
+ for (i = 0; i < G_N_ELEMENTS (release_fields); i++)
+ {
+ value = g_hash_table_lookup (os_release, release_fields[i]);
+ if (value)
+ json_object_set_string_member (osr, release_fields[i], value);
+ }
+ json_object_set_object_member (object, "os-release", osr);
+ }
+
+ add_oauth_to_environment (object);
+
+ g_autofree gchar *ca_path = locate_selfsign_ca ();
+ if (ca_path)
+ json_object_set_string_member (object, "CACertUrl", "/ca.cer");
+
+ g_autofree gchar *contents = NULL;
+ g_autoptr(GError) error = NULL;
+ gsize len;
+
+ const gchar *banner = cockpit_conf_string ("Session", "Banner");
+ if (banner)
+ {
+ // TODO: parse macros (see `man agetty` for possible macros)
+ g_file_get_contents (banner, &contents, &len, &error);
+ if (error)
+ g_message ("error loading contents of banner: %s", error->message);
+ else
+ json_object_set_string_member (object, "banner", contents);
+ }
+
+ bytes = cockpit_json_write_bytes (object);
+ json_object_unref (object);
+
+ buffer = g_bytes_unref_to_array (bytes);
+ g_byte_array_prepend (buffer, (const guint8 *)prefix, strlen (prefix));
+ g_byte_array_append (buffer, (const guint8 *)suffix, strlen (suffix));
+ return g_byte_array_free_to_bytes (buffer);
+}
+
+static void
+send_login_html (CockpitWebResponse *response,
+ CockpitHandlerData *ws,
+ const gchar *path,
+ GHashTable *headers)
+{
+ static const gchar *marker = "<meta insert=\"dynamic_content_here\" />";
+ static const gchar *po_marker = "/*insert_translations_here*/";
+
+ CockpitWebFilter *filter;
+ GBytes *environment;
+ GError *error = NULL;
+ GBytes *bytes;
+
+ GBytes *url_bytes = NULL;
+ CockpitWebFilter *filter2 = NULL;
+ const gchar *url_root = NULL;
+ const gchar *accept = NULL;
+ gchar *content_security_policy = NULL;
+ gchar *cookie_line = NULL;
+ gchar *base;
+
+ gchar *language = NULL;
+ gchar **languages = NULL;
+ GBytes *po_bytes;
+ CockpitWebFilter *filter3 = NULL;
+
+ environment = build_environment (ws->os_release);
+ filter = cockpit_web_inject_new (marker, environment, 1);
+ g_bytes_unref (environment);
+ cockpit_web_response_add_filter (response, filter);
+ g_object_unref (filter);
+
+ url_root = cockpit_web_response_get_url_root (response);
+ if (url_root)
+ base = g_strdup_printf ("<base href=\"%s/\">", url_root);
+ else
+ base = g_strdup ("<base href=\"/\">");
+
+ url_bytes = g_bytes_new_take (base, strlen(base));
+ filter2 = cockpit_web_inject_new (marker, url_bytes, 1);
+ g_bytes_unref (url_bytes);
+ cockpit_web_response_add_filter (response, filter2);
+ g_object_unref (filter2);
+
+ cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_NO_CACHE);
+
+ if (ws->login_po_js)
+ {
+ language = cockpit_web_server_parse_cookie (headers, "CockpitLang");
+ if (!language)
+ {
+ accept = g_hash_table_lookup (headers, "Accept-Language");
+ languages = cockpit_web_server_parse_accept_list (accept, NULL);
+ language = languages[0];
+ }
+
+ po_bytes = cockpit_web_response_negotiation (ws->login_po_js, NULL, language, NULL, NULL, &error);
+ if (error)
+ {
+ g_message ("%s", error->message);
+ g_clear_error (&error);
+ }
+ else if (po_bytes)
+ {
+ filter3 = cockpit_web_inject_new (po_marker, po_bytes, 1);
+ g_bytes_unref (po_bytes);
+ cockpit_web_response_add_filter (response, filter3);
+ g_object_unref (filter3);
+ }
+ }
+
+ bytes = cockpit_web_response_negotiation (ws->login_html, NULL, NULL, NULL, NULL, &error);
+ if (error)
+ {
+ g_message ("%s", error->message);
+ cockpit_web_response_error (response, 500, NULL, NULL);
+ g_error_free (error);
+ }
+ else if (!bytes)
+ {
+ cockpit_web_response_error (response, 404, NULL, NULL);
+ }
+ else
+ {
+ /* The login Content-Security-Policy allows the page to have inline <script> and <style> tags. */
+ gboolean secure = g_strcmp0 (cockpit_web_response_get_protocol (response), "https") == 0;
+ cookie_line = cockpit_auth_empty_cookie_value (path, secure);
+ content_security_policy = cockpit_web_response_security_policy ("default-src 'self' 'unsafe-inline'",
+ cockpit_web_response_get_origin (response));
+
+ cockpit_web_response_headers (response, 200, "OK", -1,
+ "Content-Type", "text/html",
+ "Content-Security-Policy", content_security_policy,
+ "Set-Cookie", cookie_line,
+ NULL);
+ if (cockpit_web_response_queue (response, bytes))
+ cockpit_web_response_complete (response);
+
+ g_bytes_unref (bytes);
+ }
+
+ g_free (cookie_line);
+ g_free (content_security_policy);
+ g_strfreev (languages);
+}
+
+static void
+send_login_response (CockpitWebResponse *response,
+ JsonObject *object,
+ GHashTable *headers)
+{
+ GBytes *content;
+
+ content = cockpit_json_write_bytes (object);
+
+ g_hash_table_replace (headers, g_strdup ("Content-Type"), g_strdup ("application/json"));
+ cockpit_web_response_content (response, headers, content, NULL);
+ g_bytes_unref (content);
+}
+
+static void
+on_login_complete (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CockpitWebResponse *response = user_data;
+ GError *error = NULL;
+ JsonObject *response_data = NULL;
+ GHashTable *headers;
+ GIOStream *io_stream;
+ GBytes *content;
+
+ io_stream = cockpit_web_response_get_stream (response);
+
+ headers = cockpit_web_server_new_table ();
+ response_data = cockpit_auth_login_finish (COCKPIT_AUTH (object), result,
+ io_stream, headers, &error);
+
+ /* Never cache a login response */
+ cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_NO_CACHE);
+ if (error)
+ {
+ if (response_data)
+ {
+ g_hash_table_insert (headers, g_strdup ("Content-Type"), g_strdup ("application/json"));
+ content = cockpit_json_write_bytes (response_data);
+ cockpit_web_response_headers_full (response, 401, "Authentication required", -1, headers);
+ cockpit_web_response_queue (response, content);
+ cockpit_web_response_complete (response);
+ g_bytes_unref (content);
+ }
+ else
+ {
+ cockpit_web_response_gerror (response, headers, error);
+ }
+ g_error_free (error);
+ }
+ else
+ {
+ send_login_response (response, response_data, headers);
+ }
+
+ if (response_data)
+ json_object_unref (response_data);
+
+ g_hash_table_unref (headers);
+ g_object_unref (response);
+}
+
+static void
+handle_login (CockpitHandlerData *data,
+ CockpitWebService *service,
+ CockpitWebRequest *request,
+ CockpitWebResponse *response)
+{
+ GHashTable *out_headers;
+ CockpitCreds *creds;
+ JsonObject *creds_json = NULL;
+
+ if (service)
+ {
+ out_headers = cockpit_web_server_new_table ();
+ creds = cockpit_web_service_get_creds (service);
+ creds_json = cockpit_creds_to_json (creds);
+ send_login_response (response, creds_json, out_headers);
+ g_hash_table_unref (out_headers);
+ json_object_unref (creds_json);
+ return;
+ }
+
+ cockpit_auth_login_async (data->auth, request, on_login_complete, g_object_ref (response));
+}
+
+static void
+handle_resource (CockpitHandlerData *data,
+ CockpitWebService *service,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response)
+{
+ gchar *where;
+
+ where = cockpit_web_response_pop_path (response);
+ if (where && (where[0] == '@' || where[0] == '$') && where[1] != '\0')
+ {
+ if (service)
+ {
+ cockpit_channel_response_serve (service, headers, response, where,
+ cockpit_web_response_get_path (response));
+ }
+ else if (g_str_has_suffix (path, ".html"))
+ {
+ send_login_html (response, data, path, headers);
+ }
+ else
+ {
+ cockpit_web_response_error (response, 401, NULL, NULL);
+ }
+ }
+ else
+ {
+ cockpit_web_response_error (response, 404, NULL, NULL);
+ }
+
+ g_free (where);
+}
+
+static void
+handle_shell (CockpitHandlerData *data,
+ CockpitWebService *service,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response)
+{
+ gboolean valid;
+ const gchar *shell_path;
+
+ /* Check if a valid path for a shell to be served at */
+ valid = g_str_equal (path, "/") ||
+ g_str_has_prefix (path, "/@") ||
+ g_str_has_prefix (path, "/=") ||
+ strspn (path + 1, COCKPIT_RESOURCE_PACKAGE_VALID) == strcspn (path + 1, "/");
+
+ if (g_str_has_prefix (path, "/=/") ||
+ g_str_has_prefix (path, "/@/") ||
+ g_str_has_prefix (path, "//"))
+ {
+ valid = FALSE;
+ }
+
+ if (!valid)
+ {
+ cockpit_web_response_error (response, 404, NULL, NULL);
+ }
+ else if (service)
+ {
+ shell_path = cockpit_conf_string ("WebService", "Shell");
+ cockpit_channel_response_serve (service, headers, response, NULL,
+ shell_path ? shell_path : cockpit_ws_shell_component);
+ cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_NO_CACHE);
+ }
+ else
+ {
+ send_login_html (response, data, path, headers);
+ }
+}
+
+gboolean
+cockpit_handler_default (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitHandlerData *data)
+{
+ CockpitWebService *service;
+ const gchar *remainder = NULL;
+ gboolean resource;
+
+ path = cockpit_web_response_get_path (response);
+ g_return_val_if_fail (path != NULL, FALSE);
+
+ resource = g_str_has_prefix (path, "/cockpit/") ||
+ g_str_has_prefix (path, "/cockpit+") ||
+ g_str_equal (path, "/cockpit");
+
+ // Check for auth
+ service = cockpit_auth_check_cookie (data->auth, request);
+
+ /* Stuff in /cockpit or /cockpit+xxx */
+ if (resource)
+ {
+ g_assert (cockpit_web_response_skip_path (response));
+ remainder = cockpit_web_response_get_path (response);
+
+ if (!remainder)
+ {
+ cockpit_web_response_error (response, 404, NULL, NULL);
+ return TRUE;
+ }
+ else if (g_str_has_prefix (remainder, "/static/"))
+ {
+ cockpit_branding_serve (service, response, path, remainder + 8,
+ data->os_release, data->branding_roots);
+ return TRUE;
+ }
+ }
+
+ if (resource)
+ {
+ if (g_str_equal (remainder, "/login"))
+ {
+ handle_login (data, service, request, response);
+ }
+ else
+ {
+ handle_resource (data, service, path, headers, response);
+ }
+ }
+ else
+ {
+ handle_shell (data, service, path, headers, response);
+ }
+
+ return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+gboolean
+cockpit_handler_root (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitHandlerData *ws)
+{
+ /* Don't cache forever */
+ cockpit_web_response_file (response, path, ws->branding_roots);
+ return TRUE;
+}
+
+gboolean
+cockpit_handler_ping (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitHandlerData *ws)
+{
+ GHashTable *out_headers;
+ const gchar *body;
+ GBytes *content;
+
+ out_headers = cockpit_web_server_new_table ();
+
+ /*
+ * The /ping request has unrestricted CORS enabled on it. This allows javascript
+ * in the browser on embedding websites to check if Cockpit is available. These
+ * websites could do this in another way (such as loading an image from Cockpit)
+ * but this does it in the correct manner.
+ *
+ * See: http://www.w3.org/TR/cors/
+ */
+ g_hash_table_insert (out_headers, g_strdup ("Access-Control-Allow-Origin"), g_strdup ("*"));
+
+ g_hash_table_insert (out_headers, g_strdup ("Content-Type"), g_strdup ("application/json"));
+ body ="{ \"service\": \"cockpit\" }";
+ content = g_bytes_new_static (body, strlen (body));
+
+ cockpit_web_response_content (response, out_headers, content, NULL);
+
+ g_bytes_unref (content);
+ g_hash_table_unref (out_headers);
+
+ return TRUE;
+}
+
+gboolean
+cockpit_handler_ca_cert (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitHandlerData *ws)
+{
+ g_autofree gchar *ca_path = NULL;
+
+ ca_path = locate_selfsign_ca ();
+ if (ca_path == NULL) {
+ cockpit_web_response_error (response, 404, NULL, "CA certificate not found");
+ return TRUE;
+ }
+
+ const gchar *root_dir[] = { "/", NULL };
+ cockpit_web_response_file (response, ca_path, root_dir);
+ return TRUE;
+}
diff --git a/src/ws/cockpithandlers.h b/src/ws/cockpithandlers.h
new file mode 100644
index 0000000..cf3151f
--- /dev/null
+++ b/src/ws/cockpithandlers.h
@@ -0,0 +1,74 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_HANDLERS_H__
+#define __COCKPIT_HANDLERS_H__
+
+#include "cockpitauth.h"
+
+#include "common/cockpitwebserver.h"
+#include "common/cockpitwebresponse.h"
+
+extern const gchar *cockpit_ws_shell_component;
+
+typedef struct {
+ CockpitAuth *auth;
+ const gchar *login_html;
+ const gchar *login_po_js;
+ const gchar **branding_roots;
+ GHashTable *os_release;
+} CockpitHandlerData;
+
+gboolean cockpit_handler_socket (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ CockpitHandlerData *data);
+
+gboolean cockpit_handler_external (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ CockpitHandlerData *data);
+
+gboolean cockpit_handler_root (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitHandlerData *ws);
+
+gboolean cockpit_handler_default (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitHandlerData *ws);
+
+gboolean cockpit_handler_ping (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitHandlerData *ws);
+
+gboolean cockpit_handler_ca_cert (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ CockpitHandlerData *ws);
+
+#endif /* __COCKPIT_HANDLERS_H__ */
diff --git a/src/ws/cockpitwebservice.c b/src/ws/cockpitwebservice.c
new file mode 100644
index 0000000..b9c2536
--- /dev/null
+++ b/src/ws/cockpitwebservice.c
@@ -0,0 +1,1476 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013-2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitwebservice.h"
+
+#include "cockpitcompat.h"
+#include "cockpitws.h"
+
+#include <string.h>
+
+#include <json-glib/json-glib.h>
+#include <gio/gunixinputstream.h>
+#include <gio/gunixoutputstream.h>
+
+#include "common/cockpitauthorize.h"
+#include "common/cockpitconf.h"
+#include "common/cockpithex.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitmemory.h"
+#include "common/cockpitsystem.h"
+#include "common/cockpitwebresponse.h"
+#include "common/cockpitwebserver.h"
+
+#include "websocket/websocket.h"
+
+
+#include <stdlib.h>
+
+guint cockpit_ws_ping_interval = 5;
+
+/* ----------------------------------------------------------------------------
+ * Web Socket Info
+ */
+
+typedef struct {
+ gchar *id;
+ WebSocketConnection *connection;
+ GHashTable *channels;
+ JsonObject *init_received;
+} CockpitSocket;
+
+typedef struct {
+ GHashTable *by_channel;
+ GHashTable *by_connection;
+ guint next_socket_id;
+} CockpitSockets;
+
+static void
+cockpit_socket_free (gpointer data)
+{
+ CockpitSocket *socket = data;
+ g_hash_table_unref (socket->channels);
+ if (socket->init_received)
+ json_object_unref (socket->init_received);
+ g_object_unref (socket->connection);
+ g_free (socket->id);
+ g_free (socket);
+}
+
+static void
+cockpit_sockets_init (CockpitSockets *sockets)
+{
+ sockets->next_socket_id = 1;
+
+ sockets->by_channel = g_hash_table_new (g_str_hash, g_str_equal);
+
+ /* This owns the socket */
+ sockets->by_connection = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+ NULL, cockpit_socket_free);
+}
+
+inline static CockpitSocket *
+cockpit_socket_lookup_by_connection (CockpitSockets *sockets,
+ WebSocketConnection *connection)
+{
+ return g_hash_table_lookup (sockets->by_connection, connection);
+}
+
+inline static CockpitSocket *
+cockpit_socket_lookup_by_channel (CockpitSockets *sockets,
+ const gchar *channel)
+{
+ return g_hash_table_lookup (sockets->by_channel, channel);
+}
+
+static void
+cockpit_socket_remove_channel (CockpitSockets *sockets,
+ CockpitSocket *socket,
+ const gchar *channel)
+{
+ g_debug ("%s remove channel %s for socket", socket->id, channel);
+ g_hash_table_remove (sockets->by_channel, channel);
+ g_hash_table_remove (socket->channels, channel);
+}
+
+static void
+cockpit_socket_add_channel (CockpitSockets *sockets,
+ CockpitSocket *socket,
+ const gchar *channel,
+ WebSocketDataType data_type)
+{
+ gchar *chan;
+
+ chan = g_strdup (channel);
+ g_hash_table_insert (sockets->by_channel, chan, socket);
+ g_hash_table_replace (socket->channels, chan, GINT_TO_POINTER (data_type));
+
+ g_debug ("%s added channel %s to socket", socket->id, channel);
+}
+
+static CockpitSocket *
+cockpit_socket_track (CockpitSockets *sockets,
+ WebSocketConnection *connection)
+{
+ CockpitSocket *socket;
+
+ socket = g_new0 (CockpitSocket, 1);
+ socket->id = g_strdup_printf ("%u:", sockets->next_socket_id++);
+ socket->connection = g_object_ref (connection);
+ socket->channels = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ g_debug ("%s new socket", socket->id);
+
+ /* This owns the socket */
+ g_hash_table_insert (sockets->by_connection, connection, socket);
+
+ return socket;
+}
+
+static void
+cockpit_socket_destroy (CockpitSockets *sockets,
+ CockpitSocket *socket)
+{
+ GHashTableIter iter;
+ const gchar *chan;
+
+ g_debug ("%s destroy socket", socket->id);
+
+ g_hash_table_iter_init (&iter, socket->channels);
+ while (g_hash_table_iter_next (&iter, (gpointer *)&chan, NULL))
+ g_hash_table_remove (sockets->by_channel, chan);
+ g_hash_table_remove_all (socket->channels);
+
+ /* This owns the socket */
+ g_hash_table_remove (sockets->by_connection, socket->connection);
+}
+
+static void
+cockpit_sockets_close (CockpitSockets *sockets,
+ const gchar *problem)
+{
+ GHashTableIter iter;
+ CockpitSocket *socket;
+
+ if (!problem)
+ problem = "terminated";
+
+ g_hash_table_iter_init (&iter, sockets->by_connection);
+ while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&socket))
+ {
+ if (web_socket_connection_get_ready_state (socket->connection) < WEB_SOCKET_STATE_CLOSING)
+ web_socket_connection_close (socket->connection, WEB_SOCKET_CLOSE_GOING_AWAY, problem);
+ }
+}
+
+static void
+cockpit_sockets_cleanup (CockpitSockets *sockets)
+{
+ g_hash_table_destroy (sockets->by_connection);
+ g_hash_table_destroy (sockets->by_channel);
+}
+
+/* ----------------------------------------------------------------------------
+ * Web Socket Routing
+ */
+
+struct _CockpitWebService {
+ GObject parent;
+
+ gchar *id;
+
+ CockpitCreds *creds;
+ CockpitSockets sockets;
+ gboolean closing;
+ GBytes *control_prefix;
+ guint ping_timeout;
+ gint callers;
+ guint next_internal_id;
+
+ CockpitTransport *transport;
+ JsonObject *init_received;
+ gulong control_sig;
+ gulong recv_sig;
+ gulong closed_sig;
+ gboolean sent_done;
+ guint credentials_timeout;
+
+ GHashTable *checksum_by_host;
+ GHashTable *host_by_checksum;
+};
+
+typedef struct {
+ GObjectClass parent;
+} CockpitWebServiceClass;
+
+static guint sig_idling = 0;
+static guint sig_destroy = 0;
+
+G_DEFINE_TYPE (CockpitWebService, cockpit_web_service, G_TYPE_OBJECT);
+
+static void
+cockpit_web_service_dispose (GObject *object)
+{
+ CockpitWebService *self = COCKPIT_WEB_SERVICE (object);
+ gboolean emit = FALSE;
+
+ if (self->control_sig)
+ g_signal_handler_disconnect (self->transport, self->control_sig);
+ self->control_sig = 0;
+
+ if (self->recv_sig)
+ g_signal_handler_disconnect (self->transport, self->recv_sig);
+ self->recv_sig = 0;
+
+ if (self->closed_sig)
+ g_signal_handler_disconnect (self->transport, self->closed_sig);
+ self->closed_sig = 0;
+
+ if (self->credentials_timeout)
+ g_source_remove (self->credentials_timeout);
+ self->credentials_timeout = 0;
+
+ if (!self->sent_done)
+ {
+ self->sent_done = TRUE;
+ cockpit_transport_close (self->transport, NULL);
+ }
+
+ if (!self->closing)
+ {
+ g_debug ("web service closing");
+ emit = TRUE;
+ }
+ self->closing = TRUE;
+
+ cockpit_sockets_close (&self->sockets, NULL);
+
+ if (emit)
+ g_signal_emit (self, sig_destroy, 0);
+
+ G_OBJECT_CLASS (cockpit_web_service_parent_class)->dispose (object);
+}
+
+static void
+cockpit_web_service_finalize (GObject *object)
+{
+ CockpitWebService *self = COCKPIT_WEB_SERVICE (object);
+
+ cockpit_sockets_cleanup (&self->sockets);
+
+ if (self->transport)
+ g_object_unref (self->transport);
+ if (self->init_received)
+ json_object_unref (self->init_received);
+
+ g_bytes_unref (self->control_prefix);
+ cockpit_creds_unref (self->creds);
+ if (self->ping_timeout)
+ g_source_remove (self->ping_timeout);
+
+ g_hash_table_destroy (self->host_by_checksum);
+ g_hash_table_destroy (self->checksum_by_host);
+ g_free (self->id);
+
+ G_OBJECT_CLASS (cockpit_web_service_parent_class)->finalize (object);
+}
+
+gchar *
+cockpit_web_service_unique_channel (CockpitWebService *self)
+{
+ return g_strdup_printf ("0:%d", self->next_internal_id++);
+}
+
+static void
+caller_begin (CockpitWebService *self)
+{
+ g_object_ref (self);
+ self->callers++;
+}
+
+static void
+caller_end (CockpitWebService *self)
+{
+ g_return_if_fail (self->callers > 0);
+ self->callers--;
+ if (self->callers == 0)
+ g_signal_emit (self, sig_idling, 0);
+ g_object_unref (self);
+}
+
+static void
+outbound_protocol_error (CockpitWebService *self,
+ CockpitTransport *transport,
+ const gchar *problem)
+{
+ if (problem == NULL)
+ problem = "protocol-error";
+ cockpit_transport_close (transport, problem);
+}
+
+static gboolean
+process_close (CockpitWebService *self,
+ CockpitSocket *socket,
+ const gchar *channel)
+{
+ if (socket)
+ cockpit_socket_remove_channel (&self->sockets, socket, channel);
+
+ return TRUE;
+}
+
+static gboolean
+process_and_relay_close (CockpitWebService *self,
+ CockpitSocket *socket,
+ const gchar *channel,
+ GBytes *payload)
+{
+ gboolean valid;
+
+ valid = process_close (self, socket, channel);
+ if (valid && !self->sent_done)
+ cockpit_transport_send (self->transport, NULL, payload);
+
+ return valid;
+}
+
+static gboolean
+process_kill (CockpitWebService *self,
+ CockpitSocket *socket,
+ JsonObject *options,
+ GBytes *payload)
+{
+ if (!self->sent_done)
+ cockpit_transport_send (self->transport, NULL, payload);
+
+ return TRUE;
+}
+
+static gboolean
+process_ping (CockpitWebService *self,
+ CockpitSocket *socket,
+ JsonObject *options)
+{
+ GBytes *payload;
+
+ /* Respond to a ping without a channel, by saying "pong" */
+ json_object_set_string_member (options, "command", "pong");
+ payload = cockpit_json_write_bytes (options);
+ if (web_socket_connection_get_ready_state (socket->connection) == WEB_SOCKET_STATE_OPEN)
+ web_socket_connection_send (socket->connection, WEB_SOCKET_DATA_TEXT, self->control_prefix, payload);
+ g_bytes_unref (payload);
+
+ return TRUE;
+}
+
+static void
+clear_and_free_string (gpointer data)
+{
+ cockpit_memory_clear (data, -1);
+ free (data);
+}
+
+static gboolean
+process_socket_authorize (CockpitWebService *self,
+ CockpitSocket *socket,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload)
+{
+ const gchar *response = NULL;
+ gboolean ret = FALSE;
+ GBytes *bytes = NULL;
+ char *password = NULL;
+ char *user = NULL;
+ char *type = NULL;
+ gpointer data;
+ gsize length;
+
+ if (!cockpit_json_get_string (options, "response", NULL, &response))
+ {
+ g_warning ("%s: received invalid \"response\" field in authorize command", socket->id);
+ goto out;
+ }
+
+ ret = TRUE;
+ if (response)
+ {
+ if (!cockpit_authorize_type (response, &type) || !g_str_equal (type, "basic"))
+ goto out;
+
+ password = cockpit_authorize_parse_basic (response, &user);
+ if (password && !user)
+ {
+ cockpit_memory_clear (password, -1);
+ free (password);
+ password = NULL;
+ }
+ }
+ else
+ {
+ goto out;
+ }
+
+ if (password == NULL)
+ {
+ bytes = NULL;
+ }
+ else
+ {
+ bytes = g_bytes_new_with_free_func (password, strlen (password),
+ clear_and_free_string, password);
+ password = NULL;
+ }
+
+ cockpit_creds_set_user (self->creds, user);
+ cockpit_creds_set_password (self->creds, bytes);
+
+ /* Clear out the payload memory */
+ data = (gpointer)g_bytes_get_data (payload, &length);
+ cockpit_memory_clear (data, length);
+
+out:
+ free (type);
+ free (user);
+ if (bytes)
+ g_bytes_unref (bytes);
+ return ret;
+}
+
+static gboolean
+authorize_check_user (CockpitCreds *creds,
+ const char *challenge)
+{
+ char *subject = NULL;
+ gboolean ret = FALSE;
+ const gchar *user;
+
+ if (!cockpit_authorize_subject (challenge, &subject))
+ goto out;
+
+ if (!subject || g_str_equal (subject, ""))
+ {
+ ret = TRUE;
+ }
+ else
+ {
+ user = cockpit_creds_get_user (creds);
+ if (user == NULL)
+ {
+ ret = TRUE;
+ }
+ else
+ {
+ char *encoded = cockpit_hex_encode (user, -1);
+ ret = g_str_equal (encoded, subject);
+ free (encoded);
+
+ /* domain users are often case insensitive, while NSS/Linux converts them to the canonical lower-case form;
+ * accept the lower-case form of the creds user as well */
+ if (!ret)
+ {
+ gchar *user_lower = g_ascii_strdown (user, -1);
+ encoded = cockpit_hex_encode (user_lower, -1);
+ free (user_lower);
+ ret = g_str_equal (encoded, subject);
+ free (encoded);
+ }
+ }
+ }
+
+out:
+ free (subject);
+ return ret;
+}
+
+static gboolean
+process_transport_authorize (CockpitWebService *self,
+ CockpitTransport *transport,
+ JsonObject *options)
+{
+ const gchar *cookie = NULL;
+ GBytes *payload;
+ char *type = NULL;
+ char *alloc = NULL;
+ const char *response = NULL;
+ const gchar *challenge;
+ const gchar *password;
+ const gchar *host;
+ GBytes *data;
+
+ if (!cockpit_json_get_string (options, "challenge", NULL, &challenge) ||
+ !cockpit_json_get_string (options, "cookie", NULL, &cookie) ||
+ !cockpit_json_get_string (options, "host", NULL, &host))
+ {
+ g_warning ("received invalid authorize command");
+ return FALSE;
+ }
+
+ if (!challenge || !cookie)
+ {
+ g_message ("unsupported or unknown authorize command");
+ return FALSE;
+ }
+
+ if (!cockpit_authorize_type (challenge, &type))
+ {
+ g_message ("received invalid authorize challenge command");
+ }
+ else if (g_str_equal (type, "plain1") ||
+ g_str_equal (type, "crypt1") ||
+ g_str_equal (type, "basic"))
+ {
+ data = cockpit_creds_get_password (self->creds);
+ if (!data)
+ {
+ g_info ("%s: received \"authorize\" %s \"challenge\", but no password", host, type);
+ }
+ else if (!g_str_equal ("basic", type) && !authorize_check_user (self->creds, challenge))
+ {
+ g_info ("received \"authorize\" %s \"challenge\", but for wrong user", type);
+ }
+ else
+ {
+ password = g_bytes_get_data (data, NULL);
+ if (g_str_equal (type, "crypt1"))
+ {
+ alloc = cockpit_compat_reply_crypt1 (challenge, password);
+ if (alloc)
+ response = alloc;
+ else
+ g_message ("failed to \"authorize\" crypt1 \"challenge\"");
+ }
+ else if (g_str_equal (type, "basic"))
+ {
+ alloc = cockpit_authorize_build_basic (cockpit_creds_get_user (self->creds), password);
+ response = alloc;
+ }
+ else
+ {
+ response = password;
+ }
+ }
+ }
+
+ if (cookie && !self->sent_done)
+ {
+ payload = cockpit_transport_build_control ("command", "authorize",
+ "cookie", cookie,
+ "response", response ? response : "",
+ "host", host,
+ NULL);
+ cockpit_transport_send (transport, NULL, payload);
+ g_bytes_unref (payload);
+ }
+
+ free (type);
+ free (alloc);
+ return TRUE;
+}
+
+static gboolean
+poison_creds (gpointer user_data)
+{
+ CockpitWebService *self = user_data;
+ cockpit_creds_poison (self->creds);
+ self->credentials_timeout = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static const gchar *
+process_transport_init (CockpitWebService *self,
+ CockpitTransport *transport,
+ JsonObject *options)
+{
+ JsonObject *object;
+ JsonObject *capabilities;
+ gboolean explicit_superuser_capability = FALSE;
+ GBytes *payload;
+ gint64 version;
+
+ if (!cockpit_json_get_int (options, "version", -1, &version))
+ {
+ g_warning ("invalid version field in init message");
+ return "protocol-error";
+ }
+
+ if (version == 1)
+ {
+ g_debug ("received init message");
+ if (self->init_received)
+ json_object_unref (self->init_received);
+ self->init_received = json_object_ref (options);
+
+ if (cockpit_json_get_object (options, "capabilities", NULL, &capabilities) && capabilities)
+ {
+ if (!cockpit_json_get_bool (capabilities, "explicit-superuser", FALSE, &explicit_superuser_capability))
+ g_warning ("invalued 'explicit-superuser' value in init message");
+ }
+
+ /* If the bridge has the explicit-superuser capability, it will
+ send a "superuser-init-done" message once any authorization
+ is over. We will poisen our credentials at that time.
+
+ For a bridge without the explicit-superuser capability, we
+ keep the credentials for two minutes after receiving an
+ "init" message.
+ */
+
+ self->credentials_timeout = g_timeout_add (2*60*1000, poison_creds, self);
+
+ /* Always send an init message down the new transport */
+ object = cockpit_transport_build_json ("command", "init", NULL);
+ json_object_set_int_member (object, "version", 1);
+ json_object_set_string_member (object, "host", "localhost");
+
+ if (explicit_superuser_capability)
+ {
+ const gchar *superuser = getenv("COCKPIT_SUPERUSER") ?: cockpit_creds_get_superuser (self->creds);
+ if (superuser && *superuser && !g_str_equal (superuser, "none"))
+ {
+ JsonObject *superuser_options;
+
+ superuser_options = json_object_new ();
+ json_object_set_string_member (superuser_options, "id", superuser);
+ json_object_set_object_member (object, "superuser", superuser_options);
+ }
+ else
+ {
+ json_object_set_boolean_member (object, "superuser", FALSE);
+ cockpit_creds_poison (self->creds);
+ }
+ }
+
+ payload = cockpit_json_write_bytes (object);
+ json_object_unref (object);
+ cockpit_transport_send (transport, NULL, payload);
+ g_bytes_unref (payload);
+ }
+ else
+ {
+ g_message ("unsupported version of cockpit protocol: %" G_GINT64_FORMAT, version);
+ return "not-supported";
+ }
+
+ return NULL;
+}
+
+static gboolean
+on_transport_control (CockpitTransport *transport,
+ const gchar *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload,
+ gpointer user_data)
+{
+ const gchar *problem = "protocol-error";
+ CockpitWebService *self = user_data;
+ CockpitSocket *socket = NULL;
+ gboolean valid = FALSE;
+ gboolean forward;
+
+ if (!channel)
+ {
+ if (g_strcmp0 (command, "init") == 0)
+ {
+ problem = process_transport_init (self, transport, options);
+ valid = (problem == NULL);
+ }
+ else if (!self->init_received)
+ {
+ g_message ("bridge did not send 'init' message first");
+ valid = FALSE;
+ }
+ else if (g_strcmp0 (command, "authorize") == 0)
+ {
+ valid = process_transport_authorize (self, transport, options);
+ }
+ else if (g_strcmp0 (command, "superuser-init-done") == 0)
+ {
+ cockpit_creds_poison (self->creds);
+ valid = TRUE;
+ }
+ else
+ {
+ g_debug ("received a %s unknown control command", command);
+ valid = TRUE;
+ }
+ }
+ else
+ {
+ socket = cockpit_socket_lookup_by_channel (&self->sockets, channel);
+
+ /* Usually all control messages with a channel are forwarded */
+ forward = TRUE;
+
+ if (g_strcmp0 (command, "close") == 0)
+ {
+ valid = process_close (self, socket, channel);
+ }
+ else
+ {
+ valid = TRUE;
+ }
+
+ if (forward)
+ {
+ /* Forward this message to the right websocket */
+ if (socket && web_socket_connection_get_ready_state (socket->connection) == WEB_SOCKET_STATE_OPEN)
+ {
+ web_socket_connection_send (socket->connection, WEB_SOCKET_DATA_TEXT,
+ self->control_prefix, payload);
+ }
+ }
+ }
+
+ if (!valid)
+ {
+ outbound_protocol_error (self, transport, problem);
+ }
+
+ return TRUE; /* handled */
+}
+
+static gboolean
+on_transport_recv (CockpitTransport *transport,
+ const gchar *channel,
+ GBytes *payload,
+ gpointer user_data)
+{
+ CockpitWebService *self = user_data;
+ WebSocketDataType data_type;
+ CockpitSocket *socket;
+ gchar *string;
+ GBytes *prefix;
+
+ if (!channel)
+ return FALSE;
+
+ /* Forward the message to the right socket */
+ socket = cockpit_socket_lookup_by_channel (&self->sockets, channel);
+ if (socket && web_socket_connection_get_ready_state (socket->connection) == WEB_SOCKET_STATE_OPEN)
+ {
+ string = g_strdup_printf ("%s\n", channel);
+ prefix = g_bytes_new_take (string, strlen (string));
+ data_type = GPOINTER_TO_INT (g_hash_table_lookup (socket->channels, channel));
+ web_socket_connection_send (socket->connection, data_type, prefix, payload);
+ g_bytes_unref (prefix);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+on_transport_closed (CockpitTransport *transport,
+ const gchar *problem,
+ gpointer user_data)
+{
+ CockpitWebService *self = user_data;
+
+ /* Close all sockets */
+ cockpit_sockets_close (&self->sockets, problem);
+
+ /* Dispose web service */
+ g_object_run_dispose (G_OBJECT (self));
+}
+
+gboolean
+cockpit_web_service_parse_binary (JsonObject *options,
+ WebSocketDataType *data_type)
+{
+ const gchar *binary;
+
+ if (!cockpit_json_get_string (options, "binary", NULL, &binary))
+ {
+ g_warning ("invalid \"binary\" option");
+ return FALSE;
+ }
+
+ if (binary && g_str_equal (binary, "raw"))
+ *data_type = WEB_SOCKET_DATA_BINARY;
+ else
+ *data_type = WEB_SOCKET_DATA_TEXT;
+ return TRUE;
+}
+
+gboolean
+cockpit_web_service_parse_external (JsonObject *options,
+ const gchar **content_type,
+ const gchar **content_encoding,
+ const gchar **content_disposition,
+ const gchar ***protocols)
+{
+ JsonObject *external;
+ const gchar *value;
+ JsonNode *node;
+
+ g_return_val_if_fail (options != NULL, FALSE);
+
+ if (!cockpit_json_get_string (options, "channel", NULL, &value) || value != NULL)
+ {
+ g_message ("don't specify \"channel\" on external channel");
+ return FALSE;
+ }
+ if (!cockpit_json_get_string (options, "command", NULL, &value) || value != NULL)
+ {
+ g_message ("don't specify \"command\" on external channel");
+ return FALSE;
+ }
+
+ node = json_object_get_member (options, "external");
+ if (node == NULL)
+ {
+ if (content_disposition)
+ *content_disposition = NULL;
+ if (content_type)
+ *content_type = NULL;
+ if (content_encoding)
+ *content_encoding = NULL;
+ if (protocols)
+ *protocols = NULL;
+ return TRUE;
+ }
+
+ if (!JSON_NODE_HOLDS_OBJECT (node))
+ {
+ g_message ("invalid \"external\" option");
+ return FALSE;
+ }
+
+ external = json_node_get_object (node);
+
+ if (!cockpit_json_get_string (external, "content-disposition", NULL, &value) ||
+ (value && !cockpit_web_response_is_header_value (value)))
+ {
+ g_message ("invalid \"content-disposition\" external option");
+ return FALSE;
+ }
+ if (content_disposition)
+ *content_disposition = value;
+
+ if (!cockpit_json_get_string (external, "content-type", NULL, &value) ||
+ (value && !cockpit_web_response_is_header_value (value)))
+ {
+ g_message ("invalid \"content-type\" external option");
+ return FALSE;
+ }
+ if (content_type)
+ *content_type = value;
+
+ if (!cockpit_json_get_string (external, "content-encoding", NULL, &value) ||
+ (value && !cockpit_web_response_is_header_value (value)))
+ {
+ g_message ("invalid \"content-encoding\" external option");
+ return FALSE;
+ }
+ if (content_encoding)
+ *content_encoding = value;
+
+ if (!cockpit_json_get_strv (external, "protocols", NULL, protocols))
+ {
+ g_message ("invalid \"protocols\" external option");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+process_and_relay_open (CockpitWebService *self,
+ CockpitSocket *socket,
+ const gchar *channel,
+ JsonObject *options)
+{
+ WebSocketDataType data_type = WEB_SOCKET_DATA_TEXT;
+ GBytes *payload;
+
+ if (self->closing)
+ {
+ g_debug ("Ignoring open command while web socket is closing");
+ return TRUE;
+ }
+
+ if (channel == NULL)
+ {
+ g_warning ("open command is missing the 'channel' field");
+ return FALSE;
+ }
+
+ if (cockpit_socket_lookup_by_channel (&self->sockets, channel))
+ {
+ g_warning ("cannot open a channel %s with the same id as another channel", channel);
+ return FALSE;
+ }
+
+ if (!cockpit_web_service_parse_binary (options, &data_type))
+ return FALSE;
+
+ if (socket)
+ cockpit_socket_add_channel (&self->sockets, socket, channel, data_type);
+
+ if (!self->sent_done)
+ {
+ payload = cockpit_json_write_bytes (options);
+ cockpit_transport_send (self->transport, NULL, payload);
+ g_bytes_unref (payload);
+ }
+
+ return TRUE;
+}
+
+static void
+process_logout (CockpitWebService *self,
+ JsonObject *options)
+{
+ /* Makes the credentials unusable */
+ cockpit_creds_poison (self->creds);
+
+ /* Destroys our web service, disconnects everything */
+ g_info ("Logging out session %s", self->id);
+ g_object_run_dispose (G_OBJECT (self));
+}
+
+static const gchar *
+process_socket_init (CockpitWebService *self,
+ CockpitSocket *socket,
+ JsonObject *options)
+{
+ gint64 version;
+
+ if (!cockpit_json_get_int (options, "version", -1, &version))
+ {
+ g_warning ("invalid version field in init message");
+ return "protocol-error";
+ }
+
+ if (version == 1)
+ {
+ g_debug ("received web socket init message");
+ if (socket->init_received)
+ json_object_unref (socket->init_received);
+ socket->init_received = json_object_ref (options);
+ return NULL;
+ }
+ else
+ {
+ g_message ("web socket used unsupported version of cockpit protocol: %"
+ G_GINT64_FORMAT, version);
+ return "not-supported";
+ }
+}
+
+static void
+inbound_protocol_error (CockpitWebService *self,
+ WebSocketConnection *connection,
+ const gchar *problem)
+{
+ GBytes *payload;
+
+ if (problem == NULL)
+ problem = "protocol-error";
+
+ if (web_socket_connection_get_ready_state (connection) == WEB_SOCKET_STATE_OPEN)
+ {
+ payload = cockpit_transport_build_control ("command", "close", "problem", problem, NULL);
+ web_socket_connection_send (connection, WEB_SOCKET_DATA_TEXT, self->control_prefix, payload);
+ g_bytes_unref (payload);
+ web_socket_connection_close (connection, WEB_SOCKET_CLOSE_SERVER_ERROR, problem);
+ }
+}
+
+static void
+dispatch_inbound_command (CockpitWebService *self,
+ CockpitSocket *socket,
+ GBytes *payload)
+{
+ const gchar *problem = "protocol-error";
+ const gchar *command;
+ const gchar *channel;
+ JsonObject *options = NULL;
+ gboolean valid = FALSE;
+
+ valid = cockpit_transport_parse_command (payload, &command, &channel, &options);
+ if (!valid)
+ goto out;
+
+ if (g_strcmp0 (command, "init") == 0)
+ {
+ problem = process_socket_init (self, socket, options);
+ valid = (problem == NULL);
+ goto out;
+ }
+
+ if (!socket->init_received)
+ {
+ g_message ("web socket did not send 'init' message first");
+ valid = FALSE;
+ goto out;
+ }
+
+ valid = TRUE;
+
+ if (g_strcmp0 (command, "open") == 0)
+ {
+ valid = process_and_relay_open (self, socket, channel, options);
+ }
+ else if (g_strcmp0 (command, "authorize") == 0)
+ {
+ valid = process_socket_authorize (self, socket, channel, options, payload);
+ }
+ else if (g_strcmp0 (command, "logout") == 0)
+ {
+ process_logout (self, options);
+ }
+ else if (g_strcmp0 (command, "close") == 0)
+ {
+ if (channel == NULL)
+ {
+ g_warning ("got close command without a channel");
+ valid = FALSE;
+ }
+ else
+ {
+ valid = process_and_relay_close (self, socket, channel, payload);
+ }
+ }
+ else if (g_strcmp0 (command, "kill") == 0)
+ {
+ valid = process_kill (self, socket, options, payload);
+ }
+ else if (!channel && g_strcmp0 (command, "ping") == 0)
+ {
+ valid = process_ping (self, socket, options);
+ }
+ else if (channel)
+ {
+ /* Relay anything with a channel by default */
+ if (!self->sent_done)
+ cockpit_transport_send (self->transport, NULL, payload);
+ }
+
+out:
+ if (!valid)
+ inbound_protocol_error (self, socket->connection, problem);
+ if (options)
+ json_object_unref (options);
+}
+
+static void
+on_web_socket_message (WebSocketConnection *connection,
+ WebSocketDataType type,
+ GBytes *message,
+ CockpitWebService *self)
+{
+ CockpitSocket *socket;
+ g_autofree gchar *channel = NULL;
+
+ socket = cockpit_socket_lookup_by_connection (&self->sockets, connection);
+ g_return_if_fail (socket != NULL);
+
+ g_autoptr(GBytes) payload = cockpit_transport_parse_frame (message, &channel);
+ if (!payload)
+ return;
+
+ /* A control channel command */
+ if (!channel)
+ dispatch_inbound_command (self, socket, payload);
+
+ /* An actual payload message */
+ else if (!self->closing)
+ {
+ if (!self->sent_done)
+ cockpit_transport_send (self->transport, channel, payload);
+ }
+}
+
+static void
+on_web_socket_open (WebSocketConnection *connection,
+ CockpitWebService *self)
+{
+ CockpitSocket *socket;
+ JsonArray *capabilities;
+ GBytes *command;
+ JsonObject *object;
+ JsonObject *info;
+
+ if (cockpit_creds_get_rhost (self->creds))
+ g_info ("New connection to session %s from %s", self->id, cockpit_creds_get_rhost (self->creds));
+ else
+ g_info ("New connection to session %s", self->id);
+
+ socket = cockpit_socket_lookup_by_connection (&self->sockets, connection);
+ g_return_if_fail (socket != NULL);
+
+ object = json_object_new ();
+ json_object_set_string_member (object, "command", "init");
+ json_object_set_int_member (object, "version", 1);
+ json_object_set_string_member (object, "channel-seed", socket->id);
+ json_object_set_string_member (object, "host", "localhost");
+ json_object_set_string_member (object, "csrf-token", cockpit_creds_get_csrf_token (self->creds));
+
+ capabilities = json_array_new ();
+ json_array_add_string_element (capabilities, "multi");
+ json_array_add_string_element (capabilities, "credentials");
+ json_array_add_string_element (capabilities, "binary");
+ json_object_set_array_member (object, "capabilities", capabilities);
+
+ info = json_object_new ();
+ json_object_set_string_member (info, "version", PACKAGE_VERSION);
+ json_object_set_object_member (object, "system", info);
+
+ command = cockpit_json_write_bytes (object);
+ json_object_unref (object);
+
+ web_socket_connection_send (connection, WEB_SOCKET_DATA_TEXT, self->control_prefix, command);
+ g_bytes_unref (command);
+
+ g_signal_connect (connection, "message",
+ G_CALLBACK (on_web_socket_message), self);
+}
+
+static gboolean
+on_web_socket_closing (WebSocketConnection *connection,
+ CockpitWebService *self)
+{
+ CockpitSocket *socket;
+ GHashTable *snapshot;
+ GHashTableIter iter;
+ const gchar *channel;
+ GBytes *payload;
+
+ g_debug ("web socket closing");
+
+ if (self->sent_done)
+ return TRUE;
+
+ /* Close any channels that were opened by this web socket */
+ snapshot = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ socket = cockpit_socket_lookup_by_connection (&self->sockets, connection);
+ if (socket)
+ {
+ g_hash_table_iter_init (&iter, socket->channels);
+ while (g_hash_table_iter_next (&iter, (gpointer *)&channel, NULL))
+ {
+ g_hash_table_add (snapshot, g_strdup (channel));
+ }
+ }
+
+ g_hash_table_iter_init (&iter, snapshot);
+ while (g_hash_table_iter_next (&iter, (gpointer *)&channel, NULL))
+ {
+ payload = cockpit_transport_build_control ("command", "close",
+ "channel", channel,
+ "problem", "disconnected",
+ NULL);
+ cockpit_transport_send (self->transport, NULL, payload);
+ g_bytes_unref (payload);
+ }
+ g_hash_table_destroy (snapshot);
+
+ return TRUE;
+}
+
+static void
+on_web_socket_close (WebSocketConnection *connection,
+ CockpitWebService *self)
+{
+ CockpitSocket *socket;
+
+ if (cockpit_creds_get_rhost (self->creds))
+ g_info ("Connection from %s to session %s closed", cockpit_creds_get_rhost (self->creds), self->id);
+ else
+ g_info ("Connection to session %s closed", self->id);
+
+ g_signal_handlers_disconnect_by_func (connection, on_web_socket_open, self);
+ g_signal_handlers_disconnect_by_func (connection, on_web_socket_closing, self);
+ g_signal_handlers_disconnect_by_func (connection, on_web_socket_close, self);
+
+ socket = cockpit_socket_lookup_by_connection (&self->sockets, connection);
+ g_return_if_fail (socket != NULL);
+
+ cockpit_socket_destroy (&self->sockets, socket);
+
+ caller_end (self);
+}
+
+static gboolean
+on_ping_time (gpointer user_data)
+{
+ CockpitWebService *self = user_data;
+ WebSocketConnection *connection;
+ GHashTableIter iter;
+ GBytes *payload;
+
+ payload = cockpit_transport_build_control ("command", "ping", NULL);
+
+ g_hash_table_iter_init (&iter, self->sockets.by_connection);
+ while (g_hash_table_iter_next (&iter, (gpointer *)&connection, NULL))
+ {
+ if (web_socket_connection_get_ready_state (connection) == WEB_SOCKET_STATE_OPEN)
+ web_socket_connection_send (connection, WEB_SOCKET_DATA_TEXT, self->control_prefix, payload);
+ }
+
+ g_bytes_unref (payload);
+ return TRUE;
+}
+
+static void
+cockpit_web_service_init (CockpitWebService *self)
+{
+ self->control_prefix = g_bytes_new_static ("\n", 1);
+ cockpit_sockets_init (&self->sockets);
+ self->ping_timeout = g_timeout_add_seconds (cockpit_ws_ping_interval, on_ping_time, self);
+ self->host_by_checksum = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+ self->checksum_by_host = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+}
+
+static void
+cockpit_web_service_class_init (CockpitWebServiceClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = cockpit_web_service_dispose;
+ object_class->finalize = cockpit_web_service_finalize;
+
+ sig_idling = g_signal_new ("idling", COCKPIT_TYPE_WEB_SERVICE,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ sig_destroy = g_signal_new ("destroy", COCKPIT_TYPE_WEB_SERVICE,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+
+/**
+ * cockpit_web_service_new:
+ * @creds: credentials of user
+ * @transport: an new cockpit transport that has not yet
+ * sent an init message.
+ *
+ * Creates a new web service to serve web sockets and pass
+ * messages to the given bridge.
+ *
+ * Returns: (transfer full): the new web service
+ */
+CockpitWebService *
+cockpit_web_service_new (CockpitCreds *creds,
+ CockpitTransport *transport)
+{
+ CockpitWebService *self;
+
+ g_return_val_if_fail (creds != NULL, NULL);
+ g_return_val_if_fail (transport != NULL, NULL);
+
+ self = g_object_new (COCKPIT_TYPE_WEB_SERVICE, NULL);
+ self->creds = cockpit_creds_ref (creds);
+ self->transport = g_object_ref (transport);
+
+ self->control_sig = g_signal_connect_after (self->transport, "control", G_CALLBACK (on_transport_control), self);
+ self->recv_sig = g_signal_connect_after (self->transport, "recv", G_CALLBACK (on_transport_recv), self);
+ self->closed_sig = g_signal_connect_after (self->transport, "closed", G_CALLBACK (on_transport_closed), self);
+
+ return self;
+}
+
+WebSocketConnection *
+cockpit_web_service_create_socket (const gchar **protocols,
+ CockpitWebRequest *request)
+{
+ WebSocketConnection *connection;
+ const gchar * const *origins;
+ gchar *allocated = NULL;
+ gchar *origin = NULL;
+ gchar *defaults[2];
+ gboolean is_https;
+ gchar *url;
+
+ const gchar *host = cockpit_web_request_get_host (request);
+ const gchar *protocol = cockpit_web_request_get_protocol (request);
+ g_debug("cockpit_web_service_create_socket: host %s, protocol %s", host, protocol);
+ is_https = g_str_equal (protocol, "https") == 0;
+
+ url = g_strdup_printf ("%s://%s%s",
+ is_https ? "wss" : "ws",
+ host ? host : "localhost",
+ cockpit_web_request_get_path (request));
+
+ origins = cockpit_conf_strv ("WebService", "Origins", ' ');
+ if (origins == NULL)
+ {
+ origin = g_strdup_printf ("%s://%s", protocol, host);
+ defaults[0] = origin;
+ defaults[1] = NULL;
+ origins = (const gchar **)defaults;
+ }
+
+ connection = web_socket_server_new_for_stream (url, origins, protocols,
+ cockpit_web_request_get_io_stream (request),
+ cockpit_web_request_get_headers (request),
+ cockpit_web_request_get_buffer (request));
+ g_free (allocated);
+ g_free (url);
+ g_free (origin);
+
+ return connection;
+}
+
+/**
+ * cockpit_web_service_socket:
+ * @io_stream: the stream to talk on
+ * @headers: optional headers already parsed
+ * @input_buffer: optional bytes already parsed after headers
+ * @auth: authentication object
+ * @creds: credentials of user or NULL for failed auth
+ * @for_tls_proxy: Assume that the Browser is making TLS connections that are terminated
+ * in a reverse proxy in front of cockpit-ws
+ *
+ * Serves the WebSocket on the given web service. Holds an extra
+ * reference to the web service until the socket is closed.
+ */
+void
+cockpit_web_service_socket (CockpitWebService *self,
+ CockpitWebRequest *request)
+{
+ const gchar *protocols[] = { "cockpit1", NULL };
+ WebSocketConnection *connection;
+
+ connection = cockpit_web_service_create_socket (protocols, request);
+
+ g_signal_connect (connection, "open", G_CALLBACK (on_web_socket_open), self);
+ g_signal_connect (connection, "closing", G_CALLBACK (on_web_socket_closing), self);
+ g_signal_connect (connection, "close", G_CALLBACK (on_web_socket_close), self);
+
+ cockpit_socket_track (&self->sockets, connection);
+ g_object_unref (connection);
+
+ caller_begin (self);
+}
+
+
+
+/**
+ * cockpit_web_service_get_creds:
+ * @self: the service
+ *
+ * Returns: (transfer none): the credentials for which this service was opened.
+ */
+CockpitCreds *
+cockpit_web_service_get_creds (CockpitWebService *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_SERVICE (self), NULL);
+ return self->creds;
+}
+
+/**
+ * cockpit_web_service_get_id:
+ * @self: the service
+ *
+ * Returns: The id of this service, for logging.
+ */
+const gchar *
+cockpit_web_service_get_id (CockpitWebService *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_SERVICE (self), NULL);
+ return self->id;
+}
+
+/**
+ * cockpit_web_service_set_id:
+ * @self: the service
+ * @id: the id
+ */
+void
+cockpit_web_service_set_id (CockpitWebService *self,
+ const gchar *id)
+{
+ g_return_if_fail (COCKPIT_IS_WEB_SERVICE (self));
+ if (!self->id)
+ self->id = g_strdup (id);
+}
+
+/**
+ * cockpit_web_service_disconnect:
+ * @self: the service
+ *
+ * Close all sockets that are running in this web
+ * service.
+ */
+void
+cockpit_web_service_disconnect (CockpitWebService *self)
+{
+ g_object_run_dispose (G_OBJECT (self));
+}
+
+gboolean
+cockpit_web_service_get_idling (CockpitWebService *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_SERVICE (self), TRUE);
+ return (self->callers == 0);
+}
+
+CockpitTransport *
+cockpit_web_service_get_transport (CockpitWebService *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_SERVICE (self), NULL);
+ return self->transport;
+}
+
+JsonObject *
+cockpit_web_service_get_init (CockpitWebService *self)
+{
+ g_return_val_if_fail (COCKPIT_IS_WEB_SERVICE (self), NULL);
+ return self->init_received;
+}
+
+const gchar *
+cockpit_web_service_get_host (CockpitWebService *self,
+ const gchar *checksum)
+{
+ return g_hash_table_lookup (self->host_by_checksum, checksum);
+}
+
+const gchar *
+cockpit_web_service_get_checksum (CockpitWebService *self,
+ const gchar *host)
+{
+ return g_hash_table_lookup (self->checksum_by_host, host);
+}
+
+void
+cockpit_web_service_set_host_checksum (CockpitWebService *self,
+ const gchar *host,
+ const gchar *checksum)
+{
+ const gchar *old_checksum = g_hash_table_lookup (self->checksum_by_host, host);
+ const gchar *old_host = g_hash_table_lookup (self->host_by_checksum, checksum);
+
+ if (g_strcmp0 (checksum, old_checksum) == 0)
+ return;
+
+ if (old_checksum)
+ g_hash_table_remove (self->host_by_checksum, old_checksum);
+
+ /* Only replace checksum if the old one wasn't localhost */
+ if (g_strcmp0 (old_host, "localhost") != 0)
+ g_hash_table_replace (self->host_by_checksum, g_strdup (checksum), g_strdup (host));
+
+ g_hash_table_replace (self->checksum_by_host, g_strdup (host), g_strdup (checksum));
+}
diff --git a/src/ws/cockpitwebservice.h b/src/ws/cockpitwebservice.h
new file mode 100644
index 0000000..89d035b
--- /dev/null
+++ b/src/ws/cockpitwebservice.h
@@ -0,0 +1,87 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013-2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_WEB_SERVICE_H__
+#define __COCKPIT_WEB_SERVICE_H__
+
+#include "cockpitcreds.h"
+
+#include "common/cockpitjson.h"
+#include "common/cockpittransport.h"
+#include "common/cockpitwebresponse.h"
+#include "common/cockpitwebserver.h"
+
+#include "websocket/websocket.h"
+
+G_BEGIN_DECLS
+
+#define COCKPIT_TYPE_WEB_SERVICE (cockpit_web_service_get_type ())
+#define COCKPIT_WEB_SERVICE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), COCKPIT_TYPE_WEB_SERVICE, CockpitWebService))
+#define COCKPIT_IS_WEB_SERVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), COCKPIT_TYPE_WEB_SERVICE))
+
+typedef struct _CockpitWebService CockpitWebService;
+
+GType cockpit_web_service_get_type (void);
+
+CockpitWebService * cockpit_web_service_new (CockpitCreds *creds,
+ CockpitTransport *local_session);
+
+void cockpit_web_service_disconnect (CockpitWebService *self);
+
+void cockpit_web_service_socket (CockpitWebService *self,
+ CockpitWebRequest *request);
+
+CockpitCreds * cockpit_web_service_get_creds (CockpitWebService *self);
+const gchar * cockpit_web_service_get_id (CockpitWebService *self);
+void cockpit_web_service_set_id (CockpitWebService *self,
+ const gchar *id);
+
+gboolean cockpit_web_service_get_idling (CockpitWebService *self);
+
+WebSocketConnection * cockpit_web_service_create_socket (const gchar **protocols,
+ CockpitWebRequest *request);
+
+gchar * cockpit_web_service_unique_channel (CockpitWebService *self);
+
+CockpitTransport * cockpit_web_service_get_transport (CockpitWebService *self);
+
+JsonObject * cockpit_web_service_get_init (CockpitWebService *self);
+
+gboolean cockpit_web_service_parse_binary (JsonObject *open,
+ WebSocketDataType *type);
+
+gboolean cockpit_web_service_parse_external (JsonObject *open,
+ const gchar **content_type,
+ const gchar **content_encoding,
+ const gchar **content_disposition,
+ const gchar ***protocols);
+
+const gchar * cockpit_web_service_get_host (CockpitWebService *self,
+ const gchar *checksum);
+
+const gchar * cockpit_web_service_get_checksum (CockpitWebService *self,
+ const gchar *host);
+
+void cockpit_web_service_set_host_checksum (CockpitWebService *self,
+ const gchar *host,
+ const gchar *checksum);
+
+G_END_DECLS
+
+#endif /* __COCKPIT_WEB_SERVICE_H__ */
diff --git a/src/ws/cockpitws.h b/src/ws/cockpitws.h
new file mode 100644
index 0000000..f9fcb06
--- /dev/null
+++ b/src/ws/cockpitws.h
@@ -0,0 +1,45 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __COCKPIT_WS_H__
+#define __COCKPIT_WS_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+/* Some tunables that can be set from tests. */
+
+/* From cockpitwebsocket.c */
+extern const gchar *cockpit_ws_session_program;
+extern const gchar *cockpit_ws_ssh_program;
+extern const gchar *cockpit_ws_default_host_header;
+extern gint cockpit_ws_specific_ssh_port;
+extern guint cockpit_ws_ping_interval;
+extern gint cockpit_ws_session_timeout;
+extern guint cockpit_ws_auth_process_timeout;
+extern guint cockpit_ws_auth_response_timeout;
+
+/* From cockpitauth.c */
+extern guint cockpit_ws_service_idle;
+extern const gchar *cockpit_ws_max_startups;
+
+G_END_DECLS
+
+#endif /* __COCKPIT_WS_H__ */
diff --git a/src/ws/com.redhat.Cockpit.DBusTests.xml b/src/ws/com.redhat.Cockpit.DBusTests.xml
new file mode 100644
index 0000000..d2d63d6
--- /dev/null
+++ b/src/ws/com.redhat.Cockpit.DBusTests.xml
@@ -0,0 +1,180 @@
+<node>
+ <!--
+ This file is part of Cockpit.
+
+ Copyright (C) 2013 Red Hat, Inc.
+
+ Cockpit is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ Cockpit is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ -->
+
+ <interface name="com.redhat.Cockpit.DBusTests.Alpha">
+ </interface>
+
+ <interface name="com.redhat.Cockpit.DBusTests.Frobber">
+ <method name="HelloWorld">
+ <arg name="greeting" direction="in" type="s"/>
+ <arg name="response" direction="out" type="s"/>
+ </method>
+
+ <method name="NeverReturn">
+ </method>
+
+ <method name="TestPrimitiveTypes">
+ <arg direction="in" type="y" name="val_byte" />
+ <arg direction="in" type="b" name="val_boolean" />
+ <arg direction="in" type="n" name="val_int16" />
+ <arg direction="in" type="q" name="val_uint16" />
+ <arg direction="in" type="i" name="val_int32" />
+ <arg direction="in" type="u" name="val_uint32" />
+ <arg direction="in" type="x" name="val_int64" />
+ <arg direction="in" type="t" name="val_uint64" />
+ <arg direction="in" type="d" name="val_double" />
+ <arg direction="in" type="s" name="val_string" />
+ <arg direction="in" type="o" name="val_objpath" />
+ <arg direction="in" type="g" name="val_signature" />
+ <arg direction="in" type="ay" name="val_bytestring" />
+ <arg direction="out" type="y" name="ret_byte" />
+ <arg direction="out" type="b" name="ret_boolean" />
+ <arg direction="out" type="n" name="ret_int16" />
+ <arg direction="out" type="q" name="ret_uint16" />
+ <arg direction="out" type="i" name="ret_int32" />
+ <arg direction="out" type="u" name="ret_uint32" />
+ <arg direction="out" type="x" name="ret_int64" />
+ <arg direction="out" type="t" name="ret_uint64" />
+ <arg direction="out" type="d" name="ret_double" />
+ <arg direction="out" type="s" name="ret_string" />
+ <arg direction="out" type="o" name="ret_objpath" />
+ <arg direction="out" type="g" name="ret_signature" />
+ <arg direction="out" type="ay" name="ret_bytestring" />
+ </method>
+
+ <method name="TestNonPrimitiveTypes">
+ <arg direction="in" type="a{ss}" name="dict_s_to_s" />
+ <arg direction="in" type="a{s(ii)}" name="dict_s_to_pairs" />
+ <arg direction="in" type="(iss)" name="a_struct" />
+ <arg direction="in" type="as" name="array_of_strings" />
+ <arg direction="in" type="ao" name="array_of_objpaths" />
+ <arg direction="in" type="ag" name="array_of_signatures" />
+ <arg direction="in" type="aay" name="array_of_bytestrings" />
+ <arg direction="out" type="s" name="result" />
+ </method>
+
+ <method name="TestAsv">
+ <arg direction="in" type="a{sv}" name="asv" />
+ <arg direction="out" type="s" name="result" />
+ </method>
+
+ <method name="TestVariant">
+ <arg direction="in" type="v" name="v" />
+ </method>
+
+ <method name="RequestSignalEmission">
+ <arg direction="in" type="i" name="which_one" />
+ </method>
+
+ <method name="RequestPropertyMods"/>
+
+ <method name="RequestMultiPropertyMods"/>
+
+ <method name="UnimplementedMethod"/>
+
+ <method name="PropertyCancellation"/>
+
+ <signal name="TestSignal">
+ <arg type="i" name="val_int32"/>
+ <arg type="as" name="array_of_strings" />
+ <arg type="ao" name="array_of_objpaths" />
+ <arg type="a{s(ii)}" name="dict_s_to_pairs" />
+ </signal>
+
+ <signal name="AnotherSignal">
+ <arg type="s" name="word" />
+ </signal>
+
+ <method name="DeleteAllObjects"/>
+
+ <method name="CreateObject">
+ <arg direction="in" type="o" name="at_path" />
+ </method>
+
+ <method name="DeleteObject">
+ <arg direction="in" type="o" name="path" />
+ </method>
+
+ <method name="AddAlpha"/>
+ <method name="RemoveAlpha"/>
+
+ <method name="CreateClique">
+ <arg direction="in" type="s" name="name" />
+ <arg direction="out" type="o" name="member" />
+ </method>
+
+ <method name="EmitHidden">
+ <arg direction="in" type="s" name="name" />
+ </method>
+
+ <method name="ClaimOtherName">
+ <arg name="name" direction="in" type="s"/>
+ </method>
+
+ <method name="ReleaseOtherName">
+ <arg name="name" direction="in" type="s"/>
+ </method>
+
+ <method name="TellMeYourName">
+ <arg direction="out" name="name" type="s"/>
+ </method>
+
+ <method name="MakeTestFd">
+ <arg direction="in" name="type" type="s"/>
+ <arg direction="out" name="fd" type="h"/>
+ </method>
+
+ <property name="y" type="y" access="readwrite"/>
+ <property name="b" type="b" access="readwrite"/>
+ <property name="n" type="n" access="readwrite"/>
+ <property name="q" type="q" access="readwrite"/>
+ <property name="i" type="i" access="readwrite"/>
+ <property name="u" type="u" access="readwrite"/>
+ <property name="x" type="x" access="readwrite"/>
+ <property name="t" type="t" access="readwrite"/>
+ <property name="d" type="d" access="readwrite"/>
+ <property name="s" type="s" access="readwrite"/>
+ <property name="o" type="o" access="readwrite"/>
+ <property name="g" type="g" access="readwrite"/>
+ <property name="ay" type="ay" access="readwrite"/>
+ <property name="as" type="as" access="readwrite"/>
+ <property name="aay" type="aay" access="readwrite"/>
+ <property name="ao" type="ao" access="readwrite"/>
+ <property name="ag" type="ag" access="readwrite"/>
+ <property name="FinallyNormalName" type="s" access="readwrite"/>
+ <property name="ReadonlyProperty" type="s" access="read"/>
+ <property name="WriteonlyProperty" type="s" access="write"/>
+ </interface>
+
+ <interface name="com.redhat.Cockpit.DBusTests.Clique">
+ <property name="Friend" type="o" access="read"/>
+ </interface>
+
+ <interface name="com.redhat.Cockpit.DBusTests.Hidden">
+ <property name="Name" type="s" access="read"/>
+ </interface>
+
+</node>
+
+<!--
+ Local Variables:
+ indent-tabs-mode: nil
+ End:
+-->
diff --git a/src/ws/main.c b/src/ws/main.c
new file mode 100644
index 0000000..bef6a20
--- /dev/null
+++ b/src/ws/main.c
@@ -0,0 +1,313 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <glib-unix.h>
+#include <glib/gstdio.h>
+
+#include <dirent.h>
+#include <string.h>
+
+#include <systemd/sd-daemon.h>
+
+#include "cockpitws.h"
+
+#include "cockpithandlers.h"
+#include "cockpitbranding.h"
+
+#include "common/cockpitconf.h"
+#include "common/cockpithacks-glib.h"
+#include "common/cockpitmemory.h"
+#include "common/cockpitsystem.h"
+#include "common/cockpitwebcertificate.h"
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gint opt_port = 9090;
+static gchar *opt_address = NULL;
+static gboolean opt_no_tls = FALSE;
+static gboolean opt_for_tls_proxy = FALSE;
+static gboolean opt_local_ssh = FALSE;
+static gchar *opt_local_session = NULL;
+static gboolean opt_version = FALSE;
+
+static GOptionEntry cmd_entries[] = {
+ {"port", 'p', 0, G_OPTION_ARG_INT, &opt_port, "Local port to bind to (9090 if unset)", NULL},
+ {"address", 'a', 0, G_OPTION_ARG_STRING, &opt_address, "Address to bind to (binds on all addresses if unset)", "ADDRESS"},
+ {"no-tls", 0, 0, G_OPTION_ARG_NONE, &opt_no_tls, "Don't use TLS", NULL},
+ {"for-tls-proxy", 0, 0, G_OPTION_ARG_NONE, &opt_for_tls_proxy,
+ "Act behind a https-terminating proxy: accept only https:// origins by default",
+ NULL},
+ {"local-ssh", 0, 0, G_OPTION_ARG_NONE, &opt_local_ssh, "Log in locally via SSH", NULL },
+ {"local-session", 0, 0, G_OPTION_ARG_STRING, &opt_local_session,
+ "Launch a bridge in the local session (path to cockpit-bridge or '-' for stdin/out); implies --no-tls",
+ "BRIDGE" },
+ {"version", 0, 0, G_OPTION_ARG_NONE, &opt_version, "Print version information", NULL },
+ {NULL}
+};
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+print_version (void)
+{
+ g_print ("Version: %s\n", PACKAGE_VERSION);
+ g_print ("Protocol: 1\n");
+ g_print ("Authorization: crypt1\n");
+}
+
+static gchar **
+setup_static_roots (GHashTable *os_release)
+{
+ gchar **roots;
+ const gchar *os_variant_id;
+ const gchar *os_id;
+ const gchar *os_id_like;
+
+ if (os_release)
+ {
+ os_id = g_hash_table_lookup (os_release, "ID");
+ os_variant_id = g_hash_table_lookup (os_release, "VARIANT_ID");
+ os_id_like = g_hash_table_lookup (os_release, "ID_LIKE");
+ }
+ else
+ {
+ os_id = NULL;
+ os_variant_id = NULL;
+ os_id_like = NULL;
+ }
+
+ roots = cockpit_branding_calculate_static_roots (os_id, os_variant_id, os_id_like, TRUE);
+
+ return roots;
+}
+
+static void
+on_local_ready (GObject *object,
+ GAsyncResult *result,
+ gpointer data)
+{
+ cockpit_web_server_start (COCKPIT_WEB_SERVER (data));
+ g_object_unref (data);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gint ret = 1;
+ g_autoptr(GOptionContext) context = NULL;
+ g_autoptr(GTlsCertificate) certificate = NULL;
+ g_autoptr(GError) error = NULL;
+ g_auto(GStrv) roots = NULL;
+ g_autoptr(GMainLoop) loop = NULL;
+ g_autofree gchar *login_html = NULL;
+ g_autofree gchar *login_po_js = NULL;
+ g_autoptr(CockpitWebServer) server = NULL;
+ CockpitWebServerFlags server_flags = COCKPIT_WEB_SERVER_NONE;
+ CockpitHandlerData data;
+
+ signal (SIGPIPE, SIG_IGN);
+ cockpit_setenv_check ("GSETTINGS_BACKEND", "memory", TRUE);
+ cockpit_setenv_check ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE);
+ cockpit_setenv_check ("GIO_USE_VFS", "local", TRUE);
+
+ /* Any interaction with a krb5 ccache should be explicit */
+ cockpit_setenv_check ("KRB5CCNAME", "FILE:/dev/null", TRUE);
+
+ memset (&data, 0, sizeof (data));
+
+ context = g_option_context_new (NULL);
+ g_option_context_add_main_entries (context, cmd_entries, NULL);
+ if (!g_option_context_parse (context, &argc, &argv, &error))
+ goto out;
+
+ /* check mutually exclusive options */
+ if (opt_for_tls_proxy && opt_no_tls)
+ {
+ g_printerr ("--for-tls-proxy and --no-tls are mutually exclusive");
+ goto out;
+ }
+
+ if (opt_version)
+ {
+ print_version ();
+ ret = 0;
+ goto out;
+ }
+
+ if (opt_for_tls_proxy || cockpit_conf_bool ("WebService", "X-For-CockpitClient", FALSE))
+ opt_no_tls = TRUE;
+
+ cockpit_hacks_redirect_gdebug_to_stderr ();
+
+ if (opt_local_session || opt_no_tls)
+ {
+ /* no certificate */
+ }
+ else
+ {
+ g_autofree char *message = NULL;
+ g_autofree gchar *cert_path = cockpit_certificate_locate (false, &message);
+ if (cert_path == NULL)
+ {
+ g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, message);
+ goto out;
+ }
+
+ g_autofree gchar *key_path = cockpit_certificate_key_path (cert_path);
+
+ certificate = g_tls_certificate_new_from_files (cert_path, key_path, &error);
+ if (certificate == NULL)
+ goto out;
+ g_info ("Using certificate: %s", cert_path);
+ }
+
+ loop = g_main_loop_new (NULL, FALSE);
+
+ data.os_release = cockpit_system_load_os_release ();
+ data.auth = cockpit_auth_new (opt_local_ssh, opt_for_tls_proxy ? COCKPIT_AUTH_FOR_TLS_PROXY : COCKPIT_AUTH_NONE);
+ roots = setup_static_roots (data.os_release);
+
+ data.branding_roots = (const gchar **)roots;
+ login_html = g_strdup (DATADIR "/cockpit/static/login.html");
+ data.login_html = (const gchar *)login_html;
+ login_po_js = g_strdup (DATADIR "/cockpit/static/po.js");
+ data.login_po_js = (const gchar *)login_po_js;
+
+ if (opt_for_tls_proxy)
+ server_flags |= COCKPIT_WEB_SERVER_FOR_TLS_PROXY;
+ if (!cockpit_conf_bool ("WebService", "AllowUnencrypted", FALSE))
+ {
+ if (!opt_no_tls)
+ server_flags |= COCKPIT_WEB_SERVER_REDIRECT_TLS;
+ }
+
+ server = cockpit_web_server_new (certificate, server_flags);
+
+ const gint n_listen_fds = sd_listen_fds (true);
+ if (n_listen_fds)
+ {
+ for (gint fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + n_listen_fds; fd++)
+ if (!cockpit_web_server_add_fd_listener (server, fd, &error))
+ {
+ g_prefix_error (&error, "Unable to acquire LISTEN_FDS: ");
+ goto out;
+ }
+
+ g_signal_connect_swapped (data.auth, "idling", G_CALLBACK (g_main_loop_quit), loop);
+ }
+ else
+ {
+ if (!cockpit_web_server_add_inet_listener (server, opt_address, opt_port, &error))
+ {
+ g_prefix_error (&error, "Error starting web server: ");
+ goto out;
+ }
+ }
+
+ if (cockpit_conf_string ("WebService", "UrlRoot"))
+ {
+ g_object_set (server, "url-root",
+ cockpit_conf_string ("WebService", "UrlRoot"),
+ NULL);
+ }
+
+ cockpit_web_server_set_protocol_header (server, cockpit_conf_string ("WebService", "ProtocolHeader"));
+ cockpit_web_server_set_forwarded_for_header (server, cockpit_conf_string ("WebService", "ForwardedForHeader"));
+
+ /* Ignores stuff it shouldn't handle */
+ g_signal_connect (server, "handle-stream",
+ G_CALLBACK (cockpit_handler_socket), &data);
+
+ /* External channels, ignore stuff they shouldn't handle */
+ g_signal_connect (server, "handle-stream",
+ G_CALLBACK (cockpit_handler_external), &data);
+
+ /* Don't redirect to TLS for /ping */
+ g_object_set (server, "ssl-exception-prefix", "/ping", NULL);
+ g_signal_connect (server, "handle-resource::/ping",
+ G_CALLBACK (cockpit_handler_ping), &data);
+
+ /* Files that cannot be cache-forever, because of well known names */
+ g_signal_connect (server, "handle-resource::/favicon.ico",
+ G_CALLBACK (cockpit_handler_root), &data);
+ g_signal_connect (server, "handle-resource::/apple-touch-icon.png",
+ G_CALLBACK (cockpit_handler_root), &data);
+ g_signal_connect (server, "handle-resource::/ca.cer",
+ G_CALLBACK (cockpit_handler_ca_cert), &data);
+
+ /* The fallback handler for everything else */
+ g_signal_connect (server, "handle-resource",
+ G_CALLBACK (cockpit_handler_default), &data);
+
+ if (opt_local_session)
+ {
+ g_autoptr(CockpitPipe) pipe = NULL;
+ struct passwd *pwd;
+
+ if (g_str_equal (opt_local_session, "-"))
+ {
+ pipe = cockpit_pipe_new (opt_local_session, 0, 1);
+ }
+ else
+ {
+ g_auto(GStrv) args = NULL;
+ if (!g_shell_parse_argv (opt_local_session, NULL, &args, &error))
+ {
+ g_prefix_error (&error, "--local-session: ");
+ goto out;
+ }
+ pipe = cockpit_pipe_spawn ((const gchar **) args, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ }
+
+ /* Spawn a local session as a bridge */
+ pwd = getpwuid (geteuid ());
+ if (!pwd)
+ {
+ g_printerr ("Failed to resolve current user id %u\n", geteuid ());
+ goto out;
+ }
+ cockpit_auth_local_async (data.auth, pwd->pw_name, pipe, on_local_ready, g_object_ref (server));
+ }
+ else
+ {
+ /* When no local bridge, start serving immediately */
+ cockpit_web_server_start (server);
+ }
+
+ g_main_loop_run (loop);
+
+ ret = 0;
+
+out:
+ if (error)
+ g_printerr ("cockpit-ws: %s\n", error->message);
+ g_clear_object (&data.auth);
+ if (data.os_release)
+ g_hash_table_unref (data.os_release);
+ g_free (opt_address);
+ g_free (opt_local_session);
+ cockpit_conf_cleanup ();
+ return ret;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
diff --git a/src/ws/mock-auth-command.c b/src/ws/mock-auth-command.c
new file mode 100755
index 0000000..b8103b3
--- /dev/null
+++ b/src/ws/mock-auth-command.c
@@ -0,0 +1,329 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "common/cockpitauthorize.h"
+#include "common/cockpitframe.h"
+
+#include <security/pam_appl.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdio.h>
+
+#define DEBUG 0
+#define EX 127
+
+static const char *auth_prefix = "\n{\"command\":\"authorize\",\"cookie\":\"xxx\"";
+static const char *auth_suffix = "\"}";
+
+static char *
+read_authorize_response (void)
+{
+ const char *auth_response = ",\"response\":\"";
+ size_t auth_response_size = 13;
+ size_t auth_prefix_size = strlen (auth_prefix);
+ size_t auth_suffix_size = strlen (auth_suffix);
+ unsigned char *message;
+ ssize_t len;
+
+ len = cockpit_frame_read (STDIN_FILENO, &message);
+ if (len < 0)
+ err (EX, "couldn't read authorize response");
+
+#if DEBUG
+ fprintf (stderr, "mock-auth-command < %.*s\n", (int)len, message);
+#endif
+ /*
+ * The authorize messages we receive always have an exact prefix and suffix:
+ *
+ * \n{"command":"authorize","cookie":"NNN","response":"...."}
+ */
+ if (len <= auth_prefix_size + auth_response_size + auth_suffix_size ||
+ memcmp (message, auth_prefix, auth_prefix_size) != 0 ||
+ memcmp (message + auth_prefix_size, auth_response, auth_response_size) != 0 ||
+ memcmp (message + (len - auth_suffix_size), auth_suffix, auth_suffix_size) != 0)
+ {
+ errx (EX, "didn't receive expected \"authorize\" message: %.*s", (int)len, message);
+ }
+
+ len -= auth_prefix_size + auth_response_size + auth_suffix_size;
+ memmove (message, message + auth_prefix_size + auth_response_size, len);
+ message[len] = '\0';
+ return (char *)message;
+}
+
+static void
+write_authorize_challenge (const char *data)
+{
+ char *message = NULL;
+ if (asprintf (&message, "%s,\"challenge\":\"%s%s", auth_prefix, data, auth_suffix) < 0)
+ errx (EX, "out of memory writing string");
+#if DEBUG
+ fprintf (stderr, "mock-auth-command > %s\n", message);
+#endif
+ if (cockpit_frame_write (STDOUT_FILENO, (unsigned char *)message, strlen (message)) < 0)
+ err (EX, "couldn't write auth request");
+ free (message);
+}
+
+static void
+write_message (const char *message)
+{
+ if (cockpit_frame_write (STDOUT_FILENO, (unsigned char *)message, strlen (message)) < 0)
+ err (EX, "coludn't write message");
+}
+
+static void
+write_init_message (const char *data)
+{
+ char *message = NULL;
+ if (asprintf (&message, "\n{\"command\":\"init\",%s,\"version\":1}", data) < 0)
+ errx (EX, "out of memory writing string");
+#if DEBUG
+ fprintf (stderr, "mock-auth-command > %s\n", message);
+#endif
+ write_message (message);
+ free (message);
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ int success = 0;
+ int launch_bridge = 0;
+ char *message;
+ const char *data = NULL;
+ char *type;
+
+ write_authorize_challenge ("*");
+
+ message = read_authorize_response ();
+ data = cockpit_authorize_type (message, &type);
+ assert (data != NULL);
+
+ if (strcmp (data, "") == 0)
+ {
+ write_init_message ("\"problem\":\"authentication-failed\"");
+ }
+ if (strcmp (data, "no-cookie") == 0)
+ {
+ write_message ("\n{\"command\":\"authorize\",\"response\": \"user me\"}");
+ free (message);
+ write_authorize_challenge ("*");
+ message = read_authorize_response ();
+ if (!message || strcmp (message, "user me") != 0)
+ {
+ write_init_message ("\"problem\": \"authentication-failed\"");
+ }
+ else
+ {
+ write_init_message ("\"user\": \"me\"");
+ success = 1;
+ }
+ }
+ else if (strcmp (data, "failslow") == 0)
+ {
+ sleep (2);
+ write_init_message ("\"problem\":\"authentication-failed\"");
+ }
+ else if (strcmp (data, "fail") == 0)
+ {
+ write_init_message ("\"problem\":\"authentication-failed\"");
+ }
+ else if (strcmp (data, "not-supported") == 0)
+ {
+ write_init_message ("\"problem\": \"authentication-not-supported\", \"auth-method-results\": {}");
+ }
+ else if (strcmp (data, "ssh-fail") == 0)
+ {
+ write_init_message ("\"problem\": \"authentication-failed\", \"auth-method-results\": { \"password\": \"denied\"}");
+ }
+ else if (strcmp (data, "denied") == 0)
+ {
+ write_init_message ("\"problem\": \"access-denied\"");
+ }
+ else if (strcmp (data, "success") == 0)
+ {
+ write_init_message ("\"user\": \"me\"");
+ success = 1;
+ }
+ else if (strcmp (data, "ssh-remote-switch") == 0 &&
+ strcmp (argv[1], "machine") == 0)
+ {
+ write_init_message ("\"user\": \"me\"");
+ success = 1;
+ }
+ else if (strcmp (data, "ssh-alt-machine") == 0 &&
+ strcmp (argv[1], "machine") == 0)
+ {
+ write_init_message ("\"user\": \"me\"");
+ success = 1;
+ }
+ else if (strcmp (data, "ssh-alt-default") == 0 &&
+ strcmp (argv[1], "default-host") == 0)
+ {
+ write_init_message ("\"user\": \"me\"");
+ success = 1;
+ }
+ else if (type && strcmp (type, "basic") == 0 &&
+ strcmp (argv[1], "127.0.0.1") == 0)
+ {
+ if (strcmp (data, "bWU6dGhpcyBpcyB0aGUgcGFzc3dvcmQ=") == 0)
+ {
+ write_init_message ("\"user\": \"me\"");
+ success = 1;
+ }
+ else if (strcmp (data, "YnJpZGdlLXVzZXI6dGhpcyBpcyB0aGUgcGFzc3dvcmQ=") == 0)
+ {
+ launch_bridge = 1;
+ success = 1;
+ }
+ else
+ {
+ write_init_message ("\"problem\": \"authentication-failed\", \"auth-method-results\": { \"password\": \"denied\"}");
+ }
+ }
+ else if (type && strcmp (type, "basic") == 0 &&
+ strcmp (argv[1], "machine") == 0)
+ {
+ if (strcmp (data, "cmVtb3RlLXVzZXI6dGhpcyBpcyB0aGUgbWFjaGluZSBwYXNzd29yZA==") == 0)
+ {
+ write_init_message ("\"user\": \"remote-user\"");
+ success = 1;
+ }
+ else if (strcmp (data, "YnJpZGdlLXVzZXI6dGhpcyBpcyB0aGUgcGFzc3dvcmQ=") == 0)
+ {
+ launch_bridge = 1;
+ success = 1;
+ }
+ else
+ {
+ write_init_message ("\"problem\": \"authentication-failed\", \"auth-method-results\": { \"password\": \"denied\"}");
+ }
+ }
+ else if (type && strcmp (type, "basic") == 0)
+ {
+ if (strcmp (data, "bWU6dGhpcyBpcyB0aGUgcGFzc3dvcmQ=") == 0)
+ {
+ write_init_message ("\"user\": \"me\"");
+ success = 1;
+ }
+ else if (strcmp (data, "YnJpZGdlLXVzZXI6dGhpcyBpcyB0aGUgcGFzc3dvcmQ=") == 0)
+ {
+ launch_bridge = 1;
+ success = 1;
+ }
+ else
+ {
+ write_init_message ("\"problem\": \"authentication-failed\"");
+ }
+ }
+ else if (strcmp (data, "data-then-success") == 0)
+ {
+ write_message ("\n{\"command\":\"authorize\",\"challenge\":\"x-login-data\",\"cookie\":\"blah\",\"login-data\":{ \"login\": \"data\"}}");
+ write_init_message ("\"user\": \"me\"");
+ success = 1;
+ }
+ else if (strcmp (data, "two-step") == 0)
+ {
+ write_authorize_challenge ("X-Conversation conv dHlwZSB0d28=");
+ free (message);
+ message = read_authorize_response ();
+ data = cockpit_authorize_type (message, NULL);
+ if (!data || strcmp (data, "conv dHdv") != 0)
+ {
+ write_init_message ("\"problem\": \"authentication-failed\"");
+ }
+ else
+ {
+ write_init_message ("\"user\": \"me\"");
+ success = 1;
+ }
+ }
+ else if (strcmp (data, "three-step") == 0)
+ {
+ write_authorize_challenge ("X-Conversation conv dHlwZSB0d28=");
+ free (message);
+ message = read_authorize_response ();
+ data = cockpit_authorize_type (message, NULL);
+ if (!data || strcmp (data, "conv dHdv") != 0)
+ {
+ write_init_message ("\"problem\": \"authentication-failed\"");
+ goto out;
+ }
+
+ write_authorize_challenge ("X-Conversation conv dHlwZSB0aHJlZQ==");
+ free (message);
+ message = read_authorize_response ();
+ data = cockpit_authorize_type (message, NULL);
+ if (!data || strcmp (data, "conv dGhyZWU=") != 0)
+ {
+ write_init_message ("\"problem\": \"authentication-failed\"");
+ }
+ else
+ {
+ write_init_message ("\"user\": \"me\"");
+ success = 1;
+ }
+ }
+ else if (strcmp (data, "success-bad-data") == 0)
+ {
+ write_init_message ("\"user\": \"me\", \"login-data\": \"bad\"");
+ success = 1;
+ }
+ else if (strcmp (data, "no-user") == 0)
+ {
+ write_init_message ("\"other\":1");
+ }
+ else if (strcmp (data, "with-error") == 0)
+ {
+ write_init_message ("\"problem\": \"unknown\", \"message\": \"detail for error\"");
+ }
+ else if (strcmp (data, "with-error") == 0)
+ {
+ write_init_message ("\"problem\": \"unknown\", \"message\": \"detail for error\"");
+ }
+ else if (strcmp (data, "too-slow") == 0)
+ {
+ sleep (10);
+ write_init_message ("\"user\": \"me\", \"login-data\": { \"login\": \"data\"}");
+ success = 1;
+ }
+
+out:
+ free (message);
+ if (success)
+ {
+ if (launch_bridge)
+ execlp (BUILDDIR "/cockpit-bridge", BUILDDIR "/cockpit-bridge", NULL);
+ else
+ execlp ("cat", "cat", NULL);
+ }
+ exit (PAM_AUTH_ERR);
+}
diff --git a/src/ws/mock-cat-with-init b/src/ws/mock-cat-with-init
new file mode 100755
index 0000000..e29195a
--- /dev/null
+++ b/src/ws/mock-cat-with-init
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+# Here we send a init message the exec cat
+
+/usr/bin/printf "37\n\n{ \"command\" : \"init\", \"version\": 1 }"
+exec /bin/cat
diff --git a/src/ws/mock-combined.crt b/src/ws/mock-combined.crt
new file mode 100644
index 0000000..c2146f8
--- /dev/null
+++ b/src/ws/mock-combined.crt
@@ -0,0 +1,54 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzuSes+7ghYHT+
+jrcAjMVF3jaGn+OcVGTSuxVzNY5zdqDTLt8GL24hhrxAJXy2SvKwye1L0Wjkcx6a
+8v4FagvNCzzzWaKBbTwwf4GRF1t5IkMo+7HBr5lYYN4Hi1aUj9pauczZf8sG2H1M
+lqorSwnPn4K9oSPjzs5FW7PxpbWi3HQqFib4OS00WP68/J5Au+slUC0HZkSTrSS0
+yvtOGu/cVWi0ARKiGbJKcw3kxWhMPZzn2R9rC+bvwrTB69KCeQJ43RowOh7FnnxW
+bnf432sxyWiTzURkmpU2eJUUW3/34HyACIezIzSZcTqwRENqZVVTQ8+34jLB3Sd+
+A20CpuPlAgMBAAECggEAEx9HLmuV+s8lg5Qxung/4SA0kvxeHlsIYzeOn7R6FKTx
+laauCh1vzVdlnjme3FJ7If5KJkAAthXdksT57B+NY+KnOIk39idmOQ7H/WUL0Yao
+uKqXDjGd0j50QT89Fg98Pxz3w8iKFI9iPYzmTDbus02Yfo6vn+R4I/8O1U45VjuK
+sG9Y4QlZNVWFvqrZP3AWNxX46TdKVQoPV1kFJUjFw/XplfwrYX6b2hXk5yEFFqrr
+k1Wk7g0RScAb79KFSQx7bnmnLpCc5eVuiV9G12E9rzCNZz3zcOPV4nVGapTzmpbb
+1z4eKhYT4l4oRwjlldgIEY1QkuCOZ3cTyqu7A5G5HQKBgQDsWumtY2QsZXfQy63c
+ckpkUTsV+mmdlP+vC6w4av1AJirAv7qj9TcDi7g6KlBEoczv7QWb9AbR/44y3QPp
+JvfreS8RAIYQoC5b1yN/bKLa5rmgHBqCQSemZ25Ov46iXKn4U9kJu8vJmMNyrGcn
+HMFCQ0PmY8T+f9Xn54JRTqdEuwKBgQDCqT9xrIpR4dVa31VLixAQ+SfZu6j/+aIs
+u8s4k/c1k73a886NAzwjuVeg/XYyLcyPO53dt3zv+ZbMt8TPV/0D8j1gTGI92sBn
+B7mwh4jHDaHORpmVuLbJbJHltTH3brMkKg7ECzsmaAZF0ths3Hf8PgYTGdiA818O
+eaDzMpg/3wKBgQDmevEMZ7Ozp27jaPst0TXCBYR8tihvxnBenh9fFNXIzG88brru
+T51KE+Getsx3YOCbxqKFpb1Q4sgjyWqa3ak9Mns3NS+G3uzWGkE/bQySMhQpkb43
+7plfp9Tlbd8FbR9CTWGEIGFlQVa2aHBer6e1miqIbBoZo0dQUW2/I9fxKQKBgQCd
+3mDjscijy4Eap5EnKOqouUhGXtkab7vEh7Cye927B8yra5sR5ZO6bS/SEnzvCRGi
+//hxvcOHY2WXNVpZxcQelTCNjqUGAQiVBkDkF6cnt7iH793S/PDQvko8fwy83HxQ
+UgOTnoAkD602p156OvrcCPkoOIfLXJS0ypNraJbtBQKBgCSlbHUl+33Jbj9wTTdB
+0p60+y1uMqvv/59EdMcJXcF0eo5yHARjeej0I8mNlw3W23iTNu0AR09F4dun/W1+
+Za+vQVaV86XM2pXwHYGsQ1EzndV3gLXtGhHBY2EhoPHJQEtSVjYTJyT+prsO+wGE
+uoryjE5mt8EfzDiSo8BXoHBY
+-----END PRIVATE KEY-----
+
+-----BEGIN CERTIFICATE-----
+MIIESzCCAzOgAwIBAgIJANMFguOfgTQhMA0GCSqGSIb3DQEBBQUAMIG7MQswCQYD
+VQQGEwItLTESMBAGA1UECAwJU29tZVN0YXRlMREwDwYDVQQHDAhTb21lQ2l0eTEZ
+MBcGA1UECgwQU29tZU9yZ2FuaXphdGlvbjEfMB0GA1UECwwWU29tZU9yZ2FuaXph
+dGlvbmFsVW5pdDEeMBwGA1UEAwwVbG9jYWxob3N0LmxvY2FsZG9tYWluMSkwJwYJ
+KoZIhvcNAQkBFhpyb290QGxvY2FsaG9zdC5sb2NhbGRvbWFpbjAeFw0xNDAyMTIw
+OTM4MjJaFw0xNTAyMTIwOTM4MjJaMIG7MQswCQYDVQQGEwItLTESMBAGA1UECAwJ
+U29tZVN0YXRlMREwDwYDVQQHDAhTb21lQ2l0eTEZMBcGA1UECgwQU29tZU9yZ2Fu
+aXphdGlvbjEfMB0GA1UECwwWU29tZU9yZ2FuaXphdGlvbmFsVW5pdDEeMBwGA1UE
+AwwVbG9jYWxob3N0LmxvY2FsZG9tYWluMSkwJwYJKoZIhvcNAQkBFhpyb290QGxv
+Y2FsaG9zdC5sb2NhbGRvbWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBALO5J6z7uCFgdP6OtwCMxUXeNoaf45xUZNK7FXM1jnN2oNMu3wYvbiGGvEAl
+fLZK8rDJ7UvRaORzHpry/gVqC80LPPNZooFtPDB/gZEXW3kiQyj7scGvmVhg3geL
+VpSP2lq5zNl/ywbYfUyWqitLCc+fgr2hI+POzkVbs/GltaLcdCoWJvg5LTRY/rz8
+nkC76yVQLQdmRJOtJLTK+04a79xVaLQBEqIZskpzDeTFaEw9nOfZH2sL5u/CtMHr
+0oJ5AnjdGjA6HsWefFZud/jfazHJaJPNRGSalTZ4lRRbf/fgfIAIh7MjNJlxOrBE
+Q2plVVNDz7fiMsHdJ34DbQKm4+UCAwEAAaNQME4wHQYDVR0OBBYEFH8rbl4WZwcK
+p6sqIcNxtIsfI5VHMB8GA1UdIwQYMBaAFH8rbl4WZwcKp6sqIcNxtIsfI5VHMAwG
+A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBACpUZFp29V0bafo10foJ+LED
+Y0kWnh7tINBMcKCWMEnX1GrmFneQrznf2UN7r7zzeNbatL4dGV+QbXJlWveDFz86
+dt+hiATsRCyV+SLuGMDkR8pt2wux6efskNlebXsTBsurSwF29oOi3+fPbfWirSe8
+IDdQM4TVwyyrw0PYUqtYxOitNsTdQ0rVL3TwWUOur34ePSIRXE7xL5+IcWQEGWCW
+/s3aa8k30n9qKf0s2dOmxbJeYbBoCVlhbX5ZtNwA6n43HDmm0h0gOuzjgB8LvKxF
+C/RG+6syX11vfScit+4Oks9ObnSUdai+zYD8EaWS0wpyqKWHGcGMfyNAgUedMj8=
+-----END CERTIFICATE-----
diff --git a/src/ws/mock-config/cockpit/cockpit-alt.conf b/src/ws/mock-config/cockpit/cockpit-alt.conf
new file mode 100644
index 0000000..df6a9ab
--- /dev/null
+++ b/src/ws/mock-config/cockpit/cockpit-alt.conf
@@ -0,0 +1,10 @@
+[WebService]
+ProtocolHeader = X-Forwarded-Proto
+
+[Ssh-Login]
+command = mock-auth-command
+host = default-host
+connectToUnknownHosts = true
+
+[testsshscheme]
+action = remote-login-ssh
diff --git a/src/ws/mock-config/cockpit/cockpit-deprecated.conf b/src/ws/mock-config/cockpit/cockpit-deprecated.conf
new file mode 100644
index 0000000..84ed142
--- /dev/null
+++ b/src/ws/mock-config/cockpit/cockpit-deprecated.conf
@@ -0,0 +1,12 @@
+[WebService]
+ProtocolHeader = X-Forwarded-Proto
+
+[Ssh-Login]
+command = mock-auth-command
+host = default-host
+# Config to test fallback to deprecated allowUnknown option
+# This option was superseded by connectToUnknownHosts
+allowUnknown = true
+
+[testsshscheme]
+action = remote-login-ssh
diff --git a/src/ws/mock-config/cockpit/cockpit.conf b/src/ws/mock-config/cockpit/cockpit.conf
new file mode 100644
index 0000000..da7cee5
--- /dev/null
+++ b/src/ws/mock-config/cockpit/cockpit.conf
@@ -0,0 +1,50 @@
+# initial comment
+
+[Section1]
+
+ [Section2]
+value1 = string
+ value2=commas, or spaces
+value3 = commas, or spaces
+empty =
+emptystrv = :
+strvemptyitems = one::three
+# another comment
+true = TRUE
+truelower = true
+yes = YES
+one = 1
+mixed = 5f
+toolarge = 18446744073709551616
+
+[WebService]
+Origins = https://another-place.com https://another-place.com:9090
+Shell = /second/test.html
+MaxStartups = 1
+
+[badcommand]
+command = bad-command
+
+[testscheme]
+command = mock-auth-command
+response-timeout = 2
+
+[testsshscheme]
+action = remote-login-ssh
+
+[Basic]
+action = remote-login-ssh
+
+[Ssh-Login]
+command = mock-auth-command
+
+[testscheme-fd-4]
+command = mock-auth-command
+authFD = 4
+
+[timeout-scheme]
+command = mock-auth-command
+timeout = 1
+
+[none]
+action = none
diff --git a/src/ws/mock-ecc.crt b/src/ws/mock-ecc.crt
new file mode 100644
index 0000000..15063b3
--- /dev/null
+++ b/src/ws/mock-ecc.crt
@@ -0,0 +1,13 @@
+# openssl req -new -key mock-ecc.key -x509 -nodes -days 3650 -subj '/CN=localhost' -out mock-ecc.crt
+
+-----BEGIN CERTIFICATE-----
+MIIBfTCCASOgAwIBAgIUZGnBgTO/jO+3Yns9Qa3CDVBVTMwwCgYIKoZIzj0EAwIw
+FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTE5MTAzMTA5MTQzOFoXDTI5MTAyODA5
+MTQzOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
+AQcDQgAE5BRvKPl0iCppAFY4luyrbS6wIRAV6CY3PkhNpWgcRk40q7Ev85LM7I2G
+wyMihC+GlrZkJ5cz0gzUrGawAsx/I6NTMFEwHQYDVR0OBBYEFPxh7cO94TEhFL0K
+ErzKK1kMC4VIMB8GA1UdIwQYMBaAFPxh7cO94TEhFL0KErzKK1kMC4VIMA8GA1Ud
+EwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgJjlmWh7neWQlLlJC/Gq+F3u+
+7Gnxltgi4V1/PHdgRNcCIQCQ03ftc6PHZfdVMILhNRKgZauPV06Nu6Bz20OSVTWR
+ig==
+-----END CERTIFICATE-----
diff --git a/src/ws/mock-ecc.key b/src/ws/mock-ecc.key
new file mode 100644
index 0000000..a6a8d2c
--- /dev/null
+++ b/src/ws/mock-ecc.key
@@ -0,0 +1,10 @@
+# openssl ecparam -out mock-ecc.key -name prime256v1 -genkey
+
+-----BEGIN EC PARAMETERS-----
+BggqhkjOPQMBBw==
+-----END EC PARAMETERS-----
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIIdevEpPq+LmSqzcYwKk0MgKLcTn7vUNPpvvoJl9v/yEoAoGCCqGSM49
+AwEHoUQDQgAE5BRvKPl0iCppAFY4luyrbS6wIRAV6CY3PkhNpWgcRk40q7Ev85LM
+7I2GwyMihC+GlrZkJ5cz0gzUrGawAsx/Iw==
+-----END EC PRIVATE KEY-----
diff --git a/src/ws/mock-echo.c b/src/ws/mock-echo.c
new file mode 100644
index 0000000..79ef6c7
--- /dev/null
+++ b/src/ws/mock-echo.c
@@ -0,0 +1,70 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <glib.h>
+
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int
+main (void)
+{
+ /* Small buffer to find partial read/write bugs */
+ gchar buffer[sizeof (gint) - 1];
+ gsize written;
+ gssize count;
+ gint i;
+
+ g_print ("37\n\n{ \"command\" : \"init\", \"version\": 1 }");
+ for (i = 0; TRUE; i++)
+ {
+ count = read (0, buffer, sizeof (buffer));
+ if (count < 0)
+ {
+ g_printerr ("mock-echo: failed to read: %s", g_strerror (errno));
+ return 1;
+ }
+ if (count == 0)
+ return 0;
+
+ written = 0;
+ while (written < count)
+ {
+ count = write (1, buffer + written, count - written);
+ if (count < 0)
+ {
+ if (errno != EAGAIN)
+ {
+ g_printerr ("mock-echo: failed to write: %s", g_strerror (errno));
+ return 1;
+ }
+ }
+ written += count;
+ }
+
+ /* Slow short reads and writes for the first, 10 and then accelerate */
+ if (i < 3)
+ g_usleep (G_USEC_PER_SEC / 10);
+ else if (i < 30)
+ g_usleep (G_USEC_PER_SEC / 100);
+ }
+}
diff --git a/src/ws/mock-kdc b/src/ws/mock-kdc
new file mode 100755
index 0000000..3cf873f
--- /dev/null
+++ b/src/ws/mock-kdc
@@ -0,0 +1,86 @@
+#!/bin/sh
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+#
+
+set -euf
+
+cd $(dirname $0)
+
+warning()
+{
+ echo "mock-kdc: $*" >&2
+}
+
+check_require()
+{
+ for n in "$@"; do
+ if ! which $n 2> /dev/null; then
+ warning "$n not present"
+ exit 18
+ fi
+ done
+}
+
+check_require kadmin.local krb5kdc kdb5_util
+
+dir=$(mktemp -d)
+
+# Generate the KDC configuration file
+sed -e "s,[@]dir[@],$dir,g" mock-krb5.conf.in > $dir/krb5.conf
+sed -e "s,[@]dir[@],$dir,g" mock-kdc.conf.in > $dir/kdc.conf
+KRB5_KDC_PROFILE=$dir/kdc.conf
+export KRB5_KDC_PROFILE
+KRB5_CONFIG=$dir/krb5.conf
+export KRB5_CONFIG
+
+echo "KRB5_CONFIG=$dir/krb5.conf"
+echo "KRB5_KTNAME=$dir/localhost.keytab"
+
+touch $dir/kadmind.log
+
+# Generate the KDC database, undocumented -W argument: no strong random
+kdb5_util create -r COCKPIT.MOCK -s -W -P foobar > /dev/null
+
+# Generate the ACL file
+echo "*/admin@COCKPIT.MOCK *" > $dir/kadm5.acl
+
+# Add the users
+kadmin="kadmin.local -r COCKPIT.MOCK"
+$kadmin -q "addprinc -pw foobar -clearpolicy root/admin"# > /dev/null
+$kadmin -q "addprinc -pw marmalade -clearpolicy $USER" > /dev/null
+
+unique_localhosts()
+{
+ grep '\blocalhost\b' /etc/hosts | while read addr names; do
+ for name in $names; do
+ echo $name
+ done
+ done | sort | uniq
+}
+
+# Add the hosts
+unique_localhosts | while read name; do
+ $kadmin -q "addprinc -randkey -clearpolicy host/$name@COCKPIT.MOCK" > /dev/null
+ $kadmin -q "ktadd -q -keytab $dir/localhost.keytab host/$name@COCKPIT.MOCK" > /dev/null
+done
+
+# Run krb5kdc
+LC_ALL=C krb5kdc -n -r COCKPIT.MOCK 2>&1
+
+rm -rf $dir/
diff --git a/src/ws/mock-kdc.conf.in b/src/ws/mock-kdc.conf.in
new file mode 100644
index 0000000..6ad31cb
--- /dev/null
+++ b/src/ws/mock-kdc.conf.in
@@ -0,0 +1,18 @@
+[kdcdefaults]
+ kdc_ports = 20088, 20750
+ kdc_tcp_ports = 20088, 20750
+
+[logging]
+ default = FILE:@dir@/krb5.log
+ kdc = FILE:@dir@/kdc.log
+ admin_server = FILE:@dir@/kadmind.log
+
+[realms]
+ COCKPIT.MOCK = {
+ #master_key_type = aes256-cts
+ key_stash_file = @dir@/key_stash
+ acl_file = @dir@/kadm5.acl
+ admin_keytab = @dir@/kadm5.keytab
+ supported_enctypes = DEFAULT
+ database_name = @dir@/database
+ }
diff --git a/src/ws/mock-krb5.conf.in b/src/ws/mock-krb5.conf.in
new file mode 100644
index 0000000..4b6d972
--- /dev/null
+++ b/src/ws/mock-krb5.conf.in
@@ -0,0 +1,19 @@
+[logging]
+ default = FILE:@dir@/krb5.log
+ kdc = FILE:@dir@/kdc.log
+ admin_server = FILE:@dir@/kadmind.log
+
+[libdefaults]
+ dns_lookup_realm = false
+ default_realm = COCKPIT.MOCK
+ rdns = false
+
+[realms]
+ COCKPIT.MOCK = {
+ kdc = localhost:20088
+ master_kdc = localhost:20088
+ }
+
+[domain_realm]
+ localhost = COCKPIT.MOCK
+
diff --git a/src/ws/mock-pam-conv-mod.c b/src/ws/mock-pam-conv-mod.c
new file mode 100644
index 0000000..0677200
--- /dev/null
+++ b/src/ws/mock-pam-conv-mod.c
@@ -0,0 +1,95 @@
+#include "config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <security/pam_appl.h>
+#include <security/pam_modules.h>
+
+
+/* this function is ripped from pam_unix/support.c, it lets us do IO via PAM */
+static int
+converse (pam_handle_t *pamh,
+ int nargs,
+ struct pam_message **message,
+ struct pam_response **response)
+{
+ int res;
+ struct pam_conv *conv;
+
+ res = pam_get_item (pamh, PAM_CONV, (const void **) &conv);
+ if (res == PAM_SUCCESS)
+ {
+ res = conv->conv (nargs, (const struct pam_message **) message,
+ response, conv->appdata_ptr);
+ }
+
+ return res;
+}
+
+
+PAM_EXTERN int
+pam_sm_authenticate (pam_handle_t *pamh,
+ int unused,
+ int argc,
+ const char **argv)
+{
+ int res;
+
+ struct pam_message msg[1], *pmsg[1];
+ struct pam_response *resp = NULL;
+ const char *user;
+
+ /* Lookup the user */
+ res = pam_get_user (pamh, &user, NULL);
+ if (res != PAM_SUCCESS)
+ {
+ syslog (LOG_WARNING, "couldn't get pam user: %s", pam_strerror (pamh, res));
+ goto out;
+ }
+
+ /* Send message */
+ pmsg[0] = &msg[0];
+ msg[0].msg_style = PAM_PROMPT_ECHO_ON;
+ msg[0].msg = "The answer to life the universe and everything: ";
+
+ res = converse (pamh, 1 , pmsg, &resp);
+ if (res != PAM_SUCCESS)
+ {
+ syslog (LOG_WARNING, "couldn't send prompt: %s", pam_strerror (pamh, res));
+ goto out;
+ }
+
+ if (!resp)
+ {
+ syslog (LOG_WARNING, "missing response");
+ res = PAM_CONV_ERR;
+ goto out;
+ }
+ else if (resp[0].resp == NULL )
+ {
+ syslog (LOG_WARNING, "got null resp");
+ res = PAM_AUTH_ERR;
+ goto out;
+ }
+
+ if (strcmp (resp[0].resp, "42") == 0)
+ res = PAM_SUCCESS;
+ else
+ res = PAM_AUTH_ERR;
+
+out:
+ if (resp)
+ free (resp);
+ return res;
+}
+
+PAM_EXTERN int
+pam_sm_setcred (pam_handle_t *pamh,
+ int flags,
+ int argc,
+ const char *argv[])
+{
+ return PAM_SUCCESS;
+}
diff --git a/src/ws/mock-pipes/cockpit-session b/src/ws/mock-pipes/cockpit-session
new file mode 100755
index 0000000..c47d228
--- /dev/null
+++ b/src/ws/mock-pipes/cockpit-session
@@ -0,0 +1,3 @@
+#!/bin/sh
+# exception like in cockpit-session
+exit 127
diff --git a/src/ws/mock-pipes/exit127 b/src/ws/mock-pipes/exit127
new file mode 100755
index 0000000..c47d228
--- /dev/null
+++ b/src/ws/mock-pipes/exit127
@@ -0,0 +1,3 @@
+#!/bin/sh
+# exception like in cockpit-session
+exit 127
diff --git a/src/ws/mock-pipes/someprogram b/src/ws/mock-pipes/someprogram
new file mode 100755
index 0000000..c47d228
--- /dev/null
+++ b/src/ws/mock-pipes/someprogram
@@ -0,0 +1,3 @@
+#!/bin/sh
+# exception like in cockpit-session
+exit 127
diff --git a/src/ws/mock-service.c b/src/ws/mock-service.c
new file mode 100644
index 0000000..fb308b8
--- /dev/null
+++ b/src/ws/mock-service.c
@@ -0,0 +1,839 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "mock-service.h"
+#include "mock-dbus-tests.h"
+
+#include <gio/gio.h>
+#include <gio/gunixfdlist.h>
+#include <glib-unix.h>
+#include <string.h>
+
+typedef struct {
+ GDBusConnection *connection;
+ GDBusObjectManagerServer *object_manager;
+ GHashTable *other_names;
+ GList *never_return_invocations;
+} MockData;
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+on_handle_hello_world (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ const gchar *greeting,
+ gpointer user_data)
+{
+ gchar *response;
+ response = g_strdup_printf ("Word! You said `%s'. I'm Skeleton, btw!", greeting);
+ test_frobber_complete_hello_world (object, invocation, response);
+ g_free (response);
+ return TRUE;
+}
+
+static gboolean
+on_handle_never_return (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ MockData *data = user_data;
+ /* just ignoring the call causes long TestDBus shutdown errors, as the unanswered call spooks around in the server's brain until
+ * the global D-Bus timeout; so remember them and answer on shutdown */
+ data->never_return_invocations = g_list_append (data->never_return_invocations, g_object_ref (invocation));
+ return TRUE;
+}
+
+static gboolean
+on_handle_test_primitive_types (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ guchar val_byte,
+ gboolean val_boolean,
+ gint16 val_int16,
+ guint16 val_uint16,
+ gint val_int32,
+ guint val_uint32,
+ gint64 val_int64,
+ guint64 val_uint64,
+ gdouble val_double,
+ const gchar *val_string,
+ const gchar *val_objpath,
+ const gchar *val_signature,
+ const gchar *val_bytestring,
+ gpointer user_data)
+{
+ gchar *s1;
+ gchar *s2;
+ gchar *s3;
+ s1 = g_strdup_printf ("Word! You said `%s'. Rock'n'roll!", val_string);
+ s2 = g_strdup_printf ("/modified%s", val_objpath);
+ s3 = g_strdup_printf ("assgit%s", val_signature);
+ test_frobber_complete_test_primitive_types (object,
+ invocation,
+ 10 + val_byte,
+ !val_boolean,
+ 100 + val_int16,
+ 1000 + val_uint16,
+ 10000 + val_int32,
+ 100000 + val_uint32,
+ 1000000 + val_int64,
+ 10000000 + val_uint64,
+ val_double / G_PI,
+ s1,
+ s2,
+ s3,
+ "bytestring!\xff");
+ g_free (s1);
+ g_free (s2);
+ g_free (s3);
+ return TRUE;
+}
+
+static gboolean
+on_handle_test_non_primitive_types (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ GVariant *dict_s_to_s,
+ GVariant *dict_s_to_pairs,
+ GVariant *a_struct,
+ const gchar* const *array_of_strings,
+ const gchar* const *array_of_objpaths,
+ GVariant *array_of_signatures,
+ const gchar* const *array_of_bytestrings,
+ gpointer user_data)
+{
+ gchar *s;
+ GString *str;
+ str = g_string_new (NULL);
+ s = g_variant_print (dict_s_to_s, TRUE);
+ g_string_append (str, s);
+ g_free (s);
+ s = g_variant_print (dict_s_to_pairs, TRUE);
+ g_string_append (str, s);
+ g_free (s);
+ s = g_variant_print (a_struct, TRUE);
+ g_string_append (str, s);
+ g_free (s);
+ s = g_strjoinv (", ", (gchar **)array_of_strings);
+ g_string_append_printf (str, "array_of_strings: [%s] ", s);
+ g_free (s);
+ s = g_strjoinv (", ", (gchar **)array_of_objpaths);
+ g_string_append_printf (str, "array_of_objpaths: [%s] ", s);
+ g_free (s);
+ s = g_variant_print (array_of_signatures, TRUE);
+ g_string_append_printf (str, "array_of_signatures: %s ", s);
+ g_free (s);
+ s = g_strjoinv (", ", (gchar **)array_of_bytestrings);
+ g_string_append_printf (str, "array_of_bytestrings: [%s] ", s);
+ g_free (s);
+ test_frobber_complete_test_non_primitive_types (object, invocation, str->str);
+ g_string_free (str, TRUE);
+ return TRUE;
+}
+
+static gboolean
+on_handle_test_variant (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ GVariant *v,
+ gpointer user_data)
+{
+ test_frobber_complete_test_variant (object, invocation);
+ return TRUE;
+}
+
+static gboolean
+on_handle_request_signal_emission (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ gint which_one,
+ gpointer user_data)
+{
+ if (which_one == 0)
+ {
+ const gchar *a_strv[] = {"foo", "frobber", NULL};
+ const gchar *a_objpath_array[] = {"/foo", "/foo/bar", NULL};
+ GVariant *a_variant = g_variant_new_parsed ("{'first': (42, 42), 'second': (43, 43)}");
+ test_frobber_emit_test_signal (object, 43, a_strv, a_objpath_array, a_variant); /* consumes a_variant */
+ test_frobber_complete_request_signal_emission (object, invocation);
+ }
+ return TRUE;
+}
+
+static gboolean
+on_handle_request_property_mods (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ test_frobber_set_y (object, test_frobber_get_y (object) + 1);
+ test_frobber_set_i (object, test_frobber_get_i (object) + 1);
+ g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (object));
+ test_frobber_complete_request_multi_property_mods (object, invocation);
+ return TRUE;
+}
+
+static gboolean
+on_handle_request_multi_property_mods (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ test_frobber_set_y (object, test_frobber_get_y (object) + 1);
+ test_frobber_set_i (object, test_frobber_get_i (object) + 1);
+ test_frobber_set_y (object, test_frobber_get_y (object) + 1);
+ test_frobber_set_i (object, test_frobber_get_i (object) + 1);
+ g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (object));
+ test_frobber_set_y (object, test_frobber_get_y (object) + 1);
+ test_frobber_set_i (object, test_frobber_get_i (object) + 1);
+ test_frobber_complete_request_multi_property_mods (object, invocation);
+ return TRUE;
+}
+
+static gboolean
+on_handle_property_cancellation (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ guint n;
+ n = test_frobber_get_n (object);
+ /* This queues up a PropertiesChange event */
+ test_frobber_set_n (object, n + 1);
+ /* this modifies the queued up event */
+ test_frobber_set_n (object, n);
+ /* this flushes all PropertiesChanges event (sends the D-Bus message right
+ * away, if any - there should not be any)
+ */
+ g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (object));
+ /* this makes us return the reply D-Bus method */
+ test_frobber_complete_property_cancellation (object, invocation);
+ return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+on_handle_create_object (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ const gchar *at_path,
+ gpointer user_data)
+{
+ MockData *data = user_data;
+ GDBusObject *previous;
+
+ previous = g_dbus_object_manager_get_object (G_DBUS_OBJECT_MANAGER (data->object_manager), at_path);
+ if (previous != NULL)
+ {
+ g_dbus_method_invocation_return_error (invocation,
+ G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Sorry, object already exists at %s",
+ at_path);
+ g_object_unref (previous);
+ }
+ else
+ {
+ TestObjectSkeleton *new_object;
+ TestFrobber *frobber;
+
+ new_object = test_object_skeleton_new (at_path);
+ frobber = test_frobber_skeleton_new ();
+ test_object_skeleton_set_frobber (new_object, frobber);
+ g_dbus_object_manager_server_export (data->object_manager, G_DBUS_OBJECT_SKELETON (new_object));
+ g_object_unref (frobber);
+ g_object_unref (new_object);
+
+ g_signal_connect (frobber,
+ "handle-request-property-mods",
+ G_CALLBACK (on_handle_request_property_mods),
+ NULL);
+ test_frobber_complete_create_object (object, invocation);
+ }
+ return TRUE;
+}
+
+static gboolean
+on_handle_delete_object (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ const gchar *path,
+ gpointer user_data)
+{
+ MockData *data = user_data;
+ GDBusObject *previous;
+
+ previous = g_dbus_object_manager_get_object (G_DBUS_OBJECT_MANAGER (data->object_manager), path);
+ if (previous != NULL)
+ {
+ g_warn_if_fail (g_dbus_object_manager_server_unexport (data->object_manager, path));
+ test_frobber_complete_delete_object (object, invocation);
+ g_object_unref (previous);
+ }
+ else
+ {
+ g_dbus_method_invocation_return_error (invocation,
+ G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Sorry, there is no object at %s",
+ path);
+ }
+ return TRUE;
+}
+
+static gboolean
+on_handle_delete_all_objects (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ MockData *data = user_data;
+ const gchar *path;
+ GList *objects;
+ GList *l;
+
+ objects = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (data->object_manager));
+ for (l = objects; l != NULL; l = g_list_next (l))
+ {
+ path = g_dbus_object_get_object_path (l->data);
+ if (!g_str_has_suffix (path, "/frobber"))
+ g_warn_if_fail (g_dbus_object_manager_server_unexport (data->object_manager, path));
+ }
+
+ test_frobber_complete_delete_all_objects (object, invocation);
+ g_list_free_full (objects, g_object_unref);
+ return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+on_handle_test_asv (TestFrobber *object,
+ GDBusMethodInvocation *invocation,
+ GVariant *asv,
+ gpointer user_data)
+{
+ gchar *s;
+ s = g_variant_print (asv, TRUE);
+ test_frobber_complete_test_asv (object, invocation, s);
+ g_free (s);
+ return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+on_handle_add_alpha (TestFrobber *frobber,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ TestObjectSkeleton *enclosing;
+ enclosing = TEST_OBJECT_SKELETON (g_dbus_interface_get_object (G_DBUS_INTERFACE (frobber)));
+ if (test_object_peek_alpha (TEST_OBJECT (enclosing)) == NULL)
+ {
+ TestAlpha *iface = test_alpha_skeleton_new ();
+ test_object_skeleton_set_alpha (enclosing, iface);
+ g_object_unref (iface);
+ }
+ test_frobber_complete_add_alpha (frobber, invocation);
+ return TRUE;
+}
+
+static gboolean
+on_handle_remove_alpha (TestFrobber *frobber,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ TestObjectSkeleton *enclosing;
+ enclosing = TEST_OBJECT_SKELETON (g_dbus_interface_get_object (G_DBUS_INTERFACE (frobber)));
+ if (test_object_peek_alpha (TEST_OBJECT (enclosing)) != NULL)
+ test_object_skeleton_set_alpha (enclosing, NULL);
+ test_frobber_complete_add_alpha (frobber, invocation);
+ return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+typedef struct {
+ GDBusMethodInvocation *invocation;
+} ClaimNameData;
+
+static void
+claim_name_data_free (gpointer data)
+{
+ ClaimNameData *claim_data = data;
+ if (claim_data->invocation)
+ g_object_unref (claim_data->invocation);
+ g_free (claim_data);
+}
+
+static void
+on_other_name_acquired (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ ClaimNameData *claim_data = user_data;
+ if (claim_data->invocation)
+ {
+ g_dbus_method_invocation_return_value (claim_data->invocation, NULL);
+ g_clear_object (&claim_data->invocation);
+ }
+}
+
+static void
+on_other_name_lost (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ ClaimNameData *claim_data = user_data;
+ if (claim_data->invocation)
+ {
+ g_dbus_method_invocation_return_error (claim_data->invocation,
+ G_IO_ERROR, G_IO_ERROR_FAILED,
+ "Couldn't claim name: %s", name);
+ g_message ("couldn't claim name: %s", name);
+ g_clear_object (&claim_data->invocation);
+ }
+}
+
+static gboolean
+on_claim_other_name (TestFrobber *frobber,
+ GDBusMethodInvocation *invocation,
+ const gchar *name,
+ gpointer user_data)
+{
+ ClaimNameData *claim_data;
+ MockData *mock_data = user_data;
+ guint id;
+
+ g_return_val_if_fail (g_hash_table_lookup (mock_data->other_names, name) == NULL, FALSE);
+
+ claim_data = g_new0 (ClaimNameData, 1);
+ claim_data->invocation = g_object_ref (invocation);
+ id = g_bus_own_name_on_connection (mock_data->connection, name,
+ G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | G_BUS_NAME_OWNER_FLAGS_NONE,
+ on_other_name_acquired,
+ on_other_name_lost,
+ claim_data, claim_name_data_free);
+
+ g_hash_table_replace (mock_data->other_names, g_strdup (name), GUINT_TO_POINTER (id));
+ return TRUE;
+}
+
+static gboolean
+on_release_other_name (TestFrobber *frobber,
+ GDBusMethodInvocation *invocation,
+ const gchar *name,
+ gpointer user_data)
+{
+ MockData *mock_data = user_data;
+ guint id;
+
+ g_return_val_if_fail (g_hash_table_lookup (mock_data->other_names, name) != NULL, FALSE);
+
+ id = GPOINTER_TO_UINT (g_hash_table_lookup (mock_data->other_names, name));
+ g_hash_table_remove (mock_data->other_names, name);
+ g_bus_unown_name (id);
+
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ return TRUE;
+
+}
+
+static gboolean
+on_tell_me_your_name (TestFrobber *frobber,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ GDBusMessage *message = g_dbus_method_invocation_get_message (invocation);
+ GVariant *name = g_variant_new_string (g_dbus_message_get_destination (message));
+ g_dbus_method_invocation_return_value (invocation, g_variant_new_tuple (&name, 1));
+ return TRUE;
+
+}
+
+static gboolean
+test_fd_pipe_readable (gint fd,
+ GIOCondition condition,
+ gpointer user_data)
+{
+ const char *expected = "Hello, fd";
+ size_t expected_len = strlen (expected);
+ char buffer[100];
+ size_t n;
+
+ g_assert (condition == G_IO_IN);
+
+ n = read (fd, buffer, 100);
+ g_assert (n == expected_len);
+ g_assert (strncmp (buffer, expected, expected_len) == 0);
+
+ close (fd);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+test_fd_deferred_close (gpointer user_data)
+{
+ gint fd = GPOINTER_TO_INT (user_data);
+
+ close (fd);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+on_make_test_fd (TestFrobber *frobber,
+ GDBusMethodInvocation *invocation,
+ const char *type,
+ gpointer user_data)
+{
+ gint fds[2] = { -1, -1 };
+ GUnixFDList *fd_list;
+ const char *data = "Hello, fd";
+ size_t data_size = strlen (data);
+ size_t n;
+ GError *error = NULL;
+
+ g_unix_open_pipe (fds, FD_CLOEXEC, &error);
+ g_assert_no_error (error);
+
+ fd_list = g_unix_fd_list_new ();
+
+ if (g_str_equal (type, "readable"))
+ {
+ g_unix_fd_list_append (fd_list, fds[0], &error);
+ g_assert_no_error (error);
+ close (fds[0]);
+
+ n = write(fds[1], data, data_size);
+ g_assert (n == data_size);
+
+ /*
+ * Close this fd a bit later, because closing the writing side
+ * makes CockpitPipe close itself immediately, which breaks
+ * testing that writing into this read-only pipe fails.
+ */
+ g_timeout_add_seconds (5, test_fd_deferred_close, GINT_TO_POINTER (fds[1]));
+ }
+ else if (g_str_equal (type, "writable"))
+ {
+ g_unix_fd_list_append (fd_list, fds[1], &error);
+ g_assert_no_error (error);
+ close (fds[1]);
+
+ g_unix_fd_add (fds[0], G_IO_IN, test_fd_pipe_readable, NULL);
+ }
+ else
+ g_assert_not_reached ();
+
+ g_dbus_method_invocation_return_value_with_unix_fd_list (invocation, g_variant_new ("(h)", 0), fd_list);
+
+ return TRUE;
+}
+
+/* ------------------------------------------------------------------------
+ * Non object manager stuff
+ */
+
+static GVariant *
+clique_get_property (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *property_name,
+ GError **error,
+ gpointer user_data)
+{
+ /* The only property is Friend */
+ gchar *friend = user_data;
+ return g_variant_new_object_path (friend);
+}
+
+static gboolean
+on_create_clique (TestFrobber *frobber,
+ GDBusMethodInvocation *invocation,
+ const gchar *name,
+ gpointer user_data)
+{
+ GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation);
+ GError *error = NULL;
+ gchar *path = NULL;
+ gchar *friend;
+ gint i;
+
+ static const GDBusInterfaceVTable vtable = {
+ .method_call = NULL,
+ .get_property = clique_get_property,
+ .set_property = NULL,
+ };
+
+ for (i = 0; i < 3; i++)
+ {
+ g_free (path);
+ path = g_strdup_printf ("/cliques/%s/%d", name, i);
+
+ friend = g_strdup_printf ("/cliques/%s/%d", name, (i + 1) % 3);
+
+ g_dbus_connection_register_object (connection, path,
+ test_clique_interface_info (),
+ &vtable, friend, g_free, &error);
+ if (error)
+ {
+ g_critical ("Couldn't register new clique: %s", error->message);
+ g_clear_error (&error);
+ }
+ }
+
+ test_frobber_complete_create_clique (frobber, invocation, path);
+ g_free (path);
+ return TRUE;
+}
+
+static GVariant *
+hidden_get_property (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *property_name,
+ GError **error,
+ gpointer user_data)
+{
+ /* The only property is Name */
+ gchar *name = user_data;
+ return g_variant_new_string (name);
+}
+
+static gboolean
+on_emit_hidden (TestFrobber *frobber,
+ GDBusMethodInvocation *invocation,
+ const gchar *name,
+ gpointer user_data)
+{
+ GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation);
+ GError *error = NULL;
+ gchar *path = NULL;
+
+ static const GDBusInterfaceVTable vtable = {
+ .method_call = NULL,
+ .get_property = hidden_get_property,
+ .set_property = NULL,
+ };
+
+ path = g_strdup_printf ("/hidden/%s", name);
+
+ g_dbus_connection_register_object (connection, path,
+ test_hidden_interface_info (),
+ &vtable, g_strdup (name), g_free, &error);
+ if (error)
+ {
+ g_critical ("Couldn't register new hidden: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ g_dbus_connection_emit_signal (connection, NULL, path,
+ "com.redhat.Cockpit.DBusTests.Hidden", "Yooohooo",
+ g_variant_new ("()"), &error);
+
+ if (error)
+ {
+ g_critical ("Couldn't emit signal on hidden: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ /* Now emit a signal from it */
+ test_frobber_complete_emit_hidden (frobber, invocation);
+ g_free (path);
+ return TRUE;
+}
+
+/* ----------------------------------------------------------------------------------------------------
+ * An Introspect() that actually fails
+ */
+
+static void
+introspect_fail_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ const gchar *dbus_error = user_data;
+ g_dbus_method_invocation_return_dbus_error (invocation, dbus_error, dbus_error);
+}
+
+static void
+mock_service_create_introspect_fail (GDBusConnection *connection)
+{
+ static const gchar introspectable_xml[] =
+ "<node>"
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">"
+ " <method name=\"Introspect\">"
+ " <arg type=\"s\" name=\"xml_data\" direction=\"out\"/>"
+ " </method>"
+ " </interface>"
+ "</node>";
+
+ const GDBusInterfaceVTable introspect_vtable = {
+ .method_call = introspect_fail_method_call,
+ };
+
+ GDBusNodeInfo *node_info;
+ GDBusInterfaceInfo *interface_info;
+ GError *error = NULL;
+
+ node_info = g_dbus_node_info_new_for_xml (introspectable_xml, &error);
+ g_assert_no_error (error);
+
+ interface_info = g_dbus_node_info_lookup_interface (node_info, "org.freedesktop.DBus.Introspectable");
+ g_assert (interface_info != NULL);
+
+ /* Return a failure when introspecting this object path */
+ g_dbus_connection_register_object (connection, "/introspect/unknown", interface_info, &introspect_vtable,
+ "org.freedesktop.DBus.Error.UnknownObject", NULL, &error);
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+ g_error_free (error);
+ else
+ g_assert_no_error (error);
+
+ g_dbus_node_info_unref (node_info);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+mock_data_free (gpointer data)
+{
+ MockData *mock_data = data;
+
+ /* answer all NeverReturn() calls now, to avoid service shutdown hang */
+ for (GList *elem = mock_data->never_return_invocations; elem; elem = elem->next)
+ g_dbus_method_invocation_return_value (elem->data, NULL);
+ g_list_free_full (mock_data->never_return_invocations, g_object_unref);
+
+ g_object_unref (mock_data->connection);
+ g_hash_table_destroy (mock_data->other_names);
+ g_free (mock_data);
+}
+
+GObject *
+mock_service_create_and_export (GDBusConnection *connection,
+ const gchar *object_manager_path)
+{
+ GError *error;
+ TestFrobber *exported_frobber;
+ TestObjectSkeleton *exported_object;
+ GDBusObjectManagerServer *object_manager;
+ MockData *mock_data;
+ gchar *path;
+
+ mock_data = g_new0 (MockData, 1);
+ mock_data->connection = g_object_ref (connection);
+ mock_data->other_names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ /* Test that we can export an object using the generated
+ * TestFrobberSkeleton subclass. Notes:
+ *
+ * 1. We handle methods by simply connecting to the appropriate
+ * GObject signal.
+ *
+ * 2. Property storage is taken care of by the class; we can
+ * use g_object_get()/g_object_set() (and the generated
+ * C bindings at will)
+ */
+ error = NULL;
+ exported_frobber = test_frobber_skeleton_new ();
+ test_frobber_set_ay (exported_frobber, "ABCabc");
+ test_frobber_set_y (exported_frobber, 42);
+ test_frobber_set_d (exported_frobber, 43.0);
+ test_frobber_set_finally_normal_name (exported_frobber, "There aint no place like home");
+ test_frobber_set_writeonly_property (exported_frobber, "Mr. Burns");
+ test_frobber_set_readonly_property (exported_frobber, "blah");
+
+ object_manager = g_dbus_object_manager_server_new (object_manager_path);
+ mock_data->object_manager = object_manager;
+ g_object_set_data_full (G_OBJECT (object_manager), "mock-data", mock_data, mock_data_free);
+
+ path = g_strdup_printf ("%s/frobber", object_manager_path);
+ exported_object = test_object_skeleton_new (path);
+ g_free (path);
+
+ test_object_skeleton_set_frobber (exported_object, exported_frobber);
+ g_dbus_object_manager_server_export (object_manager, G_DBUS_OBJECT_SKELETON (exported_object));
+ g_object_unref (exported_object);
+
+ g_dbus_object_manager_server_set_connection (object_manager, connection);
+
+ g_assert_no_error (error);
+ g_signal_connect (exported_frobber, "handle-hello-world",
+ G_CALLBACK (on_handle_hello_world), NULL);
+ g_signal_connect (exported_frobber, "handle-never-return",
+ G_CALLBACK (on_handle_never_return), mock_data);
+ g_signal_connect (exported_frobber,
+ "handle-test-primitive-types",
+ G_CALLBACK (on_handle_test_primitive_types), NULL);
+ g_signal_connect (exported_frobber,
+ "handle-test-non-primitive-types",
+ G_CALLBACK (on_handle_test_non_primitive_types), NULL);
+ g_signal_connect (exported_frobber,
+ "handle-test-variant",
+ G_CALLBACK (on_handle_test_variant), NULL);
+ g_signal_connect (exported_frobber,
+ "handle-request-signal-emission",
+ G_CALLBACK (on_handle_request_signal_emission), NULL);
+ g_signal_connect (exported_frobber,
+ "handle-request-property-mods",
+ G_CALLBACK (on_handle_request_property_mods), NULL);
+ g_signal_connect (exported_frobber,
+ "handle-request-multi-property-mods",
+ G_CALLBACK (on_handle_request_multi_property_mods), NULL);
+ g_signal_connect (exported_frobber,
+ "handle-property-cancellation",
+ G_CALLBACK (on_handle_property_cancellation), NULL);
+ g_signal_connect (exported_frobber,
+ "handle-delete-all-objects",
+ G_CALLBACK (on_handle_delete_all_objects), mock_data);
+ g_signal_connect (exported_frobber,
+ "handle-create-object",
+ G_CALLBACK (on_handle_create_object), mock_data);
+ g_signal_connect (exported_frobber,
+ "handle-delete-object",
+ G_CALLBACK (on_handle_delete_object), mock_data);
+ g_signal_connect (exported_frobber,
+ "handle-test-asv",
+ G_CALLBACK (on_handle_test_asv), NULL);
+ g_signal_connect (exported_frobber,
+ "handle-add-alpha",
+ G_CALLBACK (on_handle_add_alpha), NULL);
+ g_signal_connect (exported_frobber,
+ "handle-remove-alpha",
+ G_CALLBACK (on_handle_remove_alpha), NULL);
+ g_signal_connect (exported_frobber, "handle-create-clique",
+ G_CALLBACK (on_create_clique), NULL);
+ g_signal_connect (exported_frobber, "handle-emit-hidden",
+ G_CALLBACK (on_emit_hidden), NULL);
+
+ g_signal_connect (exported_frobber, "handle-claim-other-name",
+ G_CALLBACK (on_claim_other_name), mock_data);
+ g_signal_connect (exported_frobber, "handle-release-other-name",
+ G_CALLBACK (on_release_other_name), mock_data);
+ g_signal_connect (exported_frobber, "handle-tell-me-your-name",
+ G_CALLBACK (on_tell_me_your_name), mock_data);
+ g_signal_connect (exported_frobber, "handle-make-test-fd",
+ G_CALLBACK (on_make_test_fd), mock_data);
+
+ g_object_unref (exported_frobber);
+ mock_service_create_introspect_fail (connection);
+ return G_OBJECT (object_manager);
+}
diff --git a/src/ws/mock-service.h b/src/ws/mock-service.h
new file mode 100644
index 0000000..e58ef42
--- /dev/null
+++ b/src/ws/mock-service.h
@@ -0,0 +1,28 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <gio/gio.h>
+
+#ifndef __MOCK_SERVICE_H__
+#define __MOCK_SERVICE_H__
+
+GObject * mock_service_create_and_export (GDBusConnection *connection,
+ const gchar *object_manager_path);
+
+#endif /* __MOCK_SERVICE_H__ */
diff --git a/src/ws/mock-static/index.html b/src/ws/mock-static/index.html
new file mode 100644
index 0000000..bec27d1
--- /dev/null
+++ b/src/ws/mock-static/index.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+<title>Invalid index</title>
+</head>
+<body>
+<h1>Invalid index</h1>
+</body>
+</html>
diff --git a/src/ws/pam_cockpit_cert.c b/src/ws/pam_cockpit_cert.c
new file mode 100644
index 0000000..f9ab091
--- /dev/null
+++ b/src/ws/pam_cockpit_cert.c
@@ -0,0 +1,48 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+/* Define which PAM interfaces we provide */
+#define PAM_SM_AUTH
+
+#include <security/pam_modules.h>
+
+/* WARNING:
+ * Stub for backwards compatibility with old configs.
+ * Not in use and will be removed soon.
+ */
+
+PAM_EXTERN int
+pam_sm_authenticate (pam_handle_t *pamh,
+ int flags,
+ int argc,
+ const char **argv)
+{
+ return PAM_IGNORE;
+}
+
+PAM_EXTERN int
+pam_sm_setcred (pam_handle_t *pamh,
+ int flags,
+ int argc,
+ const char *argv[])
+{
+ return PAM_SUCCESS;
+}
diff --git a/src/ws/test-auth.c b/src/ws/test-auth.c
new file mode 100644
index 0000000..e088467
--- /dev/null
+++ b/src/ws/test-auth.c
@@ -0,0 +1,1320 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitauth.h"
+#include "cockpitws.h"
+
+#include "common/cockpitconf.h"
+#include "common/cockpiterror.h"
+#include "common/cockpitsystem.h"
+#include "common/cockpitwebrequest-private.h"
+
+#include "websocket/websocket.h"
+
+#include "testlib/cockpittest.h"
+#include "testlib/mock-auth.h"
+
+#include <string.h>
+
+/* Mock override these from other files */
+extern const gchar *cockpit_config_file;
+extern const gchar *cockpit_ws_max_startups;
+
+typedef struct {
+ CockpitAuth *auth;
+} Test;
+
+static void
+setup (Test *test,
+ gconstpointer data)
+{
+ test->auth = cockpit_auth_new (FALSE, COCKPIT_AUTH_NONE);
+}
+
+static void
+teardown (Test *test,
+ gconstpointer data)
+{
+ g_object_unref (test->auth);
+}
+
+static void
+setup_normal (Test *test,
+ gconstpointer data)
+{
+ cockpit_config_file = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf";
+ test->auth = cockpit_auth_new (FALSE, COCKPIT_AUTH_NONE);
+}
+
+static void
+setup_alt_config (Test *test,
+ gconstpointer data)
+{
+ cockpit_config_file = SRCDIR "/src/ws/mock-config/cockpit/cockpit-alt.conf";
+ test->auth = cockpit_auth_new (FALSE, COCKPIT_AUTH_NONE);
+}
+
+static void
+teardown_normal (Test *test,
+ gconstpointer data)
+{
+ cockpit_assert_expected ();
+ g_object_unref (test->auth);
+ cockpit_conf_cleanup ();
+}
+
+static void
+on_ready_get_result (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **retval = user_data;
+ g_assert (retval != NULL);
+ g_assert (*retval == NULL);
+ *retval = g_object_ref (result);
+}
+
+static void
+test_application (Test *test,
+ gconstpointer data)
+{
+ gchar *application = NULL;
+ gboolean is_host = FALSE;
+
+ application = cockpit_auth_parse_application ("/", &is_host);
+ g_assert_cmpstr ("cockpit", ==, application);
+ g_assert_false (is_host);
+ g_clear_pointer (&application, g_free);
+
+ application = cockpit_auth_parse_application ("/=", &is_host);
+ g_assert_cmpstr ("cockpit", ==, application);
+ g_assert_false (is_host);
+ g_clear_pointer (&application, g_free);
+
+ application = cockpit_auth_parse_application ("/other/other", &is_host);
+ g_assert_cmpstr ("cockpit", ==, application);
+ g_assert_false (is_host);
+ g_clear_pointer (&application, g_free);
+
+ application = cockpit_auth_parse_application ("/=other/other", &is_host);
+ g_assert_true (is_host);
+ g_assert_cmpstr ("cockpit+=other", ==, application);
+ g_clear_pointer (&application, g_free);
+
+ application = cockpit_auth_parse_application ("/=other", &is_host);
+ g_assert_true (is_host);
+ g_assert_cmpstr ("cockpit+=other", ==, application);
+ g_clear_pointer (&application, g_free);
+
+ application = cockpit_auth_parse_application ("/=other/", &is_host);
+ g_assert_true (is_host);
+ g_assert_cmpstr ("cockpit+=other", ==, application);
+ g_clear_pointer (&application, g_free);
+
+ application = cockpit_auth_parse_application ("/cockpit", &is_host);
+ g_assert_cmpstr ("cockpit", ==, application);
+ g_assert_false (is_host);
+ g_clear_pointer (&application, g_free);
+
+ application = cockpit_auth_parse_application ("/cockpit/login", &is_host);
+ g_assert_cmpstr ("cockpit", ==, application);
+ g_assert_false (is_host);
+ g_clear_pointer (&application, g_free);
+
+ application = cockpit_auth_parse_application ("/cockpit+application", &is_host);
+ g_assert_cmpstr ("cockpit+application", ==, application);
+ g_assert_false (is_host);
+ g_clear_pointer (&application, g_free);
+
+ application = cockpit_auth_parse_application ("/cockpit+application/", &is_host);
+ g_assert_false (is_host);
+ g_assert_cmpstr ("cockpit+application", ==, application);
+ g_clear_pointer (&application, g_free);
+
+ application = cockpit_auth_parse_application ("/cockpit+application/other/other", &is_host);
+ g_assert_false (is_host);
+ g_assert_cmpstr ("cockpit+application", ==, application);
+ g_clear_pointer (&application, g_free);
+}
+
+typedef struct {
+ const gchar *superuser_mode;
+ gboolean expect_stored_password;
+} UserpassFixture;
+
+static const UserpassFixture fixture_superuser_any = {
+ .superuser_mode = "any",
+ .expect_stored_password = TRUE
+};
+
+static void
+test_userpass_cookie_check (Test *test,
+ gconstpointer data)
+{
+ const UserpassFixture *fix = data;
+ GAsyncResult *result = NULL;
+ CockpitWebService *service;
+ CockpitWebService *prev_service;
+ CockpitCreds *creds;
+ CockpitCreds *prev_creds;
+ JsonObject *response = NULL;
+ GError *error = NULL;
+ GHashTable *headers;
+
+ headers = mock_auth_basic_header ("me", "this is the password");
+ if (fix && fix->superuser_mode)
+ g_hash_table_insert (headers, g_strdup ("X-Superuser"), g_strdup (fix->superuser_mode));
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path = "/cockpit/", .headers = headers),
+ on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ headers = web_socket_util_new_headers ();
+ response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
+ g_assert_no_error (error);
+
+ /* Get the service */
+ mock_auth_include_cookie_as_if_client (headers, headers, "cockpit");
+ service = cockpit_auth_check_cookie (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers));
+
+ g_object_unref (result);
+ g_assert (service != NULL);
+ g_assert (response != NULL);
+
+ creds = cockpit_web_service_get_creds (service);
+ g_assert_cmpstr ("me", ==, cockpit_creds_get_user (creds));
+ g_assert_cmpstr ("cockpit", ==, cockpit_creds_get_application (creds));
+ if (fix && fix->expect_stored_password)
+ g_assert_cmpstr ("this is the password", ==, g_bytes_get_data (cockpit_creds_get_password (creds), NULL));
+ else
+ g_assert_null (cockpit_creds_get_password (creds));
+
+ prev_service = service;
+ g_object_unref (service);
+ service = NULL;
+
+ prev_creds = creds;
+ creds = NULL;
+
+ mock_auth_include_cookie_as_if_client (headers, headers, "cockpit");
+
+ service = cockpit_auth_check_cookie (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers));
+ g_assert (prev_service == service);
+
+ creds = cockpit_web_service_get_creds (service);
+ g_assert (prev_creds == creds);
+
+ g_assert_cmpstr ("me", ==, cockpit_creds_get_user (creds));
+ if (fix && fix->expect_stored_password)
+ g_assert_cmpstr ("this is the password", ==, g_bytes_get_data (cockpit_creds_get_password (creds), NULL));
+ else
+ g_assert_null (cockpit_creds_get_password (creds));
+
+ g_hash_table_destroy (headers);
+ g_object_unref (service);
+ json_object_unref (response);
+}
+
+static void
+test_userpass_bad (Test *test,
+ gconstpointer data)
+{
+ GAsyncResult *result = NULL;
+ GError *error = NULL;
+ JsonObject *response;
+ GHashTable *headers;
+
+ headers = mock_auth_basic_header ("me", "bad");
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers),
+ on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ headers = web_socket_util_new_headers ();
+ response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
+ g_object_unref (result);
+
+ g_assert (response == NULL);
+ g_assert_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED);
+ g_clear_error (&error);
+
+ g_hash_table_destroy (headers);
+}
+
+static void
+test_userpass_emptypass (Test *test,
+ gconstpointer data)
+{
+ GAsyncResult *result = NULL;
+ JsonObject *response;
+ GError *error = NULL;
+ GHashTable *headers;
+
+ headers = mock_auth_basic_header ("aaaaaa", "");
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers),
+ on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ headers = web_socket_util_new_headers ();
+ response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
+ g_object_unref (result);
+
+ g_assert (response == NULL);
+ g_assert_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED);
+ g_clear_error (&error);
+
+ g_hash_table_destroy (headers);
+}
+
+static void
+test_headers_bad (Test *test,
+ gconstpointer data)
+{
+ GHashTable *headers;
+
+ headers = web_socket_util_new_headers ();
+
+ /* Bad version */
+ g_hash_table_insert (headers, g_strdup ("Cookie"), g_strdup ("CockpitAuth=v=1;k=blah"));
+ if (cockpit_auth_check_cookie (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers)))
+ g_assert_not_reached ();
+
+ /* Bad hash */
+ g_hash_table_remove_all (headers);
+ g_hash_table_insert (headers, g_strdup ("Cookie"), g_strdup ("CockpitAuth=v=2;k=blah"));
+ if (cockpit_auth_check_cookie (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers)))
+ g_assert_not_reached ();
+
+ /* Bad encoding */
+ g_hash_table_remove_all (headers);
+ g_hash_table_insert (headers, g_strdup ("Cookie"), g_strdup ("cockpit=d"));
+ if (cockpit_auth_check_cookie (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers)))
+ g_assert_not_reached ();
+
+ g_hash_table_destroy (headers);
+}
+
+static gboolean
+on_timeout_set_flag (gpointer data)
+{
+ gboolean *flag = data;
+ g_assert (*flag == FALSE);
+ *flag = TRUE;
+ return FALSE;
+}
+
+static gboolean
+on_idling_set_flag (CockpitAuth *auth,
+ gpointer data)
+{
+ gboolean *flag = data;
+ *flag = TRUE;
+ return FALSE;
+}
+
+static void
+test_idle_timeout (Test *test,
+ gconstpointer data)
+{
+ GAsyncResult *result = NULL;
+ CockpitWebService *service;
+ JsonObject *login_response;
+ GError *error = NULL;
+ GHashTable *headers;
+ gboolean flag = FALSE;
+ gboolean idling = FALSE;
+
+ /* The idle timeout is one second */
+ g_assert (cockpit_ws_service_idle == 1);
+
+ headers = mock_auth_basic_header ("me", "this is the password");
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers),
+ on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ headers = web_socket_util_new_headers ();
+ login_response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
+ g_assert (login_response != NULL);
+ json_object_unref (login_response);
+ g_object_unref (result);
+ g_assert_no_error (error);
+
+ /* Logged in ... the webservice is idle though */
+ mock_auth_include_cookie_as_if_client (headers, headers, "cockpit");
+ service = cockpit_auth_check_cookie (test->auth, WebRequest(.path="/cockpit", .headers=headers));
+ g_assert (service != NULL);
+ g_assert (cockpit_web_service_get_idling (service));
+ g_object_unref (service);
+
+ g_signal_connect (test->auth, "idling", G_CALLBACK (on_idling_set_flag), &idling);
+
+ /* Now wait for 2 seconds, and the service should be gone */
+ g_timeout_add_seconds (2, on_timeout_set_flag, &flag);
+ while (!flag)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Timeout, no longer logged in */
+ service = cockpit_auth_check_cookie (test->auth, WebRequest(.path="/cockpit", .headers=headers));
+ g_assert (service == NULL);
+
+ /* Now wait for 3 seconds, and the auth should have said its idling */
+ flag = FALSE;
+ g_timeout_add_seconds (3, on_timeout_set_flag, &flag);
+ while (!flag)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert (idling == TRUE);
+ g_hash_table_destroy (headers);
+}
+
+static void
+test_process_timeout (Test *test,
+ gconstpointer data)
+{
+ gboolean idling = FALSE;
+
+ g_signal_connect (test->auth, "idling", G_CALLBACK (on_idling_set_flag), &idling);
+
+ while (!idling)
+ g_main_context_iteration (NULL, TRUE);
+}
+
+static void
+test_max_startups (Test *test,
+ gconstpointer data)
+{
+ GAsyncResult *result1 = NULL;
+ GAsyncResult *result2 = NULL;
+ GAsyncResult *result3 = NULL;
+
+ JsonObject *response;
+
+ GHashTable *headers_slow;
+ GHashTable *headers_fail;
+
+ GError *error1 = NULL;
+ GError *error2 = NULL;
+ GError *error3 = NULL;
+
+ cockpit_expect_message ("Request dropped; too many startup connections: 2");
+
+ headers_slow = web_socket_util_new_headers ();
+ headers_fail = web_socket_util_new_headers ();
+ g_hash_table_insert (headers_slow, g_strdup ("Authorization"), g_strdup ("testscheme failslow"));
+ g_hash_table_insert (headers_fail, g_strdup ("Authorization"), g_strdup ("testscheme fail"));
+
+ /* Slow request that takes a while to complete */
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers_slow),
+ on_ready_get_result, &result1);
+
+ /* Request that gets dropped */
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers_fail),
+ on_ready_get_result, &result2);
+ while (result2 == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ response = cockpit_auth_login_finish (test->auth, result2, NULL, NULL, &error2);
+ g_object_unref (result2);
+ g_assert (response == NULL);
+ g_assert_cmpstr ("Connection closed by host", ==, error2->message);
+
+ /* Wait for first request to finish */
+ while (result1 == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ response = cockpit_auth_login_finish (test->auth, result1, NULL, NULL, &error1);
+ g_object_unref (result1);
+ g_assert (response == NULL);
+ g_assert_cmpstr ("Authentication failed", ==, error1->message);
+
+ /* Now that first is finished we can successfully run another one */
+ g_hash_table_insert (headers_fail, g_strdup ("Authorization"), g_strdup ("testscheme fail"));
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path="/cockpit", .headers=headers_fail),
+ on_ready_get_result, &result3);
+ while (result3 == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ response = cockpit_auth_login_finish (test->auth, result3, NULL, NULL, &error3);
+ g_object_unref (result3);
+ g_assert (response == NULL);
+ g_assert_cmpstr ("Authentication failed", ==, error3->message);
+
+ g_clear_error (&error1);
+ g_clear_error (&error2);
+ g_clear_error (&error3);
+
+ g_hash_table_destroy (headers_fail);
+ g_hash_table_destroy (headers_slow);
+}
+
+typedef struct {
+ const gchar *header;
+ const gchar *error_message;
+ const gchar *warning;
+ const gchar *path;
+ int error_code;
+} ErrorFixture;
+
+typedef struct {
+ const gchar *data;
+ const gchar *warning;
+ const gchar *header;
+ const gchar *path;
+ const gchar *user;
+ const gchar *application;
+ const gchar *cookie_name;
+ gboolean expect_stored_password;
+} SuccessFixture;
+
+static void
+test_custom_fail (Test *test,
+ gconstpointer data)
+{
+ GAsyncResult *result = NULL;
+ JsonObject *response;
+ GError *error = NULL;
+ GHashTable *headers;
+ const ErrorFixture *fix = data;
+ const gchar *path = fix->path ? fix->path : "/cockpit";
+
+ if (fix->warning)
+ cockpit_expect_warning (fix->warning);
+
+ headers = web_socket_util_new_headers ();
+ g_hash_table_insert (headers, g_strdup ("Authorization"), g_strdup (fix->header));
+
+ cockpit_auth_login_async (test->auth, WebRequest(.path=path, .headers=headers), on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ headers = web_socket_util_new_headers ();
+ response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
+ g_object_unref (result);
+
+ g_assert (response == NULL);
+ if (fix->error_code)
+ g_assert_error (error, COCKPIT_ERROR, fix->error_code);
+ else
+ g_assert (error != NULL);
+
+ g_assert_cmpstr (fix->error_message, ==, error->message);
+ g_clear_error (&error);
+
+ g_hash_table_destroy (headers);
+}
+
+static void
+test_custom_timeout (Test *test,
+ gconstpointer data)
+{
+ cockpit_expect_message ("*session timed out*");
+ test_custom_fail (test, data);
+}
+
+static void
+test_bad_command (Test *test,
+ gconstpointer data)
+{
+ cockpit_expect_possible_log ("cockpit-protocol", G_LOG_LEVEL_WARNING,
+ "*couldn't recv*");
+ cockpit_expect_possible_log ("cockpit-ws", G_LOG_LEVEL_WARNING,
+ "*Auth pipe closed: internal-error*");
+ cockpit_expect_possible_log ("cockpit-ws", G_LOG_LEVEL_WARNING,
+ "*Auth pipe closed: not-found*");
+ cockpit_expect_possible_log ("cockpit-ws", G_LOG_LEVEL_WARNING,
+ "*Auth pipe closed: terminated*");
+ cockpit_expect_possible_log ("cockpit-ws", G_LOG_LEVEL_WARNING,
+ "*couldn't write: Connection refused*");
+ cockpit_expect_possible_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE,
+ "*couldn't write: Connection refused*");
+ cockpit_expect_possible_log ("cockpit-protocol", G_LOG_LEVEL_MESSAGE,
+ "*couldn't send: Connection refused*");
+ test_custom_fail (test, data);
+}
+
+static void
+test_custom_success (Test *test,
+ gconstpointer data)
+{
+ GAsyncResult *result = NULL;
+ CockpitWebService *service;
+ JsonObject *response;
+ CockpitCreds *creds;
+ GError *error = NULL;
+ GHashTable *headers;
+ JsonObject *login_data;
+ const SuccessFixture *fix = data;
+ const gchar *path = fix->path ? fix->path : "/cockpit";
+ const gchar *application = fix->application ? fix->application : "cockpit";
+
+ if (fix->warning)
+ cockpit_expect_warning (fix->warning);
+
+ headers = web_socket_util_new_headers ();
+ g_hash_table_insert (headers, g_strdup ("Authorization"), g_strdup (fix->header));
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path=path, .headers=headers),
+ on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ headers = web_socket_util_new_headers ();
+ response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
+ g_object_unref (result);
+ g_assert_no_error (error);
+ g_assert (response != NULL);
+ json_object_unref (response);
+
+ mock_auth_include_cookie_as_if_client (headers, headers,
+ fix->cookie_name ? fix->cookie_name : "cockpit");
+ service = cockpit_auth_check_cookie (test->auth, WebRequest(.path=path, .headers=headers));
+ creds = cockpit_web_service_get_creds (service);
+ g_assert_cmpstr (application, ==, cockpit_creds_get_application (creds));
+ if (fix->expect_stored_password)
+ g_assert_cmpstr ("this is the machine password", ==, g_bytes_get_data (cockpit_creds_get_password (creds), NULL));
+ else
+ g_assert_null (cockpit_creds_get_password (creds));
+
+ login_data = cockpit_creds_get_login_data (creds);
+ if (fix->data)
+ g_assert_cmpstr (json_object_get_string_member (login_data, "login"),
+ ==, fix->data);
+ else
+ g_assert_null (login_data);
+
+ g_hash_table_destroy (headers);
+ g_object_unref (service);
+}
+
+static const SuccessFixture fixture_ssh_basic = {
+ .warning = NULL,
+ .data = NULL,
+ .header = "Basic bWU6dGhpcyBpcyB0aGUgcGFzc3dvcmQ="
+};
+
+static const SuccessFixture fixture_ssh_not_authorized = {
+ .warning = NULL,
+ .data = NULL,
+ .header = "Basic bWU6dGhpcyBpcyB0aGUgcGFzc3dvcmQ=",
+};
+
+static const SuccessFixture fixture_ssh_remote_basic = {
+ .warning = NULL,
+ .data = NULL,
+ .header = "Basic cmVtb3RlLXVzZXI6dGhpcyBpcyB0aGUgbWFjaGluZSBwYXNzd29yZA==",
+ .path = "/cockpit+=machine",
+ .user = "remote-user",
+ .application = "cockpit+=machine",
+ .cookie_name = "machine-cockpit+machine",
+ .expect_stored_password = TRUE
+};
+
+static const SuccessFixture fixture_ssh_no_data = {
+ .warning = NULL,
+ .data = NULL,
+ .header = "testsshscheme success"
+};
+
+static const SuccessFixture fixture_ssh_remote_switched = {
+ .data = NULL,
+ .header = "testscheme ssh-remote-switch",
+ .path = "/cockpit+=machine",
+ .application = "cockpit+=machine",
+ .cookie_name = "machine-cockpit+machine"
+};
+
+static const SuccessFixture fixture_ssh_alt_default = {
+ .data = NULL,
+ .header = "testsshscheme ssh-alt-default",
+};
+
+static const SuccessFixture fixture_ssh_alt = {
+ .data = NULL,
+ .path = "/cockpit+=machine",
+ .application = "cockpit+=machine",
+ .header = "testsshscheme ssh-alt-machine",
+ .cookie_name = "machine-cockpit+machine"
+};
+
+static const ErrorFixture fixture_bad_conversation = {
+ .header = "X-Conversation conversation-id xxx",
+ .error_message = "Invalid conversation token"
+};
+
+static const ErrorFixture fixture_ssh_basic_failed = {
+ .error_message = "Authentication failed",
+ .header = "Basic dXNlcjp0aGlzIGlzIHRoZSBwYXNzd29yZA=="
+};
+
+static const ErrorFixture fixture_ssh_remote_basic_failed = {
+ .error_message = "Authentication failed",
+ .header = "Basic d3Jvbmc6dGhpcyBpcyB0aGUgbWFjaGluZSBwYXNzd29yZA==",
+ .path = "/cockpit+=machine"
+};
+
+static const ErrorFixture fixture_ssh_not_supported = {
+ .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ .error_message = "Authentication failed: authentication-not-supported",
+ .header = "testsshscheme not-supported",
+};
+
+static const ErrorFixture fixture_ssh_auth_failed = {
+ .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ .error_message = "Authentication failed",
+ .header = "testsshscheme ssh-fail",
+};
+
+static const ErrorFixture fixture_ssh_auth_with_error = {
+ .error_code = COCKPIT_ERROR_FAILED,
+ .error_message = "Authentication failed: unknown: detail for error",
+ .header = "testsshscheme with-error",
+};
+
+static const SuccessFixture fixture_no_cookie = {
+ .warning = NULL,
+ .data = NULL,
+ .header = "testscheme no-cookie"
+};
+
+static const SuccessFixture fixture_no_data = {
+ .warning = NULL,
+ .data = NULL,
+ .header = "testscheme success"
+};
+
+static const SuccessFixture fixture_data_then_success = {
+ .warning = NULL,
+ .data = "data",
+ .header = "testscheme data-then-success"
+};
+
+static const ErrorFixture fixture_bad_command = {
+ .error_code = COCKPIT_ERROR_FAILED,
+ .error_message = "Internal error in login process",
+ .header = "badcommand bad",
+};
+
+static const ErrorFixture fixture_auth_failed = {
+ .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ .error_message = "Authentication failed",
+ .header = "testscheme fail",
+};
+
+static const ErrorFixture fixture_auth_denied = {
+ .error_code = COCKPIT_ERROR_PERMISSION_DENIED,
+ .error_message = "Permission denied",
+ .header = "testscheme denied",
+};
+
+static const ErrorFixture fixture_auth_with_error = {
+ .error_code = COCKPIT_ERROR_FAILED,
+ .error_message = "Authentication failed: unknown: detail for error",
+ .header = "testscheme with-error",
+};
+
+static const ErrorFixture fixture_auth_none = {
+ .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ .error_message = "Authentication disabled",
+ .header = "none invalid",
+};
+
+static const ErrorFixture fixture_auth_timeout = {
+ .error_message = "Authentication failed: Timeout",
+ .header = "timeout-scheme too-slow",
+};
+
+static const ErrorFixture fixture_non_ascii = {
+ .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ .error_message = "Authentication required",
+ .header = "testscheme süccëss",
+};
+
+static const ErrorFixture fixture_tls_cert = {
+ .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ .error_message = "Authentication required",
+ .header = "tls-cert e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+};
+
+
+typedef struct {
+ const gchar **headers;
+ const gchar **prompts;
+ const gchar *error_message;
+ const gchar *message;
+ int error_code;
+ int pause;
+} ErrorMultiFixture;
+
+typedef struct {
+ const gchar **headers;
+ const gchar **prompts;
+} SuccessMultiFixture;
+
+static inline gchar *
+str_skip (gchar *v,
+ gchar c)
+{
+ while (v[0] == c)
+ v++;
+ return v;
+}
+
+static gboolean
+parse_login_reply_challenge (GHashTable *headers,
+ gchar **out_id,
+ gchar **out_prompt)
+{
+ gchar *original = NULL;
+ gchar *line;
+ gchar *next;
+ gchar *id = NULL;
+ gchar *prompt = NULL;
+ gboolean ret = FALSE;
+ gpointer key = NULL;
+ gsize length;
+
+ if (!g_hash_table_lookup_extended (headers, "WWW-Authenticate", &key, (gpointer *)&original))
+ goto out;
+
+ line = original;
+
+ // Check challenge type
+ line = str_skip (line, ' ');
+ if (g_ascii_strncasecmp (line, "X-Conversation ", strlen("X-Conversation ")) != 0)
+ goto out;
+
+ next = strchr (line, ' ');
+ if (!next)
+ goto out;
+
+ // Get id
+ line = next;
+ line = str_skip (line, ' ');
+ next = strchr (line, ' ');
+ if (!next)
+ goto out;
+ id = g_strndup (line, next - line);
+
+ // Rest should be the base64 prompt
+ next = str_skip (next, ' ');
+ prompt = g_strdup (next);
+ if (g_base64_decode_inplace (prompt, &length) == NULL)
+ goto out;
+ prompt[length] = '\0';
+ ret = TRUE;
+
+out:
+ if (ret)
+ {
+ *out_id = id;
+ *out_prompt = prompt;
+ }
+ else
+ {
+ g_warning ("Got invalid WWW-Authenticate header: %s", original);
+ g_free (id);
+ g_free (prompt);
+ }
+
+ return ret;
+}
+
+static void
+test_multi_step_success (Test *test,
+ gconstpointer data)
+{
+ CockpitWebService *service;
+ CockpitCreds *creds;
+ GHashTable *headers = NULL;
+ gint spot = 0;
+ gchar *id = NULL;
+
+ const SuccessMultiFixture *fix = data;
+
+ for (spot = 0; fix->headers[spot]; spot++)
+ {
+ GAsyncResult *result = NULL;
+ JsonObject *response = NULL;
+ GError *error = NULL;
+ const gchar *header = fix->headers[spot];
+ const gchar *expect_prompt = fix->prompts[spot];
+ gchar *out = NULL;
+ gchar *prompt = NULL;
+
+ headers = web_socket_util_new_headers ();
+ if (id)
+ {
+ g_assert (id != NULL);
+ out = g_base64_encode ((guint8 *)header, strlen (header));
+ g_hash_table_insert (headers, g_strdup ("Authorization"),
+ g_strdup_printf ("X-Conversation %s %s", id, out));
+ g_free (id);
+ g_free (out);
+ out = NULL;
+ id = NULL;
+ }
+ else
+ {
+ g_hash_table_insert (headers, g_strdup ("Authorization"),
+ g_strdup (header));
+ }
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path="/cockpit/", .headers=headers),
+ on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ headers = web_socket_util_new_headers ();
+ g_assert (headers);
+ response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
+ g_object_unref (result);
+
+ /* Confirm we got the right prompt */
+ if (expect_prompt)
+ {
+ g_assert (prompt == NULL);
+ g_assert (id == NULL);
+
+ g_assert (parse_login_reply_challenge (headers, &id, &prompt));
+ g_assert_cmpstr (expect_prompt, ==, prompt);
+ g_assert (id != NULL);
+ g_free (prompt);
+ prompt = NULL;
+
+ g_assert_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED);
+ g_clear_error (&error);
+
+ g_hash_table_unref (headers);
+ }
+ else
+ {
+ g_assert_no_error (error);
+ }
+
+ if (response)
+ json_object_unref (response);
+ }
+
+ mock_auth_include_cookie_as_if_client (headers, headers, "cockpit");
+ service = cockpit_auth_check_cookie (test->auth, WebRequest(.path="/cockpit", .headers=headers));
+ creds = cockpit_web_service_get_creds (service);
+ g_assert_cmpstr ("cockpit", ==, cockpit_creds_get_application (creds));
+ g_assert_null (cockpit_creds_get_password (creds));
+ g_hash_table_destroy (headers);
+ g_object_unref (service);
+ g_free (id);
+}
+
+static void
+test_multi_step_fail (Test *test,
+ gconstpointer data)
+{
+ GHashTable *headers = NULL;
+ gint spot = 0;
+ g_autofree gchar *id = NULL;
+ gchar *prompt = NULL;
+ const ErrorMultiFixture *fix = data;
+
+ if (fix->message)
+ cockpit_expect_message (fix->message);
+
+ for (spot = 0; fix->headers[spot]; spot++)
+ {
+ GAsyncResult *result = NULL;
+ JsonObject *response = NULL;
+ GError *error = NULL;
+ const gchar *header = fix->headers[spot];
+ const gchar *expect_prompt = fix->prompts[spot];
+ gchar *out = NULL;
+ gboolean ready_for_next = TRUE;
+
+ headers = web_socket_util_new_headers ();
+ if (id)
+ {
+ g_assert (id != NULL);
+ out = g_base64_encode ((guint8 *)header, strlen (header));
+ g_hash_table_insert (headers, g_strdup ("Authorization"),
+ g_strdup_printf ("X-Conversation %s %s", id, out));
+ g_free (id);
+ g_free (out);
+ out = NULL;
+ id = NULL;
+ }
+ else
+ {
+ g_hash_table_insert (headers, g_strdup ("Authorization"),
+ g_strdup (header));
+ }
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path="/cockpit/", .headers=headers),
+ on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ headers = web_socket_util_new_headers ();
+ response = cockpit_auth_login_finish (test->auth, result, NULL, headers, &error);
+ g_object_unref (result);
+ g_assert (error != NULL);
+
+ /* Confirm we got the right prompt */
+ if (expect_prompt)
+ {
+ g_assert (prompt == NULL);
+ g_assert (id == NULL);
+
+ g_assert (parse_login_reply_challenge (headers, &id, &prompt));
+ g_assert_cmpstr (expect_prompt, ==, prompt);
+ g_assert (id != NULL);
+ g_free (prompt);
+ prompt = NULL;
+ if (fix->pause)
+ {
+ ready_for_next = FALSE;
+ g_timeout_add_seconds (fix->pause, on_timeout_set_flag, &ready_for_next);
+ }
+
+ while (ready_for_next == FALSE)
+ g_main_context_iteration (NULL, TRUE);
+
+ if (response)
+ json_object_unref (response);
+ g_hash_table_unref (headers);
+
+ g_assert_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED);
+ g_clear_error (&error);
+ }
+ else
+ {
+ g_assert (response == NULL);
+ if (fix->error_code)
+ g_assert_error (error, COCKPIT_ERROR, fix->error_code);
+ else
+ g_assert (error != NULL);
+
+ g_assert_cmpstr (fix->error_message, ==, error->message);
+ g_clear_error (&error);
+ break;
+ }
+ }
+
+ if (headers)
+ g_hash_table_destroy (headers);
+}
+
+const gchar *two_steps[3] = { "testscheme two-step", "two", NULL };
+const gchar *two_prompts[2] = { "type two", NULL };
+const gchar *three_steps[4] = { "testscheme three-step", "two", "three", NULL };
+const gchar *three_steps_ssh[4] = { "testsshscheme three-step", "two", "three", NULL };
+const gchar *three_prompts[3] = { "type two", "type three", NULL };
+
+static const SuccessMultiFixture fixture_two_steps = {
+ .headers = two_steps,
+ .prompts = two_prompts,
+};
+
+static const SuccessMultiFixture fixture_three_steps = {
+ .headers = three_steps,
+ .prompts = three_prompts,
+};
+
+static const SuccessMultiFixture fixture_ssh_three_steps = {
+ .headers = three_steps_ssh,
+ .prompts = three_prompts,
+};
+
+const gchar *two_steps_ssh_wrong[3] = { "testsshscheme two-step", "bad", NULL };
+const gchar *two_steps_wrong[3] = { "testscheme two-step", "bad", NULL };
+const gchar *three_steps_wrong[4] = { "testscheme three-step", "two", "bad", NULL };
+
+static const ErrorMultiFixture fixture_fail_three_steps = {
+ .headers = three_steps_wrong,
+ .prompts = three_prompts,
+ .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ .error_message = "Authentication failed",
+};
+
+static const ErrorMultiFixture fixture_fail_two_steps = {
+ .headers = two_steps_wrong,
+ .prompts = two_prompts,
+ .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ .error_message = "Authentication failed",
+};
+
+static const ErrorMultiFixture fixture_fail_ssh_two_steps = {
+ .headers = two_steps_ssh_wrong,
+ .prompts = two_prompts,
+ .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ .error_message = "Authentication failed",
+};
+
+static const ErrorMultiFixture fixture_fail_step_timeout = {
+ .headers = two_steps,
+ .prompts = two_prompts,
+ .error_code = COCKPIT_ERROR_AUTHENTICATION_FAILED,
+ .error_message = "Invalid conversation token",
+ .message = "*session timed out during authentication*",
+ .pause = 3,
+};
+
+
+typedef struct {
+ const gchar *str;
+ guint max_startups;
+ guint max_startups_rate;
+ guint max_startups_begin;
+ gboolean warn;
+} StartupFixture;
+
+static void
+setup_startups (Test *test,
+ gconstpointer data)
+{
+ const StartupFixture *fix = data;
+ cockpit_config_file = SRCDIR "does-not-exist";
+ cockpit_ws_max_startups = fix->str;
+ if (fix->warn)
+ cockpit_expect_warning ("Illegal MaxStartups spec*");
+
+ test->auth = cockpit_auth_new (FALSE, COCKPIT_AUTH_NONE);
+}
+
+static void
+teardown_startups (Test *test,
+ gconstpointer data)
+{
+ cockpit_assert_expected ();
+ g_object_unref (test->auth);
+}
+
+static const StartupFixture fixture_normal = {
+ .str = "20:50:200",
+ .max_startups = 200,
+ .max_startups_begin = 20,
+ .max_startups_rate = 50,
+ .warn = FALSE,
+};
+
+static const StartupFixture fixture_single = {
+ .str = "20",
+ .max_startups = 20,
+ .max_startups_begin = 20,
+ .max_startups_rate = 100,
+ .warn = FALSE,
+};
+
+static const StartupFixture fixture_double = {
+ .str = "20:50",
+ .max_startups = 20,
+ .max_startups_begin = 20,
+ .max_startups_rate = 100,
+ .warn = FALSE,
+};
+
+static const StartupFixture fixture_unlimited = {
+ .str = "0",
+ .max_startups = 0,
+ .max_startups_begin = 0,
+ .max_startups_rate = 100,
+ .warn = FALSE,
+};
+
+static const StartupFixture fixture_bad = {
+ .str = "bad",
+ .max_startups = 10,
+ .max_startups_begin = 10,
+ .max_startups_rate = 100,
+ .warn = TRUE,
+};
+
+static const StartupFixture fixture_bad_rate = {
+ .str = "20:101:40",
+ .max_startups = 10,
+ .max_startups_begin = 10,
+ .max_startups_rate = 100,
+ .warn = TRUE,
+};
+
+static const StartupFixture fixture_bad_startups = {
+ .str = "40:101:20",
+ .max_startups = 10,
+ .max_startups_begin = 10,
+ .max_startups_rate = 100,
+ .warn = TRUE,
+};
+
+static const StartupFixture fixture_bad_negative = {
+ .str = "-40:101:20",
+ .max_startups = 10,
+ .max_startups_begin = 10,
+ .max_startups_rate = 100,
+ .warn = TRUE,
+};
+
+static const StartupFixture fixture_bad_too_many = {
+ .str = "40:101:20:50:50",
+ .max_startups = 10,
+ .max_startups_begin = 10,
+ .max_startups_rate = 100,
+ .warn = TRUE,
+};
+
+static void
+test_max_startups_conf (Test *test,
+ gconstpointer data)
+{
+ const StartupFixture *fix = data;
+ g_assert_cmpuint (fix->max_startups_begin, ==, test->auth->max_startups_begin);
+ g_assert_cmpuint (fix->max_startups, ==, test->auth->max_startups);
+ g_assert_cmpuint (fix->max_startups_rate, ==, test->auth->max_startups_rate);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_ws_session_program = BUILDDIR "/mock-auth-command";
+ cockpit_ws_service_idle = 1;
+
+ cockpit_setenv_check ("COCKPIT_WS_PROCESS_IDLE", "2", TRUE);
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/auth/application", Test, NULL, NULL, test_application, NULL);
+ g_test_add ("/auth/userpass-header-check", Test, NULL, setup, test_userpass_cookie_check, teardown);
+ g_test_add ("/auth/userpass-store-check", Test, &fixture_superuser_any, setup, test_userpass_cookie_check, teardown);
+ g_test_add ("/auth/userpass-bad", Test, NULL, setup, test_userpass_bad, teardown);
+ g_test_add ("/auth/userpass-emptypass", Test, NULL, setup, test_userpass_emptypass, teardown);
+ g_test_add ("/auth/headers-bad", Test, NULL, setup, test_headers_bad, teardown);
+ g_test_add ("/auth/idle-timeout", Test, NULL, setup, test_idle_timeout, teardown);
+ g_test_add ("/auth/process-timeout", Test, NULL, setup, test_process_timeout, teardown);
+ g_test_add ("/auth/bad-coversation", Test, &fixture_bad_conversation,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/custom-success", Test, &fixture_no_data,
+ setup_normal, test_custom_success, teardown_normal);
+ g_test_add ("/auth/custom-no-cookie-success", Test, &fixture_no_cookie,
+ setup_normal, test_custom_success, teardown_normal);
+ g_test_add ("/auth/custom-data-then-success", Test, &fixture_data_then_success,
+ setup_normal, test_custom_success, teardown_normal);
+ g_test_add ("/auth/custom-fail-auth", Test, &fixture_auth_failed,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/custom-denied-auth", Test, &fixture_auth_denied,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/custom-with-error", Test, &fixture_auth_with_error,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/custom-timeout", Test, &fixture_auth_timeout,
+ setup_normal, test_custom_timeout, teardown_normal);
+ g_test_add ("/auth/non-ascii", Test, &fixture_non_ascii,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/bogus-tls-cert", Test, &fixture_tls_cert,
+ setup_normal, test_custom_fail, teardown_normal);
+
+ g_test_add ("/auth/custom-ssh-basic-success", Test, &fixture_ssh_basic,
+ setup_normal, test_custom_success, teardown_normal);
+ g_test_add ("/auth/custom-ssh-basic-success-not-authorized", Test, &fixture_ssh_not_authorized,
+ setup_normal, test_custom_success, teardown_normal);
+ g_test_add ("/auth/custom-ssh-remote-basic-success", Test, &fixture_ssh_remote_basic,
+ setup_normal, test_custom_success, teardown_normal);
+ g_test_add ("/auth/custom-ssh-remote-switched", Test, &fixture_ssh_remote_switched,
+ setup_normal, test_custom_success, teardown_normal);
+ g_test_add ("/auth/custom-ssh-with-conf-default", Test, &fixture_ssh_alt_default,
+ setup_alt_config, test_custom_success, teardown_normal);
+ g_test_add ("/auth/custom-ssh-with-conf-allow", Test, &fixture_ssh_alt,
+ setup_alt_config, test_custom_success, teardown_normal);
+ g_test_add ("/auth/custom-ssh-success", Test, &fixture_ssh_no_data,
+ setup_normal, test_custom_success, teardown_normal);
+ g_test_add ("/auth/custom-ssh-fail-auth", Test, &fixture_ssh_auth_failed,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/custom-ssh-fail-basic-auth", Test, &fixture_ssh_basic_failed,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/custom-ssh-remote-fail-basic-auth", Test, &fixture_ssh_remote_basic_failed,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/custom-ssh-not-supported", Test, &fixture_ssh_not_supported,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/custom-ssh-with-error", Test, &fixture_ssh_auth_with_error,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/success-ssh-multi-step-three", Test, &fixture_ssh_three_steps,
+ setup_normal, test_multi_step_success, teardown_normal);
+ g_test_add ("/auth/fail-ssh-multi-step-two", Test, &fixture_fail_ssh_two_steps,
+ setup_normal, test_multi_step_fail, teardown_normal);
+
+ g_test_add ("/auth/none", Test, &fixture_auth_none,
+ setup_normal, test_custom_fail, teardown_normal);
+ g_test_add ("/auth/bad-command", Test, &fixture_bad_command,
+ setup_normal, test_bad_command, teardown_normal);
+ g_test_add ("/auth/success-multi-step-two", Test, &fixture_two_steps,
+ setup_normal, test_multi_step_success, teardown_normal);
+ g_test_add ("/auth/success-multi-step-three", Test, &fixture_three_steps,
+ setup_normal, test_multi_step_success, teardown_normal);
+ g_test_add ("/auth/fail-multi-step-two", Test, &fixture_fail_two_steps,
+ setup_normal, test_multi_step_fail, teardown_normal);
+ g_test_add ("/auth/fail-multi-step-three", Test, &fixture_fail_three_steps,
+ setup_normal, test_multi_step_fail, teardown_normal);
+ g_test_add ("/auth/fail-multi-step-timeout", Test, &fixture_fail_step_timeout,
+ setup_normal, test_multi_step_fail, teardown_normal);
+ g_test_add ("/auth/max-startups", Test, NULL,
+ setup_normal, test_max_startups, teardown_normal);
+ g_test_add ("/auth/max-startups-normal", Test, &fixture_normal,
+ setup_startups, test_max_startups_conf, teardown_startups);
+ g_test_add ("/auth/max-startups-single", Test, &fixture_single,
+ setup_startups, test_max_startups_conf, teardown_startups);
+ g_test_add ("/auth/max-startups-double", Test, &fixture_double,
+ setup_startups, test_max_startups_conf, teardown_startups);
+ g_test_add ("/auth/max-startups-unlimited", Test, &fixture_unlimited,
+ setup_startups, test_max_startups_conf, teardown_startups);
+ g_test_add ("/auth/max-startups-bad", Test, &fixture_bad,
+ setup_startups, test_max_startups_conf, teardown_startups);
+ g_test_add ("/auth/max-startups-bad-rate", Test, &fixture_bad_rate,
+ setup_startups, test_max_startups_conf, teardown_startups);
+ g_test_add ("/auth/max-startups-bad-startups", Test, &fixture_bad_startups,
+ setup_startups, test_max_startups_conf, teardown_startups);
+ g_test_add ("/auth/max-startups-bad-negative", Test, &fixture_bad_negative,
+ setup_startups, test_max_startups_conf, teardown_startups);
+ g_test_add ("/auth/max-startups-too-many", Test, &fixture_bad_too_many,
+ setup_startups, test_max_startups_conf, teardown_startups);
+ return g_test_run ();
+}
diff --git a/src/ws/test-authssh.c b/src/ws/test-authssh.c
new file mode 100644
index 0000000..3c73515
--- /dev/null
+++ b/src/ws/test-authssh.c
@@ -0,0 +1,277 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitauth.h"
+#include "cockpitws.h"
+
+#include "common/cockpitsystem.h"
+#include "common/cockpitwebserver.h"
+#include "common/cockpiterror.h"
+#include "common/cockpitwebrequest-private.h"
+
+#include "testlib/cockpittest.h"
+#include "testlib/mock-auth.h"
+
+#include <sys/wait.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define PASSWORD "this is the password"
+
+typedef struct {
+ CockpitAuth *auth;
+
+ /* setup_mock_sshd */
+ GPid mock_sshd;
+ guint16 ssh_port;
+} TestCase;
+
+typedef struct {
+ const char *header;
+} TestFixture;
+
+static GString *
+read_all_into_string (int fd)
+{
+ GString *input = g_string_new ("");
+ gsize len;
+ gssize ret;
+
+ for (;;)
+ {
+ len = input->len;
+ g_string_set_size (input, len + 256);
+ ret = read (fd, input->str + len, 256);
+ if (ret < 0)
+ {
+ if (errno != EAGAIN)
+ {
+ g_critical ("couldn't read from mock input: %s", g_strerror (errno));
+ g_string_free (input, TRUE);
+ return NULL;
+ }
+ }
+ else if (ret == 0)
+ {
+ return input;
+ }
+ else
+ {
+ input->len = len + ret;
+ input->str[input->len] = '\0';
+ }
+ }
+}
+
+static void
+setup_mock_sshd (TestCase *tc)
+{
+ GError *error = NULL;
+ GString *port;
+ gchar *endptr;
+ guint64 value;
+ gint out_fd;
+
+ const gchar *argv[] = {
+ BUILDDIR "/mock-sshd",
+ "--user", "me",
+ "--password", PASSWORD,
+ NULL
+ };
+
+ g_spawn_async_with_pipes (BUILDDIR, (gchar **)argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL,
+ &tc->mock_sshd, NULL, &out_fd, NULL, &error);
+ g_assert_no_error (error);
+
+ /*
+ * mock-sshd prints its port on stdout, and then closes stdout
+ * This also lets us know when it has initialized.
+ */
+
+ port = read_all_into_string (out_fd);
+ g_assert (port != NULL);
+ close (out_fd);
+ g_assert_no_error (error);
+
+ g_strstrip (port->str);
+ value = g_ascii_strtoull (port->str, &endptr, 10);
+ if (!endptr || *endptr != '\0' || value == 0 || value > G_MAXUSHORT)
+ g_critical ("invalid port printed by mock-sshd: %s", port->str);
+
+ tc->ssh_port = (gushort)value;
+ g_string_free (port, TRUE);
+}
+
+static void
+setup (TestCase *tc,
+ gconstpointer data)
+{
+ tc->auth = cockpit_auth_new (TRUE, COCKPIT_AUTH_NONE);
+ setup_mock_sshd (tc);
+}
+
+static void
+teardown (TestCase *tc,
+ gconstpointer data)
+{
+ if (tc->mock_sshd)
+ {
+ kill (tc->mock_sshd, SIGTERM);
+ g_assert_cmpint (waitpid (tc->mock_sshd, 0, 0), ==, tc->mock_sshd);
+ g_spawn_close_pid (tc->mock_sshd);
+ }
+
+ g_clear_object (&tc->auth);
+}
+
+static void
+on_ready_get_result (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **retval = user_data;
+ g_assert (retval != NULL);
+ g_assert (*retval == NULL);
+ *retval = g_object_ref (result);
+}
+
+static void
+test_basic_good (TestCase *test,
+ gconstpointer data)
+{
+ GHashTable *in_headers;
+ GHashTable *out_headers;
+ GAsyncResult *result = NULL;
+ CockpitCreds *creds;
+ CockpitWebService *service;
+ JsonObject *response;
+ GError *error = NULL;
+ gchar *path = NULL;
+ gchar *application = NULL;
+ gchar *cookie = NULL;
+
+ in_headers = mock_auth_basic_header ("me", PASSWORD);
+ g_hash_table_insert (in_headers, g_strdup ("X-Authorize"), g_strdup ("password"));
+ out_headers = cockpit_web_server_new_table ();
+
+ application = g_strdup_printf ("cockpit+=127.0.0.1:%d", test->ssh_port);
+ cookie = g_strdup_printf ("machine-cockpit+127.0.0.1:%d", test->ssh_port);
+ path = g_strdup_printf ("/%s", application);
+
+ cockpit_auth_login_async (test->auth, WebRequest(.path=path, .headers=in_headers), on_ready_get_result, &result);
+ g_hash_table_unref (in_headers);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ response = cockpit_auth_login_finish (test->auth, result, NULL, out_headers, &error);
+ g_object_unref (result);
+ g_assert_no_error (error);
+ g_assert (response != NULL);
+ json_object_unref (response);
+
+ mock_auth_include_cookie_as_if_client (out_headers, out_headers, cookie);
+ service = cockpit_auth_check_cookie (test->auth, WebRequest(.path=path, .headers=out_headers));
+ g_assert (service != NULL);
+
+ creds = cockpit_web_service_get_creds (service);
+ g_assert_cmpstr (application, ==, cockpit_creds_get_application (creds));
+ g_assert_null (cockpit_creds_get_password (creds));
+
+ g_hash_table_unref (out_headers);
+ g_object_unref (service);
+ g_free (cookie);
+ g_free (path);
+ g_free (application);
+}
+
+static const TestFixture fixture_bad_format = {
+ .header = "Basic d3JvbmctZm9ybWF0Cg=="
+};
+
+static const TestFixture fixture_wrong_pw = {
+ .header = "Basic bWU6d3JvbmcK"
+};
+
+static const TestFixture fixture_empty = {
+ .header = "Basic"
+};
+
+static void
+test_basic_fail (TestCase *test,
+ gconstpointer data)
+{
+ GHashTable *headers;
+ GAsyncResult *result = NULL;
+ GError *error = NULL;
+ gchar *path = NULL;
+ gchar *application = NULL;
+ const TestFixture *fix = data;
+
+ headers = cockpit_web_server_new_table ();
+ g_hash_table_insert (headers, g_strdup ("Authorization"), g_strdup (fix->header));
+ g_hash_table_insert (headers, g_strdup ("X-Authorize"), g_strdup ("password"));
+
+ application = g_strdup_printf ("cockpit+=127.0.0.1:%d", test->ssh_port);
+ path = g_strdup_printf ("/%s", application);
+
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path=path, .headers=headers),
+ on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+ headers = cockpit_web_server_new_table ();
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert_null (cockpit_auth_login_finish (test->auth, result, NULL, headers, &error));
+ g_object_unref (result);
+ g_assert_error (error, COCKPIT_ERROR, COCKPIT_ERROR_AUTHENTICATION_FAILED);
+ g_assert (g_str_has_prefix (error->message, "Authentication failed"));
+
+ g_clear_error (&error);
+ g_hash_table_unref (headers);
+ g_free (path);
+ g_free (application);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_ws_ssh_program = BUILDDIR "/cockpit-ssh";
+
+ cockpit_setenv_check ("COCKPIT_SSH_KNOWN_HOSTS_FILE", SRCDIR "/src/ssh/mock_known_hosts", TRUE);
+ cockpit_setenv_check ("COCKPIT_SSH_BRIDGE_COMMAND", BUILDDIR "/cockpit-bridge", TRUE);
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/auth-ssh/basic-good", TestCase, NULL,
+ setup, test_basic_good, teardown);
+ g_test_add ("/auth-ssh/basic-bad-password", TestCase, &fixture_wrong_pw,
+ setup, test_basic_fail, teardown);
+ g_test_add ("/auth-ssh/basic-bad-format", TestCase, &fixture_bad_format,
+ setup, test_basic_fail, teardown);
+ g_test_add ("/auth-ssh/basic-empty", TestCase, &fixture_empty,
+ setup, test_basic_fail, teardown);
+ return g_test_run ();
+}
diff --git a/src/ws/test-channelresponse.c b/src/ws/test-channelresponse.c
new file mode 100644
index 0000000..46288db
--- /dev/null
+++ b/src/ws/test-channelresponse.c
@@ -0,0 +1,1161 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitws.h"
+#include "cockpitcreds.h"
+#include "cockpitchannelresponse.h"
+
+#include "common/cockpitpipetransport.h"
+#include "common/cockpittransport.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitwebserver.h"
+#include "common/cockpitconf.h"
+
+#include "websocket/websocket.h"
+
+#include <glib.h>
+
+#include "testlib/cockpittest.h"
+#include "testlib/mock-auth.h"
+
+#include <string.h>
+#include <errno.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <sys/syscall.h>
+#include <sys/poll.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+#ifndef SYS_pidfd_open
+#define SYS_pidfd_open 434 // same on every arch
+#endif
+
+/*
+ * To recalculate the checksums found in this file, do something like:
+ * $ XDG_DATA_DIRS=$PWD/src/bridge/mock-resource/system/ XDG_DATA_HOME=/nonexistent ./cockpit-bridge --packages
+ */
+#define CHECKSUM "$9a9ee8f5711446a46289cd1451c2a7125fb586456884b96807401ac2f055e669"
+
+#define PASSWORD "this is the password"
+
+/* headers that are present in every request */
+#define STATIC_HEADERS "X-Content-Type-Options: nosniff\r\nX-DNS-Prefetch-Control: off\r\nReferrer-Policy: no-referrer\r\nCross-Origin-Resource-Policy: same-origin\r\nX-Frame-Options: sameorigin\r\n"
+
+typedef struct {
+ CockpitWebService *service;
+ GIOStream *io;
+ GMemoryOutputStream *output;
+ CockpitPipe *pipe;
+ GHashTable *headers;
+} TestResourceCase;
+
+typedef struct {
+ const gchar *xdg_data_home;
+ gboolean org_path;
+} TestResourceFixture;
+
+static gboolean
+on_transport_control (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload,
+ gpointer data)
+{
+ gboolean *flag = data;
+ g_assert (flag != NULL);
+
+ if (g_str_equal (command, "init"))
+ *flag = TRUE;
+
+ return FALSE;
+}
+
+static void
+setup_resource (TestResourceCase *tc,
+ gconstpointer data)
+{
+ const TestResourceFixture *fixture = data;
+ CockpitTransport *transport;
+ GInputStream *input;
+ GOutputStream *output;
+ CockpitCreds *creds;
+ gchar **environ;
+ const gchar *user;
+ const gchar *home = NULL;
+ gboolean ready = FALSE;
+ GBytes *password;
+ gulong handler;
+
+ const gchar *argv[] = {
+ BUILDDIR "/cockpit-bridge",
+ NULL
+ };
+
+ environ = g_get_environ ();
+ environ = g_environ_setenv (environ, "XDG_DATA_DIRS", SRCDIR "/src/bridge/mock-resource/system", TRUE);
+
+ if (fixture)
+ home = fixture->xdg_data_home;
+ if (!home)
+ home = SRCDIR "/src/bridge/mock-resource/home";
+ environ = g_environ_setenv (environ, "XDG_DATA_HOME", home, TRUE);
+
+ /* Start up a cockpit-bridge here */
+ tc->pipe = cockpit_pipe_spawn (argv, (const gchar **)environ, NULL, COCKPIT_PIPE_FLAGS_NONE);
+
+ g_strfreev (environ);
+
+ user = g_get_user_name ();
+ password = g_bytes_new_take (g_strdup (PASSWORD), strlen (PASSWORD));
+ creds = cockpit_creds_new ("cockpit", COCKPIT_CRED_USER, user, COCKPIT_CRED_PASSWORD, password, NULL);
+ g_bytes_unref (password);
+
+ transport = cockpit_pipe_transport_new (tc->pipe);
+ tc->service = cockpit_web_service_new (creds, transport);
+
+ /* Manually created services won't be init'd yet, wait for that before sending data */
+ handler = g_signal_connect (transport, "control", G_CALLBACK (on_transport_control), &ready);
+
+ while (!ready)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (transport);
+
+ cockpit_creds_unref (creds);
+
+ input = g_memory_input_stream_new_from_data ("", 0, NULL);
+ output = g_memory_output_stream_new (NULL, 0, g_realloc, g_free);
+ tc->io = g_simple_io_stream_new (input, output);
+ tc->output = G_MEMORY_OUTPUT_STREAM (output);
+ g_object_unref (input);
+
+ tc->headers = cockpit_web_server_new_table ();
+ g_hash_table_insert (tc->headers, g_strdup ("Accept-Encoding"), g_strdup ("gzip, identity"));
+
+ g_signal_handler_disconnect (transport, handler);
+}
+
+static void
+teardown_resource (TestResourceCase *tc,
+ gconstpointer data)
+{
+ cockpit_assert_expected ();
+
+ g_hash_table_unref (tc->headers);
+
+ g_object_add_weak_pointer (G_OBJECT (tc->service), (gpointer *)&tc->service);
+ g_object_unref (tc->service);
+ g_assert (tc->service == NULL);
+
+ g_object_unref (tc->io);
+ g_object_unref (tc->output);
+ g_object_unref (tc->pipe);
+}
+
+static gboolean
+str_contains_strv (const gchar *haystack, const gchar *sewing_kit, const gchar *delim)
+{
+ gchar **needles;
+ gboolean result = TRUE;
+ if (strlen (haystack) != strlen (sewing_kit))
+ {
+ fprintf(stderr, "Length of '%s' doesn't match '%s'\n", haystack, sewing_kit);
+ return FALSE;
+ }
+
+ needles = g_strsplit (sewing_kit, delim, 0);
+ for (guint i = 0; i < g_strv_length (needles) && result; ++i)
+ result &= strstr (haystack, needles[i]) != NULL;
+ g_strfreev (needles);
+ if (!result)
+ fprintf(stderr, "String '%s' doesn't contain each element in '%s'\n",
+ haystack, sewing_kit);
+ return result;
+}
+
+static void
+test_resource_simple (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *url = "/@localhost/another/test.html";
+ const gchar *expected =
+ "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "Content-Security-Policy: default-src 'self' http://localhost; connect-src 'self' http://localhost ws://localhost; form-action 'self' http://localhost; base-uri 'self' http://localhost; object-src 'none'; font-src 'self' http://localhost data:; img-src 'self' http://localhost data:; block-all-mixed-content\r\n"
+ "Content-Type: text/html\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "Access-Control-Allow-Origin: http://localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "52\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>In home dir</title>\n"
+ "</head>\n"
+ "<body>In home dir</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n";
+
+
+ response = cockpit_web_response_new (tc->io, url, url, NULL, "GET", NULL);
+
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "@localhost", "/another/test.html");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n"
+ "52\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>In home dir</title>\n"
+ "</head>\n"
+ "<body>In home dir</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_simple_host (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *url = "/@localhost/another/test.html";
+ const gchar *expected =
+ "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "Content-Security-Policy: default-src 'self' http://my.host; connect-src 'self' http://my.host ws://my.host; form-action 'self' http://my.host; base-uri 'self' http://my.host; object-src 'none'; font-src 'self' http://my.host data:; img-src 'self' http://my.host data:; block-all-mixed-content\r\n"
+ "Content-Type: text/html\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "Access-Control-Allow-Origin: http://my.host\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "52\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>In home dir</title>\n"
+ "</head>\n"
+ "<body>In home dir</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n";
+
+ g_hash_table_insert (tc->headers, g_strdup ("Host"), g_strdup ("my.host"));
+ response = cockpit_web_response_new (tc->io, url, url, NULL, "GET", NULL);
+
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "@localhost", "/another/test.html");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n"
+ "52\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>In home dir</title>\n"
+ "</head>\n"
+ "<body>In home dir</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_language (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ gchar *url = "/@localhost/another/test.html";
+ const gchar *expected = "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "Content-Security-Policy: default-src 'self' http://localhost; connect-src 'self' http://localhost ws://localhost; form-action 'self' http://localhost; base-uri 'self' http://localhost; object-src 'none'; font-src 'self' http://localhost data:; img-src 'self' http://localhost data:; block-all-mixed-content\r\n"
+ "Content-Type: text/html\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "Access-Control-Allow-Origin: http://localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "60\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>Inlay omehay irday</title>\n"
+ "</head>\n"
+ "<body>Inlay omehay irday</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n";
+
+ response = cockpit_web_response_new (tc->io, url, url, NULL, "GET", NULL);
+
+ g_hash_table_insert (tc->headers, g_strdup ("Accept-Language"), g_strdup ("pig, blah"));
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "@localhost", "/another/test.html");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n"
+ "60\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>Inlay omehay irday</title>\n"
+ "</head>\n"
+ "<body>Inlay omehay irday</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n");
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_cookie (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *url = "/@localhost/another/test.html";
+ const gchar *expected = "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "Content-Security-Policy: default-src 'self' http://localhost; connect-src 'self' http://localhost ws://localhost; form-action 'self' http://localhost; base-uri 'self' http://localhost; object-src 'none'; font-src 'self' http://localhost data:; img-src 'self' http://localhost data:; block-all-mixed-content\r\n"
+ "Content-Type: text/html\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "Access-Control-Allow-Origin: http://localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "60\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>Inlay omehay irday</title>\n"
+ "</head>\n"
+ "<body>Inlay omehay irday</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n";
+
+ response = cockpit_web_response_new (tc->io, url, url, NULL, "GET", NULL);
+
+ g_hash_table_insert (tc->headers, g_strdup ("Cookie"), g_strdup ("CockpitLang=pig"));
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "@localhost", "/another/test.html");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str, "*\r\n"
+ "60\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>Inlay omehay irday</title>\n"
+ "</head>\n"
+ "<body>Inlay omehay irday</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_not_found (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *url = "/cockpit/another@localhost/not-exist";
+ const gchar *expected = "HTTP/1.1 404 Not Found\r\n"
+ "Content-Type: text/html; charset=utf8\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ STATIC_HEADERS
+ "\r\n13\r\n"
+ "<html><head><title>\r\n9\r\n"
+ "Not Found\r\n15\r\n"
+ "</title></head><body>\r\n9\r\n"
+ "Not Found\r\nf\r\n"
+ "</body></html>\n\r\n0\r\n\r\n";
+
+ response = cockpit_web_response_new (tc->io, url, url, NULL, "GET", NULL);
+
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "another@localhost", "/not-exist");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str, "*\r\n13\r\n"
+ "<html><head><title>\r\n9\r\n"
+ "Not Found\r\n15\r\n"
+ "</title></head><body>\r\n9\r\n"
+ "Not Found\r\nf\r\n"
+ "</body></html>\n\r\n0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_no_path (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *url = "/cockpit/another@localhost";
+ const gchar *expected = "HTTP/1.1 404 Not Found\r\n"
+ "Content-Type: text/html; charset=utf8\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ STATIC_HEADERS
+ "\r\n13\r\n"
+ "<html><head><title>\r\n9\r\n"
+ "Not Found\r\n15\r\n"
+ "</title></head><body>\r\n9\r\n"
+ "Not Found\r\nf\r\n"
+ "</body></html>\n\r\n0\r\n\r\n";
+
+ /* Missing path after package */
+ response = cockpit_web_response_new (tc->io, url, url, NULL, "GET", NULL);
+
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "another@localhost", "");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str, "*\r\n13\r\n"
+ "<html><head><title>\r\n9\r\n"
+ "Not Found\r\n15\r\n"
+ "</title></head><body>\r\n9\r\n"
+ "Not Found\r\nf\r\n"
+ "</body></html>\n\r\n0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_failure (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ GPid pid;
+ const gchar *expected = "HTTP/1.1 500 terminated\r\nContent-Type: text/html; charset=utf8\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS "\r\n13\r\n<html><head><title>\r\na\r\nterminated\r\n15\r\n</title></head><body>\r\na\r\nterminated\r\nf\r\n</body></html>\n\r\n0\r\n\r\n";
+ const gchar *expected_alt = "HTTP/1.1 502 disconnected\r\nContent-Type: text/html; charset=utf8\r\nTransfer-Encoding: chunked\r\n" STATIC_HEADERS "\r\n13\r\n<html><head><title>\r\nc\r\ndisconnected\r\n15\r\n</title></head><body>\r\nc\r\ndisconnected\r\nf\r\n</body></html>\n\r\n0\r\n\r\n";
+
+ /* We need to skip this test under Valgrind because Valgrind doesn't
+ * know about pidfd_open() yet.
+ */
+ if (cockpit_test_skip_slow ())
+ return;
+
+ cockpit_expect_possible_log ("cockpit-protocol", G_LOG_LEVEL_WARNING, "*: bridge program failed:*");
+ cockpit_expect_possible_log ("cockpit-ws", G_LOG_LEVEL_MESSAGE, "*: external channel failed: *");
+
+ /* Make a pidfd for the bridge */
+ g_assert (cockpit_pipe_get_pid (tc->pipe, &pid));
+ g_assert_cmpint (pid, >, 0);
+ int pid_fd = syscall(SYS_pidfd_open, pid, 0);
+ if (pid_fd < 0)
+ {
+ if (errno == ENOSYS)
+ g_test_skip ("no pidfd_open support, skipping");
+ else
+ g_error ("pidfd_open call failed: %m");
+
+ return;
+ }
+
+ /* Now kill the bridge */
+ g_assert_cmpint (kill (pid, SIGTERM), ==, 0);
+
+ /* The SIGTERM gets delivered to the bridge via a glib unix signal
+ * handler, and it is theoretically possible that the request that we
+ * send below could get delivered before the SIGTERM. For that
+ * reason, we need to make sure that the process actually properly
+ * exited before sending the request.
+ */
+ struct pollfd pid_pfd = { .fd = pid_fd, .events = POLLIN };
+ while (poll (&pid_pfd, 1, -1) != 1)
+ ;
+ close (pid_fd);
+
+ response = cockpit_web_response_new (tc->io, "/unused", "/unused", NULL, "GET", NULL);
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "@localhost", "/another/test.html");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Null terminate for str-match below */
+ g_output_stream_write_all (G_OUTPUT_STREAM (tc->output), "\0", 1, NULL, NULL, &error);
+ g_assert_no_error (error);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n") || str_contains_strv (str, expected_alt, "\n"));
+ cockpit_assert_strmatch (str, "*\r\n\r\n13\r\n<html><head><title>\r\n*\r\n*\r\n15\r\n</title></head><body>\r\n*\r\n*\r\nf\r\n</body></html>\n\r\n0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static const TestResourceFixture checksum_fixture = {
+ .xdg_data_home = "/nonexistent"
+};
+
+
+static void
+request_checksum (TestResourceCase *tc)
+{
+ CockpitWebResponse *response;
+ GInputStream *input;
+ GOutputStream *output;
+ GIOStream *io;
+
+ input = g_memory_input_stream_new_from_data ("", 0, NULL);
+ output = g_memory_output_stream_new (NULL, 0, g_realloc, g_free);
+ io = g_simple_io_stream_new (input, output);
+ g_object_unref (input);
+
+ /* Start the connection up, and poke it a bit */
+ response = cockpit_web_response_new (io, "/unused", "/unused", NULL, "GET", NULL);
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "@localhost", "/checksum");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_object_unref (io);
+
+ /* Use this when the checksum changes, due to mock resource changes */
+#if 0
+ bytes = g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (output));
+ g_printerr ("%.*s\n", (gint)g_bytes_get_size (bytes), (gchar *)g_bytes_get_data (bytes, NULL));
+ g_bytes_unref (bytes);
+#endif
+
+ g_object_unref (output);
+ g_object_unref (response);
+}
+
+static void
+test_resource_checksum (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *expected = "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "ETag: \"" CHECKSUM "-c\"\r\n"
+ "Access-Control-Allow-Origin: http://localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Cache-Control: max-age=86400, private\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "32\r\n"
+ "These are the contents of file.ext\nOh marmalaaade\n"
+ "\r\n"
+ "0\r\n\r\n";
+
+ /* We require that no user packages are loaded, so we have a checksum */
+ g_assert (data == &checksum_fixture);
+
+ request_checksum (tc);
+
+ response = cockpit_web_response_new (tc->io, "/unused", "/unused", NULL, "GET", NULL);
+ cockpit_channel_response_serve (tc->service, tc->headers, response,
+ CHECKSUM,
+ "/test/sub/file.ext");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n"
+ "32\r\n"
+ "These are the contents of file.ext\nOh marmalaaade\n"
+ "\r\n"
+ "0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_not_modified (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ const gchar *expected = "HTTP/1.1 304 Not Modified\r\n"
+ "ETag: \"" CHECKSUM "-c\"\r\n"
+ STATIC_HEADERS
+ "\r\n";
+
+ request_checksum (tc);
+
+ g_hash_table_insert (tc->headers, g_strdup ("If-None-Match"),
+ g_strdup ("\"" CHECKSUM "-c\""));
+
+ response = cockpit_web_response_new (tc->io, "/unused", "/unused", tc->headers, "GET", NULL);
+ cockpit_channel_response_serve (tc->service, tc->headers, response,
+ CHECKSUM,
+ "/test/sub/file.ext");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ g_assert (str_contains_strv (g_bytes_get_data (bytes, NULL), expected, "\n"));
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_not_modified_new_language (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *expected = "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "ETag: \"" CHECKSUM "-de\"\r\n"
+ "Access-Control-Allow-Origin: http://localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Cache-Control: max-age=86400, private\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "32\r\n"
+ "These are the contents of file.ext\nOh marmalaaade\n"
+ "\r\n"
+ "0\r\n\r\n";
+
+ request_checksum (tc);
+
+ g_hash_table_insert (tc->headers, g_strdup ("If-None-Match"),
+ g_strdup ("\"" CHECKSUM "-c\""));
+ g_hash_table_insert (tc->headers, g_strdup ("Accept-Language"), g_strdup ("de"));
+
+ response = cockpit_web_response_new (tc->io, "/unused", "/unused", tc->headers, "GET", NULL);
+ cockpit_channel_response_serve (tc->service, tc->headers, response,
+ CHECKSUM,
+ "/test/sub/file.ext");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n"
+ "32\r\n"
+ "These are the contents of file.ext\nOh marmalaaade\n"
+ "\r\n"
+ "0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_not_modified_cookie_language (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ gchar *cookie;
+ const gchar *expected = "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "ETag: \"" CHECKSUM "-fr\"\r\n"
+ "Access-Control-Allow-Origin: http://localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Cache-Control: max-age=86400, private\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "32\r\n"
+ "These are the contents of file.ext\nOh marmalaaade\n"
+ "\r\n"
+ "0\r\n\r\n";
+
+ request_checksum (tc);
+
+ g_hash_table_insert (tc->headers, g_strdup ("If-None-Match"),
+ g_strdup ("\"" CHECKSUM "-c\""));
+
+ cookie = g_strdup_printf ("%s; CockpitLang=fr", (gchar *)g_hash_table_lookup (tc->headers, "Cookie"));
+ g_hash_table_insert (tc->headers, g_strdup ("Cookie"), cookie);
+
+ response = cockpit_web_response_new (tc->io, "/unused", "/unused", tc->headers, "GET", NULL);
+ cockpit_channel_response_serve (tc->service, tc->headers, response,
+ CHECKSUM,
+ "/test/sub/file.ext");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n"
+ "32\r\n"
+ "These are the contents of file.ext\nOh marmalaaade\n"
+ "\r\n"
+ "0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_no_checksum (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *expected = "HTTP/1.1 404 Not Found\r\n"
+ "Content-Type: text/html; charset=utf8\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ STATIC_HEADERS
+ "\r\n13\r\n"
+ "<html><head><title>\r\n9\r\n"
+ "Not Found\r\n15\r\n"
+ "</title></head><body>\r\n9\r\n"
+ "Not Found\r\nf\r\n"
+ "</body></html>\n\r\n0\r\n\r\n";
+
+ /* Missing checksum */
+ response = cockpit_web_response_new (tc->io, "/unused", "/unused", NULL, "GET", NULL);
+
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "xxx", "/test");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n13\r\n*"
+ "*<html><head><title>\r\n9\r\n*"
+ "*Not Found\r\n15\r\n*"
+ "*</title></head><body>\r\n9\r\n*"
+ "*Not Found\r\nf\r\n*"
+ "*</body></html>\n\r\n0\r\n\r\n*");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_bad_checksum (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *expected = "HTTP/1.1 404 Not Found\r\n"
+ "Content-Type: text/html; charset=utf8\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ STATIC_HEADERS
+ "\r\n13\r\n"
+ "<html><head><title>\r\n9\r\n"
+ "Not Found\r\n15\r\n"
+ "</title></head><body>\r\n9\r\n"
+ "Not Found\r\nf\r\n"
+ "</body></html>\n\r\n0\r\n\r\n";
+
+ /* Missing checksum */
+ response = cockpit_web_response_new (tc->io, "/unused", "/unused", NULL, "GET", NULL);
+
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "09323094823029348", "/path");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n13\r\n*"
+ "*<html><head><title>\r\n9\r\n*"
+ "*Not Found\r\n15\r\n*"
+ "*</title></head><body>\r\n9\r\n*"
+ "*Not Found\r\nf\r\n*"
+ "*</body></html>\n\r\n0\r\n\r\n*");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_language_suffix (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *expected = "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "Content-Security-Policy: default-src 'self' http://localhost; connect-src 'self' http://localhost ws://localhost; form-action 'self' http://localhost; base-uri 'self' http://localhost; object-src 'none'; font-src 'self' http://localhost data:; img-src 'self' http://localhost data:; block-all-mixed-content\r\n"
+ "Content-Type: text/html\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "Access-Control-Allow-Origin: http://localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "62\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>Im Home-Verzeichnis</title>\n"
+ "</head>\n"
+ "<body>Im Home-Verzeichnis</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n";
+
+ response = cockpit_web_response_new (tc->io, "/unused", "/unused", NULL, "GET", NULL);
+
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "@localhost", "/another/test.de.html");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n"
+ "62\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>Im Home-Verzeichnis</title>\n"
+ "</head>\n"
+ "<body>Im Home-Verzeichnis</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_language_fallback (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *expected = "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "Content-Security-Policy: default-src 'self' http://localhost; connect-src 'self' http://localhost ws://localhost; form-action 'self' http://localhost; base-uri 'self' http://localhost; object-src 'none'; font-src 'self' http://localhost data:; img-src 'self' http://localhost data:; block-all-mixed-content\r\n"
+ "Content-Type: text/html\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "Access-Control-Allow-Origin: http://localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "52\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>In home dir</title>\n"
+ "</head>\n"
+ "<body>In home dir</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n";
+
+ response = cockpit_web_response_new (tc->io, "/unused", "/unused", NULL, "GET", NULL);
+
+ /* Language cookie overrides */
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "@localhost", "/another/test.fi.html");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n"
+ "52\r\n"
+ "<html>\n"
+ "<head>\n"
+ "<title>In home dir</title>\n"
+ "</head>\n"
+ "<body>In home dir</body>\n"
+ "</html>\n"
+ "\r\n"
+ "0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_gzip_encoding (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *expected = "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "Content-Encoding: gzip\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "Access-Control-Allow-Origin: http://localhost\r\n"
+ "Content-Type: text/plain\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "34\r\n"
+ "\x1F\x8B\x08\x08N1\x03U\x00\x03test-file.txt\x00sT(\xCEM\xCC\xC9Q(I-"
+ ".QH\xCB\xCCI\xE5\x02\x00>PjG\x12\x00\x00\x00\x0D\x0A"
+ "0\x0D\x0A\x0D\x0A";
+
+ response = cockpit_web_response_new (tc->io, "/unused", "/unused", NULL, "GET", NULL);
+
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "@localhost", "/another/test-file.txt");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n"
+ "34\r\n"
+ "\x1F\x8B\x08\x08N1\x03U\x00\x03test-file.txt\x00sT(\xCEM\xCC\xC9Q(I-"
+ ".QH\xCB\xCCI\xE5\x02\x00>PjG\x12\x00\x00\x00\x0D\x0A"
+ "0\x0D\x0A\x0D\x0A");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+static void
+test_resource_head (TestResourceCase *tc,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ GError *error = NULL;
+ GBytes *bytes;
+ gconstpointer str;
+ const gchar *url = "/@localhost/another/test.html";
+ const gchar *expected = "HTTP/1.1 200 OK\r\n"
+ STATIC_HEADERS
+ "Content-Security-Policy: default-src 'self' http://localhost; connect-src 'self' http://localhost ws://localhost; form-action 'self' http://localhost; base-uri 'self' http://localhost; object-src 'none'; font-src 'self' http://localhost data:; img-src 'self' http://localhost data:; block-all-mixed-content\r\n"
+ "Content-Type: text/html\r\n"
+ "Cache-Control: no-cache, no-store\r\n"
+ "Access-Control-Allow-Origin: http://localhost\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Vary: Cookie\r\n"
+ "\r\n"
+ "0\r\n\r\n";
+
+ response = cockpit_web_response_new (tc->io, url, url, NULL, "HEAD", NULL);
+
+ cockpit_channel_response_serve (tc->service, tc->headers, response, "@localhost", "/another/test.html");
+
+ while (cockpit_web_response_get_state (response) != COCKPIT_WEB_RESPONSE_SENT)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_output_stream_close (G_OUTPUT_STREAM (tc->output), NULL, &error);
+ g_assert_no_error (error);
+
+ bytes = g_memory_output_stream_steal_as_bytes (tc->output);
+ str = g_bytes_get_data (bytes, NULL);
+ g_assert (str_contains_strv (str, expected, "\n"));
+ cockpit_assert_strmatch (str,
+ "*\r\n"
+ "0\r\n\r\n");
+
+ g_bytes_unref (bytes);
+ g_object_unref (response);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ extern const gchar *cockpit_webresponse_fail_html_text;
+ cockpit_webresponse_fail_html_text =
+ "<html><head><title>@@message@@</title></head><body>@@message@@</body></html>\n";
+
+ /* We don't want to test the ping functionality in these tests */
+ cockpit_ws_ping_interval = G_MAXUINT;
+
+ g_test_add ("/web-channel/resource/simple", TestResourceCase, NULL,
+ setup_resource, test_resource_simple, teardown_resource);
+ g_test_add ("/web-channel/resource/simple_host", TestResourceCase, NULL,
+ setup_resource, test_resource_simple_host, teardown_resource);
+ g_test_add ("/web-channel/resource/language", TestResourceCase, NULL,
+ setup_resource, test_resource_language, teardown_resource);
+ g_test_add ("/web-channel/resource/cookie", TestResourceCase, NULL,
+ setup_resource, test_resource_cookie, teardown_resource);
+ g_test_add ("/web-channel/resource/not-found", TestResourceCase, NULL,
+ setup_resource, test_resource_not_found, teardown_resource);
+ g_test_add ("/web-channel/resource/no-path", TestResourceCase, NULL,
+ setup_resource, test_resource_no_path, teardown_resource);
+ g_test_add ("/web-channel/resource/failure", TestResourceCase, NULL,
+ setup_resource, test_resource_failure, teardown_resource);
+ g_test_add ("/web-channel/resource/checksum", TestResourceCase, &checksum_fixture,
+ setup_resource, test_resource_checksum, teardown_resource);
+ g_test_add ("/web-channel/resource/not-modified", TestResourceCase, &checksum_fixture,
+ setup_resource, test_resource_not_modified, teardown_resource);
+ g_test_add ("/web-channel/resource/not-modified-new-language", TestResourceCase, &checksum_fixture,
+ setup_resource, test_resource_not_modified_new_language, teardown_resource);
+ g_test_add ("/web-channel/resource/not-modified-cookie-language", TestResourceCase, &checksum_fixture,
+ setup_resource, test_resource_not_modified_cookie_language, teardown_resource);
+ g_test_add ("/web-channel/resource/no-checksum", TestResourceCase, NULL,
+ setup_resource, test_resource_no_checksum, teardown_resource);
+ g_test_add ("/web-channel/resource/bad-checksum", TestResourceCase, NULL,
+ setup_resource, test_resource_bad_checksum, teardown_resource);
+ g_test_add ("/web-channel/resource/language-suffix", TestResourceCase, NULL,
+ setup_resource, test_resource_language_suffix, teardown_resource);
+ g_test_add ("/web-channel/resource/language-fallback", TestResourceCase, NULL,
+ setup_resource, test_resource_language_fallback, teardown_resource);
+
+ g_test_add ("/web-channel/resource/gzip-encoding", TestResourceCase, NULL,
+ setup_resource, test_resource_gzip_encoding, teardown_resource);
+ g_test_add ("/web-channel/resource/head", TestResourceCase, NULL,
+ setup_resource, test_resource_head, teardown_resource);
+
+ return g_test_run ();
+}
diff --git a/src/ws/test-compat.c b/src/ws/test-compat.c
new file mode 100644
index 0000000..18277de
--- /dev/null
+++ b/src/ws/test-compat.c
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2014 Red Hat Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ * * Redistributions in binary form must reproduce the
+ * above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * * The names of contributors to this software may not be
+ * used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * Author: Stef Walter <stefw@redhat.com>
+ */
+
+#include "config.h"
+
+#include "cockpitcompat.h"
+
+#include "common/cockpitauthorize.h"
+#include "testlib/cockpittest.h"
+
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <err.h>
+#include <errno.h>
+#include <pwd.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+typedef struct {
+ const char *input;
+ const char *password;
+ const char *expected;
+ int errn;
+} CryptFixture;
+
+static CryptFixture crypt1_fixtures[] = {
+ { "crypt1:invalid", "password", NULL, EINVAL },
+ { "crypt1:invalid:$1$0123456789abcdef$", "password", NULL, EINVAL },
+ { "crypt1:invalid:$1$invalid:$1$invalid", "password", NULL, EINVAL },
+ { "crypt1:invalid:1$0123456789abcdef$:$1$0123456789abcdef$", "password", NULL, EINVAL },
+ { "crypt1:invalid:$10123456789abcdef:$1$0123456789abcdef$", "password", NULL, EINVAL },
+ { "crypt1:73637275666679:$1$0123456789abcdef$:$1$0123456789abcdef$",
+ "password", "crypt1:$1$01234567$mmR7jVZhYpBJ6s6uTlnIR0", 0 },
+ { NULL },
+};
+
+static void
+test_crypt1 (gconstpointer data)
+{
+ const CryptFixture *fix = data;
+ char *response;
+
+ if (fix->errn != 0)
+ cockpit_expect_message ("*\"authorize\" message*");
+
+ response = cockpit_compat_reply_crypt1 (fix->input, fix->password);
+ g_assert_cmpstr (response, ==, fix->expected);
+ if (fix->errn != 0)
+ g_assert_cmpint (errno, ==, fix->errn);
+ free (response);
+
+ cockpit_assert_expected ();
+}
+
+static void
+test_logger (const char *msg)
+{
+ g_assert (msg != NULL);
+ g_message ("%s", msg);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gchar *name;
+ gint i;
+
+ cockpit_test_init (&argc, &argv);
+ cockpit_authorize_logger (test_logger, 0);
+
+ for (i = 0; crypt1_fixtures[i].input != NULL; i++)
+ {
+ name = g_strdup_printf ("/compat/crypt1/%s", crypt1_fixtures[i].input);
+ g_test_add_data_func (name, crypt1_fixtures + i, test_crypt1);
+ g_free (name);
+ }
+
+ return g_test_run ();
+}
diff --git a/src/ws/test-creds.c b/src/ws/test-creds.c
new file mode 100644
index 0000000..b31ba0a
--- /dev/null
+++ b/src/ws/test-creds.c
@@ -0,0 +1,201 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "ws/cockpitcreds.h"
+
+#include "common/cockpitjson.h"
+#include "testlib/cockpittest.h"
+
+static void
+assert_all_zeros (GBytes *bytes)
+{
+ gsize size;
+ const gchar *data = g_bytes_get_data (bytes, &size);
+
+ for (gsize i = 0; i < size; i++)
+ g_assert (data[i] == '\0');
+}
+
+static void
+test_password (void)
+{
+ CockpitCreds *creds;
+ GBytes *password;
+
+ password = g_bytes_new_take (g_strdup ("password"), 8);
+ creds = cockpit_creds_new ("test",
+ COCKPIT_CRED_USER, "user",
+ COCKPIT_CRED_PASSWORD, password,
+ NULL);
+ g_bytes_unref (password);
+
+ g_assert (creds != NULL);
+
+ g_assert_cmpstr ("user", ==, cockpit_creds_get_user (creds));
+ g_assert_cmpstr ("password", ==, g_bytes_get_data (cockpit_creds_get_password (creds), NULL));
+ g_assert_cmpstr ("test", ==, cockpit_creds_get_application (creds));
+
+ cockpit_creds_unref (creds);
+}
+
+static void
+test_set_password (void)
+{
+ CockpitCreds *creds;
+ GBytes *password;
+ GBytes *out;
+ GBytes *two;
+
+ password = g_bytes_new_take (g_strdup ("password"), 8);
+ creds = cockpit_creds_new ("app", COCKPIT_CRED_PASSWORD, password, NULL);
+ g_bytes_unref (password);
+
+ g_assert (creds != NULL);
+
+ out = cockpit_creds_get_password (creds);
+ g_assert (out != NULL);
+ g_assert_cmpstr ("password", ==, g_bytes_get_data (out, NULL));
+
+ password = g_bytes_new_take (g_strdup ("second"), 6);
+ cockpit_creds_set_password (creds, password);
+ g_bytes_unref (password);
+
+ two = cockpit_creds_get_password (creds);
+ g_assert (two != NULL);
+ g_assert_cmpstr ("second", ==, g_bytes_get_data (two, NULL));
+
+ cockpit_creds_set_password (creds, NULL);
+ g_assert (NULL == cockpit_creds_get_password (creds));
+
+ /* Still hold references to all old passwords, but they are cleared */
+ assert_all_zeros (out);
+ assert_all_zeros (two);
+
+ cockpit_creds_unref (creds);
+}
+
+static void
+test_poison (void)
+{
+ CockpitCreds *creds;
+ GBytes *password;
+ GBytes *out;
+
+ password = g_bytes_new_take (g_strdup ("password"), 8);
+ creds = cockpit_creds_new ("app", COCKPIT_CRED_PASSWORD, password, NULL);
+ g_bytes_unref (password);
+
+ g_assert (creds != NULL);
+
+ g_assert_cmpstr ("password", ==, g_bytes_get_data (cockpit_creds_get_password (creds), NULL));
+ g_assert_cmpstr ("app", ==, cockpit_creds_get_application (creds));
+
+ out = cockpit_creds_get_password (creds);
+ cockpit_creds_poison (creds);
+
+ g_assert (NULL == cockpit_creds_get_password (creds));
+
+ password = g_bytes_new_take (g_strdup ("second"), 6);
+ cockpit_creds_set_password (creds, password);
+ g_bytes_unref (password);
+
+ /* Even though we set a new password, still NULL */
+ g_assert (NULL == cockpit_creds_get_password (creds));
+ assert_all_zeros (out);
+
+ cockpit_creds_unref (creds);
+}
+
+static void
+test_rhost (void)
+{
+ CockpitCreds *creds;
+
+ creds = cockpit_creds_new ("app", COCKPIT_CRED_RHOST, "remote", NULL);
+ g_assert (creds != NULL);
+
+ g_assert_cmpstr ("remote", ==, cockpit_creds_get_rhost (creds));
+ g_assert_cmpstr ("app", ==, cockpit_creds_get_application (creds));
+
+ cockpit_creds_unref (creds);
+}
+
+static void
+test_multiple (void)
+{
+ CockpitCreds *creds;
+ GBytes *password;
+
+ password = g_bytes_new_take (g_strdup ("password"), 8);
+ creds = cockpit_creds_new ("app",
+ COCKPIT_CRED_PASSWORD, password,
+ COCKPIT_CRED_RHOST, "remote",
+ NULL);
+ g_assert (creds != NULL);
+
+ g_assert_cmpstr ("remote", ==, cockpit_creds_get_rhost (creds));
+ g_assert_cmpstr ("password", ==, g_bytes_get_data (cockpit_creds_get_password (creds), NULL));
+ g_assert_cmpstr ("app", ==, cockpit_creds_get_application (creds));
+
+ g_bytes_unref (password);
+ cockpit_creds_unref (creds);
+}
+
+static void
+test_login_data (void)
+{
+ JsonObject *object;
+ const gchar *valid = "{ \"login-data\" : { \"login\": \"data\" } }";
+ CockpitCreds *creds;
+
+ creds = cockpit_creds_new ("app", NULL);
+ g_assert (cockpit_creds_get_login_data (creds) == NULL);
+
+ object = cockpit_json_parse_object (valid, -1, NULL);
+ cockpit_creds_set_login_data (creds, object);
+ json_object_unref (object);
+ cockpit_assert_json_eq (cockpit_creds_get_login_data (creds), valid);
+
+ object = cockpit_json_parse_object (valid, -1, NULL);
+ cockpit_creds_set_login_data (creds, object);
+ json_object_unref (object);
+ cockpit_assert_json_eq (cockpit_creds_get_login_data (creds), valid);
+
+ cockpit_creds_set_login_data (creds, NULL);
+ g_assert (cockpit_creds_get_login_data (creds) == NULL);
+ cockpit_creds_unref (creds);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add_func ("/creds/basic-password", test_password);
+ g_test_add_func ("/creds/set-password", test_set_password);
+ g_test_add_func ("/creds/poison", test_poison);
+ g_test_add_func ("/creds/rhost", test_rhost);
+ g_test_add_func ("/creds/multiple", test_multiple);
+ g_test_add_func ("/creds/login-data", test_login_data);
+
+ return g_test_run ();
+}
diff --git a/src/ws/test-handlers.c b/src/ws/test-handlers.c
new file mode 100644
index 0000000..6a05946
--- /dev/null
+++ b/src/ws/test-handlers.c
@@ -0,0 +1,907 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+
+#include "cockpithandlers.h"
+#include "cockpitws.h"
+
+#include "common/cockpitconf.h"
+#include "common/cockpitsocket.h"
+#include "common/cockpitsystem.h"
+#include "common/cockpitwebserver.h"
+#include "common/cockpitwebrequest-private.h"
+
+#include "testlib/cockpittest.h"
+#include "testlib/mock-auth.h"
+
+#include <glib.h>
+
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * To recalculate the checksums found in this file, do something like:
+ * $ XDG_DATA_DIRS=$PWD/src/bridge/mock-resource/system/ XDG_DATA_HOME=/nonexistent ./cockpit-bridge --packages
+ */
+#define CHECKSUM "$9a9ee8f5711446a46289cd1451c2a7125fb586456884b96807401ac2f055e669"
+
+/* Mock override this from cockpitconf.c */
+extern const gchar *cockpit_config_file;
+
+#define PASSWORD "this is the password"
+
+typedef struct {
+ CockpitHandlerData data;
+ CockpitWebServer *server;
+ CockpitAuth *auth;
+ GHashTable *headers;
+ GIOStream *io;
+ CockpitWebResponse *response;
+ gboolean response_done;
+ GMemoryOutputStream *output;
+ GMemoryInputStream *input;
+ GByteArray *buffer;
+ gchar *scratch;
+ gchar **roots;
+ gchar *login_html;
+} Test;
+
+static void
+on_ready_get_result (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **retval = user_data;
+ g_assert (retval != NULL);
+ g_assert (*retval == NULL);
+ *retval = g_object_ref (result);
+}
+
+static void
+on_web_response_done_set_flag (CockpitWebResponse *response,
+ gboolean reuse,
+ gpointer user_data)
+{
+ gboolean *flag = user_data;
+ g_assert (flag != NULL);
+ g_assert (*flag == FALSE);
+ *flag = TRUE;
+}
+
+static void
+base_setup (Test *test)
+{
+ const gchar *static_roots[] = { SRCDIR "/src/ws", SRCDIR "/src/branding/default", NULL };
+ GError *error = NULL;
+
+ test->server = cockpit_web_server_new (NULL, COCKPIT_WEB_SERVER_NONE);
+ cockpit_web_server_add_inet_listener (test->server, NULL, 0, &error);
+ g_assert_no_error (error);
+
+ cockpit_web_server_start (test->server);
+
+ /* Other test->data fields are fine NULL */
+ memset (&test->data, 0, sizeof (test->data));
+
+ test->auth = cockpit_auth_new (FALSE, COCKPIT_AUTH_NONE);
+ test->roots = cockpit_web_response_resolve_roots (static_roots);
+ test->login_html = g_strdup(SRCDIR "/pkg/static/login.html");
+
+ test->data.auth = test->auth;
+ test->data.branding_roots = (const gchar **)test->roots;
+ test->data.login_html = (const gchar *)test->login_html;
+ test->data.login_po_js = NULL;
+
+ test->headers = cockpit_web_server_new_table ();
+
+ test->output = G_MEMORY_OUTPUT_STREAM (g_memory_output_stream_new (NULL, 0, g_realloc, g_free));
+ test->input = G_MEMORY_INPUT_STREAM (g_memory_input_stream_new ());
+
+ test->io = g_simple_io_stream_new (G_INPUT_STREAM (test->input),
+ G_OUTPUT_STREAM (test->output));
+}
+
+static void
+setup (Test *test,
+ gconstpointer path)
+{
+ base_setup (test);
+ test->response = cockpit_web_response_new (test->io, path, path, NULL, "GET", NULL);
+ g_signal_connect (test->response, "done",
+ G_CALLBACK (on_web_response_done_set_flag),
+ &test->response_done);
+}
+
+static void
+teardown (Test *test,
+ gconstpointer path)
+{
+ g_clear_object (&test->auth);
+ g_clear_object (&test->server);
+ g_clear_object (&test->output);
+ g_clear_object (&test->input);
+ g_clear_object (&test->io);
+ g_free (test->login_html);
+ g_hash_table_destroy (test->headers);
+ g_free (test->scratch);
+ g_object_unref (test->response);
+ g_strfreev (test->roots);
+
+ cockpit_assert_expected ();
+}
+
+static const gchar *
+output_as_string (Test *test)
+{
+ while (!test->response_done)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_free (test->scratch);
+ test->scratch = g_strndup (g_memory_output_stream_get_data (test->output),
+ g_memory_output_stream_get_data_size (test->output));
+ return test->scratch;
+}
+
+static void
+test_login_no_cookie (Test *test,
+ gconstpointer path)
+{
+ gboolean ret;
+
+ ret = cockpit_handler_default (test->server, WebRequest(.path=path, .headers=test->headers),
+ path, test->headers, test->response, &test->data);
+
+ g_assert (ret == TRUE);
+
+ cockpit_assert_strmatch (output_as_string (test), "HTTP/1.1 401 Authentication failed\r\n*");
+}
+
+static void
+include_cookie_as_if_client (GHashTable *resp_headers,
+ GHashTable *req_headers)
+{
+ gchar *cookie;
+ gchar *end;
+
+ cookie = g_strdup (g_hash_table_lookup (resp_headers, "Set-Cookie"));
+ g_assert (cookie != NULL);
+ end = strchr (cookie, ';');
+ g_assert (end != NULL);
+ end[0] = '\0';
+
+ g_hash_table_insert (req_headers, g_strdup ("Cookie"), cookie);
+}
+
+static void
+test_login_with_cookie (Test *test,
+ gconstpointer path)
+{
+ GError *error = NULL;
+ GAsyncResult *result = NULL;
+ JsonObject *response;
+ GHashTable *headers;
+ gboolean ret;
+
+ headers = mock_auth_basic_header ("me", PASSWORD);
+
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path=path, .headers=headers),
+ on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ response = cockpit_auth_login_finish (test->auth, result, NULL, test->headers, &error);
+ g_object_unref (result);
+
+ g_assert_no_error (error);
+ g_assert (response != NULL);
+ json_object_unref (response);
+
+ include_cookie_as_if_client (test->headers, test->headers);
+
+ ret = cockpit_handler_default (test->server, WebRequest(.path=path, .headers=test->headers),
+ path, test->headers, test->response, &test->data);
+
+ g_assert (ret == TRUE);
+
+ cockpit_assert_strmatch (output_as_string (test), "HTTP/1.1 200 OK\r\n*\r\n\r\n{*");
+}
+
+static void
+test_login_bad (Test *test,
+ gconstpointer path)
+{
+ gboolean ret;
+ GHashTable *headers;
+
+ headers = cockpit_web_server_new_table ();
+ g_hash_table_insert (headers, g_strdup ("Authorization"), g_strdup ("booyah"));
+ ret = cockpit_handler_default (test->server, WebRequest(.path=path, .headers=headers),
+ path, headers, test->response, &test->data);
+ g_hash_table_unref (headers);
+
+ g_assert (ret == TRUE);
+ cockpit_assert_strmatch (output_as_string (test), "HTTP/1.1 401 Authentication disabled\r\n*");
+}
+
+static void
+test_login_fail (Test *test,
+ gconstpointer path)
+{
+ gboolean ret;
+ GHashTable *headers;
+
+ headers = mock_auth_basic_header ("booo", "yah");
+ ret = cockpit_handler_default (test->server, WebRequest(.path=path, .headers=test->headers),
+ path, test->headers, test->response, &test->data);
+ g_hash_table_unref (headers);
+
+ while (!test->response_done)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert (ret == TRUE);
+ cockpit_assert_strmatch (output_as_string (test), "HTTP/1.1 401 Authentication failed\r\n*");
+}
+
+static GHashTable *
+split_headers (const gchar *output)
+{
+ GHashTable *headers;
+ gchar **lines;
+ gchar **parts;
+ gint i;
+
+ headers = cockpit_web_server_new_table ();
+ lines = g_strsplit (output, "\r\n", -1);
+ for (i = 1; lines[i] != NULL && lines[i][0] != '\0'; i++)
+ {
+ parts = g_strsplit (lines[i], ":", 2);
+ g_hash_table_insert (headers, g_strstrip (parts[0]), g_strstrip (parts[1]));
+ g_free (parts);
+ }
+
+ g_strfreev (lines);
+ return headers;
+}
+
+static void
+test_login_accept (Test *test,
+ gconstpointer path)
+{
+ CockpitWebService *service;
+ gboolean ret;
+ const gchar *output;
+ GHashTable *headers;
+ CockpitCreds *creds;
+ const gchar *token;
+
+ headers = mock_auth_basic_header ("me", PASSWORD);
+ g_hash_table_insert (headers, g_strdup ("X-Authorize"), g_strdup ("password"));
+ ret = cockpit_handler_default (test->server, WebRequest(.path=path, .headers=headers),
+ path, headers, test->response, &test->data);
+ g_hash_table_unref (headers);
+
+ g_assert (ret == TRUE);
+
+ output = output_as_string (test);
+ cockpit_assert_strmatch (output, "HTTP/1.1 200 OK\r\n*");
+ cockpit_assert_strmatch (output, "*Secure; *");
+
+ /* Check that returned cookie that works */
+ headers = split_headers (output);
+ include_cookie_as_if_client (headers, test->headers);
+
+ service = cockpit_auth_check_cookie (test->auth, WebRequest(.path="/cockpit", .headers=test->headers));
+ g_assert (service != NULL);
+ creds = cockpit_web_service_get_creds (service);
+ g_assert_cmpstr (cockpit_creds_get_user (creds), ==, "me");
+ g_assert_null (cockpit_creds_get_password (creds));
+
+ token = cockpit_creds_get_csrf_token (creds);
+ g_assert (strstr (output, token));
+
+ g_hash_table_destroy (headers);
+ g_object_unref (service);
+}
+
+static void
+test_favicon_ico (Test *test,
+ gconstpointer path)
+{
+ const gchar *output;
+ gboolean ret;
+
+ ret = cockpit_handler_root (test->server, WebRequest(.path=path, .headers=test->headers),
+ path, test->headers, test->response, &test->data);
+
+ g_assert (ret == TRUE);
+
+ output = output_as_string (test);
+ cockpit_assert_strmatch (output,
+ "HTTP/1.1 200 OK\r\n*"
+ "Content-Length: *\r\n"
+ "*");
+}
+
+static void
+test_ping (Test *test,
+ gconstpointer path)
+{
+ const gchar *output;
+ gboolean ret;
+
+ ret = cockpit_handler_ping (test->server, WebRequest(.path=path, .headers=test->headers),
+ path, test->headers, test->response, &test->data);
+
+ g_assert (ret == TRUE);
+
+ output = output_as_string (test);
+ cockpit_assert_strmatch (output,
+ "HTTP/1.1 200 OK\r\n*"
+ "Access-Control-Allow-Origin: *\r\n*"
+ "\"cockpit\"*");
+}
+
+typedef struct {
+ const gchar *path;
+ const gchar *org_path;
+ const gchar *auth;
+ const gchar *expect;
+ const gchar *config;
+ gboolean with_home;
+} DefaultFixture;
+
+static void
+setup_default (Test *test,
+ gconstpointer data)
+{
+ const DefaultFixture *fixture = data;
+ JsonObject *response;
+ GError *error = NULL;
+ GAsyncResult *result = NULL;
+ GHashTable *headers;
+
+ cockpit_config_file = fixture->config;
+
+ if (fixture->config)
+ cockpit_setenv_check ("XDG_CONFIG_DIRS", fixture->config, TRUE);
+ else
+ g_unsetenv ("XDG_CONFIG_DIRS");
+
+ cockpit_setenv_check ("XDG_DATA_DIRS", SRCDIR "/src/bridge/mock-resource/system", TRUE);
+ if (fixture->with_home)
+ cockpit_setenv_check ("XDG_DATA_HOME", SRCDIR "/src/bridge/mock-resource/home", TRUE);
+ else
+ cockpit_setenv_check ("XDG_DATA_HOME", "/nonexistent", TRUE);
+
+ base_setup (test);
+ test->response = cockpit_web_response_new (test->io,
+ fixture->org_path ? fixture->org_path : fixture->path,
+ fixture->path, NULL, "GET", NULL);
+ g_signal_connect (test->response, "done",
+ G_CALLBACK (on_web_response_done_set_flag),
+ &test->response_done);
+
+ if (fixture->auth)
+ {
+ headers = mock_auth_basic_header ("bridge-user", PASSWORD);
+
+ cockpit_auth_login_async (test->auth,
+ WebRequest(.path=fixture->auth, .headers=headers),
+ on_ready_get_result, &result);
+ g_hash_table_unref (headers);
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ response = cockpit_auth_login_finish (test->auth, result, NULL, test->headers, &error);
+ g_object_unref (result);
+
+ g_assert_no_error (error);
+ g_assert (response != NULL);
+ json_object_unref (response);
+
+ include_cookie_as_if_client (test->headers, test->headers);
+ }
+}
+
+static void
+teardown_default (Test *test,
+ gconstpointer data)
+{
+ const DefaultFixture *fixture = data;
+
+ g_unsetenv ("XDG_DATA_DIRS");
+ g_unsetenv ("XDG_DATA_HOME");
+
+ teardown (test, fixture->path);
+ cockpit_conf_cleanup ();
+};
+
+static void
+test_default (Test *test,
+ gconstpointer data)
+{
+ const DefaultFixture *fixture = data;
+ gboolean ret;
+
+ ret = cockpit_handler_default (test->server, WebRequest(.path=fixture->path, .headers=test->headers),
+ fixture->path, test->headers, test->response, &test->data);
+
+ if (fixture->expect)
+ {
+ g_assert (ret == TRUE);
+ cockpit_assert_strmatch (output_as_string (test), fixture->expect);
+ }
+ else
+ {
+ g_assert (ret == FALSE);
+ }
+}
+
+static const DefaultFixture fixture_resource_checksum = {
+ .path = "/cockpit/" CHECKSUM "/test/sub/file.ext",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 200*"
+ "These are the contents of file.ext*"
+};
+
+static void
+test_resource_checksum (Test *test,
+ gconstpointer data)
+{
+ CockpitWebResponse *response;
+ gboolean response_done = FALSE;
+ GInputStream *input;
+ GOutputStream *output;
+ GIOStream *io;
+ gchar *string;
+ const gchar *path;
+
+ /* Prime the checksums with dummy request */
+ output = g_memory_output_stream_new (NULL, 0, g_realloc, g_free);
+ input = g_memory_input_stream_new ();
+ io = g_simple_io_stream_new (input, output);
+ path = "/cockpit/@localhost/checksum";
+ response = cockpit_web_response_new (io, path, path, NULL, "GET", NULL);
+ g_signal_connect (response, "done", G_CALLBACK (on_web_response_done_set_flag), &response_done);
+ g_assert (cockpit_handler_default (test->server, WebRequest(.path=path, .headers=test->headers),
+ path, test->headers, response, &test->data));
+
+ while (!response_done)
+ g_main_context_iteration (NULL, TRUE);
+
+ string = g_strndup (g_memory_output_stream_get_data (G_MEMORY_OUTPUT_STREAM (output)),
+ g_memory_output_stream_get_data_size (G_MEMORY_OUTPUT_STREAM (output)));
+ cockpit_assert_strmatch (string, "HTTP/1.1 200*");
+ g_free (string);
+
+ g_object_unref (output);
+ g_object_unref (input);
+ g_object_unref (io);
+ g_object_unref (response);
+
+ /* And now run the real test */
+ test_default (test, data);
+}
+
+
+static const DefaultFixture fixture_shell_path_index = {
+ .path = "/",
+ .org_path = "/path/",
+ .auth = "/cockpit",
+ .with_home = TRUE,
+ .expect = "HTTP/1.1 200*"
+ "<base href=\"/path/cockpit/@localhost/another/test.html\">*"
+ "<title>In home dir</title>*"
+};
+
+static const DefaultFixture fixture_shell_path_package = {
+ .path = "/system/host",
+ .org_path = "/path/system/host",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 200*"
+ "<base href=\"/path/cockpit/" CHECKSUM "/another/test.html\">*"
+ "<title>In system dir</title>*"
+};
+
+static const DefaultFixture fixture_shell_path_host = {
+ .path = "/@localhost/system/host",
+ .org_path = "/path/@localhost/system/host",
+ .with_home = TRUE,
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 200*"
+ "<base href=\"/path/cockpit/@localhost/another/test.html\">*"
+ "<title>In home dir</title>*"
+};
+
+static const DefaultFixture fixture_shell_path_login = {
+ .path = "/system/host",
+ .org_path = "/path/system/host",
+ .auth = NULL,
+ .expect = "HTTP/1.1 200*"
+ "Set-Cookie: cockpit=deleted; PATH=/; SameSite=strict; HttpOnly\r*"
+ "<html>*"
+ "<base href=\"/path/\">*"
+ "login-button*"
+};
+
+static const DefaultFixture fixture_shell_index = {
+ .path = "/",
+ .auth = "/cockpit",
+ .with_home = TRUE,
+ .expect = "HTTP/1.1 200*"
+ "Cache-Control: no-cache, no-store*"
+ "<base href=\"/cockpit/@localhost/another/test.html\">*"
+ "<title>In home dir</title>*"
+};
+
+static const DefaultFixture fixture_machine_shell_index = {
+ .path = "/=machine",
+ .auth = "/cockpit+=machine",
+ .config = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf",
+ .expect = "HTTP/1.1 200*"
+ "<base href=\"/cockpit+=machine/" CHECKSUM "/second/test.html\">*"
+ "<title>In system dir</title>*"
+};
+
+static const DefaultFixture fixture_shell_configured_index = {
+ .path = "/",
+ .auth = "/cockpit",
+ .with_home = TRUE,
+ .config = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf",
+ .expect = "HTTP/1.1 200*"
+ "<base href=\"/cockpit/@localhost/second/test.html\">*"
+ "<title>In system dir</title>*"
+};
+
+static const DefaultFixture fixture_shell_package = {
+ .path = "/system/host",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 200*"
+ "<base href=\"/cockpit/" CHECKSUM "/another/test.html\">*"
+ "<title>In system dir</title>*"
+};
+
+static const DefaultFixture fixture_shell_host = {
+ .path = "/@localhost/system/host",
+ .with_home = TRUE,
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 200*"
+ "<base href=\"/cockpit/@localhost/another/test.html\">*"
+ "<title>In home dir</title>*"
+};
+
+static const DefaultFixture fixture_shell_host_short = {
+ .path = "/@/system/page",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 404*"
+};
+
+static const DefaultFixture fixture_shell_package_short = {
+ .path = "//page",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 404*"
+};
+
+static const DefaultFixture fixture_machine_shell_package_short = {
+ .path = "/=/",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 404*",
+ .config = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf"
+};
+
+static const DefaultFixture fixture_shell_package_invalid = {
+ .path = "/invalid.path/page",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 404*"
+};
+
+static const DefaultFixture fixture_shell_login = {
+ .path = "/system/host",
+ .auth = NULL,
+ .expect = "HTTP/1.1 200*"
+ "Set-Cookie: cockpit=deleted*"
+ "<html>*"
+ "<base href=\"/\">*"
+ "login-button*"
+};
+
+static const DefaultFixture fixture_resource_short = {
+ .path = "/cockpit",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 404*"
+};
+
+static const DefaultFixture fixture_resource_host = {
+ .path = "/cockpit/@localhost/test/sub/file.ext",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 200*"
+ "These are the contents of file.ext*"
+};
+
+static const DefaultFixture fixture_resource_host_short = {
+ .path = "/cockpit/@/test/sub/file.ext",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 404*"
+};
+
+static const DefaultFixture fixture_resource_application = {
+ .path = "/cockpit+application/@localhost/test/sub/file.ext",
+ .auth = "/cockpit+application",
+ .expect = "HTTP/1.1 200*"
+ "These are the contents of file.ext*"
+};
+
+static const DefaultFixture fixture_resource_application_specialchars = {
+ .path = "/cockpit+application/@localhost/test/_modules/@testorg/toolkit.js",
+ .auth = "/cockpit+application",
+ .expect = "HTTP/1.1 200*"
+ "the.code()*"
+};
+
+static const DefaultFixture fixture_resource_application_short = {
+ .path = "/cockpit+/@localhost/test/sub/file.ext",
+ .auth = "/cockpit+",
+ .expect = "HTTP/1.1 401*"
+};
+
+static const DefaultFixture fixture_resource_missing = {
+ .path = "/cockpit/another/file.html",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 404*"
+};
+
+static const DefaultFixture fixture_resource_auth = {
+ .path = "/cockpit/@localhost/yyy/zzz",
+ .auth = NULL,
+ .expect = "HTTP/1.1 401*"
+};
+
+static const DefaultFixture fixture_resource_login = {
+ .path = "/cockpit/@localhost/yyy/zzz.html",
+ .auth = NULL,
+ .expect = "HTTP/1.1 200*"
+ "Set-Cookie: cockpit=deleted*"
+ "<html>*"
+ "login-button*"
+};
+
+static const DefaultFixture fixture_static_simple = {
+ .path = "/cockpit/static/branding.css",
+ .auth = "/cockpit",
+ .expect = "HTTP/1.1 200*"
+ "Cache-Control: max-age=86400, private*"
+ "#badge*"
+ "url(\"logo.png\");*"
+};
+
+static const DefaultFixture fixture_host_static = {
+ .path = "/cockpit+=host/static/branding.css",
+ .auth = "/cockpit+=host",
+ .config = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf",
+ .expect = "HTTP/1.1 200*"
+ "Cache-Control: max-age=86400, private*"
+ "#badge*"
+ "url(\"logo.png\");*"
+};
+
+static const DefaultFixture fixture_host_login = {
+ .path = "/=host/system",
+ .auth = NULL,
+ .config = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf",
+ .expect = "HTTP/1.1 200*"
+ "Set-Cookie: machine-cockpit+host=deleted*"
+ "<html>*"
+ "<base href=\"/\">*"
+ "login-button*"
+};
+
+static const DefaultFixture fixture_host_static_no_auth = {
+ .path = "/cockpit+=host/static/branding.css",
+ .expect = "HTTP/1.1 403*",
+ .config = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf"
+};
+
+static const DefaultFixture fixture_static_application = {
+ .path = "/cockpit+application/static/branding.css",
+ .auth = NULL,
+ .expect = "HTTP/1.1 200*"
+ "Cache-Control: max-age=86400, private*"
+ "#badge*"
+ "url(\"logo.png\");*"
+};
+
+static void
+on_error_not_reached (WebSocketConnection *ws,
+ GError *error,
+ gpointer user_data)
+{
+ g_assert (error != NULL);
+
+ /* At this point we know this will fail, but is informative */
+ g_assert_no_error (error);
+}
+
+static void
+on_message_get_bytes (WebSocketConnection *ws,
+ WebSocketDataType type,
+ GBytes *message,
+ gpointer user_data)
+{
+ GBytes **received = user_data;
+ g_assert_cmpint (type, ==, WEB_SOCKET_DATA_TEXT);
+ if (*received != NULL)
+ {
+ gsize length;
+ gconstpointer data = g_bytes_get_data (message, &length);
+ g_test_message ("received unexpected extra message: %.*s", (int)length, (gchar *)data);
+ g_assert_not_reached ();
+ }
+ *received = g_bytes_ref (message);
+}
+
+static void
+test_socket_unauthenticated (void)
+{
+ CockpitWebServer *server;
+ WebSocketConnection *client;
+ GBytes *received = NULL;
+ GBytes *payload;
+ const gchar *problem;
+ const gchar *command;
+ const gchar *unused;
+ gchar *channel;
+ JsonObject *options;
+
+ server = cockpit_web_server_new (NULL, COCKPIT_WEB_SERVER_NONE);
+ g_signal_connect (server, "handle-stream", G_CALLBACK (cockpit_handler_socket), NULL);
+ g_autoptr(GIOStream) connection = cockpit_web_server_connect (server);
+
+
+ client = g_object_new (WEB_SOCKET_TYPE_CLIENT,
+ "url", "ws://127.0.0.1/cockpit/socket",
+ "origin", "http://127.0.0.1",
+ "io-stream", connection,
+ NULL);
+
+ g_signal_connect (client, "error", G_CALLBACK (on_error_not_reached), NULL);
+ g_signal_connect (client, "message", G_CALLBACK (on_message_get_bytes), &received);
+
+ /* Should close right after opening */
+ while (web_socket_connection_get_ready_state (client) != WEB_SOCKET_STATE_CLOSED)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* And we should have received a message */
+ g_assert (received != NULL);
+
+ payload = cockpit_transport_parse_frame (received, &channel);
+ g_assert (payload != NULL);
+ g_assert (channel == NULL);
+ g_bytes_unref (received);
+
+ g_assert (cockpit_transport_parse_command (payload, &command, &unused, &options));
+ g_bytes_unref (payload);
+
+ g_assert_cmpstr (command, ==, "init");
+ g_assert (cockpit_json_get_string (options, "problem", NULL, &problem));
+ g_assert_cmpstr (problem, ==, "no-session");
+ json_object_unref (options);
+
+ g_object_unref (client);
+
+ while (g_main_context_iteration (NULL, FALSE));
+
+ g_object_unref (server);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ /* See mock-resource */
+ cockpit_ws_shell_component = "/another/test.html";
+ cockpit_ws_session_program = BUILDDIR "/mock-auth-command";
+
+ cockpit_test_init (&argc, &argv);
+
+ g_test_add ("/handlers/login/no-cookie", Test, "/cockpit/login",
+ setup, test_login_no_cookie, teardown);
+ g_test_add ("/handlers/login/with-cookie", Test, "/cockpit+app/login",
+ setup, test_login_with_cookie, teardown);
+ g_test_add ("/handlers/login/post-bad", Test, "/cockpit/login",
+ setup, test_login_bad, teardown);
+ g_test_add ("/handlers/login/post-fail", Test, "/cockpit/login",
+ setup, test_login_fail, teardown);
+ g_test_add ("/handlers/login/post-accept", Test, "/cockpit/login",
+ setup, test_login_accept, teardown);
+
+ g_test_add ("/handlers/ping", Test, "/ping",
+ setup, test_ping, teardown);
+
+ g_test_add ("/handlers/shell/index", Test, &fixture_shell_index,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/machine-index", Test, &fixture_machine_shell_index,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/configured_index", Test, &fixture_shell_configured_index,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/package", Test, &fixture_shell_package,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/host", Test, &fixture_shell_host,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/host-short", Test, &fixture_shell_host_short,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/package-short", Test, &fixture_shell_package_short,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/machine-package-short", Test, &fixture_machine_shell_package_short,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/package-invalid", Test, &fixture_shell_package_invalid,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/login", Test, &fixture_shell_login,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/path-index", Test, &fixture_shell_path_index,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/path-package", Test, &fixture_shell_path_package,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/path-host", Test, &fixture_shell_path_host,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/shell/path-login", Test, &fixture_shell_path_login,
+ setup_default, test_default, teardown_default);
+
+ g_test_add ("/handlers/resource/checksum", Test, &fixture_resource_checksum,
+ setup_default, test_resource_checksum, teardown_default);
+
+ g_test_add ("/handlers/resource/short", Test, &fixture_resource_short,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/resource/host", Test, &fixture_resource_host,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/resource/host-short", Test, &fixture_resource_host_short,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/resource/application", Test, &fixture_resource_application,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/resource/application-specialchars", Test, &fixture_resource_application_specialchars,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/resource/application-short", Test, &fixture_resource_application_short,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/resource/missing", Test, &fixture_resource_missing,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/resource/auth", Test, &fixture_resource_auth,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/resource/login", Test, &fixture_resource_login,
+ setup_default, test_default, teardown_default);
+
+ g_test_add ("/handlers/static/simple", Test, &fixture_static_simple,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/static/host-static", Test, &fixture_host_static,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/static/host-login", Test, &fixture_host_login,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/static/host-static-no-auth", Test, &fixture_host_static_no_auth,
+ setup_default, test_default, teardown_default);
+ g_test_add ("/handlers/static/application", Test, &fixture_static_application,
+ setup_default, test_default, teardown_default);
+
+ g_test_add ("/handlers/favicon", Test, "/favicon.ico",
+ setup, test_favicon_ico, teardown);
+
+ g_test_add_func ("/handlers/noauth", test_socket_unauthenticated);
+
+ return g_test_run ();
+}
diff --git a/src/ws/test-kerberos.c b/src/ws/test-kerberos.c
new file mode 100644
index 0000000..dc447d8
--- /dev/null
+++ b/src/ws/test-kerberos.c
@@ -0,0 +1,509 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitauth.h"
+#include "cockpitws.h"
+
+#include "common/cockpitsystem.h"
+#include "common/cockpitwebserver.h"
+#include "common/cockpitwebrequest-private.h"
+
+#include "testlib/cockpittest.h"
+
+#include <krb5/krb5.h>
+#include <gssapi/gssapi_krb5.h>
+
+#include <sys/prctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+static gboolean mock_kdc_available;
+
+static void mock_kdc_up (void);
+
+static void mock_kdc_down (void);
+
+typedef struct {
+ CockpitAuth *auth;
+ krb5_context krb;
+ krb5_ccache ccache;
+ char *ccache_name;
+} TestCase;
+
+static void
+setup (TestCase *test,
+ gconstpointer data)
+{
+ krb5_get_init_creds_opt *opt;
+ krb5_principal principal;
+ krb5_error_code code;
+ krb5_creds creds;
+ OM_uint32 status;
+ OM_uint32 minor;
+ gchar *name;
+
+ if (!mock_kdc_available)
+ return;
+
+ test->auth = cockpit_auth_new (FALSE, COCKPIT_AUTH_NONE);
+
+ mock_kdc_up ();
+
+ code = krb5_init_context (&test->krb);
+ g_assert (code != ENOMEM);
+ if (code != 0)
+ {
+ g_critical ("couldn't create krb context: %s", krb5_get_error_message (NULL, code));
+ return;
+ }
+
+ /* Initialize the client credential cache */
+ if (krb5_cc_new_unique (test->krb, "MEMORY", NULL, &test->ccache) != 0)
+ g_assert_not_reached ();
+
+ name = g_strdup_printf ("%s@COCKPIT.MOCK", g_get_user_name ());
+
+ /* Do a kerberos authentication */
+ if (krb5_parse_name (test->krb, name, &principal))
+ g_assert_not_reached ();
+ if (krb5_get_init_creds_opt_alloc (test->krb, &opt))
+ g_assert_not_reached ();
+ if (krb5_get_init_creds_opt_set_out_ccache (test->krb, opt, test->ccache))
+ g_assert_not_reached ();
+
+ code = krb5_get_init_creds_password (test->krb, &creds, principal, "marmalade",
+ NULL, NULL, 0, NULL, opt);
+
+ krb5_free_principal (test->krb, principal);
+ krb5_get_init_creds_opt_free (test->krb, opt);
+
+ g_assert (code != ENOMEM);
+ if (code != 0)
+ {
+ g_critical ("couldn't kinit for %s: %s", name, krb5_get_error_message (test->krb, code));
+ return;
+ }
+
+ krb5_free_cred_contents (test->krb, &creds);
+ g_free (name);
+
+ if (krb5_cc_get_full_name (test->krb, test->ccache, &test->ccache_name) != 0)
+ g_assert_not_reached ();
+
+ /* Sets the credential cache GSSAPI to use (for this thread) */
+ status = gss_krb5_ccache_name (&minor, test->ccache_name, NULL);
+ g_assert_cmpint (status, ==, 0);
+}
+
+static void
+teardown (TestCase *test,
+ gconstpointer data)
+{
+ if (!mock_kdc_available)
+ return;
+
+ if (test->ccache)
+ krb5_cc_close (test->krb, test->ccache);
+ if (test->ccache_name)
+ krb5_free_string (test->krb, test->ccache_name);
+ if (test->krb)
+ krb5_free_context (test->krb);
+
+ mock_kdc_down ();
+
+ g_clear_object (&test->auth);
+}
+
+static void
+on_ready_get_result (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **retval = user_data;
+ g_assert (retval != NULL);
+ g_assert (*retval == NULL);
+ *retval = g_object_ref (result);
+}
+
+static void
+assert_gss_status_msg (const gchar *domain,
+ const gchar *file,
+ gint line,
+ const gchar *func,
+ const gchar *expr,
+ const gchar *cmp,
+ OM_uint32 expected,
+ OM_uint32 major_status,
+ OM_uint32 minor_status)
+{
+ OM_uint32 major, minor;
+ OM_uint32 ctx = 0;
+ gss_buffer_desc status;
+ gboolean had_minor;
+ GString *result;
+
+ result = g_string_new ("");
+ g_string_printf (result, "assertion failed (%s): (%u %s %u)",
+ expr, (guint)expected, cmp, (guint)major_status);
+
+ for (;;)
+ {
+ major = gss_display_status (&minor, major_status, GSS_C_GSS_CODE,
+ GSS_C_NO_OID, &ctx, &status);
+ if (GSS_ERROR (major))
+ break;
+
+ if (result->len > 0)
+ g_string_append (result, ": ");
+
+ g_string_append_len (result, status.value, status.length);
+ gss_release_buffer (&minor, &status);
+
+ if (!ctx)
+ break;
+ }
+
+ ctx = 0;
+ had_minor = FALSE;
+ for (;;)
+ {
+ major = gss_display_status (&minor, minor_status, GSS_C_MECH_CODE,
+ GSS_C_NULL_OID, &ctx, &status);
+ if (GSS_ERROR (major))
+ break;
+
+ if (status.length)
+ {
+ if (!had_minor)
+ g_string_append (result, " (");
+ else
+ g_string_append (result, ", ");
+ had_minor = TRUE;
+ g_string_append_len (result, status.value, status.length);
+ }
+
+ gss_release_buffer (&minor, &status);
+
+ if (!ctx)
+ break;
+ }
+
+ if (had_minor)
+ g_string_append (result, ")");
+
+ g_assertion_message (domain, file ,line, func, result->str);
+ g_string_free (result, TRUE);
+}
+
+#define assert_gss_status(status, cmp, expect, minor) \
+ do { OM_uint32 __st = (status); OM_uint32 __ex = (expect); \
+ if (__st cmp __ex) ; else \
+ assert_gss_status_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+ #expect " " #cmp " " #status, #cmp, (__ex), (__st), (minor)); \
+ } while (0);
+
+static void
+build_authorization_header (GHashTable *headers,
+ gss_buffer_desc *buffer)
+{
+ gchar *encoded;
+ gchar *value;
+
+ if (buffer->length)
+ {
+ encoded = g_base64_encode (buffer->value, buffer->length);
+ value = g_strdup_printf ("Negotiate %s", encoded);
+ g_free (encoded);
+ }
+ else
+ {
+ value = g_strdup ("Negotiate");
+ }
+
+ g_hash_table_replace (headers, g_strdup ("Authorization"), value);
+}
+
+static void
+include_cookie_as_if_client (GHashTable *resp_headers,
+ GHashTable *req_headers)
+{
+ gchar *cookie;
+ gchar *end;
+
+ cookie = g_strdup (g_hash_table_lookup (resp_headers, "Set-Cookie"));
+ g_assert (cookie != NULL);
+ end = strchr (cookie, ';');
+ g_assert (end != NULL);
+ end[0] = '\0';
+
+ g_hash_table_insert (req_headers, g_strdup ("Cookie"), cookie);
+}
+
+static void
+test_authenticate (TestCase *test,
+ gconstpointer data)
+{
+ GHashTable *in_headers;
+ GHashTable *out_headers;
+ GAsyncResult *result = NULL;
+ gss_ctx_id_t ctx = GSS_C_NO_CONTEXT;
+ OM_uint32 status;
+ OM_uint32 minor;
+ gss_buffer_desc input = GSS_C_EMPTY_BUFFER;
+ gss_buffer_desc output = GSS_C_EMPTY_BUFFER;
+ gss_name_t name = GSS_C_NO_NAME;
+ OM_uint32 flags = 0;
+ CockpitCreds *creds;
+ CockpitWebService *service;
+ JsonObject *response;
+ GError *error = NULL;
+
+ cockpit_setenv_check ("COCKPIT_TEST_KEEP_PATH", "1", TRUE);
+ cockpit_setenv_check ("COCKPIT_TEST_KEEP_KTAB", "1", TRUE);
+
+ if (!mock_kdc_available)
+ {
+ g_test_skip ("mock kdc not available to test against");
+ return;
+ }
+
+ in_headers = cockpit_web_server_new_table ();
+ out_headers = cockpit_web_server_new_table ();
+
+ input.value = "host@localhost";
+ input.length = strlen (input.value) + 1;
+ status = gss_import_name (&minor, &input, GSS_C_NT_HOSTBASED_SERVICE, &name);
+ assert_gss_status (status, ==, GSS_S_COMPLETE, minor);
+
+ input.length = 0;
+ status = gss_init_sec_context (&minor, GSS_C_NO_CREDENTIAL, &ctx, name, GSS_C_NO_OID,
+ GSS_C_MUTUAL_FLAG, GSS_C_INDEFINITE, GSS_C_NO_CHANNEL_BINDINGS,
+ &input, NULL, &output, &flags, NULL);
+ assert_gss_status (status, ==, GSS_S_CONTINUE_NEEDED, minor);
+
+ build_authorization_header (in_headers, &output);
+ gss_release_buffer (&minor, &output);
+
+ cockpit_auth_login_async (test->auth, WebRequest(.path="/cockpit+test", .headers=in_headers), on_ready_get_result, &result);
+ g_hash_table_unref (in_headers);
+
+ while (result == NULL)
+ g_main_context_iteration (NULL, TRUE);
+
+ response = cockpit_auth_login_finish (test->auth, result, NULL, out_headers, &error);
+ g_object_unref (result);
+ g_assert_no_error (error);
+ g_assert (response != NULL);
+ json_object_unref (response);
+
+ gss_release_name (&minor, &name);
+ gss_delete_sec_context (&minor, &ctx, &output);
+
+ include_cookie_as_if_client (out_headers, out_headers);
+
+ service = cockpit_auth_check_cookie (test->auth, WebRequest(.path="/cockpit+test", .headers=out_headers));
+ g_assert (service != NULL);
+
+ creds = cockpit_web_service_get_creds (service);
+ g_assert_cmpstr ("cockpit+test", ==, cockpit_creds_get_application (creds));
+ g_assert (NULL == cockpit_creds_get_password (creds));
+
+ g_unsetenv ("COCKPIT_TEST_KEEP_PATH");
+
+ g_hash_table_unref (out_headers);
+ g_object_unref (service);
+}
+
+struct {
+ GHashTable *environ;
+ gboolean stopped;
+ GPid pid;
+} mock_kdc;
+
+static void
+on_kdc_child (GPid pid,
+ gint status,
+ gpointer user_data)
+{
+ GError *error = NULL;
+
+ mock_kdc_available = FALSE;
+ if (!mock_kdc.stopped)
+ {
+ g_spawn_check_exit_status (status, &error);
+ g_assert_no_error (error);
+ g_critical ("mock-kdc exited prematurely");
+ }
+}
+
+static void
+on_kdc_setup (gpointer user_data)
+{
+ /* Kill all sub processes when this process exits */
+ if (prctl (PR_SET_PDEATHSIG, SIGTERM) < 0)
+ g_critical ("prctl failed: %s", g_strerror (errno));
+
+ /* Start a new session for this process */
+ setsid ();
+}
+
+static void
+mock_kdc_start (void)
+{
+ GString *input;
+ GError *error = NULL;
+ gint out_fd;
+ gsize len;
+ gssize ret;
+ gchar **vars;
+ gchar *pos;
+ gint i;
+
+ const gchar *argv[] = {
+ SRCDIR "/src/ws/mock-kdc",
+ NULL
+ };
+
+ mock_kdc_available = FALSE;
+
+ g_spawn_async_with_pipes (BUILDDIR, (gchar **)argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD,
+ on_kdc_setup, NULL, &mock_kdc.pid, NULL, &out_fd, NULL, &error);
+ g_assert_no_error (error);
+
+ g_child_watch_add (mock_kdc.pid, on_kdc_child, NULL);
+
+ /*
+ * mock-kdc prints environment vars on stdout, and then closes stdout
+ * This also lets us know when it has initialized.
+ */
+ input = g_string_new ("");
+ for (;;)
+ {
+ len = input->len;
+ g_string_set_size (input, len + 256);
+ ret = read (out_fd, input->str + len, 256);
+ if (ret < 0)
+ {
+ if (errno == EAGAIN || errno == EINTR)
+ continue;
+
+ g_error ("couldn't read from mock-kdc: %s", g_strerror (errno));
+ break;
+ }
+
+ g_string_set_size (input, len + ret);
+ if (strstr (input->str, "starting..."))
+ {
+ mock_kdc_available = TRUE;
+ break;
+ }
+ else if (ret == 0)
+ {
+ break;
+ }
+ }
+
+ /* Parse into a table of environment variables */
+ mock_kdc.environ = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ vars = g_strsplit (input->str, "\n", -1);
+ for (i = 0; vars[i] != NULL; i++)
+ {
+ pos = strchr (vars[i], '=');
+ if (pos)
+ {
+ *pos = '\0';
+ g_hash_table_replace (mock_kdc.environ, vars[i], pos + 1);
+ }
+ else
+ {
+ g_free (vars[i]);
+ }
+ }
+
+ g_string_free (input, TRUE);
+ g_free (vars);
+}
+
+static void
+mock_kdc_up (void)
+{
+ GHashTableIter iter;
+ const gchar *name;
+ const gchar *value;
+ g_hash_table_iter_init (&iter, mock_kdc.environ);
+ while (g_hash_table_iter_next (&iter, (gpointer *)&name, (gpointer *)&value))
+ cockpit_setenv_check (name, value, TRUE);
+
+ /* Explicitly tell server side of GSSAPI about our keytab */
+ value = g_hash_table_lookup (mock_kdc.environ, "KRB5_KTNAME");
+ if (value)
+ cockpit_setenv_check ("KRB5_KTNAME", value, TRUE);
+}
+
+static void
+mock_kdc_down (void)
+{
+ GHashTableIter iter;
+ const gchar *name;
+ g_hash_table_iter_init (&iter, mock_kdc.environ);
+ while (g_hash_table_iter_next (&iter, (gpointer *)&name, NULL))
+ g_unsetenv (name);
+}
+
+static void
+mock_kdc_stop (void)
+{
+ mock_kdc.stopped = TRUE;
+ if (mock_kdc_available)
+ {
+ if (kill (-mock_kdc.pid, SIGTERM) < 0)
+ g_error ("couldn't kill mock-kdc: %s", g_strerror (errno));
+ }
+ if (mock_kdc.environ)
+ g_hash_table_destroy (mock_kdc.environ);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ int ret;
+
+ cockpit_ws_session_program = BUILDDIR "/cockpit-session";
+
+ cockpit_test_init (&argc, &argv);
+
+ if (g_strcmp0 (g_get_user_name (), "root") != 0)
+ mock_kdc_start ();
+
+ g_test_add ("/kerberos/authenticate", TestCase, NULL,
+ setup, test_authenticate, teardown);
+
+ ret = g_test_run ();
+
+ mock_kdc_stop ();
+
+ return ret;
+}
diff --git a/src/ws/test-server.c b/src/ws/test-server.c
new file mode 100644
index 0000000..53a26c6
--- /dev/null
+++ b/src/ws/test-server.c
@@ -0,0 +1,1003 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitwebservice.h"
+#include "cockpitchannelresponse.h"
+#include "cockpitchannelsocket.h"
+#include "cockpitws.h"
+
+#include "common/cockpitpipe.h"
+#include "common/cockpitconf.h"
+#include "common/cockpitpipetransport.h"
+#include "common/cockpitsystem.h"
+#include "testlib/cockpittest.h"
+#include "common/cockpitwebserver.h"
+#include "common/cockpitwebinject.h"
+
+#include "ws/mock-service.h"
+
+#include <gio/gio.h>
+#include <glib-unix.h>
+#include <glib/gstdio.h>
+#include <string.h>
+
+/* Override from cockpitconf.c */
+extern const gchar *cockpit_config_file;
+
+static GMainLoop *loop = NULL;
+static gboolean signalled = FALSE;
+static int exit_code = 0;
+static gint server_port = 0;
+static gchar **bridge_argv;
+static const gchar *bus_address;
+static const gchar *direct_address;
+static gchar **server_roots;
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static GObject *exported = NULL;
+static GObject *exported_b = NULL;
+static GObject *direct = NULL;
+static GObject *direct_b = NULL;
+
+static GDBusMessage *
+on_filter_func (GDBusConnection *connection,
+ GDBusMessage *message,
+ gboolean incoming,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ GDBusMessage *reply = NULL;
+
+ if (incoming)
+ {
+ if (g_dbus_message_get_message_type (message) == G_DBUS_MESSAGE_TYPE_METHOD_CALL &&
+ g_str_equal (g_dbus_message_get_path (message), "/bork") &&
+ g_str_equal (g_dbus_message_get_interface (message), "borkety.Bork") &&
+ g_str_equal (g_dbus_message_get_member (message), "Echo"))
+ {
+ reply = g_dbus_message_new_method_reply (message);
+ g_dbus_message_set_body (reply, g_dbus_message_get_body (message));
+ }
+
+ if (g_dbus_message_get_message_type (message) == G_DBUS_MESSAGE_TYPE_SIGNAL &&
+ g_str_equal (g_dbus_message_get_path (message), "/bork") &&
+ g_str_equal (g_dbus_message_get_interface (message), "borkety.Bork"))
+ {
+ reply = g_dbus_message_new_signal ("/bork", "borkety.Bork",
+ g_dbus_message_get_member (message));
+ g_dbus_message_set_body (reply, g_dbus_message_get_body (message));
+ }
+
+ if (reply)
+ {
+ g_dbus_connection_send_message (connection, reply, G_DBUS_SEND_MESSAGE_FLAGS_NONE, NULL, &error);
+ if (error != NULL)
+ {
+ g_warning ("Couldn't send DBus message: %s", error->message);
+ g_error_free (error);
+ }
+ g_object_unref (reply);
+ g_object_unref (message);
+ return NULL;
+ }
+ }
+
+ return message;
+}
+
+static void
+on_bus_acquired (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ exported = mock_service_create_and_export (connection, "/otree");
+ exported_b = mock_service_create_and_export (connection, "/different");
+ g_dbus_connection_add_filter (connection, on_filter_func, NULL, NULL);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static gboolean
+mock_http_info (CockpitWebRequest *request,
+ CockpitWebResponse *response)
+{
+ g_autoptr(JsonObject) info = json_object_new ();
+ json_object_set_boolean_member (info, "pybridge", strstr (bridge_argv[0], "py") != NULL);
+ json_object_set_boolean_member (info, "skip_slow_tests", g_getenv ("COCKPIT_SKIP_SLOW_TESTS") != NULL);
+
+ g_autoptr(GBytes) bytes = cockpit_json_write_bytes (info);
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ return TRUE;
+}
+
+static gboolean
+mock_http_qs (CockpitWebRequest *request,
+ CockpitWebResponse *response)
+{
+ const gchar *qs;
+ GBytes *bytes;
+
+ qs = cockpit_web_request_get_query (request);
+ if (!qs)
+ {
+ cockpit_web_response_error (response, 400, NULL, "No query string");
+ }
+ else
+ {
+ bytes = g_bytes_new (qs, strlen (qs));
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+on_timeout_send (gpointer data)
+{
+ CockpitWebResponse *response = data;
+ gint *at = g_object_get_data (data, "at");
+ gchar *string;
+ GBytes *bytes;
+
+ string = g_strdup_printf ("%d ", *at);
+ (*at) += 1;
+
+ bytes = g_bytes_new_take (string, strlen (string));
+ cockpit_web_response_queue (response, bytes);
+ g_bytes_unref (bytes);
+
+ if (*at == 10)
+ {
+ cockpit_web_response_complete (response);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+mock_http_stream (CockpitWebResponse *response)
+{
+ cockpit_web_response_headers (response, 200, "OK", -1, NULL);
+ g_object_set_data_full (G_OBJECT (response), "at", g_new0 (gint, 1), g_free);
+ g_timeout_add_full (G_PRIORITY_DEFAULT, 100, on_timeout_send,
+ g_object_ref (response), g_object_unref);
+
+ return TRUE;
+}
+
+static gboolean
+mock_http_headers (CockpitWebResponse *response,
+ GHashTable *in_headers)
+{
+ GHashTableIter iter;
+ GHashTable *headers;
+ gpointer name, value;
+
+ headers = cockpit_web_server_new_table();
+ g_hash_table_iter_init (&iter, in_headers);
+ while (g_hash_table_iter_next (&iter, &name, &value))
+ {
+ if (g_str_has_prefix (name, "Header"))
+ g_hash_table_insert (headers, g_strdup (name), g_strdup (value));
+ }
+ g_hash_table_replace (headers, g_strdup ("Header3"), g_strdup ("three"));
+ g_hash_table_replace (headers, g_strdup ("Header4"), g_strdup ("marmalade"));
+
+ cockpit_web_response_headers_full (response, 201, "Yoo Hoo", -1, headers);
+ cockpit_web_response_complete (response);
+
+ g_hash_table_unref (headers);
+
+ return TRUE;
+}
+
+static gboolean
+mock_http_host (CockpitWebResponse *response,
+ GHashTable *in_headers)
+{
+ GHashTable *headers;
+
+ headers = cockpit_web_server_new_table();
+ g_hash_table_insert (headers, g_strdup ("Host"), g_strdup (g_hash_table_lookup (in_headers, "Host")));
+ cockpit_web_response_headers_full (response, 201, "Yoo Hoo", -1, headers);
+ cockpit_web_response_complete (response);
+
+ g_hash_table_unref (headers);
+
+ return TRUE;
+}
+
+static gboolean
+mock_http_connection (CockpitWebResponse *response)
+{
+ GIOStream *io;
+ GBytes *bytes;
+ gchar *output;
+
+ /* Lets caller have an indication of which IO stream is being used */
+
+ io = cockpit_web_response_get_stream (response);
+ output = g_strdup_printf ("%p", io);
+ bytes = g_bytes_new_take (output, strlen (output));
+
+ cockpit_web_response_content (response, NULL, bytes, NULL);
+ g_bytes_unref (bytes);
+
+ return TRUE;
+}
+
+static gboolean
+mock_http_headonly (CockpitWebRequest *request,
+ CockpitWebResponse *response)
+{
+ if (!g_str_equal (cockpit_web_request_get_method (request), "HEAD"))
+ {
+ cockpit_web_response_error (response, 400, NULL, "Only HEAD allowed on this path");
+ }
+ else
+ {
+ const char *input_data = cockpit_web_request_lookup_header (request, "InputData");
+ if (!input_data)
+ {
+ cockpit_web_response_error (response, 400, NULL, "Requires InputData header");
+ return TRUE;
+ }
+
+ g_autoptr(GHashTable) headers = cockpit_web_server_new_table();
+ g_hash_table_insert (headers, g_strdup ("InputDataLength"), g_strdup_printf ("%zu", strlen (input_data)));
+ cockpit_web_response_headers_full (response, 200, "OK", -1, headers);
+ cockpit_web_response_complete (response);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+mock_http_expect_warnings (CockpitWebResponse *response,
+ GLogLevelFlags warnings)
+{
+ g_log_set_always_fatal (warnings | G_LOG_LEVEL_ERROR);
+
+ cockpit_web_response_headers_full (response, 200, "OK", 0, NULL);
+ cockpit_web_response_complete (response);
+
+ return TRUE;
+}
+
+static gboolean
+on_handle_mock (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ gpointer data)
+{
+ g_assert (g_str_has_prefix (path, "/mock/"));
+ path += 5;
+
+ if (g_str_equal (path, "/info"))
+ return mock_http_info (request, response);
+ if (g_str_equal (path, "/qs"))
+ return mock_http_qs (request, response);
+ if (g_str_equal (path, "/stream"))
+ return mock_http_stream (response);
+ if (g_str_equal (path, "/headers"))
+ return mock_http_headers (response, headers);
+ if (g_str_equal (path, "/host"))
+ return mock_http_host (response, headers);
+ if (g_str_equal (path, "/connection"))
+ return mock_http_connection (response);
+ if (g_str_equal (path, "/headonly"))
+ return mock_http_headonly (request, response);
+ if (g_str_equal (path, "/expect-warnings"))
+ return mock_http_expect_warnings (response, 0);
+ if (g_str_equal (path, "/dont-expect-warnings"))
+ return mock_http_expect_warnings (response, G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL);
+ else
+ return FALSE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+/* Called by @server when handling HTTP requests to /socket - runs in a separate
+ * thread dedicated to the request so it may do blocking I/O
+ */
+
+static CockpitWebService *service;
+static CockpitPipe *bridge;
+
+static gboolean
+on_transport_control (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload,
+ gpointer data)
+{
+ gboolean *flag = data;
+ g_assert (flag != NULL);
+
+ if (g_str_equal (command, "init"))
+ *flag = TRUE;
+
+ return FALSE;
+}
+
+static gboolean
+on_handle_stream_socket (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ gpointer user_data)
+{
+ const gchar *path = cockpit_web_request_get_path (request);
+
+ CockpitTransport *transport;
+ const gchar *query = NULL;
+ CockpitCreds *creds;
+ int session_stdin = -1;
+ int session_stdout = -1;
+ GError *error = NULL;
+ gboolean ready = FALSE;
+ gulong handler;
+ GPid pid = 0;
+
+ gchar *value;
+ gchar **argv;
+
+ if (!g_str_has_prefix (path, "/cockpit/socket"))
+ return FALSE;
+
+ if (path[15] == '?')
+ {
+ query = path + 16;
+ }
+ else if (path[15] != '\0')
+ {
+ return FALSE;
+ }
+
+ if (service)
+ {
+ g_object_ref (service);
+ }
+ else
+ {
+ g_clear_object (&bridge);
+
+ value = g_strdup_printf ("%d", server_port);
+
+ argv = g_strdupv (bridge_argv);
+ if (query)
+ argv[g_strv_length (argv) - 1] = g_strdup (query);
+
+ g_spawn_async_with_pipes (NULL, argv, NULL,
+ G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
+ NULL, NULL, &pid, &session_stdin, &session_stdout, NULL, &error);
+
+ g_free (argv);
+ g_free (value);
+
+ if (error)
+ {
+ g_critical ("couldn't run bridge %s: %s", bridge_argv[0], error->message);
+ return FALSE;
+ }
+
+ bridge = g_object_new (COCKPIT_TYPE_PIPE,
+ "name", "test-server-bridge",
+ "in-fd", session_stdout,
+ "out-fd", session_stdin,
+ "pid", pid,
+ NULL);
+
+ creds = cockpit_creds_new ("test",
+ COCKPIT_CRED_CSRF_TOKEN, "myspecialtoken",
+ NULL);
+
+ transport = cockpit_pipe_transport_new (bridge);
+ service = cockpit_web_service_new (creds, transport);
+
+ /* Manually created services won't be init'd yet, wait for that before sending data */
+ handler = g_signal_connect (transport, "control", G_CALLBACK (on_transport_control), &ready);
+
+ while (!ready)
+ g_main_context_iteration (NULL, TRUE);
+
+ cockpit_creds_unref (creds);
+ g_object_unref (transport);
+
+ /* Clear the pointer automatically when service is done */
+ g_object_add_weak_pointer (G_OBJECT (service), (gpointer *)&service);
+
+ g_signal_handler_disconnect (transport, handler);
+ }
+
+ cockpit_web_service_socket (service, request);
+
+ /* Keeps ref on itself until it closes */
+ g_object_unref (service);
+
+ return TRUE;
+}
+
+static void
+on_echo_socket_message (WebSocketConnection *self,
+ WebSocketDataType type,
+ GBytes *message,
+ gpointer user_data)
+{
+ GByteArray *array = g_bytes_unref_to_array (g_bytes_ref (message));
+ GBytes *payload;
+ guint i;
+
+ /* Capitalize and relay back */
+ for (i = 0; i < array->len; i++)
+ array->data[i] = g_ascii_toupper (array->data[i]);
+
+ payload = g_byte_array_free_to_bytes (array);
+ web_socket_connection_send (self, type, NULL, payload);
+ g_bytes_unref (payload);
+}
+
+static void
+on_echo_socket_close (WebSocketConnection *ws,
+ gpointer user_data)
+{
+ g_object_unref (ws);
+}
+
+static gboolean
+on_handle_stream_external (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ gpointer user_data)
+{
+ const gchar *path = cockpit_web_request_get_path (request);
+ GIOStream *io_stream = cockpit_web_request_get_io_stream (request);
+ GByteArray *input = cockpit_web_request_get_buffer (request);
+ GHashTable *headers = cockpit_web_request_get_headers (request);
+
+ gboolean handled = FALSE;
+ const gchar *upgrade;
+ CockpitCreds *creds;
+ const gchar *expected;
+ const gchar *segment;
+ JsonObject *open = NULL;
+ GBytes *bytes;
+ guchar *decoded;
+ gsize length;
+
+ if (g_str_has_prefix (path, "/cockpit/echosocket"))
+ {
+ const gchar *protocols[] = { "cockpit1", NULL };
+ const gchar *origins[2] = { NULL, NULL };
+ WebSocketConnection *ws = NULL;
+ gchar *url;
+
+ url = g_strdup_printf ("ws://localhost:%u%s", server_port, path);
+ origins[0] = g_strdup_printf ("http://localhost:%u", server_port);
+
+ ws = web_socket_server_new_for_stream (url, (const gchar **)origins,
+ protocols, io_stream, headers, input);
+
+ g_signal_connect (ws, "message", G_CALLBACK (on_echo_socket_message), NULL);
+ g_signal_connect (ws, "close", G_CALLBACK (on_echo_socket_close), NULL);
+ return TRUE;
+ }
+
+ if (!g_str_has_prefix (path, "/cockpit/channel/"))
+ return FALSE;
+
+ /* Remove /cockpit/channel/ part */
+ segment = path + 17;
+
+ if (service)
+ {
+ creds = cockpit_web_service_get_creds (service);
+ g_return_val_if_fail (creds != NULL, FALSE);
+
+ expected = cockpit_creds_get_csrf_token (creds);
+ g_return_val_if_fail (expected != NULL, FALSE);
+
+ /* No such path is valid */
+ if (g_str_equal (segment, expected))
+ {
+ decoded = g_base64_decode (cockpit_web_request_get_query (request), &length);
+ if (decoded)
+ {
+ bytes = g_bytes_new_take (decoded, length);
+ if (!cockpit_transport_parse_command (bytes, NULL, NULL, &open))
+ {
+ open = NULL;
+ g_message ("invalid external channel query");
+ }
+ g_bytes_unref (bytes);
+ }
+ }
+
+ if (open)
+ {
+ upgrade = g_hash_table_lookup (headers, "Upgrade");
+ if (upgrade && g_ascii_strcasecmp (upgrade, "websocket") == 0)
+ {
+ cockpit_channel_socket_open (service, open, request);
+ handled = TRUE;
+ }
+ else
+ {
+ cockpit_channel_response_open (service, request, open);
+ handled = TRUE;
+ }
+
+ json_object_unref (open);
+ }
+ }
+
+ return handled;
+}
+
+static void
+inject_address (CockpitWebResponse *response,
+ const gchar *name,
+ const gchar *value)
+{
+ GBytes *inject = NULL;
+ CockpitWebFilter *filter = NULL;
+ gchar *line = NULL;
+
+ if (value)
+ {
+ line = g_strconcat ("\n<script>\nvar ", name, " = '", value, "';\n</script>", NULL);
+
+ inject = g_bytes_new (line, strlen (line));
+ filter = cockpit_web_inject_new ("<head>", inject, 1);
+ g_bytes_unref (inject);
+
+ cockpit_web_response_add_filter (response, filter);
+ g_object_unref (filter);
+ }
+
+ g_free (line);
+}
+
+static void
+handle_raw_data (CockpitWebResponse *response,
+ const gchar *data)
+{
+ GBytes *block;
+
+ /* For testing code that uses "manifests" return empty manifests for now */
+ block = g_bytes_new_static (data, strlen (data));
+ cockpit_web_response_content (response, NULL, block, NULL);
+ g_bytes_unref (block);
+}
+
+static void
+handle_manifests_js (CockpitWebResponse *response)
+{
+ /* For testing code that uses "manifests" return empty manifests for now */
+ handle_raw_data (response, "define({ });");
+}
+
+static void
+handle_manifests_json (CockpitWebResponse *response)
+{
+ /* For testing code that uses "/pkg/manifests.json" return empty manifests for now */
+ handle_raw_data (response, "{ }");
+}
+
+static void
+handle_package_file (CockpitWebServer *server,
+ CockpitWebResponse *response,
+ gchar **parts)
+{
+ gchar *rebuilt;
+
+ /* TODO: This needs a better implementation later, when the tests aren't all broken */
+ if (g_strcmp0 (parts[2], "system") == 0)
+ {
+ g_free (parts[2]);
+ parts[2] = g_strdup ("systemd");
+ }
+ if (g_strcmp0 (parts[2], "base1") == 0)
+ {
+ g_free (parts[1]);
+ parts[1] = g_strdup ("src");
+ }
+ else if (g_strcmp0 (parts[2], "lib") == 0)
+ {
+ g_free (parts[1]);
+ parts[1] = g_strdup("lib");
+ parts++;
+ }
+
+ rebuilt = g_strjoinv ("/", parts);
+ cockpit_web_response_file (response, rebuilt, (const gchar **)server_roots);
+ g_free (rebuilt);
+}
+
+static gboolean
+on_handle_resource (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ gpointer user_data)
+{
+ gchar **parts;
+
+ g_assert (g_str_has_prefix (path, "/pkg"));
+
+ cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_NO_CACHE);
+
+ parts = g_strsplit (path, "/", -1);
+ if (g_strcmp0 (parts[2], "manifests.js") == 0 && !parts[3])
+ handle_manifests_js (response);
+ else if (g_strcmp0 (parts[2], "manifests.json") == 0 && !parts[3])
+ handle_manifests_json (response);
+ else
+ handle_package_file (server, response, parts);
+
+ g_strfreev (parts);
+ return TRUE;
+}
+
+static gboolean
+on_handle_source (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ gpointer user_data)
+{
+ cockpit_web_response_set_cache_type (response, COCKPIT_WEB_RESPONSE_NO_CACHE);
+ if (g_str_has_suffix (path, ".html"))
+ {
+ inject_address (response, "bus_address", bus_address);
+ inject_address (response, "direct_address", direct_address);
+ }
+ cockpit_web_response_file_or_gz (response, TRUE, path, (const gchar **)server_roots);
+ return TRUE;
+}
+
+static gboolean
+on_handle_favicon (CockpitWebServer *server,
+ CockpitWebRequest *request,
+ const gchar *path,
+ GHashTable *headers,
+ CockpitWebResponse *response,
+ gpointer user_data)
+{
+ const char* roots[] = { SRCDIR "/src/branding/default", NULL };
+ cockpit_web_response_file (response, NULL, roots);
+ return TRUE;
+}
+
+static void
+server_ready (void)
+{
+ const gchar *roots[] = { ".", SRCDIR, BUILDDIR, NULL };
+ GError *error = NULL;
+ CockpitWebServer *server;
+ gchar *url;
+ const gchar *address_fd_str = g_getenv("TEST_SERVER_ADDRESS_FD");
+
+ if (!isatty (1))
+ server_port = 0; /* select one automatically */
+ else
+ server_port = 8765;
+
+ server_roots = cockpit_web_response_resolve_roots (roots);
+ server = cockpit_web_server_new (NULL, /* TLS cert */
+ COCKPIT_WEB_SERVER_NONE);
+ server_port = cockpit_web_server_add_inet_listener (server, NULL, server_port, &error);
+ g_assert_no_error (error);
+ g_assert (server_port != 0);
+
+
+ g_signal_connect (server, "handle-stream",
+ G_CALLBACK (on_handle_stream_socket), NULL);
+ g_signal_connect (server, "handle-stream",
+ G_CALLBACK (on_handle_stream_external), NULL);
+ g_signal_connect (server, "handle-resource::/pkg/",
+ G_CALLBACK (on_handle_resource), NULL);
+ g_signal_connect (server, "handle-resource::/dist/",
+ G_CALLBACK (on_handle_source), NULL);
+ g_signal_connect (server, "handle-resource::/qunit/",
+ G_CALLBACK (on_handle_source), NULL);
+ g_signal_connect (server, "handle-resource::/favicon.ico",
+ G_CALLBACK (on_handle_favicon), NULL);
+ g_signal_connect (server, "handle-resource::/mock/",
+ G_CALLBACK (on_handle_mock), NULL);
+
+ url = g_strdup_printf("http://localhost:%d", server_port);
+
+ cockpit_web_server_start (server);
+
+ if (address_fd_str)
+ {
+ int fd = atoi (address_fd_str);
+ int r = write (fd, url, strlen (url));
+ close (fd);
+ g_assert (r == strlen (url));
+ }
+ else if (!isatty (1))
+ {
+ g_print ("%s\n", url);
+ }
+ else
+ {
+ g_print ("**********************************************************************\n"
+ "Please connect a supported web browser to\n"
+ "\n"
+ " %s/qunit/base1/test-dbus.html\n"
+ "\n"
+ "and check that the test suite passes. Press Ctrl+C to exit.\n"
+ "**********************************************************************\n"
+ "\n", url);
+ }
+
+ g_free (url);
+}
+
+static gboolean name_acquired = FALSE;
+static gboolean second_acquired = FALSE;
+
+static void
+on_name_acquired (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ name_acquired = TRUE;
+ if (name_acquired && second_acquired)
+ server_ready ();
+}
+
+static void
+on_second_acquired (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ second_acquired = TRUE;
+ if (name_acquired && second_acquired)
+ server_ready ();
+}
+
+static void
+on_name_lost (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+
+}
+
+static gboolean
+on_new_direct_connection (GDBusServer *server,
+ GDBusConnection *connection,
+ gpointer unused)
+{
+ direct = mock_service_create_and_export (connection, "/otree");
+ direct_b = mock_service_create_and_export (connection, "/different");
+ g_dbus_connection_add_filter (connection, on_filter_func, NULL, NULL);
+ return TRUE;
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+static void
+setup_path (const char *argv0)
+{
+ const gchar *old = g_getenv ("PATH");
+ gchar *dir = g_path_get_dirname (argv0);
+ gchar *path;
+
+ path = g_strdup_printf ("%s%s%s", dir,
+ old ? ":" : "",
+ old ? old : NULL);
+
+ cockpit_setenv_check ("PATH", path, TRUE);
+
+ g_free (path);
+ g_free (dir);
+}
+
+static gint
+exit_code_from_pipe_status (CockpitPipe *pipe)
+{
+ gint status;
+
+ status = cockpit_pipe_exit_status (pipe);
+ if (WIFEXITED (status))
+ return WEXITSTATUS (status);
+ else if (status != 0)
+ return 1;
+ else
+ return 0;
+}
+
+static void
+on_bridge_done (CockpitPipe *pipe,
+ const gchar *problem,
+ gpointer user_data)
+{
+ exit_code = exit_code_from_pipe_status (pipe);
+ g_main_loop_quit (loop);
+}
+
+static gboolean
+on_signal_done (gpointer data)
+{
+ gboolean first = !signalled;
+ signalled = TRUE;
+
+ if (first)
+ {
+ if (service)
+ cockpit_web_service_disconnect (service);
+ if (bridge)
+ {
+ if (!cockpit_pipe_is_closed (bridge))
+ {
+ g_signal_connect (bridge, "close", G_CALLBACK (on_bridge_done), NULL);
+ return TRUE;
+ }
+
+ exit_code = exit_code_from_pipe_status (bridge);
+ }
+ }
+
+ g_main_loop_quit (loop);
+ return TRUE;
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ GError *error = NULL;
+ guint sig_term;
+ guint sig_int;
+ int i;
+ gchar *rm_rf_argv[] = {"rm", "-rfv", NULL, NULL};
+
+ GOptionEntry entries[] = {
+ { NULL }
+ };
+
+ signal (SIGPIPE, SIG_IGN);
+ /* avoid gvfs (http://bugzilla.gnome.org/show_bug.cgi?id=526454) */
+ cockpit_setenv_check ("GIO_USE_VFS", "local", TRUE);
+
+ /* playground config directory */
+ g_autofree gchar *config_dir = g_dir_make_tmp ("cockpit.config.XXXXXX", NULL);
+ g_assert (config_dir);
+ g_autofree gchar *machines_dir = g_build_filename (config_dir, "cockpit", "machines.d", NULL);
+ g_assert (g_mkdir_with_parents (machines_dir, 0755) == 0);
+
+ cockpit_setenv_check ("PYTHONPATH", SRCDIR "/src", TRUE);
+ cockpit_setenv_check ("XDG_DATA_HOME", SRCDIR "/src/bridge/mock-resource/home", TRUE);
+ cockpit_setenv_check ("XDG_DATA_DIRS", SRCDIR "/src/bridge/mock-resource/system", TRUE);
+ cockpit_setenv_check ("XDG_CONFIG_DIRS", config_dir, TRUE);
+
+ setup_path (argv[0]);
+
+ g_log_set_always_fatal (G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_ERROR);
+
+ sig_term = g_unix_signal_add (SIGTERM, on_signal_done, NULL);
+ sig_int = g_unix_signal_add (SIGINT, on_signal_done, NULL);
+
+ // System cockpit configuration file should not be loaded
+ cockpit_config_file = NULL;
+
+ g_autoptr(GOptionContext) context = g_option_context_new ("- test dbus json server");
+ g_option_context_add_main_entries (context, entries, NULL);
+ g_option_context_set_ignore_unknown_options (context, TRUE);
+ if (!g_option_context_parse (context, &argc, &argv, &error))
+ {
+ g_printerr ("test-server: %s\n", error->message);
+ exit (2);
+ }
+
+ /* This isolates us from affecting other processes during tests */
+ g_autoptr(GTestDBus) bus = g_test_dbus_new (G_TEST_DBUS_NONE);
+ g_test_dbus_up (bus);
+ bus_address = g_test_dbus_get_bus_address (bus);
+
+ g_autofree gchar *guid = g_dbus_generate_guid ();
+ g_autoptr(GDBusServer) direct_dbus_server = g_dbus_server_new_sync (
+ "unix:tmpdir=/tmp",
+ G_DBUS_SERVER_FLAGS_NONE,
+ guid,
+ NULL,
+ NULL,
+ &error);
+ if (direct_dbus_server == NULL)
+ {
+ g_printerr ("test-server: %s\n", error->message);
+ exit (3);
+ }
+
+ /* Skip the program name */
+ argc--;
+ argv++;
+
+ /* Null terminate the bridge command line */
+ bridge_argv = g_new0 (char *, argc + 2);
+ for (i = 0; i < argc; i++)
+ bridge_argv[i] = argv[i];
+
+ /* Default case */
+ if (i == 0)
+ bridge_argv[i] = BUILDDIR "/cockpit-bridge";
+
+ // Use a local ssh session command
+ cockpit_ws_ssh_program = BUILDDIR "/cockpit-ssh";
+
+ loop = g_main_loop_new (NULL, FALSE);
+
+ g_bus_own_name (G_BUS_TYPE_SESSION,
+ "com.redhat.Cockpit.DBusTests.Test",
+ G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | G_BUS_NAME_OWNER_FLAGS_REPLACE,
+ on_bus_acquired,
+ on_name_acquired,
+ on_name_lost,
+ loop,
+ NULL);
+
+ g_bus_own_name (G_BUS_TYPE_SESSION,
+ "com.redhat.Cockpit.DBusTests.Second",
+ G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | G_BUS_NAME_OWNER_FLAGS_REPLACE,
+ NULL,
+ on_second_acquired,
+ on_name_lost,
+ loop,
+ NULL);
+
+ g_signal_connect_object (direct_dbus_server,
+ "new-connection",
+ G_CALLBACK (on_new_direct_connection),
+ NULL, 0);
+ g_dbus_server_start (direct_dbus_server);
+ direct_address = g_dbus_server_get_client_address (direct_dbus_server);
+
+ g_main_loop_run (loop);
+
+ g_source_remove (sig_term);
+ g_source_remove (sig_int);
+
+ g_clear_object (&bridge);
+ g_clear_object (&exported);
+ g_clear_object (&exported_b);
+ g_clear_object (&direct);
+ g_clear_object (&direct_b);
+ g_main_loop_unref (loop);
+
+ g_strfreev (server_roots);
+ g_test_dbus_down (bus);
+ g_free (bridge_argv);
+
+ /* clean up temporary config dir */
+ rm_rf_argv[2] = config_dir;
+ g_spawn_sync (NULL, rm_rf_argv, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL);
+
+ return exit_code;
+}
diff --git a/src/ws/test-webservice.c b/src/ws/test-webservice.c
new file mode 100644
index 0000000..b7f100d
--- /dev/null
+++ b/src/ws/test-webservice.c
@@ -0,0 +1,1354 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "cockpitcreds.h"
+#include "cockpitwebservice.h"
+#include "cockpitws.h"
+
+#include "common/cockpitconf.h"
+#include "common/cockpitjson.h"
+#include "common/cockpitpipe.h"
+#include "common/cockpitpipetransport.h"
+#include "common/cockpitsocket.h"
+#include "testlib/cockpittest.h"
+#include "common/cockpittransport.h"
+#include "common/cockpitwebserver.h"
+#include "common/cockpitwebrequest-private.h"
+
+#include "websocket/websocket.h"
+
+#include <glib.h>
+
+#include <string.h>
+#include <errno.h>
+
+/* Mock override from cockpitconf.c */
+extern const gchar *cockpit_config_file;
+
+#define TIMEOUT 30
+
+#define WAIT_UNTIL(cond) \
+ G_STMT_START \
+ while (!(cond)) g_main_context_iteration (NULL, TRUE); \
+ G_STMT_END
+
+#define PASSWORD "this is the password"
+
+typedef struct {
+ /* setup default transport */
+ CockpitTransport *mock_bridge;
+ GPid mock_bridge_pid;
+
+ /* setup_mock_webserver */
+ CockpitWebServer *web_server;
+ gulong request_handler;
+ GIOStream *connection;
+ CockpitCreds *creds;
+
+ CockpitWebService *service;
+} TestCase;
+
+typedef struct {
+ const char *origin;
+ const char *config;
+ const char *forward;
+ const char *bridge;
+ gboolean for_tls_proxy;
+} TestFixture;
+
+static gboolean
+on_transport_control (CockpitTransport *transport,
+ const char *command,
+ const gchar *channel,
+ JsonObject *options,
+ GBytes *payload,
+ gpointer data)
+{
+ gboolean *flag = data;
+ g_assert (flag != NULL);
+
+ if (g_str_equal (command, "init"))
+ *flag = TRUE;
+
+ return FALSE;
+}
+
+static void
+setup_mock_bridge (TestCase *test,
+ gconstpointer data)
+{
+ const TestFixture *fix = data;
+
+ CockpitPipe *pipe = NULL;
+ const gchar *cmd;
+
+ if (fix && fix->bridge)
+ cmd = fix->bridge;
+ else
+ cmd = BUILDDIR "/mock-echo";
+
+ const gchar *argv[] = {
+ cmd,
+ NULL
+ };
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ test->mock_bridge = cockpit_pipe_transport_new (pipe);
+ g_assert (cockpit_pipe_get_pid (pipe, &test->mock_bridge_pid));
+ g_object_unref (pipe);
+}
+
+static void
+teardown_mock_bridge (TestCase *test,
+ gconstpointer data)
+{
+ if (test->mock_bridge)
+ {
+ cockpit_transport_close (test->mock_bridge, "terminate");
+ g_object_add_weak_pointer (G_OBJECT (test->mock_bridge), (gpointer *)&test->mock_bridge);
+ g_object_unref (test->mock_bridge);
+ g_assert (test->mock_bridge == NULL);
+ }
+}
+
+static gboolean
+handle_stream (CockpitWebServer *web_server,
+ CockpitWebRequest *request,
+ gpointer user_data)
+{
+ TestCase *test = user_data;
+
+ g_assert (test->web_server == web_server);
+ cockpit_web_service_socket (test->service, request);
+ g_signal_handler_disconnect (web_server, test->request_handler);
+ test->request_handler = 0;
+
+ return TRUE;
+}
+
+static void
+setup_mock_webserver (TestCase *test,
+ gconstpointer data)
+{
+ const TestFixture *fixture = data;
+ GBytes *password;
+
+ /* Zero port makes server choose its own */
+ cockpit_config_file = fixture ? fixture->config : NULL;
+
+ CockpitWebServerFlags flags = COCKPIT_WEB_SERVER_NONE;
+ if (fixture && fixture->for_tls_proxy)
+ flags |= COCKPIT_WEB_SERVER_FOR_TLS_PROXY;
+
+ test->web_server = cockpit_web_server_new (NULL, flags);
+ cockpit_web_server_set_protocol_header (test->web_server, cockpit_conf_string ("WebService", "ProtocolHeader"));
+ test->request_handler = g_signal_connect (test->web_server, "handle-stream", G_CALLBACK (handle_stream), test);
+ test->connection = cockpit_web_server_connect (test->web_server);
+
+ password = g_bytes_new_take (g_strdup (PASSWORD), strlen (PASSWORD));
+ test->creds = cockpit_creds_new ("cockpit",
+ COCKPIT_CRED_USER, "me",
+ COCKPIT_CRED_PASSWORD, password,
+ COCKPIT_CRED_CSRF_TOKEN, "my-csrf-token",
+ NULL);
+ g_bytes_unref (password);
+}
+
+static void
+teardown_mock_webserver (TestCase *test,
+ gconstpointer data)
+{
+ g_clear_object (&test->connection);
+ g_clear_object (&test->web_server);
+ g_assert (test->request_handler == 0);
+ g_assert (test->service == NULL);
+ if (test->creds)
+ cockpit_creds_unref (test->creds);
+}
+
+static void
+setup_for_socket (TestCase *test,
+ gconstpointer data)
+{
+ alarm (TIMEOUT);
+
+ setup_mock_bridge (test, data);
+ setup_mock_webserver (test, data);
+}
+
+static void
+teardown_for_socket (TestCase *test,
+ gconstpointer data)
+{
+ teardown_mock_bridge (test, data);
+ teardown_mock_webserver (test, data);
+
+ cockpit_assert_expected ();
+ alarm (0);
+}
+
+static gboolean
+on_error_not_reached (WebSocketConnection *ws,
+ GError *error,
+ gpointer user_data)
+{
+ g_assert (error != NULL);
+
+ /* At this point we know this will fail, but is informative */
+ g_assert_no_error (error);
+ return TRUE;
+}
+
+static gboolean
+on_error_copy (WebSocketConnection *ws,
+ GError *error,
+ gpointer user_data)
+{
+ GError **result = user_data;
+ g_assert (error != NULL);
+ g_assert (result != NULL);
+ g_assert (*result == NULL);
+ *result = g_error_copy (error);
+ return TRUE;
+}
+
+static gboolean
+on_timeout_fail (gpointer data)
+{
+ g_error ("timeout during test: %s", (gchar *)data);
+ return FALSE;
+}
+
+#define BUILD_INTS GINT_TO_POINTER(1)
+
+static GBytes *
+builder_to_bytes (JsonBuilder *builder)
+{
+ GBytes *bytes;
+ gchar *data;
+ gsize length;
+ JsonNode *node;
+
+ json_builder_end_object (builder);
+ node = json_builder_get_root (builder);
+ data = cockpit_json_write (node, &length);
+ data = g_realloc (data, length + 1);
+ memmove (data + 1, data, length);
+ memcpy (data, "\n", 1);
+ bytes = g_bytes_new_take (data, length + 1);
+ json_node_free (node);
+ return bytes;
+}
+
+static GBytes *
+build_control_va (const gchar *command,
+ const gchar *channel,
+ va_list va)
+{
+ GBytes *bytes;
+ JsonBuilder *builder;
+ const gchar *option;
+ gboolean strings = TRUE;
+
+ builder = json_builder_new ();
+ json_builder_begin_object (builder);
+ json_builder_set_member_name (builder, "command");
+ json_builder_add_string_value (builder, command);
+ if (channel)
+ {
+ json_builder_set_member_name (builder, "channel");
+ json_builder_add_string_value (builder, channel);
+ }
+
+ for (;;)
+ {
+ option = va_arg (va, const gchar *);
+ if (option == BUILD_INTS)
+ {
+ strings = FALSE;
+ option = va_arg (va, const gchar *);
+ }
+ if (!option)
+ break;
+ json_builder_set_member_name (builder, option);
+ if (strings)
+ json_builder_add_string_value (builder, va_arg (va, const gchar *));
+ else
+ json_builder_add_int_value (builder, va_arg (va, gint));
+ }
+
+ bytes = builder_to_bytes (builder);
+ g_object_unref (builder);
+
+ return bytes;
+}
+
+static void
+send_control_message (WebSocketConnection *ws,
+ const gchar *command,
+ const gchar *channel,
+ ...) G_GNUC_NULL_TERMINATED;
+
+static void
+send_control_message (WebSocketConnection *ws,
+ const gchar *command,
+ const gchar *channel,
+ ...)
+{
+ GBytes *payload;
+ va_list va;
+
+ va_start (va, channel);
+ payload = build_control_va (command, channel, va);
+ va_end (va);
+
+ web_socket_connection_send (ws, WEB_SOCKET_DATA_TEXT, NULL, payload);
+ g_bytes_unref (payload);
+}
+
+static void
+expect_control_message (GBytes *message,
+ const gchar *command,
+ const gchar *expected_channel,
+ ...) G_GNUC_NULL_TERMINATED;
+
+static void
+expect_control_message (GBytes *message,
+ const gchar *expected_command,
+ const gchar *expected_channel,
+ ...)
+{
+ gchar *outer_channel;
+ const gchar *message_command;
+ const gchar *message_channel;
+ JsonObject *options;
+ GBytes *payload;
+ const gchar *expect_option;
+ const gchar *expect_value;
+ const gchar *value;
+ va_list va;
+
+ payload = cockpit_transport_parse_frame (message, &outer_channel);
+ g_assert (payload != NULL);
+ g_assert_cmpstr (outer_channel, ==, NULL);
+ g_free (outer_channel);
+
+ g_assert (cockpit_transport_parse_command (payload, &message_command,
+ &message_channel, &options));
+ g_bytes_unref (payload);
+
+ g_assert_cmpstr (expected_command, ==, message_command);
+ g_assert_cmpstr (expected_channel, ==, expected_channel);
+
+ va_start (va, expected_channel);
+ for (;;) {
+ expect_option = va_arg (va, const gchar *);
+ if (!expect_option)
+ break;
+ expect_value = va_arg (va, const gchar *);
+ g_assert (expect_value != NULL);
+ value = NULL;
+ if (json_object_has_member (options, expect_option))
+ value = json_object_get_string_member (options, expect_option);
+ g_assert_cmpstr (value, ==, expect_value);
+ }
+ va_end (va);
+
+ json_object_unref (options);
+}
+
+static void
+start_web_service_and_create_client (TestCase *test,
+ const TestFixture *fixture,
+ WebSocketConnection **ws)
+{
+ const char *origin = fixture ? fixture->origin : NULL;
+ gboolean ready = FALSE;
+ gulong handler;
+
+ test->service = cockpit_web_service_new (test->creds, test->mock_bridge);
+
+ if (!origin)
+ origin = "http://127.0.0.1";
+
+ /* Manually created services won't be init'd yet, wait for that before sending data */
+ handler = g_signal_connect (test->mock_bridge, "control", G_CALLBACK (on_transport_control), &ready);
+
+ while (!ready)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_signal_handler_disconnect (test->mock_bridge, handler);
+
+ /* This is web_socket_client_new_for_stream() */
+ *ws = g_object_new (WEB_SOCKET_TYPE_CLIENT,
+ "url", "ws://127.0.0.1/unused",
+ "origin", origin,
+ "io-stream", test->connection,
+ NULL);
+
+ if (fixture && fixture->forward)
+ web_socket_client_include_header (WEB_SOCKET_CLIENT (*ws), "X-Forwarded-Proto", fixture->forward);
+
+ g_signal_connect (*ws, "error", G_CALLBACK (on_error_not_reached), NULL);
+}
+
+static void
+start_web_service_and_connect_client (TestCase *test,
+ const TestFixture *fixture,
+ WebSocketConnection **ws)
+{
+ GBytes *message;
+
+ start_web_service_and_create_client (test, fixture, ws);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (*ws) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert (web_socket_connection_get_ready_state (*ws) == WEB_SOCKET_STATE_OPEN);
+
+ /* Send the open control message that starts the bridge. */
+ send_control_message (*ws, "init", NULL, BUILD_INTS, "version", 1, NULL);
+ send_control_message (*ws, "open", "4", "payload", "echo", NULL);
+
+ /* This message should be echoed */
+ message = g_bytes_new ("4\ntest", 6);
+ web_socket_connection_send (*ws, WEB_SOCKET_DATA_TEXT, NULL, message);
+ g_bytes_unref (message);
+}
+
+static void
+close_client_and_stop_web_service (TestCase *test,
+ WebSocketConnection *ws)
+{
+ guint timeout;
+
+ if (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_OPEN)
+ {
+ web_socket_connection_close (ws, 0, NULL);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_CLOSED);
+ }
+
+ g_object_unref (ws);
+
+ /* Wait until service is done */
+ timeout = g_timeout_add_seconds (20, on_timeout_fail, "closing web service");
+ g_object_add_weak_pointer (G_OBJECT (test->service), (gpointer *)&test->service);
+ g_object_unref (test->service);
+ while (test->service != NULL)
+ g_main_context_iteration (NULL, TRUE);
+ g_source_remove (timeout);
+ cockpit_conf_cleanup ();
+}
+
+static void
+test_handshake_and_auth (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+
+ start_web_service_and_connect_client (test, data, &ws);
+ close_client_and_stop_web_service (test, ws);
+}
+
+static void
+on_message_get_bytes (WebSocketConnection *ws,
+ WebSocketDataType type,
+ GBytes *message,
+ gpointer user_data)
+{
+ GBytes **received = user_data;
+ g_assert_cmpint (type, ==, WEB_SOCKET_DATA_TEXT);
+ if (*received != NULL)
+ {
+ gsize length;
+ gconstpointer data = g_bytes_get_data (message, &length);
+ g_test_message ("received unexpected extra message: %.*s", (int)length, (gchar *)data);
+ g_assert_not_reached ();
+ }
+ g_assert (*received == NULL);
+ *received = g_bytes_ref (message);
+}
+
+static void
+on_message_get_non_control (WebSocketConnection *ws,
+ WebSocketDataType type,
+ GBytes *message,
+ gpointer user_data)
+{
+ GBytes **received = user_data;
+ g_assert_cmpint (type, ==, WEB_SOCKET_DATA_TEXT);
+ /* Control messages have this prefix: ie: a zero channel */
+ if (g_str_has_prefix (g_bytes_get_data (message, NULL), "\n"))
+ return;
+ g_assert (*received == NULL);
+ *received = g_bytes_ref (message);
+}
+
+static void
+test_handshake_and_echo (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+ GBytes *received = NULL;
+ GBytes *control = NULL;
+ CockpitCreds *creds;
+ GBytes *sent;
+ gulong handler;
+ const gchar *token;
+
+ /* Sends a "test" message in channel "4" */
+ start_web_service_and_connect_client (test, data, &ws);
+
+ sent = g_bytes_new_static ("4\ntest", 6);
+ handler = g_signal_connect (ws, "message", G_CALLBACK (on_message_get_bytes), &control);
+
+ WAIT_UNTIL (control != NULL);
+
+ creds = cockpit_web_service_get_creds (test->service);
+ g_assert (creds != NULL);
+
+ token = cockpit_creds_get_csrf_token (creds);
+ g_assert_cmpstr (token, ==, "my-csrf-token");
+
+ expect_control_message (control, "init", NULL, "csrf-token", token, NULL);
+ g_bytes_unref (control);
+
+ g_signal_handler_disconnect (ws, handler);
+ handler = g_signal_connect (ws, "message", G_CALLBACK (on_message_get_non_control), &received);
+
+ WAIT_UNTIL (received != NULL);
+
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ g_signal_handler_disconnect (ws, handler);
+
+ close_client_and_stop_web_service (test, ws);
+}
+
+static void
+test_echo_large (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+ GBytes *received = NULL;
+ gchar *contents;
+ GBytes *sent;
+ gulong handler;
+
+ start_web_service_and_create_client (test, data, &ws);
+ while (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_CONNECTING)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_OPEN);
+
+ /* Send the open control message that starts the bridge. */
+ send_control_message (ws, "init", NULL, BUILD_INTS, "version", 1, NULL);
+ send_control_message (ws, "open", "4", "payload", "test-text", NULL);
+ handler = g_signal_connect (ws, "message", G_CALLBACK (on_message_get_non_control), &received);
+
+ /* Medium length */
+ contents = g_strnfill (1020, '!');
+ contents[0] = '4'; /* channel */
+ contents[1] = '\n';
+ sent = g_bytes_new_take (contents, 1020);
+ web_socket_connection_send (ws, WEB_SOCKET_DATA_TEXT, NULL, sent);
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ /* Extra large */
+ contents = g_strnfill (100 * 1000, '?');
+ contents[0] = '4'; /* channel */
+ contents[1] = '\n';
+ sent = g_bytes_new_take (contents, 100 * 1000);
+ web_socket_connection_send (ws, WEB_SOCKET_DATA_TEXT, NULL, sent);
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ g_signal_handler_disconnect (ws, handler);
+ close_client_and_stop_web_service (test, ws);
+}
+
+static void
+test_close_error (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+ GBytes *received = NULL;
+
+ start_web_service_and_connect_client (test, data, &ws);
+ g_signal_connect (ws, "message", G_CALLBACK (on_message_get_bytes), &received);
+
+ WAIT_UNTIL (received != NULL);
+ expect_control_message (received, "init", NULL, NULL);
+ g_bytes_unref (received);
+ received = NULL;
+
+ /* Silly test echos the "open" message */
+ WAIT_UNTIL (received != NULL);
+ expect_control_message (received, "open", "4", NULL);
+ g_bytes_unref (received);
+ received = NULL;
+
+ WAIT_UNTIL (received != NULL);
+ g_bytes_unref (received);
+ received = NULL;
+
+ /* Trigger a failure message */
+ g_assert (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_OPEN);
+ kill (test->mock_bridge_pid, SIGTERM);
+
+ /* We should now get a close command */
+ WAIT_UNTIL (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_CLOSED);
+
+ close_client_and_stop_web_service (test, ws);
+}
+
+static void
+test_no_init (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+ GBytes *received = NULL;
+
+ start_web_service_and_create_client (test, data, &ws);
+ g_signal_connect (ws, "message", G_CALLBACK (on_message_get_bytes), &received);
+
+ while (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_CONNECTING)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_OPEN);
+
+ cockpit_expect_message ("*socket did not send*init*");
+ cockpit_expect_log ("WebSocket", G_LOG_LEVEL_MESSAGE, "connection unexpectedly closed*");
+
+ /* Sending an open message before init, should cause problems */
+ send_control_message (ws, "ping", NULL, NULL);
+
+ /* The init from the other end */
+ while (received == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ expect_control_message (received, "init", NULL, NULL);
+ g_bytes_unref (received);
+ received = NULL;
+
+ /* We should now get a failure */
+ while (received == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ expect_control_message (received, "close", NULL, "problem", "protocol-error", NULL);
+ g_bytes_unref (received);
+ received = NULL;
+
+ close_client_and_stop_web_service (test, ws);
+}
+
+static void
+test_wrong_init_version (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+ GBytes *received = NULL;
+
+ start_web_service_and_create_client (test, data, &ws);
+ g_signal_connect (ws, "message", G_CALLBACK (on_message_get_bytes), &received);
+
+ while (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_CONNECTING)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_OPEN);
+
+ cockpit_expect_message ("*socket used unsupported*");
+ cockpit_expect_log ("WebSocket", G_LOG_LEVEL_MESSAGE, "connection unexpectedly closed*");
+
+ /* The init from the other end */
+ while (received == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ expect_control_message (received, "init", NULL, NULL);
+ g_bytes_unref (received);
+ received = NULL;
+
+ send_control_message (ws, "init", NULL, BUILD_INTS, "version", 888, NULL);
+
+ /* We should now get a failure */
+ while (received == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ expect_control_message (received, "close", NULL, "problem", "not-supported", NULL);
+ g_bytes_unref (received);
+ received = NULL;
+
+ close_client_and_stop_web_service (test, ws);
+}
+
+static void
+test_bad_init_version (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+ GBytes *received = NULL;
+
+ start_web_service_and_create_client (test, data, &ws);
+ g_signal_connect (ws, "message", G_CALLBACK (on_message_get_bytes), &received);
+
+ while (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_CONNECTING)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_OPEN);
+
+ cockpit_expect_warning ("*invalid version field*");
+ cockpit_expect_log ("WebSocket", G_LOG_LEVEL_MESSAGE, "connection unexpectedly closed*");
+
+ /* The init from the other end */
+ while (received == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ expect_control_message (received, "init", NULL, NULL);
+ g_bytes_unref (received);
+ received = NULL;
+
+ send_control_message (ws, "init", NULL, "version", "blah", NULL);
+
+ /* We should now get a failure */
+ while (received == NULL)
+ g_main_context_iteration (NULL, TRUE);
+ expect_control_message (received, "close", NULL, "problem", "protocol-error", NULL);
+ g_bytes_unref (received);
+ received = NULL;
+
+ close_client_and_stop_web_service (test, ws);
+}
+
+static void
+test_socket_null_creds (void)
+{
+ CockpitWebService *service;
+ CockpitTransport *session;
+ int pair[2];
+
+ /*
+ * These are tests double checking that we *never*
+ * open up a real CockpitWebService for NULL creds.
+ *
+ * Other code paths do the real checks, but these are
+ * the last resorts.
+ */
+
+ cockpit_expect_critical ("*assertion*failed*");
+
+ service = cockpit_web_service_new (NULL, NULL);
+ g_assert (service == NULL);
+
+ cockpit_expect_critical ("*assertion*failed*");
+
+ g_assert (pipe(pair) >= 0);
+ session = cockpit_pipe_transport_new_fds ("dummy", pair[0], pair[1]);
+ service = cockpit_web_service_new (NULL, session);
+ g_assert (service == NULL);
+ g_object_unref (session);
+}
+
+static const TestFixture fixture_bad_origin_rfc6455 = {
+ .origin = "http://another-place.com",
+ .config = NULL
+};
+
+static const TestFixture fixture_allowed_origin_rfc6455 = {
+ .origin = "https://another-place.com",
+ .config = SRCDIR "/src/ws/mock-config/cockpit/cockpit.conf"
+};
+
+static const TestFixture fixture_allowed_origin_proto_header = {
+ .origin = "https://127.0.0.1",
+ .forward = "https",
+ .config = SRCDIR "/src/ws/mock-config/cockpit/cockpit-alt.conf"
+};
+
+static const TestFixture fixture_allowed_origin_tls_proxy = {
+ .origin = "https://127.0.0.1",
+ .for_tls_proxy = TRUE,
+};
+
+static const TestFixture fixture_bad_origin_proto_no_header = {
+ .origin = "https://127.0.0.1",
+ .config = SRCDIR "/src/ws/mock-config/cockpit/cockpit-alt.conf"
+};
+
+static const TestFixture fixture_bad_origin_proto_no_config = {
+ .origin = "https://127.0.0.1",
+ .forward = "https",
+ .config = NULL
+};
+
+static const TestFixture fixture_bad_origin_tls_proxy = {
+ .origin = "http://127.0.0.1",
+ .for_tls_proxy = TRUE,
+};
+
+static void
+test_bad_origin (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+ GError *error = NULL;
+
+ start_web_service_and_create_client (test, data, &ws);
+ g_signal_handlers_disconnect_by_func (ws, on_error_not_reached, NULL);
+ g_signal_connect (ws, "error", G_CALLBACK (on_error_copy), &error);
+ cockpit_expect_log ("WebSocket", G_LOG_LEVEL_MESSAGE, "*received request from bad Origin*");
+ cockpit_expect_log ("WebSocket", G_LOG_LEVEL_MESSAGE, "*invalid handshake*");
+ cockpit_expect_log ("WebSocket", G_LOG_LEVEL_MESSAGE, "*unexpected status: 403*");
+
+ while (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_CONNECTING ||
+ web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_CLOSING)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert_cmpint (web_socket_connection_get_ready_state (ws), ==, WEB_SOCKET_STATE_CLOSED);
+ g_assert_error (error, WEB_SOCKET_ERROR, WEB_SOCKET_CLOSE_PROTOCOL);
+
+ close_client_and_stop_web_service (test, ws);
+ g_clear_error (&error);
+}
+
+static const TestFixture fixture_kill_group = {
+ .bridge = BUILDDIR "/cockpit-bridge"
+};
+
+static void
+test_kill_group (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+ GBytes *received = NULL;
+ GHashTable *seen;
+ gchar *ochannel;
+ const gchar *channel;
+ const gchar *command;
+ JsonObject *options;
+ GBytes *sent;
+ GBytes *payload;
+ gulong handler;
+
+ /* Sends a "test" message in channel "4" */
+ start_web_service_and_connect_client (test, data, &ws);
+
+ sent = g_bytes_new_static ("4\ntest", 6);
+ handler = g_signal_connect (ws, "message", G_CALLBACK (on_message_get_non_control), &received);
+
+ /* Drain the initial message */
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (sent, received));
+ g_bytes_unref (received);
+ received = NULL;
+
+ g_signal_handler_disconnect (ws, handler);
+ handler = g_signal_connect (ws, "message", G_CALLBACK (on_message_get_bytes), &received);
+
+ seen = g_hash_table_new (g_str_hash, g_str_equal);
+ g_hash_table_add (seen, "a");
+ g_hash_table_add (seen, "b");
+ g_hash_table_add (seen, "c");
+
+ send_control_message (ws, "open", "a", "payload", "echo", "group", "test", NULL);
+ send_control_message (ws, "open", "b", "payload", "echo", "group", "test", NULL);
+ send_control_message (ws, "open", "c", "payload", "echo", "group", "test", NULL);
+
+ /* Kill all the above channels */
+ send_control_message (ws, "kill", NULL, "group", "test", NULL);
+
+ /* All the close messages */
+ while (g_hash_table_size (seen) > 0)
+ {
+ WAIT_UNTIL (received != NULL);
+
+ payload = cockpit_transport_parse_frame (received, &ochannel);
+ g_bytes_unref (received);
+ received = NULL;
+
+ g_assert (payload != NULL);
+ g_assert_cmpstr (ochannel, ==, NULL);
+ g_free (ochannel);
+
+ g_assert (cockpit_transport_parse_command (payload, &command, &channel, &options));
+ g_bytes_unref (payload);
+
+ if (!g_str_equal (command, "open") && !g_str_equal (command, "ready"))
+ {
+ g_assert_cmpstr (command, ==, "close");
+ g_assert_cmpstr (json_object_get_string_member (options, "problem"), ==, "terminated");
+ g_assert (g_hash_table_remove (seen, channel));
+ }
+ json_object_unref (options);
+ }
+
+ g_hash_table_destroy (seen);
+
+ g_signal_handler_disconnect (ws, handler);
+ handler = g_signal_connect (ws, "message", G_CALLBACK (on_message_get_non_control), &received);
+
+ /* Now verify that the original channel is still open */
+ web_socket_connection_send (ws, WEB_SOCKET_DATA_TEXT, NULL, sent);
+
+ WAIT_UNTIL (received != NULL);
+ g_assert (g_bytes_equal (received, sent));
+ g_bytes_unref (sent);
+ g_bytes_unref (received);
+ received = NULL;
+
+ g_signal_handler_disconnect (ws, handler);
+
+ close_client_and_stop_web_service (test, ws);
+}
+
+static void
+test_kill_host (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+ GBytes *received = NULL;
+ GHashTable *seen;
+ gchar *ochannel;
+ const gchar *channel;
+ const gchar *command;
+ JsonObject *options;
+ GBytes *payload;
+ gulong handler;
+
+ /* Sends a "test" message in channel "4" */
+ start_web_service_and_connect_client (test, data, &ws);
+
+ handler = g_signal_connect (ws, "message", G_CALLBACK (on_message_get_non_control), &received);
+
+ /* Drain the initial message */
+ WAIT_UNTIL (received != NULL);
+ g_bytes_unref (received);
+ received = NULL;
+
+ g_signal_handler_disconnect (ws, handler);
+ handler = g_signal_connect (ws, "message", G_CALLBACK (on_message_get_bytes), &received);
+
+ seen = g_hash_table_new (g_str_hash, g_str_equal);
+ g_hash_table_add (seen, "a");
+ g_hash_table_add (seen, "b");
+ g_hash_table_add (seen, "c");
+ g_hash_table_add (seen, "4");
+
+ send_control_message (ws, "open", "a", "payload", "echo", "group", "test", NULL);
+ send_control_message (ws, "open", "b", "payload", "echo", "group", "test", NULL);
+ send_control_message (ws, "open", "c", "payload", "echo", "group", "test", NULL);
+
+ /* Kill all the above channels */
+ send_control_message (ws, "kill", NULL, "host", "localhost", NULL);
+
+ /* All the close messages */
+ while (g_hash_table_size (seen) > 0)
+ {
+ WAIT_UNTIL (received != NULL);
+
+ payload = cockpit_transport_parse_frame (received, &ochannel);
+ g_bytes_unref (received);
+ received = NULL;
+
+ g_assert (payload != NULL);
+ g_assert_cmpstr (ochannel, ==, NULL);
+ g_free (ochannel);
+
+ g_assert (cockpit_transport_parse_command (payload, &command, &channel, &options));
+ g_bytes_unref (payload);
+
+ if (!g_str_equal (command, "open") && !g_str_equal (command, "ready"))
+ {
+ g_assert_cmpstr (command, ==, "close");
+ g_assert_cmpstr (json_object_get_string_member (options, "problem"), ==, "terminated");
+ g_assert (g_hash_table_remove (seen, channel));
+ }
+ json_object_unref (options);
+ }
+
+ g_hash_table_destroy (seen);
+
+ g_signal_handler_disconnect (ws, handler);
+
+ close_client_and_stop_web_service (test, ws);
+}
+
+static void
+on_idling_set_flag (CockpitWebService *service,
+ gpointer data)
+{
+ gboolean *flag = data;
+ g_assert (*flag == FALSE);
+ *flag = TRUE;
+}
+
+static void
+test_idling (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *client;
+ gboolean flag = FALSE;
+ CockpitTransport *transport;
+ CockpitPipe *pipe;
+
+ const gchar *argv[] = {
+ BUILDDIR "/cockpit-bridge",
+ NULL
+ };
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ transport = cockpit_pipe_transport_new (pipe);
+ test->service = cockpit_web_service_new (test->creds, transport);
+ g_object_unref (transport);
+ g_object_unref (pipe);
+
+ g_signal_connect (test->service, "idling", G_CALLBACK (on_idling_set_flag), &flag);
+ g_assert (cockpit_web_service_get_idling (test->service));
+
+ /* This is web_socket_client_new_for_stream() */
+ client = g_object_new (WEB_SOCKET_TYPE_CLIENT,
+ "url", "ws://127.0.0.1/unused",
+ "origin", "http://127.0.0.1",
+ "io-stream", test->connection,
+ NULL);
+
+ g_assert (cockpit_web_service_get_idling (test->service));
+
+ while (web_socket_connection_get_ready_state (client) == WEB_SOCKET_STATE_CONNECTING)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (web_socket_connection_get_ready_state (client) == WEB_SOCKET_STATE_OPEN);
+ g_assert (!cockpit_web_service_get_idling (test->service));
+
+ web_socket_connection_close (client, WEB_SOCKET_CLOSE_NORMAL, "aoeuaoeuaoeu");
+ while (web_socket_connection_get_ready_state (client) != WEB_SOCKET_STATE_CLOSED)
+ g_main_context_iteration (NULL, TRUE);
+
+ /* Now the web service should go idle and fire idling signal */
+ while (!flag)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_assert (cockpit_web_service_get_idling (test->service));
+
+ g_clear_object (&test->service);
+ g_object_unref (client);
+}
+
+static void
+test_dispose (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *client;
+ CockpitTransport *transport;
+ CockpitPipe *pipe;
+
+ const gchar *argv[] = {
+ BUILDDIR "/cockpit-bridge",
+ NULL
+ };
+
+ pipe = cockpit_pipe_spawn (argv, NULL, NULL, COCKPIT_PIPE_FLAGS_NONE);
+ transport = cockpit_pipe_transport_new (pipe);
+ test->service = cockpit_web_service_new (test->creds, transport);
+ g_object_unref (transport);
+ g_object_unref (pipe);
+
+ g_signal_connect_swapped (test->web_server, "handle-stream",
+ G_CALLBACK (cockpit_web_service_socket), test->service);
+
+ /* This is web_socket_client_new_for_stream() */
+ client = g_object_new (WEB_SOCKET_TYPE_CLIENT,
+ "url", "ws://127.0.0.1/unused",
+ "origin", "http://127.0.0.1",
+ "io-stream", test->connection,
+ NULL);
+
+
+ while (web_socket_connection_get_ready_state (client) == WEB_SOCKET_STATE_CONNECTING)
+ g_main_context_iteration (NULL, TRUE);
+ g_assert (web_socket_connection_get_ready_state (client) == WEB_SOCKET_STATE_OPEN);
+
+ /* Dispose the WebSocket ... this is what happens on forceful logout */
+ g_object_run_dispose (G_OBJECT (test->service));
+
+ while (web_socket_connection_get_ready_state (client) != WEB_SOCKET_STATE_CLOSED)
+ g_main_context_iteration (NULL, TRUE);
+
+ g_clear_object (&test->service);
+ g_object_unref (client);
+}
+
+static void
+test_logout (TestCase *test,
+ gconstpointer data)
+{
+ WebSocketConnection *ws;
+ GBytes *message = NULL;
+
+ start_web_service_and_create_client (test, data, &ws);
+ WAIT_UNTIL (web_socket_connection_get_ready_state (ws) != WEB_SOCKET_STATE_CONNECTING);
+ g_assert (web_socket_connection_get_ready_state (ws) == WEB_SOCKET_STATE_OPEN);
+
+ /* Send the logout control message */
+ send_control_message (ws, "init", NULL, BUILD_INTS, "version", 1, NULL);
+
+ data = "\n{ \"command\": \"logout\", \"disconnect\": true }";
+ message = g_bytes_new_static (data, strlen (data));
+ web_socket_connection_send (ws, WEB_SOCKET_DATA_TEXT, NULL, message);
+ g_bytes_unref (message);
+
+ while (web_socket_connection_get_ready_state (ws) != WEB_SOCKET_STATE_CLOSED)
+ g_main_context_iteration (NULL, TRUE);
+
+ close_client_and_stop_web_service (test, ws);
+}
+
+static void
+test_parse_external (void)
+{
+ const gchar *content_disposition;
+ const gchar *content_type;
+ const gchar *content_encoding;
+ const gchar **protocols;
+ JsonObject *object;
+ JsonObject *external;
+ JsonArray *array;
+ gboolean ret;
+
+ object = json_object_new ();
+
+ ret = cockpit_web_service_parse_external (object, NULL, NULL, NULL, NULL);
+ g_assert (ret == TRUE);
+
+ ret = cockpit_web_service_parse_external (object, &content_type, &content_encoding, &content_disposition, &protocols);
+ g_assert (ret == TRUE);
+ g_assert (content_type == NULL);
+ g_assert (content_encoding == NULL);
+ g_assert (content_disposition == NULL);
+ g_assert (protocols == NULL);
+
+ external = json_object_new ();
+ json_object_set_object_member (object, "external", external);
+
+ ret = cockpit_web_service_parse_external (object, &content_type, &content_encoding, &content_disposition, &protocols);
+ g_assert (ret == TRUE);
+ g_assert (content_type == NULL);
+ g_assert (content_encoding == NULL);
+ g_assert (content_disposition == NULL);
+ g_assert (protocols == NULL);
+
+ array = json_array_new ();
+ json_array_add_string_element (array, "one");
+ json_array_add_string_element (array, "two");
+ json_array_add_string_element (array, "three");
+ json_object_set_array_member (external, "protocols", array);
+
+ json_object_set_string_member (external, "content-type", "text/plain");
+ json_object_set_string_member (external, "content-encoding", "gzip");
+ json_object_set_string_member (external, "content-disposition", "filename; test");
+
+ ret = cockpit_web_service_parse_external (object, &content_type, &content_encoding, &content_disposition, &protocols);
+ g_assert (ret == TRUE);
+ g_assert_cmpstr (content_type, ==, "text/plain");
+ g_assert_cmpstr (content_encoding, ==, "gzip");
+ g_assert_cmpstr (content_disposition, ==, "filename; test");
+ g_assert (protocols != NULL);
+ g_assert_cmpstr (protocols[0], ==, "one");
+ g_assert_cmpstr (protocols[1], ==, "two");
+ g_assert_cmpstr (protocols[2], ==, "three");
+ g_assert_cmpstr (protocols[3], ==, NULL);
+ g_free (protocols);
+
+ json_object_unref (object);
+}
+
+static void
+test_host_checksums (void)
+{
+ CockpitTransport *transport;
+ CockpitWebService *service;
+ CockpitCreds *creds;
+ int fds[2];
+
+ if (pipe(fds) < 0)
+ g_assert_not_reached();
+ transport = cockpit_pipe_transport_new_fds ("unused", fds[0], fds[1]);
+ creds = cockpit_creds_new ("cockpit", NULL);
+ service = cockpit_web_service_new (creds, transport);
+ cockpit_web_service_set_host_checksum(service, "localhost", "checksum1");
+ cockpit_web_service_set_host_checksum(service, "host1", "checksum1");
+ cockpit_web_service_set_host_checksum(service, "host2", "checksum2");
+
+ g_assert_cmpstr (cockpit_web_service_get_host (service, "checksum1"), ==, "localhost");
+ g_assert_cmpstr (cockpit_web_service_get_host (service, "checksum2"), ==, "host2");
+ g_assert_cmpstr (cockpit_web_service_get_host (service, "bad"), ==, NULL);
+
+ g_assert_cmpstr (cockpit_web_service_get_checksum (service, "host1"), ==, "checksum1");
+ g_assert_cmpstr (cockpit_web_service_get_checksum (service, "host2"), ==, "checksum2");
+ g_assert_cmpstr (cockpit_web_service_get_checksum (service, "localhost"), ==, "checksum1");
+ g_assert_cmpstr (cockpit_web_service_get_checksum (service, "bad"), ==, NULL);
+
+ cockpit_web_service_set_host_checksum(service, "host2", "checksum3");
+ g_assert_cmpstr (cockpit_web_service_get_checksum (service, "host2"), ==, "checksum3");
+ g_assert_cmpstr (cockpit_web_service_get_host (service, "checksum3"), ==, "host2");
+ g_assert_cmpstr (cockpit_web_service_get_host (service, "checksum2"), ==, NULL);
+
+ g_object_unref (service);
+ g_object_unref (transport);
+ cockpit_creds_unref (creds);
+}
+
+typedef struct {
+ const gchar *name;
+ const gchar *input;
+ const gchar *message;
+} ParseExternalFailure;
+
+static ParseExternalFailure external_failure_fixtures[] = {
+ { "bad-channel", "{ \"channel\": \"blah\" }", "don't specify \"channel\" on external channel" },
+ { "bad-command", "{ \"command\": \"test\" }", "don't specify \"command\" on external channel" },
+ { "bad-external", "{ \"external\": \"test\" }", "invalid \"external\" option" },
+ { "bad-disposition", "{ \"external\": { \"content-disposition\": 5 } }", "invalid*content-disposition*" },
+ { "invalid-disposition", "{ \"external\": { \"content-disposition\": \"xx\nx\" } }", "invalid*content-disposition*" },
+ { "bad-type", "{ \"external\": { \"content-type\": 5 } }", "invalid*content-type*" },
+ { "invalid-type", "{ \"external\": { \"content-type\": \"xx\nx\" } }", "invalid*content-type*" },
+ { "bad-protocols", "{ \"external\": { \"protocols\": \"xx\nx\" } }", "invalid*protocols*" },
+};
+
+static void
+test_parse_external_failure (gconstpointer data)
+{
+ const ParseExternalFailure *fixture = data;
+ GError *error = NULL;
+ JsonObject *object;
+ gboolean ret;
+
+ object = cockpit_json_parse_object (fixture->input, -1, &error);
+ g_assert_no_error (error);
+
+ cockpit_expect_message (fixture->message);
+
+ ret = cockpit_web_service_parse_external (object, NULL, NULL, NULL, NULL);
+ g_assert (ret == FALSE);
+
+ json_object_unref (object);
+
+ cockpit_assert_expected ();
+}
+
+static gboolean
+on_hack_raise_sigchld (gpointer user_data)
+{
+ raise (SIGCHLD);
+ return TRUE;
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gchar *name;
+ gint i;
+
+ cockpit_test_init (&argc, &argv);
+
+ /*
+ * HACK: Work around races in glib SIGCHLD handling.
+ *
+ * https://bugzilla.gnome.org/show_bug.cgi?id=731771
+ * https://bugzilla.gnome.org/show_bug.cgi?id=711090
+ */
+ g_timeout_add_seconds (1, on_hack_raise_sigchld, NULL);
+
+ /* We don't want to test the ping functionality in these tests */
+ cockpit_ws_ping_interval = G_MAXUINT;
+
+ static const TestFixture fixture_rfc6455 = {
+ .config = NULL
+ };
+
+ g_test_add ("/web-service/handshake-and-auth/rfc6455", TestCase,
+ &fixture_rfc6455, setup_for_socket,
+ test_handshake_and_auth, teardown_for_socket);
+
+ g_test_add ("/web-service/echo-message/rfc6455", TestCase,
+ &fixture_rfc6455, setup_for_socket,
+ test_handshake_and_echo, teardown_for_socket);
+ g_test_add ("/web-service/echo-message/large", TestCase,
+ &fixture_rfc6455, setup_for_socket,
+ test_echo_large, teardown_for_socket);
+
+ g_test_add_func ("/web-service/null-creds", test_socket_null_creds);
+ g_test_add ("/web-service/no-init", TestCase, NULL,
+ setup_for_socket, test_no_init, teardown_for_socket);
+ g_test_add ("/web-service/wrong-init-version", TestCase, NULL,
+ setup_for_socket, test_wrong_init_version, teardown_for_socket);
+ g_test_add ("/web-service/bad-init-version", TestCase, NULL,
+ setup_for_socket, test_bad_init_version, teardown_for_socket);
+
+ g_test_add ("/web-service/bad-origin/rfc6455", TestCase,
+ &fixture_bad_origin_rfc6455, setup_for_socket,
+ test_bad_origin, teardown_for_socket);
+ g_test_add ("/web-service/bad-origin/withallowed", TestCase,
+ &fixture_bad_origin_rfc6455, setup_for_socket,
+ test_bad_origin, teardown_for_socket);
+ g_test_add ("/web-service/allowed-origin/rfc6455", TestCase,
+ &fixture_allowed_origin_rfc6455, setup_for_socket,
+ test_handshake_and_auth, teardown_for_socket);
+
+ g_test_add ("/web-service/bad-origin/protocol-no-config", TestCase,
+ &fixture_bad_origin_proto_no_config, setup_for_socket,
+ test_bad_origin, teardown_for_socket);
+ g_test_add ("/web-service/bad-origin/protocol-no-header", TestCase,
+ &fixture_bad_origin_proto_no_header, setup_for_socket,
+ test_bad_origin, teardown_for_socket);
+ g_test_add ("/web-service/allowed-origin/protocol-header", TestCase,
+ &fixture_allowed_origin_proto_header, setup_for_socket,
+ test_handshake_and_auth, teardown_for_socket);
+ g_test_add ("/web-service/allowed-origin/tls-proxy", TestCase,
+ &fixture_allowed_origin_tls_proxy, setup_for_socket,
+ test_handshake_and_auth, teardown_for_socket);
+ g_test_add ("/web-service/bad-origin/tls-proxy", TestCase,
+ &fixture_bad_origin_tls_proxy, setup_for_socket,
+ test_bad_origin, teardown_for_socket);
+
+ g_test_add ("/web-service/close-error", TestCase,
+ NULL, setup_for_socket,
+ test_close_error, teardown_for_socket);
+
+ g_test_add ("/web-service/kill-group", TestCase, &fixture_kill_group,
+ setup_for_socket, test_kill_group, teardown_for_socket);
+ g_test_add ("/web-service/kill-host", TestCase, &fixture_kill_group,
+ setup_for_socket, test_kill_host, teardown_for_socket);
+
+ g_test_add ("/web-service/idling-signal", TestCase, NULL,
+ setup_for_socket, test_idling, teardown_for_socket);
+ g_test_add ("/web-service/force-dispose", TestCase, NULL,
+ setup_for_socket, test_dispose, teardown_for_socket);
+ g_test_add ("/web-service/logout", TestCase, NULL,
+ setup_for_socket, test_logout, teardown_for_socket);
+
+ g_test_add_func ("/web-service/parse-external/success", test_parse_external);
+ g_test_add_func ("/web-service/host-checksums", test_host_checksums);
+ for (i = 0; i < G_N_ELEMENTS (external_failure_fixtures); i++)
+ {
+ name = g_strdup_printf ("/web-service/parse-external/%s", external_failure_fixtures[i].name);
+ g_test_add_data_func (name, external_failure_fixtures + i, test_parse_external_failure);
+ g_free (name);
+ }
+
+ return g_test_run ();
+}
diff --git a/test/ARCHITECTURE.md b/test/ARCHITECTURE.md
new file mode 100644
index 0000000..0d20066
--- /dev/null
+++ b/test/ARCHITECTURE.md
@@ -0,0 +1,256 @@
+# Architecture
+
+This document describes the architecture of Cockpit's browser integration
+tests. The tests should automate how a normal user interacts with Cockpit.
+This requires a test machine which can add multiple disks or interfaces,
+reboot, interact with multiple machines on the same network and run potentially
+"destructive" test scenarios (e.g. installing/updating packages, formatting
+disks).
+
+For these reasons, Cockpit tests run inside a virtual machine (VM). The virtual
+machine uses Cockpit specific virtual machine images maintained and created in
+the [bots](https://github.com/cockpit-project/bots) repository. The images are
+usually based on a distribution's cloud image customized with:
+
+* A well-known password and SSH key for the admin and root users
+* Packages required to test Cockpit
+* A build chroot with Cockpit's build dependencies to build the to be tested
+ Cockpit source inside the virtual machine offline. This allows a developer on
+ Fedora to easily prepare a Debian test image without having to install Debian
+ build tools.
+* Disabling system services which interfere with testing
+
+To automate user actions on the web page, Cockpit is tested in a browser
+controlled using the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
+(CDP) which is supported by Firefox and Chromium based browsers.
+
+The Python test framework in [test/common](./common) is responsible for setting up the
+test environment, running tests and reporting of the test output.
+
+Diagram of the interaction of Browser/Machine/CDP/Test Framework.
+```mermaid
+graph TD;
+ id[Test Framework] <-->|CDP| Browser;
+ Browser <-->|HTTPS/WebSocket| Cockpit;
+ id1[Virtual Machine] <-->|HTTP/DBus/OS APIs| Cockpit;
+ id[Test Framework] <-->|SSH| id1[Virtual Machine];
+```
+
+## Integration Tests
+
+Cockpit's tests can be run via three different entry points:
+
+* `test/verify/check-$page` - run a single or multiple test(s)
+* `test/common/run-tests` - run tests through our test scheduler (retries, tracks naughties, parallelism)
+* `test/run` or `.cockpit-ci/run` - CI entry point
+
+We will start with how a single integration test is run and then explore the test
+scheduler and CI setup.
+
+### Test runtime
+
+The base of a Cockpit integration test looks as following:
+
+```python
+class TestSystem(testlib.MachineCase):
+ def testBasic(self):
+ self.login_and_go("/system")
+ self.machine.execute("echo 'test' > /etc/motd")
+ self.browser.wait_text("#motd", 'test')
+
+if __name__ == '__main__':
+ testlib.test_main()
+```
+
+In Cockpit there are two types of tests, "destructive" and "non-destructive"
+tests. Destructive tests do something to the virtual machine which makes it
+unable to run another test afterwards or requires another virtual machine for
+testing. The test above is a "destructive" test which is the default, a non
+"destructive" test makes sure any destructive action is restored after the test
+has run as can be seen below. "Non-destructive" tests were introduced to be able
+to run them on a single running machine, such as the Testing Farm or a custom VM.
+They also speed up testing, as booting a machine for every tests incurs a
+significant penalty of ~ 10-30 seconds per test.
+
+```python
+@testlib.nondestructive
+class TestSystem(testlib.MachineCase):
+ def testBasic(self):
+ self.login_and_go("/system")
+ self.write_file("/etc/motd", "test")
+ self.browser.wait_text("#motd", 'test')
+
+if __name__ == '__main__':
+ testlib.test_main()
+```
+
+The test above would be invoked via `./test/verify/check-apps TestSystem.testBasic`
+and would execute as can be seen in the diagram below:
+```mermaid
+sequenceDiagram
+ participant test
+ participant machine
+ participant browser
+
+ test->>test: test_main()
+ test->>test: setUp()
+ test->>machine: start()
+ test->>machine: wait_boot()
+ test->>browser: __init__()
+ test->>test: setup non-destructive setup
+ test->>test: run test
+ test->>browser: start()
+ test->>test: tearDown()
+ test->>browser: kill()
+ test->>machine: kill()
+```
+
+A test starts by calling `test_main`, this provides common command line
+arguments for debugging and to optionally run a test on a different
+machine/browser. These arguments are available in the `MachineCase` class as
+`opts`. `test_main` also takes care of instantiating a `TapRunner` which runs
+all the specified tests sequentially.
+
+Once a test is started it runs `MachineCase.setUp` which has the responsibility
+to start a virtual machine(s) depending on if it is a "non-destructive" or
+"destructive" test. If we run a "non-destructive" test a global machine is
+created, and re-used for other "non-destructive" tests which might run. For
+"destructive" tests a machine is created on-demand, possible multiple machines
+depending on the test class `provision` variable.
+
+ For "non-destructive" tests cleanup handlers are installed to restore files in
+ `/etc`, cleans up home directories etc.
+
+ Lastly a `Browser` class is instantiated, this does not start the Browser
+ directly but builds the required command for the `TEST_BROWSER` to start
+ either Chromium or Firefox. When a test calls any method on the browser object
+ the browser will be started, so tests which require no browser don't start a
+ browser needlessly.
+
+The `CDP` class is responsible for spawning the browser, then spawning a CDP
+driver. This uses the `chrome-remote-interface` npm module to send commands to
+the spawned drivers via standard in (stdin).
+
+On `tearDown` the test status is inspected, if it failed test logs are
+collected and if the user has passed `--sit` the test pauses execution until
+the user presses enter so that the machine/browser state can be inspected. The
+test browser is killed after the `tearDown` function completed.
+
+Virtual machines are killed by the `TapRunner` once all tests have finished or
+in `setUp` if it's a "destructive" test as "non-destructive" tests re-use the
+existing global machine.
+
+### Test runner
+
+Cockpit uses a custom test runner to run the tests, spread the load over jobs
+and special handling of test failures. The test runner is implemented in Python
+in [test/common/run-tests](./common/run-tests) and expects a list of tests to
+be provided.
+
+The provided tests are collected and split up in "destructive" and
+"non-destructive" tests, every test is given a "cost" depending on the amount
+of virtual machines required to run the test for scheduling priority. A default
+timeout is added to every test so hanging tests get killed over time, tests
+which take longer can set a custom timeout using a test decorator.
+
+To make sure our tests aren't flaky we retry tests three times when they are
+affected. An affected test is for example `test/verify/check-apps` if
+`pkg/apps` is changed, or if the changeset changes less then four tests they
+will be marked as affected as well. The changed tests are limited so big
+refactors won't retry a lot of tests.
+
+Having collected the "destructive", "non-destructive" and affected tests a
+scheduling loop is started, if a machine was provided it is used for the
+"non-destructive" tests, "destructive" tests will always spawn a new machine. If no
+machine is provided a pool of global machines is created based on the provided
+`--jobs` and "non-destructive" tests. The test runner will first try to assign all
+"non-destructive" tests on the available global machines and start the tests.
+
+The scheduling loop periodically inspects all running tests and polls if the
+test has ended. Depending on the exit code of the test process the test runner
+makes a decision on what to do next as described in the diagram below.
+```mermaid
+graph TD;
+ finished[Test finished]
+ succeeded{Test succeeded?}
+ affected{"Test affected?"}
+ affecteddone["Retry three times"]
+ skipped{Test skipped?}
+ todosuccess{Test todo?}
+ todonosuccess{Test todo?}
+ todosucceeded["Unexpected success
+ show as failure"]
+ todofail[Expected failure]
+ pixel_journal_failure{"Pixel test or
+ unexpected journal
+ message?"}
+ retry["Retry three times
+ to be robust against
+ test failures"]
+ testfailed["Test failed"]
+ failure_policy{"Known issue?"}
+
+ finished --> skipped
+ skipped --> |No| succeeded
+ skipped --> |Yes| skiptest[Show as skipped test]
+ succeeded --> |No| todonosuccess
+ succeeded --> |yes| affected
+
+ affected --> |Yes| affecteddone
+ affected --> |No| todosuccess
+
+ todosuccess --> |Yes| todosucceeded
+ todosuccess --> |No| done[Test succeeded]
+
+ todonosuccess --> |Yes| todofail
+ todonosuccess --> |No| failure_policy
+
+ failure_policy --> |Yes| known_issue[Show as known issue]
+ failure_policy --> |No| pixel_journal_failure
+
+ pixel_journal_failure --> |Yes| testfailed
+ pixel_journal_failure --> |No| retry
+```
+
+* `Skipped` - tests can be skipped because the test can not run on the given `TEST_OS`.
+* `Affected` - tests to be retried to make sure any changes affecting them do not lead to flaky tests.
+* `Todo` - tests which are incomplete and expected to fail.
+* `Known issue` - Naughties are expected test failures due to known issues in software we test,
+ we still run the test but if the test error output matches a known naughty it
+ is skipped. The [bots](https://github.com/cockpit-project/bots) repository keeps track
+ of all our known naughties per distro. (The bots repository has automation
+ setup to see if a naughty is still affected and if not open a pull request to
+ drop it).
+* `Failed` - tests can fail due to our test shared infrastructure, instead of
+ letting the whole test run fail, we re-try them unless `--no-retry-fail` is
+ passed.
+
+### Continuous Integration (CI)
+
+In CI we have two entry points, one for our tests which runs on our own
+managed infrastructure by [cockpituous](https://github.com/cockpit-project/cockpituous/)
+and one for tests which run on the [testing farm (TF)](https://docs.testing-farm.io/).
+
+For our own managed infrastructure the entry point of the Cockpit tests is
+`test/run`, or alternatively `.cockpit-ci/run`. This bash script expects a
+`TEST_OS` environment variable to be set to determine what distribution to run
+the tests under, and a `TEST_SCENARIO` environment variable to determine the
+type of test. A list of currently supported scenarios can be found in
+[test/run](./run).
+
+Cockpit's tests are split up in scenarios to heavily parallelize our testing and
+allow for faster retrying.
+
+[test/run](./run) prepares an Virtual machine image for the given `TEST_OS` and then
+runs the tests by calling `test/common/run-tests` with the provided tests.
+
+The [tmt](https://tmt.readthedocs.io) test is defined in [test/browser/main.fmf](./browser/main.fmf). These
+run on the Testing Farm (TF), triggered by [Packit](https://packit.dev/) for upstream PRs. On TF we
+get a single virtual machine without a hypervisor so tests run on the virtual
+machine directly. This also implies that only "non-destructive" tests can be run.
+The `test/browser/browser.sh` script sets up the virtual machine and calls
+`test/browser/run-tests.sh` which selects a subset of all the "non-destructive"
+tests to run using `test/common/run-tests`.
+
+The tmt test is also split up into three plans (basic, networking, optional) to
+run faster in parallel.
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..67b28ed
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,343 @@
+# Integration Tests of Cockpit
+
+This directory contains automated integration tests for Cockpit, and the
+support files for them. The architecture of the automated integration tests is
+described in [ARCHITECTURE](./ARCHITECTURE.md)
+
+To run the tests on Fedora, refer to the [HACKING](../HACKING.md) guide for
+installation of all of the necessary build and test dependencies. There's
+no need to trigger a build manually - the test suite preparation step below
+will handle that.
+
+If test failures are encountered that look like they may be related to problems
+with nested virtualization, refer to
+[this Fedora guide](https://docs.fedoraproject.org/en-US/quick-docs/using-nested-virtualization-in-kvm/index.html)
+for more details and recommendations on ensuring it is enabled correctly.
+
+## Preparation and general invocation
+
+*Warning*: Never run the build, test, or any other command here as root!
+
+You first need to build cockpit, and install it into a VM:
+
+ test/image-prepare
+
+This uses the default OS image, which is currently Fedora 38. See `$TEST_OS`
+below how to select a different one.
+
+In most cases you want to run an individual test in a suite, for example:
+
+ test/verify/check-metrics TestCurrentMetrics.testCPU
+
+You can get a list of tests by inspecting the `def test*` in the source, or by
+running the suite with `-l`/`--list`:
+
+ test/verify/check-metrics -l
+
+Sometimes you may also want to run all tests in a test file suite:
+
+ test/verify/check-session
+
+To see more verbose output from the test, use the `-v`/`--verbose` and/or `-t`/`--trace` flags:
+
+ test/verify/check-session --verbose --trace
+
+If you specify `-s`/`--sit` in addition, then the test will wait on failure and
+allow you to log into cockpit and/or the test instance and diagnose the issue.
+The cockpit and SSH addresses of the test instance will be printed:
+
+ test/verify/check-session -st
+
+You can also run *all* the tests, with some parallelism:
+
+ test/common/run-tests --test-dir test/verify --jobs 2
+
+However, this will take *really* long. You can specify a subset of tests (see
+`--help`); but usually it's better to run individual tests locally, and let the
+CI machinery run all of them in a draft pull request.
+
+The tests will automatically download the VM images they need, so expect
+that the initial run may take a few minutes.
+
+## Interactive browser
+
+Normally each test starts its own chromium headless browser process on a
+separate random port. To interactively follow what a test is doing:
+
+ TEST_SHOW_BROWSER=1 test/verify/check-session --trace
+
+You can also run a test against Firefox instead of Chromium:
+
+ TEST_BROWSER=firefox test/verify/check-session --trace
+
+See below for details.
+
+## Manual testing
+
+You can conduct manual interactive testing against a test image by starting the
+image like so:
+
+ bots/vm-run -s cockpit.socket debian-stable
+
+Once the machine is booted and the cockpit socket has been activated, a
+message will be printed describing how to access the virtual machine, via
+ssh and web. See the "Helpful tips" section below.
+
+## Pixel tests
+
+Pixel tests in Cockpit ensure that updates of our dependencies or code changes
+don't break the UI: for example slight changes of layout, padding, color and
+everything which isn't easily spotted by a human. They also give us confidence
+that an update of our UI Framework doesn't introduce changes in how Cockpit
+looks.
+
+Pixel tests make a screenshot of a selector and compare it to a known good
+reference image. if there is a difference, the test fails and a pixel
+difference is shown.
+
+This works as our tests run in the [cockpit/tasks container](https://quay.io/repository/cockpit/tasks)
+which pins the browser and font rendering so repeated runs provide the same
+pixels. To generate new pixels, this tasks container must be used; your own
+browser and font rendering software might generate different results. For more
+information read the ["introduction blog post"](https://cockpit-project.org/blog/pixel-testing.html).
+
+The test images are stored in a git submodule in the `test/reference` directory
+and be fetched with:
+
+```sh
+./test/common/pixel-tests update
+```
+
+As Cockpit tests under multiple distributions and it is not worth the effort to
+run pixel tests on every supported distribution we only run them for the
+image configured in `test/reference-image`.
+
+Our tests call `Browser.assert_pixels` at interesting and strategic places.
+This assertion method requires at least a CSS selector and an image title.
+Pixel tests are generated in five layouts by default: desktop, medium, mobile,
+dark and rtl.
+
+Take a screenshot of the content in `#detail-content`:
+```python
+browser.assert_pixels("#detail-content", "filesystem")
+```
+
+Take a screenshot of the content in `#detail-content` and ignore all elements
+with a class `disk-stats` as they change per test run:
+```python
+browser.assert_pixels("#detail-content", "filesystem", ignore=[".disks-stats"])
+```
+
+Take a screenshot of the content in `#detail-content` and skip it for a
+specific layout as it generates unstable pixels:
+```python
+browser.assert_pixels("#detail-content", "filesystem", skip_layouts=["rtl"])
+```
+
+To update pixel tests, locally run the test in the current tasks container, or
+create a draft PR and let the tests run for `test/reference-image` and
+afterwards fetch the new pixels:
+
+```
+./test/common/pixel-tests fetch "https://cockpit-logs.us-east-1.linodeobjects.com/<snip>/log.html"
+```
+
+Finally, upload the new pixel tests and commit the newly generated submodule commit:
+```
+./test/common/pixel-tests push
+```
+
+**Note** that you have to a part of the [Contributors group](https://github.com/orgs/cockpit-project/teams/contributors)
+to push pixel tests.
+
+## Test Configuration
+
+You can set these environment variables to configure the test suite:
+
+ TEST_OS The OS to run the tests in. Currently supported values:
+ "centos-8-stream"
+ "debian-stable"
+ "debian-testing"
+ "fedora-38"
+ "fedora-39"
+ "fedora-coreos"
+ "fedora-testing"
+ "rhel-8-10"
+ "rhel-8-10-distropkg"
+ "rhel-9-3"
+ "rhel4edge",
+ "ubuntu-2204"
+ "ubuntu-stable"
+ "fedora-39" is the default (TEST_OS_DEFAULT in bots/lib/constants.py)
+
+ TEST_JOBS How many tests to run in parallel. The default is 1.
+
+ TEST_CDP_PORT Attach to an actually running browser that is compatible with
+ the Chrome Debug Protocol, on the given port. Don't use this
+ with parallel tests.
+
+ TEST_BROWSER What browser should be used for testing. Currently supported values:
+ "chromium"
+ "firefox"
+ "chromium" is the default.
+
+ TEST_SHOW_BROWSER Set to run browser interactively. When not specified,
+ browser is run in headless mode. When set to "pixels",
+ the browser will be resized to the exact dimensions that
+ are used for pixel tests.
+
+ TEST_TIMEOUT_FACTOR Scale normal timeouts by given integer. Useful for
+ slow/busy testbeds or architectures.
+
+See the [bots documentation](https://github.com/cockpit-project/bots/blob/main/README.md)
+for details about the tools and configuration for these.
+
+## Convenient test VM SSH access
+
+It is recommended to add a snippet like this to your `~/.ssh/config`. Then
+you can log in to test machines without authentication:
+
+ Match final host 127.0.0.2
+ User root
+ StrictHostKeyChecking no
+ UserKnownHostsFile /dev/null
+ CheckHostIp no
+ IdentityFile CHECKOUT_DIR/bots/machine/identity
+ IdentitiesOnly yes
+
+You need to replace `CHECKOUT_DIR` with the actual directory where you cloned
+`cockpit.git`, or `bots.git` if you have a separate clone for that.
+
+Many cockpit developers take it a step further, and add an alias to
+allow typing `ssh c`:
+
+ Host c
+ Hostname 127.0.0.2
+ Port 2201
+
+The `final` keyword in the first rule will cause it to be checked (and matched)
+after the `Hostname` substitution in the `c` rule.
+
+## Fast develop/test iteration
+
+Each `image-prepare` invocation will always start from the pristine image and
+ignore the current overlay in `test/images`. It is thorough, but also rather
+slow. If you want to iterate on changing only JavaScript/HTML code, as opposed
+to the bridge or webserver, the whole build and test cycle can be done much
+faster.
+
+You always need to do at least one initial `test/image-prepare $TEST_OS` run.
+Afterwards it depends on the kind of test you want to run.
+
+### Nondestructive tests
+
+Many test methods or classes are marked as `@nondestructive`, meaning that
+they restore the state of the test VM enough that other tests can run
+afterwards. This is the fastest and most convenient situation for both
+iterating on the code and debugging failing tests.
+
+Start the prepared VM with `bots/vm-run $TEST_OS`. Note the SSH and cockpit
+ports. If this is the only running VM, it will have the ports in the
+examples below, otherwise the port will be different.
+
+Then start building the page you are working on
+[in watch and rsync mode](../HACKING.md#working-on-cockpits-session-pages), e.g.
+
+ RSYNC=c ./build.js -w users
+
+(Assuming the `c` SSH alias from the previous section and first running VM).
+
+Then you can run a corresponding test against the running VM, with additional
+debug output:
+
+ TEST_OS=... test/verify/check-users -t --machine 127.0.0.2:2201 --browser 127.0.0.2:9091 TestAccounts.testBasic
+
+### Destructive tests
+
+Other tests need one or more fresh VMs. Instead of a full `test/image-prepare`
+run (which is slow), you can update the existing VM overlay with updated
+bundles. Start the build in watch mode, but without rsyncing, e.g.
+
+ ./build.js -w storaged
+
+and after each iteration, copy the new bundles into the VM overlay:
+
+ bots/image-customize -u dist:/usr/share/cockpit/ $TEST_OS
+
+Then run the test as you would normally do, e.g.
+
+ TEST_OS=... test/verify/check-storage-stratis -t TestStorageStratis.testBasic
+
+Use `bots/vm-reset` to clean up all prepared overlays in `test/images`.
+
+## Debugging tests
+
+If you pass the `-s` ("sit on failure") option to a test program, it
+will pause when a failure occurs so that you can log into the test
+machine and investigate the problem.
+
+A test will print out the commands to access it when it fails in this
+way. You can log into a running test-machine using ssh. See the
+"Helpful tips" section below.
+
+You can also put calls to `sit()` into the tests themselves to stop them
+at strategic places.
+
+That way, you can run a test cleanly while still being able to make
+quick changes, such as adding debugging output to JavaScript.
+
+## Guidelines for writing tests
+
+If a test is not decorated with `@nondestructive`, it is OK for a test to
+destroy the test machine OS installation, or otherwise modify it without
+cleaning up. For example, it is OK to remove all of `/etc` just to see what
+happens. The next test will get a pristine test machine.
+
+Tests decorated with `@nondestructive` will all run against the same test
+machine. The nondestructive test should clean up after itself and restore the
+state of the machine, such that the next nondestructive test is not impacted.
+
+A fast running test suite is more important than independent,
+small test cases.
+
+Thus, it is OK for tests to be long. Starting the test machine is so slow that
+we should run as many checks within a single session as make sense. Note that
+nondestructive tests do not suffer from this, and are much quicker.
+
+Still, within a long test, try to have independent sections, where
+each section returns the machine to more or less the state that it was
+in before the section. This makes it easier to run these sections
+ad-hoc when doing incremental development.
+
+## Coverage
+
+Every pull request will trigger a `$DEFAULT_OS/devel` scenario which creates a
+coverage report of the JavaScript code executed and writes comments about
+uncovered code in the pull request. The overall coverage percentage is recorded
+in prometheus for a subset of our projects and [visualized in Grafana](https://grafana-cockpit.apps.ocp.cloud.ci.centos.org/d/ci/cockpit-ci?orgId=1).
+
+To generate coverage locally for `TestApps`:
+
+```
+export NODE_ENV=devel
+./build.js
+./test/image-prepare -q
+./test/common/run-tests --test-dir test/verify --coverage TestApps
+```
+
+Code which is impossible or very hard to test in our tests can be excluded from
+appearing in a pull request as comment by adding a `not-covered` comment with a
+short justification:
+
+```javascript
+return cockpit.script(data, { superuser: "try", err: "message" })
+ .catch(console.error); // not-covered: OS error
+```
+
+## Helpful tips
+
+For web access, if you'd like to avoid Chromium (or Chrome) prompting
+about certificate errors while connecting to localhost, you can change
+the following setting:
+
+ chrome://flags/#allow-insecure-localhost
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/__init__.py
diff --git a/test/browser/browser.sh b/test/browser/browser.sh
new file mode 100755
index 0000000..f6f7a51
--- /dev/null
+++ b/test/browser/browser.sh
@@ -0,0 +1,116 @@
+#!/bin/sh
+# This script is meant to be run on an ephemeral CI host, for packit/Fedora/RHEL gating.
+set -eux
+
+# like "basic", passed on to run-test.sh
+PLAN="$1"
+
+export TEST_BROWSER=${TEST_BROWSER:-firefox}
+
+MYDIR="$(realpath $(dirname "$0"))"
+SOURCE="$(realpath $MYDIR/../..)"
+# https://tmt.readthedocs.io/en/stable/overview.html#variables
+LOGS="${TMT_TEST_DATA:-$(pwd)/logs}"
+export SOURCE LOGS
+mkdir -p "$LOGS"
+chmod a+w "$LOGS"
+
+# show some system info
+nproc
+free -h
+rpm -qa | grep cockpit
+
+# install firefox (available everywhere in Fedora and RHEL)
+# we don't need the H.264 codec, and it is sometimes not available (rhbz#2005760)
+dnf install --disablerepo=fedora-cisco-openh264 -y --setopt=install_weak_deps=False firefox
+
+# nodejs 10 is too old for current Cockpit test API
+if grep -q platform:el8 /etc/os-release; then
+ dnf module switch-to -y nodejs:16
+fi
+
+# HACK: setroubleshoot-server crashes/times out randomly (breaking TestServices),
+# and is hard to disable as it does not use systemd
+if rpm -q setroubleshoot-server; then
+ dnf remove -y --setopt=clean_requirements_on_remove=False setroubleshoot-server
+fi
+
+if grep -q 'ID=.*fedora' /etc/os-release && [ "$PLAN" = "basic" ]; then
+ # Fedora-only packages which are not available in CentOS/RHEL
+ # required by TestLogin.testBasic
+ dnf install -y tcsh
+ # required by TestJournal.testAbrt*
+ dnf install -y abrt abrt-addon-ccpp reportd libreport-plugin-bugzilla libreport-fedora
+ # required by TestStorageBtrfs*
+ dnf install -y udisks2-btrfs
+fi
+
+# dnf installs "missing" weak dependencies, but we don't want them for plans other than "optional"
+if [ "$PLAN" != "optional" ] && rpm -q cockpit-packagekit; then
+ dnf remove -y cockpit-packagekit
+fi
+
+if grep -q 'ID=.*rhel' /etc/os-release; then
+ # required by TestUpdates.testKpatch, but kpatch is only in RHEL
+ dnf install -y kpatch kpatch-dnf
+fi
+
+# if we run during cross-project testing against our main-builds COPR, then let that win
+# even if Fedora has a newer revision
+main_builds_repo="$(ls /etc/yum.repos.d/*cockpit*main-builds* 2>/dev/null || true)"
+if [ -n "$main_builds_repo" ]; then
+ echo 'priority=0' >> "$main_builds_repo"
+ dnf distro-sync -y 'cockpit*'
+fi
+
+# RHEL 8 does not build cockpit-tests; when dropping RHEL 8 support, move to test/browser/main.fmf
+if [ "$PLAN" = basic ] && ! grep -q el8 /etc/os-release; then
+ dnf install -y cockpit-tests
+fi
+
+
+# On CentOS Stream 8 the cockpit package is upgraded so the file isn't touched.
+if [ ! -f /etc/cockpit/disallowed-users ]; then
+ echo 'root' > /etc/cockpit/disallowed-users
+fi
+
+#HACK: unbreak RHEL 9's default choice of 999999999 rounds, see https://bugzilla.redhat.com/show_bug.cgi?id=1993919
+sed -ie 's/#SHA_CRYPT_MAX_ROUNDS 5000/SHA_CRYPT_MAX_ROUNDS 5000/' /etc/login.defs
+
+# make libpwquality less aggressive, so that our "foobar" password works
+printf 'dictcheck = 0\nminlen = 6\n' >> /etc/security/pwquality.conf
+
+# set root password for logging in
+echo root:foobar | chpasswd
+
+# create user account for logging in
+if ! id admin 2>/dev/null; then
+ useradd -c Administrator -G wheel admin
+ echo admin:foobar | chpasswd
+fi
+
+# create user account for running the test
+if ! id runtest 2>/dev/null; then
+ useradd -c 'Test runner' runtest
+ # allow test to set up things on the machine
+ mkdir -p /root/.ssh
+ curl https://raw.githubusercontent.com/cockpit-project/bots/main/machine/identity.pub >> /root/.ssh/authorized_keys
+ chmod 600 /root/.ssh/authorized_keys
+fi
+chown -R runtest "$SOURCE"
+
+# disable core dumps, we rather investigate them upstream where test VMs are accessible
+echo core > /proc/sys/kernel/core_pattern
+
+# make sure that we can access cockpit through the firewall
+systemctl start firewalld
+firewall-cmd --add-service=cockpit --permanent
+firewall-cmd --add-service=cockpit
+
+# Run tests as unprivileged user
+# once we drop support for RHEL 8, use this:
+# runuser -u runtest --whitelist-environment=TEST_BROWSER,TEST_ALLOW_JOURNAL_MESSAGES,TEST_AUDIT_NO_SELINUX,SOURCE,LOGS "$MYDIR/run-test.sh" "$PLAN"
+runuser -u runtest --preserve-environment env USER=runtest HOME="$(getent passwd runtest | cut -f6 -d:)" "$MYDIR/run-test.sh" "$PLAN"
+
+RC=$(cat $LOGS/exitcode)
+exit ${RC:-1}
diff --git a/test/browser/main.fmf b/test/browser/main.fmf
new file mode 100644
index 0000000..a3585e8
--- /dev/null
+++ b/test/browser/main.fmf
@@ -0,0 +1,80 @@
+/basic:
+ summary: Run browser integration tests for basic Cockpit packages
+ require:
+ # ourself
+ - cockpit
+ - cockpit-kdump
+ - cockpit-networkmanager
+ - cockpit-sosreport
+ # FIXME: add this after dropping RHEL8 support
+ # - cockpit-tests
+ # build/test infra dependencies
+ - git
+ - make
+ - nodejs
+ - python3
+ - libvirt-python3
+ # required by tests
+ - dnf-automatic
+ - glibc-all-langpacks
+ - firewalld
+ - sssd
+ - sssd-dbus
+ - subscription-manager
+ - targetcli
+ - tlog
+ - tuned
+ test: ./browser.sh basic
+ duration: 1h
+
+/network:
+ summary: Run browser integration tests for cockpit-networkmanager
+ require:
+ # ourself
+ - cockpit
+ - cockpit-networkmanager
+ # build/test infra dependencies
+ - git
+ - make
+ - nodejs
+ - python3
+ - libvirt-python3
+ # required by tests
+ - NetworkManager-team
+ - firewalld
+ - libvirt-daemon-config-network
+ test: ./browser.sh network
+ duration: 1h
+
+/optional:
+ summary: Run browser integration tests for optional Cockpit packages
+ require:
+ # ourself
+ - cockpit
+ - cockpit-storaged
+ - cockpit-packagekit
+ # for at least swap metrics on storage page
+ - cockpit-pcp
+ # build/test infra dependencies
+ - git
+ - make
+ - nodejs
+ - python3
+ - libvirt-python3
+ # required by tests
+ - createrepo_c
+ - cryptsetup
+ - dnf-automatic
+ - firewalld
+ - lvm2
+ - nfs-utils
+ - python3-tracer
+ - rpm-build
+ - stratis-cli
+ - stratisd
+ - subscription-manager
+ - targetcli
+ - udisks2-lvm2
+ - udisks2-iscsi
+ test: ./browser.sh optional
+ duration: 1h
diff --git a/test/browser/run-test.sh b/test/browser/run-test.sh
new file mode 100755
index 0000000..1a4a47e
--- /dev/null
+++ b/test/browser/run-test.sh
@@ -0,0 +1,239 @@
+#!/bin/sh
+set -eux
+
+PLAN="$1"
+
+cd "$SOURCE"
+
+. /etc/os-release
+
+# on Fedora we always test all packages;
+# on RHEL/CentOS 8 we have a split package, only test basic bits for "cockpit" and optional bits for "c-appstream"
+if [ "$PLATFORM_ID" = "platform:el8" ]; then
+ if ls ../cockpit-appstream* 1> /dev/null 2>&1; then
+ if [ "$PLAN" = "basic" ] || [ "$PLAN" = "network" ]; then
+ echo "SKIP: not running basic/network tests for cockpit-appstream"
+ echo 0 > "$LOGS/exitcode"
+ exit 0
+ fi
+ else
+ if [ "$PLAN" = "optional" ]; then
+ echo "SKIP: not running optional tests for split RHEL 8 cockpit"
+ echo 0 > "$LOGS/exitcode"
+ exit 0
+ fi
+ fi
+fi
+
+# tests need cockpit's bots/ libraries
+git clone --depth=1 https://github.com/cockpit-project/bots
+
+# release tarballs include the necessary npm modules for testing
+if [ -d .git ]; then
+ ./tools/node-modules checkout
+fi
+
+export TEST_OS="${ID}-${VERSION_ID/./-}"
+
+if [ "${TEST_OS#centos-}" != "$TEST_OS" ]; then
+ TEST_OS="${TEST_OS}-stream"
+fi
+
+TEST_ALLOW_JOURNAL_MESSAGES=""
+
+# HACK: CI hits this selinux denial. Unrelated to our tests.
+TEST_ALLOW_JOURNAL_MESSAGES=".*Permission denied:.*/var/cache/app-info/xmls.*"
+
+# HACK: occasional failure, annoyingly hard to debug
+if [ "${TEST_OS#centos-8}" != "$TEST_OS" ]; then
+ TEST_ALLOW_JOURNAL_MESSAGES="${TEST_ALLOW_JOURNAL_MESSAGES},couldn't create runtime dir: /run/user/1001: Permission denied"
+fi
+
+# HACK: https://github.com/systemd/systemd/issues/24150
+if [ "$ID" = "fedora" ]; then
+ TEST_ALLOW_JOURNAL_MESSAGES="${TEST_ALLOW_JOURNAL_MESSAGES},Journal file /var/log/journal/*/user-1000@*.journal corrupted, ignoring file .*"
+fi
+
+export TEST_ALLOW_JOURNAL_MESSAGES
+
+# We only have one VM and tests should take at most one hour. So run those tests which exercise external API
+# (and thus are useful for reverse dependency testing and gating), and exclude those which test cockpit-internal
+# functionality to upstream CI. We also need to leave out some which make too strict assumptions about the testbed.
+TESTS=""
+EXCLUDES=""
+RC=0
+
+# make it easy to check in logs
+echo "TEST_ALLOW_JOURNAL_MESSAGES: ${TEST_ALLOW_JOURNAL_MESSAGES:-}"
+echo "TEST_AUDIT_NO_SELINUX: ${TEST_AUDIT_NO_SELINUX:-}"
+
+if [ "$PLAN" = "optional" ]; then
+ TESTS="$TESTS
+ TestAutoUpdates
+ TestUpdates
+ TestStorage
+ "
+
+ # Testing Farm machines often have pending restarts/reboot
+ EXCLUDES="$EXCLUDES TestUpdates.testBasic TestUpdates.testFailServiceRestart TestUpdates.testKpatch"
+
+ # FIXME: creation dialog hangs forever
+ EXCLUDES="$EXCLUDES TestStorageISCSI.testISCSI"
+
+ # These don't test more external APIs
+ EXCLUDES="$EXCLUDES
+ TestAutoUpdates.testBasic
+ TestAutoUpdates.testPrivilegeChange
+
+ TestStorageBtrfs.testNothingMounted
+
+ TestStorageFormat.testAtBoot
+ TestStorageFormat.testFormatCancel
+ TestStorageFormat.testFormatTooSmall
+ TestStorageFormat.testFormatTypes
+
+ TestStorageHidden.testHiddenRaid
+ TestStorageHidden.testHiddenSnap
+ TestStorageHiddenLuks.test
+
+ TestStorageMounting.testAtBoot
+ TestStorageMounting.testBadOption
+ TestStorageMounting.testFirstMount
+ TestStorageMounting.testMounting
+ TestStorageMounting.testMountingHelp
+ TestStorageMounting.testNeverAuto
+ TestStorageMountingLUKS.testEncryptedMountingHelp
+ TestStorageMountingLUKS.testDuplicateMountPoints
+ TestStorageMountingLUKS.testNeverAuto
+
+ TestStorageIgnored.testIgnored
+ TestStoragePackagesNFS.testNfsMissingPackages
+ TestStoragePartitions.testSizeSlider
+ TestStorageStratis.testAlerts
+ TestStorageUnused.testUnused
+
+ TestUpdates.testUnprivileged
+ TestUpdates.testPackageKitCrash
+ TestUpdates.testNoPackageKit
+ TestUpdates.testInfoTruncation
+ "
+
+ # RHEL test machines have a lot of junk mounted on /mnt
+ if [ "${TEST_OS#rhel-}" != "$TEST_OS" ]; then
+ EXCLUDES="$EXCLUDES
+ TestStorageNfs.testNfsBusy
+ TestStorageNfs.testNfsClient
+ TestStorageNfs.testNfsMountWithoutDiscovery
+ "
+ fi
+fi
+
+if [ "$PLAN" = "basic" ]; then
+ # Don't run TestPages, TestPackages, and TestTerminal at all -- not testing external APIs
+ TESTS="$TESTS
+ TestAccounts
+ TestKdump
+ TestJournal
+ TestLogin
+ TestServices
+ TestSOS
+ TestSystemInfo
+ TestTuned
+ "
+
+ # PCI devices list is not predictable
+ EXCLUDES="$EXCLUDES TestSystemInfo.testHardwareInfo"
+
+ if [ "${TEST_OS#rhel-8}" != "$TEST_OS" ] || [ "${TEST_OS#centos-8}" != "$TEST_OS" ]; then
+ # no cockpit-tests package in RHEL 8
+ EXCLUDES="$EXCLUDES TestLogin.testSELinuxRestrictedUser"
+
+ # fails to start second browser, timing out on http://127.0.0.1:{cdp_port}/json/list
+ # impossible to debug without access to the infra
+ EXCLUDES="$EXCLUDES TestAccounts.testUserPasswords"
+ fi
+
+ # These don't test more external APIs
+ EXCLUDES="$EXCLUDES
+ TestAccounts.testAccountLogs
+ TestAccounts.testExpire
+ TestAccounts.testRootLogin
+ TestAccounts.testUnprivileged
+
+ TestLogin.testConversation
+ TestLogin.testExpired
+ TestLogin.testFailingWebsocket
+ TestLogin.testFailingWebsocketSafari
+ TestLogin.testFailingWebsocketSafariNoCA
+ TestLogin.testLogging
+ TestLogin.testRaw
+ TestLogin.testServer
+ TestLogin.testUnsupportedBrowser
+
+ TestSOS.testWithUrlRoot
+ TestSOS.testCancel
+ TestSOS.testAppStream
+
+ TestSystemInfo.testInsightsStatus
+ TestSystemInfo.testMotd
+ TestSystemInfo.testShutdownStatus
+
+ TestJournal.testAbrtDelete
+ TestJournal.testAbrtReportNoReportd
+ TestJournal.testAbrtReportCancel
+ TestJournal.testBinary
+ TestJournal.testNoMessage
+
+ TestServices.testApi
+ TestServices.testConditions
+ TestServices.testHiddenFailure
+ TestServices.testLogs
+ TestServices.testLogsUser
+ TestServices.testNotFound
+ TestServices.testNotifyFailed
+ TestServices.testRelationships
+ TestServices.testRelationshipsUser
+ TestServices.testResetFailed
+ TestServices.testTransientUnits
+ TestServices.testUnprivileged
+ "
+fi
+
+if [ "$PLAN" = "network" ]; then
+ TESTS="$TESTS
+ TestBonding
+ TestBridge
+ TestFirewall
+ TestNetworking
+ TestTeam
+ "
+
+ # These don't test more external APIs
+ EXCLUDES="$EXCLUDES
+ TestBonding.testActive
+ TestBonding.testAmbiguousMember
+ TestBonding.testNonDefaultSettings
+
+ TestFirewall.testAddCustomServices
+ TestFirewall.testNetworkingPage
+
+ TestNetworkingBasic.testIpHelper
+ TestNetworkingBasic.testNoService
+ TestNetworkingUnmanaged.testUnmanaged
+ "
+fi
+
+
+exclude_options=""
+for t in $EXCLUDES; do
+ exclude_options="$exclude_options --exclude $t"
+done
+
+# execute run-tests
+test/common/run-tests --test-dir test/verify --nondestructive $exclude_options \
+ --machine localhost:22 --browser localhost:9090 $TESTS || RC=$?
+
+echo $RC > "$LOGS/exitcode"
+cp --verbose Test* "$LOGS" || true
+# deliver test result via exitcode file
+exit 0
diff --git a/test/common/__init__.py b/test/common/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/common/__init__.py
diff --git a/test/common/cdp.py b/test/common/cdp.py
new file mode 100644
index 0000000..034f7a2
--- /dev/null
+++ b/test/common/cdp.py
@@ -0,0 +1,381 @@
+import abc
+import fcntl
+import glob
+import json
+import os
+import random
+import resource
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+import typing
+import urllib.request
+from urllib.error import URLError
+
+TEST_DIR = os.path.normpath(os.path.dirname(os.path.realpath(os.path.join(__file__, ".."))))
+
+
+class Browser(abc.ABC):
+ # The name of the browser
+ NAME: str
+ # The executable names available for the browser
+ EXECUTABLES: typing.List[str]
+ # The filename of the cdp driver JS file
+ CDP_DRIVER_FILENAME: str
+
+ @property
+ def name(self):
+ return self.NAME
+
+ def find_exe(self):
+ """Try to find the path of the browser, or None if not found."""
+ for name in self.EXECUTABLES:
+ exe = shutil.which(name)
+ if exe is not None:
+ return exe
+ return None
+
+ @abc.abstractmethod
+ def _path(self, show_browser):
+ """Return the path of the browser if available, or None.
+
+ Reimplement this in subclasses, so it is easier to return None
+ than to raise the proper exception (done at once in path()).
+ """
+
+ def path(self, show_browser):
+ """Return the path of the browser, if available.
+
+ In case it is not found, this raises SystemError.
+ """
+ p = self._path(show_browser)
+ if p is not None:
+ return p
+ raise SystemError(f"{self.name} is not installed")
+
+ @abc.abstractmethod
+ def cmd(self, cdp_port, env, show_browser, browser_home, download_dir):
+ pass
+
+
+class Chromium(Browser):
+ NAME = "chromium"
+ EXECUTABLES = ["chromium-browser", "chromium", "google-chrome", "chromium-freeworld"]
+ CDP_DRIVER_FILENAME = f"{TEST_DIR}/common/chromium-cdp-driver.js"
+
+ def _path(self, show_browser):
+ """Return path to chromium browser.
+
+ Support the following locations:
+ - /usr/lib*/chromium-browser/headless_shell (chromium-headless RPM)
+ - the executables in self.EXECUTABLES available in $PATH (distro package)
+ - node_modules/chromium/lib/chromium/chrome-linux/chrome (npm install chromium)
+ """
+
+ # If we want to have interactive chromium, we don't want to use headless_shell
+ if not show_browser:
+ g = glob.glob("/usr/lib*/chromium-browser/headless_shell")
+ if g:
+ return g[0]
+
+ p = self.find_exe()
+ if p:
+ return p
+
+ p = os.path.join(os.path.dirname(TEST_DIR), "node_modules/chromium/lib/chromium/chrome-linux/chrome")
+ if os.access(p, os.X_OK):
+ return p
+
+ return None
+
+ def cmd(self, cdp_port, env, show_browser, browser_home, download_dir):
+ exe = self.path(show_browser)
+
+ return [exe, "--headless" if not show_browser else "",
+ "--no-sandbox", "--disable-setuid-sandbox",
+ "--disable-namespace-sandbox", "--disable-seccomp-filter-sandbox",
+ "--disable-sandbox-denial-logging", "--disable-pushstate-throttle",
+ "--font-render-hinting=none",
+ "--v=0", f"--remote-debugging-port={cdp_port}", "about:blank"]
+
+
+class Firefox(Browser):
+ NAME = "firefox"
+ EXECUTABLES = ["firefox-developer-edition", "firefox-nightly", "firefox"]
+ CDP_DRIVER_FILENAME = f"{TEST_DIR}/common/firefox-cdp-driver.js"
+
+ def _path(self, show_browser):
+ """Return path to Firefox browser."""
+ return self.find_exe()
+
+ def cmd(self, cdp_port, env, show_browser, browser_home, download_dir):
+ exe = self.path(show_browser)
+
+ subprocess.check_call([exe, "--headless", "--no-remote", "-CreateProfile", "blank"], env=env)
+ profile = glob.glob(os.path.join(browser_home, ".mozilla/firefox/*.blank"))[0]
+
+ with open(os.path.join(profile, "user.js"), "w") as f:
+ f.write(f"""
+ user_pref("remote.enabled", true);
+ user_pref("remote.frames.enabled", true);
+ user_pref("app.update.auto", false);
+ user_pref("datareporting.policy.dataSubmissionEnabled", false);
+ user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
+ user_pref("dom.disable_beforeunload", true);
+ user_pref("browser.download.dir", "{download_dir}");
+ user_pref("browser.download.folderList", 2);
+ user_pref("signon.rememberSignons", false);
+ user_pref("dom.navigation.locationChangeRateLimit.count", 9999);
+ // HACK: https://bugzilla.mozilla.org/show_bug.cgi?id=1746154
+ user_pref("fission.webContentIsolationStrategy", 0);
+ user_pref("fission.bfcacheInParent", false);
+ """)
+
+ with open(os.path.join(profile, "handlers.json"), "w") as f:
+ f.write('{'
+ '"defaultHandlersVersion":{"en-US":4},'
+ '"mimeTypes":{"application/xz":{"action":0,"extensions":["xz"]}}'
+ '}')
+
+ cmd = [exe, "-P", "blank", f"--remote-debugging-port={cdp_port}", "--no-remote", "localhost"]
+ if not show_browser:
+ cmd.insert(3, "--headless")
+ return cmd
+
+
+def get_browser(browser):
+ browser_classes = [
+ Chromium,
+ Firefox,
+ ]
+ for klass in browser_classes:
+ if browser == klass.NAME:
+ return klass()
+ raise SystemError(f"Unsupported browser: {browser}")
+
+
+def jsquote(obj):
+ return json.dumps(obj)
+
+
+class CDP:
+ def __init__(self, lang=None, verbose=False, trace=False, inject_helpers=None, start_profile=False):
+ self.lang = lang
+ self.timeout = 15
+ self.valid = False
+ self.verbose = verbose
+ self.trace = trace
+ self.inject_helpers = inject_helpers or []
+ self.start_profile = start_profile
+ self.browser = get_browser(os.environ.get("TEST_BROWSER", "chromium"))
+ self.show_browser = bool(os.environ.get("TEST_SHOW_BROWSER", ""))
+ self.download_dir = tempfile.mkdtemp()
+ self._driver = None
+ self._browser = None
+ self._browser_home = None
+ self._cdp_port_lockfile = None
+
+ def invoke(self, fn, **kwargs):
+ """Call a particular CDP method such as Runtime.evaluate
+
+ Use command() for arbitrary JS code.
+ """
+ trace = self.trace and not kwargs.get("no_trace", False)
+ try:
+ del kwargs["no_trace"]
+ except KeyError:
+ pass
+
+ cmd = fn + "(" + json.dumps(kwargs) + ")"
+
+ # frame support for Runtime.evaluate(): map frame name to
+ # executionContextId and insert into argument object; this must not be quoted
+ # see "Frame tracking" in cdp-driver.js for how this works
+ if fn == 'Runtime.evaluate':
+ cmd = "%s, contextId: getFrameExecId(%s)%s" % (cmd[:-2], jsquote(self.cur_frame), cmd[-2:])
+
+ if trace:
+ print("-> " + kwargs.get('trace', cmd))
+
+ # avoid having to write the "client." prefix everywhere
+ cmd = "client." + cmd
+ res = self.command(cmd)
+ if trace:
+ if res and "result" in res:
+ print("<- " + repr(res["result"]))
+ else:
+ print("<- " + repr(res))
+ return res
+
+ def command(self, cmd):
+ if not self._driver:
+ self.start()
+ self._driver.stdin.write(cmd.encode("UTF-8"))
+ self._driver.stdin.write(b"\n")
+ self._driver.stdin.flush()
+ line = self._driver.stdout.readline().decode("UTF-8")
+ if not line:
+ self.kill()
+ raise RuntimeError("CDP broken")
+ try:
+ res = json.loads(line)
+ except ValueError:
+ print(line.strip())
+ raise
+
+ if "error" in res:
+ if self.trace:
+ print("<- raise %s" % str(res["error"]))
+ raise RuntimeError(res["error"])
+ return res["result"]
+
+ def claim_port(self, port):
+ f = None
+ try:
+ f = open(os.path.join(tempfile.gettempdir(), ".cdp-%i.lock" % port), "w")
+ fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ self._cdp_port_lockfile = f
+ return True
+ except (IOError, OSError):
+ if f:
+ f.close()
+ return False
+
+ def find_cdp_port(self):
+ """Find an unused port and claim it through lock file"""
+
+ for _ in range(100):
+ # don't use the default CDP port 9222 to avoid interfering with running browsers
+ port = random.randint(9223, 10222)
+ if self.claim_port(port):
+ return port
+
+ raise RuntimeError("unable to find free port")
+
+ def start(self):
+ environ = os.environ.copy()
+ if self.lang:
+ environ["LC_ALL"] = self.lang
+ self.cur_frame = None
+
+ # allow attaching to external browser
+ cdp_port = None
+ if "TEST_CDP_PORT" in os.environ:
+ p = int(os.environ["TEST_CDP_PORT"])
+ if self.claim_port(p):
+ # can fail when a test starts multiple browsers; only show the first one
+ cdp_port = p
+
+ if not cdp_port:
+ # start browser on a new port
+ cdp_port = self.find_cdp_port()
+ self._browser_home = tempfile.mkdtemp()
+ environ = os.environ.copy()
+ environ["HOME"] = self._browser_home
+ environ["LC_ALL"] = "C.UTF-8"
+ # this might be set for the tests themselves, but we must isolate caching between tests
+ try:
+ del environ["XDG_CACHE_HOME"]
+ except KeyError:
+ pass
+
+ cmd = self.browser.cmd(cdp_port, environ, self.show_browser,
+ self._browser_home, self.download_dir)
+
+ # sandboxing does not work in Docker container
+ self._browser = subprocess.Popen(
+ cmd, env=environ, close_fds=True,
+ preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0)))
+ if self.verbose:
+ sys.stderr.write("Started %s (pid %i) on port %i\n" % (cmd[0], self._browser.pid, cdp_port))
+
+ # wait for CDP to be up and have at least one target
+ for _ in range(120):
+ try:
+ res = urllib.request.urlopen(f"http://127.0.0.1:{cdp_port}/json/list", timeout=5)
+ if res.getcode() == 200 and json.loads(res.read()):
+ break
+ except URLError:
+ pass
+ time.sleep(0.5)
+ else:
+ raise RuntimeError('timed out waiting for browser to start')
+
+ # now start the driver
+ if self.trace:
+ # enable frame/execution context debugging if tracing is on
+ environ["TEST_CDP_DEBUG"] = "1"
+
+ self._driver = subprocess.Popen([self.browser.CDP_DRIVER_FILENAME, str(cdp_port)],
+ env=environ,
+ stdout=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ close_fds=True)
+ self.valid = True
+
+ for inject in self.inject_helpers:
+ with open(inject) as f:
+ src = f.read()
+ # HACK: injecting sizzle fails on missing `document` in assert()
+ src = src.replace('function assert( fn ) {', 'function assert( fn ) { if (true) return true; else ')
+ # HACK: sizzle tracks document and when we switch frames, it sees the old document
+ # although we execute it in different context.
+ src = src.replace('context = context || document;', 'context = context || window.document;')
+ self.invoke("Page.addScriptToEvaluateOnNewDocument", source=src, no_trace=True)
+
+ if self.start_profile:
+ self.invoke("Profiler.enable")
+ self.invoke("Profiler.startPreciseCoverage", callCount=False, detailed=True)
+
+ def kill(self):
+ self.valid = False
+ self.cur_frame = None
+ if self._driver:
+ self._driver.stdin.close()
+ self._driver.wait()
+ self._driver = None
+
+ shutil.rmtree(self.download_dir, ignore_errors=True)
+
+ if self._browser:
+ if self.verbose:
+ sys.stderr.write("Killing browser (pid %i)\n" % self._browser.pid)
+ try:
+ self._browser.terminate()
+ except OSError:
+ pass # ignore if it crashed for some reason
+ self._browser.wait()
+ self._browser = None
+ shutil.rmtree(self._browser_home, ignore_errors=True)
+ os.remove(self._cdp_port_lockfile.name)
+ self._cdp_port_lockfile.close()
+
+ def set_frame(self, frame):
+ self.cur_frame = frame
+ if self.trace:
+ print("-> switch to frame %s" % frame)
+
+ def get_js_log(self):
+ """Return the current javascript console log"""
+
+ if self.valid:
+ # needs to be wrapped in Promise
+ messages = self.command("Promise.resolve(messages)")
+ return ["%s: %s" % tuple(m) for m in messages]
+ return []
+
+ def read_log(self):
+ """Returns an iterator that produces log messages one by one.
+
+ Blocks if there are no new messages right now."""
+
+ if not self.valid:
+ yield []
+ return
+
+ while True:
+ messages = self.command("waitLog()")
+ for m in messages:
+ yield m
diff --git a/test/common/chromium-cdp-driver.js b/test/common/chromium-cdp-driver.js
new file mode 100755
index 0000000..4efff7b
--- /dev/null
+++ b/test/common/chromium-cdp-driver.js
@@ -0,0 +1,346 @@
+#!/usr/bin/env node
+
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* chromium-cdp-driver -- A command-line JSON input/output wrapper around
+ * chrome-remote-interface (Chrome Debug Protocol).
+ * See https://chromedevtools.github.io/devtools-protocol/
+ * This needs support for protocol version 1.3.
+ *
+ * Set $TEST_CDP_DEBUG environment variable to enable additional
+ * frame/execution context debugging.
+ */
+
+import * as readline from 'node:readline/promises';
+import CDP from 'chrome-remote-interface';
+
+let enable_debug = false;
+
+function debug(msg) {
+ if (enable_debug)
+ process.stderr.write("CDP: " + msg + "\n");
+}
+
+/**
+ * Format response to the client
+ */
+
+function fail(err) {
+ if (typeof err === 'undefined')
+ err = null;
+ process.stdout.write(JSON.stringify({ error: err }) + '\n');
+}
+
+function success(result) {
+ if (typeof result === 'undefined')
+ result = null;
+ process.stdout.write(JSON.stringify({ result }) + '\n');
+}
+
+/**
+ * Record console.*() calls and Log messages so that we can forward them to
+ * stderr and dump them on test failure
+ */
+const messages = [];
+let logPromiseResolver;
+let nReportedLogMessages = 0;
+const unhandledExceptions = [];
+
+function clearExceptions() {
+ unhandledExceptions.length = 0;
+ return Promise.resolve();
+}
+
+function stringifyConsoleArg(arg) {
+ try {
+ if (arg.type === 'string')
+ return arg.value;
+ if (arg.type === 'number')
+ return arg.value;
+ if (arg.type === 'undefined')
+ return "undefined";
+ if (arg.value === null)
+ return "null";
+ if (arg.type === 'object' && arg.preview?.properties) {
+ const obj = {};
+ arg.preview.properties.forEach(prop => {
+ obj[prop.name] = prop.value.toString();
+ });
+ return JSON.stringify(obj);
+ }
+ return JSON.stringify(arg);
+ } catch (error) {
+ return "[error stringifying argument: " + error.toString() + "]";
+ }
+}
+
+function setupLogging(client) {
+ client.Runtime.enable();
+
+ client.Runtime.consoleAPICalled(info => {
+ const msg = info.args.map(stringifyConsoleArg).join(" ");
+ messages.push([info.type, msg]);
+ process.stderr.write("> " + info.type + ": " + msg + "\n");
+
+ resolveLogPromise();
+ });
+
+ client.Runtime.exceptionThrown(info => {
+ const details = info.exceptionDetails;
+ // don't log test timeouts, they already get handled
+ if (details.exception && details.exception.className === "PhWaitCondTimeout")
+ return;
+
+ // HACK: https://github.com/cockpit-project/cockpit/issues/14871
+ if (details.description && details.description.indexOf("Rendering components directly into document.body is discouraged") > -1)
+ return;
+
+ process.stderr.write(details.description || JSON.stringify(details) + "\n");
+
+ unhandledExceptions.push(details.exception.message ||
+ details.exception.description ||
+ details.exception.value ||
+ JSON.stringify(details.exception));
+ });
+
+ client.Log.enable();
+ client.Log.entryAdded(entry => {
+ const msg = entry.entry;
+ /* Ignore unsafe-inline messages from PatternFly's usage of Emotion
+ * (https://github.com/patternfly/patternfly-react/issues/2919) */
+ if ((msg.text || "").indexOf("Refused to apply inline style") >= 0) {
+ /* when building with --enable-debug, we have proper symbols and can reliably identify the source */
+ if (msg.stackTrace && msg.stackTrace.callFrames && msg.stackTrace.callFrames[0].functionName === "makeStyleTag")
+ return;
+ /* further trim the output by dropping the stackTrace if it's minified */
+ if (msg.stackTrace && msg.stackTrace.callFrames && msg.stackTrace.callFrames[0].functionName.length == 1)
+ msg.stackTrace = "(minified)";
+ }
+
+ messages.push(["cdp", msg]);
+ /* Ignore authentication failure log lines that don't denote failures */
+ if (!(msg.url || "").endsWith("/login") || (msg.text || "").indexOf("401") === -1) {
+ process.stderr.write("CDP: " + JSON.stringify(msg) + "\n");
+ }
+ resolveLogPromise();
+ });
+}
+
+/**
+ * Resolve the log promise created with waitLog().
+ */
+function resolveLogPromise() {
+ if (logPromiseResolver) {
+ logPromiseResolver(messages.slice(nReportedLogMessages));
+ nReportedLogMessages = messages.length;
+ logPromiseResolver = undefined;
+ }
+}
+
+/**
+ * Returns a promise that resolves when log messages are available. If there
+ * are already some unreported ones in the global messages variable, resolves
+ * immediately.
+ *
+ * Only one such promise can be active at a given time. Once the promise is
+ * resolved, this function can be called again to wait for further messages.
+ */
+function waitLog() { // eslint-disable-line no-unused-vars
+ console.assert(logPromiseResolver === undefined);
+
+ return new Promise((resolve, reject) => {
+ logPromiseResolver = resolve;
+
+ if (nReportedLogMessages < messages.length)
+ resolveLogPromise();
+ });
+}
+
+/**
+ * Frame tracking
+ *
+ * For tests to be able to select the current frame (by its name) and make
+ * subsequent queries apply to that, we need to track frame name → frameId →
+ * executionContextId. Frame and context IDs can even change through page
+ * operations (e. g. in systemd/logs.js when reporting a crash is complete),
+ * so we also need a helper function to explicitly wait for a particular frame
+ * to load. This is very laborious, see this issue for discussing improvements:
+ * https://github.com/ChromeDevTools/devtools-protocol/issues/72
+ */
+const frameIdToContextId = {};
+const frameNameToFrameId = {};
+
+let pageLoadHandler = null;
+
+function setupFrameTracking(client) {
+ client.Page.enable();
+
+ // map frame names to frame IDs; root frame has no name, no need to track that
+ client.Page.frameNavigated(info => {
+ debug("frameNavigated " + JSON.stringify(info));
+ frameNameToFrameId[info.frame.name || "cockpit1"] = info.frame.id;
+ });
+
+ client.Page.loadEventFired(() => {
+ if (pageLoadHandler) {
+ debug("loadEventFired, resolving pageLoadHandler");
+ pageLoadHandler();
+ }
+ });
+
+ // track execution contexts so that we can map between context and frame IDs
+ client.Runtime.executionContextCreated(info => {
+ debug("executionContextCreated " + JSON.stringify(info));
+ frameIdToContextId[info.context.auxData.frameId] = info.context.id;
+ });
+
+ client.Runtime.executionContextDestroyed(info => {
+ debug("executionContextDestroyed " + info.executionContextId);
+ for (const frameId in frameIdToContextId) {
+ if (frameIdToContextId[frameId] == info.executionContextId) {
+ delete frameIdToContextId[frameId];
+ break;
+ }
+ }
+ });
+}
+
+function setupLocalFunctions(client) {
+ client.waitPageLoad = (args) => new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ pageLoadHandler = null;
+ reject("Timeout waiting for page load"); // eslint-disable-line prefer-promise-reject-errors
+ }, (args.timeout ?? 15) * 1000);
+ pageLoadHandler = () => {
+ clearTimeout(timeout);
+ pageLoadHandler = null;
+ resolve({});
+ };
+ });
+
+ client.reloadPageAndWait = (args) => new Promise((resolve, reject) => {
+ pageLoadHandler = () => { pageLoadHandler = null; resolve({}) };
+ client.Page.reload(args);
+ });
+
+ async function setCSS({ text, frame }) {
+ await client.DOM.enable();
+ await client.CSS.enable();
+ const id = (await client.CSS.createStyleSheet({ frameId: frameNameToFrameId[frame] })).styleSheetId;
+ await client.CSS.setStyleSheetText({
+ styleSheetId: id,
+ text
+ });
+ }
+
+ client.setCSS = setCSS;
+}
+
+// helper functions for testlib.py which are too unwieldy to be poked in from Python
+
+// eslint-disable-next-line no-unused-vars
+const getFrameExecId = frame => frameIdToContextId[frameNameToFrameId[frame ?? "cockpit1"]];
+
+/**
+ * SSL handling
+ */
+
+// secure by default; tests can override to "continue"
+// https://chromedevtools.github.io/devtools-protocol/1-3/Security/#type-CertificateErrorAction
+let ssl_bad_certificate_action = "cancel";
+
+/**
+ * Change what happens when the browser opens a page with an invalid SSL certificate.
+ * Defaults to "cancel", can be set to "continue".
+ */
+function setSSLBadCertificateAction(action) { // eslint-disable-line no-unused-vars
+ ssl_bad_certificate_action = action;
+ return Promise.resolve();
+}
+
+function setupSSLCertHandling(client) {
+ client.Security.enable();
+
+ client.Security.setOverrideCertificateErrors({ override: true });
+ client.Security.certificateError(info => {
+ process.stderr.write(`CDP: Security.certificateError ${JSON.stringify(info)}; action: ${ssl_bad_certificate_action}\n`);
+ client.Security.handleCertificateError({ eventId: info.eventId, action: ssl_bad_certificate_action })
+ .catch(ex => {
+ // some race condition in Chromium, ok if the event is already gone
+ if (ex.response && ex.response.message && ex.response.message.indexOf("Unknown event id") >= 0)
+ debug(`setupSSLCertHandling for event ${info.eventId} failed, ignoring: ${JSON.stringify(ex.response)}`);
+ else
+ throw ex;
+ });
+ });
+}
+
+/**
+ * Main input/process loop
+ *
+ * Read one line with a JS expression, eval() it, and respond with the result:
+ * success <JSON formatted return value>
+ * fail <JSON formatted error>
+ * EOF shuts down the client.
+ */
+async function main() {
+ process.stdin.setEncoding('utf8');
+
+ if (process.env.TEST_CDP_DEBUG)
+ enable_debug = true;
+
+ const options = { };
+ if (process.argv.length >= 3) {
+ options.port = parseInt(process.argv[2]);
+ if (!options.port) {
+ process.stderr.write("Usage: chromium-cdp-driver.js [port]\n");
+ process.exit(1);
+ }
+ }
+
+ const target = await CDP.New(options);
+ target.port = options.port;
+ const client = await CDP({ target });
+ setupLogging(client);
+ setupFrameTracking(client);
+ setupSSLCertHandling(client);
+ setupLocalFunctions(client);
+
+ for await (const command of readline.createInterface(process.stdin)) {
+ try {
+ const reply = await eval(command); // eslint-disable-line no-eval
+ if (unhandledExceptions.length === 0) {
+ success(reply);
+ } else {
+ const message = unhandledExceptions[0];
+ fail(message.split("\n")[0]);
+ clearExceptions();
+ }
+ } catch (err) {
+ fail(err);
+ }
+ }
+ await CDP.Close(target);
+}
+
+main().catch(err => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/test/common/firefox-cdp-driver.js b/test/common/firefox-cdp-driver.js
new file mode 100755
index 0000000..f5b2bf8
--- /dev/null
+++ b/test/common/firefox-cdp-driver.js
@@ -0,0 +1,392 @@
+#!/usr/bin/env node
+
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* firefox-cdp-driver -- A command-line JSON input/output wrapper around
+ * chrome-remote-interface (Chrome Debug Protocol).
+ * See https://chromedevtools.github.io/devtools-protocol/
+ * This needs support for protocol version 1.3.
+ *
+ * Set $TEST_CDP_DEBUG environment variable to enable additional
+ * frame/execution context debugging.
+ */
+
+import * as readline from 'readline';
+import CDP from 'chrome-remote-interface';
+
+let enable_debug = false;
+
+function debug(msg) {
+ if (enable_debug)
+ process.stderr.write("CDP: " + msg + "\n");
+}
+
+/**
+ * Format response to the client
+ */
+
+function fatal() {
+ console.error.apply(console.error, arguments);
+ process.exit(1);
+}
+
+// We keep sequence numbers so that we never get the protocol out of
+// synch with re-ordered or duplicate replies. This only matters for
+// duplicate replies due to destroyed contexts, but that is already so
+// hairy that this big hammer seems necessary.
+
+let cur_cmd_seq = 0;
+let next_reply_seq = 1;
+
+function fail(seq, err) {
+ if (seq != next_reply_seq)
+ return;
+ next_reply_seq++;
+
+ if (typeof err === 'undefined')
+ err = null;
+ process.stdout.write(JSON.stringify({ error: err }) + '\n');
+}
+
+function success(seq, result) {
+ if (seq != next_reply_seq)
+ return;
+ next_reply_seq++;
+
+ if (typeof result === 'undefined')
+ result = null;
+ process.stdout.write(JSON.stringify({ result }) + '\n');
+}
+
+/**
+ * Record console.*() calls and Log messages so that we can forward them to
+ * stderr and dump them on test failure
+ */
+const messages = [];
+let logPromiseResolver;
+let nReportedLogMessages = 0;
+const unhandledExceptions = [];
+
+function clearExceptions() {
+ unhandledExceptions.length = 0;
+ return Promise.resolve();
+}
+
+function stringifyConsoleArg(arg) {
+ if (arg.type === 'string')
+ return arg.value;
+ if (arg.type === 'object')
+ return JSON.stringify(arg.value);
+ return JSON.stringify(arg);
+}
+
+function setupLogging(client) {
+ client.Runtime.enable();
+
+ client.Runtime.consoleAPICalled(info => {
+ const msg = info.args.map(stringifyConsoleArg).join(" ");
+ messages.push([info.type, msg]);
+ process.stderr.write("> " + info.type + ": " + msg + "\n");
+
+ resolveLogPromise();
+ });
+
+ function processException(info) {
+ let details = info.exceptionDetails;
+ if (details.exception)
+ details = details.exception;
+
+ // don't log test timeouts, they already get handled
+ if (details.className === "PhWaitCondTimeout")
+ return;
+
+ process.stderr.write(details.description || details.text || JSON.stringify(details) + "\n");
+
+ unhandledExceptions.push(details.message ||
+ details.description ||
+ details.value ||
+ JSON.stringify(details));
+ }
+
+ client.Runtime.exceptionThrown(info => processException(info));
+
+ client.Log.enable();
+ client.Log.entryAdded(entry => {
+ // HACK: Firefox does not implement `Runtime.exceptionThrown` but logs it
+ // Lets parse it to have at least some basic check that code did not throw
+ // exception
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1549528
+
+ const msg = entry.entry;
+ let text = msg.text;
+ if (typeof text !== "string")
+ if (text[0] && typeof text[0] === "string")
+ text = text[0];
+
+ if (msg.stackTrace !== undefined &&
+ typeof text === "string" &&
+ text.indexOf("Error: ") !== -1) {
+ const trace = text.split(": ", 1);
+ processException({
+ exceptionDetails: {
+ exception: {
+ className: trace[0],
+ message: trace.length > 1 ? trace[1] : "",
+ stacktrace: msg.stackTrace,
+ entry: msg,
+ },
+ }
+ });
+ } else {
+ messages.push(["cdp", msg]);
+ /* Ignore authentication failure log lines that don't denote failures */
+ if (!(msg.url || "").endsWith("/login") || (text || "").indexOf("401") === -1) {
+ process.stderr.write("CDP: " + JSON.stringify(msg) + "\n");
+ }
+ resolveLogPromise();
+ }
+ });
+}
+
+/**
+ * Resolve the log promise created with waitLog().
+ */
+function resolveLogPromise() {
+ if (logPromiseResolver) {
+ logPromiseResolver(messages.slice(nReportedLogMessages));
+ nReportedLogMessages = messages.length;
+ logPromiseResolver = undefined;
+ }
+}
+
+/**
+ * Returns a promise that resolves when log messages are available. If there
+ * are already some unreported ones in the global messages variable, resolves
+ * immediately.
+ *
+ * Only one such promise can be active at a given time. Once the promise is
+ * resolved, this function can be called again to wait for further messages.
+ */
+function waitLog() { // eslint-disable-line no-unused-vars
+ console.assert(logPromiseResolver === undefined);
+
+ return new Promise((resolve, reject) => {
+ logPromiseResolver = resolve;
+
+ if (nReportedLogMessages < messages.length)
+ resolveLogPromise();
+ });
+}
+
+/**
+ * Frame tracking
+ *
+ * For tests to be able to select the current frame (by its name) and make
+ * subsequent queries apply to that, we need to track frame name → frameId →
+ * executionContextId. Frame and context IDs can even change through page
+ * operations (e. g. in systemd/logs.js when reporting a crash is complete),
+ * so we also need a helper function to explicitly wait for a particular frame
+ * to load. This is very laborious, see this issue for discussing improvements:
+ * https://github.com/ChromeDevTools/devtools-protocol/issues/72
+ */
+const scriptsOnNewContext = [];
+const frameIdToContextId = {};
+const frameNameToFrameId = {};
+
+let pageLoadHandler = null;
+let currentExecId = null;
+
+function setupFrameTracking(client) {
+ client.Page.enable();
+
+ // map frame names to frame IDs; root frame has no name, no need to track that
+ client.Page.frameNavigated(info => {
+ if (info.frame?.url?.startsWith("about:")) {
+ debug("frameNavigated: ignoring about: frame " + JSON.stringify(info));
+ return;
+ }
+ debug("frameNavigated " + JSON.stringify(info));
+ frameNameToFrameId[info.frame.name || "cockpit1"] = info.frame.id;
+ });
+
+ client.Page.loadEventFired(() => {
+ if (pageLoadHandler) {
+ debug("loadEventFired, resolving pageLoadHandler");
+ pageLoadHandler();
+ }
+ });
+
+ // track execution contexts so that we can map between context and frame IDs
+ client.Runtime.executionContextCreated(info => {
+ debug("executionContextCreated " + JSON.stringify(info));
+ frameIdToContextId[info.context.auxData.frameId] = info.context.id;
+ scriptsOnNewContext.forEach(s => {
+ client.Runtime.evaluate({ expression: s, contextId: info.context.id })
+ .catch(ex => {
+ // race condition with short-lived frames -- OK if the frame is already gone
+ if (ex.response && ex.response.message && ex.response.message.indexOf("Cannot find context") >= 0)
+ debug(`scriptsOnNewContext for context ${info.context.id} failed, ignoring: ${JSON.stringify(ex.response)}`);
+ else
+ throw ex;
+ });
+ });
+ });
+
+ client.Runtime.executionContextDestroyed(info => {
+ debug("executionContextDestroyed " + info.executionContextId);
+ for (const frameId in frameIdToContextId) {
+ if (frameIdToContextId[frameId] == info.executionContextId) {
+ delete frameIdToContextId[frameId];
+ break;
+ }
+ }
+
+ // Firefox does not report an error when the execution context
+ // of a Runtime.evaluate call gets destroyed. It will never
+ // ever resolve or be rejected. So let's provide the failure
+ // reply from here.
+ //
+ // However, if the timing is just right, the context gets
+ // destroyed before Runtime.evaluate has started the real
+ // processing, and in that case it will return an error. Then
+ // we would send the reply here, and would also send the
+ // error. This would drive the protocol out of synch. Also, our driver
+ // might immediately send more commands after seeing the first reply,
+ // and the unwanted second reply might be triggered in the middle of one
+ // of the next commands. To reliably suppress the second reply we have
+ // the pretty general sequence number checks.
+ //
+ if (info.executionContextId == currentExecId) {
+ currentExecId = null;
+ fail(cur_cmd_seq, { response: { message: "Execution context was destroyed." } });
+ }
+ });
+}
+
+function setupLocalFunctions(client) {
+ client.waitPageLoad = (args) => new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ pageLoadHandler = null;
+ reject("Timeout waiting for page load"); // eslint-disable-line prefer-promise-reject-errors
+ }, 15000);
+ pageLoadHandler = () => {
+ clearTimeout(timeout);
+ pageLoadHandler = null;
+ resolve({});
+ };
+ });
+
+ client.reloadPageAndWait = (args) => new Promise((resolve, reject) => {
+ pageLoadHandler = () => { pageLoadHandler = null; resolve({}) };
+ client.Page.reload(args);
+ });
+}
+
+// helper functions for testlib.py which are too unwieldy to be poked in from Python
+function getFrameExecId(frame) { // eslint-disable-line no-unused-vars
+ const frameId = frameNameToFrameId[frame || "cockpit1"];
+ const execId = frameIdToContextId[frameId];
+ if (execId !== undefined)
+ currentExecId = execId;
+ else
+ debug(`WARNING: getFrameExecId: frame ${frame} ID ${frameId} has no known execution context`);
+ return execId;
+}
+
+/**
+ * Main input/process loop
+ *
+ * Read one line with a JS expression, eval() it, and respond with the result:
+ * success <JSON formatted return value>
+ * fail <JSON formatted error>
+ * EOF shuts down the client.
+ */
+process.stdin.setEncoding('utf8');
+
+if (process.env.TEST_CDP_DEBUG)
+ enable_debug = true;
+
+const options = { };
+if (process.argv.length >= 3) {
+ options.port = parseInt(process.argv[2]);
+ if (!options.port) {
+ process.stderr.write("Usage: firefox-cdp-driver.js [port]\n");
+ process.exit(1);
+ }
+}
+
+// HACK: `addScriptToEvaluateOnNewDocument` is not implemented in Firefox
+// thus save all scripts in array and on each new context just execute these
+// scripts in them
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1549465
+function addScriptToEvaluateOnNewDocument(script) { // eslint-disable-line no-unused-vars
+ return new Promise((resolve, reject) => {
+ scriptsOnNewContext.push(script.source);
+ resolve();
+ });
+}
+
+// This should work on different targets (meaning tabs)
+// CDP takes {target:target} so we can pick target
+// Problem is that CDP.New() which creates new target works only for chrome/ium
+// But we should be able to use CPD.List to list all targets and then pick one
+// Firefox just gives them ascending numbers, so we can pick the one with highest number
+// and if we feel fancy we can check that url is `about:newtab`.
+// That still though does not create new tab - but we can just call `firefox about:blank`
+// from cdline and since firefox would open it in the same browser, it should work.
+// This would work just fine in CI (as there would be only one browser) but on our machines it may
+// pick a wrong window (no idea if they can be somehow distinguish and execute it in a specific
+// one). But I guess we can live with it (and it seems it picks the last opened window anyway,
+// so having your own browser running should not interfere)
+//
+// Just calling executable to open another tab in the same browser works also for chromium, so
+// should be fine
+CDP(options)
+ .then(client => {
+ setupLogging(client);
+ setupFrameTracking(client);
+ setupLocalFunctions(client);
+ // TODO: Security handling not yet supported in Firefox
+
+ readline.createInterface(process.stdin)
+ .on('line', command => {
+ // HACKS: See description of related functions
+ if (command.startsWith("client.Page.addScriptToEvaluateOnNewDocument"))
+ command = command.substring(12);
+
+ // run the command
+ const seq = ++cur_cmd_seq;
+ eval(command).then(reply => { // eslint-disable-line no-eval
+ currentExecId = null;
+ if (unhandledExceptions.length === 0) {
+ success(seq, reply);
+ } else {
+ const message = unhandledExceptions[0];
+ fail(seq, message.split("\n")[0]);
+ clearExceptions();
+ }
+ }, err => {
+ currentExecId = null;
+ fail(seq, err);
+ });
+ })
+ .on('close', () => process.exit(0));
+ })
+ .catch(fatal);
diff --git a/test/common/git-utils.sh b/test/common/git-utils.sh
new file mode 100644
index 0000000..dc0767d
--- /dev/null
+++ b/test/common/git-utils.sh
@@ -0,0 +1,150 @@
+# shellcheck shell=sh
+# doesn't do anything on its own. must be sourced.
+
+# The script which sources this script must set the following variables:
+# GITHUB_REPO = the relative repo name of the submodule on github
+# SUBDIR = the location in the working tree where the submodule goes
+# We also expect `set -eu`, but set them ourselves for shellcheck.
+set -eu
+[ -n "${GITHUB_REPO}" ]
+[ -n "${SUBDIR}" ]
+
+# Set by git-rebase for spawned actions
+unset GIT_DIR GIT_EXEC_PATH GIT_PREFIX GIT_REFLOG_ACTION GIT_WORK_TREE
+
+GITHUB_BASE="${GITHUB_BASE:-cockpit-project/cockpit}"
+GITHUB_REPOSITORY="${GITHUB_BASE%/*}/${GITHUB_REPO}"
+HTTPS_REMOTE="https://github.com/${GITHUB_REPOSITORY}"
+# shellcheck disable=SC2034 # used in other scripts
+SSH_REMOTE="git@github.com:${GITHUB_REPOSITORY}"
+
+CACHE_DIR="${XDG_CACHE_HOME-${HOME}/.cache}/cockpit-dev/${GITHUB_REPOSITORY}.git"
+
+if [ "${V-}" = 0 ]; then
+ message() { printf " %-8s %s\n" "$1" "$2" >&2; }
+ quiet='--quiet'
+else
+ message() { :; }
+ quiet=''
+fi
+
+init_cache() {
+ if [ ! -d "${CACHE_DIR}" ]; then
+ message INIT "${CACHE_DIR}"
+ mkdir -p "${CACHE_DIR}"
+ git init --bare --template='' ${quiet} "${CACHE_DIR}"
+ git --git-dir "${CACHE_DIR}" remote add origin "${HTTPS_REMOTE}"
+ fi
+}
+
+# runs a git command on the cache dir
+git_cache() {
+ init_cache
+ git --git-dir "${CACHE_DIR}" "$@"
+}
+
+# reads the named gitlink from the current state of the index
+# returns (ie: prints) a 40-character commit ID
+get_index_gitlink() {
+ if ! git ls-files -s "$1" | grep -E -o '\<[[:xdigit:]]{40}\>'; then
+ echo "*** couldn't read gitlink for file $1 from the index" >&2
+ exit 1
+ fi
+}
+
+# This checks if the given argument "$1" (already) exists in the repository
+# we use git rev-list --objects to to avoid problems with incomplete fetches:
+# we want to make sure the complete commit is there
+check_ref() {
+ git_cache rev-list --quiet --objects "$1" -- 2>/dev/null
+}
+
+# Fetch a specific commit ID into the cache
+# Either we have this commit available locally (in which case this function
+# does nothing), or we need to fetch it. There's no chance that the object
+# changed on the server, because we define it by its checksum.
+fetch_sha_to_cache() {
+ sha="$1"
+
+ # No "offline mode" here: we either have the commit, or we don't
+ if ! check_ref "${sha}"; then
+ message FETCH "${SUBDIR} [ref: ${sha}]"
+ git_cache fetch --no-tags ${quiet} origin "${sha}"
+ # tag it to keep it from being GC'd.
+ git_cache tag "sha-${sha}" "${sha}"
+ fi
+}
+
+# General purpose "fetch" function to be used with tags, refs, or nothing at
+# all (to fetch everything). This checks the server for updates, because all
+# of those things might change at any given time. Supports an "offline" mode
+# to skip the fetch and use the possibly-stale local version, if we have it.
+fetch_to_cache() {
+ # We're fetching a named ref (or all refs), which means:
+ # - we should always do the fetch because it might have changed. but
+ # - we might be able to skip updating in case we already have it
+ if [ -z "${OFFLINE-}" ]; then
+ for retry in $(seq 3); do
+ message FETCH "${SUBDIR} ${1+[ref: $*]}"
+ if git_cache fetch --prune ${quiet} origin "$@"; then
+ return
+ fi
+ sleep $((retry * retry * 5))
+ done
+ echo "repeated git fetch failure, giving up" >&2
+ exit 1
+ fi
+}
+
+# Get the content of "$2" from cache commit "$1"
+cat_from_cache() {
+ git_cache cat-file blob "$1:$2"
+}
+
+# Consistency checking: for a given cache commit "$1", check if it contains a
+# file "$2" which is equal to the file "$3" present in the working tree.
+cmp_from_cache() {
+ cat_from_cache "$1" "$2" | cmp "$3"
+}
+
+# Like `git clone` except that it uses the original origin url and supports
+# checking out commit IDs as detached heads. The target directory must either
+# be empty, or not exist.
+clone_from_cache() {
+ message CLONE "${SUBDIR} [ref: $1]"
+ [ ! -e "${SUBDIR}" ] || rmdir "${SUBDIR}"
+ mkdir "${SUBDIR}"
+ cp -a --reflink=auto "${CACHE_DIR}" "${SUBDIR}/.git"
+ git --git-dir "${SUBDIR}/.git" config --unset core.bare
+ git -c advice.detachedHead=false -C "${SUBDIR}" checkout ${quiet} "$1"
+}
+
+# This stores a .tar file from stdin into the cache as a tree object.
+# Returns the ID. Opposite of `git archive`, basically.
+tar_to_cache() {
+ # Need to do this before we set the GIT_* variables
+ init_cache
+
+ # Use a sub-shell to enable cleanup of the temporary directory
+ (
+ tmpdir="$(mktemp --tmpdir --directory cockpit-tar-to-git.XXXXXX)"
+ # shellcheck disable=SC2064 # we want ${tmpdir} expanded now
+ trap "rm -r '${tmpdir}'" EXIT
+
+ export GIT_INDEX_FILE="${tmpdir}/tmp-index"
+ export GIT_WORK_TREE="${tmpdir}/work"
+
+ mkdir "${GIT_WORK_TREE}"
+ cd "${GIT_WORK_TREE}"
+
+ tar --extract --exclude '.git*'
+ message INDEX "${SUBDIR}"
+ git_cache add --all
+ git_cache write-tree
+ )
+}
+
+ # Small helper to run a git command on the cache directory
+cmd_git() {
+ git_cache "$@"
+}
diff --git a/test/common/lcov.py b/test/common/lcov.py
new file mode 100755
index 0000000..7d2fec5
--- /dev/null
+++ b/test/common/lcov.py
@@ -0,0 +1,505 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+# This module can convert profile data from CDP to LCOV, produce a
+# HTML report, and post review comments.
+#
+# - write_lcov (coverage_data, outlabel)
+# - create_coverage_report()
+
+import glob
+import gzip
+import itertools
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+from bisect import bisect_left
+
+from task import github
+
+BASE_DIR = os.path.realpath(f'{__file__}/../../..')
+
+debug = False
+
+# parse_vlq and parse_sourcemap are based on
+# https://github.com/mattrobenolt/python-sourcemap, licensed with
+# "BSD-2-Clause License"
+
+# Mapping of base64 letter -> integer value.
+B64 = {c: i for i, c in
+ enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+ '0123456789+/')}
+
+
+def parse_vlq(segment):
+ """Parse a string of VLQ-encoded data.
+ Returns:
+ a list of integers.
+ """
+
+ values = []
+
+ cur, shift = 0, 0
+ for c in segment:
+ val = B64[c]
+ # Each character is 6 bits:
+ # 5 of value and the high bit is the continuation.
+ val, cont = val & 0b11111, val >> 5
+ cur += val << shift
+ shift += 5
+
+ if not cont:
+ # The low bit of the unpacked value is the sign.
+ cur, sign = cur >> 1, cur & 1
+ if sign:
+ cur = -cur
+ values.append(cur)
+ cur, shift = 0, 0
+
+ if cur or shift:
+ raise Exception('leftover cur/shift in vlq decode')
+
+ return values
+
+
+def parse_sourcemap(f, line_starts, dir_name):
+ smap = json.load(f)
+ sources = smap['sources']
+ mappings = smap['mappings']
+ lines = mappings.split(';')
+
+ our_map = []
+
+ our_sources = set()
+ for s in sources:
+ if "node_modules" not in s and (s.endswith(('.js', '.jsx'))):
+ our_sources.add(s)
+
+ dst_col, src_id, src_line = 0, 0, 0
+ for dst_line, line in enumerate(lines):
+ segments = line.split(',')
+ dst_col = 0
+ for segment in segments:
+ if not segment:
+ continue
+ parse = parse_vlq(segment)
+ dst_col += parse[0]
+
+ src = None
+ if len(parse) > 1:
+ src_id += parse[1]
+ src = sources[src_id]
+ src_line += parse[2]
+
+ if src in our_sources:
+ norm_src = os.path.normpath(os.path.join(dir_name, src))
+ our_map.append((line_starts[dst_line] + dst_col, norm_src, src_line))
+
+ return our_map
+
+
+class DistFile:
+ def __init__(self, path):
+ line_starts = [0]
+ with open(path, newline='') as f:
+ for line in f.readlines():
+ line_starts.append(line_starts[-1] + len(line))
+ with open(path + ".map") as f:
+ self.smap = parse_sourcemap(f, line_starts, os.path.relpath(os.path.dirname(path), BASE_DIR))
+
+ def find_sources_slow(self, start, end):
+ res = []
+ for m in self.smap:
+ if m[0] >= start and m[0] < end:
+ res.append(m)
+ return res
+
+ def find_sources(self, start, end):
+ res = []
+ i = bisect_left(self.smap, start, key=lambda m: m[0])
+ while i < len(self.smap) and self.smap[i][0] < end:
+ res.append(self.smap[i])
+ i += 1
+ if debug and res != self.find_sources_slow(start, end):
+ raise RuntimeError("Bug in find_sources")
+ return res
+
+
+def get_dist_map(package):
+ dmap = {}
+ for manifest_json in glob.glob(f"{BASE_DIR}/dist/*/manifest.json") + glob.glob(f"{BASE_DIR}/dist/manifest.json"):
+ with open(manifest_json) as f:
+ m = json.load(f)
+ if "name" in m:
+ dmap[m["name"]] = os.path.dirname(manifest_json)
+ elif manifest_json == f"{BASE_DIR}/dist/manifest.json":
+ if "name" in package:
+ dmap[package["name"]] = os.path.dirname(manifest_json)
+ return dmap
+
+
+def get_distfile(url, dist_map):
+ parts = url.split("/")
+ if len(parts) < 3 or "cockpit" not in parts:
+ return None
+
+ base = parts[-2]
+ file = parts[-1]
+ if file == "manifests.js":
+ return None
+ if base in dist_map:
+ path = dist_map[base] + "/" + file
+ else:
+ path = f"{BASE_DIR}/dist/" + base + "/" + file
+ if os.path.exists(path) and os.path.exists(path + ".map"):
+ return DistFile(path)
+ else:
+ sys.stderr.write(f"SKIP {url} -> {path}\n")
+ return None
+
+
+def grow_array(arr, size, val):
+ if len(arr) < size:
+ arr.extend([val] * (size - len(arr)))
+
+
+def record_covered(file_hits, src, line, hits):
+ if src in file_hits:
+ line_hits = file_hits[src]
+ else:
+ line_hits = []
+ grow_array(line_hits, line + 1, None)
+ line_hits[line] = hits
+ file_hits[src] = line_hits
+
+
+def record_range(file_hits, r, distfile):
+ sources = distfile.find_sources(r['startOffset'], r['endOffset'])
+ for src in sources:
+ record_covered(file_hits, src[1], src[2], r['count'])
+
+
+def merge_hits(file_hits, hits):
+ for src in hits:
+ if src not in file_hits:
+ file_hits[src] = hits[src]
+ else:
+ lines = file_hits[src]
+ merge_lines = hits[src]
+ grow_array(lines, len(merge_lines), None)
+ for i in range(len(merge_lines)):
+ if lines[i] is None:
+ lines[i] = merge_lines[i]
+ elif merge_lines[i] is not None:
+ lines[i] += merge_lines[i]
+
+
+def print_file_coverage(path, line_hits, out):
+ lines_found = 0
+ lines_hit = 0
+ src = f"{BASE_DIR}/{path}"
+ out.write(f"SF:{src}\n")
+ for i in range(len(line_hits)):
+ if line_hits[i] is not None:
+ lines_found += 1
+ out.write(f"DA:{i + 1},{line_hits[i]}\n")
+ if line_hits[i] > 0:
+ lines_hit += 1
+ out.write(f"LH:{lines_hit}\n")
+ out.write(f"LF:{lines_found}\n")
+ out.write("end_of_record\n")
+
+
+class DiffMap:
+ # Parse a unified diff and make a index for the added lines
+ def __init__(self, diff):
+ self.map = {}
+ self.source_map = {}
+ plus_name = None
+ diff_line = 0
+ with open(diff) as f:
+ for line in f.readlines():
+ diff_line += 1
+ if line.startswith("+++ /dev/null"):
+ # removed file, only `^-` following after that until the next hunk
+ continue
+ elif line.startswith("+++ b/"):
+ plus_name = os.path.normpath(line[6:].strip())
+ plus_line = 1
+ self.map[plus_name] = {}
+ elif line.startswith("@@ "):
+ plus_line = int(line.split(" ")[2].split(",")[0])
+ elif line.startswith(" "):
+ plus_line += 1
+ elif line.startswith("+"):
+ self.map[plus_name][plus_line] = diff_line
+ self.source_map[diff_line] = (plus_name, plus_line, line[1:])
+ plus_line += 1
+
+ def find_line(self, file, line):
+ if file in self.map and line in self.map[file]:
+ return self.map[file][line]
+ return None
+
+ def find_source(self, diff_line):
+ return self.source_map.get(diff_line)
+
+
+def print_diff_coverage(path, file_hits, out):
+ if not os.path.exists(path):
+ return
+ dm = DiffMap(path)
+ src = f"{BASE_DIR}/{path}"
+ lines_found = 0
+ lines_hit = 0
+ out.write(f"SF:{src}\n")
+ for f in file_hits:
+ line_hits = file_hits[f]
+ for i in range(len(line_hits)):
+ if line_hits[i] is not None:
+ diff_line = dm.find_line(f, i + 1)
+ if diff_line:
+ lines_found += 1
+ out.write(f"DA:{diff_line},{line_hits[i]}\n")
+ if line_hits[i] > 0:
+ lines_hit += 1
+ out.write(f"LH:{lines_hit}\n")
+ out.write(f"LF:{lines_found}\n")
+ out.write("end_of_record\n")
+
+
+def write_lcov(covdata, outlabel):
+
+ with open(f"{BASE_DIR}/package.json") as f:
+ package = json.load(f)
+ dist_map = get_dist_map(package)
+ file_hits = {}
+
+ def covranges(functions):
+ for f in functions:
+ for r in f['ranges']:
+ yield r
+
+ # Coverage data is reported as a "count" value for a range of
+ # text. These ranges overlap when functions are nested. For
+ # example, take this source code:
+ #
+ # 1 . function foo(x) {
+ # 2 . function bar() {
+ # 3 . }
+ # 4 . if (x)
+ # 5 . bar();
+ # 6 . }
+ # 7 .
+ # 8 . foo(0)
+ #
+ # There will be a range with count 1 for the whole source code
+ # (lines 1 to 8) since all code is executed when loading a file.
+ # Then there will be a range with count 1 for "foo" (lines 1 to 6)
+ # since it is called from the top-level, and there will be a range
+ # with count 0 for "bar" (lines 2 and 3), since it is never
+ # actually called. If block-level precision has been enabled
+ # while collecting the coverage data, there will also be a range
+ # with count 0 for line 5, since that branch if the "if" is not
+ # executed.
+ #
+ # We process ranges like this in order, from longest to shortest,
+ # and record their counts for each line they cover. The count of a
+ # range that is processed later will overwrite any count that has
+ # been recorded earlier. This makes the count correct for nested
+ # functions since they are processed last.
+ #
+ # In the example, first lines 1 to 8 are set to count 1, then
+ # lines 1 to 6 are set to count 1 again, then lines 2 and 3 are
+ # set to count 0, and finally line 5 is also set to count 0:
+ #
+ # 1 1 function foo(x) {
+ # 2 0 function bar() {
+ # 3 0 }
+ # 4 1 if (x)
+ # 5 0 bar();
+ # 6 1 }
+ # 7 1
+ # 8 1 foo(0)
+ #
+ # Thus, when processing ranges for a single file, we must
+ # prioritize the counts of smaller ranges over larger ones, and
+ # can't just add them all up. This doesn't work, however, when
+ # something like webpack is involved, and a source file is copied
+ # into multiple files in "dist/".
+ #
+ # The coverage data contains ranges for all files that are loaded
+ # into the browser during the whole session, such as when
+ # transitioning from the login page to the shell, and when loading
+ # multiple iframes for the individual pages.
+ #
+ # For example, if both shell.js (loaded at the top-level) and
+ # overview.js (loaded into an iframe) include lib/button.js, then
+ # the coverage data might report that shell.js does execute line 5
+ # of lib/button.js and also that overview.js does not execute it.
+ # We need to add the counts up for line 5 so that the combined
+ # report says that is has been executed.
+ #
+ # The same applies to reloading and navigating in the browser. If
+ # a page is reloaded, there will be separate coverage reports for
+ # its files. For example, if a reload happens, shell.js will be
+ # mentioned twice in the report, and we need to add up the counts
+ # from each mention.
+
+ for script in covdata:
+ distfile = get_distfile(script['url'], dist_map)
+ if distfile:
+ ranges = sorted(covranges(script['functions']),
+ key=lambda r: r['endOffset'] - r['startOffset'], reverse=True)
+ hits = {}
+ for r in ranges:
+ record_range(hits, r, distfile)
+ merge_hits(file_hits, hits)
+
+ if len(file_hits) > 0:
+ os.makedirs(f"{BASE_DIR}/lcov", exist_ok=True)
+ filename = f"{BASE_DIR}/lcov/{outlabel}.info.gz"
+ with gzip.open(filename, "wt") as out:
+ for f in file_hits:
+ print_file_coverage(f, file_hits[f], out)
+ print_diff_coverage("lcov/github-pr.diff", file_hits, out)
+ print("Wrote coverage data to " + filename)
+
+
+def get_review_comments(diff_info_file):
+ comments = []
+ cur_src = None
+ start_line = None
+ cur_line = None
+
+ def is_interesting_line(text):
+ # Don't complain when being told to shut up
+ if "// not-covered: " in text:
+ return False
+ # Don't complain about lines that contain only punctuation, or
+ # nothing but "else". We don't seem to get reliable
+ # information for them.
+ if not re.search('[a-zA-Z0-9]', text.replace("else", "")):
+ return False
+ return True
+
+ def flush_cur_comment():
+ nonlocal comments
+ if cur_src:
+ ta_url = os.environ.get("TEST_ATTACHMENTS_URL", None)
+ comment = {"path": cur_src,
+ "line": cur_line}
+ if start_line != cur_line:
+ comment["start_line"] = start_line
+ body = f"These {cur_line - start_line + 1} added lines are not executed by any test."
+ else:
+ body = "This added line is not executed by any test."
+ if ta_url:
+ body += f" [Details]({ta_url}/Coverage/lcov/github-pr.diff.gcov.html)"
+ comment["body"] = body
+ comments.append(comment)
+
+ dm = DiffMap("lcov/github-pr.diff")
+
+ with open(diff_info_file) as f:
+ for line in f.readlines():
+ if line.startswith("DA:"):
+ parts = line[3:].split(",")
+ if int(parts[1]) == 0:
+ info = dm.find_source(int(parts[0]))
+ if not info:
+ continue
+ (src, line, text) = info
+ if not is_interesting_line(text):
+ continue
+ if src == cur_src and line == cur_line + 1:
+ cur_line = line
+ else:
+ flush_cur_comment()
+ cur_src = src
+ start_line = line
+ cur_line = line
+ flush_cur_comment()
+
+ return comments
+
+
+def prepare_for_code_coverage():
+ # This gives us a convenient link at the top of the logs, see link-patterns.json
+ print("Code coverage report in Coverage/index.html")
+ if os.path.exists("lcov"):
+ shutil.rmtree("lcov")
+ os.makedirs("lcov")
+ # Detect the default branch to compare with, Anaconda still uses master as main.
+ branch = "main"
+ try:
+ subprocess.check_call(["git", "rev-parse", "--quiet", "--verify", branch], stdout=subprocess.DEVNULL)
+ except subprocess.SubprocessError:
+ branch = "master"
+ with open("lcov/github-pr.diff", "w") as f:
+ subprocess.check_call(["git", "-c", "diff.noprefix=false", "diff", "--patience", branch], stdout=f)
+
+
+def create_coverage_report():
+ output = os.environ.get("TEST_ATTACHMENTS", BASE_DIR)
+ lcov_files = glob.glob(f"{BASE_DIR}/lcov/*.info.gz")
+ try:
+ title = os.path.basename(subprocess.check_output(["git", "remote", "get-url", "origin"])).decode().strip()
+ except subprocess.CalledProcessError:
+ title = "?"
+ if len(lcov_files) > 0:
+ all_file = f"{BASE_DIR}/lcov/all.info"
+ diff_file = f"{BASE_DIR}/lcov/diff.info"
+ excludes = []
+ # Exclude pkg/lib in Cockpit projects such as podman/machines.
+ if title != "cockpit.git":
+ excludes = ["--exclude", "pkg/lib"]
+ subprocess.check_call(["lcov", "--quiet", "--output", all_file, *excludes,
+ *itertools.chain(*[["--add", f] for f in lcov_files])])
+ subprocess.check_call(["lcov", "--quiet", "--ignore-errors", "empty,empty,unused,unused", "--output", diff_file,
+ "--extract", all_file, "*/github-pr.diff"])
+ summary = subprocess.check_output(["genhtml", "--no-function-coverage",
+ "--prefix", os.getcwd(),
+ "--title", title,
+ "--output-dir", f"{output}/Coverage", all_file]).decode()
+
+ coverage = summary.split("\n")[-2]
+ match = re.search(r".*lines\.*:\s*([\d\.]*%).*", coverage)
+ if match:
+ print("Overall line coverage:", match.group(1))
+
+ comments = get_review_comments(diff_file)
+ rev = os.environ.get("TEST_REVISION", None)
+ pull = os.environ.get("TEST_PULL", None)
+ if rev and pull:
+ api = github.GitHub()
+ old_comments = api.get(f"pulls/{pull}/comments?sort=created&direction=desc&per_page=100") or []
+ for oc in old_comments:
+ if ("body" in oc and "path" in oc and "line" in oc and
+ "not executed by any test." in oc["body"]):
+ api.delete(f"pulls/comments/{oc['id']}")
+ if len(comments) > 0:
+ api.post(f"pulls/{pull}/reviews",
+ {"commit_id": rev, "event": "COMMENT",
+ "comments": comments})
+ else:
+ sys.stderr.write("Error: no code coverage files generated\n")
diff --git a/test/common/link-patterns.json b/test/common/link-patterns.json
new file mode 100644
index 0000000..cef6c78
--- /dev/null
+++ b/test/common/link-patterns.json
@@ -0,0 +1,39 @@
+[
+ {
+ "label": "screenshot",
+ "pattern": "Wrote screenshot to ([A-Za-z0-9.-]+.png)$",
+ "url": "$1",
+ "icon": "bi bi-camera-fill"
+ },
+ {
+ "label": "new pixels",
+ "pattern": "New pixel test reference ([A-Za-z0-9.-]+.png)$",
+ "url": "$1"
+ },
+ {
+ "label": "journal",
+ "pattern": "Journal extracted to ([A-Za-z0-9.-]+.log(?:.[gx]z)?)$",
+ "url": "$1",
+ "icon": "bi bi-card-text"
+ },
+ {
+ "label": "changed pixels",
+ "pattern": "Differences in pixel test ([A-Za-z0-9.-]+)$",
+ "url": "pixeldiff.html#$1"
+ },
+ {
+ "label": "coverage",
+ "pattern": "Code coverage report in ([A-Za-z0-9.-]+)$",
+ "url": "$1/"
+ },
+ {
+ "label": "vm xml",
+ "pattern": "Wrote ([A-Za-z0-9.-]+) XML to ([A-Za-z0-9.-]+.xml)$",
+ "url": "$2"
+ },
+ {
+ "label": "vm log",
+ "pattern": "Wrote ([A-Za-z0-9.-]+) log to ([A-Za-z0-9.-]+.log)$",
+ "url": "$2"
+ }
+]
diff --git a/test/common/make-bots b/test/common/make-bots
new file mode 100755
index 0000000..5160e90
--- /dev/null
+++ b/test/common/make-bots
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+# Prepare bots by creating ./bots directory
+# Specify $COCKPIT_BOTS_REF to checkout non-main branch
+
+GITHUB_REPO='bots'
+SUBDIR='bots'
+
+V="${V-0}" # default to friendly messages
+
+set -eu
+cd "${0%/*}/../.."
+# shellcheck source-path=SCRIPTDIR/../..
+. test/common/git-utils.sh
+
+if [ ! -e bots ]; then
+ [ -n "${quiet}" ] || set -x
+ if [ -h ~/.config/cockpit-dev/bots ]; then
+ message SYMLINK "bots → $(realpath --relative-to=. ~/.config/cockpit-dev/bots)"
+ ln -sfT "$(realpath --relative-to=. ~/.config/cockpit-dev)/bots" bots
+ else
+ # it's small, so keep everything cached
+ fetch_to_cache ${COCKPIT_BOTS_REF+"${COCKPIT_BOTS_REF}"}
+ clone_from_cache "${COCKPIT_BOTS_REF-main}"
+ fi
+else
+ echo "bots/ already exists, skipping"
+fi
diff --git a/test/common/netlib.py b/test/common/netlib.py
new file mode 100644
index 0000000..e9e868f
--- /dev/null
+++ b/test/common/netlib.py
@@ -0,0 +1,214 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import subprocess
+
+from testlib import Error, MachineCase, wait
+
+
+class NetworkHelpers:
+ """Mix-in class for tests that require network setup"""
+
+ def add_veth(self, name, dhcp_cidr=None, dhcp_range=None):
+ """Add a veth device that is manageable with NetworkManager
+
+ This is safe for @nondestructive tests, the interface gets cleaned up automatically.
+ """
+ if dhcp_range is None:
+ dhcp_range = ['10.111.112.2', '10.111.127.254']
+ self.machine.execute(r"""
+ mkdir -p /run/udev/rules.d/
+ echo 'ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="%(name)s", ENV{NM_UNMANAGED}="0"' > /run/udev/rules.d/99-nm-veth-%(name)s-test.rules
+ udevadm control --reload
+ ip link add name %(name)s type veth peer name v_%(name)s
+ # Trigger udev to make sure that it has been renamed to its final name
+ udevadm trigger --subsystem-match=net
+ udevadm settle
+ """ % {"name": name})
+ self.addCleanup(self.machine.execute, f"rm /run/udev/rules.d/99-nm-veth-{name}-test.rules; ip link del dev {name}")
+ if dhcp_cidr:
+ # up the remote end, give it an IP, and start DHCP server
+ self.machine.execute(f"ip a add {dhcp_cidr} dev v_{name}; ip link set v_{name} up")
+ server = self.machine.spawn("dnsmasq --keep-in-foreground --log-queries --log-facility=- "
+ f"--conf-file=/dev/null --dhcp-leasefile=/tmp/leases.{name} --no-resolv "
+ f"--bind-interfaces --except-interface=lo --interface=v_{name} --dhcp-range={dhcp_range[0]},{dhcp_range[1]},4h",
+ f"dhcp-{name}.log")
+ self.addCleanup(self.machine.execute, "kill %i" % server)
+ self.machine.execute("if firewall-cmd --state >/dev/null 2>&1; then firewall-cmd --add-service=dhcp; fi")
+
+ def nm_activate_eth(self, iface):
+ """Create an NM connection for a given interface"""
+
+ m = self.machine
+ wait(lambda: m.execute(f'nmcli device | grep "{iface}.*disconnected"'))
+ m.execute(f"nmcli con add type ethernet ifname {iface} con-name {iface}")
+ m.execute(f"nmcli con up {iface} ifname {iface}")
+ self.addCleanup(m.execute, f"nmcli con delete {iface}")
+
+ def nm_checkpoints_disable(self):
+ self.browser.eval_js("window.cockpit_tests_disable_checkpoints = true;")
+
+ def nm_checkpoints_enable(self, settle_time=3.0):
+ self.browser.eval_js("window.cockpit_tests_disable_checkpoints = false;")
+ self.browser.eval_js(f"window.cockpit_tests_checkpoint_settle_time = {settle_time};")
+
+
+class NetworkCase(MachineCase, NetworkHelpers):
+ def setUp(self):
+ super().setUp()
+
+ m = self.machine
+
+ # clean up after nondestructive tests
+ if self.is_nondestructive():
+ def devs():
+ return set(self.machine.execute("ls /sys/class/net/ | grep -v bonding_masters").strip().split())
+
+ def cleanupDevs():
+ new = devs() - self.orig_devs
+ self.machine.execute(f"for d in {' '.join(new)}; do nmcli dev del $d; done")
+
+ self.orig_devs = devs()
+ self.restore_dir("/etc/NetworkManager", restart_unit="NetworkManager")
+ self.restore_dir("/etc/sysconfig/network-scripts")
+ self.restore_dir("/etc/netplan")
+ self.restore_dir("/run/NetworkManager/system-connections")
+ self.addCleanup(cleanupDevs)
+
+ m.execute("systemctl start NetworkManager")
+
+ # Ensure a clean and consistent state. We remove rogue
+ # connections that might still be here from the time of
+ # creating the image and we prevent NM from automatically
+ # creating new connections.
+ # if the command fails, try again
+ failures_allowed = 3
+ while True:
+ try:
+ print(m.execute("nmcli con show"))
+ m.execute(
+ """nmcli -f UUID,DEVICE connection show | awk '$2 == "--" { print $1 }' | xargs -r nmcli con del""")
+ break
+ except subprocess.CalledProcessError:
+ failures_allowed -= 1
+ if failures_allowed == 0:
+ raise
+
+ m.write("/etc/NetworkManager/conf.d/99-test.conf", "[main]\nno-auto-default=*\n")
+ m.execute("systemctl reload-or-restart NetworkManager")
+
+ # our assertions and pixel tests assume that virbr0 is absent
+ m.execute('[ -z "$(systemctl --legend=false list-unit-files libvirtd.service)" ] || '
+ 'systemctl try-restart libvirtd.service')
+ if 'default' in m.execute("virsh net-list --name || true"):
+ m.execute("virsh net-autostart --disable default; virsh net-destroy default")
+
+ ver = self.machine.execute(
+ "busctl --system get-property org.freedesktop.NetworkManager /org/freedesktop/NetworkManager org.freedesktop.NetworkManager Version || true")
+ ver_match = re.match('s "(.*)"', ver)
+ if ver_match:
+ self.networkmanager_version = [int(x) for x in ver_match.group(1).split(".")]
+ else:
+ self.networkmanager_version = [0]
+
+ # Something unknown sometimes goes wrong with PCP, see #15625
+ self.allow_journal_messages("pcp-archive: no such metric: network.interface.* Unknown metric name",
+ "direct: instance name lookup failed: network.*")
+
+ def get_iface(self, m, mac):
+ def getit():
+ path = m.execute(f"grep -li '{mac}' /sys/class/net/*/address")
+ return path.split("/")[-2]
+ iface = wait(getit).strip()
+ print(f"{mac} -> {iface}")
+ return iface
+
+ def add_iface(self, activate=True):
+ m = self.machine
+ mac = m.add_netiface(networking=self.network.interface())
+ # Wait for the interface to show up
+ self.get_iface(m, mac)
+ # Trigger udev to make sure that it has been renamed to its final name
+ m.execute("udevadm trigger; udevadm settle")
+ iface = self.get_iface(m, mac)
+ if activate:
+ self.nm_activate_eth(iface)
+ return iface
+
+ def wait_for_iface(self, iface, active=True, state=None, prefix="10.111."):
+ sel = f"#networking-interfaces tr[data-interface='{iface}']"
+
+ if state:
+ text = state
+ elif active:
+ text = prefix
+ else:
+ text = "Inactive"
+
+ try:
+ with self.browser.wait_timeout(30):
+ self.browser.wait_in_text(sel, text)
+ except Error as e:
+ print(f"Interface {iface} didn't show up.")
+ print(self.machine.execute(f"grep . /sys/class/net/*/address; nmcli con; nmcli dev; nmcli dev show {iface} || true"))
+ raise e
+
+ def select_iface(self, iface):
+ b = self.browser
+ b.click(f"#networking-interfaces tr[data-interface='{iface}'] button")
+
+ def iface_con_id(self, iface):
+ con_id = self.machine.execute(f"nmcli -m tabular -t -f GENERAL.CONNECTION device show {iface}").strip()
+ if con_id == "" or con_id == "--":
+ return None
+ else:
+ return con_id
+
+ def wait_for_iface_setting(self, setting_title, setting_value):
+ b = self.browser
+ b.wait_in_text(f"dt:contains('{setting_title}') + dd", setting_value)
+
+ def configure_iface_setting(self, setting_title):
+ b = self.browser
+ b.click(f"dt:contains('{setting_title}') + dd button")
+
+ def ensure_nm_uses_dhclient(self):
+ m = self.machine
+ m.write("/etc/NetworkManager/conf.d/99-dhcp.conf", "[main]\ndhcp=dhclient\n")
+ m.execute("systemctl restart NetworkManager")
+
+ def slow_down_dhclient(self, delay):
+ self.machine.execute(f"""
+ mkdir -p {self.vm_tmpdir}
+ cp -a /usr/sbin/dhclient {self.vm_tmpdir}/dhclient.real
+ printf '#!/bin/sh\\nsleep {delay}\\nexec {self.vm_tmpdir}/dhclient.real "$@"' > {self.vm_tmpdir}/dhclient
+ chmod a+x {self.vm_tmpdir}/dhclient
+ if selinuxenabled 2>&1; then chcon --reference /usr/sbin/dhclient {self.vm_tmpdir}/dhclient; fi
+ mount -o bind {self.vm_tmpdir}/dhclient /usr/sbin/dhclient
+ """)
+ self.addCleanup(self.machine.execute, "umount /usr/sbin/dhclient")
+
+ def wait_onoff(self, sel, val):
+ self.browser.wait_visible(sel + " input[type=checkbox]" + (":checked" if val else ":not(:checked)"))
+
+ def toggle_onoff(self, sel):
+ self.browser.click(sel + " input[type=checkbox]")
+
+ def login_and_go(self, *args, **kwargs):
+ super().login_and_go(*args, **kwargs)
+ self.nm_checkpoints_disable()
diff --git a/test/common/packagelib.py b/test/common/packagelib.py
new file mode 100644
index 0000000..ed2fe89
--- /dev/null
+++ b/test/common/packagelib.py
@@ -0,0 +1,428 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+import os
+import textwrap
+
+from testlib import MachineCase
+
+
+class PackageCase(MachineCase):
+ def setUp(self):
+ super().setUp()
+
+ self.repo_dir = os.path.join(self.vm_tmpdir, "repo")
+
+ if self.machine.ostree_image:
+ logging.warning("PackageCase: OSTree images can't install additional packages")
+ return
+
+ # expected backend; hardcode this on image names to check the auto-detection
+ if self.machine.image.startswith("debian") or self.machine.image.startswith("ubuntu"):
+ self.backend = "apt"
+ self.primary_arch = "all"
+ self.secondary_arch = "amd64"
+ elif self.machine.image.startswith("fedora") or self.machine.image.startswith("rhel-") or self.machine.image.startswith("centos-"):
+ self.backend = "dnf"
+ self.primary_arch = "noarch"
+ self.secondary_arch = "x86_64"
+ elif self.machine.image == "arch":
+ self.backend = "alpm"
+ self.primary_arch = "any"
+ self.secondary_arch = "x86_64"
+ else:
+ raise NotImplementedError("unknown image " + self.machine.image)
+
+ if "debian" in self.image or "ubuntu" in self.image:
+ # PackageKit refuses to work when offline, and main interface is not managed by NM on these images
+ self.machine.execute("nmcli con add type dummy con-name fake ifname fake0 ip4 1.2.3.4/24 gw4 1.2.3.1")
+ self.addCleanup(self.machine.execute, "nmcli con delete fake")
+
+ # HACK: packagekit often hangs on shutdown; https://bugzilla.redhat.com/show_bug.cgi?id=1717185
+ self.write_file("/etc/systemd/system/packagekit.service.d/timeout.conf", "[Service]\nTimeoutStopSec=5\n")
+ self.addCleanup(self.machine.execute, "systemctl stop packagekit; systemctl reset-failed packagekit || true")
+
+ # disable all existing repositories to avoid hitting the network
+ if self.backend == "apt":
+ self.restore_dir("/var/lib/apt", reboot_safe=True)
+ self.restore_dir("/var/cache/apt", reboot_safe=True)
+ self.restore_dir("/etc/apt", reboot_safe=True)
+ self.machine.execute("echo > /etc/apt/sources.list; rm -f /etc/apt/sources.list.d/*; apt-get clean; apt-get update")
+ elif self.backend == "alpm":
+ self.restore_dir("/var/lib/pacman", reboot_safe=True)
+ self.restore_dir("/var/cache/pacman", reboot_safe=True)
+ self.restore_dir("/etc/pacman.d", reboot_safe=True)
+ self.restore_dir("/var/lib/PackageKit/alpm", reboot_safe=True)
+ self.restore_file("/etc/pacman.conf")
+ self.restore_file("/etc/pacman.d/mirrorlist")
+ self.restore_file("/usr/share/libalpm/hooks/90-packagekit-refresh.hook")
+
+ self.machine.execute("rm /etc/pacman.conf /etc/pacman.d/mirrorlist /var/lib/pacman/sync/* /usr/share/libalpm/hooks/90-packagekit-refresh.hook")
+ self.machine.execute("test -d /var/lib/PackageKit/alpm && rm -r /var/lib/PackageKit/alpm || true") # Drop alpm state directory as it interferes with running offline
+ # Initial config for installation
+ empty_repo_dir = '/var/lib/cockpittest/empty'
+ config = f"""
+[options]
+Architecture = auto
+HoldPkg = pacman glibc
+
+[empty]
+SigLevel = Never
+Server = file://{empty_repo_dir}
+"""
+ # HACK: Setup empty repo for packagekit
+ self.machine.execute(f"mkdir -p {empty_repo_dir} || true")
+ self.machine.execute(f"repo-add {empty_repo_dir}/empty.db.tar.gz")
+ self.machine.write("/etc/pacman.conf", config)
+ # Clean up possible leftover lockfile
+ self.machine.execute("""
+ if [ -f /var/lib/pacman/db.lck ]; then
+ fuser -k /var/lib/pacman/db.lck || true;
+ rm /var/lib/pacman/db.lck;
+ fi
+ """)
+ self.machine.execute("pacman -Sy")
+ else:
+ self.restore_dir("/etc/yum.repos.d", reboot_safe=True)
+ self.restore_dir("/var/cache/dnf", reboot_safe=True)
+ self.machine.execute("rm -rf /etc/yum.repos.d/* /var/cache/dnf/*")
+
+ # have PackageKit start from a clean slate
+ self.machine.execute("systemctl stop packagekit")
+ self.machine.execute("systemctl kill --signal=SIGKILL packagekit || true; rm -rf /var/cache/PackageKit")
+ self.machine.execute("systemctl reset-failed packagekit || true")
+ self.restore_file("/var/lib/PackageKit/transactions.db")
+
+ if self.image in ["debian-stable", "debian-testing"]:
+ # PackageKit tries to resolve some DNS names, but our test VM is offline; temporarily disable the name server to fail quickly
+ self.machine.execute("mv /etc/resolv.conf /etc/resolv.conf.test")
+ self.addCleanup(self.machine.execute, "mv /etc/resolv.conf.test /etc/resolv.conf")
+
+ # reset automatic updates
+ if self.backend == 'dnf':
+ self.machine.execute("systemctl disable --now dnf-automatic dnf-automatic-install "
+ "dnf-automatic.service dnf-automatic-install.timer")
+ self.machine.execute("rm -r /etc/systemd/system/dnf-automatic* && systemctl daemon-reload || true")
+
+ self.updateInfo = {}
+
+ # HACK: kpatch check sometimes complains that we don't set up a full repo in unrelated tests
+ self.allow_browser_errors("Could not determine kpatch packages:.*repodata updates was not complete")
+
+ #
+ # Helper functions for creating packages/repository
+ #
+
+ def createPackage(self, name, version, release, install=False,
+ postinst=None, depends="", content=None, arch=None, provides=None, **updateinfo):
+ """Create a dummy package in repo_dir on self.machine
+
+ If install is True, install the package. Otherwise, update the package
+ index in repo_dir.
+ """
+ if provides:
+ provides = f"Provides: {provides}"
+ else:
+ provides = ""
+
+ if self.backend == "apt":
+ self.createDeb(name, version + '-' + release, depends, postinst, install, content, arch, provides)
+ elif self.backend == "alpm":
+ self.createPacmanPkg(name, version, release, depends, postinst, install, content, arch, provides)
+ else:
+ self.createRpm(name, version, release, depends, postinst, install, content, arch, provides)
+ if updateinfo:
+ self.updateInfo[(name, version, release)] = updateinfo
+
+ def createDeb(self, name, version, depends, postinst, install, content, arch, provides):
+ """Create a dummy deb in repo_dir on self.machine
+
+ If install is True, install the package. Otherwise, update the package
+ index in repo_dir.
+ """
+ m = self.machine
+
+ if arch is None:
+ arch = self.primary_arch
+ deb = f"{self.repo_dir}/{name}_{version}_{arch}.deb"
+ if postinst:
+ postinstcode = f"printf '#!/bin/sh\n{postinst}' > /tmp/b/DEBIAN/postinst; chmod 755 /tmp/b/DEBIAN/postinst"
+ else:
+ postinstcode = ''
+ if content is not None:
+ for path, data in content.items():
+ dest = "/tmp/b/" + path
+ m.execute(f"mkdir -p '{os.path.dirname(dest)}'")
+ if isinstance(data, dict):
+ m.execute(f"cp '{data['path']}' '{dest}'")
+ else:
+ m.write(dest, data)
+ m.execute(f"mkdir -p {self.repo_dir}")
+ m.write("/tmp/b/DEBIAN/control", textwrap.dedent(f"""
+ Package: {name}
+ Version: {version}
+ Priority: optional
+ Section: test
+ Maintainer: foo
+ Depends: {depends}
+ Architecture: {arch}
+ Description: dummy {name}
+ {provides}
+ """))
+
+ cmd = f"""set -e
+ {postinstcode}
+ touch /tmp/b/stamp-{name}-{version}
+ dpkg -b /tmp/b {deb}
+ rm -r /tmp/b
+ """
+ if install:
+ cmd += "dpkg -i " + deb
+ m.execute(cmd)
+ self.addCleanup(m.execute, f"dpkg -P --force-depends --force-remove-reinstreq {name} 2>/dev/null || true")
+
+ def createRpm(self, name, version, release, requires, post, install, content, arch, provides):
+ """Create a dummy rpm in repo_dir on self.machine
+
+ If install is True, install the package. Otherwise, update the package
+ index in repo_dir.
+ """
+ if post:
+ postcode = '\n%%post\n' + post
+ else:
+ postcode = ''
+ if requires:
+ requires = f"Requires: {requires}\n"
+ if arch is None:
+ arch = self.primary_arch
+ installcmds = f"touch $RPM_BUILD_ROOT/stamp-{name}-{version}-{release}\n"
+ installedfiles = f"/stamp-{name}-{version}-{release}\n"
+ if content is not None:
+ for path, data in content.items():
+ installcmds += f'mkdir -p $(dirname "$RPM_BUILD_ROOT/{path}")\n'
+ if isinstance(data, dict):
+ installcmds += f"cp {data['path']} \"$RPM_BUILD_ROOT/{path}\""
+ else:
+ installcmds += f'cat >"$RPM_BUILD_ROOT/{path}" <<\'EOF\'\n' + data + '\nEOF\n'
+ installedfiles += f"{path}\n"
+
+ architecture = ""
+ if arch == self.primary_arch:
+ architecture = f"BuildArch: {self.primary_arch}"
+ spec = f"""
+Summary: dummy {name}
+Name: {name}
+Version: {version}
+Release: {release}
+License: BSD
+{provides}
+{architecture}
+{requires}
+
+%%install
+{installcmds}
+
+%%description
+Test package.
+
+%%files
+{installedfiles}
+
+{postcode}
+"""
+ self.machine.write("/tmp/spec", spec)
+ cmd = """
+rpmbuild --quiet -bb /tmp/spec
+mkdir -p {0}
+cp ~/rpmbuild/RPMS/{4}/*.rpm {0}
+rm -rf ~/rpmbuild
+"""
+ if install:
+ cmd += "rpm -i {0}/{1}-{2}-{3}.*.rpm"
+ self.machine.execute(cmd.format(self.repo_dir, name, version, release, arch))
+ self.addCleanup(self.machine.execute, f"rpm -e --nodeps {name} 2>/dev/null || true")
+
+ def createPacmanPkg(self, name, version, release, requires, postinst, install, content, arch, provides):
+ """Create a dummy pacman package in repo_dir on self.machine
+
+ If install is True, install the package. Otherwise, update the package
+ index in repo_dir.
+ """
+
+ if arch is None:
+ arch = 'any'
+
+ sources = ""
+ installcmds = 'package() {\n'
+ if content is not None:
+ sources = "source=("
+ files = 0
+ for path, data in content.items():
+ p = os.path.dirname(path)
+ installcmds += f'mkdir -p $pkgdir{p}\n'
+ if isinstance(data, dict):
+ dpath = data["path"]
+
+ file = os.path.basename(dpath)
+ sources += file
+ files += 1
+ # TODO: hardcoded /tmp
+ self.machine.execute(f'cp {data["path"]} /tmp/{file}')
+ installcmds += f'cp {file} $pkgdir{path}\n'
+ else:
+ installcmds += f'cat >"$pkgdir{path}" <<\'EOF\'\n' + data + '\nEOF\n'
+
+ sources += ")"
+
+ # Always stamp a file
+ installcmds += f"touch $pkgdir/stamp-{name}-{version}-{release}\n"
+ installcmds += '}'
+
+ pkgbuild = f"""
+pkgname={name}
+pkgver={version}
+pkgdesc="dummy {name}"
+pkgrel={release}
+arch=({arch})
+depends=({requires})
+{sources}
+
+{installcmds}
+"""
+
+ if postinst:
+ postinstcode = f"""
+post_install() {{
+ {postinst}
+}}
+
+post_upgrade() {{
+ post_install $*
+}}
+"""
+ self.machine.write(f"/tmp/{name}.install", postinstcode)
+ pkgbuild += f"\ninstall={name}.install\n"
+
+ self.machine.write("/tmp/PKGBUILD", pkgbuild)
+
+ cmd = """
+ cd /tmp/
+ su builder -c "makepkg --cleanbuild --clean --force --nodeps --skipinteg --noconfirm"
+"""
+
+ if install:
+ cmd += f"pacman -U --overwrite '*' --noconfirm {name}-{version}-{release}-{arch}.pkg.tar.zst\n"
+
+ cmd += f"mkdir -p {self.repo_dir}\n"
+ cmd += f"mv *.pkg.tar.zst {self.repo_dir}\n"
+ # Clean up packaging files
+ cmd += "rm PKGBUILD\n"
+ if postinst:
+ cmd += f"rm /tmp/{name}.install"
+ self.machine.execute(cmd)
+ self.addCleanup(self.machine.execute, f"pacman -Rdd --noconfirm {name} 2>/dev/null || true")
+
+ def createAptChangelogs(self):
+ # apt metadata has no formal field for bugs/CVEs, they are parsed from the changelog
+ for ((pkg, ver, rel), info) in self.updateInfo.items():
+ changes = info.get("changes", "some changes")
+ if info.get("bugs"):
+ changes += f" (Closes: {', '.join([('#' + str(b)) for b in info['bugs']])})"
+ if info.get("cves"):
+ changes += "\n * " + ", ".join(info["cves"])
+
+ path = f"{self.repo_dir}/changelogs/{pkg[0]}/{pkg}/{pkg}_{ver}-{rel}"
+ contents = f"""{pkg} ({ver}-{rel}) unstable; urgency=medium
+
+ * {changes}
+
+ -- Joe Developer <joe@example.com> Wed, 31 May 2017 14:52:25 +0200
+"""
+ self.machine.execute(f"mkdir -p $(dirname {path}); echo '{contents}' > {path}")
+
+ def createYumUpdateInfo(self):
+ xml = '<?xml version="1.0" encoding="UTF-8"?>\n<updates>\n'
+ for ((pkg, ver, rel), info) in self.updateInfo.items():
+ refs = ""
+ for b in info.get("bugs", []):
+ refs += f' <reference href="https://bugs.example.com?bug={b}" id="{b}" title="Bug#{b} Description" type="bugzilla"/>\n'
+ for c in info.get("cves", []):
+ refs += f' <reference href="https://www.cve.org/CVERecord?id={c}" id="{c}" title="{c}" type="cve"/>\n'
+ if info.get("securitySeverity"):
+ refs += ' <reference href="https://access.redhat.com/security/updates/classification/#{0}" id="" title="" type="other"/>\n'.format(info[
+ "securitySeverity"])
+ for e in info.get("errata", []):
+ refs += f' <reference href="https://access.redhat.com/errata/{e}" id="{e}" title="{e}" type="self"/>\n'
+
+ xml += """ <update from="test@example.com" status="stable" type="{severity}" version="2.0">
+ <id>UPDATE-{pkg}-{ver}-{rel}</id>
+ <title>{pkg} {ver}-{rel} update</title>
+ <issued date="2017-01-01 12:34:56"/>
+ <description>{desc}</description>
+ <references>
+{refs}
+ </references>
+ <pkglist>
+ <collection short="0815">
+ <package name="{pkg}" version="{ver}" release="{rel}" epoch="0" arch="noarch">
+ <filename>{pkg}-{ver}-{rel}.noarch.rpm</filename>
+ </package>
+ </collection>
+ </pkglist>
+ </update>
+""".format(pkg=pkg, ver=ver, rel=rel, refs=refs,
+ desc=info.get("changes", ""), severity=info.get("severity", "bugfix"))
+
+ xml += '</updates>\n'
+ return xml
+
+ def addPackageSet(self, name):
+ self.machine.execute(f"mkdir -p {self.repo_dir}; cp /var/lib/package-sets/{name}/* {self.repo_dir}")
+
+ def enableRepo(self):
+ if self.backend == "apt":
+ self.createAptChangelogs()
+ self.machine.execute(f"""echo 'deb [trusted=yes] file://{self.repo_dir} /' > /etc/apt/sources.list.d/test.list
+ cd {self.repo_dir}; apt-ftparchive packages . > Packages
+ xz -c Packages > Packages.xz
+ O=$(apt-ftparchive -o APT::FTPArchive::Release::Origin=cockpittest release .); echo "$O" > Release
+ echo 'Changelogs: http://localhost:12345/changelogs/@CHANGEPATH@' >> Release
+ """)
+ pid = self.machine.spawn(f"cd {self.repo_dir}; exec python3 -m http.server 12345", "changelog")
+ # pid will not be present for rebooting tests
+ self.addCleanup(self.machine.execute, "kill %i || true" % pid)
+ self.machine.wait_for_cockpit_running(port=12345) # wait for changelog HTTP server to start up
+ elif self.backend == "alpm":
+ self.machine.execute(f"""cd {self.repo_dir}
+ repo-add {self.repo_dir}/testrepo.db.tar.gz *.pkg.tar.zst
+ """)
+
+ config = f"""
+[testrepo]
+SigLevel = Never
+Server = file://{self.repo_dir}
+ """
+ if 'testrepo' not in self.machine.execute('grep testrepo /etc/pacman.conf || true'):
+ self.machine.write("/etc/pacman.conf", config, append=True)
+
+ else:
+ self.machine.execute("""printf '[updates]\nname=cockpittest\nbaseurl=file://{0}\nenabled=1\ngpgcheck=0\n' > /etc/yum.repos.d/cockpittest.repo
+ echo '{1}' > /tmp/updateinfo.xml
+ createrepo_c {0}
+ modifyrepo_c /tmp/updateinfo.xml {0}/repodata
+ dnf clean all""".format(self.repo_dir, self.createYumUpdateInfo()))
diff --git a/test/common/pixel-tests b/test/common/pixel-tests
new file mode 100755
index 0000000..f6c7ba6
--- /dev/null
+++ b/test/common/pixel-tests
@@ -0,0 +1,261 @@
+#!/bin/bash
+
+set -eu
+
+TEST_REFERENCE_SUBDIR="${TEST_REFERENCE_SUBDIR:-test/reference}"
+REPO=pixel-test-reference
+
+GITHUB_BASE="${GITHUB_BASE:-cockpit-project/cockpit}"
+GITHUB_REPOSITORY="${GITHUB_BASE%/*}/${REPO}"
+CLONE_REMOTE="https://github.com/${GITHUB_REPOSITORY}"
+PUSH_REMOTE="git@github.com:${GITHUB_REPOSITORY}"
+
+message() {
+ [ "${V-}" != 0 ] || printf " %-8s %s\n" "$1" "$2"
+}
+
+cmd_init() {
+ git submodule add -b empty "$CLONE_REMOTE" "$TEST_REFERENCE_SUBDIR"
+}
+
+cmd_update() {
+ git submodule update --init -- "$TEST_REFERENCE_SUBDIR" || (
+ echo ""
+ echo "Updating test/reference has failed, maybe because of"
+ echo "local changes that have been accidentally made while"
+ echo "it was out of date."
+ echo ""
+ echo "If you want to throw away these local changes, run"
+ echo ""
+ echo " $ ./test/common/pixel-tests reset"
+ echo ""
+ exit 1
+ )
+}
+
+cmd_pull() {
+ cmd_update
+}
+
+cmd_status() {
+ cmd_update
+ ( cd "$TEST_REFERENCE_SUBDIR"
+ git rm --force --cached --quiet '*.png'
+ git add *.png
+ if git diff-index --name-status --cached --exit-code HEAD; then
+ echo No changes
+ fi
+ )
+}
+
+cmd_push() {
+ cmd_update
+ ( cd "$TEST_REFERENCE_SUBDIR"
+ git rm --force --cached --quiet '*.png'
+ git add *.png
+ if ! git diff-index --name-status --cached --exit-code HEAD; then
+ git fetch origin empty:empty
+ git reset --soft empty
+ git commit --quiet -m "$(date)"
+ else
+ echo No changes
+ fi
+ tag="sha-$(git rev-parse HEAD)"
+ [ $(git tag -l "$tag") ] || git tag "$tag" HEAD
+ git push "$PUSH_REMOTE" "$tag"
+ )
+ git add "$TEST_REFERENCE_SUBDIR"
+ if [ -n "$(git status --porcelain "$TEST_REFERENCE_SUBDIR")" ]; then
+ echo ""
+ echo "The test/reference link has changed. The next step is to commit and"
+ echo "push this change, just like any other change to a file."
+ echo ""
+ echo "The change has already been added with 'git add', so you could now"
+ echo "amend your current HEAD commit with it like this:"
+ echo ""
+ echo " $ git commit --amend"
+ echo ""
+ echo "Then the HEAD commit can be pushed like normally. There is nothing"
+ echo "special about committing and pushing a change to test/reference."
+ fi
+}
+
+cmd_reset() {
+ rm -rf "$TEST_REFERENCE_SUBDIR"
+ cmd_update
+}
+
+pixel_test_logs_urls() {
+ arg=${1:-}
+
+ if [[ "$arg" == http* ]]; then
+ echo $arg
+ return
+ fi
+
+ repo=$(git remote get-url origin | sed -re 's,git@github.com:|https://github.com/,,' -e 's,\.git$,,')
+
+ if [ -n "$arg" ]; then
+ revision=$(curl -s "https://api.github.com/repos/$repo/pulls/$arg" | python3 -c "
+import json
+import sys
+
+print(json.load(sys.stdin)['head']['sha'])
+")
+ else
+ revision=$(git rev-parse @{upstream})
+ fi
+
+ context=$(cat test/reference-image)
+ curl -s "https://api.github.com/repos/$repo/statuses/$revision?per_page=100" | python3 -c "
+import json
+import sys
+import os
+
+seen = set()
+for s in json.load(sys.stdin):
+ c = s['context']
+ if 'pybridge' in c or 'firefox' in c or 'devel' in c:
+ continue
+ if c.split('/')[0] == sys.argv[1] and s['target_url']:
+ url=os.path.dirname(s['target_url'])
+ if url not in seen:
+ seen.add(url)
+ print(url)
+" "$context"
+}
+
+cmd_fetch() {
+ urls=$(pixel_test_logs_urls ${1:-})
+ if [ -z "$urls" ]; then
+ echo >&2 "Can't find test results for $(cat test/reference-image), sorry."
+ exit 1
+ fi
+ cmd_update
+ for url in ${urls}; do
+ url=${url/log.html/}
+ echo "Fetching new pixel test references from $url"
+ pixels=$(curl -s "$url/index.html" | grep '[^=><"]*-pixels.png' -o | uniq)
+ for f in ${pixels}; do
+ echo "$f"
+ curl -s --output-dir test/reference/ -O "$url/$f"
+ done
+ done
+}
+
+cmd_help() {
+ cat <<EOF
+$0 - Maintain the test/reference directory
+
+The following commands are available:
+
+update - Ensure that test/reference matches the checked out branch
+
+ The test/reference directory is a git submodule, and it needs to
+ be kept in sync with the main worktree (just like any other
+ submodule). This is necessary whenever you check out a different
+ branch and then start working on pixel tests, for example.
+
+ You can use "git checkout --recurse-submodules" when changing
+ branches to achieve this, or some variation of "git submodule
+ update test/reference", or run "pixel-tests update".
+
+ Most other pixel-tests commands will run "pixel-tests update"
+ automatically, so you might not actually need to run it explicitly
+ very often, but it's good to remember that this step is
+ unfortunately necessary.
+
+ Like other ways to update submodules, this command will not
+ overwrite your local changes, so it is safe to use often and "just
+ in case". If your local changes conflict with the changes that
+ would be necessary for updating test/reference, the update will
+ not be done.
+
+ When "pixel-tests update" fails, it is probably easiest to throw
+ away local changes with "pixel-tests reset" and re-acquire the new
+ reference images that you want to install, maybe with "pixel-tests
+ fetch".
+
+pull - Old name for "update".
+
+reset - Throw away local changes
+
+ If you want to get rid of unpushed local changes, run "pixel-tests
+ reset". This will remove the test/reference directory completely
+ and then run "pixel-tests update" to recreate it.
+
+status - Show local changes
+
+ After writing new reference images into test/reference, running
+ "pixel-test status" will summarize the local changes. It will
+ list all images that have been modified (with a "M" prefix), added
+ ("A"), or deleted ("D").
+
+ The "status" command will run "update" as the first step, so it
+ might fail if it detects conflicts. To avoid that risk, run
+ "update" explicitly before making any changes in test/reference.
+
+push - Upload local changes and prepare test/reference for committing
+
+ Once you have finished writing new reference images into
+ test/reference (maybe by running "pixel-tests fetch") and are
+ ready to make them part of your pull request, you need to upload
+ them to Github and record them in the main source repository.
+
+ First, run "pixel-tests push" to do the uploading and to stage a
+ changed test/reference in the main source repository.
+
+ In the main source reository, test/reference is a special kind of
+ object. It's not a file, or directory, and not even a symlink
+ (although it is similar to a symlink). It's a "gitlink". But
+ whatever it is, changes to it need to be staged, committed, and
+ pushed, just like changes to regular files.
+
+ When "pixel-tests push" is done, the change to test/reference has
+ already been staged with "git add" to remind you that there is
+ something to commit.
+
+ Committing the change is identical to committing any other change.
+
+ "push" will also run "update" as the first step.
+
+fetch - Download fresh reference images from a test run
+
+ When code or tests are changed, we often need to install new or
+ changed reference images for the pixel tests. A good way to do
+ that is to run the tests in our CI machinery, let them fail, and
+ grab the new reference images from the test log directory.
+ Running "pixel-tests fetch" can automate this.
+
+ Without any arguments, "fetch" will figure out all by itself where
+ to download the images from. For this to work, your "origin"
+ remote needs to point to the repository on Github that the current
+ PR will be merged into. For our main Cockpit repository, that
+ would be "cockpit-project/cockpit", for example.
+
+ You can also pass a PR number to "fetch" if the current branch
+ doesn't correspond to the PR with the new pixels. Or you can pass
+ the URL of the log results.
+
+ "push" will also run "update" as the first step.
+EOF
+}
+
+main() {
+ local cmd="${1-}"
+
+ if [ -z "${cmd}" ]; then
+ echo 'This command requires a subcommand: update status push reset fetch'
+ echo "Run '$0 help' for a longer explanation"
+ exit 1
+ elif ! type -t "cmd_${cmd}" | grep -q function; then
+ echo "Unknown subcommand ${cmd}"
+ exit 1
+ fi
+
+ shift
+ [ "${V-0}" = 0 ] || set -x
+ "cmd_$cmd" "$@"
+}
+
+main "$@"
diff --git a/test/common/pixeldiff.html b/test/common/pixeldiff.html
new file mode 100644
index 0000000..36970f2
--- /dev/null
+++ b/test/common/pixeldiff.html
@@ -0,0 +1,623 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Cockpit Integration Tests - Pixel diffs</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <style>
+/*
+MIT License
+
+Copyright (c) 2018 Diamant Haxhimusa
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+.pixelcompare-wrapper {
+ background: red;
+}
+.pixelcompare-wrapper.pixelcompare-horizontal {
+ height: 100% !important;
+}
+.pixelcompare-horizontal .pixelcompare-handle:before, .pixelcompare-horizontal .pixelcompare-handle:after, .pixelcompare-vertical .pixelcompare-handle:before, .pixelcompare-vertical .pixelcompare-handle:after {
+ content: "";
+ display: block;
+ background: #fff;
+ position: absolute;
+ z-index: 30;
+}
+img.pixelcompare-before, img.pixelcompare-after {
+ object-fit: cover;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+
+.pixelcompare-before-label, .pixelcompare-after-label, .pixelcompare-overlay {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.pixelcompare-before-label, .pixelcompare-after-label, .pixelcompare-overlay {
+ transition-duration: 0.5s;
+}
+
+.pixelcompare-before-label, .pixelcompare-after-label {
+ transition-property: opacity;
+}
+
+.pixelcompare-horizontal .pixelcompare-before-label:before, .pixelcompare-horizontal .pixelcompare-after-label:before {
+ top: 50%;
+ margin-top: -19px;
+}
+
+.pixelcompare-vertical .pixelcompare-before-label:before, .pixelcompare-vertical .pixelcompare-after-label:before {
+ left: 50%;
+ margin-left: -45px;
+ text-align: center;
+ width: 90px;
+}
+
+.pixelcompare-left-arrow, .pixelcompare-right-arrow, .pixelcompare-up-arrow, .pixelcompare-down-arrow {
+ width: 0;
+ height: 0;
+ border: 6px inset transparent;
+ position: absolute;
+}
+
+.pixelcompare-left-arrow, .pixelcompare-right-arrow {
+ top: 50%;
+ margin-top: -6px;
+}
+
+.pixelcompare-up-arrow, .pixelcompare-down-arrow {
+ left: 50%;
+ margin-left: -6px;
+}
+
+.pixelcompare-container {
+ box-sizing: content-box;
+ z-index: 0;
+ height: 100%;
+ overflow: hidden;
+ position: relative;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+.pixelcompare-container img {
+ max-width: 100%;
+ position: absolute;
+ top: 0;
+ display: block;
+}
+/* .pixelcompare-container.active .pixelcompare-overlay,
+.pixelcompare-container.active :hover.pixelcompare-overlay {
+ background: transparent;
+} */
+.pixelcompare-container.active .pixelcompare-overlay .pixelcompare-before-label,
+.pixelcompare-container.active .pixelcompare-overlay .pixelcompare-after-label,
+.pixelcompare-container.active :hover.pixelcompare-overlay .pixelcompare-before-label,
+.pixelcompare-container.active :hover.pixelcompare-overlay .pixelcompare-after-label {
+ opacity: 0;
+}
+.pixelcompare-container * {
+ box-sizing: content-box;
+}
+
+.pixelcompare-before-label {
+ opacity: 0;
+}
+.pixelcompare-before-label:before {
+ content: attr(data-content);
+}
+
+.pixelcompare-after-label {
+ opacity: 0;
+}
+.pixelcompare-after-label:before {
+ content: attr(data-content);
+}
+
+.pixelcompare-horizontal .pixelcompare-before-label:before {
+ left: 10px;
+}
+
+.pixelcompare-horizontal .pixelcompare-after-label:before {
+ right: 10px;
+}
+
+.pixelcompare-vertical .pixelcompare-before-label:before {
+ top: 10px;
+}
+
+.pixelcompare-vertical .pixelcompare-after-label:before {
+ bottom: 10px;
+}
+
+.pixelcompare-overlay {
+ transition-property: background;
+ background: transparent;
+ z-index: 25;
+}
+.pixelcompare-overlay:hover,
+.pixelcompare-container.active .pixelcompare-overlay,
+.pixelcompare-handle:hover + .pixelcompare-overlay {
+ background: rgba(0, 0, 0, 0.5);
+}
+.pixelcompare-overlay:hover .pixelcompare-after-label {
+ opacity: 1;
+}
+.pixelcompare-overlay:hover .pixelcompare-before-label {
+ opacity: 1;
+}
+
+.pixelcompare-before {
+ z-index: 20;
+}
+
+.pixelcompare-after {
+ z-index: 10;
+}
+
+.pixelcompare-vertical .pixelcompare-handle {
+ background-color: rgba(247, 237, 237, 0.71);
+ height: 20px;
+ width: 65px;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin-left: -19px;
+ margin-top: -10px;
+ border-radius: 1000px;
+ box-shadow: 0px 0px 12px rgba(51, 51, 51, 0.5);
+ z-index: 40;
+ cursor: pointer;
+}
+
+.pixelcompare-horizontal .pixelcompare-handle {
+ background-color: rgba(247, 237, 237, 0.71);
+ height: 65px;
+ width: 20px;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin-top: -19px;
+ margin-left: -10px;
+ border-radius: 1000px;
+ box-shadow: 0px 0px 12px rgba(51, 51, 51, 0.5);
+ z-index: 40;
+ cursor: pointer;
+}
+
+.pixelcompare-horizontal .pixelcompare-handle:before {
+ bottom: 50%;
+ height: 799px;
+ margin-bottom: 33px;
+ margin-left: 10px;
+ background: none;
+ position: absolute;
+ z-index: 999;
+ border-right: 1px dashed rgba(255, 255, 255, 0.5);
+}
+
+.pixelcompare-horizontal .pixelcompare-handle:after {
+ top: 50%;
+ height: 257px;
+ margin-top: 33px;
+ margin-left: 10px;
+ background: none;
+ position: absolute;
+ z-index: 999;
+ border-right: 1px dashed rgba(255, 255, 255, 0.5);
+}
+
+
+.pixelcompare-vertical .pixelcompare-handle:before {
+ margin-top: 10px;
+ margin-left: 11px;
+ width: 100vw;
+ background: none;
+ position: absolute;
+ z-index: 999;
+ border-top: 1px dashed rgba(255, 255, 255, 0.5);
+ right: 50%;
+ margin-right: 33px;
+}
+
+.pixelcompare-vertical .pixelcompare-handle:after {
+ margin-top: 10px;
+ width: 100vw;
+ background: none;
+ position: absolute;
+ z-index: 999;
+ border-top: 1px dashed rgba(255, 255, 255, 0.5);
+ left: 50%;
+ margin-left: 33px;
+}
+
+.pixelcompare-left-arrow {
+ border-right: 6px solid rgba(247, 237, 237, 0.71);
+ left: 50%;
+ margin-left: -13px;
+}
+
+.pixelcompare-right-arrow {
+ border-left: 6px solid rgba(247, 237, 237, 0.71);
+ right: 50%;
+ margin-right: -13px;
+}
+
+.pixelcompare-up-arrow {
+ border-bottom: 6px solid rgba(247, 237, 237, 0.71);
+ top: 50%;
+ margin-top: -13px;
+}
+
+.pixelcompare-down-arrow {
+ border-top: 6px solid rgba(247, 237, 237, 0.71);
+ bottom: 50%;
+ margin-bottom: -13px;
+}
+ </style>
+ <script>
+"use strict";
+/**
+ * PIXELCOMPARE
+ * Javascript image comparison
+ * @author diamanthaxhimusa@gmail.com
+
+MIT License
+
+Copyright (c) 2018 Diamant Haxhimusa
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+ */
+window.setup_pixelcompare = function() {
+ var _extend = function(defaults, options) {
+ var extended = {};
+ var prop;
+ for (prop in defaults) {
+ if(Object.prototype.hasOwnProperty.call(defaults, prop)) {
+ extended[prop] = defaults[prop];
+ }
+ }
+ for (prop in options) {
+ if(Object.prototype.hasOwnProperty.call(options, prop)) {
+ extended[prop] = options[prop];
+ }
+ }
+ return extended;
+ };
+
+ var _addClass = function(element, classname) {
+ var arr;
+ arr = element.className.split(" ");
+ if(arr.indexOf(classname) == -1) {
+ element.className += " " + classname;
+ }
+ };
+
+ var _hasClass = function(element, className) {
+ return new RegExp("(\\s|^)" + className + "(\\s|$)").test(
+ element.className
+ );
+ };
+
+ var _removeClass = function(element, className) {
+ element.classList.remove(className);
+ };
+
+ var _wrap = function(element, tag, sliderOrientation) {
+ var div = document.createElement(tag);
+ _addClass(div, "pixelcompare-wrapper pixelcompare-" + sliderOrientation);
+ element.parentElement.insertBefore(div, element);
+ div.appendChild(element);
+ return div;
+ };
+
+ var _createSlider = function(beforeDirection, afterDirection) {
+ var pxcHandleNode = document.createElement("div");
+ _addClass(pxcHandleNode, "pixelcompare-handle");
+
+ var sliderChildNodeBeforeDirection = document.createElement("span");
+ _addClass(
+ sliderChildNodeBeforeDirection,
+ "pixelcompare-" + beforeDirection + "-arrow"
+ );
+ pxcHandleNode.appendChild(sliderChildNodeBeforeDirection);
+ var sliderChildNodeAfterDirection = document.createElement("span");
+ _addClass(
+ sliderChildNodeAfterDirection,
+ "pixelcompare-" + afterDirection + "-arrow"
+ );
+ pxcHandleNode.appendChild(sliderChildNodeAfterDirection);
+
+ pxcHandleNode.addEventListener("touchmove", function(e) {
+ e.preventDefault();
+ });
+ return pxcHandleNode;
+ };
+
+ var options = {
+ default_offset_pct: 0.5,
+ orientation: "horizontal",
+ overlay: false,
+ hover: false,
+ move_with_handle_only: true,
+ click_to_move: false,
+ showSlider: true,
+ };
+
+ var pxcContainers = document.querySelectorAll("[data-pixelcompare]");
+
+ pxcContainers.forEach(function(pcContainer) {
+ var sliderPct = options.default_offset_pct;
+ var imageContainer = pcContainer;
+ options.hover = pcContainer.hasAttribute("data-hover");
+ options.showSlider = options.hover ? pcContainer.hasAttribute("data-show-slider") : options.showSlider;
+ options.orientation = pcContainer.hasAttribute("data-vertical")
+ ? "vertical"
+ : "horizontal";
+ var orientations = ["vertical", "horizontal", "sides"];
+
+ var datasetOrientation = imageContainer.dataset.pixelcompareOrientation;
+ var sliderOrientation = orientations.includes(datasetOrientation)
+ ? datasetOrientation
+ : options.orientation;
+ var beforeDirection = sliderOrientation === "vertical" ? "down" : "left";
+ var afterDirection = sliderOrientation === "vertical" ? "up" : "right";
+
+ var container = _wrap(pcContainer, "div", sliderOrientation);
+
+ var beforeImg = container.querySelectorAll("img")[0];
+ beforeImg.draggable = false;
+ var afterImg = container.querySelectorAll("img")[1];
+ afterImg.draggable = false;
+
+ _addClass(container, "pixelcompare-container");
+ _addClass(beforeImg, "pixelcompare-before");
+ _addClass(afterImg, "pixelcompare-after");
+
+ var slider = null;
+ if(options.showSlider) {
+ slider = _createSlider(beforeDirection, afterDirection);
+ container.appendChild(slider);
+ }
+ if(options.overlay) {
+ var overlayNode = document.createElement("div");
+ _addClass(overlayNode, "pixelcompare-overlay");
+ container.appendChild(overlayNode);
+ }
+
+ var calcOffset = function(dimensionPct) {
+ var w = beforeImg.getBoundingClientRect().width;
+ var h = beforeImg.getBoundingClientRect().height;
+ return {
+ w: w + "px",
+ h: h + "px",
+ wp: dimensionPct * 100,
+ cw: dimensionPct * w + "px",
+ ch: dimensionPct * h + "px",
+ };
+ };
+
+ var adjustContainer = function(offset) {
+ if(sliderOrientation === "vertical") {
+ beforeImg.style.clip =
+ "rect(0, " + offset.w + ", " + offset.ch + ", 0)";
+ afterImg.style.clip =
+ "rect(" + offset.ch + ", " + offset.w + ", " + offset.h + ", 0)";
+ } else if(sliderOrientation === "sides") {
+ beforeImg.style.clipPath = `polygon(0% ${2 * (50 - offset.wp)}%, ${
+ 2 * offset.wp
+ }% 100%, 0% 100%)`;
+ afterImg.style.clipPath = `polygon(100% ${2 * (100 - offset.wp)}%, ${
+ -2 * (50 - offset.wp)
+ }% 0%, 100% 0%)`;
+ } else {
+ beforeImg.style.clip =
+ "rect(0, " + offset.cw + ", " + offset.h + ", 0)";
+ afterImg.style.clip =
+ "rect(0, " + offset.w + "," + offset.h + "," + offset.cw + ")";
+ }
+ container.style.height = offset.h;
+ };
+
+ var adjustSlider = function(pct) {
+ var offset = calcOffset(pct);
+ if(slider) {
+ if(sliderOrientation === "vertical") {
+ slider.style.top = offset.ch;
+ } else {
+ slider.style.left = offset.cw;
+ }
+ }
+ adjustContainer(offset);
+ };
+
+ // Return the number specified or the min/max number if it outside the range given.
+ var minMaxNumber = function(num, min, max) {
+ return Math.max(min, Math.min(max, num));
+ };
+
+ // Calculate the slider percentage based on the position.
+ var getSliderPercentage = function(positionX, positionY) {
+ var sliderPercentage =
+ sliderOrientation === "vertical"
+ ? (positionY - offsetY) / imgHeight
+ : (positionX - offsetX) / imgWidth;
+ return minMaxNumber(sliderPercentage, 0, 1);
+ };
+
+ window.addEventListener("resize.pixelcompare", function(e) {
+ adjustSlider(sliderPct);
+ });
+
+ var offsetX = 0;
+ var offsetY = 0;
+ var imgWidth = 0;
+ var imgHeight = 0;
+ var onMoveStart = function(e) {
+ if(
+ ((e.distX > e.distY && e.distX < -e.distY) ||
+ (e.distX < e.distY && e.distX > -e.distY)) &&
+ sliderOrientation !== "vertical"
+ ) {
+ e.preventDefault();
+ } else if(
+ ((e.distX < e.distY && e.distX < -e.distY) ||
+ (e.distX > e.distY && e.distX > -e.distY)) &&
+ sliderOrientation === "vertical"
+ ) {
+ e.preventDefault();
+ }
+ _addClass(container, "active");
+ offsetX = container.offsetLeft;
+ offsetY = container.offsetTop;
+ imgWidth = beforeImg.getBoundingClientRect().width;
+ imgHeight = beforeImg.getBoundingClientRect().height;
+ };
+ var onMove = function(e) {
+ if(_hasClass(container, "active")) {
+ sliderPct = getSliderPercentage(
+ e.pageX || e.changedTouches[0].pageX,
+ e.pageY || e.changedTouches[0].pageY
+ );
+ adjustSlider(sliderPct);
+ }
+ };
+ var onMoveEnd = function() {
+ _removeClass(container, "active");
+ };
+
+ if(options.hover) {
+ container.addEventListener("mouseenter", onMoveStart);
+ container.addEventListener("mouseleave", onMoveEnd);
+ container.addEventListener("mousemove", onMove);
+ } else {
+ var moveTarget = options.move_with_handle_only ? slider : container;
+ window.addEventListener("mouseup", onMoveEnd);
+ container.addEventListener("mousemove", onMove);
+ moveTarget.addEventListener("mousedown", onMoveStart);
+ moveTarget.addEventListener("touchstart", onMoveStart);
+ container.addEventListener("touchmove", onMove);
+ window.addEventListener("touchend", onMoveEnd);
+ }
+
+ container
+ .querySelector("img")
+ .addEventListener("mousedown", function(event) {
+ event.preventDefault();
+ });
+
+ if(options.click_to_move) {
+ container.on("click", function(e) {
+ offsetX = container.offset().left;
+ offsetY = container.offset().top;
+ imgWidth = beforeImg.width();
+ imgHeight = beforeImg.height();
+ sliderPct = getSliderPercentage(e.pageX, e.pageY);
+ adjustSlider(sliderPct);
+ });
+ }
+ window.dispatchEvent(new Event("resize.pixelcompare"));
+ });
+};
+ </script>
+ <script>
+document.addEventListener("DOMContentLoaded", () => {
+ const base = window.location.hash.replace(/[^\w-]/g, "");
+ document.getElementById("key").textContent = base;
+
+ const e_new = document.getElementById("new");
+ e_new.textContent = base + "-pixels.png";
+ e_new.setAttribute("href", base + "-pixels.png");
+
+ const img_now = document.createElement("img");
+ img_now.setAttribute("src", base + "-pixels.png");
+
+ const img_ref = document.createElement("img");
+ img_ref.setAttribute("src", base + "-reference.png");
+
+ const img_delta = document.createElement("img");
+ img_delta.setAttribute("src", base + "-delta.png");
+
+ let loaded = { };
+
+ function load_event(tag) {
+ loaded[tag] = true;
+ if (loaded.now && loaded.ref && loaded.delta) {
+ const diff = document.getElementById("diff");
+ const w = Math.max(img_now.naturalWidth, img_ref.naturalWidth);
+ const h = Math.max(img_now.naturalHeight, img_ref.naturalHeight);
+ diff.style.width = w + "px";
+ diff.style.height = h + "px";
+ const ecompare = document.createElement("div");
+ ecompare.classList.add("pixelcompare");
+ ecompare.setAttribute("data-pixelcompare", "");
+ ecompare.appendChild(img_now);
+ ecompare.appendChild(img_ref);
+ diff.appendChild(ecompare);
+ window.setup_pixelcompare();
+
+ const delta = document.getElementById("delta");
+ delta.style.width = img_delta.naturalWidth + "px";
+ delta.style.height = img_delta.naturalHeight + "px";
+ delta.appendChild(img_delta);
+ }
+ }
+
+ img_now.addEventListener("load", () => load_event("now"));
+ img_ref.addEventListener("load", () => load_event("ref"));
+ img_delta.addEventListener("load", () => load_event("delta"));
+});
+ </script>
+ </head
+ <body>
+ <h2>Pixel comparison for <span id="key"></span></h2>
+ <p>New <a id="new"></a> on the left, reference on the right.</p>
+ <div id="diff">
+ </div>
+ <h2>Changed pixels in red, ignored changes in green</h2>
+ <div id="delta">
+ </div>
+ </body>
+</html>
diff --git a/test/common/pywrap b/test/common/pywrap
new file mode 100755
index 0000000..c5e78b3
--- /dev/null
+++ b/test/common/pywrap
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+# Run a Python script, setting up PYTHONPATH for access to test/common and the
+# python libraries in bots/. Checks out the bots first, if necessary.
+
+# This is intended to be used from the interpreter line of executable Python
+# scripts, referring to it with a relative path. The interpreter line should
+# look something like so:
+
+ #!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../test/common/pywrap", sys.argv)
+
+# with the `/../test/common/` part determined by the location of the script
+# relative to this script.
+
+set -eu
+
+realpath="$(realpath "$0")"
+top_srcdir="${realpath%/*}/../.."
+
+# Check out the bots if required
+test -d "${top_srcdir}/bots" || "${top_srcdir}/test/common/make-bots"
+
+# Prepend the path
+PYTHONPATH="${top_srcdir}/test/common:${top_srcdir}/bots:${top_srcdir}/bots/machine${PYTHONPATH:+:${PYTHONPATH}}"
+export PYTHONPATH
+
+# Run the script
+# -B : don't write .pyc files on import; also PYTHONDONTWRITEBYTECODE=x
+# -P : don't prepend a potentially unsafe path to sys.path -- but not available in RHEL 8/9 yet, use once we can
+exec python3 -B "$@"
diff --git a/test/common/ruff.toml b/test/common/ruff.toml
new file mode 100644
index 0000000..6e46ee9
--- /dev/null
+++ b/test/common/ruff.toml
@@ -0,0 +1,11 @@
+extend = "../../pyproject.toml"
+
+[lint]
+ignore = [
+ "E501", # https://github.com/charliermarsh/ruff/issues/3206#issuecomment-1562681390
+
+ "B010", # Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
+ "FBT001", # Boolean positional arg in function definition
+ "FBT002", # Boolean default value in function definition
+ "PT009", # Use a regular `assert` instead of unittest-style `assertEqual`
+]
diff --git a/test/common/run-tests b/test/common/run-tests
new file mode 100755
index 0000000..58c2962
--- /dev/null
+++ b/test/common/run-tests
@@ -0,0 +1,585 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/pywrap", sys.argv)
+
+import argparse
+import binascii
+import errno
+import glob
+import importlib.machinery
+import importlib.util
+import logging
+import os
+import socket
+import string
+import subprocess
+import sys
+import tempfile
+import time
+import unittest
+from typing import List, Optional, Tuple
+
+import testlib
+import testvm
+from lcov import create_coverage_report, prepare_for_code_coverage
+
+os.environ['PYTHONUNBUFFERED'] = '1'
+
+
+def flush_stdout():
+ while True:
+ try:
+ sys.stdout.flush()
+ break
+ except BlockingIOError:
+ time.sleep(0.1)
+
+
+class Test:
+ def __init__(self, test_id, command, timeout, nondestructive, retry_when_affected, todo, cost=1):
+ self.process = None
+ self.retries = 0
+ self.test_id = test_id
+ self.command = command
+ self.timeout = timeout
+ self.nondestructive = nondestructive
+ self.machine_id = None
+ self.retry_when_affected = retry_when_affected
+ self.todo = todo
+ self.cost = cost
+ self.returncode = None
+
+ def assign_machine(self, machine_id, ssh_address, web_address):
+ assert self.nondestructive, "assigning a machine only works for nondestructive test"
+ self.machine_id = machine_id
+ self.command.insert(-2, "--machine")
+ self.command.insert(-2, ssh_address)
+ self.command.insert(-2, "--browser")
+ self.command.insert(-2, web_address)
+
+ def start(self):
+ if self.nondestructive:
+ assert self.machine_id is not None, f"need to assign nondestructive test {self} {self.command} to a machine"
+ self.outfile = tempfile.TemporaryFile()
+ self.process = subprocess.Popen(["timeout", "-v", str(self.timeout), *self.command],
+ stdout=self.outfile, stderr=subprocess.STDOUT)
+
+ def poll(self):
+ poll_result = self.process.poll()
+ if poll_result is not None:
+ self.outfile.flush()
+ self.outfile.seek(0)
+ self.output = self.outfile.read()
+ self.outfile.close()
+ self.outfile = None
+ self.returncode = self.process.returncode
+
+ return poll_result
+
+ def finish(self, affected_tests: List[str], opts: argparse.Namespace) -> Tuple[Optional[str], int]:
+ """Returns if a test should retry or not
+
+ Call test-failure-policy on the test's output, print if needed.
+
+ Return (retry_reason, exit_code). retry_reason can be None or a string.
+ """
+
+ print_tap = not opts.list
+ affected = any(self.command[0].endswith(t) for t in affected_tests)
+ retry_reason = ""
+
+ # Try affected tests 3 times
+ if self.returncode == 0 and affected and self.retry_when_affected and self.retries < 2:
+ retry_reason = "test affected tests 3 times"
+ self.retries += 1
+ self._print_test(print_tap, f"# RETRY {self.retries} ({retry_reason})")
+ return retry_reason, 0
+
+ # If test is being skipped pick up the reason
+ if self.returncode == 77:
+ lines = self.output.splitlines()
+ skip_reason = lines[-1].strip().decode("utf-8")
+ self.output = b"\n".join(lines[:-1])
+ self._print_test(print_tap, skip_reason=skip_reason)
+ return None, 0
+
+ # If the test was marked with @todo then...
+ if self.todo is not None:
+ if self.returncode == 0:
+ # The test passed, but it shouldn't have.
+ self.returncode = 1 # that's a fail
+ self._print_test(print_tap, todo_reason=f'# expected failure: {self.todo}')
+ return None, 1
+ else:
+ # The test failed as expected
+ # Outputs 'not ok 1 test # TODO ...'
+ self._print_test(print_tap, todo_reason=f'# TODO {self.todo}')
+ return None, 0
+
+ if self.returncode == 0:
+ self._print_test(print_tap)
+ return None, 0
+
+ if not opts.thorough:
+ cmd = ["test-failure-policy", "--all"]
+ if not opts.track_naughties:
+ cmd.append("--offline")
+ cmd.append(testvm.DEFAULT_IMAGE)
+ try:
+ proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ reason = proc.communicate(self.output + ("not ok " + str(self)).encode())[0].strip()
+
+ if proc.returncode == 77:
+ self.returncode = proc.returncode
+ self._print_test(skip_reason="# SKIP {0}".format(reason.decode("utf-8")))
+ return None, 0
+
+ if proc.returncode == 78:
+ self.returncode = proc.returncode
+ self._print_test(skip_reason="# NOTE {0}".format(reason.decode("utf-8")))
+ return None, 1
+
+ if proc.returncode == 1:
+ retry_reason = reason.decode("utf-8")
+
+ except OSError as ex:
+ if ex.errno != errno.ENOENT:
+ sys.stderr.write(f"\nCouldn't run test-failure-policy: {ex!s}\n")
+
+ # HACK: many tests are unstable, always retry them 3 times unless affected
+ if not affected and not retry_reason and not opts.no_retry_fail:
+ retry_reason = "be robust against unstable tests"
+
+ has_unexpected_message = testlib.UNEXPECTED_MESSAGE.encode() in self.output
+ has_pixel_test_message = testlib.PIXEL_TEST_MESSAGE.encode() in self.output
+ if self.retries < 2 and not (has_unexpected_message or has_pixel_test_message) and retry_reason:
+ self.retries += 1
+ self._print_test(retry_reason=f"# RETRY {self.retries} ({retry_reason})")
+ return retry_reason, 0
+
+ self.output += b"\n"
+ self._print_test()
+ self.machine_id = None
+ return None, 1
+
+ # internal methods
+
+ def __str__(self):
+ cost = "" if self.cost == 1 else f" ${self.cost}"
+ nd = f" [ND@{self.machine_id}]" if self.nondestructive else ""
+ return f"{self.test_id} {self.command[0]} {self.command[-1]}{cost}{nd}"
+
+ def _print_test(self, print_tap=True, retry_reason="", skip_reason="", todo_reason=""):
+ def write_line(line):
+ while line:
+ try:
+ sys.stdout.buffer.write(line)
+ break
+ except BlockingIOError as e:
+ line = line[e.characters_written:]
+ time.sleep(0.1)
+
+ # be quiet in TAP mode for successful tests
+ lines = self.output.strip().splitlines(keepends=True)
+ if print_tap and self.returncode == 0 and len(lines) > 0:
+ for line in lines[:-1]:
+ if line.startswith(b"WARNING:"):
+ write_line(line)
+ write_line(lines[-1])
+ else:
+ for line in lines:
+ write_line(line)
+
+ if retry_reason:
+ retry_reason = " " + retry_reason
+ if skip_reason:
+ skip_reason = " " + skip_reason
+ if todo_reason:
+ todo_reason = " " + todo_reason
+
+ if not print_tap:
+ print(retry_reason + skip_reason + todo_reason)
+ flush_stdout()
+ return
+
+ print() # Tap needs to start on a separate line
+ status = 'ok' if self.returncode in [0, 77] else 'not ok'
+ print(f"{status} {self}{retry_reason}{skip_reason}{todo_reason}")
+ flush_stdout()
+
+
+class GlobalMachine:
+ def __init__(self, restrict=True, cpus=None, memory_mb=None, machine_class=testvm.VirtMachine):
+ self.image = testvm.DEFAULT_IMAGE
+ self.network = testvm.VirtNetwork(image=self.image)
+ self.networking = self.network.host(restrict=restrict)
+ # provide enough RAM for cryptsetup's PBKDF, as long as that is not configurable:
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1881829
+ self.machine = machine_class(verbose=True, networking=self.networking, image=self.image, cpus=cpus,
+ memory_mb=memory_mb or 1400)
+ self.machine_class = machine_class
+ if not os.path.exists(self.machine.image_file):
+ self.machine.pull(self.machine.image_file)
+ self.machine.start()
+ self.start_time = time.time()
+ self.duration = None
+ self.ssh_address = f"{self.machine.ssh_address}:{self.machine.ssh_port}"
+ self.web_address = f"{self.machine.web_address}:{self.machine.web_port}"
+ self.running_test = None
+
+ def reset(self):
+ # It is important to re-use self.networking here, so that the
+ # machine keeps its browser and control port.
+ self.machine.kill()
+ self.machine = self.machine_class(verbose=True, networking=self.networking, image=self.image)
+ self.machine.start()
+
+ def kill(self):
+ assert self.running_test is None, "can't kill global machine with running test"
+ self.machine.kill()
+ self.network.kill()
+ self.duration = round(time.time() - self.start_time)
+ self.machine = None
+ self.ssh_address = None
+ self.web_address = None
+
+ def is_available(self):
+ return self.machine and self.running_test is None
+
+
+def check_valid(filename):
+ name = os.path.basename(filename)
+ allowed = string.ascii_letters + string.digits + '-_'
+ if not all(c in allowed for c in name):
+ return None
+ return name.replace("-", "_")
+
+
+def build_command(filename, test, opts):
+ cmd = [filename]
+ if opts.trace:
+ cmd.append("-t")
+ if opts.verbosity:
+ cmd.append("-v")
+ if not opts.fetch:
+ cmd.append("--nonet")
+ if opts.list:
+ cmd.append("-l")
+ if opts.coverage:
+ cmd.append("--coverage")
+ cmd.append(test)
+ return cmd
+
+
+def get_affected_tests(test_dir, base_branch, test_files):
+ if not base_branch:
+ return []
+
+ changed_tests = []
+
+ # Detect affected tests from changed test files
+ diff_out = subprocess.check_output(["git", "diff", "--name-only", "origin/" + base_branch, test_dir])
+ # Never consider 'test/verify/check-example' to be affected - our tests for tests count on that
+ # This file provides only examples, there is no place for it being flaky, no need to retry
+ changed_tests = [test.decode("utf-8") for test in diff_out.strip().splitlines() if not test.endswith(b"check-example")]
+
+ # If more than 3 test files were changed don't consider any of them as affected
+ # as it might be a PR that changes more unrelated things.
+ if len(changed_tests) > 3:
+ # If 'test/verify/check-testlib' is affected, keep just that one - our tests for tests count on that
+ if "test/verify/check-testlib" in changed_tests:
+ changed_tests = ["test/verify/check-testlib"]
+ else:
+ changed_tests = []
+
+ # Detect affected tests from changed pkg/* subdirectories in cockpit
+ # If affected tests get detected from pkg/* changes, don't apply the
+ # "only do this for max. 3 check-* changes" (even if the PR also changes ≥ 3 check-*)
+ # (this does not apply to other projects)
+ diff_out = subprocess.check_output(["git", "diff", "--name-only", "origin/" + base_branch, "--", "pkg/"])
+
+ # Drop changes in css files - this does not affect tests thus no reason to retry
+ files = [f.decode("utf-8") for f in diff_out.strip().splitlines() if not f.endswith(b"css")]
+
+ changed_pkgs = {"check-" + pkg.split('/')[1] for pkg in files}
+ changed_tests.extend([test for test in test_files if any(pkg in test for pkg in changed_pkgs)])
+
+ return changed_tests
+
+
+def detect_tests(test_files, image, opts):
+ """Detect tests to be run
+
+ Builds the list of tests we'll run in separate machines (destructive tests)
+ and the ones we can run on the same machine (nondestructive)
+ """
+
+ destructive_tests = []
+ nondestructive_tests = []
+ seen_classes = {}
+ machine_class = None
+ test_id = 1
+
+ for filename in test_files:
+ name = check_valid(filename)
+ if not name or not os.path.isfile(filename):
+ continue
+ loader = importlib.machinery.SourceFileLoader(name, filename)
+ module = importlib.util.module_from_spec(importlib.util.spec_from_loader(loader.name, loader))
+ loader.exec_module(module)
+ for test_suite in unittest.TestLoader().loadTestsFromModule(module):
+ for test in test_suite:
+ if hasattr(test, "machine_class") and test.machine_class is not None:
+ if machine_class is not None and machine_class != test.machine_class:
+ raise ValueError(f"only one unique machine_class can be used per project, provided with {machine_class} and {test.machine_class}")
+
+ machine_class = test.machine_class
+
+ # ensure that test classes are unique, so that they can be selected properly
+ cls = test.__class__.__name__
+ if seen_classes.get(cls) not in [None, filename]:
+ raise ValueError("test class %s in %s already defined in %s" % (cls, filename, seen_classes[cls]))
+ seen_classes[cls] = filename
+
+ test_method = getattr(test.__class__, test._testMethodName)
+ test_str = f"{cls}.{test._testMethodName}"
+ # most tests should take much less than 10mins, so default to that;
+ # longer tests can be annotated with @timeout(seconds)
+ # check the test function first, fall back to the class'es timeout
+ if opts.tests and not any(t in test_str for t in opts.tests):
+ continue
+ if test_str in opts.exclude:
+ continue
+ test_timeout = testlib.get_decorator(test_method, test, "timeout", 600)
+ nd = testlib.get_decorator(test_method, test, "nondestructive")
+ rwa = not testlib.get_decorator(test_method, test, "no_retry_when_changed")
+ todo = testlib.get_decorator(test_method, test, "todo")
+ if getattr(test.__class__, "provision", None):
+ # each additionally provisioned VM costs destructive test capacity
+ cost = len(test.__class__.provision)
+ else:
+ cost = 1
+ test = Test(test_id, build_command(filename, test_str, opts), test_timeout, nd, rwa, todo, cost=cost)
+ if nd:
+ nondestructive_tests.append(test)
+ else:
+ if not opts.nondestructive:
+ destructive_tests.append(test)
+ test_id += 1
+
+ # sort non destructive tests by class/test name, to avoid spurious errors where failures depend on the order of
+ # execution but let's make sure we always test them both ways around; hash the image name, which is
+ # robust, reproducible, and provides an even distribution of both directions
+ nondestructive_tests.sort(key=lambda t: t.command[-1], reverse=bool(binascii.crc32(image.encode()) & 1))
+
+ return (nondestructive_tests, destructive_tests, machine_class)
+
+
+def list_tests(opts):
+ test_files = glob.glob(os.path.join(opts.test_dir, opts.test_glob))
+ nondestructive_tests, destructive_tests, _ = detect_tests(test_files, "dummy", opts)
+ names = {t.command[-1] for t in nondestructive_tests + destructive_tests}
+ for n in sorted(names):
+ print(n)
+
+
+def run(opts, image):
+ fail_count = 0
+ start_time = time.time()
+
+ if opts.coverage:
+ prepare_for_code_coverage()
+
+ test_files = glob.glob(os.path.join(opts.test_dir, opts.test_glob))
+ changed_tests = get_affected_tests(opts.test_dir, opts.base, test_files)
+ nondestructive_tests, destructive_tests, machine_class = detect_tests(test_files, image, opts)
+ nondestructive_tests_len = len(nondestructive_tests)
+ destructive_tests_len = len(destructive_tests)
+
+ if opts.machine:
+ assert not destructive_tests
+
+ print(f"1..{nondestructive_tests_len + destructive_tests_len}")
+ flush_stdout()
+
+ running_tests = []
+ global_machines = []
+
+ if not opts.machine:
+ # Create appropriate number of nondestructive machines; prioritize the nondestructive tests, to get
+ # them out of the way as fast as possible, then let the destructive ones start as soon as
+ # a given nondestructive runner is done.
+ num_global = min(nondestructive_tests_len, opts.jobs)
+
+ for _ in range(num_global):
+ global_machines.append(GlobalMachine(restrict=not opts.enable_network, cpus=opts.nondestructive_cpus,
+ memory_mb=opts.nondestructive_memory_mb,
+ machine_class=machine_class or testvm.VirtMachine))
+
+ # test scheduling loop
+ while True:
+ made_progress = False
+
+ # mop up finished tests
+ logging.debug("test loop: %d running tests", len(running_tests))
+ for test in running_tests.copy():
+ poll_result = test.poll()
+ if poll_result is not None:
+ made_progress = True
+ running_tests.remove(test)
+ test_machine = test.machine_id # test_finish() resets it
+ retry_reason, test_result = test.finish(changed_tests, opts)
+ fail_count += test_result
+ logging.debug("test %s finished; result %s retry reason %s", test, test_result, retry_reason)
+
+ if test_machine is not None and not opts.machine:
+ # unassign from global machine
+ global_machines[test_machine].running_test = None
+
+ # sometimes our global machine gets messed up; also, tests that time out don't run cleanup handlers
+ # restart it to avoid an unbounded number of test retries and follow-up errors
+ if not opts.machine and (poll_result == 124 or (retry_reason and "test harness" in retry_reason)):
+ # try hard to keep the test output consistent
+ sys.stderr.write("\nRestarting global machine %s\n" % test_machine)
+ sys.stderr.flush()
+ global_machines[test_machine].reset()
+
+ # run again if needed
+ if retry_reason:
+ if test.nondestructive:
+ nondestructive_tests.insert(0, test)
+ else:
+ destructive_tests.insert(0, test)
+
+ if opts.machine:
+ if not running_tests and nondestructive_tests:
+ test = nondestructive_tests.pop(0)
+ logging.debug("Static machine is free, assigning next test %s", test)
+ test.assign_machine(-1, opts.machine, opts.browser)
+ test.start()
+ running_tests.append(test)
+ made_progress = True
+ else:
+ # find free global machines, and either assign a new non destructive test, or kill them to free resources
+ for (idx, machine) in enumerate(global_machines):
+ if machine.is_available():
+ if nondestructive_tests:
+ test = nondestructive_tests.pop(0)
+ logging.debug("Global machine %s is free, assigning next test %s", idx, test)
+ machine.running_test = test
+ test.assign_machine(idx, machine.ssh_address, machine.web_address)
+ test.start()
+ running_tests.append(test)
+ else:
+ logging.debug("Global machine %s is free, and no more non destructive tests; killing", idx)
+ machine.kill()
+
+ made_progress = True
+
+ def running_cost():
+ return sum(test.cost for test in running_tests)
+
+ # fill the remaining available job slots with destructive tests; run tests with a cost higher than #jobs by themselves
+ while destructive_tests and (running_cost() + destructive_tests[0].cost <= opts.jobs or len(running_tests) == 0):
+ test = destructive_tests.pop(0)
+ logging.debug("%d running tests with total cost %d, starting next destructive test %s",
+ len(running_tests), running_cost(), test)
+ test.start()
+ running_tests.append(test)
+ made_progress = True
+
+ # are we done?
+ if not running_tests:
+ assert not nondestructive_tests, f"nondestructive_tests should be empty: {[str(t) for t in nondestructive_tests]}"
+ assert not destructive_tests, f"destructive_tests should be empty: {[str(t) for t in destructive_tests]}"
+ break
+
+ # Sleep if we didn't make progress
+ if not made_progress:
+ time.sleep(0.5)
+
+ # Create coverage report
+ if opts.coverage:
+ create_coverage_report()
+
+ # print summary
+ duration = int(time.time() - start_time)
+ hostname = socket.gethostname().split(".")[0]
+
+ nondestructive_details = []
+ if not opts.machine:
+ for (idx, machine) in enumerate(global_machines):
+ nondestructive_details.append(f"{idx}: {machine.duration}s")
+
+ details = f"[{duration}s on {hostname}, {destructive_tests_len} destructive tests, {nondestructive_tests_len} nondestructive tests: {', '.join(nondestructive_details)}]"
+ print()
+ if fail_count > 0:
+ print(f"# {fail_count} TESTS FAILED {details}")
+ else:
+ print(f"# TESTS PASSED {details}")
+ flush_stdout()
+
+ return fail_count
+
+
+def main():
+ parser = testlib.arg_parser(enable_sit=False)
+ parser.add_argument('-j', '--jobs', type=int,
+ default=int(os.environ.get("TEST_JOBS", 1)), help="Number of concurrent jobs")
+ parser.add_argument('--thorough', action='store_true',
+ help='Thorough mode, no skipping known issues')
+ parser.add_argument('-n', '--nondestructive', action='store_true',
+ help='Only consider @nondestructive tests')
+ parser.add_argument('--machine', metavar="hostname[:port]",
+ default=None, help="Run tests against an already running machine; implies --nondestructive")
+ parser.add_argument('--browser', metavar="hostname[:port]",
+ default=None, help="When using --machine, use this cockpit web address")
+ parser.add_argument('--test-dir', default=os.environ.get("TEST_DIR", testvm.TEST_DIR),
+ help="Directory in which to glob for test files; default: %(default)s")
+ parser.add_argument('--test-glob', default="check-*",
+ help="Pattern with which to glob in the test directory; default: %(default)s")
+ parser.add_argument('--exclude', action="append", default=[], metavar="TestClass.testName",
+ help="Exclude test (exact match only); can be specified multiple times")
+ parser.add_argument('--nondestructive-cpus', type=int, default=None,
+ help="Number of CPUs for nondestructive test global machines")
+ parser.add_argument('--nondestructive-memory-mb', type=int, default=None,
+ help="RAM size for nondestructive test global machines")
+ parser.add_argument('--base', default=os.environ.get("BASE_BRANCH"),
+ help="Retry affected tests compared to given base branch; default: %(default)s")
+ parser.add_argument('--track-naughties', action='store_true',
+ help='Update the occurrence of naughties on cockpit-project/bots')
+ parser.add_argument('--no-retry-fail', action='store_true',
+ help="Don't retry failed tests")
+ opts = parser.parse_args()
+
+ if opts.machine:
+ if opts.jobs > 1:
+ parser.error("--machine cannot be used with concurrent jobs")
+ if not opts.browser:
+ parser.error("--browser must be specified together with --machine")
+ opts.nondestructive = True
+
+ # Tell any subprocesses what we are testing
+ if "TEST_REVISION" not in os.environ:
+ r = subprocess.run(["git", "rev-parse", "HEAD"],
+ universal_newlines=True, check=False, stdout=subprocess.PIPE)
+ if r.returncode == 0:
+ os.environ["TEST_REVISION"] = r.stdout.strip()
+
+ os.environ["TEST_BROWSER"] = os.environ.get("TEST_BROWSER", "chromium")
+
+ image = testvm.DEFAULT_IMAGE
+ testvm.DEFAULT_IMAGE = image
+ os.environ["TEST_OS"] = image
+
+ # Make sure tests can make relative imports
+ sys.path.append(os.path.realpath(opts.test_dir))
+
+ if opts.list:
+ list_tests(opts)
+ return 0
+
+ return run(opts, image)
+
+
+if __name__ == '__main__':
+ # logging.basicConfig(level=logging.DEBUG)
+ sys.exit(main())
diff --git a/test/common/storagelib.py b/test/common/storagelib.py
new file mode 100644
index 0000000..1602aa2
--- /dev/null
+++ b/test/common/storagelib.py
@@ -0,0 +1,669 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import os.path
+import re
+import textwrap
+
+from testlib import Error, MachineCase, wait
+
+
+def from_udisks_ascii(codepoints):
+ return ''.join(map(chr, codepoints[:-1]))
+
+
+class StorageHelpers:
+ """Mix-in class for using in tests that derive from something else than MachineCase or StorageCase"""
+
+ def inode(self, f):
+ return self.machine.execute("stat -L '%s' -c %%i" % f)
+
+ def retry(self, setup, check, teardown):
+ def step():
+ if setup:
+ setup()
+ if check():
+ return True
+ if teardown:
+ teardown()
+ return False
+
+ self.browser.wait(step)
+
+ def add_ram_disk(self, size=50):
+ """Add per-test RAM disk
+
+ The disk gets removed automatically when the test ends. This is safe for @nondestructive tests.
+
+ Return the device name.
+ """
+ # sanity test: should not yet be loaded
+ self.machine.execute("test ! -e /sys/module/scsi_debug")
+ self.machine.execute(f"modprobe scsi_debug dev_size_mb={size}")
+ dev = self.machine.execute('while true; do O=$(ls /sys/bus/pseudo/drivers/scsi_debug/adapter*/host*/target*/*:*/block 2>/dev/null || true); '
+ '[ -n "$O" ] && break || sleep 0.1; done; echo "/dev/$O"').strip()
+ # don't use addCleanup() here, this is often busy and needs to be cleaned up late; done in MachineCase.nonDestructiveSetup()
+
+ return dev
+
+ def add_loopback_disk(self, size=50, name=None):
+ """Add per-test loopback disk
+
+ The disk gets removed automatically when the test ends. This is safe for @nondestructive tests.
+
+ Unlike add_ram_disk(), this can be called multiple times, and
+ is less size constrained. The backing file starts out sparse,
+ so this can be used to create massive block devices, as long
+ as you are careful to not actually use much of it.
+
+ However, loopback devices look quite special to the OS, so
+ they are not a very good simulation of a "real" disk.
+
+ Return the device name.
+
+ """
+ # HACK: https://bugzilla.redhat.com/show_bug.cgi?id=1969408
+ # It would be nicer to remove $F immediately after the call to
+ # losetup, but that will break some versions of lvm2.
+ backf = self.machine.execute("mktemp /var/tmp/loop.XXXX").strip()
+ dev = self.machine.execute(f"truncate --size={size}MB {backf}; "
+ f"losetup --show {name if name else '--find'} {backf}").strip()
+ # If this device had partions in its last incarnation on this
+ # machine, they might come back for unknown reasons, in a
+ # non-functional state. Running partprobe will get rid of
+ # them.
+ self.machine.execute("partprobe '%s'" % dev)
+ # right after unmounting the device is often still busy, so retry a few times
+ self.addCleanup(self.machine.execute, f"until losetup -d {dev}; do sleep 1; done; rm {backf}", timeout=10)
+ self.addCleanup(self.machine.execute, f"findmnt -n -o TARGET {dev} | xargs --no-run-if-empty umount;")
+
+ return dev
+
+ def add_targetd_loopback_disk(self, index, size=50):
+ """Add per-test loopback device that can be forcefully removed.
+ """
+
+ m = self.machine
+ model = f"disk{index}"
+ wwn = f"naa.5000{index:012x}"
+
+ m.execute(f"rm -f /var/tmp/targetd.{model}")
+ m.execute(f"targetcli /backstores/fileio create name={model} size={size}M file_or_dev=/var/tmp/targetd.{model}")
+ m.execute(f"targetcli /loopback create {wwn}")
+ m.execute(f"targetcli /loopback/{wwn}/luns create /backstores/fileio/{model}")
+
+ self.addCleanup(m.execute, f"targetcli /loopback delete {wwn}")
+ self.addCleanup(m.execute, f"targetcli /backstores/fileio delete {model}")
+ self.addCleanup(m.execute, f"rm -f /var/tmp/targetd.{model}")
+
+ dev = m.execute(f'for dev in /sys/block/*; do if [ -f $dev/device/model ] && [ "$(cat $dev/device/model | tr -d [:space:])" == "{model}" ]; then echo /dev/$(basename $dev); fi; done').strip()
+ if dev == "":
+ raise Error("Device not found")
+ return dev
+
+ def force_remove_disk(self, device):
+ """Act like the given device gets physically removed.
+
+ This circumvents all the normal EBUSY failures, and thus can be used for testing
+ the cleanup after a forceful removal.
+ """
+ self.machine.execute(f'echo 1 > /sys/block/{os.path.basename(device)}/device/delete')
+ # the removal trips up PCP and our usage graphs
+ self.allow_browser_errors("direct: instance name lookup failed.*")
+
+ def addCleanupVG(self, vgname):
+ """Ensure the given VG is removed after the test"""
+
+ self.addCleanup(self.machine.execute, f"if [ -d /dev/{vgname} ]; then vgremove --force {vgname}; fi")
+
+ # Dialogs
+
+ def dialog_wait_open(self):
+ self.browser.wait_visible('#dialog')
+
+ def dialog_wait_alert(self, text):
+ self.browser.wait_in_text('#dialog .pf-v5-c-alert__title', text)
+
+ def dialog_wait_title(self, text):
+ self.browser.wait_in_text('#dialog .pf-v5-c-modal-box__title', text)
+
+ def dialog_field(self, field):
+ return f'#dialog [data-field="{field}"]'
+
+ def dialog_val(self, field):
+ sel = self.dialog_field(field)
+ ftype = self.browser.attr(sel, "data-field-type")
+ if ftype == "text-input-checked":
+ if self.browser.is_present(sel + " input[type=checkbox]:not(:checked)"):
+ return False
+ else:
+ return self.browser.val(sel + " input[type=text]")
+ elif ftype == "select":
+ return self.browser.attr(sel, "data-value")
+ else:
+ return self.browser.val(sel)
+
+ def dialog_set_val(self, field, val):
+ sel = self.dialog_field(field)
+ ftype = self.browser.attr(sel, "data-field-type")
+ if ftype == "checkbox":
+ self.browser.set_checked(sel, val)
+ elif ftype == "select-spaces":
+ for label in val:
+ self.browser.set_checked(f'{sel} :contains("{label}") input', val)
+ elif ftype == "size-slider":
+ self.browser.set_val(sel + " .size-unit select", "1000000")
+ self.browser.set_input_text(sel + " .size-text input", str(val))
+ elif ftype == "select":
+ self.browser._wait_present(sel + f" select option[value='{val}']:not([disabled])")
+ self.browser.set_val(sel + " select", val)
+ elif ftype == "select-radio":
+ self.browser.click(sel + f" input[data-data='{val}']")
+ elif ftype == "text-input":
+ self.browser.set_input_text(sel, val)
+ elif ftype == "text-input-checked":
+ if not val:
+ self.browser.set_checked(sel + " input[type=checkbox]", val=False)
+ else:
+ self.browser.set_checked(sel + " input[type=checkbox]", val=True)
+ self.browser.set_input_text(sel + " [type=text]", val)
+ elif ftype == "combobox":
+ self.browser.click(sel + " button.pf-v5-c-select__toggle-button")
+ self.browser.click(sel + f" .pf-v5-c-select__menu li:contains('{val}') button")
+ else:
+ self.browser.set_val(sel, val)
+
+ def dialog_combobox_choices(self, field):
+ return self.browser.call_js_func("""(function (sel) {
+ var lis = ph_find(sel).querySelectorAll('li');
+ var result = [];
+ for (i = 0; i < lis.length; ++i)
+ result.push(lis[i].textContent);
+ return result;
+ })""", self.dialog_field(field))
+
+ def dialog_is_present(self, field, label):
+ return self.browser.is_present(f'{self.dialog_field(field)} :contains("{label}") input')
+
+ def dialog_wait_val(self, field, val, unit=None):
+ if unit is None:
+ unit = "1000000"
+
+ sel = self.dialog_field(field)
+ ftype = self.browser.attr(sel, "data-field-type")
+ if ftype == "size-slider":
+ self.browser.wait_val(sel + " .size-unit select", unit)
+ self.browser.wait_val(sel + " .size-text input", str(val))
+ elif ftype == "select":
+ self.browser.wait_attr(sel, "data-value", val)
+ else:
+ self.browser.wait_val(sel, val)
+
+ def dialog_wait_error(self, field, val):
+ # XXX - allow for more than one error
+ self.browser.wait_in_text('#dialog .pf-v5-c-form__helper-text .pf-m-error', val)
+
+ def dialog_wait_not_present(self, field):
+ self.browser.wait_not_present(self.dialog_field(field))
+
+ def dialog_wait_apply_enabled(self):
+ self.browser.wait_attr('#dialog button.apply:nth-of-type(1)', "disabled", None)
+
+ def dialog_wait_apply_disabled(self):
+ self.browser.wait_visible('#dialog button.apply:nth-of-type(1)[disabled]')
+
+ def dialog_apply(self):
+ self.browser.click('#dialog button.apply:nth-of-type(1)')
+
+ def dialog_apply_secondary(self):
+ self.browser.click('#dialog button.apply:nth-of-type(2)')
+
+ def dialog_cancel(self):
+ self.browser.click('#dialog button.cancel')
+
+ def dialog_wait_close(self):
+ # file system operations often take longer than 10s
+ with self.browser.wait_timeout(max(self.browser.cdp.timeout, 60)):
+ self.browser.wait_not_present('#dialog')
+
+ def dialog_check(self, expect):
+ for f in expect:
+ if not self.dialog_val(f) == expect[f]:
+ return False
+ return True
+
+ def dialog_set_vals(self, values):
+ # Sometimes a certain field needs to be set before other
+ # fields come into existence and thus the order matters that
+ # we set the fields in. The tests however just give us a
+ # unordered 'dict'. Instead of changing the tests, we figure
+ # out the right order dynamically here by just setting what we
+ # can and then starting over. As long as we make progress in
+ # each iteration, everything is good.
+ failed = {}
+ last_error = Exception
+ for f in values:
+ try:
+ self.dialog_set_val(f, values[f])
+ except Error as e:
+ failed[f] = values[f]
+ last_error = e
+ if failed:
+ if len(failed) < len(values):
+ self.dialog_set_vals(failed)
+ else:
+ raise last_error
+
+ def dialog(self, values, expect=None, secondary=False):
+ if expect is None:
+ expect = {}
+ self.dialog_wait_open()
+ for f in expect:
+ self.dialog_wait_val(f, expect[f])
+ self.dialog_set_vals(values)
+ if secondary:
+ self.dialog_apply_secondary()
+ else:
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ def confirm(self):
+ self.dialog({})
+
+ # There is some asynchronous activity in the storage stack. (It
+ # used to be much worse, but it has improved over the years, yay!)
+ #
+ # The tests deal with that by waiting for the right conditions,
+ # which sometimes means opening a dialog a couple of times until
+ # it has the right contents, or applying it a couple of times
+ # until it works.
+
+ def dialog_open_with_retry(self, trigger, expect):
+ def setup():
+ trigger()
+ self.dialog_wait_open()
+
+ def check():
+ if callable(expect):
+ return expect()
+ else:
+ return self.dialog_check(expect)
+
+ def teardown():
+ self.dialog_cancel()
+ self.dialog_wait_close()
+ self.retry(setup, check, teardown)
+
+ def dialog_apply_with_retry(self, expected_errors=None):
+ def step():
+ try:
+ self.dialog_apply()
+ self.dialog_wait_close()
+ except Error:
+ if expected_errors is None:
+ return False
+ err = self.browser.text('#dialog')
+ print(err)
+ for exp in expected_errors:
+ if exp in err:
+ return False
+ raise
+ return True
+ self.browser.wait(step)
+
+ def dialog_with_retry(self, trigger, values, expect):
+ self.dialog_open_with_retry(trigger, expect)
+ if values:
+ for f in values:
+ self.dialog_set_val(f, values[f])
+ self.dialog_apply()
+ else:
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ def dialog_with_error_retry(self, trigger, errors, values=None, first_setup=None, retry_setup=None, setup=None):
+ def doit():
+ nonlocal first_setup
+ trigger()
+ self.dialog_wait_open()
+ if values:
+ self.dialog_set_vals(values)
+ if first_setup:
+ first_setup()
+ first_setup = None
+ elif retry_setup:
+ retry_setup()
+ elif setup:
+ setup()
+ self.dialog_apply()
+ try:
+ self.dialog_wait_close()
+ return True
+ except Exception:
+ dialog_text = self.browser.text('#dialog .pf-v5-c-alert__title')
+ for err in errors:
+ if err in dialog_text:
+ print("WARNING: retrying dialog")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+ return False
+ raise
+ self.browser.wait(doit)
+
+ def udisks_objects(self):
+ return json.loads(self.machine.execute(["python3", "-c", textwrap.dedent("""
+ import dbus, json
+ print(json.dumps(dbus.SystemBus().call_blocking(
+ "org.freedesktop.UDisks2",
+ "/org/freedesktop/UDisks2",
+ "org.freedesktop.DBus.ObjectManager",
+ "GetManagedObjects", "", [])))""")]))
+
+ def configuration_field(self, dev, tab, field):
+ managerObjects = self.udisks_objects()
+ for path in managerObjects:
+ if "org.freedesktop.UDisks2.Block" in managerObjects[path]:
+ iface = managerObjects[path]["org.freedesktop.UDisks2.Block"]
+ if from_udisks_ascii(iface["Device"]) == dev or from_udisks_ascii(iface["PreferredDevice"]) == dev:
+ for entry in iface["Configuration"]:
+ if entry[0] == tab:
+ if field in entry[1]:
+ print(f"{path}/{tab}/{field} = {from_udisks_ascii(entry[1][field])}")
+ return from_udisks_ascii(entry[1][field])
+ return ""
+
+ def assert_in_configuration(self, dev, tab, field, text):
+ self.assertIn(text, self.configuration_field(dev, tab, field))
+
+ def assert_not_in_configuration(self, dev, tab, field, text):
+ self.assertNotIn(text, self.configuration_field(dev, tab, field))
+
+ def child_configuration_field(self, dev, tab, field):
+ udisks_objects = self.udisks_objects()
+ for path in udisks_objects:
+ if "org.freedesktop.UDisks2.Encrypted" in udisks_objects[path]:
+ block_iface = udisks_objects[path]["org.freedesktop.UDisks2.Block"]
+ crypto_iface = udisks_objects[path]["org.freedesktop.UDisks2.Encrypted"]
+ if from_udisks_ascii(block_iface["Device"]) == dev or from_udisks_ascii(block_iface["PreferredDevice"]) == dev:
+ for entry in crypto_iface["ChildConfiguration"]:
+ if entry[0] == tab:
+ if field in entry[1]:
+ print("%s/child/%s/%s = %s" % (path, tab, field,
+ from_udisks_ascii(entry[1][field])))
+ return from_udisks_ascii(entry[1][field])
+ return ""
+
+ def assert_in_child_configuration(self, dev, tab, field, text):
+ self.assertIn(text, self.child_configuration_field(dev, tab, field))
+
+ def lvol_child_configuration_field(self, lvol, tab, field):
+ udisk_objects = self.udisks_objects()
+ for path in udisk_objects:
+ if "org.freedesktop.UDisks2.LogicalVolume" in udisk_objects[path]:
+ iface = udisk_objects[path]["org.freedesktop.UDisks2.LogicalVolume"]
+ if iface["Name"] == lvol:
+ for entry in iface["ChildConfiguration"]:
+ if entry[0] == tab:
+ if field in entry[1]:
+ print("%s/child/%s/%s = %s" % (path, tab, field,
+ from_udisks_ascii(entry[1][field])))
+ return from_udisks_ascii(entry[1][field])
+ return ""
+
+ def assert_in_lvol_child_configuration(self, lvol, tab, field, text):
+ self.assertIn(text, self.lvol_child_configuration_field(lvol, tab, field))
+
+ def setup_systemd_password_agent(self, password):
+ # This sets up a systemd password agent that replies to all
+ # queries with the given password.
+
+ self.write_file("/usr/local/bin/test-password-agent",
+ f"""#!/bin/sh
+# Sleep a bit to avoid starting this agent too quickly over and over,
+# and so that other agents get a chance as well.
+sleep 30
+
+for s in $(grep -h ^Socket= /run/systemd/ask-password/ask.* | sed 's/^Socket=//'); do
+ printf '%s' '{password}' | /usr/lib/systemd/systemd-reply-password 1 $s
+done
+""", perm="0755")
+
+ self.write_file("/etc/systemd/system/test-password-agent.service",
+ """
+[Unit]
+Description=Test Password Agent
+DefaultDependencies=no
+Conflicts=shutdown.target emergency.service
+Before=shutdown.target
+[Service]
+ExecStart=/usr/local/bin/test-password-agent
+""")
+
+ self.write_file("/etc/systemd/system/test-password-agent.path",
+ """
+[Unit]
+Description=Test Password Agent Directory Watch
+DefaultDependencies=no
+Conflicts=shutdown.target emergency.service
+Before=paths.target shutdown.target cryptsetup.target
+[Path]
+DirectoryNotEmpty=/run/systemd/ask-password
+MakeDirectory=yes
+""")
+ self.machine.execute("ln -s ../test-password-agent.path /etc/systemd/system/sysinit.target.wants/")
+
+ def encrypt_root(self, passphrase):
+ m = self.machine
+
+ # Set up a password agent in the old root and then arrange for
+ # it to be included in the initrd. This will unlock the new
+ # encrypted root during boot.
+ #
+ # The password agent and its initrd configuration will be
+ # copied to the new root, so it will stay in place also when
+ # the initrd is regenerated again from within the new root.
+
+ self.setup_systemd_password_agent(passphrase)
+ install_items = [
+ '/etc/systemd/system/sysinit.target.wants/test-password-agent.path',
+ '/etc/systemd/system/test-password-agent.path',
+ '/etc/systemd/system/test-password-agent.service',
+ '/usr/local/bin/test-password-agent',
+ ]
+ m.write("/etc/dracut.conf.d/01-askpass.conf",
+ f'install_items+=" {" ".join(install_items)} "')
+
+ # The first step is to move /boot to a new unencrypted
+ # partition on the new disk but keep it mounted at /boot.
+ # This helps when running grub2-install and grub2-mkconfig,
+ # which will look at /boot and do the right thing.
+ #
+ # Then we copy (most of) the old root to the new disk, into a
+ # logical volume sitting on top of a LUKS container.
+ #
+ # The kernel command line is changed to use the new root
+ # filesystem, and grub is installed on the new disk. The boot
+ # configuration of the VM has been changed to boot from the
+ # new disk.
+ #
+ # At that point the new root can be booted by the existing
+ # initrd, but the initrd will prompt for the passphrase (as
+ # expected). Thus, the initrd is regenerated to include the
+ # password agent from above.
+ #
+ # Before the reboot, we destroy the original disk to make
+ # really sure that it wont be used anymore.
+
+ info = m.add_disk("6G", serial="NEWROOT", boot_disk=True)
+ dev = "/dev/" + info["dev"]
+ wait(lambda: m.execute(f"test -b {dev} && echo present").strip() == "present")
+ m.execute(f"""
+set -x
+parted -s {dev} mktable msdos
+parted -s {dev} mkpart primary ext4 1M 500M
+parted -s {dev} mkpart primary ext4 500M 100%
+echo {passphrase} | cryptsetup luksFormat --pbkdf-memory=300 {dev}2
+luks_uuid=$(blkid -p {dev}2 -s UUID -o value)
+echo {passphrase} | cryptsetup luksOpen --pbkdf-memory=300 {dev}2 luks-$luks_uuid
+vgcreate root /dev/mapper/luks-$luks_uuid
+lvcreate root -n root -l100%VG
+mkfs.ext4 /dev/root/root
+mkdir /new-root
+mount /dev/root/root /new-root
+mkfs.ext4 {dev}1
+# don't move the EFI partition
+if mountpoint /boot/efi; then umount /boot/efi; fi
+mkdir /new-root/boot
+mount {dev}1 /new-root/boot
+tar --selinux --one-file-system -cf - --exclude /boot --exclude='/var/tmp/*' --exclude='/var/cache/*' \
+ --exclude='/var/lib/mock/*' --exclude='/var/lib/containers/*' --exclude='/new-root/*' \
+ / | tar --selinux -C /new-root -xf -
+tar --one-file-system -C /boot -cf - . | tar -C /new-root/boot -xf -
+umount /new-root/boot
+mount {dev}1 /boot
+echo "(hd0) {dev}" >/boot/grub2/device.map
+sed -i -e 's,/boot/,/,' /boot/loader/entries/*
+uuid=$(blkid -p /dev/root/root -s UUID -o value)
+buuid=$(blkid -p {dev}1 -s UUID -o value)
+echo "UUID=$uuid / auto defaults 0 0" >/new-root/etc/fstab
+echo "UUID=$buuid /boot auto defaults 0 0" >>/new-root/etc/fstab
+dracut --regenerate-all --force
+grub2-install {dev}
+( # HACK - grub2-mkconfig messes with /boot/loader/entries/ and /etc/kernel/cmdline
+ mv /boot/loader/entries /boot/loader/entries.stowed
+ ! test -f /etc/kernel/cmdline || mv /etc/kernel/cmdline /etc/kernel/cmdline.stowed
+ grub2-mkconfig -o /boot/grub2/grub.cfg
+ mv /boot/loader/entries.stowed /boot/loader/entries
+ ! test -f /etc/kernel/cmdline.stowed || mv /etc/kernel/cmdline.stowed /etc/kernel/cmdline
+)
+grubby --update-kernel=ALL --args="root=UUID=$uuid rootflags=defaults rd.luks.uuid=$luks_uuid rd.lvm.lv=root/root"
+! test -f /etc/kernel/cmdline || cp /etc/kernel/cmdline /new-root/etc/kernel/cmdline
+""", timeout=300)
+ m.spawn("dd if=/dev/zero of=/dev/vda bs=1M count=100; reboot", "reboot", check=False)
+ m.wait_reboot(300)
+ self.assertEqual(m.execute("findmnt -n -o SOURCE /").strip(), "/dev/mapper/root-root")
+
+ # Cards and tables
+
+ def card(self, title):
+ return f"[data-test-card-title='{title}']"
+
+ def card_parent_link(self):
+ return ".pf-v5-c-breadcrumb__item:nth-last-child(2) > a"
+
+ def card_header(self, title):
+ return self.card(title) + " .pf-v5-c-card__header"
+
+ def card_row(self, title, index=None, name=None, location=None):
+ if index is not None:
+ return self.card(title) + f" tbody tr:nth-child({index})"
+ elif name is not None:
+ name = name.replace("/dev/", "")
+ return self.card(title) + f" tbody [data-test-row-name='{name}']"
+ else:
+ return self.card(title) + f" tbody [data-test-row-location='{location}']"
+
+ def click_card_row(self, title, index=None, name=None, location=None):
+ # We need to click on a <td> element since that's where the handlers are...
+ self.browser.click(self.card_row(title, index, name, location) + " td:nth-child(1)")
+
+ def card_row_col(self, title, row_index=None, col_index=None, row_name=None, row_location=None):
+ return self.card_row(title, row_index, row_name, row_location) + f" td:nth-child({col_index})"
+
+ def card_desc(self, card_title, desc_title):
+ return self.card(card_title) + f" [data-test-desc-title='{desc_title}'] [data-test-value=true]"
+
+ def card_desc_action(self, card_title, desc_title):
+ return self.card(card_title) + f" [data-test-desc-title='{desc_title}'] [data-test-action=true] button"
+
+ def card_button(self, card_title, button_title):
+ return self.card(card_title) + f" button:contains('{button_title}')"
+
+ def dropdown_toggle(self, parent):
+ return parent + " .pf-v5-c-menu-toggle"
+
+ def dropdown_action(self, parent, title):
+ return parent + f" .pf-v5-c-menu button:contains('{title}')"
+
+ def dropdown_description(self, parent, title):
+ return parent + f" .pf-v5-c-menu button:contains('{title}') .pf-v5-c-menu__item-description"
+
+ def click_dropdown(self, parent, title):
+ self.browser.click(self.dropdown_toggle(parent))
+ self.browser.click(self.dropdown_action(parent, title))
+
+ def click_card_dropdown(self, card_title, button_title):
+ self.click_dropdown(self.card_header(card_title), button_title)
+
+ def click_devices_dropdown(self, title):
+ self.click_card_dropdown("Storage", title)
+
+ def check_dropdown_action_disabled(self, parent, title, expected_text):
+ self.browser.click(self.dropdown_toggle(parent))
+ self.browser.wait_visible(self.dropdown_action(parent, title) + "[disabled]")
+ self.browser.wait_text(self.dropdown_description(parent, title), expected_text)
+ self.browser.click(self.dropdown_toggle(parent))
+
+ def wait_mounted(self, card_title):
+ with self.browser.wait_timeout(30):
+ self.browser.wait_not_in_text(self.card_desc(card_title, "Mount point"),
+ "The filesystem is not mounted.")
+
+ def wait_not_mounted(self, card_title):
+ with self.browser.wait_timeout(30):
+ self.browser.wait_in_text(self.card_desc(card_title, "Mount point"),
+ "The filesystem is not mounted.")
+
+ def wait_card_button_disabled(self, card_title, button_title):
+ with self.browser.wait_timeout(30):
+ self.browser.wait_visible(self.card_button(card_title, button_title) + ":disabled")
+
+
+class StorageCase(MachineCase, StorageHelpers):
+
+ def setUp(self):
+
+ if self.image in ["fedora-coreos", "rhel4edge"]:
+ self.skipTest("No udisks/cockpit-storaged on OSTree images")
+
+ super().setUp()
+
+ ver = self.machine.execute("busctl --system get-property org.freedesktop.UDisks2 /org/freedesktop/UDisks2/Manager org.freedesktop.UDisks2.Manager Version || true")
+ m = re.match('s "(.*)"', ver)
+ if m:
+ self.storaged_version = list(map(int, m.group(1).split(".")))
+ else:
+ self.storaged_version = [0]
+
+ crypto_types = self.machine.execute("busctl --system get-property org.freedesktop.UDisks2 /org/freedesktop/UDisks2/Manager org.freedesktop.UDisks2.Manager SupportedEncryptionTypes || true")
+ if "luks2" in crypto_types:
+ self.default_crypto_type = "luks2"
+ else:
+ self.default_crypto_type = "luks1"
+
+ if self.image.startswith("rhel-8") or self.image.startswith("centos-8"):
+ # HACK: missing /etc/crypttab file upsets udisks: https://github.com/storaged-project/udisks/pull/835
+ self.machine.write("/etc/crypttab", "")
+
+ # starting out with empty PCP logs and pmlogger not running causes these metrics channel messages
+ self.allow_journal_messages("pcp-archive: no such metric: disk.*")
+
+ # UDisks2 invalidates the Size property and cockpit-bridge
+ # gets it immediately. But sometimes the interface is already
+ # gone.
+ self.allow_journal_messages("org.freedesktop.UDisks2: couldn't get property org.freedesktop.UDisks2.Filesystem Size .* No such interface.*")
diff --git a/test/common/tap-cdp b/test/common/tap-cdp
new file mode 100755
index 0000000..a70d530
--- /dev/null
+++ b/test/common/tap-cdp
@@ -0,0 +1,119 @@
+#!/usr/bin/python3
+
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+#
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+
+import cdp
+
+tap_line_re = re.compile(r'^(ok [0-9]+|not ok [0-9]+|bail out!|[0-9]+\.\.[0-9]+|# )', re.IGNORECASE)
+
+parser = argparse.ArgumentParser(description="A CDP driver for QUnit which outputs TAP")
+parser.add_argument("server", help="path to the test-server and the test page to run", nargs=argparse.REMAINDER)
+
+# Strip prefix from url
+# We need this to compensate for automake test generation behavior:
+# The tests are called with the path (relative to the build directory) of the testfile,
+# but from the build directory. Some tests make assumptions regarding the structure of the
+# filename. In order to make sure that they receive the same name, regardless of actual
+# build directory location, we need to strip that prefix (path from build to source directory)
+# from the filename
+parser.add_argument("--strip", dest="strip", help="strip prefix from test file paths")
+
+opts = parser.parse_args()
+
+# argparse sometimes forgets to remove this on argparse.REMAINDER args
+if opts.server[0] == '--':
+ opts.server = opts.server[1:]
+
+# The test file is the last argument, but 'server' might contain arbitrary
+# amount of options. We cannot express this with argparse, so take it apart
+# manually.
+opts.test = opts.server[-1]
+opts.server = opts.server[:-1]
+
+if opts.strip and opts.test.startswith(opts.strip):
+ opts.test = opts.test[len(opts.strip):]
+
+cdp = cdp.CDP("C.utf8")
+
+try:
+ cdp.browser.path(cdp.show_browser)
+except SystemError:
+ print('1..0 # skip web browser not found')
+ sys.exit(0)
+
+# pass the address through a separate fd, so that we can see g_debug() messages (which go to stdout)
+(addr_r, addr_w) = os.pipe()
+env = os.environ.copy()
+env["TEST_SERVER_ADDRESS_FD"] = str(addr_w)
+
+server = subprocess.Popen(opts.server,
+ stdin=subprocess.DEVNULL,
+ pass_fds=(addr_w,),
+ close_fds=True,
+ env=env)
+os.close(addr_w)
+address = os.read(addr_r, 1000).decode()
+os.close(addr_r)
+
+cdp.invoke("Page.navigate", url=address + '/' + opts.test)
+
+success = True
+ignore_resource_errors = False
+
+for t, message in cdp.read_log():
+
+ # fail on browser level errors
+ if t == 'cdp':
+ if message['level'] == "error":
+ if ignore_resource_errors and "Failed to load resource" in message["text"]:
+ continue
+ success = False
+ break
+ else:
+ continue
+
+ if message == 'cockpittest-tap-done':
+ break
+ elif message == 'cockpittest-tap-error':
+ success = False
+ break
+ elif message == 'cockpittest-tap-expect-resource-error':
+ ignore_resource_errors = True
+ continue
+
+ # TAP lines go to stdout, everything else to stderr
+ if tap_line_re.match(message):
+ if message.startswith('not ok'):
+ success = False
+ print(message)
+ else:
+ print(message, file=sys.stderr)
+
+
+server.terminate()
+server.wait()
+cdp.kill()
+
+if not success:
+ sys.exit(1)
diff --git a/test/common/test-functions.js b/test/common/test-functions.js
new file mode 100644
index 0000000..c2c28bd
--- /dev/null
+++ b/test/common/test-functions.js
@@ -0,0 +1,360 @@
+/* eslint no-unused-vars: 0 */
+
+/*
+ * These are routines used by our testing code.
+ *
+ * jQuery is not necessarily present. Don't rely on it
+ * for routine operations.
+ */
+
+function ph_select(sel) {
+ if (!window.Sizzle) {
+ return Array.from(document.querySelectorAll(sel));
+ }
+
+ if (sel.includes(":contains(")) {
+ if (!window.Sizzle) {
+ throw new Error("Using ':contains' when window.Sizzle is not available.");
+ }
+ return window.Sizzle(sel);
+ } else {
+ return Array.from(document.querySelectorAll(sel));
+ }
+}
+
+function ph_only(els, sel) {
+ if (els.length === 0)
+ throw new Error(sel + " not found");
+ if (els.length > 1)
+ throw new Error(sel + " is ambiguous");
+ return els[0];
+}
+
+function ph_find (sel) {
+ const els = ph_select(sel);
+ return ph_only(els, sel);
+}
+
+function ph_count(sel) {
+ const els = ph_select(sel);
+ return els.length;
+}
+
+function ph_count_check(sel, expected_num) {
+ return (ph_count(sel) == expected_num);
+}
+
+function ph_val (sel) {
+ const el = ph_find(sel);
+ if (el.value === undefined)
+ throw new Error(sel + " does not have a value");
+ return el.value;
+}
+
+function ph_set_val (sel, val) {
+ const el = ph_find(sel);
+ if (el.value === undefined)
+ throw new Error(sel + " does not have a value");
+ el.value = val;
+ const ev = new Event("change", { bubbles: true, cancelable: false });
+ el.dispatchEvent(ev);
+}
+
+function ph_has_val (sel, val) {
+ return ph_val(sel) == val;
+}
+
+function ph_collected_text_is (sel, val) {
+ const els = ph_select(sel);
+ const rest = els.map(el => {
+ if (el.textContent === undefined)
+ throw new Error(sel + " can not have text");
+ return el.textContent.replaceAll("\xa0", " ");
+ }).join("");
+ return rest === val;
+}
+
+function ph_text (sel) {
+ const el = ph_find(sel);
+ if (el.textContent === undefined)
+ throw new Error(sel + " can not have text");
+ // 0xa0 is a non-breakable space, which is a rendering detail of Chromium
+ // and awkward to handle in tests; turn it into normal spaces
+ return el.textContent.replaceAll("\xa0", " ");
+}
+
+function ph_attr (sel, attr) {
+ return ph_find(sel).getAttribute(attr);
+}
+
+function ph_set_attr (sel, attr, val) {
+ const el = ph_find(sel);
+ if (val === null || val === undefined)
+ el.removeAttribute(attr);
+ else
+ el.setAttribute(attr, val);
+
+ const ev = new Event("change", { bubbles: true, cancelable: false });
+ el.dispatchEvent(ev);
+}
+
+function ph_has_attr (sel, attr, val) {
+ return ph_attr(sel, attr) == val;
+}
+
+function ph_attr_contains (sel, attr, val) {
+ const a = ph_attr(sel, attr);
+ return a && a.indexOf(val) > -1;
+}
+
+function ph_mouse(sel, type, x, y, btn, ctrlKey, shiftKey, altKey, metaKey) {
+ const el = ph_find(sel);
+
+ /* The element has to be visible, and not collapsed */
+ if (el.offsetWidth <= 0 && el.offsetHeight <= 0 && el.tagName != 'svg')
+ throw new Error(sel + " is not visible");
+
+ /* The event has to actually work */
+ let processed = false;
+ function handler() {
+ processed = true;
+ }
+
+ el.addEventListener(type, handler, true);
+
+ let elp = el;
+ let left = elp.offsetLeft || 0;
+ let top = elp.offsetTop || 0;
+ while (elp.offsetParent) {
+ elp = elp.offsetParent;
+ left += elp.offsetLeft;
+ top += elp.offsetTop;
+ }
+
+ let detail = 0;
+ if (["click", "mousedown", "mouseup"].indexOf(type) > -1)
+ detail = 1;
+ else if (type === "dblclick")
+ detail = 2;
+
+ const ev = new MouseEvent(type, {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ detail,
+ screenX: left + x,
+ screenY: top + y,
+ clientX: left + x,
+ clientY: top + y,
+ button: btn,
+ ctrlKey: ctrlKey || false,
+ shiftKey: shiftKey || false,
+ altKey: altKey || false,
+ metaKey: metaKey || false
+ });
+
+ el.dispatchEvent(ev);
+
+ el.removeEventListener(type, handler, true);
+
+ /* It really had to work */
+ if (!processed)
+ throw new Error(sel + " is disabled or somehow doesn't process events");
+}
+
+function ph_get_checked (sel) {
+ const el = ph_find(sel);
+ if (el.checked === undefined)
+ throw new Error(sel + " is not checkable");
+
+ return el.checked;
+}
+
+function ph_set_checked (sel, val) {
+ const el = ph_find(sel);
+ if (el.checked === undefined)
+ throw new Error(sel + " is not checkable");
+
+ if (el.checked != val)
+ ph_mouse(sel, "click", 0, 0, 0);
+}
+
+function ph_is_visible (sel) {
+ const el = ph_find(sel);
+ return el.tagName == "svg" || ((el.offsetWidth > 0 || el.offsetHeight > 0) && !(el.style.visibility == "hidden" || el.style.display == "none"));
+}
+
+function ph_is_present(sel) {
+ const els = ph_select(sel);
+ return els.length > 0;
+}
+
+function ph_in_text (sel, text) {
+ return ph_text(sel).indexOf(text) != -1;
+}
+
+function ph_text_is (sel, text) {
+ return ph_text(sel) == text;
+}
+
+function ph_text_matches (sel, pattern) {
+ return ph_text(sel).match(pattern);
+}
+
+function ph_go(href) {
+ if (href.indexOf("#") === 0) {
+ window.location.hash = href;
+ } else {
+ if (window.name.indexOf("cockpit1") !== 0)
+ throw new Error("ph_go() called in non cockpit window");
+ const control = {
+ command: "jump",
+ location: href
+ };
+ window.parent.postMessage("\n" + JSON.stringify(control), "*");
+ }
+}
+
+function ph_focus(sel) {
+ ph_find(sel).focus();
+}
+
+function ph_scrollIntoViewIfNeeded(sel) {
+ ph_find(sel).scrollIntoViewIfNeeded();
+}
+
+function ph_blur(sel) {
+ ph_find(sel).blur();
+}
+
+function ph_blur_active() {
+ const elt = window.document.activeElement;
+ if (elt)
+ elt.blur();
+}
+
+class PhWaitCondTimeout extends Error {
+ constructor(description) {
+ if (description && description.apply)
+ description = description.apply();
+ if (description)
+ super(description);
+ else
+ super("condition did not become true");
+ }
+}
+
+function ph_wait_cond(cond, timeout, error_description) {
+ return new Promise((resolve, reject) => {
+ // poll every 100 ms for now; FIXME: poll less often and re-check on mutations using
+ // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
+ let stepTimer = null;
+ let last_err = null;
+ const tm = window.setTimeout(() => {
+ if (stepTimer)
+ window.clearTimeout(stepTimer);
+ reject(last_err || new PhWaitCondTimeout(error_description));
+ }, timeout);
+ function step() {
+ try {
+ if (cond()) {
+ window.clearTimeout(tm);
+ resolve();
+ return;
+ }
+ } catch (err) {
+ last_err = err;
+ }
+ stepTimer = window.setTimeout(step, 100);
+ }
+ step();
+ });
+}
+
+function currentFrameAbsolutePosition() {
+ let currentWindow = window;
+ let currentParentWindow;
+ const positions = [];
+ let rect;
+
+ while (currentWindow !== window.top) {
+ currentParentWindow = currentWindow.parent;
+ for (let idx = 0; idx < currentParentWindow.frames.length; idx++)
+ if (currentParentWindow.frames[idx] === currentWindow) {
+ for (const frameElement of currentParentWindow.document.getElementsByTagName('iframe')) {
+ if (frameElement.contentWindow === currentWindow) {
+ rect = frameElement.getBoundingClientRect();
+ positions.push({ x: rect.x, y: rect.y });
+ }
+ }
+ currentWindow = currentParentWindow;
+ break;
+ }
+ }
+
+ return positions.reduce((accumulator, currentValue) => {
+ return {
+ x: accumulator.x + currentValue.x,
+ y: accumulator.y + currentValue.y
+ };
+ }, { x: 0, y: 0 });
+}
+
+function flatten(array_of_arrays) {
+ if (array_of_arrays.length > 0)
+ return Array.prototype.concat.apply([], array_of_arrays);
+ else
+ return [];
+}
+
+function ph_selector_clips(sels) {
+ const f = currentFrameAbsolutePosition();
+ const elts = flatten(sels.map(ph_select));
+ return elts.map(e => {
+ const r = e.getBoundingClientRect();
+ return { x: r.x + f.x, y: r.y + f.y, width: r.width, height: r.height, scale: 1 };
+ });
+}
+
+function ph_element_clip(sel) {
+ ph_find(sel); // just to make sure it is not ambiguous
+ return ph_selector_clips([sel])[0];
+}
+
+function ph_count_animations(sel) {
+ return ph_find(sel).getAnimations({ subtree: true }).length;
+}
+
+function ph_set_texts(new_texts) {
+ for (const sel in new_texts) {
+ const elts = ph_select(sel);
+ if (elts.length == 0)
+ throw new Error(sel + " not found");
+ for (let elt of elts) {
+ // We have to be careful to not replace any actual nodes
+ // in the DOM since that would cause React to fail later
+ // when it tries to remove some of its nodes that are no
+ // longer in the DOM. This means that setting the
+ // "textContent" property is out, for example.
+ //
+ // Instead, we insist on finding an actual "Text" node
+ // that we then modify. If the given selector results in
+ // elements that have other elements in them, we refuse to
+ // mock them.
+ //
+ // However, for convenience, this function digs into
+ // elements that have exactly one other child element.
+ while (elt.children.length == 1)
+ elt = elt.children[0];
+ if (elt.children.length != 0)
+ throw new Error(sel + " can not be mocked since it contains more than text");
+ let subst = new_texts[sel];
+ for (const n of elt.childNodes) {
+ if (n.nodeType == 3) { // 3 == TEXT
+ n.data = subst;
+ subst = "";
+ }
+ }
+ }
+ }
+}
diff --git a/test/common/testlib.py b/test/common/testlib.py
new file mode 100644
index 0000000..152e046
--- /dev/null
+++ b/test/common/testlib.py
@@ -0,0 +1,2531 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+"""Tools for writing Cockpit test cases."""
+
+import argparse
+import base64
+import errno
+import fnmatch
+import functools
+import glob
+import io
+import json
+import os
+import re
+import shutil
+import socket
+import subprocess
+import sys
+import tempfile
+import time
+import traceback
+import unittest
+from time import sleep
+from typing import Any, Callable, Dict, List, Optional, Union
+
+import cdp
+import testvm
+from lcov import write_lcov
+from lib.constants import OSTREE_IMAGES
+
+try:
+ from PIL import Image
+except ImportError:
+ Image = None
+
+BASE_DIR = os.path.realpath(f'{__file__}/../../..')
+TEST_DIR = f'{BASE_DIR}/test'
+BOTS_DIR = f'{BASE_DIR}/bots'
+
+os.environ["PATH"] = "{0}:{1}:{2}".format(os.environ.get("PATH"), BOTS_DIR, TEST_DIR)
+
+# Be careful when changing this string, check in cockpit-project/bots where it is being used
+UNEXPECTED_MESSAGE = "FAIL: Test completed, but found unexpected "
+PIXEL_TEST_MESSAGE = "Some pixel tests have failed"
+
+__all__ = (
+ # Test definitions
+ 'test_main',
+ 'arg_parser',
+ 'Browser',
+ 'MachineCase',
+ 'nondestructive',
+ 'no_retry_when_changed',
+ 'onlyImage',
+ 'skipImage',
+ 'skipDistroPackage',
+ 'skipOstree',
+ 'skipBrowser',
+ 'todo',
+ 'todoPybridge',
+ 'todoPybridgeRHEL8',
+ 'timeout',
+ 'Error',
+
+ 'sit',
+ 'wait',
+ 'opts',
+ 'TEST_DIR',
+ 'UNEXPECTED_MESSAGE',
+ 'PIXEL_TEST_MESSAGE'
+)
+
+# Command line options
+opts = argparse.Namespace()
+opts.sit = False
+opts.trace = False
+opts.attachments = None
+opts.revision = None
+opts.address = None
+opts.jobs = 1
+opts.fetch = True
+opts.coverage = False
+
+# Browser layouts
+#
+# A browser can be switched into a number of different layouts, such
+# as "desktop" and "mobile". A default set of layouts is defined
+# here, but projects can override this with a file called
+# "test/browser-layouts.json".
+#
+# Each layout defines the size of the shell (where the main navigation
+# is) and also the size of the content iframe (where the actual page
+# like "Networking" or "Overview" is displayed).
+#
+# When the browser layout is switched (by calling Browset.set_layout),
+# this will either set the shell size or the content size, depending
+# on which frame is current (as set by Browser.enter_page or
+# Browser.leave_page).
+#
+# This makes sure that pixel tests for the whole content iframe are
+# always the exact size as specified in the layout definition, and
+# don't change size when the navigation stuff in the shell changes.
+#
+# The browser starts out in the first layout of this list, which is
+# "desktop" by default.
+
+default_layouts = [
+ {
+ "name": "desktop",
+ "theme": "light",
+ "shell_size": [1920, 1200],
+ "content_size": [1680, 1130]
+ },
+ {
+ "name": "medium",
+ "theme": "light",
+ "is_mobile": False,
+ "shell_size": [1280, 768],
+ "content_size": [1040, 698]
+ },
+ {
+ "name": "mobile",
+ "theme": "light",
+ "shell_size": [414, 1920],
+ "content_size": [414, 1856]
+ },
+ {
+ "name": "dark",
+ "theme": "dark",
+ "shell_size": [1920, 1200],
+ "content_size": [1680, 1130]
+ },
+ {
+ "name": "rtl",
+ "theme": "light",
+ "shell_size": [1920, 1200],
+ "content_size": [1680, 1130]
+ },
+]
+
+
+def attach(filename: str, move: bool = False):
+ """Put a file into the attachments directory.
+
+ :param filename: file to put in attachments directory
+ :param move: set this to true to move dynamically generated files which
+ are not touched by destructive tests. (default False)
+ """
+ if not opts.attachments:
+ return
+ dest = os.path.join(opts.attachments, os.path.basename(filename))
+ if os.path.exists(filename) and not os.path.exists(dest):
+ if move:
+ shutil.move(filename, dest)
+ else:
+ shutil.copy(filename, dest)
+
+
+def unique_filename(base, ext):
+ for i in range(20):
+ if i == 0:
+ f = f"{base}.{ext}"
+ else:
+ f = f"{base}-{i}.{ext}"
+ if not os.path.exists(f):
+ return f
+ return f"{base}.{ext}"
+
+
+class Browser:
+ def __init__(self, address, label, machine, pixels_label=None, coverage_label=None, port=None):
+ if ":" in address:
+ self.address, _, self.port = address.rpartition(":")
+ else:
+ self.address = address
+ self.port = 9090
+ if port is not None:
+ self.port = port
+ self.default_user = "admin"
+ self.label = label
+ self.pixels_label = pixels_label
+ self.used_pixel_references = set()
+ self.coverage_label = coverage_label
+ self.machine = machine
+ path = os.path.dirname(__file__)
+ sizzle_js = os.path.join(path, "../../node_modules/sizzle/dist/sizzle.js")
+ helpers = [os.path.join(path, "test-functions.js")]
+ if os.path.exists(sizzle_js):
+ helpers.append(sizzle_js)
+ self.cdp = cdp.CDP("C.utf8", verbose=opts.trace, trace=opts.trace,
+ inject_helpers=helpers,
+ start_profile=coverage_label is not None)
+ self.password = "foobar"
+ self.timeout_factor = int(os.getenv("TEST_TIMEOUT_FACTOR", "1"))
+ self.failed_pixel_tests = 0
+ self.allow_oops = False
+ self.body_clip = None
+ try:
+ with open(f'{TEST_DIR}/browser-layouts.json') as fp:
+ self.layouts = json.load(fp)
+ except FileNotFoundError:
+ self.layouts = default_layouts
+ # Firefox CDP does not support setting EmulatedMedia
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1549434
+ if self.cdp.browser.name != "chromium":
+ self.layouts = [layout for layout in self.layouts if layout["theme"] != "dark"]
+ self.current_layout = None
+
+ def allow_download(self) -> None:
+ """Allow browser downloads"""
+ if self.cdp.browser.name == "chromium":
+ self.cdp.invoke("Page.setDownloadBehavior", behavior="allow", downloadPath=self.cdp.download_dir)
+
+ def open(self, href: str, cookie: Optional[Dict[str, str]] = None, tls: bool = False):
+ """Load a page into the browser.
+
+ :param href: the path of the Cockpit page to load, such as "/users". Either PAGE or URL needs to be given.
+ :param cookie: a dictionary object representing a cookie.
+ :param tls: load the page using https (default False)
+
+ Raises:
+ Error: When a timeout occurs waiting for the page to load.
+ """
+ if href.startswith("/"):
+ schema = tls and "https" or "http"
+ href = "%s://%s:%s%s" % (schema, self.address, self.port, href)
+
+ if not self.current_layout and os.environ.get("TEST_SHOW_BROWSER") in [None, "pixels"]:
+ self.current_layout = self.layouts[0]
+ size = self.current_layout["shell_size"]
+ self._set_window_size(size[0], size[1])
+ if cookie:
+ self.cdp.invoke("Network.setCookie", **cookie)
+
+ self.switch_to_top()
+ opts = {}
+ if self.cdp.browser.name == "firefox":
+ # by default, Firefox optimizes this away if the current and the given href URL
+ # are the same (Like in TestKeys.testAuthorizedKeys).
+ # Force a reload in this case, to make tests and the waitPageLoad below predictable
+ # But that option has the inverse effect with Chromium (argh)
+ opts["transitionType"] = "reload"
+ elif self.cdp.browser.name == 'chromium':
+ # Chromium also optimizes this away, but doesn't have a knob to force loading
+ # so load the blank page first
+ self.cdp.invoke("Page.navigate", url="about:blank")
+ self.cdp.invoke("waitPageLoad", timeout=5)
+ self.cdp.invoke("Page.navigate", url=href, **opts)
+ self.cdp.invoke("waitPageLoad", timeout=self.cdp.timeout)
+
+ def set_user_agent(self, ua: str):
+ """Set the user agent of the browser
+
+ :param ua: user agent string
+ :type ua: str
+ """
+ self.cdp.invoke("Emulation.setUserAgentOverride", userAgent=ua)
+
+ def reload(self, ignore_cache: bool = False):
+ """Reload the current page
+
+ :param ignore_cache: if true browser cache is ignored (default False)
+ :type ignore_cache: bool
+ """
+
+ self.switch_to_top()
+ self.wait_js_cond("ph_select('iframe.container-frame').every(function (e) { return e.getAttribute('data-loaded'); })")
+ self.cdp.invoke("reloadPageAndWait", ignoreCache=ignore_cache)
+
+ self.machine.allow_restart_journal_messages()
+
+ def switch_to_frame(self, name: str):
+ """Switch to frame in browser tab
+
+ Each page has a main frame and can have multiple subframes, usually
+ iframes.
+
+ :param name: frame name
+ """
+ self.cdp.set_frame(name)
+
+ def switch_to_top(self):
+ """Switch to the main frame
+
+ Switch to the main frame from for example an iframe.
+ """
+ self.cdp.set_frame(None)
+
+ def upload_file(self, selector: str, file: str):
+ r = self.cdp.invoke("Runtime.evaluate", expression='document.querySelector(%s)' % jsquote(selector))
+ objectId = r["result"]["objectId"]
+ self.cdp.invoke("DOM.setFileInputFiles", files=[file], objectId=objectId)
+
+ def raise_cdp_exception(self, func, arg, details, trailer=None):
+ # unwrap a typical error string
+ if details.get("exception", {}).get("type") == "string":
+ msg = details["exception"]["value"]
+ elif details.get("text", None):
+ msg = details.get("text", None)
+ else:
+ msg = str(details)
+ if trailer:
+ msg += "\n" + trailer
+ raise Error("%s(%s): %s" % (func, arg, msg))
+
+ def inject_js(self, code: str):
+ """Execute JS code that does not return anything
+
+ :param code: a string containing JavaScript code
+ :type code: str
+ """
+ self.cdp.invoke("Runtime.evaluate", expression=code, trace=code,
+ silent=False, awaitPromise=True, returnByValue=False, no_trace=True)
+
+ def eval_js(self, code: str, no_trace: bool = False) -> Optional[Any]:
+ """Execute JS code that returns something
+
+ :param code: a string containing JavaScript code
+ :param no_trace: do not print information about unknown return values (default False)
+ """
+ result = self.cdp.invoke("Runtime.evaluate", expression=code, trace=code,
+ silent=False, awaitPromise=True, returnByValue=True, no_trace=no_trace)
+ if "exceptionDetails" in result:
+ self.raise_cdp_exception("eval_js", code, result["exceptionDetails"])
+ _type = result.get("result", {}).get("type")
+ if _type == 'object' and result["result"].get("subtype", "") == "error":
+ raise Error(result["result"]["description"])
+ if _type == "undefined":
+ return None
+ if _type and "value" in result["result"]:
+ return result["result"]["value"]
+
+ if opts.trace:
+ print("eval_js(%s): cannot interpret return value %s" % (code, result))
+ return None
+
+ def call_js_func(self, func: str, *args: Any) -> Optional[Any]:
+ """Call a JavaScript function
+
+ :param func: JavaScript function to call
+ :param args: arguments for the JavaScript function
+ """
+ return self.eval_js("%s(%s)" % (func, ','.join(map(jsquote, args))))
+
+ def set_mock(self, mock: Dict[str, str], base: Optional[str] = ""):
+ """Replace some DOM elements with mock text
+
+ The 'mock' parameter is a dictionary from CSS selectors to the
+ text that the elements matching the selector should be
+ replaced with.
+
+ XXX - There is no way to easily undo the effects of this
+ function. There is no coordination with React. This
+ will improve as necessary.
+
+ :param mock: the mock data, see above
+ :param base: if given, all selectors are relative to this one
+ """
+ self.call_js_func('ph_set_texts', {base + " " + k: v for k, v in mock.items()})
+
+ def cookie(self, name: str):
+ """Retrieve a browser cookie by name
+
+ :param name: the name of the cookie
+ :type name: str
+ """
+ cookies = self.cdp.invoke("Network.getCookies")
+ for c in cookies["cookies"]:
+ if c["name"] == name:
+ return c
+ return None
+
+ def go(self, url_hash: str):
+ self.call_js_func('ph_go', url_hash)
+
+ def mouse(self, selector: str, event: str, x: int = 0, y: int = 0, btn: int = 0, ctrlKey: bool = False, shiftKey: bool = False, altKey: bool = False, metaKey: bool = False):
+ """Simulate a browser mouse event
+
+ :param selector: the element to interact with
+ :param type: the mouse event to simulate, for example mouseenter, mouseleave, mousemove, click
+ :param x: the x coordinate
+ :param y: the y coordinate
+ :param btn: mouse button to click https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
+ :param crtlKey: press the ctrl key
+ :param shiftKey: press the shift key
+ :param altKey: press the alt key
+ :param metaKey: press the meta key
+ """
+ self.wait_visible(selector)
+ self.call_js_func('ph_mouse', selector, event, x, y, btn, ctrlKey, shiftKey, altKey, metaKey)
+
+ def click(self, selector: str):
+ """Click on a ui element
+
+ :param selector: the selector to click on
+ """
+ self.mouse(selector + ":not([disabled]):not([aria-disabled=true])", "click", 0, 0, 0)
+
+ def val(self, selector: str):
+ """Get the value attribute of a selector.
+
+ :param selector: the selector to get the value of
+ """
+ self.wait_visible(selector)
+ return self.call_js_func('ph_val', selector)
+
+ def set_val(self, selector: str, val):
+ """Set the value attribute of a non disabled DOM element.
+
+ This also emits a change DOM change event.
+
+ :param selector: the selector to set the value of
+ :param val: the value to set
+ """
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ self.call_js_func('ph_set_val', selector, val)
+
+ def text(self, selector: str):
+ """Get an element's textContent value.
+
+ :param selector: the selector to get the value of
+ """
+ self.wait_visible(selector)
+ return self.call_js_func('ph_text', selector)
+
+ def attr(self, selector: str, attr):
+ """Get the value of a given attribute of an element.
+
+ :param selector: the selector to get the attribute of
+ :param attr: the DOM element attribute
+ """
+ self._wait_present(selector)
+ return self.call_js_func('ph_attr', selector, attr)
+
+ def set_attr(self, selector, attr, val):
+ """Set an attribute value of an element.
+
+ :param selector: the selector
+ :param attr: the element attribute
+ :param val: the value of the attribute
+ """
+ self._wait_present(selector + ':not([disabled]):not([aria-disabled=true])')
+ self.call_js_func('ph_set_attr', selector, attr, val)
+
+ def get_checked(self, selector: str):
+ """Get checked state of a given selector.
+
+ :param selector: the selector
+ :return: the checked state
+ """
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ return self.call_js_func('ph_get_checked', selector)
+
+ def set_checked(self, selector: str, val):
+ """Set checked state of a given selector.
+
+ :param selector: the selector
+ :param val: boolean value to enable or disable checkbox
+ """
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ self.call_js_func('ph_set_checked', selector, val)
+
+ def focus(self, selector: str):
+ """Set focus on selected element.
+
+ :param selector: the selector
+ """
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ self.call_js_func('ph_focus', selector)
+
+ def blur(self, selector: str):
+ """Remove keyboard focus from selected element.
+
+ :param selector: the selector
+ """
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ self.call_js_func('ph_blur', selector)
+
+ # TODO: Unify them so we can have only one
+ def key_press(self, keys: str, modifiers: int = 0, use_ord: bool = False):
+ if self.cdp.browser.name == "chromium":
+ self._key_press_chromium(keys, modifiers, use_ord)
+ else:
+ self._key_press_firefox(keys, modifiers, use_ord)
+
+ def _key_press_chromium(self, keys: str, modifiers: int = 0, use_ord=False):
+ for key in keys:
+ args = {"type": "keyDown", "modifiers": modifiers}
+
+ # If modifiers are used we need to pass windowsVirtualKeyCode which is
+ # basically the asci decimal representation of the key
+ args["text"] = key
+ if use_ord:
+ args["windowsVirtualKeyCode"] = ord(key)
+ elif (not key.isalnum() and ord(key) < 32) or modifiers != 0:
+ args["windowsVirtualKeyCode"] = ord(key.upper())
+ else:
+ args["key"] = key
+
+ self.cdp.invoke("Input.dispatchKeyEvent", **args)
+ args["type"] = "keyUp"
+ self.cdp.invoke("Input.dispatchKeyEvent", **args)
+
+ def _key_press_firefox(self, keys: str, modifiers: int = 0, use_ord: bool = False):
+ # https://python-reference.readthedocs.io/en/latest/docs/str/ASCII.html
+ # Both line feed and carriage return are normalized to Enter (https://html.spec.whatwg.org/multipage/form-elements.html)
+ keyMap = {
+ 8: "Backspace", # Backspace key
+ 9: "Tab", # Tab key
+ 10: "Enter", # Enter key (normalized from line feed)
+ 13: "Enter", # Enter key (normalized from carriage return)
+ 27: "Escape", # Escape key
+ 37: "ArrowLeft", # Arrow key left
+ 40: "ArrowDown", # Arrow key down
+ 45: "Insert", # Insert key
+ }
+ for key in keys:
+ args = {"type": "keyDown", "modifiers": modifiers}
+
+ args["key"] = key
+ if ord(key) < 32 or use_ord:
+ args["key"] = keyMap[ord(key)]
+
+ self.cdp.invoke("Input.dispatchKeyEvent", **args)
+ args["type"] = "keyUp"
+ self.cdp.invoke("Input.dispatchKeyEvent", **args)
+
+ def select_from_dropdown(self, selector: str, value):
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ text_selector = f"{selector} option[value='{value}']"
+ self._wait_present(text_selector)
+ self.set_val(selector, value)
+ self.wait_val(selector, value)
+
+ def select_PF4(self, selector: str, value):
+ self.click(f"{selector}:not([disabled]):not([aria-disabled=true])")
+ select_entry = f"{selector} + ul button:contains('{value}')"
+ self.click(select_entry)
+ if self.is_present(f"{selector}.pf-m-typeahead"):
+ self.wait_val(f"{selector} > div input[type=text]", value)
+ else:
+ self.wait_text(f"{selector} .pf-v5-c-select__toggle-text", value)
+
+ def set_input_text(self, selector: str, val: str, append: bool = False, value_check: bool = True, blur: bool = True):
+ self.focus(selector)
+ if not append:
+ self.key_press("a", 2) # Ctrl + a
+ if val == "":
+ self.key_press("\b") # Backspace
+ else:
+ self.key_press(val)
+ if blur:
+ self.blur(selector)
+
+ if value_check:
+ self.wait_val(selector, val)
+
+ def set_file_autocomplete_val(self, group_identifier: str, location: str):
+ self.set_input_text(f"{group_identifier} .pf-v5-c-select__toggle-typeahead input", location)
+ # click away the selection list, to force a state update
+ self.click(f"{group_identifier} .pf-v5-c-select__toggle-typeahead")
+ self.wait_not_present(f"{group_identifier} .pf-v5-c-select__menu")
+
+ def wait_timeout(self, timeout: int):
+ browser = self
+
+ class WaitParamsRestorer():
+ def __init__(self, timeout):
+ self.timeout = timeout
+
+ def __enter__(self):
+ pass
+
+ def __exit__(self, type_, value, traceback):
+ browser.cdp.timeout = self.timeout
+ r = WaitParamsRestorer(self.cdp.timeout)
+ self.cdp.timeout = timeout
+ return r
+
+ def wait(self, predicate: Callable):
+ for _ in range(self.cdp.timeout * self.timeout_factor * 5):
+ val = predicate()
+ if val:
+ return val
+ time.sleep(0.2)
+ raise Error('timed out waiting for predicate to become true')
+
+ def wait_js_cond(self, cond: str, error_description: str = "null"):
+ count = 0
+ timeout = self.cdp.timeout * self.timeout_factor
+ start = time.time()
+ while True:
+ count += 1
+ try:
+ result = self.cdp.invoke("Runtime.evaluate",
+ expression="ph_wait_cond(() => %s, %i, %s)" % (cond, timeout * 1000, error_description),
+ silent=False, awaitPromise=True, trace="wait: " + cond)
+ if "exceptionDetails" in result:
+ if self.cdp.browser.name == "firefox" and count < 20 and "ph_wait_cond is not defined" in result["exceptionDetails"].get("text", ""):
+ time.sleep(0.1)
+ continue
+ trailer = "\n".join(self.cdp.get_js_log())
+ self.raise_cdp_exception("timeout\nwait_js_cond", cond, result["exceptionDetails"], trailer)
+ if timeout > 0:
+ duration = time.time() - start
+ percent = int(duration / timeout * 100)
+ if percent >= 50:
+ print(f"WARNING: Waiting for {cond} took {duration:.1f} seconds, which is {percent}% of the timeout.")
+ return
+ except RuntimeError as e:
+ data = e.args[0]
+ if count < 20 and isinstance(data, dict) and "response" in data and data["response"].get("message") in ["Execution context was destroyed.", "Cannot find context with specified id"]:
+ time.sleep(1)
+ else:
+ raise e
+
+ def wait_js_func(self, func: str, *args: Any):
+ self.wait_js_cond("%s(%s)" % (func, ','.join(map(jsquote, args))))
+
+ def is_present(self, selector: str) -> Optional[bool]:
+ return self.call_js_func('ph_is_present', selector)
+
+ def _wait_present(self, selector: str):
+ self.wait_js_func('ph_is_present', selector)
+
+ def wait_not_present(self, selector: str):
+ self.wait_js_func('!ph_is_present', selector)
+
+ def is_visible(self, selector: str) -> Optional[bool]:
+ return self.call_js_func('ph_is_visible', selector)
+
+ def wait_visible(self, selector: str):
+ self._wait_present(selector)
+ self.wait_js_func('ph_is_visible', selector)
+
+ def wait_val(self, selector: str, val: str):
+ self.wait_visible(selector)
+ self.wait_js_func('ph_has_val', selector, val)
+
+ def wait_not_val(self, selector: str, val: str):
+ self.wait_visible(selector)
+ self.wait_js_func('!ph_has_val', selector, val)
+
+ def wait_attr(self, selector, attr, val):
+ self._wait_present(selector)
+ self.wait_js_func('ph_has_attr', selector, attr, val)
+
+ def wait_attr_contains(self, selector, attr, val):
+ self._wait_present(selector)
+ self.wait_js_func('ph_attr_contains', selector, attr, val)
+
+ def wait_attr_not_contains(self, selector, attr, val):
+ self._wait_present(selector)
+ self.wait_js_func('!ph_attr_contains', selector, attr, val)
+
+ def wait_not_attr(self, selector, attr, val):
+ self._wait_present(selector)
+ self.wait_js_func('!ph_has_attr', selector, attr, val)
+
+ def wait_not_visible(self, selector: str):
+ self.wait_js_func('!ph_is_visible', selector)
+
+ def wait_in_text(self, selector: str, text: str):
+ self.wait_visible(selector)
+ self.wait_js_cond("ph_in_text(%s,%s)" % (jsquote(selector), jsquote(text)),
+ error_description="() => 'actual text: ' + ph_text(%s)" % jsquote(selector))
+
+ def wait_not_in_text(self, selector: str, text: str):
+ self.wait_visible(selector)
+ self.wait_js_func('!ph_in_text', selector, text)
+
+ def wait_collected_text(self, selector: str, text: str):
+ self.wait_js_func('ph_collected_text_is', selector, text)
+
+ def wait_text(self, selector: str, text: str):
+ self.wait_visible(selector)
+ self.wait_js_cond("ph_text_is(%s,%s)" % (jsquote(selector), jsquote(text)),
+ error_description="() => 'actual text: ' + ph_text(%s)" % jsquote(selector))
+
+ def wait_text_not(self, selector: str, text: str):
+ self.wait_visible(selector)
+ self.wait_js_func('!ph_text_is', selector, text)
+
+ def wait_text_matches(self, selector: str, pattern: str):
+ self.wait_visible(selector)
+ self.wait_js_func('ph_text_matches', selector, pattern)
+
+ def wait_popup(self, elem_id: str):
+ """Wait for a popup to open.
+
+ :param id: the 'id' attribute of the popup.
+ """
+ self.wait_visible('#' + elem_id)
+
+ def wait_popdown(self, elem_id: str):
+ """Wait for a popup to close.
+
+ :param id: the 'id' attribute of the popup.
+ """
+ self.wait_not_visible('#' + elem_id)
+
+ def wait_language(self, lang: str):
+ parts = lang.split("-")
+ code_1 = parts[0]
+ code_2 = parts[0]
+ if len(parts) > 1:
+ code_2 += "_" + parts[1].upper()
+ self.wait_js_cond("cockpit.language == '%s' || cockpit.language == '%s'" % (code_1, code_2))
+
+ def dialog_cancel(self, sel: str, button: str = "button[data-dismiss='modal']"):
+ self.click(sel + " " + button)
+ self.wait_not_visible(sel)
+
+ def enter_page(self, path: str, host: Optional[str] = None, reconnect: bool = True):
+ """Wait for a page to become current.
+
+ :param path: The identifier the page. This is a string starting with "/"
+ :type path: str
+ :param host: The host to connect too
+ :type host: str
+ :param reconnect: Try to reconnect
+ :type reconnect: bool
+ """
+ assert path.startswith("/")
+ if host:
+ frame = host + path
+ else:
+ frame = "localhost" + path
+ frame = "cockpit1:" + frame
+
+ self.switch_to_top()
+
+ while True:
+ try:
+ self._wait_present("iframe.container-frame[name='%s'][data-loaded]" % frame)
+ self.wait_not_visible(".curtains-ct")
+ self.wait_visible("iframe.container-frame[name='%s']" % frame)
+ break
+ except Error as ex:
+ if reconnect and ex.msg.startswith('timeout'):
+ reconnect = False
+ if self.is_present("#machine-reconnect"):
+ self.click("#machine-reconnect")
+ self.wait_not_visible(".curtains-ct")
+ continue
+ raise
+
+ self.switch_to_frame(frame)
+ self._wait_present("body")
+ self.wait_visible("body")
+
+ def leave_page(self):
+ self.switch_to_top()
+
+ def try_login(self, user: Optional[str] = None, password: Optional[str] = None, superuser: Optional[bool] = True, legacy_authorized: Optional[bool] = None):
+ """Fills in the login dialog and clicks the button.
+
+ This differs from login_and_go() by not expecting any particular result.
+
+ :param user: the username to login with
+ :type user: str
+ :param password: the password of the user
+ :type password: str
+ :param superuser: determines whether the new session will try to get Administrative Access (default true)
+ :type superuser: bool
+ :param legacy_authorized: old versions of the login dialog that still
+ have the "[ ] Reuse my password for magic things" checkbox. Such a
+ dialog is encountered when testing against old bastion hosts, for
+ example.
+ """
+ if user is None:
+ user = self.default_user
+ if password is None:
+ password = self.password
+ self.wait_visible("#login")
+ self.set_val('#login-user-input', user)
+ self.set_val('#login-password-input', password)
+ if legacy_authorized is not None:
+ self.set_checked('#authorized-input', legacy_authorized)
+ if superuser is not None:
+ self.eval_js('window.localStorage.setItem("superuser:%s", "%s");' % (user, "any" if superuser else "none"))
+ self.click('#login-button')
+
+ def login_and_go(self, path: Optional[str] = None, user: Optional[str] = None, host: Optional[str] = None,
+ superuser: bool = True, urlroot: Optional[str] = None, tls: bool = False, password: Optional[str] = None,
+ legacy_authorized: Optional[bool] = None):
+ """Fills in the login dialog, clicks the button and navigates to the given path
+
+ :param user: the username to login with
+ :type user: str
+ :param password: the password of the user
+ :type password: str
+ :param superuser: determines whether the new session will try to get Administrative Access (default true)
+ :type superuser: bool
+ :param legacy_authorized: old versions of the login dialog that still
+ have the "[ ] Reuse my password for magic things" checkbox. Such a
+ dialog is encountered when testing against old bastion hosts, for
+ example.
+ """
+ href = path
+ if not href:
+ href = "/"
+ if urlroot:
+ href = urlroot + href
+ if host:
+ href = "/@" + host + href
+ self.open(href, tls=tls)
+
+ self.try_login(user, password, superuser=superuser, legacy_authorized=legacy_authorized)
+
+ self._wait_present('#content')
+ self.wait_visible('#content')
+ if path:
+ self.enter_page(path.split("#")[0], host=host)
+
+ def logout(self):
+ self.assert_no_oops()
+ self.switch_to_top()
+
+ self.wait_visible("#toggle-menu")
+ if self.is_present("button#machine-reconnect") and self.is_visible("button#machine-reconnect"):
+ # happens when shutting down cockpit or rebooting machine
+ self.click("button#machine-reconnect")
+ else:
+ # happens when cockpit is still running
+ self.open_session_menu()
+ try:
+ self.click('#logout')
+ except RuntimeError as e:
+ # logging out does destroy the current frame context, it races with the CDP driver finishing the command
+ if "Execution context was destroyed" not in str(e):
+ raise
+ self.wait_visible('#login')
+
+ self.machine.allow_restart_journal_messages()
+
+ def relogin(self, path: Optional[str] = None, user: Optional[str] = None, password: Optional[str] = None,
+ superuser: Optional[bool] = None, wait_remote_session_machine: Optional[testvm.Machine] = None):
+ self.logout()
+ if wait_remote_session_machine:
+ wait_remote_session_machine.execute("while pgrep -a cockpit-ssh; do sleep 1; done")
+ self.try_login(user, password=password, superuser=superuser)
+ self._wait_present('#content')
+ self.wait_visible('#content')
+ if path:
+ if path.startswith("/@"):
+ host = path[2:].split("/")[0]
+ else:
+ host = None
+ self.enter_page(path.split("#")[0], host=host)
+
+ def open_session_menu(self):
+ self.wait_visible("#toggle-menu")
+ if (self.attr("#toggle-menu", "aria-expanded") != "true"):
+ self.click("#toggle-menu")
+
+ def layout_is_mobile(self):
+ return self.current_layout and self.current_layout["shell_size"][0] < 420
+
+ def open_superuser_dialog(self):
+ if self.layout_is_mobile():
+ self.open_session_menu()
+ self.click("#super-user-indicator-mobile button")
+ else:
+ self.click("#super-user-indicator button")
+
+ def check_superuser_indicator(self, expected: str):
+ if self.layout_is_mobile():
+ self.open_session_menu()
+ self.wait_text("#super-user-indicator-mobile", expected)
+ self.click("#toggle-menu")
+ else:
+ self.wait_text("#super-user-indicator", expected)
+
+ def become_superuser(self, user: Optional[str] = None, password: Optional[str] = None, passwordless: Optional[bool] = False):
+ cur_frame = self.cdp.cur_frame
+ self.switch_to_top()
+
+ self.open_superuser_dialog()
+
+ if passwordless:
+ self.wait_in_text("div[role=dialog]:contains('Administrative access')", "You now have administrative access.")
+ self.click("div[role=dialog] button:contains('Close')")
+ self.wait_not_present("div[role=dialog]:contains('You now have administrative access.')")
+ else:
+ self.wait_in_text("div[role=dialog]:contains('Switch to administrative access')", f"Password for {user or 'admin'}:")
+ self.set_input_text("div[role=dialog]:contains('Switch to administrative access') input", password or "foobar")
+ self.click("div[role=dialog] button:contains('Authenticate')")
+ self.wait_not_present("div[role=dialog]:contains('Switch to administrative access')")
+
+ self.check_superuser_indicator("Administrative access")
+ self.switch_to_frame(cur_frame)
+
+ def drop_superuser(self):
+ cur_frame = self.cdp.cur_frame
+ self.switch_to_top()
+
+ self.open_superuser_dialog()
+ self.click("div[role=dialog]:contains('Switch to limited access') button:contains('Limit access')")
+ self.wait_not_present("div[role=dialog]:contains('Switch to limited access')")
+ self.check_superuser_indicator("Limited access")
+
+ self.switch_to_frame(cur_frame)
+
+ def click_system_menu(self, path: str, enter: bool = True):
+ """Click on a "System" menu entry with given URL path
+
+ Enters the given target frame afterwards, unless enter=False is given
+ (useful for remote hosts).
+ """
+ self.switch_to_top()
+ self.click(f"#host-apps a[href='{path}']")
+ if enter:
+ # strip off parameters after hash
+ self.enter_page(path.split('#')[0].rstrip('/'))
+
+ def get_pf_progress_value(self, progress_bar_sel):
+ """Get numeric value of a PatternFly <ProgressBar> component"""
+ sel = progress_bar_sel + " .pf-v5-c-progress__indicator"
+ self.wait_visible(sel)
+ self.wait_attr_contains(sel, "style", "width:")
+ style = self.attr(sel, "style")
+ m = re.search(r"width: (\d+)%;", style)
+ return int(m.group(1))
+
+ def ignore_ssl_certificate_errors(self, ignore: bool):
+ action = ignore and "continue" or "cancel"
+ if opts.trace:
+ print("-> Setting SSL certificate error policy to %s" % action)
+ self.cdp.command(f"setSSLBadCertificateAction('{action}')")
+
+ def grant_permissions(self, *args: str):
+ """Grant permissions to the browser"""
+ # https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-grantPermissions
+ self.cdp.invoke("Browser.grantPermissions",
+ origin="http://%s:%s" % (self.address, self.port),
+ permissions=args)
+
+ def snapshot(self, title: str, label: Optional[str] = None):
+ """Take a snapshot of the current screen and save it as a PNG and HTML.
+
+ Arguments:
+ title: Used for the filename.
+ """
+ if self.cdp and self.cdp.valid:
+ self.cdp.command("clearExceptions()")
+
+ filename = unique_filename(f"{label or self.label}-{title}", "png")
+ if self.body_clip:
+ ret = self.cdp.invoke("Page.captureScreenshot", clip=self.body_clip, no_trace=True)
+ else:
+ ret = self.cdp.invoke("Page.captureScreenshot", no_trace=True)
+ if "data" in ret:
+ with open(filename, 'wb') as f:
+ f.write(base64.standard_b64decode(ret["data"]))
+ attach(filename, move=True)
+ print("Wrote screenshot to " + filename)
+ else:
+ print("Screenshot not available")
+
+ filename = unique_filename(f"{label or self.label}-{title}", "html")
+ html = self.cdp.invoke("Runtime.evaluate", expression="document.documentElement.outerHTML",
+ no_trace=True)["result"]["value"]
+ with open(filename, 'wb') as f:
+ f.write(html.encode('UTF-8'))
+ attach(filename, move=True)
+ print("Wrote HTML dump to " + filename)
+
+ def _set_window_size(self, width: int, height: int):
+ self.cdp.invoke("Emulation.setDeviceMetricsOverride",
+ width=width, height=height,
+ deviceScaleFactor=0, mobile=False)
+
+ def _set_emulated_media_theme(self, name: str):
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1549434
+ if self.cdp.browser.name == "chromium":
+ self.cdp.invoke("Emulation.setEmulatedMedia", features=[{'name': 'prefers-color-scheme', 'value': name}])
+
+ def _set_direction(self, direction: str):
+ cur_frame = self.cdp.cur_frame
+ if self.is_present("#shell-page"):
+ self.switch_to_top()
+ self.set_attr("#shell-page", "dir", direction)
+ self.switch_to_frame(cur_frame)
+ self.set_attr("html", "dir", direction)
+
+ def set_layout(self, name: str):
+ layout = next(lo for lo in self.layouts if lo["name"] == name)
+ if layout != self.current_layout:
+ self.current_layout = layout
+ size = layout["shell_size"]
+ self._set_window_size(size[0], size[1])
+ self._adjust_window_for_fixed_content_size()
+ self._set_emulated_media_theme(layout["theme"])
+
+ def _adjust_window_for_fixed_content_size(self):
+ if self.eval_js("window.name").startswith("cockpit1:"):
+ # Adjust the window size further so that the content is
+ # exactly the expected size. This will make sure that
+ # pixel tests of the content will not be affected by
+ # changes in shell navigation elements around it. It is
+ # important that we do this only after getting the shell
+ # into about the right size so that it switches into the
+ # right layout mode.
+ shell_size = self.current_layout["shell_size"]
+ want_size = self.current_layout["content_size"]
+ have_size = self.eval_js("[ document.body.offsetWidth, document.body.offsetHeight ]")
+ delta = (want_size[0] - have_size[0], want_size[1] - have_size[1])
+ if delta[0] != 0 or delta[1] != 0:
+ self._set_window_size(shell_size[0] + delta[0], shell_size[1] + delta[1])
+
+ def assert_pixels_in_current_layout(self, selector: str, key: str,
+ ignore: Optional[List[str]] = None,
+ mock: Optional[Dict[str, str]] = None,
+ sit_after_mock: bool = False,
+ scroll_into_view: Optional[str] = None,
+ wait_animations: bool = True,
+ wait_delay: float = 0.5):
+ """Compare the given element with its reference in the current layout"""
+
+ if ignore is None:
+ ignore = []
+
+ if not (Image and self.pixels_label):
+ return
+
+ self._adjust_window_for_fixed_content_size()
+ self.call_js_func('ph_scrollIntoViewIfNeeded', scroll_into_view or selector)
+ self.call_js_func('ph_blur_active')
+
+ # Wait for all animations to be over. This is done by
+ # counting them all over and over again until there are zero.
+ # Calling `.finish()` on all animations would miss those that
+ # are created while we wait, and would also fail with an
+ # exception if any unlimited animations are present, like
+ # spinners.
+ #
+ # There is another complication with tooltips. They are shown
+ # on top of certain elements, but are not DOM children of
+ # these elements. Also, Patternfly sometimes creates tooltips
+ # on dialog titles that are too long for the dialog, but only
+ # a little bit after the dialog has appeared.
+ #
+ # We don't want to predict whether tooltips will appear, and
+ # thus we can't wait for them to be present before waiting for
+ # their fade-in animation to be over.
+ #
+ # But we know that tooltips fade in within 300ms, so we just
+ # wait half a second to and side-step all that complexity.
+
+ if wait_animations:
+ time.sleep(wait_delay)
+ self.wait_js_cond('ph_count_animations(%s) == 0' % jsquote(selector))
+
+ if mock is not None:
+ self.set_mock(mock, base=selector)
+ if sit_after_mock:
+ sit()
+
+ rect = self.call_js_func('ph_element_clip', selector)
+
+ def relative_clips(sels):
+ return [(
+ r['x'] - rect['x'],
+ r['y'] - rect['y'],
+ r['x'] - rect['x'] + r['width'],
+ r['y'] - rect['y'] + r['height'])
+ for r in self.call_js_func('ph_selector_clips', sels)]
+
+ reference_dir = os.path.join(TEST_DIR, 'reference')
+ if not os.path.exists(os.path.join(reference_dir, '.git')):
+ raise SystemError("Pixel test references are missing, please run: test/common/pixel-tests pull")
+
+ ignore_rects = relative_clips([f"{selector} {item}" for item in ignore])
+ base = self.pixels_label + "-" + key
+ if self.current_layout != self.layouts[0]:
+ base += "-" + self.current_layout["name"]
+ filename = base + "-pixels.png"
+ ref_filename = os.path.join(reference_dir, filename)
+ self.used_pixel_references.add(ref_filename)
+ ret = self.cdp.invoke("Page.captureScreenshot", clip=rect, no_trace=True)
+ png_now = base64.standard_b64decode(ret["data"])
+ png_ref = os.path.exists(ref_filename) and open(ref_filename, "rb").read()
+ if not png_ref:
+ with open(filename, 'wb') as f:
+ f.write(png_now)
+ attach(filename, move=True)
+ print("New pixel test reference " + filename)
+ self.failed_pixel_tests += 1
+ else:
+ img_now = Image.open(io.BytesIO(png_now)).convert("RGBA")
+ img_ref = Image.open(io.BytesIO(png_ref)).convert("RGBA")
+ img_delta = Image.new("RGBA",
+ (max(img_now.size[0], img_ref.size[0]), max(img_now.size[1], img_ref.size[1])),
+ (255, 0, 0, 255))
+
+ # The current snapshot and the reference don't need to
+ # be perfectly identical. They might differ in the
+ # following ways:
+ #
+ # - A pixel in the reference image might be
+ # transparent. These pixels are ignored.
+ #
+ # - The call to assert_pixels specifies a list of
+ # rectangles (via CSS selectors). Pixels within those
+ # rectangles (and slightly outside) are ignored. Pixels
+ # just outside the rectangles are also ignored to avoid
+ # issues with rounding coordinates.
+ #
+ # - The RGB values of pixels can differ by up to 2.
+ #
+ # - There can be up to 20 different pixels
+ #
+ # Pixels that are different but have been ignored are
+ # marked in the delta image in green.
+
+ def masked(ref):
+ return ref[3] != 255
+
+ def ignorable_coord(x, y):
+ for (x0, y0, x1, y1) in ignore_rects:
+ if x >= x0 - 2 and x < x1 + 2 and y >= y0 - 2 and y < y1 + 2:
+ return True
+ return False
+
+ def ignorable_change(a, b):
+ return abs(a[0] - b[0]) <= 2 and abs(a[1] - b[1]) <= 2 and abs(a[1] - b[1]) <= 2
+
+ def img_eq(ref, now, delta):
+ # This is slow but exactly what we want.
+ # ImageMath might be able to speed this up.
+ data_ref = ref.load()
+ data_now = now.load()
+ data_delta = delta.load()
+ result = True
+ count = 0
+ width, height = delta.size
+ for y in range(height):
+ for x in range(width):
+ if x >= ref.size[0] or x >= now.size[0] or y >= ref.size[1] or y >= now.size[1]:
+ result = False
+ elif data_ref[x, y] != data_now[x, y]:
+ if masked(data_ref[x, y]) or ignorable_coord(x, y) or ignorable_change(data_ref[x, y], data_now[x, y]):
+ data_delta[x, y] = (0, 255, 0, 255)
+ else:
+ data_delta[x, y] = (255, 0, 0, 255)
+ count += 1
+ if count > 20:
+ result = False
+ else:
+ data_delta[x, y] = data_ref[x, y]
+ return result
+
+ if not img_eq(img_ref, img_now, img_delta):
+ if img_now.size == img_ref.size:
+ # Preserve alpha channel so that the 'now'
+ # image can be used as the new reference image
+ # without further changes
+ img_now.putalpha(img_ref.getchannel("A"))
+ img_now.save(filename)
+ attach(filename, move=True)
+ ref_filename_for_attach = base + "-reference.png"
+ img_ref.save(ref_filename_for_attach)
+ attach(ref_filename_for_attach, move=True)
+ delta_filename = base + "-delta.png"
+ img_delta.save(delta_filename)
+ attach(delta_filename, move=True)
+ print("Differences in pixel test " + base)
+ self.failed_pixel_tests += 1
+
+ def assert_pixels(self, selector: str, key: str,
+ ignore: Optional[List[str]] = None,
+ mock: Optional[Dict[str, str]] = None,
+ sit_after_mock: bool = False,
+ skip_layouts: Optional[List[str]] = None,
+ scroll_into_view: Optional[str] = None,
+ wait_animations: bool = True,
+ wait_after_layout_change: bool = False,
+ wait_delay: float = 0.5):
+ """Compare the given element with its reference in all layouts"""
+
+ if ignore is None:
+ ignore = []
+
+ if skip_layouts is None:
+ skip_layouts = []
+
+ if not (Image and self.pixels_label):
+ return
+
+ # If the page overflows make sure to not show a scrollbar
+ # Don't apply this hack for login and terminal and shell as they don't use PF Page
+ if not self.is_present("#shell-page") and not self.is_present("#login-details") and not self.is_present("#system-terminal-page"):
+ self.switch_to_frame(self.cdp.cur_frame)
+ classes = self.attr("main", "class")
+ if "pf-v5-c-page__main" in classes:
+ self.set_attr("main.pf-v5-c-page__main", "class", f"{classes} pixel-test")
+
+ if self.current_layout:
+ previous_layout = self.current_layout["name"]
+ for layout in self.layouts:
+ if layout["name"] not in skip_layouts:
+ self.set_layout(layout["name"])
+ if "rtl" in self.current_layout["name"]:
+ self._set_direction("rtl")
+ if wait_after_layout_change:
+ time.sleep(wait_delay)
+ self.assert_pixels_in_current_layout(selector, key, ignore=ignore,
+ mock=mock, sit_after_mock=sit_after_mock,
+ scroll_into_view=scroll_into_view,
+ wait_animations=wait_animations,
+ wait_delay=wait_delay)
+
+ if "rtl" in self.current_layout["name"]:
+ self._set_direction("ltr")
+ self.set_layout(previous_layout)
+
+ def assert_no_unused_pixel_test_references(self):
+ """Check whether all reference images in test/reference have been used."""
+
+ if not (Image and self.pixels_label):
+ return
+
+ pixel_references = set(glob.glob(os.path.join(TEST_DIR, "reference", self.pixels_label + "*-pixels.png")))
+ unused = pixel_references - self.used_pixel_references
+ for u in unused:
+ print("Unused reference image " + os.path.basename(u))
+ self.failed_pixel_tests += 1
+
+ def get_js_log(self):
+ """Return the current javascript log"""
+
+ if self.cdp:
+ return self.cdp.get_js_log()
+ return []
+
+ def copy_js_log(self, title: str, label: Optional[str] = None):
+ """Copy the current javascript log"""
+
+ logs = list(self.get_js_log())
+ if logs:
+ filename = unique_filename(f"{label or self.label}-{title}", "js.log")
+ with open(filename, 'wb') as f:
+ f.write('\n'.join(logs).encode('UTF-8'))
+ attach(filename, move=True)
+ print("Wrote JS log to " + filename)
+
+ def kill(self):
+ self.cdp.kill()
+
+ def write_coverage_data(self):
+ if self.coverage_label and self.cdp and self.cdp.valid:
+ coverage = self.cdp.invoke("Profiler.takePreciseCoverage")
+ write_lcov(coverage['result'], self.coverage_label)
+
+ def assert_no_oops(self):
+ if self.allow_oops:
+ return
+
+ if self.cdp and self.cdp.valid:
+ self.switch_to_top()
+ if self.is_present("#navbar-oops"):
+ assert not self.is_visible("#navbar-oops"), "Cockpit shows an Oops"
+
+
+class MachineCase(unittest.TestCase):
+ image = testvm.DEFAULT_IMAGE
+ libexecdir = None
+ runner = None
+ machine: testvm.Machine
+ machines = Dict[str, testvm.Machine]
+ machine_class = None
+ browser: Browser
+ network = None
+ journal_start = None
+
+ # provision is a dictionary of dictionaries, one for each additional machine to be created, e.g.:
+ # provision = { 'openshift' : { 'image': 'openshift', 'memory_mb': 1024 } }
+ # These will be instantiated during setUp, and replaced with machine objects
+ provision: Optional[Dict[str, Dict[str, Union[str, int]]]] = None
+
+ global_machine = None
+
+ @classmethod
+ def get_global_machine(cls):
+ if cls.global_machine:
+ return cls.global_machine
+ cls.global_machine = cls.new_machine(cls, restrict=True, cleanup=False)
+ if opts.trace:
+ print(f"Starting global machine {cls.global_machine.label}")
+ cls.global_machine.start()
+ return cls.global_machine
+
+ @classmethod
+ def kill_global_machine(cls):
+ if cls.global_machine:
+ cls.global_machine.kill()
+ cls.global_machine = None
+
+ def label(self):
+ return self.__class__.__name__ + '-' + self._testMethodName
+
+ def new_machine(self, image=None, forward=None, restrict=True, cleanup=True, inherit_machine_class=True, **kwargs):
+ machine_class = inherit_machine_class and self.machine_class or testvm.VirtMachine
+
+ if opts.address:
+ if forward:
+ raise unittest.SkipTest("Cannot run this test when specific machine address is specified")
+ machine = testvm.Machine(address=opts.address, image=image or self.image, verbose=opts.trace, browser=opts.browser)
+ if cleanup:
+ self.addCleanup(machine.disconnect)
+ else:
+ if image is None:
+ image = os.path.join(TEST_DIR, "images", self.image)
+ if not os.path.exists(image):
+ raise FileNotFoundError("Can't run tests without a prepared image; use test/image-prepare")
+ if not self.network:
+ network = testvm.VirtNetwork(image=image)
+ if cleanup:
+ self.addCleanup(network.kill)
+ self.network = network
+ networking = self.network.host(restrict=restrict, forward=forward or {})
+ machine = machine_class(verbose=opts.trace, networking=networking, image=image, **kwargs)
+ if opts.fetch and not os.path.exists(machine.image_file):
+ machine.pull(machine.image_file)
+ if cleanup:
+ self.addCleanup(machine.kill)
+ return machine
+
+ def new_browser(self, machine=None, coverage=False):
+ if machine is None:
+ machine = self.machine
+ label = self.label() + "-" + machine.label
+ pixels_label = None
+ if os.environ.get("TEST_BROWSER", "chromium") == "chromium" and not self.is_devel_build():
+ try:
+ with open(f'{TEST_DIR}/reference-image') as fp:
+ reference_image = fp.read().strip()
+ except FileNotFoundError:
+ # no "reference-image" file available; this most likely means that
+ # there are no pixel tests to execute
+ pass
+ else:
+ if machine.image == reference_image:
+ pixels_label = self.label()
+ browser = Browser(machine.web_address,
+ label=label, pixels_label=pixels_label, coverage_label=self.label() if coverage else None,
+ port=machine.web_port, machine=self)
+ self.addCleanup(browser.kill)
+ return browser
+
+ def getError(self):
+ # errors is a list of (method, exception) calls (usually multiple
+ # per method); None exception means success
+ errors = []
+ if hasattr(self._outcome, 'errors'):
+ # Python 3.4 - 3.10 (These two methods have no side effects)
+ result = self.defaultTestResult()
+ errors = result.errors
+ self._feedErrorsToResult(result, self._outcome.errors)
+ elif hasattr(self._outcome, 'result') and hasattr(self._outcome.result, '_excinfo'):
+ # pytest emulating unittest
+ return self._outcome.result._excinfo
+ else:
+ # Python 3.11+ now records errors and failures seperate
+ errors = self._outcome.result.errors + self._outcome.result.failures
+
+ try:
+ return errors[0][1]
+ except IndexError:
+ return None
+
+ def is_nondestructive(self):
+ test_method = getattr(self.__class__, self._testMethodName)
+ return get_decorator(test_method, self.__class__, "nondestructive")
+
+ def is_devel_build(self) -> bool:
+ return os.environ.get('NODE_ENV') == 'development'
+
+ def is_pybridge(self) -> bool:
+ # some tests start e.g. centos-7 as first machine, bridge may not exist there
+ return any('python' in m.execute('head -c 30 /usr/bin/cockpit-bridge || true') for m in self.machines.values())
+
+ def disable_preload(self, *packages, machine=None):
+ if machine is None:
+ machine = self.machine
+ for pkg in packages:
+ machine.write(f"/etc/cockpit/{pkg}.override.json", '{ "preload": [ ] }')
+
+ def enable_preload(self, package: str, *pages: str):
+ pages_str = ', '.join(f'"{page}"' for page in pages)
+ self.machine.write(f"/etc/cockpit/{package}.override.json", f'{{ "preload": [ {pages_str} ] }}')
+
+ def system_before(self, version):
+ try:
+ v = self.machine.execute("""rpm -q --qf '%{V}' cockpit-system ||
+ dpkg-query -W -f '${source:Upstream-Version}' cockpit-system ||
+ (pacman -Q cockpit | cut -f2 -d' ' | cut -f1 -d-)
+ """).split(".")
+ except subprocess.CalledProcessError:
+ return False
+
+ return int(v[0]) < version
+
+ def setUp(self, restrict=True):
+ self.allowed_messages = self.default_allowed_messages
+ self.allowed_console_errors = self.default_allowed_console_errors
+ self.allow_core_dumps = False
+
+ if os.getenv("MACHINE"):
+ # apply env variable together if MACHINE envvar is set
+ opts.address = os.getenv("MACHINE")
+ if self.is_nondestructive():
+ pass
+ elif os.getenv("DESTRUCTIVE") and not self.is_nondestructive():
+ print("Run destructive test, be careful, may lead to upredictable state of machine")
+ else:
+ raise unittest.SkipTest("Skip destructive test by default")
+ if os.getenv("BROWSER"):
+ opts.browser = os.getenv("BROWSER")
+ if os.getenv("TRACE"):
+ opts.trace = True
+ if os.getenv("SIT"):
+ opts.sit = True
+
+ if opts.address and self.provision is not None:
+ raise unittest.SkipTest("Cannot provision multiple machines if a specific machine address is specified")
+
+ self.machines = {}
+ provision = self.provision or {'machine1': {}}
+ self.tmpdir = tempfile.mkdtemp()
+ # automatically cleaned up for @nondestructive tests, but you have to create it yourself
+ self.vm_tmpdir = "/var/lib/cockpittest"
+
+ if self.is_nondestructive() and not opts.address:
+ if self.provision:
+ raise unittest.SkipTest("Cannot provision machines if test is marked as nondestructive")
+ self.machine = self.machines['machine1'] = MachineCase.get_global_machine()
+ else:
+ MachineCase.kill_global_machine()
+ first_machine = True
+ # First create all machines, wait for them later
+ for key in sorted(provision.keys()):
+ options = provision[key].copy()
+ if 'address' in options:
+ del options['address']
+ if 'dns' in options:
+ del options['dns']
+ if 'dhcp' in options:
+ del options['dhcp']
+ if 'restrict' not in options:
+ options['restrict'] = restrict
+ machine = self.new_machine(**options)
+ self.machines[key] = machine
+ if first_machine:
+ first_machine = False
+ self.machine = machine
+ if opts.trace:
+ print(f"Starting {key} {machine.label}")
+ machine.start()
+
+ self.danger_btn_class = '.pf-m-danger'
+ self.primary_btn_class = '.pf-m-primary'
+ self.default_btn_class = '.pf-m-secondary'
+
+ # Now wait for the other machines to be up
+ for key in self.machines.keys():
+ machine = self.machines[key]
+ machine.wait_boot()
+ address = provision[key].get("address")
+ if address is not None:
+ machine.set_address(address)
+ dns = provision[key].get("dns")
+ if address or dns:
+ machine.set_dns(dns)
+ dhcp = provision[key].get("dhcp", False)
+ if dhcp:
+ machine.dhcp_server()
+
+ self.journal_start = self.machine.journal_cursor()
+ self.browser: Browser = self.new_browser(coverage=opts.coverage)
+ # fail tests on criticals
+ self.machine.write("/etc/cockpit/cockpit.conf", "[Log]\nFatal = criticals\n")
+ if self.is_nondestructive():
+ self.nonDestructiveSetup()
+
+ # Pages with debug enabled are huge and loading/executing them is heavy for browsers
+ # To make it easier for browsers and thus make tests quicker, disable packagekit and systemd preloads
+ if self.is_devel_build():
+ self.disable_preload("packagekit", "systemd")
+
+ if self.machine.image.startswith('debian') or self.machine.image.startswith('ubuntu') or self.machine.image == 'arch':
+ self.libexecdir = '/usr/lib/cockpit'
+ else:
+ self.libexecdir = '/usr/libexec'
+
+ def nonDestructiveSetup(self):
+ """generic setUp/tearDown for @nondestructive tests"""
+
+ m = self.machine
+
+ # helps with mapping journal output to particular tests
+ name = "%s.%s" % (self.__class__.__name__, self._testMethodName)
+ m.execute("logger -p user.info 'COCKPITTEST: start %s'" % name)
+ self.addCleanup(m.execute, "logger -p user.info 'COCKPITTEST: end %s'" % name)
+
+ # core dumps get copied per-test, don't clobber subsequent tests with them
+ self.addCleanup(m.execute, "find /var/lib/systemd/coredump -type f -delete")
+
+ # temporary directory in the VM
+ self.addCleanup(m.execute, "if [ -d {0} ]; then findmnt --list --noheadings --output TARGET | grep ^{0} | xargs -r umount; rm -r {0}; fi".format(self.vm_tmpdir))
+
+ # users/groups/home dirs
+ self.restore_file("/etc/passwd")
+ self.restore_file("/etc/group")
+ self.restore_file("/etc/shadow")
+ self.restore_file("/etc/gshadow")
+ self.restore_file("/etc/subuid")
+ self.restore_file("/etc/subgid")
+ self.restore_file("/var/log/wtmp")
+ home_dirs = m.execute("ls /home").strip().split()
+
+ def cleanup_home_dirs():
+ for d in m.execute("ls /home").strip().split():
+ if d not in home_dirs:
+ m.execute("rm -r /home/" + d)
+ self.addCleanup(cleanup_home_dirs)
+
+ if m.image == "arch":
+ # arch configures pam_faillock by default
+ self.addCleanup(m.execute, "rm -rf /run/faillock")
+
+ # cockpit configuration
+ self.restore_dir("/etc/cockpit")
+
+ if not m.ostree_image:
+ # for storage tests
+ self.restore_file("/etc/fstab")
+ self.restore_file("/etc/crypttab")
+
+ # tests expect cockpit.service to not run at start; also, avoid log leakage into the next test
+ self.addCleanup(m.execute, "systemctl stop --quiet cockpit")
+
+ # The sssd daemon seems to get confused when we restore
+ # backups of /etc/group etc and stops following updates to it.
+ # Let's restart the daemon to reset that condition.
+ m.execute("systemctl try-restart sssd || true")
+
+ # reset scsi_debug (see e. g. StorageHelpers.add_ram_disk()
+ # this needs to happen very late in the cleanup, so that test cases can clean up the users of that disk first
+ # right after unmounting the device is often still busy, so retry a few times
+ self.addCleanup(self.machine.execute,
+ "set -e; [ -e /sys/module/scsi_debug ] || exit 0; "
+ "for dev in $(ls /sys/bus/pseudo/drivers/scsi_debug/adapter*/host*/target*/*:*/block); do "
+ " for s in /sys/block/*/slaves/${dev}*; do [ -e $s ] || break; "
+ " d=/dev/$(dirname $(dirname ${s#/sys/block/})); "
+ " while fuser --mount $d --kill; do sleep 0.1; done; "
+ " umount $d || true; dmsetup remove --force $d || true; "
+ " done; "
+ " while fuser --mount /dev/$dev --kill; do sleep 0.1; done; "
+ " umount /dev/$dev || true; "
+ " swapon --show=NAME --noheadings | grep $dev | xargs -r swapoff; "
+ "done; until rmmod scsi_debug; do sleep 0.2; done", stdout=None)
+
+ def terminate_sessions():
+ # on OSTree we don't get "web console" sessions with the cockpit/ws container; just SSH; but also, some tests start
+ # admin sessions without Cockpit
+ self.machine.execute("""for u in $(loginctl --no-legend list-users | awk '{ if ($2 != "root") print $1 }'); do
+ loginctl terminate-user $u 2>/dev/null || true
+ loginctl kill-user $u 2>/dev/null || true
+ pkill -9 -u $u || true
+ while pgrep -u $u; do sleep 0.2; done
+ while mountpoint -q /run/user/$u && ! umount /run/user/$u; do sleep 0.2; done
+ rm -rf /run/user/$u
+ done""")
+
+ # Terminate all other Cockpit sessions
+ sessions = self.machine.execute("loginctl --no-legend list-sessions | awk '/web console/ { print $1 }'").strip().split()
+ for s in sessions:
+ # Don't insist that terminating works, the session might be gone by now.
+ self.machine.execute(f"loginctl kill-session {s} || true; loginctl terminate-session {s} || true")
+
+ # Wait for sessions to be gone
+ sessions = self.machine.execute("loginctl --no-legend list-sessions | awk '/web console/ { print $1 }'").strip().split()
+ for s in sessions:
+ try:
+ m.execute(f"while loginctl show-session {s}; do sleep 0.2; done", timeout=30)
+ except RuntimeError:
+ # show the status in debug logs, to see what's wrong
+ m.execute(f"loginctl session-status {s} >&2")
+ raise
+
+ # terminate all systemd user services for users who are not logged in
+ self.machine.execute("systemctl stop user@*.service")
+
+ # Restart logind to mop up empty "closing" sessions, and clean user id cache for non-system users
+ self.machine.execute("systemctl stop systemd-logind; cd /run/systemd/users/; "
+ "for f in $(ls); do [ $f -le 500 ] || rm $f; done")
+
+ self.addCleanup(terminate_sessions)
+
+ def tearDown(self):
+ error = self.getError()
+
+ if error:
+ print(error, file=sys.stderr)
+ try:
+ self.snapshot("FAIL")
+ self.copy_js_log("FAIL")
+ self.copy_journal("FAIL")
+ self.copy_cores("FAIL")
+ except (OSError, RuntimeError):
+ # failures in these debug artifacts should not skip cleanup actions
+ sys.stderr.write("Failed to generate debug artifact:\n")
+ traceback.print_exc(file=sys.stderr)
+
+ if opts.sit:
+ sit(self.machines)
+
+ if self.browser:
+ self.browser.write_coverage_data()
+
+ if self.machine.ssh_reachable:
+ self.check_journal_messages()
+ if not error:
+ self.check_browser_errors()
+ self.check_pixel_tests()
+
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
+
+ def login_and_go(self, path: Optional[str] = None, user: Optional[str] = None, host: Optional[str] = None,
+ superuser: bool = True, urlroot: Optional[str] = None, tls: bool = False,
+ enable_root_login: bool = False):
+ if enable_root_login:
+ self.enable_root_login()
+ self.machine.start_cockpit(tls=tls)
+ # first load after starting cockpit tends to take longer, due to on-demand service start
+ with self.browser.wait_timeout(30):
+ self.browser.login_and_go(path, user=user, host=host, superuser=superuser, urlroot=urlroot, tls=tls)
+
+ def start_machine_troubleshoot(self, new=False, known_host=False, password=None, expect_closed_dialog=True, browser=None):
+ b = browser or self.browser
+
+ b.wait_visible("#machine-troubleshoot")
+ b.click('#machine-troubleshoot')
+
+ b.wait_visible('#hosts_setup_server_dialog')
+ if new:
+ b.click('#hosts_setup_server_dialog button:contains(Add)')
+ if not known_host:
+ b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to")
+ b.wait_in_text('#hosts_setup_server_dialog', "for the first time.")
+ b.click("#hosts_setup_server_dialog button:contains('Trust and add host')")
+ if password:
+ b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
+ b.set_input_text('#login-custom-password', password)
+ b.click('#hosts_setup_server_dialog button:contains(Log in)')
+ if expect_closed_dialog:
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ def add_machine(self, address, known_host=False, password="foobar", browser=None):
+ b = browser or self.browser
+ b.switch_to_top()
+ b.go(f"/@{address}")
+ self.start_machine_troubleshoot(new=True, known_host=known_host, password=password, browser=browser)
+ b.enter_page("/system", host=address)
+
+ # List of allowed journal messages during tests; these need to match the *entire* message
+ default_allowed_messages = [
+ # Reauth stuff
+ '.*Reauthorizing unix-user:.*',
+ '.*user .* was reauthorized.*',
+
+ # Happens when the user logs out during reauthorization
+ "Error executing command as another user: Not authorized",
+ "This incident has been reported.",
+
+ # Reboots are ok
+ "-- Reboot --",
+
+ # Sometimes D-Bus goes away before us during shutdown
+ "Lost the name com.redhat.Cockpit on the session message bus",
+ "GLib-GIO:ERROR:gdbusobjectmanagerserver\\.c:.*:g_dbus_object_manager_server_emit_interfaces_.*: assertion failed \\(error == NULL\\): The connection is closed \\(g-io-error-quark, 18\\)",
+ "Error sending message: The connection is closed",
+
+ # PAM noise
+ "cockpit-session: pam: Creating directory .*",
+ "cockpit-session: pam: Changing password for .*",
+
+ # btmp tracking
+ "cockpit-session: pam: Last failed login:.*",
+ "cockpit-session: pam: There .* failed login attempts? since the last successful login.",
+
+ # pam_lastlog complaints
+ ".*/var/log/lastlog: No such file or directory",
+
+ # ssh messages may be dropped when closing
+ '10.*: dropping message while waiting for child to exit',
+
+ # pkg/packagekit/autoupdates.jsx backend check often gets interrupted by logout
+ "xargs: basename: terminated by signal 13",
+
+ # SELinux messages to ignore
+ "(audit: )?type=1403 audit.*",
+ "(audit: )?type=1404 audit.*",
+ "(audit: )?type=1405 audit.*",
+
+ # apparmor loading
+ "(audit: )?type=1400.*apparmor=\"STATUS\".*",
+
+ # apparmor noise
+ "(audit: )?type=1400.*apparmor=\"ALLOWED\".*",
+
+ # Messages from systemd libraries when they are in debug mode
+ 'Successfully loaded SELinux database in.*',
+ 'calling: info',
+ 'Sent message type=method_call sender=.*',
+ 'Got message type=method_return sender=.*',
+
+ # Various operating systems see this from time to time
+ "Journal file.*truncated, ignoring file.",
+
+ # our core dump retrieval is not entirely reliable
+ "Failed to send coredump datagram:.*",
+
+ # Something crashed, but we don't have more info. Don't fail on that
+ "Failed to get (COMM|EXE).*: No such process",
+
+ # several tests change the host name
+ "sudo: unable to resolve host.*",
+
+ # The usual sudo finger wagging
+ "We trust you have received the usual lecture from the local System",
+ "Administrator. It usually boils down to these three things:",
+ r"#1\) Respect the privacy of others.",
+ r"#2\) Think before you type.",
+ r"#3\) With great power comes great responsibility.",
+ "For security reasons, the password you type will not be visible",
+
+ # starting out with empty PCP logs and pmlogger not running causes these metrics channel messages
+ "(direct|pcp-archive): no such metric: .*: Unknown metric name",
+ "(direct|pcp-archive): instance name lookup failed:.*",
+ "(direct|pcp-archive): couldn't create pcp archive context for.*",
+
+ # timedatex.service shuts down after timeout, runs into race condition with property watching
+ ".*org.freedesktop.timedate1: couldn't get all properties.*Error:org.freedesktop.DBus.Error.NoReply.*",
+
+ # https://github.com/cockpit-project/cockpit/issues/19235
+ "invalid non-UTF8 @data passed as text to web_socket_connection_send.*",
+ ]
+
+ default_allowed_messages += os.environ.get("TEST_ALLOW_JOURNAL_MESSAGES", "").split(",")
+
+ # List of allowed console.error() messages during tests; these match substrings
+ default_allowed_console_errors = [
+ # HACK: These should be fixed, but debugging these is not trivial, and the impact is very low
+ "Warning: .* setState.*on an unmounted component",
+ "Warning: Can't perform a React state update on an unmounted component",
+ "Warning: Cannot update a component.*while rendering a different component",
+ "Warning: A component is changing an uncontrolled input to be controlled",
+ "Warning: A component is changing a controlled input to be uncontrolled",
+ "Warning: Can't call.*on a component that is not yet mounted. This is a no-op",
+ "Warning: Cannot update during an existing state transition",
+ r"Warning: You are calling ReactDOMClient.createRoot\(\) on a container that has already been passed to createRoot",
+
+ # FIXME: PatternFly complains about these, but https://www.a11y-collective.com/blog/the-first-rule-for-using-aria/
+ # and https://www.accessibility-developer-guide.com/knowledge/aria/bad-practices/
+ "aria-label",
+
+ # PackageKit crashes a lot; let that not be the sole reason for failing a test
+ "error: Could not determine kpatch packages:.*PackageKit crashed",
+ ]
+
+ if testvm.DEFAULT_IMAGE.startswith('rhel-8') or testvm.DEFAULT_IMAGE.startswith('centos-8'):
+ # old occasional bugs in tracer, don't happen in newer versions any more
+ default_allowed_console_errors.append('Tracer failed:.*Traceback')
+
+ env_allow = os.environ.get("TEST_ALLOW_BROWSER_ERRORS")
+ if env_allow:
+ default_allowed_console_errors += env_allow.split(",")
+
+ def allow_journal_messages(self, *patterns: str):
+ """Don't fail if the journal contains a entry completely matching the given regexp"""
+ for p in patterns:
+ self.allowed_messages.append(p)
+
+ def allow_hostkey_messages(self):
+ self.allow_journal_messages('.*: .* host key for server is not known: .*',
+ '.*: refusing to connect to unknown host: .*',
+ '.*: .* host key for server has changed to: .*',
+ '.*: host key for this server changed key type: .*',
+ '.*: failed to retrieve resource: hostkey-unknown')
+
+ def allow_restart_journal_messages(self):
+ self.allow_journal_messages(".*Connection reset by peer.*",
+ "connection unexpectedly closed by peer",
+ ".*Broken pipe.*",
+ "g_dbus_connection_real_closed: Remote peer vanished with error: Underlying GIOStream returned 0 bytes on an async read \\(g-io-error-quark, 0\\). Exiting.",
+ "cockpit-session: .*timed out.*",
+ "ignoring failure from session process:.*",
+ "peer did not close io when expected",
+ "request timed out, closing",
+ "PolicyKit daemon disconnected from the bus.",
+ ".*couldn't create polkit session subject: No session for pid.*",
+ "We are no longer a registered authentication agent.",
+ ".*: failed to retrieve resource: terminated",
+ ".*: external channel failed: (terminated|protocol-error)",
+ 'audit:.*denied.*comm="systemd-user-se".*nologin.*',
+ ".*No session for cookie",
+
+ 'localhost: dropping message while waiting for child to exit',
+ '.*: GDBus.Error:org.freedesktop.PolicyKit1.Error.Failed: .*',
+ '.*g_dbus_connection_call_finish_internal.*G_IS_DBUS_CONNECTION.*',
+ '.*Message recipient disconnected from message bus without replying.*',
+ '.*Unable to shutdown socket: Transport endpoint is not connected.*',
+
+ # If restarts or reloads happen really fast, the code in python.js
+ # that figures out which python to use crashes with SIGPIPE,
+ # and this is the resulting message
+ 'which: no python in .*'
+ )
+
+ def check_journal_messages(self, machine=None):
+ """Check for unexpected journal entries."""
+ machine = machine or self.machine
+ # on main machine, only consider journal entries since test case start
+ cursor = (machine == self.machine) and self.journal_start or None
+
+ # Journald does not always set trusted fields like
+ # _SYSTEMD_UNIT or _EXE correctly for the last few messages of
+ # a dying process, so we filter by the untrusted but reliable
+ # SYSLOG_IDENTIFIER instead.
+
+ matches = [
+ "SYSLOG_IDENTIFIER=cockpit-ws",
+ "SYSLOG_IDENTIFIER=cockpit-bridge",
+ "SYSLOG_IDENTIFIER=cockpit/ssh",
+ # also catch GLIB_DOMAIN=<library> which apply to cockpit-ws (but not to -bridge, too much random noise)
+ "_COMM=cockpit-ws",
+ "GLIB_DOMAIN=cockpit-ws",
+ "GLIB_DOMAIN=cockpit-bridge",
+ "GLIB_DOMAIN=cockpit-ssh",
+ "GLIB_DOMAIN=cockpit-pcp"
+ ]
+
+ if not self.allow_core_dumps:
+ matches += ["SYSLOG_IDENTIFIER=systemd-coredump"]
+ self.allowed_messages.append("Resource limits disable core dumping for process.*")
+ # can happen on shutdown when /run/systemd/coredump is gone already
+ self.allowed_messages.append("Failed to connect to coredump service: No such file or directory")
+ self.allowed_messages.append("Failed to connect to coredump service: Connection refused")
+
+ messages = machine.journal_messages(matches, 6, cursor=cursor)
+
+ if "TEST_AUDIT_NO_SELINUX" not in os.environ:
+ messages += machine.audit_messages("14", cursor=cursor) # 14xx is selinux
+
+ self.allowed_messages += self.machine.allowed_messages()
+
+ all_found = True
+ first = None
+ for m in messages:
+ # remove leading/trailing whitespace
+ m = m.strip()
+ # Ignore empty lines
+ if not m:
+ continue
+ found = False
+
+ # When coredump could not be generated, we cannot do much with info about there being a coredump
+ # Ignore this message and all subsequent core dumps
+ # If there is more than just one line about coredump, it will fail and show this messages
+ if m.startswith("Failed to generate stack trace"):
+ self.allowed_messages.append("Process .* of user .* dumped core.*")
+ continue
+
+ for p in self.allowed_messages:
+ match = re.match(p, m)
+ if match and match.group(0) == m:
+ found = True
+ break
+ if not found:
+ all_found = False
+ if not first:
+ first = m
+ print(m)
+ if not all_found:
+ self.copy_js_log("FAIL")
+ self.copy_journal("FAIL")
+ self.copy_cores("FAIL")
+ if not self.getError():
+ # fail test on the unexpected messages
+ raise Error(UNEXPECTED_MESSAGE + "journal messages:\n" + first)
+
+ def allow_browser_errors(self, *patterns):
+ """Don't fail if the test caused a console error contains the given regexp"""
+ for p in patterns:
+ self.allowed_console_errors.append(p)
+
+ def check_browser_errors(self):
+ if not self.browser:
+ return
+ for log in self.browser.get_js_log():
+ if not log.startswith("error: "):
+ continue
+ # errors are fatal in general; they need to be explicitly whitelisted
+ for p in self.allowed_console_errors:
+ if re.search(p, log):
+ break
+ else:
+ raise Error(UNEXPECTED_MESSAGE + "browser errors:\n" + log)
+
+ self.browser.assert_no_oops()
+
+ def check_pixel_tests(self):
+ if self.browser:
+ self.browser.assert_no_unused_pixel_test_references()
+ if self.browser.failed_pixel_tests > 0:
+ raise Error(PIXEL_TEST_MESSAGE)
+
+ def snapshot(self, title: str, label: Optional[str] = None):
+ """Take a snapshot of the current screen and save it as a PNG.
+
+ Arguments:
+ title: Used for the filename.
+ """
+ if self.browser is not None:
+ try:
+ self.browser.snapshot(title, label)
+ except RuntimeError:
+ # this usually runs in exception handlers; raising an exception here skips cleanup handlers, so don't
+ sys.stderr.write("Unexpected exception in snapshot():\n")
+ sys.stderr.write(traceback.format_exc())
+
+ def copy_js_log(self, title, label=None):
+ if self.browser is not None:
+ try:
+ self.browser.copy_js_log(title, label)
+ except RuntimeError:
+ # this usually runs in exception handlers; raising an exception here skips cleanup handlers, so don't
+ sys.stderr.write("Unexpected exception in copy_js_log():\n")
+ sys.stderr.write(traceback.format_exc())
+
+ def copy_journal(self, title: str, label: Optional[str] = None):
+ for _, m in self.machines.items():
+ if m.ssh_reachable:
+ log = unique_filename("%s-%s-%s" % (label or self.label(), m.label, title), "log.gz")
+ with open(log, "w") as fp:
+ m.execute("journalctl|gzip", stdout=fp)
+ print("Journal extracted to %s" % (log))
+ attach(log, move=True)
+
+ def copy_cores(self, title: str, label: Optional[str] = None):
+ if self.allow_core_dumps:
+ return
+ for _, m in self.machines.items():
+ if m.ssh_reachable:
+ directory = "%s-%s-%s.core" % (label or self.label(), m.label, title)
+ dest = os.path.abspath(directory)
+ # overwrite core dumps from previous retries
+ if os.path.exists(dest):
+ shutil.rmtree(dest)
+ m.download_dir("/var/lib/systemd/coredump", dest)
+ try:
+ os.rmdir(dest)
+ except OSError as ex:
+ if ex.errno == errno.ENOTEMPTY:
+ print("Core dumps downloaded to %s" % (dest))
+ # Enable this to temporarily(!) create artifacts for core dumps, if a crash is hard to reproduce
+ # attach(dest, move=True)
+
+ def settle_cpu(self):
+ """Wait until CPU usage in the VM settles down
+
+ Wait until the process with the highest CPU usage drops below 20%
+ usage. Wait for up to a minute, then return. There is no error if the
+ CPU stays busy, as usually a test then should just try to run anyway.
+ """
+ for _ in range(20):
+ # get the CPU percentage of the most busy process
+ busy_proc = self.machine.execute("ps --no-headers -eo pcpu,pid,args | sort -k 1 -n -r | head -n1")
+ if float(busy_proc.split()[0]) < 20.0:
+ break
+ time.sleep(3)
+
+ def sed_file(self, expr: str, path: str, apply_change_action: Optional[str] = None):
+ """sed a file on primary machine
+
+ This is safe for @nondestructive tests, the file will be restored during cleanup.
+
+ The optional apply_change_action will be run both after sedding and after restoring the file.
+ """
+ m = self.machine
+ m.execute(f"sed -i.cockpittest '{expr}' {path}")
+ if apply_change_action:
+ m.execute(apply_change_action)
+
+ if self.is_nondestructive():
+ if apply_change_action:
+ self.addCleanup(m.execute, apply_change_action)
+ self.addCleanup(m.execute, f"mv {path}.cockpittest {path}")
+
+ def file_exists(self, path: str) -> bool:
+ """Check if file exists on test machine"""
+
+ return self.machine.execute(f"if test -e {path}; then echo yes; fi").strip() != ""
+
+ def restore_dir(self, path: str, post_restore_action: Optional[str] = None, reboot_safe: bool = False,
+ restart_unit: Optional[str] = None):
+ """Backup/restore a directory for a nondestructive test
+
+ This takes care to not ever touch the original content on disk, but uses transient overlays.
+ As this uses a bind mount, it does not work for files that get changed atomically (with mv);
+ use restore_file() for these.
+
+ `restart_unit` will be stopped before restoring path, and restarted afterwards if it was running.
+ The optional post_restore_action will run after restoring the original content.
+
+ If the directory needs to survive reboot, `reboot_safe=True` needs to be specified; then this
+ will just backup/restore the directory instead of bind-mounting, which is less robust.
+ """
+ if not self.is_nondestructive() and not self.machine.ostree_image:
+ return # skip for efficiency reasons
+
+ exe = self.machine.execute
+
+ if not self.file_exists(path):
+ self.addCleanup(exe, f"rm -rf '{path}'")
+ return
+
+ backup = os.path.join(self.vm_tmpdir, path.replace('/', '_'))
+ exe(f"mkdir -p {self.vm_tmpdir}; cp -a {path}/ {backup}/")
+
+ if not reboot_safe:
+ exe(f"mount -o bind {backup} {path}")
+
+ if restart_unit:
+ restart_stamp = f"/run/cockpit_restart_{restart_unit}"
+ self.addCleanup(
+ exe,
+ f"if [ -e {restart_stamp} ]; then systemctl start {restart_unit}; rm {restart_stamp}; fi"
+ )
+
+ if post_restore_action:
+ self.addCleanup(exe, post_restore_action)
+
+ if reboot_safe:
+ self.addCleanup(exe, f"rm -rf {path}; mv {backup} {path}")
+ else:
+ # HACK: a lot of tests call this on /home/...; that restoration happens before killing all user
+ # processes in nonDestructiveSetup(), so we have to do it lazily
+ if path.startswith("/home"):
+ cmd = f"umount -lf {path}"
+ else:
+ cmd = f"umount {path} || {{ fuser -uvk {path} {path}/* >&2 || true; sleep 1; umount {path}; }}"
+ self.addCleanup(exe, cmd)
+
+ if restart_unit:
+ self.addCleanup(exe, f"if systemctl --quiet is-active {restart_unit}; then touch {restart_stamp}; fi; "
+ f"systemctl stop {restart_unit}")
+
+ def restore_file(self, path: str, post_restore_action: Optional[str] = None):
+ """Backup/restore a file for a nondestructive test
+
+ This is less robust than restore_dir(), but works for files that need to get changed atomically.
+
+ If path does not currently exist, it will be removed again on cleanup.
+ """
+ if not self.is_nondestructive():
+ return # skip for efficiency reasons
+
+ if post_restore_action:
+ self.addCleanup(self.machine.execute, post_restore_action)
+
+ if self.file_exists(path):
+ backup = os.path.join(self.vm_tmpdir, path.replace('/', '_'))
+ self.machine.execute(f"mkdir -p {self.vm_tmpdir}; cp -a {path} {backup}")
+ self.addCleanup(self.machine.execute, f"mv {backup} {path}")
+ else:
+ self.addCleanup(self.machine.execute, f"rm -f {path}")
+
+ def write_file(self, path: str, content: str, append: bool = False, owner: Optional[str] = None, perm: Optional[str] = None,
+ post_restore_action: Optional[str] = None):
+ """Write a file on primary machine
+
+ This is safe for @nondestructive tests, the file will be removed during cleanup.
+
+ If @append is True, append to existing file instead of replacing it.
+ @owner is the desired file owner as chown shell string (e.g. "admin:nogroup")
+ @perm is the desired file permission as chmod shell string (e.g. "0600")
+ """
+ m = self.machine
+ self.restore_file(path, post_restore_action=post_restore_action)
+ m.write(path, content, append=append, owner=owner, perm=perm)
+
+ def enable_root_login(self):
+ """Enable root login
+
+ By default root login is disabled in cockpit, removing the root entry of /etc/cockpit/disallowed-users allows root to login.
+ """
+
+ # fedora-coreos runs cockpit-ws in a containter so does not install cockpit-ws on the host
+ disallowed_conf = '/etc/cockpit/disallowed-users'
+ if not self.machine.ostree_image and self.file_exists(disallowed_conf):
+ self.sed_file('/root/d', disallowed_conf)
+
+ def setup_provisioned_hosts(self, disable_preload: bool = False):
+ """Setup provisioned hosts for testing
+
+ This sets the hostname of all machines to the name given in the
+ provision dictionary and optionally disabled preload.
+ """
+ for name, m in self.machines.items():
+ m.execute(f"hostnamectl set-hostname {name}")
+ if disable_preload:
+ self.disable_preload("packagekit", "playground", "systemd", machine=m)
+
+ def authorize_pubkey(self, machine, account, pubkey):
+ machine.execute(f"a={account} d=/home/$a/.ssh; mkdir -p $d; chown $a:$a $d; chmod 700 $d")
+ machine.write(f"/home/{account}/.ssh/authorized_keys", pubkey)
+ machine.execute(f"a={account}; chown $a:$a /home/$a/.ssh/authorized_keys")
+
+ def get_pubkey(self, machine, account):
+ return machine.execute(f"cat /home/{account}/.ssh/id_rsa.pub")
+
+ def setup_ssh_auth(self):
+ self.machine.execute("d=/home/admin/.ssh; mkdir -p $d; chown admin:admin $d; chmod 700 $d")
+ self.machine.execute("test -f /home/admin/.ssh/id_rsa || ssh-keygen -f /home/admin/.ssh/id_rsa -t rsa -N ''")
+ self.machine.execute("chown admin:admin /home/admin/.ssh/id_rsa*")
+ pubkey = self.get_pubkey(self.machine, "admin")
+
+ for m in self.machines:
+ self.authorize_pubkey(self.machines[m], "admin", pubkey)
+
+
+###########################
+# Global helper functions
+#
+
+
+def jsquote(js: str) -> str:
+ return json.dumps(js)
+
+
+def get_decorator(method, _class, name, default=None):
+ """Get decorator value of a test method or its class
+
+ Return None if the decorator was not set.
+ """
+ attr = "_testlib__" + name
+ return getattr(method, attr, getattr(_class, attr, default))
+
+
+###########################
+# Test decorators
+#
+
+def skipBrowser(reason: str, *browsers: str):
+ """Decorator for skipping a test on given browser(s)
+
+ Skips a test for provided *reason* on *browsers*.
+ """
+ browser = os.environ.get("TEST_BROWSER", "chromium")
+ if browser in browsers:
+ return unittest.skip(f"{browser}: {reason}")
+ return lambda testEntity: testEntity
+
+
+def skipImage(reason: str, *images: str):
+ """Decorator for skipping a test for given image(s)
+
+ Skip a test for a provided *reason* for given *images*. These
+ support Unix shell style patterns via fnmatch.fnmatch.
+
+ Example: @skipImage("no btrfs support on RHEL", "rhel-*")
+ """
+ if any(fnmatch.fnmatch(testvm.DEFAULT_IMAGE, img) for img in images):
+ return unittest.skip(f"{testvm.DEFAULT_IMAGE}: {reason}")
+ return lambda testEntity: testEntity
+
+
+def onlyImage(reason: str, *images: str):
+ """Decorator to only run a test on given image(s)
+
+ Only run this test on provided *images* for *reason*. These
+ support Unix shell style patterns via fnmatch.fnmatch.
+ """
+ if not any(fnmatch.fnmatch(testvm.DEFAULT_IMAGE, arg) for arg in images):
+ return unittest.skip(f"{testvm.DEFAULT_IMAGE}: {reason}")
+ return lambda testEntity: testEntity
+
+
+def skipOstree(reason: str):
+ """Decorator for skipping a test on OSTree images
+
+ Skip test for *reason* on OSTree images defined in OSTREE_IMAGES in bots/lib/constants.py.
+ """
+ if testvm.DEFAULT_IMAGE in OSTREE_IMAGES:
+ return unittest.skip(f"{testvm.DEFAULT_IMAGE}: {reason}")
+ return lambda testEntity: testEntity
+
+
+def skipDistroPackage():
+ """For tests which apply to BaseOS packages
+
+ With that, tests can evolve with latest code, without constantly breaking them when
+ running against older package versions in the -distropkg tests.
+ """
+ if 'distropkg' in testvm.DEFAULT_IMAGE:
+ return unittest.skip(f"{testvm.DEFAULT_IMAGE}: Do not test BaseOS packages")
+ return lambda testEntity: testEntity
+
+
+def nondestructive(testEntity):
+ """Tests decorated as nondestructive will all run against the same VM
+
+ Can be used on test classes and individual test methods.
+ """
+ setattr(testEntity, '_testlib__nondestructive', True)
+ return testEntity
+
+
+def no_retry_when_changed(testEntity):
+ """Tests decorated with no_retry_when_changed will only run once if they've been changed
+
+ Tests that have been changed are expected to succeed 3 times, if the test
+ takes a long time, this prevents timeouts. Can be used on test classes and
+ individual methods.
+ """
+ setattr(testEntity, '_testlib__no_retry_when_changed', True)
+ return testEntity
+
+
+def todo(reason: str = ''):
+ """Tests decorated with @todo are expected to fail.
+
+ An optional reason can be given, and will appear in the TAP output if run
+ via run-tests.
+ """
+ def wrapper(testEntity):
+ setattr(testEntity, '_testlib__todo', reason)
+ return testEntity
+ return wrapper
+
+
+def todoPybridge(reason: Optional[str] = None):
+ if not reason:
+ reason = 'still fails with python bridge'
+
+ def wrap(test_method):
+ @functools.wraps(test_method)
+ def wrapped_test(self):
+ is_pybridge = self.is_pybridge()
+ try:
+ test_method(self)
+ if is_pybridge:
+ return self.fail(reason)
+ return None
+ # only accept our testlib Errors, plus RuntimeError for TestSuperuserDashboardOldMachine
+ except (Error, RuntimeError):
+ if is_pybridge:
+ traceback.print_exc()
+ return self.skipTest(reason)
+ raise
+
+ return wrapped_test
+
+ return wrap
+
+
+def todoPybridgeRHEL8(reason: Optional[str] = None):
+ if testvm.DEFAULT_IMAGE.startswith('rhel-8') or testvm.DEFAULT_IMAGE.startswith('centos-8'):
+ return todoPybridge(reason or 'known fail on el8 with python bridge')
+ return lambda testEntity: testEntity
+
+
+def timeout(seconds: int):
+ """Change default test timeout of 600s, for long running tests
+
+ Can be applied to an individual test method or the entire class. This only
+ applies to test/common/run-tests, not to calling check-* directly.
+ """
+ def wrapper(testEntity):
+ setattr(testEntity, '_testlib__timeout', seconds)
+ return testEntity
+ return wrapper
+
+
+class TapRunner:
+ def __init__(self, verbosity=1):
+ self.verbosity = verbosity
+
+ def runOne(self, test):
+ result = unittest.TestResult()
+ print('# ----------------------------------------------------------------------')
+ print('#', test)
+ try:
+ unittest.TestSuite([test]).run(result)
+ except KeyboardInterrupt:
+ result.addError(test, sys.exc_info())
+ return result
+ except Exception:
+ result.addError(test, sys.exc_info())
+ sys.stderr.write(f"Unexpected exception while running {test}\n")
+ sys.stderr.write(traceback.format_exc())
+ return result
+ else:
+ result.printErrors()
+
+ if result.skipped:
+ print(f"# Result {test} skipped: {result.skipped[0][1]}")
+ elif result.wasSuccessful():
+ print(f"# Result {test} succeeded")
+ else:
+ for failure in result.failures:
+ print(failure[1])
+ for error in result.errors:
+ print(error[1])
+ print(f"# Result {test} failed")
+ return result
+
+ def run(self, testable):
+ tests = []
+
+ # The things to test
+ def collapse(test, tests):
+ if isinstance(test, unittest.TestCase):
+ tests.append(test)
+ else:
+ for t in test:
+ collapse(t, tests)
+ collapse(testable, tests)
+ test_count = len(tests)
+
+ # For statistics
+ start = time.time()
+ failures = 0
+ skips = []
+ while tests:
+ # The next test to test
+ test = tests.pop(0)
+ result = self.runOne(test)
+ if not result.wasSuccessful():
+ failures += 1
+ skips += result.skipped
+
+ # Report on the results
+ duration = int(time.time() - start)
+ hostname = socket.gethostname().split(".")[0]
+ details = f"[{duration}s on {hostname}]"
+
+ MachineCase.kill_global_machine()
+
+ # Return 77 if all tests were skipped
+ if len(skips) == test_count:
+ sys.stdout.write("# SKIP {0}\n".format(", ".join([f"{s[0]!s} {s[1]}" for s in skips])))
+ return 77
+ if failures:
+ sys.stdout.write("# {0} TEST{1} FAILED {2}\n".format(failures, "S" if failures > 1 else "", details))
+ return 1
+ else:
+ sys.stdout.write("# {0} TEST{1} PASSED {2}\n".format(test_count, "S" if test_count > 1 else "", details))
+ return 0
+
+
+def print_tests(tests):
+ for test in tests:
+ if isinstance(test, unittest.TestSuite):
+ print_tests(test)
+ elif isinstance(test, unittest.loader._FailedTest):
+ name = test.id().replace("unittest.loader._FailedTest.", "")
+ print(f"Error: '{name}' does not match a test", file=sys.stderr)
+ else:
+ print(test.id().replace("__main__.", ""))
+
+
+def arg_parser(enable_sit=True):
+ parser = argparse.ArgumentParser(description='Run Cockpit test(s)')
+ parser.add_argument('-v', '--verbose', dest="verbosity", action='store_const',
+ const=2, help='Verbose output')
+ parser.add_argument('-t', "--trace", dest='trace', action='store_true',
+ help='Trace machine boot and commands')
+ parser.add_argument('-q', '--quiet', dest='verbosity', action='store_const',
+ const=0, help='Quiet output')
+ if enable_sit:
+ parser.add_argument('-s', "--sit", dest='sit', action='store_true',
+ help="Sit and wait after test failure")
+ parser.add_argument('--nonet', dest="fetch", action="store_false",
+ help="Don't go online to download images or data")
+ parser.add_argument('--enable-network', dest='enable_network', action='store_true',
+ help="Enable network access for tests")
+ parser.add_argument('--coverage', action='store_true',
+ help="Collect code coverage data")
+ parser.add_argument("-l", "--list", action="store_true", help="Print the list of tests that would be executed")
+ # TMT compatibility, pass testnames as whitespace separated list
+ parser.add_argument('tests', nargs='*', default=os.getenv("TEST_NAMES").split() if os.getenv("TEST_NAMES") else [])
+
+ parser.set_defaults(verbosity=1, fetch=True)
+ return parser
+
+
+def test_main(options=None, suite=None, attachments=None, **kwargs):
+ """
+ Run all test cases, as indicated by arguments.
+
+ If no arguments are given on the command line, all test cases are
+ executed. Otherwise only the given test cases are run.
+ """
+
+ global opts
+
+ # Turn off python stdout buffering
+ buf_arg = 0
+ os.environ['PYTHONUNBUFFERED'] = '1'
+ buf_arg = 1
+ sys.stdout.flush()
+ sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buf_arg)
+
+ standalone = options is None
+ parser = arg_parser()
+ parser.add_argument('--machine', metavar="hostname[:port]", dest="address",
+ default=None, help="Run this test against an already running machine")
+ parser.add_argument('--browser', metavar="hostname[:port]", dest="browser",
+ default=None, help="When using --machine, use this cockpit web address")
+
+ if standalone:
+ options = parser.parse_args()
+
+ # Sit should always imply verbose
+ if options.sit:
+ options.verbosity = 2
+
+ # Have to copy into opts due to python globals across modules
+ for (key, value) in vars(options).items():
+ setattr(opts, key, value)
+
+ opts.address = getattr(opts, "address", None)
+ opts.browser = getattr(opts, "browser", None)
+ opts.attachments = os.environ.get("TEST_ATTACHMENTS", attachments)
+ if opts.attachments:
+ os.makedirs(opts.attachments, exist_ok=True)
+
+ import __main__
+ if len(opts.tests) > 0:
+ if suite:
+ parser.error("tests may not be specified when running a predefined test suite")
+ suite = unittest.TestLoader().loadTestsFromNames(opts.tests, module=__main__)
+ elif not suite:
+ suite = unittest.TestLoader().loadTestsFromModule(__main__)
+
+ if options.list:
+ print_tests(suite)
+ return 0
+
+ attach(os.path.join(TEST_DIR, "common/pixeldiff.html"))
+ attach(os.path.join(TEST_DIR, "common/link-patterns.json"))
+
+ runner = TapRunner(verbosity=opts.verbosity)
+ ret = runner.run(suite)
+ if not standalone:
+ return ret
+ sys.exit(ret)
+
+
+class Error(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+
+ def __str__(self):
+ return self.msg
+
+
+def wait(func: Callable, msg: Optional[str] = None, delay: int = 1, tries: int = 60):
+ """Wait for FUNC to return something truthy, and return that.
+
+ FUNC is called repeatedly until it returns a true value or until a
+ timeout occurs. In the latter case, a exception is raised that
+ describes the situation. The exception is either the last one
+ thrown by FUNC, or includes MSG, or a default message.
+
+ :param func: The function to call
+ :param msg: A error message to use when the timeout occurs. Defaults
+ to a generic message.
+ :param delay: How long to wait between calls to FUNC, in seconds. (default 1)
+ :param tries: How often to call FUNC. (defaults 60)
+ :raises Error: When a timeout occurs.
+ """
+
+ t = 0
+ while t < tries:
+ try:
+ val = func()
+ if val:
+ return val
+ except Exception:
+ if t == tries - 1:
+ raise
+ else:
+ pass
+ t = t + 1
+ sleep(delay)
+ raise Error(msg or "Condition did not become true.")
+
+
+def sit(machines=None):
+ """
+ Wait until the user confirms to continue.
+
+ The current test case is suspended so that the user can inspect
+ the browser.
+ """
+
+ for (_, machine) in (machines or {}).items():
+ sys.stderr.write(machine.diagnose())
+ print("Press RET to continue...")
+ sys.stdin.readline()
diff --git a/test/data/100years/0-self-signed.cert b/test/data/100years/0-self-signed.cert
new file mode 100644
index 0000000..224af33
--- /dev/null
+++ b/test/data/100years/0-self-signed.cert
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEdjCCAl6gAwIBAgIIYQGIgnkoGx0wDQYJKoZIhvcNAQELBQAwVjELMAkGA1UE
+BhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS03MzEyMDcz
+ODg4MDU2NjA1OTA4MRAwDgYDVQQDDAd0b29sYm94MCAXDTIwMDEwMTAwMDAwMVoY
+DzIxMjEwNDI3MjI0MDAxWjA1MQswCQYDVQQGEwJVUzEUMBIGA1UECgwLVW5zcGVj
+aWZpZWQxEDAOBgNVBAMMB3Rvb2xib3gwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDukbLhxSdbsruCnnXexEZTK8JJ5LEtMi7rWbqUYsBRhePF0oYhu9cc
+MKN9g5T6U0t70vY6sl2+W2x9PeDKeSpfHLMd8LAMzvT2Ejc4R2fn04s4pERBY64H
+HaTKBmHM815INJD5C6ljdU6BFa1wNh5hk9iZ4TH5nkdtn/qN9OSRGQD7lQ8uQbiw
+IQrSTGg1r3Bh1cB9N1OfQxVQAqzAQ0NU3haSmIyvwrl4hF52iz3N4N12uQCBy+bW
+q5a0oNYSO2gj34xdo3rB3m5g2uROMNcMh0PwCCXMppWs0auN1KCjF+muEhJN/Yhm
+WMQyy3C5ATSH5SRbqG7Ohmscw3sBEb6FAgMBAAGjZzBlMA4GA1UdDwEB/wQEAwIF
+oDATBgNVHSUEDDAKBggrBgEFBQcDATAJBgNVHRMEAjAAMBIGA1UdEQQLMAmCB3Rv
+b2xib3gwHwYDVR0jBBgwFoAUxlnwHmO9E7pYUjB5uN04O1rrOSowDQYJKoZIhvcN
+AQELBQADggIBADe29TTrmmycf4xhvWHieVNqq51GPcrEUFHWbAv7VsZdz1XBT9X/
+YWUqmjSVdz0WXG7DJWI1uuX0HuAJOkYcjs74USxQ4slHtdVWZxKlh6LjFuxnnwxn
+Ud0udAwuZAs2gKauFK0A08JVGqIgiNxu9a7V2Oip2OeBE08DvgJISeF3ubjfHyBA
+nc04FCVw4gu3ExB2fBJmFKLXTYFiFqjkWx3Gob1dEMnAKQfqBvrEPXv3m3N97Tvz
+KUHtSDC30xpaSnAcV36FPCenU2xZo7KhU+tM/kHI+v5xm0LxcM6XkdRNq9quVKa/
+qtUyXokN/4o+G9259fcSulelVDPlHsEW6dJxF9PagK5FvH2qoab+x0kPLFZo3f26
+qJzmzVooPXj2A6twUypo/aX/+EY/ij5SQJsLPUOh5Sg+dsoNXj/BCUalZeWeJZbp
+bu0QAsgZZNwLoj1Wx9mk6ipSkPpqXG9yStasR07wT2WE1ORTw0hwTC9roQRf7Tz3
+0OjuLQB6oyUXoD2/QZpdDjbgsJ8MZNX7MyrIeSz5a6nNFhwmDFvT+IctRdd4qIjD
+2bll9ob35huxF14FYmDM6XmEyTwZVvw3Jy0uTB/clnTYTqR2htdCByViknOIJTSA
+qDCMPPi62OS4TJzschKe820HvfawJqU6M1UgepZeE0LPco/+3jl2jDyO
+-----END CERTIFICATE-----
diff --git a/test/data/100years/0-self-signed.key b/test/data/100years/0-self-signed.key
new file mode 100644
index 0000000..a471c7f
--- /dev/null
+++ b/test/data/100years/0-self-signed.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDukbLhxSdbsruC
+nnXexEZTK8JJ5LEtMi7rWbqUYsBRhePF0oYhu9ccMKN9g5T6U0t70vY6sl2+W2x9
+PeDKeSpfHLMd8LAMzvT2Ejc4R2fn04s4pERBY64HHaTKBmHM815INJD5C6ljdU6B
+Fa1wNh5hk9iZ4TH5nkdtn/qN9OSRGQD7lQ8uQbiwIQrSTGg1r3Bh1cB9N1OfQxVQ
+AqzAQ0NU3haSmIyvwrl4hF52iz3N4N12uQCBy+bWq5a0oNYSO2gj34xdo3rB3m5g
+2uROMNcMh0PwCCXMppWs0auN1KCjF+muEhJN/YhmWMQyy3C5ATSH5SRbqG7Ohmsc
+w3sBEb6FAgMBAAECggEBAKXOBTD8QPpuIfwJ/Ymxh97ODt4kXy82LX3MguwWJeuC
+zmbyhd50GnCxLS9Sf/vqYA/S8uagiJ6AE8qHcu/FfjlUTPiGCxUb0QwkZNRsbD6m
+u5/GKtn3WUAAzkLKZLZ4fhJY1lkATd2d7c2xLzVJp5vRkTyBUqZV5va9tPlsNl7Q
+VkwtLBGrbBYGwpPxdUdMFqgQOBVJwxWIFaHPq0tEa9Lyt7Kf+k3JqKEVlwGR1uVy
+z42uVTBq/YZjtWPr4dNc+kSCC8JmVbWhhLbh5eFEntZyfbFbQJHEn7OlcHs49EBV
+ASwyb6lta8Oq8C38pu5vlGr+xfEB2Mq+Vd0+U7i0rgECgYEA/UEEqWqEHAPoDxPO
+ydbhJHxQfnBdXscY+qHkFpPqMvWZpLmOH9g2AU3cfgkBSYre70vsJCATo6OfNub6
+ibLKFQBVz1KiHtDD970G7PjxnPZwmDbageFOwS3EoQA3tU6KxsDDaEwYFxW1Cnum
+xYdY6Dw7Lt0y7bRA4whbo77sIGUCgYEA8SfrHDAyFImVrB71tZy8Rw/nrrI3i8S+
+sOSKYWobYQpRIc4dzJq8l415IvTI1UNs01f6Xs43QEqf8iOoYRryinVHr3vlo3Tf
+l8xe1PDIytRG5LcsDkZddvh39r4142MRRp8M59sE6RG+KjWCNdJdkzYKfY4QAu57
+ZPzu/vG0c6ECgYAaMi6EQ7NUnobTgG4EbMivGHjPC1ptIlUQH8zTFQvFp6fz5Qmx
+e5Dig6ZKFy5d6BJ1WDod5n+Bjrw6VpwVwDus/nLh1h9U5q1Oo+vieUGsxcURyaLC
+ioa17sEHElmw1GYAdxH/aNeZE7K9Wdr/Vae5pflNOK467klD8qpf/rC4DQKBgDIZ
+V+eoP48ysgxq9iK/qAGfVUNetNltiywWnoT0VTHIwegi+N/buZhGhMb1UY+0jTQu
+z83IS4QovKfCLqXVwlNvX/Ad7iwSCK1xgLX+O8ifZwb24skoZxxjLBDvnSRbHWit
+fDHit1dwHzCHo0BpLpV5oZ5J2BuzzaDCRyhhsFphAoGAeW27iTylnpQy5JKI/Q8v
+hBr9yYT2aW4EhdzN1xTBtEaDyVfhGTuVi2g2QiWqFkBygvL+ZUW0zA8vfUDrBF2J
+B+7Kah9FhQKIvT9TspV3fAPWHUdeNb54uQOE4lxtqOmeS3NopIA167gfqwEXlawx
+HIrK6MOAbs2b+kuf1csWOWM=
+-----END PRIVATE KEY-----
diff --git a/test/data/README b/test/data/README
new file mode 100644
index 0000000..a4400ee
--- /dev/null
+++ b/test/data/README
@@ -0,0 +1,2 @@
+$ faketime '2020-01-01 00:00:00 UTC' sscg -f --lifetime 395 --cert-file=expired.crt --cert-key-file=expired.key
+$ faketime '2020-01-01 00:00:00 UTC' sscg -f --lifetime 3650 --cert-file=100years.crt --cert-key-file=100years.key
diff --git a/test/data/expired/0-self-signed.cert b/test/data/expired/0-self-signed.cert
new file mode 100644
index 0000000..bf0e0e4
--- /dev/null
+++ b/test/data/expired/0-self-signed.cert
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEdDCCAlygAwIBAgIIHpeGfB4itZgwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UE
+BhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS00NDE0MTMx
+NjY4OTIzNzg1MDU5MRAwDgYDVQQDDAd0b29sYm94MB4XDTIwMDEwMTAwMDAwMFoX
+DTIxMDIwNDExNDAwMFowNTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lm
+aWVkMRAwDgYDVQQDDAd0b29sYm94MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA04mDGfc05waOq5gTlGlkCz3cRifibIVxGLiNA/+TF3e+Qw65jNVCcouD
+gM8lc6UPKsj7rmynjJ9RI+Y6rYj4GRKPdF9lZj0dF2myL1jSSo4WAUAzRN/vF05P
+MEHG8Aoh8knHbtuHhKqzSM/IH/KHaTgT7StiGSCjUMeozOQB11BvtLmHuWqwv1+9
+7lAUAh6SxWVXmS8zhPOz1JFPiv6J2wy7X+21DEXMPXAbMbG5WA72f1Z+/3e/Ngz9
+zzYpTJ92u+PTFFP8jMpvHiYdWbHZQpLn8jlZzda6NcwZmiiG9tWf2G9lZ9C7pP5c
+2jW3KnOChEkaSez+Hdts5wHi9EYSyQIDAQABo2cwZTAOBgNVHQ8BAf8EBAMCBaAw
+EwYDVR0lBAwwCgYIKwYBBQUHAwEwCQYDVR0TBAIwADASBgNVHREECzAJggd0b29s
+Ym94MB8GA1UdIwQYMBaAFHQ46yxhOqj4B5rw9Rn1L6qAr+znMA0GCSqGSIb3DQEB
+CwUAA4ICAQAeKQ+4JdU0G7weSWw1Ul4yOm5960QYaDJUHUbelMv/bLlb8km3b3dR
+nRYygYONoTrU8ZES9WBA9GFaOZBFWxIGRjMMcXuILi0hS3nsYzwOPf67LpOaZ8wP
+w7CptIHgi+TirGh8pzQQboHOQzOoSLfzAH4fFRyihN9ydVYLNxecAIwEo8mtOrO5
+9yQn4pdrFNx1B7yrPD7exjHLRDlL5bYmtRDDQpAo5zKLDPKxSj3xBQEDOyA2Oi8o
+IMzfMLXp/QF1hS6MHXdXfCZD9GWSEWS4kEcyUvf8YHFD9e81wduz3s2dHMZaHHTU
+IfFKeUHLhEOk78MFHuOhiAIXkD3G6jwcqQBAOUVuSB1G7+dnUv4HfhpMly+WGE1G
+jdme37emcZorPImh+zoe/cS17M9W6Hy1dLrRFm7SCGDTVIopksnF9mA+A1OLoojb
+LqUgW6CwhUQQTbbx+lk0YU6MN3+YlTqXmSb93DFFqcwKffE+s831Fzk6NBCP6IM9
+d+PQjHFJFYr26sDKdQoDFcbaLcms+kavSl48FmiU03aT68Qb0vKrfB45/uJtWebC
+bIS1CRmiR7jY/pek1YXvMtdNoupCVz5TwIX59cHkVX+sSYAqpKlmS9TpP3CnXegA
+jhGKovR4GG52LTmzMDfn4R0g9FwkK8c9+S6IYW/YMb0/rwnzZNdqRg==
+-----END CERTIFICATE-----
diff --git a/test/data/expired/0-self-signed.key b/test/data/expired/0-self-signed.key
new file mode 100644
index 0000000..cf77c8e
--- /dev/null
+++ b/test/data/expired/0-self-signed.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTiYMZ9zTnBo6r
+mBOUaWQLPdxGJ+JshXEYuI0D/5MXd75DDrmM1UJyi4OAzyVzpQ8qyPuubKeMn1Ej
+5jqtiPgZEo90X2VmPR0XabIvWNJKjhYBQDNE3+8XTk8wQcbwCiHyScdu24eEqrNI
+z8gf8odpOBPtK2IZIKNQx6jM5AHXUG+0uYe5arC/X73uUBQCHpLFZVeZLzOE87PU
+kU+K/onbDLtf7bUMRcw9cBsxsblYDvZ/Vn7/d782DP3PNilMn3a749MUU/yMym8e
+Jh1ZsdlCkufyOVnN1ro1zBmaKIb21Z/Yb2Vn0Luk/lzaNbcqc4KESRpJ7P4d22zn
+AeL0RhLJAgMBAAECggEBAIblvX4UgF8HdnoE7A+LuenVVCBFFQaBfmL2Za70D5g6
+m1FmFXuzD8G+KGk/MDH0Q5zUOHO+BIcPng3Xpvm9TAGpLWRX5XDybhfdJm09JjW3
+MF8x0sAMUgs5vBQZDkVLIOTCmfSe+Gfo95XqM0P0RNgbk9F+BfviLWO5P3MRv/7O
+uuVaZqllZU0UMdh+wyQ5OoxUtgTBx/v9iUUqheavvURJXw3kgiLlRSGjsfKoEca2
+nG4Be9LH+OLRDve1O9GNsQ2enIj33lTuxnHV/tEkXESqk30RRpz80exkTxrF7V0s
+Vl2bwAU4lcLjW3SKg8utB13mcIzO5iMDbx8f+U5QsYECgYEA7LTY08SWJTdrJq0k
+I0UyiwG00/T4bA7VXGt9JCiBb87slIspCbzCvTaOR98jJ1ufFfXYNnhPoBy6riPU
+Wa99oXkbFOdeNurFT/M3pVNO/0AIOUxaqIxxAXUehUUXCjX9Y5M0JRH4xdkw/Big
+nM9bkaLzFt8J16ANbn1H50CBKnkCgYEA5Md6pzqd7Sxx018VokDB3tMi4UQIo0xb
+uQbwPlEvwZom5+vesimX0uAwWZCxch942ylLQnGb1TWMPy/3Oa7ZkuPUb3RGo1gO
+CCzbu7jqsVSvRkqMHhJzDRk2q2mTd9Jb7cePvEiVnzKyS5sPTy4rs4jUs+uS28xq
+N0QK0u+MFtECgYEAx5lMCW6I6YMMwMVZZsBv+VWMBPtaC8yKT+93y+i4FO8ZAb1C
+8qoQUHtTiPl47DRPNrZjBrN2+V2gcT1XT/Uya9nyYzMieA6KKXu/HZLdV1HIXYCs
+JdqfZAo9nUWAQyT5sWCspOG9tCAMNVgS/4e/AFS2xT94TjHxgYz09+SlxokCgYAq
++ABkalEBG8jhL0mr6MfA5xz0/Ec3DH9puBDlU3BJx38pLhbslX1LgQnzuIZrbdfQ
+KehVuWrQV7dYotSnQ9zfhuT+bfNaeI5iILLAhlGmLsagyB2bBFIRkru+5Bxrc9oK
+ReIMNgHMHcR5CM2OTJiuprN0e3lgyPrmGGSEC7PfEQKBgEm8RFLV2GtqRGwNyns+
+gV+SmQZeMhZqDY8+KYNMxcLWn1hlkETelfkW/K57uimXRyJR2qD7ztI2s4IB64Sm
+r7DyKt3/fLY/q/K/4wun4Jmnm4iPG1u9ypwcUF/jC2+SGJrZkFpJGwekLxfPxnlD
+JBQMYVyUqKk01ZNKBPiRPNoc
+-----END PRIVATE KEY-----
diff --git a/test/data/expired/1.cert b/test/data/expired/1.cert
new file mode 100644
index 0000000..bf0e0e4
--- /dev/null
+++ b/test/data/expired/1.cert
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEdDCCAlygAwIBAgIIHpeGfB4itZgwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UE
+BhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS00NDE0MTMx
+NjY4OTIzNzg1MDU5MRAwDgYDVQQDDAd0b29sYm94MB4XDTIwMDEwMTAwMDAwMFoX
+DTIxMDIwNDExNDAwMFowNTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lm
+aWVkMRAwDgYDVQQDDAd0b29sYm94MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA04mDGfc05waOq5gTlGlkCz3cRifibIVxGLiNA/+TF3e+Qw65jNVCcouD
+gM8lc6UPKsj7rmynjJ9RI+Y6rYj4GRKPdF9lZj0dF2myL1jSSo4WAUAzRN/vF05P
+MEHG8Aoh8knHbtuHhKqzSM/IH/KHaTgT7StiGSCjUMeozOQB11BvtLmHuWqwv1+9
+7lAUAh6SxWVXmS8zhPOz1JFPiv6J2wy7X+21DEXMPXAbMbG5WA72f1Z+/3e/Ngz9
+zzYpTJ92u+PTFFP8jMpvHiYdWbHZQpLn8jlZzda6NcwZmiiG9tWf2G9lZ9C7pP5c
+2jW3KnOChEkaSez+Hdts5wHi9EYSyQIDAQABo2cwZTAOBgNVHQ8BAf8EBAMCBaAw
+EwYDVR0lBAwwCgYIKwYBBQUHAwEwCQYDVR0TBAIwADASBgNVHREECzAJggd0b29s
+Ym94MB8GA1UdIwQYMBaAFHQ46yxhOqj4B5rw9Rn1L6qAr+znMA0GCSqGSIb3DQEB
+CwUAA4ICAQAeKQ+4JdU0G7weSWw1Ul4yOm5960QYaDJUHUbelMv/bLlb8km3b3dR
+nRYygYONoTrU8ZES9WBA9GFaOZBFWxIGRjMMcXuILi0hS3nsYzwOPf67LpOaZ8wP
+w7CptIHgi+TirGh8pzQQboHOQzOoSLfzAH4fFRyihN9ydVYLNxecAIwEo8mtOrO5
+9yQn4pdrFNx1B7yrPD7exjHLRDlL5bYmtRDDQpAo5zKLDPKxSj3xBQEDOyA2Oi8o
+IMzfMLXp/QF1hS6MHXdXfCZD9GWSEWS4kEcyUvf8YHFD9e81wduz3s2dHMZaHHTU
+IfFKeUHLhEOk78MFHuOhiAIXkD3G6jwcqQBAOUVuSB1G7+dnUv4HfhpMly+WGE1G
+jdme37emcZorPImh+zoe/cS17M9W6Hy1dLrRFm7SCGDTVIopksnF9mA+A1OLoojb
+LqUgW6CwhUQQTbbx+lk0YU6MN3+YlTqXmSb93DFFqcwKffE+s831Fzk6NBCP6IM9
+d+PQjHFJFYr26sDKdQoDFcbaLcms+kavSl48FmiU03aT68Qb0vKrfB45/uJtWebC
+bIS1CRmiR7jY/pek1YXvMtdNoupCVz5TwIX59cHkVX+sSYAqpKlmS9TpP3CnXegA
+jhGKovR4GG52LTmzMDfn4R0g9FwkK8c9+S6IYW/YMb0/rwnzZNdqRg==
+-----END CERTIFICATE-----
diff --git a/test/data/expired/1.key b/test/data/expired/1.key
new file mode 100644
index 0000000..cf77c8e
--- /dev/null
+++ b/test/data/expired/1.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTiYMZ9zTnBo6r
+mBOUaWQLPdxGJ+JshXEYuI0D/5MXd75DDrmM1UJyi4OAzyVzpQ8qyPuubKeMn1Ej
+5jqtiPgZEo90X2VmPR0XabIvWNJKjhYBQDNE3+8XTk8wQcbwCiHyScdu24eEqrNI
+z8gf8odpOBPtK2IZIKNQx6jM5AHXUG+0uYe5arC/X73uUBQCHpLFZVeZLzOE87PU
+kU+K/onbDLtf7bUMRcw9cBsxsblYDvZ/Vn7/d782DP3PNilMn3a749MUU/yMym8e
+Jh1ZsdlCkufyOVnN1ro1zBmaKIb21Z/Yb2Vn0Luk/lzaNbcqc4KESRpJ7P4d22zn
+AeL0RhLJAgMBAAECggEBAIblvX4UgF8HdnoE7A+LuenVVCBFFQaBfmL2Za70D5g6
+m1FmFXuzD8G+KGk/MDH0Q5zUOHO+BIcPng3Xpvm9TAGpLWRX5XDybhfdJm09JjW3
+MF8x0sAMUgs5vBQZDkVLIOTCmfSe+Gfo95XqM0P0RNgbk9F+BfviLWO5P3MRv/7O
+uuVaZqllZU0UMdh+wyQ5OoxUtgTBx/v9iUUqheavvURJXw3kgiLlRSGjsfKoEca2
+nG4Be9LH+OLRDve1O9GNsQ2enIj33lTuxnHV/tEkXESqk30RRpz80exkTxrF7V0s
+Vl2bwAU4lcLjW3SKg8utB13mcIzO5iMDbx8f+U5QsYECgYEA7LTY08SWJTdrJq0k
+I0UyiwG00/T4bA7VXGt9JCiBb87slIspCbzCvTaOR98jJ1ufFfXYNnhPoBy6riPU
+Wa99oXkbFOdeNurFT/M3pVNO/0AIOUxaqIxxAXUehUUXCjX9Y5M0JRH4xdkw/Big
+nM9bkaLzFt8J16ANbn1H50CBKnkCgYEA5Md6pzqd7Sxx018VokDB3tMi4UQIo0xb
+uQbwPlEvwZom5+vesimX0uAwWZCxch942ylLQnGb1TWMPy/3Oa7ZkuPUb3RGo1gO
+CCzbu7jqsVSvRkqMHhJzDRk2q2mTd9Jb7cePvEiVnzKyS5sPTy4rs4jUs+uS28xq
+N0QK0u+MFtECgYEAx5lMCW6I6YMMwMVZZsBv+VWMBPtaC8yKT+93y+i4FO8ZAb1C
+8qoQUHtTiPl47DRPNrZjBrN2+V2gcT1XT/Uya9nyYzMieA6KKXu/HZLdV1HIXYCs
+JdqfZAo9nUWAQyT5sWCspOG9tCAMNVgS/4e/AFS2xT94TjHxgYz09+SlxokCgYAq
++ABkalEBG8jhL0mr6MfA5xz0/Ec3DH9puBDlU3BJx38pLhbslX1LgQnzuIZrbdfQ
+KehVuWrQV7dYotSnQ9zfhuT+bfNaeI5iILLAhlGmLsagyB2bBFIRkru+5Bxrc9oK
+ReIMNgHMHcR5CM2OTJiuprN0e3lgyPrmGGSEC7PfEQKBgEm8RFLV2GtqRGwNyns+
+gV+SmQZeMhZqDY8+KYNMxcLWn1hlkETelfkW/K57uimXRyJR2qD7ztI2s4IB64Sm
+r7DyKt3/fLY/q/K/4wun4Jmnm4iPG1u9ypwcUF/jC2+SGJrZkFpJGwekLxfPxnlD
+JBQMYVyUqKk01ZNKBPiRPNoc
+-----END PRIVATE KEY-----
diff --git a/test/data/expired/combined.cert b/test/data/expired/combined.cert
new file mode 100644
index 0000000..7e6c4fb
--- /dev/null
+++ b/test/data/expired/combined.cert
@@ -0,0 +1,54 @@
+-----BEGIN CERTIFICATE-----
+MIIEdDCCAlygAwIBAgIIHpeGfB4itZgwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UE
+BhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lmaWVkMR8wHQYDVQQLDBZjYS00NDE0MTMx
+NjY4OTIzNzg1MDU5MRAwDgYDVQQDDAd0b29sYm94MB4XDTIwMDEwMTAwMDAwMFoX
+DTIxMDIwNDExNDAwMFowNTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC1Vuc3BlY2lm
+aWVkMRAwDgYDVQQDDAd0b29sYm94MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA04mDGfc05waOq5gTlGlkCz3cRifibIVxGLiNA/+TF3e+Qw65jNVCcouD
+gM8lc6UPKsj7rmynjJ9RI+Y6rYj4GRKPdF9lZj0dF2myL1jSSo4WAUAzRN/vF05P
+MEHG8Aoh8knHbtuHhKqzSM/IH/KHaTgT7StiGSCjUMeozOQB11BvtLmHuWqwv1+9
+7lAUAh6SxWVXmS8zhPOz1JFPiv6J2wy7X+21DEXMPXAbMbG5WA72f1Z+/3e/Ngz9
+zzYpTJ92u+PTFFP8jMpvHiYdWbHZQpLn8jlZzda6NcwZmiiG9tWf2G9lZ9C7pP5c
+2jW3KnOChEkaSez+Hdts5wHi9EYSyQIDAQABo2cwZTAOBgNVHQ8BAf8EBAMCBaAw
+EwYDVR0lBAwwCgYIKwYBBQUHAwEwCQYDVR0TBAIwADASBgNVHREECzAJggd0b29s
+Ym94MB8GA1UdIwQYMBaAFHQ46yxhOqj4B5rw9Rn1L6qAr+znMA0GCSqGSIb3DQEB
+CwUAA4ICAQAeKQ+4JdU0G7weSWw1Ul4yOm5960QYaDJUHUbelMv/bLlb8km3b3dR
+nRYygYONoTrU8ZES9WBA9GFaOZBFWxIGRjMMcXuILi0hS3nsYzwOPf67LpOaZ8wP
+w7CptIHgi+TirGh8pzQQboHOQzOoSLfzAH4fFRyihN9ydVYLNxecAIwEo8mtOrO5
+9yQn4pdrFNx1B7yrPD7exjHLRDlL5bYmtRDDQpAo5zKLDPKxSj3xBQEDOyA2Oi8o
+IMzfMLXp/QF1hS6MHXdXfCZD9GWSEWS4kEcyUvf8YHFD9e81wduz3s2dHMZaHHTU
+IfFKeUHLhEOk78MFHuOhiAIXkD3G6jwcqQBAOUVuSB1G7+dnUv4HfhpMly+WGE1G
+jdme37emcZorPImh+zoe/cS17M9W6Hy1dLrRFm7SCGDTVIopksnF9mA+A1OLoojb
+LqUgW6CwhUQQTbbx+lk0YU6MN3+YlTqXmSb93DFFqcwKffE+s831Fzk6NBCP6IM9
+d+PQjHFJFYr26sDKdQoDFcbaLcms+kavSl48FmiU03aT68Qb0vKrfB45/uJtWebC
+bIS1CRmiR7jY/pek1YXvMtdNoupCVz5TwIX59cHkVX+sSYAqpKlmS9TpP3CnXegA
+jhGKovR4GG52LTmzMDfn4R0g9FwkK8c9+S6IYW/YMb0/rwnzZNdqRg==
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTiYMZ9zTnBo6r
+mBOUaWQLPdxGJ+JshXEYuI0D/5MXd75DDrmM1UJyi4OAzyVzpQ8qyPuubKeMn1Ej
+5jqtiPgZEo90X2VmPR0XabIvWNJKjhYBQDNE3+8XTk8wQcbwCiHyScdu24eEqrNI
+z8gf8odpOBPtK2IZIKNQx6jM5AHXUG+0uYe5arC/X73uUBQCHpLFZVeZLzOE87PU
+kU+K/onbDLtf7bUMRcw9cBsxsblYDvZ/Vn7/d782DP3PNilMn3a749MUU/yMym8e
+Jh1ZsdlCkufyOVnN1ro1zBmaKIb21Z/Yb2Vn0Luk/lzaNbcqc4KESRpJ7P4d22zn
+AeL0RhLJAgMBAAECggEBAIblvX4UgF8HdnoE7A+LuenVVCBFFQaBfmL2Za70D5g6
+m1FmFXuzD8G+KGk/MDH0Q5zUOHO+BIcPng3Xpvm9TAGpLWRX5XDybhfdJm09JjW3
+MF8x0sAMUgs5vBQZDkVLIOTCmfSe+Gfo95XqM0P0RNgbk9F+BfviLWO5P3MRv/7O
+uuVaZqllZU0UMdh+wyQ5OoxUtgTBx/v9iUUqheavvURJXw3kgiLlRSGjsfKoEca2
+nG4Be9LH+OLRDve1O9GNsQ2enIj33lTuxnHV/tEkXESqk30RRpz80exkTxrF7V0s
+Vl2bwAU4lcLjW3SKg8utB13mcIzO5iMDbx8f+U5QsYECgYEA7LTY08SWJTdrJq0k
+I0UyiwG00/T4bA7VXGt9JCiBb87slIspCbzCvTaOR98jJ1ufFfXYNnhPoBy6riPU
+Wa99oXkbFOdeNurFT/M3pVNO/0AIOUxaqIxxAXUehUUXCjX9Y5M0JRH4xdkw/Big
+nM9bkaLzFt8J16ANbn1H50CBKnkCgYEA5Md6pzqd7Sxx018VokDB3tMi4UQIo0xb
+uQbwPlEvwZom5+vesimX0uAwWZCxch942ylLQnGb1TWMPy/3Oa7ZkuPUb3RGo1gO
+CCzbu7jqsVSvRkqMHhJzDRk2q2mTd9Jb7cePvEiVnzKyS5sPTy4rs4jUs+uS28xq
+N0QK0u+MFtECgYEAx5lMCW6I6YMMwMVZZsBv+VWMBPtaC8yKT+93y+i4FO8ZAb1C
+8qoQUHtTiPl47DRPNrZjBrN2+V2gcT1XT/Uya9nyYzMieA6KKXu/HZLdV1HIXYCs
+JdqfZAo9nUWAQyT5sWCspOG9tCAMNVgS/4e/AFS2xT94TjHxgYz09+SlxokCgYAq
++ABkalEBG8jhL0mr6MfA5xz0/Ec3DH9puBDlU3BJx38pLhbslX1LgQnzuIZrbdfQ
+KehVuWrQV7dYotSnQ9zfhuT+bfNaeI5iILLAhlGmLsagyB2bBFIRkru+5Bxrc9oK
+ReIMNgHMHcR5CM2OTJiuprN0e3lgyPrmGGSEC7PfEQKBgEm8RFLV2GtqRGwNyns+
+gV+SmQZeMhZqDY8+KYNMxcLWn1hlkETelfkW/K57uimXRyJR2qD7ztI2s4IB64Sm
+r7DyKt3/fLY/q/K/4wun4Jmnm4iPG1u9ypwcUF/jC2+SGJrZkFpJGwekLxfPxnlD
+JBQMYVyUqKk01ZNKBPiRPNoc
+-----END PRIVATE KEY-----
diff --git a/test/example/check-example b/test/example/check-example
new file mode 100755
index 0000000..829bdad
--- /dev/null
+++ b/test/example/check-example
@@ -0,0 +1,104 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import time
+import unittest
+
+import testlib
+
+
+@testlib.skipDistroPackage()
+class TestExample(testlib.MachineCase):
+
+ @unittest.expectedFailure
+ def testExpectedFail(self):
+ self.login_and_go("/system")
+ self.assertFalse(True) # noqa: FBT003
+
+ def testFail(self):
+ self.login_and_go("/system")
+ if os.environ.get('TEST_FAILURES'):
+ self.assertFalse(True) # noqa: FBT003
+
+ @unittest.skip
+ def testSkip(self):
+ self.login_and_go("/system")
+ self.assertFalse(True) # noqa: FBT003
+
+ def testRaiseSkip(self):
+ raise unittest.SkipTest("dynamic skip")
+
+ @testlib.nondestructive
+ def testNondestructive(self):
+ self.login_and_go("/system")
+
+ def testBasic(self):
+ self.login_and_go("/system")
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestNondestructiveExample(testlib.MachineCase):
+
+ def testOne(self):
+ self.assertEqual(self.machine.execute("whoami").strip(), "root")
+
+ def testTwo(self):
+ self.assertIn("usr", self.machine.execute("ls -l /").strip())
+
+
+@testlib.skipDistroPackage()
+class TestSimple(unittest.TestCase):
+
+ def testOne(self):
+ for i in range(10):
+ time.sleep(0.100)
+ if i % 2:
+ sys.stderr.write(">1%i\n" % i)
+ else:
+ print(">2", i)
+ self.assertTrue(True) # noqa: FBT003
+
+ @unittest.expectedFailure
+ def testTwo(self):
+ self.assertFalse(True) # noqa: FBT003
+
+ def testThree(self):
+ self.assertTrue(True) # noqa: FBT003
+
+
+@testlib.skipDistroPackage()
+class TestTodo(unittest.TestCase):
+ def setUp(self):
+ if not os.environ.get('TEST_TODO'):
+ self.skipTest('not testing TestTodo')
+
+ @testlib.todo('2 is not yet sufficiently large')
+ def testTodoFail(self):
+ assert 2 + 2 == 5
+
+ @testlib.todo('2 is not yet sufficiently large')
+ def testTodoPass(self):
+ assert 2 + 2 == 4
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/example/check-retry b/test/example/check-retry
new file mode 100755
index 0000000..5482f54
--- /dev/null
+++ b/test/example/check-retry
@@ -0,0 +1,24 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+import os
+import unittest
+
+import testlib
+
+
+@testlib.skipDistroPackage()
+class NoTestRetryExample(unittest.TestCase):
+
+ def testFail(self):
+ if os.environ.get('TEST_FAILURES'):
+ self.assertFalse(True) # noqa: FBT003
+
+ @testlib.no_retry_when_changed
+ def testNoRetry(self):
+ self.assertTrue(True) # noqa: FBT003
+
+ def testBasic(self):
+ self.assertTrue(True) # noqa: FBT003
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/example/ruff.toml b/test/example/ruff.toml
new file mode 100644
index 0000000..4ea884a
--- /dev/null
+++ b/test/example/ruff.toml
@@ -0,0 +1,6 @@
+extend = "../../pyproject.toml"
+
+[lint]
+ignore = [
+ "PT009", # Use a regular `assert` instead of unittest-style `assertEqual`
+]
diff --git a/test/image-prepare b/test/image-prepare
new file mode 100755
index 0000000..9314976
--- /dev/null
+++ b/test/image-prepare
@@ -0,0 +1,278 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)
+# Build and run a bots/image-customize command to prepare a VM for testing Cockpit.
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import os
+import shlex
+import shutil
+import subprocess
+import sys
+
+from lib import testmap
+from lib.constants import DEFAULT_IMAGE
+from machine.machine_core import machine_virtual
+
+BASE_DIR = os.path.realpath(f'{__file__}/../..')
+TEST_DIR = f'{BASE_DIR}/test'
+BOTS_DIR = f'{BASE_DIR}/bots'
+
+
+def build_rpms(dist_tar, image, verbose, quick):
+ """build RPMs from a tarball in an image
+
+ Return local rpm path list.
+ """
+ subprocess.check_call([os.path.join(BOTS_DIR, "image-download"), image])
+ machine = machine_virtual.VirtMachine(image=image)
+ try:
+ machine.start()
+ machine.wait_boot()
+ vm_tar = os.path.join("/var/tmp", os.path.basename(dist_tar))
+ machine.upload([dist_tar], vm_tar)
+
+ # build srpm
+ machine.execute(fr"""
+ su builder -c 'rpmbuild --define "_topdir /var/tmp/build" -ts "{vm_tar}"'
+ """)
+
+ # build rpms
+ mock_opts = ("--verbose" if verbose else "") + (" --nocheck" if quick else "")
+ machine.execute(fr"""
+ su builder -c 'mock --no-clean --disablerepo=* --offline \
+ --resultdir /var/tmp/build {mock_opts} \
+ --rebuild /var/tmp/build/SRPMS/*.src.rpm'
+ """, timeout=1800)
+
+ # download rpms
+ vm_rpms = machine.execute("find /var/tmp/build -name '*.rpm' -not -name '*.src.rpm'").strip().split()
+
+ destdir = os.path.abspath("tmp/rpms")
+ if os.path.exists(destdir):
+ shutil.rmtree(destdir)
+ os.makedirs(destdir)
+
+ rpms = []
+ for rpm in vm_rpms:
+ machine.download(rpm, destdir)
+ rpms.append(os.path.join(destdir, os.path.basename(rpm)))
+ return rpms
+ finally:
+ machine.stop()
+
+
+#
+# Helper functions to build image-customize options for various steps
+#
+
+def build_install_package(dist_tar, image):
+ """Default rpm/deb/arch package build/install"""
+
+ # our images have distro cockpit packages pre-installed, remove them
+ args = ["--run-command"]
+ if 'debian' in image or 'ubuntu' in image:
+ args.append("dpkg --purge cockpit cockpit-ws cockpit-bridge cockpit-system")
+ else:
+ # subscription-manager-cockpit needs these, thus --nodeps
+ args.append(r"""
+ if rpm -q cockpit-ws >/dev/null 2>&1
+ then rpm --erase --nodeps --verbose cockpit cockpit-ws cockpit-bridge cockpit-system
+ fi
+ """)
+
+ args += ["--build", dist_tar]
+
+ if 'debian' in image or 'ubuntu' in image:
+ args.append("--run-command")
+ suppress = {'initial-upload-closes-no-bugs', 'newer-standards-version'}
+ # older lintian not yet complain about pkg/static/login.html and src/common/fail.html
+ if image in ["ubuntu-2204"]:
+ suppress.add("mismatched-override")
+ # Ubuntu 22.04 raises elf-error on *-dbgsym: "In program headers: Unable to find program interpreter name"
+ if image == "ubuntu-2204":
+ suppress.add("elf-error")
+
+ args.append(fr"""
+ cd /var/tmp/build;
+ runuser -u admin -- \
+ lintian --fail-on warning,error \
+ --tag-display-limit 0 --display-info \
+ --suppress-tags {','.join(suppress)} \
+ cockpit*.changes >&2""")
+ return args
+
+
+def build_install_ostree(dist_tar, image, verbose, quick):
+ """Special treatment of build/install on CoreOS
+
+ OSTree image can't build packages, build them on corresponding OSes and invoke
+ test/ostree.install to install them into the OSTree and the cockpit/ws container.
+ """
+ rpms = build_rpms(dist_tar, testmap.get_build_image(image), verbose, quick)
+ args = []
+ for rpm in rpms:
+ args += ["--upload", f"{rpm}:/var/tmp/"]
+ args += [
+ "--upload", os.path.join(BASE_DIR, "containers") + ":/var/tmp/",
+ "--script", os.path.join(TEST_DIR, "ostree.install")]
+ return args
+
+
+def build_install_rhel8(dist_tar, image, verbose, quick):
+ """Special treatment of build/install on RHEL/CentOS 8
+
+ Here, cockpit is delivered as two mostly identical source packages: "cockpit" with
+ build_basic=1, and "cockpit-appstream" with build_optional=1. The spec has proper build_*
+ defaults depending on the Name:.
+ """
+ vm_dist_tar = os.path.join("/var/tmp", os.path.basename(dist_tar))
+
+ args = ["--upload", f"{dist_tar}:{vm_dist_tar}"]
+
+ # in distropkg, keep basic OS packages, otherwise build/install cockpit
+ if 'distropkg' not in image:
+ args += [
+ "--run-command",
+ fr"""
+ # remove already installed packages
+ if rpm -q cockpit-ws >/dev/null 2>&1; then
+ rpm --erase --nodeps --verbose cockpit cockpit-ws cockpit-bridge cockpit-system
+ fi
+
+ # create cockpit.spec
+ tar xf '{vm_dist_tar}' -O '*/tools/cockpit.spec' | sed '/%define build_all/d' > /var/tmp/cockpit.spec
+
+ # create srpm
+ su builder -c 'rpmbuild --define "_topdir /var/tmp/build" \
+ --define "_sourcedir /var/tmp" \
+ -bs /var/tmp/cockpit.spec'
+
+ # build rpms in mock
+ su builder -c '
+ mock --no-clean --no-cleanup-after \
+ --disablerepo=* \
+ --offline \
+ --resultdir /var/tmp/build \
+ {"--nocheck" if quick else ""} \
+ {"--verbose" if verbose else ""} \
+ --rebuild /var/tmp/build/SRPMS/*.src.rpm'
+
+ # install rpms
+ rpm -U --force --verbose $(find /var/tmp/build -name '*.rpm' -not -name '*.src.rpm')
+ """
+ ]
+
+ # always build cockpit-appstream
+ args += [
+ "--run-command",
+ fr"""
+ # create cockpit-appstream.spec
+ tar xf '{vm_dist_tar}' -O '*/tools/cockpit.spec' |
+ sed '/^Name:/ s/$/-appstream/; /%define build_all/d' > /var/tmp/cockpit-appstream.spec
+
+ # create srpm
+ su builder -c 'rpmbuild --define "_topdir /var/tmp/appstream" \
+ --define "_sourcedir /var/tmp" \
+ -bs /var/tmp/cockpit-appstream.spec'
+
+ # build rpms in mock
+ su builder -c 'mock --no-clean --disablerepo=* --offline \
+ --resultdir /var/tmp/appstream \
+ --nocheck {"--verbose" if verbose else ""} \
+ --rebuild /var/tmp/appstream/SRPMS/*.src.rpm'
+
+ # install rpms
+ rpm -i --verbose $(find /var/tmp/appstream -name '*.rpm' -not -name '*.src.rpm')
+ """
+ ]
+ return args
+
+
+def validate_packages():
+ """Post-install package checks"""
+
+ # check for files that are shipped by more than one RPM
+ return ["--run-command",
+ """set -eu
+ fail=
+ for f in $(find $(rpm -ql $(rpm -qa '*cockpit*') | sort | uniq -d) -maxdepth 0 -type f); do
+ # -debugsource overlap is legit
+ [ "${f#/usr/src/debug}" = "$f" ] || continue
+ echo "ERROR: $f is shipped by multiple packages: $(rpm -qf $f)" >&2
+ fail=1
+ done
+ [ -z "${fail}" ] || exit 1
+ """]
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Prepare testing environment, download images and build and install cockpit',
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+ parser.add_argument('-e', '--env', metavar='NAME=VAL', action='append',
+ help="Add a line to /etc/environment")
+ parser.add_argument('-d', '--debug', action='append_const', dest='env', const='COCKPIT_DEBUG=all',
+ help="Equivalent to --env=COCKPIT_DEBUG=all")
+ parser.add_argument('-v', '--verbose', action='store_true', help='Display verbose progress details')
+ parser.add_argument('-q', '--quick', action='store_true', help='Skip unit tests to build faster')
+ parser.add_argument('-o', '--overlay', action='store_true',
+ help='Install into existing test/image/ overlay instead of from pristine base image')
+ parser.add_argument('-p', '--python', action='store_true',
+ help='Install the python bridge into the image (experimental)')
+ parser.add_argument('image', nargs='?', default=DEFAULT_IMAGE, help='The image to use')
+ args = parser.parse_args()
+
+ customize = [os.path.join(BOTS_DIR, "image-customize"), "--no-network"]
+ if not args.overlay:
+ customize.append("--fresh")
+ if args.verbose:
+ customize.append("--verbose")
+ if args.quick:
+ customize.append("--quick")
+
+ makevars = ['HACK_SPEC_FOR_PYTHON=1'] if args.python else []
+ dist_tar = subprocess.check_output([f'{BASE_DIR}/tools/make-dist', *makevars], text=True).strip()
+
+ if args.image in ["fedora-coreos", "rhel4edge"]:
+ customize += build_install_ostree(dist_tar, args.image, args.verbose, args.quick)
+ elif args.image.startswith("rhel-8") or args.image.startswith("centos-8"):
+ customize += build_install_rhel8(dist_tar, args.image, args.verbose, args.quick)
+ else:
+ customize += build_install_package(dist_tar, args.image)
+
+ if not args.quick:
+ customize += validate_packages()
+
+ # post build/install test preparation
+ customize += ["--script", os.path.join(TEST_DIR, "vm.install")]
+
+ if args.env:
+ customize += ['--run-command', r'printf "%s\n" >>/etc/environment ' + shlex.join(args.env)]
+
+ customize.append(args.image)
+
+ # show final command for easy copy&paste reproduction/debugging
+ if args.verbose:
+ print(' '.join([shlex.quote(arg) for arg in customize]))
+
+ return subprocess.call(customize)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/test/ostree.install b/test/ostree.install
new file mode 100644
index 0000000..dc8ada8
--- /dev/null
+++ b/test/ostree.install
@@ -0,0 +1,42 @@
+#!/bin/sh
+set -eu
+
+# install/upgrade RPMs that apply to OSTree
+# Note: cockpit-selinux would be desirable, but needs setroubleshoot-server which isn't installed
+cd /var/tmp/
+
+# rhel4edge includes cockpit packages in the image itself, fedora-coreos overlays them
+. /usr/lib/os-release
+if [ "${ID:-}" = "rhel" ]; then
+ rpm-ostree override replace --cache-only cockpit-bridge-*.rpm \
+ cockpit-system-*.rpm
+ rpm-ostree install --cache-only cockpit-tests-*.rpm
+else
+ rpm-ostree install --cache-only cockpit-bridge-*.rpm \
+ cockpit-networkmanager-*.rpm cockpit-system-*.rpm cockpit-tests-*.rpm
+fi
+
+# rhel4edge has passwordless sudo but a loads of tests don't expect that
+if [ "${ID:-}" = "rhel" ]; then
+ sed -i '$ d' /etc/sudoers
+fi
+
+# update cockpit-ws and install scripts in the container
+for rpm in /var/tmp/cockpit-ws-*.rpm /var/tmp/cockpit-bridge-*.rpm; do
+ rpm2cpio "$rpm" | cpio -i --make-directories --directory=/var/tmp/install
+done
+podman run --name build-cockpit -i \
+ -v /var/tmp/:/run/build:Z \
+ quay.io/cockpit/ws sh -exc '
+cp -a /run/build/install/* /
+cp /run/build/containers/ws/label-* /run/build/containers/ws/default-bastion.conf /run/build/containers/ws/cockpit-auth-ssh-key /container/
+'
+podman commit --change CMD=/container/label-run build-cockpit localhost/cockpit/ws
+podman rm -f build-cockpit
+
+# move original quay.io image away, to make sure that our tests use the updated one
+podman tag quay.io/cockpit/ws:latest quay.io/cockpit/original-ws:released
+podman rmi quay.io/cockpit/ws:latest
+
+# run cockpit/ws once to generate certificate; avoids slow down on every start
+podman container runlabel INSTALL cockpit/ws
diff --git a/test/pytest/__init__.py b/test/pytest/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/pytest/__init__.py
diff --git a/test/pytest/conftest.py b/test/pytest/conftest.py
new file mode 100644
index 0000000..46aa068
--- /dev/null
+++ b/test/pytest/conftest.py
@@ -0,0 +1,63 @@
+import asyncio
+import os
+import subprocess
+import sys
+from typing import Iterable
+
+import pytest
+
+from cockpit import polyfills
+from cockpit._vendor.systemd_ctypes import EventLoopPolicy
+
+polyfills.install()
+
+
+def any_subprocesses() -> bool:
+ # Make sure we don't leak subprocesses
+ try:
+ os.waitid(os.P_ALL, -1, os.WEXITED | os.WNOHANG | os.WNOWAIT)
+ except ChildProcessError:
+ return False # good !
+ else:
+ return True # at least one process (or zombie) still waitable
+
+
+@pytest.fixture(autouse=True)
+def event_loop(monkeypatch) -> Iterable[asyncio.AbstractEventLoop]:
+ loop = EventLoopPolicy().new_event_loop()
+
+ if sys.version_info < (3, 7, 0):
+ # Polyfills for Python 3.6:
+ def all_tasks(loop=loop):
+ return {t for t in asyncio.Task.all_tasks(loop=loop) if not t.done()}
+
+ monkeypatch.setattr(asyncio, 'get_running_loop', lambda: loop, raising=False)
+ monkeypatch.setattr(asyncio, 'create_task', loop.create_task, raising=False)
+ monkeypatch.setattr(asyncio, 'all_tasks', all_tasks, raising=False)
+
+ yield loop
+
+ # Let all tasks and subprocesses run to completion
+ for _ in range(200):
+ if not (asyncio.all_tasks(loop) or any_subprocesses()):
+ break
+ loop.run_until_complete(asyncio.sleep(0.005))
+
+ # No tasks left
+ assert asyncio.all_tasks(loop=loop) == set()
+
+ # No subprocesses left
+ if any_subprocesses():
+ # Bad news. Show some helpful output.
+ subprocess.run(['ps', 'f', f'--pid={os.getpid()}', f'--ppid={os.getpid()}'])
+ # clear it out for the sake of the other tests
+ subprocess.run(['pkill', '-9', '-P', f'{os.getpid()}'])
+ try:
+ for _ in range(100): # zombie vacuum
+ os.wait()
+ except ChildProcessError:
+ pass
+
+ pytest.fail('Some subprocesses still running!')
+
+ loop.close()
diff --git a/test/pytest/mockpeer.py b/test/pytest/mockpeer.py
new file mode 100644
index 0000000..b60a231
--- /dev/null
+++ b/test/pytest/mockpeer.py
@@ -0,0 +1,46 @@
+import asyncio
+import os
+import sys
+
+from cockpit._vendor import systemd_ctypes
+from cockpit.router import Router
+from cockpit.transports import StdioTransport
+
+
+class MockPeer(Router):
+ def do_send_init(self):
+ init_type = os.environ.get('INIT_TYPE', None)
+ if init_type == 'wrong-command':
+ self.write_control(command='xnit', version=1)
+ elif init_type == 'wrong-version':
+ self.write_control(command='init', version=2)
+ elif init_type == 'channel-control':
+ self.write_control(command='init', channel='x')
+ elif init_type == 'data':
+ self.write_channel_data('x', b'123')
+ elif init_type == 'break-protocol':
+ print('i like printf debugging', flush=True)
+ elif init_type == 'exit':
+ sys.exit()
+ elif init_type == 'exit-not-found':
+ # shell error code for "command not found"
+ sys.exit(127)
+ elif init_type != 'silence':
+ self.write_control(command='init', version=1)
+
+ def channel_control_received(self, channel, command, message):
+ if command == 'open':
+ self.write_control(command='ready', channel=channel)
+
+ def channel_data_received(self, channel, data):
+ pass
+
+
+async def run():
+ protocol = MockPeer([])
+ StdioTransport(asyncio.get_running_loop(), protocol)
+ await protocol.communicate()
+
+
+if __name__ == '__main__':
+ systemd_ctypes.run_async(run())
diff --git a/test/pytest/mocktransport.py b/test/pytest/mocktransport.py
new file mode 100644
index 0000000..ed2c6f7
--- /dev/null
+++ b/test/pytest/mocktransport.py
@@ -0,0 +1,245 @@
+import asyncio
+import json
+from typing import Any, Dict, Iterable, Optional, Tuple
+
+from cockpit.jsonutil import JsonObject, JsonValue
+from cockpit.router import Router
+
+MOCK_HOSTNAME = 'mockbox'
+
+
+class MockTransport(asyncio.Transport):
+ queue: 'asyncio.Queue[Tuple[str, bytes]]'
+ next_id: int = 0
+ close_future: Optional[asyncio.Future] = None
+
+ async def assert_empty(self):
+ await asyncio.sleep(0.1)
+ assert self.queue.qsize() == 0
+
+ def send_json(self, _channel: str, **kwargs) -> None:
+ # max_read_size is one of our special keys which uses underscores
+ msg = {k.replace('_', '-') if k != "max_read_size" else k: v for k, v in kwargs.items()}
+ self.send_data(_channel, json.dumps(msg).encode('ascii'))
+
+ def send_data(self, channel: str, data: bytes) -> None:
+ msg = channel.encode('ascii') + b'\n' + data
+ msg = str(len(msg)).encode('ascii') + b'\n' + msg
+ self.protocol.data_received(msg)
+
+ def send_init(self, version=1, host=MOCK_HOSTNAME, **kwargs):
+ self.send_json('', command='init', version=version, host=host, **kwargs)
+
+ def init(self, **kwargs: Any) -> Dict[str, object]:
+ channel, data = self.queue.get_nowait()
+ assert channel == ''
+ msg = json.loads(data)
+ assert msg['command'] == 'init'
+ self.send_init(**kwargs)
+ return msg
+
+ def get_id(self, prefix: str) -> str:
+ self.next_id += 1
+ return f'{prefix}.{self.next_id}'
+
+ def send_open(self, payload, channel=None, **kwargs):
+ if channel is None:
+ channel = self.get_id('channel')
+ self.send_json('', command='open', channel=channel, payload=payload, **kwargs)
+ return channel
+
+ async def check_open(
+ self,
+ payload,
+ channel=None,
+ problem=None,
+ reply_keys: Optional[JsonObject] = None,
+ absent_keys: 'Iterable[str]' = (),
+ **kwargs,
+ ):
+ assert isinstance(self.protocol, Router)
+ ch = self.send_open(payload, channel, **kwargs)
+ if problem is None:
+ await self.assert_msg('', command='ready', channel=ch, absent_keys=absent_keys, **(reply_keys or {}))
+ # it's possible that the channel already closed
+ else:
+ await self.assert_msg('', command='close', channel=ch, problem=problem, absent_keys=absent_keys,
+ **(reply_keys or {}))
+ assert ch not in self.protocol.open_channels
+ return ch
+
+ def send_done(self, channel, **kwargs):
+ self.send_json('', command='done', channel=channel, **kwargs)
+
+ def send_close(self, channel, **kwargs):
+ self.send_json('', command='close', channel=channel, **kwargs)
+
+ async def check_close(self, channel, **kwargs):
+ self.send_close(channel, **kwargs)
+ await self.assert_msg('', command='close', channel=channel)
+
+ def send_ping(self, **kwargs):
+ self.send_json('', command='ping', **kwargs)
+
+ def __init__(self, protocol: asyncio.Protocol):
+ self.queue = asyncio.Queue()
+ self.protocol = protocol
+ protocol.connection_made(self)
+
+ def write(self, data: bytes) -> None:
+ # We know that the bridge only ever writes full frames at once, so we
+ # can disassemble them immediately.
+ _, channel, data = data.split(b'\n', 2)
+ self.queue.put_nowait((channel.decode('ascii'), data))
+
+ def stop(self, event_loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
+ keep_open = self.protocol.eof_received()
+ if keep_open:
+ assert event_loop is not None
+ self.close_future = event_loop.create_future()
+ try:
+ event_loop.run_until_complete(self.close_future)
+ finally:
+ self.close_future = None
+
+ def close(self) -> None:
+ if self.close_future is not None:
+ self.close_future.set_result(None)
+
+ if self.protocol is not None:
+ self.protocol.connection_lost(None)
+
+ async def next_frame(self) -> Tuple[str, bytes]:
+ return await self.queue.get()
+
+ async def next_msg(self, expected_channel) -> Dict[str, Any]:
+ channel, data = await self.next_frame()
+ assert channel == expected_channel, data
+ return json.loads(data)
+
+ async def assert_data(self, expected_channel: str, expected_data: bytes) -> None:
+ channel, data = await self.next_frame()
+ assert channel == expected_channel
+ assert data == expected_data
+
+ async def assert_msg(self, expected_channel: str, absent_keys: 'Iterable[str]' = (),
+ **kwargs: JsonValue) -> JsonObject:
+ msg = await self.next_msg(expected_channel)
+ assert msg == dict(msg, **{k.replace('_', '-'): v for k, v in kwargs.items()}), msg
+ for absent_key in absent_keys:
+ assert absent_key not in msg
+ return msg
+
+ # D-Bus helpers
+ internal_bus: str = ''
+
+ async def ensure_internal_bus(self):
+ if not self.internal_bus:
+ self.internal_bus = await self.check_open('dbus-json3', bus='internal')
+ assert self.protocol.open_channels[self.internal_bus].bus == self.protocol.internal_bus.client
+ return self.internal_bus
+
+ def send_bus_call(self, bus: str, path: str, iface: str, name: str, args: list) -> str:
+ tag = self.get_id('call')
+ self.send_json(bus, call=[path, iface, name, args], id=tag)
+ return tag
+
+ async def assert_bus_reply(
+ self,
+ tag: str,
+ expected_reply: Optional[list] = None,
+ bus: Optional[str] = None,
+ ) -> list:
+ if bus is None:
+ bus = await self.ensure_internal_bus()
+ reply = await self.next_msg(bus)
+ assert 'id' in reply, reply
+ assert reply['id'] == tag, reply
+ assert 'reply' in reply, reply
+ if expected_reply is not None:
+ assert reply['reply'] == [expected_reply]
+ return reply['reply'][0]
+
+ async def assert_bus_error(self, tag: str, code: str, message: str, bus: Optional[str] = None) -> None:
+ if bus is None:
+ bus = await self.ensure_internal_bus()
+ reply = await self.next_msg(bus)
+ assert 'id' in reply, reply
+ assert reply['id'] == tag, reply
+ assert 'error' in reply, reply
+ assert reply['error'] == [code, [message]], reply['error']
+
+ async def check_bus_call(
+ self,
+ path: str,
+ iface: str,
+ name: str,
+ args: list,
+ expected_reply: Optional[list] = None,
+ bus: Optional[str] = None,
+ ) -> list:
+ if bus is None:
+ bus = await self.ensure_internal_bus()
+ tag = self.send_bus_call(bus, path, iface, name, args)
+ return await self.assert_bus_reply(tag, expected_reply, bus=bus)
+
+ async def assert_bus_props(
+ self, path: str, iface: str, expected_values: JsonObject, bus: Optional[str] = None
+ ) -> None:
+ (values,) = await self.check_bus_call(path, 'org.freedesktop.DBus.Properties', 'GetAll', [iface], bus=bus)
+ for key, value in expected_values.items():
+ assert values[key]['v'] == value
+
+ async def assert_bus_meta(
+ self,
+ path: str,
+ iface: str,
+ expected: Iterable[str],
+ bus: Optional[str] = None,
+ ) -> None:
+ if bus is None:
+ bus = await self.ensure_internal_bus()
+ meta = await self.next_msg(bus)
+ assert 'meta' in meta, meta
+ assert set(meta['meta'][iface]['properties']) == set(expected)
+
+ async def assert_bus_notify(
+ self,
+ path: str,
+ iface: str,
+ expected: JsonObject,
+ bus: Optional[str] = None,
+ ) -> None:
+ if bus is None:
+ bus = await self.ensure_internal_bus()
+ notify = await self.next_msg(bus)
+ assert 'notify' in notify
+ assert notify['notify'][path][iface] == expected
+
+ async def watch_bus(self, path: str, iface: str, expected: JsonObject, bus: Optional[str] = None) -> None:
+ if bus is None:
+ bus = await self.ensure_internal_bus()
+ tag = self.get_id('watch')
+ self.send_json(bus, watch={'path': path, 'interface': iface}, id=tag)
+ await self.assert_bus_meta(path, iface, expected, bus)
+ await self.assert_bus_notify(path, iface, expected, bus)
+ await self.assert_msg(bus, id=tag, reply=[])
+
+ async def assert_bus_signal(
+ self,
+ path: str,
+ iface: str,
+ name: str,
+ args: list,
+ bus: Optional[str] = None,
+ ) -> None:
+ if bus is None:
+ bus = await self.ensure_internal_bus()
+ signal = await self.next_msg(bus)
+ assert 'signal' in signal, signal
+ assert signal['signal'] == [path, iface, name, args]
+
+ async def add_bus_match(self, path: str, iface: str, bus: Optional[str] = None) -> None:
+ if bus is None:
+ bus = await self.ensure_internal_bus()
+ self.send_json(bus, add_match={'path': path, 'interface': iface})
diff --git a/test/pytest/pseudo.py b/test/pytest/pseudo.py
new file mode 100644
index 0000000..6eaae70
--- /dev/null
+++ b/test/pytest/pseudo.py
@@ -0,0 +1,18 @@
+import os
+import sys
+
+from cockpit._vendor.ferny import interaction_client
+
+pw = os.environ.get('PSEUDO_PASSWORD')
+if pw:
+ reader, writer = os.pipe()
+ # '-' is the (ignored) argv[0], and 'can haz pw' is the message in argv[1]
+ interaction_client.askpass(2, writer, ['-', 'can haz pw?'], {})
+ os.close(writer)
+
+ response = os.read(reader, 1024).decode('utf-8').strip()
+ if response != pw:
+ sys.stderr.write('pseudo says: Bad password\n')
+ sys.exit(1)
+
+os.execvp(sys.argv[1], sys.argv[1:])
diff --git a/test/pytest/test_beiboot.py b/test/pytest/test_beiboot.py
new file mode 100644
index 0000000..8571c02
--- /dev/null
+++ b/test/pytest/test_beiboot.py
@@ -0,0 +1,28 @@
+import sys
+
+import pytest
+
+from cockpit._vendor import ferny
+from cockpit._vendor.bei import bootloader
+from cockpit.beipack import BridgeBeibootHelper
+from cockpit.peer import Peer
+from cockpit.router import Router
+
+
+class BeibootPeer(Peer):
+ async def do_connect_transport(self) -> None:
+ helper = BridgeBeibootHelper(self)
+ agent = ferny.InteractionAgent([helper])
+ transport = await self.spawn([sys.executable, '-iq'], env=[], stderr=agent)
+ transport.write(bootloader.make_bootloader(helper.steps, gadgets=ferny.BEIBOOT_GADGETS).encode())
+ await agent.communicate()
+
+
+@pytest.mark.asyncio
+async def test_bridge_beiboot():
+ # Try to beiboot a copy of the bridge and read its init message
+ peer = BeibootPeer(Router([]))
+ init_msg = await peer.start()
+ assert init_msg['version'] == 1
+ assert 'packages' not in init_msg
+ peer.close()
diff --git a/test/pytest/test_bridge.py b/test/pytest/test_bridge.py
new file mode 100644
index 0000000..3f03bf8
--- /dev/null
+++ b/test/pytest/test_bridge.py
@@ -0,0 +1,1132 @@
+import argparse
+import asyncio
+import contextlib
+import errno
+import getpass
+import grp
+import json
+import os
+import pwd
+import shlex
+import subprocess
+import sys
+import unittest.mock
+from collections import deque
+from pathlib import Path
+from typing import Dict, Iterable, Sequence
+
+import pytest
+
+from cockpit._vendor.systemd_ctypes import bus
+from cockpit.bridge import Bridge
+from cockpit.channel import Channel
+from cockpit.channels import CHANNEL_TYPES
+from cockpit.jsonutil import JsonDict, JsonObject, JsonValue, get_bool, get_dict, get_int, json_merge_patch
+from cockpit.packages import BridgeConfig
+
+from .mocktransport import MOCK_HOSTNAME, MockTransport
+
+
+class test_iface(bus.Object):
+ sig = bus.Interface.Signal('s')
+ prop = bus.Interface.Property('s', value='none')
+
+ @bus.Interface.Method('s')
+ def get_prop(self) -> str:
+ return self.prop
+
+
+@pytest.fixture
+def bridge() -> Bridge:
+ bridge = Bridge(argparse.Namespace(privileged=False, beipack=False))
+ bridge.superuser_bridges = list(bridge.superuser_rule.bridges) # type: ignore[attr-defined]
+ return bridge
+
+
+def add_pseudo(bridge: Bridge) -> None:
+ bridge.more_superuser_bridges = [*bridge.superuser_bridges, 'pseudo'] # type: ignore[attr-defined]
+
+ assert bridge.packages is not None
+
+ # Add pseudo to the existing set of superuser rules
+ bridge.superuser_rule.set_configs([
+ *bridge.packages.get_bridge_configs(),
+ BridgeConfig({
+ 'label': 'pseudo',
+ 'spawn': [
+ sys.executable, os.path.abspath(f'{__file__}/../pseudo.py'),
+ sys.executable, '-m', 'cockpit.bridge', '--privileged'
+ ],
+ 'environ': [
+ f'PYTHONPATH={":".join(sys.path)}'
+ ],
+ 'privileged': True
+ })
+ ])
+
+
+@pytest.fixture
+def no_init_transport(event_loop: asyncio.AbstractEventLoop, bridge: Bridge) -> Iterable[MockTransport]:
+ transport = MockTransport(bridge)
+ try:
+ yield transport
+ finally:
+ transport.stop(event_loop)
+
+
+@pytest.fixture
+def transport(no_init_transport: MockTransport) -> MockTransport:
+ no_init_transport.init()
+ return no_init_transport
+
+
+@pytest.mark.asyncio
+async def test_echo(transport: MockTransport) -> None:
+ echo = await transport.check_open('echo')
+
+ transport.send_data(echo, b'foo')
+ await transport.assert_data(echo, b'foo')
+
+ transport.send_ping(channel=echo)
+ await transport.assert_msg('', command='pong', channel=echo)
+
+ transport.send_done(echo)
+ await transport.assert_msg('', command='done', channel=echo)
+ await transport.assert_msg('', command='close', channel=echo)
+
+
+@pytest.mark.asyncio
+async def test_host(transport: MockTransport) -> None:
+ # try to open a null channel, explicitly naming our host
+ await transport.check_open('null', host=MOCK_HOSTNAME)
+
+ # try to open a null channel, no host
+ await transport.check_open('null')
+
+ # try to open a null channel, a different host which is sure to fail DNS
+ await transport.check_open('null', host='¡invalid!', problem='no-host')
+
+ # make sure host check happens before superuser
+ # ie: requesting superuser=True on another host should fail because we
+ # can't contact the other host ('no-host'), rather than trying to first
+ # go to our superuser bridge ('access-denied')
+ await transport.check_open('null', host='¡invalid!', superuser=True, problem='no-host')
+
+ # but make sure superuser is indeed failing as we expect, on our host
+ await transport.check_open('null', host=MOCK_HOSTNAME, superuser=True, problem='access-denied')
+
+
+@pytest.mark.asyncio
+async def test_dbus_call_internal(bridge: Bridge, transport: MockTransport) -> None:
+ my_object = test_iface()
+ bridge.internal_bus.export('/foo', my_object)
+ assert my_object._dbus_bus == bridge.internal_bus.server
+ assert my_object._dbus_path == '/foo'
+
+ values, = await transport.check_bus_call('/foo', 'org.freedesktop.DBus.Properties',
+ 'GetAll', ["test.iface"])
+ assert values == {'Prop': {'t': 's', 'v': 'none'}}
+
+ result, = await transport.check_bus_call('/foo', 'test.iface', 'GetProp', [])
+ assert result == 'none'
+
+
+@pytest.mark.asyncio
+async def test_dbus_watch(bridge: Bridge, transport: MockTransport) -> None:
+ my_object = test_iface()
+ bridge.internal_bus.export('/foo', my_object)
+ assert my_object._dbus_bus == bridge.internal_bus.server
+ assert my_object._dbus_path == '/foo'
+
+ # Add a watch
+ internal = await transport.ensure_internal_bus()
+
+ transport.send_json(internal, watch={'path': '/foo', 'interface': 'test.iface'}, id='4')
+ meta = await transport.next_msg(internal)
+ assert meta['meta']['test.iface'] == {
+ 'methods': {'GetProp': {'in': [], 'out': ['s']}},
+ 'properties': {'Prop': {'flags': 'r', 'type': 's'}},
+ 'signals': {'Sig': {'in': ['s']}}
+ }
+ notify = await transport.next_msg(internal)
+ assert notify['notify']['/foo'] == {'test.iface': {'Prop': 'none'}}
+ reply = await transport.next_msg(internal)
+ assert reply == {'id': '4', 'reply': []}
+
+ # Change a property
+ my_object.prop = 'xyz'
+ notify = await transport.next_msg(internal)
+ assert 'notify' in notify
+ assert notify['notify']['/foo'] == {'test.iface': {'Prop': 'xyz'}}
+
+
+async def verify_root_bridge_not_running(bridge: Bridge, transport: MockTransport) -> None:
+ assert bridge.superuser_rule.peer is None
+ await transport.assert_bus_props('/superuser', 'cockpit.Superuser',
+ {'Bridges': bridge.more_superuser_bridges, 'Current': 'none'}) # type: ignore[attr-defined]
+ null = await transport.check_open('null', superuser=True, problem='access-denied')
+ assert null not in bridge.open_channels
+
+
+@pytest.mark.asyncio
+async def verify_root_bridge_running(bridge: Bridge, transport: MockTransport) -> None:
+ await transport.assert_bus_props('/superuser', 'cockpit.Superuser',
+ {'Bridges': bridge.more_superuser_bridges, 'Current': 'pseudo'}) # type: ignore[attr-defined]
+ assert bridge.superuser_rule.peer is not None
+
+ # try to open dbus on the root bridge
+ root_dbus = await transport.check_open('dbus-json3', bus='internal', superuser=True)
+
+ # verify that the bridge thinks that it's the root bridge
+ await transport.assert_bus_props('/superuser', 'cockpit.Superuser',
+ {'Bridges': [], 'Current': 'root'}, bus=root_dbus)
+
+ # close up
+ await transport.check_close(channel=root_dbus)
+
+
+@pytest.mark.asyncio
+async def test_superuser_dbus(bridge: Bridge, transport: MockTransport) -> None:
+ add_pseudo(bridge)
+ await verify_root_bridge_not_running(bridge, transport)
+
+ # start the superuser bridge -- no password, so it should work straight away
+ () = await transport.check_bus_call('/superuser', 'cockpit.Superuser', 'Start', ['pseudo'])
+
+ await verify_root_bridge_running(bridge, transport)
+
+ # open a channel on the root bridge
+ root_null = await transport.check_open('null', superuser=True)
+
+ # stop the bridge
+ stop = transport.send_bus_call(transport.internal_bus, '/superuser',
+ 'cockpit.Superuser', 'Stop', [])
+
+ # that should have implicitly closed the open channel
+ await transport.assert_msg('', command='close', channel=root_null)
+ assert root_null not in bridge.open_channels
+
+ # The Stop method call is done now
+ await transport.assert_msg(transport.internal_bus, reply=[[]], id=stop)
+
+
+def format_methods(methods: Dict[str, str]):
+ return {name: {'t': 'a{sv}', 'v': {'label': {'t': 's', 'v': label}}} for name, label in methods.items()}
+
+
+@pytest.mark.asyncio
+async def test_superuser_dbus_pw(bridge: Bridge, transport: MockTransport, monkeypatch) -> None:
+ monkeypatch.setenv('PSEUDO_PASSWORD', 'p4ssw0rd')
+ add_pseudo(bridge)
+ await verify_root_bridge_not_running(bridge, transport)
+
+ # watch for signals
+ await transport.add_bus_match('/superuser', 'cockpit.Superuser')
+ await transport.watch_bus('/superuser', 'cockpit.Superuser',
+ {
+ 'Bridges': bridge.more_superuser_bridges, # type: ignore[attr-defined]
+ 'Current': 'none',
+ 'Methods': format_methods({'pseudo': 'pseudo'}),
+ })
+
+ # start the bridge. with a password this is more complicated
+ start = transport.send_bus_call(transport.internal_bus, '/superuser',
+ 'cockpit.Superuser', 'Start', ['pseudo'])
+ # first, init state
+ await transport.assert_bus_notify('/superuser', 'cockpit.Superuser', {'Current': 'init'})
+ # then, we'll be asked for a password
+ await transport.assert_bus_signal('/superuser', 'cockpit.Superuser', 'Prompt',
+ ['', 'can haz pw?', '', False, ''])
+ # give it
+ await transport.check_bus_call('/superuser', 'cockpit.Superuser', 'Answer', ['p4ssw0rd'])
+ # and now the bridge should be running
+ await transport.assert_bus_notify('/superuser', 'cockpit.Superuser', {'Current': 'pseudo'})
+
+ # Start call is now done
+ await transport.assert_bus_reply(start, [])
+
+ # double-check
+ await verify_root_bridge_running(bridge, transport)
+
+
+@pytest.mark.asyncio
+async def test_superuser_dbus_wrong_pw(bridge: Bridge, transport: MockTransport, monkeypatch) -> None:
+ monkeypatch.setenv('PSEUDO_PASSWORD', 'p4ssw0rd')
+ add_pseudo(bridge)
+ await verify_root_bridge_not_running(bridge, transport)
+
+ # watch for signals
+ await transport.add_bus_match('/superuser', 'cockpit.Superuser')
+ await transport.watch_bus('/superuser', 'cockpit.Superuser',
+ {
+ 'Bridges': bridge.more_superuser_bridges, # type: ignore[attr-defined]
+ 'Current': 'none',
+ 'Methods': format_methods({'pseudo': 'pseudo'}),
+ })
+
+ # start the bridge. with a password this is more complicated
+ start = transport.send_bus_call(transport.internal_bus, '/superuser',
+ 'cockpit.Superuser', 'Start', ['pseudo'])
+ # first, init state
+ await transport.assert_bus_notify('/superuser', 'cockpit.Superuser', {'Current': 'init'})
+ # then, we'll be asked for a password
+ await transport.assert_bus_signal('/superuser', 'cockpit.Superuser', 'Prompt',
+ ['', 'can haz pw?', '', False, ''])
+ # give it
+ await transport.check_bus_call('/superuser', 'cockpit.Superuser',
+ 'Answer', ['p5ssw0rd']) # wrong password
+ # pseudo fails after the first wrong attempt
+ await transport.assert_bus_notify('/superuser', 'cockpit.Superuser', {'Current': 'none'})
+
+ # Start call is now done and returned failure
+ await transport.assert_bus_error(start, 'cockpit.Superuser.Error', 'pseudo says: Bad password')
+
+ # double-check
+ await verify_root_bridge_not_running(bridge, transport)
+
+
+@pytest.mark.asyncio
+async def test_superuser_init(bridge: Bridge, no_init_transport: MockTransport) -> None:
+ add_pseudo(bridge)
+ no_init_transport.init(superuser={"id": "pseudo"})
+ transport = no_init_transport
+
+ # this should work right away without auth
+ await transport.assert_msg('', command='superuser-init-done')
+
+ await verify_root_bridge_running(bridge, transport)
+
+
+@pytest.mark.asyncio
+async def test_superuser_init_pw(bridge: Bridge, no_init_transport: MockTransport, monkeypatch) -> None:
+ monkeypatch.setenv('PSEUDO_PASSWORD', 'p4ssw0rd')
+ add_pseudo(bridge)
+ no_init_transport.init(superuser={"id": "pseudo"})
+ transport = no_init_transport
+
+ msg = await transport.assert_msg('', command='authorize')
+ transport.send_json('', command='authorize', cookie=msg['cookie'], response='p4ssw0rd')
+
+ # that should have worked
+ await transport.assert_msg('', command='superuser-init-done')
+
+ await verify_root_bridge_running(bridge, transport)
+
+
+@pytest.mark.asyncio
+async def test_no_login_messages(transport: MockTransport) -> None:
+ await transport.check_bus_call('/LoginMessages', 'cockpit.LoginMessages', 'Get', [], ["{}"])
+ await transport.check_bus_call('/LoginMessages', 'cockpit.LoginMessages', 'Dismiss', [], [])
+ await transport.check_bus_call('/LoginMessages', 'cockpit.LoginMessages', 'Get', [], ["{}"])
+
+
+@pytest.fixture
+def login_messages_envvar(monkeypatch):
+ if sys.version_info < (3, 8):
+ pytest.skip("os.memfd_create new in 3.8")
+ fd = os.memfd_create('login messages')
+ os.write(fd, b"msg")
+ # this is questionable (since it relies on ordering of fixtures), but it works
+ monkeypatch.setenv('COCKPIT_LOGIN_MESSAGES_MEMFD', str(fd))
+ return None
+
+
+@pytest.mark.asyncio
+async def test_login_messages(login_messages_envvar, transport: MockTransport) -> None:
+ del login_messages_envvar
+
+ await transport.check_bus_call('/LoginMessages', 'cockpit.LoginMessages', 'Get', [], ["msg"])
+ # repeated read should get the messages again
+ await transport.check_bus_call('/LoginMessages', 'cockpit.LoginMessages', 'Get', [], ["msg"])
+ # ...but not after they were dismissed
+ await transport.check_bus_call('/LoginMessages', 'cockpit.LoginMessages', 'Dismiss', [], [])
+ await transport.check_bus_call('/LoginMessages', 'cockpit.LoginMessages', 'Get', [], ["{}"])
+ # idempotency
+ await transport.check_bus_call('/LoginMessages', 'cockpit.LoginMessages', 'Dismiss', [], [])
+ await transport.check_bus_call('/LoginMessages', 'cockpit.LoginMessages', 'Get', [], ["{}"])
+
+
+@pytest.mark.asyncio
+async def test_freeze(bridge: Bridge, transport: MockTransport) -> None:
+ koelle = await transport.check_open('echo')
+ malle = await transport.check_open('echo')
+
+ # send a bunch of data to frozen koelle
+ bridge.open_channels[koelle].freeze_endpoint()
+ transport.send_data(koelle, b'x1')
+ transport.send_data(koelle, b'x2')
+ transport.send_data(koelle, b'x3')
+ transport.send_done(koelle)
+
+ # malle never freezes
+ transport.send_data(malle, b'yy')
+ transport.send_done(malle)
+
+ # unfreeze koelle
+ bridge.open_channels[koelle].thaw_endpoint()
+
+ # malle should have sent its messages first
+ await transport.assert_data(malle, b'yy')
+ await transport.assert_msg('', command='done', channel=malle)
+ await transport.assert_msg('', command='close', channel=malle)
+
+ # the data from koelle should still be in the right order, though
+ await transport.assert_data(koelle, b'x1')
+ await transport.assert_data(koelle, b'x2')
+ await transport.assert_data(koelle, b'x3')
+ await transport.assert_msg('', command='done', channel=koelle)
+ await transport.assert_msg('', command='close', channel=koelle)
+
+
+@pytest.mark.asyncio
+async def test_internal_metrics(transport: MockTransport) -> None:
+ metrics = [
+ {"name": "cpu.core.user", "derive": "rate"},
+ {"name": "memory.used"},
+ ]
+ interval = 100
+ source = 'internal'
+
+ await transport.check_open('metrics1', source=source, interval=interval, metrics=metrics)
+ _, data = await transport.next_frame()
+ # first message is always the meta message
+ meta = json.loads(data)
+ assert isinstance(meta['timestamp'], float)
+ assert meta['interval'] == interval
+ assert meta['source'] == source
+ assert isinstance(meta['metrics'], list)
+ instances = len(next(m['instances'] for m in meta['metrics'] if m['name'] == 'cpu.core.user'))
+
+ # actual data
+ _, data = await transport.next_frame()
+ data = json.loads(data)
+ assert isinstance(data, list)
+ # cpu.core.user instances should be the same as meta sent instances
+ assert instances == len(data[0][0])
+ # all instances should be False, as this is a rate
+ assert not all(d for d in data[0][0])
+ # memory.used should be an integer
+ assert isinstance(data[0][1], int)
+
+
+@pytest.mark.asyncio
+async def test_fsread1_errors(transport: MockTransport) -> None:
+ await transport.check_open('fsread1', path='/etc/shadow', problem='access-denied')
+ await transport.check_open('fsread1', path='/', problem='internal-error',
+ reply_keys={'message': "[Errno 21] Is a directory: '/'"})
+ await transport.check_open('fsread1', path='/etc/passwd', max_read_size="lol",
+ problem='protocol-error',
+ reply_keys={'message': "attribute 'max_read_size': must have type int"})
+
+
+@pytest.mark.asyncio
+async def test_fsread1_size_hint(transport: MockTransport) -> None:
+ data = None
+ stat = os.stat('/usr/lib/os-release')
+ with open('/usr/lib/os-release', 'rb') as fp:
+ data = fp.read()
+ ch = await transport.check_open('fsread1', path='/usr/lib/os-release', binary='raw',
+ reply_keys={'size-hint': stat.st_size})
+ await transport.assert_data(ch, data)
+
+
+@pytest.mark.asyncio
+async def test_fsread1_size_hint_absent(transport: MockTransport) -> None:
+ # non-binary fsread1 has no size-hint
+ await transport.check_open('fsread1', path='/etc/passwd', absent_keys=['size-hint'])
+
+
+@pytest.mark.asyncio
+async def test_fsread1_size_hint_absent_char_device(transport: MockTransport) -> None:
+ # character device fsread1 has no size-hint
+ await transport.check_open('fsread1', path='/dev/null', binary='raw', absent_keys=['size-hint'])
+
+
+@pytest.mark.asyncio
+async def test_fslist1_no_watch(transport: MockTransport, tmp_path: Path) -> None:
+ # empty
+ ch = await transport.check_open('fslist1', path=str(tmp_path), watch=False)
+ await transport.assert_msg('', command='done', channel=ch)
+ await transport.check_close(channel=ch)
+
+ # create a file and a directory in some_dir
+ (tmp_path / 'somefile').touch()
+ (tmp_path / 'somedir').mkdir()
+
+ ch = await transport.check_open('fslist1', path=str(tmp_path), watch=False)
+ # don't assume any ordering
+ msg1 = await transport.next_msg(ch)
+ msg2 = await transport.next_msg(ch)
+ if msg1['type'] == 'file':
+ msg1, msg2 = msg2, msg1
+ assert msg1 == {'event': 'present', 'path': 'somedir', 'type': 'directory'}
+ assert msg2 == {'event': 'present', 'path': 'somefile', 'type': 'file'}
+
+ await transport.assert_msg('', command='done', channel=ch)
+ await transport.check_close(channel=ch)
+
+
+@pytest.mark.asyncio
+async def test_fslist1_notexist(transport: MockTransport) -> None:
+ await transport.check_open(
+ 'fslist1', path='/nonexisting', watch=False,
+ problem='not-found',
+ reply_keys={'message': "[Errno 2] No such file or directory: '/nonexisting'"})
+
+
+@pytest.mark.asyncio
+async def test_fsreplace1(transport: MockTransport, tmp_path: Path) -> None:
+ # create non-existing file
+ myfile = tmp_path / 'newfile'
+ ch = await transport.check_open('fsreplace1', path=str(myfile))
+ transport.send_data(ch, b'some stuff')
+ transport.send_done(ch)
+ await transport.assert_msg('', command='done', channel=ch)
+ await transport.check_close(channel=ch)
+ assert myfile.read_bytes() == b'some stuff'
+ # no leftover files
+ assert os.listdir(tmp_path) == ['newfile']
+
+ # now update its contents
+ ch = await transport.check_open('fsreplace1', path=str(myfile))
+ transport.send_data(ch, b'new new new!')
+ transport.send_done(ch)
+ await transport.assert_msg('', command='done', channel=ch)
+ await transport.check_close(channel=ch)
+ assert myfile.read_bytes() == b'new new new!'
+ # no leftover files
+ assert os.listdir(tmp_path) == ['newfile']
+
+ # write empty file
+ ch = await transport.check_open('fsreplace1', path=str(myfile))
+ transport.send_data(ch, b'')
+ transport.send_done(ch)
+ await transport.assert_msg('', command='done', channel=ch)
+ await transport.check_close(channel=ch)
+ assert myfile.read_bytes() == b''
+
+ # delete file
+ ch = await transport.check_open('fsreplace1', path=str(myfile))
+ transport.send_done(ch)
+ await transport.assert_msg('', command='done', channel=ch)
+ await transport.check_close(channel=ch)
+ assert not myfile.exists()
+
+
+@pytest.mark.asyncio
+async def test_fsreplace1_error(transport: MockTransport, tmp_path: Path) -> None:
+ # trying to write a directory
+ ch = await transport.check_open('fsreplace1', path=str(tmp_path))
+ transport.send_data(ch, b'not me')
+ transport.send_done(ch)
+ await transport.assert_msg('', command='close', channel=ch, problem='access-denied')
+
+ # nonexisting directory
+ ch = await transport.check_open('fsreplace1', path='/non/existing/file')
+ transport.send_data(ch, b'not me')
+ transport.send_done(ch)
+ await transport.assert_msg('', command='close', channel=ch, problem='not-found')
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('channeltype', CHANNEL_TYPES)
+async def test_channel(bridge: Bridge, transport: MockTransport, channeltype, tmp_path: Path) -> None:
+ payload = channeltype.payload
+ args = dict(channeltype.restrictions)
+
+ async def serve_page(reader, writer):
+ while True:
+ line = await reader.readline()
+ if line.strip():
+ print('HTTP Request line', line)
+ else:
+ break
+
+ print('Sending HTTP reply')
+ writer.write(b'HTTP/1.1 200 OK\r\n\r\nA document\r\n')
+ await writer.drain()
+ writer.close()
+
+ srv = str(tmp_path / 'sock')
+ await asyncio.start_unix_server(serve_page, srv)
+
+ if payload == 'fsinfo':
+ args = {'path': '/', 'attrs': []}
+ elif payload == 'fslist1':
+ args = {'path': '/', 'watch': False}
+ elif payload == 'fsread1':
+ args = {'path': '/etc/passwd'}
+ elif payload == 'fsreplace1':
+ args = {'path': 'tmpfile'}
+ elif payload == 'fswatch1':
+ args = {'path': '/etc'}
+ elif payload == 'http-stream1':
+ args = {'internal': 'packages', 'method': 'GET', 'path': '/manifests.js',
+ 'headers': {'X-Forwarded-Proto': 'http', 'X-Forwarded-Host': 'localhost'}}
+ elif payload == 'http-stream2':
+ args = {'method': 'GET', 'path': '/bzzt', 'unix': srv}
+ elif payload == 'stream':
+ if 'spawn' in args:
+ args = {'spawn': ['cat']}
+ else:
+ args = {'unix': srv}
+ elif payload == 'metrics1':
+ args['metrics'] = [{'name': 'memory.free'}]
+ elif payload == 'dbus-json3':
+ if not os.path.exists('/run/dbus/system_bus_socket'):
+ pytest.skip('no dbus')
+ else:
+ args = {}
+
+ print('sending open', payload, args)
+ ch = transport.send_open(payload, **args)
+ saw_data = False
+
+ while True:
+ channel, msg = await transport.next_frame()
+ print(channel, msg)
+ if channel == '':
+ control = json.loads(msg)
+ assert control['channel'] == ch
+ command = control['command']
+ if command == 'ready':
+ # If we get ready, it's our turn to send data first.
+ # Hopefully we didn't receive any before.
+ assert not saw_data
+ break
+ else:
+ pytest.fail(f'unexpected event: {(payload, args, control)}')
+ else:
+ saw_data = True
+
+ # If we're here, it's our turn to talk. Say nothing.
+ print('sending done')
+ transport.send_done(ch)
+
+ if payload in ['dbus-json3', 'fswatch1', 'null']:
+ transport.send_close(ch)
+
+ while True:
+ channel, msg = await transport.next_frame()
+ print(channel, msg)
+ if channel == '':
+ control = json.loads(msg)
+ command = control['command']
+ if command == 'done':
+ continue
+ elif command == 'close':
+ assert 'problem' not in control
+ return
+
+
+@pytest.mark.parametrize(('os_release', 'expected'), [
+ # simple values, with comments and ignored space
+ (
+ '\n\n# simple\nID=mylinux\nVERSION=1.2\n\n# comment\nFOO=bar\n\n',
+ {'ID': 'mylinux', 'VERSION': '1.2', 'FOO': 'bar'}
+ ),
+ # quoted values
+ (
+ '''SINGLE='foo:bar '\nDOUBLE=" bar//foo"\n''',
+ {'SINGLE': 'foo:bar ', 'DOUBLE': ' bar//foo'}
+ ),
+ # ignore ungrammatical lines
+ (
+ 'A=a\nNOVALUE\nDOUBLEEQ=a=b\nB=b',
+ {'A': 'a', 'B': 'b'}
+ ),
+ # invalid values; anything outside [A-Za-z0-9] must be quoted; but our parser is more liberal
+ (
+ 'X=a:b\nY=a b\nZ=a-b\nV=a_b',
+ {'X': 'a:b', 'Z': 'a-b', 'V': 'a_b'}
+ ),
+])
+def test_get_os_release(os_release: str, expected: str) -> None:
+ with unittest.mock.patch('builtins.open', unittest.mock.mock_open(read_data=os_release)):
+ assert Bridge.get_os_release() == expected
+
+
+@pytest.mark.asyncio
+async def test_flow_control(transport: MockTransport, tmp_path: Path) -> None:
+ bigun = tmp_path / 'bigun'
+ total_bytes = 8 * 1024 * 1024
+ recvd_bytes = 0
+ bigun.write_bytes(b'0' * total_bytes)
+ fsread1 = await transport.check_open('fsread1', path=str(bigun), flow_control=True)
+
+ # We should receive a number of blocks of initial data, each with a ping.
+ # We save the pings to reply later.
+ pings: 'deque[JsonObject]' = deque()
+
+ async def recv_one():
+ nonlocal recvd_bytes
+
+ channel, data = await transport.next_frame()
+ assert channel == fsread1
+ assert data == b'0' * Channel.BLOCK_SIZE
+ recvd_bytes += len(data)
+
+ ping = await transport.next_msg('')
+ assert ping['command'] == 'ping'
+ assert ping['channel'] == fsread1
+ assert ping['sequence'] == recvd_bytes
+ pings.append(ping)
+
+ while recvd_bytes < Channel.SEND_WINDOW:
+ await recv_one()
+
+ # We should stall out here. Make sure nothing else arrives.
+ await transport.assert_empty()
+
+ # Start sending pongs and make sure we receive a new block of data for each
+ # one (as the window extends)
+ while recvd_bytes < total_bytes:
+ ping = pings.popleft()
+ transport.send_json('', **dict(ping, command='pong'))
+ await recv_one()
+
+ transport.send_close(fsread1)
+
+
+@pytest.mark.asyncio
+async def test_large_upload(event_loop: asyncio.AbstractEventLoop, transport: MockTransport, tmp_path: Path) -> None:
+ fifo = str(tmp_path / 'pipe')
+ os.mkfifo(fifo)
+
+ sender = await transport.check_open('stream', spawn=['dd', f'of={fifo}'])
+ # cockpit.js doesn't do flow control, so neither do we...
+ chunk = b'0' * Channel.BLOCK_SIZE
+ loops = 100
+ for _ in range(loops):
+ transport.send_data(sender, chunk)
+ transport.send_done(sender)
+
+ # we should be in a state now where we have a bunch of bytes queued up in
+ # the bridge but they can't be delivered because nobody is reading from the
+ # pipe... make sure dd is still running and we didn't get any messages.
+ await transport.assert_empty()
+
+ # start draining now, and make sure we get everything we sent.
+ with open(fifo, 'rb') as receiver:
+ received = await event_loop.run_in_executor(None, receiver.read)
+ assert len(received) == loops * Channel.BLOCK_SIZE
+
+ # and now our done and close messages should come
+ await transport.assert_msg('', command='done', channel=sender)
+ await transport.assert_msg('', command='close', channel=sender)
+
+
+class FsInfoClient:
+ def __init__(self, transport: MockTransport, channel: str):
+ self.transport = transport
+ self.channel = channel
+ self.state: JsonObject = {}
+
+ @classmethod
+ async def open(
+ cls,
+ transport: MockTransport,
+ path: 'str | Path',
+ attrs: Sequence[str] = ('type', 'target'),
+ fnmatch: str = '*.txt',
+ **kwargs: JsonValue
+ ) -> 'FsInfoClient':
+ channel = await transport.check_open('fsinfo', path=str(path), attrs=attrs,
+ fnmatch=fnmatch, reply_keys=None,
+ absent_keys=(), **kwargs)
+ return cls(transport, channel)
+
+ async def next_state(self) -> JsonObject:
+ while True:
+ patch = await self.transport.next_msg(self.channel)
+ self.state = json_merge_patch(self.state, patch)
+ if not get_bool(self.state, 'partial', None):
+ break
+ return self.state
+
+ async def wait(self) -> JsonObject:
+ state = await self.next_state()
+ await self.transport.assert_msg('', command='done', channel=self.channel)
+ await self.transport.assert_msg('', command='close', channel=self.channel)
+ return state
+
+ async def close(self):
+ await self.transport.check_close(self.channel)
+
+
+def fsinfo_err(err: int) -> JsonObject:
+ problems = {
+ errno.ENOENT: 'not-found',
+ errno.EPERM: 'access-denied',
+ errno.EACCES: 'access-denied',
+ errno.ENOTDIR: 'not-directory'
+ }
+ return {
+ 'error': {
+ 'problem': problems.get(err, 'internal-error'),
+ 'message': os.strerror(err),
+ 'errno': errno.errorcode[err],
+ }
+ }
+
+
+@pytest.fixture
+def fsinfo_test_cases(tmp_path: Path) -> 'dict[Path, JsonObject]':
+ # a normal directory
+ normal_dir = tmp_path / 'dir'
+ normal_dir.mkdir()
+ (normal_dir / 'dir-file.txt').write_text('dir file')
+ (normal_dir / 'dir-file.xtx').write_text('do not read this')
+
+ # a directory without +x (search)
+ no_x_dir = tmp_path / 'no-x-dir'
+ no_x_dir.mkdir()
+ (no_x_dir / 'no-x-dir-file.txt').write_text('file')
+ no_x_dir.chmod(0o644)
+
+ # a directory without +r (read)
+ no_r_dir = tmp_path / 'no-r-dir'
+ no_r_dir.mkdir()
+ (no_r_dir / 'no-r-dir-file.txt').write_text('file')
+ no_r_dir.chmod(0o311)
+
+ # a normal file
+ file = tmp_path / 'file'
+ file.write_text('normal file')
+
+ # a non-readable file
+ no_r_file = tmp_path / 'no-r-file'
+ no_r_file.write_text('inaccessible file')
+ no_r_file.chmod(0)
+
+ # a device
+ dev = tmp_path / 'dev'
+ dev.symlink_to('/dev/null')
+
+ # a dangling symlink
+ dangling = tmp_path / 'dangling'
+ dangling.symlink_to('does-not-exist')
+
+ # a symlink pointing to itself
+ loopy = tmp_path / 'loopy'
+ loopy.symlink_to(loopy)
+
+ expected_state: 'dict[Path, JsonObject]' = {
+ normal_dir: {"info": {"type": "dir", "entries": {'dir-file.txt': {"type": "reg"}}}},
+
+ # can't stat() the file
+ no_x_dir: {"info": {"type": "dir", "entries": {"no-x-dir-file.txt": {}}}},
+
+ # can't read the directory, so no entries
+ no_r_dir: {"info": {"type": "dir"}},
+
+ # normal file, can read its contents
+ file: {"info": {"type": "reg"}},
+
+ # can't read file, so no contents
+ no_r_file: {"info": {"type": "reg"}},
+
+ # a device
+ dev: {"info": {"type": "chr"}},
+
+ # a dangling symlink
+ dangling: fsinfo_err(errno.ENOENT),
+
+ # a link pointing at itself
+ loopy: fsinfo_err(errno.ELOOP),
+ }
+
+ if os.getuid() == 0:
+ # we can't do the permissions-dependent tests as root
+ del expected_state[no_x_dir]
+ del expected_state[no_r_dir]
+ del expected_state[no_r_file]
+
+ return expected_state
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_nopath(transport: MockTransport) -> None:
+ await transport.check_open('fsinfo', attrs=['type'], problem='protocol-error')
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_noattrs(transport: MockTransport) -> None:
+ await transport.check_open('fsinfo', path='/', problem='protocol-error')
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_relative(transport: MockTransport) -> None:
+ await transport.check_open('fsinfo', path='rel', problem='protocol-error')
+ await transport.check_open('fsinfo', path='.', problem='protocol-error')
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_nofollow_watch(transport: MockTransport) -> None:
+ await transport.check_open('fsinfo', path='/', attrs=[], watch=True, follow=False, problem='protocol-error')
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_nofollow_targets(transport: MockTransport) -> None:
+ await transport.check_open('fsinfo', path='/', attrs=['targets'], follow=False, problem='protocol-error')
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_empty_update(transport: MockTransport, tmp_path: Path) -> None:
+ # test an empty update — make sure nothing lands on the wire
+ ch = await transport.check_open('fsinfo', path=str(tmp_path), attrs=['type'], watch=True)
+ assert await transport.next_msg(ch) == {'info': {"type": "dir"}}
+ tmp_path.touch()
+ await asyncio.sleep(0.1) # fsinfo waits 0.1 before dispatching updates
+ await transport.assert_empty() # this waits another 0.1
+ await transport.check_close(ch)
+
+
+@pytest.mark.asyncio
+async def test_fsinfo(transport: MockTransport, fsinfo_test_cases: 'dict[Path, JsonObject]') -> None:
+ for path, expected_state in fsinfo_test_cases.items():
+ client = await FsInfoClient.open(transport, path)
+ assert await client.wait() == expected_state
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_nofollow(transport: MockTransport, fsinfo_test_cases: 'dict[Path, JsonObject]') -> None:
+ for path, expected_state in fsinfo_test_cases.items():
+ if path.name == 'loopy':
+ # with nofollow, this won't fail — we'll see the link itself
+ expected_state = {"info": {"type": "lnk", "target": str(path)}}
+ elif path.name == 'dev':
+ expected_state = {"info": {"type": "lnk", "target": "/dev/null"}}
+ elif path.name == 'dangling':
+ expected_state = {"info": {"type": "lnk", "target": "does-not-exist"}}
+
+ client = await FsInfoClient.open(transport, path, follow=False)
+ assert await client.wait() == expected_state
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_onlydir(transport: MockTransport, fsinfo_test_cases: 'dict[Path, JsonObject]') -> None:
+ for path, expected_state in fsinfo_test_cases.items():
+ if 'dir' not in path.name and 'error' not in expected_state:
+ expected_state = fsinfo_err(errno.ENOTDIR)
+
+ # with '/' appended, this should only open dirs
+ client = await FsInfoClient.open(transport, str(path) + '/')
+ assert await client.wait() == expected_state
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_onlydir_watch(transport: MockTransport, fsinfo_test_cases: 'dict[Path, JsonObject]') -> None:
+ for path, expected_state in fsinfo_test_cases.items():
+ # note the order here: because our check for notdir is implemented
+ # inside of the bridge, we try inotify first and check for notdir
+ # second. if systemd_ctypes supports this some day, it may change.
+ if path.name.startswith('no-r'):
+ # we need this one because we can't inotify files without +r
+ expected_state = fsinfo_err(errno.EACCES)
+ elif 'dir' not in path.name and 'error' not in expected_state:
+ # and this one to deal with not-dir
+ expected_state = fsinfo_err(errno.ENOTDIR)
+
+ # with '/' appended, this should only open dirs
+ client = await FsInfoClient.open(transport, str(path) + '/', watch=True)
+ assert await client.next_state() == expected_state
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_watch_identity_changes(
+ transport: MockTransport, tmp_path: Path, fsinfo_test_cases: 'dict[Path, JsonObject]'
+) -> None:
+ # we will now point a symlink to the various possibilities to make sure the
+ # transitions are correctly handled
+ link = tmp_path / 'link'
+ client = await FsInfoClient.open(transport, link, watch=True)
+
+ # we didn't make a link yet, so...
+ assert (await client.next_state()) == fsinfo_err(errno.ENOENT)
+
+ # in watch mode we report errors a bit more sensitively: we also report
+ # them if we can't inotify the inode in question, which requires +r
+ # permission. as such, we need to modify our 'no-r' tests:
+ for path in list(fsinfo_test_cases):
+ if path.name.startswith('no-r'):
+ fsinfo_test_cases[path] = fsinfo_err(errno.EACCES)
+ if path.name == 'dangling':
+ # this breaks our logic below because we transition from ENOENT to ENOENT
+ del fsinfo_test_cases[path]
+
+ possibilities = tuple(fsinfo_test_cases)
+ for from_file in possibilities:
+ for to_file in possibilities:
+ if from_file is to_file:
+ continue
+
+ # link to the original file and check state
+ link.symlink_to(from_file)
+ assert await client.next_state() == fsinfo_test_cases[from_file]
+
+ # Try to generate some events immediately before we switch the link
+ # to ensure the event is suppressed. Any one of these could fail
+ # due to not being a directory or not having permissions.
+ with contextlib.suppress(OSError):
+ from_file.touch()
+ with contextlib.suppress(OSError):
+ (from_file / 'a.txt').touch()
+ with contextlib.suppress(OSError):
+ (from_file / 'a.txt').unlink()
+
+ # atomic replace, check state
+ (tmp_path / 'tmp').symlink_to(to_file)
+ (tmp_path / 'tmp').rename(link)
+ assert await client.next_state() == fsinfo_test_cases[to_file]
+
+ if to_file.name == 'dir':
+ # copied from fsinfo_test_cases fixture
+ dir_entries = {'dir-file.txt': {"type": "reg"}}
+ expected_state = {"info": {"type": "dir", "entries": dir_entries}}
+
+ (to_file / 'a.txt').write_text('a file')
+ dir_entries['a.txt'] = {"type": "reg"}
+ (to_file / 'b.txt').write_text('b file')
+ dir_entries['b.txt'] = {"type": "reg"}
+ (to_file / 'a.xtx').write_text('b file')
+ (to_file / 'b.xtx').write_text('b file')
+ assert await client.next_state() == expected_state
+
+ (to_file / 'a.xtx').unlink()
+ (to_file / 'b.xtx').unlink()
+ (to_file / 'dir.txt').mkdir()
+ dir_entries['dir.txt'] = {"type": "dir"}
+ (to_file / 'sym.txt').symlink_to('/x')
+ dir_entries['sym.txt'] = {"type": "lnk", "target": "/x"}
+ assert await client.next_state() == expected_state
+
+ (to_file / 'sym.txt').unlink()
+ del dir_entries['sym.txt']
+ assert await client.next_state() == expected_state
+
+ # we intentionally try not to receive these events — we want
+ # them to get lost when the identity changes, below
+ (to_file / 'dir.txt').rmdir()
+ (to_file / 'a.txt').unlink()
+ (to_file / 'b.txt').unlink()
+
+ # delete link to start over
+ link.unlink()
+ assert (await client.next_state()) == fsinfo_err(errno.ENOENT)
+
+ # let the pending event handler occasionally run to find nothing to process
+ await asyncio.sleep(0.15)
+
+ await client.close()
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_self_owner(transport: MockTransport, tmp_path: Path) -> None:
+ client = await FsInfoClient.open(transport, tmp_path, ['user', 'uid', 'group', 'gid'])
+ state = await client.wait()
+ info = get_dict(state, 'info')
+
+ assert get_int(info, 'uid') == os.getuid()
+ assert get_int(info, 'gid') == os.getgid()
+ assert info.get('user') == getpass.getuser()
+ assert info.get('group') == grp.getgrgid(os.getgid()).gr_name # hopefully true...
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_other_owner(transport: MockTransport, tmp_path: Path) -> None:
+ tmpfile = tmp_path / 'x'
+ tmpfile.touch()
+
+ # try to get root to own this thing using a couple of tricks that may work
+ # inside or outside of toolbox or containers
+ quoted = shlex.quote(str(tmpfile))
+ subprocess.run(fr'''
+ podman unshare chown 888:888 {quoted} || SUDO_ASKPASS=true sudo -A chown 0:0 '{quoted}'
+ ''', shell=True, check=False)
+
+ # verify that we ended up with a uid/gid with no user
+ buf = tmpfile.stat()
+ try:
+ pwd.getpwuid(buf.st_uid)
+ pytest.skip('Failed to find unmapped uid')
+ except KeyError:
+ pass # good!
+ try:
+ grp.getgrgid(buf.st_gid)
+ pytest.skip('Failed to find unmapped gid')
+ except KeyError:
+ pass # good!
+
+ client = await FsInfoClient.open(transport, tmpfile, ['user', 'uid', 'group', 'gid'])
+ state = await client.wait()
+ info = get_dict(state, 'info')
+
+ assert get_int(info, 'uid') == buf.st_uid
+ assert get_int(info, 'gid') == buf.st_gid
+ assert info.get('user') == buf.st_uid # numeric fallback
+ assert info.get('group') == buf.st_gid # numeric fallback
+
+
+@pytest.mark.asyncio
+async def test_fsinfo_targets(transport: MockTransport, tmp_path: Path) -> None:
+ # we are only interested in the things that start with 'l'
+ watch = await FsInfoClient.open(transport, tmp_path, ['type', 'target', 'targets'], fnmatch='l*', watch=True)
+
+ entries: JsonDict = {}
+ targets: JsonDict = {}
+ state = {"info": {"type": "dir", "entries": entries, "targets": targets}}
+ assert await watch.next_state() == state
+
+ # none of those will show up in entries (not 'l*')
+ (tmp_path / 'dir').mkdir()
+ (tmp_path / 'dir' / 'file').write_text('abc')
+ (tmp_path / 'dir' / 'lonely').write_text('abc')
+ (tmp_path / 'dir' / 'dir').mkdir()
+ (tmp_path / 'file').write_text('abc')
+ (tmp_path / 'Lonely').write_text('abc')
+
+ # this one will show up in entries because it matches 'l*'
+ (tmp_path / 'loved').write_text('abc')
+ entries['loved'] = {'type': 'reg'}
+
+ # a link that won't show up anywhere (no fnmatch)
+ (tmp_path / 'LonelyLink').symlink_to('dir/lonely')
+
+ # link to things that will land in targets because they're not in fnmatch
+ (tmp_path / 'lfile').symlink_to('file')
+ entries['lfile'] = {'type': 'lnk', 'target': 'file'}
+ targets['file'] = {'type': 'reg'}
+ (tmp_path / 'ldir').symlink_to('dir')
+ entries['ldir'] = {'type': 'lnk', 'target': 'dir'}
+ targets['dir'] = {'type': 'dir'}
+
+ # link to things that will land in targets because they're in another dir
+ (tmp_path / 'ldirfile').symlink_to('dir/file')
+ entries['ldirfile'] = {'type': 'lnk', 'target': 'dir/file'}
+ targets['dir/file'] = {'type': 'reg'}
+ (tmp_path / 'ldirdir').symlink_to('dir/dir')
+ entries['ldirdir'] = {'type': 'lnk', 'target': 'dir/dir'}
+ targets['dir/dir'] = {'type': 'dir'}
+ (tmp_path / 'lnull').symlink_to('/dev/null')
+ entries['lnull'] = {'type': 'lnk', 'target': '/dev/null'}
+ targets['/dev/null'] = {'type': 'chr'}
+ (tmp_path / 'lroot').symlink_to('/')
+ entries['lroot'] = {'type': 'lnk', 'target': '/'}
+ targets['/'] = {'type': 'dir'}
+
+ # link to things that won't land in targets because they're in entries
+ (tmp_path / 'llfile').symlink_to('lfile')
+ entries['llfile'] = {'type': 'lnk', 'target': 'lfile'}
+ (tmp_path / 'lldir').symlink_to('ldir')
+ entries['lldir'] = {'type': 'lnk', 'target': 'ldir'}
+ (tmp_path / 'lloved').symlink_to('loved')
+ entries['lloved'] = {'type': 'lnk', 'target': 'loved'}
+
+ # make sure the watch managed to pick that all up
+ assert await watch.next_state() == state
+
+ # double-check with the non-watch variant
+ client = await FsInfoClient.open(transport, tmp_path, ['type', 'target', 'targets'], fnmatch='l*')
+ assert await client.wait() == state
diff --git a/test/pytest/test_browser.py b/test/pytest/test_browser.py
new file mode 100644
index 0000000..d02daa5
--- /dev/null
+++ b/test/pytest/test_browser.py
@@ -0,0 +1,48 @@
+import glob
+import os
+import subprocess
+import sys
+from typing import Iterable
+
+import pytest
+
+SRCDIR = os.path.realpath(f'{__file__}/../../..')
+BUILDDIR = os.environ.get('abs_builddir', SRCDIR)
+
+SKIP = {
+ 'base1/test-dbus-address.html',
+}
+
+XFAIL = {
+ 'base1/test-websocket.html',
+}
+
+
+# Changed in version 3.10: Added the root_dir and dir_fd parameters.
+def glob_py310(fnmatch: str, *, root_dir: str, recursive: bool = False) -> Iterable[str]:
+ prefix = f'{root_dir}/'
+ prefixlen = len(prefix)
+
+ for result in glob.glob(f'{prefix}{fnmatch}', recursive=recursive):
+ assert result.startswith(prefix)
+ yield result[prefixlen:]
+
+
+@pytest.mark.parametrize('html', glob_py310('**/test-*.html', root_dir=f'{SRCDIR}/qunit', recursive=True))
+def test_browser(html):
+ if not os.path.exists(f'{BUILDDIR}/test-server'):
+ pytest.skip('no test-server')
+ if html in SKIP:
+ pytest.skip()
+ elif html in XFAIL:
+ pytest.xfail()
+
+ if 'COVERAGE_RCFILE' in os.environ:
+ coverage = ['coverage', 'run', '--parallel-mode', '--module']
+ else:
+ coverage = []
+
+ # Merge 2>&1 so that pytest displays an interleaved log
+ subprocess.run(['test/common/tap-cdp', f'{BUILDDIR}/test-server',
+ sys.executable, '-m', *coverage, 'cockpit.bridge', '--debug',
+ f'./qunit/{html}'], check=True, stderr=subprocess.STDOUT)
diff --git a/test/pytest/test_packages.py b/test/pytest/test_packages.py
new file mode 100644
index 0000000..59cbadb
--- /dev/null
+++ b/test/pytest/test_packages.py
@@ -0,0 +1,286 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2023 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+
+import pytest
+
+from cockpit.packages import Packages, parse_accept_language
+
+
+@pytest.mark.parametrize(("test_input", "expected"), [
+ # correct handles empty values
+ ('', ()),
+ (' ', ()),
+ (' , ', ()),
+ (' , ,xx', ('xx',)),
+ # english → empty list
+ ('en', ()),
+ (' , en', ()),
+ # invalid q values get ignored
+ ('aa;q===,bb;q=abc,cc;q=.,zz', ('zz',)),
+ # variant-peeling works
+ ('aa-bb-cc-dd,ee-ff-gg-hh', ('aa-bb-cc-dd', 'aa-bb-cc', 'aa-bb', 'aa', 'ee-ff-gg-hh', 'ee-ff-gg', 'ee-ff', 'ee')),
+ # sorting and english-truncation are working
+ ('fr-ch;q=0.8,es-mx;q=1.0,en-ca;q=0.9', ('es-mx', 'es', 'en-ca')),
+ ('de-at, zh-CN, en,', ('de-at', 'de', 'zh-cn', 'zh')),
+ ('es-es, nl;q=0.8, fr;q=0.9', ('es-es', 'es', 'fr', 'nl')),
+ ('fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5', ('fr-ch', 'fr'))
+])
+def test_parse_accept_language(test_input: str, expected: 'tuple[str]') -> None:
+ assert parse_accept_language(test_input) == expected
+
+
+@pytest.fixture
+def pkgdir(tmp_path, monkeypatch):
+ monkeypatch.setenv('XDG_DATA_DIRS', str(tmp_path))
+ monkeypatch.setenv('XDG_DATA_HOME', '/nonexisting')
+
+ self = tmp_path / 'cockpit'
+ self.mkdir()
+ make_package(self, 'basic', description="standard package", requires={"cockpit": "42"})
+ return self
+
+
+@pytest.fixture
+def confdir(tmp_path, monkeypatch):
+ monkeypatch.setenv('XDG_CONFIG_DIRS', str(tmp_path))
+ return tmp_path / 'cockpit'
+
+
+def make_package(pkgdir, dirname: str, **kwargs: object) -> None:
+ (pkgdir / dirname).mkdir()
+ with (pkgdir / dirname / 'manifest.json').open('w') as file:
+ json.dump(kwargs, file, indent=2)
+
+
+def test_basic(pkgdir):
+ packages = Packages()
+ assert len(packages.packages) == 1
+ assert packages.packages['basic'].name == 'basic'
+ assert packages.packages['basic'].manifest['description'] == 'standard package'
+ assert packages.packages['basic'].manifest['requires'] == {'cockpit': "42"}
+ assert packages.packages['basic'].priority == 1
+
+ assert packages.manifests == '{"basic": {"description": "standard package", "requires": {"cockpit": "42"}}}'
+
+
+def test_override_etc(pkgdir, confdir):
+ (confdir / 'basic.override.json').write_text('{"description": null, "priority": 5, "does-not-exist": null}')
+
+ packages = Packages()
+ assert len(packages.packages) == 1
+ # original attributes
+ assert packages.packages['basic'].name == 'basic'
+ assert packages.packages['basic'].manifest['requires'] == {'cockpit': '42'}
+ # overridden attributes
+ assert 'description' not in packages.packages['basic'].manifest
+ assert packages.packages['basic'].priority == 5
+
+ assert json.loads(packages.manifests) == {
+ 'basic': {
+ 'requires': {'cockpit': '42'},
+ 'priority': 5,
+ }
+ }
+
+
+def test_priority(pkgdir):
+ make_package(pkgdir, 'vip', name='basic', description='VIP', priority=100)
+ make_package(pkgdir, 'guest', description='Guest')
+
+ packages = Packages()
+ assert len(packages.packages) == 2
+ assert packages.packages['basic'].name == 'basic'
+ assert packages.packages['basic'].priority == 100
+ assert packages.packages['basic'].manifest['description'] == 'VIP'
+ assert packages.packages['guest'].name == 'guest'
+ assert packages.packages['guest'].priority == 1
+
+ parsed = json.loads(packages.manifests)
+ assert parsed['basic'] == {'name': 'basic', 'description': 'VIP', 'priority': 100}
+ assert parsed['guest'] == {'description': 'Guest'}
+
+
+def test_conditions(pkgdir):
+ make_package(pkgdir, 'empty', conditions=[])
+
+ # path-exists only
+ make_package(pkgdir, 'exists-1-yes', conditions=[{'path-exists': '/usr'}])
+ make_package(pkgdir, 'exists-1-no', conditions=[{'path-exists': '/nonexisting'}])
+ make_package(pkgdir, 'exists-2-yes', conditions=[{"path-exists": "/usr"},
+ {"path-exists": "/bin/sh"}])
+ make_package(pkgdir, 'exists-2-no', conditions=[{"path-exists": "/usr"},
+ {"path-exists": "/nonexisting"}])
+
+ # path-not-exists only
+ make_package(pkgdir, 'notexists-1-yes', conditions=[{"path-not-exists": "/nonexisting"}])
+ make_package(pkgdir, 'notexists-1-no', conditions=[{"path-not-exists": "/usr"}])
+ make_package(pkgdir, 'notexists-2-yes', conditions=[{"path-not-exists": "/nonexisting"},
+ {"path-not-exists": "/obscure"}])
+ make_package(pkgdir, 'notexists-2-no', conditions=[{"path-not-exists": "/nonexisting"},
+ {"path-not-exists": "/usr"}])
+
+ # mixed
+ make_package(pkgdir, 'mixed-yes', conditions=[{"path-exists": "/usr"},
+ {"path-not-exists": "/nonexisting"}])
+ make_package(pkgdir, 'mixed-no', conditions=[{"path-exists": "/nonexisting"},
+ {"path-not-exists": "/obscure"}])
+
+ packages = Packages()
+ assert set(packages.packages.keys()) == {
+ 'basic', 'empty', 'exists-1-yes', 'exists-2-yes', 'notexists-1-yes', 'notexists-2-yes', 'mixed-yes'
+ }
+
+
+def test_conditions_errors(pkgdir):
+ make_package(pkgdir, 'broken-syntax-1', conditions=[1])
+ make_package(pkgdir, 'broken-syntax-2', conditions=[["path-exists"]])
+ make_package(pkgdir, 'broken-syntax-3', conditions=[{"path-exists": "/foo", "path-not-exists": "/bar"}])
+
+ make_package(pkgdir, 'unknown-predicate-good', conditions=[{"path-exists": "/usr"},
+ {"frobnicated": True}])
+ make_package(pkgdir, 'unknown-predicate-bad', conditions=[{"path-exists": "/nonexisting"},
+ {"frobnicated": True}])
+
+ packages = Packages()
+ assert set(packages.packages.keys()) == {'basic', 'unknown-predicate-good'}
+
+
+def test_condition_hides_priority(pkgdir):
+ make_package(pkgdir, 'vip', name="basic", description="VIP", priority=100,
+ conditions=[{"path-exists": "/nonexisting"}])
+
+ packages = Packages()
+ assert packages.packages['basic'].name == 'basic'
+ assert packages.packages['basic'].manifest['description'] == 'standard package'
+ assert packages.packages['basic'].manifest['requires'] == {'cockpit': "42"}
+ assert packages.packages['basic'].priority == 1
+
+
+def test_english_translation(pkgdir):
+ make_package(pkgdir, 'one')
+ (pkgdir / 'one' / 'po.de.js').write_text('eins')
+
+ packages = Packages()
+
+ # make sure we get German
+ document = packages.load_path('/one/po.js', {'Accept-Language': 'de'})
+ assert '/javascript' in document.content_type
+ assert document.data.read() == b'eins'
+
+ # make sure we get German here (higher q-value) even with English first
+ document = packages.load_path('/one/po.js', {'Accept-Language': 'en;q=0.9, de-ch'})
+ assert '/javascript' in document.content_type
+ assert document.data.read() == b'eins'
+
+ # make sure we get the empty ("English") translation, and not German
+ document = packages.load_path('/one/po.js', {'Accept-Language': 'en, de'})
+ assert '/javascript' in document.content_type
+ assert document.data.read() == b''
+
+ document = packages.load_path('/one/po.js', {'Accept-Language': 'de;q=0.9, fr;q=0.7, en'})
+ assert '/javascript' in document.content_type
+ assert document.data.read() == b''
+
+ document = packages.load_path('/one/po.js', {'Accept-Language': 'de;q=0.9, fr, en-ca'})
+ assert '/javascript' in document.content_type
+ assert document.data.read() == b''
+
+ document = packages.load_path('/one/po.js', {'Accept-Language': ''})
+ assert '/javascript' in document.content_type
+ assert document.data.read() == b''
+
+ document = packages.load_path('/one/po.js', {})
+ assert '/javascript' in document.content_type
+ assert document.data.read() == b''
+
+
+def test_translation(pkgdir):
+ # old style: make sure po.de.js is served as fallback for manifest translations
+ make_package(pkgdir, 'one')
+ (pkgdir / 'one' / 'po.de.js').write_text('eins')
+
+ # new style: separated translations
+ make_package(pkgdir, 'two')
+ (pkgdir / 'two' / 'po.de.js').write_text('zwei')
+ (pkgdir / 'two' / 'po.manifest.de.js').write_text('zwo')
+
+ packages = Packages()
+
+ # make sure we can read a po.js file with language fallback
+ document = packages.load_path('/one/po.js', {'Accept-Language': 'es, de'})
+ assert '/javascript' in document.content_type
+ assert document.data.read() == b'eins'
+
+ # make sure we fall back cleanly to an empty file with correct mime
+ document = packages.load_path('/one/po.js', {'Accept-Language': 'es'})
+ assert '/javascript' in document.content_type
+ assert document.data.read() == b''
+
+ # make sure the manifest translations get sent along with manifests.js
+ document = packages.load_path('/manifests.js', {'Accept-Language': 'de'})
+ contents = document.data.read()
+ assert b'eins\n' in contents
+ assert b'zwo\n' in contents
+ assert b'zwei\n' not in contents
+
+
+def test_filename_mangling(pkgdir):
+ make_package(pkgdir, 'one')
+
+ # test various filename variations
+ (pkgdir / 'one' / 'one.js').write_text('this is one.js')
+ (pkgdir / 'one' / 'two.js.gz').write_text('this is two.js')
+ (pkgdir / 'one' / 'three.min.js.gz').write_text('this is three.js')
+ (pkgdir / 'one' / 'four.min.js').write_text('this is four.js')
+
+ packages = Packages()
+ encodings = set()
+
+ for name in ['one', 'two', 'three', 'four']:
+ document = packages.load_path(f'/one/{name}.js', {})
+ assert document.data.read().decode() == f'this is {name}.js'
+ assert '/javascript' in document.content_type
+ encodings.add(document.content_encoding)
+
+ assert encodings == {None, 'gzip'} # make sure we saw both compressed and uncompressed
+
+
+def test_overlapping_minified(pkgdir):
+ make_package(pkgdir, 'one')
+ (pkgdir / 'one' / 'one.min.js').write_text('min')
+ (pkgdir / 'one' / 'one.js').write_text('max')
+
+ # try the other way around in hope of listing the files in reverse order
+ (pkgdir / 'one' / 'two.js').write_text('max')
+ (pkgdir / 'one' / 'two.min.js').write_text('min')
+
+ packages = Packages()
+
+ # if both files are present, we should find the original one
+ document = packages.load_path('/one/one.js', {})
+ assert document.data.read().decode() == 'max'
+ document = packages.load_path('/one/two.js', {})
+ assert document.data.read().decode() == 'max'
+
+ # but requesting .min. explicitly will load it
+ document = packages.load_path('/one/one.min.js', {})
+ assert document.data.read().decode() == 'min'
+ document = packages.load_path('/one/two.min.js', {})
+ assert document.data.read().decode() == 'min'
+
diff --git a/test/pytest/test_peer.py b/test/pytest/test_peer.py
new file mode 100644
index 0000000..a4ae02b
--- /dev/null
+++ b/test/pytest/test_peer.py
@@ -0,0 +1,214 @@
+import asyncio
+import os
+import sys
+import time
+
+import pytest
+
+from cockpit.channel import ChannelError
+from cockpit.packages import BridgeConfig
+from cockpit.peer import ConfiguredPeer, PeerRoutingRule
+from cockpit.protocol import CockpitProtocolError
+from cockpit.router import Router
+from cockpit.transports import SubprocessTransport
+
+from . import mockpeer
+from .mocktransport import MockTransport
+
+PEER_CONFIG = BridgeConfig({
+ "spawn": [sys.executable, mockpeer.__file__],
+ "environ": ['PYTHONPATH=' + ':'.join(sys.path)],
+ "match": {"payload": "test"},
+})
+
+
+class Bridge(Router):
+ init_host = 'localhost'
+
+ def __init__(self):
+ rule = PeerRoutingRule(self, PEER_CONFIG)
+ super().__init__([rule])
+
+ def do_send_init(self):
+ pass
+
+
+@pytest.fixture
+def bridge():
+ return Bridge()
+
+
+@pytest.fixture
+def transport(bridge):
+ return MockTransport(bridge)
+
+
+@pytest.fixture
+def rule(bridge):
+ return bridge.routing_rules[0]
+
+
+@pytest.mark.asyncio
+async def test_shutdown(transport, rule):
+ await transport.check_open('test')
+ await transport.check_open('xest', problem='not-supported')
+
+ # Force the Peer closed
+ rule.peer.close()
+ await transport.assert_msg('', command='close', channel='channel.1', problem='terminated')
+
+ # But it should spawn again
+ await transport.check_open('test')
+ await transport.check_open('xest', problem='not-supported')
+ rule.peer.close()
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('init_type', ['wrong-command', 'channel-control', 'data', 'break-protocol'])
+async def test_init_failure(rule, init_type, monkeypatch, transport):
+ monkeypatch.setenv('INIT_TYPE', init_type)
+ await transport.check_open('test', problem='protocol-error')
+
+
+@pytest.mark.asyncio
+async def test_immediate_shutdown(rule):
+ peer = rule.apply_rule({'payload': 'test'})
+ assert peer is not None
+ peer.close()
+
+
+@pytest.mark.asyncio
+async def test_shutdown_before_init(monkeypatch, transport, rule):
+ monkeypatch.setenv('INIT_TYPE', 'silence')
+ channel = transport.send_open('test')
+ assert rule.peer is not None
+ assert rule.peer.transport is None
+ while rule.peer.transport is None:
+ await asyncio.sleep(0)
+ rule.peer.close()
+ await transport.assert_msg('', command='close', channel=channel, problem='terminated')
+
+
+@pytest.mark.asyncio
+async def test_exit_without_init(monkeypatch, transport):
+ monkeypatch.setenv('INIT_TYPE', 'exit')
+ await transport.check_open('test', problem='terminated')
+
+
+@pytest.mark.asyncio
+async def test_exit_not_found(monkeypatch, transport):
+ monkeypatch.setenv('INIT_TYPE', 'exit-not-found')
+ await transport.check_open('test', problem='no-cockpit')
+
+
+@pytest.mark.asyncio
+async def test_killed(monkeypatch, transport, rule):
+ channel = await transport.check_open('test')
+ os.kill(rule.peer.transport._process.pid, 9)
+ await transport.assert_msg('', command='close', channel=channel, problem='terminated')
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('init_type', ['wrong-command', 'channel-control', 'data', 'break-protocol'])
+async def test_await_failure(init_type, monkeypatch, bridge):
+ monkeypatch.setenv('INIT_TYPE', init_type)
+ peer = ConfiguredPeer(bridge, PEER_CONFIG)
+ with pytest.raises(CockpitProtocolError):
+ await peer.start()
+ peer.close()
+
+
+@pytest.mark.asyncio
+async def test_await_broken_connect(bridge):
+ class BrokenConnect(ConfiguredPeer):
+ async def do_connect_transport(self):
+ _ = 42 / 0
+
+ peer = BrokenConnect(bridge, PEER_CONFIG)
+ with pytest.raises(ZeroDivisionError):
+ await peer.start()
+ peer.close()
+
+
+@pytest.mark.asyncio
+async def test_await_broken_after_connect(bridge):
+ class BrokenConnect(ConfiguredPeer):
+ async def do_connect_transport(self):
+ await super().do_connect_transport()
+ _ = 42 / 0
+
+ peer = BrokenConnect(bridge, PEER_CONFIG)
+ with pytest.raises(ZeroDivisionError):
+ await peer.start()
+ peer.close()
+
+
+class CancellableConnect(ConfiguredPeer):
+ was_cancelled = False
+
+ async def do_connect_transport(self):
+ await super().do_connect_transport()
+ try:
+ # We should get cancelled here when the mockpeer sends "init"
+ await asyncio.sleep(10000)
+ except asyncio.CancelledError:
+ self.was_cancelled = True
+ raise
+
+
+@pytest.mark.asyncio
+async def test_await_cancellable_connect_init(bridge):
+ peer = CancellableConnect(bridge, PEER_CONFIG)
+ await peer.start()
+ peer.close()
+ while len(asyncio.all_tasks()) > 1:
+ await asyncio.sleep(0.1)
+ assert peer.was_cancelled
+
+
+@pytest.mark.asyncio
+async def test_await_cancellable_connect_close(monkeypatch, event_loop, bridge):
+ monkeypatch.setenv('INIT_TYPE', 'silence') # make sure we never get "init"
+ peer = CancellableConnect(bridge, PEER_CONFIG)
+ event_loop.call_later(0.1, peer.close) # call peer.close() after .start() is running
+ with pytest.raises(asyncio.CancelledError):
+ await peer.start()
+ # we already called .close()
+ while len(asyncio.all_tasks()) > 1:
+ await asyncio.sleep(0.1)
+ assert peer.was_cancelled
+
+
+@pytest.mark.asyncio
+async def test_spawn_broken_pipe(bridge):
+ class BrokenPipePeer(ConfiguredPeer):
+ def __init__(self, *, specific_error=False):
+ super().__init__(bridge, PEER_CONFIG)
+ self.specific_error = specific_error
+
+ async def do_connect_transport(self) -> None:
+ transport = await self.spawn(['sh', '-c', 'read a; exit 9'], ())
+ assert isinstance(transport, SubprocessTransport)
+ # Make the process exit by writing a newline (causing `read` to finish)
+ transport.write(b'\n')
+ # The process will exit soon — try writing to it until a write fails.
+ while not transport.is_closing():
+ transport.write(b'x')
+ time.sleep(0.1)
+ while transport.get_returncode() is None:
+ await asyncio.sleep(0.1)
+ if self.specific_error:
+ raise ChannelError('not-supported', message='kaputt')
+
+ # BrokenPipe bubbles up without an error returned by do_connect_transport
+ peer = BrokenPipePeer(specific_error=False)
+ with pytest.raises(BrokenPipeError):
+ await peer.start()
+ peer.close()
+
+ # BrokenPipe gets trumped by specific error returned by do_connect_transport
+ peer = BrokenPipePeer(specific_error=True)
+ with pytest.raises(ChannelError) as raises:
+ await peer.start()
+ assert raises.value.attrs == {'message': 'kaputt', 'problem': 'not-supported'}
+ peer.close()
diff --git a/test/pytest/test_samples.py b/test/pytest/test_samples.py
new file mode 100644
index 0000000..36451eb
--- /dev/null
+++ b/test/pytest/test_samples.py
@@ -0,0 +1,158 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import collections
+import multiprocessing
+import numbers
+import os
+
+import pytest
+
+import cockpit.samples
+
+
+@pytest.fixture
+def hwmon_mock(tmpdir_factory, monkeypatch):
+ hwmon_dir = tmpdir_factory.mktemp('hwmon')
+ monkeypatch.setattr(cockpit.samples, "HWMON_PATH", str(hwmon_dir))
+
+ # hwmon1 - no name
+ (hwmon_dir / 'hwmon1').mkdir()
+
+ # hwmon2 - no label (on ARM)
+ hwmon2_dir = hwmon_dir / 'hwmon2'
+ hwmon2_dir.mkdir()
+ with open(hwmon2_dir / 'name', 'w') as fp:
+ fp.write('cpu_thermal')
+ with open(hwmon2_dir / 'temp1_input', 'w') as fp:
+ fp.write('32000')
+
+ # hwmon3 - AMD workaround #18098
+ hwmon3_dir = hwmon_dir / 'hwmon3'
+ hwmon3_dir.mkdir()
+ with open(hwmon3_dir / 'name', 'w') as fp:
+ fp.write('k10temp')
+ with open(hwmon3_dir / 'temp1_input', 'w') as fp:
+ fp.write('27500')
+ with open(hwmon3_dir / 'temp1_label', 'w') as fp:
+ fp.write('Tctl')
+ with open(hwmon3_dir / 'temp3_input', 'w') as fp:
+ fp.write('37000')
+ with open(hwmon3_dir / 'temp3_label', 'w') as fp:
+ fp.write('Tccd1')
+
+ # hwmon4 - Intel coretemp
+ hwmon4_dir = hwmon_dir / 'hwmon4'
+ hwmon4_dir.mkdir()
+
+ with open(hwmon4_dir / 'name', 'w') as fp:
+ fp.write('coretemp')
+ with open(hwmon4_dir / 'temp1_input', 'w') as fp:
+ fp.write('47000')
+ with open(hwmon4_dir / 'temp1_label', 'w') as fp:
+ fp.write('Package id 0')
+ with open(hwmon4_dir / 'temp2_input', 'w') as fp:
+ fp.write('46000')
+ with open(hwmon4_dir / 'temp2_label', 'w') as fp:
+ fp.write('Core 0')
+ with open(hwmon4_dir / 'temp3_input', 'w') as fp:
+ fp.write('46000')
+ with open(hwmon4_dir / 'temp3_label', 'w') as fp:
+ fp.write('Core 1')
+ with open(hwmon4_dir / 'temp4_input', 'w') as fp:
+ fp.write('46000')
+ with open(hwmon4_dir / 'temp4_label', 'w') as fp:
+ fp.write('Core 2')
+
+ return hwmon_dir
+
+
+def get_checked_samples(sampler: cockpit.samples.Sampler) -> cockpit.samples.Samples:
+ cls = sampler.__class__
+
+ samples: cockpit.samples.Samples = collections.defaultdict(dict)
+ sampler.sample(samples)
+
+ assert set(samples) == {descr.name for descr in cls.descriptions}
+
+ for descr in cls.descriptions:
+ sample = samples[descr.name]
+
+ if descr.instanced:
+ assert isinstance(sample, dict)
+ else:
+ assert isinstance(sample, numbers.Real)
+
+ return samples
+
+
+def test_descriptions():
+ for cls in cockpit.samples.SAMPLERS:
+ # currently broken in containers with no cgroups or temperatures present
+ if cls in [cockpit.samples.CGroupSampler, cockpit.samples.CPUTemperatureSampler]:
+ continue
+
+ get_checked_samples(cls())
+
+
+def test_cgroup_descriptions():
+ if not os.path.exists('/sys/fs/cgroup/system.slice'):
+ pytest.xfail('No cgroups present')
+
+ get_checked_samples(cockpit.samples.CGroupSampler())
+
+
+def test_temperature_descriptions():
+ samples = collections.defaultdict(dict)
+ cockpit.samples.CPUTemperatureSampler().sample(samples)
+ if not samples:
+ pytest.xfail('No CPU temperature present')
+
+ get_checked_samples(cockpit.samples.CPUTemperatureSampler())
+
+
+def test_cpu():
+ samples = get_checked_samples(cockpit.samples.CPUSampler())
+ assert len(samples['cpu.core.user']) == multiprocessing.cpu_count()
+
+
+def test_cpu_temperature(hwmon_mock):
+ samples = collections.defaultdict(dict)
+ cockpit.samples.CPUTemperatureSampler().sample(samples)
+ samples = get_checked_samples(cockpit.samples.CPUTemperatureSampler())
+ for name, temperature in samples['cpu.temperature'].items():
+ # no name
+ assert 'hwmon1' not in name
+
+ assert 20 < temperature < 50
+
+ expected = ['hwmon4/temp4_input', 'hwmon4/temp3_input', 'hwmon4/temp2_input',
+ 'hwmon4/temp1_input', 'hwmon3/temp3_input', 'hwmon3/temp1_input',
+ 'hwmon2/temp1_input']
+ sensors = [os.path.relpath(p, start=hwmon_mock) for p in samples['cpu.temperature']]
+ assert sorted(sensors) == sorted(expected)
+
+
+def test_cgroup_disk_io():
+ samples = collections.defaultdict(dict)
+ cockpit.samples.CGroupDiskIO().sample(samples)
+ samples = get_checked_samples(cockpit.samples.CGroupDiskIO())
+
+ assert len(samples['disk.cgroup.read']) == len(samples['disk.cgroup.written'])
+ for cgroup in samples['disk.cgroup.read']:
+ assert samples['disk.cgroup.read'][cgroup] >= 0
+ assert samples['disk.cgroup.written'][cgroup] >= 0
diff --git a/test/pytest/test_transport.py b/test/pytest/test_transport.py
new file mode 100644
index 0000000..81fc4ff
--- /dev/null
+++ b/test/pytest/test_transport.py
@@ -0,0 +1,392 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import asyncio
+import contextlib
+import errno
+import os
+import signal
+import subprocess
+import unittest.mock
+from typing import Any, List, Optional, Tuple
+
+import pytest
+
+import cockpit.transports
+
+
+class Protocol(cockpit.transports.SubprocessProtocol):
+ transport: Optional[asyncio.Transport] = None
+ paused: bool = False
+ sent: int = 0
+ received: int = 0
+ exited: bool = False
+ close_on_eof: bool = True
+ eof: bool = False
+ exc: Optional[Exception] = None
+ output: Optional[List[bytes]] = None
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ assert isinstance(transport, asyncio.Transport)
+ self.transport = transport
+
+ def connection_lost(self, exc: Optional[Exception] = None) -> None:
+ self.transport = None
+ self.exc = exc
+
+ def data_received(self, data: bytes) -> None:
+ if self.output is not None:
+ self.output.append(data)
+ self.received += len(data)
+
+ def eof_received(self) -> bool:
+ self.eof = True
+ return not self.close_on_eof
+
+ def pause_writing(self) -> None:
+ self.paused = True
+
+ def write_until_backlogged(self) -> None:
+ while not self.paused:
+ self.write(b'a' * 4096)
+
+ def write(self, data: bytes) -> None:
+ assert self.transport is not None
+ self.transport.write(data)
+ self.sent += len(data)
+
+ def write_a_lot(self) -> None:
+ assert self.transport is not None
+ self.write_until_backlogged()
+ assert self.transport.get_write_buffer_size() != 0
+ for _ in range(20):
+ self.write(b'b' * 1024 * 1024)
+ assert self.transport.get_write_buffer_size() > 20 * 1024 * 1024
+
+ def process_exited(self) -> None:
+ self.exited = True
+
+ def get_output(self) -> bytes:
+ assert self.output is not None
+ return b''.join(self.output)
+
+ async def eof_and_exited_with_code(self, returncode) -> None:
+ self.close_on_eof = False # otherwise we won't get process_exited()
+ transport = self.transport
+ assert isinstance(transport, cockpit.transports.SubprocessTransport)
+ while not self.exited or not self.eof:
+ await asyncio.sleep(0.1)
+ assert transport.get_returncode() == returncode
+
+
+class TestSpooler:
+ @pytest.mark.asyncio
+ async def test_bad_fd(self) -> None:
+ # Make sure failing to construct succeeds without further failures
+ loop = asyncio.get_running_loop()
+ with pytest.raises(OSError) as raises:
+ cockpit.transports.Spooler(loop, -1)
+ assert raises.value.errno == errno.EBADF
+
+ def create_spooler(self, to_write: bytes = b'') -> cockpit.transports.Spooler:
+ loop = asyncio.get_running_loop()
+ reader, writer = os.pipe()
+ try:
+ spooler = cockpit.transports.Spooler(loop, reader)
+ finally:
+ os.close(reader)
+ try:
+ os.write(writer, to_write)
+ finally:
+ os.close(writer)
+ return spooler
+
+ @pytest.mark.asyncio
+ async def test_poll_eof(self) -> None:
+ spooler = self.create_spooler()
+ while spooler._fd != -1:
+ await asyncio.sleep(0.1)
+ assert spooler.get() == b''
+
+ @pytest.mark.asyncio
+ async def test_nopoll_eof(self) -> None:
+ spooler = self.create_spooler()
+ assert spooler.get() == b''
+ assert spooler._fd == -1
+
+ @pytest.mark.asyncio
+ async def test_poll_small(self) -> None:
+ spooler = self.create_spooler(b'abcd')
+ while spooler._fd != -1:
+ await asyncio.sleep(0.1)
+ assert spooler.get() == b'abcd'
+
+ @pytest.mark.asyncio
+ async def test_nopoll_small(self) -> None:
+ spooler = self.create_spooler(b'abcd')
+ assert spooler.get() == b'abcd'
+ assert spooler._fd == -1
+
+ @pytest.mark.asyncio
+ async def test_big(self) -> None:
+ loop = asyncio.get_running_loop()
+ reader, writer = os.pipe()
+ try:
+ spooler = cockpit.transports.Spooler(loop, reader)
+ finally:
+ os.close(reader)
+
+ try:
+ os.set_blocking(writer, False)
+ written = 0
+ blob = b'a' * 64 * 1024 # NB: pipe buffer is 64k
+ while written < 1024 * 1024:
+ # Note: we should never get BlockingIOError here since we always
+ # give the reader a chance to drain the pipe.
+ written += os.write(writer, blob)
+ while len(spooler.get()) < written:
+ await asyncio.sleep(0.01)
+
+ assert spooler._fd != -1
+ finally:
+ os.close(writer)
+
+ await asyncio.sleep(0.1)
+ assert spooler._fd == -1
+
+ assert len(spooler.get()) == written
+
+
+class TestEpollLimitations:
+ # https://github.com/python/cpython/issues/73903
+ #
+ # There are some types of files that epoll doesn't work with, returning
+ # EPERM. We might be in a situation where we receive one of those on
+ # stdin/stdout for AsyncioTransport, so we'd theoretically like to support
+ # them.
+ async def spool_file(self, filename: str) -> None:
+ loop = asyncio.get_running_loop()
+ with open(filename) as fp:
+ spooler = cockpit.transports.Spooler(loop, fp.fileno())
+ while spooler._fd != -1:
+ await asyncio.sleep(0.1)
+
+ @pytest.mark.xfail
+ @pytest.mark.asyncio
+ async def test_read_file(self) -> None:
+ await self.spool_file(__file__)
+
+ @pytest.mark.xfail
+ @pytest.mark.asyncio
+ async def test_dev_null(self) -> None:
+ await self.spool_file('/dev/null')
+
+
+class TestStdio:
+ @contextlib.contextmanager
+ def create_terminal(self):
+ ours, theirs = os.openpty()
+ stdin = os.dup(theirs)
+ stdout = os.dup(theirs)
+ os.close(theirs)
+ loop = asyncio.get_running_loop()
+ protocol = Protocol()
+ yield ours, protocol, cockpit.transports.StdioTransport(loop, protocol, stdin=stdin, stdout=stdout)
+ os.close(stdin)
+ os.close(stdout)
+
+ @pytest.mark.asyncio
+ async def test_terminal_write_eof(self):
+ # Make sure write_eof() fails
+ with self.create_terminal() as (ours, protocol, transport):
+ assert not transport.can_write_eof()
+ with pytest.raises(RuntimeError):
+ transport.write_eof()
+ os.close(ours)
+
+ @pytest.mark.asyncio
+ async def test_terminal_disconnect(self):
+ # Make sure disconnecting the session shows up as an EOF
+ with self.create_terminal() as (ours, protocol, transport):
+ os.close(ours)
+ while not protocol.eof:
+ await asyncio.sleep(0.1)
+
+
+class TestSubprocessTransport:
+ def subprocess(self, args, **kwargs: Any) -> Tuple[Protocol, cockpit.transports.SubprocessTransport]:
+ loop = asyncio.get_running_loop()
+ protocol = Protocol()
+ transport = cockpit.transports.SubprocessTransport(loop, protocol, args, **kwargs)
+ assert transport._protocol == protocol
+ assert protocol.transport == transport
+ return protocol, transport
+
+ @pytest.mark.asyncio
+ async def test_true(self) -> None:
+ protocol, transport = self.subprocess(['true'])
+ await protocol.eof_and_exited_with_code(0)
+ assert transport.get_stderr() == ''
+
+ @pytest.mark.asyncio
+ async def test_cat(self) -> None:
+ protocol, transport = self.subprocess(['cat'])
+ protocol.close_on_eof = False
+ protocol.write_a_lot()
+ assert transport.can_write_eof()
+ transport.write_eof()
+ await protocol.eof_and_exited_with_code(0)
+ assert protocol.transport is not None # should not have automatically closed
+ assert transport.get_returncode() == 0
+ assert protocol.sent == protocol.received
+ transport.close()
+ assert protocol.transport is None
+
+ @pytest.mark.asyncio
+ async def test_send_signal(self) -> None:
+ protocol, transport = self.subprocess(['cat'])
+ transport.send_signal(signal.SIGINT)
+ await protocol.eof_and_exited_with_code(-signal.SIGINT)
+
+ @pytest.mark.asyncio
+ async def test_pid(self) -> None:
+ protocol, transport = self.subprocess(['sh', '-c', 'echo $$'])
+ protocol.output = []
+ await protocol.eof_and_exited_with_code(0)
+ assert int(protocol.get_output()) == transport.get_pid()
+
+ @pytest.mark.asyncio
+ async def test_terminate(self) -> None:
+ protocol, transport = self.subprocess(['cat'])
+ transport.kill()
+ await protocol.eof_and_exited_with_code(-signal.SIGKILL)
+
+ protocol, transport = self.subprocess(['cat'])
+ transport.terminate()
+ await protocol.eof_and_exited_with_code(-signal.SIGTERM)
+
+ @pytest.mark.asyncio
+ async def test_stderr(self) -> None:
+ loop = asyncio.get_running_loop()
+ protocol = Protocol()
+ transport = cockpit.transports.SubprocessTransport(loop, protocol, ['cat', '/nonexistent'],
+ stderr=subprocess.PIPE)
+ await protocol.eof_and_exited_with_code(1)
+ assert protocol.received == protocol.sent == 0
+ # Unless we reset it, we should get the same result repeatedly
+ assert '/nonexistent' in transport.get_stderr()
+ assert '/nonexistent' in transport.get_stderr()
+ assert '/nonexistent' in transport.get_stderr(reset=True)
+ # After we reset, it should be the empty string
+ assert transport.get_stderr() == ''
+ assert transport.get_stderr(reset=True) == ''
+
+ @pytest.mark.asyncio
+ async def test_safe_watcher_ENOSYS(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setattr(asyncio, 'PidfdChildWatcher', unittest.mock.Mock(side_effect=OSError), raising=False)
+ protocol, transport = self.subprocess(['true'])
+ watcher = transport._get_watcher(asyncio.get_running_loop())
+ assert isinstance(watcher, asyncio.SafeChildWatcher)
+ await protocol.eof_and_exited_with_code(0)
+
+ @pytest.mark.asyncio
+ async def test_safe_watcher_oldpy(self, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.delattr(asyncio, 'PidfdChildWatcher', raising=False)
+ protocol, transport = self.subprocess(['true'])
+ watcher = transport._get_watcher(asyncio.get_running_loop())
+ assert isinstance(watcher, asyncio.SafeChildWatcher)
+ await protocol.eof_and_exited_with_code(0)
+
+ @pytest.mark.asyncio
+ async def test_true_pty(self) -> None:
+ loop = asyncio.get_running_loop()
+ protocol = Protocol()
+ transport = cockpit.transports.SubprocessTransport(loop, protocol, ['true'], pty=True)
+ assert not transport.can_write_eof()
+ await protocol.eof_and_exited_with_code(0)
+ assert protocol.received == protocol.sent == 0
+
+ @pytest.mark.asyncio
+ async def test_broken_pipe(self) -> None:
+ loop = asyncio.get_running_loop()
+ protocol = Protocol()
+ transport = cockpit.transports.SubprocessTransport(loop, protocol, ['true'])
+ protocol.close_on_eof = False
+ while not protocol.exited:
+ await asyncio.sleep(0.1)
+
+ assert protocol.transport is transport # should not close on EOF
+
+ # Now let's write to the stdin with the other side closed.
+ # This should be enough to immediately disconnect us (EPIPE)
+ protocol.write(b'abc')
+ assert protocol.transport is None
+ assert isinstance(protocol.exc, BrokenPipeError)
+
+ @pytest.mark.asyncio
+ async def test_broken_pipe_backlog(self) -> None:
+ loop = asyncio.get_running_loop()
+ protocol = Protocol()
+ transport = cockpit.transports.SubprocessTransport(loop, protocol, ['cat'])
+ protocol.close_on_eof = False
+
+ # Since we're not reading, cat's stdout will back up and it will be
+ # forced to stop reading at some point. We'll still have a rather full
+ # write buffer.
+ protocol.write_a_lot()
+
+ # This will result in the stdin closing. Our next attempt to write to
+ # the buffer should end badly (EPIPE).
+ transport.kill()
+
+ while protocol.transport:
+ await asyncio.sleep(0.1)
+
+ assert protocol.transport is None
+ assert isinstance(protocol.exc, BrokenPipeError)
+
+ @pytest.mark.asyncio
+ async def test_window_size(self) -> None:
+ protocol, transport = self.subprocess(['bash', '-ic',
+ """
+ while true; do
+ sleep 0.1
+ echo ${LINES}x${COLUMNS}
+ done
+ """],
+ pty=True,
+ window=cockpit.transports.WindowSize({'rows': 22, 'cols': 33}))
+ protocol.output = []
+ while b'22x33\r\n' not in protocol.get_output():
+ await asyncio.sleep(0.1)
+
+ transport.set_window_size(cockpit.transports.WindowSize({'rows': 44, 'cols': 55}))
+ while b'44x55\r\n' not in protocol.get_output():
+ await asyncio.sleep(0.1)
+
+ transport.close()
+
+ @pytest.mark.asyncio
+ async def test_env(self) -> None:
+ protocol, transport = self.subprocess(['bash', '-ic', 'echo $HOME'],
+ pty=True,
+ env={'HOME': '/test'})
+ protocol.output = []
+ while b'/test\r\n' not in protocol.get_output():
+ await asyncio.sleep(0.1)
+
+ transport.close()
diff --git a/test/reference-image b/test/reference-image
new file mode 100644
index 0000000..eb90814
--- /dev/null
+++ b/test/reference-image
@@ -0,0 +1 @@
+fedora-39
diff --git a/test/run b/test/run
new file mode 100755
index 0000000..f601b98
--- /dev/null
+++ b/test/run
@@ -0,0 +1,74 @@
+#!/bin/bash
+
+# This is the expected entry point for Cockpit CI; will be called without
+# arguments but with an appropriate $TEST_OS, and optionally $TEST_SCENARIO
+
+# Currently supported scenarios:
+# devel - runs tests with coverage enabled and generates a html file
+# with coverage information and `NODE_ENV=development`
+# to get additional React checks and useful stack traces.
+# pybridge - runs tests with the Python bridge
+# firefox - runs tests using the Firefox browser instead of Chrome
+# networking - networking related tests
+# storage - storage related tests
+# expensive - expensive tests (usually tests which reboot/generate a new initramfs)
+# other - non-networking/storage/expensive tests
+# daily - runs tests with dnf-nightly and udisks-daily repositories enabled
+# updates-testing - runs tests with updates-testing installed
+
+set -eu
+
+test/common/make-bots
+test/common/pixel-tests pull
+
+PREPARE_OPTS=""
+RUN_OPTS=""
+ALL_TESTS="$(test/common/run-tests --test-dir test/verify -l)"
+
+RE_NETWORKING='Networking|Bonding|TestBridge|WireGuard|Firewall|Team|IPA|AD'
+RE_STORAGE='Storage'
+RE_EXPENSIVE='HostSwitching|MultiMachine|Updates|Superuser|Kdump|Pages'
+
+# every known case needs to set RUN_OPTS to something non-empty, so that we can check if we hit any branch
+case "${TEST_SCENARIO:=}" in
+ *devel*) RUN_OPTS="$RUN_OPTS --coverage"; export NODE_ENV=development ;;&
+ *pybridge*) RUN_OPTS="$RUN_OPTS "; PREPARE_OPTS="$PREPARE_OPTS --python" ;;&
+ *firefox*) RUN_OPTS="$RUN_OPTS "; export TEST_BROWSER=firefox ;;&
+
+ *daily*)
+ bots/image-customize --fresh -v --run-command 'dnf -y copr enable rpmsoftwaremanagement/dnf-nightly && dnf -y copr enable @storage/udisks-daily && dnf -y --setopt=install_weak_deps=False update >&2' "$TEST_OS"
+ RUN_OPTS="$RUN_OPTS "
+ PREPARE_OPTS="$PREPARE_OPTS --overlay"
+ ;;&
+ *updates-testing*)
+ bots/image-customize --fresh -v --run-command 'dnf -y update --enablerepo=updates-testing --setopt=install_weak_deps=False >&2' "$TEST_OS"
+ RUN_OPTS="$RUN_OPTS "
+ PREPARE_OPTS="$PREPARE_OPTS --overlay"
+ ;;&
+
+ # split tests into roughly equal scenarios for more parallelism
+ *networking*)
+ RUN_OPTS="$RUN_OPTS $(echo "$ALL_TESTS" | grep -E "$RE_NETWORKING")"
+ PREPARE_OPTS="$PREPARE_OPTS --quick"
+ ;;&
+ *storage*)
+ RUN_OPTS="$RUN_OPTS $(echo "$ALL_TESTS" | grep -E "$RE_STORAGE")"
+ PREPARE_OPTS="$PREPARE_OPTS --quick"
+ ;;&
+ *expensive*)
+ RUN_OPTS="$RUN_OPTS $(echo "$ALL_TESTS" | grep -E "$RE_EXPENSIVE")"
+ PREPARE_OPTS="$PREPARE_OPTS --quick"
+ ;;&
+ *other*)
+ RUN_OPTS="$RUN_OPTS $(echo "$ALL_TESTS" | grep -Ev "$RE_NETWORKING|$RE_STORAGE|$RE_EXPENSIVE")"
+ ;;&
+
+esac
+
+if [ -n "$TEST_SCENARIO" ] && [ -z "$RUN_OPTS" ]; then
+ echo "Unknown test scenario: $TEST_SCENARIO"
+ exit 1
+fi
+
+test/image-prepare --verbose ${PREPARE_OPTS} "$TEST_OS"
+test/common/run-tests --jobs ${TEST_JOBS:-1} --test-dir test/verify --track-naughties ${RUN_OPTS}
diff --git a/test/static-code b/test/static-code
new file mode 100755
index 0000000..4734a7c
--- /dev/null
+++ b/test/static-code
@@ -0,0 +1,200 @@
+#!/bin/bash
+# run static code checks like eslint, flake8, mypy, ruff, vulture.
+
+set -eu
+
+# requires: .flake8
+# requires: pyproject.toml
+# requires: containers/flatpak/test/ruff.toml
+# requires: pkg/ruff.toml
+# requires: test/common/ruff.toml
+# requires: test/example/ruff.toml
+# requires: test/verify/ruff.toml
+# requires: tools/vulture-suppressions/ruff.toml
+
+# we consider any function named test_* to be a test case
+# each test is considered to succeed if it exits with no output
+# exit with status 77 is a skip, with the message in the output
+# otherwise, any output is a failure, even if exit status is 0
+
+# note: `set -e` is not active during the tests.
+
+find_scripts() {
+ # Helper to find all scripts in the tree
+ (
+ # Any non-binary file which contains a given shebang
+ git grep --cached -lIz '^#!.*'"$1"
+ shift
+ # Any file matching the provided globs
+ git ls-files -z "$@"
+ ) | sort -z | uniq -z
+}
+
+find_python_files() {
+ find_scripts 'python3' '*.py'
+}
+
+test_flake8() {
+ command -v flake8 >/dev/null || skip 'no flake8'
+ find_python_files | xargs -r -0 flake8
+}
+
+test_ruff() {
+ command -v ruff >/dev/null || skip 'no ruff'
+ find_python_files | xargs -r -0 ruff check --no-cache
+}
+
+if [ "${WITH_PARTIAL_TREE:-0}" = 0 ]; then
+ mypy_strict_files='
+ src/cockpit/__init__.py
+ src/cockpit/_version.py
+ src/cockpit/jsonutil.py
+ src/cockpit/protocol.py
+ src/cockpit/transports.py
+ '
+ test_mypy() {
+ command -v mypy >/dev/null || skip 'no mypy'
+ for pkg in systemd_ctypes ferny bei; do
+ test -e "src/cockpit/_vendor/${pkg}/__init__.py" || skip "no ${pkg}"
+ done
+ mypy --no-error-summary src/cockpit test/pytest
+ # test scripts individually, to avoid clashing on `__main__`
+ # also skip integration tests, they are too big and not annotated
+ find_scripts 'python3' "*.none" | grep -zv 'test/' | xargs -r -0 -n1 mypy --no-error-summary
+ mypy --no-error-summary --strict $mypy_strict_files
+ }
+
+ test_vulture() {
+ # vulture to find unused variables/functions
+ command -v vulture >/dev/null || skip 'no vulture'
+ find_python_files | xargs -r -0 vulture
+ }
+fi
+
+
+test_js_translatable_strings() {
+ # Translatable strings must be marked with _(""), not _('')
+
+ ! git grep -n -E "(gettext|_)\(['\`]" -- {src,pkg}/'*'.{js,jsx}
+}
+
+if [ "${WITH_PARTIAL_TREE:-0}" = 0 ]; then
+ test_eslint() {
+ test -x node_modules/.bin/eslint -a -x /usr/bin/node || skip 'no eslint'
+ find_scripts 'node' '*.js' '*.jsx' | xargs -0 node_modules/.bin/eslint
+ }
+fi
+
+test_stylelint() {
+ test -x node_modules/.bin/stylelint -a -x /usr/bin/node || skip 'no stylelint'
+ git ls-files -z '*.css' '*.scss' | xargs -r -0 node_modules/.bin/stylelint
+}
+
+test_no_translatable_attr() {
+ # Use of translatable attribute in HTML: should be 'translate' instead
+
+ ! git grep -n 'translatable=["'\'']yes' -- pkg doc
+}
+
+test_unsafe_security_policy() {
+ # It's dangerous to have 'unsafe-inline' or 'unsafe-eval' in our
+ # content-security-policy entries.
+
+ git grep -lIz -E 'content-security-policy.*(\*|unsafe)' 'pkg/*/manifest.json' | while read -d '' filename; do
+ if test ! -f "$(dirname ${filename})/content-security-policy.override"; then
+ echo "${filename} contains unsafe content security policy"
+ fi
+ done
+}
+
+test_json_verify() {
+ # Check all JSON files for validity
+
+ git ls-files -z '*.json' | while read -d '' filename; do
+ python3 -m json.tool "${filename}" /dev/null 2>&1 | sed "s@^@${filename}: @"
+ done
+}
+
+test_html_verify() {
+ # Check all HTML files for syntactic validity
+
+ git ls-files -z 'pkg/*.html' | while read -d '' filename; do
+ if ! python3 -c "import xml.etree.ElementTree as ET; ET.parse('${filename}')"; then
+ echo "${filename} contains invalid XML"
+ fi
+ done
+}
+
+test_include_config_h() {
+ # Every C file should #include "config.h" at the top
+
+ git ls-files -cz '*.c' | while read -d '' filename; do
+ if sed -n '/^#include "config.h"$/q1; /^\s*#/q;' "${filename}"; then
+ printf '%s: #include "config.h" is not the first line\n' "${filename}"
+ fi
+ done
+}
+
+### end of tests. start of machinery.
+
+skip() {
+ printf "%s\n" "$*"
+ exit 77
+}
+
+main() {
+ if [ $# = 0 ]; then
+ tap=''
+ elif [ $# = 1 -a "$1" = "--tap" ]; then
+ tap='1'
+ else
+ printf "usage: %s [--tap]\n" "$0" >&2
+ exit 1
+ fi
+
+ cd "${0%/*}/.."
+ if [ ! -e .git ]; then
+ echo '1..0 # SKIP not in a git checkout'
+ exit 0
+ fi
+
+ exit_status=0
+ counter=0
+
+ tests=($(compgen -A function 'test_'))
+ [ -n "${tap}" ] && printf "1..%d\n" "${#tests[@]}"
+
+ for test_function in "${tests[@]}"; do
+ path="/static-code/$(echo ${test_function} | tr '_' '-')"
+ counter=$((counter + 1))
+ fail=''
+ skip=''
+
+ # run the test, capturing its output and exit status
+ output="$(${test_function} 2>&1)" && test_status=0 || test_status=$?
+
+ if [ "${test_status}" = 77 ]; then
+ if [ -z "${tap}" ]; then
+ printf >&2 "WARNING: skipping %s: %s\n" "${path}" "${output}"
+ fi
+ skip=" # SKIP ${output}"
+ output=''
+ elif [ "${test_status}" != 0 -o -n "${output}" ]; then
+ exit_status=1
+ fail=1
+ fi
+
+ # Only print output on failures or --tap mode
+ [ -n "${tap}" -o -n "${fail}" ] || continue
+
+ # excluding the plan, this is the only output that we ever generate
+ printf "%s %d %s%s\n" "${fail:+not }ok" "${counter}" "${path}" "${skip}"
+ if [ -n "${output}" ]; then
+ printf "%s\n" "${output}" | sed -e 's/^/# /'
+ fi
+ done
+
+ exit "${exit_status}"
+}
+
+main "$@"
diff --git a/test/verify/__init__.py b/test/verify/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/verify/__init__.py
diff --git a/test/verify/check-apps b/test/verify/check-apps
new file mode 100755
index 0000000..ba02c28
--- /dev/null
+++ b/test/verify/check-apps
@@ -0,0 +1,393 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+import time
+
+import packagelib
+import testlib
+
+
+@testlib.skipImage("TODO: bug in refreshing removed/installed state on Arch Linux", "arch")
+@testlib.skipOstree("Not supported")
+@testlib.nondestructive
+class TestApps(packagelib.PackageCase):
+
+ def setUp(self):
+ super().setUp()
+ self.appstream_collection = set()
+ self.machine.upload(["verify/files/test.png"], "/var/tmp/")
+
+ def createAppStreamPackage(self, name, version, revision, install=False):
+ self.createPackage(name, version, revision, content={
+ f"/usr/share/metainfo/org.cockpit-project.{name}.metainfo.xml": f"""
+<component type="addon">
+ <extends>org.cockpit_project.cockpit</extends>
+ <id>org.cockpit-project.{name}</id>
+ <icon type="local">/usr/share/pixmaps/{name}.png</icon>
+ <name>{name}</name>
+ <summary>{name} application for testing</summary>
+ <launchable type="cockpit-manifest">{name}</launchable>
+</component>
+""",
+ f"/usr/share/pixmaps/{name}.png": {"path": "/var/tmp/test.png"}}, install=install)
+ self.appstream_collection.add(name)
+
+ def createAppStreamRepoPackage(self, subdir=None):
+ if subdir is None:
+ subdir = "swcatalog/xml"
+ body = ""
+ for p in self.appstream_collection:
+ body += f"""
+ <component type="addon">
+ <extends>org.cockpit_project.cockpit</extends>
+ <id>org.cockpit-project.{p}</id>
+ <icon type="cached">test.png</icon>
+ <name>{p}</name>
+ <summary>An application for testing</summary>
+ <launchable type="cockpit-manifest">{p}</launchable>
+ <description>
+ <p>DESCRIPTION:none</p>
+ </description>
+ <url type="homepage">https://{p}.com</url>
+ <pkgname>{p}</pkgname>
+ </component>
+"""
+ self.machine.execute(f"mkdir -p /usr/share/{subdir}")
+ self.createPackage("appstream-data-test", "1.0", "1", content={
+ f"/usr/share/{subdir}/test.xml": f"""
+<components origin="test">
+{body}
+</components>
+ """,
+ f"/usr/share/{subdir.split('/')[0]}/icons/test/64x64/test.png": {"path": "/var/tmp/test.png"}})
+ self.enableRepo()
+ self.machine.execute("systemctl stop packagekit; pkcon refresh force")
+ # ignore the corresponding journal entry
+ self.allow_journal_messages("org.freedesktop.PackageKit.*org.freedesktop.DBus.Error.NoReply.*")
+
+ def testBasic(self, urlroot=""):
+ b = self.browser
+ m = self.machine
+
+ self.allow_journal_messages("can't remove watch: Invalid argument")
+
+ self.restore_dir("/usr/share/metainfo", reboot_safe=True)
+ self.restore_dir("/usr/share/swcatalog", reboot_safe=True)
+ self.restore_dir("/usr/share/app-info", reboot_safe=True)
+ self.restore_dir("/var/cache/app-info", reboot_safe=True)
+
+ # Make sure none of the appstream directories exist. They
+ # will be created later and we need to cope with that.
+ m.execute("rm -rf /usr/share/metainfo /usr/share/swcatalog /usr/share/app-info /var/cache/app-info")
+
+ # instead of the actual distro packages, use our own fake repo data package
+ self.write_file("/etc/cockpit/apps.override.json",
+ '{ "config": { "appstream_data_packages": [ "appstream-data-test" ] } }')
+
+ if urlroot != "":
+ m.write("/etc/cockpit/cockpit.conf", f"[WebService]\nUrlRoot={urlroot}")
+
+ self.login_and_go("/apps", urlroot=urlroot)
+ b.wait_in_text(".pf-v5-c-empty-state", "No applications installed or available")
+ b.wait_in_text(".pf-v5-c-empty-state", "Application information is missing")
+
+ # still no metadata, but already installed application
+ self.createAppStreamPackage("already", "1.0", "1", install=True)
+ b.wait_not_present(".pf-v5-c-empty-state")
+ b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('already') button:contains('Remove')")
+ b.wait_in_text(".pf-v5-c-alert", "Application information is missing")
+
+ self.createAppStreamPackage("app-1", "1.0", "1")
+ self.createAppStreamRepoPackage()
+
+ # Install package metadata
+ b.click(".pf-v5-c-alert button")
+
+ with b.wait_timeout(30):
+ b.wait_not_present(".pf-v5-c-alert")
+ b.click(".app-list #app-1")
+
+ b.wait_visible('a[href="https://app-1.com"]')
+ b.wait_visible(f'#app-page img[src^="{urlroot}/cockpit/channel/"]')
+ b.click(".pf-v5-c-breadcrumb a:contains('Applications')")
+
+ b.wait_visible("#list-page")
+ b.wait_not_present("#app-page")
+
+ b.click(".app-list .pf-v5-c-data-list__item-row:contains('app-1') button:contains('Install')")
+ b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('app-1') button:contains('Remove')")
+ b.wait_visible(f".app-list .pf-v5-c-data-list__item-row:contains('app-1') img[src^='{urlroot}/cockpit/channel/']")
+ m.execute("test -f /stamp-app-1-1.0-1")
+
+ b.click(".app-list .pf-v5-c-data-list__item-row:contains('app-1') button:contains('Remove')")
+ b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('app-1') button:contains('Install')")
+ b.wait_visible(f".app-list .pf-v5-c-data-list__item-row:contains('app-1') img[src^='{urlroot}/cockpit/channel/']")
+ m.execute("! test -f /stamp-app-1-1.0-1")
+
+ def testWithUrlRoot(self):
+ self.testBasic(urlroot="/webcon")
+
+ def testOsMap(self):
+ b = self.browser
+ m = self.machine
+
+ self.allow_journal_messages("can't remove watch: Invalid argument")
+
+ self.restore_dir("/usr/share/metainfo", reboot_safe=True)
+ self.restore_dir("/usr/share/swcatalog", reboot_safe=True)
+ self.restore_dir("/usr/share/app-info", reboot_safe=True)
+ self.restore_dir("/var/cache/app-info", reboot_safe=True)
+
+ # Make sure none of the appstream directories exist. They
+ # will be created later and we need to cope with that.
+ m.execute("rm -rf /usr/share/metainfo /usr/share/swcatalog /usr/share/app-info /var/cache/app-info")
+
+ # use a fake distro map
+ self.write_file("/etc/cockpit/apps.override.json",
+ '{ "config": { "appstream_data_packages":'
+ ' {"testy": ["appstream-data-test"], "otheros": ["nosuchpackage"]},'
+ ' "appstream_config_packages": []'
+ ' }}')
+
+ self.createAppStreamPackage("app-1", "1.0", "1")
+ # old subdir until Fedora 39
+ self.createAppStreamRepoPackage(subdir="app-info/xmls")
+
+ self.login_and_go("/apps")
+ b.wait_visible(".pf-v5-c-empty-state")
+
+ # os-release is a symlink target, don't clobber that
+ self.restore_file("/etc/os-release")
+ m.execute("rm /etc/os-release")
+
+ # get along with absent os-release
+ b.click("#refresh")
+ # the progress bar is too fast to reliably catch it
+ time.sleep(1)
+ b.wait_not_present("#refresh-progress")
+ b.wait_visible(".pf-v5-c-empty-state")
+ # wait until check for installed metadata package finished
+ b.wait_attr("#list-page", "data-packages-checked", "true")
+ # no appstream metadata available, don't advertise it
+ b.wait_in_text(".pf-v5-c-empty-state", "No applications installed or available")
+ self.assertNotIn("Install application information", b.text(".pf-v5-c-empty-state"))
+ b.wait_not_present(".pf-v5-c-empty-state button")
+
+ # unknown OS: nothing gets installed
+ m.write("/etc/os-release", 'ID="unmapped"\nID_LIKE="mysterious"\nVERSION_ID="1"\n')
+ b.click("#refresh")
+ # the progress bar is too fast to reliably catch it
+ time.sleep(1)
+ b.wait_not_present("#refresh-progress")
+ b.wait_visible(".pf-v5-c-empty-state")
+
+ # known OS: appstream-data-test gets installed from the map
+ m.write("/etc/os-release", 'ID="derivative"\nID_LIKE="spicy testy"\nVERSION_ID="1"\n')
+ b.click("#refresh")
+ m.execute("until test -e /usr/share/app-info/xmls/test.xml; do sleep 1; done")
+ b.wait_visible(".app-list #app-1")
+
+ def testL10N(self):
+ b = self.browser
+ m = self.machine
+
+ # Switching to a language might produce these messages, which seem to be harmless.
+ self.allow_journal_messages("invalid or unusable locale.*",
+ "Error .* data: Connection reset by peer")
+
+ # Reset everything
+ m.execute("for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info; do mkdir -p $d; mount -t tmpfs tmpfs $d; done")
+ self.addCleanup(m.execute, "for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info; do umount $d; done")
+
+ self.login_and_go("/apps")
+ b.wait_visible(".pf-v5-c-empty-state")
+
+ m.write("/usr/share/app-info/xmls/test.xml", """
+<components origin="test">
+ <component type="addon">
+ <extends>org.cockpit_project.cockpit</extends>
+ <id>org.cockpit-project.foo</id>
+ <name>NAME:none</name>
+ <name xml:lang="de">NAME:de</name>
+ <summary>SUMMARY:none</summary>
+ <summary xml:lang="de">SUMMARY:de</summary>
+ <description>
+ <p>DESCRIPTION:none</p>
+ <p xml:lang="de">DESCRIPTION:de</p>
+ </description>
+ <launchable type="cockpit-manifest">foo</launchable>
+ <pkgname>foo</pkgname>
+ </component>
+</components>""")
+
+ b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('SUMMARY:none') button:contains('Install')")
+ b.click(".app-list .pf-v5-c-data-list__item-row:contains('SUMMARY:none') .pf-m-inline")
+
+ b.wait_visible(".app-description:contains('DESCRIPTION:none')")
+
+ def set_lang(lang):
+ b.switch_to_top()
+ b.open_session_menu()
+ b.click(".display-language-menu")
+ b.wait_visible('#display-language-modal')
+ b.click(f'#display-language-modal li[data-value={lang}] button')
+ b.click("#display-language-modal footer button.pf-m-primary")
+ b.wait_language(lang)
+ b.enter_page("/apps")
+
+ set_lang("de-de")
+ b.wait_visible(".app-description:contains('DESCRIPTION:de')")
+ b.wait_not_present(".app-description:contains('DESCRIPTION:none')")
+
+ set_lang("ja-jp")
+ b.wait_visible(".app-description:contains('DESCRIPTION:none')")
+ b.wait_not_present(".app-description:contains('DESCRIPTION:de')")
+
+ # like in the general whitelist, but translated
+ self.allow_journal_messages("xargs: basename: .*Signal 13.*")
+
+ def testBrokenXML(self):
+ b = self.browser
+ m = self.machine
+
+ # Reset everything
+ m.execute("for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info; do mkdir -p $d; mount -t tmpfs tmpfs $d; done")
+ self.addCleanup(m.execute, "for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info; do umount $d; done")
+
+ self.login_and_go("/apps")
+ b.wait_visible(".pf-v5-c-empty-state")
+
+ self.allow_journal_messages(".*/usr/share/app-info/xmls/test.xml.*",
+ ".*xml.etree.ElementTree.ParseError.*")
+
+ def reset():
+ m.write("/usr/share/app-info/xmls/test.xml", """
+<components origin="test">
+ <component type="addon">
+ <extends>org.cockpit_project.cockpit</extends>
+ <id>org.cockpit-project.test</id>
+ <name>Name</name>
+ <summary>Summary</summary>
+ <description>
+ <p>Description</p>
+ </description>
+ <launchable type="cockpit-manifest">foo</launchable>
+ <pkgname>foo</pkgname>
+ </component>
+</components>""")
+ b.wait_not_present(".pf-v5-c-empty-state")
+ b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('Summary')")
+
+ # First lay down some good XML so that we can later detect the reaction to broken XML.
+ reset()
+
+ # Unparsable
+ m.write("/usr/share/app-info/xmls/test.xml", """
+This <is <not XML.
+""")
+ b.wait_visible(".pf-v5-c-empty-state")
+ reset()
+
+ # Not really AppStream
+ m.write("/usr/share/app-info/xmls/test.xml", """
+<foo></foo>
+""")
+ b.wait_visible(".pf-v5-c-empty-state")
+ reset()
+
+ # No origin
+ m.write("/usr/share/app-info/xmls/test.xml", """
+<components>
+ <component type="addon">
+ <extends>org.cockpit_project.cockpit</extends>
+ <id>org.cockpit-project.test</id>
+ <name>Name</name>
+ <summary>Summary</summary>
+ <description>
+ <p>Description</p>
+ </description>
+ <launchable type="cockpit-manifest">foo</launchable>
+ <pkgname>foo</pkgname>
+ </component>
+</components>""")
+ b.wait_visible(".pf-v5-c-empty-state")
+ reset()
+
+ # No package
+ m.write("/usr/share/app-info/xmls/test.xml", """
+<components origin="test">
+ <component type="addon">
+ <extends>org.cockpit_project.cockpit</extends>
+ <id>org.cockpit-project.test</id>
+ <name>Name</name>
+ <summary>Summary</summary>
+ <description>
+ <p>Description</p>
+ </description>
+ <launchable type="cockpit-manifest">foo</launchable>
+ </component>
+</components>""")
+ b.wait_visible(".pf-v5-c-empty-state")
+ reset()
+
+ # No id
+ m.write("/usr/share/app-info/xmls/test.xml", """
+<components origin="test">
+ <component type="addon">
+ <extends>org.cockpit_project.cockpit</extends>
+ <name>Name</name>
+ <summary>No description</summary>
+ <launchable type="cockpit-manifest">foo</launchable>
+ <pkgname>foo</pkgname>
+ </component>
+</components>""")
+ b.wait_visible(".pf-v5-c-empty-state")
+ reset()
+
+ # Error (launchable without type) in earlier entry, shouldn't affect the later entry
+ m.write("/usr/share/app-info/xmls/test.xml", """
+<components origin="test">
+ <component type="addon">
+ <extends>org.cockpit_project.cockpit</extends>
+ <id>org.cockpit-project.test2</id>
+ <name>Name</name>
+ <summary>Summary</summary>
+ <description>
+ <p>Description 2</p>
+ </description>
+ <launchable>foo</launchable>
+ <pkgname>foo</pkgname>
+ </component>
+ <component type="addon">
+ <extends>org.cockpit_project.cockpit</extends>
+ <id>org.cockpit-project.test</id>
+ <name>Name</name>
+ <summary>Summary 2</summary>
+ <description>
+ <p>Description</p>
+ </description>
+ <launchable type="cockpit-manifest">foo</launchable>
+ <pkgname>foo</pkgname>
+ </component>
+</components>""")
+ b.wait_not_present(".pf-v5-c-empty-state")
+ b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('Summary 2')")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-bots-api b/test/verify/check-bots-api
new file mode 100755
index 0000000..d8b28fd
--- /dev/null
+++ b/test/verify/check-bots-api
@@ -0,0 +1,167 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2018 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+from glob import glob
+
+import testlib
+import testvm
+
+
+@unittest.skipUnless("TEST_OS" in os.environ, "TEST_OS not set")
+@testlib.skipDistroPackage()
+class TestImageCustomize(unittest.TestCase):
+
+ def checkBoot(self, image):
+ with testvm.Timeout(seconds=300, error_message="Timed out waiting for image to run"):
+ network = testvm.VirtNetwork(0, image=image)
+ machine = testvm.VirtMachine(image=image, networking=network.host(), memory_mb=512)
+ machine.start()
+ machine.wait_boot()
+ out = machine.execute('cat /var/custom-test')
+ machine.stop()
+ self.assertEqual(out, "hello\n")
+
+ def testCustomDir(self):
+ dest = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, dest)
+
+ img = os.path.join(dest, os.environ["TEST_OS"])
+ with testvm.Timeout(seconds=300, error_message="Timed out waiting for image-customize"):
+ subprocess.check_call(["bots/image-customize", "--verbose", "--run-command",
+ "echo hello > /var/custom-test", img])
+
+ self.assertTrue(os.path.exists(img))
+ self.checkBoot(img)
+
+ def testBaseImage(self):
+ img = "custom-" + os.environ["TEST_OS"]
+
+ def cleanup():
+ for f in glob(f"test/images/{img}*"):
+ os.unlink(f)
+ self.addCleanup(cleanup)
+
+ with testvm.Timeout(seconds=300, error_message="Timed out waiting for image-customize"):
+ subprocess.check_call(["bots/image-customize", "--verbose", "--run-command",
+ "echo hello > /var/custom-test", "--base-image", os.environ["TEST_OS"], img])
+
+ self.assertTrue(os.path.exists(os.path.join("test/images", img)))
+ # notice, not giving directory here - test/images/ should be the default
+ self.checkBoot(img)
+
+ def testScriptRelativePath(self):
+ dest = tempfile.mkdtemp(dir=".")
+ self.addCleanup(shutil.rmtree, dest)
+
+ script = os.path.join(dest, "setup.sh")
+ with open(script, "w") as f:
+ f.write("#!/bin/sh -eu\necho hello > /var/custom-test\n")
+
+ img = os.path.join(dest, os.environ["TEST_OS"])
+ with testvm.Timeout(seconds=300, error_message="Timed out waiting for image-customize"):
+ subprocess.check_call(["bots/image-customize", "--verbose", "--script", script, img])
+
+ self.assertTrue(os.path.exists(img))
+ self.checkBoot(img)
+
+ def testUpload(self):
+ dest = tempfile.mkdtemp(dir=".")
+ self.addCleanup(shutil.rmtree, dest)
+ img = os.path.join(dest, os.environ["TEST_OS"])
+
+ with testvm.Timeout(seconds=300, error_message="Timed out waiting for image-customize"):
+ subprocess.check_call(["bots/image-customize", "--verbose",
+ "--upload", "/etc/passwd:/tmp/passwd",
+ "--run-command", "echo hello > /var/custom-test",
+ "--run-command", "grep ^root: /tmp/passwd", img])
+
+ self.checkBoot(img)
+
+ def testFailurePropagation(self):
+ dest = tempfile.mkdtemp(dir=".")
+ self.addCleanup(shutil.rmtree, dest)
+ img = os.path.join(dest, os.environ["TEST_OS"])
+
+ with testvm.Timeout(seconds=300, error_message="Timed out waiting for image-customize"):
+ subprocess.check_call(["bots/image-customize", "--verbose",
+ "--run-command", "true", img])
+
+ with self.assertRaises(subprocess.CalledProcessError):
+ subprocess.check_call(["bots/image-customize", "--verbose",
+ "--run-command", "false", img])
+
+ def testResize(self):
+ dest = tempfile.mkdtemp(dir=".")
+ self.addCleanup(shutil.rmtree, dest)
+ img = os.path.join(dest, os.environ["TEST_OS"])
+
+ with testvm.Timeout(seconds=300, error_message="Timed out waiting for image-customize"):
+ subprocess.check_call(["bots/image-customize", "--verbose",
+ "--resize", "20G", img])
+
+ output = subprocess.check_output(["qemu-img", "info", "--output=json", img], encoding="utf-8")
+ info = json.loads(output)
+ self.assertEqual(int(info['virtual-size']) // 1024 // 1024 // 1024, 20)
+
+
+@unittest.skipUnless("TEST_OS" in os.environ, "TEST_OS not set")
+@testlib.skipDistroPackage()
+class TestBotsVM(unittest.TestCase):
+
+ def testBasic(self):
+ dest = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, dest)
+ img = os.path.join(dest, os.environ["TEST_OS"])
+ with testvm.Timeout(seconds=300, error_message="Timed out waiting for image-customize"):
+ subprocess.check_call(["bots/image-customize", "--verbose", "--run-command",
+ "echo hello > /var/custom-test", img])
+
+ # boot it and wait for RUNNING marker, parse out ssh and cockpit addresses
+ with testvm.Timeout(seconds=300, error_message="Timed out waiting for testvm.py to boot VM"):
+ vm = subprocess.Popen(["bots/machine/testvm.py", img],
+ stdout=subprocess.PIPE, universal_newlines=True)
+ # first line should be the SSH command
+ ssh_command = vm.stdout.readline().split()
+ # second line is the redirected cockpit address
+ cockpit_address = vm.stdout.readline()
+ # third should be the "I am ready" flag
+ running = vm.stdout.readline()
+
+ self.assertEqual(running, "RUNNING\n")
+ self.assertTrue(cockpit_address.startswith("http://127.0.0.2:9"), cockpit_address)
+ # test SSH command and that we have the expected flag file
+ self.assertEqual(ssh_command[0], "ssh")
+ with testvm.Timeout(seconds=30, error_message="Timed out waiting for ssh command"):
+ out = subprocess.check_output([*ssh_command, "cat", "/var/custom-test"])
+ self.assertEqual(out, b"hello\n")
+
+ # should cleanly stop on SIGTERM
+ vm.terminate()
+ with testvm.Timeout(seconds=60, error_message="Timed out waiting for script to terminate"):
+ self.assertEqual(vm.wait(), 0)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-client b/test/verify/check-client
new file mode 100755
index 0000000..8f02479
--- /dev/null
+++ b/test/verify/check-client
@@ -0,0 +1,246 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2023 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+
+@testlib.skipImage("needs pybridge", "rhel-8*", "centos-8*")
+# enable this once our cockpit/ws container can beiboot
+@testlib.skipOstree("client setup does not work with ws container")
+class TestClient(testlib.MachineCase):
+
+ provision = {
+ "client": {"address": "10.111.113.1/24", "memory_mb": 660},
+ "target": {"address": "10.111.113.2/24", "memory_mb": 660},
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.m_target = self.machines["target"]
+ self.m_client = self.machines["client"]
+ self.m_target.execute("hostnamectl set-hostname target")
+ # validate on-demand install: this does not work on arch, non-split package
+ if self.m_target.image.startswith("debian") or self.m_target.image.startswith("ubuntu"):
+ self.m_target.execute("dpkg --purge cockpit-pcp-dbgsym || true; dpkg --purge cockpit-pcp pcp")
+ elif self.m_target.image != 'arch':
+ self.m_target.execute("rpm --erase --verbose cockpit-pcp pcp")
+
+ # replicate the plumbing bits of src/client/cockpit-client to set up cockpit-beiboot
+ self.m_client.write("/etc/cockpit/cockpit.conf", """
+[WebService]
+X-For-CockpitClient = true
+LoginTo = true
+
+[Ssh-Login]
+Command = /usr/bin/env python3 -m cockpit.beiboot
+""")
+
+ def check_login(self, expected_user):
+ b = self.browser
+ b.wait_visible('#content')
+ b.wait_in_text("#host-toggle", expected_user)
+ b.enter_page("/system")
+ if self.is_devel_build():
+ b.wait_visible("#system_information_hostname_text")
+ # devel disables preloads
+ else:
+ b.wait_visible("#page_status_notification_updates")
+ b.switch_to_top()
+
+ def logout(self, check_last_host=None):
+ b = self.browser
+
+ b.logout()
+ # FIXME: This is broken, nothing appears
+ # b.wait_text("#brand", "Connect to:")
+ if check_last_host:
+ b.wait_val("#server-field", check_last_host)
+
+ # no leaked processes
+ self.m_client.execute('''
+ while [ -n "$(pgrep -au admin | grep -Ev 'cockpit-ws' >&2)" ]; do sleep 1; done
+ ''', timeout=10)
+ self.m_target.execute("while pgrep -af '([c]ockpit|[s]sh-agent)' >&2; do sleep 1; done",
+ timeout=30)
+
+ def testBeibootNoBridge(self):
+ # set up target machine: no cockpit
+ self.m_target.execute("rm /usr/bin/cockpit-bridge; rm -r /usr/share/cockpit")
+ self.checkLoginScenarios(local_bridge=False)
+
+ def testBeibootWithBridge(self):
+ self.checkLoginScenarios(local_bridge=True)
+
+ def checkLoginScenarios(self, *, local_bridge=True):
+ self.m_client.spawn(f"runuser -u admin -- {self.libexecdir}/cockpit-ws --no-tls", "ws.log")
+
+ b = self.browser
+ b.open("/")
+
+ # same username + password login, unknown host key
+ b.wait_text("#brand", "Connect to:")
+ b.wait_not_visible("#recent-hosts-list")
+ b.set_val("#server-field", "10.111.113.2")
+ b.click("#login-button")
+ b.wait_in_text("#conversation-group", "authenticity of host '10.111.113.2")
+ b.set_val("#conversation-input", "yes")
+ b.click("#login-button")
+ b.wait_text("#conversation-prompt", "admin@10.111.113.2's password: ")
+ b.set_val("#conversation-input", "foobar")
+ b.click("#login-button")
+ self.check_login("admin@target")
+ b.wait_in_text("#host-apps", "Services")
+ b.wait_in_text("#host-apps", "Terminal")
+ b.become_superuser()
+ b.drop_superuser()
+ self.logout(check_last_host="10.111.113.2")
+
+ # remembers the last host it connected to
+ b.wait_in_text("#recent-hosts-list", "10.111.113.2")
+
+ # same username + password login, now host is known
+ b.click("#login-button")
+ b.wait_text("#conversation-prompt", "admin@10.111.113.2's password: ")
+ b.set_val("#conversation-input", "foobar")
+ b.click("#login-button")
+ self.check_login("admin@target")
+ b.become_superuser()
+
+ # cockpit-pcp is installed on client, but not on target; recognize that
+ if self.m_target.image != 'arch':
+ b.go("/metrics")
+ b.enter_page("/metrics")
+ b.wait_in_text(".pf-v5-c-empty-state", "cockpit-pcp is missing")
+ if local_bridge:
+ # on-demand install is allowed
+ b.wait_in_text(".pf-v5-c-empty-state button.pf-m-primary", "Install cockpit-pcp")
+ b.click(".pf-v5-c-empty-state button.pf-m-primary")
+ b.wait_in_text(".pf-v5-c-modal-box", "cockpit-pcp will be installed")
+ b.click(".pf-v5-c-modal-box button.cancel")
+ else:
+ # not currently supported in beiboot scenario
+ b.wait_in_text(".pf-v5-c-empty-state", "Installation not supported without installed cockpit package")
+ b.wait_not_present(".pf-v5-c-empty-state button")
+
+ b.drop_superuser()
+ self.logout()
+
+ # wrong password, SSH gives you three attempts
+ b.click("#login-button")
+ for _ in range(3):
+ b.wait_text("#conversation-prompt", "admin@10.111.113.2's password: ")
+ b.set_val("#conversation-input", "wrong")
+ b.click("#login-button")
+ b.wait_in_text("#login-fatal-message", "admin@10.111.113.2: Permission denied")
+ b.click("#login-again")
+ b.wait_text("#brand", "Connect to:")
+ # resets the host field
+ b.wait_val("#server-field", "")
+
+ # connect to most recent host
+ b.click("#recent-hosts-list .host-line button.host-name")
+ b.wait_text("#conversation-prompt", "admin@10.111.113.2's password: ")
+ b.set_val("#conversation-input", "foobar")
+ b.click("#login-button")
+ self.check_login("admin@target")
+ self.logout()
+
+ # different user name + password
+ self.m_target.execute("useradd -s /bin/bash user; echo user:barfoo | chpasswd")
+ b.set_val("#server-field", "user@10.111.113.2")
+ b.click("#login-button")
+ b.wait_text("#conversation-prompt", "user@10.111.113.2's password: ")
+ b.set_val("#conversation-input", "barfoo")
+ b.click("#login-button")
+ self.check_login("user@target")
+
+ # not a sudoer
+ b.open_superuser_dialog()
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "barfoo")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.click(".pf-v5-c-modal-box:contains('Problem becoming administrator') button:contains('Close')")
+ b.wait_not_present(".pf-v5-c-modal-box")
+ b.check_superuser_indicator("Limited access")
+
+ self.logout()
+ b.wait_in_text("#recent-hosts-list", "user@10.111.113.2")
+
+ # explicit port
+ b.set_val("#server-field", "10.111.113.2:22")
+ b.click("#login-button")
+ b.wait_text("#conversation-prompt", "admin@10.111.113.2's password: ")
+ b.set_val("#conversation-input", "foobar")
+ b.click("#login-button")
+ self.check_login("admin@target")
+ self.logout()
+ b.wait_in_text("#recent-hosts-list", "10.111.113.2:22")
+
+ # different user name + explicit port
+ b.set_val("#server-field", "user@10.111.113.2:22")
+ b.click("#login-button")
+ b.wait_text("#conversation-prompt", "user@10.111.113.2's password: ")
+ b.set_val("#conversation-input", "barfoo")
+ b.click("#login-button")
+ self.check_login("user@target")
+ self.logout()
+ b.wait_in_text("#recent-hosts-list", "user@10.111.113.2:22")
+
+ # remove that recent hosts entry, this also avoids ambiguous selectors further down
+ b.click("#recent-hosts-list .host-line:contains('user@10.111.113.2:22') button.host-remove")
+ b.wait_not_in_text("#recent-hosts-list", "user@10.111.113.2:22")
+
+ # unreachable host
+ b.set_val("#server-field", "unknownhost")
+ b.click("#login-button")
+ b.wait_in_text("#login-fatal-message", "Could not resolve hostname unknownhost")
+ b.click("#login-again")
+ b.wait_text("#brand", "Connect to:")
+ # does not appear in recent hosts
+ b.wait_in_text("#recent-hosts-list", "10.111.113.2")
+ self.assertNotIn("unknownhost", b.text("#recent-hosts-list"))
+
+ # wrong port
+ b.set_val("#server-field", "10.111.113.2:222")
+ b.click("#login-button")
+ b.wait_in_text("#login-fatal-message", "connect to host 10.111.113.2 port 222: No route to host")
+
+ # unencrypted SSH key login
+ self.m_client.execute("runuser -u admin -- ssh-keygen -t rsa -N '' -f ~admin/.ssh/id_rsa")
+ pubkey = self.m_client.execute("cat ~admin/.ssh/id_rsa.pub")
+ self.m_target.write("/home/user/.ssh/authorized_keys", pubkey, owner="user:user", perm="600")
+ b.click("#recent-hosts-list .host-line:contains('user@10.111.113.2') button.host-name")
+ self.check_login("user@target")
+ self.logout()
+
+ # encrypted SSH key login
+ self.m_client.execute("runuser -u admin -- ssh-keygen -f ~admin/.ssh/id_rsa -p -P '' -N foobarfoo")
+ b.click("#login-button")
+ b.wait_text("#conversation-prompt", "Enter passphrase for key '/home/admin/.ssh/id_rsa': ")
+ b.set_val("#conversation-input", "wrong")
+ b.click("#login-button")
+ b.wait_val("#conversation-input", "")
+ b.wait_text("#conversation-prompt", "Enter passphrase for key '/home/admin/.ssh/id_rsa': ")
+ b.set_val("#conversation-input", "foobarfoo")
+ b.click("#login-button")
+ self.check_login("user@target")
+ self.logout()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-connection b/test/verify/check-connection
new file mode 100755
index 0000000..932501d
--- /dev/null
+++ b/test/verify/check-connection
@@ -0,0 +1,1446 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import base64
+import json
+import os
+import subprocess
+import time
+
+import testlib
+
+
+@testlib.skipDistroPackage()
+class TestConnection(testlib.MachineCase):
+ def setUp(self):
+ super().setUp()
+ self.ws_executable = f"{self.libexecdir}/cockpit-ws"
+
+ def ostree_setup_ws(self):
+ """Overlay cockpit-ws package on OSTree image
+
+ Disable the cockpit/ws container. This is for tests that don't work with the container,
+ and to make sure that overlaying cockpit-ws works as well.
+ """
+ m = self.machine
+ if not m.ostree_image:
+ return
+
+ # uninstall cockpit/ws container startup script
+ m.execute("rm /etc/systemd/system/cockpit.service")
+ # overlay cockpit-ws rpm
+ m.execute("rpm-ostree install --cache-only /var/tmp/cockpit-ws-*.rpm", timeout=180)
+ m.reboot()
+
+ def assertNoAdminProcessLeaks(self):
+ """Check that machine did not leak any bridges or ssh-agent admin processes"""
+ m = self.machine
+ try:
+ # there may still be user-wide ones like dbus-broker
+ m.execute("while pgrep -au admin '(cockpit|ssh-agent)'; do sleep 0.1; done", timeout=30)
+ except RuntimeError:
+ # show the leaked processes in the assertion
+ self.fail(m.execute("pgrep -au admin '(cockpit|ssh-agent)'"))
+
+ @testlib.skipBrowser("Firefox cannot work with cookies", "firefox")
+ def testBasic(self):
+ m = self.machine
+
+ # always test with the default ws install (container on OSTree, package everywhere else)
+ self.check_basic_with_start_stop(m.start_cockpit, m.stop_cockpit)
+
+ # on OSTree, also check with overlaid cockpit-ws rpm
+ if m.ostree_image:
+ def ws_start():
+ m.execute(r"""
+ mkdir -p /etc/systemd/system/cockpit.service.d/
+ printf "[Service]\nExecStart=\n%s --no-tls" `grep ExecStart= /lib/systemd/system/cockpit.service` \
+ > /etc/systemd/system/cockpit.service.d/notls.conf
+ systemctl daemon-reload
+ systemctl start cockpit.socket""")
+
+ def ws_stop():
+ m.execute("systemctl stop cockpit cockpit.socket")
+
+ self.ostree_setup_ws()
+ # HACK: Getting SELinux errors with just rpm-ostree install; there's a plethora of failures, so just allow them all
+ m.execute("setenforce 0")
+ self.allow_journal_messages('audit.*avc: denied .*')
+ self.check_basic_with_start_stop(ws_start, ws_stop)
+
+ def check_basic_with_start_stop(self, start_cockpit, stop_cockpit):
+ m = self.machine
+ b = self.browser
+ start_cockpit()
+
+ # take cockpit-ws down on the login page
+ b.open("/system")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ stop_cockpit()
+ b.click('#login-button')
+ b.wait_text_not('#login-fatal-message', "")
+ start_cockpit()
+ b.reload()
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.enter_page("/system")
+
+ # cookie should not be marked as secure, it's not https
+ cookie = b.cookie("cockpit")
+ self.assertTrue(cookie["httpOnly"])
+ self.assertEqual(cookie["sameSite"], "Strict")
+ self.assertFalse(cookie["secure"])
+
+ # take cockpit-ws down on the server page
+ stop_cockpit()
+ b.switch_to_top()
+ b.wait_in_text(".curtains-ct h1", "Disconnected")
+
+ start_cockpit()
+ b.click("#machine-reconnect")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+
+ # sever the connection on the login page
+ m.execute("iptables -w -I INPUT -p tcp --dport 9090 -j REJECT --reject-with tcp-reset")
+ b.click('#login-button')
+ b.wait_text_not('#login-fatal-message', "")
+ m.execute("iptables -w -D INPUT -p tcp --dport 9090 -j REJECT --reject-with tcp-reset")
+ b.reload()
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.enter_page("/system")
+
+ # sever the connection on the server page
+ m.execute("iptables -w -I INPUT -p tcp --dport 9090 -j REJECT")
+ b.switch_to_top()
+ with b.wait_timeout(60):
+ b.wait_visible(".curtains-ct")
+
+ b.wait_in_text(".curtains-ct h1", "Disconnected")
+ b.wait_in_text('.curtains-ct .pf-v5-c-empty-state__body', "Connection has timed out.")
+ m.execute("iptables -w -D INPUT -p tcp --dport 9090 -j REJECT")
+ b.click("#machine-reconnect")
+ b.enter_page("/system")
+ b.logout()
+
+ # deleted cookie after logout should not be marked as secure, it's not https
+ cookie = b.cookie("cockpit")
+ self.assertEqual(cookie["value"], "deleted")
+ self.assertTrue(cookie["httpOnly"])
+ self.assertFalse(cookie["secure"])
+
+ self.assertNoAdminProcessLeaks()
+
+ if not m.ostree_image: # cannot write to /usr on OSTree, and cockpit-session is in a container
+ # damage cockpit-session permissions, expect generic error message
+ m.execute(f"chmod g-x {self.libexecdir}/cockpit-session")
+ b.open("/system")
+ b.wait_in_text('#login-fatal-message', "Internal error in login process")
+ m.execute(f"chmod g+x {self.libexecdir}/cockpit-session")
+
+ self.allow_journal_messages(".*cockpit-session: bridge program failed.*")
+
+ # pretend cockpit-bridge is not installed, expect specific error message
+ m.execute("while B=$(command -v cockpit-bridge); do mv $B ${B}.disabled; done")
+ b.open("/system")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.wait_visible('#login-fatal-message')
+ b.wait_text('#login-fatal-message', "The cockpit package is not installed")
+ m.execute("while B=$(command -v cockpit-bridge.disabled); do mv $B ${B%.disabled}; done")
+
+ # Lets crash a systemd-controlled process and see if we get a proper backtrace in the logs
+ # This helps with debugging failures in the tests elsewhere
+ m.write("/run/systemd/system/systemd-hostnamed.service.d/core.conf",
+ "[Service]\nLimitCORE=infinity\n")
+ m.execute("""systemctl daemon-reload
+ systemctl restart systemd-hostnamed
+ pkill -e -SEGV systemd-hostnam""")
+ testlib.wait(lambda: m.execute("journalctl -b | grep 'Process.*systemd-hostnam.*of user.*dumped core.'"))
+
+ # Make sure the core dumps exist in the directory, so we can download them
+ cores = m.execute("find /var/lib/systemd/coredump -type f")
+ self.assertNotEqual(cores, "")
+
+ self.allow_core_dumps = True
+ self.allow_journal_messages(".*org.freedesktop.hostname1.*DBus.Error.NoReply.*")
+
+ @testlib.skipOstree("OSTree doesn't use systemd units")
+ @testlib.nondestructive
+ def testUnitLifecycle(self):
+ m = self.machine
+
+ def expect_active(unit, is_active):
+ status = m.execute(f"systemctl is-active {unit} || true").strip()
+ self.assertIn(status, ["active", "inactive"])
+ if is_active:
+ self.assertEqual(status, "active", f"{unit} is not active")
+ else:
+ self.assertEqual(status, "inactive", f"{unit} is active")
+
+ def expect_actives(ws_socket, instance_sockets, http_instances, https_instances=0):
+ expect_active("cockpit.socket", ws_socket)
+ # http instances
+ for instance in ["http"]:
+ expect_active(f"cockpit-wsinstance-{instance}.socket", instance_sockets)
+ expect_active(f"cockpit-wsinstance-{instance}.service", instance in http_instances)
+ # number of https instances
+ expect_active("cockpit-wsinstance-https-factory.socket", instance_sockets)
+ for _type in ["service", "socket"]:
+ out = m.execute(f"systemctl --no-legend -t {_type} list-units cockpit-wsinstance-https@*")
+ count = len(out.strip().splitlines())
+ self.assertEqual(count, https_instances, out)
+
+ # at the beginning, no cockpit related units are running
+ m.execute("systemctl reset-failed")
+ m.stop_cockpit()
+ expect_actives(ws_socket=False, instance_sockets=False, http_instances=[])
+
+ # http only mode
+
+ m.start_cockpit(tls=False)
+ expect_actives(ws_socket=True, instance_sockets=False, http_instances=[])
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --head http://127.0.0.1:9090"))
+
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"])
+ self.assertRaises(subprocess.CalledProcessError, m.execute,
+ "curl --silent https://127.0.0.1:9090")
+ # c-tls knows it can't do https, and not activate that instance
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"])
+
+ m.restart_cockpit()
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"])
+
+ m.stop_cockpit()
+ expect_actives(ws_socket=False, instance_sockets=False, http_instances=[])
+
+ # cleans up also when cockpit-tls crashes or idle-exits, not just by explicit stop request
+ m.start_cockpit(tls=False)
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --head http://127.0.0.1:9090"))
+ m.execute("pkill -e cockpit-tls")
+ expect_actives(ws_socket=True, instance_sockets=False, http_instances=[])
+
+ # and recovers from that
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --head http://127.0.0.1:9090"))
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"])
+
+ # https mode
+
+ m.start_cockpit(tls=True)
+ expect_actives(ws_socket=True, instance_sockets=False, http_instances=[], https_instances=0)
+
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --head http://127.0.0.1:9090"))
+
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"], https_instances=0)
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent -k --head https://127.0.0.1:9090"))
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"], https_instances=1)
+
+ m.restart_cockpit()
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"], https_instances=1)
+
+ m.stop_cockpit()
+ expect_actives(ws_socket=False, instance_sockets=False, http_instances=[], https_instances=0)
+
+ m.start_cockpit(tls=True)
+ expect_actives(ws_socket=True, instance_sockets=False, http_instances=[], https_instances=0)
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --head http://127.0.0.1:9090"))
+
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"], https_instances=0)
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent -k --head https://127.0.0.1:9090"))
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"], https_instances=1)
+
+ # cleans up also when cockpit-tls crashes or idle-exits, not just by explicit stop request
+ m.execute("pkill -e cockpit-tls")
+ expect_actives(ws_socket=True, instance_sockets=False, http_instances=[], https_instances=0)
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --head http://127.0.0.1:9090"))
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"], https_instances=0)
+ # next https request after crash doesn't leak an instance
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent -k --head https://127.0.0.1:9090"))
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"], https_instances=1)
+
+ # instance service+socket going away does not confuse cockpit-tls' bookkeeping
+ m.execute("systemctl stop cockpit-wsinstance-https@*.service cockpit-wsinstance-https@*.socket")
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"], https_instances=0)
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --show-error -k --head https://127.0.0.1:9090"))
+ expect_actives(ws_socket=True, instance_sockets=True, http_instances=["http"], https_instances=1)
+
+ # sockets are inaccessible to users, only to cockpit-tls
+ for s in ["http.sock", "https-factory.sock"]:
+ out = m.execute(f"su -c '! nc -U /run/cockpit/wsinstance/{s} 2>&1 || exit 1' admin")
+ self.assertIn("Permission denied", out)
+
+ @testlib.skipOstree("OSTree doesn't use systemd units")
+ @testlib.nondestructive
+ def testHttpsInstanceDoS(self):
+ m = self.machine
+ # prevent generating core dump artifacts
+ orig = m.execute("cat /proc/sys/kernel/core_pattern").strip()
+ m.execute("echo core > /proc/sys/kernel/core_pattern")
+ self.addCleanup(m.execute, f"echo '{orig}' > /proc/sys/kernel/core_pattern")
+ self.addCleanup(m.execute, "systemctl reset-failed")
+ m.start_cockpit(tls=True)
+
+ # some netcat versions need an explicit shutdown option, others default to shutting down and don't have -N
+ n_opt = "-N" if "-N" in m.execute("nc -h 2>&1") else ""
+
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent -k --head https://127.0.0.1:9090"))
+
+ # number of https instances is bounded (DoS prevention)
+ # with MaxTasks=200 und 2 threads per ws instance we should have a
+ # rough limit of 100 instances, so at some point curl should start failing
+ m.execute("su -s /bin/sh -c 'RC=1; for i in `seq 120`; do "
+ " echo -n $i | nc %s -U /run/cockpit/wsinstance/https-factory.sock;"
+ " curl --silent --head --max-time 5 --unix-socket /run/cockpit/wsinstance/https@$i.sock http://dummy > /dev/null || RC=0; "
+ "done; exit $RC' cockpit-ws" % n_opt)
+
+ for type_ in ["socket", "service"]:
+ active = int(m.execute("systemctl --no-legend list-units -t %s --state=active "
+ "'cockpit-wsinstance-https@*' | wc -l" % type_).strip())
+ self.assertGreater(active, 45)
+ self.assertLess(active, 110)
+ failed = int(m.execute("systemctl --no-legend list-units --state=failed 'cockpit-wsinstance-https@*' | wc -l").strip())
+ self.assertGreater(failed, 0)
+ self.assertLess(failed, 75) # services and sockets
+
+ self.allow_journal_messages(".*cockpit-ws.*dumped core.*")
+ self.allow_journal_messages(".*Error creating thread: Resource temporarily unavailable.*")
+
+ # initial instance still works
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --show-error -k --head https://127.0.0.1:9090"))
+
+ # can launch new instances after freeing up some old ones
+ m.execute("systemctl stop cockpit-wsinstance-https@30 cockpit-wsinstance-https@31 cockpit-wsinstance-https@32")
+ m.execute(f"echo -n new | nc {n_opt} -U /run/cockpit/wsinstance/https-factory.sock")
+ out = m.execute("curl --silent --show-error --head --unix-socket /run/cockpit/wsinstance/https@new.sock http://dummy")
+ self.assertIn("HTTP/1.1 200 OK", out)
+
+ @testlib.skipBrowser("Firefox needs proper cert and CA", "firefox")
+ @testlib.nondestructive
+ def testTls(self):
+ m = self.machine
+ b = self.browser
+
+ # Start Cockpit with TLS, force cert regeneration
+ m.execute("rm -f /etc/cockpit/ws-certs.d/*")
+ m.start_cockpit(tls=True)
+
+ # A normal TLS connection works
+ output = m.execute('openssl s_client -connect 172.27.0.15:9090 2>&1')
+ m.message(output)
+ self.assertIn("DONE", output)
+
+ # has proper keyUsage and SAN (both with sscg and with self-signed)
+ output = m.execute("openssl s_client -showcerts -connect 172.27.0.15:9090 |"
+ "openssl x509 -noout -ext keyUsage,extendedKeyUsage,subjectAltName")
+ # keyUsage
+ self.assertIn("Digital Signature", output)
+ self.assertIn("Key Encipherment", output)
+ # extendedKeyUsage
+ self.assertIn("TLS Web Server Authentication", output)
+ # SAN
+ self.assertIn("IP Address:127.0.0.1", output)
+ self.assertIn("DNS:localhost", output)
+
+ # SSLv3 should not work
+ output = m.execute('openssl s_client -connect 172.27.0.15:9090 -ssl3 2>&1 || true')
+ self.assertNotIn("DONE", output)
+
+ # Some operating systems fail SSL3 on the server side
+ self.assertRegex(output, "Secure Renegotiation IS NOT supported|"
+ "ssl handshake failure|"
+ "[uU]nknown option.* -ssl3|"
+ "null ssl method passed|"
+ "wrong version number")
+
+ # RC4 should not work
+ output = m.execute('! openssl s_client -connect 172.27.0.15:9090 -tls1_2 -cipher RC4 2>&1')
+ self.assertNotIn("DONE", output)
+ self.assertRegex(
+ output, r"no cipher match|no ciphers available|ssl handshake failure|Cipher is \(NONE\)")
+
+ # get along with read-only config directory, as long as certificate exists
+ # this does not work on coreos as the user/group IDs are not mapped correctly
+ if not m.ostree_image:
+ m.stop_cockpit()
+ try:
+ m.execute("mount -o bind -r /etc/cockpit /etc/cockpit")
+ m.start_cockpit(tls=True)
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl -k --head https://127.0.0.1:9090"))
+ finally:
+ m.execute("umount /etc/cockpit")
+
+ # Install a certificate chain
+ m.upload(["verify/files/cert-chain.cert", "verify/files/cert-chain.key"], "/etc/cockpit/ws-certs.d")
+
+ def check_cert_chain():
+ # This should also reset the file context
+ m.restart_cockpit()
+ output = m.execute('openssl s_client -connect 172.27.0.15:9090 2>&1')
+ self.assertIn("DONE", output)
+ self.assertRegex(output, "s:/?CN *= *localhost")
+ self.assertRegex(output, "1 s:/?OU *= *Intermediate")
+
+ check_cert_chain()
+
+ # *.crt file also works
+ m.execute("mv /etc/cockpit/ws-certs.d/cert-chain.cert /etc/cockpit/ws-certs.d/cert-chain.crt")
+ check_cert_chain()
+
+ # backwards compat: merged cert+key file also still works with cockpit-tls (but not any more with cockpit-ws/container)
+ if not m.ostree_image:
+ m.execute("""cat /etc/cockpit/ws-certs.d/cert-chain.key >> /etc/cockpit/ws-certs.d/cert-chain.crt
+ chmod 640 /etc/cockpit/ws-certs.d/cert-chain.crt
+ chown root:cockpit-ws /etc/cockpit/ws-certs.d/cert-chain.crt
+ rm /etc/cockpit/ws-certs.d/cert-chain.key""")
+ check_cert_chain()
+
+ # certmonger generated certificate; asciibetically later than the above
+ # not all images have certmonger
+ if m.image not in ["debian-stable", "debian-testing", "fedora-coreos", "rhel4edge", "arch"]:
+ hostname = m.execute("hostname --fqdn").strip()
+ m.execute(f"getcert request -f /etc/cockpit/ws-certs.d/monger.cert -k /etc/cockpit/ws-certs.d/monger.key -D {hostname} --ca=local --wait")
+ self.addCleanup(m.execute, "getcert stop-tracking -f /etc/cockpit/ws-certs.d/monger.cert")
+ # cert generation succeeded, and it is being tracked
+ self.assertIn("MONITORING", m.execute("getcert list"))
+ self.assertIn("/etc/cockpit/ws-certs.d/monger.cert",
+ m.execute(f"{self.libexecdir}/cockpit-certificate-ensure --check"))
+ m.restart_cockpit()
+ output = m.execute('openssl s_client -connect 172.27.0.15:9090 2>&1')
+ self.assertIn("DONE", output)
+ self.assertRegex(output, f"s:/?CN *= {hostname}")
+ self.assertRegex(output, "i:/?CN *= Local Signing Authority.*")
+
+ # login handler: correct password
+ m.execute("curl -k -c cockpit.jar -s --head --header 'Authorization: Basic {}' https://127.0.0.1:9090/cockpit/login".format(
+ base64.b64encode(b"admin:foobar").decode(), ))
+ headers = m.execute("curl -k --head -b cockpit.jar -s https://127.0.0.1:9090/")
+
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src
+ #
+ # Note: connect-src 'self' does not resolve to websocket
+ # schemes in all browsers, more info in this issue.
+ #
+ # https://github.com/w3c/webappsec-csp/issues/7
+ #
+ # Make sure we send the explicit `wss://` item until we're absolutely
+ # sure about the browser support situation for `connect-src 'self';`.
+ self.assertIn("wss://127.0.0.1:9090", headers)
+
+ if self.is_pybridge():
+ # We want to make sure we're *not* sending any CORS headers.
+ CORS_HEADERS = [
+ # https://en.wikipedia.org/wiki/Cross-origin_resource_sharing#Response_headers
+ 'Access-Control-Allow-Credentials',
+ 'Access-Control-Expose-Headers',
+ 'Access-Control-Max-Age',
+ 'Access-Control-Allow-Methods',
+ 'Access-Control-Allow-Headers',
+ ]
+ for cors_header in CORS_HEADERS:
+ self.assertNotIn(cors_header, headers)
+ else:
+ self.assertIn("Access-Control-Allow-Origin: https://127.0.0.1:9090", headers)
+
+ # CORP and Frame-Options are also set for dynamic paths
+ self.assertIn("Cross-Origin-Resource-Policy: same-origin", headers)
+ self.assertIn("X-Frame-Options: sameorigin", headers)
+
+ self.allow_journal_messages(
+ ".*Peer failed to perform TLS handshake",
+ ".*Peer sent fatal TLS alert:.*",
+ ".*invalid base64 data in Basic header",
+ "Received unexpected TLS connection and no certificate was configured",
+ ".*Error performing TLS handshake: No supported cipher suites have been found.",
+ ".*Error performing TLS handshake: Could not negotiate a supported cipher suite.")
+
+ # check the Debian smoke test
+ m.upload(["../tools/debian/tests/smoke"], "/tmp")
+ m.execute("/tmp/smoke")
+
+ b.ignore_ssl_certificate_errors(ignore=True)
+ self.login_and_go("/system", tls=True)
+ cookie = b.cookie("cockpit")
+ # cookie should be marked as secure
+ self.assertTrue(cookie["httpOnly"])
+ self.assertTrue(cookie["secure"])
+ self.assertEqual(cookie["sameSite"], "Strict")
+ # same after logout
+ b.logout()
+ cookie = b.cookie("cockpit")
+ self.assertEqual(cookie["value"], "deleted")
+ self.assertTrue(cookie["httpOnly"])
+ self.assertTrue(cookie["secure"])
+ self.assertEqual(cookie["sameSite"], "Strict")
+
+ # http on localhost should not redirect to https
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --head http://127.0.0.1:9090"))
+ # http on other IP should redirect to https
+ output = m.execute("curl --head http://172.27.0.15:9090")
+ self.assertIn("HTTP/1.1 301 Moved Permanently", output)
+ self.assertIn("Location: https://172.27.0.15:9090/", output)
+ # enable AllowUnencrypted, this disables redirect
+ m.write("/etc/cockpit/cockpit.conf", "[WebService]\nAllowUnencrypted=true")
+ m.restart_cockpit()
+ # now it should not redirect
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --head http://127.0.0.1:9090"))
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --head http://172.27.0.15:9090"))
+
+ @testlib.nondestructive
+ def testConfigOrigins(self):
+ m = self.machine
+ m.write("/etc/cockpit/cockpit.conf", "[WebService]\nOrigins = http://other-origin:9090 http://localhost:9090")
+ m.start_cockpit()
+ headers = {
+ 'Connection': 'Upgrade',
+ 'Upgrade': 'websocket',
+ 'Host': 'localhost:9090',
+ 'Origin': 'http://other-origin:9090',
+ 'Sec-Websocket-Key': '3sc2c9IzwRUc3BlSIYwtSA==',
+ 'Sec-Websocket-Version': 13
+ }
+ output = m.curl('-f', '-N', 'http://localhost:9090/cockpit/socket', headers=headers)
+ self.assertIn('"no-session"', output)
+
+ # The socket should also answer at /socket
+ output = m.curl('-f', '-N', 'http://localhost:9090/socket', headers=headers)
+ self.assertIn('"no-session"', output)
+
+ self.allow_journal_messages('peer did not close io when expected')
+
+ @testlib.skipOstree("OSTree doesn't have cockpit-ws")
+ @testlib.nondestructive
+ def test100YearsCert(self):
+ m = self.machine
+
+ selfsign = '/etc/cockpit/ws-certs.d/0-self-signed.cert'
+ helper = f'{self.libexecdir}/cockpit-certificate-helper'
+ ensure = f'{self.libexecdir}/cockpit-certificate-ensure'
+
+ # Ensure things are as we expect them to be
+ m.execute('rm -f /etc/cockpit/ws-certs.d/*')
+ m.execute(f'grep DAYS=395 {helper}')
+
+ # Generate a 100 years expiry certificate
+ self.sed_file('s/DAYS=395/DAYS=36500/', helper)
+ m.execute(f'grep DAYS=36500 {helper}') # double-check
+ m.execute(ensure)
+
+ # Verify the expiry date to be in the far-future. This is a bit
+ # annoying due to the date format OpenSSL uses (which Python can't
+ # trivially parse) and the question of locales
+ expires = m.execute(f'date -d "$(openssl x509 -enddate -noout < {selfsign} | cut -f2 -d=)" +%s')
+ self.assertGreater(int(expires), time.time() + 99 * 365 * 24 * 60 * 60)
+
+ # Put things back: avoid problematic multiple invocations of .sed_file()
+ m.execute(f'sed -i s/DAYS=36500/DAYS=395/ {helper}')
+ m.execute(f'grep DAYS=395 {helper}') # double-check
+
+ # Run ensure again and make sure we get a new certificate
+ m.execute('touch /tmp/timestamp')
+ m.execute(ensure)
+ m.execute(f'test {selfsign} -nt /tmp/timestamp')
+ m.execute('rm /tmp/timestamp')
+
+ # Check that the expiry is less than 420 days
+ # See https://github.com/sgallagher/sscg/pull/28 for why 420
+ expires = m.execute(f'date -d "$(openssl x509 -enddate -noout < {selfsign} | cut -f2 -d=)" +%s')
+ self.assertLess(int(expires), time.time() + 420 * 24 * 60 * 60)
+
+ # Run ensure again and make sure we *don't* get a new certificate
+ m.execute('touch /tmp/timestamp')
+ m.execute(ensure)
+ m.execute(f'test ! {selfsign} -nt /tmp/timestamp')
+ m.execute('rm /tmp/timestamp')
+
+ @testlib.skipOstree("OSTree doesn't use systemd units")
+ @testlib.nondestructive
+ def testSocket(self):
+ m = self.machine
+
+ # non-admin user
+ m.execute("useradd user")
+
+ # enable no-password login for 'admin' and 'user'
+ m.execute("passwd -d admin")
+ m.execute("passwd -d user")
+
+ self.sed_file('$ a\\\nPermitEmptyPasswords yes', '/etc/ssh/sshd_config',
+ 'systemctl restart sshd.service')
+
+ def assertInOrNot(string, result, expected):
+ if expected:
+ self.assertIn(string, result)
+ else:
+ self.assertNotIn(string, result)
+
+ def checkMotdForUser(string, user, expected):
+ result = m.execute(f"ssh -o StrictHostKeyChecking=no -n {user}@localhost")
+ assertInOrNot(string, result, expected)
+
+ def checkMotdContent(string, expected=True):
+ # Needs https://github.com/linux-pam/linux-pam/pull/292 (or PAM 1.5.0)
+ old_pam = m.image.startswith('rhel-8-') or m.image in ['centos-8-stream', 'ubuntu-2204']
+
+ # check issue (should be exactly the same as motd)
+ assertInOrNot(string, m.execute("cat /etc/issue.d/cockpit.issue"), expected)
+
+ # check motd as 'root' (via cat) and 'admin' and 'user' (via ssh)
+ assertInOrNot(string, m.execute("cat /etc/motd.d/cockpit"), expected)
+ checkMotdForUser(string, expected=expected, user='admin')
+ checkMotdForUser(string, expected=old_pam and expected or False, user='user')
+
+ m.stop_cockpit()
+ checkMotdContent('systemctl')
+ checkMotdContent(':9090/', expected=False)
+ m.start_cockpit()
+
+ checkMotdContent(':9090/')
+ checkMotdContent('systemctl', expected=False)
+ m.execute("systemctl stop cockpit.socket")
+
+ # Change port according to documentation: https://cockpit-project.org/guide/latest/listen.html
+ m.execute('! selinuxenabled || semanage port -m -t websm_port_t -p tcp 443')
+ self.write_file("/etc/systemd/system/cockpit.socket.d/listen.conf",
+ "[Socket]\nListenStream=\nListenStream=/run/cockpit/sock\nListenStream=443",
+ post_restore_action="systemctl stop cockpit.socket; systemctl daemon-reload")
+
+ checkMotdContent('systemctl')
+ checkMotdContent(':9090/', expected=False)
+ checkMotdContent(':443/', expected=False)
+ m.start_cockpit(tls=True)
+
+ checkMotdContent('systemctl', expected=False)
+ checkMotdContent(':9090/', expected=False)
+ checkMotdContent(':443/')
+
+ output = m.execute('curl -k https://localhost 2>&1 || true')
+ self.assertIn('Loading...', output)
+ output = m.execute('curl -k --unix-socket /run/cockpit/sock https://dummy 2>&1 || true')
+ self.assertIn('Loading...', output)
+
+ output = m.execute('curl -k https://localhost:9090 2>/dev/null || echo $?')
+ self.assertIn('7', output.strip())
+
+ self.allow_journal_messages(".*Peer failed to perform TLS handshake")
+
+ @testlib.skipOstree("Can't remove/upgrade packages on OSTree")
+ def testWsPackage(self):
+ m = self.machine
+
+ # On RHEL-8 SSH allows password root login by default, so Cockpit does too.
+ ROOT_LOGIN_ENABLED = ['rhel-8', 'centos-8']
+ root_login_disallowed = not any(m.image.startswith(img) for img in ROOT_LOGIN_ENABLED)
+
+ if m.image.startswith("debian") or m.image.startswith("ubuntu"):
+ # clean up debug symbols, they get in the way of upgrading
+ m.execute("dpkg --purge cockpit-ws-dbgsym")
+ elif m.image.startswith("rhel"):
+ # subscription-manager-cockpit depends on cockpit-ws, and cockpit.rpm metapackage depends on sub-man on RHEL
+ m.execute("rpm --erase cockpit subscription-manager-cockpit")
+
+ def install():
+ if m.image.startswith("debian") or m.image.startswith("ubuntu"):
+ m.execute("dpkg --install /var/tmp/build/cockpit-ws_*.deb")
+ elif m.image == "arch":
+ m.execute("pacman -U --noconfirm /var/tmp/build/cockpit-*.pkg.tar.zst")
+ else:
+ m.execute("if rpm -q cockpit-ws; then rpm --verify cockpit-ws; fi")
+ m.execute("rpm --upgrade --force /var/tmp/build/cockpit-ws-*.rpm")
+ m.execute("rpm --verify cockpit-ws")
+
+ def remove():
+ if m.image.startswith("debian") or m.image.startswith("ubuntu"):
+ m.execute("dpkg --purge cockpit cockpit-ws")
+ elif m.image == "arch":
+ m.execute("pacman -Rdd --noconfirm cockpit")
+ elif m.image.startswith("rhel"):
+ m.execute("rpm --erase cockpit-ws")
+ else:
+ m.execute("rpm --erase cockpit cockpit-ws")
+
+ # clean install sets up dynamic motd/issue symlink and pam disallowed users.
+ self.assertIn('Activate the web console', m.execute("cat /etc/motd.d/cockpit"))
+ self.assertIn('Activate the web console', m.execute("cat /etc/issue.d/cockpit.issue"))
+ if root_login_disallowed:
+ self.assertIn('root', m.execute("cat /etc/cockpit/disallowed-users"))
+ # remove disallowed-users to simulate an upgrade where we did not have this file yet.
+ m.execute("rm /etc/cockpit/disallowed-users")
+
+ # package upgrade keeps them
+ install()
+ self.assertIn('Activate the web console', m.execute("cat /etc/motd.d/cockpit"))
+ self.assertIn('Activate the web console', m.execute("cat /etc/issue.d/cockpit.issue"))
+ # disallowed-users should not exists now as this is an upgrade, except on Arch
+ if m.image != "arch":
+ m.execute("test ! -e /etc/cockpit/disallowed-users")
+ m.execute("echo 'root' > /etc/cockpit/disallowed-users")
+ else:
+ m.execute("test -e /etc/cockpit/disallowed-users")
+
+ # HACK: On Arch Linux the symlink is overwritten, bug?
+ if m.image != "arch":
+ # manual change/removal is respected on upgrade
+ m.execute("ln -sf /dev/null /etc/motd.d/cockpit; rm /etc/issue.d/cockpit.issue")
+ install()
+ self.assertEqual(m.execute("readlink /etc/motd.d/cockpit").strip(), "/dev/null")
+ m.execute("test ! -e /etc/issue.d/cockpit.issue")
+
+ # removing the package cleans up the links
+ remove()
+ m.execute("test ! -e /etc/motd.d/cockpit")
+ m.execute("test ! -e /etc/issue.d/cockpit.issue")
+ m.execute("test ! -e /etc/cockpit/disallowed-users")
+
+ # fresh install (most of our test images have cockpit-ws preinstalled, so the first test above does not cover that)
+ install()
+ # verify that we installed relative links in the new package
+ self.assertEqual(m.execute("readlink /etc/motd.d/cockpit").strip(), "../../run/cockpit/motd")
+ self.assertEqual(m.execute("readlink /etc/issue.d/cockpit.issue").strip(), "../../run/cockpit/motd")
+ if root_login_disallowed:
+ self.assertIn('root', m.execute("cat /etc/cockpit/disallowed-users"))
+
+ @testlib.skipOstree("OSTree doesn't have cockpit-ws")
+ @testlib.nondestructive
+ def testCommandline(self):
+ m = self.machine
+
+ # Large requests are processed correctly with plain HTTP through cockpit-tls
+ m.start_cockpit(tls=True)
+ large_headers = {'Authorization': f'Negotiate {1:07000}'}
+ self.assertIn('id="login"', m.curl('http://localhost:9090/', headers=large_headers))
+
+ # Large requests are processed correctly with TLS through cockpit-tls
+ self.assertIn('id="login"', m.curl('-k', 'https://localhost:9090/', headers=large_headers))
+ m.stop_cockpit()
+
+ m.execute("rm -f /etc/cockpit/ws-certs.d/* /etc/cockpit/cockpit.conf")
+ m.write("/etc/cockpit/cockpit.conf", "[WebService]\nLoginTitle = A Custom Title\n")
+
+ m.execute(f"{self.libexecdir}/cockpit-certificate-ensure")
+ self.assertTrue(m.execute("ls /etc/cockpit/ws-certs.d/*"))
+
+ pid = m.spawn(f"{self.ws_executable} --port 9000 --address 127.0.0.1", "cockpit-ws.log")
+ self.addCleanup(m.execute, f"kill {pid}")
+
+ # The port may not be available immediately, so wait for it
+ testlib.wait(lambda: 'A Custom Title' in m.curl('-k', 'https://localhost:9000/'))
+
+ output = m.execute('curl -s -S -k https://172.27.0.15:9000/ 2>&1 || echo $?')
+ self.assertIn('7', output.strip())
+
+ # Large requests are processed correctly with plain HTTP
+ self.assertIn('A Custom Title', m.curl('http://localhost:9000/', headers=large_headers))
+
+ # Large requests are processed correctly with TLS
+ self.assertIn('A Custom Title', m.curl('-k', 'https://localhost:9000/', headers=large_headers))
+
+ # unsupported HTTP method
+ self.assertIn("HTTP/1.1 405 Method Not Allowed",
+ m.execute('curl -k --verbose -X PATCH https://localhost:9000/ 2>&1'))
+
+ # no body with HEAD request
+ out = m.execute('curl -k --head https://localhost:9000/')
+ self.assertIn("HTTP/1.1 200", out)
+ self.assertNotIn("<html>", out)
+
+ @testlib.nondestructive
+ def testHeadRequest(self):
+ m = self.machine
+ m.start_cockpit()
+
+ # static handler
+ headers = m.execute("curl -s --head http://172.27.0.15:9090/cockpit/static/login.html")
+ self.assertIn("HTTP/1.1 200 OK\r\n", headers)
+ self.assertIn("Content-Type: text/html\r\n", headers)
+ self.assertIn("Cross-Origin-Resource-Policy: same-origin\r\n", headers)
+ self.assertIn("X-Frame-Options: sameorigin\r\n", headers)
+ # login.html is not always accessible as a file (e.g. on CoreOS), so just assert a reasonable content length
+ self.assertIn("Content-Length: ", headers)
+ length = int(headers.split('Content-Length: ', 1)[1].split()[0])
+ self.assertGreater(length, 5000)
+ self.assertLess(length, 100000)
+
+ # login handler: wrong password
+ headers = m.execute("curl -s --head --header 'Authorization: Basic {}' http://172.27.0.15:9090/cockpit/login".format(
+ base64.b64encode(b"admin:hahawrong").decode()))
+ self.assertRegex(headers, r"HTTP/1.1 (401 Authentication failed|403 Permission denied)\r\n")
+ self.assertNotIn("Set-Cookie:", headers)
+
+ # login handler: correct password
+ headers = m.execute("curl -s --head --header 'Authorization: Basic {}' http://172.27.0.15:9090/cockpit/login".format(
+ base64.b64encode(b"admin:foobar").decode()))
+ self.assertIn("HTTP/1.1 200 OK\r\n", headers)
+ self.assertIn("Set-Cookie: cockpit", headers)
+
+ # socket handler; this should refuse HEAD (as it makes little sense on sockets), so 404
+ headers = m.execute("curl -s --head http://172.27.0.15:9090/cockpit/socket")
+ self.assertIn("HTTP/1.1 404 Not Found\r\n", headers)
+
+ # external channel handler; unauthenticated, thus 404
+ headers = m.execute("curl -s --head http://172.27.0.15:9090/cockpit+123/channel/foo")
+ self.assertIn("HTTP/1.1 404 Not Found\r\n", headers)
+
+ @testlib.skipOstree("ssh root login not allowed")
+ @testlib.nondestructive
+ def testFlowControlDownload(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/playground/speed", user="root", enable_root_login=True)
+ b.wait_text_not("#pid", "")
+ pid = b.text("#pid")
+
+ b.set_val("#read-path", "/dev/vda")
+ b.click("#read-sideband")
+
+ b.wait_text_not("#speed", "")
+ time.sleep(20)
+ output = m.execute(f"cat /proc/{pid}/statm")
+ rss = int(output.split(" ")[0])
+
+ # This fails when flow control is not present
+ self.assertLess(rss, 250000)
+
+ @testlib.nondestructive
+ @testlib.skipImage("Broken with C bridge", "centos-8-stream", "rhel-8*")
+ def testFlowControlUpload(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/playground/speed")
+ self.addCleanup(m.execute, "rm -f /tmp/spawninput")
+ b.click("#spawn-input")
+
+ b.wait_text_not("#speed", "")
+ with b.wait_timeout(60):
+ b.wait_text("#spawn-input-result", "success")
+
+ self.assertEqual(int(m.execute("stat -c %s /tmp/spawninput")), 65536000)
+ self.assertEqual(m.execute("tail -c10 /tmp/spawninput").strip(), "a" * 10)
+
+ @testlib.skipOstree("OSTree doesn't have cockpit-ws")
+ @testlib.nondestructive
+ def testLocalSession(self):
+ m = self.machine
+
+ # start ws with --local-session, let it spawn bridge; ensure that this works without /etc/cockpit/
+ m.spawn("su - -c 'G_MESSAGES_DEBUG=all XDG_CONFIG_DIRS=/usr/local %s -p 9999 -a 127.0.0.90 "
+ "--local-session=cockpit-bridge' admin" % self.ws_executable,
+ "cockpit-ws-local")
+ m.wait_for_cockpit_running('127.0.0.90', 9999)
+ # System frame should work directly, no login page
+ out = m.execute("curl --compressed http://127.0.0.90:9999/cockpit/@localhost/system/index.html")
+ self.assertIn('id="overview"', out)
+
+ # shut it down, wait until it is gone
+ m.execute("pkill cockpit-ws; while pgrep -a cockpit-ws; do sleep 1; done")
+
+ # start ws with --local-session and existing running bridge
+ self.write_file("/tmp/local.sh", f"""#!/bin/bash -eu
+coproc env G_MESSAGES_DEBUG=all cockpit-bridge
+export G_MESSAGES_DEBUG=all
+export XDG_CONFIG_DIRS=/usr/local
+{self.ws_executable} -p 9999 -a 127.0.0.90 --local-session=- <&${{COPROC[0]}} >&${{COPROC[1]}}
+""")
+ m.execute("chmod a+x /tmp/local.sh")
+ m.spawn("su - -c /tmp/local.sh admin", "local.sh")
+ m.wait_for_cockpit_running('127.0.0.90', 9999)
+
+ # System frame should work directly, no login page
+ out = m.execute("curl --compressed http://127.0.0.90:9999/cockpit/@localhost/system/index.html")
+ self.assertIn('id="overview"', out)
+
+ # shut it down, wait until it is gone
+ m.execute("pkill cockpit-ws; while pgrep -a cockpit-ws; do sleep 1; done")
+
+ self.allow_journal_messages("couldn't register polkit authentication agent.*")
+
+ @testlib.skipOstree("OSTree doesn't have cockpit-ws")
+ @testlib.skipImage("Kernel does not allow user namespaces", "debian-*")
+ @testlib.nondestructive
+ def testCockpitDesktop(self):
+ m = self.machine
+ m.stop_cockpit()
+
+ cases = [(['/cockpit/@localhost/system/index.html', 'system', 'system/index', 'system/'],
+ ['id="overview"']
+ ),
+ (['/cockpit/@localhost/network/firewall.html', 'network/firewall'],
+ ['div id="firewall"', 'script src="firewall.js"']
+ ),
+ (['/cockpit/@localhost/playground/react-patterns.html', 'playground/react-patterns'],
+ ['script src="react-patterns.js"']
+ ),
+ # no ssh host
+ (['/cockpit/@localhost/manifests.json'],
+ ['"system"', '"Overview"']
+ ),
+ # remote ssh host
+ (['/cockpit/@localhost/manifests.json test1@localhost'],
+ ['"system"', '"Overview"', '"HACK"']
+ )
+ ]
+
+ # prepare fake ssh target; to verify that we really use that, fake dashboard manifest
+ m.execute("""useradd test1
+ [ -f ~admin/.ssh/id_rsa ] || su -c "ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa" admin
+ mkdir -p ~test1/.ssh ~test1/.local/share/cockpit/dashboard
+ echo '{ "version": "42", "dashboard": { "index": { "label": "HACK" } } }' > ~test1/.local/share/cockpit/dashboard/manifest.json
+ cp ~admin/.ssh/id_rsa.pub ~test1/.ssh/authorized_keys
+ ssh-keyscan localhost >> ~admin/.ssh/known_hosts
+ chown admin:admin ~admin/.ssh/known_hosts
+ chown -R test1:test1 ~test1
+ su -c "ssh test1@localhost cockpit-bridge --packages" admin | grep -q test1.*dashboard # validate setup
+ """)
+
+ is_pybridge = self.is_pybridge()
+
+ for (pages, asserts) in cases:
+ for page in pages:
+ m.execute(f"""su - -c 'BROWSER="curl --silent --compressed -o /tmp/out.html" {self.libexecdir}/cockpit-desktop {page}' admin""",
+ timeout=10)
+
+ out = m.execute("cat /tmp/out.html")
+ for a in asserts:
+ self.assertIn(a, out)
+
+ if is_pybridge:
+ # FIXME: the C bridge leaks ssh-agent
+ self.assertNoAdminProcessLeaks()
+
+ # cockpit-desktop can start a privileged bridge through polkit
+ # we don't have an agent, so just allow the privilege without interactive authentication
+ if m.image == "arch":
+ self.write_file("/etc/polkit-1/rules.d/test.rules", r"""
+polkit.addRule(function(action, subject) {
+ if (action.id == "org.cockpit-project.cockpit.root-bridge" && subject.user == "admin")
+ return polkit.Result.YES;
+}); """)
+ else:
+ self.write_file("/etc/polkit-1/localauthority/50-local.d/test.pkla", r"""
+[Testing without an agent]
+Identity=unix-user:admin
+Action=org.cockpit-project.cockpit.root-bridge
+ResultAny=yes
+ResultInactive=yes
+ResultActive=yes""")
+
+ self.write_file("/tmp/browser.sh", """#!/bin/sh -e
+curl --silent --compressed -o /tmp/out.html "$@"
+# wait until privileged bridge starts
+until pgrep -f '^(/usr/[^ ]+/[^ /]*python[^ /]* )?/usr/bin/cockpit-bridge'; do sleep 1; done
+""")
+ m.execute("chmod 755 /tmp/browser.sh")
+ m.execute(f"su - -c 'BROWSER=/tmp/browser.sh {self.libexecdir}/cockpit-desktop system' admin", timeout=10)
+ self.assertIn('id="overview"', m.execute("cat /tmp/out.html"))
+
+ if is_pybridge:
+ # FIXME: the C bridge leaks ssh-agent
+ self.assertNoAdminProcessLeaks()
+
+ self.allow_journal_messages("couldn't register polkit authentication agent.*")
+ self.allow_journal_messages("Refusing to render service to dead parents.")
+ self.allow_journal_messages(".*No authentication agent found.*")
+ self.allow_journal_messages(".*Peer failed to perform TLS handshake.*")
+ self.allow_journal_messages(r".*cannot reauthorize identity\(s\): unix-user:.*")
+ self.allow_journal_messages("admin: Executing command .*COMMAND=.*cockpit-bridge --privileged.*")
+
+ @testlib.skipBrowser("Firefox needs proper cert and CA", "firefox")
+ def testReverseProxy(self):
+ m = self.machine
+ b = self.browser
+
+ self.ostree_setup_ws()
+
+ # set up a poor man's reverse TLS proxy with socat
+ m.upload(["../src/bridge/mock-server.crt", "../src/bridge/mock-server.key"], "/tmp")
+ m.spawn("socat OPENSSL-LISTEN:9090,reuseaddr,fork,cert=/tmp/mock-server.crt,"
+ "key=/tmp/mock-server.key,verify=0 TCP:localhost:9099",
+ "socat-tls.log")
+
+ # and another proxy for plain http
+ m.spawn("socat TCP-LISTEN:9091,reuseaddr,fork TCP:localhost:9099", "socat.log")
+
+ # ws with plain --no-tls should fail after login with mismatching Origin (expected http, got https)
+ m.spawn(f"su -s /bin/sh -c '{self.ws_executable} --no-tls -p 9099' cockpit-wsinstance",
+ "ws-notls.log")
+ m.wait_for_cockpit_running(tls=True)
+
+ b.ignore_ssl_certificate_errors(ignore=True)
+ b.open(f"https://{b.address}:{b.port}/system")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click('#login-button')
+
+ def check_wss_log():
+ for log in self.browser.get_js_log():
+ if 'Error during WebSocket handshake: Unexpected response code: 403' in log:
+ return True
+ return False
+ testlib.wait(check_wss_log)
+
+ testlib.wait(lambda: m.execute("grep 'received request from bad Origin' /var/log/ws-notls.log"))
+
+ # sanity check: unencrypted http through SSL proxy does not work
+ m.execute("! curl http://localhost:9090")
+
+ # does not redirect to https (through plain http proxy)
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --head http://127.0.0.1:9091"))
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --head http://172.27.0.15:9091"))
+
+ m.execute("pkill -e cockpit-ws; while pgrep -a cockpit-ws; do sleep 1; done")
+ # this page failure is reeally noisy
+ self.allow_journal_messages(".*No authentication agent found.*")
+ self.allow_journal_messages("couldn't register polkit authentication agent.*")
+ self.allow_journal_messages("received request from bad Origin.*")
+ self.allow_journal_messages(".*invalid handshake.*")
+ self.allow_browser_errors(".*received unsupported version in init message.*")
+ self.allow_browser_errors(".*received message before init.*")
+ self.allow_browser_errors("Error reading machine id")
+
+ # ws with --for-tls-proxy accepts only https origins, thus should work
+ m.spawn(f"su -s /bin/sh -c '{self.ws_executable} --for-tls-proxy -p 9099 -a 127.0.0.1' cockpit-wsinstance",
+ "ws-fortlsproxy.log")
+ m.wait_for_cockpit_running(tls=True)
+ b.open(f"https://{b.address}:{b.port}/system")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.wait_visible('#content')
+ b.enter_page("/system")
+ # cookie should be marked as secure, as for the browser it's https
+ cookie = b.cookie("cockpit")
+ self.assertTrue(cookie["httpOnly"])
+ self.assertTrue(cookie["secure"])
+ b.logout()
+ # deleted cookie after logout should be marked as secure
+ cookie = b.cookie("cockpit")
+ self.assertEqual(cookie["value"], "deleted")
+ self.assertTrue(cookie["httpOnly"])
+ self.assertTrue(cookie["secure"])
+
+ # should have https:// URLs in Content-Security-Policy
+ out = m.execute("curl --insecure --head https://localhost:9090/")
+ self.assertIn("Content-Security-Policy: connect-src 'self' https://localhost:9090 wss://localhost:9090;", out)
+
+ # sanity check: does not redirect to https (through plain http proxy) -- this isn't a supported mode, though!
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --head http://127.0.0.1:9091"))
+ self.assertIn("HTTP/1.1 200 OK", m.execute("curl --silent --head http://172.27.0.15:9091"))
+
+ @testlib.nondestructive
+ def testCaCert(self):
+ m = self.machine
+
+ # force cert regeneration
+ m.execute("systemctl stop cockpit; rm -f /etc/cockpit/ws-certs.d/*")
+ m.start_cockpit()
+ if not m.ostree_image:
+ # Really start Cockpit to make sure it has generated all its certificates.
+ m.execute("systemctl start cockpit")
+
+ # Start without a CA certificate.
+ self.addCleanup(m.execute, "rm -f /etc/cockpit/ws-certs.d/0-self-signed-ca.pem")
+ m.execute("rm -f /etc/cockpit/ws-certs.d/0-self-signed-ca.pem")
+ m.execute("! curl -sfS http://localhost:9090/ca.cer")
+
+ # Now make one up and check that is is served.
+ m.write("/etc/cockpit/ws-certs.d/0-self-signed-ca.pem", "FAKE CERT FOR TESTING\n")
+ self.assertEqual(m.execute("curl -sfS http://localhost:9090/ca.cer"), "FAKE CERT FOR TESTING\n")
+
+ @testlib.nondestructive
+ def test_branding(self):
+ m = self.machine
+ m.start_cockpit()
+
+ # for all of our CI images, the part before the dash is the name of the
+ # subdirectory in /usr/share/cockpit/branding.
+ brand = m.image.split('-')[0]
+ branddir = f'/usr/share/cockpit/branding/{brand}'
+
+ # We don't have access to the stuff on the filesystem if it's living in
+ # the container.
+ if not m.ostree_image:
+ # make sure that there are no broken links in "our" directory
+ self.assertEqual(m.execute(f'find {branddir} -xtype l'), '')
+
+ # branding.css undergoes variable substitution based on the content of
+ # /usr/lib/os-release. Perform the substitution for ourselves for
+ # validation. envsubst comes from gettext.
+ os_release_vars = m.execute("cat /usr/lib/os-release").replace('\n', ' ')
+ m.execute(f'{os_release_vars} envsubst < {branddir}/branding.css > /tmp/branding.ref')
+ self.addCleanup(m.execute, "rm /tmp/branding.ref")
+
+ # fetch some files and make sure they match what we expect
+ def curl_and_compare(name, content_type=None, reference=None):
+ url = f'http://localhost:9090/cockpit/static/{name}'
+ reference = reference or f'{branddir}/{name}'
+
+ # Check that the expected content type is served
+ if content_type is not None:
+ self.assertIn(f'Content-Type: {content_type}', m.execute(f'curl --head {url}'))
+
+ # Check that we can fetch the file
+ m.execute(f'curl --fail -o /tmp/{name} {url}')
+ self.addCleanup(m.execute, f"rm /tmp/{name}")
+
+ # compare that it matches what we expected
+ if not m.ostree_image:
+ m.execute(f'cmp /tmp/{name} {reference}')
+
+ # some brands miss the images, but the OSes we CI have them all
+ curl_and_compare('branding.css', 'text/css', reference='/tmp/branding.ref')
+ curl_and_compare('logo.png', 'image/png')
+ curl_and_compare('favicon.ico')
+
+ # do a pixel test to make sure everything looks like we expect
+ b = self.browser
+ b.open("/system")
+ b.wait_visible("#login")
+ b.assert_pixels("body", "login-screen")
+
+ @testlib.skipOstree("no cockpit-ws package")
+ @testlib.nondestructive
+ def testAuthUnixPath(self):
+ """test UnixPath for auth method in cockpit.conf"""
+ m = self.machine
+
+ m.execute(['systemctl', 'start', 'cockpit-session.socket'])
+ self.addCleanup(m.execute, 'systemctl stop cockpit-session.socket')
+ m.write('/etc/cockpit/cockpit.conf', """
+[Negotiate]
+Action=none
+
+[Basic]
+UnixPath=/run/cockpit/session
+""")
+
+ # make sure this isn't being run via spawning
+ m.execute(f'chmod 700 {self.libexecdir}/cockpit-session')
+ self.addCleanup(m.execute, f'chmod 4750 {self.libexecdir}/cockpit-session')
+
+ m.start_cockpit()
+ self.login_and_go("/system")
+
+ @testlib.skipOstree("test assumes local install")
+ @testlib.nondestructive
+ def testXdgConfig(self):
+ xdg_env = f"XDG_CONFIG_DIRS={self.vm_tmpdir}/xdg:/etc/test-xdg\n"
+ m = self.machine
+ self.write_file("/etc/systemd/system/service.d/xdg.conf",
+ f"[Service]\nEnvironment={xdg_env}\n",
+ post_restore_action="systemctl daemon-reload")
+ m.execute("systemctl daemon-reload")
+
+ m.execute("mkdir -p /etc/test-xdg/cockpit; mv /etc/cockpit/ws-certs.d /etc/test-xdg/cockpit/")
+ self.addCleanup(m.execute, "rm -rf /etc/test-xdg")
+ m.write("/etc/test-xdg/cockpit/cockpit.conf",
+ "[WebService]\nLoginTitle=ExDeeGee\nAllowUnencrypted=true\n")
+
+ # make sure this is not used
+ m.write("/etc/cockpit/cockpit.conf",
+ "[WebService]\nLoginTitle=NoNoNo\nAllowUnencrypted=false\n")
+ m.start_cockpit(tls=True)
+
+ # https works
+ out = m.execute("curl --insecure --head https://localhost:9090/")
+ self.assertIn("HTTP/1.1 200 OK", out)
+ self.assertIn("Set-Cookie: cockpit", out)
+
+ # http from non-localhost works
+ b = self.browser
+ b.open("/system")
+ b.wait_visible("#login")
+ b.wait_text("#server-name", "ExDeeGee")
+
+ # uses the expected certs
+ self.assertEqual(m.execute("readlink /run/cockpit/tls/server/key.source").strip(),
+ "/etc/test-xdg/cockpit/ws-certs.d/0-self-signed.key")
+ # and did not generate new ones
+ self.assertNotIn("ws-certs.d", m.execute("ls /etc/cockpit"))
+ # properly iterates over dirs, ignores the one without files
+ self.assertNotIn("xdg", m.execute(f"ls {self.vm_tmpdir}"))
+
+ # bridge respects it too
+ self.write_file("/etc/environment", f"{xdg_env}\n", append=True)
+ m.write("/etc/test-xdg/cockpit/systemd.override.json", """{
+ "menu": {
+ "services": { "label": "Hackices" }
+ }
+}""")
+ self.login_and_go(None)
+ b.wait_visible("#nav-system li:contains(Hackices)")
+ self.assertFalse(b.is_present("#nav-system li:contains(Services)"))
+
+ def testBridgeCLI(self):
+ m = self.machine
+
+ out = m.execute("cockpit-bridge --help")
+ # this needs to work with both the C and Python bridges
+ self.assertRegex(out, r"[uU]sage:")
+ self.assertIn("--packages", out)
+
+ version = m.execute("cockpit-bridge --version")
+ self.assertIn("Protocol: 1", version)
+ self.assertRegex(version, r"Version: \d{3,}")
+
+ packages = m.execute("cockpit-bridge --packages")
+ self.assertRegex(packages, r"(^|\n)base1\s+.*/usr/share/cockpit/base1")
+ # also includes menu and tools entries
+ self.assertRegex(packages, r"(^|\n)system\s.*Services.*Terminal.*\s/usr/share/cockpit/systemd")
+
+ if self.is_pybridge():
+ bridges = json.loads(m.execute("cockpit-bridge --bridges").strip())
+ bridge_names = [b['name'] for b in bridges]
+ self.assertGreater(len(bridges), 0)
+ self.assertIn('sudo', bridge_names)
+
+ # OStree has no PCP bridge
+ if not m.ostree_image:
+ self.assertIn(f'{self.libexecdir}/cockpit-pcp', bridge_names)
+
+
+@testlib.skipDistroPackage()
+class TestReverseProxy(testlib.MachineCase):
+
+ provision = {
+ "0": {"forward": {"443": 8443}}
+ }
+
+ def setUp(self):
+ super().setUp()
+ m = self.machine
+
+ m.execute("if firewall-cmd --state >/dev/null 2>&1; then firewall-cmd --add-service https; fi")
+
+ m.upload(["../src/tls/ca/alice.pem", "../src/tls/ca/alice.key"], "/etc/pki")
+
+ m.write("/etc/cockpit/cockpit.conf", """[WebService]
+Origins = https://%(origin)s wss://%(origin)s
+ForwardedForHeader = X-Forwarded-For
+ProtocolHeader = X-Forwarded-Proto
+""" % {"origin": m.forward["443"]}, append=True)
+
+ m.execute("setsebool -P httpd_can_network_connect on")
+ self.allow_journal_messages("audit.*bool=httpd_can_network_connect.*val=1.*")
+
+ def callProxyCurl(self, path, *args):
+ # should use nginx' certificate, not cockpit's; use --resolve so that SNI matches the certificate's CN
+ (https_host, https_port) = self.machine.forward["443"].split(':')
+ return subprocess.check_output(
+ ["curl", "--verbose",
+ "--resolve", f"alice:{https_port}:{https_host}",
+ "--cacert", os.path.join(testlib.TEST_DIR, "../src/tls/ca/ca.pem"),
+ *args,
+ f"https://alice:{https_port}{path}"],
+ stderr=subprocess.STDOUT)
+
+ def checkCockpitOnProxy(self, urlroot="", login=True):
+ b = self.new_browser()
+
+ out = self.callProxyCurl(f"{urlroot}/cockpit/static/login.html", "--head")
+ self.assertIn(b"HTTP/1.1 200 OK", out)
+ self.assertIn(b"subject: CN=alice; DC=COCKPIT", out)
+
+ # works with browser (but we can't set our CA)
+ b.ignore_ssl_certificate_errors(ignore=True)
+ (https_host, https_port) = self.machine.forward["443"].split(':')
+ b.open(f"https://{https_host}:{https_port}{urlroot}/system")
+ if login:
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.wait_visible('#content')
+ # Verify that the urlRoot is applied to links in the navbar.
+ b.wait_visible(f'#host-apps a[href="{urlroot}/system"]')
+
+ if login:
+ # check that we show up in the system logs from the browser's IP, not the proxy's
+ self.assertIn('(172.27.0.2)', self.machine.execute('who'))
+ b.logout()
+ b.cdp.invoke("Browser.close")
+
+ @testlib.skipImage("nginx not installed", "centos-8-stream", "rhel-*", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipOstree("nginx not installed")
+ @testlib.skipBrowser("Firefox needs proper cert and CA", "firefox")
+ def testNginxTLS(self):
+ """test proxying to Cockpit with TLS
+
+ As described on https://github.com/cockpit-project/cockpit/wiki/Proxying-Cockpit-over-NGINX
+ This use use case is important for proxying a remote machine.
+ """
+ m = self.machine
+
+ m.write("/etc/nginx/conf.d/cockpit.conf", """
+server {
+ listen 443 ssl;
+ server_name %(origin)s;
+ root /srv/www;
+
+ ssl_certificate "/etc/pki/alice.pem";
+ ssl_certificate_key "/etc/pki/alice.key";
+
+ location / {
+ # Required to proxy the connection to Cockpit
+ proxy_pass https://127.0.0.1:9090;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Required for web sockets to function
+ proxy_http_version 1.1;
+ proxy_buffering off;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ # Pass ETag header from Cockpit to clients.
+ # See: https://github.com/cockpit-project/cockpit/issues/5239
+ gzip off;
+ }
+}
+""" % {"origin": m.forward["443"]})
+
+ m.execute("systemctl start nginx")
+ m.start_cockpit(tls=True)
+ self.checkCockpitOnProxy()
+
+ # now test with UrlRoot
+ m.write("/etc/cockpit/cockpit.conf", "UrlRoot = cockpit-root\n", append=True)
+ m.execute("systemctl stop cockpit.service")
+ self.sed_file("s_location /_location /cockpit-root_", "/etc/nginx/conf.d/cockpit.conf",
+ "systemctl restart nginx")
+ self.checkCockpitOnProxy(urlroot="/cockpit-root")
+
+ # get a non-cockpit file from the server
+ m.execute("mkdir -p /srv/www/embed-cockpit")
+ m.upload(["verify/files/embed-cockpit/index.html",
+ "verify/files/embed-cockpit/embed.js",
+ "verify/files/embed-cockpit/embed.css"],
+ "/srv/www/embed-cockpit/")
+ m.execute("if selinuxenabled 2>&1; then chcon -R -t httpd_sys_content_t /srv/www; fi")
+
+ out = self.callProxyCurl("/embed-cockpit/embed.css")
+ self.assertIn(b"HTTP/1.1 200 OK", out)
+ self.assertIn(b"#embed-links", out)
+
+ # embedding
+ b = self.browser
+ b.ignore_ssl_certificate_errors(ignore=True)
+ (https_host, https_port) = self.machine.forward["443"].split(':')
+ b.open(f"https://{https_host}:{https_port}/embed-cockpit/index.html")
+ b.set_val("#embed-address", f"https://{https_host}:{https_port}/cockpit-root")
+ b.click("#embed-full")
+ b.wait_visible("iframe[name='embed-full'][loaded]")
+ b.switch_to_frame("embed-full")
+
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.wait_visible('.pf-v5-c-card.system-health')
+
+ @testlib.skipImage("nginx not installed", "centos-8-stream", "rhel-*", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipOstree("nginx not installed")
+ @testlib.skipBrowser("Firefox needs proper cert and CA", "firefox")
+ def testNginxNoTLS(self):
+ """test proxying to Cockpit with plain HTTP
+
+ This can be done when nginx and cockpit run on the same machine.
+ """
+ m = self.machine
+
+ m.write("/etc/nginx/conf.d/cockpit.conf", """
+server {
+ listen 443 ssl;
+ server_name %(origin)s;
+
+ ssl_certificate "/etc/pki/alice.pem";
+ ssl_certificate_key "/etc/pki/alice.key";
+
+ location / {
+ # Required to proxy the connection to Cockpit
+ proxy_pass http://127.0.0.1:9090;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Required for web sockets to function
+ proxy_http_version 1.1;
+ proxy_buffering off;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ # Pass ETag header from Cockpit to clients.
+ # See: https://github.com/cockpit-project/cockpit/issues/5239
+ gzip off;
+ }
+}
+""" % {"origin": m.forward["443"]})
+
+ m.execute("systemctl start nginx")
+
+ def run_ws(extra_opts=""):
+ m.spawn(f"su -s /bin/sh -c '{self.libexecdir}/cockpit-ws --address=127.0.0.1 --for-tls-proxy {extra_opts}' cockpit-wsinstance", "ws.log")
+ m.wait_for_cockpit_running()
+
+ def kill_ws():
+ m.execute("pkill cockpit-ws; while pgrep -a cockpit-ws; do sleep 1; done")
+
+ # start cockpit-ws in proxy mode, skip all the ws-certs.d/ steps
+ run_ws()
+ self.checkCockpitOnProxy()
+ kill_ws()
+
+ # works also without the login page (krb, oauth, --local-session, etc.)
+ run_ws("--local-session=cockpit-bridge")
+ self.checkCockpitOnProxy(login=False)
+ kill_ws()
+
+ # UrlRoot + login page
+ m.write("/etc/cockpit/cockpit.conf", "UrlRoot = myroot\n", append=True)
+ self.sed_file("s_location /_location /myroot_", "/etc/nginx/conf.d/cockpit.conf",
+ "systemctl restart nginx")
+ run_ws()
+ self.checkCockpitOnProxy(urlroot="/myroot")
+ kill_ws()
+
+ # UrlRoot without login page
+ run_ws("--local-session=cockpit-bridge")
+ self.checkCockpitOnProxy(urlroot="/myroot", login=False)
+ kill_ws()
+
+ self.allow_restart_journal_messages()
+ self.allow_journal_messages("couldn't register polkit authentication agent.*")
+ self.allow_journal_messages("couldn't change to runtime dir.*Permission denied")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-embed b/test/verify/check-embed
new file mode 100755
index 0000000..f9390ac
--- /dev/null
+++ b/test/verify/check-embed
@@ -0,0 +1,106 @@
+#! /usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import subprocess
+
+import testlib
+
+
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestEmbed(testlib.MachineCase):
+
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ self.restore_dir("/home/admin")
+ m.execute("mkdir -p /home/admin/.local/share/cockpit/embed-cockpit")
+ m.upload(["verify/files/embed-cockpit/index.html",
+ "verify/files/embed-cockpit/embed.js",
+ "verify/files/embed-cockpit/embed.css",
+ "verify/files/embed-cockpit/manifest.json"],
+ "/home/admin/.local/share/cockpit/embed-cockpit/")
+
+ # replace the shell with our embedded page, this way we can avoid
+ # cross-origin errors when executing js in the iframe
+ m.write("/etc/cockpit/cockpit.conf", """
+[WebService]
+Shell=/embed-cockpit/index.html
+""")
+ m.start_cockpit()
+ self.login_and_go()
+
+ b.wait_visible("#embed-loaded")
+ b.wait_visible("#embed-address")
+ m.write("/etc/cockpit/cockpit.conf", """
+[WebService]
+Shell=/shell/index.html
+""")
+ b.set_val("#embed-address", f"http://{m.web_address}:{m.web_port}")
+ b.click("#embed-full")
+ b.wait_visible("iframe[name='embed-full'][loaded]")
+ b.switch_to_frame("embed-full")
+ b.wait_visible("#system_information_os_text")
+
+ # Page should show automatically now that other frame logged in
+ b.switch_to_top()
+ b.click("#embed-terminal")
+ b.wait_visible("iframe[name='embed-terminal'][loaded]")
+ b.switch_to_frame("embed-terminal")
+ b.wait_visible("#terminal")
+
+ # Clicking on the link with separate auth, shouldn't log in automatically
+ b.switch_to_top()
+ b.click("#embed-auth")
+ b.wait_visible("iframe[name='embed-auth'][loaded]")
+ b.switch_to_frame("embed-auth")
+ b.wait_visible("#login-user-input")
+
+ @testlib.skipBrowser("Chromium cannot inspect cross-origin frames", "chromium")
+ def testCrossOrigin(self):
+ b = self.browser
+ m = self.machine
+
+ pyhttpd = subprocess.Popen(['python3', '-m', 'http.server', '--bind=localhost',
+ '--directory=test/verify/files/embed-cockpit', '12346'])
+
+ def clean_pyhttpd():
+ pyhttpd.terminate()
+ pyhttpd.wait()
+
+ self.addCleanup(clean_pyhttpd)
+
+ # log in normally, to get the auth cookie into the browser and thus maximize possible cross-domain exposure
+ self.login_and_go()
+
+ b.open("http://localhost:12346/index.html")
+ b.set_val("#embed-address", f"http://{m.web_address}:{m.web_port}")
+ b.click("#embed-full")
+ b.wait_visible("iframe[name='embed-full'][loaded]")
+ b.switch_to_frame("embed-full")
+
+ # X-Frame-Options sameorigin blocks frame
+ if b.cdp.browser.name == "firefox":
+ b.wait_visible("body.neterror")
+ self.assertFalse(b.is_present("#login"))
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-examples b/test/verify/check-examples
new file mode 100755
index 0000000..fa059e7
--- /dev/null
+++ b/test/verify/check-examples
@@ -0,0 +1,186 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+import os
+
+import testlib
+
+EXAMPLES_DIR = os.path.join(os.path.dirname(testlib.TEST_DIR), "examples")
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestPinger(testlib.MachineCase):
+
+ def setUp(self):
+ super().setUp()
+ self.restore_dir("/home/admin")
+ self.machine.execute("mkdir -p ~admin/.local/share/cockpit")
+ self.machine.upload([os.path.join(EXAMPLES_DIR, "pinger")], "/home/admin/.local/share/cockpit/")
+
+ def testBasic(self):
+ b = self.browser
+
+ self.login_and_go("/pinger/ping")
+ b.set_val("#address", "127.0.0.1")
+ b.click("button#ping")
+ b.wait_in_text("#result", "success")
+ b.wait_in_text("#output", "--- 127.0.0.1 ping statistics")
+
+ def testExtend(self):
+ """
+ This is example test should set summary
+
+ and it also setup description text
+ """
+ self.testBasic()
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestXHRProxy(testlib.MachineCase):
+ def setUp(self):
+ super().setUp()
+ self.restore_dir("/home/admin")
+ self.machine.execute("mkdir -p ~admin/.local/share/cockpit")
+ self.machine.upload([os.path.join(EXAMPLES_DIR, "xhr-proxy")], "/home/admin/.local/share/cockpit/")
+
+ @testlib.skipOstree("No Python installed")
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ # set up served directory
+ httpdir = self.vm_tmpdir + "/root"
+ m.write(f"{httpdir}/hello.txt", "world\n")
+ # start webserver
+ pid = m.spawn(f"cd {httpdir}; exec python3 -m http.server 12345", "httpserver")
+ self.addCleanup(m.execute, "kill %i" % pid)
+ self.machine.wait_for_cockpit_running(port=12345) # wait for changelog HTTP server to start up
+
+ self.login_and_go("/xhr-proxy/xhrproxy")
+
+ # directory index
+ b.set_val("#address", "http://localhost:12345/")
+ b.click("#get")
+ b.wait_text("#result", "200")
+ b.wait_in_text("#output", "Directory listing")
+ b.wait_in_text("#output", "hello.txt")
+
+ # specific file
+ b.set_val("#address", "http://localhost:12345/hello.txt")
+ b.click("#get")
+ b.wait_text("#result", "200")
+ b.wait_text("#output", "world\n")
+
+ # nonexisting path
+ b.set_val("#address", "http://localhost:12345/nosuchfile")
+ b.click("#get")
+ b.wait_text("#result", "404")
+ b.wait_in_text("#output", "File not found")
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestLongRunning(testlib.MachineCase):
+
+ def setUp(self):
+ super().setUp()
+ m = self.machine
+ self.restore_dir("/home/admin")
+ m.execute("mkdir -p ~admin/.local/share/cockpit")
+ m.upload([os.path.join(EXAMPLES_DIR, "long-running-process")], "/home/admin/.local/share/cockpit/")
+ # clean up after test failures
+ self.addCleanup(m.execute, "systemctl stop cockpit-longrunning.service 2>/dev/null && systemctl reset-failed cockpit-longrunning.service || true")
+
+ # sometimes fails with: unexpected failure of GetUnit(cockpit-longrunning.service): Not permitted to perform this action
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/long-running-process")
+
+ self.assertEqual(m.execute("systemctl is-active cockpit-longrunning.service || true").strip(), "inactive")
+ b.wait_text("#state", "cockpit-longrunning.service stopped")
+ b.wait_text("#output", "")
+
+ # run a command that the test can control synchronously
+ ack_file = self.vm_tmpdir + "/ack_a"
+ b.set_val("#command", f"date; echo STEP_A; until [ -e {ack_file} ]; do sleep 1; done; echo STEP_B; sleep 1; echo DONE")
+ b.wait_text("button#run", "Start")
+ b.click("button#run")
+
+ b.wait_text("#state", "cockpit-longrunning.service running")
+ b.wait_in_text("#output", "\nSTEP_A\n")
+ self.assertNotIn("\nSTEP_B", b.text("#output"))
+ self.assertEqual(m.execute("systemctl is-active cockpit-longrunning.service || true").strip(), "activating")
+
+ # reattaches in new session
+ b.logout()
+ b.login_and_go("/long-running-process")
+ b.wait_text("#state", "cockpit-longrunning.service running")
+ b.wait_text("button#run", "Terminate")
+ b.wait_in_text("#output", "\nSTEP_A\n")
+ self.assertEqual(m.execute("systemctl is-active cockpit-longrunning.service || true").strip(), "activating")
+
+ # resume process
+ m.execute(f"mkdir -p {self.vm_tmpdir}; touch {ack_file}")
+ b.wait_in_text("#output", "\nSTEP_B\n")
+ # wait for completion
+ b.wait_in_text("#output", "\nDONE\n")
+ b.wait_text("#state", "cockpit-longrunning.service stopped")
+ self.assertEqual(m.execute("systemctl is-active cockpit-longrunning.service || true").strip(), "inactive")
+
+ # in next session it is back at "not running"
+ b.logout()
+ b.login_and_go("/long-running-process")
+ b.wait_text("#state", "cockpit-longrunning.service stopped")
+ b.wait_text("#output", "")
+ b.wait_text("button#run", "Start")
+
+ # failing process
+ m.execute("rm -f " + ack_file)
+ b.set_val("#command", f"date; echo BREAK_A; until [ -e {ack_file} ]; do sleep 1; done; false; echo NOTME")
+ b.click("button#run")
+ b.wait_text("#state", "cockpit-longrunning.service running")
+ b.wait_in_text("#output", "\nBREAK_A\n")
+ m.execute("touch " + ack_file)
+ b.wait_text("#state", "cockpit-longrunning.service failed")
+ b.wait_in_text("#output", "cockpit-longrunning.service: Main process exited, code=exited, status=1/FAILURE")
+ out = b.text("#output")
+ self.assertNotIn("\nNOTME", out)
+ # does not contain previous logs
+ self.assertNotIn("STEP_B", out)
+ b.wait_text("button#run", "Start")
+
+ # failing state gets picked up on page reconnect
+ b.logout()
+ b.login_and_go("/long-running-process")
+ b.wait_text("#state", "cockpit-longrunning.service failed")
+ b.wait_in_text("#output", "cockpit-longrunning.service: Main process exited, code=exited, status=1/FAILURE")
+ out = b.text("#output")
+ self.assertIn("\nBREAK_A\n", out)
+ self.assertNotIn("\nNOTME", out)
+ b.wait_visible("button#run:disabled")
+ b.wait_text("button#run", "Start")
+
+ # reset
+ m.execute("systemctl reset-failed cockpit-longrunning.service")
+ b.wait_text("#state", "cockpit-longrunning.service stopped")
+
+ # cancel long-running command
+ b.set_val("#command", "for i in $(seq 100); do echo LONG$i; sleep 1; done")
+ b.wait_text("button#run", "Start")
+ b.click("button#run")
+ b.wait_text("#state", "cockpit-longrunning.service running")
+ b.wait_text("button#run", "Terminate")
+ b.wait_in_text("#output", "\nLONG2\n")
+ b.click("button#run")
+ # terminates cleanly
+ b.wait_text("#state", "cockpit-longrunning.service stopped")
+ self.assertEqual(m.execute("systemctl is-active cockpit-longrunning.service || true").strip(), "inactive")
+ b.wait_in_text("#output", "\nLONG2\n")
+ self.assertNotIn("\nLONG30\n", b.text("#output"))
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-kdump b/test/verify/check-kdump
new file mode 100755
index 0000000..b5368ec
--- /dev/null
+++ b/test/verify/check-kdump
@@ -0,0 +1,710 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2016 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import time
+
+import testlib
+from lib.constants import TEST_OS_DEFAULT
+
+
+class KdumpHelpers(testlib.MachineCase):
+ def setUp(self):
+ super().setUp()
+ self.allow_restart_journal_messages()
+
+ def enableLocalSsh(self, machine=None):
+ machine = machine or self.machine
+ machine.execute("[ -f /root/.ssh/id_rsa ] || ssh-keygen -t rsa -N '' -f /root/.ssh/id_rsa")
+ machine.execute("cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys")
+ machine.execute("ssh-keyscan -H localhost >> /root/.ssh/known_hosts")
+ machine.execute("cat /root/.ssh/id_rsa.pub >> /home/admin/.ssh/authorized_keys; chown admin: /home/admin/.ssh/authorized_keys")
+
+ def assertActive(self, active, browser=None):
+ browser = browser or self.browser
+ browser.wait_visible(".pf-v5-c-switch__input" + (active and ":checked" or ":not(:checked)"))
+
+ def enableKdump(self):
+ if self.machine.image.startswith("fedora"):
+ self.machine.execute("systemctl enable kdump; kdumpctl reset-crashkernel")
+ self.machine.reboot()
+
+ def crashKernel(self, message, cancel=False, machine=None, browser=None):
+ browser = browser or self.browser
+ machine = machine or self.machine
+
+ browser.click("button:contains('Test configuration')")
+ browser.wait_in_text(".pf-v5-c-modal-box__body", message)
+ if cancel:
+ browser.click(".pf-v5-c-modal-box button:contains('Cancel')")
+ else:
+ # we should get a warning dialog, confirm
+ browser.click(f".pf-v5-c-modal-box button{self.danger_btn_class}")
+ # wait until we've actually triggered a crash
+ browser.wait_visible(".apply.pf-m-in-progress")
+
+ # wait for disconnect and then try connecting again
+ browser.switch_to_top()
+ with browser.wait_timeout(120):
+ browser.wait_in_text("div.curtains-ct h1", "Disconnected")
+ machine.disconnect()
+ machine.wait_boot(timeout_sec=300)
+
+ def enableNFSServer(self, machine=None):
+ machine = machine or self.machine
+ # set up NFS server
+ self.machines["nfs"].write("/etc/exports", "/srv/kdump 10.111.113.0/24(rw,no_root_squash)\n")
+ self.machines["nfs"].execute("mkdir -p /srv/kdump/var/crash; firewall-cmd --add-service nfs; systemctl restart nfs-server")
+
+ # there shouldn't be any crash reports in the target directory
+ self.assertEqual(machine.execute("find /var/crash -maxdepth 1 -mindepth 1 -type d"), "")
+
+ def run_ansible_playbook(self, browser, machine, allow_failure=False):
+ browser.click("#kdump-automation-script")
+ ansible_script_sel = ".automation-script-modal .pf-v5-c-modal-box__body section:nth-child(2) textarea"
+ machine.execute("mkdir -p roles/kdump/tasks")
+ machine.write("roles/kdump/tasks/main.yml", browser.text(ansible_script_sel))
+ # Redirect to stderr as this can fail, if so we want logs
+ machine.execute("ansible -m include_role -a name=kdump localhost", check=not allow_failure, stdout=None)
+ browser.click(".pf-v5-c-modal-box__footer button:contains('Close')")
+ browser.wait_not_present(".automation-script-modal")
+
+
+@testlib.skipOstree("kexec-tools not installed")
+@testlib.skipImage("kexec-tools not installed", "debian-*", "ubuntu-*", "arch")
+@testlib.timeout(900)
+@testlib.skipDistroPackage()
+class TestKdump(KdumpHelpers):
+
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/kdump")
+
+ b.wait_visible("#app")
+
+ if m.image.startswith("fedora"):
+ # crashkernel command line not set, needs to be explicitly enabled in Fedora
+ b.wait_in_text(".pf-v5-c-alert__title", "Kernel did not boot with the crashkernel setting")
+ # no service on/off button
+ b.wait_not_present(".pf-v5-c-switch__input")
+
+ # disabled "Test configuration" button should have a tooltip
+ b.wait_visible("button:contains(Test configuration)[aria-disabled=true]")
+ b.mouse("button:contains(Test configuration)", "mouseenter")
+ b.wait_in_text(".pf-v5-c-tooltip", "kdump service")
+ b.mouse("button:contains(Test configuration)", "mouseleave")
+
+ # enable it
+ self.enableKdump()
+ self.login_and_go("/kdump")
+ else:
+ # enabled by default in RHEL
+ m.execute("until systemctl is-active kdump; do sleep 1; done")
+ self.assertActive(active=True)
+
+ # no alert about configuration
+ b.wait_not_present(".pf-v5-c-alert__title")
+
+ # there shouldn't be any crash reports in the target directory
+ self.assertEqual(m.execute("find /var/crash -maxdepth 1 -mindepth 1 -type d"), "")
+
+ b.wait_visible("#app")
+ self.enableLocalSsh()
+
+ # minimal nfs validation
+ b.wait_text("#kdump-target-info", "Local, /var/crash")
+
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "nfs")
+ serverInput = "#kdump-settings-nfs-server"
+ b.set_input_text(serverInput, "")
+ b.wait_visible(f"#kdump-settings-dialog button{self.primary_btn_class}:disabled")
+ b.set_input_text(serverInput, "localhost")
+ b.wait_visible(f"#kdump-settings-dialog button{self.primary_btn_class}:disabled")
+ b.click("#kdump-settings-dialog button.cancel")
+ b.wait_not_present("#kdump-settings-dialog")
+
+ # test compression
+ b.click("#kdump-change-target")
+ b.click("#kdump-settings-compression")
+ pathInput = "#kdump-settings-local-directory"
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ with b.wait_timeout(120): # needs to rebuild initrd
+ b.wait_not_present(pathInput)
+ m.execute("cat /etc/kdump.conf | grep -qE 'makedumpfile.*-c.*'")
+
+ # generate a valid kdump config with ssh target
+ b.click("#kdump-change-target")
+ b.set_val("#kdump-settings-location", "ssh")
+ sshInput = "#kdump-settings-ssh-server"
+ b.set_input_text(sshInput, "root@localhost")
+ sshKeyInput = "#kdump-settings-ssh-key"
+ pathInput = "#kdump-settings-ssh-directory"
+ b.set_input_text(sshKeyInput, "/root/.ssh/id_rsa")
+ b.set_input_text(pathInput, "/var/crash")
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ # rebuilding initrd might take a while on busy CI machines
+ with b.wait_timeout(120):
+ b.wait_not_present(pathInput)
+ self.crashKernel("copied through SSH to root@localhost:/var/crash as vmcore", cancel=True)
+
+ # we should have the amount of memory reserved that crashkernel=auto defaults to for our VM RAM size
+ b.wait_in_text("#app", "192 MiB")
+ # service should start up properly and the button should be on
+ self.assertActive(active=True)
+ b.wait_text("#kdump-target-info", "Remote over SSH, root@localhost:/var/crash")
+
+ # try to change the path to a directory that doesn't exist
+ customPath = "/var/crash2"
+ b.click("#kdump-change-target")
+ b.set_val("#kdump-settings-location", "local")
+ pathInput = "#kdump-settings-local-directory"
+ b.set_input_text(pathInput, customPath)
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ # we should get an error
+ b.wait_in_text("#kdump-settings-dialog h4.pf-v5-c-alert__title",
+ "Unable to save settings: Directory /var/crash2 isn't writable or doesn't exist")
+ # also allow the journal message about failed touch
+ self.allow_journal_messages(".*mktemp: failed to create file via template.*")
+ # create the directory and try again
+ m.execute(f"mkdir -p {customPath}")
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ with b.wait_timeout(120):
+ b.wait_not_present(pathInput)
+ b.wait_text("#kdump-target-info", f"Local, {customPath}")
+
+ # service has to restart after changing the config, wait for it to be running
+ # otherwise the button to test will be disabled
+ self.assertActive(active=True)
+
+ # crash the kernel and make sure it wrote a report into the right directory
+ self.crashKernel("stored in /var/crash2 as vmcore")
+ m.execute(f"until test -e {customPath}/127.0.0.1*/vmcore; do sleep 1; done", timeout=180)
+ self.assertIn("Kdump compressed dump", m.execute(f"file {customPath}/127.0.0.1*/vmcore"))
+
+ self.login_and_go("/kdump")
+ b.wait_visible("#app")
+
+ m.execute("cp /etc/kdump.conf /etc/kdump.conf.orig")
+ b.click("#kdump-automation-script")
+ b.click("button:contains('Shell script')")
+ shell_script_sel = ".automation-script-modal .pf-v5-c-modal-box__body section:nth-child(3) textarea"
+ # Change the kdump.conf while the dialog is open as changing it before does not allow us to open the dialog
+ m.write("/etc/kdump.conf", "invalid")
+ m.execute(b.text(shell_script_sel))
+ b.click(".pf-v5-c-modal-box__footer button:contains('Close')")
+ b.wait_not_present(".automation-script-modal")
+ m.execute("diff /etc/kdump.conf /etc/kdump.conf.orig", stdout=None)
+
+ # service errors
+
+ m.execute("systemctl disable --now kdump.service")
+ self.assertActive(active=False)
+ b.wait_not_present(".pf-v5-c-alert__title")
+
+ # service is absent (not installed)
+ m.execute("mv /usr/lib/systemd/system/kdump.service /usr/lib/systemd/system/kdump.service.disabled")
+ m.execute("systemctl daemon-reload")
+ b.wait_in_text(".pf-v5-c-alert__title", "Kdump service is not installed")
+
+ # put it back, but cause error on startup
+ m.execute("mv /usr/lib/systemd/system/kdump.service.disabled /usr/lib/systemd/system/kdump.service")
+ m.write("/etc/systemd/system/kdump.service.d/break.conf",
+ "[Service]\nExecStart=\nExecStart=/bin/false\n")
+ m.execute("systemctl daemon-reload")
+ b.wait_not_present(".pf-v5-c-alert__title")
+ b.click(".pf-v5-c-switch__input")
+ b.wait_in_text(".pf-v5-c-alert__title", "Service has an error")
+ self.assertActive(active=False)
+ # "more details" leads to services page
+ b.click(".pf-v5-c-alert__title button")
+ b.switch_to_top()
+ b.wait_js_cond('window.location.pathname === "/system/services"')
+ b.wait_js_cond('window.location.hash === "#/kdump.service"')
+
+ @testlib.nondestructive
+ def testConfiguration(self):
+ b = self.browser
+ m = self.machine
+
+ self.restore_file("/etc/kdump.conf")
+
+ m.execute("systemctl disable --now kdump")
+
+ self.login_and_go("/kdump")
+ b.wait_visible("#app")
+
+ # Check defaults
+ current = m.execute("grep '^core_collector' /etc/kdump.conf").strip()
+ m.execute("sed -i /^core_collector/d /etc/kdump.conf")
+ # Drop the custom path so we can make sure our changes are synced to JavaScript
+ m.execute("sed -i 's#^path /var/crash#path /var/tmp#' /etc/kdump.conf")
+ last_modified = m.execute("stat /etc/kdump.conf")
+ b.wait_text("#kdump-target-info", "Local, /var/tmp")
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_input_text("#kdump-settings-local-directory", "/var/crash")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ new = m.execute("grep '^core_collector' /etc/kdump.conf").strip()
+ self.assertEqual(current, new)
+ self.assertNotEqual(last_modified, m.execute("stat /etc/kdump.conf"))
+
+ # Check remote ssh location
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "ssh")
+ b.set_input_text("#kdump-settings-ssh-server", "admin@localhost")
+ b.set_input_text("#kdump-settings-ssh-key", "/home/admin/.ssh/id_rsa")
+ b.set_input_text("#kdump-settings-ssh-directory", "/var/tmp/crash")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/kdump.conf")
+ self.assertIn("path /var/tmp/crash", conf)
+ self.assertIn("ssh admin@localhost", conf)
+ self.assertIn("sshkey /home/admin/.ssh/id_rsa", conf)
+
+ # Check remote NFS location
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "nfs")
+ b.set_input_text("#kdump-settings-nfs-server", "someserver")
+ b.set_input_text("#kdump-settings-nfs-export", "/srv")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/kdump.conf")
+ self.assertIn("nfs someserver:/srv", conf)
+ # directory unspecified, using default path
+ self.assertNotIn("\npath ", conf)
+ self.assertNotIn("\nssh ", conf)
+
+ # NFS with custom path
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_input_text("#kdump-settings-nfs-directory", "dumps")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/kdump.conf")
+ self.assertIn("nfs someserver:/srv", conf)
+ self.assertIn("\npath dumps", conf)
+
+ # Check local location
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "local")
+ b.set_input_text("#kdump-settings-local-directory", "/var/tmp")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/kdump.conf")
+ self.assertIn("path /var/tmp", conf)
+ self.assertNotIn("\nnfs ", conf)
+
+ # Check compression
+ current = m.execute("grep '^core_collector' /etc/kdump.conf").strip()
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_checked("#kdump-settings-compression", val=True)
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/kdump.conf")
+ self.assertIn(current + " -c", conf)
+
+ @testlib.nondestructive
+ def testConfigurationSUSE(self):
+ b = self.browser
+ m = self.machine
+
+ testConfig = [
+ "# some comment",
+ "KDUMP_DUMPFORMAT=compressed # suffix",
+ "KDUMP_SSH_IDENTITY=\"\"",
+ "skip this line",
+ "BAD_QUOTES=unquoted value # suffix",
+ "BAD_SPACES = 42 # comment",
+ "MORE_BAD_SPACES = 4 2 # long comment",
+ "KDUMP_SAVEDIR=ssh//missing/colon",
+ ]
+
+ # clean default config to trigger SUSE config mode
+ self.write_file("/etc/kdump.conf", "")
+ # write initial SUSE config (append to keep original contents as well)
+ self.write_file("/etc/sysconfig/kdump", "\n".join(testConfig), append=True)
+
+ m.execute("systemctl disable --now kdump")
+
+ self.login_and_go("/kdump")
+ b.wait_visible("#app")
+
+ # Check malformed lines
+ b.wait_text("#kdump-target-info", "No configuration found")
+ b.wait(lambda: "warning: Malformed kdump config line: skip this line in /etc/sysconfig/kdump" in list(self.browser.get_js_log()))
+ b.wait(lambda: "warning: Malformed KDUMP_SAVEDIR entry: ssh//missing/colon in /etc/sysconfig/kdump" in list(self.browser.get_js_log()))
+
+ # Remove malformed KDUMP_SAVEDIR to check default if nothing specified
+ m.execute("sed -i '/KDUMP_SAVEDIR=.*/d' /etc/sysconfig/kdump")
+ b.wait_text("#kdump-target-info", "Local, /var/crash")
+
+ # Check fixing of (some) malformed lines and local target without file://
+ m.execute("echo KDUMP_SAVEDIR=/tmp >> /etc/sysconfig/kdump")
+ b.wait_text("#kdump-target-info", "Local, /tmp")
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_SAVEDIR=file:///tmp', conf)
+ self.assertIn('BAD_QUOTES="unquoted value" # suffix', conf)
+ self.assertIn('BAD_SPACES=42 # comment', conf)
+ self.assertIn('MORE_BAD_SPACES="4 2" # long comment', conf)
+
+ # Check remote ssh location
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "ssh")
+ b.set_input_text("#kdump-settings-ssh-server", "admin@localhost")
+ b.set_input_text("#kdump-settings-ssh-key", "/home/admin/.ssh/id_rsa")
+ b.set_input_text("#kdump-settings-ssh-directory", "/var/tmp/crash")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ b.wait_text("#kdump-target-info", "Remote over SSH, admin@localhost:/var/tmp/crash")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_SAVEDIR=ssh://admin@localhost/var/tmp/crash', conf)
+ self.assertIn('KDUMP_SSH_IDENTITY="/home/admin/.ssh/id_rsa"', conf)
+
+ # Check remote NFS location
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "nfs")
+ b.set_input_text("#kdump-settings-nfs-server", "someserver")
+ b.set_input_text("#kdump-settings-nfs-export", "/srv")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ b.wait_text("#kdump-target-info", "Remote over NFS, someserver:/srv/var/crash")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_SAVEDIR=nfs://someserver/srv', conf)
+ self.assertNotIn("ssh://", conf)
+
+ # NFS with custom path
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_input_text("#kdump-settings-nfs-directory", "dumps")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ b.wait_text("#kdump-target-info", "Remote over NFS, someserver:/srv/dumps/var/crash")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_SAVEDIR=nfs://someserver/srv/dumps', conf)
+
+ # Check local location
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "local")
+ b.set_input_text("#kdump-settings-local-directory", "/var/tmp")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ b.wait_text("#kdump-target-info", "Local, /var/tmp")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_SAVEDIR=file:///var/tmp', conf)
+ self.assertNotIn("nfs://", conf)
+
+ # Check compression
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_DUMPFORMAT=compressed', conf)
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_checked("#kdump-settings-compression", val=False)
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_DUMPFORMAT=ELF', conf)
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_checked("#kdump-settings-compression", val=True)
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/sysconfig/kdump")
+ self.assertIn('KDUMP_DUMPFORMAT=compressed', conf)
+
+ # Check remote FTP location (no config dialog)
+ m.execute("sed -i 's/KDUMP_SAVEDIR=.*/KDUMP_SAVEDIR=ftp:\\/\\/user@ftpserver\\/dumps1/g' /etc/sysconfig/kdump")
+ b.wait_text("#kdump-target-info", "Remote over FTP")
+
+ # Check remote SFTP location (no config dialog)
+ m.execute("sed -i 's/KDUMP_SAVEDIR=.*/KDUMP_SAVEDIR=sftp:\\/\\/sftpserver\\/dumps2/g' /etc/sysconfig/kdump")
+ b.wait_text("#kdump-target-info", "Remote over SFTP")
+
+ # Check remote CIFS location (no config dialog)
+ m.execute("sed -i 's/KDUMP_SAVEDIR=.*/KDUMP_SAVEDIR=cifs:\\/\\/user:pass@smbserver\\/dumps3/g' /etc/sysconfig/kdump")
+ b.wait_text("#kdump-target-info", "Remote over CIFS/SMB")
+
+
+@testlib.skipOstree("kexec-tools not installed")
+@testlib.skipImage("kexec-tools not installed", "debian-*", "ubuntu-*", "arch")
+@testlib.timeout(900)
+@testlib.skipDistroPackage()
+class TestKdumpNFS(KdumpHelpers):
+ provision = {
+ "0": {"address": "10.111.113.1/24", "memory_mb": 1024, "capture_console": True},
+ "nfs": {"image": TEST_OS_DEFAULT, "address": "10.111.113.2/24", "memory_mb": 512}
+ }
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ self.enableNFSServer()
+
+ # set up client machine
+ self.enableKdump()
+ self.login_and_go("/kdump")
+ with b.wait_timeout(120): # needs to rebuild initrd
+ b.wait_visible(".pf-v5-c-switch__input:checked")
+
+ # Wait a few seconds so that the newly written kdump.conf is
+ # guaranteed to look older to kdump.service than the just
+ # created initrd.
+ #
+ time.sleep(5)
+
+ # switch to NFS
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "nfs")
+ b.set_input_text("#kdump-settings-nfs-server", "10.111.113.2")
+ b.set_input_text("#kdump-settings-nfs-export", "/srv/kdump")
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ # rebuilding initrd might take a while on busy CI machines
+ with b.wait_timeout(300):
+ b.wait_not_present("#kdump-settings-dialog")
+
+ # explicit nfs option, unset path
+ conf = m.execute("cat /etc/kdump.conf")
+ self.assertIn("\nnfs 10.111.113.2:/srv/kdump\n", conf)
+ self.assertNotIn("\npath", conf)
+
+ try:
+ self.crashKernel("copied through NFS to 10.111.113.2:/srv/kdump/var/crash")
+
+ # dump is done during boot, so should exist now
+ self.assertIn("Kdump compressed dump",
+ self.machines["nfs"].execute("file /srv/kdump/var/crash/10.111.113.1*/vmcore"))
+ except (testlib.Error, AssertionError):
+ self.machine.print_console_log()
+ raise
+
+ # set custom path
+ self.login_and_go("/kdump")
+ b.wait_visible(".pf-v5-c-switch__input:checked")
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_input_text("#kdump-settings-nfs-directory", "dumps")
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ # dumps directory does not exist, error
+ b.wait_in_text("#kdump-settings-dialog h4.pf-v5-c-alert__title", "Unable to save settings")
+ # also shows error details
+ b.click("#kdump-settings-dialog div.pf-v5-c-alert__toggle button")
+ b.wait_in_text("#kdump-settings-dialog .pf-v5-c-code-block__code", 'does not exist in dump target "10.111.113.2:/srv/kdump"')
+ # create the directory on the NFS server
+ self.machines["nfs"].execute("mkdir /srv/kdump/dumps")
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ with b.wait_timeout(300):
+ b.wait_not_present("#kdump-settings-dialog")
+ conf = m.execute("cat /etc/kdump.conf")
+ self.assertIn("\nnfs 10.111.113.2:/srv/kdump\n", conf)
+ self.assertIn("\npath dumps\n", conf)
+
+ b.wait_visible(".pf-v5-c-switch__input:checked")
+
+ try:
+ self.crashKernel("copied through NFS to 10.111.113.2:/srv/kdump/dumps")
+ self.assertIn("Kdump compressed dump",
+ self.machines["nfs"].execute("file /srv/kdump/dumps/10.111.113.1*/vmcore"))
+ except (testlib.Error, AssertionError):
+ self.machine.print_console_log()
+ raise
+
+
+@testlib.skipOstree("kexec-tools not installed")
+@testlib.onlyImage("Only Fedora has ansible on the image", "fedora-*")
+@testlib.timeout(900)
+class TestKdumpNFSAnsible(KdumpHelpers):
+ provision = {
+ "cockpit": {"memory_mb": 512},
+ "kdump_ansible_machine": {"address": "10.111.113.1/24", "memory_mb": 1024, "capture_console": True},
+ "nfs": {"image": TEST_OS_DEFAULT, "address": "10.111.113.2/24", "memory_mb": 512},
+ }
+
+ def testBasic(self):
+ b = self.browser
+ kdump_machine = self.machines["kdump_ansible_machine"]
+
+ # Drop rpm repos as otherwise dnf will hang when we run the ansible role
+ kdump_machine.execute("rm -rf /etc/yum.repos.d/* /var/cache/yum/* /var/cache/dnf/*")
+
+ self.enableNFSServer(kdump_machine)
+
+ # switch to NFS
+ self.login_and_go("/kdump")
+ b.click("#kdump-change-target")
+ b.wait_visible("#kdump-settings-dialog")
+ b.set_val("#kdump-settings-location", "nfs")
+ b.set_input_text("#kdump-settings-nfs-server", "10.111.113.2")
+ b.set_input_text("#kdump-settings-nfs-export", "/srv/kdump")
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ b.wait_not_present("#kdump-settings-dialog")
+
+ # This requires a reboot
+ self.run_ansible_playbook(browser=b, machine=kdump_machine, allow_failure=True)
+ conf = kdump_machine.execute("cat /etc/kdump.conf")
+ self.assertIn("\n# Ansible managed\n", conf)
+ self.assertIn("\nnfs 10.111.113.2:/srv/kdump\n", conf)
+
+ kdump_machine.reboot()
+ kdump_machine.start_cockpit()
+ kdump_browser = self.new_browser(kdump_machine)
+
+ # Verify that kdump runs and crashkernel is configured
+ kdump_machine.execute("until systemctl is-active kdump; do sleep 1; done")
+ kdump_browser.login_and_go("/kdump")
+ kdump_browser.wait_visible(".pf-v5-c-switch__input:checked")
+ kdump_browser.wait_in_text("#app", "192 MiB")
+ self.assertActive(active=True, browser=kdump_browser)
+
+ try:
+ self.crashKernel("copied through NFS to 10.111.113.2:/srv/kdump/var/crash", machine=kdump_machine, browser=kdump_browser)
+
+ # dump is done during boot, so should exist now
+ self.assertIn("Kdump compressed dump",
+ self.machines["nfs"].execute("file /srv/kdump/var/crash/10.111.113.1*/vmcore"))
+ except (testlib.Error, AssertionError):
+ self.machine.print_console_log()
+ raise
+
+
+@testlib.skipOstree("kexec-tools not installed")
+@testlib.onlyImage("Only Fedora has ansible on the image", "fedora-*")
+@testlib.timeout(900)
+class TestKdumpAnsible(KdumpHelpers):
+ provision = {
+ "cockpit": {"memory_mb": 512},
+ "kdump_ansible_machine": {"memory_mb": 900},
+ }
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ kdump_machine = self.machines['kdump_ansible_machine']
+
+ # Drop rpm repos as otherwise dnf will hang when we run the ansible role
+ kdump_machine.execute("rm -rf /etc/yum.repos.d/* /var/cache/yum/* /var/cache/dnf/*")
+
+ self.login_and_go("/kdump")
+ customPath = "/var/crash2"
+
+ m.execute(f"mkdir -p {customPath}")
+ b.click("#kdump-change-target")
+ b.set_val("#kdump-settings-location", "local")
+ pathInput = "#kdump-settings-local-directory"
+ b.set_input_text(pathInput, customPath)
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ b.wait_text("#kdump-target-info", f"Local, {customPath}")
+
+ # HACK: workaround ansible role not creating /var/crash2 https://github.com/linux-system-roles/kdump/issues/8
+ kdump_machine.execute(f"mkdir -p {customPath}")
+
+ # This requires a reboot
+ self.run_ansible_playbook(browser=b, machine=kdump_machine, allow_failure=True)
+ kdump_machine.reboot()
+
+ kdump_machine.start_cockpit()
+ kdump_browser = self.new_browser(kdump_machine)
+
+ kdump_machine.execute("until systemctl is-active kdump; do sleep 1; done")
+ kdump_browser.login_and_go("/kdump")
+
+ # Verify that kdump runs and crashkernel is configured
+ kdump_browser.wait_in_text("#app", "192 MiB")
+ self.assertActive(active=True, browser=kdump_browser)
+
+ # Second run should be succeed as crashkernel is now set in the kernel cmdline
+ self.run_ansible_playbook(browser=b, machine=kdump_machine)
+
+ conf = kdump_machine.execute("cat /etc/kdump.conf")
+ self.assertIn("\n# Ansible managed\n", conf)
+ self.assertIn("\npath /var/crash2\n", conf)
+
+ self.crashKernel("stored in /var/crash2 as vmcore", machine=kdump_machine, browser=kdump_browser)
+ kdump_machine.execute(f"until test -e {customPath}/127.0.0.1*/vmcore; do sleep 1; done", timeout=180)
+ self.assertIn("Kdump compressed dump", kdump_machine.execute(f"file {customPath}/127.0.0.1*/vmcore"))
+
+ # Test SSH without a username, for this we first need to first change
+ # to local not run into a race to detect that the generated kdump.conf
+ # was changed. As the #kdump-change-target does not show the remote ssh user/server.
+ b.click("#kdump-change-target")
+ b.set_val("#kdump-settings-location", "local")
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ b.wait_not_present("#kdump-settings-dialog")
+ b.wait_text("#kdump-target-info", "Local, /var/crash")
+
+ b.click("#kdump-change-target")
+ b.set_val("#kdump-settings-location", "ssh")
+ sshInput = "#kdump-settings-ssh-server"
+ b.set_input_text(sshInput, "admin@localhost")
+ sshKeyInput = "#kdump-settings-ssh-key"
+ pathInput = "#kdump-settings-ssh-directory"
+ b.set_input_text(sshKeyInput, "/root/.ssh/id_rsa")
+ b.set_input_text(pathInput, "/var/crash")
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ b.wait_not_present(pathInput)
+ b.wait_text("#kdump-target-info", "Remote over SSH, admin@localhost:/var/crash")
+
+ self.enableLocalSsh()
+ # running the role starts kdump.service which verifies if /var/crash is readable for the admin
+ kdump_machine.start_cockpit()
+ kdump_machine.execute("chown admin: /var/crash")
+ self.run_ansible_playbook(browser=b, machine=kdump_machine)
+ conf = kdump_machine.execute("cat /etc/kdump.conf")
+ self.assertIn("\n# Ansible managed\n", conf)
+ self.assertIn("\nssh admin@localhost\n", conf)
+ self.assertIn("\nsshkey /root/.ssh/id_rsa\n", conf)
+ self.assertIn("\npath /var/crash\n", conf)
+
+ kdump_browser.login_and_go("/kdump")
+ kdump_browser.wait_text("#kdump-target-info", "Remote over SSH, admin@localhost:/var/crash")
+ self.crashKernel("copied through SSH to admin@localhost:/var/crash as vmcore", cancel=True,
+ browser=kdump_browser, machine=kdump_machine)
+
+ b.click("#kdump-change-target")
+ sshInput = "#kdump-settings-ssh-server"
+ b.set_input_text(sshInput, "localhost")
+ b.set_input_text(pathInput, "/var/crash2")
+ b.click(f"#kdump-settings-dialog button{self.primary_btn_class}")
+ b.wait_not_present(sshInput)
+ b.wait_text("#kdump-target-info", "Remote over SSH, localhost:/var/crash2")
+
+ self.run_ansible_playbook(browser=b, machine=kdump_machine)
+ conf = kdump_machine.execute("cat /etc/kdump.conf")
+ self.assertIn("\nssh root@localhost\n", conf)
+ self.assertIn("\npath /var/crash2\n", conf)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-loopback b/test/verify/check-loopback
new file mode 100755
index 0000000..1421914
--- /dev/null
+++ b/test/verify/check-loopback
@@ -0,0 +1,72 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+
+@testlib.skipOstree("OSTree always uses loopback")
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestLoopback(testlib.MachineCase):
+
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ # Start Cockpit with option to access SSH
+ m.stop_cockpit()
+ pid = m.spawn(f"{self.libexecdir}/cockpit-ws --local-ssh --no-tls", "cockpit-ws")
+ self.addCleanup(m.execute, "kill %i" % pid)
+ m.wait_for_cockpit_running()
+
+ b.login_and_go("/system", user="admin")
+ b.logout()
+ b.wait_visible("#login")
+
+ self.restore_file("/usr/bin/cockpit-bridge")
+ m.execute("rm /usr/bin/cockpit-bridge")
+
+ b.set_val('#login-user-input', "admin")
+ b.set_val('#login-password-input', "foobar")
+ b.click('#login-button')
+ b.wait_visible('#login-fatal')
+ self.assertIn("no-cockpit", b.text('#login-fatal'))
+
+ m.disconnect()
+ self.restore_dir("/etc/ssh", restart_unit="sshd.service")
+ m.execute("sed -i 's/.*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null || true)")
+ m.execute("systemctl try-restart sshd.service")
+ m.wait_execute()
+
+ b.reload()
+ b.wait_visible("#login")
+ b.set_val('#login-user-input', "admin")
+ b.set_val('#login-password-input', "foobar")
+ b.click('#login-button')
+ b.wait_visible('#login-fatal')
+ self.assertIn(
+ "The server refused to authenticate 'admin' using password authentication, and no other supported authentication methods are available.",
+ b.text('#login-fatal'))
+
+ self.allow_journal_messages("Cannot run program cockpit-bridge: No such file or directory",
+ ".*server offered unsupported authentication methods: public-key.*")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-metrics b/test/verify/check-metrics
new file mode 100755
index 0000000..295adf5
--- /dev/null
+++ b/test/verify/check-metrics
@@ -0,0 +1,1537 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+# Run this with --help to see available options for tracing and debugging
+# See https://github.com/cockpit-project/cockpit/blob/main/test/common/testlib.py
+# "class Browser" and "class MachineCase" for the available API.
+
+import re
+import time
+
+import packagelib
+import testlib
+from lib.constants import TEST_OS_DEFAULT
+from machine_core import ssh_connection
+
+
+def getMaximumSpike(test, g_type, saturation, hour, minute):
+ # only for minutes with events, which have SVG graphs
+ sel = f"#metrics-hour-{hour} div.metrics-minute[data-minute='{minute}'] div.metrics-data-{g_type} div"
+ if saturation:
+ sel += ":nth-child(2)"
+ else:
+ sel += ":first-child"
+
+ points = test.browser.attr(sel, "points")
+ xs = [float(x.split(" ")[0].rstrip("%")) for x in points.split(", ") if x != ""]
+ test.assertNotIn("NaN", xs)
+
+ return max(xs) / 100
+
+
+def getCompressedMinuteValue(test, g_type, saturation, hour, minute):
+ # only for minutes without events, which only have bars
+
+ polygon_class = ".polygon-sat" if saturation else ".polygon-use"
+ sel = f"#metrics-hour-{hour} div.metrics-minute[data-minute='{minute}'] div.metrics-data-{g_type} .compressed{polygon_class}"
+ m = re.search(r"--%s:\s*([0-9.]+);" % (saturation and "saturation" or "utilization"), test.browser.attr(sel, "style"))
+ test.assertIsNotNone(m)
+ return float(m.group(1))
+
+
+def topServiceValue(test, aria_label, col_label, row):
+ sel = "table[aria-label='%s'] tbody tr:nth-of-type(%d) td[data-label='%s']" % (aria_label, row, col_label)
+ # split off unit, like "12 MB"
+ return float(test.browser.text(sel).split(' ')[0])
+
+
+def prepareArchive(machine, name, time, hostname="localhost.localdomain"):
+ machine.upload([f"verify/files/metrics-archives/{name}"], "/tmp/")
+
+ command = f"tar -C / -xzvf /tmp/{name}"
+ if name.endswith("zip"):
+ command = f"unzip /tmp/{name} -d /"
+
+ machine.execute(f"""ntp=`timedatectl show --property NTP --value`
+ if [ $ntp == "yes" ]; then
+ timedatectl set-ntp off
+ fi
+ systemctl stop pmlogger
+ # don't let NM set transient host names from DHCP
+ systemctl stop NetworkManager
+ hostnamectl set-hostname {hostname}
+ rm -rf /var/log/pcp/pmlogger/*
+ {command}
+ # set-ntp off is asynchronous; wait until timesyncd stops before the time can be set
+ while systemctl is-active systemd-timesyncd; do sleep 1; done
+ timedatectl set-time @{time}""")
+
+
+def redisService(image):
+ if image.startswith(("debian", "ubuntu")):
+ return "redis-server"
+ return "redis"
+
+
+def applySettings(browser, dialog_selector):
+ browser.click(f"{dialog_selector} button.pf-m-primary")
+ with browser.wait_timeout(30):
+ browser.wait_not_present(dialog_selector)
+
+
+def login(self):
+ # HACK: Ubuntu and Debian need some time until metrics channel is available
+ # Really no idea what it needs to wait for, so let's just try channel until it succeeds
+ if self.machine.image.startswith("ubuntu") or self.machine.image.startswith("debian"):
+ self.login_and_go("/system")
+ self.browser.wait(lambda: self.browser.call_js_func("""(function() {
+ return new Promise((resolve, reject) => {
+ cockpit.spawn(["date", "+%s"])
+ .then(out => {
+ const now = parseInt(out.trim()) * 1000;
+ const current_hour = Math.floor(now / 3600000) * 3600000;
+ const metrics_channel = cockpit.channel({ payload: "metrics1", source: "pcp-archive",
+ interval: 5000, metrics: [{ name: "kernel.all.cpu.nice", derive: "rate" }],
+ timestamp: current_hour, limit: 10 });
+ metrics_channel.addEventListener("close", (ev, error) => {
+ if (error.problem) {
+ console.log("Channel is not ready:", error.problem);
+ resolve(0);
+ } else
+ resolve(1);
+ });
+ });
+ });
+ })"""))
+ self.browser.click("a:contains('View metrics and history')")
+ self.browser.enter_page("/metrics")
+ else:
+ self.login_and_go("/metrics")
+
+
+@testlib.skipDistroPackage()
+class TestHistoryMetrics(testlib.MachineCase):
+ def setUp(self):
+ super().setUp()
+ # start with a clean slate and avoid running into restart limits
+ self.machine.execute("systemctl stop pmlogger pmproxy; systemctl reset-failed pmlogger pmproxy 2>/dev/null || true")
+ # HACK: PF modal can have multiple id's in certain scenario's https://github.com/patternfly/patternfly-react/issues/9399
+ self.pcp_dialog_selector = "#pcp-settings-modal:first-child"
+
+ def waitStream(self, current_max):
+ # should only have at most <current_max> valid minutes, the rest should be empty
+ valid_start = self.browser.call_js_func("ph_count", ".metrics-data-cpu.valid-data")
+ self.assertLessEqual(valid_start, current_max)
+ # page auto-updates every minute
+ with self.browser.wait_timeout(90):
+ self.browser.wait_js_func("(exp => ph_count('.metrics-data-cpu.valid-data') == exp)", valid_start + 1)
+
+ # Should never show more then 4 empty leading minutes (block of 5 minutes but always at least one used)
+ leading_empty = self.browser.call_js_func("""(function () {
+ const lines = document.getElementsByClassName("metrics-data-cpu");
+ let counter = 0;
+
+ Array.from(lines).every(l => {
+ if (l.classList.contains("empty-data")) {
+ counter++;
+ return true;
+ } else {
+ return false;;
+ }
+ });
+
+ return counter;
+ })""")
+ self.assertLessEqual(leading_empty, 4)
+
+ @testlib.skipOstree("no PCP support")
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("""ntp=`timedatectl show --property NTP --value`
+ if [ $ntp == "yes" ]; then
+ timedatectl set-ntp off
+ fi""")
+ m.execute("while systemctl is-active systemd-timesyncd; do sleep 1; done")
+ m.execute("timedatectl set-time '2020-11-24 09:24:05'")
+
+ # clean slate, to avoid seeing the data from preparing the VM
+ m.execute("rm -rf /var/log/pcp/pmlogger/*; systemctl start pmlogger")
+
+ login(self)
+ # eventually finishes data loading and shows heading
+ b.wait_in_text(".metrics-heading", "CPU")
+
+ # only shows current hour
+ b.wait_js_func("ph_count_check", ".metrics-hour", 1)
+
+ # VM just started, we don't have 12 hours of data
+ b.wait_in_text(".metrics .pf-v5-c-alert", "No data available between")
+ # initial data gap is < 24 hours, does not show date
+ year = m.execute("date +%Y").strip()
+ self.assertNotIn(year, b.text(".metrics .pf-v5-c-alert"))
+
+ # can try to load earlier data; only updates "no data" alert as there is no data
+ b.wait_text(".bottom-panel button", "Load earlier data")
+ b.click(".bottom-panel button")
+ # now the gap is > 24 hours, does show date
+ b.wait_in_text(".metrics .pf-v5-c-alert", year)
+ # still only one hour
+ b.wait_js_func("ph_count_check", ".metrics-hour", 1)
+
+ self.waitStream(3)
+
+ # Graphs are by default all visible
+ b.click("button[aria-label='Graph visibility options menu']")
+ b.wait_visible("#column-visibility-option-cpu:checked")
+ b.wait_visible(".metrics-label-graph:contains(CPU)")
+ b.wait_visible("#column-visibility-option-memory:checked")
+ b.wait_visible(".metrics-label-graph:contains(Memory)")
+ b.wait_visible("#column-visibility-option-disks:checked")
+ b.wait_visible(".metrics-label-graph:contains(Disk I/O)")
+ b.wait_visible("#column-visibility-option-network:checked")
+ b.wait_visible(".metrics-label-graph:contains(Network)")
+
+ # Change graph visibility
+ b.wait_visible(".metrics-events:contains('Network I/O')")
+ b.set_checked("#column-visibility-option-network", val=False)
+ b.wait_not_present(".metrics-events:contains('Network I/O')")
+ b.wait_not_present(".metrics-label-graph:contains(Network)")
+ b.set_checked("#column-visibility-option-network", val=True)
+
+ # Change date to yesterday, should be empty
+ b.click("#date-picker-select-toggle .pf-v5-c-select__toggle-arrow")
+ b.click(".pf-v5-c-select__menu-wrapper:nth-child(2) button")
+ b.wait_text(".pf-v5-c-empty-state", "No data available")
+
+ # Breadcrumb back to Overview page
+ b.click(".pf-v5-c-breadcrumb li:first-child")
+ b.enter_page("/system")
+ b.wait_visible('.system-information')
+
+ @testlib.skipOstree("no PCP support")
+ def testEvents(self):
+ b = self.browser
+ m = self.machine
+
+ b.wait_timeout(60)
+
+ def events_at(hour, minute):
+ b.wait_visible(f"#metrics-hour-{hour}.metrics-hour-compressed")
+ b.click(f"#metrics-hour-{hour} button.metrics-events-expander")
+ events = b.text(f"#metrics-hour-{hour} div.metrics-minute[data-minute='{minute}'] .metrics-events")
+ b.click(f"#metrics-hour-{hour} button.metrics-events-expander")
+
+ return events
+
+ #
+ # Disks
+ #
+
+ # disable swap, so that we can test current metrics without swap
+ m.execute("""systemctl stop "*.swap" "swap-create@*" "systemd-zram-setup@*" || true
+ systemctl mask "swap-create@" "systemd-zram-setup@"
+ swapoff --all
+ while [ -n "$(swapon --show)" ]; do sleep 1; done""")
+
+ prepareArchive(m, "disk.tar.gz", 1597672800)
+
+ login(self)
+ # eventually finishes data loading and shows heading
+ b.wait_in_text(".metrics-heading", "CPU")
+
+ # Big spike lasting 3 minutes
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="disks", saturation=False, hour=1597662000000, minute=25), 0.9)
+ self.assertGreaterEqual(getCompressedMinuteValue(test=self, g_type="disks", saturation=False, hour=1597662000000, minute=26), 0.9)
+ self.assertGreaterEqual(getCompressedMinuteValue(test=self, g_type="disks", saturation=False, hour=1597662000000, minute=27), 0.9)
+
+ # Smaller spike lasting 2 minutes
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="disks", saturation=False, hour=1597662000000, minute=28), 0.4)
+ self.assertLessEqual(getMaximumSpike(test=self, g_type="disks", saturation=False, hour=1597662000000, minute=28), 0.6)
+ self.assertGreaterEqual(getCompressedMinuteValue(test=self, g_type="disks", saturation=False, hour=1597662000000, minute=29), 0.4)
+ # recognized as event
+
+ self.assertIn("Disk I/O", events_at(1597662000000, 28))
+
+ # No visible activity after that
+ self.assertLessEqual(getCompressedMinuteValue(test=self, g_type="disks", saturation=False, hour=1597662000000, minute=30), 0.01)
+
+ # swap usage is not shown if there is no swap
+ b.wait_visible("#current-memory-usage")
+ self.assertFalse(b.is_present("#current-swap-usage"))
+
+ # Check that we don't show too much empty minutes in the first hour
+ self.assertLessEqual(b.call_js_func("ph_count", ".metrics-data-cpu"), 35)
+
+ # Check metrics hour header in compressed and expanded mode
+ b.click("#metrics-hour-1597662000000 button.metrics-events-expander")
+ b.wait_in_text("#metrics-hour-1597662000000:not(.metrics-hour-compressed) .metrics-events-hour-header-expanded time", "1:00")
+ b.wait_in_text("#metrics-hour-1597662000000:not(.metrics-hour-compressed) .metrics-events-hour-header-expanded .spikes_count", "3 spikes")
+ b.wait_in_text("#metrics-hour-1597662000000:not(.metrics-hour-compressed) .metrics-events-hour-header-expanded .spikes_info", "1 Memory, 1 Disk I/O, 1 Network I/O")
+
+ b.assert_pixels(".metrics", "metrics-history-expanded-hour", ignore=[".spikes_count"],
+ wait_after_layout_change=True)
+
+ b.click("#metrics-hour-1597662000000 button.metrics-events-expander")
+ b.wait_in_text("#metrics-hour-1597662000000.metrics-hour-compressed", "1:00")
+ b.wait_in_text("#metrics-hour-1597662000000.metrics-hour-compressed .spikes_count", "3 spikes")
+ b.wait_in_text("#metrics-hour-1597662000000.metrics-hour-compressed .spikes_info", "1 Memory, 1 Disk I/O, 1 Network I/O")
+
+ b.assert_pixels(".metrics", "metrics-history-compressed-hour", ignore=[".nodata"],
+ wait_after_layout_change=True)
+
+ # Check that events are not visible for compressed hours
+ b.wait_not_present("#metrics-hour-1597662000000 div.metrics-minute[data-minute='28'] .metrics-events")
+ b.click("#metrics-hour-1597662000000 button.metrics-events-expander")
+ b.wait_visible("#metrics-hour-1597662000000 div.metrics-minute[data-minute='28'] .metrics-events")
+
+ b.logout()
+
+ #
+ # Network and CPU
+ #
+
+ prepareArchive(m, "cpu_network.tar.gz", 1598918400)
+
+ login(self)
+ # eventually finishes data loading and shows heading
+ b.wait_in_text(".metrics-heading", "CPU")
+
+ # Test network - Big spike lasting 2 minutes
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="network", saturation=False, hour=1598950800000, minute=3), 0.5)
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="network", saturation=False, hour=1598950800000, minute=4), 0.5)
+ # recognized as event
+ self.assertIn("Network I/O", events_at(1598950800000, 3))
+ # but it's not a new event in minute 4
+ self.assertNotIn("Network I/O", events_at(1598950800000, 4))
+
+ # Followed by smaller spike
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="network", saturation=False, hour=1598950800000, minute=5), 0.35)
+ self.assertLessEqual(getMaximumSpike(test=self, g_type="network", saturation=False, hour=1598950800000, minute=5), 0.5)
+ # still not a new spike
+ self.assertNotIn("Network I/O", events_at(1598950800000, 5))
+
+ # Followed by virtually no data
+ self.assertLessEqual(getCompressedMinuteValue(test=self, g_type="network", saturation=False, hour=1598950800000, minute=6), 0.01)
+
+ # Test CPU load - big - small - big spikes
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="cpu", saturation=False, hour=1598950800000, minute=3), 0.9)
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="cpu", saturation=False, hour=1598950800000, minute=4), 0.5)
+ self.assertLessEqual(getMaximumSpike(test=self, g_type="cpu", saturation=False, hour=1598950800000, minute=4), 0.55)
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="cpu", saturation=False, hour=1598950800000, minute=5), 0.9)
+ self.assertIn("CPU", events_at(1598950800000, 2))
+ self.assertIn("CPU", events_at(1598950800000, 5))
+
+ # Test CPU saturation - 3 spikes, each 2 minutes (medium, big, small)
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="cpu", saturation=True, hour=1598950800000, minute=3), 0.5)
+ self.assertLessEqual(getMaximumSpike(test=self, g_type="cpu", saturation=True, hour=1598950800000, minute=3), 0.6)
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="cpu", saturation=True, hour=1598950800000, minute=4), 0.5)
+ self.assertLessEqual(getMaximumSpike(test=self, g_type="cpu", saturation=True, hour=1598950800000, minute=4), 0.6)
+
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="cpu", saturation=True, hour=1598950800000, minute=5), 0.8)
+ self.assertGreaterEqual(getCompressedMinuteValue(test=self, g_type="cpu", saturation=True, hour=1598950800000, minute=6), 0.8)
+
+ self.assertGreaterEqual(getCompressedMinuteValue(test=self, g_type="cpu", saturation=True, hour=1598950800000, minute=7), 0.3)
+ self.assertLessEqual(getCompressedMinuteValue(test=self, g_type="cpu", saturation=True, hour=1598950800000, minute=7), 0.4)
+ self.assertGreaterEqual(getCompressedMinuteValue(test=self, g_type="cpu", saturation=True, hour=1598950800000, minute=8), 0.3)
+ self.assertLessEqual(getCompressedMinuteValue(test=self, g_type="cpu", saturation=True, hour=1598950800000, minute=8), 0.4)
+
+ self.assertNotIn("Load", events_at(1598950800000, 2))
+ self.assertIn("Load", events_at(1598950800000, 3))
+ self.assertNotIn("Load", events_at(1598950800000, 4))
+ self.assertIn("Load", events_at(1598950800000, 5))
+
+ b.logout()
+
+ #
+ # Memory
+ #
+
+ have_swap = m.execute("swapon --show").strip()
+
+ prepareArchive(m, "memory.tar.gz", 1600248000)
+ login(self)
+ b.wait_in_text(".metrics-heading", "CPU")
+
+ # basic RAM consumption after boot; it's still a network spike, thus event+SVG
+ self.assertLessEqual(getMaximumSpike(test=self, g_type="memory", saturation=False, hour=1600236000000, minute=44), 0.3)
+ self.assertNotIn("Memory", events_at(1600236000000, 44))
+ if have_swap:
+ self.assertAlmostEqual(getMaximumSpike(test=self, g_type="memory", saturation=True, hour=1600236000000, minute=44), 0)
+ self.assertNotIn("Swap", events_at(1600236000000, 44))
+
+ # swap event from :46 to :47
+ self.assertGreater(getMaximumSpike(test=self, g_type="memory", saturation=True, hour=1600236000000, minute=46), 0.9)
+ self.assertIn("Swap", events_at(1600236000000, 46))
+ # continuous, no new Swap event, but still a Memory+Network event
+ self.assertGreater(getMaximumSpike(test=self, g_type="memory", saturation=True, hour=1600236000000, minute=47), 0.9)
+ self.assertNotIn("Swap", events_at(1600236000000, 47))
+
+ else:
+ # If no swap, the column is hidden
+ self.assertNotIn(b.text(".metrics-heading"), "Swap")
+ b.wait_not_present(".metrics-data-memory .saturation")
+
+ # memory spike in :47
+ self.assertGreater(getMaximumSpike(test=self, g_type="memory", saturation=False, hour=1600236000000, minute=47), 0.6)
+ self.assertIn("Memory", events_at(1600236000000, 47))
+
+ # at :54 the machine is loaded to ~80% so no event even if elevated
+ self.assertGreater(getCompressedMinuteValue(test=self, g_type="memory", saturation=False, hour=1600236000000, minute=54), 0.8)
+ b.wait_not_present("#metrics-hour-1600236000000 div.metrics-minute[data-minute='54'] .metrics-events")
+ if have_swap:
+ self.assertAlmostEqual(getCompressedMinuteValue(test=self, g_type="memory", saturation=True, hour=1600236000000, minute=54), 0.0)
+
+ # everything is quiet in :55
+ self.assertLess(getCompressedMinuteValue(test=self, g_type="memory", saturation=False, hour=1600236000000, minute=55), 0.4)
+ if have_swap:
+ self.assertAlmostEqual(getCompressedMinuteValue(test=self, g_type="memory", saturation=True, hour=1600236000000, minute=55), 0.0)
+
+ b.logout()
+
+ #
+ # Check changing of time
+ #
+
+ m.execute("timedatectl set-time @1600550674")
+ login(self)
+ # self.waitStream(3) # FIXME: wait for new data - pcp does not handle time change greatly
+ b.wait_text("#date-picker-select-toggle .pf-v5-c-select__toggle-text", "Today")
+
+ b.select_PF4("#date-picker-select-toggle", "Wednesday, September 16, 2020")
+ self.assertGreater(getMaximumSpike(test=self, g_type="memory", saturation=False, hour=1600236000000, minute=51), 0.5)
+ self.assertIn("Memory", events_at(1600236000000, 51))
+
+ # Reload should keep the filters intact
+ b.reload()
+ b.enter_page("/metrics")
+ b.wait_text("#date-picker-select-toggle .pf-v5-c-select__toggle-text", "Wednesday, September 16, 2020")
+
+ b.click("#date-picker-select-toggle")
+ b.click(".pf-v5-c-select__menu-item:contains('Today')")
+ b.wait_text("#date-picker-select-toggle .pf-v5-c-select__toggle-text", "Today")
+ # self.waitStream(4) # FIXME: wait for new data - pcp does not handle time change greatly
+
+ b.logout()
+
+ #
+ # Check that for every minute only one event is present
+ #
+
+ if self.machine.image == TEST_OS_DEFAULT: # Debian/Ubuntu is unhappy about this archive, one Fedora test is enough though
+ prepareArchive(m, "double_events.zip", 1602345600, "m1.cockpit.lan")
+ login(self)
+ b.wait_in_text(".metrics-heading", "CPU")
+ b.wait_in_text("#metrics-hour-1602334800000", "CPU")
+ self.assertTrue(self.browser.call_js_func("""(function () {
+ const min_events = document.getElementsByClassName("metrics-events");
+ return Array.from(min_events).every(l => {
+ const events = Array.from(l.getElementsByTagName("dd")).map(d => d.innerHTML);
+ return (new Set(events)).size === events.length;
+ });
+ })"""))
+
+ b.logout()
+
+ #
+ # Journal logs
+ #
+
+ prepareArchive(m, "with_journal.tar.gz", 1615200500, "m1.cockpit.lan")
+ # first check the "no logs found" case
+ login(self)
+ b.wait_in_text(".metrics-heading", "CPU")
+ b.click("#metrics-hour-1615197600000 button.metrics-events-expander")
+ b.wait_in_text("#metrics-hour-1615197600000 div.metrics-minute[data-minute='39'] .metrics-events span.spikes_info", "Load")
+
+ # Now add the journal
+ # Journal was recorded on Fedora 33 and when trying to use it with older systemd it fails with:
+ # `Journal file /var/log/journal/*/journal.journal uses an unsupported feature, ignoring file.`
+
+ if self.machine.image == "centos-8-stream" or self.machine.image.startswith("rhel-8-"):
+ return
+
+ m.upload(["verify/files/metrics-archives/journal.journal.gz"], "/tmp")
+ m.execute("""gunzip /tmp/journal.journal.gz
+ cp /tmp/journal.journal /var/log/journal/*/""")
+ b.reload()
+ b.enter_page("/metrics")
+
+ b.wait_in_text(".metrics-heading", "CPU")
+ b.click("#metrics-hour-1615197600000 button.metrics-events-expander")
+ b.click("#metrics-hour-1615197600000 div.metrics-minute[data-minute='39'] .metrics-events button.spikes_info")
+ b.wait_visible(".cockpit-log-message:contains('Created slice cockpittest.slice.')")
+ b.wait_in_text(".cockpit-logline:first-child .cockpit-log-message", "cpu-piglet")
+ b.click(".cockpit-logline:first-child .cockpit-log-message")
+ b.enter_page("/system/logs")
+ b.wait_in_text(".pf-v5-c-card__title", "cpu-piglet")
+ b.click("li:contains('Logs')")
+ b.wait_visible(".cockpit-log-message:contains('Created slice cockpittest.slice.')")
+
+ b.go("/metrics")
+ b.enter_page("/metrics")
+ # logs exist, should show tight range
+ b.click("button:contains('View detailed logs')")
+ b.enter_page("/system/logs")
+ b.wait_visible(".cockpit-log-message:contains('Created slice cockpittest.slice.')")
+ url = b.eval_js('window.location.hash')
+ self.assertIn("priority=info", url)
+ self.assertIn("since=2021-3-8%2010%3A39%3A0", url)
+ self.assertIn("until=2021-3-8%2010%3A39%3A45", url)
+
+ @testlib.nondestructive
+ @testlib.skipOstree("no PCP support")
+ def testNoDataEnable(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("""mount -t tmpfs tmpfs /var/log/pcp/pmlogger
+ chown -R pcp:pcp /var/log/pcp/pmlogger
+ if selinuxenabled; then restorecon /var/log/pcp/pmlogger; fi""")
+ self.addCleanup(m.execute, "systemctl stop pmlogger; until umount /var/log/pcp/pmlogger; do sleep 1; done")
+
+ self.login_and_go("/metrics")
+
+ b.wait_in_text(".pf-v5-c-empty-state", "Metrics history could not be loaded")
+ b.wait_in_text(".pf-v5-c-empty-state", "pmlogger.service is not running")
+
+ # enable pmlogger in settings dialog from empty state
+ b.click(".pf-v5-c-empty-state button.pf-m-primary")
+ b.wait_visible(self.pcp_dialog_selector)
+ b.wait_visible("#switch-pmlogger:not(:checked)")
+ b.click("#switch-pmlogger")
+ b.wait_visible("#switch-pmlogger:checked")
+ applySettings(b, self.pcp_dialog_selector)
+
+ m.execute("until systemctl is-active pmlogger; do sleep 1; done")
+
+ # there is a transient "No data available" state, but sometimes it's very short, so don't assert that
+
+ # page auto-updates every minute and starts to receive data
+ with self.browser.wait_timeout(90):
+ self.browser.wait_js_cond("ph_count('.metrics-data-cpu.valid-data') >= 1")
+ b.wait_not_present(".pf-v5-c-empty-state")
+
+ b.logout()
+
+ @testlib.nondestructive
+ @testlib.skipOstree("no PCP support")
+ def testNoDataFailed(self):
+ b = self.browser
+ m = self.machine
+
+ m.write("/run/systemd/system/pmlogger.service.d/break.conf", "[Service]\nExecStart=\nExecStart=/bin/false")
+ m.execute(r"""mount -t tmpfs tmpfs /var/log/pcp/pmlogger
+ if selinuxenabled; then restorecon /var/log/pcp/pmlogger; fi
+ systemctl daemon-reload
+ systemctl start pmlogger || true""")
+ self.addCleanup(m.execute,
+ """rm -r /run/systemd/system/pmlogger.service.d/
+ umount /var/log/pcp/pmlogger
+ systemctl daemon-reload""")
+
+ self.login_and_go("/metrics")
+
+ b.wait_in_text(".pf-v5-c-empty-state", "Metrics history could not be loaded")
+ b.wait_in_text(".pf-v5-c-empty-state", "pmlogger.service has failed")
+
+ # Troubleshoot
+ b.click(".pf-v5-c-empty-state button.pf-m-link")
+ b.enter_page("/system/services")
+ b.wait_in_text("#service-details", "pmlogger.service")
+
+ @testlib.nondestructive
+ @testlib.skipOstree("no PCP support")
+ def testLoggerSettings(self):
+ b = self.browser
+ m = self.machine
+
+ # start in defined state
+ m.execute("systemctl enable --now pmlogger")
+ self.addCleanup(m.execute, "systemctl disable --now pmlogger")
+
+ self.login_and_go("/metrics")
+
+ # disable pmlogger in settings dialog from header bar
+ b.click("#metrics-header-section button.pf-m-secondary")
+ b.wait_visible(self.pcp_dialog_selector)
+ b.wait_visible("#switch-pmlogger:checked")
+ b.click("#switch-pmlogger")
+ b.wait_visible("#switch-pmlogger:not(:checked)")
+ applySettings(b, self.pcp_dialog_selector)
+
+ self.assertEqual(m.execute("systemctl is-active pmlogger || true").strip(), "inactive")
+ self.assertEqual(m.execute("systemctl is-enabled pmlogger || true").strip(), "disabled")
+
+ # enable pmlogger in settings dialog from header bar
+ b.click("#metrics-header-section button.pf-m-secondary")
+ b.wait_visible(self.pcp_dialog_selector)
+ b.wait_visible("#switch-pmlogger:not(:checked)")
+ b.click("#switch-pmlogger")
+ b.wait_visible("#switch-pmlogger:checked")
+ applySettings(b, self.pcp_dialog_selector)
+
+ m.execute("until systemctl is-active pmlogger; do sleep 1; done")
+ self.assertEqual(m.execute("systemctl is-enabled pmlogger").strip(), "enabled")
+
+ @testlib.nondestructive
+ @testlib.skipOstree("no PCP support")
+ def testPmProxySettings(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("systemctl start firewalld")
+
+ # Arch Linux has no active zone by default which the firewalld port alert test requires.
+ if m.image == "arch":
+ m.execute("firewall-cmd --zone=public --change-interface eth0 --permanent")
+ m.execute("firewall-cmd --reload")
+
+ redis = redisService(m.image)
+ hostname = m.execute("hostname").strip()
+
+ self.addCleanup(m.execute, f"systemctl stop {redis}")
+
+ def checkEnable(firewalld_alert):
+ b.click("#metrics-header-section button.pf-m-secondary")
+ b.wait_visible(self.pcp_dialog_selector)
+ b.wait_visible("#switch-pmproxy:not(:checked)")
+ b.click('#switch-pmproxy')
+ b.wait_visible('#switch-pmproxy:checked')
+ applySettings(b, self.pcp_dialog_selector)
+ if firewalld_alert:
+ b.wait_visible(".pf-v5-c-alert:contains(pmproxy)")
+ else:
+ b.wait_not_present(".pf-v5-c-alert:contains(pmproxy)")
+ m.execute('while [ $(systemctl is-active pmproxy) = activating ]; do sleep 1; done')
+ self.assertEqual(m.execute("systemctl is-active pmproxy").strip(), "active")
+ self.assertEqual(m.execute(f"systemctl is-active {redis}").strip(), "active")
+ self.assertEqual(m.execute("systemctl is-enabled pmproxy").strip(), "enabled")
+ self.assertIn("redis", m.execute("systemctl show -p Wants --value pmproxy").strip())
+ testlib.wait(lambda: hostname in m.execute("curl --max-time 10 --silent --show-error 'http://localhost:44322/series/labels?names=hostname'"), delay=10, tries=30)
+
+ def checkDisable():
+ b.click("#metrics-header-section button.pf-m-secondary")
+ b.wait_visible(self.pcp_dialog_selector)
+ b.wait_visible('#switch-pmproxy:checked')
+ b.click('#switch-pmproxy')
+ b.wait_visible("#switch-pmproxy:not(:checked)")
+ applySettings(b, self.pcp_dialog_selector)
+ # always clears the firewalld alert
+ b.wait_not_present(".pf-v5-c-alert:contains(pmproxy)")
+ self.assertEqual(m.execute("! systemctl is-active pmproxy").strip(), "inactive")
+ self.assertEqual(m.execute("! systemctl is-enabled pmproxy").strip(), "disabled")
+ # keeps redis running, it's a shared service
+ self.assertEqual(m.execute(f"systemctl is-active {redis}").strip(), "active")
+ # but drops the pmproxy dependency
+ self.assertNotIn("redis", m.execute("systemctl show -p Wants --value pmproxy").strip())
+ m.execute("! curl --silent --show-error --max-time 10 'http://localhost:44322/series/labels?names=hostname' 2>&1")
+
+ # start in a defined state; all test images have pcp and redis pre-installed
+ m.execute(f"systemctl disable --now pmlogger pmie pmproxy {redis}")
+ m.execute("systemctl reset-failed")
+ # ensure pmproxy is not already opened in firewall
+ m.execute("firewall-cmd --remove-service pmproxy; firewall-cmd --permanent --remove-service pmproxy")
+ self.login_and_go("/metrics")
+
+ # pmproxy can't be enabled without pmlogger
+ b.click("#metrics-header-section button.pf-m-secondary")
+ b.wait_visible(self.pcp_dialog_selector)
+ b.wait_visible("#switch-pmlogger:not(:checked)")
+ b.wait_visible("#switch-pmproxy:not(:checked)")
+ b.wait_visible("#switch-pmproxy:disabled")
+ # enable pmlogger
+ b.click('#switch-pmlogger')
+ b.wait_visible('#switch-pmlogger:checked')
+ applySettings(b, self.pcp_dialog_selector)
+ m.execute('while [ $(systemctl is-active pmlogger) = activating ]; do sleep 1; done')
+ self.assertEqual(m.execute("systemctl is-active pmlogger").strip(), "active")
+ b.wait_not_present(".pf-v5-c-alert:contains(pmproxy)")
+
+ checkEnable(firewalld_alert=True)
+ checkDisable()
+
+ # redis already running
+ m.execute(f"systemctl start {redis}")
+ checkEnable(firewalld_alert=True)
+ checkDisable()
+
+ # pmproxy already running; 44322 queries hang without redis and until restart
+ m.execute(f"systemctl disable --now {redis}; systemctl start pmproxy")
+ checkEnable(firewalld_alert=True)
+
+ # without firewalld
+ m.execute("firewall-cmd --remove-service pmproxy; firewall-cmd --permanent --remove-service pmproxy")
+ m.execute("systemctl stop firewalld")
+ self.allow_journal_messages(".*org.fedoraproject.FirewallD1.*disconnected.*")
+ checkDisable()
+ checkEnable(firewalld_alert=False)
+ m.execute("systemctl start firewalld")
+
+ # Go to firewall page from alert
+ checkDisable()
+ checkEnable(firewalld_alert=True)
+ b.click(".pf-v5-c-alert button.pf-m-link")
+ b.enter_page("/network/firewall")
+ b.wait_visible("#firewall-heading")
+ b.go("/metrics")
+ b.enter_page("/metrics")
+
+ # add pmproxy to default zone directly in alert
+ default_zone = m.execute("firewall-cmd --get-default-zone").strip()
+ b.wait_text("#firewalld-request-pmproxy", default_zone)
+ b.click(".pf-v5-c-alert button.pf-m-primary")
+ b.wait_not_present(".pf-v5-c-alert:contains(pmproxy)")
+ self.assertIn("pmproxy", m.execute("firewall-cmd --list-services").strip())
+ self.assertIn("pmproxy", m.execute("firewall-cmd --list-services --permanent").strip())
+
+ # now service is already enabled, does not show alert
+ checkDisable()
+ checkEnable(firewalld_alert=False)
+
+ # firewalld service enabled in permanent config already, does not trip over ALREADY_ENABLED
+ checkDisable()
+ m.execute("firewall-cmd --remove-service pmproxy")
+ checkEnable(firewalld_alert=True)
+ b.click(".pf-v5-c-alert button.pf-m-primary")
+ b.wait_not_present(".pf-v5-c-alert:contains(pmproxy)")
+ self.assertIn("pmproxy", m.execute("firewall-cmd --list-services").strip())
+
+ # error during zone addition: zone disappears underneath us
+ checkDisable()
+ m.execute("""set -eux
+ firewall-cmd --permanent --remove-service pmproxy
+ firewall-cmd --permanent --new-zone=comeandgo
+ systemctl start NetworkManager
+ nmcli con add type dummy con-name fake ifname fake0 ip4 1.2.3.4/24
+ firewall-cmd --permanent --zone public --remove-interface fake0
+ firewall-cmd --permanent --zone comeandgo --add-interface fake0
+ firewall-cmd --reload
+ """)
+ self.addCleanup(m.execute, "nmcli con delete fake; firewall-cmd --permanent --delete-zone comeandgo || true; firewall-cmd --reload")
+ checkEnable(firewalld_alert=True)
+ b.select_PF4("#firewalld-request-pmproxy", "comeandgo")
+ m.execute("firewall-cmd --permanent --delete-zone comeandgo; firewall-cmd --reload")
+ b.click(".pf-v5-c-alert button.pf-m-primary")
+ self.allow_browser_errors("Failed to enable pmproxy in firewalld:.*INVALID_ZONE: comeandgo.*")
+ b.wait_in_text(".pf-v5-c-alert.pf-m-warning", "Failed to enable pmproxy in firewalld")
+ b.wait_in_text(".pf-v5-c-alert.pf-m-warning", "INVALID_ZONE: comeandgo")
+ # close warning
+ b.click(".pf-v5-c-alert.pf-m-warning button.pf-m-plain")
+ b.wait_not_present(".pf-v5-c-alert:contains(pmproxy)")
+
+ # reacts to service changes from outside; this is asynchronous and the dialog deliberately
+ # does not update automatically, so retry a few times
+ def checkEnabled(expected):
+ for _ in range(10):
+ b.click("#metrics-header-section button.pf-m-secondary")
+ b.wait_visible('#switch-pmproxy')
+ found = b.is_present("#switch-pmproxy" + (expected and ":checked" or ":not(:checked)"))
+ b.click(f"{self.pcp_dialog_selector} button.btn-cancel")
+ b.wait_not_present(self.pcp_dialog_selector)
+
+ if found:
+ break
+ time.sleep(1)
+ else:
+ raise testlib.Error("PCP settings dialog did not get expected value")
+
+ m.execute(f"systemctl stop {redis}")
+ checkEnabled(expected=False)
+ m.execute(f"systemctl start {redis}")
+ checkEnabled(expected=True)
+ m.execute("systemctl stop pmproxy")
+ checkEnabled(expected=False)
+ m.execute("systemctl start pmproxy")
+ checkEnabled(expected=True)
+
+
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestCurrentMetrics(testlib.MachineCase):
+ def setUp(self):
+ super().setUp()
+ # packagekit/dnf often eats a lot of CPU; silence it to have better control over CPU usage
+ packagekitd = "/usr/lib/packagekitd" if self.machine.image == "arch" else "/usr/libexec/packagekitd"
+ self.machine.execute(f"systemctl mask packagekit && killall -9 {packagekitd} && killall -9 dnf || true")
+
+ self.addCleanup(self.machine.execute, "systemctl unmask packagekit")
+ # make sure to clean up our test resource consumers on failures
+ self.addCleanup(self.machine.execute, "systemctl stop cockpittest.slice 2>/dev/null || true")
+ self.addCleanup(self.machine.execute, "su - admin -c 'XDG_RUNTIME_DIR=/run/user/$(id -u admin) "
+ "systemctl --user stop cockpittest.slice 2>/dev/null || true'")
+
+ self.busybox_image = self.machine.execute("podman images --format '{{.Repository}}' | grep busybox").strip()
+ login(self)
+
+ def testCPU(self):
+ b = self.browser
+ m = self.machine
+
+ b.wait_timeout(60)
+
+ nproc = m.execute("nproc").strip()
+ b.wait_in_text("#current-cpu-usage", nproc + " CPU")
+ # top CPU core is not visible with just 1 core; our upstream test VMs have only 1 core,
+ # but let's not just assume this for downstream gating/custom VMs
+ if nproc == '1':
+ self.assertFalse(b.is_present("#current-top-cpu-usage"))
+ b.wait_text("#current-cpu-usage-description", "1 CPU")
+ else:
+ b.wait_visible("#current-top-cpu-usage")
+
+ # wait until system settles down
+ b.wait(lambda: b.get_pf_progress_value("#current-cpu-usage") < 20)
+ m.execute("systemd-run --collect --slice cockpittest -p CPUQuota=60% --unit cpu-hog dd if=/dev/urandom of=/dev/null")
+ m.execute("systemd-run --collect --slice cockpittest -p CPUQuota=30% --unit cpu-piglet dd if=/dev/urandom of=/dev/null")
+ b.wait(lambda: b.get_pf_progress_value("#current-cpu-usage") > 75)
+ # no other process in the test VM should take > 30% CPU, by the "settles down" assertion above
+ b.wait_text("table[aria-label='Top 5 CPU services'] tbody tr:nth-of-type(1) td[data-label='Service']", "cpu-hog")
+ b.wait_text("table[aria-label='Top 5 CPU services'] tbody tr:nth-of-type(2) td[data-label='Service']", "cpu-piglet")
+
+ # There might be some other processes which take more resources
+ # Keep this logging so we can easily debug which ones we might need to cleanup
+ try:
+ b.wait(lambda: topServiceValue(self, "Top 5 CPU services", "%", 1) > 50)
+ b.wait(lambda: topServiceValue(self, "Top 5 CPU services", "%", 1) < 70)
+ b.wait(lambda: topServiceValue(self, "Top 5 CPU services", "%", 2) > 20)
+ b.wait(lambda: topServiceValue(self, "Top 5 CPU services", "%", 2) < 40)
+ except BaseException:
+ print(m.execute("top -b -n 1"))
+ raise
+
+ m.execute("systemctl stop cpu-hog cpu-piglet")
+ # should go back to idle usage
+ b.wait(lambda: b.get_pf_progress_value("#current-cpu-usage") < 20)
+ # it could be that the table disappears completely if no service has a noticeable CPU usage;
+ # so don't assume the table exists
+ b.wait_not_in_text("#current-metrics-card-cpu", "cpu-hog")
+ b.wait_not_in_text("#current-metrics-card-cpu", "cpu-piglet")
+
+ # Load is a flex, each part looks like "1 min: 1.41,"; wait until the 1min load is low
+ b.wait(lambda: float(b.text("#load-avg .pf-v5-l-flex div:first-child").split()[-1].rstrip(',')) < 5)
+
+ m.execute("systemd-run --collect --slice cockpittest --unit load-hog sh -ec "
+ " 'for i in `seq 500`; do dd if=/dev/urandom of=/dev/zero bs=100K count=500 status=none & done'")
+ b.wait(lambda: float(b.text("#load-avg .pf-v5-l-flex div:first-child").split()[-1].rstrip(',')) > 15)
+ m.execute("systemctl stop load-hog 2>/dev/null || true") # ok to fail, as the command exits by itself
+
+ container_name = "pod-cpu-hog"
+ m.execute(f"podman run --rm -d --name {container_name} {self.busybox_image} /bin/dd if=/dev/urandom of=/dev/null")
+
+ container_sha = m.execute(f"podman inspect --format '{{{{.Id}}}}' {container_name}").strip()
+ shortid = container_sha[:12]
+
+ # On some test images the container takes a while to show up
+ with b.wait_timeout(300):
+ b.wait_in_text("#current-metrics-card-cpu", f"pod {shortid}")
+ b.wait(lambda: topServiceValue(self, "Top 5 CPU services", "%", 1) > 70)
+
+ # It takes one re-render for the name lookup
+ with b.wait_timeout(30):
+ b.wait_in_text("#current-metrics-card-cpu", f"pod {container_name}")
+
+ m.execute(f"podman stop -t 0 {container_name}")
+
+ # RHEL-8 / CentOS-8's podman user containers do not show up as
+ # libpod-$containerid but as podman-3679.scope.
+ if m.image != "centos-8-stream" and not m.image.startswith("rhel-8"):
+ # copy images for user podman tests; podman insists on user session
+ m.execute(f"podman save {self.busybox_image} | sudo -i -u admin podman load")
+
+ # Test user containers
+ admin_s = ssh_connection.SSHConnection(user="admin",
+ address=m.ssh_address,
+ ssh_port=m.ssh_port,
+ identity_file=m.identity_file)
+ user_container_name = "user-cpu-hog"
+ admin_s.execute(f"podman run --rm -d --name {user_container_name} {self.busybox_image} /bin/dd if=/dev/urandom of=/dev/null")
+
+ container_sha = admin_s.execute(f"podman inspect --format '{{{{.Id}}}}' {user_container_name}").strip()
+ shortid = container_sha[:12]
+
+ # On some test images the container takes a while to show up
+ with b.wait_timeout(300):
+ b.wait_in_text("#current-metrics-card-cpu", f"pod {shortid}")
+ b.wait(lambda: topServiceValue(self, "Top 5 CPU services", "%", 1) > 70)
+
+ # It takes one re-render for the name lookup
+ with b.wait_timeout(30):
+ b.wait_in_text("#current-metrics-card-cpu", f"pod {user_container_name}")
+
+ admin_s.execute(f"podman stop -t 0 {user_container_name}")
+
+ # this settles down slowly, don't wait for becoming really quiet
+ with b.wait_timeout(300):
+ b.wait(lambda: float(b.text("#load-avg .pf-v5-l-flex div:first-child").split()[-1].rstrip(',')) < 10)
+
+ # Files with CPU temperature do not exist, nothing is displayed
+ b.wait_not_present("#current-metrics-card-cpu .temperature")
+
+ # No matching type
+ self.addCleanup(m.execute, "rm -rf /tmp/sensor-sys-class")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon0/name", "BAT0")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon0/temp1_input", "40000")
+ m.execute("mount -o bind /tmp/sensor-sys-class /sys/class")
+ self.addCleanup(m.execute, "umount /sys/class")
+ b.logout()
+ self.login_and_go("/metrics")
+
+ b.wait_not_present("#current-metrics-card-cpu .temperature")
+
+ # create files that contain CPU temperature
+ # ARM
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/name", "cpu_thermal")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_input", "30000")
+
+ b.logout()
+ self.login_and_go("/metrics")
+
+ b.wait_in_text("#current-metrics-card-cpu", "30 °C")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_input", "45000")
+ b.wait_in_text("#current-metrics-card-cpu", "45 °C")
+
+ # AMD
+ m.execute("rm -rf /tmp/sensor-sys-class/hwmon/hwmon1/*")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/name", "k10temp")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_label", "Tctl")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_input", "40000")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_max", "100000")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_label", "Tccd1")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_input", "35000")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_max", "100000")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp3_label", "Tccd3")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp3_input", "30000")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp3_max", "100000")
+
+ b.logout()
+ self.login_and_go("/metrics")
+
+ b.wait_in_text("#current-metrics-card-cpu", "40 °C")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp3_input", "55000")
+ b.wait_in_text("#current-metrics-card-cpu", "55 °C")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_input", "90000")
+ b.wait_visible("#current-metrics-card-cpu .text-color-warning")
+ b.wait_in_text("#current-metrics-card-cpu .text-color-warning", "90 °C")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_input", "45000")
+ # temp2_input cooled down, temp3_input is the hottest again
+ b.wait_in_text("#current-metrics-card-cpu", "55 °C")
+
+ # atk0110 motherboard
+ m.execute("rm -rf /tmp/sensor-sys-class/hwmon/hwmon1/*")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/name", "atk0110")
+ # MB Temperature (temp2_label) will be ignored
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_label", "CPU Temperature")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_input", "50000")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_label", "MB Temperature")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_input", "70000")
+
+ b.logout()
+ self.login_and_go("/metrics")
+
+ b.wait_in_text("#current-metrics-card-cpu", "50 °C")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_input", "95000")
+ b.wait_visible("#current-metrics-card-cpu .text-color-critical")
+ b.wait_in_text("#current-metrics-card-cpu .text-color-critical", "95 °C")
+ # cooled down a little, warning color changes from red to yellow
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_input", "85000")
+ b.wait_visible("#current-metrics-card-cpu .text-color-warning")
+ b.wait_in_text("#current-metrics-card-cpu .text-color-warning", "85 °C")
+
+ # intel
+ m.execute("rm -rf /tmp/sensor-sys-class/hwmon/hwmon1/*")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/name", "coretemp")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_label", "Package id 0")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_input", "60000")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_crit", "100000")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_label", "Core 0")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_input", "50000")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_crit", "100000")
+
+ b.logout()
+ self.login_and_go("/metrics")
+
+ b.wait_in_text("#current-metrics-card-cpu", "60 °C")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_input", "85000")
+ b.wait_visible("#current-metrics-card-cpu .text-color-warning")
+ b.wait_in_text("#current-metrics-card-cpu .text-color-warning", "85 °C")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp2_input", "70000")
+ # cooled down, warning color is not visible
+ b.wait_not_present("#current-metrics-card-cpu .text-color-warning")
+ b.wait_in_text("#current-metrics-card-cpu", "70 °C")
+
+ # add second CPU
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon2/name", "coretemp")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon2/temp1_label", "Package id 0")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon2/temp1_input", "60000")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon2/temp2_label", "Core 0")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon2/temp2_input", "75000")
+
+ b.logout()
+ self.login_and_go("/metrics")
+
+ # CPU 2 is the hottest
+ b.wait_in_text("#current-metrics-card-cpu", "75 °C")
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon2/temp1_input", "80000")
+ b.wait_in_text("#current-metrics-card-cpu", "80 °C")
+ # CPU 1 is the hottest again
+ m.write("/tmp/sensor-sys-class/hwmon/hwmon1/temp1_input", "90000")
+ b.wait_in_text("#current-metrics-card-cpu", "90 °C")
+
+ # Test link to user services
+ # older releases don't have CPU accounting enabled for user services
+ if self.machine.image != "centos-8-stream" and not self.machine.image.startswith("rhel-8-"):
+ m.execute("su - admin -c 'XDG_RUNTIME_DIR=/run/user/$(id -u admin) systemd-run --user --collect --slice cockpittest -p CPUQuota=60% --unit cpu-userhog dd if=/dev/urandom of=/dev/null'")
+ # user services are always running underneath user@1000.service, so these two will compete for row 1 or 2
+ b.wait_in_text("table[aria-label='Top 5 CPU services'] tbody", "cpu-userhog")
+ b.click("table[aria-label='Top 5 CPU services'] tbody tr:contains(cpu-userhog) td[data-label='Service'] a span")
+ b.enter_page("/system/services")
+ b.wait_in_text(".service-name", "/usr/bin/dd if=/dev/urandom of=/dev/null")
+
+ def testMemory(self):
+ b = self.browser
+ m = self.machine
+ # only some images have swap
+ have_swap = m.execute("swapon --show").strip()
+ # wait until RAM usage is initialized
+ b.wait(lambda: b.get_pf_progress_value("#current-memory-usage") > 10)
+
+ # our test machines should use a reasonable chunk of available memory
+ initial_usage = b.get_pf_progress_value("#current-memory-usage")
+ self.assertGreater(initial_usage, 10)
+ self.assertLess(initial_usage, 80)
+ # allocate a chunk of memory; this may cause other stuff to get unmapped,
+ # thus not exact addition, but usage should go up
+ size = 300 if have_swap else 200 # MB
+ self.write_file("/usr/local/bin/memhog.sh", f"""#!/usr/bin/awk -f
+BEGIN {{
+ x = sprintf("%{size}000000s","");
+ system("touch /tmp/hogged; sleep infinity")
+}}""", perm="755")
+
+ m.execute("systemd-run --collect --slice cockpittest --unit mem-hog memhog.sh")
+ m.execute("while [ ! -e /tmp/hogged ]; do sleep 1; done")
+ # bars update every 3s
+ time.sleep(8)
+ hog_usage = b.get_pf_progress_value("#current-memory-usage")
+ self.assertGreater(hog_usage, initial_usage + 8)
+
+ b.wait_text("table[aria-label='Top 5 memory services'] tbody tr:nth-of-type(1) td[data-label='Service']", "mem-hog")
+ b.wait(lambda: topServiceValue(self, "Top 5 memory services", "Used", 1) > size)
+ b.wait(lambda: topServiceValue(self, "Top 5 memory services", "Used", 1) < size + 50)
+
+ # total memory is shown as tooltip
+ b.mouse("#current-memory-usage", "mouseenter")
+ b.wait_in_text(".pf-v5-c-tooltip", "B total")
+ b.mouse("#current-memory-usage", "mouseleave")
+
+ # table entries are links to Services page
+ b.click("table[aria-label='Top 5 memory services'] tbody tr:nth-of-type(1) td[data-label='Service'] a span")
+ b.enter_page("/system/services")
+ b.wait_in_text("#path", "/mem-hog.service")
+ b.wait_in_text(".service-name", "memhog.sh")
+
+ b.go("/metrics")
+ b.enter_page("/metrics")
+ b.wait_visible("table[aria-label='Top 5 memory services']")
+
+ if have_swap:
+ usage_hog1 = b.get_pf_progress_value("#current-memory-usage")
+
+ # use even more memory to trigger swap
+ m.execute("systemd-run --collect --slice cockpittest --unit mem-hog2 awk "
+ """'BEGIN { x = sprintf("%700000000s",""); system("sleep infinity") }'""")
+ b.wait(lambda: b.get_pf_progress_value("#current-swap-usage") > 0)
+
+ m.execute("systemctl stop mem-hog mem-hog2")
+
+ # after stopping both hogs, usage should go down
+ b.wait(lambda: b.get_pf_progress_value("#current-memory-usage") < usage_hog1)
+ self.assertGreater(b.get_pf_progress_value("#current-memory-usage"), 10)
+ b.wait_not_in_text("table[aria-label='Top 5 memory services'] tbody", "mem-hog")
+
+ # total swap is shown as tooltip
+ b.mouse("#current-swap-usage", "mouseenter")
+ b.wait_in_text(".pf-v5-c-tooltip", "B total")
+ b.mouse("#current-swap-usage", "mouseleave")
+ else:
+ m.execute("systemctl stop mem-hog")
+
+ m.execute("rm /tmp/hogged")
+
+ # Test Podman containers
+ container_name = "pod-mem-hog"
+ # pipe to tail to keep the data in memory
+ m.execute(f"""
+ podman run --rm -d --name {container_name} {self.busybox_image} /bin/sh -c '
+ head -c 300m /dev/zero | tail | sleep infinity'""")
+
+ # It takes one re-render for the name lookup
+ with b.wait_timeout(30):
+ b.wait_text("table[aria-label='Top 5 memory services'] tbody tr:nth-of-type(1) td[data-label='Service']", f"pod {container_name}")
+
+ m.execute(f"podman stop -t 0 {container_name}")
+
+ # RHEL-8 / CentOS-8's podman user containers do not show up as
+ # libpod-$containerid but as podman-3679.scope.
+ if m.image != "centos-8-stream" and not m.image.startswith("rhel-8"):
+ # copy images for user podman tests; podman insists on user session
+ m.execute(f"podman save {self.busybox_image} | sudo -i -u admin podman load")
+
+ # Test user containers
+ admin_s = ssh_connection.SSHConnection(user="admin",
+ address=m.ssh_address,
+ ssh_port=m.ssh_port,
+ identity_file=m.identity_file)
+ user_container_name = "user-mem-hog"
+ admin_s.execute(f"""
+ podman run --rm -d --name {user_container_name} {self.busybox_image} /bin/sh -c '
+ head -c 300m /dev/zero | tail | sleep infinity'
+ """)
+
+ # It takes one re-render for the name lookup
+ with b.wait_timeout(30):
+ b.wait_text("table[aria-label='Top 5 memory services'] tbody tr:nth-of-type(2) td[data-label='Service']", f"pod {user_container_name}")
+
+ admin_s.execute(f"podman stop -t 0 {user_container_name}")
+
+ # Test link to user services
+ # older releases don't have memory accounting enabled for user services
+ if m.image != "centos-8-stream" and not m.image.startswith("rhel-8"):
+ m.execute("su - admin -c 'XDG_RUNTIME_DIR=/run/user/$(id -u admin) systemd-run --user --collect --slice cockpittest --unit mem-userhog memhog.sh'")
+ m.execute("while [ ! -e /tmp/hogged ]; do sleep 1; done")
+ # user services are always running underneath user@1000.service, so these two will compete for row 1 or 2
+ b.wait_in_text("table[aria-label='Top 5 memory services'] tbody", "mem-userhog")
+ b.click("table[aria-label='Top 5 memory services'] tbody tr:contains(mem-userhog) td[data-label='Service'] a span")
+ b.enter_page("/system/services")
+ b.wait_in_text(".service-name", "memhog.sh")
+
+ def testDiskIO(self):
+ b = self.browser
+ m = self.machine
+ login(self)
+
+ b.wait_timeout(60)
+
+ # test env should be quiet enough to not transmit MB/s
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("#current-disks-read")))
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("#current-disks-write")))
+ # reading lots of data
+ m.execute("systemd-run --collect --slice cockpittest --unit disk-read-hog sh -ec 'while true; do echo 3 > /proc/sys/vm/drop_caches; grep -r . /usr >/dev/null; done'")
+ b.wait(lambda: re.match(r'^[0-9.]+ (MB|GB)/s$', b.text("#current-disks-read")))
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("#current-disks-write"))) # this should stay calm
+ # read in popover
+ b.click("#current-metrics-card-disks .all-disks-no-gap button")
+ b.wait_visible(".pf-v5-c-popover .disks-nowrap")
+ b.wait(lambda: re.match(r'^[0-9.]+ (MB|GB)/s$', b.text("[aria-label='Disks usage'] [device-name='vda'] [data-label='Read']")))
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("[aria-label='Disks usage'] [device-name='vda'] [data-label='Write']"))) # write should stay calm
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("[aria-label='Disks usage'] [device-name='sr0'] [data-label='Read']"))) # other disks should stay calm
+ # top service should be disk-read-hog
+ # unsupported on rhel 8 and centos 8 as they use cgroupv1
+ if m.image != "centos-8-stream" and not m.image.startswith("rhel-8"):
+ b.wait_text_matches("table[aria-label='Top 5 disk usage services'] tr:first-child td[data-label='Service']", "disk-read-hog")
+ b.wait(lambda: re.match(r'^[0-9.]+ (MB|GB)/s$', b.text("table[aria-label='Top 5 disk usage services'] tr:first-child td[data-label='Read']")))
+ b.wait(lambda: re.match(r'^0|([0-9.]+ (kB|B)/s)$', b.text("table[aria-label='Top 5 disk usage services'] tr:first-child td[data-label='Write']"))) # this should stay calm
+
+ m.execute("systemctl stop disk-read-hog")
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("[aria-label='Disks usage'] [device-name='vda'] [data-label='Read']"))) # back to quiet
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("#current-disks-read"))) # back to quiet
+ b.click(".pf-v5-c-popover__close > button")
+ # writing lots of data
+ m.execute("systemd-run --collect --slice cockpittest --unit disk-write-hog sh -ec "
+ " 'while true; do dd if=/dev/zero of=/var/tmp/blob bs=1M count=100; done'")
+ self.addCleanup(m.execute, "rm -f /var/tmp/blob")
+ b.wait(lambda: re.match(r'^[0-9.]+ (MB|GB)/s$', b.text("#current-disks-write")))
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("#current-disks-read"))) # this should stay calm
+ # write in popover
+ b.click("#current-metrics-card-disks .all-disks-no-gap button")
+ b.wait(lambda: re.match(r'^[0-9.]+ (MB|GB)/s$', b.text("[aria-label='Disks usage'] [device-name='vda'] [data-label='Write']")))
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("[aria-label='Disks usage'] [device-name='vda'] [data-label='Read']"))) # read should stay calm
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("[aria-label='Disks usage'] [device-name='sr0'] [data-label='Write']"))) # other disks should stay calm
+ # top service should be disk-write-hog
+ # unsupported on rhel 8 and centos 8 as they use cgroupv1
+ if m.image != "centos-8-stream" and not m.image.startswith("rhel-8"):
+ b.wait_text_matches("table[aria-label='Top 5 disk usage services'] tr:first-child td[data-label='Service']", "disk-write-hog")
+ b.wait(lambda: re.match(r'^[0-9.]+ (MB|GB)/s$', b.text("table[aria-label='Top 5 disk usage services'] tr:first-child td[data-label='Write']")))
+ b.wait(lambda: re.match(r'^0|([0-9.]+ (kB|B)/s)$', b.text("table[aria-label='Top 5 disk usage services'] tr:first-child td[data-label='Read']"))) # this should stay calm
+
+ m.execute("systemctl stop disk-write-hog")
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("[aria-label='Disks usage'] [device-name='vda'] [data-label='Write']"))) # back to quiet
+ b.wait(lambda: re.match(r'^(0|[0-9.]+ (kB|B)/s)$', b.text("#current-disks-write"))) # back to quiet
+ b.click(".pf-v5-c-popover__close > button")
+ # top service should be podman container busybox-write-hog
+ m.execute(f"podman run --rm -d --name busybox-write-hog {self.busybox_image} /bin/ash -c 'while true; do dd if=/dev/urandom of=/testfile bs=20M count=100; done'")
+ self.addCleanup(m.execute, "podman rm -f busybox-write-hog || true")
+ b.wait_text_matches("table[aria-label='Top 5 disk usage services'] tr:first-child td[data-label='Service']", "pod busybox-write-hog")
+ b.wait(lambda: re.match(r'^[0-9.]+ (MB|GB)/s$', b.text("table[aria-label='Top 5 disk usage services'] tr:first-child td[data-label='Write']")))
+ b.wait(lambda: re.match(r'^0|([0-9.]+ (kB|B)/s)$', b.text("table[aria-label='Top 5 disk usage services'] tr:first-child td[data-label='Read']"))) # this should stay calm
+ m.execute('podman stop busybox-write-hog')
+
+ # Disk usage
+
+ # add 50 MB loopback disk; mount it once rw and once ro
+ m.execute("""
+ F=$(mktemp /var/tmp/loop.XXXX)
+ dd if=/dev/zero of=$F bs=1M count=50
+ mkfs -t ext3 $F
+ mkdir -p /var/cockpittest /var/cockpit-ro-test
+ mount -o loop $F /var/cockpittest
+ RODEV=$(losetup -f --show $F)
+ mount -r $RODEV /var/cockpit-ro-test
+ losetup -d $RODEV
+ rm $F""")
+ self.addCleanup(m.execute, "umount /var/cockpittest /var/cockpit-ro-test")
+
+ self.assertLess(b.get_pf_progress_value(".pf-v5-c-progress[data-disk-usage-target='/var/cockpittest']"), 5)
+ progress_sel = ".pf-v5-c-progress[data-disk-usage-target='/var/cockpittest'] .pf-v5-c-progress__status"
+ # free size is anything between 40 and 50 MB
+ self.assertRegex(b.text(progress_sel), r"^4\d\.\d MB free$")
+ # total size is shown in tooltip
+ b.mouse(progress_sel, "mouseenter")
+ b.wait_in_text(".pf-v5-c-tooltip", "total")
+ # total size is anything between 40 and 50 MB
+ self.assertRegex(b.text(".pf-v5-c-tooltip"), r"^4\d\.\d MB total$")
+ b.mouse(progress_sel, "mouseleave")
+ # read-only loop devices are not shown
+ self.assertFalse(b.is_present(".pf-v5-c-progress[data-disk-usage-target='/var/cockpit-ro-test']"))
+
+ m.execute("dd if=/dev/zero of=/var/cockpittest/blob bs=1M count=40")
+ b.wait(lambda: b.get_pf_progress_value(".pf-v5-c-progress[data-disk-usage-target='/var/cockpittest']") >= 90)
+
+ # clicking on progress leads to the storage page
+ if not m.ostree_image:
+ self.assertTrue(b.is_present("#current-disks-usage button"))
+ b.click(progress_sel)
+ b.enter_page("/storage")
+ # weird -- storage page does not show transient mount points, only permanent ones; so check for the device
+ dev = m.execute("findmnt --noheadings -o SOURCE /var/cockpittest").strip()
+ b.wait_in_text('[data-test-card-title="Storage"]', dev.replace("/dev/", ""))
+
+ b.go("/metrics")
+ b.enter_page("/metrics")
+ b.wait_visible(progress_sel)
+ b.logout()
+
+ # without cockpit-storaged, mounts are not links
+ self.restore_file("/usr/share/cockpit/storaged/manifest.json")
+ m.write("/usr/share/cockpit/storaged/manifest.json", "")
+ if self.is_pybridge():
+ self.allow_journal_messages(".*/storaged/manifest.json: Expecting value: line 1 column 1.*")
+ else:
+ self.allow_journal_messages("storaged: couldn't read manifest.json: JSON data was empty")
+ login(self)
+ b.wait_visible(progress_sel)
+ self.assertFalse(b.is_present("#current-disks-usage button"))
+
+ @testlib.skipOstree("no netcat on CoreOS")
+ def testNetwork(self):
+ b = self.browser
+ m = self.machine
+
+ # add synthetic veth which is guaranteed quiet
+ m.execute("ip link add name cockpittest1 type veth peer name vcockpittest1")
+ self.addCleanup(m.execute, "ip link del dev cockpittest1")
+
+ # has expected interfaces
+ b.wait_in_text("[aria-label='Network usage'] [data-interface='cockpittest1']", "cockpittest1")
+ b.wait_in_text("[aria-label='Network usage'] [data-interface='lo']", "lo")
+
+ # can jump to network interface details
+ b.wait_visible("[aria-label='Network usage'] [data-interface='cockpittest1'] button")
+ b.wait_visible("[aria-label='Network usage'] [data-interface='lo'] button")
+
+ def rateMatches(label, regexp):
+ text = b.text(f"[aria-label='Network usage'] [data-interface='lo'] td[data-label='{label}']")
+ return re.match(regexp, text) is not None
+
+ # loopback is quiet enough to not transmit MB/s
+ b.wait(lambda: rateMatches("In", r'^(0|[0-9.]+ (kB|B)/s)$'))
+ b.wait(lambda: rateMatches("Out", r'^(0|[0-9.]+ (kB|B)/s)$'))
+ # pipe lots of data through lo
+ m.execute("systemd-run --collect --slice cockpittest --unit lo-hog sh -ec "
+ " 'nc -n -vv -l 2000 > /dev/null & sleep 1; nc -vv localhost 2000 </dev/zero'")
+ b.wait(lambda: rateMatches("In", r'^[0-9.]+ (MB|GB)/s$'))
+ b.wait(lambda: rateMatches("Out", r'^[0-9.]+ (MB|GB)/s$'))
+ m.execute("systemctl stop lo-hog")
+
+ # nothing happens on cockpittest1
+ b.wait_text("[aria-label='Network usage'] [data-interface='cockpittest1'] td[data-label='In']", "0")
+ b.wait_text("[aria-label='Network usage'] [data-interface='cockpittest1'] td[data-label='Out']", "0")
+
+ b.click("[aria-label='Network usage'] [data-interface='lo'] button")
+ b.enter_page("/network")
+ b.wait_visible("#network-interface-name:contains('lo')")
+ b.wait_in_text(".network-interface-details", "This device cannot be managed here.")
+
+ b.go("/metrics")
+ b.enter_page("/metrics")
+ b.click("[aria-label='Network usage'] [data-interface='cockpittest1'] button")
+ b.enter_page("/network")
+ b.wait_visible("#network-interface-name:contains('cockpittest1')")
+ b.wait_in_text(".network-interface-details", "This device cannot be managed here.")
+
+
+@testlib.skipImage("TODO: Arch Linux packagekit support", "arch")
+@testlib.skipDistroPackage()
+class TestMetricsPackages(packagelib.PackageCase):
+ # HACK: PF modal can have multiple id's in certain scenario's https://github.com/patternfly/patternfly-react/issues/9399
+ pcp_dialog_selector = "#pcp-settings-modal:first-child"
+
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ if m.ostree_image:
+ self.login_and_go("/metrics")
+ b.wait_in_text(".pf-v5-c-empty-state", "cockpit-pcp is missing")
+ b.wait_not_present(".pf-v5-c-empty-state button.pf-m-primary")
+
+ b.click("#metrics-header-section button.pf-m-secondary")
+ b.wait_visible(self.pcp_dialog_selector)
+ b.wait_visible("#switch-pmlogger:not(:checked)")
+ # no packagekit, can't enable
+ b.wait_visible("#switch-pmlogger:disabled")
+ b.wait_visible("#switch-pmproxy:disabled")
+ return
+
+ if m.image.startswith("debian") or m.image.startswith("ubuntu"):
+ m.execute("dpkg --purge cockpit-pcp-dbgsym || true; dpkg --purge cockpit-pcp pcp redis redis-server")
+ # HACK: pcp does not clean up correctly on Debian https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=986074
+ m.execute("rm -f /etc/systemd/system/pmlogger.service.requires/pmlogger_farm.service")
+ else:
+ m.execute("rpm --erase --verbose cockpit-pcp pcp redis")
+ if "centos-8" in m.image or "rhel-8" in m.image:
+ # RHEL 8 ships this in a module, make sure that doesn't hide our fake package
+ m.execute("dnf module disable -y redis || true")
+
+ redis_service = redisService(m.image)
+ dummy_service = "[Service]\nExecStart=/bin/sleep infinity\n[Install]\nWantedBy=multi-user.target\n"
+
+ cpcp_content = {
+ "/usr/share/cockpit/pcp/manifest.json": '{"requires": {"cockpit": "135"}, "bridges": [{"match": { "payload": "metrics1"},"spawn": [ "/usr/libexec/cockpit-pcp" ]}]}',
+ "/usr/libexec/cockpit-pcp": "true",
+ }
+ pcp_content = {
+ "/lib/systemd/system/pmlogger.service": dummy_service,
+ "/lib/systemd/system/pmproxy.service": dummy_service,
+ }
+ redis_content = {
+ f"/lib/systemd/system/{redis_service}.service": dummy_service,
+ }
+
+ self.createPackage("cockpit-pcp", "999", "1", content=cpcp_content, depends="pcp",
+ postinst="chmod +x /usr/libexec/cockpit-pcp")
+ self.createPackage("pcp", "999", "1", content=pcp_content, postinst="systemctl daemon-reload")
+ self.createPackage("redis", "999", "1", content=redis_content, postinst="systemctl daemon-reload")
+ self.enableRepo()
+ m.execute("pkcon refresh")
+
+ # install c-pcp from the empty state
+ self.login_and_go("/metrics")
+ b.wait_in_text(".pf-v5-c-empty-state", "cockpit-pcp is missing")
+ b.click(".pf-v5-c-empty-state button.pf-m-primary")
+ b.click("#dialog button:contains('Install')")
+ b.wait_not_present("#dialog")
+ b.click("button:contains('Log out')")
+ b.leave_page()
+ b.click("button:contains('Reconnect')")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.enter_page("/metrics")
+ b.wait_in_text(".pf-v5-c-empty-state", "Metrics history could not be loaded")
+ b.logout()
+
+ # install c-pcp from the Metrics Settings dialog
+ m.execute("pkcon remove -y cockpit-pcp pcp")
+ self.login_and_go("/metrics")
+ b.click("#metrics-header-section button.pf-m-secondary")
+ b.wait_visible(self.pcp_dialog_selector)
+ b.wait_visible("#switch-pmlogger:not(:checked)")
+ b.click("#switch-pmlogger")
+ b.wait_visible("#switch-pmlogger:checked")
+ applySettings(b, self.pcp_dialog_selector)
+ # install dialog
+ b.click("#dialog button:contains('Install')")
+ b.wait_not_present("#dialog")
+ # sets up pmlogger correctly; this is asynchronous, as it happens in the background after closing install dialog
+ m.execute('until [ $(systemctl is-enabled pmlogger) = enabled ]; do sleep 1; done')
+ # also needs to wait for activating → active
+ m.execute('until [ $(systemctl is-active pmlogger) = active ]; do sleep 1; done')
+ # triggers "needs logout"
+ b.click("button:contains('Log out')")
+ b.leave_page()
+ b.click("button:contains('Reconnect')")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.enter_page("/metrics")
+ # this is just a fake cockpit-pcp package
+ b.wait_in_text(".pf-v5-c-empty-state", "Metrics history could not be loaded")
+ b.wait_in_text(".pf-v5-c-empty-state", "pmlogger.service is failing to collect data")
+
+ # install redis
+ b.click("#metrics-header-section button.pf-m-secondary")
+ b.wait_visible(self.pcp_dialog_selector)
+ b.wait_visible("#switch-pmproxy:not(:checked)")
+ b.click("#switch-pmproxy")
+ b.wait_visible("#switch-pmproxy:checked")
+ applySettings(b, self.pcp_dialog_selector)
+ # install dialog
+ b.click("#dialog button:contains('Install')")
+ b.wait_not_present("#dialog")
+ # sets up redis correctly; this is asynchronous, as it happens in the background after closing install dialog
+ m.execute('until [ $(systemctl is-enabled pmproxy) = enabled ]; do sleep 1; done')
+ m.execute('until [ $(systemctl is-active pmproxy) = active ]; do sleep 1; done')
+ m.execute(f'until [ $(systemctl is-active {redis_service}) = active ]; do sleep 1; done')
+ self.assertIn("redis", m.execute("systemctl show -p Wants --value pmproxy").strip())
+
+
+@testlib.skipDistroPackage()
+class TestMultiCPU(testlib.MachineCase):
+
+ provision = {
+ "0": {"cpus": 2}
+ }
+
+ @testlib.skipOstree("no PCP support")
+ def testCPUUsage(self):
+ b = self.browser
+ m = self.machine
+
+ prepareArchive(m, "2corescpu.tar.gz", 1598971635)
+ login(self)
+
+ # one core is busy, the other idle -- that should be 50% total usage
+ self.assertGreaterEqual(getCompressedMinuteValue(test=self, g_type="cpu", saturation=False, hour=1598968800000, minute=44), 0.2)
+ self.assertLessEqual(getCompressedMinuteValue(test=self, g_type="cpu", saturation=False, hour=1598968800000, minute=44), 0.55)
+
+ # next minute, both cores are busy
+ self.assertGreaterEqual(getMaximumSpike(test=self, g_type="cpu", saturation=False, hour=1598968800000, minute=45), 0.5)
+ self.assertLessEqual(getMaximumSpike(test=self, g_type="cpu", saturation=False, hour=1598968800000, minute=45), 1.0)
+
+ b.wait_timeout(60)
+
+ # Test current usage of cores
+ b.wait_text("#current-cpu-usage-description", "2 CPUs")
+ b.wait(lambda: b.get_pf_progress_value("#current-cpu-usage") < 20)
+ m.execute("systemd-run --collect --slice cockpittest -p CPUQuota=50% --unit cpu-hog dd if=/dev/urandom of=/dev/null")
+ m.execute("systemd-run --collect --slice cockpittest -p CPUQuota=20% --unit cpu-piglet dd if=/dev/urandom of=/dev/null")
+ # View all CPUs
+ b.click("#current-metrics-card-cpu button")
+ b.wait(lambda: int(b.text(".pf-v5-c-popover .cpu-all dd:nth-of-type(1)")[:-1]) > 40)
+ b.wait(lambda: int(b.text(".pf-v5-c-popover .cpu-all dd:nth-of-type(2)")[:-1]) > 15)
+ b.click(".pf-v5-c-popover button")
+ b.wait_not_present(".pf-v5-c-popover")
+
+ # the top CPU core runs cpu-hog
+ b.wait(lambda: b.get_pf_progress_value("#current-top-cpu-usage") >= 38)
+ # the hoglet gets scheduled between core 1 and 2
+ b.wait(lambda: b.get_pf_progress_value("#current-top-cpu-usage") <= 80)
+ # looks like "average: 45% max: 60%"
+ b.wait(lambda: int(b.text("#current-cpu-usage .pf-v5-c-progress__status").split()[-1].rstrip('%')) >= 38)
+ b.wait(lambda: int(b.text("#current-cpu-usage .pf-v5-c-progress__status").split()[-1].rstrip('%')) <= 80)
+
+
+@testlib.skipOstree("no PCP support")
+@testlib.skipDistroPackage()
+class TestGrafanaClient(testlib.MachineCase):
+
+ provision = {
+ "0": {"address": "10.111.112.1/20", "dns": "10.111.112.1", "memory_mb": 512},
+ # forward Grafana port, so that a developer can connect to it with local browser
+ "services": {"image": "services", "forward": {"3000": 3000}, "memory_mb": 512}
+ }
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+ mg = self.machines['services']
+
+ # HACK: PF modal can have multiple id's in certain scenario's https://github.com/patternfly/patternfly-react/issues/9399
+ pcp_dialog_selector = "#pcp-settings-modal:first-child"
+
+ # avoid dynamic host name changes during PCP data collection, and start from clean slate
+ m.execute("""systemctl stop pmlogger || true
+ systemctl reset-failed pmlogger || true
+ rm -rf /var/log/pcp/pmlogger
+ hostnamectl set-hostname grafana-client""")
+
+ # start Grafana
+ mg.execute("/root/run-grafana")
+ m.execute("until curl --silent --show-error http://10.111.112.100:3000; do sleep 1; done")
+ # enable PCP plugin; like on Cog (Configuration) menu → Plugins → Performance Co-Pilot → Enable
+ mg.execute("curl --silent --show-error -u admin:foobar -d '' 'http://127.0.0.1:3000/api/plugins/performancecopilot-pcp-app/settings?enabled=true'")
+ self.login_and_go("/metrics")
+
+ # pmlogger data collection is not running initially
+ b.wait_in_text(".pf-v5-c-empty-state", "Metrics history could not be loaded")
+ b.wait_in_text(".pf-v5-c-empty-state", "pmlogger.service is not running")
+ b.click(".pf-v5-c-empty-state button.pf-m-primary")
+ b.wait_visible(pcp_dialog_selector)
+ b.wait_visible("#switch-pmlogger:not(:checked)")
+ b.click("#switch-pmlogger")
+ b.wait_visible("#switch-pmlogger:checked")
+ applySettings(b, pcp_dialog_selector)
+
+ # enable pmproxy+redis (none of our test OSes have both of them running by default)
+ b.click("#metrics-header-section button.pf-m-secondary")
+ b.wait_visible(pcp_dialog_selector)
+ b.wait_visible("#switch-pmproxy:not(:checked)")
+ b.click('#switch-pmproxy')
+ b.wait_visible('#switch-pmproxy:checked')
+ applySettings(b, pcp_dialog_selector)
+
+ # enable pmproxy service in firewalld in the alert
+ b.wait_visible("#firewalld-request-pmproxy")
+ b.click(".pf-v5-c-alert button.pf-m-primary")
+
+ # Log into Grafana (usually http://127.0.0.2:3002 if you do it interactively)
+ bg = testlib.Browser(mg.forward['3000'], label=self.label() + "-" + mg.label, machine=self)
+ try:
+ bg.open("/")
+ bg.wait_in_text("body", "Welcome to Grafana")
+ bg.set_input_text("input[name='user']", "admin")
+ bg.set_input_text("input[name='password']", "foobar")
+ bg.click("button:contains('Log in')")
+ bg.wait_in_text("body", "Add your first data source")
+
+ # Add the PCP redis data source for our client machine
+ # Cog (Configuration) menu → Data Sources → Add
+ # Select PCP redis, HTTP URL http://10.111.112.1:44322
+ redis_url = 'http://10.111.112.1:44322'
+ bg.open("/datasources/new")
+ bg.wait_visible("[aria-label='Add new data source PCP Redis']")
+ bg.click("[aria-label='Add new data source PCP Redis']")
+ bg.set_input_text("input[placeholder='http://localhost:44322']", redis_url)
+ bg.click("button:contains('Save &')") # Save & [tT]est
+ bg.wait_in_text("body", "Data source is working")
+
+ # Grafana auto-discovers "host" variable for incoming metrics; it takes a while to receive the first
+ # measurement; that event is not observable directly in Grafana, and the dashboard does not auto-update to
+ # new variables; so probe the API until it appears
+ testlib.wait(lambda: "grafana-client" in mg.execute(f"curl --max-time 10 --silent --show-error '{redis_url}/series/labels?names=hostname'"), delay=10, tries=30)
+ # ... and the load metrics as well
+ testlib.wait(lambda: mg.execute(f"curl --max-time 10 --silent --show-error '{redis_url}/series/query?expr=kernel.all.load'").strip() != '[]', delay=10, tries=30)
+
+ # Switch to "Dashboards" tab, import "Host Overview"
+ bg.click("a[href$='/dashboards'][role=tab]")
+ with bg.wait_timeout(60):
+ bg.wait_not_in_text("body", "Loading")
+ bg.click("tr:contains('PCP Redis: Host Overview') button:contains('Import')")
+ bg.wait_visible("tr:contains('PCP Redis: Host Overview') button:contains('Re-import')")
+
+ # .. and the dashboard name becomes clickable
+ bg.click("a:contains('PCP Redis: Host Overview')")
+
+ bg.wait_in_text("#var-host", "grafana-client")
+
+ # expect a "Load average" panel with a sensible number
+ max_load = bg.text("div:contains('Load average') .graph-legend-series:contains('1 minute') .max")
+ self.assertGreater(float(max_load), 0)
+ except Exception:
+ bg.snapshot("FAIL-grafana")
+ raise
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-basic b/test/verify/check-networkmanager-basic
new file mode 100755
index 0000000..1eab509
--- /dev/null
+++ b/test/verify/check-networkmanager-basic
@@ -0,0 +1,332 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import netlib
+import testlib
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestNetworkingBasic(netlib.NetworkCase):
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ if not m.ostree_image:
+ # screenshot assumes running firewalld and absent virbr0 (in particular, *3* interfaces)
+ m.execute("systemctl start firewalld")
+
+ # ensure PCP is on for pixel tests, so that we get the zoom/range selectors
+ if b.pixels_label:
+ m.execute("systemctl start pmlogger")
+
+ self.login_and_go("/network")
+ b.wait_visible("#networking")
+
+ iface = 'cockpit1'
+ self.add_veth(iface, dhcp_cidr="10.111.113.2/20")
+ self.nm_activate_eth(iface)
+ self.wait_for_iface(iface)
+ b.wait_not_present("#network-interface")
+
+ b.wait_in_text("#networking-graphs", "Transmitting")
+ b.wait_in_text("#networking-graphs", "Receiving")
+
+ # FIXME: rtl test was flaky, debug it and remove the skip
+ b.assert_pixels(
+ "#networking", "network-main",
+ [
+ "td[data-label=Sending]",
+ "td[data-label=Receiving]",
+ "td[data-label='IP address']",
+ "#networking-graphs .pf-v5-l-grid",
+ ".cockpit-log-panel .pf-v5-c-card__body",
+ "#networking-firewall-summary .pf-v5-c-card__body",
+ ],
+ # IPv6 addresses vary wildly, and their different rendered widths change the column widths
+ skip_layouts=['medium', 'mobile', 'rtl']
+ )
+
+ # Details of test iface
+ self.select_iface(iface)
+ b.wait_visible("#network-interface")
+ b.assert_pixels("#network-interface .network-interface-details",
+ "network-interface",
+ mock={
+ "#network-interface-mac": "11:22:33:44:55:66",
+ ".networking-interface-status": "12.123.123.123/20, fedc:0:0:0:1234:ff:abcd:1234/64",
+ })
+
+ con_id = testlib.wait(lambda: self.iface_con_id(iface))
+
+ # Toggle "Connect automatically"
+ #
+ b.wait_visible('#autoreconnect:checked')
+ b.set_checked('#autoreconnect', val=False)
+ self.assertEqual(m.execute(f"nmcli -g connection.autoconnect con show {con_id}"), "no\n")
+ b.wait_visible('#autoreconnect:not(:checked)')
+ b.set_checked('#autoreconnect', val=True)
+ b.wait_visible('#autoreconnect:checked')
+ self.assertEqual(m.execute(f"nmcli -g connection.autoconnect con show {con_id}"), "yes\n")
+
+ # Configure a manual IPv4 address
+ self.configure_iface_setting('IPv4')
+ b.wait_visible("#network-ip-settings-dialog")
+ b.select_from_dropdown("#network-ip-settings-dialog select", "manual")
+ # Manual mode disables automatic switches
+ b.wait_visible("#network-ip-settings-dialog [data-field='dns'] input[type=checkbox]:disabled")
+ b.wait_visible("#network-ip-settings-dialog [data-field='dns_search'] input[type=checkbox]:disabled")
+ b.wait_visible("#network-ip-settings-dialog [data-field='routes'] input[type=checkbox]:disabled")
+
+ b.set_input_text('#network-ip-settings-address-0', "1.2.3.4")
+ b.set_input_text('#network-ip-settings-netmask-0', "255.255.0.8")
+ b.click("#network-ip-settings-save")
+ b.wait_text_not("#network-ip-settings-error h4", "")
+ b.set_input_text('#network-ip-settings-netmask-0', "255.255.192.0")
+ b.click("#network-ip-settings-save")
+ b.wait_not_present("#network-ip-settings-dialog")
+ self.wait_for_iface_setting("IPv4", "Address 1.2.3.4/18 via 1.2.3.1")
+
+ m.execute(f"until ip a show dev {iface} | grep -q 'inet 1.2.3.4/18'; do sleep 0.3; done",
+ timeout=10)
+
+ # Configure manual IPv6 address
+ b.click("#networking-edit-ipv6")
+ b.wait_visible("#network-ip-settings-dialog")
+ b.select_from_dropdown("#network-ip-settings-select-method", "manual")
+ b.set_input_text("#network-ip-settings-address-0", "2001::1")
+ b.set_input_text("#network-ip-settings-netmask-0", "64")
+ b.set_input_text("#network-ip-settings-gateway-0", "::")
+ b.click("button:contains('Save')")
+ b.wait_not_present("#network-ip-settings-dialog")
+ self.wait_for_iface_setting("IPv6", "Address 2001:0:0:0:0:0:0:1/64")
+
+ m.execute(f"until ip a show dev {iface} | grep -q 'inet6 2001::1/64 scope global'; do sleep 0.3; done",
+ timeout=10)
+
+ # Disconnect
+ self.wait_onoff(f".pf-v5-c-card__header:contains('{iface}')", val=True)
+ self.toggle_onoff(f".pf-v5-c-card__header:contains('{iface}')")
+ self.wait_for_iface_setting('Status', 'Inactive')
+
+ # Reconnect through the UI
+ self.toggle_onoff(f".pf-v5-c-card__header:contains('{iface}')")
+ b.wait_in_text(f"#network-interface .pf-v5-c-card:contains('{iface}')", "1.2.3.4/18")
+
+ # Disconnect from the CLI, UI reacts
+ m.execute(f"nmcli device disconnect {iface}")
+ self.wait_onoff(f".pf-v5-c-card__header:contains('{iface}')", val=False)
+
+ # Switch it back to "auto" from the command line and bring it
+ # up again
+ #
+ m.execute(f"nmcli connection modify '{con_id}' ipv4.method auto")
+ m.execute(f"nmcli connection modify '{con_id}' ipv4.gateway ''")
+ m.execute(f"nmcli connection modify '{con_id}' ipv4.addresses ''")
+ self.wait_for_iface_setting('IPv4', "Automatic")
+ m.execute(f"nmcli connection up '{con_id}'")
+ self.wait_for_iface_setting('Status', '10.111.')
+
+ # Switch off automatic DNS
+ #
+ self.configure_iface_setting('IPv4')
+ b.wait_visible("#network-ip-settings-dialog")
+ self.wait_onoff("#network-ip-settings-dialog [data-field='dns']", val=True)
+ self.toggle_onoff("#network-ip-settings-dialog [data-field='dns']")
+ # The "DNS search domains" setting should follow suit
+ self.wait_onoff("#network-ip-settings-dialog [data-field='dns_search']", val=False)
+ b.click("#network-ip-settings-save")
+ b.wait_not_present("#network-ip-settings-dialog")
+
+ testlib.wait(lambda: "yes" in m.execute(f"nmcli -f ipv4.ignore-auto-dns connection show '{con_id}'"))
+
+ def testIpHelper(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+ b.wait_visible("#networking")
+
+ iface = "cockpit1"
+ self.add_veth(iface, dhcp_cidr="10.111.113.2/20")
+ self.nm_activate_eth(iface)
+ self.wait_for_iface(iface)
+ b.click("button:contains('cockpit1')")
+
+ # wait until dialog initialized
+ b.click("#networking-edit-ipv4:contains('edit')")
+ b.wait_visible("#network-ip-settings-dialog")
+
+ b.select_from_dropdown("#network-ip-settings-select-method", "manual")
+ b.wait_visible("#network-ip-settings-address-0")
+ # test class A IP
+ b.set_input_text("#network-ip-settings-address-0", "10.0.5.1")
+ b.wait_val("#network-ip-settings-netmask-0", "255.0.0.0")
+ b.wait_val("#network-ip-settings-gateway-0", "10.0.5.254")
+
+ # test class B IP
+ b.set_input_text("#network-ip-settings-address-0", "172.16.44.2")
+ b.wait_val("#network-ip-settings-netmask-0", "255.255.0.0")
+ b.wait_val("#network-ip-settings-gateway-0", "172.16.44.1")
+ # test class C IP
+ b.set_input_text("#network-ip-settings-address-0", "192.168.1.1")
+ b.wait_val("#network-ip-settings-netmask-0", "255.255.255.0")
+ b.wait_val("#network-ip-settings-gateway-0", "192.168.1.254")
+ # others | Need to manually reset fields
+ b.set_input_text("#network-ip-settings-netmask-0", "")
+ b.set_input_text("#network-ip-settings-gateway-0", "")
+ b.set_input_text("#network-ip-settings-address-0", "225.4.3.2")
+ b.wait_val("#network-ip-settings-netmask-0", "")
+ b.wait_val("#network-ip-settings-gateway-0", "")
+ # free manual edit & save
+ b.set_input_text("#network-ip-settings-address-0", "192.168.1.1")
+ b.set_input_text("#network-ip-settings-netmask-0", "255.255.255.128")
+ b.set_input_text("#network-ip-settings-gateway-0", "192.168.1.126")
+ b.click("#network-ip-settings-save")
+ b.wait_not_present("#network-ip-settings-dialog")
+
+ self.assertIn("192.168.1.1/25", m.execute(f"nmcli -g ipv4.addresses connection show {iface}"))
+ self.assertIn("192.168.1.126", m.execute(f"nmcli -g ipv4.gateway connection show {iface}"))
+
+ def testNoService(self):
+ b = self.browser
+ m = self.machine
+
+ iface = "cockpit42"
+ self.add_veth(iface)
+ self.addCleanup(m.execute, "systemctl enable --now NetworkManager")
+
+ def assert_running():
+ b.wait_not_present("#networking-nm-crashed")
+ b.wait_not_present("#networking-nm-disabled")
+ b.wait_visible("#networking-graphs")
+ b.wait_visible("#networking-interfaces")
+ b.wait_in_text("#networking-interfaces", iface)
+
+ def assert_stopped(enabled):
+ # should hide graphs and actions and show the appropriate notification
+ b.wait_not_present("#networking-graphs")
+ b.wait_not_present("#networking-interfaces")
+ if enabled:
+ b.wait_visible("#networking-nm-crashed")
+ b.wait_not_present("#networking-nm-disabled")
+ else:
+ b.wait_not_present("#networking-nm-crashed")
+ b.wait_visible("#networking-nm-disabled")
+
+ def assert_not_found():
+ # should hide graphs and actions and show the appropriate notification
+ b.wait_not_present("#networking-graphs")
+ b.wait_not_present("#networking-interfaces")
+ b.wait_visible("#networking-nm-not-found")
+
+ self.login_and_go("/network")
+ assert_running()
+
+ # stop/start NM on CLI, page should notice
+ m.execute("systemctl stop NetworkManager")
+ assert_stopped(enabled=True)
+ m.execute("systemctl start NetworkManager")
+ assert_running()
+
+ # stop NM, test inline start button
+ m.execute("systemctl stop NetworkManager")
+ assert_stopped(enabled=True)
+ b.click("#networking-nm-crashed button")
+ assert_running()
+
+ # stop NM, test troubleshoot button
+ m.execute("systemctl stop NetworkManager")
+ assert_stopped(enabled=True)
+ b.click("#networking-nm-crashed a")
+ b.enter_page("/system/services")
+ b.wait_text(".service-name", "Network Manager")
+ b.click(".service-top-panel .pf-v5-c-dropdown button")
+ b.click(".service-top-panel .pf-v5-c-dropdown__menu a:contains('Start')")
+ b.wait_in_text("#statuses", "Running")
+
+ # networking page should notice start from Services page
+ b.go("/network")
+ b.enter_page("/network")
+ assert_running()
+
+ # stop and disable NM, enablement info notification
+ m.execute("systemctl stop NetworkManager; systemctl disable NetworkManager")
+ assert_stopped(enabled=False)
+ b.click("#networking-nm-disabled button")
+ assert_running()
+ testlib.wait(lambda: m.execute("systemctl is-enabled NetworkManager"))
+
+ # /usr in ostree is readonly
+ if not m.ostree_image:
+ # remove the NM service file, test 'no-found' notification
+ # This works for ND tests since there is a self.addCleanup(m.execute, "systemctl enable --now NetworkManager") in the start of the test
+ self.restore_file('/usr/lib/systemd/system/NetworkManager.service')
+ m.execute("rm /usr/lib/systemd/system/NetworkManager.service; systemctl daemon-reload; systemctl stop NetworkManager")
+ assert_not_found()
+
+ self.allow_journal_messages(".*org.freedesktop.NetworkManager.*")
+ # Killing NM affects realmd as well
+ self.allow_journal_messages(".*org.freedesktop.realmd.Service.*")
+
+ def testUnprivileged(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network", superuser=False)
+ b.wait_visible("#networking")
+
+ iface = 'cockpit1'
+ self.add_veth(iface, dhcp_cidr="10.111.113.2/20")
+ self.nm_activate_eth(iface)
+ self.wait_for_iface(iface)
+ con_id = testlib.wait(lambda: self.iface_con_id(iface))
+ mac = m.execute(f"cat /sys/class/net/{iface}/address").strip()
+
+ self.select_iface(iface)
+ b.wait_visible("#network-interface")
+ # On/Off button is disabled, but shows the correct value
+ b.wait_visible(f".pf-v5-c-card__header:contains('{iface}') .pf-v5-c-switch input:disabled")
+ self.wait_onoff(f".pf-v5-c-card__header:contains('{iface}')", val=True)
+
+ # not editable
+ b.wait_visible('#autoreconnect:disabled')
+ b.wait_not_present("#networking-edit-ipv4")
+ b.wait_not_present("#networking-edit-mac")
+ b.wait_text("#network-interface-mac", mac.upper())
+
+ # unpriv UI reacts to system change
+ m.execute(f"nmcli device disconnect {iface}")
+ self.wait_onoff(f".pf-v5-c-card__header:contains('{iface}')", val=False)
+ m.execute(f"nmcli connection up '{con_id}'")
+ self.wait_onoff(f".pf-v5-c-card__header:contains('{iface}')", val=True)
+
+ # UI becomes editable when gaining privileges
+ b.become_superuser()
+ b.wait_visible('#autoreconnect:not(disabled)')
+ b.wait_visible("#networking-edit-ipv4")
+ b.wait_visible("#networking-edit-mac")
+ b.wait_visible(f".pf-v5-c-card__header:contains('{iface}') .pf-v5-c-switch input:not(disabled)")
+
+ # FIXME: if pcp is running, cockpit-pcp throws this warning
+ self.allow_journal_messages('received invalid "host" field in kill command')
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-bond b/test/verify/check-networkmanager-bond
new file mode 100755
index 0000000..17904ab
--- /dev/null
+++ b/test/verify/check-networkmanager-bond
@@ -0,0 +1,335 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import netlib
+import testlib
+from lib.constants import TEST_OS_DEFAULT
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestBonding(netlib.NetworkCase):
+ def testBasic(self):
+ b = self.browser
+
+ self.login_and_go("/network")
+
+ b.wait_visible("#networking")
+ iface1 = "cockpit1"
+ self.add_veth(iface1, dhcp_cidr="10.111.113.1/24", dhcp_range=['10.111.113.2', '10.111.113.254'])
+ self.nm_activate_eth(iface1)
+ iface2 = "cockpit2"
+ self.add_veth(iface2, dhcp_cidr="10.111.114.1/24", dhcp_range=['10.111.114.2', '10.111.114.254'])
+ self.nm_activate_eth(iface2)
+ self.wait_for_iface(iface1)
+ self.wait_for_iface(iface2)
+
+ # Bond them
+ b.click("button:contains('Add bond')")
+ b.wait_visible("#network-bond-settings-dialog")
+ # wait until dialog initialized
+ b.wait_visible("#network-bond-settings-dialog button[aria-label=Close]")
+ b.wait_visible("#network-bond-settings-mac-input-select-typeahead")
+ # menu is not visible by default, but gets initialized dynamically
+ b.assert_pixels("#network-bond-settings-dialog", "networking-bond-settings-dialog",
+ # help icon gets rendered with unstable noise
+ ignore=["#bond-help-popup-button"])
+
+ b.click("#network-bond-settings-dialog button[aria-label=Close]")
+ b.wait_not_present("#network-bond-settings-dialog")
+ b.click("button:contains('Add bond')")
+ b.wait_visible("#network-bond-settings-dialog")
+ b.click("#network-bond-settings-dialog #bond-help-popup-button")
+ b.wait_in_text("#popover-bond-help-header", "Network bond")
+ b.click("#network-bond-settings-dialog #bond-help-popup-button")
+ b.wait_not_present("#popover-bond-help-header")
+
+ b.set_input_text("#network-bond-settings-interface-name-input", "tbond")
+ b.set_checked(f"input[data-iface='{iface1}']", val=True)
+ b.set_checked(f"input[data-iface='{iface2}']", val=True)
+ b.click("#network-bond-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-bond-settings-dialog")
+ b.wait_visible("#networking-interfaces tr[data-interface='tbond']")
+
+ # Check that the members are displayed and both On
+ b.click("#networking-interfaces tr[data-interface='tbond'] button")
+ b.wait_visible("#network-interface")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface1}']")
+ self.wait_onoff(f"#network-interface-members tr[data-interface='{iface1}']", val=True)
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface2}']")
+ self.wait_onoff(f"#network-interface-members tr[data-interface='{iface2}']", val=True)
+
+ b.wait_text_not("#network-interface-mac", "")
+
+ # Check that link monitoring is correctly set up
+ b.click("#network-interface-settings dd:contains('Active backup') button")
+ b.wait_val("#network-bond-settings-link-monitoring-select", "mii")
+ b.click("#network-bond-settings-dialog button:contains('Cancel')")
+ b.wait_not_present("#network-bond-settings-dialog")
+
+ # Navigate to the first member, check it, navigate back
+ b.click(f"#network-interface-members tr[data-interface='{iface1}'] button.pf-m-link")
+ b.wait_in_text("#network-interface-name", iface1)
+ b.wait_in_text("#network-interface-settings", "MTU")
+ b.wait_not_in_text("#network-interface-settings", "IPv4")
+ b.wait_not_in_text("#network-interface-settings", "IPv6")
+ b.click("#network-interface-settings button:contains(tbond)")
+ b.wait_in_text("#network-interface-name", "tbond")
+
+ # Deactivate the bond and make sure it is still there after a
+ # reload.
+ self.wait_onoff(".pf-v5-c-card__header:contains('tbond')", val=True)
+ self.toggle_onoff(".pf-v5-c-card__header:contains('tbond')")
+ self.wait_for_iface_setting('Status', 'Inactive')
+ b.wait_not_present(".pf-v5-c-card__header:contains('tbond') input[type=checkbox]:disabled")
+
+ b.reload()
+ b.enter_page("/network")
+ b.wait_text("#network-interface-name", "tbond")
+ b.wait_text("#network-interface-hw", "Bond")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface1}']")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface2}']")
+
+ # Delete the bond
+ b.click("#network-interface button:contains('Delete')")
+ b.wait_visible("#networking")
+ b.wait_not_present("#networking-interfaces tr[data-interface='tbond']")
+
+ # Check that the former members are displayed and both On
+ self.wait_for_iface(iface1)
+ self.wait_for_iface(iface2)
+
+ def testNonDefaultSettings(self):
+ b = self.browser
+ m = self.machine
+
+ iface1 = "cockpit1"
+ self.add_veth(iface1, dhcp_cidr="10.111.113.1/24", dhcp_range=['10.111.113.2', '10.111.113.254'])
+ testlib.wait(lambda: m.execute(f'nmcli device | grep {iface1} | grep -v unavailable'))
+
+ iface2 = "cockpit2"
+ self.add_veth(iface2, dhcp_cidr="10.111.114.1/24", dhcp_range=['10.111.114.2', '10.111.114.254'])
+ testlib.wait(lambda: m.execute(f'nmcli device | grep {iface2} | grep -v unavailable'))
+
+ m.execute(f"nmcli con add type ethernet ifname {iface1} con-name TEST1")
+ m.execute(f"nmcli con add type ethernet ifname {iface2} con-name TEST2")
+
+ self.login_and_go("/network")
+ self.wait_for_iface(iface1)
+ self.wait_for_iface(iface2)
+
+ m.execute("nmcli con mod TEST1 ipv4.method link-local; nmcli con up TEST1")
+ m.execute(f"nmcli dev dis {iface2}")
+
+ b.wait_in_text(f"tr[data-interface='{iface1}'] td:nth-child(2)", "169.254.")
+ b.wait_not_in_text(f"tr[data-interface='{iface1}'] td:nth-child(2)", "10.111.")
+ b.wait_text(f"tr[data-interface='{iface2}'] td:nth-child(3)", "Inactive")
+
+ b.click("button:contains('Add bond')")
+ b.wait_visible("#network-bond-settings-dialog")
+ b.set_input_text("#network-bond-settings-interface-name-input", "tbond")
+ b.set_checked(f"input[data-iface='{iface1}']", val=True)
+ b.set_checked(f"input[data-iface='{iface2}']", val=True)
+ b.click("#network-bond-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-bond-settings-dialog")
+
+ b.click("#networking-interfaces tr[data-interface='tbond'] button")
+ b.wait_visible("#network-interface")
+ self.wait_for_iface_setting('IPv4', 'Link local')
+
+ def testRename(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+
+ # Wait until the page is really ready by waiting for some ethernet interface
+ # to show up in some row.
+ iface = m.execute("cd /sys/class/net; ls -d e* | head -n1").strip()
+ b.wait_visible(f"tr[data-interface='{iface}']")
+
+ # Make a simple bond without any members. This is enough to
+ # test the renaming.
+
+ b.click("button:contains('Add bond')")
+ b.wait_visible("#network-bond-settings-dialog")
+ b.select_from_dropdown("#network-bond-settings-link-monitoring-select", "arp")
+ b.set_input_text("#network-bond-settings-monitoring-targets-input", "1.1.1.1")
+ b.set_input_text("#network-bond-settings-interface-name-input", "tbond")
+ b.click("#network-bond-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-bond-settings-dialog")
+
+ # Rename while it is active
+ self.addCleanup(m.execute, "nmcli dev delete tbond3000 2>/dev/null || true")
+
+ b.click("#networking-interfaces tr[data-interface='tbond'] button")
+ self.wait_onoff("#network-interface .pf-v5-c-card__header", val=True)
+ self.wait_for_iface_setting('Status', 'Configuring')
+
+ self.configure_iface_setting('Bond')
+ b.wait_visible("#network-bond-settings-dialog")
+
+ # Check that link monitoring is correctly set up
+ b.wait_val("#network-bond-settings-link-monitoring-select", "arp")
+ b.wait_val("#network-bond-settings-monitoring-targets-input", "1.1.1.1")
+
+ b.set_input_text("#network-bond-settings-interface-name-input", "tbond3000")
+ b.click("#network-bond-settings-dialog button:contains('Save')")
+ b.wait_not_present("#network-bond-settings-dialog")
+ b.wait_text("#network-interface-name", "tbond3000")
+
+ def testActive(self):
+ b = self.browser
+
+ self.login_and_go("/network")
+
+ b.wait_visible("#networking")
+ iface = "cockpit1"
+ self.add_veth(iface, dhcp_cidr="10.111.112.2/20")
+ self.nm_activate_eth(iface)
+ self.wait_for_iface(iface)
+ ip = b.text(f"#networking-interfaces tr[data-interface='{iface}'] td:nth-child(2)")
+
+ # Put an active interface into a bond. The bond should get
+ # the same IP as the active interface.
+
+ b.click("button:contains('Add bond')")
+ b.wait_visible("#network-bond-settings-dialog")
+ b.set_input_text("#network-bond-settings-interface-name-input", "tbond")
+ b.set_checked(f"input[data-iface='{iface}']", val=True)
+ b.click("#network-bond-settings-mac-input")
+ b.click("li button:contains('(cockpit1)')")
+ b.click("#network-bond-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-bond-settings-dialog")
+
+ # Check that it has the interface and the right IP address
+ b.click("#networking-interfaces tr[data-interface='tbond'] button")
+ b.wait_visible("#network-interface")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface}']")
+ b.wait_in_text("#network-interface .pf-v5-c-card:contains('tbond')", ip)
+
+ def testAmbiguousMember(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+
+ b.wait_visible("#networking")
+ iface = "cockpit1"
+ self.add_veth(iface, dhcp_cidr="10.111.113.2/20")
+ self.nm_activate_eth(iface)
+ self.wait_for_iface(iface)
+
+ # Now 'iface' has a normal connection
+ con_id = self.iface_con_id(iface)
+ self.assertTrue(con_id)
+
+ # Manually create a bond and make 'iface' its member via a
+ # second connection. Cockpit should ignore that second
+ # connection and still show iface as a normal interface.
+ m.execute(f"nmcli con add type bond-slave ifname {iface} con-name bond0-member-1 master bond0")
+ m.execute("nmcli con add type bond ifname bond0 con-name bond0")
+
+ self.wait_for_iface("bond0", state="Configuring")
+ self.wait_for_iface(iface)
+
+ # Now activate 'iface' as a member. Cockpit should now ignore
+ # the first connection and show 'iface' as a member of bond0.
+
+ m.execute("nmcli con up bond0-member-1")
+ b.wait_not_present(f"#networking-interfaces tr[data-interface='{iface}']")
+
+ b.click("#networking-interfaces tr[data-interface='bond0'] button")
+ b.wait_visible("#network-interface")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface}']")
+
+
+@testlib.skipDistroPackage()
+class TestBondingVirt(netlib.NetworkCase):
+ provision = {
+ "machine1": {"address": "10.111.113.1/20", "memory_mb": 512},
+ "machine2": {"image": TEST_OS_DEFAULT, "address": "10.111.113.2/20", "dhcp": True, "memory_mb": 256}
+ }
+
+ @testlib.skipImage("Main interface can't be managed", "debian-*", "ubuntu-*")
+ def testMain(self):
+ b = self.browser
+ m = self.machine
+
+ iface = self.get_iface(m, m.networking["mac"])
+
+ self.login_and_go("/network")
+ self.wait_for_iface(iface)
+
+ # Put the main interface into a bond. Everything should keep working.
+ b.click("button:contains('Add bond')")
+ b.wait_visible("#network-bond-settings-dialog")
+ b.set_input_text("#network-bond-settings-interface-name-input", "tbond")
+ b.set_checked(f"input[data-iface='{iface}']", val=True)
+ b.click("#network-bond-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-bond-settings-dialog")
+
+ # Check that it has the main connection and the right IP address
+ b.click("#networking-interfaces tr[data-interface='tbond'] button")
+ b.wait_visible("#network-interface")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface}']")
+ b.wait_in_text("#network-interface .pf-v5-c-card:contains('tbond')", "10.111.113.1")
+
+ # Delete the bond
+ b.click("#network-interface button:contains('Delete')")
+ b.wait_visible("#networking")
+ b.wait_not_present("#networking-interfaces tr[data-interface='tbond']")
+ b.wait_visible(f"#networking-interfaces tr[data-interface='{iface}']")
+
+ @testlib.skipImage("TODO: no dhclient on Arch image", "arch")
+ @testlib.skipImage("Main interface can't be managed", "debian-*", "ubuntu-*")
+ @testlib.skipOstree("not using dhclient")
+ def testSlowly(self):
+ b = self.browser
+ m = self.machine
+
+ iface = self.get_iface(m, m.networking["mac"])
+ self.ensure_nm_uses_dhclient()
+
+ self.login_and_go("/network")
+ self.wait_for_iface(iface)
+
+ # Slow down DHCP enough that it would trigger a rollback.
+ self.slow_down_dhclient(20)
+
+ # Put the main interface into a bond. Everything should keep
+ # working since checkpoints are not used and thus no rollback
+ # is actually triggered.
+
+ b.click("button:contains('Add bond')")
+ b.wait_visible("#network-bond-settings-dialog")
+ b.set_input_text("#network-bond-settings-interface-name-input", "tbond")
+ b.set_checked(f"input[data-iface='{iface}']", val=True)
+ b.click("#network-bond-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-bond-settings-dialog")
+
+ # Check that it has the main connection and the right IP address
+ b.click("#networking-interfaces tr[data-interface='tbond'] button")
+ b.wait_visible("#network-interface")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface}']")
+ b.wait_in_text("#network-interface .pf-v5-c-card:contains('tbond')", "10.111.113.1")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-bridge b/test/verify/check-networkmanager-bridge
new file mode 100755
index 0000000..9f89c9d
--- /dev/null
+++ b/test/verify/check-networkmanager-bridge
@@ -0,0 +1,125 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import netlib
+import testlib
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestBridge(netlib.NetworkCase):
+ def testBasic(self):
+ b = self.browser
+
+ self.login_and_go("/network")
+ b.wait_visible("#networking")
+
+ # These are two independent networks. Thus there is no loop between the
+ # bridge that all VMs are connected to, and the bridge we are creating here.
+ iface1 = "cockpit1"
+ self.add_veth(iface1, dhcp_cidr="10.111.113.1/24", dhcp_range=['10.111.113.2', '10.111.113.254'])
+ self.nm_activate_eth(iface1)
+ iface2 = "cockpit2"
+ self.add_veth(iface2, dhcp_cidr="10.111.114.1/24", dhcp_range=['10.111.114.2', '10.111.114.254'])
+ self.nm_activate_eth(iface2)
+ self.wait_for_iface(iface1)
+ self.wait_for_iface(iface2)
+
+ # Bridge them
+ b.click("button:contains('Add bridge')")
+ b.wait_visible("#network-bridge-settings-dialog")
+
+ # wait until dialog initialized
+ b.wait_visible("#network-bridge-settings-dialog button[aria-label=Close]")
+ b.wait_in_text("#network-bridge-settings-body", "cockpit2")
+ b.assert_pixels("#network-bridge-settings-dialog", "networking-bridge-settings-dialog")
+
+ b.set_input_text("#network-bridge-settings-interface-name-input", "tbridge")
+ b.set_checked(f"input[data-iface='{iface1}']", val=True)
+ b.set_checked(f"input[data-iface='{iface2}']", val=True)
+ b.click("#network-bridge-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-bridge-settings-dialog")
+ b.wait_visible("#networking-interfaces tr[data-interface='tbridge']")
+
+ # Check that the members are displayed and both On
+ b.click("#networking-interfaces tr[data-interface='tbridge'] button")
+ b.wait_visible("#network-interface")
+ self.wait_onoff(f"#network-interface-members tr[data-interface='{iface1}']", val=True)
+ self.wait_onoff(f"#network-interface-members tr[data-interface='{iface2}']", val=True)
+
+ b.wait_text_not("#network-interface-mac", "")
+ self.configure_iface_setting('Bridge')
+ b.click("#network-bridge-settings-dialog button:contains('Cancel')")
+ b.wait_not_present("#network-bridge-settings-dialog")
+
+ # Delete the bridge
+ b.click("#network-interface button:contains('Delete')")
+ b.wait_visible("#networking")
+ b.wait_not_present("#networking-interfaces tr[data-interface='tbridge']")
+
+ # Check that the former members are displayed and both On
+ self.wait_for_iface(iface1)
+ self.wait_for_iface(iface2)
+
+ def testActive(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+ b.wait_visible("#networking")
+
+ iface = "cockpit1"
+ self.add_veth(iface, dhcp_cidr="10.111.112.2/20")
+ self.nm_activate_eth(iface)
+ self.wait_for_iface(iface)
+
+ # Put an active interface into a bridge. We can't select/copy the MAC, so we can't expect to
+ # get the same IP as the active interface, but it should get a valid DHCP IP.
+
+ b.click("button:contains('Add bridge')")
+ b.wait_visible("#network-bridge-settings-dialog")
+ b.set_input_text("#network-bridge-settings-interface-name-input", "tbridge")
+ b.set_checked(f"input[data-iface='{iface}']", val=True)
+ b.click("#network-bridge-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-bridge-settings-dialog")
+
+ # Check that it has the interface and the right IP address
+ b.click("#networking-interfaces tr[data-interface='tbridge'] th button")
+ b.wait_visible("#network-interface")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface}']")
+ b.wait_in_text("#network-interface .pf-v5-c-card:contains('tbridge')", "10.111")
+
+ # Check bridge port
+ b.click("#network-interface-members button:contains(cockpit1)")
+ b.click("#networking-edit-bridgeport")
+ b.wait_visible("#network-bridge-port-settings-dialog")
+ b.set_input_text("#network-bridge-port-settings-prio-input", "35")
+ b.set_input_text("#network-bridge-port-settings-path-cost-input", "90")
+ b.set_checked("#network-bridge-port-settings-hairPin-mode-input", val=True)
+ b.click("#network-bridge-port-settings-save")
+ b.wait_not_present("#network-bridge-port-settings-dialog")
+
+ # Confirm that bridge port settings are applied
+ self.assertEqual(m.execute("nmcli con show cockpit1 | grep -i bridge-port.priority | awk '{print $2}'").strip(), "35")
+ self.assertEqual(m.execute("nmcli con show cockpit1 | grep -i bridge-port.path-cost | awk '{print $2}'").strip(), "90")
+ self.assertEqual(m.execute("nmcli con show cockpit1 | grep -i bridge-port.hairpin-mode | awk '{print $2}'").strip(), "yes")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-checkpoints b/test/verify/check-networkmanager-checkpoints
new file mode 100755
index 0000000..b171718
--- /dev/null
+++ b/test/verify/check-networkmanager-checkpoints
@@ -0,0 +1,180 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import netlib
+import testlib
+
+
+@testlib.skipImage("No network checkpoint support", "ubuntu-*")
+@testlib.skipDistroPackage()
+class TestNetworkingCheckpoints(netlib.NetworkCase):
+ def testCheckpoint(self):
+ b = self.browser
+ m = self.machine
+
+ iface = self.get_iface(m, "52:54:00:12:34:56")
+
+ if "debian" in m.image:
+ self.sed_file("s/managed=false/managed=true/", "/etc/NetworkManager/NetworkManager.conf",
+ "systemctl restart NetworkManager")
+
+ if m.image == "arch":
+ self.sed_file("s/unmanaged-devices=interface-name:eth0//", "/etc/NetworkManager/conf.d/noauto.conf",
+ "systemctl restart NetworkManager")
+
+ self.login_and_go("/network")
+ self.nm_checkpoints_enable()
+ self.wait_for_iface(iface, prefix="172.")
+ self.select_iface(iface)
+ b.wait_visible("#network-interface")
+
+ # Disconnect
+ self.wait_onoff(f".pf-v5-c-card__header:contains('{iface}')", val=True)
+ self.toggle_onoff(f".pf-v5-c-card__header:contains('{iface}')")
+
+ # Wait for dialog to appear and dismiss it
+ with b.wait_timeout(60):
+ b.click("#confirm-breaking-change-popup button:contains('Keep connection')")
+ b.wait_not_present("#confirm-breaking-change-popup")
+
+ if m.image not in ["debian-testing", "debian-stable"]:
+ # Change IP
+ self.configure_iface_setting('IPv4')
+ b.wait_visible("#network-ip-settings-dialog")
+ b.select_from_dropdown("#network-ip-settings-select-method", "manual")
+ b.set_input_text('#network-ip-settings-address-0', "1.2.3.4")
+ b.set_input_text('#network-ip-settings-netmask-0', "24")
+ b.click("#network-ip-settings-save")
+ with b.wait_timeout(60):
+ b.click("#confirm-breaking-change-popup button:contains('Keep connection')")
+ b.wait_not_present("#confirm-breaking-change-popup")
+
+ @testlib.skipImage("Main interface settings are read-only", "debian-*")
+ @testlib.skipImage("not using dhclient", "arch")
+ @testlib.skipOstree("not using dhclient")
+ def testCheckpointSlowRollback(self):
+ b = self.browser
+ m = self.machine
+
+ # A slow rollback would normally cause the global health check
+ # to fail during rollback and prevent showing the
+ # #confirm-breaking-change-popup. We expect the global health
+ # check failure to be ignored.
+ #
+ # We test slow rollbacks by slowing down DHCP requests.
+ #
+ # We need at least 60 seconds of network disconnection to let
+ # the health check fail reliably. A rollback is started 7
+ # seconds after disconnection, so we need to delay the DHCP
+ # request by at least 53 seconds. However, NetworkManager has
+ # a default DHCP timeout of 45 seconds, so we need to increase
+ # that.
+ #
+ # Sometimes, NetworkManager extends the DHCP timeout by an
+ # additional 480 seconds grace period. This doesn't always
+ # happen, so we don't rely on it.
+
+ dhcp_delay = 60
+ dhcp_timeout = 120
+
+ # There are a couple of considerations for ordering the
+ # following actions:
+ #
+ # - Changing the main.dhcp config value requires a restart of NM.
+ #
+ # - A restart of NM might run dhclient, and we don't want it
+ # to be slowed down already at that point.
+ #
+ # - After restarting NM, we need to wait for it to settle
+ # again before messing with dhclient.
+ #
+ # - Simply setting ipv4.dhcp_timeout is not enough if it
+ # should be used immediately for a checkpoint rollback, see
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1690389. A
+ # additional restart is enough.
+ #
+ # So we set ipv4.dhcp_timeout and main.dhcp, do a restart, log
+ # into the UI and wait for the expected interface to appear
+ # and be active, and then slow down dhclient.
+
+ iface = self.get_iface(m, "52:54:00:12:34:56")
+ con_id = self.iface_con_id(iface)
+ m.execute(f'nmcli con mod "{con_id}" ipv4.dhcp-timeout {dhcp_timeout}')
+
+ self.ensure_nm_uses_dhclient()
+
+ self.login_and_go("/network")
+ self.nm_checkpoints_enable()
+ self.wait_for_iface(iface, prefix="172.")
+ self.select_iface(iface)
+ b.wait_visible("#network-interface")
+
+ # checkpoints are realtime sensitive, avoid long NM operations
+ self.settle_cpu()
+
+ self.slow_down_dhclient(dhcp_delay)
+
+ # Disconnect and trigger a slow rollback
+ self.wait_onoff(f".pf-v5-c-card__header:contains('{iface}')", val=True)
+ self.toggle_onoff(f".pf-v5-c-card__header:contains('{iface}')")
+ with b.wait_timeout(120):
+ b.click("#confirm-breaking-change-popup button:contains('Keep connection')")
+ b.wait_not_present("#confirm-breaking-change-popup")
+
+ def testNoRollback(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+ self.nm_checkpoints_enable()
+ b.wait_visible("#networking")
+
+ iface = 'cockpit1'
+ self.add_veth(iface, dhcp_cidr="10.111.113.2/20")
+ self.nm_activate_eth(iface)
+ self.wait_for_iface(iface)
+
+ self.select_iface(iface)
+ b.wait_visible("#network-interface")
+
+ # Disconnect
+ self.wait_onoff(f".pf-v5-c-card__header:contains('{iface}')", val=True)
+ self.toggle_onoff(f".pf-v5-c-card__header:contains('{iface}')")
+
+ # The checkpoint should be destroyed before it is being rolled
+ # back.
+
+ def checkpoint_was_destroyed():
+ lines = m.execute("journalctl -u NetworkManager | grep op=").split("\n")
+ last_checkpoint = None
+ for line in lines:
+ match = netlib.re.search('op="checkpoint-create" arg="([^"]*)"', line)
+ if match:
+ last_checkpoint = match[1]
+ if last_checkpoint:
+ for line in lines:
+ if f'op="checkpoint-destroy" arg="{last_checkpoint}"' in line:
+ return True
+ return False
+
+ testlib.wait(checkpoint_was_destroyed)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-firewall b/test/verify/check-networkmanager-firewall
new file mode 100755
index 0000000..3accb70
--- /dev/null
+++ b/test/verify/check-networkmanager-firewall
@@ -0,0 +1,567 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2018 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import time
+
+import netlib
+import testlib
+
+
+def wait_unit_state(machine, unit, state):
+
+ def active_state(unit):
+ # HACK: don't use `systemctl is-active` here because of
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1073481
+ # Also, use `systemctl --value` once that exists everywhere
+ line = machine.execute(f"systemctl show -p ActiveState {unit}")
+ return line.strip().split("=")[1]
+
+ testlib.wait(lambda: active_state(unit) == state, delay=0.2)
+
+
+def get_active_rules(machine):
+ active_zones = machine.execute("firewall-cmd --get-active-zones | grep -o '^[[:alnum:]]*'").split()
+ active_rules = []
+ for zone in active_zones:
+ active_rules += machine.execute(f"firewall-cmd --zone '{zone}' --list-services").split()
+ ports = machine.execute(f"firewall-cmd --zone '{zone}' --list-ports").strip()
+ if ports:
+ active_rules += [ports]
+ return active_rules
+
+
+@testlib.skipOstree("no firewalld")
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestFirewall(netlib.NetworkCase):
+ def setUp(self):
+ super().setUp()
+ m = self.machine
+ self.restore_dir("/etc/firewalld", restart_unit="firewalld")
+
+ m.execute("systemctl restart firewalld")
+ self.btn_danger = "pf-m-danger"
+ self.btn_primary = "pf-m-primary"
+ self.default_zone = "Public zone"
+
+ # Arch Linux image has no active zones by default
+ if m.image == "arch":
+ m.execute("firewall-cmd --zone 'public' --change-interface='eth0'; firewall-cmd --runtime-to-permanent")
+
+ def testNetworkingPage(self):
+ b = self.browser
+ m = self.machine
+
+ def get_num_zones():
+ return int(m.execute("firewall-cmd --get-active-zones | grep --count '^[a-zA-Z]'").strip())
+
+ active_zones = get_num_zones()
+
+ # Zones should be visible as unprivileged user
+ self.login_and_go("/network", superuser=False)
+ self.wait_onoff("#networking-firewall-summary", val=True)
+ wait_unit_state(m, "firewalld", "active")
+ b.wait_in_text("#networking-firewall-summary", f"{active_zones} active zone")
+ m.execute("systemctl stop firewalld")
+ b.relogin("/network", superuser=True)
+
+ self.wait_onoff("#networking-firewall-summary", val=False)
+ self.toggle_onoff("#networking-firewall-summary")
+ self.wait_onoff("#networking-firewall-summary", val=True)
+ wait_unit_state(m, "firewalld", "active")
+
+ b.wait_in_text("#networking-firewall-summary", f"{active_zones} active zone")
+
+ self.toggle_onoff("#networking-firewall-summary")
+ self.wait_onoff("#networking-firewall-summary", val=False)
+ wait_unit_state(m, "firewalld", "inactive")
+ b.wait_in_text("#networking-firewall-summary", "0 active zones")
+
+ # toggle the service from CLI, page should react
+ try:
+ m.execute("systemctl start firewalld")
+ wait_unit_state(m, "firewalld", "active")
+ except testlib.Error:
+ print("====== firewalld.service =======")
+ print(m.execute("systemctl status firewalld"))
+ raise
+
+ self.wait_onoff("#networking-firewall-summary", val=True)
+ try:
+ testlib.wait(lambda: get_num_zones() == active_zones)
+ b.wait_in_text("#networking-firewall-summary", f"{active_zones} active zone")
+ except testlib.Error:
+ m.execute("firewall-cmd --get-active-zones >&2")
+ raise
+
+ try:
+ m.execute("systemctl stop firewalld")
+ wait_unit_state(m, "firewalld", "inactive")
+ except testlib.Error:
+ print("====== firewalld.service =======")
+ print(m.execute("systemctl status firewalld"))
+ raise
+
+ self.wait_onoff("#networking-firewall-summary", val=False)
+ b.wait_in_text("#networking-firewall-summary", "0 active zones")
+
+ b.click("#networking-firewall-link")
+ b.enter_page("/network/firewall")
+
+ b.click(".pf-v5-c-breadcrumb li:first-of-type")
+
+ b.enter_page("/network")
+
+ self.allow_journal_messages(
+ ".*The name org.fedoraproject.FirewallD1 was not provided by any .service files.*",
+ ".*org.fedoraproject.FirewallD1: .*: GDBus.Error:org.freedesktop.DBus.Error.NoReply.*")
+
+ # test missing "pkcheck" binary, in that case fallback to the admin check
+ def testPkcheckMissing(self):
+ b = self.browser
+ m = self.machine
+
+ # Path to the "pkcheck" PolicyKit utility
+ pkcheck = m.execute("command -v pkcheck").strip()
+ # "Hide" pkcheck
+ m.execute(f"mount --bind /dev/null '{pkcheck}'")
+
+ try:
+ # Regular user is not allowed to change the firewall switch
+ self.login_and_go("/network", superuser=False)
+ b.wait_visible("#networking-firewall-switch:disabled")
+
+ # Super user is allowed to change the firewall switch
+ b.relogin("/network", superuser=True)
+ b.wait_visible("#networking-firewall-switch:not([disabled])")
+ finally:
+ m.execute(f"umount '{pkcheck}'")
+
+ def testFirewallPage(self):
+ b = self.browser
+ m = self.machine
+
+ # Changed in #13686, remove all existing ports before testing
+ for port in m.execute("firewall-cmd --zone 'public' --list-ports").split():
+ m.execute(f"firewall-cmd --remove-port='{port}' --zone 'public'")
+ m.execute(f"firewall-cmd --permanent --remove-port='{port}' --zone 'public'")
+
+ # Zones should be visible as unprivileged user
+ self.login_and_go("/network/firewall", superuser=False)
+ self.wait_onoff("#firewall-heading-title-group", val=True)
+ wait_unit_state(m, "firewalld", "active")
+ # Wait until the default zone is listed
+ b.wait_in_text("#zones-listing .zone-section[data-id='public']", self.default_zone)
+ b.wait_not_present("#add-zone-button")
+ # "Add services" button should not be present
+ b.wait_not_present(".zone-section[data-id='public'] .add-services-button")
+ m.execute("systemctl stop firewalld")
+ b.relogin("/network/firewall", superuser=True)
+
+ # "Add services" button should not be present
+ b.wait_not_present(".add-services-button")
+
+ self.wait_onoff("#firewall-heading-title-group", val=False)
+ self.toggle_onoff("#firewall-heading-title-group")
+ self.wait_onoff("#firewall-heading-title-group", val=True)
+ wait_unit_state(m, "firewalld", "active")
+
+ # Wait until the default zone is listed
+ b.wait_in_text("#zones-listing .zone-section[data-id='public']", self.default_zone)
+
+ # "Add services" button should be enabled
+ b.wait_visible(".zone-section[data-id='public'] .add-services-button:enabled")
+
+ # ensure that pop3 is not enabled (shouldn't be on any of our images),
+ # so that we can use it for testing
+ b.wait_not_present(".zone-section[data-id='public'] tr[data-row-id='pop3']")
+
+ m.execute("firewall-cmd --add-service=pop3")
+ b.wait_visible(".zone-section[data-id='public'] tr[data-row-id='pop3']")
+
+ # Check that all services are shown. This only works reliably since #12806
+ active_rules = get_active_rules(m)
+ b.wait_js_func("((sel, count) => ph_count(sel) == count)", ".zone-section table.ct-table tbody", len(active_rules))
+
+ b.click(".zone-section[data-id='public'] tr[data-row-id='pop3'] #expand-togglepop3")
+ b.wait_in_text(".zone-section[data-id='public'] tbody.pf-m-expanded tr.pf-v5-c-table__expandable-row", "Post Office Protocol")
+
+ b.click(".zone-section[data-id='public'] tr[data-row-id='pop3'] button.pf-v5-c-dropdown__toggle")
+ b.click(".zone-section[data-id='public'] a.pf-m-danger.pf-v5-c-dropdown__menu-item")
+ b.wait_not_present(".zone-section[data-id='public'] tr[data-row-id='pop3']")
+ self.assertNotIn('pop3', m.execute("firewall-cmd --list-services").split())
+
+ # Test that service without name is shown properly
+ m.execute("firewall-cmd --permanent --new-service=empty; firewall-cmd --reload; firewall-cmd --add-service=empty")
+ b.wait_visible(".zone-section[data-id='public'] tr[data-row-id='empty']")
+
+ m.execute("firewall-cmd --add-port=9998/udp --add-port=9999/udp --add-port=6666/tcp")
+ b.wait_in_text(".zone-section[data-id='public'] tr[data-row-id='public-ports'] > td:nth-of-type(3)", "6666")
+ b.wait_in_text(".zone-section[data-id='public'] tr[data-row-id='public-ports'] > td:nth-of-type(4)", "9998, 9999")
+ m.execute("firewall-cmd --remove-port=9998/udp --remove-port=9999/udp --remove-port=6666/tcp")
+ b.wait_not_present(".zone-section[data-id='public'] tr[data-row-id='public-ports']")
+
+ # switch service off again
+ self.toggle_onoff("#firewall-heading-title-group")
+ self.wait_onoff("#firewall-heading-title-group", val=False)
+ wait_unit_state(m, "firewalld", "inactive")
+ # "Add services" button should be hidden again
+ b.wait_not_present(".zone-section[data-id='public'] .add-services-button")
+
+ def testAddServices(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network/firewall")
+ b.wait_in_text("#zones-listing .zone-section[data-id='public']", self.default_zone)
+
+ # add a service to the runtime configuration via the cli, after all the
+ # operations it should still be there, indicating no reload took place
+ m.execute("firewall-cmd --add-service=http")
+ b.wait_visible(".zone-section[data-id='public'] tr[data-row-id='http']")
+
+ b.assert_pixels("#zones-listing .zone-section[data-id='public']", "firewall-default-zone-card",
+ # HACK: medium layout has unstable horizontal width
+ skip_layouts=["medium"])
+
+ # click on the "Add services" button
+ b.click(".zone-section[data-id='public'] .add-services-button")
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-pop3")
+
+ # check functionality of radio buttons
+ b.click("#add-services-dialog input[value='ports']")
+ b.wait_not_present("#filter-services-input")
+ b.click("#add-services-dialog input[value='services']")
+ b.wait_visible("#filter-services-input")
+
+ # filter for pop3
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-imap")
+ b.set_input_text("#filter-services-input", "pop")
+ b.wait_not_present(".pf-v5-c-modal-box .service-list #firewall-service-imap")
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-pop3")
+ self.assertIn("TCP: 110", b.text(".pf-v5-c-modal-box .service-list li:first-child .service-ports.tcp"))
+ b.wait_not_present(".pf-v5-c-modal-box .service-list li:first-child .service-ports.udp")
+ # clear filter
+ b.set_input_text("#filter-services-input", "")
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-imap")
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-pop3")
+
+ # filter for port 110
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-imap")
+ b.set_input_text("#filter-services-input", "110")
+ b.wait_not_present(".pf-v5-c-modal-box .service-list #firewall-service-imap")
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-pop3")
+ self.assertIn("TCP: 110", b.text(".pf-v5-c-modal-box .service-list li:first-child .service-ports.tcp"))
+ b.wait_not_present(".pf-v5-c-modal-box .service-list li:first-child .service-ports.udp")
+ # clear filter
+ b.set_input_text("#filter-services-input", "")
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-imap")
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-pop3")
+
+ # HACK: the dialog needs some time to set up after resizing
+ b.assert_pixels(".pf-v5-c-modal-box", "firewall-add-services-to-zone-modal", wait_delay=3)
+
+ # don't select anything in the dialog
+ b.wait_visible(f"#add-services-dialog .{self.btn_primary}.pf-m-disabled")
+ b.click("#add-services-dialog footer .btn-cancel")
+ b.wait_not_present(".pf-v5-c-modal-box")
+
+ def addService(zone, service):
+ b.click(f".zone-section[data-id='{zone}'] .add-services-button")
+ b.click(f"#add-services-dialog .service-list #firewall-service-{service}")
+ b.click(f"#add-services-dialog footer .{self.btn_primary}")
+ b.wait_not_present(".pf-v5-c-modal-box")
+ b.wait_visible(f".zone-section[data-id='{zone}'] tr[data-row-id='{service}']")
+ self.assertIn(service, m.execute(f"firewall-cmd --zone={zone} --list-services"))
+
+ # now add pop3
+ addService('public', 'pop3')
+ addService('public', 'freeipa-4')
+ b.click(".zone-section[data-id='public'] tr[data-row-id='freeipa-4'] #expand-togglefreeipa-4")
+ b.wait_visible(".zone-section[data-id='public'] tbody.pf-m-expanded .pf-v5-c-table__expandable-row:contains(Included Services)")
+ b.click(".zone-section[data-id='public'] tr[data-row-id='freeipa-4'] #expand-togglefreeipa-4")
+
+ # pop3 should now not appear any more in Add Services dialog
+ b.click(".zone-section[data-id='public'] .add-services-button")
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-imap")
+ b.wait_not_present(".pf-v5-c-modal-box .service-list #firewall-service-pop3")
+ b.click("#add-services-dialog footer .btn-cancel")
+ b.wait_not_present(".pf-v5-c-modal-box")
+
+ # should still be here
+ b.wait_visible(".zone-section[data-id='public'] tr[data-row-id='http']")
+
+ # Service without name should appear in the dialog
+ m.execute("firewall-cmd --permanent --new-service=empty; firewall-cmd --reload")
+ b.click(".zone-section[data-id='public'] .add-services-button")
+ b.wait_visible(".pf-v5-c-modal-box .service-list #firewall-service-empty")
+ b.click("#add-services-dialog footer .btn-cancel")
+ b.wait_not_present(".pf-v5-c-modal-box")
+
+ # remove all services
+ services = set(m.execute("firewall-cmd --list-services").strip().split(" "))
+ # some images come with an extra preconfigured libvirt zone so remove all
+ # the service which belong to both libvirt and public (and thus
+ # requiring checkboxes to be clicked), then remove all the services
+ # belonging either libvirt or public
+ for service in services:
+ b.click(f".zone-section[data-id='public'] tr[data-row-id='{service}'] button.pf-v5-c-dropdown__toggle")
+ b.click(".zone-section[data-id='public'] a.pf-m-danger.pf-v5-c-dropdown__menu-item")
+ # removing cockpit services requires confirmation
+ if service == "cockpit":
+ b.click(f"#delete-confirmation-dialog button.{self.btn_danger}")
+ b.wait_not_present(f".zone-section[data-id='public'] tr[data-row-id='{service}']")
+ self.assertEqual(m.execute("firewall-cmd --list-services").strip(), "")
+
+ # Test that we show 'Additional ports' even when no service is present
+ m.execute("firewall-cmd --add-port=9998/tcp --zone public")
+ b.wait_visible(".zone-section[data-id='public'] td:contains('Additional ports') + td:contains('9998')")
+
+ # test error handling
+ m.execute("firewall-cmd --add-service=pop3")
+ b.wait_visible(".zone-section[data-id='public'] tr[data-row-id='pop3']")
+ b.wait_visible(".zone-section[data-id='public'] td:contains('Additional ports') + td:contains('9998')")
+ # remove service via cli and cockpit
+ m.execute("firewall-cmd --remove-service=pop3")
+ b.click(".zone-section[data-id='public'] tr[data-row-id='pop3'] button.pf-v5-c-dropdown__toggle")
+ b.click(".zone-section[data-id='public'] a.pf-m-danger.pf-v5-c-dropdown__menu-item")
+ b.wait_not_present(".zone-section[data-id='public'] tr[data-row-id='pop3']")
+
+ def testAddCustomServices(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network/firewall")
+ b.wait_in_text("#zones-listing .zone-section[data-id='public']", self.default_zone)
+
+ # add a service to the runtime configuration via the cli, after all the
+ # operations it should be removed, indicating a reload took place
+ m.execute("firewall-cmd --add-service=http")
+ b.wait_visible("tr[data-row-id='http']")
+
+ def open_dialog():
+ b.click(".zone-section[data-id='public'] .add-services-button")
+ b.wait_visible("#add-services-dialog input[value='ports']:enabled")
+ b.click("#add-services-dialog input[value='ports']")
+ b.wait_visible("#tcp-ports")
+ b.wait_visible("#tcp-ports")
+
+ def set_field(sel, val, expected):
+ b.set_input_text(sel, val)
+ b.wait_val("#service-name", expected)
+
+ def check_error(text):
+ b.wait_visible(f".pf-v5-c-form__helper-text .pf-m-error:contains({text})")
+
+ def save(identifier, tcp, udp, desc="", submit_text="Add ports"):
+ b.click(f"button:contains({submit_text})")
+ b.wait_not_present("#add-services-dialog")
+ # expand-togglecustom--80-82-echo-83-123-snmp
+ print(identifier)
+ # expand-togglefreeipa-4
+ line_sel = f".zone-section[data-id='public'] tr[data-row-id='{identifier}']"
+ b.wait_visible(line_sel)
+ b.wait_in_text(line_sel, "".join([identifier, tcp, udp]))
+ if desc:
+ # Click expand button
+ b.click(line_sel + f" #expand-toggle{identifier}")
+ b.wait_text(line_sel + " + tr", desc)
+
+ out = m.execute("firewall-cmd --zone public --list-services")
+ self.assertIn(identifier, out)
+
+ open_dialog()
+ set_field("#tcp-ports", "80", "custom--http")
+ set_field("#tcp-ports", "", "")
+ set_field("#tcp-ports", "80,7", "custom--http-echo")
+ set_field("#udp-ports", "123", "custom--http-echo-ntp")
+ set_field("#udp-ports", "123, 50000", "custom--http-echo-ntp-50000")
+ set_field("#tcp-ports", "", "custom--ntp-50000")
+ set_field("#tcp-ports", "https", "custom--https-ntp-50000")
+ save("custom--https-ntp-50000", "443", "123, 50000")
+
+ open_dialog()
+ set_field("#tcp-ports", "80-82", "custom--80-82")
+ set_field("#tcp-ports", "80-82, echo", "custom--80-82-echo")
+ set_field("#udp-ports", "ntp, snmp", "custom--80-82-echo-ntp-snmp")
+ set_field("#udp-ports", "83-ntp", "custom--80-82-echo-83-123")
+ set_field("#udp-ports", "83-ntp, snmp", "custom--80-82-echo-83-123-snmp")
+ b.set_input_text("#service-description", "custom snmp firewall rule")
+ # Test that the service name field is really optional
+ b.set_input_text("#service-name", "")
+ save("custom--80-82-echo-83-123-snmp", "80-82, 7", "83-123, 161", "custom snmp firewall rule")
+ out = m.execute("firewall-cmd --info-service custom--80-82-echo-83-123-snmp")
+ self.assertIn("ports: 80-82/tcp 7/tcp 83-123/udp 161/udp", out)
+
+ open_dialog()
+ set_field("#tcp-ports", "80", "custom--http")
+ b.set_input_text("#service-name", "I-am-persistent")
+ b.set_input_text("#tcp-ports", "7")
+ time.sleep(5) # We need to validate that the service name did not change
+ b.wait_val("#service-name", "I-am-persistent")
+ save("I-am-persistent", "7", "")
+
+ open_dialog()
+ set_field("#tcp-ports", "500000", "")
+ check_error("Invalid port number")
+ set_field("#tcp-ports", "-1", "")
+ check_error("Invalid port number")
+ set_field("#tcp-ports", "8a", "")
+ check_error("Unknown service name")
+ set_field("#tcp-ports", "foobar", "")
+ check_error("Unknown service name")
+ set_field("#tcp-ports", "80-80", "")
+ check_error("Range must be strictly ordered")
+ set_field("#tcp-ports", "80-79", "")
+ check_error("Range must be strictly ordered")
+ set_field("#tcp-ports", "https-http", "")
+ check_error("Range must be strictly ordered")
+ set_field("#tcp-ports", "80-90-", "")
+ check_error("Invalid range")
+ set_field("#tcp-ports", "80-90-100", "")
+ check_error("Invalid range")
+ b.click("#add-services-dialog button.btn-cancel")
+
+ # test error handling
+ # attempt to create custom service which already exists
+ m.execute("firewall-cmd --permanent --new-service=custom--19834")
+ m.execute("firewall-cmd --permanent --service=custom--19834 --add-port=19834/udp")
+ open_dialog()
+ set_field("#udp-ports", "19834", "custom--19834")
+ b.click(f"#add-services-dialog .{self.btn_primary}")
+ b.wait_in_text("#add-services-dialog div.pf-m-danger", "org.fedoraproject.FirewallD1.Exception: NAME_CONFLICT: new_service(): 'custom--19834'")
+ b.click("#add-services-dialog button.btn-cancel")
+
+ # should have been removed in the reload
+ b.wait_not_present("tr[data-row-id='http']")
+
+ b.click("tr[data-row-id='custom--80-82-echo-83-123-snmp'] button[aria-label=Details]")
+ b.click("tr[data-row-id='custom--80-82-echo-83-123-snmp'] button[aria-label=Actions]")
+ b.click("a:contains(Edit)")
+ b.wait_val("#tcp-ports", "80-82, 7")
+ b.wait_val("#udp-ports", "83-123, 161")
+ b.wait_val("#service-name", "custom--80-82-echo-83-123-snmp")
+ b.wait_val("#service-description", "custom snmp firewall rule")
+ b.set_input_text("#tcp-ports", "80-82, 8")
+ b.set_input_text("#service-description", "edited snmp firewall rule")
+ b.wait_val("#service-name", "custom--80-82-echo-83-123-snmp")
+ save("custom--80-82-echo-83-123-snmp", "80-82, 8", "83-123, 161", "edited snmp firewall rule", 'Edit service')
+ out = m.execute("firewall-cmd --info-service custom--80-82-echo-83-123-snmp")
+ self.assertIn("ports: 80-82/tcp 8/tcp 83-123/udp 161/udp", out)
+
+ def testMultipleZones(self):
+ b = self.browser
+ m = self.machine
+
+ # create unused interface for adding a new zone
+ # FIXME: Firewall page does not pick this up once it's already open
+ home_iface = "ethome"
+ self.add_veth(home_iface)
+
+ def addServiceToZone(service, zone):
+ b.click(f".zone-section[data-id='{zone}'] .add-services-button")
+ b.click(f"#add-services-dialog .service-list #firewall-service-{service}")
+ b.click(f"#add-services-dialog footer .{self.btn_primary}")
+ b.wait_not_present(".pf-v5-c-modal-box")
+ b.wait_visible(f".zone-section[data-id='{zone}'] tr[data-row-id='{service}']")
+ self.assertIn(service, m.execute(f"firewall-cmd --zone={zone} --list-services"))
+
+ def addZone(zone, interfaces=None, sources=None, error=None, do_pixel=False):
+ if interfaces is None:
+ interfaces = []
+ b.click("#add-zone-button")
+ b.wait_visible("#add-zone-dialog")
+ b.wait_visible(f"#add-zone-dialog footer button.{self.btn_primary}:disabled")
+ b.click(f"#add-zone-dialog .add-zone-zones-firewalld input[value='{zone}']")
+
+ if do_pixel:
+ b.assert_pixels("#add-zone-dialog", "firewall-add-zone-dialog")
+
+ for i in interfaces:
+ b.click(f"#add-zone-dialog input[value='{i}']")
+ if sources:
+ b.click("#add-zone-dialog input[value='ip-range']")
+ b.set_input_text("#add-zone-dialog #add-zone-ip", sources)
+
+ b.click(f"#add-zone-dialog footer button.{self.btn_primary}:enabled")
+
+ if error:
+ b.wait_in_text("#add-zone-dialog div.pf-m-danger", error)
+ b.click("#add-zone-dialog footer button.btn-cancel")
+ b.wait_not_present("#add-zone-dialog")
+ return
+
+ b.wait_not_present("#add-zone-dialog")
+ for source in sources.split(",") if sources else []:
+ b.wait_in_text(f"#zones-listing .zone-section[data-id='{zone}']", source)
+ for i in interfaces:
+ b.wait_in_text(f"#zones-listing .zone-section[data-id='{zone}']", i)
+ b.wait_visible(f"#zones-listing .zone-section[data-id='{zone}']")
+ b.wait_visible(f"#zones-listing .zone-section[data-id='{zone}'] tr[data-row-id='cockpit']")
+
+ def removeZone(zone):
+ b.click(f".zone-section[data-id='{zone}'] #dropdown-{zone}")
+ b.click(f".zone-section[data-id='{zone}'] a.pf-m-danger.pf-v5-c-dropdown__menu-item")
+ b.click(f"#delete-confirmation-dialog button.{self.btn_danger}")
+ b.wait_not_present(f".zone-section[data-id='{zone}']")
+
+ self.login_and_go("/network/firewall")
+ b.wait_in_text("#zones-listing .zone-section[data-id='public']", self.default_zone)
+
+ # add predefined work zone
+ addZone("work", sources="192.168.1.0/24", do_pixel=True)
+
+ addServiceToZone("pop3", "work")
+ b.wait_visible(".zone-section[data-id='work'] tr[data-row-id='pop3']")
+ self.assertNotIn(self.default_zone.split(" ")[0], b.text("tr[data-row-id='pop3']"))
+ addServiceToZone("pop3", "public")
+ b.wait_visible(".zone-section[data-id='public'] tr[data-row-id='pop3']")
+
+ # Remove the service from public zone
+ b.click(".zone-section[data-id='public'] tr[data-row-id='pop3'] #expand-togglepop3")
+ b.click(".zone-section[data-id='public'] tr[data-row-id='pop3'] button.pf-v5-c-dropdown__toggle")
+ b.click(".zone-section[data-id='public'] a.pf-m-danger.pf-v5-c-dropdown__menu-item")
+ b.wait_not_present(".zone-section[data-id='public'] tr[data-row-id='pop3']")
+ b.wait_visible(".zone-section[data-id='work'] tr[data-row-id='pop3']")
+
+ # Remove the service from the work zone
+ b.click(".zone-section[data-id='work'] tr[data-row-id='pop3'] button.pf-v5-c-dropdown__toggle")
+ b.click(".zone-section[data-id='work'] a.pf-m-danger.pf-v5-c-dropdown__menu-item")
+ b.wait_not_present(".zone-section[data-id='work'] tr[data-row-id='pop3']")
+
+ # remove predefined work zone
+ removeZone("work")
+ # add zone with previously unused interface
+ addZone("home", interfaces=[home_iface])
+ # Interfaces which already belong to an active zone shouldn't show up in
+ # the Add Zone dialog anymore
+ b.click("#add-zone-button")
+ b.wait_visible("#add-zone-dialog")
+ b.wait_not_present(f"#add-zone-body input[value='{home_iface}']")
+ b.click("#add-zone-dialog footer button.btn-cancel")
+ b.wait_not_present("#add-zone-dialog")
+
+ addServiceToZone("pop3", "home")
+ removeZone("home")
+
+ addZone("work", sources="totally invalid address", error="org.fedoraproject.FirewallD1.Exception: INVALID_ADDR: totally invalid address")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-mac b/test/verify/check-networkmanager-mac
new file mode 100755
index 0000000..b5de1f6
--- /dev/null
+++ b/test/verify/check-networkmanager-mac
@@ -0,0 +1,105 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import netlib
+import testlib
+from lib.constants import TEST_OS_DEFAULT
+
+
+@testlib.skipDistroPackage()
+class TestNetworkingMAC(netlib.NetworkCase):
+ provision = {
+ "machine1": {"memory_mb": 512},
+ "machine2": {"image": TEST_OS_DEFAULT, "address": "10.111.113.2/20", "dhcp": True, "memory_mb": 256}
+ }
+
+ def testMac(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+ b.wait_visible("#networking")
+
+ iface = self.add_iface()
+ self.wait_for_iface(iface)
+
+ mac = m.execute(f"cat /sys/class/net/{iface}/address").strip()
+
+ self.select_iface(iface)
+ b.wait_text("#network-interface-mac", mac.upper())
+
+ if self.networkmanager_version >= [1, 4, 0]:
+ new_mac = self.network.interface()["mac"]
+ b.click("#network-interface-mac button")
+ b.wait_visible("#network-mac-settings-dialog")
+ b.set_input_text('#network-mac-settings-mac-input-select-typeahead', new_mac)
+ b.click(".pf-v5-c-select ul > li > button")
+ b.click("#network-mac-settings-save")
+ b.wait_not_present("#network-mac-settings-dialog")
+
+ b.wait_text("#network-interface-mac", new_mac.upper())
+ self.assertIn(new_mac.lower(), m.execute(f"ip link show '{iface}'"))
+
+ else:
+ b.wait_not_present("#network-interface-mac button")
+
+ def testBondMac(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+ b.wait_visible("#networking")
+
+ iface1 = self.add_iface()
+ iface2 = self.add_iface()
+ self.wait_for_iface(iface1)
+ self.wait_for_iface(iface2)
+
+ b.click("button:contains('Add bond')")
+ b.wait_visible("#network-bond-settings-dialog")
+ b.set_input_text("#network-bond-settings-interface-name-input", "tbond")
+ b.set_checked(f"input[data-iface='{iface1}']", val=True)
+ b.set_checked(f"input[data-iface='{iface2}']", val=True)
+ b.click("#network-bond-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-bond-settings-dialog")
+
+ self.select_iface('tbond')
+ b.wait_visible("#network-interface")
+
+ mac = m.execute("cat /sys/class/net/tbond/address").strip()
+ b.wait_text("#network-interface-mac", mac.upper())
+
+ if self.networkmanager_version >= [1, 6, 0]:
+ new_mac = self.network.interface()["mac"]
+ b.click("#network-interface-mac button")
+ b.wait_visible("#network-mac-settings-dialog")
+ b.set_input_text('#network-mac-settings-mac-input-select-typeahead', new_mac)
+ b.click(".pf-v5-c-select ul > li > button")
+ b.click("#network-mac-settings-save")
+ b.wait_not_present("#network-mac-settings-dialog")
+
+ b.wait_text("#network-interface-mac", new_mac.upper())
+ self.assertIn(new_mac.lower(), m.execute("ip link show tbond"))
+
+ else:
+ b.wait_not_present("#network-interface-mac button")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-mtu b/test/verify/check-networkmanager-mtu
new file mode 100755
index 0000000..1c263b0
--- /dev/null
+++ b/test/verify/check-networkmanager-mtu
@@ -0,0 +1,73 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import netlib
+import testlib
+from lib.constants import TEST_OS_DEFAULT
+
+
+@testlib.skipDistroPackage()
+class TestNetworkingMTU(netlib.NetworkCase):
+ provision = {
+ "machine1": {"memory_mb": 512},
+ "machine2": {"image": TEST_OS_DEFAULT, "address": "10.111.113.2/20", "dhcp": True, "memory_mb": 256}
+ }
+
+ def testMtu(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+ b.wait_visible("#networking")
+
+ iface = self.add_iface()
+ self.wait_for_iface(iface)
+
+ self.select_iface(iface)
+ b.wait_visible("#network-interface")
+
+ self.configure_iface_setting('MTU')
+ b.wait_visible("#network-mtu-settings-dialog")
+
+ # wait until dialog initialized
+ b.wait_visible("#network-mtu-settings-dialog button[aria-label=Close]")
+ b.wait_visible("#network-mtu-settings-custom")
+ b.assert_pixels("#network-mtu-settings-dialog", "networking-mtu-settings-dialog")
+
+ b.set_checked('#network-mtu-settings-custom', val=True)
+ b.set_input_text("#network-mtu-settings-input", "-1500")
+ b.click("button:contains('Save')")
+ b.wait_visible("#network-mtu-settings-error:contains('MTU must be a positive number')")
+ b.set_input_text('#network-mtu-settings-input', "1400")
+ b.click("#network-mtu-settings-save")
+ b.wait_not_present("#network-mtu-settings-dialog")
+ self.wait_for_iface_setting('MTU', '1400')
+
+ # We're debugging failures here log status to journal for diagnosis
+ testlib.wait(lambda: "mtu 1400" in m.execute(f"ip link show {iface} | logger -s 2>&1"))
+
+ self.configure_iface_setting("MTU")
+ b.wait_visible("#network-mtu-settings-dialog")
+ b.set_checked("#network-mtu-settings-auto", val=True)
+ b.click("button:contains('Save')")
+ self.wait_for_iface_setting("MTU", "Automatic")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-other b/test/verify/check-networkmanager-other
new file mode 100755
index 0000000..603d39e
--- /dev/null
+++ b/test/verify/check-networkmanager-other
@@ -0,0 +1,58 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import netlib
+import testlib
+
+
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestNetworkingOther(netlib.NetworkCase):
+ def testOther(self):
+ b = self.browser
+ m = self.machine
+
+ iface = "tun0"
+
+ # Create a tun device and let NetworkManager manage it
+ m.execute(f"ip tuntap add mode tun dev {iface}")
+ self.addCleanup(m.execute, f"ip link del dev {iface}")
+ testlib.wait(lambda: m.execute(f'nmcli device | grep {iface} | grep -v unavailable'))
+ m.execute(f"nmcli dev set {iface} managed yes")
+
+ self.login_and_go("/network")
+ self.wait_for_iface(iface, active=False)
+ self.select_iface(iface)
+ b.wait_visible("#network-interface")
+
+ # Configure a manual IP address
+ #
+ self.configure_iface_setting('IPv4')
+ b.wait_visible("#network-ip-settings-dialog")
+ b.select_from_dropdown("#network-ip-settings-dialog select", "manual")
+ b.set_input_text('#network-ip-settings-address-0', "1.2.3.4")
+ b.set_input_text('#network-ip-settings-netmask-0', "24")
+ b.click("#network-ip-settings-dialog button:contains('Save')")
+ self.addCleanup(m.execute, "nmcli con del tun0")
+ b.wait_not_present("#network-ip-settings-dialog")
+ self.wait_for_iface_setting('Status', '1.2.3.4/24')
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-settings b/test/verify/check-networkmanager-settings
new file mode 100755
index 0000000..493da26
--- /dev/null
+++ b/test/verify/check-networkmanager-settings
@@ -0,0 +1,221 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import netlib
+import testlib
+from lib.constants import TEST_OS_DEFAULT
+
+
+@testlib.skipDistroPackage()
+class TestNetworkingSettings(netlib.NetworkCase):
+ provision = {
+ "machine1": {"memory_mb": 512},
+ "machine2": {"image": TEST_OS_DEFAULT, "address": "10.111.113.2/20", "dhcp": True, "memory_mb": 256}
+ }
+
+ def testNoConnectionSettings(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+ b.wait_visible("#networking")
+
+ iface = self.add_iface(activate=False)
+ self.wait_for_iface(iface, active=False)
+ # checkpoints are realtime sensitive, avoid long NM operations
+ self.settle_cpu()
+
+ self.select_iface(iface)
+ b.wait_visible("#network-interface")
+
+ # Check that there is no connection for the device
+ cons = m.execute(f"nmcli -t -m tabular -f CONNECTIONS.AVAILABLE-CONNECTION-PATHS dev show {iface}")
+ self.assertEqual(cons.strip(), "")
+
+ # Edit and apply the ghost settings
+ self.configure_iface_setting('IPv4')
+ b.wait_visible("#network-ip-settings-dialog")
+ b.select_from_dropdown("#network-ip-settings-select-method", "manual")
+ b.set_input_text('#network-ip-settings-address-0', "1.2.3.4")
+ b.set_input_text('#network-ip-settings-netmask-0', "24")
+ b.click("#network-ip-settings-save")
+ b.wait_not_present("#network-ip-settings-dialog")
+ self.wait_for_iface_setting('IPv4', 'Address 1.2.3.4/24')
+
+ def assert_connection_setting(con_id, field, value):
+ self.assertEqual(m.execute(f'nmcli -m tabular -t -f {field} con show "{con_id}"').strip(),
+ value)
+
+ # Check that we now have connection settings
+ con_id = testlib.wait(lambda: self.iface_con_id(iface))
+ assert_connection_setting(con_id, "ipv4.method", "manual")
+
+ # The interface will be activated. Deactivate it.
+ self.wait_for_iface_setting('Status', '1.2.3.4/24')
+ self.toggle_onoff(f".pf-v5-c-card__header:contains('{iface}')")
+ self.wait_for_iface_setting('Status', 'Inactive')
+
+ # Delete the connection settings again and wait for the ghost
+ # settings to be re-created.
+ m.execute(f'nmcli con del "{con_id}"')
+ self.wait_for_iface_setting('IPv4', 'Automatic')
+
+ # Activate with ghost settings
+ self.toggle_onoff(f".pf-v5-c-card__header:contains('{iface}')")
+ self.wait_for_iface_setting('Status', '10.111.')
+
+ # Check again that we now have connection settings
+ con_id = testlib.wait(lambda: self.iface_con_id(iface))
+ assert_connection_setting(con_id, "ipv4.method", "auto")
+
+ # Change some more settins and check that the actual config
+ # and dialog reflects the change
+
+ self.configure_iface_setting('IPv4')
+ b.wait_visible("#network-ip-settings-dialog")
+ self.wait_onoff("[data-field=dns]", val=True)
+ self.toggle_onoff("[data-field=dns]")
+ self.wait_onoff("[data-field=dns_search]", val=False)
+ b.click("#network-ip-settings-dns-add")
+ b.set_input_text("#network-ip-settings-dns-server-0", "1.2.3.4")
+ b.click("#network-ip-settings-dns-search-add")
+ b.set_input_text("#network-ip-settings-search-domain-0", "foo.com")
+ b.click("#network-ip-settings-save")
+ b.wait_not_present("#network-ip-settings-dialog")
+
+ assert_connection_setting(con_id, "ipv4.ignore-auto-dns", "yes")
+ assert_connection_setting(con_id, "ipv4.dns", "1.2.3.4")
+ assert_connection_setting(con_id, "ipv4.dns-search", "foo.com")
+
+ self.configure_iface_setting('IPv4')
+ self.wait_onoff("[data-field=dns]", val=False)
+ self.wait_onoff("[data-field=dns_search]", val=False)
+ b.wait_val("#network-ip-settings-dns-server-0", "1.2.3.4")
+ b.wait_val("#network-ip-settings-search-domain-0", "foo.com")
+ b.click("#network-ip-settings-cancel")
+ b.wait_not_present("#network-ip-settings-dialog")
+
+ self.configure_iface_setting('IPv6')
+ b.wait_visible("#network-ip-settings-dialog")
+ b.select_from_dropdown("#network-ip-settings-select-method", "disabled")
+ b.click("#network-ip-settings-save")
+ b.wait_not_present("#network-ip-settings-dialog")
+
+ assert_connection_setting(con_id, "ipv6.method", "disabled")
+
+ # Open the dialog, expand all the fields and assert pixels
+ self.configure_iface_setting("IPv4")
+ b.click("#network-ip-settings-address-add")
+ b.click("#network-ip-settings-route-add")
+ b.click("#network-ip-settings-dns-add")
+ b.click("#network-ip-settings-dns-search-add")
+ b.assert_pixels("#network-ip-settings-dialog", "network-ip-settings-dialog")
+
+ # Check that IPv4/IPv6 settings dialogs only show the supported IP methods for wireguard
+ # Skip images without the wireguard-tools package
+ if m.image not in ["rhel4edge", "centos-8-stream"] and not m.image.startswith("rhel-8"):
+ b.go("/network")
+ b.click("#networking-add-wg")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "1.2.3.4/24")
+ wg_iface = b.val("#network-wireguard-settings-interface-name-input")
+ b.click("#network-wireguard-settings-save")
+ self.select_iface(wg_iface)
+ self.configure_iface_setting("IPv4")
+ b._wait_present("#network-ip-settings-select-method option[value='manual']")
+ b._wait_present("#network-ip-settings-select-method option[value='disabled']")
+ unsupported_ip4_methods = ['auto', 'dhcp', 'link-local', 'ignore', 'shared']
+ for method in unsupported_ip4_methods:
+ b.wait_not_present(f"#network-ip-settings-select-method option[value='{method}']")
+ b.click("#network-ip-settings-cancel")
+ b.wait_not_present("#network-ip-settings-dialog")
+ self.configure_iface_setting("IPv6")
+ supported_ip6_methods = ['link-local', 'manual', 'disabled']
+ for method in supported_ip6_methods:
+ b._wait_present(f"#network-ip-settings-select-method option[value='{method}']")
+ unsupported_ip6_methods = ['auto', 'dhcp', 'ignore', 'shared']
+ for method in unsupported_ip6_methods:
+ b.wait_not_present(f"#network-ip-settings-select-method option[value='{method}']")
+
+ def testOtherSettings(self):
+ b = self.browser
+ m = self.machine
+
+ iface = self.add_iface()
+ con_id = self.iface_con_id(iface)
+ m.execute(f"nmcli con mod '{con_id}' connection.gateway-ping-timeout 12")
+
+ self.login_and_go("/network")
+ self.wait_for_iface(iface)
+
+ # IPv4 address sharing
+
+ self.select_iface(iface)
+ b.wait_visible("#network-interface")
+
+ self.configure_iface_setting('IPv4')
+ b.wait_visible("#network-ip-settings-dialog")
+ b.select_from_dropdown("#network-ip-settings-select-method", "shared")
+ b.click("#network-ip-settings-save")
+ b.wait_not_present("#network-ip-settings-dialog")
+
+ self.assertEqual(m.execute(f"nmcli -m tabular -t -f ipv4.method con show '{con_id}'").strip(),
+ "shared")
+ self.assertEqual(m.execute(f"nmcli -m tabular -t -f connection.gateway-ping-timeout con show '{con_id}'").strip(),
+ "12")
+
+ # IPv6 route
+
+ # by default there's a route to link-local only (fe80::)
+ show_cmd = f"ip -6 route show dev {iface}"
+ testlib.wait(lambda: "fe80::/64" in m.execute(show_cmd))
+ # add a manual one
+ self.configure_iface_setting('IPv6')
+ b.wait_visible("#network-ip-settings-dialog")
+ b.click('#network-ip-settings-route-add')
+ b.set_input_text('#network-ip-settings-route-address-0', "fe80:2::")
+ b.set_input_text('#network-ip-settings-route-netmask-0', "60")
+ b.set_input_text('#network-ip-settings-route-gateway-0', "fe80:2::3")
+ b.set_input_text('#network-ip-settings-route-metric-0', "42")
+ b.click('#network-ip-settings-save')
+ b.wait_not_present("#network-ip-settings-dialog")
+
+ # setting should be applied
+ testlib.wait(lambda: "metric 42" in m.execute(show_cmd))
+ out = m.execute(show_cmd)
+ self.assertIn("fe80:2::/60 via fe80:2::3 proto static metric 42", out)
+ self.assertIn("fe80:2::3 proto static metric 42", out)
+ # original link-local route still exists
+ self.assertIn("fe80::/64", out)
+
+ b.wait_attr("#network-interface", "data-test-wait", "false")
+
+ # dialog prefills fields with current settings
+ self.configure_iface_setting('IPv6')
+ b.wait_visible("#network-ip-settings-dialog")
+
+ b.wait_val('#network-ip-settings-route-address-0', "fe80:2:0:0:0:0:0:0")
+ b.wait_val('#network-ip-settings-route-netmask-0', "60")
+ b.wait_val('#network-ip-settings-route-gateway-0', "fe80:2:0:0:0:0:0:3")
+ b.wait_val('#network-ip-settings-route-metric-0', "42")
+ b.click('#network-ip-settings-cancel')
+ b.wait_not_present("#network-ip-settings-dialog")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-team b/test/verify/check-networkmanager-team
new file mode 100755
index 0000000..52cfa9c
--- /dev/null
+++ b/test/verify/check-networkmanager-team
@@ -0,0 +1,145 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import json
+
+import netlib
+import testlib
+
+
+@testlib.skipOstree("NetworkManager-team not installed")
+@testlib.skipImage("TODO: networkmanager fails on Arch Linux", "arch")
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestTeam(netlib.NetworkCase):
+ def testBasic(self):
+ b = self.browser
+
+ self.login_and_go("/network")
+
+ b.wait_visible("button:contains('Add team')")
+
+ iface1 = "cockpit1"
+ self.add_veth(iface1, dhcp_cidr="10.111.113.1/24", dhcp_range=['10.111.113.2', '10.111.113.254'])
+ self.nm_activate_eth(iface1)
+ iface2 = "cockpit2"
+ self.add_veth(iface2, dhcp_cidr="10.111.114.1/24", dhcp_range=['10.111.114.2', '10.111.114.254'])
+ self.nm_activate_eth(iface2)
+ self.wait_for_iface(iface1)
+ self.wait_for_iface(iface2)
+
+ # team them
+ b.click("button:contains('Add team')")
+ b.wait_visible("#network-team-settings-dialog")
+
+ # wait until dialog initialized
+ b.wait_visible("#network-team-settings-dialog button[aria-label=Close]")
+ b.wait_in_text("#network-team-settings-body", "cockpit2")
+ b.assert_pixels("#network-team-settings-dialog", "networking-team-settings-dialog")
+
+ b.set_input_text("#network-team-settings-interface-name-input", "tteam")
+ b.set_checked(f"input[data-iface='{iface1}']", val=True)
+ b.set_checked(f"input[data-iface='{iface2}']", val=True)
+ b.click("#network-team-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-team-settings-dialog")
+
+ b.wait_attr("#networking", "data-test-wait", "false")
+ b.wait(lambda: 'nmcli con show | grep tteam')
+ b.wait_visible("#networking-interfaces tr[data-interface='tteam']")
+
+ # Check that the members are displayed
+ self.select_iface('tteam')
+ b.wait_visible("#network-interface")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface1}']")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface2}']")
+
+ # Deactivate the team and make sure it is still there after a
+ # reload.
+ self.wait_onoff(".pf-v5-c-card__header:contains('tteam')", val=True)
+ self.toggle_onoff(".pf-v5-c-card__header:contains('tteam')")
+ self.wait_for_iface_setting('Status', 'Inactive')
+ b.reload()
+ b.enter_page("/network")
+ b.wait_text("#network-interface-name", "tteam")
+ b.wait_text("#network-interface-hw", "Team")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface1}']")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface2}']")
+
+ # Delete the team
+ b.click("#network-interface button:contains('Delete')")
+ b.wait_visible("#networking")
+ b.wait_not_present("#networking-interfaces tr[data-interface='tteam']")
+
+ # Check that the former members are displayed and both On
+ self.wait_for_iface(iface1)
+ self.wait_for_iface(iface2)
+
+ def testActive(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+ b.wait_visible("#networking")
+
+ iface = "cockpit1"
+ self.add_veth(iface, dhcp_cidr="10.111.112.2/20")
+ self.nm_activate_eth(iface)
+ self.wait_for_iface(iface)
+
+ # Put an active interface into a team. We can't select/copy the MAC, so we can't expect to
+ # get the same IP as the active interface, but it should get a valid DHCP IP.
+
+ b.click("button:contains('Add team')")
+ b.wait_visible("#network-team-settings-dialog")
+ b.set_input_text("#network-team-settings-interface-name-input", "tteam")
+ b.set_checked(f"input[data-iface='{iface}']", val=True)
+ b.click("#network-team-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-team-settings-dialog")
+
+ b.wait_attr("#networking", "data-test-wait", "false")
+ b.wait(lambda: 'nmcli con show | grep tteam')
+
+ # Check that it has the interface and the right IP address
+ self.select_iface('tteam')
+ b.wait_visible("#network-interface")
+ b.wait_visible(f"#network-interface-members tr[data-interface='{iface}']")
+ b.wait_in_text("#network-interface .pf-v5-c-card:contains('tteam')", "10.111")
+
+ # Check team port
+ b.click("#network-interface-members button:contains(cockpit1)")
+ b.click("#networking-edit-teamport")
+ b.wait_visible("#network-team-port-settings-dialog")
+ b.set_input_text("#network-team-port-settings-activebackup-prio-input", "10")
+ b.set_checked("#network-team-port-settings-activebackup-sticky-input", val=True)
+ b.click("#network-team-port-settings-save")
+ b.wait_not_present("#network-team-port-settings-dialog")
+
+ b.wait_attr("#network-interface", "data-test-wait", "false")
+
+ # Confirm that team port settings are applied
+ dump_config_cmd = "teamdctl tteam port config dump cockpit1 || true"
+ b.wait(lambda: "sticky" in m.execute(dump_config_cmd))
+ cockpit1_config = m.execute(dump_config_cmd)
+ team_port_config = json.loads(cockpit1_config)
+ self.assertEqual(team_port_config.get("prio"), "10")
+ self.assertTrue(team_port_config.get("sticky"))
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-unmanaged b/test/verify/check-networkmanager-unmanaged
new file mode 100755
index 0000000..f210c35
--- /dev/null
+++ b/test/verify/check-networkmanager-unmanaged
@@ -0,0 +1,45 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import netlib
+import testlib
+
+
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestNetworkingUnmanaged(netlib.NetworkCase):
+ def testUnmanaged(self):
+ b = self.browser
+ m = self.machine
+
+ iface = "cockpit1"
+ self.add_veth(iface, dhcp_cidr="10.111.113.2/20")
+
+ # Now mark this interface as unmanaged
+ m.execute(r"printf '[keyfile]\nunmanaged-devices=%s\n' > /etc/NetworkManager/conf.d/unmanaged.conf" % iface)
+ m.execute("systemctl restart NetworkManager")
+
+ self.login_and_go("/network")
+
+ with b.wait_timeout(60):
+ b.wait_visible("#networking-unmanaged-interfaces tr[data-interface='" + iface + "']")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-vlan b/test/verify/check-networkmanager-vlan
new file mode 100755
index 0000000..94d9301
--- /dev/null
+++ b/test/verify/check-networkmanager-vlan
@@ -0,0 +1,72 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import netlib
+import testlib
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestNetworkingVLAN(netlib.NetworkCase):
+ def testVlan(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/network")
+ b.wait_visible("#networking")
+
+ iface = 'cockpit1'
+ self.add_veth(iface, dhcp_cidr="10.111.113.2/20")
+ self.wait_for_iface(iface, active=False)
+
+ # Make a VLAN interface
+ b.click("button:contains('Add VLAN')")
+ b.wait_visible("#network-vlan-settings-dialog")
+
+ # wait until dialog initialized
+ b.wait_visible("#network-vlan-settings-dialog button[aria-label=Close]")
+ # Remove focus ring around the close button, which causes pixel tests retries.
+ b.blur("#network-vlan-settings-dialog button[aria-label=Close]")
+ b.wait_visible("#network-vlan-settings-interface-name-input")
+ b.assert_pixels("#network-vlan-settings-dialog", "networking-vlan-settings-dialog")
+
+ b.select_from_dropdown("#network-vlan-settings-parent-select", iface)
+ b.set_input_text("#network-vlan-settings-interface-name-input", "tvlan")
+ b.set_input_text("#network-vlan-settings-vlan-id-input", "123")
+ b.click("#network-vlan-settings-dialog button:contains('Add')")
+ b.wait_not_present("#network-vlan-settings-dialog")
+ b.wait_visible("#networking-interfaces tr[data-interface='tvlan']")
+
+ # It automatically activates. It won't get an IP address, but that's okay.
+ self.wait_for_iface("tvlan", state="Configuring IP")
+
+ # Check that the actual kernel device has the REORDER_HDR flag
+ # set. NetworkManager stopped doing that for connections
+ # created via D-Bus at some point.
+ self.assertIn("REORDER_HDR", m.execute("ip -d link show tvlan | grep vlan"))
+
+ # Delete it
+ self.select_iface('tvlan')
+ b.click("#network-interface button:contains('Delete')")
+ b.wait_visible("#networking")
+ b.wait_not_present("#networking-interfaces tr[data-interface='tvlan']")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-networkmanager-wireguard b/test/verify/check-networkmanager-wireguard
new file mode 100755
index 0000000..88f7780
--- /dev/null
+++ b/test/verify/check-networkmanager-wireguard
@@ -0,0 +1,251 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+import subprocess
+import sys
+
+import netlib
+import packagelib
+import testlib
+
+
+class TestWireGuard(packagelib.PackageCase, netlib.NetworkCase):
+ provision = {
+ "machine1": {"address": "192.168.100.11/24", "memory_mb": 768},
+ "machine2": {"address": "192.168.100.12/24", "memory_mb": 512}
+ }
+
+ def testVPN(self):
+ m1 = self.machines["machine1"]
+ m2 = self.machines["machine2"]
+ b = self.browser
+
+ self.login_and_go("/network")
+
+ # Peer 1 (client)
+ m1_port = 51820
+ m1_ip4 = "10.0.0.1"
+ m1_ip6 = "2001::1"
+ b.click("button:contains('Add VPN')")
+ b.wait_visible("#network-wireguard-settings-dialog")
+ iface_name = b.val("#network-wireguard-settings-interface-name-input")
+ b.wait_visible("#network-wireguard-settings-save:disabled")
+ if m1.image in ["rhel4edge", "centos-8-stream"] or m1.image.startswith("rhel-8"):
+ b.wait_visible(".pf-v5-c-alert:contains('wireguard-tools package is not installed')")
+ b.click("button:contains('Cancel')")
+ b.wait_not_present("#network-ip-settings-dialog")
+ b.wait_visible("#networking")
+ b.wait_not_present(f"#networking-interfaces th:contains('{iface_name}')")
+ # Skip the rest of the tests for images without wireguard-tools
+ # As without it private/public key, connection over IPv4/IPv6 etc can't be tested
+ return
+
+ # Peer 2 (server)
+ m2_port = 51820
+ m2_ip4 = "10.0.0.2"
+ m2_ip6 = "2001::2"
+ b2 = self.new_browser(m2)
+ m2.start_cockpit()
+ if not m2.ostree_image:
+ m2.execute(f"firewall-cmd --add-port={m2_port}/udp")
+ b2.login_and_go("/network")
+ b2.click("button:contains('Add VPN')")
+ b2.wait_visible("#network-wireguard-settings-dialog")
+ m2_iface_name = b2.val("#network-wireguard-settings-interface-name-input")
+ b2.wait_not_val("#network-wireguard-settings-public-key input", "")
+ m2_pubkey = b2.val("#network-wireguard-settings-public-key input")
+ b2.set_input_text("#network-wireguard-settings-addresses-input", f"{m2_ip4}/24")
+ b2.set_input_text("#network-wireguard-settings-listen-port-input", str(m2_port))
+
+ # Validate each field, enter the right value, and then proceed to the next field
+ #
+ # check private-key
+ self.allow_browser_errors("wg: Key is not the correct length or format")
+ b.click("#network-wireguard-settings-paste-key")
+ b.set_input_text("#network-wireguard-settings-private-key-input", "incorrect key")
+ b.set_input_text("#network-wireguard-settings-addresses-input", m1_ip4)
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('key must be 32 bytes base64 encoded')")
+ b.click("#network-wireguard-settings-generated-key")
+
+ # check public-key
+ b.wait_not_val("#network-wireguard-settings-public-key input", "")
+ m1_pubkey = b.val("#network-wireguard-settings-public-key input")
+
+ # check listen-port
+ b.set_input_text("#network-wireguard-settings-listen-port-input", "66000")
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('out of range')")
+ b.set_input_text("#network-wireguard-settings-listen-port-input", "sometext")
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('Listen port must be a number')")
+ b.set_input_text("#network-wireguard-settings-listen-port-input", str(m1_port))
+
+ # check ip addresses
+ b.set_input_text("#network-wireguard-settings-addresses-input", "10.0.0.1/24/56")
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('Addresses are not formatted correctly')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "10.0.0")
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('Invalid address 10.0.0')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "ten.one")
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('Invalid address ten.one')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "10 1")
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('Invalid address 10')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "1.2.3.4/")
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('Invalid prefix or netmask')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", "1.2.3.4 , 5.6.7.8 1.2.3.4.5")
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('Invalid address 1.2.3.4.5')")
+ b.set_input_text("#network-wireguard-settings-addresses-input", f"{m1_ip4}/24 1.2.3.4")
+
+ # peer
+ b.click("button:contains('Add peer')")
+ b.wait_visible("#network-wireguard-settings-peer-0")
+ b.set_input_text("#network-wireguard-settings-publickey-peer-0", m2_pubkey)
+ b.set_input_text("#network-wireguard-settings-endpoint-peer-0", " 192.168.100.12 ") # test that the extra spaces are trimmed
+ b.set_input_text("#network-wireguard-settings-allowedips-peer-0", f" {m2_ip4} ") # test that the extra spaces are trimmed
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('Peer #1 has invalid endpoint. It must be specified as host:port, e.g. 1.2.3.4:51820 or example.com:51820')")
+ b.set_input_text("#network-wireguard-settings-endpoint-peer-0", "192.168.100.12:somestring")
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('Peer #1 has invalid endpoint port. Port must be a number.')")
+ b.click("button:contains('Add peer')")
+ b.wait_visible("#network-wireguard-settings-peer-1")
+ b.set_input_text("#network-wireguard-settings-publickey-peer-1", m2_pubkey)
+ b.set_input_text("#network-wireguard-settings-endpoint-peer-1", f"192.168.100.12:{m2_port}")
+ b.set_input_text("#network-wireguard-settings-allowedips-peer-1", m2_ip4)
+ b.click("button#network-wireguard-settings-btn-close-peer-0")
+ b.wait_not_present("#network-wireguard-settings-peer-1")
+ b.assert_pixels("#network-wireguard-settings-dialog", "networking-wireguard-add-generated",
+ ignore=["#network-wireguard-settings-private-key-input",
+ "#network-wireguard-settings-public-key",
+ "#network-wireguard-settings-publickey-peer-0"])
+ b.click("#network-wireguard-settings-save")
+ b.wait_not_present("#network-wireguard-settings-dialog")
+ b.wait_in_text(f"#networking-interfaces th:contains('{iface_name}') + td", f"1.2.3.4/32, {m1_ip4}/24")
+
+ # if some wg properties are not valid, for example, if it was changed by some external tool, don't crash
+ # this doesn't work on Ubuntu as the config is done through netplan; but this is just testing
+ # handling of internal errors, so it's ok to skip it
+ invalid_props_test = not m1.image.startswith("ubuntu")
+ if invalid_props_test:
+ m1.execute("sed -i '/allowed-ips/d' /etc/NetworkManager/system-connections/con-wg0.nmconnection")
+ m1.execute("systemctl restart NetworkManager")
+ b.reload()
+ b.enter_page("/network")
+ b.wait_visible("#networking")
+
+ b.click(f"#networking-interfaces button:contains('{iface_name}')")
+ b.wait_visible("#network-interface")
+ b.click("#networking-edit-wg")
+
+ if invalid_props_test:
+ b.click("#network-wireguard-settings-save")
+ b.wait_visible(".pf-v5-c-alert:contains('has invalid allowed-ips')")
+
+ b.set_input_text("#network-wireguard-settings-allowedips-peer-0", m2_ip4)
+ b.click("#network-wireguard-settings-save")
+ b.wait_not_present("#network-wireguard-settings-dialog")
+
+ m1.execute(f"until wg show wg0 | grep -q 'allowed ips.*{m2_ip4}/32'; do sleep 1; done")
+ m1.execute(f"until ip route | grep -q '10.0.0.0/24 dev wg0 proto kernel scope link src {m1_ip4} metric 50'; do sleep 1; done")
+
+ # endpoint and port is not necessary for a peer if that peer estalishes the connectio first (i.e. the client)
+ b2.click("button:contains('Add peer')")
+ b2.set_input_text("#network-wireguard-settings-publickey-peer-0", m1_pubkey)
+ b2.set_input_text("#network-wireguard-settings-allowedips-peer-0", f"{m1_ip4}/32")
+ b2.click("#network-wireguard-settings-save")
+ b2.wait_not_present("#network-wireguard-settings-dialog")
+ b2.wait_in_text(f"#networking-interfaces th:contains('{m2_iface_name}') + td", f"{m2_ip4}/24")
+
+ # check connection over ipv4
+ try:
+ m1.execute(f"ping -c 5 {m2_ip4}")
+ except (subprocess.CalledProcessError, testlib.Error):
+ print("-------- status on m1 ----------", file=sys.stderr)
+ m1.execute("set -x; ip a >&2; ip route >&2; nmcli c >&2; wg >&2")
+ print("-------- status on m2 ----------", file=sys.stderr)
+ m2.execute("set -x; ip a >&2; ip route >&2; nmcli c >&2; wg >&2")
+ raise
+
+ # check connection over ipv6
+ b2.click(f"#networking-interfaces button:contains('{m2_iface_name}')")
+
+ b2.click("#networking-edit-wg")
+ b2.wait_visible("#network-wireguard-settings-dialog")
+ b2.set_input_text("#network-wireguard-settings-allowedips-peer-0", f"{m1_ip4}/32,{m1_ip6}")
+ b2.click("#network-wireguard-settings-save")
+ b2.wait_not_present("#network-wireguard-settings-dialog")
+
+ m2.execute(f"until wg show wg0 | grep -q 'allowed ips.*{m1_ip6}/128'; do sleep 1; done")
+
+ b2.click("#networking-edit-ipv6")
+ b2.wait_visible("#network-ip-settings-dialog")
+ b2.select_from_dropdown("#network-ip-settings-select-method", "manual")
+ b2.set_input_text("#network-ip-settings-address-0", m2_ip6)
+ b2.set_input_text("#network-ip-settings-netmask-0", "64")
+ b2.set_input_text("#network-ip-settings-gateway-0", "::")
+ b2.click("#network-ip-settings-save")
+ b2.wait_not_present("#network-ip-settings-dialog")
+ b2.wait_in_text("dt:contains('IPv6') + dd", "Address 2001:0:0:0:0:0:0:2/64")
+
+ m2.execute(f"until ip a show dev {m2_iface_name} | grep -q 'inet6 {m2_ip6}/64 scope global'; do sleep 0.3; done", timeout=10)
+
+ b.click("#networking-edit-wg")
+ b.wait_visible("#network-wireguard-settings-dialog")
+ b.set_input_text("#network-wireguard-settings-allowedips-peer-0", f"{m2_ip4}/32,{m2_ip6}")
+ b.click("#network-wireguard-settings-save")
+ b.wait_not_present("#network-wireguard-settings-dialog")
+
+ m1.execute(f"until wg show wg0 | grep -q 'allowed ips.*{m2_ip6}/128'; do sleep 1; done")
+
+ b.click("#networking-edit-ipv6")
+ b.wait_visible("#network-ip-settings-dialog")
+ b.select_from_dropdown("#network-ip-settings-select-method", "manual")
+ b.set_input_text("#network-ip-settings-address-0", m1_ip6)
+ b.set_input_text("#network-ip-settings-netmask-0", "64")
+ b.set_input_text("#network-ip-settings-gateway-0", "::")
+ b.click("#network-ip-settings-save")
+ b.wait_not_present("#network-ip-settings-dialog")
+ self.wait_for_iface_setting("IPv6", "Address 2001:0:0:0:0:0:0:1/64")
+
+ m1.execute(f"until ip a show dev {iface_name} | grep -q 'inet6 {m1_ip6}/64 scope global'; do sleep 0.3; done", timeout=10)
+
+ try:
+ m1.execute(f"ping -6 -c 5 {m2_ip6}")
+ except (subprocess.CalledProcessError, testlib.Error):
+ print("-------- status on m1 ----------", file=sys.stderr)
+ m1.execute("set -x; ip a >&2; ip -6 route >&2; nmcli c >&2; wg >&2")
+ print("-------- status on m2 ----------", file=sys.stderr)
+ m2.execute("set -x; ip a >&2; ip -6 route >&2; nmcli c >&2; wg >&2")
+ raise
+
+ b.go("/network")
+ # install wireguard-tools from the install dialog
+ if not m1.ostree_image:
+ m1.execute("pkcon remove -y wireguard-tools")
+ self.createPackage("wireguard-tools", "1", "1")
+ self.enableRepo()
+ # HACK: Packagekit on Arch Linux does not deal well with detecting new repositories
+ if m1.image == "arch":
+ m1.execute("systemctl restart packagekit")
+ m1.execute("pkcon refresh force")
+ b.click("button:contains('Add VPN')")
+ b.wait_visible("#dialog button:contains('Install'):enabled")
+ b.click("#dialog button:contains('Install')")
+ b.wait_not_present("#dialog")
+ b.wait_visible("#network-wireguard-settings-dialog")
+ b.wait_visible(".pf-v5-c-alert")
+ b.click("button:contains('Cancel')")
+
+ # lastly delete the interface
+ b.click(f"#networking-interfaces button:contains('{iface_name}')")
+ b.click("#network-interface-delete")
+ b.wait_not_present(f"#networking-interfaces th:contains('{iface_name}')")
+
+
+if __name__ == "__main__":
+ testlib.test_main()
diff --git a/test/verify/check-packagekit b/test/verify/check-packagekit
new file mode 100755
index 0000000..ffb212e
--- /dev/null
+++ b/test/verify/check-packagekit
@@ -0,0 +1,1756 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import time
+
+import packagelib
+import testlib
+
+OSesWithoutTracer = ["debian-stable", "debian-testing", "ubuntu-2204", "ubuntu-stable", "fedora-coreos", "rhel4edge"]
+OSesWithoutKpatch = ["debian-*", "ubuntu-*", "arch", "fedora-*", "rhel4edge", "centos-*"]
+
+
+class NoSubManCase(packagelib.PackageCase):
+
+ def setUp(self):
+ super().setUp()
+
+ # Disable Subscription Manager on RHEL for these tests; subscriptions are tested in a separate class
+ # On other OSes (Fedora/CentOS) we expect sub-man to be disabled in dnf, so it should not get in the way there
+ if self.machine.image.startswith("rhel") or self.machine.image.startswith("centos"):
+ self.machine.execute("systemctl stop rhsm.service; systemctl mask rhsm.service")
+ self.addCleanup(self.machine.execute, "systemctl unmask rhsm.service")
+
+ # expected journal messages from enabling/disabling auto upgrade services
+ self.allow_journal_messages("(Created symlink|Removed).*dnf-automatic-install.timer.*")
+
+
+@testlib.skipImage("TODO: Fails with 401 on our test runners", "arch")
+@testlib.skipOstree("Image uses OSTree")
+class TestUpdates(NoSubManCase):
+
+ def setUp(self):
+ super().setUp()
+
+ pkcon_version = self.machine.execute("pkcon --version").strip()
+
+ # only the PackageKit ≥ 1.2.4 dnf (https://github.com/PackageKit/PackageKit/issues/268) backends
+ # properly recognize "enhancement" severity; apt does not have that metadata
+ self.supports_severity = not (self.backend == "apt" or pkcon_version < "1.2.4")
+
+ if self.supports_severity:
+ self.enhancement_severity = "enhancement"
+ else:
+ self.enhancement_severity = "bug"
+
+ self.update_icon = "#page_status_notification_updates svg"
+ self.update_text = "#page_status_notification_updates"
+ self.update_text_action = "#page_status_notification_updates a"
+
+ def assertHistory(self, path, updates):
+ selector = path + " li:nth-child({0})"
+ for index, pkg in enumerate(updates, start=1):
+ self.browser.wait_in_text(selector.format(index), pkg)
+ # make sure we don't have any extra ones
+ self.assertFalse(self.browser.is_present(selector.format(len(updates) + 1)))
+
+ def check_nth_update(self, index, pkgname, version, severity="bug",
+ num_issues=None, desc_matches=None, cves=None, bugs=None, arch=None):
+ """Check the contents of the package update table row at index
+
+ None properties will not be tested.
+ """
+ b = self.browser
+ if arch is None:
+ arch = self.primary_arch
+ row = "#available-updates table[aria-label='Available updates'] > tbody:nth-of-type(%i) " % index
+
+ if isinstance(pkgname, list):
+ for idx, pkg in enumerate(pkgname, 1):
+ self.assertEqual(b.text(row + "tr:first-child [data-label=Name] > div:nth-of-type(%i) span" % idx).split(', ', 1)[0], pkg)
+ else:
+ self.assertEqual(b.text(row + " tr:first-child [data-label=Name]"), pkgname)
+ b.mouse(row + "tr:first-child [data-label=Name] span", "mouseenter")
+ b.wait_text(".pf-v5-c-tooltip", "dummy " + pkgname + " (" + arch + ")")
+ b.mouse(row + "tr:first-child [data-label=Name] span", "mouseleave")
+ b.wait_not_present(".pf-v5-c-tooltip")
+ self.assertEqual(b.text(row + "[data-label=Version]"), version)
+ # verify type
+ severity_to_aria = {"bug": "bug fix", "enhancement": "enhancement", "security": "security"}
+ b.wait_visible(f"{row} [data-label=Severity] .severity-icon[aria-label='{severity_to_aria[severity]}']")
+ self.assertEqual(b.text(row + "[data-label=Severity]").strip(),
+ num_issues is not None and str(num_issues) or "")
+
+ # should not be expanded by default
+ self.assertNotIn("pf-m-expanded", b.attr(row, "class"))
+ # expand
+ b.click(row + "td.pf-v5-c-table__toggle button")
+ self.assertIn("pf-m-expanded", b.attr(row, "class"))
+ b.wait_in_text(row + "> tr.pf-m-expanded", "Packages")
+ desc = b.text(row + "> tr.pf-m-expanded")
+
+ # details should contain all description bits, CVEs and bug numbers
+ for m in (desc_matches or []) + (cves or []) + (bugs or []):
+ self.assertIn(m, desc)
+
+ return row
+
+ def wait_checking_updates(self):
+ """Wait until spinner is gone from updates icon for 3 s"""
+
+ good_count = 0
+ for _ in range(60):
+ classes = self.browser.attr(self.update_icon, "class")
+ if classes is not None and "pf-v5-c-spinner" in classes:
+ good_count = 0
+ else:
+ good_count += 1
+ if good_count >= 3:
+ return
+ time.sleep(1)
+
+ self.fail("Timed out waiting for updates spinner to go away")
+
+ @testlib.nondestructive
+ @testlib.skipImage("kpatch is not available", *OSesWithoutKpatch)
+ def testKpatch(self):
+ b = self.browser
+ m = self.machine
+
+ kernel_ver_arch = m.execute("uname -r").strip()
+ fields = kernel_ver_arch.split("-")
+ kpp_kernel_version = fields[0].replace(".", "_")
+ release = fields[1].split(".")[:-2] # remove el8.x86_64
+ kpp_kernel_release = "_".join(release)
+
+ sanitized_kernel_ver = "_".join(kernel_ver_arch.split(".")[:-2])
+ self.createPackage("-".join(["kpatch-patch", kpp_kernel_version, kpp_kernel_release]), "0", "0", arch=self.secondary_arch,
+ provides=f"kpatch-patch = {kernel_ver_arch}")
+ self.enableRepo()
+
+ self.restore_file("/etc/dnf/plugins/kpatch.conf")
+ m.execute("systemctl disable --now kpatch")
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+
+ # Enable and install kpatch
+ with b.wait_timeout(30):
+ b.wait_in_text("#kpatch-settings", "Disabled")
+ b.click("#kpatch-settings button:contains('Enable')")
+
+ # Apply kernel live patches for current and future kernels
+ b.click("#apply-kpatch")
+ b.wait_visible("#apply-kpatch:checked")
+ b.wait_visible("#current-future:checked")
+
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kpatch-setup")
+
+ self.assertIn("True", m.execute("grep autoupdate /etc/dnf/plugins/kpatch.conf"))
+ self.assertEqual(m.execute("systemctl is-enabled kpatch").strip(), "enabled")
+ self.assertEqual(m.execute("systemctl is-active kpatch").strip(), "active")
+ # Patches should be installed
+ m.execute("rpm -q kpatch-patch-" + sanitized_kernel_ver)
+
+ # Faking real patches is really hard, so fake `kpatch list` command
+ # To make sure it has the same structure as we expect it to be first check the real command
+ self.assertEqual(m.execute("kpatch list"), "Loaded patch modules:\n\nInstalled patch modules:\n")
+
+ kpatch = """#!/bin/bash
+echo -e "Loaded patch modules:\nkpatch_3_10_0_1062_1_1 [enabled]\n\nInstalled patch modules:\nkpatch_3_10_0_1062_1_1 (3.10.0-1062.el7.x86_64)"
+"""
+
+ self.createPackage("kpatch-patch-" + sanitized_kernel_ver, "1", "0", arch=self.secondary_arch,
+ provides=f"kpatch-patch = {kernel_ver_arch}",
+ content={"/usr/local/bin/kpatch": kpatch},
+ postinst="chmod +x /usr/local/bin/kpatch; mount -o bind /usr/local/bin/kpatch /usr/sbin/kpatch")
+ # Mix in an normal update and ensure it's not updated
+ self.createPackage("vanilla", "1.0", "1", install=True)
+ self.createPackage("vanilla", "1.0", "2")
+ self.createPackage("secdeclare", "3", "4.a1", install=True)
+ self.createPackage("secdeclare", "3", "4.b1", severity="security",
+ changes="Will crash your data center", cves=["CVE-2014-123456"])
+ self.enableRepo()
+ self.addCleanup(m.execute, "umount /usr/sbin/kpatch")
+
+ b.click("#status .pf-v5-c-card__actions button")
+ b.wait_visible(".pf-v5-c-badge:contains('patches')")
+ b.click("button:contains('Install kpatch updates')")
+ b.click("button:contains('Continue')")
+ b.wait_in_text("#status", "Kernel live patch kpatch_3_10_0_1062_1_1 is active")
+ # Other updates are not installed
+ self.check_nth_update(1, "secdeclare", "3-4.b1", "security", 1,
+ desc_matches=["Will crash your data center"], cves=["CVE-2014-123456"])
+ self.check_nth_update(2, "vanilla", "1.0-2")
+ self.assertEqual(m.execute("rpm -q vanilla").strip(), "vanilla-1.0-1.noarch")
+
+ # Switch to 'for current kernel only'
+ b.click("#kpatch-settings button:contains('Edit')")
+ b.click("#current-only")
+ b.wait_visible("#current-future:not(:checked)")
+ b.wait_visible("#current-only:checked")
+
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kpatch-setup")
+
+ self.assertIn("False", m.execute("grep autoupdate /etc/dnf/plugins/kpatch.conf"))
+ self.assertEqual(m.execute("systemctl is-enabled kpatch").strip(), "enabled")
+ self.assertEqual(m.execute("systemctl is-active kpatch").strip(), "active")
+ # Patches should be installed
+ m.execute("rpm -q kpatch-patch-" + sanitized_kernel_ver)
+
+ # Test disabling applying patches
+ b.click("#kpatch-settings button:contains('Edit')")
+ b.wait_visible("#apply-kpatch:checked")
+ b.click("#apply-kpatch")
+ b.wait_visible("#apply-kpatch:not(:checked)")
+
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kpatch-setup")
+
+ b.wait_in_text("#kpatch-settings", "Disabled")
+ b.click("#kpatch-settings button:contains('Enable')")
+
+ # Test closing of the dialog and resetting of changes
+ b.wait_visible("#apply-kpatch:not(:checked)")
+ b.click("#apply-kpatch")
+ b.wait_visible("#apply-kpatch:checked")
+ b.click("button:contains('Cancel')")
+ b.wait_not_present("#kpatch-setup")
+ b.click("#kpatch-settings button:contains('Enable')")
+ b.wait_visible("#apply-kpatch:not(:checked)")
+
+ # Test 'for current kernel only' from clean state
+ m.execute("umount /usr/sbin/kpatch")
+ m.execute("systemctl restart kpatch")
+ m.execute("dnf -y remove kpatch-patch-" + sanitized_kernel_ver)
+ b.reload() # Not listening on patches being removed
+ b.enter_page("/updates")
+
+ b.wait_in_text("#kpatch-settings", "Disabled")
+ b.click("#kpatch-settings button:contains('Enable')")
+
+ # Apply kernel live patches for current and future kernels
+ b.click("#apply-kpatch")
+ b.wait_visible("#apply-kpatch:checked")
+ b.click("#current-only")
+ b.wait_visible("#current-future:not(:checked)")
+ b.wait_visible("#current-only:checked")
+
+ b.click("button:contains('Save')")
+ b.wait_not_present("#kpatch-setup")
+
+ self.assertIn("False", m.execute("grep autoupdate /etc/dnf/plugins/kpatch.conf"))
+ self.assertEqual(m.execute("systemctl is-enabled kpatch").strip(), "enabled")
+ self.assertEqual(m.execute("systemctl is-active kpatch").strip(), "active")
+ # Patches should be installed
+ m.execute("rpm -q kpatch-patch-" + sanitized_kernel_ver)
+
+ @testlib.nondestructive
+ def testBasic(self):
+ # no security updates, no changelogs
+ b = self.browser
+ m = self.machine
+
+ def check_status():
+ if m.image in OSesWithoutTracer:
+ b.wait_in_text("#status p", "System is up to date")
+ # PK starts from a blank state, thus should force refresh and set the "time since" to 0
+ b.wait_in_text("#last-checked", "Last checked: less than a minute ago")
+ else:
+ code = m.execute("tracer > /dev/null 2>&1; echo $?").rstrip()
+ if code == "104":
+ b.wait_in_text("#status p", "a system reboot")
+ elif code == "102" or code == "101":
+ b.wait_in_text("#status p", "to be restarted")
+ else:
+ b.wait_in_text("#status p", "System is up to date")
+ # PK starts from a blank state, thus should force refresh and set the "time since" to 0
+ b.wait_in_text("#last-checked", "Last checked: less than a minute ago")
+
+ self.enable_preload("packagekit", "index")
+
+ # Refresh cache so cockpit does not try to reload page
+ m.execute("pkcon refresh")
+
+ m.start_cockpit()
+ b.login_and_go("/system")
+ # status on /system front page: no repos at all, thus no updates
+ self.wait_checking_updates()
+ b.wait_text(self.update_text, "System is up to date")
+ self.assertEqual(b.attr(self.update_icon, "data-pficon"), "check")
+
+ # no updates on the Software Updates page
+ b.go("/updates")
+ b.enter_page("/updates")
+
+ # refresh
+ b.click("#status .pf-v5-c-card__header button")
+ check_status()
+
+ install_lockfile = "/tmp/finish-pk"
+ # create two updates; force installing chocolate before vanilla
+ self.createPackage("vanilla", "1.0", "1", install=True)
+ self.createPackage("vanilla", "1.0", "2", depends="chocolate",
+ postinst=f"while [ ! -e {install_lockfile} ]; do sleep 1; done; rm -f {install_lockfile}")
+ self.createPackage("chocolate", "2.0", "1", install=True, arch=self.secondary_arch)
+ self.createPackage("chocolate", "2.0", "2", arch=self.secondary_arch)
+ self.enableRepo()
+
+ # check again
+ b.click("#status .pf-v5-c-card__header button")
+
+ with b.wait_timeout(30):
+ b.wait_visible("#available-updates")
+ b.wait_in_text("#status", "2 updates available")
+
+ b.wait_in_text("table[aria-label='Available updates']", "vanilla")
+ self.check_nth_update(1, "chocolate", "2.0-2", arch=self.secondary_arch)
+ self.check_nth_update(2, "vanilla", "1.0-2")
+
+ # updates are shown on system page
+ b.go("/system")
+ b.enter_page("/system")
+ self.wait_checking_updates()
+ b.wait_text(self.update_text, "Bug fix updates available")
+ self.assertEqual(b.attr(self.update_icon, "data-pficon"), "bug")
+ # should be a link, click on it to go to /updates
+ b.click(self.update_text_action)
+ b.enter_page("/updates")
+
+ # old versions are still installed
+ m.execute("test -f /stamp-vanilla-1.0-1; test -f /stamp-chocolate-2.0-1")
+
+ # no update history yet
+ self.assertFalse(b.is_present("table.updates-history"))
+
+ # should only have one button (no security updates)
+ self.assertEqual(b.text("#available-updates button#install-all"), "Install all updates")
+
+ # stall the download of chocolate by replacing the package with a pipe, so that we can test cancelling
+ chocolate = m.execute(f"""set -ux;
+ p=$(ls {self.vm_tmpdir}/repo/chocolate*2.0*2*)
+ f={self.vm_tmpdir}/fifo
+ mkfifo $f
+ mount -o bind $f $p
+ echo $p""").strip()
+ try:
+ b.click("#available-updates button#install-all")
+
+ # applying updates panel present
+ b.wait_visible("#app div.pf-v5-c-progress__bar")
+
+ # cancel the installation
+ b.wait_in_text(".progress-main-view button.pf-m-secondary", "Cancel")
+ b.click(".progress-main-view button.pf-m-secondary")
+ # abort the current download, so that read calls don't hang indefinitely
+ m.spawn(f"echo > {self.vm_tmpdir}/fifo", "fifo")
+
+ # going back to overview, nothing happened just yet
+ b.wait_not_present(".progress-main-view")
+ self.assertEqual(b.text("#available-updates button#install-all"), "Install all updates")
+ self.assertFalse(b.is_present("table.updates-history"))
+ m.execute("test -f /stamp-vanilla-1.0-1; test -f /stamp-chocolate-2.0-1")
+ finally:
+ # avoid keeping the rpm file busy
+ m.execute("systemctl stop packagekit")
+ m.execute(f"umount {chocolate}")
+
+ # update again; Cancel button should eventually disappear
+ b.click("#available-updates button#install-all")
+ b.wait_visible(".progress-main-view")
+ b.wait_not_present(".progress-main-view button.pf-m-secondary")
+ # gets stuck at vanilla, which needs install_lockfile
+ b.wait_in_text("#app div.progress-description", "vanilla 1.0-2")
+
+ # update log only exists in the expander, collapsed by default
+ self.assertFalse(b.is_visible("#update-log"))
+ # expand it
+ b.click(".pf-v5-c-expandable-section__toggle")
+ # should eventually show chocolate when vanilla starts installing
+ b.wait_in_text("#update-log", "chocolate")
+
+ # finish the package installation
+ m.execute(f"touch {install_lockfile}")
+
+ b.wait_visible(".pf-v5-c-empty-state__title:contains('Update was successful')")
+ # if tracer is not present, reboot is recommended
+ if m.image in OSesWithoutTracer:
+ b.wait_in_text("#app .pf-v5-c-empty-state button.pf-m-primary", "Reboot system...")
+ b.click("#ignore")
+
+ # should go back to updates overview, nothing pending any more
+ check_status()
+
+ # TODO make Packagekit GetUpdates work for tests properly
+ b.wait_not_present("#available-updates")
+
+ # new versions are now installed
+ m.execute("test -f /stamp-vanilla-1.0-2; test -f /stamp-chocolate-2.0-2")
+
+ # history shows the two packages, expanded by default
+ b.wait_text("table.updates-history tbody.pf-m-expanded td.history-pkgcount", "2 packages")
+ b.wait_in_text("table.updates-history tr.pf-m-expanded", "chocolate")
+ b.wait_in_text("table.updates-history tr.pf-m-expanded", "vanilla")
+
+ # system page has current state as well
+ b.go("/system")
+ b.enter_page("/system")
+ self.wait_checking_updates()
+ self.assertEqual(b.attr(self.update_icon, "data-pficon"), "check")
+ b.wait_text(self.update_text, "System is up to date")
+
+ @testlib.skipImage("TODO: Packagekit on Arch does not detect the pear update", "arch")
+ @testlib.skipImage("tracer not available", *OSesWithoutTracer)
+ def testTracer(self):
+ b = self.browser
+ m = self.machine
+
+ class Tracer(object):
+ def __init__(self, testObj, rebootRequired=False, testStatusCard=False, packageName="vanilla"):
+ self.testObj = testObj
+ self.rebootRequired = rebootRequired
+ self.testStatusCard = testStatusCard
+ self.packageName = packageName
+
+ def execute(self):
+ self.prepare()
+ self.test_frontend()
+ self.verify_backend()
+ self.cleanup()
+
+ def prepare(self):
+ if self.rebootRequired:
+ # setting app as static in tracer's helper file will cause it to require a reboot
+ self.testObj.write_file("/etc/tracer/applications.xml",
+ f"""
+<applications>
+ <app name="{self.packageName}" type="static" />
+</applications>
+""")
+
+ scriptContent = "#!/bin/sh\nsleep infinity"
+ unitContent = f"""
+[Service]
+ExecStart=/usr/local/bin/{self.packageName}
+"""
+ self.testObj.createPackage(self.packageName, "1", "1", install=True, changes="initial package with service and run script",
+ content={f"/usr/local/bin/{self.packageName}": scriptContent, f"/etc/systemd/system/{self.packageName}.service": unitContent},
+ postinst="chmod a+x /usr/local/bin/{0}; systemctl daemon-reload; systemctl start {0}.service".format(self.packageName))
+ self.testObj.createPackage(self.packageName, "1", "2",
+ content={f"/usr/local/bin/{self.packageName}": scriptContent, f"/etc/systemd/system/{self.packageName}.service": unitContent},
+ postinst=f"chmod a+x /usr/local/bin/{self.packageName}")
+
+ self.serviceStartTime = m.execute(f"systemctl show {self.packageName}.service --property=ExecMainStartTimestamp")
+
+ self.testObj.enableRepo()
+
+ def test_frontend(self):
+ m.start_cockpit()
+ b.login_and_go("/updates")
+
+ # check update is present
+ with b.wait_timeout(30):
+ b.wait_in_text("#status", "1 update available")
+ b.wait_in_text("#available-updates table", self.packageName)
+
+ # install updates
+ b.wait_visible("#install-all")
+ b.wait_in_text("#install-all", "Install all updates")
+ b.click("#install-all")
+ with b.wait_timeout(60):
+ b.wait_visible(".pf-v5-c-empty-state__title:contains('Update was successful')")
+
+ b.wait_visible("#ignore")
+ b.wait_visible(".updates-success-table")
+
+ if self.rebootRequired:
+ rowId = "#reboot-row"
+ else:
+ rowId = "#service-row"
+
+ b.wait_visible(f"{rowId}")
+ b.click(f"{rowId} button")
+ b.wait_visible(f"{rowId} + tr")
+ b.wait_in_text(f"{rowId} + tr", self.packageName)
+
+ # test the tracer functionality works also in the Status Card
+ if self.testStatusCard:
+ b.click("#ignore")
+
+ if self.rebootRequired:
+ b.wait_in_text("#status", "1 package needs a system reboot")
+ b.click("#packages-need-reboot button")
+ b.wait_visible("#shutdown-dialog")
+ b.click("#delay")
+ b.click("button:contains('No delay')")
+ b.wait_text("#delay .pf-v5-c-select__toggle-text", "No delay")
+ b.click("#shutdown-dialog button:contains('Reboot')")
+ b.switch_to_top()
+ b.wait_in_text(".curtains-ct h1", "Disconnected")
+
+ # ensure that rebooting actually worked
+ m.wait_reboot()
+ m.start_cockpit()
+ b.reload()
+ b.login_and_go("/updates")
+ else:
+ b.wait_in_text("#status", "1 service needs to be restarted")
+ b.click("#services-need-restart button")
+ b.wait_visible("#restart-services-modal")
+ b.wait_in_text(".restart-services-modal-body", self.packageName)
+ b.click("#restart-services-modal button:contains('Restart services')")
+ b.wait_not_present("#restart-services-modal")
+
+ # test the tracer functionality works in-page after successful update
+ else:
+ if self.rebootRequired:
+ # update required a reboot
+ b.wait_not_present("#choose-service")
+ b.click("#reboot-system")
+ b.wait_visible("#shutdown-dialog")
+ b.click("#delay")
+ b.click("button:contains('No delay')")
+ b.wait_text("#delay .pf-v5-c-select__toggle-text", "No delay")
+ b.click("#shutdown-dialog button:contains('Reboot')")
+ b.switch_to_top()
+ b.wait_in_text(".curtains-ct h1", "Disconnected")
+
+ # ensure that rebooting actually worked
+ m.wait_reboot()
+ m.start_cockpit()
+ b.reload()
+ b.login_and_go("/updates")
+ else:
+ # update required a service restart
+ b.wait_not_present("#reboot-system")
+ b.click("#choose-service")
+ b.wait_visible("#restart-services-modal")
+ b.wait_in_text(".restart-services-modal-body", self.packageName)
+ b.click("#restart-services-modal button:contains('Restart services')")
+ b.wait_not_present("#restart-services-modal")
+
+ # check no updates are present
+ b.wait_in_text("#status", "System is up to date")
+
+ # history on "up to date" page should show the recent update (expanded by default)
+ self.testObj.assertHistory("#expanded-content0 > td > div > ul", [self.packageName])
+
+ def verify_backend(self):
+ if not self.rebootRequired:
+ # Check the service was actually restarted
+ newServiceStartTime = m.execute(f"systemctl show {self.packageName}.service --property=ExecMainStartTimestamp")
+ self.testObj.assertGreater(newServiceStartTime, self.serviceStartTime)
+
+ # check no services/processes need reboot or service restart
+ # tracer returns non-zero exit code if any services/processes are affected
+ m.execute("tracer --all --root")
+
+ def cleanup(self):
+ if not self.rebootRequired:
+ m.execute(f"systemctl stop {self.packageName}")
+ if m.image == "arch":
+ m.execute(f"pacman -R --noconfirm {self.packageName}")
+ else:
+ m.execute(f"rpm -e {self.packageName}")
+ if self.rebootRequired:
+ m.execute("rm /etc/tracer/applications.xml")
+
+ Tracer(
+ testObj=self,
+ packageName="apple",
+ rebootRequired=True,
+ testStatusCard=False,
+ ).execute()
+
+ Tracer(
+ testObj=self,
+ packageName="cherry",
+ rebootRequired=False,
+ testStatusCard=False,
+ ).execute()
+
+ Tracer(
+ testObj=self,
+ packageName="pear",
+ rebootRequired=True,
+ testStatusCard=True,
+ ).execute()
+
+ Tracer(
+ testObj=self,
+ packageName="banana",
+ rebootRequired=False,
+ testStatusCard=True,
+ ).execute()
+
+ @testlib.skipImage("Arch Linux does not start services by default", "arch")
+ @testlib.skipImage("tracer not available", *OSesWithoutTracer)
+ @testlib.nondestructive
+ def testFailServiceRestart(self):
+ b = self.browser
+ m = self.machine
+
+ packageName = "apple"
+ scriptContent = "#!/bin/sh\nsleep infinity"
+ unitContent = f"""
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/{packageName}
+"""
+ self.createPackage(packageName, "1", "1", install=True, changes="initial package with service and run script",
+ content={f"/usr/local/bin/{packageName}": scriptContent, f"/etc/systemd/system/{packageName}.service": unitContent},
+ postinst=f"chmod a+x /usr/local/bin/{packageName}; systemctl daemon-reload; systemctl start {packageName}.service")
+
+ scriptContent = "#!/bin/sh\nfalse"
+ unitContent = f"""
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/usr/local/bin/{packageName}
+"""
+ self.createPackage(packageName, "1", "2",
+ content={f"/usr/local/bin/{packageName}": scriptContent, f"/etc/systemd/system/{packageName}.service": unitContent})
+
+ self.enableRepo()
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+
+ # check update is present
+ with b.wait_timeout(30):
+ b.wait_in_text("#status", "1 update available")
+ b.wait_in_text("#available-updates table", packageName)
+
+ # install updates
+ b.wait_visible("#install-all")
+ b.wait_in_text("#install-all", "Install all updates")
+ b.click("#install-all")
+ with b.wait_timeout(60):
+ b.wait_visible(".pf-v5-c-empty-state__title:contains('Update was successful')")
+
+ b.wait_visible("#ignore")
+ b.wait_visible(".updates-success-table")
+
+ b.wait_visible("#service-row")
+ b.click("#service-row button")
+ b.wait_visible("#service-row + tr")
+ b.wait_in_text("#service-row + tr", packageName)
+
+ # update required a service restart
+ b.wait_not_present("#reboot-system")
+ b.click("#choose-service")
+ b.wait_visible("#restart-services-modal")
+ b.wait_in_text(".restart-services-modal-body", packageName)
+ b.click("#restart-services-modal button:contains('Restart services')")
+ b.wait_visible("#restart-services-modal")
+
+ # tracer updated the list of services which need a restart, so our service is no longer present
+ b.wait_not_in_text("#restart-services-modal .pf-v5-l-stack__item", packageName)
+ b.wait_visible("#restart-services-modal .pf-v5-c-alert")
+
+ @testlib.skipImage("No security changelog support in packagekit", "arch")
+ def testInfoSecurity(self):
+ b = self.browser
+ m = self.machine
+
+ # just changelog
+ self.createPackage("norefs-bin", "1", "1", install=True)
+ self.createPackage("norefs-bin", "2", "1", severity="enhancement",
+ changes="Now 10% *more* [unicorns](http://unicorn.example.com)")
+ # binary from same source
+ self.createPackage("norefs-doc", "1", "1", install=True)
+ self.createPackage("norefs-doc", "2", "1", severity="enhancement",
+ changes="Now 10% *more* [unicorns](http://unicorn.example.com)")
+ # bug fixes
+ self.createPackage("buggy", "2", "1", install=True)
+ self.createPackage("buggy", "2", "2", changes="* Fixit", bugs=[123, 456])
+ # security fix with proper CVE list and severity
+ self.createPackage("secdeclare", "3", "4.a1", install=True)
+ self.createPackage("secdeclare", "3", "4.b1", severity="security",
+ changes="Will crash your data center", cves=["CVE-2014-123456"])
+ # security fix with parsing from changes
+ self.createPackage("secparse", "4", "1", install=True)
+ self.createPackage("secparse", "4", "2", changes="Fix CVE-2014-54321 and CVE-2017-9999.")
+ # security fix with RHEL severity and errata
+ self.createPackage("sevcritical", "5", "1", install=True)
+ self.createPackage("sevcritical", "5", "2", cves=["CVE-2014-54321"], securitySeverity="critical",
+ errata=["RHSA-2000:0001", "RHSA-2000:0002"], changes="More broken stuff")
+
+ self.enableRepo()
+ m.execute("pkcon refresh")
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+ with b.wait_timeout(30):
+ b.wait_visible("#available-updates")
+ b.wait_in_text("#status", "6 updates available, including 3 security fixes")
+
+ b.wait_in_text("table[aria-label='Available updates']", "sevcritical")
+
+ # security updates should get sorted on top and then alphabetically, so start with "secdeclare"
+ sel = self.check_nth_update(1, "secdeclare", "3-4.b1", "security", 1,
+ desc_matches=["Will crash your data center"], cves=["CVE-2014-123456"])
+ # should not have erratum label in details
+ self.assertNotIn("Errat", b.text(sel))
+
+ # secparse should also be considered a security update as the changelog mentions CVEs
+ self.check_nth_update(2, "secparse", "4-2", "security", 2,
+ desc_matches=["Fix CVE-2014-54321 and CVE-2017-9999."],
+ cves=["CVE-2014-54321", "CVE-2017-9999"])
+ sel = self.check_nth_update(3, "sevcritical", "5-2", "security", 1,
+ desc_matches=["More broken stuff"])
+
+ # buggy: bug refs, no security
+ sel = self.check_nth_update(4, "buggy", "2-2", "bug", 2, bugs=["123", "456"], desc_matches=["Fixit"])
+ # should filter out enumeration in overview
+ ch = b.eval_js(f"document.querySelector(\"{sel + ' td.changelog'}\").innerHTML")
+ self.assertNotIn("<li>", ch)
+ self.assertNotIn("*", ch)
+ # should show bug fix icon and pf-v5-c-tooltip
+ self.assertEqual(b.attr(sel + " .severity-icon", "aria-label"), "bug fix")
+ b.mouse(sel + " .severity-icon", "mouseenter")
+ b.wait_text(".pf-v5-c-tooltip", "bug fix")
+ b.mouse(sel + " .severity-icon", "mouseleave")
+ b.wait_not_present(".pf-v5-c-tooltip")
+
+ # norefs: just changelog, show both binary packages
+ sel = self.check_nth_update(5, ["norefs-bin", "norefs-doc"], "2-1", self.enhancement_severity,
+ desc_matches=["Now 10% more unicorns"])
+ # verify Markdown formatting in table cell
+ self.assertEqual(b.text(sel + " td.changelog em"), "more") # *more*
+ self.assertEqual(b.attr(sel + " td.changelog a", "href"), "http://unicorn.example.com")
+ self.assertEqual(b.attr(sel + " td.changelog a", "target"), "_blank")
+ # verify Markdown formatting in details
+ self.assertEqual(b.text(sel + " .pf-m-expanded em"), "more") # *more*
+ self.assertEqual(b.attr(sel + " .pf-m-expanded a:first-of-type", "href"), "http://unicorn.example.com")
+ self.assertEqual(b.attr(sel + " .pf-m-expanded a:first-of-type", "target"), "_blank")
+
+ # verify that changelog is absent in mobile
+ b.set_layout("mobile")
+ b.wait_not_visible(sel + " td.changelog em")
+ b.set_layout("desktop")
+ b.wait_visible(sel + " td.changelog em")
+
+ # updates are shown on system page
+ b.go("/system")
+ b.enter_page("/system")
+ self.wait_checking_updates()
+ b.wait_text(self.update_text, "Security updates available")
+ b.wait_attr(self.update_icon, "data-pficon", "security")
+
+ # should be a link, click on it to go to back to /updates
+ b.click(self.update_text_action)
+ b.enter_page("/updates")
+
+ # install only security updates
+ self.assertEqual(b.text("#available-updates button#install-security"), "Install security updates")
+ b.wait_not_present("#available-updates button#install-kpatches")
+ b.click("#available-updates button#install-security")
+ with b.wait_timeout(60):
+ b.wait_visible(".pf-v5-c-empty-state__title:contains('Update was successful')")
+
+ # history on restart page should show the three security updates
+ b.click(".pf-v5-c-expandable-section__toggle")
+ self.assertHistory(".pf-v5-c-expandable-section ul", ["secdeclare", "secparse", "sevcritical"])
+
+ # ignore restarting
+ b.click("#ignore")
+
+ # should have succeeded; 3 non-security updates left
+ b.wait_in_text("#status", "3 updates available")
+ b.wait_in_text("#available-updates h2", "Available updates")
+
+ b.wait_in_text("#available-updates table", "norefs-doc")
+ self.assertIn("buggy", b.text("#available-updates table"))
+ self.assertNotIn("secdeclare", b.text("#available-updates table"))
+ self.assertNotIn("secparse", b.text("#available-updates table"))
+
+ # history should show the security updates
+ self.assertHistory("table.updates-history #expanded-content0 ul", ["secdeclare", "secparse", "sevcritical"])
+
+ # stop PackageKit (e. g. idle timeout) to make sure the page survives that
+ m.execute("systemctl stop packagekit; systemctl reset-failed packagekit || true")
+
+ # new security versions are now installed
+ m.execute("test -f /stamp-secdeclare-3-4.b1; test -f /stamp-secparse-4-2; test -f /stamp-sevcritical-5-2")
+ # but the three others are untouched
+ m.execute("test -f /stamp-buggy-2-1; test -f /stamp-norefs-bin-1-1; test -f /stamp-norefs-doc-1-1")
+
+ # should now only have one button (no security updates left)
+ self.assertEqual(b.text("#available-updates button#install-all"), "Install all updates")
+ b.click("#available-updates button#install-all")
+
+ # should have succeeded and show restart
+ with b.wait_timeout(60):
+ b.wait_visible(".pf-v5-c-empty-state__title:contains('Update was successful')")
+ b.wait_visible("#ignore")
+
+ # history on restart page should show the three non-security updates
+ b.click(".pf-v5-c-expandable-section__toggle")
+ self.assertHistory(".pf-v5-c-expandable-section ul", ["buggy", "norefs-bin", "norefs-doc"])
+
+ if m.image in OSesWithoutTracer:
+ # do the reboot; this will disconnect the web UI
+ b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
+ b.wait_visible("#shutdown-dialog")
+ b.click("#delay")
+ b.click("button:contains('No delay')")
+ b.wait_text("#delay .pf-v5-c-select__toggle-text", "No delay")
+ b.click("#shutdown-dialog button:contains('Reboot')")
+ b.switch_to_top()
+ b.wait_in_text(".curtains-ct h1", "Disconnected")
+
+ # ensure that rebooting actually worked
+ m.wait_reboot()
+ m.start_cockpit()
+ b.reload()
+ b.login_and_go("/updates")
+ else:
+ b.click("#ignore")
+
+ # new versions are now installed
+ m.execute("test -f /stamp-norefs-bin-2-1; test -f /stamp-norefs-doc-2-1")
+
+ # no further updates
+ b.wait_in_text("#status", "System is up to date")
+
+ # history on "up to date" page should show the recent update (expanded by default)
+ self.assertHistory("table.updates-history tbody.pf-m-expanded ul", ["buggy", "norefs-bin", "norefs-doc"])
+ # and the previous one, not expaned
+ b.wait_visible("table.updates-history tbody:not(.pf-m-expanded)")
+
+ @testlib.skipImage("No security changelog support in packagekit", "arch")
+ @testlib.nondestructive
+ def testSecurityOnly(self):
+ b = self.browser
+ m = self.machine
+
+ # security fix with proper CVE list and severity
+ self.createPackage("secdeclare", "3", "4.a1", install=True)
+ self.createPackage("secdeclare", "3", "4.b1", severity="security",
+ changes="Will crash your data center", cves=['CVE-2014-123456'])
+
+ self.enableRepo()
+ m.execute("pkcon refresh")
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+ with b.wait_timeout(30):
+ b.wait_visible("#available-updates")
+ b.wait_in_text("#status", "1 security fix available")
+
+ # should only have one button (only security updates)
+ b.wait_not_present("#available-updates button#install-all")
+ b.wait_not_present("#available-updates button#install-kpatches")
+ self.assertEqual(b.text("#available-updates button#install-security"), "Install security updates")
+
+ # security fix without CVE URLs
+ if self.supports_severity:
+ self.createPackage("secnocve", "1", "1", install=True)
+ self.createPackage("secnocve", "1", "2", severity="security", changes="Fix leak")
+ self.enableRepo()
+ # check for updates
+ b.click("#status .pf-v5-c-card__header button")
+ with b.wait_timeout(30):
+ b.wait_visible("#available-updates")
+ b.wait_in_text("#status", "2")
+
+ b.wait_in_text("table[aria-label='Available updates']", "secnocve")
+
+ # secnocve should be displayed properly
+ self.check_nth_update(2, "secnocve", "1-2", "security", desc_matches=["Fix leak"])
+
+ @testlib.skipImage("No changelog support in Arch Linux", "arch")
+ @testlib.nondestructive
+ def testInfoTruncation(self):
+ b = self.browser
+ m = self.machine
+
+ # update with not too many binary packages
+ for i in range(4):
+ self.createPackage(f"coarse{i:02}", "1", "1", install=True)
+ self.createPackage(f"coarse{i:02}", "1", "2", changes="make it greener")
+
+ # update with lots of binary packages
+ for i in range(10):
+ self.createPackage(f"fine{i:02}", "1", "1", install=True)
+ self.createPackage(f"fine{i:02}", "1", "2", changes="make it better")
+
+ # update with long changelog
+ long_changelog = ""
+ for i in range(30):
+ long_changelog += f" - Things change #{i:02}\n"
+ self.createPackage("verbose", "1", "1", install=True)
+ self.createPackage("verbose", "1", "2", changes=long_changelog)
+
+ self.enableRepo()
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+
+ with b.wait_timeout(30):
+ b.wait_in_text("table[aria-label='Available updates']", "Things change")
+
+ # "coarse" package list should be complete
+ t = b.text("#app .ct-table tbody:nth-of-type(1) tr:first-child [data-label=Name]")
+ self.assertIn("coarse00", t)
+ self.assertIn("coarse03", t)
+ self.assertNotIn(u"…", t)
+
+ # "fine" package list should be truncated
+ t = b.text("#app .ct-table tbody:nth-of-type(2) tr:first-child [data-label=Name]")
+ self.assertIn("fine00", t)
+ self.assertIn("fine03", t)
+ self.assertNotIn("fine09", t)
+ self.assertIn(u"…", t)
+ # but complete in the details
+ self.check_nth_update(2, ["fine00", "fine01", "fine02", "fine03"], "1-2",
+ desc_matches=["fine07", "fine09"])
+
+ # changelog should be truncated
+ desc = b.text("#app .ct-table tbody:nth-of-type(3) [data-label=Details]")
+ self.assertIn("Things change #00", desc)
+ self.assertNotIn("#01", desc)
+
+ # and not visible on mobile
+ b.set_layout("mobile")
+ b.wait_not_visible("#app .ct-table tbody:nth-of-type(3) [data-label=Details]")
+ # but complete in the details
+ self.check_nth_update(3, "verbose", "1-2",
+ desc_matches=["Things change #00", "Things change #29"])
+
+ # also on desktop
+ b.set_layout("desktop")
+ b.wait_visible("#app .ct-table tbody:nth-of-type(3) [data-label=Details]")
+ # collapse it so that check_nth_update is happy
+ b.click("#available-updates table[aria-label='Available updates'] > tbody:nth-of-type(3) td.pf-v5-c-table__toggle button")
+ self.check_nth_update(3, "verbose", "1-2",
+ desc_matches=["Things change #00", "Things change #29"])
+
+ # seems we can't verify that the description has a scrollbar
+
+ def testRebootAfterSuccess(self):
+ b = self.browser
+ m = self.machine
+
+ install_lockfile = "/tmp/finish-pk"
+ # create two updates; force installing chocolate before vanilla
+ self.createPackage("vanilla", "1.0", "1", install=True)
+ self.createPackage("vanilla", "1.0", "2",
+ postinst=f"while [ ! -e {install_lockfile} ]; do sleep 1; done; rm -f {install_lockfile}")
+ self.enableRepo()
+ m.execute("pkcon refresh")
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+ with b.wait_timeout(30):
+ b.wait_visible("#available-updates")
+ b.click("#available-updates button#install-all")
+
+ b.wait_visible("#app div.pf-v5-c-progress__bar")
+ # auto-reboot is off by default
+ b.wait_visible("#reboot-after:not(:checked)")
+ b.click("#reboot-after")
+ b.wait_visible("#reboot-after:checked")
+
+ m.execute(f"touch {install_lockfile}")
+ # reboots automatically
+ b.switch_to_top()
+ b.wait_in_text(".curtains-ct h1", "Disconnected")
+ m.wait_reboot()
+ self.allow_restart_journal_messages()
+
+ @testlib.nondestructive
+ def testUpdateError(self):
+ b = self.browser
+ m = self.machine
+
+ self.createPackage("vapor", "1", "1", install=True)
+ self.createPackage("vapor", "1", "2")
+
+ self.enableRepo()
+ m.execute("pkcon refresh")
+
+ # break the upgrade by removing the generated packages from the repo
+ m.execute(f"rm -f {self.repo_dir}/vapor*.deb {self.repo_dir}/vapor*.rpm {self.repo_dir}/vapor*.pkg*")
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+ with b.wait_timeout(30):
+ b.wait_visible("#available-updates")
+ b.wait_in_text("#status", "1 update available")
+
+ b.click("#available-updates button#install-all")
+
+ # error message visible
+ b.wait_visible("#app .pf-v5-c-page .pf-v5-c-empty-state__body")
+
+ self.assertRegex(b.text("#app .pf-v5-c-page .pf-v5-l-stack .pf-v5-c-code-block .pf-v5-c-code-block__content .pf-v5-c-code-block__pre .pf-v5-c-code-block__code span:first-of-type"),
+ "missing|downloading|not.*available|No such file or directory|download library error")
+
+ # not expecting any buttons
+ self.assertFalse(b.is_present("#app button"))
+
+ @testlib.nondestructive
+ def testPackageKitCrash(self):
+ b = self.browser
+ m = self.machine
+
+ # this tends to corrupt the rpm database, so do a backup/restore
+ if self.backend == 'dnf':
+ # https://www.fedoraproject.org/wiki/Changes/RelocateRPMToUsr
+ exists = self.machine.execute("if test -e %s; then echo yes; fi" % "/usr/lib/sysimage/rpm").strip() != ""
+ if exists:
+ self.restore_dir("/usr/lib/sysimage/rpm")
+ else:
+ self.restore_dir("/var/lib/rpm")
+
+ # make sure we have enough time to crash PK
+ self.createPackage("slow", "1", "1", install=True)
+ # we don't want this installation to finish
+ self.createPackage("slow", "1", "2", postinst="sleep infinity")
+ self.enableRepo()
+ m.execute("pkcon refresh")
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+
+ with b.wait_timeout(30):
+ b.click("#available-updates button#install-all")
+
+ # let updates start and zap PackageKit
+ b.wait_visible("#app div.pf-v5-c-progress__bar")
+ m.execute("systemctl kill --signal=SEGV packagekit.service")
+ # this crash creates so many messages from systemd-coredump, debug metadata etc. that
+ # trying to keep up with specific patterns is too brittle
+ self.allow_journal_messages(".*")
+
+ # error message visible
+ b.wait_in_text("#app .pf-v5-c-page .pf-v5-l-stack .pf-v5-c-code-block__content", "PackageKit crashed")
+
+ @testlib.nondestructive
+ def testNoPackageKit(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("systemctl stop packagekit")
+ system_service = m.execute("systemctl show -p FragmentPath packagekit.service | cut -f2 -d=").strip()
+ m.execute(f"""mv {system_service} {system_service}.disabled
+ mv /usr/share/dbus-1/system-services/org.freedesktop.PackageKit.service /usr/share/dbus-1/system-services/org.freedesktop.PackageKit.service.disabled
+ systemctl daemon-reload""")
+ self.addCleanup(m.execute,
+ f"""mv {system_service}.disabled {system_service}
+ mv /usr/share/dbus-1/system-services/org.freedesktop.PackageKit.service.disabled /usr/share/dbus-1/system-services/org.freedesktop.PackageKit.service
+ systemctl daemon-reload""")
+
+ m.start_cockpit()
+ if not self.is_pybridge():
+ # TODO: conditions not implemented on C bridge; once we drop it, drop this special case and
+ # enable the manifest documentation for conditions in doc/guide/packages.xml
+ self.assertIn("\nupdates", m.execute("cockpit-bridge --packages"))
+ b.login_and_go("/updates")
+
+ # error message present
+ b.wait_in_text(".pf-v5-c-page__main-section .pf-v5-l-stack .pf-v5-c-code-block__content", "PackageKit is not installed")
+
+ # update status on front page should show error
+ b.go("/system")
+ b.enter_page("/system")
+ b.wait_text(self.update_text, "Loading available updates failed")
+ self.assertIn("exclamation", b.attr(self.update_icon, "class"))
+ else:
+ self.assertNotIn("\nupdates", m.execute("cockpit-bridge --packages"))
+ # should not appear in the menu at all
+ self.login_and_go(None)
+ b.wait_in_text("#host-apps .pf-m-current", "Overview")
+ self.assertNotIn("Updates", b.text("#host-apps .pf-m-current"))
+
+ # update status on front page should be invisible
+ b.go("/system")
+ b.enter_page("/system")
+ b.wait_visible(".system-health")
+ self.assertFalse(b.is_present(self.update_text))
+
+ @testlib.skipDistroPackage()
+ @testlib.nondestructive
+ def testUnprivileged(self):
+ b = self.browser
+ m = self.machine
+
+ self.createPackage("vanilla", "1.0", "1", install=True)
+ self.createPackage("vanilla", "2.0", "2")
+ self.enableRepo()
+ m.execute("pkcon refresh")
+
+ # getting update info is allowed to all users
+ self.login_and_go("/updates", superuser=False)
+ with b.wait_timeout(30):
+ b.wait_in_text("#status", "1 update available")
+ b.wait_visible("#available-updates")
+
+ # but applying updates is not; FIXME: this is a crappy UX
+ b.click("#available-updates button#install-all")
+ # error message visible
+ b.wait_in_text("#app .pf-v5-c-code-block__content", "authentication")
+
+ # page adjusts automatically to privilege change
+ b.become_superuser()
+ b.wait_in_text("#status", "1 update available")
+ b.wait_visible("#available-updates")
+
+ # applying updates works now
+ b.click("#available-updates button#install-all")
+
+ with b.wait_timeout(60):
+ b.wait_visible(".pf-v5-c-empty-state__title:contains('Update was successful')")
+ b.click("#ignore")
+
+ # should go back to updates overview, nothing pending any more
+ # TODO make Packagekit GetUpdates work for tests properly
+ b.wait_not_present("#available-updates")
+
+
+@testlib.skipImage("TODO: Arch Linux has no cockpit-ws package, it's in cockpit", "arch")
+@testlib.skipOstree("Image uses OSTree")
+class TestWsUpdate(NoSubManCase):
+ def testBasic(self):
+ # The main case for this is that cockpit-ws itself gets upgraded, which
+ # restarts the service and terminates the connection. As we can't
+ # (efficiently) build a newer working cockpit-ws package, test the two
+ # parts (reconnect and warning about disconnect) separately.
+
+ # no security updates, no changelogs
+ b = self.browser
+ m = self.machine
+
+ install_lockfile = "/tmp/finish-pk"
+ # updating this package takes longer than a cockpit start and building the page
+ self.createPackage("slow", "1", "1", install=True)
+ self.createPackage(
+ "slow", "1", "2", postinst="while [ ! -e {0} ]; do sleep 1; done; rm -f {0}".format(install_lockfile))
+ self.enableRepo()
+ m.execute("pkcon refresh")
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+
+ with b.wait_timeout(30):
+ b.click("#available-updates button#install-all")
+
+ # applying updates panel present
+ b.wait_in_text("#app div.progress-description", "slow")
+
+ # restarting should pick up that install progress
+ m.restart_cockpit()
+ b.login_and_go("/updates")
+
+ b.wait_in_text("#app div.progress-description", "slow 1-2")
+ # progress bar has some reasonable value
+ progress = b.get_pf_progress_value(".progress-main-view")
+ self.assertGreater(progress, 20)
+ self.assertLess(progress, 80)
+
+ # finish the package installation
+ m.execute(f"touch {install_lockfile}")
+
+ # should have succeeded and show restart page; cancel
+ b.wait_visible(".pf-v5-c-empty-state__title:contains('Update was successful')")
+ b.click("#ignore")
+ b.wait_in_text("#status", "System is up to date")
+
+ # now pretend that there is a newer cockpit-ws available, warn about disconnect
+ self.createPackage("cockpit-ws", "999", "1")
+ # these have strict version dependencies to cockpit-ws, don't get in the way
+ self.createPackage("cockpit", "999", "1")
+ m.execute("if type apt; then dpkg -P cockpit-ws-dbgsym; fi")
+ self.enableRepo()
+ b.click("#status .pf-v5-c-card__header button")
+
+ with b.wait_timeout(30):
+ b.wait_visible("#available-updates")
+ b.wait_in_text("#status", "2 updates available")
+ b.wait_in_text("table[aria-label='Available updates']", "cockpit-ws")
+
+ b.wait_visible(".cockpit-update-warning")
+ b.wait_in_text(".cockpit-update-warning-text", "Web Console will restart")
+
+ self.allow_restart_journal_messages()
+
+
+@testlib.skipImage("kpatch is not available", *OSesWithoutKpatch)
+class TestKpatchInstall(NoSubManCase):
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("rpm --erase --verbose kpatch kpatch-dnf")
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+ with b.wait_timeout(30):
+ b.wait_visible("#status")
+ b.wait_in_text("#kpatch-settings", "Not available")
+ # show unavailable packages in popover
+ b.click("#kpatch-settings .ct-info-circle")
+ b.wait_in_text(".pf-v5-c-popover", "Unavailable packages")
+ b.wait_in_text(".pf-v5-c-popover", "kpatch-dnf")
+ b.click("#kpatch-settings .ct-info-circle")
+ b.wait_not_present(".pf-v5-c-popover")
+
+ dummy_service = "[Service]\nExecStart=/bin/sleep infinity\n[Install]\nWantedBy=multi-user.target\n"
+ self.createPackage("kpatch", "999", "1", content={"/lib/systemd/system/kpatch.service": dummy_service})
+ self.createPackage("kpatch-dnf", "999", "1", content={"/etc/dnf/plugins/kpatch.conf": ""})
+ self.enableRepo()
+
+ b.reload()
+ b.enter_page("/updates")
+
+ b.wait_in_text("#kpatch-settings", "Not installed")
+ b.click("#kpatch-settings button:contains('Install')")
+ b.wait_in_text("#dialog", "kpatch, kpatch-dnf will be installed")
+ b.click("#dialog button:contains('Install')")
+
+ b.wait_in_text("#kpatch-settings", "Disabled")
+
+ # kpatch and kpatch-dnf should be installed
+ m.execute("rpm -q kpatch kpatch-dnf")
+
+
+@testlib.onlyImage("No subscriptions", "rhel-*")
+class TestUpdatesSubscriptions(packagelib.PackageCase):
+ provision = {
+ "0": {"address": "10.111.112.1/20", "dns": "10.111.112.1", "memory_mb": 512},
+ "services": {"image": "services", "memory_mb": 1024}
+ }
+
+ def register(self):
+ # this fails with "Unable to find available subscriptions for all your installed products", but works anyway
+ self.machine.execute(
+ "LC_ALL=C.UTF-8 subscription-manager register --serverurl https://services.cockpit.lan:8443/candlepin --org=admin --activationkey=awesome_os_pool || true")
+ self.machine.execute("LC_ALL=C.UTF-8 subscription-manager attach --auto")
+
+ def setUp(self):
+ super().setUp()
+ self.candlepin = self.machines['services']
+ m = self.machine
+
+ # wait for candlepin to be active and verify
+ self.candlepin.execute("/root/run-candlepin")
+
+ # remove all existing products (RHEL server), as we can't control them
+ m.execute("rm -f /etc/pki/product-default/*.pem /etc/pki/product/*.pem")
+
+ # download product info from the candlepin machine and install it
+ product_file = os.path.join(self.tmpdir, "88888.pem")
+ self.candlepin.download("/home/admin/candlepin/generated_certs/88888.pem", product_file)
+
+ # # upload product info to the test machine
+ m.execute("mkdir -p /etc/pki/product")
+ m.upload([product_file], "/etc/pki/product")
+
+ # set up CA
+ ca = self.candlepin.execute("cat /home/admin/candlepin/certs/candlepin-ca.crt")
+ m.write("/etc/pki/ca-trust/source/anchors/candlepin-ca.crt", ca)
+ m.write("/etc/hosts", "10.111.112.100 services.cockpit.lan\n", append=True)
+ m.execute("cp /etc/pki/ca-trust/source/anchors/candlepin-ca.crt /etc/rhsm/ca/candlepin-ca.pem")
+ m.execute("update-ca-trust")
+
+ # Wait for the web service to be accessible
+ m.execute("until curl --fail --silent --show-error https://services.cockpit.lan:8443/candlepin/status; do sleep 1; done")
+ self.update_icon = "#page_status_notification_updates svg"
+ self.update_text = "#page_status_notification_updates"
+ self.update_text_action = "#page_status_notification_updates a"
+
+ def testNoUpdates(self):
+ m = self.machine
+ b = self.browser
+
+ # fresh machine, no updates available; by default our rhel-* images are not registered
+ m.start_cockpit()
+ b.login_and_go("/system")
+ # show unregistered status on system front page
+ with b.wait_timeout(30):
+ b.wait_in_text(self.update_text, "Not registered")
+ self.assertIn("triangle", b.attr(self.update_icon, "class"))
+
+ # software updates page also shows unregistered
+ b.go("/updates")
+ b.enter_page("/updates")
+ # empty state visible in main area
+ b.wait_visible(".pf-v5-c-empty-state button")
+ b.wait_in_text(".pf-v5-c-empty-state", "This system is not registered")
+
+ # test the button to switch to Subscriptions
+ b.click(".pf-v5-c-empty-state button")
+ b.switch_to_top()
+ b.wait_js_cond('window.location.pathname === "/subscriptions"')
+
+ # after registration it should show the usual "system is up to date", through the "status changed" signal
+ self.register()
+ b.go("/updates")
+ b.enter_page("/updates")
+ # check updates
+ b.wait_visible("#status .pf-v5-c-card__header button")
+ b.wait_in_text("#status", "System is up to date")
+
+ # same on system page
+ b.go("/system")
+ b.enter_page("/system")
+ self.assertEqual(b.attr(self.update_icon, "data-pficon"), "check")
+ b.wait_text(self.update_text, "System is up to date")
+
+ def testAvailableUpdates(self):
+ m = self.machine
+ b = self.browser
+
+ # one available update
+ self.createPackage("vanilla", "1.0", "1", install=True)
+ self.createPackage("vanilla", "1.0", "2")
+ self.enableRepo()
+
+ m.start_cockpit()
+
+ b.login_and_go("/system")
+ # by default our rhel-* images are not registered; show warning on system page
+ with b.wait_timeout(30):
+ b.wait_in_text(self.update_text, "Not registered")
+ self.assertIn("triangle", b.attr(self.update_icon, "class"))
+ # should be a link leading to subscriptions page
+ b.click(self.update_text_action)
+ b.enter_page("/subscriptions")
+
+ # software updates page also shows unregistered
+ b.go("/updates")
+ b.enter_page("/updates")
+
+ # empty state visible in main area
+ b.wait_visible(".pf-v5-c-empty-state button")
+ b.wait_in_text(".pf-v5-c-empty-state", "This system is not registered")
+
+ # after registration it should show available updates
+ self.register()
+ b.go("/updates")
+ b.enter_page("/updates")
+ b.wait_not_present(".pf-v5-c-empty-state")
+ with b.wait_timeout(30):
+ b.wait_visible("#available-updates")
+ # no update history yet
+ self.assertFalse(b.is_present("table.updates-history"))
+
+ # has action buttons
+ b.wait_visible("#status .pf-v5-c-card__header button")
+ self.assertEqual(b.text("#available-updates button#install-all"), "Install all updates")
+
+ # show available updates on system page too
+ b.go("/system")
+ b.enter_page("/system")
+ b.wait_text(self.update_text, "Bug fix updates available")
+ self.assertEqual(b.attr(self.update_icon, "data-pficon"), "bug")
+
+ def testNoSubOsRepo(self):
+ m = self.machine
+ b = self.browser
+
+ # pretend we have a proper OS repo that does not require subscription
+ self.createPackage("coreutils", "999", "1")
+ self.enableRepo()
+ m.execute("pkcon refresh")
+
+ m.start_cockpit()
+ b.login_and_go("/system")
+ with b.wait_timeout(30):
+ b.wait_text(self.update_text, "System is up to date")
+
+ b.go("/updates")
+ b.enter_page("/updates")
+ b.wait_visible("#status .pf-v5-c-card__header button")
+ b.wait_in_text("#status", "System is up to date")
+
+
+@testlib.skipOstree("Image uses OSTree")
+@testlib.nondestructive
+class TestAutoUpdates(NoSubManCase):
+
+ def setUp(self):
+ super().setUp()
+ # not implemented for apt yet, only dnf
+ self.supported_backend = self.backend in ["dnf"]
+ if self.backend == 'dnf':
+ self.restore_file("/etc/dnf/automatic.conf")
+ self.addCleanup(self.machine.execute, "systemctl disable --now dnf-automatic-install.timer 2>/dev/null; rm -rf /etc/systemd/system/dnf-automatic-*")
+
+ def closeSettings(self, browser):
+ browser.click("#automatic-updates-dialog button:contains('Save changes')")
+ with browser.wait_timeout(30):
+ browser.wait_not_present("#automatic-updates-dialog")
+
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+ with b.wait_timeout(30):
+ b.wait_visible("#status")
+
+ if not self.supported_backend:
+ self.assertFalse(b.is_present("#automatic"))
+ self.assertFalse(b.is_present("#auto-update-type"))
+ return
+
+ def assertTimerDnf(hour, dow):
+ out = m.execute("systemctl --no-legend list-timers dnf-automatic-install.timer")
+ if hour:
+ # don't test the minutes, due to RandomizedDelaySec=60m
+ self.assertRegex(out, f" {hour}:")
+ else:
+ self.assertEqual(out, "")
+ if dow:
+ self.assertRegex(out, r"^%s\s" % dow)
+ else:
+ # "every day" should not have a "LEFT" time > 1 day
+ self.assertNotIn(" day", out)
+
+ # service should not run right away
+ self.assertEqual(m.execute("systemctl is-active dnf-automatic-install.service || true").strip(), "inactive")
+
+ # automatic reboots should be enabled whenever timer is enabled
+ out = m.execute("systemctl cat dnf-automatic-install.service")
+ if hour:
+ if m.image.startswith("rhel-8") or m.image == "centos-8-stream":
+ # for RHEL 8, dnf-automatic does not support reboot; we have a unit drop-in hack
+ self.assertRegex(out, "ExecStartPost=/.*shutdown")
+ # validate our assumption
+ self.assertNotIn("reboot", m.execute("cat /etc/dnf/automatic.conf"))
+ else:
+ # newer dnf supports that natively
+ self.assertNotIn("ExecStartPost", out)
+ self.assertIn("reboot = when-needed", m.execute("cat /etc/dnf/automatic.conf"))
+ else:
+ self.assertNotIn("ExecStartPost", out)
+
+ def assertTimer(hour, dow=None):
+ if self.backend == "dnf":
+ assertTimerDnf(hour, dow)
+ else:
+ raise NotImplementedError(self.backend)
+
+ def assertTypeDnf(_type):
+ if _type == "all":
+ match = '= default'
+ elif _type == "security":
+ match = '= security'
+ else:
+ raise ValueError(_type)
+
+ self.assertIn(match, m.execute("grep upgrade_type /etc/dnf/automatic.conf"))
+
+ def assertType(_type):
+ if self.backend == "dnf":
+ assertTypeDnf(_type)
+ else:
+ raise NotImplementedError(self.backend)
+
+ # automatic updates are supported, but off
+ b.wait_in_text("#autoupdates-settings", "Disabled")
+ assertTimer(None)
+
+ # enable
+ b.click("#autoupdates-settings button:contains('Edit')")
+ b.wait_visible("#automatic-updates-dialog")
+ b.click("#all-updates")
+ b.wait_val("#auto-update-time-input", "06:00")
+ b.wait_in_text("#auto-update-day", "every day")
+ self.closeSettings(b)
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 06:00")
+ assertTimer("06")
+ assertType("all")
+
+ # change type to security
+ b.click("#autoupdates-settings button:contains('Edit')")
+ b.wait_visible("#automatic-updates-dialog")
+ b.click("#security-updates")
+ self.closeSettings(b)
+ b.wait_in_text("#autoupdates-settings", "Security updates will be applied every day at 06:00")
+ assertType("security")
+ assertTimer("06")
+
+ # change it back
+ b.click("#autoupdates-settings button:contains('Edit')")
+ b.wait_visible("#automatic-updates-dialog")
+ b.click("#all-updates")
+ self.closeSettings(b)
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 06:00")
+ assertType("all")
+ assertTimer("06")
+
+ # change day
+ b.click("#autoupdates-settings button:contains('Edit')")
+ b.wait_visible("#automatic-updates-dialog")
+ b.select_from_dropdown("#auto-update-day", "thu")
+ self.closeSettings(b)
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every Thursday at 06:00")
+ assertType("all")
+ assertTimer("06", "Thu")
+
+ # change time
+ b.click("#autoupdates-settings button:contains('Edit')")
+ b.wait_visible("#automatic-updates-dialog")
+ b.set_input_text("#auto-update-time-input", "21:00")
+ self.closeSettings(b)
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every Thursday at 21:00")
+ assertType("all")
+ assertTimer("21", "Thu")
+
+ # page should parse it correctly from the timer
+ b.logout()
+ b.login_and_go("/updates")
+ with b.wait_timeout(30):
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every Thursday at 21:00")
+
+ # change back to daily
+ b.click("#autoupdates-settings button:contains('Edit')")
+ b.wait_visible("#automatic-updates-dialog")
+ b.select_from_dropdown("#auto-update-day", "everyday")
+ self.closeSettings(b)
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 21:00")
+ assertType("all")
+ assertTimer("21")
+
+ # disable
+ b.click("#autoupdates-settings button:contains('Edit')")
+ b.wait_visible("#automatic-updates-dialog")
+ b.click("#no-updates")
+ self.closeSettings(b)
+ b.wait_in_text("#autoupdates-settings", "Disabled")
+ assertTimer(None)
+
+ if self.backend == "dnf":
+ b.click("#autoupdates-settings button:contains('Edit')")
+ b.wait_visible("#automatic-updates-dialog")
+ b.click("#all-updates")
+ self.closeSettings(b)
+ # OnCalendar= parsing: only time
+ m.execute("mkdir -p /etc/systemd/system/dnf-automatic.timer.d")
+ m.execute(r'printf "[Timer]\nOnUnitInactiveSec=\nOnCalendar=08:00\n" > '
+ r'/etc/systemd/system/dnf-automatic-install.timer.d/time.conf; systemctl daemon-reload')
+ b.reload()
+ b.enter_page("/updates")
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 8:00")
+ b.wait_visible("#autoupdates-settings button")
+
+ # OnCalendar= parsing: weekday and time
+ m.execute(r'printf "[Timer]\nOnUnitInactiveSec=\nOnCalendar=Tue 20:00\n" > '
+ r'/etc/systemd/system/dnf-automatic-install.timer.d/time.conf; systemctl daemon-reload')
+ b.reload()
+ b.enter_page("/updates")
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every Tuesday at 20:00")
+ b.wait_visible("#autoupdates-settings button")
+
+ # OnCalendar= parsing: "every day" calendar and time
+ m.execute(r'printf "[Timer]\nOnUnitInactiveSec=\nOnCalendar=*-*-* 07:00\n" > '
+ r'/etc/systemd/system/dnf-automatic-install.timer.d/time.conf; systemctl daemon-reload')
+ b.reload()
+ b.enter_page("/updates")
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 7:00")
+ b.wait_visible("#autoupdates-settings button")
+
+ # OnCalendar= parsing: unsupported
+ m.execute(r'printf "[Timer]\nOnUnitInactiveSec=\nOnCalendar=*-02-* 11:00\n" > '
+ r'/etc/systemd/system/dnf-automatic-install.timer.d/time.conf; systemctl daemon-reload')
+ b.reload()
+ b.enter_page("/updates")
+ time.sleep(5)
+ b.wait_visible("#settings .pf-v5-c-alert")
+ # don't allow stomping over unparsable custom settings
+ b.wait_not_present("#autoupdates-settings button")
+
+ def testWithAvailableUpdates(self):
+ b = self.browser
+ m = self.machine
+
+ # use a package which dnf recognizes as "needs reboot"
+ self.createPackage("kernel-rt", "1.0", "1", install=True)
+ self.createPackage("kernel-rt", "1.0", "2")
+ self.enableRepo()
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+ with b.wait_timeout(30):
+ b.wait_visible("#available-updates")
+
+ if not self.supported_backend:
+ return
+
+ b.wait_in_text("#autoupdates-settings", "Disabled")
+
+ # enable
+ b.click("#autoupdates-settings button:contains('Edit')")
+ b.wait_visible("#automatic-updates-dialog")
+ b.click("#all-updates")
+ self.closeSettings(b)
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 06:00")
+
+ if self.backend == 'dnf':
+ # dial down the random sleep to avoid the test having to wait 5 mins
+ self.sed_file("/random_sleep/ s/=.*$/= 3/", "/etc/dnf/automatic.conf")
+ # then manually start the upgrade job like the timer would
+ m.execute("systemctl start dnf-automatic-install.service")
+ try:
+ # new kernel-rt package got installed
+ m.execute("test -f /stamp-kernel-rt-1.0-2")
+ # triggered reboot
+ m.execute("until test -f /run/nologin; do sleep 1; done")
+ # old distros don't have that yet; rely on /run/nologin to indicate scheduled shutdown
+ if not m.image.startswith("rhel-8") and not m.image.startswith("centos-8"):
+ self.assertIn("scheduled for", m.execute("shutdown --show 2>&1"))
+ finally:
+ # always cancel the scheduled reboot
+ m.execute("shutdown -c; test ! -f /run/nologin")
+ # service should show kernel-rt upgrade
+ out = m.execute(
+ "if systemctl status --lines=50 dnf-automatic-install.service; then echo 'expected service to be stopped'; exit 1; fi")
+ self.assertIn("kernel-rt", out)
+
+ # run it again, now there are no available updates → no reboot
+ m.execute("systemctl start dnf-automatic-install.service")
+ m.execute("test -f /stamp-kernel-rt-1.0-2; test ! -f /run/nologin")
+ # service should not do much
+ out = m.execute(
+ "if systemctl status dnf-automatic-install.service; then echo 'expected service to be stopped'; exit 1; fi")
+ self.assertNotIn("kernel-rt", out)
+ else:
+ raise NotImplementedError(self.backend)
+
+ @testlib.skipDistroPackage()
+ def testPrivilegeChange(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("pkcon refresh")
+
+ self.login_and_go("/updates", superuser=False)
+
+ if not self.supported_backend:
+ return
+
+ # detecting auto updates configuration works unprivileged, but changing does not
+ with b.wait_timeout(30):
+ b.wait_visible("#autoupdates-settings button:disabled")
+
+ # become superuser, enable auto-updates
+ b.become_superuser()
+ b.wait_in_text("#autoupdates-settings", "Disabled")
+ b.click("#autoupdates-settings button:contains('Edit')")
+ b.wait_visible("#automatic-updates-dialog")
+ b.click("#all-updates")
+ self.closeSettings(b)
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 06:00")
+
+ # without superuser, auto-update status still visible, but disabled
+ b.drop_superuser()
+ b.wait_in_text("#autoupdates-settings", "Updates will be applied every day at 06:00")
+ b.wait_visible("#autoupdates-settings button:disabled")
+
+
+@testlib.skipImage("Image uses OSTree", "fedora-coreos", "rhel4edge")
+class TestAutoUpdatesInstall(NoSubManCase):
+ def testUnsupported(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("if type dnf; then dnf remove -y dnf-automatic; elif type apt; then dpkg -P unattended-upgrades; fi")
+
+ # first test with available upgrades
+ self.createPackage("vanilla", "1.0", "1", install=True)
+ self.createPackage("vanilla", "1.0", "2")
+ self.enableRepo()
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+ with b.wait_timeout(30):
+ b.wait_visible("#available-updates")
+
+ # apply updates
+ b.click("#available-updates button#install-all")
+ # wait until installation is finished
+ with b.wait_timeout(60):
+ b.wait_visible(".pf-v5-c-empty-state__title:contains('Update was successful')")
+ b.click("#ignore")
+
+ if self.backend == 'dnf':
+ b.wait_in_text("#autoupdates-settings", "Not set up")
+ b.wait_not_present("#settings .pf-v5-c-alert")
+ b.click("#autoupdates-settings button:contains('Enable')")
+ else:
+ b.wait_not_present("#settings")
+ b.wait_not_present("#autoupdates-settings")
+
+ @testlib.skipImage("No supported auto update backend", "debian-*", "ubuntu-*", "arch")
+ def testInstall(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute('dnf remove -y dnf-automatic')
+
+ # provide minimal content in order for the backend to be seen as supported
+ timerContent = """
+[Unit]
+Description=dnf-automatic timer
+# See comment in dnf-makecache.service
+ConditionPathExists=!/run/ostree-booted
+
+[Timer]
+OnBootSec=1h
+OnUnitInactiveSec=1d
+Unit=-.mount
+
+[Install]
+WantedBy=basic.target
+ """
+ self.createPackage('dnf-automatic', '1', '1', content={
+ '/etc/dnf/automatic.conf': '',
+ '/usr/lib/systemd/system/dnf-automatic.timer': timerContent,
+ '/usr/lib/systemd/system/dnf-automatic-install.timer': timerContent
+ })
+ self.enableRepo()
+
+ m.start_cockpit()
+ b.login_and_go('/updates')
+
+ # click through install dialog
+ with b.wait_timeout(30):
+ b.wait_in_text("#autoupdates-settings", "Not set up")
+ b.wait_not_present("#settings .pf-v5-c-alert")
+ b.click("#autoupdates-settings button:contains('Enable')")
+ b.wait_popup('dialog')
+ b.wait_visible('#dialog button.apply')
+ b.wait_not_attr('#dialog button.apply', 'disabled', '')
+ b.click('#dialog button.apply')
+
+ # as dnf-automatic isn't actually installed DnfImpl.setConfig will fail,
+ # but we can check that the backend is now enabled
+ b.wait_visible("#automatic-updates-dialog")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-packages b/test/verify/check-packages
new file mode 100755
index 0000000..4e7933c
--- /dev/null
+++ b/test/verify/check-packages
@@ -0,0 +1,113 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+test_manifest = """
+{
+ "tools": {
+ "test": {
+ "label": "Test"
+ }
+ },
+
+ "content-security-policy": "script-src 'self' 'unsafe-inline'"
+}
+"""
+
+test_html = """
+<html>
+ <head>
+ <meta charset="utf-8">
+ <script src="../base1/cockpit.js"></script>
+ <script>
+ cockpit.transport.wait(function () { });
+ </script>
+ </head>
+ <body>
+ <h1>Test</h1>
+ </body>
+</html>
+"""
+
+
+@testlib.nondestructive
+class TestPackages(testlib.MachineCase):
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ self.restore_dir("/usr/local/share/cockpit")
+
+ def reload_packages():
+ b.go("/playground/pkgs")
+ b.enter_page("/playground/pkgs")
+ b.click("#reload")
+
+ def check_nav_entry(label, present):
+ b.switch_to_top()
+ if present:
+ b.wait_visible(f"#nav-system li:contains({label})")
+ else:
+ b.wait_not_present(f"#nav-system li:contains({label})")
+
+ self.login_and_go("/playground/pkgs")
+
+ check_nav_entry("Terminal", present=True)
+
+ m.write("/usr/local/share/cockpit/test/manifest.json", test_manifest)
+ m.write("/usr/local/share/cockpit/test/test.html", test_html)
+ reload_packages()
+
+ b.switch_to_top()
+ b.click("#nav-system a:contains(Test)")
+
+ b.enter_page("/test/test")
+ b.wait_text("h1", "Test")
+
+ m.execute("rm -rf /usr/local/share/cockpit/test")
+ reload_packages()
+ check_nav_entry("Test", present=False)
+
+ # Hide the terminal with a system override
+ m.write("/etc/cockpit/systemd.override.json", """{
+ "tools": {
+ "terminal": null
+ }
+}""")
+ reload_packages()
+ check_nav_entry("Terminal", present=False)
+
+ # user override on top which renames the services label
+ self.restore_dir("/home/admin")
+ m.write("/home/admin/.config/cockpit/systemd.override.json", """{
+ "menu": {
+ "services": { "label": "Hackices" }
+ }
+}""")
+ reload_packages()
+ check_nav_entry("Services", present=False)
+ check_nav_entry("Hackices", present=True)
+ # it adds to, not replaces the /etc override
+ check_nav_entry("Terminal", present=False)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-pages b/test/verify/check-pages
new file mode 100755
index 0000000..cdfa609
--- /dev/null
+++ b/test/verify/check-pages
@@ -0,0 +1,868 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import time
+
+import testlib
+
+RHEL_DOC_BASE = "https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_systems_using_the_rhel_8_web_console"
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestPages(testlib.MachineCase):
+ def checkDocs(self, items):
+ m = self.machine
+ b = self.browser
+
+ b.click("#toggle-docs")
+ b.wait_visible("#toggle-docs + ul")
+ expected = "Web Console"
+ expected += "".join(items)
+ expected += "About Web Console"
+ # DOCUMENTATION_URL is only in Fedora, RHEL and Arch
+ if "fedora" in m.image:
+ expected = "Fedora Linux documentation" + expected
+ elif m.image.startswith("rhel"):
+ expected = "Red Hat Enterprise Linux documentation" + expected
+ elif m.image == "arch":
+ expected = "Arch Linux documentation" + expected
+
+ b.wait_collected_text("#toggle-docs + ul", expected)
+ b.click("#toggle-docs")
+ b.wait_not_present("#toggle-docs + ul")
+
+ def check_system_menu(self, label, present):
+ b = self.browser
+ if present:
+ b.wait_visible(f"#host-apps li a:contains('{label}')")
+ else:
+ b.wait_not_present(f"#host-apps li a:contains('{label}')")
+
+ def open_lang_modal(self):
+ self.browser.switch_to_top()
+ self.browser.open_session_menu()
+
+ self.browser.click(".display-language-menu")
+ self.browser.wait_visible('#display-language-modal')
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ # HACK: somehow the "services" preload causes a race condition and injects some spurious `window.hashchange /`
+ # event when switching between pages, losing the "on details page" state
+ # see debugging history in https://github.com/cockpit-project/cockpit/pull/18766
+ self.disable_preload("systemd")
+
+ self.restore_dir("/etc/systemd/system", post_restore_action="systemctl daemon-reload")
+ self.addCleanup(m.execute, "systemctl stop test.timer test.service")
+ m.write("/etc/systemd/system/test.service",
+ """
+[Unit]
+Description=Test Service
+
+[Service]
+ExecStart=/bin/true
+
+[Install]
+WantedBy=default.target
+""")
+ m.write("/etc/systemd/system/test.timer",
+ """
+[Unit]
+Description=Test timer
+
+[Timer]
+OnCalendar=daily
+""")
+ # After writing files out tell systemd about them
+ m.execute("systemctl daemon-reload")
+
+ m.execute("systemctl start test.timer")
+
+ self.allow_journal_messages("Failed to get realtime timestamp: Cannot assign requested address")
+
+ # On Debian and Ubuntu we have to generate the other locales
+ if "debian" in m.image:
+ m.write("/etc/locale.gen", "de_DE.UTF-8 UTF-8\n", append=True)
+ m.execute("locale-gen; update-locale")
+ elif "arch" == m.image:
+ m.write("/etc/locale.gen", "de_DE.UTF-8 UTF-8\n", append=True)
+ m.execute("locale-gen")
+ elif "ubuntu" in m.image:
+ m.execute("locale-gen de_DE; locale-gen de_DE.UTF-8; update-locale")
+
+ # login so that we have a cookie.
+ self.login_and_go("/system/services#/test.service")
+
+ # check that reloading a page with parameters works
+ b.enter_page("/system/services")
+ b.reload()
+ b.enter_page("/system/services")
+ b.wait_text(".service-name", "Test Service")
+ b.switch_to_top()
+ self.checkDocs(["Managing services"])
+ b.click("#toggle-docs")
+ b.wait_visible(f'#toggle-docs + ul a:contains("Managing services")[href="{RHEL_DOC_BASE}/'
+ 'managing-services-in-the-web-console_system-management-using-the-rhel-8-web-console"]')
+ b.wait_visible(f'#toggle-docs + ul a:contains("Web Console")[href="{RHEL_DOC_BASE}/index"]')
+ b.click("#toggle-docs")
+ b.wait_not_present("#toggle-docs + ul")
+ b.go("/network")
+ self.checkDocs(["Managing networking bonds", "Managing networking teams",
+ "Managing networking bridges", "Managing VLANs", "Managing firewall"])
+ b.go("/system/services")
+
+ m.restart_cockpit()
+ b.relogin("/system/services")
+ b.wait_text(".service-name", "Test Service")
+
+ # check that navigating away and back preserves place
+ b.click_system_menu("/system")
+ b.wait_visible("#system_information_systime_button")
+ b.switch_to_top()
+ self.checkDocs(["Configuring system settings"])
+ b.click_system_menu("/system/services")
+ b.wait_visible("ol.pf-v5-c-breadcrumb__list")
+ b.wait_text(".service-name", "Test Service")
+ b.switch_to_top()
+ b.wait_js_cond('window.location.pathname === "/system/services"')
+ b.wait_js_cond('window.location.hash === "#/test.service"')
+
+ # check that when inside the component clicking the navbar
+ # takes you home
+ b.click_system_menu("/system/services")
+ b.wait_visible("#services-list")
+ b.wait_not_present("#service-details")
+ b.switch_to_top()
+ b.wait_js_cond('window.location.pathname === "/system/services"')
+ b.wait_js_cond('window.location.hash === ""')
+
+ # Navigate inside an iframe
+ b.switch_to_top()
+ b.go("/@localhost/playground/test")
+ b.enter_page("/playground/test")
+ b.click("button:contains('Go down')")
+ b.click("button:contains('Go down')")
+ b.switch_to_top()
+ b.wait_js_cond("window.location.hash == '#/0/1?length=1'")
+
+ # This should be visible now
+ b.switch_to_frame("cockpit1:localhost/playground/test")
+ b.wait_visible("#hidden")
+
+ # This should now say invisible
+ b.switch_to_top()
+ b.go("/@localhost/system/services")
+ b.switch_to_frame("cockpit1:localhost/playground/test")
+ b.wait_not_visible("#hidden")
+
+ # Test 'parent' manifest option
+ b.switch_to_top()
+ b.go("/metrics")
+ self.check_system_menu("Overview", present=True)
+ self.checkDocs(["Performance Co-Pilot"])
+
+ # Lets try changing the language
+
+ self.open_lang_modal()
+ b.click('#display-language-modal li[data-value=de-de] button')
+ b.click("#display-language-modal footer button.pf-m-primary")
+ b.wait_language("de-de")
+
+ # Check that the system page is translated
+ b.go("/system")
+ b.enter_page("/system")
+ b.click(".ct-overview-header button:contains('Neustart')")
+
+ # Restart dialog is loaded from pkg/lib and it also needs to be translated
+ b.wait_in_text("#shutdown-dialog", "Nachricht an angemeldete Benutzer")
+
+ # Systemd timer localization
+ b.go("/system/services")
+ b.switch_to_top()
+ b.wait_js_cond('document.title.indexOf("Dienste") === 0')
+ b.enter_page("/system/services")
+ b.click('#services-filter li:nth-child(4) a')
+ # HACK: the timers' next run/last trigger (col 3/4) don't always get filled (issue #9439)
+ # b.wait_in_text("tr[data-goto-unit='test\.timer'] td:nth-child(3)", "morgen um")
+
+ # BIOS date parsing; we don't want to introduce too many assumptions, just that the original MM/DD/YYYY
+ # was parsed at all, and the bios is from the 21st century (20YY)
+ # TestSystemInfo.testHardwareInfo does this more carefully
+ b.go("/system/hwinfo")
+ b.enter_page("/system/hwinfo")
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v5-c-description-list__group:nth-of-type(3) dd', " 20")
+
+ # Check the playground page
+ b.switch_to_top()
+ b.go("/playground/translate")
+ b.wait_js_cond('document.title.indexOf("Entwicklung") === 0')
+ b.enter_page("/playground/translate")
+
+ # HTML section
+ self.assertEqual(b.text("#translate-html"), "Bereit")
+ self.assertEqual(b.text("#translate-html-context"), "Bereiten")
+ self.assertEqual(b.text("#translate-html-yes"), "Nicht bereit")
+ self.assertEqual(b.attr("#translate-html-title", "title"), u"Nicht verfügbar")
+ self.assertEqual(b.text("#translate-html-title"), "Cancel")
+ self.assertEqual(b.attr("#translate-html-yes-title", "title"), u"Nicht verfügbar")
+ self.assertEqual(b.text("#translate-html-yes-title"), "Abbrechen")
+
+ # Glade section
+ self.assertEqual(b.text("#translatable-glade"), "Leer")
+ self.assertEqual(b.text("#translatable-glade-context"), "Leeren")
+
+ # Javascript
+ self.assertEqual(b.text("#underscore-empty"), "Leer")
+ self.assertEqual(b.text("#underscore-context-empty"), "Leeren")
+ self.assertEqual(b.text("#cunderscore-context-empty"), "Leeren")
+ self.assertEqual(b.text("#gettext-control"), "Steuerung")
+ self.assertEqual(b.text("#gettext-context-control"), "Strg")
+ self.assertEqual(b.text("#ngettext-disks-1"), "$0 Festplatte fehlt")
+ self.assertEqual(b.text("#ngettext-disks-2"), "$0 Festplatten fehlen")
+ self.assertEqual(b.text("#ngettext-context-disks-1"), u"$0 Datenträger fehlt")
+ self.assertEqual(b.text("#ngettext-context-disks-2"), u"$0 Datenträger fehlen")
+
+ # Frame title
+ b.switch_to_top()
+ b.wait_attr("iframe[name='cockpit1:localhost/system']", "title", "Überblick")
+
+ # Log out and check that login page is translated now
+ b.logout()
+ b.wait_visible('#password-group')
+ b.wait_text("#password-group > label", "Passwort")
+
+ # Test all languages
+ # Test that pages do not oops
+
+ if not m.image.startswith("rhel-"):
+ return
+
+ def line_sel(i):
+ return '.terminal .xterm-accessibility-tree div:nth-child(%d)' % i
+
+ pages = ["/system", "/system/logs", "/network", "/users", "/system/services", "/system/terminal"]
+
+ self.login_and_go('/system')
+ b.wait_visible('#overview')
+
+ self.open_lang_modal()
+ languages = b.eval_js("ph_select('#display-language-list li').map(e => e.attributes['data-value'].nodeValue)")
+ self.assertIn('en-us', languages)
+ b.click("#display-language-modal footer button.pf-m-link") # Close the menu
+
+ is_pybridge = self.is_pybridge()
+
+ for language in languages:
+ # Remove failed units which will show up in the first terminal line
+ m.execute("systemctl reset-failed")
+
+ b.go("/system")
+ b.enter_page("/system")
+
+ self.open_lang_modal()
+ b.click(f"#display-language-modal li[data-value={language}] button")
+ b.click("#display-language-modal footer button.pf-m-primary")
+ b.wait_language(language)
+
+ # Test some pages
+ for page in pages:
+ b.go(page)
+ b.enter_page(page)
+ b.wait_language(language)
+
+ if not is_pybridge:
+ locale = language.split("-")
+ if len(locale) == 1:
+ locale.append("")
+ locale = f"{locale[0]}_{locale[1].upper()}.UTF-8"
+
+ b.wait_visible(".terminal .xterm-accessibility-tree")
+ b.wait_in_text(line_sel(1), "admin")
+ b.key_press("echo $LANG\r")
+ b.wait_in_text(line_sel(2), locale)
+
+ b.switch_to_top()
+
+ b.wait_js_func("""(function (lang) {
+ let correct = true;
+ const rtl_langs = ["ar-eg", "fa-ir", "he-il", "ur-in"];
+ const dir = rtl_langs.includes(lang) ? "rtl" : "ltr";
+ document.querySelectorAll('#content iframe').forEach(el => {
+ if (el.contentDocument.documentElement.lang !== lang)
+ correct = false;
+ if (el.contentDocument.documentElement.dir !== dir)
+ correct = false;
+ });
+ return correct;
+ })""", language)
+ b.wait_attr(".index-page", "lang", language)
+
+ # the quick iteration starts/stops tracer in quick succession, before it can finish
+ self.allow_browser_errors("Tracer failed:.*internal-error")
+
+ def testPtBRLocale(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute('useradd scruffy -s /bin/bash -c Scruffy')
+ m.execute('echo scruffy:foobar | chpasswd')
+
+ if "debian" in m.image:
+ m.execute('echo \'pt_BR.UTF-8 UTF-8\' >> /etc/locale.gen; locale-gen; update-locale')
+ elif "ubuntu" in m.image:
+ m.execute('locale-gen pt_BR; locale-gen pt_BR.UTF-8; update-locale')
+ elif "arch" == m.image:
+ m.execute('echo \'pt_BR.UTF-8 UTF-8\' >> /etc/locale.gen; locale-gen')
+
+ self.login_and_go('/system')
+ b.wait_visible('#overview')
+ self.open_lang_modal()
+ b.click('#display-language-modal li[data-value=pt-br] button')
+ b.click('#display-language-modal footer button.pf-m-primary')
+ b.wait_language("pt-br")
+
+ # Check that the system page is translated
+ b.go('/system')
+ b.enter_page('/system')
+ b.wait_language("pt-br")
+ b.wait_in_text('.ct-overview-header', 'Reiniciar')
+
+ # Systemd timer localization
+ b.go('/system/services')
+ b.enter_page('/system/services')
+ b.wait_language("pt-br")
+ b.click('#services-filter li:nth-child(4) a')
+ # HACK: the timers' next run/last trigger (col 3/4) don't always get filled (issue #9439)
+ # b.wait_in_text('tr[data-goto-unit=\'test\.timer\'] td:nth-child(3)', 'morgen um')
+
+ # Check the playground page
+ b.switch_to_top()
+ b.go('/playground/translate')
+ b.enter_page('/playground/translate')
+ b.wait_language("pt-br")
+
+ # HTML section
+ self.assertEqual(b.text('#translate-html'), 'Pronto')
+ self.assertEqual(b.text('#translate-html-context'), 'Pronto')
+ self.assertEqual(b.text('#translate-html-yes'), u'Não está pronto')
+ self.assertEqual(b.attr('#translate-html-title', 'title'), u'Indisponível')
+ self.assertEqual(b.text('#translate-html-title'), 'Cancel')
+ self.assertEqual(b.attr('#translate-html-yes-title', 'title'), u'Indisponível')
+ self.assertEqual(b.text('#translate-html-yes-title'), 'Cancelar')
+
+ # Glade section
+ self.assertEqual(b.text('#translatable-glade'), 'Vazio')
+ self.assertEqual(b.text('#translatable-glade-context'), 'Vazio')
+
+ # Javascript
+ self.assertEqual(b.text('#underscore-empty'), 'Vazio')
+ self.assertEqual(b.text('#underscore-context-empty'), 'Vazio')
+ self.assertEqual(b.text('#cunderscore-context-empty'), 'Vazio')
+ self.assertEqual(b.text('#gettext-control'), 'Controle')
+ self.assertEqual(b.text('#gettext-context-control'), 'Controle')
+ self.assertEqual(b.text('#ngettext-disks-1'), u'$0 disco não encontrado')
+ self.assertEqual(b.text('#ngettext-disks-2'), u'$0 discos não encontrados')
+ self.assertEqual(b.text('#ngettext-context-disks-1'), u'$0 disco não encontrado')
+ self.assertEqual(b.text('#ngettext-context-disks-2'), u'$0 discos não encontrados')
+
+ # Log out and check that login page is translated now
+ b.logout()
+ b.wait_text('#password-group > label', 'Senha')
+
+ # translated variants of standard messages in testlib.py
+ self.allow_journal_messages("xargs: basename: .*13.*")
+
+ def testFrameReload(self):
+ b = self.browser
+ frame = "cockpit1:localhost/playground/test"
+ self.addCleanup(self.machine.execute, "rm -f /tmp/counter")
+
+ self.login_and_go("/playground/test")
+
+ b.wait_text('#file-content', "0")
+ b.click("#modify-file")
+ b.wait_text('#file-content', "1")
+
+ b.switch_to_top()
+ b.eval_js('ph_set_attr("iframe[name=\'%s\']", "data-ready", null)' % frame)
+ b.eval_js('ph_set_attr("iframe[name=\'%s\']", "src", "../playground/test.html?i=1#/")' % frame)
+ b.wait_visible(f"iframe.container-frame[name='{frame}'][data-ready]")
+
+ b.enter_page("/playground/test")
+
+ b.wait_text('#file-content', "1")
+
+ self.allow_restart_journal_messages()
+
+ def testShellReload(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go()
+
+ self.check_system_menu("Overview", present=True)
+ self.restore_dir("/home/admin")
+ m.write("/home/admin/.local/share/cockpit/foo/manifest.json",
+ '{ "menu": { "index": { "label": "FOO!" } } }')
+ b.reload()
+ self.check_system_menu("FOO!", present=True)
+
+ def testMenuSearch(self):
+ b = self.browser
+ m = self.machine
+
+ # On Ubuntu and Debian we would need to generate locales - just ignore it
+ self.allow_journal_messages("invalid or unusable locale: de_DE.UTF-8")
+
+ # Clean up failed services for screenshots
+ m.execute("systemctl reset-failed")
+
+ self.login_and_go()
+
+ filter_sel = ".pf-v5-c-text-input-group__text-input"
+
+ # Check that some page disappears and some stay
+ b.focus(filter_sel)
+ b.key_press("se")
+ b.wait_not_present("#host-apps li a:contains('Logs')")
+ b.wait_visible("#host-apps li a:contains('Services')")
+ b.wait_text("#host-apps li a:contains('Services') mark", "Se")
+
+ b.focus(filter_sel)
+ b.key_press("\b\b")
+ b.wait_visible("#host-apps li a:contains('Logs')")
+ b.wait_visible("#host-apps li a:contains('Services')")
+
+ # Check that any substring work
+ b.focus(filter_sel)
+ b.key_press("CoUN")
+ b.wait_not_present("#host-apps li a:contains('Overview')")
+ b.wait_visible("#host-apps li a:contains('Accounts')")
+ b.wait_text("#host-apps li a:contains('Accounts') mark", "coun")
+
+ # Check it can also search by keywords
+ b.focus(filter_sel)
+ b.key_press("\b\b\b\bsystemd")
+ b.wait_visible("#host-apps li a:contains('Services')")
+ b.wait_text("#host-apps li a:contains('Services')", "ServicesContains: systemd")
+ b.wait_text("#host-apps li a:contains('Services') mark", "systemd")
+
+ b.wait_not_present("#services-error")
+ b.assert_pixels("#nav-system", "menu-search", skip_layouts=["mobile"])
+ b.set_layout("mobile")
+ b.click("#nav-system-item")
+ b.assert_pixels_in_current_layout("#nav-system", "menu-search")
+ b.click("#nav-system-item")
+ b.set_layout("desktop")
+
+ # Check that enter activates first result
+ b.focus(filter_sel)
+ b.key_press("\b\b\b\b\b\b\blogs")
+ b.wait_not_present("#host-apps li a:contains('Services')")
+ b.wait_visible("#host-apps li a:contains('Logs')")
+ b.focus(filter_sel)
+ b.key_press("\r")
+ b.enter_page("/system/logs")
+ b.wait_visible("#journal")
+
+ # Visited page, search should be cleaned up
+ b.switch_to_top()
+ b.wait_val(filter_sel, "")
+
+ # Check that escape cleans the search
+ b.key_press("logs")
+ b.wait_not_present("#host-apps li a:contains('Services')")
+ b.wait_visible("#host-apps li a:contains('Logs')")
+ b.focus(filter_sel)
+ b.key_press(chr(27)) # escape
+ b.wait_val(filter_sel, "")
+ b.wait_visible("#host-apps li a:contains('Services')")
+
+ # Check that clicking on `Clear search` cleans the search
+ b.key_press("logs")
+ b.wait_not_present("#host-apps li a:contains('Services')")
+ b.wait_visible("#host-apps li a:contains('Logs')")
+ b.click("button:contains('Clear search')")
+ b.key_press("\b\b\b\b")
+ b.wait_visible("#host-apps li a:contains('Services')")
+ b.wait_not_present("button:contains('Clear search')")
+
+ # Check that arrows navigate the menu
+ b.focus(filter_sel)
+ b.key_press("s")
+ b.wait_not_present("#host-apps li a:contains('Logs')")
+ b.key_press(chr(40), use_ord=True) # arrow down
+ b.key_press(chr(40), use_ord=True) # arrow down
+ b.key_press("\r")
+ if m.ostree_image:
+ b.enter_page("/users")
+ else:
+ b.enter_page("/storage")
+
+ # Check we jump into subpage when defined in manifest
+ b.switch_to_top()
+ b.focus(filter_sel)
+ b.key_press("firew")
+ b.wait_visible("#host-apps li a:contains('Networking')")
+ b.wait_not_present("#host-apps li a:contains('Overview')")
+ b.click("#host-apps li a:contains('Networking')")
+ b.enter_page("/network/firewall")
+
+ # Search internationalized menu
+ self.open_lang_modal()
+
+ # Filter the available languages
+ b.set_input_text('#display-language-modal input[type=search]', "Deutsch")
+ b.click('#display-language-modal li[data-value=de-de] button')
+ b.wait_js_func("ph_count_check", "#display-language-modal li", 1)
+ b.set_input_text('#display-language-modal input[type=search]', "")
+
+ b.click('#display-language-modal li[data-value=de-de] button')
+ b.click("#display-language-modal footer button.pf-m-primary")
+ b.wait_language("de-de")
+ b.go("/system")
+ b.enter_page("/system")
+ b.wait_in_text(".ct-overview-header", "Neustart")
+
+ b.switch_to_top()
+ b.wait_visible("#host-apps li a:contains('Dienste')")
+ b.wait_visible("#host-apps li a:contains('Protokolle')")
+ b.focus(filter_sel)
+ b.key_press("dien")
+ b.wait_not_present("#host-apps li a:contains('Protokolle')")
+ b.wait_visible("#host-apps li a:contains('Dienste')")
+ b.wait_text("#host-apps li a:contains('Dienste') mark", "Dien")
+
+ def testShellPreload(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go()
+
+ # Check what's going on while playground/preloaded is still invisible
+ b.switch_to_top()
+ b.wait_attr('iframe[name="cockpit1:localhost/playground/preloaded"]', 'data-loaded', 1)
+ b.switch_to_frame("cockpit1:localhost/playground/preloaded")
+ b.wait_js_func('ph_text_is', "#host", m.execute("hostname").replace("\n", ""))
+ time.sleep(3)
+ b.wait_js_func('ph_text_is', "#release", "")
+
+ # Now navigate to it.
+ b.switch_to_top()
+ b.go("/playground/preloaded")
+ b.enter_page("/playground/preloaded")
+ b.wait_text("#release", m.execute("cat /etc/os-release").replace("\n", ""))
+
+ def testReactPatterns(self):
+ b = self.browser
+ m = self.machine
+
+ self.restore_dir('/home/admin')
+
+ stuff = os.path.join(self.vm_tmpdir, "stuff")
+ # prepare a directory for testing file autocomplete widget
+ m.execute(f"mkdir -p {stuff}/dir")
+ m.execute(f"mkdir -p {stuff}/dir1")
+ m.write(f"{stuff}/file1.txt", "")
+
+ self.login_and_go("/playground/react-patterns")
+
+ # test file completion widget
+ b.focus("#demo-file-ac input[type=text]")
+ b.key_press(stuff + "/")
+ # need to wait for the widget's "fast typing" inhibition delay to trigger the completion popup
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", stuff + "/")
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(2) button", "dir/")
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(3) button", "dir1/")
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(4) button", "file1.txt")
+ b.click("#file-autocomplete-widget li:nth-of-type(2) button")
+
+ # clear the file completion widget
+ b.click("#demo-file-ac div:first-of-type div:first-of-type button:nth-of-type(1)")
+ # test if input matches one entry, but is the prefix of other entry, widget should not descend into directory
+ b.focus("#demo-file-ac input[type=text]")
+ b.key_press(stuff + "/dir")
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", stuff + "/dir")
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(2) button", stuff + "/dir1")
+
+ # clear the file completion widget
+ b.click("#demo-file-ac div:first-of-type div:first-of-type button:nth-of-type(1)")
+ b.wait_not_present("#file-autocomplete-widget li")
+ b.focus("#demo-file-ac input[type=text]")
+ b.key_press(stuff + "/")
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", stuff + "/")
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(4) button", "file1.txt")
+ b.click("#file-autocomplete-widget li:nth-of-type(4) button")
+ b.wait_not_present("#file-autocomplete-widget li")
+
+ # now update file1, check robustness with dynamic events
+ m.execute(f"touch {stuff}/file1.txt")
+ b.focus("#demo-file-ac input[type=text]")
+ time.sleep(1)
+ b.key_press(["\b"] * 5)
+ # input is now $stuff/file
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(1) button", "file1.txt")
+ b.key_press(["\b"] * 4)
+ # input is now $stuff/, so all listings should be available
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(4) button", "file1.txt")
+
+ # add new file
+ m.execute(f"touch {stuff}/other")
+ b.focus("#demo-file-ac input[type=text]")
+ # We need to tickle the widget to re-read the directory by changing to
+ # the previous directory and back to the directory we want to list.
+ # This is an implementation choice, to avoid re-reading the directories
+ # content with every user input change, which is definitely a performance cost
+ b.key_press(["\b"] * 6)
+ time.sleep(1)
+ b.key_press("stuff/")
+ b.wait_in_text("#file-autocomplete-widget li:nth-of-type(5) button", "other")
+
+ # Create test folder with known files
+ m.execute("mkdir /home/admin/newdir")
+ m.execute("mkdir /home/admin/newdir/dir1")
+ m.execute("mkdir /home/admin/newdir/dir2")
+ m.execute("touch /home/admin/newdir/file1")
+ m.execute("touch /home/admin/newdir/file2")
+ # test pre-selected autocomplete widget
+ b.wait_val("div#demo-file-ac-preselected input", "/home/admin/newdir/file1")
+ b.click("div#demo-file-ac-preselected input")
+ b.click("div#demo-file-ac input")
+ b.click("div#demo-file-ac-preselected input")
+ paths = ["/home/admin/newdir", "/home/admin/newdir/dir1", "/home/admin/newdir/dir2", "/home/admin/newdir/file1", "/home/admin/newdir/file2"]
+ for i in range(5):
+ b.wait_in_text(f"#file-autocomplete-widget-preselected li:nth-of-type({i + 1}) button", paths[i])
+
+ @testlib.skipOstree("No PCP available")
+ def testPlots(self):
+ b = self.browser
+ m = self.machine
+
+ self.addCleanup(m.execute, "systemctl stop pmcd")
+ m.execute("systemctl start pmcd")
+
+ self.login_and_go("/playground/plot")
+ b.wait_visible("#plot-direct")
+ b.wait_visible("#plot-pmcd")
+
+ def read_mem_info(machine):
+ info = {}
+ for line in machine.execute("cat /proc/meminfo").splitlines():
+ (name, value) = line.strip().split(":")
+ if value.endswith("kB"):
+ info[name] = int(value[:-2]) * 1024
+ else:
+ info[name] = int(value)
+ return info
+
+ # When checking whether the plots show the expected results,
+ # we look for a segment of the data of a certain duration
+ # whose average is in a certain range. Otherwise any short
+ # outlier will make us miss the expected plateau. Such
+ # outliers happen frequently with the CPU plot. We also
+ # insist that the first and last value of the segment are in
+ # range, otherwise we would find any arbitrary average in a
+ # graph with a slope.
+
+ b.inject_js("""
+ ph_plateau = function (data, min, max, duration, label) {
+ var i, j;
+ var sum; // sum of data[i..j]
+
+ function ok(val) {
+ return val >= min && val <= max;
+ }
+
+ sum = 0;
+ i = 0;
+ for (j = 0; j < data.length; j++) {
+ sum += data[j][1];
+ while (i < j && (data[j][0] - data[i][0]) > duration * 1000) {
+ avg = sum / (j - i + 1);
+ if (ok(avg) && ok(data[i][1]) && ok(data[j][1]))
+ return true;
+ sum -= data[i][1];
+ i++;
+ }
+ }
+ return false;
+ }
+ """)
+
+ b.inject_js("""
+ ph_plot_data_plateau = function (sel, min, max, duration, label) {
+ return ph_plateau(window.plot_state.data(sel)[0].data, min, max, duration, label);
+ }
+ """)
+
+ meminfo = read_mem_info(m)
+ mem_avail = meminfo['MemAvailable']
+ with b.wait_timeout(60):
+ b.wait_js_func("ph_plot_data_plateau", "direct", mem_avail * 0.85, mem_avail * 1.15, 15, "mem")
+
+ meminfo = read_mem_info(m)
+ mem_avail = meminfo['MemAvailable']
+ with b.wait_timeout(60):
+ b.wait_js_func("ph_plot_data_plateau", "pmcd", mem_avail * 0.85, mem_avail * 1.15, 15, "mem")
+
+ def testPageStatus(self):
+ b = self.browser
+
+ self.login_and_go("/playground")
+
+ b.set_input_text("#type", "info")
+ b.set_input_text("#title", "My Little Page Status")
+ b.click("#set-status")
+
+ b.switch_to_top()
+ b.wait_visible("#development-info")
+ b.mouse("#development-info", "mouseenter")
+ b.wait_in_text(".pf-v5-c-tooltip", "My Little Page Status")
+ b.mouse("#development-info", "mouseleave")
+
+ b.go("/playground/notifications-receiver")
+ b.enter_page("/playground/notifications-receiver")
+ b.wait_text("#received-type", "info")
+ b.wait_text("#received-title", "My Little Page Status")
+
+ b.switch_to_top()
+ b.go("/playground")
+ b.enter_page("/playground")
+ b.click("#clear-status")
+
+ b.switch_to_top()
+ b.wait_not_present("#development-info")
+
+ b.go("/playground/notifications-receiver")
+ b.enter_page("/playground/notifications-receiver")
+ b.wait_text("#received-type", "-")
+ b.wait_text("#received-title", "-")
+
+ def testHistory(self):
+
+ b = self.browser
+
+ def assert_location(path_hash):
+ self.assertEqual(path_hash,
+ self.browser.eval_js("window.location.pathname + window.location.hash"))
+
+ self.login_and_go("/system")
+
+ # Create a login entry so that the "View last login" button appears
+ b.logout()
+ self.login_and_go("/system")
+
+ b.switch_to_top()
+ assert_location("/system")
+
+ b.click('#nav-system a[href="/users"]')
+ b.enter_page("/users")
+ b.switch_to_top()
+ assert_location("/users")
+
+ b.enter_page("/users")
+ b.click('a[href="#/root"]')
+ b.wait_visible("#account-title")
+ self.assertIn(b.text("#account-title"), ["root", "Super User"])
+ b.switch_to_top()
+ assert_location("/users#/root")
+
+ b.enter_page("/users")
+ b.click("nav a:contains(Accounts)")
+ b.wait_visible("button:contains('Create new account')")
+ b.switch_to_top()
+ assert_location("/users")
+
+ b.eval_js("window.history.back()")
+ b.enter_page("/users")
+ b.wait_visible("#account-title")
+ self.assertIn(b.text("#account-title"), ["root", "Super User"])
+ b.switch_to_top()
+ assert_location("/users#/root")
+
+ b.eval_js("window.history.forward()")
+ b.enter_page("/users")
+ b.wait_visible("button:contains('Create new account')")
+ b.switch_to_top()
+ assert_location("/users")
+
+ b.eval_js("window.history.back()")
+ b.enter_page("/users")
+ b.wait_visible("#account-title")
+ self.assertIn(b.text("#account-title"), ["root", "Super User"])
+ b.switch_to_top()
+ assert_location("/users#/root")
+
+ b.eval_js("window.history.back()")
+ b.enter_page("/users")
+ b.wait_visible("button:contains('Create new account')")
+ b.switch_to_top()
+ assert_location("/users")
+
+ b.click('#nav-system a[href="/system/terminal"]')
+ b.enter_page("/system/terminal")
+ b.switch_to_top()
+ assert_location("/system/terminal")
+
+ b.eval_js("window.history.back()")
+ b.enter_page("/users")
+ b.wait_visible("button:contains('Create new account')")
+ b.switch_to_top()
+ assert_location("/users")
+
+ b.eval_js("window.history.back()")
+ b.enter_page("/system")
+ b.switch_to_top()
+ assert_location("/system")
+
+ # CoreOS does not keep login history
+ if not self.machine.ostree_image:
+
+ b.enter_page("/system")
+ b.click("button:contains(View login history)")
+ b.enter_page("/users")
+ b.wait_text("#account-title", "Administrator")
+ b.switch_to_top()
+ assert_location("/users#/admin")
+
+ b.eval_js("window.history.back()")
+ b.enter_page("/system")
+ b.switch_to_top()
+ assert_location("/system")
+
+ def testAllNavEntries(self):
+ b = self.browser
+ self.login_and_go()
+
+ # the <a> links should be unique by their href= attributes, so get these
+ hrefs = b.eval_js("[...document.querySelectorAll('#nav-system .nav-item a')].map(el => el.getAttribute('href'))")
+ for href in hrefs:
+ b.click(f"#nav-system .nav-item a[href='{href}']")
+ b.wait_visible(f"iframe.container-frame[name='cockpit1:localhost{href}'][data-loaded]")
+
+ # logging out too fast, some D-Bus services get disconnected
+ self.allow_restart_journal_messages()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-reauthorize b/test/verify/check-reauthorize
new file mode 100755
index 0000000..ad98196
--- /dev/null
+++ b/test/verify/check-reauthorize
@@ -0,0 +1,127 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestReauthorize(testlib.MachineCase):
+ def testBasic(self):
+ self.allow_journal_messages('.*dropping message while waiting for child to exit.*')
+ b = self.browser
+ m = self.machine
+
+ # Log in without being authorized
+ self.login_and_go("/playground/test", superuser=False)
+ b.leave_page()
+ b.check_superuser_indicator("Limited access")
+ b.enter_page("/playground/test")
+ b.click(".cockpit-internal-reauthorize button")
+ b.wait_in_text(".cockpit-internal-reauthorize span", 'result:')
+ self.assertEqual(b.text(".cockpit-internal-reauthorize span"), 'result: access-denied')
+
+ # Log in again but be authorized
+ b.relogin("/playground/test", superuser=True)
+ b.leave_page()
+ b.check_superuser_indicator("Administrative access")
+ b.enter_page("/playground/test")
+ b.click(".cockpit-internal-reauthorize button")
+ b.wait_in_text(".cockpit-internal-reauthorize span", 'result:')
+ self.assertEqual(b.text(".cockpit-internal-reauthorize span"), 'result: authorized')
+
+ # Lock a file so that we can check that the lock went away
+ # after deauthorizing.
+ m.execute("touch /tmp/playground-test-lock")
+ b.click(".lock-channel button")
+ b.wait_in_text(".lock-channel span", 'locked')
+ m.execute("! flock --nonblock /tmp/playground-test-lock true")
+
+ # Deauthorize user
+ b.drop_superuser()
+ m.execute("flock --timeout 10 /tmp/playground-test-lock true")
+ b.click(".cockpit-internal-reauthorize button")
+ b.wait_in_text(".cockpit-internal-reauthorize span", 'result:')
+ self.assertEqual(b.text(".cockpit-internal-reauthorize span"), 'result: access-denied')
+
+ @testlib.skipOstree("ssh root login not allowed")
+ def testSuper(self):
+ b = self.browser
+
+ self.login_and_go("/playground/test")
+
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: uid=0')
+
+ # Deauthorize
+ b.drop_superuser()
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: access-denied')
+
+ # When root, the 'Limited access' etc indicators should not be visible
+ b.logout()
+ self.login_and_go("/playground/test", user="root", enable_root_login=True)
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: uid=0')
+ b.leave_page()
+ b.check_superuser_indicator("")
+
+ def testSudo(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute("useradd user -s /bin/bash -c Barney")
+ m.execute("echo user:foobar | chpasswd")
+
+ b.default_user = "user"
+ self.login_and_go("/playground/test")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+
+ # TODO: this should only be 'access-denied'
+ legit_results = [f'result: {err}' for err in ('access-denied', 'authentication-failed', 'terminated')]
+ self.assertIn(b.text(".super-channel span"), legit_results)
+
+ b.logout()
+
+ # So first ask the user to retype their password
+ self.write_file("/etc/sudoers.d/user-override", "user ALL=(ALL) ALL", append=True)
+ self.login_and_go("/playground/test")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+ self.assertIn('result: uid=0', b.text(".super-channel span"))
+ b.logout()
+
+ # Next login without starting a privileged bridge
+ self.login_and_go("/playground/test", superuser=False)
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+ self.assertEqual(b.text(".super-channel span"), 'result: access-denied')
+ b.logout()
+
+ # Even if sudo doesn't require a password, we shouldn't start a privileged bridge
+ self.write_file("/etc/sudoers.d/user-override", "user ALL=(ALL) NOPASSWD:ALL", append=True)
+ self.login_and_go("/playground/test", superuser=False)
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+ self.assertEqual(b.text(".super-channel span"), 'result: access-denied')
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-selinux b/test/verify/check-selinux
new file mode 100755
index 0000000..f523f21
--- /dev/null
+++ b/test/verify/check-selinux
@@ -0,0 +1,353 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2016 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import time
+
+import testlib
+from lib.constants import TEST_OS_DEFAULT
+
+# simulate httpd accessing user ~/public_html without httpd_enable_homedirs
+SELINUX_SEBOOL_ALERT_SCRIPT = """
+set -e
+mkdir -p ~/selinux_temp
+cd ~/selinux_temp
+cp /bin/ls ls
+chcon -t httpd_exec_t ls
+su - -c 'mkdir public_html; echo world > public_html/hello.txt' admin
+# this won't work, but it generates an error
+runcon -u system_u -r system_r -t httpd_t -- ./ls ~admin/public_html || true
+"""
+
+SELINUX_RESTORECON_ALERT_SCRIPT = """
+set -e
+mkdir -p ~/.ssh
+ssh-keygen -t rsa -f ~/.ssh/test-avc-rsa -N ""
+mv -f ~/.ssh/authorized_keys ~/.ssh/authorized_keys.test-avc
+cat .ssh/test-avc-rsa.pub >> ~/.ssh/authorized_keys
+chcon -t httpd_exec_t ~/.ssh/authorized_keys
+auditctl -D
+auditctl -w ~/.ssh/authorized_keys -p a
+ssh -o StrictHostKeyChecking=no -o 'BatchMode=yes' -i ~/.ssh/test-avc-rsa localhost || true
+mv -f ~/.ssh/authorized_keys.test-avc ~/.ssh/authorized_keys
+"""
+
+
+@testlib.skipDistroPackage()
+class TestSelinux(testlib.MachineCase):
+ provision = {
+ "0": {},
+ "ansible_machine": {"image": TEST_OS_DEFAULT, "memory_mb": 512}
+ }
+
+ @testlib.skipImage("No setroubleshoot", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipOstree("No setroubleshoot")
+ @testlib.skipBrowser("Firefox needs http to access clipboard", "firefox")
+ def testTroubleshootAlerts(self):
+ b = self.browser
+ m = self.machine
+
+ # Do some local modifications
+ m.execute("semanage fcontext --add -t samba_share_t /var/tmp/")
+ m.execute("semanage boolean --modify --on zebra_write_config")
+ self.allow_journal_messages("audit: type=1405 .* bool=zebra_write_config val=1 old_val=0.*")
+ self.allow_journal_messages(".*couldn't .* /org/fedoraproject/Setroubleshootd: GDBus.Error:org.freedesktop.DBus.Error.NoReply:.*")
+
+ self.login_and_go("/selinux")
+
+ # Starting Cockpit might produce some alerts (e.g. from
+ # starting rhsm.service), so let's give them some time to
+ # appear before cleaning it all out.
+ with b.wait_timeout(60):
+ b.wait_js_cond("ph_in_text('body', 'No SELinux alerts.') || ph_is_present('tbody .pf-v5-c-table__toggle')")
+ time.sleep(30)
+
+ dismiss_sel = ".selinux-alert-dismiss"
+
+ # there are some distros with broken SELinux policies which always appear; so first, clean these up
+ while True:
+ # we either have no alerts, or an expand button in the table
+ b.wait_js_cond("ph_in_text('body', 'No SELinux alerts.') || ph_is_present('tbody .pf-v5-c-table__toggle')")
+ if "No SELinux alerts." in b.text('body'):
+ break
+ num_alerts = b.call_js_func("ph_count", "#selinux-alerts tbody")
+ b.click('tbody:first-of-type input[type=checkbox]')
+ b.click(dismiss_sel)
+ # wait for the dismissed alert to go away
+ b.wait_js_func("ph_count_check", "#selinux-alerts tbody", num_alerts - 1)
+
+ # wait for Modifications table to initialize
+ b.wait_text_matches(".modifications-table", "Allow zebra.*write.*config")
+
+ # httpd_read_user_content should be off by default
+ self.assertIn("-> off", m.execute("getsebool httpd_read_user_content"))
+ # and not part of modifications
+ b.wait_not_in_text(".modifications-table", "httpd to read user content")
+
+ #########################################################
+ # trigger an sebool alert
+ # this triggers two solutions -- the preferred sebool module, which can be applied automatically,
+ # and the fallback "report a bug" ausearch one, which cannot be applied automatically
+ self.machine.execute(SELINUX_SEBOOL_ALERT_SCRIPT)
+
+ row_selector = "tbody:contains('ls from read access on the directory')"
+
+ # wait for the alert to appear
+ with b.wait_timeout(60):
+ b.wait_visible(row_selector)
+
+ # expand it to see details
+ toggle_selector = row_selector + " .pf-v5-c-table__toggle button"
+ b.click(toggle_selector)
+
+ # this should have two alerts, but there seems to be a known issue that some messages are delayed
+ # b.wait_in_text(row_selector, "2 occurrences")
+
+ panel_selector = row_selector + " tr.pf-m-expanded .ct-listing-panel-body"
+
+ # manual label change solution is present
+ relabel_selector = panel_selector + " .selinux-details:contains('public_content_t')"
+ b.wait_in_text(relabel_selector, "You need to change the label on public_html to public_content_t")
+ b.wait_in_text(relabel_selector, "Unable to apply this solution automatically")
+
+ # an automatic solution is present for sebool
+ sebool_selector1 = panel_selector + " .selinux-details:contains('httpd_enable_homedirs')"
+ b.wait_in_text(sebool_selector1, "by enabling the 'httpd_enable_homedirs' boolean.")
+
+ # sabotage the solution command to test failure alerts
+ sebool_path = m.execute("command -v setsebool").strip()
+ m.execute(f"chmod a-x {sebool_path}")
+ b.click(f"{sebool_selector1} button{self.default_btn_class}")
+ b.wait_in_text(f"{sebool_selector1} .pf-v5-c-alert", "Solution failed")
+ b.wait_in_text(f"{sebool_selector1} .pf-v5-c-alert", "setsebool: Permission denied")
+
+ # we can't re-attempt the solution, dismiss it
+ b.click(row_selector + " input[type=checkbox]")
+ b.click(dismiss_sel)
+ b.wait_not_present(row_selector)
+ # unbreak sebool, and trigger the error again
+ m.execute(f"chmod a+x {sebool_path}; rm -r ~admin/public_html")
+ self.machine.execute(SELINUX_SEBOOL_ALERT_SCRIPT)
+ with b.wait_timeout(60):
+ b.wait_visible(row_selector)
+ b.click(toggle_selector)
+
+ # applying solution works now
+ b.click(sebool_selector1 + " button" + self.default_btn_class)
+ b.wait_in_text(sebool_selector1 + " .pf-v5-c-alert__title", "Solution applied successfully")
+ self.assertIn("-> on", m.execute("getsebool httpd_enable_homedirs"))
+ # system modifications automatically update for this new sebool
+ # was "enable homedir", is now "read home directory"
+ b.wait_text_matches(".modifications-table", "Allow httpd to .* home ?dir")
+
+ # Second sebool solution can still be applied separately
+ sebool_selector2 = panel_selector + " .selinux-details:contains('httpd_unified')"
+ b.wait_text(sebool_selector2 + " button" + self.default_btn_class, "Apply this solution")
+
+ # now dismiss the alert
+ b.click(row_selector + " input[type=checkbox]")
+ b.assert_pixels("#app", "selinux", ignore=["table .ct-listing-panel-actions"])
+ b.click(dismiss_sel)
+ b.wait_not_present(row_selector)
+ b.wait_in_text("body", "No SELinux alerts.")
+
+ #########################################################
+ # trigger a fixable restorecon alert
+ self.machine.execute(SELINUX_RESTORECON_ALERT_SCRIPT)
+
+ row_selector = "tbody:contains('read access on the file /root/.ssh/authorized_keys')"
+
+ # wait for the alert to appear
+ with b.wait_timeout(60):
+ b.wait_visible(row_selector)
+
+ # expand it to see details
+ toggle_selector = row_selector + " .pf-v5-c-table__toggle button"
+ b.click(toggle_selector)
+
+ try:
+ # a solution is present
+ b.wait_in_text(row_selector + " tr.pf-m-expanded .ct-listing-panel-body", "you can run restorecon")
+
+ # and it can be applied
+ btn_sel = f".selinux-details:contains('you can run restorecon') button{self.default_btn_class}"
+ b.click(btn_sel)
+
+ # the button should disappear
+ b.wait_not_present(btn_sel)
+
+ # the fix should be applied
+ b.wait_in_text(row_selector + " .pf-v5-c-alert__title", "Solution applied successfully")
+
+ # and the button should not come back
+ b.wait_not_present(btn_sel)
+ except testlib.Error:
+ print("==== sealert -l '*' ====")
+ print(m.execute("sealert -l '*'"))
+ print("==== audit.log ====")
+ print(m.execute("cat /var/log/audit/audit.log"))
+ print("====================")
+ raise
+
+ b.click(row_selector + " input[type=checkbox]")
+ b.click(dismiss_sel)
+ b.wait_not_present(row_selector)
+
+ try:
+ b.grant_permissions("clipboardReadWrite", "clipboardSanitizedWrite")
+ except RuntimeError:
+ # fallback for older Chrome releases
+ b.grant_permissions("clipboardRead", "clipboardWrite")
+
+ b.wait_visible(".pf-v5-c-data-list__cell:contains(Allow zebra)")
+ b.wait_visible(".pf-v5-c-data-list__cell:contains(fcontext -a -f a -t samba_share_t -r 's0' '/var/tmp/')")
+ b.click("button:contains('View automation script')")
+ ansible_script_sel = ".automation-script-modal .pf-v5-c-modal-box__body section:nth-child(2) textarea"
+ shell_script_sel = ".automation-script-modal .pf-v5-c-modal-box__body section:nth-child(3) textarea"
+ b.click("button:contains('Shell')")
+ b.wait_in_text(shell_script_sel, "boolean -D")
+ b.wait_in_text(shell_script_sel, "fcontext -D")
+ b.wait_in_text(shell_script_sel, "boolean -m -1 zebra_write_config")
+ b.wait_in_text(shell_script_sel, "fcontext -a -f a -t samba_share_t -r 's0' '/var/tmp/'")
+
+ # Check ansible
+ def normalize(script):
+ # HACK: `permissive` type is exported only since policycoreutils 3.0
+ # See https://github.com/SELinuxProject/selinux/commit/3a9b4505b
+ # Combining of versions < 3.0 with versions >= 3.0 provides a bit
+ # different outputs.
+ return script.replace("permissive -D\n", "")
+
+ b.click("button:contains('Ansible')")
+ b.wait_text_matches(ansible_script_sel, "Allow zebra.*write.*config")
+
+ ansible_m = self.machines["ansible_machine"]
+ ansible_m.execute("semanage module -D")
+ ansible_m.write("roles/selinux/tasks/main.yml", b.text(ansible_script_sel))
+ se_before = normalize(ansible_m.execute("semanage export"))
+ ansible_m.execute("ansible -m include_role -a name=selinux localhost")
+ se_after = normalize(ansible_m.execute("semanage export"))
+ local = normalize(m.execute("semanage export"))
+
+ self.assertNotEqual(se_before, se_after)
+ self.assertEqual(local, se_after)
+
+ # Check that ansible is idempotent
+ m.execute("semanage boolean --modify --off zebra_write_config")
+ b.reload()
+ b.enter_page("/selinux")
+ with b.wait_timeout(30):
+ b.wait_visible(".pf-v5-c-data-list__cell:contains(Disallow zebra)")
+ b.click("button:contains('View automation script')")
+ ansible_m.write("roles/selinux/tasks/main.yml", b.text(ansible_script_sel))
+ se_before = se_after
+ ansible_m.execute("ansible -m include_role -a name=selinux localhost")
+ se_after = normalize(ansible_m.execute("semanage export"))
+ local = normalize(m.execute("semanage export"))
+ self.assertNotEqual(se_before, se_after)
+ self.assertEqual(local, se_after)
+
+ # Check the content of clipboard by pasting the clipboard to the terminal
+ b.click("button:contains('Shell script')")
+ b.wait_in_text(shell_script_sel, "boolean -D")
+ b.click(".automation-script-modal .btn-clipboard")
+ b.go("/system/terminal")
+ b.enter_page("/system/terminal")
+ b.focus('.terminal')
+ b.key_press("\"\r")
+ # Right click and paste
+ b.mouse(".terminal", "contextmenu", btn=2)
+ b.click('.contextMenu li:nth-child(2) button')
+ b.wait_in_text(".xterm-accessibility-tree", "semanage import <<EOF")
+ b.wait_in_text(".xterm-accessibility-tree", "boolean -D")
+ b.wait_in_text(".xterm-accessibility-tree", "boolean -m -0 zebra_write_config")
+ b.wait_in_text(".xterm-accessibility-tree", "fcontext -a -f a -t samba_share_t -r 's0' '/var/tmp/'")
+
+ m.execute("semanage boolean -D; semanage fcontext -D; semanage module -D")
+ b.go("/selinux")
+ b.enter_page("/selinux")
+ b.reload()
+ b.enter_page("/selinux")
+ with b.wait_timeout(30):
+ if m.image.startswith("rhel"):
+ # HACK: rhc sets itself as permissive on RHEL; https://issues.redhat.com/browse/RHEL-20352
+ b.wait_text_matches("ul[aria-label='System modifications']", "No system modifications|permissive -a rhcd_t")
+ else:
+ b.wait_text("ul[aria-label='System modifications']", "No system modifications")
+ b.relogin("/selinux", "admin", superuser=False)
+ b.wait_text("ul[aria-label='System modifications']", "The logged in user is not permitted to view system modifications")
+
+
+@testlib.skipDistroPackage()
+@testlib.skipImage("No cockpit-selinux", "debian-*", "ubuntu-*", "arch")
+@testlib.skipOstree("No cockpit-selinux")
+@testlib.nondestructive
+class TestSelinuxEnforcing(testlib.MachineCase):
+ def test(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/selinux")
+
+ #########################################################
+ # wait for the page to be present
+ b.wait_in_text("body", "SELinux policy")
+
+ def assertEnforce(active):
+ # polled every 10s
+ with b.wait_timeout(20):
+ b.wait_visible(".pf-v5-c-switch__input" + (active and ":checked" or ":not(:checked)"))
+
+ # SELinux should be enabled and enforcing at the beginning
+ assertEnforce(active=True)
+ self.addCleanup(m.execute, "setenforce 1")
+ m.execute("getenforce | grep -q 'Enforcing'")
+
+ # now set to permissive using the ui button
+ b.click(".pf-v5-c-switch__input")
+ assertEnforce(active=False)
+ m.execute("getenforce | grep -q 'Permissive'")
+
+ # when in permissive mode, expect a warning
+ b.wait_in_text("div.selinux-policy-ct", "Setting deviates")
+
+ # switch back using cli, ui should react
+ m.execute("setenforce 1")
+ assertEnforce(active=True)
+
+ # warning should disappear
+ b.wait_not_in_text("div.selinux-policy-ct", "Setting deviates")
+
+ # Switch to another page
+ b.switch_to_top()
+ b.go("/system")
+ b.enter_page("/system")
+
+ # Now on another page change the status
+ m.execute("setenforce 0")
+
+ # Switch back into the page and we get the updated status
+ b.switch_to_top()
+ b.go("/selinux")
+ b.enter_page("/selinux")
+ assertEnforce(active=False)
+ b.wait_in_text("div.selinux-policy-ct", "Setting deviates")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-session b/test/verify/check-session
new file mode 100755
index 0000000..3ff0ef5
--- /dev/null
+++ b/test/verify/check-session
@@ -0,0 +1,70 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestSession(testlib.MachineCase):
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ def wait_session(should_exist):
+ testlib.wait(lambda: (should_exist == ("admin" in m.execute("loginctl list-sessions"))))
+
+ wait_session(should_exist=False)
+
+ # Login
+ self.login_and_go("/system")
+ wait_session(should_exist=True)
+
+ # Check session type
+ if not m.ostree_image:
+ session_id = m.execute("loginctl list-sessions | awk '/admin/ {print $1}'").strip()
+ self.assertEqual(m.execute(f"loginctl show-session -p Type {session_id}").strip(), "Type=web")
+
+ # Logout
+ b.logout()
+ b.wait_visible("#login")
+ wait_session(should_exist=False)
+
+ # Login again
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.enter_page("/system")
+ wait_session(should_exist=True)
+
+ # Kill session from the outside
+ m.execute("loginctl terminate-user admin")
+ wait_session(should_exist=False)
+
+ b.relogin("/system", "admin")
+ wait_session(should_exist=True)
+
+ # Kill session from the inside
+ b.logout()
+ wait_session(should_exist=False)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-shell-active-pages b/test/verify/check-shell-active-pages
new file mode 100755
index 0000000..bb61cbb
--- /dev/null
+++ b/test/verify/check-shell-active-pages
@@ -0,0 +1,101 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestActivePages(testlib.MachineCase):
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ # disable preloads; they are different between OSes, make the counting hard, and happen asynchronously
+ self.disable_preload("packagekit", "playground", "systemd")
+
+ self.login_and_go("/system")
+
+ b.wait_visible("#overview")
+
+ def showPagesAssertCount(count):
+ b.switch_to_top()
+ b.mouse("#toggle-menu", "click", altKey=True)
+ b.click("#active-pages")
+ b.wait_visible("#active-pages-dialog")
+ b.wait_js_func("ph_count_check", "#active-pages-dialog tr", count)
+
+ # Initially we have /system
+ showPagesAssertCount(1)
+ b.click("button:contains('Cancel')")
+ b.wait_not_present("#active-pages-dialog")
+
+ b.go("/users")
+ b.enter_page("/users")
+ b.wait_visible("#accounts-list")
+
+ # now we have the users page be present in the pages menu
+ showPagesAssertCount(2)
+ b.click("button:contains('Cancel')")
+ b.wait_not_present("#active-pages-dialog")
+
+ # open the terminal page and start a session we can see on the machine
+ # Remove failed units which will show up in the first terminal line
+ m.execute("systemctl reset-failed")
+ b.go("/system/terminal")
+ b.enter_page("/system/terminal")
+
+ def line_sel(i):
+ return '.terminal .xterm-accessibility-tree div:nth-child(%d)' % i
+
+ # wait for prompt in first line
+ b.wait_visible(".terminal .xterm-accessibility-tree")
+ b.wait_in_text(line_sel(1), '$')
+
+ # run a command that we can easily identify, and which will die with the terminal
+ b.key_press("bash -c 'exec -a kitten cat'\r")
+
+ # wait for command to run
+ m.execute("until pgrep -afx kitten; do sleep 1; done")
+
+ # now close the page
+ showPagesAssertCount(3)
+ # terminal should be pre-checked by default
+ b.wait_visible('tr[data-row-id="cockpit1:localhost/system/terminal"] input[type=checkbox]:checked')
+ # click on the main page as well
+ b.click('tr[data-row-id="cockpit1:localhost/system"] input[type=checkbox]')
+ # wait until it's highlighted
+ b.wait_visible('tr[data-row-id="cockpit1:localhost/system"] input[type=checkbox]:checked')
+ # close
+ b.click("#active-pages-dialog button.pf-m-primary")
+ b.wait_not_present("#active-pages-dialog")
+
+ # this should kill the two frames
+ b.wait_not_present("iframe[name='cockpit1:localhost/system']")
+ b.wait_not_present("iframe[name='cockpit1:localhost/system/terminal']")
+
+ # running shell command should disappear on the system
+ m.execute("while pgrep -afx kitten; do sleep 1; done")
+
+ # there should only be the original page left
+ showPagesAssertCount(1)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-shell-host-switching b/test/verify/check-shell-host-switching
new file mode 100755
index 0000000..ddd141e
--- /dev/null
+++ b/test/verify/check-shell-host-switching
@@ -0,0 +1,524 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2020 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import time
+
+import testlib
+
+
+@testlib.skipDistroPackage()
+class HostSwitcherHelpers:
+
+ def check_discovered_addresses(self, b, addresses):
+ b.click("button:contains('Add new host')")
+ b.wait_visible('#hosts_setup_server_dialog')
+ self.wait_discovered_addresses(b, addresses)
+ b.click('#hosts_setup_server_dialog .pf-m-link')
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ def wait_discovered_addresses(self, b, expected):
+ b.wait_js_cond(f'ph_select("#hosts_setup_server_dialog datalist option").length == {len(expected)}')
+ # Check that we rendered all expected hosts
+ for address in expected:
+ b._wait_present(f"#hosts_setup_server_dialog datalist option[value='{address}']")
+
+ def wait_host_addresses(self, b, expected):
+ b.wait_js_cond(f'ph_select("#nav-hosts .nav-item a").length == {len(expected)}')
+ for address in expected:
+ b.wait_visible(f"#nav-hosts .nav-item a[href='/@{address}']")
+
+ def machine_remove(self, b, address, second_to_last=False):
+ b.click("button:contains('Edit hosts')")
+ b.click(f".nav-item a[href='/@{address}'] + span button.nav-action.pf-m-danger")
+ if second_to_last:
+ b.wait_not_present("button:contains('Stop editing hosts')")
+ b.wait_not_visible(".nav-item a[href='/@localhost'] + span button.nav-action.pf-m-danger")
+ else:
+ b.click("button:contains('Stop editing hosts')")
+
+ # Wait until all related iframes are gone
+ b.wait_js_func("""(function (dropped) {
+ const frames = document.getElementsByTagName("iframe");
+ for (i = 0; i < frames.length; i++)
+ if (frames[i].getAttribute['data-host'] === dropped)
+ return false;
+ return true;
+ })""", address)
+
+ # HACK: Dropping the machine does not terminate SSH connection; https://github.com/cockpit-project/cockpit/issues/19672
+ self.machine.execute(f"pkill -f [s]sh.*{address}; while pgrep -f [s]sh.*{address}; do sleep 1; done")
+
+ def add_new_machine(self, b, address, known_host=False, pixel_label=None, user=None, expect_password_auth=False):
+ b.click("button:contains('Add new host')")
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.set_input_text('#add-machine-address', address)
+ if user:
+ b.set_input_text('#add-machine-user', user)
+ if pixel_label:
+ b.assert_pixels("#hosts_setup_server_dialog", pixel_label)
+ b.click('#hosts_setup_server_dialog .pf-m-primary:contains("Add")')
+ if not known_host:
+ b.wait_in_text('#hosts_setup_server_dialog',
+ f"You are connecting to {address.removeprefix('ssh://')} for the first time")
+ b.click('#hosts_setup_server_dialog .pf-m-primary')
+ if expect_password_auth:
+ b.wait_in_text("#hosts_setup_server_dialog", "Unable to log in to")
+ b.set_input_text('#login-custom-password', "foobar")
+ b.click('#hosts_setup_server_dialog button:contains("Log in")')
+ with b.wait_timeout(30):
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ def connect_and_wait(self, b, address, expected_user=None):
+ b.click(f"a[href='/@{address}']")
+ b.click("#hosts-sel button")
+ b.wait_visible(f".connected a[href='/@{address}']")
+ if expected_user:
+ b.wait_text("#current-username", expected_user)
+ # Switch back to localhost, since the rest of the test expects that
+ b.click("a[href='/@localhost']")
+ b.click("#hosts-sel button")
+
+
+@testlib.skipDistroPackage()
+@testlib.todoPybridgeRHEL8()
+class TestHostSwitching(testlib.MachineCase, HostSwitcherHelpers):
+ provision = {
+ 'machine1': {"address": "10.111.113.1/20", "memory_mb": 512},
+ 'machine2': {"address": "10.111.113.2/20", "memory_mb": 512},
+ 'machine3': {"address": "10.111.113.3/20", "memory_mb": 512}
+ }
+
+ def setUp(self):
+ super().setUp()
+
+ self.setup_provisioned_hosts(disable_preload=True)
+ # Override hostname from machine1 to localhost
+ self.machines["machine1"].execute("hostnamectl set-hostname localhost")
+
+ self.setup_ssh_auth()
+
+ # removing machines interrupts channels
+ self.allow_restart_journal_messages()
+ self.allow_hostkey_messages()
+
+ def testBasic(self):
+ b = self.browser
+ m1 = self.machines["machine1"]
+ m2 = self.machines["machine2"]
+ m3 = self.machines["machine3"]
+
+ m2.execute("hostnamectl set-hostname machine2")
+ m3.execute("hostnamectl set-hostname machine3")
+
+ # This should all work without being admin on machine1
+ self.login_and_go(superuser=False)
+
+ b.assert_pixels("#nav-system", "nav-system", skip_layouts=["mobile"])
+ b.set_layout("mobile")
+ b.click("#nav-system-item")
+ b.wait_visible("#nav-system.interact")
+ b.assert_pixels_in_current_layout("#nav-system", "nav-system")
+ b.click("#nav-system-item")
+ b.wait_not_present("#nav-system.interact")
+ b.set_layout("desktop")
+
+ b.assert_pixels("#hosts-sel", "hosts-sel-closed")
+
+ b.click("#hosts-sel button")
+ self.wait_host_addresses(b, ["localhost"])
+
+ b.wait_not_present("button:contains('Edit hosts')")
+
+ # Test that transient hostname shows up
+ m1.execute("hostnamectl set-hostname ''")
+ m1.execute("hostnamectl set-hostname --transient 'mydhcpname'")
+ b.wait_in_text("#nav-hosts .nav-item a", "mydhcpname")
+ m1.execute("hostnamectl set-hostname 'localhost'")
+
+ self.add_new_machine(b, "10.111.113.2", pixel_label="host-add-dialog")
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ # defaults to current host user name "admin"
+ self.connect_and_wait(b, "10.111.113.2", "admin")
+
+ # Main host should have both buttons disabled, the second both enabled
+ b.click("button:contains('Edit hosts')")
+ b.wait_visible(".nav-item a[href='/@localhost'] + span button.nav-action.pf-m-danger:disabled")
+ b.wait_visible(".nav-item a[href='/@localhost'] + span button.nav-action.pf-m-secondary:disabled")
+ b.wait_visible(".nav-item a[href='/@10.111.113.2'] + span button.nav-action.pf-m-danger:not(:disabled)")
+ b.wait_visible(".nav-item a[href='/@10.111.113.2'] + span button.nav-action.pf-m-secondary:not(:disabled)")
+ b.assert_pixels(".edit-hosts", "edit-hosts")
+ b.click("button:contains('Stop editing hosts')")
+ b.wait_not_visible(".nav-item a[href='/@localhost'] + span button.nav-action.pf-m-danger")
+ b.wait_not_visible(".nav-item a[href='/@10.111.113.2'] + span button.nav-action.pf-m-secondary")
+
+ b.wait_not_present(".nav-item a[href='/@10.111.113.2'] .nav-status")
+
+ self.add_new_machine(b, "10.111.113.3")
+ self.wait_host_addresses(b, ["localhost", "10.111.113.3", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.3", "admin")
+
+ b.assert_pixels("#nav-hosts", "nav-hosts-2-remotes")
+
+ # Remove two
+ self.machine_remove(b, "10.111.113.2")
+ self.wait_host_addresses(b, ["localhost", "10.111.113.3"])
+
+ self.machine_remove(b, "10.111.113.3", second_to_last=True)
+ self.wait_host_addresses(b, ["localhost"])
+
+ # Check that the two removed machines are listed in "Add Host"
+ self.check_discovered_addresses(b, ["10.111.113.2", "10.111.113.3"])
+
+ # Add one back, check addresses on both browsers
+ self.add_new_machine(b, "10.111.113.2", known_host=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2")
+ self.check_discovered_addresses(b, ["10.111.113.3"])
+
+ b.wait_not_present(".nav-item a[href='/@10.111.113.2'] .nav-status")
+
+ # And the second one, check addresses
+ self.add_new_machine(b, "10.111.113.3", known_host=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2", "10.111.113.3"])
+ self.connect_and_wait(b, "10.111.113.3")
+ self.check_discovered_addresses(b, [])
+
+ # Test change user, not doing in edit to reuse machines
+
+ # Navigate to load iframe
+ b.click("#nav-hosts .nav-item a[href='/@10.111.113.3']")
+ b.wait_visible("iframe.container-frame[name='cockpit1:10.111.113.3/system']")
+
+ b.click("#hosts-sel button")
+ b.click("button:contains('Edit hosts')")
+
+ b.click("#nav-hosts .nav-item a[href='/@10.111.113.3'] + span button.nav-action.pf-m-secondary")
+
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.set_input_text('#add-machine-user', 'bad-user')
+ b.click('#hosts_setup_server_dialog .pf-m-primary')
+ b.wait_in_text("#hosts_setup_server_dialog", "Unable to log in to")
+ b.click('#hosts_setup_server_dialog button:contains("Cancel")')
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ # Test switching
+ b.wait_js_cond('ph_select("#nav-hosts .nav-item a").length == 3')
+
+ b.click("#nav-hosts .nav-item a[href='/@localhost']")
+ b.wait_js_cond('window.location.pathname == "/system"')
+
+ b.click("#hosts-sel button")
+ b.click("#nav-hosts .nav-item a[href='/@10.111.113.2']")
+ b.wait_js_cond('window.location.pathname.indexOf("/@10.111.113.2") === 0')
+
+ b.click("#hosts-sel button")
+ b.click("#nav-hosts .nav-item a[href='/@10.111.113.3']")
+ b.wait_js_cond('window.location.pathname.indexOf("/@10.111.113.3") === 0')
+
+ b.enter_page("/system", "10.111.113.3")
+ b.wait_text_not("#system_information_systime_button", "")
+ b.click(".system-information a") # View hardware details
+ b.enter_page("/system/hwinfo", "10.111.113.3")
+ b.click(".pf-v5-c-breadcrumb li:first-child")
+ b.enter_page("/system", "10.111.113.3")
+ b.wait_in_text(".ct-overview-header-hostname", "machine3")
+
+ # Remove host underneath ourselves
+ b.switch_to_top()
+ b.click("#hosts-sel button")
+ b.click("button:contains('Edit hosts')")
+ b.click("#nav-hosts .nav-item a[href='/@10.111.113.3'] + span button.nav-action.pf-m-danger")
+ b.wait_not_present("iframe.container-frame[name='cockpit1:10.111.113.3/network']")
+ b.wait_js_cond('window.location.pathname == "/system"')
+
+ b.enter_page("/system", "localhost")
+
+ # remove machine2 as well, to return to a blank slate
+ b.switch_to_top()
+ b.click("#hosts-sel button")
+ self.machine_remove(b, "10.111.113.2", second_to_last=True)
+ self.wait_host_addresses(b, ["localhost"])
+
+ #
+ # check various connection string formats and user names
+ # the tests above only covers implied "admin" user
+ #
+
+ self.machines["machine2"].execute("useradd --create-home someone; echo someone:foobar | chpasswd")
+
+ # plain address and separate "User name:" field
+ self.add_new_machine(b, "10.111.113.2", known_host=True, user="someone", expect_password_auth=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2", "someone")
+ self.machine_remove(b, "10.111.113.2", second_to_last=True)
+
+ # address with user and different "User name:" field, latter wins
+ self.add_new_machine(b, "admin@10.111.113.2", known_host=True, user="someone", expect_password_auth=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2", "someone")
+ self.machine_remove(b, "10.111.113.2", second_to_last=True)
+
+ # reset session store to forget previous user/host connections
+ b.relogin()
+ b.click("#hosts-sel button")
+
+ # ssh:// prefix and implied user
+ self.add_new_machine(b, "ssh://10.111.113.2", known_host=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2", "admin")
+ self.machine_remove(b, "10.111.113.2", second_to_last=True)
+
+ # ssh:// prefix and separate "User name:" field
+ self.add_new_machine(b, "ssh://10.111.113.2", known_host=True, user="admin")
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2", "admin")
+ self.machine_remove(b, "10.111.113.2", second_to_last=True)
+
+ self.add_new_machine(b, "ssh://10.111.113.2", known_host=True, user="someone", expect_password_auth=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2", "someone")
+ self.machine_remove(b, "10.111.113.2", second_to_last=True)
+
+ # ssh:// prefix with user name
+ self.add_new_machine(b, "ssh://someone@10.111.113.2", known_host=True, expect_password_auth=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2", "someone")
+ self.machine_remove(b, "10.111.113.2", second_to_last=True)
+
+ # ssh:// prefix with user and different "User name:" field, latter wins
+ self.add_new_machine(b, "ssh://admin@10.111.113.2", known_host=True, user="someone", expect_password_auth=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2", "someone")
+ self.machine_remove(b, "10.111.113.2", second_to_last=True)
+
+ # ssh:// prefix with user name and port in the connection target
+ self.add_new_machine(b, "ssh://admin@10.111.113.2:22", known_host=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2")
+ self.machine_remove(b, "10.111.113.2", second_to_last=True)
+
+ self.allow_journal_messages(".*server offered unsupported authentication methods: password public-key.*")
+
+ def testBasicAsAdmin(self):
+ b = self.browser
+
+ # When being admin, changes in the host switcher are supposed
+ # to be reflected in all browser sessions.
+
+ self.login_and_go()
+
+ b.click("#hosts-sel button")
+ self.wait_host_addresses(b, ["localhost"])
+
+ b.wait_not_present("button:contains('Edit hosts')")
+
+ # Start second browser and check that it is in sync
+ b2 = self.new_browser()
+ b2.default_user = "admin"
+ b2.login_and_go()
+
+ b2.click("#hosts-sel button")
+ self.wait_host_addresses(b2, ["localhost"])
+
+ self.add_new_machine(b, "10.111.113.2")
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.wait_host_addresses(b2, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2")
+ self.connect_and_wait(b2, "10.111.113.2")
+
+ # Main host should have both buttons disabled, the second both enabled
+ b.click("button:contains('Edit hosts')")
+ b.wait_visible(".nav-item a[href='/@localhost'] + span button.nav-action.pf-m-danger:disabled")
+ b.wait_visible(".nav-item a[href='/@localhost'] + span button.nav-action.pf-m-secondary:disabled")
+ b.wait_visible(".nav-item a[href='/@10.111.113.2'] + span button.nav-action.pf-m-danger:not(:disabled)")
+ b.wait_visible(".nav-item a[href='/@10.111.113.2'] + span button.nav-action.pf-m-secondary:not(:disabled)")
+ b.click("button:contains('Stop editing hosts')")
+ b.wait_not_visible(".nav-item a[href='/@localhost'] + span button.nav-action.pf-m-danger")
+ b.wait_not_visible(".nav-item a[href='/@10.111.113.2'] + span button.nav-action.pf-m-secondary")
+
+ b.wait_not_present(".nav-item a[href='/@10.111.113.2'] .nav-status")
+
+ self.add_new_machine(b, "10.111.113.3")
+ self.wait_host_addresses(b, ["localhost", "10.111.113.3", "10.111.113.2"])
+ self.wait_host_addresses(b2, ["localhost", "10.111.113.3", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.3")
+ self.connect_and_wait(b2, "10.111.113.3")
+
+ # Remove two
+ self.machine_remove(b, "10.111.113.2")
+ self.wait_host_addresses(b, ["localhost", "10.111.113.3"])
+ self.wait_host_addresses(b2, ["localhost", "10.111.113.3"])
+
+ self.machine_remove(b, "10.111.113.3", second_to_last=True)
+ self.wait_host_addresses(b, ["localhost"])
+ self.wait_host_addresses(b2, ["localhost"])
+
+ # Check that the two removed machines are listed in "Add Host"
+ # on both browsers
+ self.check_discovered_addresses(b, ["10.111.113.2", "10.111.113.3"])
+ self.check_discovered_addresses(b2, ["10.111.113.2", "10.111.113.3"])
+
+ # Add one back, check addresses on both browsers
+ self.add_new_machine(b, "10.111.113.2", known_host=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2"])
+ self.wait_host_addresses(b2, ["localhost", "10.111.113.2"])
+ self.connect_and_wait(b, "10.111.113.2")
+ self.check_discovered_addresses(b, ["10.111.113.3"])
+ self.check_discovered_addresses(b2, ["10.111.113.3"])
+
+ b.wait_not_present(".nav-item a[href='/@10.111.113.2'] .nav-status")
+
+ # And the second one, check addresses on both browsers
+ self.add_new_machine(b, "10.111.113.3", known_host=True)
+ self.wait_host_addresses(b, ["localhost", "10.111.113.2", "10.111.113.3"])
+ self.wait_host_addresses(b2, ["localhost", "10.111.113.2", "10.111.113.3"])
+ self.connect_and_wait(b, "10.111.113.3")
+ self.check_discovered_addresses(b, [])
+ self.check_discovered_addresses(b2, [])
+
+ @testlib.no_retry_when_changed
+ def testEdit(self):
+ b = self.browser
+ m1 = self.machines['machine1']
+ m2 = self.machines['machine2']
+ m3 = self.machines['machine3']
+
+ m2.execute("hostnamectl set-hostname machine2")
+ m3.execute("hostnamectl set-hostname machine3")
+
+ for user in ["franz", "hera"]:
+ self.allow_journal_messages(f"Could not chdir to home directory /home/{user}: No such file or directory")
+ m1.execute(f"useradd {user}")
+ m1.execute(f"echo {user}:foobar | chpasswd")
+ m3.execute(f"useradd {user}")
+ m3.execute(f"echo {user}:foobar | chpasswd")
+ self.authorize_pubkey(m3, user, self.get_pubkey(m1, "admin"))
+
+ # This should all work without being admin on m1
+ self.login_and_go(superuser=False)
+
+ b.click("#hosts-sel button")
+ self.add_new_machine(b, "10.111.113.3")
+ self.wait_host_addresses(b, ["localhost", "10.111.113.3"])
+ self.connect_and_wait(b, "10.111.113.3")
+
+ b.click("button:contains('Edit hosts')")
+ b.click("#nav-hosts .nav-item a[href='/@10.111.113.3'] + span button.nav-action.pf-m-secondary")
+
+ b.wait_visible('#hosts_setup_server_dialog .pf-v5-c-modal-box__title:contains("Edit host")')
+ b.set_input_text('#add-machine-user', 'hera')
+ b.click('#hosts_setup_server_dialog .pf-m-primary')
+ with b.wait_timeout(30):
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ b.wait_text(".nav-item a[href='/@10.111.113.3']", "hera @machine3")
+
+ # Editing the username of an existing host is possible
+ b.click("#nav-hosts .nav-item a[href='/@10.111.113.3'] + span button.nav-action.pf-m-secondary")
+ b.set_input_text('#add-machine-user', 'franz')
+
+ def tab(n):
+ for _ in range(n):
+ args = {"type": "keyDown", "key": "Tab"}
+ b.cdp.invoke("Input.dispatchKeyEvent", **args)
+ args["type"] = "keyUp"
+ b.cdp.invoke("Input.dispatchKeyEvent", **args)
+
+ def enter():
+ args = {"type": "keyDown", "key": "Enter"}
+ b.cdp.invoke("Input.dispatchKeyEvent", **args)
+ args["type"] = "keyUp"
+ b.cdp.invoke("Input.dispatchKeyEvent", **args)
+
+ # <input type="color /> is rather difficult to set with tests
+ # On Firefox the popup window cannot be targeted nor with mouse nor keayboard
+ # On Chomium it is possible to tab-navigate through the color selector
+ # So tab to RGB inputs and type in zeros
+ if b.cdp.browser.name == "chromium":
+ b.focus("input[type=color]")
+ b.click("input[type=color]")
+ time.sleep(1) # We cannot wait until the popup opens up, so just little of waiting
+ tab(3)
+ b.key_press("0")
+ tab(1)
+ b.key_press("0")
+ tab(1)
+ b.key_press("0")
+ enter()
+
+ b.click('#hosts_setup_server_dialog .pf-m-primary')
+ with b.wait_timeout(30):
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ b.wait_text(".nav-item a[href='/@10.111.113.3']", "franz @machine3")
+
+ # Go to the updated machine and try to change whilst on it
+ b.click("#nav-hosts .nav-item a[href='/@10.111.113.3']")
+ b.wait_visible("iframe.container-frame[name='cockpit1:franz@10.111.113.3/system']")
+
+ b.wait_text("#hosts-sel button .pf-v5-c-select__toggle-text", "franz@machine3")
+ b.click("#hosts-sel button")
+ b.wait_text(".nav-item a[href='/@10.111.113.3']", "franz @machine3")
+ b.click("button:contains('Edit hosts')")
+ b.click("#nav-hosts .nav-item a[href='/@10.111.113.3'] + span button.nav-action.pf-m-secondary")
+
+ b.wait_val('#add-machine-address', "10.111.113.3")
+ if b.cdp.browser.name == "chromium":
+ self.assertEqual(b.attr("input[type=color]", "value"), "#000000")
+ b.wait_val('#add-machine-user', 'franz')
+ b.set_input_text('#add-machine-address', "10.111.113.2")
+ b.set_input_text('#add-machine-user', 'admin')
+ if b.cdp.browser.name == "chromium":
+ self.assertNotEqual(b.attr("input[type=color]", "value"), "#000000")
+ b.click('#hosts_setup_server_dialog .pf-m-primary')
+ b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
+ b.click('#hosts_setup_server_dialog .pf-m-primary')
+ with b.wait_timeout(30):
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ b.wait_text("#hosts-sel button .pf-v5-c-select__toggle-text", "admin@machine2")
+
+ # Changing the address of a host will navigate to that host,
+ # and that will close the host switcher. Let's open it again
+ # to check it.
+ b.click("#hosts-sel button")
+ b.wait_not_present(".nav-item a[href='/@10.111.113.3']")
+ b.wait_text(".nav-item a[href='/@10.111.113.2']", "admin @machine2")
+
+ def testNoAutoconnect(self):
+ b = self.browser
+ m2 = self.machines["machine2"]
+
+ self.login_and_go(None)
+
+ # And and connect to a second machine
+ b.click("#hosts-sel button")
+ self.add_new_machine(b, "10.111.113.2")
+ b.click("a[href='/@10.111.113.2']")
+ b.wait_visible("iframe.container-frame[name='cockpit1:10.111.113.2/system']")
+ self.assertIn("admin", m2.execute("loginctl"))
+ b.click("#hosts-sel button")
+ b.click("a[href='/@localhost']")
+ b.relogin()
+ time.sleep(60)
+ self.assertNotIn(m2.execute("loginctl"), "admin")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-shell-keys b/test/verify/check-shell-keys
new file mode 100755
index 0000000..edc1bdd
--- /dev/null
+++ b/test/verify/check-shell-keys
@@ -0,0 +1,355 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+FP_SHA256 = "SHA256:iyVAl4Z8riL9Jg4fV9Wv/6cbqebdDtsBEMkojNLLYX8"
+KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEAkRTvQCSEZNPXpA5bP82ilQn3TMeQ6z2NO3O0UwY9z test-name"
+
+
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestKeys(testlib.MachineCase):
+ def testAuthorizedKeys(self):
+ m = self.machine
+ b = self.browser
+
+ # Create a user without any role
+ m.execute("useradd user -s /bin/bash -m -c User")
+ m.execute("echo user:foobar | chpasswd")
+
+ def login(user):
+ self.login_and_go("/users#/" + user, user=user, superuser=False)
+ b.wait_text("#account-user-name", user)
+
+ def add_key(key, fp_sh256, comment):
+ b.click('#authorized-key-add')
+ b.wait_visible("#add-authorized-key-dialog")
+ b.wait_val("#authorized-keys-text", "")
+ b.set_input_text("#authorized-keys-text", key)
+ b.click("#add-authorized-key-dialog button.apply")
+ b.wait_not_present("#add-authorized-key-dialog")
+
+ b.wait_in_text("#account-authorized-keys-list", comment)
+
+ b.wait_not_in_text("#account-authorized-keys-list", "no authorized public keys")
+ text = b.text("#account-authorized-keys-list")
+ self.assertIn(fp_sh256, text)
+
+ # no keys
+ login("user")
+ b.wait_in_text("#account-authorized-keys", "no authorized public keys")
+
+ # add bad
+ b.click('#authorized-key-add')
+ b.wait_visible("#add-authorized-key-dialog")
+ b.wait_val("#authorized-keys-text", "")
+ b.set_input_text("#authorized-keys-text", "bad")
+ b.click("#add-authorized-key-dialog button.apply")
+ b.wait_in_text("#add-authorized-key-dialog", "The key you provided was not valid")
+ b.click("#add-authorized-key-dialog button.cancel")
+
+ # add good
+ add_key(KEY, FP_SHA256, "test-name")
+
+ # Try see admin
+ b.go("#/admin")
+ b.wait_text("#account-user-name", "admin")
+
+ # Not allowed, except on Ubuntu, where we can find out that ~/.ssh doesn't exist, which is shown as "no keys".
+ if "ubuntu" not in m.image and "debian" not in m.image:
+ b.wait_in_text("#account-authorized-keys", "You do not have permission")
+
+ b.logout()
+
+ # delete whole ssh to start fresh
+ m.execute("rm -rf /home/user/.ssh")
+ self.assertNotIn(".ssh", m.execute("ls /home/user"))
+
+ # Log in as admin
+ login("admin")
+ b.go("#/user")
+
+ if "ubuntu" not in m.image and "debian" not in m.image:
+ b.wait_in_text("#account-authorized-keys",
+ "You do not have permission to view the authorized public keys for this account.")
+
+ b.become_superuser()
+
+ b.wait_in_text("#account-authorized-keys", "no authorized public keys")
+
+ def check_perms():
+ perms = m.execute("getfacl -a /home/user/.ssh")
+ self.assertIn("owner: user", perms)
+
+ perms = m.execute("getfacl -a /home/user/.ssh/authorized_keys")
+ self.assertIn("owner: user", perms)
+ self.assertIn("user::rw-", perms)
+ self.assertIn("group::---", perms)
+ self.assertIn("other::---", perms)
+
+ # Adding keys sets permissions properly
+ b.wait_text("#account-user-name", "user")
+ add_key(KEY, FP_SHA256, "test-name")
+ check_perms()
+
+ b.wait_js_func("ph_count_check", "#account-authorized-keys-list tr", 1)
+
+ # Add invalid key directly
+ m.write("/home/user/.ssh/authorized_keys", "\nbad\n", append=True)
+ b.wait_in_text("#account-authorized-keys-list tbody:last-child", "Invalid key")
+ b.wait_js_func("ph_count_check", "#account-authorized-keys-list tr", 2)
+
+ # Removing the key
+ b.click("#account-authorized-keys-list tbody:last-child button")
+ b.wait_not_in_text("#account-authorized-keys", "Invalid key")
+ b.wait_js_func("ph_count_check", "#account-authorized-keys-list tr", 1)
+ data = m.execute("cat /home/user/.ssh/authorized_keys")
+ self.assertEqual(data, KEY + "\n\n")
+ # Permissions are still ok
+ check_perms()
+ b.logout()
+
+ # User can still see their keys
+ login("user")
+ b.wait_in_text("#account-authorized-keys-list tbody:first-child", "test-name")
+
+ b.click("#account-authorized-keys-list tbody:first-child button")
+ b.wait_in_text("#account-authorized-keys", "no authorized public keys")
+
+ self.allow_journal_messages('authorized_keys is not a public key file.')
+ self.allow_journal_messages('Missing callback called fullpath = /home/user/.ssh/authorized_keys')
+ self.allow_journal_messages('')
+
+ # Possible workaround - ssh as `admin` and just do `m.execute()`
+ @testlib.skipBrowser("Firefox cannot do `cockpit.spawn`", "firefox")
+ def testPrivateKeys(self):
+ b = self.browser
+ m = self.machine
+
+ def list_keys():
+ return b.eval_js("cockpit.spawn([ '/bin/sh', '-c', 'ssh-add -l || true' ])")
+
+ def toggleExpandedStateKey(identifier):
+ b.click(f"tr[data-name='{identifier}'] .pf-v5-c-table__toggle > button")
+
+ def waitKeyPresent(identifier, present):
+ if present:
+ b.wait_visible(f"tr[data-name='{identifier}']")
+ else:
+ b.wait_not_present(f"tr[data-name='{identifier}']")
+
+ def waitKeyLoaded(identifier, enabled):
+ b.wait_visible(f"tr[data-name='{identifier}'] input[type=checkbox]" + (":checked" if enabled else ":not(checked)"))
+ b.wait_visible(f"tr[data-name='{identifier}'][data-loaded={'true' if enabled else 'false'}]")
+
+ def toggleKeyState(identifier):
+ b.click(f"tr[data-name='{identifier}'] .pf-v5-c-switch__input")
+
+ def selectTab(identifier, title):
+ b.click(f"tr[data-name='{identifier}'] + tr li > button:contains('{title}')")
+
+ def waitTabActive(identifier, title):
+ b.wait_visible(f"tr[data-name='{identifier}'] + tr li.pf-m-current > button:contains('{title}')")
+
+ def waitKeyRowExpanded(identifier, expanded):
+ if expanded:
+ b.wait_visible(f"tr[data-name='{identifier}'] + tr .ct-listing-panel-body:not([hidden])")
+ else:
+ b.wait_not_visible(f"tr[data-name='{identifier}'] + tr")
+
+ def waitKeyDetail(identifier, dtype, value):
+ b.wait_in_text(f"tr[data-name='{identifier}'] + tr dt:contains({dtype}) + dd > div", value)
+
+ def getKeyDetail(identifier, dtype):
+ return b.text(f"tr[data-name='{identifier}'] + tr dt:contains({dtype}) + dd > div")
+
+ # Operating systems where auto loading doesn't work
+ auto_load = not m.ostree_image
+
+ if m.image == "arch":
+ self.write_file("/etc/pam.d/cockpit", """
+auth optional pam_ssh_add.so
+session optional pam_ssh_add.so
+""", append=True)
+
+ self.restore_dir("/home/admin")
+
+ # Put all the keys in place
+ m.execute("mkdir -p /home/admin/.ssh")
+ m.upload([
+ "verify/files/ssh/id_rsa",
+ "verify/files/ssh/id_rsa.pub",
+ "verify/files/ssh/id_ed25519",
+ "verify/files/ssh/id_ed25519.pub"
+ ], "/home/admin/.ssh/")
+ m.execute("chmod 600 /home/admin/.ssh/*")
+ m.execute("chown -R admin:admin /home/admin/.ssh")
+
+ self.login_and_go()
+
+ id_rsa = "2048 SHA256:SRvBhCmkCEVnJ6ascVH0AoVEbS3nPbowZkNevJnXtgw"
+ id_ed25519 = "256 SHA256:Wd028KYmG3OVLp7dBmdx0gMR7VcarJVIfaTtKqYCmak"
+
+ keys = list_keys()
+ if auto_load:
+ self.assertIn(id_rsa, keys)
+ self.assertNotIn(id_ed25519, keys)
+
+ b.open_session_menu()
+ b.click("#sshkeys")
+
+ # Automatically loaded
+ waitKeyLoaded('id_rsa', enabled=auto_load)
+ toggleExpandedStateKey('id_rsa')
+ waitKeyRowExpanded('id_rsa', expanded=True)
+ waitTabActive('id_rsa', 'Details')
+ waitKeyDetail('id_rsa', 'Comment', "test@test")
+ waitKeyDetail('id_rsa', 'Type', "RSA")
+ text = b.text("tr[data-name=id_rsa] + tr dt:contains(Fingerprint) + dd > div")
+ self.assertEqual(text, id_rsa[5:])
+
+ selectTab('id_rsa', 'Public key')
+ b.wait_val("tr[data-name=id_rsa] + tr .pf-v5-c-clipboard-copy input",
+ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDG4iipTovcMg0xn+089QLNKVGpP"
+ "2Pgq2duxHgAXre2XgA3dZL+kooioGFwBQSEjbWssKy82hKIN/W82/lQtL6krf7JQW"
+ "nT3LZwD5DPsvHFKhOLghbiFzSI0uEL4NFFcZOMo5tGLrM5LsZsaIkkv5QkAE0tHIy"
+ "eYinK6dQ2d8ZsfmgqxHDUQUWnz1T75X9fWQsUugSWI+8xAe0cfa4qZRz/IC+K7DEB"
+ "3x4Ot5pl8FBuydJj/gb+Lwo2Vs27/d87W/0KHCqOHNwaVC8RBb1WcmXRDDetLGH1A"
+ "9m5x7Ip/KU/cyvWWxw8S4VKZkTIcrGUhFYJDnjtE3Axz+D7agtps41t test@test")
+ toggleExpandedStateKey('id_rsa')
+ waitKeyRowExpanded('id_rsa', expanded=False)
+
+ # Load the id_ed25519 key
+ waitKeyLoaded('id_ed25519', enabled=False)
+ toggleKeyState('id_ed25519')
+ b.set_input_text("#id_ed25519-password", "locked")
+ b.click("#id_ed25519-unlock")
+ waitKeyLoaded('id_ed25519', enabled=True)
+
+ # Both keys are now loaded
+ keys = list_keys()
+ if auto_load:
+ self.assertIn(id_rsa, keys)
+ self.assertIn(id_ed25519, keys)
+
+ # Unload the RSA key
+ if auto_load:
+ toggleKeyState('id_rsa')
+ waitKeyLoaded('id_rsa', enabled=False)
+
+ # Only DSA keys now loaded
+ keys = list_keys()
+ self.assertIn(id_ed25519, keys)
+ self.assertNotIn(id_rsa, keys)
+
+ # Change password of DSA key
+ toggleExpandedStateKey('id_ed25519')
+ selectTab('id_ed25519', 'Password')
+ b.set_input_text("#id_ed25519-old-password", "locked")
+ b.set_input_text("#id_ed25519-new-password", "foobar")
+ b.set_input_text("#id_ed25519-confirm-password", "foobar")
+ b.assert_pixels("#credentials-modal", "ssh-keys-dialog")
+
+ b.click("#id_ed25519-change-password")
+ b.wait_visible('#credentials-modal .pf-v5-c-helper-text__item.pf-m-success')
+
+ # Log off and log back in, and we should have both loaded automatically
+ if auto_load:
+ b.logout()
+ b.login_and_go()
+ keys = list_keys()
+ self.assertIn(id_rsa, keys)
+ self.assertIn(id_ed25519, keys)
+
+ b.open_session_menu()
+ b.click("#sshkeys")
+ b.wait_visible("#credentials-modal")
+
+ # Add bad keys
+ # generate a new key
+ m.execute("ssh-keygen -t rsa -N '' -f /tmp/new.rsa")
+ self.addCleanup(m.execute, "rm -f /tmp/new.rsa*")
+ m.execute("chown admin:admin /tmp/new.rsa")
+ new_pk = m.execute("cat /tmp/new.rsa.pub").strip().split()[0]
+ m.execute("rm /tmp/new.rsa.pub")
+
+ waitKeyPresent('id_rsa', present=True)
+
+ b.wait_visible("#credential-keys")
+ b.wait_not_present("#ssh-file-add")
+ b.click("#ssh-file-add-custom")
+ b.set_file_autocomplete_val("#ssh-file-add-key", "/bad")
+ b.click("#ssh-file-add")
+ b.wait_text("#credentials-modal .pf-m-error > .pf-v5-c-helper-text__item-text", "Not a valid private key")
+
+ b.click("#credentials-modal .pf-v5-c-select__toggle-typeahead")
+ b.set_input_text("#credentials-modal .pf-v5-c-select__toggle-typeahead input", "/var/test/")
+ b.wait_in_text("#credentials-modal .pf-v5-c-select__menu-item.pf-m-disabled", "No such file or directory")
+ b.focus("#credentials-modal .pf-v5-c-select__toggle-typeahead input")
+ b.key_press("\b\b\b\b\b")
+ b.wait_visible("#credentials-modal .pf-v5-c-select__menu-item.directory:contains('/var/lib/')")
+ b.click("#credentials-modal .pf-v5-c-select__toggle-clear")
+ b.wait_val("#credentials-modal .pf-v5-c-select__toggle-typeahead input", "")
+
+ b.set_file_autocomplete_val("#ssh-file-add-key", "/tmp/new.rsa")
+ b.click("#ssh-file-add")
+ b.wait_not_present("#ssh-file-add")
+ # OpenSSH 7.8 and up has a new default key format where
+ # keys are marked as "agent_only", thereby limiting functionality
+ keys = list_keys()
+ keys_length = 3 if auto_load else 2
+ # Keys are like: 256 SHA256:x6S6fxMuEyqhpwNRAIK7ms6bZDY6xK9wzdDr2kCaWVY id_ed25519 (ED25519)
+ # We need the id_ed25519 part, (name or comment)
+ new_key = keys.splitlines()[keys_length - 1].split(' ')[2]
+ toggleKeyState(new_key)
+
+ toggleExpandedStateKey(new_key)
+ waitKeyRowExpanded(new_key, expanded=True)
+ waitTabActive(new_key, 'Details')
+ waitKeyDetail(new_key, 'Type', 'RSA')
+ self.assertNotEqual(getKeyDetail(new_key, 'Fingerprint'), "")
+ selectTab(new_key, 'Public key')
+ self.assertTrue(b.val(f"tr[data-name='{new_key}'] + tr .pf-v5-c-clipboard-copy input").startswith(new_pk))
+
+ # OpenSSH 7.8 and up has a new default key format where
+ # keys are marked as "agent_only", thereby limiting functionality
+ # "agent_only" keys cannot be turned off, or have their passwords changed
+ waitKeyLoaded(new_key, enabled=True)
+
+ # Test adding key with passphrase
+ m.execute("ssh-keygen -t rsa -N 'foobar' -f /tmp/new_with_passphrase.rsa")
+ self.addCleanup(m.execute, "rm -f /tmp/new_with_passphrase.rsa*")
+ m.execute("chown admin:admin /tmp/new_with_passphrase.rsa")
+ m.execute("rm /tmp/new_with_passphrase.rsa.pub")
+
+ b.wait_not_present("#ssh-file-add")
+ b.click("#ssh-file-add-custom")
+ b.set_file_autocomplete_val("#ssh-file-add-key", "/tmp/new_with_passphrase.rsa")
+ b.click("#ssh-file-add")
+ b.wait_visible("h1:contains('Unlock key /tmp/new_with_passphrase.rsa')")
+ b.set_input_text("#\\/tmp\\/new_with_passphrase\\.rsa-password", "foobar")
+ b.click("button:contains('Unlock')")
+ b.wait_not_present("h1:contains('Unlock key /tmp/new_with_passphrase.rsa')")
+ b.wait_not_present("#ssh-file-add")
+ b.wait_js_func("ph_count_check", "#credential-keys tbody", 4)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-shell-menu b/test/verify/check-shell-menu
new file mode 100755
index 0000000..ce673a5
--- /dev/null
+++ b/test/verify/check-shell-menu
@@ -0,0 +1,160 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import time
+
+import testlib
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestMenu(testlib.MachineCase):
+
+ def testDarkThemeSwitcher(self):
+ b = self.browser
+
+ def switch_style(style_class, prevPage="system"):
+ b.switch_to_top()
+ b.click("#toggle-menu")
+ b.click(f"#topnav {style_class}")
+ b.enter_page(f"/{prevPage}")
+
+ self.login_and_go("/system")
+ switch_style("#dark")
+ b._wait_present("html.pf-v5-theme-dark")
+
+ switch_style("#light")
+ b.wait_not_present("html.pf-v5-theme-dark")
+
+ # Test overriding, switching only works on Chromium
+ if b.cdp.browser.name == "chromium":
+ # Light theme overrides browser defaults
+ b._set_emulated_media_theme("dark")
+ b.wait_not_present("html.pf-v5-theme-dark")
+
+ switch_style("#auto")
+ b._wait_present("html.pf-v5-theme-dark")
+
+ b._set_emulated_media_theme("light")
+ b.wait_not_present("html.pf-v5-theme-dark")
+
+ switch_style("#dark")
+ b._wait_present("html.pf-v5-theme-dark")
+
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ # Add a link with a hash in it to test that this works
+ m.execute("mkdir -p /usr/local/share/cockpit/systemd; cp -rp /usr/share/cockpit/systemd/* /usr/local/share/cockpit/systemd")
+ m.execute(
+ """sed -i '/"menu"/a "memory": { "label": "Memory", "path": "#/memory" },' /usr/local/share/cockpit/systemd/manifest.json""")
+ self.addCleanup(m.execute, "rm -r /usr/local/share/cockpit")
+
+ self.login_and_go("/system")
+
+ b.switch_to_top()
+ b.click('#toggle-docs')
+ b.click('button:contains("About Web Console")')
+ b.wait_visible('#about-cockpit-modal:contains("Cockpit is an interactive Linux server admin interface")')
+ if not m.ostree_image:
+ pkgname = "cockpit" if m.image == "arch" else "cockpit-bridge"
+ b.wait_visible(f'#about-cockpit-modal:contains("{pkgname}")')
+ b.click('.pf-v5-c-about-modal-box__close button')
+ b.wait_not_present('#about-cockpit-modal')
+
+ # Clicking inside the iframed pages should close the docs menu
+ b.click("#toggle-docs")
+ b.wait_visible("#toggle-docs + ul")
+ b.enter_page("/system")
+ b.focus("#overview main")
+ b.switch_to_top()
+ b.wait_not_present("#toggle-docs + ul")
+
+ # Check that we can use a link with a hash in it
+ b.click_system_menu("/system/#/memory")
+
+ # Ensure that our tests pick up unhandled JS exceptions
+ b.switch_to_top()
+ b.go("/playground/exception")
+
+ # Test that subpages are correctly shown in the navigation (twice - once that only one page is shown as active)
+ b.wait_in_text("#host-apps .pf-m-current", "Development")
+
+ b.enter_page("/playground/exception")
+ b.wait_visible("button")
+ with self.assertRaisesRegex(RuntimeError, "TypeError:.*undefined"):
+ b.click("button")
+ # Some round trips, one of which should update the deferred exception
+ for _ in range(5):
+ b.wait_visible("button")
+ time.sleep(2)
+
+ # UI should also show the crash
+ b.switch_to_top()
+ b.wait_visible("#navbar-oops")
+
+ # normally called at the end of the test, should fail due to the oops
+ with self.assertRaisesRegex(AssertionError, "Cockpit shows an Oops"):
+ self.check_browser_errors()
+
+ # don't actually fail this test
+ b.allow_oops = True
+
+ def testSessionTimeout(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("printf '[Session]\nIdleTimeout = 1\n' >> /etc/cockpit/cockpit.conf")
+
+ # does not time out immediately
+ self.login_and_go()
+ time.sleep(20)
+ self.assertFalse(b.is_present("#session-timeout-modal"))
+ b.wait_visible("#hosts-sel")
+
+ # a mouse event resets the timer
+ b.enter_page("/system")
+ b.mouse("#system_information_hardware_text", "mousemove", 24, 24)
+ b.switch_to_top()
+
+ # 30s before the 1 min timeout the dialog pops up
+ time.sleep(35)
+ with b.wait_timeout(3):
+ b.wait_visible("#session-timeout-modal")
+ self.assertGreater(int(b.text("#session-timeout-modal .pf-v5-c-modal-box__body").split()[-2]), 15)
+ # click on "Continue session"
+ b.click("#session-timeout-modal footer button")
+ b.wait_not_present("#session-timeout-modal")
+
+ # now wait for timeout dialog again, but don't click; instead, wait for the full minute
+ time.sleep(30)
+ with b.wait_timeout(8):
+ b.wait_popup("session-timeout-modal")
+ self.assertGreater(int(b.text("#session-timeout-modal .pf-v5-c-modal-box__body").split()[-2]), 20)
+
+ time.sleep(30)
+ # that logs you out
+ b.wait_visible("#login")
+ b.wait_visible("#login-info-message")
+ b.wait_text("#login-info-message", "You have been logged out due to inactivity.")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-shell-multi-machine b/test/verify/check-shell-multi-machine
new file mode 100755
index 0000000..c79be85
--- /dev/null
+++ b/test/verify/check-shell-multi-machine
@@ -0,0 +1,838 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import time
+
+import testlib
+
+
+def break_hostkey(m, address):
+ filename = "/home/admin/.ssh/known_hosts"
+
+ m.execute(f'su admin -c "mkdir -p -m 700 `dirname {filename}`"')
+ key = f"{address} ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJqfgO2FPiix1n2sCJCXbaffwog1Vvi3zRdmcAxG//5T"
+ m.write(filename, key, owner="admin:admin")
+
+
+def fix_hostkey(m, key=None):
+ filename = "/home/admin/.ssh/known_hosts"
+ m.execute(f'su admin -c "mkdir -p -m 700 `dirname {filename}`"')
+ m.write(filename, key or '', owner="admin:admin")
+
+
+def break_bridge(m):
+ # we really want to get a "not found" in the shell, not a "permission denied" (which we would get with a
+ # non-executable present file or link)
+ m.execute("""
+ mkdir -p /tmp/overlay /tmp/work
+ mount -t overlay overlay -o lowerdir=/usr/bin,upperdir=/tmp/overlay,workdir=/tmp/work /usr/bin
+ rm /usr/bin/cockpit-bridge
+ """)
+
+
+def fix_bridge(m):
+ # umount lives in /usr, so needs a little dance for the EBUSY
+ m.execute("""
+ umount -l /usr/bin
+ while mountpoint -q /usr/bin; do sleep 0.5; done
+ rm -rf /tmp/overlay /tmp/work /tmp/umount
+ """)
+
+
+def check_failed_state(b, expected_title):
+ b.wait_in_text('#hosts_setup_server_dialog h1', expected_title)
+ b.click("#hosts_setup_server_dialog button:contains(Close)")
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+
+def fail_login(b):
+ b.click('#hosts_setup_server_dialog button:contains(Log in)')
+ b.wait_visible('#hosts_setup_server_dialog button:contains(Log in):not([disabled])')
+ b.wait_in_text("#hosts_setup_server_dialog .pf-v5-c-alert", "Login failed")
+
+
+def kill_user_admin(machine):
+ machine.execute("loginctl terminate-user admin")
+
+
+def change_ssh_port(machine, address, port=None, timeout_sec=120):
+ try:
+ port = int(port)
+ except (ValueError, TypeError):
+ port = 22
+
+ # Keep in mind that not all operating systems have firewalld
+ machine.execute(f"firewall-cmd --permanent --zone=public --add-port={port}/tcp || true")
+ machine.execute("firewall-cmd --reload || true")
+ if machine.ostree_image: # no semanage
+ machine.execute("setenforce 0")
+ else:
+ machine.execute(f"! selinuxenabled || semanage port -a -t ssh_port_t -p tcp {port}")
+ if machine.image in ["ubuntu-stable"]: # always socket activated
+ machine.write("/etc/systemd/system/ssh.socket.d/override.conf",
+ f"[Socket]\nListenStream=\nListenStream=127.27.0.15:22\nListenStream={address}:{port}")
+ machine.execute("systemctl daemon-reload")
+ machine.execute("systemctl restart ssh.socket")
+ else:
+ machine.execute("sed -i 's/.*Port .*/#\\0/' /etc/ssh/sshd_config")
+ machine.execute(
+ f"printf 'ListenAddress 127.27.0.15:22\nListenAddress {address}:{port}\n' >> /etc/ssh/sshd_config")
+ # We stop the sshd.socket unit and just go with a regular
+ # daemon. This is more portable and reloading/restarting the
+ # socket doesn't seem to work well.
+ #
+ machine.execute("( ! systemctl is-active sshd.socket || systemctl stop sshd.socket) && systemctl restart sshd.service")
+
+ start_time = time.time()
+ error = None
+ while (time.time() - start_time) < timeout_sec:
+ try:
+ machine.execute(
+ f"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o CheckHostIP=no -o PasswordAuthentication=no -p {port} {address} 2>&1 | grep -q 'Permission denied'", quiet=True)
+ return
+ except Exception as e:
+ error = e
+ time.sleep(0.5)
+ raise error
+
+
+@testlib.skipDistroPackage()
+@testlib.todoPybridgeRHEL8()
+class TestMultiMachineAdd(testlib.MachineCase):
+ provision = {
+ "machine1": {"address": "10.111.113.1/20", "memory_mb": 660},
+ "machine2": {"address": "10.111.113.2/20", "memory_mb": 660},
+ "machine3": {"address": "10.111.113.3/20", "memory_mb": 660},
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.machine2 = self.machines['machine2']
+ self.machine3 = self.machines['machine3']
+
+ # Disable preloading on all machines
+ # Preloading on machines debug build can overload the browser and cause slowness and browser crashes,
+ # and failing to load sofware updates breaks pixel tests in release builds
+ self.setup_provisioned_hosts(disable_preload=True)
+ self.setup_ssh_auth()
+
+ def testBasic(self):
+ b = self.browser
+ m2 = self.machine2
+ m3 = self.machine3
+ m3_host = "10.111.113.3:2222"
+ change_ssh_port(m3, "10.111.113.3", 2222)
+
+ hostname_selector = "#system_information_hostname_text"
+
+ self.login_and_go(None)
+ self.add_machine("10.111.113.2", password=None)
+ self.add_machine(m3_host, password=None)
+
+ b.switch_to_top()
+ b.click("#hosts-sel button")
+
+ kill_user_admin(m2)
+ with b.wait_timeout(30):
+ b.wait_visible("#machine2-error")
+
+ kill_user_admin(m3)
+ with b.wait_timeout(30):
+ b.wait_visible("#machine3-error")
+
+ # Navigating reconnects
+ b.click("a[href='/@10.111.113.2']")
+
+ b.wait_js_cond('window.location.pathname == "/@10.111.113.2/system"')
+ b.enter_page("/system", host="10.111.113.2")
+ b.wait_text(hostname_selector, "machine2")
+
+ b.switch_to_top()
+ b.click("#hosts-sel button")
+ b.wait_visible("a[href='/@10.111.113.2']")
+ b.wait_not_present("#machine2-error")
+
+ b.click("a[href='/@10.111.113.3']")
+
+ b.wait_js_cond('window.location.pathname == "/@10.111.113.3/system"')
+ b.enter_page("/system", host=m3_host)
+ b.wait_text(hostname_selector, "machine3")
+
+ b.switch_to_top()
+ b.click("#hosts-sel button")
+ b.wait_visible("a[href='/@10.111.113.3']")
+ b.wait_not_present("#machine3-error")
+
+ self.allow_restart_journal_messages()
+ self.allow_hostkey_messages()
+ # Might happen when killing the bridge.
+ self.allow_journal_messages("localhost: dropping message while waiting for child to exit",
+ "Received message for unknown channel: .*",
+ '.*: Socket error: disconnected',
+ ".*: error reading from ssh",
+ ".*: bridge failed: .*",
+ ".*: bridge program failed: Child process exited with code .*")
+
+ def testGlobalSSHConfig(self):
+ b = self.browser
+ m = self.machine
+ m3 = self.machine3
+
+ change_ssh_port(m3, "10.111.113.3", 2222)
+ m.write("/etc/ssh/ssh_config", "Host m2\n\tHostName 10.111.113.2\n", append=True)
+ m.write("/etc/ssh/ssh_config", "Host m3\n\tHostName 10.111.113.3\n\tPort 2222\n", append=True)
+
+ self.login_and_go(None)
+ self.add_machine("m2", password=None)
+ self.add_machine("m3", password=None)
+
+ b.switch_to_top()
+ b.click("#hosts-sel button")
+ b.wait_visible("a[href='/@m2']")
+ b.wait_visible("a[href='/@m3']")
+ b.wait_not_present("#page-sidebar .nav-status")
+
+ self.allow_hostkey_messages()
+
+
+@testlib.skipDistroPackage()
+class TestMultiMachine(testlib.MachineCase):
+ provision = {
+ "machine1": {"address": "10.111.113.1/20", "memory_mb": 660},
+ "machine2": {"address": "10.111.113.2/20", "memory_mb": 660},
+ "machine3": {"address": "10.111.113.3/20", "memory_mb": 660},
+ }
+
+ def setUp(self):
+ super().setUp()
+
+ self.machine2 = self.machines['machine2']
+ self.machine3 = self.machines['machine3']
+ self.allow_journal_messages("sudo: unable to resolve host machine1: .*")
+
+ self.setup_provisioned_hosts(disable_preload=True)
+
+ def checkDirectLogin(self, root='/', known_host=False):
+ b = self.browser
+ m2 = self.machine2
+ m = self.machine
+
+ hostname_selector = "#system_information_hostname_text"
+
+ # Direct to machine2, new login
+ m2.execute("echo admin:alt-password | chpasswd")
+ b.switch_to_top()
+ b.open(f"{root}=10.111.113.2")
+ b.wait_visible("#login")
+ b.wait_visible("#server-name")
+ b.wait_not_visible("#badge")
+ b.wait_not_visible("#brand")
+ b.wait_in_text("#server-name", "10.111.113.2")
+ b.wait_val("#server-field", "10.111.113.2")
+ b.set_input_text("#login-user-input", "admin")
+ b.set_input_text("#login-password-input", "alt-password")
+ b.click('#login-button')
+ if not known_host:
+ b.wait_in_text("#hostkey-message-1", "10.111.113.2")
+ match = re.match(r'\((?:ssh-)?([^-]*).*\)', b.text("#hostkey-type"))
+ self.assertIsNotNone(match)
+ algo = match.groups()[0]
+ # This assumes that all fingerprints use SHA256.
+ line = m2.execute(f"ssh-keygen -l -E SHA256 -f /etc/ssh/ssh_host_{algo.lower()}_key.pub", quiet=True)
+ fp = line.split(" ")[1]
+ self.assertEqual(b.text('#hostkey-fingerprint'), fp)
+ b.click('#login-button')
+
+ b.enter_page("/system")
+ b.wait_in_text(hostname_selector, "machine2")
+ b.switch_to_top()
+
+ b.wait_js_cond(f'window.location.pathname == "{root}=10.111.113.2/system"')
+
+ b.click("#hosts-sel button")
+ b.wait_in_text(f"a[href='{root}=10.111.113.2/@localhost']", "machine2")
+ b.wait_not_present("a[href='/@10.111.113.2']")
+ b.logout()
+
+ # Bad host key
+ m.write("/etc/ssh/ssh_known_hosts", "10.111.113.2 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDgPMmTosSQ4NxMtq+aL2NKLC+W4I9/jbD1e74cnOKTW")
+ b.open(f"{root}=10.111.113.2")
+ b.wait_visible("#login")
+ b.set_input_text("#login-user-input", "admin")
+ b.set_input_text("#login-password-input", "alt-password")
+ b.click('#login-button')
+
+ b.wait_not_visible("#conversation-group")
+ b.wait_visible("#password-group")
+ b.wait_visible("#user-group")
+ b.wait_visible("#option-group")
+ b.wait_visible("#server-group")
+ b.wait_in_text("#login-error-message", "Hostkey does not match")
+
+ # Clear bad host key in /etc and set bad host key in
+ # localStorage.
+ m.write("/etc/ssh/ssh_known_hosts", "")
+ b.eval_js("""window.localStorage.setItem("known_hosts", '{"10.111.113.2":"BAD"}')""")
+ b.set_input_text("#login-user-input", "admin")
+ b.set_input_text("#login-password-input", "alt-password")
+ b.click('#login-button')
+ b.wait_visible("#hostkey-group")
+ b.wait_in_text("#hostkey-title", "10.111.113.2 key changed")
+ b.click('#login-button')
+
+ b.enter_page("/system")
+ b.wait_in_text(hostname_selector, "machine2")
+ b.logout()
+
+ # Clear localStorage and set correct host key in /etc
+ b.eval_js("""window.localStorage.setItem("known_hosts", '{}')""")
+ m.execute("ssh-keyscan 10.111.113.2 > /etc/ssh/ssh_known_hosts")
+ b.set_input_text("#login-user-input", "admin")
+ b.set_input_text("#login-password-input", "alt-password")
+ b.click('#login-button')
+ b.enter_page("/system")
+ b.wait_in_text(hostname_selector, "machine2")
+ b.logout()
+
+ login_options = '#show-other-login-options'
+
+ # Connect to bad machine
+ b.open(f"{root}other")
+ b.set_input_text("#login-user-input", "admin")
+ b.set_input_text("#login-password-input", "bad-password")
+ b.click(login_options)
+ b.wait_visible("#server-group")
+ b.set_input_text("#server-field", "bad")
+ b.click(login_options)
+ b.wait_not_visible("#server-group")
+ b.click('#login-button')
+ b.wait_visible("#server-group")
+ b.wait_in_text("#login-error-message", "Unable to connect")
+
+ # Might happen when we switch away.
+ self.allow_hostkey_messages()
+ self.allow_journal_messages(".* Failed to resolve hostname bad .*")
+
+ def testDirectLogin(self):
+ self.machine.start_cockpit()
+ self.checkDirectLogin('/')
+
+ @testlib.todoPybridgeRHEL8()
+ def testUrlRoot(self):
+ b = self.browser
+ m = self.machine
+
+ hostname_selector = "#system_information_hostname_text"
+
+ m.write("/etc/cockpit/cockpit.conf", "[WebService]\nUrlRoot = cockpit-new")
+ m.start_cockpit()
+
+ # Make sure normal urls don't work.
+ output = m.execute('curl -s -o /dev/null -w "%{http_code}" http://localhost:9090/cockpit/socket')
+ self.assertIn('404', output)
+
+ output = m.execute('curl -s -o /dev/null -w "%{http_code}" http://localhost:9090/cockpit/socket')
+ self.assertIn('404', output)
+
+ b.open("/cockpit-new/system")
+ b.wait_visible("#login")
+ b.set_input_text("#login-user-input", "admin")
+ b.set_input_text("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.enter_page("/system")
+ b.switch_to_top()
+ b.wait_js_cond('window.location.pathname == "/cockpit-new/system"')
+
+ # Test 2nd machine
+ self.add_machine("10.111.113.2")
+ b.enter_page("/system", host="10.111.113.2")
+ b.wait_text(hostname_selector, "machine2")
+ b.switch_to_top()
+ b.wait_js_cond('window.location.pathname == "/cockpit-new/@10.111.113.2/system"')
+
+ # Test subnav
+ b.click_system_menu("/cockpit-new/@10.111.113.2/users", enter=False)
+ b.enter_page("/users", host="10.111.113.2")
+ b.click('#accounts-list td[data-label="Username"] a[href="#/admin"]')
+ b.wait_text("#account-user-name", "admin")
+ b.switch_to_top()
+ b.wait_js_cond('window.location.pathname == "/cockpit-new/@10.111.113.2/users"')
+ b.wait_js_cond('window.location.hash == "#/admin"')
+
+ b.logout()
+ self.checkDirectLogin('/cockpit-new/')
+ self.allow_hostkey_messages()
+
+ def testUrlRootWithQuery(self):
+ b = self.browser
+ m = self.machine
+
+ m.write("/etc/cockpit/cockpit.conf", "[WebService]\nUrlRoot = cockpit-new")
+ m.start_cockpit()
+
+ b.open("/cockpit-new/system?access_token=XXXX")
+ b.wait_visible("#login")
+ b.set_input_text("#login-user-input", "admin")
+ b.set_input_text("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.enter_page("/system")
+ b.switch_to_top()
+ b.wait_js_cond('window.location.pathname == "/cockpit-new/system"')
+
+ @testlib.todoPybridgeRHEL8()
+ def testExternalPage(self):
+ b = self.browser
+ m1 = self.machine
+ m2 = self.machine2
+
+ # Modify the terminals to be different on the two machines.
+ for machine, name in zip((m1, m2), ('m1', 'm2'), strict=True):
+ # This page may be either compressed or uncompressed
+ machine.execute(f"""
+ FILENAME=/usr/share/cockpit/systemd/terminal.html*
+ cp $FILENAME /tmp
+ test -f /tmp/terminal.html || gzip -d /tmp/terminal.html.gz
+ sed -ie 's|</body>|magic-{name}-token</body>|' /tmp/terminal.html
+ gzip < /tmp/terminal.html > /tmp/terminal.html.gz
+ mount -o bind /tmp/"$(basename $FILENAME)" $FILENAME""")
+
+ self.login_and_go("/system")
+ self.add_machine("10.111.113.2")
+
+ b.leave_page()
+ b.go("/@10.111.113.2/system/terminal")
+ b.enter_page("/system/terminal", host="10.111.113.2")
+ b.wait_in_text("body", "magic-m2-token")
+
+ b.leave_page()
+ b.go("/@localhost/system/terminal")
+ b.enter_page("/system/terminal")
+ b.wait_in_text("body", "magic-m1-token")
+
+ self.allow_hostkey_messages()
+
+ @testlib.todoPybridgeRHEL8()
+ def testFrameNavigation(self):
+ b = self.browser
+ m2 = self.machine2
+
+ m2_path = "/@10.111.113.2/playground/test"
+
+ # Add a machine
+ self.login_and_go(None)
+ self.add_machine("10.111.113.2")
+
+ # Go to the path, remove the image
+ b.go(m2_path)
+ b.enter_page("/playground/test", "10.111.113.2")
+ b.click("img[src='hammer.gif']")
+ b.switch_to_top()
+
+ # kill admin, lock account
+ m2.execute('passwd -l admin')
+ kill_user_admin(m2)
+
+ with b.wait_timeout(30):
+ b.wait_text(".curtains-ct h1", "Not connected to host")
+ b.wait_text("#machine-reconnect", "Reconnect")
+
+ b.click("#hosts-sel button")
+ b.wait_visible("a[href='/@10.111.113.2']")
+ b.wait_visible("#machine2-error")
+ b.go("/system")
+ b.enter_page("/system")
+ b.wait_in_text("#system_information_hostname_text", "machine1")
+ b.switch_to_top()
+
+ # navigating there again will fail
+ b.go(m2_path)
+ with b.wait_timeout(30):
+ b.wait_text(".curtains-ct h1", "Not connected to host")
+ b.wait_text("#machine-troubleshoot", "Log in")
+
+ # wait for system to load
+ b.go("/system")
+ b.enter_page("/system")
+ b.wait_in_text("#system_information_hostname_text", "machine1")
+ b.switch_to_top()
+
+ # renable admin
+ m2.execute('passwd -u admin')
+
+ # path should reconnect at this point
+ b.reload()
+ b.go(m2_path)
+ with b.wait_timeout(30):
+ b.wait_text(".curtains-ct h1", "Not connected to host")
+ self.start_machine_troubleshoot(password="foobar")
+
+ b.enter_page("/playground/test", "10.111.113.2", reconnect=True)
+ # image is back because it page was reloaded after disconnection
+ b.wait_visible("img[src='hammer.gif']")
+ b.switch_to_top()
+
+ # Host shows it is up
+ b.click("#hosts-sel button")
+ b.wait_visible("a[href='/@10.111.113.2']")
+ b.wait_not_present("#page-sidebar .nav-status")
+
+ # Bad host also bounces
+ b.go("/@10.0.0.0/playground/test")
+ with b.wait_timeout(30):
+ b.wait_text(".curtains-ct h1", "Not connected to host")
+ self.assertEqual(b.text(".curtains-ct .pf-v5-c-empty-state__body"), "Cannot connect to an unknown host")
+
+ self.allow_hostkey_messages()
+ # Might happen when killing the bridge.
+ self.allow_journal_messages("localhost: dropping message while waiting for child to exit",
+ "Received message for unknown channel: .*",
+ '.*: Socket error: disconnected',
+ ".*: error reading from ssh",
+ ".*: bridge failed: .*",
+ ".*: bridge program failed: Child process exited with code .*",
+ "/playground/test.html: failed to retrieve resource: authentication-failed")
+
+ @testlib.todoPybridgeRHEL8()
+ def testFrameReload(self):
+ b = self.browser
+
+ frame = "cockpit1:10.111.113.2/playground/test"
+ m2_path = "/@10.111.113.2/playground/test"
+
+ # Add a machine
+ self.login_and_go(None)
+ self.add_machine("10.111.113.2")
+
+ b.switch_to_top()
+ b.go(m2_path)
+ b.enter_page("/playground/test", "10.111.113.2")
+
+ b.wait_text('#file-content', "0")
+ b.click("#modify-file")
+ b.wait_text('#file-content', "1")
+
+ # load the same page on m1
+ b.switch_to_top()
+ b.go("/@localhost/playground/test")
+ b.enter_page("/playground/test")
+ b.wait_text('#file-content', "0")
+
+ # go back to m2 and reload frame.
+ b.switch_to_top()
+ b.go(m2_path)
+ b.enter_page("/playground/test", "10.111.113.2")
+ b.wait_text('#file-content', "1")
+ b.switch_to_top()
+
+ b.eval_js('ph_set_attr("iframe[name=\'%s\']", "data-ready", null)' % frame)
+ b.eval_js('ph_set_attr("iframe[name=\'%s\']", "src", "../playground/test.html?i=1#/")' % frame)
+ b.wait_visible(f"iframe.container-frame[name='{frame}'][data-ready]")
+
+ b.enter_page("/playground/test", "10.111.113.2")
+
+ b.wait_text('#file-content', "1")
+
+ self.allow_hostkey_messages()
+
+ @testlib.todoPybridgeRHEL8()
+ def testTroubleshooting(self):
+ b = self.browser
+ m1 = self.machine
+ m2 = self.machine2
+
+ # Logging in as root is no longer allowed by default by sshd
+ m2.execute("sed -ri 's/#?PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config")
+ m2.execute("systemctl restart sshd")
+
+ machine_path = "/@10.111.113.2"
+
+ self.login_and_go(None)
+
+ # Troubleshoot while adding
+ b.go(machine_path)
+
+ # Bad hostkey
+ break_hostkey(m1, "10.111.113.2")
+ self.start_machine_troubleshoot(new=True, known_host=True, expect_closed_dialog=False)
+ b.wait_in_text('#hosts_setup_server_dialog', "10.111.113.2 key changed")
+ b.click("#hosts_setup_server_dialog button:contains(Cancel)")
+ b.wait_not_present('#hosts_setup_server_dialog')
+ fix_hostkey(m1)
+
+ # Host key path is correct
+ m1.execute("mkdir -p /home/admin/.ssh/")
+ break_hostkey(m1, "10.111.113.2")
+
+ self.start_machine_troubleshoot(new=True, known_host=True, expect_closed_dialog=False)
+ b.wait_in_text('#hosts_setup_server_dialog', "10.111.113.2 key changed")
+ b.click("#hosts_setup_server_dialog button:contains(Cancel)")
+ fix_hostkey(m1)
+
+ # Bad cockpit
+ break_bridge(m2)
+ self.start_machine_troubleshoot(new=True, password="foobar", expect_closed_dialog=False)
+ check_failed_state(b, "Cockpit is not installed")
+ fix_bridge(m2)
+
+ # Troubleshoot existing
+ # Properly add machine
+ fix_hostkey(m1)
+ self.add_machine("10.111.113.2")
+ b.logout()
+ b.wait_visible("#login")
+
+ # Bad cockpit
+ break_bridge(m2)
+ self.login_and_go(None)
+ b.go(machine_path)
+ with b.wait_timeout(240):
+ self.start_machine_troubleshoot(password="foobar", expect_closed_dialog=False)
+
+ check_failed_state(b, "Cockpit is not installed")
+ b.wait_visible("#machine-troubleshoot")
+ fix_bridge(m2)
+
+ # Clear host key
+ fix_hostkey(m1)
+ self.start_machine_troubleshoot(expect_closed_dialog=False)
+ b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
+
+ # show fingerprint validation
+ b.wait_not_visible("#hosts_setup_server_dialog .hostkey-verify-help-cmds")
+ b.click("#hosts_setup_server_dialog .pf-v5-c-expandable-section__toggle")
+ b.wait_visible("#hosts_setup_server_dialog .hostkey-verify-help-cmds")
+ # run validation command
+ cmd = b.val("#hosts_setup_server_dialog .hostkey-verify-help-cmds input")
+ self.assertIn("ssh-keyscan", cmd)
+ fingerprint = m2.execute(cmd).strip()
+ # there is some additional noise around it, like the OpenSSH version and host name
+ self.assertIn(b.val("#hosts_setup_server_dialog .hostkey-fingerprint input"), fingerprint)
+
+ b.click("#hosts_setup_server_dialog button:contains('Trust and add host')")
+ b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
+ b.set_input_text('#login-custom-password', "foobar")
+ # Submit the dialog with Enter instead of clicking the Login button, for variety
+ b.focus("#login-custom-password")
+ b.key_press("\r")
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ # Reconnect
+ b.wait_not_visible(".curtains-ct")
+ b.enter_page('/system', "10.111.113.2")
+
+ b.logout()
+ b.wait_visible("#login")
+
+ # Break auth
+ m2.execute("echo admin:alt-password | chpasswd")
+ self.login_and_go(None)
+ b.go(machine_path)
+
+ with b.wait_timeout(120):
+ b.wait_visible("#machine-troubleshoot")
+ self.start_machine_troubleshoot(expect_closed_dialog=False)
+ b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
+ b.set_input_text('#login-custom-password', "")
+ fail_login(b)
+
+ b.set_input_text("#login-custom-password", "bad")
+ fail_login(b)
+ b.set_input_text("#login-custom-password", "alt-password")
+ b.click(f'#hosts_setup_server_dialog {self.primary_btn_class}')
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ # Reconnect
+ b.wait_not_visible(".curtains-ct")
+ b.enter_page('/system', "10.111.113.2")
+ b.logout()
+ b.wait_visible("#login")
+
+ change_ssh_port(m2, "10.111.113.2", 2222)
+ m2.disconnect()
+ del self.machines["machine2"] # No more access to m2
+
+ self.login_and_go(None)
+ b.go(machine_path)
+ with b.wait_timeout(120):
+ b.wait_visible("#machine-troubleshoot")
+ self.start_machine_troubleshoot(expect_closed_dialog=False)
+ b.wait_in_text('#hosts_setup_server_dialog h1', "Could not contact")
+ b.set_input_text("#edit-machine-port", "2222")
+ b.click(f'#hosts_setup_server_dialog {self.primary_btn_class}')
+ if not self.is_pybridge():
+ # Using libssh's knownhosts api port is taken into account when verifying a host
+ # with python bridge, ssh(1) tracks known hosts by name/IP, not by port
+ b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
+ b.click("#hosts_setup_server_dialog button:contains('Trust and add host')")
+ b.wait_in_text('#hosts_setup_server_dialog h1', "Log in to")
+ b.set_input_text("#login-custom-password", "alt-password")
+ b.click(f'#hosts_setup_server_dialog {self.primary_btn_class}')
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ b.wait_not_visible(".curtains-ct")
+ b.enter_page('/system', "10.111.113.2:2222")
+ b.logout()
+
+ self.allow_hostkey_messages()
+ self.allow_journal_messages('.* couldn\'t connect: .*',
+ '.* failed to retrieve resource: invalid-hostkey',
+ '.* host key for server has changed to: .*',
+ '.* spawning remote bridge failed .*',
+ '.*: bridge failed: .*',
+ '.*: cockpit-bridge: command not found',
+ '.*: received truncated .*',
+ '.*: Socket error: disconnected',
+ '.*: host key for this server changed key type: .*',
+ '.*: server offered unsupported authentication methods: .*')
+
+ @testlib.skipImage("TODO: Broken on Arch Linux", "arch")
+ @testlib.todoPybridgeRHEL8()
+ def testSshKeySetup(self):
+ b = self.browser
+ m1 = self.machine
+ m2 = self.machine2
+
+ # Let's not use "admin" on the remote machine. Creating a
+ # dedicated user gives us a guaranteed clean slate and also
+ # tests more code paths.
+
+ m2.execute("useradd -m fred")
+ m2.execute("echo fred:foobar | chpasswd")
+
+ self.login_and_go(None)
+ b.go("/@10.111.113.2")
+ self.start_machine_troubleshoot(expect_closed_dialog=False)
+ b.wait_visible("#machine-troubleshoot")
+ b.click('#machine-troubleshoot')
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.wait_in_text('#hosts_setup_server_dialog', "new host")
+ b.set_input_text('#add-machine-user', "fred")
+ b.click('#hosts_setup_server_dialog button:contains(Add)')
+ b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
+ b.click("#hosts_setup_server_dialog button:contains('Trust and add host')")
+ b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
+
+ # There is no key yet. Create and authorize it.
+
+ m1.execute("! test -f /home/admin/.ssh/id_rsa")
+ m2.execute("! test -f /home/fred/.ssh/authorized_keys")
+
+ b.wait_in_text("#hosts_setup_server_dialog", "Create a new SSH key and authorize it")
+ b.set_input_text('#login-custom-password', "foobar")
+ b.set_checked("#login-setup-keys", val=True)
+ # Leave passphrase empty on Coreos, since it can't load keys into the agent
+ if not m1.ostree_image:
+ b.set_input_text('#hosts_setup_server_dialog #login-setup-new-key-password', "foobar")
+ b.set_input_text('#hosts_setup_server_dialog #login-setup-new-key-password2', "foobar")
+ b.click('#hosts_setup_server_dialog button:contains(Log in)')
+ with b.wait_timeout(30):
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ b.enter_page("/system", host="fred@10.111.113.2")
+ m1.execute("test -f /home/admin/.ssh/id_rsa; test -f /home/admin/.ssh/id_rsa.pub")
+ self.assertEqual(m1.execute("cat /home/admin/.ssh/id_rsa.pub"),
+ m2.execute("cat /home/fred/.ssh/authorized_keys"))
+
+ # Relogin. This should now work seamlessly.
+ b.relogin(None, wait_remote_session_machine=m1)
+ b.enter_page("/system", host="fred@10.111.113.2")
+
+ # De-authorize key and relogin, then re-authorize.
+ m2.execute("rm /home/fred/.ssh/authorized_keys")
+ b.relogin(None, wait_remote_session_machine=m1)
+ b.wait_visible("#machine-troubleshoot")
+ b.click('#machine-troubleshoot')
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
+ b.wait_in_text("#hosts_setup_server_dialog", "Authorize SSH key")
+ b.set_checked("#login-setup-keys", val=True)
+ b.set_input_text('#login-custom-password', "foobar")
+ b.click('#hosts_setup_server_dialog button:contains(Log in)')
+ b.wait_not_present('#hosts_setup_server_dialog')
+ b.enter_page("/system", host="fred@10.111.113.2")
+ self.assertEqual(m1.execute("cat /home/admin/.ssh/id_rsa.pub"),
+ m2.execute("cat /home/fred/.ssh/authorized_keys"))
+
+ # Put a 'better' passphrase on the key and relogin, then
+ # change the passphrase back to the login password
+ m1.execute("ssh-keygen -q -f /home/admin/.ssh/id_rsa -p -P foobar -N foobarfoo")
+ b.relogin(None, wait_remote_session_machine=m1)
+ b.wait_visible("#machine-troubleshoot")
+ b.click('#machine-troubleshoot')
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.wait_in_text('#hosts_setup_server_dialog', "The SSH key for logging in")
+ b.set_checked('#hosts_setup_server_dialog input[value=key]', val=True)
+ b.set_input_text('#hosts_setup_server_dialog #locked-identity-password', "foobarfoo")
+ b.set_checked("#login-setup-keys", val=True)
+ b.set_input_text('#hosts_setup_server_dialog #login-setup-new-key-password', "foobar")
+ b.set_input_text('#hosts_setup_server_dialog #login-setup-new-key-password2', "foobar")
+ b.click('#hosts_setup_server_dialog button:contains(Log in)')
+ b.wait_not_present('#hosts_setup_server_dialog')
+ b.enter_page("/system", host="fred@10.111.113.2")
+
+ # Relogin. This should now work seamlessly (except on fedora-coreos and rhel4edge
+ # which don't have pam-ssh-add in its PAM stack.)
+ if not m1.ostree_image:
+ b.relogin(None, wait_remote_session_machine=m1)
+ b.enter_page("/system", host="fred@10.111.113.2")
+
+ # The authorized_keys files should still only have a single key
+ self.assertEqual(m1.execute("cat /home/admin/.ssh/id_rsa.pub"),
+ m2.execute("cat /home/fred/.ssh/authorized_keys"))
+
+ self.allow_hostkey_messages()
+
+ @testlib.todoPybridgeRHEL8()
+ def testSshKeySetupCustom(self):
+ b = self.browser
+ m1 = self.machine
+ m2 = self.machine2
+
+ # This tests how the ssh key setup reacts to a already
+ # existing configuration involving a custom key with a
+ # passphrase.
+
+ m1.execute("d=/home/admin/.ssh; mkdir -p $d; chown admin:admin $d; chmod 700 $d")
+ m1.execute("ssh-keygen -f /home/admin/.ssh/id_local -t rsa -N 'foobar'")
+ m1.execute("chown admin:admin /home/admin/.ssh/id_local*")
+ m1.write("/home/admin/.ssh/config", "Host 10.111.113.2\n IdentityFile /home/admin/.ssh/id_local\n")
+ pubkey = self.machine.execute("cat /home/admin/.ssh/id_local.pub")
+
+ m2.execute("d=/home/admin/.ssh; mkdir -p $d; chown admin:admin $d; chmod 700 $d")
+ m2.write("/home/admin/.ssh/authorized_keys", pubkey)
+ m2.execute("chown admin:admin /home/admin/.ssh/authorized_keys")
+
+ self.login_and_go(None)
+ b.go("/@10.111.113.2")
+ b.wait_visible("#machine-troubleshoot")
+ b.click('#machine-troubleshoot')
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.click('#hosts_setup_server_dialog button:contains(Add)')
+ b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
+ b.click("#hosts_setup_server_dialog button:contains('Trust and add host')")
+ b.wait_in_text('#hosts_setup_server_dialog', "The SSH key")
+ b.wait_not_present('.password-change-advice')
+ b.wait_not_present('.login-setup-auto')
+
+ self.allow_hostkey_messages()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-shell-multi-machine-key b/test/verify/check-shell-multi-machine-key
new file mode 100755
index 0000000..3f1f459
--- /dev/null
+++ b/test/verify/check-shell-multi-machine-key
@@ -0,0 +1,310 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import subprocess
+
+import testlib
+
+
+def kill_user_admin(machine):
+ # logind from systemd 208 is buggy, so we use systemd directly if it fails
+ # https://bugs.freedesktop.org/show_bug.cgi?id=71092
+ machine.execute("loginctl terminate-user admin || systemctl kill user-1000.slice")
+
+
+def authorize_user(m, user, public_keys=None):
+ if public_keys is None:
+ public_keys = ["verify/files/ssh/id_rsa.pub"]
+ m.execute(f"mkdir -p /home/{user}/.ssh")
+ m.upload(public_keys, f"/home/{user}/.ssh/authorized_keys")
+ m.execute(f"chown -R {user}:{user} /home/{user}/.ssh/")
+ m.execute(f"chmod 600 /home/{user}/.ssh/authorized_keys")
+ m.execute(f"chmod 700 /home/{user}/.ssh")
+
+
+LOAD_KEYS = [
+ "id_rsa", # password: foobar
+ "id_ecdsa", # no password
+ "id_ed25519", # password: locked
+]
+
+KEY_IDS = [
+ "2048 SHA256:SRvBhCmkCEVnJ6ascVH0AoVEbS3nPbowZkNevJnXtgw /home/admin/.ssh/id_rsa (RSA)",
+ "256 SHA256:dyHF4jiKz6RolQqORIATqhbZ4kil5cyiMQWizbQWU8k /home/admin/.ssh/id_ecdsa (ECDSA)",
+ "256 SHA256:Wd028KYmG3OVLp7dBmdx0gMR7VcarJVIfaTtKqYCmak /home/admin/.ssh/id_ed25519 (ED25519)"
+]
+
+KEY_IDS_MD5 = [
+ "2048 93:40:9e:67:82:78:a8:99:89:39:d5:ba:e0:50:70:e1 /home/admin/.ssh/id_rsa (RSA)",
+ "256 bd:56:df:c3:ff:e4:1d:9d:f5:c4:b9:cc:64:00:d5:93 /home/admin/.ssh/id_ecdsa (ECDSA)",
+ "256 b5:80:1b:f5:98:89:2a:39:f3:78:b3:64:5c:64:33:17 /home/admin/.ssh/id_ed25519 (ED25519)",
+]
+
+
+@testlib.skipImage("TODO: ssh key check fails on Arch Linux", "arch")
+@testlib.skipDistroPackage()
+class TestMultiMachineKeyAuth(testlib.MachineCase):
+ provision = {
+ "machine1": {"address": "10.111.113.1/20", "memory_mb": 512},
+ "machine2": {"address": "10.111.113.2/20", "memory_mb": 512},
+ }
+
+ def load_key(self, name, password):
+ self.browser.switch_to_top()
+ self.browser.eval_js("loaded = false")
+ self.browser.eval_js(f"""
+ load = function (user) {{
+ const proc = cockpit.spawn([ 'ssh-add', '{name}' ], {{ pty: true, directory: user.home + '/.ssh' }});
+ proc.stream(data => {{
+ if (data.indexOf('passphrase') !== -1)
+ proc.input('{password}\\n', true);
+ console.log(data);
+ }})
+ .then(() => {{
+ loaded = true;
+ }})
+ .catch(ex => {{
+ console.error(JSON.stringify(ex));
+ }});
+ }}
+ """)
+ self.browser.eval_js("cockpit.user().done(load)")
+ self.browser.wait_js_cond('loaded === true')
+
+ def check_keys(self, keys_md5, keys):
+ def normalize(k):
+ return re.sub("/home/admin/\\.ssh/[^ ]*|test@test|ecdsa w/o comment", "", k)
+ self.assertIn(normalize(self.browser.eval_js("cockpit.spawn([ 'ssh-add', '-l' ])")),
+ [normalize("\n".join(keys_md5) + "\n"),
+ normalize("\n".join(keys) + "\n")])
+
+ def setUp(self):
+ super().setUp()
+ self.machine2 = self.machines['machine2']
+
+ # Add user
+ self.machine2.disconnect()
+ self.machine2.execute("useradd user -c User", direct=True)
+ self.machine2.execute(
+ "sed -i 's/.*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null || true)", direct=True)
+ # HACK - Increase MaxAuthTries because of
+ # https://bugs.libssh.org/T233 and because we have a lot of
+ # keys and cockpit-ssh tries them all, and many of them twice.
+ self.machine2.write("/etc/ssh/sshd_config", "MaxAuthTries 100", append=True)
+ self.machine2.execute(
+ "( ! systemctl is-active sshd.socket || systemctl stop sshd.socket) && systemctl restart sshd.service", direct=True)
+ self.machine2.wait_execute()
+
+ # Disable preloading on all machines ("machine1" is done in testlib.py)
+ # Preloading on machines with debug build can overload the browser and cause slowness and browser crashes
+ # In these tests we actually switch between machines in quick succession which can make things even worse
+ if self.is_devel_build():
+ self.disable_preload("packagekit", "playground", "systemd", machine=self.machine2)
+
+ # Possible workaround - ssh as `admin` and just do `m.execute()`
+ @testlib.skipBrowser("Firefox cannot do `cockpit.spawn`", "firefox")
+ @testlib.todoPybridgeRHEL8()
+ def testBasic(self):
+ b = self.browser
+ m1 = self.machine
+ m2 = self.machine2
+
+ # Load keys
+ m1.execute("mkdir -p /home/admin/.ssh")
+
+ m1.upload([f"verify/files/ssh/{k}" for k in LOAD_KEYS],
+ "/home/admin/.ssh/")
+ m1.upload([f"verify/files/ssh/{k}.pub" for k in LOAD_KEYS],
+ "/home/admin/.ssh/")
+ m1.execute("chmod 400 /home/admin/.ssh/*")
+ m1.execute("chown -R admin:admin /home/admin/.ssh")
+
+ self.login_and_go("/system")
+
+ try:
+ m1.execute("ps xa | grep ssh-agent | grep -v grep")
+ except subprocess.CalledProcessError:
+ self.fail("No running ssh-agent found")
+
+ # pam-ssh-add isn't used on OSTree
+ if m1.ostree_image:
+ self.load_key('id_rsa', 'foobar')
+ self.check_keys(["2048 93:40:9e:67:82:78:a8:99:89:39:d5:ba:e0:50:70:e1 id_rsa (RSA)"],
+ ["2048 SHA256:SRvBhCmkCEVnJ6ascVH0AoVEbS3nPbowZkNevJnXtgw id_rsa (RSA)"])
+ else:
+ # Check our keys were loaded.
+ self.load_key("id_ed25519", "locked")
+ self.check_keys(KEY_IDS_MD5, KEY_IDS)
+
+ # Add machine
+ b.switch_to_top()
+ b.go("/@10.111.113.2")
+ b.click('#machine-troubleshoot')
+ b.wait_visible('#hosts_setup_server_dialog')
+
+ b.wait_text(f'#hosts_setup_server_dialog {self.primary_btn_class}', "Add")
+ b.click(f'#hosts_setup_server_dialog {self.primary_btn_class}')
+ b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
+ b.click(f'#hosts_setup_server_dialog {self.primary_btn_class}')
+ b.wait_in_text('#hosts_setup_server_dialog h1', "Log in to")
+ b.wait_in_text('#hosts_setup_server_dialog', "accept password login")
+ b.click("#hosts_setup_server_dialog button:contains('Cancel')")
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ # add key
+ authorize_user(m2, "admin")
+
+ # Login
+ b.click('#machine-troubleshoot')
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.wait_text(f'#hosts_setup_server_dialog {self.primary_btn_class}', "Add")
+ b.click(f'#hosts_setup_server_dialog {self.primary_btn_class}')
+ b.wait_not_present('#hosts_setup_server_dialog')
+ b.enter_page("/system", host="10.111.113.2")
+
+ # Logout
+ b.logout()
+ b.wait_visible("#login")
+
+ # Make sure ssh-agent exits
+ testlib.wait(lambda: "ssh-agent" not in m1.execute("ps xa | grep ss[h]-agent || true"))
+
+ self.login_and_go("/system")
+ b.switch_to_top()
+ # pam-ssh-add isn't used on OSTree
+ if m1.ostree_image:
+ self.load_key('id_rsa', 'foobar')
+
+ b.go("/@10.111.113.2")
+ b.wait_visible("iframe.container-frame[name='cockpit1:10.111.113.2/system']")
+
+ # Change user
+ authorize_user(m2, "user")
+ m2.execute("rm /home/admin/.ssh/authorized_keys")
+
+ b.click("#hosts-sel button")
+ b.click("button:contains('Edit hosts')")
+ b.click("#nav-hosts .nav-item a[href='/@10.111.113.2'] + span button.nav-action.pf-m-secondary")
+
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.wait_visible('#add-machine-user')
+ self.assertEqual(b.val("#add-machine-user"), "")
+ b.set_input_text('#add-machine-user', 'user')
+ b.click("#hosts_setup_server_dialog .pf-m-primary")
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ # We now expect this iframe to disappear
+ b.wait_not_present("iframe.container-frame[name='cockpit1:10.111.113.2/system']")
+
+ # And then we expect it to be reloaded after clicking through
+ b.wait_visible("a[href='/@10.111.113.2']")
+ b.enter_page("/system", host="user@10.111.113.2")
+
+ self.allow_hostkey_messages()
+ # Might happen when killing the bridge.
+ self.allow_journal_messages("localhost: dropping message while waiting for child to exit",
+ "Received message for unknown channel: .*",
+ ".*: error reading from ssh",
+ ".*: bridge program failed: Child process exited with code .*",
+ # Since there is not password,
+ # reauthorize doesn't work on m2
+ "received authorize command for wrong user: user",
+ ".*: user admin reauthorization failed",
+ "Error executing command as another user: Not authorized",
+ "This incident has been reported.",
+ "sudo: a password is required")
+
+ # Possible workaround - ssh as `admin` and just do `m.execute()`
+ @testlib.skipBrowser("Firefox cannot do `cockpit.spawn`", "firefox")
+ @testlib.todoPybridgeRHEL8()
+ def testLockedIdentity(self):
+ b = self.browser
+ m1 = self.machine
+ m2 = self.machine2
+
+ # upload id_ed25519 (password: locked)
+ m1.write("/home/admin/.ssh/config", """
+Host 10.111.113.2
+ User user
+ IdentityFile /home/admin/.ssh/id_ed25519
+""")
+ m1.upload(["verify/files/ssh/id_ed25519", "verify/files/ssh/id_ed25519.pub",
+ "verify/files/ssh/id_rsa", "verify/files/ssh/id_rsa.pub"],
+ "/home/admin/.ssh/")
+ m1.execute("chmod 400 /home/admin/.ssh/*")
+ m1.execute("chown -R admin:admin /home/admin/.ssh")
+ authorize_user(m2, "user", ["verify/files/ssh/id_ed25519.pub"])
+
+ self.login_and_go("/system")
+ b.switch_to_top()
+
+ self.load_key('id_rsa', 'foobar')
+
+ b.click("#hosts-sel button")
+ b.click("button:contains('Add new host')")
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.set_input_text('#add-machine-address', "10.111.113.2")
+ b.click("#hosts_setup_server_dialog .pf-m-primary")
+ b.wait_in_text("#hosts_setup_server_dialog", "You are connecting to 10.111.113.2 for the first time.")
+ b.click("#hosts_setup_server_dialog .pf-m-primary")
+ b.wait_in_text("#hosts_setup_server_dialog", "/home/admin/.ssh/id_ed25519")
+ b.set_input_text("#locked-identity-password", "locked")
+ b.click("#hosts_setup_server_dialog .pf-m-primary")
+ b.wait_visible("a[href='/@10.111.113.2']")
+ self.allow_hostkey_messages()
+
+ @testlib.todoPybridgeRHEL8()
+ def testLockedDefaultIdentity(self):
+ b = self.browser
+ m1 = self.machine
+ m2 = self.machine2
+
+ # Upload id_rsa and change its password to something else so
+ # that it is not automatically loaded into the agent. id_rsa
+ # should be tried autoamtically without needing to configure
+ # it explicitly in ~/.ssh/config.
+
+ m1.execute("mkdir -p /home/admin/.ssh")
+ m1.upload(["verify/files/ssh/id_rsa", "verify/files/ssh/id_rsa.pub"],
+ "/home/admin/.ssh/")
+ m1.execute("chmod 400 /home/admin/.ssh/*")
+ m1.execute("chown -R admin:admin /home/admin/.ssh")
+ m1.execute("ssh-keygen -p -f /home/admin/.ssh/id_rsa -P foobar -N foobarfoo")
+ authorize_user(m2, "admin", ["verify/files/ssh/id_rsa.pub"])
+
+ self.login_and_go("/system")
+ b.switch_to_top()
+
+ b.click("#hosts-sel button")
+ b.click("button:contains('Add new host')")
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.set_input_text("#add-machine-address", "10.111.113.2")
+ b.click("#hosts_setup_server_dialog .pf-m-primary")
+ b.wait_in_text("#hosts_setup_server_dialog", "You are connecting to 10.111.113.2 for the first time.")
+ b.click("#hosts_setup_server_dialog .pf-m-primary")
+ b.wait_in_text("#hosts_setup_server_dialog", "/home/admin/.ssh/id_rsa")
+ b.set_input_text("#locked-identity-password", "foobarfoo")
+ b.click("#hosts_setup_server_dialog .pf-m-primary")
+ b.wait_visible("a[href='/@10.111.113.2']")
+ self.allow_hostkey_messages()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-shell-multi-os b/test/verify/check-shell-multi-os
new file mode 100755
index 0000000..08828b9
--- /dev/null
+++ b/test/verify/check-shell-multi-os
@@ -0,0 +1,247 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+
+def wait_addresses(b, expected):
+ b.wait_js_cond(f'ph_select("#nav-hosts .nav-item a").length == {len(expected)}')
+ for address in expected:
+ b.wait_visible(f"#nav-hosts .nav-item a[href='/@{address}']")
+
+
+def add_machine(b, address, password):
+ b.click("button:contains('Add new host')")
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.set_input_text('#add-machine-address', address)
+ b.click('#hosts_setup_server_dialog .pf-m-primary:contains("Add")')
+ b.wait_in_text('#hosts_setup_server_dialog', f"You are connecting to {address} for the first time.")
+ b.click('#hosts_setup_server_dialog .pf-m-primary')
+
+ if password:
+ b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
+ b.set_input_text('#login-custom-password', password)
+ b.click('#hosts_setup_server_dialog button:contains(Log in)')
+
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+
+def wait_stock_addresses(b, expected):
+ b.wait_js_func(
+ """(function(expected) {
+ var nodes = document.querySelectorAll('#dashboard-hosts .list-group-item');
+ var addresses = Array.prototype.map.call(nodes, function(e) {
+ return e.getAttribute("data-address");
+ });
+ return expected.sort().toString() == addresses.sort().toString();
+ })""", expected)
+
+
+def add_stock_machine(b, address, password):
+ b.click('#dashboard-add')
+ b.wait_popup('dashboard_setup_server_dialog')
+ b.set_input_text('#add-machine-address', address)
+
+ b.click('#dashboard_setup_server_dialog button:contains(Add)')
+ b.wait_in_text('#dashboard_setup_server_dialog', "Fingerprint")
+ b.click('#dashboard_setup_server_dialog button:contains(Connect)')
+ if password:
+ b.wait_in_text('#dashboard_setup_server_dialog', "Unable to log in")
+ b.set_input_text('#login-custom-password', password)
+ b.click('#dashboard_setup_server_dialog button:contains(Log in)')
+ b.wait_popdown('dashboard_setup_server_dialog')
+
+
+@testlib.skipDistroPackage()
+class TestCentos7(testlib.MachineCase):
+ provision = {
+ "0": {"address": "10.111.113.1/20", "memory_mb": 512},
+ "centos-7": {"address": "10.111.113.5/20", "image": "centos-7", "memory_mb": 512}
+ }
+
+ def check_spawn(self, b, address):
+ # HACK: Firefox cannot do `cockpit.spawn` as it returns promise
+ # Firefox can wait for promise to resolve, but then cannot get value from it
+ if b.cdp.browser.name == "firefox":
+ return
+ result = b.call_js_func("""(function(address) {
+ return cockpit.spawn(['echo', 'hi'], { host: address });
+ })""", address)
+ self.assertEqual(result, "hi\n")
+
+ def check_dbus(self, b, address):
+ # HACK: Firefox cannot do `cockpit.dbus.proxy.call` as it returns promise
+ if b.cdp.browser.name == "firefox":
+ return
+ b.call_js_func("""(function(address) {
+ return cockpit.dbus("org.freedesktop.DBus", { host: address })
+ .proxy("org.freedesktop.DBus", "/").call("GetId");
+ })""", address)
+
+ @testlib.todoPybridgeRHEL8()
+ def test(self):
+ dev_m = self.machine
+ dev_b = self.browser
+
+ self.allow_hostkey_messages()
+
+ self.login_and_go()
+ dev_b.click("#hosts-sel button")
+ dev_addresses = ["localhost"]
+ wait_addresses(dev_b, dev_addresses)
+
+ stock_m = self.machines['centos-7']
+ stock_m.execute("firewall-cmd --add-service cockpit")
+ stock_m.start_cockpit()
+
+ # Wait for connectivity between the two
+ testlib.wait(lambda: stock_m.execute("ip addr >&2 && ping -q -w5 -c5 10.111.113.1"))
+ testlib.wait(lambda: dev_m.execute("ip addr >&2 && ping -q -w5 -c5 10.111.113.5"))
+
+ stock_m.execute("hostnamectl set-hostname stock")
+ stock_b = self.new_browser(stock_m)
+
+ stock_b.login_and_go("/dashboard", legacy_authorized=True)
+ wait_stock_addresses(stock_b, ["localhost"])
+
+ add_stock_machine(stock_b, "10.111.113.1", password=None)
+ wait_stock_addresses(stock_b, ["localhost", "10.111.113.1"])
+
+ add_machine(dev_b, "10.111.113.5", password="foobar")
+ dev_addresses.append("10.111.113.5")
+ wait_addresses(dev_b, dev_addresses)
+
+ stock_b.switch_to_top()
+ self.check_dbus(stock_b, "10.111.113.1")
+ self.check_dbus(dev_b, "10.111.113.5")
+
+ self.check_spawn(stock_b, "10.111.113.1")
+ self.check_spawn(dev_b, "10.111.113.5")
+
+ dev_b.go("/@10.111.113.5/network")
+ dev_b.wait_visible("iframe.container-frame[name='cockpit1:10.111.113.5/network'][src$='/network/index.html#/']")
+
+ dev_b.go("/@10.111.113.5/storage")
+ dev_b.wait_visible("iframe.container-frame[name='cockpit1:10.111.113.5/storage'][src$='/storage/index.html#/']")
+
+ dev_b.switch_to_top()
+ dev_b.go("/@10.111.113.5/users")
+ dev_b.wait_visible("iframe.container-frame[name='cockpit1:10.111.113.5/users'][src$='/users/index.html#/']")
+
+ stock_b.go("/@10.111.113.1/system")
+ stock_b.wait_visible("iframe.container-frame[name='cockpit1:10.111.113.1/system'][data-loaded]")
+ stock_b.switch_to_frame('cockpit1:10.111.113.1/system')
+ stock_b.wait_text_not("#system_information_hardware_text", "")
+
+ # Messages from previous versions of cockpit
+ self.allow_journal_messages(".*pam_authenticate failed: Authentication failure")
+ self.allow_restart_journal_messages()
+
+
+@testlib.skipDistroPackage()
+class TestRHEL8(testlib.MachineCase):
+ provision = {
+ "0": {"address": "10.111.113.1/20", "memory_mb": 768},
+ "stock": {"address": "10.111.113.5/20", "image": "rhel-8-8", "memory_mb": 512}
+ }
+
+ @testlib.todoPybridgeRHEL8()
+ def test(self):
+ dev_m = self.machine
+ dev_b = self.browser
+
+ stock_m = self.machines['stock']
+ stock_m.execute("hostnamectl set-hostname stock")
+
+ # Wait for connectivity between the two
+ stock_m.execute("ping -q -w5 -c5 10.111.113.1")
+ dev_m.execute("ping -q -w5 -c5 10.111.113.5")
+
+ self.allow_hostkey_messages()
+
+ # New machine adding old machine
+
+ self.login_and_go()
+ dev_b.click("#hosts-sel button")
+ dev_addresses = ["localhost"]
+ wait_addresses(dev_b, dev_addresses)
+
+ add_machine(dev_b, "10.111.113.5", password="foobar")
+ dev_addresses.append("10.111.113.5")
+ wait_addresses(dev_b, dev_addresses)
+
+ dev_b.go("/@10.111.113.5/")
+ dev_b.wait_visible("iframe.container-frame[name='cockpit1:10.111.113.5/system'][data-loaded]")
+ dev_b.enter_page("/system", host="10.111.113.5")
+ dev_b.wait_in_text(".ct-overview-header-hostname", "stock")
+ dev_b.wait_in_text(".ct-overview-header-hostname", "running Red Hat Enterprise Linux 8")
+
+
+@testlib.skipDistroPackage()
+class TestMultiOSDirect(testlib.MachineCase):
+ provision = {
+ "0": {"address": "10.111.113.1/20", "memory_mb": 512},
+ "centos-7": {"address": "10.111.113.5/20", "image": "centos-7", "memory_mb": 512}
+ }
+
+ @testlib.todoPybridgeRHEL8()
+ def testCentos7Direct(self):
+ b = self.browser
+
+ self.allow_hostkey_messages()
+
+ self.login_and_go()
+ b.click("#hosts-sel button")
+
+ dev_addresses = ["localhost"]
+ wait_addresses(b, dev_addresses)
+
+ stock_m = self.machines['centos-7']
+ stock_m.execute("hostnamectl set-hostname stock")
+
+ add_machine(b, "10.111.113.5", password="foobar")
+ dev_addresses.append("10.111.113.5")
+ wait_addresses(b, dev_addresses)
+ b.logout()
+
+ # Access stock directly from dev
+ b.open("/=10.111.113.5")
+ b.wait_visible("#login")
+ b.wait_not_visible("#badge")
+ b.wait_not_visible("#brand")
+ b.set_input_text("#login-user-input", "admin")
+ b.set_input_text("#login-password-input", "foobar")
+ b.click('#login-button')
+ b.wait_visible("#hostkey-group")
+ b.wait_in_text("#hostkey-message-1", "You are connecting to 10.111.113.5 for the first time.")
+ b.click('#login-button')
+
+ b.wait_visible("iframe.container-frame[name='cockpit1:localhost/system'][data-loaded]")
+ b.wait_not_visible(".curtains-ct")
+ b.wait_visible("iframe.container-frame[name='cockpit1:localhost/system']")
+ b.switch_to_frame("cockpit1:localhost/system")
+ b.wait_visible("body")
+ b.wait_in_text('#system_information_hostname_button', "stock")
+ b.switch_to_top()
+
+ b.wait_js_cond('window.location.pathname == "/=10.111.113.5/system"')
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-sosreport b/test/verify/check-sosreport
new file mode 100755
index 0000000..4e6e502
--- /dev/null
+++ b/test/verify/check-sosreport
@@ -0,0 +1,191 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import glob
+import os.path
+import subprocess
+import tarfile
+
+import testlib
+
+
+@testlib.skipOstree("No sosreport")
+@testlib.skipImage("No sosreport", "arch")
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestSOS(testlib.MachineCase):
+
+ def testBasic(self, urlroot=""):
+ b = self.browser
+ m = self.machine
+
+ m.execute("rm -rf /var/tmp/*sos*")
+
+ self.write_file("/etc/sos/sos.conf",
+ """
+[global]
+threads=1
+
+[report]
+only-plugins=release,date,host,cgroups,networking
+""")
+
+ if urlroot != "":
+ m.write("/etc/cockpit/cockpit.conf", f"[WebService]\nUrlRoot={urlroot}")
+
+ self.login_and_go("/sosreport", urlroot=urlroot, superuser=False)
+
+ b.wait_in_text("#app", "Administrative access required")
+ if urlroot == "":
+ b.assert_pixels("#app", "limited")
+
+ b.become_superuser()
+
+ b.wait_in_text("#app", "No system reports.")
+ if urlroot == "":
+ b.assert_pixels("#app", "empty")
+
+ b.click("button:contains('Run report')")
+ b.wait_visible("#sos-dialog")
+ b.set_input_text("#sos-dialog .pf-v5-c-form__group:contains(Report label) input", "mylabel")
+ b.set_input_text("#sos-dialog .pf-v5-c-form__group:contains(Encryption passphrase) input", "foobar")
+ b.set_checked("#sos-dialog .pf-v5-c-check:contains(Obfuscate) input", val=True)
+ if urlroot == "":
+ b.assert_pixels("#sos-dialog", "dialog")
+ b.click("#sos-dialog button:contains(Run report)")
+ with b.wait_timeout(120):
+ b.wait_not_present("#sos-dialog")
+
+ b.wait_visible("tr:contains(mylabel) button:contains(Download)")
+ b.allow_download()
+
+ def downloaded_sosreports():
+ return glob.glob(os.path.join(b.cdp.download_dir, "*sosreport-*.xz.gpg"))
+
+ b.click("tr:contains(mylabel) button:contains(Download)")
+ # while the download is ongoing, it will have an *.xz.tmpsuffix name, gets renamed to *.xz when done
+ testlib.wait(lambda: len(downloaded_sosreports()) > 0)
+ report_gpg = downloaded_sosreports()[0]
+ base_report_gpg = os.path.basename(report_gpg)
+ report = report_gpg.replace(".gpg", "")
+
+ m.execute(f"test -f /var/tmp/{base_report_gpg}")
+
+ # Check that /etc/release was saved. It the files does not exist, getmember raises KeyError
+ # Sometimes it takes a bit of time until the file can be opened. Try it 3 times.
+ subprocess.call(["gpg", "--batch", "--yes", "--passphrase", "foobar",
+ "--output", report, "--decrypt", report_gpg])
+ with tarfile.open(report) as tar:
+ # the tarball contains a single subdirectory, get its name
+ names = tar.getnames()
+ topdir = names[0].split("/")[0]
+ tar.getmember(os.path.join(topdir, "etc/os-release"))
+
+ b.click("tr:contains(mylabel) button.pf-v5-c-dropdown__toggle")
+ b.click("tr:contains(mylabel) li:contains(Delete)")
+ b.click("#sos-remove-dialog button:contains(Delete)")
+ testlib.wait(lambda: m.execute(f"! test -f /var/tmp/{base_report_gpg} && echo yes"))
+
+ # error reporting
+ self.write_file("/usr/sbin/sos", """#!/bin/sh
+echo "EssOhEss is kaputt" >&2
+exit 1""", perm="755")
+ b.click("button:contains('Run report')")
+ b.wait_visible("#sos-dialog")
+ b.click("#sos-dialog button:contains(Run report)")
+ b.wait_in_text("#sos-dialog .pf-v5-c-alert",
+ "sos report failed" if self.is_pybridge() else "sos exited with code 1")
+ b.wait_in_text("#sos-dialog .pf-v5-c-alert", "EssOhEss is kaputt")
+ b.click("#sos-dialog button:contains(Cancel)")
+ b.wait_not_present("#sos-dialog")
+
+ self.allow_journal_messages('.*comm="sosreport".*')
+ self.allow_browser_errors('error: Failed to call sos report: {"problem":null,"exit_status":1,"exit_signal":null')
+
+ def testWithUrlRoot(self):
+ self.testBasic(urlroot="/webcon")
+
+ def testVerbose(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("rm -rf /var/tmp/*sos*")
+
+ self.write_file("/etc/sos/sos.conf",
+ """
+[global]
+threads=1
+
+[report]
+only-plugins=release,date,host,cgroups,networking
+""")
+
+ self.login_and_go("/sosreport")
+
+ b.wait_in_text("#app", "No system reports.")
+
+ b.click("button:contains('Run report')")
+ b.wait_visible("#sos-dialog")
+ b.set_input_text("#sos-dialog .pf-v5-c-form__group:contains(Report label) input", "mylabel")
+ b.set_checked("#sos-dialog .pf-v5-c-check:contains(Use verbose logging) input", val=True)
+ b.click("#sos-dialog button:contains(Run report)")
+ with b.wait_timeout(120):
+ b.wait_not_present("#sos-dialog")
+
+ # There should be one archive and it should contain a bunch of debug messages
+ self.assertEqual(m.execute("ls -l /var/tmp/sosreport*mylabel*.tar.xz | wc -l").strip(), "1")
+ self.assertGreater(int(m.execute("tar --wildcards -xaOf /var/tmp/sosreport*mylabel*.tar.xz '*/sos_logs/sos.log' | grep -c 'DEBUG: \\[plugin:release\\]'")), 5)
+
+ def testCancel(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute("rm -rf /var/tmp/*sos*")
+
+ self.login_and_go("/sosreport")
+ b.click("button:contains('Run report')")
+ b.click("#sos-dialog button:contains(Run report)")
+ m.execute("until pgrep -x sos >/dev/null; do sleep 1; done")
+
+ b.click("button:contains('Stop report')")
+ b.wait_not_present("#sos-dialog")
+ # cleans up properly; unfortunately closing the process is async, so need to retry a few times
+ m.execute("while pgrep -a -x sos; do sleep 1; done", timeout=10)
+ self.assertEqual(m.execute("ls /var/tmp/sosreport* 2>/dev/null || true"), "")
+
+ def testAppStream(self):
+ b = self.browser
+ m = self.machine
+
+ self.allow_journal_messages("invalid or unusable locale.*")
+ # chromium rpm has broken appstream data, which causes various parser errors
+ self.allow_journal_messages(".*xml.*")
+
+ self.login_and_go("/apps")
+ b.wait_not_present(".pf-v5-c-empty-state")
+ image_os = m.image.split('-')[0]
+ if image_os in ['fedora', 'debian', 'ubuntu']:
+ b.wait_visible(".app-list .pf-v5-c-data-list__item-row div[rowId='Diagnostic reports']")
+ b.wait_visible(".app-list .pf-v5-c-data-list__item-row div[rowId='Diagnostic reports'] button:contains('Remove')")
+ else:
+ b.wait_not_present(".app-list .pf-v5-c-data-list__item-row div[rowId='Diagnostic reports']")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-static-login b/test/verify/check-static-login
new file mode 100755
index 0000000..a1eee16
--- /dev/null
+++ b/test/verify/check-static-login
@@ -0,0 +1,956 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import base64
+import re
+import subprocess
+import time
+
+import testlib
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestLogin(testlib.MachineCase):
+ def check_shell(self):
+ b = self.browser
+ b.wait_visible("#content")
+ b.wait_text('#current-username', 'admin')
+ b.set_layout("mobile")
+ b.click("#hosts-sel button")
+ b.wait_in_text(".view-hosts .pf-m-current", "admin @")
+ b.click("#hosts-sel button")
+ b.set_layout("desktop")
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ # Setup users and passwords
+ m.execute("useradd user -c 'Barney Bär'")
+ m.execute("echo user:abcdefg | chpasswd")
+
+ admins_only_pam = """account sufficient pam_succeed_if.so uid = 0\\
+account required pam_succeed_if.so user ingroup %s""" % m.get_admin_group()
+
+ # Setup a special PAM config that disallows non-wheel users
+ def deny_non_root(remote_filename):
+ if m.image == "arch":
+ self.sed_file(f'1 a {admins_only_pam}', remote_filename)
+ else:
+ self.sed_file(f'/nologin/a {admins_only_pam}', remote_filename)
+
+ deny_non_root("/etc/pam.d/cockpit")
+ deny_non_root("/etc/pam.d/sshd")
+
+ m.start_cockpit()
+ b.open("/system")
+
+ # Test banner
+ # Test that we don't show banner when not specified
+ b.wait_visible("#login-user-input")
+ b.wait_not_visible("#banner")
+
+ if m.ostree_image:
+ m.execute("podman exec ws printf '[Session]\nBanner = /host/etc/issue\n' > /etc/cockpit/cockpit.conf")
+ else:
+ m.execute("printf '[Session]\nBanner = /etc/issue\n' > /etc/cockpit/cockpit.conf")
+ m.restart_cockpit()
+ b.reload()
+ b.wait_visible("#login-user-input")
+ b.wait_visible("#banner")
+ self.assertEqual(b.text("#banner-message"), m.execute("cat /etc/issue").rstrip())
+
+ # Test non existent file
+ m.execute("printf '[Session]\nBanner = /etc/non-existing-file\n' > /etc/cockpit/cockpit.conf")
+ self.allow_journal_messages("error loading contents of banner: Failed to open file “/etc/non-existing-file”: No such file or directory")
+ m.restart_cockpit()
+ b.reload()
+ b.wait_visible("#login-user-input")
+ b.wait_not_visible("#banner")
+
+ # Try to login as a non-existing user
+ b.try_login("nonexisting", "blahblah")
+ b.wait_text_not("#login-error-message", "")
+ self.assertNotIn("web", m.execute("who"))
+
+ # Try to login as user with a wrong password
+ b.try_login("user", "gfedcba")
+ b.wait_text_not("#login-error-message", "")
+ self.assertNotIn("web", m.execute("who"))
+ self.allow_journal_messages(".* user: Authentication failure.*")
+
+ # Try to login as user with correct password
+ b.try_login("user", "abcdefg")
+ if m.ostree_image:
+ b.wait_in_text("#login-error-message", "Server closed connection")
+ else:
+ b.wait_text("#login-error-message", "Permission denied")
+ self.assertNotIn("web", m.execute("who"))
+
+ # Try to login with disabled shell; this does not work on OSTree where
+ # we log in through ssh
+ if not m.ostree_image:
+ m.execute("usermod --shell /bin/false admin; sync")
+ b.reload()
+ b.try_login("admin", "foobar")
+ b.wait_text_not("#login-error-message", "")
+ m.execute("usermod --shell /bin/bash admin; sync")
+
+ # Login as admin
+ b.open("/system")
+ b.login_and_go()
+ self.check_shell()
+
+ if not m.ostree_image: # logs in via ssh, not cockpit-session
+ self.assertRegex(m.execute("who"), r"(^|\n)admin *web.*(\d+\.\d+|::)")
+
+ # reload, which should log us in with the cookie
+ b.reload()
+ self.check_shell()
+
+ if not m.ostree_image: # logs in via ssh, not cockpit-session
+ self.assertRegex(m.execute("who"), r"(^|\n)admin *web.*(\d+\.\d+|::)")
+
+ b.go("/users#/admin")
+ b.enter_page("/users")
+ b.wait_text("#account-user-name", "admin")
+ try:
+ m.execute("journalctl -p 7 SYSLOG_IDENTIFIER=cockpit-ws | grep 'cockpit-session: opening pam session'")
+ self.fail("cockpit-session debug messages found")
+ except subprocess.CalledProcessError:
+ pass
+
+ # Change login screen options
+ b.logout()
+ b.wait(lambda: "web console" not in m.execute("who"))
+ b.wait_visible("#option-group")
+ m.execute("printf '[WebService]\nLoginTo = false\n' > /etc/cockpit/cockpit.conf")
+ m.restart_cockpit()
+ b.open("/system")
+ b.wait_visible("#login")
+ b.wait_not_visible("#option-group")
+
+ # LoginTo= also disables direct URL
+ b.open("/=192.168.99.99/")
+ b.wait_visible("#login")
+ b.wait_not_visible("#option-group")
+ # logging in does not go via cockpit-ssh (which would cause a connection failure)
+ b.try_login()
+ # this isn't the most helpful error message, but this is essentially hacking
+ b.wait_text("#login-error-message", "Wrong user name or password")
+
+ # Default options be to display these options
+ m.execute("rm /etc/cockpit/cockpit.conf")
+ m.restart_cockpit()
+ b.open("/system")
+ b.wait_visible("#option-group")
+
+ # And now we remove cockpit-ssh which affects the default
+ if not m.ostree_image:
+ self.restore_file(f"{self.libexecdir}/cockpit-ssh")
+ m.execute(f"rm {self.libexecdir}/cockpit-ssh")
+ m.restart_cockpit()
+ b.open("/system")
+ b.wait_visible("#login")
+ b.wait_not_visible("#option-group")
+
+ # test login with tcsh
+ if not m.image.startswith("rhel") and not m.image.startswith("centos") and not m.image == "arch": # no tcsh in RHEL and in Arch Linux (TODO: available in [community])
+ try:
+ m.execute("sed -r -i.bak '/^admin:/ s_:[^:]+$_:/bin/tcsh_' /etc/passwd")
+ b.login_and_go()
+ b.enter_page('/system')
+ b.wait_visible('.system-information')
+ b.logout()
+ finally:
+ m.execute("mv /etc/passwd.bak /etc/passwd")
+
+ # login with user shell that prints some stdout/err noise
+ # having stdout output in ~/.bashrc confuses docker, so don't run on OSTree
+ m.execute("cd ~admin; cp -a .bashrc .bashrc.bak; [ ! -e .profile ] || cp -a .profile .profile.bak; "
+ "echo 'echo noise-rc-out; echo noise-rc-err >&2' >> .bashrc; "
+ "echo 'echo noise-profile-out; echo noise-profile-err >&2' >> .profile")
+ self.addCleanup(m.execute, "cd ~admin; mv .bashrc.bak .bashrc; "
+ "if [ -e .profile.bak ]; then mv .profile.bak .profile; else rm .profile; fi")
+ b.login_and_go()
+
+ self.allow_journal_messages(r"pam_unix\(cockpit:auth\): authentication failure; .*",
+ r"pam_unix\(cockpit:auth\): check pass; user unknown",
+ r"pam_succeed_if\(cockpit:auth\): requirement .* not met by user .*",
+ "noise-rc-.*")
+
+ @testlib.skipOstree("logs in via ssh, not cockpit-session")
+ def testLogging(self):
+ m = self.machine
+ b = self.browser
+
+ def assert_messages(has_last, n_fail):
+ if has_last:
+ b.wait_visible('#system_last_login')
+ b.wait_in_text('#system_last_login', "Last successful login")
+ b.wait_in_text('#system_last_login_from', "from") # only present if IP was logged
+
+ if n_fail:
+ b.wait_visible('#system_last_login')
+ b.wait_in_text('#system_last_login', f'{n_fail} failed login')
+ b.wait_in_text('#system_last_login_from', "from")
+ b.wait_in_text('#system_last_login_success', "Last successful login")
+ else:
+ self.assertFalse(b.is_present('#system_last_login_success'))
+
+ def verify_correct(has_last, n_fail):
+ b.login_and_go('/system')
+ assert_messages(has_last, n_fail)
+
+ # reload and make sure it's still there (or not)
+ b.reload()
+ b.enter_page('/system')
+ assert_messages(has_last, n_fail)
+
+ b.logout()
+
+ m.start_cockpit()
+
+ # Clean out the relevant logfiles
+ m.execute("truncate -s0 /var/log/{[bw]tmp,lastlog} /var/run/utmp")
+
+ if m.image == "arch":
+ self.sed_file("s/# deny = 3/deny = 4/", "/etc/security/faillock.conf")
+
+ # First login should have no messages
+ verify_correct(has_last=False, n_fail=0)
+
+ # Next login should see the last login
+ verify_correct(has_last=True, n_fail=0)
+
+ # Do some bogus login attempts
+ b.try_login('admin', 'xyz')
+ b.wait_text_not("#login-error-message", "")
+ b.try_login('admin', 'xyz')
+ b.wait_text_not("#login-error-message", "")
+ b.try_login('admin', 'xyz')
+ b.wait_text_not("#login-error-message", "")
+
+ # We should see those bogus attempts now
+ verify_correct(has_last=False, n_fail=3)
+
+ # But after that login, they should be gone again
+ verify_correct(has_last=True, n_fail=0)
+
+ @testlib.skipImage("Arch Linux has no pwquality by default", "arch")
+ def testExpired(self):
+ m = self.machine
+ b = self.browser
+
+ # On OSTree this happens over ssh
+ if m.ostree_image:
+ self.restore_dir("/etc/ssh", restart_unit="sshd")
+ m.execute("sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*")
+ m.execute("( ! systemctl is-active sshd.socket || systemctl stop sshd.socket) && systemctl restart sshd.service")
+
+ # test steps below assume a pam_pwquality config with retry > 1; on some images authselect drops that setting
+ if not m.image.startswith('debian') and not m.image.startswith('ubuntu') and not m.image.startswith("arch"):
+ self.sed_file("/password.*requisite.*pam_pwquality/ s/$/ retry=3/", "/etc/pam.d/password-auth")
+
+ m.execute("chage -d 0 admin")
+ m.start_cockpit()
+ b.open("/system")
+
+ b.wait_visible("#login")
+ b.wait_not_visible("#conversation-group")
+ b.wait_visible("#password-group")
+ b.wait_visible("#user-group")
+ b.set_val('#login-user-input', "admin")
+ b.set_val('#login-password-input', "foobar")
+ b.click('#login-button')
+
+ b.wait_visible("#conversation-group")
+ b.wait_not_visible("#password-group")
+ b.wait_not_visible("#user-group")
+ if m.ostree_image:
+ b.wait_in_text("#conversation-prompt", "You are required to change your password")
+ else:
+ b.wait_in_text("#conversation-message", "You are required to change your password")
+ b.set_val('#conversation-input', 'foobar')
+ b.click('#login-button')
+
+ # Type a bad password
+ b.wait_visible("#conversation-group")
+ b.wait_not_visible("#password-group")
+ b.wait_not_visible("#user-group")
+
+ b.wait_in_text("#conversation-prompt", "New password")
+ b.set_val('#conversation-input', 'admin')
+ b.click('#login-button')
+
+ # We should see a message
+ if m.ostree_image:
+ b.wait_in_text("#conversation-prompt", "BAD PASSWORD")
+ else:
+ b.wait_in_text("#conversation-message", "BAD PASSWORD")
+
+ # Now choose a better password
+ b.wait_not_present("#login-button:disabled")
+ b.wait_visible("#conversation-group")
+ b.wait_not_visible("#password-group")
+ b.wait_not_visible("#user-group")
+ b.wait_in_text("#conversation-prompt", "New password")
+ b.set_val('#conversation-input', '123foobar!@#')
+ b.click('#login-button')
+
+ # Retype the password wrong
+ b.wait_visible("#conversation-group")
+ b.wait_not_visible("#password-group")
+ b.wait_not_visible("#user-group")
+ b.wait_in_text("#conversation-prompt", "Retype")
+ b.set_val('#conversation-input', '123foobar!') # wrong
+ b.click('#login-button')
+
+ # We should see a message
+ if m.ostree_image:
+ b.wait_in_text("#conversation-prompt", "passwords do not match")
+ else:
+ b.wait_in_text("#conversation-message", "passwords do not match")
+
+ # Type the password again
+ b.wait_visible("#conversation-group")
+ b.wait_not_visible("#password-group")
+ b.wait_not_visible("#user-group")
+
+ b.wait_in_text("#conversation-prompt", "New password")
+ b.set_val('#conversation-input', '123foobar!@#')
+ b.click('#login-button')
+
+ # Now type it right
+ b.wait_visible("#conversation-group")
+ b.wait_not_visible("#password-group")
+ b.wait_not_visible("#user-group")
+ b.wait_in_text("#conversation-prompt", "Retype")
+ b.set_val('#conversation-input', '123foobar!@#')
+ b.click('#login-button')
+
+ self.check_shell()
+
+ self.allow_journal_messages('.*You are required to change your password immediately.*',
+ '.*user account or password has expired.*',
+ '.*BAD PASSWORD.*',
+ '.*Sorry, passwords do not match.')
+ self.allow_restart_journal_messages()
+
+ def testConversation(self):
+ m = self.machine
+ b = self.browser
+ conf = "/etc/pam.d/cockpit"
+ if m.ostree_image:
+ conf = "/etc/pam.d/sshd"
+ self.restore_dir("/etc/ssh", restart_unit="sshd")
+ m.execute("sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*")
+ m.execute("( ! systemctl is-active sshd.socket || systemctl stop sshd.socket) && systemctl restart sshd.service")
+
+ # On Arch Linux the ordering matters due to an auth include for system-remote-login
+ if self.machine.image == "arch":
+ self.sed_file('1 a auth required mock-pam-conv-mod.so', conf)
+ else:
+ self.sed_file('5 a auth required mock-pam-conv-mod.so', conf)
+
+ m.start_cockpit()
+ b.open("/system")
+
+ # Try to login as a non-existing user
+ b.try_login("nonexisting", "blahblah")
+
+ b.wait_visible("#conversation-group")
+ b.wait_not_visible("#password-group")
+ b.wait_not_visible("#user-group")
+ b.wait_in_text("#conversation-prompt", "life the universe")
+ b.set_val('#conversation-input', '43')
+ b.click('#login-button')
+
+ b.wait_text_not("#login-error-message", "")
+ b.try_login("admin", "foobar")
+ b.wait_visible("#conversation-group")
+ b.wait_not_visible("#password-group")
+ b.wait_not_visible("#user-group")
+ b.wait_in_text("#conversation-prompt", "life the universe")
+ b.set_val('#conversation-input', '42')
+ b.click('#login-button')
+
+ self.check_shell()
+
+ self.allow_restart_journal_messages()
+
+ @testlib.skipImage("No tlog", "debian-*", "ubuntu-*", "arch", "centos-8-stream")
+ @testlib.skipOstree("No tlog")
+ def testSessionRecordingShell(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute("useradd user --shell /usr/bin/tlog-rec-session")
+ m.execute("echo user:abcdefg | chpasswd")
+ # this doesn't actually record anything, but logging into cockpit should work
+ m.start_cockpit()
+ b.login_and_go("/system", user="user", password="abcdefg")
+ b.wait_visible(".pf-v5-c-alert:contains('Web console is running in limited access mode.')")
+ b.logout()
+
+ self.allow_journal_messages(".*value for the SHELL variable was not found the /etc/shells.*",
+ "Locale charset is ANSI.*",
+ "Assuming locale environment is lost.*",
+ "ATTENTION! Your session is being recorded!")
+
+ def curl_auth(self, url, userpass):
+ header = "Authorization: Basic " + base64.b64encode(userpass.encode()).decode()
+ return subprocess.check_output(['/usr/bin/curl', '-s', '-k', '-D', '-', '--header', header,
+ f'http://{self.machine.web_address}:{self.machine.web_port}{url}'],
+ universal_newlines=True)
+
+ def curl_auth_code(self, url, userpass):
+ lines = self.curl_auth(url, userpass).splitlines()
+ self.assertGreater(len(lines), 0)
+ tokens = lines[0].split(' ', 2)
+ self.assertEqual(len(tokens), 3)
+ return int(tokens[1])
+
+ def testRaw(self):
+ self.machine.start_cockpit()
+ time.sleep(0.5)
+ self.assertEqual(self.curl_auth_code('/cockpit/login', ''), 401)
+ self.assertEqual(self.curl_auth_code('/cockpit/login', 'foo:'), 401)
+ self.assertEqual(self.curl_auth_code('/cockpit/login', 'foo:bar\n'), 401)
+ self.assertEqual(self.curl_auth_code('/cockpit/login', 'foo:bar:baz'), 401)
+ self.assertEqual(self.curl_auth_code('/cockpit/login', ':\n\n'), 401)
+ self.assertIn(self.curl_auth_code('/cockpit/login', 'admin:bar'), [401, 403])
+ self.assertEqual(self.curl_auth_code('/cockpit/login', 'foo:bar'), 401)
+ self.assertIn(self.curl_auth_code('/cockpit/login', 'admin:' + 'x' * 4000), [401, 403])
+ self.assertEqual(self.curl_auth_code('/cockpit/login', 'x' * 4000 + ':bar'), 401)
+ self.assertEqual(self.curl_auth_code('/cockpit/login', 'a' * 4000 + ':'), 401)
+ self.assertEqual(self.curl_auth_code('/cockpit/login', 'a' * 4000 + ':b\nc'), 401)
+ self.assertEqual(self.curl_auth_code('/cockpit/login', 'a' * 4000 + ':b\nc\n'), 401)
+
+ self.allow_journal_messages("Returning error-response ... with reason .*",
+ r"pam_unix\(cockpit:auth\): authentication failure; .*",
+ r"pam_unix\(cockpit:auth\): check pass; user unknown",
+ r"pam_succeed_if\(cockpit:auth\): requirement .* not met by user .*",
+ "couldn't parse login input: Malformed input",
+ "couldn't parse login input: Authentication failed")
+
+ @testlib.skipImage("No SELinux", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipOstree("No semanage")
+ def testSELinuxRestrictedUser(self):
+ m = self.machine
+ b = self.browser
+
+ # non-admin user_u
+ m.execute("useradd unpriv; echo 'unpriv:foobar' | chpasswd; semanage login -a -s user_u unpriv")
+ self.addCleanup(m.execute, "semanage login -d -s user_u unpriv")
+ m.start_cockpit()
+ b.login_and_go("/system", user="unpriv")
+ # generate lastlog entry
+ b.logout()
+ b.login_and_go("/system", user="unpriv")
+ # not an admin
+ b.wait_visible(".pf-v5-c-alert:contains('Web console is running in limited access mode.')")
+
+ b.wait_in_text('#system_last_login', "Last successful login")
+ b.wait_in_text('#system_last_login_from', "web console")
+
+ b.logout()
+ # not allowed to restricted users
+ self.allow_journal_messages('.*type=1400.*avc: denied { map }.*comm="cockpit-pcp".*')
+ self.allow_journal_messages('.*type=1400.*avc: denied .* comm="sudo".*')
+ self.allow_journal_messages('.*type=1400.*avc: denied .* comm="systemd".*')
+ self.allow_journal_messages('.*type=1400.*avc: denied { watch } .* comm="cockpit-bridge".*')
+ self.allow_journal_messages('.*sudo:.* setresuid(.*): Operation not permitted')
+ self.allow_journal_messages('.*sudo: error initializing audit plugin sudoers_audit')
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1727887
+ self.allow_journal_messages('.*type=1400.*avc: denied { connectto } .* path="/run/user/.*/bus" scontext=user_u:user_r:user_t:s0.*')
+
+ # sysadm_u
+ m.execute("useradd priv; echo 'priv:foobar' | chpasswd")
+ m.execute("usermod -aG wheel priv")
+ m.execute("semanage login -a -s sysadm_u priv")
+ self.addCleanup(m.execute, "semanage login -d -s sysadm_u priv")
+ b.login_and_go("/system", user="priv")
+ # passing login info memfd should work
+ b.logout()
+ b.login_and_go("/system", user="priv")
+ b.wait_in_text('#system_last_login', "Last successful login")
+ b.wait_in_text('#system_last_login_from', "web console")
+
+ b.go("/playground/test")
+ b.enter_page("/playground/test")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+
+ if m.image.startswith("rhel-8") or m.image in ["centos-8-stream", "rhel-9-1"]:
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1814569
+ self.assertIn('result: access-denied', b.text(".super-channel span"))
+ else:
+ self.assertIn('result: uid=0', b.text(".super-channel span"))
+
+ def testNoAdminGroup(self):
+ m = self.machine
+ b = self.browser
+
+ # Create a user that is not in wheel but can sudo
+ m.execute("useradd user -s /bin/bash -c User")
+ m.execute("echo user:foobar | chpasswd")
+ self.write_file("/etc/sudoers.d/user", "user ALL=(ALL) ALL")
+ self.password = "foobar"
+
+ self.login_and_go("/system", user="user")
+
+ # session is privileged
+ b.switch_to_top()
+ b.check_superuser_indicator("Administrative access")
+ b.enter_page('/system')
+
+ # shutdown button should be enabled and working
+ b.click("#reboot-button")
+ b.wait_popup("shutdown-dialog")
+ b.wait_in_text(f"#shutdown-dialog button{self.danger_btn_class}", 'Reboot')
+ b.click("#delay")
+ b.click("button:contains('5 minutes')")
+ b.wait_text("#delay .pf-v5-c-select__toggle-text", "5 minutes")
+ b.click(f"#shutdown-dialog button{self.danger_btn_class}")
+
+ # cancel reboot
+ b.wait_in_text('#system-health-shutdown-status-text', "Scheduled reboot")
+ b.click("#system-health-shutdown-status-cancel-btn")
+ b.wait_not_present('#system-health-shutdown-status')
+
+ def testUnsupportedBrowser(self):
+ m = self.machine
+ b = self.browser
+
+ m.start_cockpit()
+ # pretend browser doesn't support a required capability
+ b.cdp.invoke("Page.addScriptToEvaluateOnNewDocument", source="window.WebSocket = undefined;")
+ b.open("/system")
+ b.wait_visible("#unsupported-browser")
+ b.wait_not_visible("#login-fatal")
+ b.wait_not_visible("#login")
+ b.wait_not_visible("#login-details")
+
+ def testBypassSoftRequirements(self):
+ m = self.machine
+ b = self.browser
+
+ m.start_cockpit()
+ b.cdp.invoke("Page.addScriptToEvaluateOnNewDocument", source="""
+ window.CSS.supports = function() { return false; };
+ """)
+ b.open("/system")
+ b.wait_visible("#unsupported-browser")
+ b.wait_in_text("#bypass-browser-check", "Bypass browser check")
+ b.click(".pf-v5-c-expandable-section")
+
+ b.set_val('#login-user-input', "admin")
+ b.set_val('#login-password-input', "foobar")
+ b.click("#login-button")
+
+ self.check_shell()
+
+ @testlib.skipOstree("Starting on OSTree is weird")
+ def testFailingWebsocket(self, safari=False, cacert=False):
+ m = self.machine
+ b = self.browser
+
+ # Cause cockpit-ws to reject WebSocket connections
+ m.write("/etc/cockpit/cockpit.conf", "[WebService]\nOrigins=foo.bar.com\n")
+ self.allow_journal_messages("received request from bad Origin: .*",
+ "connection unexpectedly closed by peer",
+ "Received invalid handshake request from the client")
+ self.allow_browser_errors(".*")
+
+ m.start_cockpit()
+ # Really start Cockpit to make sure it has generated all its certificates.
+ m.execute("systemctl start cockpit")
+
+ if safari:
+ b.set_user_agent("Safari/300")
+
+ if cacert:
+ m.write("/etc/cockpit/ws-certs.d/0-self-signed-ca.pem", "FAKE CERT FOR TESTING\n")
+ else:
+ m.execute("rm -f /etc/cockpit/ws-certs.d/0-self-signed-ca.pem")
+
+ # Log in.
+ b.open("/system")
+ b.wait_visible("#login")
+ b.set_val('#login-user-input', "admin")
+ b.set_val('#login-password-input', "foobar")
+ b.click('#login-button')
+
+ b.wait_visible("#early-failure")
+ if safari and cacert:
+ b.wait_visible("#safari-cert-help")
+ else:
+ b.wait_not_present("#safari-cert-help")
+
+ @testlib.skipBrowser("Enough when only chromium pretends to be a different browser", "firefox")
+ @testlib.skipOstree("Starting on OSTree is weird")
+ def testFailingWebsocketSafari(self):
+ self.testFailingWebsocket(safari=True, cacert=True)
+
+ @testlib.skipBrowser("Enough when only chromium pretends to be a different browser", "firefox")
+ @testlib.skipOstree("Starting on OSTree is weird")
+ def testFailingWebsocketSafariNoCA(self):
+ self.testFailingWebsocket(safari=True, cacert=False)
+
+ def testFaillock(self):
+ m = self.machine
+ b = self.browser
+
+ module = 'faillock'
+
+ def logfile(user):
+ return '/var/run/faillock/' + user
+
+ def expect_fail_count(expected, user="admin", must_exist=True):
+ if must_exist:
+ cksum = m.execute(f"cksum {logfile(user)}")
+
+ output = m.execute(f"{module} --user {user}")
+
+ n_lines = len(output.splitlines())
+ if n_lines:
+ n_lines -= 2 # remove the header, if it's there
+
+ self.assertEqual(expected, n_lines)
+
+ # make sure the above command didn't change the file
+ if must_exist:
+ self.assertEqual(cksum, m.execute(f"cksum {logfile(user)}"))
+
+ def expect_failed_login(user, password, n_failed):
+ b.try_login(user, password)
+ b.wait_text_not("#login-error-message", "")
+ expect_fail_count(n_failed, user=user)
+
+ def expect_successful_login(user, password):
+ b.login_and_go('/system', user=user, password=password)
+ expect_fail_count(0, user=user)
+ b.logout()
+
+ self.enable_root_login()
+
+ # ensure we have no module in our pam config already
+ # arch has faillock enabled by default
+ if m.image != "arch":
+ m.execute(f"! grep -r '{module}' /etc/pam.d")
+
+ # add it to the pam config
+ if m.image.startswith('debian-') or m.image.startswith('ubuntu'):
+ # enable pam_faillock. see example in pam_faillock(8)
+ self.sed_file(r"/fallback if no module succeeds/ s/^/"
+ r"auth [default=die] pam_faillock.so authfail deny=4\n"
+ r"auth sufficient pam_faillock.so authsucc deny=4\n/",
+ "/etc/pam.d/common-auth")
+ elif m.image == "arch":
+ self.sed_file("s/# deny = 3/deny = 4/", "/etc/security/faillock.conf")
+ else:
+ # see https://access.redhat.com/solutions/62949
+ self.sed_file("""/pam_unix/ {
+ s/sufficient/[success=1 default=ignore]/\n
+ aauth [default=die] pam_faillock.so authfail audit deny=4 unlock_time=600\n
+ aauth sufficient pam_faillock.so authsucc audit deny=4 unlock_time=600\n
+ }""", "/etc/pam.d/password-auth")
+
+ m.execute(f"grep -r '{module}' /etc/pam.d")
+
+ m.start_cockpit()
+
+ # start from a clean slate
+ clean_cmd = f"rm -f {logfile('admin')}"
+ self.addCleanup(m.execute, clean_cmd)
+ m.execute(clean_cmd)
+
+ # make sure we can still login
+ b.login_and_go("/system")
+ b.logout()
+
+ # and it should show zero fails
+ expect_fail_count(0, must_exist=False)
+
+ # try three bogus login attempts
+ for n in [1, 2, 3]:
+ expect_failed_login("admin", "bad", n)
+ expect_fail_count(3)
+
+ # make sure we can still login
+ expect_successful_login("admin", "foobar")
+
+ # after the success, try three more bogus login attempts
+ for n in [1, 2, 3]:
+ expect_failed_login("admin", "bad", n)
+ expect_successful_login("admin", "foobar")
+
+ # now try four, which should lock the account
+ for n in [1, 2, 3, 4]:
+ expect_failed_login("admin", "bad", n)
+
+ # logging in should fail now
+ expect_failed_login("admin", "foobar", n)
+
+ # having given the correct password the last time should not have helped things
+ expect_failed_login("admin", "foobar", n)
+
+ # but we can reset the lockout
+ m.execute(module + " --reset --user admin")
+ expect_fail_count(0)
+
+ # and login again
+ expect_successful_login("admin", "foobar")
+
+ # ostree images log in via sshd, which forbids root logging in with a password
+ if not m.ostree_image:
+ # make sure root never gets locked out
+ for n in range(1, 10):
+ expect_failed_login("root", "bad", n)
+ expect_successful_login("root", "foobar")
+
+ self.allow_journal_messages(".*[aA]ccount .*locked due to .* failed logins.*")
+ self.allow_journal_messages(".*minutes left to unlock.*")
+
+ @testlib.skipOstree("root logins disabled by default with ssh")
+ def testPamAccess(self):
+ b = self.browser
+ m = self.machine
+
+ m.start_cockpit()
+ b.open("/system")
+
+ # root login is disabled by default via /etc/cockpit/disallowed-users on everything except RHEL 8
+ if not m.image.startswith("rhel-8") and not m.image.startswith("centos-8"):
+ b.try_login("root", "foobar")
+ b.wait_in_text("#login-error-message", "Wrong user name or password")
+
+ # disable root login with pam_access
+ self.enable_root_login()
+ self.write_file("/etc/security/access.conf", "- : root : ALL\n", append=True)
+ self.sed_file("1 aaccount required pam_access.so", "/etc/pam.d/cockpit")
+ b.try_login(user="root")
+ b.wait_in_text("#login-error-message", "Permission denied")
+
+ self.allow_journal_messages(r"cockpit-session: user account access failed.*root.*")
+
+ @testlib.skipOstree("sssd not available")
+ def testClientCertAuthentication(self):
+ m = self.machine
+
+ if m.image.startswith("debian") or m.image.startswith("ubuntu"):
+ # on Debian/Ubuntu, an unconfigured sssd fails to start, so only restart it at the end if it was running before
+ # also, sssd is split into several services
+ self.restore_dir("/etc/sssd", post_restore_action="systemctl stop 'sssd*'")
+ else:
+ self.restore_dir("/etc/sssd", restart_unit="sssd")
+
+ m.execute("useradd alice; echo alice:foobar123 | chpasswd")
+ m.upload(["alice.pem", "alice.key", "alice-expired.pem", "bob.pem", "bob.key"], self.vm_tmpdir,
+ relative_dir="src/tls/ca/")
+ alice_cert = self.vm_tmpdir + "/alice.pem"
+ alice_cert_expired = self.vm_tmpdir + "/alice-expired.pem"
+ alice_key = self.vm_tmpdir + "/alice.key"
+ alice_cert_key = ['--cert', alice_cert, '--key', alice_key]
+
+ # set up local (NIS) sssd provider and certificate mapping
+ # newer sssd drops "file" provider, but the "proxy" provider cannot do this yet in old versions
+ if m.image in ["debian-stable", "ubuntu-2204", "ubuntu-stable"]:
+ id_provider = "id_provider = files"
+ else:
+ id_provider = """id_provider = proxy
+ local_auth_policy = only
+ proxy_lib_name = files"""
+
+ self.write_file("/etc/sssd/sssd.conf", f"""
+[sssd]
+domains = local
+
+[domain/local]
+{id_provider}
+
+[certmap/local/alice]
+# Requires sssd >= 2.6.1 and installing sssd_auth_ca_db.pem; with earlier sssd this is completely unsafe
+matchrule = <SUBJECT>^DC=LAN,DC=COCKPIT,CN=alice$
+""", perm="0600")
+ m.execute("systemctl restart sssd")
+
+ # ensure sssd certificate lookup without validation works
+ user_obj = m.execute('busctl call org.freedesktop.sssd.infopipe /org/freedesktop/sssd/infopipe/Users '
+ 'org.freedesktop.sssd.infopipe.Users FindByCertificate s -- '
+ """"$(cat %s)" | sed 's/^o "//; s/"$//' """ % alice_cert)
+ self.assertEqual(m.execute('busctl get-property org.freedesktop.sssd.infopipe ' + user_obj.strip() +
+ ' org.freedesktop.sssd.infopipe.Users.User name').strip(),
+ 's "alice"')
+
+ err = m.execute('! busctl call org.freedesktop.sssd.infopipe /org/freedesktop/sssd/infopipe/Users '
+ 'org.freedesktop.sssd.infopipe.Users FindByValidCertificate s -- '
+ """"$(cat %s)" 2>&1""" % alice_cert)
+ self.assertIn("Certificate authority file not found", err)
+
+ # install our CA, so that sssd can validate
+ with open("src/tls/ca/ca.pem") as f:
+ m.write("/etc/sssd/pki/sssd_auth_ca_db.pem", f.read())
+ u = m.execute('busctl call org.freedesktop.sssd.infopipe /org/freedesktop/sssd/infopipe/Users '
+ 'org.freedesktop.sssd.infopipe.Users FindByCertificate s -- '
+ """"$(cat %s)" | sed 's/^o "//; s/"$//' """ % alice_cert)
+ self.assertEqual(u, user_obj)
+
+ # These tests have to be run with curl, as chromium-headless does not support selecting/handling client-side
+ # certificates; it just rejects cert requests. For interactive tests, grab src/tls/ca/alice.p12 and import
+ # it into the browser.
+
+ def do_test(authopts, expected, not_expected=None, session_leader=None):
+ m.start_cockpit(tls=True)
+ output = m.execute(['curl', '-ksS', '-D-', *authopts, 'https://localhost:9090/cockpit/login'])
+ for s in expected:
+ self.assertIn(s, output)
+ for s in (not_expected or []):
+ self.assertNotIn(s, output)
+ # sessions/users often hang around in State=closing for a long time, ignore these
+ if session_leader:
+ m.execute('until [ "$(loginctl show-user --property=State --value alice)" = "active" ]; do sleep 1; done')
+ sessions = m.execute('loginctl show-user --property=Sessions --value alice').strip().split()
+ self.assertGreaterEqual(len(sessions), 1)
+ for session in sessions:
+ out = m.execute('loginctl session-status ' + session)
+ if "State: active" in out: # skip closing sessions
+ self.assertIn(session_leader, out)
+ self.assertIn('cockpit-bridge', out)
+ # systemd < 255: "Service: cockpit; type web; class user"
+ # systemd ≥ 255: "Service: cockpit\n Type: web\n Class: user"
+ self.assertRegex(out, r"Service:\s+cockpit")
+ self.assertRegex(out, "[tT]ype.*web")
+ break
+ else:
+ self.fail("no active session for active user")
+
+ # sessions time out after 10s, but let's not wait for that
+ m.execute('loginctl terminate-session ' + sessions[0])
+ # wait until the session is gone
+ m.execute("while loginctl show-user alice | grep -q 'State=active'; do sleep 1; done")
+
+ m.stop_cockpit()
+
+ # from sssd
+ self.allow_journal_messages("alice is not allowed to run sudo.*")
+
+ # cert auth should not be enabled by default
+ do_test(alice_cert_key, ["HTTP/1.1 401 Authentication failed"])
+ # password auth should work
+ do_test(['-u', 'alice:foobar123'],
+ ['HTTP/1.1 200 OK', '"csrf-token"'],
+ session_leader='cockpit-session')
+
+ # enable cert based auth
+ m.write("/etc/cockpit/cockpit.conf", '[WebService]\nClientCertAuthentication = true\n', append=True)
+ # cert auth should work now
+ do_test(alice_cert_key, ['HTTP/1.1 200 OK', '"csrf-token"'])
+ # password auth, too
+ do_test(['-u', 'alice:foobar123'],
+ ['HTTP/1.1 200 OK', '"csrf-token"'],
+ session_leader='cockpit-session')
+
+ # another certificate gets rejected
+ self.allow_journal_messages("cockpit-session: No matching user for certificate")
+ do_test(["--cert", self.vm_tmpdir + "/bob.pem", "--key", self.vm_tmpdir + "/bob.key"],
+ ["HTTP/1.1 401 Authentication failed", '<h1>Authentication failed</h1>'],
+ not_expected=["crsf-token"])
+
+ # check expired certificate
+ m.start_cockpit(tls=True)
+ journal_cursor = self.machine.journal_cursor()
+ m.execute(f'! curl -ksS --cert {alice_cert_expired} --key {alice_key} https://localhost:9090/cockpit/login')
+ m.stop_cockpit()
+ testlib.wait(lambda: re.search(r'.*Invalid TLS peer certificate.* expired',
+ m.execute(f"journalctl -ocat --cursor '{journal_cursor}' SYSLOG_IDENTIFIER=cockpit-tls")),
+ tries=10)
+ self.allow_journal_messages('.*Invalid TLS peer certificate.* expired')
+ self.allow_journal_messages('.*TLS handshake failed: Error in the certificate verification.*')
+
+ # disallow password auth
+ m.write("/etc/cockpit/cockpit.conf", "[Basic]\naction = none\n", append=True)
+ do_test(alice_cert_key, ['HTTP/1.1 200 OK', '"csrf-token"'])
+ do_test(['-u', 'alice:foobar123'],
+ ['HTTP/1.1 401 Authentication disabled', '<h1>Authentication disabled</h1>'],
+ not_expected=["crsf-token"])
+
+ # wwithout a CA, alice's cert fails
+ m.execute("rm /etc/sssd/pki/sssd_auth_ca_db.pem")
+ self.allow_journal_messages("cockpit-session: Failed to map .* Invalid certificate provided")
+ self.allow_journal_messages("cockpit-session: Failed to map .* Certificate authority file not found")
+ do_test(alice_cert_key, ['HTTP/1.1 401 Authentication failed'])
+
+ # sssd-dbus not available
+ self.allow_journal_messages("cockpit-session: Failed to map .* Could not activate remote peer.*")
+ self.allow_journal_messages("cockpit-session: Failed to map .* Unit sssd-ifp.service is masked.")
+ m.execute("systemctl mask sssd-ifp; systemctl stop sssd-ifp")
+ do_test(alice_cert_key, ["HTTP/1.1 401 Authentication failed", '<h1>Authentication failed</h1>'],
+ not_expected=["crsf-token"])
+ m.execute("systemctl unmask sssd-ifp")
+
+ def testServer(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute("useradd --create-home user")
+ m.execute("echo user:foobar | chpasswd")
+ m.start_cockpit()
+ # the quick succession of login/logout is too much for packagekit's brain
+ self.disable_preload("packagekit", "playground", "systemd")
+
+ def check_server(server, expect_fp_ack):
+ b.open('/')
+ b.wait_visible("#login")
+ # start with no known keys every time
+ b.eval_js("""window.localStorage.setItem("known_hosts", "{ }")""")
+ b.set_val('#login-user-input', "user")
+ b.set_val('#login-password-input', "foobar")
+ b.click("#show-other-login-options")
+ b.set_val("#server-field", server)
+ b.click("#login-button")
+
+ if expect_fp_ack:
+ b.wait_in_text("#hostkey-title", "New host")
+ b.wait_in_text("#hostkey-message-1", "for the first time")
+ b.click("#login-button")
+
+ b.wait_visible('#content')
+ b.enter_page('/system')
+ b.wait_visible('.system-information')
+ b.logout()
+
+ # by name
+ check_server("localhost", expect_fp_ack=True)
+ # by name and port
+ check_server("localhost:22", expect_fp_ack=True)
+ # by IPv4 address; 127.0.0.1 is treated specially as ignore_hostkey fallback, and does not require FP
+ check_server("127.0.0.1", expect_fp_ack=False)
+ # by IPv4 address and port
+ check_server("127.0.0.1:22", expect_fp_ack=False)
+ # by IPv6 address
+ check_server("::1", expect_fp_ack=True)
+ # by IPv6 address and port
+ check_server("[::1]:22", expect_fp_ack=True)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-anaconda b/test/verify/check-storage-anaconda
new file mode 100755
index 0000000..1b88155
--- /dev/null
+++ b/test/verify/check-storage-anaconda
@@ -0,0 +1,487 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2023 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import json
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageAnaconda(storagelib.StorageCase):
+
+ def enterAnacondaMode(self, config):
+ b = self.browser
+ b.call_js_func("window.sessionStorage.setItem", "cockpit_anaconda", json.dumps(config))
+ b.reload()
+ b.enter_page("/storage")
+
+ def expectExportedDevice(self, device, value):
+ mpm = json.loads(self.browser.call_js_func("window.sessionStorage.getItem", "cockpit_mount_points"))
+ self.assertIn(device, mpm)
+ self.assertEqual(mpm[device], value)
+
+ def expectExportedDevicePassphrase(self, device, value):
+ pp = json.loads(self.browser.call_js_func("window.sessionStorage.getItem", "cockpit_passphrases"))
+ self.assertIn(device, pp)
+ self.assertEqual(pp[device], value)
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ disk = self.add_ram_disk()
+
+ anaconda_config = {
+ "mount_point_prefix": "/sysroot",
+ "available_devices": [disk],
+ }
+
+ self.login_and_go("/storage")
+ self.enterAnacondaMode(anaconda_config)
+
+ # There should be only one row, for our disk
+ b.wait(lambda: b.call_js_func('ph_count', self.card("Storage") + " tbody tr") == 1)
+ b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
+ b.wait_not_present(self.card_row("Storage", location="/"))
+
+ # Create a volume group with a logical volume
+ self.click_devices_dropdown("Create LVM2 volume group")
+ self.dialog_wait_open()
+ b.wait(lambda: b.call_js_func('ph_count', "#dialog .select-space-name") == 1)
+ self.dialog_set_val("disks", {disk: True})
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.click_dropdown(self.card_row("Storage", name="vgroup0"), "Create new logical volume")
+ self.dialog({})
+
+ # Create an encrypted filesystem
+ self.click_dropdown(self.card_row("Storage", name="lvol0"), "Format")
+ self.dialog_wait_open()
+ b.wait_not_present(self.dialog_field("at_boot"))
+ b.wait_not_present(self.dialog_field("mount_options"))
+ self.dialog_set_val("type", "ext4")
+ self.dialog_set_val("mount_point", "/")
+ self.dialog_set_val("crypto", self.default_crypto_type)
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_set_val("passphrase2", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot"))
+ self.assertNotIn("nofail", m.execute("findmnt --fstab -n -o OPTIONS /sysroot"))
+
+ b.assert_pixels("body", "page")
+
+ # Edit mount point
+ self.click_dropdown(self.card_row("Storage", name="lvol0"), "Edit mount point")
+ self.dialog_wait_open()
+ self.dialog_wait_val("mount_point", "/")
+ # Empty mount point should be failure
+ self.dialog_set_val("mount_point", "")
+ self.dialog_apply()
+ self.dialog_wait_error("mount_point", "Mount point cannot be empty")
+ self.dialog_set_val("mount_point", "/var")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))
+
+ # Mount/Unmount the filesystem
+ self.click_card_row("Storage", location="/var")
+ b.wait_visible(self.card("Encryption"))
+ b.wait_in_text(self.card_desc("Filesystem", "Mount point"), "/var")
+
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog_wait_open()
+ self.dialog_wait_val("mount_point", "/var")
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assertNotIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))
+
+ b.click(self.card_button("ext4 filesystem", "Unmount"))
+ self.dialog_wait_open()
+ b.wait_text("#dialog .pf-v5-c-modal-box__title-text", "Unmount filesystem /var")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))
+
+ # Deactivate
+ b.click(self.card_button("LVM2 logical volume", "Deactivate"))
+ self.confirm()
+ testlib.wait(lambda: m.execute("if ! test -e /dev/vgroup0/lvol0; then echo gone; fi").strip() == "gone")
+
+ # Check exported information.
+ self.expectExportedDevicePassphrase("/dev/vgroup0/lvol0", "vainu-reku-toma-rolle-kaja")
+ self.expectExportedDevice("/dev/vgroup0/lvol0",
+ {
+ "type": "crypto",
+ "content": {
+ "type": "filesystem",
+ "dir": "/var"
+ }
+ })
+
+ # Activate and mount again, to check location in tear down information
+ b.click(self.card_button("Inactive logical volume", "Activate"))
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog_wait_open()
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ # Check and delete volume group
+ b.click(self.card_parent_link())
+ b.wait_visible(self.card_row("LVM2 volume group", name=disk))
+ self.click_card_dropdown("LVM2 volume group", "Delete group")
+ self.dialog_wait_open()
+ b.wait_text("#dialog td[data-label='Location']", "/var")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ m.execute("! findmnt --fstab -n /sysroot/var")
+
+ # Back to the beginning
+ b.wait_visible(self.card("Storage"))
+ b.wait(lambda: b.call_js_func('ph_count', self.card("Storage") + " tbody tr") == 1)
+ b.wait_not_present(self.card_row("Storage", location="/var"))
+
+ @testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
+ def testStratis(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute("systemctl start stratisd")
+ self.addCleanup(m.execute, "systemctl stop stratisd")
+
+ PV_SIZE = 4000 # 4 GB in MB
+
+ disk = self.add_loopback_disk(PV_SIZE, name="loop10")
+
+ anaconda_config = {
+ "mount_point_prefix": "/sysroot",
+ "available_devices": [disk],
+ }
+
+ self.login_and_go("/storage")
+ self.enterAnacondaMode(anaconda_config)
+
+ # Create a Stratis pool
+ self.click_devices_dropdown("Create Stratis pool")
+ self.dialog_wait_open()
+ b.wait(lambda: b.call_js_func('ph_count', "#dialog .select-space-name") == 1)
+ self.dialog_set_val("disks", {disk: True})
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.click_dropdown(self.card_row("Storage", name="pool0"), "Create new filesystem")
+ self.dialog_wait_open()
+ b.wait_not_present(self.dialog_field("at_boot"))
+ b.wait_not_present(self.dialog_field("mount_options"))
+ self.dialog_set_val("name", "root")
+ self.dialog_set_val("mount_point", "/")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot"))
+ self.assertNotIn("nofail", m.execute("findmnt --fstab -n -o OPTIONS /sysroot"))
+
+ b.wait_visible(self.card_row_col("Storage", 3, 5) + " .usage-bar[role=progressbar]")
+
+ # Edit mount point
+ self.click_dropdown(self.card_row("Storage", name="root"), "Edit mount point")
+ self.dialog_wait_open()
+ self.dialog_wait_val("mount_point", "/")
+ self.dialog_set_val("mount_point", "/var")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))
+
+ # Mount/Unmount the filesystem
+ self.click_card_row("Storage", location="/var")
+ b.wait_in_text(self.card_desc("Stratis filesystem", "Mount point"), "/var")
+ b.click(self.card_button("Stratis filesystem", "Mount"))
+ self.dialog_wait_open()
+ self.dialog_wait_val("mount_point", "/var")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assertNotIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))
+
+ b.click(self.card_button("Stratis filesystem", "Unmount"))
+ self.dialog_wait_open()
+ b.wait_text("#dialog .pf-v5-c-modal-box__title-text", "Unmount filesystem /var")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))
+
+ # Check exported mount point information.
+
+ self.expectExportedDevice("/dev/stratis/pool0/root",
+ {
+ "type": "filesystem",
+ "dir": "/var"
+ })
+
+ # Mount again, to check location in tear down information
+ b.click(self.card_button("Stratis filesystem", "Mount"))
+ self.confirm()
+ self.assertNotIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))
+
+ # Check and delete pool
+ b.click(self.card_parent_link())
+ b.wait_visible(self.card_row("Stratis pool", name=disk))
+ self.click_card_dropdown("Stratis pool", "Delete")
+ self.dialog_wait_open()
+ b.wait_text("#dialog td[data-label='Location']", "/var")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ m.execute("! findmnt --fstab -n /sysroot/var")
+
+ @testlib.skipImage('no btrfs support', 'rhel-*', 'centos-*')
+ def testBtrfs(self):
+ b = self.browser
+
+ disk = self.add_ram_disk(200)
+
+ anaconda_config = {
+ "mount_point_prefix": "/sysroot",
+ "available_devices": [disk],
+ }
+
+ self.login_and_go("/storage")
+ self.enterAnacondaMode(anaconda_config)
+
+ # Create a encrypted Btrfs filesystem
+
+ b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
+ self.click_dropdown(self.card_row("Storage", 1), "Format")
+ self.dialog_wait_open()
+ b.wait_not_present(self.dialog_field("at_boot"))
+ b.wait_not_present(self.dialog_field("mount_options"))
+ self.dialog_set_val("name", "butter")
+ self.dialog_set_val("type", "btrfs")
+ self.dialog_set_val("mount_point", "/mnt/butter")
+ self.dialog_set_val("crypto", self.default_crypto_type)
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_set_val("passphrase2", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ # Unlock and mount so that we can create subvolumes
+ b.wait_text(self.card_row_col("Storage", 1, 3), "Locked data (encrypted)")
+ self.click_dropdown(self.card_row("Storage", 1), "Unlock")
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja"})
+ self.click_dropdown(self.card_row("Storage", location="/mnt/butter"), "Mount")
+ self.dialog({})
+
+ # Create two subvolumes
+ self.click_dropdown(self.card_row("Storage", location="/mnt/butter"), "Create subvolume")
+ self.dialog_wait_open()
+ b.wait_not_present(self.dialog_field("at_boot"))
+ b.wait_not_present(self.dialog_field("mount_options"))
+ self.dialog_set_val("name", "root")
+ self.dialog_set_val("mount_point", "/")
+ b.wait(lambda: b.call_js_func('ph_count', "#dialog button.apply") == 1)
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ self.click_dropdown(self.card_row("Storage", location="/mnt/butter"), "Create subvolume")
+ self.dialog_wait_open()
+ self.dialog_set_val("name", "home")
+ self.dialog_set_val("mount_point", "/home")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ # Unmount and lock again, mount point exporting should still work
+
+ self.click_dropdown(self.card_row("Storage", location="/mnt/butter"), "Unmount")
+ self.dialog({})
+ self.click_dropdown(self.card_row("Storage", name=disk), "Lock")
+ b.wait_text(self.card_row_col("Storage", 1, 3), "Locked data (encrypted)")
+
+ self.expectExportedDevice(disk,
+ {
+ "type": "crypto",
+ "content": {
+ "type": "filesystem",
+ "subvolumes": {
+ "/": {
+ "dir": "/mnt/butter"
+ },
+ "root": {
+ "dir": "/"
+ },
+ "home": {
+ "dir": "/home"
+ },
+ }
+ }
+ })
+
+ def testBiosboot(self):
+ b = self.browser
+
+ disk = self.add_ram_disk()
+
+ anaconda_config = {
+ "mount_point_prefix": "/sysroot",
+ "available_devices": [disk],
+ "efi": False,
+ }
+
+ self.login_and_go("/storage")
+ self.enterAnacondaMode(anaconda_config)
+
+ # Create a biosboot partition on GPT
+ b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
+ self.click_dropdown(self.card_row("Storage", 1), "Create partition table")
+ self.confirm()
+ b.wait_text(self.card_row_col("Storage", 2, 2), "Free space")
+ self.click_dropdown(self.card_row("Storage", 2), "Create partition")
+ self.dialog({"type": "biosboot"})
+
+ # Check the type and set it to something else
+ b.wait_text(self.card_row_col("Storage", 2, 3), "Unformatted data (BIOS boot partition)")
+ self.click_card_row("Storage", 2)
+ b.wait_text(self.card_desc("Partition", "Type"), "BIOS boot partition")
+ b.click(self.card_desc_action("Partition", "Type"))
+ self.dialog({"type": "0fc63daf-8483-4772-8e79-3d69d8477de4"})
+ b.wait_text(self.card_desc("Partition", "Type"), "Linux filesystem data")
+
+ # Correct it by reformatting as "biosboot"
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "biosboot"})
+ b.wait_text(self.card_desc("Partition", "Type"), "BIOS boot partition")
+
+ def testEfiSystemPartition(self):
+ b = self.browser
+
+ disk = self.add_ram_disk()
+
+ anaconda_config = {
+ "mount_point_prefix": "/sysroot",
+ "available_devices": [disk],
+ "efi": True,
+ }
+
+ self.login_and_go("/storage")
+ self.enterAnacondaMode(anaconda_config)
+
+ # Create a EFI system partition on GPT
+ b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
+ self.click_dropdown(self.card_row("Storage", 1), "Create partition table")
+ self.confirm()
+ b.wait_text(self.card_row_col("Storage", 2, 2), "Free space")
+ self.click_dropdown(self.card_row("Storage", 2), "Create partition")
+ self.dialog({"type": "efi"})
+
+ # Check the type and set it to something else
+ b.wait_text(self.card_row_col("Storage", 2, 3), "vfat filesystem (EFI system partition)")
+ b.wait_text(self.card_row_col("Storage", 2, 4), "/boot/efi")
+ self.click_card_row("Storage", 2)
+ b.wait_visible(self.card("vfat filesystem"))
+ b.wait_text(self.card_desc("Partition", "Type"), "EFI system partition")
+ b.click(self.card_desc_action("Partition", "Type"))
+ self.dialog({"type": "0fc63daf-8483-4772-8e79-3d69d8477de4"})
+ b.wait_text(self.card_desc("Partition", "Type"), "Linux filesystem data")
+
+ # Correct it by reformatting as "efi"
+ self.click_card_dropdown("vfat filesystem", "Format")
+ self.dialog({"type": "efi"})
+ b.wait_text(self.card_desc("Partition", "Type"), "EFI system partition")
+
+ def testFormat(self):
+ m = self.machine
+ b = self.browser
+
+ disk = self.add_ram_disk()
+
+ anaconda_config = {
+ "mount_point_prefix": "/sysroot",
+ "available_devices": [disk],
+ "default_fsys_type": "vfat",
+ }
+
+ self.login_and_go("/storage")
+ self.enterAnacondaMode(anaconda_config)
+
+ b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
+ self.click_dropdown(self.card_row("Storage", 1), "Create partition table")
+ self.confirm()
+ b.wait_text(self.card_row_col("Storage", 2, 2), "Free space")
+
+ # Only one apply button in the Create Partition dialog,
+ # default filesystem type should be "vfat".
+ self.click_dropdown(self.card_row("Storage", 2), "Create partition")
+ self.dialog_wait_open()
+ self.dialog_wait_val("type", "vfat")
+ self.dialog_set_val("type", "ext4")
+ self.dialog_set_val("size", "30")
+ b.wait(lambda: b.call_js_func('ph_count', "#dialog button.apply") == 1)
+ b.wait_text("#dialog button.apply", "Create")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ # Page talks about assigned mount points instead of "not mounted".
+ b.wait_text(self.card_row_col("Storage", 2, 4), "(no assigned mount point)")
+
+ # Format it again and make it the root filesystem. It should
+ # keep ext4 as the type.
+ self.click_dropdown(self.card_row("Storage", 2), "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("mount_point", "/")
+ self.dialog_wait_val("type", "ext4")
+ b.wait(lambda: b.call_js_func('ph_count', "#dialog button.apply") == 1)
+ b.wait_text("#dialog button.apply", "Format")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ # Filesystem is not mounted but page doesn't mention "not mounted".
+ m.execute(f"! findmnt {disk}")
+ b.wait_text(self.card_row_col("Storage", 2, 4), "/")
+
+ # Create another partition, it should inherit ext4
+ # from the root
+ b.wait_text(self.card_row_col("Storage", 3, 2), "Free space")
+ self.click_dropdown(self.card_row("Storage", 3), "Create partition")
+ self.dialog_wait_open()
+ self.dialog_wait_val("type", "ext4")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ def testSwap(self):
+ b = self.browser
+
+ disk = self.add_ram_disk()
+
+ anaconda_config = {
+ "mount_point_prefix": "/sysroot",
+ "available_devices": [disk],
+ }
+
+ self.login_and_go("/storage")
+ self.enterAnacondaMode(anaconda_config)
+
+ b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
+ self.click_dropdown(self.card_row("Storage", 1), "Format")
+ self.dialog({"type": "swap"})
+
+ self.expectExportedDevice(disk, {"type": "swap"})
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-basic b/test/verify/check-storage-basic
new file mode 100755
index 0000000..7208216
--- /dev/null
+++ b/test/verify/check-storage-basic
@@ -0,0 +1,93 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageBasic(storagelib.StorageCase):
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage", superuser=False)
+ self.allow_browser_errors("error: findmnt.*")
+
+ create_dropdown = self.dropdown_toggle(self.card_header("Storage"))
+
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(create_dropdown)
+
+ b.relogin('/storage', superuser=True)
+
+ b.wait_visible(create_dropdown)
+
+ # Add a disk, partition it, format it, and finally remove it.
+ disk = self.add_ram_disk()
+
+ self.click_card_row("Storage", name=disk)
+ b.wait_visible(self.card("Solid State Drive"))
+
+ b.wait_text(self.card_desc("Solid State Drive", "Model"), "scsi_debug")
+ b.wait_in_text(self.card_desc("Solid State Drive", "Capacity"), "50 MiB")
+
+ self.assertEqual(self.inode(b.text(self.card_desc("Solid State Drive", "Device file"))), self.inode(disk))
+
+ m.execute(f'parted -s {disk} mktable gpt')
+ m.execute(f'parted -s {disk} mkpart primary ext2 1M 8M')
+ b.wait_text(self.card_row_col("GPT partitions", 1, 2), "Unformatted data")
+
+ # create filesystem on the first partition
+ # HACK - the block device might disappear briefly when udevd does its BLKRRPART.
+ testlib.wait(lambda: m.execute(f'mke2fs {disk}1'), delay=1, tries=5)
+ b.wait_text(self.card_row_col("GPT partitions", 1, 2), "ext2 filesystem")
+
+ self.click_card_row("GPT partitions", 1)
+ b.wait_text(self.card_desc("Partition", "Name"), "primary")
+ b.assert_pixels(self.card("Partition"), "partition",
+ mock={"dt:contains(UUID) + dd": "a12978a1-5d6e-f24f-93de-11789977acde"})
+ b.assert_pixels(self.card("ext2 filesystem"), "filesystem")
+
+ b.go("#/")
+ b.wait_visible(self.card("Storage"))
+ b.wait_visible(self.card_row("Storage", name=disk))
+
+ # Create a subvolume with a really long name to show
+ # truncation in the pixel test.
+
+ if b.pixels_label:
+ long = "really-" * 15 + "long-name-that-will-be-truncated"
+ m.execute(f"btrfs subvol create /{long}")
+ self.addCleanup(m.execute, f"btrfs subvol delete /{long}")
+ b.wait_visible(self.card_row("Storage", name=f"root/{long}"))
+ b.assert_pixels(self.card("Storage"), "overview",
+ # Usage numbers are not stable and also cause
+ # the table columns to shift. The usage bars
+ # are not stable but are always the same size,
+ # so it is good enough to ignore them.
+ mock={".usage-text": "---"},
+ ignore=[".usage-bar"])
+ self.force_remove_disk(disk)
+ b.wait_not_present(self.card_row("Storage", name=disk))
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-btrfs b/test/verify/check-storage-btrfs
new file mode 100755
index 0000000..b209696
--- /dev/null
+++ b/test/verify/check-storage-btrfs
@@ -0,0 +1,596 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2023 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import os.path
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+@testlib.skipImage('no btrfs support', 'rhel-*', 'centos-*')
+class TestStorageBtrfs(storagelib.StorageCase):
+ def setUp(self):
+ super().setUp()
+ self.allow_browser_errors("unable to obtain subvolumes for mount point.*")
+ self.allow_browser_errors("unable to obtain default subvolume for mount point.*")
+ self.allow_browser_errors("error: unable to run findmnt.*")
+ self.allow_browser_errors("error: findmnt.*")
+
+ def checkTeardownAction(self, row, label, text):
+ self.browser.wait_in_text(f".modal-footer-teardown tbody:nth-of-type({row}) td[data-label='{label}']", text)
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ mount_point = "/run/butter"
+ # btrfs requires a 128 MB
+ dev_1 = self.add_ram_disk(size=128)
+
+ self.login_and_go("/storage")
+ with b.wait_timeout(30):
+ b.wait_visible(self.card_row("Storage", name="sda"))
+
+ self.click_dropdown(self.card_row("Storage", name=dev_1), "Format")
+ self.dialog({"name": "butter", "type": "btrfs", "mount_point": mount_point})
+
+ # disk(s) are part of the volume card
+ b.wait_visible(self.card_row("Storage", location=mount_point))
+ self.assertIn("subvol=/", m.execute(f"findmnt --fstab -n -o OPTIONS {mount_point}"))
+ self.click_card_row("Storage", name="sda")
+
+ b.wait_text(self.card_desc("btrfs filesystem", "Label"), "butter")
+ # UDisks does not allow us to change the label of a mounted FS
+ b.wait_visible(self.card_desc_action("btrfs filesystem", "Label") + ":disabled")
+
+ # Unmount to change label
+ self.click_dropdown(self.card_row("btrfs filesystem", name="/"), "Unmount")
+ self.confirm()
+ b.wait_visible(self.card_row("btrfs filesystem", location=f"{mount_point} (not mounted)"))
+
+ label = "butter"
+ b.click(self.card_desc_action("btrfs filesystem", "Label"))
+ self.dialog({"name": label})
+ b.wait_text(self.card_desc("btrfs filesystem", "Label"), label)
+
+ self.click_dropdown(self.card_row("btrfs filesystem", name="/"), "Mount")
+ self.confirm()
+ b.wait_visible(self.card_row("btrfs filesystem", location=f"{mount_point}"))
+
+ # detect new subvolume
+ subvol = "/run/butter/cake"
+ m.execute(f"btrfs subvolume create {subvol}")
+ b.wait_visible(self.card_row("btrfs filesystem", name=os.path.basename(subvol)))
+
+ self.click_dropdown(self.card_row("btrfs filesystem", location=mount_point), "Unmount")
+ self.confirm()
+
+ b.wait_visible(self.card_row("btrfs filesystem", location=f"{mount_point} (not mounted)"))
+ self.click_dropdown(self.card_row("btrfs filesystem", name="/"), "Mount")
+ self.confirm()
+
+ b.wait_visible(self.card_row("btrfs filesystem", location=mount_point))
+ # try to mount a subvol
+ subvol_mount_point = "/run/kitchen"
+ self.click_dropdown(self.card_row("btrfs filesystem", name=os.path.basename(subvol)), "Mount")
+ self.dialog({"mount_point": subvol_mount_point})
+
+ b.wait_in_text(self.card_row("btrfs filesystem", location=subvol_mount_point), "cake")
+ b.wait_visible(self.card_row("btrfs filesystem", location=mount_point))
+
+ b.go("#/")
+ b.wait_visible(self.card("Storage"))
+
+ # mount outside of fstab, should be cleaned up when re-formatting
+ m.execute(f"""
+ btrfs subvolume create {mount_point}/nonfstab
+ mkdir -p /run/basement
+ mount -o subvol=nonfstab {dev_1} /run/basement
+ """)
+
+ b.wait_visible(self.card_row("Storage", name="nonfstab"))
+
+ # Format the btrfs device
+ self.click_dropdown(self.card_row("Storage", name="sda"), "Format")
+ self.dialog_wait_open()
+ self.checkTeardownAction(1, "Device", dev_1)
+ self.checkTeardownAction(1, "Location", "/run/basement")
+ self.checkTeardownAction(1, "Action", "unmount, format")
+ self.checkTeardownAction(2, "Location", mount_point)
+ self.checkTeardownAction(2, "Action", "unmount, format")
+ self.checkTeardownAction(3, "Location", subvol_mount_point)
+ self.checkTeardownAction(3, "Action", "unmount, format")
+ self.dialog_set_val("type", "empty")
+ self.dialog_apply()
+
+ # subvolume is gone
+ b.wait_not_present(self.card_row("Storage", name="nonfstab"))
+
+ # All mounts should be gone
+ m.execute(f"! findmnt -n | grep {dev_1}")
+
+ def testCreateSubvolume(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_ram_disk(size=140)
+ label = "test_subvol"
+ mount_point = "/run/butter"
+ subvol = "cake"
+
+ m.execute(f"mkfs.btrfs -L {label} {disk1}")
+ self.login_and_go("/storage")
+
+ # creation of btrfs partition can take a while on TF.
+ with b.wait_timeout(30):
+ b.wait_visible(self.card_row("Storage", name="sda"))
+
+ root_sel = self.card_row("Storage", name="sda") + " + tr"
+ b.click(self.dropdown_toggle(root_sel))
+ b.wait_visible(self.dropdown_action(root_sel, "Create subvolume") + "[disabled]")
+ b.wait_text(self.dropdown_description(root_sel, "Create subvolume"),
+ "Subvolume needs to be mounted")
+ b.click(self.dropdown_toggle(root_sel))
+
+ self.click_dropdown(root_sel, "Mount")
+ self.dialog({"mount_point": mount_point})
+ self.addCleanup(self.machine.execute, f"umount {mount_point} || true")
+ b.wait_in_text(self.card_row("Storage", location=mount_point), "btrfs subvolume")
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+
+ # Validation
+ self.dialog_wait_open()
+ self.dialog_apply_secondary()
+ self.dialog_wait_error("name", "Name cannot be empty")
+ self.dialog_set_val("name", "foo/bar")
+ self.dialog_apply_secondary()
+ self.dialog_wait_error("name", "Name cannot contain the character '/'.")
+ self.dialog_set_val("name", "a" * 256)
+ self.dialog_apply_secondary()
+ self.dialog_wait_error("name", "Name cannot be longer than 255 characters.")
+
+ # Without mount point
+ self.dialog_set_val("name", subvol)
+ self.dialog_apply_secondary()
+ self.dialog_wait_close()
+ b.wait_visible(self.card_row("Storage", name=subvol))
+ # no fstab entry
+ m.execute(f"! findmnt --fstab -n | grep subvol={subvol}")
+
+ subvol_mount = "quiche"
+ subvol_mount_point = "/run/oven"
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+
+ # With mount point
+ self.dialog_wait_open()
+ self.dialog_set_val("name", subvol_mount)
+ self.dialog_apply()
+ self.dialog_wait_error("mount_point", "Mount point cannot be empty")
+ self.dialog_set_val("mount_point", subvol_mount_point)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.addCleanup(self.machine.execute, f"umount {subvol_mount_point} || true")
+ b.wait_in_text(self.card_row("Storage", location=subvol_mount_point), "btrfs subvolume")
+
+ # Finding the correct subvolume parent from a non-mounted subvolume
+ m.execute(f"btrfs subvolume create {subvol_mount_point}/pizza")
+ self.click_dropdown(self.card_row("Storage", name=f"{subvol_mount}/pizza"), "Create subvolume")
+ self.dialog({"name": "pineapple"}, secondary=True)
+ b.wait_in_text(self.card_row("Storage", name=f"{subvol_mount}/pizza/pineapple"), "btrfs subvolume")
+
+ left_subvol_mount = "/run/left"
+ right_subvol_mount = "/run/right"
+
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+ self.dialog({"name": os.path.basename(left_subvol_mount), "mount_point": left_subvol_mount})
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+ self.dialog({"name": os.path.basename(right_subvol_mount), "mount_point": right_subvol_mount})
+ self.addCleanup(self.machine.execute, f"umount {left_subvol_mount} || true")
+ self.addCleanup(self.machine.execute, f"umount {right_subvol_mount} || true")
+
+ b.wait_in_text(self.card_row("Storage", location=left_subvol_mount), "btrfs subvolume")
+ b.wait_in_text(self.card_row("Storage", location=right_subvol_mount), "btrfs subvolume")
+
+ self.click_dropdown(self.card_row("Storage", location=left_subvol_mount), "Create subvolume")
+ self.dialog({"name": "links"}, secondary=True)
+ b.wait_in_text(self.card_row("Storage", name="left/links"), "btrfs subvolume")
+
+ self.click_dropdown(self.card_row("Storage", location=right_subvol_mount), "Create subvolume")
+ self.dialog({"name": "rechts"}, secondary=True)
+ b.wait_in_text(self.card_row("Storage", name="right/rechts"), "btrfs subvolume")
+
+ # Read only mount, cannot create subvolumes once /run/butter
+ # is unmounted.
+
+ ro_subvol = "/run/ro"
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+ self.dialog({"name": os.path.basename(ro_subvol), "mount_point": ro_subvol, "mount_options.ro": True})
+
+ # Since /run/butter is still mounted read-write, we can create
+ # subvolumes of "ro" using that.
+
+ self.click_dropdown(self.card_row("Storage", location=ro_subvol), "Create subvolume")
+ self.dialog({"name": "bot"}, secondary=True)
+ b.wait_visible(self.card_row("Storage", name="ro/bot"))
+
+ # But once /run/butter has been unmounted, we can't create
+ # subvolumes of "ro" anymore.
+
+ self.click_dropdown(self.card_row("Storage", location="/run/butter"), "Unmount")
+ self.confirm()
+
+ b.click(self.dropdown_toggle(self.card_row("Storage", location=ro_subvol)))
+ b.wait_visible(self.dropdown_action(self.card_row("Storage", location=ro_subvol), "Create subvolume") + "[disabled]")
+ b.wait_text(self.dropdown_description(self.card_row("Storage", location=ro_subvol), "Create subvolume"),
+ "Subvolume needs to be mounted writable")
+ b.click(self.dropdown_toggle(self.card_row("Storage", location=ro_subvol)))
+ # remount as rw, create subvolume and remount as ro to see parents are also checked
+ m.execute(f"""
+ mount -o remount,rw /dev/sda {ro_subvol}
+ btrfs subvolume create {ro_subvol}/readonly
+ mount -o remount,ro /dev/sda {ro_subvol}
+ """)
+
+ subvol_loc = f"{os.path.basename(ro_subvol)}/readonly"
+ self.check_dropdown_action_disabled(self.card_row("Storage", name=subvol_loc), "Create subvolume", "Subvolume needs to be mounted")
+
+ def testDeleteSubvolume(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_ram_disk(size=140)
+ label = "test_subvol"
+ mount_point = "/run/butter"
+ m.execute(f"mkfs.btrfs -L {label} {disk1}")
+ self.login_and_go("/storage")
+
+ # creation of btrfs partition can take a while on TF.
+ with b.wait_timeout(30):
+ b.wait_visible(self.card_row("Storage", name="sda"))
+
+ root_sel = self.card_row("Storage", name="sda") + " + tr"
+ self.click_dropdown(root_sel, "Mount")
+ self.dialog({"mount_point": mount_point})
+ self.addCleanup(self.machine.execute, f"umount {mount_point} || true")
+
+ # No Delete button for the root subvolume
+ b.click(self.dropdown_toggle(root_sel))
+ b.wait_not_present(self.dropdown_action(root_sel, "Delete"))
+ b.click(self.dropdown_toggle(root_sel))
+
+ # Delete subvolume
+ subvol = "subvol"
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+ self.dialog({"name": subvol}, secondary=True)
+ b.wait_visible(self.card_row("Storage", name=subvol))
+
+ self.click_dropdown(self.card_row("Storage", name=subvol), "Delete")
+ self.checkTeardownAction(1, "Device", subvol)
+ self.checkTeardownAction(1, "Action", "delete")
+ self.confirm()
+ b.wait_not_present(self.card_row("Storage", name=subvol))
+
+ # Delete with subvolume with children
+ child_subvol = "child-subvol"
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+ self.dialog({"name": subvol}, secondary=True)
+ b.wait_visible(self.card_row("Storage", name=subvol))
+
+ self.click_dropdown(self.card_row("Storage", name=subvol), "Create subvolume")
+ self.dialog({"name": child_subvol}, secondary=True)
+ b.wait_visible(self.card_row("Storage", name=f"{subvol}/{child_subvol}"))
+
+ self.click_dropdown(self.card_row("Storage", name=subvol), "Delete")
+ self.checkTeardownAction(1, "Device", f"{subvol}/{child_subvol}")
+ self.checkTeardownAction(1, "Action", "delete")
+ self.checkTeardownAction(2, "Device", subvol)
+ self.checkTeardownAction(2, "Action", "delete")
+ self.confirm()
+
+ b.wait_not_present(self.card_row("Storage", name=f"{subvol}/{child_subvol}"))
+ b.wait_not_present(self.card_row("Storage", name=subvol))
+
+ # Delete with subvolume with children and self mounted
+ child_subvol = "child-subvol"
+ subvol_mount_point = "/run/delete"
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+ self.dialog({"name": subvol, "mount_point": subvol_mount_point})
+ b.wait_visible(self.card_row("Storage", location=subvol_mount_point))
+
+ self.click_dropdown(self.card_row("Storage", name=subvol), "Create subvolume")
+ self.dialog({"name": child_subvol}, secondary=True)
+ b.wait_visible(self.card_row("Storage", name=f"{subvol}/{child_subvol}"))
+
+ self.click_dropdown(self.card_row("Storage", name=subvol), "Delete")
+ self.checkTeardownAction(1, "Device", f"{subvol}/{child_subvol}")
+ self.checkTeardownAction(1, "Action", "delete")
+ self.checkTeardownAction(1, "Device", subvol)
+ self.checkTeardownAction(2, "Location", subvol_mount_point)
+ self.checkTeardownAction(2, "Action", "unmount, delete")
+ self.confirm()
+
+ b.wait_not_present(self.card_row("Storage", location=subvol_mount_point))
+ b.wait_not_present(self.card_row("Storage", name=subvol))
+
+ # Delete with subvolume which is mounted and busy
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+ self.dialog({"name": subvol, "mount_point": subvol_mount_point})
+ b.wait_visible(self.card_row("Storage", location=subvol_mount_point))
+ sleep_pid = m.spawn(f"cd {subvol_mount_point}; sleep infinity", "sleep")
+
+ self.click_dropdown(self.card_row("Storage", location=subvol_mount_point), "Delete")
+ self.checkTeardownAction(1, "Location", subvol_mount_point)
+ self.checkTeardownAction(1, "Action", "unmount, delete")
+ b.wait_in_text(".modal-footer-teardown tbody:nth-of-type(1)", "Currently in use")
+ b.click(".modal-footer-teardown tbody:nth-of-type(1) button")
+ b.wait_in_text(".pf-v5-c-popover", str(sleep_pid))
+
+ self.confirm()
+ b.wait_not_present(self.card_row("Storage", location=subvol_mount_point))
+
+ # Cannot delete subvolume when no parent is mounted
+ subvol = "new-subvol"
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+ self.dialog({"name": subvol, "mount_point": subvol_mount_point})
+ self.click_dropdown(root_sel, "Unmount")
+ self.confirm()
+
+ self.check_dropdown_action_disabled(self.card_row("Storage", location=subvol_mount_point), "Delete", "At least one parent needs to be mounted writable")
+ self.click_dropdown(root_sel, "Mount")
+ self.confirm()
+ b.wait_visible(self.card_row("Storage", location=mount_point))
+
+ self.click_dropdown(self.card_row("Storage", location=subvol_mount_point), "Delete")
+ self.confirm()
+
+ # Cannot delete read only mounted subvolume children and itself
+ child_subvol = "child-subvol"
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
+ self.dialog({"name": subvol, "mount_point": subvol_mount_point, "mount_options.ro": True})
+ b.wait_visible(self.card_row("Storage", location=subvol_mount_point))
+ self.assertIn("ro", m.execute(f"findmnt -s -n -o OPTIONS {subvol_mount_point}"))
+
+ self.click_dropdown(self.card_row("Storage", name=subvol), "Create subvolume")
+ self.dialog({"name": child_subvol}, secondary=True)
+ b.wait_visible(self.card_row("Storage", name=f"{subvol}/{child_subvol}"))
+
+ # Allowed as root is mounted
+ self.click_dropdown(self.card_row("Storage", name=f"{subvol}/{child_subvol}"), "Delete")
+ self.dialog_wait_open()
+ self.dialog_cancel()
+
+ # Unmount root as we can otherwise delete via root
+ self.click_dropdown(self.card_row("Storage", location=mount_point), "Unmount")
+ self.confirm()
+ b.wait_visible(self.card_row("Storage", location=f"{mount_point} (not mounted)"))
+
+ self.check_dropdown_action_disabled(self.card_row("Storage", name=f"{subvol}/{child_subvol}"), "Delete", "At least one parent needs to be mounted writable")
+
+ def testMultiDevice(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_ram_disk(size=140)
+ disk2 = self.add_loopback_disk(size=140)
+ label = "raid1"
+ mount_point = "/run/butter"
+ subvol_mount_point = "/run/cake"
+ subvol = "/run/butter/cake"
+ subvol2 = "/run/butter/bread"
+ subvol_name = os.path.basename(subvol)
+
+ m.execute(f"mkfs.btrfs -L {label} -d raid1 {disk1} {disk2}")
+ self.login_and_go("/storage")
+
+ # creation of btrfs partition can take a while on TF.
+ with b.wait_timeout(30):
+ b.wait_visible(self.card_row("Storage", name=label))
+
+ b.wait_in_text(self.card_row("Storage", name=os.path.basename(disk1)), "btrfs device")
+ b.wait_in_text(self.card_row("Storage", name=os.path.basename(disk2)), "btrfs device")
+
+ # We don't allow formatting of multi device btrfs filesystems
+ self.click_dropdown(self.card_row("Storage", name=os.path.basename(disk1)), "Format")
+ self.dialog_wait_open()
+ self.dialog_wait_title(f"{disk1} is in use")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # mount /
+ self.click_dropdown(self.card_row("Storage", name=label) + " + tr", "Mount")
+ self.dialog({"mount_point": mount_point})
+ b.wait_visible(self.card_row("Storage", location=mount_point))
+
+ # create subvolume
+ m.execute(f"""
+ btrfs subvolume create {subvol}
+ btrfs subvolume create {subvol2}
+ """)
+ b.wait_visible(self.card_row("Storage", name=os.path.basename(subvol)))
+
+ self.click_dropdown(self.card_row("Storage", name=os.path.basename(subvol)), "Mount")
+ self.dialog({"mount_point": subvol_mount_point})
+ b.wait_visible(self.card_row("Storage", location=subvol_mount_point))
+
+ # devices overview
+ self.click_card_row("Storage", name=label)
+ b.wait_visible(self.card_row("btrfs volume", name=disk1))
+ b.wait_visible(self.card_row("btrfs volume", name=disk2))
+
+ # unmount via main page
+ b.go("#/")
+ b.wait_visible(self.card("Storage"))
+
+ self.click_dropdown(self.card_row("Storage", location=subvol_mount_point), "Unmount")
+ self.confirm()
+ b.wait_visible(self.card_row("Storage", location=f"{subvol_mount_point} (not mounted)"))
+
+ self.click_dropdown(self.card_row("Storage", name=os.path.basename(subvol)), "Mount")
+ self.dialog({"mount_point": subvol_mount_point})
+ b.wait_visible(self.card_row("Storage", location=subvol_mount_point))
+
+ mount_options = m.execute(f"findmnt --fstab -n -o OPTIONS {subvol_mount_point}").strip()
+ self.assertIn(f"subvol={subvol_name}", mount_options)
+ self.assertEqual(mount_options.count(subvol_name), 1)
+
+ self.click_dropdown(self.card_row("Storage", name=os.path.basename(subvol2)), "Mount")
+ self.dialog_wait_open()
+ self.dialog_set_val("mount_point", subvol_mount_point)
+ self.dialog_apply()
+ self.dialog_wait_error("mount_point", f"Mount point is already used for btrfs subvolume {os.path.basename(subvol)} of raid1")
+ self.dialog_cancel()
+
+ self.click_dropdown(self.card_row("Storage", location=subvol_mount_point), "Unmount")
+ self.confirm()
+ b.wait_visible(self.card_row("Storage", location=f"{subvol_mount_point} (not mounted)"))
+
+ def testDefaultSubvolume(self):
+ m = self.machine
+ b = self.browser
+
+ disk1 = self.add_ram_disk(size=140)
+ label = "test_subvol"
+ mount_point = "/run/butter"
+ subvol = "cake"
+ subvol_path = f"{mount_point}/{subvol}"
+
+ m.execute(f"mkfs.btrfs -L {label} {disk1}")
+ self.login_and_go("/storage")
+
+ # creation of btrfs partition can take a while on TF.
+ with b.wait_timeout(30):
+ b.wait_visible(self.card_row("Storage", name="sda"))
+
+ b.wait_in_text(self.card_row("Storage", name=os.path.basename(disk1)), "btrfs filesystem")
+
+ # Create a new btrfs subvolume and set it as default and mount it.
+ m.execute(f"""
+ mkdir -p {mount_point}
+ mount {disk1} {mount_point}
+ btrfs subvolume create {subvol_path}
+ btrfs subvolume set-default {subvol_path}
+ umount {mount_point}
+ mount {disk1} {mount_point}
+ """)
+
+ # Show a warning for mismounting in details.
+ b.wait_visible(self.card_row("Storage", name=subvol))
+ b.wait_visible(self.card_row("Storage", name=subvol) + ' .ct-icon-exclamation-triangle')
+ self.click_card_row("Storage", name=subvol)
+ b.wait_text(self.card_desc("btrfs subvolume", "Name"), subvol)
+
+ # Mount automatically on /run/butter on boot
+ b.click(self.card_button("btrfs subvolume", f"Mount automatically on {mount_point} on boot"))
+ b.wait_not_present(self.card_button("btrfs subvolume", f"Mount automatically on {mount_point} on boot"))
+
+ # No warnings on main page for either subvolumes
+ b.go("#/")
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name=subvol) + ' .ct-icon-exclamation-triangle')
+ b.wait_not_present(self.card_row("Storage", name="/") + ' .ct-icon-exclamation-triangle')
+
+ def testLuksEncrypted(self):
+ m = self.machine
+ b = self.browser
+
+ disk = self.add_ram_disk(size=128)
+ label = "butter"
+ mount_point = "/run/butter"
+ passphrase = "einszweidrei"
+
+ m.execute(f"""
+ echo {passphrase} | cryptsetup luksFormat --pbkdf-memory 32768 {disk}
+ echo {passphrase} | cryptsetup luksOpen {disk} btrfs-test
+ mkfs.btrfs -L {label} /dev/mapper/btrfs-test
+ """)
+
+ self.login_and_go("/storage")
+ # creation of btrfs partition can take a while on TF.
+ with b.wait_timeout(30):
+ b.wait_visible(self.card_row("Storage", name="sda"))
+ b.wait_in_text(self.card_row("Storage", name="sda"), "btrfs filesystem (encrypted)")
+ self.click_dropdown(self.card_row("Storage", name="sda") + " + tr", "Mount")
+ self.dialog({"mount_point": mount_point})
+
+ m.execute(f"""
+ umount {mount_point}
+ cryptsetup luksClose /dev/mapper/btrfs-test
+ """)
+ b.wait_in_text(self.card_row("Storage", name="sda"), "Locked data (encrypted)")
+ self.click_dropdown(self.card_row("Storage", name="sda"), "Unlock")
+ self.dialog({"passphrase": "einszweidrei"})
+ b.wait_in_text(self.card_row("Storage", name="sda"), "btrfs filesystem (encrypted)")
+
+ self.click_dropdown(self.card_row("Storage", name="sda") + " + tr", "Mount")
+ self.confirm()
+ b.wait_in_text(self.card_row("Storage", location=mount_point), "btrfs subvolume")
+
+ def testNoSubvolMount(self):
+ m = self.machine
+ b = self.browser
+
+ disk = self.add_ram_disk(size=128)
+ mount_point = "/run/butter"
+
+ m.execute(f"""
+ mkfs.btrfs -L butter {disk}
+ mkdir -p {mount_point}
+ mount {disk} {mount_point}
+ echo '{disk} {mount_point} auto defaults 0 0' >> /etc/fstab
+ """)
+
+ self.login_and_go("/storage")
+
+ self.click_card_row("Storage", name="sda")
+ b.wait_visible(self.card_row("btrfs filesystem", name="/"))
+
+ def testNothingMounted(self):
+ m = self.machine
+ b = self.browser
+
+ disk = self.add_ram_disk(size=128)
+
+ m.execute(f"mkfs.btrfs -L butter {disk}; mount {disk} /mnt; btrfs subvolume create /mnt/home; btrfs subvolume create /mnt/backups; umount /mnt")
+
+ self.login_and_go("/storage")
+
+ self.click_card_row("Storage", name="sda")
+ b.wait_not_present(self.card_row("btrfs filesystem", name="home"))
+ b.wait_not_present(self.card_row("btrfs filesystem", name="backups"))
+
+ # Add some fstab entries. Cockpit should pick up the
+ # subvolumes mentioned in them and show them.
+
+ m.execute(f"echo >>/etc/fstab '{disk} /mnt/home auto noauto,subvol=home 0 0'")
+ m.execute(f"echo >>/etc/fstab '{disk} /mnt/backups auto noauto,subvol=backups 0 0'")
+
+ b.wait_text(self.card_row_col("btrfs filesystem", row_name="home", col_index=3), "/mnt/home (not mounted)")
+ b.wait_text(self.card_row_col("btrfs filesystem", row_name="backups", col_index=3), "/mnt/backups (not mounted)")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-format b/test/verify/check-storage-format
new file mode 100755
index 0000000..64f89a2
--- /dev/null
+++ b/test/verify/check-storage-format
@@ -0,0 +1,194 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageFormat(storagelib.StorageCase):
+
+ def testFormatTooSmall(self):
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Try to format a disk that is too small for XFS.
+
+ disk = self.add_ram_disk(size=5)
+ self.click_card_row("Storage", name=disk)
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "xfs")
+ self.dialog_set_val("mount_point", "/foo")
+ self.dialog_apply_secondary()
+
+ b.wait_in_text("#dialog", "Error creating")
+ b.wait_in_text("#dialog", "too small")
+
+ def testFormatTypes(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk(size=320) # xfs minimum size is ~300MB
+ self.click_card_row("Storage", name=disk)
+
+ def check_type(fstype, label_limit):
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", fstype)
+ self.dialog_set_val("mount_point", "/foo")
+ self.dialog_set_val("name", "X" * (label_limit + 1))
+ self.dialog_apply_secondary()
+ self.dialog_wait_error("name", "Name cannot be longer than %d characters" % label_limit)
+ self.dialog_set_val("name", "X" * label_limit)
+ self.dialog_apply_secondary()
+ self.dialog_wait_close()
+ b.wait_visible(self.card(fstype + " filesystem"))
+ self.click_card_dropdown(fstype + " filesystem", "Format")
+ self.dialog({"type": "empty"})
+ b.wait_visible(self.card("Unformatted data"))
+
+ def check_unsupported_type(fstype):
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog_wait_open()
+ b.wait_not_present(f'#dialog li[value={fstype}]')
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ check_type("xfs", 12)
+ check_type("ext4", 16)
+ check_type("vfat", 11)
+
+ if m.image.startswith("rhel") or m.image.startswith("centos"):
+ check_unsupported_type("ntfs")
+ else:
+ check_type("ntfs", 128)
+
+ # Format without mount point
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "xfs")
+ self.dialog_apply_secondary()
+ self.dialog_wait_close()
+
+ # Verify button text is 'Format' when no filesystem is selected
+ self.click_card_dropdown("xfs filesystem", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "empty")
+ b.wait_text("#dialog .apply", "Format")
+
+ def testFormatCancel(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Make a super slow block device so that we have enough
+ # chances to cancel the operation
+
+ disk = self.add_ram_disk()
+ blocks = int(m.execute(f"blockdev --getsz {disk}"))
+
+ m.execute(f"echo '0 {blocks} delay {disk} 0 500' | dmsetup create superslow")
+ self.click_card_row("Storage", name="/dev/mapper/superslow")
+
+ # Put a signature near the end
+ sigoff = (blocks - 1) * 512
+ m.execute(f"echo hello | dd if=/dev/stdin of=/dev/mapper/superslow bs=1 count=5 seek={sigoff}")
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("erase.on", val=True)
+ self.dialog_set_val("mount_point", "/foo")
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ b.wait_in_text("footer", "Erasing /dev/mapper/superslow")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # The signature should still be there
+ sig = m.execute(f"dd if=/dev/mapper/superslow of=/dev/stdout bs=1 count=5 skip={sigoff}")
+ self.assertEqual(sig, 'hello')
+
+ def testAtBoot(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk()
+ self.click_card_row("Storage", name=disk)
+
+ def format_partition(expected_at_boot, at_boot, keep, first=False):
+ self.click_card_dropdown("Unformatted data" if first else "ext4 filesystem", "Format")
+ self.dialog_wait_open()
+ self.dialog_wait_val("at_boot", expected_at_boot)
+ self.dialog_set_val("type", "ext4")
+ self.dialog_set_val("mount_point", "/foo")
+ self.dialog_set_val("at_boot", at_boot)
+ if keep:
+ self.dialog_set_val("crypto", " keep")
+ else:
+ self.dialog_set_val("crypto", "luks1")
+ self.dialog_set_val("passphrase", "foobarfoo")
+ self.dialog_set_val("passphrase2", "foobarfoo")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ format_partition("nofail", "local", keep=False, first=True)
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "stop boot on failure")
+ self.assertNotIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+
+ format_partition("local", "nofail", keep=True)
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "ignore failure")
+ self.assertIn("nofail", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assert_in_configuration(disk, "crypttab", "options", "nofail")
+
+ format_partition("nofail", "netdev", keep=True)
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "after network")
+ self.assertIn("_netdev", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assert_in_configuration(disk, "crypttab", "options", "_netdev")
+
+ format_partition("netdev", "never", keep=True)
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "never mount")
+ self.assertIn("x-cockpit-never-auto", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assert_in_configuration(disk, "crypttab", "options", "noauto")
+
+ format_partition("never", "nofail", keep=False)
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "ignore failure")
+ self.assertIn("nofail", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assert_in_configuration(disk, "crypttab", "options", "nofail")
+
+ format_partition("nofail", "netdev", keep=False)
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "after network")
+ self.assertIn("_netdev", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assert_in_configuration(disk, "crypttab", "options", "_netdev")
+
+ format_partition("netdev", "never", keep=False)
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "never mount")
+ self.assertIn("x-cockpit-never-auto", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assert_in_configuration(disk, "crypttab", "options", "noauto")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-hidden b/test/verify/check-storage-hidden
new file mode 100755
index 0000000..74848d0
--- /dev/null
+++ b/test/verify/check-storage-hidden
@@ -0,0 +1,153 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageHiddenLuks(storagelib.StorageCase):
+ def test(self):
+ m = self.machine
+ b = self.browser
+
+ mount_point_1 = "/run/mount1"
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk()
+ b.wait_visible(self.card_row("Storage", name=disk))
+
+ # Create a volume group with a logical volume with a encrypted
+ # filesystem.
+
+ self.click_dropdown(self.card_header("Storage"), "Create LVM2 volume group")
+ self.dialog_wait_open()
+ self.dialog_set_val('name', "TEST")
+ self.dialog_set_val('disks', {disk: True})
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ self.click_card_row("Storage", name="TEST")
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog({'purpose': "block",
+ 'name': "lvol",
+ 'size': 48})
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol")
+
+ self.click_card_row("LVM2 logical volumes", 1)
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "crypto": self.default_crypto_type,
+ "name": "FS",
+ "passphrase": "einszweidrei",
+ "passphrase2": "einszweidrei",
+ "mount_point": mount_point_1,
+ "crypto_options": "my-crypto-tag"},
+ secondary=True)
+ self.assert_in_configuration("/dev/TEST/lvol", "crypttab", "options", "my-crypto-tag")
+ self.assert_in_child_configuration("/dev/TEST/lvol", "fstab", "dir", mount_point_1)
+ self.assert_in_lvol_child_configuration("lvol", "crypttab", "options", "my-crypto-tag")
+ self.assert_in_lvol_child_configuration("lvol", "fstab", "dir", mount_point_1)
+ b.click(self.card_parent_link())
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "Filesystem (encrypted)")
+
+ # Now the filesystem is hidden because the LUKS device is
+ # locked. Doubly hide it by deactivating /dev/TEST/lvol
+ self.click_dropdown(self.card_row("LVM2 logical volumes", 1), "Deactivate")
+ self.confirm()
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "Inactive logical volume")
+
+ # Deleting the volume group should still remove the fstab entry
+ self.click_card_dropdown("LVM2 volume group", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("Storage"))
+ self.assertEqual(m.execute(f"grep {mount_point_1} /etc/fstab || true"), "")
+ self.assertEqual(m.execute(f"grep {'my-crypto-tag'} /etc/crypttab || true"), "")
+
+
+@testlib.nondestructive
+class TestStorageHidden(storagelib.StorageCase):
+
+ def testHiddenRaid(self):
+ m = self.machine
+ b = self.browser
+
+ mount_point_2 = "/run/mount2"
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_loopback_disk()
+ disk2 = self.add_loopback_disk()
+ b.wait_visible(self.card_row("Storage", name=disk1))
+ b.wait_visible(self.card_row("Storage", name=disk2))
+
+ self.dialog_with_retry(trigger=lambda: self.click_dropdown(self.card_header("Storage"),
+ "Create MDRAID device"),
+ expect=lambda: (self.dialog_is_present('disks', disk1) and
+ self.dialog_is_present('disks', disk2)),
+ values={"name": "ARR",
+ "disks": {disk1: True,
+ disk2: True}})
+
+ self.click_card_row("Storage", name="/dev/md/ARR")
+ b.wait_visible(self.card("MDRAID device"))
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "name": "FS2",
+ "mount_point": mount_point_2})
+ self.assert_in_configuration("/dev/md127", "fstab", "dir", mount_point_2)
+ b.wait_visible(self.card("ext4 filesystem"))
+
+ # we need to wait for mdadm --monitor to stop using the device before delete
+ m.execute("while fuser -s /dev/md127; do sleep 0.2; done", timeout=20)
+
+ self.click_card_dropdown("MDRAID device", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("Storage"))
+ self.assertEqual(m.execute(f"grep {mount_point_2} /etc/fstab || true"), "")
+
+ @testlib.onlyImage("Only test snaps on Ubuntu", "ubuntu*")
+ def testHiddenSnap(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # We expect there to be at least one loop back device mounted
+ # somewhere below /snap.
+ snap_loops = []
+ devices = storagelib.json.loads(m.execute("lsblk --json --list --output NAME,TYPE,MOUNTPOINT"))
+ for d in devices["blockdevices"]:
+ if d["type"] == "loop" and d["mountpoint"].startswith("/snap/"):
+ snap_loops.append(d["name"])
+ self.assertGreater(len(snap_loops), 0)
+
+ # Make one more loopback device that we expect to see in the UI.
+ dev = self.add_loopback_disk()
+
+ # Now we wait until the regular loopback device is shown. The
+ # snaps should not be shown.
+ b.wait_visible(self.card_row("Storage", name=dev))
+ for sl in snap_loops:
+ b.wait_not_present(self.card_row("Storage", name=sl))
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-ignored b/test/verify/check-storage-ignored
new file mode 100755
index 0000000..34f844d
--- /dev/null
+++ b/test/verify/check-storage-ignored
@@ -0,0 +1,52 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageIgnored(storagelib.StorageCase):
+
+ def testIgnored(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+ disk = self.add_loopback_disk()
+ self.click_card_row("Storage", name=disk)
+ b.wait_visible(self.card("Block device"))
+ m.execute(f"yes | mke2fs -q -L TESTLABEL {disk}")
+ with b.wait_timeout(30):
+ b.wait_text(self.card_desc("ext2 filesystem", "Name"), "TESTLABEL")
+
+ # Hide it via a udev rule.
+ m.write("/run/udev/rules.d/99-ignore.rules",
+ 'SUBSYSTEM=="block", ENV{ID_FS_LABEL}=="TESTLABEL", ENV{UDISKS_IGNORE}="1"\n')
+ self.addCleanup(m.execute, "rm /run/udev/rules.d/99-ignore.rules; udevadm control --reload; udevadm trigger")
+ m.execute("udevadm control --reload; udevadm trigger")
+
+ b.wait_in_text(".pf-v5-c-breadcrumb", "Not found")
+ b.go("#/")
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name=disk))
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-iscsi b/test/verify/check-storage-iscsi
new file mode 100755
index 0000000..1dce49f
--- /dev/null
+++ b/test/verify/check-storage-iscsi
@@ -0,0 +1,126 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.skipImage("UDisks doesn't have support for iSCSI", "debian-*", "ubuntu-*", "arch")
+@testlib.nondestructive
+class TestStorageISCSI(storagelib.StorageCase):
+
+ def testISCSI(self):
+ m = self.machine
+ b = self.browser
+
+ self.restore_dir("/etc/iscsi")
+ self.restore_dir("/var/lib/iscsi")
+
+ # ensure that we generate a /etc/iscsi/initiatorname.iscsi
+ m.execute("systemctl start iscsid; systemctl stop iscsid")
+
+ target_iqn = "iqn.2015-09.cockpit.lan"
+ initiator_iqn = "iqn.2015-10.cockpit.lan"
+
+ # Increase the iSCSI timeouts for heavy load during our testing
+ self.sed_file(r"s|^\(node\..*log.*_timeout = \).*|\1 60|", "/etc/iscsi/iscsid.conf")
+
+ # Setup a iSCSI target with authentication for discovery and join
+ #
+ # HACK - some versions of targetcli crash when TERM is not set
+ # and stdout is not a tty.
+ #
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1441121
+
+ m.execute("""
+ export TERM=dumb
+ targetcli /backstores/ramdisk create test 50M
+ targetcli /iscsi set discovery_auth enable=1 userid=admin password=foobar
+ targetcli /iscsi create %(tgt)s
+ targetcli /iscsi/%(tgt)s/tpg1/luns create /backstores/ramdisk/test
+ targetcli /iscsi/%(tgt)s/tpg1 set attribute authentication=1
+ targetcli /iscsi/%(tgt)s/tpg1/acls create %(ini)s
+ targetcli /iscsi/%(tgt)s/tpg1/acls/%(ini)s set auth userid=admin password=barfoo
+ """ % {"tgt": target_iqn, "ini": initiator_iqn})
+ self.addCleanup(m.execute, f"""
+ targetcli /iscsi delete {target_iqn}
+ iscsiadm -m node -o delete || true
+ targetcli /backstores/ramdisk delete test""")
+ # m.execute("targetcli ls")
+
+ self.login_and_go("/storage")
+
+ # Set initiator IQN
+ orig_iqn = m.execute("sed </etc/iscsi/initiatorname.iscsi -e 's/^.*=//'").rstrip()
+ self.click_dropdown(self.card_header("Storage"), "Change iSCSI initiater name")
+ self.dialog(expect={"name": orig_iqn},
+ values={"name": initiator_iqn})
+ new_iqn = m.execute("sed </etc/iscsi/initiatorname.iscsi -e 's/^.*=//'").rstrip()
+ self.assertEqual(new_iqn, initiator_iqn)
+
+ # Access the target
+
+ self.click_dropdown(self.card_header("Storage"), "Add iSCSI portal")
+
+ self.dialog_wait_open()
+ self.dialog_set_val("address", "127.0.0.1")
+ self.dialog_set_val("username", "admin")
+ self.dialog_set_val("password", "foobar")
+ self.dialog_apply()
+ # The dialog closes and a new dialog opens but we can't get
+ # between that, so we just wait for the new fields to
+ # appear.
+ b.wait_visible(self.dialog_field("target"))
+ b.wait_in_text(self.dialog_field("target"), target_iqn)
+ self.dialog_apply()
+ # Login will fail and a new dialog opens
+ b.wait_visible(self.dialog_field("username"))
+ self.dialog_wait_val("username", "admin")
+ self.dialog_wait_val("password", "foobar")
+ self.dialog_set_val("password", "barfoo")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ self.click_card_row("Storage", name=target_iqn)
+ b.wait_text(self.card_desc("iSCSI portal", "Address"), "127.0.0.1")
+ b.wait_text(self.card_desc("iSCSI portal", "Target"), target_iqn)
+ b.wait_in_text(self.card_row_col("iSCSI drives", 1, 1), "LIO-ORG test")
+
+ # Make a filesystem on it to prove that the disk really works.
+
+ self.click_card_row("iSCSI drives", 1)
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "name": "FILESYSTEM",
+ "mount_point": "/data"})
+ # _netdev should have been prefilled
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "after network")
+ b.click(self.card_parent_link())
+ b.wait_in_text(self.card_row_col("iSCSI drives", 1, 2), "ext4 filesystem")
+
+ self.click_card_dropdown("iSCSI portal", "Disconnect")
+ with b.wait_timeout(120):
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name=target_iqn))
+
+ b.wait_not_in_text(self.card("Storage"), "LIO-ORG test")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-luks b/test/verify/check-storage-luks
new file mode 100755
index 0000000..fa811ce
--- /dev/null
+++ b/test/verify/check-storage-luks
@@ -0,0 +1,720 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import subprocess
+
+import packagelib
+import storagelib
+import testlib
+
+
+def console_screenshot(machine, name):
+ subprocess.run("virsh -c qemu:///session screenshot %s '%s'" % (str(machine._domain.ID()), name),
+ shell=True)
+ testlib.attach(name, move=True)
+ print("Wrote screenshot to " + name)
+
+
+@testlib.nondestructive
+class TestStorageLuks(storagelib.StorageCase):
+
+ def testLuks(self):
+ self.allow_journal_messages("Device is not initialized.*", ".*could not be opened.")
+ m = self.machine
+ b = self.browser
+
+ mount_point_secret = "/run/secret"
+
+ self.login_and_go("/storage")
+
+ # Add a disk and partition it
+ disk = self.add_ram_disk(100)
+ self.click_card_row("Storage", name=disk)
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.dialog({"type": "gpt"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+
+ self.assertEqual(m.execute("grep -v ^# /etc/crypttab || true").strip(), "")
+
+ # Format it with luks
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition")
+ self.dialog_wait_open()
+ self.dialog_set_val("size", 60)
+ self.dialog_set_val("type", "ext4")
+ self.dialog_set_val("crypto", self.default_crypto_type)
+ self.dialog_set_val("name", "ENCRYPTED")
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_set_val("passphrase2", "vainu-reku-toma-rolle-kaja")
+ self.dialog_set_val("store_passphrase.on", val=True)
+ self.dialog_set_val("crypto_options", "crypto,options")
+ self.dialog_set_val("mount_point", mount_point_secret)
+ b.assert_pixels("#dialog", "format")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_row_col("GPT partitions", 1, 2), "ext4 filesystem (encrypted)")
+ b.wait_text(self.card_row_col("GPT partitions", 1, 3), mount_point_secret)
+
+ dev = "/dev/" + b.text(self.card_row_col("GPT partitions", 1, 1))
+ self.click_card_row("GPT partitions", 1)
+
+ if self.default_crypto_type == "luks1":
+ b.wait_text(self.card_desc("Encryption", "Encryption type"), "LUKS1")
+ elif self.default_crypto_type == "luks2":
+ b.wait_text(self.card_desc("Encryption", "Encryption type"), "LUKS2")
+
+ uuid = m.execute(f"cryptsetup luksUUID {dev}").strip()
+ cleartext_dev = "/dev/mapper/luks-" + uuid
+ passphrase_path = "/etc/luks-keys/luks-" + uuid
+
+ self.assert_in_configuration(dev, "crypttab", "options", "crypto,options")
+ self.assert_not_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_in_configuration(dev, "crypttab", "passphrase-path", passphrase_path)
+ self.assertEqual(m.execute(f"cat {passphrase_path}"), "vainu-reku-toma-rolle-kaja")
+
+ self.assert_in_configuration(cleartext_dev, "fstab", "dir", mount_point_secret)
+ self.assert_not_in_configuration(cleartext_dev, "fstab", "opts", "noauto")
+
+ # cut off minutes, to avoid a too wide race condition in the test
+ date_no_mins = b.eval_js("Intl.DateTimeFormat('en', { dateStyle: 'medium', timeStyle: 'short' }).format()").split(':')[0]
+ b.wait_in_text(self.card_desc("Encryption", "Stored passphrase"),
+ f"Last modified: {date_no_mins}:")
+
+ b.assert_pixels(self.card("Encryption"), "card",
+ ignore=["dt:contains(Stored passphrase) + dd",
+ "dt:contains(Cleartext device) + dd"])
+
+ # Unmount. This locks it
+ b.click(self.card_button("ext4 filesystem", "Unmount"))
+ self.confirm()
+ self.assert_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_in_child_configuration(dev, "fstab", "opts", "noauto")
+ self.wait_not_mounted("Filesystem")
+
+ # Mount, this uses the stored passphrase for unlocking
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog_wait_open()
+ self.dialog_wait_val("mount_point", mount_point_secret)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assert_not_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_not_in_configuration(cleartext_dev, "fstab", "opts", "noauto")
+ self.wait_mounted("ext4 filesystem")
+
+ b.click(self.card_desc_action("Encryption", "Options"))
+ self.dialog({"options": "weird,options"})
+ self.assert_in_configuration(dev, "crypttab", "options", "weird,options")
+
+ # Change stored passphrase
+ b.click(self.card_desc_action("Encryption", "Stored passphrase"))
+ self.dialog({"passphrase": "wrong-passphrase"})
+ self.assert_in_configuration(dev, "crypttab", "passphrase-path", passphrase_path)
+ self.assertEqual(m.execute(f"cat {passphrase_path}"), "wrong-passphrase")
+
+ # Unmount it
+ b.click(self.card_button("ext4 filesystem", "Unmount"))
+ self.confirm()
+ self.assert_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_in_child_configuration(dev, "fstab", "opts", "noauto")
+ self.wait_not_mounted("Filesystem")
+
+ # Mount, this tries the wrong passphrase but eventually prompts.
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog_wait_open()
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Failed to activate device:")
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assert_not_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_not_in_configuration(cleartext_dev, "fstab", "opts", "noauto")
+ self.wait_mounted("ext4 filesystem")
+
+ # Remove passphrase
+ b.click(self.card_desc_action("Encryption", "Stored passphrase"))
+ self.dialog({"passphrase": ""})
+ self.assert_in_configuration(dev, "crypttab", "passphrase-path", "")
+
+ # Unmount it
+ b.click(self.card_button("ext4 filesystem", "Unmount"))
+ self.confirm()
+ self.assert_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_in_child_configuration(dev, "fstab", "opts", "noauto")
+ self.wait_not_mounted("Filesystem")
+
+ # Mount it readonly. This asks for a passphrase.
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog({"mount_options.ro": True,
+ "passphrase": "vainu-reku-toma-rolle-kaja"})
+ self.wait_mounted("ext4 filesystem")
+ self.assertIn("ro", m.execute(f"findmnt -s -n -o OPTIONS {mount_point_secret}"))
+ self.assert_in_configuration(dev, "crypttab", "options", "readonly")
+ self.assert_in_configuration(cleartext_dev, "fstab", "opts", "ro")
+
+ # Delete the partition.
+ self.click_card_dropdown("Partition", "Delete")
+ self.confirm()
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+ # luksmeta-monitor-hack.py races with the partition deletion
+ self.allow_journal_messages('Unknown device .*: No such file or directory')
+
+ self.assertEqual(m.execute("grep -v ^# /etc/crypttab || true").strip(), "")
+ self.assertEqual(m.execute(f"grep {mount_point_secret} /etc/fstab || true"), "")
+
+ # luksmeta-monitor-hack.py might leave a udevadm process
+ # behind, so let's check that the session goes away cleanly
+ # after a logout.
+
+ b.logout()
+ testlib.wait(lambda: m.execute("(loginctl list-users | grep admin) || true") == "")
+
+ # FIXME: race condition after unmounting; hard to investigate, re-check after storage redesign
+ self.allow_browser_errors("validateDOMNesting.*cannot appear as a child.*<tr> ul")
+
+ def testNoFsys(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Add a disk and format it with luks, but without filesystem
+ disk = self.add_ram_disk()
+ self.click_card_row("Storage", name=disk)
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "empty",
+ "crypto": self.default_crypto_type,
+ "passphrase": "vainu-reku-toma-rolle-kaja",
+ "passphrase2": "vainu-reku-toma-rolle-kaja"})
+ b.wait_visible(self.card("Locked data"))
+
+ # Make it readonly
+
+ # The locking above changes crypttab asynchronously, and there
+ # is no indication in the UI when cockpit has caught up with
+ # that. So we retry the dialog when it looks like Cockpit is
+ # out of synch...
+
+ self.dialog_with_error_retry(trigger=lambda: b.click(self.card_desc_action("Encryption", "Options")),
+ values={"options": "readonly"},
+ errors=["Didn't find entry to remove"])
+ self.assertNotEqual(m.execute("grep readonly /etc/crypttab"), "")
+
+ # Unlock it
+ b.click(self.card_button("Locked data", "Unlock"))
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja"})
+
+ # Try to format it, just for kicks
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "mount_point": "/run/foo"})
+ b.wait_visible(self.card("ext4 filesystem"))
+ self.wait_mounted("ext4 filesystem")
+
+ # Now create a empty, encrypted partition
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.dialog({"type": "gpt"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition")
+ self.dialog({"type": "empty",
+ "crypto": self.default_crypto_type,
+ "passphrase": "vainu-reku-toma-rolle-kaja",
+ "passphrase2": "vainu-reku-toma-rolle-kaja"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 2), "Locked data (encrypted)")
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Unlock")
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 2), "Unformatted data (encrypted)")
+
+ def testKeepKeys(self):
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Add a disk and format it with luks and a filesystem
+
+ disk = self.add_ram_disk()
+ self.click_card_row("Storage", name=disk)
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "crypto": self.default_crypto_type,
+ "passphrase": "vainu-reku-toma-rolle-kaja",
+ "passphrase2": "vainu-reku-toma-rolle-kaja",
+ "mount_point": "/run/foo"})
+ b.wait_visible(self.card("ext4 filesystem"))
+
+ # Format it again but keep the keys
+
+ self.click_card_dropdown("ext4 filesystem", "Format")
+ self.dialog({"type": "ext4",
+ "crypto": " keep",
+ "mount_point": "/run/foo"})
+ b.wait_visible(self.card("ext4 filesystem"))
+
+ # Unmount (and lock) it
+
+ b.click(self.card_button("ext4 filesystem", "Unmount"))
+ self.confirm()
+ self.wait_not_mounted("Filesystem")
+
+ # Format it again and keep the keys. Because it's locked, we
+ # need the old passphrase.
+
+ self.click_card_dropdown("Filesystem", "Format")
+ self.dialog({"type": "ext4",
+ "crypto": " keep",
+ "old_passphrase": "vainu-reku-toma-rolle-kaja",
+ "mount_point": "/run/foo"})
+ self.wait_mounted("ext4 filesystem")
+
+
+class TestStorageLuksDestructive(storagelib.StorageCase):
+
+ # LUKS uses memory hard PBKDF, 1 GiB is not enough; see https://bugzilla.redhat.com/show_bug.cgi?id=1881829
+ provision = {
+ "0": {"memory_mb": 1536}
+ }
+
+ def testLuks1Slots(self):
+ self.allow_journal_messages("Device is not initialized.*", ".*could not be opened.")
+ m = self.machine
+ b = self.browser
+
+ # This should work without any of the Clevis stuff.
+ m.execute("rm -f /usr/bin/luksmeta /usr/bin/clevis*")
+
+ mount_point_secret = "/run/secret"
+
+ error_base = "Error unlocking /dev/sda: Failed to activate device: "
+ error_messages = [error_base + "Operation not permitted",
+ error_base + "Incorrect passphrase."]
+
+ self.login_and_go("/storage")
+
+ m.add_disk("50M", serial="MYDISK")
+ dev = "/dev/sda"
+ self.click_card_row("Storage", name=dev)
+ # create volume and passphrase
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "crypto": "luks1",
+ "name": "ENCRYPTED",
+ "mount_point": mount_point_secret,
+ "passphrase": "vainu-reku-toma-rolle-kaja",
+ "passphrase2": "vainu-reku-toma-rolle-kaja"},
+ secondary=True)
+ self.wait_not_mounted("Filesystem")
+ b.wait_text(self.card_desc("Encryption", "Encryption type"), "LUKS1")
+ b.wait_in_text(self.card_desc("Encryption", "Options"), "nofail")
+
+ uuid = m.execute(f"cryptsetup luksUUID {dev}").strip()
+ cleartext_dev = "/dev/mapper/luks-" + uuid
+
+ # add one more passphrase
+ panel = "#encryption-keys "
+ b.wait_visible(panel)
+ b.click(self.card("Encryption") + " [aria-label='Add']")
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ self.dialog_set_val("new_passphrase", "vainu-reku-toma-rolle-kaja-1")
+ self.dialog_set_val("new_passphrase2", "vainu-reku-toma-rolle-kaja-1")
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ # unlock with first passphrase
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja"})
+ self.wait_mounted("ext4 filesystem")
+ self.assert_not_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_not_in_configuration(cleartext_dev, "fstab", "opts", "noauto")
+ # unlock with second passphrase
+ b.click(self.card_button("ext4 filesystem", "Unmount"))
+ self.confirm()
+ self.assert_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_in_child_configuration(dev, "fstab", "opts", "noauto")
+ self.wait_not_mounted("Filesystem")
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja-1"})
+ self.assert_not_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_not_in_configuration(cleartext_dev, "fstab", "opts", "noauto")
+ self.wait_mounted("ext4 filesystem")
+ # delete second key slot
+ b.click(panel + "li:nth-child(2) button[aria-label='Remove']")
+ # do not accept the same passphrase
+ b.set_input_text("#remove-passphrase", "vainu-reku-toma-rolle-kaja-1")
+ b.click("button:contains('Remove')")
+ b.wait_in_text(".pf-v5-c-alert__title", "No key available with this passphrase.")
+ # delete with passphrase from slot 0
+ b.set_input_text("#remove-passphrase", "vainu-reku-toma-rolle-kaja")
+ b.click("button:contains('Remove')")
+ with b.wait_timeout(30):
+ b.wait_not_present("#remove-passphrase")
+ # check that it is not possible to unlock with deleted passphrase
+ b.click(self.card_button("ext4 filesystem", "Unmount"))
+ self.confirm()
+ self.assert_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_in_child_configuration(dev, "fstab", "opts", "noauto")
+ self.wait_not_mounted("Filesystem")
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog_wait_open()
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja-1")
+ self.dialog_apply()
+ b.wait_visible(".pf-v5-c-alert")
+ self.assertIn(b.text("h4.pf-v5-c-alert__title:not(span)").split("Danger alert:", 1).pop(), error_messages)
+ self.dialog_cancel()
+
+ # add more passphrases, seven exactly, to reach the limit of eight for LUKSv1
+ for i in range(1, 8):
+ b.click(self.card("Encryption") + " [aria-label='Add']")
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ self.dialog_set_val("new_passphrase", f"vainu-reku-toma-rolle-kaja-{i}")
+ self.dialog_set_val("new_passphrase2", f"vainu-reku-toma-rolle-kaja-{i}")
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ # check if add button is inactive
+ b.wait_visible(self.card("Encryption") + " [aria-label='Add']:disabled")
+ # check if edit button is inactive
+ slots_row = panel + " ul li:first-child"
+ b.wait_visible(slots_row + " button:disabled")
+
+ # remove one slot
+ slots_list = panel + " ul "
+ b.wait_visible(".pf-v5-c-data-list__cell:contains('Slot 7')")
+ b.click(slots_list + "li:last-child button[aria-label='Remove']")
+ b.set_input_text("#remove-passphrase", "vainu-reku-toma-rolle-kaja-6")
+ b.click("button:contains('Remove')")
+ with b.wait_timeout(30):
+ b.wait_not_present("#remove-passphrase")
+ # check if buttons have become enabled after removing last slot
+ b.wait_not_present(slots_list + ":disabled")
+ b.wait_not_present(panel + ":disabled")
+ # remove slot 0, with the original passphrase
+ b.click(slots_list + "li:nth-child(1) button[aria-label='Remove']")
+ b.click("#force-remove-passphrase")
+ b.click("button:contains('Remove')")
+ with b.wait_timeout(30):
+ b.wait_not_present("#remove-passphrase")
+ # check that it is not possible to unlock with deleted passphrase
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog_wait_open()
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ b.wait_visible(".pf-v5-c-alert")
+ self.assertIn(b.text("h4.pf-v5-c-alert__title:not(span)").split("Danger alert:", 1).pop(), error_messages)
+ self.dialog_cancel()
+ # change one of the passphrases
+ b.wait_visible(slots_list + "li:last-child [aria-label='Edit']")
+ b.click(slots_list + "li:last-child [aria-label='Edit']")
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ self.dialog_set_val("old_passphrase", "vainu-reku-toma-rolle-kaja-6")
+ self.dialog_set_val("new_passphrase", "vainu-reku-toma-rolle-kaja-8")
+ self.dialog_set_val("new_passphrase2", "vainu-reku-toma-rolle-kaja-8")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ # unlock volume with the negwly created passphrase
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja-8"})
+ self.wait_mounted("ext4 filesystem")
+ self.assert_not_in_configuration(dev, "crypttab", "options", "noauto")
+ self.assert_not_in_configuration(cleartext_dev, "fstab", "opts", "noauto")
+
+ def testReboot(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Add a disk and format it with luks and a filesystem, then
+ # reboot and check that the filesystem is mounted.
+
+ dev = "/dev/sda"
+ m.add_disk("50M", serial="MYDISK")
+ self.click_card_row("Storage", name=dev)
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "crypto": self.default_crypto_type,
+ "passphrase": "vainu-reku-toma-rolle-kaja",
+ "passphrase2": "vainu-reku-toma-rolle-kaja",
+ "mount_point": "/run/foo"})
+ self.wait_mounted("ext4 filesystem")
+
+ self.setup_systemd_password_agent("vainu-reku-toma-rolle-kaja")
+ m.reboot()
+ m.start_cockpit()
+ b.relogin()
+ b.enter_page("/storage")
+
+ self.wait_mounted("ext4 filesystem")
+
+
+class TestStorageNBDE(storagelib.StorageCase, packagelib.PackageCase):
+ provision = {
+ "0": {"address": "10.111.112.1/20", "memory_mb": 2048},
+ "tang": {"address": "10.111.112.5/20"}
+ }
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ # Only Arch gets it right...
+ need_fixing = (m.image != "arch")
+
+ mount_point_secret = "/run/secret"
+
+ tang_m = self.machines["tang"]
+ tang_m.execute("systemctl start tangd.socket")
+ tang_m.execute("firewall-cmd --add-port 80/tcp")
+
+ if need_fixing:
+ self.addPackageSet("clevis")
+ self.enableRepo()
+
+ self.login_and_go("/storage")
+
+ # Add a disk and format it with luks
+ dev = "/dev/sda"
+ m.add_disk("50M", serial="MYDISK")
+ self.click_card_row("Storage", name=dev)
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "crypto": self.default_crypto_type,
+ "name": "ENCRYPTED",
+ "mount_point": mount_point_secret,
+ "passphrase": "vainu-reku-toma-rolle-kaja",
+ "passphrase2": "vainu-reku-toma-rolle-kaja"},
+ secondary=True)
+ self.wait_not_mounted("Filesystem")
+
+ b.wait_in_text(self.card_desc("Encryption", "Options"), "nofail")
+ panel = "#encryption-keys "
+ b.wait_visible(panel)
+ b.wait_in_text(panel + "ul li:nth-child(1)", "Passphrase")
+
+ # Add a key
+ #
+ b.click(self.card("Encryption") + " [aria-label='Add']")
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ self.dialog_set_val("type", "tang")
+ self.dialog_set_val("tang_url", "10.111.112.5")
+ self.dialog_set_val("passphrase", "wrong-passphrase")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash with the Tang server")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ if need_fixing:
+ b.wait_in_text("#dialog", "Add Network Bound Disk Encryption")
+ b.wait_in_text("#dialog", "The clevis-systemd package must be installed")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "No key available with this passphrase.")
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_visible(panel + "ul li:nth-child(2)")
+ b.wait_in_text(panel + "ul li:nth-child(2)", "10.111.112.5")
+
+ # Adding the key should add "_netdev" options
+ #
+ b.wait_in_text(self.card_desc("Filesystem", "Mount point"), "after network")
+ b.wait_in_text(self.card_desc("Encryption", "Options"), "_netdev")
+
+ # Mount it. This should succeed without passphrase.
+ #
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog_wait_open()
+ self.dialog_wait_val("mount_point", mount_point_secret)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.wait_mounted("ext4 filesystem")
+
+ # Edit the key, without providing an existing passphrase
+ #
+ b.click(panel + "ul li:nth-child(2) [aria-label='Edit']")
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ self.dialog_wait_val("tang_url", "10.111.112.5")
+ self.dialog_set_val("tang_url", "http://10.111.112.5/")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash with the Tang server")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_in_text(panel + "ul li:nth-child(2)", "http://10.111.112.5/")
+
+ # Reset the options so that we can check that they get added
+ # also when a second key is added.
+ #
+ b.click(self.card_desc("ext4 filesystem", "Mount point") + " button")
+ self.dialog({"at_boot": "nofail"})
+ b.wait_in_text(self.card_desc("Encryption", "Options"), "nofail")
+
+ if need_fixing:
+ # Break things again, including PackageKit
+ m.execute("if type dnf; then dnf remove -y clevis-systemd; else apt-get purge -y clevis-systemd; fi")
+ m.execute("systemctl mask --now packagekit")
+
+ # Add a second key, this should try to fix things again, but
+ # we have to help with the package install
+ #
+ b.click(self.card("Encryption") + " [aria-label='Add']")
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ self.dialog_set_val("type", "tang")
+ self.dialog_set_val("tang_url", "http://10.111.112.5")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash with the Tang server")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ if need_fixing:
+ b.wait_in_text("#dialog", "Add Network Bound Disk Encryption")
+ b.wait_in_text("#dialog", "The clevis-systemd package must be installed")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "PackageKit is not installed")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Manually install the missing package, Cockpit should
+ # be happy with that even when PackageKit is not
+ # available. Also disable the unit, for variety.
+ # Cockpit will enable it without needing explicit
+ # confirmation.
+ #
+ m.execute("if type dnf; then dnf install -y clevis-systemd; else apt-get install -y clevis-systemd; fi")
+ m.execute("systemctl disable --now clevis-luks-askpass.path")
+
+ b.click(self.card("Encryption") + " [aria-label='Add']")
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ self.dialog_set_val("type", "tang")
+ self.dialog_set_val("tang_url", "http://10.111.112.5")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash with the Tang server")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+
+ self.dialog_wait_close()
+
+ b.wait_visible(panel + "ul li:nth-child(3)")
+ b.wait_in_text(panel + "ul li:nth-child(3)", "http://10.111.112.5")
+
+ # This should bring the options back.
+ #
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "after network")
+ b.wait_in_text(self.card_desc("Encryption", "Options"), "_netdev")
+
+ # Reboot
+ #
+ m.reboot()
+ m.start_cockpit()
+ b.relogin()
+ b.enter_page("/storage")
+
+ self.wait_mounted("ext4 filesystem")
+
+ # Remove one key on client
+ #
+ b.click(panel + 'ul li:contains("Slot 1") button[aria-label="Remove"]')
+ self.confirm()
+ b.wait_not_present(panel + 'ul li:contains("Slot 1")')
+
+ @testlib.skipImage("TODO: don't know how to encrypt the rootfs", "debian-*", "ubuntu-*", "arch")
+ @testlib.timeout(1200)
+ def testRootReboot(self):
+ m = self.machine
+ b = self.browser
+
+ tang_m = self.machines["tang"]
+ tang_m.execute("systemctl start tangd.socket")
+ tang_m.execute("firewall-cmd --add-port 80/tcp")
+
+ try:
+ self.encrypt_root("einszweidrei")
+ except Exception:
+ console_screenshot(m, "failed-encrypt.ppm")
+ raise
+
+ self.assertIn("crypt", m.execute("lsblk -snlo TYPE $(findmnt -no SOURCE /)"))
+
+ self.addPackageSet("clevis")
+ self.enableRepo()
+
+ self.login_and_go("/storage")
+
+ # Add a clevis key and then reboot.
+ #
+ # We also remove the original passphrase in order to be sure
+ # that it was in fact clevis that has unlocked the rootfs, and
+ # not the magic provided by "encrypt_root"
+
+ self.click_card_row("Storage", location="root")
+
+ panel = "#encryption-keys "
+ b.wait_visible(panel)
+ b.wait_in_text(panel + "ul li:nth-child(1)", "Passphrase")
+
+ with b.wait_timeout(360):
+ b.click(self.card("Encryption") + " [aria-label='Add']")
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ self.dialog_set_val("type", "tang")
+ self.dialog_set_val("tang_url", "10.111.112.5")
+ self.dialog_set_val("passphrase", "einszweidrei")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash with the Tang server")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Add Network Bound Disk Encryption")
+ b.wait_in_text("#dialog", "The clevis-dracut package must be installed")
+ b.wait_in_text("#dialog", "The initrd must be regenerated")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.click(panel + "ul li:nth-child(1) button[aria-label='Remove']")
+ b.click("#force-remove-passphrase")
+ b.click("button:contains('Remove')")
+ b.wait_in_text(panel + "ul li:nth-child(1)", "Keyserver")
+
+ # Tell the initrd to configure our special inter-machine
+ # network that has the "tang" machine.
+ #
+ m.execute("grubby --update-kernel=ALL --args='ip=10.111.112.1::10.111.112.1:255.255.255.0::eth1:off'")
+
+ try:
+ m.reboot(timeout_sec=300)
+ except Exception:
+ console_screenshot(m, "failed-reboot.ppm")
+ raise
+
+ m.start_cockpit()
+ b.relogin()
+ b.enter_page("/storage")
+
+ self.assertIn("crypt", m.execute("lsblk -snlo TYPE $(findmnt -no SOURCE /)"))
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-lvm2 b/test/verify/check-storage-lvm2
new file mode 100755
index 0000000..742a295
--- /dev/null
+++ b/test/verify/check-storage-lvm2
@@ -0,0 +1,926 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import math
+import os
+import unittest
+
+import storagelib
+import testlib
+
+
+# We use basename a lot, let's reduce the noise
+def bn(path):
+ return os.path.basename(path)
+
+
+@testlib.nondestructive
+class TestStorageLvm2(storagelib.StorageCase):
+
+ def can_do_layouts(self):
+ return self.storaged_version >= [2, 10]
+
+ def skip_if_no_layouts(self):
+ if not self.can_do_layouts():
+ raise unittest.SkipTest("raid layouts not supported")
+
+ def testLvm(self):
+ m = self.machine
+ b = self.browser
+
+ mount_point_one = "/run/one"
+ mount_point_thin = "/run/thin"
+
+ self.login_and_go("/storage")
+
+ dev_1 = self.add_ram_disk()
+ dev_2 = self.add_loopback_disk(name="loop10")
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+ b.wait_visible(self.card_row("Storage", name=dev_2))
+
+ # Create a volume group out of two disks
+ m.execute(f"vgcreate TEST1 {dev_1} {dev_2}")
+ # just in case the test fails
+ self.addCleanupVG("TEST1")
+ self.click_card_row("Storage", name="TEST1")
+ b.wait_visible(self.card_row("LVM2 volume group", name=dev_1))
+ b.wait_visible(self.card_row("LVM2 volume group", name=dev_2))
+
+ # Create two logical volumes
+ m.execute("lvcreate TEST1 -n one -L 20m")
+ b.wait_visible(self.card_row("LVM2 logical volumes", name="one"))
+ m.execute("lvcreate TEST1 -n two -L 20m")
+ b.wait_visible(self.card_row("LVM2 logical volumes", name="two"))
+
+ # Deactivate one
+ m.execute("lvchange TEST1/one -a n")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "Inactive logical volume")
+
+ # and remove it
+ m.execute("until lvremove -f TEST1/one; do sleep 5; done")
+ b.wait_not_present(self.card_row("LVM2 logical volumes", name="one"))
+
+ # remove a disk from the volume group
+ m.execute(f"pvmove {dev_2} &>/dev/null || true")
+ m.execute(f"vgreduce TEST1 {dev_2}")
+ b.wait_not_present(self.card_row("LVM2 volume group", name=dev_2))
+
+ # The remaining lone disk is not removable
+ b.click(self.dropdown_toggle(self.card_row("LVM2 volume group", name=dev_1)))
+ b.wait_visible(self.dropdown_action(self.card_row("LVM2 volume group", name=dev_1), "Remove") + "[disabled]")
+ b.wait_text(self.dropdown_description(self.card_row("LVM2 volume group", name=dev_1), "Remove"),
+ "Last cannot be removed")
+
+ # Wipe the disk and make sure lvmetad forgets about it. This
+ # might help with reusing it in the second half of this test.
+ #
+ # HACK - the pvscan is necessary because of
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1063813
+ #
+ m.execute(f"wipefs -a {dev_2}")
+ m.execute(f"pvscan --cache {dev_2}")
+
+ # Thin volumes
+ m.execute("lvcreate TEST1 --thinpool pool -L 20m")
+ b.wait_visible(self.card_row("LVM2 logical volumes", name="pool"))
+ m.execute("lvcreate -T TEST1/pool -n thin -V 100m")
+ b.wait_visible(self.card_row("LVM2 logical volumes", name="thin"))
+ self.click_card_row("LVM2 logical volumes", name="pool")
+ b.wait_visible(self.card_row("Thinly provisioned LVM2 logical volumes", name="thin"))
+
+ m.execute("dd if=/dev/urandom of=/dev/mapper/TEST1-thin bs=1M count=10 status=none")
+ b.wait_text(self.card_desc("Pool for thinly provisioned LVM2 logical volumes", "Data used"), "50%")
+ m.execute("until lvremove -f TEST1/thin; do sleep 5; done")
+ b.wait_text(self.card_desc("Pool for thinly provisioned LVM2 logical volumes", "Data used"), "0%")
+
+ # remove the volume group
+ b.go("#/")
+ m.execute("vgremove -f TEST1")
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name="TEST1"))
+
+ # create volume group in the UI
+
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
+ expect=lambda: (self.dialog_is_present('disks', dev_1) and
+ self.dialog_is_present('disks', dev_2) and
+ self.dialog_check({"name": "vgroup0"})),
+ values={"disks": {dev_1: True,
+ dev_2: True}})
+
+ # just in case the test fails
+ self.addCleanupVG("vgroup0")
+ b.wait_visible(self.card_row("Storage", name="vgroup0"))
+
+ # Check that the next name is "vgroup1"
+ self.click_devices_dropdown("Create LVM2 volume group")
+ self.dialog_wait_open()
+ self.dialog_wait_val("name", "vgroup1")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ self.click_card_row("Storage", name="vgroup0")
+
+ # create a logical volume
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog(expect={"name": "lvol0"},
+ values={"purpose": "block",
+ "size": 20})
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
+
+ # check that the next default name is "lvol1"
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog_wait_open()
+ self.dialog_wait_val("name", "lvol1")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # rename lvol0
+ self.click_card_row("LVM2 logical volumes", 1)
+ b.click(self.card_desc_action("LVM2 logical volume", "Name"))
+ self.dialog_wait_open()
+ self.dialog({"name": "lvol1"})
+ b.wait_text(self.card_desc("LVM2 logical volume", "Name"), "lvol1")
+
+ if self.can_do_layouts():
+ # check that it is stored on dev_2
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(dev_2))
+
+ # grow it
+ b.click(self.card_button("LVM2 logical volume", "Grow"))
+ self.dialog({"size": 30})
+
+ # format and mount it
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "mount_point": mount_point_one})
+ self.wait_mounted("ext4 filesystem")
+ self.assert_in_configuration("/dev/vgroup0/lvol1", "fstab", "dir", mount_point_one)
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), mount_point_one)
+
+ # unmount it
+ b.click(self.card_button("ext4 filesystem", "Unmount"))
+ self.confirm()
+ self.wait_not_mounted("ext4 filesystem")
+
+ # shrink it, this time with a filesystem.
+ b.click(self.card_button("LVM2 logical volume", "Shrink"))
+ self.dialog({"size": 10})
+
+ # delete it
+ self.click_card_dropdown("LVM2 logical volume", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("LVM2 logical volumes"))
+ b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol1"))
+ self.assertEqual(m.execute(f"grep {mount_point_one} /etc/fstab || true"), "")
+
+ # remove disk2
+ self.click_dropdown(self.card_row("LVM2 volume group", name=dev_2), "Remove")
+ b.wait_not_present(self.card_row("LVM2 volume group", name=dev_2))
+ b.wait_in_text(self.card_desc("LVM2 volume group", "Capacity"), "50.3 MB")
+
+ # create thin pool and volume
+ # the pool will be maximum size, 50.3 MB
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog(expect={"size": 50.3},
+ values={"purpose": "pool",
+ "name": "pool",
+ "size": 38})
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "pool")
+
+ self.click_dropdown(self.card_row("LVM2 logical volumes", 1),
+ "Create new thinly provisioned logical volume")
+ self.dialog(expect={"name": "lvol0"},
+ values={"name": "thin",
+ "size": 50})
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "thin")
+
+ # add a disk and resize the pool
+ b.click(self.card_button("LVM2 volume group", "Add physical volume"))
+ self.dialog_wait_open()
+ self.dialog_set_val('disks', {dev_2: True})
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_visible(self.card_row("LVM2 volume group", name=dev_2))
+ # this is sometimes 96, sometimes 100 MB
+ cap = self.card_desc("LVM2 volume group", "Capacity")
+ b.wait_js_func("(sel => { const c = Number(ph_text(sel).split(' ')[0]); return c >= 96 && c <= 101 })", cap)
+
+ self.click_card_row("LVM2 logical volumes", name="pool")
+ b.click(self.card_button("Pool for thinly provisioned LVM2 logical volumes", "Grow"))
+ self.dialog({"size": 70})
+
+ # use almost all of the pool by erasing the thin volume
+ self.click_card_row("Thinly provisioned LVM2 logical volumes", 1)
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"erase.on": True,
+ "type": "ext4",
+ "mount_point": mount_point_thin})
+ self.assert_in_configuration("/dev/vgroup0/thin", "fstab", "dir", mount_point_thin)
+ b.click(self.card_parent_link())
+ b.wait_text(self.card_row_col("Thinly provisioned LVM2 logical volumes", 1, 3), mount_point_thin)
+
+ b.assert_pixels('body', "pool-page")
+
+ # remove pool
+ self.click_card_dropdown("Pool for thinly provisioned LVM2 logical volumes", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("LVM2 logical volumes"))
+ b.wait_not_present(self.card_row("LVM2 logical volumes", name="pool"))
+ self.assertEqual(m.execute(f"grep {mount_point_thin} /etc/fstab || true"), "")
+
+ # make another logical volume and format it, just so that we
+ # can see whether deleting the volume group will clean it all
+ # up.
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog(expect={"name": "lvol0"},
+ values={"purpose": "block"})
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
+ self.click_card_row("LVM2 logical volumes", 1)
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "mount_point": mount_point_one})
+ self.assert_in_configuration("/dev/vgroup0/lvol0", "fstab", "dir", mount_point_one)
+ b.click(self.card_parent_link())
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "ext4 filesystem")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 3), mount_point_one)
+
+ b.wait_visible(self.card_row_col("LVM2 logical volumes", 1, 4) + " .usage-bar[role=progressbar]")
+ b.assert_pixels('body', "page",
+ # Usage numbers are not stable and also cause
+ # the table columns to shift. The usage bars
+ # are not stable but are always the same size,
+ # so it is good enough to ignore them.
+ mock={self.card_desc("LVM2 volume group", "UUID"): "a12978a1-5d6e-f24f-93de-11789977acde",
+ ".usage-text": "---"},
+ ignore=[".usage-bar"])
+
+ # rename volume group
+ b.click(self.card_desc_action("LVM2 volume group", "Name"))
+ self.dialog_wait_open()
+ self.dialog_set_val("name", "vgroup1")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ # remove volume group
+ self.click_card_dropdown("LVM2 volume group", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name="vgroup1"))
+ self.assertEqual(m.execute(f"grep {mount_point_thin} /etc/fstab || true"), "")
+
+ def testUnpartitionedSpace(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_ram_disk()
+ disk2 = self.add_loopback_disk()
+ b.wait_visible(self.card_row("Storage", name=disk1))
+ b.wait_visible(self.card_row("Storage", name=disk2))
+
+ # Put a partition table on disk1 and disk2
+ m.execute(f'parted -s {disk1} mktable gpt')
+ m.execute(f'parted -s {disk2} mktable gpt')
+
+ # Create a volume group out of disk1
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown('Create LVM2 volume group'),
+ expect=lambda: self.dialog_is_present('disks', disk1) and
+ self.dialog_is_present('disks', "unpartitioned space on Linux scsi_debug"),
+ values={"disks": {disk1: True}})
+
+ self.click_card_row("Storage", name="vgroup0")
+
+ # Check the we are really using a partition on disk1 now
+ b.wait_in_text(self.card_row_col("LVM2 volume group", 1, 2), "Partition - Linux scsi_debug")
+
+ # Add the unused space of disk2
+ self.dialog_with_retry(trigger=lambda: b.click(self.card_button("LVM2 volume group",
+ "Add physical volume")),
+ expect=lambda: self.dialog_is_present(
+ 'disks', "unpartitioned space on " + disk2),
+ values={"disks": {disk2: True}})
+ b.wait_in_text(self.card_row_col("LVM2 volume group", 1, 2), "Partition - Block device")
+
+ def testSnapshots(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk(100)
+ b.wait_visible(self.card_row("Storage", name=disk))
+
+ m.execute(f"vgcreate TEST {disk}")
+
+ self.addCleanupVG("TEST")
+ self.click_card_row("Storage", name="TEST")
+
+ # Create a thinpool and a thin volume in it
+
+ m.execute("lvcreate TEST --thinpool pool -L 10m")
+ m.execute("lvcreate -T TEST/pool -n thin -V 30m")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "thin")
+
+ # Create a logical volume and take a snapshot of it. We will
+ # later check that the snapshot isn't shown.
+
+ m.execute("lvcreate TEST -n lvol0 -L 10m")
+ m.execute("lvcreate -s -n snap0 -L 10m TEST/lvol0")
+
+ # the above lvol0 will be the new first entry in the table, so
+ # TEST/thin moves to he third row
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "pool")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 3, 1), "thin")
+
+ # Take a snapshot of the thin volume and check that it appears
+
+ self.click_dropdown(self.card_row("LVM2 logical volumes", 3), "Create snapshot")
+ self.dialog({"name": "mysnapshot"})
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 3, 1), "mysnapshot")
+
+ # Now check that the traditional snapshot is not shown. We do
+ # this here to be sure that Cockpit is fully caught up and has
+ # actually ignored it instead of just not having gotten around
+ # to showing it.
+
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
+ b.wait_not_present(self.card_row("LVM2 logical volumes", name="snap0"))
+
+ def testRaid(self):
+ b = self.browser
+
+ self.skip_if_no_layouts()
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_ram_disk()
+ disk2 = self.add_loopback_disk(name="loop10")
+ disk3 = self.add_loopback_disk(name="loop11")
+ disk4 = self.add_loopback_disk(name="loop12")
+
+ # Make a volume group with four physical volumes
+
+ with b.wait_timeout(60):
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
+ expect=lambda: (self.dialog_is_present('disks', disk1) and
+ self.dialog_is_present('disks', disk2) and
+ self.dialog_is_present('disks', disk3) and
+ self.dialog_is_present('disks', disk4) and
+ self.dialog_check({"name": "vgroup0"})),
+ values={"disks": {disk1: True,
+ disk2: True,
+ disk3: True,
+ disk4: True}})
+
+ self.addCleanupVG("vgroup0")
+
+ # Make a raid5 on three PVs, using about half of each
+
+ self.click_card_row("Storage", name="vgroup0")
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog_wait_open()
+ self.dialog_wait_val("name", "lvol0")
+ self.dialog_set_val("purpose", "block")
+ self.dialog_set_val("layout", "raid5")
+ self.dialog_set_val("pvs", {disk1: False})
+ self.dialog_set_val("size", 40)
+ b.assert_pixels("#dialog", "create-raid5")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
+ self.click_card_row("LVM2 logical volumes", 1)
+
+ b.wait_text(self.card_desc("LVM2 logical volume", "Layout"), "Distributed parity (RAID 5)")
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk2))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk3))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk4))
+
+ b.assert_pixels(self.card("LVM2 logical volume"), "raid5-card")
+
+ # Make linear volume to fully use second PV
+
+ b.click(self.card_parent_link())
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog(expect={"name": "lvol1"},
+ values={"purpose": "block",
+ "layout": "linear",
+ "pvs": {disk1: False, disk3: False, disk4: False}})
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "lvol1")
+ self.click_card_row("LVM2 logical volumes", 2)
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk2))
+
+ b.click(self.card_parent_link())
+ self.click_card_row("LVM2 logical volumes", 1)
+
+ # Grow raid5 to about maximum
+ b.click(self.card_button("LVM2 logical volume", "Grow"))
+ self.dialog_wait_open()
+ self.dialog_set_val("size", 80)
+ b.assert_pixels("#dialog", "grow-raid5")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk1))
+
+ # Check that each PV is used exactly once.
+ card = self.card("LVM2 logical volume")
+ b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk2)}')", 1)
+ b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk3)}')", 1)
+ b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk4)}')", 1)
+ b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk1)}')", 1)
+ b.assert_pixels(card, "raid5-card2")
+
+ def testBrokenLinear(self):
+ b = self.browser
+
+ self.skip_if_no_layouts()
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_ram_disk()
+ disk2 = self.add_loopback_disk()
+ disk3 = self.add_loopback_disk()
+
+ # Make a volume group with three physical volumes
+
+ with b.wait_timeout(60):
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
+ expect=lambda: (self.dialog_is_present('disks', disk1) and
+ self.dialog_is_present('disks', disk2) and
+ self.dialog_is_present('disks', disk3) and
+ self.dialog_check({"name": "vgroup0"})),
+ values={"disks": {disk1: True,
+ disk2: True,
+ disk3: True}})
+
+ self.addCleanupVG("vgroup0")
+
+ # Make a linear volume on two of them
+
+ self.click_card_row("Storage", name="vgroup0")
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog(expect={"name": "lvol0"},
+ values={"purpose": "block",
+ "layout": "linear",
+ "pvs": {disk2: False}})
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
+ self.click_card_row("LVM2 logical volumes", 1)
+
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk1))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk3))
+
+ # Kill one PV
+
+ self.force_remove_disk(disk1)
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"),
+ "This logical volume has lost some of its physical volumes and can no longer be used.")
+ b.wait_not_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk1))
+
+ b.click(self.card_parent_link())
+ b.wait_in_text(".pf-v5-c-alert", "This volume group is missing some physical volumes.")
+ b.wait_visible(self.card_desc_action("LVM2 volume group", "Name") + ":disabled")
+ b.wait_visible(self.card_row("LVM2 logical volumes", name="lvol0") + ' .ct-icon-times-circle')
+
+ # Dismiss alert, this will delete the volume
+
+ b.click(".pf-v5-c-alert button:contains(Dismiss)")
+ self.dialog_wait_open()
+ b.wait_in_text("#dialog", "/dev/vgroup0/lvol0")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.wait_not_present(".pf-v5-c-alert")
+ b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0"))
+
+ def testMaxLayoutSizes(self):
+ b = self.browser
+
+ self.skip_if_no_layouts()
+
+ # Make sure that Cockpit gets the computation of the maximum
+ # size right.
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_loopback_disk(name="loop10")
+ disk2 = self.add_loopback_disk(name="loop11")
+ disk3 = self.add_loopback_disk(name="loop12")
+ disk4 = self.add_loopback_disk(name="loop13")
+ disk5 = self.add_loopback_disk(name="loop14")
+ disk6 = self.add_loopback_disk(name="loop15")
+
+ with b.wait_timeout(60):
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
+ expect=lambda: (self.dialog_is_present('disks', disk1) and
+ self.dialog_is_present('disks', disk2) and
+ self.dialog_is_present('disks', disk3) and
+ self.dialog_is_present('disks', disk4) and
+ self.dialog_is_present('disks', disk5) and
+ self.dialog_is_present('disks', disk6) and
+ self.dialog_check({"name": "vgroup0"})),
+ values={"disks": {disk1: True,
+ disk2: True,
+ disk3: True,
+ disk4: True,
+ disk5: True,
+ disk6: True}})
+ self.addCleanupVG("vgroup0")
+ self.click_card_row("Storage", name="vgroup0")
+
+ def mb(size):
+ if size < 100e6:
+ return str(round(size / 1e6, 1)) + " MB"
+ else:
+ return str(round(size / 1e6)) + " MB"
+
+ def test(layout, expected_size):
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog_wait_open()
+ self.dialog_wait_val("name", "lvol0")
+ self.dialog_set_val("purpose", "block")
+ self.dialog_set_val("layout", layout)
+ if layout == "raid10":
+ self.dialog_set_val("pvs", {disk6: False})
+ self.dialog_apply()
+ self.dialog_wait_error("pvs", "an even number")
+ self.dialog_set_val("pvs", {disk6: True})
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
+ self.click_card_row("LVM2 logical volumes", 1)
+
+ field = "Physical volumes" if layout.startswith("linear") else "Stripes"
+
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk1))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk2))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk3))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk4))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk5))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk6))
+
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), mb(expected_size))
+
+ self.click_card_dropdown("LVM2 logical volume", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("LVM2 logical volumes"))
+ b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0"))
+
+ ext_size = 4 * 1024 * 1024
+ pv_size = math.floor(50e6 / ext_size) * ext_size
+ n_pvs = 6
+
+ test("linear", n_pvs * pv_size)
+ test("raid0", n_pvs * pv_size)
+ test("raid1", pv_size - ext_size)
+ test("raid10", (n_pvs / 2) * (pv_size - ext_size))
+ test("raid5", (n_pvs - 1) * (pv_size - ext_size))
+ test("raid6", (n_pvs - 2) * (pv_size - ext_size))
+
+ def testMaxLayoutGrowth(self):
+ b = self.browser
+
+ self.skip_if_no_layouts()
+
+ # Make sure that Cockpit gets the computation of the maximum
+ # size right when growing a logical volume.
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_loopback_disk(name="loop10")
+ disk2 = self.add_loopback_disk(name="loop11")
+ disk3 = self.add_loopback_disk(name="loop12")
+ disk4 = self.add_loopback_disk(name="loop13")
+ disk5 = self.add_loopback_disk(name="loop14")
+ disk6 = self.add_loopback_disk(name="loop15")
+
+ with b.wait_timeout(60):
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
+ expect=lambda: (self.dialog_is_present('disks', disk1) and
+ self.dialog_is_present('disks', disk2) and
+ self.dialog_is_present('disks', disk3) and
+ self.dialog_is_present('disks', disk4) and
+ self.dialog_is_present('disks', disk5) and
+ self.dialog_is_present('disks', disk6) and
+ self.dialog_check({"name": "vgroup0"})),
+ values={"disks": {disk1: True,
+ disk2: True,
+ disk3: True,
+ disk4: True,
+ disk5: True,
+ disk6: True}})
+ self.addCleanupVG("vgroup0")
+ self.click_card_row("Storage", name="vgroup0")
+
+ def mb(size):
+ if size < 100e6:
+ return str(round(size / 1e6, 1)) + " MB"
+ else:
+ return str(round(size / 1e6)) + " MB"
+
+ def test(layout, expected_size):
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog(expect={"name": "lvol0"},
+ values={"purpose": "block",
+ "layout": layout,
+ "size": round(expected_size / 2e6)})
+
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
+ self.click_card_row("LVM2 logical volumes", 1)
+
+ field = "Stripes"
+
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk1))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk2))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk3))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk4))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk5))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", field), bn(disk6))
+
+ # Grow to max with default pvs
+ b.click(self.card_button("LVM2 logical volume", "Grow"))
+ self.dialog_wait_open()
+ slider = self.dialog_field("size") + " .pf-v5-c-slider .pf-v5-c-slider__rail"
+ width = b.call_js_func('(function (sel) { return ph_find(sel).offsetWidth; })', slider)
+ b.mouse(slider, "click", width, 0)
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ card = self.card("LVM2 logical volume")
+ b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk1)}')", 1)
+ b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk2)}')", 1)
+ b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk3)}')", 1)
+ b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk4)}')", 1)
+ b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk5)}')", 1)
+ b.wait_js_func("ph_count_check", card + f" .storage-stripe-pv-box-dev:contains('{bn(disk6)}')", 1)
+
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), mb(expected_size))
+
+ self.click_card_dropdown("LVM2 logical volume", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("LVM2 logical volumes"))
+ b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0"))
+
+ ext_size = 4 * 1024 * 1024
+ pv_size = math.floor(50e6 / ext_size) * ext_size
+ n_pvs = 6
+
+ test("raid0", n_pvs * pv_size)
+ test("raid1", pv_size - ext_size)
+ test("raid10", (n_pvs / 2) * (pv_size - ext_size))
+ test("raid5", (n_pvs - 1) * (pv_size - ext_size))
+ test("raid6", (n_pvs - 2) * (pv_size - ext_size))
+
+ @testlib.skipImage("No targetd in Arch Linux", "arch")
+ def testDegradation(self):
+ b = self.browser
+
+ self.skip_if_no_layouts()
+
+ # Make one (very small) logical volume for each RAID type and
+ # then break them.
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_targetd_loopback_disk(index=1, size=100)
+ disk2 = self.add_targetd_loopback_disk(index=2, size=100)
+ disk3 = self.add_targetd_loopback_disk(index=3, size=100)
+ disk4 = self.add_targetd_loopback_disk(index=4, size=100)
+ disk5 = self.add_targetd_loopback_disk(index=5, size=100)
+
+ with b.wait_timeout(60):
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
+ expect=lambda: (self.dialog_is_present('disks', disk1) and
+ self.dialog_is_present('disks', disk2) and
+ self.dialog_is_present('disks', disk3) and
+ self.dialog_is_present('disks', disk4) and
+ self.dialog_is_present('disks', disk5) and
+ self.dialog_check({"name": "vgroup0"})),
+ values={"disks": {disk1: True,
+ disk2: True,
+ disk3: True,
+ disk4: True,
+ disk5: True}})
+ self.addCleanupVG("vgroup0")
+ self.click_card_row("Storage", name="vgroup0")
+
+ def create(row, layout, expected_name):
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog_wait_open()
+ self.dialog_wait_val("name", expected_name)
+ self.dialog_set_val("layout", layout)
+ if layout == "raid10":
+ self.dialog_set_val("pvs", {disk5: False})
+ elif layout == "linear":
+ self.dialog_set_val("pvs", {disk2: False, disk3: False, disk4: False, disk5: False})
+ self.dialog_set_val("size", 10)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_row_col("LVM2 logical volumes", row, 1), expected_name)
+
+ create(1, "linear", "lvol0")
+ create(2, "raid0", "lvol1")
+ create(3, "raid1", "lvol2")
+ create(4, "raid10", "lvol3")
+ create(5, "raid5", "lvol4")
+ create(6, "raid6", "lvol5")
+
+ def wait_msg(row, msg):
+ self.click_card_row("LVM2 logical volumes", row)
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes" if row == 1 else "Stripes"),
+ msg)
+ b.click(self.card_parent_link())
+
+ def wait_partial(row):
+ wait_msg(row, "This logical volume has lost some of its physical volumes and can no longer be used.")
+
+ def wait_degraded(row):
+ wait_msg(row, "This logical volume has lost some of its physical volumes but has not lost any data yet.")
+
+ def wait_maybe_partial(row):
+ wait_msg(row, "This logical volume has lost some of its physical volumes but might not have lost any data yet.")
+
+ self.force_remove_disk(disk1)
+
+ wait_partial(1) # linear is broken now
+ wait_partial(2) # striped as well
+ wait_degraded(3) # mirror is fine
+ wait_degraded(4) # striped mirror as well
+ wait_degraded(5) # raid5 and ...
+ wait_degraded(6) # ... raid6 are doing their job
+
+ self.force_remove_disk(disk2)
+
+ wait_degraded(3) # mirror is still fine
+ wait_maybe_partial(4) # striped mirror is not sure anymore
+ wait_partial(5) # raid5 is now toast
+ wait_degraded(6) # but raid6 still hangs on
+
+ self.force_remove_disk(disk3)
+
+ wait_degraded(3) # mirror is _still_ fine
+ wait_partial(4) # striped mirror has lost more than half and is kaputt
+ wait_partial(6) # raid6 is finally toast as well
+
+ def testLvmOnLuks(self):
+ b = self.browser
+ m = self.machine
+
+ self.skip_if_no_layouts()
+
+ # Make sure that Cockpit gets the layout description right for
+ # encrypted physical volumes
+
+ self.login_and_go("/storage")
+
+ disk = self.add_loopback_disk()
+ b.wait_visible(self.card_row("Storage", name=disk))
+ m.execute(f"echo einszweidrei | cryptsetup luksFormat --pbkdf-memory 32768 {disk}")
+ m.execute(f"echo einszweidrei | cryptsetup luksOpen {disk} dm-test")
+ self.addCleanup(m.execute, "cryptsetup close dm-test || true")
+ m.execute("vgcreate vgroup0 /dev/mapper/dm-test")
+ self.addCleanupVG("vgroup0")
+ m.execute("lvcreate vgroup0 -n lvol0 -l100%FREE")
+
+ self.click_card_row("Storage", name="lvol0")
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Physical volumes"), bn(disk))
+
+
+class TestStorageLvm2Destructive(storagelib.StorageCase):
+
+ def can_do_layouts(self):
+ return self.storaged_version >= [2, 10]
+
+ def skip_if_no_layouts(self):
+ if not self.can_do_layouts():
+ raise unittest.SkipTest("raid layouts not supported")
+
+ # This is only "destructive" because of
+ #
+ # https://bugzilla.redhat.com/show_bug.cgi?id=2256432
+ #
+ # We can't revocer the machine after that bug has hit us.
+
+ def testRaidRepair(self):
+ b = self.browser
+ m = self.machine
+
+ self.skip_if_no_layouts()
+
+ self.login_and_go("/storage")
+
+ disk1 = self.add_ram_disk()
+ disk2 = self.add_loopback_disk(name="loop10")
+ disk3 = self.add_loopback_disk(name="loop11")
+ disk4 = self.add_loopback_disk(name="loop12")
+
+ # Make a volume group with three physical volumes
+
+ with b.wait_timeout(60):
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create LVM2 volume group"),
+ expect=lambda: (self.dialog_is_present('disks', disk1) and
+ self.dialog_is_present('disks', disk2) and
+ self.dialog_is_present('disks', disk3) and
+ self.dialog_check({"name": "vgroup0"})),
+ values={"disks": {disk1: True,
+ disk2: True,
+ disk3: True}})
+
+ self.addCleanupVG("vgroup0")
+
+ # Make a raid5 on the three PVs
+
+ self.click_card_row("Storage", name="vgroup0")
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog(expect={"name": "lvol0"},
+ values={"purpose": "block",
+ "layout": "raid5"})
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "lvol0")
+ self.click_card_row("LVM2 logical volumes", 1)
+
+ b.wait_text(self.card_desc("LVM2 logical volume", "Layout"), "Distributed parity (RAID 5)")
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk1))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk2))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk3))
+
+ # Kill one PV
+
+ self.force_remove_disk(disk1)
+
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"),
+ "This logical volume has lost some of its physical volumes but has not lost any data yet.")
+ b.wait_not_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk1))
+
+ b.click(self.card_parent_link())
+ b.wait_in_text(".pf-v5-c-alert", "This volume group is missing some physical volumes.")
+ b.wait_visible(self.card_desc_action("LVM2 volume group", "Name") + ":disabled")
+ b.wait_visible(self.card_row("LVM2 logical volumes", name="lvol0") + ' .ct-icon-exclamation-triangle')
+
+ # Repair with fourth
+
+ self.click_card_row("LVM2 logical volumes", 1)
+ b.click(self.card_button("LVM2 logical volume", "Repair"))
+ self.dialog_wait_open()
+ b.wait_in_text("#dialog", "There is not enough space available")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ b.click(self.card_parent_link())
+ b.click(self.card_button("LVM2 volume group", "Add physical volume"))
+ self.dialog_wait_open()
+ self.dialog_set_val('disks', {disk4: True})
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_visible(self.card_row("LVM2 volume group", name=disk4))
+ self.click_card_row("LVM2 logical volumes", 1)
+
+ b.click(self.card_button("LVM2 logical volume", "Repair"))
+ self.dialog_wait_open()
+ self.dialog_apply()
+ self.dialog_wait_error("pvs", "An additional 46.1 MB must be selected")
+ self.dialog_set_val("pvs", {disk4: True})
+ self.dialog_apply()
+ try:
+ self.dialog_wait_close() # wait for repair to finish
+ except testlib.Error:
+ # Produce evidence of lvconvert hanging, so that our maughties can match on it
+ print(m.execute("for p in $(pgrep lvm); do cat /proc/$p/stack; done"))
+ raise
+
+ b.wait_not_in_text(self.card_desc("LVM2 logical volume", "Stripes"),
+ "This logical volume has lost some")
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Stripes"), bn(disk4))
+
+ # Dismiss alert
+
+ b.click(self.card_parent_link())
+ b.wait_visible(self.card_row("LVM2 logical volumes", name="lvol0"))
+ b.wait_not_present(self.card_row("LVM2 logical volumes", name="lvol0") + ' .ct-icon-exclamation-triangle')
+
+ b.click(".pf-v5-c-alert button:contains(Dismiss)")
+ self.dialog({})
+ b.wait_not_present(".pf-v5-c-alert")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-mdraid b/test/verify/check-storage-mdraid
new file mode 100755
index 0000000..e2d2292
--- /dev/null
+++ b/test/verify/check-storage-mdraid
@@ -0,0 +1,352 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+from time import sleep
+
+import storagelib
+import testlib
+
+
+class TestStorageMdRaid(storagelib.StorageCase):
+
+ def wait_states(self, states):
+ for s in states.keys():
+ self.browser.wait_in_text(self.card_row("MDRAID device", name=s), states[s])
+
+ def raid_add_disk(self, name):
+ self.dialog_open_with_retry(trigger=lambda: self.browser.click(self.card_button("MDRAID device", "Add disk")),
+ expect=lambda: self.dialog_is_present('disks', name))
+ self.dialog_set_val("disks", {name: True})
+ self.dialog_apply_with_retry()
+
+ def raid_remove_disk(self, name):
+ self.click_dropdown(self.card_row("MDRAID device", name=name), "Remove")
+
+ def raid_action(self, action):
+ self.browser.click(self.card_button("MDRAID device", action))
+
+ def raid_default_action_start(self, action):
+ self.raid_action(action)
+
+ def raid_default_action_finish(self, action):
+ if action == "Stop":
+ # Right after assembling an array the device might be busy
+ # from udev rules probing or the mdadm monitor; retry a
+ # few times. Also, it might come back spontaneously after
+ # having been stopped successfully; wait a bit before
+ # checking whether it is really off.
+ for _ in range(3):
+ try:
+ sleep(10)
+ self.browser.wait_text(self.card_desc("MDRAID device", "State"), "Not running")
+ break
+ except testlib.Error as ex:
+ if not ex.msg.startswith('timeout'):
+ raise
+ print("Stopping failed, retrying...")
+ if self.browser.is_present("#dialog"):
+ self.browser.wait_in_text("#dialog", "Error stopping RAID array")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+ self.raid_default_action_start(action)
+ else:
+ self.fail("Timed out waiting for array to get stopped")
+
+ def raid_default_action(self, action):
+ self.raid_default_action_start(action)
+ self.raid_default_action_finish(action)
+
+ def testRaid(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Add four disks and make a RAID out of three of them
+ disk1 = "/dev/" + m.add_disk("50M", serial="DISK1")["dev"]
+ disk2 = "/dev/" + m.add_disk("50M", serial="DISK2")["dev"]
+ disk3 = "/dev/" + m.add_disk("50M", serial="DISK3")["dev"]
+ disk4 = "/dev/" + m.add_disk("50M", serial="DISK4")["dev"]
+ b.wait_visible(self.card_row("Storage", name=disk1))
+ b.wait_visible(self.card_row("Storage", name=disk2))
+ b.wait_visible(self.card_row("Storage", name=disk3))
+ b.wait_visible(self.card_row("Storage", name=disk4))
+
+ self.click_devices_dropdown('Create MDRAID device')
+ self.dialog_wait_open()
+ self.dialog_wait_val("name", "raid0")
+ self.dialog_wait_val("level", "raid5")
+ self.dialog_apply()
+ self.dialog_wait_error("disks", "At least 2 disks are needed")
+ self.dialog_set_val("disks", {disk1: True})
+ self.dialog_apply()
+ self.dialog_wait_error("disks", "At least 2 disks are needed")
+ self.dialog_set_val("disks", {disk2: True, disk3: True})
+ self.dialog_set_val("level", "raid6")
+ self.dialog_apply()
+ self.dialog_wait_error("disks", "At least 4 disks are needed")
+ self.dialog_set_val("level", "raid5")
+ self.dialog_set_val("name", "raid 0")
+ self.dialog_apply()
+ self.dialog_wait_error("name", "Name cannot contain whitespace.")
+ self.dialog_set_val("name", "raid0")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_visible(self.card_row("Storage", name="/dev/md/raid0"))
+
+ # Check that Cockpit suggest name that does not yet exists
+ self.click_devices_dropdown('Create MDRAID device')
+ b.wait(lambda: self.dialog_val("name") == "raid1")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ self.click_card_row("Storage", name="/dev/md/raid0")
+
+ self.wait_states({disk1: "In sync",
+ disk2: "In sync",
+ disk3: "In sync"})
+
+ def wait_degraded_state(is_degraded):
+ degraded_selector = ".pf-v5-c-alert h4"
+ if is_degraded:
+ b.wait_in_text(degraded_selector, 'The MDRAID device is in a degraded state')
+ else:
+ b.wait_not_present(degraded_selector)
+
+ # The preferred device should be /dev/md/raid0, but a freshly
+ # created array seems to have no symlink in /dev/md/.
+ #
+ # See https://bugzilla.redhat.com/show_bug.cgi?id=1397320
+ #
+ dev = b.text(self.card_desc("MDRAID device", "Device"))
+
+ # Degrade and repair it
+
+ m.execute(f"mdadm --quiet {dev} --fail /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_DISK1; udevadm trigger")
+ wait_degraded_state(is_degraded=True)
+
+ self.wait_states({disk1: "Failed"})
+ self.raid_remove_disk(disk1)
+ b.wait_not_present(self.card_row("Disks", name=disk1))
+
+ self.raid_add_disk(disk1)
+ self.wait_states({disk1: "In sync",
+ disk2: "In sync",
+ disk3: "In sync"})
+
+ wait_degraded_state(is_degraded=False)
+
+ # Turn it off and on again
+ with b.wait_timeout(30):
+ self.raid_default_action("Stop")
+ b.wait_text(self.card_desc("MDRAID device", "State"), "Not running")
+ self.raid_default_action("Start")
+ b.wait_text(self.card_desc("MDRAID device", "State"), "Running")
+
+ # Stopping and starting the array should not change the device.
+ #
+ b.wait_text(self.card_desc("MDRAID device", "Device"), "/dev/md/raid0")
+
+ # Partitions also get symlinks in /dev/md/, so they should
+ # have names like "/dev/md/raid0p1".
+ #
+ part_prefix = "md/raid0p"
+
+ # Create Partition Table
+ self.click_card_dropdown("MDRAID device", "Create partition table")
+ self.dialog({"type": "gpt"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+
+ # Create first partition
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition")
+ self.dialog({"size": 20,
+ "type": "ext4",
+ "mount_point": "/foo1",
+ "name": "One"},
+ secondary=True)
+ b.wait_text(self.card_row_col("GPT partitions", 1, 2), "ext4 filesystem")
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), part_prefix + "1")
+
+ # Create second partition
+ self.click_dropdown(self.card_row("GPT partitions", 2), "Create partition")
+ self.dialog({"type": "ext4",
+ "mount_point": "/foo2",
+ "name": "Two"})
+ b.wait_text(self.card_row_col("GPT partitions", 2, 2), "ext4 filesystem")
+ b.wait_text(self.card_row_col("GPT partitions", 2, 1), part_prefix + "2")
+ b.wait_not_present(self.card_row("GPT partitions", name="Free space"))
+
+ b.assert_pixels('body', "page",
+ ignore=[self.card_desc("MDRAID device", "UUID")])
+
+ # Replace a disk by adding a spare and then removing a "In sync" disk
+
+ # Add a spare
+ self.raid_add_disk(disk4)
+ self.wait_states({disk4: "Spare"})
+
+ # Remove DISK1. The spare takes over.
+ self.raid_remove_disk(disk1)
+ b.wait_not_present(self.card_row("MDRAID device", name=disk1))
+ self.wait_states({disk4: "In sync"})
+
+ # Stop the array, destroy a disk, and start the array
+ self.raid_default_action_start("Stop")
+ self.dialog_wait_open()
+ b.wait_in_text('#dialog', "unmount, stop")
+ b.assert_pixels('#dialog', "stop-busy")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.raid_default_action_finish("Stop")
+ b.wait_text(self.card_desc("MDRAID device", "State"), "Not running")
+ m.execute("wipefs -a /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_DISK2")
+ b.wait_not_present(self.card_row("MDRAID device", name=disk2))
+ self.raid_default_action("Start")
+ b.wait_text(self.card_desc("MDRAID device", "State"), "Running")
+ wait_degraded_state(is_degraded=True)
+
+ # Add DISK1. The array recovers.
+ self.raid_add_disk(disk1)
+ self.wait_states({disk1: "In sync"})
+ wait_degraded_state(is_degraded=False)
+
+ # Add DISK2 again, as a spare
+ self.raid_add_disk(disk2)
+ self.wait_states({disk2: "Spare"})
+
+ # Remove it by formatting DISK2
+ self.click_card_row("MDRAID device", name=disk2)
+ self.click_card_dropdown("Hard Disk Drive", "Create partition table")
+ b.wait_in_text('#dialog', "remove from MDRAID, initialize")
+ self.dialog_set_val("type", "empty")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.go("#/")
+ self.click_card_row("Storage", name="/dev/md/raid0")
+ b.wait_visible(self.card("MDRAID device"))
+ b.wait_not_present(self.card_row("MDRAID device", name=disk2))
+
+ # Delete the array. We are back on the storage page.
+ self.click_card_dropdown("MDRAID device", "Delete")
+ self.confirm()
+ with b.wait_timeout(120):
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name="/dev/md/raid0"))
+
+ def testNotRemovingDisks(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk1 = "/dev/" + m.add_disk("50M", serial="DISK1")["dev"]
+ disk2 = "/dev/" + m.add_disk("50M", serial="DISK2")["dev"]
+ disk3 = "/dev/" + m.add_disk("50M", serial="DISK3")["dev"]
+ b.wait_visible(self.card_row("Storage", name=disk1))
+ b.wait_visible(self.card_row("Storage", name=disk2))
+ b.wait_visible(self.card_row("Storage", name=disk3))
+
+ self.click_devices_dropdown('Create MDRAID device')
+ self.dialog_wait_open()
+ self.dialog_set_val("level", "raid5")
+ self.dialog_set_val("disks", {disk1: True, disk2: True})
+ self.dialog_set_val("name", "ARR")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ self.click_card_row("Storage", name="/dev/md/ARR")
+
+ self.wait_states({disk1: "In sync",
+ disk2: "In sync"})
+
+ # All buttons should be disabled when the array is stopped
+
+ self.raid_default_action("Stop")
+ b.wait_text(self.card_desc("MDRAID device", "State"), "Not running")
+
+ b.wait_visible(self.card_button("MDRAID device", "Add disk") + ":disabled")
+ self.check_dropdown_action_disabled(self.card_row("MDRAID device", name=disk1), "Remove", "The MDRAID device must be running")
+
+ self.raid_default_action("Start")
+ b.wait_text(self.card_desc("MDRAID device", "State"), "Running")
+
+ # With a running array, we can add spares, but not remove "in-sync" disks
+ b.wait_not_present(self.card_button("MDRAID device", "Add disk") + ":disabled")
+ self.check_dropdown_action_disabled(self.card_row("MDRAID device", name=disk1), "Remove", "Need a spare disk")
+
+ # Adding a spare will allow removal of the "in-sync" disks.
+ self.raid_add_disk(disk3)
+ self.wait_states({disk3: "Spare"})
+ self.raid_remove_disk(disk1)
+ self.wait_states({disk3: "In sync",
+ disk2: "In sync"})
+
+ # Removing the disk will make the rest un-removable again
+ self.check_dropdown_action_disabled(self.card_row("MDRAID device", name=disk3), "Remove", "Need a spare disk")
+
+ # A failed disk can be removed
+ dev = b.text(self.card_desc("MDRAID device", "Device"))
+ m.execute(f"mdadm --quiet {dev} --fail /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_DISK3")
+ self.wait_states({disk3: "Failed"})
+ self.raid_remove_disk(disk3)
+ b.wait_not_present(self.card_row("MDRAID device", name=disk3))
+
+ # The last disk can not be removed
+ self.check_dropdown_action_disabled(self.card_row("MDRAID device", name=disk2), "Remove", "Need a spare disk")
+
+ def testBitmap(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Make two huge block devices, so that we can make a array
+ # that is beyond the threshold where Cockpit and mdadm start
+ # to worry about bitmaps. The backing files are sparse, so
+ # this is okay as long nobody actually writes a lot to these
+ # devices.
+
+ dev1 = self.add_loopback_disk(110000)
+ dev2 = self.add_loopback_disk(110000)
+
+ # We need to use "--assume-clean" so that mdraid doesn't try
+ # to write 110GB to one of the devices during synchronization.
+
+ m.execute(f"mdadm --create md0 --level=1 --assume-clean --run --raid-devices=2 {dev1} {dev2}")
+ m.execute("udevadm trigger")
+
+ self.click_card_row("Storage", name="/dev/md/md0")
+
+ self.wait_states({dev1: "In sync",
+ dev2: "In sync"})
+
+ b.wait_not_present('.pf-v5-c-alert:contains("This MDRAID device has no write-intent bitmap")')
+
+ # Remove the bitmap, Cockpit should complain and let us put it back.
+
+ m.execute("mdadm --grow --bitmap=none /dev/md/md0; udevadm trigger /dev/md/md0")
+
+ b.wait_visible('.pf-v5-c-alert:contains("This MDRAID device has no write-intent bitmap")')
+ b.click('button:contains("Add a bitmap")')
+ b.wait_not_present('.pf-v5-c-alert:contains("This MDRAID device has no write-intent bitmap")')
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-mounting b/test/verify/check-storage-mounting
new file mode 100755
index 0000000..13dfccd
--- /dev/null
+++ b/test/verify/check-storage-mounting
@@ -0,0 +1,691 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageMounting(storagelib.StorageCase):
+
+ def _navigate_root_subvolume(self):
+ b = self.browser
+
+ b.wait_visible(self.card("btrfs filesystem"))
+ self.click_card_row("btrfs filesystem", name="/")
+ b.wait_visible(self.card("btrfs subvolume"))
+
+ def testMounting(self):
+ self._testMounting()
+
+ @testlib.skipImage('no btrfs support', 'rhel-*', 'centos-*')
+ def testMountingBtrfs(self):
+ self._testMounting("btrfs", "btrfs subvolume")
+
+ def _testMounting(self, fstype="ext4", filesystem="ext4 filesystem"):
+ m = self.machine
+ b = self.browser
+
+ mount_point_foo = "/run/foo"
+ mount_point_bar = "/run/bar"
+
+ self.login_and_go("/storage")
+
+ # Add a disk
+ disk_size = 128
+ disk = self.add_ram_disk(size=disk_size)
+ self.click_card_row("Storage", name=disk)
+
+ # Format it
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", fstype)
+ self.dialog_set_val("name", "FILESYSTEM")
+ self.dialog_set_val("mount_point", "")
+ self.dialog_apply()
+ self.dialog_wait_error("mount_point", "Mount point cannot be empty")
+ self.dialog_set_val("mount_point", mount_point_foo)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assert_in_configuration("/dev/sda", "fstab", "dir", mount_point_foo)
+ if fstype == "btrfs":
+ self._navigate_root_subvolume()
+ else:
+ b.wait_text(self.card_desc(filesystem, "Name"), "FILESYSTEM")
+
+ b.wait_in_text(self.card_desc(filesystem, "Mount point"), mount_point_foo)
+
+ # Keep the mount point busy
+ sleep_pid = m.spawn(f"cd {mount_point_foo}; sleep infinity", "sleep")
+ self.write_file("/etc/systemd/system/keep-mnt-busy.service",
+ f"""
+[Unit]
+Description=Test Service
+
+[Service]
+WorkingDirectory={mount_point_foo}
+ExecStart=/usr/bin/sleep infinity
+""")
+ m.execute("systemctl start keep-mnt-busy")
+
+ b.click(self.card_button(filesystem, "Unmount"))
+ b.wait_in_text("#dialog", str(sleep_pid))
+ b.wait_in_text("#dialog", "sleep infinity")
+ b.wait_in_text("#dialog", "keep-mnt-busy")
+ b.wait_in_text("#dialog", "Test Service")
+ b.wait_in_text("#dialog", "/usr/bin/sleep infinity")
+ b.wait_in_text("#dialog", "The listed processes and services will be forcefully stopped.")
+ if fstype != "btrfs":
+ b.assert_pixels("#dialog", "busy-unmount", mock={"td[data-label='PID']": "1234",
+ "td[data-label='Runtime']": "a little while"})
+ self.confirm()
+ b.wait_in_text(self.card_desc(filesystem, "Mount point"), "The filesystem is not mounted")
+
+ m.execute("! systemctl --quiet is-active keep-mnt-busy")
+
+ if fstype != "btrfs": # tested in test-storage-btrfs
+ b.click(self.card_desc_action(filesystem, "Name"))
+ self.dialog({"name": "filesystem"})
+ b.wait_text(self.card_desc(filesystem, "Name"), "filesystem")
+
+ b.click(self.card_desc(filesystem, "Mount point") + " button")
+ self.dialog(expect={"mount_point": mount_point_foo},
+ values={"mount_point": mount_point_bar})
+ self.assert_in_configuration("/dev/sda", "fstab", "dir", mount_point_bar)
+ b.wait_in_text(self.card_desc(filesystem, "Mount point"), mount_point_bar)
+
+ b.click(self.card_button(filesystem, "Mount"))
+ self.confirm()
+ b.wait_not_in_text(self.card_desc(filesystem, "Mount point"), "The filesystem is not mounted")
+
+ # Set the "Never unlock at boot option"
+ b.click(self.card_desc(filesystem, "Mount point") + " button")
+ self.dialog({"at_boot": "never"})
+ self.assertIn("noauto", m.execute(f"findmnt -s -n -o OPTIONS {mount_point_bar}"))
+ self.assertIn("x-cockpit-never-auto", m.execute(f"findmnt -s -n -o OPTIONS {mount_point_bar}"))
+
+ # Go to overview page and check that the filesystem usage is
+ # displayed correctly.
+
+ if fstype == "btrfs": # TODO: Cockpit/UDisks reports the wrong usage for btrfs subvolumes
+ return
+
+ def wait_ratio_in_range(sel, low, high):
+ b.wait_js_func("""(function (sel, low, high) {
+ var text = ph_text(sel);
+ var match = text.match('([0-9.]+) / ([0-9]+)');
+ if (!match)
+ return false;
+ var ratio = parseFloat(match[1]) / parseFloat(match[2]);
+ return low <= ratio && ratio <= high;
+ })""", sel, low, high)
+
+ b.go("#/")
+ b.wait_visible(self.card_row("Storage", location=mount_point_bar))
+ bar_selector = self.card_row("Storage", location=mount_point_bar) + " td:nth-child(5)"
+ wait_ratio_in_range(bar_selector, 0.0, 0.1)
+ dd_count = disk_size // 2 + 10
+ m.execute(f"dd if=/dev/zero of={mount_point_bar}/zero bs=1M count={dd_count} status=none")
+ wait_ratio_in_range(bar_selector, 0.5, 1.0)
+ m.execute(f"rm {mount_point_bar}/zero")
+ wait_ratio_in_range(bar_selector, 0.0, 0.1)
+
+ self.click_card_row("Storage", location=mount_point_bar)
+ b.wait_in_text(self.card_desc("Solid State Drive", "Serial number"), "8000") # scsi_debug serial
+
+ @testlib.skipImage('no btrfs support', 'rhel-*', 'centos-*')
+ def testMountingHelpBtrfs(self):
+ self._testMountingHelp("btrfs", "btrfs subvolume")
+
+ def testMountingHelp(self):
+ self._testMountingHelp()
+
+ def _testMountingHelp(self, fstype="ext4", filesystem="ext4 filesystem"):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Add a disk
+ disk = self.add_ram_disk(size=128)
+ self.click_card_row("Storage", name=disk)
+
+ # Format it
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", fstype)
+ self.dialog_set_val("name", "FILESYSTEM")
+ self.dialog_set_val("mount_point", "/run/foo")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ if fstype == "btrfs":
+ self._navigate_root_subvolume()
+ b.wait_in_text(self.card_desc("btrfs subvolume", "Mount point"), "/run/foo")
+ self.addCleanup(m.execute, "umount /run/foo || true")
+ else:
+ b.wait_text(self.card_desc(filesystem, "Name"), "FILESYSTEM")
+ b.wait_in_text(self.card_desc(filesystem, "Mount point"), "/run/foo")
+
+ # Unmount externally, remount with Cockpit
+
+ m.execute("umount /run/foo")
+ b.click(self.card_button(filesystem, "Mount now"))
+ b.wait_not_present(self.card_button(filesystem, "Mount now"))
+ b.wait_not_in_text(self.card_desc(filesystem, "Mount point"), "The filesystem is not mounted")
+
+ # Unmount externally, adjust fstab with Cockpit
+
+ m.execute("umount /run/foo")
+ b.click(self.card_button(filesystem, "Do not mount automatically on boot"))
+ b.wait_not_present(self.card_button(filesystem, "Do not mount automatically on boot"))
+
+ # Mount somewhere else externally while "noauto", unmount with Cockpit
+
+ m.execute(f"mkdir -p /run/bar; mount {disk} /run/bar")
+ b.click(self.card_button(filesystem, "Unmount now"))
+ b.wait_not_present(self.card_button(filesystem, "Unmount now"))
+
+ # Mount externally, unmount with Cockpit
+
+ m.execute("mount /run/foo")
+ b.click(self.card_button(filesystem, "Unmount now"))
+ b.wait_not_present(self.card_button(filesystem, "Unmount now"))
+
+ # Mount externally, adjust fstab with Cockpit
+
+ m.execute("mount /run/foo")
+ b.click(self.card_button(filesystem, "Mount also automatically on boot"))
+ b.wait_not_present(self.card_button(filesystem, "Mount also automatically on boot"))
+
+ # Move mount point externally, move back with Cockpit
+
+ m.execute("umount /run/foo")
+ m.execute(f"mkdir -p /run/bar; mount {disk} /run/bar")
+ b.click(self.card_button(filesystem, "Mount on /run/foo now"))
+ b.wait_not_present(self.card_button(filesystem, "Mount on /run/foo now"))
+
+ # Move mount point externally, adjust fstab with Cockpit
+
+ m.execute("umount /run/foo")
+ m.execute(f"mkdir -p /run/bar; mount {disk} /run/bar")
+ b.click(self.card_button(filesystem, "Mount automatically on /run/bar on boot"))
+ b.wait_not_present(self.card_button(filesystem, "Mount automatically on /run/bar on boot"))
+
+ # Using noauto,x-systemd.automount should not show a warning
+ m.execute("sed -i -e 's/auto nofail/auto nofail,noauto/' /etc/fstab")
+ b.wait_visible(self.card_button(filesystem, "Mount also automatically on boot"))
+ m.execute("sed -i -e 's/noauto/noauto,x-systemd.automount/' /etc/fstab")
+ b.wait_not_present(self.card_button(filesystem, "Mount also automatically on boot"))
+
+ # Without fstab entry, mount and try to unmount
+ m.execute("sed -i '/run\\/bar/d' /etc/fstab")
+ b.wait_visible(self.card_button(filesystem, "Mount automatically on /run/bar on boot"))
+ b.click(self.card_button(filesystem, "Unmount now"))
+ b.wait_not_present(self.card_button(filesystem, "Mount automatically on /run/bar on boot"))
+
+ def testFstabOptions(self):
+ self._testFstabOptions()
+
+ @testlib.skipImage('no btrfs support', 'rhel-*', 'centos-*')
+ def testFsabOptionsBtrfs(self):
+ self._testFstabOptions("btrfs", "btrfs subvolume")
+
+ def _testFstabOptions(self, filesystem="ext4", desc="ext4 filesystem"):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk(size=128)
+ self.click_card_row("Storage", name=disk)
+
+ m.execute(f"mkfs.{filesystem} -L testfs {disk}")
+ if filesystem == "btrfs":
+ self._navigate_root_subvolume()
+ else:
+ b.wait_visible(self.card(desc))
+
+ m.execute("! grep /run/data /etc/fstab")
+ b.click(self.card_button(desc, "Mount"))
+ self.dialog({"mount_point": "/run/data",
+ "mount_options.extra": "x-foo"})
+ m.execute("grep /run/data /etc/fstab")
+ m.execute("grep 'x-foo' /etc/fstab")
+
+ b.wait_in_text(self.card_desc(desc, "Mount point"), "/run/data (ignore failure, x-foo)")
+
+ # absent mntopts and fsck columns implies "defaults"
+ if filesystem == "btrfs":
+ m.execute(r"sed -i '/run\/data/ s/auto.*$/auto subvol=\//' /etc/fstab")
+ else:
+ m.execute(r"sed -i '/run\/data/ s/auto.*$/auto/' /etc/fstab")
+ b.wait_in_text(self.card_desc(desc, "Mount point"), "/run/data (stop boot on failure)")
+
+ def testBadOption(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk()
+ self.click_card_row("Storage", name=disk)
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "mount_point": "/run/foo"},
+ secondary=True)
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "The filesystem is not mounted")
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /run/foo"))
+
+ b.click(self.card_button("ext4 filesystem", "Mount"))
+ self.dialog_wait_open()
+ self.dialog_set_val("mount_options.extra", "hurr")
+ self.dialog_apply()
+ self.dialog_wait_alert("bad option")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # No changes should have been done to fstab, and the
+ # filesystem should not be mounted.
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /run/foo"))
+ self.assertNotIn("hurr", m.execute("findmnt --fstab -n -o OPTIONS /run/foo"))
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "The filesystem is not mounted")
+
+ # Mount
+ b.click(self.card_button("ext4 filesystem", "Mount"))
+ self.dialog({})
+
+ # Apply the dialog without changes and verify that the
+ # filesystem is still mounted afterwards. Cockpit used to
+ # have a bug where this would accidentally unmount the
+ # filesystem.
+
+ b.click(self.card_desc("ext4 filesystem", "Mount point") + " button")
+ self.dialog({})
+ b.wait_not_in_text(self.card_desc("ext4 filesystem", "Mount point"), "The filesystem is not mounted")
+
+ # Try to set a bad option while the filesystem is mounted.
+ b.click(self.card_desc("ext4 filesystem", "Mount point") + " button")
+ self.dialog_wait_open()
+ self.dialog_set_val("mount_options.extra", "hurr")
+ self.dialog_apply()
+ self.dialog_wait_alert("bad option")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ self.assertNotIn("hurr", m.execute("findmnt --fstab -n -o OPTIONS /run/foo"))
+ b.wait_not_in_text(self.card_desc("ext4 filesystem", "Mount point"), "The filesystem is not mounted")
+
+ def testAtBoot(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk()
+ self.click_card_row("Storage", name=disk)
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "mount_point": "/foo",
+ "at_boot": "local",
+ "crypto": "luks1",
+ "passphrase": "foobarfoo",
+ "passphrase2": "foobarfoo"},
+ secondary=True)
+ b.wait_in_text(self.card_desc("Filesystem", "Mount point"), "The filesystem is not mounted")
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assert_in_configuration(disk, "crypttab", "options", "noauto")
+
+ def mount(expected_at_boot, at_boot):
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog_wait_open()
+ self.dialog_wait_val("at_boot", expected_at_boot)
+ self.dialog_set_val("at_boot", at_boot)
+ self.dialog_set_val("passphrase", "foobarfoo")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ def unmount():
+ b.click(self.card_button("ext4 filesystem", "Unmount"))
+ self.dialog_wait_open()
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_in_text(self.card_desc("Filesystem", "Mount point"), "The filesystem is not mounted")
+
+ mount("local", "nofail")
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "ignore failure")
+ self.assertIn("nofail", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assert_in_configuration(disk, "crypttab", "options", "nofail")
+ unmount()
+
+ mount("nofail", "netdev")
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "after network")
+ self.assertIn("_netdev", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assert_in_configuration(disk, "crypttab", "options", "_netdev")
+ unmount()
+
+ mount("netdev", "never")
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "never mount")
+ self.assertIn("x-cockpit-never-auto", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assert_in_configuration(disk, "crypttab", "options", "noauto")
+ unmount()
+
+
+@testlib.nondestructive
+class TestStorageMountingLUKS(storagelib.StorageCase):
+ def testEncryptedMountingHelp(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Add a disk
+ disk = self.add_ram_disk()
+ self.click_card_row("Storage", name=disk)
+
+ # Format it with encryption
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "ext4")
+ self.dialog_set_val("name", "FILESYSTEM")
+ self.dialog_set_val("crypto", self.default_crypto_type)
+ self.dialog_set_val("crypto_options", "xxx")
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_set_val("passphrase2", "vainu-reku-toma-rolle-kaja")
+ self.dialog_set_val("mount_point", "/run/foo")
+ self.dialog_set_val("at_boot", "netdev")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_desc("ext4 filesystem", "Name"), "FILESYSTEM")
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "/run/foo")
+
+ # Unmount and lock externally, unlock and remount with Cockpit
+
+ m.execute("umount /run/foo")
+ m.execute(f"udisksctl lock -b {disk}")
+ # wait until the UI updated to the locking
+ b.wait_text(self.card_desc("Encryption", "Cleartext device"), "-")
+ b.click(self.card_button("Filesystem", "Mount now"))
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja"})
+ b.wait_not_in_text(self.card_desc("ext4 filesystem", "Mount point"),
+ "The filesystem is not mounted")
+ b.wait_not_present(self.card_button("ext4 filesystem", "Mount now"))
+
+ # Unmount and lock externally, adjust fstab with Cockpit
+
+ m.execute("umount /run/foo")
+ m.execute(f"udisksctl lock -b {disk}")
+ # wait until the UI updated to the locking
+ b.wait_text(self.card_desc("Encryption", "Cleartext device"), "-")
+ b.click(self.card_button("Filesystem", "Do not mount automatically on boot"))
+ b.wait_not_present(self.card_button("Filesystem", "Do not mount automatically on boot"))
+
+ # Unlock and mount externally, unmount and lock with Cockpit
+
+ m.execute(f"echo -n vainu-reku-toma-rolle-kaja | udisksctl unlock --key-file /dev/stdin -b {disk}")
+ m.execute("mount /run/foo")
+ b.click(self.card_button("ext4 filesystem", "Unmount now"))
+ b.wait_visible(self.card("Filesystem"))
+ b.wait_not_present(self.card_button("Filesystem", "Unmount now"))
+
+ # Unlock and mount externally, adjust fstab with Cockpit
+
+ m.execute(f"echo -n vainu-reku-toma-rolle-kaja | udisksctl unlock --key-file /dev/stdin -b {disk}")
+ m.execute("mount /run/foo")
+ b.click(self.card_button("ext4 filesystem", "Mount also automatically on boot"))
+ b.wait_not_present(self.card_button("ext4 filesystem", "Mount also automatically on boot"))
+ b.wait_visible(self.card("ext4 filesystem"))
+
+ # Add noauto to crypttab (but not fstab), remove with Cockpit
+
+ m.execute("sed -i -e 's/xxx/xxx,noauto/' /etc/crypttab")
+ b.click(self.card_button("ext4 filesystem", "Unlock automatically on boot"))
+ b.wait_not_present(self.card_button("ext4 filesystem", "Unlock automatically on boot"))
+ b.wait_visible(self.card("ext4 filesystem"))
+
+ # Add noauto to crypttab (but not fstab), add also to fstab with Cockpit
+
+ m.execute("sed -i -e 's/xxx/xxx,noauto/' /etc/crypttab")
+ b.click(self.card_button("ext4 filesystem", "Do not mount automatically on boot"))
+ b.wait_not_present(self.card_button("ext4 filesystem", "Do not mount automatically on boot"))
+ b.wait_visible(self.card("ext4 filesystem"))
+
+ def testDuplicateMountPoints(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ self.addCleanupVG("test")
+ self.addCleanup(m.execute,
+ "umount /run/data || true;"
+ "cryptsetup close $(lsblk -lno NAME /dev/test/one | tail -1) || true")
+
+ # Quickly make two logical volumes
+ disk = self.add_ram_disk()
+ b.wait_visible(self.card_row("Storage", name=disk))
+ m.execute(f"vgcreate test {disk}; lvcreate test -n one -L 20M; lvcreate test -n two -L 20M")
+ self.click_card_row("Storage", name="test")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "one")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 2, 1), "two")
+
+ # Encrypt and format the first and give it /run/data as the mount point
+ self.click_card_row("LVM2 logical volumes", 1)
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "crypto": self.default_crypto_type,
+ "passphrase": "vainu-reku-toma-rolle-kaja",
+ "passphrase2": "vainu-reku-toma-rolle-kaja",
+ "mount_point": "/run/data"})
+ b.click(self.card_parent_link())
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "ext4 filesystem (encrypted)")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 3), "/run/data")
+
+ # Format the second and also try to use /run/data as the mount point
+ self.click_card_row("LVM2 logical volumes", 2)
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "ext4")
+ self.dialog_set_val("mount_point", "/run/data")
+ self.dialog_apply()
+ self.dialog_wait_error("mount_point", "Mount point is already used for /dev/test/one")
+ self.dialog_set_val("mount_point", "/run/data") # to clear the error
+ self.dialog_apply_secondary()
+ self.dialog_wait_error("mount_point", "Mount point is already used for /dev/test/one")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+ b.click(self.card_parent_link())
+
+ # Format the first and re-use /run/data as the mount point.
+ # This should be allowed.
+ self.click_card_row("LVM2 logical volumes", 1)
+ self.click_card_dropdown("ext4 filesystem", "Format")
+ self.dialog({"type": "ext4",
+ "mount_point": "/run/data"})
+ b.click(self.card_parent_link())
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "ext4 filesystem (encrypted)")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 3), "/run/data")
+
+ def testNeverAuto(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Add a disk and format it with luks and a filesystem, but with "Never unlock at boot"
+ disk = self.add_ram_disk()
+ self.click_card_row("Storage", name=disk)
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "ext4",
+ "crypto": self.default_crypto_type,
+ "passphrase": "vainu-reku-toma-rolle-kaja",
+ "passphrase2": "vainu-reku-toma-rolle-kaja",
+ "at_boot": "never",
+ "mount_point": "/run/foo"})
+ b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "never mount at boot")
+
+ # The filesystem should be mounted but have the "noauto"
+ # option in both fstab and crypttab
+ self.wait_mounted("ext4 filesystem")
+ self.assertIn("noauto", m.execute("grep /run/foo /etc/fstab || true"))
+ self.assertNotEqual(m.execute("grep noauto /etc/crypttab"), "")
+
+ # Unmounting should keep the noauto option, as always
+ b.click(self.card_button("ext4 filesystem", "Unmount"))
+ self.confirm()
+ self.wait_not_mounted("Filesystem")
+ self.assertIn("noauto", m.execute("grep /run/foo /etc/fstab || true"))
+ self.assertNotEqual(m.execute("grep noauto /etc/crypttab"), "")
+
+ # Mounting should also keep the "noauto", but it should not show up in the extra options
+ b.click(self.card_button("Filesystem", "Mount"))
+ self.dialog_check({"mount_options.extra": False})
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.wait_mounted("ext4 filesystem")
+ self.assertIn("noauto", m.execute("grep /run/foo /etc/fstab || true"))
+ self.assertNotEqual(m.execute("grep noauto /etc/crypttab"), "")
+
+ # As should updating the mount information
+ b.click(self.card_desc("ext4 filesystem", "Mount point") + " button")
+ self.dialog_check({"mount_options.extra": False})
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assertIn("noauto", m.execute("grep /run/foo /etc/fstab || true"))
+ self.assertNotEqual(m.execute("grep noauto /etc/crypttab"), "")
+
+ # Removing "noauto" from fstab but not from crypttab externally should show a warning
+ m.execute("sed -i -e 's/noauto//' /etc/fstab")
+ b.wait_in_text(self.card("ext4 filesystem"),
+ "The filesystem is configured to be automatically mounted on boot but its encryption container will not be unlocked at that time.")
+ b.click(self.card_button("ext4 filesystem", "Do not mount automatically"))
+ b.wait_not_present(self.card_button("ext4 filesystem", "Do not mount automatically"))
+
+ def testOverMounting(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Add a disk and make two partitions on it, one on /run/foo
+ # and one on /run/foo/bar
+
+ disk = self.add_ram_disk(100)
+ self.click_card_row("Storage", name=disk)
+
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.dialog({"type": "gpt"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition")
+ self.dialog({"type": "ext4",
+ "size": 50,
+ "crypto": self.default_crypto_type,
+ "passphrase": "vainu-reku-toma-rolle-kaja",
+ "passphrase2": "vainu-reku-toma-rolle-kaja",
+ "mount_point": "/run/foo"},
+ secondary=True)
+ b.wait_text(self.card_row_col("GPT partitions", 1, 3), "/run/foo (not mounted)")
+
+ self.click_dropdown(self.card_row("GPT partitions", 2), "Create partition")
+ self.dialog({"type": "ext4",
+ "crypto": self.default_crypto_type,
+ "passphrase": "vainu-reku-toma-rolle-kaja",
+ "passphrase2": "vainu-reku-toma-rolle-kaja",
+ "mount_point": "/run/foo/bar"},
+ secondary=True)
+ b.wait_text(self.card_row_col("GPT partitions", 2, 3), "/run/foo/bar (not mounted)")
+
+ # Mount /run/foo/bar first and check that mounting /run/foo is
+ # rejected
+
+ self.click_dropdown(self.card_row("GPT partitions", 2), "Mount")
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja"})
+ b.wait_text(self.card_row_col("GPT partitions", 2, 3), "/run/foo/bar")
+
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Mount")
+ self.dialog_wait_open()
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_error("mount_point", "Filesystems are already mounted below this mountpoint.")
+ b.assert_pixels("#dialog", "overmounting-rejection")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Unmount /run/foo/bar, mount /run/foo, mount /run/foo/bar
+ # again and then check that unmounting /run/foo will also
+ # unmount /run/foo/bar.
+
+ self.click_dropdown(self.card_row("GPT partitions", 2), "Unmount")
+ self.confirm()
+
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Mount")
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 3), "/run/foo")
+
+ self.click_dropdown(self.card_row("GPT partitions", 2), "Mount")
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja"})
+ b.wait_text(self.card_row_col("GPT partitions", 2, 3), "/run/foo/bar")
+
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Unmount")
+ self.dialog_wait_open()
+ b.wait_in_text("#dialog tr:contains('/run/foo/bar')", "unmount")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ # Now /run/foo/bar should be noauto.
+ self.assert_in_configuration(disk + "2", "crypttab", "options", "noauto")
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /run/foo/bar"))
+
+ # Mount them again and check that initializing the disk will
+ # unmount /run/foo/bar first.
+
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Mount")
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 3), "/run/foo")
+
+ self.click_dropdown(self.card_row("GPT partitions", 2), "Mount")
+ self.dialog({"passphrase": "vainu-reku-toma-rolle-kaja"})
+ b.wait_text(self.card_row_col("GPT partitions", 2, 3), "/run/foo/bar")
+
+ # Sometimes a block device is still held open by
+ # something immediately after locking it. This
+ # prevents the kernel from reading the new partition
+ # table. Let's just retry in that case.
+
+ def first_setup():
+ b.wait_text("#dialog tbody:nth-of-type(1) td[data-label=Location]", "/run/foo/bar")
+ b.wait_text("#dialog tbody:nth-of-type(2) td[data-label=Location]", "/run/foo")
+
+ self.dialog_with_error_retry(trigger=lambda: self.click_card_dropdown("Solid State Drive",
+ "Create partition table"),
+ first_setup=first_setup,
+ errors=["Timed out waiting for object"])
+
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-msdos b/test/verify/check-storage-msdos
new file mode 100755
index 0000000..f9ce82f
--- /dev/null
+++ b/test/verify/check-storage-msdos
@@ -0,0 +1,88 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageMsDOS(storagelib.StorageCase):
+
+ def testDosParts(self):
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Add a disk
+ disk = self.add_ram_disk()
+ self.click_card_row("Storage", name=disk)
+
+ # Format it with a DOS partition table
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.dialog({"type": "dos"})
+ b.wait_text(self.card_row_col("DOS partitions", 1, 1), "Free space")
+
+ # Create a primary partition
+ self.click_dropdown(self.card_row("DOS partitions", 1), "Create partition")
+ self.dialog({"size": 10,
+ "type": "ext4",
+ "mount_point": "/foo",
+ "name": "FIRST"},
+ secondary=True)
+ b.wait_text(self.card_row_col("DOS partitions", 1, 2), "ext4 filesystem")
+
+ # Open dialog for formatting the primary partition and check that "dos-extended" is not offered.
+ self.click_card_row("DOS partitions", 1)
+ self.click_card_dropdown("ext4 filesystem", "Format")
+ self.dialog_wait_open()
+ b.wait_not_present("select option[value='dos-extended']")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+ b.click(self.card_parent_link())
+
+ # Create a extended partition to fill the rest of the disk
+ self.click_dropdown(self.card_row("DOS partitions", 2), "Create partition")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "dos-extended")
+ self.dialog_wait_not_present("name")
+ self.dialog_wait_not_present("mount_point")
+ self.dialog_wait_not_present("mount_options")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_row_col("DOS partitions", 2, 1), "Extended partition")
+ b.wait_text(self.card_row_col("DOS partitions", 3, 1), "Free space")
+
+ # Create logical partitions and check that "dos-extended" is
+ # not offered.
+ self.click_dropdown(self.card_row("DOS partitions", 3), "Create partition")
+ self.dialog_wait_open()
+ b.wait_not_present("select option[value='dos-extended']")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Delete it
+
+ self.click_dropdown(self.card_row("DOS partitions", 2), "Delete")
+ self.confirm()
+
+ b.wait_text(self.card_row_col("DOS partitions", 2, 1), "Free space")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-multipath b/test/verify/check-storage-multipath
new file mode 100755
index 0000000..91f7e2d
--- /dev/null
+++ b/test/verify/check-storage-multipath
@@ -0,0 +1,98 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.skipImage("No support for multipath", "debian-*", "ubuntu-*", "arch")
+class TestStorageMultipath(storagelib.StorageCase):
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ def check_free_block_devices(expected):
+ allowed = ["/dev/vda4"]
+ blocks = list(filter(lambda b: b not in allowed,
+ b.eval_js("ph_texts('#dialog [data-field=\"disks\"] .select-space-details')")))
+ self.assertEqual(len(blocks), len(expected))
+ for i in range(len(expected)):
+ self.assertIn(expected[i], blocks[i])
+
+ # At least on Fedora 27, multipath only looks at SCSI_IDENT_
+ # and ID_WWN properties, so we install a custom udev rule to
+ # set ID_WWN to something that can identify multipathed devices.
+ #
+ m.write("/etc/udev/rules.d/99-fake-wwn.rules", 'SUBSYSTEM=="block", ENV{ID_WWN}="$env{ID_SCSI_SERIAL}"\n')
+ m.execute("udevadm control --reload")
+ m.execute("udevadm trigger")
+
+ self.login_and_go("/storage")
+
+ b.inject_js("""
+ ph_texts = function (sel) {
+ return ph_select(sel).map(function (e) { return e.textContent });
+ }""")
+
+ # Add a disk
+ info1 = m.add_disk("10M", serial="MYSERIAL")
+ dev1 = "/dev/" + info1["dev"]
+
+ self.click_card_row("Storage", name=dev1)
+ b.wait_text(self.card_desc("Hard Disk Drive", "Device file"), dev1)
+
+ # Add another disk with the same serial, which fools
+ # multipathd into treating it as another path to the first
+ # disk. Since we never actually write to it, this is fine.
+
+ # The primary device file should disappear and multipathed
+ # devices should be listed.
+ info2 = m.add_disk("10M", serial="MYSERIAL")
+ dev2 = "/dev/" + info2["dev"]
+
+ b.wait_text(self.card_desc("Hard Disk Drive", "Device file"), "-")
+ b.wait_in_text(self.card_desc("Hard Disk Drive", "Multipathed devices"), dev1)
+ b.wait_in_text(self.card_desc("Hard Disk Drive", "Multipathed devices"), dev2)
+
+ # Check that neither is offered as a free block device
+ b.go("#/")
+ self.click_dropdown(self.card_header("Storage"), "Create LVM2 volume group")
+ self.dialog_wait_open()
+ check_free_block_devices([])
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Switch on multipathd. A primary device should appear.
+
+ b.wait_visible('.pf-m-danger:contains(There are devices with multiple paths on the system, but)')
+ b.click('button:contains(Start multipath)')
+ b.wait_not_present('.pf-m-danger:contains(There are devices with multiple paths on the system, but)')
+ b.wait_visible(self.card_row("Storage", name="/dev/mapper/mpatha"))
+
+ # Check that (exactly) the primary device is offered as free
+ self.click_dropdown(self.card_header("Storage"), "Create LVM2 volume group")
+ self.dialog_wait_open()
+ check_free_block_devices(["/dev/mapper/mpatha"])
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-nfs b/test/verify/check-storage-nfs
new file mode 100755
index 0000000..f963ba6
--- /dev/null
+++ b/test/verify/check-storage-nfs
@@ -0,0 +1,361 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import packagelib
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageNfs(storagelib.StorageCase):
+
+ def testNfsClient(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ m.execute("mkdir /home/foo /home/bar /mnt/test")
+ self.write_file("/etc/exports", "/home/foo 127.0.0.0/24(rw)\n/home/bar 127.0.0.0/24(rw)\n",
+ post_restore_action="systemctl restart nfs-server")
+ m.execute("systemctl restart nfs-server")
+ # Removing the nfs mount removes the target dir, if the test fails it
+ # won't. It's important to umount before restoring /etc/exports as
+ # otherwise nfs is confused and we can't umount the share.
+ self.addCleanup(m.execute, "if [ -e /mnt/test ]; then umount /mnt/test 2>/dev/null || true; rm -r /mnt/test; fi")
+
+ orig_fstab = m.execute("cat /etc/fstab")
+
+ # Add /home/foo
+ self.click_dropdown(self.card_header("Storage"), "New NFS mount")
+ self.dialog_wait_open()
+ self.dialog_set_val("server", "127.0.0.1")
+ self.dialog_set_val("remote", "/home/foo")
+ self.dialog_set_val("dir", "/mnt/test")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.wait_visible(self.card_row("Storage", name="127.0.0.1:/home/foo"))
+ b.wait_visible(self.card_row("Storage", location="/mnt/test"))
+ b.wait_visible(self.card_row("Storage", location="/mnt/test") + " .usage-bar[role=progressbar]")
+
+ # Should be saved to fstab
+ self.assertEqual(m.execute("cat /etc/fstab"), orig_fstab +
+ "127.0.0.1:/home/foo /mnt/test nfs defaults\n")
+
+ # Try to add some non-exported directory
+ self.click_dropdown(self.card_header("Storage"), "New NFS mount")
+ self.dialog_wait_open()
+ self.dialog_set_val("server", "127.0.0.1")
+ b.set_input_text(self.dialog_field("remote") + " input", "/usr/share")
+ b.click(self.dialog_field("remote") + " .pf-v5-c-select ul > li > button")
+ self.dialog_set_val("dir", "/run/share")
+ self.dialog_apply()
+ b.wait_visible("#dialog .pf-v5-c-alert.pf-m-danger")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Add /home/bar
+ self.click_dropdown(self.card_header("Storage"), "New NFS mount")
+ self.dialog_wait_open()
+ self.dialog_set_val("server", "127.0.0.1")
+ self.dialog_set_val("remote", "/home/bar")
+ self.dialog_set_val("dir", "/mounts/bar")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.wait_visible(self.card_row("Storage", name="127.0.0.1:/home/bar"))
+ b.wait_visible(self.card_row("Storage", location="/mounts/bar"))
+ b.wait_visible(self.card_row("Storage", location="/mounts/bar") + " .usage-bar[role=progressbar]")
+ m.execute("test -d /mounts/bar")
+
+ # Go to details of /home/bar
+ self.click_card_row("Storage", name="127.0.0.1:/home/bar")
+ b.wait_text(self.card_desc("NFS mount", "Server"), "127.0.0.1:/home/bar")
+ b.wait_text(self.card_desc("NFS mount", "Mount point"), "/mounts/bar")
+
+ # Change mount point of /home/bar
+ b.click(self.card_button("NFS mount", "Edit"))
+ self.dialog_wait_open()
+ self.dialog_set_val("dir", "/mounts/barbar")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.addCleanup(m.execute, "umount /mounts/barbar; rmdir /mounts/barbar")
+
+ b.wait_text(self.card_desc("NFS mount", "Mount point"), "/mounts/barbar")
+ m.execute("! test -e /mounts/bar")
+ m.execute("test -d /mounts/barbar")
+ self.assertEqual(m.execute("findmnt -s -n -o OPTIONS /mounts/barbar").strip(), "defaults")
+
+ # Set options for /home/bar
+ b.click(self.card_button("NFS mount", "Edit"))
+
+ def wait_checked(field):
+ b.wait_visible(self.dialog_field(field) + ":checked")
+
+ def wait_not_checked(field):
+ b.wait_visible(self.dialog_field(field) + ":not(:checked)")
+
+ self.dialog_wait_open()
+ wait_checked("mount_options.auto")
+ wait_not_checked("mount_options.ro")
+ self.dialog_set_val("mount_options.auto", val=False)
+ self.dialog_set_val("mount_options.ro", val=True)
+ self.dialog_set_val("mount_options.extra", "ac")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ self.assertEqual(m.execute("findmnt -s -n -o OPTIONS /mounts/barbar").strip(), "noauto,ro,ac")
+
+ # Should be saved to fstab
+ self.assertEqual(m.execute("cat /etc/fstab"), orig_fstab +
+ "127.0.0.1:/home/foo /mnt/test nfs defaults\n" +
+ "127.0.0.1:/home/bar /mounts/barbar nfs noauto,ro,ac\n")
+
+ # Go to details of /home/foo
+ b.go("#/")
+ self.click_card_row("Storage", name="127.0.0.1:/home/foo")
+ b.wait_text(self.card_desc("NFS mount", "Server"), "127.0.0.1:/home/foo")
+ b.wait_text(self.card_desc("NFS mount", "Mount point"), "/mnt/test")
+
+ # Unmount and remount /home/foo
+ b.click(self.card_button("NFS mount", "Unmount"))
+ b.click(self.card_button("NFS mount", "Mount"))
+ b.wait_visible(self.card_button("NFS mount", "Unmount"))
+
+ # Remove /home/foo
+ self.click_card_dropdown("NFS mount", "Remove")
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name="127.0.0.1:/home/foo"))
+ m.execute("! test -e /mnt/test")
+ # Should be removed from fstab, too
+ self.assertEqual(m.execute("cat /etc/fstab"), orig_fstab +
+ "127.0.0.1:/home/bar /mounts/barbar nfs noauto,ro,ac\n")
+
+ # Picks up mounts in fstab
+ m.execute("echo '1.2.3.4:/something /somewhere nfs defaults 0 0' >> /etc/fstab")
+ b.wait_visible(self.card_row("Storage", name="1.2.3.4:/something"))
+ b.wait_visible(self.card_row("Storage", location="/somewhere (not mounted)"))
+
+ # Ignores non-FS mounts which look similar
+ m.execute("echo '2.3.4.5:/marmalade /dunno rfs defaults 0 0' >> /etc/fstab")
+ # But recognizes variants like "nfs4"
+ m.execute("echo '5.6.7.8:/stuff /four nfs4 defaults 0 0' >> /etc/fstab")
+ b.wait_visible(self.card_row("Storage", name="5.6.7.8:/stuff"))
+ b.wait_visible(self.card_row("Storage", location="/four (not mounted)"))
+ b.wait_not_present(self.card_row("Storage", name="2.3.4.5:/marmalade"))
+
+ def testNfsListExports(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ m.execute("mkdir /home/foo /home/bar")
+ self.write_file("/etc/exports", "/home/foo 127.0.0.0/24(rw)\n/home/bar 127.0.0.0/24(rw)\n",
+ post_restore_action="systemctl restart nfs-server")
+ m.execute("systemctl restart nfs-server")
+
+ self.click_dropdown(self.card_header("Storage"), "New NFS mount")
+ self.dialog_wait_open()
+ self.dialog_set_val("server", "127.0.0.1")
+
+ def wait_for_exports():
+ def check():
+ choices = self.dialog_combobox_choices("remote")
+ return len(choices) == 2 and "/home/foo" in choices and "/home/bar" in choices
+ self.retry(None, check, None)
+
+ b.click('#dialog [data-field="remote"] button.pf-v5-c-select__toggle-button')
+ wait_for_exports()
+
+ def testNfsMountWithoutDiscovery(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ m.execute("mkdir /home/foo /home/bar")
+ self.addCleanup(m.execute, "systemctl restart nfs-server")
+ self.write_file("/etc/exports", "/home/foo 127.0.0.0/24(rw)\n/home/bar 127.0.0.0/24(rw)\n",
+ post_restore_action="systemctl restart nfs-server")
+ m.execute("systemctl restart nfs-server")
+
+ # Break showmount. Cockpit uses showmount to list exported
+ # directories, but that relies on a properly configured
+ # firewall etc. Even if showmount doesn't work, Cockpit
+ # should allow people to mount arbitrary directories.
+ self.restore_file("/usr/sbin/showmount")
+ m.execute("chmod a-x /usr/sbin/showmount")
+
+ self.click_dropdown(self.card_header("Storage"), "New NFS mount")
+ self.dialog_wait_open()
+ self.dialog_set_val("server", "127.0.0.1")
+ # Manually add a remote location to the select
+ b.set_input_text("[data-field=remote] input", "/home/foo")
+ b.click("button.pf-v5-c-select__menu-item")
+ self.dialog_set_val("dir", "/mnt")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.addCleanup(m.execute, "umount /mnt")
+
+ b.wait_visible(self.card_row("Storage", name="127.0.0.1:/home/foo"))
+ b.wait_visible(self.card_row("Storage", location="/mnt"))
+ b.wait_visible(self.card_row("Storage", location="/mnt") + " .usage-bar[role=progressbar]")
+
+ def testNfsBusy(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ m.execute("mkdir /home/foo /home/bar")
+ self.addCleanup(m.execute, "systemctl restart nfs-server")
+ self.write_file("/etc/exports", "/home/foo 127.0.0.0/24(rw)\n/home/bar 127.0.0.0/24(rw)\n",
+ post_restore_action="systemctl restart nfs-server")
+ m.execute("systemctl restart nfs-server")
+
+ # Add /home/foo
+ self.click_dropdown(self.card_header("Storage"), "New NFS mount")
+ self.dialog_wait_open()
+ self.dialog_set_val("server", "127.0.0.1")
+ self.dialog_set_val("remote", "/home/foo")
+ self.dialog_set_val("dir", "/mounts/foo")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.wait_visible(self.card_row("Storage", name="127.0.0.1:/home/foo"))
+ b.wait_visible(self.card_row("Storage", location="/mounts/foo"))
+ b.wait_visible(self.card_row("Storage", location="/mounts/foo") + " .usage-bar[role=progressbar]")
+
+ # Go to details of /home/foo
+ self.click_card_row("Storage", name="127.0.0.1:/home/foo")
+ b.wait_text(self.card_desc("NFS mount", "Server"), "127.0.0.1:/home/foo")
+ b.wait_text(self.card_desc("NFS mount", "Mount point"), "/mounts/foo")
+
+ sleep_pid = m.spawn("cd /mounts/foo; sleep infinity", "busy")
+ b.click(self.card_button("NFS mount", "Edit"))
+
+ self.dialog_wait_open()
+ self.dialog_wait_alert("This NFS mount is in use")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ b.click(self.card_button("NFS mount", "Unmount"))
+ self.dialog_wait_open()
+ b.wait_in_text("#dialog", str(sleep_pid))
+ b.wait_in_text("#dialog", "sleep infinity")
+ b.wait_in_text("#dialog", "The listed processes will be forcefully stopped.")
+ b.wait_in_text("#dialog", "Stop and unmount")
+ b.assert_pixels("#dialog", "unmount", mock={"td[data-label='PID']": "1234",
+ "td[data-label='Runtime']": "a little while"})
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.click(self.card_button("NFS mount", "Mount"))
+ b.wait_visible(self.card_button("NFS mount", "Unmount"))
+
+ sleep_pid = m.spawn("cd /mounts/foo; sleep infinity", "busy")
+ self.click_card_dropdown("NFS mount", "Remove")
+ b.wait_in_text("#dialog", str(sleep_pid))
+ b.wait_in_text("#dialog", "sleep infinity")
+ b.wait_in_text("#dialog", "The listed processes will be forcefully stopped.")
+ b.wait_in_text("#dialog", "Stop and remove")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ # We are back on the Overview with nothing there
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name="127.0.0.1:/home/foo"))
+ b.wait_not_present(self.card_row("Storage", location="/mounts/foo"))
+
+
+# Re-use allowed journal messages from StorageCase
+@testlib.skipOstree("No udisks/cockpit-storaged on OSTree images")
+@testlib.nondestructive
+class TestStoragePackagesNFS(packagelib.PackageCase, storagelib.StorageCase, storagelib.StorageHelpers):
+
+ def testNfsMissingPackages(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute("systemctl restart nfs-server")
+
+ # Override configuration so that we don't have to remove the
+ # real package.
+
+ self.machine.write("/etc/cockpit/storaged.override.json",
+ """{ "config": { "nfs_client_package": "fake-nfs-utils" } }""")
+
+ if m.execute("if test -e /sbin/mount.nfs; then echo yes; fi").strip():
+ m.execute("mv /sbin/mount.nfs /sbin/mount.nfs.off")
+ self.addCleanup(m.execute, "mv /sbin/mount.nfs.off /sbin/mount.nfs")
+
+ self.login_and_go("/storage")
+
+ # The fake-nfs-utils package is not available yet
+
+ self.click_dropdown(self.card_header("Storage"), "New NFS mount")
+ self.dialog_wait_open()
+ b.wait_in_text("#dialog", "fake-nfs-utils is not available from any repository.")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Now make the package available
+
+ self.createPackage("dummy", "1.0", "1")
+ self.createPackage("fake-nfs-utils", "1.0", "1", depends="fake-libnfs")
+ self.createPackage("fake-libnfs", "1.0", "1")
+ self.enableRepo()
+
+ # HACK
+ #
+ # The first simulated install seems to silently not report
+ # anything on the Debian test images, for unknown reasons. So
+ # we install a dummy package to warm up all parts of the
+ # machinery and distribute the fluids evenly.
+ #
+ if "debian" in m.image or "ubuntu" in m.image:
+ m.execute("pkcon refresh; pkcon install -y dummy")
+
+ # HACK
+ # Packagekit on Arch Linux does not deal well with detecting new repositories
+ if m.image == "arch":
+ m.execute("systemctl restart packagekit")
+ m.execute("pkcon refresh force; pkcon install -y dummy")
+
+ self.click_dropdown(self.card_header("Storage"), "New NFS mount")
+ self.dialog_wait_open()
+ b.wait_in_text("#dialog", "fake-nfs-utils will be installed")
+ b.wait_in_text("#dialog", "fake-libnfs")
+ self.dialog_apply()
+ self.dialog_set_val("server", "127.0.0.1")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Now we should go straight to the main dialog
+ self.click_dropdown(self.card_header("Storage"), "New NFS mount")
+ self.dialog_wait_open()
+ self.dialog_set_val("server", "127.0.0.1")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-partitions b/test/verify/check-storage-partitions
new file mode 100755
index 0000000..e55baf9
--- /dev/null
+++ b/test/verify/check-storage-partitions
@@ -0,0 +1,231 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStoragePartitions(storagelib.StorageCase):
+
+ def testPartitions(self):
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # A loopback device ends with a digit and partitions have
+ # names like "/dev/loop0p1". Check that the storage stack has
+ # no difficulties with that.
+ #
+ # We are especially careful to use a device name that doesn't
+ # end in all zeros, because that would be too easy and
+ # wouldn't trigger this bug:
+ #
+ # https://github.com/storaged-project/storaged/issues/97
+
+ dev = self.add_loopback_disk(10, "loop12")
+ self.click_card_row("Storage", name=dev)
+
+ self.click_card_dropdown("Block device", "Create partition table")
+ self.dialog({"type": "gpt"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition")
+ self.dialog({"type": "ext4",
+ "mount_point": "/foo"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 3), "/foo")
+
+ self.click_card_row("GPT partitions", 1)
+ self.click_card_dropdown("Partition", "Delete")
+ self.confirm()
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+
+ def testSizeSlider(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk()
+ self.click_card_row("Storage", name=disk)
+
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.dialog({"type": "gpt"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "empty")
+
+ slider = self.dialog_field("size") + " .pf-v5-c-slider .pf-v5-c-slider__rail"
+
+ # Move the slider one pixel past the middle, this should give a fractional size.
+ # See https://github.com/cockpit-project/cockpit/pull/10968 for more about this.
+ width = b.call_js_func('(function (sel) { return ph_find(sel).offsetWidth; })', slider)
+ about_half_way = width / 2 + 1
+
+ b.mouse(slider, "click", about_half_way, 0)
+ self.dialog_wait_val("size", 27.3)
+ b.focus(slider + " + .pf-v5-c-slider__thumb")
+ b.key_press(chr(37), use_ord=True) # arrow left
+ b.key_press(chr(37), use_ord=True)
+ b.key_press(chr(37), use_ord=True)
+ b.key_press(chr(37), use_ord=True)
+ b.key_press(chr(37), use_ord=True)
+ self.dialog_wait_val("size", 26.2)
+
+ # Check that changing units doesn't affect the text input
+ unit = "1000000000"
+ b.select_from_dropdown(".size-unit > select", unit)
+ self.dialog_wait_val("size", 26.2, unit)
+
+ # Change unit back to MB
+ unit = "1000000"
+ b.select_from_dropdown(".size-unit > select", unit)
+
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ testlib.wait(lambda: m.execute(f"lsblk -no SIZE {disk}1").strip() == "25M")
+
+ def testResize(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk(100)
+ self.click_card_row("Storage", name=disk)
+
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.dialog({"type": "gpt"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+
+ # Make two partitions that cover the whole disk.
+
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition")
+ self.dialog({"type": "ext4",
+ "mount_point": "/foo1",
+ "size": 80},
+ secondary=True)
+ self.click_dropdown(self.card_row("GPT partitions", 2), "Create partition")
+ self.dialog({"type": "ext4",
+ "mount_point": "/foo2",
+ "size": 23},
+ secondary=True)
+
+ b.wait_text(self.card_row_col("GPT partitions", 1, 4), "79.7 MB")
+ b.wait_text(self.card_row_col("GPT partitions", 2, 4), "23.1 MB")
+
+ # Shrink the first
+ self.click_card_row("GPT partitions", 1)
+ b.click(self.card_button("Partition", "Shrink"))
+ self.dialog({"size": 50})
+ b.wait_in_text(self.card_desc("Partition", "Size"), "50.3 MB")
+
+ # Grow it back externally, Cockpit should complain. Shrink it
+ # again with Cockpit.
+ m.execute(f"parted -s {disk} resizepart 1 80.7MB")
+ b.click(self.card_button("Partition", "Shrink partition"))
+ b.wait_in_text(self.card_desc("Partition", "Size"), "50.3 MB")
+
+ # Grow it back externally again. Grow the filesystem with
+ # Cockpit.
+ m.execute(f"parted -s {disk} resizepart 1 80.7MB")
+ b.click(self.card_button("Partition", "Grow content"))
+ b.wait_in_text(self.card_desc("Partition", "Size"), "79.7 MB")
+ b.wait_visible(self.card_button("Partition", "Grow") + ":disabled")
+
+ # Delete second partition and grow the first to take all the
+ # space.
+ b.click(self.card_parent_link())
+ self.click_card_row("GPT partitions", 2)
+ self.click_card_dropdown("Partition", "Delete")
+ self.confirm()
+ self.click_card_row("GPT partitions", location="/foo1 (not mounted)")
+ b.click(self.card_button("Partition", "Grow"))
+ self.dialog({"size": 103})
+ b.wait_visible(self.card_button("Partition", "Grow") + ":disabled")
+ b.wait_in_text(self.card_desc("Partition", "Size"), "103 MB")
+
+ def testType(self):
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk(100)
+ self.click_card_row("Storage", name=disk)
+
+ # GPT
+
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.dialog({"type": "gpt"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition")
+ self.dialog({"type": "empty"})
+
+ self.click_card_row("GPT partitions", 1)
+ b.wait_text(self.card_desc("Partition", "Type"), "Linux filesystem data")
+ b.click(self.card_desc_action("Partition", "Type"))
+ self.dialog({"type": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"})
+ b.wait_text(self.card_desc("Partition", "Type"), "EFI system partition")
+ b.click(self.card_desc_action("Partition", "Type"))
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "custom")
+ self.dialog_set_val("custom", "bla bla")
+ self.dialog_apply()
+ self.dialog_wait_error("custom", "Type can only contain the characters 0 to 9, A to F, and \"-\".")
+ self.dialog_set_val("custom", "7D0359A3-02B3-4F0A865C-654403E70625")
+ self.dialog_apply()
+ self.dialog_wait_error("custom", "Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN.")
+ self.dialog_set_val("custom", "7D0359A3-02B3-4F0A-865C-654403E70625")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_desc("Partition", "Type"), "7d0359a3-02b3-4f0a-865c-654403e70625")
+
+ # DOS
+
+ b.click(self.card_parent_link())
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.dialog({"type": "dos"})
+ b.wait_text(self.card_row_col("DOS partitions", 1, 1), "Free space")
+
+ self.click_dropdown(self.card_row("DOS partitions", 1), "Create partition")
+ self.dialog({"size": 100, "type": "empty"})
+
+ self.click_card_row("DOS partitions", 1)
+ b.wait_text(self.card_desc("Partition", "Type"), "Linux filesystem data")
+ b.click(self.card_desc_action("Partition", "Type"))
+ self.dialog({"type": "ef"})
+ b.wait_text(self.card_desc("Partition", "Type"), "EFI system partition")
+ b.click(self.card_desc_action("Partition", "Type"))
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "custom")
+ self.dialog_set_val("custom", "bla bla")
+ self.dialog_apply()
+ self.dialog_wait_error("custom", "Type must contain exactly two hexadecimal characters (0 to 9, A to F).")
+ self.dialog_set_val("custom", "C8")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_desc("Partition", "Type"), "c8")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-raid1 b/test/verify/check-storage-raid1
new file mode 100755
index 0000000..f1289db
--- /dev/null
+++ b/test/verify/check-storage-raid1
@@ -0,0 +1,54 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageRaid1(storagelib.StorageCase):
+
+ def testRaidLevelOne(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Create two disks and make a RAID out of them
+ disk1 = self.add_loopback_disk()
+ disk2 = self.add_loopback_disk()
+ b.wait_visible(self.card_row("Storage", name=disk1))
+ b.wait_visible(self.card_row("Storage", name=disk2))
+
+ self.addCleanup(m.execute, "mdadm --manage --stop /dev/md/SOMERAID")
+ self.click_dropdown(self.card_header("Storage"), "Create MDRAID device")
+ self.dialog_wait_open()
+ # No swap block devices should show up
+ b.wait_not_in_text("#dialog .pf-v5-c-data-list", "zram")
+ self.dialog_set_val("level", "raid1")
+ self.dialog_set_val("disks", {disk1: True, disk2: True})
+ self.dialog_set_val("name", "SOMERAID")
+ # The dialog should make sure that the Chunk size is ignored (has to be 0 for RAID 1)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_visible(self.card_row("Storage", name="/dev/md/SOMERAID"))
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-resize b/test/verify/check-storage-resize
new file mode 100755
index 0000000..1732e71
--- /dev/null
+++ b/test/verify/check-storage-resize
@@ -0,0 +1,346 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import unittest
+
+import storagelib
+import testlib
+
+
+class TestStorageResize(storagelib.StorageCase):
+
+ # LUKS uses memory hard PBKDF, 1 GiB is not enough; see https://bugzilla.redhat.com/show_bug.cgi?id=1881829
+ provision = {
+ "0": {"memory_mb": 1536}
+ }
+
+ def checkResize(self, fsys, crypto, can_shrink, can_grow, shrink_needs_unmount=None, grow_needs_unmount=None):
+ m = self.machine
+ b = self.browser
+
+ need_passphrase = crypto and self.default_crypto_type == "luks2"
+ filesystem_desc = f"{fsys} filesystem"
+ filesystem_desc_enc = filesystem_desc
+ if crypto:
+ filesystem_desc_enc = filesystem_desc + " (encrypted)"
+
+ self.login_and_go("/storage")
+ disk = m.add_disk("500M", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=f"/dev/{disk['dev']}"))
+
+ m.execute("vgcreate TEST /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_DISK1")
+ b.wait_visible(self.card_row("Storage", name="TEST"))
+
+ m.execute("lvcreate TEST -n vol -L 320m") # minimum xfs size is ~300MB
+ b.wait_visible(self.card_row("Storage", name="vol"))
+
+ self.click_card_row("Storage", name="TEST")
+ b.wait_visible(self.card("LVM2 volume group"))
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "vol")
+
+ self.click_dropdown(self.card_row("LVM2 logical volumes", 1), "Format")
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ self.dialog_set_val("name", "FSYS")
+ self.dialog_set_val("type", fsys)
+ self.dialog_set_val("mount_point", "/run/foo")
+ if crypto:
+ self.dialog_set_val("crypto", self.default_crypto_type)
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_set_val("passphrase2", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ self.dialog_wait_close()
+
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), filesystem_desc_enc)
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 3), "/run/foo")
+
+ if can_grow:
+ b.click(self.card_row_col("LVM2 logical volumes", 1, 1))
+ b.click(self.card_button("LVM2 logical volume", "Grow"))
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ if grow_needs_unmount:
+ b.wait_in_text("#dialog", "unmount, grow")
+ self.dialog_set_val("size", 400)
+ if need_passphrase:
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), "398 MB")
+ with b.wait_timeout(30):
+ self.wait_mounted(filesystem_desc)
+ size = int(m.execute("df -k --output=size /run/foo | tail -1").strip())
+ self.assertGreater(size, 300000)
+ else:
+ self.wait_card_button_disabled("LVM2 logical volume", "Grow")
+
+ if can_shrink:
+ b.click(self.card_button("LVM2 logical volume", "Shrink"))
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ if shrink_needs_unmount:
+ b.wait_in_text("#dialog", "unmount, shrink")
+ self.dialog_set_val("size", 200)
+ if need_passphrase:
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), "201 MB")
+ self.wait_mounted(filesystem_desc)
+ size = int(m.execute("df -k --output=size /run/foo | tail -1").strip())
+ self.assertLess(size, 300000)
+ else:
+ self.wait_card_button_disabled("LVM2 logical volume", "Shrink")
+
+ def testResizeExt4(self):
+ self.checkResize("ext4", crypto=False,
+ can_shrink=True, shrink_needs_unmount=True,
+ can_grow=True, grow_needs_unmount=False)
+
+ def testResizeXfs(self):
+ self.checkResize("xfs", crypto=False,
+ can_shrink=False,
+ can_grow=True, grow_needs_unmount=False)
+
+ @testlib.skipImage("NTFS not supported on RHEL", "rhel-*", "centos-*")
+ def testResizeNtfs(self):
+ self.checkResize("ntfs", crypto=False,
+ can_shrink=True, shrink_needs_unmount=True,
+ can_grow=True, grow_needs_unmount=True)
+
+ @testlib.skipImage("TODO: arch does not mount the LUKS partition after growing", "arch")
+ def testResizeLuks(self):
+ self.checkResize("ext4", crypto=True,
+ can_shrink=True, shrink_needs_unmount=True,
+ can_grow=True, grow_needs_unmount=False)
+
+ def shrink_extfs(self, fs_dev, size):
+ # fsadm can automatically unmount and check the fs when doing
+ # a resize, but in that case it will try to remount it
+ # afterwards. This remounting will mostly fail because
+ # UDisks2 has removed the mount point directory in the mean
+ # time. But sometimes it will succeed. So we take control
+ # and unmount the fs explicitly. But then we also need to
+ # check it explicitly.
+ #
+ self.machine.execute(f"(! findmnt -S '{fs_dev}' || umount '{fs_dev}'); fsadm -y check '{fs_dev}'; fsadm -y resize '{fs_dev}' '{size}'; udevadm trigger")
+
+ def testGrowShrinkHelp(self):
+ m = self.machine
+ b = self.browser
+
+ if self.storaged_version < [2, 7, 6]:
+ # No Filesystem.Size property
+ raise unittest.SkipTest("UDisks2 too old")
+
+ self.login_and_go("/storage")
+ disk = m.add_disk("500M", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=f"/dev/{disk['dev']}"))
+
+ m.execute("vgcreate TEST /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_DISK1")
+ b.wait_visible(self.card_row("Storage", name="TEST"))
+
+ m.execute("lvcreate TEST -n vol -L 200m")
+ b.wait_visible(self.card_row("Storage", name="vol"))
+
+ self.click_card_row("Storage", name="TEST")
+ b.wait_visible(self.card("LVM2 volume group"))
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "vol")
+
+ mountpoint = "/run/foo"
+ self.click_dropdown(self.card_row("LVM2 logical volumes", 1), "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("name", "FSYS")
+ self.dialog_set_val("type", "ext4")
+ self.dialog_set_val("mount_point", mountpoint)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "ext4 filesystem")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 3), mountpoint)
+
+ # Click on the card to check the label
+ b.click(self.card_row_col("LVM2 logical volumes", 1, 1))
+ b.wait_text(self.card_desc("ext4 filesystem", "Name"), "FSYS")
+
+ # Grow the logical volume and let Cockpit grow the filesystem
+ m.execute("lvresize TEST/vol -L+100M")
+ b.click(self.card_button("LVM2 logical volume", "Grow content"))
+ b.wait_not_present(self.card_button("LVM2 logical volume", "Grow content"))
+ size = int(m.execute(f"df -k --output=size {mountpoint} | tail -1").strip())
+ self.assertGreater(size, 250000)
+
+ # Shrink the filesystem and let Cockpit shrink the logical volume
+
+ fs_dev = m.execute("lsblk -pnl /dev/TEST/vol -o NAME | tail -1").strip()
+ self.shrink_extfs(fs_dev, "200M")
+
+ b.click(self.card_button("ext4 filesystem", "Mount now"))
+ self.wait_mounted("ext4 filesystem")
+ b.click(self.card_button("LVM2 logical volume", "Shrink volume"))
+ b.wait_not_present(self.card_button("LVM2 logical volume", "Shrink volume"))
+ size = int(m.execute("lvs TEST/vol -o lv_size --noheading --units b --nosuffix"))
+ self.assertLess(size, 250000000)
+
+ def testGrowShrinkEncryptedHelp(self):
+ m = self.machine
+ b = self.browser
+
+ if self.storaged_version < [2, 8, 0]:
+ # No Encrypted.MetadataSize property
+ raise unittest.SkipTest("UDisks2 too old")
+
+ self.login_and_go("/storage")
+ disk = m.add_disk("500M", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=f"/dev/{disk['dev']}"))
+
+ m.execute("vgcreate TEST /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_DISK1")
+ b.wait_visible(self.card_row("Storage", name="TEST"))
+
+ m.execute("lvcreate TEST -n vol -L 200m")
+ b.wait_visible(self.card_row("Storage", name="vol"))
+
+ self.click_card_row("Storage", name="TEST")
+ b.wait_visible(self.card("LVM2 volume group"))
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "vol")
+
+ mountpoint = "/run/foo"
+ self.click_dropdown(self.card_row("LVM2 logical volumes", 1), "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("name", "FSYS")
+ self.dialog_set_val("type", "ext4")
+ self.dialog_set_val("crypto", self.default_crypto_type)
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_set_val("passphrase2", "vainu-reku-toma-rolle-kaja")
+ self.dialog_set_val("mount_point", mountpoint)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "ext4 filesystem (encrypted)")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 3), mountpoint)
+
+ # Grow the logical volume and let Cockpit grow the LUKS container and the filesystem
+ m.execute("lvresize TEST/vol -L+100M")
+
+ def confirm_with_passphrase():
+ if self.default_crypto_type == "luks1":
+ return
+ self.dialog_wait_open()
+ self.dialog_wait_apply_enabled()
+ self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.click(self.card_row_col("LVM2 logical volumes", 1, 1))
+ b.click(self.card_button("LVM2 logical volume", "Grow content"))
+ confirm_with_passphrase()
+ b.wait_not_present(self.card_button("LVM2 logical volume", "Grow content"))
+ size = int(m.execute(f"df -k --output=size {mountpoint} | tail -1").strip())
+ self.assertGreater(size, 250000)
+
+ # Shrink the filesystem and let Cockpit shrink the LUKS container and logical volume
+
+ fs_dev = m.execute("lsblk -pnl /dev/TEST/vol -o NAME | tail -1").strip()
+ self.shrink_extfs(fs_dev, "200M")
+ b.click(self.card_button("ext4 filesystem", "Mount now"))
+ self.wait_mounted("ext4 filesystem")
+
+ b.click(self.card_button("LVM2 logical volume", "Shrink volume"))
+ confirm_with_passphrase()
+ b.wait_not_present(self.card_button("LVM2 logical volume", "Shrink volume"))
+ size = int(m.execute("lvs TEST/vol -o lv_size --noheading --units b --nosuffix"))
+ self.assertLess(size, 250000000)
+
+ # Grow the logical volume and the LUKS container and let Cockpit grow the filesystem
+
+ m.execute("lvresize TEST/vol -L+100M")
+ m.execute(f"echo vainu-reku-toma-rolle-kaja | cryptsetup resize {fs_dev}")
+
+ b.click(self.card_button("LVM2 logical volume", "Grow content"))
+ confirm_with_passphrase()
+ b.wait_not_present(self.card_button("LVM2 logical volume", "Grow volume"))
+ size = int(m.execute(f"df -k --output=size {mountpoint} | tail -1").strip())
+ self.assertGreater(size, 250000)
+
+ # Shrink the filesystem and the LUKS container and let Cockpit shrink the logical volume
+ self.shrink_extfs(fs_dev, "198M")
+ m.execute(f"echo vainu-reku-toma-rolle-kaja | cryptsetup resize '{fs_dev}' 200M")
+ b.click(self.card_button("ext4 filesystem", "Mount now"))
+ self.wait_mounted("ext4 filesystem")
+ b.click(self.card_button("LVM2 logical volume", "Shrink volume"))
+ confirm_with_passphrase()
+ b.wait_not_present(self.card_button("LVM2 logical volume", "Shrink volume"))
+ size = int(m.execute("lvs TEST/vol -o lv_size --noheading --units b --nosuffix"))
+ self.assertLess(size, 250000000)
+
+ def testUnsupported(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+ b.wait_visible(self.card("Storage"))
+ disk = m.add_disk("500M", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=f"/dev/{disk['dev']}"))
+
+ m.execute("vgcreate TEST /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_DISK1; udevadm trigger")
+ b.wait_visible(self.card_row("Storage", name="TEST"))
+
+ m.execute("lvcreate TEST -n vol -L 320m")
+ b.wait_visible(self.card_row("Storage", name="vol"))
+
+ self.click_card_row("Storage", name="TEST")
+ b.wait_visible(self.card("LVM2 volume group"))
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "vol")
+
+ m.execute("mkfs.ext4 -L FSYS /dev/TEST/vol")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "ext4 filesystem")
+
+ def fake_fstype(fstype):
+ # Change fstype via a udev rule
+ m.write("/run/udev/rules.d/99-fake-fstype.rules",
+ f'SUBSYSTEM=="block", ENV{{ID_FS_LABEL}}=="FSYS", ENV{{ID_FS_TYPE}}="{fstype}"\n')
+ m.execute("udevadm control --reload; udevadm trigger")
+
+ self.addCleanup(m.execute,
+ "rm /run/udev/rules.d/99-fake-fstype.rules; udevadm control --reload; udevadm trigger")
+
+ def check_btn(title, excuse):
+ btn = self.card_button("LVM2 logical volume", title)
+ b.wait_visible(btn)
+ b.mouse(btn, "mouseenter", 0, 0, 0)
+ b.wait_in_text("#tip-storage", excuse)
+ b.mouse(btn, "mouseleave", 0, 0, 0)
+ b.wait_not_present("#tip-storage")
+
+ fake_fstype("udf") # UDF is a real filesystem type that UDisks knows about. It can definitely not be resized.
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "udf filesystem")
+ b.click(self.card_row_col("LVM2 logical volumes", 1, 1))
+ check_btn("Shrink", "udf can not be resized")
+ check_btn("Grow", "udf can not be resized")
+ b.click(".pf-v5-c-breadcrumb__link:contains('TEST')")
+
+ fake_fstype("fake") # This is not a real filesystem and UDisks2 knows nothing about it.
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 2), "fake filesystem")
+ b.click(self.card_row_col("LVM2 logical volumes", 1, 1))
+ check_btn("Shrink", "fake can not be resized here")
+ check_btn("Grow", "fake can not be resized here")
+ b.click(".pf-v5-c-breadcrumb__link:contains('TEST')")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-scaling b/test/verify/check-storage-scaling
new file mode 100755
index 0000000..9b4043f
--- /dev/null
+++ b/test/verify/check-storage-scaling
@@ -0,0 +1,50 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+class TestStorageScaling(storagelib.StorageCase):
+
+ def testScaling(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ def count_rows():
+ return b.call_js_func('ph_count', self.card("Storage") + " tbody tr")
+
+ b.wait_visible(self.card_row("Storage", name="/dev/vda"))
+ # Wait on btrfs subvolumes on OS'es with the install on btrfs
+ if m.image.startswith('fedora'):
+ b.wait_in_text(self.card_row("Storage", name="root/var/lib/machines"), "btrfs subvolume")
+ elif m.image == "arch":
+ b.wait_in_text(self.card_row("Storage", name="swap"), "btrfs subvolume")
+ n_rows = count_rows()
+
+ m.execute("modprobe scsi_debug num_tgts=200")
+ with b.wait_timeout(60):
+ b.click(self.card_button("Storage", f"Show all {n_rows + 200} rows"))
+ b.wait(lambda: count_rows() == n_rows + 200)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-stratis b/test/verify/check-storage-stratis
new file mode 100755
index 0000000..5bdfa76
--- /dev/null
+++ b/test/verify/check-storage-stratis
@@ -0,0 +1,980 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2021 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import subprocess
+
+import packagelib
+import storagelib
+import testlib
+
+PV_SIZE = 4000 # 4 GB in MB
+
+
+def get_stratis_stop_type_opt(execute):
+ """Get `stratis stop pool` required option for a pool name
+
+ The CLI changed in an incompatible way in Fedora 40, it needs an extra --name option
+ which cannot be provided in earlier versions.
+ """
+ try:
+ if '--name' in execute("stratis pool stop --help"):
+ return "--name"
+ except subprocess.CalledProcessError:
+ # on RHEL 8 this fails with "error: invalid choice"
+ pass
+ return ""
+
+
+def create_pool_key(machine, keyname, passphrase):
+ # this is a bit complicated, see https://bugzilla.redhat.com/show_bug.cgi?id=2246923
+ machine.execute(f"echo -n '{passphrase}' | stratis key set --keyfile-path /dev/stdin {keyname}")
+
+
+@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
+@testlib.nondestructive
+class TestStorageStratis(storagelib.StorageCase):
+ def setUp(self):
+ super().setUp()
+ exe = self.machine.execute
+
+ exe("systemctl start stratisd")
+ self.addCleanup(exe, "systemctl stop stratisd")
+
+ self.stratis_v2 = self.image.startswith("rhel-8") or self.image == "centos-8-stream"
+ self.stop_type_opt = get_stratis_stop_type_opt(exe)
+
+ self.addCleanup(exe,
+ "stratis report | jq -r '.pools[] | .name' |"
+ "xargs -n1 --no-run-if-empty stratis pool destroy")
+ self.addCleanup(exe,
+ "stratis report | jq -r '.pools[] | .name' |"
+ f"xargs -n1 --no-run-if-empty stratis pool stop {self.stop_type_opt}")
+ self.addCleanup(exe,
+ "mount | grep mapper/stratis | awk '{print $1}' | xargs --no-run-if-empty umount")
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # use fixed names, to avoid grabbing loop1 and loop12 (which losetup sometimes likes to do)
+ # as that clashes with :contains(loop1) below; also, fix names for pixel test
+ dev_1 = self.add_loopback_disk(PV_SIZE, name="loop10")
+ dev_2 = self.add_loopback_disk(PV_SIZE, name="loop11")
+ dev_3 = self.add_loopback_disk(PV_SIZE, name="loop12")
+ dev_4 = self.add_loopback_disk(PV_SIZE, name="loop13")
+ dev_5 = self.add_loopback_disk(PV_SIZE, name="loop14")
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+ b.wait_visible(self.card_row("Storage", name=dev_2))
+ b.wait_visible(self.card_row("Storage", name=dev_3))
+ b.wait_visible(self.card_row("Storage", name=dev_4))
+ b.wait_visible(self.card_row("Storage", name=dev_5))
+
+ # Create a pool
+ self.dialog_open_with_retry(trigger=lambda: self.click_devices_dropdown("Create Stratis pool"),
+ expect=lambda: (self.dialog_is_present('disks', dev_1) and
+ self.dialog_is_present('disks', dev_2) and
+ self.dialog_check({"name": "pool0"})))
+ self.dialog_set_val("disks", {dev_1: True, dev_2: True})
+ b.assert_pixels("#dialog", "create-pool")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.wait_visible(self.card_row("Storage", name="pool0"))
+ b.wait_not_present(self.card_row("Storage", name="pool0") + " .ct-icon-exclamation-triangle")
+
+ # Check that the next name is "pool1"
+ self.click_devices_dropdown("Create Stratis pool")
+ self.dialog_wait_open()
+ self.dialog_wait_val("name", "pool1")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ if not self.stratis_v2:
+ # Stop the pool (only works with Stratis 3)
+ pool_uuid = m.execute("stratis --unhyphenated-uuids pool list --name pool0 | grep ^UUID | cut -d' ' -f2").strip()
+ m.execute(f"stratis pool stop {self.stop_type_opt} pool0")
+ b.wait_in_text(self.card_row("Storage", name=pool_uuid), "Stratis pool")
+
+ # Start it
+ self.click_dropdown(self.card_row("Storage", name=pool_uuid), "Start")
+ b.wait_visible(self.card_row("Storage", name="pool0"))
+
+ self.click_card_row("Storage", name="pool0")
+ b.wait_text(self.card_desc("Stratis pool", "Name"), "pool0")
+ b.wait_in_text(self.card_desc("Stratis pool", "Usage"), "8 GB")
+ b.wait_not_present('.pf-v5-c-alert')
+
+ udisk_contains_stratis_private = "physical-originsub" in m.execute("udisksctl dump")
+
+ # Create two filesystems
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog_wait_open()
+ self.dialog_set_val('name', 'fsys1')
+ self.dialog_set_val('mount_point', '/run/fsys1')
+ b.assert_pixels("#dialog", "create-fsys")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.addCleanup(m.execute, "umount /run/fsys1 || true")
+
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1")
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys1")
+
+ self.assertEqual(self.inode(m.execute("findmnt -n -o SOURCE /run/fsys1").strip()),
+ self.inode("/dev/stratis/pool0/fsys1"))
+
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog({'name': 'fsys2',
+ 'mount_point': '/run/fsys2'})
+ self.addCleanup(m.execute, "umount /run/fsys2 || true")
+ b.wait_text(self.card_row_col("Stratis filesystems", 2, 1), "fsys2")
+ b.wait_text(self.card_row_col("Stratis filesystems", 2, 3), "/run/fsys2")
+ b.assert_pixels(self.card("Stratis filesystems"), "fsys-rows")
+ self.assertEqual(self.inode(m.execute("findmnt -n -o SOURCE /run/fsys2").strip()),
+ self.inode("/dev/stratis/pool0/fsys2"))
+ m.write("/run/fsys2/FILE", "Hello Stratis!")
+
+ # Check that they have entries in fstab
+ self.assertNotEqual(m.execute("grep /run/fsys1 /etc/fstab"), "")
+ self.assertNotEqual(m.execute("grep /run/fsys2 /etc/fstab"), "")
+
+ # Rename one filesystem
+ self.click_card_row("Stratis filesystems", 1)
+ b.click(self.card_desc_action("Stratis filesystem", "Name"))
+ self.dialog({'name': "fsys1-renamed"})
+ b.wait_text(self.card_desc("Stratis filesystem", "Name"), "fsys1-renamed")
+ b.click(self.card_parent_link())
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1-renamed")
+
+ # Destroy one filesystem
+ self.click_card_row("Stratis filesystems", 1)
+ self.click_card_dropdown("Stratis filesystem", "Delete")
+ self.dialog_wait_open()
+ b.assert_pixels("#dialog", "delete-fsys")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_visible(self.card("Stratis filesystems"))
+ b.wait_not_present(self.card_row("Stratis filesystems", name="fsys1-renamed"))
+
+ # Unmount and remount the other filesystem
+ self.click_dropdown(self.card_row("Stratis filesystems", 1), "Unmount")
+ self.confirm()
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys2 (not mounted)")
+ self.click_dropdown(self.card_row("Stratis filesystems", 1), "Mount")
+ self.dialog({})
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys2")
+
+ # Make a copy of the filesystem
+ self.click_dropdown(self.card_row("Stratis filesystems", 1), "Snapshot")
+ self.dialog_wait_open()
+ self.dialog_set_val('name', 'fsys2-copy')
+ self.dialog_set_val('mount_point', '/run/fsys2-copy')
+ self.dialog_set_val('at_boot', 'never')
+ b.assert_pixels("#dialog", "copy-fsys")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_row_col("Stratis filesystems", 2, 1), "fsys2-copy")
+ b.wait_text(self.card_row_col("Stratis filesystems", 2, 3), "/run/fsys2-copy")
+
+ self.assertEqual("Hello Stratis!", m.execute("cat /run/fsys2-copy/FILE"))
+
+ # Delete the copy
+ self.click_card_row("Stratis filesystems", 2)
+ self.click_card_dropdown("Stratis filesystem", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("Stratis filesystems"))
+ b.wait_not_present(self.card_row("Stratis filesystems", name="fsys2-copy"))
+
+ # Make an unmounted copy of the filesystem
+ self.click_dropdown(self.card_row("Stratis filesystems", 1), "Snapshot")
+ self.dialog_wait_open()
+ self.dialog_set_val('name', 'fsys2-copy')
+ self.dialog_set_val('at_boot', 'never')
+ self.dialog_apply_secondary()
+ self.dialog_wait_close()
+ b.wait_text(self.card_row_col("Stratis filesystems", 2, 1), "fsys2-copy")
+ b.wait_text(self.card_row_col("Stratis filesystems", 2, 3), "(not mounted)")
+
+ # Delete the copy
+ self.click_card_row("Stratis filesystems", 2)
+ self.click_card_dropdown("Stratis filesystem", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("Stratis filesystems"))
+ b.wait_not_present(self.card_row("Stratis filesystems", name="fsys2-copy"))
+
+ # Create an unmounted filesystem
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog_wait_open()
+ self.dialog_set_val('name', 'fsys-unmounted')
+ self.dialog_apply_secondary()
+ self.dialog_wait_close()
+
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys-unmounted")
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "(not mounted)")
+
+ # Delete the unmounted filesystem
+ self.click_card_row("Stratis filesystems", 1)
+ self.click_card_dropdown("Stratis filesystem", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("Stratis filesystems"))
+ b.wait_not_present(self.card_row("Stratis filesystems", name="fsys-unmounted"))
+
+ # Add a data blockdev
+ b.click(self.card_button("Stratis pool", "Add block device"))
+ self.dialog_wait_open()
+ self.dialog_apply()
+ self.dialog_wait_error("disks", "At least one")
+ self.dialog_set_val('disks', {dev_3: True})
+ b.assert_pixels("#dialog", "add-disk")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.wait_visible(self.card_row("Stratis pool", name=dev_3))
+ b.wait_in_text(self.card_desc("Stratis pool", "Usage"), "12 GB")
+
+ # Add a cache blockdev
+ b.click(self.card_button("Stratis pool", "Add block device"))
+ self.dialog({'tier': "cache",
+ 'disks': {dev_4: True}})
+ b.wait_in_text(self.card_row("Stratis pool", name=dev_4), "cache")
+
+ # Add a second cache blockdev, this uses a different code path
+ b.click(self.card_button("Stratis pool", "Add block device"))
+ self.dialog({'tier': "cache",
+ 'disks': {dev_5: True}})
+ b.wait_in_text(self.card_row("Stratis pool", name=dev_5), "cache")
+
+ # Rename the pool
+ b.click(self.card_desc_action("Stratis pool", "Name"))
+ self.dialog({'name': "pool0-renamed"})
+ b.wait_text(self.card_desc("Stratis pool", "Name"), "pool0-renamed")
+
+ # Create another filesystem
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog({'name': 'fsys3',
+ 'mount_point': '/run/fsys3'})
+ b.wait_text(self.card_row_col("Stratis filesystems", 2, 1), "fsys3")
+ b.wait_text(self.card_row_col("Stratis filesystems", 2, 3), "/run/fsys3")
+ self.assertEqual(self.inode(m.execute("findmnt -n -o SOURCE /run/fsys3").strip()),
+ self.inode("/dev/stratis/pool0-renamed/fsys3"))
+
+ # Destroy the pool
+ self.click_card_dropdown("Stratis pool", "Delete")
+ self.dialog_wait_open()
+ b.assert_pixels('#dialog', "delete-pool")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name="pool0-renamed"))
+
+ # Check that the entries have disappeared from fstab
+ self.assertEqual(m.execute("grep /run/fsys1 /etc/fstab || true"), "")
+ self.assertEqual(m.execute("grep /run/fsys2 /etc/fstab || true"), "")
+ self.assertEqual(m.execute("grep /run/fsys3 /etc/fstab || true"), "")
+
+ m.execute("! findmnt /run/fsys1")
+ m.execute("! findmnt /run/fsys2")
+ m.execute("! findmnt /run/fsys2-copy")
+ m.execute("! findmnt /run/fsys3")
+
+ # https://bugzilla.redhat.com/show_bug.cgi?id=2183084
+ # Do this assersion in the end so that the previous checks still run.
+ # After the stratis pool is deleted we can't check this, so use the value from earlier.
+ self.assertFalse(udisk_contains_stratis_private)
+
+ @testlib.skipImage("Stratis too old", "rhel-8-*", "centos-8-*")
+ def testAlerts(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ dev_1 = self.add_loopback_disk(PV_SIZE)
+ dev_2 = self.add_loopback_disk(PV_SIZE)
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+ b.wait_visible(self.card_row("Storage", name=dev_2))
+
+ # Create an encrypted pool with two block devices
+ self.click_devices_dropdown("Create Stratis pool")
+ self.dialog_wait_open()
+ self.dialog_set_val("encrypt_pass.on", val=True)
+ self.dialog_set_val("passphrase", "foodeeboodeebar")
+ self.dialog_set_val("passphrase2", "foodeeboodeebar")
+ self.dialog_set_val("disks", {dev_1: True})
+ self.dialog_set_val("disks", {dev_2: True})
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ b.wait_visible(self.card_row("Storage", name="pool0"))
+ b.wait_not_present(self.card_row("Storage", name="pool0") + " .ct-icon-exclamation-triangle")
+
+ # Check that there is no alert on the details page
+ self.click_card_row("Storage", name="pool0")
+ b.wait_visible(self.card("Encrypted Stratis pool"))
+ b.wait_not_present('.pf-v5-c-alert')
+
+ m.execute(f"""
+JSON=$(sudo cryptsetup token export --token-id=1 {dev_1} \
+ | jq '.key_description = "stratis-1-key-no-other-is-the-same"')
+sudo cryptsetup token remove --token-id=1 {dev_1}
+echo $JSON | sudo cryptsetup token import --token-id=1 {dev_1}
+systemctl restart stratisd
+ """)
+
+ b.go('#/')
+ b.wait_visible(self.card_row("Storage", name="pool0") + " .ct-icon-exclamation-triangle")
+
+ self.click_card_row("Storage", name="pool0")
+ b.wait_visible('.pf-v5-c-alert:contains("This pool is in a degraded state")')
+
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog_wait_open()
+ self.dialog_set_val("name", "fsys1")
+ self.dialog_set_val("mount_point", "/run/fsys1")
+ self.dialog_apply()
+ self.dialog_wait_alert("Pool is in state NoRequests where this action cannot be performed until the issue is resolved manually")
+
+ @testlib.nondestructive
+ def testCli(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ dev_1 = self.add_loopback_disk(PV_SIZE)
+ dev_2 = self.add_loopback_disk(PV_SIZE)
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+ b.wait_visible(self.card_row("Storage", name=dev_2))
+
+ # Create a pool outside of Cockpit
+ m.execute(f"stratis pool create TEST1 {dev_1} {dev_2}")
+ b.wait_visible(self.card_row("Storage", name="TEST1"))
+ b.wait_in_text(self.card_row("Storage", name=dev_1), "Stratis block device")
+ b.wait_in_text(self.card_row("Storage", name=dev_1), "TEST1")
+ b.wait_in_text(self.card_row("Storage", name=dev_2), "Stratis block device")
+ b.wait_in_text(self.card_row("Storage", name=dev_1), "TEST1")
+
+ # Create two filesystems outside of Cockpit
+ m.execute("stratis filesystem create TEST1 fsys1")
+ b.wait_visible(self.card_row("Storage", name="fsys1"))
+ m.execute("stratis filesystem create TEST1 fsys2")
+ b.wait_visible(self.card_row("Storage", name="fsys2"))
+
+ mount = f"{self.vm_tmpdir}/fsys1"
+
+ # Mount externally, adjust fstab with Cockpit
+ self.click_card_row("Storage", name="fsys1")
+ m.execute(f"mkdir {mount}; mount /dev/stratis/TEST1/fsys1 {mount}")
+ b.click(self.card_button("Stratis filesystem", f"Mount automatically on {mount} on boot"))
+ b.wait_not_present(self.card_button("Stratis filesystem", f"Mount automatically on {mount} on boot"))
+ self.assertIn("stratis-fstab-setup", m.execute(f"grep {mount} /etc/fstab"))
+
+ # Unmount externally, adjust fstab with Cockpit
+ m.execute(f"umount {mount}")
+ b.click(self.card_button("Stratis filesystem", "Do not mount automatically on boot"))
+ b.wait_not_present(self.card_button("Stratis filesystem", "Do not mount automatically on boot"))
+ self.assertIn("noauto", m.execute(f"grep {mount} /etc/fstab"))
+
+ # Destroy them outside of Cockpit
+ b.click(self.card_parent_link())
+ b.wait_visible(self.card("Stratis filesystems"))
+ m.execute("stratis filesystem destroy TEST1 fsys1")
+ b.wait_not_present(self.card_row("Stratis filesystems", name="fsys1"))
+ m.execute("stratis filesystem destroy TEST1 fsys2")
+ b.wait_not_present(self.card_row("Stratis filesystems", name="fsys2"))
+
+ # Destroy the pool outside of Cockpit
+ m.execute("stratis pool destroy TEST1")
+ b.wait_in_text("main", "Not found")
+
+ b.go("#/")
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name="TEST1"))
+
+
+@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
+class TestStorageStratisReboot(storagelib.StorageCase):
+ # LUKS uses memory hard PBKDF, 1 GiB is not enough; see https://bugzilla.redhat.com/show_bug.cgi?id=1881829
+ provision = {
+ "0": {"memory_mb": 1536}
+ }
+
+ def setUp(self):
+ super().setUp()
+ exe = self.machine.execute
+
+ if self.image == "arch":
+ # Arch Linux does not enable systemd units by default
+ exe("systemctl enable --now stratisd")
+ self.addCleanup(self.machine.execute, "systemctl disable --now stratisd")
+
+ self.stratis_v2 = self.image.startswith("rhel-8") or self.image == "centos-8-stream"
+
+ def testEncrypted(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ dev_1 = "/dev/sda"
+ m.add_disk("4G", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+
+ dev_2 = "/dev/sdb"
+ m.add_disk("4G", serial="DISK2")
+ b.wait_visible(self.card_row("Storage", name=dev_2))
+
+ dev_3 = "/dev/sdc"
+ m.add_disk("4G", serial="DISK3")
+ b.wait_visible(self.card_row("Storage", name=dev_3))
+
+ passphrase = "foodeeboodeebar"
+
+ # Create an encrypted pool with a filesystem, but don't mount
+ # it. Cockpit will chose a key description for the pool and
+ # we occupy its first choice in order to force Cockpit to use
+ # something else.
+ create_pool_key(m, "pool0", "not-the-passphrase")
+ self.dialog_open_with_retry(trigger=lambda: self.click_devices_dropdown("Create Stratis pool"),
+ expect=lambda: (self.dialog_is_present('disks', dev_1) and
+ self.dialog_check({"name": "pool0"})))
+ self.dialog_set_val("encrypt_pass.on", val=True)
+ self.dialog_set_val("passphrase", passphrase)
+ self.dialog_set_val("passphrase2", passphrase)
+ self.dialog_set_val("disks", {dev_1: True})
+ b.assert_pixels("#dialog", "create-encrypted-pool",
+ # The small checkbox ticks render inconsistently
+ ignore=["input[type=checkbox]"])
+ self.dialog_apply()
+ self.dialog_wait_close()
+ m.execute("stratis key unset pool0")
+
+ self.click_card_row("Storage", name="pool0")
+ b.wait_visible(self.card("Encrypted Stratis pool"))
+
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog({'name': 'fsys1',
+ 'mount_point': '/run/fsys1',
+ 'at_boot': 'local'},
+ secondary=True)
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1")
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys1 (not mounted)")
+
+ # Check that it has an entry in fstab and that it is "noauto"
+ self.assertIn("noauto", m.execute("grep /run/fsys1 /etc/fstab"))
+
+ # Add a data blockdev
+ b.click(self.card_button("Encrypted Stratis pool", "Add block device"))
+ self.dialog_wait_open()
+ self.dialog_set_val('disks', {dev_2: True})
+ self.dialog_apply()
+ self.dialog_wait_error("passphrase", "Passphrase cannot be empty")
+ self.dialog_set_val('passphrase', passphrase)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_in_text(self.card_row("Encrypted Stratis pool", name=dev_2), "data")
+
+ # Change the passphrase (if supported)
+ if not self.stratis_v2:
+ b.wait_visible(self.card_desc("Encrypted Stratis pool", "Passphrase") + " button:contains(Remove):disabled")
+ b.click(self.card_desc("Encrypted Stratis pool", "Passphrase") + " button:contains(Change)")
+ self.dialog({'old_passphrase': passphrase,
+ 'new_passphrase': "boodeefoodeebar",
+ 'new_passphrase2': "boodeefoodeebar"})
+ # do it again, with the old passphrase in the keyring
+ create_pool_key(m, "pool0", "boodeefoodeebar")
+ b.click(self.card_desc("Encrypted Stratis pool", "Passphrase") + " button:contains(Change)")
+ self.dialog({'new_passphrase': passphrase,
+ 'new_passphrase2': passphrase})
+ m.execute("stratis key unset pool0")
+
+ # Add a cache blockdev (if supported)
+ if not self.stratis_v2:
+ b.click(self.card_button("Encrypted Stratis pool", "Add block device"))
+ self.dialog_wait_open()
+ self.dialog_set_val('tier', "cache")
+ self.dialog_set_val('disks', {dev_3: True})
+ self.dialog_set_val('passphrase', passphrase)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_in_text(self.card_row("Encrypted Stratis pool", name=dev_3), "cache")
+
+ m.reboot()
+ m.start_cockpit()
+ b.relogin()
+ b.enter_page("/storage")
+
+ b.wait_visible(self.card("Stratis pool"))
+ b.wait_in_text(self.card("Stratis pool"), "DISK1")
+ b.wait_in_text(self.card("Stratis pool"), "DISK2")
+
+ # Unlock the pool
+ b.click(self.card_button("Stratis pool", "Start"))
+ self.dialog_wait_open()
+ self.dialog_set_val('passphrase', "wrong-passphrase")
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ b.wait_visible("#dialog .pf-v5-c-alert.pf-m-danger")
+ self.dialog_set_val('passphrase', passphrase)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_desc("Encrypted Stratis pool", "Name"), "pool0")
+
+ # Mount the filesystem
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys1 (not mounted)")
+ self.click_dropdown(self.card_row("Stratis filesystems", 1), "Mount")
+ self.dialog({})
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys1")
+
+ # Reboot (this requires the passphrase)
+ self.setup_systemd_password_agent(passphrase)
+ m.reboot()
+ m.start_cockpit()
+ b.relogin()
+ b.enter_page("/storage")
+ b.wait_text(self.card_desc("Encrypted Stratis pool", "Name"), "pool0")
+
+ # Filesystem should be mounted now
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys1")
+
+ # Destroy the pool
+ self.click_card_dropdown("Encrypted Stratis pool", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("Storage"))
+ b.wait_not_present(self.card_row("Storage", name="pool0"))
+
+ # Check that the entry has disappeared from fstab
+ self.assertEqual(m.execute("grep /run/fsys1 /etc/fstab || true"), "")
+
+ def testReboot(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ dev_1 = "/dev/sda"
+ m.add_disk("4G", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+
+ # Create a pool
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create Stratis pool"),
+ expect=lambda: (self.dialog_is_present('disks', dev_1) and
+ self.dialog_check({"name": "pool0"})),
+ values={"disks": {dev_1: True}})
+ self.click_card_row("Storage", name="pool0")
+
+ # Create a filesystems
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog({'name': 'fsys1',
+ 'mount_point': '/run/fsys1'})
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1")
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys1")
+
+ m.reboot()
+ m.start_cockpit()
+ b.relogin()
+ b.enter_page("/storage")
+
+ # Filesystem should be mounted now
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1")
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys1")
+
+ def testAtBoot(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ dev_1 = "/dev/sda"
+ m.add_disk("4G", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+
+ # Create a pool
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create Stratis pool"),
+ expect=lambda: (self.dialog_is_present('disks', dev_1) and
+ self.dialog_check({"name": "pool0"})),
+ values={"disks": {dev_1: True}})
+ self.click_card_row("Storage", name="pool0")
+
+ def create(at_boot):
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog({'name': 'fsys1',
+ 'mount_point': '/foo',
+ 'at_boot': at_boot})
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1")
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/foo")
+
+ def destroy():
+ self.click_card_row("Stratis filesystems", 1)
+ self.click_card_dropdown("Stratis filesystem", "Delete")
+ self.dialog_wait_open()
+ self.dialog_apply_with_retry("Device or resource busy")
+ b.wait_visible(self.card("Stratis filesystems"))
+ b.wait_not_present(self.card_row("Stratis filesystems", name="fsys1"))
+
+ create("local")
+ self.assertNotIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ destroy()
+
+ create("nofail")
+ self.assertIn("nofail", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ destroy()
+
+ create("netdev")
+ self.assertIn("_netdev", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ destroy()
+
+ create("never")
+ self.assertIn("x-cockpit-never-auto", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /foo"))
+ destroy()
+
+ @testlib.skipImage("Stratis too old", "rhel-8-*", "centos-8-*")
+ def testManagedSizes(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ dev_1 = "/dev/sda"
+ m.add_disk("4G", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+
+ # Create a "managed" pool
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create Stratis pool"),
+ expect=lambda: (self.dialog_is_present('disks', dev_1) and
+ self.dialog_check({"name": "pool0"})),
+ values={"managed.on": True, "disks": {dev_1: True}})
+ self.click_card_row("Storage", name="pool0")
+
+ # Create a small filesystem
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog({'name': 'fsys1',
+ 'size': 900,
+ 'mount_point': '/run/fsys1'})
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1")
+
+ # Make a snapshot of it
+ self.click_dropdown(self.card_row("Stratis filesystems", 1), "Snapshot")
+ self.dialog({'name': 'fsys1-copy',
+ 'mount_point': '/run/fsys1-copy'})
+ b.wait_text(self.card_row_col("Stratis filesystems", 2, 1), "fsys1-copy")
+
+ # And another filesystem
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog({'name': 'fsys2',
+ 'size': 800,
+ 'mount_point': '/run/fsys2'})
+ b.wait_text(self.card_row_col("Stratis filesystems", 3, 1), "fsys2")
+
+ # And fill the rest by accepting the default size
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog({'name': 'fsys3',
+ 'mount_point': '/run/fsys3'})
+ b.wait_text(self.card_row_col("Stratis filesystems", 4, 1), "fsys3")
+ b.wait_visible(self.card_button("Stratis filesystems", "Create new filesystem") + ":disabled")
+
+ # Snapshots are impossible now
+ self.click_dropdown(self.card_row("Stratis filesystems", 1), "Snapshot")
+ self.dialog_wait_open()
+ b.wait_in_text('#dialog', "Not enough space")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Delete a filesystem, and make another snapshot
+ self.click_card_row("Stratis filesystems", 1)
+ self.click_card_dropdown("Stratis filesystem", "Delete")
+ self.confirm()
+ b.wait_visible(self.card_button("Stratis filesystems", "Create new filesystem") + ":not(:disabled)")
+ self.click_dropdown(self.card_row("Stratis filesystems", 2), "Snapshot")
+ self.dialog({'name': 'fsys2-copy',
+ 'mount_point': '/run/fsys2-copy'})
+ b.wait_visible(self.card_row("Stratis filesystems", name="fsys2-copy"))
+
+ # And the pool should be full again
+ b.wait_visible("button:contains(Create new filesystem):disabled")
+
+ @testlib.skipImage("Stratis too old", "rhel-8-*", "centos-8-*")
+ def testPoolResize(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ dev = "/dev/sda"
+ m.add_disk("4G", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=dev))
+
+ # Create a logical volume that we will later grow
+ m.execute(f"vgcreate vgroup0 {dev}; lvcreate vgroup0 -n lvol0 -L 1500000256b")
+ b.wait_visible(self.card_row("Storage", name="lvol0"))
+
+ # Create a pool
+ self.dialog_with_retry(trigger=lambda: self.click_devices_dropdown("Create Stratis pool"),
+ expect=lambda: self.dialog_is_present('disks', "lvol0"),
+ values={"disks": {"lvol0": True}})
+ b.wait_in_text(self.card_row("Storage", name="pool0"), "1.5 GB")
+
+ # Grow the logical volume in Cockpit, the pool should grow automatically
+ self.click_card_row("Storage", name="lvol0")
+ b.click(self.card_button("LVM2 logical volume", "Grow"))
+ self.dialog({"size": 1600})
+ b.go("#/")
+ b.wait_in_text(self.card_row("Storage", name="pool0"), "1.6 GB")
+
+ # Grow the logical volume from outside of Cockpit, the pool should complain
+ m.execute("lvresize vgroup0/lvol0 -L +100000256b")
+ b.wait_visible(self.card_row("Storage", name="pool0") + ' .ct-icon-exclamation-triangle')
+ self.click_card_row("Storage", name="pool0")
+ b.wait_visible('.pf-v5-c-alert:contains("This pool does not use all the space")')
+ b.click('button:contains("Grow the pool")')
+ b.wait_not_present('.pf-v5-c-alert')
+ b.wait_in_text(self.card_desc("Stratis pool", "Usage"), "1.7 GB")
+
+ b.go("#/")
+
+ # Grow the logical volume from outside of Cockpit, the logical volume should also complain
+ m.execute("lvresize vgroup0/lvol0 -L +100000256b")
+ b.wait_visible(self.card_row("Storage", name="lvol0") + " .ct-icon-exclamation-triangle")
+ self.click_card_row("Storage", name="lvol0")
+ # First shrink the volume to test whether Cockpit can figure out the right size for that
+ b.click(self.card_button("LVM2 logical volume", "Shrink volume"))
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), "1.70 GB")
+ b.wait_not_present(self.card_button("LVM2 logical volume", "Shrink volume"))
+ # Then enlarge the volume from the outside again and grow the blockdev
+ m.execute("lvresize vgroup0/lvol0 -L +100000256b")
+ b.click(self.card_button("LVM2 logical volume", "Grow content"))
+ b.wait_not_present(self.card_button("LVM2 logical volume", "Grow content"))
+ b.go("#/")
+ b.wait_in_text(self.card_row("Storage", name="pool0"), "1.8 GB")
+
+
+@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*", "arch")
+class TestStoragePackagesStratis(packagelib.PackageCase, storagelib.StorageCase):
+
+ def testStratisOndemandInstallation(self):
+ m = self.machine
+ b = self.browser
+
+ # RHEL 8 should not offer installation of Stratis from Cockpit
+ # itself.
+ #
+ ondemand_stratis = "rhel-8" not in m.image
+
+ m.execute("systemctl stop stratisd && dnf remove -y stratisd stratis")
+ if ondemand_stratis:
+ self.addPackageSet("stratis")
+ self.enableRepo()
+
+ self.login_and_go("/storage")
+
+ dev_1 = "/dev/sda"
+ m.add_disk("4G", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+
+ if ondemand_stratis:
+ self.click_devices_dropdown("Create Stratis pool")
+ self.dialog_wait_open()
+ b.wait_in_text("#dialog", "The stratisd package must be installed")
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ self.dialog_wait_val("name", "pool0")
+ self.dialog_set_val("disks", {dev_1: True})
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_visible(self.card_row("Storage", name="pool0"))
+ else:
+ dropdown_toggle = self.dropdown_toggle(self.card_header("Storage"))
+ raid_action = self.dropdown_action(self.card_header("Storage"), "Create MDRAID device")
+ stratis_action = self.dropdown_action(self.card_header("Storage"), "Create Stratis pool")
+ b.click(dropdown_toggle)
+ b.wait_visible(raid_action)
+ b.wait_not_present(stratis_action)
+
+
+@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
+@testlib.skipImage("Stratis too old", "rhel-8-*", "centos-8-*")
+class TestStorageStratisNBDE(packagelib.PackageCase, storagelib.StorageCase):
+ provision = {
+ "0": {"address": "10.111.112.1/20", "memory_mb": 2048},
+ "tang": {"address": "10.111.112.5/20"}
+ }
+
+ def setUp(self):
+ super().setUp()
+
+ if self.image == "arch":
+ # Arch Linux does not enable systemd units by default
+ self.machine.execute("systemctl enable --now stratisd")
+ self.addCleanup(self.machine.execute, "systemctl disable --now stratisd")
+
+ self.stop_type_opt = get_stratis_stop_type_opt(self.machine.execute)
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ tang_m = self.machines["tang"]
+ tang_m.execute("systemctl start tangd.socket")
+ tang_m.execute("firewall-cmd --add-port 80/tcp")
+
+ self.login_and_go("/storage")
+
+ dev_1 = "/dev/sda"
+ m.add_disk("4G", serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+
+ dev_2 = "/dev/sdb"
+ m.add_disk("5G", serial="DISK2")
+ b.wait_visible(self.card_row("Storage", name=dev_2))
+
+ # Create an encrypted pool with both a passphrase and a keyserver
+ self.dialog_open_with_retry(trigger=lambda: self.click_devices_dropdown("Create Stratis pool"),
+ expect=lambda: (self.dialog_is_present('disks', dev_1) and
+ self.dialog_check({"name": "pool0"})))
+ self.dialog_set_val("encrypt_pass.on", val=True)
+ self.dialog_set_val("passphrase", "foodeeboodeebar")
+ self.dialog_set_val("passphrase2", "foodeeboodeebar")
+ self.dialog_set_val("encrypt_tang.on", val=True)
+ self.dialog_set_val("tang_url", "10.111.112.5")
+ self.dialog_set_val("disks", {dev_1: True})
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ self.dialog_wait_close()
+
+ self.click_card_row("Storage", name="pool0")
+ b.wait_visible(self.card_desc("Encrypted Stratis pool", "Passphrase"))
+ b.wait_in_text(self.card_desc("Encrypted Stratis pool", "Keyserver"), "10.111.112.5")
+
+ b.assert_pixels(self.card("Encrypted Stratis pool"), "header",
+ ignore=['.pf-v5-c-description-list__group:contains(UUID)'])
+
+ # Remove passphrase
+ b.click(self.card_desc("Encrypted Stratis pool", "Passphrase") + " button:contains(Remove)")
+ self.confirm()
+ b.wait_visible(self.card_desc("Encrypted Stratis pool", "Passphrase") + " button:contains(Add passphrase)")
+ b.wait_visible(self.card_desc("Encrypted Stratis pool", "Keyserver") + " button:contains(Remove):disabled")
+
+ # Stop the pool and start it again. This should not ask
+ # for the passphrase (since there isn't any)
+ m.execute(f"stratis pool stop {self.stop_type_opt} pool0")
+ b.wait_visible(self.card("Stratis pool"))
+ tang_m.execute("systemctl stop tangd.socket")
+ b.click(self.card_button("Stratis pool", "Start"))
+ self.dialog_wait_open()
+ # stratis' error message for unreachable tang server is very poor:
+ # https://bugzilla.redhat.com/show_bug.cgi?id=2246920 Version < 3.6.0 said
+ # "Error communicating with server 10.111.112.5", check this again after fixing
+ b.wait_in_text("#dialog", "Error")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ tang_m.execute("systemctl start tangd.socket")
+ b.click(self.card_button("Stratis pool", "Start"))
+ b.wait_visible(self.card("Encrypted Stratis pool"))
+
+ # Put passphrase back and do the stopping starting again, but
+ # without tangd running. This should try clevis but then fall
+ # back to asking for a passphrase.
+
+ b.click(self.card_desc("Encrypted Stratis pool", "Passphrase") + " button:contains(Add passphrase)")
+ self.dialog({'passphrase': "foodeeboodeebar",
+ 'passphrase2': "foodeeboodeebar"})
+ b.wait_visible(self.card_desc("Encrypted Stratis pool", "Passphrase") + " button:contains(Remove):not(:disabled)")
+ m.execute(f"stratis pool stop {self.stop_type_opt} pool0")
+ tang_m.execute("systemctl stop tangd.socket")
+ b.click(self.card_button("Stratis pool", "Start"))
+ self.dialog_wait_open()
+ self.dialog_set_val("passphrase", "foobar")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Finally start tang
+ tang_m.execute("systemctl start tangd.socket")
+ b.click(self.card_button("Stratis pool", "Start"))
+ b.wait_visible(self.card("Encrypted Stratis pool"))
+
+ # Add a blockdevice. This requires the passphrase.
+
+ b.click(self.card_button("Encrypted Stratis pool", "Add block device"))
+ self.dialog({'disks': {dev_2: True}, 'passphrase': "foodeeboodeebar"})
+
+ # Remove the keyserver and add it back
+
+ b.click(self.card_desc("Encrypted Stratis pool", "Keyserver") + " button:contains(Remove)")
+ self.confirm()
+
+ b.click(self.card_desc("Encrypted Stratis pool", "Keyserver") + " button:contains(Add keyserver)")
+ self.dialog_wait_open()
+ self.dialog_set_val("tang_url", "10.111.112.5")
+ self.dialog_set_val("passphrase", "foodeeboodeebar")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ self.dialog_wait_close()
+ b.wait_in_text(self.card_desc("Encrypted Stratis pool", "Keyserver"), "10.111.112.5")
+
+ # Remove the keyserver and add it back a second time, but try
+ # first with the wrong passphrase already in the keyring
+
+ b.click(self.card_desc("Encrypted Stratis pool", "Keyserver") + " button:contains(Remove)")
+ self.confirm()
+
+ create_pool_key(m, "pool0", "foobar")
+ b.click(self.card_desc("Encrypted Stratis pool", "Keyserver") + " button:contains(Add keyserver)")
+ self.dialog_wait_open()
+ self.dialog_set_val("tang_url", "10.111.112.5")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ b.wait_in_text('#dialog', "Command failed")
+ m.execute("stratis key unset pool0")
+ create_pool_key(m, "pool0", "foodeeboodeebar")
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ self.dialog_wait_close()
+ b.wait_in_text(self.card_desc("Encrypted Stratis pool", "Keyserver"), "10.111.112.5")
+ m.execute("stratis key unset pool0")
+
+ # Create a mounted filesystem and reboot.
+
+ b.click(self.card_button("Stratis filesystems", "Create new filesystem"))
+ self.dialog({'name': 'fsys1',
+ 'mount_point': '/run/fsys1'})
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1")
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys1")
+ m.reboot()
+ m.start_cockpit()
+ b.relogin()
+ b.enter_page("/storage")
+ b.wait_visible(self.card("Stratis pool"))
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1") # should be started after boot
+ b.wait_text(self.card_row_col("Stratis filesystems", 1, 3), "/run/fsys1") # should be mounted after boot
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-swap b/test/verify/check-storage-swap
new file mode 100755
index 0000000..1e9374a
--- /dev/null
+++ b/test/verify/check-storage-swap
@@ -0,0 +1,120 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageswap(storagelib.StorageCase):
+
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ disk = self.add_ram_disk()
+ self.addCleanup(m.execute, f"swapoff {disk} || true; swapoff {disk}1 || true")
+
+ self.login_and_go("/storage")
+
+ # Create a swap partition on GPT
+ self.click_card_row("Storage", name=disk)
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.confirm()
+ b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space")
+ self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition")
+ self.dialog({"type": "swap"})
+ b.wait_text(self.card_row_col("GPT partitions", 1, 2), "Swap")
+
+ # It should have been started and have a fstab entry
+ self.click_card_row("GPT partitions", 1)
+ b.wait_text(self.card_desc("Swap", "Used"), "0")
+ self.assertIn("defaults", m.execute(f"findmnt --fstab -n -o OPTIONS {disk}1"))
+
+ # Stopping should set it to noauto
+ b.click(self.card_button("Swap", "Stop"))
+ b.wait_text(self.card_desc("Swap", "Used"), "-")
+ self.assertIn("noauto", m.execute(f"findmnt --fstab -n -o OPTIONS {disk}1"))
+
+ # Start it again to test teardown below
+ b.click(self.card_button("Swap", "Start"))
+ b.wait_text(self.card_desc("Swap", "Used"), "0")
+ self.assertIn("defaults", m.execute(f"findmnt --fstab -n -o OPTIONS {disk}1"))
+
+ # It should have the right partition type
+ b.wait_visible(self.card("Swap"))
+ b.wait_text(self.card_desc("Partition", "Type"), "Linux swap space")
+
+ # Set it to something else
+ b.click(self.card_desc_action("Partition", "Type"))
+ self.dialog({"type": "0fc63daf-8483-4772-8e79-3d69d8477de4"})
+ b.wait_text(self.card_desc("Partition", "Type"), "Linux filesystem data")
+
+ # Correct it by reformatting
+ self.click_card_dropdown("Swap", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "swap")
+ b.wait_in_text("#dialog .modal-footer-teardown", f"{disk}1")
+ b.wait_in_text("#dialog .modal-footer-teardown", "stop, format")
+ self.dialog_apply_secondary()
+ self.dialog_wait_close()
+ b.wait_text(self.card_desc("Partition", "Type"), "Linux swap space")
+
+ # Delete the partition, the fstab entry should disappear
+ self.click_card_dropdown("Partition", "Delete")
+ self.confirm()
+ b.wait_visible(self.card("Solid State Drive"))
+ m.execute(f"! findmnt --fstab -n -o OPTIONS {disk}1")
+
+ # Format as swap on the command line, starting it should add
+ # fstab entry
+ m.execute(f"mkswap -f {disk}")
+ b.click(self.card_button("Swap", "Start"))
+ b.wait_text(self.card_desc("Swap", "Used"), "0")
+ testlib.wait(lambda: "defaults" in m.execute(f"findmnt --fstab -n -o OPTIONS {disk}"))
+
+ def testEncrypted(self):
+ b = self.browser
+ m = self.machine
+
+ disk = self.add_ram_disk()
+ self.addCleanup(m.execute, f"swapoff $(lsblk -plno NAME {disk} | tail -1) || true")
+
+ self.login_and_go("/storage")
+
+ # Create a encrypted swap partition directly on a disk
+ self.click_dropdown(self.card_row("Storage", name=disk), "Format")
+ with b.wait_timeout(60):
+ self.dialog({
+ "type": "swap",
+ "crypto": "luks2",
+ "passphrase": "foobar",
+ "passphrase2": "foobar",
+ })
+ b.wait_in_text(self.card_row("Storage", name=disk), "Swap (encrypted)")
+
+ # It should have been started and have a fstab entry
+ self.click_card_row("Storage", name=disk)
+ b.wait_text(self.card_desc("Swap", "Used"), "0")
+ dev = b.text(self.card_desc("Encryption", "Cleartext device"))
+ testlib.wait(lambda: "defaults" in m.execute(f"findmnt --fstab -n -o OPTIONS {dev}"))
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-unrecognized b/test/verify/check-storage-unrecognized
new file mode 100755
index 0000000..f14b17b
--- /dev/null
+++ b/test/verify/check-storage-unrecognized
@@ -0,0 +1,61 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2023 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageUnrecognized(storagelib.StorageCase):
+ def test(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk()
+
+ # Initially, new disks should have zeroes in their superblock
+ # and be recognized as "unformatted" by Cockpit.
+
+ self.click_card_row("Storage", name=disk)
+ b.wait_visible(self.card("Unformatted data"))
+
+ # This is the superblock of a legacy VDO device. Cockpit does
+ # not recognize it.
+
+ data = """
+ZG12ZG8wMDEFAAAABAAAAAAAAABdAAAAAAAAAJQJAgCGsH0mrQgGAC4WnB4G50Fzu20jY6J1rfwA
+AAAAAQAAAAAAAAABAAAA2FwKAAAAAAAA////AAAAAAA7tw9zAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+"""
+ m.execute(f"base64 -d >{disk}", input=data)
+
+ b.wait_text(self.card_desc("Unrecognized data", "Usage"), "other")
+ b.wait_text(self.card_desc("Unrecognized data", "Type"), "vdo")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-unused b/test/verify/check-storage-unused
new file mode 100755
index 0000000..0e370a1
--- /dev/null
+++ b/test/verify/check-storage-unused
@@ -0,0 +1,83 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageUnused(storagelib.StorageCase):
+
+ def testUnused(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # The following block devices should not be offered for creating a
+ # raid
+ #
+ # - filesystems
+ # - extended partition containers
+ # - partition tables
+ #
+ # So we partition a disk with two logical partitions, one of which
+ # has a filesystem on it.
+
+ disk1 = self.add_ram_disk()
+ disk2 = self.add_loopback_disk()
+ b.wait_visible(self.card_row("Storage", name=disk1))
+ b.wait_visible(self.card_row("Storage", name=disk2))
+ script = """mktable msdos \
+mkpart extended 1 50 \
+mkpart logical ext2 2 24 \
+mkpart logical ext2 24 48"""
+ m.execute(f"parted -s {disk1} {script}")
+ m.execute("udevadm settle")
+ m.execute(f"mke2fs -q -L TEST {disk1}5")
+
+ b.inject_js("""
+ ph_texts = function (sel) {
+ return ph_select(sel).map(function(e) { return e.textContent });
+ }""")
+
+ def check_free_block_devices():
+ blocks = b.eval_js("ph_texts('#dialog [data-field=\"disks\"] .select-space-details')")
+ print("blocks", blocks)
+ # On Ubuntu we see /dev/ram devices as well
+ # On Fedora also /dev/zram0 - see https://github.com/cockpit-project/cockpit/issues/14516
+ allowed = ["/dev/sda", "/dev/loop", "/dev/vda", "/dev/ram", "/dev/zram"]
+ for block in blocks:
+ for allow in allowed:
+ if allow in block:
+ break
+ else:
+ return False
+
+ # Require these two to be present
+ return f"{disk1}6" in blocks and disk2 in blocks
+
+ self.dialog_with_retry(trigger=lambda: self.click_dropdown(self.card_header("Storage"),
+ "Create MDRAID device"),
+ expect=check_free_block_devices,
+ values=None)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-used b/test/verify/check-storage-used
new file mode 100755
index 0000000..28f1b1e
--- /dev/null
+++ b/test/verify/check-storage-used
@@ -0,0 +1,195 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import storagelib
+import testlib
+
+
+@testlib.nondestructive
+class TestStorageUsed(storagelib.StorageCase):
+
+ def testUsed(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk()
+ b.wait_visible(self.card_row("Storage", name=disk))
+ m.execute(f"parted -s {disk} mktable msdos")
+ m.execute(f"parted -s {disk} mkpart primary ext2 1M 25")
+ m.execute("udevadm settle")
+ m.execute(f"echo einszweidrei | cryptsetup luksFormat --pbkdf-memory 32768 {disk}1")
+ m.execute(f"echo einszweidrei | cryptsetup luksOpen {disk}1 dm-test")
+ m.execute("udevadm settle")
+ m.execute("mke2fs -q -L TEST /dev/mapper/dm-test")
+ m.execute("mount /dev/mapper/dm-test /mnt")
+
+ # Keep the mount point busy. The extra "true" is here to
+ # prevent bash from applying tail call optimization to the
+ # "sleep" invocation.
+ sleep_pid = m.spawn("cd /mnt; sleep infinity; true", "sleep")
+ self.write_file("/etc/systemd/system/keep-mnt-busy.service",
+ """
+[Unit]
+Description=Test Service
+
+[Service]
+WorkingDirectory=/mnt
+ExecStart=/usr/bin/sleep infinity
+""")
+ m.execute("systemctl start keep-mnt-busy")
+
+ # Now all of /dev/mapper/dm-test, /dev/sda1, and /dev/sda
+ # should be 'in use' but Cockpit can clean them all up anyway.
+
+ self.click_card_row("Storage", name=disk)
+ b.wait_visible(self.card("Solid State Drive"))
+
+ self.click_card_row("DOS partitions", 1)
+ self.click_card_dropdown("ext2 filesystem", "Format")
+ self.dialog_wait_open()
+ self.dialog_set_val("type", "ext4")
+ b.click("#dialog button:contains(Currently in use)")
+ b.wait_in_text(".pf-v5-c-popover", str(sleep_pid))
+ b.wait_in_text(".pf-v5-c-popover", "keep-mnt-busy")
+ b.assert_pixels(".pf-v5-c-popover", "popover",
+ mock={".pf-v5-c-popover__body ul:nth-of-type(2) li": "process (user: root, pid: 1234)"},
+ scroll_into_view="#dialog button:contains(Currently in use)")
+ b.click(".pf-v5-c-popover button")
+ b.assert_pixels('#dialog', "format", wait_after_layout_change=True)
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ self.click_card_dropdown("Partition", "Delete")
+ self.dialog_wait_open()
+ b.wait_visible("#dialog button:contains(Currently in use)")
+ b.assert_pixels('#dialog', "delete")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Now go ahead and let the automatic teardown take care of the mount
+
+ b.click(self.card_parent_link())
+
+ # Sometimes /dev/sda1 is still held open by something
+ # immediately after locking it. This prevents the
+ # kernel from reading the new partition table. Let's
+ # just retry.
+
+ def first_setup():
+ self.dialog_set_val("type", "empty")
+ b.wait_visible("#dialog tbody:first-of-type button:contains(Currently in use)")
+ b.assert_pixels('#dialog', "format-disk")
+
+ def retry_setup():
+ self.dialog_set_val("type", "empty")
+
+ self.dialog_with_error_retry(trigger=lambda: self.click_card_dropdown("Solid State Drive",
+ "Create partition table"),
+ first_setup=first_setup,
+ retry_setup=retry_setup,
+ errors=["Timed out waiting for object"])
+
+ m.execute("! systemctl --quiet is-active keep-mnt-busy")
+
+ b.wait_visible(self.card("Unformatted data"))
+
+ def testUsedAsPV(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ dev_1 = self.add_ram_disk()
+ dev_2 = self.add_loopback_disk()
+ b.wait_visible(self.card_row("Storage", name=dev_1))
+ b.wait_visible(self.card_row("Storage", name=dev_2))
+
+ # Create a volume group out of two disks
+ m.execute(f"vgcreate TEST1 {dev_1} {dev_2}")
+ self.addCleanupVG("TEST1")
+ b.wait_visible(self.card_row("Storage", name="TEST1"))
+
+ # Formatting dev_1 should cleanly remove it from the volume
+ # group.
+
+ self.click_card_row("Storage", name=dev_1)
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ b.wait_in_text('#dialog', "remove from LVM2, initialize")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ self.assertEqual(int(m.execute("vgs TEST1 -o pv_count --noheadings")), 1)
+
+ # Formatting dev_2 should now cleanly remove the whole volume
+ # group.
+
+ b.go("#/")
+ self.click_card_row("Storage", name=dev_2)
+ self.click_card_dropdown("Block device", "Create partition table")
+ b.wait_in_text('#dialog', "remove from LVM2, initialize")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ self.assertEqual(m.execute("vgs TEST1 || echo GONE").strip(), "GONE")
+
+ def testTeardownRetry(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ disk = self.add_ram_disk()
+ b.wait_visible(self.card_row("Storage", name=disk))
+ m.execute(f"mke2fs -q -L TEST {disk}")
+ m.execute(f"mount {disk} /mnt")
+
+ self.click_card_row("Storage", name=disk)
+ b.wait_in_text(self.card("ext2 filesystem"), "The filesystem is currently mounted on /mnt")
+
+ # Start formatting, and while the dialog is open, make the
+ # filesystem unmountable.
+ #
+ # We have two processes that keep the filesystem busy: one
+ # that is supposed to be picked up by the dialog, and one that
+ # is not. The first is only used to figure out when the dialog
+ # is done initializing.
+
+ m.spawn("cd /mnt; sleep infinity; true", "sleep")
+
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.dialog_wait_open()
+ b.wait_visible("#dialog tbody:first-of-type button:contains(Currently in use)")
+ self.dialog_wait_apply_enabled()
+ m.spawn("cd /mnt; sleep infinity; true", "sleep")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "umount: /mnt: target is busy")
+ self.dialog_wait_apply_disabled()
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ self.click_card_dropdown("Solid State Drive", "Create partition table")
+ self.dialog_wait_open()
+ b.wait_visible("#dialog button:contains(Currently in use)")
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-storage-vdo b/test/verify/check-storage-vdo
new file mode 100755
index 0000000..1176172
--- /dev/null
+++ b/test/verify/check-storage-vdo
@@ -0,0 +1,436 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import packagelib
+import storagelib
+import testlib
+
+SIZE_10G = "10000000000"
+
+
+class TestStorageVDO(storagelib.StorageCase):
+
+ provision = {"0": {"memory_mb": 1800}}
+
+ def testVdo(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Make a volume group in which to create the VDO LV
+ dev = "/dev/" + m.add_disk(SIZE_10G, serial="DISK1")["dev"]
+ b.wait_visible(self.card_row("Storage", name=dev))
+ m.execute("vgcreate vdo_vgroup /dev/sda")
+
+ self.click_card_row("Storage", name="vdo_vgroup")
+ b.wait_in_text(self.card("LVM2 logical volumes"), "No logical volumes")
+
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog_wait_open()
+ b._wait_present("[data-field='purpose'] select option[value='block']")
+
+ # vdo only exists on RHEL
+ if not m.image.startswith("rhel") and not m.image.startswith("centos"):
+ b.wait_not_present("[data-field='purpose'] select option[value='vdo']")
+ return
+
+ # create VDO LV with default options and default virtual size
+ self.dialog_set_val("name", "vdo0")
+ self.dialog_set_val("purpose", "vdo")
+ self.dialog_set_val("vdo_psize", 6000)
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ # pool name gets auto-generated
+ pool_name = "vpool0"
+
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "vdo0")
+ # the pool does not appear as a top-level volume
+ b.wait_not_present(self.card_row("LVM2 logical volumes", name=pool_name))
+ self.click_card_row("LVM2 logical volumes", 1)
+ # Volume card
+ b.wait_text(self.card_desc("LVM2 logical volume", "Name"), "vdo0")
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), "10.0 GB")
+ # VDO pool card
+ b.wait_text(self.card_desc("LVM2 VDO pool", "Name"), pool_name)
+ b.wait_in_text(self.card_desc("LVM2 VDO pool", "Size"), "6.00 GB")
+ # initial physical usage is ~ 4 GB, overhead for the deduplication index
+ b.wait_text(self.card_desc("LVM2 VDO pool", "Data used"), "3.86 GB (64%)")
+ b.wait_text(self.card_desc("LVM2 VDO pool", "Metadata used"), "0%")
+ b.wait_visible(self.card("LVM2 VDO pool") + " input[aria-label='Use compression']:checked")
+ b.wait_visible(self.card("LVM2 VDO pool") + " input[aria-label='Use deduplication']:checked")
+
+ # create a filesystem
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "xfs",
+ "name": "vdofs",
+ "mount_point": "/run/data"})
+ b.wait_visible(self.card("xfs filesystem"))
+
+ # compressible data should affect logical usage
+ m.execute("dd if=/dev/zero of=/run/data/empty bs=1M count=1000")
+ b.wait_in_text(self.card_desc("xfs filesystem", "Usage"), "1.2 / ")
+ # but not physical usage
+ b.wait_text(self.card_desc("LVM2 VDO pool", "Data used"), "3.86 GB (64%)")
+
+ # incompressible data
+ m.execute("dd if=/dev/urandom of=/run/data/gibberish bs=1M count=1000")
+ b.wait_in_text(self.card_desc("xfs filesystem", "Usage"), "2.2 / ")
+ # equal amount of physical space (not completely predictable due to random data)
+ b.wait_in_text(self.card_desc("LVM2 VDO pool", "Data used"), "4.")
+
+ def wait_prop(device, prop, value):
+ m.execute(f"until lvdisplay --noheadings -Co {prop} /dev/vdo_vgroup/{device} | grep -q '{value}'; do sleep 0.1; done")
+
+ # change compression/deduplication
+ b.click("input[aria-label='Use compression']")
+ b.wait_visible("input[aria-label='Use compression']:not(checked):not([disabled])")
+ wait_prop(pool_name, "vdo_compression_state", "offline")
+ b.click("input[aria-label='Use compression']")
+ b.wait_visible("input[aria-label='Use compression']:checked:not([disabled])")
+ wait_prop(pool_name, "vdo_compression_state", "online")
+
+ b.click("input[aria-label='Use deduplication']")
+ b.wait_visible("input[aria-label='Use deduplication']:not(checked):not([disabled])")
+ wait_prop(pool_name, "vdo_index_state", r"offline\|closed")
+ b.click("input[aria-label='Use deduplication']")
+ b.wait_visible("input[aria-label='Use deduplication']:checked:not([disabled])")
+ wait_prop(pool_name, "vdo_index_state", "online")
+
+ # grow volume
+ b.click(self.card_button("LVM2 logical volume", "Grow"))
+ self.dialog({"size": 12000})
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), "12.0 GB")
+ wait_prop("vdo0", "lv_size", "11.18g")
+
+ # grow pool
+ b.click(self.card_button("LVM2 VDO pool", "Grow"))
+ self.dialog({"size": 8000})
+ b.wait_in_text(self.card_desc("LVM2 VDO pool", "Size"), "8.15 GB")
+ wait_prop(pool_name, "lv_size", "7.59g")
+
+ # deleting the vdo0 volume deletes the pool as well
+ self.click_card_dropdown("LVM2 logical volume", "Delete")
+ self.confirm()
+ b.wait_in_text(self.card("LVM2 logical volumes"), "No logical volumes")
+ self.assertEqual(m.execute("lvs --noheadings").strip(), "")
+
+ # create VDO LV with customized options
+ b.click(self.card_button("LVM2 logical volumes", "Create new logical volume"))
+ self.dialog_wait_open()
+ b._wait_present("[data-field='purpose'] select option[value='block']")
+ self.dialog_set_val("name", "vdo0")
+ self.dialog_set_val("purpose", "vdo")
+ self.dialog_set_val("vdo_psize", 6000)
+ # grossly overcommitted
+ self.dialog_set_val("vdo_lsize", 20000)
+ self.dialog_set_val("vdo_options.compression", val=False)
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "vdo0")
+
+ self.click_card_row("LVM2 logical volumes", 1)
+ # Volume card
+ b.wait_text(self.card_desc("LVM2 logical volume", "Name"), "vdo0")
+ b.wait_in_text(self.card_desc("LVM2 logical volume", "Size"), "20.0 GB")
+ # VDO Pool tab
+ b.wait_in_text(self.card_desc("LVM2 VDO pool", "Size"), "6.00 GB")
+ b.wait_visible("input[aria-label='Use compression']:not(:checked)")
+ b.wait_visible("input[aria-label='Use deduplication']:checked")
+ wait_prop(pool_name, "vdo_compression_state", "offline")
+ wait_prop(pool_name, "vdo_index_state", "online")
+
+ # delete again
+ self.click_card_dropdown("LVM2 logical volume", "Delete")
+ self.confirm()
+ b.wait_in_text(self.card("LVM2 logical volumes"), "No logical volumes")
+ self.assertEqual(m.execute("lvs --noheadings").strip(), "")
+
+ # react to CLI
+ m.execute("lvcreate --type vdo --size 6g --virtualsize 10g --name vdo1 --yes vdo_vgroup")
+ b.wait_text(self.card_row_col("LVM2 logical volumes", 1, 1), "vdo1")
+ m.execute("lvremove --yes /dev/vdo_vgroup/vdo1")
+ b.wait_in_text(self.card("LVM2 logical volumes"), "No logical volumes")
+
+
+@testlib.onlyImage("legacy VDO API only supported on RHEL 8", "rhel-8*", "centos-8*")
+class TestStorageLegacyVDO(storagelib.StorageCase):
+
+ def setUp(self):
+ super().setUp()
+ # packagekit eats too much CPU/memory in the background
+ self.machine.execute("systemctl mask packagekit; systemctl stop packagekit")
+
+ def testVdo(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ # Make a logical volume for use as the backing device.
+ m.add_disk(SIZE_10G, serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name="/dev/sda"))
+ m.execute("vgcreate vdo_vgroup /dev/sda; lvcreate -n lvol -L 5G vdo_vgroup")
+ # Create VDO; this is not supported any more, thus no UI for it
+ m.execute("vdo create --device /dev/vdo_vgroup/lvol --name vdo0 --vdoLogicalSize 5G", timeout=300)
+
+ self.click_card_row("Storage", name="lvol")
+
+ def detail(index):
+ card = self.card("VDO device vdo0")
+ return f'{card} .pf-v5-c-description-list__group:nth-of-type({index}) > dd'
+
+ b.wait_text(detail(1), "/dev/mapper/vdo0")
+ b.wait_in_text(detail(2), "used of 5.37 GB")
+ b.wait_in_text(detail(3), "used of 5.37 GB")
+ b.wait_text(detail(4), "268 MB")
+ b.wait_visible(detail(5) + " input:checked")
+ b.wait_visible(detail(6) + " input:checked")
+
+ # Make a filesystem on it
+
+ self.click_card_dropdown("Unformatted data", "Format")
+ self.dialog({"type": "xfs",
+ "name": "FILESYSTEM",
+ "mount_point": "/run/data"})
+ b.wait_in_text(self.card_desc("xfs filesystem", "Mount point"), "after network")
+ b.wait_in_text(self.card_desc("xfs filesystem", "Mount point"), "x-systemd.device-timeout=0")
+ b.wait_in_text(self.card_desc("xfs filesystem", "Mount point"), "x-systemd.requires=vdo.service")
+ b.wait_in_text(self.card_desc("xfs filesystem", "Usage"), "/ 5.4 GB")
+
+ # Grow physical
+
+ m.execute("lvresize vdo_vgroup/lvol -L 9G")
+ b.wait_in_text(".pf-v5-c-alert__description", 'Only 5.37 GB of 9')
+ b.click("button:contains('Grow to take all space')")
+ b.wait_not_present(".pf-v5-c-alert")
+ b.wait_in_text(detail(2), "used of 9.66 GB")
+
+ # Grow logical
+
+ b.click(detail(3) + " button:contains(Grow)")
+ self.dialog({"lsize": 10000})
+ b.wait_in_text(detail(3), "used of 10.0 GB")
+ b.wait_in_text(self.card_desc("xfs filesystem", "Usage"), "/ 10 GB")
+
+ # Stop
+
+ b.wait_visible(self.card("xfs filesystem"))
+ b.click(self.card_button("VDO device vdo0", "Stop"))
+ self.dialog_wait_open()
+ b.wait_in_text("#dialog", "unmount, stop")
+ self.dialog_apply()
+ self.dialog_wait_close()
+ b.wait_not_present(self.card("xfs filesystem"))
+
+ # Delete
+
+ self.click_card_dropdown("VDO device vdo0", "Delete")
+ self.dialog_wait_open()
+ self.dialog_apply_with_retry(expected_errors=["Device or resource busy"])
+ b.wait_not_present(self.card("VDO device vdo0"))
+ b.wait_visible(self.card("Unformatted data"))
+ b.wait_visible(self.card("LVM2 logical volume"))
+
+ def testBrokenVdo(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ m.add_disk(SIZE_10G, serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name="/dev/sda"))
+
+ # Install a valid configuration file that describes a broken VDO
+ m.write("/etc/vdoconf.yml", """
+config: !Configuration
+ vdos:
+ vdo0: !VDOService
+ _operationState: beginCreate
+ ackThreads: 1
+ activated: enabled
+ bioRotationInterval: 64
+ bioThreads: 4
+ blockMapCacheSize: 128M
+ blockMapPeriod: 16380
+ compression: enabled
+ cpuThreads: 2
+ deduplication: enabled
+ device: /dev/sda
+ hashZoneThreads: 1
+ indexCfreq: 0
+ indexMemory: 0.25
+ indexSparse: disabled
+ indexThreads: 0
+ logicalBlockSize: 4096
+ logicalSize: 10G
+ logicalThreads: 1
+ name: vdo0
+ physicalSize: 10G
+ physicalThreads: 1
+ readCache: disabled
+ readCacheSize: 0M
+ slabSize: 2G
+ writePolicy: sync
+ version: 538380551
+""")
+
+ self.click_card_row("Storage", name="/dev/sda")
+ b.click(".pf-m-danger button:contains('Remove device')")
+ b.wait_visible(self.card("Unformatted data"))
+
+ def testBrokenVdoConfig(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/storage")
+
+ m.add_disk(SIZE_10G, serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name="/dev/sda"))
+
+ # Install a valid configuration file
+ m.write("/etc/vdoconf.yml", """
+config: !Configuration
+ vdos:
+ vdo0: !VDOService
+ _operationState: finished
+ ackThreads: 1
+ activated: enabled
+ bioRotationInterval: 64
+ bioThreads: 4
+ blockMapCacheSize: 128M
+ blockMapPeriod: 16380
+ compression: enabled
+ cpuThreads: 2
+ deduplication: enabled
+ device: /dev/sda
+ hashZoneThreads: 1
+ indexCfreq: 0
+ indexMemory: 0.25
+ indexSparse: disabled
+ indexThreads: 0
+ logicalBlockSize: 4096
+ logicalSize: 10G
+ logicalThreads: 1
+ name: vdo0
+ physicalSize: 10G
+ physicalThreads: 1
+ readCache: disabled
+ readCacheSize: 0M
+ slabSize: 2G
+ writePolicy: sync
+ version: 538380551
+""")
+
+ b.wait_in_text(self.card_row("Storage", name="/dev/sda"), "VDO device")
+
+ # Install a broken configuration file
+ m.write("/etc/vdoconf.yml", """
+config: !Configuration
+ vdos:
+ vdo0: !VDOService
+ blah: 12
+""")
+
+ b.wait_in_text(self.card_row("Storage", name="/dev/sda"), "Unformatted data")
+
+ # Install a valid configuration file again
+ m.write("/etc/vdoconf.yml", """
+config: !Configuration
+ vdos:
+ vdo1: !VDOService
+ _operationState: finished
+ ackThreads: 1
+ activated: enabled
+ bioRotationInterval: 64
+ bioThreads: 4
+ blockMapCacheSize: 128M
+ blockMapPeriod: 16380
+ compression: enabled
+ cpuThreads: 2
+ deduplication: enabled
+ device: /dev/sda
+ hashZoneThreads: 1
+ indexCfreq: 0
+ indexMemory: 0.25
+ indexSparse: disabled
+ indexThreads: 0
+ logicalBlockSize: 4096
+ logicalSize: 10G
+ logicalThreads: 1
+ name: vdo1
+ physicalSize: 10G
+ physicalThreads: 1
+ readCache: disabled
+ readCacheSize: 0M
+ slabSize: 2G
+ writePolicy: sync
+ version: 538380551
+""")
+
+ b.wait_in_text(self.card_row("Storage", name="/dev/sda"), "VDO device")
+
+
+@testlib.onlyImage("VDO API only supported on RHEL", "rhel-*", "centos-*")
+class TestStoragePackagesVDO(packagelib.PackageCase, storagelib.StorageHelpers):
+
+ provision = {"0": {"memory_mb": 1500}}
+
+ def testVdoMissingPackages(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute("pkcon remove -y vdo")
+ m.execute("pkcon refresh")
+
+ self.login_and_go("/storage")
+ m.add_disk(SIZE_10G, serial="DISK1")
+ b.wait_visible(self.card_row("Storage", name="/dev/sda"))
+ m.execute("vgcreate vdo_vgroup /dev/sda")
+
+ self.click_card_row("Storage", name="vdo_vgroup")
+ b.click("button:contains(Create new logical volume)")
+ self.dialog_wait_open()
+ b._wait_present("[data-field='purpose'] select option[value='block']")
+ # no package installation helper text
+ self.assertFalse(b.is_present("#dialog .pf-v5-c-helper-text"))
+ self.dialog_set_val("purpose", "vdo")
+ # shows the package installation note
+ b.wait_in_text("#dialog .pf-v5-c-helper-text", "vdo package will be installed")
+
+ # vdo package does not exist
+ self.dialog_apply()
+ b.wait_in_text("#dialog .pf-v5-c-alert.pf-m-danger", "vdo is not available from any repository")
+
+ self.createPackage("vdo", "999", "1")
+ self.enableRepo()
+
+ self.dialog_apply()
+ # gets over package installation now, but it's a mock package
+ b.wait_in_text("#dialog .pf-v5-c-alert.pf-m-danger", "vdoformat")
+ b.wait_in_text("#dialog .pf-v5-c-alert.pf-m-danger", "No such file or directory")
+ # but it got past package installation
+ self.assertIn("999", m.execute("rpm -q vdo"))
+
+ self.dialog_cancel()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-superuser b/test/verify/check-superuser
new file mode 100755
index 0000000..aab9654
--- /dev/null
+++ b/test/verify/check-superuser
@@ -0,0 +1,758 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2020 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import time
+
+import testlib
+
+
+def allow_old_cockpit_ws_messages(test):
+ # noisy debug message from old cockpit-ws/polkit
+ test.allow_journal_messages("logged in user session",
+ "New connection to session from.*",
+ "pam_unix(polkit-1:session): session opened for user root by .*uid=.*",
+ "admin: Executing command .*COMMAND=.*cockpit-bridge --privileged.*")
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestSuperuser(testlib.MachineCase):
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ # Log in with all defaults
+ self.login_and_go()
+ b.check_superuser_indicator("Administrative access")
+
+ b.assert_pixels("#topnav", "topnav-privileged")
+
+ # Drop privileges
+ b.open_superuser_dialog()
+ b.click(".pf-v5-c-modal-box:contains('Switch to limited access') button:contains('Limit access')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to limited access')")
+ b.check_superuser_indicator("Limited access")
+
+ b.assert_pixels("#topnav", "topnav-unprivileged")
+
+ # Check they are still gone after logout
+ b.relogin()
+ b.leave_page()
+ b.check_superuser_indicator("Limited access")
+
+ # A reload should not lose privileges
+ b.become_superuser()
+ b.reload()
+ b.check_superuser_indicator("Administrative access")
+
+ # Drop privileges
+ b.drop_superuser()
+
+ # We want to be lectured again
+ self.restore_dir("/var/db/sudo/lectured")
+
+ m.execute("rm -rf /var/{db,lib}/sudo/lectured/admin")
+ # Sudo since 1.9.15 uses a UID not a username https://www.sudo.ws/releases/stable/#1.9.15
+ uid = m.execute("id -u admin").strip()
+ m.execute(f"rm -rf /var/{{db,lib}}/sudo/lectured/{uid}")
+
+ # Get the privileges back, this time in the mobile layout
+ b.set_layout("mobile")
+ b.open_superuser_dialog()
+ if "ubuntu" not in m.image and "debian" not in m.image:
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')",
+ "We trust you have received the usual lecture")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
+ b.focus(".pf-v5-c-modal-box button:contains('Cancel')")
+ b.assert_pixels(".pf-v5-c-modal-box:contains('Switch to administrative access')", "superuser-modal")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
+ b.check_superuser_indicator("Administrative access")
+ b.set_layout("desktop")
+
+ # Check we still have them after logout
+ b.relogin()
+ b.leave_page()
+ b.check_superuser_indicator("Administrative access")
+
+ # Test that closing instead of cancelling the dialog keeps the "switch
+ # to administrative button"
+ b.open_superuser_dialog()
+ b.click(".pf-v5-c-modal-box:contains('Switch to limited access') button:contains('Limit access')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to limited access')")
+ b.check_superuser_indicator("Limited access")
+
+ b.open_superuser_dialog()
+ b.click(".pf-v5-c-modal-box__close button")
+ b.check_superuser_indicator("Limited access")
+
+ def testSudoIOLogging(self):
+ b = self.browser
+ m = self.machine
+
+ self.write_file("/etc/sudoers.d/log", "Defaults log_output\n")
+
+ # with explicitly becoming superuser
+ self.login_and_go(superuser=False)
+ b.check_superuser_indicator("Limited access")
+ b.become_superuser()
+ self.assertIn("00\n", m.execute("ls /var/log/sudo-io"))
+ b.logout()
+
+ # with immediate superuser at login
+ b.login_and_go()
+ b.check_superuser_indicator("Administrative access")
+ b.logout()
+
+ def testNoPasswd(self):
+ b = self.browser
+
+ # Log in with limited access
+ self.login_and_go(superuser=False)
+ b.check_superuser_indicator("Limited access")
+
+ # Give us password-less sudo and use it
+ self.write_file("/etc/sudoers.d/admin", "admin ALL=(ALL) NOPASSWD:ALL")
+ b.become_superuser(passwordless=True)
+
+ def testTwoPasswds(self):
+ b = self.browser
+ m = self.machine
+
+ # Log in with limited access
+ self.login_and_go(superuser=False)
+ b.check_superuser_indicator("Limited access")
+
+ # Configure the sudo PAM stack to make two prompts
+ if "debian" in m.image or "ubuntu" in m.image:
+ self.write_file("/etc/pam.d/sudo", """
+auth required pam_unix.so
+auth required mock-pam-conv-mod.so
+@include common-account
+@include common-session-noninteractive
+""")
+ else:
+ self.write_file("/etc/pam.d/sudo", """
+auth required pam_unix.so
+auth required mock-pam-conv-mod.so
+account include system-auth
+password include system-auth
+session include system-auth
+""")
+
+ b.open_superuser_dialog()
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ # Let the dialog sit there for 45 seconds, to test that this doesn't trigger a D-Bus timeout.
+ time.sleep(45)
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "universe and everything")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "42")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
+ b.check_superuser_indicator("Administrative access")
+
+ def testWrongPasswd(self):
+ b = self.browser
+
+ # Log in with limited access
+ self.login_and_go(superuser=False)
+ b.check_superuser_indicator("Limited access")
+
+ b.open_superuser_dialog()
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "wrong")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Sorry, try again")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "wronger")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Sorry, try again")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "wrongest")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Problem becoming administrator')", "Sudo: 3 incorrect password attempts")
+ b.click(".pf-v5-c-modal-box:contains('Problem becoming administrator') button:contains('Close')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Problem becoming administrator')")
+ b.check_superuser_indicator("Limited access")
+
+ def testNotAdmin(self):
+ b = self.browser
+ m = self.machine
+
+ # Remove special treatment of the "admin" group on Ubuntu.
+ # Our main test user is unfortunately called "admin" and has
+ # "admin" as its primary group.
+ #
+ if "ubuntu" in m.image:
+ self.sed_file("/^%admin/d", "/etc/sudoers")
+
+ m.execute(f"gpasswd -d admin {m.get_admin_group()}")
+
+ self.login_and_go()
+ b.check_superuser_indicator("Limited access")
+
+ b.open_superuser_dialog()
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Problem becoming administrator')", "Admin is not in the sudoers file.")
+ b.click(".pf-v5-c-modal-box:contains('Problem becoming administrator') button:contains('Close')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Problem becoming administrator')")
+
+ # no stray/hanging sudo process
+ testlib.wait(lambda: "sudo" not in m.execute("loginctl --lines=0 user-status admin"), tries=5)
+
+ # cancelling auth dialog stops sudo
+ b.open_superuser_dialog()
+ b.wait_in_text(".pf-v5-c-modal-box", "Switch to administrative access")
+ b.wait_in_text(".pf-v5-c-modal-box", "Password for admin")
+ status = m.execute("loginctl --lines=0 user-status admin")
+ self.assertIn("sudo", status)
+ self.assertIn("cockpit-askpass", status)
+
+ b.click(".pf-v5-c-modal-box button:contains('Cancel')")
+ b.wait_not_present(".pf-v5-c-modal-box")
+
+ testlib.wait(lambda: "sudo" not in m.execute("loginctl --lines=0 user-status admin"), tries=5)
+
+ # logging out cleans up pending sudo auth; user should either go to "State: closing" or disappear completely
+ b.open_superuser_dialog()
+ b.wait_in_text(".pf-v5-c-modal-box", "Password for admin")
+ self.assertIn("cockpit-askpass", m.execute("loginctl --lines=0 user-status admin"))
+ b.logout()
+ testlib.wait(lambda: "sudo" not in m.execute("loginctl --lines=0 user-status admin || true"), tries=10)
+ self.assertNotIn("cockpit", m.execute("loginctl --lines=0 user-status admin || true"))
+
+ def testBrokenBridgeConfig(self):
+ b = self.browser
+ m = self.machine
+
+ self.write_file("/etc/cockpit/shell.override.json", """
+{
+ "bridges": [
+ {
+ "privileged": true,
+ "spawn": [
+ "sudo",
+ "-k",
+ "-A",
+ "cockpit-bridge",
+ "--privileged"
+ ]
+ }
+ ]
+}
+""")
+
+ # We don't want to be lectured in this test just to control
+ # the content of the dialog better.
+ m.execute("touch /var/{db,lib}/sudo/lectured/admin 2>/dev/null || true")
+
+ self.login_and_go(superuser=False)
+ b.check_superuser_indicator("Limited access")
+
+ b.open_superuser_dialog()
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Problem becoming administrator')", "Sudo: no askpass program specified, try setting SUDO_ASKPASS")
+ b.click(".pf-v5-c-modal-box:contains('Problem becoming administrator') button:contains('Close')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Problem becoming administrator')")
+ b.check_superuser_indicator("Limited access")
+
+ def testRemoveBridgeConfig(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/playground/pkgs", superuser=True)
+ b.leave_page()
+ b.check_superuser_indicator("Administrative access")
+ # superuser bridge is running
+ m.execute("pgrep -u root -a cockpit-bridge")
+
+ self.write_file("/etc/cockpit/shell.override.json", """
+{
+ "bridges": [ ]
+}
+""")
+
+ b.enter_page("/playground/pkgs")
+ b.click("#reload")
+ b.leave_page()
+ if self.is_pybridge():
+ b.check_superuser_indicator("")
+ else:
+ # C bridge gets that wrong: Its .Bridges property remains at [sudo, pkexec], showing a broken indicator
+ b.check_superuser_indicator("Limited access")
+ # superuser bridge goes away
+ m.execute("while pgrep -u root -a cockpit-bridge; do sleep 1; done", timeout=5)
+
+ def testSingleLabelBridgeConfig(self):
+ b = self.browser
+
+ # When there is a single labeled privileged bridge, Cockit will start it automatically.
+
+ self.write_file("/etc/cockpit/shell.override.json", """
+{
+ "bridges": [
+ {
+ "privileged": true,
+ "label": "Always fails",
+ "spawn": [
+ "/bin/bash", "-c", "echo >&2 'Hello from the bash method'; exit 1"
+ ]
+ }
+ ]
+}
+""")
+
+ self.login_and_go("/playground/pkgs", superuser=False)
+ b.leave_page()
+ b.check_superuser_indicator("Limited access")
+ b.open_superuser_dialog()
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Problem becoming administrator')", "Hello from the bash method")
+ b.click(".pf-v5-c-modal-box button:contains('Close')")
+ b.check_superuser_indicator("Limited access")
+
+ def testMultipleBridgeConfig(self):
+ b = self.browser
+
+ self.write_file("/etc/cockpit/shell.override.json", """
+{
+ "bridges": [
+ {
+ "privileged": true,
+ "label": "Sudo",
+ "environ": [
+ "SUDO_ASKPASS=${libexecdir}/cockpit-askpass"
+ ],
+ "spawn": [
+ "sudo",
+ "-k",
+ "-A",
+ "cockpit-bridge",
+ "--privileged"
+ ]
+ },
+ {
+ "privileged": true,
+ "label": "Polkit",
+ "spawn": [
+ "pkexec",
+ "--disable-internal-agent",
+ "cockpit-bridge",
+ "--privileged"
+ ]
+ }
+ ]
+}
+""")
+
+ self.login_and_go("/playground/pkgs", superuser=False)
+ b.leave_page()
+ b.check_superuser_indicator("Limited access")
+
+ # Get admin rights with Polkit method
+ b.open_superuser_dialog()
+ b.set_val(".pf-v5-c-modal-box:contains('Switch to administrative access') select", "Polkit")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access') select", "Polkit")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Please authenticate")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
+ b.check_superuser_indicator("Administrative access")
+
+ # Drop them
+ b.open_superuser_dialog()
+ b.click(".pf-v5-c-modal-box:contains('Switch to limited access') button:contains('Limit access')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to limited access')")
+ b.check_superuser_indicator("Limited access")
+
+ # Run the regular sudo method, which should work as always
+ b.open_superuser_dialog()
+ b.set_val(".pf-v5-c-modal-box:contains('Switch to administrative access') select", "Sudo")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access') select", "Sudo")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
+ b.check_superuser_indicator("Administrative access")
+
+ def testOverview(self):
+ b = self.browser
+
+ self.login_and_go("/system", superuser=False)
+ b.wait_visible(".pf-v5-c-alert:contains('Web console is running in limited access mode.')")
+ b.click(".pf-v5-c-alert:contains('Web console is running in limited access mode.') button:contains('Turn on')")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
+ b.wait_not_present(".pf-v5-c-alert:contains('Web console is running in limited access mode.')")
+
+
+@testlib.skipDistroPackage()
+class TestSuperuserOldShell(testlib.MachineCase):
+ provision = {
+ "machine1": {"address": "10.111.113.1/20", "memory_mb": 512},
+ "machine2": {"address": "10.111.113.2/20", "image": "centos-7", "memory_mb": 512},
+ }
+
+ def test(self):
+ b = self.browser
+ m = self.machine
+
+ m.start_cockpit()
+
+ # Use m1 to login into m2
+ b.open("/")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.click("#show-other-login-options")
+ b.wait_visible("#server-group")
+ b.set_val("#server-field", "10.111.113.2")
+ b.click('#login-button')
+ b.wait_in_text("#server-name", "10.111.113.2")
+ b.wait_visible("#hostkey-group")
+ b.wait_in_text("#hostkey-message-1", "You are connecting to 10.111.113.2 for the first time.")
+ b.click('#login-button')
+ b.wait_visible('#content')
+
+ # The old shell should have gotten the password from cockpit-ws and it should work
+ b.enter_page("/system")
+ b.click("#shutdown-group > button:contains('Restart')")
+ b.wait_popup("shutdown-dialog")
+ b.click("#shutdown-dialog button:contains('Restart')")
+ b.wait_popdown("shutdown-dialog")
+
+
+@testlib.skipImage("TODO: broken on Arch Linux", "arch")
+@testlib.skipDistroPackage()
+class TestSuperuserOldWebserver(testlib.MachineCase):
+ provision = {
+ "machine1": {"address": "10.111.113.1/20", "image": "centos-7", "memory_mb": 512},
+ "machine2": {"address": "10.111.113.2/20", "memory_mb": 512},
+ }
+
+ def test(self):
+ b = self.browser
+ m = self.machine
+
+ allow_old_cockpit_ws_messages(self)
+
+ m.execute("firewall-cmd --add-service cockpit")
+ m.start_cockpit()
+
+ # Use m1 to login into m2
+ b.open("/")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.set_checked('#authorized-input', val=True)
+ b.click("#show-other-login-options")
+ b.wait_visible("#server-group")
+ b.set_val("#server-field", "10.111.113.2")
+ b.click('#login-button')
+ b.wait_in_text("#server-name", "10.111.113.2")
+ b.wait_visible("#conversation-group")
+ b.wait_in_text("#conversation-prompt", "Fingerprint")
+ b.wait_in_text("#conversation-message", "Do you want to proceed this time?")
+ b.click('#login-button')
+ b.wait_visible('#content')
+
+ if not self.is_pybridge():
+ # The C bridge will recognize that it is being started by
+ # a old webserver (or old bridge) and will start "any"
+ # privileged bridge during startup.
+ b.check_superuser_indicator("Administrative access")
+ b.go("/playground/test")
+ b.enter_page("/playground/test")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+ self.assertIn('result: uid=0', b.text(".super-channel span"))
+
+ else:
+ # The Python bridge will not start any privileged bridge
+ # if not explicitly told, and a old webserver (or old
+ # bridge) does not tell it anything.
+ b.check_superuser_indicator("Limited access")
+ b.go("/playground/test")
+ b.enter_page("/playground/test")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'access-denied')
+
+ b.become_superuser()
+ b.go("/playground/test")
+ b.enter_page("/playground/test")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+ self.assertIn('result: uid=0', b.text(".super-channel span"))
+
+ def testNotAuth(self):
+ b = self.browser
+ m = self.machine
+
+ allow_old_cockpit_ws_messages(self)
+
+ m.execute("firewall-cmd --add-service cockpit")
+ m.start_cockpit()
+
+ # Use m1 to login into m2, but don't reuse the password
+ b.open("/")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.set_checked('#authorized-input', val=False)
+ b.click("#show-other-login-options")
+ b.wait_visible("#server-group")
+ b.set_val("#server-field", "10.111.113.2")
+ b.click('#login-button')
+ b.wait_in_text("#server-name", "10.111.113.2")
+ b.wait_visible("#conversation-group")
+ b.wait_in_text("#conversation-prompt", "Fingerprint")
+ b.wait_in_text("#conversation-message", "Do you want to proceed this time?")
+ b.click('#login-button')
+ b.wait_visible('#content')
+
+ # We should not have gotten the password from the old
+ # cockpit-ws, but we can get it back.
+
+ b.check_superuser_indicator("Limited access")
+ b.go("/playground/test")
+ b.enter_page("/playground/test")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'access-denied')
+
+ b.switch_to_top()
+ b.open_superuser_dialog()
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
+ b.check_superuser_indicator("Administrative access")
+ b.go("/playground/test")
+ b.enter_page("/playground/test")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+ self.assertIn('result: uid=0', b.text(".super-channel span"))
+
+
+@testlib.skipDistroPackage()
+class TestSuperuserDashboard(testlib.MachineCase):
+ provision = {
+ "machine1": {"address": "10.111.113.1/20", "memory_mb": 512},
+ "machine2": {"address": "10.111.113.2/20", "memory_mb": 512},
+ }
+
+ @testlib.todoPybridgeRHEL8()
+ def test(self):
+ b = self.browser
+ self.setup_provisioned_hosts()
+
+ self.login_and_go()
+ b.go("/@10.111.113.2")
+ b.wait_visible("#machine-troubleshoot")
+ b.click('#machine-troubleshoot')
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.wait_visible('#hosts_setup_server_dialog button:contains("Add")')
+ b.click('#hosts_setup_server_dialog button:contains("Add")')
+ b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
+ b.click('#hosts_setup_server_dialog button.pf-m-primary')
+ b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
+ b.set_input_text("#login-custom-password", "foobar")
+ b.click('#hosts_setup_server_dialog button:contains("Log in")')
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ # There should be no superuser indicator in the Overview
+
+ b.go("/@10.111.113.2/system")
+ b.enter_page("/system", host="10.111.113.2")
+ b.wait_not_present(".ct-overview-header-actions button:contains('Administrative access')")
+ b.wait_not_present(".ct-overview-header-actions button:contains('Limited access')")
+ b.leave_page()
+
+ # The superuser indicator in the Shell should apply to machine2
+
+ b.check_superuser_indicator("Limited access")
+ b.become_superuser()
+ b.go("/@10.111.113.2/playground/test")
+ b.enter_page("/playground/test", host="10.111.113.2")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+ self.assertIn('result: uid=0', b.text(".super-channel span"))
+
+ # Logging out and logging back in should give us immediate
+ # superuser on m2 (once we have logged in there).
+ b.relogin()
+ b.wait_visible("#machine-troubleshoot")
+ b.click('#machine-troubleshoot')
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.set_input_text("#login-custom-password", "foobar")
+ b.click('#hosts_setup_server_dialog button:contains("Log in")')
+ b.wait_not_present('#hosts_setup_server_dialog')
+ b.check_superuser_indicator("Administrative access")
+
+ b.enter_page("/playground/test", host="10.111.113.2")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+ self.assertIn('result: uid=0', b.text(".super-channel span"))
+
+ b.drop_superuser()
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'access-denied')
+
+ # back to superuser on machine2
+ b.become_superuser()
+ user = self.machines["machine2"].execute("loginctl user-status admin")
+ self.assertIn("cockpit-bridge --privileged", user)
+ # no stray askpass process
+ self.assertNotIn("cockpit-askpass", user)
+ # logging out cleans up logind sessions on both machines
+ b.logout()
+ for m in [self.machine, self.machines["machine2"]]:
+ m.execute('while [ "$(loginctl show-user --property=State --value admin)" = "active" ]; do sleep 1; done')
+ self.assertNotIn("cockpit", "loginctl user-status admin")
+
+ self.allow_hostkey_messages()
+
+
+@testlib.skipDistroPackage()
+class TestSuperuserOldDashboard(testlib.MachineCase):
+ provision = {
+ "machine1": {"address": "10.111.113.1/20", "image": "centos-7", "memory_mb": 512},
+ "machine2": {"address": "10.111.113.2/20", "memory_mb": 512},
+ }
+
+ def test(self):
+ b = self.browser
+ m = self.machines["machine1"]
+
+ allow_old_cockpit_ws_messages(self)
+ self.allow_hostkey_messages()
+ self.setup_provisioned_hosts()
+
+ m.execute("firewall-cmd --add-service cockpit")
+ m.start_cockpit()
+
+ # Log into m1 and add m2
+ b.open("/")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.set_checked('#authorized-input', val=True)
+ b.click('#login-button')
+ b.wait_visible('#content')
+
+ b.go("/@10.111.113.2")
+ b.wait_visible("#machine-troubleshoot")
+ b.click('#machine-troubleshoot')
+ b.wait_popup('troubleshoot-dialog')
+ b.click('#troubleshoot-dialog button:contains("Add")')
+ b.wait_in_text('#troubleshoot-dialog', "Fingerprint")
+ b.click('#troubleshoot-dialog button:contains("Connect")')
+ b.wait_popdown('troubleshoot-dialog')
+
+ # There should be a superuser button in the Overview of machine2
+
+ def get_admin():
+ b.go("/@10.111.113.2")
+ b.enter_page("/system", host="10.111.113.2")
+ b.click(".ct-overview-header-actions button:contains('Limited access')")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
+ b.wait_visible(".ct-overview-header-actions button:contains('Administrative access')")
+ b.go("/@10.111.113.2/playground/test")
+ b.enter_page("/playground/test", host="10.111.113.2")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'result: ')
+ self.assertIn('result: uid=0', b.text(".super-channel span"))
+
+ def drop_admin():
+ b.go("/@10.111.113.2")
+ b.enter_page("/system", host="10.111.113.2")
+ b.click(".ct-overview-header-actions button:contains('Administrative access')")
+ b.click(".pf-v5-c-modal-box:contains('Switch to limited access') button:contains('Limit access')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to limited access')")
+ b.wait_visible(".ct-overview-header-actions button:contains('Limited access')")
+ b.go("/@10.111.113.2/playground/test")
+ b.enter_page("/playground/test", host="10.111.113.2")
+ b.click(".super-channel button")
+ b.wait_in_text(".super-channel span", 'access-denied')
+
+ if not self.is_pybridge():
+ # The C bridge will recognize that it is being started by
+ # a old bridge (or old webserver) and will start "any"
+ # privileged bridge during startup. So we have admin at
+ # this point.
+ #
+ drop_admin()
+ get_admin()
+
+ else:
+ # The Python bridge will not start any privileged bridge
+ # if not explicitly told, and a old bridge (or old
+ # webserver) does not tell it anything. So we don't have
+ # admin at this point.
+ #
+ get_admin()
+ drop_admin()
+
+
+@testlib.skipDistroPackage()
+class TestSuperuserDashboardOldMachine(testlib.MachineCase):
+ provision = {
+ "machine1": {"address": "10.111.113.1/20"},
+ "machine2": {"address": "10.111.113.2/20", "image": "centos-7"},
+ }
+
+ @testlib.todoPybridgeRHEL8()
+ def test(self):
+ b = self.browser
+
+ self.setup_provisioned_hosts()
+ self.login_and_go()
+ b.go("/@10.111.113.2")
+ b.wait_visible("#machine-troubleshoot")
+ b.click('#machine-troubleshoot')
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.click('#hosts_setup_server_dialog button:contains("Add")')
+ b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to 10.111.113.2 for the first time.")
+ b.click('#hosts_setup_server_dialog button.pf-m-primary')
+ b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
+ b.set_input_text("#login-custom-password", "foobar")
+ b.click('#hosts_setup_server_dialog button:contains("Log in")')
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ # Since user and password are the same on machine2, we should
+ # have gotten admin rights.
+
+ b.enter_page("/system", host="10.111.113.2")
+ b.click("#shutdown-group > button:contains('Restart')")
+ b.wait_popup("shutdown-dialog")
+ b.click("#shutdown-dialog button:contains('Restart')")
+ b.wait_popdown("shutdown-dialog")
+
+ self.allow_hostkey_messages()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-system-info b/test/verify/check-system-info
new file mode 100755
index 0000000..2d41e94
--- /dev/null
+++ b/test/verify/check-system-info
@@ -0,0 +1,1109 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import time
+
+import packagelib
+import testlib
+
+os_release = """
+NAME="Foobar Adventure Linux Server"
+VERSION="2.0 (Day of Doom)"
+ID="foobar"
+VERSION_ID="2.0"
+PRETTY_NAME="Foobar Adventure Linux Server 2.0 (Day of Doom)"
+"""
+
+lscpu = """#!/bin/sh
+echo 'CPU(s): 8'
+echo 'On-line CPU(s) list: 0-7'
+echo 'Thread(s) per core: {0}'
+echo 'Core(s) per socket: 4'
+echo 'Socket(s): 1'
+"""
+
+
+def ssh_reconnect(machine, timeout_sec=120):
+ start_time = time.time()
+ error = None
+ while (time.time() - start_time) < timeout_sec:
+ try:
+ machine.execute("true", quiet=True)
+ return
+ except Exception as e:
+ error = e
+ time.sleep(0.5)
+
+ raise error
+
+
+@testlib.skipDistroPackage()
+class TestSystemInfo(testlib.MachineCase):
+ def setUp(self):
+ super().setUp()
+
+ # Most OSes don't set nosmt by default, but there are some exceptions
+ self.expect_smt_default = self.machine.image in ["fedora-coreos"]
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ # /etc/os-release might be a symlink and file watching doesn't
+ # follow symlinks, so we remove it and then create a regular
+ # file.
+ #
+ # In addition hostnamed does not expect os-release to change so
+ # we force a restart. Usually any such changes to os-release are
+ # expected to happen during reboot, or picked up after a reboot.
+ #
+ # subscription-manager also screws with os-release so set it
+ # to immutable
+ #
+ m.execute("rm /etc/os-release")
+ m.write("/etc/os-release", os_release)
+ m.execute("chattr +i /etc/os-release; (systemctl restart systemd-hostnamed || systemctl restart hostnamed)")
+
+ self.login_and_go("/system")
+
+ b.wait_visible('#system_information_os_text')
+
+ mid = m.execute("cat /etc/machine-id")
+ b.wait_text('#system_machine_id', mid)
+
+ # Health card can contain only one item - it normally is "Loading available updates fail"
+ # But sometimes it also contains information about failed services which breaks mobile pixel tests
+ m.execute("systemctl reset-failed")
+ b.wait_not_present("#page_status_notification_system_services")
+
+ # ensure general page/card layout without the changing specifics
+ # need to wait until CPU usage settles down, to avoid a layout-shifting error/warning icon
+ b.wait_not_present("#system-usage-cpu-progress + td .pf-v5-c-progress__status-icon")
+ testlib.wait(lambda: b.get_pf_progress_value("#system-usage-cpu-progress + td") < 30)
+ b.assert_pixels("#overview", "overview", ignore=[
+ ".system-health .pf-v5-c-card__body",
+ "#system_machine_id",
+ "#system_uptime",
+ # #system_information_systime_button is not enough, need to grab the icon as well
+ "tr:contains('System time') td",
+ # CPU/memory metrics
+ "#system-usage-cpu-progress + td",
+ "#system-usage-memory-progress + td",
+ "#tuned-status-button",
+ ])
+
+ # Generate a new rsa key and change the config
+ m.execute("ssh-keygen -f /etc/ssh/weirdname -t rsa -N ''")
+ m.execute("chmod 600 /etc/ssh/weirdname")
+ m.execute("restorecon /etc/ssh/weirdname || true")
+
+ new_default = m.execute("ssh-keygen -l -f /etc/ssh/weirdname -E md5 | cut -d' ' -f2 | tr -d '\n'")
+ new_alt = m.execute("ssh-keygen -l -f /etc/ssh/weirdname -E sha256 | cut -d' ' -f2 | tr -d '\n'")
+ old_default = m.execute("ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key -E md5 | cut -d' ' -f2 | tr -d '\n'")
+ old_alt = m.execute("ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key -E sha256 | cut -d' ' -f2 | tr -d '\n'")
+
+ b.click("#system-ssh-keys-link")
+ b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", "ED25519")
+ b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", "RSA")
+ b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", "ECDSA")
+ b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", new_default)
+ b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", old_default)
+ b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", new_alt)
+ b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", old_alt)
+
+ b.click('#system_information_ssh_keys button:contains("Close")')
+ b.wait_not_present("#system_information_ssh_keys")
+
+ # Change ssh config and restart
+ self.sed_file(r"s,.*HostKey *,#,; $ a HostKey /etc/ssh/weirdname", "/etc/ssh/sshd_config",
+ # Restart sshd but stop socket so we can make sure we are restarted
+ "( ! systemctl is-active sshd.socket || systemctl stop sshd.socket) && systemctl restart sshd.service")
+ ssh_reconnect(m)
+
+ b.click("#system-ssh-keys-link")
+ b.wait_visible("#system_information_ssh_keys")
+ b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", "ED25519")
+ b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", "RSA")
+ b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", "ECDSA")
+ b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", new_default)
+ b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", old_default)
+ b.wait_not_in_text("#system_information_ssh_keys .pf-v5-c-list", old_alt)
+ b.wait_in_text("#system_information_ssh_keys .pf-v5-c-list", new_alt)
+
+ b.wait_in_text('#system_information_os_text',
+ "Foobar Adventure Linux Server 2.0 (Day of Doom)")
+
+ b.click('#system_information_ssh_keys button:contains("Close")')
+ b.wait_not_present("#system_information_ssh_keys")
+
+ m.execute("hostnamectl set-hostname --static --pretty 'Adventure Box'")
+ b.wait_in_text('#system_information_hostname_text', "Adventure Box")
+
+ b.click('#system_information_hostname_button')
+ b.wait_visible("#system_information_change_hostname")
+ b.wait_val("#sich-pretty-hostname", "Adventure Box")
+ # Test setting the pretty hostname, changes the normal hostname
+ b.set_input_text("#sich-pretty-hostname", "Adventure Time")
+ b.wait_val("#sich-hostname", "adventure-time")
+ # Changing the hostname should validate
+ b.set_input_text("#sich-hostname", 65 * "x")
+ b.wait_in_text("#system_information_change_hostname .pf-v5-c-helper-text__item-text", "64 characters or less")
+ b.set_input_text("#sich-hostname", "host1.cockpit.lan$")
+ b.wait_in_text("#system_information_change_hostname .pf-v5-c-helper-text__item-text", "Real host name can only contain")
+ b.set_input_text("#sich-hostname", "host1.cockpit.lan")
+ b.click("#system_information_change_hostname button:contains('Change')")
+ b.wait_not_present("#system_information_change_hostname")
+
+ b.wait_in_text('#system_information_hostname_text', "Adventure Time (host1.cockpit.lan)")
+ self.assertEqual(m.execute("hostname").strip(), "host1.cockpit.lan")
+
+ m.execute("hostnamectl set-hostname ''")
+ m.execute("hostnamectl set-hostname --transient 'mydhcpname'")
+ b.wait_in_text('#system_information_hostname_text', 'mydhcpname')
+
+ b.logout()
+ m.execute("chattr -i /etc/os-release; rm /etc/os-release")
+ m.execute("rm /usr/lib/os-release || true")
+
+ self.login_and_go("/system")
+ b.wait_text('#system_machine_id', mid)
+
+ # uptime (introduced in PR #13885)
+ b.wait_text_not("#system_uptime", "")
+ # replace it with a known value, it should automatically update every minute
+ m.write("/tmp/fake_uptime", "2000.12 12345.30\n")
+ m.execute("mount -o bind /tmp/fake_uptime /proc/uptime")
+ self.addCleanup(m.execute, "umount /proc/uptime")
+ with b.wait_timeout(70):
+ b.wait_text("#system_uptime", "33 minutes")
+ # 4 months and a bit, timeformat rounds quite aggressively; also, test a slightly different format
+ m.write("/tmp/fake_uptime", "10370000 12345.30\n")
+ with b.wait_timeout(70):
+ b.wait_text("#system_uptime", "4 months")
+
+ self.allow_journal_messages("error loading contents of os-release: .*", # C bridge
+ ".* Neither /etc/os-release nor /usr/lib/os-release exists", # py bridge
+ "sudo: unable to resolve host host1.cockpit.lan: .*")
+
+ @testlib.nondestructive
+ def testMotd(self):
+ m = self.machine
+ b = self.browser
+
+ self.restore_file("/etc/motd")
+ m.execute("rm -f /etc/motd")
+
+ self.login_and_go("/system")
+ b.wait_not_present('#motd-box')
+
+ m.execute(r"printf '\n \n Hello\n World\n\n' >/etc/motd")
+ b.wait_visible('#motd-box')
+ # strips empty lines, but not leading spaces
+ b.wait_text('#motd', " Hello\n World")
+
+ # For some reason, switching to RTL takes a long time
+ # here. Let's increase the delay.
+ b.assert_pixels("#motd-box", "motd", wait_delay=2.0)
+
+ b.click('#motd-box button:not(#motd-box-edit)')
+ b.wait_not_present('#motd-box')
+
+ # motd should stay dismissed after a reload
+ b.reload()
+ b.enter_page("/system")
+ b.wait_not_present('#motd-box')
+
+ m.execute("echo Hello again >/etc/motd")
+ b.wait_visible('#motd-box')
+ b.wait_text('#motd', "Hello again")
+
+ # Cancel button
+ b.click("#motd-box-edit")
+ b.click("#motd-box-edit-modal button.pf-m-link")
+ b.wait_not_present("motd-box-edit-modal")
+
+ b.click("#motd-box-edit")
+ b.set_input_text("#motd-box-edit-modal textarea", "Hello cockpit team")
+ b.click("#motd-box-edit-modal button.pf-m-primary")
+ b.wait_not_present("motd-box-edit-modal")
+ b.wait_text('#motd', "Hello cockpit team")
+ self.assertEqual("Hello cockpit team", self.machine.execute("cat /etc/motd").rstrip())
+
+ @testlib.nondestructive
+ def testHardwareInfo(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/system")
+ b.wait_in_text('#system_information_hardware_text', "QEMU")
+
+ hardware_page_link = '.system-information a'
+ b.click(hardware_page_link)
+ b.enter_page("/system/hwinfo")
+
+ # system info
+ b.wait_in_text('#hwinfo-system-info-list', "CPU")
+ # QEMU VM type
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v5-c-description-list__group:nth-of-type(1) dd', "Other")
+ # Name
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v5-c-description-list__group:nth-of-type(2) dd', "Standard PC")
+ # BIOS
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v5-c-description-list__group:nth-of-type(1) dd', "SeaBIOS")
+ # BIOS date gets parsed
+ parsed_bios_date = m.execute("date --date $(cat /sys/class/dmi/id/bios_date) '+%B %-d, %Y'").strip()
+ b.wait_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v5-c-description-list__group:nth-of-type(3) dd', parsed_bios_date)
+
+ pci_selector = '#hwinfo #pci-listing'
+ heading_selector = ' .pf-v5-c-card__title'
+ # PCI
+ b.wait_in_text(pci_selector + heading_selector, "PCI")
+
+ b.wait_in_text(pci_selector + ' tbody:first-of-type td[data-label=Slot]', "0000:00:00.0")
+
+ # sorted by device class by default; this makes some assumptions about QEMU devices
+ b.wait_in_text(pci_selector + ' tbody:first-of-type td[data-label=Class]', "Bridge")
+ b.wait_in_text(pci_selector + ' tbody:last-of-type td[data-label=Class]', "Unclassified")
+
+ # sort by model
+ b.click(pci_selector + ' thead th:nth-child(2) button')
+ b.wait_in_text(pci_selector + ' tbody:first-of-type td[data-label=Model]', "440")
+ b.wait_in_text(pci_selector + ' tbody:last-of-type td[data-label=Model]', "Virtio SCSI")
+ b.wait_not_in_text(pci_selector + ' tbody:last-of-type td[data-label=Model]', "Unclassified")
+
+ # go back to system page
+ b.click('.pf-v5-c-breadcrumb li:first-of-type')
+
+ b.enter_page("/system")
+
+ # now pretend this is a system without DMI
+ b.logout()
+ m.execute("mount -t tmpfs none /sys/class/dmi/id")
+ # check if it's mounted as the memory tests umount it.
+ self.addCleanup(m.execute, "! mountpoint -q /sys/class/dmi/id || umount /sys/class/dmi/id")
+ self.login_and_go("/system")
+ # asset tag should be hidden
+ b.wait_not_present('#system_information_asset_tag_text')
+
+ # Hardware should be hidden
+ b.wait_not_present('#system_information_hardware_text')
+ b.click(hardware_page_link)
+ b.enter_page("/system/hwinfo")
+
+ # CPU should still be shown, but not the DMI fields
+ b.wait_in_text('#hwinfo-system-info-list', "CPU")
+ self.assertNotIn('Type', b.text('#hwinfo-system-info-list'))
+ self.assertNotIn('BIOS', b.text('#hwinfo-system-info-list'))
+
+ # PCI should be shown
+ b.wait_in_text(pci_selector + heading_selector, "PCI")
+ b.wait_in_text(pci_selector + ' tbody:first-of-type td[data-label=Slot]', "0000:00:00.0")
+
+ # Check also variants when only some fields are present
+ m.write("/sys/class/dmi/id/chassis_type", "10")
+ b.go("/system")
+ b.enter_page('/system')
+ b.wait_not_present('#system_information_hardware_text')
+
+ m.write("/sys/class/dmi/id/board_vendor", "VENDOR")
+ m.write("/sys/class/dmi/id/board_name", "NAME")
+ b.reload()
+ b.enter_page('/system')
+ b.wait_in_text('#system_information_hardware_text', "VENDOR NAME")
+ b.click(hardware_page_link)
+ b.enter_page("/system/hwinfo")
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v5-c-description-list__group:nth-of-type(2) dd', "NAME")
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v5-c-description-list__group:nth-of-type(3) dd', "VENDOR")
+
+ # Clean up after lazy OEMs, falls back to board vendor/name
+ m.write("/sys/class/dmi/id/sys_vendor", "To Be Filled By O.E.M.")
+ m.write("/sys/class/dmi/id/product_name", "To Be Filled By O.E.M.")
+ m.write("/sys/class/dmi/id/product_serial", "PL1234")
+ m.write("/sys/class/dmi/id/board_vendor", "brdven")
+ m.write("/sys/class/dmi/id/board_name", "brdnam")
+ b.reload()
+ b.go("/system")
+ b.enter_page('/system')
+ b.wait_in_text('#system_information_hardware_text', "brdven brdnam")
+ if m.execute("uname -m").strip() == "x86_64":
+ b.wait_in_text('#system_information_asset_tag_text', "PL1234")
+ else:
+ b.wait_not_present('#system_information_asset_tag_text')
+ b.click(hardware_page_link)
+ b.enter_page("/system/hwinfo")
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v5-c-description-list__group:nth-of-type(2) dd', "brdnam")
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(1) .pf-v5-c-description-list__group:nth-of-type(3) dd', "brdven")
+
+ # /proc/cpuinfo on x86; very incomplete, just what pkg/lib/machine-info.js looks at
+ m.write("/tmp/cpuinfo", """processor\t: 0
+vendor_id\t: GenuineIntel
+model\t\t: 42
+model name\t: Professor NumberCrunch
+
+processor\t: 1
+vendor_id\t: GenuineIntel
+model\t\t: 42
+model name\t: Professor NumberCrunch
+""")
+ m.execute("mount -o bind /tmp/cpuinfo /proc/cpuinfo")
+ self.addCleanup(m.execute, "umount /proc/cpuinfo")
+
+ b.reload()
+ b.enter_page('/system/hwinfo')
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v5-c-description-list__group:nth-of-type(1) dd', "2x Professor NumberCrunch")
+
+ # /proc/cpuinfo on PowerPC; complete info
+ m.write("/tmp/cpuinfo", """processor\t: 0
+cpu\t\t: POWER9 (architected), altivec supported
+clock\t\t: 3000.000000MHz
+revision\t: 2.3 (pvr 004e 1203)
+
+processor\t: 1
+cpu\t\t: POWER9 (architected), altivec supported
+clock\t\t: 3000.000000MHz
+revision\t: 2.3 (pvr 004e 1203)
+""")
+
+ b.reload()
+ b.enter_page('/system/hwinfo')
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v5-c-description-list__group:nth-of-type(1) dd', "2x POWER9 (architected), altivec supported")
+
+ # correct CPU count on overview
+ b.go("/system")
+ b.enter_page("/system")
+ b.wait_in_text("#system-usage-cpu-progress + td", "of 2 CPUs")
+
+ # /proc/cpuinfo on s390x (reduced)
+ m.write("/tmp/cpuinfo", """vendor_id : IBM/S390
+# processors : 2
+bogomips per cpu: 3241.00
+max thread id : 0
+features : esan3 zarch stfle msa ldisp eimm dfp edat etf3eh highgprs te vx vxd vxe gs vxe2 vxp sort dflt sie
+processor 0: version = FF, identification = 2EB428, machine = 8561
+processor 1: version = FF, identification = 2EB428, machine = 8561
+
+cpu number : 0
+cpu cores : 1
+version : FF
+identification : 2EB428
+machine : 8561
+
+cpu number : 1
+cpu cores : 1
+version : FF
+identification : 2EB428
+machine : 8561
+""")
+
+ b.reload()
+ b.enter_page("/system")
+ b.wait_in_text("#system-usage-cpu-progress + td", "of 2 CPUs")
+
+ b.go('/system/hwinfo')
+ b.enter_page('/system/hwinfo')
+ b.wait_in_text('#hwinfo-system-info-list .hwinfo-system-info-list-item:nth-of-type(2) .pf-v5-c-description-list__group:nth-of-type(1) dd', "2x IBM/S390")
+
+ # umount mocked /sys/class/dmi/id
+ m.execute("umount /sys/class/dmi/id")
+ m.execute("udevadm trigger --verbose /sys/devices/virtual/dmi/id")
+ b.reload()
+ b.go("/system/hwinfo")
+ b.enter_page('/system/hwinfo')
+
+ # Memory details should be shown from our mocked DMI information from systemd's test files.
+ b.wait_in_text('#hwinfo #memory-listing' + heading_selector, "Memory")
+ b.wait_in_text('#hwinfo #memory-listing table', "DIMM")
+ b.wait_in_text('#hwinfo #memory-listing table', "RAM")
+
+ tmp_dmi_tables = "/tmp/dmi_tables"
+ m.execute(f"mkdir {tmp_dmi_tables}")
+ self.addCleanup(m.execute, f"rm -rf {tmp_dmi_tables}")
+ m.upload(["verify/files/dmi/smbios_entry_point", "verify/files/dmi/DMI"], tmp_dmi_tables)
+ m.execute(f"mount -o bind {tmp_dmi_tables} /sys/firmware/dmi/tables")
+ self.addCleanup(m.execute, "umount /sys/firmware/dmi/tables")
+ m.execute("udevadm trigger --verbose /sys/devices/virtual/dmi/id")
+
+ b.reload()
+ b.enter_page('/system/hwinfo')
+ distro_without_systemd_memory_dmi = m.image == 'centos-8-stream' or m.image.startswith('rhel-8-')
+
+ # Test more specific memory data with a fake dmidecode
+ b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=ID]', "BANK 0: ChannelA-DIMM0")
+ b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Type]', "DDR4")
+ if distro_without_systemd_memory_dmi:
+ b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Size]', "4 GB")
+ else:
+ b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Size]', "4 GiB")
+ b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=State]', "Present")
+ b.wait_text('#memory-listing tbody:nth-of-type(1) td[data-label="Memory technology"]', "Unknown")
+ b.wait_text('#memory-listing tbody:nth-of-type(1) td[data-label=Rank]', "Single rank")
+ b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Speed]', "2400 MT/s")
+
+ b.wait_in_text('#memory-listing tbody:nth-of-type(2) td[data-label=ID]', "BANK 2: ChannelB-DIMM0")
+ b.wait_in_text('#memory-listing tbody:nth-of-type(2) td[data-label=Type]', "DDR4")
+ if distro_without_systemd_memory_dmi:
+ b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Size]', "4 GB")
+ else:
+ b.wait_in_text('#memory-listing tbody:nth-of-type(1) td[data-label=Size]', "4 GiB")
+ b.wait_in_text('#memory-listing tbody:nth-of-type(2) td[data-label=State]', "Present")
+ b.wait_text('#memory-listing tbody:nth-of-type(2) td[data-label="Memory technology"]', "Unknown")
+ b.wait_text('#memory-listing tbody:nth-of-type(2) td[data-label=Rank]', "Single rank")
+ b.wait_in_text('#memory-listing tbody:nth-of-type(2) td[data-label=Speed]', "2400 MT/s")
+
+ @ testlib.nondestructive
+ def testCPUSecurityMitigationsDetect(self):
+ b = self.browser
+ m = self.machine
+
+ self.restore_dir("/usr/local/bin")
+ m.start_cockpit()
+
+ def spoof_threads(threads_per_core, expect_link_present, expect_smt_state=None, cmdline=None):
+ m.write('/usr/local/bin/lscpu', lscpu.format(threads_per_core))
+ m.execute('chmod +x /usr/local/bin/lscpu')
+ if cmdline:
+ m.write('/run/cmdline', cmdline)
+ m.execute('if selinuxenabled 2>/dev/null; then chcon --reference /proc/cmdline /run/cmdline; fi')
+ m.execute('mount --bind /run/cmdline /proc/cmdline; rm /run/cmdline')
+
+ try:
+ b.login_and_go('/system/hwinfo')
+
+ if not expect_link_present:
+ b.wait_in_text('#hwinfo-system-info-list', "CPU")
+ b.wait_not_in_text('#hwinfo-system-info-list', "CPU security")
+ else:
+ b.click('#hwinfo button:contains(Mitigations)')
+
+ if expect_smt_state is not None:
+ b.wait_visible('#cpu-mitigations-dialog .nosmt-heading:contains(nosmt)')
+ b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +
+ (expect_smt_state and ":checked" or ":not(:checked)"))
+
+ b.logout()
+ finally:
+ if cmdline:
+ m.execute('while ! umount /proc/cmdline; do sleep 1; done')
+
+ spoof_threads(1, expect_link_present=False)
+ spoof_threads(2, expect_link_present=True, expect_smt_state=True, cmdline='param1 param2 nosmt param3=value3')
+ spoof_threads(2, expect_link_present=True, expect_smt_state=True, cmdline='param1 param2 nosmt=force param3=value3')
+ spoof_threads(2, expect_link_present=True, expect_smt_state=True, cmdline='param1 mitigations=auto,nosmt param3=value3')
+ spoof_threads(2, expect_link_present=True, expect_smt_state=True, cmdline='param1 mitigations=nosmt,something param3=value3')
+ spoof_threads(2, expect_link_present=True, expect_smt_state=False, cmdline='param1 mitigations=something param3=value3')
+ spoof_threads(2, expect_link_present=False, cmdline='param1 nosmt=someunknown param3=value3')
+ spoof_threads(2, expect_link_present=True, expect_smt_state=self.expect_smt_default, cmdline=None)
+
+ @testlib.skipImage("TODO: add Arch Linux grub entry support", "arch")
+ @testlib.timeout(1200)
+ def testCPUSecurityMitigationsEnable(self):
+ b = self.browser
+ m = self.machine
+
+ # spoof SMT
+ m.write('/usr/local/bin/lscpu', lscpu.format(2))
+ m.execute('chmod +x /usr/local/bin/lscpu')
+
+ # Switch nosmt option
+ self.login_and_go('/system/hwinfo')
+ b.click('#hwinfo button:contains(Mitigations)')
+ b.click('#cpu-mitigations-dialog #nosmt-switch input')
+ b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +
+ (self.expect_smt_default and ':not(:checked)' or ':checked'))
+ b.click('#cpu-mitigations-dialog Button:contains(Save and reboot)')
+
+ m.wait_reboot()
+ if self.expect_smt_default:
+ self.assertNotIn('nosmt', m.execute('cat /proc/cmdline'))
+ else:
+ self.assertIn('nosmt', m.execute('cat /proc/cmdline'))
+
+ # Ensure that future kernel upgrades also retain the option
+ # - Debian: no BLS, options go into /etc/default/grub and grub.cfg
+ # - BLS, options go directly into entries, or entries use $kernelopt (defined in grubenv)
+ if not m.ostree_image:
+ m.execute(r"""
+echo dummy > /boot/vmlinuz-42.0.0; mkdir -p /lib/modules/42.0.0/
+if type update-grub >/dev/null 2>&1; then
+ update-grub # Debian/Ubuntu
+ grep -q 'linux.*/vmlinuz-42.0.0.*nosmt' /boot/grub*/grub.cfg
+else
+ cp -a /boot/grub2/grubenv /boot/grub2/grubenv.prev
+ kernel-install add 42.0.0 /boot/vmlinuz-42.0.0 2>/dev/null
+ grep -q '^options.*\bnosmt\b' /boot/loader/entries/*42.0.0*.conf ||
+ ( grub2-editenv list | grep -q kernelopts.*nosmt &&
+ grep -q '^options.*$kernelopts' /boot/loader/entries/*42.0.0*.conf )
+fi
+""")
+ # clean up so that next reboot works
+ m.execute(r"""
+rm /boot/vmlinuz-42.0.0
+if type update-grub >/dev/null 2>&1; then
+ update-grub # Debian/Ubuntu
+else
+ kernel-install remove 42.0.0 /boot/vmlinuz-42.0.0
+ # HACK: https://bugzilla.redhat.com/show_bug.cgi?id=2078359 and https://bugzilla.redhat.com/show_bug.cgi?id=2078379
+ mv /boot/grub2/grubenv.prev /boot/grub2/grubenv
+fi
+""")
+
+ # Switch back nosmt option
+ self.login_and_go('/system/hwinfo')
+ b.click('#hwinfo button:contains(Mitigations)')
+ b.wait_visible('#cpu-mitigations-dialog .nosmt-heading:contains(nosmt)')
+ b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +
+ (self.expect_smt_default and ':not(:checked)' or ':checked'))
+ b.click('#cpu-mitigations-dialog #nosmt-switch input')
+ b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input' +
+ (self.expect_smt_default and ':checked' or ':not(:checked)'))
+ b.click('#cpu-mitigations-dialog Button:contains(Save and reboot)')
+ m.wait_reboot()
+ if self.expect_smt_default:
+ self.assertIn('nosmt', m.execute('cat /proc/cmdline'))
+ else:
+ self.assertNotIn('nosmt', m.execute('cat /proc/cmdline'))
+
+ # updates mitigations=nosmt when that is present
+ m.upload(["../pkg/systemd/kernelopt.sh"], "/tmp/")
+ m.execute("/tmp/kernelopt.sh remove nosmt; /tmp/kernelopt.sh set mitigations=auto,nosmt")
+ m.reboot()
+ self.login_and_go('/system/hwinfo')
+ b.click('#hwinfo button:contains(Mitigations)')
+ b.wait_visible('#cpu-mitigations-dialog .nosmt-heading:contains(nosmt)')
+ b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input:checked')
+ b.click('#cpu-mitigations-dialog #nosmt-switch input')
+ b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input:not(:checked)')
+ b.click('#cpu-mitigations-dialog Button:contains(Save and reboot)')
+ m.wait_reboot()
+ self.assertNotIn('nosmt', m.execute('cat /proc/cmdline'))
+ self.assertIn('mitigations=auto', m.execute('cat /proc/cmdline'))
+
+ # Behaviour for non-admins
+ self.login_and_go('/system/hwinfo', superuser=False)
+ b.wait_visible('#cpu_mitigations[disabled]')
+ b.mouse('#tip-cpu-security', 'mouseenter')
+ b.wait_text('.pf-v5-c-tooltip', 'The user admin is not permitted to change cpu security mitigations')
+ b.mouse('#tip-cpu-security', 'mouseleave')
+ b.wait_not_present("div.pf-v5-c-tooltip")
+
+ # Behaviour if grub update tools are missing
+ b.logout()
+ m.execute('mv /etc/default/grub /etc/default/grub.bak || true')
+ m.write('/tmp/grubby', '#!/bin/sh\necho 0')
+ m.execute('[ ! -f /usr/sbin/grubby ] || mount --bind /tmp/grubby /usr/sbin/grubby')
+ m.execute('systemctl stop rpm-ostreed.service || true; systemctl mask rpm-ostreed.service')
+ self.login_and_go('/system/hwinfo')
+ b.click('#hwinfo button:contains(Mitigations)')
+ b.click('#cpu-mitigations-dialog #nosmt-switch input')
+ b.wait_visible('#cpu-mitigations-dialog #nosmt-switch input:checked')
+ b.click('#cpu-mitigations-dialog Button:contains(Save and reboot)')
+ b.wait_visible('#cpu-mitigations-dialog .pf-v5-c-alert__title:contains(No supported grub update mechanism found)')
+
+ self.allow_journal_messages('Sourcing file `/etc/default/grub.*',
+ 'Generating grub configuration file.*',
+ 'Found linux image.*',
+ 'Found initrd image.*',
+ '.*warning: setlocale: LC_ALL: cannot change locale.*',
+ 'done')
+
+ @testlib.onlyImage("insights-client is only on RHEL", "rhel*")
+ @testlib.nondestructive
+ def testInsightsStatus(self):
+ m = self.machine
+ b = self.browser
+
+ # insights-client might get started during boot and might then
+ # run concurrently with our explicit "insights-client
+ # --register" below. insights-client is not designed to be
+ # run concurrently and there is no protection against it,
+ # apparently. So let's prevent that.
+ m.execute("systemctl disable --now insights-client")
+
+ self.restore_dir("/etc/insights-client")
+ self.restore_dir("/etc/yum")
+ self.restore_dir("/var/lib/insights")
+
+ # Pretend that the Subscriptions page can do Insights stuff
+ self.write_file("/etc/cockpit/subscription-manager.override.json", '{ "features": { "insights": true } }')
+
+ # Run a mock version of the Insights API locally and configure
+ # insights-client to access it. That requires a good enough
+ # TLS mock insights server certificate
+ m.upload(["verify/files/mock-insights", "../src/tls/ca/alice.key", "../src/tls/ca/alice.pem"], self.vm_tmpdir)
+ pid = m.spawn(f"{self.vm_tmpdir}/mock-insights", "mock-insights")
+ self.addCleanup(m.execute, f"kill {pid}")
+ m.execute("while ! ss -tulpn | grep 8443; do sleep 1; done")
+
+ hostname = m.execute("hostname").rstrip()
+ self.write_file("/etc/insights-client/insights-client.conf", f"""
+[insights-client]
+auto_config=False
+auto_update=False
+base_url={hostname}:8443/r/insights
+cert_verify=/var/lib/insights/mock-certs/ca.crt
+username=admin
+password=foobar
+""")
+
+ # Initially we are not registered
+ self.login_and_go('/system')
+ b.wait_text(".system-health-insights a", "Not connected to Insights")
+
+ # Enable insights, results should appear automatically
+ m.execute("insights-client --register", timeout=180)
+ self.addCleanup(m.execute, "insights-client --unregister")
+ with b.wait_timeout(60):
+ b.wait_in_text(".system-health-insights a", "3 hits, including important")
+ self.assertIn("123-nice-id", b.attr(".system-health-insights a", "href"))
+
+ # Switch to limited access, insights status will disappear completely
+ b.drop_superuser()
+ b.wait_not_present(".system-health-insights")
+
+ # Switch to admin access, insights status will re-appear
+ b.become_superuser()
+ b.wait_in_text(".system-health-insights a", "3 hits, including important")
+ self.assertIn("123-nice-id", b.attr(".system-health-insights a", "href"))
+ b.logout()
+
+ def testOverview(self):
+ m = self.machine
+ b = self.browser
+
+ # packagekit often eats a lot of CPU; silence it to not screw up the "system is idle" test
+ m.execute("systemctl mask packagekit")
+
+ def progressValue(number):
+ return b.get_pf_progress_value(f".system-usage tr:nth-child({number})")
+
+ self.login_and_go("/system")
+
+ # CPU
+ # first wait until system settles down
+ testlib.wait(lambda: progressValue(1) < 20)
+ m.spawn("for i in $(seq $(nproc)); do cat /dev/urandom > /dev/null & done", "cpu_hog.log")
+ testlib.wait(lambda: progressValue(1) > 75)
+ m.execute("pkill -e -f [c]at.*urandom")
+ # should go back to idle usage
+ # HACK: work around pmie CPU usage https://bugzilla.redhat.com/show_bug.cgi?id=2140572
+ testlib.wait(lambda: progressValue(1) < 20, tries=200)
+
+ # memory: our test machines should use a reasonable chunk of available memory; MiB or GiB
+ b.wait_in_text(".system-usage tr:nth-child(2)", "iB")
+ initial_usage = progressValue(2)
+ self.assertGreater(initial_usage, 10)
+ self.assertLess(initial_usage, 80)
+ # allocate an extra 200 MB; this may cause other stuff to get unmapped,
+ # thus not exact addition, but usage should go up
+ #
+ # The "true" after "sleep" is there to prevent bash from
+ # replacing it's own process with the sleep (as a "tail call
+ # optimization") and thereby dropping the memory blob too early.
+ #
+ mem_hog = m.spawn("MEMBLOB=$(yes | dd bs=1M count=200 iflag=fullblock); touch /tmp/hogged; sleep infinity; true", "mem_hog.log")
+ m.execute("while [ ! -e /tmp/hogged ]; do sleep 1; done")
+ # bars update every 5s
+ time.sleep(8)
+ hog_usage = progressValue(2)
+ self.assertGreater(hog_usage, initial_usage + 10)
+
+ m.execute("kill %d" % mem_hog)
+ # Should go back to initial_usage, but it doesn't always, for example on fedora.
+ # So let's be happy if the usage drops significantly
+ testlib.wait(lambda: progressValue(2) <= hog_usage - 15)
+ self.assertGreater(progressValue(2), 10)
+
+ @testlib.nondestructive
+ def testShutdownStatus(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/system")
+ b.wait_not_present("#system-health-shutdown-status")
+
+ # Schedule a reboot
+ m.execute("shutdown --reboot +10")
+ self.addCleanup(m.execute, "shutdown -c")
+
+ b.wait_in_text('#system-health-shutdown-status-text', "Scheduled reboot")
+
+ # Check that reloading still shows the reboot text
+ b.reload()
+ b.enter_page("/system")
+ b.wait_in_text('#system-health-shutdown-status-text', "Scheduled reboot")
+
+ # Cancel
+ b.click("#system-health-shutdown-status-cancel-btn")
+ b.wait_not_present('#system-health-shutdown-status')
+
+ # Schedule a poweroff
+ m.execute("shutdown --poweroff +10")
+ b.wait_in_text('#system-health-shutdown-status-text', "Scheduled poweroff")
+
+ # Cancel
+ b.click("#system-health-shutdown-status-cancel-btn")
+ b.wait_not_present('#system-health-shutdown-status')
+ dbus_call = 'busctl get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager ScheduledShutdown'
+ self.assertIn('(st) "" ', m.execute(dbus_call).strip())
+
+ @testlib.skipImage("crypto-policies not available", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipOstree("crypto-policies not available")
+ def testCryptoPolicies(self):
+ m = self.machine
+ b = self.browser
+ self.allow_restart_journal_messages()
+
+ def shown_profile_text(profile):
+ return "Default" if profile == "DEFAULT" else profile
+
+ def change_profile(profile, new_profile):
+ b.click("#crypto-policy-button")
+ b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected", shown_profile_text(profile))
+ profile_button_name = shown_profile_text(new_profile)
+ b.click(f"#crypto-policy-dialog .pf-v5-c-menu__list-item[data-value='{profile_button_name}'] button")
+ b.click("#crypto-policy-save-reboot")
+ # Initramfs re-generation takes a while
+ m.wait_reboot(timeout_sec=600)
+ m.start_cockpit()
+ self.login_and_go("/system")
+ b.wait_text("#crypto-policy-button", shown_profile_text(new_profile))
+
+ cmd = "update-crypto-policies"
+
+ self.login_and_go("/system")
+
+ profile = m.execute(cmd + " --show").strip()
+ b.wait_text("#crypto-policy-button", shown_profile_text(profile))
+
+ # RHEL 8 has no SHA1 policy, so do not show it.
+ b.click("#crypto-policy-button")
+ func = b.wait_not_present if m.image.startswith('rhel-8') or m.image.startswith('centos-8') else b.wait_visible
+ func(".pf-v5-c-menu__item-main .pf-v5-c-menu__item-text:contains('DEFAULT:SHA1')")
+ b.click("#crypto-policy-dialog button:contains('Cancel')")
+ b.wait_not_present("#crypto-policy-dialog")
+
+ # Test if a new subpolicy can be set
+ new_profile = "LEGACY:AD-SUPPORT"
+ change_profile(profile, new_profile)
+
+ profile = m.execute(cmd + " --show").strip()
+ self.assertEqual(profile, new_profile)
+ b.wait_text("#crypto-policy-button", shown_profile_text(profile))
+ new_profile = "FIPS"
+ change_profile(profile, new_profile)
+
+ # Select a custom policy (non-selectable option)
+ profile = "EMPTY"
+ m.execute(cmd + f" --set {profile}")
+ b.enter_page("/system")
+ b.wait_text("#crypto-policy-button", shown_profile_text(profile))
+ b.click("#crypto-policy-button")
+ b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected", shown_profile_text(profile))
+ b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected", "Custom cryptographic policy")
+ b.click("#crypto-policy-dialog button.pf-v5-c-button.pf-m-link")
+
+ @testlib.skipImage("crypto-policies not available", "debian-*", "ubuntu-*", "arch")
+ @testlib.skipOstree("crypto-policies not available")
+ def testInconsistentCryptoPolicy(self):
+ m = self.machine
+ b = self.browser
+ self.allow_restart_journal_messages()
+ cmd = "update-crypto-policies"
+
+ # Admin sets FIPS crypto policy in terminal, but FIPS mode is disabled
+ m.execute(cmd + " --set FIPS")
+ self.login_and_go("/system")
+ b.wait_text("#inconsistent_crypto_policy", "FIPS is not properly enabled")
+ b.click(".system-health-crypto-policies button.pf-v5-c-button.pf-m-link")
+ b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected .pf-v5-c-label.pf-m-orange", "inconsistent")
+ b.click("#crypto-policy-save-reboot")
+ # Initramfs re-generation takes a while
+ m.wait_reboot(timeout_sec=600)
+ m.start_cockpit()
+ self.login_and_go("/system")
+ b.wait_text("#crypto-policy-button", "FIPS")
+ self.assertEqual(m.execute("cat /proc/sys/crypto/fips_enabled").strip(), "1")
+
+ m.execute(cmd + " --set DEFAULT")
+ b.wait_text("#inconsistent_crypto_policy", "Cryptographic policy is inconsistent")
+ m.execute(cmd + " --set FIPS:OSPP")
+ b.wait_text("#crypto-policy-button", "FIPS:OSPP")
+ b.wait_not_present("#inconsistent_crypto_policy")
+
+ # Setting via dialog
+ m.execute(cmd + " --set DEFAULT")
+ b.wait_text("#inconsistent_crypto_policy", "Cryptographic policy is inconsistent")
+ b.click(".system-health-crypto-policies button.pf-v5-c-button.pf-m-link")
+ b.wait_in_text(".pf-v5-c-menu__item.pf-m-selected .pf-v5-c-label.pf-m-orange", "inconsistent")
+ b.click("#crypto-policy-save-reboot")
+ m.wait_reboot()
+ m.start_cockpit()
+ self.login_and_go("/system")
+ b.wait_text("#crypto-policy-button", "Default")
+ self.assertEqual(m.execute("cat /proc/sys/crypto/fips_enabled").strip(), "0")
+
+
+@testlib.skipDistroPackage()
+class TestSystemInfoTime(packagelib.PackageCase):
+ def set_change_time_dialog_mode(self, mode):
+ b = self.browser
+ b.click("#system_information_change_systime .pf-v5-c-form__group-label:contains('Set time') + div > .pf-v5-c-select > button")
+ b.click(f"#change_systime button:contains('{mode}')")
+ b.wait_in_text("#system_information_change_systime .pf-v5-c-form__group-label:contains('Set time') + div > .pf-v5-c-select > button", mode)
+
+ def testTime(self):
+ m = self.machine
+ b = self.browser
+
+ def ntp_enabled():
+ return 'true' in m.execute(
+ 'busctl get-property org.freedesktop.timedate1 /org/freedesktop/timedate1 org.freedesktop.timedate1 NTP')
+
+ # make sure system is on expected timezone EEST
+ m.execute("timedatectl set-timezone Europe/Helsinki")
+
+ # Something gets confused when systemd-timesyncd isn't
+ # available. This is harmless.
+ #
+ self.allow_journal_messages(
+ "org.freedesktop.systemd1: couldn't get property org.freedesktop.systemd1.Service ExecMain "
+ "at /org/freedesktop/systemd1/unit/systemd_2dtimedated_2eservice: "
+ "GDBus.Error:org.freedesktop.DBus.Error.UnknownProperty.*")
+ # journal gets confused with time jumps
+ self.allow_journal_messages(r"Journal file .*\.journal corrupted, ignoring file.*")
+
+ self.login_and_go("/system", superuser=False)
+ b.wait_text_not("#system_information_systime_button", "")
+ b.wait_visible('#system_information_systime_button[disabled]')
+
+ # Gain admin access
+ b.click(".pf-v5-c-alert:contains('Web console is running in limited access mode.') button:contains('Turn on')")
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for admin:")
+ b.set_input_text(".pf-v5-c-modal-box:contains('Switch to administrative access') input", "foobar")
+ b.click(".pf-v5-c-modal-box button:contains('Authenticate')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
+ b.wait_not_present(".pf-v5-c-alert:contains('Web console is running in limited access mode.')")
+
+ # Change the date
+ b.click("#system_information_systime_button")
+ b.wait_visible("#system_information_change_systime")
+ self.set_change_time_dialog_mode("Manually")
+ b.set_input_text("#systime-date-input input", "2037-01-24")
+ # invalid time
+ b.set_input_text("#systime-time-input-input", "25:61")
+ b.click("#system_information_change_systime .apply")
+ b.wait_text("#systime-manual-row .dialog-error", "Invalid time format")
+ # valid time
+ b.set_input_text("#systime-time-input-input", "08:03")
+ # wait until icon settles down
+ b.wait_visible("#systime-time-input-input[aria-invalid='false']")
+ b.wait_not_present("#systime-manual-row .dialog-error")
+ b.assert_pixels("#system_information_change_systime", "systime-manual-time")
+ b.click("#system_information_change_systime .apply")
+ with b.wait_timeout(60):
+ b.wait_not_present("#system_information_change_systime")
+
+ b.wait_text("#system_information_systime_button", "Jan 24, 2037, 8:03 AM")
+
+ self.assertFalse(ntp_enabled())
+ self.assertIn("Sat Jan 24 08:03:", m.execute("date"))
+ self.assertIn("EET 2037\n", m.execute("date"))
+
+ # Set to NTP
+ b.click("#system_information_systime_button")
+ b.wait_visible("#system_information_change_systime")
+ self.set_change_time_dialog_mode("Automatically using NTP")
+ b.click("#system_information_change_systime .apply")
+ with b.wait_timeout(60):
+ b.wait_not_present("#system_information_change_systime")
+
+ testlib.wait(ntp_enabled)
+
+ # Change the date
+ b.click("#system_information_systime_button")
+ b.wait_visible("#system_information_change_systime")
+ self.set_change_time_dialog_mode("Manually")
+ b.set_input_text("#systime-date-input input", "2018-06-04")
+ b.set_input_text("#systime-time-input-input", "06:34")
+ b.click("#system_information_change_systime .apply")
+ with b.wait_timeout(120): # Changing time on Arch can be slow
+ b.wait_not_present("#system_information_change_systime")
+
+ self.assertFalse(ntp_enabled())
+ self.assertIn("Mon Jun 4 06:34:", m.execute("date"))
+ self.assertIn("EEST 2018\n", m.execute("date"))
+
+ @testlib.skipImage("timesyncd not available", "rhel*", "centos-*")
+ def testTimeServersTimesyncd(self):
+ m = self.machine
+ b = self.browser
+
+ if m.image.startswith("debian") or m.image.startswith("ubuntu") or m.image == "arch":
+ if m.execute("type chronyc || true").strip() != "":
+ # chronyd is default, install timesyncd
+ self.addPackageSet("timesyncd")
+ self.enableRepo()
+ m.execute("apt-get update; apt-get install -y systemd-timesyncd")
+ m.execute("systemctl restart systemd-timedated; timedatectl set-ntp off; timedatectl set-ntp on")
+ else:
+ # timesyncd is default
+ pass
+ else:
+ # chronyd is default, give priority to timesyncd
+ self.write_file("/etc/systemd/ntp-units.d/10-test.list", "systemd-timesyncd.service")
+
+ conf = "/etc/systemd/timesyncd.conf.d/50-cockpit.conf"
+
+ self.login_and_go("/system")
+
+ # Wait until everything is ready to go...
+ b.wait_attr("#system_information_systime_button", "data-timedated-initialized", "true")
+
+ b.click("#system_information_systime_button")
+ b.wait_visible("#system_information_change_systime")
+
+ def get_timesyncd_start():
+ return int(m.execute("systemctl show -p ExecMainStartTimestampMonotonic --value systemd-timesyncd").strip())
+
+ prev_timesyncd_start = get_timesyncd_start()
+
+ # Add two NTP servers.
+ self.set_change_time_dialog_mode("Automatically using specific NTP servers")
+ b.set_input_text("#systime-ntp-servers div:nth-child(1) input", "0.pool.ntp.org")
+ b.click('#systime-ntp-servers div:nth-child(1) button')
+ b.set_input_text("#systime-ntp-servers div:nth-child(2) input", "1.pool.ntp.org")
+ b.click("#system_information_change_systime .apply")
+ with b.wait_timeout(120): # Changing time on Arch can be slow
+ b.wait_not_present("#system_information_change_systime")
+
+ self.assertIn("0.pool.ntp.org", m.execute(f"grep '^NTP=' {conf}"))
+ self.assertIn("1.pool.ntp.org", m.execute(f"grep '^NTP=' {conf}"))
+
+ # restarts timesyncd to pick up the new config
+ testlib.wait(lambda: get_timesyncd_start() > prev_timesyncd_start, delay=0.2)
+ prev_timesyncd_start = get_timesyncd_start()
+
+ # Set conf from the outside, check that we pick that up, and
+ # switch to default servers.
+ m.write(conf, "[Time]\nNTP=2.pool.ntp.org\n")
+ b.wait_attr("#system_information_systime_button", "data-timedated-initialized", "true")
+ b.click("#system_information_systime_button")
+ b.wait_visible("#system_information_change_systime")
+ b.wait_val("#systime-ntp-servers div:nth-child(1) input", "2.pool.ntp.org")
+ self.set_change_time_dialog_mode("Automatically using NTP")
+ b.wait_not_present("#systime-ntp-servers")
+ b.click("#system_information_change_systime .apply")
+ with b.wait_timeout(120): # Changing time on Arch can be slow
+ b.wait_not_present("#system_information_change_systime")
+
+ self.assertIn("2.pool.ntp.org", m.execute(f"grep '^#NTP=' {conf}"))
+
+ # restarts timesyncd to pick up the new config
+ testlib.wait(lambda: get_timesyncd_start() > prev_timesyncd_start, delay=0.2)
+
+ @testlib.skipImage("chronyd not available", "arch")
+ def testTimeServersChronyd(self):
+ m = self.machine
+ b = self.browser
+
+ enabled_conf = "/etc/chrony/sources.d/cockpit.sources"
+ disabled_conf = "/etc/chrony/sources.d/cockpit.disabled"
+
+ if m.image.startswith("debian") or m.image.startswith("ubuntu"):
+ # timesyncd is default, install chronyd
+ self.addPackageSet("chronyd")
+ self.enableRepo()
+ m.execute("apt-get update; apt-get install -y chrony")
+ m.execute("systemctl restart systemd-timedated; timedatectl set-ntp off; timedatectl set-ntp on")
+ else:
+ # chronyd is default
+ pass
+
+ self.login_and_go("/system")
+
+ # Wait until everything is ready to go...
+ b.wait_attr("#system_information_systime_button", "data-timedated-initialized", "true")
+
+ b.click("#system_information_systime_button")
+ b.wait_visible("#system_information_change_systime")
+
+ def get_chronyd_start():
+ return int(m.execute("systemctl show -p ExecMainStartTimestampMonotonic --value chronyd").strip())
+
+ prev_chronyd_start = get_chronyd_start()
+
+ # Add two NTP servers.
+ self.set_change_time_dialog_mode("Automatically using additional NTP servers")
+ b.set_input_text("#systime-ntp-servers div:nth-child(1) input", "0.pool.ntp.org")
+ b.click('#systime-ntp-servers div:nth-child(1) button')
+ b.set_input_text("#systime-ntp-servers div:nth-child(2) input", "1.pool.ntp.org")
+ b.click("#system_information_change_systime .apply")
+ with b.wait_timeout(60):
+ b.wait_not_present("#system_information_change_systime")
+
+ m.execute(f"grep 0.pool.ntp.org {enabled_conf}")
+ m.execute(f"grep 1.pool.ntp.org {enabled_conf}")
+ m.execute(f"! test -f {disabled_conf}")
+
+ # restarts chronyd to pick up the new config
+ testlib.wait(lambda: get_chronyd_start() > prev_chronyd_start, delay=0.2)
+ prev_chronyd_start = get_chronyd_start()
+
+ # Set conf from the outside, check that we pick that up, and
+ # switch to default servers.
+ m.write(enabled_conf, "server 2.pool.ntp.org\n")
+ b.wait_attr("#system_information_systime_button", "data-timedated-initialized", "true")
+ b.click("#system_information_systime_button")
+ b.wait_visible("#system_information_change_systime")
+ b.wait_val("#systime-ntp-servers div:nth-child(1) input", "2.pool.ntp.org")
+ self.set_change_time_dialog_mode("Automatically using NTP")
+ b.wait_not_present("#systime-ntp-servers")
+ b.click("#system_information_change_systime .apply")
+ with b.wait_timeout(60):
+ b.wait_not_present("#system_information_change_systime")
+
+ m.execute(f"! test -f {enabled_conf}")
+ m.execute(f"grep 2.pool.ntp.org {disabled_conf}")
+
+ # restarts timesyncd to pick up the new config
+ testlib.wait(lambda: get_chronyd_start() > prev_chronyd_start, delay=0.2)
+
+ def testTimeServersUnsupported(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute("! systemctl is-active chronyd || systemctl stop chronyd")
+ m.execute("! systemctl is-active systemd-timesyncd || systemctl stop systemd-timesyncd")
+ m.execute("systemctl mask chronyd.service || systemctl mask chrony.service")
+ m.execute("systemctl mask systemd-timesyncd.service")
+
+ self.login_and_go("/system")
+
+ # Wait until everything is ready to go...
+ b.wait_attr("#system_information_systime_button", "data-timedated-initialized", "true")
+
+ b.click("#system_information_systime_button")
+ b.wait_visible("#system_information_change_systime")
+ b.click("#system_information_change_systime .pf-v5-c-form__group-label:contains('Set time') + div > .pf-v5-c-select > button")
+ b.wait_visible("#change_systime button:contains('Automatically using NTP')")
+ b.wait_not_present("#change_systime button:contains('Automatically using specific NTP servers')")
+ b.wait_not_present("#change_systime button:contains('Automatically using additional NTP servers')")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-system-journal b/test/verify/check-system-journal
new file mode 100755
index 0000000..31146b2
--- /dev/null
+++ b/test/verify/check-system-journal
@@ -0,0 +1,725 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import time
+
+import testlib
+
+sleep_crash_list_sel = "#journal-box .cockpit-logline .cockpit-log-message:contains('(sleep) crashed in')"
+
+NO_ABRT = ["debian-*", "ubuntu-*", "fedora-coreos", "rhel*", "centos-*", "arch"]
+
+
+@testlib.skipDistroPackage()
+class TestJournal(testlib.MachineCase):
+ def setUp(self):
+ super().setUp()
+ self.test_start_time = self.machine.execute("date +%s").strip()
+
+ def injectExtras(self):
+ self.browser.inject_js("""
+ ph_get_log_lines = function () {
+ var lines = [ ];
+ var panels = ph_find('.cockpit-log-panel').childNodes;
+ for (var i = 0; i < panels.length; ++i) {
+ var e = panels[i];
+ if (e.className === 'panel-heading') {
+ lines.push (e.textContent);
+ } else {
+ var msg = e.querySelector('.cockpit-log-message');
+ msg = msg ? msg.textContent : "";
+ var ident = '';
+ var count = '';
+ if (e.querySelectorAll('.cockpit-log-service-container').length > 0) {
+ ident = e.querySelector('.cockpit-log-service-reduced').textContent;
+ count = e.querySelector('.pf-v5-c-badge').textContent;
+ } else {
+ ident = e.querySelector('.cockpit-log-service');
+ ident = ident ? ident.textContent : "";
+ }
+ lines.push ([ ident, msg, count ]);
+ }
+ }
+ var journal_start_text = ph_find('.journal-start').textContent;
+ if (journal_start_text !== "")
+ lines.push(journal_start_text);
+ // console.log(JSON.stringify(lines));
+ return lines;
+ }
+ """)
+
+ def crash(self):
+ self.allow_core_dumps = True
+
+ m = self.machine
+
+ m.execute("ulimit -c unlimited")
+ sleep = m.spawn("sleep 1m", "sleep.log")
+ m.execute("kill -SEGV %d" % (sleep))
+
+ def testBasic(self):
+ b = self.browser
+
+ m = self.machine
+
+ # Certain versions of journald won't set _SYSTEMD_UNIT
+ # correctly for entries that are processed after the
+ # originating process has already exited. So we keep the
+ # process around for a bit longer after the last line has been
+ # output.
+ #
+ self.write_file("/etc/systemd/system/log123.service",
+ """
+[Unit]
+Description=123 different log lines
+
+[Service]
+ExecStart=/bin/sh -c '/usr/bin/seq 123; sleep 10'
+""")
+
+ self.write_file("/etc/systemd/system/slow10.service",
+ """
+[Unit]
+Description=Slowly log 10 identical lines
+
+[Service]
+ExecStart=/bin/sh -c 'sleep 5; for s in $(seq 10); do echo SLOW; sleep 0.1; done; sleep 10'
+""")
+ self.addCleanup(m.execute, "systemctl daemon-reload")
+
+ self.login_and_go("/system/logs")
+ self.injectExtras()
+
+ def wait_log_lines(expected):
+ b.wait_visible(".cockpit-log-panel")
+ b.wait_js_func("""(function (expected) {
+ var lines = ph_get_log_lines ();
+
+ // Ignore date
+ if (lines.length > 1)
+ lines.shift();
+
+ if (expected.length != lines.length)
+ return false;
+ for (i = 0; i < expected.length; i++)
+ if (!new RegExp(JSON.stringify(expected[i])).test(JSON.stringify(lines[i])))
+ return false;
+ return true;
+ })""", expected)
+
+ def wait_journal_empty():
+ b.wait_in_text(".pf-v5-c-empty-state__body", "Can not find any logs using the current combination of filters.")
+
+ b.go("#/?priority=debug&service=log123.service")
+ wait_journal_empty()
+
+ def wait_log123():
+ b.wait_visible(".cockpit-log-panel")
+ b.wait_js_func("""(function () {
+ var lines = ph_get_log_lines();
+
+ var seq = 123;
+ var seen_day = false;
+
+ for (i = 1; i < lines.length; i++) {
+ l = lines[i];
+ if (l[2] != "") {
+ // console.log("repeat", l[2], "?");
+ return false;
+ }
+ if (l[0] == "systemd") {
+ // console.log(l[1], "?");
+ i++;
+ } else if (l[0] == "sh") {
+ if (l[1] != seq.toString()) {
+ // console.log(l[1], "?");
+ return false;
+ }
+ seq = seq - 1;
+ } else {
+ // console.log(l[0], "?");
+ return false;
+ }
+ }
+
+ if (seq != 0) {
+ // console.log("Didn't see all 'seq' lines.")
+ return false;
+ }
+
+ return true;
+ })""")
+
+ m.execute("systemctl start log123")
+
+ wait_log123()
+
+ b.go("#/?priority=debug&service=nonexisting.service")
+ wait_journal_empty()
+
+ b.go("#/?priority=debug&service=log123")
+ wait_log123()
+
+ b.go("#/?priority=debug&service=slow10.service")
+ wait_journal_empty()
+
+ def wait_slow10():
+ systemd_version = int(m.execute("systemctl --version | awk '{print $2; exit}'").strip())
+
+ if systemd_version >= 248:
+ # changed in https://github.com/systemd/systemd/commit/f5ec78e503
+ stop_message = "Deactivated successfully."
+ else:
+ stop_message = "Succeeded."
+
+ with b.wait_timeout(30):
+ wait_log_lines([["systemd", "slow10.service: " + stop_message, ""], ["sh", "SLOW", "10"],
+ ["systemd", "Started.*Slowly log 10 identical lines.", ""]])
+
+ def wait_log_present(message):
+ b.wait_visible(f"#journal-box .cockpit-logline .cockpit-log-message:contains('{message}')")
+
+ def wait_log_not_present(message):
+ b.wait_not_present(f"#journal-box .cockpit-logline .cockpit-log-message:contains('{message}')")
+
+ m.execute("systemctl start slow10")
+
+ wait_slow10()
+
+ b.go("#/?priority=debug&service=nonexisting.service")
+ wait_journal_empty()
+ b.go("#/?priority=debug&service=slow10.service")
+ wait_slow10()
+
+ b.go("#/?priority=debug")
+
+ m.execute("logger -p user.err --tag check-journal JUST ERROR")
+ m.execute("logger -p 5 --tag just-a-service THE SERVICE")
+ wait_log_present("THE SERVICE")
+ wait_log_present("JUST ERROR")
+
+ # We cannot open this select with tests but we need to wait until it gets loaded
+ b.select_PF4("#journal-identifier-menu .pf-v5-c-select__toggle", "just-a-service")
+ wait_log_not_present("JUST ERROR")
+ wait_log_present("THE SERVICE")
+ b.select_PF4("#journal-identifier-menu .pf-v5-c-select__toggle", "All")
+ wait_log_present("JUST ERROR")
+ wait_log_present("THE SERVICE")
+
+ self.browser.select_PF4("#journal-prio-menu", "Error and above")
+ wait_log_not_present("THE SERVICE")
+ wait_log_present("JUST ERROR")
+ b.select_PF4("#journal-identifier-menu .pf-v5-c-select__toggle", "check-journal")
+ b.wait_not_present("#journal-identifier-menu + ul button:contains('just-a-service')")
+
+ b.go("#/?tag=test-stream&priority=notice")
+ wait_journal_empty()
+
+ # Test inline help
+ b.click("#journal-grep button[aria-label='Open advanced search']")
+ b.wait_val("#journal-cmd-copy input", "journalctl --no-tail --since=-24hours --priority=notice --reverse -- SYSLOG_IDENTIFIER=test-stream")
+ b.click("#journal-grep button[aria-label='Open advanced search']")
+ b.wait_not_present("#journal-cmd-copy")
+
+ # Test following
+ m.execute("logger --tag test-stream Message 1")
+ m.execute("logger --tag test-stream Message 2")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 1')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 2')")
+
+ b.click("#journal-follow:contains('Pause')")
+ b.wait_visible("#journal-follow:contains('Resume')")
+ m.execute("logger --tag test-stream Message 3")
+ time.sleep(3)
+ b.wait_not_present("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 3')")
+
+ b.click("#journal-follow:contains('Resume')")
+ b.wait_visible("#journal-follow:contains('Pause')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 3')")
+ # This fails if all logs are reloaded
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 1')")
+ m.execute("logger --tag test-stream Message 4")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 4')")
+
+ # Test message filtering
+
+ b.focus("#journal-grep input")
+ b.key_press("1")
+ b.click("#journal-grep button[aria-label=Search]")
+
+ b.wait_js_cond('window.location.hash === "#/?priority=notice&tag=test-stream&grep=1"')
+ b.wait_not_present("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 2')")
+ b.wait_not_present("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 3')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 1')")
+
+ b.focus("#journal-grep input")
+ b.key_press("\bm.*[2-5]")
+ b.click("#journal-grep button[aria-label=Search]")
+ b.wait_not_present("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 1')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 2')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 3')")
+
+ m.execute(r"printf 'MESSAGE=Message 4\nPRIORITY=3\nFOO=bar\n' | logger --journald")
+ m.execute(r"printf 'MESSAGE=Message 5\nPRIORITY=3\nFOO=bar\n' | logger --journald")
+ m.execute(r"printf 'MESSAGE=Message 6\nPRIORITY=3\nBAR=foo\n' | logger --journald")
+
+ b.go("#/?tag=logger")
+
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 4')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 5')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 6')")
+
+ b.focus("#journal-grep input")
+ b.key_press("FOO=bar [5-6]")
+ b.click("#journal-grep button[aria-label=Search]")
+ b.wait_val("#journal-grep input", "priority:err identifier:logger FOO=bar [5-6]")
+
+ b.wait_not_present("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 4')")
+ b.wait_not_present("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 6')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 5')")
+
+ b.focus("#journal-grep input")
+ b.key_press("\b" * 13 + "[5-6]")
+ b.click("#journal-grep button[aria-label=Search]")
+ b.wait_not_present("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 4')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 6')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 5')")
+
+ # Test filters
+ b.select_PF4("#logs-predefined-filters", "Current boot")
+ b.wait_val("#journal-grep input", "priority:err identifier:logger boot:0 [5-6]")
+
+ b.select_PF4("#logs-predefined-filters", "Previous boot")
+ b.wait_val("#journal-grep input", "priority:err identifier:logger boot:-1 [5-6]")
+
+ b.select_PF4("#logs-predefined-filters", "Last 24 hours")
+ b.wait_val("#journal-grep input", "priority:err identifier:logger since:-24hours [5-6]")
+
+ b.select_PF4("#logs-predefined-filters", "Last 7 days")
+ b.wait_val("#journal-grep input", "priority:err identifier:logger since:-7days [5-6]")
+
+ # Check that 'until' qualifier keeps streaming until given time
+ b.focus("#journal-grep input")
+ b.key_press("\b\b\b\b6-9] until:+15s")
+ b.click("#journal-grep button[aria-label=Search]")
+ b.wait_not_present("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 5')")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 6')")
+ m.execute(r"printf 'MESSAGE=Message 7\nPRIORITY=3\nBAR=foo\n' | logger --journald")
+ b.wait_visible("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 7')")
+ time.sleep(20)
+ m.execute(r"printf 'MESSAGE=Message 8\nPRIORITY=3\nBAR=foo\n' | logger --journald")
+ time.sleep(5)
+ b.wait_not_present("#journal-box .cockpit-logline .cockpit-log-message:contains('Message 8')")
+
+ self.allow_journal_messages(".*Data from the specified boot .* is not available: No such boot ID in journal.*")
+
+ def testRebootAndTime(self):
+ b = self.browser
+ m = self.machine
+
+ self.allow_journal_messages(".*Failed to get realtime timestamp: Cannot assign requested address.*")
+
+ # HACK: pmie and pmlogger take a looooong time to shutdown (https://bugzilla.redhat.com/show_bug.cgi?id=1703348)
+ # so disable them for this test, we don't test PCP here
+ m.execute("systemctl disable --now pmie pmlogger || true")
+
+ m.execute("""ntp=`timedatectl show --property NTP --value`
+ if [ $ntp == "yes" ]; then
+ timedatectl set-ntp off
+ fi""")
+ # this is asynchronous; must wait until timesyncd stops before the time can be set
+ m.execute("while systemctl is-active systemd-timesyncd; do sleep 1; done")
+ m.execute("timedatectl set-time 2037-01-01")
+
+ def wait_log_lines(expected):
+ b.wait_visible(".cockpit-log-panel")
+ b.wait_js_func("""(function (expected) {
+ var lines = ph_get_log_lines ();
+
+ if (expected.length != lines.length)
+ return false;
+ for (i = 0; i < expected.length; i++)
+ if (JSON.stringify(expected[i]) != JSON.stringify(lines[i]))
+ return false;
+ return true;
+ })""", expected)
+
+ self.login_and_go("/system/logs")
+
+ self.injectExtras()
+ b.inject_js("""
+ localTime = function(time) {
+ var month_names = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December'
+ ];
+ var d = new Date(time);
+ return month_names[d.getMonth()] + ' ' + d.getDate().toFixed() + ', ' + d.getFullYear().toFixed();
+ }
+ """)
+ # January 1, 2037 00:00:00Z in browser time
+ expected_date = b.eval_js("localTime(2114380800000)")
+
+ # insert messages as errors because we know these will be shown by default
+ m.execute("systemctl start systemd-journald.service")
+ m.execute("logger -p user.err --tag check-journal BEFORE BOOT")
+ b.go("#/?tag=check-journal")
+ wait_log_lines([expected_date, ["check-journal", "BEFORE BOOT", ""]])
+ m.execute("systemctl stop systemd-journald.service")
+
+ # Now reboot things
+ m.reboot()
+ m.execute("timedatectl set-time '2037-01-01 00:05:00'")
+ m.execute("logger -p user.err --tag check-journal AFTER BOOT")
+
+ m.start_cockpit()
+ b.switch_to_top()
+ b.relogin('/system/logs')
+ self.injectExtras()
+
+ b.go("#/?tag=check-journal")
+ wait_log_lines([expected_date,
+ ["check-journal", "AFTER BOOT", ""],
+ ["", "Reboot", ""],
+ ["check-journal", "BEFORE BOOT", ""]
+ ])
+
+ b.go("#/?boot=0&tag=check-journal")
+ wait_log_lines([expected_date,
+ ["check-journal", "AFTER BOOT", ""],
+ "Load earlier entries",
+ ])
+
+ b.click('#start-box button:contains("Load earlier entries")')
+
+ wait_log_lines([expected_date,
+ ["check-journal", "AFTER BOOT", ""],
+ ["", "Reboot", ""],
+ ["check-journal", "BEFORE BOOT", ""]
+ ])
+
+ # Check selecting of services
+ b.go("#/?boot=-1&tag=check-journal")
+ wait_log_lines([expected_date,
+ ["check-journal", "BEFORE BOOT", ""],
+ "Load earlier entries"
+ ])
+
+ b.go("#/?boot=0&tag=check-journal")
+ wait_log_lines([expected_date,
+ ["check-journal", "AFTER BOOT", ""],
+ "Load earlier entries"
+ ])
+
+ # Check that 'until' qualifier shows only until some date
+ b.go("#/?tag=check-journal")
+ wait_log_lines([expected_date,
+ ["check-journal", "AFTER BOOT", ""],
+ ["", "Reboot", ""],
+ ["check-journal", "BEFORE BOOT", ""]
+ ])
+
+ b.focus("#journal-grep input")
+ b.key_press("until:2037-01-01\\ 00:03")
+ b.click("#journal-grep button[aria-label=Search]")
+ wait_log_lines([expected_date,
+ ["check-journal", "BEFORE BOOT", ""],
+ "Load earlier entries"
+ ])
+
+ b.click("#journal-identifier-menu .pf-v5-c-select__toggle")
+ b.wait_visible("#journal-identifier-menu .pf-v5-c-select__toggle + ul button:contains('check-journal')")
+
+ # New messages are not streamed when 'until' is in the past
+ m.execute("logger -p user.err --tag check-journal RIGHT NOW")
+ time.sleep(5)
+ wait_log_lines([expected_date,
+ ["check-journal", "BEFORE BOOT", ""],
+ "Load earlier entries"
+ ])
+
+ @testlib.nondestructive
+ def testBinary(self):
+ b = self.browser
+ m = self.machine
+ bin_data_long = {"MSG": (r"Hel\01" * 250) + "a", "BIN": (r"Hel\01" * 250) + "a"}
+ bin_data_short = {"MSG": "Hello \01 World", "BIN": "a\01b\02c\03"}
+
+ def wait_field(name, value):
+ row_sel = "dt:contains('" + name + "')"
+ b.wait_visible(row_sel + "+ dd:contains('" + value + "')")
+
+ def check_entry(msg, bin_msg):
+ b.click(f"#journal-box .cockpit-logline .cockpit-log-message:contains('{msg}')")
+ b.wait_text(".pf-v5-c-card__title", msg)
+ wait_field("FOO", "bar")
+ wait_field("BIN", bin_msg)
+ b.click('.pf-v5-c-breadcrumb li:first-of-type')
+ b.wait_visible(f"#journal-box .cockpit-logline .cockpit-log-message:contains('{msg}')")
+
+ m.execute(fr"printf 'MESSAGE={bin_data_short['MSG']}\nPRIORITY=3\nFOO=bar\nBIN={bin_data_short['BIN']}\n' | logger --journald")
+ self.login_and_go("/system/logs#/?tag=logger&since=@" + self.test_start_time)
+ check_entry(bin_data_short["MSG"], "[binary data]") # Test when binary msg is <= 1000 bytes
+ m.execute(fr"printf 'MESSAGE={bin_data_long['MSG']}\nPRIORITY=3\nFOO=bar\nBIN={bin_data_long['BIN']}\n' | logger --journald")
+ b.go("/system/logs#/?tag=logger&since=@" + self.test_start_time)
+ check_entry("[binary data]", "[binary data]") # Test when binary msg is > 1000 bytes
+
+ @testlib.nondestructive
+ def testNoMessage(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute(r"printf 'PRIORITY=3\nFOO=bar\n' | logger --journald")
+ self.login_and_go("/system/logs#/?tag=logger&since=@" + self.test_start_time)
+ b.click("#journal-box .cockpit-logline .cockpit-log-message:contains('[no data]')")
+ b.wait_text(".pf-v5-c-card__title", "[no data]")
+
+ @testlib.nondestructive
+ def testLogLevel(self):
+
+ b = self.browser
+ m = self.machine
+
+ m.execute("logger -p user.info --tag check-journal INFO_MESSAGE")
+ m.execute("logger -p user.warn --tag check-journal WARNING_MESSAGE")
+ m.execute("logger -p user.emerg --tag check-journal EMERGENCY_MESSAGE")
+
+ self.login_and_go("/system/logs#/?tag=check-journal&since=@" + self.test_start_time)
+
+ journal_line_selector = "#journal-box .cockpit-logline:has(span:contains({0}))"
+
+ self.browser.select_PF4("#journal-prio-menu", "Only emergency")
+ b.wait_visible(journal_line_selector.format("EMERGENCY_MESSAGE"))
+ b.wait_not_present(journal_line_selector.format("WARNING_MESSAGE"))
+ b.wait_not_present(journal_line_selector.format("INFO_MESSAGE"))
+
+ self.browser.select_PF4("#journal-prio-menu", "Warning and above")
+ b.wait_visible(journal_line_selector.format("EMERGENCY_MESSAGE"))
+ b.wait_visible(journal_line_selector.format("WARNING_MESSAGE"))
+ b.wait_not_present(journal_line_selector.format("INFO_MESSAGE"))
+
+ self.browser.select_PF4("#journal-prio-menu", "Info and above")
+ b.wait_visible(journal_line_selector.format("EMERGENCY_MESSAGE"))
+ b.wait_visible(journal_line_selector.format("WARNING_MESSAGE"))
+ b.wait_visible(journal_line_selector.format("INFO_MESSAGE"))
+
+ @testlib.skipImage("ABRT not available", *NO_ABRT)
+ @testlib.nondestructive
+ def testAbrtSegv(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("systemctl restart systemd-sysctl; systemctl start abrtd abrt-journal-core")
+ self.addCleanup(m.execute, "rm -rf /var/spool/abrt/*")
+ self.crash()
+
+ self.login_and_go("/system/logs#/?since=@" + self.test_start_time)
+
+ self.browser.select_PF4("#journal-prio-menu", "Critical and above")
+ b.wait_visible(sleep_crash_list_sel)
+
+ self.browser.select_PF4("#journal-prio-menu", "Alert and above")
+ b.wait_not_present(sleep_crash_list_sel)
+
+ self.browser.select_PF4("#journal-prio-menu", "Critical and above")
+ b.click(sleep_crash_list_sel)
+
+ def wait_field(name, value):
+ row_sel = "dt:contains('" + name + "')"
+ b.wait_visible(row_sel + "+ dd:contains('" + value + "')")
+
+ wait_field("SYSLOG_IDENTIFIER", "abrt-notification")
+
+ b.click("button:contains(Problem info)")
+ wait_field("reason", "killed by SIGSEGV")
+ b.click("button:contains(Problem details)")
+ b.click("#abrt-details .pf-v5-c-accordion__toggle:contains('core_backtrace')")
+ b.wait_visible("#abrt-details .pf-v5-c-accordion__expandable-content.pf-m-expanded dt:contains('signal') + dd:contains('11')")
+
+ @testlib.skipImage("ABRT not available", *NO_ABRT)
+ @testlib.nondestructive
+ def testAbrtDelete(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("systemctl restart systemd-sysctl; systemctl start abrtd abrt-journal-core")
+ self.addCleanup(m.execute, "rm -rf /var/spool/abrt/*")
+
+ # A bit of a race might happen if you delete the journal entry whilst
+ # the reporting code is doing its thing.
+ self.allow_browser_errors("Failed to get workflows for problem /org/freedesktop/Problems2/Entry/.*:.*")
+ self.allow_browser_errors("Getting properties for problem /org/freedesktop/Problems2/Entry/.* failed:.*")
+
+ self.crash()
+
+ self.login_and_go("/system/logs#/?since=@" + self.test_start_time)
+ b.click(sleep_crash_list_sel)
+
+ b.wait_in_text("#entry-heading", "sleep")
+ b.wait_visible("h2:contains('Crash reporting')")
+ b.click("button.pf-m-danger:contains('Delete')")
+ b.wait_in_text('#journal-box', "(sleep) crashed in")
+ b.click(sleep_crash_list_sel)
+
+ b.wait_in_text("#entry-heading", "sleep")
+ # details view should hide log view
+ b.wait_not_present("h2:contains('Crash reporting')")
+ b.wait_visible(".pf-v5-c-card__title:contains('(sleep) crashed in')")
+ b.wait_not_present("button.pf-m-danger:contains('Delete')")
+
+ @testlib.skipImage("ABRT not available", *NO_ABRT)
+ @testlib.nondestructive
+ def testAbrtReport(self):
+ # The testing server is located at verify/files/mock-faf-server.py
+ # Adjust Handler.known for for the expected succession of known/unknown problems
+ b = self.browser
+ m = self.machine
+
+ m.execute("systemctl restart systemd-sysctl; systemctl start abrtd abrt-journal-core")
+ self.addCleanup(m.execute, "rm -rf /var/spool/abrt/*")
+
+ # Restarting the reportd service will trigger some errors. Some depend
+ # on timing (interrupting the GetWorkflows call), so let's ignore some
+ # variations of the same thing.
+ self.allow_journal_messages('.*Remote peer disconnected')
+ self.allow_browser_errors('.*Failed to get workflows for problem .*: disconnected')
+
+ self.addCleanup(m.execute, "pkill -f '[m]ock-.*-server' || true")
+ m.upload(["verify/files/mock-bugzilla-server.py"], "/tmp/")
+ m.spawn("setsid /tmp/mock-bugzilla-server.py", "mock-bugzilla.log")
+ self.restore_file("/etc/libreport/plugins/bugzilla.conf")
+ m.execute("echo 'BugzillaURL=http://localhost:8080' > /etc/libreport/plugins/bugzilla.conf")
+
+ # start mock FAF server
+ m.upload(["verify/files/mock-faf-server.py"], "/tmp/")
+ m.spawn("setsid /tmp/mock-faf-server.py", "mock-faf.log")
+ self.restore_file("/etc/libreport/plugins/ureport.conf")
+ m.execute("echo 'URL=http://localhost:12345' > /etc/libreport/plugins/ureport.conf")
+
+ m.upload(["verify/files/cockpit_event.conf"], "/etc/libreport/events.d/")
+ m.upload(["verify/files/workflow_Cockpit.xml"], "/usr/share/libreport/workflows/")
+ m.execute("systemctl try-restart reportd")
+
+ self.crash()
+
+ self.login_and_go("/system/logs#/?since=@" + self.test_start_time)
+ b.click(sleep_crash_list_sel)
+ b.click("#abrt-reporting .pf-v5-l-split:first-child button:contains('Report')")
+ b.wait_visible("#abrt-reporting .pf-v5-l-split:first-child a[href$='/reports/bthash/123deadbeef']")
+
+ # "Unreport" the problem to test reporting unknown problem
+ m.execute('find /var/spool/abrt -name "reported_to" -or -name "ureports_counter" | xargs rm')
+ # reporter-bugzilla will not be run without a duphash file present.
+ m.execute("find /var/spool/abrt -depth -maxdepth 1 | head -1 | xargs -I {} cp '{}/uuid' '{}/duphash'")
+ # The service also needs to be restarted, because the daemon maintains
+ # its own cache that interferes with resetting the state.
+ m.execute('systemctl restart reportd')
+
+ b.reload()
+ b.enter_page("/system/logs")
+
+ b.click("#abrt-reporting .pf-v5-l-split:contains('Report to Cockpit') button:contains('Report')")
+
+ test_user = 'correcthorsebatterystaple'
+
+ # Since libreport version `2.15.2-7` bugzilla plugin only requires API key
+ b.wait_visible(".pf-v5-c-modal-box__body input[type='password']")
+ b.set_val(".pf-v5-c-modal-box__body input", test_user)
+ b.click(".pf-v5-c-modal-box__footer button:contains('Send')")
+
+ b.wait_not_present(".pf-v5-c-modal-box__body p:contains('Password')")
+ b.click(".pf-v5-c-modal-box__footer button:contains('No')")
+ b.wait_visible("#abrt-reporting .pf-v5-l-split:contains('Report to Cockpit') a[href='https://bugzilla.example.com/show_bug.cgi?id=123456']")
+
+ @testlib.skipImage("ABRT not available", *NO_ABRT)
+ @testlib.nondestructive
+ def testAbrtReportCancel(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("systemctl restart systemd-sysctl; systemctl start abrtd abrt-journal-core")
+ self.addCleanup(m.execute, "rm -rf /var/spool/abrt/*")
+
+ self.addCleanup(m.execute, "pkill -f '[m]ock-.*-server' || true")
+ m.upload(["verify/files/mock-bugzilla-server.py"], "/tmp/")
+ m.spawn("setsid /tmp/mock-bugzilla-server.py", "mock-bugzilla.log")
+ self.restore_file("/etc/libreport/plugins/bugzilla.conf")
+ m.execute("echo 'BugzillaURL=http://localhost:8080' > /etc/libreport/plugins/bugzilla.conf")
+
+ # start mock FAF server
+ m.upload(["verify/files/mock-faf-server.py"], "/tmp/")
+ m.spawn("setsid /tmp/mock-faf-server.py", "mock-faf.log")
+ self.restore_file("/etc/libreport/plugins/ureport.conf")
+ m.execute("echo 'URL=http://localhost:12345' > /etc/libreport/plugins/ureport.conf")
+
+ m.upload(["verify/files/cockpit_event.conf"], "/etc/libreport/events.d/")
+ m.upload(["verify/files/workflow_Cockpit.xml"], "/usr/share/libreport/workflows/")
+ m.execute("systemctl try-restart reportd")
+
+ self.crash()
+
+ self.login_and_go("/system/logs#/?since=@" + self.test_start_time)
+
+ b.click(sleep_crash_list_sel)
+ b.click("#abrt-reporting .pf-v5-l-split:contains('Report to Cockpit') button:contains('Report')")
+ b.wait_visible("#abrt-reporting .pf-v5-l-split:contains('Report to Cockpit') .pf-v5-l-split__item:contains('Cancel me')")
+ b.click("#abrt-reporting .pf-v5-l-split:contains('Report to Cockpit') button:contains('Cancel')")
+ b.wait_visible("#abrt-reporting .pf-v5-l-split:contains('Report to Cockpit') .pf-v5-l-split__item:contains('Reporting was canceled')")
+
+ @testlib.skipImage("ABRT not available", *NO_ABRT)
+ @testlib.nondestructive
+ def testAbrtReportNoReportd(self):
+ b = self.browser
+ m = self.machine
+
+ self.allow_browser_errors("Channel for reportd D-Bus client closed: .*")
+
+ m.execute("systemctl restart systemd-sysctl; systemctl start abrtd abrt-journal-core")
+ self.addCleanup(m.execute, "rm -rf /var/spool/abrt/*")
+
+ # start mock FAF server
+ self.addCleanup(m.execute, "pkill -f '[m]ock-.*-server' || true")
+ m.upload(["verify/files/mock-faf-server.py"], "/tmp/")
+ m.execute("setsid /tmp/mock-faf-server.py >/tmp/mock-faf.log 2>&1 &")
+ self.restore_file("/etc/libreport/plugins/ureport.conf")
+ m.execute("echo 'URL=http://localhost:12345' > /etc/libreport/plugins/ureport.conf")
+
+ m.execute("systemctl mask --now reportd")
+ self.addCleanup(m.execute, "systemctl unmask --now reportd")
+
+ self.crash()
+
+ self.login_and_go("/system/logs#/?since=@" + self.test_start_time)
+ b.click(sleep_crash_list_sel)
+ b.click("#abrt-reporting .pf-v5-l-split:first-child button:contains('Report')")
+ b.wait_visible("#abrt-reporting .pf-v5-l-split:first-child a[href$='/reports/bthash/123deadbeef']")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-system-realms b/test/verify/check-system-realms
new file mode 100755
index 0000000..eac612e
--- /dev/null
+++ b/test/verify/check-system-realms
@@ -0,0 +1,1244 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import base64
+import hashlib
+import hmac
+import re
+import struct
+import time
+
+import packagelib
+import testlib
+
+WAIT_KRB_SCRIPT = """
+set -ex
+# HACK: This needs to work, but may take a minute
+for x in $(seq 1 60); do
+ if getent passwd {0}; then
+ break
+ fi
+ if systemctl --quiet is-failed sssd.service; then
+ systemctl status --lines=100 sssd.service >&2
+ exit 1
+ fi
+ sss_cache -E || true
+ systemctl restart sssd.service
+ sleep $x
+done
+# ensure this works now, if the above loop timed out
+getent passwd {0}
+
+# HACK: This needs to work but may take a minute
+for x in $(seq 1 60); do
+ if ssh -oStrictHostKeyChecking=no -oBatchMode=yes -l {0} x0.cockpit.lan true; then
+ break
+ fi
+ sss_cache -E || true
+ systemctl restart sssd.service
+ sleep $x
+done
+"""
+
+# https://en.wikipedia.org/wiki/HMAC-based_One-time_Password_algorithm
+# https://stackoverflow.com/questions/8529265/google-authenticator-implementation-in-python
+
+
+def hotp_token(secret, counter, digits=6, hash_alg=hashlib.sha1):
+ counter_bytes = struct.pack('>Q', int(counter))
+ hs = hmac.new(secret, counter_bytes, hash_alg).digest()
+ ofs = hs[-1] & 0xF
+ numbers = str(int.from_bytes(hs[ofs:ofs + 4], 'big') & 0x7fffffff)
+ return numbers[-digits:].rjust(digits, '0')
+
+
+def maybe_setup_fake_chrony(machine):
+ # Some of our VM images have systemd-timesyncd installed, which
+ # conflicts with chrony. Set up a mock chrony.service to make
+ # ipa-client-install happy.
+ if machine.execute("type chronyc || echo not-found").strip() == "not-found":
+ machine.write("/run/systemd/system/chrony.service", """
+[Service]
+Type=oneshot
+ExecStart=/bin/true
+""")
+ machine.execute("ln -s /bin/true /usr/bin/chronyc")
+ machine.execute("systemctl unmask chrony")
+
+
+@testlib.skipDistroPackage()
+class CommonTests:
+
+ def wait_discover(self):
+ with self.browser.wait_timeout(60):
+ self.browser.wait_attr("#realms-op-address", "data-discover", "done")
+
+ def wait_address_helper(self, expected=None):
+ with self.browser.wait_timeout(60):
+ self.browser.wait_text("#realms-op-address-helper", expected or "Contacted domain")
+
+ @testlib.timeout(900)
+ def testQualifiedUsers(self):
+ m = self.machine
+ b = self.browser
+
+ # Tell realmd to enable domain-qualified logins; unqualified ones are covered in testUnqualifiedUsers
+ m.write("/etc/realmd.conf", "[cockpit.lan]\nfully-qualified-names = yes\n", append=True)
+
+ # Test that we reconnect on privileges change
+ self.login_and_go("/system", superuser=False)
+ b.wait_visible(f"{self.domain_sel}:disabled")
+ b.become_superuser()
+
+ def wait_number_domains(n):
+ if n == 0:
+ b.wait_text(self.domain_sel, "Join domain")
+ else:
+ b.wait_text_not(self.domain_sel, "Join domain")
+ b.wait_not_attr(self.domain_sel, "disabled", "disabled")
+
+ wait_number_domains(0)
+
+ def wait_domain_detected():
+ with b.wait_timeout(60):
+ b.wait_val(self.op_address, "cockpit.lan")
+
+ # Join cockpit.lan
+ b.click(self.domain_sel)
+ b.wait_popup("realms-join-dialog")
+ wait_domain_detected()
+ self.wait_address_helper()
+ # admin gets auto-detected
+ b.wait_val(self.op_admin, self.admin_user)
+ b.set_input_text(self.op_admin_password, self.admin_password)
+ # FIXME: there is tons of subpixel noise in the fonts, impossible to match with our naïve algorithm
+ # b.assert_pixels("#realms-join-dialog", "realm-join")
+ b.click(f"#realms-join-dialog button{self.primary_btn_class}")
+ # running operation cannot be cancelled any more
+ b.wait_visible("#realms-join-dialog button.pf-m-link:disabled")
+ # disables inputs during join
+ b.wait_visible("#realms-op-address:disabled")
+ b.wait_visible("#realms-op-admin:disabled")
+ b.wait_visible("#realms-op-admin-password:disabled")
+ with b.wait_timeout(300):
+ b.wait_not_present("#realms-join-dialog")
+
+ # Check that this has worked
+ wait_number_domains(1)
+
+ # when joined to a domain, changing the hostname is fatal, so should be disabled
+ b.wait_not_present("#system_information_hostname_button")
+
+ # should not have any leftover tickets from the joining
+ m.execute("! klist")
+ m.execute("! su -c klist " + self.admin_user)
+ b.logout()
+
+ # wait until IPA user works
+ m.execute(f'while ! su - -c "echo {self.admin_password} | sudo -S true" {self.admin_user}@cockpit.lan; do '
+ ' sleep 5; sss_cache -E || true; systemctl reset-failed sssd; systemctl try-restart sssd; done',
+ timeout=300)
+
+ # change existing local "admin" home dir to domain "admin" user
+ m.execute(f"chown -R {self.admin_user}@cockpit.lan /home/admin")
+
+ # log in as domain admin and check that we can do privileged operations
+ b.login_and_go('/system/services#/systemd-tmpfiles-clean.timer', user=f'{self.admin_user}@cockpit.lan', password=self.admin_password)
+ b.wait_in_text("#statuses", "Running")
+ b.click(".service-top-panel .pf-v5-c-dropdown button")
+ b.click(".service-top-panel .pf-v5-c-dropdown__menu a:contains('Stop')")
+ b.wait_in_text("#statuses", "Not running")
+ # stopping the unit may interrupt the D-Bus proxy inspection of that unit
+ self.allow_journal_messages(".*systemd1:.*systemd_2dtmpfiles_2dclean_2etimer: Timeout was reached")
+ b.logout()
+
+ # should also work with capitalized domain and lower-case user (fixed in PR #13934)
+ # need to change URL to actually reload the page
+ b.login_and_go('/system', user=f'{self.admin_user.lower()}@COCKPIT.LAN', password=self.admin_password)
+ b.go('/system/services#/systemd-tmpfiles-clean.timer')
+ b.enter_page('/system/services')
+ b.wait_in_text("#statuses", "Not running")
+ b.click(".service-top-panel .pf-v5-c-dropdown button")
+ b.click(".service-top-panel .pf-v5-c-dropdown__menu a:contains('Start')")
+ b.wait_in_text("#statuses", "Running")
+ b.logout()
+
+ self.checkBackendSpecifics()
+
+ # change home directory ownership back to local user
+ m.execute("chown -R admin /home/admin")
+
+ if m.image.startswith("debian"):
+ # HACK: https://bugs.debian.org/1038925
+ m.execute(r"sed -i '/\[ntp\]/,/^$/ d' /var/lib/ipa-client/sysrestore/sysrestore.state")
+
+ # Test domain info (PR #11096), leave the domain
+ b.login_and_go("/system")
+ b.wait_in_text(self.domain_sel, "cockpit.lan")
+ b.click(self.domain_sel)
+ b.wait_popup("realms-leave-dialog")
+ b.wait_text("#realms-op-info-domain", "cockpit.lan")
+ b.wait_text("#realms-op-info-login-format", "username@cockpit.lan")
+ b.wait_text("#realms-op-info-server-sw", self.expected_server_software)
+ b.wait_text("#realms-op-info-client-sw", "sssd")
+ # leave button should be hidden behind expander by default
+ b.wait_not_visible("#realms-op-leave")
+ b.wait_not_visible("#realms-leave-dialog .pf-v5-c-alert")
+ b.click("#realms-leave-dialog .pf-v5-c-expandable-section__toggle")
+ b.wait_visible("#realms-leave-dialog .pf-v5-c-alert")
+ # caret expander is animated and not reproducible even after a sleep
+ # FIXME: there is tons of subpixel noise in the fonts, impossible to match with our naïve algorithm
+ # b.assert_pixels("#realms-leave-dialog", "realm-leave", [".pf-v5-c-expandable-section__toggle-icon"])
+ b.click("#realms-op-leave")
+
+ with b.wait_timeout(60):
+ b.wait_not_present("#realms-leave-dialog")
+ wait_number_domains(0)
+ # re-enables hostname changing
+ b.wait_visible("#system_information_hostname_button:not([disabled])")
+
+ self.checkBackendSpecificCleanup()
+
+ # Sometimes with some versions of realmd the Leave operation
+ # from above is still active in the realmd daemon. So we loop
+ # here until we get the expected error instead of "Already
+ # running another action".
+
+ tries = 0
+ while tries < 3:
+ # Send a wrong password
+ b.click(self.domain_sel)
+ b.wait_popup("realms-join-dialog")
+ wait_domain_detected()
+ b.wait_val(self.op_admin, self.admin_user)
+ b.set_input_text(self.op_admin_password, "foo")
+ b.click(f"#realms-join-dialog button{self.primary_btn_class}")
+ with b.wait_timeout(60):
+ b.wait_text_not(".realms-op-error", "")
+ error = b.text(".realms-op-error")
+ if "Already running another action" not in error:
+ # "More" link is part of the message component, so this looks a little funny here
+ if self.expected_server_software == 'active-directory':
+ self.assertEqual(error, "Danger alert:Failed to join the domainDetails")
+ else:
+ self.assertEqual(error, "Danger alert:Password is incorrectDetails")
+ # "More" should be visible, and diagnostics not shown by default
+ b.wait_not_present(".realms-op-diagnostics")
+ b.click(".realms-op-error button")
+ # that hides the Details link
+ b.wait_not_present(".realms-op-error button")
+ # and shows the raw log
+ b.wait_visible(".realms-op-diagnostics")
+ if self.expected_server_software == 'active-directory':
+ b.wait_in_text(".realms-op-diagnostics", "Couldn't authenticate as: Administrator@COCKPIT.LAN")
+ else:
+ b.wait_in_text(".realms-op-diagnostics", "ipa-client-install command failed")
+ b.click("#realms-join-dialog button.pf-m-link")
+ b.wait_not_present("#realms-join-dialog")
+ if "Already running another action" not in error:
+ break
+ print("Another operation running, retry")
+ time.sleep(20)
+ tries += 1
+
+ # Try to join a non-existing domain
+ b.click(self.domain_sel)
+ b.wait_popup("realms-join-dialog")
+ # wait for auto-detection
+ wait_domain_detected()
+ b.set_input_text(self.op_address, "NOPE")
+ self.wait_address_helper("Domain could not be contacted")
+ b.wait_visible(f"#realms-join-dialog button{self.primary_btn_class}:disabled")
+ b.click("#realms-join-dialog button.pf-m-link")
+ b.wait_not_present("#realms-join-dialog")
+
+ # Join a domain with the server as address (input differs from domain name)
+ b.click(self.domain_sel)
+ b.wait_popup("realms-join-dialog")
+ self.wait_discover()
+ b.set_input_text(self.op_address, "f0.cockpit.lan")
+ self.wait_address_helper()
+ # admin gets auto-detected
+ b.wait_val(self.op_admin, self.admin_user)
+ b.set_input_text(self.op_admin_password, self.admin_password)
+ b.click(f"#realms-join-dialog button{self.primary_btn_class}")
+ with b.wait_timeout(300):
+ b.wait_not_present("#realms-join-dialog")
+ wait_number_domains(1)
+
+ self.allow_journal_messages(".*No authentication agent found.*")
+ # sometimes polling for info and joining a domain creates this noise
+ self.allow_journal_messages('.*org.freedesktop.DBus.Error.Spawn.ChildExited.*')
+
+ def testUnqualifiedUsers(self):
+ m = self.machine
+ b = self.browser
+
+ # delete the local admin user, going to use the domain one instead
+ m.execute("userdel --remove admin; systemctl try-restart sssd")
+
+ # Tell realmd to not enable domain-qualified logins
+ # (https://bugzilla.redhat.com/show_bug.cgi?id=1575538)
+ m.write("/etc/realmd.conf", "[cockpit.lan]\nfully-qualified-names = no\n", append=True)
+ m.execute(f"echo {self.admin_password} | realm join -vU {self.admin_user} cockpit.lan", timeout=300)
+
+ # wait until domain user works
+ m.execute('while ! su - -c "echo %s | sudo -S true" %s; do sleep 5; sss_cache -E || true; systemctl try-restart sssd; done' %
+ (self.admin_password, self.admin_user), timeout=300)
+
+ # login should now work with the domain admin user
+ b.password = self.admin_password
+ self.login_and_go("/system", user=self.admin_user)
+ b.wait_in_text(self.domain_sel, "cockpit.lan")
+
+ # Show domain information
+ b.click(self.domain_sel)
+ b.wait_popup("realms-leave-dialog")
+ b.wait_text("#realms-op-info-domain", "cockpit.lan")
+ b.wait_text("#realms-op-info-login-format", "username") # no @domain
+ b.wait_text("#realms-op-info-server-sw", self.expected_server_software)
+ b.wait_text("#realms-op-info-client-sw", "sssd")
+ b.click(f"#realms-leave-dialog button{self.default_btn_class}")
+ with b.wait_timeout(30):
+ b.wait_not_present("#realms-leave-dialog")
+
+ # should be able to run admin operations
+ b.go('/system/services#/systemd-tmpfiles-clean.timer')
+ b.enter_page('/system/services')
+
+ b.wait_in_text("#statuses", "Running")
+ b.click(".service-top-panel .pf-v5-c-dropdown button")
+ b.click(".service-top-panel .pf-v5-c-dropdown__menu a:contains('Stop')")
+ b.wait_in_text("#statuses", "Not running")
+
+ b.go('/system')
+ b.enter_page('/system')
+ # shutdown button should be enabled and working
+ # it takes a while for the permission check to finish, it is always enabled at first
+ b.click("#overview #reboot-button")
+
+ b.wait_popup("shutdown-dialog")
+ b.click("#delay")
+ b.click("button:contains('No delay')")
+ b.wait_text("#delay .pf-v5-c-select__toggle-text", "No delay")
+ b.click("#shutdown-dialog button:contains(Reboot)")
+ b.switch_to_top()
+ b.wait_in_text(".curtains-ct h1", "Disconnected")
+ m.wait_reboot()
+
+ self.allow_journal_messages(".*No authentication agent found.*")
+ self.allow_restart_journal_messages()
+ # sometimes polling for info and joining a domain creates this noise
+ self.allow_journal_messages('.*org.freedesktop.DBus.Error.Spawn.ChildExited.*')
+
+ def checkClientCertAuthentication(self):
+ """Common tests for certificate authentication
+
+ This assumes that IdM and sssd are all set up correctly already.
+ """
+ m = self.machine
+ b = self.browser
+
+ # join domain, wait until it works
+ m.write("/etc/realmd.conf", "[cockpit.lan]\nfully-qualified-names = no\n", append=True)
+ # join client machine with Cockpit, to create the HTTP/ principal and /etc/cockpit/krb5.keytab
+ self.login_and_go("/system")
+ b.click("#system_information_domain_button")
+ b.wait_popup("realms-join-dialog")
+ self.wait_discover()
+
+ b.set_input_text("#realms-op-address", "cockpit.lan")
+ self.wait_address_helper()
+ b.set_input_text("#realms-op-admin", self.admin_user)
+ b.set_input_text("#realms-op-admin-password", self.admin_password)
+ b.click(f"#realms-join-dialog button{self.primary_btn_class}")
+ with b.wait_timeout(300):
+ b.wait_not_present("#realms-join-dialog")
+ b.logout()
+ m.execute('while ! id alice; do sleep 5; done', timeout=300)
+
+ # alice's certificate was written by testClientCertAuthentication()
+ alice_cert_key = ['--cert', "/var/tmp/alice.pem", '--key', "/var/tmp/alice.key"]
+ alice_user_pass = ['-u', 'alice:' + self.alice_password]
+
+ if self.__class__ == TestIPA:
+ # `realm join` does not automatically configure sssd to be able to validate certificates;
+ # it needs to be explicitly told the CA to trust. Use the IPA's CA. (That would be an
+ # excellent default, but oh well, we can't have all nice things.)
+ m.execute("mkdir -p /etc/sssd/pki/; cp /etc/ipa/ca.crt /etc/sssd/pki/sssd_auth_ca_db.pem")
+
+ # ensure sssd certificate lookup works
+ user_obj = m.execute('busctl call org.freedesktop.sssd.infopipe /org/freedesktop/sssd/infopipe/Users '
+ 'org.freedesktop.sssd.infopipe.Users FindByCertificate s -- '
+ """"$(cat /var/tmp/alice.pem)" | sed 's/^o "//; s/"$//' """)
+ self.assertEqual(m.execute('busctl get-property org.freedesktop.sssd.infopipe ' + user_obj.strip() +
+ ' org.freedesktop.sssd.infopipe.Users.User name').strip(),
+ 's "alice"')
+
+ # These tests have to be run with curl, as chromium-headless does not support selecting/handling client-side
+ # certificates; it just rejects cert requests. For interactive tests, grab src/tls/ca/alice.p12 and import
+ # it into the browser.
+
+ def do_test(authopts, expected, not_expected=None, session_leader=None, retry=False):
+ m.start_cockpit(tls=True)
+
+ def try_auth():
+ output = m.execute(['curl', '-ksS', '-D-', *authopts, 'https://localhost:9090/cockpit/login'])
+ for s in expected:
+ self.assertIn(s, output)
+ for s in (not_expected or []):
+ self.assertNotIn(s, output)
+ return True
+
+ if retry:
+ testlib.wait(try_auth, delay=5, tries=10)
+ else:
+ try_auth()
+
+ # sessions/users often hang around in State=closing for a long time, ignore these
+ if session_leader:
+ m.execute('until [ "$(loginctl show-user --property=State --value alice)" = "active" ]; do sleep 1; done')
+ sessions = m.execute('loginctl show-user --property=Sessions --value alice').strip().split()
+ self.assertGreaterEqual(len(sessions), 1)
+ for session in sessions:
+ out = m.execute('loginctl session-status ' + session)
+ if "State: active" in out: # skip closing sessions
+ self.assertIn(session_leader, out)
+ self.assertIn('cockpit-bridge', out)
+ # systemd < 255: "Service: cockpit; type web; class user"
+ # systemd ≥ 255: "Service: cockpit\n Type: web\n Class: user"
+ self.assertRegex(out, r"Service:\s+cockpit")
+ self.assertRegex(out, "[tT]ype.*web")
+ break
+ else:
+ self.fail("no active session for active user")
+
+ # sessions time out after 10s, but let's not wait for that
+ m.execute('loginctl terminate-session ' + sessions[0])
+ # wait until the session is gone
+ m.execute("while loginctl show-user alice | grep -q 'State=active'; do sleep 1; done")
+
+ m.stop_cockpit()
+
+ # from sssd
+ self.allow_journal_messages("alice is not allowed to run sudo on x0. This incident will be reported.")
+ # occasional intermediate error during password auth
+ self.allow_journal_messages("cockpit-session: user account access failed: 4 alice: System error")
+
+ # cert auth should not be enabled by default
+ do_test(alice_cert_key, ["HTTP/1.1 401 Authentication required", '"authorize"'])
+ # password auth should work (but might need to be retried)
+ do_test(alice_user_pass, ['HTTP/1.1 200 OK', '"csrf-token"'], session_leader='cockpit-session', retry=True)
+
+ # enable cert based auth
+ m.write("/etc/cockpit/cockpit.conf", '[WebService]\nClientCertAuthentication = true\n', append=True)
+ # cert auth should work now
+ do_test(alice_cert_key, ['HTTP/1.1 200 OK', '"csrf-token"'])
+ # password auth, too
+ do_test(alice_user_pass, ['HTTP/1.1 200 OK', '"csrf-token"'], session_leader='cockpit-session')
+ # cert auth should go through PAM stack and re-create home dir
+ m.execute("rm -r ~alice")
+ do_test(alice_cert_key, ['HTTP/1.1 200 OK', '"csrf-token"'])
+ m.execute("test -f ~alice/.bashrc")
+
+ # another certificate gets rejected
+ self.allow_journal_messages("cockpit-session: .*User not found")
+ self.allow_journal_messages("cockpit-session: No matching user for certificate")
+ m.upload(["bob.pem", "bob.key"], "/var/tmp", relative_dir="src/tls/ca/")
+ do_test(['--cert', "/var/tmp/bob.pem", '--key', "/var/tmp/bob.key"],
+ ["HTTP/1.1 401 Authentication failed", '<h1>Authentication failed</h1>'],
+ not_expected=["crsf-token"])
+ self.allow_journal_messages("cockpit-session: Failed to map certificate to user: .* Invalid certificate provided")
+
+ # disallow password auth
+ m.write("/etc/cockpit/cockpit.conf", "[Basic]\naction = none\n", append=True)
+ do_test(alice_cert_key, ['HTTP/1.1 200 OK', '"csrf-token"'])
+ do_test(alice_user_pass, ['HTTP/1.1 401 Authentication disabled', '<h1>Authentication disabled</h1>'],
+ not_expected=["crsf-token"])
+
+ # valid user certificate which fails CA validation
+ m.execute("mv /etc/sssd/pki/sssd_auth_ca_db.pem /etc/sssd/pki/sssd_auth_ca_db.pem.valid")
+ self.allow_journal_messages("cockpit-session: Failed to map certificate to user: .* Certificate authority file not found")
+ with open("src/tls/ca/alice-expired.pem") as f:
+ m.write("/etc/sssd/pki/sssd_auth_ca_db.pem", f.read())
+ api = m.execute("busctl introspect org.freedesktop.sssd.infopipe /org/freedesktop/sssd/infopipe/Users")
+ has_validate_api = 'FindByValidCertificate' in api
+ if has_validate_api:
+ do_test(alice_cert_key, ["HTTP/1.1 401 Authentication failed"])
+ else:
+ # earlier sssd just matches the certificate verbatim, without CA validation
+ do_test(alice_cert_key, ['HTTP/1.1 200 OK', '"csrf-token"'])
+ m.execute("mv /etc/sssd/pki/sssd_auth_ca_db.pem.valid /etc/sssd/pki/sssd_auth_ca_db.pem")
+
+
+@testlib.skipOstree("No realmd available")
+@testlib.skipImage("No realmd available", "arch")
+@testlib.skipDistroPackage()
+@testlib.no_retry_when_changed
+class TestRealms(testlib.MachineCase):
+ """Common variables and tests for all supported domain backends"""
+
+ provision = {
+ "0": {"address": "10.111.113.1/20", "dns": "10.111.112.100", "memory_mb": 700},
+ "services": {"image": "services", "memory_mb": 1500}
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.op_address = "#realms-op-address"
+ self.op_admin = "#realms-op-admin"
+ self.op_admin_password = "#realms-op-admin-password"
+ self.domain_sel = "#system_information_domain_button"
+ self.machine.execute("hostnamectl set-hostname x0.cockpit.lan")
+
+ # realmd times out on inactivity, which occasionally races with the proxy
+ self.allow_journal_messages("couldn't get all properties of org.freedesktop.realmd.Service.*org.freedesktop.DBus.Error.NoReply: Remote peer disconnected")
+
+
+@testlib.skipDistroPackage()
+@testlib.no_retry_when_changed
+class TestIPA(TestRealms, CommonTests):
+ def setUp(self):
+ super().setUp()
+ self.admin_user = "admin"
+ self.admin_password = "foobarfoo"
+ self.alice_password = 'WonderLand123'
+ self.expected_server_software = "ipa"
+ self.machines['services'].execute("/root/run-freeipa")
+ # Wait for FreeIPA to come up and DNS to work as expected
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1071356#c11
+ testlib.wait(lambda: self.machine.execute("nslookup -type=SRV _ldap._tcp.cockpit.lan"))
+
+ maybe_setup_fake_chrony(self.machine)
+
+ # wait until FreeIPA started up
+ self.machines['services'].execute("""podman exec -i freeipa sh -ec '
+ while ! echo %s | kinit -f %s; do sleep 5; done
+ while ! ipa user-find >/dev/null; do sleep 5; done'
+ """ % (self.admin_password, self.admin_user), timeout=300)
+
+ # during image creation the /var/cache directory gets cleaned up, recreate the krb5rcache
+ self.machine.execute("mkdir -pZ /var/cache/krb5rcache")
+
+ # IPA CA cert has OCSP entry with that host name, make it available
+ self.machine.execute("echo '10.111.112.100 ipa-ca.cockpit.lan' >> /etc/hosts")
+
+ # HACK: Figure out why this happens
+ self.allow_journal_messages(""".*didn't receive expected "authorize" message""",
+ 'cockpit-session:$')
+ self.allow_journal_messages('/bin/bash: /home/admin/.bashrc: Permission denied')
+
+ def checkBackendSpecifics(self):
+ """Check domain backend specific integration"""
+
+ m = self.machine
+ b = self.browser
+
+ # should have added SPN to ws keytab
+ output = m.execute(['klist', '-k', '/etc/cockpit/krb5.keytab'])
+ self.assertIn('HTTP/x0.cockpit.lan@COCKPIT.LAN', output)
+
+ # validate Kerberos setup for ws
+ m.execute(f"echo {self.admin_password} | kinit -f {self.admin_user}@COCKPIT.LAN")
+ m.execute(WAIT_KRB_SCRIPT.format(f"{self.admin_user}@cockpit.lan"), timeout=300)
+
+ # kerberos login should work
+ output = m.execute(['curl', '-s', '--negotiate', '--delegation', 'always', '-u', ':', "-D", "-",
+ 'http://x0.cockpit.lan:9090/cockpit/login'])
+ self.assertIn("HTTP/1.1 200 OK", output)
+ self.assertIn('"csrf-token"', output)
+
+ # Restart cockpit with SSL enabled, this should have gotten an SSL cert from FreeIPA
+ m.stop_cockpit()
+ m.start_cockpit(tls=True)
+ # OpenSSL and curl should use the system PKI which should trust the IPA server CA
+ out = m.execute("openssl s_client -verify 5 -verify_return_error -connect localhost:9090")
+ self.assertRegex(out, "subject=/?O *= *COCKPIT.LAN.*CN *= *x0.cockpit.lan", out)
+ self.assertRegex(out, "issuer=/?O *= *COCKPIT.LAN.*CN *= *Certificate Authority")
+ self.assertIn("Content-Type: text/html", m.execute("curl --head https://x0.cockpit.lan:9090"))
+ # don't leave the secret key copy behind
+ m.execute("! test -e /run/cockpit/ipa.key")
+ # cockpit-certificate-ensure agrees
+ self.assertIn("/etc/cockpit/ws-certs.d/10-ipa.cert",
+ m.execute(f"{self.libexecdir}/cockpit-certificate-ensure --check"))
+ # correct permissions
+ self.assertEqual("root:root/640", m.execute("stat --printf '%U:%G/%a' /etc/cockpit/ws-certs.d/10-ipa.key"))
+ # cert is being tracked
+ out = m.execute("ipa-getcert list")
+ self.assertIn("MONITORING", out)
+ # certmonger must be able to directly write and auto-refresh the certificates
+ self.assertIn("/etc/cockpit/ws-certs.d/10-ipa.key", out)
+ # ensure that refreshing works
+ old_cert = m.execute("cat /etc/cockpit/ws-certs.d/10-ipa.cert").strip()
+ m.execute("ipa-getcert rekey --verbose --wait -f /etc/cockpit/ws-certs.d/10-ipa.cert")
+ new_cert = m.execute("cat /etc/cockpit/ws-certs.d/10-ipa.cert").strip()
+ self.assertNotEqual(old_cert, new_cert)
+
+ # Restart without SSL (IPA certificate is not on the testing host)
+ m.stop_cockpit()
+ m.start_cockpit()
+
+ # check respecting FreeIPA's/sssd's ssh known host keys
+ b.login_and_go("/system", user=f'{self.admin_user}@cockpit.lan', password=self.admin_password)
+ b.switch_to_top()
+ b.click("#hosts-sel button")
+ b.click("button:contains('Add new host')")
+ b.wait_popup('hosts_setup_server_dialog')
+ b.set_input_text('#add-machine-address', "x0.cockpit.lan")
+ b.click('#hosts_setup_server_dialog button:contains(Add)')
+
+ b.wait_not_present('#hosts_setup_server_dialog')
+ b.wait_visible("a[href='/@x0.cockpit.lan']")
+ # starts proper session
+ m.execute("until loginctl --no-legend list-sessions | grep -qi 'admin@COCKPIT.LAN.*web console'; do sleep 1; done", timeout=10)
+ b.logout()
+
+ # does not leak any processes or the session itself
+ m.execute("while loginctl --no-legend list-sessions | grep -qi 'admin@COCKPIT.LAN.*web console'; do sleep 1; done", timeout=10)
+
+ # FIXME: Something above triggers this error message
+ self.allow_journal_messages("Received unexpected TLS connection and no certificate was configured")
+
+ def checkBackendSpecificCleanup(self):
+ """Check domain backend specific integration after leaving domain"""
+
+ m = self.machine
+
+ # should have cleaned up ws keytab
+ m.execute("! klist -k /etc/cockpit/krb5.keytab | grep COCKPIT.LAN")
+ # should have cleaned up certificates
+ m.execute("! test -e /etc/cockpit/ws-certs.d/10-ipa.cert")
+ m.execute("! test -e /etc/cockpit/ws-certs.d/10-ipa.key")
+ # should have stopped cert tracking
+ testlib.wait(lambda: "status:" not in m.execute("ipa-getcert list"))
+
+ def testUnqualifiedUsers(self):
+ """Extend the common test with 2FA login"""
+
+ m = self.machine
+ b = self.browser
+
+ super().testUnqualifiedUsers()
+
+ if "debian" in m.image or "ubuntu" in m.image:
+ # additional PAM setup on Debian to actually use pam_sss for non-local users
+ self.sed_file(r"/pam_unix/ s/^/auth [default=1 ignore=ignore success=ok] pam_localuser.so\n/; "
+ r"/pam_sss/ s/use_first_pass/forward_pass/", "/etc/pam.d/common-auth")
+
+ # set up "alice" user with HOTP; that won't affect existing users (admin)
+ # https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/linux_domain_identity_authentication_and_policy_guide/otp
+ out = self.machines['services'].execute("""podman exec -i freeipa sh -ec '
+ ipa config-mod --user-auth-type=otp
+ ipa user-add --first=Alice --last=Developer alice
+ yes alicessecret | ipa user-mod --password alice
+ ipa user-mod --password-expiration="2030-01-01T00:00:00Z" alice
+ ipa otptoken-add --type=hotp --owner=alice
+ ' """)
+ # if the default ever changes, hotp_token() needs to be updated
+ self.assertIn(" Algorithm: sha1\n", out)
+ alice_hotp_key = re.search(r'^ Key: (.*)', out, re.M).group(1)
+ # print("alice's HOTP key:", alice_hotp_key)
+ alice_hotp_key = base64.b64decode(alice_hotp_key)
+
+ # wait until this propagates to the client
+ testlib.wait(lambda: m.execute("su -s /bin/sh -c 'su - alice </dev/null' - nobody 2>&1 | grep 'Second Factor'"))
+
+ m.start_cockpit()
+
+ # normal b.login_and_go() doesn't support 2FA
+ b.open("/")
+ b.wait_visible("#login")
+ b.set_val('#login-user-input', "alice")
+ b.set_val('#login-password-input', "alicessecret")
+ b.click('#login-button')
+ b.wait_in_text("#conversation-prompt", "Second Factor")
+ # wrong token (wrong number of digits)
+ b.set_val("#conversation-input", "1234")
+ b.click('#login-button')
+ b.wait_text("#login-error-message", "Authentication failed")
+
+ b.set_val('#login-user-input', "alice")
+ b.set_val('#login-password-input', "alicessecret")
+ b.click('#login-button')
+ b.wait_in_text("#conversation-prompt", "Second Factor")
+ token = hotp_token(alice_hotp_key, 0) # first usage, counter == 0
+ # print("alice first token:", token)
+ b.set_val("#conversation-input", token)
+ b.click('#login-button')
+ b.wait_visible('#content')
+
+ def testNotSupported(self):
+ m = self.machine
+ b = self.browser
+
+ # Disable sssd support in realmd
+ m.execute("echo -e '[providers]\nsssd = no\n' >> /usr/lib/realmd/realmd-distro.conf")
+
+ self.login_and_go("/system")
+
+ # Join cockpit.lan
+ b.click(self.domain_sel)
+ b.wait_popup("realms-join-dialog")
+ self.wait_discover()
+ b.set_input_text(self.op_address, "cockpit.lan")
+ self.wait_address_helper("Domain is not supported")
+ # no admin name auto-detection for unsupported domains
+ b.wait_val(self.op_admin, "")
+ b.set_input_text(self.op_admin, self.admin_user)
+ b.set_input_text(self.op_admin_password, self.admin_password)
+ # Join button disabled
+ b.wait_visible(f"#realms-join-dialog button{self.primary_btn_class}:disabled")
+
+ self.allow_journal_messages(".*couldn't introspect /org/freedesktop/realmd.*",
+ "sudo: unable to resolve host x0.cockpit.lan: Name or service not known")
+
+ @testlib.timeout(900)
+ def testClientCertAuthentication(self):
+ m = self.machine
+ b = self.browser
+
+ ipa_machine = self.machines['services']
+ # set up an IPA user with a TLS certificate; can't use "admin" due to https://pagure.io/freeipa/issue/6683
+ ipa_machine.execute(f"""podman exec -i freeipa sh -exc '
+ipa user-add --first=Alice --last="Developer" --shell=/bin/bash alice
+yes "{self.alice_password}" | ipa user-mod --password alice
+ipa user-mod --password-expiration=2030-01-01T00:00:00Z alice' """)
+
+ ipa_machine.execute(r"""podman exec -i freeipa sh -exc '
+# generate IPA CA signed certificate for alice
+openssl req -new -newkey rsa:2048 -days 365 -nodes -keyout /tmp/alice.key -out /tmp/alice.csr -subj "/CN=alice"
+ipa cert-request /tmp/alice.csr --principal=alice --certificate-out=/tmp/alice.pem
+# make alice an admin
+ipa group-add-member admins --users=alice
+ipa-advise enable-admins-sudo | sh -ex
+' """)
+ # download certificate to cockpit machine
+ m.write("/var/tmp/alice.pem", ipa_machine.execute("podman exec -i freeipa cat /tmp/alice.pem").strip())
+ m.write("/var/tmp/alice.key", ipa_machine.execute("podman exec -i freeipa cat /tmp/alice.key").strip())
+
+ self.checkClientCertAuthentication()
+
+ # the above password login implicitly creates a persistent user ticket
+ alice_klist_cmd = 'su -c klist alice'
+ persistent_ticket = m.execute(alice_klist_cmd)
+
+ # enable sudo GSSAPI authentication
+ m.execute(r"""#!/bin/sh -eu
+ sed -i '/\[domain\/cockpit.lan\]/ a pam_gssapi_services = sudo, sudo-i' /etc/sssd/sssd.conf
+ sed -i '1 a auth sufficient pam_sss_gss.so' /etc/pam.d/sudo
+ systemctl restart sssd
+ """)
+
+ # enable ssh GSSAPI authentication
+ m.execute("sed -ri 's/#GSSAPIAuthentication.*/GSSAPIAuthentication yes/' /etc/ssh/sshd_config")
+ m.execute("systemctl restart sshd")
+
+ # avoid "unknown host" error in SSH
+ m.execute("su -c 'mkdir -p ~/.ssh; ssh-keyscan localhost > ~/.ssh/known_hosts' alice")
+
+ # the test below assumes exactly one running bridge
+ m.execute("! pgrep cockpit-bridge")
+
+ # check S4U proxy ticket for the user (not functional for anything without delegation rules)
+ # this is specific to IPA, as with AD cockpit-ws does not get a keytab
+
+ # as we can't do cert auth in the browser, splice socat in between to do that for us
+ m.execute(f"""! selinuxenabled || semanage port -m -t websm_port_t -p tcp 443
+ mkdir -p /run/systemd/system/cockpit.socket.d/
+ printf "[Socket]\nListenStream=\nListenStream=443" > /run/systemd/system/cockpit.socket.d/listen.conf
+ sed -i '/\\[WebService/ aOrigins = http://{m.web_address}:{m.web_port}' /etc/cockpit/cockpit.conf""")
+ m.spawn("socat TCP-LISTEN:9090,reuseaddr,fork OPENSSL:x0.cockpit.lan:443,cert=/var/tmp/alice.pem,key=/var/tmp/alice.key", "socat-certauth.log")
+ m.start_cockpit(tls=True)
+
+ # S4U ticket is in the session
+ b.open("/system/terminal")
+ b.enter_page("/system/terminal")
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "alice")
+ b.key_press("klist\r")
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "Ticket cache: FILE:/run/user")
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "Default principal: alice@COCKPIT.LAN")
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "for client HTTP/x0.cockpit.lan@COCKPIT.LAN")
+ self.assertIn("cockpit-session-", m.execute("ls /run/user/$(id -u alice)/*.ccache"))
+ ccache_env = m.execute("xargs -0n1 < /proc/$(pgrep cockpit-bridge)/environ | grep KRB5CCNAME=").strip()
+
+ # does not interfere with persistent ticket in other sessions
+ self.assertEqual(m.execute(alice_klist_cmd), persistent_ticket)
+
+ # destroy global user ccache, so that sudo/ssh really have to use the ccache_env one
+ m.execute("su -c 'kdestroy || true' alice")
+
+ # sanity check: sudo and ssh do not work without a ticket and password
+ self.assertIn("no askpass program", m.execute("su -c '! sudo -A whoami' alice 2>&1"))
+ self.assertIn("Permission denied", m.execute("su -c '! ssh x0.cockpit.lan 2>&1' alice"))
+
+ # in default configuration, ticket is not trusted by IPA
+ self.assertIn("no askpass program", m.execute(f"su -c '! {ccache_env} sudo -A whoami' alice 2>&1"))
+ self.assertIn("Permission denied", m.execute(f"su -c '! {ccache_env} ssh x0.cockpit.lan 2>&1' alice"))
+ b.switch_to_top()
+ b.open_superuser_dialog()
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Switch to administrative access')", "Password for alice:")
+ b.click(".pf-v5-c-modal-box:contains('Switch to administrative access') .btn-cancel")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Switch to administrative access')")
+
+ # set up delegation rule
+ script = """
+ ipa servicedelegationtarget-add cockpit-target
+ ipa servicedelegationtarget-add-member cockpit-target --principals="host/x0.cockpit.lan@COCKPIT.LAN"
+ ipa servicedelegationrule-add cockpit-delegation
+ ipa servicedelegationrule-add-member cockpit-delegation --principals="HTTP/x0.cockpit.lan@COCKPIT.LAN"
+ ipa servicedelegationrule-add-target cockpit-delegation --servicedelegationtargets="cockpit-target"
+ """
+ ipa_machine.execute(f"podman exec freeipa bash -euc '{script}'")
+
+ b.become_superuser(passwordless=True)
+ self.assertEqual("root\n", m.execute(f"su -c '{ccache_env} sudo -A whoami' alice"))
+
+ b.go("/system")
+ b.enter_page("/system")
+ b.wait_visible("#reboot-button")
+
+ # ssh works with the delegated ticket
+ out = m.execute(f"su -c '{ccache_env} ssh -vv -K x0.cockpit.lan echo hello' alice")
+ self.assertEqual(out.strip(), "hello")
+
+ # cockpit-ssh works with the delegated ticket
+ b.switch_to_top()
+ b.click("#hosts-sel button")
+ b.click(".nav-hosts-actions button")
+ b.set_input_text("#add-machine-address", "x0.cockpit.lan")
+ b.click("#hosts_setup_server_dialog .pf-m-primary")
+ b.click(".view-hosts a[href='/@x0.cockpit.lan']")
+ b.wait_js_cond("window.location.pathname == '/@x0.cockpit.lan/system'")
+ # no root privs in that session (see below)
+ b.enter_page("/system", "x0.cockpit.lan")
+ b.wait_visible(".pf-v5-c-alert:contains('Web console is running in limited access mode.')")
+ b.wait_not_present("#reboot-button")
+
+ # Getting root privs through sudo in the remote SSH session does not currently work.
+ # ssh -K is supposed to forward the credentials cache, but doesn't; klist in the ssh session is empty
+ # and there is no ccache; so, emulate what cockpit-ssh could eventually do and check that *if* the
+ # session had the ticket forwarded, it *could* do sudo. See https://issues.redhat.com/browse/COCKPIT-643
+ with b.wait_timeout(60):
+ b.open("/@x0.cockpit.lan/system/terminal")
+ b.enter_page("/system/terminal", host="x0.cockpit.lan")
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "alice")
+ b.key_press(f"{ccache_env} sudo whoami\r")
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "root")
+
+ # S4U proxy ticket gets cleaned up on logout
+ b.logout()
+ m.execute("while ls /run/user/$(id -u alice)/*.ccache; do sleep 1; done")
+ m.execute(f"! su -c '{ccache_env} klist' alice")
+
+
+@testlib.skipImage("adcli not on test images", "debian-*", "ubuntu-*")
+@testlib.skipDistroPackage()
+@testlib.no_retry_when_changed
+class TestAD(TestRealms, CommonTests):
+ def setUp(self):
+ super().setUp()
+ self.admin_user = "Administrator"
+ self.admin_password = "foobarFoo123"
+ self.alice_password = 'WonderLand123'
+ self.expected_server_software = "active-directory"
+ self.machines['services'].execute("/root/run-samba-domain")
+
+ m = self.machine
+
+ # Wait for AD to come up and DNS to work as expected
+ testlib.wait(lambda: m.execute("nslookup -type=SRV _ldap._tcp.cockpit.lan"))
+ # DNS is not sufficient yet, needs to start LDAP server as well
+ m.execute("until nc -z f0.cockpit.lan 389; do sleep 1; done")
+ # also wait for Kerberos
+ m.execute(f"until echo {self.admin_password} | kinit {self.admin_user}@COCKPIT.LAN; do sleep 1; done; kdestroy")
+
+ # allow sudo access to domain admins; FIXME: Is there a server-side setting for this,
+ # similar to "ipa-advise enable-admins-sudo"?
+ m.write("/etc/sudoers.d/domain-admins", r"%domain\ admins@COCKPIT.LAN ALL=(ALL) ALL")
+
+ # HACK: Figure out why this happens
+ self.allow_journal_messages(""".*didn't receive expected "authorize" message""",
+ 'cockpit-session:$')
+ self.allow_journal_messages('/bin/bash: /home/admin/.bashrc: Permission denied')
+
+ def checkBackendSpecifics(self):
+ """Check domain backend specific integration"""
+
+ def checkBackendSpecificCleanup(self):
+ """Check domain backend specific integration after leaving domain"""
+
+ def testUnqualifiedUsers(self):
+ """Extend the test to check a new AD user"""
+
+ super().testUnqualifiedUsers()
+
+ m = self.machine
+ b = self.browser
+
+ m.start_cockpit()
+ # create another AD user
+ self.machines['services'].execute(f"podman exec -i samba samba-tool user add alice {self.alice_password}")
+ # ensure it works
+ m.execute('while ! id alice; do sleep 5; done', timeout=300)
+ b.login_and_go('/system', user='alice', password=self.alice_password)
+ b.wait_visible("#overview")
+ b.logout()
+
+ def testClientCertAuthentication(self):
+ m = self.machine
+
+ services_machine = self.machines['services']
+ # samba has no default CA and no helpers, so just re-use our completely independent cockpit-tls unit test one
+ m.upload(["alice.pem", "alice.key"], "/var/tmp", relative_dir="src/tls/ca/")
+
+ with open("src/tls/ca/alice.pem") as f:
+ alice_cert = f.read().strip()
+ # mangle into form palatable for LDAP
+ alice_cert = ''.join([line for line in alice_cert.splitlines() if not line.startswith("----")])
+ # set up an AD user and import their TLS certificate
+ services_machine.write("/tmp/alice_edit", f'''#!/bin/sh -eu
+sed -i "/^$/d" "$1"
+echo "userCertificate: {alice_cert}" >> "$1"
+''', perm="755")
+ services_machine.execute(f"""
+podman cp /tmp/alice_edit samba:/tmp/
+podman exec -i samba sh -exc '
+samba-tool user add alice {self.alice_password}
+samba-tool user edit --editor=/tmp/alice_edit alice
+# for debugging:
+samba-tool user show alice
+' """, stdout=None)
+
+ # set up sssd for certificate mapping to AD
+ # see sssd.conf(5) "CERTIFICATE MAPPING SECTION" and sss-certmap(5)
+ m.write("/etc/sssd/conf.d/certmap.conf", """
+[certmap/cockpit.lan/certs]
+# our test certificates don't have EKU, and as we match full certificates it is not important to check anything here
+matchrule = <KU>digitalSignature
+# default rule; doesn't work because samba's LDAP doesn't understand ";binary"
+# maprule = LDAP:(userCertificate;binary={cert!bin})
+# match verbatim base64 certificate
+maprule = LDAP:(userCertificate={cert!base64})
+# match cert properties only; this looks at SubjectAlternativeName, which our test certs don't have
+# this also requires CA validation in cockpit-tls or sssd, which we don't have yet
+# maprule = (|(userPrincipalName={subject_principal})(sAMAccountName={subject_principal.short_name}))
+""", perm="0600")
+ # tell sssd about our CA for validating certs
+ with open("src/tls/ca/ca.pem") as f:
+ m.write("/etc/sssd/pki/sssd_auth_ca_db.pem", f.read())
+
+ self.checkClientCertAuthentication()
+
+
+JOIN_SCRIPT = """
+set -ex
+# Wait until zones from LDAP get loaded
+for x in $(seq 1 20); do
+ if nslookup -type=SRV _ldap._tcp.cockpit.lan; then
+ break
+ else
+ sleep $x
+ fi
+done
+
+if ! echo '%(password)s' | realm join -vU admin cockpit.lan; then
+ if systemctl --quiet is-failed sssd.service; then
+ systemctl status --lines=100 sssd.service >&2
+ fi
+ journalctl -u realmd.service
+ exit 1
+fi
+
+# On certain OS's it takes time for sssd to come up properly
+# [8347] 1528294262.886088: Sending initial UDP request to dgram 172.27.0.15:88
+# kinit: Cannot contact any KDC for realm 'COCKPIT.LAN' while getting initial credentials
+for x in $(seq 1 20); do
+ if echo '%(password)s' | KRB5_TRACE=/dev/stderr kinit -f admin@COCKPIT.LAN; then
+ break
+ else
+ sleep $x
+ fi
+done
+
+# create SPN and keytab for ws
+if type ipa >/dev/null 2>&1; then
+ LC_ALL=C.UTF-8 ipa service-add --ok-as-delegate=true --force HTTP/x0.cockpit.lan@COCKPIT.LAN
+else
+ curl --insecure -s --negotiate -u : \\
+ --header 'Referer: https://services.cockpit.lan/ipa' \\
+ --header "Content-Type: application/json" \\
+ --header "Accept: application/json" \\
+ --data '{"params":
+ [
+ ["HTTP/x0.cockpit.lan@COCKPIT.LAN"],
+ {"raw": false, "all": false, "version": "2.101",
+ "force": true, "no_members": false, "ipakrbokasdelegate": true}
+ ], "method": "service_add", "id": 0}' \\
+ https://services.cockpit.lan/ipa/json
+fi
+ipa-getkeytab -p HTTP/x0.cockpit.lan -k %(keytab)s
+
+# HACK: due to sudo's "last rule wins", our /etc/sudoers rule becomes trumped by sssd's, so swap the order
+sed -i '/^sudoers:/ s/files sss/sss files/' /etc/nsswitch.conf
+"""
+
+# This is here because our test framework can't run ipa VM's twice
+
+
+@testlib.skipOstree("No realmd available")
+@testlib.skipImage("No realmd available", "arch")
+@testlib.skipDistroPackage()
+@testlib.no_retry_when_changed
+class TestKerberos(testlib.MachineCase):
+ provision = {
+ "0": {"address": "10.111.113.1/20", "dns": "10.111.112.100", "memory_mb": 512},
+ "services": {"image": "services", "memory_mb": 1500}
+ }
+
+ def setUp(self):
+ super().setUp()
+ maybe_setup_fake_chrony(self.machine)
+
+ def configure_kerberos(self, keytab):
+ self.machines["services"].execute("/root/run-freeipa")
+
+ # Setup a place for kerberos caches
+ args = {"addr": "10.111.112.100", "password": "foobarfoo", "keytab": keytab}
+ self.machine.execute("hostnamectl set-hostname x0.cockpit.lan")
+ if "ubuntu" in self.machine.image:
+ # no nss-myhostname there
+ self.machine.execute("echo '10.111.113.1 x0.cockpit.lan' >> /etc/hosts")
+ self.machine.execute(JOIN_SCRIPT % args, timeout=1800)
+ self.machine.execute(WAIT_KRB_SCRIPT.format("admin"), timeout=300)
+
+ @testlib.skipBrowser("Firefox cannot work with cookies", "firefox")
+ def testNegotiate(self):
+ self.allow_hostkey_messages()
+ b = self.browser
+ m = self.machine
+
+ # Tell realmd to not enable domain-qualified logins
+ # (https://bugzilla.redhat.com/show_bug.cgi?id=1575538)
+ m.write("/etc/realmd.conf", "[cockpit.lan]\nfully-qualified-names = no\n", append=True)
+
+ # delete the local admin user, going to use the IPA one instead
+ m.execute("userdel --remove admin")
+
+ # HACK: There is no operating system where the domain admins can do passwordless sudo
+ # while having a kerberos ticket, so we can't start a root bridge.
+ # This is something that needs to be worked on at an OS level. We use admin level
+ # features below, such as adding a machine to the host switcher
+ m.execute("echo 'admin ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers")
+
+ # Make sure negotiate auth is not offered first
+ m.start_cockpit()
+
+ output = m.execute('/usr/bin/curl -v -s '
+ '--resolve x0.cockpit.lan:9090:10.111.113.1 '
+ 'http://x0.cockpit.lan:9090/cockpit/login 2>&1')
+ self.assertIn("HTTP/1.1 401", output)
+ self.assertNotIn("WWW-Authenticate: Negotiate", output)
+
+ self.configure_kerberos("/etc/cockpit/krb5.keytab")
+ m.restart_cockpit()
+
+ # user has no cockpit kerberos session tickets initially
+ m.execute("! ls /run/user/$(id -u admin)/*.ccache")
+
+ output = m.execute(['/usr/bin/curl', '-s', '--negotiate', '--delegation', 'always', '-u', ':', "-D", "-",
+ '--resolve', 'x0.cockpit.lan:9090:10.111.113.1',
+ 'http://x0.cockpit.lan:9090/cockpit/login'])
+ self.assertIn("HTTP/1.1 200 OK", output)
+ self.assertIn('"csrf-token"', output)
+
+ cookie = re.search("Set-Cookie: cockpit=([^ ;]+)", output).group(1)
+ b.open("/system/terminal", cookie={"name": "cockpit", "value": cookie, "domain": m.web_address, "path": "/"})
+ b.wait_visible('#content')
+
+ # Remove failed units which will show up in the first terminal line
+ m.execute("systemctl reset-failed")
+
+ # kerberos ticket got forwarded into the session
+ b.enter_page("/system/terminal")
+ # wait for prompt
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "admin")
+ b.key_press("klist\r")
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "Ticket cache")
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "Default principal: admin@COCKPIT.LAN")
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "krbtgt/COCKPIT.LAN")
+ self.assertIn("cockpit-session-", m.execute("ls /run/user/$(id -u admin)/*.ccache"))
+
+ # Now connect to another machine
+ self.assertNotIn("admin", m.execute("ps -xa | grep sshd"))
+ b.switch_to_top()
+ b.go("/@x0.cockpit.lan/system/terminal")
+ b.click("#machine-troubleshoot")
+ b.wait_visible('#hosts_setup_server_dialog')
+ b.click('#hosts_setup_server_dialog button:contains(Add)')
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ b.enter_page("/system/terminal", host="x0.cockpit.lan")
+ b.wait_visible(".terminal")
+
+ # Make sure we connected via SSH
+ self.assertIn("admin", m.execute("ps -xa | grep sshd"))
+
+ # forwarded ticket gets cleaned up with the session; this is not completely synchronous
+ b.logout()
+ m.execute("while ls /run/user/$(id -u admin)/*.ccache; do sleep 1; done", timeout=10)
+
+ # Remove cockpit keytab
+ m.execute("mv /etc/cockpit/krb5.keytab /etc/cockpit/bk.keytab")
+ output = m.execute(['/usr/bin/curl', '-s', '--negotiate', '--delegation', 'always', '-u', ':', "-D", "-",
+ '--resolve', 'x0.cockpit.lan:9090:10.111.113.1',
+ 'http://x0.cockpit.lan:9090/cockpit/login'])
+ self.assertIn("HTTP/1.1 401", output)
+
+ # Pull http into default keytab
+ m.execute('printf "rkt /etc/cockpit/bk.keytab\nwkt /etc/krb5.keytab\nq" | ktutil')
+ output = m.execute(['/usr/bin/curl', '-s', '--negotiate', '--delegation', 'always', '-u', ':', "-D", "-",
+ '--resolve', 'x0.cockpit.lan:9090:10.111.113.1',
+ 'http://x0.cockpit.lan:9090/cockpit/login'])
+ self.assertIn("HTTP/1.1 200 OK", output)
+ self.assertIn('"csrf-token"', output)
+
+ m.write("/etc/cockpit/cockpit.conf", "[Negotiate]\naction = none\n", append=True)
+ m.restart_cockpit()
+ output = m.execute(['/usr/bin/curl', '-s', '--negotiate', '--delegation', 'always', '-u', ':', "-D", "-",
+ '--resolve', 'x0.cockpit.lan:9090:10.111.113.1',
+ 'http://x0.cockpit.lan:9090/cockpit/login'])
+ self.assertIn("HTTP/1.1 401 Authentication disabled", output)
+ self.allow_journal_messages(".*Request ticket server HTTP/x0.cockpit.lan@COCKPIT.LAN not found in keytab.*")
+
+
+@testlib.skipImage("No realmd available", "arch")
+@testlib.skipOstree("Package (un)install does not work on OSTree")
+@testlib.skipDistroPackage()
+class TestPackageInstall(packagelib.PackageCase):
+
+ def setUp(self):
+ super().setUp()
+ self.domain_sel = "#system_information_domain_button"
+
+ self.machine.execute("systemctl stop realmd")
+
+ def waitTooltip(self, text):
+ b = self.browser
+
+ # the wait_timeout affects both the waiting in b.mouse() and the b.wait() (times 5!), thus is quadratic
+ with b.wait_timeout(4):
+ def check():
+ try:
+ b.mouse("#system_information_domain_tooltip", "mouseenter")
+ b.wait_in_text("div.pf-v5-c-tooltip", text)
+ b.mouse("#system_information_domain_tooltip", "mouseleave")
+ return True
+ except (RuntimeError, testlib.Error):
+ return False
+ b.wait(check)
+
+ def testInstall(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute("dpkg --purge realmd 2>/dev/null || rpm --erase realmd || dnf remove -y realmd")
+
+ # case 1: disable PackageKit
+ m.execute("systemctl mask packagekit; systemctl stop packagekit.service || true")
+ self.allow_browser_errors('checkRealm failed.*"problem":"not-found"')
+ self.login_and_go("/system")
+ b.wait_text(self.domain_sel, "Join domain")
+ b.wait_visible(self.domain_sel + "[disabled]")
+ self.waitTooltip("realmd is not available on this system")
+ b.logout()
+
+ # case 2: enable PackageKit, but no realmd package available
+ m.execute("systemctl unmask packagekit")
+ self.login_and_go("/system")
+ # Joining a domain should bring up the install dialog
+ b.wait_text(self.domain_sel, "Install realmd support")
+ self.waitTooltip("requires installation of realmd")
+
+ b.click(self.domain_sel)
+ with b.wait_timeout(30):
+ b.wait_in_text(".pf-v5-c-modal-box:contains('Install software')", "realmd is not available")
+ b.wait_visible(".pf-v5-c-modal-box:contains('Install software') .pf-v5-c-modal-box__footer button:contains(Install):disabled")
+ b.click(".pf-v5-c-modal-box:contains('Install software') .pf-v5-c-modal-box__footer button.cancel")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Install software')")
+ b.logout()
+
+ # case 3: provide an available realmd package
+ self.createPackage("realmd", "1", "1", content={"/realmd-stub": ""})
+ self.enableRepo()
+ m.execute("pkcon refresh")
+
+ self.login_and_go("/system")
+
+ # Joining a domain should bring up the install dialog
+ b.wait_text(self.domain_sel, "Install realmd support")
+ self.waitTooltip("requires installation of realmd")
+
+ b.click(self.domain_sel)
+ b.click(".pf-v5-c-modal-box:contains('Install software') .pf-v5-c-modal-box__footer button:contains(Install)")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Install software')")
+
+ # the stub package doesn't provide a realmd D-Bus service, so the "join
+ # domain" dialog won't ever appear; just check that it was installed
+ m.execute("test -e /realmd-stub")
+
+ @testlib.nondestructive
+ def testDialogTransition(self):
+ m = self.machine
+ b = self.browser
+
+ # disable the realmd package's service, so that we can restore it, but
+ # the package install code path will be triggered
+ m.execute("systemctl stop realmd; systemctl mask realmd")
+
+ self.login_and_go("/system")
+
+ # Joining a domain should bring up the install dialog
+ b.wait_text(self.domain_sel, "Install realmd support")
+ self.waitTooltip("requires installation of realmd")
+
+ b.click(self.domain_sel)
+ # restore realmd service, to pretend that package install completed
+ m.execute("systemctl unmask realmd")
+ b.click(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button:contains('Install')")
+ b.wait_not_present(".pf-v5-c-modal-box:contains('Install software')")
+
+ # should continue straight to join dialog
+ b.wait_visible("#realms-join-dialog")
+
+ # no auto-detected domain/admin
+ with b.wait_timeout(60):
+ b.wait_attr("#realms-op-address", "data-discover", "done")
+ self.assertEqual(b.val("#realms-op-address"), "")
+ self.assertEqual(b.val("#realms-op-admin"), "")
+
+ # no running IPA server for this test, so just cancel
+ b.click("#realms-join-dialog button.pf-m-link")
+ b.wait_not_present("#realms-join-dialog")
+
+ # should not have a tooltip any more
+ b.wait_not_present("#system_information_domain_tooltip")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-system-services b/test/verify/check-system-services
new file mode 100755
index 0000000..dc64b28
--- /dev/null
+++ b/test/verify/check-system-services
@@ -0,0 +1,1630 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import time
+
+import testlib
+import testvm
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestServices(testlib.MachineCase):
+
+ def setUp(self):
+ super().setUp()
+
+ # Make sure the system finishes "booting" so that
+ # when we add additional services to this target below
+ # we don't race with the boot process
+ self.machine.execute("while ! systemctl is-active default.target; do sleep 1; done")
+
+ # We manipulate with `/etc/systemd/system` so `daemon-reload` to keep it in proper state
+ # Also `reset-failed` as failed units can be still listed with `systemctl` even when all files
+ # were removed and daemon reloaded.
+ self.restore_dir("/etc/systemd/system", "systemctl daemon-reload; systemctl reset-failed", reboot_safe=True)
+ user_post_restore = "su - admin -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u admin); systemctl --user daemon-reload; systemctl --user reset-failed'"
+ self.restore_dir("/etc/systemd/user", user_post_restore, reboot_safe=True)
+ self.restore_dir("/etc/xdg/systemd/user", user_post_restore, reboot_safe=True)
+ self.restore_dir("/home/admin/.config", user_post_restore, reboot_safe=True)
+
+ def run_systemctl(self, user, cmd):
+ if user:
+ self.machine.execute(f"su - admin -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u admin); systemctl --user {cmd}'")
+ else:
+ self.machine.execute(f"systemctl {cmd}")
+
+ def pick_tab(self, n):
+ self.browser.click(f'#services-filter li:nth-child({n}) a')
+
+ def wait_onoff(self, val):
+ self.browser.wait_visible(".service-top-panel .pf-v5-c-switch__input" + (":checked" if val else ":not(:checked)"))
+
+ def toggle_onoff(self):
+ self.browser.click(".service-top-panel .pf-v5-c-switch__input")
+
+ def do_action(self, action):
+ self.browser.click(".service-top-panel .pf-v5-c-dropdown button")
+ self.browser.click(f".service-top-panel .pf-v5-c-dropdown__menu a:contains('{action}')")
+
+ def check_service_details(self, statuses, actions, enabled, onoff=True, kebab=True):
+ with self.browser.wait_timeout(60):
+ self.browser.wait_collected_text("#statuses .status", "".join(statuses))
+ if onoff:
+ self.wait_onoff(val=enabled)
+ else:
+ self.browser.wait_not_present(".service-top-panel .pf-v5-c-switch")
+
+ if kebab:
+ self.browser.click(".service-top-panel .pf-v5-c-dropdown button")
+ self.browser.wait_text(".service-top-panel .pf-v5-c-dropdown__menu", "".join(actions))
+ # Click away to close the pf-v5-c-dropdown__menu
+ self.browser.click(".service-top-panel .pf-v5-c-dropdown button")
+ else:
+ self.browser.wait_not_present(".service-top-panel .pf-v5-c-dropdown")
+
+ def check_service_on(self, expect_reload=True):
+ self.check_service_details(
+ ["Running", "Automatically starts"],
+ ["Reload" if expect_reload else "", "Restart", "Stop", "Disallow running (mask)", "Pin unit"],
+ enabled=True)
+
+ def check_service_off(self):
+ self.check_service_details(["Disabled"],
+ ["Start", "Disallow running (mask)", "Pin unit"],
+ enabled=False)
+
+ def svc_sel(self, service):
+ return f'tr[data-goto-unit="{service}"]'
+
+ def goto_service(self, service):
+ return self.browser.click(self.svc_sel(service) + ' a')
+
+ def wait_service_state(self, service, state):
+ state_new = state
+ if 'inactive' in state:
+ state_new = 'Not running'
+ elif 'active' in state:
+ state_new = 'Running'
+ elif 'failed' in state:
+ state_new = 'Failed to'
+
+ self.browser.wait_in_text(self.svc_sel(service), state_new)
+
+ def select_file_state(self, states):
+ if not isinstance(states, list):
+ states = [states]
+
+ self.browser.click("#services-dropdown-file-state")
+ for state in states:
+ self.browser.set_checked(f".pf-v5-c-select__menu label:contains('{state}') input", val=True)
+ self.browser.click("#services-dropdown-file-state")
+ for state in states:
+ self.browser.wait_visible(f".pf-v5-c-chip-group__label:contains('File state') + .pf-v5-c-chip-group__list li:contains('{state}')")
+
+ def select_active_state(self, states):
+ if not isinstance(states, list):
+ states = [states]
+
+ self.browser.click("#services-dropdown-active-state")
+ for state in states:
+ self.browser.set_checked(f".pf-v5-c-select__menu label:contains('{state}') input", val=True)
+ self.browser.click("#services-dropdown-active-state")
+ for state in states:
+ self.browser.wait_visible(f".pf-v5-c-chip-group__label:contains('Active state') + .pf-v5-c-chip-group__list li:contains('{state}')")
+
+ def wait_page_load(self):
+ self.browser.wait_not_present(".pf-v5-c-empty-state .pf-v5-c-spinner[aria-valuetext='Loading...']")
+
+ def wait_service_in_panel(self, service, title):
+ self.wait_service_state(service, title)
+
+ def wait_service_present(self, service):
+ service_name = service[:-8] if service.endswith(".service") else service
+ self.browser.wait_text(f'tr[data-goto-unit="{service}"] .service-unit-id', service_name)
+
+ def wait_service_not_present(self, service):
+ self.browser.wait_not_present(self.svc_sel(service))
+
+ def make_test_service(self, path="/etc/systemd/system/"):
+ self.write_file(f"{path}/test.service",
+ """
+[Unit]
+Description=Test Service
+
+[Service]
+ExecStart=/usr/local/bin/test-service
+ExecReload=/bin/true
+
+[Install]
+WantedBy=default.target
+""")
+ self.write_file("/usr/local/bin/test-service",
+ """#!/bin/sh
+set -eu
+trap "echo STOP" 0
+
+if [ $(id -u) -eq 0 ]; then
+ journalctl --sync
+fi
+
+# increase the chance for journal to catch up
+sleep 5
+
+echo START
+while true; do
+ sleep 5
+ echo WORKING
+done
+""", perm="755")
+ # After writing files out tell systemd about them
+ self.run_systemctl(user=False, cmd="daemon-reload")
+ self.run_systemctl(user=True, cmd="daemon-reload || true")
+
+ # When the test fails while `test.service` is active, the process keeps running and
+ # `systemctl status test.service` still shows it as active until this process dies
+ self.addCleanup(
+ self.machine.execute,
+ "for op in stop reset-failed disable; do systemctl $op test test-fail || true; done")
+ self.addCleanup(
+ self.machine.execute,
+ "su - admin -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u admin); "
+ "for op in stop reset-failed disable; do systemctl --user $op test test-fail || true; done'")
+
+ def testBasic(self):
+ self._testBasic()
+
+ def testBasicSession(self):
+ self._testBasic(user=True)
+
+ def _testBasic(self, user=False):
+ m = self.machine
+ b = self.browser
+
+ path = "/etc/systemd/user" if user else "/etc/systemd/system"
+
+ self.write_file(f"{path}/test-fail.service",
+ """
+[Unit]
+Description=Failing Test Service
+
+[Service]
+ExecStart=/usr/bin/false
+
+[Install]
+WantedBy=default.target
+""")
+ self.write_file(f"{path}/special@:-characters.service",
+ """
+[Unit]
+Description=Service With Special Characters in Id
+
+[Service]
+ExecStart=/usr/bin/true
+
+[Install]
+WantedBy=default.target
+""")
+
+ self.make_test_service(path)
+
+ url = "/system/services#/?owner=user" if user else "/system/services"
+ self.login_and_go(url, user="admin")
+ self.wait_page_load()
+
+ self.wait_service_present("test.service")
+ self.wait_service_in_panel("test.service", "Disabled")
+ self.wait_service_state("test.service", "inactive")
+
+ self.run_systemctl(user, "start test.service")
+ self.wait_service_state("test.service", "active")
+ self.wait_service_in_panel("test.service", "Disabled")
+
+ self.run_systemctl(user, "stop test.service")
+ self.wait_service_state("test.service", "inactive")
+ self.wait_service_in_panel("test.service", "Disabled")
+
+ self.run_systemctl(user, "enable test.service")
+ self.wait_service_state("test.service", "inactive")
+ self.wait_service_in_panel("test.service", "Enabled")
+
+ self.run_systemctl(user, "start test.service")
+
+ b.wait_attr("#services-toolbar", "data-loading", "false")
+
+ self.wait_service_present("special@:-characters.service")
+ self.wait_service_in_panel("special@:-characters.service", "Disabled")
+ self.wait_service_state("special@:-characters.service", "inactive")
+
+ # Test breadcrumb link when service id contains special characters
+ self.goto_service("special@:-characters.service")
+ b.wait_in_text(".pf-v5-c-breadcrumb", "special@:-characters.service")
+ b.click(".pf-v5-c-breadcrumb a:contains('Services')")
+ b.wait_visible("#services-list")
+
+ suffix = "?owner=user" if user else ''
+
+ # Check empty state error for nonexistent unit with valid name
+ b.go(f'/system/services#/nonexistent.service{suffix}')
+ b.wait_in_text(".pf-v5-c-empty-state", "Unit nonexistent.service not found")
+ b.click("a:contains('View all services')")
+ b.switch_to_top()
+ b.wait_js_cond('window.location.pathname == "/system/services"')
+ if user:
+ b.wait_js_cond(f"window.location.hash === '#/{suffix}'")
+
+ # Check empty state error for invalid unit name
+ b.go(f'/system/services#/nonexistent{suffix}')
+ b.enter_page("/system/services")
+ b.wait_in_text(".pf-v5-c-empty-state", "Loading unit failed")
+ b.wait_in_text(".pf-v5-c-empty-state", "Unit name nonexistent is not valid")
+
+ b.go(f"#/{suffix}")
+
+ # Survives a burst of events
+ self.wait_service_present("test.service")
+ m.execute("udevadm trigger; udevadm settle")
+ self.goto_service("test.service")
+ self.check_service_on()
+
+ # Stop and disable and back again
+ self.toggle_onoff()
+ self.check_service_off()
+ self.toggle_onoff()
+ self.check_service_on()
+ self.toggle_onoff() # later on we need some disabled test
+ self.check_service_off()
+
+ # Check service that fails to start
+ b.go(f'/system/services#/{suffix}')
+ self.goto_service("test-fail.service")
+ self.check_service_off()
+ self.toggle_onoff()
+ self.check_service_details(["Failed to start", "Automatically starts"],
+ ["Start", "Clear 'Failed to start'", "Disallow running (mask)", "Pin unit"],
+ enabled=True)
+ if not user:
+ b.assert_pixels("#service-details-unit", "details-test-fail",
+ # in medium layout we sometimes get a scrollbar depending on how many test-fail logs exist
+ skip_layouts=["medium"],
+ # ignore the switcher, it causes a tiny flake around the sides.
+ ignore=[".pf-v5-c-switch__toggle"])
+ b.click(".action-button:contains('Start service')")
+ b.go(f'/system/services#/{suffix}')
+ self.wait_service_present("test-fail.service")
+ self.wait_service_state("test-fail.service", "failed")
+
+ # Check static service
+ self.goto_service("systemd-exit.service")
+ self.check_service_details(["Static", "Not running"],
+ ["Start", "Disallow running (mask)", "Pin unit"],
+ enabled=True, onoff=False)
+ # Check that journalbox shows empty state
+ b.wait_text('.cockpit-log-panel .pf-v5-c-card__body', "No log entries")
+ b.wait_not_present("button:contains('View all logs')")
+
+ # Mask and unmask
+ self.do_action("Disallow running (mask)")
+ b.click("#mask-service button.pf-m-danger")
+ self.check_service_details(["Masked"], ["Allow running (unmask)"], enabled=False, onoff=False)
+
+ # Masked services have no relationships and therefore the expandable section should not be present
+ b.wait_not_present("#service-details-show-relationships")
+
+ self.do_action("Allow running (unmask)")
+ self.check_service_details(["Static", "Not running"],
+ ["Start", "Disallow running (mask)", "Pin unit"],
+ enabled=True, onoff=False)
+
+ # Pin unit
+ b.go(f'/system/services#/{suffix}')
+ b.wait_not_present('#test.service > svg.service-thumbtack-icon-color')
+
+ self.goto_service("test.service")
+ self.check_service_details(["Disabled"], ["Start", "Disallow running (mask)", "Pin unit"], enabled=False)
+ b.wait_not_present('#service-details-unit > article > div > svg.service-thumbtack-icon')
+ self.do_action("Pin unit")
+ b.is_present('#service-details-unit > article > div > svg.service-thumbtack-icon')
+ b.go(f'/system/services#/{suffix}')
+
+ # returns index of first unit that isn't failed
+ b.inject_js("""
+ function firstWorkingUnitPos() {
+ const services = document.getElementById('services-list');
+ for (let i = 0; i < services.childElementCount; i++) {
+ const tbody = services.children[i];
+ if (!tbody.firstChild.classList.contains('service-unit-failed'))
+ return i;
+ }
+ }
+ """)
+ pos = b.eval_js('firstWorkingUnitPos();')
+ b.wait_text(f'#services-list > tbody:nth-child({pos + 1}) > tr > th > div > div > a', 'test')
+ b.is_present('#test.service > svg.service-thumbtack-icon-color')
+
+ # Unpin unit
+ self.goto_service("test.service")
+ self.check_service_details(["Disabled"], ["Start", "Disallow running (mask)", "Unpin unit"], enabled=False)
+ self.do_action("Unpin unit")
+ b.wait_not_present('#service-details-unit > article > div > svg.service-thumbtack-icon')
+ self.check_service_off()
+ b.go(f'/system/services#/{suffix}')
+ b.wait_not_present('#test.service > svg.service-thumbtack-icon-color')
+
+ def testFilter(self):
+ b = self.browser
+
+ self.write_file("/run/systemd/system/test-fail.service",
+ """
+[Unit]
+Description=Failing Test Service
+
+[Service]
+ExecStart=/usr/bin/false
+
+[Install]
+WantedBy=default.target
+""")
+ self.make_test_service("/etc/systemd/system")
+ self.machine.execute("systemctl enable --now test-fail.service")
+
+ def init_filter_state():
+ if not b.is_visible("#services-text-filter"):
+ b.click(".pf-v5-c-toolbar__toggle button")
+
+ if b.is_present(".pf-v5-c-toolbar__expandable-content.pf-m-expanded button:contains('Clear all filters')"):
+ b.click(".pf-v5-c-toolbar__expandable-content.pf-m-expanded button:contains('Clear all filters')")
+ elif b.is_present(".pf-v5-c-toolbar__content > .pf-v5-c-toolbar__item > button:contains('Clear all filters')"):
+ b.click(".pf-v5-c-toolbar__content > .pf-v5-c-toolbar__item > button:contains('Clear all filters')")
+ else:
+ b.set_input_text("#services-text-filter input", "")
+
+ self.wait_service_present("test.service")
+ self.wait_service_present("test-fail.service")
+
+ self.login_and_go('/system/services')
+ self.wait_service_present("test-fail.service")
+ self.wait_service_state("test-fail.service", "failed")
+
+ # Filter by id
+ init_filter_state()
+ b.set_input_text("#services-text-filter input", "fail.ser")
+ self.wait_service_not_present("test.service")
+ self.wait_service_present("test-fail.service")
+
+ # Filter by description capital letters included
+ init_filter_state()
+ b.set_input_text("#services-text-filter input", "Test Service")
+ # test.service is not loaded, thus description search does not find it
+ self.wait_service_present("test-fail.service")
+ b.assert_pixels("#services-page", "text-filter-test", skip_layouts=["mobile"])
+ b.set_layout("mobile")
+ # Waiting a bit for the layout to stabilize. The scrolling of
+ # the header happens asynchronously with an animation.
+ time.sleep(2)
+ # Now scroll the header all the way to the left to get a conistent pixel test result
+ nav_scroll_btn = ".services-header button[aria-label='Scroll back']"
+ while b.call_js_func("ph_attr", nav_scroll_btn, "disabled") is None:
+ b.click(nav_scroll_btn)
+ time.sleep(0.5)
+ b.assert_pixels_in_current_layout("#services-page", "text-filter-test")
+ b.set_layout("desktop")
+
+ # Filter by description capitalization not matching the unit description
+ init_filter_state()
+ b.set_input_text("#services-text-filter input", "failing test service")
+ self.wait_service_not_present("test.service")
+ self.wait_service_present("test-fail.service")
+
+ # Filter by Id capitalization not matching the unit Id
+ init_filter_state()
+ b.set_input_text("#services-text-filter input", "networkmanager")
+ self.wait_service_present("NetworkManager.service")
+
+ # Select only static services
+ init_filter_state()
+ self.select_file_state("Static")
+ self.wait_service_not_present("test.service")
+ self.wait_service_not_present("test-fail.service")
+ self.wait_service_present("systemd-exit.service")
+
+ # Select only disabled services
+ init_filter_state()
+ self.select_file_state("Disabled")
+ self.wait_service_present("test.service")
+ self.wait_service_not_present("test-fail.service")
+ self.wait_service_not_present("systemd-exit.service")
+
+ # Select only stopped services
+ init_filter_state()
+ self.select_active_state("Not running")
+ self.wait_service_present("test.service")
+ self.wait_service_not_present("test-fail.service")
+
+ # Select only failed services
+ init_filter_state()
+ self.select_active_state("Failed to start")
+ self.wait_service_not_present("test.service")
+ self.wait_service_present("test-fail.service")
+
+ # Select Alias and Masked services
+ self.machine.execute("systemctl mask test-fail.service")
+ self.wait_service_in_panel("test-fail.service", "Masked")
+ init_filter_state()
+ self.select_file_state(["Indirect", "Masked"])
+ self.wait_service_not_present("test.service")
+ self.wait_service_present("test-fail.service")
+ self.wait_service_present("getty@tty1.service")
+
+ # Check filtering and selecting together - empty state
+ init_filter_state()
+ b.set_input_text("#services-text-filter input", "failing")
+ self.select_active_state("Not running")
+ self.wait_service_not_present("test.service")
+ b.wait_visible("#services-page .pf-v5-c-empty-state")
+
+ # Check resetting filter
+ b.click("#clear-all-filters")
+ self.wait_service_present("test.service")
+ self.wait_service_present("test-fail.service")
+ self.wait_service_present("systemd-exit.service")
+ self.assertEqual(b.val("#services-text-filter input"), "")
+
+ # Check that closing filter chip groups or single chips works
+ init_filter_state()
+ self.select_active_state("Not running")
+ self.select_active_state("Running")
+ self.wait_service_present("test.service")
+ b.click(".pf-v5-c-chip-group__label:contains('Active state') + .pf-v5-c-chip-group__list li:contains('Not running') button")
+ b.wait_not_present(".pf-v5-c-chip-group__label:contains('Active state') + .pf-v5-c-chip-group__list li:contains('Not running')")
+ self.wait_service_not_present("test.service")
+ b.click(".pf-v5-c-chip-group:contains('Active state') .pf-v5-c-chip-group__close > button")
+ b.wait_not_present(".pf-v5-c-chip-group")
+
+ def testTimer(self):
+ self._testTimer(user=False)
+
+ def testTimerSession(self):
+ self._testTimer(user=True)
+
+ def _testTimer(self, user):
+ m = self.machine
+ b = self.browser
+
+ path = "/etc/systemd/user" if user else "/etc/systemd/system"
+ self.write_file(f"{path}/test.timer",
+ """
+[Unit]
+Description=Test Timer
+
+[Timer]
+OnCalendar=*:1/2
+""")
+ self.write_file(f"{path}/test-onboot.timer",
+ """
+[Unit]
+Description=Test OnBoot Timer
+
+[Timer]
+OnBootSec=200min
+Unit=test.service
+""")
+ self.write_file("/usr/local/lib/systemd/system/cockpit-system.timer",
+ """
+[Unit]
+Description=Not deleteable Timer
+
+[Timer]
+OnCalendar=*:1/2
+""")
+ self.write_file(f"{path}/deleteme.service",
+ """
+[Unit]
+Description=Delete Me Service
+
+[Service]
+ExecStart=/usr/bin/true
+
+[Install]
+WantedBy=default.target
+""")
+ self.write_file(f"{path}/deleteme.timer",
+ """
+[Unit]
+Description=Delete Me Timer
+
+[Timer]
+OnCalendar=*:1/2
+
+# just for user mode
+[Install]
+WantedBy=default.target
+""")
+ self.make_test_service(path)
+
+ # ensure we have one running timer; in user mode we do not have
+ # a running logind session at this point yet, so enable instead
+ self.run_systemctl(user=False, cmd=("enable --global --runtime" if user else "start") + " deleteme.timer")
+ self.addCleanup(self.run_systemctl, user, "stop deleteme.timer || true")
+
+ # Select Timer tab
+ self.login_and_go(f'/system/services#/{"?owner=user" if user else ""}')
+ self.pick_tab(4)
+ b.wait_not_in_text("#services-list", "test-fail")
+ b.wait_not_in_text("#services-list", ".target")
+ b.wait_not_in_text("#services-list", ".socket")
+ b.wait_visible(self.svc_sel('test.timer'))
+ b.wait_text(self.svc_sel('test.timer') + ' .service-unit-triggers', '')
+ today = b.eval_js("Intl.DateTimeFormat('en', { dateStyle: 'medium' }).format()")
+ # timer from initial page/units load
+ b.wait_in_text(self.svc_sel('deleteme.timer') + ' .service-unit-next-trigger', today)
+ # timer gets updated on PropertiesChanged event
+ self.run_systemctl(user, "start test.timer")
+ b.wait_in_text(self.svc_sel('test.timer') + ' .service-unit-next-trigger', today) # next run
+ b.wait_in_text(self.svc_sel('test.timer') + ' .service-unit-last-trigger', "unknown") # last trigger
+
+ # Timer details should also show information about next and last trigger
+ self.goto_service("test.timer")
+ b.wait_in_text('.service-unit-next-trigger', today) # next run
+ b.wait_in_text('.service-unit-last-trigger', "unknown") # last trigger
+ b.click(".pf-v5-c-breadcrumb a:contains('Services')")
+
+ self.run_systemctl(user, "stop test.timer")
+
+ b.wait_visible(self.svc_sel('test-onboot.timer'))
+ b.wait_text(self.svc_sel('test-onboot.timer') + ' .service-unit-triggers', '')
+ self.run_systemctl(user, "start test-onboot.timer")
+ # Check the next run. Since it triggers 200mins after the boot, it might be today or tomorrow
+ # this is too racy to predict accurately
+ today = m.execute("date '+%b %-d, %Y'").strip()
+ tomorrow = m.execute("date --date tomorrow '+%b %-d, %Y'").strip()
+ sel_next = self.svc_sel('test-onboot.timer') + ' .service-unit-next-trigger'
+ b.wait_in_text(sel_next, ", ")
+ self.assertRegex(b.text(sel_next), f"{today}|{tomorrow}")
+ b.wait_in_text(self.svc_sel('test-onboot.timer') + ' .service-unit-last-trigger', "unknown") # last trigger
+ self.run_systemctl(user, "stop test-onboot.timer")
+
+ if not user:
+ self.goto_service("deleteme.timer")
+ self.do_action("Delete")
+ b.click("#delete-timer-modal-btn")
+ b.wait_not_present(".pf-v5-c-modal-box")
+ self.wait_page_load()
+ self.assertNotIn("deleteme", m.execute("systemctl list-timers"))
+ m.execute(f"! test -f {path}/deleteme.service")
+ m.execute(f"! test -f {path}/deleteme.timer")
+
+ # system timers are not allowed to be deleted
+ self.goto_service("cockpit-system.timer")
+ b.click(".service-top-panel .pf-v5-c-dropdown button")
+ b.wait_in_text(".service-top-panel .pf-v5-c-dropdown__menu", "Disallow running")
+ b.wait_not_in_text(".service-top-panel .pf-v5-c-dropdown__menu", "Delete")
+ b.click(".pf-v5-c-breadcrumb a:contains('Services')")
+
+ def testServiceMetrics(self):
+ self._testServiceMetrics(user=False)
+
+ def testServiceMetricsSession(self):
+ self._testServiceMetrics(user=True)
+
+ def _testServiceMetrics(self, user):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go(f'/system/services#/{"?owner=user" if user else ""}')
+
+ params = "-user" if user else ""
+ path = "/etc/systemd/user" if user else "/etc/systemd/system"
+
+ self.write_file(f"/tmp/mem-hog{params}.awk", f'BEGIN {{ system("while [ ! -f /tmp/continue{params} ]; do sleep 1; done"); y = sprintf("%50000000s",""); system("sleep infinity") }}')
+ self.write_file(f"{path}/mem-hog.service",
+ f"""
+[Unit]
+Description=Mem Hog Service
+
+[Service]
+ExecStart=/bin/awk -f /tmp/mem-hog{params}.awk
+""")
+
+ self.run_systemctl(user, "daemon-reload")
+
+ # Check that MemoryCurrent is shown
+ self.goto_service("mem-hog.service")
+ b.wait_not_present("#memory")
+ # In some distros systemctl is showing memory current as [Not Set] for user units
+ if not user or (m.image != "centos-8-stream" and not m.image.startswith("rhel-8-")):
+ self.run_systemctl(user, "start mem-hog.service")
+ # If the test fails before we stop mem-hog, the next test run will not get the correct memory usage here
+ self.addCleanup(self.run_systemctl, user, "stop mem-hog.service || true")
+ # initial memory detection takes very long especially on RHEL 8
+ with b.wait_timeout(60):
+ b.wait_visible("#memory")
+ initial_memory = float(b.text("#memory").split(" ")[0])
+ self.assertGreater(initial_memory, 0.5)
+ m.execute(f"touch /tmp/continue{params}")
+ self.addCleanup(m.execute, f"rm /tmp/continue{params}")
+ # MemoryCurrent auto-updates every 30s - when it updates the memory used should be ~50MiB
+ with b.wait_timeout(60):
+ b.wait(lambda: float(b.text("#memory").split(" ")[0]) > 40)
+ self.run_systemctl(user, "stop mem-hog.service")
+ b.wait_not_present("#memory")
+ # Check that listen is not shown for .service units
+ b.wait_not_present("#listen")
+
+ def testOtherTypes(self):
+ self._testOtherTypes(user=False)
+
+ def testOtherTypesSession(self):
+ self._testOtherTypes(user=True)
+
+ def _testOtherTypes(self, user):
+ b = self.browser
+
+ self.login_and_go(f'/system/services#/{"?owner=user" if user else ""}')
+
+ path = "/etc/systemd/user" if user else "/etc/systemd/system"
+ self.make_test_service(path)
+
+ # Targets tab
+ self.pick_tab(2)
+ b.wait_not_in_text("#services-list", "test")
+ self.wait_service_state("basic.target", "active")
+
+ if not user:
+ # Make sure that symlinked services also appear in the list
+ self.wait_service_state("reboot.target", "inactive")
+ self.wait_service_present("ctrl-alt-del.target")
+ self.goto_service("ctrl-alt-del.target")
+ b.wait_in_text(".service-name", "Reboot")
+ b.click("#services-page .pf-v5-c-breadcrumb__link")
+
+ # Sockets tab
+ self.pick_tab(3)
+ b.wait_not_in_text("#services-list", "test")
+ b.wait_not_in_text("#services-list", ".target")
+ self.wait_service_state("dbus.socket", "active (running)")
+
+ # Check that `Listen` and `Triggers` properties are shown for socket units
+ self.goto_service("dbus.socket")
+ if not user:
+ self.assertIn("/run/dbus/system_bus_socket (Stream)", b.text("#listen"))
+ else:
+ self.assertIn("/run/user/", b.text("#listen"))
+ self.assertIn(b.text("#Triggers"), ["dbus.service", "dbus-broker.service"])
+ b.click(".pf-v5-c-breadcrumb a:contains('Services')")
+
+ # Paths tab - the test VM does not have any user path units
+ if not user:
+ self.pick_tab(5)
+ b.wait_not_in_text("#services-list", "test")
+ b.wait_not_in_text("#services-list", ".target")
+ b.wait_not_in_text("#services-list", ".socket")
+ b.wait_not_in_text("#services-list", ".timer")
+ b.wait_visible(self.svc_sel("systemd-ask-password-console.path"))
+
+ def testLogs(self):
+ self._testLogs(user=False)
+
+ def testLogsUser(self):
+ self._testLogs(user=True)
+
+ def _testLogs(self, user=False):
+ b = self.browser
+ service_type = "user-service" if user else "service"
+
+ def append_user(url):
+ if user:
+ return url + ("" if "#/" in url else "#/") + "?owner=user"
+ return url
+
+ self.make_test_service("/etc/systemd/user" if user else "/etc/systemd/system")
+
+ self.login_and_go(append_user("/system/services"))
+ self.wait_page_load()
+
+ # Check test.service and then start it
+ self.goto_service("test.service")
+ self.check_service_off()
+ b.wait_text('.cockpit-log-panel .pf-v5-c-card__body', "No log entries")
+ self.toggle_onoff()
+ self.check_service_on()
+ b.wait_in_text('.cockpit-log-panel .pf-v5-c-card__body', "START")
+ b.wait_in_text('.cockpit-log-panel .pf-v5-c-card__body', "WORKING")
+ b.click(".cockpit-log-panel .pf-v5-c-card__header button")
+
+ b.enter_page("/system/logs")
+ b.wait_in_text("#journal-box .cockpit-log-panel > div:nth-child(2)", "WORKING")
+ t = b.text("#journal-box .cockpit-log-panel > div:nth-child(3)") + b.text("#journal-box .cockpit-log-panel > div:nth-child(4)")
+ self.assertIn("START", t)
+ b.wait_val("#journal-grep input", f"priority:debug {service_type}:test.service ")
+ b.go(append_user("/system/services#/test.service"))
+ b.enter_page("/system/services")
+
+ b.wait_in_text(".cockpit-logline:nth-child(2)", "WORKING")
+ b.click(".cockpit-logline:nth-child(2)")
+ b.enter_page("/system/logs")
+ b.wait_text(".pf-v5-c-card__title", "WORKING")
+ b.click(".pf-v5-c-breadcrumb li:contains('Logs')")
+ b.wait_val("#journal-grep input", f"priority:debug {service_type}:test.service ")
+ b.go(append_user("/system/services#/test.service"))
+ b.enter_page("/system/services")
+
+ b.wait_in_text(".cockpit-logline:nth-child(2)", "WORKING")
+ b.click(".cockpit-logline:nth-child(2)")
+ b.enter_page("/system/logs")
+ b.click("button:contains('Go to test.service')")
+ b.enter_page("/system/services")
+ b.wait_js_cond('window.location.hash === "' + append_user("#/test.service") + '"')
+
+ def testApi(self):
+ b = self.browser
+
+ self.make_test_service()
+
+ self.login_and_go("/playground/service#/test")
+
+ b.wait_text('#exists', 'true')
+ b.wait_text('#state', '"stopped"')
+ b.wait_text('#enabled', 'false')
+
+ b.click('#start')
+ b.wait_text('#state', '"running"')
+ b.click('#stop')
+ b.wait_text('#state', '"stopped"')
+
+ b.click('#enable')
+ b.wait_text('#enabled', 'true')
+ b.click('#disable')
+ b.wait_text('#enabled', 'false')
+
+ b.go('#/foo')
+ b.wait_text('#exists', 'false')
+
+ def testConditions(self):
+ m = self.machine
+ b = self.browser
+ self.write_file("/etc/systemd/system/condtest.service",
+ """
+[Unit]
+Description=Test Service
+ConditionDirectoryNotEmpty=/var/tmp/empty
+
+[Service]
+ExecStart=/bin/sh -c "while true; do sleep 5; done"
+
+[Install]
+WantedBy=multi-user.target
+""")
+
+ m.execute("mkdir -p /var/tmp/empty")
+ m.execute("rm -rf /var/tmp/empty/*")
+ self.addCleanup(m.execute, "systemctl stop condtest")
+
+ # After writing files out tell systemd about them
+ m.execute("systemctl daemon-reload")
+ self.machine.execute("systemctl start multi-user.target")
+
+ # This does not work for not enabled services. See:
+ # https://github.com/systemd/systemd/issues/2234
+ self.machine.execute("systemctl enable condtest")
+
+ self.login_and_go("/system/services")
+
+ # Selects Services tab
+ self.pick_tab(1)
+
+ self.wait_service_in_panel("condtest.service", "Enabled")
+ self.goto_service("condtest.service")
+ self.check_service_details(["Not running", "Automatically starts"],
+ ["Start", "Disallow running (mask)", "Pin unit"],
+ enabled=True)
+ self.do_action("Start")
+ b.wait_text("#condition", "Condition ConditionDirectoryNotEmpty=/var/tmp/empty was not met")
+
+ # If the condition succeeds the message disappears
+ m.execute("touch /var/tmp/empty/non-empty")
+ self.addCleanup(m.execute, "rm /var/tmp/empty/non-empty")
+ self.do_action("Start")
+ self.check_service_on(expect_reload=False)
+ b.wait_not_present("#condition")
+
+ def testRelationships(self):
+ self._testRelationships()
+
+ def testRelationshipsUser(self):
+ self._testRelationships(user=True)
+
+ def _testRelationships(self, user=False):
+ b = self.browser
+
+ systemd_path = "/etc/systemd/user" if user else "/etc/systemd/system"
+
+ self.write_file(f"{systemd_path}/test-a.service",
+ """
+[Unit]
+Description=A Service
+Before=test-b.service
+Conflicts=test-c.service
+
+[Service]
+ExecStart=/bin/sh -c "while true; do sleep 5; done"
+""")
+
+ self.write_file(f"{systemd_path}/test-b.service",
+ """
+[Unit]
+Description=B Service
+After=test-a.service
+
+[Service]
+ExecStart=/bin/sh -c "while true; do sleep 5; done"
+""")
+
+ self.write_file(f"{systemd_path}/test-c.service",
+ """
+[Unit]
+Description=C Service
+Conflicts=test-a.service
+PartOf=test-b.service
+
+[Service]
+ExecStart=/bin/sh -c "while true; do sleep 5; done"
+""")
+
+ # After writing files out tell systemd about them
+ self.run_systemctl(user, "daemon-reload || true")
+
+ def rel_sel(reltype, service):
+ return f"#{''.join(reltype.split(' '))} a:contains('{service}')"
+
+ # services page
+ url = "/system/services#/?owner=user" if user else "/system/services"
+ self.login_and_go(url, user="admin")
+ self.wait_page_load()
+
+ self.wait_service_present("test-a.service")
+ self.goto_service("test-a.service")
+
+ usr_query_str = "?owner=user" if user else ""
+
+ # service a
+ b.wait_js_cond(f'window.location.hash === "#/test-a.service{usr_query_str}"')
+ b.click("#service-details-show-relationships button")
+ b.wait_visible(rel_sel("Before", "test-b.service"))
+ b.wait_visible(rel_sel("Conflicts", "test-c.service"))
+ b.click(rel_sel("Before", "test-b.service"))
+
+ # service b
+ b.wait_js_cond(f'window.location.hash === "#/test-b.service{usr_query_str}"')
+ b.click("#service-details-show-relationships button")
+ b.wait_visible(rel_sel("After", "test-a.service"))
+ b.click(rel_sel("After", "test-a.service"))
+
+ # service a
+ b.wait_js_cond(f'window.location.hash === "#/test-a.service{usr_query_str}"')
+ b.click("#service-details-show-relationships button")
+ b.wait_visible(rel_sel("Conflicts", "test-c.service"))
+ b.click(rel_sel("Conflicts", "test-c.service"))
+
+ # service c
+ b.wait_js_cond(f'window.location.hash === "#/test-c.service{usr_query_str}"')
+ b.click("#service-details-show-relationships button")
+ b.wait_visible(rel_sel("Conflicts", "test-a.service"))
+ b.wait_visible(rel_sel("Part of", "test-b.service"))
+ b.click(rel_sel("Part of", "test-b.service"))
+
+ # service b
+ b.wait_js_cond(f'window.location.hash === "#/test-b.service{usr_query_str}"')
+ b.click("#service-details-show-relationships button")
+ b.wait_visible(rel_sel("After", "test-a.service"))
+
+ def testNotFound(self):
+ m = self.machine
+ b = self.browser
+
+ self.write_file("/etc/systemd/system/test.service",
+ """
+[Unit]
+Description=Test Service
+Requires=not-found.service
+
+[Service]
+ExecStart=/bin/true
+""")
+ m.execute("systemctl daemon-reload")
+ self.machine.execute("systemctl start default.target")
+
+ self.login_and_go("/system/services")
+ self.wait_page_load()
+
+ b.wait_visible(self.svc_sel("test.service"))
+ b.wait_not_present(self.svc_sel("not-found.service"))
+
+ self.goto_service("test.service")
+ b.wait_js_cond('window.location.hash === "#/test.service"')
+
+ b.click("#service-details-show-relationships button")
+ b.wait_visible("#Requires a.pf-m-disabled:contains('not-found.service')")
+
+ def testResetFailed(self):
+ m = self.machine
+ b = self.browser
+
+ # Put it in /run instead of in /etc so that we can mask it,
+ # which needs to put a symlink into /etc.
+ self.write_file("/run/systemd/system/test-fail.service",
+ """
+[Unit]
+Description=Failing Test Service
+
+[Service]
+ExecStart=/usr/bin/false
+
+[Install]
+WantedBy=default.target
+""")
+
+ m.execute("systemctl daemon-reload")
+ self.machine.execute("systemctl start default.target")
+
+ self.login_and_go("/system/services")
+ self.wait_page_load()
+
+ self.goto_service("test-fail.service")
+ b.wait_js_cond('window.location.hash === "#/test-fail.service"')
+
+ self.check_service_off()
+
+ self.wait_onoff(val=False)
+ self.toggle_onoff()
+
+ self.check_service_details(["Failed to start", "Automatically starts"],
+ ["Start", "Clear 'Failed to start'", "Disallow running (mask)", "Pin unit"], enabled=True)
+
+ # Disabling should reset the "Failed to start" status
+ self.wait_onoff(val=True)
+ self.toggle_onoff()
+ self.check_service_off()
+
+ self.wait_onoff(val=False)
+ self.toggle_onoff()
+
+ self.check_service_details(["Failed to start", "Automatically starts"],
+ ["Start", "Clear 'Failed to start'", "Disallow running (mask)", "Pin unit"], enabled=True)
+
+ # Just reset the failed status, but leave it enabled
+ self.do_action("Clear")
+
+ self.check_service_details(["Not running", "Automatically starts"],
+ ["Start", "Disallow running (mask)", "Pin unit"], enabled=True)
+
+ self.do_action("Start")
+ self.check_service_details(["Failed to start", "Automatically starts"],
+ ["Start", "Clear 'Failed to start'", "Disallow running (mask)", "Pin unit"], enabled=True)
+
+ # Masking should also clear the failed status
+ self.do_action("Disallow running (mask)")
+ b.click("#mask-service button.pf-m-danger")
+ self.check_service_details(["Masked"], ["Allow running (unmask)"], enabled=False, onoff=False)
+
+ def count_failures(self):
+ return int(self.machine.execute("systemctl --failed --plain --no-legend | wc -l"))
+
+ def check_system_menu_services_error(self, expected, pixel_label=None):
+ b = self.browser
+ b.switch_to_top()
+ if expected:
+ b.wait_visible("#services-error")
+ else:
+ b.wait_not_present("#services-error")
+
+ if pixel_label:
+ b.assert_pixels("#nav-system", pixel_label, skip_layouts=["mobile"])
+ b.set_layout("mobile")
+ b.click("#nav-system-item")
+ b.assert_pixels_in_current_layout("#nav-system", pixel_label)
+ b.click("#nav-system-item")
+ b.set_layout("desktop")
+
+ def testNotifyFailed(self):
+ m = self.machine
+ b = self.browser
+
+ # We use two services with prefix "aaa" and "aab" so that
+ # they always occupy the first two rows and we can check their
+ # relative order depending on whether "aab" has failed or not.
+
+ self.write_file("/etc/systemd/system/aaa.service",
+ """
+[Unit]
+Description=First Row Service
+
+[Service]
+ExecStart=/usr/bin/true
+""")
+ self.write_file("/etc/systemd/system/aab-fail.service",
+ """
+[Unit]
+Description=Failing Test Service
+
+[Service]
+ExecStart=/usr/bin/false
+""")
+
+ m.execute("systemctl daemon-reload")
+ self.machine.execute("systemctl start default.target")
+ self.machine.execute("systemctl reset-failed")
+
+ self.login_and_go("/system/services")
+ self.wait_page_load()
+
+ # Start test-fail
+ m.execute("systemctl start aab-fail")
+
+ # Services tab should have icon
+ # Aab-fail should be at the top, and have failed
+ b.wait_visible('a:contains("Services") .ct-exclamation-circle')
+ b.wait_visible('tr[data-goto-unit="aab-fail.service"]')
+ b.wait_visible('tr[data-goto-unit="aab-fail.service"] .service-unit-status:contains("Failed")')
+
+ # Nav link should have icon
+ self.check_system_menu_services_error(expected=True, pixel_label="menu_error")
+
+ n_failures = self.count_failures()
+ health_message = "1 service has failed" if n_failures == 1 else f"{n_failures} services have failed"
+
+ # System page should have notification
+ b.click_system_menu("/system")
+ b.wait_in_text(".system-health-events", health_message)
+
+ # Clear aab-fail
+ m.execute("systemctl reset-failed aab-fail")
+
+ # aab-fail should not be at the top
+ b.switch_to_top()
+ b.click_system_menu("/system/services")
+ b.wait_not_present('li:nth-child(1)[data-goto-unit="aab-fail.service"]')
+
+ if self.count_failures() == 0:
+ # Services tab should not have icon
+ b.wait_not_present('a:contains("Services") .ct-exclamation-circle')
+
+ # Nav link should not have icon
+ self.check_system_menu_services_error(expected=False)
+
+ # System page should not have notification
+ b.click_system_menu('/system')
+ b.wait_not_in_text(".system-health-events", "service has failed")
+ b.wait_not_in_text(".system-health-events", "services have failed")
+
+ def testHiddenFailure(self):
+ m = self.machine
+ b = self.browser
+
+ self.write_file("/etc/systemd/system/fail.mount",
+ """
+[Unit]
+Description=Failing Mount
+
+[Mount]
+What=wrong
+Where=/fail
+""")
+
+ m.execute("systemctl daemon-reload")
+ self.machine.execute("systemctl start default.target")
+ self.machine.execute("systemctl reset-failed")
+ m.execute("systemctl start fail.mount || true")
+
+ self.login_and_go("/system/services")
+ self.wait_page_load()
+
+ if self.count_failures() == 1:
+ # Nav link should not have icon
+ self.check_system_menu_services_error(expected=False)
+
+ # System page should not have notification
+ b.click_system_menu("/system")
+ b.wait_not_in_text(".system-health-events", "service has failed")
+ b.wait_not_in_text(".system-health-events", "services have failed")
+
+ self.allow_journal_messages(".*type=1400 audit(.*): avc: denied { create } .* comm=\"systemd\" name=\"fail\".*")
+
+ def testTransientUnits(self):
+ m = self.machine
+ b = self.browser
+
+ self.login_and_go("/system/services")
+ self.wait_page_load()
+
+ m.execute("systemd-run --collect --unit test-autocollect@1.service sh -c 'sleep 5; false'")
+ m.execute("systemd-run --unit test-manual-collect@1.service sh -c 'sleep 5; false'")
+
+ self.wait_service_present("test-autocollect@1.service")
+ self.wait_service_present("test-manual-collect@1.service")
+
+ self.wait_service_in_panel("test-manual-collect@1.service", "Failed to start")
+
+ self.wait_service_not_present("test-autocollect@1.service")
+ self.wait_service_present("test-manual-collect@1.service")
+
+ m.execute("systemctl reset-failed")
+ self.wait_service_not_present("test-manual-collect@1.service")
+
+ # details page handles units going away
+ m.execute("systemd-run --collect --unit test-transient.service sleep infinity")
+ self.addCleanup(m.execute, "systemctl stop test-transient.service || true")
+ self.wait_service_present("test-transient.service")
+ self.goto_service("test-transient.service")
+ self.check_service_details(["Static", "Running"], ["Restart", "Stop", "Disallow running (mask)", "Pin unit"], enabled=False, onoff=False)
+
+ self.do_action("Stop")
+
+ b.wait_in_text(".pf-v5-c-empty-state", "Unit test-transient.service not found")
+ b.click("a:contains('View all services')")
+ b.switch_to_top()
+ b.wait_js_cond('window.location.pathname == "/system/services"')
+
+ def testServicesThemeConsistency(self):
+ b = self.browser
+
+ self.login_and_go("/system/services")
+ self.wait_page_load()
+
+ b.wait_not_present("html.pf-v5-theme-dark")
+ b.switch_to_top()
+ b.open_superuser_dialog()
+ b.click(".pf-v5-c-modal-box:contains('Switch to limited access') button:contains('Limit access')")
+ b.check_superuser_indicator("Limited access")
+ b.enter_page("/system/services")
+ b.wait_not_present("html.pf-v5-theme-dark")
+
+ def testServicesFiltersURLConsistency(self):
+ b = self.browser
+
+ # this filter matches nothing
+ self.login_and_go("/system/services#/?activestate=[\"Running\"]&filestate=[\"Disabled\"%2C\"Enabled\"]&name=test&type=service&owner=user")
+ self.wait_page_load()
+
+ b.wait_visible(".pf-v5-c-chip-group:contains(Active state) li:contains(Running)")
+ b.wait_visible(".pf-v5-c-chip-group:contains(File state) li:contains(Disabled)")
+ b.wait_visible(".pf-v5-c-chip-group:contains(File state) li:contains(Enabled)")
+ b.wait_visible(".services-text-filter input[value=test]")
+ b.wait_visible(".pf-v5-c-nav__list a.pf-m-current:contains(Services)")
+ b.wait_visible("#user.pf-m-selected")
+ # there is no running "test" service
+ self.browser.wait_in_text(".pf-v5-c-empty-state", "No matching results")
+ # filter survives a reload, same URL
+ b.reload()
+ b.enter_page("/system/services")
+ self.browser.wait_in_text(".pf-v5-c-empty-state", "No matching results")
+
+ # Changing the UI filters should also affect the URL
+ b.click("#system")
+ url = b.eval_js("window.location.hash")
+ self.assertIn("owner=system", url)
+
+ # Mass remove active state filters and check the URL
+ b.click(".pf-v5-c-chip-group:contains(Active state) .pf-v5-c-chip-group__close button")
+ b.wait_js_cond('window.location.hash == "#/?filestate=%5B%22Disabled%22%2C%22Enabled%22%5D&name=test&type=service&owner=system"')
+
+ b.click(".pf-v5-c-toolbar__content > .pf-v5-c-toolbar__item > button:contains('Clear all filters')")
+ b.wait_js_cond('window.location.hash == "#/?type=service&owner=system"')
+
+ # Test text input clear button
+ b.go('/system/services#/?name=test')
+ b.wait_visible(".services-text-filter input[value=test]")
+ b.click(".services-text-filter button")
+ b.wait_js_cond('window.location.hash == "#/"')
+
+ # ensure that there is at least one running service
+ self.make_test_service("/etc/systemd/user")
+ self.run_systemctl(user=True, cmd="enable --now test.service")
+ b.click("#user")
+ self.wait_service_state("test.service", "active")
+ self.wait_service_state("test.service", "Enabled")
+
+ # drop the name search; all our images have at least stopped and static unit, which should not appear
+ b.go("/system/services#/?activestate=[\"Running\"]&filestate=[\"Disabled\"%2C\"Enabled\"]&type=service&owner=user")
+ self.wait_page_load()
+ b.wait_in_text(".services-list", "Running")
+ # filters out non-matching units
+ self.assertNotIn("Not running", b.text(".services-list"))
+ self.assertNotIn("Static", b.text(".services-list"))
+ # survives a page reload
+ b.reload()
+ b.enter_page("/system/services")
+ b.wait_in_text(".services-list", "Running")
+ self.assertNotIn("Not running", b.text(".services-list"))
+ self.assertNotIn("Static", b.text(".services-list"))
+
+ def testAlias(self):
+ m = self.machine
+ b = self.browser
+ self.write_file("/etc/systemd/system/flower-rose.service",
+ """
+[Unit]
+Description=Smell sweet
+
+[Service]
+ExecStart=/bin/echo Perfume
+RemainAfterExit=yes
+
+[Install]
+Alias=flower-byanyothername.service
+WantedBy=multi-user.target
+""")
+ m.execute("systemctl enable flower-rose.service")
+ self.addCleanup(m.execute, "systemctl disable --now flower-rose.service")
+
+ self.login_and_go("/system/services#/?name=flower")
+
+ # overview: primary unit
+ self.wait_service_present("flower-rose.service")
+ self.wait_service_in_panel("flower-rose.service", "Enabled")
+ self.wait_service_state("flower-rose.service", "inactive")
+
+ # overview: alias
+ self.wait_service_present("flower-byanyothername.service")
+ if m.image.startswith("rhel-8") or m.image.startswith("centos-8"):
+ # old systemd does not have unit file state "alias" yet
+ self.wait_service_in_panel("flower-byanyothername.service", "Enabled")
+ else:
+ self.wait_service_in_panel("flower-byanyothername.service", "Alias")
+ # runtime status of aliases is unknown in list view
+ self.wait_service_state("flower-byanyothername.service", "")
+
+ # both react to state changes
+ m.execute("systemctl start flower-byanyothername.service")
+ self.wait_service_state("flower-rose.service", "active")
+ self.wait_service_state("flower-byanyothername.service", "")
+
+ # details: primary unit
+ self.goto_service("flower-rose.service")
+ b.wait_in_text("#statuses", "Running")
+ b.wait_in_text("#statuses", "Automatically starts")
+ b.wait_in_text("#service-details-unit", "/etc/systemd/system/flower-rose.service")
+ b.wait_in_text('.cockpit-log-panel .pf-v5-c-card__body', "Perfume")
+
+ # details: alias
+ b.click(".pf-v5-c-breadcrumb a:contains('Services')")
+ self.goto_service("flower-byanyothername.service")
+ b.wait_in_text("#statuses", "Running")
+ b.wait_in_text("#statuses", "Automatically starts")
+ b.wait_in_text("#service-details-unit", "/etc/systemd/system/flower-rose.service")
+ b.wait_in_text('.cockpit-log-panel .pf-v5-c-card__body', "Perfume")
+
+ def testUnprivileged(self):
+ b = self.browser
+ m = self.machine
+
+ self.make_test_service()
+
+ self.login_and_go("/system/services", superuser=False)
+ self.wait_page_load()
+
+ # service list
+ self.wait_service_in_panel("NetworkManager.service", "Enabled")
+ self.wait_service_state("NetworkManager.service", "active")
+ self.wait_service_in_panel("test.service", "Disabled")
+ self.wait_service_state("test.service", "inactive")
+ # reacts to changes
+ m.execute("systemctl start test.service")
+ self.wait_service_state("test.service", "active")
+
+ # details page
+ self.goto_service("test.service")
+ self.check_service_details(["Read-only", "Running"], [], enabled=False, onoff=False, kebab=False)
+ b.wait_in_text("#service-details-unit", "/etc/systemd/system/test.service")
+ # unprivileged users cannot read the system journal on all OSes; only check it on Fedora, where we know it's allowed
+ if m.image.startswith("fedora"):
+ b.wait_in_text('.cockpit-log-panel .pf-v5-c-card__body', "WORKING")
+ # reacts to changes
+ m.execute("systemctl stop test.service")
+ self.check_service_details(["Read-only", "Disabled"], [], enabled=False, onoff=False, kebab=False)
+
+
+@testlib.skipDistroPackage()
+class TestTimers(testlib.MachineCase):
+ def svc_sel(self, service):
+ return f'tr[data-goto-unit="{service}"]'
+
+ def wait_page_load(self):
+ self.browser.wait_not_present(".pf-v5-c-empty-state .pf-v5-c-spinner[aria-valuetext='Loading...']")
+
+ def testCreate(self):
+ m = self.machine
+ b = self.browser
+
+ def wait_systemctl_timer(time):
+ with testvm.Timeout(seconds=20, machine=m, error_message="Timeout while waiting for systemctl list-timers"):
+ m.execute(f"cmd='systemctl list-timers'; until $cmd | grep -m 1 '{time}'; do sleep 1; done")
+
+ # HACK: pmie and pmlogger take a looooong time to shutdown (https://bugzilla.redhat.com/show_bug.cgi?id=1703348)
+ # so disable them for this test, we don't test PCP here
+ m.execute("systemctl disable --now pmie pmlogger || true")
+
+ # set an initial baseline date/time, to ensure that we never jump backwards in subsequent tests
+ m.execute("timedatectl set-timezone UTC")
+ m.execute("""ntp=`timedatectl show --property NTP --value`
+ if [ $ntp == "yes" ]; then
+ timedatectl set-ntp off
+ fi""")
+ testlib.wait(lambda: "false" in m.execute(
+ "busctl get-property org.freedesktop.timedate1 /org/freedesktop/timedate1 org.freedesktop.timedate1 NTP"))
+ m.execute("timedatectl set-time '2036-01-01 12:00:00'")
+ m.reboot()
+
+ m.execute("timedatectl set-time '2036-01-01 15:30:00'")
+ self.login_and_go("/system/services")
+ self.wait_page_load()
+ # Select "Timers" tab
+ self.browser.click('#services-filter li:nth-child(4) a')
+ b.click('#create-timer')
+ b.wait_visible("#timer-dialog")
+ b.set_input_text("#servicename", "testing timer")
+ m.execute("rm -f /tmp/date")
+ b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
+ b.click("input[value=specific-time]")
+ b.set_input_text(".create-timer-time-picker input", "24:6s")
+ b.click("#timer-save-button")
+
+ # checks for invalid input messages
+ b.wait_text("#servicename-helper", "Only alphabets, numbers, : , _ , . , @ , - are allowed")
+ b.wait_text("#description-helper", "This field cannot be empty")
+ b.wait_text(".pf-v5-c-date-picker__helper-text", "Invalid time format")
+
+ # checks for command not found
+ b.set_input_text("#servicename", "testing")
+ b.set_input_text("#description", "desc")
+ b.set_input_text("#command", "this is not found")
+ b.set_input_text(".create-timer-time-picker input", "14:12")
+ b.click("#timer-save-button")
+
+ b.wait_text("#command-helper", "Command not found")
+
+ # creates a new yearly timer at 2036-01-01 16:00 and at 2037-01-01 01:22
+ b.set_input_text("#servicename", "yearly_timer")
+ b.set_input_text("#description", "Yearly timer")
+ b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
+ b.select_from_dropdown("#drop-repeat", "yearly")
+ b.click("[data-index='0'] [aria-label='Add']")
+ b.set_input_text("[data-index='0'] .pf-v5-c-date-picker:nth-child(1) input", "2036-01-01")
+ b.set_input_text("[data-index='0'] .pf-v5-c-date-picker:nth-child(2) input", "16:00")
+ b.set_input_text("[data-index='1'] .pf-v5-c-date-picker:nth-child(1) input", "2037-01-01")
+ b.set_input_text("[data-index='1'] .pf-v5-c-date-picker:nth-child(2) input", "01:22")
+
+ # shows creation errors
+ m.execute("chattr +i /etc/systemd/system")
+ try:
+ b.click("#timer-save-button")
+ b.wait_in_text("#timer-dialog .pf-v5-c-alert", "Timer creation failed")
+ b.wait_in_text("#timer-dialog .pf-v5-c-alert", "Not permitted")
+ finally:
+ m.execute("chattr -i /etc/systemd/system")
+
+ b.click("#timer-save-button")
+ b.wait_not_present("#timer-dialog")
+ b.wait_visible(self.svc_sel('yearly_timer.timer'))
+
+ m.execute("timedatectl set-time '2036-01-01 15:30:00'")
+ b.wait_visible(self.svc_sel('yearly_timer.timer'))
+ b.wait_in_text(self.svc_sel('yearly_timer.timer'), "Yearly timer")
+ wait_systemctl_timer("2036-01-01 16:00")
+ self.assertIn("2036-01-01 16:00", m.execute("systemctl list-timers"))
+ m.execute("timedatectl set-time '2036-01-01 16:10:00'")
+ # checks if yearly timer repeats yearly on 2037-01-01 01:22
+ wait_systemctl_timer("2037-01-01 01:22")
+ self.assertIn("2037-01-01 01:22", m.execute("systemctl list-timers"))
+ # creates a new monthly timer that runs on 6th at 14:12 and 8th at 21:12 of every month
+ b.wait_visible("#create-timer")
+ b.click('#create-timer')
+ b.wait_visible("#timer-dialog")
+ b.set_input_text("#servicename", "monthly_timer")
+ b.set_input_text("#description", "Monthly timer")
+ b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
+ b.click("input[value=specific-time]")
+ b.select_from_dropdown("#drop-repeat", "monthly")
+ b.select_from_dropdown("[data-index='0'] .month-days select", "6")
+ b.set_input_text("[data-index='0'] .create-timer-time-picker input", "14:12")
+ b.click("[data-index='0'] [aria-label='Add']")
+ b.select_from_dropdown("[data-index='1'] .month-days select", "8")
+ b.set_input_text("[data-index='1'] .create-timer-time-picker input", "21:12")
+ b.click("#timer-save-button")
+ b.wait_not_present("#timer-dialog")
+ b.wait_visible(self.svc_sel('monthly_timer.timer'))
+
+ m.execute("timedatectl set-time '2036-01-01 16:15:00'")
+ b.wait_visible(self.svc_sel('monthly_timer.timer'))
+ b.wait_in_text(self.svc_sel('monthly_timer.timer'), "Monthly timer")
+ wait_systemctl_timer("2036-01-06 14:12")
+ self.assertIn("2036-01-06 14:12", m.execute("systemctl list-timers"))
+ m.execute("timedatectl set-time '2036-01-07 00:00:00'")
+ wait_systemctl_timer("2036-01-08 21:12")
+ self.assertIn("2036-01-08 21:12", m.execute("systemctl list-timers"))
+ # checks if timer runs on next month February 2036 on same dates
+ m.execute("timedatectl set-time '2036-01-08 21:23'")
+ wait_systemctl_timer("2036-02-06 14:12")
+ self.assertIn("2036-02-06 14:12", m.execute("systemctl list-timers"))
+ # checks if timer runs on 8th March 2036 at 21:12
+ m.execute("timedatectl set-time '2036-03-07 00:00:00'")
+ wait_systemctl_timer("2036-03-08 21:12")
+ self.assertIn("2036-03-08 21:12", m.execute("systemctl list-timers"))
+ # creates a new weekly timer that runs on Fri at 12:45 and Sun at 20:12 every week
+ b.wait_visible("#create-timer")
+ b.click('#create-timer')
+ b.wait_visible("#timer-dialog")
+ b.set_input_text("#servicename", "weekly_timer")
+ b.set_input_text("#description", "Weekly timer")
+ b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
+ b.click("input[value=specific-time]")
+ b.select_from_dropdown("#drop-repeat", "weekly")
+ b.wait_visible("[data-index='0'] .week-days")
+ b.click("[data-index='0'] [aria-label='Add']")
+ b.select_from_dropdown("[data-index='0'] .week-days select", "fri")
+ b.set_input_text("[data-index='0'] .create-timer-time-picker input", "12:45")
+ b.select_from_dropdown("[data-index='1'] .week-days select", "sun")
+ b.set_input_text("[data-index='1'] .create-timer-time-picker input", "20:12")
+ b.click("#timer-save-button")
+ b.wait_not_present("#timer-dialog")
+ b.wait_visible(self.svc_sel('weekly_timer.timer'))
+
+ m.execute("timedatectl set-time '2036-03-08 00:00:00'")
+ b.wait_visible(self.svc_sel('weekly_timer.timer'))
+ b.wait_in_text(self.svc_sel('weekly_timer.timer'), "Weekly timer")
+ wait_systemctl_timer("Sun 2036-03-09 20:12")
+ self.assertIn("Sun 2036-03-09 20:12", m.execute("systemctl list-timers"))
+ m.execute("timedatectl set-time '2036-03-10 00:00:00'")
+ wait_systemctl_timer("Fri 2036-03-14 12:45")
+ self.assertIn("Fri 2036-03-14 12:45", m.execute("systemctl list-timers"))
+ # checks if timer runs on next week's Friday and Sunday
+ m.execute("timedatectl set-time '2036-03-15 00:00:00'")
+ wait_systemctl_timer("Sun 2036-03-16 20:12")
+ self.assertIn("Sun 2036-03-16 20:12", m.execute("systemctl list-timers"))
+ m.execute("timedatectl set-time '2036-03-17 00:00:00'")
+ wait_systemctl_timer("Fri 2036-03-21 12:45")
+ self.assertIn("Fri 2036-03-21 12:45", m.execute("systemctl list-timers"))
+ # creates a new daily timer that runs at 2:40 and at 21:15 every day
+ b.wait_visible("#create-timer")
+ b.click('#create-timer')
+ b.wait_visible("#timer-dialog")
+ b.set_input_text("#servicename", "daily_timer")
+ b.set_input_text("#description", "Daily timer")
+ b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
+ b.click("input[value=specific-time]")
+ b.select_from_dropdown("#drop-repeat", "daily")
+ b.click("[data-index='0'] [aria-label='Add']")
+ b.set_input_text("[data-index='0'] .create-timer-time-picker input", "2:40")
+ b.set_input_text("[data-index='1'] .create-timer-time-picker input", "21:15")
+ b.click("#timer-save-button")
+ b.wait_not_present("#timer-dialog")
+ b.wait_visible(self.svc_sel('daily_timer.timer'))
+
+ m.execute("timedatectl set-time '2036-03-17 00:00:00'")
+ b.wait_visible(self.svc_sel('daily_timer.timer'))
+ b.wait_in_text(self.svc_sel('daily_timer.timer'), "Daily timer")
+ wait_systemctl_timer("2036-03-17 02:40")
+ self.assertIn("2036-03-17 02:40", m.execute("systemctl list-timers"))
+ m.execute("timedatectl set-time '2036-03-17 03:00:00'")
+ wait_systemctl_timer("2036-03-17 21:15")
+ self.assertIn("2036-03-17 21:15", m.execute("systemctl list-timers"))
+ # checks if timer runs on 2036-04-10 at 02:40 and 21:15
+ m.execute("timedatectl set-time '2036-04-10 00:00:00'")
+ wait_systemctl_timer("2036-04-10 02:40")
+ self.assertIn("2036-04-10 02:40", m.execute("systemctl list-timers"))
+ m.execute("timedatectl set-time '2036-04-10 03:00:00'")
+ wait_systemctl_timer("2036-04-10 21:15")
+ self.assertIn("2036-04-10 21:15", m.execute("systemctl list-timers"))
+ # creates a new houry timer that runs at *:05 and at *:26
+ b.wait_visible("#create-timer")
+ b.click('#create-timer')
+ b.wait_visible("#timer-dialog")
+ b.set_input_text("#servicename", "hourly_timer")
+ b.set_input_text("#description", "Hourly timer")
+ b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
+ b.click("input[value=specific-time]")
+ b.select_from_dropdown("#drop-repeat", "hourly")
+ b.click("[data-index='0'] [aria-label='Add']")
+ b.set_input_text("[data-index='0'] input", "05")
+ b.set_input_text("[data-index='1'] input", "26")
+ b.click("#timer-save-button")
+ b.wait_not_present("#timer-dialog")
+ b.wait_visible(self.svc_sel('hourly_timer.timer'))
+
+ m.execute("timedatectl set-time '2036-04-10 03:00:00'")
+ b.wait_visible(self.svc_sel('hourly_timer.timer'))
+ b.wait_in_text(self.svc_sel('hourly_timer.timer'), "Hourly timer")
+ wait_systemctl_timer("2036-04-10 03:05")
+ self.assertIn("2036-04-10 03:05", m.execute("systemctl list-timers"))
+ m.execute("timedatectl set-time '2036-04-10 03:07:00'")
+ wait_systemctl_timer("2036-04-10 03:26")
+ self.assertIn("2036-04-10 03:26", m.execute("systemctl list-timers"))
+ m.execute("timedatectl set-time '2036-04-10 04:00:00'")
+ wait_systemctl_timer("2036-04-10 04:05")
+ # checks if timer runs on next hour at 5 min and 26 min
+ self.assertIn("2036-04-10 04:05", m.execute("systemctl list-timers"))
+ m.execute("timedatectl set-time '2036-04-10 04:10:00'")
+ wait_systemctl_timer("2036-04-10 04:26")
+ self.assertIn("2036-04-10 04:26", m.execute("systemctl list-timers"))
+
+ # creates a new minutely timer that runs at *:*:05 and at *:*:20
+ b.click('#create-timer')
+ b.wait_visible("#timer-dialog")
+ b.set_input_text("#servicename", "minutely_timer")
+ b.set_input_text("#description", "Minutely timer")
+ b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
+ b.select_from_dropdown("#drop-repeat", "minutely")
+ b.click("[data-index='0'] [aria-label='Add']")
+ b.set_input_text("[data-index='0'] input", "05")
+ b.set_input_text("[data-index='1'] input", "20")
+ b.click("#timer-save-button")
+ b.wait_not_present("#timer-dialog")
+ b.wait_visible(self.svc_sel('minutely_timer.timer'))
+
+ m.execute("timedatectl set-time '2036-04-10 04:15:07'")
+ b.wait_visible(self.svc_sel('minutely_timer.timer'))
+ b.wait_in_text(self.svc_sel('minutely_timer.timer'), "Minutely timer")
+ wait_systemctl_timer("2036-04-10 04:15:20")
+ self.assertIn("2036-04-10 04:15:20", m.execute("systemctl list-timers | grep minutely_timer.timer"))
+ m.execute("timedatectl set-time '2036-04-10 04:15:55'")
+ wait_systemctl_timer("2036-04-10 04:16:05")
+ self.assertIn("2036-04-10 04:16:05", m.execute("systemctl list-timers | grep minutely_timer.timer"))
+
+ # creates a new timer that runs at today at 23:59
+ b.wait_visible("#create-timer")
+ b.click('#create-timer')
+ b.wait_visible("#timer-dialog")
+ b.set_input_text("#servicename", "no_repeat_timer")
+ b.set_input_text("#description", "No repeat timer")
+ b.set_input_text("#command", "/bin/sh -c '/bin/date >> /tmp/date'")
+ b.click("input[value=specific-time]")
+ b.set_input_text(".create-timer-time-picker input", "23:59")
+ b.click("#timer-save-button")
+ b.wait_not_present("#timer-dialog")
+ b.wait_visible(self.svc_sel('no_repeat_timer.timer'))
+ b.wait_in_text(self.svc_sel('no_repeat_timer.timer'), "No repeat timer")
+
+ m.execute("timedatectl set-time '2036-04-10 04:10:00'")
+ b.wait_visible(self.svc_sel('no_repeat_timer.timer'))
+ wait_systemctl_timer("2036-04-10 23:59")
+ self.assertIn("2036-04-10 23:59", m.execute("systemctl list-timers"))
+
+ # creates a boot timer that runs after 10 sec from boot
+ b.wait_visible("#create-timer")
+ b.click('#create-timer')
+ b.wait_visible("#timer-dialog")
+ b.set_input_text("#servicename", "boot_timer")
+ b.set_input_text("#description", "Boot timer")
+ b.set_input_text("#command", "/bin/sh -c 'echo hello >> /tmp/hello'")
+ b.click("input[value=system-boot]")
+ b.set_input_text(".delay-group input", "2")
+ b.click("#timer-save-button")
+ b.wait_not_present("#timer-dialog")
+ b.wait_visible(self.svc_sel('boot_timer.timer'))
+ m.reboot()
+ m.start_cockpit()
+ with testvm.Timeout(seconds=15, machine=m, error_message="Timeout while waiting for boot timer to run"):
+ m.execute("while [ ! -f /tmp/hello ] ; do sleep 0.5; done")
+ self.assertIn("hello", m.execute("cat /tmp/hello"))
+
+ self.allow_restart_journal_messages()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-system-shutdown-restart b/test/verify/check-system-shutdown-restart
new file mode 100755
index 0000000..eb4a2b5
--- /dev/null
+++ b/test/verify/check-system-shutdown-restart
@@ -0,0 +1,145 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+
+@testlib.skipDistroPackage()
+class TestShutdownRestart(testlib.MachineCase):
+ provision = {
+ "machine1": {"address": "10.111.113.1/20", "memory_mb": 512},
+ "machine2": {"address": "10.111.113.2/20", "memory_mb": 512}
+ }
+
+ def setUp(self):
+ super().setUp()
+ self.setup_provisioned_hosts()
+
+ @testlib.todoPybridgeRHEL8()
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ m2 = self.machines['machine2']
+ b2 = self.new_browser(m2)
+
+ m.start_cockpit()
+
+ self.login_and_go("/system")
+
+ # Reboot
+ b.click("#reboot-button")
+ b.wait_popup("shutdown-dialog")
+ b.wait_in_text(f"#shutdown-dialog button{self.danger_btn_class}", 'Reboot')
+ b.set_input_text("#message", "Rebooting for maintenance")
+ b.click("#delay")
+ b.click("button:contains('No delay')")
+ b.wait_text("#delay .pf-v5-c-select__toggle-text", "No delay")
+ b.click(f"#shutdown-dialog button{self.danger_btn_class}")
+ b.switch_to_top()
+
+ b.wait_in_text(".curtains-ct h1", "Disconnected")
+
+ m.wait_reboot()
+ m.start_cockpit()
+ b.click("#machine-reconnect")
+ b.wait_visible("#login")
+
+ self.allow_restart_journal_messages()
+ self.allow_journal_messages("Shutdown scheduled for .*")
+ self.check_journal_messages()
+
+ m2.start_cockpit()
+ b2.login_and_go("/system")
+
+ # Add machine with static IP to m2
+ self.add_machine("10.111.113.1", browser=b2)
+ b2.wait_text("#system_information_hostname_text", "machine1")
+
+ # Check auto reconnect on reboot
+ b2.become_superuser()
+ b2.click("#reboot-button")
+ b2.wait_popup("shutdown-dialog")
+ b2.wait_in_text(f"#shutdown-dialog button{self.danger_btn_class}", 'Reboot')
+ b2.click("#delay")
+ b2.click("button:contains('No delay')")
+ b2.wait_text("#delay .pf-v5-c-select__toggle-text", "No delay")
+ b2.click(f"#shutdown-dialog button{self.danger_btn_class}")
+ b2.switch_to_top()
+
+ b2.wait_visible(".curtains-ct")
+ b2.wait_visible(".curtains-ct .pf-v5-c-spinner")
+ b2.wait_in_text(".curtains-ct h1", "rebooting")
+
+ m.wait_reboot()
+
+ with b2.wait_timeout(30):
+ b2.wait_visible("#machine-troubleshoot")
+ self.start_machine_troubleshoot(password="foobar", browser=b2)
+
+ b2.enter_page("/system", host="10.111.113.1", reconnect=False)
+
+ # Try to insert an invalid time
+ self.login_and_go("/system")
+ b.enter_page("/system")
+ b.click("#shutdown-group") # caret
+ b.click("#shutdown")
+ b.wait_popup("shutdown-dialog")
+ b.wait_not_present("#delay-helper")
+ b.click("#delay")
+ b.click("button:contains('Specific time')")
+ b.wait_text("#delay .pf-v5-c-select__toggle-text", "Specific time")
+ b.wait_not_val("#shutdown-time-input", "")
+ b.wait_not_present("#delay-helper")
+ old = b.val("#shutdown-time-input")
+ b.set_input_text("#shutdown-time-input", "blah")
+ b.wait_text("#delay-helper", "Invalid time format")
+ b.wait_visible(f"#shutdown-dialog button{self.danger_btn_class}:disabled")
+
+ # Now set a correct time
+ b.set_input_text("#shutdown-time-input", old)
+ b.wait_visible(f"#shutdown-dialog button{self.danger_btn_class}:not(disabled)")
+ b.wait_not_present("#delay-helper")
+
+ # Try to insert an invalid date
+ b.set_input_text(".shutdown-date-picker input", "20/0x/2021")
+ b.wait_val(".shutdown-date-picker input", "20/0x/2021")
+ b.wait_text("#delay-helper", "Invalid date format")
+ b.wait_visible(f"#shutdown-dialog button{self.danger_btn_class}:disabled")
+
+ # Insert a date using the widget dropdown
+ b.click("button[aria-label='Toggle date picker']")
+ today_label = m.execute("date +'%-d %B %Y'").strip()
+ today_format = m.execute("date +'%-m/%-d/%Y'").strip()
+ b.click(f".pf-v5-c-calendar-month__dates-cell:not(.pf-m-adjacent-month) button[aria-label='{today_label}']")
+ b.wait_val(".shutdown-date-picker input", today_format)
+ b.blur(".shutdown-date-picker input")
+
+ # Power off
+ b.click(f"#shutdown-dialog button{self.danger_btn_class}")
+ b.switch_to_top()
+
+ # we don't need to wait for the dialog to close here, just the disconnect
+ b.wait_in_text(".curtains-ct h1", "Disconnected")
+
+ m.wait_poweroff()
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-system-terminal b/test/verify/check-system-terminal
new file mode 100755
index 0000000..b23e824
--- /dev/null
+++ b/test/verify/check-system-terminal
@@ -0,0 +1,266 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+
+def line_sel(i):
+ return '.terminal .xterm-accessibility-tree div:nth-child(%d)' % i
+
+
+@testlib.skipDistroPackage()
+class TestTerminal(testlib.MachineCase):
+ def setUp(self):
+ super().setUp()
+
+ self.allow_journal_messages(".*external channel failed: terminated")
+
+ # Make sure we get what we expect
+ self.write_file("/home/admin/.bashrc", r"""
+PS1="\u@local \W]\$ "
+PROMPT_COMMAND='printf "\033]0;%s@%s:%s\007" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/\~}"'
+""", append=True)
+
+ # Remove failed units which will show up in the first terminal line
+ self.machine.execute("systemctl reset-failed")
+
+ @testlib.skipBrowser("Firefox needs http to access clipboard", "firefox")
+ @testlib.nondestructive
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+ b.default_user = "admin"
+
+ self.login_and_go("/system/terminal")
+
+ blank_state = ' '
+
+ def wait_line(i, t):
+ try:
+ b.wait_text(line_sel(i), t)
+ except Exception as e:
+ print("-----")
+ for j in range(max(1, i - 5), i + 5):
+ print(b.text(line_sel(j)))
+ print("-----")
+ raise e
+
+ # wait until first line is not empty
+ n = 1
+ b.wait_visible(".terminal .xterm-accessibility-tree")
+ function_str = "(function (sel) { return ph_text(sel).trim() != '%s'})" % blank_state
+ b.wait_js_func(function_str, line_sel(n))
+
+ # clear any messages (for example, instructions about sudo) and wait for prompt
+ b.key_press("clear")
+ b.wait_js_cond("ph_text('.terminal').indexOf('clear') >= 0")
+ # now wait for clear to take effect
+ b.key_press("\r")
+ b.wait_js_cond("ph_text('.xterm-accessibility-tree').indexOf('clear') < 0")
+ # now we should get a clean prompt
+ b.wait_in_text(line_sel(n), '$')
+
+ # cut trailing non-breaking spaces
+ prompt = b.text(line_sel(n))
+
+ # Make sure we are started in home directory
+ # Account for non-standard prompting
+ if "]" not in prompt:
+ self.assertIn(":~$", prompt)
+ else:
+ self.assertIn("~]$", prompt)
+
+ # Run some commands
+ b.key_press("whoami\r")
+ wait_line(n + 1, "admin")
+
+ wait_line(n + 2, prompt)
+
+ b.key_press('echo -e "1\\u0041"\r')
+ wait_line(n + 3, '1A')
+
+ # The '@' sign is in the default prompt
+ b.wait_in_text(".terminal-title", '@')
+
+ # output flooding
+ b.key_press("seq 1000000\r")
+ with b.wait_timeout(300):
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "9999989999991000000admin@")
+
+ b.assert_pixels("#terminal .terminal-group", "header", ignore=[".terminal-title"])
+ b.assert_pixels("#terminal .xterm-text-layer", "text")
+
+ # now reset terminal
+ b.click('button:contains("Reset")')
+
+ # assert that the output from earlier is gone
+ wait_line(n + 1, blank_state)
+ self.assertNotIn('admin', b.text(line_sel(n + 1)))
+
+ # Check that when we `exit` we can still reconnect with the 'Reset' button
+ b.key_press("exit\r")
+ b.wait_in_text(".terminal .xterm-accessibility-tree", "disconnected")
+ b.click('button:contains("Reset")')
+ wait_line(n, prompt)
+ b.wait_not_in_text(".terminal .xterm-accessibility-tree", "disconnected")
+
+ def select_line(sel, width):
+ # Select line by dragging mouse
+ # Height on a line is around 14px, so start approx. in the middle of line
+ b.mouse(sel, "mousedown", 0, 17)
+ b.mouse(sel, "mousemove", width, 17)
+ b.mouse(sel, "mouseup", width, 17)
+
+ # Firefox does not support setting of permissions
+ # and therefore we cannot test copy/paste with context menu
+ if b.cdp.browser.name != "firefox":
+ try:
+ b.grant_permissions("clipboardReadWrite", "clipboardSanitizedWrite")
+ except RuntimeError:
+ # fallback for older Chrome releases
+ b.grant_permissions("clipboardRead", "clipboardWrite")
+
+ # Execute command
+ wait_line(n, prompt)
+ b.key_press('echo "XYZ"\r')
+ echo_result_line = n + 1
+
+ wait_line(echo_result_line, "XYZ")
+
+ sel = line_sel(echo_result_line)
+
+ # Highlight 40px (3 letters, never wider that ~14px)
+ select_line(sel, 40)
+
+ # Right click and pick copy
+ b.mouse(sel, "contextmenu", btn=2)
+ # Skipping mobile and medium layouts as these are the same with the default desktop layout
+ b.assert_pixels("#terminal .contextMenu", "context-menu", skip_layouts=["mobile", "medium"])
+ b.click('.contextMenu li:first-child button')
+
+ # Right click and pick paste
+ b.mouse(sel, "contextmenu", btn=2)
+ b.click('.contextMenu li:nth-child(2) button')
+
+ # Wait for text to show up
+ wait_line(echo_result_line + 1, prompt + "XYZ")
+ b.key_press('\r')
+ b.wait_in_text(line_sel(echo_result_line + 2), 'XYZ')
+
+ # now reset terminal
+ b.click('button:contains("Reset")')
+
+ # assert that the output from earlier is gone
+ wait_line(n + 1, blank_state)
+
+ # Execute another command
+ wait_line(n, prompt)
+ b.key_press('echo "foo"\r')
+ echo_result_line = n + 1
+
+ wait_line(echo_result_line, "foo")
+
+ sel = line_sel(echo_result_line)
+ # Highlight 40px (3 letters, never wider that ~14px)
+ select_line(sel, 40)
+
+ # Use keyboard shortcuts to copy text
+ b.key_press(chr(45), 2, use_ord=True) # Ctrl + Insert
+ b.key_press(chr(45), 8, use_ord=True) # Shift + Insert
+
+ # Wait for text to show up
+ wait_line(echo_result_line + 1, prompt + "foo")
+
+ # check that we get a sensible $PATH; this varies across OSes, so don't be too strict about it
+ b.key_press('\rclear\recho $PATH > /tmp/path\r')
+ # don't use wait_line() for the full match here, as line breaks get in the way; just wait until command has run
+ wait_line(echo_result_line, prompt)
+ path = m.execute("cat /tmp/path").strip()
+ if m.ostree_image:
+ self.assertIn("/usr/local/bin:/usr/bin", path)
+ elif m.image == "arch":
+ self.assertIn("/usr/local/sbin:/usr/local/bin:/usr/bin", path)
+ else:
+ self.assertIn("/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", path)
+ self.assertNotIn(":/.local", path) # would happen with empty $HOME
+
+ def check_theme_select(name, style):
+ b.select_from_dropdown("#theme-select + div select", name)
+ b.wait_attr_contains(".xterm-viewport", "style", style)
+
+ # Check that color theme changes
+ white_style = "background-color: rgb(255, 255, 255);"
+
+ check_theme_select("black-theme", "background-color: rgb(0, 0, 0);")
+ check_theme_select("dark-theme", "background-color: rgb(0, 43, 54);")
+ check_theme_select("light-theme", "background-color: rgb(253, 246, 227);")
+ check_theme_select("white-theme", white_style)
+
+ # Test changing of font size
+ b.wait_val("#toolbar input", 16)
+ current_style = b.attr(".xterm-accessibility-tree div:first-child", "style")
+ b.click("#toolbar button[aria-label='Increase by one']")
+ new_style = b.attr(".xterm-accessibility-tree div:first-child", "style")
+ self.assertNotEqual(current_style, new_style)
+
+ # Relogin and white-style and bigger font should be remembered
+ # Relogin on different page, as reloging on terminal prompts asking if you want to leave the site
+ b.go("/system")
+ b.relogin("/system")
+ b.go("/system/terminal")
+ b.enter_page("/system/terminal")
+ b.wait_visible('.terminal')
+ b.wait_attr_contains(".xterm-viewport", "style", white_style)
+ b.wait_val("#toolbar input", 17)
+ b.wait_attr(".xterm-accessibility-tree div:first-child", "style", new_style)
+ b.click("#toolbar button[aria-label='Decrease by one']")
+ b.wait_val("#toolbar input", 16)
+ b.wait_attr(".xterm-accessibility-tree div:first-child", "style", current_style)
+
+ # Check limit for font size
+ for i in range(15, 5, -1):
+ b.click("#toolbar button[aria-label='Decrease by one']")
+ b.wait_val("#toolbar input", i)
+ b.wait_visible("#toolbar button[aria-label='Decrease by one']:disabled")
+
+ @testlib.nondestructive
+ def testOnlyTerminal(self):
+ b = self.browser
+ m = self.machine
+
+ m.write("/etc/cockpit/cockpit.conf", "[WebService]\nShell = /system/terminal.html\n")
+ m.start_cockpit()
+ b.open("/")
+
+ b.try_login("admin", "foobar")
+ b.wait_visible("#terminal")
+
+ # wait until first line is not empty
+ n = 1
+ b.wait_visible(".terminal .xterm-accessibility-tree")
+ b.wait_js_func("(function (sel) { return ph_text(sel).trim() != ' '})", line_sel(n))
+
+ # execute a command we can see the effect of
+ b.wait_js_cond("ph_text('.terminal').indexOf('uid=') == -1")
+ b.key_press("id\r")
+ b.wait_js_cond("ph_text('.terminal').indexOf('uid=') >= 0")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-system-tuned b/test/verify/check-system-tuned
new file mode 100755
index 0000000..94696eb
--- /dev/null
+++ b/test/verify/check-system-tuned
@@ -0,0 +1,123 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+
+@testlib.nondestructive
+@testlib.skipOstree("No tuned packaged")
+@testlib.skipImage("No tuned packaged", "arch")
+@testlib.skipDistroPackage()
+class TestTuned(testlib.MachineCase):
+
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ recommended_profile = m.execute("tuned-adm recommend").strip()
+
+ # Stop tuned in case it is running by default, as on RHEL.
+
+ m.execute("systemctl stop tuned")
+ m.execute("systemctl disable tuned")
+ self.addCleanup(m.execute, "tuned-adm auto_profile") # Enable and set default profile
+
+ # Login and check initial state
+
+ self.login_and_go("/system")
+
+ def check_status_tooltip(expected):
+ b.mouse("#tuned-status-button", "mouseenter")
+ b.wait_text("#tuned-status-tooltip", expected)
+ # move mouse away again, to let the tooltip disappear and update
+ b.mouse("#tuned-status-button", "mouseleave")
+ b.wait_not_present("#tuned-status-tooltip")
+
+ b.wait_text('#tuned-status-button', "none")
+ check_status_tooltip("Tuned is not running")
+
+ # Start tuned manually. The recommended profile will be activated automatically.
+ m.execute("systemctl start tuned")
+ b.wait_text('#tuned-status-button', recommended_profile)
+ check_status_tooltip("This system is using the recommended profile")
+
+ # Disabled when not authorized
+ b.relogin('/system', superuser=False)
+ b.wait_text('#tuned-status-button.pf-m-aria-disabled', recommended_profile)
+ # page adjusts to privilege changes, button becomes clickable
+ b.become_superuser()
+
+ # Switch the profile
+
+ b.wait_text('#tuned-status-button', recommended_profile)
+ b.click('#tuned-status-button')
+ title_selector = ".pf-v5-c-modal-box__header:contains('Change performance profile')"
+ b.wait_visible(title_selector)
+ body_selector = ".pf-v5-c-modal-box:contains('Change performance profile')"
+ b.wait_visible(f"{body_selector} li > button.pf-m-selected p")
+
+ # Make sure we see the recommended profile and its badges
+ b.wait_in_text(f"{body_selector} li > button.pf-m-selected p", recommended_profile)
+ b.wait_visible(f"{body_selector} li > button.pf-m-selected .pf-v5-c-label:contains('recommended')")
+ b.wait_visible(f"{body_selector} li > button.pf-m-selected .pf-v5-c-label:contains('active')")
+
+ b.click(f"{body_selector} li > button p:contains('balanced')")
+ b.wait_visible(f"{body_selector} li > button.pf-m-selected p:contains('balanced')")
+
+ b.click(f"{body_selector} button.pf-m-primary")
+ b.wait_not_present(title_selector)
+
+ b.wait_text('#tuned-status-button', "balanced")
+ check_status_tooltip("This system is using a custom profile")
+
+ # Check the status of tuned, it should show the profile
+ output = m.execute("tuned-adm active 2>&1 || true")
+ self.assertIn("balanced", output)
+ output = m.execute("systemctl status tuned")
+ self.assertIn("enabled;", output)
+
+ # Now disable tuned
+ b.click('#tuned-status-button')
+ b.wait_visible(title_selector)
+ b.wait_visible(f"{body_selector} li > button.pf-m-selected p")
+
+ b.click(f"{body_selector} li > button p:contains('None')")
+ b.wait_visible(f"{body_selector} li > button.pf-m-selected p:contains('None')")
+ b.click(f"{body_selector} button.pf-m-primary")
+ b.wait_not_present(title_selector)
+
+ b.wait_text('#tuned-status-button', "none")
+
+ # Check the status of tuned, it should show disabled
+ output = m.execute("tuned-adm active 2>&1 || true")
+ self.assertIn("No current active profile", output)
+ output = m.execute("systemctl status tuned")
+ self.assertIn("disabled;", output)
+
+ # Click on the button again
+ b.click('#tuned-status-button')
+ b.wait_visible(title_selector)
+
+ # Tuned should still be disabled
+ output = m.execute("systemctl status tuned")
+ self.assertIn("disabled;", output)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-testlib b/test/verify/check-testlib
new file mode 100755
index 0000000..287fd37
--- /dev/null
+++ b/test/verify/check-testlib
@@ -0,0 +1,290 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2020 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import subprocess
+import unittest
+
+import testlib
+
+dirname = os.path.dirname(__file__)
+run_tests = os.path.join(testlib.TEST_DIR, "common", "run-tests")
+EXAMPLE_DIR = os.path.join(testlib.TEST_DIR, "example")
+VERIFY_DIR = os.path.join(testlib.TEST_DIR, "verify")
+ROOT_DIR = os.path.dirname(testlib.TEST_DIR)
+
+
+@testlib.skipDistroPackage()
+class TestRunTestListing(unittest.TestCase):
+
+ def testBasic(self):
+ # Listing on check-* file
+ self.assertEqual(subprocess.check_output([os.path.join(EXAMPLE_DIR, "check-example"), "-l", "TestNondestructiveExample"]).strip().decode(),
+ "TestNondestructiveExample.testOne\nTestNondestructiveExample.testTwo")
+ # Filter on class
+ p = subprocess.run([run_tests, "--test-dir", EXAMPLE_DIR, "-l", "TestNondestructiveExample"], capture_output=True, check=True)
+ self.assertIn(b"TestNondestructiveExample.testOne\nTestNondestructiveExample.testTwo", p.stdout.strip())
+ # Filter a specific test
+ self.assertIn("TestNondestructiveExample.testOne",
+ subprocess.check_output([run_tests, "--test-dir", EXAMPLE_DIR, "-l", "TestNondestructiveExample.testOne"]).strip().decode())
+ # Exclude test patterns
+ out = subprocess.check_output([run_tests, "--test-dir", EXAMPLE_DIR, "-l",
+ "--exclude", "bogus", "--exclude", "TestNondestructiveExample.testTwo",
+ "TestNondestructiveExample"]).strip().decode()
+ self.assertIn("TestNondestructiveExample.testOne", out)
+ self.assertNotIn("testTwo", out)
+
+ ndtests = subprocess.run([run_tests, "--test-dir", EXAMPLE_DIR, "-n", "-l"], check=True, capture_output=True)
+ self.assertIn(b"TestExample.testNondestructive\n", ndtests.stdout)
+ self.assertIn(b"TestNondestructiveExample.testOne\nTestNondestructiveExample.testTwo", ndtests.stdout)
+
+ # nondestructive tests are sorted alphabetically
+ verify_ndtests = subprocess.run([run_tests, "--test-dir", VERIFY_DIR, "-n", "-l"], check=True, capture_output=True)
+ self.assertRegex(verify_ndtests.stdout, re.compile(b".*TestAccounts.*TestFirewall.*TestLogin.*TestServices.*TestTerminal.*", re.S))
+
+ def testNonDestructive(self):
+ self.assertEqual(subprocess.check_output([run_tests, "--test-dir", EXAMPLE_DIR, "--nondestructive", "-l", "TestExample"]).strip().decode(),
+ "TestExample.testNondestructive")
+
+ # with short option and substring
+ self.assertEqual(subprocess.check_output([run_tests, "--test-dir", EXAMPLE_DIR, "-nl", "TestExamp"]).strip().decode(),
+ "TestExample.testNondestructive")
+
+
+# This can't be @nondestructive, as we run our own @nondestructive test nested inside this test. This will already call the
+# cleanup handlers, and the outside cleanup would then fail to do that again.
+@testlib.skipDistroPackage()
+class TestRunTest(testlib.MachineCase):
+ def testExistingMachine(self):
+ env = os.environ.copy()
+ try:
+ del env["TEST_JOBS"]
+ except KeyError:
+ pass
+ out = subprocess.check_output([run_tests, "--test-dir", EXAMPLE_DIR, "-vt",
+ "--machine", self.machine.ssh_address + ":" + self.machine.ssh_port,
+ "--browser", self.machine.web_address + ":" + self.machine.web_port,
+ "TestNondestructiveExample"], env=env)
+ self.assertRegex(out, b"\nok .* TestNondestructiveExample.testOne")
+ self.assertRegex(out, b"\nok .* TestNondestructiveExample.testTwo")
+ self.assertIn(b"TESTS PASSED", out)
+
+ # can't be called with concurrency
+ p = subprocess.Popen([run_tests, "--test-dir", EXAMPLE_DIR, "--machine", "1.2.3.4:56", "--browser", "1.2.3.4:67", "-j2"],
+ stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
+ (out, err) = p.communicate()
+ self.assertGreaterEqual(p.returncode, 1)
+ self.assertIn(b"--machine cannot be used with concurrent jobs", err)
+
+ # implies --nondestructive
+ out = subprocess.check_output([run_tests, "--test-dir", EXAMPLE_DIR, "--machine", "1.2.3.4:56", "--browser", "1.2.3.4:67",
+ "TestExample.testBasic"], env=env)
+ self.assertIn(b"1..0", out)
+ self.assertNotIn(b"TestExample", out)
+
+ def testRetry(self):
+ # Don't test this if not in git repo as we use `git diff` for this logic
+ if not os.path.exists(os.path.join(ROOT_DIR, ".git")):
+ return
+
+ env = os.environ.copy()
+ env["TEST_FAILURES"] = "1"
+ # pretend this was a PR against main (set by CI normally, or by the user with --base)
+ env["BASE_BRANCH"] = "main"
+ try:
+ del env["TEST_JOBS"]
+ except KeyError:
+ pass
+
+ # Check that we retry 3 times failing tests
+ process = subprocess.run([run_tests, "--test-dir", EXAMPLE_DIR, "TestExample.testFail", "TestExample.testSkip"],
+ env=env, capture_output=True)
+ stdout = process.stdout
+ self.assertRegex(stdout, rb"\nnot ok 1 .*test\/example\/check-example TestExample.testFail # RETRY 1 \(be robust against unstable tests\)\n")
+ self.assertRegex(stdout, rb"\nnot ok 1 .*test\/example\/check-example TestExample.testFail # RETRY 2 \(be robust against unstable tests\)\n")
+ self.assertRegex(stdout, rb"\nnot ok 1 .*test\/example\/check-example TestExample.testFail\n")
+ self.assertNotRegex(stdout, b"RETRY 3")
+ self.assertRegex(stdout, rb"\nok 2 .*test\/example\/check-example TestExample.testSkip # SKIP testSkip \(__main__\.TestExample")
+ self.assertRegex(stdout, rb"# 1 TESTS FAILED \[\d*s on .*, 2 destructive tests, 0 nondestructive tests: \]")
+
+ # Check retry logic for changed tests
+ test_file = os.path.join(EXAMPLE_DIR, "check-retry")
+ with open(test_file, 'r') as f:
+ original_test = f.read()
+
+ def write_file(file, content, mode=0o666):
+ # test files need to be executable, respect umask
+ content_bin = content.encode()
+ fd = os.open(file, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, mode=mode)
+ try:
+ written = os.write(fd, content_bin)
+ assert written == len(content_bin)
+ finally:
+ os.close(fd)
+
+ self.addCleanup(write_file, test_file, original_test)
+ write_file(test_file, original_test.replace("class NoTest", "class Test"))
+
+ process = subprocess.run([run_tests, "--test-dir", EXAMPLE_DIR, "TestRetryExample.testFail", "TestRetryExample.testBasic",
+ "TestRetryExample.testNoRetry"], env=env, capture_output=True)
+ stdout = process.stdout
+
+ # Changed test need to succeed 3 times
+ self.assertRegex(stdout, rb"\nok 1 .*test\/example\/check-retry TestRetryExample.testBasic # RETRY 1 \(test affected tests 3 times\)\n")
+ self.assertRegex(stdout, rb"\nok 1 .*test\/example\/check-retry TestRetryExample.testBasic # RETRY 2 \(test affected tests 3 times\)\n")
+ self.assertRegex(stdout, rb"\nok 1 .*test\/example\/check-retry TestRetryExample.testBasic\n")
+ self.assertNotRegex(stdout, b"RETRY 3")
+
+ # Changed test is never retried
+ self.assertRegex(stdout, rb"\nnot ok 2 .*test\/example\/check-retry TestRetryExample.testFail\n")
+ self.assertNotRegex(stdout, b"testFail # RETRY")
+
+ # Using @no_retry_when_changed prevents this retry logic
+ self.assertRegex(stdout, rb"\nok 3 .*test\/example\/check-retry TestRetryExample.testNoRetry\n")
+ self.assertNotRegex(stdout, b"testNoRetry # RETRY")
+
+ # Check retry logic for changed source
+ shell = os.path.join("pkg", "shell", "hosts.jsx")
+ self.assertTrue(os.path.exists(shell))
+
+ with open(shell, 'r') as f:
+ original_source = f.read()
+
+ self.addCleanup(write_file, shell, original_source)
+ write_file(shell, original_source + "\n")
+
+ # create affected test for shell changes
+ affected_testfile = os.path.join(EXAMPLE_DIR, "check-shell-testlib")
+ new_content = original_test.replace("class NoTest", "class NeinTest")
+ write_file(affected_testfile, new_content.replace("class NeinTestRetryExample", "class TestShell"), mode=0o777)
+ self.addCleanup(os.unlink, affected_testfile)
+
+ process = subprocess.run([run_tests, "--test-dir", EXAMPLE_DIR,
+ "TestShell.testBasic", "TestShell.testNoRetry"],
+ env=env, capture_output=True)
+ stdout = process.stdout
+ self.assertRegex(stdout, rb"\nok .* TestShell.testBasic # RETRY 1 \(test affected tests 3 times\)\n")
+ self.assertRegex(stdout, rb"\nok .* TestShell.testBasic # RETRY 2 \(test affected tests 3 times\)\n")
+ self.assertRegex(stdout, rb"\nok .* TestShell.testBasic\n")
+ self.assertNotRegex(stdout, b"RETRY 3")
+ self.assertRegex(stdout, b"\nok .* TestShell.testNoRetry\n")
+ self.assertNotRegex(stdout, b"TestShell.testNoRetry # RETRY")
+
+ # Check that just .css changes do no affect retry logic
+ systemd = os.path.join("pkg", "systemd", "overview.scss")
+ self.assertTrue(os.path.exists(systemd))
+
+ with open(systemd, 'r') as f:
+ original_source = f.read()
+
+ self.addCleanup(write_file, systemd, original_source)
+ write_file(systemd, original_source + "\n")
+
+ # create affected test for systemd changes
+ affected_testfile = os.path.join(EXAMPLE_DIR, "check-system-testlib")
+ write_file(affected_testfile, """#!/usr/bin/env python3
+import unittest
+class TestSystemd(unittest.TestCase):
+ def testBasic(self):
+ self.assertTrue(True)
+ """, mode=0o777)
+ self.addCleanup(os.unlink, affected_testfile)
+
+ process = subprocess.run([run_tests, "--test-dir", EXAMPLE_DIR, "TestSystemd.testBasic"],
+ env=env, capture_output=True)
+ stdout = process.stdout
+ self.assertRegex(stdout, rb"\nok .* TestSystemd.testBasic\n")
+ self.assertNotRegex(stdout, b"RETRY")
+
+ def testTodo(self):
+ env = os.environ.copy()
+ try:
+ del env["TEST_JOBS"]
+ except KeyError:
+ pass
+ env["TEST_TODO"] = "1"
+
+ process = subprocess.run([run_tests, "--test-dir", EXAMPLE_DIR, "TestTodo"],
+ env=env, capture_output=True)
+ stdout = process.stdout
+
+ # A @todo test which fails should write 'not ok ... # TODO ...'
+ self.assertRegex(stdout, rb"\nnot ok . .*test/example/check-example TestTodo.testTodoFail # TODO 2 is not yet sufficiently large\n")
+
+ # A @todo test which passes should write 'not ok ... # expected failure ...' and hard fail
+ self.assertRegex(stdout, rb"\nnot ok . .*test/example/check-example TestTodo.testTodoPass # expected failure: 2 is not yet sufficiently large\n")
+
+ # There should have been 2 cases, one fail
+ self.assertEqual(process.returncode, 1)
+
+
+@testlib.skipDistroPackage()
+@testlib.nondestructive
+class TestTestlib(testlib.MachineCase):
+ def testRestoreAPI(self):
+ m = self.machine
+
+ self.assertEqual(m.execute("whoami").strip(), "root")
+ # existing file
+ m.execute("echo original > /etc/someconfig")
+ self.restore_file("/etc/someconfig")
+ m.execute("echo changed > /etc/someconfig")
+ # nonexisting file
+ self.restore_file("/var/lib/cockpittest.txt")
+ m.execute("echo data > /var/lib/cockpittest.txt")
+
+ # existing dir
+ m.execute("mkdir -p /var/lib/existing_dir; echo hello > /var/lib/existing_dir/original")
+ self.restore_dir("/var/lib/existing_dir")
+ m.execute("rm /var/lib/existing_dir/original; echo pwned > /var/lib/existing_dir/new")
+ # nonexisting dir
+ self.restore_dir("/var/lib/cockpittestnew")
+ m.execute("mkdir -p /var/lib/cockpittestnew; echo hello > /var/lib/cockpittestnew/cruft")
+
+ # NSS is backed up by default
+ m.execute("useradd cockpittest")
+
+ # now pretend the test ends here
+ self.doCleanups()
+
+ # correctly restored existing file
+ self.assertEqual("original", m.execute("cat /etc/someconfig").strip())
+ m.execute("rm /etc/someconfig")
+ # correctly cleaned up nonexisting file
+ m.execute("test ! -e /var/lib/cockpittest.txt")
+
+ # correctly restored existing dir
+ self.assertEqual("original", m.execute("ls /var/lib/existing_dir").strip())
+ self.assertEqual("hello\n", m.execute("cat /var/lib/existing_dir/original"))
+ m.execute("rm -r /var/lib/existing_dir")
+ # correctly removed nonexisting dir
+ m.execute("test ! -e /var/lib/cockpittestnew")
+
+ # NSS/home got restored
+ self.assertNotIn("cockpittest", self.machine.execute("cat /etc/passwd"))
+ self.machine.execute("test ! -e /home/cockpittest")
+
+ def testMiscAPI(self):
+ assert self.system_before(1000)
+ assert not self.system_before(100)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-users b/test/verify/check-users
new file mode 100755
index 0000000..17d7f57
--- /dev/null
+++ b/test/verify/check-users
@@ -0,0 +1,1289 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import datetime
+import os
+
+import testlib
+
+good_password = "tqymuVh.ZfZnP§9Wr=LM3JyG5yx"
+
+
+def getUserAddDetails(machine):
+ useradd_defaults = {}
+ for line in machine.execute("useradd -D").splitlines():
+ key, value = line.split("=")
+ useradd_defaults[key] = value
+
+ return useradd_defaults
+
+
+def performUserAction(browser, user, action):
+ browser.wait_visible(f"#accounts-list tbody tr:contains({user}) .pf-v5-c-dropdown button")
+ browser.click(f"#accounts-list tbody tr:contains({user}) .pf-v5-c-dropdown button")
+ browser.wait_visible(f"#accounts-list tbody tr:contains({user}) .pf-v5-c-dropdown button[aria-expanded=true]")
+ browser.click(f".pf-v5-c-dropdown__menu-item:contains({action})")
+
+
+def createUser(
+ browser,
+ machine,
+ user_name,
+ real_name,
+ locked,
+ force_password_change,
+ password=None,
+ custom_home_dir=None,
+ default_shell=None,
+ custom_shell=None,
+ uid=None,
+ expected_uid=None,
+ verify_created=True,
+ run_assert_pixels=False,
+):
+ if default_shell is None:
+ default_shell = getUserAddDetails(machine)["SHELL"] or "/bin/bash"
+ browser.wait_visible('#accounts-create')
+ browser.click('#accounts-create')
+ browser.wait_visible('#accounts-create-dialog')
+ if run_assert_pixels:
+ browser.assert_pixels("#accounts-create-dialog", "accounts-create-dialog")
+
+ browser.set_input_text('#accounts-create-user-name', user_name)
+ browser.set_input_text('#accounts-create-real-name', real_name)
+ if password:
+ browser.set_input_text('#accounts-create-password-pw1', password)
+ browser.set_input_text('#accounts-create-password-pw2', password)
+
+ if locked:
+ browser.click('#accounts-create-locked')
+ browser.wait_visible('#accounts-create-locked:checked')
+ else:
+ browser.wait_visible('#account-use-password:checked')
+ browser.set_checked('#accounts-create-force-password-change', force_password_change)
+ browser.wait_visible('#accounts-create-locked:not(:checked)')
+
+ if expected_uid is not None:
+ browser.wait_visible(f'#accounts-create-user-uid[value="{expected_uid}"]')
+ if uid is not None:
+ browser.set_input_text('#accounts-create-user-uid', uid)
+ if custom_home_dir is not None:
+ browser.set_input_text('#accounts-create-user-home-dir', custom_home_dir)
+ else:
+ default_home_dir = getUserAddDetails(machine)["HOME"]
+ expected_home_dir = default_home_dir + "/" + user_name
+ browser.wait_visible(f"#accounts-create-user-home-dir[value='{expected_home_dir}']")
+
+ if custom_shell:
+ browser.select_from_dropdown("#accounts-create-user-shell", custom_shell)
+ else:
+ browser.wait_visible(f"#accounts-create-user-shell[data-selected='{default_shell}']")
+
+ browser.click('#accounts-create-dialog button.apply')
+
+ if verify_created:
+ browser.wait_not_present('#accounts-create-dialog')
+ browser.wait_in_text('#accounts-list', real_name)
+
+
+@testlib.nondestructive
+@testlib.skipDistroPackage()
+class TestAccounts(testlib.MachineCase):
+
+ def testBasic(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/users")
+
+ # Add a user externally
+ m.execute("useradd anton")
+ m.execute("echo anton:foobar | chpasswd")
+ with b.wait_timeout(30):
+ b.wait_in_text('#accounts-list', "anton")
+
+ # FIXME: rtl test was flaky, debug it and remove the skip
+ b.assert_pixels("#users-page", "users-page", ignore=["td[data-label='Last active']", "button:contains('more...')"], skip_layouts=["rtl"])
+
+ # There is only one badge and it is for admin
+ b.wait_text('#current-account-badge', 'Your account')
+ b.wait_js_cond('document.querySelector("#current-account-badge").previousSibling.getAttribute("href") === "#/admin"')
+
+ # The current account is the first in the list
+ b.wait_visible("#accounts-list > tbody :first-child #current-account-badge")
+
+ # Set a real name
+ b.go("#/anton")
+ b.wait_visible("#account-locked:not([disabled])")
+ b.assert_pixels("#users-page", "user-detail-page")
+ b.wait_text("#account-user-name", "anton")
+ b.wait_text("#account-title", "anton")
+ b.wait_not_attr("#account-delete", "disabled", "disabled")
+ b.set_input_text('#account-real-name', "") # Check that we can delete the name before setting it up
+ b.set_input_text('#account-real-name', "Anton Arbitrary")
+ b.wait_visible('#account-real-name:not([disabled])')
+ b.wait_text("#account-title", "Anton Arbitrary")
+ b.wait_text("#account-home-dir", os.path.join(getUserAddDetails(m)["HOME"] + "/anton"))
+ b.wait_text("#account-shell", getUserAddDetails(m)["SHELL"])
+ self.assertIn(":Anton Arbitrary:", m.execute("grep anton /etc/passwd"))
+
+ # Add some other GECOS fields
+ b.set_input_text('#account-real-name', "Anton Arbitrary,1,123")
+ b.wait_visible('#account-real-name:not([disabled])')
+ self.assertIn(":Anton Arbitrary,1,123:", m.execute("grep anton /etc/passwd"))
+ # Table title only shows real name, no other GECOS fields
+ b.wait_text("#account-title", "Anton Arbitrary")
+ # On the overview page it also shows only real name
+ b.go("/users")
+ b.wait_text('#accounts-list td[data-label="Full name"]:contains("Anton")', "Anton Arbitrary")
+ b.go("/users/#anton")
+
+ # Delete it
+ b.click('#account-delete')
+ b.wait_visible('#account-confirm-delete-dialog')
+ b.click('#account-confirm-delete-dialog button.apply')
+ b.wait_not_present('#account-confirm-delete-dialog')
+ b.wait_visible("#accounts")
+ b.wait_not_in_text('#accounts-list', "Anton Arbitrary")
+
+ # Attempt a real name with a colon
+ b.click('#accounts-create')
+ b.wait_visible('#accounts-create-dialog')
+ b.set_input_text('#accounts-create-real-name', "Col:n Colon") # This should fail
+ b.set_input_text('#accounts-create-password-pw1', good_password)
+ b.set_input_text('#accounts-create-password-pw2', good_password)
+ b.click('#accounts-create-dialog button.apply')
+ b.wait_in_text("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-error", "The full name must not contain colons.")
+ b.click('#accounts-create-dialog button.cancel')
+ b.wait_visible("#accounts")
+
+ # Check root user
+ b.go("#/root")
+ b.wait_text("#account-user-name", "root")
+ # some operations are not allowed for root user
+ b.wait_visible("#account-delete[disabled]")
+ b.wait_visible("#account-real-name[disabled]")
+ b.wait_visible("#account-logout[disabled]")
+ b.wait_visible("#account-locked:not(:checked)")
+ # check home directory and shell for root
+ b.wait_text("#account-home-dir", "/root")
+ b.wait_in_text("#account-shell", "/bin/bash") # can be /usr/bin/bash or /bin/bash
+ # root account should not be locked by default on our images
+ self.assertIn(m.execute("passwd -S root").split()[1], ["P", "PS"])
+ # now lock account
+ b.set_checked("#account-locked", val=True)
+ b.wait(lambda: m.execute("passwd -S root").split()[1] in ["L", "LK"])
+
+ # go back to accounts overview, check pf-v5-c-breadcrumb
+ b.click("#account .pf-v5-c-breadcrumb a")
+ b.wait_visible("#accounts-create")
+
+ # Create a user from the UI
+ self.sed_file('s@^SHELL=.*$@SHELL=/bin/true@', '/etc/default/useradd')
+ b.click('#accounts-create')
+ b.wait_visible('#accounts-create-dialog')
+ b.set_input_text('#accounts-create-user-name', "Berta")
+ b.set_input_text('#accounts-create-real-name', "Berta Bestimmt")
+ b.set_input_text('#accounts-create-password-pw1', "foo")
+ b.wait_visible("#accounts-create-password-meter.danger")
+ b.set_input_text('#accounts-create-password-pw1', good_password)
+ b.wait_visible("#accounts-create-password-meter.success")
+
+ # Test password show/hide functionality
+ b.wait_visible('#accounts-create-password-pw1[type="password"]')
+ b.click("#accounts-create-password-pw1-group button[aria-label='Show password']")
+ b.wait_visible('#accounts-create-password-pw1[type="text"]')
+ b.click("#accounts-create-password-pw1-group button[aria-label='Hide password']")
+ b.wait_visible('#accounts-create-password-pw1[type="password"]')
+
+ # Test confirmation password show/hide functionality
+ b.wait_visible('#accounts-create-password-pw2[type="password"]')
+ b.click("#accounts-create-password-pw2-group button[aria-label='Show confirmation password']")
+ b.wait_visible('#accounts-create-password-pw2[type="text"]')
+ b.click("#accounts-create-password-pw2-group button[aria-label='Hide confirmation password']")
+ b.wait_visible('#accounts-create-password-pw2[type="password"]')
+
+ # wrong password confirmation
+ b.set_input_text('#accounts-create-password-pw2', good_password + 'b')
+ b.click('#accounts-create-dialog button.apply')
+ b.wait_in_text("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-error", "The passwords do not match")
+ b.wait_not_present('#accounts-create-dialog button.pf-m-warning')
+
+ # too long password
+ long_password = "2a02-x!h4a" * 30
+ b.set_input_text('#accounts-create-password-pw1', long_password)
+ b.set_input_text('#accounts-create-password-pw2', long_password)
+ b.click('#accounts-create-dialog button.apply')
+ b.wait_in_text("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning", "Password is longer than 256 characters")
+ b.wait_not_present('#accounts-create-dialog button.pf-m-warning')
+
+ # changing input clears the error message
+ b.set_input_text('#accounts-create-password-pw1', "test")
+ b.set_input_text('#accounts-create-password-pw2', "test")
+ b.wait_not_present("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning")
+
+ # correct password confirmation
+ b.set_input_text('#accounts-create-password-pw1', good_password)
+ b.set_input_text('#accounts-create-password-pw2', good_password)
+ b.click('#accounts-create-dialog button.apply')
+ b.wait_not_present("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning")
+ b.wait_not_present('#accounts-create-dialog')
+ b.wait_in_text('#accounts-list', "Berta Bestimmt")
+
+ # Check home directory
+ home_dir = m.execute("getent passwd Berta | cut -f6 -d:").strip()
+ self.assertTrue(home_dir.endswith("/Berta"))
+ self.assertEqual(m.execute(f"stat -c '%U' {home_dir}").strip(), "Berta")
+
+ # Check that we set up shell configured in /etc/default/useradd
+ shell = m.execute("getent passwd Berta | cut -f7 -d:").strip()
+ self.assertEqual(shell, '/bin/true')
+
+ # Delete it externally
+ m.execute("userdel Berta")
+ b.wait_not_in_text('#accounts-list', "Berta Bestimmt")
+
+ b.logout()
+ b.login_and_go("/users")
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="robert",
+ real_name="Robert Robertson",
+ password=good_password,
+ locked=False,
+ force_password_change=True,
+ default_shell="/bin/true",
+ run_assert_pixels=True
+ )
+
+ # Test actions in kebab menu
+ # disable password
+ performUserAction(b, 'robert', 'Lock account')
+ b.click("#account-confirm-lock-dialog footer .pf-m-danger.apply")
+ # lock option is now disabled
+ b.click("#accounts-list tbody tr:contains(robert) .pf-v5-c-dropdown button")
+ b.wait_in_text(".pf-v5-c-dropdown__menu li:contains('Lock account') .pf-m-disabled", 'Lock account')
+ b.click("#accounts-list tbody tr:contains(robert) .pf-v5-c-dropdown button")
+ # change is visible on details page
+ performUserAction(b, 'robert', 'Edit user')
+ b.wait_in_text('#account-title', 'Robert Robertson')
+ b.wait_visible('#account-locked:checked')
+ b.click("#account-locked")
+ b.go("/users")
+
+ # In fedora-core userdel for this user consistently fails
+ # userdel: user robert is currently used by process *
+ if not m.ostree_image:
+ performUserAction(b, 'robert', 'Delete account')
+ b.click("#account-confirm-delete-dialog footer button.pf-m-danger.apply")
+ b.wait_not_in_text('#accounts-list', "Robert Robertson")
+
+ self.allow_journal_messages("Password quality check failed:")
+ self.allow_journal_messages("The password is a palindrome")
+ self.allow_journal_messages("passwd: user.*does not exist")
+ self.allow_journal_messages("passwd: Unknown user name '.*'.")
+ self.allow_journal_messages("lastlog: Unknown user or range: anton")
+ # when sed'ing, there is a short time when the config file does not exist
+ self.allow_journal_messages(".*libuser initialization error: .*/etc/default/useradd.*: No such file or directory")
+
+ def testFilterAndSort(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/users")
+
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="robert",
+ real_name="Robert Robertson",
+ password=good_password,
+ locked=False,
+ force_password_change=True,
+ )
+
+ # Check Robert's groups
+ b.wait_not_present("#accounts-list tbody tr:contains(robert) td[data-label='Group'] .pf-v5-c-label:contains(users)")
+ if not m.ostree_image: # Users group does not exist in coreos image
+ m.execute("/usr/bin/gpasswd -a robert users")
+ b.wait_in_text("#accounts-list tbody tr:contains(robert) td[data-label='Group'] .pf-v5-c-label.pf-m-cyan:contains(users)", "users")
+ m.execute(f"/usr/bin/gpasswd -a robert {m.get_admin_group()}")
+ b.wait_in_text("#accounts-list tbody tr:contains(robert) td[data-label='Group'] .pf-v5-c-label.pf-m-gold", m.get_admin_group())
+ m.execute(f"/usr/bin/gpasswd -d robert {m.get_admin_group()}")
+ b.wait_not_present("#accounts-list tbody tr:contains(robert) td[data-label='Group'] .pf-v5-c-label.pf-m-gold")
+
+ # test filters
+ b.set_input_text("#accounts-filter input", "root")
+ b.wait_in_text("#accounts-list tbody:first-of-type td[data-label='Username']", "root")
+ b.set_input_text("#accounts-filter input", "rOBeRt")
+ b.wait_in_text("#accounts-list tbody:first-of-type td[data-label='Username']", "robert")
+
+ uid = "1000"
+ if "debian" in m.image or "ubuntu" in m.image:
+ uid = "1001"
+ b.set_input_text("#accounts-filter input", uid)
+ b.wait_in_text("#accounts-list tbody:first-of-type td[data-label='ID']", uid)
+ b.set_input_text("#accounts-filter input", "spooky")
+ b.wait_visible("#accounts div.pf-v5-c-empty-state")
+
+ b.inject_js("""
+ function getTextColumn(query_selector) {
+ const values = [];
+ document.querySelectorAll(query_selector).forEach(node => values.push(node.innerText));
+ return values;
+ }""")
+
+ def check_column_sort(query_selector, invert=False):
+ # current account is always in the first row
+ b.wait_in_text("#accounts-list tbody:first-of-type td[data-label='Username']", "admin")
+ values = b.eval_js(f"getTextColumn(\"{query_selector}\")")
+ for i in range(2, len(values)):
+ if values[i].isnumeric():
+ value_current = int(values[i])
+ value_prev = int(values[i - 1])
+ else:
+ value_current = values[i].lower()
+ value_prev = values[i - 1].lower()
+
+ if (invert):
+ b.wait(lambda: value_prev > value_current)
+ else:
+ b.wait(lambda: value_prev < value_current)
+
+ # robert should be in users group
+ if not m.ostree_image: # Users group does not exist in coreos image
+ b.set_input_text("#accounts-filter input", "users")
+ names = b.eval_js("getTextColumn(\"[data-label='Username'] a\")")
+ b.wait(lambda: "robert" in names)
+
+ # clear text filters
+ b.click("#accounts-filter button[aria-label='Reset']")
+
+ # check alphabetical order of Username
+ check_column_sort("[data-label='Username'] a")
+ b.wait_visible("#accounts-list > thead > tr > th:nth-child(1) > button")
+ b.click("#accounts-list > thead > tr > th:nth-child(1) > button")
+ check_column_sort("[data-label='Username'] a", invert=True)
+
+ # sort by full name
+ b.click("#accounts-list > thead > tr > th:nth-child(2) > button")
+ check_column_sort("[data-label='Full name']")
+ b.click("#accounts-list > thead > tr > th:nth-child(2) > button")
+ check_column_sort("[data-label='Full name']", invert=True)
+
+ # sort by ID
+ b.click("#accounts-list > thead > tr > th:nth-child(3) > button")
+ check_column_sort("[data-label='ID']", invert=True)
+ b.click("#accounts-list > thead > tr > th:nth-child(3) > button")
+ check_column_sort("[data-label='ID']")
+
+ def testUserPasswords(self):
+ b = self.browser
+ m = self.machine
+
+ # Clean out the relevant logfiles
+ m.execute("truncate -s0 /var/log/{[bw]tmp,lastlog} /var/run/utmp")
+
+ self.login_and_go("/users")
+ # Create a locked user with weak password
+ self.sed_file('s/^SHELL=.*$/SHELL=/', '/etc/default/useradd')
+ self.allow_journal_messages(".*required to change your password immediately.*")
+ self.allow_journal_messages(".*user account or password has expired.*")
+
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="js",
+ real_name="Jussi Senior",
+ locked=True,
+ force_password_change=False,
+ verify_created=False,
+ )
+
+ # Check Confirm password is not validated until it's at least the same length as the first password
+ # Once they are the same length, it should valitate the Confirm password after each keystroke
+ b.set_input_text('#accounts-create-password-pw1', "foobar")
+ b.set_input_text('#accounts-create-password-pw2', "bar")
+ b.wait_not_present("#accounts-create-password-pw2-helper")
+ b.set_input_text('#accounts-create-password-pw2', "foobarfoo")
+ b.wait_visible("#accounts-create-password-pw2-helper")
+ b.wait_in_text("#accounts-create-password-pw2-helper", "The passwords do not match")
+ b.set_input_text('#accounts-create-password-pw2', "bar")
+ b.wait_visible("#accounts-create-password-pw2-helper")
+ b.wait_in_text("#accounts-create-password-pw2-helper", "The passwords do not match")
+ b.set_input_text('#accounts-create-password-pw2', "foobar")
+ b.wait_not_present("#accounts-create-password-pw2-helper")
+
+ b.click('#accounts-create-dialog button.cancel')
+ b.wait_not_present('#account-set-password-dialog')
+
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="js",
+ real_name="Jussi Senior",
+ locked=True,
+ force_password_change=False,
+ verify_created=False,
+ )
+
+ # Check Confirm password is not validated until the form is submitted
+ # Once it is submitted, it should valitate the Confirm password after each keystroke
+ b.set_input_text('#accounts-create-password-pw1', "foobar")
+ b.set_input_text('#accounts-create-password-pw2', "bar")
+ b.wait_not_present("#accounts-create-password-pw2-helper")
+ b.click('#accounts-create-dialog button.apply')
+ b.wait_visible("#accounts-create-password-pw2-helper")
+ b.wait_in_text("#accounts-create-password-pw2-helper", "The passwords do not match")
+ b.set_input_text('#accounts-create-password-pw2', "barfoo")
+ b.wait_visible("#accounts-create-password-pw2-helper")
+ b.wait_in_text("#accounts-create-password-pw2-helper", "The passwords do not match")
+ b.set_input_text('#accounts-create-password-pw2', "foobarfoo")
+ b.wait_visible("#accounts-create-password-pw2-helper")
+ b.wait_in_text("#accounts-create-password-pw2-helper", "The passwords do not match")
+ b.set_input_text('#accounts-create-password-pw2', "foobar")
+ b.wait_not_present("#accounts-create-password-pw2-helper")
+
+ b.click('#accounts-create-dialog button.cancel')
+ b.wait_not_present('#account-set-password-dialog')
+
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="jussi",
+ real_name="Jussi Junior",
+ password="foo",
+ locked=True,
+ force_password_change=False,
+ verify_created=False,
+ )
+
+ # Password is weak, lets change it to another weak - this should still not accept
+ b.wait_in_text("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning", "Password quality check failed:")
+ b.wait_visible('#accounts-create-dialog button.pf-m-warning')
+ b.set_input_text('#accounts-create-password-pw1', "bar")
+ b.set_input_text('#accounts-create-password-pw2', "bar")
+ b.wait_not_present("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning")
+ b.wait_not_present('#accounts-create-dialog button.pf-m-warning')
+ b.click('#accounts-create-dialog button.apply')
+
+ # Password is weak, confirm button should be disabled after first click
+ b.wait_in_text("#accounts-create-dialog .pf-v5-c-form__helper-text .pf-m-warning", "Password quality check failed:")
+ b.wait_visible("button.apply:disabled")
+
+ # Lets confirm the weak password now
+ b.click('#accounts-create-dialog button.pf-m-warning')
+
+ b.wait_not_present('#accounts-create-dialog')
+ b.wait_in_text('#accounts-list', "Jussi Junior")
+
+ def is_locked():
+ return m.execute("passwd -S jussi | cut -d' ' -f2").strip() in ["L", "LK"]
+
+ def is_admin():
+ return "jussi" in m.execute(f"getent group {m.get_admin_group()}")
+
+ admin_role_sel = '#account-groups-form-group'
+ b.wait(lambda: "jussi" in m.execute("grep jussi /etc/passwd"))
+ b.wait(lambda: not is_admin())
+ b.wait(is_locked)
+
+ # Check that by default we set up `/bin/bash`
+ shell = m.execute("getent passwd jussi | cut -f7 -d:").strip()
+ self.assertEqual(shell, '/bin/bash')
+
+ # Unlock it and make it an admin
+ b.go("#/jussi")
+ b.wait_text("#account-user-name", "jussi")
+ b.wait_visible("#account-locked:checked")
+ b.set_checked('#account-locked', val=False)
+ b.wait(lambda: not is_locked())
+ b.wait_not_present(admin_role_sel + f" .pf-v5-c-label:contains(:{m.get_admin_group()})")
+ b.click("#account-groups")
+ b.click(admin_role_sel + f" li:contains({m.get_admin_group()}) > button")
+ b.wait(is_admin)
+ b.wait_not_present("#account-groups-helper")
+
+ # Login as jussi and change role admin for itself
+ b.logout()
+ b.login_and_go("/users", user="jussi", password="bar")
+
+ # There is only one badge and it is for jussi
+ b.wait_text('#current-account-badge', 'Your account')
+ b.wait_js_cond('document.querySelector("#current-account-badge").previousSibling.getAttribute("href") === "#/jussi"')
+
+ # The current account is the first in the list
+ b.wait_visible("#accounts-list > tbody:first-of-type #current-account-badge")
+
+ # Use [x] button on the group label to remove the account from group
+ b.go("#/jussi")
+ b.wait_text("#account-user-name", "jussi")
+ b.wait_visible(admin_role_sel + f" .pf-v5-c-label:contains({m.get_admin_group()})")
+ b.wait_not_present("#account-groups-helper")
+ b.click(f".pf-v5-c-label-group__list .pf-v5-c-label__content:contains({m.get_admin_group()}) + span > button[aria-label='Close {m.get_admin_group()}']")
+ b.wait(lambda: not is_admin())
+ b.wait_not_present(admin_role_sel + f" .pf-v5-c-label:contains({m.get_admin_group()})")
+ if not m.ostree_image: # User is not shown as logged in when logged in through Cockpit
+ b.wait_visible("#account-groups-helper")
+ m.execute(f"/usr/bin/gpasswd -a jussi {m.get_admin_group()}")
+ with b.wait_timeout(20):
+ b.wait_visible(admin_role_sel + f" .pf-v5-c-label:contains({m.get_admin_group()})")
+
+ # Cannot lock the current account
+ b.wait_visible("#account-locked[disabled]")
+
+ b.go("#/admin")
+ b.wait_text("#account-user-name", "admin")
+ b.wait_visible(admin_role_sel + f" .pf-v5-c-label:contains({m.get_admin_group()})")
+ b.wait_not_present("#account-groups-helper")
+ b.logout()
+ b.login_and_go("/users")
+
+ # Change the password of this account
+ b.go("#/jussi")
+ b.wait_text("#account-user-name", "jussi")
+ b.click('#account-set-password')
+ b.wait_visible('#account-set-password-dialog')
+
+ # weak password
+ b.set_input_text("#account-set-password-pw1", 'a')
+ b.set_input_text("#account-set-password-pw2", 'a')
+ b.wait_visible("#account-set-password-meter.danger")
+ b.click('#account-set-password-dialog button.apply')
+ b.wait_in_text("#account-set-password-dialog .pf-v5-c-form__helper-text .pf-m-warning", "Password quality check failed:")
+ b.wait_visible('#account-set-password-dialog button.pf-m-warning')
+
+ # password mismatch
+ b.set_input_text("#account-set-password-pw1", good_password + 'a')
+ b.set_input_text("#account-set-password-pw2", good_password + 'b')
+ b.click('#account-set-password-dialog button.apply')
+ b.wait_in_text("#account-set-password-dialog .pf-v5-c-form__helper-text .pf-m-error", "The passwords do not match")
+ b.wait_not_present('#account-set-password-dialog button.pf-m-warning')
+
+ # too long password
+ long_password = "2a02-x!h4a" * 30
+ b.set_input_text('#account-set-password-pw1', long_password)
+ b.set_input_text('#account-set-password-pw2', long_password)
+ b.wait_not_present("#account-set-password-dialog .pf-v5-c-form__helper-text .pf-m-warning")
+ b.click('#account-set-password-dialog button.apply')
+ b.wait_in_text("#account-set-password-dialog .pf-v5-c-form__helper-text .pf-m-warning", "Password is longer than 256 characters")
+ b.wait_not_present('#account-set-password-dialog button.pf-m-warning')
+
+ good_password_2 = "cEwghLY§X9R&m8RLwk4Xfed9Bw="
+ # Now set to something valid
+ b.set_input_text("#account-set-password-pw1", good_password_2)
+ b.set_input_text("#account-set-password-pw2", good_password_2)
+ b.wait_visible("#account-set-password-meter.success")
+ b.wait_not_present("#account-set-password-dialog .pf-v5-c-form__helper-text .pf-m-warning")
+ b.click('#account-set-password-dialog button.apply')
+ b.wait_not_present('#account-set-password-dialog')
+
+ # incomplete passwd entry; fixed in PR #13384
+ m.execute('echo "damaged:x:1234:1234:Damaged" >> /etc/passwd')
+
+ # Logout and login with the new password
+ b.relogin(path="/users", user="jussi", password=good_password_2)
+
+ b.go("/users")
+ b.enter_page("/users")
+ b.wait_in_text('#accounts-list', "damaged")
+ b.click('#accounts-list td[data-label="Username"] a[href="#/damaged"]')
+ b.wait_in_text("#account-title", "Damaged")
+
+ if not m.ostree_image: # User is not shown as logged in when logged in through Cockpit
+ b.go("#/admin")
+ b.wait_visible("#account-logout[disabled]")
+
+ (year, month) = m.execute("date +'%Y %b'").strip().split()
+
+ # Log in as "admin" and the open details in other browser should update
+ b2 = self.new_browser(m)
+ b2.login_and_go("/system")
+ b.wait_text("#account-last-login", "Logged in")
+ b.wait_visible("#account-logout:not(:disabled)")
+
+ # Now log out and it should update again
+ b2.logout()
+ b.wait_in_text("#account-last-login", year)
+ b.wait_in_text("#account-last-login", month)
+ b.wait_visible("#account-logout[disabled]")
+
+ # Terminate session
+ b2.login_and_go("/system")
+ b.wait_text("#account-last-login", "Logged in")
+ b.click("#account-details button:contains('Terminate session')")
+ b.wait_in_text("#account-last-login", year)
+ b.wait_in_text("#account-last-login", month)
+ b.wait_visible("#account-logout[disabled]")
+
+ # Create an account and force password change on first login
+ b.go('/users')
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="robert",
+ real_name="Robert Robertson",
+ password=good_password,
+ locked=False,
+ force_password_change=True,
+ )
+ # Login as robert and check if password change is required
+ b.logout()
+
+ # login in second window to check if last login is updated in accounts list
+ if not m.ostree_image: # User is not shown as logged in when logged in through Cockpit
+ b2.login_and_go("/users")
+ b2.wait_in_text("#accounts-list tbody tr:contains(robert) td[data-label='Last active']", "Never logged in")
+
+ # On OSTree this happens over ssh
+ if m.ostree_image:
+ self.restore_dir("/etc/ssh", restart_unit="sshd")
+ m.execute("sed -i 's/.*ChallengeResponseAuthentication.*/ChallengeResponseAuthentication yes/' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*")
+ m.execute("systemctl try-restart sshd.service")
+
+ b.wait_visible("#login")
+ b.wait_not_visible("#conversation-group")
+ b.try_login(user="robert", password=good_password)
+ b.wait_visible('#conversation-input')
+ b.set_val('#conversation-input', good_password)
+ b.click('#login-button')
+
+ # Set new password
+ b.wait_in_text('#conversation-prompt', "New password:")
+ b.set_val('#conversation-input', good_password_2)
+ b.click('#login-button')
+
+ # Confirm new password
+ b.wait_in_text('#conversation-prompt', "Retype new password:")
+ b.set_val('#conversation-input', good_password_2)
+ b.click('#login-button')
+ b.wait_visible('#content')
+
+ def testCustomUID(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/users")
+
+ # Test custom UID
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="bob",
+ real_name="Bob Bobson",
+ password=good_password,
+ locked=False,
+ force_password_change=False,
+ uid="1500",
+ verify_created=True,
+ )
+ b.wait_visible("#accounts-list td[data-label='Username']:contains('bob') + td + td:contains('1500')")
+
+ # Test dialog predicts corrent next available UID
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="john",
+ real_name="John Johnson",
+ password=good_password,
+ locked=False,
+ force_password_change=False,
+ expected_uid="1501",
+ verify_created=True,
+ )
+ b.wait_visible("#accounts-list td[data-label='Username']:contains(john) + td + td:contains(1501)")
+
+ # Test creation of users with the same UID
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="jack",
+ real_name="Jack Jackson",
+ password=good_password,
+ locked=False,
+ force_password_change=False,
+ uid="1501",
+ verify_created=False,
+ )
+ b.wait_visible("#accounts-create-dialog")
+ b.wait_in_text("#accounts-create-user-uid-helper", "already used")
+ b.wait_visible("button.apply:disabled")
+ b.click("button:contains('Create account with non-unique UID')")
+ b.wait_not_present("#accounts-create-dialog")
+ b.wait_in_text("#accounts-list", "Jack Jackson")
+ b.wait_visible("#accounts-list td[data-label='Username']:contains(jack) + td + td:contains(1501)")
+ b.wait_visible("#accounts-list td[data-label='Username']:contains(john) + td + td:contains(1501)")
+
+ # No UID specified -> useradd chooses UID for us
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="nouidspecified",
+ real_name="NoUID Specified",
+ password=good_password,
+ locked=False,
+ force_password_change=False,
+ uid="",
+ verify_created=True,
+ )
+
+ # UID cannot be lower than UID_MIN
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="failedfailson",
+ real_name="Failed Failson",
+ password=good_password,
+ locked=False,
+ force_password_change=False,
+ uid="1",
+ verify_created=False,
+ )
+ b.wait_visible("#accounts-create-dialog")
+ b.wait_in_text("#accounts-create-user-uid-helper", "lower than")
+ b.click("#accounts-create-dialog button.cancel")
+ b.wait_not_present("#accounts-create-dialog")
+
+ # UID cannot be higher than UID_MAX
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="failedfailson",
+ real_name="Failed Failson",
+ password=good_password,
+ locked=False,
+ force_password_change=False,
+ uid="9999999",
+ verify_created=False,
+ )
+ b.wait_visible("#accounts-create-dialog")
+ b.wait_in_text("#accounts-create-user-uid-helper", "higher than")
+ b.click("#accounts-create-dialog button.cancel")
+ b.wait_not_present("#accounts-create-dialog")
+
+ # UID must be a positive integer
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="failedfailson",
+ real_name="Failed Failson",
+ password=good_password,
+ locked=False,
+ force_password_change=False,
+ uid="abc",
+ verify_created=False,
+ )
+ b.wait_visible("#accounts-create-dialog")
+ b.wait_in_text("#accounts-create-user-uid-helper", "positive integer")
+ b.click("#accounts-create-dialog button.cancel")
+ b.wait_not_present("#accounts-create-dialog")
+
+ def testCustomUserProperties(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/users")
+
+ # Test custom home directory
+ custom_dir_path = "/home/mycustomdir"
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="jussi",
+ real_name="Jussi Junior",
+ password=good_password,
+ locked=False,
+ custom_home_dir=custom_dir_path,
+ force_password_change=False,
+ verify_created=True,
+ )
+
+ b.go("#/jussi")
+ b.wait_text("#account-home-dir", custom_dir_path)
+ m.execute(f"test -d {custom_dir_path}")
+ m.execute("! test -d /home/jussi")
+
+ b.go("/users")
+ # Check assigning a file as home directory fails
+ m.execute("touch /home/isfile")
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="file",
+ real_name="File Filerson",
+ password=good_password,
+ locked=False,
+ custom_home_dir="/home/isfile",
+ force_password_change=False,
+ verify_created=False,
+ )
+ b.wait_visible("#accounts-create-dialog")
+ b.wait_in_text("#accounts-create-user-home-dir-helper", "existing file")
+ b.wait_visible("button.apply:disabled")
+ b.click("button.cancel")
+ b.wait_not_present("#accounts-create-dialog")
+
+ b.go("/users")
+ # Check assigning existing home directory to a new user
+ m.execute("mkdir /home/existingdir")
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="stealer",
+ real_name="Stealer OfHomeDirectison",
+ password=good_password,
+ locked=False,
+ custom_home_dir="/home/existingdir",
+ force_password_change=False,
+ verify_created=False,
+ )
+ b.wait_visible("#accounts-create-dialog")
+ b.wait_in_text("#accounts-create-user-home-dir-helper", "already exists")
+ b.wait_visible("button.apply:disabled")
+ self.assertEqual(m.execute("stat -c '%U %G' /home/existingdir").rstrip(), "root root")
+ b.click("button:contains('Create and change ownership of home directory')")
+ b.wait_not_present("#accounts-create-dialog")
+ b.wait_in_text("#accounts-list", "Stealer OfHomeDirectison")
+ # Verify that ownership of home directory was changed to the new user
+ self.assertEqual(m.execute("stat -c '%U %G' /home/existingdir").rstrip(), "stealer stealer")
+
+ default_shell = getUserAddDetails(m)["SHELL"]
+ custom_shell = "/bin/sh"
+ if default_shell == custom_shell:
+ custom_shell = "/bin/bash"
+
+ createUser(
+ browser=b,
+ machine=m,
+ user_name="robert",
+ real_name="Robert Robertson",
+ password=good_password,
+ locked=False,
+ custom_shell=custom_shell,
+ force_password_change=False,
+ verify_created=True,
+ )
+
+ b.go("#/robert")
+ b.wait_text("#account-shell", custom_shell)
+
+ def testUnprivileged(self):
+ m = self.machine
+ b = self.browser
+ new_password = "tqymuVh.Zf5"
+ new_password_2 = "cEwghLYX"
+
+ m.execute("useradd antoine; echo antoine:foobar | chpasswd")
+ self.login_and_go("/users", user="antoine", superuser=False)
+ b.go("#/antoine")
+ b.wait_text("#account-user-name", "antoine")
+ b.wait_visible('#account-set-password:enabled')
+ b.click('#account-set-password')
+ b.wait_visible('#account-set-password-dialog')
+ b.set_input_text("#account-set-password-old", "foobar")
+ b.set_input_text("#account-set-password-pw1", new_password)
+ b.set_input_text("#account-set-password-pw2", new_password)
+ b.click('#account-set-password-dialog button.apply')
+ b.wait_not_present('#account-set-password-dialog')
+
+ # Logout and login with the new password
+ b.logout()
+ b.open("/users")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "antoine")
+ b.set_val("#login-password-input", new_password)
+ b.click('#login-button')
+ b.wait_visible('#content')
+
+ # Set minimum age to disallow changing it immediately again
+ m.execute("chage --mindays 7 antoine")
+ b.enter_page("/users")
+ b.go("#/antoine")
+ b.wait_text("#account-user-name", "antoine")
+ b.wait_visible('#account-set-password:enabled')
+ b.click('#account-set-password')
+ b.wait_visible('#account-set-password-dialog')
+ b.set_input_text("#account-set-password-old", new_password)
+ b.set_input_text("#account-set-password-pw1", new_password_2)
+ b.set_input_text("#account-set-password-pw2", new_password_2)
+ b.click('#account-set-password-dialog button.apply')
+ b.wait_in_text("#account-set-password-dialog .pf-v5-c-modal-box__body", "must wait longer")
+
+ @testlib.skipOstree("ssh root login not allowed")
+ def testRootLogin(self):
+ m = self.machine
+ b = self.browser
+ new_password = "tqymuVh.Zf5"
+
+ # this test uses quick logouts; async preloads cause "ReferenceError: cockpit is not defined"
+ self.disable_preload("packagekit", "playground", "systemd")
+
+ m.execute("useradd anton; echo anton:foobar | chpasswd")
+ self.enable_root_login()
+ self.login_and_go("/users", user="root", superuser=False)
+
+ # test this on root and a normal user account
+ for user in ["anton", "root"]:
+ b.go("#/" + user)
+ b.wait_text("#account-user-name", user)
+ b.wait_visible('#account-set-password:enabled')
+ b.click('#account-set-password')
+ b.wait_visible('#account-set-password-dialog')
+ b.wait_visible("#account-set-password-pw1")
+ # root does not need to know old password
+ b.wait_not_present("#account-set-password-old")
+ b.set_input_text("#account-set-password-pw1", new_password)
+ b.set_input_text("#account-set-password-pw2", new_password)
+ b.click('#account-set-password-dialog button.apply')
+ b.wait_not_present('#account-set-password-dialog')
+
+ # Logout and login with the new password
+ for user in ["anton", "root"]:
+ b.logout()
+ b.open("/users")
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", user)
+ b.set_val("#login-password-input", new_password)
+ b.click('#login-button')
+ b.wait_visible('#content')
+
+ def accountExpiryInfo(self, account, field):
+ for line in self.machine.execute(f"LC_ALL=C chage -l {account}").split("\n"):
+ if line.startswith(field):
+ _, _, value = line.partition(":")
+ return value.strip()
+ return None
+
+ def testExpire(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute("useradd scruffy -s /bin/bash -c Scruffy")
+ m.execute("echo scruffy:foobar | chpasswd")
+
+ self.login_and_go("/users")
+ b.go("#/scruffy")
+ b.wait_text("#account-user-name", "scruffy")
+
+ # Try to expire the account
+ b.wait_text("#account-expiration-text", "Never expire account")
+ self.assertEqual(self.accountExpiryInfo("scruffy", "Account expires"), "never")
+ b.click("#account-expiration-button")
+ b.wait_visible("#account-expiration")
+ b.click("#account-expiration-expires")
+
+ # Try an invalid date
+ b.set_input_text("#account-expiration-input input", "blah")
+ b.click("#account-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
+ b.wait_text("#account-expiration .pf-v5-c-form__helper-text .pf-m-error", "Invalid expiration date")
+
+ # Now a valid date 30 days in the future
+ when = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=30)
+ b.set_input_text("#account-expiration-input input", when.isoformat().split("T")[0])
+ b.click("#account-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
+ b.wait_not_present("#account-expiration")
+ b.wait_in_text("#account-expiration-text", "Expire account on")
+ self.assertNotEqual(self.accountExpiryInfo("scruffy", "Account expires"), "never")
+
+ # Now try and change it back
+ b.click("#account-expiration-button")
+ b.wait_visible("#account-expiration")
+ b.click("#account-expiration-never")
+ b.click("#account-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
+ b.wait_not_present("#account-expiration")
+ b.wait_text("#account-expiration-text", "Never expire account")
+ self.assertEqual(self.accountExpiryInfo("scruffy", "Account expires"), "never")
+
+ # Try to expire a password
+ b.wait_text("#password-expiration-text", "Never expire password")
+ self.assertEqual(self.accountExpiryInfo("scruffy", "Password expires"), "never")
+ b.click("#password-expiration-button")
+ b.wait_visible("#password-expiration")
+ b.click("#password-expiration-expires")
+
+ # Try an invalid number
+ b.set_input_text("#password-expiration-input", "-3")
+ b.click("#password-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
+ b.wait_text("#password-expiration .pf-v5-c-form__helper-text .pf-m-error", "Invalid number of days")
+
+ # Expire password every 30 days
+ b.set_input_text("#password-expiration-input", "30")
+ b.click("#password-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
+ b.wait_not_present("#password-expiration")
+ b.wait_in_text("#password-expiration-text", "Require password change on")
+ self.assertNotEqual(self.accountExpiryInfo("scruffy", "Password expires"), "never")
+
+ # Now try and change it back
+ b.click("#password-expiration-button")
+ b.wait_visible("#password-expiration")
+ b.click("#password-expiration-never")
+ b.click("#password-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
+ b.wait_not_present("#password-expiration")
+ b.wait_text("#password-expiration-text", "Never expire password")
+ self.assertEqual(self.accountExpiryInfo("scruffy", "Password expires"), "never")
+
+ # Now change it to expire again
+ b.click("#password-expiration-button")
+ b.wait_visible("#password-expiration")
+ b.click("#password-expiration-expires")
+ b.set_input_text("#password-expiration-input", "30")
+ b.click("#password-expiration .pf-v5-c-modal-box__footer button:contains(Change)")
+ b.wait_not_present("#password-expiration")
+
+ b.logout()
+ self.login_and_go("/users", user="scruffy")
+ b.go("#/scruffy")
+ b.wait_text("#account-user-name", "scruffy")
+ b.wait_text("#account-expiration-text", "Never expire account")
+ b.wait_visible("#account-expiration-button[disabled]")
+ b.wait_in_text("#password-expiration-text", "Require password change on")
+ b.wait_visible("#password-expiration-button[disabled]")
+
+ # Lastly force a password change
+ b.logout()
+ self.login_and_go("/users")
+ b.go("#/scruffy")
+ b.wait_text("#account-user-name", "scruffy")
+ b.click("#password-reset-button")
+ b.wait_visible("#password-reset")
+ b.click("#password-reset .pf-v5-c-modal-box__footer button:contains(Reset)")
+ b.wait_not_present("password-reset")
+ b.wait_in_text("#password-expiration-text", "Password must be changed")
+ self.assertEqual(self.accountExpiryInfo("scruffy", "Password expires"), "password must be changed")
+
+ @testlib.skipOstree("User is not shown as logged in when logged in through Cockpit")
+ def testAccountLogs(self):
+ b = self.browser
+ m = self.machine
+
+ # Clean out the relevant logfiles
+ m.execute("truncate -s0 /var/log/{[bw]tmp,lastlog} /var/run/utmp")
+
+ # Login once to create an entry
+ self.login_and_go("/users")
+ b.logout()
+
+ self.login_and_go("/users")
+ b.go("#/admin")
+ b.wait_visible("#account-logs")
+ # Header + one line of logins
+ b.wait_js_func("ph_count_check", "#account-logs tr", 2)
+
+ def testGroups(self):
+ b = self.browser
+ m = self.machine
+
+ def performGroupAction(browser, group, action):
+ browser.click(f"#groups-list tbody tr:contains({group}) .pf-v5-c-dropdown button")
+ browser.click(f"#groups-list tbody tr:contains({group}) .pf-v5-c-dropdown__menu li:contains({action})")
+
+ def selectGroupFromMenu(group, enabled):
+ if enabled:
+ testlib.wait(lambda: "testgroup0" not in m.execute("groups admin"))
+ else:
+ testlib.wait(lambda: "testgroup0" in m.execute("groups admin"))
+
+ b.wait_not_present(".pf-v5-c-select__menu")
+ b.click("#account-groups")
+ b.click(f".pf-v5-c-select__menu li:contains({group}) button")
+ if enabled:
+ b.wait_in_text(".pf-v5-c-label-group__list", group)
+ b.wait_not_present(f".pf-v5-c-select__menu li:contains({group}) button")
+ testlib.wait(lambda: "testgroup0" in m.execute("groups admin"))
+ else:
+ b.wait_not_in_text(".pf-v5-c-label-group__list", group)
+ b.wait_not_present(f".pf-v5-c-select__menu li:contains({group}) button")
+ testlib.wait(lambda: "testgroup0" not in m.execute("groups admin"))
+
+ m.execute("groupadd testgroup0")
+ m.execute("useradd anton")
+
+ self.login_and_go("/users")
+
+ # Groups filter is only visible in the expanded view
+ b.wait_not_present("#groups-filter")
+
+ b.click("#groups-view-toggle")
+ b.wait_visible('#groups-list td[data-label="Group name"]:contains("testgroup0")')
+
+ # Check group filter in expanded mode and the filter clear button
+ b.set_input_text("#groups-filter input", "casablanca")
+ b.wait_not_present('#groups-list td[data-label="Group name"]:contains("testgroup0")')
+ b.click("#groups-filter button")
+ b.wait_val("#groups-filter input", "")
+ b.wait_visible('#groups-list td[data-label="Group name"]:contains("testgroup0")')
+
+ # FIXME: rtl test was flaky, debug it and remove the skip
+ b.assert_pixels("#groups-list tr:contains(testgroup0)", "group-row", skip_layouts=["rtl"])
+
+ # Delete it
+ performGroupAction(b, 'testgroup0', 'Delete group')
+ b.wait_text("#group-confirm-delete-dialog footer .pf-v5-c-button.apply", "Delete")
+ b.click("#group-confirm-delete-dialog footer .pf-v5-c-button.apply")
+ b.wait_not_present('#group-confirm-delete-dialog')
+ b.wait_visible("#groups-list")
+ b.wait_not_in_text('#groups-list', "testgroup0")
+
+ # Add testgroup0 back
+ m.execute("groupadd testgroup0")
+
+ # Groups used as primary need force deletion
+ performGroupAction(b, 'anton', 'Delete group')
+ b.wait_text("#group-confirm-delete-dialog footer .pf-v5-c-button.apply", "Force delete")
+ b.assert_pixels("#group-confirm-delete-dialog", "group-delete-dialog")
+ b.click("#group-confirm-delete-dialog footer .pf-v5-c-button.apply")
+ b.wait_not_present('#account-confirm-delete-dialog')
+ b.wait(lambda: "#/" == b.eval_js('window.location.hash'))
+ b.wait_visible("#groups-list")
+ b.wait_not_in_text('#groups-list', "anton")
+
+ b.click('#accounts-list td[data-label="Username"] a[href="#/admin"]')
+
+ # Existing groups appear in labels
+ b.wait_in_text(".pf-v5-c-label-group__list", "admin")
+ b.wait_in_text(".pf-v5-c-label-group__list", m.get_admin_group())
+
+ # Primary group cannot be remove but others have a remove button
+ b.wait_visible(".pf-v5-c-label-group__list .pf-v5-c-label__content:contains(admin)")
+ b.wait_not_present(".pf-v5-c-label-group__list .pf-v5-c-label__content:contains(admin) + span > button")
+ b.wait_visible(f".pf-v5-c-label-group__list .pf-v5-c-label__content:contains({m.get_admin_group()}) + span > button[aria-label='Close {m.get_admin_group()}']")
+
+ # Clicking on the close button removes the group
+ b.click(f".pf-v5-c-label-group__list .pf-v5-c-label__content:contains({m.get_admin_group()}) + span > button[aria-label='Close {m.get_admin_group()}']")
+ b.wait_not_present(f".pf-v5-c-label-group__list .pf-v5-c-label__content:contains({m.get_admin_group()})")
+ b.wait_not_present(".pf-v5-c-select__menu")
+
+ # Add admin to the testgroup0 group
+ selectGroupFromMenu("testgroup0", enabled=True)
+
+ # Check that changes ar persistent after reload
+ b.reload()
+ b.enter_page("/users")
+ b.wait_in_text(".pf-v5-c-label-group__list", "testgroup0")
+
+ # Clicking on a used groups in the menu will remove it
+ selectGroupFromMenu("testgroup0", enabled=False)
+
+ # Clicking on the undo button will add the removed group back
+ b.click("#group-undo-btn")
+ b.wait_in_text(".pf-v5-c-label-group__list", "testgroup0")
+ m.execute("/usr/bin/gpasswd -d admin testgroup0")
+
+ # Clicking on the undo button will remove the added group back
+ b.reload()
+ b.enter_page("/users")
+ selectGroupFromMenu("testgroup0", enabled=True)
+ b.click("#group-undo-btn")
+ b.wait_not_in_text(".pf-v5-c-label-group__list", "testgroup0")
+ testlib.wait(lambda: "testgroup0" not in m.execute("groups admin"))
+
+ def testGroupCreate(self):
+ b = self.browser
+
+ self.login_and_go("/users")
+
+ # Create new group
+ b.click("#groups-create")
+ b.set_input_text("#groups-create-name", "titan")
+ b.wait_not_val("#groups-create-id", "")
+ b.click("#groups-create-dialog button.pf-m-primary")
+ b.wait_not_present("#groups-create-dialog")
+ b.wait_visible('#groups-list td[data-label="Group name"]:contains("titan")')
+
+ # Validation check for duplicate group name and id
+ b.click("#groups-create")
+ b.set_input_text("#groups-create-name", "titan")
+ b.set_input_text("#groups-create-id", "0")
+ b.click("#groups-create-dialog button.pf-m-primary")
+ b.wait_in_text("#groups-create-name-helper", "A group with this name already exists")
+ b.set_input_text("#groups-create-name", "titans")
+ b.click("#groups-create-dialog button.pf-m-primary")
+ b.wait_in_text("#groups-create-dialog .pf-v5-c-alert", "GID '0' already exists")
+
+ # Validation check for chars used in group name and valid group ID
+ b.set_input_text("#groups-create-name", "titan@1000")
+ b.set_input_text("#groups-create-id", "12f")
+ b.click("#groups-create-dialog button.pf-m-primary")
+ b.wait_in_text("#groups-create-name-helper", "The group name can only consist of letters")
+ b.wait_in_text("#groups-create-id-helper", "The group ID must be positive integer")
+ b.set_input_text("#groups-create-id", "-12")
+ b.wait_not_present("#groups-create-id-helper")
+ b.click("#groups-create-dialog button.pf-m-primary")
+ b.wait_in_text("#groups-create-id-helper", "The group ID must be positive integer")
+
+ # Validate no name and no group
+ b.set_input_text("#groups-create-name", "")
+ b.set_input_text("#groups-create-id", "")
+ b.click("#groups-create-dialog button.pf-m-primary")
+ b.wait_in_text("#groups-create-name-helper", "No group name specified")
+ b.wait_in_text("#groups-create-id-helper", "No ID specified")
+
+ # Create new group with custom ID
+ b.set_input_text("#groups-create-name", "saturn")
+ b.set_input_text("#groups-create-id", "1234")
+ b.click("#groups-create-dialog button.pf-m-primary")
+ b.wait_not_present("#groups-create-dialog")
+ b.wait_visible('#groups-list td[data-label="Group name"]:contains("saturn")')
+
+ def testGroupRename(self):
+ b = self.browser
+ m = self.machine
+
+ m.execute("groupadd titan; useradd uranus")
+
+ self.login_and_go("/users")
+ b.click("button:contains('more...')")
+ b.wait_visible('#groups-list td[data-label="Group name"]:contains("titan")')
+ b.click("#groups-list tbody tr:contains(titan) .pf-v5-c-dropdown button")
+ b.click("#groups-list tbody tr:contains(titan) .pf-v5-c-dropdown__menu li:contains('Rename group')")
+
+ b.set_input_text("#group-confirm-rename-dialog #group-name", "phoebe")
+ b.click("#group-confirm-rename-dialog .apply")
+ b.wait_not_present("#group-confirm-rename-dialog")
+ self.assertIn("phoebe", m.execute("getent group"))
+ self.assertNotIn("titan", m.execute("getent group"))
+
+ # Rename and delete actions are available only for user created groups
+ b.wait_not_present(f"#groups-list tbody tr:contains({m.get_admin_group()}) .pf-v5-c-dropdown button")
+
+ def testChangeShell(self):
+ b = self.browser
+ m = self.machine
+
+ self.login_and_go("/users")
+
+ # Add a user externally
+ m.execute("useradd anton")
+ m.execute("echo anton:foobar | chpasswd")
+ with b.wait_timeout(30):
+ b.wait_in_text('#accounts-list', "anton")
+
+ b.go("#/anton")
+ b.wait_text("#account-shell", getUserAddDetails(m)["SHELL"])
+ b.click("#change-shell-button")
+ b.wait_visible("#shell-dialog")
+ new_shell = "/bin/sh"
+ b.select_from_dropdown("#edit-user-shell", new_shell)
+ b.click('#shell-dialog button.apply')
+ b.wait_text("#account-shell", new_shell)
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-users-roles b/test/verify/check-users-roles
new file mode 100755
index 0000000..8504f69
--- /dev/null
+++ b/test/verify/check-users-roles
@@ -0,0 +1,126 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+
+@testlib.skipDistroPackage()
+class TestRoles(testlib.MachineCase):
+
+ @testlib.nondestructive
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ # Create a user without any role
+ m.execute("useradd user -s /bin/bash -c User")
+ m.execute("echo user:foobar | chpasswd")
+
+ # Give name to admin account
+ m.execute("usermod -c Administrator admin")
+
+ m.start_cockpit()
+
+ def login(user, password):
+ b.set_val("#login-user-input", user)
+ b.set_val("#login-password-input", password)
+ b.click('#login-button')
+
+ # login
+ b.open("/system")
+ b.wait_visible("#login")
+ login("user", "foobar")
+ b.enter_page("/system")
+ b.switch_to_top()
+ b.wait_text('#current-username', 'user')
+ b.set_layout("mobile")
+ b.click("#hosts-sel button")
+ b.wait_in_text(".view-hosts .pf-m-current", "user @")
+ b.click("#hosts-sel button")
+ b.set_layout("desktop")
+
+ b.go("/users#/user")
+ b.enter_page("/users")
+
+ admin_role_sel = f'.pf-v5-c-label:contains({m.get_admin_group()})'
+
+ b.wait_text("#account-user-name", "user")
+ b.wait_visible("#account-locked[disabled]")
+ b.wait_visible('output#account-real-name')
+ b.wait_visible("#account-set-password:not([disabled])")
+ b.wait_not_present("#account-delete")
+ b.wait_not_present("#account-logout")
+ b.wait_not_present(admin_role_sel + f" > button:contains(Close {m.get_admin_group()})")
+
+ # Check permissions for admin account
+ b.go("/users")
+ b.wait_visible("#accounts-list tbody tr:contains(admin) td[data-label='Group'] .pf-v5-c-label.pf-m-cyan:contains(admin)")
+ b.go("#/admin")
+ b.wait_text("#account-user-name", "admin")
+ b.wait_visible("#account-locked")
+ b.wait_visible('output#account-real-name')
+ b.wait_not_present("#account-set-password")
+ b.wait_not_present("#account-delete")
+ b.wait_not_present("#account-logout")
+ b.wait_not_present("#account-groups")
+ b.wait_visible(admin_role_sel)
+
+ # Add admin role from the outside (and wait for it to stick)
+ b.go("#/user")
+ b.wait_text("#account-user-name", "user")
+
+ b.wait_not_present(admin_role_sel)
+ m.execute(f"usermod -a -G {m.get_admin_group()} user")
+ b.wait_visible(admin_role_sel)
+
+ b.relogin("/users", "user", superuser=True)
+ b.wait_text("#account-user-name", "user")
+
+ # Check permissions for admin again
+ b.go("#/admin")
+ b.wait_text("#account-user-name", "admin")
+ b.wait_visible("#account-locked:not([disabled])")
+ b.wait_visible("input#account-real-name")
+ b.wait_visible("#account-set-password:not([disabled])")
+ b.wait_visible("#account-delete:not([disabled])")
+ # admin is not logged in, thus still disabled
+ b.wait_visible("#account-logout[disabled]")
+ b.wait_visible(admin_role_sel)
+ b.wait_visible("#account-groups")
+
+ @testlib.nondestructive
+ def testDynamic(self):
+ m = self.machine
+ b = self.browser
+
+ self.addCleanup(m.execute, "groupdel docker || true")
+ m.execute("getent group docker >/dev/null || groupadd docker")
+
+ self.login_and_go("/users")
+ b.go("#/admin")
+
+ b.click("#account-groups")
+ b.wait_visible(".pf-v5-c-select__menu li:contains(docker)")
+
+ m.execute("groupdel docker")
+ b.wait_not_present(".pf-v5-c-select__menu li:contains(docker)")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/check-ws-bastion b/test/verify/check-ws-bastion
new file mode 100755
index 0000000..81962a6
--- /dev/null
+++ b/test/verify/check-ws-bastion
@@ -0,0 +1,261 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import testlib
+
+HOST = "host.containers.internal"
+
+
+@testlib.onlyImage("no cockpit/ws container on this image", "fedora-coreos", "rhel4edge")
+@testlib.nondestructive
+class TestWsBastionContainer(testlib.MachineCase):
+ def setUp(self):
+ super().setUp()
+ # stop ws container from previous runs
+ self.machine.stop_cockpit()
+ # undo cockpit/ws install steps
+ self.restore_file("/etc/systemd/system/cockpit.service")
+ self.addCleanup(self.machine.execute, "podman rm -f --all")
+
+ def approve_key(self, b, hostname):
+ b.wait_visible("#hostkey-group")
+ b.wait_in_text("#hostkey-message-1", f"You are connecting to {hostname} for the first time.")
+ b.click("#login-button")
+
+ def testPasswordLogin(self):
+ m = self.machine
+ b = self.browser
+ m.execute("podman run -d --name cockpit-bastion -p 9090:9090 localhost/cockpit/ws")
+ m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done")
+
+ b.ignore_ssl_certificate_errors(ignore=True)
+ b.open("/", tls=True)
+
+ b.wait_visible("#login")
+ # should be pre-configured to RequireHost
+ b.wait_not_visible("#option-group")
+ b.wait_visible("#server-field")
+ # LoginTitle from default-bastion.conf
+ b.wait_text("#server-name", "Cockpit Bastion")
+ # No branding by default
+ b.wait_text("#brand", "")
+
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+
+ # Requires a host
+ b.click("#login-button")
+ b.wait_in_text("#login-error-message", "host to connect")
+ # so connect to our own container host
+ b.set_val("#server-field", HOST)
+ b.set_val("#login-password-input", "foobar")
+ # key is unknown
+ b.click("#login-button")
+ self.approve_key(b, HOST)
+
+ b.wait_visible('#content')
+ b.wait_text('#current-username', 'admin')
+ b.logout()
+
+ # remembers the last host via URL, server field should be pre-filled
+ self.assertEqual(b.eval_js("window.location.pathname"), f"/={HOST}/system")
+ # FIXME: login page does not really set this in the DOM? DOM has empty value, but browser shows the value
+ # b.wait_text("#server-field", host)
+ # this is only for Cockpit Client
+ b.wait_not_visible("#recent-hosts")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ # second time SSH key is known
+ b.click("#login-button")
+ b.wait_visible('#content')
+ b.logout()
+
+ def testKnownHosts(self):
+ m = self.machine
+ b = self.browser
+
+ m.execute(f"ssh-keyscan localhost | sed 's/^localhost/{HOST}/' > /root/known_hosts")
+ self.addCleanup(m.execute, "rm /root/known_hosts")
+ b.ignore_ssl_certificate_errors(ignore=True)
+
+ def check_login():
+ m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done")
+ b.open("/", tls=True)
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+ b.set_val("#server-field", HOST)
+ b.click("#login-button")
+ b.wait_visible('#content')
+ b.logout()
+ m.execute("podman rm -f cockpit-bastion")
+
+ # default location
+ m.execute("podman run -d --name cockpit-bastion -p 9090:9090 "
+ "-v /root/known_hosts:/etc/ssh/ssh_known_hosts:ro,Z "
+ "localhost/cockpit/ws")
+ check_login()
+
+ # custom location
+ m.execute("podman run -d --name cockpit-bastion -p 9090:9090 "
+ "-v /root/known_hosts:/known_hosts:ro,Z "
+ "-e COCKPIT_SSH_KNOWN_HOSTS_FILE=/known_hosts "
+ "localhost/cockpit/ws")
+ check_login()
+
+ # connect to all unknown hosts
+ # FIXME: this does not work
+ # m.execute("podman run -d --name cockpit-bastion -p 9090:9090 "
+ # "-e COCKPIT_SSH_CONNECT_TO_UNKNOWN_HOSTS=1 "
+ # "localhost/cockpit/ws")
+ # check_login()
+
+ def testCustomConf(self):
+ m = self.machine
+ b = self.browser
+
+ # custom cockpit.conf and pretend we are Fedora CoreOS/RHEL
+ self.write_file("/root/cockpit.conf", """[WebService]
+ LoginTitle = My Walden
+""")
+ m.execute("cp /etc/os-release /root; "
+ "podman run -d --name cockpit-bastion -p 9090:9090 "
+ "-v /root/cockpit.conf:/etc/cockpit/cockpit.conf:ro,Z "
+ "-v /root/os-release:/etc/os-release:ro,Z "
+ "localhost/cockpit/ws")
+ m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done")
+
+ b.ignore_ssl_certificate_errors(ignore=True)
+ b.open("/", tls=True)
+
+ b.wait_visible("#login")
+ b.wait_text("#server-name", "My Walden")
+ # custom conf does not have RequireHost
+ b.wait_visible("#option-group")
+ # Shows os-release branding
+ b.wait_in_text("#brand", "Fedora" if m.image == "fedora-coreos" else "Red Hat Enterprise Linux")
+
+ # pre-fill target host
+ b.open(f"/={HOST}/", tls=True)
+ b.wait_visible("#login")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#login-password-input", "foobar")
+
+ b.click("#login-button")
+ self.approve_key(b, HOST)
+
+ b.wait_visible('#content')
+ b.wait_text('#current-username', 'admin')
+
+ def testKeyLogin(self):
+ m = self.machine
+ b = self.browser
+
+ KEY_PASSWORD = "sshfoobar"
+ # old RSA/PEM format
+ m.execute(f"ssh-keygen -q -f /root/id_bastion -t rsa -m PEM -N {KEY_PASSWORD}")
+ m.execute(f"ssh-keyscan localhost | sed 's/^localhost/{HOST}/' > /root/known_hosts")
+ self.addCleanup(m.execute, "rm /root/known_hosts /root/id_bastion /root/id_bastion.pub")
+
+ m.execute("podman run -d --name cockpit-bastion -p 9090:9090 "
+ "-v /root/known_hosts:/etc/ssh/ssh_known_hosts:ro,Z "
+ "-v /root/id_bastion:/id_bastion:ro,Z "
+ "-e COCKPIT_SSH_KEY_PATH=/id_bastion "
+ "-e G_MESSAGES_DEBUG=cockpit-ssh "
+ "localhost/cockpit/ws")
+ m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done")
+
+ b.ignore_ssl_certificate_errors(ignore=True)
+ b.open("/", tls=True)
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#server-field", HOST)
+
+ # the account password does not work
+ b.set_val("#login-password-input", "foobar")
+ b.click("#login-button")
+ b.wait_text_not("#login-error-message", "")
+
+ # SSH key password, but key is not authorized
+ b.set_val("#login-password-input", KEY_PASSWORD)
+ b.click("#login-button")
+ b.wait_text_not("#login-error-message", "")
+
+ # authorize key
+ self.restore_file("/home/admin/.ssh/authorized_keys")
+ # Do not use authorized_keys.d as that does not work on rhel4edge
+ # Do not append but overwrite so we are sure the right key is used
+ m.execute("cat /root/id_bastion.pub > /home/admin/.ssh/authorized_keys")
+
+ # fails with wrong key password
+ b.set_val("#login-password-input", "notthispassword")
+ b.click("#login-button")
+ b.wait_text_not("#login-error-message", "")
+
+ # works with correct key password
+ b.set_val("#login-password-input", KEY_PASSWORD)
+ b.click("#login-button")
+ b.wait_visible('#content')
+ b.logout()
+
+ # now test with current OpenSSH format
+ m.execute(f"yes | ssh-keygen -q -f /root/id_bastion -t rsa -N {KEY_PASSWORD}")
+ m.execute("cat /root/id_bastion.pub > /home/admin/.ssh/authorized_keys")
+ b.set_val("#login-user-input", "admin")
+ b.set_val("#server-field", HOST)
+ b.set_val("#login-password-input", KEY_PASSWORD)
+ b.click("#login-button")
+ b.wait_visible('#content')
+
+
+@testlib.onlyImage("no cockpit/ws container on this image", "fedora-coreos", "rhel4edge")
+@testlib.nondestructive
+class TestWsPrivileged(testlib.MachineCase):
+ def testService(self):
+ # the install script should have created a cockpit.service, but not started it
+ m = self.machine
+
+ self.assertEqual(m.execute("! systemctl is-enabled cockpit.service").strip(), "disabled")
+ self.assertEqual(m.execute("! systemctl is-active cockpit.service").strip(), "inactive")
+ # stop ws container from previous test runs
+ self.machine.stop_cockpit()
+ self.assertEqual(m.execute("podman ps --noheading"), "")
+ self.addCleanup(m.execute, "systemctl disable --now cockpit.service")
+
+ m.execute("systemctl start cockpit.service")
+ self.assertEqual(m.execute("systemctl is-active cockpit.service").strip(), "active")
+ firstStartTime = m.execute("podman inspect cockpit-ws| jq '.[0].Created'")
+ m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done")
+
+ m.execute("systemctl restart cockpit.service")
+ self.assertEqual(m.execute("systemctl is-active cockpit.service").strip(), "active")
+ secondStartTime = m.execute("podman inspect cockpit-ws| jq '.[0].Created'")
+ m.execute("until curl --fail --head -k https://localhost:9090/; do sleep 1; done")
+
+ self.assertNotEqual(firstStartTime, secondStartTime)
+
+ m.execute("systemctl stop cockpit.service")
+ self.assertEqual(m.execute("! systemctl is-enabled cockpit.service").strip(), "disabled")
+ self.assertEqual(m.execute("! systemctl is-active cockpit.service").strip(), "inactive")
+ self.assertEqual(m.execute("podman ps --noheading"), "")
+
+ # current container has `set -x` in its startup script, which ends up in the journal
+ self.allow_journal_messages("^[/+'].*")
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/verify/conftest.py b/test/verify/conftest.py
new file mode 100644
index 0000000..4a1defc
--- /dev/null
+++ b/test/verify/conftest.py
@@ -0,0 +1,80 @@
+import importlib.machinery
+import importlib.util
+import os
+import sys
+from pathlib import Path
+from typing import List, Optional
+
+import pytest
+
+TOPDIR = os.path.realpath(__file__ + '../../../..')
+COMMON_PATH = [
+ f'{TOPDIR}/bots',
+ f'{TOPDIR}/bots/machine',
+ f'{TOPDIR}/test/common',
+]
+
+
+@pytest.hookimpl
+def pytest_collect_file(file_path: Path, parent: pytest.Collector) -> Optional[pytest.Collector]:
+ """Support for loading our check-* scripts as if they were modules
+
+ Also adds our testlib paths to sys.path. Doesn't check out the bots
+ because it's not possible to run tests without first running
+ test/image-prepare, and that will check out the bots automatically.
+ """
+ if file_path.name.startswith('check-'):
+ # Pretend that test/verify/check-name is called like test.verify.check_name
+ modname = 'test.verify.' + file_path.name.replace('-', '_')
+ loader = importlib.machinery.SourceFileLoader(modname, str(file_path))
+ spec = importlib.util.spec_from_loader(loader.name, loader)
+ assert spec is not None
+ module = importlib.util.module_from_spec(spec)
+
+ old_path = sys.path
+ try:
+ sys.path = [*COMMON_PATH, *sys.path]
+ loader.exec_module(module)
+ finally:
+ sys.path = old_path
+
+ # Return the tree node with our module pre-loaded inside of it
+ collector = pytest.Module.from_parent(parent, path=file_path)
+ collector._obj = module
+ return collector
+
+ return None
+
+
+@pytest.hookimpl
+def pytest_collection_modifyitems(session: pytest.Session, items: List[pytest.Item]) -> None:
+ """Sorts the tests to place all non-destructive tests together"""
+ assert isinstance(items, list)
+
+ def is_nondestructive(item: pytest.Item) -> bool:
+ attr = '_testlib__nondestructive'
+ assert isinstance(item, pytest.Function)
+ assert isinstance(item.parent, pytest.Class | pytest.Module)
+ return getattr(item.obj, attr, getattr(item.parent.obj, attr, False))
+
+ # put the destructive tests last under the assumption that they're slower
+ items.sort(key=is_nondestructive, reverse=True)
+
+
+@pytest.hookimpl
+def pytest_configure(config: pytest.Config) -> None:
+ """Tweaks test distribution for long-running tasks
+
+ pytest-xdist sends large chunks of tasks to the workers to reduce
+ latency, but since our tasks are long, this isn't helpful. It also
+ means that we can end up with a large string of very slow tests on
+ one worker. Disable it, if possible.
+
+ https://github.com/pytest-dev/pytest-xdist/issues/855
+ """
+ try:
+ # If parallel enabled and maxschedchunk not otherwise given...
+ if config.option.numprocesses and config.option.maxschedchunk is None:
+ config.option.maxschedchunk = 1
+ except AttributeError:
+ pass # no pytest-xdist plugin installed, or plugin is too old
diff --git a/test/verify/files/cert-chain.cert b/test/verify/files/cert-chain.cert
new file mode 100644
index 0000000..c009aa4
--- /dev/null
+++ b/test/verify/files/cert-chain.cert
@@ -0,0 +1,31 @@
+# How to regenerate the following certificates
+$ openssl req -new -newkey rsa:1024 -nodes -out ca.csr -keyout ca.key -subj '/OU=CA'
+$ openssl x509 -trustout -signkey ca.key -days 36500 -req -in ca.csr -out ca.pem
+$ openssl req -new -newkey rsa:1024 -nodes -keyout inter.key -out inter.csr -subj '/OU=Intermediate'
+$ openssl x509 -req -days 36500 -in inter.csr -CA ca.pem -CAkey ca.key -set_serial 01 -out inter.pem
+$ openssl req -new -newkey rsa:1024 -nodes -keyout cert-chain.key -out cert-chain.csr -subj '/CN=localhost'
+$ openssl x509 -req -days 36500 -in cert-chain.csr -CA inter.pem -CAkey inter.key -set_serial 02 -out cert-chain.pem
+$ cat server.pem inter.pem > cert-chain.cert
+
+-----BEGIN CERTIFICATE-----
+MIIBnDCCAQUCAQIwDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UECwwMSW50ZXJtZWRp
+YXRlMCAXDTE1MTAyMDA5MTI0M1oYDzIxMTUwOTI2MDkxMjQzWjAUMRIwEAYDVQQD
+DAlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKmd9yN41PjZ
+9EnULLm2gaaiBZaV3W61XjUe/etEOU8m5ullhNiv0DGZNbt0ZgfL9aW8dbekTV9Q
+e8+YaPPzFKlQnaqt7lt4IVu+xA6qzEmRC6mP5odAqmoElJich3GrF6x2x3jWrCbX
+zijIlOKDAAgLedLixML7aVPOCpY1Bmk7AgMBAAEwDQYJKoZIhvcNAQELBQADgYEA
+DmvvCUi/xEgvsRWoDt4dJmhga+qgZ7649a2EdYIavNqphpmLWncCrvi9AstpWnvY
+BJSivqYCUwLrgteRDxPf7XkRT17OafgW0Xyss+rng+PzSGVR77swAf7v9rzrfWKC
+JWma5uFMIDqJCReG1Ox93N2juZdI6auqbYnUYi/4YgA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBlDCB/gIBATANBgkqhkiG9w0BAQsFADANMQswCQYDVQQLDAJDQTAgFw0xNTEw
+MjAwOTA4MDNaGA8yMTE1MDkyNjA5MDgwM1owFzEVMBMGA1UECwwMSW50ZXJtZWRp
+YXRlMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYLKL3UTbKWrvZKq06NW/b
+8f87lLw/sH0DSNs09aT8aVq4wLqGA/RC0ky53rH/IQXrHvoxR6h/+DrrxtGERBFO
+MIKhU5rH0UuE+vPcNCt30W3Igs4l2pYTDGEeUV5U5WpqQarnjFv5RTDM3ky2WEWR
+hLstwlEVKWMcmL5n3x/InwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAJczX3JveUjJ
+a4WMLnKBJsAvyTcpFyuY+NwlmAnJyWCkdxYFloIUqk0RyyC0zeJ/8zhCQ2ebGBGW
+Veuxze4Co53Ukikt2S3lbGKz56yjYXUvxcus731KY7m0U5PF+yNdwbLds21Qn3oF
+ykBKf8Wtclujqbuf7W97JiiNsPzMnQR0
+-----END CERTIFICATE-----
diff --git a/test/verify/files/cert-chain.key b/test/verify/files/cert-chain.key
new file mode 100644
index 0000000..82d8380
--- /dev/null
+++ b/test/verify/files/cert-chain.key
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAKmd9yN41PjZ9EnU
+LLm2gaaiBZaV3W61XjUe/etEOU8m5ullhNiv0DGZNbt0ZgfL9aW8dbekTV9Qe8+Y
+aPPzFKlQnaqt7lt4IVu+xA6qzEmRC6mP5odAqmoElJich3GrF6x2x3jWrCbXzijI
+lOKDAAgLedLixML7aVPOCpY1Bmk7AgMBAAECgYEAl/30ozW46pIspSfbOEPCJHbV
+uR3sQjOGCuYNtY/6JuJ+UzFkfOP3FSrsimEAuQXcxzp85iX2K24stFcn3Uq6/JS6
+nBCuLtpex6RIunTHuhACgqsBKhsxJu9YPX5t8PwOlPKD8GnkLv3A7Y6zrMf5AJbu
+RkBhXWS2tdIVVlL3RvECQQDUNUYFG8LqgHjI1QISoPMLThshTuWj+roV5OpYR7kO
+931c6YqrhX+DfO8voXOdBRLbrbFddqdv/1EEH/2PtEFjAkEAzJ6nCPeQRdW18qHd
+/isXFtP4+Mlzmp+OzPaMVh18pTZLKRXQo/DGZtS56Sdp+5buIAtDhgw/AqQPNXgW
+l+5sSQJAGbGFk9LDq974QODBin3gT4sab9L8rPkLlOENmri8aFqA3EIOnyvfv4zd
+7PdwZx+ZI+4I3pciOMZCCezRp5ecnwJBALBiuv6B2b45YWTMn/6vynGFPIPJtHu/
+u2t3xLWfKYmJafT6tyX7MiwGiFQyA6w7CP5ad1SgTdjK78lZwbAqePECQQDT1mml
+lz5/qGORJzvzT+FR7B66aJ/ogeYySoKl12bDKJQ5/6v1b+nsA1LagqwdLDFb6HhH
+1hi9cF60gY7OQPR1
+-----END PRIVATE KEY-----
diff --git a/test/verify/files/cockpit_event.conf b/test/verify/files/cockpit_event.conf
new file mode 100644
index 0000000..9f78589
--- /dev/null
+++ b/test/verify/files/cockpit_event.conf
@@ -0,0 +1,5 @@
+EVENT=workflow_Cockpit type=CCpp
+
+EVENT=report_Bugzilla type=CCpp duphash=
+ echo Cancel me
+ sleep 5m
diff --git a/test/verify/files/dmi/DMI b/test/verify/files/dmi/DMI
new file mode 100644
index 0000000..4c02fb7
--- /dev/null
+++ b/test/verify/files/dmi/DMI
Binary files differ
diff --git a/test/verify/files/dmi/smbios_entry_point b/test/verify/files/dmi/smbios_entry_point
new file mode 100644
index 0000000..5c70352
--- /dev/null
+++ b/test/verify/files/dmi/smbios_entry_point
Binary files differ
diff --git a/test/verify/files/embed-cockpit/embed.css b/test/verify/files/embed-cockpit/embed.css
new file mode 100644
index 0000000..e416072
--- /dev/null
+++ b/test/verify/files/embed-cockpit/embed.css
@@ -0,0 +1,25 @@
+#embed-links .card-pf-body {
+ opacity: 0.3;
+}
+
+#embed-here {
+ display: block;
+ inline-size: 1024px;
+ block-size: 768px;
+}
+
+#embed-address {
+ display: block;
+ margin-block-end: 20px;
+}
+
+a[target] {
+ cursor: pointer;
+}
+
+iframe {
+ display: block;
+ inline-size: 100%;
+ block-size: 100%;
+ border: 1px solid #ddd;
+}
diff --git a/test/verify/files/embed-cockpit/embed.js b/test/verify/files/embed-cockpit/embed.js
new file mode 100644
index 0000000..35db991
--- /dev/null
+++ b/test/verify/files/embed-cockpit/embed.js
@@ -0,0 +1,31 @@
+const frames = { };
+
+function click(ev) {
+ const href = ev.target.getAttribute("href");
+ ev.preventDefault();
+
+ let address = document.getElementById("embed-address").value;
+ if (address.indexOf(":") === -1)
+ address += ":9090";
+ const url = address + href;
+
+ let frame = frames[url];
+ if (!frame) {
+ frame = frames[url] = document.createElement("iframe");
+ frame.setAttribute("src", url);
+ frame.setAttribute("name", ev.target.getAttribute("id"));
+ document.getElementById("embed-here").appendChild(frame);
+ frame.addEventListener("load", ev => ev.target.setAttribute("loaded", "1"));
+ }
+
+ document.querySelectorAll("iframe")
+ .forEach(f => f.setAttribute("hidden", "hidden"));
+ frame.removeAttribute("hidden");
+ document.getElementById("embed-title").innerText = ev.target.innerText;
+ return false;
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ document.querySelectorAll("#embed-links a[href]")
+ .forEach(l => l.addEventListener("click", click));
+});
diff --git a/test/verify/files/embed-cockpit/index.html b/test/verify/files/embed-cockpit/index.html
new file mode 100644
index 0000000..8c3e0cf
--- /dev/null
+++ b/test/verify/files/embed-cockpit/index.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Embed Example</title>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <script type="text/javascript" src="embed.js"></script>
+ <link rel="stylesheet" href="embed.css">
+</head>
+<body id="content" class="cards-pf">
+
+ <!-- BOILERPLATE: Makes this look like another app -->
+ <main class="pf-v5-c-page__main" tabindex="-1">
+ <section class="pf-v5-c-page__main-section pf-m-light container-cards-pf">
+ <div class="row row-cards-pf">
+ <div id="embed-links" class="col-xs-3 col-sm-3 col-md-2">
+ <div class="row row-cards-pf">
+ <input id="embed-address" class="form-control" value="http://127.0.0.1"/>
+ </div>
+ <div class="card-pf card-pf-accented card-pf-aggregate-status">
+ <h2 class="card-pf-title">
+ <a id="embed-full" href="/cockpit/@localhost/system/index.html">
+ Full Server
+ </a>
+ </h2>
+ <div class="card-pf-body">
+ <p class="card-pf-aggregate-status-notifications">
+ <span class="card-pf-aggregate-status-notification" />
+ </p>
+ </div>
+ </div>
+ <div class="card-pf card-pf-accented card-pf-aggregate-status card-pf-with-action">
+ <h2 class="card-pf-title">
+ <a id="embed-terminal" href="/cockpit/@localhost/system/terminal.html">
+ Server Terminal</a>
+ </h2>
+ <div class="card-pf-body">
+ <p class="card-pf-aggregate-status-notifications">
+ <span class="card-pf-aggregate-status-notification">4</span>
+ <span class="card-pf-aggregate-status-notification">1</span>
+ </p>
+ </div>
+ </div>
+ <div class="card-pf card-pf-accented card-pf-aggregate-status">
+ <h2 class="card-pf-title">
+ <a id="embed-network" href="/cockpit/@localhost/network/index.html">
+ Server Networking</a>
+ </h2>
+ <div class="card-pf-body">
+ <p class="card-pf-aggregate-status-notifications">
+ <span class="card-pf-aggregate-status-notification"></span>1</span>
+ </p>
+ </div>
+ </div>
+ <div class="card-pf card-pf-accented card-pf-aggregate-status">
+ <h2 class="card-pf-title">
+ <a id="embed-storage" href="/cockpit/@localhost/storage/index.html">
+ Server Storage</a>
+ </h2>
+ <div class="card-pf-body">
+ <p class="card-pf-aggregate-status-notifications">
+ <span class="card-pf-aggregate-status-notification"></span>1</span>
+ </p>
+ </div>
+ </div>
+ <div class="card-pf card-pf-accented card-pf-aggregate-status">
+ <h2 class="card-pf-title">
+ <a id="embed-auth" href="/cockpit+app/@localhost/shell/index.html">
+ Separate Auth
+ </a>
+ </h2>
+ <div class="card-pf-body">
+ <p class="card-pf-aggregate-status-notifications">
+ <span class="card-pf-aggregate-status-notification">1</span>
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-9 col-sm-9 col-md-10">
+ <div class="card-pf">
+ <div class="card-pf-heading">
+ <h2 id="embed-title" class="card-pf-title">
+ Embedded Cockpit Here
+ </h2>
+ </div>
+ <div id="embed-here" class="card-pf-body">
+ </div>
+ </div>
+ </div>
+ </div>
+ </div><!-- /row -->
+ </div><!-- /container -->
+ <div id="embed-loaded">
+ </div>
+</body>
+</html>
diff --git a/test/verify/files/embed-cockpit/manifest.json b/test/verify/files/embed-cockpit/manifest.json
new file mode 100644
index 0000000..2ebc534
--- /dev/null
+++ b/test/verify/files/embed-cockpit/manifest.json
@@ -0,0 +1,12 @@
+{
+ "version": "999",
+ "requires": {
+ "cockpit": "122"
+ },
+
+ "tools": {
+ "index": {
+ "label": "Embed Cockpit"
+ }
+ }
+}
diff --git a/test/verify/files/metrics-archives/2corescpu.tar.gz b/test/verify/files/metrics-archives/2corescpu.tar.gz
new file mode 100644
index 0000000..f29ae70
--- /dev/null
+++ b/test/verify/files/metrics-archives/2corescpu.tar.gz
Binary files differ
diff --git a/test/verify/files/metrics-archives/cpu_network.tar.gz b/test/verify/files/metrics-archives/cpu_network.tar.gz
new file mode 100644
index 0000000..eb4c976
--- /dev/null
+++ b/test/verify/files/metrics-archives/cpu_network.tar.gz
Binary files differ
diff --git a/test/verify/files/metrics-archives/disk.tar.gz b/test/verify/files/metrics-archives/disk.tar.gz
new file mode 100644
index 0000000..862f8a8
--- /dev/null
+++ b/test/verify/files/metrics-archives/disk.tar.gz
Binary files differ
diff --git a/test/verify/files/metrics-archives/double_events.zip b/test/verify/files/metrics-archives/double_events.zip
new file mode 100644
index 0000000..3f75d0c
--- /dev/null
+++ b/test/verify/files/metrics-archives/double_events.zip
Binary files differ
diff --git a/test/verify/files/metrics-archives/journal.journal.gz b/test/verify/files/metrics-archives/journal.journal.gz
new file mode 100644
index 0000000..6ebce13
--- /dev/null
+++ b/test/verify/files/metrics-archives/journal.journal.gz
Binary files differ
diff --git a/test/verify/files/metrics-archives/memory.tar.gz b/test/verify/files/metrics-archives/memory.tar.gz
new file mode 100644
index 0000000..b4d60b2
--- /dev/null
+++ b/test/verify/files/metrics-archives/memory.tar.gz
Binary files differ
diff --git a/test/verify/files/metrics-archives/with_journal.tar.gz b/test/verify/files/metrics-archives/with_journal.tar.gz
new file mode 100644
index 0000000..ba5e9af
--- /dev/null
+++ b/test/verify/files/metrics-archives/with_journal.tar.gz
Binary files differ
diff --git a/test/verify/files/mock-bugzilla-server.py b/test/verify/files/mock-bugzilla-server.py
new file mode 100755
index 0000000..ba38462
--- /dev/null
+++ b/test/verify/files/mock-bugzilla-server.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python3
+
+from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
+
+
+class RequestHandler(SimpleXMLRPCRequestHandler):
+ rpc_paths = ('/xmlrpc.cgi',)
+
+
+with SimpleXMLRPCServer(('', 8080), requestHandler=RequestHandler) as server:
+ class Bugzilla:
+ @server.register_function(name='Bugzilla.version')
+ def version(self):
+ return {'version': '42'}
+
+ server.register_instance(Bugzilla())
+
+ server.serve_forever()
diff --git a/test/verify/files/mock-faf-server.py b/test/verify/files/mock-faf-server.py
new file mode 100755
index 0000000..acf68d6
--- /dev/null
+++ b/test/verify/files/mock-faf-server.py
@@ -0,0 +1,65 @@
+#!/usr/bin/python3
+# export uReport_URL="http://localhost:12345"
+
+import cgi
+import json
+import sys
+from http.server import BaseHTTPRequestHandler, HTTPServer
+
+
+class Handler(BaseHTTPRequestHandler):
+ def do_POST_attach(self):
+ self.wfile.write(json.dumps({'result': True}).encode("UTF-8"))
+
+ def do_POST_new(self):
+ response = {
+ 'bthash': '123deadbeef',
+ 'message': 'http://localhost:12345/reports/42/\nhttps://bugzilla.example.com/show_bug.cgi?id=123456',
+ 'reported_to': [
+ {
+ 'type': 'url',
+ 'value': 'http://localhost:12345/reports/42/',
+ 'reporter': 'ABRT Server'
+ },
+ {
+ 'type': 'url',
+ 'value': 'https://bugzilla.example.com/show_bug.cgi?id=123456',
+ 'reporter': 'Bugzilla'
+ }
+ ],
+ 'result': False
+ }
+ self.wfile.write(json.dumps(response, indent=2).encode('UTF-8'))
+
+ def do_POST(self):
+ form = cgi.FieldStorage(
+ fp=self.rfile,
+ headers=self.headers,
+ environ={
+ 'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': self.headers['Content-Type'],
+ }
+ )
+
+ self.send_response(202)
+ self.send_header('Content-Type', 'application/json')
+ self.send_header('Connection', 'close')
+ self.end_headers()
+
+ json_str = form['file'].file.read()
+ try:
+ # just check that it's a JSON
+ json.loads(json_str)
+ except ValueError:
+ sys.stderr.write(f'Received invalid JSON data:\n{json_str}\n')
+ return
+
+ if self.path == '/reports/attach/':
+ self.do_POST_attach()
+ elif self.path == '/reports/new/':
+ self.do_POST_new()
+
+
+PORT = 12345
+httpd = HTTPServer(("", PORT), Handler)
+httpd.serve_forever()
diff --git a/test/verify/files/mock-insights b/test/verify/files/mock-insights
new file mode 100755
index 0000000..550f42e
--- /dev/null
+++ b/test/verify/files/mock-insights
@@ -0,0 +1,156 @@
+#! /usr/bin/python3
+
+# This is just enough of the Insights REST API to make the following
+# work:
+#
+# insights-client --register
+# insights-client --status
+# insights-client --check-results
+# insights-client --unregister
+#
+# You need these in your insights-client.conf:
+#
+# auto_config=False
+# auto_update=False
+# base_url=127.0.0.1:8443/r/insights
+# cert_verify=/var/lib/insights/mock-certs/ca.crt
+# username=admin
+# password=foobar
+
+import json
+import os
+import re
+import ssl
+import subprocess
+from http.server import BaseHTTPRequestHandler, HTTPServer
+
+systems = {}
+
+
+class handler(BaseHTTPRequestHandler):
+ def match(self, p):
+ return re.fullmatch(p, self.path)
+
+ def do_GET(self):
+
+ m = self.match("/r/insights")
+ if m:
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(b"lub-dup")
+ return
+
+ m = self.match("/r/insights/v1/static/uploader.v2.json")
+ if m:
+ # This is not a valid response and will cause the client
+ # to fall back to the builtin rules.
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(b"{ }\n")
+ return
+
+ m = self.match("/r/insights/v1/systems/([^/]+)")
+ if m:
+ machine_id = m[1]
+ self.send_response(200)
+ self.end_headers()
+ if machine_id in systems:
+ self.wfile.write(json.dumps(systems[machine_id]).encode('utf-8') + b"\n")
+ else:
+ self.wfile.write(b"{ }\n")
+ return
+
+ m = self.match("/r/insights/v1/branch_info")
+ if m:
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(b'{ "remote_branch": -1, "remote_leaf": -1 }\n')
+ return
+
+ m = self.match("/r/insights/platform/inventory/v1/hosts\\?insights_id=(.*)")
+ if m:
+ machine_id = m[1]
+ self.send_response(200)
+ self.end_headers()
+ if machine_id in systems:
+ self.wfile.write(b'{ "total": 1, "results": [ { "id": "123-nice-id" } ] }\n')
+ else:
+ self.wfile.write(b'{ "total": 0, "results": [ ] }\n')
+ return
+
+ m = self.match("/r/insights/platform/insights/v1/system/([^/]+)/reports/")
+ if m:
+ platform_id = m[1]
+ self.send_response(200)
+ self.end_headers()
+ if platform_id == "123-nice-id":
+ self.wfile.write(b'[ { "rule": { "total_risk": 3 } }, { "rule": { "total_risk": 2 } }, { "rule": { "total_risk": 1 } }]\n')
+ else:
+ self.wfile.write(b'[ ]\n')
+ return
+
+ self.send_response(404)
+ self.end_headers()
+
+ def do_POST(self):
+ content_length = int(self.headers.get('content-length', 0))
+ data = self.rfile.read(content_length)
+
+ m = self.match("/r/insights/v1/systems")
+ if m:
+ s = json.loads(data)
+ s["unregistered_at"] = None
+ s["account_number"] = "123456"
+ print(s)
+ systems[s["machine_id"]] = s
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(data)
+ return
+
+ m = self.match("/r/insights/uploads/([^/])+")
+ if m:
+ self.send_response(200)
+ self.end_headers()
+ self.wfile.write(b'{ "reports": [ "foo", "bar" ] }\n')
+ return
+
+ self.send_response(404)
+ self.end_headers()
+
+ def do_DELETE(self):
+
+ m = self.match("/r/insights/v1/systems/([^/]+)")
+ if m:
+ machine_id = m[1]
+ self.send_response(200)
+ self.end_headers()
+ if machine_id in systems:
+ del systems[machine_id]
+ return
+
+ self.send_response(404)
+ self.end_headers()
+
+
+def insights_server(port):
+ # Let's put the certs into /var/lib/insights so that SELinux
+ # allows insights-client to actually read ca.crt when running as a
+ # systemd service.
+ certdir = "/var/lib/insights/mock-certs"
+ if not os.path.exists(certdir):
+ os.makedirs(certdir)
+ subprocess.check_call(["sscg"], cwd=certdir)
+
+ httpd = HTTPServer(('', port), handler)
+ ctx = ssl.create_default_context()
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_OPTIONAL
+ # with newer Pythons this is ctx.minimum_version = ssl.TLSVersion.TLSv1_2
+ ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
+ ctx.load_cert_chain(f'{certdir}/service.pem', f'{certdir}/service-key.pem')
+ httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
+ httpd.serve_forever()
+
+
+insights_server(8443)
diff --git a/test/verify/files/ssh/id_ecdsa b/test/verify/files/ssh/id_ecdsa
new file mode 100644
index 0000000..a1f2687
--- /dev/null
+++ b/test/verify/files/ssh/id_ecdsa
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIJe1hvpnasPsWMUOinLNwqbZx4TmFVe1+XJBUox1n8aroAoGCCqGSM49
+AwEHoUQDQgAEzsak3rpxXmypaI4Bs4gy7YlhS4D1yiaILN8zqt0IzM3OruC+l3JV
+kQ7JSIAq9lFG0PDitqvix9PvfI0cHXgT3Q==
+-----END EC PRIVATE KEY-----
diff --git a/test/verify/files/ssh/id_ecdsa.pub b/test/verify/files/ssh/id_ecdsa.pub
new file mode 100644
index 0000000..03310e9
--- /dev/null
+++ b/test/verify/files/ssh/id_ecdsa.pub
@@ -0,0 +1 @@
+ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBM7GpN66cV5sqWiOAbOIMu2JYUuA9comiCzfM6rdCMzNzq7gvpdyVZEOyUiAKvZRRtDw4rar4sfT73yNHB14E90= test@test
diff --git a/test/verify/files/ssh/id_ed25519 b/test/verify/files/ssh/id_ed25519
new file mode 100644
index 0000000..db919b1
--- /dev/null
+++ b/test/verify/files/ssh/id_ed25519
@@ -0,0 +1,8 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBxFawn02
+Gy2Roqjr/Wfgj0AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIEFGhZ5twoV3DEA4
+yRIsTvF7t8xFKcj8qHW4PJKbKyFTAAAAkMJqtFfsq9acB6uD265qVKgx0JZ2+SuQeLum/9
+6kulHUT5Fo3ggPjHPzPzBe8l/4yL4nJGpIynYh3HvoA4wchFGILocZQbx/duRnBuxqVgtb
+P/eaZgO2tsM0+WcB5uvDBxMim7ZCLDfUZ027Q2vehS0hBgaE0JwtDVFpuZQa3myNYQPV0Z
+2rDfHMiz6ieApyOw==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/test/verify/files/ssh/id_ed25519.pub b/test/verify/files/ssh/id_ed25519.pub
new file mode 100644
index 0000000..37cb0c4
--- /dev/null
+++ b/test/verify/files/ssh/id_ed25519.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEFGhZ5twoV3DEA4yRIsTvF7t8xFKcj8qHW4PJKbKyFT test@test
diff --git a/test/verify/files/ssh/id_rsa b/test/verify/files/ssh/id_rsa
new file mode 100644
index 0000000..1d15c44
--- /dev/null
+++ b/test/verify/files/ssh/id_rsa
@@ -0,0 +1,30 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,6FB2545F53E2B528A133FF65060910E9
+
+T13Cv+h4AsCd7KNOWVmSUfbrVWfdfGVsxm0jMqbFJ4QhZ1rugzytr0HujD600GCt
+oLoECE9Yl/k0sYlVWafdaiIpn+h8GZiIillRGzKSykFvGgSYA4H1VYVb+YMqypB2
+zvlxlO/LiZFx7+XK0zsKJc+bnT6Ihrjb1RbjGPARUFc5d1+mk8Rn/gEPg5b6cXUk
+IXxA9IaHe/jxvz/pU6IKQue8rlH8OdmjeEI2yoxspRXagab7RCLrlSn+/F8Bm0Nz
+DL2XEsSKMF4xIGJzrcKrMPmtR1aeL6aiToZwMjGLq/aQLDd+jirOy2EPxtBTfz4x
+8DXvTZW0zqajYxct4JrP+R5lYuaxSi0qzIDf2slX2InsP62GjiijunLGjZkNmrUS
+DkGM5YuCszaxDYYLZBwksaEoqUQiBAIB9UNSBnjEyxdJWhFNZVfIUEB/DOOAADul
+8IxAEIbnZ4ap8kLrc852GwzCJX6mrhWDXXK9AhGzdRTTLBAeQY78Z75Tou23eOUT
+fPlYA+1rUZmQMQQB05Qej+jLdBNOw4BCEh3Y+NhXG3sX8qMQBPXZHVTli4Q30nYa
+141WlFYcoz1mdf/StDpbeC01T/rm4+rtXwwKYuP5xowaNXJ7XurhssO6G7kyMw0x
+ry8IGfJgQ+mTbNno6UXNwxzrNAp/l1JxJ2K+G6HumYYJEz5lSjCENBe4BUCAykSz
+s52vvcvEwIxTm7JalIl5a6zaa/62FXM7Jvk2gRjJ0xmC1zCTZygGVoXLU/Nomip+
+JQVlTF/nYzzAyHQyVlYX86Hd4e125qygblyYIrM30A22fXy5FPiCYdvHp7B0Zrbv
+9oQf5Q+4rAHuHoSApDzxoG2MJJuXlsf7nBCzwY670ifwpKgqEb7U+7GDKPO6mXq8
+Q2aTW5oUjB/hPqyZNF0PzJJTSb1zMxnzK6dOAys42CEreBGuGv3yitcu5rm0OIRq
+LmOMrw6+AObwbpcJLfzmzCkrjA1YmGAqumBSwyMtawzegYl/GtRNA57soCca5XTD
+DOmu/JB7ErNDNEw+Qp7z59ASZR3d8grdErmF9/ZlZI3jwJpflT/eArczT5Y0gr06
+RnyRsRd4QYPp5AquYtt72PlhX/cmwQzT+vGIbrrtNX+Pyjx+G4n3n6c6rqzCF7sJ
+t0hSDofsrC1cijWo8RKCLeUTEEpuENAMsIZyzsS9dFEy0Ro4lLAVzS3OT72XX8fO
+IWJfchV+DfbaXP5ylX3xuwFcwUtSZkaTA9ijmpU2gTOzyr7/o4Dp7eUO6SM8MNTa
+N2OuHgkWyVTPngpes9yPsKzn0ckjMldNYIq+dlmiTIapHnc8Iy4ooZfofbqafIOI
+4jrauFXiW9RfX1dRljCuIkIa9P4YO6Bd+WVNt7GaJx4y1wlN9KbminH1GYnidCSQ
+aqrj7/jAeT0TKMen1/CUxvX6f2iGnsnXA3Lv6brry7mXTnIMqWtKklFfUM4WwIKQ
+JCn2QwzQw3nQAOaPV0o+5B+F2XF1WiiyJxeddALrlxdSGss3scWYuhtRnClM8NFD
+2YYY0ytc7pjFem3Sx0lgoJ3zNPJaRlcX0T/2H0YQgWP33zdyMvpMhAdkijZiyveO
+-----END RSA PRIVATE KEY-----
diff --git a/test/verify/files/ssh/id_rsa.pub b/test/verify/files/ssh/id_rsa.pub
new file mode 100644
index 0000000..11b269c
--- /dev/null
+++ b/test/verify/files/ssh/id_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDG4iipTovcMg0xn+089QLNKVGpP2Pgq2duxHgAXre2XgA3dZL+kooioGFwBQSEjbWssKy82hKIN/W82/lQtL6krf7JQWnT3LZwD5DPsvHFKhOLghbiFzSI0uEL4NFFcZOMo5tGLrM5LsZsaIkkv5QkAE0tHIyeYinK6dQ2d8ZsfmgqxHDUQUWnz1T75X9fWQsUugSWI+8xAe0cfa4qZRz/IC+K7DEB3x4Ot5pl8FBuydJj/gb+Lwo2Vs27/d87W/0KHCqOHNwaVC8RBb1WcmXRDDetLGH1A9m5x7Ip/KU/cyvWWxw8S4VKZkTIcrGUhFYJDnjtE3Axz+D7agtps41t test@test
diff --git a/test/verify/files/test.png b/test/verify/files/test.png
new file mode 100644
index 0000000..27c7295
--- /dev/null
+++ b/test/verify/files/test.png
Binary files differ
diff --git a/test/verify/files/workflow_Cockpit.xml b/test/verify/files/workflow_Cockpit.xml
new file mode 100644
index 0000000..38237de
--- /dev/null
+++ b/test/verify/files/workflow_Cockpit.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<workflow>
+ <name>Report to Cockpit</name>
+ <description>Process the crash using Cockpit infrastructure</description>
+ <events>
+ <event>report_uReport</event>
+ <event>report_Bugzilla</event>
+ <event>post_report</event>
+ </events>
+</workflow>
diff --git a/test/verify/ruff.toml b/test/verify/ruff.toml
new file mode 100644
index 0000000..d67427c
--- /dev/null
+++ b/test/verify/ruff.toml
@@ -0,0 +1,7 @@
+extend = "../common/ruff.toml"
+
+[lint]
+ignore = [
+ "B023", # Function definition does not bind loop variable
+ "PT", # pytest rules do not apply as our integration tests use a custom unittest runner
+]
diff --git a/test/vm.install b/test/vm.install
new file mode 100644
index 0000000..ecebf4b
--- /dev/null
+++ b/test/vm.install
@@ -0,0 +1,57 @@
+#!/bin/sh
+# image-customize script to prepare a bots VM for testing cockpit
+# The cockpit packages will be installed separately
+set -eu
+
+if type firewall-cmd >/dev/null 2>&1; then
+ firewall-cmd --add-service=cockpit --permanent
+fi
+
+. /usr/lib/os-release
+
+# our tests expect cockpit.socket to not be running after boot, only after start_cockpit()
+if ! type rpm-ostree >/dev/null 2>&1; then
+ systemctl disable cockpit.socket
+fi
+
+# OS specific hacks
+
+if [ "$ID" = "debian" ] || [ "$ID" = "ubuntu" ]; then
+ # avoid random dpkg database locks, they break our package related tests
+ systemctl disable apt-daily-upgrade.timer
+
+ # create public firewalld zone for our firewall tests
+ systemctl start firewalld
+ firewall-cmd --zone=public --permanent --add-interface=eth1
+
+ # HACK: tuned breaks QEMU (https://launchpad.net/bugs/1774000)
+ systemctl disable tuned.service 2>/dev/null || true
+
+ # disarm 10-cloudimg-settings.conf
+ sed -i '/.*PasswordAuthentication no/d' /etc/ssh/sshd_config $(ls /etc/ssh/sshd_config.d/* 2>/dev/null || true)
+fi
+
+if [ "$ID" = "debian" ]; then
+ # make libpwquality less aggressive, so that our "foobar" password works
+ printf 'dictcheck = 0\nminlen = 6\n' >> /etc/security/pwquality.conf
+
+ # Allow libvirtd coredumps
+ echo '* soft core unlimited' >> /etc/security/limits.conf
+fi
+
+PLATFORM_ID="${PLATFORM_ID:-}"
+if [ "${PLATFORM_ID#platform:el}" != "$PLATFORM_ID" ]; then
+ # allow /usr/local/bin/ in sudo shells, to use our installed tools like the Python bridge
+ # Fedora, Debian etc. do that
+ echo 'Defaults secure_path = /sbin:/usr/sbin:/usr/local/bin:/bin:/usr/bin' > /etc/sudoers.d/usr-local
+fi
+
+# start cockpit once to ensure it works, and generate the certificate (to avoid re-doing that for each test)
+systemctl start cockpit
+systemctl stop cockpit
+
+# clean out the journal
+journalctl --flush
+journalctl --sync || killall systemd-journald
+rm -rf /var/log/journal/*
+rm -rf /var/lib/NetworkManager/dhclient-*.lease
diff --git a/tools/Makefile-tools.am b/tools/Makefile-tools.am
new file mode 100644
index 0000000..d5685a8
--- /dev/null
+++ b/tools/Makefile-tools.am
@@ -0,0 +1,25 @@
+
+if WITH_COVERAGE
+coverage:
+ mkdir -p tools/coverage
+ $(MAKE)
+ lcov --directory . --capture --initial \
+ --output-file tools/coverage.base
+ $(MAKE) check
+ lcov --directory . --capture \
+ --output-file tools/coverage.test
+ lcov --directory . \
+ --output tools/coverage.all \
+ --add-tracefile tools/coverage.base \
+ --add-tracefile tools/coverage.test
+ lcov --directory . \
+ --remove tools/coverage.all \
+ --output tools/coverage.info \
+ $(BUILT_SOURCES) 'test-*' 'mock-*' 'frob-*' '/usr/include/*'
+ genhtml --output-directory tools/coverage \
+ --title "cockpit $(PACKAGE_VERSION)" \
+ tools/coverage.info
+ @echo "file://$(abs_top_builddir)/tools/coverage/index.html"
+endif
+
+EXTRA_DIST += pkg/apps/content-security-policy.override
diff --git a/tools/Makefile.redirect b/tools/Makefile.redirect
new file mode 100644
index 0000000..617d417
--- /dev/null
+++ b/tools/Makefile.redirect
@@ -0,0 +1,12 @@
+# This redirects all make targets to builddir
+.PHONY: dist
+all:
+ $(MAKE) $(ARG) -C $(REDIRECT) all
+dist:
+ $(MAKE) $(ARG) -C $(REDIRECT) dist
+%:
+ $(MAKE) $(ARG) -C $(REDIRECT) $@
+ifeq ($(MAKEFLAGS), )
+ARG = -j8
+endif
+# REDIRECT=build
diff --git a/tools/README.dist b/tools/README.dist
new file mode 100644
index 0000000..9204dea
--- /dev/null
+++ b/tools/README.dist
@@ -0,0 +1,13 @@
+Webpack build output
+====================
+
+This directory is populated with the built Cockpit packages (see HACKING.md).
+
+You can link this directory into your home directory and have
+Cockpit on your local machine use the built packages:
+
+ $ mkdir -p ~/.local/share/
+ $ ln -s $(pwd)/dist ~/.local/share/cockpit
+
+Make sure to log into Cockpit as the same user on the same local
+machine as you are running the commands above.
diff --git a/tools/README.node_modules b/tools/README.node_modules
new file mode 100644
index 0000000..7a99109
--- /dev/null
+++ b/tools/README.node_modules
@@ -0,0 +1,72 @@
+# Cockpit Javascript Dependencies
+
+For included Javascript libraries, and build processes
+Cockpit uses Node.js dependencies to process its code. The required Node.JS
+javascript code is packaged in the `node_modules/` directory when distributed.
+
+Cockpit does not use Node.js or any npm packages code at runtime. In addition
+when building straight from a tarball it is also not necessary to have Node.js
+installed.
+
+## Git builds
+
+When building from Git, the `node_modules/` directory is managed as a git
+submodule from the `node-cache` repository in the same project as the main
+repository (ie: `cockpit-project/node-cache`). That means that normal builds
+won't need to run `npm install`. Checking out the correct version of the
+`node_modules` directory is managed by the script `tools/node-modules`, which
+is automatically invoked from the build system on an as-needed basis, and has
+good caching. You can also do a recursive git checkout, if you'd prefer to
+build in a no-network environment, but you'll lose the caching benefits.
+
+The gitlink for `node_modules/` is always kept in sync with the `package.json`
+file used to fetch that set of modules with `npm install`.
+
+## Making changes
+
+In the event that you need to modify `package.json`, the `node_modules` will
+need to be regenerated, and a new commit recorded. That can be managed locally
+by running
+
+```
+tools/node_modules install
+```
+
+which will produce a commit which is available for local testing.
+
+When you are ready to open a pull request with your changes, push the newly
+created `node_modules` commit to the cache by running
+
+```
+tools/node_modules push
+```
+
+Then create a pull request with your `package.json` and `node_modules` changes.
+
+## GitHub setup notes (documentation for project admins)
+
+This section documents the way to setup the `node-cache` repository on GitHub,
+from scratch. This could potentially be automated, but it doesn't need to
+happen regularly, so it's not a high priority. These instructions may also be
+useful for other projects or for forks.
+
+On the GitHub side, the workflow is driven from the `npm-install.yml` script,
+and makes use of the `node-cache upload` environment, which needs to be
+configured with a `NODE_CACHE_DEPLOY_KEY` secret. The exact steps:
+
+ - create the `node-cache` repository under the same project as the main
+ repository (or fork it)
+ - add a commit to that repository with a `README`. At least one commit on a
+ named branch needs to be present in order for the GitHub web UI to function
+ properly (eg: generate diffs, show tree contents, etc.)
+ - generate an SSH key locally, without a passphrase, without touching the disk:
+ `ssh-keygen -t ed25519 -C 'node-cache deploy' -f "${XDG_RUNTIME_DIR}/deploy-key" -N ''`
+ - create an environment called `node-cache upload` in the settings of the
+ primary repository:
+ https://github.com/cockpit-project/cockpit/settings/environments/new
+ - add the content of the private key `deploy-key` to the `node-cache upload`
+ environment as a secret with the name `NODE_CACHE_DEPLOY_KEY`
+ - upload the public key `deploy-key.pub` as a deploy key to `node-cache` with
+ write access enabled:
+ https://github.com/cockpit-project/node-cache/settings/keys/new
+ - destroy the local copy: `rm ${XDG_RUNTIME_DIR}/deploy-key*`
diff --git a/tools/adjust-distdir-timestamps b/tools/adjust-distdir-timestamps
new file mode 100755
index 0000000..96ac0d8
--- /dev/null
+++ b/tools/adjust-distdir-timestamps
@@ -0,0 +1,48 @@
+#!/bin/sh
+
+# Help create a deterministic distribution tarball with timestamps
+# based on the time of the latest git commit.
+
+set -eu
+
+distdir="$1"
+
+# This will work for git or for tarball
+#
+# Note: we give the .tarball file the exact same timestamp as the git commit,
+# so producing a tarball from a tarball should also create the exact same
+# output.
+
+stamp="$(cd "${0%/*}/.." && git show --format=%ct --no-patch 2>/dev/null || stat -c %Y version.m4)"
+
+test -n "${stamp}"
+
+timestamp() {
+ find "$@" -exec touch -d "@${stamp}" '{}' '+'
+ stamp="$((stamp + 1))"
+}
+
+# we add the date of the release to the Cockpit Client metainfo
+sed -i "s/@DATE@/$(date -d@${stamp} +'%Y-%m-%d')/" \
+ "${distdir}/src/client/org.cockpit_project.CockpitClient.metainfo.xml"
+
+# all files get the same timestamp as the commit date
+timestamp "${distdir}"
+
+# aclocal.m4 comes next
+timestamp "${distdir}/aclocal.m4"
+
+# then the other automake generated files
+timestamp \
+ "${distdir}/config.h.in" \
+ "${distdir}/configure" \
+ "${distdir}/Makefile.in"
+
+# package-lock.json needs to be newer than package.json
+timestamp "${distdir}/package-lock.json"
+
+# and dist/ needs to be newer than that
+timestamp "${distdir}/dist"
+
+# finally, the directories should be newer than everything else
+timestamp "${distdir}" -type d
diff --git a/tools/arch/PKGBUILD b/tools/arch/PKGBUILD
new file mode 100644
index 0000000..d9bf787
--- /dev/null
+++ b/tools/arch/PKGBUILD
@@ -0,0 +1,142 @@
+# Maintainer: Massimiliano Torromeo <massimiliano.torromeo@gmail.com>
+# Contributor: Iwan Timmer <irtimmer@gmail.com>
+# Contributor: Mark Constable <markc@renta.net>
+# Contributor: Anatol Pomozov <anatol.pomozov@gmail.com>
+
+pkgbase=cockpit
+pkgname=(cockpit cockpit-packagekit cockpit-pcp cockpit-storaged cockpit-test)
+pkgver=311
+pkgrel=1
+pkgdesc='A systemd web based user interface for Linux servers'
+arch=('x86_64')
+url='https://cockpit-project.org/'
+license=(LGPL)
+makedepends=(krb5 libssh accountsservice json-glib glib-networking
+ git intltool gtk-doc gobject-introspection networkmanager xmlto npm pcp
+ python-build python-installer python-wheel)
+source=("cockpit-${pkgver}.tar.xz"
+ "cockpit.pam"
+ "cockpit-ws.sysuser.conf"
+ "cockpit-wsinstance.sysuser.conf")
+sha256sums=('SKIP'
+ 'b95cdb0a336b7c1af9af9da1eed8520dfb8eae51869609416d380fee0357c523'
+ '1ad9dad75858264778bd94799b60c651f7cc1c7f7fa1c54622174303e639287a'
+ '46ee8ecad7bc97ba588ab9471dde76e41c00daf40658902425626c3a1938b438')
+
+prepare() {
+ cd cockpit-$pkgver
+ # TODO: disable buggy package-lock check
+ sed -r '/^cmd_make_package_lock_json\b/ a exit 0' -i tools/node-modules
+}
+
+build() {
+ cd cockpit-$pkgver
+ ./configure \
+ --prefix=/usr \
+ --sbindir=/usr/bin \
+ --libexecdir=/usr/lib/$pkgname/ \
+ --sysconfdir=/etc \
+ --localstatedir=/var \
+ --disable-dependency-tracking \
+ --disable-silent-rules \
+ --with-admin-group=wheel \
+ --with-cockpit-user=cockpit-ws \
+ --with-cockpit-ws-instance-user=cockpit-wsinstance
+ make all
+}
+
+package_cockpit() {
+ depends=(krb5 libssh json-glib glib-networking python)
+ backup=('etc/pam.d/cockpit' 'etc/cockpit/disallowed-users')
+ optdepends=("cockpit-pcp: reading performance metrics"
+ "cockpit-storaged: manage storage"
+ "cockpit-packagekit: manage packaged"
+ "cockpit-podman: user interface for managing podman containers"
+ "cockpit-machines: user interface for managing virtual machines"
+ "polkit: elevate privileges"
+ "sudo: elevate privileges"
+ "networkmanager: manage network connections"
+ "sssd: authentication"
+ "sscg: generate self-signed certificate")
+
+ cd cockpit-$pkgver
+ make DESTDIR="$pkgdir" install
+ rm -rf "$pkgdir"/usr/{src,lib/firewalld}
+ install -Dm644 "$srcdir"/cockpit.pam "$pkgdir"/etc/pam.d/cockpit
+ install -Dm644 "$srcdir"/cockpit-ws.sysuser.conf "$pkgdir"/usr/lib/sysusers.d/cockpit-ws.conf
+ install -Dm644 "$srcdir"/cockpit-wsinstance.sysuser.conf "$pkgdir"/usr/lib/sysusers.d/cockpit-wsinstance.conf
+
+ echo "z /usr/lib/cockpit/cockpit-session - - cockpit-wsinstance -" >> "$pkgdir"/usr/lib/tmpfiles.d/cockpit-tempfiles.conf
+
+ # remove unused plugins
+ rm -rf "$pkgdir"/usr/share/cockpit/{selinux,playground,sosreport} \
+ "$pkgdir"/usr/share/metainfo/org.cockpit-project.cockpit-{selinux,sosreport}.metainfo.xml
+
+ # remove plugins packaged separately
+ rm -rf "$pkgdir"/usr/share/cockpit/{apps,packagekit,pcp,storaged} \
+ "$pkgdir"/usr/share/metainfo/org.cockpit-project.cockpit-storaged.metainfo.xml \
+ "$pkgdir"/usr/lib/cockpit/cockpit-pcp \
+ "$pkgdir"/var/lib/pcp
+
+ # Disallow root login by default
+ printf "# List of users which are not allowed to login to Cockpit\nroot\n" > "$pkgdir"/etc/cockpit/disallowed-users
+ chmod 644 "$pkgdir"/etc/cockpit/disallowed-users
+}
+
+package_cockpit-test() {
+ pkgdesc='Cockpit test playground'
+ depends=(cockpit)
+
+ # Install test
+ cd cockpit-$pkgver
+ make DESTDIR="$pkgdir" install-tests
+ cd ..
+
+ _do_package_component playground
+}
+
+package_cockpit-pcp() {
+ pkgdesc='Cockpit support for reading PCP metrics and loading PCP archives'
+ depends=(cockpit pcp)
+
+ cd cockpit-$pkgver
+ make DESTDIR="$pkgdir"/tmp install
+
+ cd "$pkgdir"/tmp
+ bsdtar -cf - usr/share/cockpit/pcp usr/lib/cockpit/cockpit-pcp var/lib/pcp \
+ | bsdtar -xf - -C "$pkgdir"
+ rm -rf "$pkgdir"/tmp
+}
+
+_do_package_component() {
+ _component="${1:-${pkgname#cockpit-}}"
+
+ cd "$srcdir"/cockpit-$pkgver
+ make DESTDIR="$pkgdir"/tmp install
+
+ cd "$pkgdir"/tmp
+ bsdtar -cf - usr/share/cockpit/$_component \
+ | bsdtar -xf - -C "$pkgdir"
+
+ [ -f usr/share/metainfo/org.cockpit-project.$pkgname.metainfo.xml ] && \
+ install -Dm644 usr/share/metainfo/org.cockpit-project.$pkgname.metainfo.xml \
+ "$pkgdir"/usr/share/metainfo/org.cockpit-project.$pkgname.metainfo.xml
+
+ rm -rf "$pkgdir"/tmp
+}
+
+package_cockpit-storaged() {
+ pkgdesc='Cockpit user interface for storage, using udisks'
+ depends=(cockpit udisks2 dbus-python)
+ optdepends=(
+ "clevis: manage disk encryption"
+ )
+ _do_package_component
+}
+
+package_cockpit-packagekit() {
+ pkgdesc='Cockpit user interface for packages'
+ depends=(cockpit polkit packagekit python)
+ _do_package_component
+ _do_package_component apps
+}
diff --git a/tools/arch/cockpit-ws.sysuser.conf b/tools/arch/cockpit-ws.sysuser.conf
new file mode 100644
index 0000000..3ff838c
--- /dev/null
+++ b/tools/arch/cockpit-ws.sysuser.conf
@@ -0,0 +1 @@
+u cockpit-ws - "User for cockpit web service"
diff --git a/tools/arch/cockpit-wsinstance.sysuser.conf b/tools/arch/cockpit-wsinstance.sysuser.conf
new file mode 100644
index 0000000..ec8be39
--- /dev/null
+++ b/tools/arch/cockpit-wsinstance.sysuser.conf
@@ -0,0 +1 @@
+u cockpit-wsinstance - "User for cockpit-ws instances"
diff --git a/tools/arch/cockpit.pam b/tools/arch/cockpit.pam
new file mode 100644
index 0000000..eaeef60
--- /dev/null
+++ b/tools/arch/cockpit.pam
@@ -0,0 +1,7 @@
+#%PAM-1.0
+auth include system-remote-login
+# List of users to deny access to Cockpit, by default root is included.
+auth required pam_listfile.so item=user sense=deny file=/etc/cockpit/disallowed-users onerr=succeed
+account include system-remote-login
+password include system-remote-login
+session include system-remote-login
diff --git a/tools/build-debian-copyright b/tools/build-debian-copyright
new file mode 100755
index 0000000..bd75adb
--- /dev/null
+++ b/tools/build-debian-copyright
@@ -0,0 +1,145 @@
+#!/usr/bin/python3
+# generate debian/copyright from debian/copyright.template and node_modules
+# Author: Martin Pitt <mpitt@debian.org>
+# Allison Karlitskaya <allison.karlitskaya@redhat.com>
+
+import argparse
+import gzip
+import os
+import re
+import sys
+import time
+from typing import Dict, Set
+
+BASE_DIR = os.path.realpath(f'{__file__}/../..')
+TEMPLATE_FILE = f'{BASE_DIR}/tools/debian/copyright.template'
+
+
+own_copyright = f"Copyright (C) 2013 - {time.strftime('%Y')} Red Hat, Inc."
+
+license_patterns = {
+ # Common patterns
+ r'\bMIT\b': ['MIT'],
+
+ # https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ r'\bfocus-trap\b': ['MIT'],
+}
+
+copyright_patterns = {
+ # Common patterns
+ r'Copyright (.*)$': [r'\1'],
+ r'@copyright (.*)$': [r'\1'],
+ r'\(c\) (.*)$': [r'\1'],
+
+ # https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ r'\bfocus-trap\b': ['2015-2016 David Clark'],
+}
+
+used_patterns = set()
+
+
+def parse_args():
+ p = argparse.ArgumentParser(description='Generate debian/copyright file from template and node_modules')
+ return p.parse_args()
+
+
+def template_licenses(template):
+ """Return set of existing License: short names"""
+
+ ids = set()
+ for line in template.splitlines():
+ if line.startswith('License:'):
+ ids.add(line.split(None, 1)[1].lower())
+ return ids
+
+
+def find_patterns(patterns, text):
+ results = set()
+
+ for pattern, templates in patterns.items():
+ for match in re.finditer(pattern, text, re.MULTILINE):
+ used_patterns.add(pattern)
+ results.update(match.expand(template) for template in templates)
+
+ return results
+
+#
+# main
+#
+
+
+args = parse_args()
+
+with open(TEMPLATE_FILE, encoding='UTF-8') as f:
+ template = f.read()
+
+license_ids = template_licenses(template)
+
+# scan dist/ bundles for third-party copyrights and licenses
+
+dist_copyrights: Dict[str, Set[str]] = {} # Files: dirglob → set(copyrights)
+dist_licenses: Dict[str, Set[str]] = {} # Files: dirglob → set(licenses)
+
+for directory, _subdirs, files in os.walk(f'{BASE_DIR}/dist'):
+ for file in files:
+ if '.LEGAL.txt' not in file:
+ continue
+
+ full_filename = os.path.join(directory, file)
+ directory_glob = os.path.relpath(directory, start=BASE_DIR) + '/*'
+
+ if file.endswith('.gz'):
+ with gzip.open(full_filename, 'rt') as license_file_gz:
+ contents = license_file_gz.read()
+ else:
+ with open(full_filename, 'rt') as license_file:
+ contents = license_file.read()
+
+ for comment in contents.split('\n\n'):
+ if (comment.strip() == "" or "Bundled license information:" in comment):
+ continue
+
+ licenses = find_patterns(license_patterns, comment)
+ if not licenses:
+ raise SystemError('Can not determine licenses of:\n%s' % comment)
+ for license_id in licenses:
+ if license_id.lower() not in license_ids:
+ raise KeyError('License {license_id} not found in {TEMPLATE_FILE}')
+
+ # All bundles also contain our own code
+ licenses.add("LGPL-2.1-or-later")
+
+ dist_licenses.setdefault(directory_glob, set()).update(licenses)
+
+ copyrights = find_patterns(copyright_patterns, comment)
+ if not copyrights:
+ raise SystemError('Did not find any copyrights in:\n%s' % comment)
+
+ # All bundles also contain our own code
+ copyrights.add(own_copyright)
+
+ dist_copyrights.setdefault(directory_glob, set()).update(copyrights)
+
+for pattern in set.union(set(license_patterns), set(copyright_patterns)):
+ if pattern not in used_patterns:
+ # We'll have no LEGAL.txt files in that dev builds
+ # so of course we won't use any of the patterns
+ if os.getenv('NODE_ENV') == 'development' or os.getenv('IGNORE_UNUSED_PATTERNS'):
+ continue
+
+ sys.exit(f'build-debian-copyright: Unused pattern: {pattern}')
+
+paragraphs = []
+for dirglob in sorted(dist_copyrights):
+ paragraphs.append("Files: {0}\nCopyright: {1}\nLicense: {2}".format(
+ dirglob,
+ '\n '.join(sorted(dist_copyrights[dirglob])),
+ ' and '.join(sorted(dist_licenses[dirglob]))))
+
+# force UTF-8 output, even when running in C locale
+for line in template.splitlines():
+ if '#NPM' in line:
+ sys.stdout.buffer.write('\n\n'.join(paragraphs).encode('UTF-8'))
+ else:
+ sys.stdout.buffer.write(line.encode('UTF-8'))
+ sys.stdout.buffer.write(b'\n')
diff --git a/tools/cockpit.debian.pam b/tools/cockpit.debian.pam
new file mode 100644
index 0000000..8e33d4d
--- /dev/null
+++ b/tools/cockpit.debian.pam
@@ -0,0 +1,24 @@
+#%PAM-1.0
+auth required pam_sepermit.so
+auth substack common-auth
+auth optional pam_ssh_add.so
+# List of users to deny access to Cockpit, by default root is included.
+auth required pam_listfile.so item=user sense=deny file=/etc/cockpit/disallowed-users onerr=succeed
+account required pam_nologin.so
+account include common-account
+password include common-password
+# pam_selinux.so close should be the first session rule
+session required pam_selinux.so close
+session required pam_loginuid.so
+# pam_selinux.so open should only be followed by sessions to be executed in the user context
+session required pam_selinux.so open env_params
+session optional pam_keyinit.so force revoke
+session optional pam_ssh_add.so
+session include common-session
+
+# Read environment variables from /etc/environment and
+# /etc/security/pam_env.conf.
+session required pam_env.so # [1]
+# In Debian 4.0 (etch), locale-related environment variables were moved to
+# /etc/default/locale, so read that as well.
+session required pam_env.so user_readenv=1 envfile=/etc/default/locale
diff --git a/tools/cockpit.pam b/tools/cockpit.pam
new file mode 100644
index 0000000..1cf2c9f
--- /dev/null
+++ b/tools/cockpit.pam
@@ -0,0 +1,19 @@
+#%PAM-1.0
+auth required pam_sepermit.so
+auth substack password-auth
+auth include postlogin
+auth optional pam_ssh_add.so
+# List of users to deny access to Cockpit, by default root is included.
+auth required pam_listfile.so item=user sense=deny file=/etc/cockpit/disallowed-users onerr=succeed
+account required pam_nologin.so
+account include password-auth
+password include password-auth
+# pam_selinux.so close should be the first session rule
+session required pam_selinux.so close
+session required pam_loginuid.so
+# pam_selinux.so open should only be followed by sessions to be executed in the user context
+session required pam_selinux.so open env_params
+session optional pam_keyinit.so force revoke
+session optional pam_ssh_add.so
+session include password-auth
+session include postlogin
diff --git a/tools/cockpit.spec b/tools/cockpit.spec
new file mode 100644
index 0000000..2ff03f6
--- /dev/null
+++ b/tools/cockpit.spec
@@ -0,0 +1,814 @@
+#
+# Copyright (C) 2014-2020 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+#
+
+# This file is maintained at the following location:
+# https://github.com/cockpit-project/cockpit/blob/main/tools/cockpit.spec
+#
+# If you are editing this file in another location, changes will likely
+# be clobbered the next time an automated release is done.
+#
+# Check first cockpit-devel@lists.fedorahosted.org
+#
+
+# earliest base that the subpackages work on; this is still required as long as
+# we maintain the basic/optional split, then it can be replaced with just %{version}.
+%define required_base 266
+
+# we generally want CentOS packages to be like RHEL; special cases need to check %{centos} explicitly
+%if 0%{?centos}
+%define rhel %{centos}
+%endif
+
+%define _hardened_build 1
+
+%define __lib lib
+
+%if %{defined _pamdir}
+%define pamdir %{_pamdir}
+%else
+%define pamdir %{_libdir}/security
+%endif
+
+Name: cockpit
+Summary: Web Console for Linux servers
+
+License: LGPL-2.1-or-later
+URL: https://cockpit-project.org/
+
+Version: 311
+Release: 1%{?dist}
+Source0: https://github.com/cockpit-project/cockpit/releases/download/%{version}/cockpit-%{version}.tar.xz
+
+# Don't change the bridge in the RHEL 8; the old SSH breaks some features, see @todoPybridgeRHEL8
+%if 0%{?rhel} == 8 && !%{defined enable_old_bridge}
+%define enable_old_bridge 1
+%endif
+
+# in RHEL 8 the source package is duplicated: cockpit (building basic packages like cockpit-{bridge,system})
+# and cockpit-appstream (building optional packages like cockpit-{pcp})
+# This split does not apply to EPEL/COPR nor packit c8s builds, only to our own
+# image-prepare rhel-8-Y builds (which will disable build_all).
+# In Fedora ELN/RHEL 9+ there is just one source package, which ships rpms in both BaseOS and AppStream
+%define build_all 1
+%if 0%{?rhel} == 8 && 0%{?epel} == 0 && !0%{?build_all}
+
+%if "%{name}" == "cockpit"
+%define build_basic 1
+%define build_optional 0
+%else
+%define build_basic 0
+%define build_optional 1
+%endif
+
+%else
+%define build_basic 1
+%define build_optional 1
+%endif
+
+# Allow root login in Cockpit on RHEL 8 and lower as it also allows password login over SSH.
+%if 0%{?rhel} && 0%{?rhel} <= 8
+%define disallow_root 0
+%else
+%define disallow_root 1
+%endif
+
+# pcp stopped building on ix86
+%define build_pcp 1
+%if 0%{?fedora} >= 40 || 0%{?rhel} >= 10
+%ifarch %ix86
+%define build_pcp 0
+%endif
+%endif
+
+# Ship custom SELinux policy (but not for cockpit-appstream)
+%if "%{name}" == "cockpit"
+%define selinuxtype targeted
+%define selinux_configure_arg --enable-selinux-policy=%{selinuxtype}
+%endif
+
+BuildRequires: gcc
+BuildRequires: pkgconfig(gio-unix-2.0)
+BuildRequires: pkgconfig(json-glib-1.0)
+BuildRequires: pkgconfig(polkit-agent-1) >= 0.105
+BuildRequires: pam-devel
+
+BuildRequires: autoconf automake
+BuildRequires: make
+BuildRequires: python3-devel
+%if 0%{?rhel} && 0%{?rhel} <= 8
+# RHEL 8's gettext does not yet have metainfo.its
+BuildRequires: gettext >= 0.19.7
+BuildRequires: libappstream-glib-devel
+%else
+BuildRequires: gettext >= 0.21
+%endif
+%if 0%{?build_basic}
+BuildRequires: libssh-devel >= 0.8.5
+%endif
+BuildRequires: openssl-devel
+BuildRequires: gnutls-devel >= 3.4.3
+BuildRequires: zlib-devel
+BuildRequires: krb5-devel >= 1.11
+BuildRequires: libxslt-devel
+BuildRequires: glib-networking
+BuildRequires: sed
+
+BuildRequires: glib2-devel >= 2.50.0
+# this is for runtimedir in the tls proxy ace21c8879
+BuildRequires: systemd-devel >= 235
+%if 0%{?suse_version}
+BuildRequires: distribution-release
+%if %{build_pcp}
+BuildRequires: libpcp-devel
+BuildRequires: pcp-devel
+BuildRequires: libpcp3
+BuildRequires: libpcp_import1
+%endif
+BuildRequires: openssh
+BuildRequires: distribution-logos
+BuildRequires: wallpaper-branding
+%else
+%if %{build_pcp}
+BuildRequires: pcp-libs-devel
+%endif
+BuildRequires: openssh-clients
+BuildRequires: docbook-style-xsl
+%endif
+BuildRequires: krb5-server
+BuildRequires: gdb
+
+# For documentation
+BuildRequires: xmlto
+
+BuildRequires: selinux-policy
+BuildRequires: selinux-policy-devel
+
+# This is the "cockpit" metapackage. It should only
+# Require, Suggest or Recommend other cockpit-xxx subpackages
+
+Requires: cockpit-bridge
+Requires: cockpit-ws
+Requires: cockpit-system
+
+# Optional components
+Recommends: (cockpit-storaged if udisks2)
+Recommends: (cockpit-packagekit if dnf)
+Suggests: cockpit-pcp
+
+%if 0%{?rhel} == 0
+Recommends: (cockpit-networkmanager if NetworkManager)
+# c-ostree is not in RHEL 8/9
+Recommends: (cockpit-ostree if rpm-ostree)
+Suggests: cockpit-selinux
+%endif
+%if 0%{?rhel} && 0%{?centos} == 0
+Requires: subscription-manager-cockpit
+%endif
+
+%if 0%{?enable_old_bridge} == 0
+BuildRequires: python3-devel
+BuildRequires: python3-pip
+%if 0%{?rhel} == 0
+# All of these are only required for running pytest (which we only do on Fedora)
+BuildRequires: procps-ng
+BuildRequires: pyproject-rpm-macros
+BuildRequires: python3-pytest-asyncio
+BuildRequires: python3-pytest-cov
+BuildRequires: python3-pytest-timeout
+BuildRequires: python3-tox-current-env
+%endif
+%endif
+
+%prep
+%setup -q -n cockpit-%{version}
+
+%build
+%configure \
+ %{?selinux_configure_arg} \
+ --with-cockpit-user=cockpit-ws \
+ --with-cockpit-ws-instance-user=cockpit-wsinstance \
+%if 0%{?suse_version}
+ --docdir=%_defaultdocdir/%{name} \
+%endif
+ --with-pamdir='%{pamdir}' \
+%if 0%{?enable_old_bridge}
+ --enable-old-bridge \
+%endif
+%if 0%{?build_basic} == 0
+ --disable-ssh \
+%endif
+%if %{build_pcp} == 0
+ --disable-pcp \
+%endif
+
+%make_build
+
+%check
+make -j$(nproc) check
+
+%if 0%{?enable_old_bridge} == 0 && 0%{?rhel} == 0
+%tox
+%endif
+
+%install
+%make_install
+make install-tests DESTDIR=%{buildroot}
+mkdir -p $RPM_BUILD_ROOT%{_sysconfdir}/pam.d
+install -p -m 644 tools/cockpit.pam $RPM_BUILD_ROOT%{_sysconfdir}/pam.d/cockpit
+rm -f %{buildroot}/%{_libdir}/cockpit/*.so
+install -D -p -m 644 AUTHORS COPYING README.md %{buildroot}%{_docdir}/cockpit/
+
+# Build the package lists for resource packages
+# cockpit-bridge is the basic dependency for all cockpit-* packages, so centrally own the page directory
+echo '%dir %{_datadir}/cockpit' > base.list
+echo '%dir %{_datadir}/cockpit/base1' >> base.list
+find %{buildroot}%{_datadir}/cockpit/base1 -type f -o -type l >> base.list
+echo '%{_sysconfdir}/cockpit/machines.d' >> base.list
+echo %{buildroot}%{_datadir}/polkit-1/actions/org.cockpit-project.cockpit-bridge.policy >> base.list
+%if 0%{?enable_old_bridge} && 0%{?build_basic}
+echo '%dir %{_datadir}/cockpit/ssh' >> base.list
+find %{buildroot}%{_datadir}/cockpit/ssh -type f >> base.list
+%endif
+echo '%{_libexecdir}/cockpit-ssh' >> base.list
+
+%if %{build_pcp}
+echo '%dir %{_datadir}/cockpit/pcp' > pcp.list
+find %{buildroot}%{_datadir}/cockpit/pcp -type f >> pcp.list
+%endif
+
+echo '%dir %{_datadir}/cockpit/shell' >> system.list
+find %{buildroot}%{_datadir}/cockpit/shell -type f >> system.list
+
+echo '%dir %{_datadir}/cockpit/systemd' >> system.list
+find %{buildroot}%{_datadir}/cockpit/systemd -type f >> system.list
+
+echo '%dir %{_datadir}/cockpit/users' >> system.list
+find %{buildroot}%{_datadir}/cockpit/users -type f >> system.list
+
+echo '%dir %{_datadir}/cockpit/metrics' >> system.list
+find %{buildroot}%{_datadir}/cockpit/metrics -type f >> system.list
+
+echo '%dir %{_datadir}/cockpit/kdump' > kdump.list
+find %{buildroot}%{_datadir}/cockpit/kdump -type f >> kdump.list
+
+echo '%dir %{_datadir}/cockpit/sosreport' > sosreport.list
+find %{buildroot}%{_datadir}/cockpit/sosreport -type f >> sosreport.list
+
+echo '%dir %{_datadir}/cockpit/storaged' > storaged.list
+find %{buildroot}%{_datadir}/cockpit/storaged -type f >> storaged.list
+
+echo '%dir %{_datadir}/cockpit/networkmanager' > networkmanager.list
+find %{buildroot}%{_datadir}/cockpit/networkmanager -type f >> networkmanager.list
+
+echo '%dir %{_datadir}/cockpit/packagekit' > packagekit.list
+find %{buildroot}%{_datadir}/cockpit/packagekit -type f >> packagekit.list
+
+echo '%dir %{_datadir}/cockpit/apps' >> packagekit.list
+find %{buildroot}%{_datadir}/cockpit/apps -type f >> packagekit.list
+
+echo '%dir %{_datadir}/cockpit/selinux' > selinux.list
+find %{buildroot}%{_datadir}/cockpit/selinux -type f >> selinux.list
+
+echo '%dir %{_datadir}/cockpit/playground' > tests.list
+find %{buildroot}%{_datadir}/cockpit/playground -type f >> tests.list
+
+echo '%dir %{_datadir}/cockpit/static' > static.list
+echo '%dir %{_datadir}/cockpit/static/fonts' >> static.list
+find %{buildroot}%{_datadir}/cockpit/static -type f >> static.list
+
+# when not building basic packages, remove their files
+%if 0%{?build_basic} == 0
+for pkg in base1 branding motd kdump networkmanager selinux shell sosreport static systemd users metrics; do
+ rm -r %{buildroot}/%{_datadir}/cockpit/$pkg
+ rm -f %{buildroot}/%{_datadir}/metainfo/org.cockpit-project.cockpit-${pkg}.metainfo.xml
+done
+for data in doc man pixmaps polkit-1; do
+ rm -r %{buildroot}/%{_datadir}/$data
+done
+rm -r %{buildroot}/%{_prefix}/%{__lib}/tmpfiles.d
+find %{buildroot}/%{_unitdir}/ -type f ! -name 'cockpit-session*' -delete
+for libexec in cockpit-askpass cockpit-session cockpit-ws cockpit-tls cockpit-wsinstance-factory cockpit-client cockpit-client.ui cockpit-desktop cockpit-certificate-helper cockpit-certificate-ensure; do
+ rm -f %{buildroot}/%{_libexecdir}/$libexec
+done
+rm -r %{buildroot}/%{_sysconfdir}/pam.d %{buildroot}/%{_sysconfdir}/motd.d %{buildroot}/%{_sysconfdir}/issue.d
+rm -f %{buildroot}/%{_libdir}/security/pam_*
+rm -f %{buildroot}/usr/bin/cockpit-bridge
+rm -f %{buildroot}%{_libexecdir}/cockpit-ssh
+rm -f %{buildroot}%{_datadir}/metainfo/cockpit.appdata.xml
+rm -rf %{buildroot}%{python3_sitelib}/cockpit*
+%endif
+
+# when not building optional packages, remove their files
+%if 0%{?build_optional} == 0
+for pkg in apps packagekit pcp playground storaged; do
+ rm -rf %{buildroot}/%{_datadir}/cockpit/$pkg
+done
+# files from -tests
+rm -f %{buildroot}/%{pamdir}/mock-pam-conv-mod.so
+rm -f %{buildroot}/%{_unitdir}/cockpit-session.socket
+rm -f %{buildroot}/%{_unitdir}/cockpit-session@.service
+# files from -pcp
+rm -r %{buildroot}/%{_libexecdir}/cockpit-pcp %{buildroot}/%{_localstatedir}/lib/pcp/
+# files from -storaged
+rm -f %{buildroot}/%{_prefix}/share/metainfo/org.cockpit-project.cockpit-storaged.metainfo.xml
+%endif
+
+sed -i "s|%{buildroot}||" *.list
+
+%if ! 0%{?suse_version}
+%global _debugsource_packages 1
+%global _debuginfo_subpackages 0
+
+%define find_debug_info %{_rpmconfigdir}/find-debuginfo.sh %{?_missing_build_ids_terminate_build:--strict-build-id} %{?_include_minidebuginfo:-m} %{?_find_debuginfo_dwz_opts} %{?_find_debuginfo_opts} %{?_debugsource_packages:-S debugsourcefiles.list} "%{_builddir}/%{?buildsubdir}"
+
+%endif
+# /suse_version
+rm -rf %{buildroot}/usr/src/debug
+
+# On RHEL kdump, networkmanager, selinux, and sosreport are part of the system package
+%if 0%{?rhel}
+cat kdump.list sosreport.list networkmanager.list selinux.list >> system.list
+rm -f %{buildroot}%{_datadir}/metainfo/org.cockpit-project.cockpit-sosreport.metainfo.xml
+rm -f %{buildroot}%{_datadir}/metainfo/org.cockpit-project.cockpit-kdump.metainfo.xml
+rm -f %{buildroot}%{_datadir}/metainfo/org.cockpit-project.cockpit-selinux.metainfo.xml
+rm -f %{buildroot}%{_datadir}/metainfo/org.cockpit-project.cockpit-networkmanager.metainfo.xml
+rm -f %{buildroot}%{_datadir}/pixmaps/cockpit-sosreport.png
+%endif
+
+# -------------------------------------------------------------------------------
+# Basic Sub-packages
+
+%if 0%{?build_basic}
+
+%description
+The Cockpit Web Console enables users to administer GNU/Linux servers using a
+web browser.
+
+It offers network configuration, log inspection, diagnostic reports, SELinux
+troubleshooting, interactive command-line sessions, and more.
+
+%files
+%{_docdir}/cockpit/AUTHORS
+%{_docdir}/cockpit/COPYING
+%{_docdir}/cockpit/README.md
+%{_datadir}/metainfo/cockpit.appdata.xml
+%{_datadir}/pixmaps/cockpit.png
+%doc %{_mandir}/man1/cockpit.1.gz
+
+
+%package bridge
+Summary: Cockpit bridge server-side component
+Requires: glib-networking
+Provides: cockpit-ssh = %{version}-%{release}
+# 233 dropped jquery.js, pages started to bundle it (commit 049e8b8dce)
+Conflicts: cockpit-dashboard < 233
+Conflicts: cockpit-networkmanager < 233
+Conflicts: cockpit-storaged < 233
+Conflicts: cockpit-system < 233
+Conflicts: cockpit-tests < 233
+Conflicts: cockpit-docker < 233
+
+%description bridge
+The Cockpit bridge component installed server side and runs commands on the
+system on behalf of the web based user interface.
+
+%files bridge -f base.list
+%doc %{_mandir}/man1/cockpit-bridge.1.gz
+%{_bindir}/cockpit-bridge
+%{_libexecdir}/cockpit-askpass
+%if 0%{?enable_old_bridge} == 0
+%{python3_sitelib}/%{name}*
+%endif
+
+%package doc
+Summary: Cockpit deployment and developer guide
+BuildArch: noarch
+
+%description doc
+The Cockpit Deployment and Developer Guide shows sysadmins how to
+deploy Cockpit on their machines as well as helps developers who want to
+embed or extend Cockpit.
+
+%files doc
+%exclude %{_docdir}/cockpit/AUTHORS
+%exclude %{_docdir}/cockpit/COPYING
+%exclude %{_docdir}/cockpit/README.md
+%{_docdir}/cockpit
+
+%package system
+Summary: Cockpit admin interface package for configuring and troubleshooting a system
+BuildArch: noarch
+Requires: cockpit-bridge >= %{version}-%{release}
+%if !0%{?suse_version}
+Requires: shadow-utils
+%endif
+Requires: grep
+Requires: /usr/bin/pwscore
+Requires: /usr/bin/date
+Provides: cockpit-shell = %{version}-%{release}
+Provides: cockpit-systemd = %{version}-%{release}
+Provides: cockpit-tuned = %{version}-%{release}
+Provides: cockpit-users = %{version}-%{release}
+Obsoletes: cockpit-dashboard < %{version}-%{release}
+%if 0%{?rhel}
+Requires: NetworkManager >= 1.6
+Requires: kexec-tools
+Requires: sos
+Requires: sudo
+Recommends: PackageKit
+Recommends: setroubleshoot-server >= 3.3.3
+Suggests: NetworkManager-team
+Provides: cockpit-kdump = %{version}-%{release}
+Provides: cockpit-networkmanager = %{version}-%{release}
+Provides: cockpit-selinux = %{version}-%{release}
+Provides: cockpit-sosreport = %{version}-%{release}
+%endif
+%if 0%{?fedora}
+Recommends: (reportd if abrt)
+%endif
+
+Provides: bundled(npm(@patternfly/patternfly)) = 5.1.0
+Provides: bundled(npm(@patternfly/react-core)) = 5.1.2
+Provides: bundled(npm(@patternfly/react-icons)) = 5.1.2
+Provides: bundled(npm(@patternfly/react-styles)) = 5.1.2
+Provides: bundled(npm(@patternfly/react-table)) = 5.1.2
+Provides: bundled(npm(@patternfly/react-tokens)) = 5.1.2
+Provides: bundled(npm(argparse)) = 1.0.10
+Provides: bundled(npm(array-buffer-byte-length)) = 1.0.1
+Provides: bundled(npm(attr-accept)) = 2.2.2
+Provides: bundled(npm(autolinker)) = 3.16.2
+Provides: bundled(npm(available-typed-arrays)) = 1.0.6
+Provides: bundled(npm(call-bind)) = 1.0.7
+Provides: bundled(npm(date-fns)) = 3.3.1
+Provides: bundled(npm(deep-equal)) = 2.2.3
+Provides: bundled(npm(define-data-property)) = 1.1.4
+Provides: bundled(npm(define-properties)) = 1.2.1
+Provides: bundled(npm(es-define-property)) = 1.0.0
+Provides: bundled(npm(es-errors)) = 1.3.0
+Provides: bundled(npm(es-get-iterator)) = 1.1.3
+Provides: bundled(npm(file-selector)) = 0.6.0
+Provides: bundled(npm(focus-trap)) = 7.5.2
+Provides: bundled(npm(for-each)) = 0.3.3
+Provides: bundled(npm(function-bind)) = 1.1.2
+Provides: bundled(npm(functions-have-names)) = 1.2.3
+Provides: bundled(npm(get-intrinsic)) = 1.2.4
+Provides: bundled(npm(gopd)) = 1.0.1
+Provides: bundled(npm(has-bigints)) = 1.0.2
+Provides: bundled(npm(has-property-descriptors)) = 1.0.2
+Provides: bundled(npm(has-proto)) = 1.0.1
+Provides: bundled(npm(has-symbols)) = 1.0.3
+Provides: bundled(npm(has-tostringtag)) = 1.0.2
+Provides: bundled(npm(hasown)) = 2.0.1
+Provides: bundled(npm(internal-slot)) = 1.0.7
+Provides: bundled(npm(is-arguments)) = 1.1.1
+Provides: bundled(npm(is-array-buffer)) = 3.0.4
+Provides: bundled(npm(is-bigint)) = 1.0.4
+Provides: bundled(npm(is-boolean-object)) = 1.1.2
+Provides: bundled(npm(is-callable)) = 1.2.7
+Provides: bundled(npm(is-date-object)) = 1.0.5
+Provides: bundled(npm(is-map)) = 2.0.2
+Provides: bundled(npm(is-number-object)) = 1.0.7
+Provides: bundled(npm(is-regex)) = 1.1.4
+Provides: bundled(npm(is-set)) = 2.0.2
+Provides: bundled(npm(is-shared-array-buffer)) = 1.0.2
+Provides: bundled(npm(is-string)) = 1.0.7
+Provides: bundled(npm(is-symbol)) = 1.0.4
+Provides: bundled(npm(is-weakmap)) = 2.0.1
+Provides: bundled(npm(is-weakset)) = 2.0.2
+Provides: bundled(npm(isarray)) = 2.0.5
+Provides: bundled(npm(js-sha1)) = 0.7.0
+Provides: bundled(npm(js-sha256)) = 0.11.0
+Provides: bundled(npm(js-tokens)) = 4.0.0
+Provides: bundled(npm(json-stable-stringify-without-jsonify)) = 1.0.1
+Provides: bundled(npm(lodash)) = 4.17.21
+Provides: bundled(npm(loose-envify)) = 1.4.0
+Provides: bundled(npm(object-assign)) = 4.1.1
+Provides: bundled(npm(object-inspect)) = 1.13.1
+Provides: bundled(npm(object-is)) = 1.1.5
+Provides: bundled(npm(object-keys)) = 1.1.1
+Provides: bundled(npm(object.assign)) = 4.1.5
+Provides: bundled(npm(prop-types)) = 15.8.1
+Provides: bundled(npm(react-dom)) = 18.2.0
+Provides: bundled(npm(react-dropzone)) = 14.2.3
+Provides: bundled(npm(react-is)) = 16.13.1
+Provides: bundled(npm(react)) = 18.2.0
+Provides: bundled(npm(regexp.prototype.flags)) = 1.5.2
+Provides: bundled(npm(remarkable)) = 2.0.1
+Provides: bundled(npm(scheduler)) = 0.23.0
+Provides: bundled(npm(set-function-length)) = 1.2.1
+Provides: bundled(npm(set-function-name)) = 2.0.1
+Provides: bundled(npm(side-channel)) = 1.0.5
+Provides: bundled(npm(sprintf-js)) = 1.0.3
+Provides: bundled(npm(stop-iteration-iterator)) = 1.0.0
+Provides: bundled(npm(tabbable)) = 6.2.0
+Provides: bundled(npm(throttle-debounce)) = 5.0.0
+Provides: bundled(npm(tslib)) = 2.6.2
+Provides: bundled(npm(uuid)) = 9.0.1
+Provides: bundled(npm(which-boxed-primitive)) = 1.0.2
+Provides: bundled(npm(which-collection)) = 1.0.1
+Provides: bundled(npm(which-typed-array)) = 1.1.14
+Provides: bundled(npm(xterm-addon-canvas)) = 0.5.0
+Provides: bundled(npm(xterm)) = 5.3.0
+
+%description system
+This package contains the Cockpit shell and system configuration interfaces.
+
+%files system -f system.list
+%dir %{_datadir}/cockpit/shell/images
+
+%package ws
+Summary: Cockpit Web Service
+Requires: glib-networking
+Requires: openssl
+Requires: glib2 >= 2.50.0
+Requires: (selinux-policy >= %{_selinux_policy_version} if selinux-policy-%{selinuxtype})
+Requires(post): (policycoreutils if selinux-policy-%{selinuxtype})
+Conflicts: firewalld < 0.6.0-1
+Recommends: sscg >= 2.3
+Recommends: system-logos
+Suggests: sssd-dbus >= 2.6.2
+# for cockpit-desktop
+Suggests: python3
+
+# prevent hard python3 dependency for cockpit-desktop, it falls back to other browsers
+%global __requires_exclude_from ^%{_libexecdir}/cockpit-client$
+
+%description ws
+The Cockpit Web Service listens on the network, and authenticates users.
+
+If sssd-dbus is installed, you can enable client certificate/smart card
+authentication via sssd/FreeIPA.
+
+%files ws -f static.list
+%doc %{_mandir}/man1/cockpit-desktop.1.gz
+%doc %{_mandir}/man5/cockpit.conf.5.gz
+%doc %{_mandir}/man8/cockpit-ws.8.gz
+%doc %{_mandir}/man8/cockpit-tls.8.gz
+%doc %{_mandir}/man8/pam_ssh_add.8.gz
+%dir %{_sysconfdir}/cockpit
+%config(noreplace) %{_sysconfdir}/cockpit/ws-certs.d
+%config(noreplace) %{_sysconfdir}/pam.d/cockpit
+# created in %post, so that users can rm the files
+%ghost %{_sysconfdir}/issue.d/cockpit.issue
+%ghost %{_sysconfdir}/motd.d/cockpit
+%ghost %attr(0644, root, root) %{_sysconfdir}/cockpit/disallowed-users
+%dir %{_datadir}/cockpit/motd
+%{_datadir}/cockpit/motd/update-motd
+%{_datadir}/cockpit/motd/inactive.motd
+%{_unitdir}/cockpit.service
+%{_unitdir}/cockpit-motd.service
+%{_unitdir}/cockpit.socket
+%{_unitdir}/cockpit-wsinstance-http.socket
+%{_unitdir}/cockpit-wsinstance-http.service
+%{_unitdir}/cockpit-wsinstance-https-factory.socket
+%{_unitdir}/cockpit-wsinstance-https-factory@.service
+%{_unitdir}/cockpit-wsinstance-https@.socket
+%{_unitdir}/cockpit-wsinstance-https@.service
+%{_unitdir}/system-cockpithttps.slice
+%{_prefix}/%{__lib}/tmpfiles.d/cockpit-tempfiles.conf
+%{pamdir}/pam_ssh_add.so
+%{pamdir}/pam_cockpit_cert.so
+%{_libexecdir}/cockpit-ws
+%{_libexecdir}/cockpit-wsinstance-factory
+%{_libexecdir}/cockpit-tls
+%{_libexecdir}/cockpit-client
+%{_libexecdir}/cockpit-client.ui
+%{_libexecdir}/cockpit-desktop
+%{_libexecdir}/cockpit-certificate-ensure
+%{_libexecdir}/cockpit-certificate-helper
+%attr(4750, root, cockpit-wsinstance) %{_libexecdir}/cockpit-session
+%{_datadir}/cockpit/branding
+%{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.bz2
+%{_mandir}/man8/%{name}_session_selinux.8cockpit.*
+%{_mandir}/man8/%{name}_ws_selinux.8cockpit.*
+%ghost %{_sharedstatedir}/selinux/%{selinuxtype}/active/modules/200/%{name}
+
+%pre ws
+getent group cockpit-ws >/dev/null || groupadd -r cockpit-ws
+getent passwd cockpit-ws >/dev/null || useradd -r -g cockpit-ws -d /nonexisting -s /sbin/nologin -c "User for cockpit web service" cockpit-ws
+getent group cockpit-wsinstance >/dev/null || groupadd -r cockpit-wsinstance
+getent passwd cockpit-wsinstance >/dev/null || useradd -r -g cockpit-wsinstance -d /nonexisting -s /sbin/nologin -c "User for cockpit-ws instances" cockpit-wsinstance
+
+if %{_sbindir}/selinuxenabled 2>/dev/null; then
+ %selinux_relabel_pre -s %{selinuxtype}
+fi
+
+%post ws
+if [ -x %{_sbindir}/selinuxenabled ]; then
+ %selinux_modules_install -s %{selinuxtype} %{_datadir}/selinux/packages/%{selinuxtype}/%{name}.pp.bz2
+ %selinux_relabel_post -s %{selinuxtype}
+fi
+
+# set up dynamic motd/issue symlinks on first-time install; don't bring them back on upgrades if admin removed them
+# disable root login on first-time install; so existing installations aren't changed
+if [ "$1" = 1 ]; then
+ mkdir -p /etc/motd.d /etc/issue.d
+ ln -s ../../run/cockpit/motd /etc/motd.d/cockpit
+ ln -s ../../run/cockpit/motd /etc/issue.d/cockpit.issue
+ printf "# List of users which are not allowed to login to Cockpit\n" > /etc/cockpit/disallowed-users
+%if 0%{?disallow_root}
+ printf "root\n" >> /etc/cockpit/disallowed-users
+%endif
+ chmod 644 /etc/cockpit/disallowed-users
+fi
+
+%tmpfiles_create cockpit-tempfiles.conf
+%systemd_post cockpit.socket cockpit.service
+# firewalld only partially picks up changes to its services files without this
+test -f %{_bindir}/firewall-cmd && firewall-cmd --reload --quiet || true
+
+# check for deprecated PAM config
+if test -f %{_sysconfdir}/pam.d/cockpit && grep -q pam_cockpit_cert %{_sysconfdir}/pam.d/cockpit; then
+ echo '**** WARNING:'
+ echo '**** WARNING: pam_cockpit_cert is a no-op and will be removed in a'
+ echo '**** WARNING: future release; remove it from your /etc/pam.d/cockpit.'
+ echo '**** WARNING:'
+fi
+
+%preun ws
+%systemd_preun cockpit.socket cockpit.service
+
+%postun ws
+if [ -x %{_sbindir}/selinuxenabled ]; then
+ %selinux_modules_uninstall -s %{selinuxtype} %{name}
+ %selinux_relabel_post -s %{selinuxtype}
+fi
+%systemd_postun_with_restart cockpit.socket cockpit.service
+
+# -------------------------------------------------------------------------------
+# Sub-packages that are part of cockpit-system in RHEL/CentOS, but separate in Fedora
+
+%if 0%{?rhel} == 0
+
+%package kdump
+Summary: Cockpit user interface for kernel crash dumping
+Requires: cockpit-bridge >= %{required_base}
+Requires: cockpit-shell >= %{required_base}
+Requires: kexec-tools
+BuildArch: noarch
+
+%description kdump
+The Cockpit component for configuring kernel crash dumping.
+
+%files kdump -f kdump.list
+%{_datadir}/metainfo/org.cockpit-project.cockpit-kdump.metainfo.xml
+
+%package sosreport
+Summary: Cockpit user interface for diagnostic reports
+Requires: cockpit-bridge >= %{required_base}
+Requires: cockpit-shell >= %{required_base}
+Requires: sos
+BuildArch: noarch
+
+%description sosreport
+The Cockpit component for creating diagnostic reports with the
+sosreport tool.
+
+%files sosreport -f sosreport.list
+%{_datadir}/metainfo/org.cockpit-project.cockpit-sosreport.metainfo.xml
+%{_datadir}/pixmaps/cockpit-sosreport.png
+
+%package networkmanager
+Summary: Cockpit user interface for networking, using NetworkManager
+Requires: cockpit-bridge >= %{required_base}
+Requires: cockpit-shell >= %{required_base}
+Requires: NetworkManager >= 1.6
+# Optional components
+Recommends: NetworkManager-team
+BuildArch: noarch
+
+%description networkmanager
+The Cockpit component for managing networking. This package uses NetworkManager.
+
+%files networkmanager -f networkmanager.list
+%{_datadir}/metainfo/org.cockpit-project.cockpit-networkmanager.metainfo.xml
+
+%endif
+
+%if 0%{?rhel} == 0
+
+%package selinux
+Summary: Cockpit SELinux package
+Requires: cockpit-bridge >= %{required_base}
+Requires: cockpit-shell >= %{required_base}
+Requires: setroubleshoot-server >= 3.3.3
+BuildArch: noarch
+
+%description selinux
+This package contains the Cockpit user interface integration with the
+utility setroubleshoot to diagnose and resolve SELinux issues.
+
+%files selinux -f selinux.list
+%{_datadir}/metainfo/org.cockpit-project.cockpit-selinux.metainfo.xml
+
+%endif
+
+#/ build basic packages
+%else
+
+# RPM requires this
+%description
+Dummy package from building optional packages only; never install or publish me.
+
+#/ build basic packages
+%endif
+
+# -------------------------------------------------------------------------------
+# Sub-packages that are optional extensions
+
+%if 0%{?build_optional}
+
+%package -n cockpit-storaged
+Summary: Cockpit user interface for storage, using udisks
+Requires: cockpit-shell >= %{required_base}
+Requires: udisks2 >= 2.9
+Recommends: udisks2-lvm2 >= 2.9
+Recommends: udisks2-iscsi >= 2.9
+%if ! 0%{?rhel}
+Recommends: udisks2-btrfs >= 2.9
+%endif
+Recommends: device-mapper-multipath
+Recommends: clevis-luks
+Requires: %{__python3}
+%if 0%{?suse_version}
+Requires: python3-dbus-python
+%else
+Requires: python3-dbus
+%endif
+BuildArch: noarch
+
+%description -n cockpit-storaged
+The Cockpit component for managing storage. This package uses udisks.
+
+%files -n cockpit-storaged -f storaged.list
+%{_datadir}/metainfo/org.cockpit-project.cockpit-storaged.metainfo.xml
+
+%package -n cockpit-tests
+Summary: Tests for Cockpit
+Requires: cockpit-bridge >= %{required_base}
+Requires: cockpit-system >= %{required_base}
+Requires: openssh-clients
+Provides: cockpit-test-assets = %{version}-%{release}
+
+%description -n cockpit-tests
+This package contains tests and files used while testing Cockpit.
+These files are not required for running Cockpit.
+
+%files -n cockpit-tests -f tests.list
+%{pamdir}/mock-pam-conv-mod.so
+%{_unitdir}/cockpit-session.socket
+%{_unitdir}/cockpit-session@.service
+
+%if %{build_pcp}
+
+%package -n cockpit-pcp
+Summary: Cockpit PCP integration
+Requires: cockpit-bridge >= %{required_base}
+Requires: pcp
+
+%description -n cockpit-pcp
+Cockpit support for reading PCP metrics and loading PCP archives.
+
+%files -n cockpit-pcp -f pcp.list
+%{_libexecdir}/cockpit-pcp
+%{_localstatedir}/lib/pcp/config/pmlogconf/tools/cockpit
+
+%post -n cockpit-pcp
+systemctl reload-or-try-restart pmlogger
+
+%endif
+
+%package -n cockpit-packagekit
+Summary: Cockpit user interface for packages
+BuildArch: noarch
+Requires: cockpit-bridge >= %{required_base}
+Requires: PackageKit
+Recommends: python3-tracer
+# HACK: https://bugzilla.redhat.com/show_bug.cgi?id=1800468
+Requires: polkit
+
+%description -n cockpit-packagekit
+The Cockpit components for installing OS updates and Cockpit add-ons,
+via PackageKit.
+
+%files -n cockpit-packagekit -f packagekit.list
+
+#/ build optional extension packages
+%endif
+
+# The changelog is automatically generated and merged
+%changelog
diff --git a/tools/compile b/tools/compile
new file mode 100755
index 0000000..df363c8
--- /dev/null
+++ b/tools/compile
@@ -0,0 +1,348 @@
+#! /bin/sh
+# Wrapper for compilers which do not understand '-c -o'.
+
+scriptversion=2018-03-07.03; # UTC
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+# Written by Tom Tromey <tromey@cygnus.com>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# This file is maintained in Automake, please report
+# bugs to <bug-automake@gnu.org> or send patches to
+# <automake-patches@gnu.org>.
+
+nl='
+'
+
+# We need space, tab and new line, in precisely that order. Quoting is
+# there to prevent tools from complaining about whitespace usage.
+IFS=" "" $nl"
+
+file_conv=
+
+# func_file_conv build_file lazy
+# Convert a $build file to $host form and store it in $file
+# Currently only supports Windows hosts. If the determined conversion
+# type is listed in (the comma separated) LAZY, no conversion will
+# take place.
+func_file_conv ()
+{
+ file=$1
+ case $file in
+ / | /[!/]*) # absolute file, and not a UNC file
+ if test -z "$file_conv"; then
+ # lazily determine how to convert abs files
+ case `uname -s` in
+ MINGW*)
+ file_conv=mingw
+ ;;
+ CYGWIN* | MSYS*)
+ file_conv=cygwin
+ ;;
+ *)
+ file_conv=wine
+ ;;
+ esac
+ fi
+ case $file_conv/,$2, in
+ *,$file_conv,*)
+ ;;
+ mingw/*)
+ file=`cmd //C echo "$file " | sed -e 's/"\(.*\) " *$/\1/'`
+ ;;
+ cygwin/* | msys/*)
+ file=`cygpath -m "$file" || echo "$file"`
+ ;;
+ wine/*)
+ file=`winepath -w "$file" || echo "$file"`
+ ;;
+ esac
+ ;;
+ esac
+}
+
+# func_cl_dashL linkdir
+# Make cl look for libraries in LINKDIR
+func_cl_dashL ()
+{
+ func_file_conv "$1"
+ if test -z "$lib_path"; then
+ lib_path=$file
+ else
+ lib_path="$lib_path;$file"
+ fi
+ linker_opts="$linker_opts -LIBPATH:$file"
+}
+
+# func_cl_dashl library
+# Do a library search-path lookup for cl
+func_cl_dashl ()
+{
+ lib=$1
+ found=no
+ save_IFS=$IFS
+ IFS=';'
+ for dir in $lib_path $LIB
+ do
+ IFS=$save_IFS
+ if $shared && test -f "$dir/$lib.dll.lib"; then
+ found=yes
+ lib=$dir/$lib.dll.lib
+ break
+ fi
+ if test -f "$dir/$lib.lib"; then
+ found=yes
+ lib=$dir/$lib.lib
+ break
+ fi
+ if test -f "$dir/lib$lib.a"; then
+ found=yes
+ lib=$dir/lib$lib.a
+ break
+ fi
+ done
+ IFS=$save_IFS
+
+ if test "$found" != yes; then
+ lib=$lib.lib
+ fi
+}
+
+# func_cl_wrapper cl arg...
+# Adjust compile command to suit cl
+func_cl_wrapper ()
+{
+ # Assume a capable shell
+ lib_path=
+ shared=:
+ linker_opts=
+ for arg
+ do
+ if test -n "$eat"; then
+ eat=
+ else
+ case $1 in
+ -o)
+ # configure might choose to run compile as 'compile cc -o foo foo.c'.
+ eat=1
+ case $2 in
+ *.o | *.[oO][bB][jJ])
+ func_file_conv "$2"
+ set x "$@" -Fo"$file"
+ shift
+ ;;
+ *)
+ func_file_conv "$2"
+ set x "$@" -Fe"$file"
+ shift
+ ;;
+ esac
+ ;;
+ -I)
+ eat=1
+ func_file_conv "$2" mingw
+ set x "$@" -I"$file"
+ shift
+ ;;
+ -I*)
+ func_file_conv "${1#-I}" mingw
+ set x "$@" -I"$file"
+ shift
+ ;;
+ -l)
+ eat=1
+ func_cl_dashl "$2"
+ set x "$@" "$lib"
+ shift
+ ;;
+ -l*)
+ func_cl_dashl "${1#-l}"
+ set x "$@" "$lib"
+ shift
+ ;;
+ -L)
+ eat=1
+ func_cl_dashL "$2"
+ ;;
+ -L*)
+ func_cl_dashL "${1#-L}"
+ ;;
+ -static)
+ shared=false
+ ;;
+ -Wl,*)
+ arg=${1#-Wl,}
+ save_ifs="$IFS"; IFS=','
+ for flag in $arg; do
+ IFS="$save_ifs"
+ linker_opts="$linker_opts $flag"
+ done
+ IFS="$save_ifs"
+ ;;
+ -Xlinker)
+ eat=1
+ linker_opts="$linker_opts $2"
+ ;;
+ -*)
+ set x "$@" "$1"
+ shift
+ ;;
+ *.cc | *.CC | *.cxx | *.CXX | *.[cC]++)
+ func_file_conv "$1"
+ set x "$@" -Tp"$file"
+ shift
+ ;;
+ *.c | *.cpp | *.CPP | *.lib | *.LIB | *.Lib | *.OBJ | *.obj | *.[oO])
+ func_file_conv "$1" mingw
+ set x "$@" "$file"
+ shift
+ ;;
+ *)
+ set x "$@" "$1"
+ shift
+ ;;
+ esac
+ fi
+ shift
+ done
+ if test -n "$linker_opts"; then
+ linker_opts="-link$linker_opts"
+ fi
+ exec "$@" $linker_opts
+ exit 1
+}
+
+eat=
+
+case $1 in
+ '')
+ echo "$0: No command. Try '$0 --help' for more information." 1>&2
+ exit 1;
+ ;;
+ -h | --h*)
+ cat <<\EOF
+Usage: compile [--help] [--version] PROGRAM [ARGS]
+
+Wrapper for compilers which do not understand '-c -o'.
+Remove '-o dest.o' from ARGS, run PROGRAM with the remaining
+arguments, and rename the output as expected.
+
+If you are trying to build a whole package this is not the
+right script to run: please start by reading the file 'INSTALL'.
+
+Report bugs to <bug-automake@gnu.org>.
+EOF
+ exit $?
+ ;;
+ -v | --v*)
+ echo "compile $scriptversion"
+ exit $?
+ ;;
+ cl | *[/\\]cl | cl.exe | *[/\\]cl.exe | \
+ icl | *[/\\]icl | icl.exe | *[/\\]icl.exe )
+ func_cl_wrapper "$@" # Doesn't return...
+ ;;
+esac
+
+ofile=
+cfile=
+
+for arg
+do
+ if test -n "$eat"; then
+ eat=
+ else
+ case $1 in
+ -o)
+ # configure might choose to run compile as 'compile cc -o foo foo.c'.
+ # So we strip '-o arg' only if arg is an object.
+ eat=1
+ case $2 in
+ *.o | *.obj)
+ ofile=$2
+ ;;
+ *)
+ set x "$@" -o "$2"
+ shift
+ ;;
+ esac
+ ;;
+ *.c)
+ cfile=$1
+ set x "$@" "$1"
+ shift
+ ;;
+ *)
+ set x "$@" "$1"
+ shift
+ ;;
+ esac
+ fi
+ shift
+done
+
+if test -z "$ofile" || test -z "$cfile"; then
+ # If no '-o' option was seen then we might have been invoked from a
+ # pattern rule where we don't need one. That is ok -- this is a
+ # normal compilation that the losing compiler can handle. If no
+ # '.c' file was seen then we are probably linking. That is also
+ # ok.
+ exec "$@"
+fi
+
+# Name of file we expect compiler to create.
+cofile=`echo "$cfile" | sed 's|^.*[\\/]||; s|^[a-zA-Z]:||; s/\.c$/.o/'`
+
+# Create the lock directory.
+# Note: use '[/\\:.-]' here to ensure that we don't use the same name
+# that we are using for the .o file. Also, base the name on the expected
+# object file name, since that is what matters with a parallel build.
+lockdir=`echo "$cofile" | sed -e 's|[/\\:.-]|_|g'`.d
+while true; do
+ if mkdir "$lockdir" >/dev/null 2>&1; then
+ break
+ fi
+ sleep 1
+done
+# FIXME: race condition here if user kills between mkdir and trap.
+trap "rmdir '$lockdir'; exit 1" 1 2 15
+
+# Run the compile.
+"$@"
+ret=$?
+
+if test -f "$cofile"; then
+ test "$cofile" = "$ofile" || mv "$cofile" "$ofile"
+elif test -f "${cofile}bj"; then
+ test "${cofile}bj" = "$ofile" || mv "${cofile}bj" "$ofile"
+fi
+
+rmdir "$lockdir"
+exit $ret
+
+# Local Variables:
+# mode: shell-script
+# sh-indentation: 2
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-time-zone: "UTC0"
+# time-stamp-end: "; # UTC"
+# End:
diff --git a/tools/debian/changelog b/tools/debian/changelog
new file mode 100644
index 0000000..bc22a45
--- /dev/null
+++ b/tools/debian/changelog
@@ -0,0 +1,5 @@
+cockpit (311-1) UNRELEASED; urgency=medium
+
+ * Work in progress
+
+ -- Cockpit <cockpit@cockpit-project.org> Fri, 13 Nov 2015 18:43:38 +0100
diff --git a/tools/debian/clean b/tools/debian/clean
new file mode 100644
index 0000000..18f0ba5
--- /dev/null
+++ b/tools/debian/clean
@@ -0,0 +1,2 @@
+tmp/
+.pytest_cache/
diff --git a/tools/debian/cockpit-bridge.install b/tools/debian/cockpit-bridge.install
new file mode 100644
index 0000000..2b68a23
--- /dev/null
+++ b/tools/debian/cockpit-bridge.install
@@ -0,0 +1,8 @@
+etc/cockpit/machines.d
+usr/bin/cockpit-bridge
+usr/lib/cockpit/cockpit-askpass
+usr/lib/cockpit/cockpit-ssh
+usr/lib/python*
+usr/share/cockpit/base1/
+usr/share/man/man1/cockpit-bridge.1
+usr/share/polkit-1/actions/org.cockpit-project.cockpit-bridge.policy
diff --git a/tools/debian/cockpit-doc.install b/tools/debian/cockpit-doc.install
new file mode 100644
index 0000000..db09819
--- /dev/null
+++ b/tools/debian/cockpit-doc.install
@@ -0,0 +1 @@
+usr/share/doc/cockpit/* usr/share/doc/cockpit/guide/
diff --git a/tools/debian/cockpit-doc.lintian-overrides b/tools/debian/cockpit-doc.lintian-overrides
new file mode 100644
index 0000000..8d72da5
--- /dev/null
+++ b/tools/debian/cockpit-doc.lintian-overrides
@@ -0,0 +1,2 @@
+cockpit-doc: font-in-non-font-package *usr/share/doc/cockpit/guide/*
+cockpit-doc: font-outside-font-dir *usr/share/doc/cockpit/guide/*
diff --git a/tools/debian/cockpit-networkmanager.install b/tools/debian/cockpit-networkmanager.install
new file mode 100644
index 0000000..8d743d6
--- /dev/null
+++ b/tools/debian/cockpit-networkmanager.install
@@ -0,0 +1,2 @@
+usr/share/cockpit/networkmanager/
+usr/share/metainfo/org.cockpit-project.cockpit-networkmanager.metainfo.xml
diff --git a/tools/debian/cockpit-packagekit.install b/tools/debian/cockpit-packagekit.install
new file mode 100644
index 0000000..c8ccf0b
--- /dev/null
+++ b/tools/debian/cockpit-packagekit.install
@@ -0,0 +1,2 @@
+usr/share/cockpit/packagekit/
+usr/share/cockpit/apps/
diff --git a/tools/debian/cockpit-pcp.install b/tools/debian/cockpit-pcp.install
new file mode 100644
index 0000000..170be00
--- /dev/null
+++ b/tools/debian/cockpit-pcp.install
@@ -0,0 +1,3 @@
+var/lib/pcp/config/pmlogconf/tools/cockpit
+usr/lib/cockpit/cockpit-pcp
+usr/share/cockpit/pcp/
diff --git a/tools/debian/cockpit-sosreport.install b/tools/debian/cockpit-sosreport.install
new file mode 100644
index 0000000..7bb8152
--- /dev/null
+++ b/tools/debian/cockpit-sosreport.install
@@ -0,0 +1,3 @@
+usr/share/cockpit/sosreport/
+usr/share/pixmaps/cockpit-sosreport.png
+usr/share/metainfo/org.cockpit-project.cockpit-sosreport.metainfo.xml
diff --git a/tools/debian/cockpit-storaged.install b/tools/debian/cockpit-storaged.install
new file mode 100644
index 0000000..c9514c0
--- /dev/null
+++ b/tools/debian/cockpit-storaged.install
@@ -0,0 +1,2 @@
+usr/share/cockpit/storaged/
+usr/share/metainfo/org.cockpit-project.cockpit-storaged.metainfo.xml
diff --git a/tools/debian/cockpit-system.install b/tools/debian/cockpit-system.install
new file mode 100644
index 0000000..025307b
--- /dev/null
+++ b/tools/debian/cockpit-system.install
@@ -0,0 +1,4 @@
+usr/share/cockpit/shell/
+usr/share/cockpit/systemd/
+usr/share/cockpit/users/
+usr/share/cockpit/metrics/
diff --git a/tools/debian/cockpit-tests.install b/tools/debian/cockpit-tests.install
new file mode 100644
index 0000000..ef197f1
--- /dev/null
+++ b/tools/debian/cockpit-tests.install
@@ -0,0 +1,3 @@
+usr/share/cockpit/playground
+${env:deb_systemdsystemunitdir}/cockpit-session.socket
+${env:deb_systemdsystemunitdir}/cockpit-session@.service
diff --git a/tools/debian/cockpit-ws.install b/tools/debian/cockpit-ws.install
new file mode 100644
index 0000000..44f0cd0
--- /dev/null
+++ b/tools/debian/cockpit-ws.install
@@ -0,0 +1,32 @@
+etc/cockpit/ws-certs.d
+etc/pam.d/cockpit
+${env:deb_systemdsystemunitdir}/cockpit.service
+${env:deb_systemdsystemunitdir}/cockpit-motd.service
+${env:deb_systemdsystemunitdir}/cockpit.socket
+${env:deb_systemdsystemunitdir}/cockpit-wsinstance-http.service
+${env:deb_systemdsystemunitdir}/cockpit-wsinstance-http.socket
+${env:deb_systemdsystemunitdir}/cockpit-wsinstance-https-factory@.service
+${env:deb_systemdsystemunitdir}/cockpit-wsinstance-https-factory.socket
+${env:deb_systemdsystemunitdir}/cockpit-wsinstance-https@.service
+${env:deb_systemdsystemunitdir}/cockpit-wsinstance-https@.socket
+${env:deb_systemdsystemunitdir}/system-cockpithttps.slice
+${env:deb_pamlibdir}/security/pam_ssh_add.so
+${env:deb_pamlibdir}/security/pam_cockpit_cert.so
+usr/lib/tmpfiles.d/cockpit-tempfiles.conf
+usr/lib/cockpit/cockpit-session
+usr/lib/cockpit/cockpit-ws
+usr/lib/cockpit/cockpit-wsinstance-factory
+usr/lib/cockpit/cockpit-tls
+usr/lib/cockpit/cockpit-client
+usr/lib/cockpit/cockpit-client.ui
+usr/lib/cockpit/cockpit-desktop
+usr/lib/cockpit/cockpit-certificate-ensure
+usr/lib/cockpit/cockpit-certificate-helper
+usr/share/cockpit/branding/
+usr/share/cockpit/motd/
+usr/share/cockpit/static/
+usr/share/man/man1/cockpit-desktop.1
+usr/share/man/man5/cockpit.conf.5
+usr/share/man/man8/cockpit-ws.8
+usr/share/man/man8/cockpit-tls.8
+usr/share/man/man8/pam_ssh_add.8
diff --git a/tools/debian/cockpit-ws.lintian-overrides b/tools/debian/cockpit-ws.lintian-overrides
new file mode 100644
index 0000000..6840a09
--- /dev/null
+++ b/tools/debian/cockpit-ws.lintian-overrides
@@ -0,0 +1,5 @@
+# this is just an empty stub to avoid breaking existing PAM files
+cockpit-ws: shared-library-lacks-prerequisites *security/pam_cockpit_cert.so*
+cockpit-ws: font-outside-font-dir *usr/share/cockpit/static/fonts/*
+cockpit-ws: font-in-non-font-package *usr/share/cockpit/static/fonts/*
+cockpit-ws: groff-message *macro *an-trap*usr/share/man/man8/cockpit-ws.8.gz*
diff --git a/tools/debian/cockpit-ws.postinst b/tools/debian/cockpit-ws.postinst
new file mode 100644
index 0000000..1e497bd
--- /dev/null
+++ b/tools/debian/cockpit-ws.postinst
@@ -0,0 +1,39 @@
+#!/bin/sh
+set -e
+
+adduser --system --group --home /nonexistent --no-create-home --quiet cockpit-ws
+adduser --system --group --home /nonexistent --no-create-home --quiet cockpit-wsinstance
+
+# change group of cockpit-session on upgrades (changed in version 203)
+if OUT=$(dpkg-statoverride --list /usr/lib/cockpit/cockpit-session) && [ "$OUT#root cockpit-ws 4750}" != "$OUT" ]; then
+ echo "Adjusting /usr/lib/cockpit/cockpit-session permissions..."
+ dpkg-statoverride --remove /usr/lib/cockpit/cockpit-session
+fi
+
+if ! dpkg-statoverride --list /usr/lib/cockpit/cockpit-session >/dev/null; then
+ dpkg-statoverride --update --add root cockpit-wsinstance 4750 /usr/lib/cockpit/cockpit-session
+fi
+
+#DEBHELPER#
+
+# restart cockpit.service on package upgrades, if it's already running
+if [ -d /run/systemd/system ] && [ -n "$2" ]; then
+ deb-systemd-invoke try-restart cockpit.service >/dev/null || true
+fi
+
+# set up dynamic motd/issue symlinks on first-time install or upgrades from < 244 (which moved them out of the .deb)
+if [ "$1" = "configure" ] && dpkg --compare-versions "$2" lt 244; then
+ mkdir -p /etc/motd.d /etc/issue.d
+ ln -s ../../run/cockpit/motd /etc/motd.d/cockpit
+ ln -s ../../run/cockpit/motd /etc/issue.d/cockpit.issue
+ printf "# List of users which are not allowed to login to Cockpit\nroot\n" > /etc/cockpit/disallowed-users
+ chmod 644 /etc/cockpit/disallowed-users
+fi
+
+# check for deprecated PAM config
+if grep --color=auto pam_cockpit_cert /etc/pam.d/cockpit; then
+ echo '**** WARNING:'
+ echo '**** WARNING: pam_cockpit_cert is a no-op and will be removed in a'
+ echo '**** WARNING: future release; remove it from your /etc/pam.d/cockpit.'
+ echo '**** WARNING:'
+fi
diff --git a/tools/debian/cockpit-ws.postrm b/tools/debian/cockpit-ws.postrm
new file mode 100644
index 0000000..8267b0d
--- /dev/null
+++ b/tools/debian/cockpit-ws.postrm
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -e
+
+#DEBHELPER#
+
+# clean up dynamic motd/issue symlinks on removal
+if [ "$1" = purge ]; then
+ [ -L /etc/motd.d/cockpit ] && rm /etc/motd.d/cockpit || true
+ [ -L /etc/issue.d/cockpit.issue ] && rm /etc/issue.d/cockpit.issue || true
+ rm -f /etc/cockpit/disallowed-users
+fi
diff --git a/tools/debian/cockpit.install b/tools/debian/cockpit.install
new file mode 100644
index 0000000..a434ecb
--- /dev/null
+++ b/tools/debian/cockpit.install
@@ -0,0 +1,3 @@
+usr/share/metainfo/cockpit.appdata.xml
+usr/share/pixmaps/cockpit.png
+usr/share/man/man1/cockpit.1
diff --git a/tools/debian/control b/tools/debian/control
new file mode 100644
index 0000000..602d742
--- /dev/null
+++ b/tools/debian/control
@@ -0,0 +1,193 @@
+Source: cockpit
+Section: admin
+Priority: optional
+Maintainer: Cockpit <cockpit@cockpit-project.org>
+Build-Depends: debhelper-compat (= 13),
+ dh-python,
+ gettext (>= 0.19.7),
+ gettext (>= 0.21) | appstream,
+ libssh-dev (>= 0.8.5),
+ zlib1g-dev,
+ libkrb5-dev (>= 1.11),
+ libxslt1-dev,
+ libglib2.0-dev (>= 2.50),
+ libgnutls28-dev (>= 3.4.3) | gnutls-dev,
+ libsystemd-dev (>= 235),
+ libpolkit-agent-1-dev,
+ libpcp3-dev,
+ libjson-glib-dev,
+ libpam0g-dev,
+ libpcp-import1-dev,
+ libpcp-pmda3-dev,
+ pkgconf,
+ systemd-dev | systemd (<< 253-2~),
+ xsltproc,
+ xmlto,
+ docbook-xsl,
+ glib-networking,
+ python3,
+ python3-pip,
+ python3-setuptools,
+ python3-wheel,
+ openssh-client <!nocheck>,
+ procps <!nocheck>,
+ python3-pytest-asyncio <!nocheck>,
+ python3-pytest-timeout <!nocheck>,
+Standards-Version: 4.6.2
+Homepage: https://cockpit-project.org/
+Vcs-Git: https://salsa.debian.org/utopia-team/cockpit.git
+Vcs-Browser: https://salsa.debian.org/utopia-team/cockpit
+Rules-Requires-Root: no
+
+Package: cockpit
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends},
+ cockpit-bridge (>= ${source:Version}),
+ cockpit-ws (>= ${source:Version}),
+ cockpit-system (>= ${source:Version}),
+Recommends: cockpit-storaged (>= ${source:Version}),
+ cockpit-networkmanager (>= ${source:Version}),
+ cockpit-packagekit (>= ${source:Version}),
+Suggests: cockpit-doc (>= ${source:Version}),
+ cockpit-pcp (>= ${source:Version}),
+ cockpit-sosreport (>= ${source:Version}),
+ xdg-utils,
+Description: Web Console for Linux servers
+ The Cockpit Web Console enables users to administer GNU/Linux servers using a
+ web browser.
+ .
+ It offers network configuration, log inspection, diagnostic reports, SELinux
+ troubleshooting, interactive command-line sessions, and more.
+
+Package: cockpit-bridge
+Architecture: any
+Depends: ${misc:Depends},
+ ${shlibs:Depends},
+ ${python3:Depends},
+ glib-networking
+Recommends: openssh-client
+Provides: cockpit-ssh
+Breaks: cockpit-ws (<< 181.x),
+Replaces: cockpit-dashboard (<< 170.x), cockpit-ws (<< 181.x)
+Description: Cockpit bridge server-side component
+ The Cockpit bridge component installed server side and runs commands on
+ the system on behalf of the web based user interface.
+
+Package: cockpit-doc
+Section: doc
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends}
+Description: Cockpit deployment and developer guide
+ The Cockpit Deployment and Developer Guide shows sysadmins how to deploy
+ Cockpit on their machines as well as helps developers who want to embed
+ or extend Cockpit.
+
+Package: cockpit-networkmanager
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends},
+ cockpit-bridge (>= ${source:Version}),
+ network-manager (>= 1.6)
+Description: Cockpit user interface for networking
+ The Cockpit components for interacting with networking configuration.
+ Incompatible with systemd-networkd/ifupdown defined networks.
+
+Package: cockpit-pcp
+Architecture: any
+Depends: ${misc:Depends},
+ ${shlibs:Depends},
+ cockpit-bridge (>= ${source:Version}),
+ pcp
+Description: Cockpit PCP integration
+ Cockpit support for reading PCP metrics and loading PCP archives.
+
+Package: cockpit-packagekit
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends},
+ cockpit-bridge (>= ${source:Version}),
+ packagekit,
+ python3,
+Description: Cockpit user interface for apps and package updates
+ The Cockpit components installing OS updates and Cockpit add-ons,
+ via PackageKit.
+
+Package: cockpit-storaged
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends},
+ udisks2 (>= 2.9),
+ udisks2 (>= 2.10) | libblockdev-mdraid2,
+ cockpit-bridge (>= ${source:Version}),
+ python3,
+ python3-dbus
+Suggests: udisks2-btrfs,
+ udisks2-lvm2,
+ mdadm,
+Description: Cockpit user interface for storage
+ The Cockpit components for interacting with storage.
+ .
+ Install udisks2-lvm2 if you use LVM and want to manage it with Cockpit.
+
+Package: cockpit-system
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends},
+ cockpit-bridge (>= ${source:Version}),
+ libpwquality-tools,
+ openssl,
+# policykit-1 was split into multiple packages; keep old name for Debian 11 and Ubuntu
+Recommends: sudo | pkexec | policykit-1
+Provides: cockpit-shell,
+ cockpit-systemd,
+ cockpit-tuned,
+ cockpit-users
+Conflicts: cockpit-shell
+Breaks: cockpit-dashboard
+Replaces: cockpit-shell, cockpit-dashboard
+Description: Cockpit admin interface for a system
+ Cockpit admin interface package for configuring and
+ troubleshooting a system.
+
+Package: cockpit-tests
+Architecture: any
+Multi-Arch: foreign
+Depends: ${misc:Depends},
+ ${shlibs:Depends},
+ cockpit-system (>= ${source:Version}),
+ openssh-client
+Conflicts: cockpit-test-assets
+Replaces: cockpit-test-assets
+Provides: cockpit-test-assets
+Description: Tests for Cockpit
+ This package contains tests and files used while testing Cockpit.
+ These files are not required for running Cockpit.
+
+Package: cockpit-ws
+Architecture: any
+Depends: ${misc:Depends},
+ ${shlibs:Depends},
+ glib-networking,
+ adduser,
+ openssl,
+ systemd (>= 235),
+Suggests: sssd-dbus (>= 2.6.2),
+ python3,
+Description: Cockpit Web Service
+ The Cockpit Web Service listens on the network, and authenticates
+ users.
+ .
+ Install sssd-dbus for supporting client certificate/smart card authentication
+ via sssd/FreeIPA.
+
+Package: cockpit-sosreport
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends},
+ cockpit-bridge (>= ${source:Version}),
+ sosreport
+Description: Cockpit user interface for diagnostic reports
+ The Cockpit component for creating diagnostic reports with the
+ sosreport tool.
diff --git a/tools/debian/copyright b/tools/debian/copyright
new file mode 100644
index 0000000..71f1ca3
--- /dev/null
+++ b/tools/debian/copyright
@@ -0,0 +1,301 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: cockpit
+Source: https://github.com/cockpit-project/cockpit
+Comment:
+ This does not directly cover the files in dist/*. These are "minified" and
+ compressed JavaScript/HTML files built from pkg/* and node_modules/*. Their
+ copyrights and licenses are described below. Rebuilding these requires
+ internet access as that process needs to download additional npm modules from
+ the Internet, thus upstream ships the pre-minified bundles as part of the
+ upstream release tarball so that the package can be built without internet
+ access and lots of extra unpackaged build dependencies.
+
+Files: *
+Copyright: 2013-2016 Red Hat, Inc.
+License: LGPL-2.1-or-later
+
+Files: src/common/cockpitunixsignal.c
+Copyright: Copyright (c) 1988, 1993, 1994 The Regents of the University of California
+ Portions Copyright (C) 2014 Red Hat, Inc.
+ Copyright (C) 2014 Sami Kerola <kerolasa@iki.fi>
+ Copyright (C) 2014 Karel Zak <kzak@redhat.com>
+License: BSD-4-clause
+
+Files: src/common/cockpitbase64.*
+Copyright: Copyright (c) 1996, 1998 by Internet Software Consortium
+ Portions Copyright (c) 1995 by International Business Machines, Inc.
+License: MIT-IBM-immunity
+
+Files: pkg/base1/test-utf8.js
+Copyright: Copyright 2014 Joshua Bell. All rights reserved
+License: Apache-2.0
+
+Files: src/bridge/cockpitpcpmetrics.c
+Copyright: Copyright (c) 2014 Red Hat.
+ Copyright (c) 1995 Silicon Graphics, Inc.
+License: LGPL-2.1-or-later
+
+Files: src/bridge/mock-pmda.c
+Copyright: Copyright (c) 2014 - 2016 Red Hat
+ Copyright (c) 1995,2004 Silicon Graphics, Inc. All Rights Reserved.
+License: GPL-2+
+
+Files: src/ssh/mock-sshd.c
+Copyright: Copyright 2003-2011 Aris Adamantiadis
+License: public-domain
+ You are free to copy this file, modify it in any way, consider it being public
+ domain.
+
+Files: tools/install-sh
+Copyright: Copyright (C) 1994 X Consortium
+License: MIT/X11
+
+Files: src/appstream/*.metainfo.xml.in src/client/*.metainfo.xml
+Copyright: Copyright (C) 2018 Red Hat, Inc.
+License: CC0-1.0
+ On Debian systems, the complete text of the Creative Commons Zero v1.0
+ Universal Public License is in "/usr/share/common-licenses/LGPL-2.1".
+
+Files: dist/apps/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/kdump/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/metrics/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/networkmanager/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/packagekit/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/playground/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/selinux/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/shell/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/sosreport/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/storaged/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Chen, Yi-Cyuan 2014-2024
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/systemd/*
+Copyright: (c) 2011 Fabrice Bellard
+ (c) 2012-2013, Christopher Jeffrey (MIT License)
+ (c) 2014 The xterm.js authors. All rights reserved.
+ (c) Facebook, Inc. and its affiliates.
+ 2011 Fabrice Bellard
+ 2012-2013, Christopher Jeffrey (MIT License)
+ 2014 The xterm.js authors. All rights reserved.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+Files: dist/users/*
+Copyright: (c) Facebook, Inc. and its affiliates.
+ 2015-2016 David Clark
+ Copyright (C) 2013 - 2024 Red Hat, Inc.
+ Facebook, Inc. and its affiliates.
+License: LGPL-2.1-or-later and MIT
+
+
+Files: node_modules/chrome-remote-interface/*
+Copyright: (c) 2020 Andrea Cardaci <cyrus.and@gmail.com>
+License: MIT
+
+Files: node_modules/commander/*
+Copyright: (c) 2011 TJ Holowaychuk <tj@vision-media.ca>
+License: MIT
+
+Files: node_modules/sizzle/*
+Copyright: JS Foundation and other contributors, https://js.foundation/
+License: MIT
+
+Files: node_modules/ws/*
+Copyright: (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+License: MIT
+
+License: LGPL-2.1-or-later
+ This package is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+ .
+ This package is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the complete text of the GNU Lesser General
+ Public License can be found in "/usr/share/common-licenses/LGPL-2.1".
+
+License: GPL-2+
+ This program is free software; you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by the
+ Free Software Foundation; either version 2 of the License, or (at your
+ option) any later version.
+ .
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ for more details.
+ .
+ On Debian systems, the complete text of the GNU General
+ Public License can be found in "/usr/share/common-licenses/GPL-2".
+
+License: BSD-4-clause
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgment:
+ This product includes software developed by the University of
+ California, Berkeley and its contributors.
+ 4. Neither the name of the University nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+ .
+ THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ SUCH DAMAGE.
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC-
+ TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+License: MIT/X11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC-
+ TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ .
+ Except as contained in this notice, the name of the X Consortium shall not
+ be used in advertising or otherwise to promote the sale, use or other deal-
+ ings in this Software without prior written authorization from the X Consor-
+ tium.
+
+License: MIT-IBM-immunity
+ International Business Machines, Inc. (hereinafter called IBM) grants
+ permission under its copyrights to use, copy, modify, and distribute this
+ Software with or without fee, provided that the above copyright notice and
+ all paragraphs of this notice appear in all copies, and that the name of IBM
+ not be used in connection with the marketing of any product incorporating
+ the Software or modifications thereof, without specific, written prior
+ permission.
+ .
+ To the extent it has a right to do so, IBM grants an immunity from suit
+ under its patents, if any, for the use, sale or manufacture of products to
+ the extent that such products are used for performing Domain Name System
+ dynamic updates in TCP/IP networks by means of the Software. No immunity is
+ granted for any product per se or for any other function of any product.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES,
+ INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE. IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL,
+ DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING
+ OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN
+ IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+License: Apache-2.0
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ .
+ https://www.apache.org/licenses/LICENSE-2.0.html
+ .
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/tools/debian/copyright.template b/tools/debian/copyright.template
new file mode 100644
index 0000000..2298f8d
--- /dev/null
+++ b/tools/debian/copyright.template
@@ -0,0 +1,212 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: cockpit
+Source: https://github.com/cockpit-project/cockpit
+Comment:
+ This does not directly cover the files in dist/*. These are "minified" and
+ compressed JavaScript/HTML files built from pkg/* and node_modules/*. Their
+ copyrights and licenses are described below. Rebuilding these requires
+ internet access as that process needs to download additional npm modules from
+ the Internet, thus upstream ships the pre-minified bundles as part of the
+ upstream release tarball so that the package can be built without internet
+ access and lots of extra unpackaged build dependencies.
+
+Files: *
+Copyright: 2013-2016 Red Hat, Inc.
+License: LGPL-2.1-or-later
+
+Files: src/common/cockpitunixsignal.c
+Copyright: Copyright (c) 1988, 1993, 1994 The Regents of the University of California
+ Portions Copyright (C) 2014 Red Hat, Inc.
+ Copyright (C) 2014 Sami Kerola <kerolasa@iki.fi>
+ Copyright (C) 2014 Karel Zak <kzak@redhat.com>
+License: BSD-4-clause
+
+Files: src/common/cockpitbase64.*
+Copyright: Copyright (c) 1996, 1998 by Internet Software Consortium
+ Portions Copyright (c) 1995 by International Business Machines, Inc.
+License: MIT-IBM-immunity
+
+Files: pkg/base1/test-utf8.js
+Copyright: Copyright 2014 Joshua Bell. All rights reserved
+License: Apache-2.0
+
+Files: src/bridge/cockpitpcpmetrics.c
+Copyright: Copyright (c) 2014 Red Hat.
+ Copyright (c) 1995 Silicon Graphics, Inc.
+License: LGPL-2.1-or-later
+
+Files: src/bridge/mock-pmda.c
+Copyright: Copyright (c) 2014 - 2016 Red Hat
+ Copyright (c) 1995,2004 Silicon Graphics, Inc. All Rights Reserved.
+License: GPL-2+
+
+Files: src/ssh/mock-sshd.c
+Copyright: Copyright 2003-2011 Aris Adamantiadis
+License: public-domain
+ You are free to copy this file, modify it in any way, consider it being public
+ domain.
+
+Files: tools/install-sh
+Copyright: Copyright (C) 1994 X Consortium
+License: MIT/X11
+
+Files: src/appstream/*.metainfo.xml.in src/client/*.metainfo.xml
+Copyright: Copyright (C) 2018 Red Hat, Inc.
+License: CC0-1.0
+ On Debian systems, the complete text of the Creative Commons Zero v1.0
+ Universal Public License is in "/usr/share/common-licenses/LGPL-2.1".
+
+#NPM# semi-autogenerated records for node_modules/ go here
+
+
+Files: node_modules/chrome-remote-interface/*
+Copyright: (c) 2020 Andrea Cardaci <cyrus.and@gmail.com>
+License: MIT
+
+Files: node_modules/commander/*
+Copyright: (c) 2011 TJ Holowaychuk <tj@vision-media.ca>
+License: MIT
+
+Files: node_modules/sizzle/*
+Copyright: JS Foundation and other contributors, https://js.foundation/
+License: MIT
+
+Files: node_modules/ws/*
+Copyright: (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+License: MIT
+
+License: LGPL-2.1-or-later
+ This package is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+ .
+ This package is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the complete text of the GNU Lesser General
+ Public License can be found in "/usr/share/common-licenses/LGPL-2.1".
+
+License: GPL-2+
+ This program is free software; you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by the
+ Free Software Foundation; either version 2 of the License, or (at your
+ option) any later version.
+ .
+ This program is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ for more details.
+ .
+ On Debian systems, the complete text of the GNU General
+ Public License can be found in "/usr/share/common-licenses/GPL-2".
+
+License: BSD-4-clause
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgment:
+ This product includes software developed by the University of
+ California, Berkeley and its contributors.
+ 4. Neither the name of the University nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+ .
+ THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ SUCH DAMAGE.
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC-
+ TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+License: MIT/X11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC-
+ TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ .
+ Except as contained in this notice, the name of the X Consortium shall not
+ be used in advertising or otherwise to promote the sale, use or other deal-
+ ings in this Software without prior written authorization from the X Consor-
+ tium.
+
+License: MIT-IBM-immunity
+ International Business Machines, Inc. (hereinafter called IBM) grants
+ permission under its copyrights to use, copy, modify, and distribute this
+ Software with or without fee, provided that the above copyright notice and
+ all paragraphs of this notice appear in all copies, and that the name of IBM
+ not be used in connection with the marketing of any product incorporating
+ the Software or modifications thereof, without specific, written prior
+ permission.
+ .
+ To the extent it has a right to do so, IBM grants an immunity from suit
+ under its patents, if any, for the use, sale or manufacture of products to
+ the extent that such products are used for performing Domain Name System
+ dynamic updates in TCP/IP networks by means of the Software. No immunity is
+ granted for any product per se or for any other function of any product.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES,
+ INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE. IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL,
+ DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING
+ OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN
+ IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+License: Apache-2.0
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ .
+ https://www.apache.org/licenses/LICENSE-2.0.html
+ .
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/tools/debian/rules b/tools/debian/rules
new file mode 100755
index 0000000..ef13edf
--- /dev/null
+++ b/tools/debian/rules
@@ -0,0 +1,71 @@
+#!/usr/bin/make -f
+
+DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH)
+
+# riscv is an emulated architecture for now, and too slow to run expensive unit tests
+# hppa's threading is absurdly slow (#981127)
+SLOW_ARCHES = $(filter $(shell dpkg-architecture -qDEB_BUILD_ARCH),riscv64 hppa)
+ifneq ($(SLOW_ARCHES),)
+ export COCKPIT_SKIP_SLOW_TESTS=1
+endif
+
+export deb_systemdsystemunitdir = $(shell pkgconf --variable=systemdsystemunitdir systemd | sed s,^/,,)
+# pam.pc doesn't yet have a libdir on older releases
+export deb_pamlibdir = $(shell { pkgconf --variable=libdir pam || echo /lib/$(DEB_HOST_MULTIARCH); } | sed s,^/,,)
+
+%:
+ dh $@ --buildsystem=autoconf --with=python3
+
+override_dh_auto_configure:
+ dh_auto_configure -- \
+ --with-cockpit-user=cockpit-ws \
+ --with-cockpit-ws-instance-user=cockpit-wsinstance \
+ --with-pamdir=/$(deb_pamlibdir)/security \
+ --libexecdir=/usr/lib/cockpit $(CONFIG_OPTIONS)
+
+# HACK: Debian's pip breaks --prefix: https://bugs.debian.org/1035546 with
+# default install layout
+override_dh_auto_install:
+ DEB_PYTHON_INSTALL_LAYOUT=deb dh_auto_install
+
+# avoid trying to start cockpit-motd.service and cockpit-wsinstance-*.socket etc.
+override_dh_installsystemd:
+ dh_installsystemd -p cockpit-ws cockpit.socket
+
+override_dh_install:
+ # Debian/Ubuntu PAM config
+ mkdir -p debian/tmp/etc/pam.d
+ install -p -m 644 tools/cockpit.debian.pam debian/tmp/etc/pam.d/cockpit
+
+ # don't ship broken branding symlinks
+ for d in rhel fedora centos scientific opensuse; do rm -r debian/tmp/usr/share/cockpit/branding/$$d; done
+ dpkg-vendor --derives-from ubuntu || rm -r debian/tmp/usr/share/cockpit/branding/ubuntu
+
+ # handled by package maintainer scripts
+ rm debian/tmp/etc/motd.d/cockpit debian/tmp/etc/issue.d/cockpit.issue
+
+ # unpackaged modules
+ rm -r debian/tmp/usr/share/cockpit/kdump
+ rm debian/tmp/usr/share/metainfo/org.cockpit-project.cockpit-kdump.metainfo.xml
+ rm -r debian/tmp/usr/share/cockpit/selinux
+ rm debian/tmp/usr/share/metainfo/org.cockpit-project.cockpit-selinux.metainfo.xml
+
+ dh_install -Xusr/src/debug
+ # we don't need this, it contains full build paths and breaks reproducibility
+ rm -r debian/tmp/usr/lib/python*/*-packages/*.dist-info
+
+ make install-tests DESTDIR=debian/cockpit-tests
+
+execute_after_dh_install-indep:
+ # avoid dh_missing failure
+ rm -r debian/tmp/usr/lib/python*
+
+# run pytests *after* installation, so that we can make sure that we installed the right files
+execute_after_dh_install-arch:
+ifeq (, $(findstring nocheck, $(DEB_BUILD_OPTIONS)))
+ifeq ($(shell . /etc/os-release; echo $${VERSION_ID:-unstable}),22.04)
+ PYTHONPATH=$$(ls -d debian/cockpit-bridge/usr/lib/python3*/dist-packages) python3 -m pytest -vv -k 'not linter and not test_descriptions'
+else
+ pytest -vv -k 'not linter and not test_descriptions' -opythonpath=$$(ls -d debian/cockpit-bridge/usr/lib/python3*/dist-packages)
+endif
+endif
diff --git a/tools/debian/source/format b/tools/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/tools/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/tools/debian/source/lintian-overrides b/tools/debian/source/lintian-overrides
new file mode 100644
index 0000000..70145d0
--- /dev/null
+++ b/tools/debian/source/lintian-overrides
@@ -0,0 +1,8 @@
+# false positive: this *is* the source
+cockpit source: source-is-missing *pkg/static/login.html*
+cockpit source: source-is-missing *src/cockpit/data/fail.html*
+cockpit source: source-is-missing *src/common/fail.html*
+# source contains NPM modules required for running browser integration tests
+cockpit source: source-is-missing *node_modules/*
+# dist/ is (pre-)built from pkg/ and node_modules, see ./build.js
+cockpit source: source-is-missing *dist/*
diff --git a/tools/debian/source/options b/tools/debian/source/options
new file mode 100644
index 0000000..a06658f
--- /dev/null
+++ b/tools/debian/source/options
@@ -0,0 +1,2 @@
+# FIXME: Running the unit tests changes the identifier in that file
+extend-diff-ignore = "src/ssh/mock_rsa_key.pub$"
diff --git a/tools/debian/tests/control b/tools/debian/tests/control
new file mode 100644
index 0000000..fbe3826
--- /dev/null
+++ b/tools/debian/tests/control
@@ -0,0 +1,4 @@
+Tests: smoke
+Depends: cockpit,
+ curl,
+Restrictions: isolation-container
diff --git a/tools/debian/tests/smoke b/tools/debian/tests/smoke
new file mode 100755
index 0000000..8b1c7fe
--- /dev/null
+++ b/tools/debian/tests/smoke
@@ -0,0 +1,27 @@
+#!/bin/sh
+set -e
+
+check_out() {
+ echo "$OUT" | grep -q "$1" || {
+ echo "output does not match '$1'" >&2
+ exit 1
+ }
+}
+
+echo " * bridge works and has expected packages"
+OUT=$(cockpit-bridge --packages)
+echo "$OUT"
+check_out "^base1.* /usr/share/cockpit/base1"
+check_out "^system"
+check_out "^users"
+
+# on an RPM based system we expect cockpit.socket not to be enabled by default;
+# on a Debian-based system we do
+if rpm -q cockpit >/dev/null 2>&1; then
+ systemctl start cockpit.socket
+fi
+
+echo " * socket unit is set up correctly, login page available"
+OUT=$(curl --silent --show-error --insecure https://localhost:9090)
+check_out "login-user-input.*User"
+echo "smoke test passed"
diff --git a/tools/debian/watch b/tools/debian/watch
new file mode 100644
index 0000000..cfbae0b
--- /dev/null
+++ b/tools/debian/watch
@@ -0,0 +1,5 @@
+version=4
+opts="searchmode=plain, \
+filenamemangle=s/.+\/@PACKAGE@-@ANY_VERSION@.tar.gz/@PACKAGE@-$1\.tar\.xz/" \
+https://api.github.com/repos/cockpit-project/@PACKAGE@/releases \
+https://github.com/cockpit-project/@PACKAGE@/releases/download/\d[\.\d]*/@PACKAGE@-@ANY_VERSION@.tar.xz
diff --git a/tools/depcomp b/tools/depcomp
new file mode 100755
index 0000000..715e343
--- /dev/null
+++ b/tools/depcomp
@@ -0,0 +1,791 @@
+#! /bin/sh
+# depcomp - compile a program generating dependencies as side-effects
+
+scriptversion=2018-03-07.03; # UTC
+
+# Copyright (C) 1999-2021 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# Originally written by Alexandre Oliva <oliva@dcc.unicamp.br>.
+
+case $1 in
+ '')
+ echo "$0: No command. Try '$0 --help' for more information." 1>&2
+ exit 1;
+ ;;
+ -h | --h*)
+ cat <<\EOF
+Usage: depcomp [--help] [--version] PROGRAM [ARGS]
+
+Run PROGRAMS ARGS to compile a file, generating dependencies
+as side-effects.
+
+Environment variables:
+ depmode Dependency tracking mode.
+ source Source file read by 'PROGRAMS ARGS'.
+ object Object file output by 'PROGRAMS ARGS'.
+ DEPDIR directory where to store dependencies.
+ depfile Dependency file to output.
+ tmpdepfile Temporary file to use when outputting dependencies.
+ libtool Whether libtool is used (yes/no).
+
+Report bugs to <bug-automake@gnu.org>.
+EOF
+ exit $?
+ ;;
+ -v | --v*)
+ echo "depcomp $scriptversion"
+ exit $?
+ ;;
+esac
+
+# Get the directory component of the given path, and save it in the
+# global variables '$dir'. Note that this directory component will
+# be either empty or ending with a '/' character. This is deliberate.
+set_dir_from ()
+{
+ case $1 in
+ */*) dir=`echo "$1" | sed -e 's|/[^/]*$|/|'`;;
+ *) dir=;;
+ esac
+}
+
+# Get the suffix-stripped basename of the given path, and save it the
+# global variable '$base'.
+set_base_from ()
+{
+ base=`echo "$1" | sed -e 's|^.*/||' -e 's/\.[^.]*$//'`
+}
+
+# If no dependency file was actually created by the compiler invocation,
+# we still have to create a dummy depfile, to avoid errors with the
+# Makefile "include basename.Plo" scheme.
+make_dummy_depfile ()
+{
+ echo "#dummy" > "$depfile"
+}
+
+# Factor out some common post-processing of the generated depfile.
+# Requires the auxiliary global variable '$tmpdepfile' to be set.
+aix_post_process_depfile ()
+{
+ # If the compiler actually managed to produce a dependency file,
+ # post-process it.
+ if test -f "$tmpdepfile"; then
+ # Each line is of the form 'foo.o: dependency.h'.
+ # Do two passes, one to just change these to
+ # $object: dependency.h
+ # and one to simply output
+ # dependency.h:
+ # which is needed to avoid the deleted-header problem.
+ { sed -e "s,^.*\.[$lower]*:,$object:," < "$tmpdepfile"
+ sed -e "s,^.*\.[$lower]*:[$tab ]*,," -e 's,$,:,' < "$tmpdepfile"
+ } > "$depfile"
+ rm -f "$tmpdepfile"
+ else
+ make_dummy_depfile
+ fi
+}
+
+# A tabulation character.
+tab=' '
+# A newline character.
+nl='
+'
+# Character ranges might be problematic outside the C locale.
+# These definitions help.
+upper=ABCDEFGHIJKLMNOPQRSTUVWXYZ
+lower=abcdefghijklmnopqrstuvwxyz
+digits=0123456789
+alpha=${upper}${lower}
+
+if test -z "$depmode" || test -z "$source" || test -z "$object"; then
+ echo "depcomp: Variables source, object and depmode must be set" 1>&2
+ exit 1
+fi
+
+# Dependencies for sub/bar.o or sub/bar.obj go into sub/.deps/bar.Po.
+depfile=${depfile-`echo "$object" |
+ sed 's|[^\\/]*$|'${DEPDIR-.deps}'/&|;s|\.\([^.]*\)$|.P\1|;s|Pobj$|Po|'`}
+tmpdepfile=${tmpdepfile-`echo "$depfile" | sed 's/\.\([^.]*\)$/.T\1/'`}
+
+rm -f "$tmpdepfile"
+
+# Avoid interferences from the environment.
+gccflag= dashmflag=
+
+# Some modes work just like other modes, but use different flags. We
+# parameterize here, but still list the modes in the big case below,
+# to make depend.m4 easier to write. Note that we *cannot* use a case
+# here, because this file can only contain one case statement.
+if test "$depmode" = hp; then
+ # HP compiler uses -M and no extra arg.
+ gccflag=-M
+ depmode=gcc
+fi
+
+if test "$depmode" = dashXmstdout; then
+ # This is just like dashmstdout with a different argument.
+ dashmflag=-xM
+ depmode=dashmstdout
+fi
+
+cygpath_u="cygpath -u -f -"
+if test "$depmode" = msvcmsys; then
+ # This is just like msvisualcpp but w/o cygpath translation.
+ # Just convert the backslash-escaped backslashes to single forward
+ # slashes to satisfy depend.m4
+ cygpath_u='sed s,\\\\,/,g'
+ depmode=msvisualcpp
+fi
+
+if test "$depmode" = msvc7msys; then
+ # This is just like msvc7 but w/o cygpath translation.
+ # Just convert the backslash-escaped backslashes to single forward
+ # slashes to satisfy depend.m4
+ cygpath_u='sed s,\\\\,/,g'
+ depmode=msvc7
+fi
+
+if test "$depmode" = xlc; then
+ # IBM C/C++ Compilers xlc/xlC can output gcc-like dependency information.
+ gccflag=-qmakedep=gcc,-MF
+ depmode=gcc
+fi
+
+case "$depmode" in
+gcc3)
+## gcc 3 implements dependency tracking that does exactly what
+## we want. Yay! Note: for some reason libtool 1.4 doesn't like
+## it if -MD -MP comes after the -MF stuff. Hmm.
+## Unfortunately, FreeBSD c89 acceptance of flags depends upon
+## the command line argument order; so add the flags where they
+## appear in depend2.am. Note that the slowdown incurred here
+## affects only configure: in makefiles, %FASTDEP% shortcuts this.
+ for arg
+ do
+ case $arg in
+ -c) set fnord "$@" -MT "$object" -MD -MP -MF "$tmpdepfile" "$arg" ;;
+ *) set fnord "$@" "$arg" ;;
+ esac
+ shift # fnord
+ shift # $arg
+ done
+ "$@"
+ stat=$?
+ if test $stat -ne 0; then
+ rm -f "$tmpdepfile"
+ exit $stat
+ fi
+ mv "$tmpdepfile" "$depfile"
+ ;;
+
+gcc)
+## Note that this doesn't just cater to obsosete pre-3.x GCC compilers.
+## but also to in-use compilers like IMB xlc/xlC and the HP C compiler.
+## (see the conditional assignment to $gccflag above).
+## There are various ways to get dependency output from gcc. Here's
+## why we pick this rather obscure method:
+## - Don't want to use -MD because we'd like the dependencies to end
+## up in a subdir. Having to rename by hand is ugly.
+## (We might end up doing this anyway to support other compilers.)
+## - The DEPENDENCIES_OUTPUT environment variable makes gcc act like
+## -MM, not -M (despite what the docs say). Also, it might not be
+## supported by the other compilers which use the 'gcc' depmode.
+## - Using -M directly means running the compiler twice (even worse
+## than renaming).
+ if test -z "$gccflag"; then
+ gccflag=-MD,
+ fi
+ "$@" -Wp,"$gccflag$tmpdepfile"
+ stat=$?
+ if test $stat -ne 0; then
+ rm -f "$tmpdepfile"
+ exit $stat
+ fi
+ rm -f "$depfile"
+ echo "$object : \\" > "$depfile"
+ # The second -e expression handles DOS-style file names with drive
+ # letters.
+ sed -e 's/^[^:]*: / /' \
+ -e 's/^['$alpha']:\/[^:]*: / /' < "$tmpdepfile" >> "$depfile"
+## This next piece of magic avoids the "deleted header file" problem.
+## The problem is that when a header file which appears in a .P file
+## is deleted, the dependency causes make to die (because there is
+## typically no way to rebuild the header). We avoid this by adding
+## dummy dependencies for each header file. Too bad gcc doesn't do
+## this for us directly.
+## Some versions of gcc put a space before the ':'. On the theory
+## that the space means something, we add a space to the output as
+## well. hp depmode also adds that space, but also prefixes the VPATH
+## to the object. Take care to not repeat it in the output.
+## Some versions of the HPUX 10.20 sed can't process this invocation
+## correctly. Breaking it into two sed invocations is a workaround.
+ tr ' ' "$nl" < "$tmpdepfile" \
+ | sed -e 's/^\\$//' -e '/^$/d' -e "s|.*$object$||" -e '/:$/d' \
+ | sed -e 's/$/ :/' >> "$depfile"
+ rm -f "$tmpdepfile"
+ ;;
+
+hp)
+ # This case exists only to let depend.m4 do its work. It works by
+ # looking at the text of this script. This case will never be run,
+ # since it is checked for above.
+ exit 1
+ ;;
+
+sgi)
+ if test "$libtool" = yes; then
+ "$@" "-Wp,-MDupdate,$tmpdepfile"
+ else
+ "$@" -MDupdate "$tmpdepfile"
+ fi
+ stat=$?
+ if test $stat -ne 0; then
+ rm -f "$tmpdepfile"
+ exit $stat
+ fi
+ rm -f "$depfile"
+
+ if test -f "$tmpdepfile"; then # yes, the sourcefile depend on other files
+ echo "$object : \\" > "$depfile"
+ # Clip off the initial element (the dependent). Don't try to be
+ # clever and replace this with sed code, as IRIX sed won't handle
+ # lines with more than a fixed number of characters (4096 in
+ # IRIX 6.2 sed, 8192 in IRIX 6.5). We also remove comment lines;
+ # the IRIX cc adds comments like '#:fec' to the end of the
+ # dependency line.
+ tr ' ' "$nl" < "$tmpdepfile" \
+ | sed -e 's/^.*\.o://' -e 's/#.*$//' -e '/^$/ d' \
+ | tr "$nl" ' ' >> "$depfile"
+ echo >> "$depfile"
+ # The second pass generates a dummy entry for each header file.
+ tr ' ' "$nl" < "$tmpdepfile" \
+ | sed -e 's/^.*\.o://' -e 's/#.*$//' -e '/^$/ d' -e 's/$/:/' \
+ >> "$depfile"
+ else
+ make_dummy_depfile
+ fi
+ rm -f "$tmpdepfile"
+ ;;
+
+xlc)
+ # This case exists only to let depend.m4 do its work. It works by
+ # looking at the text of this script. This case will never be run,
+ # since it is checked for above.
+ exit 1
+ ;;
+
+aix)
+ # The C for AIX Compiler uses -M and outputs the dependencies
+ # in a .u file. In older versions, this file always lives in the
+ # current directory. Also, the AIX compiler puts '$object:' at the
+ # start of each line; $object doesn't have directory information.
+ # Version 6 uses the directory in both cases.
+ set_dir_from "$object"
+ set_base_from "$object"
+ if test "$libtool" = yes; then
+ tmpdepfile1=$dir$base.u
+ tmpdepfile2=$base.u
+ tmpdepfile3=$dir.libs/$base.u
+ "$@" -Wc,-M
+ else
+ tmpdepfile1=$dir$base.u
+ tmpdepfile2=$dir$base.u
+ tmpdepfile3=$dir$base.u
+ "$@" -M
+ fi
+ stat=$?
+ if test $stat -ne 0; then
+ rm -f "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3"
+ exit $stat
+ fi
+
+ for tmpdepfile in "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3"
+ do
+ test -f "$tmpdepfile" && break
+ done
+ aix_post_process_depfile
+ ;;
+
+tcc)
+ # tcc (Tiny C Compiler) understand '-MD -MF file' since version 0.9.26
+ # FIXME: That version still under development at the moment of writing.
+ # Make that this statement remains true also for stable, released
+ # versions.
+ # It will wrap lines (doesn't matter whether long or short) with a
+ # trailing '\', as in:
+ #
+ # foo.o : \
+ # foo.c \
+ # foo.h \
+ #
+ # It will put a trailing '\' even on the last line, and will use leading
+ # spaces rather than leading tabs (at least since its commit 0394caf7
+ # "Emit spaces for -MD").
+ "$@" -MD -MF "$tmpdepfile"
+ stat=$?
+ if test $stat -ne 0; then
+ rm -f "$tmpdepfile"
+ exit $stat
+ fi
+ rm -f "$depfile"
+ # Each non-empty line is of the form 'foo.o : \' or ' dep.h \'.
+ # We have to change lines of the first kind to '$object: \'.
+ sed -e "s|.*:|$object :|" < "$tmpdepfile" > "$depfile"
+ # And for each line of the second kind, we have to emit a 'dep.h:'
+ # dummy dependency, to avoid the deleted-header problem.
+ sed -n -e 's|^ *\(.*\) *\\$|\1:|p' < "$tmpdepfile" >> "$depfile"
+ rm -f "$tmpdepfile"
+ ;;
+
+## The order of this option in the case statement is important, since the
+## shell code in configure will try each of these formats in the order
+## listed in this file. A plain '-MD' option would be understood by many
+## compilers, so we must ensure this comes after the gcc and icc options.
+pgcc)
+ # Portland's C compiler understands '-MD'.
+ # Will always output deps to 'file.d' where file is the root name of the
+ # source file under compilation, even if file resides in a subdirectory.
+ # The object file name does not affect the name of the '.d' file.
+ # pgcc 10.2 will output
+ # foo.o: sub/foo.c sub/foo.h
+ # and will wrap long lines using '\' :
+ # foo.o: sub/foo.c ... \
+ # sub/foo.h ... \
+ # ...
+ set_dir_from "$object"
+ # Use the source, not the object, to determine the base name, since
+ # that's sadly what pgcc will do too.
+ set_base_from "$source"
+ tmpdepfile=$base.d
+
+ # For projects that build the same source file twice into different object
+ # files, the pgcc approach of using the *source* file root name can cause
+ # problems in parallel builds. Use a locking strategy to avoid stomping on
+ # the same $tmpdepfile.
+ lockdir=$base.d-lock
+ trap "
+ echo '$0: caught signal, cleaning up...' >&2
+ rmdir '$lockdir'
+ exit 1
+ " 1 2 13 15
+ numtries=100
+ i=$numtries
+ while test $i -gt 0; do
+ # mkdir is a portable test-and-set.
+ if mkdir "$lockdir" 2>/dev/null; then
+ # This process acquired the lock.
+ "$@" -MD
+ stat=$?
+ # Release the lock.
+ rmdir "$lockdir"
+ break
+ else
+ # If the lock is being held by a different process, wait
+ # until the winning process is done or we timeout.
+ while test -d "$lockdir" && test $i -gt 0; do
+ sleep 1
+ i=`expr $i - 1`
+ done
+ fi
+ i=`expr $i - 1`
+ done
+ trap - 1 2 13 15
+ if test $i -le 0; then
+ echo "$0: failed to acquire lock after $numtries attempts" >&2
+ echo "$0: check lockdir '$lockdir'" >&2
+ exit 1
+ fi
+
+ if test $stat -ne 0; then
+ rm -f "$tmpdepfile"
+ exit $stat
+ fi
+ rm -f "$depfile"
+ # Each line is of the form `foo.o: dependent.h',
+ # or `foo.o: dep1.h dep2.h \', or ` dep3.h dep4.h \'.
+ # Do two passes, one to just change these to
+ # `$object: dependent.h' and one to simply `dependent.h:'.
+ sed "s,^[^:]*:,$object :," < "$tmpdepfile" > "$depfile"
+ # Some versions of the HPUX 10.20 sed can't process this invocation
+ # correctly. Breaking it into two sed invocations is a workaround.
+ sed 's,^[^:]*: \(.*\)$,\1,;s/^\\$//;/^$/d;/:$/d' < "$tmpdepfile" \
+ | sed -e 's/$/ :/' >> "$depfile"
+ rm -f "$tmpdepfile"
+ ;;
+
+hp2)
+ # The "hp" stanza above does not work with aCC (C++) and HP's ia64
+ # compilers, which have integrated preprocessors. The correct option
+ # to use with these is +Maked; it writes dependencies to a file named
+ # 'foo.d', which lands next to the object file, wherever that
+ # happens to be.
+ # Much of this is similar to the tru64 case; see comments there.
+ set_dir_from "$object"
+ set_base_from "$object"
+ if test "$libtool" = yes; then
+ tmpdepfile1=$dir$base.d
+ tmpdepfile2=$dir.libs/$base.d
+ "$@" -Wc,+Maked
+ else
+ tmpdepfile1=$dir$base.d
+ tmpdepfile2=$dir$base.d
+ "$@" +Maked
+ fi
+ stat=$?
+ if test $stat -ne 0; then
+ rm -f "$tmpdepfile1" "$tmpdepfile2"
+ exit $stat
+ fi
+
+ for tmpdepfile in "$tmpdepfile1" "$tmpdepfile2"
+ do
+ test -f "$tmpdepfile" && break
+ done
+ if test -f "$tmpdepfile"; then
+ sed -e "s,^.*\.[$lower]*:,$object:," "$tmpdepfile" > "$depfile"
+ # Add 'dependent.h:' lines.
+ sed -ne '2,${
+ s/^ *//
+ s/ \\*$//
+ s/$/:/
+ p
+ }' "$tmpdepfile" >> "$depfile"
+ else
+ make_dummy_depfile
+ fi
+ rm -f "$tmpdepfile" "$tmpdepfile2"
+ ;;
+
+tru64)
+ # The Tru64 compiler uses -MD to generate dependencies as a side
+ # effect. 'cc -MD -o foo.o ...' puts the dependencies into 'foo.o.d'.
+ # At least on Alpha/Redhat 6.1, Compaq CCC V6.2-504 seems to put
+ # dependencies in 'foo.d' instead, so we check for that too.
+ # Subdirectories are respected.
+ set_dir_from "$object"
+ set_base_from "$object"
+
+ if test "$libtool" = yes; then
+ # Libtool generates 2 separate objects for the 2 libraries. These
+ # two compilations output dependencies in $dir.libs/$base.o.d and
+ # in $dir$base.o.d. We have to check for both files, because
+ # one of the two compilations can be disabled. We should prefer
+ # $dir$base.o.d over $dir.libs/$base.o.d because the latter is
+ # automatically cleaned when .libs/ is deleted, while ignoring
+ # the former would cause a distcleancheck panic.
+ tmpdepfile1=$dir$base.o.d # libtool 1.5
+ tmpdepfile2=$dir.libs/$base.o.d # Likewise.
+ tmpdepfile3=$dir.libs/$base.d # Compaq CCC V6.2-504
+ "$@" -Wc,-MD
+ else
+ tmpdepfile1=$dir$base.d
+ tmpdepfile2=$dir$base.d
+ tmpdepfile3=$dir$base.d
+ "$@" -MD
+ fi
+
+ stat=$?
+ if test $stat -ne 0; then
+ rm -f "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3"
+ exit $stat
+ fi
+
+ for tmpdepfile in "$tmpdepfile1" "$tmpdepfile2" "$tmpdepfile3"
+ do
+ test -f "$tmpdepfile" && break
+ done
+ # Same post-processing that is required for AIX mode.
+ aix_post_process_depfile
+ ;;
+
+msvc7)
+ if test "$libtool" = yes; then
+ showIncludes=-Wc,-showIncludes
+ else
+ showIncludes=-showIncludes
+ fi
+ "$@" $showIncludes > "$tmpdepfile"
+ stat=$?
+ grep -v '^Note: including file: ' "$tmpdepfile"
+ if test $stat -ne 0; then
+ rm -f "$tmpdepfile"
+ exit $stat
+ fi
+ rm -f "$depfile"
+ echo "$object : \\" > "$depfile"
+ # The first sed program below extracts the file names and escapes
+ # backslashes for cygpath. The second sed program outputs the file
+ # name when reading, but also accumulates all include files in the
+ # hold buffer in order to output them again at the end. This only
+ # works with sed implementations that can handle large buffers.
+ sed < "$tmpdepfile" -n '
+/^Note: including file: *\(.*\)/ {
+ s//\1/
+ s/\\/\\\\/g
+ p
+}' | $cygpath_u | sort -u | sed -n '
+s/ /\\ /g
+s/\(.*\)/'"$tab"'\1 \\/p
+s/.\(.*\) \\/\1:/
+H
+$ {
+ s/.*/'"$tab"'/
+ G
+ p
+}' >> "$depfile"
+ echo >> "$depfile" # make sure the fragment doesn't end with a backslash
+ rm -f "$tmpdepfile"
+ ;;
+
+msvc7msys)
+ # This case exists only to let depend.m4 do its work. It works by
+ # looking at the text of this script. This case will never be run,
+ # since it is checked for above.
+ exit 1
+ ;;
+
+#nosideeffect)
+ # This comment above is used by automake to tell side-effect
+ # dependency tracking mechanisms from slower ones.
+
+dashmstdout)
+ # Important note: in order to support this mode, a compiler *must*
+ # always write the preprocessed file to stdout, regardless of -o.
+ "$@" || exit $?
+
+ # Remove the call to Libtool.
+ if test "$libtool" = yes; then
+ while test "X$1" != 'X--mode=compile'; do
+ shift
+ done
+ shift
+ fi
+
+ # Remove '-o $object'.
+ IFS=" "
+ for arg
+ do
+ case $arg in
+ -o)
+ shift
+ ;;
+ $object)
+ shift
+ ;;
+ *)
+ set fnord "$@" "$arg"
+ shift # fnord
+ shift # $arg
+ ;;
+ esac
+ done
+
+ test -z "$dashmflag" && dashmflag=-M
+ # Require at least two characters before searching for ':'
+ # in the target name. This is to cope with DOS-style filenames:
+ # a dependency such as 'c:/foo/bar' could be seen as target 'c' otherwise.
+ "$@" $dashmflag |
+ sed "s|^[$tab ]*[^:$tab ][^:][^:]*:[$tab ]*|$object: |" > "$tmpdepfile"
+ rm -f "$depfile"
+ cat < "$tmpdepfile" > "$depfile"
+ # Some versions of the HPUX 10.20 sed can't process this sed invocation
+ # correctly. Breaking it into two sed invocations is a workaround.
+ tr ' ' "$nl" < "$tmpdepfile" \
+ | sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' \
+ | sed -e 's/$/ :/' >> "$depfile"
+ rm -f "$tmpdepfile"
+ ;;
+
+dashXmstdout)
+ # This case only exists to satisfy depend.m4. It is never actually
+ # run, as this mode is specially recognized in the preamble.
+ exit 1
+ ;;
+
+makedepend)
+ "$@" || exit $?
+ # Remove any Libtool call
+ if test "$libtool" = yes; then
+ while test "X$1" != 'X--mode=compile'; do
+ shift
+ done
+ shift
+ fi
+ # X makedepend
+ shift
+ cleared=no eat=no
+ for arg
+ do
+ case $cleared in
+ no)
+ set ""; shift
+ cleared=yes ;;
+ esac
+ if test $eat = yes; then
+ eat=no
+ continue
+ fi
+ case "$arg" in
+ -D*|-I*)
+ set fnord "$@" "$arg"; shift ;;
+ # Strip any option that makedepend may not understand. Remove
+ # the object too, otherwise makedepend will parse it as a source file.
+ -arch)
+ eat=yes ;;
+ -*|$object)
+ ;;
+ *)
+ set fnord "$@" "$arg"; shift ;;
+ esac
+ done
+ obj_suffix=`echo "$object" | sed 's/^.*\././'`
+ touch "$tmpdepfile"
+ ${MAKEDEPEND-makedepend} -o"$obj_suffix" -f"$tmpdepfile" "$@"
+ rm -f "$depfile"
+ # makedepend may prepend the VPATH from the source file name to the object.
+ # No need to regex-escape $object, excess matching of '.' is harmless.
+ sed "s|^.*\($object *:\)|\1|" "$tmpdepfile" > "$depfile"
+ # Some versions of the HPUX 10.20 sed can't process the last invocation
+ # correctly. Breaking it into two sed invocations is a workaround.
+ sed '1,2d' "$tmpdepfile" \
+ | tr ' ' "$nl" \
+ | sed -e 's/^\\$//' -e '/^$/d' -e '/:$/d' \
+ | sed -e 's/$/ :/' >> "$depfile"
+ rm -f "$tmpdepfile" "$tmpdepfile".bak
+ ;;
+
+cpp)
+ # Important note: in order to support this mode, a compiler *must*
+ # always write the preprocessed file to stdout.
+ "$@" || exit $?
+
+ # Remove the call to Libtool.
+ if test "$libtool" = yes; then
+ while test "X$1" != 'X--mode=compile'; do
+ shift
+ done
+ shift
+ fi
+
+ # Remove '-o $object'.
+ IFS=" "
+ for arg
+ do
+ case $arg in
+ -o)
+ shift
+ ;;
+ $object)
+ shift
+ ;;
+ *)
+ set fnord "$@" "$arg"
+ shift # fnord
+ shift # $arg
+ ;;
+ esac
+ done
+
+ "$@" -E \
+ | sed -n -e '/^# [0-9][0-9]* "\([^"]*\)".*/ s:: \1 \\:p' \
+ -e '/^#line [0-9][0-9]* "\([^"]*\)".*/ s:: \1 \\:p' \
+ | sed '$ s: \\$::' > "$tmpdepfile"
+ rm -f "$depfile"
+ echo "$object : \\" > "$depfile"
+ cat < "$tmpdepfile" >> "$depfile"
+ sed < "$tmpdepfile" '/^$/d;s/^ //;s/ \\$//;s/$/ :/' >> "$depfile"
+ rm -f "$tmpdepfile"
+ ;;
+
+msvisualcpp)
+ # Important note: in order to support this mode, a compiler *must*
+ # always write the preprocessed file to stdout.
+ "$@" || exit $?
+
+ # Remove the call to Libtool.
+ if test "$libtool" = yes; then
+ while test "X$1" != 'X--mode=compile'; do
+ shift
+ done
+ shift
+ fi
+
+ IFS=" "
+ for arg
+ do
+ case "$arg" in
+ -o)
+ shift
+ ;;
+ $object)
+ shift
+ ;;
+ "-Gm"|"/Gm"|"-Gi"|"/Gi"|"-ZI"|"/ZI")
+ set fnord "$@"
+ shift
+ shift
+ ;;
+ *)
+ set fnord "$@" "$arg"
+ shift
+ shift
+ ;;
+ esac
+ done
+ "$@" -E 2>/dev/null |
+ sed -n '/^#line [0-9][0-9]* "\([^"]*\)"/ s::\1:p' | $cygpath_u | sort -u > "$tmpdepfile"
+ rm -f "$depfile"
+ echo "$object : \\" > "$depfile"
+ sed < "$tmpdepfile" -n -e 's% %\\ %g' -e '/^\(.*\)$/ s::'"$tab"'\1 \\:p' >> "$depfile"
+ echo "$tab" >> "$depfile"
+ sed < "$tmpdepfile" -n -e 's% %\\ %g' -e '/^\(.*\)$/ s::\1\::p' >> "$depfile"
+ rm -f "$tmpdepfile"
+ ;;
+
+msvcmsys)
+ # This case exists only to let depend.m4 do its work. It works by
+ # looking at the text of this script. This case will never be run,
+ # since it is checked for above.
+ exit 1
+ ;;
+
+none)
+ exec "$@"
+ ;;
+
+*)
+ echo "Unknown depmode $depmode" 1>&2
+ exit 1
+ ;;
+esac
+
+exit 0
+
+# Local Variables:
+# mode: shell-script
+# sh-indentation: 2
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-time-zone: "UTC0"
+# time-stamp-end: "; # UTC"
+# End:
diff --git a/tools/escape-to-c b/tools/escape-to-c
new file mode 100755
index 0000000..b973261
--- /dev/null
+++ b/tools/escape-to-c
@@ -0,0 +1,12 @@
+#!/usr/bin/python3
+
+# ...until #embed comes along...
+# eg: tools/escape-to-c cockpit_webresponse_fail_html_text < fail.html > fail.html.c
+
+import json
+import sys
+
+# Use a pointer to allow overriding the value
+print('const char *', sys.argv[1], '=\n',
+ *[json.dumps(line) + '\n' for line in sys.stdin],
+ ';')
diff --git a/tools/fix-spec b/tools/fix-spec
new file mode 100755
index 0000000..9572df3
--- /dev/null
+++ b/tools/fix-spec
@@ -0,0 +1,17 @@
+#!/bin/sh
+# patch given spec file to have the correct version and declare bundled NPM dependencies
+# Usage: fix-spec <spec-file> <version>
+set -eu
+
+spec="$1"
+version="$2"
+
+PROVIDES=$(npm ls --omit dev --package-lock-only --depth=Infinity |
+ grep -Eo '[^[:space:]]+@[^[:space:]]+' |
+ sort -u |
+ # only replace the *last* occurrence of @, not e.g. the one in @patternfly/..
+ sed 's/^/Provides: bundled(npm(/; s/\(.*\)@/\1)) = /')
+
+
+awk -v p="$PROVIDES" '/Version/ { gsub(/0/, "'$version'") }; gsub(/#NPM_PROVIDES/, p) 1' "$spec" > "$spec".new
+mv -f "$spec".new "$spec"
diff --git a/tools/git-hook-post-commit b/tools/git-hook-post-commit
new file mode 100755
index 0000000..7d84cbd
--- /dev/null
+++ b/tools/git-hook-post-commit
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+set -eu
+
+# if being run directly, add a dash of red
+if [ -t 1 ]; then
+ trap 'printf "\e[0m"' EXIT
+ printf "\e[1;31m"
+fi
+
+# resolve to a sha, ensure validity
+readonly commit="$(git rev-parse --verify "${1:-HEAD}^{commit}")"
+
+tmpdir="tmp/pre-push/${commit}"
+rm -rf "tmp/pre-push/${commit}"
+
+# unpack all of the changed files, plus the test/static-code from that commit
+changed_files="$(git diff-tree --no-commit-id --no-renames --diff-filter=d --name-only -r "${commit}" --)"
+required_files="$(git cat-file -p "${commit}":test/static-code | sed -n 's/^# requires: //p') test/static-code"
+git archive --prefix="${tmpdir}/" "${commit}" -- ${changed_files} ${required_files} | tar x
+
+# check that node_modules always gets updated with package.json
+if [ -e "${tmpdir}/package.json" -a ! -e "${tmpdir}/node_modules" ]; then
+ echo 'not ok 0 /node-modules/update'
+ echo '# package.json changed, but node_modules not updated'
+fi
+
+# if any of the following commands fail, the function
+# will abort, leaving the temp directory in place
+(
+ unset -- $(env | sed -n '/^GIT_/{s/=.*//p}')
+ cd "${tmpdir}"
+ git init -qb main .
+ git add .
+ WITH_PARTIAL_TREE=1 test/static-code
+)
+
+# paranoia: make sure we delete what we think we will
+rm -rf "tmp/pre-push/${commit}"
+rmdir --ignore-fail-on-non-empty tmp/pre-push tmp
diff --git a/tools/git-hook-pre-push b/tools/git-hook-pre-push
new file mode 100755
index 0000000..7c868ed
--- /dev/null
+++ b/tools/git-hook-pre-push
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+set -eu
+
+if [ -t 2 ]; then
+ red="\e[1;31m"
+ green="\e[1;32m"
+ blue="\e[1;34m"
+ grey="\e[0m"
+else
+ red=""
+ green=""
+ blue=""
+ grey=""
+fi
+
+post_commit_hook="$(realpath -m "$0"/../git-hook-post-commit)"
+fail=''
+
+# cf. man 5 githooks
+# <local ref> SP <local object name> SP <remote ref> SP <remote object name> LF
+while read local_ref local_object_name remote_ref remote_object_name; do
+ # never check commits already on origin
+ opts='^origin'
+
+ # if we have the commit that the remote branch pointed to before, exclude it as well
+ opts="${opts} $(git rev-parse --not --quiet --verify "${remote_object_name}^{commit}" || true)"
+
+ # check the object being pushed (if it's not null)
+ opts="${opts} $(git rev-parse --quiet --verify "${local_object_name}^{commit}" || true)"
+
+ # compute the list of commits to check
+ commits="$(git rev-list ${opts})"
+ [ -z "${commits}" ] && continue
+
+ printf "${blue}Performing pre-push checks for %s:${grey}\n" "${remote_ref}" >&2
+ for commit in ${commits}; do
+ subject="$(git show --no-patch --format='%h %s' "${commit}" --)"
+
+ if output="$("${post_commit_hook}" "${commit}")" && test -z "${output}"; then
+ printf " - ${green}%s${grey}\n" "${subject}"
+ else
+ printf " - ${red}%s${grey}${output:+:}\n" "${subject}"
+ printf "%s" "${output}" | sed 's/^/ /;$a\\'
+ fail=1
+ fi
+ done
+done
+
+exit "${fail:-0}"
diff --git a/tools/git-hook-pre-rebase b/tools/git-hook-pre-rebase
new file mode 100755
index 0000000..3a98a10
--- /dev/null
+++ b/tools/git-hook-pre-rebase
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+rebase_onto="$1"
+
+for dir in node_modules test/reference; do
+ # If a particular submodule is...
+ if test -e "${dir}/.git" && # ...checked out...
+ test -z "$(git status --porcelain "${dir}")" && # ...clean...
+ ! git diff --quiet "${rebase_onto}" -- "${dir}" # ...and changed on origin...
+ then
+ # then save ourselves the trouble of it being wrong post-rebase.
+ echo "Removing soon-to-be out-of-date ${dir}..."
+ rm -rf "${dir}"
+ mkdir "${dir}"
+ fi
+done
diff --git a/tools/glib.supp b/tools/glib.supp
new file mode 100644
index 0000000..58a9151
--- /dev/null
+++ b/tools/glib.supp
@@ -0,0 +1,15 @@
+# cockpit specific valgrind glib suppressions which are not in /usr/share/glib-2.0/valgrind/glib.supp
+{
+ leak_dbus_message_from_blob
+ Memcheck:Leak
+ match-leak-kinds: definite
+ fun:malloc
+ fun:g_malloc
+ fun:g_slice_alloc
+ ...
+ fun:g_variant_new_dict_entry
+ ...
+ fun:g_dbus_message_new_from_blob
+ ...
+}
+
diff --git a/tools/install-sh b/tools/install-sh
new file mode 100755
index 0000000..ec298b5
--- /dev/null
+++ b/tools/install-sh
@@ -0,0 +1,541 @@
+#!/bin/sh
+# install - install a program, script, or datafile
+
+scriptversion=2020-11-14.01; # UTC
+
+# This originates from X11R5 (mit/util/scripts/install.sh), which was
+# later released in X11R6 (xc/config/util/install.sh) with the
+# following copyright and license.
+#
+# Copyright (C) 1994 X Consortium
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC-
+# TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# Except as contained in this notice, the name of the X Consortium shall not
+# be used in advertising or otherwise to promote the sale, use or other deal-
+# ings in this Software without prior written authorization from the X Consor-
+# tium.
+#
+#
+# FSF changes to this file are in the public domain.
+#
+# Calling this script install-sh is preferred over install.sh, to prevent
+# 'make' implicit rules from creating a file called install from it
+# when there is no Makefile.
+#
+# This script is compatible with the BSD install script, but was written
+# from scratch.
+
+tab=' '
+nl='
+'
+IFS=" $tab$nl"
+
+# Set DOITPROG to "echo" to test this script.
+
+doit=${DOITPROG-}
+doit_exec=${doit:-exec}
+
+# Put in absolute file names if you don't have them in your path;
+# or use environment vars.
+
+chgrpprog=${CHGRPPROG-chgrp}
+chmodprog=${CHMODPROG-chmod}
+chownprog=${CHOWNPROG-chown}
+cmpprog=${CMPPROG-cmp}
+cpprog=${CPPROG-cp}
+mkdirprog=${MKDIRPROG-mkdir}
+mvprog=${MVPROG-mv}
+rmprog=${RMPROG-rm}
+stripprog=${STRIPPROG-strip}
+
+posix_mkdir=
+
+# Desired mode of installed file.
+mode=0755
+
+# Create dirs (including intermediate dirs) using mode 755.
+# This is like GNU 'install' as of coreutils 8.32 (2020).
+mkdir_umask=22
+
+backupsuffix=
+chgrpcmd=
+chmodcmd=$chmodprog
+chowncmd=
+mvcmd=$mvprog
+rmcmd="$rmprog -f"
+stripcmd=
+
+src=
+dst=
+dir_arg=
+dst_arg=
+
+copy_on_change=false
+is_target_a_directory=possibly
+
+usage="\
+Usage: $0 [OPTION]... [-T] SRCFILE DSTFILE
+ or: $0 [OPTION]... SRCFILES... DIRECTORY
+ or: $0 [OPTION]... -t DIRECTORY SRCFILES...
+ or: $0 [OPTION]... -d DIRECTORIES...
+
+In the 1st form, copy SRCFILE to DSTFILE.
+In the 2nd and 3rd, copy all SRCFILES to DIRECTORY.
+In the 4th, create DIRECTORIES.
+
+Options:
+ --help display this help and exit.
+ --version display version info and exit.
+
+ -c (ignored)
+ -C install only if different (preserve data modification time)
+ -d create directories instead of installing files.
+ -g GROUP $chgrpprog installed files to GROUP.
+ -m MODE $chmodprog installed files to MODE.
+ -o USER $chownprog installed files to USER.
+ -p pass -p to $cpprog.
+ -s $stripprog installed files.
+ -S SUFFIX attempt to back up existing files, with suffix SUFFIX.
+ -t DIRECTORY install into DIRECTORY.
+ -T report an error if DSTFILE is a directory.
+
+Environment variables override the default commands:
+ CHGRPPROG CHMODPROG CHOWNPROG CMPPROG CPPROG MKDIRPROG MVPROG
+ RMPROG STRIPPROG
+
+By default, rm is invoked with -f; when overridden with RMPROG,
+it's up to you to specify -f if you want it.
+
+If -S is not specified, no backups are attempted.
+
+Email bug reports to bug-automake@gnu.org.
+Automake home page: https://www.gnu.org/software/automake/
+"
+
+while test $# -ne 0; do
+ case $1 in
+ -c) ;;
+
+ -C) copy_on_change=true;;
+
+ -d) dir_arg=true;;
+
+ -g) chgrpcmd="$chgrpprog $2"
+ shift;;
+
+ --help) echo "$usage"; exit $?;;
+
+ -m) mode=$2
+ case $mode in
+ *' '* | *"$tab"* | *"$nl"* | *'*'* | *'?'* | *'['*)
+ echo "$0: invalid mode: $mode" >&2
+ exit 1;;
+ esac
+ shift;;
+
+ -o) chowncmd="$chownprog $2"
+ shift;;
+
+ -p) cpprog="$cpprog -p";;
+
+ -s) stripcmd=$stripprog;;
+
+ -S) backupsuffix="$2"
+ shift;;
+
+ -t)
+ is_target_a_directory=always
+ dst_arg=$2
+ # Protect names problematic for 'test' and other utilities.
+ case $dst_arg in
+ -* | [=\(\)!]) dst_arg=./$dst_arg;;
+ esac
+ shift;;
+
+ -T) is_target_a_directory=never;;
+
+ --version) echo "$0 $scriptversion"; exit $?;;
+
+ --) shift
+ break;;
+
+ -*) echo "$0: invalid option: $1" >&2
+ exit 1;;
+
+ *) break;;
+ esac
+ shift
+done
+
+# We allow the use of options -d and -T together, by making -d
+# take the precedence; this is for compatibility with GNU install.
+
+if test -n "$dir_arg"; then
+ if test -n "$dst_arg"; then
+ echo "$0: target directory not allowed when installing a directory." >&2
+ exit 1
+ fi
+fi
+
+if test $# -ne 0 && test -z "$dir_arg$dst_arg"; then
+ # When -d is used, all remaining arguments are directories to create.
+ # When -t is used, the destination is already specified.
+ # Otherwise, the last argument is the destination. Remove it from $@.
+ for arg
+ do
+ if test -n "$dst_arg"; then
+ # $@ is not empty: it contains at least $arg.
+ set fnord "$@" "$dst_arg"
+ shift # fnord
+ fi
+ shift # arg
+ dst_arg=$arg
+ # Protect names problematic for 'test' and other utilities.
+ case $dst_arg in
+ -* | [=\(\)!]) dst_arg=./$dst_arg;;
+ esac
+ done
+fi
+
+if test $# -eq 0; then
+ if test -z "$dir_arg"; then
+ echo "$0: no input file specified." >&2
+ exit 1
+ fi
+ # It's OK to call 'install-sh -d' without argument.
+ # This can happen when creating conditional directories.
+ exit 0
+fi
+
+if test -z "$dir_arg"; then
+ if test $# -gt 1 || test "$is_target_a_directory" = always; then
+ if test ! -d "$dst_arg"; then
+ echo "$0: $dst_arg: Is not a directory." >&2
+ exit 1
+ fi
+ fi
+fi
+
+if test -z "$dir_arg"; then
+ do_exit='(exit $ret); exit $ret'
+ trap "ret=129; $do_exit" 1
+ trap "ret=130; $do_exit" 2
+ trap "ret=141; $do_exit" 13
+ trap "ret=143; $do_exit" 15
+
+ # Set umask so as not to create temps with too-generous modes.
+ # However, 'strip' requires both read and write access to temps.
+ case $mode in
+ # Optimize common cases.
+ *644) cp_umask=133;;
+ *755) cp_umask=22;;
+
+ *[0-7])
+ if test -z "$stripcmd"; then
+ u_plus_rw=
+ else
+ u_plus_rw='% 200'
+ fi
+ cp_umask=`expr '(' 777 - $mode % 1000 ')' $u_plus_rw`;;
+ *)
+ if test -z "$stripcmd"; then
+ u_plus_rw=
+ else
+ u_plus_rw=,u+rw
+ fi
+ cp_umask=$mode$u_plus_rw;;
+ esac
+fi
+
+for src
+do
+ # Protect names problematic for 'test' and other utilities.
+ case $src in
+ -* | [=\(\)!]) src=./$src;;
+ esac
+
+ if test -n "$dir_arg"; then
+ dst=$src
+ dstdir=$dst
+ test -d "$dstdir"
+ dstdir_status=$?
+ # Don't chown directories that already exist.
+ if test $dstdir_status = 0; then
+ chowncmd=""
+ fi
+ else
+
+ # Waiting for this to be detected by the "$cpprog $src $dsttmp" command
+ # might cause directories to be created, which would be especially bad
+ # if $src (and thus $dsttmp) contains '*'.
+ if test ! -f "$src" && test ! -d "$src"; then
+ echo "$0: $src does not exist." >&2
+ exit 1
+ fi
+
+ if test -z "$dst_arg"; then
+ echo "$0: no destination specified." >&2
+ exit 1
+ fi
+ dst=$dst_arg
+
+ # If destination is a directory, append the input filename.
+ if test -d "$dst"; then
+ if test "$is_target_a_directory" = never; then
+ echo "$0: $dst_arg: Is a directory" >&2
+ exit 1
+ fi
+ dstdir=$dst
+ dstbase=`basename "$src"`
+ case $dst in
+ */) dst=$dst$dstbase;;
+ *) dst=$dst/$dstbase;;
+ esac
+ dstdir_status=0
+ else
+ dstdir=`dirname "$dst"`
+ test -d "$dstdir"
+ dstdir_status=$?
+ fi
+ fi
+
+ case $dstdir in
+ */) dstdirslash=$dstdir;;
+ *) dstdirslash=$dstdir/;;
+ esac
+
+ obsolete_mkdir_used=false
+
+ if test $dstdir_status != 0; then
+ case $posix_mkdir in
+ '')
+ # With -d, create the new directory with the user-specified mode.
+ # Otherwise, rely on $mkdir_umask.
+ if test -n "$dir_arg"; then
+ mkdir_mode=-m$mode
+ else
+ mkdir_mode=
+ fi
+
+ posix_mkdir=false
+ # The $RANDOM variable is not portable (e.g., dash). Use it
+ # here however when possible just to lower collision chance.
+ tmpdir=${TMPDIR-/tmp}/ins$RANDOM-$$
+
+ trap '
+ ret=$?
+ rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir" 2>/dev/null
+ exit $ret
+ ' 0
+
+ # Because "mkdir -p" follows existing symlinks and we likely work
+ # directly in world-writeable /tmp, make sure that the '$tmpdir'
+ # directory is successfully created first before we actually test
+ # 'mkdir -p'.
+ if (umask $mkdir_umask &&
+ $mkdirprog $mkdir_mode "$tmpdir" &&
+ exec $mkdirprog $mkdir_mode -p -- "$tmpdir/a/b") >/dev/null 2>&1
+ then
+ if test -z "$dir_arg" || {
+ # Check for POSIX incompatibilities with -m.
+ # HP-UX 11.23 and IRIX 6.5 mkdir -m -p sets group- or
+ # other-writable bit of parent directory when it shouldn't.
+ # FreeBSD 6.1 mkdir -m -p sets mode of existing directory.
+ test_tmpdir="$tmpdir/a"
+ ls_ld_tmpdir=`ls -ld "$test_tmpdir"`
+ case $ls_ld_tmpdir in
+ d????-?r-*) different_mode=700;;
+ d????-?--*) different_mode=755;;
+ *) false;;
+ esac &&
+ $mkdirprog -m$different_mode -p -- "$test_tmpdir" && {
+ ls_ld_tmpdir_1=`ls -ld "$test_tmpdir"`
+ test "$ls_ld_tmpdir" = "$ls_ld_tmpdir_1"
+ }
+ }
+ then posix_mkdir=:
+ fi
+ rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir"
+ else
+ # Remove any dirs left behind by ancient mkdir implementations.
+ rmdir ./$mkdir_mode ./-p ./-- "$tmpdir" 2>/dev/null
+ fi
+ trap '' 0;;
+ esac
+
+ if
+ $posix_mkdir && (
+ umask $mkdir_umask &&
+ $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir"
+ )
+ then :
+ else
+
+ # mkdir does not conform to POSIX,
+ # or it failed possibly due to a race condition. Create the
+ # directory the slow way, step by step, checking for races as we go.
+
+ case $dstdir in
+ /*) prefix='/';;
+ [-=\(\)!]*) prefix='./';;
+ *) prefix='';;
+ esac
+
+ oIFS=$IFS
+ IFS=/
+ set -f
+ set fnord $dstdir
+ shift
+ set +f
+ IFS=$oIFS
+
+ prefixes=
+
+ for d
+ do
+ test X"$d" = X && continue
+
+ prefix=$prefix$d
+ if test -d "$prefix"; then
+ prefixes=
+ else
+ if $posix_mkdir; then
+ (umask $mkdir_umask &&
+ $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir") && break
+ # Don't fail if two instances are running concurrently.
+ test -d "$prefix" || exit 1
+ else
+ case $prefix in
+ *\'*) qprefix=`echo "$prefix" | sed "s/'/'\\\\\\\\''/g"`;;
+ *) qprefix=$prefix;;
+ esac
+ prefixes="$prefixes '$qprefix'"
+ fi
+ fi
+ prefix=$prefix/
+ done
+
+ if test -n "$prefixes"; then
+ # Don't fail if two instances are running concurrently.
+ (umask $mkdir_umask &&
+ eval "\$doit_exec \$mkdirprog $prefixes") ||
+ test -d "$dstdir" || exit 1
+ obsolete_mkdir_used=true
+ fi
+ fi
+ fi
+
+ if test -n "$dir_arg"; then
+ { test -z "$chowncmd" || $doit $chowncmd "$dst"; } &&
+ { test -z "$chgrpcmd" || $doit $chgrpcmd "$dst"; } &&
+ { test "$obsolete_mkdir_used$chowncmd$chgrpcmd" = false ||
+ test -z "$chmodcmd" || $doit $chmodcmd $mode "$dst"; } || exit 1
+ else
+
+ # Make a couple of temp file names in the proper directory.
+ dsttmp=${dstdirslash}_inst.$$_
+ rmtmp=${dstdirslash}_rm.$$_
+
+ # Trap to clean up those temp files at exit.
+ trap 'ret=$?; rm -f "$dsttmp" "$rmtmp" && exit $ret' 0
+
+ # Copy the file name to the temp name.
+ (umask $cp_umask &&
+ { test -z "$stripcmd" || {
+ # Create $dsttmp read-write so that cp doesn't create it read-only,
+ # which would cause strip to fail.
+ if test -z "$doit"; then
+ : >"$dsttmp" # No need to fork-exec 'touch'.
+ else
+ $doit touch "$dsttmp"
+ fi
+ }
+ } &&
+ $doit_exec $cpprog "$src" "$dsttmp") &&
+
+ # and set any options; do chmod last to preserve setuid bits.
+ #
+ # If any of these fail, we abort the whole thing. If we want to
+ # ignore errors from any of these, just make sure not to ignore
+ # errors from the above "$doit $cpprog $src $dsttmp" command.
+ #
+ { test -z "$chowncmd" || $doit $chowncmd "$dsttmp"; } &&
+ { test -z "$chgrpcmd" || $doit $chgrpcmd "$dsttmp"; } &&
+ { test -z "$stripcmd" || $doit $stripcmd "$dsttmp"; } &&
+ { test -z "$chmodcmd" || $doit $chmodcmd $mode "$dsttmp"; } &&
+
+ # If -C, don't bother to copy if it wouldn't change the file.
+ if $copy_on_change &&
+ old=`LC_ALL=C ls -dlL "$dst" 2>/dev/null` &&
+ new=`LC_ALL=C ls -dlL "$dsttmp" 2>/dev/null` &&
+ set -f &&
+ set X $old && old=:$2:$4:$5:$6 &&
+ set X $new && new=:$2:$4:$5:$6 &&
+ set +f &&
+ test "$old" = "$new" &&
+ $cmpprog "$dst" "$dsttmp" >/dev/null 2>&1
+ then
+ rm -f "$dsttmp"
+ else
+ # If $backupsuffix is set, and the file being installed
+ # already exists, attempt a backup. Don't worry if it fails,
+ # e.g., if mv doesn't support -f.
+ if test -n "$backupsuffix" && test -f "$dst"; then
+ $doit $mvcmd -f "$dst" "$dst$backupsuffix" 2>/dev/null
+ fi
+
+ # Rename the file to the real destination.
+ $doit $mvcmd -f "$dsttmp" "$dst" 2>/dev/null ||
+
+ # The rename failed, perhaps because mv can't rename something else
+ # to itself, or perhaps because mv is so ancient that it does not
+ # support -f.
+ {
+ # Now remove or move aside any old file at destination location.
+ # We try this two ways since rm can't unlink itself on some
+ # systems and the destination file might be busy for other
+ # reasons. In this case, the final cleanup might fail but the new
+ # file should still install successfully.
+ {
+ test ! -f "$dst" ||
+ $doit $rmcmd "$dst" 2>/dev/null ||
+ { $doit $mvcmd -f "$dst" "$rmtmp" 2>/dev/null &&
+ { $doit $rmcmd "$rmtmp" 2>/dev/null; :; }
+ } ||
+ { echo "$0: cannot unlink or rename $dst" >&2
+ (exit 1); exit 1
+ }
+ } &&
+
+ # Now rename the file to the real destination.
+ $doit $mvcmd "$dsttmp" "$dst"
+ }
+ fi || exit 1
+
+ trap '' 0
+ fi
+done
+
+# Local variables:
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-time-zone: "UTC0"
+# time-stamp-end: "; # UTC"
+# End:
diff --git a/tools/make-debs b/tools/make-debs
new file mode 100755
index 0000000..863b94f
--- /dev/null
+++ b/tools/make-debs
@@ -0,0 +1,77 @@
+#!/bin/bash
+# This file is part of Cockpit.
+#
+# Copyright (C) 2021 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+set -euf
+
+mydir=$(dirname $(realpath "$0"))
+
+usage()
+{
+ echo "usage: make-debs [--quick] [tarball]"
+}
+
+args=$(getopt -o "h,q" -l "help,quick" -- "$@")
+eval set -- "$args"
+while [ $# -gt 0 ]; do
+ case $1 in
+ -q|--quick)
+ export DEB_BUILD_OPTIONS=nocheck
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ --)
+ shift
+ break
+ ;;
+ esac
+ shift
+done
+
+if [ $# -gt 1 ]; then
+ usage
+ exit 2
+fi
+
+if [ -z "${1:-}" ]; then
+ source="$("$mydir/../tools/make-dist")"
+else
+ source="$(realpath "$1")"
+fi
+
+version=$(echo "$source" | sed -n 's|.*cockpit-\([^ /-]\+\)\.tar\..*|\1|p')
+if [ -z "$version" ]; then
+ echo "make-debs: couldn't parse version from tarball: $source"
+ exit 2
+fi
+
+builddir=tmp/builddeb
+rm -rf "$builddir"
+mkdir -p "$builddir"
+cd "$builddir"
+tar --strip-components=1 -xf "$source"
+cp -r tools/debian .
+# plug in our version
+sed -i "1 s/(0-/(${version}-/" debian/changelog
+dpkg-buildpackage -us -uc -b -j$(nproc)
+cd -
+
+find "$builddir"/.. -name '*.deb' -printf '%f\n' -exec mv {} . \;
+
+rm -r "$builddir"
diff --git a/tools/make-dist b/tools/make-dist
new file mode 100755
index 0000000..d2edb8e
--- /dev/null
+++ b/tools/make-dist
@@ -0,0 +1,56 @@
+#!/bin/sh
+
+set -eu
+cd "$(realpath -m "$0"/../..)"
+
+message() { printf " %-8s %s\n" "$1" "$2" >&2; }
+
+# Set the version number from the state at the time the build was requested
+VERSION="$(git describe --tags --long --dirty=.dirty | sed 's/-\([0-9]*\)-g/.dev\1+g/')"
+
+while [ $# != 0 ] ; do
+ case "$1" in
+ *=*)
+ break;;
+ *)
+ echo "usage: $0 [MAKE_VAR=VALUE...]" >&2
+ exit 1
+ esac
+ shift
+done
+
+# NB: only the filename of the tarball should be written to stdout.
+# Everything else should go to stderr.
+
+# autogen, if not already done
+if [ ! configure -nt configure.ac ]; then
+ message AUTOGEN 'configure, Makefile.in'
+ # Unfortunately there's no better way to silence this
+ NOCONFIGURE=1 ./autogen.sh >/dev/null 2>&1
+fi
+
+# If we have a configured tree, use it to build the tarball. Otherwise, create
+# a temporary directory and configure it with a minimal config to build the
+# tarball.
+if [ -f Makefile ]; then
+ :
+else
+ builddir='tmp/build-dist'
+ rm -rf "${builddir}"
+ mkdir -p "${builddir}"
+ cd "${builddir}"
+ message CONFIG "${builddir}/Makefile"
+ ../../configure \
+ CPPFLAGS=-I../../tools/mock-build-env \
+ PKG_CONFIG_PATH=../../tools/mock-build-env \
+ --disable-doc \
+ --enable-prefix-only \
+ --disable-pcp \
+ --disable-polkit \
+ --disable-ssh \
+ > /dev/null
+fi
+
+# Do the actual build
+make -s -j"$(nproc)" XZ_OPT='-0' VERSION="${VERSION}" "$@" dist >&2
+realpath cockpit-${VERSION}.tar.xz # (only) result to stdout
diff --git a/tools/make-rpms b/tools/make-rpms
new file mode 100755
index 0000000..bea4fee
--- /dev/null
+++ b/tools/make-rpms
@@ -0,0 +1,71 @@
+#!/bin/bash
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+set -euf
+
+base=$(cd $(dirname $0)/../; pwd -P)
+
+usage()
+{
+ echo "usage: make-rpms [--quick] [--verbose] [tarball]"
+}
+
+quiet="--quiet"
+check=""
+
+args=$(getopt -o "h,q,v" -l "help,quick,verbose" -- "$@")
+eval set -- "$args"
+while [ $# -gt 0 ]; do
+ case $1 in
+ -v|--verbose)
+ quiet=""
+ ;;
+ -q|--quick)
+ check="--nocheck"
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ --)
+ shift
+ break
+ ;;
+ esac
+ shift
+done
+
+if [ $# -gt 1 ]; then
+ usage
+ exit 2
+fi
+
+if [ -z "${1-}" ]; then
+ source="$("$base/tools/make-dist")"
+else
+ source="$1"
+fi
+
+tmpdir=$(mktemp -d $PWD/rpm-build.XXXXXX)
+rpmbuild $check $quiet \
+ --define "_topdir $tmpdir" \
+ --define "_rpmdir $tmpdir/output" \
+ -tb "$source"
+
+find $tmpdir/output -name '*.rpm' -printf '%f\n' -exec mv {} . \;
+rm -r $tmpdir
diff --git a/tools/missing b/tools/missing
new file mode 100755
index 0000000..1fe1611
--- /dev/null
+++ b/tools/missing
@@ -0,0 +1,215 @@
+#! /bin/sh
+# Common wrapper for a few potentially missing GNU programs.
+
+scriptversion=2018-03-07.03; # UTC
+
+# Copyright (C) 1996-2021 Free Software Foundation, Inc.
+# Originally written by Fran,cois Pinard <pinard@iro.umontreal.ca>, 1996.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+if test $# -eq 0; then
+ echo 1>&2 "Try '$0 --help' for more information"
+ exit 1
+fi
+
+case $1 in
+
+ --is-lightweight)
+ # Used by our autoconf macros to check whether the available missing
+ # script is modern enough.
+ exit 0
+ ;;
+
+ --run)
+ # Back-compat with the calling convention used by older automake.
+ shift
+ ;;
+
+ -h|--h|--he|--hel|--help)
+ echo "\
+$0 [OPTION]... PROGRAM [ARGUMENT]...
+
+Run 'PROGRAM [ARGUMENT]...', returning a proper advice when this fails due
+to PROGRAM being missing or too old.
+
+Options:
+ -h, --help display this help and exit
+ -v, --version output version information and exit
+
+Supported PROGRAM values:
+ aclocal autoconf autoheader autom4te automake makeinfo
+ bison yacc flex lex help2man
+
+Version suffixes to PROGRAM as well as the prefixes 'gnu-', 'gnu', and
+'g' are ignored when checking the name.
+
+Send bug reports to <bug-automake@gnu.org>."
+ exit $?
+ ;;
+
+ -v|--v|--ve|--ver|--vers|--versi|--versio|--version)
+ echo "missing $scriptversion (GNU Automake)"
+ exit $?
+ ;;
+
+ -*)
+ echo 1>&2 "$0: unknown '$1' option"
+ echo 1>&2 "Try '$0 --help' for more information"
+ exit 1
+ ;;
+
+esac
+
+# Run the given program, remember its exit status.
+"$@"; st=$?
+
+# If it succeeded, we are done.
+test $st -eq 0 && exit 0
+
+# Also exit now if we it failed (or wasn't found), and '--version' was
+# passed; such an option is passed most likely to detect whether the
+# program is present and works.
+case $2 in --version|--help) exit $st;; esac
+
+# Exit code 63 means version mismatch. This often happens when the user
+# tries to use an ancient version of a tool on a file that requires a
+# minimum version.
+if test $st -eq 63; then
+ msg="probably too old"
+elif test $st -eq 127; then
+ # Program was missing.
+ msg="missing on your system"
+else
+ # Program was found and executed, but failed. Give up.
+ exit $st
+fi
+
+perl_URL=https://www.perl.org/
+flex_URL=https://github.com/westes/flex
+gnu_software_URL=https://www.gnu.org/software
+
+program_details ()
+{
+ case $1 in
+ aclocal|automake)
+ echo "The '$1' program is part of the GNU Automake package:"
+ echo "<$gnu_software_URL/automake>"
+ echo "It also requires GNU Autoconf, GNU m4 and Perl in order to run:"
+ echo "<$gnu_software_URL/autoconf>"
+ echo "<$gnu_software_URL/m4/>"
+ echo "<$perl_URL>"
+ ;;
+ autoconf|autom4te|autoheader)
+ echo "The '$1' program is part of the GNU Autoconf package:"
+ echo "<$gnu_software_URL/autoconf/>"
+ echo "It also requires GNU m4 and Perl in order to run:"
+ echo "<$gnu_software_URL/m4/>"
+ echo "<$perl_URL>"
+ ;;
+ esac
+}
+
+give_advice ()
+{
+ # Normalize program name to check for.
+ normalized_program=`echo "$1" | sed '
+ s/^gnu-//; t
+ s/^gnu//; t
+ s/^g//; t'`
+
+ printf '%s\n' "'$1' is $msg."
+
+ configure_deps="'configure.ac' or m4 files included by 'configure.ac'"
+ case $normalized_program in
+ autoconf*)
+ echo "You should only need it if you modified 'configure.ac',"
+ echo "or m4 files included by it."
+ program_details 'autoconf'
+ ;;
+ autoheader*)
+ echo "You should only need it if you modified 'acconfig.h' or"
+ echo "$configure_deps."
+ program_details 'autoheader'
+ ;;
+ automake*)
+ echo "You should only need it if you modified 'Makefile.am' or"
+ echo "$configure_deps."
+ program_details 'automake'
+ ;;
+ aclocal*)
+ echo "You should only need it if you modified 'acinclude.m4' or"
+ echo "$configure_deps."
+ program_details 'aclocal'
+ ;;
+ autom4te*)
+ echo "You might have modified some maintainer files that require"
+ echo "the 'autom4te' program to be rebuilt."
+ program_details 'autom4te'
+ ;;
+ bison*|yacc*)
+ echo "You should only need it if you modified a '.y' file."
+ echo "You may want to install the GNU Bison package:"
+ echo "<$gnu_software_URL/bison/>"
+ ;;
+ lex*|flex*)
+ echo "You should only need it if you modified a '.l' file."
+ echo "You may want to install the Fast Lexical Analyzer package:"
+ echo "<$flex_URL>"
+ ;;
+ help2man*)
+ echo "You should only need it if you modified a dependency" \
+ "of a man page."
+ echo "You may want to install the GNU Help2man package:"
+ echo "<$gnu_software_URL/help2man/>"
+ ;;
+ makeinfo*)
+ echo "You should only need it if you modified a '.texi' file, or"
+ echo "any other file indirectly affecting the aspect of the manual."
+ echo "You might want to install the Texinfo package:"
+ echo "<$gnu_software_URL/texinfo/>"
+ echo "The spurious makeinfo call might also be the consequence of"
+ echo "using a buggy 'make' (AIX, DU, IRIX), in which case you might"
+ echo "want to install GNU make:"
+ echo "<$gnu_software_URL/make/>"
+ ;;
+ *)
+ echo "You might have modified some files without having the proper"
+ echo "tools for further handling them. Check the 'README' file, it"
+ echo "often tells you about the needed prerequisites for installing"
+ echo "this package. You may also peek at any GNU archive site, in"
+ echo "case some other package contains this missing '$1' program."
+ ;;
+ esac
+}
+
+give_advice "$1" | sed -e '1s/^/WARNING: /' \
+ -e '2,$s/^/ /' >&2
+
+# Propagate the correct exit status (expected to be 127 for a program
+# not found, 63 for a program that failed due to version mismatch).
+exit $st
+
+# Local variables:
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-time-zone: "UTC0"
+# time-stamp-end: "; # UTC"
+# End:
diff --git a/tools/mock-build-env/README.md b/tools/mock-build-env/README.md
new file mode 100644
index 0000000..b76620f
--- /dev/null
+++ b/tools/mock-build-env/README.md
@@ -0,0 +1,5 @@
+These are some stub pkg-config and include files that allow us to run
+`make dist` in a restricted build environment without the regular build
+dependencies, in particular [Packit](https://packit.dev/)'s [sandcastle container](https://github.com/packit/sandcastle).
+
+These are used in [packit.yaml](./packit.yaml).
diff --git a/tools/mock-build-env/gnutls.pc b/tools/mock-build-env/gnutls.pc
new file mode 100644
index 0000000..35e266a
--- /dev/null
+++ b/tools/mock-build-env/gnutls.pc
@@ -0,0 +1,3 @@
+Name: GnuTLS
+Description: Mock PC for gnutls
+Version: 3.6.12.fake
diff --git a/tools/mock-build-env/json-glib-1.0.pc b/tools/mock-build-env/json-glib-1.0.pc
new file mode 100644
index 0000000..1f4bcce
--- /dev/null
+++ b/tools/mock-build-env/json-glib-1.0.pc
@@ -0,0 +1,3 @@
+Name: JSON-GLib
+Description: Mock PC for json-glib
+Version: 1.6.fake
diff --git a/tools/mock-build-env/krb5-gssapi.pc b/tools/mock-build-env/krb5-gssapi.pc
new file mode 100644
index 0000000..e1caa15
--- /dev/null
+++ b/tools/mock-build-env/krb5-gssapi.pc
@@ -0,0 +1,3 @@
+Name: krb5-gssapi
+Description: Mock PC for krb5-gssapi
+Version: 1.18.2.fake
diff --git a/tools/mock-build-env/krb5.pc b/tools/mock-build-env/krb5.pc
new file mode 100644
index 0000000..019742d
--- /dev/null
+++ b/tools/mock-build-env/krb5.pc
@@ -0,0 +1,3 @@
+Name: krb5
+Description: Fake PC for krb5
+Version: 1.18.2.fake
diff --git a/tools/mock-build-env/security/pam_appl.h b/tools/mock-build-env/security/pam_appl.h
new file mode 100644
index 0000000..823bc4e
--- /dev/null
+++ b/tools/mock-build-env/security/pam_appl.h
@@ -0,0 +1 @@
+/* stub header to satisfy configure check */
diff --git a/tools/node-modules b/tools/node-modules
new file mode 100755
index 0000000..a8519e1
--- /dev/null
+++ b/tools/node-modules
@@ -0,0 +1,198 @@
+#!/bin/sh
+
+# shellcheck disable=SC3043 # local is not POSIX, but every shell has it
+# shellcheck disable=SC3013,SC3045 # ditto for test {-nt,-t}
+
+GITHUB_REPO='node-cache'
+SUBDIR='node_modules'
+
+V="${V-0}" # default to friendly messages
+
+set -eu
+cd "${0%/*}/.."
+# shellcheck source-path=SCRIPTDIR/..
+. test/common/git-utils.sh
+
+cmd_remove() {
+ # if we did this for ourselves the rm is enough, but it might be the case
+ # that someone actually used git-submodule to fetch this, so clean up after
+ # that as well. NB: deinit nicely recreates the empty directory for us.
+ message REMOVE node_modules
+ rm -rf node_modules
+ git submodule deinit node_modules
+ rm -rf -- "$(git rev-parse --absolute-git-dir)/modules/node_modules"
+}
+
+cmd_checkout() {
+ # we default to check out the node_modules corresponding to the gitlink in the index
+ local force=""
+ if [ "${1-}" = "--force" ]; then
+ force="1"
+ shift
+ fi
+
+ local sha="${1-$(get_index_gitlink node_modules)}"
+
+ # fetch by sha to prevent us from downloading something we don't want
+ fetch_sha_to_cache "${sha}"
+
+ # verify that our package.json is equal to the one the cached node_modules
+ # was created with, unless --force is given
+ if [ -z "$force" ]; then
+ if ! cmp_from_cache "${sha}" '.package.json' 'package.json'; then
+ cat >&2 <<EOF
+
+*** node_modules ${sha} doesn't match our package.json
+*** refusing to automatically check out node_modules
+
+Options:
+
+ - tools/node-modules checkout --force # disable this check
+
+ - tools/node-modules install # npm install with our package.json
+
+$0: *** aborting
+
+EOF
+ exit 1
+ fi
+ fi
+
+ # we're actually going to do this; let's remove the old one
+ cmd_remove
+
+ # and check out the new one
+ # we need to use the tag name here, unfortunately
+ clone_from_cache "${sha}"
+}
+
+cmd_install() {
+ test -e bots || test/common/make-bots
+
+ # We first read the result directly into the cache, then we unpack it.
+ tree="$(bots/npm download < package.json | tar_to_cache)"
+ commit="$(sha256sum package.json | git_cache commit-tree "${tree}")"
+ git_cache tag "sha-${commit}" "${commit}"
+ cmd_checkout "${commit}"
+
+ cat <<EOF
+Next steps:
+
+ - git add node_modules && git commit
+ - tools/node-modules push
+
+EOF
+}
+
+cmd_push() {
+ # push via the cache: the shared history with the remote helps to thin out the pack we send
+ tag="sha-$(git -C node_modules rev-parse HEAD)"
+ message PUSH "${GITHUB_REPO} ${tag}"
+ git_cache push "${SSH_REMOTE}" "${tag}"
+}
+
+cmd_verify() {
+ test -e bots || test/common/make-bots
+
+ # Verifies that the package.json and node_modules of the given commit match.
+ commit="$(git rev-parse "$1:node_modules")"
+ fetch_sha_to_cache "${commit}"
+
+ committed_tree="$(git_cache rev-parse "${commit}^{tree}")"
+ expected_tree="$(git cat-file blob "$1:package.json" | bots/npm download | tar_to_cache)"
+
+ if [ "${committed_tree}" != "${expected_tree}" ]; then
+ exec >&2
+ printf "\nCommit %s package.json and node_modules aren't in sync!\n\n" "$1"
+ git --no-pager show --stat "$1"
+
+ printf "\nThe above commit refers to the following node_modules commit:\n\n"
+ git_cache --no-pager show --no-patch "${commit}"
+
+ printf "\nOur attempt to recreate that commit differs as follows:\n\n"
+ git_cache --no-pager diff --stat "${commit}" "${expected_tree}" --
+ git_cache --no-pager diff "${commit}" "${expected_tree}" -- .package-lock.json
+ exit 1
+ fi
+}
+
+# called from Makefile.am
+cmd_make_package_lock_json() {
+ # Run from make to ensure package-lock.json is up to date
+
+ # package-lock.json is used as the stamp file for all things that use
+ # node_modules, so this is the main bit of glue that drives the entire process
+
+ # We try our best not to touch package-lock.json unless it actually changes
+
+ # This isn't going to work for a tarball, but as long as
+ # package-lock.json is already there, and newer than package.json,
+ # we're OK
+ if [ ! -e .git ]; then
+ if [ package-lock.json -nt package.json ]; then
+ exit 0
+ fi
+
+ echo "*** Can't update node modules unless running from git" >&2
+ exit 1
+ fi
+
+ # Otherwise, our main goal is to ensure that the node_modules from
+ # the index is the one that we actually have.
+ local sha
+ sha="$(get_index_gitlink node_modules)"
+ if [ ! -e node_modules/.git ]; then
+ # nothing there yet...
+ cmd_checkout
+ elif [ "$(git -C node_modules rev-parse HEAD)" != "${sha}" ]; then
+ # wrong thing there...
+ cmd_checkout
+ fi
+
+ # This check is more about catching local changes to package.json than
+ # about validating something we just checked out:
+ if ! cmp -s node_modules/.package.json package.json; then
+ cat 2>&1 <<EOF
+*** package.json is out of sync with node_modules
+*** If you modified package.json, please run:
+***
+*** tools/node-modules install
+***
+*** and add the result to the index.
+EOF
+ exit 1
+ fi
+
+ # Only copy the package-lock.json if it differs from the one we have
+ if ! cmp -s node_modules/.package-lock.json package-lock.json; then
+ message COPY package-lock.json
+ cp node_modules/.package-lock.json package-lock.json
+ fi
+
+ # We're now in a situation where:
+ # - the checked out node_modules is equal to the gitlink in the index
+ # - the package.json in the tree is equal to the one in node_modules
+ # - ditto package-lock.json
+ exit 0
+}
+
+main() {
+ if [ $# = 0 ]; then
+ # don't list the "private" ones
+ echo 'This command requires a subcommand: remove checkout install push verify'
+ exit 1
+ fi
+
+ local fname
+ fname="$(printf 'cmd_%s' "$1" | tr '-' '_')"
+ if ! type -t "${fname}" | grep -q function; then
+ echo "Unknown subcommand '$1'"
+ exit 1
+ fi
+
+ shift
+ [ -n "${quiet}" ] || set -x
+ "${fname}" "$@"
+}
+
+main "$@"
diff --git a/tools/patch-metainfo b/tools/patch-metainfo
new file mode 100755
index 0000000..9e3f744
--- /dev/null
+++ b/tools/patch-metainfo
@@ -0,0 +1,35 @@
+#!/usr/bin/python3
+
+# Merges a downstream releases.xml file with the upstream metainfo.xml.
+#
+# Opens the metainfo file, finds the <releases> tag, and replaces its contents
+# with the contents of the <releases> tag found in the releases file.
+#
+# This facilitates keeping a downstream changelog, not shipped in tarballs.
+#
+# This substitution is meant to be done at `make install` time. Set the
+# `DOWNSTREAM_RELEASES_XML` environment variable while calling `make install`.
+
+import argparse
+import xml.etree.ElementTree as ET
+
+parser = argparse.ArgumentParser()
+parser.add_argument('metainfo')
+parser.add_argument('releases')
+args = parser.parse_args()
+
+releases = ET.parse(args.releases)
+releases_releases = releases.find('releases')
+assert releases_releases is not None
+
+metainfo = ET.parse(args.metainfo)
+metainfo_releases = metainfo.find('releases')
+assert metainfo_releases is not None
+
+stub_release = metainfo_releases.find('release')
+if stub_release is not None:
+ metainfo_releases.remove(stub_release)
+
+metainfo_releases.extend(releases_releases)
+
+metainfo.write(args.metainfo, encoding='utf-8', xml_declaration=True)
diff --git a/tools/termschutz b/tools/termschutz
new file mode 100755
index 0000000..0c78f2d
--- /dev/null
+++ b/tools/termschutz
@@ -0,0 +1,29 @@
+#!/usr/bin/python3
+
+import argparse
+import os
+
+# Implement `command < /dev/null 2>&1 | cat >&2` with `command`
+# as the main process (for signals, return status, etc).
+
+# This protects us against node setting stdio non-blocking.
+
+# NB: as of today (node v14.17.0) node doesn't mess with the
+# controlling terminal.
+
+parser = argparse.ArgumentParser()
+parser.add_argument('cmd', nargs='+', help='The command to run')
+args = parser.parse_args()
+
+n = os.open('/dev/null', os.O_RDONLY)
+r, w = os.pipe()
+
+if os.fork():
+ os.dup2(n, 0) # stdin from /dev/null
+ os.dup2(w, 1) # stdout to cat
+ os.dup2(w, 2) # stderr to cat
+ os.execvp(args.cmd[0], args.cmd)
+else:
+ os.dup2(r, 0) # read from wrapped process
+ os.dup2(2, 1) # all output to stderr
+ os.execvp('cat', ['cat'])
diff --git a/tools/test-driver b/tools/test-driver
new file mode 100755
index 0000000..be73b80
--- /dev/null
+++ b/tools/test-driver
@@ -0,0 +1,153 @@
+#! /bin/sh
+# test-driver - basic testsuite driver script.
+
+scriptversion=2018-03-07.03; # UTC
+
+# Copyright (C) 2011-2021 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+# As a special exception to the GNU General Public License, if you
+# distribute this file as part of a program that contains a
+# configuration script generated by Autoconf, you may include it under
+# the same distribution terms that you use for the rest of that program.
+
+# This file is maintained in Automake, please report
+# bugs to <bug-automake@gnu.org> or send patches to
+# <automake-patches@gnu.org>.
+
+# Make unconditional expansion of undefined variables an error. This
+# helps a lot in preventing typo-related bugs.
+set -u
+
+usage_error ()
+{
+ echo "$0: $*" >&2
+ print_usage >&2
+ exit 2
+}
+
+print_usage ()
+{
+ cat <<END
+Usage:
+ test-driver --test-name NAME --log-file PATH --trs-file PATH
+ [--expect-failure {yes|no}] [--color-tests {yes|no}]
+ [--enable-hard-errors {yes|no}] [--]
+ TEST-SCRIPT [TEST-SCRIPT-ARGUMENTS]
+
+The '--test-name', '--log-file' and '--trs-file' options are mandatory.
+See the GNU Automake documentation for information.
+END
+}
+
+test_name= # Used for reporting.
+log_file= # Where to save the output of the test script.
+trs_file= # Where to save the metadata of the test run.
+expect_failure=no
+color_tests=no
+enable_hard_errors=yes
+while test $# -gt 0; do
+ case $1 in
+ --help) print_usage; exit $?;;
+ --version) echo "test-driver $scriptversion"; exit $?;;
+ --test-name) test_name=$2; shift;;
+ --log-file) log_file=$2; shift;;
+ --trs-file) trs_file=$2; shift;;
+ --color-tests) color_tests=$2; shift;;
+ --expect-failure) expect_failure=$2; shift;;
+ --enable-hard-errors) enable_hard_errors=$2; shift;;
+ --) shift; break;;
+ -*) usage_error "invalid option: '$1'";;
+ *) break;;
+ esac
+ shift
+done
+
+missing_opts=
+test x"$test_name" = x && missing_opts="$missing_opts --test-name"
+test x"$log_file" = x && missing_opts="$missing_opts --log-file"
+test x"$trs_file" = x && missing_opts="$missing_opts --trs-file"
+if test x"$missing_opts" != x; then
+ usage_error "the following mandatory options are missing:$missing_opts"
+fi
+
+if test $# -eq 0; then
+ usage_error "missing argument"
+fi
+
+if test $color_tests = yes; then
+ # Keep this in sync with 'lib/am/check.am:$(am__tty_colors)'.
+ red='' # Red.
+ grn='' # Green.
+ lgn='' # Light green.
+ blu='' # Blue.
+ mgn='' # Magenta.
+ std='' # No color.
+else
+ red= grn= lgn= blu= mgn= std=
+fi
+
+do_exit='rm -f $log_file $trs_file; (exit $st); exit $st'
+trap "st=129; $do_exit" 1
+trap "st=130; $do_exit" 2
+trap "st=141; $do_exit" 13
+trap "st=143; $do_exit" 15
+
+# Test script is run here. We create the file first, then append to it,
+# to ameliorate tests themselves also writing to the log file. Our tests
+# don't, but others can (automake bug#35762).
+: >"$log_file"
+"$@" >>"$log_file" 2>&1
+estatus=$?
+
+if test $enable_hard_errors = no && test $estatus -eq 99; then
+ tweaked_estatus=1
+else
+ tweaked_estatus=$estatus
+fi
+
+case $tweaked_estatus:$expect_failure in
+ 0:yes) col=$red res=XPASS recheck=yes gcopy=yes;;
+ 0:*) col=$grn res=PASS recheck=no gcopy=no;;
+ 77:*) col=$blu res=SKIP recheck=no gcopy=yes;;
+ 99:*) col=$mgn res=ERROR recheck=yes gcopy=yes;;
+ *:yes) col=$lgn res=XFAIL recheck=no gcopy=yes;;
+ *:*) col=$red res=FAIL recheck=yes gcopy=yes;;
+esac
+
+# Report the test outcome and exit status in the logs, so that one can
+# know whether the test passed or failed simply by looking at the '.log'
+# file, without the need of also peaking into the corresponding '.trs'
+# file (automake bug#11814).
+echo "$res $test_name (exit status: $estatus)" >>"$log_file"
+
+# Report outcome to console.
+echo "${col}${res}${std}: $test_name"
+
+# Register the test result, and other relevant metadata.
+echo ":test-result: $res" > $trs_file
+echo ":global-test-result: $res" >> $trs_file
+echo ":recheck: $recheck" >> $trs_file
+echo ":copy-in-global-log: $gcopy" >> $trs_file
+
+# Local Variables:
+# mode: shell-script
+# sh-indentation: 2
+# eval: (add-hook 'before-save-hook 'time-stamp)
+# time-stamp-start: "scriptversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-time-zone: "UTC0"
+# time-stamp-end: "; # UTC"
+# End:
diff --git a/tools/urls-check b/tools/urls-check
new file mode 100755
index 0000000..6c36094
--- /dev/null
+++ b/tools/urls-check
@@ -0,0 +1,144 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../test/common/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2020 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import fnmatch
+import os
+import subprocess
+import sys
+import time
+import urllib
+import urllib.parse
+from urllib.request import Request, urlopen
+
+import task
+
+BASE_DIR = os.path.realpath(f'{__file__}/../..')
+
+DAYS = 7
+TASK_NAME = "Validate all URLs"
+
+USER_AGENT = "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0"
+
+KNOWN_REDIRECTS = [
+ # fnmatch-like
+ "https://access.redhat.com/security/updates/classification/#",
+ "https://firefox.com/",
+ "https://www.microsoft.com/",
+ "https://github.com/patternfly/*"
+]
+
+
+def main():
+ parser = argparse.ArgumentParser(description=TASK_NAME)
+ parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
+ parser.add_argument('-n', '--dry-run', action="store_true",
+ help="Only show urls")
+ opts = parser.parse_args()
+
+ task.verbose = opts.verbose
+
+ if opts.dry_run:
+ (success, err) = check_urls(opts.verbose)
+ if err:
+ print(err)
+ sys.exit(1 if err else 0)
+
+ since = time.time() - (DAYS * 86400)
+
+ # When there is an open issue, don't do anything
+ issues = task.api.issues(state="open")
+ issues = [i for i in issues if i["title"] == TASK_NAME]
+ if issues:
+ return
+
+ issues = task.api.issues(state="closed", since=since)
+ issues = [i for i in issues if i["title"] == TASK_NAME]
+
+ # If related issue was not modified in last n-DAYS, then do your thing
+ if not issues:
+ (success, err) = check_urls(opts.verbose)
+ if err:
+ # Create a new issue
+ data = {
+ "title": TASK_NAME,
+ "body": err,
+ "labels": ["bot"]
+ }
+ task.api.post("issues", data)
+ else:
+ # Try to comment on the last issue (look in the last 4*DAYS)
+ # If there is not issue to be found, then just open a new one and close it
+ success = success or "Task hasn't produced any output"
+ since = time.time() - (DAYS * 4 * 86400)
+ issues = task.api.issues(state="closed", since=since)
+ issues = [i for i in issues if i["title"] == TASK_NAME]
+ if issues:
+ task.comment(issues[0], success)
+ else:
+ data = {
+ "title": TASK_NAME,
+ "body": success,
+ "labels": ["bot"]
+ }
+ new_issue = task.api.post("issues", data)
+ task.api.post("issues/{0}".format(new_issue["number"]), {"state": "closed"})
+
+
+def check_urls(verbose):
+ command = r'git grep -IEho "(https?)://[-a-zA-Z0-9@:%_\+.~#?&=/]+" -- pkg ":!*.svg" | sort -u'
+ urls = subprocess.check_output(command, shell=True, universal_newlines=True, cwd=BASE_DIR).split("\n")
+ redirects = []
+ failed = []
+ for url in urls:
+ if not url:
+ continue
+
+ if urllib.parse.urlparse(url).hostname.endswith(".example.com"): # Some tests use demo urls
+ continue
+
+ if verbose:
+ print(f"Checking: {url}")
+
+ try:
+ # Specify agent as some websites otherwise block requests
+ req = Request(url=url, headers={"User-Agent": USER_AGENT})
+ resp = urlopen(req)
+ if resp.geturl() != url and not any(fnmatch.fnmatch(url, pattern) for pattern in KNOWN_REDIRECTS):
+ redirects.append(url)
+ if resp.getcode() >= 400:
+ failed.append(url)
+ except urllib.error.URLError:
+ failed.append(url)
+
+ err = ""
+ success = ""
+ if failed:
+ err = f"Checked {len(urls)} URLs out of which {len(failed)} is/are invalid:\n"
+ err += ''.join(f' {url}\n' for url in failed)
+ else:
+ success = f"Checked {len(urls)} URLs and all are valid."
+ if redirects:
+ success += "\nFollowing URLs are redirected:\n"
+ success += ''.join(f' {url}\n' for url in redirects)
+ return success, err
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tools/vulture-suppressions/cockpit.py b/tools/vulture-suppressions/cockpit.py
new file mode 100644
index 0000000..9e4aef3
--- /dev/null
+++ b/tools/vulture-suppressions/cockpit.py
@@ -0,0 +1,16 @@
+from cockpit.internal_endpoints import User
+from cockpit.misc.print import Printer
+
+# D-Bus properties
+User.full
+User.groups
+User.home
+User.id
+User.shell
+
+# getattr()
+Printer.dbus_call
+Printer.fsinfo
+Printer.help
+Printer.packages_reload
+Printer.wait
diff --git a/tools/vulture-suppressions/ferny.py b/tools/vulture-suppressions/ferny.py
new file mode 100644
index 0000000..1855208
--- /dev/null
+++ b/tools/vulture-suppressions/ferny.py
@@ -0,0 +1,4 @@
+from cockpit._vendor.ferny import InteractionHandler
+
+InteractionHandler.commands
+InteractionHandler.run_command
diff --git a/tools/vulture-suppressions/pytest.py b/tools/vulture-suppressions/pytest.py
new file mode 100644
index 0000000..9890506
--- /dev/null
+++ b/tools/vulture-suppressions/pytest.py
@@ -0,0 +1,3 @@
+import pytest
+
+pytest.Module._obj # type: ignore[attr-defined]
diff --git a/tools/vulture-suppressions/ruff.toml b/tools/vulture-suppressions/ruff.toml
new file mode 100644
index 0000000..59d605b
--- /dev/null
+++ b/tools/vulture-suppressions/ruff.toml
@@ -0,0 +1,6 @@
+extend = "../../pyproject.toml"
+
+[lint]
+ignore = [
+ "B018" # Found useless expression. Either assign it to a variable or remove it.
+]
diff --git a/tools/vulture-suppressions/stdlib.py b/tools/vulture-suppressions/stdlib.py
new file mode 100644
index 0000000..d7f30c1
--- /dev/null
+++ b/tools/vulture-suppressions/stdlib.py
@@ -0,0 +1,22 @@
+import asyncio
+import ssl
+import tempfile
+import unittest
+import xmlrpc.server
+
+asyncio.BaseTransport.get_protocol
+asyncio.BaseTransport.set_protocol
+asyncio.ReadTransport.is_reading
+asyncio.SubprocessTransport.get_pipe_transport
+asyncio.WriteTransport.get_write_buffer_limits
+asyncio.WriteTransport.get_write_buffer_size
+asyncio.WriteTransport.set_write_buffer_limits
+
+ssl.create_default_context().check_hostname
+ssl.create_default_context().verify_mode
+
+unittest.IsolatedAsyncioTestCase.asyncTearDown
+
+tempfile.TemporaryDirectory._rmtree # type: ignore[attr-defined]
+
+xmlrpc.server.SimpleXMLRPCRequestHandler.rpc_paths
diff --git a/tools/vulture-suppressions/testlib.py b/tools/vulture-suppressions/testlib.py
new file mode 100644
index 0000000..98ff8f3
--- /dev/null
+++ b/tools/vulture-suppressions/testlib.py
@@ -0,0 +1,8 @@
+from .testlib import Browser
+
+# used in cockpit-machines, cockpit-podman, cockpit-certificates
+Browser.get_checked
+
+# kept as being potentially useful in the future
+Browser.upload_file
+Browser.wait_attr_not_contains
diff --git a/tools/webpack-make.js b/tools/webpack-make.js
new file mode 100755
index 0000000..2c629f8
--- /dev/null
+++ b/tools/webpack-make.js
@@ -0,0 +1,3 @@
+#!/bin/env node
+console.error("This got renamed to ./build.js. Please see HACKING.md");
+process.exit(1);
diff --git a/tools/webpack-watch b/tools/webpack-watch
new file mode 100755
index 0000000..2c629f8
--- /dev/null
+++ b/tools/webpack-watch
@@ -0,0 +1,3 @@
+#!/bin/env node
+console.error("This got renamed to ./build.js. Please see HACKING.md");
+process.exit(1);
diff --git a/version.m4 b/version.m4
new file mode 100644
index 0000000..8df3ed2
--- /dev/null
+++ b/version.m4
@@ -0,0 +1 @@
+m4_define(VERSION_NUMBER, [311])